From ca6f2b467ded88c0b0a8ac662f1e75145394f0a3 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 24 Apr 2023 11:40:33 +0200 Subject: [PATCH 0001/1233] Add Fusion USD loader --- .../hosts/fusion/plugins/load/load_usd.py | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 openpype/hosts/fusion/plugins/load/load_usd.py diff --git a/openpype/hosts/fusion/plugins/load/load_usd.py b/openpype/hosts/fusion/plugins/load/load_usd.py new file mode 100644 index 0000000000..8c2c69f52f --- /dev/null +++ b/openpype/hosts/fusion/plugins/load/load_usd.py @@ -0,0 +1,73 @@ +from openpype.pipeline import ( + load, + get_representation_path, +) +from openpype.hosts.fusion.api import ( + imprint_container, + get_current_comp, + comp_lock_and_undo_chunk +) + + +class FusionLoadAlembicMesh(load.LoaderPlugin): + """Load USD into Fusion + + Support for USD was added since Fusion 18.5 + """ + + families = ["*"] + representations = ["*"] + extensions = {"usd", "usda", "usdz"} + + label = "Load USD" + order = -10 + icon = "code-fork" + color = "orange" + + tool_type = "uLoader" + + def load(self, context, name, namespace, data): + # Fallback to asset name when namespace is None + if namespace is None: + namespace = context['asset']['name'] + + # Create the Loader with the filename path set + comp = get_current_comp() + with comp_lock_and_undo_chunk(comp, "Create tool"): + + path = self.fname + + args = (-32768, -32768) + tool = comp.AddTool(self.tool_type, *args) + tool["Filename"] = path + + imprint_container(tool, + name=name, + namespace=namespace, + context=context, + loader=self.__class__.__name__) + + def switch(self, container, representation): + self.update(container, representation) + + def update(self, container, representation): + + tool = container["_tool"] + assert tool.ID == self.tool_type, f"Must be {self.tool_type}" + comp = tool.Comp() + + path = get_representation_path(representation) + + with comp_lock_and_undo_chunk(comp, "Update tool"): + tool["Filename"] = path + + # Update the imprinted representation + tool.SetData("avalon.representation", str(representation["_id"])) + + def remove(self, container): + tool = container["_tool"] + assert tool.ID == self.tool_type, f"Must be {self.tool_type}" + comp = tool.Comp() + + with comp_lock_and_undo_chunk(comp, "Remove tool"): + tool.Delete() From 00b68805069207584dc955883268b3bbcab1005e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A0=20Serra=20Arrizabalaga?= Date: Tue, 30 May 2023 13:06:13 +0200 Subject: [PATCH 0002/1233] Fix Nuke workfile template builder so representations get loaded next to each other for a single placeholder --- .../maya/api/workfile_template_builder.py | 2 +- .../nuke/api/workfile_template_builder.py | 8 +++-- .../workfile/workfile_template_builder.py | 29 ++++++++++++------- 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/maya/api/workfile_template_builder.py b/openpype/hosts/maya/api/workfile_template_builder.py index 6e6166c2ef..504db4dc06 100644 --- a/openpype/hosts/maya/api/workfile_template_builder.py +++ b/openpype/hosts/maya/api/workfile_template_builder.py @@ -250,7 +250,7 @@ class MayaPlaceholderLoadPlugin(PlaceholderPlugin, PlaceholderLoadMixin): def get_placeholder_options(self, options=None): return self.get_load_plugin_options(options) - def cleanup_placeholder(self, placeholder, failed): + def cleanup_placeholder(self, placeholder): """Hide placeholder, add them to placeholder set """ node = placeholder._scene_identifier diff --git a/openpype/hosts/nuke/api/workfile_template_builder.py b/openpype/hosts/nuke/api/workfile_template_builder.py index 72d4ffb476..3def140c92 100644 --- a/openpype/hosts/nuke/api/workfile_template_builder.py +++ b/openpype/hosts/nuke/api/workfile_template_builder.py @@ -156,8 +156,10 @@ class NukePlaceholderLoadPlugin(NukePlaceholderPlugin, PlaceholderLoadMixin): ) return loaded_representation_ids - def _before_repre_load(self, placeholder, representation): + def _before_placeholder_load(self, placeholder): placeholder.data["nodes_init"] = nuke.allNodes() + + def _before_repre_load(self, placeholder, representation): placeholder.data["last_repre_id"] = str(representation["_id"]) def collect_placeholders(self): @@ -189,7 +191,7 @@ class NukePlaceholderLoadPlugin(NukePlaceholderPlugin, PlaceholderLoadMixin): def get_placeholder_options(self, options=None): return self.get_load_plugin_options(options) - def cleanup_placeholder(self, placeholder, failed): + def cleanup_placeholder(self, placeholder): # deselect all selected nodes placeholder_node = nuke.toNode(placeholder.scene_identifier) @@ -603,7 +605,7 @@ class NukePlaceholderCreatePlugin( def get_placeholder_options(self, options=None): return self.get_create_plugin_options(options) - def cleanup_placeholder(self, placeholder, failed): + def cleanup_placeholder(self, placeholder): # deselect all selected nodes placeholder_node = nuke.toNode(placeholder.scene_identifier) diff --git a/openpype/pipeline/workfile/workfile_template_builder.py b/openpype/pipeline/workfile/workfile_template_builder.py index 896ed40f2d..a8a7ffec75 100644 --- a/openpype/pipeline/workfile/workfile_template_builder.py +++ b/openpype/pipeline/workfile/workfile_template_builder.py @@ -1457,8 +1457,15 @@ class PlaceholderLoadMixin(object): context_filters=context_filters )) + def _before_placeholder_load(self, placeholder): + """Can be overridden. It's called before placeholder representations + are loaded. + """ + + pass + def _before_repre_load(self, placeholder, representation): - """Can be overriden. Is called before representation is loaded.""" + """Can be overridden. It's called before representation is loaded.""" pass @@ -1491,7 +1498,7 @@ class PlaceholderLoadMixin(object): return output def populate_load_placeholder(self, placeholder, ignore_repre_ids=None): - """Load placeholder is goind to load matching representations. + """Load placeholder is going to load matching representations. Note: Ignore repre ids is to avoid loading the same representation again @@ -1513,7 +1520,7 @@ class PlaceholderLoadMixin(object): # TODO check loader existence loader_name = placeholder.data["loader"] - loader_args = placeholder.data["loader_args"] + loader_args = self.parse_loader_args(placeholder.data["loader_args"]) placeholder_representations = self._get_representations(placeholder) @@ -1535,6 +1542,9 @@ class PlaceholderLoadMixin(object): self.project_name, filtered_representations ) loaders_by_name = self.builder.get_loaders_by_name() + self._before_placeholder_load( + placeholder + ) for repre_load_context in repre_load_contexts.values(): representation = repre_load_context["representation"] repre_context = representation["context"] @@ -1547,24 +1557,24 @@ class PlaceholderLoadMixin(object): repre_context["subset"], repre_context["asset"], loader_name, - loader_args + placeholder.data["loader_args"], ) ) try: container = load_with_repre_context( loaders_by_name[loader_name], repre_load_context, - options=self.parse_loader_args(loader_args) + options=loader_args ) except Exception: - failed = True self.load_failed(placeholder, representation) else: - failed = False self.load_succeed(placeholder, container) - self.cleanup_placeholder(placeholder, failed) + + # Cleanup placeholder after load of all representations + self.cleanup_placeholder(placeholder) def load_failed(self, placeholder, representation): if hasattr(placeholder, "load_failed"): @@ -1574,7 +1584,7 @@ class PlaceholderLoadMixin(object): if hasattr(placeholder, "load_succeed"): placeholder.load_succeed(container) - def cleanup_placeholder(self, placeholder, failed): + def cleanup_placeholder(self, placeholder): """Cleanup placeholder after load of single representation. Can be called multiple times during placeholder item populating and is @@ -1583,7 +1593,6 @@ class PlaceholderLoadMixin(object): Args: placeholder (PlaceholderItem): Item which was just used to load representation. - failed (bool): Loading of representation failed. """ pass From c253bc11d0af425744384ee6a1cb39bc9a8c0277 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A0=20Serra=20Arrizabalaga?= Date: Tue, 30 May 2023 13:13:25 +0200 Subject: [PATCH 0003/1233] Fix docstring --- openpype/pipeline/workfile/workfile_template_builder.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/openpype/pipeline/workfile/workfile_template_builder.py b/openpype/pipeline/workfile/workfile_template_builder.py index a8a7ffec75..a081b66789 100644 --- a/openpype/pipeline/workfile/workfile_template_builder.py +++ b/openpype/pipeline/workfile/workfile_template_builder.py @@ -1585,10 +1585,7 @@ class PlaceholderLoadMixin(object): placeholder.load_succeed(container) def cleanup_placeholder(self, placeholder): - """Cleanup placeholder after load of single representation. - - Can be called multiple times during placeholder item populating and is - called even if loading failed. + """Cleanup placeholder after load of its corresponding representations. Args: placeholder (PlaceholderItem): Item which was just used to load From ac7e53a44a038df8c31ad1fbd4d49c8c90c3a95b Mon Sep 17 00:00:00 2001 From: Fabia Serra Arrizabalaga Date: Mon, 3 Jul 2023 16:55:53 -0500 Subject: [PATCH 0004/1233] Set some default `failed` value on the right scope --- openpype/pipeline/workfile/workfile_template_builder.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/workfile/workfile_template_builder.py b/openpype/pipeline/workfile/workfile_template_builder.py index 13fcd3693e..8853097b21 100644 --- a/openpype/pipeline/workfile/workfile_template_builder.py +++ b/openpype/pipeline/workfile/workfile_template_builder.py @@ -1547,6 +1547,8 @@ class PlaceholderLoadMixin(object): self._before_placeholder_load( placeholder ) + + failed = False for repre_load_context in repre_load_contexts.values(): representation = repre_load_context["representation"] repre_context = representation["context"] @@ -1571,9 +1573,10 @@ class PlaceholderLoadMixin(object): except Exception: self.load_failed(placeholder, representation) - + failed = True else: self.load_succeed(placeholder, container) + # Run post placeholder process after load of all representations self.post_placeholder_process(placeholder, failed) From 833d2fcb737aa6e8e561a927a93ee60de6225014 Mon Sep 17 00:00:00 2001 From: Fabia Serra Arrizabalaga Date: Mon, 3 Jul 2023 17:01:10 -0500 Subject: [PATCH 0005/1233] Fix docstrings --- .../hosts/maya/api/workfile_template_builder.py | 8 +++++++- .../hosts/nuke/api/workfile_template_builder.py | 14 ++++++++++++++ .../pipeline/workfile/workfile_template_builder.py | 10 ++-------- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/maya/api/workfile_template_builder.py b/openpype/hosts/maya/api/workfile_template_builder.py index e2f30f46d0..30113578c5 100644 --- a/openpype/hosts/maya/api/workfile_template_builder.py +++ b/openpype/hosts/maya/api/workfile_template_builder.py @@ -244,8 +244,14 @@ class MayaPlaceholderLoadPlugin(PlaceholderPlugin, PlaceholderLoadMixin): return self.get_load_plugin_options(options) def post_placeholder_process(self, placeholder, failed): - """Hide placeholder, add them to placeholder set + """Cleanup placeholder after load of its corresponding representations. + + Args: + placeholder (PlaceholderItem): Item which was just used to load + representation. + failed (bool): Loading of representation failed. """ + # Hide placeholder and add them to placeholder set node = placeholder.scene_identifier cmds.sets(node, addElement=PLACEHOLDER_SET) diff --git a/openpype/hosts/nuke/api/workfile_template_builder.py b/openpype/hosts/nuke/api/workfile_template_builder.py index 7fc1ff385b..672c5cb836 100644 --- a/openpype/hosts/nuke/api/workfile_template_builder.py +++ b/openpype/hosts/nuke/api/workfile_template_builder.py @@ -193,6 +193,13 @@ class NukePlaceholderLoadPlugin(NukePlaceholderPlugin, PlaceholderLoadMixin): return self.get_load_plugin_options(options) def post_placeholder_process(self, placeholder, failed): + """Cleanup placeholder after load of its corresponding representations. + + Args: + placeholder (PlaceholderItem): Item which was just used to load + representation. + failed (bool): Loading of representation failed. + """ # deselect all selected nodes placeholder_node = nuke.toNode(placeholder.scene_identifier) @@ -607,6 +614,13 @@ class NukePlaceholderCreatePlugin( return self.get_create_plugin_options(options) def post_placeholder_process(self, placeholder, failed): + """Cleanup placeholder after load of its corresponding representations. + + Args: + placeholder (PlaceholderItem): Item which was just used to load + representation. + failed (bool): Loading of representation failed. + """ # deselect all selected nodes placeholder_node = nuke.toNode(placeholder.scene_identifier) diff --git a/openpype/pipeline/workfile/workfile_template_builder.py b/openpype/pipeline/workfile/workfile_template_builder.py index 8853097b21..8ef83a4cdd 100644 --- a/openpype/pipeline/workfile/workfile_template_builder.py +++ b/openpype/pipeline/workfile/workfile_template_builder.py @@ -1598,10 +1598,7 @@ class PlaceholderLoadMixin(object): placeholder.load_succeed(container) def post_placeholder_process(self, placeholder, failed): - """Cleanup placeholder after load of single representation. - - Can be called multiple times during placeholder item populating and is - called even if loading failed. + """Cleanup placeholder after load of its corresponding representations. Args: placeholder (PlaceholderItem): Item which was just used to load @@ -1788,10 +1785,7 @@ class PlaceholderCreateMixin(object): placeholder.create_succeed(creator_instance) def post_placeholder_process(self, placeholder, failed): - """Cleanup placeholder after load of single representation. - - Can be called multiple times during placeholder item populating and is - called even if loading failed. + """Cleanup placeholder after load of its corresponding representations. Args: placeholder (PlaceholderItem): Item which was just used to load From 9847f879d827a56108348bae27985fd25316ffdc Mon Sep 17 00:00:00 2001 From: Fabia Serra Arrizabalaga Date: Mon, 3 Jul 2023 17:03:23 -0500 Subject: [PATCH 0006/1233] Revert to original docstring --- openpype/pipeline/workfile/workfile_template_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/workfile/workfile_template_builder.py b/openpype/pipeline/workfile/workfile_template_builder.py index 8ef83a4cdd..3b06ef059b 100644 --- a/openpype/pipeline/workfile/workfile_template_builder.py +++ b/openpype/pipeline/workfile/workfile_template_builder.py @@ -1603,7 +1603,7 @@ class PlaceholderLoadMixin(object): Args: placeholder (PlaceholderItem): Item which was just used to load representation. - failed (bool): True if loading failed. + failed (bool): Loading of representation failed. """ pass From f814db2ce0c4432023b18903c7819729997abfb5 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 6 Jul 2023 10:45:00 +0100 Subject: [PATCH 0007/1233] Use colorspace data when creating thumbnail. --- openpype/lib/transcoding.py | 13 +++-- openpype/plugins/publish/extract_thumbnail.py | 51 ++++++++++++------- 2 files changed, 42 insertions(+), 22 deletions(-) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index de6495900e..771f670f89 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -1056,7 +1056,8 @@ def convert_colorspace( view=None, display=None, additional_command_args=None, - logger=None + logger=None, + input_args=None ): """Convert source file from one color space to another. @@ -1084,13 +1085,17 @@ def convert_colorspace( if logger is None: logger = logging.getLogger(__name__) - oiio_cmd = [ - get_oiio_tools_path(), + oiio_cmd = [get_oiio_tools_path()] + + if input_args: + oiio_cmd.extend(input_args) + + oiio_cmd.extend([ input_path, # Don't add any additional attributes "--nosoftwareattrib", "--colorconfig", config_path - ] + ]) if all([target_colorspace, view, display]): raise ValueError("Colorspace and both screen and display" diff --git a/openpype/plugins/publish/extract_thumbnail.py b/openpype/plugins/publish/extract_thumbnail.py index b98ab64f56..1d86741470 100644 --- a/openpype/plugins/publish/extract_thumbnail.py +++ b/openpype/plugins/publish/extract_thumbnail.py @@ -10,6 +10,7 @@ from openpype.lib import ( run_subprocess, path_to_subprocess_arg, ) +from openpype.lib.transcoding import convert_colorspace class ExtractThumbnail(pyblish.api.InstancePlugin): @@ -98,8 +99,18 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): self.log.debug("Trying to convert with OIIO") # If the input can read by OIIO then use OIIO method for # conversion otherwise use ffmpeg + colorspace_data = repre["colorspaceData"] + source_colorspace = colorspace_data["colorspace"] + config_path = colorspace_data.get("config", {}).get("path") + display = colorspace_data["display"] + view = colorspace_data["view"] thumbnail_created = self.create_thumbnail_oiio( - full_input_path, full_output_path + full_input_path, + full_output_path, + config_path, + source_colorspace, + display, + view ) # Try to use FFMPEG if OIIO is not supported or for cases when @@ -172,24 +183,28 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): filtered_repres.append(repre) return filtered_repres - def create_thumbnail_oiio(self, src_path, dst_path): + def create_thumbnail_oiio( + self, + src_path, + dst_path, + config_path, + source_colorspace, + display, + view + ): self.log.info("Extracting thumbnail {}".format(dst_path)) - oiio_tool_path = get_oiio_tools_path() - oiio_cmd = [ - oiio_tool_path, - "-a", src_path, - "-o", dst_path - ] - self.log.debug("running: {}".format(" ".join(oiio_cmd))) - try: - run_subprocess(oiio_cmd, logger=self.log) - return True - except Exception: - self.log.warning( - "Failed to create thumbnail using oiiotool", - exc_info=True - ) - return False + + convert_colorspace( + src_path, + dst_path, + config_path, + source_colorspace, + view=view, + display=display, + input_args=["-i:ch=R,G,B"] + ) + + return dst_path def create_thumbnail_ffmpeg(self, src_path, dst_path): self.log.info("outputting {}".format(dst_path)) From 08d17e527443fab05731eb0428806568e8e297aa Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Thu, 20 Jul 2023 15:44:19 +0200 Subject: [PATCH 0008/1233] Added possibility to discard changes with comp_lock_and_undo_chunk() --- openpype/hosts/fusion/api/lib.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/fusion/api/lib.py b/openpype/hosts/fusion/api/lib.py index d96557571b..56705b1a40 100644 --- a/openpype/hosts/fusion/api/lib.py +++ b/openpype/hosts/fusion/api/lib.py @@ -354,7 +354,11 @@ def get_current_comp(): @contextlib.contextmanager -def comp_lock_and_undo_chunk(comp, undo_queue_name="Script CMD"): +def comp_lock_and_undo_chunk( + comp, + undo_queue_name="Script CMD", + keep_undo=True, +): """Lock comp and open an undo chunk during the context""" try: comp.Lock() @@ -362,4 +366,4 @@ def comp_lock_and_undo_chunk(comp, undo_queue_name="Script CMD"): yield finally: comp.Unlock() - comp.EndUndo() + comp.EndUndo(keep_undo) From 6a3729c22923ad76b205a8f750501c7b6c6e7336 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Thu, 20 Jul 2023 15:44:38 +0200 Subject: [PATCH 0009/1233] Add resolution validator --- .../publish/validator_saver_resolution.py | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 openpype/hosts/fusion/plugins/publish/validator_saver_resolution.py diff --git a/openpype/hosts/fusion/plugins/publish/validator_saver_resolution.py b/openpype/hosts/fusion/plugins/publish/validator_saver_resolution.py new file mode 100644 index 0000000000..43e2ea5093 --- /dev/null +++ b/openpype/hosts/fusion/plugins/publish/validator_saver_resolution.py @@ -0,0 +1,96 @@ +import pyblish.api +from openpype.pipeline import ( + PublishValidationError, + OptionalPyblishPluginMixin, +) + +from openpype.hosts.fusion.api.action import SelectInvalidAction +from openpype.hosts.fusion.api import ( + get_current_comp, + comp_lock_and_undo_chunk, +) + + +class ValidateSaverResolution( + pyblish.api.InstancePlugin, OptionalPyblishPluginMixin +): + """Validate that the saver input resolution matches the projects""" + + order = pyblish.api.ValidatorOrder + label = "Validate Saver Resolution" + families = ["render"] + hosts = ["fusion"] + optional = True + actions = [SelectInvalidAction] + + @classmethod + def get_invalid(cls, instance, wrong_resolution=None): + """Validate if the saver rescive the expected resolution""" + if wrong_resolution is None: + wrong_resolution = [] + + saver = instance[0] + firstFrame = saver.GetAttrs("TOOLNT_Region_Start")[1] + comp = get_current_comp() + + # If the current saver hasn't bin rendered its input resolution + # hasn't bin saved. To combat this, add an expression in + # the comments field to read the resolution + + # False undo removes the undo-stack from the undo list + with comp_lock_and_undo_chunk(comp, "Read resolution", False): + # Save old comment + oldComment = "" + hasExpression = False + if saver["Comments"][firstFrame] != "": + if saver["Comments"].GetExpression() is not None: + hasExpression = True + oldComment = saver["Comments"].GetExpression() + saver["Comments"].SetExpression(None) + else: + oldComment = saver["Comments"][firstFrame] + saver["Comments"][firstFrame] = "" + + # Get input width + saver["Comments"].SetExpression("self.Input.OriginalWidth") + width = int(saver["Comments"][firstFrame]) + + # Get input height + saver["Comments"].SetExpression("self.Input.OriginalHeight") + height = int(saver["Comments"][firstFrame]) + + # Reset old comment + saver["Comments"].SetExpression(None) + if hasExpression: + saver["Comments"].SetExpression(oldComment) + else: + saver["Comments"][firstFrame] = oldComment + + # Time to compare! + wrong_resolution.append("{}x{}".format(width, height)) + entityData = instance.data["assetEntity"]["data"] + if entityData["resolutionWidth"] != width: + return [saver] + if entityData["resolutionHeight"] != height: + return [saver] + + return [] + + def process(self, instance): + if not self.is_active(instance.data): + return + + wrong_resolution = [] + invalid = self.get_invalid(instance, wrong_resolution) + if invalid: + entityData = instance.data["assetEntity"]["data"] + raise PublishValidationError( + "The input's resolution does not match" + " the asset's resolution of {}x{}.\n\n" + "The input's resolution is {}".format( + entityData["resolutionWidth"], + entityData["resolutionHeight"], + wrong_resolution[0], + ), + title=self.label, + ) From 644897a89d81a5be31371bde9d03a81b92e6b8f8 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Thu, 20 Jul 2023 15:44:50 +0200 Subject: [PATCH 0010/1233] Black formatting --- openpype/hosts/fusion/api/lib.py | 106 +++++++++++++++++++------------ 1 file changed, 67 insertions(+), 39 deletions(-) diff --git a/openpype/hosts/fusion/api/lib.py b/openpype/hosts/fusion/api/lib.py index 56705b1a40..19db484856 100644 --- a/openpype/hosts/fusion/api/lib.py +++ b/openpype/hosts/fusion/api/lib.py @@ -22,8 +22,14 @@ self = sys.modules[__name__] self._project = None -def update_frame_range(start, end, comp=None, set_render_range=True, - handle_start=0, handle_end=0): +def update_frame_range( + start, + end, + comp=None, + set_render_range=True, + handle_start=0, + handle_end=0, +): """Set Fusion comp's start and end frame range Args: @@ -49,15 +55,17 @@ def update_frame_range(start, end, comp=None, set_render_range=True, attrs = { "COMPN_GlobalStart": start - handle_start, - "COMPN_GlobalEnd": end + handle_end + "COMPN_GlobalEnd": end + handle_end, } # set frame range if set_render_range: - attrs.update({ - "COMPN_RenderStart": start, - "COMPN_RenderEnd": end - }) + attrs.update( + { + "COMPN_RenderStart": start, + "COMPN_RenderEnd": end, + } + ) with comp_lock_and_undo_chunk(comp): comp.SetAttrs(attrs) @@ -70,9 +78,13 @@ def set_asset_framerange(): end = asset_doc["data"]["frameEnd"] handle_start = asset_doc["data"]["handleStart"] handle_end = asset_doc["data"]["handleEnd"] - update_frame_range(start, end, set_render_range=True, - handle_start=handle_start, - handle_end=handle_end) + update_frame_range( + start, + end, + set_render_range=True, + handle_start=handle_start, + handle_end=handle_end, + ) def set_asset_resolution(): @@ -82,12 +94,15 @@ def set_asset_resolution(): height = asset_doc["data"]["resolutionHeight"] comp = get_current_comp() - print("Setting comp frame format resolution to {}x{}".format(width, - height)) - comp.SetPrefs({ - "Comp.FrameFormat.Width": width, - "Comp.FrameFormat.Height": height, - }) + print( + "Setting comp frame format resolution to {}x{}".format(width, height) + ) + comp.SetPrefs( + { + "Comp.FrameFormat.Width": width, + "Comp.FrameFormat.Height": height, + } + ) def validate_comp_prefs(comp=None, force_repair=False): @@ -108,7 +123,7 @@ def validate_comp_prefs(comp=None, force_repair=False): "data.fps", "data.resolutionWidth", "data.resolutionHeight", - "data.pixelAspect" + "data.pixelAspect", ] asset_doc = get_current_project_asset(fields=fields) asset_data = asset_doc["data"] @@ -125,7 +140,7 @@ def validate_comp_prefs(comp=None, force_repair=False): ("resolutionWidth", "Width", "Resolution Width"), ("resolutionHeight", "Height", "Resolution Height"), ("pixelAspectX", "AspectX", "Pixel Aspect Ratio X"), - ("pixelAspectY", "AspectY", "Pixel Aspect Ratio Y") + ("pixelAspectY", "AspectY", "Pixel Aspect Ratio Y"), ] invalid = [] @@ -133,9 +148,9 @@ def validate_comp_prefs(comp=None, force_repair=False): asset_value = asset_data[key] comp_value = comp_frame_format_prefs.get(comp_key) if asset_value != comp_value: - invalid_msg = "{} {} should be {}".format(label, - comp_value, - asset_value) + invalid_msg = "{} {} should be {}".format( + label, comp_value, asset_value + ) invalid.append(invalid_msg) if not force_repair: @@ -146,7 +161,8 @@ def validate_comp_prefs(comp=None, force_repair=False): pref=label, value=comp_value, asset_name=asset_doc["name"], - asset_value=asset_value) + asset_value=asset_value, + ) ) if invalid: @@ -167,6 +183,7 @@ def validate_comp_prefs(comp=None, force_repair=False): from . import menu from openpype.widgets import popup from openpype.style import load_stylesheet + dialog = popup.Popup(parent=menu.menu) dialog.setWindowTitle("Fusion comp has invalid configuration") @@ -181,10 +198,12 @@ def validate_comp_prefs(comp=None, force_repair=False): dialog.setStyleSheet(load_stylesheet()) -def switch_item(container, - asset_name=None, - subset_name=None, - representation_name=None): +def switch_item( + container, + asset_name=None, + subset_name=None, + representation_name=None, +): """Switch container asset, subset or representation of a container by name. It'll always switch to the latest version - of course a different @@ -211,7 +230,8 @@ def switch_item(container, repre_id = container["representation"] representation = get_representation_by_id(project_name, repre_id) repre_parent_docs = get_representation_parents( - project_name, representation) + project_name, representation + ) if repre_parent_docs: version, subset, asset, _ = repre_parent_docs else: @@ -228,14 +248,18 @@ def switch_item(container, # Find the new one asset = get_asset_by_name(project_name, asset_name, fields=["_id"]) - assert asset, ("Could not find asset in the database with the name " - "'%s'" % asset_name) + assert asset, ( + "Could not find asset in the database with the name " + "'%s'" % asset_name + ) subset = get_subset_by_name( project_name, subset_name, asset["_id"], fields=["_id"] ) - assert subset, ("Could not find subset in the database with the name " - "'%s'" % subset_name) + assert subset, ( + "Could not find subset in the database with the name " + "'%s'" % subset_name + ) version = get_last_version_by_subset_id( project_name, subset["_id"], fields=["_id"] @@ -247,8 +271,10 @@ def switch_item(container, representation = get_representation_by_name( project_name, representation_name, version["_id"] ) - assert representation, ("Could not find representation in the database " - "with the name '%s'" % representation_name) + assert representation, ( + "Could not find representation in the database " + "with the name '%s'" % representation_name + ) switch_container(container, representation) @@ -273,11 +299,13 @@ def maintained_selection(comp=None): @contextlib.contextmanager -def maintained_comp_range(comp=None, - global_start=True, - global_end=True, - render_start=True, - render_end=True): +def maintained_comp_range( + comp=None, + global_start=True, + global_end=True, + render_start=True, + render_end=True, +): """Reset comp frame ranges from before the context after the context""" if comp is None: comp = get_current_comp() @@ -321,7 +349,7 @@ def get_frame_path(path): filename, ext = os.path.splitext(path) # Find a final number group - match = re.match('.*?([0-9]+)$', filename) + match = re.match(".*?([0-9]+)$", filename) if match: padding = len(match.group(1)) # remove number from end since fusion From 0cd2095af326a480d5d9dacfb409e7df3d8b412a Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Thu, 20 Jul 2023 17:08:30 +0200 Subject: [PATCH 0011/1233] Added MapPath and ReverseMapPath to all Fusion paths --- .../hosts/fusion/plugins/create/create_saver.py | 3 ++- openpype/hosts/fusion/plugins/load/load_sequence.py | 5 +++-- .../hosts/fusion/plugins/publish/collect_render.py | 13 +++++++++---- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index 04898d0a45..9a3640b176 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -166,7 +166,8 @@ class CreateSaver(NewCreator): filepath = self.temp_rendering_path_template.format( **formatting_data) - tool["Clip"] = os.path.normpath(filepath) + comp = get_current_comp() + tool["Clip"] = comp.ReverseMapPath(os.path.normpath(filepath)) # Rename tool if tool.Name != subset: diff --git a/openpype/hosts/fusion/plugins/load/load_sequence.py b/openpype/hosts/fusion/plugins/load/load_sequence.py index 20be5faaba..fde5b27e70 100644 --- a/openpype/hosts/fusion/plugins/load/load_sequence.py +++ b/openpype/hosts/fusion/plugins/load/load_sequence.py @@ -161,7 +161,8 @@ class FusionLoadSequence(load.LoaderPlugin): with comp_lock_and_undo_chunk(comp, "Create Loader"): args = (-32768, -32768) tool = comp.AddTool("Loader", *args) - tool["Clip"] = path + comp = get_current_comp() + tool["Clip"] = comp.ReverseMapPath(path) # Set global in point to start frame (if in version.data) start = self._get_start(context["version"], tool) @@ -244,7 +245,7 @@ class FusionLoadSequence(load.LoaderPlugin): "TimeCodeOffset", ), ): - tool["Clip"] = path + tool["Clip"] = comp.ReverseMapPath(path) # Set the global in to the start frame of the sequence global_in_changed = loader_shift(tool, start, relative=False) diff --git a/openpype/hosts/fusion/plugins/publish/collect_render.py b/openpype/hosts/fusion/plugins/publish/collect_render.py index a20a142701..62dd295e59 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_render.py +++ b/openpype/hosts/fusion/plugins/publish/collect_render.py @@ -4,7 +4,10 @@ import pyblish.api from openpype.pipeline import publish from openpype.pipeline.publish import RenderInstance -from openpype.hosts.fusion.api.lib import get_frame_path +from openpype.hosts.fusion.api.lib import ( + get_frame_path, + get_current_comp, +) @attr.s @@ -146,9 +149,11 @@ class CollectFusionRender( start = render_instance.frameStart - render_instance.handleStart end = render_instance.frameEnd + render_instance.handleEnd - path = ( - render_instance.tool["Clip"] - [render_instance.workfileComp.TIME_UNDEFINED] + comp = get_current_comp() + path = comp.MapPath( + render_instance.tool["Clip"][ + render_instance.workfileComp.TIME_UNDEFINED + ] ) output_dir = os.path.dirname(path) render_instance.outputDir = output_dir From 811f54763e9d3b5642bc3cc9c66b4b322c1913bf Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Thu, 20 Jul 2023 17:08:42 +0200 Subject: [PATCH 0012/1233] Black formatting --- .../fusion/plugins/create/create_saver.py | 72 ++++++++----------- .../fusion/plugins/publish/collect_render.py | 12 ++-- 2 files changed, 32 insertions(+), 52 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index 9a3640b176..42d96ab82f 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -14,7 +14,7 @@ from openpype.pipeline import ( legacy_io, Creator as NewCreator, CreatedInstance, - Anatomy + Anatomy, ) @@ -27,28 +27,20 @@ class CreateSaver(NewCreator): description = "Fusion Saver to generate image sequence" icon = "fa5.eye" - instance_attributes = [ - "reviewable" - ] - default_variants = [ - "Main", - "Mask" - ] + instance_attributes = ["reviewable"] + default_variants = ["Main", "Mask"] # TODO: This should be renamed together with Nuke so it is aligned temp_rendering_path_template = ( - "{workdir}/renders/fusion/{subset}/{subset}.{frame}.{ext}") + "{workdir}/renders/fusion/{subset}/{subset}.{frame}.{ext}" + ) def create(self, subset_name, instance_data, pre_create_data): - self.pass_pre_attributes_to_instance( - instance_data, - pre_create_data - ) + self.pass_pre_attributes_to_instance(instance_data, pre_create_data) - instance_data.update({ - "id": "pyblish.avalon.instance", - "subset": subset_name - }) + instance_data.update( + {"id": "pyblish.avalon.instance", "subset": subset_name} + ) # TODO: Add pre_create attributes to choose file format? file_format = "OpenEXRFormat" @@ -156,15 +148,12 @@ class CreateSaver(NewCreator): # Subset change detected workdir = os.path.normpath(legacy_io.Session["AVALON_WORKDIR"]) - formatting_data.update({ - "workdir": workdir, - "frame": "0" * frame_padding, - "ext": "exr" - }) + formatting_data.update( + {"workdir": workdir, "frame": "0" * frame_padding, "ext": "exr"} + ) # build file path to render - filepath = self.temp_rendering_path_template.format( - **formatting_data) + filepath = self.temp_rendering_path_template.format(**formatting_data) comp = get_current_comp() tool["Clip"] = comp.ReverseMapPath(os.path.normpath(filepath)) @@ -200,7 +189,7 @@ class CreateSaver(NewCreator): attr_defs = [ self._get_render_target_enum(), self._get_reviewable_bool(), - self._get_frame_range_enum() + self._get_frame_range_enum(), ] return attr_defs @@ -208,11 +197,7 @@ class CreateSaver(NewCreator): """Settings for publish page""" return self.get_pre_create_attr_defs() - def pass_pre_attributes_to_instance( - self, - instance_data, - pre_create_data - ): + def pass_pre_attributes_to_instance(self, instance_data, pre_create_data): creator_attrs = instance_data["creator_attributes"] = {} for pass_key in pre_create_data.keys(): creator_attrs[pass_key] = pre_create_data[pass_key] @@ -235,13 +220,13 @@ class CreateSaver(NewCreator): frame_range_options = { "asset_db": "Current asset context", "render_range": "From render in/out", - "comp_range": "From composition timeline" + "comp_range": "From composition timeline", } return EnumDef( "frame_range_source", items=frame_range_options, - label="Frame range source" + label="Frame range source", ) def _get_reviewable_bool(self): @@ -251,23 +236,22 @@ class CreateSaver(NewCreator): label="Review", ) - def apply_settings( - self, - project_settings, - system_settings - ): + def apply_settings(self, project_settings, system_settings): """Method called on initialization of plugin to apply settings.""" # plugin settings - plugin_settings = ( - project_settings["fusion"]["create"][self.__class__.__name__] - ) + plugin_settings = project_settings["fusion"]["create"][ + self.__class__.__name__ + ] # individual attributes - self.instance_attributes = plugin_settings.get( - "instance_attributes") or self.instance_attributes - self.default_variants = plugin_settings.get( - "default_variants") or self.default_variants + self.instance_attributes = ( + plugin_settings.get("instance_attributes") + or self.instance_attributes + ) + self.default_variants = ( + plugin_settings.get("default_variants") or self.default_variants + ) self.temp_rendering_path_template = ( plugin_settings.get("temp_rendering_path_template") or self.temp_rendering_path_template diff --git a/openpype/hosts/fusion/plugins/publish/collect_render.py b/openpype/hosts/fusion/plugins/publish/collect_render.py index 62dd295e59..9e48cc000e 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_render.py +++ b/openpype/hosts/fusion/plugins/publish/collect_render.py @@ -25,16 +25,13 @@ class FusionRenderInstance(RenderInstance): class CollectFusionRender( - publish.AbstractCollectRender, - publish.ColormanagedPyblishPluginMixin + publish.AbstractCollectRender, publish.ColormanagedPyblishPluginMixin ): - order = pyblish.api.CollectorOrder + 0.09 label = "Collect Fusion Render" hosts = ["fusion"] def get_instances(self, context): - comp = context.data.get("currentComp") comp_frame_format_prefs = comp.GetPrefs("Comp.FrameFormat") aspect_x = comp_frame_format_prefs["AspectX"] @@ -74,7 +71,7 @@ class CollectFusionRender( asset=inst.data["asset"], task=task_name, attachTo=False, - setMembers='', + setMembers="", publish=True, name=subset_name, resolutionWidth=comp_frame_format_prefs.get("Width"), @@ -93,7 +90,7 @@ class CollectFusionRender( frameStep=1, fps=comp_frame_format_prefs.get("Rate"), app_version=comp.GetApp().Version, - publish_attributes=inst.data.get("publish_attributes", {}) + publish_attributes=inst.data.get("publish_attributes", {}), ) render_target = inst.data["creator_attributes"]["render_target"] @@ -166,8 +163,7 @@ class CollectFusionRender( for frame in range(start, end + 1): expected_files.append( os.path.join( - output_dir, - f"{head}{str(frame).zfill(padding)}{ext}" + output_dir, f"{head}{str(frame).zfill(padding)}{ext}" ) ) From 3841d298b2d1d3837a5a5e569cc120b3f87b9655 Mon Sep 17 00:00:00 2001 From: Fabia Serra Arrizabalaga Date: Mon, 7 Aug 2023 18:22:49 +0200 Subject: [PATCH 0013/1233] First draft at adding support in Houdini renders to split render in export + render tasks --- .../plugins/create/create_arnold_rop.py | 14 ++ .../plugins/create/create_mantra_rop.py | 11 + .../houdini/plugins/create/create_vray_rop.py | 15 ++ .../plugins/publish/collect_arnold_rop.py | 15 ++ .../plugins/publish/collect_mantra_rop.py | 15 ++ .../plugins/publish/collect_vray_rop.py | 16 ++ .../deadline/abstract_submit_deadline.py | 22 +- .../publish/submit_houdini_render_deadline.py | 205 ++++++++++++++++-- 8 files changed, 293 insertions(+), 20 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_arnold_rop.py b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py index ca516619f6..d19795d3a9 100644 --- a/openpype/hosts/houdini/plugins/create/create_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py @@ -14,6 +14,9 @@ class CreateArnoldRop(plugin.HoudiniCreator): # Default extension ext = "exr" + # Default to split export and render jobs + export_job = True + def create(self, subset_name, instance_data, pre_create_data): import hou @@ -49,6 +52,14 @@ class CreateArnoldRop(plugin.HoudiniCreator): "ar_exr_half_precision": 1 # half precision } + if pre_create_data.get("export_job"): + ass_filepath = "{export_dir}{subset_name}/{subset_name}.$F4.ass".format( + export_dir=hou.text.expandString("$HIP/pyblish/ass/"), + subset_name=subset_name, + ) + parms["ar_ass_export_enable"] = 1 + parms["ar_ass_file"] = ass_filepath + instance_node.setParms(parms) # Lock any parameters in this list @@ -67,6 +78,9 @@ class CreateArnoldRop(plugin.HoudiniCreator): BoolDef("farm", label="Submitting to Farm", default=True), + BoolDef("export_job", + label="Split export and render jobs", + default=self.export_job), EnumDef("image_format", image_format_enum, default=self.ext, diff --git a/openpype/hosts/houdini/plugins/create/create_mantra_rop.py b/openpype/hosts/houdini/plugins/create/create_mantra_rop.py index 5c29adb33f..41311a1e30 100644 --- a/openpype/hosts/houdini/plugins/create/create_mantra_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_mantra_rop.py @@ -13,6 +13,9 @@ class CreateMantraROP(plugin.HoudiniCreator): icon = "magic" defaults = ["master"] + # Default to split export and render jobs + export_job = True + def create(self, subset_name, instance_data, pre_create_data): import hou # noqa @@ -45,6 +48,14 @@ class CreateMantraROP(plugin.HoudiniCreator): "vm_picture": filepath, } + if pre_create_data.get("export_job"): + ifd_filepath = "{export_dir}{subset_name}/{subset_name}.$F4.ifd".format( + export_dir=hou.text.expandString("$HIP/pyblish/ifd/"), + subset_name=subset_name, + ) + parms["soho_outputmode"] = 1 + parms["soho_diskfile"] = ifd_filepath + if self.selected_nodes: # If camera found in selection # we will use as render camera diff --git a/openpype/hosts/houdini/plugins/create/create_vray_rop.py b/openpype/hosts/houdini/plugins/create/create_vray_rop.py index 58748d4c34..29fab0c60c 100644 --- a/openpype/hosts/houdini/plugins/create/create_vray_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_vray_rop.py @@ -18,6 +18,9 @@ class CreateVrayROP(plugin.HoudiniCreator): ext = "exr" + # Default to split export and render jobs + export_job = True + def create(self, subset_name, instance_data, pre_create_data): instance_data.pop("active", None) @@ -54,6 +57,15 @@ class CreateVrayROP(plugin.HoudiniCreator): "SettingsEXR_bits_per_channel": "16" # half precision } + if pre_create_data.get("export_job"): + scene_filepath = "{export_dir}{subset_name}/{subset_name}.$F4.vrscene".format( + export_dir=hou.text.expandString("$HIP/pyblish/vrscene/"), + subset_name=subset_name, + ) + # TODO: don't have VRay to check the names of these + parms["render_export_mode"] = 1 + parms["render_export_filepath"] = scene_filepath + if self.selected_nodes: # set up the render camera from the selected node camera = None @@ -142,6 +154,9 @@ class CreateVrayROP(plugin.HoudiniCreator): BoolDef("farm", label="Submitting to Farm", default=True), + BoolDef("export_job", + label="Split export and render jobs", + default=self.export_job), EnumDef("image_format", image_format_enum, default=self.ext, diff --git a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py index 43b8428c60..063dd728e1 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py @@ -38,6 +38,21 @@ class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin): default_prefix = evalParmNoFrame(rop, "ar_picture") render_products = [] + # Store whether we are splitting the render job in an export + render + export_job = bool(rop.parm("ar_ass_export_enable").eval()) + instance.data["exportJob"] = export_job + export_prefix = None + export_products = [] + if export_job: + export_prefix = evalParmNoFrame(rop, "ar_ass_file", pad_character="0") + beauty_export_product = self.get_render_product_name( + prefix=export_prefix, + suffix=None) + export_products.append(beauty_export_product) + self.log.debug("Found export product: {}".format(beauty_export_product)) + instance.data["ifdFile"] = beauty_export_product + instance.data["exportFiles"] = list(export_products) + # Default beauty AOV beauty_product = self.get_render_product_name(prefix=default_prefix, suffix=None) diff --git a/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py b/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py index c4460f5350..b235e2c110 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py @@ -42,6 +42,21 @@ class CollectMantraROPRenderProducts(pyblish.api.InstancePlugin): default_prefix = evalParmNoFrame(rop, "vm_picture") render_products = [] + # Store whether we are splitting the render job in an export + render + export_job = bool(rop.parm("soho_outputmode").eval()) + instance.data["exportJob"] = export_job + export_prefix = None + export_products = [] + if export_job: + export_prefix = evalParmNoFrame(rop, "soho_diskfile", pad_character="0") + beauty_export_product = self.get_render_product_name( + prefix=export_prefix, + suffix=None) + export_products.append(beauty_export_product) + self.log.debug("Found export product: {}".format(beauty_export_product)) + instance.data["ifdFile"] = beauty_export_product + instance.data["exportFiles"] = list(export_products) + # Default beauty AOV beauty_product = self.get_render_product_name( prefix=default_prefix, suffix=None diff --git a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py index d4fe37f993..89a7c8bf72 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py @@ -43,6 +43,22 @@ class CollectVrayROPRenderProducts(pyblish.api.InstancePlugin): render_products = [] # TODO: add render elements if render element + # Store whether we are splitting the render job in an export + render + # TODO: check names of VRay parms + export_job = bool(rop.parm("render_export_mode").eval()) + instance.data["exportJob"] = export_job + export_prefix = None + export_products = [] + if export_job: + export_prefix = evalParmNoFrame(rop, "render_export_filepath", pad_character="0") + beauty_export_product = self.get_render_product_name( + prefix=export_prefix, + suffix=None) + export_products.append(beauty_export_product) + self.log.debug("Found export product: {}".format(beauty_export_product)) + instance.data["ifdFile"] = beauty_export_product + instance.data["exportFiles"] = list(export_products) + beauty_product = self.get_beauty_render_product(default_prefix) render_products.append(beauty_product) files_by_aov = { diff --git a/openpype/modules/deadline/abstract_submit_deadline.py b/openpype/modules/deadline/abstract_submit_deadline.py index 3fa427204b..56d8afbe3e 100644 --- a/openpype/modules/deadline/abstract_submit_deadline.py +++ b/openpype/modules/deadline/abstract_submit_deadline.py @@ -446,11 +446,29 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin, self.scene_path = file_path self.log.info("Using {} for render/export.".format(file_path)) - self.job_info = self.get_job_info() + # Check whether we are splitting render job in export + render + # and if so, create a separate task for the render + # TODO: Find a cleaner way so `get_job_info` can take extra args depending + # on host + export_job = instance.data["exportJob"] + self.job_info = self.get_job_info(split_render_job=export_job, export_job=True) self.plugin_info = self.get_plugin_info() self.aux_files = self.get_aux_files() - self.process_submission() + job_id = self.process_submission() + if export_job: + self.log.info("Splitting export and render in two jobs") + self.log.info("Export job id: %s", job_id) + render_job_info = self.get_job_info( + split_render_job=True, dependency_job_ids=[job_id] + ) + render_plugin_info = self.get_plugin_info(split_render_job=True) + payload = self.assemble_payload( + job_info=render_job_info, + plugin_info=render_plugin_info + ) + render_job_id = self.submit(payload) + self.log.info("Render job id: %s", render_job_id) def process_submission(self): """Process data for submission. 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 af341ca8e8..82f6876970 100644 --- a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py @@ -6,12 +6,15 @@ import getpass from datetime import datetime import pyblish.api -from openpype.pipeline import legacy_io +from openpype.pipeline import legacy_io, OpenPypePyblishPluginMixin from openpype.tests.lib import is_in_tests 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 - +from openpype.lib import ( + is_running_from_build, + BoolDef, + NumberDef +) @attr.s class DeadlinePluginInfo(): @@ -20,8 +23,27 @@ class DeadlinePluginInfo(): Version = attr.ib(default=None) IgnoreInputs = attr.ib(default=True) +@attr.s +class ArnoldRenderDeadlinePluginInfo(): + InputFile = attr.ib(default=None) + Verbose = attr.ib(default=4) -class HoudiniSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): + +@attr.s +class MantraRenderDeadlinePluginInfo(): + SceneFile = attr.ib(default=None) + Version = attr.ib(default=None) + + +@attr.s +class VrayRenderPluginInfo(): + InputFilename = attr.ib(default=None) + + +class HoudiniSubmitDeadline( + abstract_submit_deadline.AbstractSubmitDeadline, + OpenPypePyblishPluginMixin +): """Submit Solaris USD Render ROPs to Deadline. Renders are submitted to a Deadline Web Service as @@ -46,21 +68,117 @@ class HoudiniSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): targets = ["local"] use_published = True - def get_job_info(self): - job_info = DeadlineJobInfo(Plugin="Houdini") + # presets + priority = 50 + chunk_size = 1 + export_priority = 50 + export_chunk_size = 10 + group = "" + export_group = "" + department = "" + limit_groups = {} + env_allowed_keys = [] + env_search_replace_values = {} + + @classmethod + def apply_settings(cls, project_settings, system_settings): + settings = project_settings["deadline"]["publish"]["HoudiniSubmitDeadline"] # noqa + + # Take some defaults from settings + cls.use_published = settings.get( + "use_published", cls.use_published + ) + cls.priority = settings.get( + "priority", cls.priority + ) + cls.export_priority = settings.get( + "export_priority", cls.export_priority + ) + cls.chunk_size = settings.get("chunk_size", cls.chunk_size) + cls.export_chunk_size = settings.get( + "export_chunk_size", cls.export_chunk_size + ) + cls.group = settings.get("group", cls.group) + cls.export_group = settings.get("export_group", cls.export_group) + cls.department = settings.get("department", cls.department) + cls.env_allowed_keys = settings.get("env_allowed_keys", cls.env_allowed_keys) + cls.env_search_replace_values = settings.get( + "env_search_replace_values", cls.env_allowed_keys + ) + + @classmethod + def get_attribute_defs(cls): + return [ + NumberDef( + "priority", + label="Priority", + default=cls.priority, + decimals=0 + ), + NumberDef( + "chunk", + label="Frames Per Task", + default=cls.chunk_size, + decimals=0, + minimum=1, + maximum=1000 + ), + NumberDef( + "export_priority", + label="Export Priority", + default=cls.priority, + decimals=0 + ), + NumberDef( + "export_chunk", + label="Export Frames Per Task", + default=cls.export_chunk_size, + decimals=0, + minimum=1, + maximum=1000 + ), + BoolDef( + "suspend_publish", + default=False, + label="Suspend publish" + ) + ] + + def get_job_info(self, split_render_job=False, export_job=False, dependency_job_ids=None): instance = self._instance context = instance.context + attribute_values = self.get_attr_values_from_data(instance.data) + + if split_render_job and not export_job: + # Convert from family to Deadline plugin name + # i.e., arnold_rop -> Arnold + plugin = instance.data.get("family").replace("_rop", "").capitalize() + else: + plugin = "Houdini" + + job_info = DeadlineJobInfo(Plugin=plugin) + filepath = context.data["currentFile"] filename = os.path.basename(filepath) - job_info.Name = "{} - {}".format(filename, instance.name) job_info.BatchName = filename - job_info.Plugin = "Houdini" + job_info.UserName = context.data.get( "deadlineUser", getpass.getuser()) + if split_render_job and export_job: + job_info.Priority = attribute_values.get( + "export_priority", self.export_priority + ) + else: + job_info.Priority = attribute_values.get( + "priority", self.priority + ) + + job_info.Department = self.department + if is_in_tests(): job_info.BatchName += datetime.now().strftime("%d%m%Y%H%M%S") @@ -72,9 +190,23 @@ class HoudiniSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): ) job_info.Frames = frames + # Make sure we make job frame dependent so render tasks pick up a soon + # as export tasks are done + if split_render_job and not export_job: + job_info.IsFrameDependent = True + job_info.Pool = instance.data.get("primaryPool") job_info.SecondaryPool = instance.data.get("secondaryPool") - job_info.ChunkSize = instance.data.get("chunkSize", 10) + job_info.Group = self.group + if split_render_job and export_job: + job_info.ChunkSize = attribute_values.get( + "export_chunk", self.export_chunk_size + ) + else: + job_info.ChunkSize = attribute_values.get( + "chunk", self.chunk_size + ) + job_info.Comment = context.data.get("comment") keys = [ @@ -98,8 +230,19 @@ class HoudiniSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): if self._instance.context.data.get("deadlinePassMongoUrl"): keys.append("OPENPYPE_MONGO") + # add allowed keys from preset if any + if self.env_allowed_keys: + keys += self.env_allowed_keys + environment = dict({key: os.environ[key] for key in keys if key in os.environ}, **legacy_io.Session) + + # finally search replace in values of any key + if self.env_search_replace_values: + for key, value in environment.items(): + for _k, _v in self.env_search_replace_values.items(): + environment[key] = value.replace(_k, _v) + for key in keys: value = environment.get(key) if value: @@ -114,23 +257,49 @@ class HoudiniSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): job_info.OutputDirectory += dirname.replace("\\", "/") job_info.OutputFilename += fname + # Add dependencies if given + if dependency_job_ids: + job_info.JobDependencies = ",".join(dependency_job_ids) + return job_info - def get_plugin_info(self): + def get_plugin_info(self, split_render_job=False): instance = self._instance context = instance.context - # Output driver to render - driver = hou.node(instance.data["instance_node"]) hou_major_minor = hou.applicationVersionString().rsplit(".", 1)[0] - plugin_info = DeadlinePluginInfo( - SceneFile=context.data["currentFile"], - OutputDriver=driver.path(), - Version=hou_major_minor, - IgnoreInputs=True - ) + # Output driver to render + if split_render_job: + family = instance.data.get("family") + if family == "arnold_rop": + plugin_info = ArnoldRenderDeadlinePluginInfo( + InputFile=instance.data["ifdFile"] + ) + elif family == "mantra_rop": + plugin_info = MantraRenderDeadlinePluginInfo( + SceneFile=instance.data["ifdFile"], + Version=hou_major_minor, + ) + elif family == "vray_rop": + plugin_info = VrayRenderPluginInfo( + InputFilename=instance.data["ifdFile"], + ) + else: + self.log.error( + "Family %s not supported yet to split export and render job", + family + ) + return + else: + driver = hou.node(instance.data["instance_node"]) + plugin_info = DeadlinePluginInfo( + SceneFile=context.data["currentFile"], + OutputDriver=driver.path(), + Version=hou_major_minor, + IgnoreInputs=True + ) return attr.asdict(plugin_info) From fa261c6a3269815575824dfd035e8424195ecbec Mon Sep 17 00:00:00 2001 From: Fabia Serra Arrizabalaga Date: Mon, 7 Aug 2023 18:23:36 +0200 Subject: [PATCH 0014/1233] Add way to query export parm from Houdini and fix output parm --- openpype/hosts/houdini/api/lib.py | 121 ++++++++++++++++++++++++++---- 1 file changed, 106 insertions(+), 15 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index b03f8c8fc1..b1673b746d 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -104,8 +104,8 @@ def get_id_required_nodes(): return list(nodes) -def get_output_parameter(node): - """Return the render output parameter name of the given node +def get_export_parameter(node): + """Return the export output parameter of the given node Example: root = hou.node("/obj") @@ -120,21 +120,112 @@ def get_output_parameter(node): hou.Parm """ + node_type = node.type().description() - node_type = node.type().name() - if node_type == "geometry": - return node.parm("sopoutput") - elif node_type == "alembic": - return node.parm("filename") - elif node_type == "comp": - return node.parm("copoutput") - elif node_type == "opengl": - return node.parm("picture") - elif node_type == "arnold": - if node.evalParm("ar_ass_export_enable"): - return node.parm("ar_ass_file") - elif node_type == "Redshift_Proxy_Output": + # Ensures the proper Take is selected for each ROP to retrieve the correct ifd + try: + rop_take = hou.takes.findTake(node.parm("take").eval()) + if rop_take is not None: + hou.takes.setCurrentTake(rop_take) + except AttributeError: + # hou object doesn't always have the 'takes' attribute + pass + + if node_type == "Mantra" and node.parm("soho_outputmode").eval(): + return node.parm("soho_diskfile") + elif node_type == "Alfred": + return node.parm("alf_diskfile") + elif (node_type == "RenderMan" or node_type == "RenderMan RIS"): + pre_ris22 = node.parm("rib_outputmode") and node.parm("rib_outputmode").eval() + ris22 = node.parm("diskfile") and node.parm("diskfile").eval() + if pre_ris22 or ris22: + return node.parm("soho_diskfile") + elif node_type == "Redshift" and node.parm("RS_archive_enable").eval(): return node.parm("RS_archive_file") + elif node_type == "Wedge" and node.parm("driver").eval(): + return get_export_parameter(node.node(node.parm("driver").eval())) + elif node_type == "Arnold": + return node.parm("ar_ass_file") + elif node_type == "Alembic" and node.parm("use_sop_path").eval(): + return node.parm("sop_path") + elif node_type == "Shotgun Mantra" and node.parm("soho_outputmode").eval(): + return node.parm("sgtk_soho_diskfile") + elif node_type == "Shotgun Alembic" and node.parm("use_sop_path").eval(): + return node.parm("sop_path") + elif node.type().nameWithCategory() == "Driver/vray_renderer": + return node.parm("render_export_filepath") + + raise TypeError("Node type '%s' not supported" % node_type) + + +def get_output_parameter(node): + """Return the render output parameter of the given node + + Example: + root = hou.node("/obj") + my_alembic_node = root.createNode("alembic") + get_output_parameter(my_alembic_node) + # Result: "output" + + Args: + node(hou.Node): node instance + + Returns: + hou.Parm + + """ + node_type = node.type().description() + category = node.type().category().name() + + # Figure out which type of node is being rendered + if node_type == "Geometry" or node_type == "Filmbox FBX" or (node_type == "ROP Output Driver" and category == "Sop"): + return node.parm("sopoutput") + elif node_type == "Composite": + return node.parm("copoutput") + elif node_type == "Channel": + return node.parm("chopoutput") + elif node_type == "Dynamics" or (node_type == "ROP Output Driver" and category == "Dop"): + return node.parm("dopoutput") + elif node_type == "Alfred": + return node.parm("alf_diskfile") + elif node_type == "RenderMan" or node_type == "RenderMan RIS": + return node.parm("ri_display") + elif node_type == "Redshift": + return node.parm("RS_returnmePrefix") + elif node_type == "Mantra": + return node.parm("vm_picture") + elif node_type == "Wedge": + driver_node = node.node(node.parm("driver").eval()) + if driver_node: + return get_output_parameter(driver_node) + elif node_type == "Arnold": + return node.parm("ar_picture") + elif node_type == "HQueue Simulation": + inner_node = node.node(node.parm("hq_driver").eval()) + if inner_node: + return get_output_parameter(inner_node) + elif node_type == "ROP Alembic Output": + return node.parm("filename") + elif node_type == "Redshift": + return node.parm("RS_returnmePrefix") + elif node_type == "Alembic": + return node.parm("filename") + elif node_type == "Shotgun Mantra": + return node.parm("sgtk_vm_picture") + elif node_type == "Shotgun Alembic": + return node.parm("filename") + elif node_type == "Bake Texture": + return node.parm("vm_uvoutputpicture1") + elif node_type == "OpenGL": + return node.parm("picture") + elif node_type == "Octane": + return node.parm("HO_img_fileName") + elif node_type == "Fetch": + inner_node = node.node(node.parm("source").eval()) + if inner_node: + return get_output_parameter(inner_node) + elif node.type().nameWithCategory() == "Driver/vray_renderer": + return node.parm("SettingsOutput_img_file_path") raise TypeError("Node type '%s' not supported" % node_type) From 2b2ad8f1ed8ff4922fa9216983958e79ea93e26a Mon Sep 17 00:00:00 2001 From: Fabia Serra Arrizabalaga Date: Mon, 7 Aug 2023 18:30:12 +0200 Subject: [PATCH 0015/1233] Add missing pre create attr for splitting render job to Mantra ROP --- openpype/hosts/houdini/plugins/create/create_mantra_rop.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/houdini/plugins/create/create_mantra_rop.py b/openpype/hosts/houdini/plugins/create/create_mantra_rop.py index 41311a1e30..0601124ac0 100644 --- a/openpype/hosts/houdini/plugins/create/create_mantra_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_mantra_rop.py @@ -90,6 +90,9 @@ class CreateMantraROP(plugin.HoudiniCreator): BoolDef("farm", label="Submitting to Farm", default=True), + BoolDef("export_job", + label="Split export and render jobs", + default=self.export_job), EnumDef("image_format", image_format_enum, default="exr", From aab913acbc7f33e0c79d2a966a52b22fd99c54fc Mon Sep 17 00:00:00 2001 From: Fabia Serra Arrizabalaga Date: Mon, 7 Aug 2023 18:32:09 +0200 Subject: [PATCH 0016/1233] Add Houdini deadline submission settings --- .../defaults/project_settings/deadline.json | 15 +++ .../schema_project_deadline.json | 91 +++++++++++++++++++ 2 files changed, 106 insertions(+) diff --git a/openpype/settings/defaults/project_settings/deadline.json b/openpype/settings/defaults/project_settings/deadline.json index 1b8c8397d7..d088ef5747 100644 --- a/openpype/settings/defaults/project_settings/deadline.json +++ b/openpype/settings/defaults/project_settings/deadline.json @@ -19,6 +19,21 @@ "deadline" ] }, + "HoudiniSubmitDeadline": { + "enabled": true, + "active": true, + "use_published": true, + "priority": 50, + "render_chunk_size": 1, + "group": "none", + "export_priority": 50, + "export_chunk_size": 10, + "export_group": "none", + "department": "", + "env_allowed_keys": [], + "env_search_replace_values": {}, + "limit_groups": {} + }, "MayaSubmitDeadline": { "enabled": true, "optional": false, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json index 6d59b5a92b..dfbcd8ef27 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json @@ -89,6 +89,97 @@ } ] }, + { + "type": "dict", + "collapsible": true, + "key": "HoudiniSubmitDeadline", + "label": "Houdini Submit to Deadline", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "boolean", + "key": "active", + "label": "Active" + }, + { + "type": "splitter" + }, + { + "type": "boolean", + "key": "use_published", + "label": "Use Published scene" + }, + { + "type": "number", + "key": "priority", + "label": "Priority" + }, + { + "type": "number", + "key": "render_chunk_size", + "label": "Render Chunk Size" + }, + { + "type": "text", + "key": "group", + "label": "Group" + }, + { + "type": "splitter" + }, + { + "type": "number", + "key": "export_priority", + "label": "Export Priority" + }, + { + "type": "number", + "key": "export_chunk_size", + "label": "Export Chunk Size" + }, + { + "type": "text", + "key": "export_group", + "label": "Export Group" + }, + { + "type": "splitter" + }, + { + "type": "text", + "key": "department", + "label": "Department" + }, + { + "type": "list", + "key": "env_allowed_keys", + "object_type": "text", + "label": "Allowed environment keys" + }, + { + "type": "dict-modifiable", + "key": "env_search_replace_values", + "label": "Search & replace in environment values", + "object_type": { + "type": "text" + } + }, + { + "type": "dict-modifiable", + "key": "limit_groups", + "label": "Limit Groups", + "object_type": { + "type": "list", + "object_type": "text" + } + } + ] + }, { "type": "dict", "collapsible": true, From 33dff766fe6777ff5e801beebe2b366ed5122c20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A0=20Serra=20Arrizabalaga?= Date: Mon, 7 Aug 2023 18:51:53 +0200 Subject: [PATCH 0017/1233] Shush Hound --- openpype/hosts/houdini/api/lib.py | 12 ++++++++---- .../houdini/plugins/create/create_arnold_rop.py | 9 +++++---- .../houdini/plugins/create/create_mantra_rop.py | 9 +++++---- .../hosts/houdini/plugins/create/create_vray_rop.py | 9 +++++---- .../houdini/plugins/publish/collect_arnold_rop.py | 10 +++++++--- .../houdini/plugins/publish/collect_mantra_rop.py | 10 +++++++--- .../houdini/plugins/publish/collect_vray_rop.py | 8 ++++++-- .../publish/submit_houdini_render_deadline.py | 11 ++++++++--- 8 files changed, 51 insertions(+), 27 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index b1673b746d..94c4529ae2 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -122,7 +122,8 @@ def get_export_parameter(node): """ node_type = node.type().description() - # Ensures the proper Take is selected for each ROP to retrieve the correct ifd + # Ensures the proper Take is selected for each ROP to retrieve the correct + # ifd try: rop_take = hou.takes.findTake(node.parm("take").eval()) if rop_take is not None: @@ -136,7 +137,8 @@ def get_export_parameter(node): elif node_type == "Alfred": return node.parm("alf_diskfile") elif (node_type == "RenderMan" or node_type == "RenderMan RIS"): - pre_ris22 = node.parm("rib_outputmode") and node.parm("rib_outputmode").eval() + pre_ris22 = node.parm("rib_outputmode") and \ + node.parm("rib_outputmode").eval() ris22 = node.parm("diskfile") and node.parm("diskfile").eval() if pre_ris22 or ris22: return node.parm("soho_diskfile") @@ -178,13 +180,15 @@ def get_output_parameter(node): category = node.type().category().name() # Figure out which type of node is being rendered - if node_type == "Geometry" or node_type == "Filmbox FBX" or (node_type == "ROP Output Driver" and category == "Sop"): + if node_type == "Geometry" or node_type == "Filmbox FBX" or \ + (node_type == "ROP Output Driver" and category == "Sop"): return node.parm("sopoutput") elif node_type == "Composite": return node.parm("copoutput") elif node_type == "Channel": return node.parm("chopoutput") - elif node_type == "Dynamics" or (node_type == "ROP Output Driver" and category == "Dop"): + elif node_type == "Dynamics" or \ + (node_type == "ROP Output Driver" and category == "Dop"): return node.parm("dopoutput") elif node_type == "Alfred": return node.parm("alf_diskfile") diff --git a/openpype/hosts/houdini/plugins/create/create_arnold_rop.py b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py index d19795d3a9..b2f04b64ec 100644 --- a/openpype/hosts/houdini/plugins/create/create_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py @@ -53,10 +53,11 @@ class CreateArnoldRop(plugin.HoudiniCreator): } if pre_create_data.get("export_job"): - ass_filepath = "{export_dir}{subset_name}/{subset_name}.$F4.ass".format( - export_dir=hou.text.expandString("$HIP/pyblish/ass/"), - subset_name=subset_name, - ) + ass_filepath = \ + "{export_dir}{subset_name}/{subset_name}.$F4.ass".format( + export_dir=hou.text.expandString("$HIP/pyblish/ass/"), + subset_name=subset_name, + ) parms["ar_ass_export_enable"] = 1 parms["ar_ass_file"] = ass_filepath diff --git a/openpype/hosts/houdini/plugins/create/create_mantra_rop.py b/openpype/hosts/houdini/plugins/create/create_mantra_rop.py index 0601124ac0..62074fe432 100644 --- a/openpype/hosts/houdini/plugins/create/create_mantra_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_mantra_rop.py @@ -49,10 +49,11 @@ class CreateMantraROP(plugin.HoudiniCreator): } if pre_create_data.get("export_job"): - ifd_filepath = "{export_dir}{subset_name}/{subset_name}.$F4.ifd".format( - export_dir=hou.text.expandString("$HIP/pyblish/ifd/"), - subset_name=subset_name, - ) + ifd_filepath = \ + "{export_dir}{subset_name}/{subset_name}.$F4.ifd".format( + export_dir=hou.text.expandString("$HIP/pyblish/ifd/"), + subset_name=subset_name, + ) parms["soho_outputmode"] = 1 parms["soho_diskfile"] = ifd_filepath diff --git a/openpype/hosts/houdini/plugins/create/create_vray_rop.py b/openpype/hosts/houdini/plugins/create/create_vray_rop.py index 29fab0c60c..de3cfc3858 100644 --- a/openpype/hosts/houdini/plugins/create/create_vray_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_vray_rop.py @@ -58,10 +58,11 @@ class CreateVrayROP(plugin.HoudiniCreator): } if pre_create_data.get("export_job"): - scene_filepath = "{export_dir}{subset_name}/{subset_name}.$F4.vrscene".format( - export_dir=hou.text.expandString("$HIP/pyblish/vrscene/"), - subset_name=subset_name, - ) + scene_filepath = \ + "{export_dir}{subset_name}/{subset_name}.$F4.vrscene".format( + export_dir=hou.text.expandString("$HIP/pyblish/vrscene/"), + subset_name=subset_name, + ) # TODO: don't have VRay to check the names of these parms["render_export_mode"] = 1 parms["render_export_filepath"] = scene_filepath diff --git a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py index 063dd728e1..4af1d80a2d 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py @@ -38,18 +38,22 @@ class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin): default_prefix = evalParmNoFrame(rop, "ar_picture") render_products = [] - # Store whether we are splitting the render job in an export + render + # Store whether we are splitting the render job (export + render) export_job = bool(rop.parm("ar_ass_export_enable").eval()) instance.data["exportJob"] = export_job export_prefix = None export_products = [] if export_job: - export_prefix = evalParmNoFrame(rop, "ar_ass_file", pad_character="0") + export_prefix = evalParmNoFrame( + rop, "ar_ass_file", pad_character="0" + ) beauty_export_product = self.get_render_product_name( prefix=export_prefix, suffix=None) export_products.append(beauty_export_product) - self.log.debug("Found export product: {}".format(beauty_export_product)) + self.log.debug( + "Found export product: {}".format(beauty_export_product) + ) instance.data["ifdFile"] = beauty_export_product instance.data["exportFiles"] = list(export_products) diff --git a/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py b/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py index b235e2c110..7385a87679 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py @@ -42,18 +42,22 @@ class CollectMantraROPRenderProducts(pyblish.api.InstancePlugin): default_prefix = evalParmNoFrame(rop, "vm_picture") render_products = [] - # Store whether we are splitting the render job in an export + render + # Store whether we are splitting the render job (export + render) export_job = bool(rop.parm("soho_outputmode").eval()) instance.data["exportJob"] = export_job export_prefix = None export_products = [] if export_job: - export_prefix = evalParmNoFrame(rop, "soho_diskfile", pad_character="0") + export_prefix = evalParmNoFrame( + rop, "soho_diskfile", pad_character="0" + ) beauty_export_product = self.get_render_product_name( prefix=export_prefix, suffix=None) export_products.append(beauty_export_product) - self.log.debug("Found export product: {}".format(beauty_export_product)) + self.log.debug( + "Found export product: {}".format(beauty_export_product) + ) instance.data["ifdFile"] = beauty_export_product instance.data["exportFiles"] = list(export_products) diff --git a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py index 89a7c8bf72..4259f19eb2 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py @@ -50,12 +50,16 @@ class CollectVrayROPRenderProducts(pyblish.api.InstancePlugin): export_prefix = None export_products = [] if export_job: - export_prefix = evalParmNoFrame(rop, "render_export_filepath", pad_character="0") + export_prefix = evalParmNoFrame( + rop, "render_export_filepath", pad_character="0" + ) beauty_export_product = self.get_render_product_name( prefix=export_prefix, suffix=None) export_products.append(beauty_export_product) - self.log.debug("Found export product: {}".format(beauty_export_product)) + self.log.debug( + "Found export product: {}".format(beauty_export_product) + ) instance.data["ifdFile"] = beauty_export_product instance.data["exportFiles"] = list(export_products) 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 82f6876970..d7d5ed906b 100644 --- a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py @@ -144,7 +144,12 @@ class HoudiniSubmitDeadline( ) ] - def get_job_info(self, split_render_job=False, export_job=False, dependency_job_ids=None): + def get_job_info( + self, + split_render_job=False, + export_job=False, + dependency_job_ids=None + ): instance = self._instance context = instance.context @@ -154,7 +159,7 @@ class HoudiniSubmitDeadline( if split_render_job and not export_job: # Convert from family to Deadline plugin name # i.e., arnold_rop -> Arnold - plugin = instance.data.get("family").replace("_rop", "").capitalize() + plugin = instance.data["family"].replace("_rop", "").capitalize() else: plugin = "Houdini" @@ -288,7 +293,7 @@ class HoudiniSubmitDeadline( ) else: self.log.error( - "Family %s not supported yet to split export and render job", + "Family '%s' not supported yet to split render job", family ) return From 2f62dd74ab9aed69f40d21b0ed3cc163c15d7bfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A0=20Serra=20Arrizabalaga?= Date: Mon, 7 Aug 2023 18:59:16 +0200 Subject: [PATCH 0018/1233] Shush Hound --- .../modules/deadline/abstract_submit_deadline.py | 8 +++++--- .../publish/submit_houdini_render_deadline.py | 15 +++++++++------ 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/openpype/modules/deadline/abstract_submit_deadline.py b/openpype/modules/deadline/abstract_submit_deadline.py index 56d8afbe3e..ae251f39b9 100644 --- a/openpype/modules/deadline/abstract_submit_deadline.py +++ b/openpype/modules/deadline/abstract_submit_deadline.py @@ -448,10 +448,12 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin, # Check whether we are splitting render job in export + render # and if so, create a separate task for the render - # TODO: Find a cleaner way so `get_job_info` can take extra args depending - # on host + # TODO: Find a cleaner way so `get_job_info` can take extra args + # depending on host export_job = instance.data["exportJob"] - self.job_info = self.get_job_info(split_render_job=export_job, export_job=True) + self.job_info = self.get_job_info( + split_render_job=export_job, export_job=True + ) self.plugin_info = self.get_plugin_info() self.aux_files = self.get_aux_files() 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 d7d5ed906b..e5cd6467f7 100644 --- a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py @@ -23,6 +23,7 @@ class DeadlinePluginInfo(): Version = attr.ib(default=None) IgnoreInputs = attr.ib(default=True) + @attr.s class ArnoldRenderDeadlinePluginInfo(): InputFile = attr.ib(default=None) @@ -101,7 +102,9 @@ class HoudiniSubmitDeadline( cls.group = settings.get("group", cls.group) cls.export_group = settings.get("export_group", cls.export_group) cls.department = settings.get("department", cls.department) - cls.env_allowed_keys = settings.get("env_allowed_keys", cls.env_allowed_keys) + cls.env_allowed_keys = settings.get( + "env_allowed_keys", cls.env_allowed_keys + ) cls.env_search_replace_values = settings.get( "env_search_replace_values", cls.env_allowed_keys ) @@ -145,11 +148,11 @@ class HoudiniSubmitDeadline( ] def get_job_info( - self, - split_render_job=False, - export_job=False, - dependency_job_ids=None - ): + self, + split_render_job=False, + export_job=False, + dependency_job_ids=None + ): instance = self._instance context = instance.context From a0987388c9a3202e34a76f5057528f5f9f7d63ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A0=20Serra=20Arrizabalaga?= Date: Mon, 7 Aug 2023 22:18:13 +0200 Subject: [PATCH 0019/1233] Revert "Add Houdini deadline submission settings" This reverts commit aab913acbc7f33e0c79d2a966a52b22fd99c54fc. --- .../defaults/project_settings/deadline.json | 15 --- .../schema_project_deadline.json | 91 ------------------- 2 files changed, 106 deletions(-) diff --git a/openpype/settings/defaults/project_settings/deadline.json b/openpype/settings/defaults/project_settings/deadline.json index d088ef5747..1b8c8397d7 100644 --- a/openpype/settings/defaults/project_settings/deadline.json +++ b/openpype/settings/defaults/project_settings/deadline.json @@ -19,21 +19,6 @@ "deadline" ] }, - "HoudiniSubmitDeadline": { - "enabled": true, - "active": true, - "use_published": true, - "priority": 50, - "render_chunk_size": 1, - "group": "none", - "export_priority": 50, - "export_chunk_size": 10, - "export_group": "none", - "department": "", - "env_allowed_keys": [], - "env_search_replace_values": {}, - "limit_groups": {} - }, "MayaSubmitDeadline": { "enabled": true, "optional": false, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json index dfbcd8ef27..6d59b5a92b 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json @@ -89,97 +89,6 @@ } ] }, - { - "type": "dict", - "collapsible": true, - "key": "HoudiniSubmitDeadline", - "label": "Houdini Submit to Deadline", - "checkbox_key": "enabled", - "children": [ - { - "type": "boolean", - "key": "enabled", - "label": "Enabled" - }, - { - "type": "boolean", - "key": "active", - "label": "Active" - }, - { - "type": "splitter" - }, - { - "type": "boolean", - "key": "use_published", - "label": "Use Published scene" - }, - { - "type": "number", - "key": "priority", - "label": "Priority" - }, - { - "type": "number", - "key": "render_chunk_size", - "label": "Render Chunk Size" - }, - { - "type": "text", - "key": "group", - "label": "Group" - }, - { - "type": "splitter" - }, - { - "type": "number", - "key": "export_priority", - "label": "Export Priority" - }, - { - "type": "number", - "key": "export_chunk_size", - "label": "Export Chunk Size" - }, - { - "type": "text", - "key": "export_group", - "label": "Export Group" - }, - { - "type": "splitter" - }, - { - "type": "text", - "key": "department", - "label": "Department" - }, - { - "type": "list", - "key": "env_allowed_keys", - "object_type": "text", - "label": "Allowed environment keys" - }, - { - "type": "dict-modifiable", - "key": "env_search_replace_values", - "label": "Search & replace in environment values", - "object_type": { - "type": "text" - } - }, - { - "type": "dict-modifiable", - "key": "limit_groups", - "label": "Limit Groups", - "object_type": { - "type": "list", - "object_type": "text" - } - } - ] - }, { "type": "dict", "collapsible": true, From d309df3b49b2d20cdac28a4150f1546f83ee5a8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A0=20Serra=20Arrizabalaga?= Date: Mon, 7 Aug 2023 22:22:31 +0200 Subject: [PATCH 0020/1233] Remove Houdini Deadline settings as it's not relevant to feature --- .../publish/submit_houdini_render_deadline.py | 28 ------------------- 1 file changed, 28 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 e5cd6467f7..fdfc66b2c0 100644 --- a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py @@ -81,34 +81,6 @@ class HoudiniSubmitDeadline( env_allowed_keys = [] env_search_replace_values = {} - @classmethod - def apply_settings(cls, project_settings, system_settings): - settings = project_settings["deadline"]["publish"]["HoudiniSubmitDeadline"] # noqa - - # Take some defaults from settings - cls.use_published = settings.get( - "use_published", cls.use_published - ) - cls.priority = settings.get( - "priority", cls.priority - ) - cls.export_priority = settings.get( - "export_priority", cls.export_priority - ) - cls.chunk_size = settings.get("chunk_size", cls.chunk_size) - cls.export_chunk_size = settings.get( - "export_chunk_size", cls.export_chunk_size - ) - cls.group = settings.get("group", cls.group) - cls.export_group = settings.get("export_group", cls.export_group) - cls.department = settings.get("department", cls.department) - cls.env_allowed_keys = settings.get( - "env_allowed_keys", cls.env_allowed_keys - ) - cls.env_search_replace_values = settings.get( - "env_search_replace_values", cls.env_allowed_keys - ) - @classmethod def get_attribute_defs(cls): return [ From 20583173c663e84b84e0512692cf1495f51fb1ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A0=20Serra=20Arrizabalaga?= Date: Mon, 7 Aug 2023 22:26:02 +0200 Subject: [PATCH 0021/1233] Remove some other noise from PR feature --- .../publish/submit_houdini_render_deadline.py | 15 --------------- 1 file changed, 15 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 fdfc66b2c0..d6c658944a 100644 --- a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py @@ -76,10 +76,6 @@ class HoudiniSubmitDeadline( export_chunk_size = 10 group = "" export_group = "" - department = "" - limit_groups = {} - env_allowed_keys = [] - env_search_replace_values = {} @classmethod def get_attribute_defs(cls): @@ -157,8 +153,6 @@ class HoudiniSubmitDeadline( "priority", self.priority ) - job_info.Department = self.department - if is_in_tests(): job_info.BatchName += datetime.now().strftime("%d%m%Y%H%M%S") @@ -210,18 +204,9 @@ class HoudiniSubmitDeadline( if self._instance.context.data.get("deadlinePassMongoUrl"): keys.append("OPENPYPE_MONGO") - # add allowed keys from preset if any - if self.env_allowed_keys: - keys += self.env_allowed_keys - environment = dict({key: os.environ[key] for key in keys if key in os.environ}, **legacy_io.Session) - # finally search replace in values of any key - if self.env_search_replace_values: - for key, value in environment.items(): - for _k, _v in self.env_search_replace_values.items(): - environment[key] = value.replace(_k, _v) for key in keys: value = environment.get(key) From f58c4de0d4a284867eb0fb43b3d235abff9f26ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A0=20Serra=20Arrizabalaga?= Date: Mon, 7 Aug 2023 22:44:45 +0200 Subject: [PATCH 0022/1233] Move some of the logic to Houdini submitter so abstract functions are a bit more generic --- .../deadline/abstract_submit_deadline.py | 19 +++++------- .../publish/submit_houdini_render_deadline.py | 30 +++++++++++-------- 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/openpype/modules/deadline/abstract_submit_deadline.py b/openpype/modules/deadline/abstract_submit_deadline.py index ae251f39b9..dddb4400c1 100644 --- a/openpype/modules/deadline/abstract_submit_deadline.py +++ b/openpype/modules/deadline/abstract_submit_deadline.py @@ -446,25 +446,20 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin, self.scene_path = file_path self.log.info("Using {} for render/export.".format(file_path)) - # Check whether we are splitting render job in export + render - # and if so, create a separate task for the render - # TODO: Find a cleaner way so `get_job_info` can take extra args - # depending on host - export_job = instance.data["exportJob"] - self.job_info = self.get_job_info( - split_render_job=export_job, export_job=True - ) + self.job_info = self.get_job_info() self.plugin_info = self.get_plugin_info() self.aux_files = self.get_aux_files() job_id = self.process_submission() + self.log.info("Submitted job to Deadline: {}.".format(job_id)) + + # TODO: Find a way that's more generic and not render type specific + export_job = instance.data["exportJob"] if export_job: self.log.info("Splitting export and render in two jobs") self.log.info("Export job id: %s", job_id) - render_job_info = self.get_job_info( - split_render_job=True, dependency_job_ids=[job_id] - ) - render_plugin_info = self.get_plugin_info(split_render_job=True) + render_job_info = self.get_job_info(dependency_job_ids=[job_id]) + render_plugin_info = self.get_plugin_info(job_type="render") payload = self.assemble_payload( job_info=render_job_info, plugin_info=render_plugin_info 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 d6c658944a..83f6e81485 100644 --- a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py @@ -115,19 +115,24 @@ class HoudiniSubmitDeadline( ) ] - def get_job_info( - self, - split_render_job=False, - export_job=False, - dependency_job_ids=None - ): + def get_job_info(self, dependency_job_ids=None): instance = self._instance context = instance.context attribute_values = self.get_attr_values_from_data(instance.data) - if split_render_job and not export_job: + # Whether Deadline render submission is being split in two + # (extract + render) + split_render_job = instance.data["exportJob"] + + # If there's some dependency job ids we can assume this is a render job + # and not an export job + is_export_job = True + if dependency_job_ids: + is_export_job = False + + if split_render_job and not is_export_job: # Convert from family to Deadline plugin name # i.e., arnold_rop -> Arnold plugin = instance.data["family"].replace("_rop", "").capitalize() @@ -144,7 +149,7 @@ class HoudiniSubmitDeadline( job_info.UserName = context.data.get( "deadlineUser", getpass.getuser()) - if split_render_job and export_job: + if split_render_job and is_export_job: job_info.Priority = attribute_values.get( "export_priority", self.export_priority ) @@ -166,13 +171,13 @@ class HoudiniSubmitDeadline( # Make sure we make job frame dependent so render tasks pick up a soon # as export tasks are done - if split_render_job and not export_job: + if split_render_job and not is_export_job: job_info.IsFrameDependent = True job_info.Pool = instance.data.get("primaryPool") job_info.SecondaryPool = instance.data.get("secondaryPool") job_info.Group = self.group - if split_render_job and export_job: + if split_render_job and is_export_job: job_info.ChunkSize = attribute_values.get( "export_chunk", self.export_chunk_size ) @@ -207,7 +212,6 @@ class HoudiniSubmitDeadline( environment = dict({key: os.environ[key] for key in keys if key in os.environ}, **legacy_io.Session) - for key in keys: value = environment.get(key) if value: @@ -228,7 +232,7 @@ class HoudiniSubmitDeadline( return job_info - def get_plugin_info(self, split_render_job=False): + def get_plugin_info(self, job_type=None): instance = self._instance context = instance.context @@ -236,7 +240,7 @@ class HoudiniSubmitDeadline( hou_major_minor = hou.applicationVersionString().rsplit(".", 1)[0] # Output driver to render - if split_render_job: + if job_type == "render": family = instance.data.get("family") if family == "arnold_rop": plugin_info = ArnoldRenderDeadlinePluginInfo( From 12b2a74ea963a72737f9409f5bfd04acabb53f79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A0=20Serra=20Arrizabalaga?= Date: Mon, 7 Aug 2023 22:52:01 +0200 Subject: [PATCH 0023/1233] Fix docstring --- .../deadline/plugins/publish/submit_houdini_render_deadline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 83f6e81485..8434ddacd3 100644 --- a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py @@ -45,7 +45,7 @@ class HoudiniSubmitDeadline( abstract_submit_deadline.AbstractSubmitDeadline, OpenPypePyblishPluginMixin ): - """Submit Solaris USD Render ROPs to Deadline. + """Submit Render ROPs to Deadline. Renders are submitted to a Deadline Web Service as supplied via the environment variable AVALON_DEADLINE. From ad11a4d3d3c7ec8be0c605cf4da18deea3ab102e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A0=20Serra=20Arrizabalaga?= Date: Tue, 8 Aug 2023 16:08:45 +0200 Subject: [PATCH 0024/1233] Fix VRay submission parms --- openpype/hosts/houdini/plugins/create/create_vray_rop.py | 3 +-- openpype/hosts/houdini/plugins/publish/collect_vray_rop.py | 5 ++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_vray_rop.py b/openpype/hosts/houdini/plugins/create/create_vray_rop.py index de3cfc3858..222088523a 100644 --- a/openpype/hosts/houdini/plugins/create/create_vray_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_vray_rop.py @@ -63,8 +63,7 @@ class CreateVrayROP(plugin.HoudiniCreator): export_dir=hou.text.expandString("$HIP/pyblish/vrscene/"), subset_name=subset_name, ) - # TODO: don't have VRay to check the names of these - parms["render_export_mode"] = 1 + parms["render_export_mode"] = "1" parms["render_export_filepath"] = scene_filepath if self.selected_nodes: diff --git a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py index 4259f19eb2..d7d53ff566 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py @@ -44,7 +44,6 @@ class CollectVrayROPRenderProducts(pyblish.api.InstancePlugin): # TODO: add render elements if render element # Store whether we are splitting the render job in an export + render - # TODO: check names of VRay parms export_job = bool(rop.parm("render_export_mode").eval()) instance.data["exportJob"] = export_job export_prefix = None @@ -63,7 +62,7 @@ class CollectVrayROPRenderProducts(pyblish.api.InstancePlugin): instance.data["ifdFile"] = beauty_export_product instance.data["exportFiles"] = list(export_products) - beauty_product = self.get_beauty_render_product(default_prefix) + beauty_product = self.get_render_product_name(default_prefix) render_products.append(beauty_product) files_by_aov = { "RGB Color": self.generate_expected_files(instance, @@ -97,7 +96,7 @@ class CollectVrayROPRenderProducts(pyblish.api.InstancePlugin): instance.data["colorspaceDisplay"] = colorspace_data["display"] instance.data["colorspaceView"] = colorspace_data["view"] - def get_beauty_render_product(self, prefix, suffix=""): + def get_render_product_name(self, prefix, suffix=""): """Return the beauty output filename if render element enabled """ aov_parm = ".{}".format(suffix) From cb7274bf1a79d5c5d315b5ae08e7f6042c598539 Mon Sep 17 00:00:00 2001 From: Fabia Serra Arrizabalaga Date: Wed, 9 Aug 2023 15:19:24 +0200 Subject: [PATCH 0025/1233] Fix render_export_mode for VRay so it only exports --- openpype/hosts/houdini/plugins/create/create_vray_rop.py | 4 +++- openpype/hosts/houdini/plugins/publish/collect_vray_rop.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_vray_rop.py b/openpype/hosts/houdini/plugins/create/create_vray_rop.py index 222088523a..cd5ef2013d 100644 --- a/openpype/hosts/houdini/plugins/create/create_vray_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_vray_rop.py @@ -63,7 +63,9 @@ class CreateVrayROP(plugin.HoudiniCreator): export_dir=hou.text.expandString("$HIP/pyblish/vrscene/"), subset_name=subset_name, ) - parms["render_export_mode"] = "1" + # Setting render_export_mode to "2" because that's for + # "Export only" ("1" is for "Export & Render") + parms["render_export_mode"] = "2" parms["render_export_filepath"] = scene_filepath if self.selected_nodes: diff --git a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py index d7d53ff566..184c644fd6 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py @@ -44,7 +44,7 @@ class CollectVrayROPRenderProducts(pyblish.api.InstancePlugin): # TODO: add render elements if render element # Store whether we are splitting the render job in an export + render - export_job = bool(rop.parm("render_export_mode").eval()) + export_job = rop.parm("render_export_mode").eval() == "2" instance.data["exportJob"] = export_job export_prefix = None export_products = [] From bfe8f88b241e4a71f3a9aabbfda2d87087f26e38 Mon Sep 17 00:00:00 2001 From: Fabia Serra Arrizabalaga Date: Thu, 10 Aug 2023 13:43:27 +0200 Subject: [PATCH 0026/1233] Add SeparateFilesPerFrame attribute to Vray plugin --- .../deadline/plugins/publish/submit_houdini_render_deadline.py | 1 + 1 file changed, 1 insertion(+) 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 8434ddacd3..cc982cd2eb 100644 --- a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py @@ -39,6 +39,7 @@ class MantraRenderDeadlinePluginInfo(): @attr.s class VrayRenderPluginInfo(): InputFilename = attr.ib(default=None) + SeparateFilesPerFrame = attr.ib(default=True) class HoudiniSubmitDeadline( From fb4567560ec9973aab354c566bd1e00674190e5b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 30 Aug 2023 14:53:04 +0200 Subject: [PATCH 0027/1233] colorspace: aggregating typed config context data --- openpype/scripts/ocio_wrapper.py | 36 +++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/openpype/scripts/ocio_wrapper.py b/openpype/scripts/ocio_wrapper.py index 16558642c6..e8a503e42e 100644 --- a/openpype/scripts/ocio_wrapper.py +++ b/openpype/scripts/ocio_wrapper.py @@ -96,11 +96,41 @@ def _get_colorspace_data(config_path): config = ocio.Config().CreateFromFile(str(config_path)) - return { - c.getName(): c.getFamily() - for c in config.getColorSpaces() + colorspace_data = { + color.getName(): { + "type": "colorspace", + "family": color.getFamily(), + "categories": list(color.getCategories()), + "aliases": list(color.getAliases()), + "equalitygroup": color.getEqualityGroup(), + } + for color in config.getColorSpaces() } + # add looks + looks = config.getLooks() + if looks: + colorspace_data.update({ + look.getName(): { + "type": "look", + "process_space": look.getProcessSpace() + } + for look in looks + }) + + # add roles + roles = config.getRoles() + if roles: + colorspace_data.update({ + role[0]: { + "type": "role", + "colorspace": role[1] + } + for role in roles + }) + + return colorspace_data + @config.command( name="get_views", From 3cc8c51ea2cb4bf9655bf3ae9cd5f53befb84b95 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 30 Aug 2023 14:53:58 +0200 Subject: [PATCH 0028/1233] traypublish: adding colorspace look product type publishing workflow --- .../plugins/create/create_colorspace_look.py | 185 ++++++++++++++++++ .../publish/collect_colorspace_look.py | 46 +++++ .../publish/extract_colorspace_look.py | 43 ++++ .../publish/validate_colorspace_look.py | 70 +++++++ openpype/plugins/publish/integrate.py | 1 + 5 files changed, 345 insertions(+) create mode 100644 openpype/hosts/traypublisher/plugins/create/create_colorspace_look.py create mode 100644 openpype/hosts/traypublisher/plugins/publish/collect_colorspace_look.py create mode 100644 openpype/hosts/traypublisher/plugins/publish/extract_colorspace_look.py create mode 100644 openpype/hosts/traypublisher/plugins/publish/validate_colorspace_look.py diff --git a/openpype/hosts/traypublisher/plugins/create/create_colorspace_look.py b/openpype/hosts/traypublisher/plugins/create/create_colorspace_look.py new file mode 100644 index 0000000000..62ecc391f6 --- /dev/null +++ b/openpype/hosts/traypublisher/plugins/create/create_colorspace_look.py @@ -0,0 +1,185 @@ +# -*- coding: utf-8 -*- +"""Creator of colorspace look files. + +This creator is used to publish colorspace look files thanks to +production type `ociolook`. All files are published as representation. +""" +from pathlib import Path + +from openpype.client import get_asset_by_name +from openpype.lib.attribute_definitions import ( + FileDef, EnumDef, TextDef, UISeparatorDef +) +from openpype.pipeline import ( + CreatedInstance, + CreatorError +) +from openpype.pipeline.create import ( + get_subset_name, + TaskNotSetError, +) +from openpype.pipeline import colorspace +from openpype.hosts.traypublisher.api.plugin import TrayPublishCreator + + +class CreateColorspaceLook(TrayPublishCreator): + """Creates colorspace look files.""" + + identifier = "io.openpype.creators.traypublisher.colorspace_look" + label = "Colorspace Look" + family = "ociolook" + description = "Publishes color space look file." + extensions = [".cc", ".cube", ".3dl", ".spi1d", ".spi3d", ".csp", ".lut"] + enabled = False + + colorspace_items = [ + (None, "Not set") + ] + colorspace_attr_show = False + + def get_detail_description(self): + return """# Colorspace Look + +This creator publishes color space look file (LUT). + """ + + def get_icon(self): + return "mdi.format-color-fill" + + def create(self, subset_name, instance_data, pre_create_data): + repr_file = pre_create_data.get("luts_file") + if not repr_file: + raise CreatorError("No files specified") + + files = repr_file.get("filenames") + if not files: + # this should never happen + raise CreatorError("Missing files from representation") + + asset_doc = get_asset_by_name( + self.project_name, instance_data["asset"]) + + subset_name = self._get_subset( + asset_doc, instance_data["variant"], self.project_name, + instance_data["task"] + ) + + instance_data["creator_attributes"] = { + "abs_lut_path": ( + Path(repr_file["directory"]) / files[0]).as_posix() + } + + # Create new instance + new_instance = CreatedInstance(self.family, subset_name, + instance_data, self) + self._store_new_instance(new_instance) + + def get_instance_attr_defs(self): + return [ + EnumDef( + "working_colorspace", + self.colorspace_items, + default="Not set", + label="Working Colorspace", + ), + UISeparatorDef( + label="Advanced1" + ), + TextDef( + "abs_lut_path", + label="LUT Path", + ), + EnumDef( + "input_colorspace", + self.colorspace_items, + default="Not set", + label="Input Colorspace", + ), + EnumDef( + "direction", + [ + (None, "Not set"), + ("forward", "Forward"), + ("inverse", "Inverse") + ], + default="Not set", + label="Direction" + ), + EnumDef( + "interpolation", + [ + (None, "Not set"), + ("linear", "Linear"), + ("tetrahedral", "Tetrahedral"), + ("best", "Best"), + ("nearest", "Nearest") + ], + default="Not set", + label="Interpolation" + ), + EnumDef( + "output_colorspace", + self.colorspace_items, + default="Not set", + label="Output Colorspace", + ), + ] + + def get_pre_create_attr_defs(self): + return [ + FileDef( + "luts_file", + folders=False, + extensions=self.extensions, + allow_sequences=False, + single_item=True, + label="Look Files", + ) + ] + + def apply_settings(self, project_settings, system_settings): + host = self.create_context.host + host_name = host.name + project_name = host.get_current_project_name() + config_data = colorspace.get_imageio_config( + project_name, host_name, + project_settings=project_settings + ) + + if config_data: + filepath = config_data["path"] + config_items = colorspace.get_ocio_config_colorspaces(filepath) + + self.colorspace_items.extend(( + (name, f"{name} [{data_['type']}]") + for name, data_ in config_items.items() + if data_.get("type") == "colorspace" + )) + self.enabled = True + + def _get_subset(self, asset_doc, variant, project_name, task_name=None): + """Create subset name according to standard template process""" + + try: + subset_name = get_subset_name( + self.family, + variant, + task_name, + asset_doc, + project_name + ) + except TaskNotSetError: + # Create instance with fake task + # - instance will be marked as invalid so it can't be published + # but user have ability to change it + # NOTE: This expect that there is not task 'Undefined' on asset + task_name = "Undefined" + subset_name = get_subset_name( + self.family, + variant, + task_name, + asset_doc, + project_name + ) + + return subset_name diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_colorspace_look.py b/openpype/hosts/traypublisher/plugins/publish/collect_colorspace_look.py new file mode 100644 index 0000000000..739ab33f9c --- /dev/null +++ b/openpype/hosts/traypublisher/plugins/publish/collect_colorspace_look.py @@ -0,0 +1,46 @@ +import os +import pyblish.api +from openpype.pipeline import publish + + +class CollectColorspaceLook(pyblish.api.InstancePlugin, + publish.OpenPypePyblishPluginMixin): + """Collect OCIO colorspace look from LUT file + """ + + label = "Collect Colorspace Look" + order = pyblish.api.CollectorOrder + hosts = ["traypublisher"] + families = ["ociolook"] + + def process(self, instance): + creator_attrs = instance.data["creator_attributes"] + + lut_repre_name = "LUTfile" + file_url = creator_attrs["abs_lut_path"] + file_name = os.path.basename(file_url) + _, ext = os.path.splitext(file_name) + + # create lut representation data + lut_repre = { + "name": lut_repre_name, + "ext": ext.lstrip("."), + "files": file_name, + "stagingDir": os.path.dirname(file_url), + "tags": [] + } + instance.data.update({ + "representations": [lut_repre], + "source": file_url, + "ocioLookItems": [ + { + "name": lut_repre_name, + "ext": ext.lstrip("."), + "working_colorspace": creator_attrs["working_colorspace"], + "input_colorspace": creator_attrs["input_colorspace"], + "output_colorspace": creator_attrs["output_colorspace"], + "direction": creator_attrs["direction"], + "interpolation": creator_attrs["interpolation"] + } + ] + }) diff --git a/openpype/hosts/traypublisher/plugins/publish/extract_colorspace_look.py b/openpype/hosts/traypublisher/plugins/publish/extract_colorspace_look.py new file mode 100644 index 0000000000..ffd877af1d --- /dev/null +++ b/openpype/hosts/traypublisher/plugins/publish/extract_colorspace_look.py @@ -0,0 +1,43 @@ +import os +import json +import pyblish.api +from openpype.pipeline import publish + + +class ExtractColorspaceLook(publish.Extractor, + publish.OpenPypePyblishPluginMixin): + """Extract OCIO colorspace look from LUT file + """ + + label = "Extract Colorspace Look" + order = pyblish.api.ExtractorOrder + hosts = ["traypublisher"] + families = ["ociolook"] + + def process(self, instance): + ociolook_items = instance.data["ocioLookItems"] + staging_dir = self.staging_dir(instance) + + # create ociolook file attributes + ociolook_file_name = "ocioLookFile.json" + ociolook_file_content = { + "version": 1, + "data": { + "ocioLookItems": ociolook_items + } + } + + # write ociolook content into json file saved in staging dir + file_url = os.path.join(staging_dir, ociolook_file_name) + with open(file_url, "w") as f_: + json.dump(ociolook_file_content, f_, indent=4) + + # create lut representation data + ociolook_repre = { + "name": "ocioLookFile", + "ext": "json", + "files": ociolook_file_name, + "stagingDir": staging_dir, + "tags": [] + } + instance.data["representations"].append(ociolook_repre) diff --git a/openpype/hosts/traypublisher/plugins/publish/validate_colorspace_look.py b/openpype/hosts/traypublisher/plugins/publish/validate_colorspace_look.py new file mode 100644 index 0000000000..7de8881321 --- /dev/null +++ b/openpype/hosts/traypublisher/plugins/publish/validate_colorspace_look.py @@ -0,0 +1,70 @@ +import pyblish.api + +from openpype.pipeline import ( + publish, + PublishValidationError +) + + +class ValidateColorspaceLook(pyblish.api.InstancePlugin, + publish.OpenPypePyblishPluginMixin): + """Validate colorspace look attributes""" + + label = "Validate colorspace look attributes" + order = pyblish.api.ValidatorOrder + hosts = ["traypublisher"] + families = ["ociolook"] + + def process(self, instance): + create_context = instance.context.data["create_context"] + created_instance = create_context.get_instance_by_id( + instance.data["instance_id"]) + creator_defs = created_instance.creator_attribute_defs + + ociolook_items = instance.data.get("ocioLookItems", []) + + for ociolook_item in ociolook_items: + self.validate_colorspace_set_attrs(ociolook_item, creator_defs) + + def validate_colorspace_set_attrs(self, ociolook_item, creator_defs): + """Validate colorspace look attributes""" + + self.log.debug(f"Validate colorspace look attributes: {ociolook_item}") + self.log.debug(f"Creator defs: {creator_defs}") + + check_keys = [ + "working_colorspace", + "input_colorspace", + "output_colorspace", + "direction", + "interpolation" + ] + not_set_keys = [] + for key in check_keys: + if ociolook_item[key]: + # key is set and it is correct + continue + + def_label = next( + (d_.label for d_ in creator_defs if key == d_.key), + None + ) + if not def_label: + def_attrs = [(d_.key, d_.label) for d_ in creator_defs] + # raise since key is not recognized by creator defs + raise KeyError( + f"Colorspace look attribute '{key}' is not " + f"recognized by creator attributes: {def_attrs}" + ) + not_set_keys.append(def_label) + + if not_set_keys: + message = ( + f"Colorspace look attributes are not set: " + f"{', '.join(not_set_keys)}" + ) + raise PublishValidationError( + title="Colorspace Look attributes", + message=message, + description=message + ) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index be07cffe72..fe4bfc81f6 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -107,6 +107,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): "rig", "plate", "look", + "ociolook", "audio", "yetiRig", "yeticache", From 7727a017da8e0a27debae5dd5c0a3140adab3803 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 30 Aug 2023 14:57:22 +0200 Subject: [PATCH 0029/1233] filtering families into explicit colorspace --- .../traypublisher/plugins/publish/collect_explicit_colorspace.py | 1 + .../hosts/traypublisher/plugins/publish/validate_colorspace.py | 1 + 2 files changed, 2 insertions(+) diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_explicit_colorspace.py b/openpype/hosts/traypublisher/plugins/publish/collect_explicit_colorspace.py index eb7fbd87a0..860c36ccf8 100644 --- a/openpype/hosts/traypublisher/plugins/publish/collect_explicit_colorspace.py +++ b/openpype/hosts/traypublisher/plugins/publish/collect_explicit_colorspace.py @@ -13,6 +13,7 @@ class CollectColorspace(pyblish.api.InstancePlugin, label = "Choose representation colorspace" order = pyblish.api.CollectorOrder + 0.49 hosts = ["traypublisher"] + families = ["render", "plate", "reference", "image", "online"] colorspace_items = [ (None, "Don't override") diff --git a/openpype/hosts/traypublisher/plugins/publish/validate_colorspace.py b/openpype/hosts/traypublisher/plugins/publish/validate_colorspace.py index 75b41cf606..03f9f299b2 100644 --- a/openpype/hosts/traypublisher/plugins/publish/validate_colorspace.py +++ b/openpype/hosts/traypublisher/plugins/publish/validate_colorspace.py @@ -18,6 +18,7 @@ class ValidateColorspace(pyblish.api.InstancePlugin, label = "Validate representation colorspace" order = pyblish.api.ValidatorOrder hosts = ["traypublisher"] + families = ["render", "plate", "reference", "image", "online"] def process(self, instance): From 74ca6a3c44071035e9c3ef5a1ae22cd077647cee Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 30 Aug 2023 14:58:15 +0200 Subject: [PATCH 0030/1233] explicit colorspace includes types and aliases to offered colorspace --- .../publish/collect_explicit_colorspace.py | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_explicit_colorspace.py b/openpype/hosts/traypublisher/plugins/publish/collect_explicit_colorspace.py index 860c36ccf8..be8cf20e22 100644 --- a/openpype/hosts/traypublisher/plugins/publish/collect_explicit_colorspace.py +++ b/openpype/hosts/traypublisher/plugins/publish/collect_explicit_colorspace.py @@ -1,5 +1,4 @@ import pyblish.api -from openpype.pipeline import registered_host from openpype.pipeline import publish from openpype.lib import EnumDef from openpype.pipeline import colorspace @@ -38,7 +37,7 @@ class CollectColorspace(pyblish.api.InstancePlugin, @classmethod def apply_settings(cls, project_settings): - host = registered_host() + host = self.create_context.host host_name = host.name project_name = host.get_current_project_name() config_data = colorspace.get_imageio_config( @@ -49,9 +48,28 @@ class CollectColorspace(pyblish.api.InstancePlugin, if config_data: filepath = config_data["path"] config_items = colorspace.get_ocio_config_colorspaces(filepath) + aliases = set() + for _, value_ in config_items.items(): + if value_.get("type") != "colorspace": + continue + if not value_.get("aliases"): + continue + for alias in value_.get("aliases"): + aliases.add(alias) + + colorspaces = { + name + for name, data_ in config_items.items() + if name not in aliases and data_.get("type") == "colorspace" + } + cls.colorspace_items.extend(( - (name, name) for name in config_items.keys() + (name, name) for name in colorspaces )) + if aliases: + cls.colorspace_items.extend(( + (name, name) for name in aliases + )) cls.colorspace_attr_show = True @classmethod From a10206c05493bee85ca2c2bf92818bbb1fb3bdc1 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 30 Aug 2023 15:24:56 +0200 Subject: [PATCH 0031/1233] explicit colorspace switch and item labeling --- .../publish/collect_explicit_colorspace.py | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_explicit_colorspace.py b/openpype/hosts/traypublisher/plugins/publish/collect_explicit_colorspace.py index be8cf20e22..08479b8363 100644 --- a/openpype/hosts/traypublisher/plugins/publish/collect_explicit_colorspace.py +++ b/openpype/hosts/traypublisher/plugins/publish/collect_explicit_colorspace.py @@ -1,5 +1,8 @@ import pyblish.api -from openpype.pipeline import publish +from openpype.pipeline import ( + publish, + registered_host +) from openpype.lib import EnumDef from openpype.pipeline import colorspace @@ -13,6 +16,7 @@ class CollectColorspace(pyblish.api.InstancePlugin, order = pyblish.api.CollectorOrder + 0.49 hosts = ["traypublisher"] families = ["render", "plate", "reference", "image", "online"] + enabled = False colorspace_items = [ (None, "Don't override") @@ -37,7 +41,7 @@ class CollectColorspace(pyblish.api.InstancePlugin, @classmethod def apply_settings(cls, project_settings): - host = self.create_context.host + host = registered_host() host_name = host.name project_name = host.get_current_project_name() config_data = colorspace.get_imageio_config( @@ -46,6 +50,7 @@ class CollectColorspace(pyblish.api.InstancePlugin, ) if config_data: + filepath = config_data["path"] config_items = colorspace.get_ocio_config_colorspaces(filepath) aliases = set() @@ -58,19 +63,18 @@ class CollectColorspace(pyblish.api.InstancePlugin, aliases.add(alias) colorspaces = { - name - for name, data_ in config_items.items() - if name not in aliases and data_.get("type") == "colorspace" + name for name, data_ in config_items.items() + if data_.get("type") == "colorspace" } cls.colorspace_items.extend(( - (name, name) for name in colorspaces + (name, f"{name} [colorspace]") for name in colorspaces )) if aliases: cls.colorspace_items.extend(( - (name, name) for name in aliases + (name, f"{name} [alias]") for name in aliases )) - cls.colorspace_attr_show = True + cls.enabled = True @classmethod def get_attribute_defs(cls): @@ -79,7 +83,6 @@ class CollectColorspace(pyblish.api.InstancePlugin, "colorspace", cls.colorspace_items, default="Don't override", - label="Override Colorspace", - hidden=not cls.colorspace_attr_show + label="Override Colorspace" ) ] From 265dee372e8f4b99705fe7dbc5f6463241a433b6 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 4 Sep 2023 12:20:53 +0200 Subject: [PATCH 0032/1233] Blender: Change logs to `debug` in favor of new publisher artist-facing report Note that Blender is still using old publisher currently --- .../hosts/blender/plugins/publish/extract_abc.py | 2 +- .../blender/plugins/publish/extract_abc_animation.py | 2 +- .../hosts/blender/plugins/publish/extract_blend.py | 2 +- .../plugins/publish/extract_blend_animation.py | 2 +- .../blender/plugins/publish/extract_camera_abc.py | 2 +- .../blender/plugins/publish/extract_camera_fbx.py | 2 +- .../hosts/blender/plugins/publish/extract_fbx.py | 2 +- .../blender/plugins/publish/extract_fbx_animation.py | 2 +- .../hosts/blender/plugins/publish/extract_layout.py | 2 +- .../blender/plugins/publish/extract_playblast.py | 12 +++++------- .../blender/plugins/publish/extract_thumbnail.py | 6 +++--- 11 files changed, 17 insertions(+), 19 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/extract_abc.py b/openpype/hosts/blender/plugins/publish/extract_abc.py index f4babc94d3..f5b4c664a3 100644 --- a/openpype/hosts/blender/plugins/publish/extract_abc.py +++ b/openpype/hosts/blender/plugins/publish/extract_abc.py @@ -24,7 +24,7 @@ class ExtractABC(publish.Extractor): context = bpy.context # Perform extraction - self.log.info("Performing extraction..") + self.log.debug("Performing extraction..") plugin.deselect_all() diff --git a/openpype/hosts/blender/plugins/publish/extract_abc_animation.py b/openpype/hosts/blender/plugins/publish/extract_abc_animation.py index e141ccaa44..793e460390 100644 --- a/openpype/hosts/blender/plugins/publish/extract_abc_animation.py +++ b/openpype/hosts/blender/plugins/publish/extract_abc_animation.py @@ -23,7 +23,7 @@ class ExtractAnimationABC(publish.Extractor): context = bpy.context # Perform extraction - self.log.info("Performing extraction..") + self.log.debug("Performing extraction..") plugin.deselect_all() diff --git a/openpype/hosts/blender/plugins/publish/extract_blend.py b/openpype/hosts/blender/plugins/publish/extract_blend.py index d4f26b4f3c..c8eeef7fd7 100644 --- a/openpype/hosts/blender/plugins/publish/extract_blend.py +++ b/openpype/hosts/blender/plugins/publish/extract_blend.py @@ -21,7 +21,7 @@ class ExtractBlend(publish.Extractor): filepath = os.path.join(stagingdir, filename) # Perform extraction - self.log.info("Performing extraction..") + self.log.debug("Performing extraction..") data_blocks = set() diff --git a/openpype/hosts/blender/plugins/publish/extract_blend_animation.py b/openpype/hosts/blender/plugins/publish/extract_blend_animation.py index 477411b73d..661cecce81 100644 --- a/openpype/hosts/blender/plugins/publish/extract_blend_animation.py +++ b/openpype/hosts/blender/plugins/publish/extract_blend_animation.py @@ -21,7 +21,7 @@ class ExtractBlendAnimation(publish.Extractor): filepath = os.path.join(stagingdir, filename) # Perform extraction - self.log.info("Performing extraction..") + self.log.debug("Performing extraction..") data_blocks = set() diff --git a/openpype/hosts/blender/plugins/publish/extract_camera_abc.py b/openpype/hosts/blender/plugins/publish/extract_camera_abc.py index a21a59b151..185259d987 100644 --- a/openpype/hosts/blender/plugins/publish/extract_camera_abc.py +++ b/openpype/hosts/blender/plugins/publish/extract_camera_abc.py @@ -24,7 +24,7 @@ class ExtractCameraABC(publish.Extractor): context = bpy.context # Perform extraction - self.log.info("Performing extraction..") + self.log.debug("Performing extraction..") plugin.deselect_all() diff --git a/openpype/hosts/blender/plugins/publish/extract_camera_fbx.py b/openpype/hosts/blender/plugins/publish/extract_camera_fbx.py index 315994140e..a541f5b375 100644 --- a/openpype/hosts/blender/plugins/publish/extract_camera_fbx.py +++ b/openpype/hosts/blender/plugins/publish/extract_camera_fbx.py @@ -21,7 +21,7 @@ class ExtractCamera(publish.Extractor): filepath = os.path.join(stagingdir, filename) # Perform extraction - self.log.info("Performing extraction..") + self.log.debug("Performing extraction..") plugin.deselect_all() diff --git a/openpype/hosts/blender/plugins/publish/extract_fbx.py b/openpype/hosts/blender/plugins/publish/extract_fbx.py index 0ad797c226..f2ce117dcd 100644 --- a/openpype/hosts/blender/plugins/publish/extract_fbx.py +++ b/openpype/hosts/blender/plugins/publish/extract_fbx.py @@ -22,7 +22,7 @@ class ExtractFBX(publish.Extractor): filepath = os.path.join(stagingdir, filename) # Perform extraction - self.log.info("Performing extraction..") + self.log.debug("Performing extraction..") plugin.deselect_all() diff --git a/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py b/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py index 062b42e99d..5fe5931e65 100644 --- a/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py +++ b/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py @@ -23,7 +23,7 @@ class ExtractAnimationFBX(publish.Extractor): stagingdir = self.staging_dir(instance) # Perform extraction - self.log.info("Performing extraction..") + self.log.debug("Performing extraction..") # The first collection object in the instance is taken, as there # should be only one that contains the asset group. diff --git a/openpype/hosts/blender/plugins/publish/extract_layout.py b/openpype/hosts/blender/plugins/publish/extract_layout.py index f2d04f1178..05f86b8370 100644 --- a/openpype/hosts/blender/plugins/publish/extract_layout.py +++ b/openpype/hosts/blender/plugins/publish/extract_layout.py @@ -117,7 +117,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"] = [] diff --git a/openpype/hosts/blender/plugins/publish/extract_playblast.py b/openpype/hosts/blender/plugins/publish/extract_playblast.py index 196e75b8cc..b0099cce85 100644 --- a/openpype/hosts/blender/plugins/publish/extract_playblast.py +++ b/openpype/hosts/blender/plugins/publish/extract_playblast.py @@ -24,9 +24,7 @@ class ExtractPlayblast(publish.Extractor): order = pyblish.api.ExtractorOrder + 0.01 def process(self, instance): - self.log.info("Extracting capture..") - - self.log.info(instance.data) + self.log.debug("Extracting capture..") # get scene fps fps = instance.data.get("fps") @@ -34,14 +32,14 @@ class ExtractPlayblast(publish.Extractor): fps = bpy.context.scene.render.fps instance.data["fps"] = fps - self.log.info(f"fps: {fps}") + self.log.debug(f"fps: {fps}") # If start and end frames cannot be determined, # get them from Blender timeline. start = instance.data.get("frameStart", bpy.context.scene.frame_start) end = instance.data.get("frameEnd", bpy.context.scene.frame_end) - self.log.info(f"start: {start}, end: {end}") + self.log.debug(f"start: {start}, end: {end}") assert end > start, "Invalid time range !" # get cameras @@ -55,7 +53,7 @@ class ExtractPlayblast(publish.Extractor): filename = instance.name path = os.path.join(stagingdir, filename) - self.log.info(f"Outputting images to {path}") + self.log.debug(f"Outputting images to {path}") project_settings = instance.context.data["project_settings"]["blender"] presets = project_settings["publish"]["ExtractPlayblast"]["presets"] @@ -100,7 +98,7 @@ class ExtractPlayblast(publish.Extractor): frame_collection = collections[0] - self.log.info(f"We found collection of interest {frame_collection}") + self.log.debug(f"Found collection of interest {frame_collection}") instance.data.setdefault("representations", []) diff --git a/openpype/hosts/blender/plugins/publish/extract_thumbnail.py b/openpype/hosts/blender/plugins/publish/extract_thumbnail.py index 65c3627375..52e5d98fc4 100644 --- a/openpype/hosts/blender/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/blender/plugins/publish/extract_thumbnail.py @@ -24,13 +24,13 @@ class ExtractThumbnail(publish.Extractor): presets = {} def process(self, instance): - self.log.info("Extracting capture..") + self.log.debug("Extracting capture..") stagingdir = self.staging_dir(instance) filename = instance.name path = os.path.join(stagingdir, filename) - self.log.info(f"Outputting images to {path}") + self.log.debug(f"Outputting images to {path}") camera = instance.data.get("review_camera", "AUTO") start = instance.data.get("frameStart", bpy.context.scene.frame_start) @@ -61,7 +61,7 @@ class ExtractThumbnail(publish.Extractor): thumbnail = os.path.basename(self._fix_output_path(path)) - self.log.info(f"thumbnail: {thumbnail}") + self.log.debug(f"thumbnail: {thumbnail}") instance.data.setdefault("representations", []) From eadce2ce29f0df9da2a1b1c740e10bed4fa8a95e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 4 Sep 2023 12:22:24 +0200 Subject: [PATCH 0033/1233] Fusion: Create Saver fix redeclaration of `default_variants` --- 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 04898d0a45..27ff590cb3 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 5d221cf476000483a307fa212dd0d1b5ce0c618e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 4 Sep 2023 12:23:17 +0200 Subject: [PATCH 0034/1233] Fusion: Fix saver being created in incorrect state without saving directly after create Without this fix creating a saver, then resetting the publisher will leave the saver in a broken imprinted state and will not be found by the publisher --- openpype/hosts/fusion/plugins/create/create_saver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index 27ff590cb3..81dd235242 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -69,8 +69,6 @@ class CreateSaver(NewCreator): # TODO Is this needed? saver[file_format]["SaveAlpha"] = 1 - self._imprint(saver, instance_data) - # Register the CreatedInstance instance = CreatedInstance( family=self.family, @@ -78,6 +76,8 @@ class CreateSaver(NewCreator): data=instance_data, creator=self, ) + data = instance.data_to_store() + self._imprint(saver, data) # Insert the transient data instance.transient_data["tool"] = saver From 56147ca26e5f994e02c4568d75be8bfcaf9fe2cb Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 4 Sep 2023 12:23:41 +0200 Subject: [PATCH 0035/1233] Fusion: Allow reset frame range on `render` family --- openpype/hosts/fusion/plugins/load/actions.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/fusion/plugins/load/actions.py b/openpype/hosts/fusion/plugins/load/actions.py index f83ab433ee..94ba361b50 100644 --- a/openpype/hosts/fusion/plugins/load/actions.py +++ b/openpype/hosts/fusion/plugins/load/actions.py @@ -11,6 +11,7 @@ class FusionSetFrameRangeLoader(load.LoaderPlugin): families = ["animation", "camera", "imagesequence", + "render", "yeticache", "pointcache", "render"] @@ -46,6 +47,7 @@ class FusionSetFrameRangeWithHandlesLoader(load.LoaderPlugin): families = ["animation", "camera", "imagesequence", + "render", "yeticache", "pointcache", "render"] From 2e6bdad7a0ede6c1445ef4e154df14cfbd355f40 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 4 Sep 2023 12:24:13 +0200 Subject: [PATCH 0036/1233] Fusion: Tweak logging level for artist-facing report --- 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 5eddd0f601b8cf61c83267bca031832af622b0c1 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 4 Sep 2023 17:21:44 +0200 Subject: [PATCH 0037/1233] Tweak code for readability --- .../publish/validator_saver_resolution.py | 148 +++++++++--------- 1 file changed, 77 insertions(+), 71 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/validator_saver_resolution.py b/openpype/hosts/fusion/plugins/publish/validator_saver_resolution.py index 43e2ea5093..0dac040c7f 100644 --- a/openpype/hosts/fusion/plugins/publish/validator_saver_resolution.py +++ b/openpype/hosts/fusion/plugins/publish/validator_saver_resolution.py @@ -5,10 +5,56 @@ from openpype.pipeline import ( ) from openpype.hosts.fusion.api.action import SelectInvalidAction -from openpype.hosts.fusion.api import ( - get_current_comp, - comp_lock_and_undo_chunk, -) +from openpype.hosts.fusion.api import comp_lock_and_undo_chunk + + +def get_tool_resolution(tool, frame): + """Return the 2D input resolution to a Fusion tool + + If the current tool hasn't been rendered its input resolution + hasn't been saved. To combat this, add an expression in + the comments field to read the resolution + + Args + tool (Fusion Tool): The tool to query input resolution + frame (int): The frame to query the resolution on. + + Returns: + tuple: width, height as 2-tuple of integers + + """ + comp = tool.Composition + + # False undo removes the undo-stack from the undo list + with comp_lock_and_undo_chunk(comp, "Read resolution", False): + # Save old comment + old_comment = "" + has_expression = False + if tool["Comments"][frame] != "": + if tool["Comments"].GetExpression() is not None: + has_expression = True + old_comment = tool["Comments"].GetExpression() + tool["Comments"].SetExpression(None) + else: + old_comment = tool["Comments"][frame] + tool["Comments"][frame] = "" + + # Get input width + tool["Comments"].SetExpression("self.Input.OriginalWidth") + width = int(tool["Comments"][frame]) + + # Get input height + tool["Comments"].SetExpression("self.Input.OriginalHeight") + height = int(tool["Comments"][frame]) + + # Reset old comment + tool["Comments"].SetExpression(None) + if has_expression: + tool["Comments"].SetExpression(old_comment) + else: + tool["Comments"][frame] = old_comment + + return width, height class ValidateSaverResolution( @@ -23,74 +69,34 @@ class ValidateSaverResolution( optional = True actions = [SelectInvalidAction] - @classmethod - def get_invalid(cls, instance, wrong_resolution=None): - """Validate if the saver rescive the expected resolution""" - if wrong_resolution is None: - wrong_resolution = [] - - saver = instance[0] - firstFrame = saver.GetAttrs("TOOLNT_Region_Start")[1] - comp = get_current_comp() - - # If the current saver hasn't bin rendered its input resolution - # hasn't bin saved. To combat this, add an expression in - # the comments field to read the resolution - - # False undo removes the undo-stack from the undo list - with comp_lock_and_undo_chunk(comp, "Read resolution", False): - # Save old comment - oldComment = "" - hasExpression = False - if saver["Comments"][firstFrame] != "": - if saver["Comments"].GetExpression() is not None: - hasExpression = True - oldComment = saver["Comments"].GetExpression() - saver["Comments"].SetExpression(None) - else: - oldComment = saver["Comments"][firstFrame] - saver["Comments"][firstFrame] = "" - - # Get input width - saver["Comments"].SetExpression("self.Input.OriginalWidth") - width = int(saver["Comments"][firstFrame]) - - # Get input height - saver["Comments"].SetExpression("self.Input.OriginalHeight") - height = int(saver["Comments"][firstFrame]) - - # Reset old comment - saver["Comments"].SetExpression(None) - if hasExpression: - saver["Comments"].SetExpression(oldComment) - else: - saver["Comments"][firstFrame] = oldComment - - # Time to compare! - wrong_resolution.append("{}x{}".format(width, height)) - entityData = instance.data["assetEntity"]["data"] - if entityData["resolutionWidth"] != width: - return [saver] - if entityData["resolutionHeight"] != height: - return [saver] - - return [] - def process(self, instance): - if not self.is_active(instance.data): - return - - wrong_resolution = [] - invalid = self.get_invalid(instance, wrong_resolution) - if invalid: - entityData = instance.data["assetEntity"]["data"] + resolution = self.get_resolution(instance) + expected_resolution = self.get_expected_resolution(instance) + if resolution != expected_resolution: raise PublishValidationError( - "The input's resolution does not match" - " the asset's resolution of {}x{}.\n\n" + "The input's resolution does not match " + "the asset's resolution {}.\n\n" "The input's resolution is {}".format( - entityData["resolutionWidth"], - entityData["resolutionHeight"], - wrong_resolution[0], - ), - title=self.label, + expected_resolution, + resolution, + ) ) + + @classmethod + def get_invalid(cls, instance): + resolution = cls.get_resolution(instance) + expected_resolution = cls.get_expected_resolution(instance) + if resolution != expected_resolution: + saver = instance.data["tool"] + return [saver] + + @classmethod + def get_resolution(cls, instance): + saver = instance.data["tool"] + first_frame = saver.GetAttrs("TOOLNT_Region_Start")[1] + return get_tool_resolution(saver, frame=first_frame) + + @classmethod + def get_expected_resolution(cls, instance): + data = instance.data["assetEntity"]["data"] + return data["resolutionWidth"], data["resolutionHeight"] From 7b62a204a35bbd29ccbdfcc485020a63d1b26dee Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 4 Sep 2023 17:22:09 +0200 Subject: [PATCH 0038/1233] Fix optional support --- .../hosts/fusion/plugins/publish/validator_saver_resolution.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/fusion/plugins/publish/validator_saver_resolution.py b/openpype/hosts/fusion/plugins/publish/validator_saver_resolution.py index 0dac040c7f..1c38533cf9 100644 --- a/openpype/hosts/fusion/plugins/publish/validator_saver_resolution.py +++ b/openpype/hosts/fusion/plugins/publish/validator_saver_resolution.py @@ -70,6 +70,9 @@ class ValidateSaverResolution( actions = [SelectInvalidAction] def process(self, instance): + if not self.is_active(instance.data): + return + resolution = self.get_resolution(instance) expected_resolution = self.get_expected_resolution(instance) if resolution != expected_resolution: From 7a2cc22e6d2e4b949b78243a467547be63e56104 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 4 Sep 2023 17:24:22 +0200 Subject: [PATCH 0039/1233] Fix docstring --- .../hosts/fusion/plugins/publish/validator_saver_resolution.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/publish/validator_saver_resolution.py b/openpype/hosts/fusion/plugins/publish/validator_saver_resolution.py index 1c38533cf9..8317864fcf 100644 --- a/openpype/hosts/fusion/plugins/publish/validator_saver_resolution.py +++ b/openpype/hosts/fusion/plugins/publish/validator_saver_resolution.py @@ -60,7 +60,7 @@ def get_tool_resolution(tool, frame): class ValidateSaverResolution( pyblish.api.InstancePlugin, OptionalPyblishPluginMixin ): - """Validate that the saver input resolution matches the projects""" + """Validate that the saver input resolution matches the asset resolution""" order = pyblish.api.ValidatorOrder label = "Validate Saver Resolution" From c3e299f9adfd49b8f87a2be1a3ef58804cf22eb4 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 4 Sep 2023 17:26:13 +0200 Subject: [PATCH 0040/1233] Use `frameStartHandle` since we only care about the saver during the publish frame range --- .../hosts/fusion/plugins/publish/validator_saver_resolution.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/publish/validator_saver_resolution.py b/openpype/hosts/fusion/plugins/publish/validator_saver_resolution.py index 8317864fcf..bcc101ba16 100644 --- a/openpype/hosts/fusion/plugins/publish/validator_saver_resolution.py +++ b/openpype/hosts/fusion/plugins/publish/validator_saver_resolution.py @@ -96,7 +96,7 @@ class ValidateSaverResolution( @classmethod def get_resolution(cls, instance): saver = instance.data["tool"] - first_frame = saver.GetAttrs("TOOLNT_Region_Start")[1] + first_frame = instance.data["frameStartHandle"] return get_tool_resolution(saver, frame=first_frame) @classmethod From bed16ae663c7d4dbb7e4c32db72d7e5e53d277d4 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 4 Sep 2023 18:07:32 +0200 Subject: [PATCH 0041/1233] Match filename with other validators --- ...validator_saver_resolution.py => validate_saver_resolution.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename openpype/hosts/fusion/plugins/publish/{validator_saver_resolution.py => validate_saver_resolution.py} (100%) diff --git a/openpype/hosts/fusion/plugins/publish/validator_saver_resolution.py b/openpype/hosts/fusion/plugins/publish/validate_saver_resolution.py similarity index 100% rename from openpype/hosts/fusion/plugins/publish/validator_saver_resolution.py rename to openpype/hosts/fusion/plugins/publish/validate_saver_resolution.py From a50ab622b7968ec0bbf377f152db603b1cf1984c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 4 Sep 2023 18:08:59 +0200 Subject: [PATCH 0042/1233] Fix message readability --- .../fusion/plugins/publish/validate_saver_resolution.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/validate_saver_resolution.py b/openpype/hosts/fusion/plugins/publish/validate_saver_resolution.py index bcc101ba16..b43a5023fa 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_saver_resolution.py +++ b/openpype/hosts/fusion/plugins/publish/validate_saver_resolution.py @@ -78,10 +78,10 @@ class ValidateSaverResolution( if resolution != expected_resolution: raise PublishValidationError( "The input's resolution does not match " - "the asset's resolution {}.\n\n" - "The input's resolution is {}".format( - expected_resolution, - resolution, + "the asset's resolution {}x{}.\n\n" + "The input's resolution is {}x{}.".format( + expected_resolution[0], expected_resolution[1], + resolution[0], resolution[1] ) ) From 49e29116414e87b92732094e5a5cdf95a5d8c8d7 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 4 Sep 2023 20:25:50 +0200 Subject: [PATCH 0043/1233] Expose ValidateSaverResolution in settings --- .../defaults/project_settings/fusion.json | 7 +++++++ .../projects_schema/schema_project_fusion.json | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/openpype/settings/defaults/project_settings/fusion.json b/openpype/settings/defaults/project_settings/fusion.json index 0ee7d6127d..ab24727db5 100644 --- a/openpype/settings/defaults/project_settings/fusion.json +++ b/openpype/settings/defaults/project_settings/fusion.json @@ -27,5 +27,12 @@ "farm_rendering" ] } + }, + "publish": { + "ValidateSaverResolution": { + "enabled": true, + "optional": true, + "active": true + } } } diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json b/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json index 656c50dd98..342411f8a5 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json @@ -84,6 +84,24 @@ ] } ] + }, + { + "type": "dict", + "collapsible": true, + "key": "publish", + "label": "Publish plugins", + "children": [ + { + "type": "schema_template", + "name": "template_publish_plugin", + "template_data": [ + { + "key": "ValidateSaverResolution", + "label": "Validate Saver Resolution" + } + ] + } + ] } ] } From cd4adfefd27e51cd26422273b78d4f52e75d3f4d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 5 Sep 2023 14:34:48 +0200 Subject: [PATCH 0044/1233] Remove Publish GUI Filter settings from Maya since new publisher doesn't support it --- .../defaults/project_settings/maya.json | 9 ----- .../projects_schema/schema_project_maya.json | 4 -- server_addon/maya/server/settings/main.py | 37 +------------------ 3 files changed, 1 insertion(+), 49 deletions(-) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 38f14ec022..d6c1a886f8 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -1564,14 +1564,5 @@ }, "templated_workfile_build": { "profiles": [] - }, - "filters": { - "preset 1": { - "ValidateNoAnimation": false, - "ValidateShapeDefaultNames": false - }, - "preset 2": { - "ValidateNoAnimation": false - } } } diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json index dca955dab4..a6fd742b40 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json @@ -258,10 +258,6 @@ { "type": "schema", "name": "schema_templated_workfile_build" - }, - { - "type": "schema", - "name": "schema_publish_gui_filter" } ] } diff --git a/server_addon/maya/server/settings/main.py b/server_addon/maya/server/settings/main.py index c8021614be..f204401061 100644 --- a/server_addon/maya/server/settings/main.py +++ b/server_addon/maya/server/settings/main.py @@ -23,23 +23,6 @@ class ExtMappingItemModel(BaseSettingsModel): value: str = Field(title="Extension") -class PublishGUIFilterItemModel(BaseSettingsModel): - _layout = "compact" - name: str = Field(title="Name") - value: bool = Field(True, title="Active") - - -class PublishGUIFiltersModel(BaseSettingsModel): - _layout = "compact" - name: str = Field(title="Name") - value: list[PublishGUIFilterItemModel] = Field(default_factory=list) - - @validator("value") - def validate_unique_outputs(cls, value): - ensure_unique_names(value) - return value - - class MayaSettings(BaseSettingsModel): """Maya Project Settings.""" @@ -76,9 +59,6 @@ class MayaSettings(BaseSettingsModel): templated_workfile_build: TemplatedProfilesModel = Field( default_factory=TemplatedProfilesModel, title="Templated Workfile Build Settings") - filters: list[PublishGUIFiltersModel] = Field( - default_factory=list, - title="Publish GUI Filters") @validator("filters", "ext_mapping") def validate_unique_outputs(cls, value): @@ -122,20 +102,5 @@ DEFAULT_MAYA_SETTING = { "publish": DEFAULT_PUBLISH_SETTINGS, "load": DEFAULT_LOADERS_SETTING, "workfile_build": DEFAULT_WORKFILE_SETTING, - "templated_workfile_build": DEFAULT_TEMPLATED_WORKFILE_SETTINGS, - "filters": [ - { - "name": "preset 1", - "value": [ - {"name": "ValidateNoAnimation", "value": False}, - {"name": "ValidateShapeDefaultNames", "value": False}, - ] - }, - { - "name": "preset 2", - "value": [ - {"name": "ValidateNoAnimation", "value": False}, - ] - }, - ] + "templated_workfile_build": DEFAULT_TEMPLATED_WORKFILE_SETTINGS } From 15631e7ba85c6aeef7defddae103805bc780ccb3 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 5 Sep 2023 14:35:25 +0200 Subject: [PATCH 0045/1233] Bump Ayon Maya server addon version due to new settings definition --- server_addon/maya/server/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/maya/server/version.py b/server_addon/maya/server/version.py index e57ad00718..de699158fd 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.3" +__version__ = "0.1.4" From 412b6b480d45434dc6895dae9b7efec2df421a85 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 5 Sep 2023 14:35:58 +0200 Subject: [PATCH 0046/1233] Remove Publish GUI Filter settings from Nuke since new publisher doesn't support it --- .../defaults/project_settings/nuke.json | 3 +-- .../projects_schema/schema_project_nuke.json | 4 ---- server_addon/nuke/server/settings/filters.py | 19 ------------------- server_addon/nuke/server/settings/main.py | 8 +------- 4 files changed, 2 insertions(+), 32 deletions(-) delete mode 100644 server_addon/nuke/server/settings/filters.py diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index b736c462ff..8b77251956 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -552,6 +552,5 @@ }, "templated_workfile_build": { "profiles": [] - }, - "filters": {} + } } diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json b/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json index 6b516ddf4a..0b24c8231c 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json @@ -291,10 +291,6 @@ { "type": "schema", "name": "schema_templated_workfile_build" - }, - { - "type": "schema", - "name": "schema_publish_gui_filter" } ] } diff --git a/server_addon/nuke/server/settings/filters.py b/server_addon/nuke/server/settings/filters.py deleted file mode 100644 index 7e2702b3b7..0000000000 --- a/server_addon/nuke/server/settings/filters.py +++ /dev/null @@ -1,19 +0,0 @@ -from pydantic import Field, validator -from ayon_server.settings import BaseSettingsModel, ensure_unique_names - - -class PublishGUIFilterItemModel(BaseSettingsModel): - _layout = "compact" - name: str = Field(title="Name") - value: bool = Field(True, title="Active") - - -class PublishGUIFiltersModel(BaseSettingsModel): - _layout = "compact" - name: str = Field(title="Name") - value: list[PublishGUIFilterItemModel] = Field(default_factory=list) - - @validator("value") - def validate_unique_outputs(cls, value): - ensure_unique_names(value) - return value diff --git a/server_addon/nuke/server/settings/main.py b/server_addon/nuke/server/settings/main.py index 4687d48ac9..f7ce2d044d 100644 --- a/server_addon/nuke/server/settings/main.py +++ b/server_addon/nuke/server/settings/main.py @@ -44,7 +44,6 @@ from .workfile_builder import ( from .templated_workfile_build import ( TemplatedWorkfileBuildModel ) -from .filters import PublishGUIFilterItemModel class NukeSettings(BaseSettingsModel): @@ -100,10 +99,6 @@ class NukeSettings(BaseSettingsModel): default_factory=TemplatedWorkfileBuildModel ) - filters: list[PublishGUIFilterItemModel] = Field( - default_factory=list - ) - @validator("filters") def ensure_unique_names(cls, value): """Ensure name fields within the lists have unique names.""" @@ -123,6 +118,5 @@ DEFAULT_VALUES = { "workfile_builder": DEFAULT_WORKFILE_BUILDER_SETTINGS, "templated_workfile_build": { "profiles": [] - }, - "filters": [] + } } From 7850fd01b037a49e4c00ddbc4c734da7e3ac6a9d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 5 Sep 2023 14:36:20 +0200 Subject: [PATCH 0047/1233] Bump Ayon Nuke server addon version due to new settings definition --- server_addon/nuke/server/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/nuke/server/version.py b/server_addon/nuke/server/version.py index b3f4756216..ae7362549b 100644 --- a/server_addon/nuke/server/version.py +++ b/server_addon/nuke/server/version.py @@ -1 +1 @@ -__version__ = "0.1.2" +__version__ = "0.1.3" From 684ce0fc7d4080d106a002f2cade2c038326c089 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 7 Sep 2023 17:40:01 +0200 Subject: [PATCH 0048/1233] :art: WIP on the creator --- .../plugins/create/create_multishot_layout.py | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 openpype/hosts/maya/plugins/create/create_multishot_layout.py diff --git a/openpype/hosts/maya/plugins/create/create_multishot_layout.py b/openpype/hosts/maya/plugins/create/create_multishot_layout.py new file mode 100644 index 0000000000..fbd7172ac4 --- /dev/null +++ b/openpype/hosts/maya/plugins/create/create_multishot_layout.py @@ -0,0 +1,36 @@ +from openpype.hosts.maya.api import plugin +from openpype.lib import BoolDef +from openpype import AYON_SERVER_ENABLED +from ayon_api import get_folder_by_name + + +class CreateMultishotLayout(plugin.MayaCreator): + """A grouped package of loaded content""" + + identifier = "io.openpype.creators.maya.multishotlayout" + label = "Multishot Layout" + family = "layout" + icon = "camera" + + def get_instance_attr_defs(self): + + return [ + BoolDef("groupLoadedAssets", + label="Group Loaded Assets", + tooltip="Enable this when you want to publish group of " + "loaded asset", + default=False) + ] + + def create(self, subset_name, instance_data, pre_create_data): + # TODO: get this needs to be switched to get_folder_by_path + # once the fork to pure AYON is done. + # WARNING: this will not work for projects where the asset name + # is not unique across the project until the switch mentioned + # above is done. + current_folder = get_folder_by_name(instance_data["asset"]) + + +# blast this creator if Ayon server is not enabled +if not AYON_SERVER_ENABLED: + del CreateMultishotLayout From 7f2e5e8fa9fa41f41914fb4f4d43048de0c7beb1 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 8 Sep 2023 18:49:12 +0200 Subject: [PATCH 0049/1233] :recycle: multishot layout creator WIP still need to add Task information to created layouts --- .../plugins/create/create_multishot_layout.py | 150 ++++++++++++++++-- 1 file changed, 138 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_multishot_layout.py b/openpype/hosts/maya/plugins/create/create_multishot_layout.py index fbd7172ac4..706203bdab 100644 --- a/openpype/hosts/maya/plugins/create/create_multishot_layout.py +++ b/openpype/hosts/maya/plugins/create/create_multishot_layout.py @@ -1,20 +1,68 @@ -from openpype.hosts.maya.api import plugin -from openpype.lib import BoolDef +from ayon_api import get_folder_by_name, get_folder_by_path, get_folders +from maya import cmds # noqa: F401 + from openpype import AYON_SERVER_ENABLED -from ayon_api import get_folder_by_name +from openpype.client import get_assets +from openpype.hosts.maya.api import plugin +from openpype.lib import BoolDef, EnumDef +from openpype.pipeline import ( + Creator, + get_current_asset_name, + get_current_project_name +) +from openpype.pipeline.create import CreatorError class CreateMultishotLayout(plugin.MayaCreator): - """A grouped package of loaded content""" + """Create a multishot layout in the Maya scene. + This creator will create a Camera Sequencer in the Maya scene based on + the shots found under the specified folder. The shots will be added to + the sequencer in the order of their clipIn and clipOut values. For each + shot a Layout will be created. + + """ identifier = "io.openpype.creators.maya.multishotlayout" label = "Multishot Layout" family = "layout" - icon = "camera" + icon = "project-diagram" - def get_instance_attr_defs(self): + def get_pre_create_attr_defs(self): + # Present artist with a list of parents of the current context + # to choose from. This will be used to get the shots under the + # selected folder to create the Camera Sequencer. + + """ + Todo: get this needs to be switched to get_folder_by_path + once the fork to pure AYON is done. + + Warning: this will not work for projects where the asset name + is not unique across the project until the switch mentioned + above is done. + """ + + current_folder = get_folder_by_name( + project_name=get_current_project_name(), + folder_name=get_current_asset_name(), + ) + + items_with_label = [ + dict(label=p if p != current_folder["name"] else f"{p} (current)", + value=str(p)) + for p in current_folder["path"].split("/") + ] + + items_with_label.insert(0, + dict(label=f"{self.project_name} " + "(shots directly under the project)", + value=None)) return [ + EnumDef("shotParent", + default=current_folder["name"], + label="Shot Parent Folder", + items=items_with_label, + ), BoolDef("groupLoadedAssets", label="Group Loaded Assets", tooltip="Enable this when you want to publish group of " @@ -23,12 +71,90 @@ class CreateMultishotLayout(plugin.MayaCreator): ] def create(self, subset_name, instance_data, pre_create_data): - # TODO: get this needs to be switched to get_folder_by_path - # once the fork to pure AYON is done. - # WARNING: this will not work for projects where the asset name - # is not unique across the project until the switch mentioned - # above is done. - current_folder = get_folder_by_name(instance_data["asset"]) + shots = self.get_related_shots( + folder_path=pre_create_data["shotParent"] + ) + if not shots: + # There are no shot folders under the specified folder. + # We are raising an error here but in the future we might + # want to create a new shot folders by publishing the layouts + # and shot defined in the sequencer. Sort of editorial publish + # in side of Maya. + raise CreatorError("No shots found under the specified folder.") + + # Get layout creator + layout_creator_id = "io.openpype.creators.maya.layout" + layout_creator: Creator = self.create_context.creators.get( + layout_creator_id) + + # Get OpenPype style asset documents for the shots + op_asset_docs = get_assets( + self.project_name, [s["id"] for s in shots]) + for shot in shots: + # we are setting shot name to be displayed in the sequencer to + # `shot name (shot label)` if the label is set, otherwise just + # `shot name`. So far, labels are used only when the name is set + # with characters that are not allowed in the shot name. + if not shot["active"]: + continue + + shot_name = f"{shot['name']}%s" % ( + f" ({shot['label']})" if shot["label"] else "") + cmds.shot(sst=shot["attrib"]["clipIn"], + set=shot["attrib"]["clipOut"], + shotName=shot_name) + + # Create layout instance by the layout creator + layout_creator.create( + subset_name=layout_creator.get_subset_name( + self.get_default_variant(), + self.create_context.get_current_task_name(), + next( + asset_doc for asset_doc in op_asset_docs + if asset_doc["_id"] == shot["id"] + ), + self.project_name), + instance_data={ + "asset": shot["name"], + }, + pre_create_data={ + "groupLoadedAssets": pre_create_data["groupLoadedAssets"] + } + ) + + def get_related_shots(self, folder_path: str): + """Get all shots related to the current asset. + + Get all folders of type Shot under specified folder. + + Args: + folder_path (str): Path of the folder. + + Returns: + list: List of dicts with folder data. + + """ + # if folder_path is None, project is selected as a root + # and its name is used as a parent id + parent_id = [self.project_name] + if folder_path: + current_folder = get_folder_by_path( + project_name=self.project_name, + folder_path=folder_path, + ) + parent_id = [current_folder["id"]] + + # get all child folders of the current one + child_folders = get_folders( + project_name=self.project_name, + parent_ids=parent_id, + fields=[ + "attrib.clipIn", "attrib.clipOut", + "attrib.frameStart", "attrib.frameEnd", + "name", "label", "path", "folderType", "id" + ] + ) + return [f for f in child_folders if f["folderType"] == "Shot"] # blast this creator if Ayon server is not enabled From 6c8e162fa86f995168fb69428510705b95e0e9e7 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 14 Sep 2023 19:27:36 +0200 Subject: [PATCH 0050/1233] :art: add settings --- server_addon/maya/server/settings/creators.py | 11 +++++++++++ server_addon/maya/server/version.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/server_addon/maya/server/settings/creators.py b/server_addon/maya/server/settings/creators.py index 11e2b8a36c..84e873589d 100644 --- a/server_addon/maya/server/settings/creators.py +++ b/server_addon/maya/server/settings/creators.py @@ -1,6 +1,7 @@ from pydantic import Field from ayon_server.settings import BaseSettingsModel +from ayon_server.settings import task_types_enum class CreateLookModel(BaseSettingsModel): @@ -120,6 +121,16 @@ class CreateVrayProxyModel(BaseSettingsModel): default_factory=list, title="Default Products") +class CreateMultishotLayout(BasicCreatorModel): + shotParent: str = Field(title="Shot Parent Folder") + groupLoadedAssets: bool = Field(title="Group Loaded Assets") + task_type: list[str] = Field( + title="Task types", + enum_resolver=task_types_enum + ) + task_name: str = Field(title="Task name (regex)") + + class CreatorsModel(BaseSettingsModel): CreateLook: CreateLookModel = Field( default_factory=CreateLookModel, diff --git a/server_addon/maya/server/version.py b/server_addon/maya/server/version.py index e57ad00718..de699158fd 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.3" +__version__ = "0.1.4" From f6de6d07bc7e59575c54b81b531d165fae822fbf Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 14 Sep 2023 21:01:14 +0300 Subject: [PATCH 0051/1233] add self publish button --- openpype/hosts/houdini/api/lib.py | 62 ++++++++++++++++++++++++++++ openpype/hosts/houdini/api/plugin.py | 3 +- 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 75c7ff9fee..755368616b 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -649,3 +649,65 @@ def get_color_management_preferences(): "display": hou.Color.ocio_defaultDisplay(), "view": hou.Color.ocio_defaultView() } + + +def publisher_show_and_publish(): + """Open publisher window and trigger publishing action. """ + + from openpype.tools.utils.host_tools import get_tool_by_name + + main_window = get_main_window() + publisher_window = get_tool_by_name( + tool_name="publisher", + parent=main_window + ) + + publisher_window.set_current_tab("publish") + publisher_window.make_sure_is_visible() + publisher_window._reset_on_show = False + + publisher_window._controller.reset() + publisher_window._controller.publish() + + +def self_publish(): + """Self publish from ROP nodes. """ + from openpype.pipeline import registered_host + from openpype.pipeline.create import CreateContext + + current_node = hou.node(".").path() + + host = registered_host() + context = CreateContext(host, reset=True) + + for instance in context.instances: + node_path = instance.data.get("instance_node") + if not node_path: + continue + print(node_path) + + if current_node == node_path: + instance["active"] = True + else: + instance["active"] = False + + context.save_changes() + publisher_show_and_publish() + +def add_self_publish_button(node): + """Adds a self publish button in the rop node. """ + label = os.environ.get("AVALON_LABEL") or "OpenPype" + + button_parm = hou.ButtonParmTemplate( + "{}_publish".format(label.lower()), + "{} Publish".format(label), + script_callback="from openpype.hosts.houdini.api.lib import " + "self_publish; self_publish()", + script_callback_language=hou.scriptLanguage.Python, + join_with_next=True + ) + + template = node.parmTemplateGroup() + template.insertBefore((0,), button_parm) + # parm_group.append(button_parm) + node.setParmTemplateGroup(template) diff --git a/openpype/hosts/houdini/api/plugin.py b/openpype/hosts/houdini/api/plugin.py index 730a627dc3..756b33f7f7 100644 --- a/openpype/hosts/houdini/api/plugin.py +++ b/openpype/hosts/houdini/api/plugin.py @@ -13,7 +13,7 @@ from openpype.pipeline import ( CreatedInstance ) from openpype.lib import BoolDef -from .lib import imprint, read, lsattr +from .lib import imprint, read, lsattr, add_self_publish_button class OpenPypeCreatorError(CreatorError): @@ -194,6 +194,7 @@ class HoudiniCreator(NewCreator, HoudiniCreatorBase): self) self._add_instance_to_context(instance) imprint(instance_node, instance.data_to_store()) + add_self_publish_button(instance_node) return instance except hou.Error as er: From dc065171f9452bae9994835e3bdcacdae12f431b Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 14 Sep 2023 21:16:17 +0300 Subject: [PATCH 0052/1233] resolve hound --- openpype/hosts/houdini/api/lib.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 755368616b..a91f319ae8 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -694,6 +694,7 @@ def self_publish(): context.save_changes() publisher_show_and_publish() + def add_self_publish_button(node): """Adds a self publish button in the rop node. """ label = os.environ.get("AVALON_LABEL") or "OpenPype" From 2d8034ae77bccb4dd54438dad023bd3b15e7e93d Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 14 Sep 2023 23:36:16 +0300 Subject: [PATCH 0053/1233] BigRoy's Comment --- openpype/hosts/houdini/api/lib.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index a91f319ae8..c175b32b83 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -10,8 +10,14 @@ import json import six from openpype.client import get_asset_by_name -from openpype.pipeline import get_current_project_name, get_current_asset_name +from openpype.pipeline import ( + get_current_project_name, + get_current_asset_name, + registered_host +) from openpype.pipeline.context_tools import get_current_project_asset +from openpype.tools.utils.host_tools import get_tool_by_name +from openpype.pipeline.create import CreateContext import hou @@ -652,9 +658,7 @@ def get_color_management_preferences(): def publisher_show_and_publish(): - """Open publisher window and trigger publishing action. """ - - from openpype.tools.utils.host_tools import get_tool_by_name + """Open publisher window and trigger publishing action.""" main_window = get_main_window() publisher_window = get_tool_by_name( @@ -671,9 +675,7 @@ def publisher_show_and_publish(): def self_publish(): - """Self publish from ROP nodes. """ - from openpype.pipeline import registered_host - from openpype.pipeline.create import CreateContext + """Self publish from ROP nodes.""" current_node = hou.node(".").path() @@ -684,19 +686,16 @@ def self_publish(): node_path = instance.data.get("instance_node") if not node_path: continue - print(node_path) - if current_node == node_path: - instance["active"] = True - else: - instance["active"] = False + instance["active"] = current_node == node_path context.save_changes() publisher_show_and_publish() def add_self_publish_button(node): - """Adds a self publish button in the rop node. """ + """Adds a self publish button in the rop node.""" + label = os.environ.get("AVALON_LABEL") or "OpenPype" button_parm = hou.ButtonParmTemplate( @@ -710,5 +709,4 @@ def add_self_publish_button(node): template = node.parmTemplateGroup() template.insertBefore((0,), button_parm) - # parm_group.append(button_parm) node.setParmTemplateGroup(template) From a4f46380657dc24924627dc877cfa59312d572b0 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 15 Sep 2023 10:35:50 +0300 Subject: [PATCH 0054/1233] add publish comment --- openpype/hosts/houdini/api/lib.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index c175b32b83..b6b551a592 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -687,7 +687,20 @@ def self_publish(): if not node_path: continue - instance["active"] = current_node == node_path + active = current_node == node_path + if not active: + continue + + instance["active"] = active + + result, comment = hou.ui.readInput( + "Add Publish Note", + buttons=("Ok", "Cancel"), + title="Publish Note", + close_choice=1 + ) + if not result: + instance.data["comment"] = comment context.save_changes() publisher_show_and_publish() From e48090757f6f7c9b88abc8779d16e5f26078db87 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 15 Sep 2023 12:15:15 +0300 Subject: [PATCH 0055/1233] fix bug - disable other instances --- openpype/hosts/houdini/api/lib.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index b6b551a592..7e346d7285 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -688,19 +688,17 @@ def self_publish(): continue active = current_node == node_path - if not active: - continue - instance["active"] = active - result, comment = hou.ui.readInput( - "Add Publish Note", - buttons=("Ok", "Cancel"), - title="Publish Note", - close_choice=1 - ) - if not result: - instance.data["comment"] = comment + if active: + result, comment = hou.ui.readInput( + "Add Publish Note", + buttons=("Ok", "Cancel"), + title="Publish Note", + close_choice=1 + ) + if not result: + instance.data["comment"] = comment context.save_changes() publisher_show_and_publish() From 5f0ce4f88dd09caee68a04e94db672620c6d0416 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 15 Sep 2023 18:38:40 +0800 Subject: [PATCH 0056/1233] add tycache family --- .../max/plugins/create/create_tycache.py | 34 ++++ .../hosts/max/plugins/load/load_tycache.py | 67 +++++++ .../max/plugins/publish/extract_tycache.py | 183 ++++++++++++++++++ .../plugins/publish/validate_pointcloud.py | 59 +----- .../plugins/publish/validate_tyflow_data.py | 74 +++++++ 5 files changed, 361 insertions(+), 56 deletions(-) create mode 100644 openpype/hosts/max/plugins/create/create_tycache.py create mode 100644 openpype/hosts/max/plugins/load/load_tycache.py create mode 100644 openpype/hosts/max/plugins/publish/extract_tycache.py create mode 100644 openpype/hosts/max/plugins/publish/validate_tyflow_data.py diff --git a/openpype/hosts/max/plugins/create/create_tycache.py b/openpype/hosts/max/plugins/create/create_tycache.py new file mode 100644 index 0000000000..0fe0f32eed --- /dev/null +++ b/openpype/hosts/max/plugins/create/create_tycache.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +"""Creator plugin for creating TyCache.""" +from openpype.hosts.max.api import plugin +from openpype.pipeline import EnumDef + + +class CreateTyCache(plugin.MaxCreator): + """Creator plugin for TyCache.""" + identifier = "io.openpype.creators.max.tycache" + label = "TyCache" + family = "tycache" + icon = "gear" + + def create(self, subset_name, instance_data, pre_create_data): + from pymxs import runtime as rt + instance_data = pre_create_data.get("tycache_type") + super(CreateTyCache, self).create( + subset_name, + instance_data, + pre_create_data) + + def get_pre_create_attr_defs(self): + attrs = super(CreateTyCache, self).get_pre_create_attr_defs() + + tycache_format_enum = ["tycache", "tycachespline"] + + + return attrs + [ + + EnumDef("tycache_type", + tycache_format_enum, + default="tycache", + label="TyCache Type") + ] diff --git a/openpype/hosts/max/plugins/load/load_tycache.py b/openpype/hosts/max/plugins/load/load_tycache.py new file mode 100644 index 0000000000..657e743087 --- /dev/null +++ b/openpype/hosts/max/plugins/load/load_tycache.py @@ -0,0 +1,67 @@ +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, + get_previous_loaded_object, + update_custom_attribute_data +) +from openpype.pipeline import get_representation_path, load + + +class PointCloudLoader(load.LoaderPlugin): + """Point Cloud Loader.""" + + families = ["tycache"] + representations = ["tyc"] + order = -8 + icon = "code-fork" + color = "green" + + def load(self, context, name=None, namespace=None, data=None): + """Load tyCache""" + from pymxs import runtime as rt + filepath = os.path.normpath(self.filepath_from_context(context)) + obj = rt.tyCache() + obj.filename = filepath + + namespace = unique_namespace( + name + "_", + suffix="_", + ) + obj.name = f"{namespace}:{obj.name}" + + return containerise( + name, [obj], context, + namespace, loader=self.__class__.__name__) + + def update(self, container, representation): + """update the container""" + from pymxs import runtime as rt + + path = get_representation_path(representation) + node = rt.GetNodeByName(container["instance_node"]) + node_list = get_previous_loaded_object(node) + update_custom_attribute_data( + node, node_list) + with maintained_selection(): + rt.Select(node_list) + for prt in rt.Selection: + prt.filename = path + lib.imprint(container["instance_node"], { + "representation": str(representation["_id"]) + }) + + def switch(self, container, representation): + self.update(container, representation) + + def remove(self, container): + """remove the container""" + from pymxs import runtime as rt + + node = rt.GetNodeByName(container["instance_node"]) + rt.Delete(node) diff --git a/openpype/hosts/max/plugins/publish/extract_tycache.py b/openpype/hosts/max/plugins/publish/extract_tycache.py new file mode 100644 index 0000000000..8fcdd6d65c --- /dev/null +++ b/openpype/hosts/max/plugins/publish/extract_tycache.py @@ -0,0 +1,183 @@ +import os + +import pyblish.api +from pymxs import runtime as rt + +from openpype.hosts.max.api import maintained_selection +from openpype.pipeline import publish + + +class ExtractTyCache(publish.Extractor): + """ + Extract tycache format with tyFlow operators. + Notes: + - TyCache only works for TyFlow Pro Plugin. + + Args: + self.export_particle(): sets up all job arguments for attributes + to be exported in MAXscript + + self.get_operators(): get the export_particle operator + + self.get_files(): get the files with tyFlow naming convention + before publishing + """ + + order = pyblish.api.ExtractorOrder - 0.2 + label = "Extract TyCache" + hosts = ["max"] + families = ["tycache"] + + def process(self, instance): + # TODO: let user decide the param + start = int(instance.context.data.get("frameStart")) + end = int(instance.context.data.get("frameEnd")) + self.log.info("Extracting Tycache...") + + stagingdir = self.staging_dir(instance) + filename = "{name}.tyc".format(**instance.data) + path = os.path.join(stagingdir, filename) + filenames = self.get_file(path, start, end) + with maintained_selection(): + job_args = None + if instance.data["tycache_type"] == "tycache": + job_args = self.export_particle( + instance.data["members"], + start, end, path) + elif instance.data["tycache_type"] == "tycachespline": + job_args = self.export_particle( + instance.data["members"], + start, end, path, + tycache_spline_enabled=True) + + for job in job_args: + rt.Execute(job) + + representation = { + 'name': 'tyc', + 'ext': 'tyc', + 'files': filenames if len(filenames) > 1 else filenames[0], + "stagingDir": stagingdir + } + instance.data["representations"].append(representation) + self.log.info(f"Extracted instance '{instance.name}' to: {path}") + + def get_file(self, filepath, start_frame, end_frame): + filenames = [] + filename = os.path.basename(filepath) + orig_name, _ = os.path.splitext(filename) + for frame in range(int(start_frame), int(end_frame) + 1): + actual_name = "{}_{:05}".format(orig_name, frame) + actual_filename = filepath.replace(orig_name, actual_name) + filenames.append(os.path.basename(actual_filename)) + + return filenames + + def export_particle(self, members, start, end, + filepath, tycache_spline_enabled=False): + """Sets up all job arguments for attributes. + + Those attributes are to be exported in MAX Script. + + Args: + members (list): Member nodes of the instance. + start (int): Start frame. + end (int): End frame. + filepath (str): Output path of the TyCache file. + + Returns: + list of arguments for MAX Script. + + """ + job_args = [] + opt_list = self.get_operators(members) + for operator in opt_list: + if tycache_spline_enabled: + export_mode = f'{operator}.exportMode=3' + has_tyc_spline = f'{operator}.tycacheSplines=true' + job_args.extend([export_mode, has_tyc_spline]) + else: + export_mode = f'{operator}.exportMode=2' + job_args.append(export_mode) + start_frame = f"{operator}.frameStart={start}" + job_args.append(start_frame) + end_frame = f"{operator}.frameEnd={end}" + job_args.append(end_frame) + filepath = filepath.replace("\\", "/") + tycache_filename = f'{operator}.tyCacheFilename="{filepath}"' + job_args.append(tycache_filename) + additional_args = self.get_custom_attr(operator) + job_args.extend(iter(additional_args)) + tycache_export = f"{operator}.exportTyCache()" + job_args.append(tycache_export) + + return job_args + + @staticmethod + def get_operators(members): + """Get Export Particles Operator. + + Args: + members (list): Instance members. + + Returns: + list of particle operators + + """ + opt_list = [] + for member in members: + obj = member.baseobject + # TODO: to see if it can be used maxscript instead + anim_names = rt.GetSubAnimNames(obj) + for anim_name in anim_names: + sub_anim = rt.GetSubAnim(obj, anim_name) + boolean = rt.IsProperty(sub_anim, "Export_Particles") + if boolean: + event_name = sub_anim.Name + opt = f"${member.Name}.{event_name}.export_particles" + opt_list.append(opt) + + return opt_list + +""" +.exportMode : integer +.frameStart : integer +.frameEnd : integer + + .tycacheChanAge : boolean + .tycacheChanGroups : boolean + .tycacheChanPos : boolean + .tycacheChanRot : boolean + .tycacheChanScale : boolean + .tycacheChanVel : boolean + .tycacheChanSpin : boolean + .tycacheChanShape : boolean + .tycacheChanMatID : boolean + .tycacheChanMapping : boolean + .tycacheChanMaterials : boolean + .tycacheChanCustomFloat : boolean + .tycacheChanCustomVector : boolean + .tycacheChanCustomTM : boolean + .tycacheChanPhysX : boolean + .tycacheMeshBackup : boolean + .tycacheCreateObject : boolean + .tycacheCreateObjectIfNotCreated : boolean + .tycacheLayer : string + .tycacheObjectName : string + .tycacheAdditionalCloth : boolean + .tycacheAdditionalSkin : boolean + .tycacheAdditionalSkinID : boolean + .tycacheAdditionalSkinIDValue : integer + .tycacheAdditionalTerrain : boolean + .tycacheAdditionalVDB : boolean + .tycacheAdditionalSplinePaths : boolean + .tycacheAdditionalTyMesher : boolean + .tycacheAdditionalGeo : boolean + .tycacheAdditionalObjectList_deprecated : node array + .tycacheAdditionalObjectList : maxObject array + .tycacheAdditionalGeoActivateModifiers : boolean + .tycacheSplinesAdditionalSplines : boolean + .tycacheSplinesAdditionalSplinesObjectList_deprecated : node array + .tycacheSplinesAdditionalObjectList : maxObject array + +""" diff --git a/openpype/hosts/max/plugins/publish/validate_pointcloud.py b/openpype/hosts/max/plugins/publish/validate_pointcloud.py index 295a23f1f6..3ccc9dfda8 100644 --- a/openpype/hosts/max/plugins/publish/validate_pointcloud.py +++ b/openpype/hosts/max/plugins/publish/validate_pointcloud.py @@ -14,29 +14,16 @@ class ValidatePointCloud(pyblish.api.InstancePlugin): def process(self, instance): """ Notes: - - 1. Validate the container only include tyFlow objects - 2. Validate if tyFlow operator Export Particle exists - 3. Validate if the export mode of Export Particle is at PRT format - 4. Validate the partition count and range set as default value + 1. Validate if the export mode of Export Particle is at PRT format + 2. Validate the partition count and range set as default value Partition Count : 100 Partition Range : 1 to 1 - 5. Validate if the custom attribute(s) exist as parameter(s) + 3. Validate if the custom attribute(s) exist as parameter(s) of export_particle operator """ report = [] - invalid_object = self.get_tyflow_object(instance) - if invalid_object: - report.append(f"Non tyFlow object found: {invalid_object}") - - invalid_operator = self.get_tyflow_operator(instance) - if invalid_operator: - report.append(( - "tyFlow ExportParticle operator not " - f"found: {invalid_operator}")) - if self.validate_export_mode(instance): report.append("The export mode is not at PRT") @@ -52,46 +39,6 @@ class ValidatePointCloud(pyblish.api.InstancePlugin): if report: raise PublishValidationError(f"{report}") - def get_tyflow_object(self, instance): - invalid = [] - container = instance.data["instance_node"] - self.log.info(f"Validating tyFlow container for {container}") - - selection_list = instance.data["members"] - for sel in selection_list: - sel_tmp = str(sel) - if rt.ClassOf(sel) in [rt.tyFlow, - rt.Editable_Mesh]: - if "tyFlow" not in sel_tmp: - invalid.append(sel) - else: - invalid.append(sel) - - return invalid - - def get_tyflow_operator(self, instance): - invalid = [] - container = instance.data["instance_node"] - self.log.info(f"Validating tyFlow object for {container}") - selection_list = instance.data["members"] - bool_list = [] - for sel in selection_list: - obj = sel.baseobject - anim_names = rt.GetSubAnimNames(obj) - for anim_name in anim_names: - # get all the names of the related tyFlow nodes - sub_anim = rt.GetSubAnim(obj, anim_name) - # check if there is export particle operator - boolean = rt.IsProperty(sub_anim, "Export_Particles") - bool_list.append(str(boolean)) - # if the export_particles property is not there - # it means there is not a "Export Particle" operator - if "True" not in bool_list: - self.log.error("Operator 'Export Particles' not found!") - invalid.append(sel) - - return invalid - def validate_custom_attribute(self, instance): invalid = [] container = instance.data["instance_node"] diff --git a/openpype/hosts/max/plugins/publish/validate_tyflow_data.py b/openpype/hosts/max/plugins/publish/validate_tyflow_data.py new file mode 100644 index 0000000000..de8d161b9d --- /dev/null +++ b/openpype/hosts/max/plugins/publish/validate_tyflow_data.py @@ -0,0 +1,74 @@ +import pyblish.api +from openpype.pipeline import PublishValidationError +from pymxs import runtime as rt + + +class ValidatePointCloud(pyblish.api.InstancePlugin): + """Validate that TyFlow plugins or + relevant operators being set correctly.""" + + order = pyblish.api.ValidatorOrder + families = ["pointcloud", "tycache"] + hosts = ["max"] + label = "TyFlow Data" + + def process(self, instance): + """ + Notes: + 1. Validate the container only include tyFlow objects + 2. Validate if tyFlow operator Export Particle exists + + """ + report = [] + + invalid_object = self.get_tyflow_object(instance) + if invalid_object: + report.append(f"Non tyFlow object found: {invalid_object}") + + invalid_operator = self.get_tyflow_operator(instance) + if invalid_operator: + report.append(( + "tyFlow ExportParticle operator not " + f"found: {invalid_operator}")) + if report: + raise PublishValidationError(f"{report}") + + def get_tyflow_object(self, instance): + invalid = [] + container = instance.data["instance_node"] + self.log.info(f"Validating tyFlow container for {container}") + + selection_list = instance.data["members"] + for sel in selection_list: + sel_tmp = str(sel) + if rt.ClassOf(sel) in [rt.tyFlow, + rt.Editable_Mesh]: + if "tyFlow" not in sel_tmp: + invalid.append(sel) + else: + invalid.append(sel) + + return invalid + + def get_tyflow_operator(self, instance): + invalid = [] + container = instance.data["instance_node"] + self.log.info(f"Validating tyFlow object for {container}") + selection_list = instance.data["members"] + bool_list = [] + for sel in selection_list: + obj = sel.baseobject + anim_names = rt.GetSubAnimNames(obj) + for anim_name in anim_names: + # get all the names of the related tyFlow nodes + sub_anim = rt.GetSubAnim(obj, anim_name) + # check if there is export particle operator + boolean = rt.IsProperty(sub_anim, "Export_Particles") + bool_list.append(str(boolean)) + # if the export_particles property is not there + # it means there is not a "Export Particle" operator + if "True" not in bool_list: + self.log.error("Operator 'Export Particles' not found!") + invalid.append(sel) + + return invalid From b7ceeaa3542046c20899ac3e3979a40a3ff3410e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 15 Sep 2023 20:21:34 +0800 Subject: [PATCH 0057/1233] hound & add tycache family into integrate --- .../max/plugins/create/create_tycache.py | 4 +-- .../max/plugins/publish/extract_tycache.py | 28 ++++++++++++++++--- .../plugins/publish/collect_resources_path.py | 3 +- openpype/plugins/publish/integrate.py | 3 +- 4 files changed, 29 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/max/plugins/create/create_tycache.py b/openpype/hosts/max/plugins/create/create_tycache.py index 0fe0f32eed..b99ca37c9b 100644 --- a/openpype/hosts/max/plugins/create/create_tycache.py +++ b/openpype/hosts/max/plugins/create/create_tycache.py @@ -12,7 +12,6 @@ class CreateTyCache(plugin.MaxCreator): icon = "gear" def create(self, subset_name, instance_data, pre_create_data): - from pymxs import runtime as rt instance_data = pre_create_data.get("tycache_type") super(CreateTyCache, self).create( subset_name, @@ -24,11 +23,10 @@ class CreateTyCache(plugin.MaxCreator): tycache_format_enum = ["tycache", "tycachespline"] - return attrs + [ EnumDef("tycache_type", tycache_format_enum, default="tycache", label="TyCache Type") - ] + ] diff --git a/openpype/hosts/max/plugins/publish/extract_tycache.py b/openpype/hosts/max/plugins/publish/extract_tycache.py index 8fcdd6d65c..9bef175d27 100644 --- a/openpype/hosts/max/plugins/publish/extract_tycache.py +++ b/openpype/hosts/max/plugins/publish/extract_tycache.py @@ -5,6 +5,7 @@ from pymxs import runtime as rt from openpype.hosts.max.api import maintained_selection from openpype.pipeline import publish +from openpype.lib import EnumDef class ExtractTyCache(publish.Extractor): @@ -93,7 +94,7 @@ class ExtractTyCache(publish.Extractor): opt_list = self.get_operators(members) for operator in opt_list: if tycache_spline_enabled: - export_mode = f'{operator}.exportMode=3' + export_mode = f'{operator}.exportMode=3' has_tyc_spline = f'{operator}.tycacheSplines=true' job_args.extend([export_mode, has_tyc_spline]) else: @@ -133,12 +134,31 @@ class ExtractTyCache(publish.Extractor): sub_anim = rt.GetSubAnim(obj, anim_name) boolean = rt.IsProperty(sub_anim, "Export_Particles") if boolean: - event_name = sub_anim.Name - opt = f"${member.Name}.{event_name}.export_particles" - opt_list.append(opt) + event_name = sub_anim.Name + opt = f"${member.Name}.{event_name}.export_particles" + opt_list.append(opt) return opt_list + def get_custom_attr(operators): + additonal_args = + ] + + + @classmethod + def get_attribute_defs(cls): + tycache_enum ={ + "Age": "tycacheChanAge", + "Groups": "tycacheChanGroups", + } + return [ + EnumDef("dspGeometry", + items=tycache_enum, + default="", + multiselection=True) + ] + + """ .exportMode : integer .frameStart : integer diff --git a/openpype/plugins/publish/collect_resources_path.py b/openpype/plugins/publish/collect_resources_path.py index f96dd0ae18..2fa944718f 100644 --- a/openpype/plugins/publish/collect_resources_path.py +++ b/openpype/plugins/publish/collect_resources_path.py @@ -62,7 +62,8 @@ class CollectResourcesPath(pyblish.api.InstancePlugin): "effect", "staticMesh", "skeletalMesh", - "xgen" + "xgen", + "tycache" ] def process(self, instance): diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 7e48155b9e..2b4c054fdc 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -139,7 +139,8 @@ class IntegrateAsset(pyblish.api.InstancePlugin): "simpleUnrealTexture", "online", "uasset", - "blendScene" + "blendScene", + "tycache" ] default_template_name = "publish" From ab90af0f5e485f069550dde8ffa83697b8bb03dc Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 15 Sep 2023 20:23:17 +0800 Subject: [PATCH 0058/1233] hound --- .../max/plugins/publish/extract_tycache.py | 65 +------------------ 1 file changed, 1 insertion(+), 64 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_tycache.py b/openpype/hosts/max/plugins/publish/extract_tycache.py index 9bef175d27..bbb6dc115f 100644 --- a/openpype/hosts/max/plugins/publish/extract_tycache.py +++ b/openpype/hosts/max/plugins/publish/extract_tycache.py @@ -107,8 +107,7 @@ class ExtractTyCache(publish.Extractor): filepath = filepath.replace("\\", "/") tycache_filename = f'{operator}.tyCacheFilename="{filepath}"' job_args.append(tycache_filename) - additional_args = self.get_custom_attr(operator) - job_args.extend(iter(additional_args)) + # TODO: add the additional job args for tycache attributes tycache_export = f"{operator}.exportTyCache()" job_args.append(tycache_export) @@ -139,65 +138,3 @@ class ExtractTyCache(publish.Extractor): opt_list.append(opt) return opt_list - - def get_custom_attr(operators): - additonal_args = - ] - - - @classmethod - def get_attribute_defs(cls): - tycache_enum ={ - "Age": "tycacheChanAge", - "Groups": "tycacheChanGroups", - } - return [ - EnumDef("dspGeometry", - items=tycache_enum, - default="", - multiselection=True) - ] - - -""" -.exportMode : integer -.frameStart : integer -.frameEnd : integer - - .tycacheChanAge : boolean - .tycacheChanGroups : boolean - .tycacheChanPos : boolean - .tycacheChanRot : boolean - .tycacheChanScale : boolean - .tycacheChanVel : boolean - .tycacheChanSpin : boolean - .tycacheChanShape : boolean - .tycacheChanMatID : boolean - .tycacheChanMapping : boolean - .tycacheChanMaterials : boolean - .tycacheChanCustomFloat : boolean - .tycacheChanCustomVector : boolean - .tycacheChanCustomTM : boolean - .tycacheChanPhysX : boolean - .tycacheMeshBackup : boolean - .tycacheCreateObject : boolean - .tycacheCreateObjectIfNotCreated : boolean - .tycacheLayer : string - .tycacheObjectName : string - .tycacheAdditionalCloth : boolean - .tycacheAdditionalSkin : boolean - .tycacheAdditionalSkinID : boolean - .tycacheAdditionalSkinIDValue : integer - .tycacheAdditionalTerrain : boolean - .tycacheAdditionalVDB : boolean - .tycacheAdditionalSplinePaths : boolean - .tycacheAdditionalTyMesher : boolean - .tycacheAdditionalGeo : boolean - .tycacheAdditionalObjectList_deprecated : node array - .tycacheAdditionalObjectList : maxObject array - .tycacheAdditionalGeoActivateModifiers : boolean - .tycacheSplinesAdditionalSplines : boolean - .tycacheSplinesAdditionalSplinesObjectList_deprecated : node array - .tycacheSplinesAdditionalObjectList : maxObject array - -""" From d6dc61c031a1661485bf5234ed72590480ac3fd9 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 15 Sep 2023 21:54:29 +0800 Subject: [PATCH 0059/1233] make sure all instanceplugins got the right class --- openpype/hosts/max/plugins/create/create_tycache.py | 5 +++-- openpype/hosts/max/plugins/publish/extract_tycache.py | 3 +-- openpype/hosts/max/plugins/publish/validate_tyflow_data.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/max/plugins/create/create_tycache.py b/openpype/hosts/max/plugins/create/create_tycache.py index b99ca37c9b..c48094028a 100644 --- a/openpype/hosts/max/plugins/create/create_tycache.py +++ b/openpype/hosts/max/plugins/create/create_tycache.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """Creator plugin for creating TyCache.""" from openpype.hosts.max.api import plugin -from openpype.pipeline import EnumDef +from openpype.lib import EnumDef class CreateTyCache(plugin.MaxCreator): @@ -12,7 +12,8 @@ class CreateTyCache(plugin.MaxCreator): icon = "gear" def create(self, subset_name, instance_data, pre_create_data): - instance_data = pre_create_data.get("tycache_type") + instance_data["tycache_type"] = pre_create_data.get( + "tycache_type") super(CreateTyCache, self).create( subset_name, instance_data, diff --git a/openpype/hosts/max/plugins/publish/extract_tycache.py b/openpype/hosts/max/plugins/publish/extract_tycache.py index bbb6dc115f..242d12bd4c 100644 --- a/openpype/hosts/max/plugins/publish/extract_tycache.py +++ b/openpype/hosts/max/plugins/publish/extract_tycache.py @@ -5,7 +5,6 @@ from pymxs import runtime as rt from openpype.hosts.max.api import maintained_selection from openpype.pipeline import publish -from openpype.lib import EnumDef class ExtractTyCache(publish.Extractor): @@ -61,7 +60,7 @@ class ExtractTyCache(publish.Extractor): "stagingDir": stagingdir } instance.data["representations"].append(representation) - self.log.info(f"Extracted instance '{instance.name}' to: {path}") + self.log.info(f"Extracted instance '{instance.name}' to: {filenames}") def get_file(self, filepath, start_frame, end_frame): filenames = [] diff --git a/openpype/hosts/max/plugins/publish/validate_tyflow_data.py b/openpype/hosts/max/plugins/publish/validate_tyflow_data.py index de8d161b9d..4574950495 100644 --- a/openpype/hosts/max/plugins/publish/validate_tyflow_data.py +++ b/openpype/hosts/max/plugins/publish/validate_tyflow_data.py @@ -3,7 +3,7 @@ from openpype.pipeline import PublishValidationError from pymxs import runtime as rt -class ValidatePointCloud(pyblish.api.InstancePlugin): +class ValidateTyFlowData(pyblish.api.InstancePlugin): """Validate that TyFlow plugins or relevant operators being set correctly.""" From 2f5494cc76e25e4e5b7bdc3e743da0e279d8c256 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 15 Sep 2023 17:02:49 +0300 Subject: [PATCH 0060/1233] make few publisher attributes and methods public --- openpype/hosts/houdini/api/lib.py | 34 +++++++++++++++--------------- openpype/tools/publisher/window.py | 14 ++++++++++++ openpype/tools/utils/host_tools.py | 12 +++++++---- 3 files changed, 39 insertions(+), 21 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 7e346d7285..688916a507 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -657,21 +657,22 @@ def get_color_management_preferences(): } -def publisher_show_and_publish(): +def publisher_show_and_publish(comment = ""): """Open publisher window and trigger publishing action.""" main_window = get_main_window() publisher_window = get_tool_by_name( tool_name="publisher", - parent=main_window + parent=main_window, + reset_on_show=False ) publisher_window.set_current_tab("publish") publisher_window.make_sure_is_visible() - publisher_window._reset_on_show = False - - publisher_window._controller.reset() - publisher_window._controller.publish() + publisher_window.reset_on_show = False + publisher_window.set_comment_input_text(comment) + publisher_window.reset() + publisher_window.click_publish() def self_publish(): @@ -689,19 +690,18 @@ def self_publish(): active = current_node == node_path instance["active"] = active - - if active: - result, comment = hou.ui.readInput( - "Add Publish Note", - buttons=("Ok", "Cancel"), - title="Publish Note", - close_choice=1 - ) - if not result: - instance.data["comment"] = comment + hou.node(node_path).parm("active").set(active) context.save_changes() - publisher_show_and_publish() + + result, comment = hou.ui.readInput( + "Add Publish Note", + buttons=("Ok", "Cancel"), + title="Publish Note", + close_choice=1 + ) + + publisher_show_and_publish(comment) def add_self_publish_button(node): diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index 39e78c01bb..9214c0a43f 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -388,6 +388,20 @@ class PublisherWindow(QtWidgets.QDialog): def controller(self): return self._controller + @property + def reset_on_show(self): + return self._reset_on_show + + @reset_on_show.setter + def reset_on_show(self, value): + self._reset_on_show = value + + def set_comment_input_text(self, text=""): + self._comment_input.setText(text) + + def click_publish(self): + self._on_publish_clicked() + def make_sure_is_visible(self): if self._window_is_visible: self.setWindowState(QtCore.Qt.WindowActive) diff --git a/openpype/tools/utils/host_tools.py b/openpype/tools/utils/host_tools.py index 2ebc973a47..3e891e1847 100644 --- a/openpype/tools/utils/host_tools.py +++ b/openpype/tools/utils/host_tools.py @@ -261,7 +261,7 @@ class HostToolsHelper: dialog.activateWindow() dialog.showNormal() - def get_publisher_tool(self, parent=None, controller=None): + def get_publisher_tool(self, parent=None, controller=None, reset_on_show=None): """Create, cache and return publisher window.""" if self._publisher_tool is None: @@ -271,15 +271,19 @@ class HostToolsHelper: ILoadHost.validate_load_methods(host) publisher_window = PublisherWindow( - controller=controller, parent=parent or self._parent + controller=controller, + parent=parent or self._parent, + reset_on_show=reset_on_show ) self._publisher_tool = publisher_window return self._publisher_tool - def show_publisher_tool(self, parent=None, controller=None, tab=None): + def show_publisher_tool( + self, parent=None, controller=None, reset_on_show=None, tab=None + ): with qt_app_context(): - window = self.get_publisher_tool(parent, controller) + window = self.get_publisher_tool(parent, controller, reset_on_show) if tab: window.set_current_tab(tab) window.make_sure_is_visible() From 53a626e9ac69cf2239e527a7bc4c6154102374c6 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 15 Sep 2023 17:04:43 +0300 Subject: [PATCH 0061/1233] resolve hound --- openpype/hosts/houdini/api/lib.py | 2 +- openpype/tools/utils/host_tools.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 688916a507..0bde308263 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -657,7 +657,7 @@ def get_color_management_preferences(): } -def publisher_show_and_publish(comment = ""): +def publisher_show_and_publish(comment=""): """Open publisher window and trigger publishing action.""" main_window = get_main_window() diff --git a/openpype/tools/utils/host_tools.py b/openpype/tools/utils/host_tools.py index 3e891e1847..6885fb86c1 100644 --- a/openpype/tools/utils/host_tools.py +++ b/openpype/tools/utils/host_tools.py @@ -261,7 +261,9 @@ class HostToolsHelper: dialog.activateWindow() dialog.showNormal() - def get_publisher_tool(self, parent=None, controller=None, reset_on_show=None): + def get_publisher_tool( + self, parent=None, controller=None, reset_on_show=None + ): """Create, cache and return publisher window.""" if self._publisher_tool is None: From 28a3cf943dec1320b070381982cf44f20cc6fd1e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 15 Sep 2023 22:26:08 +0800 Subject: [PATCH 0062/1233] finished up the extractor --- .../publish/collect_tycache_attributes.py | 70 +++++++++++++++++++ .../max/plugins/publish/extract_tycache.py | 38 ++++++++-- 2 files changed, 104 insertions(+), 4 deletions(-) create mode 100644 openpype/hosts/max/plugins/publish/collect_tycache_attributes.py diff --git a/openpype/hosts/max/plugins/publish/collect_tycache_attributes.py b/openpype/hosts/max/plugins/publish/collect_tycache_attributes.py new file mode 100644 index 0000000000..e312dd8826 --- /dev/null +++ b/openpype/hosts/max/plugins/publish/collect_tycache_attributes.py @@ -0,0 +1,70 @@ +import pyblish.api + +from openpype.lib import EnumDef +from openpype.pipeline.publish import OpenPypePyblishPluginMixin + + +class CollectTyCacheData(pyblish.api.InstancePlugin, + OpenPypePyblishPluginMixin): + """Collect Review Data for Preview Animation""" + + order = pyblish.api.CollectorOrder + 0.02 + label = "Collect tyCache attribute Data" + hosts = ['max'] + families = ["tycache"] + + def process(self, instance): + all_tyc_attributes_dict = {} + attr_values = self.get_attr_values_from_data(instance.data) + tycache_boolean_attributes = attr_values.get("all_tyc_attrs") + if tycache_boolean_attributes: + for attrs in tycache_boolean_attributes: + all_tyc_attributes_dict[attrs] = True + self.log.debug(f"Found tycache attributes: {tycache_boolean_attributes}") + + @classmethod + def get_attribute_defs(cls): + tyc_attr_enum = ["tycacheChanAge", "tycacheChanGroups", "tycacheChanPos", + "tycacheChanRot", "tycacheChanScale", "tycacheChanVel", + "tycacheChanSpin", "tycacheChanShape", "tycacheChanMatID", + "tycacheChanMapping", "tycacheChanMaterials", + "tycacheChanCustomFloat" + ] + + return [ + EnumDef("all_tyc_attrs", + tyc_attr_enum, + default=None, + multiselection=True + + ) + ] +""" + + .tycacheChanCustomFloat : boolean + .tycacheChanCustomVector : boolean + .tycacheChanCustomTM : boolean + .tycacheChanPhysX : boolean + .tycacheMeshBackup : boolean + .tycacheCreateObject : boolean + .tycacheCreateObjectIfNotCreated : boolean + .tycacheLayer : string + .tycacheObjectName : string + .tycacheAdditionalCloth : boolean + .tycacheAdditionalSkin : boolean + .tycacheAdditionalSkinID : boolean + .tycacheAdditionalSkinIDValue : integer + .tycacheAdditionalTerrain : boolean + .tycacheAdditionalVDB : boolean + .tycacheAdditionalSplinePaths : boolean + .tycacheAdditionalTyMesher : boolean + .tycacheAdditionalGeo : boolean + .tycacheAdditionalObjectList_deprecated : node array + .tycacheAdditionalObjectList : maxObject array + .tycacheAdditionalGeoActivateModifiers : boolean + .tycacheSplines: boolean + .tycacheSplinesAdditionalSplines : boolean + .tycacheSplinesAdditionalSplinesObjectList_deprecated : node array + .tycacheSplinesAdditionalObjectList : maxObject array + +""" diff --git a/openpype/hosts/max/plugins/publish/extract_tycache.py b/openpype/hosts/max/plugins/publish/extract_tycache.py index 242d12bd4c..e98fad5c2b 100644 --- a/openpype/hosts/max/plugins/publish/extract_tycache.py +++ b/openpype/hosts/max/plugins/publish/extract_tycache.py @@ -38,16 +38,20 @@ class ExtractTyCache(publish.Extractor): filename = "{name}.tyc".format(**instance.data) path = os.path.join(stagingdir, filename) filenames = self.get_file(path, start, end) + additional_attributes = instance.data.get("tyc_attrs", {}) + with maintained_selection(): job_args = None if instance.data["tycache_type"] == "tycache": job_args = self.export_particle( instance.data["members"], - start, end, path) + start, end, path, + additional_attributes) elif instance.data["tycache_type"] == "tycachespline": job_args = self.export_particle( instance.data["members"], start, end, path, + additional_attributes, tycache_spline_enabled=True) for job in job_args: @@ -74,7 +78,8 @@ class ExtractTyCache(publish.Extractor): return filenames def export_particle(self, members, start, end, - filepath, tycache_spline_enabled=False): + filepath, additional_attributes, + tycache_spline_enabled=False): """Sets up all job arguments for attributes. Those attributes are to be exported in MAX Script. @@ -94,8 +99,7 @@ class ExtractTyCache(publish.Extractor): for operator in opt_list: if tycache_spline_enabled: export_mode = f'{operator}.exportMode=3' - has_tyc_spline = f'{operator}.tycacheSplines=true' - job_args.extend([export_mode, has_tyc_spline]) + job_args.append(export_mode) else: export_mode = f'{operator}.exportMode=2' job_args.append(export_mode) @@ -107,6 +111,11 @@ class ExtractTyCache(publish.Extractor): tycache_filename = f'{operator}.tyCacheFilename="{filepath}"' job_args.append(tycache_filename) # TODO: add the additional job args for tycache attributes + if additional_attributes: + additional_args = self.get_additional_attribute_args( + operator, additional_attributes + ) + job_args.extend(additional_args) tycache_export = f"{operator}.exportTyCache()" job_args.append(tycache_export) @@ -137,3 +146,24 @@ class ExtractTyCache(publish.Extractor): opt_list.append(opt) return opt_list + + def get_additional_attribute_args(self, operator, attrs): + """Get Additional args with the attributes pre-set by user + + Args: + operator (str): export particle operator + attrs (dict): a dict which stores the additional attributes + added by user + + Returns: + additional_args(list): a list of additional args for MAX script + """ + additional_args = [] + for key, value in attrs.items(): + tyc_attribute = None + if isinstance(value, bool): + tyc_attribute = f"{operator}.{key}=True" + elif isinstance(value, str): + tyc_attribute = f"{operator}.{key}={value}" + additional_args.append(tyc_attribute) + return additional_args From 8e25677aa73e6034e8254c99921fac4e9d9a303f Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 18 Sep 2023 15:48:47 +0800 Subject: [PATCH 0063/1233] finish the tycache attributes collector --- .../publish/collect_tycache_attributes.py | 81 ++++++++++--------- 1 file changed, 42 insertions(+), 39 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/collect_tycache_attributes.py b/openpype/hosts/max/plugins/publish/collect_tycache_attributes.py index e312dd8826..122b0d6451 100644 --- a/openpype/hosts/max/plugins/publish/collect_tycache_attributes.py +++ b/openpype/hosts/max/plugins/publish/collect_tycache_attributes.py @@ -1,6 +1,6 @@ import pyblish.api -from openpype.lib import EnumDef +from openpype.lib import EnumDef, TextDef from openpype.pipeline.publish import OpenPypePyblishPluginMixin @@ -20,51 +20,54 @@ class CollectTyCacheData(pyblish.api.InstancePlugin, if tycache_boolean_attributes: for attrs in tycache_boolean_attributes: all_tyc_attributes_dict[attrs] = True - self.log.debug(f"Found tycache attributes: {tycache_boolean_attributes}") + tyc_layer_attr = attr_values.get("tycache_layer") + if tyc_layer_attr: + all_tyc_attributes_dict["tycacheLayer"] = ( + tyc_layer_attr) + tyc_objname_attr = attr_values.get("tycache_objname") + if tyc_objname_attr: + all_tyc_attributes_dict["tycache_objname"] = ( + tyc_objname_attr) + self.log.debug( + f"Found tycache attributes: {all_tyc_attributes_dict}") @classmethod def get_attribute_defs(cls): - tyc_attr_enum = ["tycacheChanAge", "tycacheChanGroups", "tycacheChanPos", - "tycacheChanRot", "tycacheChanScale", "tycacheChanVel", - "tycacheChanSpin", "tycacheChanShape", "tycacheChanMatID", - "tycacheChanMapping", "tycacheChanMaterials", - "tycacheChanCustomFloat" + # TODO: Support the attributes with maxObject array + tyc_attr_enum = ["tycacheChanAge", "tycacheChanGroups", + "tycacheChanPos", "tycacheChanRot", + "tycacheChanScale", "tycacheChanVel", + "tycacheChanSpin", "tycacheChanShape", + "tycacheChanMatID", "tycacheChanMapping", + "tycacheChanMaterials", "tycacheChanCustomFloat" + "tycacheChanCustomVector", "tycacheChanCustomTM", + "tycacheChanPhysX", "tycacheMeshBackup", + "tycacheCreateObjectIfNotCreated", + "tycacheAdditionalCloth", + "tycacheAdditionalSkin", + "tycacheAdditionalSkinID", + "tycacheAdditionalSkinIDValue", + "tycacheAdditionalTerrain", + "tycacheAdditionalVDB", + "tycacheAdditionalSplinePaths", + "tycacheAdditionalGeo", + "tycacheAdditionalGeoActivateModifiers", + "tycacheSplines", + "tycacheSplinesAdditionalSplines" ] return [ EnumDef("all_tyc_attrs", tyc_attr_enum, default=None, - multiselection=True - - ) + multiselection=True, + label="TyCache Attributes"), + TextDef("tycache_layer", + label="TyCache Layer", + tooltip="Name of tycache layer", + default=""), + TextDef("tycache_objname", + label="TyCache Object Name", + tooltip="TyCache Object Name", + default="") ] -""" - - .tycacheChanCustomFloat : boolean - .tycacheChanCustomVector : boolean - .tycacheChanCustomTM : boolean - .tycacheChanPhysX : boolean - .tycacheMeshBackup : boolean - .tycacheCreateObject : boolean - .tycacheCreateObjectIfNotCreated : boolean - .tycacheLayer : string - .tycacheObjectName : string - .tycacheAdditionalCloth : boolean - .tycacheAdditionalSkin : boolean - .tycacheAdditionalSkinID : boolean - .tycacheAdditionalSkinIDValue : integer - .tycacheAdditionalTerrain : boolean - .tycacheAdditionalVDB : boolean - .tycacheAdditionalSplinePaths : boolean - .tycacheAdditionalTyMesher : boolean - .tycacheAdditionalGeo : boolean - .tycacheAdditionalObjectList_deprecated : node array - .tycacheAdditionalObjectList : maxObject array - .tycacheAdditionalGeoActivateModifiers : boolean - .tycacheSplines: boolean - .tycacheSplinesAdditionalSplines : boolean - .tycacheSplinesAdditionalSplinesObjectList_deprecated : node array - .tycacheSplinesAdditionalObjectList : maxObject array - -""" From bd049da6f93fdffac69d673a66d9387351471576 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 20 Sep 2023 16:11:13 +0200 Subject: [PATCH 0064/1233] Update openpype/hosts/fusion/plugins/load/load_usd.py --- openpype/hosts/fusion/plugins/load/load_usd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/load/load_usd.py b/openpype/hosts/fusion/plugins/load/load_usd.py index 8c2c69f52f..f12fbd5ed0 100644 --- a/openpype/hosts/fusion/plugins/load/load_usd.py +++ b/openpype/hosts/fusion/plugins/load/load_usd.py @@ -9,7 +9,7 @@ from openpype.hosts.fusion.api import ( ) -class FusionLoadAlembicMesh(load.LoaderPlugin): +class FusionLoadUSD(load.LoaderPlugin): """Load USD into Fusion Support for USD was added since Fusion 18.5 From 5e5db076d1795256ecf7b1cebe064651452cefaa Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 21 Sep 2023 11:42:42 +0800 Subject: [PATCH 0065/1233] remove unrelated codes to this pull request --- openpype/hosts/houdini/api/lib.py | 50 +- .../plugins/create/create_arnold_ass.py | 16 + .../houdini/plugins/create/create_bgeo.py | 16 +- .../plugins/create/create_mantra_ifd.py | 56 ++ .../plugins/create/create_pointcache.py | 17 + .../plugins/create/create_redshift_proxy.py | 16 + .../plugins/create/create_vbd_cache.py | 22 +- .../hosts/houdini/plugins/load/load_camera.py | 81 ++- .../plugins/publish/collect_cache_farm.py | 75 +++ .../plugins/publish/collect_chunk_size.py | 39 ++ .../houdini/plugins/publish/collect_frames.py | 3 +- .../plugins/publish/extract_alembic.py | 4 + .../houdini/plugins/publish/extract_ass.py | 5 +- .../houdini/plugins/publish/extract_bgeo.py | 4 +- .../plugins/publish/extract_mantra_ifd.py | 51 ++ .../plugins/publish/extract_redshift_proxy.py | 5 +- .../plugins/publish/extract_vdb_cache.py | 4 +- .../plugins/publish/increment_current_file.py | 3 +- .../publish/submit_houdini_cache_deadline.py | 183 +++++++ .../publish/submit_publish_cache_job.py | 501 ++++++++++++++++++ .../publish/validate_deadline_pools.py | 3 +- openpype/pipeline/farm/pyblish_functions.py | 232 ++++++++ .../defaults/project_settings/deadline.json | 8 + .../defaults/project_settings/houdini.json | 5 + .../schemas/schema_houdini_publish.json | 25 + .../server/settings/publish_plugins.py | 22 + server_addon/deadline/server/version.py | 2 +- .../server/settings/publish_plugins.py | 15 + website/docs/artist_hosts_houdini.md | 24 + .../assets/houdini_farm_cache_creator.png | Bin 0 -> 95572 bytes .../docs/assets/houdini_farm_cache_loader.png | Bin 0 -> 134816 bytes .../docs/assets/houdini_frame_per_task.png | Bin 0 -> 59257 bytes 32 files changed, 1380 insertions(+), 107 deletions(-) create mode 100644 openpype/hosts/houdini/plugins/create/create_mantra_ifd.py create mode 100644 openpype/hosts/houdini/plugins/publish/collect_cache_farm.py create mode 100644 openpype/hosts/houdini/plugins/publish/collect_chunk_size.py create mode 100644 openpype/hosts/houdini/plugins/publish/extract_mantra_ifd.py create mode 100644 openpype/modules/deadline/plugins/publish/submit_houdini_cache_deadline.py create mode 100644 openpype/modules/deadline/plugins/publish/submit_publish_cache_job.py create mode 100644 website/docs/assets/houdini_farm_cache_creator.png create mode 100644 website/docs/assets/houdini_farm_cache_loader.png create mode 100644 website/docs/assets/houdini_frame_per_task.png diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index eff98c05f1..856e7abbd3 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -138,6 +138,9 @@ def get_output_parameter(node): return node.parm("ar_ass_file") elif node_type == "Redshift_Proxy_Output": return node.parm("RS_archive_file") + elif node_type == "ifd": + if node.evalParm("soho_outputmode"): + return node.parm("soho_diskfile") raise TypeError("Node type '%s' not supported" % node_type) @@ -649,50 +652,3 @@ def get_color_management_preferences(): "display": hou.Color.ocio_defaultDisplay(), "view": hou.Color.ocio_defaultView() } - - -def get_resolution_from_doc(doc): - """Get resolution from the given asset document. """ - - if not doc or "data" not in doc: - print("Entered document is not valid. \"{}\"".format(str(doc))) - return None - - resolution_width = doc["data"].get("resolutionWidth") - resolution_height = doc["data"].get("resolutionHeight") - - # Make sure both width and height are set - if resolution_width is None or resolution_height is None: - print("No resolution information found for \"{}\"".format(doc["name"])) - return None - - return int(resolution_width), int(resolution_height) - - -def set_camera_resolution(camera, asset_doc=None): - """Apply resolution to camera from asset document of the publish""" - - if not asset_doc: - asset_doc = get_current_project_asset() - - resolution = get_resolution_from_doc(asset_doc) - - if resolution: - print("Setting camera resolution: {} -> {}x{}".format( - camera.name(), resolution[0], resolution[1] - )) - camera.parm("resx").set(resolution[0]) - camera.parm("resy").set(resolution[1]) - - -def get_camera_from_container(container): - """Get camera from container node. """ - - cameras = container.recursiveGlob( - "*", - filter=hou.nodeTypeFilter.ObjCamera, - include_subnets=False - ) - - assert len(cameras) == 1, "Camera instance must have only one camera" - return cameras[0] diff --git a/openpype/hosts/houdini/plugins/create/create_arnold_ass.py b/openpype/hosts/houdini/plugins/create/create_arnold_ass.py index 12d08f7d83..437a14c723 100644 --- a/openpype/hosts/houdini/plugins/create/create_arnold_ass.py +++ b/openpype/hosts/houdini/plugins/create/create_arnold_ass.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- """Creator plugin for creating Arnold ASS files.""" from openpype.hosts.houdini.api import plugin +from openpype.lib import BoolDef class CreateArnoldAss(plugin.HoudiniCreator): @@ -21,6 +22,9 @@ class CreateArnoldAss(plugin.HoudiniCreator): instance_data.pop("active", None) instance_data.update({"node_type": "arnold"}) + creator_attributes = instance_data.setdefault( + "creator_attributes", dict()) + creator_attributes["farm"] = pre_create_data["farm"] instance = super(CreateArnoldAss, self).create( subset_name, @@ -52,3 +56,15 @@ class CreateArnoldAss(plugin.HoudiniCreator): # Lock any parameters in this list to_lock = ["ar_ass_export_enable", "family", "id"] self.lock_parameters(instance_node, to_lock) + + def get_instance_attr_defs(self): + return [ + BoolDef("farm", + label="Submitting to Farm", + default=False) + ] + + def get_pre_create_attr_defs(self): + attrs = super().get_pre_create_attr_defs() + # Use same attributes as for instance attributes + return attrs + self.get_instance_attr_defs() diff --git a/openpype/hosts/houdini/plugins/create/create_bgeo.py b/openpype/hosts/houdini/plugins/create/create_bgeo.py index a3f31e7e94..6d903fb02d 100644 --- a/openpype/hosts/houdini/plugins/create/create_bgeo.py +++ b/openpype/hosts/houdini/plugins/create/create_bgeo.py @@ -2,7 +2,7 @@ """Creator plugin for creating pointcache bgeo files.""" from openpype.hosts.houdini.api import plugin from openpype.pipeline import CreatedInstance, CreatorError -from openpype.lib import EnumDef +from openpype.lib import EnumDef, BoolDef class CreateBGEO(plugin.HoudiniCreator): @@ -18,6 +18,9 @@ class CreateBGEO(plugin.HoudiniCreator): instance_data.pop("active", None) instance_data.update({"node_type": "geometry"}) + creator_attributes = instance_data.setdefault( + "creator_attributes", dict()) + creator_attributes["farm"] = pre_create_data["farm"] instance = super(CreateBGEO, self).create( subset_name, @@ -58,6 +61,13 @@ class CreateBGEO(plugin.HoudiniCreator): instance_node.setParms(parms) + def get_instance_attr_defs(self): + return [ + BoolDef("farm", + label="Submitting to Farm", + default=False) + ] + def get_pre_create_attr_defs(self): attrs = super().get_pre_create_attr_defs() bgeo_enum = [ @@ -88,5 +98,5 @@ class CreateBGEO(plugin.HoudiniCreator): ] return attrs + [ - EnumDef("bgeo_type", bgeo_enum, label="BGEO Options"), - ] + EnumDef("bgeo_type", bgeo_enum, label="BGEO Options") + ] + self.get_instance_attr_defs() diff --git a/openpype/hosts/houdini/plugins/create/create_mantra_ifd.py b/openpype/hosts/houdini/plugins/create/create_mantra_ifd.py new file mode 100644 index 0000000000..7ea7d1042f --- /dev/null +++ b/openpype/hosts/houdini/plugins/create/create_mantra_ifd.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +"""Creator plugin for creating pointcache alembics.""" +from openpype.hosts.houdini.api import plugin +from openpype.pipeline import CreatedInstance +from openpype.lib import BoolDef + + +class CreateMantraIFD(plugin.HoudiniCreator): + """Mantra .ifd Archive""" + identifier = "io.openpype.creators.houdini.mantraifd" + label = "Mantra IFD" + family = "mantraifd" + icon = "gears" + + def create(self, subset_name, instance_data, pre_create_data): + import hou + instance_data.pop("active", None) + instance_data.update({"node_type": "ifd"}) + creator_attributes = instance_data.setdefault( + "creator_attributes", dict()) + creator_attributes["farm"] = pre_create_data["farm"] + instance = super(CreateMantraIFD, self).create( + subset_name, + instance_data, + pre_create_data) # type: CreatedInstance + + instance_node = hou.node(instance.get("instance_node")) + + filepath = "{}{}".format( + hou.text.expandString("$HIP/pyblish/"), + "{}.$F4.ifd".format(subset_name)) + parms = { + # Render frame range + "trange": 1, + # Arnold ROP settings + "soho_diskfile": filepath, + "soho_outputmode": 1 + } + + instance_node.setParms(parms) + + # Lock any parameters in this list + to_lock = ["soho_outputmode", "family", "id"] + self.lock_parameters(instance_node, to_lock) + + def get_instance_attr_defs(self): + return [ + BoolDef("farm", + label="Submitting to Farm", + default=False) + ] + + def get_pre_create_attr_defs(self): + attrs = super().get_pre_create_attr_defs() + # Use same attributes as for instance attributes + return attrs + self.get_instance_attr_defs() diff --git a/openpype/hosts/houdini/plugins/create/create_pointcache.py b/openpype/hosts/houdini/plugins/create/create_pointcache.py index 7eaf2aff2b..2d2f89cc48 100644 --- a/openpype/hosts/houdini/plugins/create/create_pointcache.py +++ b/openpype/hosts/houdini/plugins/create/create_pointcache.py @@ -1,10 +1,12 @@ # -*- coding: utf-8 -*- """Creator plugin for creating pointcache alembics.""" from openpype.hosts.houdini.api import plugin +from openpype.lib import BoolDef import hou + class CreatePointCache(plugin.HoudiniCreator): """Alembic ROP to pointcache""" identifier = "io.openpype.creators.houdini.pointcache" @@ -15,6 +17,9 @@ class CreatePointCache(plugin.HoudiniCreator): def create(self, subset_name, instance_data, pre_create_data): instance_data.pop("active", None) instance_data.update({"node_type": "alembic"}) + creator_attributes = instance_data.setdefault( + "creator_attributes", dict()) + creator_attributes["farm"] = pre_create_data["farm"] instance = super(CreatePointCache, self).create( subset_name, @@ -105,3 +110,15 @@ class CreatePointCache(plugin.HoudiniCreator): else: return min(outputs, key=lambda node: node.evalParm('outputidx')) + + def get_instance_attr_defs(self): + return [ + BoolDef("farm", + label="Submitting to Farm", + default=False) + ] + + def get_pre_create_attr_defs(self): + attrs = super().get_pre_create_attr_defs() + # Use same attributes as for instance attributes + return attrs + self.get_instance_attr_defs() diff --git a/openpype/hosts/houdini/plugins/create/create_redshift_proxy.py b/openpype/hosts/houdini/plugins/create/create_redshift_proxy.py index b814dd9d57..de1f1719fb 100644 --- a/openpype/hosts/houdini/plugins/create/create_redshift_proxy.py +++ b/openpype/hosts/houdini/plugins/create/create_redshift_proxy.py @@ -2,6 +2,7 @@ """Creator plugin for creating Redshift proxies.""" from openpype.hosts.houdini.api import plugin from openpype.pipeline import CreatedInstance +from openpype.lib import BoolDef class CreateRedshiftProxy(plugin.HoudiniCreator): @@ -24,6 +25,9 @@ class CreateRedshiftProxy(plugin.HoudiniCreator): # TODO: Somehow enforce so that it only shows the original limited # attributes of the Redshift_Proxy_Output node type instance_data.update({"node_type": "Redshift_Proxy_Output"}) + creator_attributes = instance_data.setdefault( + "creator_attributes", dict()) + creator_attributes["farm"] = pre_create_data["farm"] instance = super(CreateRedshiftProxy, self).create( subset_name, @@ -44,3 +48,15 @@ class CreateRedshiftProxy(plugin.HoudiniCreator): # Lock some Avalon attributes to_lock = ["family", "id", "prim_to_detail_pattern"] self.lock_parameters(instance_node, to_lock) + + def get_instance_attr_defs(self): + return [ + BoolDef("farm", + label="Submitting to Farm", + default=False) + ] + + def get_pre_create_attr_defs(self): + attrs = super().get_pre_create_attr_defs() + # Use same attributes as for instance attributes + return attrs + self.get_instance_attr_defs() diff --git a/openpype/hosts/houdini/plugins/create/create_vbd_cache.py b/openpype/hosts/houdini/plugins/create/create_vbd_cache.py index 9c96e48e3a..8589600fc6 100644 --- a/openpype/hosts/houdini/plugins/create/create_vbd_cache.py +++ b/openpype/hosts/houdini/plugins/create/create_vbd_cache.py @@ -2,6 +2,7 @@ """Creator plugin for creating VDB Caches.""" from openpype.hosts.houdini.api import plugin from openpype.pipeline import CreatedInstance +from openpype.lib import BoolDef import hou @@ -19,15 +20,20 @@ class CreateVDBCache(plugin.HoudiniCreator): instance_data.pop("active", None) instance_data.update({"node_type": "geometry"}) - + creator_attributes = instance_data.setdefault( + "creator_attributes", dict()) + creator_attributes["farm"] = pre_create_data["farm"] instance = super(CreateVDBCache, self).create( subset_name, instance_data, pre_create_data) # type: CreatedInstance instance_node = hou.node(instance.get("instance_node")) + file_path = "{}{}".format( + hou.text.expandString("$HIP/pyblish/"), + "{}.$F4.vdb".format(subset_name)) parms = { - "sopoutput": "$HIP/pyblish/{}.$F4.vdb".format(subset_name), + "sopoutput": file_path, "initsim": True, "trange": 1 } @@ -102,3 +108,15 @@ class CreateVDBCache(plugin.HoudiniCreator): else: return min(outputs, key=lambda node: node.evalParm('outputidx')) + + def get_instance_attr_defs(self): + return [ + BoolDef("farm", + label="Submitting to Farm", + default=False) + ] + + def get_pre_create_attr_defs(self): + attrs = super().get_pre_create_attr_defs() + # Use same attributes as for instance attributes + return attrs + self.get_instance_attr_defs() diff --git a/openpype/hosts/houdini/plugins/load/load_camera.py b/openpype/hosts/houdini/plugins/load/load_camera.py index e16146a267..7b4a04809e 100644 --- a/openpype/hosts/houdini/plugins/load/load_camera.py +++ b/openpype/hosts/houdini/plugins/load/load_camera.py @@ -4,13 +4,6 @@ from openpype.pipeline import ( ) from openpype.hosts.houdini.api import pipeline -from openpype.hosts.houdini.api.lib import ( - set_camera_resolution, - get_camera_from_container -) - -import hou - ARCHIVE_EXPRESSION = ('__import__("_alembic_hom_extensions")' '.alembicGetCameraDict') @@ -32,15 +25,7 @@ def transfer_non_default_values(src, dest, ignore=None): channel expression and ignore certain Parm types. """ - - ignore_types = { - hou.parmTemplateType.Toggle, - hou.parmTemplateType.Menu, - hou.parmTemplateType.Button, - hou.parmTemplateType.FolderSet, - hou.parmTemplateType.Separator, - hou.parmTemplateType.Label, - } + import hou src.updateParmStates() @@ -77,6 +62,14 @@ def transfer_non_default_values(src, dest, ignore=None): continue # Ignore folders, separators, etc. + ignore_types = { + hou.parmTemplateType.Toggle, + hou.parmTemplateType.Menu, + hou.parmTemplateType.Button, + hou.parmTemplateType.FolderSet, + hou.parmTemplateType.Separator, + hou.parmTemplateType.Label, + } if parm.parmTemplate().type() in ignore_types: continue @@ -97,8 +90,13 @@ class CameraLoader(load.LoaderPlugin): def load(self, context, name=None, namespace=None, data=None): + import os + import hou + # Format file name, Houdini only wants forward slashes - file_path = self.filepath_from_context(context).replace("\\", "/") + file_path = self.filepath_from_context(context) + file_path = os.path.normpath(file_path) + file_path = file_path.replace("\\", "/") # Get the root node obj = hou.node("/obj") @@ -108,21 +106,19 @@ class CameraLoader(load.LoaderPlugin): node_name = "{}_{}".format(namespace, name) if namespace else name # Create a archive node - node = self.create_and_connect(obj, "alembicarchive", node_name) + container = self.create_and_connect(obj, "alembicarchive", node_name) # TODO: add FPS of project / asset - node.setParms({"fileName": file_path, "channelRef": True}) + container.setParms({"fileName": file_path, + "channelRef": True}) # Apply some magic - node.parm("buildHierarchy").pressButton() - node.moveToGoodPosition() + container.parm("buildHierarchy").pressButton() + container.moveToGoodPosition() # Create an alembic xform node - nodes = [node] + nodes = [container] - camera = get_camera_from_container(node) - self._match_maya_render_mask(camera) - set_camera_resolution(camera, asset_doc=context["asset"]) self[:] = nodes return pipeline.containerise(node_name, @@ -147,14 +143,14 @@ class CameraLoader(load.LoaderPlugin): # Store the cam temporarily next to the Alembic Archive # so that we can preserve parm values the user set on it # after build hierarchy was triggered. - old_camera = get_camera_from_container(node) + old_camera = self._get_camera(node) temp_camera = old_camera.copyTo(node.parent()) # Rebuild node.parm("buildHierarchy").pressButton() # Apply values to the new camera - new_camera = get_camera_from_container(node) + new_camera = self._get_camera(node) transfer_non_default_values(temp_camera, new_camera, # The hidden uniform scale attribute @@ -162,9 +158,6 @@ class CameraLoader(load.LoaderPlugin): # "icon_scale" just skip that completely ignore={"scale"}) - self._match_maya_render_mask(new_camera) - set_camera_resolution(new_camera) - temp_camera.destroy() def remove(self, container): @@ -172,6 +165,15 @@ class CameraLoader(load.LoaderPlugin): node = container["node"] node.destroy() + def _get_camera(self, node): + import hou + cameras = node.recursiveGlob("*", + filter=hou.nodeTypeFilter.ObjCamera, + include_subnets=False) + + assert len(cameras) == 1, "Camera instance must have only one camera" + return cameras[0] + def create_and_connect(self, node, node_type, name=None): """Create a node within a node which and connect it to the input @@ -192,20 +194,5 @@ class CameraLoader(load.LoaderPlugin): new_node.moveToGoodPosition() return new_node - def _match_maya_render_mask(self, camera): - """Workaround to match Maya render mask in Houdini""" - - # print("Setting match maya render mask ") - parm = camera.parm("aperture") - expression = parm.expression() - expression = expression.replace("return ", "aperture = ") - expression += """ -# Match maya render mask (logic from Houdini's own FBX importer) -node = hou.pwd() -resx = node.evalParm('resx') -resy = node.evalParm('resy') -aspect = node.evalParm('aspect') -aperture *= min(1, (resx / resy * aspect) / 1.5) -return aperture -""" - parm.setExpression(expression, language=hou.exprLanguage.Python) + def switch(self, container, representation): + self.update(container, representation) diff --git a/openpype/hosts/houdini/plugins/publish/collect_cache_farm.py b/openpype/hosts/houdini/plugins/publish/collect_cache_farm.py new file mode 100644 index 0000000000..36ade32a35 --- /dev/null +++ b/openpype/hosts/houdini/plugins/publish/collect_cache_farm.py @@ -0,0 +1,75 @@ +import os +import pyblish.api +import hou +from openpype.hosts.houdini.api import lib + + +class CollectDataforCache(pyblish.api.InstancePlugin): + """Collect data for caching to Deadline.""" + + order = pyblish.api.CollectorOrder + 0.04 + families = ["ass", "pointcache", + "mantraifd", "redshiftproxy", + "vdbcache"] + hosts = ["houdini"] + targets = ["local", "remote"] + label = "Collect Data for Cache" + + def process(self, instance): + creator_attribute = instance.data["creator_attributes"] + farm_enabled = creator_attribute["farm"] + instance.data["farm"] = farm_enabled + if not farm_enabled: + self.log.debug("Caching on farm is disabled. " + "Skipping farm collecting.") + return + # Why do we need this particular collector to collect the expected + # output files from a ROP node. Don't we have a dedicated collector + # for that yet? + # Collect expected files + ropnode = hou.node(instance.data["instance_node"]) + output_parm = lib.get_output_parameter(ropnode) + expected_filepath = output_parm.eval() + instance.data.setdefault("files", list()) + instance.data.setdefault("expectedFiles", list()) + if instance.data.get("frames"): + files = self.get_files(instance, expected_filepath) + # list of files + instance.data["files"].extend(files) + else: + # single file + instance.data["files"].append(output_parm.eval()) + cache_files = {"_": instance.data["files"]} + # Convert instance family to pointcache if it is bgeo or abc + # because ??? + for family in instance.data["families"]: + if family == "bgeo" or "abc": + instance.data["family"] = "pointcache" + break + instance.data.update({ + "plugin": "Houdini", + "publish": True + }) + instance.data["families"].append("publish.hou") + instance.data["expectedFiles"].append(cache_files) + + self.log.debug("{}".format(instance.data)) + + def get_files(self, instance, output_parm): + """Get the files with the frame range data + + Args: + instance (_type_): instance + output_parm (_type_): path of output parameter + + Returns: + files: a list of files + """ + directory = os.path.dirname(output_parm) + + files = [ + os.path.join(directory, frame).replace("\\", "/") + for frame in instance.data["frames"] + ] + + return files diff --git a/openpype/hosts/houdini/plugins/publish/collect_chunk_size.py b/openpype/hosts/houdini/plugins/publish/collect_chunk_size.py new file mode 100644 index 0000000000..1c867e930a --- /dev/null +++ b/openpype/hosts/houdini/plugins/publish/collect_chunk_size.py @@ -0,0 +1,39 @@ +import pyblish.api +from openpype.lib import NumberDef +from openpype.pipeline import OpenPypePyblishPluginMixin + + +class CollectChunkSize(pyblish.api.InstancePlugin, + OpenPypePyblishPluginMixin): + """Collect chunk size for cache submission to Deadline.""" + + order = pyblish.api.CollectorOrder + 0.05 + families = ["ass", "pointcache", + "vdbcache", "mantraifd", + "redshiftproxy"] + hosts = ["houdini"] + targets = ["local", "remote"] + label = "Collect Chunk Size" + chunkSize = 999999 + + def process(self, instance): + # need to get the chunk size info from the setting + attr_values = self.get_attr_values_from_data(instance.data) + instance.data["chunkSize"] = attr_values.get("chunkSize") + + @classmethod + def apply_settings(cls, project_settings): + project_setting = project_settings["houdini"]["publish"]["CollectChunkSize"] # noqa + cls.chunkSize = project_setting["chunk_size"] + + @classmethod + def get_attribute_defs(cls): + return [ + NumberDef("chunkSize", + minimum=1, + maximum=999999, + decimals=0, + default=cls.chunkSize, + label="Frame Per Task") + + ] diff --git a/openpype/hosts/houdini/plugins/publish/collect_frames.py b/openpype/hosts/houdini/plugins/publish/collect_frames.py index 01df809d4c..cb4ff1a45b 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_frames.py +++ b/openpype/hosts/houdini/plugins/publish/collect_frames.py @@ -14,7 +14,8 @@ class CollectFrames(pyblish.api.InstancePlugin): order = pyblish.api.CollectorOrder + 0.01 label = "Collect Frames" families = ["vdbcache", "imagesequence", "ass", - "redshiftproxy", "review", "bgeo"] + "mantraifd", "redshiftproxy", "review", + "bgeo"] def process(self, instance): diff --git a/openpype/hosts/houdini/plugins/publish/extract_alembic.py b/openpype/hosts/houdini/plugins/publish/extract_alembic.py index bdd19b23d4..df2fdda241 100644 --- a/openpype/hosts/houdini/plugins/publish/extract_alembic.py +++ b/openpype/hosts/houdini/plugins/publish/extract_alembic.py @@ -14,8 +14,12 @@ class ExtractAlembic(publish.Extractor): label = "Extract Alembic" hosts = ["houdini"] families = ["abc", "camera"] + targets = ["local", "remote"] def process(self, instance): + if instance.data.get("farm"): + self.log.debug("Should be processed on farm, skipping.") + return ropnode = hou.node(instance.data["instance_node"]) diff --git a/openpype/hosts/houdini/plugins/publish/extract_ass.py b/openpype/hosts/houdini/plugins/publish/extract_ass.py index 0d246625ba..cf04a3994e 100644 --- a/openpype/hosts/houdini/plugins/publish/extract_ass.py +++ b/openpype/hosts/houdini/plugins/publish/extract_ass.py @@ -14,9 +14,12 @@ class ExtractAss(publish.Extractor): label = "Extract Ass" families = ["ass"] hosts = ["houdini"] + targets = ["local", "remote"] def process(self, instance): - + if instance.data.get("farm"): + self.log.debug("Should be processed on farm, skipping.") + return ropnode = hou.node(instance.data["instance_node"]) # Get the filename from the filename parameter diff --git a/openpype/hosts/houdini/plugins/publish/extract_bgeo.py b/openpype/hosts/houdini/plugins/publish/extract_bgeo.py index c9625ec880..fc5cf0bb65 100644 --- a/openpype/hosts/houdini/plugins/publish/extract_bgeo.py +++ b/openpype/hosts/houdini/plugins/publish/extract_bgeo.py @@ -17,7 +17,9 @@ class ExtractBGEO(publish.Extractor): families = ["bgeo"] def process(self, instance): - + if instance.data.get("farm"): + self.log.debug("Should be processed on farm, skipping.") + return ropnode = hou.node(instance.data["instance_node"]) # Get the filename from the filename parameter diff --git a/openpype/hosts/houdini/plugins/publish/extract_mantra_ifd.py b/openpype/hosts/houdini/plugins/publish/extract_mantra_ifd.py new file mode 100644 index 0000000000..894260d1bf --- /dev/null +++ b/openpype/hosts/houdini/plugins/publish/extract_mantra_ifd.py @@ -0,0 +1,51 @@ +import os + +import pyblish.api + +from openpype.pipeline import publish + +import hou + + +class ExtractMantraIFD(publish.Extractor): + + order = pyblish.api.ExtractorOrder + label = "Extract Mantra ifd" + hosts = ["houdini"] + families = ["mantraifd"] + targets = ["local", "remote"] + + def process(self, instance): + if instance.data.get("farm"): + self.log.debug("Should be processed on farm, skipping.") + return + + ropnode = hou.node(instance.data.get("instance_node")) + output = ropnode.evalParm("soho_diskfile") + staging_dir = os.path.dirname(output) + instance.data["stagingDir"] = staging_dir + + files = instance.data["frames"] + missing_frames = [ + frame + for frame in instance.data["frames"] + if not os.path.exists( + os.path.normpath(os.path.join(staging_dir, frame))) + ] + if missing_frames: + raise RuntimeError("Failed to complete Mantra ifd extraction. " + "Missing output files: {}".format( + missing_frames)) + + if "representations" not in instance.data: + instance.data["representations"] = [] + + representation = { + 'name': 'ifd', + 'ext': 'ifd', + 'files': files, + "stagingDir": staging_dir, + "frameStart": instance.data["frameStart"], + "frameEnd": instance.data["frameEnd"], + } + instance.data["representations"].append(representation) diff --git a/openpype/hosts/houdini/plugins/publish/extract_redshift_proxy.py b/openpype/hosts/houdini/plugins/publish/extract_redshift_proxy.py index 1d99ac665c..0ca0a01c5c 100644 --- a/openpype/hosts/houdini/plugins/publish/extract_redshift_proxy.py +++ b/openpype/hosts/houdini/plugins/publish/extract_redshift_proxy.py @@ -14,9 +14,12 @@ class ExtractRedshiftProxy(publish.Extractor): label = "Extract Redshift Proxy" families = ["redshiftproxy"] hosts = ["houdini"] + targets = ["local", "remote"] def process(self, instance): - + if instance.data.get("farm"): + self.log.debug("Should be processed on farm, skipping.") + return ropnode = hou.node(instance.data.get("instance_node")) # Get the filename from the filename parameter diff --git a/openpype/hosts/houdini/plugins/publish/extract_vdb_cache.py b/openpype/hosts/houdini/plugins/publish/extract_vdb_cache.py index 4bca758f08..7d8634b83c 100644 --- a/openpype/hosts/houdini/plugins/publish/extract_vdb_cache.py +++ b/openpype/hosts/houdini/plugins/publish/extract_vdb_cache.py @@ -16,7 +16,9 @@ class ExtractVDBCache(publish.Extractor): hosts = ["houdini"] def process(self, instance): - + if instance.data.get("farm"): + self.log.debug("Should be processed on farm, skipping.") + return ropnode = hou.node(instance.data["instance_node"]) # Get the filename from the filename parameter diff --git a/openpype/hosts/houdini/plugins/publish/increment_current_file.py b/openpype/hosts/houdini/plugins/publish/increment_current_file.py index 3569de7693..4788cca3cf 100644 --- a/openpype/hosts/houdini/plugins/publish/increment_current_file.py +++ b/openpype/hosts/houdini/plugins/publish/increment_current_file.py @@ -22,7 +22,8 @@ class IncrementCurrentFile(pyblish.api.ContextPlugin): "arnold_rop", "mantra_rop", "karma_rop", - "usdrender"] + "usdrender", + "publish.hou"] optional = True def process(self, context): diff --git a/openpype/modules/deadline/plugins/publish/submit_houdini_cache_deadline.py b/openpype/modules/deadline/plugins/publish/submit_houdini_cache_deadline.py new file mode 100644 index 0000000000..b1717d09ee --- /dev/null +++ b/openpype/modules/deadline/plugins/publish/submit_houdini_cache_deadline.py @@ -0,0 +1,183 @@ +import os +import getpass +from datetime import datetime + +import hou + +import attr +import pyblish.api +from openpype.lib import ( + TextDef, + NumberDef, +) +from openpype.pipeline import ( + legacy_io, + OpenPypePyblishPluginMixin +) +from openpype.tests.lib import is_in_tests +from openpype.lib import is_running_from_build +from openpype_modules.deadline import abstract_submit_deadline +from openpype_modules.deadline.abstract_submit_deadline import DeadlineJobInfo + + +@attr.s +class HoudiniPluginInfo(object): + Build = attr.ib(default=None) + IgnoreInputs = attr.ib(default=True) + ScriptJob = attr.ib(default=True) + SceneFile = attr.ib(default=None) # Input + SaveFile = attr.ib(default=True) + ScriptFilename = attr.ib(default=None) + OutputDriver = attr.ib(default=None) + Version = attr.ib(default=None) # Mandatory for Deadline + ProjectPath = attr.ib(default=None) + + +class HoudiniCacheSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, # noqa + OpenPypePyblishPluginMixin): + """Submit Houdini scene to perform a local publish in Deadline. + + Publishing in Deadline can be helpful for scenes that publish very slow. + This way it can process in the background on another machine without the + Artist having to wait for the publish to finish on their local machine. + + Submission is done through the Deadline Web Service as + supplied via the environment variable AVALON_DEADLINE. + + """ + + label = "Submit Scene to Deadline" + order = pyblish.api.IntegratorOrder + hosts = ["houdini"] + families = ["publish.hou"] + targets = ["local"] + + priority = 50 + jobInfo = {} + pluginInfo = {} + group = None + + def get_job_info(self): + job_info = DeadlineJobInfo(Plugin="Houdini") + + job_info.update(self.jobInfo) + instance = self._instance + context = instance.context + assert all( + result["success"] for result in context.data["results"] + ), "Errors found, aborting integration.." + + # Deadline connection + AVALON_DEADLINE = legacy_io.Session.get( + "AVALON_DEADLINE", "http://localhost:8082" + ) + assert AVALON_DEADLINE, "Requires AVALON_DEADLINE" + + project_name = instance.context.data["projectName"] + filepath = context.data["currentFile"] + scenename = os.path.basename(filepath) + job_name = "{scene} - {instance} [PUBLISH]".format( + scene=scenename, instance=instance.name) + batch_name = "{code} - {scene}".format(code=project_name, + scene=scenename) + if is_in_tests(): + batch_name += datetime.now().strftime("%d%m%Y%H%M%S") + + job_info.Name = job_name + job_info.BatchName = batch_name + job_info.Plugin = instance.data["plugin"] + job_info.UserName = context.data.get("deadlineUser", getpass.getuser()) + rop_node = self.get_rop_node(instance) + if rop_node.type().name() != "alembic": + frames = "{start}-{end}x{step}".format( + start=int(instance.data["frameStart"]), + end=int(instance.data["frameEnd"]), + step=int(instance.data["byFrameStep"]), + ) + + job_info.Frames = frames + + job_info.Pool = instance.data.get("primaryPool") + job_info.SecondaryPool = instance.data.get("secondaryPool") + + attr_values = self.get_attr_values_from_data(instance.data) + + job_info.ChunkSize = instance.data["chunkSize"] + job_info.Comment = context.data.get("comment") + job_info.Priority = attr_values.get("priority", self.priority) + job_info.Group = attr_values.get("group", self.group) + + keys = [ + "FTRACK_API_USER", + "FTRACK_API_KEY", + "FTRACK_SERVER" + ] + + # 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") + + environment = dict({key: os.environ[key] for key in keys + if key in os.environ}, **legacy_io.Session) + + for key in keys: + value = environment.get(key) + if not value: + continue + job_info.EnvironmentKeyValue[key] = value + # to recognize render jobs + job_info.add_render_job_env_var() + + return job_info + + def get_plugin_info(self): + instance = self._instance + version = hou.applicationVersionString() + version = ".".join(version.split(".")[:2]) + rop = self.get_rop_node(instance) + plugin_info = HoudiniPluginInfo( + Build=None, + IgnoreInputs=True, + ScriptJob=True, + SceneFile=self.scene_path, + SaveFile=True, + OutputDriver=rop.path(), + Version=version, + ProjectPath=os.path.dirname(self.scene_path) + ) + + plugin_payload = attr.asdict(plugin_info) + + return plugin_payload + + def process(self, instance): + super(HoudiniCacheSubmitDeadline, self).process(instance) + output_dir = os.path.dirname(instance.data["files"][0]) + instance.data["outputDir"] = output_dir + instance.data["toBeRenderedOn"] = "deadline" + + def get_rop_node(self, instance): + rop = instance.data.get("instance_node") + rop_node = hou.node(rop) + + return rop_node + + @classmethod + def get_attribute_defs(cls): + defs = super(HoudiniCacheSubmitDeadline, cls).get_attribute_defs() + defs.extend([ + NumberDef("priority", + minimum=1, + maximum=250, + decimals=0, + default=cls.priority, + label="Priority"), + TextDef("group", + default=cls.group, + label="Group Name"), + ]) + + return defs diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_cache_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_cache_job.py new file mode 100644 index 0000000000..5651ff4c83 --- /dev/null +++ b/openpype/modules/deadline/plugins/publish/submit_publish_cache_job.py @@ -0,0 +1,501 @@ +# -*- coding: utf-8 -*- +"""Submit publishing job to farm.""" +import os +import json +import re +from copy import deepcopy +import requests + +import pyblish.api + +from openpype import AYON_SERVER_ENABLED +from openpype.client import ( + get_last_version_by_subset_name, +) +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_cache, + create_instances_for_cache, + attach_instances_to_subset, + prepare_cache_representations, + create_metadata_path +) + + +class ProcessSubmittedCacheJobOnFarm(pyblish.api.InstancePlugin, + publish.OpenPypePyblishPluginMixin, + publish.ColormanagedPyblishPluginMixin): + """Process Cache Job submitted on farm + This is replicated version of submit publish job + specifically for cache(s). + + These jobs are dependent on a deadline job + submission prior to this plug-in. + + - In case of Deadline, it creates dependent job on farm publishing + rendered image sequence. + + Options in instance.data: + - deadlineSubmissionJob (dict, Required): The returned .json + data from the job submission to deadline. + + - outputDir (str, Required): The output directory where the metadata + file should be generated. It's assumed that this will also be + final folder containing the output files. + + - ext (str, Optional): The extension (including `.`) that is required + in the output filename to be picked up for image sequence + publishing. + + - expectedFiles (list or dict): explained below + + """ + + label = "Submit cache jobs to Deadline" + order = pyblish.api.IntegratorOrder + 0.2 + icon = "tractor" + + targets = ["local"] + + hosts = ["houdini"] + + families = ["publish.hou"] + + environ_job_filter = [ + "OPENPYPE_METADATA_FILE" + ] + + environ_keys = [ + "AVALON_APP_NAME", + "OPENPYPE_USERNAME", + "OPENPYPE_SG_USER", + ] + + # custom deadline attributes + deadline_department = "" + deadline_pool = "" + deadline_pool_secondary = "" + deadline_group = "" + deadline_chunk_size = 1 + deadline_priority = None + + # regex for finding frame number in string + R_FRAME_NUMBER = re.compile(r'.+\.(?P[0-9]+)\..+') + + plugin_pype_version = "3.0" + + # script path for publish_filesequence.py + publishing_script = None + + def _submit_deadline_post_job(self, instance, job): + """Submit publish job to Deadline. + + Deadline specific code separated from :meth:`process` for sake of + more universal code. Muster post job is sent directly by Muster + submitter, so this type of code isn't necessary for it. + + Returns: + (str): deadline_publish_job_id + """ + data = instance.data.copy() + subset = data["subset"] + job_name = "Publish - {subset}".format(subset=subset) + + anatomy = instance.context.data['anatomy'] + + # instance.data.get("subset") != instances[0]["subset"] + # 'Main' vs 'renderMain' + override_version = None + instance_version = instance.data.get("version") # take this if exists + if instance_version != 1: + override_version = instance_version + + output_dir = self._get_publish_folder( + anatomy, + deepcopy(instance.data["anatomyData"]), + instance.data.get("asset"), + instance.data["subset"], + instance.context, + instance.data["family"], + override_version + ) + + # Transfer the environment from the original job to this dependent + # job so they use the same environment + metadata_path, rootless_metadata_path = \ + create_metadata_path(instance, anatomy) + + environment = { + "AVALON_PROJECT": instance.context.data["projectName"], + "AVALON_ASSET": instance.context.data["asset"], + "AVALON_TASK": instance.context.data["task"], + "OPENPYPE_USERNAME": instance.context.data["user"], + "OPENPYPE_LOG_NO_COLORS": "1", + "IS_TEST": str(int(is_in_tests())) + } + + if AYON_SERVER_ENABLED: + environment["AYON_PUBLISH_JOB"] = "1" + environment["AYON_RENDER_JOB"] = "0" + environment["AYON_REMOTE_PUBLISH"] = "0" + environment["AYON_BUNDLE_NAME"] = os.environ["AYON_BUNDLE_NAME"] + deadline_plugin = "Ayon" + else: + environment["OPENPYPE_PUBLISH_JOB"] = "1" + environment["OPENPYPE_RENDER_JOB"] = "0" + environment["OPENPYPE_REMOTE_PUBLISH"] = "0" + deadline_plugin = "OpenPype" + # Add OpenPype version if we are running from build. + if is_running_from_build(): + self.environ_keys.append("OPENPYPE_VERSION") + + # add environments from self.environ_keys + for env_key in self.environ_keys: + if os.getenv(env_key): + environment[env_key] = os.environ[env_key] + + # pass environment keys from self.environ_job_filter + job_environ = job["Props"].get("Env", {}) + for env_j_key in self.environ_job_filter: + if job_environ.get(env_j_key): + environment[env_j_key] = job_environ[env_j_key] + + # Add mongo url if it's enabled + if instance.context.data.get("deadlinePassMongoUrl"): + mongo_url = os.environ.get("OPENPYPE_MONGO") + if mongo_url: + environment["OPENPYPE_MONGO"] = mongo_url + + priority = self.deadline_priority or instance.data.get("priority", 50) + + instance_settings = self.get_attr_values_from_data(instance.data) + initial_status = instance_settings.get("publishJobState", "Active") + # TODO: Remove this backwards compatibility of `suspend_publish` + if instance.data.get("suspend_publish"): + initial_status = "Suspended" + + args = [ + "--headless", + 'publish', + '"{}"'.format(rootless_metadata_path), + "--targets", "deadline", + "--targets", "farm" + ] + + if is_in_tests(): + args.append("--automatic-tests") + + # Generate the payload for Deadline submission + secondary_pool = ( + self.deadline_pool_secondary or instance.data.get("secondaryPool") + ) + payload = { + "JobInfo": { + "Plugin": deadline_plugin, + "BatchName": job["Props"]["Batch"], + "Name": job_name, + "UserName": job["Props"]["User"], + "Comment": instance.context.data.get("comment", ""), + + "Department": self.deadline_department, + "ChunkSize": self.deadline_chunk_size, + "Priority": priority, + "InitialStatus": initial_status, + + "Group": self.deadline_group, + "Pool": self.deadline_pool or instance.data.get("primaryPool"), + "SecondaryPool": secondary_pool, + # ensure the outputdirectory with correct slashes + "OutputDirectory0": output_dir.replace("\\", "/") + }, + "PluginInfo": { + "Version": self.plugin_pype_version, + "Arguments": " ".join(args), + "SingleFrameOnly": "True", + }, + # Mandatory for Deadline, may be empty + "AuxFiles": [], + } + + if job.get("_id"): + payload["JobInfo"]["JobDependency0"] = job["_id"] + + for index, (key_, value_) in enumerate(environment.items()): + payload["JobInfo"].update( + { + "EnvironmentKeyValue%d" + % index: "{key}={value}".format( + key=key_, value=value_ + ) + } + ) + # remove secondary pool + payload["JobInfo"].pop("SecondaryPool", None) + + self.log.debug("Submitting Deadline publish job ...") + + url = "{}/api/jobs".format(self.deadline_url) + response = requests.post(url, json=payload, timeout=10) + if not response.ok: + raise Exception(response.text) + + deadline_publish_job_id = response.json()["_id"] + + return deadline_publish_job_id + + def process(self, instance): + # type: (pyblish.api.Instance) -> None + """Process plugin. + + Detect type of render farm submission and create and post dependent + job in case of Deadline. It creates json file with metadata needed for + publishing in directory of render. + + Args: + instance (pyblish.api.Instance): Instance data. + + """ + if not instance.data.get("farm"): + self.log.debug("Skipping local instance.") + return + + anatomy = instance.context.data["anatomy"] + + instance_skeleton_data = create_skeleton_instance_cache(instance) + """ + if content of `expectedFiles` list are dictionaries, we will handle + it as list of AOVs, creating instance for every one of them. + + Example: + -------- + + expectedFiles = [ + { + "beauty": [ + "foo_v01.0001.exr", + "foo_v01.0002.exr" + ], + + "Z": [ + "boo_v01.0001.exr", + "boo_v01.0002.exr" + ] + } + ] + + This will create instances for `beauty` and `Z` subset + adding those files to their respective representations. + + If we have only list of files, we collect all file sequences. + More then one doesn't probably make sense, but we'll handle it + like creating one instance with multiple representations. + + Example: + -------- + + expectedFiles = [ + "foo_v01.0001.exr", + "foo_v01.0002.exr", + "xxx_v01.0001.exr", + "xxx_v01.0002.exr" + ] + + This will result in one instance with two representations: + `foo` and `xxx` + """ + + if isinstance(instance.data.get("expectedFiles")[0], dict): + instances = create_instances_for_cache( + instance, instance_skeleton_data) + else: + representations = prepare_cache_representations( + instance_skeleton_data, + instance.data.get("expectedFiles"), + anatomy + ) + + if "representations" not in instance_skeleton_data.keys(): + instance_skeleton_data["representations"] = [] + + # add representation + instance_skeleton_data["representations"] += representations + instances = [instance_skeleton_data] + + # attach instances to subset + if instance.data.get("attachTo"): + instances = attach_instances_to_subset( + instance.data.get("attachTo"), instances + ) + + r''' SUBMiT PUBLiSH JOB 2 D34DLiN3 + ____ + ' ' .---. .---. .--. .---. .--..--..--..--. .---. + | | --= \ | . \/ _|/ \| . \ || || \ |/ _| + | JOB | --= / | | || __| .. | | | |;_ || \ || __| + | | |____./ \.__|._||_.|___./|_____|||__|\__|\.___| + ._____. + + ''' + + render_job = None + submission_type = "" + if instance.data.get("toBeRenderedOn") == "deadline": + render_job = instance.data.pop("deadlineSubmissionJob", None) + submission_type = "deadline" + + if not render_job: + import getpass + + render_job = {} + 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. + # + # Batch name reflect original scene name + + if instance.data.get("assemblySubmissionJobs"): + render_job["Props"]["Batch"] = instance.data.get( + "jobBatchName") + else: + batch = os.path.splitext(os.path.basename( + instance.context.data.get("currentFile")))[0] + render_job["Props"]["Batch"] = batch + # User is deadline user + render_job["Props"]["User"] = instance.context.data.get( + "deadlineUser", getpass.getuser()) + + deadline_publish_job_id = None + if submission_type == "deadline": + # get default deadline webservice url from deadline module + self.deadline_url = instance.context.data["defaultDeadline"] + # if custom one is set in instance, use that + if instance.data.get("deadlineUrl"): + self.deadline_url = instance.data.get("deadlineUrl") + assert self.deadline_url, "Requires Deadline Webservice URL" + + deadline_publish_job_id = \ + self._submit_deadline_post_job(instance, render_job) + + # Inject deadline url to instances. + for inst in instances: + inst["deadlineUrl"] = self.deadline_url + + # publish job file + publish_job = { + "asset": instance_skeleton_data["asset"], + "frameStart": instance_skeleton_data["frameStart"], + "frameEnd": instance_skeleton_data["frameEnd"], + "fps": instance_skeleton_data["fps"], + "source": instance_skeleton_data["source"], + "user": instance.context.data["user"], + "version": instance.context.data["version"], # workfile version + "intent": instance.context.data.get("intent"), + "comment": instance.context.data.get("comment"), + "job": render_job or None, + "session": legacy_io.Session.copy(), + "instances": instances + } + + if deadline_publish_job_id: + publish_job["deadline_publish_job_id"] = deadline_publish_job_id + + metadata_path, rootless_metadata_path = \ + create_metadata_path(instance, anatomy) + + with open(metadata_path, "w") as f: + json.dump(publish_job, f, indent=4, sort_keys=True) + + def _get_publish_folder(self, anatomy, template_data, + asset, subset, context, + family, version=None): + """ + Extracted logic to pre-calculate real publish folder, which is + calculated in IntegrateNew inside of Deadline process. + This should match logic in: + 'collect_anatomy_instance_data' - to + get correct anatomy, family, version for subset and + 'collect_resources_path' + get publish_path + + Args: + anatomy (openpype.pipeline.anatomy.Anatomy): + template_data (dict): pre-calculated collected data for process + asset (string): asset name + subset (string): subset name (actually group name of subset) + family (string): for current deadline process it's always 'render' + TODO - for generic use family needs to be dynamically + calculated like IntegrateNew does + version (int): override version from instance if exists + + Returns: + (string): publish folder where rendered and published files will + be stored + based on 'publish' template + """ + + project_name = context.data["projectName"] + if not version: + version = get_last_version_by_subset_name( + project_name, + subset, + asset_name=asset + ) + if version: + version = int(version["name"]) + 1 + else: + 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 {} + + template_name = publish.get_publish_template_name( + project_name, + host_name, + family, + task_info.get("name"), + task_info.get("type"), + ) + + template_data["subset"] = subset + template_data["family"] = family + template_data["version"] = version + + render_templates = anatomy.templates_obj[template_name] + if "folder" in render_templates: + publish_folder = render_templates["folder"].format_strict( + template_data + ) + else: + # solve deprecated situation when `folder` key is not underneath + # `publish` anatomy + self.log.warning(( + "Deprecation warning: Anatomy does not have set `folder`" + " key underneath `publish` (in global of for project `{}`)." + ).format(project_name)) + + file_path = render_templates["path"].format_strict(template_data) + publish_folder = os.path.dirname(file_path) + + return publish_folder + + @classmethod + def get_attribute_defs(cls): + return [ + EnumDef("publishJobState", + label="Publish Job State", + items=["Active", "Suspended"], + default="Active") + ] diff --git a/openpype/modules/deadline/plugins/publish/validate_deadline_pools.py b/openpype/modules/deadline/plugins/publish/validate_deadline_pools.py index 949caff7d8..90b8241803 100644 --- a/openpype/modules/deadline/plugins/publish/validate_deadline_pools.py +++ b/openpype/modules/deadline/plugins/publish/validate_deadline_pools.py @@ -22,7 +22,8 @@ class ValidateDeadlinePools(OptionalPyblishPluginMixin, "render.frames_farm", "renderFarm", "renderlayer", - "maxrender"] + "maxrender", + "publish.hou"] optional = True # cache diff --git a/openpype/pipeline/farm/pyblish_functions.py b/openpype/pipeline/farm/pyblish_functions.py index fe3ab97de8..33ec770be0 100644 --- a/openpype/pipeline/farm/pyblish_functions.py +++ b/openpype/pipeline/farm/pyblish_functions.py @@ -744,6 +744,238 @@ def get_resources(project_name, version, extension=None): return resources +def create_skeleton_instance_cache(instance): + # type: (pyblish.api.Instance, list, dict) -> dict + """Create skeleton instance from original instance data. + + This will create dictionary containing skeleton + - common - data used for publishing rendered instances. + This skeleton instance is then extended with additional data + and serialized to be processed by farm job. + + Args: + instance (pyblish.api.Instance): Original instance to + be used as a source of data. + + Returns: + dict: Dictionary with skeleton instance data. + + """ + # list of family names to transfer to new family if present + + context = instance.context + data = instance.data.copy() + anatomy = instance.context.data["anatomy"] # type: Anatomy + + # get time related data from instance (or context) + time_data = get_time_data_from_instance_or_context(instance) + + if data.get("extendFrames", False): + time_data.start, time_data.end = extend_frames( + data["asset"], + data["subset"], + time_data.start, + time_data.end, + ) + + source = data.get("source") or context.data.get("currentFile") + success, rootless_path = ( + anatomy.find_root_template_from_path(source) + ) + if success: + source = rootless_path + else: + # `rootless_path` is not set to `source` if none of roots match + log = Logger.get_logger("farm_publishing") + log.warning(("Could not find root path for remapping \"{}\". " + "This may cause issues.").format(source)) + + family = instance.data["family"] + # Make sure "render" is in the families to go through + # validating expected and rendered files + # during publishing job. + families = ["render", family] + + instance_skeleton_data = { + "family": family, + "subset": data["subset"], + "families": families, + "asset": data["asset"], + "frameStart": time_data.start, + "frameEnd": time_data.end, + "handleStart": time_data.handle_start, + "handleEnd": time_data.handle_end, + "frameStartHandle": time_data.start - time_data.handle_start, + "frameEndHandle": time_data.end + time_data.handle_end, + "comment": data.get("comment"), + "fps": time_data.fps, + "source": source, + "extendFrames": data.get("extendFrames"), + "overrideExistingFrame": data.get("overrideExistingFrame"), + "jobBatchName": data.get("jobBatchName", ""), + # map inputVersions `ObjectId` -> `str` so json supports it + "inputVersions": list(map(str, data.get("inputVersions", []))), + } + + # skip locking version if we are creating v01 + instance_version = data.get("version") # take this if exists + if instance_version != 1: + instance_skeleton_data["version"] = instance_version + + representations = get_transferable_representations(instance) + instance_skeleton_data["representations"] = representations + + persistent = instance.data.get("stagingDir_persistent") is True + instance_skeleton_data["stagingDir_persistent"] = persistent + + return instance_skeleton_data + + +def prepare_cache_representations(skeleton_data, exp_files, anatomy): + """Create representations for file sequences. + + This will return representations of expected files if they are not + in hierarchy of aovs. There should be only one sequence of files for + most cases, but if not - we create representation from each of them. + + Arguments: + skeleton_data (dict): instance data for which we are + setting representations + exp_files (list): list of expected files + anatomy (Anatomy) + Returns: + list of representations + + """ + representations = [] + collections, remainders = clique.assemble(exp_files) + + log = Logger.get_logger("farm_publishing") + + # create representation for every collected sequence + for collection in collections: + ext = collection.tail.lstrip(".") + + staging = os.path.dirname(list(collection)[0]) + success, rootless_staging_dir = ( + anatomy.find_root_template_from_path(staging) + ) + if success: + staging = rootless_staging_dir + else: + log.warning(( + "Could not find root path for remapping \"{}\"." + " This may cause issues on farm." + ).format(staging)) + + frame_start = int(skeleton_data.get("frameStartHandle")) + rep = { + "name": ext, + "ext": ext, + "files": [os.path.basename(f) for f in list(collection)], + "frameStart": frame_start, + "frameEnd": int(skeleton_data.get("frameEndHandle")), + # If expectedFile are absolute, we need only filenames + "stagingDir": staging, + "fps": skeleton_data.get("fps") + } + + representations.append(rep) + + return representations + + +def create_instances_for_cache(instance, skeleton): + """Create instance for cache. + + This will create new instance for every AOV it can detect in expected + files list. + + Args: + instance (pyblish.api.Instance): Original instance. + skeleton (dict): Skeleton data for instance (those needed) later + by collector. + + + Returns: + list of instances + + Throws: + ValueError: + + """ + anatomy = instance.context.data["anatomy"] + subset = skeleton["subset"] + family = skeleton["family"] + exp_files = instance.data["expectedFiles"] + log = Logger.get_logger("farm_publishing") + + instances = [] + # go through AOVs in expected files + for _, files in exp_files[0].items(): + cols, rem = clique.assemble(files) + # we shouldn't have any reminders. And if we do, it should + # be just one item for single frame renders. + if not cols and rem: + if len(rem) != 1: + raise ValueError("Found multiple non related files " + "to render, don't know what to do " + "with them.") + col = rem[0] + ext = os.path.splitext(col)[1].lstrip(".") + else: + # but we really expect only one collection. + # Nothing else make sense. + if len(cols) != 1: + raise ValueError("Only one image sequence type is expected.") # noqa: E501 + ext = cols[0].tail.lstrip(".") + col = list(cols[0]) + + if isinstance(col, (list, tuple)): + staging = os.path.dirname(col[0]) + else: + staging = os.path.dirname(col) + + try: + staging = remap_source(staging, anatomy) + except ValueError as e: + log.warning(e) + + new_instance = deepcopy(skeleton) + + new_instance["subset"] = subset + log.info("Creating data for: {}".format(subset)) + new_instance["family"] = family + new_instance["families"] = skeleton["families"] + # create representation + if isinstance(col, (list, tuple)): + files = [os.path.basename(f) for f in col] + else: + files = os.path.basename(col) + + rep = { + "name": ext, + "ext": ext, + "files": files, + "frameStart": int(skeleton["frameStartHandle"]), + "frameEnd": int(skeleton["frameEndHandle"]), + # If expectedFile are absolute, we need only filenames + "stagingDir": staging, + "fps": new_instance.get("fps"), + "tags": [], + } + + new_instance["representations"] = [rep] + + # if extending frames from existing version, copy files from there + # into our destination directory + if new_instance.get("extendFrames", False): + copy_extend_frames(new_instance, rep) + instances.append(new_instance) + log.debug("instances:{}".format(instances)) + return instances + + def copy_extend_frames(instance, representation): """Copy existing frames from latest version. diff --git a/openpype/settings/defaults/project_settings/deadline.json b/openpype/settings/defaults/project_settings/deadline.json index 1b8c8397d7..3becb4e32d 100644 --- a/openpype/settings/defaults/project_settings/deadline.json +++ b/openpype/settings/defaults/project_settings/deadline.json @@ -99,6 +99,14 @@ "deadline_chunk_size": 10, "deadline_job_delay": "00:00:00:00" }, + "ProcessSubmittedCacheJobOnFarm": { + "enabled": true, + "deadline_department": "", + "deadline_pool": "", + "deadline_group": "", + "deadline_chunk_size": 1, + "deadline_priority": 50 + }, "ProcessSubmittedJobOnFarm": { "enabled": true, "deadline_department": "", diff --git a/openpype/settings/defaults/project_settings/houdini.json b/openpype/settings/defaults/project_settings/houdini.json index 93d5c50d5e..c0052185e8 100644 --- a/openpype/settings/defaults/project_settings/houdini.json +++ b/openpype/settings/defaults/project_settings/houdini.json @@ -81,6 +81,11 @@ } }, "publish": { + "CollectChunkSize": { + "enabled": true, + "optional": true, + "chunk_size": 999999 + }, "ValidateWorkfilePaths": { "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 b57089007e..e2f1be8e3e 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 @@ -4,6 +4,31 @@ "key": "publish", "label": "Publish plugins", "children": [ + { + "type": "dict", + "collapsible": true, + "checkbox_key": "enabled", + "key": "CollectChunkSize", + "label": "Collect Chunk Size", + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "boolean", + "key": "optional", + "label": "Optional" + }, + { + "type": "number", + "key": "chunk_size", + "label": "Frames Per Task" + } + ] + }, { "type": "dict", "collapsible": true, diff --git a/server_addon/deadline/server/settings/publish_plugins.py b/server_addon/deadline/server/settings/publish_plugins.py index 8d1b667345..2a60ab4aa8 100644 --- a/server_addon/deadline/server/settings/publish_plugins.py +++ b/server_addon/deadline/server/settings/publish_plugins.py @@ -217,6 +217,17 @@ class AOVFilterSubmodel(BaseSettingsModel): ) +class ProcessCacheJobFarmModel(BaseSettingsModel): + """Process submitted job on farm.""" + + enabled: bool = Field(title="Enabled") + deadline_department: str = Field(title="Department") + deadline_pool: str = Field(title="Pool") + deadline_group: str = Field(title="Group") + deadline_chunk_size: int = Field(title="Chunk Size") + deadline_priority: int = Field(title="Priority") + + class ProcessSubmittedJobOnFarmModel(BaseSettingsModel): """Process submitted job on farm.""" @@ -278,6 +289,9 @@ class PublishPluginsModel(BaseSettingsModel): default_factory=CelactionSubmitDeadlineModel, title="Celaction Submit Deadline" ) + ProcessSubmittedCacheJobOnFarm: ProcessCacheJobFarmModel = Field( + default_factory=ProcessCacheJobFarmModel, + title="Process submitted cache Job on farm.") ProcessSubmittedJobOnFarm: ProcessSubmittedJobOnFarmModel = Field( default_factory=ProcessSubmittedJobOnFarmModel, title="Process submitted job on farm.") @@ -384,6 +398,14 @@ DEFAULT_DEADLINE_PLUGINS_SETTINGS = { "deadline_chunk_size": 10, "deadline_job_delay": "00:00:00:00" }, + "ProcessSubmittedCacheJobOnFarm": { + "enabled": True, + "deadline_department": "", + "deadline_pool": "", + "deadline_group": "", + "deadline_chunk_size": 1, + "deadline_priority": 50 + }, "ProcessSubmittedJobOnFarm": { "enabled": True, "deadline_department": "", diff --git a/server_addon/deadline/server/version.py b/server_addon/deadline/server/version.py index 485f44ac21..b3f4756216 100644 --- a/server_addon/deadline/server/version.py +++ b/server_addon/deadline/server/version.py @@ -1 +1 @@ -__version__ = "0.1.1" +__version__ = "0.1.2" diff --git a/server_addon/houdini/server/settings/publish_plugins.py b/server_addon/houdini/server/settings/publish_plugins.py index 4534d8d0d9..e3092c103e 100644 --- a/server_addon/houdini/server/settings/publish_plugins.py +++ b/server_addon/houdini/server/settings/publish_plugins.py @@ -107,6 +107,12 @@ DEFAULT_HOUDINI_CREATE_SETTINGS = { # Publish Plugins +class CollectChunkSizeModel(BaseSettingsModel): + enabled: bool = Field(title="Enabled") + optional: bool = Field(title="Optional") + chunk_size: int = Field(title="Frame Per Task") + + class ValidateWorkfilePathsModel(BaseSettingsModel): enabled: bool = Field(title="Enabled") optional: bool = Field(title="Optional") @@ -127,6 +133,10 @@ class BasicValidateModel(BaseSettingsModel): class PublishPluginsModel(BaseSettingsModel): + CollectChunkSize: CollectChunkSizeModel = Field( + default_factory=CollectChunkSizeModel, + title="Collect Chunk Size" + ) ValidateWorkfilePaths: ValidateWorkfilePathsModel = Field( default_factory=ValidateWorkfilePathsModel, title="Validate workfile paths settings.") @@ -139,6 +149,11 @@ class PublishPluginsModel(BaseSettingsModel): DEFAULT_HOUDINI_PUBLISH_SETTINGS = { + "CollectChunkSize": { + "enabled": True, + "optional": True, + "chunk_size": 999999 + }, "ValidateWorkfilePaths": { "enabled": True, "optional": True, diff --git a/website/docs/artist_hosts_houdini.md b/website/docs/artist_hosts_houdini.md index 940d5ac351..c4e8293c36 100644 --- a/website/docs/artist_hosts_houdini.md +++ b/website/docs/artist_hosts_houdini.md @@ -83,6 +83,30 @@ select your render camera. All the render outputs are stored in the pyblish/render directory within your project path.\ For Karma-specific render, it also outputs the USD render as default. +## Publishing cache to Deadline +Artist can publish cache to deadline which increases productivity as artist can use local machine +could be used for other tasks. +Caching on the farm is supported for: + +**Arnold ASS (.ass)** +**Pointcache (.bgeo and .abc)** +**VDB (.vdb)** +**Redshift Proxy (.rs)** + +To submit your cache to deadline, you need to create the instance(s) with clicking +**Submitting to Farm** and you can also enable **Use selection** to +select the object for caching in farm. +![Houdini Farm Cache Creator](assets/houdini_farm_cache_creator.png) + +When you go to Publish Tab and click the instance(s), you can set up your preferred +**Frame per task**. +![Houdini Farm Per Task](assets/houdini_frame_per_task.png) + +Once you hit **Publish**, the cache would be submitted and rendered in deadline. +When the render is finished, all the caches would be located in your publish folder. +You can see them in the Loader. +![Houdini Farm Per Task](assets/houdini_farm_cache_loader.png) + ## USD (experimental support) ### Publishing USD You can publish your Solaris Stage as USD file. diff --git a/website/docs/assets/houdini_farm_cache_creator.png b/website/docs/assets/houdini_farm_cache_creator.png new file mode 100644 index 0000000000000000000000000000000000000000..bf0af8033614d37d8e988bf1b060ce17bc07b085 GIT binary patch literal 95572 zcmd3OWl&sA8)ZTu2@nVj9^Bo7TX1)GmoT_HfuO-1LXhC@gAW?q-6goY>)w3dn|Jr` z*4A$At(uyexy-$NyPxjoJm;JyR6$N03E>06ix)4DBqcs0~hwb%}{Zq)PmEs3Z?;dm2LJOveuR17i#CY`(}EQV-wo>TO) z=P~eF{?Sz3M?psH_u}Q#2#2lt*6{GKj7!o|_4cwcCMISgEXm))<0xgnPUP=#@k2U6 z6PqC83zGUP6l~0}*;%q*9RwY9UZCwa#yobN7jdNGRLjE4Wepaoq1o`8`Gz_Deej(t#osn$ z0vR7}X}o!Ux$kSPb_&NbQp z{HboyV26JBo$)|uYP~0e0(eYKBe_#1)u2o^(M+OhnnIhXofsZoE>yx!{+EB2YRt&^ zaK)>Cf39P5eYC{syoVkeJ6?VdHZuzv&y~(rE7iLDzW6&y-$!uC<8n2XGOC3Fl3HhD z*mHHbv)EAGLUFGIhp4XCknJD%GRa?c#uy&t!L{&H4V6&`DXN?JS%FXE?Jpiml3{uR zzH>|=^98Rv0+w3WV?8`R4;6iwv(16vM0)K#X|ChC6(3;(gUl9Kl>&vAQ^o2tNe)RF zK6mg)xaf6Dfj@$IxlHf&ipyE}q6V*o{|?pbfkb-YU}Ss)Tifz-NqP#3*R<+o zBFD?nOSXouu&?SK?ioL@TRO?NF;&+dNK7Gd5mUp%n@LJkvAim&p|C!?tProWgkvsJ zltg!Og{7m1^a;sIswLJ3Sv)S|EYw=#4rd5N&FK5`xqtiitz*UaxzzU5VzN+KoKJ0k zx>PmuY-=dhV_-sZn7r;aZ|Fa9kd7IvNaZI42JbsjQc~u>t47WsgNKn~Qt2pG#jct{M$HxN*av({SNnW{IXbSPOv$1Qk4`XjH2=&0c(*)b)mAfK}EqYdJ|yuPxb0{_lA{;fCuywKl| z&=rO?Gm}&A^T@rnwx&WP-#FXd(^G8K`Ss2Fwc#|LzU!DTYu_+vCp}WmEGQXJ_2$%I zBD?Q(TD&x!XE4s{)6>%}L9uf?hldfsNOlenGq^)d`1BeaL`}JER$6oqje(xC^mX8M zu~T&UQS$QQ1tMT5-N)Fhc3OWVNZoZN{wJ(6M>Hpg@9)$%99wnJg*KE z_XC|6*=ze+!mpXh_f333tF($)ut&?#;yP@Y#LN8|_4BivqoX5B{qev#&h9MOc@k36 zI6`q4mnsInydKh*yzrx@fq22EKA`3Si)W#EvI_IXj`%~=-V~=`s4Sh?pX<1VL!ftfthp>ij4bb*6avg>`qWh zqiy0%l=bB4#ySe^4GNu$kZF;V^i97Db=jIPZK$;h3aIBK>st+&e=eaLRG992xqglz z2JX3c)nt2JJ5K8N@$*OsbF{_a6>C~mhsLT1g>j%kL&A%nurG6YDl1cM#B_9YCQD&o z$`T3`C;X88`g1Nukv%WkG1?df^e{k&I40p=p4Y=@EC%{_W|1Fa2Y&8h^3u3=5|*ow z63T+YA|k!)MCfWh8KRx4tZ_H=8mmBQDO1Hplimu>$jz#GWGa2NApU3&? z)`5M&f+-sFDa+(WQtbzGCljT_7X_F=P|3$b585bWx_XixlBMsPH)I;r#sp@8pAA$R zrKOWhiI;*RmNGV($IXz_d0gc(Se;C-&d$b@SSXhA$wN{!F|(q*ahk}`{%I=T@!;Sf z3L08A-gXQQToF?B}3-L&u6~OMz{dN}$P| zCmS`R+aivud=UbBk~5?c@XJZlKdifh=yPAA_x)AjVR%a5)6)|$whH$*$F0wk#vj4c zG8kwvhh%Rp=)6ecc;)832fQP;4HmnT{dQJYGyQ|Bp1LD&EnBhc{GNS0tqU5Qy6`!jzy@bq}|3LydBkR=*<`rBU z3`TUNhGu4krOe_O+UCv2+9D82a&p620S;6voUa;n7U5KTVRy)7MTdrxu3E-3^F4wr z_)X6Hd5k#z4lH~!*E;a4DUqHMfAiDDN69IqIk9fa8d(wlSJ4JZAtI@;vkN&^B)dtm zh6NI1CUHiT@@Tt8R$8+Ge_=$(I4q*R6Lm=UuwKFLj{k?q;rV7VoyR}(V-(>tiBo1Cu^|E?J! z5{4Dg|K1e+fB!`FnU8bqh5KI|LE;g~T>XD^!Twd z08WIsUjP(p9~vYE5Bd&%ri=^3i6t}m3{R%@QgT{SqDE3kNQm>nS2J)nENM#j-(jXQ zZ>V=~o1T`_?@vDn#G1UdJ%9jU; zd~|g9a{ndCP?IRi>fhiN;gV@25Gb&MLyQBVsa7zp*#r>BDZsrnW%-(tuJ{(wI#7mY zV3aAtL?=<32bc2_97Tp9m}4H3pM>>8_s3g&mWV&e#s(9TsWJT{^+# zTVAzkCPRzDH~dT;QD zsp6kHbK@0NRpT>xy=D~(2`q{Uk#Teo4B!^`77*3v5I z8(cpP|9s8+oyYa%){uTC9%>m<^Uer6KyMFKjZ)Ay(Vv7(LEG+EWR;e}s`MZXP zF_rUW{j$G8TY~->o}Iqkg*wV*A4g6-?_Acq>-`A(W9Tb$N=oPyk!QQmrG6|iQ-Oa# zkYpnt!r4kBur4^U1@&}i<-QTo6FUeos*Nw`IU>PkP$jTukCM@TDCY~|axF>QKxMJnTG0pbw?(d)FvP1p-O1iBqru_x`bTiBG zblXFz>iD)lh2SQ<8J3QJSk9DXKP@wq+3b06apn1({u@DZZ;ymg_tcD@UqWs!!WC>V ztnu<VN=PjAR~%4XUjn?JprAZwK|6u*~H$+8=HaFFmmv zFKcbDVSH}vxt=s0gIh3@Z=T$om*XhB3(6(+F$4sfS@-B=GMLf6zDE91U0u!H(px~~ zRe2$OFb~nTrfjb&Z+!wO=lg_NXBFLDnO7UCnqoTzIg*{ zaGEt@wi^Y$Y8?l_lPbED@E(1HycFCY^t~YOcw7%YT4lz7$EwZWf3%$z zNlS`Gm|gE~jUpC)P3rrdq4)WICF@i&WmpnG|p z?>pjv#|vK0*V%qeC{{C0m9*;WSg30w^L}bUsId&K4-gRv>>-j=6=at`@~1mThNEeN z7~ylqm%K&n8&+P5g!E~pmXO^uNjAxfWU+SycW!+*r+LG>AaM>nw*{DQtctDxb$g zSD9xM3bpblwDfeno$*|>tHXt;=x7v3nhJ)^Tou>Tb!XI#)fngjXg;Uo)enR<>#<>0 z-xjZNczjavme3Vn{fLSD5wbV;BIjm9Dim2YIFfc(o$o2u7H`j!FWQ4|+;0(JVP9u1 z(CVUo`0(v~o3Y?3Y^@Ew=G@b^93t=iGFz_HzAM5JNVF0P40ZOo+whCdry@j4OG&g z)}Lu32pH&z_x~0LQju~g@1NBnmZna7vjla=H^x)tt)%??g0_O}O~k3>mT|BTgD)^1 zSY1TfVd2o?K+z^sufU*MvtND=Hw!`(X zZbE}vt&S!-y%y}s^GV#^7OkdZoy|&jGaRC-n)d!Q{iWwhv2{~zz1?pTwbBPw)Bdhs zzYON8Y*EV(>lCwJTjIj|)LJaRTc|y5(+5u%l?;wXD;z8|$?H-!$ospbCc}z|_Y_D* z4#cY%$noeIuIEJ<8iT)%VERT@j51x;-EL>l(+HRUsfg5Gk13AboDG=1ML-tQu_{4O zuvw_Zm;KJ(kiSsFI}LCGQ?+?y3RHp(Cnt8!i}%-uJwvG+iuDSDQ#RJtxh!z6fVB-0 zZM|Vz>3K~s#c%-GmR2z#Bk4eDpBb&NbH=NJ_V#o2)t-7#7|Y1iH0pK#&!MDtKi?9( zR&QGCpW06r@U8r&ncS~eq42~its*FtK%HzRB_pBGo=kGxv(C_W-{AuY58am#0Sd1U4#;QmTYoAq-iOqYtw@ZVa9ilZ-ygOMbg35a?^})r!*WhSQpMxH+W%?8>g30tCBKapi^jcwy|~)zTOi_O2Mw? zyqQ4Pe71RMU>H#}>${qXz>>!44l`Bs!9Q?wAfa=NG^ou;4W|h{_1oL`!EskNd1P_) z^xE3O6|^!7{H3E6Ota<+_UbU!po7>`WZ4nTyn!T!^*spKlIIQIJqz!}@}qg6&U<)t zLJk}rMNUr6A|sN|ggjcFEa}|NVk`YbB)m!)(x^Z65h>#?H2WUn!{y-*zQ;23+BS26 z9JhzW7F-xJ-9p!jD#8QNr5>95jN0uTu+?N_-zi~~YD8dy_S8}@ znBmdT&{R(7B>f#fS3zRe&`@rVkzwUDLw-5=3q^em*QVsxf8gT(&T^Q(JFhCddax5i zS|nAbQjuU;_k6fiJWV+3`vTzdR75#*Vvl~4&JA?L?e%?bXsjpVq>|9@Pj-UGtn$`k z3DR@ZNN$P$4y%wWT@HIX&a+MEbE9ZYxxOEIQFLb}3NVGLsEIW%$9jQTxisN<@OgrN ztd7Obu9;(Xch8bje*&xmfMHd)O}ForB)M-R&>3$}I-^dW0zx(F?JDnH-$1&1Kc zYO+L8RXW4#fb1Ilt9c&m?Xu0&nM_IPIy=9ZQ5Q z-4XEXs1FlTWTHKLo}LfksxjcnqMtUbB%<;r4;^as--}mg;Gz4+Est+huI3S~wTgvs zda7{f=ydaH_|1uQefglxDvD9}Tt%+OrCRitgi1$@egkMJ=$c$*X|7#T7*mzz8~q8z zMTmBgy{QYfYpIYhWgO?-LWPz^KHca**b81LuVRjbp|@-|0v1h95ngIh8NR%U2-(ry z{XTf3vVVOsFw9a%P}%)_TUk&Pr*2fDF?0L0uO3_h%eg(GpZSz7TC<;=7#>QM62emU zGhCCp%0n*pGc8?-lB@Aw4a7r9M>h?a!LP8kJ<-NR{`62TZWa5kU&lsYw;isJ8;^h8 zbLbZ&fzIc0!r!efT>k8jej&p5(b(>`Sc^S}2My<(ZmTGb&_&S*=A_n7J{to`@^HP} z&PN5daH=mg?yNX$#5Q@Z>%Ug%A>_cC5tEL_!xEa7e8Rz1gb5}Ud#C)e z94VE3U)w~6Kpw%>>F}=VgZRk6g0!hX%lTDm$atjlQe*tLkuU;`be31R z?2p;yl0$!CXY)Lp5N|9B*e0)tP+II~C8$b86j{4f^A5VFy+1hgTdf*SXTR=?sUG1k z9Vi{Ox`**_ber5*mgwn%Z85NDD~;8wm3{}PxVsIT+WV{H;M7!Ft-8+<)1_KjqG4MF zYNa$bD~%C=(okq^asUN&efwz_({W?ESzd3y9;C&n1G!f#(~(r|;rnhj*c^Uycb8?e z#C@|CRt{vaS^(^^eqgi8EP5VJt5Z5A!ZIx=XJhSj^YJXJuzs0w5TIhm9RlZ9YU%?FFdCRKu&{z3=h>1nO|%bv9ENi591CN~Aw4{0EcXF;=W~wRcGtuB01J+2 zUDGPRJDz)T+^T)LEcZj{MMrskmI;5w=;W6UNbW+7}?i3@k-z4mQ8MVE(LVqxtUPr_ET3I zBHJaEa5AG0o+1OL<5``(qoW|8&Wx5VA?|TCZm!T5Mf?p3Mf;D1kI@8@yaOSc?ycOC zcMskPn%t<&@RA`Pt3RSMGFL0arI1lsdNy2ciI4^64ijD2OyM(3eIRaUKt4sb58?Dh ze|es?S1KQg{uFJ++^2zv3=gpJdwZQgb2BoEnt5Cf+!9i#raU(9;BM}V+e}>gDw~>` zNJ(gvRE6chdy`Qpc&Qf`f+n}+wyLr@W^nbb=dDm>XBSjoNrn4M*K@lk#v?8Q9j@~ z$g2{-_h0VJ;XmG8jhxt0XhtC;U(KJwnbb!(xh5J%ev1?xmT3!b3?qC&q&K`1mS1y3^0$Z%VBBPXRZV1Z75~2Sr0yLnUciT z>=Kx5*%hj&H^~ac1{zkK(ZV~bQ+j`;6i4t~Y<8B82)wTF>u9{(%LG8pcv_BUSxk!F zMVA243jlHPD`YD4R<1O9^DKS(38*ycek~Ug1}ar3tO)1`@R|KjxeLx7?Tk1*^1#UJ z4@=Sz=g-h{((gKW19;u}`Qi$kXj0Fwb-MQO;+5OzmsvJ2kJ3IMA+z zhliKEJ|>#ccoK6+$ALrWdK{MQWv{bcG0gl+@Dd70dtPN9WkW5`nWLl6Xtc!?X@V-c59o1!A-lZf7{|AIUWOWxH%CLiW3+rZqsxUoyE zbS=Nzu-Rp^X!*=S^5^@$W*(=Tik7Xh*l*)a*VskMe^clf)X12yxy`P&k=XOf8ZMQ> zC!`i478^)D2pW1}%`$O?a=$_yFnlAo!V;)ms&-;EjRWs;8d4V}yp!&$s=BcqT8VpB zZ;xj8+y7^XnA|zQ8Oigf=T#xdr*nqRIc^n74a0IpW?J;vUXeNfH+3nP#M5- z8rr;WdcA3oID#md2&nnQ3dweq*p;d8NvN|=e}791wEnpka@FeJn;q+ycp2=4*?Z%I zeC6^sDrB%mdcTupCjAA7&TH0YO5Q(w-#jrs-e$)5pJ*ZJCxFN1ZmjR5}g-|B6$nF&zfPf9WJt>i=&}7^y9n%f<3PF9N7`Y&&Gf z9(bSH{^t>>I1J4G|7r9VjGCI-$;ruR($VJL9)dQmw)gH+RTH10*0rbHKm9yb-S^)N z$UgvZKchvWh#zg2->DYgwGRv+s+a4D3JLkQbWM;mu#07Qass8FSn+h}?`?HB(J(C0 zEEhf)7?_Lc@B}JFka15$=XZhU$(fw1Sq=w9-zu}_xeR{xVvLkXi zT6AC8Hj(QqoFVupBsRmX%rUnZqX}8_e8atN&y0?r9yz@(u)WT1i1_)`u zQbz4rfp#wT({*mVIy$;cPP0tj)Yr`X<$y&XY;_c6vQ+(jSdv#zM<61Aqih}m;O}&{ z{QbY4tfceZ{f%F~sH!y0n_(VXqY+=v(;<$P-*=FGn(UL&pPdp?QbGW5SUos+2jHfU z;AM2zqh(P5aRTJkH!&pKg2CMV8Cx=QA}NIeo2AAAH7L#E(xRR0LI#k{nZI*@zs)9d zx_P);?w1!{0<@NB8aL$k+D5=WhfRSq7jb@7D%rU-3%u|1qbor9;?>zMbxY&(td+BY z74l?o68v_LkIQQzt`#%G)i}T-y^)ufS1H#+>WRb`KHs8OD6k`TwvKo?D;9yf4y0ll z05#(5id-sDLG$y~)z!Q+ht9&{ule~803qAm zL+BfRzCCO_lFnD4-^vf@HL=ws((zP&W<%e98npY@yIg8Hu(X!eDSve*!JQJu+~0PRM$uCzLDbm4ctw!Yb;jg@O1}C{m%GwdgUnu}Od6 z=%)`KDV}OA?*@p+3g-b#=fx#GcR27&*;E~j<#bWtYorftqMk_o;O@ae8QuJ8Yb|hj zuEu3!v%}^$gh+PFS!{^qjDK)&lNM#UzW5vQKfs#Lb%TrguzRorfNqu}vJKN1zjt;5 zaOd~skIpNSHUE)`;+srpt4D{Hbm45DwBkUSOyq*e1bmIz`vYmH1Ru&^&>@ynzrMVD zM8U=`@W&wZVnPK3S?{MwXGGBCG&ztY@d3Ef%DMdV%Nv)jM2ZeFfq)Ph)+ z)@|sW)zwv23d%UuXqBnA5(GtI`@ML6akJ6)Ro+ZS-sk9okm*JCGEQz;Im9I+h1HBW zsx~4z^8z5j48Y*9=xBxJ+i7i!`J6(_^Q$Ytay=-s<%Pv1XQ+gKLM{df4TOq;k+6aC@2L!FQ~N3-ht52F4g`?DISR?v>H2r za#2+$!&*Nc8=kW}VZK%l0jmaS%-Ap*?~XH8n(nhd0Cg`-FOi_=>Gd6w#^t{4Y=bf7 zAAS{3!lWR_^AKG7)5_WATm6Ft-%q9jQ)SAC0He?odB{^~*DmyjmWNApz};m4`cE}s zXsXp$;Ni5#5=!pqQtOh;V~z}Xe+q`>RZ5jqm&|@G^x&G0q<7U?FH}6_XV~405<}r; zTWtvw8TBJ&aXHqK^wlz}^i84U*Y3NyNhfCmjK*s@l9qJcP|xe4OFoZ_U;ek}m7mWk z=Lq65IZ_J_~zj1qYGa=1{JExW*8RI0o!r%`8vuU4Y*b6y~g4H0^G zp=K@QukUC!(hbNI-@HAqf6rE~*L!cXPnYYLmu*jcf;k0!NjgO&zpwTA`g(rfhPxJU zmYEIx2n9@X?eqN~7HVd6t#nGbIr1hz3P+oAG${8!*|lOUdXcXuDk)cs)K zrZ_{ywi$n@FRiDgy)!0WZg3PgUF93vu%+Kd$H58opTCv5+y@X=!Tv|%Eg3Qj@**N5 z56h{d2>bQ!-`Uj4qtG6(^sVz@7RY7V&yj+j(ny`}?AW=y zV229!?hVl>b*VRKHb8#U90X2I~8hj*_kbjTPE>a%FpI0jege(FfN#saz8%Jlr2qibNS$? z+jZ2RVzWCHeAjc&#hosd-i2YDOBSnjahbpTrgT2Oq~<>_29iKK;{{c%MWX9Cj-9V!4^ZAE2ekbXjZS&dwz+}+{?F%c6=AR^Xy zk_m6i-Q5qr;^JZ&e-a?gDzgtF7(%XE0u`USd4+hqwxBeB(QOCewIB? zd}>^l9{Y}J8~*FLYK;C_6s!&(F zbR_(1_SeS5*q@D-Q&PWrx;kY&BmV_%l-lh=r|VGz_9v^vUKeA*8Z~wVm<-9m%*M-8 z+Q*ldGb@=AzlD-He3A3yGlF(Ayh^I}kPie3zR}45`XdlfSyiw@DtGvk$szYwhn*!U zgvPTcKLOy@{-fc1Y#lPgjcqeP45Pz2d9#MYPdDs?JdLS7(k`Ksd+j zZ(zmRJ(x>owJrM~%>W%_QqC4{4TD3(T#ehEI5i4JzHy$Sfo|?PT_5R|JQ=gSeos)6 zC;;%G0JrdsvE>1D4mpqS8yi!Ez?+*>PF-G|k0h-EocL_>=W4CzZ9-onDyFy6i40Te zuIGw%dic(1EDkrlhV}LJ1+CHmV!4DgNC#u_tojN(eX(u5mwPNtvfO?Mc1b6ppb&Fj zIa;VA#AMJ47~y|@m%V~tI^WUJS>btYWBn-9%xZ~#b#yDQ8j!e8vF&;I-AR3z^)uVXSF(~v;1|T6(!lwJz$W0R1{(tAh zVE_G;4rRt2Pb#)kd^A|js@5a4Dl^-0Gn954a{ z0MpLNB@hC+B=sz#q@W;TXUF_a+%7S?^|yc0&tFy!0Yne-X8yLt3ThO#Jn1uDQz!u-G7TwKV?+6n-|IgF@$k69!g z2G-WujJeTeJyEHu+&zN-#MVW<36BQVocthFj(MoleF8J*ga>HdUW>O|M(jV~l~n_5 ziU_&8-2Z7-`u$`mP8{&2#Zf?`lyar^q(?Yh-@6W`3Q%^YTYg^9uCYLN<=|k;h`B$E zqoO?36pQ~_q*@pNY4whBZhd|a0jo{T{C8E|^Z{H80J_XpY)}&YSfWp;3@FyTEt-hf zQcMGTs29VQON|NqNqk7Slfh|FXv>A0IvFJK3a9GED+YJ80=8_!m6Y#BQ348cOQ}M zaDA=?YO5Q({ZJ#pG0zCCXr=%I9v#VTtS%A3QxtE-!_ zY=jKDh$o`{Zyi21G2Ag2U7fc=1y3OyO|RLA4J6gEq|7bTv_gag8l$I&hU8WQhP;s* zk%hcyEyngzpU1nk^DabYs#g(+ z7OT;p-kOI26nMTyy#aycRLRfjd~zHw$8Dc)k!gBL+oabx99MjtO`OG77sF85{b{%T z(wI9C;CqQ0o`1w*_?~|x;4#T#=ioH1t(*$vzj#`8wV@;$U4)F`^w8Z5na~CY7@N2&Pi!orA)Vz zX#WI!7NukhmZpYAM}NN<#_IaTf$Z+-Ov}Xa9Q4h4PlTc|rWDFZJR|{f<*lwi+S^>h zs#+uh-~4z*^$w5eEno@m3HjYQ>AF#6DwXYZ>wwK`f*pi}>!*0!91P&{!h5HkQ39K? zGHiaY97}l_sb2$(uI*FcnaYD=Tf6NtfuMKze(-1%g*n-JP!FTEU+WT2wU*cdQQh=k>KNv+uTh7Qi;xOK_dWX&V366Y1YFzKt^71mE zz^eDKS*#jBo~G0r3l}bimliwl5;3V~WFn`!yD`7ML7E`wv3s^DcT%nUtf`?%5hXe+ z%LAYf?b6|L{b#|Oy<$^KXf&9*PfIvBeDsP-~`cqF7j^`?tdEKfQlWqW% z5&^*xpg$B$tH~Xb(qpM(#WN_2E$J_Z$Q&R4-u0`RH%_3_1Oe1?a8y*C*7532=2HGg zsd&2FEA1Q$MTj2fo^_P~m)u-yZz>0&&JSG`sTzA|Gb%c*o{Y72krR$H9!~DNiRx_a zB;=d*B-_zqPb}{tsNUgS?0lYl)Wx$Q(VqeDN7}eghJHmmIKYQ6=78QxDy?VRdtBv( z52m)Fv_9SO%-4CoM8q5t9>5&BOe~pu7-NG|!D2T*DY<;&r#GBL8VzZPt7 z_W*BM&J+e(YuEk;SR%{Y@!YUkpwiv5jLL{%1J95EiKx=v{$_oItw3o?-nQ>Mzz!B_ z_l?GTs}}3Y(wlG1U+x1cQYZ~3sozMG>(L(lYu~3zu~PZ*JksFz=2`(1&{n?ueBan> zGxw;(YfGL(zI#O{N#C4yp=z?jkAq10Mp9MCfSLtaAo{^;55rNmqOgyMN%Q4olnHV) z*{4iHWbtk^B{ijeW(Ld8&yUk%g9?xuCq0y7P!%~{uSy!9cUjG+QiH?OEb^ArVZ647 z(;UNsvfR-D6NrG-4)oB}6N7$Z01O;Lf$t~V z#riU6rRe864i#+)ySKc2d?;AB`H7~_SU^>1YjSZ=Ra5h6MQ>^Hyts!pZLH~jz(e}m zOr!I!eLkdx-exE?ZCU6~R*UDs?v8O(Sy{>=>4Y4s8iquE6VcHrLFl&oGpZ6Odn>+m z29rD26>g{7J6BR1fZ0jWR#vuUJqH8yp=d{}7~TkVC>;@k@ZOnI_Pjf`y#P2%!(-ojCa)VNuZK#P zA*rNs?RWmdLKj?zA9^SY!n?zBuIsihF67Ua>5j+F^zgHx=Be#?wJyl>j!fG^UR<^L zyzX9#KaB!6`uI~-gULX9tE(ZSCrq=m^Bv>;N^s9I_ZPuIfyJMU#WwsUzYLG}BVf)W zDlGpfDJgx|3j3?ONS_U$iy<6sWI*jxc>l}7mgQ(ZHK_9Bn-%4cv^2`zw;zTSz|LMq7U<(y(ohw z_2iyVorHx&O8?JnBk?%f7|1OxgQ#@CvC?;e27N~dG-pZnshC}RhYy!l@P zmejeK6@!~m)R4D`ozd-IFA~vr`64zs$JOu>H!naj;U+g9NvU6uGQ%fK<@0^1T&RfI ztrcUCqdtA7Udl(uE3+iP3YyS(?8(69UqiB5ZpJZQ#!aJfJn*^B-+BA~IIptzX{u zf#Q!^;m)>$lj@QT@r3v!mAe&XBqUU9r2@mygH%c|X3AF-T7A&zR3spMsVUK?Z#IU8 z+~`=kdb)|JnIC<5zd|@T`#3Mo&V+Cdv2zAK^4%vsSAEd;InQp%;d|rVe9`QM)I$3MWwL&3mle*s%4gmYVS33BzR}K4WG~B zMc}i6JY}(xi?jmCMd?{;dMcv)SsYf}UxYS}bhFllXg8VeX|#Qn=2q$L$+nnr8Qre%ogv5Rw-yxPo?#d2G0VZhd6Iafw zZN!itI5@(Jpy-^`(-!Fc)TIoM51FFY_!J)v4X8GjJpC12vo`=85lmxLI3wT6OVVT7 z*x1lSW1$k1!T5CRXOk6uHn+_mcMb%$4RihC{E_IW@^&+AM&h*!)bXRiu?dz_AUHG4&PA3z9Rs>9%5N2bs;?ozomUgU%tcQO*lJ;-AbPt;VwxB%o^F*G-L>`1Dh$NJy^|?h0 zQa61>nqt=(j5IZ`{nPDqO{2o#=bev!ft{etkB_JTmD+)5tzEpm%(3EA?wt|Yk;1yI zkRyTm4tw_fP%67%Pb6cZO*f@{nv!NPMt_3nOc`Ug*wBaBO7Axm@=q_UYrH>xUc?44 zX%P&O>v0~zjJ$@XMwWcWvc+g7L8QeBpq23DBcvnaa4Vz<$5BL_EL{t%p8W2-<5QAA z(d`EFI`4gajl2V}5zPhO@?mXQ`aZcvFh$}s{_C4YDAZV#lj=~ zN+Gz#W_Pq#ci?N^N~lW)w%2Lk@2BzQJZIVxt;^zX84^Se&rh#tqVGIbW{`SxTg$jd z0H4_JU!iE%i{j`sIufcsma9xtwJ2h@gU|&f-FgjA|5FVMs4~1ljvTIcq;(El2PBLl zr}>&!wv9~WLFpw5K_aaCb{-=}6vv@ihB`PJFbZyeA3hTM(1OSViGnpgEY@IY9}pZtVso9-U!eP3%?U0JENl%6YlM63CY z!*)qUP?~F|0@(9m{TXcj#+>r-!!q+?l*5~2UbrLp@!OlNneD1ANE!ZEzWE*rg}~E; zcF|yGNxk<3lZ~0pPoJsrXXe-$W1HJ4_t90qf?RKd=pqZ2S_`%K8Gef!)yq!Tl_z=q za|?;2Z|aTCT~Pw`J$oz9qyMS-HVL*8F)KdS`8(;8vc!%&#r~#TU+r_9tJ%2!;+26E z^;Uf)VuJME&dE-N8Af^BdSP4!p~tp4S*YCk_E2|m`K8Jdw{=PB1fN-7MZ6;tJd)(oaC{jA(B|t z{f2CbTPVoVcYG@c+MwHNuY0IIUPAsjJ2a#VS!wZ>el#L_iQ0#EF_eVG&(FU%yU9!5 zq%gmcC7nxwQ%9tjS_i2Xk0A-1@@a+~ZVvT6AL(9-Y1U|yxRlkvJ$M>HBQAh7FX~SgFr*MbMTX6$fFWRGCn{NPM23 zdY$iu^(HK>6b~UW`kr*VcNtfN2DeOWhow*($Tgh1FVo{2K$cPlyw{|F@I2`;mvQAV zp$CTFb3R;KEAjp`#*d!7Xx{e4r$D8U1n|=%)8Za>R`$(i0kzEuP~85?`L#ia8Q`T6 zOGvWuNY0YRz^$g1xOYmXt);i_K*#ddKJaYQe&a+@2kN4$I{f*eD`nCiYAI*CO*1smh6XUR@O8@pD zgJI6q>Fz#q?8^L3N3|z;**J%IrITFEKx&vc`<#_!Xf1BNZznldwoKe?<20NNtxBq8 zqTtc;n!KL@USYshHdK|b0I!eE3mX)ggavK>5>EDsNO}HJlNd|XQ?xKsRx5~|wUqu6 zQ8&WcBJp$`=g9R~0H_w?+1D9qEH!xuZ@(tDv`cnpS&`l+T5tTAXfYg%OF#q0hCY=| z>1B?`c8%ND1?SZo%nkPkef5?BFUVf>wA**%kIfW@}B-zrvd6xAAF6L8xU_os35!NPYfTOe8*5^JX5* zcZ`co-fvB|Wn%cb6hR&6Z(6&v%y7Yv%4bG0w%%&JSZ5b7fgPhR#{xlB-eyte^(#@-0&7r4Y`mT*I$-G^3QCuSu7&U%q#BeQ5TAYk7Wp!#TfDZ2v-*HYZ=T z(g+SC8rZH?iO`5ar-2gjfnC9R!Sz6)PN_j>zV*p6MGGLfqY%)K78?-51hoAD?{}d+ zN>_|U#Or*>)zV-~UC&(;B1vJt(gIa=4JmKA`>^8uEb#Dn?J;Gxpe#v3{z{bxpnb-&DRf!6)QA=D zU|zxeAp+i^k1I=pW=%9&gJTX2e!{*lDN*WoLE}5jK@{^4+@L)DE!j9PAjM+VvyEuJ zUQCX7vblY(*rJ02B@Ybm0jM^(8k;#8(;K~JCmh=rR!l;<6u*;@x2k+4rsot?s#Wy} zgM4&2WEw08&sx=L^hvb(n176@OaAW(x&8Hh@A1LY4}ob-*KNzg45}^$9*GRMFbj3Z zZFiTclT}W}m`#xTu=*Q~sg~_l=pGhUYkvX?uve)oOrU3)Ui;4$ArwqYZ2ir}TSS^T zMHu()FRkH%G(|H7Wy8Kh-`Qx|sCu3OnYd`kqGwpw@{y5Ny|6@NcbrZ+G?sjl-*&ON zuu*=1laeL5@|~R3u|fGtDzHH8jKz#FcaLz00Df(FQw`>tw{_r<%F}pkY zgQb|!HeV-Bs}+5>*V>J&J`Ksm=U}D;{)>sfkC4>;Orh?psp8v0nRXfgkX&|PtAMAf zs9WyuSev;VLhewwPCKJ2?awhwG;VQQ^sWR{lP&IQ-dC*kn4Sz7zx}~Sbt^v0IbB#8 z9UYEDrTbM>p~A3P85PgOBB}BvE6IJFC748vkmuKIASWLvIvz~w$2&o1XlQO5MXgLx zEJ?$t4wUevNq+HxfRDBDGD&I=hY3IRBgOoQSJkKsk)IH8d;84&g;Lx@T2509EZRQ= z?|;Z;@?=A6^Y`Xht{Pezd*IM?{#Zo|@bL>ubJt(_XgP@Q;eARH^8c{Hj!IB&NX6Fm zrUsKa_o9xjLTM2|FRpH@OTs-b<1zmaw%#%<%BT(7wE!grrAt6Uq>++tl$Mrmq`QUz z0Ridm2C1RDTNvpaIt8Rly1#|*`|kZ6``Gh?KY}yQGwWIFj_W#a0^-}xerfZKn~ZN{ zyzJygZ~H#r2aJiz z3b~$yxL02g^q#Q7`}q6-^g0c1=iJ;2g%xbSHw+l!u*fEVAxsx`hPAe~Ht>6Fu;}db zzC!%`EqT9Bh>0D)Hg0SSjC2&Qsj6|?7?;SR6|XSL(S({zd*J;6b#^GIXjXnpwPU#P_;xP^?kKM zv%3@SFA_G#CqekkVLj2b1%?`p)}%l{rI;b~5LhAE>)U$%8%8OF^Mgw3!Qo+d+e6>y zxTK*#cZi(MC!pSZDbLZ8_0m4l@M`g{LRa|ysi158VQW0|$P3(COV_Eha}-QG9K}5v zLrhOTV*`8q)uPKkjpVEjr;iX_uDj%KqYAx=dkFkV-@ABiw0lz5(Q5l)k-iUk?_^%+ zws)fF7CeC7KDO|!-g8EeB(}vyhKT9T&ZA&XBs60EG>LVU=u8&{$jgwowgZ<<( z>8y)Edx!WL2A;paUnf^7Gw=jr^?nI|tDQq_dvn%s0#iW!w<&f5x#CD=aJvX)?{MAO zf<$dR@0N6wY~t7AS(oMhX@`TuQ<2Kt&?PfAiG}hN+L6M2y`SCP`g(T)1e|s0pYOK! zy(n#TbjOQyRFL-E&xnH_qhJ-*pisB-Z%)h=nv$#Bs7uA^e8%}6%`kjCL-wsY7I$Zt z^f?Y*5-hpCDBccLw6o(=v5T|4n3hKqy-yUjDZc5??w@?atREwB-`L{7NQXL+_3j_k ziY@(Csa~w!u)&hDni_v)RT|2y09rrQ9M6uZdqGwHU#Z1m~2D7{4vCudHkj)(BBtF3+M?f*{X+i|;Xp~pe5 zs0LGb@@m}FO=i4Ad@Ko(WxC2s#D}|=LO%f!)^UiPqvOw%L?vZA zOnEF9M73(a<~dKC=S6W6*P2>AWy^(qB>f$)!oMQfw)-7M=x#s~vW!EpA$PKZHUuRR^{}-$$7{jnB@~UPytSWYT67x%7nSYg^l^ z{zML3C0cof-N8Y*^X1tbSBKRIW!#wm?(G}XhhlJss_JryA$-kD1)cO&O4Lc~`L5d5 zQqiAKSU3)UA(?Z(h*I_JJB$36i$; zh}MZdUNqCA<$SYr07xm7A9gY^5--Pjy{g$WeC3lS&x6ZgIafyZr#~rfx1>|>?k_Qo zeCoqRuq9o=QPtIdkVQ*0aJg*E@xx)1_j>4&6*zcw75C15`1s*1v>?%J9%k&`gaJgR;`5yOYQ)tRh_;+QwS#5J zvBSlM$N8VD*>Q)`B4z3gJGC;9xF91PJb+miX&nYzJ+|EwJI*g zPLIf7#$Lk&cj@>Ht1XiXK3w!g6nA5rU5~)W$49YIwYSa>hhS&g^m$YjexU9es(P90 zo4th-xDU@5!Z~V}_ir)lRQ z-xcq@NmDL6D0pK_nJDMtE23v;SX|nZ5FYRrFO1k9x3XJ*;CBzfvAt$9l54518v1x# zYtl8IYCl=HQ@X71EHcNjYL;mquzro)=BTkauF?ULgTW-&(SzH;77E@a&<#yc@@Rrt ze=~k@i}B5BwP}pqMk?6j?q6w0^-Ps4>>Bf)-Lp4kXH#FAc(2|TO;@1U9D95~B4XBV z4V6H=%@j|icU_4dDl_WR<#yP7FF-Q!V%vpONL}nl3rZG%oUx&WQc`6{J@#B)#-`J) zLARc7@dZQ+{k76UyY)eB1RnMm*IV5NN2FQjo$j?hHHAVG4p6**=(3NTVIJE{@IIf} zz`%gj^S9*rb+C=?MX!nzIoNtA-5Y>*;yc}$p@}}`;i0W)*C+Avt6y(k4(y}3;G2>N zx)5@_jTxlK^r-HkQ6;Nn~@=>dum61Xt-i) zA92U|?!Rg9;@3>8*UR)$pz9wPI9S*zWigofy}B73yM4^~fD4}%I@s1Xfb@_;AE=`P zTPFYm%d2Lo)QR}u7G><<|8rc}h~IC9;58IQOT-Wo80ei>h}Pe~=SA=-R>jAiP3N`iZzfm04KWaf<4kub5hHtVk)dvQDZuk5262b+@G% zM#~;g!$bUwt8r--zcC69%mw`T3?|Xr+*_?0dDY3^78EAusqf{+%rU zmMj?tisU*98TTTbt5qQWs^_d9_))Vwo8NPL-BM;5WJQ8j7f|K5V+r17*_45Ly4_E|S7Mb0DpWP-4?1NILf?s|U z^ruxmZ*RxCO-=ru#wbs(ZnnIusTU=XPK~AHKFzAj3f()5;JVCDm{P{oBvvl``hA07 zN6-!rbQbjzBJ7i=8nNyQU*E4WMU$dpE3)&()NG~R=koQ5PnO)zJVC;H8yrn5ziojL zvWPuDKR=x3f&~x?M6yZmM1b#l{Cv)C#zmafx}SGT^>nOESlcSuzs~81^ZMrT!KVOx zg=`6q7&;9SjIBvU;TFx>{S5W;ECRMVW+IOK0AONbsM7Mow4 z)C$_&020CHYN8i{3Av9vfYjc#dC21Y%f&~TOIxCr%Prof(PA{fS1EIzm#E_gRkZmT zTSt2_+Fd)KF!uZPi;m&evS4OCuM$5W%Gwz5J?+OjlX~4!z~W(NlGpKlW&VCM!keC* z&Y%4bti#E1z?#xX&^68tA6z9 zGfY6NeEoOzP*}#2L8P-op-Kw-Cmg;qyYD5kJuxuSxLyb#X*b+1w?3Zolv#5t^=Np# z=YGqPivP1p@CSELQX+~AlM5SU`w$31#gUPMZd`OsOqKcg)Ku+b4k0tC1OXHv|81M& zTPYo}W-6hFlqm(A_9)rvvHU)<9^kTB_%xpq{E{hTAh{OiBqmlrV901IB6haEl<>Fx z-&e4q=&FO75(3?41bGim;hlP6>4)XD&A@WQh3-Z}8X6RIKw@O^+~Fm-S$OiZt83-v z**=V^3uoyCTlezS%3>F>O>9oabV89VrUECecOCmdfJlao)U zRDlXi{A{wppsRQBU~K5EVwG9u>h6z>AimstKLZV=dlZ=khY@9ipoN7dH0*wd*%)yp zgHZ$n^hT%PtLMd^-w9vgf6n&2X)qfp!UizMVBkFY-9LR~Hd-ezQ)OL3PYMT^iHo7a zadqD;{Z4m!c2=R;fU?R;(N5%(vc3IKa~`1ZCDri&Y*Uqyy%)aH_{=N4h{BEG%v4mq zkH&^yW*hGwym#`z7WQseZ`Ef9um#)QON?XJ;R&^<{<0?0EI75FrGx-7u4|-Tw*kw{tp;9Tu z#~AwJM}8vK!Lf9Km!U)j#{Zt7f6tKz+55^eXW#^0tkwO!S}^9?yr?(V;vi{L-|Ecl z)FI96G8WGUFA{avxj+5Ei;TkrnBLYr<*iJXOXeZqJUmkXu%@Ex3$LwHMc0TTQ43-V zjs;P75)x82%gU;%{<8l$SHEv)#8P3G11!ml<||B};9z#u>tlYHS|O}3gMI^V%Wgs<_r}V+nvR z5yYa%7!o&^7z(x4+CaFc*Cy1&YyR%?WH%s<-(9bBaT@{cA7H53-7-ANmE9Q20H;EQ62|n#(4wk7;xGA04&>teT(4&|XJexJZB|y+arIe6ZLK6w$1ks~g&wYS zXMPzoaa}`L!ypxrAx<0H$@u9&i_zl0F#?$~|HWkO|CQ(dY>ZTtif9!uZ8HatO8sIq z%>R10y#pZCmh3Aj%dg7 zRsi`JnB$lu5+d^H$!FE4^ZSL#)A@iuyqW+O9t`qm~^h8PfD2oUDwqMn23uf2Z`Ju~wSzMPNWfaW_mWWNPXQ&Ur= zh?g&O=J^FKG`b=v0t9Ik6c2N8O&sqOP=z`u$w7_K#+ zesIfeq%aL|XAsawe*dxvIp2v{JzuCN=+K}(8UPp zix~K{a9)AaW`y0pUF5%ia2gDPs5Kpu{|jPi{{%#QLoE3C_`v-~3M8$PeyD5uR>nCK z@=3=6fP`A`CtZAxZ13Jd(9gDk6J65`Kn9=_pmao7zbFvM7CmBds9|Dd<%>x=LyM0^ zb^lgbd5>F9aQJ;!``p0e&k9O|;3UDOSB8Pg1|-8~Q2?g>NLO`Y!u(dl$lCfLv%I{~ z%Pgr`odSFDx_VPGhNeYVVPasj?G1?#IpP;#9u!*m`sYxL9s^JYqn7JjG-80 z_z~Z`(iz)9_f5GG=t+<1IrT^e zhK4Mj9k0KA`_|UkNeXb|{`b1Trxk8vt>>G0Yv$>>uYGNK?MQb#CJI{;^ll?c>m7PXFp04!Lvz zU(j%oRCi4l_}V7T087^`98)nvaG6-C_`Zo;C z@l+n{)-GmuO7H4_(GLX4h{51aeckqcDY>8UP};o8FbzFDYB?+G`QbY5#(Xvk4U~lt zBuFZ=Qm>qreJ;+;dX)jFncUMeR$u>1;BR2(+z-W^YH)fR2R%xqPh|F<)M76~}M9CHL?o2Dl$IzVi(iwGpL<6u2U+FJSUS&v}m~S-|TNRa= zwVU2EF38Z)Ql4Y;H~y=fZE=X%Dm=e*+4rB+-SqLC*-h1~RcuwHf(D}~HOPFaYpmav ze$yNC<}nvki|v^*umEx(l<&D8636cx5NRuEWMtgVJ9RBIK;x<0-nbvuwItkmu%2!) z(&!@Jj6}d=K*y9O81DVpU$5>F%EEE8~>Jq0u*S%6+B z%M`Ue{>2K4RhrofdCrXEFb&b_#E)$3zO}VnBs)8-9_Lc%L}pJ>TP{CxIa!o6J7{R; z4DPVkvIOLae{_-T83sxcm31zmR7w)*PMukmmA){99?u z=iCXjp6`E82C_--o?4}gE|EX~ddgfgrl}+)boh7eE&bDXMjKBpXDcv3k|&hijd9HQ zm59Uql>+3h(e*IO-Ey;~-l|U(os{ztaKeU~gJ;{bxxrkM@N8fJp`sn!F6vLg`UacX z0{)?1Q&lJU?_9Lda=%7;mSp(C7LQYF>sbjYFRQWzaW21F%f%_LxqGJAv#{j8c!`diNn zBw(9X*4^5l^xVC4iUHPdq}Ct7e|KkH#yzRcK2<1xAQ7;Ca)GuQP+9nPFoieIDtXk3 zwHPRpVekl zn@YihX5Z*+_jzq@7oDp!0O&`8np+I@aLkm0IR^R0Jp!EUwF-i5guAnl2QGUAI%83R z9?Km_$}r^*jEnNBr|BbCQP&TaS9gH1bvB>;Y8B=ChFPa4$RF$rkuivu)|l#7c>e&i zInc55Km)(iIe-FjLRxd9OXi}5wn}d1W{i?XK(4`x0jqO=!s_PDvlqvBS$S$w>In@e z2gk~_gwHTBC)U5#eR~qiLCZHc_neS0@!C)42{JOwxhJMU_o_s@@o*ks4E*qC3~qhu z0g9ktrW=^`=RZ2@?WggNuWs#xJda+HddoL%%S0&!L8q0>TV0XqV@U0#>b9U*wM$7Q zVQ*?3;hHJmzp)|r$0GL+7fgdSP$#2KRK6D1YI2@us5s?x-#RAzW`0(zS{+3Hgd%Zp zwsv<|(_10h5ZAt?A~nM29R@LnREzchRL}RjzM$$UFp-y{q&*0&t0kJ2|;*zV?jMF8#z&5;WjT0inD<&nR_gqV> zlG9N{^_1Lb)cUgJweVFBxp^rZr>k7=mhX5AvC?7~i32(j)JWCzYOo)k!%-JSVWDrq zoQz;(jIC{HEXnSzVx>IJt%*WkoUUNw%l+RsQsMEDHX+0DWMQwnn@(p6r)muIT9ES< zGMgGqV(C@IHa}4#Ovm8HU`WYKYdSyq*5z~H`>j*DJ}ESt8Vo9?k$@{#wLmY7ne)yGHoaFT&ecX!V!6QX!6&KBfQ_SYC5Wk zqByT3uxKj=Q%!E{-A!EYRam31<`<>HSQ$5+-;f(wn3Iczlko;EMZ^dd zBuj4E_zzT5T!e;!E_Ps z*;PZlZu7gdt%8UaBQ}%kBvjOA^?CGlO*OuclpeW$(KRKTIn8YMf8g#u_rn7tqj zmm?i+uzGvEG5t6gkNIaIA$oN5*bm9CA&IG{0k)J`$9x>^9g^)GbB{uaxIU)Qo0PxB zRkW@(==xY?j=%xJ5m_=U837Wbway`+KN{zD2N>A=IO@tgLpUa||F>>odbFPM2?+Gy zY7M3EWIuWc$_odr+u;C}nrSf^AN?5mmx8&{(0n1Qz9F`xv3Rzs!~5n6QE@l_K;3+M zb<&XHVaR_Z$pF5oRBInHPz5FBwv}Qum1r>gsG#JTF;(l*+Thu3kwE%sr)w6jsmlfe zpX6RTW$lQ&>8w(Y2&SCa8}xnQPp>7q-|}U+NH5ewX>}|Yb#S$??fH#TxO;ENR;v@H z7d0zhc2H3&4}VUZWK1H~p_2HL?gBW)pQZzHaiiE_aSSV%H5RipJlss$dHwwZwxGvX z&2SC7ED&K3X|a6y=F<}tEW)6WeE3Amwejn>S3}>MXhNp(M=>t{DY#3qKnH;*`R)QA zP?U@4Z=~CcZ*Q((Uh6VFSp8orr!g(=oTcoVtBe|tVGDU`yI!CIu(jyJCDG!|50zE(7tj$^I(dA4>I`bg!P@f@$!GULl*HIj~?2_QKzPCNMGkw&xbxN)M=LZtK9I? z%Q(}ta}QK#rq`v1TjtSbP@DU*Kq3{0R12##ke$cXn4O62m2gk4|xIrDv@b{@>DRLIbK=+s(e zlp>t?XCSrM(<#=6MsO=Onp)I$jk%D;`dVE@*y;DxipmIz(&Vvqvi)6GFkZgUJp$s# zgmJD?{2(Awhis1J>2FnqAKaV?f`m-OW}u;Zdt%wI#)=jAxqDhG`aChekcSX}1Cv65u9;@;`2(ysp7g>5cN zv2p`(#F5e9jz*pP$wwG$3akb;iH%-e&#s>zRuE+mK>5g@Y+e^pD`(Xeq(Wb4SALol z2ajXu=1Ct^#2pC51hOqK775Mak4MnRep7MtUaK5gr!D+6^`K`ZTgRbU7L}6F*!Yg{ zuxBNdu+juJ*{Bum6vOcRsGLW_}S49OK4LSAagDce1spVl1JAhR5_%UhwAo?rcrA za=~Z+@S8rzBP(f`G6^tx(hdCUa>=97ij5vicB3<| zxoM{Tzw~UYdv?XzU51cv$b5)Peey&HT1?wN(65(mWy9dE_mK=f(vD4$NkQoB9Yc;( zWM%mrHxWmnp8SGiVxk+9LO*kpOjngKe5k6?uGJH&Vk<^JN%X;AWu-gxp#29H!jP8c zbuMK7HI-7g8+!OtNiD6@+qmOZ=TAvZ8NZTs z-$wz?D}Eq`pK)m%Hyknf$9|m}S-B^uo7bD;3WgBypx<0!_!Kvjnhd4+T^!*r4Yzl~ zq!3ptaQ*p3;<7JeG$+$}fv3}^QK!?AQV|O7*&crn9Z$#>bDb`7IRuoi?YNd66-Rvf{?5n#HZ>>kDt^BUmwX-Kvv&`CgVfZU;r>Nb67Z%d40) z8J+fEeYV%~rB>iJO(Rm3d}Nm{lk7w8sa4Xw7WW|k8ghPn9%z2WX{DL1m@PqHIT~}V zc|GsaH-6Yt_F7`mvk|?u=v@3}*rX7O7w7X8q`qCC){rq{ETi*GK<>^6`FGV2YN?YK zhShAWH9n(GmH_-*rKG6h4**(&eEnc@rrK7i*>S2Z0u0zK4Q^vbJsS)Pv87B)vfYaZqEQ;{53H^~D2R`=PJ^&&eAi~AjD8tEk zkWpwQ@4ck>gQ;r5A6#n34gb!}PQzw5enIXA=rwE5VJe0GFa)e)dYVV&keSfsaEQZV zoFm3-{|?L`STIlkq!+~1Pf6airi;J7j|;|NZ6W#Ed3ZHe({U+>%!W66z%uPr4JKAa zifA@M8r@!C=iH}le~}hw!);(ydm&O1!eomcGtj+7Qrp=HE1(r*48miC0plV_WBglN z1(|f3@+Sy2G&O1ERk2u@2(d?QewA)kn#jUqddPnRf|v4&z`YQMfA>)pXobtA{MHYi#YU*cX)jE$H+J(K%7wA}CJ>A0tJJJj}RZs%POk1ExRM*J6B_kv?JYQCGul;dy z6r@|Gw@ev};5P1k=V-R5gY?WV88ivyB>qw*ZerQ)&G7Wq6Ob$pOwh{%u<%XjqUtTZ=L6A;Gp9VszB^{8eIPe>bSK z(1d@W6IK8kKn`XjSE8|u0wbZ@`?#D8@~3DTCi^JZK2)~Af-C{YBPLU%qm!&jlM z5Qqg7MjP^ElfJf;wuFEhQPk$^S&w)6mT`-7o>#kSO{cwu1Z-w3I=iQ+3!n*7Yn^L! zlp+8{-JPwL=5X4Y0woGyRrmRJJA;yE`oT=STe~52J}uCm-3?Pv>P#O*>$f|LE($8l zoX(N4<##_=ehsEJBJ7<%C=681v_9G81mcdzmnY)&f7g_m-%%irs0$TP{l0CBnn;xu ziy!@g%}YJBypqo0Mihfw9&d?QE zIc%-Isorb5^)*9ex&6HWbVHeWqTRWc7XU3=+Kq|F###Fkf<5c9r#%0IpC5fXc~tgu z1MmKf)y5~rpqGpx((z2&Kqx{8$!hO_Q@nfku5U&3zU2bCJe`r`!s`R6;hrE^HUH)C zr@sS8Wy?KpkfFglA2fG$88lBZK({!oc7Y(!rsejLu4nlZb`#GcA7cm{T7>hO zo-$}PyHg{ubOaf7y}y^Sc-&r4GJI}&@Pa06Qj^UE%(5xEUQ+`GH?kxA{;tuS^i0V! zwUdc);Ft0Lh}#RwbplD(4?RJf-SF#gH=(2kVP_Nm3>ZD;|x7ihVLlXD^WwOpu%YMZ@opIw26 z|M%h%5rG>zJlYtE_bp4h;(+3qL|itxjF5Q}JiOaszZEvvDRZLlXfvnc6|)wEWQB$E z?-bJ@)A#S_LuGUcp!N>5s>(heOpuO7skB234YJr7$Y=JP0XWmM;e`wj}PG{{U@`qlD&g!kDsfD)6sT(u;&MAWrPUxm|&v6{>)*SI>AU~ zIQVGyAL={ZWaNXCC&gr(V7~{Srd?QrIo$C5LN#Aa(GWNTe#mxT_E`hE>0W0s=$lX|G98sA`gw%ntf8A+5Ve@`oU8hq_L!7aeIT?2`ZKb0G-8@92cB&^BRqk9 z;AdW5T}{wmTdOic?_7YoQ_#@hnfx)Kr#d_uid#C~WWU72l4C>5`JVAbDE51rHr5r{ zkREuCPVi&zpa}(yx+hDo8T&hWOBiv0m$1jXRs|eversHvBru>;YnNLAvN@u;D0gh zA7BI=KNKuxa&@z#`M<6flD|KPH8$&)rbBb+4V$ z03UC}p#u&_G472Si6<)tvF-2qvxTwvJw(ke8mRMTw8RJiA>??GhLTE!bl*fb9*6n3 z&=`%ZVu8Ie7%H~Sm{^MqShQ2ztoPbjLOsyU0xCyrV~qRORN&;*X%`B)=Tp#)gAQa< z&Fjp~&x4*R&-?dn(>zCqOG%to{&p*HK(zP^IxE1guh)YwJ#yQHzpU1U1xPOK-y^B^ zXYWCk$9xqm>uLhPgi40TP0?#iChq+LkR8B-MgU4eoxmMX=6U1ZOw9or9H6mH#Y&O$ zG0AAFr)|p4&h9d4{Pj5j807*YoQzWOK0H^KV>4)|ce5`)@Ys%2Td3XlquF!iCpGst zTo(AWnp_p3ZK)gZRoF3c^4E?MwgIdtgQ;MmMM*#GcSc7dm+1Y~E7t0(_7|%@g zjzLS;5QI<|0D+al>!PC@?c@parbz`kW$~u;tO3Ec5#7b%c=RA#W@I&dHPEDHL0o-{ zXrGdfrw;{Ej%83_yqpR7;D5NM-T3@D0jL;JW>t|@wSD+pYojx^{)z8+>1=|tY)BJ* zLXL^0(nubYGAkwGC3)~v*>kZ4?(m;e1W>Ef^0sH#^YM;7{2m7^x6{{HNwsE~pi1yS z6AUXnTwd)t-@Do$Dbp8y2T1e#O-Gmu#OxBrTQ~b~au$;vbzE0=Fb#^dM=X7x)#j-8rCdARu}P@+R6BeT*>gX z`*L&sr<|Y`8u4vaD`m|-$3Y%`6_~1bqhvQBXo$y@QzChcrjp@5dkG3DsKi{%|CgM+ z3Em6YjRvUU5r+(oqd!w%=FaKfd@u;QvH+&k9Ze5Z+X3@Lf5jhTW4xT|_GVo-Az33s zxYK{C!5Gc`bi-Sq8Ilb|WBE#XVGZ)YNj_39jUPgE5@vq4ksYq=qC0j&DG$KOIS& zHMTyvu(DFB?Gw=5J7wG1sSspn?|DJ1d}{l7Ac?DMgi7)_XZ*D&PjL^q=RFf1)bC$E z)(8U=>qbp23`2b`niT84%EJou1em9$1=8A5jb3(i+@lh*Nb;KDu(KK3Z|U8EX?Dha zaVokkSY+?No}asd_Q`JAPR9+(>AyL+0&jX)*xCV_vK-LbImofeyn(o!0$A70>j7yJ z0N$dvtm(iI;sE}}vkb+Rn2fYYBP{I($7capq@VgK7Qr}xf{SA?4J||X#^3zTVC;`< zL718knoS77GcY?4WZaxI&;#$S3Aa|TWURoL4p^V{;qNzhzoZXIH?ttSTO7DQA^Y=+ z>J>(;CPRsVgrJ$);&CzVtOMrw)`Ll1FeqU5m_zSwZ-xsTBssiVuZGgt(MWZ-zWM>? z5MURbpRW`g9RK~~UH5@p<{Qvl)huF_OIZxb`KLE|7OEDaf~*r7x(vN~@oCF-a!UR;{pZ&7;4 zziDFmv69S6CnKpAJ2Y*G z)8J9hEJ-kzTF%Z6y`w9mPl<^9A3hFPI!)zuRkV`fGMV6R(Bbxmb*0kbgu5^y!)#$xVKr7 zLvvd_&68~EQpf0Es+*GGGr0tohx<)8VM!9`F`B#!GW7i%6@Z;yW(66SJoiD6PfojnQBwmbYy4Nk&BJHE${d{t_pRL?fNE>76M znf2QA8WhfNx&Tz0%_9=rtjNxQQq~#^bzIak)%KL4hqP zuRVJ2h3Dt<^rdn4=dhf58qcKF5x4>e^7h@;4=o$DW|7sDPc|=H`daCENBa7t8KsR4 zp9xqmMzUchkF@)usHo~~vgIHu3Jc$Ejmz-|F1Cz*b_*pXM9{Rflr) z#=XsJphtVwHeRYmj?b*4F#Rd98k_%_WEiQ^!zwo8z6|HxB;Xx#w8zuEeRRR<_V=|+ z3im^1m(7r%wZ1Pyv*YbGv$derbJp8?1Fq(T=M|H{!#{XDM?<$>cQ<)Dcc8Q((R_Jg zdw!t*Eg-Mtl0i_l>1wYLrfIk62}N)Ru(cRSJo5cy$}SzjYL!cgjh$1o+xlY0Q44Hs zl)Z$2dC1@STH0o%8{_g*5YR5b$~#`qt_y=XT3GjL22d+)kD``V@s6fWXI7jR`tqU7 z29UfH&Tn6Pv={}>IQLC3x?7mDaW0o=^Q!#GE_gvJm(P&x?zEi=M8oPW*QN;Zi#)I@ zYCL%QlX#@9;=%SF{fZFU=)8NcBUS{zy2J7;{|wTMbzdbdH-sB5G$Bn@L&6uLI5g_L zBnSfuiw4!d%J-`G&O1+OO-<3s-aQ@5l|$%63vM!Q_HC&D3ye1o zK^(V1czUX7qYqAFaAAP%?C5$Zj{7T_7lPSxx5wMur8kxv`Y){M5vsX5gp;387xZ4D zxu0wVDrRHgMy?4+eA#FA{s#6im11H6kGd@rKEq4LW)8oibdI+-OKDF?IBga}!t-Q1 z`v5vDnQBq!Y*I(*stJ6dGc@2xr!~GhC54rJ0Mm}kAVXf4pYzLkfMM;}4RSvTX22BD zIVT8Mg+TU=rSd>3=y|o-$fE_F1@0gCENsU6OBody)Rk&%(Aky3>(N z1tReK#xe^~YGTP$1dI5H9_RFbXcOvw-ygU`@yrp7vdf>XVzZ!f#~Dm-{dzvd4O++V zR3>&x3q3_^OSJC^#0l5SKUafRHBMO0;wP8W3-5k5W=rZ%6m6RKcka9gU<~@Os*Ci2 zP3Mz4ivgk-IzON|XaE{kJDZRhVpQI;}?x7<^N z{uv+)OZiv5t!b&3{AS9?A|!^@y-d-UeZ=uGhMnxRIsuoBCcQE`8pd+{%Fse!=Vo`u zQs3G-GbsFrO4S7l_vJS6<*08Yvm={afe}G>SJ$QmsDBsIH@!o_=al&tZsQ4+J*76g z6F6Vwd>O&EwzN?MwMIw8SHL@&b0}|DhAXvSG%aK%Cf!+Fd7isBw3j+6o~%s%+LX3i zMMrCTK2rH+N$*nBb$+O`366FzM8)Q(h)R7cE=Bfwn3nAHR#9lY9uM>} zLp%b}P*DxQsEmzgLBXLT>uEY(``j5mX<4$(FnR^Ll7+wU^2&GwaE7dr{F`9BL8ka{ zeOd8G4e*hO$y2?l!X=CPL@3BrUR~ZDQhTX4xZ#<=4@o$19{3&t0VPQpbH z_^DYhbX}p{1<1GFU|=h<+Pc_@WN+ar6n(YH#XSSJ0rF3nmj!u}Hv4qahhtf@g|ui_ z>!ccl`|lzcnl=C!d9Jy5fOf+oo$uKmAzJjyfpy=lwT~J01zCVN`I54F^4?#S*uMj5`dWdD|(!UDGkLfVYpe05i1O)l{5`-WRPD_WpE zVVH(g3@=|8D_T0T+y$(oSlv*4hI`WjXsqp`SkcJ+;QS`~p?|3l+auKM&X2`quKwaz z73*R=eqTGTnd$R&tW_saZT6j5ZQRM>bdb|ijPRcjDDSrBgU(#2zu|gG{>7dZai%UD z&s@j9dE;~seGcPwhXzLllW_ilw*5e@=Vnllt^ z0z~nTN(@(SJGs61o=!8KUHs@~Fe;0niw9~sIv2hBv@XpUFkjU(#q|3<3bBAgY~0>b zt7*$1MODk=Q4s^fE&LW>5H!j*< zbdze;d1}rF0R3U&?CbU%NAx0XHgc*t5|jM+R+_E#(#$_&AH{HP`CIV|CrKncs#@64 z)PXC4>4bmqm!YZiCuv4YS@-hBvTO+NY=#s;Ve~N#@JTdzrOA z0Q1z}9pFAe#CdRZ1h|>Yoq3UVf+{d&;et;&%R2@ega0sV;qmdwm8Tkv)Z7U=843iC z{X`l_A&reNnT`QS*eljI&-{+u?;qF$=OIi$A{5t?BDkH0yj(AxcenHwv5=OoDpKK@ z42j%OgFU@$G0T5>^D5}ik;F75l0ue{4pcU%NoNlxOCuc|(<&>mnuNYRgG|*>BfDoU zcl<8ewA`X#>Izi6pP&+(E4c%k{C4uoLT_g)dXqBodo*sA)*nS zx@^O6;Sp#~+dyuDN2IQy(b+rm`RY;IB7WfaG@8J`XHwt8B`uK=5#%mht6UAWk^$Y- z$JCv5L5<-l-F_CeV2WG_vPxfP`=EpklALyv|1vqVaEdb~|13xQHYxhU^46MtNir-o zR_)?^QZb=hHm%yQK9s5`m%s#TAuNP|sI>3F8IT8(lZuS$Ic-e3l z0;q?XyAKWFWj6Z)MY^pzHXjBS7RW|eN2Fw=E&}E!x>YekNb&LWNnO{>MJ{APk zm7hRce9}}locrs51!@%pCuNAyOMh}B=4eUMJ$QwOBA2GtM;V@%3=7k0gDWvgKd}8t z>rd`>1P&qF_zBq6UrA{%|Ljyo0a671m3#`^#mibXHj+H1+8JuVexCBy3a6<5<3yGR zS*-QM2y4M(G-#EBfb%lyDH+#whSkSY^3q-13L4oDcqSqq9sJvVd|}1a)L91Ta7!YF znEw7JyAB@iXD>EW3!k7t`R4S#=!dNl*BH*WN1Hc4c}A^HC#;R;aHQNW;zlBm--B1( zHov4GCZ3m48Qq-PfDs(>skVY4oG|-l*C;*8*7YsRH*Ya_`Z~2R|Jb9O^g>eUDhZzO zD4?PuZFWOW_lmE6wG@qt_u2IdFX6|K=?5+3RvB!?LEu?O03Wt{=}3*$jQRFT8JJeq zG+aW;neYTH0D~OU#Rcnl2Z8n&1*0RCw-$`{!*<*oC&VD){J9Oi%Ce{}q0?!4;=DVT zc&%TMT9f|l#e!tx>F#jLP4K~4YcP<9z-qF@gR|5R4M6hY`x&%x1#;z5{Z*|#XS-FJ z%%J$IgO|a27%6OCdD`9>AZm-lRK&!^GgWs#AG8vGWj7tnD0SwuZ@fHPRB0RmaWvqd z<6{#Ld_dAL%rvt;krX@Jq-s3%S#x;~k-ViV>S8J> zLp(w1wa>$VudypFKz6n}{4%sKPLh3BfNLMQ;MM26+-k)_kvB2)G5Ddd7*PfFC)P+$ zsP}b)nyN}x@`|zGC%oSIS`|)Z>_YNE)(~dakj3 zmzNTfv2418?DFeBHJ_LSEM1*CPc$+nxv8f8eAK^jjGrH5Lt1{2in@&#D@*WBpneYt z>yFj*@9){9a?kp3f~sEIcW@SKyeXj5Y-#6c{)rglx;N`nV8@ip-zImhthxRIb&`tF z`dpPriL#oqiTJ&t_6ba47ji^A8@|(a#ea>LQ+a?zK3wGHUq%pJRZG59hhE&u&S5eUjQ@^&;Oht7MWNZAt2 z*)xKd8&sfM&4%y{ea(LG|5s1iF9o1mI<2~=a%u0bdo`3b1)R2GlDMc_dv%&@ABqR# zjZOZbf!{Mcv>LAUY-+@Yh&SCC2!ZeO*irg>@W{fLQp1^mT-a$1tg+0IJ??AHh%-Ng zvDk2(i56uicg0-c9?lqP4G>L;v=VLwIXvAdMXX5c$!oPZp|H2y$QmEYUb#~z zDHrBHa>{Z_rKe}#hZgUq^7OHs!3CpBy`P+f>DFbDd3BfynoahhdFBMeIG@RI7HeRq zGhE@W;NN`ZhYw}RLqz@aB10(L#2QbjtE_e{(O14+eVLT>0rPMQ9=qe0E_R%X(v}~r zSF2YUr1tXdjQG~Ga30?8jB&>K_~GCO17Wk*TK6;WIj`B6=Xzac zZip#Fp8^r$riMgScP(&S(o%zy$ysin%cYzb4B2(IL)`D|$HeTp{y_~y>g?uA zHX_J>+AX$4fYPLr>yV>Y>HHqZ9;ZYvg)0`bpL5&KdTL#iSpXmE6OrMDmFNL&uVq+<8| z3IBxf1Fm3I-OA9d2a|9yS0NA=ypa_R?V&lSz3(4bV`+pQ3d1X!ORqV)y>HA#a1eO& zs&ACob6BQ#Rpd4@*DMgricd%YJxk^Q&-?Dq)ZOxSKn8rBBP|^cqN2Xq&>tZUjtii@ z!2it4ooEQDtDBth#+j>G#exjs0cc|IYfRgI9OO_oc!;E^jONM1clo10Vz;GrE4l-X zJRPL;CU?>cSMH}mbu&m`7n4TWtnQfbj)dPGU8+yuYFCK|2A5L2l8aBHrZ(Ac<>Xk1E>45QOw$M7 zef{q*WW7*@kWP~E!)BW2>Z18jO%&!B*E1 zmp#tXAqIw97XfDh=nR2iVyXH)e}1EL!;&r%1^iclmDUC^-Y1#tqrst|pkR<^6FQi= zSz0pik-c@m$Bc|C{8(f+)g_JE1xnK=wV$?ETgoU=;XmtM^f|6?%&X~s+`Ud1+f-rH zZ+WdlvOm#s_l1-QGY}iUf(9Rv-PITmgZNoQ$cODr+CVu?99V8GKdQ}R4j9rjFsm^dMZcC86MO)#=p zDXt?YMDfFK;dL5~LA80Uwoy{GXXz{QsU_+9yL$TF2$3??BW8>ehaxH8RiKBVE4 z*!462FP)tf4$fR^%A8K02k(-)i;66C8ojkl1;~V)aD4O*^Xi}VMUwuFY^`gH|M}|| zTE1N3&lsR`bA5T#h1zJgKli%WQ$CeY^#42x({CpO#b8K_f*v4m$7A{DmJYHry`4+- zV*hrn)vd^%wr64~$hR1_0R|hcW5{+nO&9AOH+jENFeNa_9mt%N64>&fM z_2aBgFywU4q?)j^THGO~>gvp@sOGSF6|IB`W*$5qZ>!OV5xAFPDLJkX|oTXfKEa_(EFSgyGyXc~9e{z(nF6Gu)WCuxk*V$pArXqvApLpKXzgj3o`Y5luKd{KV8|``Nm(1W=w9@qXDXPJkNLX zY#N=cuBY@ho=~n^)1`$wu}o{c3xr(Ag&(tiD#roIJ8rU2krf0|i9k<)MAywZyXjzX z|7u$8mz?I{TPmbCwRZDzSzaw5*0r>^lhYe8h>S>!j~nf!He9nBL&<_M$OFa1VbRjd zQ2j(E+*b0di_sctO^d&8Z+mXii%mu8sq-PgQ-#gsCasm-)E(o@r#3GQh~sgW%1TO5T_NXLA% z!yGbl=a{obbYB-FqMG)5zf2dyI%Itv5fhWNyw~O(zvFRg$pKTK*jE#ZLEfM`&>gkD zPS9%oxt60xtrIhkYLg|$GU8jw?@+dVsq_>d+R?PK!M^9@$bub}5P_wh^T4qv)yXedK~SV>C0G@QNb#kM(N|BvUrtnZ~b z4lITei~E>-lnP!<6ej3t>*z3zWbzg@apjwWiaS_crtr0ab=m_yMeXCe`P{EMQ8n}V zA7p#=J%{rd)o3ntL3*QMD$}Cg=UqK9_sF-`me{<)h~nI*tRvc`CFk4oQvMMp*BZ7n8QT zLtF?Gx1;qw_r#m%+T9(75Uu_&pOvE1Vk4j2`vSD(jJuQ;|zQM&^SQAqUCtw@3Q zdskOibz}4mHPO+@Nj&rm^jDzs*nfbRSAuWZP(O^LZu1VoncZkDt-))vqK&&JHb0Er zW$?33lgZV(UWsoMDG$+$8w~ZK(~Z$i5G_%z|45o(NC(T1M8UV5)mMLC*#^6OAF(qr zzO*;Vt*%+K#0dZ!1W#Qd6V3P~>gTGfBpE_pcwxe~BxH7W1m_2I?oG|5Iq-0BFW3!# zzCG96c69hWxu7U^P95+E*ZR%U2pVpG+*JGLs3xZe6Q}HI=VP9jEr*C%(86o3xqCA_r1gK8`lI|^LK0NKzCl@2p9k0@A(po zV&s{N_x*A0uRfB^l+Og})O!bpQE>?B2wb?&H5l=tPmlba)o|&FHE#F4M)iMz)2A)YS$dB&%br6v3s{%v%DAM)0< zzX})c3%Ntv#wWv#JHXJLra%zgs~s)3 z?8A${@9XWQn*82eX@Wi24(-jQwUj+K6(FYf#mZPe<%&I+J!yRZtwa!&c;EglRD|zk z?xvsj$J+CV;0#4UCem1r^4RpgPGT4D((Gt?UkJ6FGx$p!`2ze=2`vr|>?}Ziy6>%R?I*i= zsb0gXuCJH0mb<>etrySRcZYO;7_QR9Gpl}bp-mKBOXY%E-cCo(*4tacKmxR#wUZ}` zbs>uv33}J~lFNERbj+xy#CHleCs|a!LUW$ih~P}!LFpT?;Ls4f+r&Fq zXbFnm994T(6#{~-Zgxu}i_oQkJg=GS%@ohaA<2ZPIK-Jc`>v^7_7gk?bn^YjkPrv= zwLwH{ZN!8LIzM@ltiqiKj&3cu*ed(!VEFgd)|UyJBxU=GaBbAP?HBA;Jm$~!3y+yYK*IYxoOuA`g0LSyUf1pyVxYxo z4_PlC?neBMHsIwq52y|kcxZDa#5kOn_yWT?!DT`}EW~&{z$zI+m$JfN*f)`=$vu6# zEgwoX;mNw#5WtTsw>(DSFA0&Mj`Ki)3V9)PZNOBaJaV`rVG8;W@T`l^hKa|- z=N0|kR5cDdiW#9RGy70AGoxA#N#%MK`kD%D1d%&envraMogEE^2>F>~!Sizszo9Fc zBNc&^UF!kb6wlt}o&uBGf-JPJB#I5fb380n%q}_uucsbj#p?`-mfjA&`uT!$^}37c zRg-mO_U!Q@**)&JzngZeo2yAjU1QEWkDU1SUAM9)6@ghIAM`Fh%Z(hbOf<#=Jt>}P zNTcj$lKulmxpr;$;Ka~ajjVAitt0xK(`yQfAd!(4}Et3N*oy6IF_o?su@DLK-2gr|qLinNCy>GMvXkMLj$9OKEKM z^yOIlH#kGnD;S%iXYxn*N@zSY&(Kb6!0NMuQ()9(UQ&se+%pvx-e1h+uZwjJdU$Gd zuC9e4eoDu8T3=j;4QK9E@mj#C_b-c1b10cAzVS&_`Oz^(_ckMYA?$i}LrC!NL!#`) zQuC^CcjV#9jS3^bLmz8cXow#9f152mY@`xkpm?=P`{8FQ*Dmmo(1HtI>M(-7qH+7~ zE-x7UYhWN18`~@Nj~}fxPraNsax9j`G#ich0H!sP>BfPD9w?%Eq-Q&0y7PlctWmd@ z56V8podln;Ihf=*ysJ$rl{)zdSF9PB-o0b6z{el>!THS_lb+ST?SccvIn2GCG{L5A z#k7>t?IG9Uv_C&chM0%?76T>XHcbbKE%n=Yg+PP|BGQIzOhUQ`a!J}A)$c{=U7inh z_G^c``EXyp@i%ec#koYlW;CdYxpX&U)hOF?-9i$)b6 z2*-9K7gpBt--O+l+im(rvAm%qgsyZHhY@f?nJ0ZN@{rLZcc(w%@>@dck@&B{BB54#n6s@*rItp0IeV)jIIiUB7Pw6n2#({cU86o<(sIbJvDsQ)zsm2_~@ zrp)KhAr+Z)oub+T+6j_$CKX*#c+8#OWmTWr9BO=<6Ra+e&wG-;YxfmyZ${+(^um?q)XUH)PXH*17w)7m~`S^#?@{P{-xUJI_*w) zQ2q9=0z5E6xF~+{Uc@#A;VBE;dN9-AUs`n z=<*V8HnNH0wz3nkZ+}K2=z_!(%ch?bO-W_AoIc|hLMQxR*JA6-X&v1dPV)OhN~~Frq$+~T+}+T#u^-{1U*i0E*~2)Xr*a+|IHS= z78CfMZY=(&=2Al!24zSKcJoAaH%%tBjm^!)>!SNBGRSCJ+t|aj!OnGi3^H_cf=;E& zRUF-ui+w1GlXXAbhD&a~vjvxWMHU&h$x=^y;Hi&^+nXK$dK57vTw)+z7GcTcpQFiI zSkd)eRa1i{%>1+HUqu0yslCRD7XP202^__|TQiGLMF~A^Oqy)uzLm)!m<&k85C8nP zRvV4_p)opPSQ5z&3sZT8t6Ym>2c?cYGQCqv2!*{qxHNSUY$a9j0jwaO{V^YoR$^OA zZZXW`WTSI!09%bBu71Jl8>l1)!Gwzvk5@f$g^FY4+UDyGD~8!fYG}~Y8ZpewYN4Zi*7eQRwX;t9 z1OCb8Xw>r;w>geoFD<4j${ASBZpooVWnF)$<+F|Z+(2J_0(@9{Hy3>s&Pue$ozpsF zS=D-(?Veho!N5&iNl)h}Tx}cWj_)7V=Ms<>3FAcSFe@Ij3Bqrl?!_K2B-I{8f*y`S z*w*@0Wqy&;b!7y=-Kpio>0F12-tNI&l6br(B0_~87CQ9rl$D@0H<$*i5hX7F;^;H5 zkO?V)10XX9X;P{5M-vxE4Fs+49ib7fcUW7TvAGN}n9HwFk069!s}|=Fz7?1Blg&Js z9Ej1UQ?51mXCV~t^vAYtt*sQm<@Flm*bHedNkpoaFvv$MW#-{%#ZZhykrObGq_yD% z8HfC?Gp6&VLcLduo$8xBSkDQOjagXCds@bTLOh3+ZlqS~ zb6KU0jEnu_Wkm8V=6KBfvsSvdVIMf|IMPgm+V#)w0;E(7r#cs&`@Q#LIAvm~LKDyy zfQKh4*(Z{9%?fC3y`Xrb2GNUDxXByL~ z+fqy6Iiu9V;?=1V4QH_6eJJ9nurTk#{_j-4vc9_`|HyKkW!wBpRL84PyFp+%07REnd0Ni5l4DKH0N_9hMaEicTQP zsB&3Pc6ZChoz-^V0_*3}%AP++^=>mu3{c^}11>&CuuaAhJw88;;8CJ8A+7`qZGTe^ zsG$y6F5kz>5(p^K-MxUrM#Vu!iUhS@z0_@@IWiHVInhh{d*EM6#sct#ec1ZhN-|$q z_`v5;@|M5^?`#Mx91pfmEKxoqcQB5Oq-Z}e%G{}7pc8xsjoCt;7hg7S&cTF>G^kPb z1tc7dkD>wOFNwW`f- zh?IM&oL{D<5i>AhV~#;J%&I>i`KEv+Jq|H&cW>*2;W3YWcT^20<3a`w)Lv;6e4~HA zb2Mw65_~aUpva?ez2y*A34{cx5Dnb_QYgvoH8UlA^tP`eCr(Qagwv~qZkOsbazv5} zmDEdE&wZ~6TJNHus?HsFJ zpBK~oIR@}wdVzO>aH*E&jKnVt;AxL1NUx5ji;(@rheCZ-Yq%=qech9EbunvyHkoWS zUgRh2bNlndt*E7`X&|}p9ocN1ovKe)5Jq2IUzBpWr%OUkQjZ`?BCBpexrR6>s)u|5 z@EYHzd&u~`%Wj351swn(G0D_GmmPP&&jU;~Q zL1BF$4w0N$@9RfW0ZNXaub<%nYg_O5_Fp!$HH)%yo#2I5FH6|_=$*LmE*gb2i@xPx zO3ARFAdL=!OBmYR_R3u%pbE46H$ZT3v8J}@8B0a=iag%s-AqArs5R_N-zw@T4g#EFT*__7d!pQ~D7laqwn-z%P+0AL1 zax^tI8kf5+mTFWi1z-q^0uWZJ8%@rAT8UB)+|9*qVP;q{$d6dedIpuxoTjrI(*Wdn z#9`a*$8*^q4r@g*$+r);C2!;=%d{$w@}hnKBQ*)X`)d#w?Mr4iW_Q`a0Sr9p^C3rq z&8`$7b97$;CwNeXt-2du`Q$2ldIqt)&5+lSE?|0Wy@rK`Jjhdai7h){C$}ZOV*-fD z<5-3=v2Sm>rTxGBVFh;=oU_x*0P8v%PgaMOH6#~u$K{`r=a0Z60DDOKk_|sCC@47Y z30!!Nporifpsy+}v|OFuxFlfG^#E1yuw1KJ3fG(M`im}{Ob7g2g}83 zs^=;GfAjF-c8qsY$pnd?h>`a8>S_thLGJ-EW!y8vbH*6Tmz^09?fW;hnp%A9lfV;x zobnqUQ3pI0xVV}3&}eo6)-XO&c1b~|R}XL#_P?DIbA$8dvc=yzG*HZIjLP#4?VXgw zSE?%yhj7^{Q(l;snK&lAhK5Ew69n$X&80>?i*D1>N+1ebeR$^V5ioz=#CDcm*yE63 zykptG z+bC9_NO(%`w7R=K23>SUXT^ks=n?Oel#bNeKQU>yle1vtz~mA<#y0W(R3i`NKX!isM z&zFwLp*Xr;MHTbTEHZQgcL@HqbcRiJzP`c63x(zMKncqaRwG(;RudJ00KH`ZRO+Xa zOfhkBNKj@(E!sCc#&6F9Ugkm4Mm-2E%(zm@}wu@Q)-B_6LrvoOyx6qNS+x8$;Kl z;d(GvJW0O_vS8GZf)6Q6ntyOVQdf5lP7|dtuRMO7?3zU5a#EAf3r_zrU=(3)IC^}! zI(y}Mh|$pU=p9?|svkc>w7RK^u{@)K zEPO@-vZT>oaL2m-T{5(aeq(4c=Ay|4zg=s!0l-5*tnI;5$FbiS|0`^8%bct zD>@|tZo4z2!0jYPfRb*aN$V+gvw(^CFXV}3wK-gk5&icPGCP?`p&=hY0rAO9WC&e`+%{h(7!}@a zxV%Jp+?!e-+jPiDOVXQG+i;Quq8qUKbxAu{3h9o3rhRSkMM; zJqDf=VOZ2*yVmxR)nS~&%uHvsYcU<>4|msRTu(%AT!wgQJZEPxnD+lRPLqH~Hr;aE z*MXeXe&u3PCe9}!+OYVKl6=GS#DtZQIIgOzQ*qAd?fLUZMPE+S%0~R{XZ`KCYv~B! zT$iFBp@a z-l1dd!3ZBi{v=On0dFK0s}Mf#^-_Qb7(5Og*81%M?%qF;e5DkS^1wYtsYX*7mE<4! zhMC!q+S;C4k;m>}awfG)M!go^8;mU!d1sV2&NMcQO{9H_>1sX&vCR?djEO)0I==v= zwq=vlM^gp1))G=}qF8fJmp){WQy4&Ff?{x<5vc(Xv7dLTs>4~?DbG-U( z_|9@h({^fTh?t!DlYgONX3>2r19yXwf6s?-DUCfu0cUNW{RuenfZy$#qCOEJbS_Wn zGv%9vKhA=jD8RSAMV+y*|K-u=oGU1bAj0<^(eQ5pbIJV32zp*$xKd|JPM4Iyz)C&{ z8NSicNr$u-kN*pTf1zDxUG)k<@pQ8 zopE}b*}4~C0P9R-o@E2*w{%`#I93i*6|JwmEp0JWL7-fWn1I(HYQ-}fZ>A5 zenIdi)h{6Lggh<;v;#nlleooZcKo77{S*CRM3MGzG=K7QXCjq3{v=TEsD z?VFW0U0NT^KKu95q^fw0WblB8VbCa}VYt=7?Hnuibj>UF(-!!5E!IW1!S90Gzn<6w>O? zYrn^3X9Y^mIsXC!8MepCTIat?Y+$>PbFpJBMx^01=T_BP2~E553kfj-*Ed~&gOigO z0466O%#%vKTh3Q*`TLk86#Zjvg-=`HlLcWhzo+4hoX&^ zSN6ioK5C7XN?#VGO~n!By6NCaa0uga7~+uAf&3# ztIG?0ZawL_)9M&!D=!72E7alt9oB@y&jF$;b9X9FHjai)zY^}|{4lCh{N?WDmHm?! z>?B2v+jgFRqkdWzlzWiQwfPV~-WfMIu&TH40-)~K4dzcBlQhStp{+<9YR(twd_|f$ zxyW~b4k2;-gXBI(P>2uuAXrvbrj)xj1o5IbZOIgdIA5g1=6s6nJeJ6cV-*x6(TAQg zgX=W#1tPk^$#ctpdQ#=O8Zjeo?b}BEWXn}KIB54Wi05V3jSUN zZ>iXXeSIH(R91#6vr${Co1YWt+d`#~Y`fwQ6OuOB!-vLh&0mLh+o~R1&M%Okae>)m z{Uo2UKT}8mcy`7;!3q1g?+{wV>dc@{2b=l+0|MZ&j>^rj=#J$TNt8#vu$vT}zTiK^fe%o)p zhskfPTL7X*v(*zpy+Sk6(O9~aH40=8?7;9J-}~LwFAmIEIjV&plX)G*)EqwaBl0#z z^KeWFOUOzoOGtcOz=|^&2l+1RS&Tp0*(`Md0Red&F-Fx@HDV4jbK-E=Gg*7`{t_n& zjyg{3gMoycW=0)f9z5mWUH_)&$Gm&U+H|iC`eSKEQph3UPxo^Vi_OzU$Qf`h7sKYfAH+sBCU%FgQ(Q-kPc>Clm4gKC|{v zso(WA^1Yrm5Ri*D<4BP1b=KY2xWepjPyYS>@jEG!xy|@M#NZ3^Kj50tv%kLlz6nPQ z<}h(UTRrYXL~+=iv^{U00;i^UW({;z8bwHZ9>@6Qg2?5>E~~h&Mukqnz2M6(rZB=p zmWNlFyF4pb$Jkb0PBELihmXLhzh>73a>>)+xcB+WU^w&KcbxA-mAZ+Rf0{g?tgTQF zL8L+jF@W+5fj2!_3vF<{=Wn~ZlRqawimcFVgV$}oP--`KqVNQj+wT06NB6ptC)KwB2CR%%}tCx|#0CeCZG?mwSv*!V(y za$h7$NXRDq88}o3Gm0P@-texDLch0&^wx zNLF+9r1^)mZ{(K`Uje_h^t2*<6n5{3CZEWnF}>*Y5Z8`qJyoWve*$KFOzWv~jZHgH zip;a`96?xTvrsa*E;ygVG)g7jAK!7kVhaD@TSLcVrXA%=jB z9FU@+QCXv^KJ=CDxLz!NQZRvD|Ib-6kA#~?p7TcB|Fuoy6K=~IMN3&9`DAJX} z$|(4=SNomSI5_9r^Ykhv zXZuG31ETtA8y>f793%z{qqcYtdHBh4emW7UF_w@o_{1U%Ca z;8d|BSG#93AMpa`ROV0h9@@8}w}_d-TV9S6SRvuiofeP zEshKTiSN_x7n|S(pPM3#X5hFE6?UaZKCW|3U-UlZ^7lu5ul-ll-+EKt9+5J5W)RK+l#N9Y^NhRBT8czb0o?8_$~E7>Yo+UJ zBqTw_3~n^$Pqm_;$n@xTqI8Fgy-g3xB4(;I;=)S>fUwhroVj;+hvMl2w5rYXfCqi6 z=zc*)avET9Y21g!CQCI+BeQ&OWS_3Lv)1H9fK*bvo%chIuCwyAhOLy1 zvlf$g%=_sscv0jcy?N46HK*dq7i5GXKsUXhp zE44hTfnnTNWgGG;^fn9uBz6r94O4Qhn@RYpsJvX=o~*9-HK+G=wzK1Wjb4{_{~k|A zi_}|*#A;7r-t=#sf66_xzpN12sm!6s0Y_|c&|P>m#2P&AQEwWyakbPHsGLO@cqasM z^~5vgMb7~O5pg6#pz!h;IF>4F9dFi-#)ysljrDOq$~N>W09fo7h)YQU?InCp)JiEm z!>AM1YJCf>x8ugxU4yA9ArfbYC`ge4vg;6VQJMn~2vJ;WN?`AYnjG<?Cn>x+7=s+(tEXQ@z3KzZ549!ppI?s~a(vw58sVv* zogXxlsy$OVVD-z^;xsP~JE)lH@qE$?+>hQ!NJ-E%h)f^+FzlM}p?07+Ws(a@os)_r zLx2_PfdX>@291g`INySlK)}5*=oCv9fo6K{0abZCSYZI~9W`NszV~(h&70j#;`>i2 zW_WU<&RxA|S9{fpmA1Rw|7NF+CsHRE==d#HB%b=?4fMg;h#llR&)tzcWzEuIFc`8) z#pzl1()$3lZ1Jj9z&tJ06CWVj`&-DmV0X$Lgi2QC&mAh{%C0=y4zJ*LxDdD)d54EP zj(fT}szIJBa=nJl%gbvpw$k&j%7YV-gm`d=_h3FsWIV2Ao}{H}#ku~Ut<_<@7$SFH zz+XD|^EO8^r+Hl)SJB=tK;T)C-#?Gd`qi(T2P#^4-EZBk`xak%e$C zYR88ZPRl+%*;c$NpW?4ujdnunWttU{l2uac=B$31UqM(hA<*jVGWc+6-W;`VQp_KzY z`Z5kh7nvakahJ^*Hb`$Du+J$Z>Xc@^H%D_Ua9#C#g!R3uRXXi2nKmog@!%vIs;BW37?~yG8azz67D| zSVKpniY@8Z!ZN+DPmPXOOm~FEIQniewhDbNh>ulhvSUo}%*KRi<+AM-S}=0k((z;m znC2>t86Vm_P;zLf$k$HSrNa`ptbuGJFE@JpByE$%g*i}6>pMESxq%vQ$>Zb9sENR0 z@8p?AYg(&`lKiUq@VBM%{wi2|&ED%(sUJSU1+J_FhFQI`RNvh4=<=PrH6CZ10Ej{O z1G|I%1jb9oOIklJBqrI7OjPQ4168Bop~+Hxa$3bqi3<|GXht_XfH#ta$VDgweGJ%U4Voa=7{!7(Uqq)0FW{U?T=H#(Wb2u=~X&$Z0 z+NRQ|f;$Iw5Mg*mC!&gk)3%cwXhK5-H9PaXLWJ)Xc&F#@gk=%N*iXB*d0O&D|LM)K zVC;y19Ed8!jfR8lp@#%M_eP2)0{?OQV_Q`UcrXxdY2sef_W)^*#pDE>(H zVyspk{HFQb& zLq%Wd%>;k$L9g8r{oqcgPPLp>(&~VRo8S9(&apib;n8J=rpnbHqs4j{QD!^ zY2N`A>fE~de0h+$=Bj@;b%^QgwOryt%sNc!))I3U=ZnF&J4 z%Gy4ZYqUqE2LImLYjg&u+SCh=K1Bsuijj2T!i_X$)qTYPdy1A(if$z-$ydZzr3C$S0bgZX}^Xj|b zzGuAt{Z^7T{B1U^7IT4L*7uDD0>)LO0e-cvXFlFowe7?_4?lr4!0y!j3WrsXWMAO3 z(td$S8|>fOF*&pa@kzmM8gMm=GsrQi<1>vA?!9`;-7Fon#ce+gmnR+h6VSaXZMHce z-CZ&e8<%ejS*TqJ&x)0eqJ--H7Jgj)d3{Q!Qd2Y&4!Y7kKrov)+l7d6?$!MWib`)<;uYTLxeSX1We`>K0WpTo zHm2CL%7i;Cby0J!`7-Lp=_+^9{B2fXBCEFF|KQKSUHa!?`(vcfEtUxFG1&4f$8^15 z)~+4b#yUI&7gis2SE{Ld)n*e{kyc*N0yqa(KCL1~fHr|${;5{1+U!?u5T5)GxuBsz zHZ^*VH-Qpg;eUMR?F+^3Gae31QUjD_mG@#}bqd;o-e<2b9~v(Q?@fQDLVjt}nq+pj zKiqJREYkqh5imK_M!UAA0)l_%bTxxv);?UQN*V*3n|jfIlT}GnU!D)?%iysH1{g;Z z`G`pO&_>0dQa8-nH6eo%HVZ9{H;Z(YeJp_zPMag;u;)OsmR`=4JgUD)9pt$@dEW}? zBFeWhIddn@cpUh~VBDBsv#0RWB$To_+gdip`K^b2U7#-m_46NtjlqJ^i}$4e-lwt9 zN(2#ts$vWvQmE{nI47;TPq2ppoKZ;@KG6U2g|fVF_-@WfUmJISCE#;H<+k}(1S3pu zcX&=E9o}0*$jR=oqy#i8@m~{6CT+U_AJbZsf!QU!yfd*lQDwprNhpNA=zSmYDXA|_ zo%7-RH_WO{4AaYtLm`kvvEBCtyK%1#Dv=;Om;(}as3>!j{(JEO{T?24o2Vw(fhV-; z-oHj{&@L}}{S8;`(H}!ZOSUQ--c9W8O%}AIq=cBqRtz-KRszD& zIGbL~?+38f`r-MD2%@ZoY_>M4|4iTaIB%m);n~Sv}!f|4yL>zBiHCi>45th+g>4@4SHu_6OOShchm_;BLs5aBf~> z+~=w}`_Eu@a)0MvRikMbX1Ky$rMa_n7l1W((5kqb-nv3i*KWbnv)Rwzg3qedp9j}gHB~4{GM8{`3QEmclrEOz8{2nJ?Q^TD8_Wmkq*S)m?vJD7tN+s$EI z`~8EjBYw*bPrt9}!_OaV>S?r;a<57$u=ClAwa@^Z(lw6n+5L+`%-3n0?=Z>{eXjBvvhbok+(I%MS1i|;+ zrGz#_YLzg-G#kb4)Ai1qkN8dEt@^?KtCd-+TBVZH*{sVuLqft{MM9!sa1JF5ayLMi zpTGgck^TQ~J%EPAq*G}Wm@3c8Ow4aX*V58bX?|(fx_%xLE{o(Zgu(&eGE-|E40Nb$ z{lzw`dxcO&&E&*z0(KQN-A6E@o+m=_0|MhUS!)zkCb{c?94jfBjX`;C2#WrSdwx;z zkcqUE!l)w~8xcT5B*^q;W%ek$fb|4mv@_;!x}%HJe;)F6ylbsls!8Q`{Ok^ zhN&96UeNo*$sKYQjCKIvu-b;tr?E0E&VT=AR!&ES|7z2f%MY-bP((!ukuc|rkAv1c zgUuy)6ciM7Kk(M2j)}pv`mUFq1sglXE zKC>Un5+NTc3%UaU%-Uvv8@Sz0m3elBvGIZb=m zc6~TSRj5v{wTVurD0?B54FyFWG<~3>5xoV)=3j4yPLmtvtSoGsaKvUSOWpwGTBjwhrJh z9V{~V5*&=Ux4)11`gQiki+2;blJf3e9@IOHkP;mv!1;H-IV)a>b$7~{%D2|o&)p4K zIN<=jscL1Kn3so(Q`!sZ7yGka!03Goeb6eWy>t`TTYQPOy9yAop7L`)seVG9m55z` z1O0b33pa}~e=dzau~{5F8!yr$<#SqBdfs)hzxW<>OE~Uz4;1lTfMo=m@15q8E^upL z6u37n1iJZ1_f58hFKfV?pxhvXY1^Jir(MUy@9K%)CM?|Ga~tUk(F21({rCDiFEPNh2F5^8V|UQteOlzusLq`uK&TR&8ig9}-eFVSl zU%W>{@4eZe&&__N24GT2QMet<6{!^G?!@hNBE``ZiS~vp=Yb6hvJ0rBrc}3hOuN}G zya#7zyYc-kdud2B{2LTpmaji86Ias36RiqTSuXmaw?%CeVsvy}Q+M5O|9J!>qip z4Nt)r(UkZ&X8AK#O?X(#$K&(-mwXjpM+C5q}tN0BSgZ9_+;qA71&ggnBF>k(QqIoHs*Pt{j2mPFJ5wj>rQF+-AF`cE>lu z&^BaXAi)t=Vd*vsJM`ZePWh`@_c^5v@0Wuk+35o?8YQ#&3W|xZj7TA)Vf2&%`P6nt z?QC}z!+8;^k9KO6Hy^%`#)$})FokVqZ^*4yn=Iy9t?_-Y7G8_Sq14(5HJ93S{9Dt+ zZrTc01O=K;-IVVv|47(}%3k13xvn<%<2mZ8$z7NM@7Gyv67@#zIV9HE@cw97T+s9E z6(|e;t-YmyjEed-tDSuKY_X96QrfYwfS1N?FA0L%R-DKaoab1S?ZFX5Tpi#P910ex zTjin-4zKEL7yZ5^Fq91U{Ii-U3VpV(37UYRo?(9QH^8JHT%bT)d~Q=wp;>3cKGXUa zk5QeetEsjWB(UD%+Qj>erV3$?XjK~>%<*ZqS~cFpigLUu{Fs(Y#%gkP{=b$ctx%I& zAZ~+-D%&4&UM+j80T>&SXj86$L^w?N_lJZ)5U}AG^qf#LVDnYF?#&dpULtQ>p9nL3 zp9u$Evr-Nh(|>!ZT-LQyHqC<)+_7aa;M(q~5NfVe70^H<7S^eikG9(Sk@K(8nU>pb z<}2uRp#+e~62*Sk_4I1Pdnxw{{iq4+$!ri4y>jS;E7NMm^Adj0{H107c6NUu4-t=b zOpj{+0Whys^gk2GwtG*rZMI&HL8U^i-`6Caj(zuUaQw90buH7WlxxN7-DKPpoE}Xq*RBNpn;#}dj(bH5;hQ`! zw*zzq+>9eG=FF_1FF33>*RJ{x!6_l55}+AvrhGBXhm(;IZ@0EzzZIp~-<|X{0)CdQ zf5q592F$w@^J%Pho##LQWV_HLX&OV^x>L&oX^=6z^SJl)CyZ2))y^z!`3R2lVnE}~ z^%Yt77#cO5s+cc<-h>l^Gl5BXJMHm3>Q|GMA!pE}8{k>q>~cu9YTtKmJvDK^ZjXG= z6G6=NZeez}H>XcR0+y9Bu(%JMO`GM!%%=xOEERl3r$uamOP-s4>u>$s$;5l~`f1rBVt-aLPH z&+e4NgfA-wr{hFHgTD-@7-xHa`9q0dlbv3o&jNml?Oc}ZHL;nVX@oF*R^>h#XMNe% z9}ie}wd-tq%BHKaeGDND4u-t^d`7d0_y|V1YabX{zIh*BKB%rm~L~yg`&OG!Ca$KxfL!jn_z?0jb9e@StPkg@y$1U)^Cfj z2fyfO4GiUIRGsOdW3xj8*a8($>qd9k15Q^K=Dl8_ph)V)4eRCsH0)BZK2*{al5%qk zBkoTcJxa!(X_WMZ*_PX~{a0AWt6A%|JtwOm!e)OUSPLK=sHlYePdGbGef7Cn~Om*)f?y!!2JJ4yRq4b7#S9PUSKs06(5QDh>$v*p?)552(lejG#=l!D!;6WI}3{p-*_s){chbRj{mM z{_I&#wct}9z&{lVyk!(#7bIJMOL>}H_P+rnq0vqFw(@M^{^VGlfW>do&xex=2z%=! zd)tj4_su)~F~;ic+lxA4wD36J`IOS*d0*x1=wB_v?a%Qt?X;uoJzm0 z-+6noywqxUthDe(|AIn1APLmJMDE`=9+tL4wT98!IXUb%zSGBnW;x`ugL#zG+QrJ4 zJSoXv`4dyz+2&I=Z77j7nIRA!42D3fyG+l!nmiV8hYs1{7T^!%ySH=2Ve7tqby_xWdd;Pb`VcEZ_W9T48 zw|pqD3LaGg+$Ridjq@@!qR8eaNPG~Gaeik;EibK8YO=Jf`CB%*zGv(WCyaQKzmc}d zMLfcL!thJ`hMeOJ`)aL;S=RI<~Xf0c)~h?AObvp3p*>&d(TmVPW0ogWvT{hCCvamn4kx$bC6u3KJbe= zK4w8{7CpKS+at<#!q&p=K-TCaJet$9VJI~B> z{%{0l;KY6Jz1LdT^|?R>(IV&wSzUm##_Ekag+V3`vo?-Em7T`=_wJ9c&o794)Uh64 zABM9&A;BsbUrS|Np90z{AnE+2Ad-JUx#o~mtgAjX&U0Irb;+V%lh<_Iy_Kp~7>?cb zCeNvq7XT;gd^=DQM(cQ;PSJdBPBbc+iW3lY2QvgP1s-{FouzpZQuuOG?^8QzB{FNM zdk>QrgU($rOlC;=K@AA@Rx?QNu4U$(+$Y(xoDl#7)iUcr5slqhkky3Jo5XXS3RTaC zJc#e4q;PE3`1$Y(41A$?RFstaV|Pu;{ya)q%D}ViL=5x>YTJ%B%{Qkh+IR_2RrQzV z$(S1H8uqUrdwF>&W(O`+%J*B_nVOjt-q@_%3h~5Fto{haw$eI1>SJw|cxDYG$zzw; zLe?6==nG(&$s8W&gOtFarY;h~dFfSp>vMa)FmWdy5R5)r7tC}+`t%wu`>`b$4G!Ws5TnMs=+xg@2w*#$PW7cE#I&oilZ3U#pWBE(m%A zqdM2umX{;UUk+w}u*{_#p-^|&Cu)<4LtH1l&H>evVIKpUcHQp17TH08@zTyS&ge7e zor%}ClJ#{z1V#F;TVn6a-Pv~Dqj(6ASMk~aAKH75+VG?m3_IV&!pv~aUIn=vtdJaK zx2e*`v57f?WwB0lJVcEq1mWO^N^ue+Q6I_M+%DQ=CahFhrk>fjr8XdKdI`w3s?G~Z ztY-%5P<30)8BLSj26wB+#~|ZWO)^K79B*4l$N}WrM39NRL~lY;23w=uZ~;zsqA z@Ppq(Ml#sP5*+ANG;O(xvwXp>D@o+T2XQ-j!Q^MK35}mlcmm{`pM=}j@0y^;9E9oVJw??RwM?EumMrd*q!$MeKxKFUhaGs z7ANFRF3h(bD7f}(TWY{l@IGA>nfCG#0%ZMg8JDmdi>()OgOL(z!;jqkIhGY`+3EU+ zG_I*$sTVH4s}Fuh82&F$efcIdb__z?&gY179o6P!24v0{q0o=PQ5l%J#)#glUd)6J zlG17REic&UsiB%5Yz_`a288G;CM3%;_M z7HHRE0bx|betAEs@)LI?6X+0g5yTEz@tHd=dRQe7vSRs8YA#OBp=)z|RFX-8$d;@@EK_@4Vp7 zFw3cllj9J}s5|>*XV0E@mKdTE^sTdQ<7J4Ifb%8V?tK0CoAfB9cKlEN`hC2=*Wzsi z@=Ga{<)uoQ0=dEsKGH8$Ez#BPB?IBjp2Ek|F z(KL3a%IoXsXX{-vtU%6QTy%?0`ER>?8&e9x_ADbKEB*GGR6cJEKBwOjgpEY?2y|d} z_;;eF5p#j|^Q3kM5EU`V>@qacXXK=5(-2I8OotlHlvP*NNJTsf%3K7TcfMA4cT_G3 z-fU+X0LWuXo>MT&NQ#+^(tWCYF}u_xg{B62ps^j;D0b3a^`cGoQ`Vd5u3<7*B>0{l z-BNPR9Al%{^EnPpgd#c2_vXZWbo*kvReS@uukxHPw1@+yIRxwV8$GN@g(+Tko`;1I zaM_LP-mo^G{nV)dlSfd{sSL^oyP*=|+42u=0iIVhl_$n1c405l|=BeE@qjk^uV>y+KZL>{^q zFcF71%8IInWs%&TlxX8S+8mxO5TsIHR=?lA#&EIK3gYS$c|B@d?Xp0sH@zJNx~{$t z!6`B@AOr(KN!__r$K;l%y|_wS#}KaydBaqgPf7O&Q0Y)CI;Jhnn?ox$u~-71d;$+_&)t z(%Ji+lL+9BKty^$p$X_rLSGVg+991tN)#T8stWqfbfbRi-Q2wNqOR@hu2qhmDx*Qmpr4DM$DMCxI=QTjX?%LojkKHn`pf-sbjWT+nE zDw}!ERKYZBtr8gO-mi2n@PZMR%exart#0K&nJ}KUK?BjBcXX@aNXU`BM=`?Bzo=G6 zW^ElVwKv0qr ziVMA#Xu2r7Ja60ht{wONwgenkOHLaGTb$CC&ZGwJuqwD^z;b*r2pRPYk`4KMYbl8`%5NUFU;aYj?&18~!eJoJ0r4?H*spJPYPyC{ z7YfszIMg(tQiK$cAiwK*r30h{r9gOa4GzLv4=fh_UP8JSof;pcS6+ubbXG+o zRJDF_GKq%`G=8+=-bZUcLl&dg8A_&V9IzvZ_>|>wL>!OlxuW85o<8ANkCSU+6}MaP z-Sc*ywQD>{{F2kVFo@}UefvnPU9426-i^mwKNqMu(UTc%0g%f>wgSYRFRQ@ZpsIzC zu$FGbG&J!zhP=D0g}Z&1l`ioV*U{L+1L$Y{%0i~7`69AYO!fk)U$cjS^L z3{KdI_$->44|1I5Jr&GkWJ>nKS%sydc7=wu>dEn}ngyy&MnA?Br*?4^N^g1|G)wyU za8w8}QR?CfeZ0AqMdM1!Jguis{u=s|G_haEUDt4kGcdRlt)|AR*_<(v^<`3#P&A(k zt){l-kK}xd!nAS@U`RwCIH$R4|ZX_>@rVuY_U!aCJ`-~T?VyaBnvS4|x)sFRZMYrmJd z2GO--CY-mrrUNZN>jz~up#EqfqaWU%T(Pjudzv&kp|7t)haohj<6RgrdQ>$Gp3Rx_k`&oeN^x@}1Gfpz#Gq*RUXv+p?_S3B+>6-tW)~Pgz!iwy z6y{b8{-knkSWo^D-Sr5u^I9*_Nn@w#hvQ(ntbxpz4?qz%*NbT7zW-5r|1eabyizmf z!)H$KhD#mCgTp;~ow`8n`I{b%$qLw-5*iSIjpaNV?)Vu?Nwe+-(qw>Yak{nbYcwij zejxFLy*@=NV{LS(b(?q0C~B>5oUTn@h#aBv&A(|fgCby$kjG54xf!wGJH(<}e1XLi znbw!FCTsjWrep!9St>p}Vr$}d5?$IT>_g62w4|CcU1Ng^+8|xc-l{c99+xMtStQrQ z)bxud@z2nSU#N<|rj!2yW;w2=6^2IIrf7KJKtrX~+UF}P*4lRWVbb8sS68?b`|x&8K-&w5PZ}b)!H^76tXBS=(L*1q=eIlH=vZk$A3x?0aX84l4CMx1i)=` zl=~rwBv?*D$u!Tv3rM)8#14R-3n6SoJ5w>ZQCjk=X^EikEeVi{{D`AdSO$u(+6+si zD8cnn3>=iHA`+<`zYNnKrJqVY6|dDbDpC^y0t)-jn{l_OmLYpZAuj7 zVA4q`$h4{T4X{jpApkZGp^TvI<$Ivir|1ArS8H2&C~TuyRFbQK$M<<;8gEWh=Fasy z;sK-A631WI;m&sVxa6SCn0|9!`+f32K}@B%p%8=+!>N&PR`X>MuY+5p$zRYorO+gr zn)lvDXtCdM^)q=LQRk?IF$;MgO4v!K6cpGl?Oov*896*U|Ha=!lvuDGk70hx8bhg9 z#a(5^A70bsCM~;)O3LN??t#H?rOTw){x&8prp3==h^>Pr7(I8zrg*12B3SpQFQg?) z)wvW=GH1W zV7lgeCsZ5$Fa&dD51V}*K>-auo(^hl8>PGdh)|!I(9_|k-YXn@BrtdfZodOcN)jKR zc@I=z8kCVP8S3rCOH{7{-X7K+Ukc67(f;kUi=97bfDvzaJ7#Jc&1EdrF#V^PU8v4m zQ?Wu_H6;NJ+)5H1YW>Bb6O;3ISD246!#qiMYU~L(o(@}6id&cwu!Dl~m_N`Ge-Uq- z+ur`YBwjfaFewhF&=3Y;(O0!0sb$`9?;g!)KAmCobDsy&I2ZR zTBEJ_gMIrm(G|&)v3)Cc$8*Eg!3+&%I`wqH+Qr|A@&;7dqi+pP#=xiS-&QxxTm;w- zrF>ac%_ZEuOM+a*7Ulc+fY2?*k_+X%-8UU2hBl^(Q;vNGk4ZGP8xWOgMb5~0}^zv>WwdzWb1g+6aG%nAy6 zl^V3y8_yE-g$qhqA0nV$a6GC%_}wGwWp016k)`ERbHBmqY3*lrx*glbdkX0p2sBAd zdWZ%C88U<)#lYpZ>vk5@$}9>s}BsJ@rl5TL3`ztlhzo}6#oMBv4krM++-_Tc?~-gX{96dhx&MXvw{q3b3DQ(x5L%vNa!2kct2AXS9>^t&5Jx;1hdnC z=T`lGZ44C>Rd{l8Z>$Xz)-^Rn$OX|DP7g;1x|@UfRLgVAyO)a{sH25Cs7b}wMbocf z+FV1$c9^3@dJln~v*l+>wOSTG72%=RMXwqHA|i#S%UK?I4gxahafS~(TX^}&N^Q7C z+FP*Z?y*9KN0$0eE31?-sM^c&x1VCP1ggLo8q(EK*$f(pmV-u*1%aA1ON?*ySLDHU zknH2c{@2;>2dYv77-BCN?AD4Op_Xf4s&SW9mUV)0%-950;;f$md_oUAz2BTb2u5H^ zN`2GYXaU!EIE?CY2C@Pm0#d2e_?0uGQZ@nz3I{gCuVH52dJ~2!Hg)opa*#-jy$vf% zD?_K>fLIWHk6R%Bl!#}Seo|~P+-9W}e2~bXgtla4G`+r(3Mu?J?s_zC%eC%2xSQs1 zwyP(X0@L@;01u#Ho*2>e@}b8{Z^DAZc{e+* z;ngA3?qK6Ur9V}OY~ij1lfzi43~vSH70$SDJCB7TV0dWzCzt}47;X%oN9Bd5 zaV%Z)x}H9Glg=N|o4^QJr$WK)Ezd*sVwRC>9>Cw*t??5X9N~{q{II5Dm7^x*w@N0XOK!j~qoJMK_@jE{e>1|KOzX@%W|^QUGWP?GsAbJH++{Z ztRegROH|%JMRSW_GxZ_8{o&TUj>f7G)%z0_CJ8n&0JII%viMVg0lr~)9@}7*5LV;3 zh6x%JB(8h60hyWW^h#~fXp3vGhXMjRqfi|wGae)aBuZ*()t^Qe$D1k*C+gxgH8tc+ zOgOig4y>}Q?Ij7P zuoHdYfuVEa=S%1f_If6pIk^}{&tB%(P>rObZ8+UasM&1$%w=~v_(W&I=&ZiLZHRTP)3E*EJi|RU>jtQUPF-4Q@Rah3j`}yTXXU2pb5Wj@;BO&AA zG3u+LnL9}mMmyCfZ13eutmT9nf?ig$X6yyO*|M!+2+xSd5q_@6?SH8 zQ2~W$$kWfLJ!GY@`is$k=bq733W_%rptXco=nG0L~s{R3ZCJ{&L15{vM`sd`(Yi#Z>2n(Z>YWFHRD0%fZyhg-W zI-!BT7Lp(4&hfM=t=45hF4@T3D4e5K#!z{IeS@=5go%XLoE97_C<)&Oq2K{geTT!+ zGRD0~HS*`itP z^c%S>udlBiHha|ywCjaCVVf#;I1IX?=mz>F_W2C_l+|j`i7d@wN~L7Kv)hYD#N4*+ zZBdC-zD-^hqa{sNXA{D*zPBBBY}NyUf}VJRMSAmUFk~2OBoVy?p;-4%^GoHn$a#?* z2noOv>?O5_oK#kX1OEV8OYbxh2sCLc&5+xL&>(WhGwN`6GW*_odF(aI>SyT6qqcA-HO^x-Yu7@u zi(KL|jdZ^@DuwcIaF;#e1ZX7vxP@Xpsh~r%r%ws9IMB^L03qnk4*c6#~}x=lO@jtN5c(ncq+T=u7xuDzFo zn-X2%itqas))N?sczEf|LOFnj}_Z*Yi|%NQ%rROCGP7)wDvHm$3NPNux9 zT&5~rpj!!&Xy3IQCv%o~O>xZD80GEI2FaTaB-Fg_%t8z2E@P{10c$>+!w5B3m(go` zwx;W4)W)QZ1&Y=x;E>FAEl#Ibf#84y;^11R1zFW6^iFR%O@GZZ<`c3o=oF!q@e_Xt zihZWk=)qNKqgr7RLvWu*huB&*PmxqhE|>VV{Le3q-&kqVNUnyA0){VPiAvR5W651zj2@7f|{Tdb( zW-}}}&vl!ZZwpmoz9N+;hm(I!L`6=Bh(P9hhBlb)KJGtZ+v~}X}^SA@%=#y>t9fz&0ENRkVAyU-bEm7T5a zH(F&L;dL-w=?g?&VY3<~Xr%cfIKiaV|CGqeP1wQm#)T1wQ5gpn&*y7(L;q}>n812q zmnR8Ue@FQ1z~G=w<{nRGvq&*Qk81`P3Xh6-6!vyoBlO(xx^G&dM*CDbe1lW zDf8mfrSe2vb?*NLA(dsvhGS<)>F9FyhXXt>l}Em=lCXXT8HV@IFV7ciPo?xw{3~pK z;eC@$QimyP)c{Ew0m#;PJDC0n2>kLfk%|u~V~*G#_gH~?wgt#v*x-|=3%96w?CSNw z0EhKcTinrx_Xk)&CRcdYea)JrnEPG}_T%jEl{*;x!#kUwXav{}8{>_{9>*KO092N#QA`Rcw5o(yk%>`D0X003|^;%Qu+W0Pyz)W%{xz>I1`CKaqR3 zqg4PRUfziX;%r>7ZxeNkx%% zalz;6lNFT=^BX>18_(-T8Pq z?C1_AkM3eQ{dbm@L9GG7MlCI^kU|x6@x|?+DrQVq*AvpX+bnUZY(xJC8*{S1vC{ls z1_Unym#35z&q=T&Y~(Yxx)A#@E+9}KrI^;rv5eHHhSom zzt8J%O&Mqmgty9qQjE)jKWY7WJ*NhGD2GGIW zGjfSaY7wB<-0bh}1F`E@2I3@Uoi~U)%D>JQyuX(@jJtb4W(sF`p5hREJ)_+@Sm`=n z4rgUE?T*gNO8NC&L1iJh2-IW(F1tU{qeLXrXKUHrN2t#)9KRsQ=&a71fl%+IGibBP zc?pGP&Hh8R9qQ*k=dEnTq1U&*XYH7IsZI?rVo? zHMOr4RMUx0_e7o~sF6D*dAPadt=UYKfyj2j^DSU+d=uLPk4>yh)dYv>7S{!@kmx7k zcs~Y3_M~$A-_ol$eAbSV|7{rGFSSx=CflC$6fk$KE@N4N(p3ZsEAY-A56O< z^|rXU209pGue2qDJ4I%v;PIdVvzu&u77JkOiOhaIt3QyUw_N{8FsgZL(UroZ)-> zvbo8d9oX8@2n7j5yzUjrr?we2BMhRZ)37@#~Xw4otN*AE^X)7VZ; zXsD?q7VuK}HgAx-o4Qedc#s`%uoUx@6_-l}o^|St{P@>=Qm2W-z}D9X7rE_eUJZ>W z-+nmse8HWkoxv9tp0{_2tiN3T)|bR?u^*i`0|1|DSM|^4mBh0ip>0GZGJk39)c*Z8 z%*P=^&Fu2E?IiL|6f3zM(=AAARK^&#kn)Y#owMVzQgiYZPoM!YzfRN!a&`nsq zX`gt-;Y!^S{l%%VyQ-m~F`EMrd-;*w?)H2afY<3k?a`|G9_os&pQLgYFSpdy21ETD zZC_DC(FoBb%eT79KTc|X*f2JEo?l);KS<_^2iw{#vtAxh{_4{?JU9x6jN1Tp6sK2% z3DT2K!1mgzxx&oEL;^yXM1d-v-R$s%SB)XXc)^K;rrYvwcbQ%5P0zK^rudSav%w!a z6s+8}3$C8NLlYlAj)eHNWac@+F2~50`hr&N=$I z6YceF7guFE0kEGz>KyFtOSFr%ay(R(K2LP)^{ODug%8~OG}xZm#p|{AwJEXWMgw!@ z@OJb<3;XFeTn-Hw&%)i;58jE>weuq*9F{yV_QOfLO55%#Ksh`46UMv2^<1ee5K>fc z)Xoo5N%4d{ox94+7)R|rdY)Wgm$R}tPvfYWx&4l?fg~fW5zuB`SJSaGbL7nhDy-myWTZ-|dv)`CMaN|V#cJm$DYz&|tN>e;w z_SADYM$B%yYATmePOin{zQbWl3?{jkrhg{6!l6irWq$XDw@t0j4%vQuUHsQrwV)>S zKRrEVn1NlCn|oNl$ak|bw|*qz+w2w~9{bJQ$dYheJpIT8u6sxc{<;lpXZ+g7N5*OtFKdEQ&9j#DkZb zUowaZB)$()>9Hwsta<}ZP9cTFR;I;%4Ux5~38v5AQaa)E4w>}On*t-5J`aFQ!<=%O z0Ri^Ui4MFJejyKg1u&J1PhCL-i>;GS$vv|gfbm%QIJF3_t-QRn79cRTCyVEyCryTNCJ zPjCQmQfa!Szil;!;ui4%6B9*up*Wk^{&Y%qUW!u)5~{lauQUr`bkN z4K-I984UQdq!*Iy2{PqA+v!f7+9!msEm0r4UkiG_H!zU>5|4!XbGB?f{TnNtj3yQX z7M9Y;UpNu=l@PzaqQIBtdH#8G)Q zcpoRm=W8?z1BfA}l$4Ziqa&`5kI&Y{=*0BVT3-m9VVVL21nfOJkT0-u7G9RiE(1WJM38c1gza#@Xn9$Dq~I6Q}Kvc!Q3_#wk> zXR31mGzn5{aCe)hRZfe9M(`0}n={`QYTE?W5=Uc6t#oHFSFx6evI&&h?9DZ;Zqdc^Aya|qMabAI%_11 z>(e|z@OC|WzL5D6@OB&k0dOW5=kogQ3O3!G5|0TxuD*uLE4mFb{TL)lELUj9(&-dP z;WmE2>|yAhDA!;;hW(N{k@g#$=_3XtfKk#TG`r>-?k`OzcyPANCD8YW-Fe^O!aGZa z^|iIodn;p%8vCt}$lyjz5b^%yC+?EblGV9=mCY2~#1ilbUAeV!676M2B`*j;)lOP> zTDo@kx>!rHni~apL;p~0+Prl$t6#Z40Z>F?ALzO_m?erk`Tkz)n_lW*Y~b?km}C%y zcB7H2&>G$iisTsO1Y6DZ_M!kmbZeG)NVN2oxlHgCT=FZLfpDg)r*}g75V=Mgu-5Of z0;r=EpRiV$X)7>4sb)Pz#t8%NEy%^Av~qrER*%oE58NkvLKBR@M#E;&04$CYd!Z>d z3nRT^20)Jue`KG-Wza8E1>Kd+4y28uwJWVbX@knDhEi2k)d}R*V^3Cr*8}2?Vdf*I zGCx0miS>k(6cFtLBr%QAop+}z8OQA=GsB=2mNQ6TI@?WRErL7+3~ZnPB6Ysnmh6{n zn=JD*4FRcraHr&-#{oPe9xP8E6_W$eiMhmbBtNO;8rc9&13>2n5Mc@aSJ}s$<&LUP zJ!!271_v+xH<|*}#aoS`*hEBOnP5BDF_pM>T7!Fd&3N=1p14xr|9yRwVVyvVfaGX+ z=L38c2=At5?QgH$8@axGDlF{fj);g*B@AHxQa$8}MMqA4kCb%kq_Sqp>MGiB!Ko3U zp+DS0+l__`3`z2rhW8qdFoMwtKY%QV)`bkG!++N3Mz{fxup6L)z=%PdeH= zF91#-OAQqdMtcPa@5keT%=%?;tO5-lvvx!oXk)a7kSz?3=Mn*;+*2_K72A-atfI7Xw>=0+uRd5>kcuS0V;ry;ZHRz`wAF|NoN?lS)xPw57ef{m z6#)UM;n?FzvdpF@m?+URmzR{RRz7>G#1H4e!Z%Ynu%&@JDhRB6%OD!-2Pn8t)B^yF zr4_Jcz{`zvS|8Xf?9l)jAiz}C22iMW(#a1HQI;Fe=0n26IeQbC!(Hbsqoymj7bg#L z9wW00Dgbo7oA0jf3!Bj=iHYi9h{{&yx`$!DUa-YS`0xX;lSxLV@+lQ)vIC495txg% zj6s{*ySj8vusgtY3!ok5@VEL8zu5^G?~&l-HxP;t(}Y*3v=UtN{vf*vFD}9C+CNq< znIpfTc~ZN|DhQkhwyoKvPcuHr_jhS(YUiB>1_p;1Pghueeqo6PXx^buV1W}^-j5pb zfjO13p|O$OqD8bng9j77f8@nbDK%=NSJ+G`Hz)b8*Ez!z=I{fm|19HgVW8e0((wcPC!oBp*W4}o&l?XFdb;ia_$qXwp!F0cpqL|x-uMX2 zQh+VnA{0J?0nNb4DgYK!FUY%u6Gg!y2FR5DlMQC5AUqY3hldB=r_Dp zF?72>ta`BKS^U}kd~R=R+?FV}Mhl_U_u!;2VEsnpu`#($v^helVVlVE0YHO#`=UdN=HfqiN7Z;GX%m92w z{)L~IIl88!l-x9V@t8FIUXd{?w@0(;H5CLuMMdVQ)5hDry3&w3F8MiY@U$*+XS%SA2|H^F~pS^) zX&GLSbwaz)zdS}c{*?p{%O#agqe-#gG_91JGt|=~ZAK6r6tr0wN+%u&gTa~}pZEJB!>kW^nx&L*AT}T}we^n3?73I!);ear z3_!Zn?p+;7BL)lIOEAtFS{!TZn44}xFrh70qJ zpV__ngVCRbaapUIOztl+SeVIhV3?RUr13cs^(C_Wn#k_Cp{8Q3N-WgQM-x+_l8gv! zY!tdY+YSM?+$UuH8BriZcri)5dE<=jHT{YbZ1DHm4KiTK;7*1QLOGs8)u3T3U6GsC z!w~gC$cK-7j=x30n{qsE!AeOr=AcFUrw9uMBTZY2@% z(M2o;2K7-7kQ7t$7N%XWPy*VZFp28jxttGnC&Z&&z!(iND|MLzJJ>K5oRSPap;Fr` z7p@;#uTQhlFiF`AGkf(5w4iu`WuAF0lV8T52cXV%7I{iY7z(_bl}uMN086D_tQ7#8 zam@sHb%@^UOk02^I6se580B8${nUj=_|zZ7ibGcB8_h9k<|H3>Y3ryD`cK_dP5k7Uh_PT+) z#ho*1u01xMTpq(d!QI)wq?-o|ee+BJa7P3guuE6!_kjNFIl~aWOd>-NXh=g>qPy^( zYilAh1|f*fR9S})Q%PmH4(jvTFW!TLg)il#helh6?*lLUhz^?RXNUZeZw??Vg16-E z9?=V%bLCUx1LF=h(|yDD+tavk7kQOk!yvG(k2>uhzmsaXdiMxax$>3aT~VYIU~2!L z?UvpaUZy;ZW@-jGL-{s57rvV#-yWl*zXD_)xRMK4khJFpK}im*OuiOFY_}H(zSjqj z1VK6kGni5^3{ccbx>Qx|AD#W0Satzc6bVqo*)(KklGoJLZ3&6O>YVqd%rhv-*+dIH zuiT(_9RLcr5Bw?lYnd(N*9TW*N?pp$KHjM!uB0v)r`>TpthWt5&j8ZiKfFAAi-!Zl z*5{Uqe_IJoS^oYeUChvs{PuF26#)Su(=h?=ga&gkxJ5Ye?yR{udXB1|;W5>qI7-dV zWRnm>fbxS}$wQJAVrBWix507lpXt4z`!zultG;@&ABStE=?$_5KE9i$)T70~2^H2t zA$~*rl+BxVuwnPsa00UCuSEl!@$qp$CeJ4NFk5Y40Gb^1oeiO}OLSe>`0$=|4>=R1 z;FhLo290b27?9kI6#|SOICnkhXU-|74l>n5kqzH)M>M;G7lNP}F~(+H>kh^OMm@RU zY;rEL`+N*;31)xYoiBK(2m9`57R3i8RK(RezeJ~~D&jA3<%iC+E z(1WQ%YQl|#y-!!HRHwlBmL}c}TXig5*^-a#hPydbqmdNn4++$aRF+M*m)3B-AH1XqKniY|u5-3l+qXUZ+H=0T zqEPC2Mp~fThyp6M_Oodp6nvI~Q$|o(*@0fgrz*RJ$a}uNHVdU7Tf?@a0rU5^5VC;h zDbwlBbif6fPbLI+Z?+ND`|@7ct!)#dY3)*E4V?b30EA~j1cSJwI7f7$MPeV zgJOV%^Vz=i0i_I~j)0W+bNARI(u8}TRS^^WMW1)aU~y1S%+KF`<3przQVd>{Z2BnS z*IY%`2r1o}`Ez#Zp5WsnKj_5$46++Mb+2MvO!nDW4s1EuR~_Ij%jrJq z==GSnXYNO9Dq0EBxUntU%&iLf{I;Xjb|N5}M9hp+c{*w245U6NX}>ix>ahmOg7*yI z&w{4!H+DP9a<-iEk_yGC=mRzVH_WkG=f|6I2oH1OT|mni-bwhcpdqILG0NV3F>x^i zPjJ{O#euu+;SYLdlRqr$5gXboEHzO0%Y^zt=>UTsyu1*A%aYi{{}apd@2*w@jj4Z^ zpM(Uw#}dC(nWutyGsE+1V!oi~hCf@mhw^W<4LsZiE+up#pU?Z)UT@HG8R?t+nBbw;;mlGd|eoM?7PTd=kn&3uGrXC33iMs}oGu za-H6~NZQ?p#4|wj%OTT+V8CHtRz2pa-+V(iVDkQ~>bZya-zWJowy&Srb64xqF*`6Q zctaMn={5f{^OKpsH<_|Ii<-YE(!YP<QC)t&HIb0U~O)Z@wTVb}I7* zoY8DM)aA6Q5N##4^afaWioyfyh1_n>iZaid&hpKiT^m+>9i4^UFrpZC*@LUkqW zDE=NaFVEn8%vKV9ZzaSULEUo52?`C0(O_H8xlPae>yqEzXd6Q$$tRghI+YAY9q|-a zT00jODAPPepI8&$bnOPI?86{@Ry9QgV~xyP8Z6=LLetB>jBm4EjyZO3?m|B3li6ON z4SvXJ9{cEZ@E|5824E=8Znn-!tXoaJzTX%q%ml|I1R>I%y5ox!XB1aG@3Iu+c5g71 z_i1{MbXt@Wau$3R@$|*c&{yuJ;mAD`*yrk>#8Dq=%eRHtaap5@HE!#^6?eeKZ=|+- zO$-$ud?Sn8g+`u1*UH&8=I|!@T}ckzOjT~MB;x&c?kbv!6Mw3nC7{XXV90Ohb@|j; zKu*t-tu!n18F}78SN^^wl$X_i4z}I1L9JO?=j+2Nrp_fQ{sm?N&0^sczlJT_yhigi zQ3|&;G|ssCQVD-+Wpl1DRjhS#_2$fcMYQ00k5uJbPDWKHb#;li;FdMkNDYqT+| z2F`P)1c$>%8eEpz3K&gYaI`}x%F_^Bt|I~Oh{HT-;e zEANE7a`}JzKl=g|MlL$Fn0tI<&a4)dR^yg~;?c|IKt&j~v$RzJ>cl^f8_z$41|rqx zA(jv84cFqcK67WH&Kpz{XBjQ!DHR3wei3lFb#)doxD)ldUMaTF7r4!)HGqi(XRfze zX?<|6oz)w2)TK^&Q_tQL%0C&8DUuljhap(U5PFoy!$^Cie%q-G$=& z@VUQaekAM}DeF5g#v)X%5LTAj@E$$VrHVe9QK0D7TMBVSx11m1L$xyWcIAj&t;vl7 zd`v`@jETxG$G!{u0F}>oWx}27TR#tUv&sEPyR&T}B;_A#-BXO+uyl0y^7@swN@9lB zj6@T8eq=sEp;u9jqjmlA^zD%;<9T`GK@AuO==J|vVIFCJUO%buNoVcCU0ao%kK`5m zUVec{%+D!lTmklz$r+8xuC5n`_@qx`7QOtj=-)eq`s#42C%XB%DeP#oAyd^=y$r8Y zGwDy+_%inLnSySE_rqhFht5pN=v|ceyUIFHj8)ILx5lgWR8pH9pC#h`hAhzYQp?@g z;dV6yZ;M>rFP{LGN#FMMV#zagbP!{2IYy@ypwN&L5Hjy`pJCCG`pQnJT4wZCP4Seh zUGcY#*0X~jt6VrymA1)9(#%X{d7{*RKg{9gKfM)Ej_szaldrv1s9tz!ujpQ~AnPZ` zI{#v-o7A_s!`f~;NrAMyK5^_gN&NhEt2nOl8ULr0kRDkcqobrN!1dx2M(*$E;3eT> zQSiR8U+p)G_}om}DIct#61y}x7$Cm5V^|DY5*);2f+v!@JMXOKc*qsah>4`LuzqFc zBdD&`e%|$5j{O7~oMB}fn79t@v1Y;j_D%c6|9X-;)#XWJseYXlWgIq|C;{DqA9^TW zR;IpVxg=5@I)FaxV#~LaQf1IWNIvNo!I$z(P_r<%MZC)-$32g0qk$2PsoqF7r`LXc z5T?8#@q<*7B*R;35Ype|MVpW;pCqr>e1jRb+LsTO&9DSu;!BR(zb4~Rov~Xl$`fPl zT>n*M)W4E=)7KDqTE%T81iU;9rlxzOHbsh@-xh+;PfS-`dW3p z`d1~`!9%ki9vz~#RIKT!vh9`A{Mz4K&gVVrLiaj2vbz+D{k_r{qH6S%P@U-r3FW!= z;mwlDXPz?rQ@itfSAu|#9}>5?`vo_{`s%qpIi|4I3)~C~kf1vMkn1ipX8rpD6s>+w zeXx~>(cI*u!U!m}-{@XHOlg`7=i&@r=`-WpUzx5YnCGgq4|`6Su3H;Vp=o8+zN5rj zW*g(uaHO`lcjH`df1w~gw(30XFr}%ptONTjY`~%C$?}Fh7+qFsgRAp9Qf(|cNr5ziW~NEe37<&qxt`oJI5!(ShMSq;qE{@DTt#dgg1dbQ zN*O+#`X6S49bHWlxZckXOw&c_Jax_yg~d%xk7qr5C~$+H+1RR*kHni|^bYCuP{!ZO zbRYnSM6LhB-h0P2xousexGks%7JA=`(yKJ7QQ62w1f=&aAiahTu>nd|>0Ns7y(1+^ zm);X0^w2^Jgmza@&v)*5&pr43?!ABA-v_sxFqb2XkyjX!taeNKUQSD@oMVF#UObsSTpWTeFhRM zDPYY2{3-8{Z+`xw_Vm2(9ZvDtbi9G{m$KaB=nF?LkOK^O`{tx>gXkgf|Ltv*tYr5? zXHHOy{nhT?_4C7yRn<+s@TV8I6D=kqZ+C>&c^P;hwmdqId>tf&*}mNZbEY1Z7H@0} zPP>2ijOuTmSz;poGBbL@qV}17%^HrTuaAF!dPf2xY=rGaVXE+Wmz$w>+fb>g(AfD_ux z>S|tLefMvi?iFyvojo}|r25_nY1m(NdLSZ_4Eo=|*VJ*b^X>AW`4=_x$kIqYEf~em zcoWP<{O(tS&eZv=*Wn2}x(USbTE5+6+MGGM-rop=jZKV$9vMX>1;FywRo(!1*Y2L4 z-SVi8id!Dms}20-3MS(NrZ^g4u)&SzJpks?=Jib(V3h|btM2u}w#hTe(SlCQ%<{4C zfacp*IGko}vU1cjb$o2>&r+@KV;wE6WuU=-_IMG1hXEIWz6HD6!pdyUh+b*6G2-Yz zb8-2DjaWshOdmsz-q`rK5j-e{oz~$PPP{apO9_+-cJ;rw2QG}2*yMJuP69wo)MX_N zOYrIDO)3$`yZFvC4iAx$3*-SeRYa1QMt;{G5t!_FBz4IIc#^#wD9fHu1O%%g4`jpf zkjbrE|7+yNQtv)1(j%98p$^MXu`%`1z|yPfrM zBB@Knz$6ix^5!KV=Yq;j1m8r`m+)xr=5$h3*N^1X;x%k~_>2y6?ijZx6}mo+8AN#J zC<065tC@;P>Y;0cnZf~NTy*8r-7(#&8OASO+y!E@!IPDCCcB+#>{9jsVjZirxiRN&?=n;V%Dr02;K`- zzzt@9E4T0l1x3clDR%yJ=oGY`b$j^7vgOR(+}!@Wd-nq9ih1yCF7-1V0f%%;5mw&I zc*zP!z-Bs0zXUo}*BB)qlPlvTFCM>niNKm4x zf9<1m%Cmh0T5YWWzqeod@#q2s76K+wvs^SZTAt4Vt)J}KJC06{&hJ7iZ{LU<7U95~ z7H3_>`vX`D{}GN~o28kbxP|!-S-jf~p0$WAzza30xt5sc0Js)$@%i38(L+Ax&&;eW@pZ~Z0c`wE<|r5 z=&Yy8WPAKs)xw2s-JpD*TR$2N0zF5Kh;Tv@nzYJ{pbL_X2RK?ztqQWlM6}v#(mrUS z<$80S`*w;`CbwqY%TF|jqZTZrsjp9wT$XR57Aq6ZrbBTELljp2>Z7GYVxC46eo8BT z^XRqDnYes~O9&8`zl)WyG_p7cbTUA%i|PIyWjn#5;&LS$5W9ijqorSkVyQv?LVaClv|(Kj+-AX%?= zbdQzyZ@c86H!UCWG(i+#G6F9H{1^JEi+xFW(}J@H^(BB3{eGnVD}3~iZ$$sY2>bgD zkSpl}A|nSt`ivW80HN{y9|uH#0;h(e(o!k=zj^WQn25A(;N_u`sd$^0_qm?FJ99{3! zU{}vE@@!=NneHGKq7VY`@IOSMD?wMuIezS8OxUhwDdU>{9jT=V+s?>mGa(Q}$M*+g z@97Odyz$RZ$VXU#w&LsV7?k8a0ID@jP3anV##nPCx5L-PoE8Kkx%4s$&s94vHn$N` z-k_tqc1i#P0`2sG^)u>;qM%{ONgTuvJ?CmDM(~XRw#zWf*T(<5Y3aum{8kIk$`k%M zq4^aa?o*tF2hhE^Q>tZFRa2D%1?-+^VW9Nw?qNI`vbNM;>TgMT-`XAIFk>Z9z>AxD zm00g_MjN;Y#tcKaCM%4=aX9w>B8{nealhfIo7)4QMn`hn^>xd7{b>(N%`G6Jn=%r7 z_ixY}lEoXA0daW0mr0;+1hgT(>--G5?|=OPmxyU{st@r3HY4pJ)PZ0C8Q)B!y8p6> zrkBHsBme~WmgZ(U3D2E}g$xo#W|3ON%jAG#sMO*Q&dygp-iOID*!6vj4as4a-yOR4 zJuq7<9Y7H>VGDYL>?(Zs@85D-=!S!Yu-IdsJz~7lZbN@{vXZiq0jHL4H&yjWg84Sd z9tl{6_rV-_%E&e)>nrqV;cXDI^5byZI4Bvb=0k%(**8(ZlR>>*>gVPSzj@Z80#ri^ z4vqZginYo9o&1bwK`Z9RQc`LTA7=smXmz|4-zkePF>5Qg8hu_+XLQ+qI7baLuVEnF zgTyxSoMxU-e(rGvE|ktI1Jn!*43-5C?HKLqDcyFKZsKhN%jw9d-a&j$rO0o|a#1W$ ztcc|6RZ;*pOe;7>H+x2rS(0qlw{9q&qv&{0R#5>I&L^oQy)G~-#J&S8qT6>LP~okK z@b**qM|d$Fm^?=(>U;wvrcKFj32z<&aha*Ao8z8gfik@o6mP+%=y~mu?vQ%40+4-huJB0 z4lTKXpYHlyi|b94sLe?exBvcUy`Ycsw^pZVQRk)2f>?->ABAF)r2Iv`^nh(V?`;tk z`iaHW_F1BUEVZOZYcjA_yB|lj-)f-BHEIy_=*#v1iO=oWwsu0TI zdhh^f*qGUwhdztp|B6H!*vZm=?paz|x(tCdW>sGflgiZ`sekJ3v22 zN~inmz!DZpJ0mIIQ3g#NwV6;bCWzr&(u#lm_~s3&k1ODyKleC>ln=CnZ%4$}*VR+8 z{;H{;Uig2+r}?=A-oO7Z44i@LH@;U>0P7row16h!HE_1pvm9`8arGxl5#mWRzblfI zomXeYJm@|7CwlIwDRSP?bUOx!TRu{hO+C=jrvcDrWCwpD9B= z&hHb`lOHVyj480wlcMs{VNbkH$43#TS%oX0Ci!XV@pzZwZJITn$c%psFz64Hn({_* z>-}X`+HDVYP(v4t>+;#1c;x|K(+c{@emwCV0hs7P)copPw9w^&i=e*@jm_I_o)9BlmuUUGvB$gw2AEXGcrvQoxDCE?!VFy=e1*~65`TIl&M#sgS=TX(D zo2>F$z6DsTv=KI%sb<9DRWL@i991Q>FA@18=F(?)Vq>|&uUEDEDKsAk_{)yN?ADVF z^#6?-uR$CfCcd@Ub-;9qwWI~M8}HJ6$-yjW_DJxp2{>4q}M^xb>>p2$a3yA@pJZ?0M}6j!+jnJ3GNTH!zBk zLlA(euo~qaK1C->0XwIvPKNN?9gyRlFiYXx92+nmLjex4C*T-#SY6^%Bby&cm&?+w zX9spiIet99*jixT2G7yEb?erfUo{cPKwI!d(Z?BlQAE!1iM<0~6gfULd`06j1$zhs zCZdeMsz+%pL%*E3gN{?V>hjYl?qKjc*%c~4I&N*mF4vtD>_=6QwVyre-SKyjSB8Y~ zYyM>A_fic-!DsuazKlTlscx*=gi^?Q>OHWr`y_0je~f6t4_yL@b&M08mhY_Ef zTZ_zfz{n%w;gK@j1TpYi0!wJ@;%}DRK*+Q7&e8Aa7-og%Uy>StJ;>Yqgw&r6YHdyJ zCiui9Fevyw=x_b6pN7URDQW+P>CDbz%?^HehE+m#StJoOgVVn0zWIDH`ZL}D_4fut z(EWcLPKxiFb~S(f`p6(~VP@v_9qE9qc_0#y{42l!&khnXF|lFW&gp5rX&GD7^25cR zILa9QM%>QK+}t~$_K*rHYD)+O1%-#=Ky_7(VGBI?n>Bop;^*z|NB>_i-TAyjNoWKv z#OUv*v(h_m4-gEbn zP^C4n3>VM_^(~i9MJvz`#`}P+o3H~PDv*Z~!b4~?_fVgE$cbr${sNku_Ots}j`d|t z>=s}3>WP{Fp+jJGiNn6 zQBIylxNQmeDtqo(dzIPW#*+ZmI5d{AyuAGUQUG@WwnYFl%xW^R{;c=Xh#D4|5^?XX z_^-gzftQjLb(w9w0=O@NmfwoOust*0K1{6A4$Z|U0X&Z6D0tMRLWsy%0bi925baQ3W7o%wpsKd9rxxC!B=C=2?o&XQzU5czPsTYu-dI}Cn5KRWw)B9UpJiCaO zy}iAbOWVFgi@T9g5>BTdJ*!$ER>Uj@05cRwuf(FqEUIoXH8sv8j zpaCh(@)QV&Sp-QJCP2pXJjLw_op`u?(ewR-!><3FAvej9BLO}np(Q0H`4(7@fJcRz zpV5oI?)D~%yp0!e%-RD`WiCNxcXBj~o&E*?cUkR`tV+tezk*^hg!&HPc%J0-p^{&G`AtS65c9F6Q+DL>)G? z2H>a5M8s;xzt31p5Gd1~KEHMa4g;zMy3}9<&Tan23#CeZ2{pGl;8yL?)rIHU&3&`| zw!%NZi1zoyt|zXP{_0zTLPHM>>&V6Dz<*yu7_xJ%z1XoO0@wsK$Qua*vvtcUMu)Z4TCIFqU3N zcy-*1%ERuj*}-(I&k+z1Sceqds5mc-u$Lma>N;&RT6J)0%WXLtWyXv!@~59wEA&vS zJ6^L3wev#dytKCF94UyWT`X z%A?$ZoZd$-q>48L#yh~!Tzpr@h7We~0Va*IEl;a-%Q5z*J8voNDJB&5ZDyXYok3;j zuf(GlU$nx&Wxd*spy!2so%q^xDY@EW5cTm~i-rayo7E;p-dm<|XP&1Gyx%Al4PB+7 z9o)%vpBrjwYVNvCkqeP-o5_SI8OV{*lm|M$jpAmZM^7#)c(H-cqsG&Y(n7Zh@F}?( zPIe31!Gd|aqDc>Wnq@qGL#YuP(?&nvv zfjCU5PnBrcPS}_m#XmzRf1fA^K(n^clA9-38Bb3cuQjwmAb;Nj)cA-mOMeXW7 z39y3iqy4T~tFaa76g#$gJTe-Xn5gOa!Yi9kIQW8D ziuh_+Mh5ev+xUfW&Itd3;Nuk{{JMDcO!G4S%NNa$@gM#K&xZdPQ*;CW;TyYI?>MF; zMVys>%|`ml7)?&DRp3CR7FNvT$0Ncvz=BT6w>jQe1GS}zKfTL);5ge3)&L!tmr*!f=P4c=eL zMrgXzlzEc(mJb*jA)`-NqpcIBV*7Rf^z=}6Vz2lttFUfHw}b98QByRm+1LeA^P9CM zxFWQ`GC^bOSeO-}Z@^BrlTh0R`;WUP9ook{le}k#R1|~x4cyhmfe2!;^YVZ(dd73K z7D3<62=iL4+wOH(b!cy^wi6fDjvWmT*?y>Rpru8bcKuqhd&E4WE_H0Y2`a8eC1?`l?fJNc|o{4B>zgn;i)C=9GlC zkz*!Lpn!k>d%*)?@22y#oyjk_T;ku#+QWF7Ouf(ltM(YViU3 z-t7RcG%l>)W4F9RLFer(zBgu4?AD)>V&?6NaFFSa7tGZRaBHbvgbR*(aWrhU@xX^Q z3`haE@oiVbYemHYxI->FCNuF?Py`fb?MNy|S%T{_+<)iG>z9r>0>K2CKU%Z7 zPZ|;m6xP(L3IYNG>eeJ&ksy*Btz^W_z7bAhq|~+d@!HKb_#oVvUO7Ai=2<9td^+SD zMPh)I$7zd5vcF1ERbjUJ~PpSo$z%w8sm+f*2vT2xEKq@PKqbjYbgcM zz*a(=VR5rx{z4MClBVyMiHw43F>u=1F_X|9u6^aI3S`4!Td;6GBZbe8^Niglls@Q5 zWN%pf5gKwTSIlgTnQiMI9w!Itb^!|u3&7UXqu_M6&?kZA>3IF+tq`&G{i-#^$~Hj# z+;W7OCqmV|*0+;FVU4(z2lGQJvn5^JJ&z3K<>mF0nAOLlRg-$N`?zaYZ*6!j*3Nzo z^%0Q7R`<1+kLuI9BB2_^6yPGMNWgY*s60K`W98roJg>j`=E1_432NPQJ!HOXB*}Dt zCHGXKpl++vZReBBY;jv)ssq%dfKk|1*K6}FW4GQ%yDUpWJT@XtJy3YsO(p(l zHf($A3QOXR#-n*2M|Tt3nq~O@P9`KlN3fzmixfRyLRpllaz!~`1?;}@GQSZt`8+8Weo;|Y|`Wz5LE?rSjgk;3$A zl_(?+?4Yaq;tvwB?w;L3uXzrW=GG6GC_7J`MCUn3!y#jJLtpvmi9m?NLe-c}<@8ow zYvq)qT(OxHhelp^d(}d$mGMa4xWho8Lg#L_*cKRpV~}BRf1$5G3XV7idU|@ykMs4V z%q(E)B+%gKh|*mgu6hYv)sugFHVW^2V+w8E9o1vBgB=%fQGXx2-&IqwyyNCb z@7a_0$rCz-Kgg?N|A7nwKYEgMletQPGL*=r=v`J<@A%J*VB_;x60Tg&*rM z*jCsWIJ;0?1_w`l#A}v55$m6pCh5@pRf{~-_nK>oYE?X0gl%zlfY)qtN?m<*K;#jy^+iB9Yw@RN$^Qzbs1ZFOx*XPokYyluoOMG?k8Jvyv2%P?^L)Q#qb??c)yu*3{ z{N0O*MK{LoMCrR|c^%Vgv^D!vE;x|jQ-Vdv^hcv`YMbeg!lqMI4%M}r3-E{U7CELr zf@tb2b&0AL{8RBk&}!uJRq*qMK{x-;M-~XU;PH@|6TAum!TtZ8)9!zM^}m%A{JKr* zE=@-lC8XV-gZH1}o}txCTT9NTIvLg3$*kvEl<;ZXxu&tOlLFfHjRb-q1(2dxY5k{M z{xoCEb3+1u+&gmW zA89HPnHV^!m?%x$?~x~#zCsmJgOioMLI6+Pahvhn<7NNiS(@YYX@$yI35D@PQ6Mii?z~ zsVR_-YCU4*;pu|zlCGFU>A3_@^JgyZ;umdGB&|zVvm{$t?Hm(~^t!~3{n2#9>GPo{ zb+hD|?OloxuFg(Rrw%_je7||I@fH!I&x3{aBB?sG9*i7#z;&Q&YHLxD6BGo6qqUv* zVmdQ!-YBH+JbML$Mjja)O*ZP07Q&{jW?Kgw4eYCc4%hxtyCgLTknYrpg=L)Y`X6;Y zCV=1PB02{zutAjAJ1Gm#onl)~LpUJEXWwv!1AsVY1)_rEa(&L(fA2kZ*0-M z3QNss_@8xiOpM*rVSS=#a*zmpt!}8JGT5B3f-yAWhV6{E(qyaSFi1Vj2l0ZL8hzZr z;Fgo8r;f9W%Yvijqy1{wv9b8^&gTcSn^M~+$W6b|dFan~Z9E5C&sM6hLpDxXE|9}vkShmOFCPN zJTBC%c2vV$#lMzeRwqex;}_SFPmi7s@?0N&hpSmyS`8W*+0`Lt26cdvh6W`U7nhVJ zb@ZDSTn#3DG2o@8KK=Bxlm{)ZC9%23cG`akHFmq>AH#;_d%y`4CZN@UM>VE=AN1tK z?@d0rhrh}<+LfpzOXp(ZeD~@0zf^Ac8}hpZn`AdKn%<|p_A+0ygoSKx?Y|-`r31i=5VzJTps@CCNl$KoLuViSg$|r396R1kmAv|e;E%O zkwfjbkc7~iDTIpfXS5abi1R8uF!mWz)o7CnOk%B|tJ|qbTu%(HP|Rb5aEpr73TX;A zR4_waqx46SRk15et1Il09>>FIgPXxUR>m_XiWGuHYh^t2IP43(vr>{OWMyR{y9_SP+JPAHQQ+b#+sk_}-3mc%-VPP9cYu`#LRiI&alK$Y0fxU_bT< z)9cjxerv0Q?&SDKlyG}g0~WaWMX)f^FD4y)ez~y}Bulz7t8?lEX8Qagp>OvPT#{Zh z=#-L_JBoRZqf()8Ps!u?qu9~9xiL2sHBU(5`nqO$jfmt)+u6mo)%Hnn=tH;o`UfUx z^4I7weWORb1*gYr2ZBXhc@2w^DP^XyaWnz9cYU` z>#qLkiDKJ8MD8rE5GHyu>#flmCKn}R1BE7fyC39$aDR`oUC{?B=Mx?k(YCs*vMuDl zS}EhTlW*Rx!+o4tH;q;lyRGkXbkm_1sT>9cDq2BJ1}wFr{&)(NRV^w7MKv?aO zH)a`Ich5{AAs&6Yyu#Qj536I6JkhWVf;G+_SL--_9$_r&TgR+B^>v2|H*SURqo%LV zVQ!qoNqz9LKG+E~D%2I3)CKS$^5A-|6Wdzs>wSb`A-D+Qx2?hMGd`XR4j1}r=NM<} zPd6i~jyRAX_Ii7bZCbbdhA}a)L!bDA-cT7S6(_G%o5eZY(8^})X|<&O?r2gFqy8xj zGk(2Jknl~Hx?roFZ9NC#Ae*NE>t@_hawLl1U54*knrj)n?z&Pc(5M%N0FLCMqlt>s zl*4JWE<@1JT2*~2QzgSQeXpV0)kF@TnR&)7_JVY6^#^Gh8@C&xH6fBbnFVbiSLmOf zrf;m-Ey&8w30ld?**`cvW>Wx)U}L8KTJE(kropqybtj1AYvRMitF^)R3ibmm^3Emu z+2$#wr5r9ui}jDpLb5rz>?lUaX2qC=e@uT#hM;Yuoe@Lr<|L&0_PV#5gBQB9@xu^P zb$U6MqP^pRMk6V#W-+;HR&TNG>*4CBcHa2=#gYXl8|c~EQA9X4#!N~9MORT#-d+_O zTje>E;6bb&+Pwb(lmQR`l{GU zwa0JDyvoQZX(+ZRwUpog&2jq3JBw>AL(}ehC`&72FL`Jl=Wr&4-?4mx!0nN;a>%4* zv7(x(^s_wLq}m596?gHOist1eO_1wT)jpZG{uY?%#hj>H!pY$2VIh9dy~TAEM1O`E z_?zatQ2KR7GG?|5&t485*2F(C3JksPC%&nTI|M;y|2T7{G?bKX z?vk>|K%vm^C=Igmmci+1a)9Uc4;8W3Je~haBft2EHRQ>5z4;kD0X8aCS-=2e+h9gt zz{G?D(;lJY=e>wM5gT}(SI}3gv!90J>d!WP)0bhzSUS^roejl~vJL0|`t5XXR~;QS zsJs%**h^7L!$ZE24T&=}{gTby+9{i-ZGY;l24y&%t6(%aMxY7^@zLS;-rx{3Jb9A{ z@b$Agb&Lj-(&Xt}C=gjp{ux`b=8W_+8;N(9w%G3M?3}INaF^kREtH>_eZ-MmV(2_|G3A9}XOE zVgZbOf~tkES_+k=V*zgpMG7Jxh=uHyR#wJ{Df%s@2r#X+T54Vgx*)xwKu>^Wq^zI= zd@^}Bjsxmvxnnej`y?^2O|!SncWuHd=!5@#zu=$Vv*BSuog+_^<98px-;$HVj+zTo z_%@F&XvDWY@E1GCZsxS;xQ8~NtprtGvGsUB)#+=0e1gHN5=w(iTA6w=T@Byy{L zmYh~Noosw+9hr&j`qjx?_1J5R3`t9V z*HVLk4xT9Zq)Bs67b?XdTp%O~b+HUfUZU21QqDZl6cfbb8xA0LT zkS0g_HOBIDTQ%rQgYtG&C(eiTpR{iX2?)VS6(ydsFBG*`lV<0CSqk`;-!^MV95YPDkhoQ(DREa zXt-#D1QLAGw5>xETHrG~msL;@vb7bE;&KRblz-ZCCl%^l%|qpG)6cn~5+_SCh@`Hh zx}%NM^mLJ^u#u69b8lv4bA7$fP%!&p{v~SQKNaLM2ZAig#;vzzi39 z^N$k$)%Dm=@BFl<^60DtcB*~{nfQ8g5$fvN{`NX|cgtPtfV1mn`jS$2Cqq7oyu2J$ z(FM--4YB%D3x2bCG)7l;#vryeZKK%sL3Ag&z_UPmN3gQ&FKJWLpxk^e%R82qmX2M` z1Jqb`f}oFNC+Q!;Y!6!U&r1#noxkWi`5E5}qf%L6kq;^9DO(!TPeXq!Xy41rt`=eE zU{7(GsxdNxIzyq-GM+L@=Xvjwc+T8*==WW0*Wsbr|5PD68S>b#k zHHtb3@RA{0C0j48tO9|K%1a&zS7+x1?lGtiR9*q zn3Af~6T*Oef+z4K+gk7EEu24B)D;E8aK5isf=HMjoO3&&=}x1Zj5~QdXP19P;mN?? z^7i*{fkZ9OstoA~KH>vb(B*#uQeUmX-{Ei%HZKx_XwtyDhp_6)%E{pz3DRdt@-psD zf;}%DLX-1AxLlClqsGhC!_(7h;T4Gm>+2~3Mg7kBaFmDi#2-^l(HiRN6g*^#4zx*}q4_p~%KG{Y zAhGot780+oCl0MfJVEJQ zk?pC06z%ocX+|FLxgQUXilk7VWEg`ZI5fx$$Er@wPitt5T4OeX6Vf4{+k*?cMlG$D z*z^|t$h!yyP zcaLxHeY#%H19a&q>}r>K9B^_5w?L=F3anAMmA<$7QJa+kH4B65v(<`8Zifx;>wlS> zoBQ<0$xSA>u8%wHD9OM(SQ2h@Y2>}XCztr4wAV|E&SyWlcv0UIbT7HztL4(nE-QOq z|OHXq0=>d@Y%?y#w_1TJEgAV zerG2~ir2dnO$EL6iAn8k#&3FWS7(J|doPap1nqhD*|#*8oL}IfQhqi*HS`>SCdmap z$^gf0Naelncc^#4a56^FTwiHL&|)BMe?OfuKvC~toNmFUyIm6Zb!mO&L!sWT9cE`x z&T(2!(c1wJ!4}{a1gq6%-0eigXlT^iv9Qh5I-lOvm}2XLPC$o!FSWo(23k)R7>weP zo<0_Vtz!b6E@4c&j$K!m=X$Ycy!W2fEaT}iQUBN&6-&ImHJ!u0k;8l@ByRLN9N+Jq ztu&xgVO9*WYB;X1TC0GxgMRqzY-1g7PEIb0!{G1#PO<1&_vHNN!I(PHAK!I&g(&=o?{!vr|ZWU#vaqOv&#h>0aH5iAs%#+&%IJlt0T3=z;Zr_UV zn2MB?$&@=*0=>o|0mfOCtcxw;u3bIdYR)RSMfL}8$9uc;Qi3yXLmeAHMa*pk5-`eTCRpafC`W?W#-ELdEPB*FtQ}MCs zij91*9TYXM6*~E}F=-lnHyt@Bii&sA4>OmRoNwa^5GggEzv1v_f?k4>o(gGW<9w(H z>of4&#?9|)s`ktq>71XB!M+^kbmr7;u$sNSWM~a~4WR)6olC1Pc`W^Ut-tYhMiWy% zAuPRpUL3ZYL|oligMlh3Y2EDU>FFOHRvWU_)>^(^cx87N^eS%wI)SuJc0*oXrB6@n z?#iL6s%m(Mpd^h(A-APR`e+$2ZI%0qa!Nn7{w7$CO38KZO-P`ZCMGa>WNHdsZgiOM zADPe|x|U%34Fr9c^y+jG8d_RHax!L#>1p@Flh=kTZ{puH1z+@)oq&PDTx)u0C;pNz z?Vz4$mEZ%?EtElh_Wdt2^(1FfycD-KUiP=lZ^+Zkgi;8*ZB&JQ+LB>7Iw%A)VUBLJXmQVWn*LY^pra=b^=pRP9R8=T zf-44v^!C2AV~qL2%Z4ukZbM(#+FG(cZ5nxg#(?@?2bGOlxW-?{X&p*xdMfO|phsiH zBVL;<<4cj%<>`_^#w7BH`E`0OP6w`rshPSQ$5_$RdH(9=sum(g%8t0WiH zG5g!dNYSFp0fkN}t(Q<2&JS82xPt#Nf=P8XHE0m62BjeQUb+QTNz7Sl#ZOmBi#maR zt^a$`v!B3xDiBli6c8#15(HE&B`viflIf4l%@eT`n)M#0KY{BYW~*2YQ18hX3ph(% zCF<(zD#y2#30^7X9e^S(-2`SZ{n_=E^vG6#SgOpp;w^ z*k`OfgU=m{gj}apiUe(^_NZ-b)=SIQaRTJ-$M0~u4Db6`z~yXu@y;$wU%h}vv-~x< zk`XO#aZI5BbQ}Y&9KVSecQTWamXI zWsa>a_*4Yv(xQsnLdoUo2lxm*>~uU7@Ha?DXZ+kY#6W9M!VLnHW4alXW5PopaH`cKRf)0PjyJF1`&9rlmx+k48j_rDkSkmaV<5uInnO zox`oKuMebhP)KaO1PrhF<1}^M?S6&w zY^Q`!jG9A%1=t>>Ce80Yh&;2@9KHeAvL;+?e907{6+LOEAgMLZwrU~JGlJVe+@ z9l=~*Q0Sw#Jh4%UY`}b%B1)>KSf0Xi9eaT;*j;cCQ`QookjAXAWwD;<4bWMyRqq?&S&Q*w?#F3+{n4Fo7~x4gVTQJI>X%S6HD!P&^I zlcaqkM8w%ePAg3>3pm2bqTUQ@$F|8 zJQ93Xd#iOlaO)MSG^Jbl}j+Vx4&JGZ^!R2SnsLK94I=nNCnB{84vwhnnB%N6luk@496=NPG@sp3<1NFo#$R4aXZxt=P>&dnDoB( zeN8P~bzi&)6O3fQ+}>h2wore1-}m{PosZ z8OpXCD7+O7TI`)U=l+NbUbAPqgnzexWF!9mI~;v@QCDpkO&IuAEUw(I#QW+Wv^46A z+|y`DT&3A$^dr3tpUE_)*6e@FE6a5a;rN2NqM2)#i8_TUlM)9YXup@sPHGhw zOD}7v7Dzm?e59?fk69I&`(m3w;AnE&@M`^G+O{m$gTZP$1sbX_v^D9N z8PNcBJ+`?RQPZ`cPK#?*0t$vJA@ON9%$@S9_X*(#Y&))X zvctgS?U$iAS(NLA>)xxEl;0cB_wc5%bNW*-9X*bXU08_xn>6*B07?M-*-bC`jg5?G z;_THLwJgE}_+<3-ycv>g=5rzr+>?&?K2$p`F6>%NHR`!e3+^Gzfk)eq&-WDDffTn1 zKq5mVuoGbJLzh+{eZ(9n3U!jg(rkMV8H-L!zs@|zuj53ImF1~BT=c;9W-oAhZ%ks8 z#~qhFMeGi?MGyaUZ?qtj;$NT5uKRGObw~~T#ag>jZN9!0Yqp=j@9J;7wy@i{ehj9K zIQ^3?ylI?xgNrMz4@$;zodNOKH3?5DL3OkzdAU59IZ+;qX6DMy&+q8-28%*K(vU`n z7iy}wk(*Orp${T{e$1v-7d;H|cBeQ333saPv0`Nx9A^T$doW`%RiH2Yx+Jd;kluErIrcQj`-X8 zsz0@)MD+SbMY3ew z0MEU2VEBD~0nf1aZJG={Ghm%k2;**&OW z{n;snC*FqQadQA~a{m*^2ri~O8V^V}MXUbj_D-)$5uKRP)iNXw}~2A-bE2_UssXUa_7)!F(Ic&gXm5YucnMEUjhP zDzh@D$Bhns5?Jzb+RC}?_CV#r*CDk99bwFE`kre_qUzp2Se71(JT>%T$V;Yk-P##f zQW28`U7>*3VD$3xhrN+V&Fa}gm^ul)F&lVU< zOuSbGj9>#?>rpmmxJ9VXQJC8*h9y97GU;U0y69Ic2VX?-GM{0kLOWZ7iXQ+P1urq} zLgnNZgwz6<6QZms2;CaeEZZ-}GvZKR&SOK{lib(={HR{%DAgk$4DhCh?ec&LiOSdofX&n8kFx z@AdrbKwoKoef^m6rc1oAFrFj{%vJ?0+lQZ6S?K^gl-!C?2soDPHytAB8*x2$Dpz%e z3Wj0F#qFH${nUv2M@H1Gi<5iSN*nRF!g5Q=8Xu_KKo*i~~QF|MX0(B~g_g4MI3>EaB|gPvbG-_^PKS_BxGw_y z!D*vT*JJ_(>lGfXM_2tg5+;-TRLhy<(%IoK4v^)>pIwqf!K=kZ7lsQm# zUbT{IwYc3Dli29-#%SR5`9j z9ZfejzY&Le@j~(D9`Z_4seoU(R$nVTGT$uCgN-$Uogm49f;ox~Z+v=>QXgoYCA8ts zDDPsSEOOR}6I{&Opn={??%_Bia&i9gjxA@swwf1jk;0~x^}dh;mvvABa1`ASpBIEt z7C(^g(hi0X8~$jSu7_s3zFU7`-C&HSPV3CgAOc3V45WfuR7ED6t!my-@>K4chTlHL}J=rjwGc zdR%dKQ-O9b^_@&2%VhsG=Wqq9t`YzYkRw?EkuJ}e6z^LNI@hSbABAjkJG&s; z)aL&*b2U`&6`y_Xi9r6y&gV}FBYh&(;EZjcHNfLmh{l@fWdlJ&8~aO{sM33~^(PGq z-3F6$Y;@&k7Q!uCO>w?{9-4BhMhPG@l`<_j){z_cOH-P z3Y_2KA)s=}Qd{A&GWLPExFGwc8)>6GnnVI|9vLOk4!V)4R2t_iRfVU`HnAX8+XSF> zNX=cyv08t3jBpTPu6YmZ!EatLW6NCWWRBIK4CE+X*i|vj^k4VzKstc;oJcHlAYt5l zmmIXS8I-YdK7Xe6fr#_GudgltWr=~^X9rS2VBv+>Ae2}#aP3j^(Mg%Q58x7sug3st z4;Xsq@xNHx?sAh!RMoPaPP^oRRic^N8JRk9pFAP&ZB^|q2mNks!+@ZzZr#tApw;W~ zm9#lNs$>hGI79rs=V-7;Q(m!I!0oTpbA?-2h6u1s-Cb;=QdQs$=IpxrheqT04<)Z% zD2(w5c#xf#9ZU5U#>Bg)3_A^`R*u^c)~Me;p-sy(xb@oB+m#Up%yQuRU+nXL##4`Q Z5bNbauk6me<0ZB`p9u~>B|3TG`d_-PdpH09 literal 0 HcmV?d00001 diff --git a/website/docs/assets/houdini_farm_cache_loader.png b/website/docs/assets/houdini_farm_cache_loader.png new file mode 100644 index 0000000000000000000000000000000000000000..27b0122fab3e2f1b0509148fd1c7c00246c63264 GIT binary patch literal 134816 zcmbSzWmH>R*KSLZB838_xDjjJ-)#)}CvwDbF(}(dw#luP{h3o;`c^N=}0N-=F7$E@hU_o;?*S zNK0z_m>jL31iaCmM?GawG@?rQW3s%mQZAw7=FOpQt5hyU;2mim?stFXpk$aQ;g*iX zhTTusN0s9;vGUy#-zzyz5@|AbE#bRxdp%EmeLauOWaqr|?fJG_wvZfgay-|ll}ZZwf42pf*q*3kpbCzOg#Lk0OOX;;+>AX>XUwYxnl1`2cmq4LjW%c=OE<=7W)cTmpXpqN`UbUyHI@J*ooI^I`P{HR3b_IL*y z$`uzUd2gGSZrK%hf3<$R(rya|rJ5wUHMy)+!%J-ZEs0Cja&y+%FZVC#YT;-k9vLCE0Hb4*|_|TG^hQvABWZZ0`HJE(txPP#%Kp# z5i5t{&ufsDsjB~kB$_SYD|ALmqo)3cXFa_B0_H0>4o9( z;F?7lUI@89VjY>>KX?6vdKDDe!m$RXOmGKVm(MMiY0x}euLa`(c=;yD9A--5Mx>Jm zNhNs;?JF#I$c4R>Q;ET!$16}VH*X`hKzy0q)yRTzgO1BBTpBw?$Y)qPhk!au1=##c z)Qrgr_53dw&q!)0iHOR+lqK;w#3jbrR)HGE(zi<+?du(DD${Fm;ymA^T~!^Sp*xcR z6L(4P`CA{EL(GYbNSbOX;QVL^!`ZxJM3SW{34k^Tg$eNv3KCJ%=2U`>?qv~H3 zE2V3N0@SQaxjV! zcoH=2n!bFg*=2Jr_$e;fbh{CoOyG65__k-9FP`3VomC8mI-lc$x~ObCx^FzI;hg?R zWmREiXh(W~3?VR>!|HLXMnv~Zg58Sgf0Z7+g5hp(QF z;cgM_fcx1m6kc|>w`w>zyg9glW)Vu>rvujrQco9R!sQhSPl{|(JndU(0xxq zCf}|d_H0PPyYNiwSr{ma9TQnJp$VOYbfpdw5r=bkyKj%2%_NyIT~u*RTBZyRH`I#L zX8!)hKnzm21|I3^GkDC+O-=hN{6`4DcuVLaxB38A&4i=LxL{$?vTQ*`DX1-$ru(@S z$uH-h0a~pwXs-znS|5dcQ%I%U!C^BW>i%rOic3Fk@{Wau<2_S~zik^%Fv=UJTJ)ywT%ryUxR_UE zceRUd3}lfMTYQrJ!zm(Sq@o1gnmxa|nj6%l2meeSIl9jMkH=xGP=e5#PxImPd>9Fl z#-#wk7r+4&37|1!`d;TDJn(bO>PH8A8jXSe?5EFrj59vmv=!=gQ)4eAxaAXmi#Vu$ z`GD7ugvA?bIZ3=|C!1D4hxduQ?$FT;d2x(~f=+<`kZJ5igzV3mk8rXJSmG{;6B$IY z$rQ9_V2`hX6kBd=VnEmI5~uvZ*Ch;B@BLLO@6Jih5f8eA0fzHiMs*3O#f63BoXp6$ zzM-7bNwMUr+{O04mLDDAsSr}%)51v>NEV^5P#}?hZ~2LntA9dAI?Iww6@;ekfYUy4 za#);^ARLvsq_h;%^Mmw>?N2JHoA@{=(G`6%rF3>|pI_3qo2yKkI-hrb`-MeuGDlPe@LP?}FSqZf{eI&CFbH;O$JKwwG zIqL}GcE58>>bdv^>ZdiTDvSvea}C`Q-w%omF)nf3vs95sA}*KM$4a#z&9agF145Y` z3{1YUSa>4ukvazMsYjD1bjB*&7*=ow9MBEtG`^kq7aCpVlNp{<)YTLG$GUSLt#%sr z(`;<8Mvl@(G1f*y8XXSl^n2$`gX8>QhlWr+nJ-bo@H))PVPU6v}S)!_g{eYac_#zAysq6N0nv|LDrXYbxtRWwgK)Y#h3Hpm@ zwcMibVIS%g?-F)H40!fI^a76^p@pATy6)Ro6c{~HTY`?W33Iq>{9{1F8c$eG>n!1Q)kBD&&7BRK$G|{ol`|P`9504hHr&|9eOJ zEf>xI$26#T|B5k+Q3i|lU;V|^`1=2|-l6J%)HXznr?RoJ@zKQvhftRvS`%ubczM&` zC+#4GRX|O1nG`7~An>zy6m4Q+;=9+wz`y_%H#c6sU)|pNI z<#=UIDXNYFM~%|6azI*cpN@XDtM$*u21remWPKgy^&20#T<&4!!$gCJhre5|p<@ZX z>sHvjBNEBu?+_7%;Vp%QMVPNAr7oQBn);!Osu0N+mFh>J;jJu-xb9APT2pJJ(Gv%E z1wHq171!qA8iwXxC+ZHyMaT6+Np)dN$(3~!<^7Z5e^tjyX0G~azDEB`RFr&{2&~4$ zJd?XPS2CV#VsF|a&*Xy|J}Tb6UD|qxv^NgNaz~JUF7-&;Ir1B6J=-5*RvWp^pT(|h zS6Z%e#jO7y$&O6Jo+iaB2b;RR);A}u_vxB^10QVPSDK8%ycB~yTetJD(#R)d-F?{!pri=;`eKB zQ82dTppeGjKg?dFaOl3BfOPh6&emCBZe7B_rBRG5O5RjcV6aM~pqZGwS_8YF`_K6! zY4r|E^p^JaeJ$xlsdAgI6`!*U3uzS7ne)@dNLt7z1Tkb7$;NQbo*ZeCS)4bgh} z1b)FJe3CjV2-j%V@q8o(%6UCK2n!1R6vO}WqpM5M>?z7`oBr>Sx*Sek!bx89eL+ES zJXC7D=9ghSSh97!#=rh}UD}5u?0q!5`)y%yu?IYU*I7u5JOn$F+E8Xo5EK+%3NU7h zMjKA7>_x9p`#GM;&t<>G@{QLap2Q>&#yd6zr>8q)pT9bvr#hsbzuPyn6BWPzL^6>h zTB=5u5NP9c4{dNKOuwwc#4R?7Sl(Z4^+20|mFHwPty|8?Xl`x>CwnL| zD13JbqE=#V-8IA{5)jZcEh*mK9?2$Uug1y!u~l2gI&7S_nuj-#D)<%4+TWMs_F2WQ+a=9~hZA>-e~0D$>yQ5*UawWl|jgcDkT z+*f~tRyj(Qo(|u7uHYWB4)_2v!TZ=!W1cP{XDsW!fGTgUE%dKd)p__5j5OnPvQxpH#+Y67|?GO6U#vI=r;*=V7 z3OL+XyR|*C{VHpk@m86sak-!$Pu23=A!RIid)RN}T&64Fb{)E1rTQS-`tU`&L)s0t zpjkXx2K#YSfoJ7wEYz6?d3Ur@m~3+X!#P{HZ157gsLwI%6Gu9$5lqNo8uKUNHLtU6 z(YS+pO_~@A!N=IRI7Ge`A$NP*Ji|_l@rE%kO&8P#UpE#2pKZi4*R1=C)_pj$o zX)W^2n=YoPc4z(WcGAWB90hYrcYbEZhVAD%&EREg6O(4c%Y*qELUA9z@6}S|S!5bD zCdM6na8E39ZX|K|DZby;LG@0wVY7?H>H`W9x1~S?4rw`vdA>@^Fc&k08HNRa@Tl-R zZgIoce*HsrCF~Xz2L~sXSQsOQkfZ+;w~*7~P8o}WThCaW0Z^jMlqKSZf$$rC(N=ZG zf%mYUS-Y93dK=2+E)O)0&Ksoj8?=u%ry4{ewl5Kx+86ZA!GY$gy-MTB%t1jbqYY~}J#J4vJ0H#7SW>Sv z=@cU2w=27X{(0Bi;CYK5N5VU6a&h+lTKckKxIv=8I4is9GLpxI)^L9!yB)G70j2dq z(ycK9Y=>hIO53_dF$;U1r?h?N|KfcG8TUmX7H-$a#$>{NSRvjo0IjX}W;m`pmv*p+ zV7O?Ui0`=s#6FqSA03`8=pk!(tdMnc@3K6VOZocs=S+TQ(D~+I{N_k-iHQ?ka+zyl zK}*1Goxy59CgE%i8{Sci9}MGHFQsj84aisW`pci)bWDvz4SR#MVEFQ4WYBtk<)p^_ zuYccW!+bVuBB6Dx0gox9S`F(oK2Q%y=wndqiq4O=Ju6!ym6rVQnFht2gXMdYw4Clixv>S#!I?97}Wms@hiD@eIyd|15A# zYBK`8GjT+8tq;AX4~;0jS}(L}313&KNd_KptKMjzdLxPCAEA=Tw+RnLvw!%VFLjXd z`!xY^^b`A2DaFaj3DB_lq>QrmRkQQzFZc^I@J(l)|3cRz2Z4l=qKQ^DP)bS)jB~n< zi+gZ(ZsD!zd`$5Y17o^052-AP$qq}FO6}n!_^EVmQoYo~_#s!v8u{JRjYaa64;sv( zwF>_GD)Q{%r27dM4uO3N60M~wf4s-}1A!gdKl}>6fbgN%UEh>6jXTViML?Il%AS=9 zDl!I>ESn7Q@%K#cr9=En zY;=I+ONW92YQX8l)G#_W1f7$a*}$+mxvdsBt2+JsIox$(J$7M1K>E;b33q*c=sS;1 z0K@BZf%Wa}VrO_^p~dW7U*7<=qG3~eYel7a z^x7G@#O}?^%pm=%fiNX^_g!pLN>ivgo3!!g5uvs2VAIFcBPpfIY=^(02BXI?aC#ay zPaBisdW3VfT3;m~++4wCJ%J>7ZM)?~xbe$S2+3 zSLVqn53?wb7x6l>6?uB_7|P|Z@$$$yweVJV{UK(J_vs|3-)o>4QfriXwGZYAYVkPa zD_}EnK}AEUn9#wRj-75l=>Xnd5Q26h$AW{p-7X>!q2lN~q4#)`95U)EV7?`d_L0jp z%zpk76}>1TY(=-mLE&SP!xPbBu-}Yi(sxD$)E!njQe2H4#O7^`lg^)AuQ6;yqTSW7 zH@SPcYx6^I8IZzkOI}LT_I1Nbql;p()Uz&=cB0^iwTPR1z(wTJeVKk|UB>m) zy~`dQaBvg;prn3$^u6BI!&7Rp&Pw}jelcb`a`djuAngQ2e0;1wpQZ5@y$$<9AbDGmbe3PEO`GGv&nFxFMNoJFIKPc?*Db})T zBEd)ro01DlDVtqSslhXop)LO30*1~>)JxLugPxEwc7vq`{f^n^Elc|q9VZ`MID{^O$t{b zrR}6&8vX5*HY$%rp>V_Y>ML%;G*^#Z81GbJ(z}qpg%*LnfxbQ*yf^Z%hAC1=gBA3} z{I47zTp#^5OeHH5?IT(r9<%|!5NKhAD#e(tq-uh0wb7Jn7Q-wDaX1En=5}39v$LXS zuU3o4H8ui80|n82{Ti>?o$#$D2J4K$88edskcEJ_%S@4 zPO^P3{#r(Cd&o7F(;c|R!QLfc4KDSK*E^9!jl~~&r3mManzd1jc4idn_y|X7<{zE1A%=T*_#aFI%*- zU2f5MxUkZnZ#>+58siplqPx$R>})%OYNRo0AcbRQR?kE;UWld%FMiu20v{$za(_LJ z4DS)Aa(Nh5o=z*ysAJoTFd`Y`70ODu0>@HlS^3IdpL2^#3+t0!6nP-OZ?RRL1=D7Zs zujn%xuHH&74|YdV69D!JraW=VQn!_g;G^FOe=H}-HC1FlZt}dupzmv26iiS`fw)2 zmQ-^svfr1|IKA$=UKRc(1jEN7`;8O)a1|*wFAb4)T&)wd9?ObYtS{NTIkNz+|cZIC? z2Nf>3J|`4!ZAa5{5F5IN&C@s^H|u~MSQ`}D1a;!_*!jdegtMm*{88NFinS#0D*6D1 zki*5!gq#t{)l;=`yW8n++_4NUmCiKB_J=YxX<&r$EwiVSDy94N>96s_XszlP0+xt` z->+eqVXawRg6oIr_%>>}8Z(;Eo008Nh=mlm<~7|{Lb)tsvJYQAXb%994~mjs2(HTv z_JMvbZ!4wGj{E0-K*7Z&2q%RMui^PtUVdTbf2^(L0)5RdEW~$SX)A`X`tV*stj8bK{%10zMn5J|%lD^eDn8Y@O8I9Y<37GPFPnI4>6_w~blx=}x?2{>>rTk?;`~ zbBy33&Lo8S%dF(2GazAZKOzf5UsRiRR@b3!3nBvd4}MNpI4{-pzv10<1dC(sPG;*~ z6dB8OFSmjP*MHq!fDY=5J>n|;&+zCPF>`q!XCx(lB?wT)=iNU+y--^9Q!w02wCS-C zlQNi3iG$0NQ?tSv@KEQLtcX;*1v@L8`VmCl5ZD!Ck|bzkW*^3KQ(qnlBnpczdY9O6^@9LlXLi-H&4pvGvbDgKyrqx-?Im@Z+VPM^kc)&362 zVXZ_kgWu^hbvOp--~}PaW8`@7@lPCaCuMML3Nd04Lqn6@XTB+uImIzlS`p@zO-6@= z^5+jlxgKWFWwQim0#;wqXqrdNzHf<@f$+TikD0vy855XjS0g$83{$2262Hk<& zW^ySUtmLRwa>)al(@&>(0mD<|K&NFl>CmPF7p2ppwzRdBHNETSnM~=+Os3yxJgceT zqnwHPi&k0PN>FQ94-ZM0`ym$pjd+?={*#-h+-c`h0>$({AZHzhoSco%{mzG*&^~>4 z6_WYEKhHbYx9#keCUrC)eW#`;Yv~k(w^O``iRH&KIXG6cZX&L_gv(0ejFydlm_5!f zG}tA>qb|+<0~uOUh19)pAjE5Zy!f@cnqqcWCVyhp>|z8iRjwQHD@sfD!0zjpreWNx zHu>;Zu`X{~TUu!%V}uJ}S9gqu!{1pLK7>j+f(o2i5C{-JcxV>Yci^ANn~?ETM8g*3 zL07Gl}fK8{RPUZAwy_T2UXwjTfF7$v{o9JoxzgqqLzNPwbvs0wy{-^+0S9-&co?z;`QAPY|pA>!1-X z8tEV4((b?bb^*hy7txpRM^E|w7m(iD|9`=7L|MQP{V|2f9TD#LcHc+tUs5Lp>d}gR z=vEP>PU06Wes`y$&E(gqP@m%fcgudK(J@bq`rp1gg}*@K|1)*(htSK~Sr*NrZ(_32 zz+MDgbXnVgkoXYy>TfZU4&o99@->eoH`B%V-3bOco#m!6BL)OQPJz>u_%B|s7WUwK zL1ijI5%@KV0`0%>Iir%tk7l_z(6&asfiJJBG;omS>5_m+hm38hQ7UscM_1~C&nc1L zR4kDF9|0glU~Z*I5E&B_Lo$-WUY(9NRd^-;BKj8NjdXKY;QG#Se)_XjF|j>iOQMdz zKp5UXU@GDTAIG~{xO^7(nk+VDm4uOABMZ_p54ME1_ zFv0ni{Zn09X0VL=_MS?F#253e6=&ieuE$Vv^Pl!RpRa%6|JvD$voo3Xg8o0p>mkX5 z!2M$SC*!uLPGN67GOwpb@q@`-2V+er1wzdQxdurwY z{)l#`2SFgFzX$O~b2&Ih(wIV@1qb)pn(mohpB#E5&9}I{KmaT+r=~PoT-TFK6jL6< z!cl32&YyR9pzhJ3eesahp^Au#y6U#`O}x~i?~bunA+@;J z5r-vJa#Un_*S|2lPOY)GW@hi5OfOGey{KSYBK^cc#Kl$O-%BRwZo5WPyq>>Oh>E(Z zWkpQIlzZS<8|xZa!_krTLt}D$RzT7@4AxUN zMRRtw2Jtq{^V@t_dI)ND zFRx?I7T%{Lo&@(_1|IEL--%7cp6qT_4^s|$x^?(*;<#e`)4mN`{#jq|JT%Smpg`fwGSQy zMwg@6O5+R#mx(bQX>1uA6D#fC1RW@LIP_x0pO)jLB7tSMt0pF=m*?7>ADn%P#`@`o zfk~pf#Mke)b_+M_p$wV@S(9qqCwwsk9(B!er^LJj5G1OY9*-zy*HEGYy zp#BPz)u=`K)jbZ`tkU*^w-&!6f`ExEEG#qu1tKWAGK+bQd-mjWY8ec=wyDWvVgB>o z(NYSVh30Fj0bp)ar~lO<9lE=~uKFm?6WQ>t(g-)r?<+>=lDEWTl|f_D9gAfacsDkAJlXcF2E;hfnF^0t;$uYulrv zKNlk%O`(*u^2K|j7!%hGk0>71*|?_+(wyOUUe=!Be(=8Sj*NeCF`s53U$+=*p%Rbz z{p$Qub7p6&gZT9yoF@@qAM9~zX5H$tAU*M5_?J7wR!x~e?AbGx!CKt1*Y0B(@zAjA z`~G-x^?Y+dfOC-Y;2y8{mQQ-mv_CsAebX??qcbu(alwBg>^I?u?V_|IGa{J zVX){_59D;-&k9O@`P%A{iOAH`9lLJ@mAd$g#un7<7HbNemiWpHOVbR}&Wuwa7Y^37 zO{>w}x#Y7#`WStqyQ=Z81g*`@&0*79blx4b48;!TTh%e!jawwNb_p6?!XhxrBKMt3 z1bprb_AaXGdOVhMgfz&+oh-If=GGV#4vYf{1m}m`oo?e5$|sTK_kO`&R-Abs}Jbh-yO$bL;8OM#o=Yyd&^*Sru5ugA#@6HI6t#Jq3$_KgZHEf>_*u8 zIL~O2J1X|;09}11MnS2JtgcfUW<|uS1H0VO!<*xKN>|u{Cz+@h+4}mrJ-nlpN4-0U z{Ne8K#J(-Z3zx_2m!5!-;KoU}8t6dZYBWVwS{k{{`y|#{C9_O98gW=EKwAn4meZLv z`Nm6gwzFl=4Vvvke*ECf>UPyIwvD6UB|{{JACd=Nfjzc)g0@Eh4R$Md937`wZF?2E zHP{G)6yFI!QMR=$Gr%c`O)~-%4nS%Tq0sQg!>+!?`dwGReMeZ>xA^|u9EZp$n#k|m zVGWxtlD&Qc{yStyFJ5Rg*y1X~|B$!X@mH+0dFl)X$Ly$p<8 z59Wy8za}6EPh-nmPt}#B5}_tT)WXF*#b%56DsI3Krci?)E|IC zcMau7*{tJsxYrlEXES)OOH3d9P^+H`E;q(R6mk8`HEdp=Ee}o-wIR9}LU38xCI?aR z6Ee7E_fOX5;LRoB?`DzXaU={G~cm{}s;9TsbZ@^$Y zM;R5+GI{u%#J;!t>Dz>Nn%>gtQuE25?x$)h&31Z+g&$pQbNb!uEP%Jw*=orYXaf#P zfQ5W1)PlsTtcbV+yFs&;vOtRbc$U`Zolk#4Fw~=y=#*>+2Lu|S;~BiNRR-X*lkQSn zGO~gltsVJtNC8{1{^l%a#*hT~xOmVd4f{_6OPv9de6m?DR-h`cd?6BX^8OLC&+n8~S5ANnb++2LA^+JOc3pEXeGoff<5v)VlVa~?B9kJG% zKOv8Qw)TpujR)e0!;c}YO(9;!G=l+F&N@5ZcM4`+L9(y{3_iFQ4q}z*%Yc42ki26qmj~PPq6$2krRbxnz1*k)gM;2HB;n zX&OJLpq!5{>{iRt?su;o!U=420ITcgsWfcsRB0-pkZya9e;=d>2w+1y97jP}og176 z0y6jQuB5e{nq-5ffHSJsCYn06dqFElGT%Fu1wDU+rLS>14LCkLjLIVF zAb=SR0Ie-*$Cb8M-_F-dTS$F$0TwBy6_Yu_HC6L$uojoDM-!`kWE3maOn&Jm=0{}( zsCio!$(8`&dEvbG+7nK0fRM3OsC?Sb7!I+T9pbHD#_Tuz0kF8ciRAr`+jFzU^GU#; zp*3;f$d3H5_|roLlIrY2wq>=~roK`if#! z4Mn&_EjLuZez*MRhwiRUaDbl(?Tc!o*1DEY`$-sBCM~YG+oP#k`ch2@L;b7gMI^Wd zxp%wW?IJCV4|lu6^5kR92%XQn@7kV+a~c;Jd=9a)uSox7T|F}EJ20qaf2hjf)qXa; zJhU#Q-5$5}1d0CrJJz9swTd;{Tc#=l+z2f%4GI&?N8|T`E`|as<~Is~CFX@#i_&LBNz{erMNhl<(b`4zvq$cH@E!4(KE>Fg)XrYuvfiWwzxPFD$2-cQQs zoh*)me?-*RAvE5F+z<+Uzh-;dpcI@P@@iJJrwv>Qf7FpWm9R zj5L0j*KfG@2YFDA|jDqPR|n4p3vFu{GAnis;tjqL7h zG{lKXt`gFVa!s#jM{3N^M@OdVl(@W)J8sr@bAzWS^Nj@<&`Iv>O;_JN@Mbeg4b?>O z*h~QUi#1=#1}UT}XU;Y)HfXg=BvWl1G#&z)fZ8;Pq>KL7%HkNaWdL&D#}}tDUOd52 zh^Ob7N$m%eut>QwEE4phA(ifj0%L9eYRhq=?`U#BvccmOe=rKg)aGonY0Zt-7mT-j zE4EqU!MF^nIogsW=y({zwww%Lo$lKm{|ERIy=O0T*W-?`r$?RVk{55b7H>jsozs%? z_qTWwDIhcVcN}ZcNsS3B-*sKmD#eE@+kzh3+9VbBv&>#%wgbJ_GAohjbgNAo{je1k zK{bFfbrY2Lxh*$d|@yj4bB5 zZ2Q3nwym?tsycp|>B&;$A=+f<(n*l|%+wSY#3m^2ZtUst8XZ8-TcRnnOQb)9Cjipx zBEz;j{8%ZcL$&+C%p`U3ZTU)#?CE=EAw_cUG;VG!k06y-!+JG7j!84S73 z*5*okR%Pjz#tw`1t6WFiNyCT=g2kC6ap_bURLsd-0f)wS26F`5?O5iUqtOECt^61H z5*P_32|0|^$w5R=<`#RxmZT{&M$@j25u-Q#2t>K(L-@uFD4OxndATDRD3$|)ShNP- z^MSO>!%E`Vyql;A71P2ZzSX_mD(NlbW9Z(1-yOOz|77f59j$>=2H$ku^-nL28a7T* z6Y6O~rBR2c6HHKzjVM&w1*OZaR&~;mn`v!nz}5_%w`~U^g^C1P_gz;Ina#m;;n-5QXy!4|4hx|anjKlb9vdas-|%(?YlW!}m+ zrMLa7oh5$?s&=u4_c))XdLV*~OfF7ctfn9g(tGZ`oPAMl$OHF~ucka|{EuSevRG;- zo9Hsy!NjQFqeUw6rj%M~);(WPHm}nytr5lLC6;?<37;bq>>DXnd`$spZyFu+Po$;A z(3+Ag=J!y9KGdk%lI6-S*IdHK zNBVd!+cwy)BGX+Saovi1;}MgRO@;pcq3*egKzPt|^HU}S>vHD;&*%`0%T$|+Qb1W} zZqj%fK;A}$x|Jd}&WnG%*t46Sfp>R)_S@y$JrSqtHrJF^Fu!+9{h+KkFqOr z=Sp35b@hPB-`+2(u7Bh#pCyr$x6%`y)0C8HRM|fqj(BTLUqoo2h%ly#vNJHWUc3@j zP}b=Efk}v4o+}>5Jp!lLXW*lyeR+%dMjc$|zB6Jqqn@Y!ZfgSnR2?iGZHr)B6%BPv z0v@I!eXcvbPv;DiXX{=i>4<;)Bhm4AwS*`cq2jAt`z(AXBZ6BDv@9nwz0;JrICSaK z-JG5PCj88MV0I9Z4Kiz_P0!sYl(#$5ch(7iuR--3h;R+-jr^;c&=UCww@kjO*JZ{M zBjNfBkM~56BR;WYyzD^-iBS#4PucB8Hc29~R{`pxR!6MK?vIMtaktiGbC7{ZBB!?TcyXZdg zTH(5}(L-I+J(OiMJ(GKLM+o794{93RtS07AuX&^#r#A-_4Sr?|mt?kY4gwhV6Gda&E&W$)v~rA=j=?J%Pw8n3^-wImKh?d<$W|a&K?~Pe|6{Al*et_mb;56fImr ztd|!&Bwe7w3_-&7OEok=zz?oBMVz|n* z#wM)Nt2*y8u~#XctMytd7$(`})R$@+JP=*3<>m($30vzZpDa8&MqqwG3ZXDW&{FX9 z1DELR068-v=LPEeO=7P=@XoraARUX_#8E3kPYu8Ec?zutT(D(LS2#trd!PKNR}=4@ zoh;da&9D7DUP0%_l1>o=ROk%YS#o07BnTRG8d& z1{h#5r5WB!)s2K{_sn=si6ozXM6pOQDwos@1zh*p1t=7?l{> zlYi!G&ww;rlk7Lg%OV^{1$+Nu5Jj|F+MvDRTctE6a(t0{BSWa?W}Vf2vL>%8XiB$I zPs3_-u1rIPcW$j*OYRZ(vP{80%-!*1u~V(l${Nex>qK{ZdVhMHw0V|`0eolbv#t+q zZ?y^VcS5y$E1$yth=stlsvt&<{iedo;sQgyAeGFty|uMjlM;=bmHxfmO&i5j^}+jk zNTZZr(Wi-WgJSMB&p>_HFDGpVUhWCXbV6Y*h#f?^tJM+B9hdQ(E zM@lirgpoWY1;u;77wqZEFAWz=yXomB2YHhUrvei-`=S(<*~P}@<-VMjLlQ9kPn*!2Z#BME6QN1tp$zcZlCAt2(pmQo1#xTtd}XBGRw&^ zKC-S$jzm5-F1kn8VgBQJQMAuf+`J=+n4dph5PV%Y5Is#sMz-8En8F#@e@VyCz07^C zl2Tg)Z7M6X1(3_Jvvzg{Uh=gl^S?cu4=(1;v+TNzxCYO!=|7MKJ#DI$Y1K5@6B#%^ z%vT%Mj1=4V*Fzy6_kM>Q%)7OgJ)R-TlXet(x2EY;iob4i*?tti+*<_zg1H{xgVum; zd5~wrobzXoA0s?gT$%*B5VoXvwJoBwcy#WHK~;}Z_^n)^*EYSY9N&p09n-#@CU-;8 z-QsFsNsi+yrfeQog=4am6z%*JFOt!8mclPC?$wC2kdED`rAVCmK7n+!_2TUYCG^Iz zB3m5IfxijwTU~QPzbsI8Jfp;Ze44$WwbyUp8}hRv$a?cyCgj+mhP)ZkxiknArx!D+ zG3hL+bD(pj5kFNxd)t&S8BcaUou7!zbiR$Lp8eWsdTD9w7{1V;4=_x7mV5cB(`q#L zO#ho*>aw65_adU$Fec>1eL-;dDhR=)Rm_2WfiXrL3ZHL}vn9aq25q!#26UIi$^!Wn z_}VSIMi2or_l2XggX3b2yO9D_6<~Cy%%!+KPA)KLJZjSnmccQL!##c9io72*TMbN3 zPCH@rU@}fNY-3N`9n0>P=jEaeaoYEh^^4_O^$soHJ=QshjNTlC&naomHR#i5l`@m; zBg;-GU0Qyf7US>^zJKKgsl_iOx51gn>FFdFcHQ00b-P${K@{&L)Vt#PykUOXuKm7M zQa|$_$~^u@cz8z8xO@z?%-4MjpzL+d3IGOtx*tB55ucjbNz*rEw_86>{0%P?w98r> zUR*SnlTBh;-`+1b?g%TlC!HJG+!~M-GUQ?)j?*-jB8hIkbm&BuLL{Va;twHrl08}4 zXQy+fhR(6k+@s@%>9`brgyD+c{4i8s4NF8#$E%fR+%w82Kzo@^K-kX_mCl|7N0aQ+ z^|`MQc^<36K0*x|+_sOlDxDXmS!kO0JBti*+B4g|vcf=_fg?u5XE~%N`tH#03MDCp zp@AUrCwF+lDp2Z!6vaKxCGq1eXSHsTpPj4;kDl-@(LJ{iy+O7qx0WoFWf&vnpkJW| zoMbT?lzLH_>7|V7u=_nS`2*g9A7W>q<7JzY5Ty=EueF^E8^tvZCMUzX`K@6i!qE$| z#_RPizDZluO#~*D7tmUFb6Tk8cD7-KAWsdJuZbV^-xI#gaHU`|s%#LmUg`Hh=f$5n zT#(dygipb{G%K{^`tEp&6GCNSUiP8Kt70qM>^JT8WVLLzncHHGL~pLBgPZ8)vGi%(m`F z2mVv{TAI=3PqlQWCYn}JlF}n1Te5HsWL&xyDt(MX*;iVkQc9B*i%e19G?IjE^pd6# z@t&`dEHZ9AM)?2jswLIX;P;YHbgp+qTG=sM3<=7bLD&;x} zdy+rV1~zNlm7hF!YH7E_Hy;Z!;vS*1B`PI%@WQ?-p~}~JONbxy?WY8?^U_y`3f;}) zG`1+k5*q3eNY27Py*u9p1O!0LcADezAay00^S{imv>mSd0}*w#@yq~iraI2sk=LX# z0_e2%Pe(A&m?pcVC(UmnLh|tYQZ<^>n(oLD zgev5QgEIuAQ8N5+N{@CE8xdqCFugqz8pmg7UKJ-@P-<-5gd>ff)ATsDd9dGmz3jED z+qBtwF`P)y%*Hbx?L=#B{Ek75%J$xix4I51u3IxuaAcM&wy+{I1@zHe+Y;Hl(Bk9C zUfC;bP_~|#jSaA))2}Cl+`^hk&6D)MT}vr$1yV(9s>0>Pl@N*da0Ph{Aw#!MZXum1 z4x!p^I<>M;ahRSRj|M}JoPHW^Y|&BZf9YXrMGfrkWo8N6K4buqI%&GH)c!{qdh<=? z=h6fMv2h`x4e5-VccChhGE_S~FPQQicM{R?l+xKE*+u>H<<{W#d$Uyx7=%oqNxFXs z%fIbKL&dIU0=GNbAsqJ;Ug=Aa7T#JPp6cv?hG0cB-{+9;@V+*t>*@ z)v5h6pCTvPw`?XzE?weuU8)_DZ5P28U|rYUIkwKQC~gD4CHW4I#|{L!ykjKd4!R0q z^bmEr|2_zwi9BhAw$wNddh90Cus9o9S8G*&4zr|bO8nL+|BUnD0I&>Sd!qePHnOD_ zKwt%Raa691Eprq0*WxQcCc*zU;L&LJYW_>OHdRs`sv$&PS(mC-0{*afsfos_--MT+ zpYK=h>6GjLoWm1Ztwwis0bJY6O%6WPLPtEV8+?pddOyw!PXkiKpD;9$_P165Q7pjO z?QKTIb(olEwy;0*Qq&7eqge&r3{Nkq2^ZXgt34~9pk4yiJ}9^q5k~&Bk0i(8LPMdT zFHwSqcQ{)e-dK^gzEvLl+vTVFa*MB4uEkc4pA7?uyLNR zblyIi^`D1doXyX%-K9jEI*Z4W3uF6UY=SP8HeVYuA`aq2T5+-Ns_PcZOV|rLpQ4 z%x^OIasS(@SNFo2*4p%}r}rC?G$J1#V$pr=_DE82yd099%+p~%_t~&^<43Yd&UVO{ z{Fo-UD9zLlk9p^gQ>D>E*|CgHcQ-1(=n z<8OJ!fdasa_=uc*J%fX?!(mn;$0k>*Yc(*T*^u8SB>?pb8@q&g9E;joiCCwGpN~(6 zRM~TlQil9`xz(dkd9hBw=adSAfE|m{)b!zEQoPKl7^9%D&<%C~__RMq@cj96g#P_p z`5CraRBhTGFY13)c4pY@Y&Nmhqpe0_p;+zMt#(G*U>>p7!&7x`(>GE90*^ z@38ywd*v<#zYik%GammBX>T2s_11NND+mZkDjP;kl+e`417V#i#tjv=3ecg z>E7hd_))O^P$8b^cZE|kFNOVlqnoNBuS_BnT=YBXZ^V;Dnz`!;@bG#gZGM|mHS`}p z`Y;;xt0~jDd%Q$IA@8OPe+(}E^U*hoC03}O#c3q{@XB~7RkTY++xGet*F$E#-sL0* z+B;XFD*Kx&fmM)Gre`bpB@pVnb)L#Q3<)9$;!&I&>pWIiB@e-Ow^W?&?hll zeQR`+R!$AtJW~7V2pi{h6pLrEC(T1U^e=EK}K;b5HdDI?cR!!t-SIVF3Zu__JZ zP8jOr1N+lSWhin(LbK@aWsYeqn=;DmYL9~?k70>P(ISY7zQE*{hkdCk- zFTJy4a@U@A;3#Pym_TWL(;DRCoko;aT2UitV^a!&!4k6=DTjl2?XlE?BuFmQ}dwwT7mhEC7$nOi7`y$a)h$Q)t* zT3*@n6%0V*Ib8`daEPZ<#+xe9=>mw3Q-vK+rG?MeJ1!5-bGafuI&|c(K}Pq~*`SVJ zPld$0Bd$o}PZi?JQ_RdvPnE@847h^IN|;`JxWY}R^`e96qlTJl-KNX$~;8OCJZWtT0tO7USOwxRWMoDsS2XyInv3{D{?XB^+vGdWJ0pjQ|Ti2(D?8OJ*yJbeVO-O}1tcKj2=zBpyeUPz;(G(BUaN14%pM!!z! z6Cg6n?^r~5yqSF`5<(CJ`qKKH{kYp@JI!<7nF#)gRB!ftn$>24%^xtjSt21q7sd0X9ydD94!6!& z1gGamz$LNRF`6w2vRr7u*z^o&aUc8QicXDZTYGvChw;553|Y*^pOcD)@E<@L4+o}n zvL3!XHCq50h0Xn(3k{^Eqc&v1rGtZ#b%(7vI5=eNj}J|E!&htZN9*`6S8D3Cu8w~dEQ*ZsLdiT7UMxyOR8Q z7dT#LPXnp-s8>xf!tBV0gHkhxxi^;9*YCtAzkEc$Yh`$u)a=&w#$l!8dH8cx5T7|C zzdgCk;`jE+gEYd^=`@AF-bnAkf%wZx0I?VUm{CrlfTZW$8O|IC<%Ay0)}Vp)Q*kA3 zJP-0xwL}~ELvpHB+S=Qd*VBQYj@8G(X98><;rI*GY;SukLEc`e^Hhwtv7nNlfFF7r#;_`NJkNW>toyw24<&6WGtmCHug{WU z>^{zLy+A?s3lB%TwOebOuSXQ%wB0x-EShs|>AyV!sz*;T$a`?u-kj5dH6R~}W+GZt zf>kd7XK8;O)vYnKQ)|Yx$5jmz&~w5Qg?7vUguU@Fep)NJzJckNKHoe z?CDdNv-kP;9&(0vppB|6-RCMf0wyr9w(SA6YPg5{y z)jC6}dj7TX%2GV?GiJ1KXlaVsTPH?bv65N z)zH+CiU@6O>@OFEXG^>CJ-m<9UHYD;z2fUz9mSu;Et&>8yeFfEeGDK<1%j)uz}W@l z6DK#wBB~$A6y=zZ$XgOXm@Mq@^wd3%R#4p&SYW_|eu`Nn#f2auEc(l-B`W6hHSKip z=zB@C0#r%-h|#*>7pCMl!f~XDLGtqQuJ0e8E%|lEW~cZ6Y1`zUmYs4r4v*)*85zs_ zIN^Go;1JFn@%y|EM-HsY2|Q;n8#`80(fC>yyTblF7K7yfDF*l8Su^$+1h<&7ZBMQT zgc=jSfg?*E4Srpalf9E*;;7*%$GcJ1glsc+A=&a>bTMX_Kum(lY+51BbW-mQ4*T_` z=X@7`j|A#O07?=r8i!2AJG(5rO0V`D*?4GJQc9!R16x|Nvhp{6UIB=t!N4+J?Xh7* zk`)+D?oMCs*IOTL#POYBM%SIJFZZF~mje4+43#ooy8AVvObUC(P%K0x>*!q58gLzC z0}6@6`lbG5lNb_%Pk4x?-$f)4^sru=tP{A`8L^=5&KJce{l*i!)$K)azRH%l*2>Hs z8zT_wUyDb6)A|N#|MKs`(mMmz6<2NxM6#I$^lAOK%2>WI2W=A*$^cL%LAZ7JRp4LV z3LqsMdv}B^=^lLPl*a2q!=hFm{r80d_aTxtEsQ_Y7uIY%50@{7PFW&d@INJmP<8jD zw^07MMZPaK27dgoo4J7!8Sl;M*yVB`ee?ML_3tQmAUT>WO74#C(n)73b1Dr;Pbaq0 zYR-uN*P{*Yx2xd}|DSWq*^y#xu2+s0;dk3KgXF30iQz%l?XYeKGCL1I$ZYo$C5S@4@cTOuSNWHeYc zQ7YEZ4xN1qpf6n2_dyeV@tfpvE-o(1_jfmeVPSy2(oOI$=_ZDd;omd(TMQoFf1dz( z#}@v#BP*XINfOqSsi@k4yeN&TZQ20TxPl0Bs}d2k=eBIBUQL1*;Ud4SRZ^P1uTP2xc!owY7t7g7fk#4P{x1D$qQrAFN&}>~NPE_`UQeIJ7waW? zz9^qLil4SKVS977Js}Ltb4y!W`SD4$b@drCdow1(-+6&FoE@LkeV?zDkD0_~ZFF*U zlZQp6hJJOd4ygfN<;+j*@Ojz=agbvMHa6AqLU~wl$eAS4k)oH5m>rVcOFmxfRlju) z)|&CutTgeSxQbz19aBv{JqAT=JRhQ4Pzy-_;q4eGb0FVLyC1qIDmAeKx^muroWAN-b zuZarDHd<=3u1uwSwY6llpGNhQwg!VEQ)|z(IK4O|aWs2e+Q6u1`45cFVkN=>aGLtF zExN-hLzN~aF)OVm6b8(vGyNb@4OGh1q*E_niG`HeDZ1vWAgAEoTK)Mn%yoI-_cYMf zc8oZd4!Z0Q<{2@ht5|#`kfyCgXgu@ch@R2kbV51G|4*!j_aKW2hC^zo9AH#KGx=YF2mD&s4|IDq@#}5P0)?xbD~!#m!wHU|>Abtz^pzfctuTKvJbh-~!?yr&O<7 zT_ZbhI-&vZ8{s1Up&7VrkFN2p-!i>b0F_mjcmzd$wdE|d|D~kCRotYi58`CCU}Ko0 zw?$Azg<|=vwWX$PDz{&bG*MHJ33T3>fX`7YU7~-qJV)y4dG1ikNG@i#r}*AY)Sk(K zq((Vb#q7}qWskJJoXztE*Bfp3<1ORgRrYM0&9_lWPU~|V4JZ3<3EYNAz(W_L<9UZQ z#5)BUpjOX(230P~IUfs2tSHr8etRmp?tdb`1L^`ZpyL71;*=!j>GlXLBcr^tVLv;K zdikq@slt(sgz|9iIiB%L5KLsWx;1xxxa&!1)_ux5&g*75Q>lVmkSUuEQL8jzT<=c{ zK6(jJ?Gil!hGK4WvI^-1H**@o@ccJB?=~oX8`84%S>L-z>(|qWo8oS1AB4PX4Pm2b z${>tKw*7PP67XU~+|O6p%U&@L(na04lT{fX@P>FAzQ`F$$R9*-I+zn!@kas2?5NQ7 z?|X`-idW*8n3z7(_UoU3WT3y_aJ8*<5kkNL6M#w#SgQ0KIY3_bH~nCw zJGsl@d^b=toa11=T82p=wSVKQF!Uy|-x$~rvh8Uwrq>ahKajpH-*04FY-xe;{6WyP zeziW38hXAr`)S$7MeV0SMpE38uRwXi`aoi+F!IKyi4E#;`uom-RD&<>b<4q*+Xly4 z`J>dz{eWAeUZ$7W8-{*&b0!Ro)(pD*d^DQnVxh?TSJbLBzpm*gp=RPAp{Bid>qDto zkQ$H|fogS!RvIW5YSwFLZjf(Zb#ngt)uFo@xU5!d^K`Nh;s;<_(w{%f6kBT2=rp}L z*%;^y*-|8i!c>_L5h)dDkjqaH_go^Op>*4-#=xm>-UcTug07`m~BjeEJ*VY_jPseVM`m** zV+wR+fgcPG`C~cAh6vepzsw8*BB$=J(d+~keSa#pFm-!ObT{)+Ar`OuTMnw5UT#Xd ziKaCIvaX%N#1F&yW0J3V%zp5)U7dMqk!kMl)lhtDJSRF>XcVQdx}T1=&r2B^aM-$K zf_Pc_@WzsWza)Wo?*onY&)0qNwDKjyzWLcFjSl|xMegRo&ThyqHcW`YYB z6XugVk=|jzyeCRLR?Wk%=5am{Zp+~rM#S?CEF;_G)2fkee#@Infh%3od{z>}m;KWf zl-Akb?Clnb4U)eZersJE!inw0p6U$FlDLXGJpVkkk>uGxr=5uojiJsMp!SXuKB2(3 zAn6{oFRr8ofHW%c>(@5< zY#1E(=d!KHfQMl3C;f1hCfe!tylQ>MToIAib-(G}yIY4B!$e$0{p{Qia}`Ie;qf~A zKjwvSI$8t-hl||Mq+D-OFLiw@u|6-g2%KHRBA|4B03j$OJf4R(``2t%+}CKYztVbH z{rX*|Kh7;!>IiFh`rDS-Vwj)a8_M+4bj)Zkhs@R}_!${IDsCo3w7R6Esp#XmU#cg@ z*3#-1R!dF&nM&NvDK8QLE35vQ;TAO`CsnZPd}|nIIde3gSN1;QJ(*^~=X`}rK)<-W zyj}`(tT3qsj!C=gYmFDr$MY2tZY~JrR(NOh_va9mR={yif^em-a?KD9^g#Hfr1}MC zBiTqwelqq&Q=l!l9cVY4O(sK8gxZ1TaMT|DFqSLjEsEG8PU6@d(W(Z@L?94M*gV{O z>{YB6@L_hR&Z0G`RO`T?xtzf1esT2NoUci*PPfj|@4^{xhzS$zIkmHs(i(u|WNQIn zS=J{(MG>_cA|Alw`WQp4h>cZ+f9(ki4?XG5lP_87=!xDwS~nmry3uRo6j_^dP;is# zLd`ilo>*96ED7l>RJR{@^*BB1GFdFvc7F$v*tugD`|v7yAtjJ!W&_+|uPI6CLXUG+ z>uDcwS|NiLQ@2iA&Jqt!);ru_8x1CfHqD#^YoUiyGo&}P$e0D}wC^K{+j`@v!+jA@ zx5!%#KM5YL#p~qjUyD;LULl|ohBVVf%iLmUn^w!h-TEq*hq-I&YjA;#XjK3EV=>qs2S zRVF=Of67y*HrHSp4DF@)L6{E?Y5KF(&6O@tO3?F-$9PGY#fkbH8O2v8N`|g~Z@vOe zwIq=VZzz>3z->mF|BKhUQ(!cqLCp$%h1l7zDDrPz2sCTX^CgzP7EXYQ82BwjVGYzk z9}V-#Es2lojAuy$Dn0}EHa$_3aab9oqT#M~`+rC-xJysoz%#_h@R)n{a4zI-%L zYHO0(4mj{qdiuOhobU5tgyV}u&N{Kaql9r^go`r0wGAqEvS|T;5%!6X$4PVB=a|Td zHi+r5HR%Bvq@_Y&1^wi5GpPMYAzK4op4MM|@qY>|AYfSmJ!tS!kxp`DIcy#_aI#9q zzc27Xu7F|+=nOL`BMh;Tf7aJ?QdgyM42}zKQfGsv`be5blY_qn zg$+HA;@1JBp5bO4K9bGV}6M3UnwiN9;SntyJ^YzvO|Whez8p?rX&MJX<(d;PG@ z-k@QI4`B*F96P65oQ?(i1!uX2W(?vvG9Lyp~^S zeKR8+>=Lo>^pUVl3?&)SER8$dk3gaKehFZ7Um5!k3-&JmCRtrC~ z*VXkrQG>pP1(l@5#jP-Im7uX%Eqoh_!aQ#8(zI^&9nF@|>-i&0HEfzM&3Cmd2yOCW zUxWR;H;Nlet@5?8$>Qn_*_N&QhE?0uJL#R&9?vU7Qi#_I%AWmxC>?!sVSEzV#dP%U zuN-OVJxvMOg{V*yf}Qo%3I^*1KrWndo(K^F)UZDnfWVzwf(DG>nf{~MX(N)-KX4H^ zCV>MoKPS8Ecz^yUc+)-k0c-!+CZE#f%|=BD@rtg#p0yk;!~`1c=a5D06ONMp8>W&rI3I?OvME85gI_NawFD1&h^Ix~geK z)!DVvsZ)q67s^2#;z*PuvSATTh30RKnDz|V(`$^6M7|@j;#-v{Qll# zMxjvt35|d|uh)9MA?S8Sx?EtUtKL#Yxk&F9(rboNKHdawa`_0U&XZ(ALy0Q6o^FukR33Y={Q?f9I?;~FrBKm~ka-n8R#D5e;-pfVBWquxjO4wPe+ND2piu=24EJm2xXCJP0 zmcmxqvS<9mk2U+4X@gX<8>j8@E$92XjX!R%o-rw6MS_51rs;uJFKh1&p0<3^8B$~${8A$9z} zo7Zrm`q-ua=337F^#K0m!8Vq%p%fiEwJP+-de49JandlQk)v>cBgreq?YdOs3q@0r{qb?Qx4g82#wpeX1_(V=LEiVTXm(+xl86i zBcRbdZl{3<0%}B>^^UYGY1rj5<*D>dzcGL)jz>Hm#Y%|JoQFnFFB@C-&eCdswqACC zTE2lDgKyQeJ97KEjnK0N_SPt z5?$cEH_|RA38kS|7e#a?7p<0DP<*quoXX{foi@W58&d%NW8OO#yT-;^5a+(xjj{Kd zG6(t@iNk*) zh5MepFcld%Zf|@?>ABxxenM-D99lf}4K;YXj&>-h8=*ydENYOs`{nPlae2{?+n>k* z3+R|aCppp#_MJ2nya@&t(y5#%f&nP4nQ$kiy7yYQAk}kw+*fk_L2)BD-LnmOh%cO@ z!Wg<>OP;_e=vIW$9snhBR4uvIs?u~iToTZ#ca$y)SCWmkpw6E$i5LVnk&mTzYfKZQ z8>>B!#0d(^K+E5Uu}~t)K1ib(`Stl$_K@`MT;0h`=$(Vb)ydGMZdhHq*ButQL`;jj zWjf99N*$Q<&OY-{1N<-xP_fHM)}Hjh6&~#sGA~&;c2nP6*C&QF3T=^lM8A zzSr1-mm*1-V=AX}zP>06odDC;FEv;F_zT52eE-9wC9{_)g(51;OML+$e03 z>aCfv1q_us#KGQ0KP)_UGlh{W@vE=c9d~Q5m33wo#3eknV^N=e{3tjMXX4sjvK(bg z%2u;ZhK_ke0)r8WsBRGzgh@zk$kTh z5BKZ)$U%_?Zf*?IPxAxfj55qx3@kq)$Tl$laHk`lR_5;x9gG0g|&y8|gnPF)-yZ-vh7oIBOgV)nABA420 zI(eg&BheU71wW1QJipu8H-r_1563$~iNUhpJ_!auEz&_$79dB2)QQ0!6OI6Wd39il zLS_UHZ?|dt=kbrGKeF5+S=q^+0bLp7_3(k!(@gQ))$vUu2bo={f&Ii6d{mf*U z7JlABnO%yT zl#SoL?S=kFf_j{rkf{`fO7XfQqsy<0SiUpjoCg*bHoY78r5#Ohg~)(QHKg-@%>w`k zq%_RJ8L7Xr%XI6?8o834J=xMG9Pmp%fJVy4f-d2W}U#ZOTT0 zsEIR_`uOK6{yqrZsNNj{JoS1<+w^~c{2K8j6UeW9|6HTL4iWiDF!^aj@$CoLAh_cV z(cE}W-JbH;=O`!>l8O}d#$;jmY^Wez)-uWWFfKXTIWd8SMXl1Y(CGHpG{zzLie8=o z+m#`Mw2(S$0qs$2b3Fw&viGNj?9H3E<-lGA^P0o`Icwn! z!M)4zW_G^*Gnmj9?Q1&jhPDhtD$HJkD}y4ZXJ>xN1qHmZjsT$01psCl?pMdI;_HV; zM*_a^D5vkEf%RY+IKf0Fpvq@a&nz`+lutCOtg_<`EOE~~|Ir2IVZhDeA!n*LJ3$_* z>B=RR?gTm5VW$@K6kdCkbbYRq?SVKb75>RUA8o9S(_ z*2E>x7<@tU zMH`XLjPhnaYsIG52zevu-$q|U43CEQm3?pVciBn=n+}AXgJZcS6u)fyM0xv2I*C=F z3Occr7aLEjiA`5b$Nu>cofo(lENAtU_{(J%n@lFkv517YTm0-dBHMq#&DK~3qiI!H zKhSVC&LNiBmOV#B>O3_adz=BZ+7pdn-F$-DG*|jm5cH32Y|sI+A~Q`944-Panj{M$ z66%wc1q-wT+v9QZhKv1rsuQcWuZYiwtJ_CFgfwy) z1WXHRoet7wR3vAAm&%jAz-N5{%%V`LYn|DLwsJ#BWs;*MZ`jR|0t)!kz3?UUF`P(X zocD$Qv*;yD3JQ4Mh~HVpvOwj+i;RpkyF$k9?*hPS`65>bfV+ap5)B`%rhk2Zf1kkV zkW_u^gpNs|XM0mrRM6dj;rr+^Do^Xe6+0c$FjXkNFOedXm79IQ zFic3TcU)O3ZdB@=+K!c!dqnNXbVe?HYA#NVLLqp!27GNkfzi2AB3-D2HanBFA^2RV z0ceCD7Z*Gjq>`t;3kZ%EsLJvzEDQ?30gYw;8-&L#(>k4(64LN`d$OoteQJVeD$kY_ z91*2>bbOT8l_Rr5+ffO*bo)0|$8W(f0{z>sh}T+Nt9|ikkGj-%Z&*epH%r0bINDOf z6YM#ZhG4)2&$7+xye9Z=98Pq(T~NaC{o%=@$c2nUBexc%?!2-teZ~o_$Sps_qm|I* z)n-Meix)j-^X}$zs-=Ab{l72i67bk5SvLRI%C+WH5?=W5(sIeVCrS+M^J?GwY9Y^ZEWNYJ&mJP z{|2I4OQ417AD#8G>3UnzW3YYpq4u9w9k>+$_D?A0)Ao3AP|0M8e|<8(>ltAn;9PCm zE673_DW*mw%(Ux0rHKXITM7VIn2K8FAE$R?L9g)AJikXj8Pz+^`!oCX(_g8xH50YV z#nbkNS8KiTp*oH=R`WKe-5Bg^HlxHDMMG6z&oBUL>PtZ~b&mVx8LONR*#JWdw=*E$2Wt4J8;JjL zOT;iey7mOQXPanwb(&shWPEbYmqgp0uGdMAb`?1@%tEps-8zjTiGJ3YBbAs5g~4wJ z?$%RL{;CX<(9a@o)$P~0xw%gnm%^%V!2|nFqxywN^>=cGBA2yqSWpi^+|DrE0_btI z#iwm@bxzf*^<--XU0pAWxl3xW7;8^321UpSgRk+;2VP4^qgw_rbiTLBN_%b^S;<9- z8#s`}2GmJfC4W5yFkSC!Zaj`ZDy~mB?=AWcTjA73g0txf1T@~7Z*POmnupa?CCmTZ z2>kP-p#jfw;AoK%P`_F36jOMdhzPkHer=5zpK{=m0*0maLPI`u@O*bd@$8g(j%O}% zj4c;Wwg+ee!?m{h&Nm5o-f`QVe#kf~D<5ILD6rH5+p;SnW++80pO+^PpkeU6X7h?> zL;q4YM8N8)gf0UG2xn=goAccnSYuJ+2|?S1hMYlh(DJ9Sy%;2Pc6H9F4ISMI^7_(@ zS0e;fljQOTwu9E?_JFg|XIA_BRFd*A^~aD(^GCiglijM=?o6GeLyO(=E!^pWwHesh}iVSRW2_Ky3jQW#;u&Cn-_#p!tEM;aBX8oybXaGg_2*4@@Tgs~% z+nq^1Wpulq(d^gU9pmPebrr@#f3DAVem59a{Gu67e>iwhq8|&!rdMhA3*K09OR$=N zvLIno{a~W;`}{(&-qCWrP+d`);*5PU90m?~>6Z+BM#Q~Fkw%S*Z-ixp3*2$V2Jhox z>FwK3s!bB}22{1=*w}bK!ZG9>SN1>kPD&K?0|GmPjoeW2QLBKa_;>j<(jjTdD&wAW zdq8Bj5jzyzyUZ41daA9hHDOlAOGe)rh`6zL-S?pCNzNw)_Bd0cfb( z*@%UfS-Zy8APfR$+9%oG6#YVcnitVTL|Aw$d+UUK0nN+n;d0QNOi&&wk1rtKJm*>K zL2Y{z{P_IF)b$@|BjHdNg{wOq&As=axw}0%7G3Q}hiYdnK2BHqyX}2Z7X%!_e{t8L!K0v3{+;t9;)!s1p8uwDSLn4)E^iKcj>Vs-7YS z=lj1Ou1J8Z0uMkY#XGRYy{nep(oKU>UiCiX`w7EL@Yy~)$7-~%kdTI5w zZ)*%E74OxUyg0Wfi!p^Yf`{>vP0WEwljM8gQKf47qXQDJ~qzEgYD6mfDu zyA(uKNcyDd6o*De1RRD<1`;baCVnn7IN$>psSbE)%iXtHhEpo+Q9-sR2=J^{01z~Y zlQyQS*W3ue3+~(F%~f>=z6mFMutah^`27us>w>@O77h^gZ5-_;yJ#e>nzEwx%WoJ0 zg3mm5H#eUlAcz95u;cl8&tUxdU1~~ENpfKg4Qavu0+%wr?X^ltE%M6v4b-B4?%ONy zxVXFDoigtDz*z6-Z*44ZWrveH-gVM`-a3T=HsPRpZ*OR!yszuwP&7W6<=~rSH4Ei{ z;PWBvX|RcA`JdTrFOEG&Vye<)Gwu|v1)b@IBz9D7E+>#T;TkR#AZ)nbrUVwD=h~3{ z?@V(tDYpn<)QxaDMEhu?nsB=g@SQq;uytmvoLE+65Nc^_A6_6T)<@Lc-5zcQSho)h zVCjpX{5sjw%o1pvVRv7B-L}7|8qTO%DfT9YM*c;!^$av;tqL+6M?JJPUxCc&;EoNP zWvyu*2odzwPZkmw@Hjj?h5_qZsYnaa3&f|m>+Tplcb}hbA2FqQRntmf(|^nAk_7|Y zBxuK?#P&Q#HSj&*kg&XKYS3f|_zL4qZf>@*F0a-^0PxlJ_#g{fD7T!sIhw_12GZn7 z4tON&?9ahyHl-8z(AUUrTwL5!Wyoez)*%wg1uNTL@yfFyZDX_e^bT90*P%&SVXTY4 zCa`dZ(v?pDiWbA=1dD$R+p@|?|poIh8xZ*Krg7Xs^B5NE3lXcp>1M7dbU{7RUF1t z^;CYo;=_OE@KNGoKA)|4K*t-G?Ut}mE5WYbV=yF@C_T8^(9pldKhSF#X1uhYl?fr_ zP#SnCBNO)ecQXZV8Yh0L8}PD!Cpq1jM|rt;k1i-0CfY1evJ^frJFZVoElot<(Aao* zU66HjwM{>4eP|Lwa073qT=ETj05AV4iaVaIg%|=!d(E(lbxO#L1O)jGP)t8cvpyE088+wznvfD_#g)r(+h^xG1kd{|&Hm&o#-=+vlo`H@C?s?w1UFG`#T@Q9 z`V>F)bdN8JUc`z5!ftMgj?4D+pF92Ra~Z8WMb)^iIeTLYzA6|}=eeApqvok(p?Y3A z@~q1|pzo;AiL}3?UCKv@#b>q3XQJOQIWcH@Ml`K8fT~F*hx^)DcHo0*k%X{T zlN}ZUYGV1ZkFVfibGnz{ipbe`<#N}9rI3~ufu6p;?O`|LD)RH;jUT(~MVh}Gjhpe} zX*$8QBj1LnE&zHq`YG6e+4F25eESTfQ%jc%4&+K?q}jPBim_86QG3QpRdkSYW}rzP zEw#d4t|9U(DZK(w3%OFckDB38Z(ieK<##&WSHUgtazu^|5knSusdnVcpRCpG6Q$Lj zFOt8aRr$!Hl5)QR@{^2))sSXKxQtI3+gF(e`Aokn%mb>6M$B9n_nXIc1na&j+pI4} z9}cVqXnDFEE@mTBYE=HzAC{6;xFhy^S zC;Xk8gQ{RWj`YNL`vk*c@ulH8t^+S01d#IFi@rGQyd3GyrX_uks0P2SoR4yx%16f!&JUkQ%R@wU#u=Jf@NR(VDH?Ae?4JQ>I#`4AT{{B;0dTSCoknX7o z^C~uY?(a|bZ0#}J!WgYKjewdHk=S5mV$*gXs|ylPD3FsCCj)N-s zUmD%zd#86h%WHnR6^8g^a@QASqb^$ZDkERfE5t-#qKQko#e&St5_8y3uS zV}JZaFb7RN*5GFEcNM}!fr`jpl5*Kz$yXT_vSgb@?{?^Q$yEfivkBcH1U(K6h4OCi zBq3vdmF15D$V0U_uL^yF*2xl_D8y|>Xo_Q9^PTO68+B{z>)vh4y~`~rK`Mk>kZh3I zdP5kk1XX}FeqgDB!v*HSrSEQ+dS~TAzDlsHV#C8EsnAs$TR}CG=WS$x#>N5GtcBvI zbIsNF8GIU!-e(@@j(jhyh!Fb*m;YqKa_)oKjeSS9{=?O%>*F(oR=#y%DsS)u|t@wDow^ovunRka_gu z#{kb(zmS;9pJmc6<*Lme;Nle0QV>(A(&Xl32W&VNC9RaCx=oydp)J=ADO_F{^i9JM zX-WRQ?64s{FbJl{v09JX6j|N@I%@x9mvFb#yel1gySo*Acy=b3XqV%N;#Y}J`BYHw zSMS71uKA=@;P)1;TDOgNK0f=)+p&En;tE4Jz~mYxelX5sRJ*G)88`c_hETwC-hgJ%9j0Dj&qNK^W9!JgWj%CrRk#lnM#A% zG|X2%=kM!_Xvpm~I>lghE@H6TO@7V0j`Dej2Yql5F)>oLYU9OgPV9D(<{ap=hs$~R ziFX+W`1h9su9s`5EqSVoox}1k8B96~y7#O#xuO6SdK}V)n^oz6&*|!ebSfV3-k)%m zZDq0u|8)+n>Uy~A8Ep)$j(U1>L&Orv?ZGUH$;n)>S$$`Smtifm&e_J{R4DypAvE0IIUEk&tp5XoSkJP!}obbE=6 zL*edX{jBd==~`?(J@x#c)%PoZ@iPRk`^B2t&&xDgT3Rm$KIB5V&AX<2Z$43VC*W~_ z;UF7g0hDoIE^~5nz&3j4W&$}289y!-R{o4Ab4-yOZq*KpLZdtdg)G0oSHE|a?Foqq z|4lW-2>j3%)bt5_%gjvk3#bBxKS?a>)X4)W4dyKo-x@LZC{(_pjFIRoPcMYS@cZ0` ziA~k|dmIi!SjhS71o(PGiR1uN#JAsWF4{5SFtWSM5Z~@EM%Tt~c7^_@CIK1)&7UtE=AsAX#LQIv~&?`uK14VqzC*`H>e)<_Y}ls}a^n z`%hxVv>p5-vO$HADs>+pK`^q=XN_7B>lMLVZoF$4Hybqz_djCCR zb>@{lkukbFfZex%(Y5Ik^*Pc>e?r+Hf_3GFXHoqikRA%kZ4k(xuXhrB@bm!5+adka zO?kEI&lpL}riFl<-`vbc6+@pZn_Vcir9fqqsBTwV+cL$L`yY$}t~a>65;fXx=gWYO z^)cwrufvI-vD?X=EdT;eqlwQn~; z;22vxm{YJ?&E>_700Vi$rGjXO=?)f{rIlD=1VHwZ-GGGuPC6;Dr0HHnGyUmf{~wIu z9JiNd%@AYdYX9fa-Jq&EjZ_SSTZ6GlZrZ7ZIC2?7EvNBZ#*#2(ZKFR{k=CoVO_(ZA zBiGULbZZtLb;gZeU_giM9{T~ZW;5swqT8T%3?GrS>!l3yTM3`iu~j0s(W6en&>e zk?ng{DyEOp#C(-6*9yj!S9-uumd*D^d7PT>wYPC;beItq79g9GZX{s@JjFxeiL~BA zS=`xONg!hM6wJRk8tEcl86{V&Cs5>dCJqQEkblYpEI97xJzc=A7*a<-koe&j%-RVf6ANlwCQ zH~zfgdx0pRIxz;HFtSoRfv zN%3bX!}szEXDq&`@F0g)vlOIV(HTA4 z2~sqVjFXdfdbw~caoC`8?G*^2FQoB0&ukVvSHDhi#{jtejkD>&0m4CJz{AZI&MlbF zVU{kAz19(A4c+MFZFak$Hi65jyk48j^Ntu~nH8nMv zrQ;uD?CPsSMlT}%szb#<9g5MFsOxoD-d+#e>=yg_72fOulMy5^DFULSD;(1|!U$%{ zJo%VA*7@J=xMIlre*N^k9DV_v=K!Ag?I)Y`p$m=%iA9HAHK&(2$6S6aP?O2xB{1{e4$Qolo(~ zD-XCHfy_~U8A6gtJ8L2ik|@gPr*{{NzbN+(z|c($TwK3L^EBUH6)>XB!(5B9n`3)UdeZ zlX5VU{cSHpmAOjS^+ezD`no?aY{1H4r2civcyvhnDyN2c(GFYjj(#B8$FtpOSP-aD z^gUH^AEJ5gaIwJiS0~X>F2ceb{#k^8fufK-xdg_Q_r}q_Y~BTW;L7HRJ``HIeEler zIPCP4q!nK-`CgRj{bG7S#QYr48$u5v47SR#Kz&Nz8Ace4JC+ND4t^}(i8DO`qop}$ z)Em%&^rrMzBm%-q+mXK4%B*!$`%*@`QmlTYAr?uzG;64=(_SaSj1U-X22x4*${gHQt@|F2|T^HQfj zoc|AdZygrp+pl{YAc80Y(j_8-fFK|tDWG)2(A^zU(gFg~-2wtqL&wkqN_Xeb(jC(I zUZcO?dY)(Rwbx$HvDcsP{NZuvp)%aub6?*$&(HY~KYcoHxj$RJm7DHK=fkbZiq=5t zUy8AsE995OPO~@NrPmXj%AZU)iDeIMNYC-cyg1#XNu2|sH9-{hDh&djpA3}Nsqg`$ zA{Ue2ZkwF^EWYBR)(N&Xn`$z_LO9_aD|$2!=p&GGKbAVKc{YpLD}qE0R)-oO&k5P7 zKPUI`f6!vxa2@J2|9Y|w~(d(l+u^aAbr?fWewQIeSDr>itK5#Q*PXv5eo)%2&T_JBJraJ`?J zj!r!F-qcV^+)(4Mw-)c|-yqLR1K(sgFDv$1E~Pu^w`ZnE&8&DO@kpLttFeO3(G+Uk zFbqpN^>tof2O&{%@`sIOsf+WShSeE((z>&%D2Yq#E!Eh~SP3WeJbqlSyrz&XJr6jOVCV9!DkP1Pau;hCP=3ZMts4-2oiUP-3`13b9{6$e zs)Bx)RB_>b|!ADfVfRePpy1sODNOd?f~8%AQkIwhByA zNPfX?`(OM5Q4xCNKBl8JiCrHb1CQ<#gkCu(Q~)^4qG$=@zce^*eg{4X$O_fH<-Vq_ zWbM{M&8(wGEon{uCAv+@CpMjEDs>+ym5S5?>Jg4AL+Prln=@rJa(BLR+U`epR^=&I zGS=8{1;^0G>MsYc#j|g*=4-&j4Lt!!mW)1@sU=^Pwb*`5?+dq+H0q^1q7_s}RF5Cu z2*U4f0=>SX>iehqHtWNtgj^P%U3$Gw?U0eWyFoKVl4h3&Loy%02ZY)9ND^#%RbU*i zh16$}az*HLp0ax(LMl2Xwn(!!>u90Q*-o}Z4;8j^-+atH%{!f5nXzf#P~xTY{yAVL zK=Ra3nHr|VV9L*hvZa}FS>#}95Fn=)L$&(*Lc0F0$Dn}QB<}4c%+#HWjv@YaAT}kb zAi{#5gFqR@-!I3aVK`j!&6+0X)2DBloa4hPD=T)M{A67p6w>N~kaz=~d_f*c<5LMH z1_nik;K*p0W%T9d)>0-HONqm*m`#s_tF$Qbk+QrQoR^2(tV@;!n~MDH<0mYFOkRlk z)zzGn2P%<~$AT_tv6dxDmXlxVD&mf2rbZ@fqRl38%0mVw-$z?0{0db^9!0AI&Qoyq zF|&SRj-n(kv!^b^i|DkUc_%$#xno9P8IgiBK^bm>@{pR-L_&xDKZHi0J4qJ-%91B8 zq5$lKELgCy(ZOFQFBnF_u`J^MJNWJGTbaK(cF3>xUzy6mX6R2*3x0jB2R5RAzvTZV z(5}7EEvCj6Ahs3gWXRu@eIlclv{!(kngPyJ*UWV-HfYOWUH-<5N zv-4k7UV}R)X~{VwU)ij*fN|j=?U~qTn1vA$diGRJp%*-0i8IV zcq{`tP(`^*Baq8hP=+5o?*i%x|1m93%~Tf^uUn}7<)#Ym7+}ud7|*P~cM64^tY(D$ zO<8bx^czn@uTp$N_yL9Y{S7{U&lDc8^A+8dz2zh9mnLWUdQFG?)xV+o(w+M%usU8s zg``Qvqo)oQ z9FpOORgwL<>7rl%es`0=U(y=t36L+3+BmceA)Z5B@=AIxKE9DlfMk+FJK6Wz->#G> zgz>e8BK{ZZqX$PewzdP=3{flUR7D%hv78%4@4E2E>zr8uMJ6}y=`*6B&`>P2l`qN3 z1%*MGmiY?V4*=9GU50x6WTPnll_`#J7_qMm-x&&+Rw|x%YrH>~9Bo2e;gsetrX<UtCzP#2+=#Mv%Z7EG7qB_#Y!Phiwl)XY*x1PaqJB70sm-6SevS%4emz!_opg%&Sj(-i z^PwKKI(vfMiM%983BpojCWq4u&nt`ZhvuV&$l4oH9lqW?!#AS>+qWBrYI4inD6*)2 zk8osY{8DXK4A@}6RbC6#V4=(m+)1YnDlzDgXV9!rG+aYB|1I7+*d7u&pL_^*@Nchy z2gX^xEtCxML%u=7!;Gui{n-d&*5jNkrlsnwI-0770f=5CFqtfTp;UNAT4vPKNU$-M zVHd}EM*5ij*&QIoUV&87G8zz-RnTbwFd>-!{KqU@j;kM$gvP0H zTTZvthi6CIKDvmyJNJp9M`xel$v()nNev!wEWe%QbC<3Az&u=FZuX&~@Q5>4kK3*J z<>3X;6jIAU7C|eU{ue2XO`8R?O(y;Qr?VX<|B?~4~ zv}rv6$>cJZS5)8l@#=Pefx0q-M)8}NO4Fe`z$&F!Gz6$69D*Po2=~%v22tWI!mz1Z4{Kn;JU<|vZ zfB4cXt0_XT#af*@HSA927Wtb7Q-OT+vqYnu=;<#gj?OHszHCmkZn(scW7JYysJ2@X zPAE0B1@3}JbQ*+w_4OwpNJh3=To7-9gu_Z_r8n7313Hkx-8O+v>K1YQ8q>pP7vxeIQ&R+Gg7Aj`L0 zOv)NjhyJ1)%AsB3Mg*8+f(a~!!+f!axN)$3@fiAC$6M6;`)9BX=v1s_`J(RwGR4+K z=t$mxxGUUg)*THf2%j^`aD6`AKHtH{!^_O_JlRG&R0d1byvw0Xb!A{d1X~qBREhhg zFSs2yWf#Pb9fWJH=lEio)cxY)<4-Q7|2(5AkUAk2$z%lE-pJXAjgy^FQT=V()m?04 z(XOm3Z3!`wDAeRI96`@TFq|E%Wt84B1eP%2x#>@HO8;%ym#7JMfBQb`aMA3ILaRa$ z73EL&nZtfn7Kze1{KgQ=pcM?5kNOuU=ul|6q60n<;j|65Nc5-ai!i8{kb>(Y_q&}@ zue{3TVxBl#Nxkd2d&9Ud1+oN=KaLJQ^g4ogT9xDF<^aV0KM_Y+Q6awN4ko>B+Q#32 zbw8^%S?af7AU&H1S)Ed*SkB_7GHD0*_rI(g&O|%foG$7UOSlC>;4_g&9D#>&z9O9cpeuoc4QBSV>hAXr9)p5(KEQ9q+ z@|Oq>>zNaF6o8Nn04X!yhniShEHTj^c>SgLqF~Z4k#wM=5*Un*t@NDr;5nbN+uI2$ zpEe1n8w0pM<%Q#&*|szveg#HGWK;2JOBdO94Q)EUiQ?9ERw}(R*V$DIxLpDs^Y~g8IY)48BWU zTF`+uBN%2rs=HQds8Vjjj6ooF_s%op^CL?-n`iJhOn+&JDNvA8AJRt+jrGc6d$#vv^Zs_{~?(MPl@H<%n<(0vaqO-5>9#BV{I&~V+Y(|b9lf22&eHanGb&Fxx> z|4|7l9BHT-9<6A5d-=FHhG0EFh~Em?Cv~<`gYrsLq$cH&cnf($Dzwc+pMA{>%5y1X zlb_}Q)06va)LvZ5%F3WUr&d#5t83{Rx(C+MQ2pp{|Dy0}sh6-y6}yuft%}gwoDV-q612OQVTXtOg&$P`((QtwgqQ zn0efvnfz}Dei%OZs2_v_cG=U;yMtEx^4Rm(&@y7=W$GjcN3Tn{Jg#Ut{0b1=gSCG4*a*pxPu{|6eO|E)oZre;v9#sKIbIUhLOGCQlB$N$ee{XE$E zpEpgvzZ({R6jcAC4Ik7lx&Ev6s-o>#)gqW#J5f#ro56d^HLo{?l=UfMVx+SC>Mn zIDn~!0jkrgLO_DG-87d2!1HplUC|z_4UzQmALswQ$_cymdwAiCvDmgtd3(17n%N#j zQ;B@6@I-(A8lh{Vs4C4Bk9NN&^;cCHw!53#-1>T!Os3}0GpW5+lA<(?iPGP0k|SY| zw0ZD+t{PCg&mFhG8s_$$1EFLD>>#(fyYjVv&^3nJYB|HS2MG+k7-15@tPBg$-58y(K~;0`%~A3K?J9toCC|CbN?< zGc(hfi~z$zxy6jeR@^V3`fX_;LIxy;kZ?c^fsh!VseTyS7U6=5hNi!qKT-8z4`+X| zHv}-ee&!W542&Az06H>ew`SVIFOk&ZITp>fMbh6dob6uf%;|jc_04q#4uBt@=bPph z=WaMTVSPC}rf}Q+@d&6?=0~E9`HT)`Poe4mCnOxCCDexkiP7>?4220o`CA~6D_fDn!AwYZD&1y5M zwG$?l1VqB?C&yWuwG|(#6=?7tMK8g*bZ-@ERR>y4*QAeJmpM`B$Z2R2e&l}__)+hj zVt+FZ3I1mKcCjr$e~)*_*U^{t=MK>54oI$KLbcRwH-lgynZcx|730_ z-I6T7kPhUUR$L9-B%iec;OAIhA?Jl39$np~kLwodz?nIVCTNS#jSt^!wm&+e`^>o) z#MJ-uQmM*$1OLu_!jB+hS8&XVx4%-`^8|7P*}4b-LpP|->u+L>tySh&t{CWM(!6C)czmxHq#;GZ=%zLQXN2A^mfDq7K= z?5K+TRG{zo{@ggowLJqz1DsR2jWL+341itT3VTBSWA^$6^z2<-clg;`qzvDvOgTB|b| zG_j28p}m@1x$l_``fq5|+oM||9N%nISe7_WbGc*8DZ^O6F3g8fvsQn2l^LLOUa%PV z=4Zjj>Ry+cH1WECJnmFJ_pDp7e?F=Q7s!s2V11%2o35RP$T$|7RF5%xy-UTY&3*xm z=T&Ea#3)aA^QK4yO<0`S#?CCOv23ZL7@H=_bQN|C9*wF+_7!izt&f6SLJ{3npjOY@ z0~7dpgoBW*q^R&hNk&^ZrB9yAbs55SWp7_Fmcfr5g__EG{%6bG`}amF;wYbMu&hHB z3%xBTopSapxhsG?NX<+|<8%iD&=E4c9ZKB|PcfIS(+oOr=yYg^0=&HZ_D}NlElQpeGIB)X20lg@I=dDy@Ii8-+%a+oovHh$E)>e=oD#-S}&oexgXb;=>6Ou4~1+n z8p@FK%T4&}rs=k4^{BU>s9VE9{Pbv^gU8w8bMM2)xtw#JFU^R0Q~ASO5w6Pz?P;yl ztIluKTfe;Kom)#Fp^-^>`<#B`?~VyMRAiED**7fFJ!IkJ=FU(!=HqnPw00M76k{_o zCo6cWI2ZE9{Vue#$Yi#jtj2LIo!p9O+Gb~46T-MXRhx}P^p?*VliU3&^3YKND~D(r zZYM%9IKywQ^yr1!({(tAunK6nI#Y*KvSHMb!+9DjOPL0_vjLGiq-B2zu*XHP>VTJ| zy{M9-+FoNklsQrlRxRSldCgA5cN#=AihK{?2x2> zFqJYP=IkR82p^zp>WenTYOLIu8W_En4L*+bXIAeg1up9&luUOaXH2rIG^n>mZEV_5 zOj{Vm!ugKp8?d3+Yg_I?0@Bup-)>Gf6g5;`UYobdZ6DH-EMETJP(uGKdXLlT z>dB1xG`ewb%20M4Art7vjAS1o7gAtelcV+7_h8w=Lc1?k$o~NtjY4hgSnYMFMnZlq zcVzhaqTHY+=^YwUU=j$PAAi5p>wD{Te?IJm`fw=`1#ox@LK^7kbZ8+N)Guj&@OfVQ z6w9pKK--EKYNCQF|3q$rdbeuZ=hn^a$b@!gMIOM3i=XG#Ym~^pTXbb*I@)Ga+`_LI zRjpcSPvgqP9$Y8`_}d5THw{faOF(wX`fEWKtJs-XYFD;buM|;H4O7{3X5UB=2ncXM zU@OP7)@gB%p)OS~2Cwvg8THJs>)mw}hgoB}yJzOR*E*UJ`6}Mys0d47*1yBnGtr_N z^2e(q_X^po4isv^{haT`es%CKtg@vNWs)M)59?lbF=98OYV`nSonG=W0>ngFCck51K<(Agzj^y>c3TW~V)tQ-k9nWvgIAeXvceEAspazwcHxR@f-@$G5 z^aJZsokJ+zYJPz~wS(D6qlc#_&t2EAafBCk`_X2{MWsbdz}6dp*A=G=+@rvGiJ_FQ zmRZW*6+<7i)D^3<+`Xn!Y7k*YzdQFshHPv@TPNGb#8Kie@rrkzNhv)*}0{oQ#(Y3k^Ahy$%D=WDJtp*J&bzK4`ujLH0&SkyCn zAK^K3$`k@#7TQT#6x-xlyR&fg8NPa#07Fne!=K3uJ{}-^6FsNnc1ykQ*YA$+iTDgS z`^43d^)+b+?_cYor@#LU9sOx*1F{YL>jkd>fBff)f?t#VpMNv>;rxVe$lns!???SV z(`MS*It=-zVDAm;ms`ngb9d{0^iKv@A-e`H*%%L23yK16;~vOqDkx?aak+Q_y76|ys59i74AFG zO#-{JLjdbG)zFweNL%?;+UJRmabz+}9HdkvBP(01J{h~twvPb!zCB_-Hhf@MZ0Khzsr^WH4f=({uZ2%6?Xx~WT4LV+W zJs=*<5D-Mb_!zkuWd?HcH-G~S0)R~6PYpXFsL*(Ld2N?Bi9VsUX>iCv6`z0d_g@&! zB{*LCeT?CNg?{?Cm~bGNxrHN_4(QnF+NoM6X4B$}Hcn#_xUGbDUl|b+=P|T-#2e8k z)-OYhsf>ER5cRGNZRqzEDHR|HPdjLKkB&wqv_d5lSdd8)6t@%W&lzVt&Mn;;)Q-t{ zWpm}J-5Hj0Q<9=$f1#o$K!whshSd z@o+moR6FD^dCEDS=@3L1)_mvQ&^NK$1j+=5v+X|yewabbXhdknCnq=bnKNM$_W)?t zy_?4wPR*hX7y<6xU!|wHkkT`xvpcT&DN)ecVCGO;`fE;36%Bm54cZ?+Fl%%yg7h48&6A(gSY@E5M^lJT6_!1mJ!Wy>2#(?`3UcoUMy_7aSZ-2fm`rQCpdv{2hWOeFYYa&mGa zT?Zn^gzAUl*8 zZK+8n@kzx5d86KId=W*WRG<=ofkzM3Nt7GVYbife`3#6^`aOv-LSCa_x;V-9fKu`x z0^Plz)F3n`pp*Sk?5Nx=Kj5q#yi#rfQE2@zbB>eq^Soo1je^QyyHD0jomKCbFc`IK z3~Z|>Vb(>l#-M>Rn>;%smY)X(`=I$Ie#ItXFyC(d96@zZQq{dh19qrulOjEKU_KcS zX1Bu?kKE4>h0Y(4l`kcPz6-j4E?&wdrFps&2rbpjl00C3zN0QT*$R@`$0YvcVo%gs z^UqjM-<%Hv0c99qsZS=URT_2Dace5z!PaDkUAA<4D!;a3xa2L15va0IYCT2H*fr3- zF+-n7koZd_v9)}?d3(gZ<0UDu-Se^Cf4|h3$@0L}ekXJWQ5W)*#P+;mtIF>9LFlzR z=|~=AEc?dRB&-QAz4Gg7?W?hJAwfu&d^Wp`ME6!USVvO=kId>|E-M0NnHTFioSmU~C#zV(*dL@C>F$3M zTOCikoh6wVTz`4iNR{oM$T*k>!~N!CH`b26aE4cA_hjHt0(sdl!h`54N1&e8UC zo?nw-lg;|#b39R8yiVosp^90=LB?H?cN`(v2Q<3ChL%2F!djzNR=or^fh;LJ9=Dv1N$=jf zr=a>tFjffqsH@`PAbFaB3i^L}?zq-@s= zsw@jf1?pzwtDzm{OP4{;1R*Xv?3ReRBKcEufbjCz86?_ltBQs9+a=}y1} zefUA{pA+B_GIL|#-k#%vpGvg_HZmelE6;J4-$pbw(k6?nK=ep+bFVq_Ybr`zYZyfl z;1y5+Cd5$cO`f4EhH3)Re3lDIAjMc0!db1k4KQ8IGwS5676NwP)P4mnburtw%N~Mw z`C^PZ;82l~^1L_!e!?Sl`;8gAhU}M#as-@SbAk?$Ul6oP7yi%Y6HXU@5QpNMQjvcZH?$hQyz% zu!oGFqE}jftqH8ghtHW>gLhkb@!a2!Mw|nYP|$#)nf!` zZVKfRzwi=3_Yi|<`P)CRDRz=gaA?)aL~gja!Q5je*0NyXGW@)KK0@ez5~9St7UM~M z!j9BM^|1_mn8%oa0MeuCU0cde%x3XThWW$(B6@|zv^P@bskO`dh1*p+6~}WhRDURI zM;KKKGMb@D40OzbAwln#y7EE|KN*I7le zP69J3Z44_d8(JDLyQ6^W9g0nZefOR9$rVoVIx$knUDiBL&~G0_`mlv0sgg zw4jor=iU(!*mpT`z9Rg}jTVX;peb3I$|lYlb@r5i8sJ*`ApmE*)UZq|@+Gh-w1C-U ziC&91?7?6_4*~8oXs>Q?C@GhB1QoyBx!y2M27s(i<*?&QiRst@sYjLlDNdngEi_Iz z>Z5tyXn|~@+iNz)YjvbxBh$Sdf2Sd=)G%XG>reUbGW~7uXb-00$rM}MAHOAwE!R2! z)2uzkRg3BTB2Gsq>YCE zJH*8y))6B$4iH>6H@AMG(4k=xe+CvVU9k3ox}1W090zi#34Paf6b@Z1)igXOoHXhF z@|smpP*CwrB(glvA|G;%t2@Qm5Q^30@)V~KuUok3D)*y^&-+#pV6x?=*7_4%I9YpF zh81b)A(!Mu0ns3L=nfF;5vJ<&|ElwgRGRg;)Jg3nAIg?dSO7q2VZgx{F3x4v7tZ^r zBPjM*j_Z?ZnG5WXOXoQff-Qzr^#xK@qv1SvFrBP;VKxfQbWtSdb$KP|eP1;R=Okt( zSKswyZr%>n8~BO9r4V=j-ID!o(=eaP%SW2c_A>ScKzm6kh**u9qq1en7lvG1Ly>oU z^m`Ca6J3sjBO2@}WB~qP@97qG{U&f-vETd6ZbSZm#Q)Dtk|}6RQrDW;eSPaM$TBDD;1)1lU# z!2_P6zXM8FqQHdUIDyYXLB*t)4`?Gpg7T914%`|kd^d!c!1B)3Y3-xsCaKpIX)Iooooe$r&6nFE&zJ8~7L6=(xx;E77 z>7==>g}gWwU@jgo|7my!$8M$97r!S_r(+)i6fr{*xWcyrT9L`23%HNmQpUbLvR>>7 z;+S!}4SLP05Az_UyP;4isN~OUJeQLUFBNJU86NQ)+ zGXxT=&o5bqC#9|b;F!p3I`TcbBPtw7 zcio|ep!|@{6gLD76iCY=e7&!KauPi#kv>wStSptMo*k;Cmn^d;-=0Vv1oEeUzy5k8 zADvP(_lF6my}5EbF`CBs)4IWhY6-(udO+P#ObMFN!;*MXDUYr4ci!=qlQ037>;o6E zMo#W*5w+U*xasj*t@6|1hP5&^B$wseh;>r!}zV= zj-_1AfniR)S}mqw*U74aR{kf);z5{%@yVHzV5OC2vHhMP8v{7hK7&+FFo0792@?{) z?j;iAWHO5n6mf#UGdEmiohcsA?Zgh;elKksKNxgGq5?6E{7wv;xsn^F8{v#cZHY_a z05qm6f+>|U{!CzJCP%%Ju{HSW5fEn!LHPM0R;E{uXKo?C2*8Ou;Q!HLqV(slzE-`f zsn>|}7w-V}22!1>jb{l0!;8%DU>4vMkYCbEZQ$>WJ`w=)sRNv^fUIyAB#az>kB4MU zmYaseAl~;T@G8JPk>0uI-=|)e$xcnG-&Zcuy6=N_zj=3F2n4)q02ic&aKpU1=^gh zl|gMvtUG@I-iTk}w2RJpXfh$shj5pxv-{dqc3IsfvJHGwQ&atL5svQE$~Uk}PM2<# z!!No`e2O~`K#($?UFNTu|1rzQhj(z@>kYRnH{O@28cUKH>j`3TT9$Hw!`~{`kd$Lt z0WUx5n1#*c`wdd=qc|5$@PwXMo=`>wqv;u+9`y>H<3e7uiC8A>w1!)E(b0csds2j9 z=mi21uI|RDC7AIfYv~n>xTbEyT}(Ho6)bCeHPdfLu5E5b2>0bkMm;OQ(CWP4a@vNz zkdayJJAOt734FlwXXqri{-^WlOW}nG@O9*{_CO`1pT2S0TxB+Bp9|!1uCqDnCR*Iv zm!I$hs8f+ZZc+3j#z6oObg3pc#B*74)t}B*BIoWZ zr5~_~*>j3I%Z$7H8sA}1J)X;P0c~%>ZqX4UGE*G77TfLtI-XjPVgSo3noY*bVTr7vBHL4=;-!e96Q88}A% zPm7%dC0%Jz$nr_}oXSA@yMCAi!J)}SDfQLa#!k*=gG3zT0+Z!L&?ClRpq7=3p%%?w z)tCz*IScnHIF)#;REmm*N%|?F%s2y}%yyqc#3APRELLs>2kmairI1z64+?hj@=H3Bau2s4zlUmKP~B`-kr+4*~aL`uW`*P=xpYi197;NC!$ zt~-M^k1}euTE&LyOE zstLG4__>_cb2^*L;;l1yoR3gU2Zbx|(N1`TJ_tRFcdl5rUf)flqFYtoE%2^#KTg5e zbZ0h|4<0L+-HmZ6G~iZyvc&)vpz5e-U|}BsJQN!W>0);b{DO)4E3G{x6M0azJ%5Gp zweZlF0S+Q~r7I$ZCjY19NI;J?CJ;;lE%E_#j#(?rZ1nR$RGIDgL<`Yvw1c+BmLO)Q z8Mm0%v_x)Cp8SkB`uz4m*y=Gx4fZF!cY$wT$RH^Tp62;Iv%nNXxb&KrN48i;K!els zCWnu=4H*2DTTVWM9)sGPz@Ybw>V5y21`7QNy~zr9ZnLq%f@RyyqK1P(lSnbTNpWvj zDc}9|NiBlkZ~KXhJMtC@a6DXR>sBvDDd4tm2_X?c&LoRvy_H+9g|j9;mr(qQD|mUH zIPP(}L{K;R4c(|O(TkW3N;S7C=XN;45&!avl|ii>1Q!>)F}CDOSMDR{Jna)3nGZNH zMslH|#L zya{z{I%89*cYFR!=JZy>WrY|-D)v<7f7_XmIKym9%*U)geE6U4IDGC=p zFkL!Kixcwpg4fyyVmxS#Lm^&b+gCFmNg2wN@W(7HtRl(NY>>3FLe%ie3pAZXR-kMR zW(5tj>VPqo%*-zRY7kn)!xXGGdke2g9TDf!-Fp?~g5hIn6kwGH! zC^8ml%VkYJt+?G5jcS$sdg-|sftL2Ji?B7y3sC>5%wzMtrDDDcI&z3F z5hZ+e>vVoQMTmlRcYj~Lt?^f+z6qcUDpgvF#{xaW2lttp+STl}k;6vPYRC5K zr)>&mIJ3W+rcQygv515F_=}c4*z-K0B40!Pz&SbNuf0jN#T>30hU!o~uCCN$+zFFx z5iP$~c27F=>n78w9j#}xWTeOgA3eWSDQN{bErrB=aZLPt=bLt0&tBaFW}krNo>`w% zZ3qw-5rFyNHTw&d-4{r`(AD|nA&$5e`SSD6HhmXoD&_uKsJ2CJTk&Dt?|5_Y^^OO z>QtEmj`ludn@m+P4}S9nm^Y-Z52-FqeD=fFHr3M)3}(K{&NrHj)$=-TPGPSkLds3# zBvV*WftcUhHIx)QW4W&`kF`-yi4s$Cv>R^T!6g0M76z=Z&#bA`k1X!qy(>G!&@($T zLteJhE$7u{f3zlxWH~}1_mC2J&~uHLhvfHYd-Jmz^}d;Jd^fkc`U!+Un*0(Ein_kY zXh86wTShN*_1?GFUU_kSjl9`$V6Xr>U)q)`$3uPw)pNYqg6lh5+)hIEw@^p! zzi|_@`9AndQ$w>`S|XJX-m-~nH*qgy)PNL z{Ig+`*iy>w*KQ{dje6sA!`?}>yUMcclTHLi{-ACvFntyW)1Ug%*Ba6Uri2H~x1HfA zcJejgCr<_Py{SZkzc-`f79aP)O(vk3iAtbahz1bqCjSR*z#YYGz0G64H42f6!Yl~D zZFpm%Ld&RG^Q}`p0{~?)C=Rv@3aOg>v4r+;s1=XC=FEP*S${rTX;#}_Q441KbV@ln zSqWUO+YG{{V-+R(Z9lu7D-_!<6n)4R!)22yOaf5b?~e-HKv{BPbK-9@UX1;I@uk72 zYNDrs!P8dL9S(zCo$dr)5|D%FDLn8^}rPqI}6CNRJEeF%E(mF4E;34ug8_<>L}PY|M0r zGRy5XD%eIUGzl-jl3a2_={PtdBlHZ`c!fhr1Ds|pp?)nNZ{8lRc>w zIZ7)yd#{~TtJvlya?uIQXSwcxNX{O!My*Qq*Df+6bT2fSD>s-#0VIn40EJqP-x56d zCNW6NJ=hxi&2=p`>>4Sd@)GY5RV@P_XXYnf!PoBR1&$V@$_skU?bs&!g_IwoqVtof zW@cJ!cfWh(Y&6ffyTTk^@Punf$zRlp8=Vl&@}>W+E0o`4kZ zlgB~`$MDfPaGwuXp_ulj@ZJL}jFBqXDcYSo&}+dXZNw|ny_Y;1OaVrgSB>#MXuN%V zM)nEA7Nil?0Pb^(&0@Ghlbe*+5fk*vBNCW_c*m_bJ%x@c4Jgmdn_47V?_1Pc5KXh_ zb&r>*G&sUL3)xB!h{h@`)iEioILAVsmPz-Hjen~SCPJ)yb#v@6PVW}E74^Iae2mh) zz4tm!;%=M*uiOFiYmaTpY69=>w(G4j0fzVl(i)nZ4FyAmt6<=jr9X)&>mEoe2< z@45vN2u#Sggm=8~;lr~TuD5$c|AoR(91k*tf!EK+DzEwr6o5oKqye-e-0sWeXCrXC z73tO4hE$AvvF~r{UrF@uH0d@C0L7}k$t-!2=d}eIMAnI)p3E)gdPe34l2fOksQ>3H+Z_G<+m?xnsc|~{fK;p)F2G{$ zJB2xx55eVz>4PVsm;*cLS??Kid_mQ$wHE;6@7;q5RtH>n5M1uCDeB-tY1xdb)lhg@ z^tIJcts^pXcZ^x+JMD8-ZV@0O4OXwT-ca}mhRl=_u?%J;2ECC)mDVE4E3tM@IG?IW z5ww!5ERyqtTQWh)q&lihUq0p&kD>GR4Xe!<6^!aVKtJ4n`uIt)( zJY_A)+{+KhIdl9B&u8JT%BV&m;5_!tKVxUdbG;HoTt_)#i2wsG|MirQr!-+l&+*l3pwa)od)GId$E-Tl&Ip?dvVr3t@hNKg-p>k#Ai z2bpv}?%=pPp5<7}yc5f}P+>J0h=FjXyD3WbT03~9A@^&NKs-oLI3tbT`26MGdonT8 zr*|(GJ8-wAM(+I-<4XboMqW*5SZma8X0O}> zgbBK0Er{1oCKLyS69+Or+c=PeFpNCQheSlYQg1(rn2h<46^U4$Ql^6>2rv^0fGRO8 zs(lcuRa!ji4&l*qwprWH%(>x19T`EVTHtxHS4-?<|30|nvL$0zM6^iLM=BwyVYgs2 z2?#K3XA$^ve%Z!F3nT(2@SdSXD=e-+wakdlN2GK~-};_Jsx5Mfp5)ZJUh}?CJhp_nb}%}>QbWPObFn)? z0mk}#EOuCsZy|Gt!WqOrW-X%KMkltKEz#6u^<^G%{zYLsNg!njZlzIhD}4gv?95x| zKYQU1zl{}TOCwh7?rf>qA12P%!m8ZZ9;&j|QlI$0eB#3PAqz(iN;6W6&!p<@5t%!1 zpjpLP`?w&b@bOl*FF&R-q=t`(jSkA8m^)epOm-lqLIa{5o5VpmLIHN-@4~jHM}(jO zR}Pzmn`HV%s0opDu2D2Ht;0;&(7C1Aw)+ir7|#6Fr>ymZ! z6kg2M7zPou8-g5k?fT&2bKMU><^tqP??}1q@A{V?M@!zWUSrhwc6X9r+^Y~x*y)P$ zTw4?(wQ$w9_>Am|(EF!4yY;&eVn6*H;gCcq>&*oMSA~5(Kr#z-343qYH6Q4B@tNwT zh%8938;J-%n5mxiY%o)o^NtzJE0pxa1Lq}<3dkPd8mC%;qd*s1RkUb`w6eY(YXhI? zb5%i>lBYVmYwmSqj&dj{&ml2Wie-uh({4>g(rVFmEv?+D|m ztXYmUQ2&@!B|3Tlusv%pJ5K5SOlQmqr5*U#t(27Nh{XN=r$QC>2`uZBHz5jxK(!V` zA8R%#D|!P4;(kw#JZGT`0^Z?`RQ~>4ehRpDEB>%wUbK`?Bn#R4QiM4~p0b&J!)07+ zZlitjL>~2!Y*WFCq~L84F7lZJiGsG@C*hC>c^v{CrsMpLPjSUqj`0~)J~Dz`^ws4< zV8c;~k@zlC3ISPRP-sL55x1}*=XAXt+SL`BB8g-c)&jwC z>tqcnP-{OIizyx70lYS|uGk)=QmnHklFkw5Zg9`2t0+_M;4=*{!vdUpt&3lM{7^)h z!NpXK1FnF;K*L>yPAXo;uDG4Og|@cwYamuo7^1d$%xl-#Q=(`$W08fXO}X=HM43dd z&c7}@Tt7!Pq7mA3pD0%6ivd$7Gj*HN_jzT4;0pUtl~Dxx%^@-U@#=2UKF zdp!Ewvjj?^J5x2urO=mr?njSI^&s5c(a+2H43Z6uJ%Ef1X$b=Sn9Mlh&L#gSsfNp} zR%!`#F5>6fX?4V51#dedSAmls%&N#n)XTyx8Ge`!J@`7zS7kbm7Q0N5WFUg{g&R&L z>YH9Y0n<-HZYu_*LKUwjqrzliYdGW~pP{&Z=P=VqMxrB+7);B#089jkW~+hVUm~)^ zoF_K2lXt6Z9b_QH^O&+E)=$2o6B-{io~nnRgR%w}7~aC@V)9H@B&NWiqRg_rKMi_2 z4jaLjz-|deFy(fDY=ym+=74p|HlK*{AdPZSx!~)IUpegDkRb{LS>!9CCv8nDxR_%C zYaLVLS2kukYZt|id=IIEPgAL$!0cXVa&eDc*zA#&o0B_(=wL{BCfE;u{BLT^gE6cBYVMDGssaUG261(qg+OlL=+8@hkndGH zvF^)0OEp)FQQltEfYTp+9R9t}wP#kJ{ij6qk=#@-rG*$t#O|2r?pr8JB>>IV*N=^U)e_FskhWrIzKCEq>3{6-Lap8+A&urRLVHP?h8c19)dW zbEL&fbUot97&b%8EBSX$n^m?amJ8N&@kU>sZBQAg6LDt}vD*_OV5TI8<0q3vx8!U^8AenI-3`$0633pJoEi0B_1MWjV1`{v4w#lD;UTo_ZZH8$U$bY&*UFJAaAkv!4hxYxO~;B^x#b~<)GPs zT?@LHC9}F#tS2aCot~Zo-vN=Cnz&B48=uX0Z9^_5^%_zD@f5B-+A489wqolc?vRe5 zAfQvs`MPynw0p=go8ym?-3)Zj{iP1{4h*79bAu*V$|USLrdh;nR^LirXQDYTxV8(i z95n~84UO0tkYHW!3L)nNFl|$CW4%xYa@z(EAB59T`EfV{*F;;1YEr|i*~k2M#rl@A zhMs)h4aWCwZ7@W$xo2?sdTs(G}kWPhv z4kpr2Eb1{72Q_7KpZ0|eFwS&n5?fQuewdmkM8=H$>y(+=yg?8MZ59rL3a?0&Wjm==b9&d3N3J-Wx?BAX*o->k#SB>z%_UxBM%bx%yn5**-5wJ3g%w97`lH2r z8dVY;jU*21LNk&<(LzXM&vE+g|Bt!1jH}6Vk#31eNJ@8i zDk;mlHTrJ;8W zV(1eK%%p9J45umWeANX67{oa^ImaYA3sOhtDIrFjq0~GVC)?Q`o8g9H0+F(R zpc%xc^}VbZ!i=>9Rm-ltU})&d_LU z(lry^r!wlvC*WTM&hUf)KKhmN&GXrxwnTg%u;BZ%=@WvzcU3l(0L$W`Z3G7N&#Tn$ z|Kf|+K~*7p%&wDJ#h*^Dgd$~Y{#rs@$y5V~1@SsM_Hd@@<054fSAIBfEX$Ty=n;>SjGK9;5y+3Oz`bvW0`FAdoJvj zNS=O?`P{Y^zba5nv65QEFUuv#!z|w4R}<`)+}6au`QZK(u|8=7xQ@)Qn8=GTP;p5u zIMwLL%zAV6#K(;bX{xktJ9Av}$6FFXH2mR?aD|(z^G~pj2Zrz^bu%?p)zNa*8tYBU zaRiT}T&gqFL0waKt03y-yut|Np(ngtzEmI4E}yk0kK}PB$q@>}Uu9V7YRCiff`Cb9 zQ2bE>*wW-`37(xp_@eWbkB5H@8(ijtV(+eldh3C(vmE>~)b$kW0qjN58ZU|>~Z$YLZfJXa56*ZDQY&fN_P?z2{V zVx$==dcoA4CsXXD>E}ZY$K|pRu)={Bjojb98b_`;@vq zf2&-f?Y8*qT#q+i_MUi$naOyt40(;+nF2H;xM#E#=BNi186tB4V>aNBBO`+|w{mAs zJOdYN=Xc>h7r^7kYfUdYCYGr}z{+$RZXdriLF)c#?1wh9RGq_O{pX}teR0|Y{lpjY zJ#+3&_a8q%UT6<`?j$PAzS zbSewK&e1_zL$LbCuBm%PNDe=?C0%J@`~z3ja+OZK!m&oML-b#(ORqCriE?>I>J=Ti zvMF#ZF08sK92}j-NfxO=l!d&wwvCiS4i0V2pQiq94jnQ;Xl--rf^)Z_suelr(;*GBimPrQ|wLEKh zcC!1>+{nc-duFj+QY;NH;_=*?GAYpdv%Ex3kn?Utp{jsE^HjHc=kWoI6+VFh*DoA4D}b(y*Y@jcrYJ zMU4)`7JeYy@PfYUQ)#U$gQM?gR88HUP+k(}BDFH`7RA0SM(kduxG}C7GOBd7L<1CDUL8 zVJXo3hD0q_i@|f@K#?`@!w(c=rio_<8^M6|3-|+6F+eaK%(wuLUZgC?0zX)N^NeYS zKk)e}`&wUe2r7&P;px_!kYi${k~$*-p_&o#rPA^c-MaVhpB3g>(Tki^_gc4p58DF4 zU^=5X8CjAy?B&OV>`U6F@pM0VscweE9}xj^?l#uon@(=*Tz&St=wLPLeByMW@B)Ku z(~;4qCKYx`1YM{8Zr2xN3)J_%N4~dHU`k%C3*`DvtqTO%-(>?!$a#fMAH&l~P|vkA zK#Yv`b2ZS2(*5j>9ob)tABxq&dn5nTe^s2(yx*iZfl*Bt>+#0CbTS(5h}k4Cj0M$L z>uGI<3qt)P!e)MgM933>8p`cmuSRxRLLb5pPbbXcT(#s2A_H@N|F%%Y42;dBp5l2E z!_bm2#XQC&IwqbDzC&ttRTW`$e0;Mfiq_fhRHb4~dT_QaWO^&B0@fRSa+hcK_taPS z6A~mYw2-#Oa|=QUTu!Ex@J9FPKV$elCibtD+Bu!aMC=7DTGJn@);q{N3RqZJc-HKI zTKH&V9{-JE!SJrt(JvhVD47_RWk+ygj@p~G3-`e~Hdv*p=WDrFUm9y!^9Peiulao> zVqR?j7)mzk=wgyTJ{-vwn6BPubK%gVCDR%c*graGEtsxKD^1%ooQ6-%yCcH>Ml~*>qlD(GNpOYh)l_xK#{7Uqe|2qb&Mk-zZn>L?I_B%x^yDznvYDKH z)}0{iTKAHdi?((f>s~bg=nswgNG%>{Sj_hcqCnqI$MoIS9FX?*7UQJ1XTbf zaUsaB1^$3D+tl& z8R<=x62Rn*rw+Is999IX%o;|W&F4z-$a!z5Y2pRF3^WFBFD@MdpP&4V{=t}9d#@(T zoH1)px4d_`owPPfS2t!dBY5t^9tCp`Z)lWed`>NgS2|CS*o;Tl8CNBBChtkgLve{6 zGDf!{Bm-St^l+D4g@#%lz0UD)(=z?N8G3Akv1#$E0=q&=whExgssw4Ljo)8Zx})SO9_Z`noBKN1ZZCj6cB*)c*_qYW zJMjD(Q>pQYk0W+2)m6o0w(`+ll}S({Z2w8Te0ZH100t-zwqO&NWeh7;AJ5pc*Y6a;)sPz>zABgDEwOdN-jfeA-W(1dmVYl z3N#RuSPYQ`KYmPWL)UoLk&aG(f7*gi z;(st&N8dOO^Q(zNToEU1h%7pgtV3xA z1W+m0HOFZQEU)I#AHH`(nea3tFUnrD7QyX|NB@FJvhs%7jh@_bCQw zEl41U-}m^D6m_;L#OvOHewCA=~5;`E(W+$Qo@*<|np zLPaI(TB7;wN+#9>hl1T^vj6o%$QU>MXECOAzJOX z`{?+F>qBl=$EWi-=?!L+#i>T0!LJ<2cvhL|f?_-R_QGI#KC3S3h=t^LhA;S(&n-Xn z-+}?WAY>wHAyj^Z$KiJOC5}VwtR|Mj+wwsl9PxQ7<5;r6WP*-%j zB9(31HC<*fY6I6Z)lhXKI4-oqS@30Ju6T8GB?!)6gkqn>twh1G4(>sMRs` zAIKyHx{#R~dudXp?=B$rvnr4~pU0CFfu2kFM%i%;N?7We2g&mX&}iOb=p{^2{Nl(+ zsro%rHtksLyyLp5MBF=NNx3ok2S+W^ytn=s1ggMo!4Gll&oC|Pja|~~X+W#k z8dYdc$`pU9eSVN>lt}s2D2~$M)~&aFkjOR*qmC%3fmmGHN~7CGYiKQY$+{F0u^27CC_Y-Dg;?nKC1oOM357QRD^b_8 z{VwV#1n*GT1AXm; zB6y?s(brN39+2jk2uwvA9y^WW;DT*Z7GZQ!CFS!WRo|E3Q6=I5sq1@;zIYbFUDcY< z@$PkLu?WFBo{O`y9O1=}=3s?c5+bwtGut$t6;aqQ=5{>jp3@oDGOIUT_ByEMFyHmE z@;yjYef01gyXdJH5mYf>#$*-P$NOSc;AnJ^9>veW$>nxHL32+*K!8VK-VrXz)AODB ze2JI}yW#1K1k|qUQB|JZGQh^Uk>}BURPy6_x+dR-KmVPg!Uo~r$qKo?@j1ywj1bu( zvr2Wk{5fHL6|ZxRDpcyeMJ%Muj1qVfXMuQop`CToT@VOUOyqaD?qj>aJ;VW5T5d1m zt$&i6rZQIKzx9*9)D@GIJ+0du<^Ibmy9kq&&DZct!3WCXk<`Ivvvo^l10BN0;|a|y zo?DAGx;;;U+BnV0!D$V&m&4OxiOx|g9YAJJqjfZC`Rrz~V~f$^Hx`$}?>l%j0{Y6j z{GSdW5I%t} zbTHI7`W$*EsYERg5Kp-nakn>h{y?bp>4iHFq(Zp!QGt0InvoI_j|*}Qmlc%uBe^|% zZD{ggL;27vf*0+Uo1-6~x>twNGtZ3;i=aS;Aib0H40YQ3Fd8V0L95YLN8N3SDs zrZ&espOBemz#k(CSiMh*2>ALnokj@;ywy81CCoG}U-sr)p~ORh1k5>_#-2XMfP3L% zDfn=6^^C~?OHU&C+jBM#`8%>C;fs^0+Uw6kfEH70EngWLv!vKJP((woTJIGBy9xb} zyI*2EZ8thpNGCLG<54o(LnavTqN%dcH$>@iCpYHiu?lP=GFZ0o0m@e9PzFE#W`Wyq zBm-nxvD7t)+nbcb;UU5>2Qh7%4tj1peeQuqAy&D%A}^fsSGE%)0roor00a5pLL@#JPf%PJ0cT0(?cm2QC#j~FbrA6y*4i<;u7Eq{>; zzS#Nl-fH!&i&R$yOO-I|%%kYNpI1={)RdZtBS}-X^ga+7Yk~BIQn@C{oY^SWh<@*< zyANtmlbbZL0o>{beh!*f`pfbvo$#`%D1`U?vSkv}9pALgfm$M|PKc#&A9DHuAzdXDe0EP35#WE%E zpk)8-*%v1sOIpUY!6T4vBcF_r2wq=V+KkACo@#Y*vS)yI+HXD~C{E=x|-5XXx8Sfa+^HdT^ur0tjuM&uTzn$$1lYY6W zuYr~k87>b@>S<}&ZjH(u<2H(L@_6idD`CVnOzfzQH3tx-x5MikhFn-Aow^g*%RaoRooA3_ z4X<_>JEY7fmBXT6l?2MRLK%RsH3nUUb^u7LQR35dI0LMx$*gsgS$_zOL|~0P;8C|I zPklVos8k0=!lTpK%@V%VXXWw(w7I`4L*&J^mb_4Za2uoWY>YjLXT2^2LvwKQ%uTE~ z45WUY(2(M~ZfUE~z#215a&oe1vEOa$N`p;l|A)oDmZYW~{gmkWU6urYV&kMFN%+iK zN0B?k`;9`XeT*)M=es^C6FtmFND@c z)J%v!<*&7|^Lu>N#iVCtJz}==tG7~HP$v=xyjvfj_p-(H&F#45(m~(U(n+6+QWaPp zbYPP3d_QGj;>jRKF_e~qicy`oW=irY!Y3ntYPsCAwlWcKSJTl!1`ETSm`0`WHkt{I zTrt`VwKfp{mVcns#gaomy|TZ&q034+l?y7w_)bY0Mnh1eMTFvX$i?*q1q4K!rRo~H zkLOiir9~vQ(I2e6$;VW-+q`=1O1&TaU`OzN)5Sxy`m`9VOt*ize}-6MXup6l3gw%L zLUqX9Uu}8glP)v$mgIcOFAr3$n~@DU#mv5Ep27lQeM-6-8VMI{W)Su+STSy3sJfYk zz@T8XOf(p2mr_y^4Rn1&R#1<5p%wYg$1Ycgw5bzBN1ZL~iXKeTaxO-I&r0zF06Gm7vBF6?O*nZTnMT`3L`z0a| z-U3%9OPDMZj)pflH0LIv@|Qnn?BbYeaK;CU=YwD7KmEdyLe?kZyG20VHrR_#h;P_Z zHn1xe$q=9Ne|&pVHb6i@k%sFH=GUTcUK05INj5waDVKt7v)8>R2V|CRGFGMzSCWPX z*!VDUO*Ybqj#+ND_9(m}!&Uwuao?!#_{NgT00mj`}HQ0D-G zAo=|~47r(Ru!69`j(WFE6GYQm(`pzs`n@-JrfR zW9<|)wa6%%@f%79=>#L?QpfiIA=P9Q&(j^ zlAT2lEaX75=nfJa!0%G0ZUly3UkS?efM?C=Y3vf3Tp2FSzFcgB}puq)$<+@KI%fCN%et#mGa z+vTn$fBTyQkY4$76Ok3?n9ttd>5F4i++K#OVI+dg@y+$63IB5J>chIPD}Dg5ES<(w;O*J)(Hep}mHIORtI?94xglnCfZgmeJPbelwK(<;C4I zR8Z>IHIn__YUR7SSmZmfH)Yc82aU{6T~4EcIZZZmirM@wNPYe>8oZ;jI6eS+!=qtw zT5vl9_gz9HKixM9AM5&O2Ui+SWrnlgG3j@CXwY&^5Y=>5R8@6=^Y{_Yy$8Y%j!B~D zs9Z0OXa;}FUy{+SWVH+p4K4N6>9B~Tj1V~%)NAPI9QA##IoI!rmb{NmX}mI++Vb#m zuvvPke=;dBNg{)Ji_2jj8)Gy-6UNFHRRzoI3zjlI0mr0@_hog^5 zLA$Gk$XhU5^CERSyX$S*6G!LpK7@0YA=BM!$ss-1Sy3? z$USSIRPqI$4pcBOmkXhRKy$X+K`A?gKw__H9*5LrP{ou%*+&Y%VZx?$ZLcO$Y>g+h z=I2}|OROItFl3O9SOnif;S_wX#Whc3G~8rXaU5LIh?~G8Slb%!&EPs=AM8rE$^Gzn zVv8Ht*-*{*mT1t&o_!5By{6_`TTj+0UhRtz)(v#Ogjdm2>zFD($#V)Ktj2zpgXCv5 z8$%i!*in${mEcTIU+9qC#R=l^dM{+6l}W~f16K0Es)1(vsFnBj#nmEz-nZ7X9cJrG z4i<+D*KAHO`56bnA9r_m)oP2oK!4kMdvnTrP4He&FMSgA({v`b_}TpQhn1k1X;pmB&6Km8Fy@Wg9nD3aNm_tN|5Nu2pU=HKY!p zb6yq36RDs=1*+QF*An`Y|EfbBzqdo1#arf2Ia{F-r*7vx6NOLT6xC-ZO&$4}1c?_pms|)@WgYS+%6- z@hSnczSg-vo>GYsCD6jMBEnJV%tq4#Bn9SB3dx{Glq zY52U>km74_ItGy!R4i7bQKkD{0J_GqO`NPVXAp~|&hMy9T+;{pTspn|L{4XVnorj+cJ7i0 z^9a@2_?(thCWCkr)6<}-%#UUQbh@|7@wcG$RvWo*#f??fJsAjNv_aYyN?0N@POCgr zsOFqM-2Z4KUc54)pj zd?d^Wj&i@Id*1WM>knbd^oJUHqt$=2ft{+8i4pwaoy<3iQvpdx_PJpm2Yh}$)-kGP zc1we2g;ai@l)pj;q;sxSxkmO98Isz*Mq_!}J!^dk!59xQ3t9o-*T+ceY_+gW+&)q>mg+0p{G8?u|EwpQyPj|KlS|ydJ&+$#63`Z(eCj8)M zl-4*Sjzv`!LlsOC6swDE!~Ae0OOoIB1U_~}DYCS#2og-TeviE}`1%vrE1t`;Rj9)& zo_qiN`Qe>=Q4LRGcwM9uR})5^-R{Rn5huKqD&kCz_Ip#_;M1}^4xhfjT^R3E*{Toy z`c-}cY^yyggZn^(9WI_p80%Q?et{~%15BdPj&NT)GyVb{aIvhC%+Jnn4^3mY40n@I~o?1kWhy~L)!vkelB|Yj#ld+-&`$AgRY}e zI|*9>PPprOe{6b|vK&~dak(5}tq(jer7nO|$(5`#P!uavsw%%T-zNb^8E+tQ-Ik*b z;C6pGrH2P@?*kwoSvhsqQv|(AI3emPj?_$-`HSJdc&1wtt1{kdC6-JAWEIVg4bJ;u z(0>Hh`s;J!rYbU^)~g~haK&XRH<7JG`4U4^bmdT@wEPL-DRb4pQdyB)0H~N>K!g$a zYz^JzkT&xa3d$W#UocSxoy2e(>k)BU68Z)w{jB);qCSl3;z1rrk}SAw+#^DJ|IPDm z$5_ef+Vi9fc`~uxKbMbWX%(XUkR?wskKwg+G8cqE=%bm^s^S@KY*~(7ZH8F7Jg~C4 zU2`BIA&nN7Us8^-u^F;i9PotCS}nC&f>n?Fi1BF7xAeQhi*bzi4uD35Ia@j}-nq*F z>p=V#2vi^_L5zp(<5#QVlQq`*kaht;h${kF^6A1LOn{w3b;$0nwJulWhbnpr9MdMI}EqoTS_}-lr&N>`O zBIuPztKs~>xmvf|tEwD+rZkNLD3lgM)9d!&=h;w)c%uWeCwN|$%OOADk&R+FS5f)D zzNQP0x=lA&+b84;7}G%Vz4j^&c|T6GmhLrPv)0n2Q{s$9q;#JUPeZV#H@1%RfPqUu zW={+DXwkLewjgXJH_>oG=9w8nwHL33#`*6 zbbpI<;FoZUl-TUvSI3k$o$ETM)wxzxRkQfXYbJ+}dW|7!xFU>lzRyoGT;l)Q`jVFC zlA|JY09Sb)?+wN`hnmM?&+`!}f+@P`n5J^;AIqZjoqXeaqfC9)9dLiO}oXf1uSn)r$^YC0{9(dJUYhEw){-y1oi zU^9yt>?h`AO>YTB$Bn>Nn$hI!6VSy}L-nrD&j#oee4`dxv$c63i3wLV`XTB@uDk>Q z(hp^wmQQcTmjrekuwhF{w|kI z=8zOgBsoCT3kN18(Sdi}6qHT0_&kzJyi;K|E5yK3Y`5DLav%IMmT3u(qV3cD!I|{r zf*^cYWX*UJ6n2r@nNO6QD!ZK88oijRE*${2+d`xP(5n{wK08qbCn5US-qLQYGUQNv z;BEFO)vuM$hdYKg)&jEPLXFPdr;pPg!kLvC*EuFtZfMEVR{ki%$9CuH0*?AOmvsoc zBrRIngXG5~YrZA0YiFH!Q>`)V4U{*X)w-@fzPiTDsf?8%ZD&|3Y16+E0)b&+fSW9? zuV-^S0M=1|p0*EWGrkzaP$-_5?|tygpsg)noO9=`UYJaGIAQQQKB#5;3)h#h0riTh z{7z%j&-psx1c`=k;4`zs_Y5XLaz-xS^lW7_B4r{Cdv6APmvYwWq;+BKZG0oiP= zgN!bN3o|)zo>h&S<~YspB$#>yY?;DQ-H`G_%3VyEfkVW5`Sb+;2^F-X8}H4L?BSo! zFiWVEH@Io#Rr)6M{Dbl5*~SR7z%OXVW>#wiP_=K?zl*?C#bf#(+r3KW4t9F#B(M_D zV23l*(~PhW7+FO)oge6y8%>EZFuqHnCq@<-YxG^jgv8nqy)>oZX6QNyHQ^f(I+yeX z8NI8_xV52l;eEbjp2^awUdfaO&u$^K$Baj`R6%RvoSH{QnXb5O#)Hx2*y9=zi1$v? z2`6eRj&9C8)nm7Prxpa|(&>a*#=Zq6kUo0O;tw5u-4MAp-X9Tu;Yh|uz{bWl_9_V( z+vjfD#Vqcqtprt3^61e! z4enA`>Ft@&VjESmZ%Ugp8EWyBG9hMawa|5u_Ck60ipqS3@OSnlz^IEs!X%KWUD?l_ zt5B`G{S@@&D&G&wT#lVopBTUrzw8}H93k_R^phrB3=|tYCL6`iwwv~=G)fFd zUL9>siMzANEP46zew_65?M&;%DSI2A@)|_0KRO352feYlUN2VH zR5xNz(x5P;l7)_m=@Vp)-B2n~-~9%ZTY09(64Q2nSaO?0E<+rYBT|rhR@Ce)PW{1S z8rQvJJX#&LmGIH2{RX62(@2sOka9&cf$o{+9y2Q{uIL8|*x=P>M{;Njx$jn(+KI3H zNaRA<5SZ{le97_@3K4~tp|jj|Xacc^>H%xNJ2z94^k*K6M>9Z1z|P;NOus)TDH)$m zJ-@U{=ed}QUzW~IIOFHkkM~W^w(@dL!#?~dMm*U!ef0KMYOo7hJX0-`y59(*1u0pv zr7X9srqhT59)-hDl7Q7`N|n8DLp;p!ZRt)rJ< z`(Jh^v5U6#MPzr&R)jcY!|cpC?Y3|$b`L@qXk=vHyPl9S=~>8tQTc0Sd5Ara`#$f# zjmp&~Uek^`k;4QcUH(Rffw$y866F-r0!-2?SzRJbY-FUuP#SFpfBB*a)}7u8ZT#tg zgr@FNOPfywfsq%PC(Gx8VOu`|o(OUFU+ZL2iHi;w^+`s4NC#(Qcgyg*mx~5gp11tj zpi*=M6_t`eqL4s!xx6{{Q{_nnd$=IgCBr+~QLVS{cqo=X!SL$wt)%Z~Ns400lyXII z8n3HY?o5IH9WtBE;i)aRCH;rklod8G{qN;dSECt^1j+x58?`D;jrubWvw>xSlcX3Q z?`n&j`mLVzM}gefe+h2@8T+@u5l4iG&5?$y;b23193*{x!uU~28+ggm9KwUiDU_&U z?LRu*i{5ITttEt-%WjQ;N>;p#D3Wf!@3LqhJQj{9P{S1la4`-x5a^n29PiAchmi1m zB&>UfXz*bMTct#qPg`US?BXG}FJCr417b(!M*toMXdJa6!YXHp;0y#lLR-acbvD1Y z{)iRH4Jl_|+gVE3Fj@z)5k%eApR>1JtSL z!kor^N|^s=g1NMng~^@#TX^weu!qIzO2EO%aZ{TSD6TNp2NIjZ&(STm^+NxySIhPL zpZ!0;Pu58?Bl5%2FTA|LI&ms4*9&P=5=w^)A0JfV8~L!=8P_kc>(co1cg zy}n#}`W+6cuiPMHWv2YG-6H0ZJ^~(>F#&rB$^tKB9Oa7G@3N6_aFkyj;Qg}EK^{t1 z2j;1>UZ~45z_jSvTZwdjsNEeY3UUj(>{cn|A(%|Hx})M+G|HvOXn6kf^*xbvi7BfY zEASf?O6=XR4lDs&*zIqkq=&C}w4b?4F&sKbnG#jRhjvG~$}~8!=yZni{qgdI$Wn@@ zfkL6qD9#u#z};C26vKP-YoG|NRHqLb^9s|>&d#U?oR)&?9V?c5yvnSdA{i8=Bzs~Q zlGTmWNEjGay78>9y@}YsaZK3K+`OWow+m_-?~T4rUO?O=)hGd;Ao%T0e@AcSeTKU4Vb4T3~H9(DXXOC&@a zQg;AAxzDZF5e3}TiiNa#0||El#5-E9DCv<}rY9vF&srdpZy*!+oEZgBN{|tXoN9|^ z7f=O<2{8X=s^q{ARfTG!@5F(wkugj15(glAgW?!GR~FBF9us2#Sag(?kX_4bN>l4I zCug<(z~k0BGj#3{P;8T8mFa)+<7M&to5KOkn0_e+(f7NY;PdOG4lWZJe@pll1k&KR z4`nf!PJJ@hn@kEkrMFD4*nWkl>i9)OM2wwUTiCqiydLe6%TXvWN-zXFh1S0rXXhf& z@j?hzreMqyp<7v1HPJy0aSAqRi?QX(ehQ}1h-#z23mc4Al1Uyw0g%3WxtD8rA078Y z?6B_dp$>9J*LI0sjbbb{4qN$={v_UzA?^01!~JDOQy9iorQwq~x9pO*6X$q-0m9ec(|_p>&(w!FolPR7=us0OUK6664H_e z<~}(}ja-1305Ye``P)XpdAEC`^*n@(IADXl+{ZRQm>owIYicE-Ox6L->}@{_Jq}H& z4(pX>5;m1t_iKFATXIa|r?!hP>9I&I?2=DhhW>lN&;3Z&6W>y*$1>e>Pb|#0muv_3RF-(v<<1p$BaOtE^`;!B}m8-eABv?Fr`#s#SRh_dD4t$dt2@gQ6I}d5`tFRAxsHZ zm_*>c&u^G`(BPb|+jBZrEZ;`NW$?lWlhhyZOWiNR#b-THv1Er1M9D|~W!ecDpzV9BDIuOySeVJmxo&5Mh{xY7ig z>jCsWV5pWxgWufT+(i?5yVrEP#Sbg>_R2{KK@QR3_B5Lr%zkFpPr~cwK_#7>vkxi? zzVs)=8Y!8~oIU3uW(Kfl$Wgcf;HxLpFO?qgK6L)t5f%n>={3J+I8X`9_&_BPW=Bge zJGKGIikD*IS?^Es_J-L?1haKzy+f&^AeSvP&eBos_Evr1e7qAZmB;~|u-hA#ypIJ2 z?cS^6pdjdI6FPGQGp3m6mPro$G_u|oU%Ubz#@|4f?WPr)`B}|jj_{4Kf}I<<0lmW`UiaM ziBKMscmQ$HqHnTWFw~5eZQ2v%kyn$Q+<30F;?XAO9dGnDlfm=eDP~w7TzF%=A^T+J zrNKe}G|hr$k#{k4KGggc>EA0?p_~ zKAj?H>ibj2rz6`Cq^^OuQ;XWc=RCpKb6eZTq~Mn~+LwVj-mur!uK<6il8gl6;Lnwr z^cAM5BTjBl;B^Ypc7dfY;L(wI#}9PLG`^CEMO}|$;XNc6}6~P zEJ{+UHjt73faqsBli^edG@GgZ1_Wn0@Gn8gxtV1JfFjzUWE8ca)2GL@*&e?~%xOPV zc}VxpSkGXq8K@wDO6p~Ffs)yKlFMVov0mc|R%Ca0`2?&3?NfJkBznzbaNCp&sxq7) zIb37-P(mgsljdM)vpG&Y>wJXhj^7{Xir6~O(wQAEnE(2inA3Oy2pw~|vz6sqtaQgu zZb6s&TkAEG3e_h~XGlTdXa+Q!6*EO1=@CzO2-rVzR-J-osQ6>V=&{^|K8ituUg&uv zPQq*jr`gl3J{2}b?KRr#!z^Ez5QIi?JR1j_GSH?AFjlBKDcZ)m6W} z##_E1-eI#J?w8m0EG86pA9h`ZJ%5vnLUq^IWGanQGxYK|NO&}NWaBdFy|dZ)l&AN| z1X#TLC2I>nSKO2!0DFb7pFZ`GyG;uIZ06L^fcVUZ8P(iJUkO3;5(`}kt>mWQpBx&x z8-^VQRpoLz61l$xC*FayTdXWUboFme7seLZBTA%uIpI zr5ka*{Zu!;JDum#7NE3?UjPo5Th(-`898Xr;!AC$dYlru9MQ(|6d(f;cFwx-qMz7V zyIP~R_Dcu2xwtIkru&Pur=bEzOUSde*=lupP)=C}%>PRe4I8;bONZ)0_Petqz8GZ5T*2L=sT0#Z zJV|nBxp6Ll$d}LC%5Kjr82(RmuEF-}=C`OnW|~NGwBthIScHVokh_iGQ%Y=l30U1J zBP=*1R6Q%xX)AxKOuo2Ngy{`v#v>r-A@BwVU+te?-5kJ1^f(ulobOF8VZHG{@?@Po zc=(!Ai!g?)fc3}NNQV9bFdXN6NuRDVGhS6dk}q*{sR26;nq}EeOxkoF;vjGn(b`Y* zhVm0wn_jGTEoVX-rJvmre6BL`lcf}UQE)Q{B8W0QU$!c{91m1`uEVcSruWV^mP5i` zM?C-BU~Yia(g|ayn*t@>gr%!_M(ch$C1j!D6rUMB55 zP{l&dPqoQ3UZ|EG^I6Y{ z*?{J3jvFtMISM4`W@@x2`wwd0*)e)TaHz{x!Po$y?J9pf5}`^s6qm@WIOF(ZR3iS}tuI90Y%;wt{7 zl839#PoiAk2;PTDimH4c4miJZME~8DGX(WN%(GeT!DzLIlW)BNEJ zp{+hkESUq4NX2d#N4j?p+kqPkRQt*TC53FA9vus9LPdNJc=pc0U;vOMC>>3`uriTV zlVDN+YVsV9=YW4}pk6I=)@iKGWHBUPwsQ%?VmNS+dQG83!IuI`j}TfU89utMv^@oOtA4=o|FNZh3 zmluHPY@j^eGpLCGY48oY}(VQ>$cIFPV37fESGniflaZ~j$iT&(}>y2d+ zWO?}#&XvS#|Cpd0j-%oPX`2{~4X$^$jWFuE+Yy}mI3UBRI`l*7ACh#-W+?SyEWMI5djux{L)MOPSW(6$y?Pw5W>V|^o44)xc{zlwK368-a8`a>^n z4BEkh;mh(3pGGagfya^N=FQ)L1w?>DY8xp*iZxaTyk%WVZnt`dR z#9#;s0vBhBdQ8fs!`1eR&7{r_{wwp1q6Hkr((vx{$GHI6MvUx`=GVWiu4Kh4))C3$ zcDZQg-lb(hl7ZLWIybwMB><~!9HW@t0z5r*B(@D5A3RawZ8>VVURT7uf^YT3SKUbt zzCZ>gVP7?(x__`Z2gi*Ue{scABILBg7%x(Ja_o@Q+lc=c&q>YwHlALkq1G0%Anx=^ zz0w2~7|{Je?S$sq&Xf!!`Hce9>lAnqTE=w-0KK8v6Z6Km!dOALPkZ6#-3#U=X(*Tb z`KtWUO-f8^%)XqUduUOW6-0sdQ zRl#SB_yARCxX*qET`6hi^O#HEvva(*)Ybs0YghwL@FgG{Yz3~$a#K$JihvqJx!y{M z-*@$lGW_yAuEKuk#F{LuWsZlKUcIJJnlf#L9>nF%>sxOAErDK{7obeuVE4-VZ%9-| zS#X?UrDp$S^S#Z4pOZbgue*LbT)mft(23{cpQ{T*Q;h8=%w$UMdD;9Kca=;{Pq&P< z4Sw;pSqU2!9naXA?(F=Y)9(Oy&k7SmwSBl#7IL+ZFq(Gs{XioMLJ1OywH~#>kmBj+^ z2k`e{{kq@3hkKOgzDaIcW5Q1SpZ8B7ObNmv$q%I&7+ia6pHCENj|#WQ3Eue%zHhL6 zDv$e(2VwupS{8@i)bHb4Egkx-rwF%asjFUpTtN&LHl;gavO$M%DFsL3ASO{xQ$RzX z;WU9+<4zho!}R5JR}DOhcA@uJNI%YCTL;j>vYW`mcDEDu|7sS7+c(rKW7~>Dgp;t7 zN6(9yTMPE$1<6rcy`U-^?>~;i1d0-Y=+xqLcUGQ202k3R6G&0+m)rkr(?vZyKKQ7-5v^ z+fQilMRIZ5+Wb0RHn0`!ffOEJX}DUw4nsvn|GuHc5{) z3;g4mzPAI1Ga3wP=BrkqJ)x2s71#b;ZotT8<&*=S!>v+4InbYqx()@vIRNtUGDZ#C zS(*CgzXmt2SoUc`Uj7Gm^T#?rp{e`X*%_kMu``jgN}W&+bQp#*rk@{fKq3z7MZ(A~ zCIfW!^|LF3cB<+f54SQcyG|StPhut2K*=;#37SLFVE9@2rtZdv_I7qcpxbA(9H9Oi znbBRxWndl+iXzq&)d^x8N1 zw($10tJ#&yblbpIA#9x9b`EW&E8;v%W4v$O+*#Di z6eMkzK|bik&U_Q;;^HE7Mjps+i^1c%mzP(HTMNp@L{XDnx`}W4U2yluSZO~5)1a|O zXM0WN!{T%S4YauKn1{Fm)%tD-t3r`SwPK@PuW-$0haabO?~{4`%0tMH^&6cSmK~&M za4h0DTnX#z4{|j5I;4n8%;$N$e0-pr{JxL)XKQT{^-09#hJaCT0n)+jEcW*tDekv- zH^vK|2LlOVp+-|-cSOX^J(Lt%kn}oVPu>UMX~VmH3IirUMZ!PJ{7psP8_tmYBZB5_ zN%`;%>Bn13vLr57e{$jAjm#lX!IGv{=9BYP4dhD@vLLVsCk`PY;1Q=qyu&e?9lcX! zp_JkZK;@nmRCxnTr5#}{zGKequP$AIe9B2?uFCZMVw`pe}9pbxaXMBiXK zix|`|`T|N4x|!wwl+M3PE-S~nnOpd=&E6a_IA$?;cz6tgNhIEP+BG=wai}9Pw-W{% z(!FDQk@~y$P>hln|3!vc5#L(xsn3i?FFWd3^yLuq7LW-ZQzMn^O?nwYq1#^?Q;32iVHC{V>B68&?diCoX7+Y50-_s&JT`cracGpg*?``KNs);e#p zMwi)hM~gHx?vD!xKl=uZ8=8B`&O@aTao|O0_Mld&r1ilh4$hU!)|#7f+<+D2#l4!g zU20{(B7Eu(YDaQQSK2{uX+4-HZVcUtqSW_GPfzF3N#an~(#`PxPnu{RjkGprx}#ha zt675g7Aa#tJ+3x}0Edj&Z}Ob4JORU)=PX8}pGN-N0b;(MEPV5Pt9N98@J}ZPJ@*BX zp;$HPuE~UVj!a;dogX96vmEu{e)&KMkbyr8cuN1P+)!=^JRu$D`oPI16fI9?lmmyO z-NAKdq{eD?g|`Q;$gBd5CK4bAAvq2`4cFdn-Cs7k=vi1?{3es^=3isI)*2g9V9mwT zo7nQ&?S z1vNFbPRWln+`Uha80H~)5dIFS#PVB6iAx)rEkC)=jckQks$B#Ke-Y{c@)vTNz-XQ2 zH0KX_!zffI8WsQHB^!OXDH*K7JmlhdE04+1i~zniJvN4y?8ZU(Mxn43#E?h1$jQh@ z4}%J?>;-2B#aKAWYo?-~)7CgL>6*Gq#t4==gD8j>X!e8ZD%ZLYfCrJxsE&Lq;ekCv zE+${OgZ-;C$#W|-FcirSAqWITP(g$&K2`RXL|hgxwm3@c=hY*rWn>>YJ3BA0bh(fA zm4~i(If?w<1Y?mOthO{yjXRvI9_5TemC^sfJkyEMvviUx_Xh8dWJxP_bij_cKjkTy zxaj z)tlYSvsJgG9z1IiQj24H#>OfO%48 z_lJ&v_XB>k`>$o5k?c-=sujRK!)5R&&Gdg6p@QNHJxLsGzT1YjE*vW>I9C4g{@e$S zy3sM0J2FOy(`cp>SkC69hyY>0kou(-EgL4zq*A%TOAaHp8E{vW<-=LV-x$%C%G1~c zV+=1q$}i5^#|zCaz%tAD)7yg>bq{cDwg$0D-Om`oHIi-i9?`J-SGO&CBB$*yaC;p! z*0}-f?)cg{dmM|YKQNGyZRi1aO1#gXCf*LI&TvZft1FCHLBo+r!P%TbdVi?x+Ij5Z~B_U!xG9?L}b{~_(IqpDi__Fn@5%S5 zx>G=;8|m&kH}1WkcR#=J{?0gK9EX1xu-0O(HRrtV>w8_FOG!~2*!@Du2URZGK5SVn?iGR^7Iz!Ll|J{Daf}45IYa>y>cc@%;q!p$y1N zA%bQ3?@+j~oTU*h97{)&e<%%4n`?A-3yxviD(YXNy>hs{psB$6V2_J!zs$(1a0sTM z0S(zYN^h&mcqFT+z8~%ra0yJYuC#fzIr_TFzyZ+;-RW>a1Rh~m!{y0kLZ6}>8%K+Q zHM;!e-K(wj9@W{K5Ck)Z<68pHx5hb{C3ysGj|%r3WO~avB^3R*ERN_z?T}FPJ+1AP zHLVr*NB5W1EaJ7f)iMb@pz{uzEPm3Sm!b!FkyY3E^Bl$XWdWV#g#_CfaCs^}v%DNxu&G48zr6nTIRrqf3b^?P z;^@Eibgz&EX1{np&D;b7Sj;aRX;f*|)?%h;Em=msa4GGdUPZl4hcK?S_?ZoG(?yk6 zpB`sUF@U9}(0o_%6&sL(d4tY7#ocvUJkj7UA}Dusq??jk%0`e#peFa12-1!}Os+4G z7Y$TF%oxSeE|UaC7VK0o2!VmA^6N-@ccdon##}qzBiFCyn;q&pYCb{}xb4p$>-XbZ zi|jZH`yydu`son1)w|;wj3$$|o3A75Ro0YJFl^FMl2I`2X{e3vo{-H>%&bByIUf8U z#!5xh%0xaS>9t}96(=ubc|w4Fc=!iu@b=~m#Ek#wm9rKgZ9FzMVh`2P6k-!Cgs*k( zM?Q-kipV?c5NT?Vu|W1{GMbRhbk1tYfS@dHz{jkrdfgANU2aXpkIyBjLUw*O6n^!v zSyt)tNrZ!v&YQrl{aK+8#XAU)OW;tjwZlY2tSceW+Qpem;~7sDj;wKz8!V!fpl8Dy z4JJ}CBD1F4_lCZ1a~MQtqg7G)k?*A1DLAd=b7*TsTNVq;poE;t;@B0FI#sBq_R3AQ zSUp(fn_9MO_)gJHy&2nHRMhXUNm0wHc|*x(OhVHMbQ7_ad!yhq{q{NT_gDqVNE+Wa zA5~jE8yiRe(AtGCZ*4oEPL}fZqDj>ljZyN?EzRHgLyYx;JWwS6%^T1d)bq97H=03@ z-^2V4+^=5Xaocy5uL!Bk1t^MWGk+{m*@!o~TOgNFaZBkTun9As<1n2;YUFSwe;Vi4lkWlOJdXDC#-SjSZUFN6;Kxu3)ALnJ}*+f+cB9CXO^c`X`crj-8(AE_&n7q@B8&x&@#@$hi7k;Cd6fW?F=P8U@J~(F{9*$ zJc9T@QSdtspQt$&ZmsFTL72L;XYTT2aO)#zI^B{*o+f=!&+9%>98v?)XTI9=425h_ zgWpk^JAilX#S5mLA@NyN#nQe3PiW$L3>y89;k;gZ^%d$3hW_TV?Y~RvpgZ!V&7_lW za$Wl(Q5El1(k)~MRZHam;RrvgE3dhy8x~;i&xtqBPoHg-2|^~5D!Od(7>we(8e|ao zM%(|#ylE7>b@}@jicwFPYO-sEL?Fimc?m1tab@3w+tm>fx!jkqyhC=8 zkR$3_Pk{Q{im9yX`myW=R~CAL=R`lf3R^II8( zrwldD3R_&!?>F00M9sG}L2S7Q^lFPi3ZYOUMQ^4Q5oWFXKY=AtCazkIn|Wcq3VJqP zE6mD&dCNR_@RFWt}<8(cjy_(*z%^6%~5%I?u+V>{MEe-v)xBwwb*>4ekUH8rKnTY zeM?sk$QK3dtq*s0k+rQ;%2S>itpVZKA}ZT)><5cL7c3acA(vU~20wq+3#opU8uKZe z<2#W%yH!8v9xQ?we~r3N_W3Me<0=NO@{{Gt{_)@Y03v#-o-Sse({Y~juJ)J%%`axT zn3q(_VP?$0^k<&sq|usHn;~%d{+_BzeAqFa$PBfIX0cfnu92I(<;0$~t*N~)8P=Qi ziQ8!sow!zpzr^MAxY27N^?dQNEc-LQGrD5U52_ZFQau*qS;;NdlSRSWg(HJbrt?7t z6}Oz84W<%LinL;J3Id6DUkKD2Y+t=bz{(nVueDXLOcUqm2-Z2po_@^i-ul!P84^_S zxkk#|l}vmiO%6an{Q`?_(Tm{y`7>E}iG&hm znG#@NSFSNxIXopw%nN)0lgh-el0ot=bxHA5i%=d0qx~n?=1gQa=Tx{tMY#bufD6n_o-0K1V}r@$on%TpsJ&F|)mOX*%7P839y-Gr z6VTnNufA5-=HVa0<01a@4%Qg4*0_O_ZTyt`|J__E208&vQg}cS^Z%5>{g=5iMLuR6 z=^CiYWKxv<$~Ja(O8YPxTI^7-CF7a5F;`v0wPcWmp%CQU(X|Yx6Jy%e?RljfxLp_u zMJd~bT&#>B(Iz62m66!kM;?g99rSPQFN$f1T8>%#f|Pq|khACr;yzp2YvTR_~! zlCTQ}RNP|v^ZvIM5|M-t?uEmGc|_`ae9X|JzX``Tt26_`g#L{$>x56IW6`A%f3r!|?@> z(sgxj)TKdPZ70B$lp8+)_X$=`;-4d-V7|KkI|t#p%PHD)AZ?TVLmUVb_rAQy^3n5v zc%?fo6{INOgurlhxCmiIpx4q>3GoWKQ_%OEh}aeFk9MAusO;VK_h`lW=B!W`Stie; z#glGl(&94%g{(efbWBY?ylnmSSGW#4XS0l2Gg4&u{IB2o8qDzaItYG`KABsyX8!Ze znX`Z%!#NJCo}LV^jNfhY#Yr6q{Tr~b<{Xr zwl1<#e+-#?fjqETHZs4L$`)U_;!A`*Xj4I6!FPduC!BeYFuD>P)xjTdj7rFbH#*v> z&;%8|QKs91&1~HFH4&S$i8EWVJLKr}n-A@*D|X9q56(_=@O`;Geham&C&d((nUxhn zhGK3MyX8<>IJ0h%u>>en6td*7fL{n*V7P89HJ<=oxc-o`rI zt>dcPN2gDXtafZh1J0l1`w5SI&a)fNetc>m_ z)=%?qB0C8FaA8?uR6*itEPoVo$K(-2h+GT3=RZH8VF4qc^AGJWLFN3^7bG51`P^g~ zh_vvX(Y|+TTkRN)pDy`60;{Tzay}5Dap1 z#-r(Y`#ja15BCoskt{S$?9XJ=0a4%Z-lxgU(SCp8RsJJi_}3H;g*IKC$pQ@&P}m#p z%4#_a0RT)e9KijybU(E_clomV<2AqMy#om_!nyqX0JZJ`+-0-u&KOS{!E|0G<4~z* z=4biY@|DU^mU~YY34b8wuS^!!V-XQ}a>9mVv%dwnDfxU^%Qz~X*Pxb4;Yaa)e%;ci zFz@@@Zx-sutBcNU?_u`lYk=>YPq0Ljjmdq6(ZPMw+DQy}LlK=x5YhZ~%bu%g9u&?2F6fANe+02RipyLfYa_^ zLaK>K9NxQ&d1D~kvRBqQ-;5=WQbY1qF&eAw;moM{r^YAuw6ru}DZ(p=#t~{@V!y

tWIr~7{r5kDaf^&o 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 literal 0 HcmV?d00001 diff --git a/website/docs/assets/houdini_frame_per_task.png b/website/docs/assets/houdini_frame_per_task.png new file mode 100644 index 0000000000000000000000000000000000000000..a7d8b33114e640499c892657fcd9ec9f3a79ef3c GIT binary patch literal 59257 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 Kjy4>*aO-bXs4lty literal 0 HcmV?d00001 From 82b75d53b319c2b7ddbc3da7b3773154a255a88a Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 21 Sep 2023 15:03:58 +0800 Subject: [PATCH 0066/1233] update the latest lib --- openpype/hosts/houdini/api/lib.py | 48 ++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 856e7abbd3..1ab38ae2c1 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -141,7 +141,6 @@ def get_output_parameter(node): elif node_type == "ifd": if node.evalParm("soho_outputmode"): return node.parm("soho_diskfile") - raise TypeError("Node type '%s' not supported" % node_type) @@ -652,3 +651,50 @@ def get_color_management_preferences(): "display": hou.Color.ocio_defaultDisplay(), "view": hou.Color.ocio_defaultView() } + + +def get_resolution_from_doc(doc): + """Get resolution from the given asset document. """ + + if not doc or "data" not in doc: + print("Entered document is not valid. \"{}\"".format(str(doc))) + return None + + resolution_width = doc["data"].get("resolutionWidth") + resolution_height = doc["data"].get("resolutionHeight") + + # Make sure both width and height are set + if resolution_width is None or resolution_height is None: + print("No resolution information found for \"{}\"".format(doc["name"])) + return None + + return int(resolution_width), int(resolution_height) + + +def set_camera_resolution(camera, asset_doc=None): + """Apply resolution to camera from asset document of the publish""" + + if not asset_doc: + asset_doc = get_current_project_asset() + + resolution = get_resolution_from_doc(asset_doc) + + if resolution: + print("Setting camera resolution: {} -> {}x{}".format( + camera.name(), resolution[0], resolution[1] + )) + camera.parm("resx").set(resolution[0]) + camera.parm("resy").set(resolution[1]) + + +def get_camera_from_container(container): + """Get camera from container node. """ + + cameras = container.recursiveGlob( + "*", + filter=hou.nodeTypeFilter.ObjCamera, + include_subnets=False + ) + + assert len(cameras) == 1, "Camera instance must have only one camera" + return cameras[0] From 6ab3f2d0a9fc3599461b7ea3ad052956b2bcad7a Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 21 Sep 2023 15:05:44 +0800 Subject: [PATCH 0067/1233] update to exlcude unncessary codes --- .../hosts/houdini/plugins/load/load_camera.py | 81 +++++++++++-------- 1 file changed, 47 insertions(+), 34 deletions(-) diff --git a/openpype/hosts/houdini/plugins/load/load_camera.py b/openpype/hosts/houdini/plugins/load/load_camera.py index 7b4a04809e..e16146a267 100644 --- a/openpype/hosts/houdini/plugins/load/load_camera.py +++ b/openpype/hosts/houdini/plugins/load/load_camera.py @@ -4,6 +4,13 @@ from openpype.pipeline import ( ) from openpype.hosts.houdini.api import pipeline +from openpype.hosts.houdini.api.lib import ( + set_camera_resolution, + get_camera_from_container +) + +import hou + ARCHIVE_EXPRESSION = ('__import__("_alembic_hom_extensions")' '.alembicGetCameraDict') @@ -25,7 +32,15 @@ def transfer_non_default_values(src, dest, ignore=None): channel expression and ignore certain Parm types. """ - import hou + + ignore_types = { + hou.parmTemplateType.Toggle, + hou.parmTemplateType.Menu, + hou.parmTemplateType.Button, + hou.parmTemplateType.FolderSet, + hou.parmTemplateType.Separator, + hou.parmTemplateType.Label, + } src.updateParmStates() @@ -62,14 +77,6 @@ def transfer_non_default_values(src, dest, ignore=None): continue # Ignore folders, separators, etc. - ignore_types = { - hou.parmTemplateType.Toggle, - hou.parmTemplateType.Menu, - hou.parmTemplateType.Button, - hou.parmTemplateType.FolderSet, - hou.parmTemplateType.Separator, - hou.parmTemplateType.Label, - } if parm.parmTemplate().type() in ignore_types: continue @@ -90,13 +97,8 @@ class CameraLoader(load.LoaderPlugin): def load(self, context, name=None, namespace=None, data=None): - import os - import hou - # Format file name, Houdini only wants forward slashes - file_path = self.filepath_from_context(context) - file_path = os.path.normpath(file_path) - file_path = file_path.replace("\\", "/") + file_path = self.filepath_from_context(context).replace("\\", "/") # Get the root node obj = hou.node("/obj") @@ -106,19 +108,21 @@ class CameraLoader(load.LoaderPlugin): node_name = "{}_{}".format(namespace, name) if namespace else name # Create a archive node - container = self.create_and_connect(obj, "alembicarchive", node_name) + node = self.create_and_connect(obj, "alembicarchive", node_name) # TODO: add FPS of project / asset - container.setParms({"fileName": file_path, - "channelRef": True}) + node.setParms({"fileName": file_path, "channelRef": True}) # Apply some magic - container.parm("buildHierarchy").pressButton() - container.moveToGoodPosition() + node.parm("buildHierarchy").pressButton() + node.moveToGoodPosition() # Create an alembic xform node - nodes = [container] + nodes = [node] + camera = get_camera_from_container(node) + self._match_maya_render_mask(camera) + set_camera_resolution(camera, asset_doc=context["asset"]) self[:] = nodes return pipeline.containerise(node_name, @@ -143,14 +147,14 @@ class CameraLoader(load.LoaderPlugin): # Store the cam temporarily next to the Alembic Archive # so that we can preserve parm values the user set on it # after build hierarchy was triggered. - old_camera = self._get_camera(node) + old_camera = get_camera_from_container(node) temp_camera = old_camera.copyTo(node.parent()) # Rebuild node.parm("buildHierarchy").pressButton() # Apply values to the new camera - new_camera = self._get_camera(node) + new_camera = get_camera_from_container(node) transfer_non_default_values(temp_camera, new_camera, # The hidden uniform scale attribute @@ -158,6 +162,9 @@ class CameraLoader(load.LoaderPlugin): # "icon_scale" just skip that completely ignore={"scale"}) + self._match_maya_render_mask(new_camera) + set_camera_resolution(new_camera) + temp_camera.destroy() def remove(self, container): @@ -165,15 +172,6 @@ class CameraLoader(load.LoaderPlugin): node = container["node"] node.destroy() - def _get_camera(self, node): - import hou - cameras = node.recursiveGlob("*", - filter=hou.nodeTypeFilter.ObjCamera, - include_subnets=False) - - assert len(cameras) == 1, "Camera instance must have only one camera" - return cameras[0] - def create_and_connect(self, node, node_type, name=None): """Create a node within a node which and connect it to the input @@ -194,5 +192,20 @@ class CameraLoader(load.LoaderPlugin): new_node.moveToGoodPosition() return new_node - def switch(self, container, representation): - self.update(container, representation) + def _match_maya_render_mask(self, camera): + """Workaround to match Maya render mask in Houdini""" + + # print("Setting match maya render mask ") + parm = camera.parm("aperture") + expression = parm.expression() + expression = expression.replace("return ", "aperture = ") + expression += """ +# Match maya render mask (logic from Houdini's own FBX importer) +node = hou.pwd() +resx = node.evalParm('resx') +resy = node.evalParm('resy') +aspect = node.evalParm('aspect') +aperture *= min(1, (resx / resy * aspect) / 1.5) +return aperture +""" + parm.setExpression(expression, language=hou.exprLanguage.Python) From 7a765bd52836f50f90088d7738990130da957bcb Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 21 Sep 2023 20:09:59 +0800 Subject: [PATCH 0068/1233] mustafa's comment on deadline plugin and fix the bug of not getting right env variable --- .../publish/submit_houdini_cache_deadline.py | 2 - .../publish/submit_publish_cache_job.py | 2 +- .../schema_project_deadline.json | 40 +++++++++++++++++++ 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_houdini_cache_deadline.py b/openpype/modules/deadline/plugins/publish/submit_houdini_cache_deadline.py index b1717d09ee..985f29a814 100644 --- a/openpype/modules/deadline/plugins/publish/submit_houdini_cache_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_houdini_cache_deadline.py @@ -128,8 +128,6 @@ class HoudiniCacheSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline if not value: continue job_info.EnvironmentKeyValue[key] = value - # to recognize render jobs - job_info.add_render_job_env_var() return job_info diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_cache_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_cache_job.py index 5651ff4c83..f8e4613b1c 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_cache_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_cache_job.py @@ -239,7 +239,7 @@ class ProcessSubmittedCacheJobOnFarm(pyblish.api.InstancePlugin, self.log.debug("Submitting Deadline publish job ...") url = "{}/api/jobs".format(self.deadline_url) - response = requests.post(url, json=payload, timeout=10) + response = requests.post(url, json=payload, timeout=10, verify=False) if not response.ok: raise Exception(response.text) diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json index 6d59b5a92b..4e582209bb 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json @@ -531,6 +531,46 @@ } ] }, + { + "type": "dict", + "collapsible": true, + "key": "ProcessSubmittedCacheJobOnFarm", + "label": "ProcessSubmittedCacheJobOnFarm", + "checkbox_key": "enabled", + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "text", + "key": "deadline_department", + "label": "Deadline department" + }, + { + "type": "text", + "key": "deadline_pool", + "label": "Deadline Pool" + }, + { + "type": "text", + "key": "deadline_group", + "label": "Deadline Group" + }, + { + "type": "number", + "key": "deadline_chunk_size", + "label": "Deadline Chunk Size" + }, + { + "type": "number", + "key": "deadline_priority", + "label": "Deadline Priotity" + } + ] + }, { "type": "dict", "collapsible": true, From 98cc7357d30bc1966c8c48d7604dcac3d7ee0c70 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 21 Sep 2023 22:44:38 +0800 Subject: [PATCH 0069/1233] make sure all the necessary environment variables needed to be there --- .../plugins/publish/submit_houdini_cache_deadline.py | 12 +++++++++--- .../plugins/publish/submit_publish_cache_job.py | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_houdini_cache_deadline.py b/openpype/modules/deadline/plugins/publish/submit_houdini_cache_deadline.py index 985f29a814..f3b4d37a0f 100644 --- a/openpype/modules/deadline/plugins/publish/submit_houdini_cache_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_houdini_cache_deadline.py @@ -108,9 +108,13 @@ class HoudiniCacheSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline job_info.Group = attr_values.get("group", self.group) keys = [ - "FTRACK_API_USER", - "FTRACK_API_KEY", - "FTRACK_SERVER" + "OPENPYPE_SG_USER", + "AVALON_PROJECT", + "AVALON_ASSET", + "AVALON_TASK", + "AVALON_APP_NAME", + "OPENPYPE_DEV", + "OPENPYPE_LOG_NO_COLORS", ] # Add OpenPype version if we are running from build. @@ -128,6 +132,8 @@ class HoudiniCacheSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline if not value: continue job_info.EnvironmentKeyValue[key] = value + # to recognize render jobs + job_info.add_render_job_env_var() return job_info diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_cache_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_cache_job.py index f8e4613b1c..5651ff4c83 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_cache_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_cache_job.py @@ -239,7 +239,7 @@ class ProcessSubmittedCacheJobOnFarm(pyblish.api.InstancePlugin, self.log.debug("Submitting Deadline publish job ...") url = "{}/api/jobs".format(self.deadline_url) - response = requests.post(url, json=payload, timeout=10, verify=False) + response = requests.post(url, json=payload, timeout=10) if not response.ok: raise Exception(response.text) From 559e49abccd8bbed9920a069b945ac1d8004c95a Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 21 Sep 2023 23:41:54 +0800 Subject: [PATCH 0070/1233] add ftrack and kitsu environments --- .../plugins/publish/submit_houdini_cache_deadline.py | 3 +++ .../deadline/plugins/publish/submit_publish_cache_job.py | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/openpype/modules/deadline/plugins/publish/submit_houdini_cache_deadline.py b/openpype/modules/deadline/plugins/publish/submit_houdini_cache_deadline.py index f3b4d37a0f..2b6231e916 100644 --- a/openpype/modules/deadline/plugins/publish/submit_houdini_cache_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_houdini_cache_deadline.py @@ -108,6 +108,9 @@ class HoudiniCacheSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline job_info.Group = attr_values.get("group", self.group) keys = [ + "FTRACK_API_KEY", + "FTRACK_API_USER", + "FTRACK_SERVER", "OPENPYPE_SG_USER", "AVALON_PROJECT", "AVALON_ASSET", diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_cache_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_cache_job.py index 5651ff4c83..b2ebebc303 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_cache_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_cache_job.py @@ -70,9 +70,14 @@ class ProcessSubmittedCacheJobOnFarm(pyblish.api.InstancePlugin, ] environ_keys = [ + "FTRACK_API_USER", + "FTRACK_API_KEY", + "FTRACK_SERVER", "AVALON_APP_NAME", "OPENPYPE_USERNAME", "OPENPYPE_SG_USER", + "KITSU_LOGIN", + "KITSU_PWD" ] # custom deadline attributes From 4f52c70093217d79ad542e88a6078b5bb3be7df6 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 22 Sep 2023 12:06:37 +0300 Subject: [PATCH 0071/1233] make self publish button optional --- openpype/hosts/houdini/api/plugin.py | 11 ++++++++++- .../defaults/project_settings/houdini.json | 3 +++ .../projects_schema/schema_project_houdini.json | 4 ++++ .../schemas/schema_houdini_general.json | 14 ++++++++++++++ server_addon/houdini/server/settings/general.py | 15 +++++++++++++++ server_addon/houdini/server/settings/main.py | 9 +++++++++ server_addon/houdini/server/version.py | 2 +- 7 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_general.json create mode 100644 server_addon/houdini/server/settings/general.py diff --git a/openpype/hosts/houdini/api/plugin.py b/openpype/hosts/houdini/api/plugin.py index 756b33f7f7..8670103a81 100644 --- a/openpype/hosts/houdini/api/plugin.py +++ b/openpype/hosts/houdini/api/plugin.py @@ -168,6 +168,7 @@ class HoudiniCreator(NewCreator, HoudiniCreatorBase): """Base class for most of the Houdini creator plugins.""" selected_nodes = [] settings_name = None + _add_self_publish_button = False def create(self, subset_name, instance_data, pre_create_data): try: @@ -194,7 +195,10 @@ class HoudiniCreator(NewCreator, HoudiniCreatorBase): self) self._add_instance_to_context(instance) imprint(instance_node, instance.data_to_store()) - add_self_publish_button(instance_node) + + if self._add_self_publish_button: + add_self_publish_button(instance_node) + return instance except hou.Error as er: @@ -300,6 +304,11 @@ class HoudiniCreator(NewCreator, HoudiniCreatorBase): def apply_settings(self, project_settings): """Method called on initialization of plugin to apply settings.""" + # Apply General Settings + self._add_self_publish_button = \ + project_settings["houdini"]["general"]["add_self_publish_button"] + + # Apply Creator Settings settings_name = self.settings_name if settings_name is None: settings_name = self.__class__.__name__ diff --git a/openpype/settings/defaults/project_settings/houdini.json b/openpype/settings/defaults/project_settings/houdini.json index 5392fc34dd..f60f5f2761 100644 --- a/openpype/settings/defaults/project_settings/houdini.json +++ b/openpype/settings/defaults/project_settings/houdini.json @@ -1,4 +1,7 @@ { + "general": { + "add_self_publish_button": false + }, "imageio": { "activate_host_color_management": true, "ocio_config": { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json b/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json index 7f782e3647..d4d0565ec9 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json @@ -5,6 +5,10 @@ "label": "Houdini", "is_file": true, "children": [ + { + "type": "schema", + "name": "schema_houdini_general" + }, { "key": "imageio", "type": "dict", diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_general.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_general.json new file mode 100644 index 0000000000..a69501c98c --- /dev/null +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_general.json @@ -0,0 +1,14 @@ +{ + "type": "dict", + "key": "general", + "label": "General", + "collapsible": true, + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "add_self_publish_button", + "label": "Add Self Publish Button" + } + ] +} diff --git a/server_addon/houdini/server/settings/general.py b/server_addon/houdini/server/settings/general.py new file mode 100644 index 0000000000..9c19acd6c9 --- /dev/null +++ b/server_addon/houdini/server/settings/general.py @@ -0,0 +1,15 @@ +from pydantic import Field +from ayon_server.settings import BaseSettingsModel + + + +class GeneralSettingsModel(BaseSettingsModel): + add_self_publish_button: bool = Field( + False, + title="Add Self Publish Button" + ) + + +DEFAULT_GENERAL_SETTINGS = { + "add_self_publish_button": False +} diff --git a/server_addon/houdini/server/settings/main.py b/server_addon/houdini/server/settings/main.py index fdb6838f5c..8de8d8aeae 100644 --- a/server_addon/houdini/server/settings/main.py +++ b/server_addon/houdini/server/settings/main.py @@ -5,6 +5,10 @@ from ayon_server.settings import ( MultiplatformPathListModel, ) +from .general import ( + GeneralSettingsModel, + DEFAULT_GENERAL_SETTINGS +) from .imageio import HoudiniImageIOModel from .publish_plugins import ( PublishPluginsModel, @@ -52,6 +56,10 @@ class ShelvesModel(BaseSettingsModel): class HoudiniSettings(BaseSettingsModel): + general: GeneralSettingsModel = Field( + default_factory=GeneralSettingsModel, + title="General" + ) imageio: HoudiniImageIOModel = Field( default_factory=HoudiniImageIOModel, title="Color Management (ImageIO)" @@ -73,6 +81,7 @@ class HoudiniSettings(BaseSettingsModel): DEFAULT_VALUES = { + "general" : DEFAULT_GENERAL_SETTINGS, "shelves": [], "create": DEFAULT_HOUDINI_CREATE_SETTINGS, "publish": DEFAULT_HOUDINI_PUBLISH_SETTINGS diff --git a/server_addon/houdini/server/version.py b/server_addon/houdini/server/version.py index ae7362549b..bbab0242f6 100644 --- a/server_addon/houdini/server/version.py +++ b/server_addon/houdini/server/version.py @@ -1 +1 @@ -__version__ = "0.1.3" +__version__ = "0.1.4" From 792e92ca44a22ad4318bc8ba330024719b86e068 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 22 Sep 2023 12:08:58 +0300 Subject: [PATCH 0072/1233] resolve hound --- server_addon/houdini/server/settings/general.py | 1 - server_addon/houdini/server/settings/main.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/server_addon/houdini/server/settings/general.py b/server_addon/houdini/server/settings/general.py index 9c19acd6c9..ce20a30e7c 100644 --- a/server_addon/houdini/server/settings/general.py +++ b/server_addon/houdini/server/settings/general.py @@ -2,7 +2,6 @@ from pydantic import Field from ayon_server.settings import BaseSettingsModel - class GeneralSettingsModel(BaseSettingsModel): add_self_publish_button: bool = Field( False, diff --git a/server_addon/houdini/server/settings/main.py b/server_addon/houdini/server/settings/main.py index 8de8d8aeae..1a3968bf28 100644 --- a/server_addon/houdini/server/settings/main.py +++ b/server_addon/houdini/server/settings/main.py @@ -81,7 +81,7 @@ class HoudiniSettings(BaseSettingsModel): DEFAULT_VALUES = { - "general" : DEFAULT_GENERAL_SETTINGS, + "general": DEFAULT_GENERAL_SETTINGS, "shelves": [], "create": DEFAULT_HOUDINI_CREATE_SETTINGS, "publish": DEFAULT_HOUDINI_PUBLISH_SETTINGS From ed7b321f640617bc529d548b613186b4fbd3b7f8 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 22 Sep 2023 16:18:04 +0300 Subject: [PATCH 0073/1233] BigRoy's comments --- openpype/hosts/houdini/api/lib.py | 19 +++++++++++-------- openpype/hosts/houdini/api/plugin.py | 6 +++--- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 27f5476894..0e5aa1e74a 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -776,6 +776,16 @@ def publisher_show_and_publish(comment=""): def self_publish(): """Self publish from ROP nodes.""" + result, comment = hou.ui.readInput( + "Add Publish Comment", + buttons=("Publish", "Cancel"), + title="Publish comment", + close_choice=1 + ) + + if result: + return + current_node = hou.node(".").path() host = registered_host() @@ -792,18 +802,11 @@ def self_publish(): context.save_changes() - result, comment = hou.ui.readInput( - "Add Publish Note", - buttons=("Ok", "Cancel"), - title="Publish Note", - close_choice=1 - ) - publisher_show_and_publish(comment) def add_self_publish_button(node): - """Adds a self publish button in the rop node.""" + """Adds a self publish button to the rop node.""" label = os.environ.get("AVALON_LABEL") or "OpenPype" diff --git a/openpype/hosts/houdini/api/plugin.py b/openpype/hosts/houdini/api/plugin.py index 8670103a81..2cd7ff83e3 100644 --- a/openpype/hosts/houdini/api/plugin.py +++ b/openpype/hosts/houdini/api/plugin.py @@ -168,7 +168,7 @@ class HoudiniCreator(NewCreator, HoudiniCreatorBase): """Base class for most of the Houdini creator plugins.""" selected_nodes = [] settings_name = None - _add_self_publish_button = False + add_publish_button = False def create(self, subset_name, instance_data, pre_create_data): try: @@ -196,7 +196,7 @@ class HoudiniCreator(NewCreator, HoudiniCreatorBase): self._add_instance_to_context(instance) imprint(instance_node, instance.data_to_store()) - if self._add_self_publish_button: + if self.add_publish_button: add_self_publish_button(instance_node) return instance @@ -305,7 +305,7 @@ class HoudiniCreator(NewCreator, HoudiniCreatorBase): """Method called on initialization of plugin to apply settings.""" # Apply General Settings - self._add_self_publish_button = \ + self.add_publish_button = \ project_settings["houdini"]["general"]["add_self_publish_button"] # Apply Creator Settings From 1cd3a9e701cb6cea04844c6dec7a7672c8a2758d Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 22 Sep 2023 23:00:58 +0300 Subject: [PATCH 0074/1233] make self publish to publish input dependencies --- openpype/hosts/houdini/api/lib.py | 39 ++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 0e5aa1e74a..3780087bd0 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -773,8 +773,37 @@ def publisher_show_and_publish(comment=""): publisher_window.click_publish() +def find_rop_input_dependencies(input_tuple): + """Self publish from ROP nodes. + + Arguments: + tuple (hou.RopNode.inputDependencies) which can be a nested tuples + represents the input dependencies of the ROP node, consisting of ROPs, + and the frames that need to be be rendered prior to rendering the ROP. + + Returns: + list of the RopNode.path() that can be found inside + the input tuple. + """ + + out_list = [] + if isinstance(input_tuple[0], hou.RopNode): + return input_tuple[0].path() + + if isinstance(input_tuple[0], tuple): + for item in input_tuple: + out_list.append(find_rop_input_dependencies(item)) + + return out_list + + def self_publish(): - """Self publish from ROP nodes.""" + """Self publish from ROP nodes. + + Firstly, it gets the node and its dependencies. + Then, it deactivates all other ROPs + And finaly, it triggers the publishing action. + """ result, comment = hou.ui.readInput( "Add Publish Comment", @@ -786,7 +815,11 @@ def self_publish(): if result: return - current_node = hou.node(".").path() + current_node = hou.node(".") + inputs_paths = find_rop_input_dependencies( + current_node.inputDependencies() + ) + inputs_paths.append(current_node.path()) host = registered_host() context = CreateContext(host, reset=True) @@ -796,7 +829,7 @@ def self_publish(): if not node_path: continue - active = current_node == node_path + active = node_path in inputs_paths instance["active"] = active hou.node(node_path).parm("active").set(active) From 131a55b32c880d4d9983d529aea97ae926aed183 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 25 Sep 2023 11:22:58 +0200 Subject: [PATCH 0075/1233] :bug: add colorspace argument --- .../hosts/maya/plugins/publish/extract_look.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 74fcb58d29..b079d2cd62 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -176,6 +176,21 @@ class MakeRSTexBin(TextureProcessor): source ] + + # if color management is enabled we pass color space information + if color_management["enabled"]: + config_path = color_management["config"] + if not os.path.exists(config_path): + raise RuntimeError("OCIO config not found at: " + "{}".format(config_path)) + + # render_colorspace = color_management["rendering_space"] + + self.log.debug("converting colorspace {0} to redshift render " + "colorspace".format(colorspace)) + subprocess_args.extend(["-cs", colorspace]) + + hash_args = ["rstex"] texture_hash = source_hash(source, *hash_args) From 2a9d90d742f38f8abd5bb0deee224d2f469ac9c1 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 25 Sep 2023 11:38:23 +0200 Subject: [PATCH 0076/1233] :dog: hound fix --- openpype/hosts/maya/plugins/publish/extract_look.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index b079d2cd62..cf1dd90416 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -176,7 +176,6 @@ class MakeRSTexBin(TextureProcessor): source ] - # if color management is enabled we pass color space information if color_management["enabled"]: config_path = color_management["config"] From 54e9efba95192b3f34ab03f50b992535ee56e72c Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 25 Sep 2023 12:51:38 +0200 Subject: [PATCH 0077/1233] :recycle: remove comment --- openpype/hosts/maya/plugins/publish/extract_look.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index cf1dd90416..1660c7b663 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -183,8 +183,6 @@ class MakeRSTexBin(TextureProcessor): raise RuntimeError("OCIO config not found at: " "{}".format(config_path)) - # render_colorspace = color_management["rendering_space"] - self.log.debug("converting colorspace {0} to redshift render " "colorspace".format(colorspace)) subprocess_args.extend(["-cs", colorspace]) From 49af8c21ecc4d88153f0f438873528948409fa8f Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 25 Sep 2023 14:03:04 +0200 Subject: [PATCH 0078/1233] :bug: add quotes to colorspace name --- openpype/hosts/maya/plugins/publish/extract_look.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 1660c7b663..9ef847e350 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -185,8 +185,7 @@ class MakeRSTexBin(TextureProcessor): self.log.debug("converting colorspace {0} to redshift render " "colorspace".format(colorspace)) - subprocess_args.extend(["-cs", colorspace]) - + subprocess_args.extend(["-cs", '"{}"'.format(colorspace)]) hash_args = ["rstex"] texture_hash = source_hash(source, *hash_args) From 7ed5442f3b4d762b9211664ff7358c9b43635595 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 25 Sep 2023 15:22:04 +0200 Subject: [PATCH 0079/1233] :recycle: passing logger to run_subprocess to get more info --- openpype/hosts/maya/plugins/publish/extract_look.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 9ef847e350..e6ae2530c2 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -183,9 +183,16 @@ class MakeRSTexBin(TextureProcessor): raise RuntimeError("OCIO config not found at: " "{}".format(config_path)) + if not os.getenv("OCIO"): + self.log.warning( + "OCIO environment variable not set." + "Setting it with OCIO config from OpenPype/AYON Settings." + ) + os.environ["OCIO"] = config_path + self.log.debug("converting colorspace {0} to redshift render " "colorspace".format(colorspace)) - subprocess_args.extend(["-cs", '"{}"'.format(colorspace)]) + subprocess_args.extend(["-cs", colorspace]) hash_args = ["rstex"] texture_hash = source_hash(source, *hash_args) @@ -197,10 +204,11 @@ class MakeRSTexBin(TextureProcessor): self.log.debug(" ".join(subprocess_args)) try: - run_subprocess(subprocess_args) + output = run_subprocess(subprocess_args, logger=self.log) except Exception: self.log.error("Texture .rstexbin conversion failed", exc_info=True) + self.log.debug(output) raise return TextureResult( From fd6c6f3b3cbd6c4d820f1725c117ef43e9cde89d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 25 Sep 2023 21:59:03 +0800 Subject: [PATCH 0080/1233] add docstrings for the functions in tycache families --- .../max/plugins/publish/extract_tycache.py | 19 ++++++++++++++++++ .../plugins/publish/validate_tyflow_data.py | 20 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/openpype/hosts/max/plugins/publish/extract_tycache.py b/openpype/hosts/max/plugins/publish/extract_tycache.py index e98fad5c2b..c3e9489d43 100644 --- a/openpype/hosts/max/plugins/publish/extract_tycache.py +++ b/openpype/hosts/max/plugins/publish/extract_tycache.py @@ -67,6 +67,25 @@ class ExtractTyCache(publish.Extractor): self.log.info(f"Extracted instance '{instance.name}' to: {filenames}") def get_file(self, filepath, start_frame, end_frame): + """Get file names for tyFlow in tyCache format. + + Set the filenames accordingly to the tyCache file + naming extension(.tyc) for the publishing purpose + + Actual File Output from tyFlow in tyCache format: + _.tyc + + e.g. tyFlow_cloth_CCCS_blobbyFill_001_00004.tyc + + Args: + fileapth (str): Output directory. + start_frame (int): Start frame. + end_frame (int): End frame. + + Returns: + filenames(list): list of filenames + + """ filenames = [] filename = os.path.basename(filepath) orig_name, _ = os.path.splitext(filename) diff --git a/openpype/hosts/max/plugins/publish/validate_tyflow_data.py b/openpype/hosts/max/plugins/publish/validate_tyflow_data.py index 4574950495..c0a6d23022 100644 --- a/openpype/hosts/max/plugins/publish/validate_tyflow_data.py +++ b/openpype/hosts/max/plugins/publish/validate_tyflow_data.py @@ -34,6 +34,16 @@ class ValidateTyFlowData(pyblish.api.InstancePlugin): raise PublishValidationError(f"{report}") def get_tyflow_object(self, instance): + """Get the nodes which are not tyFlow object(s) + and editable mesh(es) + + Args: + instance (str): instance node + + Returns: + invalid(list): list of invalid nodes which are not + tyFlow object(s) and editable mesh(es). + """ invalid = [] container = instance.data["instance_node"] self.log.info(f"Validating tyFlow container for {container}") @@ -51,6 +61,16 @@ class ValidateTyFlowData(pyblish.api.InstancePlugin): return invalid def get_tyflow_operator(self, instance): + """_summary_ + + Args: + instance (str): instance node + + Returns: + invalid(list): list of invalid nodes which do + not consist of Export Particle Operators as parts + of the node connections + """ invalid = [] container = instance.data["instance_node"] self.log.info(f"Validating tyFlow object for {container}") From f8463f6e9e30e1e96a50239c9a6e96cc3dbc2961 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 26 Sep 2023 15:11:00 +0200 Subject: [PATCH 0081/1233] :recycle: pass logger, :bug: fix `output` var --- openpype/hosts/maya/plugins/publish/extract_look.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index e6ae2530c2..d2e3e2c937 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -204,11 +204,10 @@ class MakeRSTexBin(TextureProcessor): self.log.debug(" ".join(subprocess_args)) try: - output = run_subprocess(subprocess_args, logger=self.log) - except Exception: + run_subprocess(subprocess_args, logger=self.log) + except Exception as e: self.log.error("Texture .rstexbin conversion failed", exc_info=True) - self.log.debug(output) raise return TextureResult( @@ -491,7 +490,7 @@ class ExtractLook(publish.Extractor): "rstex": MakeRSTexBin }.items(): if instance.data.get(key, False): - processor = Processor() + processor = Processor(log=self.log) processor.apply_settings(context.data["system_settings"], context.data["project_settings"]) processors.append(processor) From 499b4ddae529f2d2f2a880df08e7ff0bb320f617 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 26 Sep 2023 15:22:06 +0200 Subject: [PATCH 0082/1233] :recycle: reraise exception --- openpype/hosts/maya/plugins/publish/extract_look.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index d2e3e2c937..b2b3330df1 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- """Maya look extractor.""" +import sys from abc import ABCMeta, abstractmethod from collections import OrderedDict import contextlib @@ -205,10 +206,10 @@ class MakeRSTexBin(TextureProcessor): self.log.debug(" ".join(subprocess_args)) try: run_subprocess(subprocess_args, logger=self.log) - except Exception as e: + except Exception: self.log.error("Texture .rstexbin conversion failed", exc_info=True) - raise + six.reraise(*sys.exc_info()) return TextureResult( path=destination, From bf16f8492f39cce8c6b5c7cf39714a459180f94e Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 28 Sep 2023 15:05:13 +0100 Subject: [PATCH 0083/1233] Improved update for Static Meshes --- openpype/hosts/unreal/api/pipeline.py | 72 +++++++++ .../plugins/load/load_staticmesh_abc.py | 131 ++++++++++------- .../plugins/load/load_staticmesh_fbx.py | 137 +++++++++++------- 3 files changed, 237 insertions(+), 103 deletions(-) diff --git a/openpype/hosts/unreal/api/pipeline.py b/openpype/hosts/unreal/api/pipeline.py index 72816c9b81..ae38601df2 100644 --- a/openpype/hosts/unreal/api/pipeline.py +++ b/openpype/hosts/unreal/api/pipeline.py @@ -649,6 +649,78 @@ def generate_sequence(h, h_dir): return sequence, (min_frame, max_frame) +def replace_static_mesh_actors(old_assets, new_assets): + eas = unreal.get_editor_subsystem(unreal.EditorActorSubsystem) + smes = unreal.get_editor_subsystem(unreal.StaticMeshEditorSubsystem) + + comps = eas.get_all_level_actors_components() + static_mesh_comps = [ + c for c in comps if isinstance(c, unreal.StaticMeshComponent) + ] + + # Get all the static meshes among the old assets in a dictionary with + # the name as key + old_meshes = {} + for a in old_assets: + asset = unreal.EditorAssetLibrary.load_asset(a) + if isinstance(asset, unreal.StaticMesh): + old_meshes[asset.get_name()] = asset + + # Get all the static meshes among the new assets in a dictionary with + # the name as key + new_meshes = {} + for a in new_assets: + asset = unreal.EditorAssetLibrary.load_asset(a) + if isinstance(asset, unreal.StaticMesh): + new_meshes[asset.get_name()] = asset + + for old_name, old_mesh in old_meshes.items(): + new_mesh = new_meshes.get(old_name) + + if not new_mesh: + continue + + smes.replace_mesh_components_meshes( + static_mesh_comps, old_mesh, new_mesh) + +def delete_previous_asset_if_unused(container, asset_content): + ar = unreal.AssetRegistryHelpers.get_asset_registry() + + references = set() + + for asset_path in asset_content: + asset = ar.get_asset_by_object_path(asset_path) + refs = ar.get_referencers( + asset.package_name, + unreal.AssetRegistryDependencyOptions( + include_soft_package_references=False, + include_hard_package_references=True, + include_searchable_names=False, + include_soft_management_references=False, + include_hard_management_references=False + )) + if not refs: + continue + references = references.union(set(refs)) + + # Filter out references that are in the Temp folder + cleaned_references = { + ref for ref in references if not str(ref).startswith("/Temp/")} + + # Check which of the references are Levels + for ref in cleaned_references: + loaded_asset = unreal.EditorAssetLibrary.load_asset(ref) + if isinstance(loaded_asset, unreal.World): + # If there is at least a level, we can stop, we don't want to + # delete the container + return + + unreal.log("Previous version unused, deleting...") + + # No levels, delete the asset + unreal.EditorAssetLibrary.delete_directory(container["namespace"]) + + @contextmanager def maintained_selection(): """Stub to be either implemented or replaced. diff --git a/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py b/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py index bb13692f9e..13c4ec23e9 100644 --- a/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py +++ b/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py @@ -7,7 +7,12 @@ from openpype.pipeline import ( AYON_CONTAINER_ID ) from openpype.hosts.unreal.api import plugin -from openpype.hosts.unreal.api import pipeline as unreal_pipeline +from openpype.hosts.unreal.api.pipeline import ( + create_container, + imprint, + replace_static_mesh_actors, + delete_previous_asset_if_unused, +) import unreal # noqa @@ -20,6 +25,8 @@ class StaticMeshAlembicLoader(plugin.Loader): icon = "cube" color = "orange" + root = "/Game/Ayon/Assets" + @staticmethod def get_task(filename, asset_dir, asset_name, replace, default_conversion): task = unreal.AssetImportTask() @@ -53,14 +60,40 @@ class StaticMeshAlembicLoader(plugin.Loader): return task + def import_and_containerize( + self, filepath, asset_dir, asset_name, container_name, + default_conversion=False + ): + unreal.EditorAssetLibrary.make_directory(asset_dir) + + task = self.get_task( + filepath, asset_dir, asset_name, False, default_conversion) + + unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) + + # Create Asset Container + create_container(container=container_name, path=asset_dir) + + def imprint( + self, asset, asset_dir, container_name, asset_name, representation + ): + data = { + "schema": "ayon:container-2.0", + "id": AYON_CONTAINER_ID, + "asset": asset, + "namespace": asset_dir, + "container_name": container_name, + "asset_name": asset_name, + "loader": str(self.__class__.__name__), + "representation": representation["_id"], + "parent": representation["parent"], + "family": representation["context"]["family"] + } + imprint(f"{asset_dir}/{container_name}", data) + def load(self, context, name, namespace, options): """Load and containerise representation into Content Browser. - This is two step process. First, import FBX to temporary path and - then call `containerise()` on it - this moves all content to new - directory and then it will create AssetContainer there and imprint it - with metadata. This will mark this path as container. - Args: context (dict): application context name (str): subset name @@ -68,15 +101,13 @@ class StaticMeshAlembicLoader(plugin.Loader): This is not passed here, so namespace is set by `containerise()` because only then we know real path. - data (dict): Those would be data to be imprinted. This is not used - now, data are imprinted by `containerise()`. + data (dict): Those would be data to be imprinted. Returns: list(str): list of container content """ # Create directory for asset and Ayon container - root = "/Game/Ayon/Assets" asset = context.get('asset').get('name') suffix = "_CON" asset_name = f"{asset}_{name}" if asset else f"{name}" @@ -93,39 +124,22 @@ class StaticMeshAlembicLoader(plugin.Loader): tools = unreal.AssetToolsHelpers().get_asset_tools() asset_dir, container_name = tools.create_unique_asset_name( - f"{root}/{asset}/{name_version}", suffix="") + f"{self.root}/{asset}/{name_version}", suffix="") container_name += suffix if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): - unreal.EditorAssetLibrary.make_directory(asset_dir) - path = self.filepath_from_context(context) - task = self.get_task( - path, asset_dir, asset_name, False, default_conversion) - unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) # noqa: E501 + self.import_and_containerize(path, asset_dir, asset_name, + container_name, default_conversion) - # Create Asset Container - unreal_pipeline.create_container( - container=container_name, path=asset_dir) - - data = { - "schema": "ayon:container-2.0", - "id": AYON_CONTAINER_ID, - "asset": asset, - "namespace": asset_dir, - "container_name": container_name, - "asset_name": asset_name, - "loader": str(self.__class__.__name__), - "representation": context["representation"]["_id"], - "parent": context["representation"]["parent"], - "family": context["representation"]["context"]["family"] - } - unreal_pipeline.imprint(f"{asset_dir}/{container_name}", data) + self.imprint( + asset, asset_dir, container_name, asset_name, + context["representation"]) asset_content = unreal.EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=True + asset_dir, recursive=True, include_folder=False ) for a in asset_content: @@ -134,32 +148,51 @@ class StaticMeshAlembicLoader(plugin.Loader): return asset_content def update(self, container, representation): - name = container["asset_name"] - source_path = get_representation_path(representation) - destination_path = container["namespace"] + context = representation.get("context", {}) - task = self.get_task(source_path, destination_path, name, True, False) + if not context: + raise RuntimeError("No context found in representation") - # do import fbx and replace existing data - unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) + # Create directory for asset and Ayon container + asset = context.get('asset') + name = context.get('subset') + 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 + name_version = f"{name}_v{version:03d}" if version else f"{name}_hero" + tools = unreal.AssetToolsHelpers().get_asset_tools() + asset_dir, container_name = tools.create_unique_asset_name( + f"{self.root}/{asset}/{name_version}", suffix="") - container_path = "{}/{}".format(container["namespace"], - container["objectName"]) - # update metadata - unreal_pipeline.imprint( - container_path, - { - "representation": str(representation["_id"]), - "parent": str(representation["parent"]) - }) + container_name += suffix + + if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): + path = get_representation_path(representation) + + self.import_and_containerize(path, asset_dir, asset_name, + container_name) + + self.imprint( + asset, asset_dir, container_name, asset_name, representation) asset_content = unreal.EditorAssetLibrary.list_assets( - destination_path, recursive=True, include_folder=True + asset_dir, recursive=True, include_folder=False ) for a in asset_content: unreal.EditorAssetLibrary.save_asset(a) + old_assets = unreal.EditorAssetLibrary.list_assets( + container["namespace"], recursive=True, include_folder=False + ) + + replace_static_mesh_actors(old_assets, asset_content) + + unreal.EditorLevelLibrary.save_current_level() + + delete_previous_asset_if_unused(container, old_assets) + def remove(self, container): path = container["namespace"] parent_path = os.path.dirname(path) diff --git a/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py b/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py index ffc68d8375..947e0cc041 100644 --- a/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py +++ b/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py @@ -7,7 +7,12 @@ from openpype.pipeline import ( AYON_CONTAINER_ID ) from openpype.hosts.unreal.api import plugin -from openpype.hosts.unreal.api import pipeline as unreal_pipeline +from openpype.hosts.unreal.api.pipeline import ( + create_container, + imprint, + replace_static_mesh_actors, + delete_previous_asset_if_unused, +) import unreal # noqa @@ -20,6 +25,8 @@ class StaticMeshFBXLoader(plugin.Loader): icon = "cube" color = "orange" + root = "/Game/Ayon/Assets" + @staticmethod def get_task(filename, asset_dir, asset_name, replace): task = unreal.AssetImportTask() @@ -46,14 +53,39 @@ class StaticMeshFBXLoader(plugin.Loader): return task + def import_and_containerize( + self, filepath, asset_dir, asset_name, container_name + ): + unreal.EditorAssetLibrary.make_directory(asset_dir) + + task = self.get_task( + filepath, asset_dir, asset_name, False) + + unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) + + # Create Asset Container + create_container(container=container_name, path=asset_dir) + + def imprint( + self, asset, asset_dir, container_name, asset_name, representation + ): + data = { + "schema": "ayon:container-2.0", + "id": AYON_CONTAINER_ID, + "asset": asset, + "namespace": asset_dir, + "container_name": container_name, + "asset_name": asset_name, + "loader": str(self.__class__.__name__), + "representation": representation["_id"], + "parent": representation["parent"], + "family": representation["context"]["family"] + } + imprint(f"{asset_dir}/{container_name}", data) + def load(self, context, name, namespace, options): """Load and containerise representation into Content Browser. - This is two step process. First, import FBX to temporary path and - then call `containerise()` on it - this moves all content to new - directory and then it will create AssetContainer there and imprint it - with metadata. This will mark this path as container. - Args: context (dict): application context name (str): subset name @@ -61,23 +93,16 @@ class StaticMeshFBXLoader(plugin.Loader): This is not passed here, so namespace is set by `containerise()` because only then we know real path. - options (dict): Those would be data to be imprinted. This is not - used now, data are imprinted by `containerise()`. + options (dict): Those would be data to be imprinted. Returns: list(str): list of container content """ # Create directory for asset and Ayon container - root = "/Game/Ayon/Assets" - if options and options.get("asset_dir"): - root = options["asset_dir"] asset = context.get('asset').get('name') suffix = "_CON" - if asset: - asset_name = "{}_{}".format(asset, name) - else: - asset_name = "{}".format(name) + 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": @@ -87,35 +112,20 @@ class StaticMeshFBXLoader(plugin.Loader): tools = unreal.AssetToolsHelpers().get_asset_tools() asset_dir, container_name = tools.create_unique_asset_name( - f"{root}/{asset}/{name_version}", suffix="" + f"{self.root}/{asset}/{name_version}", suffix="" ) container_name += suffix - unreal.EditorAssetLibrary.make_directory(asset_dir) + if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): + path = self.filepath_from_context(context) - path = self.filepath_from_context(context) - task = self.get_task(path, asset_dir, asset_name, False) + self.import_and_containerize( + path, asset_dir, asset_name, container_name) - unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) # noqa: E501 - - # Create Asset Container - unreal_pipeline.create_container( - container=container_name, path=asset_dir) - - data = { - "schema": "ayon:container-2.0", - "id": AYON_CONTAINER_ID, - "asset": asset, - "namespace": asset_dir, - "container_name": container_name, - "asset_name": asset_name, - "loader": str(self.__class__.__name__), - "representation": context["representation"]["_id"], - "parent": context["representation"]["parent"], - "family": context["representation"]["context"]["family"] - } - unreal_pipeline.imprint(f"{asset_dir}/{container_name}", data) + self.imprint( + asset, asset_dir, container_name, asset_name, + context["representation"]) asset_content = unreal.EditorAssetLibrary.list_assets( asset_dir, recursive=True, include_folder=True @@ -127,32 +137,51 @@ class StaticMeshFBXLoader(plugin.Loader): return asset_content def update(self, container, representation): - name = container["asset_name"] - source_path = get_representation_path(representation) - destination_path = container["namespace"] + context = representation.get("context", {}) - task = self.get_task(source_path, destination_path, name, True) + if not context: + raise RuntimeError("No context found in representation") - # do import fbx and replace existing data - unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) + # Create directory for asset and Ayon container + asset = context.get('asset') + name = context.get('subset') + 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 + name_version = f"{name}_v{version:03d}" if version else f"{name}_hero" + tools = unreal.AssetToolsHelpers().get_asset_tools() + asset_dir, container_name = tools.create_unique_asset_name( + f"{self.root}/{asset}/{name_version}", suffix="") - container_path = "{}/{}".format(container["namespace"], - container["objectName"]) - # update metadata - unreal_pipeline.imprint( - container_path, - { - "representation": str(representation["_id"]), - "parent": str(representation["parent"]) - }) + container_name += suffix + + if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): + path = get_representation_path(representation) + + self.import_and_containerize( + path, asset_dir, asset_name, container_name) + + self.imprint( + asset, asset_dir, container_name, asset_name, representation) asset_content = unreal.EditorAssetLibrary.list_assets( - destination_path, recursive=True, include_folder=True + asset_dir, recursive=True, include_folder=False ) for a in asset_content: unreal.EditorAssetLibrary.save_asset(a) + old_assets = unreal.EditorAssetLibrary.list_assets( + container["namespace"], recursive=True, include_folder=False + ) + + replace_static_mesh_actors(old_assets, asset_content) + + unreal.EditorLevelLibrary.save_current_level() + + delete_previous_asset_if_unused(container, old_assets) + def remove(self, container): path = container["namespace"] parent_path = os.path.dirname(path) From ebaaa448086a7fefc7214813884d356b942947cc Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 29 Sep 2023 15:44:20 +0800 Subject: [PATCH 0084/1233] make sure tycache output filenames in representation data are correct --- .../max/plugins/publish/extract_tycache.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_tycache.py b/openpype/hosts/max/plugins/publish/extract_tycache.py index c3e9489d43..409ade8f76 100644 --- a/openpype/hosts/max/plugins/publish/extract_tycache.py +++ b/openpype/hosts/max/plugins/publish/extract_tycache.py @@ -37,7 +37,8 @@ class ExtractTyCache(publish.Extractor): stagingdir = self.staging_dir(instance) filename = "{name}.tyc".format(**instance.data) path = os.path.join(stagingdir, filename) - filenames = self.get_file(path, start, end) + filenames = self.get_file(instance, start, end) + self.log.debug(f"filenames: {filenames}") additional_attributes = instance.data.get("tyc_attrs", {}) with maintained_selection(): @@ -66,19 +67,20 @@ class ExtractTyCache(publish.Extractor): instance.data["representations"].append(representation) self.log.info(f"Extracted instance '{instance.name}' to: {filenames}") - def get_file(self, filepath, start_frame, end_frame): + def get_file(self, instance, start_frame, end_frame): """Get file names for tyFlow in tyCache format. Set the filenames accordingly to the tyCache file naming extension(.tyc) for the publishing purpose Actual File Output from tyFlow in tyCache format: - _.tyc + __tyPart_.tyc + __tyMesh.tyc - e.g. tyFlow_cloth_CCCS_blobbyFill_001_00004.tyc + e.g. tycacheMain__tyPart_00000.tyc Args: - fileapth (str): Output directory. + instance (str): instance. start_frame (int): Start frame. end_frame (int): End frame. @@ -87,13 +89,11 @@ class ExtractTyCache(publish.Extractor): """ filenames = [] - filename = os.path.basename(filepath) - orig_name, _ = os.path.splitext(filename) + # should we include frame 0 ? for frame in range(int(start_frame), int(end_frame) + 1): - actual_name = "{}_{:05}".format(orig_name, frame) - actual_filename = filepath.replace(orig_name, actual_name) - filenames.append(os.path.basename(actual_filename)) - + filename = "{}__tyPart_{:05}.tyc".format(instance.name, frame) + filenames.append(filename) + filenames.append("{}__tyMesh.tyc".format(instance.name)) return filenames def export_particle(self, members, start, end, From be353bd4395dce0450dbdd4ad6811089a1cc8b34 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 29 Sep 2023 15:47:04 +0800 Subject: [PATCH 0085/1233] remove unnecessary debug check --- openpype/hosts/max/plugins/publish/extract_tycache.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/extract_tycache.py b/openpype/hosts/max/plugins/publish/extract_tycache.py index 409ade8f76..5fa8642809 100644 --- a/openpype/hosts/max/plugins/publish/extract_tycache.py +++ b/openpype/hosts/max/plugins/publish/extract_tycache.py @@ -38,7 +38,6 @@ class ExtractTyCache(publish.Extractor): filename = "{name}.tyc".format(**instance.data) path = os.path.join(stagingdir, filename) filenames = self.get_file(instance, start, end) - self.log.debug(f"filenames: {filenames}") additional_attributes = instance.data.get("tyc_attrs", {}) with maintained_selection(): From 671844fa077ac9ef7d379f4b4bf46b3bee972537 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 29 Sep 2023 11:02:25 +0100 Subject: [PATCH 0086/1233] Improved update for Skeletal Meshes --- openpype/hosts/unreal/api/pipeline.py | 36 +++ .../plugins/load/load_skeletalmesh_abc.py | 198 +++++++++------ .../plugins/load/load_skeletalmesh_fbx.py | 233 +++++++++--------- .../plugins/load/load_staticmesh_abc.py | 1 - .../plugins/load/load_staticmesh_fbx.py | 1 - 5 files changed, 274 insertions(+), 195 deletions(-) diff --git a/openpype/hosts/unreal/api/pipeline.py b/openpype/hosts/unreal/api/pipeline.py index ae38601df2..39638ac40f 100644 --- a/openpype/hosts/unreal/api/pipeline.py +++ b/openpype/hosts/unreal/api/pipeline.py @@ -683,6 +683,42 @@ def replace_static_mesh_actors(old_assets, new_assets): smes.replace_mesh_components_meshes( static_mesh_comps, old_mesh, new_mesh) + +def replace_skeletal_mesh_actors(old_assets, new_assets): + eas = unreal.get_editor_subsystem(unreal.EditorActorSubsystem) + + comps = eas.get_all_level_actors_components() + skeletal_mesh_comps = [ + c for c in comps if isinstance(c, unreal.SkeletalMeshComponent) + ] + + # Get all the static meshes among the old assets in a dictionary with + # the name as key + old_meshes = {} + for a in old_assets: + asset = unreal.EditorAssetLibrary.load_asset(a) + if isinstance(asset, unreal.SkeletalMesh): + old_meshes[asset.get_name()] = asset + + # Get all the static meshes among the new assets in a dictionary with + # the name as key + new_meshes = {} + for a in new_assets: + asset = unreal.EditorAssetLibrary.load_asset(a) + if isinstance(asset, unreal.SkeletalMesh): + new_meshes[asset.get_name()] = asset + + for old_name, old_mesh in old_meshes.items(): + new_mesh = new_meshes.get(old_name) + + if not new_mesh: + continue + + for comp in skeletal_mesh_comps: + if comp.get_skeletal_mesh_asset() == old_mesh: + comp.set_skeletal_mesh_asset(new_mesh) + + def delete_previous_asset_if_unused(container, asset_content): ar = unreal.AssetRegistryHelpers.get_asset_registry() diff --git a/openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py b/openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py index 9285602b64..2e6557bd2d 100644 --- a/openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py +++ b/openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py @@ -7,7 +7,13 @@ from openpype.pipeline import ( AYON_CONTAINER_ID ) from openpype.hosts.unreal.api import plugin -from openpype.hosts.unreal.api import pipeline as unreal_pipeline +from openpype.hosts.unreal.api.pipeline import ( + create_container, + imprint, + replace_static_mesh_actors, + replace_skeletal_mesh_actors, + delete_previous_asset_if_unused, +) import unreal # noqa @@ -20,10 +26,12 @@ class SkeletalMeshAlembicLoader(plugin.Loader): icon = "cube" color = "orange" - def get_task(self, filename, asset_dir, asset_name, replace): + root = "/Game/Ayon/Assets" + + @staticmethod + def get_task(filename, asset_dir, asset_name, replace, default_conversion): task = unreal.AssetImportTask() options = unreal.AbcImportSettings() - sm_settings = unreal.AbcStaticMeshSettings() conversion_settings = unreal.AbcConversionSettings( preset=unreal.AbcConversionPreset.CUSTOM, flip_u=False, flip_v=False, @@ -37,72 +45,38 @@ class SkeletalMeshAlembicLoader(plugin.Loader): task.set_editor_property('automated', True) task.set_editor_property('save', True) - # set import options here - # Unreal 4.24 ignores the settings. It works with Unreal 4.26 options.set_editor_property( 'import_type', unreal.AlembicImportType.SKELETAL) - options.static_mesh_settings = sm_settings - options.conversion_settings = conversion_settings + if not default_conversion: + conversion_settings = unreal.AbcConversionSettings( + preset=unreal.AbcConversionPreset.CUSTOM, + flip_u=False, flip_v=False, + rotation=[0.0, 0.0, 0.0], + scale=[1.0, 1.0, 1.0]) + options.conversion_settings = conversion_settings + task.options = options return task - def load(self, context, name, namespace, data): - """Load and containerise representation into Content Browser. + def import_and_containerize( + self, filepath, asset_dir, asset_name, container_name, + default_conversion=False + ): + unreal.EditorAssetLibrary.make_directory(asset_dir) - This is two step process. First, import FBX to temporary path and - then call `containerise()` on it - this moves all content to new - directory and then it will create AssetContainer there and imprint it - with metadata. This will mark this path as container. + task = self.get_task( + filepath, asset_dir, asset_name, False, default_conversion) - Args: - context (dict): application context - name (str): subset name - namespace (str): in Unreal this is basically path to container. - This is not passed here, so namespace is set - by `containerise()` because only then we know - real path. - data (dict): Those would be data to be imprinted. This is not used - now, data are imprinted by `containerise()`. + unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) - Returns: - list(str): list of container content - """ - - # Create directory for asset and ayon container - root = "/Game/Ayon/Assets" - asset = context.get('asset').get('name') - suffix = "_CON" - if asset: - asset_name = "{}_{}".format(asset, name) - 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: - name_version = f"{name}_v{version.get('name'):03d}" - - tools = unreal.AssetToolsHelpers().get_asset_tools() - asset_dir, container_name = tools.create_unique_asset_name( - f"{root}/{asset}/{name_version}", suffix="") - - container_name += suffix - - if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): - unreal.EditorAssetLibrary.make_directory(asset_dir) - - path = self.filepath_from_context(context) - task = self.get_task(path, asset_dir, asset_name, False) - - unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) # noqa: E501 - - # Create Asset Container - unreal_pipeline.create_container( - container=container_name, path=asset_dir) + # Create Asset Container + create_container(container=container_name, path=asset_dir) + def imprint( + self, asset, asset_dir, container_name, asset_name, representation + ): data = { "schema": "ayon:container-2.0", "id": AYON_CONTAINER_ID, @@ -111,12 +85,57 @@ class SkeletalMeshAlembicLoader(plugin.Loader): "container_name": container_name, "asset_name": asset_name, "loader": str(self.__class__.__name__), - "representation": context["representation"]["_id"], - "parent": context["representation"]["parent"], - "family": context["representation"]["context"]["family"] + "representation": representation["_id"], + "parent": representation["parent"], + "family": representation["context"]["family"] } - unreal_pipeline.imprint( - f"{asset_dir}/{container_name}", data) + imprint(f"{asset_dir}/{container_name}", data) + + def load(self, context, name, namespace, options): + """Load and containerise representation into Content Browser. + + Args: + context (dict): application context + name (str): subset name + namespace (str): in Unreal this is basically path to container. + This is not passed here, so namespace is set + by `containerise()` because only then we know + real path. + data (dict): Those would be data to be imprinted. + + Returns: + list(str): list of container content + """ + # Create directory for asset and ayon container + asset = context.get('asset').get('name') + 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: + name_version = f"{name}_v{version.get('name'):03d}" + + default_conversion = False + if options.get("default_conversion"): + default_conversion = options.get("default_conversion") + + tools = unreal.AssetToolsHelpers().get_asset_tools() + asset_dir, container_name = tools.create_unique_asset_name( + f"{self.root}/{asset}/{name_version}", suffix="") + + container_name += suffix + + if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): + path = self.filepath_from_context(context) + + self.import_and_containerize(path, asset_dir, asset_name, + container_name, default_conversion) + + self.imprint( + asset, asset_dir, container_name, asset_name, + context["representation"]) asset_content = unreal.EditorAssetLibrary.list_assets( asset_dir, recursive=True, include_folder=True @@ -128,31 +147,52 @@ class SkeletalMeshAlembicLoader(plugin.Loader): return asset_content def update(self, container, representation): - name = container["asset_name"] - source_path = get_representation_path(representation) - destination_path = container["namespace"] + context = representation.get("context", {}) - task = self.get_task(source_path, destination_path, name, True) + if not context: + raise RuntimeError("No context found in representation") - # do import fbx and replace existing data - unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) - container_path = "{}/{}".format(container["namespace"], - container["objectName"]) - # update metadata - unreal_pipeline.imprint( - container_path, - { - "representation": str(representation["_id"]), - "parent": str(representation["parent"]) - }) + # Create directory for asset and Ayon container + asset = context.get('asset') + name = context.get('subset') + 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 + name_version = f"{name}_v{version:03d}" if version else f"{name}_hero" + tools = unreal.AssetToolsHelpers().get_asset_tools() + asset_dir, container_name = tools.create_unique_asset_name( + f"{self.root}/{asset}/{name_version}", suffix="") + + container_name += suffix + + if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): + path = get_representation_path(representation) + + self.import_and_containerize(path, asset_dir, asset_name, + container_name) + + self.imprint( + asset, asset_dir, container_name, asset_name, representation) asset_content = unreal.EditorAssetLibrary.list_assets( - destination_path, recursive=True, include_folder=True + asset_dir, recursive=True, include_folder=False ) for a in asset_content: unreal.EditorAssetLibrary.save_asset(a) + old_assets = unreal.EditorAssetLibrary.list_assets( + container["namespace"], recursive=True, include_folder=False + ) + + replace_static_mesh_actors(old_assets, asset_content) + replace_skeletal_mesh_actors(old_assets, asset_content) + + unreal.EditorLevelLibrary.save_current_level() + + delete_previous_asset_if_unused(container, old_assets) + def remove(self, container): path = container["namespace"] parent_path = os.path.dirname(path) diff --git a/openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py b/openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py index 9aa0e4d1a8..3c84f36399 100644 --- a/openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py +++ b/openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py @@ -7,7 +7,13 @@ from openpype.pipeline import ( AYON_CONTAINER_ID ) from openpype.hosts.unreal.api import plugin -from openpype.hosts.unreal.api import pipeline as unreal_pipeline +from openpype.hosts.unreal.api.pipeline import ( + create_container, + imprint, + replace_static_mesh_actors, + replace_skeletal_mesh_actors, + delete_previous_asset_if_unused, +) import unreal # noqa @@ -20,14 +26,79 @@ class SkeletalMeshFBXLoader(plugin.Loader): icon = "cube" color = "orange" + root = "/Game/Ayon/Assets" + + @staticmethod + def get_task(filename, asset_dir, asset_name, replace): + task = unreal.AssetImportTask() + options = unreal.FbxImportUI() + + task.set_editor_property('filename', filename) + task.set_editor_property('destination_path', asset_dir) + task.set_editor_property('destination_name', asset_name) + task.set_editor_property('replace_existing', replace) + task.set_editor_property('automated', True) + task.set_editor_property('save', True) + + options.set_editor_property( + 'automated_import_should_detect_type', False) + options.set_editor_property('import_as_skeletal', True) + options.set_editor_property('import_animations', False) + options.set_editor_property('import_mesh', True) + options.set_editor_property('import_materials', False) + options.set_editor_property('import_textures', False) + options.set_editor_property('skeleton', None) + options.set_editor_property('create_physics_asset', False) + + options.set_editor_property( + 'mesh_type_to_import', + unreal.FBXImportType.FBXIT_SKELETAL_MESH) + + options.skeletal_mesh_import_data.set_editor_property( + 'import_content_type', + unreal.FBXImportContentType.FBXICT_ALL) + + options.skeletal_mesh_import_data.set_editor_property( + 'normal_import_method', + unreal.FBXNormalImportMethod.FBXNIM_IMPORT_NORMALS) + + task.options = options + + return task + + def import_and_containerize( + self, filepath, asset_dir, asset_name, container_name + ): + unreal.EditorAssetLibrary.make_directory(asset_dir) + + task = self.get_task( + filepath, asset_dir, asset_name, False) + + unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) + + # Create Asset Container + create_container(container=container_name, path=asset_dir) + + def imprint( + self, asset, asset_dir, container_name, asset_name, representation + ): + data = { + "schema": "ayon:container-2.0", + "id": AYON_CONTAINER_ID, + "asset": asset, + "namespace": asset_dir, + "container_name": container_name, + "asset_name": asset_name, + "loader": str(self.__class__.__name__), + "representation": representation["_id"], + "parent": representation["parent"], + "family": representation["context"]["family"] + } + imprint(f"{asset_dir}/{container_name}", data) + def load(self, context, name, namespace, options): """Load and containerise representation into Content Browser. - This is a two step process. First, import FBX to temporary path and - then call `containerise()` on it - this moves all content to new - directory and then it will create AssetContainer there and imprint it - with metadata. This will mark this path as container. - Args: context (dict): application context name (str): subset name @@ -35,23 +106,15 @@ class SkeletalMeshFBXLoader(plugin.Loader): This is not passed here, so namespace is set by `containerise()` because only then we know real path. - options (dict): Those would be data to be imprinted. This is not - used now, data are imprinted by `containerise()`. + data (dict): Those would be data to be imprinted. Returns: list(str): list of container content - """ # Create directory for asset and Ayon container - root = "/Game/Ayon/Assets" - if options and options.get("asset_dir"): - root = options["asset_dir"] asset = context.get('asset').get('name') suffix = "_CON" - if asset: - asset_name = "{}_{}".format(asset, name) - else: - asset_name = "{}".format(name) + 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": @@ -61,67 +124,20 @@ class SkeletalMeshFBXLoader(plugin.Loader): tools = unreal.AssetToolsHelpers().get_asset_tools() asset_dir, container_name = tools.create_unique_asset_name( - f"{root}/{asset}/{name_version}", suffix="") + f"{self.root}/{asset}/{name_version}", suffix="" + ) container_name += suffix if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): - unreal.EditorAssetLibrary.make_directory(asset_dir) - - task = unreal.AssetImportTask() - path = self.filepath_from_context(context) - task.set_editor_property('filename', path) - task.set_editor_property('destination_path', asset_dir) - task.set_editor_property('destination_name', asset_name) - task.set_editor_property('replace_existing', False) - task.set_editor_property('automated', True) - task.set_editor_property('save', False) - # set import options here - options = unreal.FbxImportUI() - options.set_editor_property('import_as_skeletal', True) - options.set_editor_property('import_animations', False) - options.set_editor_property('import_mesh', True) - options.set_editor_property('import_materials', False) - options.set_editor_property('import_textures', False) - options.set_editor_property('skeleton', None) - options.set_editor_property('create_physics_asset', False) + self.import_and_containerize( + path, asset_dir, asset_name, container_name) - options.set_editor_property( - 'mesh_type_to_import', - unreal.FBXImportType.FBXIT_SKELETAL_MESH) - - options.skeletal_mesh_import_data.set_editor_property( - 'import_content_type', - unreal.FBXImportContentType.FBXICT_ALL) - # set to import normals, otherwise Unreal will compute them - # and it will take a long time, depending on the size of the mesh - options.skeletal_mesh_import_data.set_editor_property( - 'normal_import_method', - unreal.FBXNormalImportMethod.FBXNIM_IMPORT_NORMALS) - - task.options = options - unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) # noqa: E501 - - # Create Asset Container - unreal_pipeline.create_container( - container=container_name, path=asset_dir) - - data = { - "schema": "ayon:container-2.0", - "id": AYON_CONTAINER_ID, - "asset": asset, - "namespace": asset_dir, - "container_name": container_name, - "asset_name": asset_name, - "loader": str(self.__class__.__name__), - "representation": context["representation"]["_id"], - "parent": context["representation"]["parent"], - "family": context["representation"]["context"]["family"] - } - unreal_pipeline.imprint( - f"{asset_dir}/{container_name}", data) + self.imprint( + asset, asset_dir, container_name, asset_name, + context["representation"]) asset_content = unreal.EditorAssetLibrary.list_assets( asset_dir, recursive=True, include_folder=True @@ -133,63 +149,52 @@ class SkeletalMeshFBXLoader(plugin.Loader): return asset_content def update(self, container, representation): - name = container["asset_name"] - source_path = get_representation_path(representation) - destination_path = container["namespace"] + context = representation.get("context", {}) - task = unreal.AssetImportTask() + if not context: + raise RuntimeError("No context found in representation") - task.set_editor_property('filename', source_path) - task.set_editor_property('destination_path', destination_path) - task.set_editor_property('destination_name', name) - task.set_editor_property('replace_existing', True) - task.set_editor_property('automated', True) - task.set_editor_property('save', True) + # Create directory for asset and Ayon container + asset = context.get('asset') + name = context.get('subset') + 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 + name_version = f"{name}_v{version:03d}" if version else f"{name}_hero" + tools = unreal.AssetToolsHelpers().get_asset_tools() + asset_dir, container_name = tools.create_unique_asset_name( + f"{self.root}/{asset}/{name_version}", suffix="") - # set import options here - options = unreal.FbxImportUI() - options.set_editor_property('import_as_skeletal', True) - options.set_editor_property('import_animations', False) - options.set_editor_property('import_mesh', True) - options.set_editor_property('import_materials', True) - options.set_editor_property('import_textures', True) - options.set_editor_property('skeleton', None) - options.set_editor_property('create_physics_asset', False) + container_name += suffix - options.set_editor_property('mesh_type_to_import', - unreal.FBXImportType.FBXIT_SKELETAL_MESH) + if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): + path = get_representation_path(representation) - options.skeletal_mesh_import_data.set_editor_property( - 'import_content_type', - unreal.FBXImportContentType.FBXICT_ALL - ) - # set to import normals, otherwise Unreal will compute them - # and it will take a long time, depending on the size of the mesh - options.skeletal_mesh_import_data.set_editor_property( - 'normal_import_method', - unreal.FBXNormalImportMethod.FBXNIM_IMPORT_NORMALS - ) + self.import_and_containerize( + path, asset_dir, asset_name, container_name) - task.options = options - # do import fbx and replace existing data - unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) # noqa: E501 - container_path = "{}/{}".format(container["namespace"], - container["objectName"]) - # update metadata - unreal_pipeline.imprint( - container_path, - { - "representation": str(representation["_id"]), - "parent": str(representation["parent"]) - }) + self.imprint( + asset, asset_dir, container_name, asset_name, representation) asset_content = unreal.EditorAssetLibrary.list_assets( - destination_path, recursive=True, include_folder=True + asset_dir, recursive=True, include_folder=False ) for a in asset_content: unreal.EditorAssetLibrary.save_asset(a) + old_assets = unreal.EditorAssetLibrary.list_assets( + container["namespace"], recursive=True, include_folder=False + ) + + replace_static_mesh_actors(old_assets, asset_content) + replace_skeletal_mesh_actors(old_assets, asset_content) + + unreal.EditorLevelLibrary.save_current_level() + + delete_previous_asset_if_unused(container, old_assets) + def remove(self, container): path = container["namespace"] parent_path = os.path.dirname(path) diff --git a/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py b/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py index 13c4ec23e9..cc7aed7b93 100644 --- a/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py +++ b/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py @@ -105,7 +105,6 @@ class StaticMeshAlembicLoader(plugin.Loader): Returns: list(str): list of container content - """ # Create directory for asset and Ayon container asset = context.get('asset').get('name') diff --git a/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py b/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py index 947e0cc041..0aac69b57b 100644 --- a/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py +++ b/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py @@ -98,7 +98,6 @@ class StaticMeshFBXLoader(plugin.Loader): Returns: list(str): list of container content """ - # Create directory for asset and Ayon container asset = context.get('asset').get('name') suffix = "_CON" From 759dc59132bc32ce07b6f204c8da7df8ae9f2715 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 29 Sep 2023 17:33:27 +0200 Subject: [PATCH 0087/1233] add clip to timeline in correct place --- openpype/hosts/resolve/api/lib.py | 9 ++++++++- openpype/hosts/resolve/api/plugin.py | 10 +++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index eaee3bb9ba..4e8b3a4107 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -246,7 +246,8 @@ def get_media_pool_item(fpath, root: object = None) -> object: def create_timeline_item(media_pool_item: object, timeline: object = None, source_start: int = None, - source_end: int = None) -> object: + source_end: int = None, + timeline_in: int = None) -> object: """ Add media pool item to current or defined timeline. @@ -278,6 +279,12 @@ def create_timeline_item(media_pool_item: object, clip_data.update({"endFrame": source_end}) print(clip_data) + + if timeline_in: + timeline_start = timeline.GetStartFrame() + # Create a clipInfo dictionary with the necessary information + clip_data["recordFrame"] = int(timeline_start + timeline_in) + # add to timeline media_pool.AppendToTimeline([clip_data]) diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py index e2bd76ffa2..c679aa062d 100644 --- a/openpype/hosts/resolve/api/plugin.py +++ b/openpype/hosts/resolve/api/plugin.py @@ -402,6 +402,9 @@ class ClipLoader: if handle_end is None: handle_end = int(self.data["assetData"]["handleEnd"]) + self.timeline_in = int(self.data["assetData"]["clipIn"]) + + source_in = int(_clip_property("Start")) source_out = int(_clip_property("End")) @@ -416,7 +419,12 @@ class ClipLoader: # make track item from source in bin as item timeline_item = lib.create_timeline_item( - media_pool_item, self.active_timeline, source_in, source_out) + media_pool_item, + self.active_timeline, + source_in, + source_out, + self.timeline_in + ) print("Loading clips: `{}`".format(self.data["clip_name"])) return timeline_item From 7cd8be0afa68005bc615523fd4a0ee55f73e1a30 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 2 Oct 2023 11:49:51 +0200 Subject: [PATCH 0088/1233] resolve: add option for adding clips sequentially - or to asset define place - also create track with a name --- openpype/hosts/resolve/api/lib.py | 12 +++--- openpype/hosts/resolve/api/plugin.py | 55 +++++++++++++++++++++++++++- 2 files changed, 58 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index 4e8b3a4107..8f7eba8a90 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -274,17 +274,15 @@ def create_timeline_item(media_pool_item: object, # add source time range if input was given if source_start is not None: - clip_data.update({"startFrame": source_start}) + clip_data["startFrame"] = source_start if source_end is not None: - clip_data.update({"endFrame": source_end}) + clip_data["endFrame"] = source_end + + # Create a clipInfo dictionary with the necessary information + clip_data["recordFrame"] = timeline_in print(clip_data) - if timeline_in: - timeline_start = timeline.GetStartFrame() - # Create a clipInfo dictionary with the necessary information - clip_data["recordFrame"] = int(timeline_start + timeline_in) - # add to timeline media_pool.AppendToTimeline([clip_data]) diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py index c679aa062d..b1bde212fe 100644 --- a/openpype/hosts/resolve/api/plugin.py +++ b/openpype/hosts/resolve/api/plugin.py @@ -312,6 +312,9 @@ class ClipLoader: # try to get value from options or evaluate key value for `load_to` self.new_timeline = options.get("newTimeline") or bool( "New timeline" in options.get("load_to", "")) + # try to get value from options or evaluate key value for `load_how` + self.sequential_load = options.get("sequentially") or bool( + "Sequentially in order" in options.get("load_how", "")) assert self._populate_data(), str( "Cannot Load selected data, look into database " @@ -352,6 +355,7 @@ class ClipLoader: asset = str(repr_cntx["asset"]) subset = str(repr_cntx["subset"]) representation = str(repr_cntx["representation"]) + self.data["track_name"] = "{}_{}".format(asset, representation) self.data["clip_name"] = "_".join([asset, subset, representation]) self.data["versionData"] = self.context["version"]["data"] # gets file path @@ -383,6 +387,33 @@ class ClipLoader: asset_name = self.context["representation"]["context"]["asset"] self.data["assetData"] = get_current_project_asset(asset_name)["data"] + def _set_active_track(self): + """ Set active track to `track` """ + track_type = "video" + track_name = self.data["track_name"] + track_exists = False + + # get total track count + track_count = self.active_timeline.GetTrackCount(track_type) + # loop all tracks by track indexes + for track_index in range(1, int(track_count) + 1): + # get current track name + _track_name = self.active_timeline.GetTrackName( + track_type, track_index) + if track_name != _track_name: + continue + track_exists = True + break + + if not track_exists: + self.active_timeline.AddTrack(track_type) + self.active_timeline.SetTrackName( + track_type, + track_index + 1, + track_name + ) + + def load(self): # create project bin for the media to be imported into self.active_bin = lib.create_bin(self.data["binPath"]) @@ -402,8 +433,18 @@ class ClipLoader: if handle_end is None: handle_end = int(self.data["assetData"]["handleEnd"]) - self.timeline_in = int(self.data["assetData"]["clipIn"]) + # handle timeline tracks + self._set_active_track() + # get timeline in + timeline_start = self.active_timeline.GetStartFrame() + if self.sequential_load: + # set timeline start frame + timeline_in = int(timeline_start) + else: + # set timeline start frame + original clip in frame + timeline_in = int( + timeline_start + self.data["assetData"]["clipIn"]) source_in = int(_clip_property("Start")) source_out = int(_clip_property("End")) @@ -423,7 +464,7 @@ class ClipLoader: self.active_timeline, source_in, source_out, - self.timeline_in + timeline_in ) print("Loading clips: `{}`".format(self.data["clip_name"])) @@ -478,6 +519,16 @@ class TimelineItemLoader(LoaderPlugin): ], default=0, help="Where do you want clips to be loaded?" + ), + qargparse.Choice( + "load_how", + label="How to load clips", + items=[ + "Original timing", + "Sequentially in order" + ], + default="Original timing", + help="Would you like to place it at original timing?" ) ] From ec893d45e3e47ac95ec6f9d3ba16513c70009a78 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 2 Oct 2023 11:50:10 +0200 Subject: [PATCH 0089/1233] updating docs readme to latest python api --- ...0.4.txt => RESOLVE_API_v18.5.1-build6.txt} | 81 +++++++++++++++---- 1 file changed, 65 insertions(+), 16 deletions(-) rename openpype/hosts/resolve/{RESOLVE_API_v18.0.4.txt => RESOLVE_API_v18.5.1-build6.txt} (89%) diff --git a/openpype/hosts/resolve/RESOLVE_API_v18.0.4.txt b/openpype/hosts/resolve/RESOLVE_API_v18.5.1-build6.txt similarity index 89% rename from openpype/hosts/resolve/RESOLVE_API_v18.0.4.txt rename to openpype/hosts/resolve/RESOLVE_API_v18.5.1-build6.txt index 98597a12cb..7d1d6edf61 100644 --- a/openpype/hosts/resolve/RESOLVE_API_v18.0.4.txt +++ b/openpype/hosts/resolve/RESOLVE_API_v18.5.1-build6.txt @@ -1,4 +1,4 @@ -Updated as of 9 May 2022 +Updated as of 26 May 2023 ---------------------------- In this package, you will find a brief introduction to the Scripting API for DaVinci Resolve Studio. Apart from this README.txt file, this package contains folders containing the basic import modules for scripting access (DaVinciResolve.py) and some representative examples. @@ -19,7 +19,7 @@ DaVinci Resolve scripting requires one of the following to be installed (for all Lua 5.1 Python 2.7 64-bit - Python 3.6 64-bit + Python >= 3.6 64-bit Using a script @@ -171,6 +171,10 @@ Project GetRenderResolutions(format, codec) --> [{Resolution}] # Returns list of resolutions applicable for the given render format (string) and render codec (string). Returns full list of resolutions if no argument is provided. Each element in the list is a dictionary with 2 keys "Width" and "Height". RefreshLUTList() --> Bool # Refreshes LUT List GetUniqueId() --> string # Returns a unique ID for the project item + InsertAudioToCurrentTrackAtPlayhead(mediaPath, --> Bool # Inserts the media specified by mediaPath (string) with startOffsetInSamples (int) and durationInSamples (int) at the playhead on a selected track on the Fairlight page. Returns True if successful, otherwise False. + startOffsetInSamples, durationInSamples) + LoadBurnInPreset(presetName) --> Bool # Loads user defined data burn in preset for project when supplied presetName (string). Returns true if successful. + ExportCurrentFrameAsStill(filePath) --> Bool # Exports current frame as still to supplied filePath. filePath must end in valid export file format. Returns True if succssful, False otherwise. MediaStorage GetMountedVolumeList() --> [paths...] # Returns list of folder paths corresponding to mounted volumes displayed in Resolve’s Media Storage. @@ -179,6 +183,7 @@ MediaStorage RevealInStorage(path) --> Bool # Expands and displays given file/folder path in Resolve’s Media Storage. AddItemListToMediaPool(item1, item2, ...) --> [clips...] # Adds specified file/folder paths from Media Storage into current Media Pool folder. Input is one or more file/folder paths. Returns a list of the MediaPoolItems created. AddItemListToMediaPool([items...]) --> [clips...] # Adds specified file/folder paths from Media Storage into current Media Pool folder. Input is an array of file/folder paths. Returns a list of the MediaPoolItems created. + AddItemListToMediaPool([{itemInfo}, ...]) --> [clips...] # Adds list of itemInfos specified as dict of "media", "startFrame" (int), "endFrame" (int) from Media Storage into current Media Pool folder. Returns a list of the MediaPoolItems created. AddClipMattesToMediaPool(MediaPoolItem, [paths], stereoEye) --> Bool # Adds specified media files as mattes for the specified MediaPoolItem. StereoEye is an optional argument for specifying which eye to add the matte to for stereo clips ("left" or "right"). Returns True if successful. AddTimelineMattesToMediaPool([paths]) --> [MediaPoolItems] # Adds specified media files as timeline mattes in current media pool folder. Returns a list of created MediaPoolItems. @@ -189,20 +194,22 @@ MediaPool CreateEmptyTimeline(name) --> Timeline # Adds new timeline with given name. AppendToTimeline(clip1, clip2, ...) --> [TimelineItem] # Appends specified MediaPoolItem objects in the current timeline. Returns the list of appended timelineItems. AppendToTimeline([clips]) --> [TimelineItem] # Appends specified MediaPoolItem objects in the current timeline. Returns the list of appended timelineItems. - AppendToTimeline([{clipInfo}, ...]) --> [TimelineItem] # Appends list of clipInfos specified as dict of "mediaPoolItem", "startFrame" (int), "endFrame" (int), (optional) "mediaType" (int; 1 - Video only, 2 - Audio only). Returns the list of appended timelineItems. + AppendToTimeline([{clipInfo}, ...]) --> [TimelineItem] # Appends list of clipInfos specified as dict of "mediaPoolItem", "startFrame" (int), "endFrame" (int), (optional) "mediaType" (int; 1 - Video only, 2 - Audio only), "trackIndex" (int) and "recordFrame" (int). Returns the list of appended timelineItems. CreateTimelineFromClips(name, clip1, clip2,...) --> Timeline # Creates new timeline with specified name, and appends the specified MediaPoolItem objects. CreateTimelineFromClips(name, [clips]) --> Timeline # Creates new timeline with specified name, and appends the specified MediaPoolItem objects. - CreateTimelineFromClips(name, [{clipInfo}]) --> Timeline # Creates new timeline with specified name, appending the list of clipInfos specified as a dict of "mediaPoolItem", "startFrame" (int), "endFrame" (int). - ImportTimelineFromFile(filePath, {importOptions}) --> Timeline # Creates timeline based on parameters within given file and optional importOptions dict, with support for the keys: - # "timelineName": string, specifies the name of the timeline to be created - # "importSourceClips": Bool, specifies whether source clips should be imported, True by default + CreateTimelineFromClips(name, [{clipInfo}]) --> Timeline # Creates new timeline with specified name, appending the list of clipInfos specified as a dict of "mediaPoolItem", "startFrame" (int), "endFrame" (int), "recordFrame" (int). + ImportTimelineFromFile(filePath, {importOptions}) --> Timeline # Creates timeline based on parameters within given file (AAF/EDL/XML/FCPXML/DRT/ADL) and optional importOptions dict, with support for the keys: + # "timelineName": string, specifies the name of the timeline to be created. Not valid for DRT import + # "importSourceClips": Bool, specifies whether source clips should be imported, True by default. Not valid for DRT import # "sourceClipsPath": string, specifies a filesystem path to search for source clips if the media is inaccessible in their original path and if "importSourceClips" is True - # "sourceClipsFolders": List of Media Pool folder objects to search for source clips if the media is not present in current folder and if "importSourceClips" is False + # "sourceClipsFolders": List of Media Pool folder objects to search for source clips if the media is not present in current folder and if "importSourceClips" is False. Not valid for DRT import # "interlaceProcessing": Bool, specifies whether to enable interlace processing on the imported timeline being created. valid only for AAF import DeleteTimelines([timeline]) --> Bool # Deletes specified timelines in the media pool. GetCurrentFolder() --> Folder # Returns currently selected Folder. SetCurrentFolder(Folder) --> Bool # Sets current folder by given Folder. DeleteClips([clips]) --> Bool # Deletes specified clips or timeline mattes in the media pool + ImportFolderFromFile(filePath, sourceClipsPath="") --> Bool # Returns true if import from given DRB filePath is successful, false otherwise + # sourceClipsPath is a string that specifies a filesystem path to search for source clips if the media is inaccessible in their original path, empty by default DeleteFolders([subfolders]) --> Bool # Deletes specified subfolders in the media pool MoveClips([clips], targetFolder) --> Bool # Moves specified clips to target folder. MoveFolders([folders], targetFolder) --> Bool # Moves specified folders to target folder. @@ -225,6 +232,7 @@ Folder GetSubFolderList() --> [folders...] # Returns a list of subfolders in the folder. GetIsFolderStale() --> bool # Returns true if folder is stale in collaboration mode, false otherwise GetUniqueId() --> string # Returns a unique ID for the media pool folder + Export(filePath) --> bool # Returns true if export of DRB folder to filePath is successful, false otherwise MediaPoolItem GetName() --> string # Returns the clip name. @@ -257,6 +265,8 @@ MediaPoolItem UnlinkProxyMedia() --> Bool # Unlinks any proxy media associated with clip. ReplaceClip(filePath) --> Bool # Replaces the underlying asset and metadata of MediaPoolItem with the specified absolute clip path. GetUniqueId() --> string # Returns a unique ID for the media pool item + TranscribeAudio() --> Bool # Transcribes audio of the MediaPoolItem. Returns True if successful; False otherwise + ClearTranscription() --> Bool # Clears audio transcription of the MediaPoolItem. Returns True if successful; False otherwise. Timeline GetName() --> string # Returns the timeline name. @@ -266,6 +276,23 @@ Timeline SetStartTimecode(timecode) --> Bool # Set the start timecode of the timeline to the string 'timecode'. Returns true when the change is successful, false otherwise. GetStartTimecode() --> string # Returns the start timecode for the timeline. GetTrackCount(trackType) --> int # Returns the number of tracks for the given track type ("audio", "video" or "subtitle"). + AddTrack(trackType, optionalSubTrackType) --> Bool # Adds track of trackType ("video", "subtitle", "audio"). Second argument optionalSubTrackType is required for "audio" + # optionalSubTrackType can be one of {"mono", "stereo", "5.1", "5.1film", "7.1", "7.1film", "adaptive1", ... , "adaptive24"} + DeleteTrack(trackType, trackIndex) --> Bool # Deletes track of trackType ("video", "subtitle", "audio") and given trackIndex. 1 <= trackIndex <= GetTrackCount(trackType). + SetTrackEnable(trackType, trackIndex, Bool) --> Bool # Enables/Disables track with given trackType and trackIndex + # trackType is one of {"audio", "video", "subtitle"} + # 1 <= trackIndex <= GetTrackCount(trackType). + GetIsTrackEnabled(trackType, trackIndex) --> Bool # Returns True if track with given trackType and trackIndex is enabled and False otherwise. + # trackType is one of {"audio", "video", "subtitle"} + # 1 <= trackIndex <= GetTrackCount(trackType). + SetTrackLock(trackType, trackIndex, Bool) --> Bool # Locks/Unlocks track with given trackType and trackIndex + # trackType is one of {"audio", "video", "subtitle"} + # 1 <= trackIndex <= GetTrackCount(trackType). + GetIsTrackLocked(trackType, trackIndex) --> Bool # Returns True if track with given trackType and trackIndex is locked and False otherwise. + # trackType is one of {"audio", "video", "subtitle"} + # 1 <= trackIndex <= GetTrackCount(trackType). + DeleteClips([timelineItems], Bool) --> Bool # Deletes specified TimelineItems from the timeline, performing ripple delete if the second argument is True. Second argument is optional (The default for this is False) + SetClipsLinked([timelineItems], Bool) --> Bool # Links or unlinks the specified TimelineItems depending on second argument. GetItemListInTrack(trackType, index) --> [items...] # Returns a list of timeline items on that track (based on trackType and index). 1 <= index <= GetTrackCount(trackType). AddMarker(frameId, color, name, note, duration, --> Bool # Creates a new marker at given frameId position and with given marker information. 'customData' is optional and helps to attach user specific data to the marker. customData) @@ -301,7 +328,7 @@ Timeline # "sourceClipsFolders": string, list of Media Pool folder objects to search for source clips if the media is not present in current folder Export(fileName, exportType, exportSubtype) --> Bool # Exports timeline to 'fileName' as per input exportType & exportSubtype format. - # Refer to section "Looking up timeline exports properties" for information on the parameters. + # Refer to section "Looking up timeline export properties" for information on the parameters. GetSetting(settingName) --> string # Returns value of timeline setting (indicated by settingName : string). Check the section below for more information. SetSetting(settingName, settingValue) --> Bool # Sets timeline setting (indicated by settingName : string) to the value (settingValue : string). Check the section below for more information. InsertGeneratorIntoTimeline(generatorName) --> TimelineItem # Inserts a generator (indicated by generatorName : string) into the timeline. @@ -313,6 +340,8 @@ Timeline GrabStill() --> galleryStill # Grabs still from the current video clip. Returns a GalleryStill object. GrabAllStills(stillFrameSource) --> [galleryStill] # Grabs stills from all the clips of the timeline at 'stillFrameSource' (1 - First frame, 2 - Middle frame). Returns the list of GalleryStill objects. GetUniqueId() --> string # Returns a unique ID for the timeline + CreateSubtitlesFromAudio() --> Bool # Creates subtitles from audio for the timeline. Returns True on success, False otherwise. + DetectSceneCuts() --> Bool # Detects and makes scene cuts along the timeline. Returns True if successful, False otherwise. TimelineItem GetName() --> string # Returns the item name. @@ -362,6 +391,7 @@ TimelineItem GetStereoLeftFloatingWindowParams() --> {keyframes...} # For the LEFT eye -> returns a dict (offset -> dict) of keyframe offsets and respective floating window params. Value at particular offset includes the left, right, top and bottom floating window values. GetStereoRightFloatingWindowParams() --> {keyframes...} # For the RIGHT eye -> returns a dict (offset -> dict) of keyframe offsets and respective floating window params. Value at particular offset includes the left, right, top and bottom floating window values. GetNumNodes() --> int # Returns the number of nodes in the current graph for the timeline item + ApplyArriCdlLut() --> Bool # Applies ARRI CDL and LUT. Returns True if successful, False otherwise. SetLUT(nodeIndex, lutPath) --> Bool # Sets LUT on the node mapping the node index provided, 1 <= nodeIndex <= total number of nodes. # The lutPath can be an absolute path, or a relative path (based off custom LUT paths or the master LUT path). # The operation is successful for valid lut paths that Resolve has already discovered (see Project.RefreshLUTList). @@ -376,8 +406,16 @@ TimelineItem SelectTakeByIndex(idx) --> Bool # Selects a take by index, 1 <= idx <= number of takes. FinalizeTake() --> Bool # Finalizes take selection. CopyGrades([tgtTimelineItems]) --> Bool # Copies the current grade to all the items in tgtTimelineItems list. Returns True on success and False if any error occurred. + SetClipEnabled(Bool) --> Bool # Sets clip enabled based on argument. + GetClipEnabled() --> Bool # Gets clip enabled status. UpdateSidecar() --> Bool # Updates sidecar file for BRAW clips or RMD file for R3D clips. GetUniqueId() --> string # Returns a unique ID for the timeline item + LoadBurnInPreset(presetName) --> Bool # Loads user defined data burn in preset for clip when supplied presetName (string). Returns true if successful. + GetNodeLabel(nodeIndex) --> string # Returns the label of the node at nodeIndex. + CreateMagicMask(mode) --> Bool # Returns True if magic mask was created successfully, False otherwise. mode can "F" (forward), "B" (backward), or "BI" (bidirection) + RegenerateMagicMask() --> Bool # Returns True if magic mask was regenerated successfully, False otherwise. + Stabilize() --> Bool # Returns True if stabilization was successful, False otherwise + SmartReframe() --> Bool # Performs Smart Reframe. Returns True if successful, False otherwise. Gallery GetAlbumName(galleryStillAlbum) --> string # Returns the name of the GalleryStillAlbum object 'galleryStillAlbum'. @@ -422,9 +460,11 @@ Invoke "Project:SetSetting", "Timeline:SetSetting" or "MediaPoolItem:SetClipProp ensure the success of the operation. You can troubleshoot the validity of keys and values by setting the desired result from the UI and checking property snapshots before and after the change. The following Project properties have specifically enumerated values: -"superScale" - the property value is an enumerated integer between 0 and 3 with these meanings: 0=Auto, 1=no scaling, and 2, 3 and 4 represent the Super Scale multipliers 2x, 3x and 4x. +"superScale" - the property value is an enumerated integer between 0 and 4 with these meanings: 0=Auto, 1=no scaling, and 2, 3 and 4 represent the Super Scale multipliers 2x, 3x and 4x. + for super scale multiplier '2x Enhanced', exactly 4 arguments must be passed as outlined below. If less than 4 arguments are passed, it will default to 2x. Affects: • x = Project:GetSetting('superScale') and Project:SetSetting('superScale', x) +• for '2x Enhanced' --> Project:SetSetting('superScale', 2, sharpnessValue, noiseReductionValue), where sharpnessValue is a float in the range [0.0, 1.0] and noiseReductionValue is a float in the range [0.0, 1.0] "timelineFrameRate" - the property value is one of the frame rates available to the user in project settings under "Timeline frame rate" option. Drop Frame can be configured for supported frame rates by appending the frame rate with "DF", e.g. "29.97 DF" will enable drop frame and "29.97" will disable drop frame @@ -432,9 +472,11 @@ Affects: • x = Project:GetSetting('timelineFrameRate') and Project:SetSetting('timelineFrameRate', x) The following Clip properties have specifically enumerated values: -"superScale" - the property value is an enumerated integer between 1 and 3 with these meanings: 1=no scaling, and 2, 3 and 4 represent the Super Scale multipliers 2x, 3x and 4x. +"Super Scale" - the property value is an enumerated integer between 1 and 4 with these meanings: 1=no scaling, and 2, 3 and 4 represent the Super Scale multipliers 2x, 3x and 4x. + for super scale multiplier '2x Enhanced', exactly 4 arguments must be passed as outlined below. If less than 4 arguments are passed, it will default to 2x. Affects: • x = MediaPoolItem:GetClipProperty('Super Scale') and MediaPoolItem:SetClipProperty('Super Scale', x) +• for '2x Enhanced' --> MediaPoolItem:SetClipProperty('Super Scale', 2, sharpnessValue, noiseReductionValue), where sharpnessValue is a float in the range [0.0, 1.0] and noiseReductionValue is a float in the range [0.0, 1.0] Looking up Render Settings @@ -478,11 +520,6 @@ exportType can be one of the following constants: - resolve.EXPORT_DRT - resolve.EXPORT_EDL - resolve.EXPORT_FCP_7_XML - - resolve.EXPORT_FCPXML_1_3 - - resolve.EXPORT_FCPXML_1_4 - - resolve.EXPORT_FCPXML_1_5 - - resolve.EXPORT_FCPXML_1_6 - - resolve.EXPORT_FCPXML_1_7 - resolve.EXPORT_FCPXML_1_8 - resolve.EXPORT_FCPXML_1_9 - resolve.EXPORT_FCPXML_1_10 @@ -492,6 +529,8 @@ exportType can be one of the following constants: - resolve.EXPORT_TEXT_TAB - resolve.EXPORT_DOLBY_VISION_VER_2_9 - resolve.EXPORT_DOLBY_VISION_VER_4_0 + - resolve.EXPORT_DOLBY_VISION_VER_5_1 + - resolve.EXPORT_OTIO exportSubtype can be one of the following enums: - resolve.EXPORT_NONE - resolve.EXPORT_AAF_NEW @@ -504,6 +543,16 @@ When exportType is resolve.EXPORT_AAF, valid exportSubtype values are resolve.EX When exportType is resolve.EXPORT_EDL, valid exportSubtype values are resolve.EXPORT_CDL, resolve.EXPORT_SDL, resolve.EXPORT_MISSING_CLIPS and resolve.EXPORT_NONE. Note: Replace 'resolve.' when using the constants above, if a different Resolve class instance name is used. +Unsupported exportType types +--------------------------------- +Starting with DaVinci Resolve 18.1, the following export types are not supported: + - resolve.EXPORT_FCPXML_1_3 + - resolve.EXPORT_FCPXML_1_4 + - resolve.EXPORT_FCPXML_1_5 + - resolve.EXPORT_FCPXML_1_6 + - resolve.EXPORT_FCPXML_1_7 + + Looking up Timeline item properties ----------------------------------- This section covers additional notes for the function "TimelineItem:SetProperty" and "TimelineItem:GetProperty". These functions are used to get and set properties mentioned. From fac33119ec0e7a7087b053b95711b85dd13dc791 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 2 Oct 2023 13:28:57 +0200 Subject: [PATCH 0090/1233] Enable only in Fusion 18.5+ --- openpype/hosts/fusion/plugins/load/load_usd.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/openpype/hosts/fusion/plugins/load/load_usd.py b/openpype/hosts/fusion/plugins/load/load_usd.py index f12fbd5ed0..beab0c8ecf 100644 --- a/openpype/hosts/fusion/plugins/load/load_usd.py +++ b/openpype/hosts/fusion/plugins/load/load_usd.py @@ -7,6 +7,7 @@ from openpype.hosts.fusion.api import ( get_current_comp, comp_lock_and_undo_chunk ) +from openpype.hosts.fusion.api.lib import get_fusion_module class FusionLoadUSD(load.LoaderPlugin): @@ -26,6 +27,19 @@ class FusionLoadUSD(load.LoaderPlugin): tool_type = "uLoader" + @classmethod + def apply_settings(cls, project_settings, system_settings): + super(FusionLoadUSD, cls).apply_settings(project_settings, + system_settings) + if cls.enabled: + # Enable only in Fusion 18.5+ + fusion = get_fusion_module() + version = fusion.GetVersion() + major = version[1] + minor = version[2] + is_usd_supported = (major, minor) >= (18, 5) + cls.enabled = is_usd_supported + def load(self, context, name, namespace, data): # Fallback to asset name when namespace is None if namespace is None: From 277a0baa7bbe89b2a58fbe9568271e4d5f72e86b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 2 Oct 2023 13:29:49 +0200 Subject: [PATCH 0091/1233] Hound --- openpype/hosts/fusion/plugins/load/load_usd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/load/load_usd.py b/openpype/hosts/fusion/plugins/load/load_usd.py index beab0c8ecf..4f1813a646 100644 --- a/openpype/hosts/fusion/plugins/load/load_usd.py +++ b/openpype/hosts/fusion/plugins/load/load_usd.py @@ -29,7 +29,7 @@ class FusionLoadUSD(load.LoaderPlugin): @classmethod def apply_settings(cls, project_settings, system_settings): - super(FusionLoadUSD, cls).apply_settings(project_settings, + super(FusionLoadUSD, cls).apply_settings(project_settings, system_settings) if cls.enabled: # Enable only in Fusion 18.5+ From df5fc6154dcd26cda86247e484c4798aae4a099f Mon Sep 17 00:00:00 2001 From: Ember Light <49758407+EmberLightVFX@users.noreply.github.com> Date: Mon, 2 Oct 2023 14:23:42 +0200 Subject: [PATCH 0092/1233] Change name from Validate Saver to Validate Asset Co-authored-by: Roy Nieterau --- .../hosts/fusion/plugins/publish/validate_saver_resolution.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/publish/validate_saver_resolution.py b/openpype/hosts/fusion/plugins/publish/validate_saver_resolution.py index b43a5023fa..efa7295d11 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_saver_resolution.py +++ b/openpype/hosts/fusion/plugins/publish/validate_saver_resolution.py @@ -63,7 +63,7 @@ class ValidateSaverResolution( """Validate that the saver input resolution matches the asset resolution""" order = pyblish.api.ValidatorOrder - label = "Validate Saver Resolution" + label = "Validate Asset Resolution" families = ["render"] hosts = ["fusion"] optional = True From 333c282eba0467883e3709245e8f0b763537155c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 2 Oct 2023 14:24:02 +0200 Subject: [PATCH 0093/1233] Don't query comp again --- openpype/hosts/fusion/plugins/load/load_sequence.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/load/load_sequence.py b/openpype/hosts/fusion/plugins/load/load_sequence.py index fde5b27e70..4401af97eb 100644 --- a/openpype/hosts/fusion/plugins/load/load_sequence.py +++ b/openpype/hosts/fusion/plugins/load/load_sequence.py @@ -161,7 +161,6 @@ class FusionLoadSequence(load.LoaderPlugin): with comp_lock_and_undo_chunk(comp, "Create Loader"): args = (-32768, -32768) tool = comp.AddTool("Loader", *args) - comp = get_current_comp() tool["Clip"] = comp.ReverseMapPath(path) # Set global in point to start frame (if in version.data) From ffeb0282b93b05a1d345bfa39928c195d5f1ff74 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Mon, 2 Oct 2023 14:39:27 +0200 Subject: [PATCH 0094/1233] Restore formatting of non-modified code --- openpype/hosts/fusion/api/lib.py | 75 ++++++++++++-------------------- 1 file changed, 28 insertions(+), 47 deletions(-) diff --git a/openpype/hosts/fusion/api/lib.py b/openpype/hosts/fusion/api/lib.py index 19db484856..8a18080393 100644 --- a/openpype/hosts/fusion/api/lib.py +++ b/openpype/hosts/fusion/api/lib.py @@ -21,15 +21,8 @@ from openpype.pipeline.context_tools import get_current_project_asset self = sys.modules[__name__] self._project = None - -def update_frame_range( - start, - end, - comp=None, - set_render_range=True, - handle_start=0, - handle_end=0, -): +def update_frame_range(start, end, comp=None, set_render_range=True, + handle_start=0, handle_end=0): """Set Fusion comp's start and end frame range Args: @@ -55,17 +48,15 @@ def update_frame_range( attrs = { "COMPN_GlobalStart": start - handle_start, - "COMPN_GlobalEnd": end + handle_end, + "COMPN_GlobalEnd": end + handle_end } # set frame range if set_render_range: - attrs.update( - { - "COMPN_RenderStart": start, - "COMPN_RenderEnd": end, - } - ) + attrs.update({ + "COMPN_RenderStart": start, + "COMPN_RenderEnd": end + }) with comp_lock_and_undo_chunk(comp): comp.SetAttrs(attrs) @@ -78,13 +69,9 @@ def set_asset_framerange(): end = asset_doc["data"]["frameEnd"] handle_start = asset_doc["data"]["handleStart"] handle_end = asset_doc["data"]["handleEnd"] - update_frame_range( - start, - end, - set_render_range=True, - handle_start=handle_start, - handle_end=handle_end, - ) + update_frame_range(start, end, set_render_range=True, + handle_start=handle_start, + handle_end=handle_end) def set_asset_resolution(): @@ -94,15 +81,12 @@ def set_asset_resolution(): height = asset_doc["data"]["resolutionHeight"] comp = get_current_comp() - print( - "Setting comp frame format resolution to {}x{}".format(width, height) - ) - comp.SetPrefs( - { - "Comp.FrameFormat.Width": width, - "Comp.FrameFormat.Height": height, - } - ) + print("Setting comp frame format resolution to {}x{}".format(width, + height)) + comp.SetPrefs({ + "Comp.FrameFormat.Width": width, + "Comp.FrameFormat.Height": height, + }) def validate_comp_prefs(comp=None, force_repair=False): @@ -123,7 +107,7 @@ def validate_comp_prefs(comp=None, force_repair=False): "data.fps", "data.resolutionWidth", "data.resolutionHeight", - "data.pixelAspect", + "data.pixelAspect" ] asset_doc = get_current_project_asset(fields=fields) asset_data = asset_doc["data"] @@ -140,7 +124,7 @@ def validate_comp_prefs(comp=None, force_repair=False): ("resolutionWidth", "Width", "Resolution Width"), ("resolutionHeight", "Height", "Resolution Height"), ("pixelAspectX", "AspectX", "Pixel Aspect Ratio X"), - ("pixelAspectY", "AspectY", "Pixel Aspect Ratio Y"), + ("pixelAspectY", "AspectY", "Pixel Aspect Ratio Y") ] invalid = [] @@ -148,9 +132,9 @@ def validate_comp_prefs(comp=None, force_repair=False): asset_value = asset_data[key] comp_value = comp_frame_format_prefs.get(comp_key) if asset_value != comp_value: - invalid_msg = "{} {} should be {}".format( - label, comp_value, asset_value - ) + invalid_msg = "{} {} should be {}".format(label, + comp_value, + asset_value) invalid.append(invalid_msg) if not force_repair: @@ -161,8 +145,7 @@ def validate_comp_prefs(comp=None, force_repair=False): pref=label, value=comp_value, asset_name=asset_doc["name"], - asset_value=asset_value, - ) + asset_value=asset_value) ) if invalid: @@ -299,13 +282,11 @@ def maintained_selection(comp=None): @contextlib.contextmanager -def maintained_comp_range( - comp=None, - global_start=True, - global_end=True, - render_start=True, - render_end=True, -): +def maintained_comp_range(comp=None, + global_start=True, + global_end=True, + render_start=True, + render_end=True): """Reset comp frame ranges from before the context after the context""" if comp is None: comp = get_current_comp() @@ -349,7 +330,7 @@ def get_frame_path(path): filename, ext = os.path.splitext(path) # Find a final number group - match = re.match(".*?([0-9]+)$", filename) + match = re.match('.*?([0-9]+)$', filename) if match: padding = len(match.group(1)) # remove number from end since fusion From 80febccf0ef2162623d02a83b362b85df7d33c8b Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Mon, 2 Oct 2023 14:41:54 +0200 Subject: [PATCH 0095/1233] hound --- openpype/hosts/fusion/api/lib.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/fusion/api/lib.py b/openpype/hosts/fusion/api/lib.py index 0393c1f7d5..85f9c54a73 100644 --- a/openpype/hosts/fusion/api/lib.py +++ b/openpype/hosts/fusion/api/lib.py @@ -21,6 +21,7 @@ from openpype.pipeline.context_tools import get_current_project_asset self = sys.modules[__name__] self._project = None + def update_frame_range(start, end, comp=None, set_render_range=True, handle_start=0, handle_end=0): """Set Fusion comp's start and end frame range @@ -70,8 +71,8 @@ def set_asset_framerange(): handle_start = asset_doc["data"]["handleStart"] handle_end = asset_doc["data"]["handleEnd"] update_frame_range(start, end, set_render_range=True, - handle_start=handle_start, - handle_end=handle_end) + handle_start=handle_start, + handle_end=handle_end) def set_asset_resolution(): @@ -133,8 +134,8 @@ def validate_comp_prefs(comp=None, force_repair=False): comp_value = comp_frame_format_prefs.get(comp_key) if asset_value != comp_value: invalid_msg = "{} {} should be {}".format(label, - comp_value, - asset_value) + comp_value, + asset_value) invalid.append(invalid_msg) if not force_repair: @@ -166,7 +167,6 @@ def validate_comp_prefs(comp=None, force_repair=False): from . import menu from openpype.widgets import popup from openpype.style import load_stylesheet - dialog = popup.Popup(parent=menu.menu) dialog.setWindowTitle("Fusion comp has invalid configuration") From 73a122b79a1069e767227307cb30dc23446f8c61 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Mon, 2 Oct 2023 15:09:30 +0200 Subject: [PATCH 0096/1233] Restore formatting of non-modified code --- .../fusion/plugins/create/create_saver.py | 57 +++++++++++-------- .../fusion/plugins/publish/collect_render.py | 12 ++-- 2 files changed, 40 insertions(+), 29 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index 6b38af6ee4..2c627666b6 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -14,7 +14,7 @@ from openpype.pipeline import ( legacy_io, Creator as NewCreator, CreatedInstance, - Anatomy, + Anatomy ) @@ -33,16 +33,19 @@ class CreateSaver(NewCreator): # TODO: This should be renamed together with Nuke so it is aligned temp_rendering_path_template = ( - "{workdir}/renders/fusion/{subset}/{subset}.{frame}.{ext}" - ) + "{workdir}/renders/fusion/{subset}/{subset}.{frame}.{ext}") def create(self, subset_name, instance_data, pre_create_data): - self.pass_pre_attributes_to_instance(instance_data, pre_create_data) - - instance_data.update( - {"id": "pyblish.avalon.instance", "subset": subset_name} + self.pass_pre_attributes_to_instance( + instance_data, + pre_create_data ) + instance_data.update({ + "id": "pyblish.avalon.instance", + "subset": subset_name + }) + # TODO: Add pre_create attributes to choose file format? file_format = "OpenEXRFormat" @@ -149,12 +152,15 @@ class CreateSaver(NewCreator): # Subset change detected workdir = os.path.normpath(legacy_io.Session["AVALON_WORKDIR"]) - formatting_data.update( - {"workdir": workdir, "frame": "0" * frame_padding, "ext": "exr"} - ) + formatting_data.update({ + "workdir": workdir, + "frame": "0" * frame_padding, + "ext": "exr" + }) # build file path to render - filepath = self.temp_rendering_path_template.format(**formatting_data) + filepath = self.temp_rendering_path_template.format( + **formatting_data) comp = get_current_comp() tool["Clip"] = comp.ReverseMapPath(os.path.normpath(filepath)) @@ -190,7 +196,7 @@ class CreateSaver(NewCreator): attr_defs = [ self._get_render_target_enum(), self._get_reviewable_bool(), - self._get_frame_range_enum(), + self._get_frame_range_enum() ] return attr_defs @@ -198,7 +204,11 @@ class CreateSaver(NewCreator): """Settings for publish page""" return self.get_pre_create_attr_defs() - def pass_pre_attributes_to_instance(self, instance_data, pre_create_data): + def pass_pre_attributes_to_instance( + self, + instance_data, + pre_create_data + ): creator_attrs = instance_data["creator_attributes"] = {} for pass_key in pre_create_data.keys(): creator_attrs[pass_key] = pre_create_data[pass_key] @@ -221,13 +231,13 @@ class CreateSaver(NewCreator): frame_range_options = { "asset_db": "Current asset context", "render_range": "From render in/out", - "comp_range": "From composition timeline", + "comp_range": "From composition timeline" } return EnumDef( "frame_range_source", items=frame_range_options, - label="Frame range source", + label="Frame range source" ) def _get_reviewable_bool(self): @@ -242,18 +252,15 @@ class CreateSaver(NewCreator): """Method called on initialization of plugin to apply settings.""" # plugin settings - plugin_settings = project_settings["fusion"]["create"][ - self.__class__.__name__ - ] + plugin_settings = ( + project_settings["fusion"]["create"][self.__class__.__name__] + ) # individual attributes - self.instance_attributes = ( - plugin_settings.get("instance_attributes") - or self.instance_attributes - ) - self.default_variants = ( - plugin_settings.get("default_variants") or self.default_variants - ) + self.instance_attributes = plugin_settings.get( + "instance_attributes") or self.instance_attributes + self.default_variants = plugin_settings.get( + "default_variants") or self.default_variants self.temp_rendering_path_template = ( plugin_settings.get("temp_rendering_path_template") or self.temp_rendering_path_template diff --git a/openpype/hosts/fusion/plugins/publish/collect_render.py b/openpype/hosts/fusion/plugins/publish/collect_render.py index 117347a4c2..facc9e6aef 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_render.py +++ b/openpype/hosts/fusion/plugins/publish/collect_render.py @@ -25,13 +25,16 @@ class FusionRenderInstance(RenderInstance): class CollectFusionRender( - publish.AbstractCollectRender, publish.ColormanagedPyblishPluginMixin + publish.AbstractCollectRender, + publish.ColormanagedPyblishPluginMixin ): + order = pyblish.api.CollectorOrder + 0.09 label = "Collect Fusion Render" hosts = ["fusion"] def get_instances(self, context): + comp = context.data.get("currentComp") comp_frame_format_prefs = comp.GetPrefs("Comp.FrameFormat") aspect_x = comp_frame_format_prefs["AspectX"] @@ -71,7 +74,7 @@ class CollectFusionRender( asset=inst.data["asset"], task=task_name, attachTo=False, - setMembers="", + setMembers='', publish=True, name=subset_name, resolutionWidth=comp_frame_format_prefs.get("Width"), @@ -90,7 +93,7 @@ class CollectFusionRender( frameStep=1, fps=comp_frame_format_prefs.get("Rate"), app_version=comp.GetApp().Version, - publish_attributes=inst.data.get("publish_attributes", {}), + publish_attributes=inst.data.get("publish_attributes", {}) ) render_target = inst.data["creator_attributes"]["render_target"] @@ -162,7 +165,8 @@ class CollectFusionRender( for frame in range(start, end + 1): expected_files.append( os.path.join( - output_dir, f"{head}{str(frame).zfill(padding)}{ext}" + output_dir, + f"{head}{str(frame).zfill(padding)}{ext}" ) ) From 441bb73afc303cca3cf4da95fb623c4609426f76 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Mon, 2 Oct 2023 15:10:46 +0200 Subject: [PATCH 0097/1233] hound --- openpype/hosts/fusion/plugins/create/create_saver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index 2c627666b6..edac113e85 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -160,7 +160,7 @@ class CreateSaver(NewCreator): # build file path to render filepath = self.temp_rendering_path_template.format( - **formatting_data) + **formatting_data) comp = get_current_comp() tool["Clip"] = comp.ReverseMapPath(os.path.normpath(filepath)) From 8c508b4b00082e7f0427fa929a5d2e0d8be19d99 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 2 Oct 2023 15:45:38 +0200 Subject: [PATCH 0098/1233] removing testing scripts --- .../utility_scripts/tests/test_otio_as_edl.py | 49 ------------- .../testing_create_timeline_item_from_path.py | 73 ------------------- .../tests/testing_load_media_pool_item.py | 24 ------ .../tests/testing_startup_script.py | 5 -- .../tests/testing_timeline_op.py | 13 ---- 5 files changed, 164 deletions(-) delete mode 100644 openpype/hosts/resolve/utility_scripts/tests/test_otio_as_edl.py delete mode 100644 openpype/hosts/resolve/utility_scripts/tests/testing_create_timeline_item_from_path.py delete mode 100644 openpype/hosts/resolve/utility_scripts/tests/testing_load_media_pool_item.py delete mode 100644 openpype/hosts/resolve/utility_scripts/tests/testing_startup_script.py delete mode 100644 openpype/hosts/resolve/utility_scripts/tests/testing_timeline_op.py diff --git a/openpype/hosts/resolve/utility_scripts/tests/test_otio_as_edl.py b/openpype/hosts/resolve/utility_scripts/tests/test_otio_as_edl.py deleted file mode 100644 index 92f2e43a72..0000000000 --- a/openpype/hosts/resolve/utility_scripts/tests/test_otio_as_edl.py +++ /dev/null @@ -1,49 +0,0 @@ -#! python3 -import os -import sys - -import opentimelineio as otio - -from openpype.pipeline import install_host - -import openpype.hosts.resolve.api as bmdvr -from openpype.hosts.resolve.api.testing_utils import TestGUI -from openpype.hosts.resolve.otio import davinci_export as otio_export - - -class ThisTestGUI(TestGUI): - extensions = [".exr", ".jpg", ".mov", ".png", ".mp4", ".ari", ".arx"] - - def __init__(self): - super(ThisTestGUI, self).__init__() - # activate resolve from openpype - install_host(bmdvr) - - def _open_dir_button_pressed(self, event): - # selected_path = self.fu.RequestFile(os.path.expanduser("~")) - selected_path = self.fu.RequestDir(os.path.expanduser("~")) - self._widgets["inputTestSourcesFolder"].Text = selected_path - - # main function - def process(self, event): - self.input_dir_path = self._widgets["inputTestSourcesFolder"].Text - project = bmdvr.get_current_project() - otio_timeline = otio_export.create_otio_timeline(project) - print(f"_ otio_timeline: `{otio_timeline}`") - edl_path = os.path.join(self.input_dir_path, "this_file_name.edl") - print(f"_ edl_path: `{edl_path}`") - # xml_string = otio_adapters.fcpx_xml.write_to_string(otio_timeline) - # print(f"_ xml_string: `{xml_string}`") - otio.adapters.write_to_file( - otio_timeline, edl_path, adapter_name="cmx_3600") - project = bmdvr.get_current_project() - media_pool = project.GetMediaPool() - timeline = media_pool.ImportTimelineFromFile(edl_path) - # at the end close the window - self._close_window(None) - - -if __name__ == "__main__": - test_gui = ThisTestGUI() - test_gui.show_gui() - sys.exit(not bool(True)) diff --git a/openpype/hosts/resolve/utility_scripts/tests/testing_create_timeline_item_from_path.py b/openpype/hosts/resolve/utility_scripts/tests/testing_create_timeline_item_from_path.py deleted file mode 100644 index 91a361ec08..0000000000 --- a/openpype/hosts/resolve/utility_scripts/tests/testing_create_timeline_item_from_path.py +++ /dev/null @@ -1,73 +0,0 @@ -#! python3 -import os -import sys - -import clique - -from openpype.pipeline import install_host -from openpype.hosts.resolve.api.testing_utils import TestGUI -import openpype.hosts.resolve.api as bmdvr -from openpype.hosts.resolve.api.lib import ( - create_media_pool_item, - create_timeline_item, -) - - -class ThisTestGUI(TestGUI): - extensions = [".exr", ".jpg", ".mov", ".png", ".mp4", ".ari", ".arx"] - - def __init__(self): - super(ThisTestGUI, self).__init__() - # activate resolve from openpype - install_host(bmdvr) - - def _open_dir_button_pressed(self, event): - # selected_path = self.fu.RequestFile(os.path.expanduser("~")) - selected_path = self.fu.RequestDir(os.path.expanduser("~")) - self._widgets["inputTestSourcesFolder"].Text = selected_path - - # main function - def process(self, event): - self.input_dir_path = self._widgets["inputTestSourcesFolder"].Text - - self.dir_processing(self.input_dir_path) - - # at the end close the window - self._close_window(None) - - def dir_processing(self, dir_path): - collections, reminders = clique.assemble(os.listdir(dir_path)) - - # process reminders - for _rem in reminders: - _rem_path = os.path.join(dir_path, _rem) - - # go deeper if directory - if os.path.isdir(_rem_path): - print(_rem_path) - self.dir_processing(_rem_path) - else: - self.file_processing(_rem_path) - - # process collections - for _coll in collections: - _coll_path = os.path.join(dir_path, list(_coll).pop()) - self.file_processing(_coll_path) - - def file_processing(self, fpath): - print(f"_ fpath: `{fpath}`") - _base, ext = os.path.splitext(fpath) - # skip if unwanted extension - if ext not in self.extensions: - return - media_pool_item = create_media_pool_item(fpath) - print(media_pool_item) - - track_item = create_timeline_item(media_pool_item) - print(track_item) - - -if __name__ == "__main__": - test_gui = ThisTestGUI() - test_gui.show_gui() - sys.exit(not bool(True)) diff --git a/openpype/hosts/resolve/utility_scripts/tests/testing_load_media_pool_item.py b/openpype/hosts/resolve/utility_scripts/tests/testing_load_media_pool_item.py deleted file mode 100644 index 2e83188bde..0000000000 --- a/openpype/hosts/resolve/utility_scripts/tests/testing_load_media_pool_item.py +++ /dev/null @@ -1,24 +0,0 @@ -#! python3 -from openpype.pipeline import install_host -from openpype.hosts.resolve import api as bmdvr -from openpype.hosts.resolve.api.lib import ( - create_media_pool_item, - create_timeline_item, -) - - -def file_processing(fpath): - media_pool_item = create_media_pool_item(fpath) - print(media_pool_item) - - track_item = create_timeline_item(media_pool_item) - print(track_item) - - -if __name__ == "__main__": - path = "C:/CODE/__openpype_projects/jtest03dev/shots/sq01/mainsq01sh030/publish/plate/plateMain/v006/jt3d_mainsq01sh030_plateMain_v006.0996.exr" - - # activate resolve from openpype - install_host(bmdvr) - - file_processing(path) diff --git a/openpype/hosts/resolve/utility_scripts/tests/testing_startup_script.py b/openpype/hosts/resolve/utility_scripts/tests/testing_startup_script.py deleted file mode 100644 index b64714ab16..0000000000 --- a/openpype/hosts/resolve/utility_scripts/tests/testing_startup_script.py +++ /dev/null @@ -1,5 +0,0 @@ -#! python3 -from openpype.hosts.resolve.startup import main - -if __name__ == "__main__": - main() diff --git a/openpype/hosts/resolve/utility_scripts/tests/testing_timeline_op.py b/openpype/hosts/resolve/utility_scripts/tests/testing_timeline_op.py deleted file mode 100644 index 8270496f64..0000000000 --- a/openpype/hosts/resolve/utility_scripts/tests/testing_timeline_op.py +++ /dev/null @@ -1,13 +0,0 @@ -#! python3 -from openpype.pipeline import install_host -from openpype.hosts.resolve import api as bmdvr -from openpype.hosts.resolve.api.lib import get_current_project - -if __name__ == "__main__": - install_host(bmdvr) - project = get_current_project() - timeline_count = project.GetTimelineCount() - print(f"Timeline count: {timeline_count}") - timeline = project.GetTimelineByIndex(timeline_count) - print(f"Timeline name: {timeline.GetName()}") - print(timeline.GetTrackCount("video")) From 88a1f97ad595000b6a440643b61cbcd3b8f659c9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 2 Oct 2023 15:48:27 +0200 Subject: [PATCH 0099/1233] resolve: improving loading --- openpype/hosts/resolve/api/lib.py | 54 +++++++++++++++++----------- openpype/hosts/resolve/api/plugin.py | 43 ++++------------------ 2 files changed, 40 insertions(+), 57 deletions(-) diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index 8f7eba8a90..22be929412 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -6,7 +6,10 @@ import contextlib from opentimelineio import opentime from openpype.lib import Logger -from openpype.pipeline.editorial import is_overlapping_otio_ranges +from openpype.pipeline.editorial import ( + is_overlapping_otio_ranges, + frames_to_timecode +) from ..otio import davinci_export as otio_export @@ -243,11 +246,13 @@ def get_media_pool_item(fpath, root: object = None) -> object: return None -def create_timeline_item(media_pool_item: object, - timeline: object = None, - source_start: int = None, - source_end: int = None, - timeline_in: int = None) -> object: +def create_timeline_item( + media_pool_item: object, + source_start: int, + source_end: int, + timeline_in: int, + timeline: object = None +) -> object: """ Add media pool item to current or defined timeline. @@ -267,20 +272,24 @@ def create_timeline_item(media_pool_item: object, clip_name = _clip_property("File Name") timeline = timeline or get_current_timeline() + # timing variables + fps = project.GetSetting("timelineFrameRate") + duration = source_end - source_start + timecode_in = frames_to_timecode(timeline_in, fps) + timecode_out = frames_to_timecode(timeline_in + duration, fps) + # if timeline was used then switch it to current timeline with maintain_current_timeline(timeline): # Add input mediaPoolItem to clip data - clip_data = {"mediaPoolItem": media_pool_item} - - # add source time range if input was given - if source_start is not None: - clip_data["startFrame"] = source_start - if source_end is not None: - clip_data["endFrame"] = source_end - - # Create a clipInfo dictionary with the necessary information - clip_data["recordFrame"] = timeline_in + clip_data = { + "mediaPoolItem": media_pool_item, + "startFrame": source_start, + "endFrame": source_end, + "recordFrame": timeline_in, + } + print("clip_data", "_" * 50) + print(media_pool_item.GetName()) print(clip_data) # add to timeline @@ -289,10 +298,15 @@ def create_timeline_item(media_pool_item: object, output_timeline_item = get_timeline_item( media_pool_item, timeline) - assert output_timeline_item, AssertionError( - "Track Item with name `{}` doesn't exist on the timeline: `{}`".format( - clip_name, timeline.GetName() - )) + assert output_timeline_item, AssertionError(( + "Clip name '{}' was't created on the timeline: '{}' \n\n" + "Please check if the clip is in the media pool or if the timeline \n" + "is having activated correct track name, or if it is not already \n" + "having any clip add place on the timeline in: '{}' out: '{}'. \n\n" + "Clip data: {}" + ).format( + clip_name, timeline.GetName(), timecode_in, timecode_out, clip_data + )) return output_timeline_item diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py index b1bde212fe..1c817d8e0d 100644 --- a/openpype/hosts/resolve/api/plugin.py +++ b/openpype/hosts/resolve/api/plugin.py @@ -355,7 +355,6 @@ class ClipLoader: asset = str(repr_cntx["asset"]) subset = str(repr_cntx["subset"]) representation = str(repr_cntx["representation"]) - self.data["track_name"] = "{}_{}".format(asset, representation) self.data["clip_name"] = "_".join([asset, subset, representation]) self.data["versionData"] = self.context["version"]["data"] # gets file path @@ -387,32 +386,6 @@ class ClipLoader: asset_name = self.context["representation"]["context"]["asset"] self.data["assetData"] = get_current_project_asset(asset_name)["data"] - def _set_active_track(self): - """ Set active track to `track` """ - track_type = "video" - track_name = self.data["track_name"] - track_exists = False - - # get total track count - track_count = self.active_timeline.GetTrackCount(track_type) - # loop all tracks by track indexes - for track_index in range(1, int(track_count) + 1): - # get current track name - _track_name = self.active_timeline.GetTrackName( - track_type, track_index) - if track_name != _track_name: - continue - track_exists = True - break - - if not track_exists: - self.active_timeline.AddTrack(track_type) - self.active_timeline.SetTrackName( - track_type, - track_index + 1, - track_name - ) - def load(self): # create project bin for the media to be imported into @@ -420,7 +393,6 @@ class ClipLoader: # create mediaItem in active project bin # create clip media - media_pool_item = lib.create_media_pool_item( self.data["path"], self.active_bin) _clip_property = media_pool_item.GetClipProperty @@ -433,9 +405,6 @@ class ClipLoader: if handle_end is None: handle_end = int(self.data["assetData"]["handleEnd"]) - # handle timeline tracks - self._set_active_track() - # get timeline in timeline_start = self.active_timeline.GetStartFrame() if self.sequential_load: @@ -454,17 +423,17 @@ class ClipLoader: source_out -= handle_end # include handles - if self.with_handles: - source_in -= handle_start - source_out += handle_end + if not self.with_handles: + source_in += handle_start + source_out -= handle_end # make track item from source in bin as item timeline_item = lib.create_timeline_item( media_pool_item, - self.active_timeline, source_in, source_out, - timeline_in + timeline_in, + self.active_timeline, ) print("Loading clips: `{}`".format(self.data["clip_name"])) @@ -504,7 +473,7 @@ class TimelineItemLoader(LoaderPlugin): """ options = [ - qargparse.Toggle( + qargparse.Boolean( "handles", label="Include handles", default=0, From bd31dbaf35d9f2a9c054223827d58cf34ebe14dd Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Mon, 2 Oct 2023 16:12:20 +0200 Subject: [PATCH 0100/1233] Get the comp from render_instance instead of get_current_comp() --- openpype/hosts/fusion/plugins/publish/collect_render.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/collect_render.py b/openpype/hosts/fusion/plugins/publish/collect_render.py index facc9e6aef..5474b677cf 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_render.py +++ b/openpype/hosts/fusion/plugins/publish/collect_render.py @@ -6,7 +6,6 @@ from openpype.pipeline import publish from openpype.pipeline.publish import RenderInstance from openpype.hosts.fusion.api.lib import ( get_frame_path, - get_current_comp, ) @@ -148,7 +147,7 @@ class CollectFusionRender( start = render_instance.frameStart - render_instance.handleStart end = render_instance.frameEnd + render_instance.handleEnd - comp = get_current_comp() + comp = render_instance.workfileComp path = comp.MapPath( render_instance.tool["Clip"][ render_instance.workfileComp.TIME_UNDEFINED From 8267736bed9968a314658b45de2cbc904c0f2547 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 2 Oct 2023 16:24:23 +0200 Subject: [PATCH 0101/1233] Fix Fusion 18.6+ support: Avoid issues with Fusion's `BlackmagicFusion.PyRemoteObject` instances being unhashable. ```python Traceback (most recent call last): File "E:\openpype\OpenPype\.venv\lib\site-packages\pyblish\plugin.py", line 527, in __explicit_process runner(*args) File "E:\openpype\OpenPype\openpype\hosts\fusion\plugins\publish\extract_render_local.py", line 61, in process result = self.render(instance) File "E:\openpype\OpenPype\openpype\hosts\fusion\plugins\publish\extract_render_local.py", line 118, in render with enabled_savers(current_comp, savers_to_render): File "C:\Users\User\AppData\Local\Programs\Python\Python39\lib\contextlib.py", line 119, in __enter__ return next(self.gen) File "E:\openpype\OpenPype\openpype\hosts\fusion\plugins\publish\extract_render_local.py", line 33, in enabled_savers original_states[saver] = original_state TypeError: unhashable type: 'BlackmagicFusion.PyRemoteObject' ``` --- .../hosts/fusion/plugins/publish/extract_render_local.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/extract_render_local.py b/openpype/hosts/fusion/plugins/publish/extract_render_local.py index 25c101cf00..e7bf010a9d 100644 --- a/openpype/hosts/fusion/plugins/publish/extract_render_local.py +++ b/openpype/hosts/fusion/plugins/publish/extract_render_local.py @@ -25,12 +25,13 @@ def enabled_savers(comp, savers): """ passthrough_key = "TOOLB_PassThrough" original_states = {} + savers_by_name = {saver.Name: saver for saver in savers} enabled_save_names = {saver.Name for saver in savers} try: all_savers = comp.GetToolList(False, "Saver").values() for saver in all_savers: original_state = saver.GetAttrs()[passthrough_key] - original_states[saver] = original_state + original_states[saver.Name] = original_state # The passthrough state we want to set (passthrough != enabled) state = saver.Name not in enabled_save_names @@ -38,7 +39,8 @@ def enabled_savers(comp, savers): saver.SetAttrs({passthrough_key: state}) yield finally: - for saver, original_state in original_states.items(): + for saver_name, original_state in original_states.items(): + saver = savers_by_name.get(saver_name) saver.SetAttrs({"TOOLB_PassThrough": original_state}) From 74070e0f25d9b53ec16512057e8e08ad228040ae Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 2 Oct 2023 16:28:46 +0200 Subject: [PATCH 0102/1233] Be more explicit that key should exist --- openpype/hosts/fusion/plugins/publish/extract_render_local.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/publish/extract_render_local.py b/openpype/hosts/fusion/plugins/publish/extract_render_local.py index e7bf010a9d..7cc03410c6 100644 --- a/openpype/hosts/fusion/plugins/publish/extract_render_local.py +++ b/openpype/hosts/fusion/plugins/publish/extract_render_local.py @@ -40,7 +40,7 @@ def enabled_savers(comp, savers): yield finally: for saver_name, original_state in original_states.items(): - saver = savers_by_name.get(saver_name) + saver = savers_by_name[saver_name] saver.SetAttrs({"TOOLB_PassThrough": original_state}) From 33ebd706a9232a3d8b0c34f79eb46474fde2f640 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 2 Oct 2023 16:31:25 +0200 Subject: [PATCH 0103/1233] Correctly produce the `savers_by_name` dict to include all savers --- .../fusion/plugins/publish/extract_render_local.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/extract_render_local.py b/openpype/hosts/fusion/plugins/publish/extract_render_local.py index 7cc03410c6..08d608139d 100644 --- a/openpype/hosts/fusion/plugins/publish/extract_render_local.py +++ b/openpype/hosts/fusion/plugins/publish/extract_render_local.py @@ -25,16 +25,18 @@ def enabled_savers(comp, savers): """ passthrough_key = "TOOLB_PassThrough" original_states = {} - savers_by_name = {saver.Name: saver for saver in savers} - enabled_save_names = {saver.Name for saver in savers} + enabled_saver_names = {saver.Name for saver in savers} + + all_savers = comp.GetToolList(False, "Saver").values() + savers_by_name = {saver.Name: saver for saver in all_savers} + try: - all_savers = comp.GetToolList(False, "Saver").values() for saver in all_savers: original_state = saver.GetAttrs()[passthrough_key] original_states[saver.Name] = original_state # The passthrough state we want to set (passthrough != enabled) - state = saver.Name not in enabled_save_names + state = saver.Name not in enabled_saver_names if state != original_state: saver.SetAttrs({passthrough_key: state}) yield From 42f7549e059e9bdf85b6c360b9808bf02b7727e5 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 2 Oct 2023 16:37:45 +0200 Subject: [PATCH 0104/1233] resolve: adding input arg to create new timeline --- openpype/hosts/resolve/api/lib.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index eaee3bb9ba..a88564a3ef 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -125,7 +125,7 @@ def get_any_timeline(): return project.GetTimelineByIndex(1) -def get_new_timeline(): +def get_new_timeline(timeline_name: str = None): """Get new timeline object. Returns: @@ -133,7 +133,8 @@ def get_new_timeline(): """ project = get_current_project() media_pool = project.GetMediaPool() - new_timeline = media_pool.CreateEmptyTimeline(self.pype_timeline_name) + new_timeline = media_pool.CreateEmptyTimeline( + timeline_name or self.pype_timeline_name) project.SetCurrentTimeline(new_timeline) return new_timeline From b8cee701a36742a40b8111227bd5c30bc8f183d9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 2 Oct 2023 16:38:42 +0200 Subject: [PATCH 0105/1233] resolve: load multiple clips to new timeline fix --- openpype/hosts/resolve/api/plugin.py | 40 ++++++++++++------- .../hosts/resolve/plugins/load/load_clip.py | 6 --- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py index e2bd76ffa2..ddf0df662b 100644 --- a/openpype/hosts/resolve/api/plugin.py +++ b/openpype/hosts/resolve/api/plugin.py @@ -291,17 +291,17 @@ class ClipLoader: active_bin = None data = dict() - def __init__(self, cls, context, path, **options): + def __init__(self, loader_obj, context, path, **options): """ Initialize object Arguments: - cls (openpype.pipeline.load.LoaderPlugin): plugin object + loader_obj (openpype.pipeline.load.LoaderPlugin): plugin object context (dict): loader plugin context options (dict)[optional]: possible keys: projectBinPath: "path/to/binItem" """ - self.__dict__.update(cls.__dict__) + self.__dict__.update(loader_obj.__dict__) self.context = context self.active_project = lib.get_current_project() self.fname = path @@ -319,23 +319,29 @@ class ClipLoader: # inject asset data to representation dict self._get_asset_data() - print("__init__ self.data: `{}`".format(self.data)) # add active components to class if self.new_timeline: - if options.get("timeline"): + loader_cls = loader_obj.__class__ + if loader_cls.timeline: # if multiselection is set then use options sequence - self.active_timeline = options["timeline"] + self.active_timeline = loader_cls.timeline else: # create new sequence - self.active_timeline = ( - lib.get_current_timeline() or - lib.get_new_timeline() + self.active_timeline = lib.get_new_timeline( + "{}_{}_{}".format( + self.subset, + self.representation, + str(uuid.uuid4())[:8] + ) ) + loader_cls.timeline = self.active_timeline + + print(self.active_timeline.GetName()) else: self.active_timeline = lib.get_current_timeline() - cls.timeline = self.active_timeline + def _populate_data(self): """ Gets context and convert it to self.data @@ -349,10 +355,14 @@ class ClipLoader: # create name repr = self.context["representation"] repr_cntx = repr["context"] - asset = str(repr_cntx["asset"]) - subset = str(repr_cntx["subset"]) - representation = str(repr_cntx["representation"]) - self.data["clip_name"] = "_".join([asset, subset, representation]) + self.asset = str(repr_cntx["asset"]) + self.subset = str(repr_cntx["subset"]) + self.representation = str(repr_cntx["representation"]) + self.data["clip_name"] = "_".join([ + self.asset, + self.subset, + self.representation + ]) self.data["versionData"] = self.context["version"]["data"] # gets file path file = self.fname @@ -367,7 +377,7 @@ class ClipLoader: hierarchy = str("/".join(( "Loader", repr_cntx["hierarchy"].replace("\\", "/"), - asset + self.asset ))) self.data["binPath"] = hierarchy diff --git a/openpype/hosts/resolve/plugins/load/load_clip.py b/openpype/hosts/resolve/plugins/load/load_clip.py index 3a59ecea80..1d66c97041 100644 --- a/openpype/hosts/resolve/plugins/load/load_clip.py +++ b/openpype/hosts/resolve/plugins/load/load_clip.py @@ -48,12 +48,6 @@ class LoadClip(plugin.TimelineItemLoader): def load(self, context, name, namespace, options): - # in case loader uses multiselection - if self.timeline: - options.update({ - "timeline": self.timeline, - }) - # load clip to timeline and get main variables path = self.filepath_from_context(context) timeline_item = plugin.ClipLoader( From 01eb8ade9bc522012f1621741d5d89622456f420 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 2 Oct 2023 17:04:04 +0200 Subject: [PATCH 0106/1233] fixing inventory management for version update --- openpype/hosts/resolve/api/lib.py | 20 +++++++++++++------ .../hosts/resolve/plugins/load/load_clip.py | 6 +++--- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index a88564a3ef..65c91fcdf6 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -395,14 +395,22 @@ def get_current_timeline_items( def get_pype_timeline_item_by_name(name: str) -> object: - track_itmes = get_current_timeline_items() - for _ti in track_itmes: - tag_data = get_timeline_item_pype_tag(_ti["clip"]["item"]) - tag_name = tag_data.get("name") + """Get timeline item by name. + + Args: + name (str): name of timeline item + + Returns: + object: resolve.TimelineItem + """ + for _ti_data in get_current_timeline_items(): + _ti_clip = _ti_data["clip"]["item"] + tag_data = get_timeline_item_pype_tag(_ti_clip) + tag_name = tag_data.get("namespace") if not tag_name: continue - if tag_data.get("name") in name: - return _ti + if tag_name in name: + return _ti_clip return None diff --git a/openpype/hosts/resolve/plugins/load/load_clip.py b/openpype/hosts/resolve/plugins/load/load_clip.py index 1d66c97041..eea44a3726 100644 --- a/openpype/hosts/resolve/plugins/load/load_clip.py +++ b/openpype/hosts/resolve/plugins/load/load_clip.py @@ -102,8 +102,8 @@ class LoadClip(plugin.TimelineItemLoader): context.update({"representation": representation}) name = container['name'] namespace = container['namespace'] - timeline_item_data = lib.get_pype_timeline_item_by_name(namespace) - timeline_item = timeline_item_data["clip"]["item"] + timeline_item = lib.get_pype_timeline_item_by_name(namespace) + project_name = get_current_project_name() version = get_version_by_id(project_name, representation["parent"]) version_data = version.get("data", {}) @@ -111,8 +111,8 @@ class LoadClip(plugin.TimelineItemLoader): colorspace = version_data.get("colorspace", None) object_name = "{}_{}".format(name, namespace) path = get_representation_path(representation) - context["version"] = {"data": version_data} + context["version"] = {"data": version_data} loader = plugin.ClipLoader(self, context, path) timeline_item = loader.update(timeline_item) From e0e8673bdacc0d1cc54f094c0631b548ced6a432 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 2 Oct 2023 16:20:38 +0100 Subject: [PATCH 0107/1233] Add Maya 2024 and remove pre 2022. --- .../system_settings/applications.json | 80 +++++-------------- 1 file changed, 20 insertions(+), 60 deletions(-) diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index f2fc7d933a..66df1ab7d8 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -12,6 +12,26 @@ "LC_ALL": "C" }, "variants": { + "2024": { + "use_python_2": false, + "executables": { + "windows": [ + "C:\\Program Files\\Autodesk\\Maya2024\\bin\\maya.exe" + ], + "darwin": [], + "linux": [ + "/usr/autodesk/maya2024/bin/maya" + ] + }, + "arguments": { + "windows": [], + "darwin": [], + "linux": [] + }, + "environment": { + "MAYA_VERSION": "2024" + } + }, "2023": { "use_python_2": false, "executables": { @@ -51,66 +71,6 @@ "environment": { "MAYA_VERSION": "2022" } - }, - "2020": { - "use_python_2": true, - "executables": { - "windows": [ - "C:\\Program Files\\Autodesk\\Maya2020\\bin\\maya.exe" - ], - "darwin": [], - "linux": [ - "/usr/autodesk/maya2020/bin/maya" - ] - }, - "arguments": { - "windows": [], - "darwin": [], - "linux": [] - }, - "environment": { - "MAYA_VERSION": "2020" - } - }, - "2019": { - "use_python_2": true, - "executables": { - "windows": [ - "C:\\Program Files\\Autodesk\\Maya2019\\bin\\maya.exe" - ], - "darwin": [], - "linux": [ - "/usr/autodesk/maya2019/bin/maya" - ] - }, - "arguments": { - "windows": [], - "darwin": [], - "linux": [] - }, - "environment": { - "MAYA_VERSION": "2019" - } - }, - "2018": { - "use_python_2": true, - "executables": { - "windows": [ - "C:\\Program Files\\Autodesk\\Maya2018\\bin\\maya.exe" - ], - "darwin": [], - "linux": [ - "/usr/autodesk/maya2018/bin/maya" - ] - }, - "arguments": { - "windows": [], - "darwin": [], - "linux": [] - }, - "environment": { - "MAYA_VERSION": "2018" - } } } }, From be09038e222f833a2eb3290006f08d02ea88d9a3 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 3 Oct 2023 14:54:54 +0800 Subject: [PATCH 0108/1233] separating tyMesh and tyType into two representations --- .../hosts/max/plugins/publish/extract_tycache.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_tycache.py b/openpype/hosts/max/plugins/publish/extract_tycache.py index 5fa8642809..f4a5e0f4a6 100644 --- a/openpype/hosts/max/plugins/publish/extract_tycache.py +++ b/openpype/hosts/max/plugins/publish/extract_tycache.py @@ -66,6 +66,17 @@ class ExtractTyCache(publish.Extractor): instance.data["representations"].append(representation) self.log.info(f"Extracted instance '{instance.name}' to: {filenames}") + # Get the tyMesh filename for extraction + mesh_filename = "{}__tyMesh.tyc".format(instance.name) + mesh_repres = { + 'name': 'tyMesh', + 'ext': 'tyc', + 'files': mesh_filename, + "stagingDir": stagingdir + } + instance.data["representations"].append(mesh_repres) + self.log.info(f"Extracted instance '{instance.name}' to: {mesh_filename}") + def get_file(self, instance, start_frame, end_frame): """Get file names for tyFlow in tyCache format. @@ -74,7 +85,6 @@ class ExtractTyCache(publish.Extractor): Actual File Output from tyFlow in tyCache format: __tyPart_.tyc - __tyMesh.tyc e.g. tycacheMain__tyPart_00000.tyc @@ -92,7 +102,6 @@ class ExtractTyCache(publish.Extractor): for frame in range(int(start_frame), int(end_frame) + 1): filename = "{}__tyPart_{:05}.tyc".format(instance.name, frame) filenames.append(filename) - filenames.append("{}__tyMesh.tyc".format(instance.name)) return filenames def export_particle(self, members, start, end, From 74a6d33baa3061363df4795c44c0547af2fb505d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 3 Oct 2023 14:55:54 +0800 Subject: [PATCH 0109/1233] hound --- openpype/hosts/max/plugins/publish/extract_tycache.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/extract_tycache.py b/openpype/hosts/max/plugins/publish/extract_tycache.py index f4a5e0f4a6..56fd39406e 100644 --- a/openpype/hosts/max/plugins/publish/extract_tycache.py +++ b/openpype/hosts/max/plugins/publish/extract_tycache.py @@ -75,7 +75,8 @@ class ExtractTyCache(publish.Extractor): "stagingDir": stagingdir } instance.data["representations"].append(mesh_repres) - self.log.info(f"Extracted instance '{instance.name}' to: {mesh_filename}") + self.log.info( + f"Extracted instance '{instance.name}' to: {mesh_filename}") def get_file(self, instance, start_frame, end_frame): """Get file names for tyFlow in tyCache format. From 308d54ba267fc9ae767868a766eb7372ce9f0f8b Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 3 Oct 2023 08:48:51 +0100 Subject: [PATCH 0110/1233] Ayon settings --- .../applications/server/applications.json | 80 +++++-------------- 1 file changed, 20 insertions(+), 60 deletions(-) diff --git a/server_addon/applications/server/applications.json b/server_addon/applications/server/applications.json index 8e5b28623e..a8daf79f7b 100644 --- a/server_addon/applications/server/applications.json +++ b/server_addon/applications/server/applications.json @@ -7,6 +7,26 @@ "host_name": "maya", "environment": "{\n \"MAYA_DISABLE_CLIC_IPM\": \"Yes\",\n \"MAYA_DISABLE_CIP\": \"Yes\",\n \"MAYA_DISABLE_CER\": \"Yes\",\n \"PYMEL_SKIP_MEL_INIT\": \"Yes\",\n \"LC_ALL\": \"C\"\n}\n", "variants": [ + { + "name": "2024", + "label": "2024", + "executables": { + "windows": [ + "C:\\Program Files\\Autodesk\\Maya2024\\bin\\maya.exe" + ], + "darwin": [], + "linux": [ + "/usr/autodesk/maya2024/bin/maya" + ] + }, + "arguments": { + "windows": [], + "darwin": [], + "linux": [] + }, + "environment": "{\n \"MAYA_VERSION\": \"2024\"\n}", + "use_python_2": false + }, { "name": "2023", "label": "2023", @@ -45,66 +65,6 @@ "linux": [] }, "environment": "{\n \"MAYA_VERSION\": \"2022\"\n}", - "use_python_2": false - }, - { - "name": "2020", - "label": "2020", - "executables": { - "windows": [ - "C:\\Program Files\\Autodesk\\Maya2020\\bin\\maya.exe" - ], - "darwin": [], - "linux": [ - "/usr/autodesk/maya2020/bin/maya" - ] - }, - "arguments": { - "windows": [], - "darwin": [], - "linux": [] - }, - "environment": "{\n \"MAYA_VERSION\": \"2020\"\n}", - "use_python_2": true - }, - { - "name": "2019", - "label": "2019", - "executables": { - "windows": [ - "C:\\Program Files\\Autodesk\\Maya2019\\bin\\maya.exe" - ], - "darwin": [], - "linux": [ - "/usr/autodesk/maya2019/bin/maya" - ] - }, - "arguments": { - "windows": [], - "darwin": [], - "linux": [] - }, - "environment": "{\n \"MAYA_VERSION\": \"2019\"\n}", - "use_python_2": true - }, - { - "name": "2018", - "label": "2018", - "executables": { - "windows": [ - "C:\\Program Files\\Autodesk\\Maya2018\\bin\\maya.exe" - ], - "darwin": [], - "linux": [ - "/usr/autodesk/maya2018/bin/maya" - ] - }, - "arguments": { - "windows": [], - "darwin": [], - "linux": [] - }, - "environment": "{\n \"MAYA_VERSION\": \"2018\"\n}", "use_python_2": true } ] From a5b85d36f0d8e8b534d3016c8358e5be45604661 Mon Sep 17 00:00:00 2001 From: Ember Light <49758407+EmberLightVFX@users.noreply.github.com> Date: Tue, 3 Oct 2023 10:35:50 +0200 Subject: [PATCH 0111/1233] Removed double space in end of file Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/fusion/plugins/create/create_saver.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index edac113e85..21711f0229 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -247,7 +247,6 @@ class CreateSaver(NewCreator): label="Review", ) - def apply_settings(self, project_settings): """Method called on initialization of plugin to apply settings.""" From c5b9667aa292584c38e03f538c876a44cb31ad03 Mon Sep 17 00:00:00 2001 From: Ember Light <49758407+EmberLightVFX@users.noreply.github.com> Date: Tue, 3 Oct 2023 10:36:17 +0200 Subject: [PATCH 0112/1233] Place get_frame_path import on one row Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/fusion/plugins/publish/collect_render.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/collect_render.py b/openpype/hosts/fusion/plugins/publish/collect_render.py index 5474b677cf..a7daa0b64c 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_render.py +++ b/openpype/hosts/fusion/plugins/publish/collect_render.py @@ -4,9 +4,7 @@ import pyblish.api from openpype.pipeline import publish from openpype.pipeline.publish import RenderInstance -from openpype.hosts.fusion.api.lib import ( - get_frame_path, -) +from openpype.hosts.fusion.api.lib import get_frame_path @attr.s From f0b38dbb9a830d4475cdc34d87f8b5bd2d245645 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Tue, 3 Oct 2023 12:49:37 +0200 Subject: [PATCH 0113/1233] Update openpype/hosts/resolve/api/lib.py Co-authored-by: Roy Nieterau --- openpype/hosts/resolve/api/lib.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index 65c91fcdf6..92e600d55b 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -128,6 +128,9 @@ def get_any_timeline(): def get_new_timeline(timeline_name: str = None): """Get new timeline object. +Arguments: + timeline_name (str): New timeline name. + Returns: object: resolve.Timeline """ From 4bd820bc30299b018c5f21d5f1b659045c360279 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 3 Oct 2023 13:00:49 +0200 Subject: [PATCH 0114/1233] removing attachmets from self and moving into `timeline_basename` --- openpype/hosts/resolve/api/lib.py | 4 ++-- openpype/hosts/resolve/api/plugin.py | 21 +++++++++++---------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index 92e600d55b..9bdd62d52e 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -128,8 +128,8 @@ def get_any_timeline(): def get_new_timeline(timeline_name: str = None): """Get new timeline object. -Arguments: - timeline_name (str): New timeline name. + Arguments: + timeline_name (str): New timeline name. Returns: object: resolve.Timeline diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py index ddf0df662b..1fc3ed226c 100644 --- a/openpype/hosts/resolve/api/plugin.py +++ b/openpype/hosts/resolve/api/plugin.py @@ -329,9 +329,8 @@ class ClipLoader: else: # create new sequence self.active_timeline = lib.get_new_timeline( - "{}_{}_{}".format( - self.subset, - self.representation, + "{}_{}".format( + self.data["timeline_basename"], str(uuid.uuid4())[:8] ) ) @@ -355,13 +354,13 @@ class ClipLoader: # create name repr = self.context["representation"] repr_cntx = repr["context"] - self.asset = str(repr_cntx["asset"]) - self.subset = str(repr_cntx["subset"]) - self.representation = str(repr_cntx["representation"]) + asset = str(repr_cntx["asset"]) + subset = str(repr_cntx["subset"]) + representation = str(repr_cntx["representation"]) self.data["clip_name"] = "_".join([ - self.asset, - self.subset, - self.representation + asset, + subset, + representation ]) self.data["versionData"] = self.context["version"]["data"] # gets file path @@ -372,12 +371,14 @@ class ClipLoader: "Representation id `{}` is failing to load".format(repr_id)) return None self.data["path"] = file.replace("\\", "/") + self.data["timeline_basename"] = "timeline_{}_{}".format( + subset, representation) # solve project bin structure path hierarchy = str("/".join(( "Loader", repr_cntx["hierarchy"].replace("\\", "/"), - self.asset + asset ))) self.data["binPath"] = hierarchy From f5e8f4d3faf50f3da237702dd2b26c2cdb24ef40 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 3 Oct 2023 13:02:17 +0200 Subject: [PATCH 0115/1233] removing debugging prints --- openpype/hosts/resolve/api/plugin.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py index 1fc3ed226c..da5e649576 100644 --- a/openpype/hosts/resolve/api/plugin.py +++ b/openpype/hosts/resolve/api/plugin.py @@ -336,7 +336,6 @@ class ClipLoader: ) loader_cls.timeline = self.active_timeline - print(self.active_timeline.GetName()) else: self.active_timeline = lib.get_current_timeline() @@ -660,8 +659,6 @@ class PublishClip: # define ui inputs if non gui mode was used self.shot_num = self.ti_index - print( - "____ self.shot_num: {}".format(self.shot_num)) # ui_inputs data or default values if gui was not used self.rename = self.ui_inputs.get( From 4656e59759ad0ae6ac31564ece150802759779b2 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 3 Oct 2023 13:10:51 +0200 Subject: [PATCH 0116/1233] debug logging cleaning --- openpype/hosts/resolve/api/lib.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index 9bdd62d52e..735d2057f8 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -281,7 +281,6 @@ def create_timeline_item(media_pool_item: object, if source_end is not None: clip_data.update({"endFrame": source_end}) - print(clip_data) # add to timeline media_pool.AppendToTimeline([clip_data]) @@ -560,7 +559,6 @@ def get_pype_marker(timeline_item): note = timeline_item_markers[marker_frame]["note"] color = timeline_item_markers[marker_frame]["color"] name = timeline_item_markers[marker_frame]["name"] - print(f"_ marker data: {marker_frame} | {name} | {color} | {note}") if name == self.pype_marker_name and color == self.pype_marker_color: self.temp_marker_frame = marker_frame return json.loads(note) @@ -630,7 +628,7 @@ def create_compound_clip(clip_data, name, folder): if c.GetName() in name), None) if cct: - print(f"_ cct exists: {cct}") + print(f"Compound clip exists: {cct}") else: # Create empty timeline in current folder and give name: cct = mp.CreateEmptyTimeline(name) @@ -639,7 +637,7 @@ def create_compound_clip(clip_data, name, folder): clips = folder.GetClipList() cct = next((c for c in clips if c.GetName() in name), None) - print(f"_ cct created: {cct}") + print(f"Compound clip created: {cct}") with maintain_current_timeline(cct, tl_origin): # Add input clip to the current timeline: From 2317ab057f56138154308dc83bf5f510e6514052 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Tue, 3 Oct 2023 13:20:07 +0200 Subject: [PATCH 0117/1233] Update openpype/hosts/traypublisher/plugins/publish/validate_colorspace_look.py Co-authored-by: Roy Nieterau --- .../traypublisher/plugins/publish/validate_colorspace_look.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/traypublisher/plugins/publish/validate_colorspace_look.py b/openpype/hosts/traypublisher/plugins/publish/validate_colorspace_look.py index 7de8881321..2a9b2040d1 100644 --- a/openpype/hosts/traypublisher/plugins/publish/validate_colorspace_look.py +++ b/openpype/hosts/traypublisher/plugins/publish/validate_colorspace_look.py @@ -60,7 +60,7 @@ class ValidateColorspaceLook(pyblish.api.InstancePlugin, if not_set_keys: message = ( - f"Colorspace look attributes are not set: " + "Colorspace look attributes are not set: " f"{', '.join(not_set_keys)}" ) raise PublishValidationError( From 774050eff300c28ea33ef58f5f5227cf66d94ea8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Tue, 3 Oct 2023 13:27:14 +0200 Subject: [PATCH 0118/1233] Update openpype/scripts/ocio_wrapper.py Co-authored-by: Roy Nieterau --- openpype/scripts/ocio_wrapper.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/scripts/ocio_wrapper.py b/openpype/scripts/ocio_wrapper.py index 2bd25002c5..092d94623f 100644 --- a/openpype/scripts/ocio_wrapper.py +++ b/openpype/scripts/ocio_wrapper.py @@ -132,11 +132,11 @@ def _get_colorspace_data(config_path): roles = config.getRoles() if roles: colorspace_data.update({ - role[0]: { + role: { "type": "role", - "colorspace": role[1] + "colorspace": colorspace } - for role in roles + for (role, colorspace) in roles }) return colorspace_data From c30eb6ed4d9eea479e4bfd2f7235d941a0350f08 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 3 Oct 2023 13:32:39 +0200 Subject: [PATCH 0119/1233] improving creator def aggregation cycle in validator --- .../plugins/publish/validate_colorspace_look.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/publish/validate_colorspace_look.py b/openpype/hosts/traypublisher/plugins/publish/validate_colorspace_look.py index 7de8881321..c24bd6ee11 100644 --- a/openpype/hosts/traypublisher/plugins/publish/validate_colorspace_look.py +++ b/openpype/hosts/traypublisher/plugins/publish/validate_colorspace_look.py @@ -39,22 +39,21 @@ class ValidateColorspaceLook(pyblish.api.InstancePlugin, "direction", "interpolation" ] + creator_defs_by_key = {_def.key: _def.label for _def in creator_defs} + not_set_keys = [] for key in check_keys: if ociolook_item[key]: # key is set and it is correct continue - def_label = next( - (d_.label for d_ in creator_defs if key == d_.key), - None - ) + def_label = creator_defs_by_key.get(key) + if not def_label: - def_attrs = [(d_.key, d_.label) for d_ in creator_defs] # raise since key is not recognized by creator defs raise KeyError( f"Colorspace look attribute '{key}' is not " - f"recognized by creator attributes: {def_attrs}" + f"recognized by creator attributes: {creator_defs_by_key}" ) not_set_keys.append(def_label) From 2ccc2a101cfd304d1767e8f667fb6db3a73c2cab Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 3 Oct 2023 22:10:51 +0200 Subject: [PATCH 0120/1233] Add save current file button + "Save" shortcut when menu is active --- openpype/hosts/resolve/api/menu.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/resolve/api/menu.py b/openpype/hosts/resolve/api/menu.py index b3717e01ea..fd5a4cec78 100644 --- a/openpype/hosts/resolve/api/menu.py +++ b/openpype/hosts/resolve/api/menu.py @@ -1,13 +1,14 @@ import os import sys -from qtpy import QtWidgets, QtCore +from qtpy import QtWidgets, QtCore, QtGui from openpype.tools.utils import host_tools +from openpype.pipeline import registered_host from .pipeline import ( publish, - launch_workfiles_app + launch_workfiles_app, ) @@ -54,6 +55,7 @@ class OpenPypeMenu(QtWidgets.QWidget): ) self.setWindowTitle("OpenPype") + save_current_btn = QtWidgets.QPushButton("Save current file", self) workfiles_btn = QtWidgets.QPushButton("Workfiles ...", self) create_btn = QtWidgets.QPushButton("Create ...", self) publish_btn = QtWidgets.QPushButton("Publish ...", self) @@ -75,6 +77,10 @@ class OpenPypeMenu(QtWidgets.QWidget): layout = QtWidgets.QVBoxLayout(self) layout.setContentsMargins(10, 20, 10, 20) + layout.addWidget(save_current_btn) + + layout.addWidget(Spacer(15, self)) + layout.addWidget(workfiles_btn) layout.addWidget(create_btn) layout.addWidget(publish_btn) @@ -99,6 +105,8 @@ class OpenPypeMenu(QtWidgets.QWidget): self.setLayout(layout) + save_current_btn.clicked.connect(self.on_save_current_clicked) + save_current_btn.setShortcut(QtGui.QKeySequence.Save) workfiles_btn.clicked.connect(self.on_workfile_clicked) create_btn.clicked.connect(self.on_create_clicked) publish_btn.clicked.connect(self.on_publish_clicked) @@ -111,6 +119,15 @@ class OpenPypeMenu(QtWidgets.QWidget): # reset_resolution_btn.clicked.connect(self.on_set_resolution_clicked) experimental_btn.clicked.connect(self.on_experimental_clicked) + def on_save_current_clicked(self): + host = registered_host() + current_file = host.current_file() + if not current_file: + return + + print(f"Saving current file to: {current_file}") + host.save_file(current_file) + def on_workfile_clicked(self): print("Clicked Workfile") launch_workfiles_app() From fe11a3aed27cf3b0b215209c2148fa4167d52e96 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 4 Oct 2023 08:50:52 +0200 Subject: [PATCH 0121/1233] Revert cosmetic change --- openpype/hosts/resolve/api/menu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/resolve/api/menu.py b/openpype/hosts/resolve/api/menu.py index fd5a4cec78..024b367235 100644 --- a/openpype/hosts/resolve/api/menu.py +++ b/openpype/hosts/resolve/api/menu.py @@ -8,7 +8,7 @@ from openpype.pipeline import registered_host from .pipeline import ( publish, - launch_workfiles_app, + launch_workfiles_app ) From 32b4fc5f645c638a9d11b3e00a4283a80a865b17 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 4 Oct 2023 16:10:19 +0800 Subject: [PATCH 0122/1233] add resolution validator for render instance in maya --- .../plugins/publish/validate_resolution.py | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 openpype/hosts/maya/plugins/publish/validate_resolution.py diff --git a/openpype/hosts/maya/plugins/publish/validate_resolution.py b/openpype/hosts/maya/plugins/publish/validate_resolution.py new file mode 100644 index 0000000000..4c350388e2 --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/validate_resolution.py @@ -0,0 +1,54 @@ +import pyblish.api +from openpype.pipeline import ( + PublishValidationError, + OptionalPyblishPluginMixin +) +from maya import cmds +from openpype.hosts.maya.api.lib import reset_scene_resolution + + +class ValidateResolution(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): + """Validate the resolution setting aligned with DB""" + + order = pyblish.api.ValidatorOrder - 0.01 + families = ["renderlayer"] + hosts = ["maya"] + label = "Validate Resolution" + optional = True + + def process(self, instance): + if not self.is_active(instance.data): + return + width, height = self.get_db_resolution(instance) + current_width = cmds.getAttr("defaultResolution.width") + current_height = cmds.getAttr("defaultResolution.height") + if current_width != width and current_height != height: + raise PublishValidationError("Resolution Setting " + "not matching resolution " + "set on asset or shot.") + if current_width != width: + raise PublishValidationError("Width in Resolution Setting " + "not matching resolution set " + "on asset or shot.") + + if current_height != height: + raise PublishValidationError("Height in Resolution Setting " + "not matching resolution set " + "on asset or shot.") + + def get_db_resolution(self, instance): + 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) + + # Defaults if not found in asset document or project document + return 1920, 1080 + + @classmethod + def repair(cls, instance): + return reset_scene_resolution() From 1d0e55aa833d99180b99cbbd718954933c5103b2 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 4 Oct 2023 13:00:42 +0200 Subject: [PATCH 0123/1233] improving colorspace categorization also adding new abstract function for returning all settings options nicely labelled --- .../plugins/create/create_colorspace_look.py | 13 ++-- .../publish/collect_explicit_colorspace.py | 29 ++----- openpype/pipeline/colorspace.py | 77 ++++++++++++++++++- openpype/scripts/ocio_wrapper.py | 33 ++++---- 4 files changed, 101 insertions(+), 51 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/create/create_colorspace_look.py b/openpype/hosts/traypublisher/plugins/create/create_colorspace_look.py index 62ecc391f6..3f3fa5348a 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_colorspace_look.py +++ b/openpype/hosts/traypublisher/plugins/create/create_colorspace_look.py @@ -148,13 +148,12 @@ This creator publishes color space look file (LUT). if config_data: filepath = config_data["path"] - config_items = colorspace.get_ocio_config_colorspaces(filepath) - - self.colorspace_items.extend(( - (name, f"{name} [{data_['type']}]") - for name, data_ in config_items.items() - if data_.get("type") == "colorspace" - )) + labeled_colorspaces = colorspace.get_labeled_colorspaces( + filepath, + include_aliases=True, + include_roles=True + ) + self.colorspace_items.extend(labeled_colorspaces) self.enabled = True def _get_subset(self, asset_doc, variant, project_name, task_name=None): diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_explicit_colorspace.py b/openpype/hosts/traypublisher/plugins/publish/collect_explicit_colorspace.py index 08479b8363..06ceac5923 100644 --- a/openpype/hosts/traypublisher/plugins/publish/collect_explicit_colorspace.py +++ b/openpype/hosts/traypublisher/plugins/publish/collect_explicit_colorspace.py @@ -50,30 +50,13 @@ class CollectColorspace(pyblish.api.InstancePlugin, ) if config_data: - filepath = config_data["path"] - config_items = colorspace.get_ocio_config_colorspaces(filepath) - aliases = set() - for _, value_ in config_items.items(): - if value_.get("type") != "colorspace": - continue - if not value_.get("aliases"): - continue - for alias in value_.get("aliases"): - aliases.add(alias) - - colorspaces = { - name for name, data_ in config_items.items() - if data_.get("type") == "colorspace" - } - - cls.colorspace_items.extend(( - (name, f"{name} [colorspace]") for name in colorspaces - )) - if aliases: - cls.colorspace_items.extend(( - (name, f"{name} [alias]") for name in aliases - )) + labeled_colorspaces = colorspace.get_labeled_colorspaces( + filepath, + include_aliases=True, + include_roles=True + ) + cls.colorspace_items.extend(labeled_colorspaces) cls.enabled = True @classmethod diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 2800050496..39fdef046b 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -356,7 +356,10 @@ def parse_colorspace_from_filepath( "Must provide `config_path` if `colorspaces` is not provided." ) - colorspaces = colorspaces or get_ocio_config_colorspaces(config_path) + colorspaces = ( + colorspaces + or get_ocio_config_colorspaces(config_path)["colorspace"] + ) underscored_colorspaces = { key.replace(" ", "_"): key for key in colorspaces if " " in key @@ -393,7 +396,7 @@ def validate_imageio_colorspace_in_config(config_path, colorspace_name): Returns: bool: True if exists """ - colorspaces = get_ocio_config_colorspaces(config_path) + colorspaces = get_ocio_config_colorspaces(config_path)["colorspace"] if colorspace_name not in colorspaces: raise KeyError( "Missing colorspace '{}' in config file '{}'".format( @@ -530,6 +533,76 @@ def get_ocio_config_colorspaces(config_path): return CachedData.ocio_config_colorspaces[config_path] +def get_labeled_colorspaces( + config_path, + include_aliases=False, + include_looks=False, + include_roles=False, + +): + """Get all colorspace data with labels + + Wrapper function for aggregating all names and its families. + Families can be used for building menu and submenus in gui. + + Args: + config_path (str): path leading to config.ocio file + include_aliases (bool): include aliases in result + include_looks (bool): include looks in result + include_roles (bool): include roles in result + + Returns: + list[tuple[str,str]]: colorspace and family in couple + """ + config_items = get_ocio_config_colorspaces(config_path) + labeled_colorspaces = [] + aliases = set() + colorspaces = set() + looks = set() + roles = set() + for items_type, colorspace_items in config_items.items(): + if items_type == "colorspace": + for color_name, color_data in colorspace_items.items(): + if color_data.get("aliases"): + aliases.update([ + "{} ({})".format(alias_name, color_name) + for alias_name in color_data["aliases"] + ]) + colorspaces.add(color_name) + elif items_type == "look": + looks.update([ + "{} ({})".format(name, role_data["process_space"]) + for name, role_data in colorspace_items.items() + ]) + elif items_type == "role": + roles.update([ + "{} ({})".format(name, role_data["colorspace"]) + for name, role_data in colorspace_items.items() + ]) + + if roles and include_roles: + labeled_colorspaces.extend(( + (name, f"[role] {name}") for name in roles + )) + + labeled_colorspaces.extend(( + (name, f"[colorspace] {name}") for name in colorspaces + )) + + if aliases and include_aliases: + labeled_colorspaces.extend(( + (name, f"[alias] {name}") for name in aliases + )) + + if looks and include_looks: + labeled_colorspaces.extend(( + (name, f"[look] {name}") for name in looks + )) + + + return labeled_colorspaces + + # TODO: remove this in future - backward compatibility @deprecated("_get_wrapped_with_subprocess") def get_colorspace_data_subprocess(config_path): diff --git a/openpype/scripts/ocio_wrapper.py b/openpype/scripts/ocio_wrapper.py index 092d94623f..be21f0984f 100644 --- a/openpype/scripts/ocio_wrapper.py +++ b/openpype/scripts/ocio_wrapper.py @@ -107,37 +107,32 @@ def _get_colorspace_data(config_path): config = ocio.Config().CreateFromFile(str(config_path)) colorspace_data = { - color.getName(): { - "type": "colorspace", - "family": color.getFamily(), - "categories": list(color.getCategories()), - "aliases": list(color.getAliases()), - "equalitygroup": color.getEqualityGroup(), + "colorspace": { + color.getName(): { + "family": color.getFamily(), + "categories": list(color.getCategories()), + "aliases": list(color.getAliases()), + "equalitygroup": color.getEqualityGroup(), + } + for color in config.getColorSpaces() } - for color in config.getColorSpaces() } # add looks looks = config.getLooks() if looks: - colorspace_data.update({ - look.getName(): { - "type": "look", - "process_space": look.getProcessSpace() - } + colorspace_data["look"] = { + look.getName(): {"process_space": look.getProcessSpace()} for look in looks - }) + } # add roles roles = config.getRoles() if roles: - colorspace_data.update({ - role: { - "type": "role", - "colorspace": colorspace - } + colorspace_data["role"] = { + role: {"colorspace": colorspace} for (role, colorspace) in roles - }) + } return colorspace_data From af3ebd190cd9c2cbf71eaa3c16bbd7bee4632ed5 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 4 Oct 2023 13:24:18 +0200 Subject: [PATCH 0124/1233] fix in labeling --- openpype/pipeline/colorspace.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 39fdef046b..c456baa70f 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -565,39 +565,33 @@ def get_labeled_colorspaces( for color_name, color_data in colorspace_items.items(): if color_data.get("aliases"): aliases.update([ - "{} ({})".format(alias_name, color_name) + (alias_name, "[alias] {} ({})".format(alias_name, color_name)) for alias_name in color_data["aliases"] ]) colorspaces.add(color_name) elif items_type == "look": looks.update([ - "{} ({})".format(name, role_data["process_space"]) + (name, "[look] {} ({})".format(name, role_data["process_space"])) for name, role_data in colorspace_items.items() ]) elif items_type == "role": roles.update([ - "{} ({})".format(name, role_data["colorspace"]) + (name, "[role] {} ({})".format(name, role_data["colorspace"])) for name, role_data in colorspace_items.items() ]) if roles and include_roles: - labeled_colorspaces.extend(( - (name, f"[role] {name}") for name in roles - )) + labeled_colorspaces.extend(roles) labeled_colorspaces.extend(( (name, f"[colorspace] {name}") for name in colorspaces )) if aliases and include_aliases: - labeled_colorspaces.extend(( - (name, f"[alias] {name}") for name in aliases - )) + labeled_colorspaces.extend(aliases) if looks and include_looks: - labeled_colorspaces.extend(( - (name, f"[look] {name}") for name in looks - )) + labeled_colorspaces.extend(looks) return labeled_colorspaces From 919540038ff969c966acb7fe098cc64da77363d7 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 4 Oct 2023 13:24:41 +0200 Subject: [PATCH 0125/1233] unit testing of labeling --- .../pipeline/test_colorspace_labels.py | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 tests/unit/openpype/pipeline/test_colorspace_labels.py diff --git a/tests/unit/openpype/pipeline/test_colorspace_labels.py b/tests/unit/openpype/pipeline/test_colorspace_labels.py new file mode 100644 index 0000000000..a135c3258b --- /dev/null +++ b/tests/unit/openpype/pipeline/test_colorspace_labels.py @@ -0,0 +1,81 @@ +import unittest +from unittest.mock import patch +from openpype.pipeline.colorspace import get_labeled_colorspaces + + +class TestGetLabeledColorspaces(unittest.TestCase): + @patch('openpype.pipeline.colorspace.get_ocio_config_colorspaces') + def test_returns_list_of_tuples(self, mock_get_ocio_config_colorspaces): + mock_get_ocio_config_colorspaces.return_value = { + 'colorspace': { + 'sRGB': {}, + 'Rec.709': {}, + }, + 'look': { + 'sRGB to Rec.709': { + 'process_space': 'Rec.709', + }, + }, + 'role': { + 'reference': { + 'colorspace': 'sRGB', + }, + }, + } + result = get_labeled_colorspaces('config.ocio') + self.assertIsInstance(result, list) + self.assertTrue(all(isinstance(item, tuple) for item in result)) + + @patch('openpype.pipeline.colorspace.get_ocio_config_colorspaces') + def test_includes_colorspaces(self, mock_get_ocio_config_colorspaces): + mock_get_ocio_config_colorspaces.return_value = { + 'colorspace': { + 'sRGB': {} + }, + 'look': {}, + 'role': {}, + } + result = get_labeled_colorspaces('config.ocio', include_aliases=False, include_looks=False, include_roles=False) + self.assertEqual(result, [('sRGB', '[colorspace] sRGB')]) + + @patch('openpype.pipeline.colorspace.get_ocio_config_colorspaces') + def test_includes_aliases(self, mock_get_ocio_config_colorspaces): + mock_get_ocio_config_colorspaces.return_value = { + 'colorspace': { + 'sRGB': { + 'aliases': ['sRGB (D65)'], + }, + }, + 'look': {}, + 'role': {}, + } + result = get_labeled_colorspaces('config.ocio', include_aliases=True, include_looks=False, include_roles=False) + self.assertEqual(result, [('sRGB', '[colorspace] sRGB'), ('sRGB (D65)', '[alias] sRGB (D65) (sRGB)')]) + + @patch('openpype.pipeline.colorspace.get_ocio_config_colorspaces') + def test_includes_looks(self, mock_get_ocio_config_colorspaces): + mock_get_ocio_config_colorspaces.return_value = { + 'colorspace': {}, + 'look': { + 'sRGB to Rec.709': { + 'process_space': 'Rec.709', + }, + }, + 'role': {}, + } + result = get_labeled_colorspaces('config.ocio', include_aliases=False, include_looks=True, include_roles=False) + self.assertEqual(result, [('sRGB to Rec.709', '[look] sRGB to Rec.709 (Rec.709)')]) + + @patch('openpype.pipeline.colorspace.get_ocio_config_colorspaces') + def test_includes_roles(self, mock_get_ocio_config_colorspaces): + mock_get_ocio_config_colorspaces.return_value = { + 'colorspace': {}, + 'look': {}, + 'role': { + 'reference': { + 'colorspace': 'sRGB', + }, + }, + } + result = get_labeled_colorspaces('config.ocio', include_aliases=False, include_looks=False, include_roles=True) + self.assertEqual(result, [('reference', '[role] reference (sRGB)')]) From 7782c333dc6d4e0934b3f14dbb792730f9887eac Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 4 Oct 2023 13:26:18 +0200 Subject: [PATCH 0126/1233] renaming test file --- ...space_labels.py => test_colorspace_get_labeled_colorspaces.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/unit/openpype/pipeline/{test_colorspace_labels.py => test_colorspace_get_labeled_colorspaces.py} (100%) diff --git a/tests/unit/openpype/pipeline/test_colorspace_labels.py b/tests/unit/openpype/pipeline/test_colorspace_get_labeled_colorspaces.py similarity index 100% rename from tests/unit/openpype/pipeline/test_colorspace_labels.py rename to tests/unit/openpype/pipeline/test_colorspace_get_labeled_colorspaces.py From b908665d4c4196acb9aae46f861eea55ffad35d9 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 4 Oct 2023 16:31:27 +0300 Subject: [PATCH 0127/1233] consider handleStart and End when collecting frames --- openpype/hosts/houdini/api/lib.py | 23 ++++++++--- .../publish/collect_instance_frame_data.py | 40 ++++--------------- .../plugins/publish/collect_instances.py | 14 +------ .../publish/collect_rop_frame_range.py | 16 +++++--- 4 files changed, 35 insertions(+), 58 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index a3f691e1fc..ff6c62a143 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -548,7 +548,7 @@ def get_template_from_value(key, value): return parm -def get_frame_data(node): +def get_frame_data(self, node, asset_data={}): """Get the frame data: start frame, end frame and steps. Args: @@ -561,16 +561,27 @@ def get_frame_data(node): data = {} if node.parm("trange") is None: - + self.log.debug( + "Node has no 'trange' parameter: {}".format(node.path()) + ) return data if node.evalParm("trange") == 0: - self.log.debug("trange is 0") + self.log.debug( + "Node '{}' has 'Render current frame' set. " + "Time range data ignored.".format(node.path()) + ) return data - data["frameStart"] = node.evalParm("f1") - data["frameEnd"] = node.evalParm("f2") - data["steps"] = node.evalParm("f3") + data["frameStartHandle"] = node.evalParm("f1") + data["frameStart"] = node.evalParm("f1") + asset_data.get("handleStart", 0) + data["handleStart"] = asset_data.get("handleStart", 0) + + data["frameEndHandle"] = node.evalParm("f2") + data["frameEnd"] = node.evalParm("f2") - asset_data.get("handleEnd", 0) + data["handleEnd"] = asset_data.get("handleEnd", 0) + + data["byFrameStep"] = node.evalParm("f3") return data diff --git a/openpype/hosts/houdini/plugins/publish/collect_instance_frame_data.py b/openpype/hosts/houdini/plugins/publish/collect_instance_frame_data.py index 584343cd64..97d18f97f0 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_instance_frame_data.py +++ b/openpype/hosts/houdini/plugins/publish/collect_instance_frame_data.py @@ -1,7 +1,7 @@ import hou import pyblish.api - +from openpype.hosts.houdini.api import lib class CollectInstanceNodeFrameRange(pyblish.api.InstancePlugin): """Collect time range frame data for the instance node.""" @@ -15,42 +15,16 @@ class CollectInstanceNodeFrameRange(pyblish.api.InstancePlugin): node_path = instance.data.get("instance_node") node = hou.node(node_path) if node_path else None if not node_path or not node: - self.log.debug("No instance node found for instance: " - "{}".format(instance)) + self.log.debug( + "No instance node found for instance: {}".format(instance) + ) return - frame_data = self.get_frame_data(node) + asset_data = instance.context.data["assetEntity"]["data"] + frame_data = lib.get_frame_data(self, node, asset_data) + if not frame_data: return self.log.info("Collected time data: {}".format(frame_data)) instance.data.update(frame_data) - - def get_frame_data(self, node): - """Get the frame data: start frame, end frame and steps - Args: - node(hou.Node) - - Returns: - dict - - """ - - data = {} - - if node.parm("trange") is None: - self.log.debug("Node has no 'trange' parameter: " - "{}".format(node.path())) - return data - - if node.evalParm("trange") == 0: - # Ignore 'render current frame' - self.log.debug("Node '{}' has 'Render current frame' set. " - "Time range data ignored.".format(node.path())) - return data - - data["frameStart"] = node.evalParm("f1") - data["frameEnd"] = node.evalParm("f2") - data["byFrameStep"] = node.evalParm("f3") - - return data diff --git a/openpype/hosts/houdini/plugins/publish/collect_instances.py b/openpype/hosts/houdini/plugins/publish/collect_instances.py index 3772c9e705..132d297d1d 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_instances.py +++ b/openpype/hosts/houdini/plugins/publish/collect_instances.py @@ -102,16 +102,4 @@ class CollectInstances(pyblish.api.ContextPlugin): """ - data = {} - - if node.parm("trange") is None: - return data - - if node.evalParm("trange") == 0: - return data - - data["frameStart"] = node.evalParm("f1") - data["frameEnd"] = node.evalParm("f2") - data["byFrameStep"] = node.evalParm("f3") - - return data + return lib.get_frame_data(self, node) diff --git a/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py b/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py index 2a6be6b9f1..d91b9333b2 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py +++ b/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py @@ -19,16 +19,20 @@ class CollectRopFrameRange(pyblish.api.InstancePlugin): return ropnode = hou.node(node_path) - frame_data = lib.get_frame_data(ropnode) + + asset_data = instance.context.data["assetEntity"]["data"] + frame_data = lib.get_frame_data(self, ropnode, asset_data) if "frameStart" in frame_data and "frameEnd" in frame_data: # Log artist friendly message about the collected frame range message = ( "Frame range {0[frameStart]} - {0[frameEnd]}" - ).format(frame_data) - if frame_data.get("step", 1.0) != 1.0: - message += " with step {0[step]}".format(frame_data) + .format(frame_data) + ) + if frame_data.get("byFrameStep", 1.0) != 1.0: + message += " with step {0[byFrameStep]}".format(frame_data) + self.log.info(message) instance.data.update(frame_data) @@ -36,6 +40,6 @@ class CollectRopFrameRange(pyblish.api.InstancePlugin): # Add frame range to label if the instance has a frame range. label = instance.data.get("label", instance.data["name"]) instance.data["label"] = ( - "{0} [{1[frameStart]} - {1[frameEnd]}]".format(label, - frame_data) + "{0} [{1[frameStart]} - {1[frameEnd]}]" + .format(label,frame_data) ) From 1629c76f2c4fa015b5ba472cc889fc4a9a6c10f2 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 4 Oct 2023 16:42:01 +0300 Subject: [PATCH 0128/1233] consider handleStart and End in expected files --- openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py | 4 ++-- openpype/hosts/houdini/plugins/publish/collect_karma_rop.py | 4 ++-- openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py | 4 ++-- .../hosts/houdini/plugins/publish/collect_redshift_rop.py | 4 ++-- openpype/hosts/houdini/plugins/publish/collect_vray_rop.py | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py index 43b8428c60..f1bd10345f 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py @@ -126,8 +126,8 @@ class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin): return path expected_files = [] - start = instance.data["frameStart"] - end = instance.data["frameEnd"] + start = instance.data.get("frameStartHandle") or instance.data["frameStart"] + end = instance.data.get("frameEndHandle") or instance.data["frameEnd"] for i in range(int(start), (int(end) + 1)): expected_files.append( os.path.join(dir, (file % i)).replace("\\", "/")) diff --git a/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py b/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py index eabb1128d8..0a2fbfbac8 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py @@ -95,8 +95,8 @@ class CollectKarmaROPRenderProducts(pyblish.api.InstancePlugin): return path expected_files = [] - start = instance.data["frameStart"] - end = instance.data["frameEnd"] + start = instance.data.get("frameStartHandle") or instance.data["frameStart"] + end = instance.data.get("frameEndHandle") or instance.data["frameEnd"] for i in range(int(start), (int(end) + 1)): expected_files.append( os.path.join(dir, (file % i)).replace("\\", "/")) diff --git a/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py b/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py index c4460f5350..5a2e8fc24a 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py @@ -118,8 +118,8 @@ class CollectMantraROPRenderProducts(pyblish.api.InstancePlugin): return path expected_files = [] - start = instance.data["frameStart"] - end = instance.data["frameEnd"] + start = instance.data.get("frameStartHandle") or instance.data["frameStart"] + end = instance.data.get("frameEndHandle") or instance.data["frameEnd"] for i in range(int(start), (int(end) + 1)): expected_files.append( os.path.join(dir, (file % i)).replace("\\", "/")) diff --git a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py index dbb15ab88f..8cfcd93dae 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py @@ -132,8 +132,8 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin): return path expected_files = [] - start = instance.data["frameStart"] - end = instance.data["frameEnd"] + start = instance.data.get("frameStartHandle") or instance.data["frameStart"] + end = instance.data.get("frameEndHandle") or instance.data["frameEnd"] for i in range(int(start), (int(end) + 1)): expected_files.append( os.path.join(dir, (file % i)).replace("\\", "/")) diff --git a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py index 277f922ba4..823a1a6593 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py @@ -115,8 +115,8 @@ class CollectVrayROPRenderProducts(pyblish.api.InstancePlugin): return path expected_files = [] - start = instance.data["frameStart"] - end = instance.data["frameEnd"] + start = instance.data.get("frameStartHandle") or instance.data["frameStart"] + end = instance.data.get("frameEndHandle") or instance.data["frameEnd"] for i in range(int(start), (int(end) + 1)): expected_files.append( os.path.join(dir, (file % i)).replace("\\", "/")) From 8ab9e6d6b2e27d254e6ca79f9efdd5c7eafa14e6 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 4 Oct 2023 17:00:01 +0300 Subject: [PATCH 0129/1233] consider handleStart and End when submitting a deadline render job --- .../plugins/publish/submit_houdini_render_deadline.py | 7 +++++-- 1 file changed, 5 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 8f21a920be..2dbc17684b 100644 --- a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py @@ -65,9 +65,12 @@ class HoudiniSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): job_info.BatchName += datetime.now().strftime("%d%m%Y%H%M%S") # Deadline requires integers in frame range + start = instance.data.get("frameStartHandle") or instance.data["frameStart"] + end = instance.data.get("frameEndHandle") or instance.data["frameEnd"] + frames = "{start}-{end}x{step}".format( - start=int(instance.data["frameStart"]), - end=int(instance.data["frameEnd"]), + start=int(start), + end=int(end), step=int(instance.data["byFrameStep"]), ) job_info.Frames = frames From cc7f152404f0559520f00a1274fcf1003043213d Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 4 Oct 2023 17:15:42 +0300 Subject: [PATCH 0130/1233] resolve hound --- openpype/hosts/houdini/api/lib.py | 5 ++++- .../hosts/houdini/plugins/publish/collect_arnold_rop.py | 8 ++++++-- .../hosts/houdini/plugins/publish/collect_karma_rop.py | 8 ++++++-- .../hosts/houdini/plugins/publish/collect_mantra_rop.py | 8 ++++++-- .../hosts/houdini/plugins/publish/collect_redshift_rop.py | 8 ++++++-- .../houdini/plugins/publish/collect_rop_frame_range.py | 2 +- .../hosts/houdini/plugins/publish/collect_vray_rop.py | 8 ++++++-- .../plugins/publish/submit_houdini_render_deadline.py | 7 +++++-- 8 files changed, 40 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index ff6c62a143..964a866a79 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -548,7 +548,7 @@ def get_template_from_value(key, value): return parm -def get_frame_data(self, node, asset_data={}): +def get_frame_data(self, node, asset_data=None): """Get the frame data: start frame, end frame and steps. Args: @@ -558,6 +558,9 @@ def get_frame_data(self, node, asset_data={}): dict: frame data for star, end and steps. """ + if asset_data is None: + asset_data = {} + data = {} if node.parm("trange") is None: diff --git a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py index f1bd10345f..edd71bfa39 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py @@ -126,8 +126,12 @@ class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin): return path expected_files = [] - start = instance.data.get("frameStartHandle") or instance.data["frameStart"] - end = instance.data.get("frameEndHandle") or instance.data["frameEnd"] + start = instance.data.get("frameStartHandle") or \ + instance.data["frameStart"] + + end = instance.data.get("frameEndHandle") or \ + instance.data["frameEnd"] + for i in range(int(start), (int(end) + 1)): expected_files.append( os.path.join(dir, (file % i)).replace("\\", "/")) diff --git a/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py b/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py index 0a2fbfbac8..564b58ebc2 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py @@ -95,8 +95,12 @@ class CollectKarmaROPRenderProducts(pyblish.api.InstancePlugin): return path expected_files = [] - start = instance.data.get("frameStartHandle") or instance.data["frameStart"] - end = instance.data.get("frameEndHandle") or instance.data["frameEnd"] + start = instance.data.get("frameStartHandle") or \ + instance.data["frameStart"] + + end = instance.data.get("frameEndHandle") or \ + instance.data["frameEnd"] + for i in range(int(start), (int(end) + 1)): expected_files.append( os.path.join(dir, (file % i)).replace("\\", "/")) diff --git a/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py b/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py index 5a2e8fc24a..5ece889694 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py @@ -118,8 +118,12 @@ class CollectMantraROPRenderProducts(pyblish.api.InstancePlugin): return path expected_files = [] - start = instance.data.get("frameStartHandle") or instance.data["frameStart"] - end = instance.data.get("frameEndHandle") or instance.data["frameEnd"] + start = instance.data.get("frameStartHandle") or \ + instance.data["frameStart"] + + end = instance.data.get("frameEndHandle") or \ + instance.data["frameEnd"] + for i in range(int(start), (int(end) + 1)): expected_files.append( os.path.join(dir, (file % i)).replace("\\", "/")) diff --git a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py index 8cfcd93dae..1705da6a69 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py @@ -132,8 +132,12 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin): return path expected_files = [] - start = instance.data.get("frameStartHandle") or instance.data["frameStart"] - end = instance.data.get("frameEndHandle") or instance.data["frameEnd"] + start = instance.data.get("frameStartHandle") or \ + instance.data["frameStart"] + + end = instance.data.get("frameEndHandle") or \ + instance.data["frameEnd"] + for i in range(int(start), (int(end) + 1)): expected_files.append( os.path.join(dir, (file % i)).replace("\\", "/")) diff --git a/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py b/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py index d91b9333b2..bf44e019a9 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py +++ b/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py @@ -41,5 +41,5 @@ class CollectRopFrameRange(pyblish.api.InstancePlugin): label = instance.data.get("label", instance.data["name"]) instance.data["label"] = ( "{0} [{1[frameStart]} - {1[frameEnd]}]" - .format(label,frame_data) + .format(label, frame_data) ) diff --git a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py index 823a1a6593..ec87e3eda3 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py @@ -115,8 +115,12 @@ class CollectVrayROPRenderProducts(pyblish.api.InstancePlugin): return path expected_files = [] - start = instance.data.get("frameStartHandle") or instance.data["frameStart"] - end = instance.data.get("frameEndHandle") or instance.data["frameEnd"] + start = instance.data.get("frameStartHandle") or \ + instance.data["frameStart"] + + end = instance.data.get("frameEndHandle") or \ + instance.data["frameEnd"] + for i in range(int(start), (int(end) + 1)): expected_files.append( os.path.join(dir, (file % i)).replace("\\", "/")) 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 2dbc17684b..5dd16306c3 100644 --- a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py @@ -65,8 +65,11 @@ class HoudiniSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): job_info.BatchName += datetime.now().strftime("%d%m%Y%H%M%S") # Deadline requires integers in frame range - start = instance.data.get("frameStartHandle") or instance.data["frameStart"] - end = instance.data.get("frameEndHandle") or instance.data["frameEnd"] + start = instance.data.get("frameStartHandle") or \ + instance.data["frameStart"] + + end = instance.data.get("frameEndHandle") or \ + instance.data["frameEnd"] frames = "{start}-{end}x{step}".format( start=int(start), From 042d5d9d16546a6a0a57687a103870976f833141 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 4 Oct 2023 19:10:26 +0200 Subject: [PATCH 0131/1233] colorspace types in plural also updating and fixing unit tests --- openpype/pipeline/colorspace.py | 23 ++++++++------ openpype/scripts/ocio_wrapper.py | 6 ++-- .../pipeline/publish/test_publish_plugins.py | 13 ++++---- ...test_colorspace_get_labeled_colorspaces.py | 30 +++++++++---------- 4 files changed, 38 insertions(+), 34 deletions(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index c456baa70f..1088a15157 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -358,7 +358,7 @@ def parse_colorspace_from_filepath( colorspaces = ( colorspaces - or get_ocio_config_colorspaces(config_path)["colorspace"] + or get_ocio_config_colorspaces(config_path)["colorspaces"] ) underscored_colorspaces = { key.replace(" ", "_"): key for key in colorspaces @@ -396,7 +396,7 @@ def validate_imageio_colorspace_in_config(config_path, colorspace_name): Returns: bool: True if exists """ - colorspaces = get_ocio_config_colorspaces(config_path)["colorspace"] + colorspaces = get_ocio_config_colorspaces(config_path)["colorspaces"] if colorspace_name not in colorspaces: raise KeyError( "Missing colorspace '{}' in config file '{}'".format( @@ -561,20 +561,25 @@ def get_labeled_colorspaces( looks = set() roles = set() for items_type, colorspace_items in config_items.items(): - if items_type == "colorspace": + if items_type == "colorspaces": for color_name, color_data in colorspace_items.items(): if color_data.get("aliases"): aliases.update([ - (alias_name, "[alias] {} ({})".format(alias_name, color_name)) - for alias_name in color_data["aliases"] - ]) + ( + alias_name, + "[alias] {} ({})".format(alias_name, color_name) + ) + for alias_name in color_data["aliases"] + ]) colorspaces.add(color_name) - elif items_type == "look": + + elif items_type == "looks": looks.update([ (name, "[look] {} ({})".format(name, role_data["process_space"])) for name, role_data in colorspace_items.items() ]) - elif items_type == "role": + + elif items_type == "roles": roles.update([ (name, "[role] {} ({})".format(name, role_data["colorspace"])) for name, role_data in colorspace_items.items() @@ -583,6 +588,7 @@ def get_labeled_colorspaces( if roles and include_roles: labeled_colorspaces.extend(roles) + # add colorspace after roles so it is first in menu labeled_colorspaces.extend(( (name, f"[colorspace] {name}") for name in colorspaces )) @@ -593,7 +599,6 @@ def get_labeled_colorspaces( if looks and include_looks: labeled_colorspaces.extend(looks) - return labeled_colorspaces diff --git a/openpype/scripts/ocio_wrapper.py b/openpype/scripts/ocio_wrapper.py index be21f0984f..bca977cc3b 100644 --- a/openpype/scripts/ocio_wrapper.py +++ b/openpype/scripts/ocio_wrapper.py @@ -107,7 +107,7 @@ def _get_colorspace_data(config_path): config = ocio.Config().CreateFromFile(str(config_path)) colorspace_data = { - "colorspace": { + "colorspaces": { color.getName(): { "family": color.getFamily(), "categories": list(color.getCategories()), @@ -121,7 +121,7 @@ def _get_colorspace_data(config_path): # add looks looks = config.getLooks() if looks: - colorspace_data["look"] = { + colorspace_data["looks"] = { look.getName(): {"process_space": look.getProcessSpace()} for look in looks } @@ -129,7 +129,7 @@ def _get_colorspace_data(config_path): # add roles roles = config.getRoles() if roles: - colorspace_data["role"] = { + colorspace_data["roles"] = { role: {"colorspace": colorspace} for (role, colorspace) in roles } diff --git a/tests/unit/openpype/pipeline/publish/test_publish_plugins.py b/tests/unit/openpype/pipeline/publish/test_publish_plugins.py index aace8cf7e3..1f7f551237 100644 --- a/tests/unit/openpype/pipeline/publish/test_publish_plugins.py +++ b/tests/unit/openpype/pipeline/publish/test_publish_plugins.py @@ -37,7 +37,7 @@ class TestPipelinePublishPlugins(TestPipeline): # files are the same as those used in `test_pipeline_colorspace` TEST_FILES = [ ( - "1Lf-mFxev7xiwZCWfImlRcw7Fj8XgNQMh", + "1csqimz8bbNcNgxtEXklLz6GRv91D3KgA", "test_pipeline_colorspace.zip", "" ) @@ -123,8 +123,7 @@ class TestPipelinePublishPlugins(TestPipeline): def test_get_colorspace_settings(self, context, config_path_asset): expected_config_template = ( - "{root[work]}/{project[name]}" - "/{hierarchy}/{asset}/config/aces.ocio" + "{root[work]}/{project[name]}/config/aces.ocio" ) expected_file_rules = { "comp_review": { @@ -177,16 +176,16 @@ class TestPipelinePublishPlugins(TestPipeline): # load plugin function for testing plugin = publish_plugins.ColormanagedPyblishPluginMixin() plugin.log = log + context.data["imageioSettings"] = (config_data_nuke, file_rules_nuke) plugin.set_representation_colorspace( - representation_nuke, context, - colorspace_settings=(config_data_nuke, file_rules_nuke) + representation_nuke, context ) # load plugin function for testing plugin = publish_plugins.ColormanagedPyblishPluginMixin() plugin.log = log + context.data["imageioSettings"] = (config_data_hiero, file_rules_hiero) plugin.set_representation_colorspace( - representation_hiero, context, - colorspace_settings=(config_data_hiero, file_rules_hiero) + representation_hiero, context ) colorspace_data_nuke = representation_nuke.get("colorspaceData") diff --git a/tests/unit/openpype/pipeline/test_colorspace_get_labeled_colorspaces.py b/tests/unit/openpype/pipeline/test_colorspace_get_labeled_colorspaces.py index a135c3258b..ae3e4117bc 100644 --- a/tests/unit/openpype/pipeline/test_colorspace_get_labeled_colorspaces.py +++ b/tests/unit/openpype/pipeline/test_colorspace_get_labeled_colorspaces.py @@ -7,16 +7,16 @@ class TestGetLabeledColorspaces(unittest.TestCase): @patch('openpype.pipeline.colorspace.get_ocio_config_colorspaces') def test_returns_list_of_tuples(self, mock_get_ocio_config_colorspaces): mock_get_ocio_config_colorspaces.return_value = { - 'colorspace': { + 'colorspaces': { 'sRGB': {}, 'Rec.709': {}, }, - 'look': { + 'looks': { 'sRGB to Rec.709': { 'process_space': 'Rec.709', }, }, - 'role': { + 'roles': { 'reference': { 'colorspace': 'sRGB', }, @@ -29,11 +29,11 @@ class TestGetLabeledColorspaces(unittest.TestCase): @patch('openpype.pipeline.colorspace.get_ocio_config_colorspaces') def test_includes_colorspaces(self, mock_get_ocio_config_colorspaces): mock_get_ocio_config_colorspaces.return_value = { - 'colorspace': { + 'colorspaces': { 'sRGB': {} }, - 'look': {}, - 'role': {}, + 'looks': {}, + 'roles': {}, } result = get_labeled_colorspaces('config.ocio', include_aliases=False, include_looks=False, include_roles=False) self.assertEqual(result, [('sRGB', '[colorspace] sRGB')]) @@ -41,13 +41,13 @@ class TestGetLabeledColorspaces(unittest.TestCase): @patch('openpype.pipeline.colorspace.get_ocio_config_colorspaces') def test_includes_aliases(self, mock_get_ocio_config_colorspaces): mock_get_ocio_config_colorspaces.return_value = { - 'colorspace': { + 'colorspaces': { 'sRGB': { 'aliases': ['sRGB (D65)'], }, }, - 'look': {}, - 'role': {}, + 'looks': {}, + 'roles': {}, } result = get_labeled_colorspaces('config.ocio', include_aliases=True, include_looks=False, include_roles=False) self.assertEqual(result, [('sRGB', '[colorspace] sRGB'), ('sRGB (D65)', '[alias] sRGB (D65) (sRGB)')]) @@ -55,13 +55,13 @@ class TestGetLabeledColorspaces(unittest.TestCase): @patch('openpype.pipeline.colorspace.get_ocio_config_colorspaces') def test_includes_looks(self, mock_get_ocio_config_colorspaces): mock_get_ocio_config_colorspaces.return_value = { - 'colorspace': {}, - 'look': { + 'colorspaces': {}, + 'looks': { 'sRGB to Rec.709': { 'process_space': 'Rec.709', }, }, - 'role': {}, + 'roles': {}, } result = get_labeled_colorspaces('config.ocio', include_aliases=False, include_looks=True, include_roles=False) self.assertEqual(result, [('sRGB to Rec.709', '[look] sRGB to Rec.709 (Rec.709)')]) @@ -69,9 +69,9 @@ class TestGetLabeledColorspaces(unittest.TestCase): @patch('openpype.pipeline.colorspace.get_ocio_config_colorspaces') def test_includes_roles(self, mock_get_ocio_config_colorspaces): mock_get_ocio_config_colorspaces.return_value = { - 'colorspace': {}, - 'look': {}, - 'role': { + 'colorspaces': {}, + 'looks': {}, + 'roles': { 'reference': { 'colorspace': 'sRGB', }, From 957a713db216c82f9e30d01c8aecea7f2ac36663 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 4 Oct 2023 20:04:06 +0200 Subject: [PATCH 0132/1233] adding display and views --- openpype/pipeline/colorspace.py | 11 +++++++++++ openpype/scripts/ocio_wrapper.py | 13 ++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 1088a15157..8985b07cde 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -538,6 +538,7 @@ def get_labeled_colorspaces( include_aliases=False, include_looks=False, include_roles=False, + include_display_views=False ): """Get all colorspace data with labels @@ -560,6 +561,7 @@ def get_labeled_colorspaces( colorspaces = set() looks = set() roles = set() + display_views = set() for items_type, colorspace_items in config_items.items(): if items_type == "colorspaces": for color_name, color_data in colorspace_items.items(): @@ -579,6 +581,12 @@ def get_labeled_colorspaces( for name, role_data in colorspace_items.items() ]) + elif items_type == "displays_views": + display_views.update([ + (name, "[view (display)] {}".format(name)) + for name, _ in colorspace_items.items() + ]) + elif items_type == "roles": roles.update([ (name, "[role] {} ({})".format(name, role_data["colorspace"])) @@ -599,6 +607,9 @@ def get_labeled_colorspaces( if looks and include_looks: labeled_colorspaces.extend(looks) + if display_views and include_display_views: + labeled_colorspaces.extend(display_views) + return labeled_colorspaces diff --git a/openpype/scripts/ocio_wrapper.py b/openpype/scripts/ocio_wrapper.py index bca977cc3b..fa231cd047 100644 --- a/openpype/scripts/ocio_wrapper.py +++ b/openpype/scripts/ocio_wrapper.py @@ -107,6 +107,7 @@ def _get_colorspace_data(config_path): config = ocio.Config().CreateFromFile(str(config_path)) colorspace_data = { + "roles": {}, "colorspaces": { color.getName(): { "family": color.getFamily(), @@ -115,7 +116,17 @@ def _get_colorspace_data(config_path): "equalitygroup": color.getEqualityGroup(), } for color in config.getColorSpaces() - } + }, + "displays_views": { + f"{view} ({display})": { + "display": display, + "view": view + + } + for display in config.getDisplays() + for view in config.getViews(display) + }, + "looks": {} } # add looks From 0c63b2691544723769f64ae110ba359bbdb0d73b Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 4 Oct 2023 23:29:25 +0300 Subject: [PATCH 0133/1233] resolve some comments and add frame range validator --- openpype/hosts/houdini/api/lib.py | 13 ++-- .../plugins/publish/collect_arnold_rop.py | 7 +- .../publish/collect_instance_frame_data.py | 2 +- .../plugins/publish/collect_instances.py | 2 +- .../plugins/publish/collect_karma_rop.py | 7 +- .../plugins/publish/collect_mantra_rop.py | 7 +- .../plugins/publish/collect_redshift_rop.py | 7 +- .../publish/collect_rop_frame_range.py | 2 +- .../plugins/publish/collect_vray_rop.py | 7 +- .../plugins/publish/validate_frame_range.py | 75 +++++++++++++++++++ .../publish/submit_houdini_render_deadline.py | 8 +- 11 files changed, 98 insertions(+), 39 deletions(-) create mode 100644 openpype/hosts/houdini/plugins/publish/validate_frame_range.py diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 964a866a79..711fae7cc4 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -548,7 +548,7 @@ def get_template_from_value(key, value): return parm -def get_frame_data(self, node, asset_data=None): +def get_frame_data(node, asset_data=None, log=None): """Get the frame data: start frame, end frame and steps. Args: @@ -561,28 +561,31 @@ def get_frame_data(self, node, asset_data=None): if asset_data is None: asset_data = {} + if log is None: + log = self.log + data = {} if node.parm("trange") is None: - self.log.debug( + log.debug( "Node has no 'trange' parameter: {}".format(node.path()) ) return data if node.evalParm("trange") == 0: - self.log.debug( + log.debug( "Node '{}' has 'Render current frame' set. " "Time range data ignored.".format(node.path()) ) return data data["frameStartHandle"] = node.evalParm("f1") - data["frameStart"] = node.evalParm("f1") + asset_data.get("handleStart", 0) data["handleStart"] = asset_data.get("handleStart", 0) + data["frameStart"] = data["frameStartHandle"] + data["handleStart"] data["frameEndHandle"] = node.evalParm("f2") - data["frameEnd"] = node.evalParm("f2") - asset_data.get("handleEnd", 0) data["handleEnd"] = asset_data.get("handleEnd", 0) + data["frameEnd"] = data["frameEndHandle"] - data["handleEnd"] data["byFrameStep"] = node.evalParm("f3") diff --git a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py index edd71bfa39..9933572f4a 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py @@ -126,11 +126,8 @@ class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin): return path expected_files = [] - start = instance.data.get("frameStartHandle") or \ - instance.data["frameStart"] - - end = instance.data.get("frameEndHandle") or \ - instance.data["frameEnd"] + start = instance.data.get("frameStartHandle") + end = instance.data.get("frameEndHandle") for i in range(int(start), (int(end) + 1)): expected_files.append( diff --git a/openpype/hosts/houdini/plugins/publish/collect_instance_frame_data.py b/openpype/hosts/houdini/plugins/publish/collect_instance_frame_data.py index 97d18f97f0..1426eadda1 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_instance_frame_data.py +++ b/openpype/hosts/houdini/plugins/publish/collect_instance_frame_data.py @@ -21,7 +21,7 @@ class CollectInstanceNodeFrameRange(pyblish.api.InstancePlugin): return asset_data = instance.context.data["assetEntity"]["data"] - frame_data = lib.get_frame_data(self, node, asset_data) + frame_data = lib.get_frame_data(node, asset_data, self.log) if not frame_data: return diff --git a/openpype/hosts/houdini/plugins/publish/collect_instances.py b/openpype/hosts/houdini/plugins/publish/collect_instances.py index 132d297d1d..b2e6107435 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_instances.py +++ b/openpype/hosts/houdini/plugins/publish/collect_instances.py @@ -102,4 +102,4 @@ class CollectInstances(pyblish.api.ContextPlugin): """ - return lib.get_frame_data(self, node) + return lib.get_frame_data(node) diff --git a/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py b/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py index 564b58ebc2..32790dd550 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py @@ -95,11 +95,8 @@ class CollectKarmaROPRenderProducts(pyblish.api.InstancePlugin): return path expected_files = [] - start = instance.data.get("frameStartHandle") or \ - instance.data["frameStart"] - - end = instance.data.get("frameEndHandle") or \ - instance.data["frameEnd"] + start = instance.data.get("frameStartHandle") + end = instance.data.get("frameEndHandle") for i in range(int(start), (int(end) + 1)): expected_files.append( diff --git a/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py b/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py index 5ece889694..daaf87c04c 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py @@ -118,11 +118,8 @@ class CollectMantraROPRenderProducts(pyblish.api.InstancePlugin): return path expected_files = [] - start = instance.data.get("frameStartHandle") or \ - instance.data["frameStart"] - - end = instance.data.get("frameEndHandle") or \ - instance.data["frameEnd"] + start = instance.data.get("frameStartHandle") + end = instance.data.get("frameEndHandle") for i in range(int(start), (int(end) + 1)): expected_files.append( diff --git a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py index 1705da6a69..5ade67d181 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py @@ -132,11 +132,8 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin): return path expected_files = [] - start = instance.data.get("frameStartHandle") or \ - instance.data["frameStart"] - - end = instance.data.get("frameEndHandle") or \ - instance.data["frameEnd"] + start = instance.data.get("frameStartHandle") + end = instance.data.get("frameEndHandle") for i in range(int(start), (int(end) + 1)): expected_files.append( diff --git a/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py b/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py index bf44e019a9..ccaa8b58e0 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py +++ b/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py @@ -21,7 +21,7 @@ class CollectRopFrameRange(pyblish.api.InstancePlugin): ropnode = hou.node(node_path) asset_data = instance.context.data["assetEntity"]["data"] - frame_data = lib.get_frame_data(self, ropnode, asset_data) + frame_data = lib.get_frame_data(ropnode, asset_data, self.log) if "frameStart" in frame_data and "frameEnd" in frame_data: diff --git a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py index ec87e3eda3..e5c6ec20c4 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py @@ -115,11 +115,8 @@ class CollectVrayROPRenderProducts(pyblish.api.InstancePlugin): return path expected_files = [] - start = instance.data.get("frameStartHandle") or \ - instance.data["frameStart"] - - end = instance.data.get("frameEndHandle") or \ - instance.data["frameEnd"] + start = instance.data.get("frameStartHandle") + end = instance.data.get("frameEndHandle") for i in range(int(start), (int(end) + 1)): expected_files.append( diff --git a/openpype/hosts/houdini/plugins/publish/validate_frame_range.py b/openpype/hosts/houdini/plugins/publish/validate_frame_range.py new file mode 100644 index 0000000000..e1be99dbcf --- /dev/null +++ b/openpype/hosts/houdini/plugins/publish/validate_frame_range.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +import pyblish.api +from openpype.pipeline import PublishValidationError +from openpype.pipeline.publish import RepairAction +from openpype.hosts.houdini.api.action import SelectInvalidAction + +import hou + + +class HotFixAction(RepairAction): + """Set End frame to the minimum valid value.""" + + label = "End frame hotfix" + + +class ValidateFrameRange(pyblish.api.InstancePlugin): + """Validate Frame Range. + + Due to the usage of start and end handles, + then Frame Range must be >= (start handle + end handle) + which results that frameEnd be smaller than frameStart + """ + + order = pyblish.api.ValidatorOrder - 0.1 + hosts = ["houdini"] + label = "Validate Frame Range" + actions = [HotFixAction, SelectInvalidAction] + + def process(self, instance): + + invalid = self.get_invalid(instance) + if invalid: + nodes = [n.path() for n in invalid] + raise PublishValidationError( + "Invalid Frame Range on: {0}".format(nodes), + title="Invalid Frame Range" + ) + + @classmethod + def get_invalid(cls, instance): + + if not instance.data.get("instance_node"): + return + + rop_node = hou.node(instance.data["instance_node"]) + if instance.data["frameStart"] > instance.data["frameEnd"]: + cls.log.error( + "Wrong frame range, please consider handle start and end.\n" + "frameEnd should at least be {}.\n" + "Use \"End frame hotfix\" action to do that." + .format( + instance.data["handleEnd"] + + instance.data["handleStart"] + + instance.data["frameStartHandle"] + ) + ) + return [rop_node] + + @classmethod + def repair(cls, instance): + rop_node = hou.node(instance.data["instance_node"]) + + frame_start = int(instance.data["frameStartHandle"]) + frame_end = int( + instance.data["frameStartHandle"] + + instance.data["handleStart"] + + instance.data["handleEnd"] + ) + + if rop_node.parm("f2").rawValue() == "$FEND": + hou.playbar.setFrameRange(frame_start, frame_end) + hou.playbar.setPlaybackRange(frame_start, frame_end) + hou.setFrame(frame_start) + else: + rop_node.parm("f2").set(frame_end) 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 5dd16306c3..cd71095920 100644 --- a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py @@ -65,12 +65,8 @@ class HoudiniSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): job_info.BatchName += datetime.now().strftime("%d%m%Y%H%M%S") # Deadline requires integers in frame range - start = instance.data.get("frameStartHandle") or \ - instance.data["frameStart"] - - end = instance.data.get("frameEndHandle") or \ - instance.data["frameEnd"] - + start = instance.data.get("frameStartHandle") + end = instance.data.get("frameEndHandle") frames = "{start}-{end}x{step}".format( start=int(start), end=int(end), From 664c27ced2688894ddef217f0fbe423119d68c4d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 5 Oct 2023 15:21:12 +0800 Subject: [PATCH 0134/1233] make sure it also validates resolution for vray renderer --- .../plugins/publish/validate_resolution.py | 70 +++++++++++++------ 1 file changed, 48 insertions(+), 22 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_resolution.py b/openpype/hosts/maya/plugins/publish/validate_resolution.py index 4c350388e2..7b89e9a3e6 100644 --- a/openpype/hosts/maya/plugins/publish/validate_resolution.py +++ b/openpype/hosts/maya/plugins/publish/validate_resolution.py @@ -4,50 +4,76 @@ from openpype.pipeline import ( OptionalPyblishPluginMixin ) from maya import cmds +from openpype.pipeline.publish import RepairAction +from openpype.hosts.maya.api import lib from openpype.hosts.maya.api.lib import reset_scene_resolution -class ValidateResolution(pyblish.api.InstancePlugin, - OptionalPyblishPluginMixin): - """Validate the resolution setting aligned with DB""" +class ValidateSceneResolution(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): + """Validate the scene resolution setting aligned with DB""" order = pyblish.api.ValidatorOrder - 0.01 families = ["renderlayer"] hosts = ["maya"] label = "Validate Resolution" + actions = [RepairAction] optional = True def process(self, instance): if not self.is_active(instance.data): return - width, height = self.get_db_resolution(instance) - current_width = cmds.getAttr("defaultResolution.width") - current_height = cmds.getAttr("defaultResolution.height") - if current_width != width and current_height != height: - raise PublishValidationError("Resolution Setting " - "not matching resolution " - "set on asset or shot.") - if current_width != width: - raise PublishValidationError("Width in Resolution Setting " - "not matching resolution set " - "on asset or shot.") - - if current_height != height: - raise PublishValidationError("Height in Resolution Setting " - "not matching resolution set " - "on asset or shot.") + width, height, pixelAspect = self.get_db_resolution(instance) + current_renderer = cmds.getAttr( + "defaultRenderGlobals.currentRenderer") + layer = instance.data["renderlayer"] + if current_renderer == "vray": + vray_node = "vraySettings" + if cmds.objExists(vray_node): + control_node = vray_node + current_width = lib.get_attr_in_layer( + "{}.width".format(control_node), layer=layer) + current_height = lib.get_attr_in_layer( + "{}.height".format(control_node), layer=layer) + current_pixelAspect = lib.get_attr_in_layer( + "{}.pixelAspect".format(control_node), layer=layer + ) + else: + raise PublishValidationError( + "Can't set VRay resolution because there is no node " + "named: `%s`" % vray_node) + else: + current_width = lib.get_attr_in_layer( + "defaultResolution.width", layer=layer) + current_height = lib.get_attr_in_layer( + "defaultResolution.height", layer=layer) + current_pixelAspect = lib.get_attr_in_layer( + "defaultResolution.pixelAspect", layer=layer + ) + if current_width != width or current_height != height: + raise PublishValidationError( + "Render resolution is {}x{} does not match asset resolution is {}x{}".format( + current_width, current_height, width, height + )) + if current_pixelAspect != pixelAspect: + raise PublishValidationError( + "Render pixel aspect is {} does not match asset pixel aspect is {}".format( + current_pixelAspect, pixelAspect + )) def get_db_resolution(self, instance): 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: + if "resolutionWidth" in data and "resolutionHeight" in data \ + and "pixelAspect" in data: width = data["resolutionWidth"] height = data["resolutionHeight"] - return int(width), int(height) + pixelAspect = data["pixelAspect"] + return int(width), int(height), int(pixelAspect) # Defaults if not found in asset document or project document - return 1920, 1080 + return 1920, 1080, 1 @classmethod def repair(cls, instance): From c81b0af8390a97fb86befae0aa8a310b8864d716 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 5 Oct 2023 15:24:33 +0800 Subject: [PATCH 0135/1233] hound --- .../hosts/maya/plugins/publish/validate_resolution.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_resolution.py b/openpype/hosts/maya/plugins/publish/validate_resolution.py index 7b89e9a3e6..856c2811ea 100644 --- a/openpype/hosts/maya/plugins/publish/validate_resolution.py +++ b/openpype/hosts/maya/plugins/publish/validate_resolution.py @@ -52,12 +52,12 @@ class ValidateSceneResolution(pyblish.api.InstancePlugin, ) if current_width != width or current_height != height: raise PublishValidationError( - "Render resolution is {}x{} does not match asset resolution is {}x{}".format( + "Render resolution {}x{} does not match asset resolution {}x{}".format( # noqa:E501 current_width, current_height, width, height )) if current_pixelAspect != pixelAspect: - raise PublishValidationError( - "Render pixel aspect is {} does not match asset pixel aspect is {}".format( + raise PublishValidationError( + "Render pixel aspect {} does not match asset pixel aspect {}".format( # noqa:E501 current_pixelAspect, pixelAspect )) @@ -66,7 +66,7 @@ class ValidateSceneResolution(pyblish.api.InstancePlugin, project_doc = instance.context.data["projectEntity"] for data in [asset_doc["data"], project_doc["data"]]: if "resolutionWidth" in data and "resolutionHeight" in data \ - and "pixelAspect" in data: + and "pixelAspect" in data: width = data["resolutionWidth"] height = data["resolutionHeight"] pixelAspect = data["pixelAspect"] From d1f5f6eb4a1bfa5f55907eb0bccb9591855914fb Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 5 Oct 2023 15:28:30 +0800 Subject: [PATCH 0136/1233] hound --- openpype/hosts/maya/plugins/publish/validate_resolution.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_resolution.py b/openpype/hosts/maya/plugins/publish/validate_resolution.py index 856c2811ea..578c99e006 100644 --- a/openpype/hosts/maya/plugins/publish/validate_resolution.py +++ b/openpype/hosts/maya/plugins/publish/validate_resolution.py @@ -65,8 +65,9 @@ class ValidateSceneResolution(pyblish.api.InstancePlugin, 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 \ - and "pixelAspect" in data: + if "resolutionWidth" in data and ( + "resolutionHeight" in data and "pixelAspect" in data + ): width = data["resolutionWidth"] height = data["resolutionHeight"] pixelAspect = data["pixelAspect"] From c8f6fb209bafc06dd12f536e64979e244af61c17 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 5 Oct 2023 11:31:30 +0300 Subject: [PATCH 0137/1233] allow reseting handles, and remove the redundant collector --- openpype/hosts/houdini/api/lib.py | 8 +-- .../publish/collect_instance_frame_data.py | 30 -------- .../plugins/publish/collect_instances.py | 12 ---- .../publish/collect_rop_frame_range.py | 70 ++++++++++++++----- 4 files changed, 58 insertions(+), 62 deletions(-) delete mode 100644 openpype/hosts/houdini/plugins/publish/collect_instance_frame_data.py diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 711fae7cc4..3c39b32b0d 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -564,20 +564,20 @@ def get_frame_data(node, asset_data=None, log=None): if log is None: log = self.log - data = {} - if node.parm("trange") is None: log.debug( "Node has no 'trange' parameter: {}".format(node.path()) ) - return data + return if node.evalParm("trange") == 0: log.debug( "Node '{}' has 'Render current frame' set. " "Time range data ignored.".format(node.path()) ) - return data + return + + data = {} data["frameStartHandle"] = node.evalParm("f1") data["handleStart"] = asset_data.get("handleStart", 0) diff --git a/openpype/hosts/houdini/plugins/publish/collect_instance_frame_data.py b/openpype/hosts/houdini/plugins/publish/collect_instance_frame_data.py deleted file mode 100644 index 1426eadda1..0000000000 --- a/openpype/hosts/houdini/plugins/publish/collect_instance_frame_data.py +++ /dev/null @@ -1,30 +0,0 @@ -import hou - -import pyblish.api -from openpype.hosts.houdini.api import lib - -class CollectInstanceNodeFrameRange(pyblish.api.InstancePlugin): - """Collect time range frame data for the instance node.""" - - order = pyblish.api.CollectorOrder + 0.001 - label = "Instance Node Frame Range" - hosts = ["houdini"] - - def process(self, instance): - - node_path = instance.data.get("instance_node") - node = hou.node(node_path) if node_path else None - if not node_path or not node: - self.log.debug( - "No instance node found for instance: {}".format(instance) - ) - return - - asset_data = instance.context.data["assetEntity"]["data"] - frame_data = lib.get_frame_data(node, asset_data, self.log) - - if not frame_data: - return - - self.log.info("Collected time data: {}".format(frame_data)) - instance.data.update(frame_data) diff --git a/openpype/hosts/houdini/plugins/publish/collect_instances.py b/openpype/hosts/houdini/plugins/publish/collect_instances.py index b2e6107435..52966fb3c2 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_instances.py +++ b/openpype/hosts/houdini/plugins/publish/collect_instances.py @@ -91,15 +91,3 @@ class CollectInstances(pyblish.api.ContextPlugin): context[:] = sorted(context, key=sort_by_family) return context - - def get_frame_data(self, node): - """Get the frame data: start frame, end frame and steps - Args: - node(hou.Node) - - Returns: - dict - - """ - - return lib.get_frame_data(node) diff --git a/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py b/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py index ccaa8b58e0..75c101ed0f 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py +++ b/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py @@ -2,12 +2,17 @@ """Collector plugin for frames data on ROP instances.""" import hou # noqa import pyblish.api +from openpype.lib import BoolDef from openpype.hosts.houdini.api import lib +from openpype.pipeline import OptionalPyblishPluginMixin -class CollectRopFrameRange(pyblish.api.InstancePlugin): +class CollectRopFrameRange(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): + """Collect all frames which would be saved from the ROP nodes""" + hosts = ["houdini"] order = pyblish.api.CollectorOrder label = "Collect RopNode Frame Range" @@ -16,30 +21,63 @@ class CollectRopFrameRange(pyblish.api.InstancePlugin): node_path = instance.data.get("instance_node") if node_path is None: # Instance without instance node like a workfile instance + self.log.debug( + "No instance node found for instance: {}".format(instance) + ) return ropnode = hou.node(node_path) - asset_data = instance.context.data["assetEntity"]["data"] + + attr_values = self.get_attr_values_from_data(instance.data) + if attr_values.get("reset_handles"): + asset_data["handleStart"] = 0 + asset_data["handleEnd"] = 0 + frame_data = lib.get_frame_data(ropnode, asset_data, self.log) - if "frameStart" in frame_data and "frameEnd" in frame_data: + if not frame_data: + return - # Log artist friendly message about the collected frame range - message = ( - "Frame range {0[frameStart]} - {0[frameEnd]}" + # Log artist friendly message about the collected frame range + message = "" + + if attr_values.get("reset_handles"): + message += ( + "Reset frame handles is activated for this instance, " + "start and end handles are set to 0.\n" + ) + else: + message += ( + "Full Frame range with Handles " + "{0[frameStartHandle]} - {0[frameEndHandle]}\n" .format(frame_data) ) - if frame_data.get("byFrameStep", 1.0) != 1.0: - message += " with step {0[byFrameStep]}".format(frame_data) - self.log.info(message) + message += ( + "Frame range {0[frameStart]} - {0[frameEnd]}" + .format(frame_data) + ) - instance.data.update(frame_data) + if frame_data.get("byFrameStep", 1.0) != 1.0: + message += "\nFrame steps {0[byFrameStep]}".format(frame_data) - # Add frame range to label if the instance has a frame range. - label = instance.data.get("label", instance.data["name"]) - instance.data["label"] = ( - "{0} [{1[frameStart]} - {1[frameEnd]}]" - .format(label, frame_data) - ) + self.log.info(message) + + instance.data.update(frame_data) + + # Add frame range to label if the instance has a frame range. + label = instance.data.get("label", instance.data["name"]) + instance.data["label"] = ( + "{0} [{1[frameStart]} - {1[frameEnd]}]" + .format(label, frame_data) + ) + + @classmethod + def get_attribute_defs(cls): + return [ + BoolDef("reset_handles", + tooltip="Set frame handles to zero", + default=False, + label="Reset frame handles") + ] From db029884b0cc2d527dceac07ed0dd85663b1f48f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 5 Oct 2023 11:57:48 +0200 Subject: [PATCH 0138/1233] colorspace labeled unittests for display and view --- ...test_colorspace_get_labeled_colorspaces.py | 57 +++++++++++++++++-- 1 file changed, 51 insertions(+), 6 deletions(-) diff --git a/tests/unit/openpype/pipeline/test_colorspace_get_labeled_colorspaces.py b/tests/unit/openpype/pipeline/test_colorspace_get_labeled_colorspaces.py index ae3e4117bc..1760000e45 100644 --- a/tests/unit/openpype/pipeline/test_colorspace_get_labeled_colorspaces.py +++ b/tests/unit/openpype/pipeline/test_colorspace_get_labeled_colorspaces.py @@ -35,7 +35,12 @@ class TestGetLabeledColorspaces(unittest.TestCase): 'looks': {}, 'roles': {}, } - result = get_labeled_colorspaces('config.ocio', include_aliases=False, include_looks=False, include_roles=False) + result = get_labeled_colorspaces( + 'config.ocio', + include_aliases=False, + include_looks=False, + include_roles=False + ) self.assertEqual(result, [('sRGB', '[colorspace] sRGB')]) @patch('openpype.pipeline.colorspace.get_ocio_config_colorspaces') @@ -49,8 +54,18 @@ class TestGetLabeledColorspaces(unittest.TestCase): 'looks': {}, 'roles': {}, } - result = get_labeled_colorspaces('config.ocio', include_aliases=True, include_looks=False, include_roles=False) - self.assertEqual(result, [('sRGB', '[colorspace] sRGB'), ('sRGB (D65)', '[alias] sRGB (D65) (sRGB)')]) + result = get_labeled_colorspaces( + 'config.ocio', + include_aliases=True, + include_looks=False, + include_roles=False + ) + self.assertEqual( + result, [ + ('sRGB', '[colorspace] sRGB'), + ('sRGB (D65)', '[alias] sRGB (D65) (sRGB)') + ] + ) @patch('openpype.pipeline.colorspace.get_ocio_config_colorspaces') def test_includes_looks(self, mock_get_ocio_config_colorspaces): @@ -63,8 +78,14 @@ class TestGetLabeledColorspaces(unittest.TestCase): }, 'roles': {}, } - result = get_labeled_colorspaces('config.ocio', include_aliases=False, include_looks=True, include_roles=False) - self.assertEqual(result, [('sRGB to Rec.709', '[look] sRGB to Rec.709 (Rec.709)')]) + result = get_labeled_colorspaces( + 'config.ocio', + include_aliases=False, + include_looks=True, + include_roles=False + ) + self.assertEqual( + result, [('sRGB to Rec.709', '[look] sRGB to Rec.709 (Rec.709)')]) @patch('openpype.pipeline.colorspace.get_ocio_config_colorspaces') def test_includes_roles(self, mock_get_ocio_config_colorspaces): @@ -77,5 +98,29 @@ class TestGetLabeledColorspaces(unittest.TestCase): }, }, } - result = get_labeled_colorspaces('config.ocio', include_aliases=False, include_looks=False, include_roles=True) + result = get_labeled_colorspaces( + 'config.ocio', + include_aliases=False, + include_looks=False, + include_roles=True + ) self.assertEqual(result, [('reference', '[role] reference (sRGB)')]) + + @patch('openpype.pipeline.colorspace.get_ocio_config_colorspaces') + def test_includes_display_views(self, mock_get_ocio_config_colorspaces): + mock_get_ocio_config_colorspaces.return_value = { + 'colorspaces': {}, + 'looks': {}, + 'roles': {}, + 'displays_views': { + 'sRGB (ACES)': { + 'view': 'sRGB', + 'display': 'ACES', + }, + }, + } + result = get_labeled_colorspaces( + 'config.ocio', + include_display_views=True + ) + self.assertEqual(result, [('sRGB (ACES)', '[view (display)] sRGB (ACES)')]) From 0c0b52d850341681dfde08acc94d36fc660f1617 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 5 Oct 2023 11:43:28 +0100 Subject: [PATCH 0139/1233] Dont update node name on update --- openpype/hosts/nuke/plugins/load/load_image.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/nuke/plugins/load/load_image.py b/openpype/hosts/nuke/plugins/load/load_image.py index 0dd3a940db..6bffb97e6f 100644 --- a/openpype/hosts/nuke/plugins/load/load_image.py +++ b/openpype/hosts/nuke/plugins/load/load_image.py @@ -204,8 +204,6 @@ 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) From 839269562347abde3132439fec3c2a78e9a23a44 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 5 Oct 2023 11:49:19 +0100 Subject: [PATCH 0140/1233] Improved update for Geometry Caches --- openpype/hosts/unreal/api/pipeline.py | 82 +++++---- .../plugins/load/load_geometrycache_abc.py | 162 +++++++++++------- 2 files changed, 153 insertions(+), 91 deletions(-) diff --git a/openpype/hosts/unreal/api/pipeline.py b/openpype/hosts/unreal/api/pipeline.py index 39638ac40f..2893550325 100644 --- a/openpype/hosts/unreal/api/pipeline.py +++ b/openpype/hosts/unreal/api/pipeline.py @@ -649,30 +649,43 @@ def generate_sequence(h, h_dir): return sequence, (min_frame, max_frame) -def replace_static_mesh_actors(old_assets, new_assets): +def _get_comps_and_assets( + component_class, asset_class, old_assets, new_assets +): eas = unreal.get_editor_subsystem(unreal.EditorActorSubsystem) - smes = unreal.get_editor_subsystem(unreal.StaticMeshEditorSubsystem) - comps = eas.get_all_level_actors_components() - static_mesh_comps = [ - c for c in comps if isinstance(c, unreal.StaticMeshComponent) + components = [ + c for c in comps if isinstance(c, component_class) ] # Get all the static meshes among the old assets in a dictionary with # the name as key - old_meshes = {} + selected_old_assets = {} for a in old_assets: asset = unreal.EditorAssetLibrary.load_asset(a) - if isinstance(asset, unreal.StaticMesh): - old_meshes[asset.get_name()] = asset + if isinstance(asset, asset_class): + selected_old_assets[asset.get_name()] = asset # Get all the static meshes among the new assets in a dictionary with # the name as key - new_meshes = {} + selected_new_assets = {} for a in new_assets: asset = unreal.EditorAssetLibrary.load_asset(a) - if isinstance(asset, unreal.StaticMesh): - new_meshes[asset.get_name()] = asset + if isinstance(asset, asset_class): + selected_new_assets[asset.get_name()] = asset + + return components, selected_old_assets, selected_new_assets + + +def replace_static_mesh_actors(old_assets, new_assets): + smes = unreal.get_editor_subsystem(unreal.StaticMeshEditorSubsystem) + + static_mesh_comps, old_meshes, new_meshes = _get_comps_and_assets( + unreal.StaticMeshComponent, + unreal.StaticMesh, + old_assets, + new_assets + ) for old_name, old_mesh in old_meshes.items(): new_mesh = new_meshes.get(old_name) @@ -685,28 +698,12 @@ def replace_static_mesh_actors(old_assets, new_assets): def replace_skeletal_mesh_actors(old_assets, new_assets): - eas = unreal.get_editor_subsystem(unreal.EditorActorSubsystem) - - comps = eas.get_all_level_actors_components() - skeletal_mesh_comps = [ - c for c in comps if isinstance(c, unreal.SkeletalMeshComponent) - ] - - # Get all the static meshes among the old assets in a dictionary with - # the name as key - old_meshes = {} - for a in old_assets: - asset = unreal.EditorAssetLibrary.load_asset(a) - if isinstance(asset, unreal.SkeletalMesh): - old_meshes[asset.get_name()] = asset - - # Get all the static meshes among the new assets in a dictionary with - # the name as key - new_meshes = {} - for a in new_assets: - asset = unreal.EditorAssetLibrary.load_asset(a) - if isinstance(asset, unreal.SkeletalMesh): - new_meshes[asset.get_name()] = asset + skeletal_mesh_comps, old_meshes, new_meshes = _get_comps_and_assets( + unreal.SkeletalMeshComponent, + unreal.SkeletalMesh, + old_assets, + new_assets + ) for old_name, old_mesh in old_meshes.items(): new_mesh = new_meshes.get(old_name) @@ -719,6 +716,25 @@ def replace_skeletal_mesh_actors(old_assets, new_assets): comp.set_skeletal_mesh_asset(new_mesh) +def replace_geometry_cache_actors(old_assets, new_assets): + geometry_cache_comps, old_caches, new_caches = _get_comps_and_assets( + unreal.SkeletalMeshComponent, + unreal.SkeletalMesh, + old_assets, + new_assets + ) + + for old_name, old_mesh in old_caches.items(): + new_mesh = new_caches.get(old_name) + + if not new_mesh: + continue + + for comp in geometry_cache_comps: + if comp.get_editor_property("geometry_cache") == old_mesh: + comp.set_geometry_cache(new_mesh) + + def delete_previous_asset_if_unused(container, asset_content): ar = unreal.AssetRegistryHelpers.get_asset_registry() diff --git a/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py b/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py index 13ba236a7d..879574f75b 100644 --- a/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py +++ b/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py @@ -7,7 +7,12 @@ from openpype.pipeline import ( AYON_CONTAINER_ID ) from openpype.hosts.unreal.api import plugin -from openpype.hosts.unreal.api import pipeline as unreal_pipeline +from openpype.hosts.unreal.api.pipeline import ( + create_container, + imprint, + replace_geometry_cache_actors, + delete_previous_asset_if_unused, +) import unreal # noqa @@ -21,8 +26,11 @@ class PointCacheAlembicLoader(plugin.Loader): icon = "cube" color = "orange" + root = "/Game/Ayon/Assets" + + @staticmethod def get_task( - self, filename, asset_dir, asset_name, replace, + filename, asset_dir, asset_name, replace, frame_start=None, frame_end=None ): task = unreal.AssetImportTask() @@ -38,8 +46,6 @@ class PointCacheAlembicLoader(plugin.Loader): task.set_editor_property('automated', True) task.set_editor_property('save', True) - # set import options here - # Unreal 4.24 ignores the settings. It works with Unreal 4.26 options.set_editor_property( 'import_type', unreal.AlembicImportType.GEOMETRY_CACHE) @@ -64,13 +70,42 @@ class PointCacheAlembicLoader(plugin.Loader): return task - def load(self, context, name, namespace, data): - """Load and containerise representation into Content Browser. + def import_and_containerize( + self, filepath, asset_dir, asset_name, container_name, + frame_start, frame_end + ): + unreal.EditorAssetLibrary.make_directory(asset_dir) - This is two step process. First, import FBX to temporary path and - then call `containerise()` on it - this moves all content to new - directory and then it will create AssetContainer there and imprint it - with metadata. This will mark this path as container. + task = self.get_task( + filepath, asset_dir, asset_name, False, frame_start, frame_end) + + unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) + + # Create Asset Container + create_container(container=container_name, path=asset_dir) + + def imprint( + self, asset, asset_dir, container_name, asset_name, representation, + frame_start, frame_end + ): + data = { + "schema": "ayon:container-2.0", + "id": AYON_CONTAINER_ID, + "asset": asset, + "namespace": asset_dir, + "container_name": container_name, + "asset_name": asset_name, + "loader": str(self.__class__.__name__), + "representation": representation["_id"], + "parent": representation["parent"], + "family": representation["context"]["family"], + "frame_start": frame_start, + "frame_end": frame_end + } + imprint(f"{asset_dir}/{container_name}", data) + + def load(self, context, name, namespace, options): + """Load and containerise representation into Content Browser. Args: context (dict): application context @@ -79,30 +114,28 @@ class PointCacheAlembicLoader(plugin.Loader): This is not passed here, so namespace is set by `containerise()` because only then we know real path. - data (dict): Those would be data to be imprinted. This is not used - now, data are imprinted by `containerise()`. + data (dict): Those would be data to be imprinted. Returns: list(str): list of container content - """ # Create directory for asset and Ayon container - 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') + # 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: - asset_name = "{}".format(name) + name_version = f"{name}_v{version.get('name'):03d}" tools = unreal.AssetToolsHelpers().get_asset_tools() asset_dir, container_name = tools.create_unique_asset_name( - "{}/{}/{}".format(root, asset, name), suffix="") + f"{self.root}/{asset}/{name_version}", suffix="") container_name += suffix - unreal.EditorAssetLibrary.make_directory(asset_dir) - frame_start = context.get('asset').get('data').get('frameStart') frame_end = context.get('asset').get('data').get('frameEnd') @@ -111,30 +144,17 @@ class PointCacheAlembicLoader(plugin.Loader): if frame_start == frame_end: frame_end += 1 - path = self.filepath_from_context(context) - task = self.get_task( - path, asset_dir, asset_name, False, frame_start, frame_end) + if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): + path = self.filepath_from_context(context) - unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) # noqa: E501 - # Create Asset Container - unreal_pipeline.create_container( - container=container_name, path=asset_dir) + self.import_and_containerize( + path, asset_dir, asset_name, container_name, + frame_start, frame_end) - data = { - "schema": "ayon:container-2.0", - "id": AYON_CONTAINER_ID, - "asset": asset, - "namespace": asset_dir, - "container_name": container_name, - "asset_name": asset_name, - "loader": str(self.__class__.__name__), - "representation": context["representation"]["_id"], - "parent": context["representation"]["parent"], - "family": context["representation"]["context"]["family"] - } - unreal_pipeline.imprint( - "{}/{}".format(asset_dir, container_name), data) + self.imprint( + asset, asset_dir, container_name, asset_name, + context["representation"], frame_start, frame_end) asset_content = unreal.EditorAssetLibrary.list_assets( asset_dir, recursive=True, include_folder=True @@ -146,32 +166,58 @@ class PointCacheAlembicLoader(plugin.Loader): return asset_content def update(self, container, representation): - name = container["asset_name"] - source_path = get_representation_path(representation) - destination_path = container["namespace"] - representation["context"] + context = representation.get("context", {}) - task = self.get_task(source_path, destination_path, name, False) - # do import fbx and replace existing data - unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) + unreal.log_warning(context) - container_path = "{}/{}".format(container["namespace"], - container["objectName"]) - # update metadata - unreal_pipeline.imprint( - container_path, - { - "representation": str(representation["_id"]), - "parent": str(representation["parent"]) - }) + if not context: + raise RuntimeError("No context found in representation") + + # Create directory for asset and Ayon container + asset = context.get('asset') + name = context.get('subset') + 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 + name_version = f"{name}_v{version:03d}" if version else f"{name}_hero" + tools = unreal.AssetToolsHelpers().get_asset_tools() + asset_dir, container_name = tools.create_unique_asset_name( + f"{self.root}/{asset}/{name_version}", suffix="") + + container_name += suffix + + frame_start = int(container.get("frame_start")) + frame_end = int(container.get("frame_end")) + + if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): + path = get_representation_path(representation) + + self.import_and_containerize( + path, asset_dir, asset_name, container_name, + frame_start, frame_end) + + self.imprint( + asset, asset_dir, container_name, asset_name, representation, + frame_start, frame_end) asset_content = unreal.EditorAssetLibrary.list_assets( - destination_path, recursive=True, include_folder=True + asset_dir, recursive=True, include_folder=False ) for a in asset_content: unreal.EditorAssetLibrary.save_asset(a) + old_assets = unreal.EditorAssetLibrary.list_assets( + container["namespace"], recursive=True, include_folder=False + ) + + replace_geometry_cache_actors(old_assets, asset_content) + + unreal.EditorLevelLibrary.save_current_level() + + delete_previous_asset_if_unused(container, old_assets) + def remove(self, container): path = container["namespace"] parent_path = os.path.dirname(path) From dad73b4adb98da4ac91e8f690e65b96051f38b50 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 5 Oct 2023 11:51:37 +0100 Subject: [PATCH 0141/1233] Hound fixes --- openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py b/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py index 879574f75b..8ac2656bd7 100644 --- a/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py +++ b/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py @@ -147,7 +147,6 @@ class PointCacheAlembicLoader(plugin.Loader): if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): path = self.filepath_from_context(context) - self.import_and_containerize( path, asset_dir, asset_name, container_name, frame_start, frame_end) From fa11a2bfdcddb6b085641f1c3c078ee34c5405aa Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 5 Oct 2023 19:17:25 +0800 Subject: [PATCH 0142/1233] small tweaks on loader and extractor & use raise PublishValidationError for each instead of using reports append list of error. --- openpype/hosts/max/plugins/load/load_tycache.py | 3 +-- .../hosts/max/plugins/publish/extract_tycache.py | 13 ++++++------- .../max/plugins/publish/validate_tyflow_data.py | 16 ++++++++-------- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_tycache.py b/openpype/hosts/max/plugins/load/load_tycache.py index 657e743087..7eac0de3e5 100644 --- a/openpype/hosts/max/plugins/load/load_tycache.py +++ b/openpype/hosts/max/plugins/load/load_tycache.py @@ -49,8 +49,7 @@ class PointCloudLoader(load.LoaderPlugin): update_custom_attribute_data( node, node_list) with maintained_selection(): - rt.Select(node_list) - for prt in rt.Selection: + for prt in node_list: prt.filename = path lib.imprint(container["instance_node"], { "representation": str(representation["_id"]) diff --git a/openpype/hosts/max/plugins/publish/extract_tycache.py b/openpype/hosts/max/plugins/publish/extract_tycache.py index 56fd39406e..0327564b3a 100644 --- a/openpype/hosts/max/plugins/publish/extract_tycache.py +++ b/openpype/hosts/max/plugins/publish/extract_tycache.py @@ -42,18 +42,17 @@ class ExtractTyCache(publish.Extractor): with maintained_selection(): job_args = None + has_tyc_spline = ( + True + if instance.data["tycache_type"] == "tycachespline" + else False + ) if instance.data["tycache_type"] == "tycache": - job_args = self.export_particle( - instance.data["members"], - start, end, path, - additional_attributes) - elif instance.data["tycache_type"] == "tycachespline": job_args = self.export_particle( instance.data["members"], start, end, path, additional_attributes, - tycache_spline_enabled=True) - + tycache_spline_enabled=has_tyc_spline) for job in job_args: rt.Execute(job) diff --git a/openpype/hosts/max/plugins/publish/validate_tyflow_data.py b/openpype/hosts/max/plugins/publish/validate_tyflow_data.py index c0a6d23022..59dafef901 100644 --- a/openpype/hosts/max/plugins/publish/validate_tyflow_data.py +++ b/openpype/hosts/max/plugins/publish/validate_tyflow_data.py @@ -23,15 +23,14 @@ class ValidateTyFlowData(pyblish.api.InstancePlugin): invalid_object = self.get_tyflow_object(instance) if invalid_object: - report.append(f"Non tyFlow object found: {invalid_object}") + raise PublishValidationError( + f"Non tyFlow object found: {invalid_object}") invalid_operator = self.get_tyflow_operator(instance) if invalid_operator: - report.append(( + raise PublishValidationError(( "tyFlow ExportParticle operator not " f"found: {invalid_operator}")) - if report: - raise PublishValidationError(f"{report}") def get_tyflow_object(self, instance): """Get the nodes which are not tyFlow object(s) @@ -46,7 +45,7 @@ class ValidateTyFlowData(pyblish.api.InstancePlugin): """ invalid = [] container = instance.data["instance_node"] - self.log.info(f"Validating tyFlow container for {container}") + self.log.debug(f"Validating tyFlow container for {container}") selection_list = instance.data["members"] for sel in selection_list: @@ -61,7 +60,8 @@ class ValidateTyFlowData(pyblish.api.InstancePlugin): return invalid def get_tyflow_operator(self, instance): - """_summary_ + """Check if the Export Particle Operators in the node + connections. Args: instance (str): instance node @@ -73,7 +73,7 @@ class ValidateTyFlowData(pyblish.api.InstancePlugin): """ invalid = [] container = instance.data["instance_node"] - self.log.info(f"Validating tyFlow object for {container}") + self.log.debug(f"Validating tyFlow object for {container}") selection_list = instance.data["members"] bool_list = [] for sel in selection_list: @@ -88,7 +88,7 @@ class ValidateTyFlowData(pyblish.api.InstancePlugin): # if the export_particles property is not there # it means there is not a "Export Particle" operator if "True" not in bool_list: - self.log.error("Operator 'Export Particles' not found!") + self.log.error("Operator 'Export Particles' not found.") invalid.append(sel) return invalid From 291fd65b0969f07ce2767971ba5a095233c682fa Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 5 Oct 2023 19:20:14 +0800 Subject: [PATCH 0143/1233] hound --- openpype/hosts/max/plugins/publish/validate_tyflow_data.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_tyflow_data.py b/openpype/hosts/max/plugins/publish/validate_tyflow_data.py index 59dafef901..dc2de55e4f 100644 --- a/openpype/hosts/max/plugins/publish/validate_tyflow_data.py +++ b/openpype/hosts/max/plugins/publish/validate_tyflow_data.py @@ -19,12 +19,10 @@ class ValidateTyFlowData(pyblish.api.InstancePlugin): 2. Validate if tyFlow operator Export Particle exists """ - report = [] - invalid_object = self.get_tyflow_object(instance) if invalid_object: - raise PublishValidationError( - f"Non tyFlow object found: {invalid_object}") + raise PublishValidationError( + f"Non tyFlow object found: {invalid_object}") invalid_operator = self.get_tyflow_operator(instance) if invalid_operator: From 6e56ba2cc17a384dff0f27ed96e00dd2d7e54e22 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 5 Oct 2023 14:49:34 +0300 Subject: [PATCH 0144/1233] Bigroy's comments and update attribute def label and tip --- openpype/hosts/houdini/api/lib.py | 8 +++---- .../publish/collect_rop_frame_range.py | 24 ++++++++++--------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 3c39b32b0d..711fae7cc4 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -564,20 +564,20 @@ def get_frame_data(node, asset_data=None, log=None): if log is None: log = self.log + data = {} + if node.parm("trange") is None: log.debug( "Node has no 'trange' parameter: {}".format(node.path()) ) - return + return data if node.evalParm("trange") == 0: log.debug( "Node '{}' has 'Render current frame' set. " "Time range data ignored.".format(node.path()) ) - return - - data = {} + return data data["frameStartHandle"] = node.evalParm("f1") data["handleStart"] = asset_data.get("handleStart", 0) diff --git a/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py b/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py index 75c101ed0f..1f65d4eea6 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py +++ b/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py @@ -30,7 +30,7 @@ class CollectRopFrameRange(pyblish.api.InstancePlugin, asset_data = instance.context.data["assetEntity"]["data"] attr_values = self.get_attr_values_from_data(instance.data) - if attr_values.get("reset_handles"): + if not attr_values.get("use_handles"): asset_data["handleStart"] = 0 asset_data["handleEnd"] = 0 @@ -42,17 +42,17 @@ class CollectRopFrameRange(pyblish.api.InstancePlugin, # Log artist friendly message about the collected frame range message = "" - if attr_values.get("reset_handles"): - message += ( - "Reset frame handles is activated for this instance, " - "start and end handles are set to 0.\n" - ) - else: + if attr_values.get("use_handles"): message += ( "Full Frame range with Handles " "{0[frameStartHandle]} - {0[frameEndHandle]}\n" .format(frame_data) ) + else: + message += ( + "Use handles is deactivated for this instance, " + "start and end handles are set to 0.\n" + ) message += ( "Frame range {0[frameStart]} - {0[frameEnd]}" @@ -76,8 +76,10 @@ class CollectRopFrameRange(pyblish.api.InstancePlugin, @classmethod def get_attribute_defs(cls): return [ - BoolDef("reset_handles", - tooltip="Set frame handles to zero", - default=False, - label="Reset frame handles") + BoolDef("use_handles", + tooltip="Disable this if you don't want the publisher" + " to ignore start and end handles specified in the asset data" + " for this publish instance", + default=True, + label="Use asset handles") ] From ec2ee09fe974f8500765ec65571996a93945951a Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 5 Oct 2023 14:51:33 +0300 Subject: [PATCH 0145/1233] resolve hound --- .../hosts/houdini/plugins/publish/collect_rop_frame_range.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py b/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py index 1f65d4eea6..37db922201 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py +++ b/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py @@ -78,8 +78,8 @@ class CollectRopFrameRange(pyblish.api.InstancePlugin, return [ BoolDef("use_handles", tooltip="Disable this if you don't want the publisher" - " to ignore start and end handles specified in the asset data" - " for this publish instance", + " to ignore start and end handles specified in the" + " asset data for this publish instance", default=True, label="Use asset handles") ] From ad252347c0bf59db86ee46a19525d254837c092b Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 5 Oct 2023 20:27:22 +0800 Subject: [PATCH 0146/1233] improve the validation report --- .../max/plugins/publish/validate_tyflow_data.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_tyflow_data.py b/openpype/hosts/max/plugins/publish/validate_tyflow_data.py index dc2de55e4f..bcdc9c6dfe 100644 --- a/openpype/hosts/max/plugins/publish/validate_tyflow_data.py +++ b/openpype/hosts/max/plugins/publish/validate_tyflow_data.py @@ -19,16 +19,20 @@ class ValidateTyFlowData(pyblish.api.InstancePlugin): 2. Validate if tyFlow operator Export Particle exists """ + report = [] + invalid_object = self.get_tyflow_object(instance) - if invalid_object: - raise PublishValidationError( - f"Non tyFlow object found: {invalid_object}") + self.log.error(f"Non tyFlow object found: {invalid_object}") invalid_operator = self.get_tyflow_operator(instance) if invalid_operator: - raise PublishValidationError(( - "tyFlow ExportParticle operator not " - f"found: {invalid_operator}")) + self.log.error( + "Operator 'Export Particles' not found in tyFlow editor.") + if report: + raise PublishValidationError( + "issues occurred", + description="Container should only include tyFlow object\n " + "and tyflow operator Export Particle should be in the tyFlow editor") def get_tyflow_object(self, instance): """Get the nodes which are not tyFlow object(s) @@ -86,7 +90,6 @@ class ValidateTyFlowData(pyblish.api.InstancePlugin): # if the export_particles property is not there # it means there is not a "Export Particle" operator if "True" not in bool_list: - self.log.error("Operator 'Export Particles' not found.") invalid.append(sel) return invalid From e364f4e74c86e3e5e6779617ca6fb5a835b18e3b Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 5 Oct 2023 20:41:44 +0800 Subject: [PATCH 0147/1233] hound --- openpype/hosts/max/plugins/publish/validate_tyflow_data.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_tyflow_data.py b/openpype/hosts/max/plugins/publish/validate_tyflow_data.py index bcdc9c6dfe..a359100e6e 100644 --- a/openpype/hosts/max/plugins/publish/validate_tyflow_data.py +++ b/openpype/hosts/max/plugins/publish/validate_tyflow_data.py @@ -19,20 +19,21 @@ class ValidateTyFlowData(pyblish.api.InstancePlugin): 2. Validate if tyFlow operator Export Particle exists """ - report = [] invalid_object = self.get_tyflow_object(instance) + if invalid_object: self.log.error(f"Non tyFlow object found: {invalid_object}") invalid_operator = self.get_tyflow_operator(instance) if invalid_operator: self.log.error( "Operator 'Export Particles' not found in tyFlow editor.") - if report: + if invalid_object or invalid_operator: raise PublishValidationError( "issues occurred", description="Container should only include tyFlow object\n " - "and tyflow operator Export Particle should be in the tyFlow editor") + "and tyflow operator 'Export Particle' should be in \n" + "the tyFlow editor") def get_tyflow_object(self, instance): """Get the nodes which are not tyFlow object(s) From 597ea868765b8a31e7743e5a94224a3d7738319a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 5 Oct 2023 14:44:56 +0200 Subject: [PATCH 0148/1233] fix source path, not destination path (#5702) --- start.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/start.py b/start.py index 3b020c76c0..54a252ccf3 100644 --- a/start.py +++ b/start.py @@ -366,8 +366,8 @@ def run_disk_mapping_commands(settings): destination = destination.replace("/", "\\").rstrip("\\") source = source.replace("/", "\\").rstrip("\\") # Add slash after ':' ('G:' -> 'G:\') - if destination.endswith(":"): - destination += "\\" + if source.endswith(":"): + source += "\\" else: destination = destination.rstrip("/") source = source.rstrip("/") From f3e02c1e95ac0e085630c370431301de0b74ccd4 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 5 Oct 2023 15:05:18 +0100 Subject: [PATCH 0149/1233] Add MayaPy application --- ...oundry_apps.py => pre_new_console_apps.py} | 8 ++- openpype/hosts/maya/api/pipeline.py | 3 +- openpype/hosts/maya/hooks/pre_copy_mel.py | 2 +- .../system_settings/applications.json | 59 +++++++++++++++++++ .../host_settings/schema_mayapy.json | 39 ++++++++++++ .../system_schema/schema_applications.json | 4 ++ 6 files changed, 110 insertions(+), 5 deletions(-) rename openpype/hooks/{pre_foundry_apps.py => pre_new_console_apps.py} (82%) create mode 100644 openpype/settings/entities/schemas/system_schema/host_settings/schema_mayapy.json diff --git a/openpype/hooks/pre_foundry_apps.py b/openpype/hooks/pre_new_console_apps.py similarity index 82% rename from openpype/hooks/pre_foundry_apps.py rename to openpype/hooks/pre_new_console_apps.py index 7536df4c16..9727b4fb78 100644 --- a/openpype/hooks/pre_foundry_apps.py +++ b/openpype/hooks/pre_new_console_apps.py @@ -2,7 +2,7 @@ import subprocess from openpype.lib.applications import PreLaunchHook, LaunchTypes -class LaunchFoundryAppsWindows(PreLaunchHook): +class LaunchNewConsoleApps(PreLaunchHook): """Foundry applications have specific way how to launch them. Nuke is executed "like" python process so it is required to pass @@ -13,13 +13,15 @@ class LaunchFoundryAppsWindows(PreLaunchHook): # Should be as last hook because must change launch arguments to string order = 1000 - app_groups = {"nuke", "nukeassist", "nukex", "hiero", "nukestudio"} + app_groups = { + "nuke", "nukeassist", "nukex", "hiero", "nukestudio", "mayapy" + } platforms = {"windows"} launch_types = {LaunchTypes.local} def execute(self): # Change `creationflags` to CREATE_NEW_CONSOLE - # - on Windows nuke will create new window using its console + # - on Windows some apps will create new window using its console # Set `stdout` and `stderr` to None so new created console does not # have redirected output to DEVNULL in build self.launch_context.kwargs.update({ diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py index 38d7ae08c1..6b791c9665 100644 --- a/openpype/hosts/maya/api/pipeline.py +++ b/openpype/hosts/maya/api/pipeline.py @@ -95,6 +95,8 @@ class MayaHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): self.log.info("Installing callbacks ... ") register_event_callback("init", on_init) + _set_project() + if lib.IS_HEADLESS: self.log.info(( "Running in headless mode, skipping Maya save/open/new" @@ -103,7 +105,6 @@ class MayaHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): return - _set_project() self._register_callbacks() menu.install(project_settings) diff --git a/openpype/hosts/maya/hooks/pre_copy_mel.py b/openpype/hosts/maya/hooks/pre_copy_mel.py index 0fb5af149a..6cd2c69e20 100644 --- a/openpype/hosts/maya/hooks/pre_copy_mel.py +++ b/openpype/hosts/maya/hooks/pre_copy_mel.py @@ -7,7 +7,7 @@ class PreCopyMel(PreLaunchHook): Hook `GlobalHostDataHook` must be executed before this hook. """ - app_groups = {"maya"} + app_groups = {"maya", "mayapy"} launch_types = {LaunchTypes.local} def execute(self): diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index f2fc7d933a..b100704ffe 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -114,6 +114,65 @@ } } }, + "mayapy": { + "enabled": true, + "label": "MayaPy", + "icon": "{}/app_icons/maya.png", + "host_name": "maya", + "environment": { + "MAYA_DISABLE_CLIC_IPM": "Yes", + "MAYA_DISABLE_CIP": "Yes", + "MAYA_DISABLE_CER": "Yes", + "PYMEL_SKIP_MEL_INIT": "Yes", + "LC_ALL": "C" + }, + "variants": { + "2024": { + "use_python_2": false, + "executables": { + "windows": [ + "C:\\Program Files\\Autodesk\\Maya2024\\bin\\mayapy.exe" + ], + "darwin": [], + "linux": [ + "/usr/autodesk/maya2024/bin/mayapy" + ] + }, + "arguments": { + "windows": [ + "-I" + ], + "darwin": [], + "linux": [ + "-I" + ] + }, + "environment": {} + }, + "2023": { + "use_python_2": false, + "executables": { + "windows": [ + "C:\\Program Files\\Autodesk\\Maya2023\\bin\\mayapy.exe" + ], + "darwin": [], + "linux": [ + "/usr/autodesk/maya2024/bin/mayapy" + ] + }, + "arguments": { + "windows": [ + "-I" + ], + "darwin": [], + "linux": [ + "-I" + ] + }, + "environment": {} + } + } + }, "3dsmax": { "enabled": true, "label": "3ds max", diff --git a/openpype/settings/entities/schemas/system_schema/host_settings/schema_mayapy.json b/openpype/settings/entities/schemas/system_schema/host_settings/schema_mayapy.json new file mode 100644 index 0000000000..bbdc7e13b0 --- /dev/null +++ b/openpype/settings/entities/schemas/system_schema/host_settings/schema_mayapy.json @@ -0,0 +1,39 @@ +{ + "type": "dict", + "key": "mayapy", + "label": "Autodesk MayaPy", + "collapsible": true, + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "schema_template", + "name": "template_host_unchangables" + }, + { + "key": "environment", + "label": "Environment", + "type": "raw-json" + }, + { + "type": "dict-modifiable", + "key": "variants", + "collapsible_key": true, + "use_label_wrap": false, + "object_type": { + "type": "dict", + "collapsible": true, + "children": [ + { + "type": "schema_template", + "name": "template_host_variant_items" + } + ] + } + } + ] +} diff --git a/openpype/settings/entities/schemas/system_schema/schema_applications.json b/openpype/settings/entities/schemas/system_schema/schema_applications.json index abea37a9ab..7965c344ae 100644 --- a/openpype/settings/entities/schemas/system_schema/schema_applications.json +++ b/openpype/settings/entities/schemas/system_schema/schema_applications.json @@ -9,6 +9,10 @@ "type": "schema", "name": "schema_maya" }, + { + "type": "schema", + "name": "schema_mayapy" + }, { "type": "schema", "name": "schema_3dsmax" From 9d6340a8a18a0e41651580a38fedbb9dec732f5c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 5 Oct 2023 17:03:54 +0200 Subject: [PATCH 0150/1233] colorspace: add`convert_colorspace_enumerator_item` - improving unittests - adding unittest for `convert_colorspace_enumerator_item` - separating `config_items` from `get_colorspaces_enumerator_items` so they can be stored in context --- .../plugins/create/create_colorspace_look.py | 7 +- .../publish/collect_explicit_colorspace.py | 7 +- openpype/pipeline/colorspace.py | 94 ++++++++++--- ...pace_convert_colorspace_enumerator_item.py | 118 ++++++++++++++++ ...rspace_get_colorspaces_enumerator_items.py | 114 ++++++++++++++++ ...test_colorspace_get_labeled_colorspaces.py | 126 ------------------ 6 files changed, 321 insertions(+), 145 deletions(-) create mode 100644 tests/unit/openpype/pipeline/test_colorspace_convert_colorspace_enumerator_item.py create mode 100644 tests/unit/openpype/pipeline/test_colorspace_get_colorspaces_enumerator_items.py delete mode 100644 tests/unit/openpype/pipeline/test_colorspace_get_labeled_colorspaces.py diff --git a/openpype/hosts/traypublisher/plugins/create/create_colorspace_look.py b/openpype/hosts/traypublisher/plugins/create/create_colorspace_look.py index 3f3fa5348a..3e1c20d96a 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_colorspace_look.py +++ b/openpype/hosts/traypublisher/plugins/create/create_colorspace_look.py @@ -36,6 +36,7 @@ class CreateColorspaceLook(TrayPublishCreator): (None, "Not set") ] colorspace_attr_show = False + config_items = None def get_detail_description(self): return """# Colorspace Look @@ -148,11 +149,13 @@ This creator publishes color space look file (LUT). if config_data: filepath = config_data["path"] - labeled_colorspaces = colorspace.get_labeled_colorspaces( - filepath, + config_items = colorspace.get_ocio_config_colorspaces(filepath) + labeled_colorspaces = colorspace.get_colorspaces_enumerator_items( + config_items, include_aliases=True, include_roles=True ) + self.config_items = config_items self.colorspace_items.extend(labeled_colorspaces) self.enabled = True diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_explicit_colorspace.py b/openpype/hosts/traypublisher/plugins/publish/collect_explicit_colorspace.py index 06ceac5923..5db2b0cbad 100644 --- a/openpype/hosts/traypublisher/plugins/publish/collect_explicit_colorspace.py +++ b/openpype/hosts/traypublisher/plugins/publish/collect_explicit_colorspace.py @@ -22,6 +22,7 @@ class CollectColorspace(pyblish.api.InstancePlugin, (None, "Don't override") ] colorspace_attr_show = False + config_items = None def process(self, instance): values = self.get_attr_values_from_data(instance.data) @@ -51,11 +52,13 @@ class CollectColorspace(pyblish.api.InstancePlugin, if config_data: filepath = config_data["path"] - labeled_colorspaces = colorspace.get_labeled_colorspaces( - filepath, + config_items = colorspace.get_ocio_config_colorspaces(filepath) + labeled_colorspaces = colorspace.get_colorspaces_enumerator_items( + config_items, include_aliases=True, include_roles=True ) + cls.config_items = config_items cls.colorspace_items.extend(labeled_colorspaces) cls.enabled = True diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 8985b07cde..8bebc934fc 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -1,4 +1,3 @@ -from copy import deepcopy import re import os import json @@ -7,6 +6,7 @@ import functools import platform import tempfile import warnings +from copy import deepcopy from openpype import PACKAGE_DIR from openpype.settings import get_project_settings @@ -533,13 +533,63 @@ def get_ocio_config_colorspaces(config_path): return CachedData.ocio_config_colorspaces[config_path] -def get_labeled_colorspaces( - config_path, +def convert_colorspace_enumerator_item( + colorspace_enum_item, + config_items +): + """Convert colorspace enumerator item to dictionary + + Args: + colorspace_item (str): colorspace and family in couple + config_items (dict[str,dict]): colorspace data + + Returns: + dict: colorspace data + """ + # split string with `::` separator and set first as key and second as value + item_type, item_name = colorspace_enum_item.split("::") + + item_data = None + if item_type == "aliases": + # loop through all colorspaces and find matching alias + for name, _data in config_items.get("colorspaces", {}).items(): + if item_name in _data.get("aliases", []): + item_data = deepcopy(_data) + item_data.update({ + "name": name, + "type": "colorspace" + }) + break + else: + # find matching colorspace item found in labeled_colorspaces + item_data = config_items.get(item_type, {}).get(item_name) + if item_data: + item_data = deepcopy(item_data) + item_data.update({ + "name": item_name, + "type": item_type + }) + + # raise exception if item is not found + if not item_data: + message_config_keys = ", ".join( + "'{}':{}".format(key, set(config_items.get(key, {}).keys())) for key in config_items.keys() + ) + raise KeyError( + "Missing colorspace item '{}' in config data: [{}]".format( + colorspace_enum_item, message_config_keys + ) + ) + + return item_data + + +def get_colorspaces_enumerator_items( + config_items, include_aliases=False, include_looks=False, include_roles=False, include_display_views=False - ): """Get all colorspace data with labels @@ -547,7 +597,7 @@ def get_labeled_colorspaces( Families can be used for building menu and submenus in gui. Args: - config_path (str): path leading to config.ocio file + config_items (dict[str,dict]): colorspace data include_aliases (bool): include aliases in result include_looks (bool): include looks in result include_roles (bool): include roles in result @@ -555,7 +605,6 @@ def get_labeled_colorspaces( Returns: list[tuple[str,str]]: colorspace and family in couple """ - config_items = get_ocio_config_colorspaces(config_path) labeled_colorspaces = [] aliases = set() colorspaces = set() @@ -568,46 +617,61 @@ def get_labeled_colorspaces( if color_data.get("aliases"): aliases.update([ ( - alias_name, + "aliases::{}".format(alias_name), "[alias] {} ({})".format(alias_name, color_name) ) for alias_name in color_data["aliases"] ]) - colorspaces.add(color_name) + colorspaces.add(( + "{}::{}".format(items_type, color_name), + "[colorspace] {}".format(color_name) + )) elif items_type == "looks": looks.update([ - (name, "[look] {} ({})".format(name, role_data["process_space"])) + ( + "{}::{}".format(items_type, name), + "[look] {} ({})".format(name, role_data["process_space"]) + ) for name, role_data in colorspace_items.items() ]) elif items_type == "displays_views": display_views.update([ - (name, "[view (display)] {}".format(name)) + ( + "{}::{}".format(items_type, name), + "[view (display)] {}".format(name) + ) for name, _ in colorspace_items.items() ]) elif items_type == "roles": roles.update([ - (name, "[role] {} ({})".format(name, role_data["colorspace"])) + ( + "{}::{}".format(items_type, name), + "[role] {} ({})".format(name, role_data["colorspace"]) + ) for name, role_data in colorspace_items.items() ]) if roles and include_roles: + roles = sorted(roles, key=lambda x: x[0]) labeled_colorspaces.extend(roles) - # add colorspace after roles so it is first in menu - labeled_colorspaces.extend(( - (name, f"[colorspace] {name}") for name in colorspaces - )) + # add colorspaces as second so it is not first in menu + colorspaces = sorted(colorspaces, key=lambda x: x[0]) + labeled_colorspaces.extend(colorspaces) if aliases and include_aliases: + aliases = sorted(aliases, key=lambda x: x[0]) labeled_colorspaces.extend(aliases) if looks and include_looks: + looks = sorted(looks, key=lambda x: x[0]) labeled_colorspaces.extend(looks) if display_views and include_display_views: + display_views = sorted(display_views, key=lambda x: x[0]) labeled_colorspaces.extend(display_views) return labeled_colorspaces diff --git a/tests/unit/openpype/pipeline/test_colorspace_convert_colorspace_enumerator_item.py b/tests/unit/openpype/pipeline/test_colorspace_convert_colorspace_enumerator_item.py new file mode 100644 index 0000000000..bffe8eda90 --- /dev/null +++ b/tests/unit/openpype/pipeline/test_colorspace_convert_colorspace_enumerator_item.py @@ -0,0 +1,118 @@ +from ast import alias +import unittest +from openpype.pipeline.colorspace import convert_colorspace_enumerator_item + + +class TestConvertColorspaceEnumeratorItem(unittest.TestCase): + def setUp(self): + self.config_items = { + "colorspaces": { + "sRGB": { + "aliases": ["sRGB_1"], + "family": "colorspace", + "categories": ["colors"], + "equalitygroup": "equalitygroup", + }, + "Rec.709": { + "aliases": ["rec709_1", "rec709_2"], + }, + }, + "looks": { + "sRGB_to_Rec.709": { + "process_space": "sRGB", + }, + }, + "displays_views": { + "sRGB (ACES)": { + "view": "sRGB", + "display": "ACES", + }, + "Rec.709 (ACES)": { + "view": "Rec.709", + "display": "ACES", + }, + }, + "roles": { + "compositing_linear": { + "colorspace": "linear", + }, + }, + } + + def test_valid_item(self): + colorspace_item_data = convert_colorspace_enumerator_item( + "colorspaces::sRGB", self.config_items) + self.assertEqual( + colorspace_item_data, + { + "name": "sRGB", + "type": "colorspaces", + "aliases": ["sRGB_1"], + "family": "colorspace", + "categories": ["colors"], + "equalitygroup": "equalitygroup" + } + ) + + alias_item_data = convert_colorspace_enumerator_item( + "aliases::rec709_1", self.config_items) + self.assertEqual( + alias_item_data, + { + "aliases": ["rec709_1", "rec709_2"], + "name": "Rec.709", + "type": "colorspace" + } + ) + + display_view_item_data = convert_colorspace_enumerator_item( + "displays_views::sRGB (ACES)", self.config_items) + self.assertEqual( + display_view_item_data, + { + "type": "displays_views", + "name": "sRGB (ACES)", + "view": "sRGB", + "display": "ACES" + } + ) + + role_item_data = convert_colorspace_enumerator_item( + "roles::compositing_linear", self.config_items) + self.assertEqual( + role_item_data, + { + "name": "compositing_linear", + "type": "roles", + "colorspace": "linear" + } + ) + + look_item_data = convert_colorspace_enumerator_item( + "looks::sRGB_to_Rec.709", self.config_items) + self.assertEqual( + look_item_data, + { + "type": "looks", + "name": "sRGB_to_Rec.709", + "process_space": "sRGB" + } + ) + + def test_invalid_item(self): + config_items = { + "RGB": { + "sRGB": {"red": 255, "green": 255, "blue": 255}, + "AdobeRGB": {"red": 255, "green": 255, "blue": 255}, + } + } + with self.assertRaises(KeyError): + convert_colorspace_enumerator_item("RGB::invalid", config_items) + + def test_missing_config_data(self): + config_items = {} + with self.assertRaises(KeyError): + convert_colorspace_enumerator_item("RGB::sRGB", config_items) + +if __name__ == '__main__': + unittest.main() diff --git a/tests/unit/openpype/pipeline/test_colorspace_get_colorspaces_enumerator_items.py b/tests/unit/openpype/pipeline/test_colorspace_get_colorspaces_enumerator_items.py new file mode 100644 index 0000000000..de3e333670 --- /dev/null +++ b/tests/unit/openpype/pipeline/test_colorspace_get_colorspaces_enumerator_items.py @@ -0,0 +1,114 @@ +import unittest + +from openpype.pipeline.colorspace import get_colorspaces_enumerator_items + + +class TestGetColorspacesEnumeratorItems(unittest.TestCase): + def setUp(self): + self.config_items = { + "colorspaces": { + "sRGB": { + "aliases": ["sRGB_1"], + }, + "Rec.709": { + "aliases": ["rec709_1", "rec709_2"], + }, + }, + "looks": { + "sRGB_to_Rec.709": { + "process_space": "sRGB", + }, + }, + "displays_views": { + "sRGB (ACES)": { + "view": "sRGB", + "display": "ACES", + }, + "Rec.709 (ACES)": { + "view": "Rec.709", + "display": "ACES", + }, + }, + "roles": { + "compositing_linear": { + "colorspace": "linear", + }, + }, + } + + def test_colorspaces(self): + result = get_colorspaces_enumerator_items(self.config_items) + expected = [ + ("colorspaces::Rec.709", "[colorspace] Rec.709"), + ("colorspaces::sRGB", "[colorspace] sRGB"), + ] + self.assertEqual(result, expected) + + def test_aliases(self): + result = get_colorspaces_enumerator_items(self.config_items, include_aliases=True) + expected = [ + ("colorspaces::Rec.709", "[colorspace] Rec.709"), + ("colorspaces::sRGB", "[colorspace] sRGB"), + ("aliases::rec709_1", "[alias] rec709_1 (Rec.709)"), + ("aliases::rec709_2", "[alias] rec709_2 (Rec.709)"), + ("aliases::sRGB_1", "[alias] sRGB_1 (sRGB)"), + ] + self.assertEqual(result, expected) + + def test_looks(self): + result = get_colorspaces_enumerator_items(self.config_items, include_looks=True) + expected = [ + ("colorspaces::Rec.709", "[colorspace] Rec.709"), + ("colorspaces::sRGB", "[colorspace] sRGB"), + ("looks::sRGB_to_Rec.709", "[look] sRGB_to_Rec.709 (sRGB)"), + ] + self.assertEqual(result, expected) + + def test_display_views(self): + result = get_colorspaces_enumerator_items(self.config_items, include_display_views=True) + expected = [ + ("colorspaces::Rec.709", "[colorspace] Rec.709"), + ("colorspaces::sRGB", "[colorspace] sRGB"), + ("displays_views::Rec.709 (ACES)", "[view (display)] Rec.709 (ACES)"), + ("displays_views::sRGB (ACES)", "[view (display)] sRGB (ACES)"), + + ] + self.assertEqual(result, expected) + + def test_roles(self): + result = get_colorspaces_enumerator_items(self.config_items, include_roles=True) + expected = [ + ("roles::compositing_linear", "[role] compositing_linear (linear)"), + ("colorspaces::Rec.709", "[colorspace] Rec.709"), + ("colorspaces::sRGB", "[colorspace] sRGB"), + ] + self.assertEqual(result, expected) + + def test_all(self): + message_config_keys = ", ".join( + "'{}':{}".format(key, set(self.config_items.get(key, {}).keys())) for key in self.config_items.keys() + ) + print("Testing with config: [{}]".format(message_config_keys)) + result = get_colorspaces_enumerator_items( + self.config_items, + include_aliases=True, + include_looks=True, + include_roles=True, + include_display_views=True, + ) + expected = [ + ("roles::compositing_linear", "[role] compositing_linear (linear)"), + ("colorspaces::Rec.709", "[colorspace] Rec.709"), + ("colorspaces::sRGB", "[colorspace] sRGB"), + ("aliases::rec709_1", "[alias] rec709_1 (Rec.709)"), + ("aliases::rec709_2", "[alias] rec709_2 (Rec.709)"), + ("aliases::sRGB_1", "[alias] sRGB_1 (sRGB)"), + ("looks::sRGB_to_Rec.709", "[look] sRGB_to_Rec.709 (sRGB)"), + ("displays_views::Rec.709 (ACES)", "[view (display)] Rec.709 (ACES)"), + ("displays_views::sRGB (ACES)", "[view (display)] sRGB (ACES)"), + ] + self.assertEqual(result, expected) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit/openpype/pipeline/test_colorspace_get_labeled_colorspaces.py b/tests/unit/openpype/pipeline/test_colorspace_get_labeled_colorspaces.py deleted file mode 100644 index 1760000e45..0000000000 --- a/tests/unit/openpype/pipeline/test_colorspace_get_labeled_colorspaces.py +++ /dev/null @@ -1,126 +0,0 @@ -import unittest -from unittest.mock import patch -from openpype.pipeline.colorspace import get_labeled_colorspaces - - -class TestGetLabeledColorspaces(unittest.TestCase): - @patch('openpype.pipeline.colorspace.get_ocio_config_colorspaces') - def test_returns_list_of_tuples(self, mock_get_ocio_config_colorspaces): - mock_get_ocio_config_colorspaces.return_value = { - 'colorspaces': { - 'sRGB': {}, - 'Rec.709': {}, - }, - 'looks': { - 'sRGB to Rec.709': { - 'process_space': 'Rec.709', - }, - }, - 'roles': { - 'reference': { - 'colorspace': 'sRGB', - }, - }, - } - result = get_labeled_colorspaces('config.ocio') - self.assertIsInstance(result, list) - self.assertTrue(all(isinstance(item, tuple) for item in result)) - - @patch('openpype.pipeline.colorspace.get_ocio_config_colorspaces') - def test_includes_colorspaces(self, mock_get_ocio_config_colorspaces): - mock_get_ocio_config_colorspaces.return_value = { - 'colorspaces': { - 'sRGB': {} - }, - 'looks': {}, - 'roles': {}, - } - result = get_labeled_colorspaces( - 'config.ocio', - include_aliases=False, - include_looks=False, - include_roles=False - ) - self.assertEqual(result, [('sRGB', '[colorspace] sRGB')]) - - @patch('openpype.pipeline.colorspace.get_ocio_config_colorspaces') - def test_includes_aliases(self, mock_get_ocio_config_colorspaces): - mock_get_ocio_config_colorspaces.return_value = { - 'colorspaces': { - 'sRGB': { - 'aliases': ['sRGB (D65)'], - }, - }, - 'looks': {}, - 'roles': {}, - } - result = get_labeled_colorspaces( - 'config.ocio', - include_aliases=True, - include_looks=False, - include_roles=False - ) - self.assertEqual( - result, [ - ('sRGB', '[colorspace] sRGB'), - ('sRGB (D65)', '[alias] sRGB (D65) (sRGB)') - ] - ) - - @patch('openpype.pipeline.colorspace.get_ocio_config_colorspaces') - def test_includes_looks(self, mock_get_ocio_config_colorspaces): - mock_get_ocio_config_colorspaces.return_value = { - 'colorspaces': {}, - 'looks': { - 'sRGB to Rec.709': { - 'process_space': 'Rec.709', - }, - }, - 'roles': {}, - } - result = get_labeled_colorspaces( - 'config.ocio', - include_aliases=False, - include_looks=True, - include_roles=False - ) - self.assertEqual( - result, [('sRGB to Rec.709', '[look] sRGB to Rec.709 (Rec.709)')]) - - @patch('openpype.pipeline.colorspace.get_ocio_config_colorspaces') - def test_includes_roles(self, mock_get_ocio_config_colorspaces): - mock_get_ocio_config_colorspaces.return_value = { - 'colorspaces': {}, - 'looks': {}, - 'roles': { - 'reference': { - 'colorspace': 'sRGB', - }, - }, - } - result = get_labeled_colorspaces( - 'config.ocio', - include_aliases=False, - include_looks=False, - include_roles=True - ) - self.assertEqual(result, [('reference', '[role] reference (sRGB)')]) - - @patch('openpype.pipeline.colorspace.get_ocio_config_colorspaces') - def test_includes_display_views(self, mock_get_ocio_config_colorspaces): - mock_get_ocio_config_colorspaces.return_value = { - 'colorspaces': {}, - 'looks': {}, - 'roles': {}, - 'displays_views': { - 'sRGB (ACES)': { - 'view': 'sRGB', - 'display': 'ACES', - }, - }, - } - result = get_labeled_colorspaces( - 'config.ocio', - include_display_views=True - ) - self.assertEqual(result, [('sRGB (ACES)', '[view (display)] sRGB (ACES)')]) From 124c528c5ce4dcf9580defb0e0788e7548b2721c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 5 Oct 2023 18:24:22 +0200 Subject: [PATCH 0151/1233] colorspace: improving collected ocio lut data --- .../plugins/create/create_colorspace_look.py | 39 +++++++++++----- .../publish/collect_colorspace_look.py | 44 ++++++++++++++++--- 2 files changed, 66 insertions(+), 17 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/create/create_colorspace_look.py b/openpype/hosts/traypublisher/plugins/create/create_colorspace_look.py index 3e1c20d96a..0daffc728c 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_colorspace_look.py +++ b/openpype/hosts/traypublisher/plugins/create/create_colorspace_look.py @@ -37,6 +37,7 @@ class CreateColorspaceLook(TrayPublishCreator): ] colorspace_attr_show = False config_items = None + config_data = None def get_detail_description(self): return """# Colorspace Look @@ -73,8 +74,20 @@ This creator publishes color space look file (LUT). # Create new instance new_instance = CreatedInstance(self.family, subset_name, instance_data, self) + new_instance.transient_data["config_items"] = self.config_items + new_instance.transient_data["config_data"] = self.config_data + self._store_new_instance(new_instance) + + def collect_instances(self): + super().collect_instances() + for instance in self.create_context.instances: + if instance.creator_identifier == self.identifier: + instance.transient_data["config_items"] = self.config_items + instance.transient_data["config_data"] = self.config_data + + def get_instance_attr_defs(self): return [ EnumDef( @@ -147,17 +160,21 @@ This creator publishes color space look file (LUT). project_settings=project_settings ) - if config_data: - filepath = config_data["path"] - config_items = colorspace.get_ocio_config_colorspaces(filepath) - labeled_colorspaces = colorspace.get_colorspaces_enumerator_items( - config_items, - include_aliases=True, - include_roles=True - ) - self.config_items = config_items - self.colorspace_items.extend(labeled_colorspaces) - self.enabled = True + if not config_data: + self.enabled = False + return + + filepath = config_data["path"] + config_items = colorspace.get_ocio_config_colorspaces(filepath) + labeled_colorspaces = colorspace.get_colorspaces_enumerator_items( + config_items, + include_aliases=True, + include_roles=True + ) + self.config_items = config_items + self.config_data = config_data + self.colorspace_items.extend(labeled_colorspaces) + self.enabled = True def _get_subset(self, asset_doc, variant, project_name, task_name=None): """Create subset name according to standard template process""" diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_colorspace_look.py b/openpype/hosts/traypublisher/plugins/publish/collect_colorspace_look.py index 739ab33f9c..4dc5348fb1 100644 --- a/openpype/hosts/traypublisher/plugins/publish/collect_colorspace_look.py +++ b/openpype/hosts/traypublisher/plugins/publish/collect_colorspace_look.py @@ -1,7 +1,8 @@ import os +from pprint import pformat import pyblish.api from openpype.pipeline import publish - +from openpype.pipeline import colorspace class CollectColorspaceLook(pyblish.api.InstancePlugin, publish.OpenPypePyblishPluginMixin): @@ -19,11 +20,36 @@ class CollectColorspaceLook(pyblish.api.InstancePlugin, lut_repre_name = "LUTfile" file_url = creator_attrs["abs_lut_path"] file_name = os.path.basename(file_url) - _, ext = os.path.splitext(file_name) + base_name, ext = os.path.splitext(file_name) + + # set output name with base_name which was cleared + # of all symbols and all parts were capitalized + output_name = (base_name.replace("_", " ") + .replace(".", " ") + .replace("-", " ") + .title() + .replace(" ", "")) + + + # get config items + config_items = instance.data["transientData"]["config_items"] + config_data = instance.data["transientData"]["config_data"] + + # get colorspace items + converted_color_data = {} + for colorspace_key in [ + "working_colorspace", + "input_colorspace", + "output_colorspace" + ]: + color_data = colorspace.convert_colorspace_enumerator_item( + creator_attrs[colorspace_key], config_items) + converted_color_data[colorspace_key] = color_data # create lut representation data lut_repre = { "name": lut_repre_name, + "output": output_name, "ext": ext.lstrip("."), "files": file_name, "stagingDir": os.path.dirname(file_url), @@ -36,11 +62,17 @@ class CollectColorspaceLook(pyblish.api.InstancePlugin, { "name": lut_repre_name, "ext": ext.lstrip("."), - "working_colorspace": creator_attrs["working_colorspace"], - "input_colorspace": creator_attrs["input_colorspace"], - "output_colorspace": creator_attrs["output_colorspace"], + "working_colorspace": converted_color_data[ + "working_colorspace"], + "input_colorspace": converted_color_data[ + "input_colorspace"], + "output_colorspace": converted_color_data[ + "output_colorspace"], "direction": creator_attrs["direction"], - "interpolation": creator_attrs["interpolation"] + "interpolation": creator_attrs["interpolation"], + "config_data": config_data } ] }) + + self.log.debug(pformat(instance.data)) From 908e980a404bf33dd7657414658cbd801ceb86d0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 6 Oct 2023 13:21:25 +0200 Subject: [PATCH 0152/1233] updating importing to media pool to newer api --- openpype/hosts/resolve/api/lib.py | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index 735d2057f8..fb4b08cc1e 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -196,7 +196,6 @@ def create_media_pool_item(fpath: str, object: resolve.MediaPoolItem """ # get all variables - media_storage = get_media_storage() media_pool = get_current_project().GetMediaPool() root_bin = root or media_pool.GetRootFolder() @@ -205,23 +204,10 @@ def create_media_pool_item(fpath: str, if existing_mpi: return existing_mpi + # add all data in folder to media pool + media_pool_items = media_pool.ImportMedia(fpath) - dirname, file = os.path.split(fpath) - _name, ext = os.path.splitext(file) - - # add all data in folder to mediapool - media_pool_items = media_storage.AddItemListToMediaPool( - os.path.normpath(dirname)) - - if not media_pool_items: - return False - - # if any are added then look into them for the right extension - media_pool_item = [mpi for mpi in media_pool_items - if ext in mpi.GetClipProperty("File Path")] - - # return only first found - return media_pool_item.pop() + return media_pool_items.pop() if media_pool_items else False def get_media_pool_item(fpath, root: object = None) -> object: From 69c8d1985b58b2e5151cb00ec102f81c26d9d93b Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 6 Oct 2023 19:46:49 +0800 Subject: [PATCH 0153/1233] tweaks on the validation report & repair action --- .../plugins/publish/validate_resolution.py | 40 +++++++++++++------ 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_resolution.py b/openpype/hosts/maya/plugins/publish/validate_resolution.py index 578c99e006..b1752aa4bd 100644 --- a/openpype/hosts/maya/plugins/publish/validate_resolution.py +++ b/openpype/hosts/maya/plugins/publish/validate_resolution.py @@ -11,7 +11,7 @@ from openpype.hosts.maya.api.lib import reset_scene_resolution class ValidateSceneResolution(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): - """Validate the scene resolution setting aligned with DB""" + """Validate the render resolution setting aligned with DB""" order = pyblish.api.ValidatorOrder - 0.01 families = ["renderlayer"] @@ -23,25 +23,34 @@ class ValidateSceneResolution(pyblish.api.InstancePlugin, def process(self, instance): if not self.is_active(instance.data): return + invalid = self.get_invalid_resolution(instance) + if invalid: + raise PublishValidationError("issues occurred", description=( + "Wrong render resolution setting. Please use repair button to fix it.\n" + "If current renderer is vray, make sure vraySettings node has been created" + )) + + def get_invalid_resolution(self, instance): width, height, pixelAspect = self.get_db_resolution(instance) current_renderer = cmds.getAttr( "defaultRenderGlobals.currentRenderer") layer = instance.data["renderlayer"] + invalids = [] if current_renderer == "vray": vray_node = "vraySettings" if cmds.objExists(vray_node): - control_node = vray_node current_width = lib.get_attr_in_layer( - "{}.width".format(control_node), layer=layer) + "{}.width".format(vray_node), layer=layer) current_height = lib.get_attr_in_layer( - "{}.height".format(control_node), layer=layer) + "{}.height".format(vray_node), layer=layer) current_pixelAspect = lib.get_attr_in_layer( - "{}.pixelAspect".format(control_node), layer=layer + "{}.pixelAspect".format(vray_node), layer=layer ) else: - raise PublishValidationError( - "Can't set VRay resolution because there is no node " - "named: `%s`" % vray_node) + invalid = self.log.error( + "Can't detect VRay resolution because there is no node " + "named: `{}`".format(vray_node)) + invalids.append(invalid) else: current_width = lib.get_attr_in_layer( "defaultResolution.width", layer=layer) @@ -51,15 +60,18 @@ class ValidateSceneResolution(pyblish.api.InstancePlugin, "defaultResolution.pixelAspect", layer=layer ) if current_width != width or current_height != height: - raise PublishValidationError( + invalid = self.log.error( "Render resolution {}x{} does not match asset resolution {}x{}".format( # noqa:E501 current_width, current_height, width, height )) + invalids.append("{0}\n".format(invalid)) if current_pixelAspect != pixelAspect: - raise PublishValidationError( + invalid = self.log.error( "Render pixel aspect {} does not match asset pixel aspect {}".format( # noqa:E501 current_pixelAspect, pixelAspect )) + invalids.append("{0}\n".format(invalid)) + return invalids def get_db_resolution(self, instance): asset_doc = instance.data["assetEntity"] @@ -71,11 +83,13 @@ class ValidateSceneResolution(pyblish.api.InstancePlugin, width = data["resolutionWidth"] height = data["resolutionHeight"] pixelAspect = data["pixelAspect"] - return int(width), int(height), int(pixelAspect) + return int(width), int(height), float(pixelAspect) # Defaults if not found in asset document or project document - return 1920, 1080, 1 + return 1920, 1080, 1.0 @classmethod def repair(cls, instance): - return reset_scene_resolution() + layer = instance.data["renderlayer"] + with lib.renderlayer(layer): + reset_scene_resolution() From 8d7664420fdabdf3fed6f0f572d627f12ac29551 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 6 Oct 2023 19:50:33 +0800 Subject: [PATCH 0154/1233] hound --- .../maya/plugins/publish/validate_resolution.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_resolution.py b/openpype/hosts/maya/plugins/publish/validate_resolution.py index b1752aa4bd..8e761d8958 100644 --- a/openpype/hosts/maya/plugins/publish/validate_resolution.py +++ b/openpype/hosts/maya/plugins/publish/validate_resolution.py @@ -25,9 +25,12 @@ class ValidateSceneResolution(pyblish.api.InstancePlugin, return invalid = self.get_invalid_resolution(instance) if invalid: - raise PublishValidationError("issues occurred", description=( - "Wrong render resolution setting. Please use repair button to fix it.\n" - "If current renderer is vray, make sure vraySettings node has been created" + raise PublishValidationError( + "issues occurred", description=( + "Wrong render resolution setting. " + "Please use repair button to fix it.\n" + "If current renderer is V-Ray, " + "make sure vraySettings node has been created" )) def get_invalid_resolution(self, instance): @@ -62,7 +65,8 @@ class ValidateSceneResolution(pyblish.api.InstancePlugin, if current_width != width or current_height != height: invalid = self.log.error( "Render resolution {}x{} does not match asset resolution {}x{}".format( # noqa:E501 - current_width, current_height, width, height + current_width, current_height, + width, height )) invalids.append("{0}\n".format(invalid)) if current_pixelAspect != pixelAspect: From 8fc0c3b81f1e8bfa786e0fa9f71c6da5c9bc57e7 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 6 Oct 2023 19:54:15 +0800 Subject: [PATCH 0155/1233] hound --- .../maya/plugins/publish/validate_resolution.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_resolution.py b/openpype/hosts/maya/plugins/publish/validate_resolution.py index 8e761d8958..f00b2329ed 100644 --- a/openpype/hosts/maya/plugins/publish/validate_resolution.py +++ b/openpype/hosts/maya/plugins/publish/validate_resolution.py @@ -27,10 +27,10 @@ class ValidateSceneResolution(pyblish.api.InstancePlugin, if invalid: raise PublishValidationError( "issues occurred", description=( - "Wrong render resolution setting. " - "Please use repair button to fix it.\n" - "If current renderer is V-Ray, " - "make sure vraySettings node has been created" + "Wrong render resolution setting. " + "Please use repair button to fix it.\n" + "If current renderer is V-Ray, " + "make sure vraySettings node has been created" )) def get_invalid_resolution(self, instance): @@ -52,7 +52,8 @@ class ValidateSceneResolution(pyblish.api.InstancePlugin, else: invalid = self.log.error( "Can't detect VRay resolution because there is no node " - "named: `{}`".format(vray_node)) + "named: `{}`".format(vray_node) + ) invalids.append(invalid) else: current_width = lib.get_attr_in_layer( @@ -63,12 +64,12 @@ class ValidateSceneResolution(pyblish.api.InstancePlugin, "defaultResolution.pixelAspect", layer=layer ) if current_width != width or current_height != height: - invalid = self.log.error( + invalid = self.log.error( "Render resolution {}x{} does not match asset resolution {}x{}".format( # noqa:E501 current_width, current_height, width, height )) - invalids.append("{0}\n".format(invalid)) + invalids.append("{0}\n".format(invalid)) if current_pixelAspect != pixelAspect: invalid = self.log.error( "Render pixel aspect {} does not match asset pixel aspect {}".format( # noqa:E501 From 145716211d6c04fea0d0eb3c422b07c2d3edd300 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 6 Oct 2023 19:55:32 +0800 Subject: [PATCH 0156/1233] hound --- .../hosts/maya/plugins/publish/validate_resolution.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_resolution.py b/openpype/hosts/maya/plugins/publish/validate_resolution.py index f00b2329ed..c920be4602 100644 --- a/openpype/hosts/maya/plugins/publish/validate_resolution.py +++ b/openpype/hosts/maya/plugins/publish/validate_resolution.py @@ -27,10 +27,10 @@ class ValidateSceneResolution(pyblish.api.InstancePlugin, if invalid: raise PublishValidationError( "issues occurred", description=( - "Wrong render resolution setting. " - "Please use repair button to fix it.\n" - "If current renderer is V-Ray, " - "make sure vraySettings node has been created" + "Wrong render resolution setting. " + "Please use repair button to fix it.\n" + "If current renderer is V-Ray, " + "make sure vraySettings node has been created" )) def get_invalid_resolution(self, instance): From 4dc4d665b05c77aa2bc69a517aae0389522bc4b4 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 6 Oct 2023 19:56:34 +0800 Subject: [PATCH 0157/1233] hound --- openpype/hosts/maya/plugins/publish/validate_resolution.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_resolution.py b/openpype/hosts/maya/plugins/publish/validate_resolution.py index c920be4602..237a0fa186 100644 --- a/openpype/hosts/maya/plugins/publish/validate_resolution.py +++ b/openpype/hosts/maya/plugins/publish/validate_resolution.py @@ -31,7 +31,7 @@ class ValidateSceneResolution(pyblish.api.InstancePlugin, "Please use repair button to fix it.\n" "If current renderer is V-Ray, " "make sure vraySettings node has been created" - )) + )) def get_invalid_resolution(self, instance): width, height, pixelAspect = self.get_db_resolution(instance) From 91d41c86c5310d5239ce5638dcb01c1c66b600b9 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 6 Oct 2023 19:57:35 +0800 Subject: [PATCH 0158/1233] hound --- openpype/hosts/maya/plugins/publish/validate_resolution.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_resolution.py b/openpype/hosts/maya/plugins/publish/validate_resolution.py index 237a0fa186..fadb41302c 100644 --- a/openpype/hosts/maya/plugins/publish/validate_resolution.py +++ b/openpype/hosts/maya/plugins/publish/validate_resolution.py @@ -30,8 +30,7 @@ class ValidateSceneResolution(pyblish.api.InstancePlugin, "Wrong render resolution setting. " "Please use repair button to fix it.\n" "If current renderer is V-Ray, " - "make sure vraySettings node has been created" - )) + "make sure vraySettings node has been created")) def get_invalid_resolution(self, instance): width, height, pixelAspect = self.get_db_resolution(instance) From 26e0cacd3a676d085ff28719a6c52176d1757253 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 6 Oct 2023 14:03:15 +0200 Subject: [PATCH 0159/1233] removing test scrips --- .../utility_scripts/tests/test_otio_as_edl.py | 49 ------------- .../testing_create_timeline_item_from_path.py | 73 ------------------- .../tests/testing_load_media_pool_item.py | 24 ------ .../tests/testing_startup_script.py | 5 -- .../tests/testing_timeline_op.py | 13 ---- 5 files changed, 164 deletions(-) delete mode 100644 openpype/hosts/resolve/utility_scripts/tests/test_otio_as_edl.py delete mode 100644 openpype/hosts/resolve/utility_scripts/tests/testing_create_timeline_item_from_path.py delete mode 100644 openpype/hosts/resolve/utility_scripts/tests/testing_load_media_pool_item.py delete mode 100644 openpype/hosts/resolve/utility_scripts/tests/testing_startup_script.py delete mode 100644 openpype/hosts/resolve/utility_scripts/tests/testing_timeline_op.py diff --git a/openpype/hosts/resolve/utility_scripts/tests/test_otio_as_edl.py b/openpype/hosts/resolve/utility_scripts/tests/test_otio_as_edl.py deleted file mode 100644 index 92f2e43a72..0000000000 --- a/openpype/hosts/resolve/utility_scripts/tests/test_otio_as_edl.py +++ /dev/null @@ -1,49 +0,0 @@ -#! python3 -import os -import sys - -import opentimelineio as otio - -from openpype.pipeline import install_host - -import openpype.hosts.resolve.api as bmdvr -from openpype.hosts.resolve.api.testing_utils import TestGUI -from openpype.hosts.resolve.otio import davinci_export as otio_export - - -class ThisTestGUI(TestGUI): - extensions = [".exr", ".jpg", ".mov", ".png", ".mp4", ".ari", ".arx"] - - def __init__(self): - super(ThisTestGUI, self).__init__() - # activate resolve from openpype - install_host(bmdvr) - - def _open_dir_button_pressed(self, event): - # selected_path = self.fu.RequestFile(os.path.expanduser("~")) - selected_path = self.fu.RequestDir(os.path.expanduser("~")) - self._widgets["inputTestSourcesFolder"].Text = selected_path - - # main function - def process(self, event): - self.input_dir_path = self._widgets["inputTestSourcesFolder"].Text - project = bmdvr.get_current_project() - otio_timeline = otio_export.create_otio_timeline(project) - print(f"_ otio_timeline: `{otio_timeline}`") - edl_path = os.path.join(self.input_dir_path, "this_file_name.edl") - print(f"_ edl_path: `{edl_path}`") - # xml_string = otio_adapters.fcpx_xml.write_to_string(otio_timeline) - # print(f"_ xml_string: `{xml_string}`") - otio.adapters.write_to_file( - otio_timeline, edl_path, adapter_name="cmx_3600") - project = bmdvr.get_current_project() - media_pool = project.GetMediaPool() - timeline = media_pool.ImportTimelineFromFile(edl_path) - # at the end close the window - self._close_window(None) - - -if __name__ == "__main__": - test_gui = ThisTestGUI() - test_gui.show_gui() - sys.exit(not bool(True)) diff --git a/openpype/hosts/resolve/utility_scripts/tests/testing_create_timeline_item_from_path.py b/openpype/hosts/resolve/utility_scripts/tests/testing_create_timeline_item_from_path.py deleted file mode 100644 index 91a361ec08..0000000000 --- a/openpype/hosts/resolve/utility_scripts/tests/testing_create_timeline_item_from_path.py +++ /dev/null @@ -1,73 +0,0 @@ -#! python3 -import os -import sys - -import clique - -from openpype.pipeline import install_host -from openpype.hosts.resolve.api.testing_utils import TestGUI -import openpype.hosts.resolve.api as bmdvr -from openpype.hosts.resolve.api.lib import ( - create_media_pool_item, - create_timeline_item, -) - - -class ThisTestGUI(TestGUI): - extensions = [".exr", ".jpg", ".mov", ".png", ".mp4", ".ari", ".arx"] - - def __init__(self): - super(ThisTestGUI, self).__init__() - # activate resolve from openpype - install_host(bmdvr) - - def _open_dir_button_pressed(self, event): - # selected_path = self.fu.RequestFile(os.path.expanduser("~")) - selected_path = self.fu.RequestDir(os.path.expanduser("~")) - self._widgets["inputTestSourcesFolder"].Text = selected_path - - # main function - def process(self, event): - self.input_dir_path = self._widgets["inputTestSourcesFolder"].Text - - self.dir_processing(self.input_dir_path) - - # at the end close the window - self._close_window(None) - - def dir_processing(self, dir_path): - collections, reminders = clique.assemble(os.listdir(dir_path)) - - # process reminders - for _rem in reminders: - _rem_path = os.path.join(dir_path, _rem) - - # go deeper if directory - if os.path.isdir(_rem_path): - print(_rem_path) - self.dir_processing(_rem_path) - else: - self.file_processing(_rem_path) - - # process collections - for _coll in collections: - _coll_path = os.path.join(dir_path, list(_coll).pop()) - self.file_processing(_coll_path) - - def file_processing(self, fpath): - print(f"_ fpath: `{fpath}`") - _base, ext = os.path.splitext(fpath) - # skip if unwanted extension - if ext not in self.extensions: - return - media_pool_item = create_media_pool_item(fpath) - print(media_pool_item) - - track_item = create_timeline_item(media_pool_item) - print(track_item) - - -if __name__ == "__main__": - test_gui = ThisTestGUI() - test_gui.show_gui() - sys.exit(not bool(True)) diff --git a/openpype/hosts/resolve/utility_scripts/tests/testing_load_media_pool_item.py b/openpype/hosts/resolve/utility_scripts/tests/testing_load_media_pool_item.py deleted file mode 100644 index 2e83188bde..0000000000 --- a/openpype/hosts/resolve/utility_scripts/tests/testing_load_media_pool_item.py +++ /dev/null @@ -1,24 +0,0 @@ -#! python3 -from openpype.pipeline import install_host -from openpype.hosts.resolve import api as bmdvr -from openpype.hosts.resolve.api.lib import ( - create_media_pool_item, - create_timeline_item, -) - - -def file_processing(fpath): - media_pool_item = create_media_pool_item(fpath) - print(media_pool_item) - - track_item = create_timeline_item(media_pool_item) - print(track_item) - - -if __name__ == "__main__": - path = "C:/CODE/__openpype_projects/jtest03dev/shots/sq01/mainsq01sh030/publish/plate/plateMain/v006/jt3d_mainsq01sh030_plateMain_v006.0996.exr" - - # activate resolve from openpype - install_host(bmdvr) - - file_processing(path) diff --git a/openpype/hosts/resolve/utility_scripts/tests/testing_startup_script.py b/openpype/hosts/resolve/utility_scripts/tests/testing_startup_script.py deleted file mode 100644 index b64714ab16..0000000000 --- a/openpype/hosts/resolve/utility_scripts/tests/testing_startup_script.py +++ /dev/null @@ -1,5 +0,0 @@ -#! python3 -from openpype.hosts.resolve.startup import main - -if __name__ == "__main__": - main() diff --git a/openpype/hosts/resolve/utility_scripts/tests/testing_timeline_op.py b/openpype/hosts/resolve/utility_scripts/tests/testing_timeline_op.py deleted file mode 100644 index 8270496f64..0000000000 --- a/openpype/hosts/resolve/utility_scripts/tests/testing_timeline_op.py +++ /dev/null @@ -1,13 +0,0 @@ -#! python3 -from openpype.pipeline import install_host -from openpype.hosts.resolve import api as bmdvr -from openpype.hosts.resolve.api.lib import get_current_project - -if __name__ == "__main__": - install_host(bmdvr) - project = get_current_project() - timeline_count = project.GetTimelineCount() - print(f"Timeline count: {timeline_count}") - timeline = project.GetTimelineByIndex(timeline_count) - print(f"Timeline name: {timeline.GetName()}") - print(timeline.GetTrackCount("video")) From 19840862426e74c6558803864ec212014ada186f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 6 Oct 2023 14:03:38 +0200 Subject: [PATCH 0160/1233] finalizing update of importing clips with multiple frames --- openpype/hosts/resolve/api/lib.py | 43 +++++++++++++++++++++-- openpype/hosts/resolve/api/pipeline.py | 1 - openpype/hosts/resolve/api/plugin.py | 47 ++++++++++++++++++++------ 3 files changed, 77 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index fb4b08cc1e..8564a24ac1 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -2,6 +2,7 @@ import sys import json import re import os +import glob import contextlib from opentimelineio import opentime @@ -183,8 +184,14 @@ def create_bin(name: str, root: object = None) -> object: return media_pool.GetCurrentFolder() -def create_media_pool_item(fpath: str, - root: object = None) -> object: +def create_media_pool_item( + fpath: str, + frame_start: int, + frame_end: int, + handle_start: int, + handle_end: int, + root: object = None, +) -> object: """ Create media pool item. @@ -204,8 +211,38 @@ def create_media_pool_item(fpath: str, if existing_mpi: return existing_mpi + + files = [] + first_frame = frame_start - handle_start + last_frame = frame_end + handle_end + dir_path = os.path.dirname(fpath) + base_name = os.path.basename(fpath) + + # prepare glob pattern for searching + padding = len(str(last_frame)) + str_first_frame = str(first_frame).zfill(padding) + + # convert str_first_frame to glob pattern + # replace all digits with `?` and all other chars with `[char]` + # example: `0001` -> `????` + glob_pattern = re.sub(r"\d", "?", str_first_frame) + + # in filename replace number with glob pattern + # example: `filename.0001.exr` -> `filename.????.exr` + base_name = re.sub(str_first_frame, glob_pattern, base_name) + + # get all files in folder + for file in glob.glob(os.path.join(dir_path, base_name)): + files.append(file) + + # iterate all files and check if they exists + # if not then remove them from list + for file in files[:]: + if not os.path.exists(file): + files.remove(file) + # add all data in folder to media pool - media_pool_items = media_pool.ImportMedia(fpath) + media_pool_items = media_pool.ImportMedia(files) return media_pool_items.pop() if media_pool_items else False diff --git a/openpype/hosts/resolve/api/pipeline.py b/openpype/hosts/resolve/api/pipeline.py index 899cb825bb..b379c7b2e0 100644 --- a/openpype/hosts/resolve/api/pipeline.py +++ b/openpype/hosts/resolve/api/pipeline.py @@ -117,7 +117,6 @@ def containerise(timeline_item, for k, v in data.items(): data_imprint.update({k: v}) - print("_ data_imprint: {}".format(data_imprint)) lib.set_timeline_item_pype_tag(timeline_item, data_imprint) return timeline_item diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py index da5e649576..b4c03d6809 100644 --- a/openpype/hosts/resolve/api/plugin.py +++ b/openpype/hosts/resolve/api/plugin.py @@ -1,6 +1,5 @@ import re import uuid - import qargparse from qtpy import QtWidgets, QtCore @@ -393,16 +392,15 @@ class ClipLoader: asset_name = self.context["representation"]["context"]["asset"] self.data["assetData"] = get_current_project_asset(asset_name)["data"] - def load(self): - # create project bin for the media to be imported into - self.active_bin = lib.create_bin(self.data["binPath"]) - + def _get_frame_data(self): # create mediaItem in active project bin # create clip media - - media_pool_item = lib.create_media_pool_item( - self.data["path"], self.active_bin) - _clip_property = media_pool_item.GetClipProperty + frame_start = self.data["versionData"].get("frameStart") + frame_end = self.data["versionData"].get("frameEnd") + if frame_start is None: + frame_start = int(self.data["assetData"]["frameStart"]) + if frame_end is None: + frame_end = int(self.data["assetData"]["frameEnd"]) # get handles handle_start = self.data["versionData"].get("handleStart") @@ -412,6 +410,26 @@ class ClipLoader: if handle_end is None: handle_end = int(self.data["assetData"]["handleEnd"]) + return frame_start, frame_end, handle_start, handle_end + + def load(self): + # create project bin for the media to be imported into + self.active_bin = lib.create_bin(self.data["binPath"]) + + frame_start, frame_end, handle_start, handle_end = \ + self._get_frame_data() + + media_pool_item = lib.create_media_pool_item( + self.data["path"], + frame_start, + frame_end, + handle_start, + handle_end, + self.active_bin + ) + _clip_property = media_pool_item.GetClipProperty + + source_in = int(_clip_property("Start")) source_out = int(_clip_property("End")) @@ -435,10 +453,19 @@ class ClipLoader: # create project bin for the media to be imported into self.active_bin = lib.create_bin(self.data["binPath"]) + frame_start, frame_end, handle_start, handle_end = \ + self._get_frame_data() + # create mediaItem in active project bin # create clip media media_pool_item = lib.create_media_pool_item( - self.data["path"], self.active_bin) + self.data["path"], + frame_start, + frame_end, + handle_start, + handle_end, + self.active_bin + ) _clip_property = media_pool_item.GetClipProperty source_in = int(_clip_property("Start")) From 5262c0c7acab605ccecbd13357e58b8666d0f2f9 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 6 Oct 2023 20:29:56 +0800 Subject: [PATCH 0161/1233] tweaks on get_invalid_resolution --- .../plugins/publish/validate_resolution.py | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_resolution.py b/openpype/hosts/maya/plugins/publish/validate_resolution.py index fadb41302c..38deca9ecf 100644 --- a/openpype/hosts/maya/plugins/publish/validate_resolution.py +++ b/openpype/hosts/maya/plugins/publish/validate_resolution.py @@ -34,10 +34,9 @@ class ValidateSceneResolution(pyblish.api.InstancePlugin, def get_invalid_resolution(self, instance): width, height, pixelAspect = self.get_db_resolution(instance) - current_renderer = cmds.getAttr( - "defaultRenderGlobals.currentRenderer") + current_renderer = instance.data["renderer"] layer = instance.data["renderlayer"] - invalids = [] + invalid = False if current_renderer == "vray": vray_node = "vraySettings" if cmds.objExists(vray_node): @@ -49,11 +48,11 @@ class ValidateSceneResolution(pyblish.api.InstancePlugin, "{}.pixelAspect".format(vray_node), layer=layer ) else: - invalid = self.log.error( + self.log.error( "Can't detect VRay resolution because there is no node " "named: `{}`".format(vray_node) ) - invalids.append(invalid) + invalid = True else: current_width = lib.get_attr_in_layer( "defaultResolution.width", layer=layer) @@ -63,19 +62,21 @@ class ValidateSceneResolution(pyblish.api.InstancePlugin, "defaultResolution.pixelAspect", layer=layer ) if current_width != width or current_height != height: - invalid = self.log.error( - "Render resolution {}x{} does not match asset resolution {}x{}".format( # noqa:E501 + self.log.error( + "Render resolution {}x{} does not match " + "asset resolution {}x{}".format( current_width, current_height, width, height )) - invalids.append("{0}\n".format(invalid)) + invalid = True if current_pixelAspect != pixelAspect: - invalid = self.log.error( - "Render pixel aspect {} does not match asset pixel aspect {}".format( # noqa:E501 + self.log.error( + "Render pixel aspect {} does not match " + "asset pixel aspect {}".format( current_pixelAspect, pixelAspect )) - invalids.append("{0}\n".format(invalid)) - return invalids + invalid = True + return invalid def get_db_resolution(self, instance): asset_doc = instance.data["assetEntity"] From b4c0f2880a32f5e9e7a56597307894bbe63f24c0 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 6 Oct 2023 20:35:38 +0800 Subject: [PATCH 0162/1233] add validate resolution as parts of maya settings --- .../deadline/plugins/publish/submit_publish_job.py | 2 +- openpype/settings/defaults/project_settings/maya.json | 5 +++++ .../projects_schema/schemas/schema_maya_publish.json | 4 ++++ server_addon/maya/server/settings/publishers.py | 9 +++++++++ 4 files changed, 19 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 6ed5819f2b..57ce8c438f 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -321,7 +321,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, self.log.debug("Submitting Deadline publish job ...") url = "{}/api/jobs".format(self.deadline_url) - response = requests.post(url, json=payload, timeout=10) + response = requests.post(url, json=payload, timeout=10, verify=False) if not response.ok: raise Exception(response.text) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 300d63985b..7719a5e255 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -829,6 +829,11 @@ "redshift_render_attributes": [], "renderman_render_attributes": [] }, + "ValidateResolution": { + "enabled": true, + "optional": true, + "active": true + }, "ValidateCurrentRenderLayerIsRenderable": { "enabled": true, "optional": false, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json index 8a0815c185..d2e7c51e24 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json @@ -431,6 +431,10 @@ "type": "schema_template", "name": "template_publish_plugin", "template_data": [ + { + "key": "ValidateResolution", + "label": "Validate Resolution Settings" + }, { "key": "ValidateCurrentRenderLayerIsRenderable", "label": "Validate Current Render Layer Has Renderable Camera" diff --git a/server_addon/maya/server/settings/publishers.py b/server_addon/maya/server/settings/publishers.py index 6c5baa3900..dd8d4a0a37 100644 --- a/server_addon/maya/server/settings/publishers.py +++ b/server_addon/maya/server/settings/publishers.py @@ -433,6 +433,10 @@ class PublishersModel(BaseSettingsModel): default_factory=ValidateRenderSettingsModel, title="Validate Render Settings" ) + ValidateResolution: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Resolution Setting" + ) ValidateCurrentRenderLayerIsRenderable: BasicValidateModel = Field( default_factory=BasicValidateModel, title="Validate Current Render Layer Has Renderable Camera" @@ -902,6 +906,11 @@ DEFAULT_PUBLISH_SETTINGS = { "redshift_render_attributes": [], "renderman_render_attributes": [] }, + "ValidateResolution": { + "enabled": True, + "optional": True, + "active": True + }, "ValidateCurrentRenderLayerIsRenderable": { "enabled": True, "optional": False, From 13c9aec4a7b8a58e4f03bd6fa20462c051aaaf3c Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 6 Oct 2023 20:59:40 +0800 Subject: [PATCH 0163/1233] Rename ValidateSceneResolution to ValidateResolution --- openpype/hosts/maya/plugins/publish/validate_resolution.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_resolution.py b/openpype/hosts/maya/plugins/publish/validate_resolution.py index 38deca9ecf..66962afce5 100644 --- a/openpype/hosts/maya/plugins/publish/validate_resolution.py +++ b/openpype/hosts/maya/plugins/publish/validate_resolution.py @@ -9,8 +9,8 @@ from openpype.hosts.maya.api import lib from openpype.hosts.maya.api.lib import reset_scene_resolution -class ValidateSceneResolution(pyblish.api.InstancePlugin, - OptionalPyblishPluginMixin): +class ValidateResolution(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Validate the render resolution setting aligned with DB""" order = pyblish.api.ValidatorOrder - 0.01 From 64b03447f128b3b564441050edfed23bf2926cd5 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 6 Oct 2023 21:01:46 +0800 Subject: [PATCH 0164/1233] restore unrelated code --- 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 57ce8c438f..6ed5819f2b 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -321,7 +321,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, self.log.debug("Submitting Deadline publish job ...") url = "{}/api/jobs".format(self.deadline_url) - response = requests.post(url, json=payload, timeout=10, verify=False) + response = requests.post(url, json=payload, timeout=10) if not response.ok: raise Exception(response.text) From b2588636e9970d94a6537a2ac4a735a03978ee9c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 6 Oct 2023 15:11:19 +0200 Subject: [PATCH 0165/1233] add removing of media pool item for clip remove. no way to remove timeline item so they stay offline at timeline --- openpype/hosts/resolve/api/lib.py | 6 ++++++ openpype/hosts/resolve/plugins/load/load_clip.py | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index 8564a24ac1..5d80866e6a 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -184,6 +184,12 @@ def create_bin(name: str, root: object = None) -> object: return media_pool.GetCurrentFolder() +def remove_media_pool_item(media_pool_item: object) -> bool: + print(media_pool_item) + media_pool = get_current_project().GetMediaPool() + return media_pool.DeleteClips([media_pool_item]) + + def create_media_pool_item( fpath: str, frame_start: int, diff --git a/openpype/hosts/resolve/plugins/load/load_clip.py b/openpype/hosts/resolve/plugins/load/load_clip.py index eea44a3726..fd181bae41 100644 --- a/openpype/hosts/resolve/plugins/load/load_clip.py +++ b/openpype/hosts/resolve/plugins/load/load_clip.py @@ -163,3 +163,10 @@ class LoadClip(plugin.TimelineItemLoader): timeline_item.SetClipColor(cls.clip_color_last) else: timeline_item.SetClipColor(cls.clip_color) + + def remove(self, container): + namespace = container['namespace'] + timeline_item = lib.get_pype_timeline_item_by_name(namespace) + take_mp_item = timeline_item.GetMediaPoolItem() + + lib.remove_media_pool_item(take_mp_item) From c7df127becf48474494f59087900c4aceaa39e58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Fri, 6 Oct 2023 15:19:27 +0200 Subject: [PATCH 0166/1233] Update openpype/hosts/resolve/api/lib.py Co-authored-by: Roy Nieterau --- openpype/hosts/resolve/api/lib.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index 5d80866e6a..c3ab1a263b 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -243,9 +243,7 @@ def create_media_pool_item( # iterate all files and check if they exists # if not then remove them from list - for file in files[:]: - if not os.path.exists(file): - files.remove(file) + files = [f for f in files if os.path.exists(f)] # add all data in folder to media pool media_pool_items = media_pool.ImportMedia(files) From 446ee7983113c5e36578ab9650d99d81a566a1a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Fri, 6 Oct 2023 15:19:35 +0200 Subject: [PATCH 0167/1233] Update openpype/hosts/resolve/api/lib.py Co-authored-by: Roy Nieterau --- openpype/hosts/resolve/api/lib.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index c3ab1a263b..4d186e199d 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -241,8 +241,7 @@ def create_media_pool_item( for file in glob.glob(os.path.join(dir_path, base_name)): files.append(file) - # iterate all files and check if they exists - # if not then remove them from list + # keep only existing files files = [f for f in files if os.path.exists(f)] # add all data in folder to media pool From 5ac109a7aeaca0a8797a0c43c81d51e6957bc2ce Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 6 Oct 2023 15:21:04 +0200 Subject: [PATCH 0168/1233] :art: add task name option --- .../plugins/create/create_multishot_layout.py | 69 +++++++++++++------ 1 file changed, 48 insertions(+), 21 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_multishot_layout.py b/openpype/hosts/maya/plugins/create/create_multishot_layout.py index 706203bdab..d0c4137ac4 100644 --- a/openpype/hosts/maya/plugins/create/create_multishot_layout.py +++ b/openpype/hosts/maya/plugins/create/create_multishot_layout.py @@ -1,20 +1,24 @@ -from ayon_api import get_folder_by_name, get_folder_by_path, get_folders +from ayon_api import ( + get_folder_by_name, + get_folder_by_path, + get_folders, +) from maya import cmds # noqa: F401 from openpype import AYON_SERVER_ENABLED from openpype.client import get_assets from openpype.hosts.maya.api import plugin -from openpype.lib import BoolDef, EnumDef +from openpype.lib import BoolDef, EnumDef, TextDef from openpype.pipeline import ( Creator, get_current_asset_name, - get_current_project_name + get_current_project_name, ) from openpype.pipeline.create import CreatorError class CreateMultishotLayout(plugin.MayaCreator): - """Create a multishot layout in the Maya scene. + """Create a multi-shot layout in the Maya scene. This creator will create a Camera Sequencer in the Maya scene based on the shots found under the specified folder. The shots will be added to @@ -23,7 +27,7 @@ class CreateMultishotLayout(plugin.MayaCreator): """ identifier = "io.openpype.creators.maya.multishotlayout" - label = "Multishot Layout" + label = "Multi-shot Layout" family = "layout" icon = "project-diagram" @@ -46,16 +50,19 @@ class CreateMultishotLayout(plugin.MayaCreator): folder_name=get_current_asset_name(), ) + current_path_parts = current_folder["path"].split("/") + items_with_label = [ - dict(label=p if p != current_folder["name"] else f"{p} (current)", - value=str(p)) - for p in current_folder["path"].split("/") + dict( + label=current_path_parts[p] if current_path_parts[p] != current_folder["name"] else f"{current_path_parts[p]} (current)", # noqa + value="/".join(current_path_parts[:p+1]), + ) + for p in range(len(current_path_parts)) ] - items_with_label.insert(0, - dict(label=f"{self.project_name} " - "(shots directly under the project)", - value=None)) + items_with_label.insert( + 0, dict(label=f"{self.project_name} " + "(shots directly under the project)", value="")) return [ EnumDef("shotParent", @@ -67,7 +74,12 @@ class CreateMultishotLayout(plugin.MayaCreator): label="Group Loaded Assets", tooltip="Enable this when you want to publish group of " "loaded asset", - default=False) + default=False), + TextDef("taskName", + label="Associated Task Name", + tooltip=("Task name to be associated " + "with the created Layout"), + default="layout"), ] def create(self, subset_name, instance_data, pre_create_data): @@ -98,6 +110,18 @@ class CreateMultishotLayout(plugin.MayaCreator): if not shot["active"]: continue + # get task for shot + asset_doc = next( + asset_doc for asset_doc in op_asset_docs + if asset_doc["_id"] == shot["id"] + + ) + + tasks = list(asset_doc.get("data").get("tasks").keys()) + layout_task = None + if pre_create_data["taskName"] in tasks: + layout_task = pre_create_data["taskName"] + shot_name = f"{shot['name']}%s" % ( f" ({shot['label']})" if shot["label"] else "") cmds.shot(sst=shot["attrib"]["clipIn"], @@ -105,18 +129,21 @@ class CreateMultishotLayout(plugin.MayaCreator): shotName=shot_name) # Create layout instance by the layout creator + + instance_data = { + "asset": shot["name"], + "variant": layout_creator.get_default_variant() + } + if layout_task: + instance_data["task"] = layout_task + layout_creator.create( subset_name=layout_creator.get_subset_name( - self.get_default_variant(), + layout_creator.get_default_variant(), self.create_context.get_current_task_name(), - next( - asset_doc for asset_doc in op_asset_docs - if asset_doc["_id"] == shot["id"] - ), + asset_doc, self.project_name), - instance_data={ - "asset": shot["name"], - }, + instance_data=instance_data, pre_create_data={ "groupLoadedAssets": pre_create_data["groupLoadedAssets"] } From d4d48aacf894ac1e893b97d4c4a2c6b749c201e1 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 6 Oct 2023 15:30:44 +0200 Subject: [PATCH 0169/1233] removing debugging print. --- openpype/hosts/resolve/api/lib.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index 5d80866e6a..0f24a71cff 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -185,7 +185,6 @@ def create_bin(name: str, root: object = None) -> object: def remove_media_pool_item(media_pool_item: object) -> bool: - print(media_pool_item) media_pool = get_current_project().GetMediaPool() return media_pool.DeleteClips([media_pool_item]) From af67b4780f2daf62bafea4f227aea8e541d83dc6 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 6 Oct 2023 16:33:27 +0300 Subject: [PATCH 0170/1233] fabia's comments --- .../plugins/publish/collect_rop_frame_range.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py b/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py index 37db922201..d64ff37eb0 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py +++ b/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py @@ -43,26 +43,24 @@ class CollectRopFrameRange(pyblish.api.InstancePlugin, message = "" if attr_values.get("use_handles"): - message += ( + self.log.info( "Full Frame range with Handles " "{0[frameStartHandle]} - {0[frameEndHandle]}\n" .format(frame_data) ) else: - message += ( + self.log.info( "Use handles is deactivated for this instance, " "start and end handles are set to 0.\n" ) - message += ( + self.log.info( "Frame range {0[frameStart]} - {0[frameEnd]}" .format(frame_data) ) if frame_data.get("byFrameStep", 1.0) != 1.0: - message += "\nFrame steps {0[byFrameStep]}".format(frame_data) - - self.log.info(message) + self.log.info("Frame steps {0[byFrameStep]}".format(frame_data)) instance.data.update(frame_data) @@ -77,8 +75,8 @@ class CollectRopFrameRange(pyblish.api.InstancePlugin, def get_attribute_defs(cls): return [ BoolDef("use_handles", - tooltip="Disable this if you don't want the publisher" - " to ignore start and end handles specified in the" + tooltip="Disable this if you want the publisher to" + " ignore start and end handles specified in the" " asset data for this publish instance", default=True, label="Use asset handles") From 435ff3389f73ce0c0f39f5c1642ce61bd40d7bf6 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 6 Oct 2023 15:39:35 +0200 Subject: [PATCH 0171/1233] :dog: calm the hound --- openpype/hosts/maya/plugins/create/create_multishot_layout.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/create/create_multishot_layout.py b/openpype/hosts/maya/plugins/create/create_multishot_layout.py index d0c4137ac4..90a6b08134 100644 --- a/openpype/hosts/maya/plugins/create/create_multishot_layout.py +++ b/openpype/hosts/maya/plugins/create/create_multishot_layout.py @@ -55,7 +55,7 @@ class CreateMultishotLayout(plugin.MayaCreator): items_with_label = [ dict( label=current_path_parts[p] if current_path_parts[p] != current_folder["name"] else f"{current_path_parts[p]} (current)", # noqa - value="/".join(current_path_parts[:p+1]), + value="/".join(current_path_parts[:p + 1]), ) for p in range(len(current_path_parts)) ] From 0d47f4f57a5f472adbef5ef18c5f80b5c773d250 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 6 Oct 2023 16:41:56 +0300 Subject: [PATCH 0172/1233] remove repair action --- .../plugins/publish/validate_frame_range.py | 27 +------------------ 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_frame_range.py b/openpype/hosts/houdini/plugins/publish/validate_frame_range.py index e1be99dbcf..cf85e59041 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_frame_range.py +++ b/openpype/hosts/houdini/plugins/publish/validate_frame_range.py @@ -1,18 +1,11 @@ # -*- coding: utf-8 -*- import pyblish.api from openpype.pipeline import PublishValidationError -from openpype.pipeline.publish import RepairAction from openpype.hosts.houdini.api.action import SelectInvalidAction import hou -class HotFixAction(RepairAction): - """Set End frame to the minimum valid value.""" - - label = "End frame hotfix" - - class ValidateFrameRange(pyblish.api.InstancePlugin): """Validate Frame Range. @@ -24,7 +17,7 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): order = pyblish.api.ValidatorOrder - 0.1 hosts = ["houdini"] label = "Validate Frame Range" - actions = [HotFixAction, SelectInvalidAction] + actions = [SelectInvalidAction] def process(self, instance): @@ -55,21 +48,3 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): ) ) return [rop_node] - - @classmethod - def repair(cls, instance): - rop_node = hou.node(instance.data["instance_node"]) - - frame_start = int(instance.data["frameStartHandle"]) - frame_end = int( - instance.data["frameStartHandle"] + - instance.data["handleStart"] + - instance.data["handleEnd"] - ) - - if rop_node.parm("f2").rawValue() == "$FEND": - hou.playbar.setFrameRange(frame_start, frame_end) - hou.playbar.setPlaybackRange(frame_start, frame_end) - hou.setFrame(frame_start) - else: - rop_node.parm("f2").set(frame_end) From 28c19f1a9b8ae39fe9746b3323f6118fbb71371c Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 6 Oct 2023 16:51:44 +0300 Subject: [PATCH 0173/1233] resolve hound --- .../hosts/houdini/plugins/publish/collect_rop_frame_range.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py b/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py index d64ff37eb0..f0a473995c 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py +++ b/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py @@ -40,8 +40,6 @@ class CollectRopFrameRange(pyblish.api.InstancePlugin, return # Log artist friendly message about the collected frame range - message = "" - if attr_values.get("use_handles"): self.log.info( "Full Frame range with Handles " @@ -60,7 +58,7 @@ class CollectRopFrameRange(pyblish.api.InstancePlugin, ) if frame_data.get("byFrameStep", 1.0) != 1.0: - self.log.info("Frame steps {0[byFrameStep]}".format(frame_data)) + self.log.info("Frame steps {0[byFrameStep]}".format(frame_data)) instance.data.update(frame_data) From c51ed6409c27b017c357a0ccf91016103b6850d1 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 6 Oct 2023 15:51:47 +0200 Subject: [PATCH 0174/1233] removing also timeline item --- openpype/hosts/resolve/plugins/load/load_clip.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/hosts/resolve/plugins/load/load_clip.py b/openpype/hosts/resolve/plugins/load/load_clip.py index fd181bae41..e9e83ad05d 100644 --- a/openpype/hosts/resolve/plugins/load/load_clip.py +++ b/openpype/hosts/resolve/plugins/load/load_clip.py @@ -168,5 +168,9 @@ class LoadClip(plugin.TimelineItemLoader): namespace = container['namespace'] timeline_item = lib.get_pype_timeline_item_by_name(namespace) take_mp_item = timeline_item.GetMediaPoolItem() + timeline = lib.get_current_timeline() + + if timeline.DeleteClips is not None: + timeline.DeleteClips([timeline_item]) lib.remove_media_pool_item(take_mp_item) From ff7af16fdda73c9614a4b324da91d54ba6caaa35 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 6 Oct 2023 15:20:42 +0100 Subject: [PATCH 0175/1233] Added animation family for alembic loader --- openpype/hosts/blender/plugins/load/load_abc.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/blender/plugins/load/load_abc.py b/openpype/hosts/blender/plugins/load/load_abc.py index 292925c833..a1779b7778 100644 --- a/openpype/hosts/blender/plugins/load/load_abc.py +++ b/openpype/hosts/blender/plugins/load/load_abc.py @@ -26,8 +26,7 @@ class CacheModelLoader(plugin.AssetLoader): Note: At least for now it only supports Alembic files. """ - - families = ["model", "pointcache"] + families = ["model", "pointcache", "animation"] representations = ["abc"] label = "Load Alembic" @@ -61,8 +60,6 @@ class CacheModelLoader(plugin.AssetLoader): relative_path=relative ) - parent = bpy.context.scene.collection - imported = lib.get_selection() # Children must be linked before parents, @@ -90,13 +87,15 @@ class CacheModelLoader(plugin.AssetLoader): material_slot.material.name = f"{group_name}:{name_mat}" if not obj.get(AVALON_PROPERTY): - obj[AVALON_PROPERTY] = dict() + obj[AVALON_PROPERTY] = {} avalon_info = obj[AVALON_PROPERTY] avalon_info.update({"container_name": group_name}) plugin.deselect_all() + collection.objects.link(asset_group) + return objects def process_asset( @@ -131,8 +130,6 @@ class CacheModelLoader(plugin.AssetLoader): objects = self._process(libpath, asset_group, group_name) - bpy.context.scene.collection.objects.link(asset_group) - asset_group[AVALON_PROPERTY] = { "schema": "openpype:container-2.0", "id": AVALON_CONTAINER_ID, From 4b02645618f1b31ec35452ceaa41dbcd5b623df6 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 6 Oct 2023 17:27:35 +0300 Subject: [PATCH 0176/1233] update frame data when rendering current frame --- openpype/hosts/houdini/api/lib.py | 19 +++++++++++-------- .../plugins/publish/collect_arnold_rop.py | 4 ++-- .../plugins/publish/collect_karma_rop.py | 4 ++-- .../plugins/publish/collect_mantra_rop.py | 4 ++-- .../plugins/publish/collect_redshift_rop.py | 4 ++-- .../plugins/publish/collect_vray_rop.py | 4 ++-- .../publish/submit_houdini_render_deadline.py | 4 ++-- 7 files changed, 23 insertions(+), 20 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 711fae7cc4..d713782efe 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -573,22 +573,25 @@ def get_frame_data(node, asset_data=None, log=None): return data if node.evalParm("trange") == 0: + data["frameStartHandle"] = hou.intFrame() + data["frameEndHandle"] = hou.intFrame() + data["byFrameStep"] = 1.0 log.debug( - "Node '{}' has 'Render current frame' set. " - "Time range data ignored.".format(node.path()) - ) - return data + "Node '{}' has 'Render current frame' set. " + "frameStart and frameEnd are set to the " + "current frame".format(node.path()) + ) + else: + data["frameStartHandle"] = node.evalParm("f1") + data["frameEndHandle"] = node.evalParm("f2") + data["byFrameStep"] = node.evalParm("f3") - data["frameStartHandle"] = node.evalParm("f1") data["handleStart"] = asset_data.get("handleStart", 0) data["frameStart"] = data["frameStartHandle"] + data["handleStart"] - data["frameEndHandle"] = node.evalParm("f2") data["handleEnd"] = asset_data.get("handleEnd", 0) data["frameEnd"] = data["frameEndHandle"] - data["handleEnd"] - data["byFrameStep"] = node.evalParm("f3") - return data diff --git a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py index 9933572f4a..28389c3b31 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py @@ -126,8 +126,8 @@ class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin): return path expected_files = [] - start = instance.data.get("frameStartHandle") - end = instance.data.get("frameEndHandle") + start = instance.data["frameStartHandle"] + end = instance.data["frameEndHandle"] for i in range(int(start), (int(end) + 1)): expected_files.append( diff --git a/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py b/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py index 32790dd550..b66dcde13f 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py @@ -95,8 +95,8 @@ class CollectKarmaROPRenderProducts(pyblish.api.InstancePlugin): return path expected_files = [] - start = instance.data.get("frameStartHandle") - end = instance.data.get("frameEndHandle") + start = instance.data["frameStartHandle"] + end = instance.data["frameEndHandle"] for i in range(int(start), (int(end) + 1)): expected_files.append( diff --git a/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py b/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py index daaf87c04c..3b7cf59f32 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py @@ -118,8 +118,8 @@ class CollectMantraROPRenderProducts(pyblish.api.InstancePlugin): return path expected_files = [] - start = instance.data.get("frameStartHandle") - end = instance.data.get("frameEndHandle") + start = instance.data["frameStartHandle"] + end = instance.data["frameEndHandle"] for i in range(int(start), (int(end) + 1)): expected_files.append( diff --git a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py index 5ade67d181..ca171a91f9 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py @@ -132,8 +132,8 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin): return path expected_files = [] - start = instance.data.get("frameStartHandle") - end = instance.data.get("frameEndHandle") + start = instance.data["frameStartHandle"] + end = instance.data["frameEndHandle"] for i in range(int(start), (int(end) + 1)): expected_files.append( diff --git a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py index e5c6ec20c4..b1ff4c1886 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py @@ -115,8 +115,8 @@ class CollectVrayROPRenderProducts(pyblish.api.InstancePlugin): return path expected_files = [] - start = instance.data.get("frameStartHandle") - end = instance.data.get("frameEndHandle") + start = instance.data["frameStartHandle"] + end = instance.data["frameEndHandle"] for i in range(int(start), (int(end) + 1)): expected_files.append( 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 cd71095920..6f885c578a 100644 --- a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py @@ -65,8 +65,8 @@ class HoudiniSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): job_info.BatchName += datetime.now().strftime("%d%m%Y%H%M%S") # Deadline requires integers in frame range - start = instance.data.get("frameStartHandle") - end = instance.data.get("frameEndHandle") + start = instance.data["frameStartHandle"] + end = instance.data["frameEndHandle"] frames = "{start}-{end}x{step}".format( start=int(start), end=int(end), From b4a01faa65ebc7a08528eae75271159ac886c38f Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 6 Oct 2023 17:28:59 +0300 Subject: [PATCH 0177/1233] resolve hound --- openpype/hosts/houdini/api/lib.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index d713782efe..52d5fb4e03 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -577,10 +577,10 @@ def get_frame_data(node, asset_data=None, log=None): data["frameEndHandle"] = hou.intFrame() data["byFrameStep"] = 1.0 log.debug( - "Node '{}' has 'Render current frame' set. " - "frameStart and frameEnd are set to the " - "current frame".format(node.path()) - ) + "Node '{}' has 'Render current frame' set. " + "frameStart and frameEnd are set to the " + "current frame".format(node.path()) + ) else: data["frameStartHandle"] = node.evalParm("f1") data["frameEndHandle"] = node.evalParm("f2") From 787a0d1847e7aa5e78b5bc51d13cf68f31dd1379 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 6 Oct 2023 15:47:34 +0100 Subject: [PATCH 0178/1233] Fix issues with the collections where the objects are linked to --- .../hosts/blender/plugins/load/load_abc.py | 38 ++++++++++++++++--- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/blender/plugins/load/load_abc.py b/openpype/hosts/blender/plugins/load/load_abc.py index a1779b7778..1442e65f68 100644 --- a/openpype/hosts/blender/plugins/load/load_abc.py +++ b/openpype/hosts/blender/plugins/load/load_abc.py @@ -52,8 +52,6 @@ class CacheModelLoader(plugin.AssetLoader): def _process(self, libpath, asset_group, group_name): plugin.deselect_all() - collection = bpy.context.view_layer.active_layer_collection.collection - relative = bpy.context.preferences.filepaths.use_relative_paths bpy.ops.wm.alembic_import( filepath=libpath, @@ -76,6 +74,10 @@ class CacheModelLoader(plugin.AssetLoader): objects.reverse() for obj in objects: + # Unlink the object from all collections + collections = obj.users_collection + for collection in collections: + collection.objects.unlink(obj) name = obj.name obj.name = f"{group_name}:{name}" if obj.type != 'EMPTY': @@ -94,8 +96,6 @@ class CacheModelLoader(plugin.AssetLoader): plugin.deselect_all() - collection.objects.link(asset_group) - return objects def process_asset( @@ -130,6 +130,21 @@ class CacheModelLoader(plugin.AssetLoader): objects = self._process(libpath, asset_group, group_name) + # Link the asset group to the active collection + collection = bpy.context.view_layer.active_layer_collection.collection + collection.objects.link(asset_group) + + # Link the imported objects to any collection where the asset group is + # linked to, except the AVALON_CONTAINERS collection + group_collections = [ + collection + for collection in asset_group.users_collection + if collection != avalon_containers] + + for obj in objects: + for collection in group_collections: + collection.objects.link(obj) + asset_group[AVALON_PROPERTY] = { "schema": "openpype:container-2.0", "id": AVALON_CONTAINER_ID, @@ -204,7 +219,20 @@ class CacheModelLoader(plugin.AssetLoader): mat = asset_group.matrix_basis.copy() self._remove(asset_group) - self._process(str(libpath), asset_group, object_name) + objects = self._process(str(libpath), asset_group, object_name) + + # Link the imported objects to any collection where the asset group is + # linked to, except the AVALON_CONTAINERS collection + avalon_containers = bpy.data.collections.get(AVALON_CONTAINERS) + group_collections = [ + collection + for collection in asset_group.users_collection + if collection != avalon_containers] + + for obj in objects: + for collection in group_collections: + collection.objects.link(obj) + asset_group.matrix_basis = mat metadata["libpath"] = str(libpath) From dd46d48ffc1fcec9116d27b4aa7ce437db5e3ac2 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 6 Oct 2023 22:50:16 +0800 Subject: [PATCH 0179/1233] upversion for maya server addon & fix on repair action in validate resolution --- .../plugins/publish/validate_resolution.py | 46 +++++++++++++------ server_addon/maya/server/version.py | 2 +- 2 files changed, 32 insertions(+), 16 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_resolution.py b/openpype/hosts/maya/plugins/publish/validate_resolution.py index 66962afce5..092860164f 100644 --- a/openpype/hosts/maya/plugins/publish/validate_resolution.py +++ b/openpype/hosts/maya/plugins/publish/validate_resolution.py @@ -13,7 +13,7 @@ class ValidateResolution(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validate the render resolution setting aligned with DB""" - order = pyblish.api.ValidatorOrder - 0.01 + order = pyblish.api.ValidatorOrder families = ["renderlayer"] hosts = ["maya"] label = "Validate Resolution" @@ -26,14 +26,17 @@ class ValidateResolution(pyblish.api.InstancePlugin, invalid = self.get_invalid_resolution(instance) if invalid: raise PublishValidationError( - "issues occurred", description=( + "Render resolution is invalid. See log for details.", + description=( "Wrong render resolution setting. " "Please use repair button to fix it.\n" "If current renderer is V-Ray, " - "make sure vraySettings node has been created")) - - def get_invalid_resolution(self, instance): - width, height, pixelAspect = self.get_db_resolution(instance) + "make sure vraySettings node has been created" + ) + ) + @classmethod + def get_invalid_resolution(cls, instance): + width, height, pixelAspect = cls.get_db_resolution(instance) current_renderer = instance.data["renderer"] layer = instance.data["renderlayer"] invalid = False @@ -48,11 +51,11 @@ class ValidateResolution(pyblish.api.InstancePlugin, "{}.pixelAspect".format(vray_node), layer=layer ) else: - self.log.error( + cls.log.error( "Can't detect VRay resolution because there is no node " "named: `{}`".format(vray_node) ) - invalid = True + return True else: current_width = lib.get_attr_in_layer( "defaultResolution.width", layer=layer) @@ -62,7 +65,7 @@ class ValidateResolution(pyblish.api.InstancePlugin, "defaultResolution.pixelAspect", layer=layer ) if current_width != width or current_height != height: - self.log.error( + cls.log.error( "Render resolution {}x{} does not match " "asset resolution {}x{}".format( current_width, current_height, @@ -70,7 +73,7 @@ class ValidateResolution(pyblish.api.InstancePlugin, )) invalid = True if current_pixelAspect != pixelAspect: - self.log.error( + cls.log.error( "Render pixel aspect {} does not match " "asset pixel aspect {}".format( current_pixelAspect, pixelAspect @@ -78,12 +81,15 @@ class ValidateResolution(pyblish.api.InstancePlugin, invalid = True return invalid - def get_db_resolution(self, instance): + @classmethod + def get_db_resolution(cls, instance): 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 and "pixelAspect" in data + if ( + "resolutionWidth" in data and + "resolutionHeight" in data and + "pixelAspect" in data ): width = data["resolutionWidth"] height = data["resolutionHeight"] @@ -95,6 +101,16 @@ class ValidateResolution(pyblish.api.InstancePlugin, @classmethod def repair(cls, instance): - layer = instance.data["renderlayer"] - with lib.renderlayer(layer): + # Usually without renderlayer overrides the renderlayers + # all share the same resolution value - so fixing the first + # will have fixed all the others too. It's much faster to + # check whether it's invalid first instead of switching + # into all layers individually + if not cls.get_invalid_resolution(instance): + cls.log.debug( + "Nothing to repair on instance: {}".format(instance) + ) + return + layer_node = instance.data['setMembers'] + with lib.renderlayer(layer_node): reset_scene_resolution() diff --git a/server_addon/maya/server/version.py b/server_addon/maya/server/version.py index de699158fd..90ce344d3e 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.4" +__version__ = "0.1.5" From 1d531b1ad5c94e4843c0d245fd61fa8372473ee1 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 6 Oct 2023 22:54:16 +0800 Subject: [PATCH 0180/1233] hound --- openpype/hosts/maya/plugins/publish/validate_resolution.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/maya/plugins/publish/validate_resolution.py b/openpype/hosts/maya/plugins/publish/validate_resolution.py index 092860164f..b214f87906 100644 --- a/openpype/hosts/maya/plugins/publish/validate_resolution.py +++ b/openpype/hosts/maya/plugins/publish/validate_resolution.py @@ -34,6 +34,7 @@ class ValidateResolution(pyblish.api.InstancePlugin, "make sure vraySettings node has been created" ) ) + @classmethod def get_invalid_resolution(cls, instance): width, height, pixelAspect = cls.get_db_resolution(instance) From f94cd729f6d88565f593979921748920e48e05ec Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 6 Oct 2023 17:49:20 +0200 Subject: [PATCH 0181/1233] Refactor to new style host --- openpype/hosts/resolve/api/menu.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/resolve/api/menu.py b/openpype/hosts/resolve/api/menu.py index ff8d5d4c6a..160cc44fdb 100644 --- a/openpype/hosts/resolve/api/menu.py +++ b/openpype/hosts/resolve/api/menu.py @@ -116,12 +116,12 @@ class OpenPypeMenu(QtWidgets.QWidget): def on_save_current_clicked(self): host = registered_host() - current_file = host.current_file() + current_file = host.get_current_workfile() if not current_file: return print(f"Saving current file to: {current_file}") - host.save_file(current_file) + host.save_workfile(current_file) def on_workfile_clicked(self): print("Clicked Workfile") From e03fe24a07126403b384f5cbae18653d55111356 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Fri, 6 Oct 2023 18:18:28 +0200 Subject: [PATCH 0182/1233] Update openpype/hosts/resolve/plugins/load/load_clip.py Co-authored-by: Roy Nieterau --- openpype/hosts/resolve/plugins/load/load_clip.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/resolve/plugins/load/load_clip.py b/openpype/hosts/resolve/plugins/load/load_clip.py index e9e83ad05d..799b85ea7f 100644 --- a/openpype/hosts/resolve/plugins/load/load_clip.py +++ b/openpype/hosts/resolve/plugins/load/load_clip.py @@ -170,6 +170,9 @@ class LoadClip(plugin.TimelineItemLoader): take_mp_item = timeline_item.GetMediaPoolItem() timeline = lib.get_current_timeline() + # DeleteClips function was added in Resolve 18.5+ + # by checking None we can detect whether the + # function exists in Resolve if timeline.DeleteClips is not None: timeline.DeleteClips([timeline_item]) From 931d847f891af55c2f8d10ed29cca8c853b96d8a Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 6 Oct 2023 18:32:29 +0200 Subject: [PATCH 0183/1233] :recycle: fix readability of the code --- .../plugins/create/create_multishot_layout.py | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_multishot_layout.py b/openpype/hosts/maya/plugins/create/create_multishot_layout.py index 90a6b08134..6ff40851e3 100644 --- a/openpype/hosts/maya/plugins/create/create_multishot_layout.py +++ b/openpype/hosts/maya/plugins/create/create_multishot_layout.py @@ -52,14 +52,31 @@ class CreateMultishotLayout(plugin.MayaCreator): current_path_parts = current_folder["path"].split("/") - items_with_label = [ - dict( - label=current_path_parts[p] if current_path_parts[p] != current_folder["name"] else f"{current_path_parts[p]} (current)", # noqa - value="/".join(current_path_parts[:p + 1]), - ) - for p in range(len(current_path_parts)) - ] + items_with_label = [] + # populate the list with parents of the current folder + # this will create menu items like: + # [ + # { + # "value": "", + # "label": "project (shots directly under the project)" + # }, { + # "value": "shots/shot_01", "label": "shot_01 (current)" + # }, { + # "value": "shots", "label": "shots" + # } + # ] + # go through the current folder path and add each part to the list, + # but mark the current folder. + for part_idx in range(len(current_path_parts)): + label = current_path_parts[part_idx] + if current_path_parts[part_idx] == current_folder["name"]: + label = f"{current_path_parts[part_idx]} (current)" + items_with_label.append( + dict(label=label, + value="/".join(current_path_parts[:part_idx + 1])) + ) + # add the project as the first item items_with_label.insert( 0, dict(label=f"{self.project_name} " "(shots directly under the project)", value="")) From 47425e3aa460a012a4fc6265ff5b4fdfcc345fc5 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 6 Oct 2023 22:54:00 +0300 Subject: [PATCH 0184/1233] expose use asset handles to houdini settings --- .../plugins/publish/collect_rop_frame_range.py | 7 ++++--- .../defaults/project_settings/houdini.json | 3 +++ .../schemas/schema_houdini_publish.json | 17 +++++++++++++++++ .../houdini/server/settings/publish_plugins.py | 17 +++++++++++++++++ 4 files changed, 41 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py b/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py index f0a473995c..becadf2833 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py +++ b/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py @@ -15,6 +15,7 @@ class CollectRopFrameRange(pyblish.api.InstancePlugin, hosts = ["houdini"] order = pyblish.api.CollectorOrder label = "Collect RopNode Frame Range" + use_asset_handles = True def process(self, instance): @@ -40,7 +41,7 @@ class CollectRopFrameRange(pyblish.api.InstancePlugin, return # Log artist friendly message about the collected frame range - if attr_values.get("use_handles"): + if attr_values.get("use_asset_handles"): self.log.info( "Full Frame range with Handles " "{0[frameStartHandle]} - {0[frameEndHandle]}\n" @@ -72,10 +73,10 @@ class CollectRopFrameRange(pyblish.api.InstancePlugin, @classmethod def get_attribute_defs(cls): return [ - BoolDef("use_handles", + BoolDef("use_asset_handles", tooltip="Disable this if you want the publisher to" " ignore start and end handles specified in the" " asset data for this publish instance", - default=True, + default=cls.use_asset_handles, label="Use asset handles") ] diff --git a/openpype/settings/defaults/project_settings/houdini.json b/openpype/settings/defaults/project_settings/houdini.json index 4f57ee52c6..14fc0c1655 100644 --- a/openpype/settings/defaults/project_settings/houdini.json +++ b/openpype/settings/defaults/project_settings/houdini.json @@ -106,6 +106,9 @@ } }, "publish": { + "CollectRopFrameRange": { + "use_asset_handles": true + }, "ValidateWorkfilePaths": { "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 d5f70b0312..d030b3bdd3 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 @@ -4,6 +4,23 @@ "key": "publish", "label": "Publish plugins", "children": [ + { + "type": "dict", + "collapsible": true, + "key": "CollectRopFrameRange", + "label": "Collect Rop Frame Range", + "children": [ + { + "type": "label", + "label": "Disable this if you want the publisher to ignore start and end handles specified in the asset data for publish instances" + }, + { + "type": "boolean", + "key": "use_asset_handles", + "label": "Use asset handles" + } + ] + }, { "type": "dict", "collapsible": true, diff --git a/server_addon/houdini/server/settings/publish_plugins.py b/server_addon/houdini/server/settings/publish_plugins.py index 58240b0205..b975a9edfd 100644 --- a/server_addon/houdini/server/settings/publish_plugins.py +++ b/server_addon/houdini/server/settings/publish_plugins.py @@ -151,6 +151,16 @@ class ValidateWorkfilePathsModel(BaseSettingsModel): ) +class CollectRopFrameRangeModel(BaseSettingsModel): + """Collect Frame Range + + Disable this if you want the publisher to + ignore start and end handles specified in the + asset data for publish instances + """ + use_asset_handles: bool = Field(title="Use asset handles") + + class BasicValidateModel(BaseSettingsModel): enabled: bool = Field(title="Enabled") optional: bool = Field(title="Optional") @@ -158,6 +168,10 @@ class BasicValidateModel(BaseSettingsModel): class PublishPluginsModel(BaseSettingsModel): + CollectRopFrameRange:CollectRopFrameRangeModel = Field( + default_factory=CollectRopFrameRangeModel, + title="Collect Rop Frame Range." + ) ValidateWorkfilePaths: ValidateWorkfilePathsModel = Field( default_factory=ValidateWorkfilePathsModel, title="Validate workfile paths settings.") @@ -179,6 +193,9 @@ class PublishPluginsModel(BaseSettingsModel): DEFAULT_HOUDINI_PUBLISH_SETTINGS = { + "CollectRopFrameRange": { + "use_asset_handles": True + }, "ValidateWorkfilePaths": { "enabled": True, "optional": True, From ffb61e27efb222ec9f93f4c42b8491e7249fff3e Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 6 Oct 2023 23:20:59 +0300 Subject: [PATCH 0185/1233] fix a bug --- .../hosts/houdini/plugins/publish/collect_rop_frame_range.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py b/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py index becadf2833..c9bc1766cb 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py +++ b/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py @@ -41,7 +41,7 @@ class CollectRopFrameRange(pyblish.api.InstancePlugin, return # Log artist friendly message about the collected frame range - if attr_values.get("use_asset_handles"): + if attr_values.get("use_handles"): self.log.info( "Full Frame range with Handles " "{0[frameStartHandle]} - {0[frameEndHandle]}\n" @@ -73,7 +73,7 @@ class CollectRopFrameRange(pyblish.api.InstancePlugin, @classmethod def get_attribute_defs(cls): return [ - BoolDef("use_asset_handles", + BoolDef("use_handles", tooltip="Disable this if you want the publisher to" " ignore start and end handles specified in the" " asset data for this publish instance", From 5f952c89d3933a61a335ae283f5ebe9ece398c09 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 6 Oct 2023 23:28:58 +0300 Subject: [PATCH 0186/1233] resolve hound and bump addon version --- server_addon/houdini/server/settings/publish_plugins.py | 2 +- server_addon/houdini/server/version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server_addon/houdini/server/settings/publish_plugins.py b/server_addon/houdini/server/settings/publish_plugins.py index b975a9edfd..76030bdeea 100644 --- a/server_addon/houdini/server/settings/publish_plugins.py +++ b/server_addon/houdini/server/settings/publish_plugins.py @@ -168,7 +168,7 @@ class BasicValidateModel(BaseSettingsModel): class PublishPluginsModel(BaseSettingsModel): - CollectRopFrameRange:CollectRopFrameRangeModel = Field( + CollectRopFrameRange: CollectRopFrameRangeModel = Field( default_factory=CollectRopFrameRangeModel, title="Collect Rop Frame Range." ) diff --git a/server_addon/houdini/server/version.py b/server_addon/houdini/server/version.py index bbab0242f6..1276d0254f 100644 --- a/server_addon/houdini/server/version.py +++ b/server_addon/houdini/server/version.py @@ -1 +1 @@ -__version__ = "0.1.4" +__version__ = "0.1.5" From 2932debbaf959df7f54856c88c0757ac14d5aa78 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 7 Oct 2023 19:45:23 +0200 Subject: [PATCH 0187/1233] Cleanup + fix updating/remove logic - Use container `_timeline_item` to ensure we act on the expected timeline item - otherwise `lib.get_pype_timeline_item_by_name` can take the wrong one if the same subset is loaded more than once which made update/remove actually pick an unexpected timeline item. - On update, remove media pool item if previous version now has no usage - On remove, only remove media pool item if it has no usage - Don't duplicate logic to define version data to put in tag data, now uses a `get_tag_data` method - Don't create a `fake context` but use the `get_representation_context` to get the context on load to ensure whatever uses it has the correct context. --- .../hosts/resolve/plugins/load/load_clip.py | 106 +++++++----------- 1 file changed, 42 insertions(+), 64 deletions(-) diff --git a/openpype/hosts/resolve/plugins/load/load_clip.py b/openpype/hosts/resolve/plugins/load/load_clip.py index e9e83ad05d..8c702a4dfc 100644 --- a/openpype/hosts/resolve/plugins/load/load_clip.py +++ b/openpype/hosts/resolve/plugins/load/load_clip.py @@ -1,12 +1,7 @@ -from copy import deepcopy - -from openpype.client import ( - get_version_by_id, - get_last_version_by_subset_id, -) -# from openpype.hosts import resolve +from openpype.client import get_last_version_by_subset_id from openpype.pipeline import ( get_representation_path, + get_representation_context, get_current_project_name, ) from openpype.hosts.resolve.api import lib, plugin @@ -53,37 +48,11 @@ class LoadClip(plugin.TimelineItemLoader): timeline_item = plugin.ClipLoader( self, context, path, **options).load() namespace = namespace or timeline_item.GetName() - version = context['version'] - version_data = version.get("data", {}) - version_name = version.get("name", None) - colorspace = version_data.get("colorspace", None) - object_name = "{}_{}".format(name, namespace) - - # add additional metadata from the version to imprint Avalon knob - add_keys = [ - "frameStart", "frameEnd", "source", "author", - "fps", "handleStart", "handleEnd" - ] - - # move all version data keys to tag data - data_imprint = {} - for key in add_keys: - data_imprint.update({ - key: version_data.get(key, str(None)) - }) - - # add variables related to version context - data_imprint.update({ - "version": version_name, - "colorspace": colorspace, - "objectName": object_name - }) # update color of clip regarding the version order - self.set_item_color(timeline_item, version) - - self.log.info("Loader done: `{}`".format(name)) + self.set_item_color(timeline_item, version=context["version"]) + data_imprint = self.get_tag_data(context, name, namespace) return containerise( timeline_item, name, namespace, context, @@ -97,53 +66,60 @@ class LoadClip(plugin.TimelineItemLoader): """ Updating previously loaded clips """ - # load clip to timeline and get main variables - context = deepcopy(representation["context"]) - context.update({"representation": representation}) + context = get_representation_context(representation) name = container['name'] namespace = container['namespace'] - timeline_item = lib.get_pype_timeline_item_by_name(namespace) + timeline_item = container["_timeline_item"] - project_name = get_current_project_name() - version = get_version_by_id(project_name, representation["parent"]) + media_pool_item = timeline_item.GetMediaPoolItem() + + path = get_representation_path(representation) + loader = plugin.ClipLoader(self, context, path) + timeline_item = loader.update(timeline_item) + + # update color of clip regarding the version order + self.set_item_color(timeline_item, version=context["version"]) + + # if original media pool item has no remaining usages left + # remove it from the media pool + if int(media_pool_item.GetClipProperty("Usage")) == 0: + lib.remove_media_pool_item(media_pool_item) + + data_imprint = self.get_tag_data(context, name, namespace) + return update_container(timeline_item, data_imprint) + + def get_tag_data(self, context, name, namespace): + """Return data to be imprinted on the timeline item marker""" + + representation = context["representation"] + version = context['version'] version_data = version.get("data", {}) version_name = version.get("name", None) colorspace = version_data.get("colorspace", None) object_name = "{}_{}".format(name, namespace) - path = get_representation_path(representation) - - context["version"] = {"data": version_data} - loader = plugin.ClipLoader(self, context, path) - timeline_item = loader.update(timeline_item) # add additional metadata from the version to imprint Avalon knob - add_keys = [ + # move all version data keys to tag data + add_version_data_keys = [ "frameStart", "frameEnd", "source", "author", "fps", "handleStart", "handleEnd" ] - - # move all version data keys to tag data - data_imprint = {} - for key in add_keys: - data_imprint.update({ - key: version_data.get(key, str(None)) - }) + data = { + key: version_data.get(key, "None") for key in add_version_data_keys + } # add variables related to version context - data_imprint.update({ + data.update({ "representation": str(representation["_id"]), "version": version_name, "colorspace": colorspace, "objectName": object_name }) - - # update color of clip regarding the version order - self.set_item_color(timeline_item, version) - - return update_container(timeline_item, data_imprint) + return data @classmethod def set_item_color(cls, timeline_item, version): + """Color timeline item based on whether it is outdated or latest""" # define version name version_name = version.get("name", None) # get all versions in list @@ -165,12 +141,14 @@ class LoadClip(plugin.TimelineItemLoader): timeline_item.SetClipColor(cls.clip_color) def remove(self, container): - namespace = container['namespace'] - timeline_item = lib.get_pype_timeline_item_by_name(namespace) - take_mp_item = timeline_item.GetMediaPoolItem() + timeline_item = container["_timeline_item"] + media_pool_item = timeline_item.GetMediaPoolItem() timeline = lib.get_current_timeline() if timeline.DeleteClips is not None: timeline.DeleteClips([timeline_item]) - lib.remove_media_pool_item(take_mp_item) + # if media pool item has no remaining usages left + # remove it from the media pool + if int(media_pool_item.GetClipProperty("Usage")) == 0: + lib.remove_media_pool_item(media_pool_item) From bb74f9b3ba7a9dea03dcd8451d7ec9d12ffbe92b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 7 Oct 2023 19:45:49 +0200 Subject: [PATCH 0188/1233] Cosmetics --- openpype/hosts/resolve/api/pipeline.py | 3 +-- openpype/hosts/resolve/api/plugin.py | 3 --- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/openpype/hosts/resolve/api/pipeline.py b/openpype/hosts/resolve/api/pipeline.py index 28be387ce9..93dec300fb 100644 --- a/openpype/hosts/resolve/api/pipeline.py +++ b/openpype/hosts/resolve/api/pipeline.py @@ -127,8 +127,7 @@ def containerise(timeline_item, }) if data: - for k, v in data.items(): - data_imprint.update({k: v}) + data_imprint.update(data) lib.set_timeline_item_pype_tag(timeline_item, data_imprint) diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py index b4c03d6809..85245a5d12 100644 --- a/openpype/hosts/resolve/api/plugin.py +++ b/openpype/hosts/resolve/api/plugin.py @@ -338,8 +338,6 @@ class ClipLoader: else: self.active_timeline = lib.get_current_timeline() - - def _populate_data(self): """ Gets context and convert it to self.data data structure: @@ -429,7 +427,6 @@ class ClipLoader: ) _clip_property = media_pool_item.GetClipProperty - source_in = int(_clip_property("Start")) source_out = int(_clip_property("End")) From 708aef05375ae41109e260797ff23fe9f9aa4097 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 7 Oct 2023 20:02:10 +0200 Subject: [PATCH 0189/1233] Code cosmetics --- openpype/hosts/resolve/api/lib.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index 3139c32093..942caca72a 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -580,11 +580,11 @@ def set_pype_marker(timeline_item, tag_data): def get_pype_marker(timeline_item): timeline_item_markers = timeline_item.GetMarkers() - for marker_frame in timeline_item_markers: - note = timeline_item_markers[marker_frame]["note"] - color = timeline_item_markers[marker_frame]["color"] - name = timeline_item_markers[marker_frame]["name"] + for marker_frame, marker in timeline_item_markers.items(): + color = marker["color"] + name = marker["name"] if name == self.pype_marker_name and color == self.pype_marker_color: + note = marker["note"] self.temp_marker_frame = marker_frame return json.loads(note) From 26bbb702df9eaa9c86117e6dbe7654268f4e590a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 7 Oct 2023 20:04:21 +0200 Subject: [PATCH 0190/1233] Implement legacy logic where we remove the pype tag in older versions of Resolve - Unfortunately due to API limitations cannot remove the TimelineItem from the Timeline in old versions of Resolve --- openpype/hosts/resolve/plugins/load/load_clip.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/openpype/hosts/resolve/plugins/load/load_clip.py b/openpype/hosts/resolve/plugins/load/load_clip.py index a17db376be..5e81441332 100644 --- a/openpype/hosts/resolve/plugins/load/load_clip.py +++ b/openpype/hosts/resolve/plugins/load/load_clip.py @@ -150,6 +150,15 @@ class LoadClip(plugin.TimelineItemLoader): # function exists in Resolve if timeline.DeleteClips is not None: timeline.DeleteClips([timeline_item]) + else: + # Resolve versions older than 18.5 can't delete clips via API + # so all we can do is just remove the pype marker to 'untag' it + if lib.get_pype_marker(timeline_item): + # Note: We must call `get_pype_marker` because + # `delete_pype_marker` uses a global variable set by + # `get_pype_marker` to delete the right marker + # TODO: Improve code to avoid the global `temp_marker_frame` + lib.delete_pype_marker(timeline_item) # if media pool item has no remaining usages left # remove it from the media pool From 823250f427a21895e4a09c36a333b8921cbd3d29 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 7 Oct 2023 20:31:51 +0200 Subject: [PATCH 0191/1233] Remove publish gui filter setting from TV Paint --- .../schema_project_tvpaint.json | 4 --- server_addon/tvpaint/server/settings/main.py | 27 +------------------ 2 files changed, 1 insertion(+), 30 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json index e9255f426e..5b2647bc6d 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json @@ -436,10 +436,6 @@ "workfile_builder/builder_on_start", "workfile_builder/profiles" ] - }, - { - "type": "schema", - "name": "schema_publish_gui_filter" } ] } diff --git a/server_addon/tvpaint/server/settings/main.py b/server_addon/tvpaint/server/settings/main.py index 4cd6ac4b1a..102acfaf3d 100644 --- a/server_addon/tvpaint/server/settings/main.py +++ b/server_addon/tvpaint/server/settings/main.py @@ -1,4 +1,4 @@ -from pydantic import Field, validator +from pydantic import Field from ayon_server.settings import ( BaseSettingsModel, ensure_unique_names, @@ -14,23 +14,6 @@ from .publish_plugins import ( ) -class PublishGUIFilterItemModel(BaseSettingsModel): - _layout = "compact" - name: str = Field(title="Name") - value: bool = Field(True, title="Active") - - -class PublishGUIFiltersModel(BaseSettingsModel): - _layout = "compact" - name: str = Field(title="Name") - value: list[PublishGUIFilterItemModel] = Field(default_factory=list) - - @validator("value") - def validate_unique_outputs(cls, value): - ensure_unique_names(value) - return value - - class TvpaintSettings(BaseSettingsModel): imageio: TVPaintImageIOModel = Field( default_factory=TVPaintImageIOModel, @@ -52,14 +35,6 @@ class TvpaintSettings(BaseSettingsModel): default_factory=WorkfileBuilderPlugin, title="Workfile Builder" ) - filters: list[PublishGUIFiltersModel] = Field( - default_factory=list, - title="Publish GUI Filters") - - @validator("filters") - def validate_unique_outputs(cls, value): - ensure_unique_names(value) - return value DEFAULT_VALUES = { From e4535c32504c5e44a92ca8140386e97816e7344c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 7 Oct 2023 20:32:28 +0200 Subject: [PATCH 0192/1233] Remove remaining traces of `filters` in Maya and Nuke server addons --- server_addon/maya/server/settings/main.py | 2 +- server_addon/nuke/server/settings/main.py | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/server_addon/maya/server/settings/main.py b/server_addon/maya/server/settings/main.py index f204401061..be123e5c38 100644 --- a/server_addon/maya/server/settings/main.py +++ b/server_addon/maya/server/settings/main.py @@ -60,7 +60,7 @@ class MayaSettings(BaseSettingsModel): default_factory=TemplatedProfilesModel, title="Templated Workfile Build Settings") - @validator("filters", "ext_mapping") + @validator("ext_mapping") def validate_unique_outputs(cls, value): ensure_unique_names(value) return value diff --git a/server_addon/nuke/server/settings/main.py b/server_addon/nuke/server/settings/main.py index f1e9b6774e..b6729e7c2f 100644 --- a/server_addon/nuke/server/settings/main.py +++ b/server_addon/nuke/server/settings/main.py @@ -97,12 +97,6 @@ class NukeSettings(BaseSettingsModel): default_factory=TemplatedWorkfileBuildModel ) - @validator("filters") - def ensure_unique_names(cls, value): - """Ensure name fields within the lists have unique names.""" - ensure_unique_names(value) - return value - DEFAULT_VALUES = { "general": DEFAULT_GENERAL_SETTINGS, From 1baec5503af8d814718dc785232e5308fbdb8ead Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 7 Oct 2023 20:33:43 +0200 Subject: [PATCH 0193/1233] Remove remaining traces of `filters` in Nuke AYON settings conversion --- openpype/settings/ayon_settings.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index d54d71e851..7e4ebcbcc0 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -694,16 +694,6 @@ def _convert_nuke_project_settings(ayon_settings, output): dirmap["paths"][dst_key] = dirmap["paths"].pop(src_key) ayon_nuke["nuke-dirmap"] = dirmap - # --- Filters --- - new_gui_filters = {} - for item in ayon_nuke.pop("filters"): - subvalue = {} - key = item["name"] - for subitem in item["value"]: - subvalue[subitem["name"]] = subitem["value"] - new_gui_filters[key] = subvalue - ayon_nuke["filters"] = new_gui_filters - # --- Load --- ayon_load = ayon_nuke["load"] ayon_load["LoadClip"]["_representations"] = ( From f3f3a4aadc5663ef457f88a897f65dfa906f1e99 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 7 Oct 2023 20:34:01 +0200 Subject: [PATCH 0194/1233] Remove filters from tvpaint.json --- openpype/settings/defaults/project_settings/tvpaint.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/settings/defaults/project_settings/tvpaint.json b/openpype/settings/defaults/project_settings/tvpaint.json index fdbd6d5d0f..d03b8b7227 100644 --- a/openpype/settings/defaults/project_settings/tvpaint.json +++ b/openpype/settings/defaults/project_settings/tvpaint.json @@ -107,6 +107,5 @@ "workfile_builder": { "create_first_version": false, "custom_templates": [] - }, - "filters": {} + } } From 864f043f071f14f6101c920688bf9e2c7e6a9e54 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 7 Oct 2023 20:35:08 +0200 Subject: [PATCH 0195/1233] Bump TVPaint Server Addon patch version --- server_addon/tvpaint/server/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/tvpaint/server/version.py b/server_addon/tvpaint/server/version.py index 3dc1f76bc6..485f44ac21 100644 --- a/server_addon/tvpaint/server/version.py +++ b/server_addon/tvpaint/server/version.py @@ -1 +1 @@ -__version__ = "0.1.0" +__version__ = "0.1.1" From 38958b2e2d8e4b78f2e774c31bc8f6b9e1602651 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 7 Oct 2023 20:40:28 +0200 Subject: [PATCH 0196/1233] Bump Nuke Server Addon patch version --- server_addon/nuke/server/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/nuke/server/version.py b/server_addon/nuke/server/version.py index ae7362549b..bbab0242f6 100644 --- a/server_addon/nuke/server/version.py +++ b/server_addon/nuke/server/version.py @@ -1 +1 @@ -__version__ = "0.1.3" +__version__ = "0.1.4" From 847f73deadd24f35b9f7567ea844cbc23db192fb Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Mon, 9 Oct 2023 10:52:13 +0300 Subject: [PATCH 0197/1233] ignore asset handles when rendering the current frame --- openpype/hosts/houdini/api/lib.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 56444afc12..6d57d959e6 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -580,8 +580,11 @@ def get_frame_data(node, asset_data=None, log=None): data["frameStartHandle"] = hou.intFrame() data["frameEndHandle"] = hou.intFrame() data["byFrameStep"] = 1.0 - log.debug( - "Node '{}' has 'Render current frame' set. " + data["handleStart"] = 0 + data["handleEnd"] = 0 + log.info( + "Node '{}' has 'Render current frame' set. \n" + "Asset Handles are ignored. \n" "frameStart and frameEnd are set to the " "current frame".format(node.path()) ) @@ -589,11 +592,10 @@ def get_frame_data(node, asset_data=None, log=None): data["frameStartHandle"] = node.evalParm("f1") data["frameEndHandle"] = node.evalParm("f2") data["byFrameStep"] = node.evalParm("f3") + data["handleStart"] = asset_data.get("handleStart", 0) + data["handleEnd"] = asset_data.get("handleEnd", 0) - data["handleStart"] = asset_data.get("handleStart", 0) data["frameStart"] = data["frameStartHandle"] + data["handleStart"] - - data["handleEnd"] = asset_data.get("handleEnd", 0) data["frameEnd"] = data["frameEndHandle"] - data["handleEnd"] return data From f863e3f0b41d244323c7c61688ecbf2f2a996e42 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 9 Oct 2023 10:43:15 +0200 Subject: [PATCH 0198/1233] change version regex to support blender 4 (#5723) --- openpype/hosts/blender/hooks/pre_pyside_install.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/blender/hooks/pre_pyside_install.py b/openpype/hosts/blender/hooks/pre_pyside_install.py index 777e383215..2aa3a5e49a 100644 --- a/openpype/hosts/blender/hooks/pre_pyside_install.py +++ b/openpype/hosts/blender/hooks/pre_pyside_install.py @@ -31,7 +31,7 @@ class InstallPySideToBlender(PreLaunchHook): def inner_execute(self): # Get blender's python directory - version_regex = re.compile(r"^[2-3]\.[0-9]+$") + version_regex = re.compile(r"^[2-4]\.[0-9]+$") platform = system().lower() executable = self.launch_context.executable.executable_path From b711758e1f504030afe131bd517446a20d01b0fc Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 9 Oct 2023 10:14:03 +0100 Subject: [PATCH 0199/1233] Code improvements --- .../hosts/blender/plugins/load/load_abc.py | 47 ++++++++----------- 1 file changed, 20 insertions(+), 27 deletions(-) diff --git a/openpype/hosts/blender/plugins/load/load_abc.py b/openpype/hosts/blender/plugins/load/load_abc.py index 1442e65f68..9b3d940536 100644 --- a/openpype/hosts/blender/plugins/load/load_abc.py +++ b/openpype/hosts/blender/plugins/load/load_abc.py @@ -98,6 +98,18 @@ class CacheModelLoader(plugin.AssetLoader): return objects + def _link_objects(self, objects, collection, containers, asset_group): + # Link the imported objects to any collection where the asset group is + # linked to, except the AVALON_CONTAINERS collection + group_collections = [ + collection + for collection in asset_group.users_collection + if collection != containers] + + for obj in objects: + for collection in group_collections: + collection.objects.link(obj) + def process_asset( self, context: dict, name: str, namespace: Optional[str] = None, options: Optional[Dict] = None @@ -119,14 +131,13 @@ class CacheModelLoader(plugin.AssetLoader): group_name = plugin.asset_name(asset, subset, unique_number) namespace = namespace or f"{asset}_{unique_number}" - avalon_containers = bpy.data.collections.get(AVALON_CONTAINERS) - if not avalon_containers: - avalon_containers = bpy.data.collections.new( - name=AVALON_CONTAINERS) - bpy.context.scene.collection.children.link(avalon_containers) + containers = bpy.data.collections.get(AVALON_CONTAINERS) + if not containers: + containers = bpy.data.collections.new(name=AVALON_CONTAINERS) + bpy.context.scene.collection.children.link(containers) asset_group = bpy.data.objects.new(group_name, object_data=None) - avalon_containers.objects.link(asset_group) + containers.objects.link(asset_group) objects = self._process(libpath, asset_group, group_name) @@ -134,16 +145,7 @@ class CacheModelLoader(plugin.AssetLoader): collection = bpy.context.view_layer.active_layer_collection.collection collection.objects.link(asset_group) - # Link the imported objects to any collection where the asset group is - # linked to, except the AVALON_CONTAINERS collection - group_collections = [ - collection - for collection in asset_group.users_collection - if collection != avalon_containers] - - for obj in objects: - for collection in group_collections: - collection.objects.link(obj) + self._link_objects(objects, asset_group, containers, asset_group) asset_group[AVALON_PROPERTY] = { "schema": "openpype:container-2.0", @@ -221,17 +223,8 @@ class CacheModelLoader(plugin.AssetLoader): objects = self._process(str(libpath), asset_group, object_name) - # Link the imported objects to any collection where the asset group is - # linked to, except the AVALON_CONTAINERS collection - avalon_containers = bpy.data.collections.get(AVALON_CONTAINERS) - group_collections = [ - collection - for collection in asset_group.users_collection - if collection != avalon_containers] - - for obj in objects: - for collection in group_collections: - collection.objects.link(obj) + containers = bpy.data.collections.get(AVALON_CONTAINERS) + self._link_objects(objects, asset_group, containers, asset_group) asset_group.matrix_basis = mat From 548ca106ad6ab8021f1c326ac375dd1dc42a3482 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 9 Oct 2023 17:14:21 +0800 Subject: [PATCH 0200/1233] paragraph tweaks on description for validator --- openpype/hosts/maya/plugins/publish/validate_resolution.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_resolution.py b/openpype/hosts/maya/plugins/publish/validate_resolution.py index b214f87906..4c3fbcddf0 100644 --- a/openpype/hosts/maya/plugins/publish/validate_resolution.py +++ b/openpype/hosts/maya/plugins/publish/validate_resolution.py @@ -29,7 +29,7 @@ class ValidateResolution(pyblish.api.InstancePlugin, "Render resolution is invalid. See log for details.", description=( "Wrong render resolution setting. " - "Please use repair button to fix it.\n" + "Please use repair button to fix it.\n\n" "If current renderer is V-Ray, " "make sure vraySettings node has been created" ) From 60834f6997247823c2d1d0463809207cb39cffed Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 9 Oct 2023 17:18:34 +0800 Subject: [PATCH 0201/1233] hound --- openpype/hosts/maya/plugins/publish/validate_resolution.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_resolution.py b/openpype/hosts/maya/plugins/publish/validate_resolution.py index 4c3fbcddf0..91b473b250 100644 --- a/openpype/hosts/maya/plugins/publish/validate_resolution.py +++ b/openpype/hosts/maya/plugins/publish/validate_resolution.py @@ -31,7 +31,7 @@ class ValidateResolution(pyblish.api.InstancePlugin, "Wrong render resolution setting. " "Please use repair button to fix it.\n\n" "If current renderer is V-Ray, " - "make sure vraySettings node has been created" + "make sure vraySettings node has been created." ) ) From 521707340af3e2357c7764d906da3659dec95e9d Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Mon, 9 Oct 2023 12:30:58 +0300 Subject: [PATCH 0202/1233] allow using template keys in houdini shelves manager --- openpype/hosts/houdini/api/shelves.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/api/shelves.py b/openpype/hosts/houdini/api/shelves.py index 21e44e494a..c961b0242d 100644 --- a/openpype/hosts/houdini/api/shelves.py +++ b/openpype/hosts/houdini/api/shelves.py @@ -6,6 +6,9 @@ import platform from openpype.settings import get_project_settings from openpype.pipeline import get_current_project_name +from openpype.lib import StringTemplate +from openpype.pipeline.context_tools import get_current_context_template_data + import hou log = logging.getLogger("openpype.hosts.houdini.shelves") @@ -26,9 +29,15 @@ def generate_shelves(): log.debug("No custom shelves found in project settings.") return + # Get Template data + template_data = get_current_context_template_data() + for shelf_set_config in shelves_set_config: shelf_set_filepath = shelf_set_config.get('shelf_set_source_path') shelf_set_os_filepath = shelf_set_filepath[current_os] + shelf_set_os_filepath = get_path_using_template_data( + shelf_set_os_filepath, template_data + ) if shelf_set_os_filepath: if not os.path.isfile(shelf_set_os_filepath): log.error("Shelf path doesn't exist - " @@ -81,7 +90,7 @@ def generate_shelves(): "script path of the tool.") continue - tool = get_or_create_tool(tool_definition, shelf) + tool = get_or_create_tool(tool_definition, shelf, template_data) if not tool: continue @@ -144,7 +153,7 @@ def get_or_create_shelf(shelf_label): return new_shelf -def get_or_create_tool(tool_definition, shelf): +def get_or_create_tool(tool_definition, shelf, template_data): """This function verifies if the tool exists and updates it. If not, creates a new one. @@ -162,6 +171,7 @@ def get_or_create_tool(tool_definition, shelf): return script_path = tool_definition["script"] + script_path = get_path_using_template_data(script_path, template_data) if not script_path or not os.path.exists(script_path): log.warning("This path doesn't exist - {}".format(script_path)) return @@ -184,3 +194,10 @@ def get_or_create_tool(tool_definition, shelf): tool_name = re.sub(r"[^\w\d]+", "_", tool_label).lower() return hou.shelves.newTool(name=tool_name, **tool_definition) + + +def get_path_using_template_data(path, template_data): + path = StringTemplate.format_template(path, template_data) + path = path.replace("\\", "/") + + return path From 587beadd4d430ba2ae33f0a6fee8461f5e9abb7f Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Mon, 9 Oct 2023 12:36:23 +0300 Subject: [PATCH 0203/1233] resolve hound --- openpype/hosts/houdini/api/shelves.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/api/shelves.py b/openpype/hosts/houdini/api/shelves.py index c961b0242d..a93f8becfb 100644 --- a/openpype/hosts/houdini/api/shelves.py +++ b/openpype/hosts/houdini/api/shelves.py @@ -90,7 +90,9 @@ def generate_shelves(): "script path of the tool.") continue - tool = get_or_create_tool(tool_definition, shelf, template_data) + tool = get_or_create_tool( + tool_definition, shelf, template_data + ) if not tool: continue From 02e1bfd22ba501f28efa2dfff4a94c87a17630c7 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 9 Oct 2023 18:20:34 +0800 Subject: [PATCH 0204/1233] add default channel data for tycache export --- .../max/plugins/publish/collect_tycache_attributes.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/collect_tycache_attributes.py b/openpype/hosts/max/plugins/publish/collect_tycache_attributes.py index 122b0d6451..d735b2f2c0 100644 --- a/openpype/hosts/max/plugins/publish/collect_tycache_attributes.py +++ b/openpype/hosts/max/plugins/publish/collect_tycache_attributes.py @@ -55,11 +55,15 @@ class CollectTyCacheData(pyblish.api.InstancePlugin, "tycacheSplines", "tycacheSplinesAdditionalSplines" ] - + tyc_default_attrs = ["tycacheChanGroups", "tycacheChanPos", + "tycacheChanRot", "tycacheChanScale", + "tycacheChanVel", "tycacheChanShape", + "tycacheChanMatID", "tycacheChanMapping", + "tycacheChanMaterials"] return [ EnumDef("all_tyc_attrs", tyc_attr_enum, - default=None, + default=tyc_default_attrs, multiselection=True, label="TyCache Attributes"), TextDef("tycache_layer", From d208ef644ba9f7bd77619c09c3c9ef2124489f70 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 9 Oct 2023 12:46:25 +0100 Subject: [PATCH 0205/1233] Improved error reporting for sequence frame validator --- .../publish/validate_sequence_frames.py | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py b/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py index 96485d5a2d..6ba4ea0d2f 100644 --- a/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py +++ b/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py @@ -39,8 +39,20 @@ class ValidateSequenceFrames(pyblish.api.InstancePlugin): collections, remainder = clique.assemble( repr["files"], minimum_items=1, patterns=patterns) - assert not remainder, "Must not have remainder" - assert len(collections) == 1, "Must detect single collection" + if remainder: + raise ValueError( + "Some files have been found outside a sequence." + f"Invalid files: {remainder}") + if not collections: + raise ValueError( + "No collections found. There should be a single " + "collection per representation.") + if len(collections) > 1: + raise ValueError( + "Multiple collections detected. There should be a single" + "collection per representation." + f"Collections identified: {collections}") + collection = collections[0] frames = list(collection.indexes) @@ -57,4 +69,7 @@ class ValidateSequenceFrames(pyblish.api.InstancePlugin): f"expected: {required_range}") missing = collection.holes().indexes - assert not missing, "Missing frames: %s" % (missing,) + if missing: + raise ValueError( + "Missing frames have been detected." + f"Missing frames: {missing}") From 1824670bcce30df524820bb9880002e3f72d8b71 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Mon, 9 Oct 2023 14:46:32 +0300 Subject: [PATCH 0206/1233] save unnecessary call --- openpype/hosts/houdini/api/shelves.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/houdini/api/shelves.py b/openpype/hosts/houdini/api/shelves.py index a93f8becfb..4d6a05b79d 100644 --- a/openpype/hosts/houdini/api/shelves.py +++ b/openpype/hosts/houdini/api/shelves.py @@ -35,10 +35,10 @@ def generate_shelves(): for shelf_set_config in shelves_set_config: shelf_set_filepath = shelf_set_config.get('shelf_set_source_path') shelf_set_os_filepath = shelf_set_filepath[current_os] - shelf_set_os_filepath = get_path_using_template_data( - shelf_set_os_filepath, template_data - ) if shelf_set_os_filepath: + shelf_set_os_filepath = get_path_using_template_data( + shelf_set_os_filepath, template_data + ) if not os.path.isfile(shelf_set_os_filepath): log.error("Shelf path doesn't exist - " "{}".format(shelf_set_os_filepath)) From 31f3e68349f287e9a9a8c4da6d6f7094f8712563 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 9 Oct 2023 12:53:24 +0100 Subject: [PATCH 0207/1233] Fixed some spacing issues in the error reports --- .../unreal/plugins/publish/validate_sequence_frames.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py b/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py index 6ba4ea0d2f..e24729391f 100644 --- a/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py +++ b/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py @@ -41,7 +41,7 @@ class ValidateSequenceFrames(pyblish.api.InstancePlugin): if remainder: raise ValueError( - "Some files have been found outside a sequence." + "Some files have been found outside a sequence. " f"Invalid files: {remainder}") if not collections: raise ValueError( @@ -49,8 +49,8 @@ class ValidateSequenceFrames(pyblish.api.InstancePlugin): "collection per representation.") if len(collections) > 1: raise ValueError( - "Multiple collections detected. There should be a single" - "collection per representation." + "Multiple collections detected. There should be a single " + "collection per representation. " f"Collections identified: {collections}") collection = collections[0] @@ -71,5 +71,5 @@ class ValidateSequenceFrames(pyblish.api.InstancePlugin): missing = collection.holes().indexes if missing: raise ValueError( - "Missing frames have been detected." + "Missing frames have been detected. " f"Missing frames: {missing}") From 523633c1aaa1f3d54f518a5a3a3f2e8240f3479a Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 9 Oct 2023 12:53:46 +0100 Subject: [PATCH 0208/1233] Optional workfile dependency --- .../plugins/publish/submit_nuke_deadline.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py index 0295c2b760..0e57c54959 100644 --- a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py @@ -48,6 +48,7 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin, use_gpu = False env_allowed_keys = [] env_search_replace_values = {} + workfile_dependency = True @classmethod def get_attribute_defs(cls): @@ -83,6 +84,11 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin, "suspend_publish", default=False, label="Suspend publish" + ), + BoolDef( + "workfile_dependency", + default=True, + label="Workfile Dependency" ) ] @@ -313,6 +319,13 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin, "AuxFiles": [] } + # Add workfile dependency. + workfile_dependency = instance.data["attributeValues"].get( + "workfile_dependency", self.workfile_dependency + ) + if workfile_dependency: + payload["JobInfo"].update({"AssetDependency0": script_path}) + # TODO: rewrite for baking with sequences if baking_submission: payload["JobInfo"].update({ From 0a71b89ddd857676d1561c3cdbfeb690ebae6103 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 9 Oct 2023 13:57:53 +0200 Subject: [PATCH 0209/1233] global: adding abstracted `get_representation_files` --- openpype/pipeline/__init__.py | 2 ++ openpype/pipeline/load/__init__.py | 2 ++ openpype/pipeline/load/utils.py | 50 ++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+) diff --git a/openpype/pipeline/__init__.py b/openpype/pipeline/__init__.py index 8f370d389b..ca2a6bcf2c 100644 --- a/openpype/pipeline/__init__.py +++ b/openpype/pipeline/__init__.py @@ -48,6 +48,7 @@ from .load import ( loaders_from_representation, get_representation_path, get_representation_context, + get_representation_files, get_repres_contexts, ) @@ -152,6 +153,7 @@ __all__ = ( "loaders_from_representation", "get_representation_path", "get_representation_context", + "get_representation_files", "get_repres_contexts", # --- Publish --- diff --git a/openpype/pipeline/load/__init__.py b/openpype/pipeline/load/__init__.py index 7320a9f0e8..c07388fd45 100644 --- a/openpype/pipeline/load/__init__.py +++ b/openpype/pipeline/load/__init__.py @@ -11,6 +11,7 @@ from .utils import ( get_contexts_for_repre_docs, get_subset_contexts, get_representation_context, + get_representation_files, load_with_repre_context, load_with_subset_context, @@ -64,6 +65,7 @@ __all__ = ( "get_contexts_for_repre_docs", "get_subset_contexts", "get_representation_context", + "get_representation_files", "load_with_repre_context", "load_with_subset_context", diff --git a/openpype/pipeline/load/utils.py b/openpype/pipeline/load/utils.py index b10d6032b3..81175a8261 100644 --- a/openpype/pipeline/load/utils.py +++ b/openpype/pipeline/load/utils.py @@ -1,4 +1,6 @@ import os +import re +import glob import platform import copy import getpass @@ -286,6 +288,54 @@ def get_representation_context(representation): return context +def get_representation_files(context, filepath): + """Return list of files for representation. + + Args: + representation (dict): Representation document. + filepath (str): Filepath of the representation. + + Returns: + list[str]: List of files for representation. + """ + version = context["version"] + frame_start = version["data"]["frameStart"] + frame_end = version["data"]["frameEnd"] + handle_start = version["data"]["handleStart"] + handle_end = version["data"]["handleEnd"] + + first_frame = frame_start - handle_start + last_frame = frame_end + handle_end + dir_path = os.path.dirname(filepath) + base_name = os.path.basename(filepath) + + # prepare glob pattern for searching + padding = len(str(last_frame)) + str_first_frame = str(first_frame).zfill(padding) + + # convert str_first_frame to glob pattern + # replace all digits with `?` and all other chars with `[char]` + # example: `0001` -> `????` + glob_pattern = re.sub(r"\d", "?", str_first_frame) + + # in filename replace number with glob pattern + # example: `filename.0001.exr` -> `filename.????.exr` + base_name = re.sub(str_first_frame, glob_pattern, base_name) + + files = [] + # get all files in folder + for file in glob.glob(os.path.join(dir_path, base_name)): + files.append(file) + + # keep only existing files + files = [f for f in files if os.path.exists(f)] + + # sort files by frame number + files.sort(key=lambda f: int(re.findall(r"\d+", f)[-1])) + + return files + + def load_with_repre_context( Loader, repre_context, namespace=None, name=None, options=None, **kwargs ): From 26b2817a7067cf74d8754579236292fa22752e86 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 9 Oct 2023 13:58:19 +0200 Subject: [PATCH 0210/1233] refactor loading for abstracted `get_representation_files` --- openpype/hosts/resolve/api/lib.py | 40 ++-------- openpype/hosts/resolve/api/plugin.py | 75 +++++-------------- .../hosts/resolve/plugins/load/load_clip.py | 15 ++-- 3 files changed, 35 insertions(+), 95 deletions(-) diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index 942caca72a..70a7680d8d 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -190,11 +190,7 @@ def remove_media_pool_item(media_pool_item: object) -> bool: def create_media_pool_item( - fpath: str, - frame_start: int, - frame_end: int, - handle_start: int, - handle_end: int, + files: list, root: object = None, ) -> object: """ @@ -212,49 +208,23 @@ def create_media_pool_item( root_bin = root or media_pool.GetRootFolder() # try to search in bin if the clip does not exist - existing_mpi = get_media_pool_item(fpath, root_bin) + existing_mpi = get_media_pool_item(files[0], root_bin) if existing_mpi: return existing_mpi - files = [] - first_frame = frame_start - handle_start - last_frame = frame_end + handle_end - dir_path = os.path.dirname(fpath) - base_name = os.path.basename(fpath) - - # prepare glob pattern for searching - padding = len(str(last_frame)) - str_first_frame = str(first_frame).zfill(padding) - - # convert str_first_frame to glob pattern - # replace all digits with `?` and all other chars with `[char]` - # example: `0001` -> `????` - glob_pattern = re.sub(r"\d", "?", str_first_frame) - - # in filename replace number with glob pattern - # example: `filename.0001.exr` -> `filename.????.exr` - base_name = re.sub(str_first_frame, glob_pattern, base_name) - - # get all files in folder - for file in glob.glob(os.path.join(dir_path, base_name)): - files.append(file) - - # keep only existing files - files = [f for f in files if os.path.exists(f)] - # add all data in folder to media pool media_pool_items = media_pool.ImportMedia(files) return media_pool_items.pop() if media_pool_items else False -def get_media_pool_item(fpath, root: object = None) -> object: +def get_media_pool_item(filepath, root: object = None) -> object: """ Return clip if found in folder with use of input file path. Args: - fpath (str): absolute path to a file + filepath (str): absolute path to a file root (resolve.Folder)[optional]: root folder / bin object Returns: @@ -262,7 +232,7 @@ def get_media_pool_item(fpath, root: object = None) -> object: """ media_pool = get_current_project().GetMediaPool() root = root or media_pool.GetRootFolder() - fname = os.path.basename(fpath) + fname = os.path.basename(filepath) for _mpi in root.GetClipList(): _mpi_name = _mpi.GetClipProperty("File Name") diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py index 85245a5d12..b1d6b595c1 100644 --- a/openpype/hosts/resolve/api/plugin.py +++ b/openpype/hosts/resolve/api/plugin.py @@ -290,7 +290,7 @@ class ClipLoader: active_bin = None data = dict() - def __init__(self, loader_obj, context, path, **options): + def __init__(self, loader_obj, context, **options): """ Initialize object Arguments: @@ -303,7 +303,6 @@ class ClipLoader: self.__dict__.update(loader_obj.__dict__) self.context = context self.active_project = lib.get_current_project() - self.fname = path # try to get value from options or evaluate key value for `handles` self.with_handles = options.get("handles") or bool( @@ -343,37 +342,29 @@ class ClipLoader: data structure: { "name": "assetName_subsetName_representationName" - "path": "path/to/file/created/by/get_repr..", "binPath": "projectBinPath", } """ # create name - repr = self.context["representation"] - repr_cntx = repr["context"] - asset = str(repr_cntx["asset"]) - subset = str(repr_cntx["subset"]) - representation = str(repr_cntx["representation"]) + representation = self.context["representation"] + representation_context = representation["context"] + asset = str(representation_context["asset"]) + subset = str(representation_context["subset"]) + representation_name = str(representation_context["representation"]) self.data["clip_name"] = "_".join([ asset, subset, - representation + representation_name ]) self.data["versionData"] = self.context["version"]["data"] - # gets file path - file = self.fname - if not file: - repr_id = repr["_id"] - print( - "Representation id `{}` is failing to load".format(repr_id)) - return None - self.data["path"] = file.replace("\\", "/") + self.data["timeline_basename"] = "timeline_{}_{}".format( - subset, representation) + subset, representation_name) # solve project bin structure path hierarchy = str("/".join(( "Loader", - repr_cntx["hierarchy"].replace("\\", "/"), + representation_context["hierarchy"].replace("\\", "/"), asset ))) @@ -390,39 +381,20 @@ class ClipLoader: asset_name = self.context["representation"]["context"]["asset"] self.data["assetData"] = get_current_project_asset(asset_name)["data"] - def _get_frame_data(self): - # create mediaItem in active project bin - # create clip media - frame_start = self.data["versionData"].get("frameStart") - frame_end = self.data["versionData"].get("frameEnd") - if frame_start is None: - frame_start = int(self.data["assetData"]["frameStart"]) - if frame_end is None: - frame_end = int(self.data["assetData"]["frameEnd"]) - # 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"]) + def load(self, files): + """Load clip into timeline - return frame_start, frame_end, handle_start, handle_end - - def load(self): + Arguments: + files (list): list of files to load into timeline + """ # create project bin for the media to be imported into self.active_bin = lib.create_bin(self.data["binPath"]) - - frame_start, frame_end, handle_start, handle_end = \ - self._get_frame_data() + handle_start = self.data["versionData"].get("handleStart", 0) + handle_end = self.data["versionData"].get("handleEnd", 0) media_pool_item = lib.create_media_pool_item( - self.data["path"], - frame_start, - frame_end, - handle_start, - handle_end, + files, self.active_bin ) _clip_property = media_pool_item.GetClipProperty @@ -446,21 +418,14 @@ class ClipLoader: print("Loading clips: `{}`".format(self.data["clip_name"])) return timeline_item - def update(self, timeline_item): + def update(self, timeline_item, files): # create project bin for the media to be imported into self.active_bin = lib.create_bin(self.data["binPath"]) - frame_start, frame_end, handle_start, handle_end = \ - self._get_frame_data() - # create mediaItem in active project bin # create clip media media_pool_item = lib.create_media_pool_item( - self.data["path"], - frame_start, - frame_end, - handle_start, - handle_end, + files, self.active_bin ) _clip_property = media_pool_item.GetClipProperty diff --git a/openpype/hosts/resolve/plugins/load/load_clip.py b/openpype/hosts/resolve/plugins/load/load_clip.py index 5e81441332..35a6b97eea 100644 --- a/openpype/hosts/resolve/plugins/load/load_clip.py +++ b/openpype/hosts/resolve/plugins/load/load_clip.py @@ -3,6 +3,7 @@ from openpype.pipeline import ( get_representation_path, get_representation_context, get_current_project_name, + get_representation_files ) from openpype.hosts.resolve.api import lib, plugin from openpype.hosts.resolve.api.pipeline import ( @@ -44,9 +45,11 @@ class LoadClip(plugin.TimelineItemLoader): def load(self, context, name, namespace, options): # load clip to timeline and get main variables - path = self.filepath_from_context(context) + filepath = self.filepath_from_context(context) + files = get_representation_files(context, filepath) + timeline_item = plugin.ClipLoader( - self, context, path, **options).load() + self, context, **options).load(files) namespace = namespace or timeline_item.GetName() # update color of clip regarding the version order @@ -73,9 +76,11 @@ class LoadClip(plugin.TimelineItemLoader): media_pool_item = timeline_item.GetMediaPoolItem() - path = get_representation_path(representation) - loader = plugin.ClipLoader(self, context, path) - timeline_item = loader.update(timeline_item) + filepath = get_representation_path(representation) + files = get_representation_files(context, filepath) + + loader = plugin.ClipLoader(self, context) + timeline_item = loader.update(timeline_item, files) # update color of clip regarding the version order self.set_item_color(timeline_item, version=context["version"]) From 1d02f46e1558feeea019178fc46928e3cddde36e Mon Sep 17 00:00:00 2001 From: Sharkitty <81646000+Sharkitty@users.noreply.github.com> Date: Mon, 9 Oct 2023 12:12:26 +0000 Subject: [PATCH 0211/1233] Feature: Copy resources when downloading last workfile (#4944) * Feature: Copy resources when downloading workfile * Fixed resources dir var name * Removing prints * Fix wrong resources path * Fixed workfile copied to resources folder + lint * Added comments * Handling resource already exists * linting * more linting * Bugfix: copy resources backslash in main path * linting * Using more continue statements, and more comments --------- Co-authored-by: Petr Kalis --- .../pre_copy_last_published_workfile.py | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) 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 047e35e3ac..4a8099606b 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 @@ -1,5 +1,6 @@ import os import shutil +import filecmp from openpype.client.entities import get_representations from openpype.lib.applications import PreLaunchHook, LaunchTypes @@ -194,3 +195,71 @@ class CopyLastPublishedWorkfile(PreLaunchHook): self.data["last_workfile_path"] = local_workfile_path # Keep source filepath for further path conformation self.data["source_filepath"] = last_published_workfile_path + + # Get resources directory + resources_dir = os.path.join( + os.path.dirname(local_workfile_path), 'resources' + ) + # Make resource directory if it doesn't exist + if not os.path.exists(resources_dir): + os.mkdir(resources_dir) + + # Copy resources to the local resources directory + for file in workfile_representation['files']: + # Get resource main path + resource_main_path = file["path"].replace( + "{root[main]}", str(anatomy.roots["main"]) + ) + + # Only copy if the resource file exists, and it's not the workfile + if ( + not os.path.exists(resource_main_path) + and not resource_main_path != last_published_workfile_path + ): + continue + + # Get resource file basename + resource_basename = os.path.basename(resource_main_path) + + # Get resource path in workfile folder + resource_work_path = os.path.join( + resources_dir, resource_basename + ) + if not os.path.exists(resource_work_path): + continue + + # Check if the resource file already exists + # in the workfile resources folder, + # and both files are the same. + if filecmp.cmp(resource_main_path, resource_work_path): + self.log.warning( + 'Resource "{}" already exists.' + .format(resource_basename) + ) + continue + else: + # Add `.old` to existing resource path + resource_path_old = resource_work_path + '.old' + if os.path.exists(resource_work_path + '.old'): + for i in range(1, 100): + p = resource_path_old + '%02d' % i + if not os.path.exists(p): + # Rename existing resource file to + # `resource_name.old` + 2 digits + shutil.move(resource_work_path, p) + break + else: + self.log.warning( + 'There are a hundred old files for ' + 'resource "{}". ' + 'Perhaps is it time to clean up your ' + 'resources folder' + .format(resource_basename) + ) + continue + else: + # Rename existing resource file to `resource_name.old` + shutil.move(resource_work_path, resource_path_old) + + # Copy resource file to workfile resources folder + shutil.copy(resource_main_path, resources_dir) From 366bfb24354f62896db7f34baba80d28e54d431d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 9 Oct 2023 15:30:37 +0200 Subject: [PATCH 0212/1233] hound --- openpype/hosts/resolve/api/lib.py | 1 - openpype/hosts/resolve/api/plugin.py | 1 - 2 files changed, 2 deletions(-) diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index 70a7680d8d..4066dd34fd 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -2,7 +2,6 @@ import sys import json import re import os -import glob import contextlib from opentimelineio import opentime diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py index b1d6b595c1..f3a65034fb 100644 --- a/openpype/hosts/resolve/api/plugin.py +++ b/openpype/hosts/resolve/api/plugin.py @@ -381,7 +381,6 @@ class ClipLoader: asset_name = self.context["representation"]["context"]["asset"] self.data["assetData"] = get_current_project_asset(asset_name)["data"] - def load(self, files): """Load clip into timeline From 92a256d7571afa6023ab95fdb54dfea38754d9f0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 9 Oct 2023 15:41:03 +0200 Subject: [PATCH 0213/1233] false docstring --- openpype/pipeline/load/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/load/utils.py b/openpype/pipeline/load/utils.py index 81175a8261..5193eaa86e 100644 --- a/openpype/pipeline/load/utils.py +++ b/openpype/pipeline/load/utils.py @@ -292,7 +292,7 @@ def get_representation_files(context, filepath): """Return list of files for representation. Args: - representation (dict): Representation document. + context (dict): The full loading context. filepath (str): Filepath of the representation. Returns: From 8168c96ae5d8392e05d07e7690bddf82d29c7ac8 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 9 Oct 2023 15:44:59 +0200 Subject: [PATCH 0214/1233] :recycle: some more fixes --- .../plugins/create/create_multishot_layout.py | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_multishot_layout.py b/openpype/hosts/maya/plugins/create/create_multishot_layout.py index 6ff40851e3..0f40f74be8 100644 --- a/openpype/hosts/maya/plugins/create/create_multishot_layout.py +++ b/openpype/hosts/maya/plugins/create/create_multishot_layout.py @@ -66,20 +66,22 @@ class CreateMultishotLayout(plugin.MayaCreator): # } # ] + # add the project as the first item + items_with_label = [ + dict(label=f"{self.project_name} " + "(shots directly under the project)", value="") + ] + # go through the current folder path and add each part to the list, # but mark the current folder. - for part_idx in range(len(current_path_parts)): - label = current_path_parts[part_idx] - if current_path_parts[part_idx] == current_folder["name"]: - label = f"{current_path_parts[part_idx]} (current)" - items_with_label.append( - dict(label=label, - value="/".join(current_path_parts[:part_idx + 1])) - ) - # add the project as the first item - items_with_label.insert( - 0, dict(label=f"{self.project_name} " - "(shots directly under the project)", value="")) + for part_idx, part in enumerate(current_path_parts): + label = part + if label == current_folder["name"]: + label = f"{label} (current)" + + value = "/".join(current_path_parts[:part_idx + 1]) + + items_with_label.append({"label": label, "value": value}) return [ EnumDef("shotParent", @@ -115,10 +117,14 @@ class CreateMultishotLayout(plugin.MayaCreator): layout_creator_id = "io.openpype.creators.maya.layout" layout_creator: Creator = self.create_context.creators.get( layout_creator_id) + if not layout_creator: + raise CreatorError( + f"Creator {layout_creator_id} not found.") # Get OpenPype style asset documents for the shots op_asset_docs = get_assets( self.project_name, [s["id"] for s in shots]) + asset_docs_by_id = {doc["_id"]: doc for doc in op_asset_docs} for shot in shots: # we are setting shot name to be displayed in the sequencer to # `shot name (shot label)` if the label is set, otherwise just @@ -128,13 +134,9 @@ class CreateMultishotLayout(plugin.MayaCreator): continue # get task for shot - asset_doc = next( - asset_doc for asset_doc in op_asset_docs - if asset_doc["_id"] == shot["id"] + asset_doc = asset_docs_by_id[shot["id"]] - ) - - tasks = list(asset_doc.get("data").get("tasks").keys()) + tasks = asset_doc.get("data").get("tasks").keys() layout_task = None if pre_create_data["taskName"] in tasks: layout_task = pre_create_data["taskName"] From 77a0930ed04cb567ea673fe28123efb28c370eb5 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 9 Oct 2023 15:51:31 +0200 Subject: [PATCH 0215/1233] :dog: happy dog --- .../hosts/maya/plugins/create/create_multishot_layout.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_multishot_layout.py b/openpype/hosts/maya/plugins/create/create_multishot_layout.py index 0f40f74be8..eb36825fc4 100644 --- a/openpype/hosts/maya/plugins/create/create_multishot_layout.py +++ b/openpype/hosts/maya/plugins/create/create_multishot_layout.py @@ -68,8 +68,11 @@ class CreateMultishotLayout(plugin.MayaCreator): # add the project as the first item items_with_label = [ - dict(label=f"{self.project_name} " - "(shots directly under the project)", value="") + { + "label": f"{self.project_name} " + "(shots directly under the project)", + "value": "" + } ] # go through the current folder path and add each part to the list, From ac9f08edf6ae02557b4b3a48d5fbca0388227f81 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 9 Oct 2023 16:05:52 +0200 Subject: [PATCH 0216/1233] :recycle: use long arguments --- openpype/hosts/maya/plugins/create/create_multishot_layout.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_multishot_layout.py b/openpype/hosts/maya/plugins/create/create_multishot_layout.py index eb36825fc4..36fee655e6 100644 --- a/openpype/hosts/maya/plugins/create/create_multishot_layout.py +++ b/openpype/hosts/maya/plugins/create/create_multishot_layout.py @@ -146,8 +146,8 @@ class CreateMultishotLayout(plugin.MayaCreator): shot_name = f"{shot['name']}%s" % ( f" ({shot['label']})" if shot["label"] else "") - cmds.shot(sst=shot["attrib"]["clipIn"], - set=shot["attrib"]["clipOut"], + cmds.shot(sequenceStartTime=shot["attrib"]["clipIn"], + sequenceEndTime=shot["attrib"]["clipOut"], shotName=shot_name) # Create layout instance by the layout creator From 9ff279d52f2a92471bf530a2664dafcbf9018c03 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 9 Oct 2023 16:07:32 +0200 Subject: [PATCH 0217/1233] using `self.get_subset_name` rather then own function --- .../plugins/create/create_colorspace_look.py | 39 +++---------------- 1 file changed, 5 insertions(+), 34 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/create/create_colorspace_look.py b/openpype/hosts/traypublisher/plugins/create/create_colorspace_look.py index 0daffc728c..a1b7896fba 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_colorspace_look.py +++ b/openpype/hosts/traypublisher/plugins/create/create_colorspace_look.py @@ -14,10 +14,6 @@ from openpype.pipeline import ( CreatedInstance, CreatorError ) -from openpype.pipeline.create import ( - get_subset_name, - TaskNotSetError, -) from openpype.pipeline import colorspace from openpype.hosts.traypublisher.api.plugin import TrayPublishCreator @@ -61,9 +57,11 @@ This creator publishes color space look file (LUT). asset_doc = get_asset_by_name( self.project_name, instance_data["asset"]) - subset_name = self._get_subset( - asset_doc, instance_data["variant"], self.project_name, - instance_data["task"] + subset_name = self.get_subset_name( + variant=instance_data["variant"], + task_name=instance_data["task"] or "Not set", + project_name=self.project_name, + asset_doc=asset_doc, ) instance_data["creator_attributes"] = { @@ -175,30 +173,3 @@ This creator publishes color space look file (LUT). self.config_data = config_data self.colorspace_items.extend(labeled_colorspaces) self.enabled = True - - def _get_subset(self, asset_doc, variant, project_name, task_name=None): - """Create subset name according to standard template process""" - - try: - subset_name = get_subset_name( - self.family, - variant, - task_name, - asset_doc, - project_name - ) - except TaskNotSetError: - # Create instance with fake task - # - instance will be marked as invalid so it can't be published - # but user have ability to change it - # NOTE: This expect that there is not task 'Undefined' on asset - task_name = "Undefined" - subset_name = get_subset_name( - self.family, - variant, - task_name, - asset_doc, - project_name - ) - - return subset_name From 5c6a7b6b25f49bdd7da2e372d5d3172844ca8948 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 9 Oct 2023 16:13:25 +0200 Subject: [PATCH 0218/1233] hound suggestions --- openpype/pipeline/colorspace.py | 5 +++- ...pace_convert_colorspace_enumerator_item.py | 2 +- ...rspace_get_colorspaces_enumerator_items.py | 25 ++++++++++++------- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 8bebc934fc..1dbe869ad9 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -573,7 +573,10 @@ def convert_colorspace_enumerator_item( # raise exception if item is not found if not item_data: message_config_keys = ", ".join( - "'{}':{}".format(key, set(config_items.get(key, {}).keys())) for key in config_items.keys() + "'{}':{}".format( + key, + set(config_items.get(key, {}).keys()) + ) for key in config_items.keys() ) raise KeyError( "Missing colorspace item '{}' in config data: [{}]".format( diff --git a/tests/unit/openpype/pipeline/test_colorspace_convert_colorspace_enumerator_item.py b/tests/unit/openpype/pipeline/test_colorspace_convert_colorspace_enumerator_item.py index bffe8eda90..56ac2a5d28 100644 --- a/tests/unit/openpype/pipeline/test_colorspace_convert_colorspace_enumerator_item.py +++ b/tests/unit/openpype/pipeline/test_colorspace_convert_colorspace_enumerator_item.py @@ -1,4 +1,3 @@ -from ast import alias import unittest from openpype.pipeline.colorspace import convert_colorspace_enumerator_item @@ -114,5 +113,6 @@ class TestConvertColorspaceEnumeratorItem(unittest.TestCase): with self.assertRaises(KeyError): convert_colorspace_enumerator_item("RGB::sRGB", config_items) + if __name__ == '__main__': unittest.main() diff --git a/tests/unit/openpype/pipeline/test_colorspace_get_colorspaces_enumerator_items.py b/tests/unit/openpype/pipeline/test_colorspace_get_colorspaces_enumerator_items.py index de3e333670..c221712d70 100644 --- a/tests/unit/openpype/pipeline/test_colorspace_get_colorspaces_enumerator_items.py +++ b/tests/unit/openpype/pipeline/test_colorspace_get_colorspaces_enumerator_items.py @@ -45,7 +45,8 @@ class TestGetColorspacesEnumeratorItems(unittest.TestCase): self.assertEqual(result, expected) def test_aliases(self): - result = get_colorspaces_enumerator_items(self.config_items, include_aliases=True) + result = get_colorspaces_enumerator_items( + self.config_items, include_aliases=True) expected = [ ("colorspaces::Rec.709", "[colorspace] Rec.709"), ("colorspaces::sRGB", "[colorspace] sRGB"), @@ -56,7 +57,8 @@ class TestGetColorspacesEnumeratorItems(unittest.TestCase): self.assertEqual(result, expected) def test_looks(self): - result = get_colorspaces_enumerator_items(self.config_items, include_looks=True) + result = get_colorspaces_enumerator_items( + self.config_items, include_looks=True) expected = [ ("colorspaces::Rec.709", "[colorspace] Rec.709"), ("colorspaces::sRGB", "[colorspace] sRGB"), @@ -65,20 +67,22 @@ class TestGetColorspacesEnumeratorItems(unittest.TestCase): self.assertEqual(result, expected) def test_display_views(self): - result = get_colorspaces_enumerator_items(self.config_items, include_display_views=True) + result = get_colorspaces_enumerator_items( + self.config_items, include_display_views=True) expected = [ ("colorspaces::Rec.709", "[colorspace] Rec.709"), ("colorspaces::sRGB", "[colorspace] sRGB"), - ("displays_views::Rec.709 (ACES)", "[view (display)] Rec.709 (ACES)"), + ("displays_views::Rec.709 (ACES)", "[view (display)] Rec.709 (ACES)"), # noqa: E501 ("displays_views::sRGB (ACES)", "[view (display)] sRGB (ACES)"), ] self.assertEqual(result, expected) def test_roles(self): - result = get_colorspaces_enumerator_items(self.config_items, include_roles=True) + result = get_colorspaces_enumerator_items( + self.config_items, include_roles=True) expected = [ - ("roles::compositing_linear", "[role] compositing_linear (linear)"), + ("roles::compositing_linear", "[role] compositing_linear (linear)"), # noqa: E501 ("colorspaces::Rec.709", "[colorspace] Rec.709"), ("colorspaces::sRGB", "[colorspace] sRGB"), ] @@ -86,7 +90,10 @@ class TestGetColorspacesEnumeratorItems(unittest.TestCase): def test_all(self): message_config_keys = ", ".join( - "'{}':{}".format(key, set(self.config_items.get(key, {}).keys())) for key in self.config_items.keys() + "'{}':{}".format( + key, + set(self.config_items.get(key, {}).keys()) + ) for key in self.config_items.keys() ) print("Testing with config: [{}]".format(message_config_keys)) result = get_colorspaces_enumerator_items( @@ -97,14 +104,14 @@ class TestGetColorspacesEnumeratorItems(unittest.TestCase): include_display_views=True, ) expected = [ - ("roles::compositing_linear", "[role] compositing_linear (linear)"), + ("roles::compositing_linear", "[role] compositing_linear (linear)"), # noqa: E501 ("colorspaces::Rec.709", "[colorspace] Rec.709"), ("colorspaces::sRGB", "[colorspace] sRGB"), ("aliases::rec709_1", "[alias] rec709_1 (Rec.709)"), ("aliases::rec709_2", "[alias] rec709_2 (Rec.709)"), ("aliases::sRGB_1", "[alias] sRGB_1 (sRGB)"), ("looks::sRGB_to_Rec.709", "[look] sRGB_to_Rec.709 (sRGB)"), - ("displays_views::Rec.709 (ACES)", "[view (display)] Rec.709 (ACES)"), + ("displays_views::Rec.709 (ACES)", "[view (display)] Rec.709 (ACES)"), # noqa: E501 ("displays_views::sRGB (ACES)", "[view (display)] sRGB (ACES)"), ] self.assertEqual(result, expected) From b9185af47fa0f631ff054dff62b1bb865e03f7cf Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 9 Oct 2023 16:30:38 +0200 Subject: [PATCH 0219/1233] hound and docstring suggestion --- .../traypublisher/plugins/create/create_colorspace_look.py | 2 -- .../traypublisher/plugins/publish/collect_colorspace_look.py | 2 +- openpype/pipeline/colorspace.py | 3 ++- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/create/create_colorspace_look.py b/openpype/hosts/traypublisher/plugins/create/create_colorspace_look.py index a1b7896fba..5628d0973f 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_colorspace_look.py +++ b/openpype/hosts/traypublisher/plugins/create/create_colorspace_look.py @@ -77,7 +77,6 @@ This creator publishes color space look file (LUT). self._store_new_instance(new_instance) - def collect_instances(self): super().collect_instances() for instance in self.create_context.instances: @@ -85,7 +84,6 @@ This creator publishes color space look file (LUT). instance.transient_data["config_items"] = self.config_items instance.transient_data["config_data"] = self.config_data - def get_instance_attr_defs(self): return [ EnumDef( diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_colorspace_look.py b/openpype/hosts/traypublisher/plugins/publish/collect_colorspace_look.py index 4dc5348fb1..c7a886a619 100644 --- a/openpype/hosts/traypublisher/plugins/publish/collect_colorspace_look.py +++ b/openpype/hosts/traypublisher/plugins/publish/collect_colorspace_look.py @@ -4,6 +4,7 @@ import pyblish.api from openpype.pipeline import publish from openpype.pipeline import colorspace + class CollectColorspaceLook(pyblish.api.InstancePlugin, publish.OpenPypePyblishPluginMixin): """Collect OCIO colorspace look from LUT file @@ -30,7 +31,6 @@ class CollectColorspaceLook(pyblish.api.InstancePlugin, .title() .replace(" ", "")) - # get config items config_items = instance.data["transientData"]["config_items"] config_data = instance.data["transientData"]["config_data"] diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 1dbe869ad9..82d9b17a37 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -600,7 +600,8 @@ def get_colorspaces_enumerator_items( Families can be used for building menu and submenus in gui. Args: - config_items (dict[str,dict]): colorspace data + config_items (dict[str,dict]): colorspace data coming from + `get_ocio_config_colorspaces` function include_aliases (bool): include aliases in result include_looks (bool): include looks in result include_roles (bool): include roles in result From 15aec6db161f86bf4258f2148fa853ccbfe24ad1 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Mon, 9 Oct 2023 17:52:30 +0300 Subject: [PATCH 0220/1233] allow icon path to include template keys --- openpype/hosts/houdini/api/shelves.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/hosts/houdini/api/shelves.py b/openpype/hosts/houdini/api/shelves.py index 4d6a05b79d..4b5ebd4202 100644 --- a/openpype/hosts/houdini/api/shelves.py +++ b/openpype/hosts/houdini/api/shelves.py @@ -178,6 +178,11 @@ def get_or_create_tool(tool_definition, shelf, template_data): log.warning("This path doesn't exist - {}".format(script_path)) return + icon_path = tool_definition["icon"] + if icon_path: + icon_path = get_path_using_template_data(icon_path, template_data) + tool_definition["icon"] = icon_path + existing_tools = shelf.tools() existing_tool = next( (tool for tool in existing_tools if tool.label() == tool_label), From 71a1365216fc9f89b150fa3903be99cbf85c9c38 Mon Sep 17 00:00:00 2001 From: Sharkitty <81646000+Sharkitty@users.noreply.github.com> Date: Mon, 9 Oct 2023 15:11:35 +0000 Subject: [PATCH 0221/1233] Fix: Hardcoded main site and wrongly copied workfile (#5733) --- .../pre_copy_last_published_workfile.py | 82 +++++++++---------- 1 file changed, 40 insertions(+), 42 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 4a8099606b..bdb4b109a1 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 @@ -207,59 +207,57 @@ class CopyLastPublishedWorkfile(PreLaunchHook): # Copy resources to the local resources directory for file in workfile_representation['files']: # Get resource main path - resource_main_path = file["path"].replace( - "{root[main]}", str(anatomy.roots["main"]) - ) + resource_main_path = anatomy.fill_root(file["path"]) + + # Get resource file basename + resource_basename = os.path.basename(resource_main_path) # Only copy if the resource file exists, and it's not the workfile if ( not os.path.exists(resource_main_path) - and not resource_main_path != last_published_workfile_path + or resource_basename == os.path.basename( + last_published_workfile_path + ) ): continue - # Get resource file basename - resource_basename = os.path.basename(resource_main_path) - # Get resource path in workfile folder resource_work_path = os.path.join( resources_dir, resource_basename ) - if not os.path.exists(resource_work_path): - continue - # Check if the resource file already exists - # in the workfile resources folder, - # and both files are the same. - if filecmp.cmp(resource_main_path, resource_work_path): - self.log.warning( - 'Resource "{}" already exists.' - .format(resource_basename) - ) - continue - else: - # Add `.old` to existing resource path - resource_path_old = resource_work_path + '.old' - if os.path.exists(resource_work_path + '.old'): - for i in range(1, 100): - p = resource_path_old + '%02d' % i - if not os.path.exists(p): - # Rename existing resource file to - # `resource_name.old` + 2 digits - shutil.move(resource_work_path, p) - break - else: - self.log.warning( - 'There are a hundred old files for ' - 'resource "{}". ' - 'Perhaps is it time to clean up your ' - 'resources folder' - .format(resource_basename) - ) - continue + # Check if the resource file already exists in the resources folder + if os.path.exists(resource_work_path): + # Check if both files are the same + if filecmp.cmp(resource_main_path, resource_work_path): + self.log.warning( + 'Resource "{}" already exists.' + .format(resource_basename) + ) + continue else: - # Rename existing resource file to `resource_name.old` - shutil.move(resource_work_path, resource_path_old) + # Add `.old` to existing resource path + resource_path_old = resource_work_path + '.old' + if os.path.exists(resource_work_path + '.old'): + for i in range(1, 100): + p = resource_path_old + '%02d' % i + if not os.path.exists(p): + # Rename existing resource file to + # `resource_name.old` + 2 digits + shutil.move(resource_work_path, p) + break + else: + self.log.warning( + 'There are a hundred old files for ' + 'resource "{}". ' + 'Perhaps is it time to clean up your ' + 'resources folder' + .format(resource_basename) + ) + continue + else: + # Rename existing resource file to `resource_name.old` + shutil.move(resource_work_path, resource_path_old) - # Copy resource file to workfile resources folder - shutil.copy(resource_main_path, resources_dir) + # Copy resource file to workfile resources folder + shutil.copy(resource_main_path, resources_dir) From 4ff71554d3c7f76c753f3c6e0796f367b3548dcb Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 9 Oct 2023 17:16:40 +0200 Subject: [PATCH 0222/1233] nuke: ocio look loader wip --- .../hosts/nuke/plugins/load/load_ociolook.py | 260 ++++++++++++++++++ 1 file changed, 260 insertions(+) create mode 100644 openpype/hosts/nuke/plugins/load/load_ociolook.py diff --git a/openpype/hosts/nuke/plugins/load/load_ociolook.py b/openpype/hosts/nuke/plugins/load/load_ociolook.py new file mode 100644 index 0000000000..76216e14cc --- /dev/null +++ b/openpype/hosts/nuke/plugins/load/load_ociolook.py @@ -0,0 +1,260 @@ +import json +from collections import OrderedDict +import nuke +import six + +from openpype.client import ( + get_version_by_id, + get_last_version_by_subset_id, +) +from openpype.pipeline import ( + load, + get_current_project_name, + get_representation_path, +) +from openpype.hosts.nuke.api import ( + containerise, + update_container, + viewer_update_and_undo_stop +) + + +class LoadOcioLook(load.LoaderPlugin): + """Loading Ocio look to the nuke node graph""" + + families = ["ociolook"] + representations = ["*"] + extension = {"json"} + + label = "Load OcioLook" + order = 0 + icon = "cc" + color = "white" + ignore_attr = ["useLifetime"] + + # json file variables + schema_version = 1 + + + def load(self, context, name, namespace, data): + """ + Loading function to get the soft effects to particular read node + + Arguments: + context (dict): context of version + name (str): name of the version + namespace (str): asset name + data (dict): compulsory attribute > not used + + Returns: + nuke node: containerised nuke node object + """ + # get main variables + version = context['version'] + version_data = version.get("data", {}) + vname = version.get("name", None) + root_working_colorspace = nuke.root()["working_colorspace"].value() + + namespace = namespace or context['asset']['name'] + object_name = "{}_{}".format(name, namespace) + + data_imprint = { + "version": vname, + "objectName": object_name, + "source": version_data.get("source", None), + "author": version_data.get("author", None), + "fps": version_data.get("fps", None), + } + + # getting file path + file = self.filepath_from_context(context).replace("\\", "/") + + # getting data from json file with unicode conversion + with open(file, "r") as f: + json_f = { + self.byteify(key): self.byteify(value) + for key, value in json.load(f).items() + } + + # check if the version in json_f is the same as plugin version + if json_f["version"] != self.schema_version: + raise KeyError( + "Version of json file is not the same as plugin version") + + json_data = json_f["data"] + ocio_working_colorspace = json_data["ocioLookWorkingSpace"] + + # adding nodes to node graph + # just in case we are in group lets jump out of it + nuke.endGroup() + + GN = nuke.createNode( + "Group", + "name {}_1".format(object_name), + inpanel=False + ) + + # adding content to the group node + with GN: + pre_colorspace = root_working_colorspace + pre_node = nuke.createNode("Input") + pre_node["name"].setValue("rgb") + + # Compare script working colorspace with ocio working colorspace + # found in json file and convert to json's if needed + if pre_colorspace != ocio_working_colorspace: + pre_node = _add_ocio_colorspace_node( + pre_node, + pre_colorspace, + ocio_working_colorspace + ) + pre_colorspace = ocio_working_colorspace + + for ocio_item in json_data["ocioLookItems"]: + input_space = _colorspace_name_by_type( + ocio_item["input_colorspace"]) + output_space = _colorspace_name_by_type( + ocio_item["output_colorspace"]) + + # making sure we are set to correct colorspace for otio item + if pre_colorspace != input_space: + pre_node = _add_ocio_colorspace_node( + pre_node, + pre_colorspace, + input_space + ) + + node = nuke.createNode("OCIOFileTransform") + + # TODO: file path from lut representation + node["file"].setValue(ocio_item["file"]) + node["name"].setValue(ocio_item["name"]) + node["direction"].setValue(ocio_item["direction"]) + node["interpolation"].setValue(ocio_item["interpolation"]) + node["working_space"].setValue(input_space) + + node.setInput(0, pre_node) + # pass output space into pre_colorspace for next iteration + # or for output node comparison + pre_colorspace = output_space + pre_node = node + + # making sure we are back in script working colorspace + if pre_colorspace != root_working_colorspace: + pre_node = _add_ocio_colorspace_node( + pre_node, + pre_colorspace, + root_working_colorspace + ) + + output = nuke.createNode("Output") + output.setInput(0, pre_node) + + GN["tile_color"].setValue(int("0x3469ffff", 16)) + + self.log.info("Loaded lut setup: `{}`".format(GN["name"].value())) + + return containerise( + node=GN, + name=name, + namespace=namespace, + context=context, + loader=self.__class__.__name__, + data=data_imprint) + + def update(self, container, representation): + """Update the Loader's path + + Nuke automatically tries to reset some variables when changing + the loader's path to a new file. These automatic changes are to its + inputs: + + """ + # get main variables + # Get version from io + project_name = get_current_project_name() + version_doc = get_version_by_id(project_name, representation["parent"]) + + # get corresponding node + GN = nuke.toNode(container['objectName']) + + file = get_representation_path(representation).replace("\\", "/") + name = container['name'] + version_data = version_doc.get("data", {}) + vname = version_doc.get("name", None) + namespace = container['namespace'] + object_name = "{}_{}".format(name, namespace) + + + def byteify(self, input): + """ + Converts unicode strings to strings + It goes through all dictionary + + Arguments: + input (dict/str): input + + Returns: + dict: with fixed values and keys + + """ + + if isinstance(input, dict): + return {self.byteify(key): self.byteify(value) + for key, value in input.items()} + elif isinstance(input, list): + return [self.byteify(element) for element in input] + elif isinstance(input, six.text_type): + return str(input) + else: + return input + + def switch(self, container, representation): + self.update(container, representation) + + def remove(self, container): + node = nuke.toNode(container['objectName']) + with viewer_update_and_undo_stop(): + nuke.delete(node) + + +def _colorspace_name_by_type(colorspace_data): + """ + Returns colorspace name by type + + Arguments: + colorspace_data (dict): colorspace data + + Returns: + str: colorspace name + """ + if colorspace_data["type"] == "colorspaces": + return colorspace_data["name"] + elif colorspace_data["type"] == "roles": + return colorspace_data["colorspace"] + else: + raise KeyError("Unknown colorspace type: {}".format( + colorspace_data["type"])) + + + + +def _add_ocio_colorspace_node(pre_node, input_space, output_space): + """ + Adds OCIOColorSpace node to the node graph + + Arguments: + pre_node (nuke node): node to connect to + input_space (str): input colorspace + output_space (str): output colorspace + + Returns: + nuke node: node with OCIOColorSpace node + """ + node = nuke.createNode("OCIOColorSpace") + node.setInput(0, pre_node) + node["in_colorspace"].setValue(input_space) + node["out_colorspace"].setValue(output_space) + + node.setInput(0, pre_node) + return node From 5e01929cb652725cd8999854ed5c6ac3cdb5667b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 9 Oct 2023 17:35:25 +0200 Subject: [PATCH 0223/1233] ocio look loader wip2: final loader --- .../hosts/nuke/plugins/load/load_ociolook.py | 52 ++++++++++++------- 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/openpype/hosts/nuke/plugins/load/load_ociolook.py b/openpype/hosts/nuke/plugins/load/load_ociolook.py index 76216e14cc..9f5a68dfc4 100644 --- a/openpype/hosts/nuke/plugins/load/load_ociolook.py +++ b/openpype/hosts/nuke/plugins/load/load_ociolook.py @@ -1,12 +1,9 @@ +import os import json -from collections import OrderedDict import nuke import six -from openpype.client import ( - get_version_by_id, - get_last_version_by_subset_id, -) +from openpype.client import get_version_by_id from openpype.pipeline import ( load, get_current_project_name, @@ -19,14 +16,14 @@ from openpype.hosts.nuke.api import ( ) -class LoadOcioLook(load.LoaderPlugin): +class LoadOcioLookNodes(load.LoaderPlugin): """Loading Ocio look to the nuke node graph""" families = ["ociolook"] representations = ["*"] - extension = {"json"} + extensions = {"json"} - label = "Load OcioLook" + label = "Load OcioLook [nodes]" order = 0 icon = "cc" color = "white" @@ -47,13 +44,13 @@ class LoadOcioLook(load.LoaderPlugin): data (dict): compulsory attribute > not used Returns: - nuke node: containerised nuke node object + nuke node: containerized nuke node object """ # get main variables version = context['version'] version_data = version.get("data", {}) vname = version.get("name", None) - root_working_colorspace = nuke.root()["working_colorspace"].value() + root_working_colorspace = nuke.root()["workingSpaceLUT"].value() namespace = namespace or context['asset']['name'] object_name = "{}_{}".format(name, namespace) @@ -67,14 +64,16 @@ class LoadOcioLook(load.LoaderPlugin): } # getting file path - file = self.filepath_from_context(context).replace("\\", "/") + file = self.filepath_from_context(context) + print(file) + + dir_path = os.path.dirname(file) + all_files = os.listdir(dir_path) # getting data from json file with unicode conversion with open(file, "r") as f: - json_f = { - self.byteify(key): self.byteify(value) - for key, value in json.load(f).items() - } + json_f = {self.bytify(key): self.bytify(value) + for key, value in json.load(f).items()} # check if the version in json_f is the same as plugin version if json_f["version"] != self.schema_version: @@ -82,7 +81,8 @@ class LoadOcioLook(load.LoaderPlugin): "Version of json file is not the same as plugin version") json_data = json_f["data"] - ocio_working_colorspace = json_data["ocioLookWorkingSpace"] + ocio_working_colorspace = _colorspace_name_by_type( + json_data["ocioLookWorkingSpace"]) # adding nodes to node graph # just in case we are in group lets jump out of it @@ -127,7 +127,19 @@ class LoadOcioLook(load.LoaderPlugin): node = nuke.createNode("OCIOFileTransform") # TODO: file path from lut representation - node["file"].setValue(ocio_item["file"]) + extension = ocio_item["ext"] + item_lut_file = next( + (file for file in all_files if file.endswith(extension)), + None + ) + if not item_lut_file: + raise ValueError( + "File with extension {} not found in directory".format( + extension)) + + item_lut_path = os.path.join( + dir_path, item_lut_file).replace("\\", "/") + node["file"].setValue(item_lut_path) node["name"].setValue(ocio_item["name"]) node["direction"].setValue(ocio_item["direction"]) node["interpolation"].setValue(ocio_item["interpolation"]) @@ -186,7 +198,7 @@ class LoadOcioLook(load.LoaderPlugin): object_name = "{}_{}".format(name, namespace) - def byteify(self, input): + def bytify(self, input): """ Converts unicode strings to strings It goes through all dictionary @@ -200,10 +212,10 @@ class LoadOcioLook(load.LoaderPlugin): """ if isinstance(input, dict): - return {self.byteify(key): self.byteify(value) + return {self.bytify(key): self.bytify(value) for key, value in input.items()} elif isinstance(input, list): - return [self.byteify(element) for element in input] + return [self.bytify(element) for element in input] elif isinstance(input, six.text_type): return str(input) else: From ca1492c839d91d05d81ec5f9ec063be3552ce8b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A0=20Serra=20Arrizabalaga?= Date: Mon, 9 Oct 2023 17:36:11 +0200 Subject: [PATCH 0224/1233] General: Avoid fallback if value is 0 for handle start/end (#5652) * Change defaults for handleStart so if it returns 0 it doesn't fallback to the context data * Update get fallbacks for the rest of arguments * Create context variable to shorten lines * Add step to TimeData object --- openpype/pipeline/farm/pyblish_functions.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/openpype/pipeline/farm/pyblish_functions.py b/openpype/pipeline/farm/pyblish_functions.py index fe3ab97de8..7ef3439dbd 100644 --- a/openpype/pipeline/farm/pyblish_functions.py +++ b/openpype/pipeline/farm/pyblish_functions.py @@ -107,17 +107,18 @@ def get_time_data_from_instance_or_context(instance): TimeData: dataclass holding time information. """ + context = instance.context return TimeData( - start=(instance.data.get("frameStart") or - instance.context.data.get("frameStart")), - end=(instance.data.get("frameEnd") or - instance.context.data.get("frameEnd")), - fps=(instance.data.get("fps") or - instance.context.data.get("fps")), - handle_start=(instance.data.get("handleStart") or - instance.context.data.get("handleStart")), # noqa: E501 - handle_end=(instance.data.get("handleEnd") or - instance.context.data.get("handleEnd")) + start=instance.data.get("frameStart", context.data.get("frameStart")), + end=instance.data.get("frameEnd", context.data.get("frameEnd")), + fps=instance.data.get("fps", context.data.get("fps")), + step=instance.data.get("byFrameStep", instance.data.get("step", 1)), + handle_start=instance.data.get( + "handleStart", context.data.get("handleStart") + ), + handle_end=instance.data.get( + "handleEnd", context.data.get("handleEnd") + ) ) From b59dd55726a8df09a7d91f4ed6ef05179e58a015 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Mon, 9 Oct 2023 17:36:56 +0100 Subject: [PATCH 0225/1233] Update openpype/settings/defaults/system_settings/applications.json Co-authored-by: Kayla Man <64118225+moonyuet@users.noreply.github.com> --- openpype/settings/defaults/system_settings/applications.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index b100704ffe..2cb75a9515 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -157,7 +157,7 @@ ], "darwin": [], "linux": [ - "/usr/autodesk/maya2024/bin/mayapy" + "/usr/autodesk/maya2023/bin/mayapy" ] }, "arguments": { From f2772d58574c6c6845671f8c3f42f2499a56cad2 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 9 Oct 2023 17:41:35 +0100 Subject: [PATCH 0226/1233] AYON settings --- .../applications/server/applications.json | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/server_addon/applications/server/applications.json b/server_addon/applications/server/applications.json index e40b8d41f6..60305cf1c4 100644 --- a/server_addon/applications/server/applications.json +++ b/server_addon/applications/server/applications.json @@ -109,6 +109,55 @@ } ] }, + "maya": { + "enabled": true, + "label": "Maya", + "icon": "{}/app_icons/maya.png", + "host_name": "maya", + "environment": "{\n \"MAYA_DISABLE_CLIC_IPM\": \"Yes\",\n \"MAYA_DISABLE_CIP\": \"Yes\",\n \"MAYA_DISABLE_CER\": \"Yes\",\n \"PYMEL_SKIP_MEL_INIT\": \"Yes\",\n \"LC_ALL\": \"C\"\n}\n", + "variants": [ + { + "name": "2024", + "label": "2024", + "executables": { + "windows": [ + "C:\\Program Files\\Autodesk\\Maya2024\\bin\\mayapy.exe" + ], + "darwin": [], + "linux": [ + "/usr/autodesk/maya2024/bin/mayapy" + ] + }, + "arguments": { + "windows": [], + "darwin": [], + "linux": [] + }, + "environment": "{\n \"MAYA_VERSION\": \"2024\"\n}", + "use_python_2": false + }, + { + "name": "2023", + "label": "2023", + "executables": { + "windows": [ + "C:\\Program Files\\Autodesk\\Maya2023\\bin\\mayapy.exe" + ], + "darwin": [], + "linux": [ + "/usr/autodesk/maya2023/bin/mayapy" + ] + }, + "arguments": { + "windows": [], + "darwin": [], + "linux": [] + }, + "environment": "{\n \"MAYA_VERSION\": \"2023\"\n}", + "use_python_2": false + } + ] + }, "adsk_3dsmax": { "enabled": true, "label": "3ds Max", From 31ffb5e8260e7c41b0d19c3092ebf7d97d790e18 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 9 Oct 2023 18:26:29 +0100 Subject: [PATCH 0227/1233] Ingest Maya usersetup --- .../hosts/maya/input/startup/userSetup.py | 26 +++++++++++++++++++ tests/integration/hosts/maya/lib.py | 16 ++++++------ 2 files changed, 34 insertions(+), 8 deletions(-) create mode 100644 tests/integration/hosts/maya/input/startup/userSetup.py diff --git a/tests/integration/hosts/maya/input/startup/userSetup.py b/tests/integration/hosts/maya/input/startup/userSetup.py new file mode 100644 index 0000000000..6914b41b1a --- /dev/null +++ b/tests/integration/hosts/maya/input/startup/userSetup.py @@ -0,0 +1,26 @@ +import logging +import sys + +from maya import cmds + + +def setup_pyblish_logging(): + log = logging.getLogger("pyblish") + hnd = logging.StreamHandler(sys.stdout) + fmt = logging.Formatter( + "pyblish (%(levelname)s) (line: %(lineno)d) %(name)s:" + "\n%(message)s" + ) + hnd.setFormatter(fmt) + log.addHandler(hnd) + + +def main(): + cmds.evalDeferred("setup_pyblish_logging()", evaluateNext=True) + cmds.evalDeferred( + "import pyblish.util;pyblish.util.publish()", lowestPriority=True + ) + cmds.evalDeferred("cmds.quit(force=True)", lowestPriority=True) + + +main() diff --git a/tests/integration/hosts/maya/lib.py b/tests/integration/hosts/maya/lib.py index e7480e25fa..f27d516605 100644 --- a/tests/integration/hosts/maya/lib.py +++ b/tests/integration/hosts/maya/lib.py @@ -33,16 +33,16 @@ class MayaHostFixtures(HostFixtures): yield dest_path @pytest.fixture(scope="module") - def startup_scripts(self, monkeypatch_session, download_test_data): + def startup_scripts(self, monkeypatch_session): """Points Maya to userSetup file from input data""" - startup_path = os.path.join(download_test_data, - "input", - "startup") + startup_path = os.path.join( + os.path.dirname(__file__), "input", "startup" + ) original_pythonpath = os.environ.get("PYTHONPATH") - monkeypatch_session.setenv("PYTHONPATH", - "{}{}{}".format(startup_path, - os.pathsep, - original_pythonpath)) + monkeypatch_session.setenv( + "PYTHONPATH", + "{}{}{}".format(startup_path, os.pathsep, original_pythonpath) + ) @pytest.fixture(scope="module") def skip_compare_folders(self): From 2f2e100231089b3bb321ed1e72962b929f75621f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 10 Oct 2023 09:43:28 +0200 Subject: [PATCH 0228/1233] Fix Show in usdview loader action --- .../houdini/plugins/load/show_usdview.py | 35 +++++++++++-------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/openpype/hosts/houdini/plugins/load/show_usdview.py b/openpype/hosts/houdini/plugins/load/show_usdview.py index 7b03a0738a..d56c4acc4f 100644 --- a/openpype/hosts/houdini/plugins/load/show_usdview.py +++ b/openpype/hosts/houdini/plugins/load/show_usdview.py @@ -1,4 +1,5 @@ import os +import platform import subprocess from openpype.lib.vendor_bin_utils import find_executable @@ -8,17 +9,31 @@ from openpype.pipeline import load class ShowInUsdview(load.LoaderPlugin): """Open USD file in usdview""" - families = ["colorbleed.usd"] label = "Show in usdview" - representations = ["usd", "usda", "usdlc", "usdnc"] - order = 10 + representations = ["*"] + families = ["*"] + extensions = {"usd", "usda", "usdlc", "usdnc", "abc"} + order = 15 icon = "code-fork" color = "white" def load(self, context, name=None, namespace=None, data=None): + from pathlib import Path - usdview = find_executable("usdview") + if platform.system() == "Windows": + executable = "usdview.bat" + else: + executable = "usdview" + + usdview = find_executable(executable) + if not usdview: + raise RuntimeError("Unable to find usdview") + + # For some reason Windows can return the path like: + # C:/PROGRA~1/SIDEEF~1/HOUDIN~1.435/bin/usdview + # convert to resolved path so `subprocess` can take it + usdview = str(Path(usdview).resolve().as_posix()) filepath = self.filepath_from_context(context) filepath = os.path.normpath(filepath) @@ -30,14 +45,4 @@ class ShowInUsdview(load.LoaderPlugin): self.log.info("Start houdini variant of usdview...") - # For now avoid some pipeline environment variables that initialize - # Avalon in Houdini as it is redundant for usdview and slows boot time - env = os.environ.copy() - env.pop("PYTHONPATH", None) - env.pop("HOUDINI_SCRIPT_PATH", None) - env.pop("HOUDINI_MENU_PATH", None) - - # Force string to avoid unicode issues - env = {str(key): str(value) for key, value in env.items()} - - subprocess.Popen([usdview, filepath, "--renderer", "GL"], env=env) + subprocess.Popen([usdview, filepath, "--renderer", "GL"]) From 067aa2ca4d4961e6d00c13c80ea67ed4045ae2b7 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 10 Oct 2023 10:49:39 +0200 Subject: [PATCH 0229/1233] Bugfix: ServerDeleteOperation asset -> folder conversion typo (#5735) * Fix typo * Fix docstring typos --- openpype/client/server/operations.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/client/server/operations.py b/openpype/client/server/operations.py index eeb55784e1..5b38405c34 100644 --- a/openpype/client/server/operations.py +++ b/openpype/client/server/operations.py @@ -422,7 +422,7 @@ def failed_json_default(value): class ServerCreateOperation(CreateOperation): - """Opeartion to create an entity. + """Operation to create an entity. Args: project_name (str): On which project operation will happen. @@ -634,7 +634,7 @@ class ServerUpdateOperation(UpdateOperation): class ServerDeleteOperation(DeleteOperation): - """Opeartion to delete an entity. + """Operation to delete an entity. Args: project_name (str): On which project operation will happen. @@ -647,7 +647,7 @@ class ServerDeleteOperation(DeleteOperation): self._session = session if entity_type == "asset": - entity_type == "folder" + entity_type = "folder" elif entity_type == "hero_version": entity_type = "version" From bb4134d96a5cc14a74285fb9931f100b89e0ca1f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 10 Oct 2023 13:43:46 +0200 Subject: [PATCH 0230/1233] fixing variable name to be plural --- openpype/hosts/nuke/plugins/load/actions.py | 2 +- openpype/hosts/nuke/plugins/load/load_backdrop.py | 2 +- openpype/hosts/nuke/plugins/load/load_camera_abc.py | 2 +- openpype/hosts/nuke/plugins/load/load_effects.py | 2 +- openpype/hosts/nuke/plugins/load/load_effects_ip.py | 2 +- openpype/hosts/nuke/plugins/load/load_gizmo.py | 2 +- openpype/hosts/nuke/plugins/load/load_gizmo_ip.py | 2 +- openpype/hosts/nuke/plugins/load/load_matchmove.py | 2 +- openpype/hosts/nuke/plugins/load/load_model.py | 2 +- openpype/hosts/nuke/plugins/load/load_script_precomp.py | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/nuke/plugins/load/actions.py b/openpype/hosts/nuke/plugins/load/actions.py index 3227a7ed98..635318f53d 100644 --- a/openpype/hosts/nuke/plugins/load/actions.py +++ b/openpype/hosts/nuke/plugins/load/actions.py @@ -17,7 +17,7 @@ class SetFrameRangeLoader(load.LoaderPlugin): "yeticache", "pointcache"] representations = ["*"] - extension = {"*"} + extensions = {"*"} label = "Set frame range" order = 11 diff --git a/openpype/hosts/nuke/plugins/load/load_backdrop.py b/openpype/hosts/nuke/plugins/load/load_backdrop.py index fe82d70b5e..0cbd380697 100644 --- a/openpype/hosts/nuke/plugins/load/load_backdrop.py +++ b/openpype/hosts/nuke/plugins/load/load_backdrop.py @@ -27,7 +27,7 @@ class LoadBackdropNodes(load.LoaderPlugin): families = ["workfile", "nukenodes"] representations = ["*"] - extension = {"nk"} + extensions = {"nk"} label = "Import Nuke Nodes" order = 0 diff --git a/openpype/hosts/nuke/plugins/load/load_camera_abc.py b/openpype/hosts/nuke/plugins/load/load_camera_abc.py index 2939ceebae..e245b0cb5e 100644 --- a/openpype/hosts/nuke/plugins/load/load_camera_abc.py +++ b/openpype/hosts/nuke/plugins/load/load_camera_abc.py @@ -26,7 +26,7 @@ class AlembicCameraLoader(load.LoaderPlugin): families = ["camera"] representations = ["*"] - extension = {"abc"} + extensions = {"abc"} label = "Load Alembic Camera" icon = "camera" diff --git a/openpype/hosts/nuke/plugins/load/load_effects.py b/openpype/hosts/nuke/plugins/load/load_effects.py index 89597e76cc..cacc00854e 100644 --- a/openpype/hosts/nuke/plugins/load/load_effects.py +++ b/openpype/hosts/nuke/plugins/load/load_effects.py @@ -24,7 +24,7 @@ class LoadEffects(load.LoaderPlugin): families = ["effect"] representations = ["*"] - extension = {"json"} + extensions = {"json"} label = "Load Effects - nodes" order = 0 diff --git a/openpype/hosts/nuke/plugins/load/load_effects_ip.py b/openpype/hosts/nuke/plugins/load/load_effects_ip.py index efe67be4aa..bdf3cd6965 100644 --- a/openpype/hosts/nuke/plugins/load/load_effects_ip.py +++ b/openpype/hosts/nuke/plugins/load/load_effects_ip.py @@ -25,7 +25,7 @@ class LoadEffectsInputProcess(load.LoaderPlugin): families = ["effect"] representations = ["*"] - extension = {"json"} + extensions = {"json"} label = "Load Effects - Input Process" order = 0 diff --git a/openpype/hosts/nuke/plugins/load/load_gizmo.py b/openpype/hosts/nuke/plugins/load/load_gizmo.py index 6b848ee276..ede05c422b 100644 --- a/openpype/hosts/nuke/plugins/load/load_gizmo.py +++ b/openpype/hosts/nuke/plugins/load/load_gizmo.py @@ -26,7 +26,7 @@ class LoadGizmo(load.LoaderPlugin): families = ["gizmo"] representations = ["*"] - extension = {"gizmo"} + extensions = {"gizmo"} label = "Load Gizmo" order = 0 diff --git a/openpype/hosts/nuke/plugins/load/load_gizmo_ip.py b/openpype/hosts/nuke/plugins/load/load_gizmo_ip.py index a8e1218cbe..d567aaf7b0 100644 --- a/openpype/hosts/nuke/plugins/load/load_gizmo_ip.py +++ b/openpype/hosts/nuke/plugins/load/load_gizmo_ip.py @@ -28,7 +28,7 @@ class LoadGizmoInputProcess(load.LoaderPlugin): families = ["gizmo"] representations = ["*"] - extension = {"gizmo"} + extensions = {"gizmo"} label = "Load Gizmo - Input Process" order = 0 diff --git a/openpype/hosts/nuke/plugins/load/load_matchmove.py b/openpype/hosts/nuke/plugins/load/load_matchmove.py index f942422c00..14ddf20dc3 100644 --- a/openpype/hosts/nuke/plugins/load/load_matchmove.py +++ b/openpype/hosts/nuke/plugins/load/load_matchmove.py @@ -9,7 +9,7 @@ class MatchmoveLoader(load.LoaderPlugin): families = ["matchmove"] representations = ["*"] - extension = {"py"} + extensions = {"py"} defaults = ["Camera", "Object"] diff --git a/openpype/hosts/nuke/plugins/load/load_model.py b/openpype/hosts/nuke/plugins/load/load_model.py index 0bdcd93dff..b9b8a0f4c0 100644 --- a/openpype/hosts/nuke/plugins/load/load_model.py +++ b/openpype/hosts/nuke/plugins/load/load_model.py @@ -24,7 +24,7 @@ class AlembicModelLoader(load.LoaderPlugin): families = ["model", "pointcache", "animation"] representations = ["*"] - extension = {"abc"} + extensions = {"abc"} label = "Load Alembic" icon = "cube" diff --git a/openpype/hosts/nuke/plugins/load/load_script_precomp.py b/openpype/hosts/nuke/plugins/load/load_script_precomp.py index 48d4a0900a..d5f9d24765 100644 --- a/openpype/hosts/nuke/plugins/load/load_script_precomp.py +++ b/openpype/hosts/nuke/plugins/load/load_script_precomp.py @@ -22,7 +22,7 @@ class LinkAsGroup(load.LoaderPlugin): families = ["workfile", "nukenodes"] representations = ["*"] - extension = {"nk"} + extensions = {"nk"} label = "Load Precomp" order = 0 From 58e5cf20b3023ea0c440304b2ba5184af6110312 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 10 Oct 2023 14:34:35 +0200 Subject: [PATCH 0231/1233] loader ociolook with updating --- .../hosts/nuke/plugins/load/load_ociolook.py | 209 +++++++++++------- 1 file changed, 130 insertions(+), 79 deletions(-) diff --git a/openpype/hosts/nuke/plugins/load/load_ociolook.py b/openpype/hosts/nuke/plugins/load/load_ociolook.py index 9f5a68dfc4..6cf9236e1b 100644 --- a/openpype/hosts/nuke/plugins/load/load_ociolook.py +++ b/openpype/hosts/nuke/plugins/load/load_ociolook.py @@ -1,5 +1,6 @@ import os import json +import secrets import nuke import six @@ -17,7 +18,7 @@ from openpype.hosts.nuke.api import ( class LoadOcioLookNodes(load.LoaderPlugin): - """Loading Ocio look to the nuke node graph""" + """Loading Ocio look to the nuke.Node graph""" families = ["ociolook"] representations = ["*"] @@ -27,7 +28,7 @@ class LoadOcioLookNodes(load.LoaderPlugin): order = 0 icon = "cc" color = "white" - ignore_attr = ["useLifetime"] + igroup_nodeore_attr = ["useLifetime"] # json file variables schema_version = 1 @@ -44,61 +45,98 @@ class LoadOcioLookNodes(load.LoaderPlugin): data (dict): compulsory attribute > not used Returns: - nuke node: containerized nuke node object + nuke.Node: containerized nuke.Node object """ - # get main variables - version = context['version'] - version_data = version.get("data", {}) - vname = version.get("name", None) - root_working_colorspace = nuke.root()["workingSpaceLUT"].value() - namespace = namespace or context['asset']['name'] - object_name = "{}_{}".format(name, namespace) - - data_imprint = { - "version": vname, - "objectName": object_name, - "source": version_data.get("source", None), - "author": version_data.get("author", None), - "fps": version_data.get("fps", None), - } + suffix = secrets.token_hex(nbytes=4) + object_name = "{}_{}_{}".format( + name, namespace, suffix) # getting file path - file = self.filepath_from_context(context) - print(file) + filepath = self.filepath_from_context(context) - dir_path = os.path.dirname(file) + json_f = self._load_json_data(filepath) + + group_node = self._create_group_node( + object_name, filepath, json_f["data"]) + + group_node["tile_color"].setValue(int("0x3469ffff", 16)) + + self.log.info("Loaded lut setup: `{}`".format(group_node["name"].value())) + + return containerise( + node=group_node, + name=name, + namespace=namespace, + context=context, + loader=self.__class__.__name__, + data={ + "objectName": object_name, + } + ) + + def _create_group_node( + self, + object_name, + filepath, + data + ): + """Creates group node with all the nodes inside. + + Creating mainly `OCIOFileTransform` nodes with `OCIOColorSpace` nodes + in between - in case those are needed. + + Arguments: + object_name (str): name of the group node + filepath (str): path to json file + data (dict): data from json file + + Returns: + nuke.Node: group node with all the nodes inside + """ + # get corresponding node + + root_working_colorspace = nuke.root()["workingSpaceLUT"].value() + + dir_path = os.path.dirname(filepath) all_files = os.listdir(dir_path) - # getting data from json file with unicode conversion - with open(file, "r") as f: - json_f = {self.bytify(key): self.bytify(value) - for key, value in json.load(f).items()} - - # check if the version in json_f is the same as plugin version - if json_f["version"] != self.schema_version: - raise KeyError( - "Version of json file is not the same as plugin version") - - json_data = json_f["data"] ocio_working_colorspace = _colorspace_name_by_type( - json_data["ocioLookWorkingSpace"]) + data["ocioLookWorkingSpace"]) # adding nodes to node graph # just in case we are in group lets jump out of it nuke.endGroup() - GN = nuke.createNode( - "Group", - "name {}_1".format(object_name), - inpanel=False - ) + input_node = None + output_node = None + group_node = nuke.toNode(object_name) + if group_node: + # remove all nodes between Input and Output nodes + for node in group_node.nodes(): + if node.Class() not in ["Input", "Output"]: + nuke.delete(node) + if node.Class() == "Input": + input_node = node + if node.Class() == "Output": + output_node = node + else: + group_node = nuke.createNode( + "Group", + "name {}_1".format(object_name), + inpanel=False + ) # adding content to the group node - with GN: + with group_node: pre_colorspace = root_working_colorspace - pre_node = nuke.createNode("Input") - pre_node["name"].setValue("rgb") + + # reusing input node if it exists during update + if input_node: + pre_node = input_node + else: + pre_node = nuke.createNode("Input") + pre_node["name"].setValue("rgb") # Compare script working colorspace with ocio working colorspace # found in json file and convert to json's if needed @@ -110,7 +148,7 @@ class LoadOcioLookNodes(load.LoaderPlugin): ) pre_colorspace = ocio_working_colorspace - for ocio_item in json_data["ocioLookItems"]: + for ocio_item in data["ocioLookItems"]: input_space = _colorspace_name_by_type( ocio_item["input_colorspace"]) output_space = _colorspace_name_by_type( @@ -126,10 +164,16 @@ class LoadOcioLookNodes(load.LoaderPlugin): node = nuke.createNode("OCIOFileTransform") - # TODO: file path from lut representation + # file path from lut representation extension = ocio_item["ext"] + item_name = ocio_item["name"] + item_lut_file = next( - (file for file in all_files if file.endswith(extension)), + ( + file for file in all_files + if file.endswith(extension) + and item_name in file + ), None ) if not item_lut_file: @@ -140,12 +184,14 @@ class LoadOcioLookNodes(load.LoaderPlugin): item_lut_path = os.path.join( dir_path, item_lut_file).replace("\\", "/") node["file"].setValue(item_lut_path) - node["name"].setValue(ocio_item["name"]) + node["name"].setValue(item_name) node["direction"].setValue(ocio_item["direction"]) node["interpolation"].setValue(ocio_item["interpolation"]) node["working_space"].setValue(input_space) + pre_node.autoplace() node.setInput(0, pre_node) + node.autoplace() # pass output space into pre_colorspace for next iteration # or for output node comparison pre_colorspace = output_space @@ -159,46 +205,48 @@ class LoadOcioLookNodes(load.LoaderPlugin): root_working_colorspace ) - output = nuke.createNode("Output") + # reusing output node if it exists during update + if not output_node: + output = nuke.createNode("Output") + else: + output = output_node + output.setInput(0, pre_node) - GN["tile_color"].setValue(int("0x3469ffff", 16)) - - self.log.info("Loaded lut setup: `{}`".format(GN["name"].value())) - - return containerise( - node=GN, - name=name, - namespace=namespace, - context=context, - loader=self.__class__.__name__, - data=data_imprint) + return group_node def update(self, container, representation): - """Update the Loader's path - Nuke automatically tries to reset some variables when changing - the loader's path to a new file. These automatic changes are to its - inputs: + object_name = container['objectName'] - """ - # get main variables - # Get version from io - project_name = get_current_project_name() - version_doc = get_version_by_id(project_name, representation["parent"]) + filepath = get_representation_path(representation) - # get corresponding node - GN = nuke.toNode(container['objectName']) + json_f = self._load_json_data(filepath) - file = get_representation_path(representation).replace("\\", "/") - name = container['name'] - version_data = version_doc.get("data", {}) - vname = version_doc.get("name", None) - namespace = container['namespace'] - object_name = "{}_{}".format(name, namespace) + new_group_node = self._create_group_node( + object_name, + filepath, + json_f["data"] + ) + + self.log.info("Updated lut setup: `{}`".format( + new_group_node["name"].value())) - def bytify(self, input): + def _load_json_data(self, filepath): + # getting data from json file with unicode conversion + with open(filepath, "r") as _file: + json_f = {self._bytify(key): self._bytify(value) + for key, value in json.load(_file).items()} + + # check if the version in json_f is the same as plugin version + if json_f["version"] != self.schema_version: + raise KeyError( + "Version of json file is not the same as plugin version") + + return json_f + + def _bytify(self, input): """ Converts unicode strings to strings It goes through all dictionary @@ -212,10 +260,10 @@ class LoadOcioLookNodes(load.LoaderPlugin): """ if isinstance(input, dict): - return {self.bytify(key): self.bytify(value) + return {self._bytify(key): self._bytify(value) for key, value in input.items()} elif isinstance(input, list): - return [self.bytify(element) for element in input] + return [self._bytify(element) for element in input] elif isinstance(input, six.text_type): return str(input) else: @@ -256,17 +304,20 @@ def _add_ocio_colorspace_node(pre_node, input_space, output_space): Adds OCIOColorSpace node to the node graph Arguments: - pre_node (nuke node): node to connect to + pre_node (nuke.Node): node to connect to input_space (str): input colorspace output_space (str): output colorspace Returns: - nuke node: node with OCIOColorSpace node + nuke.Node: node with OCIOColorSpace node """ node = nuke.createNode("OCIOColorSpace") node.setInput(0, pre_node) node["in_colorspace"].setValue(input_space) node["out_colorspace"].setValue(output_space) + pre_node.autoplace() node.setInput(0, pre_node) + node.autoplace() + return node From e422d1900f10acc3c70bd5377a4930bedf2e329c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 10 Oct 2023 17:18:14 +0200 Subject: [PATCH 0232/1233] adding color to loaded nodes --- .../hosts/nuke/plugins/load/load_ociolook.py | 38 ++++++++++++++++--- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/nuke/plugins/load/load_ociolook.py b/openpype/hosts/nuke/plugins/load/load_ociolook.py index 6cf9236e1b..d2143b5527 100644 --- a/openpype/hosts/nuke/plugins/load/load_ociolook.py +++ b/openpype/hosts/nuke/plugins/load/load_ociolook.py @@ -4,7 +4,10 @@ import secrets import nuke import six -from openpype.client import get_version_by_id +from openpype.client import ( + get_version_by_id, + get_last_version_by_subset_id +) from openpype.pipeline import ( load, get_current_project_name, @@ -12,7 +15,6 @@ from openpype.pipeline import ( ) from openpype.hosts.nuke.api import ( containerise, - update_container, viewer_update_and_undo_stop ) @@ -28,7 +30,11 @@ class LoadOcioLookNodes(load.LoaderPlugin): order = 0 icon = "cc" color = "white" - igroup_nodeore_attr = ["useLifetime"] + ignore_attr = ["useLifetime"] + + # plugin attributes + current_node_color = "0x4ecd91ff" + old_node_color = "0xd88467ff" # json file variables schema_version = 1 @@ -60,7 +66,8 @@ class LoadOcioLookNodes(load.LoaderPlugin): group_node = self._create_group_node( object_name, filepath, json_f["data"]) - group_node["tile_color"].setValue(int("0x3469ffff", 16)) + + self._node_version_color(context["version"], group_node) self.log.info("Loaded lut setup: `{}`".format(group_node["name"].value())) @@ -217,20 +224,25 @@ class LoadOcioLookNodes(load.LoaderPlugin): def update(self, container, representation): + project_name = get_current_project_name() + version_doc = get_version_by_id(project_name, representation["parent"]) + object_name = container['objectName'] filepath = get_representation_path(representation) json_f = self._load_json_data(filepath) - new_group_node = self._create_group_node( + group_node = self._create_group_node( object_name, filepath, json_f["data"] ) + self._node_version_color(version_doc, group_node) + self.log.info("Updated lut setup: `{}`".format( - new_group_node["name"].value())) + group_node["name"].value())) def _load_json_data(self, filepath): @@ -277,6 +289,20 @@ class LoadOcioLookNodes(load.LoaderPlugin): with viewer_update_and_undo_stop(): nuke.delete(node) + def _node_version_color(self, version, node): + """ Coloring a node by correct color by actual version""" + + project_name = get_current_project_name() + last_version_doc = get_last_version_by_subset_id( + project_name, version["parent"], fields=["_id"] + ) + + # change color of node + if version["_id"] == last_version_doc["_id"]: + color_value = self.current_node_color + else: + color_value = self.old_node_color + node["tile_color"].setValue(int(color_value, 16)) def _colorspace_name_by_type(colorspace_data): """ From ac9ead71fdc5c0e326bd46132707fb9fd08cd20f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 10 Oct 2023 17:21:06 +0200 Subject: [PATCH 0233/1233] hound --- openpype/hosts/nuke/plugins/load/load_ociolook.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/nuke/plugins/load/load_ociolook.py b/openpype/hosts/nuke/plugins/load/load_ociolook.py index d2143b5527..29503ef4de 100644 --- a/openpype/hosts/nuke/plugins/load/load_ociolook.py +++ b/openpype/hosts/nuke/plugins/load/load_ociolook.py @@ -323,8 +323,6 @@ def _colorspace_name_by_type(colorspace_data): colorspace_data["type"])) - - def _add_ocio_colorspace_node(pre_node, input_space, output_space): """ Adds OCIOColorSpace node to the node graph From b49c04f5706a2d21755d646e02d80a3651a43f9b Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 10 Oct 2023 17:56:47 +0100 Subject: [PATCH 0234/1233] Rely less on deferred execution --- .../hosts/maya/input/startup/userSetup.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/tests/integration/hosts/maya/input/startup/userSetup.py b/tests/integration/hosts/maya/input/startup/userSetup.py index 6914b41b1a..67352af63d 100644 --- a/tests/integration/hosts/maya/input/startup/userSetup.py +++ b/tests/integration/hosts/maya/input/startup/userSetup.py @@ -3,6 +3,8 @@ import sys from maya import cmds +import pyblish.util + def setup_pyblish_logging(): log = logging.getLogger("pyblish") @@ -15,12 +17,12 @@ def setup_pyblish_logging(): log.addHandler(hnd) -def main(): - cmds.evalDeferred("setup_pyblish_logging()", evaluateNext=True) - cmds.evalDeferred( - "import pyblish.util;pyblish.util.publish()", lowestPriority=True - ) - cmds.evalDeferred("cmds.quit(force=True)", lowestPriority=True) +def _run_publish_test_deferred(): + try: + pyblish.util.publish() + finally: + cmds.quit(force=True) -main() +cmds.evalDeferred("setup_pyblish_logging()", evaluateNext=True) +cmds.evalDeferred("_run_publish_test_deferred()", lowestPriority=True) From 32000bd160657a784e9b74b171901d228f80a9ec Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 10 Oct 2023 22:32:22 +0200 Subject: [PATCH 0235/1233] reverting global abstraction --- openpype/pipeline/__init__.py | 2 -- openpype/pipeline/load/__init__.py | 2 -- openpype/pipeline/load/utils.py | 50 ------------------------------ 3 files changed, 54 deletions(-) diff --git a/openpype/pipeline/__init__.py b/openpype/pipeline/__init__.py index ca2a6bcf2c..8f370d389b 100644 --- a/openpype/pipeline/__init__.py +++ b/openpype/pipeline/__init__.py @@ -48,7 +48,6 @@ from .load import ( loaders_from_representation, get_representation_path, get_representation_context, - get_representation_files, get_repres_contexts, ) @@ -153,7 +152,6 @@ __all__ = ( "loaders_from_representation", "get_representation_path", "get_representation_context", - "get_representation_files", "get_repres_contexts", # --- Publish --- diff --git a/openpype/pipeline/load/__init__.py b/openpype/pipeline/load/__init__.py index c07388fd45..7320a9f0e8 100644 --- a/openpype/pipeline/load/__init__.py +++ b/openpype/pipeline/load/__init__.py @@ -11,7 +11,6 @@ from .utils import ( get_contexts_for_repre_docs, get_subset_contexts, get_representation_context, - get_representation_files, load_with_repre_context, load_with_subset_context, @@ -65,7 +64,6 @@ __all__ = ( "get_contexts_for_repre_docs", "get_subset_contexts", "get_representation_context", - "get_representation_files", "load_with_repre_context", "load_with_subset_context", diff --git a/openpype/pipeline/load/utils.py b/openpype/pipeline/load/utils.py index 5193eaa86e..b10d6032b3 100644 --- a/openpype/pipeline/load/utils.py +++ b/openpype/pipeline/load/utils.py @@ -1,6 +1,4 @@ import os -import re -import glob import platform import copy import getpass @@ -288,54 +286,6 @@ def get_representation_context(representation): return context -def get_representation_files(context, filepath): - """Return list of files for representation. - - Args: - context (dict): The full loading context. - filepath (str): Filepath of the representation. - - Returns: - list[str]: List of files for representation. - """ - version = context["version"] - frame_start = version["data"]["frameStart"] - frame_end = version["data"]["frameEnd"] - handle_start = version["data"]["handleStart"] - handle_end = version["data"]["handleEnd"] - - first_frame = frame_start - handle_start - last_frame = frame_end + handle_end - dir_path = os.path.dirname(filepath) - base_name = os.path.basename(filepath) - - # prepare glob pattern for searching - padding = len(str(last_frame)) - str_first_frame = str(first_frame).zfill(padding) - - # convert str_first_frame to glob pattern - # replace all digits with `?` and all other chars with `[char]` - # example: `0001` -> `????` - glob_pattern = re.sub(r"\d", "?", str_first_frame) - - # in filename replace number with glob pattern - # example: `filename.0001.exr` -> `filename.????.exr` - base_name = re.sub(str_first_frame, glob_pattern, base_name) - - files = [] - # get all files in folder - for file in glob.glob(os.path.join(dir_path, base_name)): - files.append(file) - - # keep only existing files - files = [f for f in files if os.path.exists(f)] - - # sort files by frame number - files.sort(key=lambda f: int(re.findall(r"\d+", f)[-1])) - - return files - - def load_with_repre_context( Loader, repre_context, namespace=None, name=None, options=None, **kwargs ): From 9145072d514d0ef33edd49a8245d412aa73a6379 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 10 Oct 2023 22:50:11 +0200 Subject: [PATCH 0236/1233] resolve: get representation files from host api plugin and as suggested here https://github.com/ynput/OpenPype/pull/5673#discussion_r1350315699 --- openpype/hosts/resolve/api/plugin.py | 10 ++++++++++ openpype/hosts/resolve/plugins/load/load_clip.py | 10 +++------- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py index f3a65034fb..a0dba6fd05 100644 --- a/openpype/hosts/resolve/api/plugin.py +++ b/openpype/hosts/resolve/api/plugin.py @@ -8,6 +8,7 @@ from openpype.pipeline.context_tools import get_current_project_asset from openpype.pipeline import ( LegacyCreator, LoaderPlugin, + Anatomy ) from . import lib @@ -825,3 +826,12 @@ class PublishClip: for key in par_split: parent = self._convert_to_entity(key) self.parents.append(parent) + + +def get_representation_files(representation): + anatomy = Anatomy() + files = [] + for file_data in representation["files"]: + path = anatomy.fill_root(file_data["path"]) + files.append(path) + return files diff --git a/openpype/hosts/resolve/plugins/load/load_clip.py b/openpype/hosts/resolve/plugins/load/load_clip.py index 35a6b97eea..d3f83c7f24 100644 --- a/openpype/hosts/resolve/plugins/load/load_clip.py +++ b/openpype/hosts/resolve/plugins/load/load_clip.py @@ -1,9 +1,7 @@ from openpype.client import get_last_version_by_subset_id from openpype.pipeline import ( - get_representation_path, get_representation_context, - get_current_project_name, - get_representation_files + get_current_project_name ) from openpype.hosts.resolve.api import lib, plugin from openpype.hosts.resolve.api.pipeline import ( @@ -45,8 +43,7 @@ class LoadClip(plugin.TimelineItemLoader): def load(self, context, name, namespace, options): # load clip to timeline and get main variables - filepath = self.filepath_from_context(context) - files = get_representation_files(context, filepath) + files = plugin.get_representation_files(context["representation"]) timeline_item = plugin.ClipLoader( self, context, **options).load(files) @@ -76,8 +73,7 @@ class LoadClip(plugin.TimelineItemLoader): media_pool_item = timeline_item.GetMediaPoolItem() - filepath = get_representation_path(representation) - files = get_representation_files(context, filepath) + files = plugin.get_representation_files(representation) loader = plugin.ClipLoader(self, context) timeline_item = loader.update(timeline_item, files) From a00d456cb1dbf99fc92e3fec5601ec32029f3012 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 11 Oct 2023 03:25:25 +0000 Subject: [PATCH 0237/1233] [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 01c000e54d..1a316df989 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.17.2-nightly.3" +__version__ = "3.17.2-nightly.4" From d1e1e591f2b0f6fb77cf1d119c9816a705e86723 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 11 Oct 2023 03:26:11 +0000 Subject: [PATCH 0238/1233] 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 78bea3d838..f74904f79d 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.17.2-nightly.4 - 3.17.2-nightly.3 - 3.17.2-nightly.2 - 3.17.2-nightly.1 @@ -134,7 +135,6 @@ body: - 3.14.11-nightly.1 - 3.14.10 - 3.14.10-nightly.9 - - 3.14.10-nightly.8 validations: required: true - type: dropdown From 519db56b8769a71f18083a1cb972c2e9ae0f567c Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 11 Oct 2023 09:38:48 +0300 Subject: [PATCH 0239/1233] re-arrange settings files --- .../{publish_plugins.py => create.py} | 85 +------------------ server_addon/houdini/server/settings/main.py | 64 +++----------- .../houdini/server/settings/publish.py | 84 ++++++++++++++++++ .../houdini/server/settings/shelves.py | 43 ++++++++++ server_addon/houdini/server/version.py | 2 +- 5 files changed, 141 insertions(+), 137 deletions(-) rename server_addon/houdini/server/settings/{publish_plugins.py => create.py} (61%) create mode 100644 server_addon/houdini/server/settings/publish.py create mode 100644 server_addon/houdini/server/settings/shelves.py diff --git a/server_addon/houdini/server/settings/publish_plugins.py b/server_addon/houdini/server/settings/create.py similarity index 61% rename from server_addon/houdini/server/settings/publish_plugins.py rename to server_addon/houdini/server/settings/create.py index 58240b0205..a1f8d24c30 100644 --- a/server_addon/houdini/server/settings/publish_plugins.py +++ b/server_addon/houdini/server/settings/create.py @@ -1,5 +1,4 @@ from pydantic import Field - from ayon_server.settings import BaseSettingsModel @@ -37,7 +36,7 @@ class CreateStaticMeshModel(BaseSettingsModel): class CreatePluginsModel(BaseSettingsModel): CreateArnoldAss: CreateArnoldAssModel = Field( default_factory=CreateArnoldAssModel, - title="Create Alembic Camera") + title="Create Arnold Ass") # "-" is not compatible in the new model CreateStaticMesh: CreateStaticMeshModel = Field( default_factory=CreateStaticMeshModel, @@ -135,85 +134,3 @@ DEFAULT_HOUDINI_CREATE_SETTINGS = { "default_variants": ["Main"] }, } - - -# Publish Plugins -class ValidateWorkfilePathsModel(BaseSettingsModel): - enabled: bool = Field(title="Enabled") - optional: bool = Field(title="Optional") - node_types: list[str] = Field( - default_factory=list, - title="Node Types" - ) - prohibited_vars: list[str] = Field( - default_factory=list, - title="Prohibited Variables" - ) - - -class BasicValidateModel(BaseSettingsModel): - enabled: bool = Field(title="Enabled") - optional: bool = Field(title="Optional") - active: bool = Field(title="Active") - - -class PublishPluginsModel(BaseSettingsModel): - ValidateWorkfilePaths: ValidateWorkfilePathsModel = Field( - default_factory=ValidateWorkfilePathsModel, - title="Validate workfile paths settings.") - ValidateReviewColorspace: BasicValidateModel = Field( - default_factory=BasicValidateModel, - title="Validate Review Colorspace.") - ValidateContainers: BasicValidateModel = Field( - default_factory=BasicValidateModel, - title="Validate Latest Containers.") - ValidateSubsetName: BasicValidateModel = Field( - default_factory=BasicValidateModel, - title="Validate Subset Name.") - ValidateMeshIsStatic: BasicValidateModel = Field( - default_factory=BasicValidateModel, - title="Validate Mesh is Static.") - ValidateUnrealStaticMeshName: BasicValidateModel = Field( - default_factory=BasicValidateModel, - title="Validate Unreal Static Mesh Name.") - - -DEFAULT_HOUDINI_PUBLISH_SETTINGS = { - "ValidateWorkfilePaths": { - "enabled": True, - "optional": True, - "node_types": [ - "file", - "alembic" - ], - "prohibited_vars": [ - "$HIP", - "$JOB" - ] - }, - "ValidateReviewColorspace": { - "enabled": True, - "optional": True, - "active": True - }, - "ValidateContainers": { - "enabled": True, - "optional": True, - "active": True - }, - "ValidateSubsetName": { - "enabled": True, - "optional": True, - "active": True - }, - "ValidateMeshIsStatic": { - "enabled": True, - "optional": True, - "active": True - }, - "ValidateUnrealStaticMeshName": { - "enabled": False, - "optional": True, - "active": True - } -} diff --git a/server_addon/houdini/server/settings/main.py b/server_addon/houdini/server/settings/main.py index 0c2e160c87..9cfec54f22 100644 --- a/server_addon/houdini/server/settings/main.py +++ b/server_addon/houdini/server/settings/main.py @@ -1,57 +1,19 @@ from pydantic import Field -from ayon_server.settings import ( - BaseSettingsModel, - MultiplatformPathModel, - MultiplatformPathListModel, -) +from ayon_server.settings import BaseSettingsModel from .general import ( GeneralSettingsModel, DEFAULT_GENERAL_SETTINGS ) from .imageio import HoudiniImageIOModel -from .publish_plugins import ( - PublishPluginsModel, +from .shelves import ShelvesModel +from .create import ( CreatePluginsModel, - DEFAULT_HOUDINI_PUBLISH_SETTINGS, DEFAULT_HOUDINI_CREATE_SETTINGS ) - - -class ShelfToolsModel(BaseSettingsModel): - name: str = Field(title="Name") - help: str = Field(title="Help text") - script: MultiplatformPathModel = Field( - default_factory=MultiplatformPathModel, - title="Script Path " - ) - icon: MultiplatformPathModel = Field( - default_factory=MultiplatformPathModel, - title="Icon Path " - ) - - -class ShelfDefinitionModel(BaseSettingsModel): - _layout = "expanded" - shelf_name: str = Field(title="Shelf name") - tools_list: list[ShelfToolsModel] = Field( - default_factory=list, - title="Shelf Tools" - ) - - -class ShelvesModel(BaseSettingsModel): - _layout = "expanded" - shelf_set_name: str = Field(title="Shelfs set name") - - shelf_set_source_path: MultiplatformPathListModel = Field( - default_factory=MultiplatformPathListModel, - title="Shelf Set Path (optional)" - ) - - shelf_definition: list[ShelfDefinitionModel] = Field( - default_factory=list, - title="Shelf Definitions" - ) +from .publish import ( + PublishPluginsModel, + DEFAULT_HOUDINI_PUBLISH_SETTINGS, +) class HoudiniSettings(BaseSettingsModel): @@ -65,18 +27,16 @@ class HoudiniSettings(BaseSettingsModel): ) shelves: list[ShelvesModel] = Field( default_factory=list, - title="Houdini Scripts Shelves", + title="Shelves Manager", ) - - publish: PublishPluginsModel = Field( - default_factory=PublishPluginsModel, - title="Publish Plugins", - ) - create: CreatePluginsModel = Field( default_factory=CreatePluginsModel, title="Creator Plugins", ) + publish: PublishPluginsModel = Field( + default_factory=PublishPluginsModel, + title="Publish Plugins", + ) DEFAULT_VALUES = { diff --git a/server_addon/houdini/server/settings/publish.py b/server_addon/houdini/server/settings/publish.py new file mode 100644 index 0000000000..7612c446bf --- /dev/null +++ b/server_addon/houdini/server/settings/publish.py @@ -0,0 +1,84 @@ +from pydantic import Field +from ayon_server.settings import BaseSettingsModel + + +# Publish Plugins +class ValidateWorkfilePathsModel(BaseSettingsModel): + enabled: bool = Field(title="Enabled") + optional: bool = Field(title="Optional") + node_types: list[str] = Field( + default_factory=list, + title="Node Types" + ) + prohibited_vars: list[str] = Field( + default_factory=list, + title="Prohibited Variables" + ) + + +class BasicValidateModel(BaseSettingsModel): + enabled: bool = Field(title="Enabled") + optional: bool = Field(title="Optional") + active: bool = Field(title="Active") + + +class PublishPluginsModel(BaseSettingsModel): + ValidateWorkfilePaths: ValidateWorkfilePathsModel = Field( + default_factory=ValidateWorkfilePathsModel, + title="Validate workfile paths settings.") + ValidateReviewColorspace: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Review Colorspace.") + ValidateContainers: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Latest Containers.") + ValidateSubsetName: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Subset Name.") + ValidateMeshIsStatic: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Mesh is Static.") + ValidateUnrealStaticMeshName: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Unreal Static Mesh Name.") + + +DEFAULT_HOUDINI_PUBLISH_SETTINGS = { + "ValidateWorkfilePaths": { + "enabled": True, + "optional": True, + "node_types": [ + "file", + "alembic" + ], + "prohibited_vars": [ + "$HIP", + "$JOB" + ] + }, + "ValidateReviewColorspace": { + "enabled": True, + "optional": True, + "active": True + }, + "ValidateContainers": { + "enabled": True, + "optional": True, + "active": True + }, + "ValidateSubsetName": { + "enabled": True, + "optional": True, + "active": True + }, + "ValidateMeshIsStatic": { + "enabled": True, + "optional": True, + "active": True + }, + "ValidateUnrealStaticMeshName": { + "enabled": False, + "optional": True, + "active": True + } +} diff --git a/server_addon/houdini/server/settings/shelves.py b/server_addon/houdini/server/settings/shelves.py new file mode 100644 index 0000000000..2319357f59 --- /dev/null +++ b/server_addon/houdini/server/settings/shelves.py @@ -0,0 +1,43 @@ +from pydantic import Field +from ayon_server.settings import ( + BaseSettingsModel, + MultiplatformPathModel, + MultiplatformPathListModel, +) + + +class ShelfToolsModel(BaseSettingsModel): + name: str = Field(title="Name") + help: str = Field(title="Help text") + script: MultiplatformPathModel = Field( + default_factory=MultiplatformPathModel, + title="Script Path " + ) + icon: MultiplatformPathModel = Field( + default_factory=MultiplatformPathModel, + title="Icon Path " + ) + + +class ShelfDefinitionModel(BaseSettingsModel): + _layout = "expanded" + shelf_name: str = Field(title="Shelf name") + tools_list: list[ShelfToolsModel] = Field( + default_factory=list, + title="Shelf Tools" + ) + + +class ShelvesModel(BaseSettingsModel): + _layout = "expanded" + shelf_set_name: str = Field(title="Shelfs set name") + + shelf_set_source_path: MultiplatformPathListModel = Field( + default_factory=MultiplatformPathListModel, + title="Shelf Set Path (optional)" + ) + + shelf_definition: list[ShelfDefinitionModel] = Field( + default_factory=list, + title="Shelf Definitions" + ) diff --git a/server_addon/houdini/server/version.py b/server_addon/houdini/server/version.py index bbab0242f6..1276d0254f 100644 --- a/server_addon/houdini/server/version.py +++ b/server_addon/houdini/server/version.py @@ -1 +1 @@ -__version__ = "0.1.4" +__version__ = "0.1.5" From b1b24d49b00f4369fcd15e77b471d352bedf684f Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 11 Oct 2023 10:45:27 +0300 Subject: [PATCH 0240/1233] use int frameStartHandle and frameEndHandle --- openpype/hosts/houdini/api/lib.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 6d57d959e6..7ff86fe5ae 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -589,8 +589,8 @@ def get_frame_data(node, asset_data=None, log=None): "current frame".format(node.path()) ) else: - data["frameStartHandle"] = node.evalParm("f1") - data["frameEndHandle"] = node.evalParm("f2") + data["frameStartHandle"] = int(node.evalParm("f1")) + data["frameEndHandle"] = int(node.evalParm("f2")) data["byFrameStep"] = node.evalParm("f3") data["handleStart"] = asset_data.get("handleStart", 0) data["handleEnd"] = asset_data.get("handleEnd", 0) From f52dc88a17415c667220dc3e3ef4fc3f5b14a074 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 11 Oct 2023 12:03:05 +0300 Subject: [PATCH 0241/1233] fix typo --- 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 7ff86fe5ae..8810e1520f 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -559,7 +559,7 @@ def get_frame_data(node, asset_data=None, log=None): node(hou.Node) Returns: - dict: frame data for star, end and steps. + dict: frame data for start, end and steps. """ if asset_data is None: From 99dcca56d55035a7ddec249cd7bf85e9a4168e6b Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 11 Oct 2023 12:03:50 +0300 Subject: [PATCH 0242/1233] better error message --- .../plugins/publish/validate_frame_range.py | 36 ++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_frame_range.py b/openpype/hosts/houdini/plugins/publish/validate_frame_range.py index cf85e59041..30b736dbd0 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_frame_range.py +++ b/openpype/hosts/houdini/plugins/publish/validate_frame_range.py @@ -23,10 +23,25 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): invalid = self.get_invalid(instance) if invalid: - nodes = [n.path() for n in invalid] + node = invalid[0].path() raise PublishValidationError( - "Invalid Frame Range on: {0}".format(nodes), - title="Invalid Frame Range" + title="Invalid Frame Range", + message=( + "Invalid frame range because the instance start frame ({0[frameStart]}) " + "is higher than the end frame ({0[frameEnd]})" + .format(instance.data) + ), + description=( + "## Invalid Frame Range\n" + "The frame range for the instance is invalid because the " + "start frame is higher than the end frame.\n\nThis is likely " + "due to asset handles being applied to your instance or may " + "be because the ROP node's start frame is set higher than the " + "end frame.\n\nIf your ROP frame range is correct and you do " + "not want to apply asset handles make sure to disable Use " + "asset handles on the publish instance.\n\n" + "Associated Node: \"{0}\"".format(node) + ) ) @classmethod @@ -37,14 +52,11 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): rop_node = hou.node(instance.data["instance_node"]) if instance.data["frameStart"] > instance.data["frameEnd"]: - cls.log.error( - "Wrong frame range, please consider handle start and end.\n" - "frameEnd should at least be {}.\n" - "Use \"End frame hotfix\" action to do that." - .format( - instance.data["handleEnd"] + - instance.data["handleStart"] + - instance.data["frameStartHandle"] - ) + cls.log.info( + "The ROP node render range is set to " + "{0[frameStartHandle]} - {0[frameEndHandle]} " + "The asset handles applied to the instance are start handle " + "{0[handleStart]} and end handle {0[handleEnd]}" + .format(instance.data) ) return [rop_node] From b2f613966cf7b6014d0fba47a134365a9079faf3 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 11 Oct 2023 12:07:56 +0300 Subject: [PATCH 0243/1233] rexolve hound --- .../plugins/publish/validate_frame_range.py | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_frame_range.py b/openpype/hosts/houdini/plugins/publish/validate_frame_range.py index 30b736dbd0..936eb1180d 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_frame_range.py +++ b/openpype/hosts/houdini/plugins/publish/validate_frame_range.py @@ -27,20 +27,22 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): raise PublishValidationError( title="Invalid Frame Range", message=( - "Invalid frame range because the instance start frame ({0[frameStart]}) " - "is higher than the end frame ({0[frameEnd]})" + "Invalid frame range because the instance " + "start frame ({0[frameStart]}) is higher than " + "the end frame ({0[frameEnd]})" .format(instance.data) ), description=( "## Invalid Frame Range\n" - "The frame range for the instance is invalid because the " - "start frame is higher than the end frame.\n\nThis is likely " - "due to asset handles being applied to your instance or may " - "be because the ROP node's start frame is set higher than the " - "end frame.\n\nIf your ROP frame range is correct and you do " - "not want to apply asset handles make sure to disable Use " - "asset handles on the publish instance.\n\n" - "Associated Node: \"{0}\"".format(node) + "The frame range for the instance is invalid because " + "the start frame is higher than the end frame.\n\nThis " + "is likely due to asset handles being applied to your " + "instance or may be because the ROP node's start frame " + "is set higher than the end frame.\n\nIf your ROP frame " + "range is correct and you do not want to apply asset " + "handles make sure to disable Use asset handles on the " + "publish instance.\n\nAssociated Node: \"{0}\"" + .format(node) ) ) From b06935efb74a6a0bb4c9e1865e61bc64d2e6be12 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 11 Oct 2023 17:13:18 +0800 Subject: [PATCH 0244/1233] Enhancement on the setting in review family --- openpype/hosts/max/api/lib.py | 3 + .../hosts/max/plugins/create/create_review.py | 46 +++++++++---- .../max/plugins/publish/collect_review.py | 7 +- .../publish/extract_review_animation.py | 65 +++++++++++++++---- .../max/plugins/publish/extract_thumbnail.py | 11 +++- 5 files changed, 102 insertions(+), 30 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 8b70b3ced7..6f41cf9260 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -324,6 +324,7 @@ def is_headless(): @contextlib.contextmanager def viewport_camera(camera): original = rt.viewport.getCamera() + has_autoplay = rt.preferences.playPreviewWhenDone if not original: # if there is no original camera # use the current camera as original @@ -331,9 +332,11 @@ def viewport_camera(camera): review_camera = rt.getNodeByName(camera) try: rt.viewport.setCamera(review_camera) + rt.preferences.playPreviewWhenDone = False yield finally: rt.viewport.setCamera(original) + rt.preferences.playPreviewWhenDone = has_autoplay def set_timeline(frameStart, frameEnd): diff --git a/openpype/hosts/max/plugins/create/create_review.py b/openpype/hosts/max/plugins/create/create_review.py index 5737114fcc..5638327d72 100644 --- a/openpype/hosts/max/plugins/create/create_review.py +++ b/openpype/hosts/max/plugins/create/create_review.py @@ -17,27 +17,41 @@ class CreateReview(plugin.MaxCreator): instance_data["imageFormat"] = pre_create_data.get("imageFormat") instance_data["keepImages"] = pre_create_data.get("keepImages") instance_data["percentSize"] = pre_create_data.get("percentSize") - instance_data["rndLevel"] = pre_create_data.get("rndLevel") + instance_data["visualStyleMode"] = pre_create_data.get("visualStyleMode") + # Transfer settings from pre create to instance + creator_attributes = instance_data.setdefault( + "creator_attributes", dict()) + for key in ["imageFormat", + "keepImages", + "percentSize", + "visualStyleMode", + "viewportPreset"]: + if key in pre_create_data: + creator_attributes[key] = pre_create_data[key] super(CreateReview, self).create( subset_name, instance_data, pre_create_data) - def get_pre_create_attr_defs(self): - attrs = super(CreateReview, self).get_pre_create_attr_defs() - + def get_instance_attr_defs(self): image_format_enum = [ "bmp", "cin", "exr", "jpg", "hdr", "rgb", "png", "rla", "rpf", "dds", "sgi", "tga", "tif", "vrimg" ] - rndLevel_enum = [ - "smoothhighlights", "smooth", "facethighlights", - "facet", "flat", "litwireframe", "wireframe", "box" + visual_style_preset_enum = [ + "Realistic", "Shaded", "Facets", + "ConsistentColors", "HiddenLine", + "Wireframe", "BoundingBox", "Ink", + "ColorInk", "Acrylic", "Tech", "Graphite", + "ColorPencil", "Pastel", "Clay", "ModelAssist" ] + preview_preset_enum = [ + "Quality", "Standard", "Performance", + "DXMode", "Customize"] - return attrs + [ + return [ BoolDef("keepImages", label="Keep Image Sequences", default=False), @@ -50,8 +64,16 @@ class CreateReview(plugin.MaxCreator): default=100, minimum=1, decimals=0), - EnumDef("rndLevel", - rndLevel_enum, - default="smoothhighlights", - label="Preference") + EnumDef("visualStyleMode", + visual_style_preset_enum, + default="Realistic", + label="Preference"), + EnumDef("viewportPreset", + preview_preset_enum, + default="Quality", + label="Pre-View Preset") ] + + def get_pre_create_attr_defs(self): + # Use same attributes as for instance attributes + return self.get_instance_attr_defs() diff --git a/openpype/hosts/max/plugins/publish/collect_review.py b/openpype/hosts/max/plugins/publish/collect_review.py index 8e27a857d7..1f0dca5329 100644 --- a/openpype/hosts/max/plugins/publish/collect_review.py +++ b/openpype/hosts/max/plugins/publish/collect_review.py @@ -25,10 +25,15 @@ class CollectReview(pyblish.api.InstancePlugin, if rt.classOf(node) in rt.Camera.classes: camera_name = node.name focal_length = node.fov - + creator_attrs = instance.data["creator_attributes"] attr_values = self.get_attr_values_from_data(instance.data) data = { "review_camera": camera_name, + "imageFormat": creator_attrs["imageFormat"], + "keepImages": creator_attrs["keepImages"], + "percentSize": creator_attrs["percentSize"], + "visualStyleMode": creator_attrs["visualStyleMode"], + "viewportPreset": creator_attrs["viewportPreset"], "frameStart": instance.context.data["frameStart"], "frameEnd": instance.context.data["frameEnd"], "fps": instance.context.data["fps"], diff --git a/openpype/hosts/max/plugins/publish/extract_review_animation.py b/openpype/hosts/max/plugins/publish/extract_review_animation.py index 8e06e52b5c..64ecbe5d85 100644 --- a/openpype/hosts/max/plugins/publish/extract_review_animation.py +++ b/openpype/hosts/max/plugins/publish/extract_review_animation.py @@ -1,8 +1,12 @@ import os +import contextlib import pyblish.api from pymxs import runtime as rt from openpype.pipeline import publish -from openpype.hosts.max.api.lib import viewport_camera, get_max_version +from openpype.hosts.max.api.lib import ( + viewport_camera, + get_max_version +) class ExtractReviewAnimation(publish.Extractor): @@ -32,10 +36,24 @@ class ExtractReviewAnimation(publish.Extractor): " '%s' to '%s'" % (filename, staging_dir)) review_camera = instance.data["review_camera"] - with viewport_camera(review_camera): - preview_arg = self.set_preview_arg( - instance, filepath, start, end, fps) - rt.execute(preview_arg) + if get_max_version() >= 2024: + with viewport_camera(review_camera): + preview_arg = self.set_preview_arg( + instance, filepath, start, end, fps) + rt.execute(preview_arg) + else: + visual_style_preset = instance.data.get("visualStyleMode") + nitrousGraphicMgr = rt.NitrousGraphicsManager + viewport_setting = nitrousGraphicMgr.GetActiveViewportSetting() + with viewport_camera(review_camera) and ( + self._visual_style_option( + viewport_setting, visual_style_preset) + ): + viewport_setting.VisualStyleMode = rt.Name( + visual_style_preset) + preview_arg = self.set_preview_arg( + instance, filepath, start, end, fps) + rt.execute(preview_arg) tags = ["review"] if not instance.data.get("keepImages"): @@ -76,10 +94,6 @@ class ExtractReviewAnimation(publish.Extractor): job_args.append(default_option) frame_option = f"outputAVI:false start:{start} end:{end} fps:{fps}" # noqa job_args.append(frame_option) - rndLevel = instance.data.get("rndLevel") - if rndLevel: - option = f"rndLevel:#{rndLevel}" - job_args.append(option) options = [ "percentSize", "dspGeometry", "dspShapes", "dspLights", "dspCameras", "dspHelpers", "dspParticles", @@ -90,13 +104,36 @@ class ExtractReviewAnimation(publish.Extractor): enabled = instance.data.get(key) if enabled: job_args.append(f"{key}:{enabled}") - - if get_max_version() == 2024: - # hardcoded for current stage - auto_play_option = "autoPlay:false" - job_args.append(auto_play_option) + if get_max_version() >= 2024: + visual_style_preset = instance.data.get("visualStyleMode") + if visual_style_preset == "Realistic": + visual_style_preset = "defaultshading" + else: + visual_style_preset = visual_style_preset.lower() + # new argument exposed for Max 2024 for visual style + visual_style_option = f"vpStyle:#{visual_style_preset}" + job_args.append(visual_style_option) job_str = " ".join(job_args) self.log.debug(job_str) return job_str + + @contextlib.contextmanager + def _visual_style_option(self, viewport_setting, visual_style): + """Function to set visual style options + + Args: + visual_style (str): visual style for active viewport + + Returns: + list: the argument which can set visual style + """ + current_setting = viewport_setting.VisualStyleMode + if visual_style != current_setting: + try: + viewport_setting.VisualStyleMode = rt.Name( + visual_style) + yield + finally: + viewport_setting.VisualStyleMode = current_setting diff --git a/openpype/hosts/max/plugins/publish/extract_thumbnail.py b/openpype/hosts/max/plugins/publish/extract_thumbnail.py index 82f4fc7a8b..4f9f3de6ab 100644 --- a/openpype/hosts/max/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/max/plugins/publish/extract_thumbnail.py @@ -81,9 +81,14 @@ class ExtractThumbnail(publish.Extractor): if enabled: job_args.append(f"{key}:{enabled}") if get_max_version() == 2024: - # hardcoded for current stage - auto_play_option = "autoPlay:false" - job_args.append(auto_play_option) + visual_style_preset = instance.data.get("visualStyleMode") + if visual_style_preset == "Realistic": + visual_style_preset = "defaultshading" + else: + visual_style_preset = visual_style_preset.lower() + # new argument exposed for Max 2024 for visual style + visual_style_option = f"vpStyle:#{visual_style_preset}" + job_args.append(visual_style_option) job_str = " ".join(job_args) self.log.debug(job_str) From d27d3435d97681fb7fd9b7ea72f3b8ed4700996a Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 11 Oct 2023 10:20:18 +0100 Subject: [PATCH 0245/1233] Use the right class for the exception --- .../plugins/publish/validate_sequence_frames.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py b/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py index e24729391f..1f7753db37 100644 --- a/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py +++ b/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py @@ -3,6 +3,7 @@ import os import re import pyblish.api +from openpype.pipeline.publish import PublishValidationError class ValidateSequenceFrames(pyblish.api.InstancePlugin): @@ -40,15 +41,15 @@ class ValidateSequenceFrames(pyblish.api.InstancePlugin): repr["files"], minimum_items=1, patterns=patterns) if remainder: - raise ValueError( + raise PublishValidationError( "Some files have been found outside a sequence. " f"Invalid files: {remainder}") if not collections: - raise ValueError( + raise PublishValidationError( "No collections found. There should be a single " "collection per representation.") if len(collections) > 1: - raise ValueError( + raise PublishValidationError( "Multiple collections detected. There should be a single " "collection per representation. " f"Collections identified: {collections}") @@ -65,11 +66,11 @@ class ValidateSequenceFrames(pyblish.api.InstancePlugin): data["clipOut"]) if current_range != required_range: - raise ValueError(f"Invalid frame range: {current_range} - " + raise PublishValidationError(f"Invalid frame range: {current_range} - " f"expected: {required_range}") missing = collection.holes().indexes if missing: - raise ValueError( + raise PublishValidationError( "Missing frames have been detected. " f"Missing frames: {missing}") From ca07d562552f81ecbfae9d0582f145bcc6d7e182 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 11 Oct 2023 10:20:43 +0100 Subject: [PATCH 0246/1233] Changed error message for not finding any collection --- .../unreal/plugins/publish/validate_sequence_frames.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py b/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py index 1f7753db37..334baf0cee 100644 --- a/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py +++ b/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py @@ -46,8 +46,10 @@ class ValidateSequenceFrames(pyblish.api.InstancePlugin): f"Invalid files: {remainder}") if not collections: raise PublishValidationError( - "No collections found. There should be a single " - "collection per representation.") + "We have been unable to find a sequence in the " + "files. Please ensure the files are named " + "appropriately. " + f"Files: {repr_files}") if len(collections) > 1: raise PublishValidationError( "Multiple collections detected. There should be a single " From 19b58d8095e26e985a922a441fd654fbab301784 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 11 Oct 2023 10:24:55 +0100 Subject: [PATCH 0247/1233] Hound fixes --- .../hosts/unreal/plugins/publish/validate_sequence_frames.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py b/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py index 334baf0cee..06acbf0992 100644 --- a/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py +++ b/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py @@ -68,8 +68,9 @@ class ValidateSequenceFrames(pyblish.api.InstancePlugin): data["clipOut"]) if current_range != required_range: - raise PublishValidationError(f"Invalid frame range: {current_range} - " - f"expected: {required_range}") + raise PublishValidationError( + f"Invalid frame range: {current_range} - " + f"expected: {required_range}") missing = collection.holes().indexes if missing: From c389ccb5875353d32c3e535880108c0a648cd060 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 11 Oct 2023 13:18:47 +0300 Subject: [PATCH 0248/1233] make description not instance specific --- .../hosts/houdini/plugins/publish/validate_frame_range.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_frame_range.py b/openpype/hosts/houdini/plugins/publish/validate_frame_range.py index 936eb1180d..2411d29e3e 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_frame_range.py +++ b/openpype/hosts/houdini/plugins/publish/validate_frame_range.py @@ -23,7 +23,6 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): invalid = self.get_invalid(instance) if invalid: - node = invalid[0].path() raise PublishValidationError( title="Invalid Frame Range", message=( @@ -41,8 +40,7 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): "is set higher than the end frame.\n\nIf your ROP frame " "range is correct and you do not want to apply asset " "handles make sure to disable Use asset handles on the " - "publish instance.\n\nAssociated Node: \"{0}\"" - .format(node) + "publish instance." ) ) From 3314605db8447d5e92b3e31735cba80de179241e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 11 Oct 2023 18:59:07 +0800 Subject: [PATCH 0249/1233] move the preview arguments into the lib.py --- openpype/hosts/max/api/lib.py | 54 +++++++++++++++++++ .../hosts/max/plugins/create/create_review.py | 5 -- .../publish/extract_review_animation.py | 41 ++------------ .../max/plugins/publish/extract_thumbnail.py | 45 +++------------- 4 files changed, 66 insertions(+), 79 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 6f41cf9260..1ca2da81f8 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -500,3 +500,57 @@ def get_plugins() -> list: plugin_info_list.append(plugin_info) return plugin_info_list + +def set_preview_arg(instance, filepath, + start, end, fps): + """Function to set up preview arguments in MaxScript. + + Args: + instance (str): instance + filepath (str): output of the preview animation + start (int): startFrame + end (int): endFrame + fps (float): fps value + + Returns: + list: job arguments + """ + job_args = list() + default_option = f'CreatePreview filename:"{filepath}"' + job_args.append(default_option) + frame_option = f"outputAVI:false start:{start} end:{end} fps:{fps}" # noqa + job_args.append(frame_option) + options = [ + "percentSize", "dspGeometry", "dspShapes", + "dspLights", "dspCameras", "dspHelpers", "dspParticles", + "dspBones", "dspBkg", "dspGrid", "dspSafeFrame", "dspFrameNums" + ] + + for key in options: + enabled = instance.data.get(key) + if enabled: + job_args.append(f"{key}:{enabled}") + if get_max_version() >= 2024: + visual_style_preset = instance.data.get("visualStyleMode") + if visual_style_preset == "Realistic": + visual_style_preset = "defaultshading" + else: + visual_style_preset = visual_style_preset.lower() + # new argument exposed for Max 2024 for visual style + visual_style_option = f"vpStyle:#{visual_style_preset}" + job_args.append(visual_style_option) + # new argument for pre-view preset exposed in Max 2024 + preview_preset = instance.data.get("viewportPreset") + if preview_preset == "Quality": + preview_preset = "highquality" + elif preview_preset == "Customize": + preview_preset = "userdefined" + else: + preview_preset = preview_preset.lower() + preview_preset.option = f"vpPreset:#{visual_style_preset}" + job_args.append(preview_preset) + + job_str = " ".join(job_args) + log.debug(job_str) + + return job_str diff --git a/openpype/hosts/max/plugins/create/create_review.py b/openpype/hosts/max/plugins/create/create_review.py index 5638327d72..e654783a33 100644 --- a/openpype/hosts/max/plugins/create/create_review.py +++ b/openpype/hosts/max/plugins/create/create_review.py @@ -13,11 +13,6 @@ class CreateReview(plugin.MaxCreator): icon = "video-camera" def create(self, subset_name, instance_data, pre_create_data): - - instance_data["imageFormat"] = pre_create_data.get("imageFormat") - instance_data["keepImages"] = pre_create_data.get("keepImages") - instance_data["percentSize"] = pre_create_data.get("percentSize") - instance_data["visualStyleMode"] = pre_create_data.get("visualStyleMode") # Transfer settings from pre create to instance creator_attributes = instance_data.setdefault( "creator_attributes", dict()) diff --git a/openpype/hosts/max/plugins/publish/extract_review_animation.py b/openpype/hosts/max/plugins/publish/extract_review_animation.py index 64ecbe5d85..9c26ef7e7d 100644 --- a/openpype/hosts/max/plugins/publish/extract_review_animation.py +++ b/openpype/hosts/max/plugins/publish/extract_review_animation.py @@ -5,7 +5,8 @@ from pymxs import runtime as rt from openpype.pipeline import publish from openpype.hosts.max.api.lib import ( viewport_camera, - get_max_version + get_max_version, + set_preview_arg ) @@ -25,7 +26,7 @@ class ExtractReviewAnimation(publish.Extractor): filename = "{0}..{1}".format(instance.name, ext) start = int(instance.data["frameStart"]) end = int(instance.data["frameEnd"]) - fps = int(instance.data["fps"]) + fps = float(instance.data["fps"]) filepath = os.path.join(staging_dir, filename) filepath = filepath.replace("\\", "/") filenames = self.get_files( @@ -38,7 +39,7 @@ class ExtractReviewAnimation(publish.Extractor): review_camera = instance.data["review_camera"] if get_max_version() >= 2024: with viewport_camera(review_camera): - preview_arg = self.set_preview_arg( + preview_arg = set_preview_arg( instance, filepath, start, end, fps) rt.execute(preview_arg) else: @@ -51,7 +52,7 @@ class ExtractReviewAnimation(publish.Extractor): ): viewport_setting.VisualStyleMode = rt.Name( visual_style_preset) - preview_arg = self.set_preview_arg( + preview_arg = set_preview_arg( instance, filepath, start, end, fps) rt.execute(preview_arg) @@ -87,38 +88,6 @@ class ExtractReviewAnimation(publish.Extractor): return file_list - def set_preview_arg(self, instance, filepath, - start, end, fps): - job_args = list() - default_option = f'CreatePreview filename:"{filepath}"' - job_args.append(default_option) - frame_option = f"outputAVI:false start:{start} end:{end} fps:{fps}" # noqa - job_args.append(frame_option) - options = [ - "percentSize", "dspGeometry", "dspShapes", - "dspLights", "dspCameras", "dspHelpers", "dspParticles", - "dspBones", "dspBkg", "dspGrid", "dspSafeFrame", "dspFrameNums" - ] - - for key in options: - enabled = instance.data.get(key) - if enabled: - job_args.append(f"{key}:{enabled}") - if get_max_version() >= 2024: - visual_style_preset = instance.data.get("visualStyleMode") - if visual_style_preset == "Realistic": - visual_style_preset = "defaultshading" - else: - visual_style_preset = visual_style_preset.lower() - # new argument exposed for Max 2024 for visual style - visual_style_option = f"vpStyle:#{visual_style_preset}" - job_args.append(visual_style_option) - - job_str = " ".join(job_args) - self.log.debug(job_str) - - return job_str - @contextlib.contextmanager def _visual_style_option(self, viewport_setting, visual_style): """Function to set visual style options diff --git a/openpype/hosts/max/plugins/publish/extract_thumbnail.py b/openpype/hosts/max/plugins/publish/extract_thumbnail.py index 4f9f3de6ab..22c45f3e11 100644 --- a/openpype/hosts/max/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/max/plugins/publish/extract_thumbnail.py @@ -3,7 +3,11 @@ import tempfile import pyblish.api from pymxs import runtime as rt from openpype.pipeline import publish -from openpype.hosts.max.api.lib import viewport_camera, get_max_version +from openpype.hosts.max.api.lib import ( + viewport_camera, + get_max_version, + set_preview_arg +) class ExtractThumbnail(publish.Extractor): @@ -23,7 +27,7 @@ class ExtractThumbnail(publish.Extractor): self.log.debug( f"Create temp directory {tmp_staging} for thumbnail" ) - fps = int(instance.data["fps"]) + fps = float(instance.data["fps"]) frame = int(instance.data["frameStart"]) instance.context.data["cleanupFullPaths"].append(tmp_staging) filename = "{name}_thumbnail..png".format(**instance.data) @@ -36,7 +40,7 @@ class ExtractThumbnail(publish.Extractor): " '%s' to '%s'" % (filename, tmp_staging)) review_camera = instance.data["review_camera"] with viewport_camera(review_camera): - preview_arg = self.set_preview_arg( + preview_arg = set_preview_arg( instance, filepath, fps, frame) rt.execute(preview_arg) @@ -59,38 +63,3 @@ class ExtractThumbnail(publish.Extractor): filename, target_frame ) return thumbnail_name - - def set_preview_arg(self, instance, filepath, fps, frame): - job_args = list() - default_option = f'CreatePreview filename:"{filepath}"' - job_args.append(default_option) - frame_option = f"outputAVI:false start:{frame} end:{frame} fps:{fps}" # noqa - job_args.append(frame_option) - rndLevel = instance.data.get("rndLevel") - if rndLevel: - option = f"rndLevel:#{rndLevel}" - job_args.append(option) - options = [ - "percentSize", "dspGeometry", "dspShapes", - "dspLights", "dspCameras", "dspHelpers", "dspParticles", - "dspBones", "dspBkg", "dspGrid", "dspSafeFrame", "dspFrameNums" - ] - - for key in options: - enabled = instance.data.get(key) - if enabled: - job_args.append(f"{key}:{enabled}") - if get_max_version() == 2024: - visual_style_preset = instance.data.get("visualStyleMode") - if visual_style_preset == "Realistic": - visual_style_preset = "defaultshading" - else: - visual_style_preset = visual_style_preset.lower() - # new argument exposed for Max 2024 for visual style - visual_style_option = f"vpStyle:#{visual_style_preset}" - job_args.append(visual_style_option) - - job_str = " ".join(job_args) - self.log.debug(job_str) - - return job_str From 7e5ce50fe1536675bdb35b5de43cb4c331007495 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 11 Oct 2023 19:00:29 +0800 Subject: [PATCH 0250/1233] 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 1ca2da81f8..68baf720bc 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -501,6 +501,7 @@ def get_plugins() -> list: return plugin_info_list + def set_preview_arg(instance, filepath, start, end, fps): """Function to set up preview arguments in MaxScript. From 63828671745691d7463cb49740aaae6846f524b7 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 11 Oct 2023 13:29:56 +0200 Subject: [PATCH 0251/1233] :recycle: move list creation closer to the caller --- .../hosts/maya/plugins/create/create_multishot_layout.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_multishot_layout.py b/openpype/hosts/maya/plugins/create/create_multishot_layout.py index 36fee655e6..c109a76a31 100644 --- a/openpype/hosts/maya/plugins/create/create_multishot_layout.py +++ b/openpype/hosts/maya/plugins/create/create_multishot_layout.py @@ -185,18 +185,18 @@ class CreateMultishotLayout(plugin.MayaCreator): """ # if folder_path is None, project is selected as a root # and its name is used as a parent id - parent_id = [self.project_name] + parent_id = self.project_name if folder_path: current_folder = get_folder_by_path( project_name=self.project_name, folder_path=folder_path, ) - parent_id = [current_folder["id"]] + parent_id = current_folder["id"] # get all child folders of the current one child_folders = get_folders( project_name=self.project_name, - parent_ids=parent_id, + parent_ids=[parent_id], fields=[ "attrib.clipIn", "attrib.clipOut", "attrib.frameStart", "attrib.frameEnd", From 91c37916cb82f75515f81a54b922769dab5b1816 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 11 Oct 2023 15:23:26 +0200 Subject: [PATCH 0252/1233] improving variable names and fixing nodes overrides - also bumping vrsion and fixing label for input process node --- openpype/settings/ayon_settings.py | 31 ++++++- server_addon/nuke/server/settings/imageio.py | 90 +++++++++++-------- .../nuke/server/settings/publish_plugins.py | 20 +++-- server_addon/nuke/server/version.py | 2 +- 4 files changed, 95 insertions(+), 48 deletions(-) diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index d54d71e851..0b3f6725d8 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -819,9 +819,36 @@ def _convert_nuke_project_settings(ayon_settings, output): # NOTE 'monitorOutLut' is maybe not yet in v3 (ut should be) _convert_host_imageio(ayon_nuke) ayon_imageio = ayon_nuke["imageio"] - for item in ayon_imageio["nodes"]["requiredNodes"]: + + # workfile + imageio_workfile = ayon_imageio["workfile"] + workfile_keys_mapping = ( + ("color_management", "colorManagement"), + ("native_ocio_config", "OCIO_config"), + ("working_space", "workingSpaceLUT"), + ("thumbnail_space", "monitorLut"), + ) + for src, dst in workfile_keys_mapping: + if ( + src in imageio_workfile + and dst not in imageio_workfile + ): + imageio_workfile[dst] = imageio_workfile.pop(src) + + # regex inputs + regex_inputs = ayon_imageio.get("regex_inputs") + if regex_inputs: + ayon_imageio.pop("regex_inputs") + ayon_imageio["regexInputs"] = regex_inputs + + # nodes + for item in ayon_imageio["nodes"]["required_nodes"]: + if item.get("nuke_node_class"): + item["nukeNodeClass"] = item["nuke_node_class"] item["knobs"] = _convert_nuke_knobs(item["knobs"]) - for item in ayon_imageio["nodes"]["overrideNodes"]: + for item in ayon_imageio["nodes"]["override_nodes"]: + if item.get("nuke_node_class"): + item["nukeNodeClass"] = item["nuke_node_class"] item["knobs"] = _convert_nuke_knobs(item["knobs"]) output["nuke"] = ayon_nuke diff --git a/server_addon/nuke/server/settings/imageio.py b/server_addon/nuke/server/settings/imageio.py index 811b12104b..15ccd4e89a 100644 --- a/server_addon/nuke/server/settings/imageio.py +++ b/server_addon/nuke/server/settings/imageio.py @@ -14,10 +14,30 @@ class NodesModel(BaseSettingsModel): default_factory=list, title="Used in plugins" ) - nukeNodeClass: str = Field( + nuke_node_class: str = Field( title="Nuke Node Class", ) + +class RequiredNodesModel(NodesModel): + knobs: list[KnobModel] = Field( + default_factory=list, + title="Knobs", + ) + + @validator("knobs") + def ensure_unique_names(cls, value): + """Ensure name fields within the lists have unique names.""" + ensure_unique_names(value) + return value + + +class OverrideNodesModel(NodesModel): + subsets: list[str] = Field( + default_factory=list, + title="Subsets" + ) + knobs: list[KnobModel] = Field( default_factory=list, title="Knobs", @@ -31,13 +51,11 @@ class NodesModel(BaseSettingsModel): class NodesSetting(BaseSettingsModel): - # TODO: rename `requiredNodes` to `required_nodes` - requiredNodes: list[NodesModel] = Field( + required_nodes: list[RequiredNodesModel] = Field( title="Plugin required", default_factory=list ) - # TODO: rename `overrideNodes` to `override_nodes` - overrideNodes: list[NodesModel] = Field( + override_nodes: list[OverrideNodesModel] = Field( title="Plugin's node overrides", default_factory=list ) @@ -46,38 +64,40 @@ class NodesSetting(BaseSettingsModel): def ocio_configs_switcher_enum(): return [ {"value": "nuke-default", "label": "nuke-default"}, - {"value": "spi-vfx", "label": "spi-vfx"}, - {"value": "spi-anim", "label": "spi-anim"}, - {"value": "aces_0.1.1", "label": "aces_0.1.1"}, - {"value": "aces_0.7.1", "label": "aces_0.7.1"}, - {"value": "aces_1.0.1", "label": "aces_1.0.1"}, - {"value": "aces_1.0.3", "label": "aces_1.0.3"}, - {"value": "aces_1.1", "label": "aces_1.1"}, - {"value": "aces_1.2", "label": "aces_1.2"}, - {"value": "aces_1.3", "label": "aces_1.3"}, - {"value": "custom", "label": "custom"} + {"value": "spi-vfx", "label": "spi-vfx (11)"}, + {"value": "spi-anim", "label": "spi-anim (11)"}, + {"value": "aces_0.1.1", "label": "aces_0.1.1 (11)"}, + {"value": "aces_0.7.1", "label": "aces_0.7.1 (11)"}, + {"value": "aces_1.0.1", "label": "aces_1.0.1 (11)"}, + {"value": "aces_1.0.3", "label": "aces_1.0.3 (11, 12)"}, + {"value": "aces_1.1", "label": "aces_1.1 (12, 13)"}, + {"value": "aces_1.2", "label": "aces_1.2 (13, 14)"}, + {"value": "studio-config-v1.0.0_aces-v1.3_ocio-v2.1", + "label": "studio-config-v1.0.0_aces-v1.3_ocio-v2.1 (14)"}, + {"value": "cg-config-v1.0.0_aces-v1.3_ocio-v2.1", + "label": "cg-config-v1.0.0_aces-v1.3_ocio-v2.1 (14)"}, ] class WorkfileColorspaceSettings(BaseSettingsModel): """Nuke workfile colorspace preset. """ - colorManagement: Literal["Nuke", "OCIO"] = Field( - title="Color Management" + color_management: Literal["Nuke", "OCIO"] = Field( + title="Color Management Workflow" ) - OCIO_config: str = Field( - title="OpenColorIO Config", - description="Switch between OCIO configs", + native_ocio_config: str = Field( + title="Native OpenColorIO Config", + description="Switch between native OCIO configs", enum_resolver=ocio_configs_switcher_enum, conditionalEnum=True ) - workingSpaceLUT: str = Field( + working_space: str = Field( title="Working Space" ) - monitorLut: str = Field( - title="Monitor" + thumbnail_space: str = Field( + title="Thumbnail Space" ) @@ -182,12 +202,10 @@ class ImageIOSettings(BaseSettingsModel): title="Nodes" ) """# TODO: enhance settings with host api: - - [ ] old settings are using `regexInputs` key but we - need to rename to `regex_inputs` - [ ] no need for `inputs` middle part. It can stay directly on `regex_inputs` """ - regexInputs: RegexInputsModel = Field( + regex_inputs: RegexInputsModel = Field( default_factory=RegexInputsModel, title="Assign colorspace to read nodes via rules" ) @@ -201,18 +219,18 @@ DEFAULT_IMAGEIO_SETTINGS = { "viewerProcess": "rec709" }, "workfile": { - "colorManagement": "Nuke", - "OCIO_config": "nuke-default", - "workingSpaceLUT": "linear", - "monitorLut": "sRGB", + "color_management": "Nuke", + "native_ocio_config": "nuke-default", + "working_space": "linear", + "thumbnail_space": "sRGB", }, "nodes": { - "requiredNodes": [ + "required_nodes": [ { "plugins": [ "CreateWriteRender" ], - "nukeNodeClass": "Write", + "nuke_node_class": "Write", "knobs": [ { "type": "text", @@ -264,7 +282,7 @@ DEFAULT_IMAGEIO_SETTINGS = { "plugins": [ "CreateWritePrerender" ], - "nukeNodeClass": "Write", + "nuke_node_class": "Write", "knobs": [ { "type": "text", @@ -316,7 +334,7 @@ DEFAULT_IMAGEIO_SETTINGS = { "plugins": [ "CreateWriteImage" ], - "nukeNodeClass": "Write", + "nuke_node_class": "Write", "knobs": [ { "type": "text", @@ -360,9 +378,9 @@ DEFAULT_IMAGEIO_SETTINGS = { ] } ], - "overrideNodes": [] + "override_nodes": [] }, - "regexInputs": { + "regex_inputs": { "inputs": [ { "regex": "(beauty).*(?=.exr)", diff --git a/server_addon/nuke/server/settings/publish_plugins.py b/server_addon/nuke/server/settings/publish_plugins.py index 19206149b6..2d3a282c46 100644 --- a/server_addon/nuke/server/settings/publish_plugins.py +++ b/server_addon/nuke/server/settings/publish_plugins.py @@ -155,8 +155,10 @@ class IntermediateOutputModel(BaseSettingsModel): title="Filter", default_factory=BakingStreamFilterModel) read_raw: bool = Field(title="Read raw switch") viewer_process_override: str = Field(title="Viewer process override") - bake_viewer_process: bool = Field(title="Bake view process") - bake_viewer_input_process: bool = Field(title="Bake viewer input process") + bake_viewer_process: bool = Field(title="Bake viewer process") + bake_viewer_input_process: bool = Field( + title="Bake viewer input process node (LUT)" + ) reformat_nodes_config: ReformatNodesConfigModel = Field( default_factory=ReformatNodesConfigModel, title="Reformat Nodes") @@ -407,12 +409,12 @@ DEFAULT_PUBLISH_PLUGIN_SETTINGS = { "text": "Lanczos6" }, { - "type": "bool", + "type": "boolean", "name": "black_outside", "boolean": True }, { - "type": "bool", + "type": "boolean", "name": "pbb", "boolean": False } @@ -427,7 +429,7 @@ DEFAULT_PUBLISH_PLUGIN_SETTINGS = { "enabled": False }, "ExtractReviewDataMov": { - "enabled": True, + "enabled": False, "viewer_lut_raw": False, "outputs": [ { @@ -463,12 +465,12 @@ DEFAULT_PUBLISH_PLUGIN_SETTINGS = { "text": "Lanczos6" }, { - "type": "bool", + "type": "boolean", "name": "black_outside", "boolean": True }, { - "type": "bool", + "type": "boolean", "name": "pbb", "boolean": False } @@ -518,12 +520,12 @@ DEFAULT_PUBLISH_PLUGIN_SETTINGS = { "text": "Lanczos6" }, { - "type": "bool", + "type": "boolean", "name": "black_outside", "boolean": True }, { - "type": "bool", + "type": "boolean", "name": "pbb", "boolean": False } diff --git a/server_addon/nuke/server/version.py b/server_addon/nuke/server/version.py index ae7362549b..bbab0242f6 100644 --- a/server_addon/nuke/server/version.py +++ b/server_addon/nuke/server/version.py @@ -1 +1 @@ -__version__ = "0.1.3" +__version__ = "0.1.4" From 47a02c2386f79bbbd63ac7328154bd1943499849 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 11 Oct 2023 14:34:10 +0100 Subject: [PATCH 0253/1233] Change pointcache creator to be in line with other creators --- .../plugins/create/create_pointcache.py | 42 +++++++++++++------ 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/blender/plugins/create/create_pointcache.py b/openpype/hosts/blender/plugins/create/create_pointcache.py index 6220f68dc5..65cf18472d 100644 --- a/openpype/hosts/blender/plugins/create/create_pointcache.py +++ b/openpype/hosts/blender/plugins/create/create_pointcache.py @@ -3,11 +3,11 @@ import bpy from openpype.pipeline import get_current_task_name -import openpype.hosts.blender.api.plugin -from openpype.hosts.blender.api import lib +from openpype.hosts.blender.api import plugin, lib, ops +from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES -class CreatePointcache(openpype.hosts.blender.api.plugin.Creator): +class CreatePointcache(plugin.Creator): """Polygonal static geometry""" name = "pointcacheMain" @@ -16,20 +16,36 @@ class CreatePointcache(openpype.hosts.blender.api.plugin.Creator): icon = "gears" def process(self): + """ Run the creator on Blender main thread""" + mti = ops.MainThreadItem(self._process) + ops.execute_in_main_thread(mti) + def _process(self): + # Get Instance Container or create it if it does not exist + instances = bpy.data.collections.get(AVALON_INSTANCES) + if not instances: + instances = bpy.data.collections.new(name=AVALON_INSTANCES) + bpy.context.scene.collection.children.link(instances) + + # Create instance object asset = self.data["asset"] subset = self.data["subset"] - name = openpype.hosts.blender.api.plugin.asset_name(asset, subset) - collection = bpy.data.collections.new(name=name) - bpy.context.scene.collection.children.link(collection) + name = plugin.asset_name(asset, subset) + asset_group = bpy.data.objects.new(name=name, object_data=None) + asset_group.empty_display_type = 'SINGLE_ARROW' + instances.objects.link(asset_group) self.data['task'] = get_current_task_name() - lib.imprint(collection, self.data) + lib.imprint(asset_group, self.data) + # Add selected objects to instance if (self.options or {}).get("useSelection"): - objects = lib.get_selection() - for obj in objects: - collection.objects.link(obj) - if obj.type == 'EMPTY': - objects.extend(obj.children) + bpy.context.view_layer.objects.active = asset_group + selected = lib.get_selection() + for obj in selected: + if obj.parent in selected: + obj.select_set(False) + continue + selected.append(asset_group) + bpy.ops.object.parent_set(keep_transform=True) - return collection + return asset_group From 7fec582a2d472c8a956621a53d556a8ee784e52a Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 11 Oct 2023 14:34:55 +0100 Subject: [PATCH 0254/1233] Improve instance collector --- .../plugins/publish/collect_instances.py | 101 +++++++----------- 1 file changed, 40 insertions(+), 61 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/collect_instances.py b/openpype/hosts/blender/plugins/publish/collect_instances.py index bc4b5ab092..4915e4a7cf 100644 --- a/openpype/hosts/blender/plugins/publish/collect_instances.py +++ b/openpype/hosts/blender/plugins/publish/collect_instances.py @@ -1,4 +1,5 @@ import json +from itertools import chain from typing import Generator import bpy @@ -19,85 +20,63 @@ class CollectInstances(pyblish.api.ContextPlugin): @staticmethod def get_asset_groups() -> Generator: - """Return all 'model' collections. - - Check if the family is 'model' and if it doesn't have the - representation set. If the representation is set, it is a loaded model - and we don't want to publish it. + """Return all instances that are empty objects asset groups. """ instances = bpy.data.collections.get(AVALON_INSTANCES) for obj in instances.objects: - avalon_prop = obj.get(AVALON_PROPERTY) or dict() + avalon_prop = obj.get(AVALON_PROPERTY) or {} if avalon_prop.get('id') == 'pyblish.avalon.instance': yield obj @staticmethod def get_collections() -> Generator: - """Return all 'model' collections. - - Check if the family is 'model' and if it doesn't have the - representation set. If the representation is set, it is a loaded model - and we don't want to publish it. + """Return all instances that are collections. """ - for collection in bpy.data.collections: - avalon_prop = collection.get(AVALON_PROPERTY) or dict() + instances = bpy.data.collections.get(AVALON_INSTANCES) + for collection in instances.children: + avalon_prop = collection.get(AVALON_PROPERTY) or {} if avalon_prop.get('id') == 'pyblish.avalon.instance': yield collection + @staticmethod + def create_instance(context, group): + avalon_prop = group[AVALON_PROPERTY] + asset = avalon_prop['asset'] + family = avalon_prop['family'] + subset = avalon_prop['subset'] + task = avalon_prop['task'] + name = f"{asset}_{subset}" + return context.create_instance( + name=name, + family=family, + families=[family], + subset=subset, + asset=asset, + task=task, + ), family + def process(self, context): """Collect the models from the current Blender scene.""" asset_groups = self.get_asset_groups() collections = self.get_collections() - for group in asset_groups: - avalon_prop = group[AVALON_PROPERTY] - asset = avalon_prop['asset'] - family = avalon_prop['family'] - subset = avalon_prop['subset'] - task = avalon_prop['task'] - name = f"{asset}_{subset}" - instance = context.create_instance( - name=name, - family=family, - families=[family], - subset=subset, - asset=asset, - task=task, - ) - objects = list(group.children) - members = set() - for obj in objects: - objects.extend(list(obj.children)) - members.add(obj) - members.add(group) - instance[:] = list(members) - self.log.debug(json.dumps(instance.data, indent=4)) - for obj in instance: - self.log.debug(obj) + instances = chain(asset_groups, collections) - for collection in collections: - avalon_prop = collection[AVALON_PROPERTY] - asset = avalon_prop['asset'] - family = avalon_prop['family'] - subset = avalon_prop['subset'] - task = avalon_prop['task'] - name = f"{asset}_{subset}" - instance = context.create_instance( - name=name, - family=family, - families=[family], - subset=subset, - asset=asset, - task=task, - ) - members = list(collection.objects) - if family == "animation": - for obj in collection.objects: - if obj.type == 'EMPTY' and obj.get(AVALON_PROPERTY): - for child in obj.children: - if child.type == 'ARMATURE': - members.append(child) - members.append(collection) + for group in instances: + instance, family = self.create_instance(context, group) + members = [] + if type(group) == bpy.types.Collection: + members = list(group.objects) + if family == "animation": + for obj in group.objects: + if obj.type == 'EMPTY' and obj.get(AVALON_PROPERTY): + members.extend( + child for child in obj.children + if child.type == 'ARMATURE') + else: + members = group.children_recursive + + members.append(group) instance[:] = members self.log.debug(json.dumps(instance.data, indent=4)) for obj in instance: From 9f82f8ee2ff35aa66f0c3447a8aefb0adff101c6 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 11 Oct 2023 14:38:23 +0100 Subject: [PATCH 0255/1233] Changed how alembic files are extracted --- .../plugins/publish/collect_instances.py | 3 +++ .../blender/plugins/publish/extract_abc.py | 3 +-- .../plugins/publish/extract_abc_model.py | 17 +++++++++++++++++ .../defaults/project_settings/blender.json | 2 +- .../schemas/schema_blender_publish.json | 8 ++++---- .../blender/server/settings/publish_plugins.py | 4 ++-- 6 files changed, 28 insertions(+), 9 deletions(-) create mode 100644 openpype/hosts/blender/plugins/publish/extract_abc_model.py diff --git a/openpype/hosts/blender/plugins/publish/collect_instances.py b/openpype/hosts/blender/plugins/publish/collect_instances.py index 4915e4a7cf..1e0db9d9ce 100644 --- a/openpype/hosts/blender/plugins/publish/collect_instances.py +++ b/openpype/hosts/blender/plugins/publish/collect_instances.py @@ -76,6 +76,9 @@ class CollectInstances(pyblish.api.ContextPlugin): else: members = group.children_recursive + if family == "pointcache": + instance.data["families"].append("abc.export") + members.append(group) instance[:] = members self.log.debug(json.dumps(instance.data, indent=4)) diff --git a/openpype/hosts/blender/plugins/publish/extract_abc.py b/openpype/hosts/blender/plugins/publish/extract_abc.py index 87159e53f0..b113685842 100644 --- a/openpype/hosts/blender/plugins/publish/extract_abc.py +++ b/openpype/hosts/blender/plugins/publish/extract_abc.py @@ -12,8 +12,7 @@ class ExtractABC(publish.Extractor): label = "Extract ABC" hosts = ["blender"] - families = ["model", "pointcache"] - optional = True + families = ["abc.export"] def process(self, instance): # Define extract output file path diff --git a/openpype/hosts/blender/plugins/publish/extract_abc_model.py b/openpype/hosts/blender/plugins/publish/extract_abc_model.py new file mode 100644 index 0000000000..b31e36c681 --- /dev/null +++ b/openpype/hosts/blender/plugins/publish/extract_abc_model.py @@ -0,0 +1,17 @@ +import pyblish.api +from openpype.pipeline import publish + + +class ExtractModelABC(publish.Extractor): + """Extract model as ABC.""" + + order = pyblish.api.ExtractorOrder - 0.1 + label = "Extract Model ABC" + hosts = ["blender"] + families = ["model"] + optional = True + + def process(self, instance): + # Add abc.export family to the instance, to allow the extraction + # as alembic of the asset. + instance.data["families"].append("abc.export") diff --git a/openpype/settings/defaults/project_settings/blender.json b/openpype/settings/defaults/project_settings/blender.json index f3eb31174f..2bc518e329 100644 --- a/openpype/settings/defaults/project_settings/blender.json +++ b/openpype/settings/defaults/project_settings/blender.json @@ -89,7 +89,7 @@ "optional": true, "active": false }, - "ExtractABC": { + "ExtractModelABC": { "enabled": true, "optional": true, "active": false diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json index 7f1a8a915b..b84c663e6c 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json @@ -181,12 +181,12 @@ "name": "template_publish_plugin", "template_data": [ { - "key": "ExtractFBX", - "label": "Extract FBX (model and rig)" + "key": "ExtractModelABC", + "label": "Extract ABC (model)" }, { - "key": "ExtractABC", - "label": "Extract ABC (model and pointcache)" + "key": "ExtractFBX", + "label": "Extract FBX (model and rig)" }, { "key": "ExtractBlendAnimation", diff --git a/server_addon/blender/server/settings/publish_plugins.py b/server_addon/blender/server/settings/publish_plugins.py index 5e047b7013..102320cfed 100644 --- a/server_addon/blender/server/settings/publish_plugins.py +++ b/server_addon/blender/server/settings/publish_plugins.py @@ -103,7 +103,7 @@ class PublishPuginsModel(BaseSettingsModel): default_factory=ValidatePluginModel, title="Extract FBX" ) - ExtractABC: ValidatePluginModel = Field( + ExtractModelABC: ValidatePluginModel = Field( default_factory=ValidatePluginModel, title="Extract ABC" ) @@ -197,7 +197,7 @@ DEFAULT_BLENDER_PUBLISH_SETTINGS = { "optional": True, "active": False }, - "ExtractABC": { + "ExtractModelABC": { "enabled": True, "optional": True, "active": False From 08e10fd59af02725011886e0e133fc0a70b084d1 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 11 Oct 2023 14:43:38 +0100 Subject: [PATCH 0256/1233] Make extraction of models as alembic on by default --- openpype/settings/defaults/project_settings/blender.json | 2 +- server_addon/blender/server/settings/publish_plugins.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/settings/defaults/project_settings/blender.json b/openpype/settings/defaults/project_settings/blender.json index 2bc518e329..7fb8c333a6 100644 --- a/openpype/settings/defaults/project_settings/blender.json +++ b/openpype/settings/defaults/project_settings/blender.json @@ -92,7 +92,7 @@ "ExtractModelABC": { "enabled": true, "optional": true, - "active": false + "active": true }, "ExtractBlendAnimation": { "enabled": true, diff --git a/server_addon/blender/server/settings/publish_plugins.py b/server_addon/blender/server/settings/publish_plugins.py index 102320cfed..27dc0b232f 100644 --- a/server_addon/blender/server/settings/publish_plugins.py +++ b/server_addon/blender/server/settings/publish_plugins.py @@ -200,7 +200,7 @@ DEFAULT_BLENDER_PUBLISH_SETTINGS = { "ExtractModelABC": { "enabled": True, "optional": True, - "active": False + "active": True }, "ExtractBlendAnimation": { "enabled": True, From 23b29c947cb59fc626047846d86cf5d714d515f5 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 11 Oct 2023 15:17:33 +0100 Subject: [PATCH 0257/1233] Improved function to create the instance --- openpype/hosts/blender/plugins/publish/collect_instances.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/collect_instances.py b/openpype/hosts/blender/plugins/publish/collect_instances.py index 1e0db9d9ce..cc163fc97e 100644 --- a/openpype/hosts/blender/plugins/publish/collect_instances.py +++ b/openpype/hosts/blender/plugins/publish/collect_instances.py @@ -53,7 +53,7 @@ class CollectInstances(pyblish.api.ContextPlugin): subset=subset, asset=asset, task=task, - ), family + ) def process(self, context): """Collect the models from the current Blender scene.""" @@ -63,7 +63,8 @@ class CollectInstances(pyblish.api.ContextPlugin): instances = chain(asset_groups, collections) for group in instances: - instance, family = self.create_instance(context, group) + instance = self.create_instance(context, group) + family = instance.data["family"] members = [] if type(group) == bpy.types.Collection: members = list(group.objects) From 70abe6b7b7576699918ef6cb818c019b888567bf Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 11 Oct 2023 15:21:44 +0100 Subject: [PATCH 0258/1233] Merged the two functions to get asset groups --- .../plugins/publish/collect_instances.py | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/collect_instances.py b/openpype/hosts/blender/plugins/publish/collect_instances.py index cc163fc97e..b4fc167638 100644 --- a/openpype/hosts/blender/plugins/publish/collect_instances.py +++ b/openpype/hosts/blender/plugins/publish/collect_instances.py @@ -1,5 +1,4 @@ import json -from itertools import chain from typing import Generator import bpy @@ -23,21 +22,11 @@ class CollectInstances(pyblish.api.ContextPlugin): """Return all instances that are empty objects asset groups. """ instances = bpy.data.collections.get(AVALON_INSTANCES) - for obj in instances.objects: + for obj in list(instances.objects) + list(instances.children): avalon_prop = obj.get(AVALON_PROPERTY) or {} if avalon_prop.get('id') == 'pyblish.avalon.instance': yield obj - @staticmethod - def get_collections() -> Generator: - """Return all instances that are collections. - """ - instances = bpy.data.collections.get(AVALON_INSTANCES) - for collection in instances.children: - avalon_prop = collection.get(AVALON_PROPERTY) or {} - if avalon_prop.get('id') == 'pyblish.avalon.instance': - yield collection - @staticmethod def create_instance(context, group): avalon_prop = group[AVALON_PROPERTY] @@ -58,11 +47,8 @@ class CollectInstances(pyblish.api.ContextPlugin): def process(self, context): """Collect the models from the current Blender scene.""" asset_groups = self.get_asset_groups() - collections = self.get_collections() - instances = chain(asset_groups, collections) - - for group in instances: + for group in asset_groups: instance = self.create_instance(context, group) family = instance.data["family"] members = [] From 0ba3e00abc688d8b5604e983feb10c21fb9fc346 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 11 Oct 2023 16:28:33 +0200 Subject: [PATCH 0259/1233] fixing missing `overrideNodes` and `requiredNodes` --- openpype/settings/ayon_settings.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index 0b3f6725d8..db3624b2d0 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -842,11 +842,20 @@ def _convert_nuke_project_settings(ayon_settings, output): ayon_imageio["regexInputs"] = regex_inputs # nodes - for item in ayon_imageio["nodes"]["required_nodes"]: + ayon_imageio_nodes = ayon_imageio["nodes"] + if ayon_imageio_nodes.get("required_nodes"): + ayon_imageio_nodes["requiredNodes"] = ( + ayon_imageio_nodes.pop("required_nodes")) + if ayon_imageio_nodes.get("override_nodes"): + ayon_imageio_nodes["overrideNodes"] = ( + ayon_imageio_nodes.pop("override_nodes")) + + for item in ayon_imageio_nodes["requiredNodes"]: if item.get("nuke_node_class"): item["nukeNodeClass"] = item["nuke_node_class"] item["knobs"] = _convert_nuke_knobs(item["knobs"]) - for item in ayon_imageio["nodes"]["override_nodes"]: + + for item in ayon_imageio["nodes"]["overrideNodes"]: if item.get("nuke_node_class"): item["nukeNodeClass"] = item["nuke_node_class"] item["knobs"] = _convert_nuke_knobs(item["knobs"]) From bdf86cffec47e08ab747799a4ae275562b0e01d3 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 11 Oct 2023 16:30:54 +0200 Subject: [PATCH 0260/1233] tunnig previous commit --- openpype/settings/ayon_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index db3624b2d0..f8ab067fca 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -855,7 +855,7 @@ def _convert_nuke_project_settings(ayon_settings, output): item["nukeNodeClass"] = item["nuke_node_class"] item["knobs"] = _convert_nuke_knobs(item["knobs"]) - for item in ayon_imageio["nodes"]["overrideNodes"]: + for item in ayon_imageio_nodes["overrideNodes"]: if item.get("nuke_node_class"): item["nukeNodeClass"] = item["nuke_node_class"] item["knobs"] = _convert_nuke_knobs(item["knobs"]) From 135f2a9e5741d7623417bd944dbc066af56fd9f4 Mon Sep 17 00:00:00 2001 From: sjt-rvx <72554834+sjt-rvx@users.noreply.github.com> Date: Wed, 11 Oct 2023 15:03:47 +0000 Subject: [PATCH 0261/1233] do not override the output argument (#5745) --- openpype/settings/ayon_settings.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index d54d71e851..3ccb18111a 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -1164,19 +1164,19 @@ def _convert_global_project_settings(ayon_settings, output, default_settings): for profile in extract_oiio_transcode_profiles: new_outputs = {} name_counter = {} - for output in profile["outputs"]: - if "name" in output: - name = output.pop("name") + for profile_output in profile["outputs"]: + if "name" in profile_output: + name = profile_output.pop("name") else: # Backwards compatibility for setting without 'name' in model - name = output["extension"] + name = profile_output["extension"] if name in new_outputs: name_counter[name] += 1 name = "{}_{}".format(name, name_counter[name]) else: name_counter[name] = 0 - new_outputs[name] = output + new_outputs[name] = profile_output profile["outputs"] = new_outputs # Extract Burnin plugin From df431b665c058d74d04f233101b3dfa419fe183b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 11 Oct 2023 17:42:26 +0200 Subject: [PATCH 0262/1233] Nuke: failing multiple thumbnails integration (#5741) * OP-7031 - fix thumbnail outputName This handles situation when ExtractReviewDataMov has multiple outputs for which are thumbnails created. This would cause an issue in integrate if thumbnail repre should be integrated. * thumbnail name the same as output name - added `delete` tag so it is not integrated - adding output preset name to thumb name if multiple bake streams - adding thumbnails to explicit cleanup paths - thumbnail file name inherited from representation name * hound * comment for py compatibility of unicode * Update openpype/hosts/nuke/plugins/publish/extract_thumbnail.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> * thumbnail path key should be `thumbnailPath` * Updates to nuke automatic test Default changed to NOT integrating thumbnail representation. * Update openpype/hosts/nuke/plugins/publish/extract_thumbnail.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> * OP-7031 - updated check for thumbnail representation To allow use this plugin as 'name' might not contain only 'thumbnail' for multiple outputs. * Remove possibility of double _ * Implement possibility of multiple thumbnails This could happen if there are multiple output as in Nuke's ExtractREviewMov --------- Co-authored-by: Jakub Jezek Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../nuke/plugins/publish/extract_thumbnail.py | 42 +++++++++++++------ .../preintegrate_thumbnail_representation.py | 28 ++++++------- .../hosts/nuke/test_publish_in_nuke.py | 4 +- 3 files changed, 45 insertions(+), 29 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py index b20df4ffe2..46288db743 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py @@ -8,6 +8,7 @@ from openpype.hosts.nuke import api as napi from openpype.hosts.nuke.api.lib import set_node_knobs_from_settings +# Python 2/3 compatibility if sys.version_info[0] >= 3: unicode = str @@ -45,11 +46,12 @@ class ExtractThumbnail(publish.Extractor): for o_name, o_data in instance.data["bakePresets"].items(): self.render_thumbnail(instance, o_name, **o_data) else: - viewer_process_swithes = { + viewer_process_switches = { "bake_viewer_process": True, "bake_viewer_input_process": True } - self.render_thumbnail(instance, None, **viewer_process_swithes) + self.render_thumbnail( + instance, None, **viewer_process_switches) def render_thumbnail(self, instance, output_name=None, **kwargs): first_frame = instance.data["frameStartHandle"] @@ -61,8 +63,6 @@ class ExtractThumbnail(publish.Extractor): # solve output name if any is set output_name = output_name or "" - if output_name: - output_name = "_" + output_name bake_viewer_process = kwargs["bake_viewer_process"] bake_viewer_input_process_node = kwargs[ @@ -166,26 +166,42 @@ class ExtractThumbnail(publish.Extractor): previous_node = dag_node temporary_nodes.append(dag_node) + thumb_name = "thumbnail" + # only add output name and + # if there are more than one bake preset + if ( + output_name + and len(instance.data.get("bakePresets", {}).keys()) > 1 + ): + thumb_name = "{}_{}".format(output_name, thumb_name) + # create write node write_node = nuke.createNode("Write") - file = fhead[:-1] + output_name + ".jpg" - name = "thumbnail" - path = os.path.join(staging_dir, file).replace("\\", "/") - instance.data["thumbnail"] = path - write_node["file"].setValue(path) + file = fhead[:-1] + thumb_name + ".jpg" + thumb_path = os.path.join(staging_dir, file).replace("\\", "/") + + # add thumbnail to cleanup + instance.context.data["cleanupFullPaths"].append(thumb_path) + + # make sure only one thumbnail path is set + # and it is existing file + instance_thumb_path = instance.data.get("thumbnailPath") + if not instance_thumb_path or not os.path.isfile(instance_thumb_path): + instance.data["thumbnailPath"] = thumb_path + + write_node["file"].setValue(thumb_path) write_node["file_type"].setValue("jpg") write_node["raw"].setValue(1) write_node.setInput(0, previous_node) temporary_nodes.append(write_node) - tags = ["thumbnail", "publish_on_farm"] repre = { - 'name': name, + 'name': thumb_name, 'ext': "jpg", - "outputName": "thumb", + "outputName": thumb_name, 'files': file, "stagingDir": staging_dir, - "tags": tags + "tags": ["thumbnail", "publish_on_farm", "delete"] } instance.data["representations"].append(repre) diff --git a/openpype/plugins/publish/preintegrate_thumbnail_representation.py b/openpype/plugins/publish/preintegrate_thumbnail_representation.py index 1c95b82c97..77bf2edba5 100644 --- a/openpype/plugins/publish/preintegrate_thumbnail_representation.py +++ b/openpype/plugins/publish/preintegrate_thumbnail_representation.py @@ -29,13 +29,12 @@ class PreIntegrateThumbnails(pyblish.api.InstancePlugin): if not repres: return - thumbnail_repre = None + thumbnail_repres = [] for repre in repres: - if repre["name"] == "thumbnail": - thumbnail_repre = repre - break + if "thumbnail" in repre.get("tags", []): + thumbnail_repres.append(repre) - if not thumbnail_repre: + if not thumbnail_repres: return family = instance.data["family"] @@ -60,14 +59,15 @@ class PreIntegrateThumbnails(pyblish.api.InstancePlugin): if not found_profile: return - thumbnail_repre.setdefault("tags", []) + for thumbnail_repre in thumbnail_repres: + thumbnail_repre.setdefault("tags", []) - if not found_profile["integrate_thumbnail"]: - if "delete" not in thumbnail_repre["tags"]: - thumbnail_repre["tags"].append("delete") - else: - if "delete" in thumbnail_repre["tags"]: - thumbnail_repre["tags"].remove("delete") + if not found_profile["integrate_thumbnail"]: + if "delete" not in thumbnail_repre["tags"]: + thumbnail_repre["tags"].append("delete") + else: + if "delete" in thumbnail_repre["tags"]: + thumbnail_repre["tags"].remove("delete") - self.log.debug( - "Thumbnail repre tags {}".format(thumbnail_repre["tags"])) + self.log.debug( + "Thumbnail repre tags {}".format(thumbnail_repre["tags"])) diff --git a/tests/integration/hosts/nuke/test_publish_in_nuke.py b/tests/integration/hosts/nuke/test_publish_in_nuke.py index bfd84e4fd5..b7bb8716c0 100644 --- a/tests/integration/hosts/nuke/test_publish_in_nuke.py +++ b/tests/integration/hosts/nuke/test_publish_in_nuke.py @@ -68,7 +68,7 @@ class TestPublishInNuke(NukeLocalPublishTestClass): name="workfileTest_task")) failures.append( - DBAssert.count_of_types(dbcon, "representation", 4)) + DBAssert.count_of_types(dbcon, "representation", 3)) additional_args = {"context.subset": "workfileTest_task", "context.ext": "nk"} @@ -85,7 +85,7 @@ class TestPublishInNuke(NukeLocalPublishTestClass): additional_args = {"context.subset": "renderTest_taskMain", "name": "thumbnail"} failures.append( - DBAssert.count_of_types(dbcon, "representation", 1, + DBAssert.count_of_types(dbcon, "representation", 0, additional_args=additional_args)) additional_args = {"context.subset": "renderTest_taskMain", From 9bdbc3a8b7f6fb0e1424ad22e47951cd58f61d2a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 11 Oct 2023 17:54:52 +0200 Subject: [PATCH 0263/1233] refactor validator for asset name to be validator of asset context renaming plugin name and changing functionality to be working with asset key and task key --- ...et_name.xml => validate_asset_context.xml} | 11 +- .../plugins/publish/validate_asset_context.py | 134 +++++++++++++++++ .../plugins/publish/validate_asset_name.py | 138 ------------------ .../publish/validate_output_resolution.py | 2 +- .../defaults/project_settings/nuke.json | 2 +- .../schemas/schema_nuke_publish.json | 2 +- .../nuke/server/settings/publish_plugins.py | 4 +- 7 files changed, 146 insertions(+), 147 deletions(-) rename openpype/hosts/nuke/plugins/publish/help/{validate_asset_name.xml => validate_asset_context.xml} (64%) create mode 100644 openpype/hosts/nuke/plugins/publish/validate_asset_context.py delete mode 100644 openpype/hosts/nuke/plugins/publish/validate_asset_name.py diff --git a/openpype/hosts/nuke/plugins/publish/help/validate_asset_name.xml b/openpype/hosts/nuke/plugins/publish/help/validate_asset_context.xml similarity index 64% rename from openpype/hosts/nuke/plugins/publish/help/validate_asset_name.xml rename to openpype/hosts/nuke/plugins/publish/help/validate_asset_context.xml index 0422917e9c..85efef799a 100644 --- a/openpype/hosts/nuke/plugins/publish/help/validate_asset_name.xml +++ b/openpype/hosts/nuke/plugins/publish/help/validate_asset_context.xml @@ -3,10 +3,13 @@ Shot/Asset name -## Invalid Shot/Asset name in subset +## Invalid node context keys and values -Following Node with name `{node_name}`: -Is in context of `{correct_name}` but Node _asset_ knob is set as `{wrong_name}`. +Following Node with name: \`{node_name}\` + +Context keys and values: \`{correct_values}\` + +Wrong keys and values: \`{wrong_values}\`. ### How to repair? @@ -15,4 +18,4 @@ Is in context of `{correct_name}` but Node _asset_ knob is set as `{wrong_name}` 3. Hit Reload button on the publisher. - \ No newline at end of file + diff --git a/openpype/hosts/nuke/plugins/publish/validate_asset_context.py b/openpype/hosts/nuke/plugins/publish/validate_asset_context.py new file mode 100644 index 0000000000..2a7b7a47d5 --- /dev/null +++ b/openpype/hosts/nuke/plugins/publish/validate_asset_context.py @@ -0,0 +1,134 @@ +# -*- coding: utf-8 -*- +"""Validate if instance asset is the same as context asset.""" +from __future__ import absolute_import + +import pyblish.api + +import openpype.hosts.nuke.api.lib as nlib + +from openpype.pipeline.publish import ( + RepairAction, + ValidateContentsOrder, + PublishXmlValidationError, + OptionalPyblishPluginMixin, + get_errored_instances_from_context +) + + +class SelectInvalidNodesAction(pyblish.api.Action): + + label = "Select Failed Node" + icon = "briefcase" + on = "failed" + + def process(self, context, plugin): + if not hasattr(plugin, "select"): + raise RuntimeError("Plug-in does not have repair method.") + + # Get the failed instances + self.log.debug("Finding failed plug-ins..") + failed_instance = get_errored_instances_from_context(context, plugin) + if failed_instance: + self.log.debug("Attempting selection ...") + plugin.select(failed_instance.pop()) + + +class ValidateCorrectAssetContext( + pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin +): + """Validator to check if instance asset context match context asset. + + When working in per-shot style you always publish data in context of + current asset (shot). This validator checks if this is so. It is optional + so it can be disabled when needed. + + Checking `asset` and `task` keys. + + Action on this validator will select invalid instances in Outliner. + """ + order = ValidateContentsOrder + label = "Validate asset context" + hosts = ["nuke"] + actions = [ + RepairAction, + SelectInvalidNodesAction, + ] + optional = True + + # TODO: apply_settigs to maintain backwards compatibility + # with `ValidateCorrectAssetName` + def process(self, instance): + if not self.is_active(instance.data): + return + + invalid_keys = self.get_invalid(instance, compute=True) + + if not invalid_keys: + return + + message_values = { + "node_name": instance.data["transientData"]["node"].name(), + "correct_values": ", ".join([ + "{} > {}".format(_key, instance.context.data[_key]) + for _key in invalid_keys + ]), + "wrong_values": ", ".join([ + "{} > {}".format(_key, instance.data.get(_key)) + for _key in invalid_keys + ]) + } + + msg = ( + "Instance `{node_name}` has wrong context keys:\n" + "Correct: `{correct_values}` | Wrong: `{wrong_values}`").format( + **message_values) + + self.log.debug(msg) + + raise PublishXmlValidationError( + self, msg, formatting_data=message_values + ) + + @classmethod + def get_invalid(cls, instance, compute=False): + invalid = instance.data.get("invalid_keys", []) + + if compute: + testing_keys = ["asset", "task"] + for _key in testing_keys: + if _key not in instance.data: + invalid.append(_key) + continue + if instance.data[_key] != instance.context.data[_key]: + invalid.append(_key) + + instance.data["invalid_keys"] = invalid + + return invalid + + @classmethod + def repair(cls, instance): + invalid = cls.get_invalid(instance) + + create_context = instance.context.data["create_context"] + + instance_id = instance.data.get("instance_id") + created_instance = create_context.get_instance_by_id( + instance_id + ) + for _key in invalid: + created_instance[_key] = instance.context.data[_key] + + create_context.save_changes() + + + @classmethod + def select(cls, instance): + invalid = cls.get_invalid(instance) + if not invalid: + return + + select_node = instance.data["transientData"]["node"] + nlib.reset_selection() + select_node["selected"].setValue(True) diff --git a/openpype/hosts/nuke/plugins/publish/validate_asset_name.py b/openpype/hosts/nuke/plugins/publish/validate_asset_name.py deleted file mode 100644 index df05f76a5b..0000000000 --- a/openpype/hosts/nuke/plugins/publish/validate_asset_name.py +++ /dev/null @@ -1,138 +0,0 @@ -# -*- coding: utf-8 -*- -"""Validate if instance asset is the same as context asset.""" -from __future__ import absolute_import - -import pyblish.api - -import openpype.hosts.nuke.api.lib as nlib - -from openpype.pipeline.publish import ( - ValidateContentsOrder, - PublishXmlValidationError, - OptionalPyblishPluginMixin -) - -class SelectInvalidInstances(pyblish.api.Action): - """Select invalid instances in Outliner.""" - - label = "Select" - 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.deselect() - 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): - for inst in instances: - if inst.data.get("transientData", {}).get("node"): - select_node = inst.data["transientData"]["node"] - select_node["selected"].setValue(True) - - def deselect(self): - nlib.reset_selection() - - -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 - 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) - self.log.debug(instances) - - context_asset = context.data["assetEntity"]["name"] - for instance in instances: - node = instance.data["transientData"]["node"] - node_data = nlib.get_node_data(node, nlib.INSTANCE_DATA_KNOB) - node_data["asset"] = context_asset - nlib.set_node_data(node, nlib.INSTANCE_DATA_KNOB, node_data) - - -class ValidateCorrectAssetName( - 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 - current asset (shot). This validator checks if this is so. It is optional - so it can be disabled when needed. - - Action on this validator will select invalid instances in Outliner. - """ - order = ValidateContentsOrder - label = "Validate correct asset name" - hosts = ["nuke"] - actions = [ - SelectInvalidInstances, - RepairSelectInvalidInstances - ] - optional = True - - def process(self, instance): - if not self.is_active(instance.data): - return - - asset = instance.data.get("asset") - context_asset = instance.context.data["assetEntity"]["name"] - node = instance.data["transientData"]["node"] - - msg = ( - "Instance `{}` has wrong shot/asset name:\n" - "Correct: `{}` | Wrong: `{}`").format( - instance.name, asset, context_asset) - - self.log.debug(msg) - - if asset != context_asset: - raise PublishXmlValidationError( - self, msg, formatting_data={ - "node_name": node.name(), - "wrong_name": asset, - "correct_name": context_asset - } - ) diff --git a/openpype/hosts/nuke/plugins/publish/validate_output_resolution.py b/openpype/hosts/nuke/plugins/publish/validate_output_resolution.py index dbcd216a84..39114c80c8 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_output_resolution.py +++ b/openpype/hosts/nuke/plugins/publish/validate_output_resolution.py @@ -23,7 +23,7 @@ class ValidateOutputResolution( order = pyblish.api.ValidatorOrder optional = True families = ["render"] - label = "Write resolution" + label = "Validate Write resolution" hosts = ["nuke"] actions = [RepairAction] diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index ad9f46c8ab..3b69ef54fd 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -341,7 +341,7 @@ "write" ] }, - "ValidateCorrectAssetName": { + "ValidateCorrectAssetContext": { "enabled": true, "optional": true, "active": true diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json index fa08e19c63..9e012e560f 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json @@ -61,7 +61,7 @@ "name": "template_publish_plugin", "template_data": [ { - "key": "ValidateCorrectAssetName", + "key": "ValidateCorrectAssetContext", "label": "Validate Correct Asset Name" } ] diff --git a/server_addon/nuke/server/settings/publish_plugins.py b/server_addon/nuke/server/settings/publish_plugins.py index 19206149b6..692b2bd240 100644 --- a/server_addon/nuke/server/settings/publish_plugins.py +++ b/server_addon/nuke/server/settings/publish_plugins.py @@ -236,7 +236,7 @@ class PublishPuginsModel(BaseSettingsModel): default_factory=CollectInstanceDataModel, section="Collectors" ) - ValidateCorrectAssetName: OptionalPluginModel = Field( + ValidateCorrectAssetContext: OptionalPluginModel = Field( title="Validate Correct Folder Name", default_factory=OptionalPluginModel, section="Validators" @@ -308,7 +308,7 @@ DEFAULT_PUBLISH_PLUGIN_SETTINGS = { "write" ] }, - "ValidateCorrectAssetName": { + "ValidateCorrectAssetContext": { "enabled": True, "optional": True, "active": True From 0498a4016d6f0f6dd1a599f9b985daba1805d317 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 11 Oct 2023 18:16:45 +0200 Subject: [PATCH 0264/1233] Loader tool: Refactor loader tool (for AYON) (#5729) * initial commitof ayon loader * tweaks in ayon utils * implemented product type filtering * products have icons and proper style * fix refresh of products * added enable grouping checkbox * added icons and sorting of grouped items * fix version delegate * add splitter between context and product type filtering * fix products filtering by name * implemented 'filter_repre_contexts_by_loader' * implemented base of action items * implemented folder underline colors * changed version items to dictionary * use 'product_id' instead of 'subset_id' * base implementation of info widget * require less to trigger action * set selection of version ids in controller * added representation widget and related logic changes * implemented actions in representations widget * handle load error * use versions for subset loader * fix representations widget * implemente "in scene" logic properly * use ayon loader in host tools * fix used function to get tasks * show actions per representation name * center window * add window flag to loader window * added 'ThumbnailPainterWidget' to tool utils * implemented thumbnails model * implement thumbnail widget * fix FolderItem args docstring * bypass bug in ayon_api * fix sorting of folders * added refresh button * added expected selection and go to current context * added information if project item is library project * added more filtering options to projects widget * added missing information abou is library to model items * remove select project item on selection change * filter out non library projects * set current context project to project combobox * change window title * fix hero version queries * move current project to the top * fix reset * change icon for library projects * added libraries separator to project widget * show libraries separator in loader * ise single line expression * library loader tool is loader tool in AYON mode * fixes in grouping model * implemented grouping logic * use loader in tray action * better initial sizes * moved 'ActionItem' to abstract * filter loaders by tool name based on current context project * formatting fixes * separate abstract classes into frontend and backend abstractions * added docstrings to abstractions * implemented 'to_data' and 'from_data' for action item options * added more docstrings * first filter representation contexts and then create action items * implemented 'refresh' method * do not reset controller in '_on_first_show' Method '_on_show_timer' will take about the reset. * 'ThumbnailPainterWidget' have more options of bg painting * do not use checkerboard in loader thumbnail * fix condition Co-authored-by: Roy Nieterau --------- Co-authored-by: Roy Nieterau --- openpype/modules/avalon_apps/avalon_app.py | 40 +- openpype/pipeline/load/__init__.py | 2 + openpype/pipeline/load/utils.py | 18 + openpype/pipeline/thumbnail.py | 10 +- openpype/tools/ayon_loader/__init__.py | 6 + openpype/tools/ayon_loader/abstract.py | 851 +++++++++++++++++ openpype/tools/ayon_loader/control.py | 343 +++++++ openpype/tools/ayon_loader/models/__init__.py | 10 + openpype/tools/ayon_loader/models/actions.py | 870 ++++++++++++++++++ openpype/tools/ayon_loader/models/products.py | 682 ++++++++++++++ .../tools/ayon_loader/models/selection.py | 85 ++ openpype/tools/ayon_loader/ui/__init__.py | 6 + .../tools/ayon_loader/ui/actions_utils.py | 118 +++ .../tools/ayon_loader/ui/folders_widget.py | 416 +++++++++ openpype/tools/ayon_loader/ui/info_widget.py | 141 +++ .../ayon_loader/ui/product_group_dialog.py | 45 + .../ayon_loader/ui/product_types_widget.py | 220 +++++ .../ayon_loader/ui/products_delegates.py | 191 ++++ .../tools/ayon_loader/ui/products_model.py | 590 ++++++++++++ .../tools/ayon_loader/ui/products_widget.py | 400 ++++++++ .../tools/ayon_loader/ui/repres_widget.py | 338 +++++++ openpype/tools/ayon_loader/ui/window.py | 511 ++++++++++ openpype/tools/ayon_utils/models/__init__.py | 3 + openpype/tools/ayon_utils/models/hierarchy.py | 94 +- openpype/tools/ayon_utils/models/projects.py | 10 +- .../tools/ayon_utils/models/thumbnails.py | 118 +++ openpype/tools/ayon_utils/widgets/__init__.py | 4 + .../ayon_utils/widgets/folders_widget.py | 27 +- .../ayon_utils/widgets/projects_widget.py | 315 ++++++- .../tools/ayon_utils/widgets/tasks_widget.py | 4 +- openpype/tools/utils/__init__.py | 3 + openpype/tools/utils/host_tools.py | 24 +- openpype/tools/utils/images/__init__.py | 56 ++ openpype/tools/utils/images/thumbnail.png | Bin 0 -> 5118 bytes .../tools/utils/thumbnail_paint_widget.py | 366 ++++++++ 35 files changed, 6843 insertions(+), 74 deletions(-) create mode 100644 openpype/tools/ayon_loader/__init__.py create mode 100644 openpype/tools/ayon_loader/abstract.py create mode 100644 openpype/tools/ayon_loader/control.py create mode 100644 openpype/tools/ayon_loader/models/__init__.py create mode 100644 openpype/tools/ayon_loader/models/actions.py create mode 100644 openpype/tools/ayon_loader/models/products.py create mode 100644 openpype/tools/ayon_loader/models/selection.py create mode 100644 openpype/tools/ayon_loader/ui/__init__.py create mode 100644 openpype/tools/ayon_loader/ui/actions_utils.py create mode 100644 openpype/tools/ayon_loader/ui/folders_widget.py create mode 100644 openpype/tools/ayon_loader/ui/info_widget.py create mode 100644 openpype/tools/ayon_loader/ui/product_group_dialog.py create mode 100644 openpype/tools/ayon_loader/ui/product_types_widget.py create mode 100644 openpype/tools/ayon_loader/ui/products_delegates.py create mode 100644 openpype/tools/ayon_loader/ui/products_model.py create mode 100644 openpype/tools/ayon_loader/ui/products_widget.py create mode 100644 openpype/tools/ayon_loader/ui/repres_widget.py create mode 100644 openpype/tools/ayon_loader/ui/window.py create mode 100644 openpype/tools/ayon_utils/models/thumbnails.py create mode 100644 openpype/tools/utils/images/__init__.py create mode 100644 openpype/tools/utils/images/thumbnail.png create mode 100644 openpype/tools/utils/thumbnail_paint_widget.py diff --git a/openpype/modules/avalon_apps/avalon_app.py b/openpype/modules/avalon_apps/avalon_app.py index a0226ecc5c..57754793c4 100644 --- a/openpype/modules/avalon_apps/avalon_app.py +++ b/openpype/modules/avalon_apps/avalon_app.py @@ -1,5 +1,6 @@ import os +from openpype import AYON_SERVER_ENABLED from openpype.modules import OpenPypeModule, ITrayModule @@ -75,20 +76,11 @@ class AvalonModule(OpenPypeModule, ITrayModule): def show_library_loader(self): if self._library_loader_window is None: - from qtpy import QtCore - from openpype.tools.libraryloader import LibraryLoaderWindow from openpype.pipeline import install_openpype_plugins - - libraryloader = LibraryLoaderWindow( - show_projects=True, - show_libraries=True - ) - # Remove always on top flag for tray - window_flags = libraryloader.windowFlags() - if window_flags | QtCore.Qt.WindowStaysOnTopHint: - window_flags ^= QtCore.Qt.WindowStaysOnTopHint - libraryloader.setWindowFlags(window_flags) - self._library_loader_window = libraryloader + if AYON_SERVER_ENABLED: + self._init_ayon_loader() + else: + self._init_library_loader() install_openpype_plugins() @@ -106,3 +98,25 @@ class AvalonModule(OpenPypeModule, ITrayModule): if self.tray_initialized: from .rest_api import AvalonRestApiResource self.rest_api_obj = AvalonRestApiResource(self, server_manager) + + def _init_library_loader(self): + from qtpy import QtCore + from openpype.tools.libraryloader import LibraryLoaderWindow + + libraryloader = LibraryLoaderWindow( + show_projects=True, + show_libraries=True + ) + # Remove always on top flag for tray + window_flags = libraryloader.windowFlags() + if window_flags | QtCore.Qt.WindowStaysOnTopHint: + window_flags ^= QtCore.Qt.WindowStaysOnTopHint + libraryloader.setWindowFlags(window_flags) + self._library_loader_window = libraryloader + + def _init_ayon_loader(self): + from openpype.tools.ayon_loader.ui import LoaderWindow + + libraryloader = LoaderWindow() + + self._library_loader_window = libraryloader diff --git a/openpype/pipeline/load/__init__.py b/openpype/pipeline/load/__init__.py index 7320a9f0e8..ca11b26211 100644 --- a/openpype/pipeline/load/__init__.py +++ b/openpype/pipeline/load/__init__.py @@ -32,6 +32,7 @@ from .utils import ( loaders_from_repre_context, loaders_from_representation, + filter_repre_contexts_by_loader, any_outdated_containers, get_outdated_containers, @@ -85,6 +86,7 @@ __all__ = ( "loaders_from_repre_context", "loaders_from_representation", + "filter_repre_contexts_by_loader", "any_outdated_containers", "get_outdated_containers", diff --git a/openpype/pipeline/load/utils.py b/openpype/pipeline/load/utils.py index b10d6032b3..c81aeff6bd 100644 --- a/openpype/pipeline/load/utils.py +++ b/openpype/pipeline/load/utils.py @@ -790,6 +790,24 @@ def loaders_from_repre_context(loaders, repre_context): ] +def filter_repre_contexts_by_loader(repre_contexts, loader): + """Filter representation contexts for loader. + + Args: + repre_contexts (list[dict[str, Ant]]): Representation context. + loader (LoaderPlugin): Loader plugin to filter contexts for. + + Returns: + list[dict[str, Any]]: Filtered representation contexts. + """ + + return [ + repre_context + for repre_context in repre_contexts + if is_compatible_loader(loader, repre_context) + ] + + def loaders_from_representation(loaders, representation): """Return all compatible loaders for a representation.""" diff --git a/openpype/pipeline/thumbnail.py b/openpype/pipeline/thumbnail.py index b2b3679450..63c55d0c19 100644 --- a/openpype/pipeline/thumbnail.py +++ b/openpype/pipeline/thumbnail.py @@ -166,8 +166,12 @@ class ServerThumbnailResolver(ThumbnailResolver): # 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) + # NOTE Use 'get_server_api_connection' because public function + # 'get_thumbnail_by_id' does not return output of 'ServerAPI' + # method. + con = ayon_api.get_server_api_connection() + if hasattr(con, "get_thumbnail_by_id"): + result = con.get_thumbnail_by_id(thumbnail_id) if result.is_valid: filepath = cache.store_thumbnail( project_name, @@ -178,7 +182,7 @@ class ServerThumbnailResolver(ThumbnailResolver): else: # Backwards compatibility for ayon api where 'get_thumbnail_by_id' # is not implemented and output is filepath - filepath = ayon_api.get_thumbnail( + filepath = con.get_thumbnail( project_name, entity_type, entity_id, thumbnail_id ) diff --git a/openpype/tools/ayon_loader/__init__.py b/openpype/tools/ayon_loader/__init__.py new file mode 100644 index 0000000000..09ecf65f3a --- /dev/null +++ b/openpype/tools/ayon_loader/__init__.py @@ -0,0 +1,6 @@ +from .control import LoaderController + + +__all__ = ( + "LoaderController", +) diff --git a/openpype/tools/ayon_loader/abstract.py b/openpype/tools/ayon_loader/abstract.py new file mode 100644 index 0000000000..45042395d9 --- /dev/null +++ b/openpype/tools/ayon_loader/abstract.py @@ -0,0 +1,851 @@ +from abc import ABCMeta, abstractmethod +import six + +from openpype.lib.attribute_definitions import ( + AbstractAttrDef, + serialize_attr_defs, + deserialize_attr_defs, +) + + +class ProductTypeItem: + """Item representing product type. + + Args: + name (str): Product type name. + icon (dict[str, Any]): Product type icon definition. + checked (bool): Is product type checked for filtering. + """ + + def __init__(self, name, icon, checked): + self.name = name + self.icon = icon + self.checked = checked + + def to_data(self): + return { + "name": self.name, + "icon": self.icon, + "checked": self.checked, + } + + @classmethod + def from_data(cls, data): + return cls(**data) + + +class ProductItem: + """Product item with it versions. + + Args: + product_id (str): Product id. + product_type (str): Product type. + product_name (str): Product name. + product_icon (dict[str, Any]): Product icon definition. + product_type_icon (dict[str, Any]): Product type icon definition. + product_in_scene (bool): Is product in scene (only when used in DCC). + group_name (str): Group name. + folder_id (str): Folder id. + folder_label (str): Folder label. + version_items (dict[str, VersionItem]): Version items by id. + """ + + def __init__( + self, + product_id, + product_type, + product_name, + product_icon, + product_type_icon, + product_in_scene, + group_name, + folder_id, + folder_label, + version_items, + ): + self.product_id = product_id + self.product_type = product_type + self.product_name = product_name + self.product_icon = product_icon + self.product_type_icon = product_type_icon + self.product_in_scene = product_in_scene + self.group_name = group_name + self.folder_id = folder_id + self.folder_label = folder_label + self.version_items = version_items + + def to_data(self): + return { + "product_id": self.product_id, + "product_type": self.product_type, + "product_name": self.product_name, + "product_icon": self.product_icon, + "product_type_icon": self.product_type_icon, + "product_in_scene": self.product_in_scene, + "group_name": self.group_name, + "folder_id": self.folder_id, + "folder_label": self.folder_label, + "version_items": { + version_id: version_item.to_data() + for version_id, version_item in self.version_items.items() + }, + } + + @classmethod + def from_data(cls, data): + version_items = { + version_id: VersionItem.from_data(version) + for version_id, version in data["version_items"].items() + } + data["version_items"] = version_items + return cls(**data) + + +class VersionItem: + """Version item. + + Object have implemented comparison operators to be sortable. + + Args: + version_id (str): Version id. + version (int): Version. Can be negative when is hero version. + is_hero (bool): Is hero version. + product_id (str): Product id. + thumbnail_id (Union[str, None]): Thumbnail id. + published_time (Union[str, None]): Published time in format + '%Y%m%dT%H%M%SZ'. + author (Union[str, None]): Author. + frame_range (Union[str, None]): Frame range. + duration (Union[int, None]): Duration. + handles (Union[str, None]): Handles. + step (Union[int, None]): Step. + comment (Union[str, None]): Comment. + source (Union[str, None]): Source. + """ + + def __init__( + self, + version_id, + version, + is_hero, + product_id, + thumbnail_id, + published_time, + author, + frame_range, + duration, + handles, + step, + comment, + source + ): + self.version_id = version_id + self.product_id = product_id + self.thumbnail_id = thumbnail_id + self.version = version + self.is_hero = is_hero + self.published_time = published_time + self.author = author + self.frame_range = frame_range + self.duration = duration + self.handles = handles + self.step = step + self.comment = comment + self.source = source + + def __eq__(self, other): + if not isinstance(other, VersionItem): + return False + return ( + self.is_hero == other.is_hero + and self.version == other.version + and self.version_id == other.version_id + and self.product_id == other.product_id + ) + + def __ne__(self, other): + return not self.__eq__(other) + + def __gt__(self, other): + if not isinstance(other, VersionItem): + return False + if ( + other.version == self.version + and self.is_hero + ): + return True + return other.version < self.version + + def to_data(self): + return { + "version_id": self.version_id, + "product_id": self.product_id, + "thumbnail_id": self.thumbnail_id, + "version": self.version, + "is_hero": self.is_hero, + "published_time": self.published_time, + "author": self.author, + "frame_range": self.frame_range, + "duration": self.duration, + "handles": self.handles, + "step": self.step, + "comment": self.comment, + "source": self.source, + } + + @classmethod + def from_data(cls, data): + return cls(**data) + + +class RepreItem: + """Representation item. + + Args: + representation_id (str): Representation id. + representation_name (str): Representation name. + representation_icon (dict[str, Any]): Representation icon definition. + product_name (str): Product name. + folder_label (str): Folder label. + """ + + def __init__( + self, + representation_id, + representation_name, + representation_icon, + product_name, + folder_label, + ): + self.representation_id = representation_id + self.representation_name = representation_name + self.representation_icon = representation_icon + self.product_name = product_name + self.folder_label = folder_label + + def to_data(self): + return { + "representation_id": self.representation_id, + "representation_name": self.representation_name, + "representation_icon": self.representation_icon, + "product_name": self.product_name, + "folder_label": self.folder_label, + } + + @classmethod + def from_data(cls, data): + return cls(**data) + + +class ActionItem: + """Action item that can be triggered. + + Action item is defined for a specific context. To trigger the action + use 'identifier' and context, it necessary also use 'options'. + + Args: + identifier (str): Action identifier. + label (str): Action label. + icon (dict[str, Any]): Action icon definition. + tooltip (str): Action tooltip. + options (Union[list[AbstractAttrDef], list[qargparse.QArgument]]): + Action options. Note: 'qargparse' is considered as deprecated. + order (int): Action order. + project_name (str): Project name. + folder_ids (list[str]): Folder ids. + product_ids (list[str]): Product ids. + version_ids (list[str]): Version ids. + representation_ids (list[str]): Representation ids. + """ + + def __init__( + self, + identifier, + label, + icon, + tooltip, + options, + order, + project_name, + folder_ids, + product_ids, + version_ids, + representation_ids, + ): + self.identifier = identifier + self.label = label + self.icon = icon + self.tooltip = tooltip + self.options = options + self.order = order + self.project_name = project_name + self.folder_ids = folder_ids + self.product_ids = product_ids + self.version_ids = version_ids + self.representation_ids = representation_ids + + def _options_to_data(self): + options = self.options + if not options: + return options + if isinstance(options[0], AbstractAttrDef): + return serialize_attr_defs(options) + # NOTE: Data conversion is not used by default in loader tool. But for + # future development of detached UI tools it would be better to be + # prepared for it. + raise NotImplementedError( + "{}.to_data is not implemented. Use Attribute definitions" + " from 'openpype.lib' instead of 'qargparse'.".format( + self.__class__.__name__ + ) + ) + + def to_data(self): + options = self._options_to_data() + return { + "identifier": self.identifier, + "label": self.label, + "icon": self.icon, + "tooltip": self.tooltip, + "options": options, + "order": self.order, + "project_name": self.project_name, + "folder_ids": self.folder_ids, + "product_ids": self.product_ids, + "version_ids": self.version_ids, + "representation_ids": self.representation_ids, + } + + @classmethod + def from_data(cls, data): + options = data["options"] + if options: + options = deserialize_attr_defs(options) + data["options"] = options + return cls(**data) + + +@six.add_metaclass(ABCMeta) +class _BaseLoaderController(object): + """Base loader controller abstraction. + + Abstract base class that is required for both frontend and backed. + """ + + @abstractmethod + def get_current_context(self): + """Current context is a context of the current scene. + + Example output: + { + "project_name": "MyProject", + "folder_id": "0011223344-5566778-99", + "task_name": "Compositing", + } + + Returns: + dict[str, Union[str, None]]: Context data. + """ + + pass + + @abstractmethod + def reset(self): + """Reset all cached data to reload everything. + + Triggers events "controller.reset.started" and + "controller.reset.finished". + """ + + pass + + # Model wrappers + @abstractmethod + def get_folder_items(self, project_name, sender=None): + """Folder items for a project. + + Args: + project_name (str): Project name. + sender (Optional[str]): Sender who requested the name. + + Returns: + list[FolderItem]: Folder items for the project. + """ + + pass + + # Expected selection helpers + @abstractmethod + def get_expected_selection_data(self): + """Full expected selection information. + + Expected selection is a selection that may not be yet selected in UI + e.g. because of refreshing, this data tell the UI what should be + selected when they finish their refresh. + + Returns: + dict[str, Any]: Expected selection data. + """ + + pass + + @abstractmethod + def set_expected_selection(self, project_name, folder_id): + """Set expected selection. + + Args: + project_name (str): Name of project to be selected. + folder_id (str): Id of folder to be selected. + """ + + pass + + +class BackendLoaderController(_BaseLoaderController): + """Backend loader controller abstraction. + + What backend logic requires from a controller for proper logic. + """ + + @abstractmethod + def emit_event(self, topic, data=None, source=None): + """Emit event with a certain topic, data and source. + + The event should be sent to both frontend and backend. + + Args: + topic (str): Event topic name. + data (Optional[dict[str, Any]]): Event data. + source (Optional[str]): Event source. + """ + + pass + + @abstractmethod + def get_loaded_product_ids(self): + """Return set of loaded product ids. + + Returns: + set[str]: Set of loaded product ids. + """ + + pass + + +class FrontendLoaderController(_BaseLoaderController): + @abstractmethod + def register_event_callback(self, topic, callback): + """Register callback for an event topic. + + Args: + topic (str): Event topic name. + callback (func): Callback triggered when the event is emitted. + """ + + pass + + # Expected selection helpers + @abstractmethod + def expected_project_selected(self, project_name): + """Expected project was selected in frontend. + + Args: + project_name (str): Project name. + """ + + pass + + @abstractmethod + def expected_folder_selected(self, folder_id): + """Expected folder was selected in frontend. + + Args: + folder_id (str): Folder id. + """ + + pass + + # Model wrapper calls + @abstractmethod + def get_project_items(self, sender=None): + """Items for all projects available on server. + + Triggers event topics "projects.refresh.started" and + "projects.refresh.finished" with data: + { + "sender": sender + } + + Notes: + Filtering of projects is done in UI. + + Args: + sender (Optional[str]): Sender who requested the items. + + Returns: + list[ProjectItem]: List of project items. + """ + + pass + + @abstractmethod + def get_product_items(self, project_name, folder_ids, sender=None): + """Product items for folder ids. + + Triggers event topics "products.refresh.started" and + "products.refresh.finished" with data: + { + "project_name": project_name, + "folder_ids": folder_ids, + "sender": sender + } + + Args: + project_name (str): Project name. + folder_ids (Iterable[str]): Folder ids. + sender (Optional[str]): Sender who requested the items. + + Returns: + list[ProductItem]: List of product items. + """ + + pass + + @abstractmethod + def get_product_item(self, project_name, product_id): + """Receive single product item. + + Args: + project_name (str): Project name. + product_id (str): Product id. + + Returns: + Union[ProductItem, None]: Product info or None if not found. + """ + + pass + + @abstractmethod + def get_product_type_items(self, project_name): + """Product type items for a project. + + Product types have defined if are checked for filtering or not. + + Returns: + list[ProductTypeItem]: List of product type items for a project. + """ + + pass + + @abstractmethod + def get_representation_items( + self, project_name, version_ids, sender=None + ): + """Representation items for version ids. + + Triggers event topics "model.representations.refresh.started" and + "model.representations.refresh.finished" with data: + { + "project_name": project_name, + "version_ids": version_ids, + "sender": sender + } + + Args: + project_name (str): Project name. + version_ids (Iterable[str]): Version ids. + sender (Optional[str]): Sender who requested the items. + + Returns: + list[RepreItem]: List of representation items. + """ + + pass + + @abstractmethod + def get_version_thumbnail_ids(self, project_name, version_ids): + """Get thumbnail ids for version ids. + + Args: + project_name (str): Project name. + version_ids (Iterable[str]): Version ids. + + Returns: + dict[str, Union[str, Any]]: Thumbnail id by version id. + """ + + pass + + @abstractmethod + def get_folder_thumbnail_ids(self, project_name, folder_ids): + """Get thumbnail ids for folder ids. + + Args: + project_name (str): Project name. + folder_ids (Iterable[str]): Folder ids. + + Returns: + dict[str, Union[str, Any]]: Thumbnail id by folder id. + """ + + pass + + @abstractmethod + def get_thumbnail_path(self, project_name, thumbnail_id): + """Get thumbnail path for thumbnail id. + + This method should get a path to a thumbnail based on thumbnail id. + Which probably means to download the thumbnail from server and store + it locally. + + Args: + project_name (str): Project name. + thumbnail_id (str): Thumbnail id. + + Returns: + Union[str, None]: Thumbnail path or None if not found. + """ + + pass + + # Selection model wrapper calls + @abstractmethod + def get_selected_project_name(self): + """Get selected project name. + + The information is based on last selection from UI. + + Returns: + Union[str, None]: Selected project name. + """ + + pass + + @abstractmethod + def get_selected_folder_ids(self): + """Get selected folder ids. + + The information is based on last selection from UI. + + Returns: + list[str]: Selected folder ids. + """ + + pass + + @abstractmethod + def get_selected_version_ids(self): + """Get selected version ids. + + The information is based on last selection from UI. + + Returns: + list[str]: Selected version ids. + """ + + pass + + @abstractmethod + def get_selected_representation_ids(self): + """Get selected representation ids. + + The information is based on last selection from UI. + + Returns: + list[str]: Selected representation ids. + """ + + pass + + @abstractmethod + def set_selected_project(self, project_name): + """Set selected project. + + Project selection changed in UI. Method triggers event with topic + "selection.project.changed" with data: + { + "project_name": self._project_name + } + + Args: + project_name (Union[str, None]): Selected project name. + """ + + pass + + @abstractmethod + def set_selected_folders(self, folder_ids): + """Set selected folders. + + Folder selection changed in UI. Method triggers event with topic + "selection.folders.changed" with data: + { + "project_name": project_name, + "folder_ids": folder_ids + } + + Args: + folder_ids (Iterable[str]): Selected folder ids. + """ + + pass + + @abstractmethod + def set_selected_versions(self, version_ids): + """Set selected versions. + + Version selection changed in UI. Method triggers event with topic + "selection.versions.changed" with data: + { + "project_name": project_name, + "folder_ids": folder_ids, + "version_ids": version_ids + } + + Args: + version_ids (Iterable[str]): Selected version ids. + """ + + pass + + @abstractmethod + def set_selected_representations(self, repre_ids): + """Set selected representations. + + Representation selection changed in UI. Method triggers event with + topic "selection.representations.changed" with data: + { + "project_name": project_name, + "folder_ids": folder_ids, + "version_ids": version_ids, + "representation_ids": representation_ids + } + + Args: + repre_ids (Iterable[str]): Selected representation ids. + """ + + pass + + # Load action items + @abstractmethod + def get_versions_action_items(self, project_name, version_ids): + """Action items for versions selection. + + Args: + project_name (str): Project name. + version_ids (Iterable[str]): Version ids. + + Returns: + list[ActionItem]: List of action items. + """ + + pass + + @abstractmethod + def get_representations_action_items( + self, project_name, representation_ids + ): + """Action items for representations selection. + + Args: + project_name (str): Project name. + representation_ids (Iterable[str]): Representation ids. + + Returns: + list[ActionItem]: List of action items. + """ + + pass + + @abstractmethod + def trigger_action_item( + self, + identifier, + options, + project_name, + version_ids, + representation_ids + ): + """Trigger action item. + + Triggers event "load.started" with data: + { + "identifier": identifier, + "id": , + } + + And triggers "load.finished" with data: + { + "identifier": identifier, + "id": , + "error_info": [...], + } + + Args: + identifier (str): Action identifier. + options (dict[str, Any]): Action option values from UI. + project_name (str): Project name. + version_ids (Iterable[str]): Version ids. + representation_ids (Iterable[str]): Representation ids. + """ + + pass + + @abstractmethod + def change_products_group(self, project_name, product_ids, group_name): + """Change group of products. + + Triggers event "products.group.changed" with data: + { + "project_name": project_name, + "folder_ids": folder_ids, + "product_ids": product_ids, + "group_name": group_name, + } + + Args: + project_name (str): Project name. + product_ids (Iterable[str]): Product ids. + group_name (str): New group name. + """ + + pass + + @abstractmethod + def fill_root_in_source(self, source): + """Fill root in source path. + + Args: + source (Union[str, None]): Source of a published version. Usually + rootless workfile path. + """ + + pass + + # NOTE: Methods 'is_loaded_products_supported' and + # 'is_standard_projects_filter_enabled' are both based on being in host + # or not. Maybe we could implement only single method 'is_in_host'? + @abstractmethod + def is_loaded_products_supported(self): + """Is capable to get information about loaded products. + + Returns: + bool: True if it is supported. + """ + + pass + + @abstractmethod + def is_standard_projects_filter_enabled(self): + """Is standard projects filter enabled. + + This is used for filtering out when loader tool is used in a host. In + that case only current project and library projects should be shown. + + Returns: + bool: Frontend should filter out non-library projects, except + current context project. + """ + + pass diff --git a/openpype/tools/ayon_loader/control.py b/openpype/tools/ayon_loader/control.py new file mode 100644 index 0000000000..2b779f5c2e --- /dev/null +++ b/openpype/tools/ayon_loader/control.py @@ -0,0 +1,343 @@ +import logging + +import ayon_api + +from openpype.lib.events import QueuedEventSystem +from openpype.pipeline import Anatomy, get_current_context +from openpype.host import ILoadHost +from openpype.tools.ayon_utils.models import ( + ProjectsModel, + HierarchyModel, + NestedCacheItem, + CacheItem, + ThumbnailsModel, +) + +from .abstract import BackendLoaderController, FrontendLoaderController +from .models import SelectionModel, ProductsModel, LoaderActionsModel + + +class ExpectedSelection: + def __init__(self, controller): + self._project_name = None + self._folder_id = None + + self._project_selected = True + self._folder_selected = True + + self._controller = controller + + def _emit_change(self): + self._controller.emit_event( + "expected_selection_changed", + self.get_expected_selection_data(), + ) + + def set_expected_selection(self, project_name, folder_id): + self._project_name = project_name + self._folder_id = folder_id + + self._project_selected = False + self._folder_selected = False + self._emit_change() + + def get_expected_selection_data(self): + project_current = False + folder_current = False + if not self._project_selected: + project_current = True + elif not self._folder_selected: + folder_current = True + return { + "project": { + "name": self._project_name, + "current": project_current, + "selected": self._project_selected, + }, + "folder": { + "id": self._folder_id, + "current": folder_current, + "selected": self._folder_selected, + }, + } + + def is_expected_project_selected(self, project_name): + return project_name == self._project_name and self._project_selected + + def is_expected_folder_selected(self, folder_id): + return folder_id == self._folder_id and self._folder_selected + + def expected_project_selected(self, project_name): + if project_name != self._project_name: + return False + self._project_selected = True + self._emit_change() + return True + + def expected_folder_selected(self, folder_id): + if folder_id != self._folder_id: + return False + self._folder_selected = True + self._emit_change() + return True + + +class LoaderController(BackendLoaderController, FrontendLoaderController): + """ + + Args: + host (Optional[AbstractHost]): Host object. Defaults to None. + """ + + def __init__(self, host=None): + self._log = None + self._host = host + + self._event_system = self._create_event_system() + + self._project_anatomy_cache = NestedCacheItem( + levels=1, lifetime=60) + self._loaded_products_cache = CacheItem( + default_factory=set, lifetime=60) + + self._selection_model = SelectionModel(self) + self._expected_selection = ExpectedSelection(self) + self._projects_model = ProjectsModel(self) + self._hierarchy_model = HierarchyModel(self) + self._products_model = ProductsModel(self) + self._loader_actions_model = LoaderActionsModel(self) + self._thumbnails_model = ThumbnailsModel() + + @property + def log(self): + if self._log is None: + self._log = logging.getLogger(self.__class__.__name__) + return self._log + + # --------------------------------- + # Implementation of abstract methods + # --------------------------------- + # Events system + 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 reset(self): + self._emit_event("controller.reset.started") + + project_name = self.get_selected_project_name() + folder_ids = self.get_selected_folder_ids() + + self._project_anatomy_cache.reset() + self._loaded_products_cache.reset() + + self._products_model.reset() + self._hierarchy_model.reset() + self._loader_actions_model.reset() + self._projects_model.reset() + self._thumbnails_model.reset() + + self._projects_model.refresh() + + if not project_name and not folder_ids: + context = self.get_current_context() + project_name = context["project_name"] + folder_id = context["folder_id"] + self.set_expected_selection(project_name, folder_id) + + self._emit_event("controller.reset.finished") + + # Expected selection helpers + def get_expected_selection_data(self): + return self._expected_selection.get_expected_selection_data() + + def set_expected_selection(self, project_name, folder_id): + self._expected_selection.set_expected_selection( + project_name, folder_id + ) + + def expected_project_selected(self, project_name): + self._expected_selection.expected_project_selected(project_name) + + def expected_folder_selected(self, folder_id): + self._expected_selection.expected_folder_selected(folder_id) + + # Entity model wrappers + def get_project_items(self, sender=None): + return self._projects_model.get_project_items(sender) + + def get_folder_items(self, project_name, sender=None): + return self._hierarchy_model.get_folder_items(project_name, sender) + + def get_product_items(self, project_name, folder_ids, sender=None): + return self._products_model.get_product_items( + project_name, folder_ids, sender) + + def get_product_item(self, project_name, product_id): + return self._products_model.get_product_item( + project_name, product_id + ) + + def get_product_type_items(self, project_name): + return self._products_model.get_product_type_items(project_name) + + def get_representation_items( + self, project_name, version_ids, sender=None + ): + return self._products_model.get_repre_items( + project_name, version_ids, sender + ) + + def get_folder_thumbnail_ids(self, project_name, folder_ids): + return self._thumbnails_model.get_folder_thumbnail_ids( + project_name, folder_ids) + + def get_version_thumbnail_ids(self, project_name, version_ids): + return self._thumbnails_model.get_version_thumbnail_ids( + project_name, version_ids) + + def get_thumbnail_path(self, project_name, thumbnail_id): + return self._thumbnails_model.get_thumbnail_path( + project_name, thumbnail_id + ) + + def change_products_group(self, project_name, product_ids, group_name): + self._products_model.change_products_group( + project_name, product_ids, group_name + ) + + def get_versions_action_items(self, project_name, version_ids): + return self._loader_actions_model.get_versions_action_items( + project_name, version_ids) + + def get_representations_action_items( + self, project_name, representation_ids): + return self._loader_actions_model.get_representations_action_items( + project_name, representation_ids) + + def trigger_action_item( + self, + identifier, + options, + project_name, + version_ids, + representation_ids + ): + self._loader_actions_model.trigger_action_item( + identifier, + options, + project_name, + version_ids, + representation_ids + ) + + # Selection model wrappers + def get_selected_project_name(self): + return self._selection_model.get_selected_project_name() + + def set_selected_project(self, project_name): + self._selection_model.set_selected_project(project_name) + + # Selection model wrappers + def get_selected_folder_ids(self): + return self._selection_model.get_selected_folder_ids() + + def set_selected_folders(self, folder_ids): + self._selection_model.set_selected_folders(folder_ids) + + def get_selected_version_ids(self): + return self._selection_model.get_selected_version_ids() + + def set_selected_versions(self, version_ids): + self._selection_model.set_selected_versions(version_ids) + + def get_selected_representation_ids(self): + return self._selection_model.get_selected_representation_ids() + + def set_selected_representations(self, repre_ids): + self._selection_model.set_selected_representations(repre_ids) + + def fill_root_in_source(self, source): + project_name = self.get_selected_project_name() + anatomy = self._get_project_anatomy(project_name) + if anatomy is None: + return source + + try: + return anatomy.fill_root(source) + except Exception: + return source + + def get_current_context(self): + if self._host is None: + return { + "project_name": None, + "folder_id": None, + "task_name": None, + } + if hasattr(self._host, "get_current_context"): + context = self._host.get_current_context() + else: + context = get_current_context() + folder_id = None + project_name = context.get("project_name") + asset_name = context.get("asset_name") + if project_name and asset_name: + folder = ayon_api.get_folder_by_name( + project_name, asset_name, fields=["id"] + ) + if folder: + folder_id = folder["id"] + return { + "project_name": project_name, + "folder_id": folder_id, + "task_name": context.get("task_name"), + } + + def get_loaded_product_ids(self): + if self._host is None: + return set() + + context = self.get_current_context() + project_name = context["project_name"] + if not project_name: + return set() + + if not self._loaded_products_cache.is_valid: + if isinstance(self._host, ILoadHost): + containers = self._host.get_containers() + else: + containers = self._host.ls() + repre_ids = {c.get("representation") for c in containers} + repre_ids.discard(None) + product_ids = self._products_model.get_product_ids_by_repre_ids( + project_name, repre_ids + ) + self._loaded_products_cache.update_data(product_ids) + return self._loaded_products_cache.get_data() + + def is_loaded_products_supported(self): + return self._host is not None + + def is_standard_projects_filter_enabled(self): + return self._host is not None + + def _get_project_anatomy(self, project_name): + if not project_name: + return None + cache = self._project_anatomy_cache[project_name] + if not cache.is_valid: + cache.update_data(Anatomy(project_name)) + return cache.get_data() + + def _create_event_system(self): + return QueuedEventSystem() + + def _emit_event(self, topic, data=None): + self._event_system.emit(topic, data or {}, "controller") diff --git a/openpype/tools/ayon_loader/models/__init__.py b/openpype/tools/ayon_loader/models/__init__.py new file mode 100644 index 0000000000..6adfe71d86 --- /dev/null +++ b/openpype/tools/ayon_loader/models/__init__.py @@ -0,0 +1,10 @@ +from .selection import SelectionModel +from .products import ProductsModel +from .actions import LoaderActionsModel + + +__all__ = ( + "SelectionModel", + "ProductsModel", + "LoaderActionsModel", +) diff --git a/openpype/tools/ayon_loader/models/actions.py b/openpype/tools/ayon_loader/models/actions.py new file mode 100644 index 0000000000..3edb04e9eb --- /dev/null +++ b/openpype/tools/ayon_loader/models/actions.py @@ -0,0 +1,870 @@ +import sys +import traceback +import inspect +import copy +import collections +import uuid + +from openpype.client import ( + get_project, + get_assets, + get_subsets, + get_versions, + get_representations, +) +from openpype.pipeline.load import ( + discover_loader_plugins, + SubsetLoaderPlugin, + filter_repre_contexts_by_loader, + get_loader_identifier, + load_with_repre_context, + load_with_subset_context, + load_with_subset_contexts, + LoadError, + IncompatibleLoaderError, +) +from openpype.tools.ayon_utils.models import NestedCacheItem +from openpype.tools.ayon_loader.abstract import ActionItem + +ACTIONS_MODEL_SENDER = "actions.model" +NOT_SET = object() + + +class LoaderActionsModel: + """Model for loader actions. + + This is probably only part of models that requires to use codebase from + 'openpype.client' because of backwards compatibility with loaders logic + which are expecting mongo documents. + + TODOs: + Deprecate 'qargparse' usage in loaders and implement conversion + of 'ActionItem' to data (and 'from_data'). + Use controller to get entities (documents) -> possible only when + loaders are able to handle AYON vs. OpenPype logic. + Add missing site sync logic, and if possible remove it from loaders. + Implement loader actions to replace load plugins. + Ask loader actions to return action items instead of guessing them. + """ + + # Cache loader plugins for some time + # NOTE Set to '0' for development + loaders_cache_lifetime = 30 + + def __init__(self, controller): + self._controller = controller + self._current_context_project = NOT_SET + self._loaders_by_identifier = NestedCacheItem( + levels=1, lifetime=self.loaders_cache_lifetime) + self._product_loaders = NestedCacheItem( + levels=1, lifetime=self.loaders_cache_lifetime) + self._repre_loaders = NestedCacheItem( + levels=1, lifetime=self.loaders_cache_lifetime) + + def reset(self): + """Reset the model with all cached items.""" + + self._current_context_project = NOT_SET + self._loaders_by_identifier.reset() + self._product_loaders.reset() + self._repre_loaders.reset() + + def get_versions_action_items(self, project_name, version_ids): + """Get action items for given version ids. + + Args: + project_name (str): Project name. + version_ids (Iterable[str]): Version ids. + + Returns: + list[ActionItem]: List of action items. + """ + + ( + version_context_by_id, + repre_context_by_id + ) = self._contexts_for_versions( + project_name, + version_ids + ) + return self._get_action_items_for_contexts( + project_name, + version_context_by_id, + repre_context_by_id + ) + + def get_representations_action_items( + self, project_name, representation_ids + ): + """Get action items for given representation ids. + + Args: + project_name (str): Project name. + representation_ids (Iterable[str]): Representation ids. + + Returns: + list[ActionItem]: List of action items. + """ + + ( + product_context_by_id, + repre_context_by_id + ) = self._contexts_for_representations( + project_name, + representation_ids + ) + return self._get_action_items_for_contexts( + project_name, + product_context_by_id, + repre_context_by_id + ) + + def trigger_action_item( + self, + identifier, + options, + project_name, + version_ids, + representation_ids + ): + """Trigger action by identifier. + + Triggers the action by identifier for given contexts. + + Triggers events "load.started" and "load.finished". Finished event + also contains "error_info" key with error information if any + happened. + + Args: + identifier (str): Loader identifier. + options (dict[str, Any]): Loader option values. + project_name (str): Project name. + version_ids (Iterable[str]): Version ids. + representation_ids (Iterable[str]): Representation ids. + """ + + event_data = { + "identifier": identifier, + "id": uuid.uuid4().hex, + } + self._controller.emit_event( + "load.started", + event_data, + ACTIONS_MODEL_SENDER, + ) + loader = self._get_loader_by_identifier(project_name, identifier) + if representation_ids is not None: + error_info = self._trigger_representation_loader( + loader, + options, + project_name, + representation_ids, + ) + elif version_ids is not None: + error_info = self._trigger_version_loader( + loader, + options, + project_name, + version_ids, + ) + else: + raise NotImplementedError( + "Invalid arguments to trigger action item") + + event_data["error_info"] = error_info + self._controller.emit_event( + "load.finished", + event_data, + ACTIONS_MODEL_SENDER, + ) + + def _get_current_context_project(self): + """Get current context project name. + + The value is based on controller (host) and cached. + + Returns: + Union[str, None]: Current context project. + """ + + if self._current_context_project is NOT_SET: + context = self._controller.get_current_context() + self._current_context_project = context["project_name"] + return self._current_context_project + + def _get_action_label(self, loader, representation=None): + """Pull label info from loader class. + + Args: + loader (LoaderPlugin): Plugin class. + representation (Optional[dict[str, Any]]): Representation data. + + Returns: + str: Action label. + """ + + label = getattr(loader, "label", None) + if label is None: + label = loader.__name__ + if representation: + # Add the representation as suffix + label = "{} ({})".format(label, representation["name"]) + return label + + def _get_action_icon(self, loader): + """Pull icon info from loader class. + + Args: + loader (LoaderPlugin): Plugin class. + + Returns: + Union[dict[str, Any], None]: Icon definition based on + loader plugin. + """ + + # Support font-awesome icons using the `.icon` and `.color` + # attributes on plug-ins. + icon = getattr(loader, "icon", None) + if icon is not None and not isinstance(icon, dict): + icon = { + "type": "awesome-font", + "name": icon, + "color": getattr(loader, "color", None) or "white" + } + return icon + + def _get_action_tooltip(self, loader): + """Pull tooltip info from loader class. + + Args: + loader (LoaderPlugin): Plugin class. + + Returns: + str: Action tooltip. + """ + + # Add tooltip and statustip from Loader docstring + return inspect.getdoc(loader) + + def _filter_loaders_by_tool_name(self, project_name, loaders): + """Filter loaders by tool name. + + Tool names are based on OpenPype tools loader tool and library + loader tool. The new tool merged both into one tool and the difference + is based only on current project name. + + Args: + project_name (str): Project name. + loaders (list[LoaderPlugin]): List of loader plugins. + + Returns: + list[LoaderPlugin]: Filtered list of loader plugins. + """ + + # Keep filtering by tool name + # - if current context project name is same as project name we do + # expect the tool is used as OpenPype loader tool, otherwise + # as library loader tool. + if project_name == self._get_current_context_project(): + tool_name = "loader" + else: + tool_name = "library_loader" + filtered_loaders = [] + for loader in loaders: + tool_names = getattr(loader, "tool_names", None) + if ( + tool_names is None + or "*" in tool_names + or tool_name in tool_names + ): + filtered_loaders.append(loader) + return filtered_loaders + + def _create_loader_action_item( + self, + loader, + contexts, + project_name, + folder_ids=None, + product_ids=None, + version_ids=None, + representation_ids=None, + repre_name=None, + ): + label = self._get_action_label(loader) + if repre_name: + label = "{} ({})".format(label, repre_name) + return ActionItem( + get_loader_identifier(loader), + label=label, + icon=self._get_action_icon(loader), + tooltip=self._get_action_tooltip(loader), + options=loader.get_options(contexts), + order=loader.order, + project_name=project_name, + folder_ids=folder_ids, + product_ids=product_ids, + version_ids=version_ids, + representation_ids=representation_ids, + ) + + def _get_loaders(self, project_name): + """Loaders with loaded settings for a project. + + Questions: + Project name is required because of settings. Should we actually + pass in current project name instead of project name where + we want to show loaders for? + + Returns: + tuple[list[SubsetLoaderPlugin], list[LoaderPlugin]]: Discovered + loader plugins. + """ + + loaders_by_identifier_c = self._loaders_by_identifier[project_name] + product_loaders_c = self._product_loaders[project_name] + repre_loaders_c = self._repre_loaders[project_name] + if loaders_by_identifier_c.is_valid: + return product_loaders_c.get_data(), repre_loaders_c.get_data() + + # Get all representation->loader combinations available for the + # index under the cursor, so we can list the user the options. + available_loaders = self._filter_loaders_by_tool_name( + project_name, discover_loader_plugins(project_name) + ) + + repre_loaders = [] + product_loaders = [] + loaders_by_identifier = {} + for loader_cls in available_loaders: + if not loader_cls.enabled: + continue + + identifier = get_loader_identifier(loader_cls) + loaders_by_identifier[identifier] = loader_cls + if issubclass(loader_cls, SubsetLoaderPlugin): + product_loaders.append(loader_cls) + else: + repre_loaders.append(loader_cls) + + loaders_by_identifier_c.update_data(loaders_by_identifier) + product_loaders_c.update_data(product_loaders) + repre_loaders_c.update_data(repre_loaders) + return product_loaders, repre_loaders + + def _get_loader_by_identifier(self, project_name, identifier): + if not self._loaders_by_identifier[project_name].is_valid: + self._get_loaders(project_name) + loaders_by_identifier_c = self._loaders_by_identifier[project_name] + loaders_by_identifier = loaders_by_identifier_c.get_data() + return loaders_by_identifier.get(identifier) + + def _actions_sorter(self, action_item): + """Sort the Loaders by their order and then their name. + + Returns: + tuple[int, str]: Sort keys. + """ + + return action_item.order, action_item.label + + def _get_version_docs(self, project_name, version_ids): + """Get version documents for given version ids. + + This function also handles hero versions and copies data from + source version to it. + + Todos: + Remove this function when this is completely rewritten to + use AYON calls. + """ + + version_docs = list(get_versions( + project_name, version_ids=version_ids, hero=True + )) + hero_versions_by_src_id = collections.defaultdict(list) + src_hero_version = set() + for version_doc in version_docs: + if version_doc["type"] != "hero": + continue + version_id = "" + src_hero_version.add(version_id) + hero_versions_by_src_id[version_id].append(version_doc) + + src_versions = [] + if src_hero_version: + src_versions = get_versions(project_name, version_ids=version_ids) + for src_version in src_versions: + src_version_id = src_version["_id"] + for hero_version in hero_versions_by_src_id[src_version_id]: + hero_version["data"] = copy.deepcopy(src_version["data"]) + + return version_docs + + def _contexts_for_versions(self, project_name, version_ids): + """Get contexts for given version ids. + + Prepare version contexts for 'SubsetLoaderPlugin' and representation + contexts for 'LoaderPlugin' for all children representations of + given versions. + + This method is very similar to '_contexts_for_representations' but the + queries of documents are called in a different order. + + Args: + project_name (str): Project name. + version_ids (Iterable[str]): Version ids. + + Returns: + tuple[list[dict[str, Any]], list[dict[str, Any]]]: Version and + representation contexts. + """ + + # TODO fix hero version + version_context_by_id = {} + repre_context_by_id = {} + if not project_name and not version_ids: + return version_context_by_id, repre_context_by_id + + version_docs = self._get_version_docs(project_name, version_ids) + version_docs_by_id = {} + version_docs_by_product_id = collections.defaultdict(list) + for version_doc in version_docs: + version_id = version_doc["_id"] + product_id = version_doc["parent"] + version_docs_by_id[version_id] = version_doc + version_docs_by_product_id[product_id].append(version_doc) + + _product_ids = set(version_docs_by_product_id.keys()) + _product_docs = get_subsets(project_name, subset_ids=_product_ids) + product_docs_by_id = {p["_id"]: p for p in _product_docs} + + _folder_ids = {p["parent"] for p in product_docs_by_id.values()} + _folder_docs = get_assets(project_name, asset_ids=_folder_ids) + folder_docs_by_id = {f["_id"]: f for f in _folder_docs} + + project_doc = get_project(project_name) + project_doc["code"] = project_doc["data"]["code"] + + for version_doc in version_docs: + product_id = version_doc["parent"] + product_doc = product_docs_by_id[product_id] + folder_id = product_doc["parent"] + folder_doc = folder_docs_by_id[folder_id] + version_context_by_id[product_id] = { + "project": project_doc, + "asset": folder_doc, + "subset": product_doc, + "version": version_doc, + } + + repre_docs = get_representations( + project_name, version_ids=version_ids) + for repre_doc in repre_docs: + version_id = repre_doc["parent"] + version_doc = version_docs_by_id[version_id] + product_id = version_doc["parent"] + product_doc = product_docs_by_id[product_id] + folder_id = product_doc["parent"] + folder_doc = folder_docs_by_id[folder_id] + + repre_context_by_id[repre_doc["_id"]] = { + "project": project_doc, + "asset": folder_doc, + "subset": product_doc, + "version": version_doc, + "representation": repre_doc, + } + + return version_context_by_id, repre_context_by_id + + def _contexts_for_representations(self, project_name, repre_ids): + """Get contexts for given representation ids. + + Prepare version contexts for 'SubsetLoaderPlugin' and representation + contexts for 'LoaderPlugin' for all children representations of + given versions. + + This method is very similar to '_contexts_for_versions' but the + queries of documents are called in a different order. + + Args: + project_name (str): Project name. + repre_ids (Iterable[str]): Representation ids. + + Returns: + tuple[list[dict[str, Any]], list[dict[str, Any]]]: Version and + representation contexts. + """ + + product_context_by_id = {} + repre_context_by_id = {} + if not project_name and not repre_ids: + return product_context_by_id, repre_context_by_id + + repre_docs = list(get_representations( + project_name, representation_ids=repre_ids + )) + version_ids = {r["parent"] for r in repre_docs} + version_docs = self._get_version_docs(project_name, version_ids) + version_docs_by_id = { + v["_id"]: v for v in version_docs + } + + product_ids = {v["parent"] for v in version_docs_by_id.values()} + product_docs = get_subsets(project_name, subset_ids=product_ids) + product_docs_by_id = { + p["_id"]: p for p in product_docs + } + + folder_ids = {p["parent"] for p in product_docs_by_id.values()} + folder_docs = get_assets(project_name, asset_ids=folder_ids) + folder_docs_by_id = { + f["_id"]: f for f in folder_docs + } + + project_doc = get_project(project_name) + project_doc["code"] = project_doc["data"]["code"] + + for product_id, product_doc in product_docs_by_id.items(): + folder_id = product_doc["parent"] + folder_doc = folder_docs_by_id[folder_id] + product_context_by_id[product_id] = { + "project": project_doc, + "asset": folder_doc, + "subset": product_doc, + } + + for repre_doc in repre_docs: + version_id = repre_doc["parent"] + version_doc = version_docs_by_id[version_id] + product_id = version_doc["parent"] + product_doc = product_docs_by_id[product_id] + folder_id = product_doc["parent"] + folder_doc = folder_docs_by_id[folder_id] + + repre_context_by_id[repre_doc["_id"]] = { + "project": project_doc, + "asset": folder_doc, + "subset": product_doc, + "version": version_doc, + "representation": repre_doc, + } + return product_context_by_id, repre_context_by_id + + def _get_action_items_for_contexts( + self, + project_name, + version_context_by_id, + repre_context_by_id + ): + """Prepare action items based on contexts. + + Actions are prepared based on discovered loader plugins and contexts. + The context must be valid for the loader plugin. + + Args: + project_name (str): Project name. + version_context_by_id (dict[str, dict[str, Any]]): Version + contexts by version id. + repre_context_by_id (dict[str, dict[str, Any]]): Representation + """ + + action_items = [] + if not version_context_by_id and not repre_context_by_id: + return action_items + + product_loaders, repre_loaders = self._get_loaders(project_name) + + repre_contexts_by_name = collections.defaultdict(list) + for repre_context in repre_context_by_id.values(): + repre_name = repre_context["representation"]["name"] + repre_contexts_by_name[repre_name].append(repre_context) + + for loader in repre_loaders: + # # do not allow download whole repre, select specific repre + # if tools_lib.is_sync_loader(loader): + # continue + + for repre_name, repre_contexts in repre_contexts_by_name.items(): + filtered_repre_contexts = filter_repre_contexts_by_loader( + repre_contexts, loader) + if not filtered_repre_contexts: + continue + + repre_ids = set() + repre_version_ids = set() + repre_product_ids = set() + repre_folder_ids = set() + for repre_context in filtered_repre_contexts: + repre_ids.add(repre_context["representation"]["_id"]) + repre_product_ids.add(repre_context["subset"]["_id"]) + repre_version_ids.add(repre_context["version"]["_id"]) + repre_folder_ids.add(repre_context["asset"]["_id"]) + + item = self._create_loader_action_item( + loader, + repre_contexts, + project_name=project_name, + folder_ids=repre_folder_ids, + product_ids=repre_product_ids, + version_ids=repre_version_ids, + representation_ids=repre_ids, + repre_name=repre_name, + ) + action_items.append(item) + + # Subset Loaders. + version_ids = set(version_context_by_id.keys()) + product_folder_ids = set() + product_ids = set() + for product_context in version_context_by_id.values(): + product_ids.add(product_context["subset"]["_id"]) + product_folder_ids.add(product_context["asset"]["_id"]) + + version_contexts = list(version_context_by_id.values()) + for loader in product_loaders: + item = self._create_loader_action_item( + loader, + version_contexts, + project_name=project_name, + folder_ids=product_folder_ids, + product_ids=product_ids, + version_ids=version_ids, + ) + action_items.append(item) + + action_items.sort(key=self._actions_sorter) + return action_items + + def _trigger_version_loader( + self, + loader, + options, + project_name, + version_ids, + ): + """Trigger version loader. + + This triggers 'load' method of 'SubsetLoaderPlugin' for given version + ids. + + Note: + Even when the plugin is 'SubsetLoaderPlugin' it actually expects + versions and should be named 'VersionLoaderPlugin'. Because it + is planned to refactor load system and introduce + 'LoaderAction' plugins it is not relevant to change it + anymore. + + Args: + loader (SubsetLoaderPlugin): Loader plugin to use. + options (dict): Option values for loader. + project_name (str): Project name. + version_ids (Iterable[str]): Version ids. + """ + + project_doc = get_project(project_name) + project_doc["code"] = project_doc["data"]["code"] + + version_docs = self._get_version_docs(project_name, version_ids) + product_ids = {v["parent"] for v in version_docs} + product_docs = get_subsets(project_name, subset_ids=product_ids) + product_docs_by_id = {f["_id"]: f for f in product_docs} + folder_ids = {p["parent"] for p in product_docs_by_id.values()} + folder_docs = get_assets(project_name, asset_ids=folder_ids) + folder_docs_by_id = {f["_id"]: f for f in folder_docs} + product_contexts = [] + for version_doc in version_docs: + product_id = version_doc["parent"] + product_doc = product_docs_by_id[product_id] + folder_id = product_doc["parent"] + folder_doc = folder_docs_by_id[folder_id] + product_contexts.append({ + "project": project_doc, + "asset": folder_doc, + "subset": product_doc, + "version": version_doc, + }) + + return self._load_products_by_loader( + loader, product_contexts, options + ) + + def _trigger_representation_loader( + self, + loader, + options, + project_name, + representation_ids, + ): + """Trigger representation loader. + + This triggers 'load' method of 'LoaderPlugin' for given representation + ids. For that are prepared contexts for each representation, with + all parent documents. + + Args: + loader (LoaderPlugin): Loader plugin to use. + options (dict): Option values for loader. + project_name (str): Project name. + representation_ids (Iterable[str]): Representation ids. + """ + + project_doc = get_project(project_name) + project_doc["code"] = project_doc["data"]["code"] + repre_docs = list(get_representations( + project_name, representation_ids=representation_ids + )) + version_ids = {r["parent"] for r in repre_docs} + version_docs = self._get_version_docs(project_name, version_ids) + version_docs_by_id = {v["_id"]: v for v in version_docs} + product_ids = {v["parent"] for v in version_docs_by_id.values()} + product_docs = get_subsets(project_name, subset_ids=product_ids) + product_docs_by_id = {p["_id"]: p for p in product_docs} + folder_ids = {p["parent"] for p in product_docs_by_id.values()} + folder_docs = get_assets(project_name, asset_ids=folder_ids) + folder_docs_by_id = {f["_id"]: f for f in folder_docs} + repre_contexts = [] + for repre_doc in repre_docs: + version_id = repre_doc["parent"] + version_doc = version_docs_by_id[version_id] + product_id = version_doc["parent"] + product_doc = product_docs_by_id[product_id] + folder_id = product_doc["parent"] + folder_doc = folder_docs_by_id[folder_id] + repre_contexts.append({ + "project": project_doc, + "asset": folder_doc, + "subset": product_doc, + "version": version_doc, + "representation": repre_doc, + }) + + return self._load_representations_by_loader( + loader, repre_contexts, options + ) + + def _load_representations_by_loader(self, loader, repre_contexts, options): + """Loops through list of repre_contexts and loads them with one loader + + Args: + loader (LoaderPlugin): Loader plugin to use. + repre_contexts (list[dict]): Full info about selected + representations, containing repre, version, subset, asset and + project documents. + options (dict): Data from options. + """ + + error_info = [] + for repre_context in repre_contexts: + version_doc = repre_context["version"] + if version_doc["type"] == "hero_version": + version_name = "Hero" + else: + version_name = version_doc.get("name") + try: + load_with_repre_context( + loader, + repre_context, + options=options + ) + + except IncompatibleLoaderError as exc: + print(exc) + error_info.append(( + "Incompatible Loader", + None, + repre_context["representation"]["name"], + repre_context["subset"]["name"], + version_name + )) + + except Exception as exc: + formatted_traceback = None + if not isinstance(exc, LoadError): + exc_type, exc_value, exc_traceback = sys.exc_info() + formatted_traceback = "".join(traceback.format_exception( + exc_type, exc_value, exc_traceback + )) + + error_info.append(( + str(exc), + formatted_traceback, + repre_context["representation"]["name"], + repre_context["subset"]["name"], + version_name + )) + return error_info + + def _load_products_by_loader(self, loader, version_contexts, options): + """Triggers load with SubsetLoader type of loaders. + + Warning: + Plugin is named 'SubsetLoader' but version is passed to context + too. + + Args: + loader (SubsetLoder): Loader used to load. + version_contexts (list[dict[str, Any]]): For context for each + version. + options (dict[str, Any]): Options for loader that user could fill. + """ + + error_info = [] + if loader.is_multiple_contexts_compatible: + subset_names = [] + for context in version_contexts: + subset_name = context.get("subset", {}).get("name") or "N/A" + subset_names.append(subset_name) + try: + load_with_subset_contexts( + loader, + version_contexts, + options=options + ) + + except Exception as exc: + formatted_traceback = None + if not isinstance(exc, LoadError): + exc_type, exc_value, exc_traceback = sys.exc_info() + formatted_traceback = "".join(traceback.format_exception( + exc_type, exc_value, exc_traceback + )) + error_info.append(( + str(exc), + formatted_traceback, + None, + ", ".join(subset_names), + None + )) + else: + for version_context in version_contexts: + subset_name = ( + version_context.get("subset", {}).get("name") or "N/A" + ) + try: + load_with_subset_context( + loader, + version_context, + options=options + ) + + except Exception as exc: + formatted_traceback = None + if not isinstance(exc, LoadError): + exc_type, exc_value, exc_traceback = sys.exc_info() + formatted_traceback = "".join( + traceback.format_exception( + exc_type, exc_value, exc_traceback + ) + ) + + error_info.append(( + str(exc), + formatted_traceback, + None, + subset_name, + None + )) + + return error_info diff --git a/openpype/tools/ayon_loader/models/products.py b/openpype/tools/ayon_loader/models/products.py new file mode 100644 index 0000000000..33023cc164 --- /dev/null +++ b/openpype/tools/ayon_loader/models/products.py @@ -0,0 +1,682 @@ +import collections +import contextlib + +import arrow +import ayon_api +from ayon_api.operations import OperationsSession + +from openpype.style import get_default_entity_icon_color +from openpype.tools.ayon_utils.models import NestedCacheItem +from openpype.tools.ayon_loader.abstract import ( + ProductTypeItem, + ProductItem, + VersionItem, + RepreItem, +) + +PRODUCTS_MODEL_SENDER = "products.model" + + +def version_item_from_entity(version): + version_attribs = version["attrib"] + frame_start = version_attribs.get("frameStart") + frame_end = version_attribs.get("frameEnd") + handle_start = version_attribs.get("handleStart") + handle_end = version_attribs.get("handleEnd") + step = version_attribs.get("step") + comment = version_attribs.get("comment") + source = version_attribs.get("source") + + frame_range = None + duration = None + handles = None + if frame_start is not None and frame_end is not None: + # Remove superfluous zeros from numbers (3.0 -> 3) to improve + # readability for most frame ranges + frame_start = int(frame_start) + frame_end = int(frame_end) + frame_range = "{}-{}".format(frame_start, frame_end) + duration = frame_end - frame_start + 1 + + if handle_start is not None and handle_end is not None: + handles = "{}-{}".format(int(handle_start), int(handle_end)) + + # NOTE There is also 'updatedAt', should be used that instead? + # TODO skip conversion - converting to '%Y%m%dT%H%M%SZ' is because + # 'PrettyTimeDelegate' expects it + created_at = arrow.get(version["createdAt"]) + published_time = created_at.strftime("%Y%m%dT%H%M%SZ") + author = version["author"] + version_num = version["version"] + is_hero = version_num < 0 + + return VersionItem( + version_id=version["id"], + version=version_num, + is_hero=is_hero, + product_id=version["productId"], + thumbnail_id=version["thumbnailId"], + published_time=published_time, + author=author, + frame_range=frame_range, + duration=duration, + handles=handles, + step=step, + comment=comment, + source=source, + ) + + +def product_item_from_entity( + product_entity, + version_entities, + product_type_items_by_name, + folder_label, + product_in_scene, +): + product_attribs = product_entity["attrib"] + group = product_attribs.get("productGroup") + product_type = product_entity["productType"] + product_type_item = product_type_items_by_name[product_type] + product_type_icon = product_type_item.icon + + product_icon = { + "type": "awesome-font", + "name": "fa.file-o", + "color": get_default_entity_icon_color(), + } + version_items = { + version_entity["id"]: version_item_from_entity(version_entity) + for version_entity in version_entities + } + + return ProductItem( + product_id=product_entity["id"], + product_type=product_type, + product_name=product_entity["name"], + product_icon=product_icon, + product_type_icon=product_type_icon, + product_in_scene=product_in_scene, + group_name=group, + folder_id=product_entity["folderId"], + folder_label=folder_label, + version_items=version_items, + ) + + +def product_type_item_from_data(product_type_data): + # TODO implement icon implementation + # icon = product_type_data["icon"] + # color = product_type_data["color"] + icon = { + "type": "awesome-font", + "name": "fa.folder", + "color": "#0091B2", + } + # TODO implement checked logic + return ProductTypeItem(product_type_data["name"], icon, True) + + +class ProductsModel: + """Model for products, version and representation. + + All of the entities are product based. This model prepares data for UI + and caches it for faster access. + + Note: + Data are not used for actions model because that would require to + break OpenPype compatibility of 'LoaderPlugin's. + """ + + lifetime = 60 # In seconds (minute by default) + + def __init__(self, controller): + self._controller = controller + + # Mapping helpers + # NOTE - mapping must be cleaned up with cache cleanup + self._product_item_by_id = collections.defaultdict(dict) + self._version_item_by_id = collections.defaultdict(dict) + self._product_folder_ids_mapping = collections.defaultdict(dict) + + # Cache helpers + self._product_type_items_cache = NestedCacheItem( + levels=1, default_factory=list, lifetime=self.lifetime) + self._product_items_cache = NestedCacheItem( + levels=2, default_factory=dict, lifetime=self.lifetime) + self._repre_items_cache = NestedCacheItem( + levels=2, default_factory=dict, lifetime=self.lifetime) + + def reset(self): + """Reset model with all cached data.""" + + self._product_item_by_id.clear() + self._version_item_by_id.clear() + self._product_folder_ids_mapping.clear() + + self._product_type_items_cache.reset() + self._product_items_cache.reset() + self._repre_items_cache.reset() + + def get_product_type_items(self, project_name): + """Product type items for project. + + Args: + project_name (str): Project name. + + Returns: + list[ProductTypeItem]: Product type items. + """ + + cache = self._product_type_items_cache[project_name] + if not cache.is_valid: + product_types = ayon_api.get_project_product_types(project_name) + cache.update_data([ + product_type_item_from_data(product_type) + for product_type in product_types + ]) + return cache.get_data() + + def get_product_items(self, project_name, folder_ids, sender): + """Product items with versions for project and folder ids. + + Product items also contain version items. They're directly connected + to product items in the UI and the separation is not needed. + + Args: + project_name (Union[str, None]): Project name. + folder_ids (Iterable[str]): Folder ids. + sender (Union[str, None]): Who triggered the method. + + Returns: + list[ProductItem]: Product items. + """ + + if not project_name or not folder_ids: + return [] + + project_cache = self._product_items_cache[project_name] + output = [] + folder_ids_to_update = set() + for folder_id in folder_ids: + cache = project_cache[folder_id] + if cache.is_valid: + output.extend(cache.get_data().values()) + else: + folder_ids_to_update.add(folder_id) + + self._refresh_product_items( + project_name, folder_ids_to_update, sender) + + for folder_id in folder_ids_to_update: + cache = project_cache[folder_id] + output.extend(cache.get_data().values()) + return output + + def get_product_item(self, project_name, product_id): + """Get product item based on passed product id. + + This method is using cached items, but if cache is not valid it also + can query the item. + + Args: + project_name (Union[str, None]): Where to look for product. + product_id (Union[str, None]): Product id to receive. + + Returns: + Union[ProductItem, None]: Product item or 'None' if not found. + """ + + if not any((project_name, product_id)): + return None + + product_items_by_id = self._product_item_by_id[project_name] + product_item = product_items_by_id.get(product_id) + if product_item is not None: + return product_item + for product_item in self._query_product_items_by_ids( + project_name, product_ids=[product_id] + ).values(): + return product_item + + def get_product_ids_by_repre_ids(self, project_name, repre_ids): + """Get product ids based on passed representation ids. + + Args: + project_name (str): Where to look for representations. + repre_ids (Iterable[str]): Representation ids. + + Returns: + set[str]: Product ids for passed representation ids. + """ + + # TODO look out how to use single server call + if not repre_ids: + return set() + repres = ayon_api.get_representations( + project_name, repre_ids, fields=["versionId"] + ) + version_ids = {repre["versionId"] for repre in repres} + if not version_ids: + return set() + versions = ayon_api.get_versions( + project_name, version_ids=version_ids, fields=["productId"] + ) + return {v["productId"] for v in versions} + + def get_repre_items(self, project_name, version_ids, sender): + """Get representation items for passed version ids. + + Args: + project_name (str): Project name. + version_ids (Iterable[str]): Version ids. + sender (Union[str, None]): Who triggered the method. + + Returns: + list[RepreItem]: Representation items. + """ + + output = [] + if not any((project_name, version_ids)): + return output + + invalid_version_ids = set() + project_cache = self._repre_items_cache[project_name] + for version_id in version_ids: + version_cache = project_cache[version_id] + if version_cache.is_valid: + output.extend(version_cache.get_data().values()) + else: + invalid_version_ids.add(version_id) + + if invalid_version_ids: + self.refresh_representation_items( + project_name, invalid_version_ids, sender + ) + + for version_id in invalid_version_ids: + version_cache = project_cache[version_id] + output.extend(version_cache.get_data().values()) + + return output + + def change_products_group(self, project_name, product_ids, group_name): + """Change group name for passed product ids. + + Group name is stored in 'attrib' of product entity and is used in UI + to group items. + + Method triggers "products.group.changed" event with data: + { + "project_name": project_name, + "folder_ids": folder_ids, + "product_ids": product_ids, + "group_name": group_name + } + + Args: + project_name (str): Project name. + product_ids (Iterable[str]): Product ids to change group name for. + group_name (str): Group name to set. + """ + + if not product_ids: + return + + product_items = self._get_product_items_by_id( + project_name, product_ids + ) + if not product_items: + return + + session = OperationsSession() + folder_ids = set() + for product_item in product_items.values(): + session.update_entity( + project_name, + "product", + product_item.product_id, + {"attrib": {"productGroup": group_name}} + ) + folder_ids.add(product_item.folder_id) + product_item.group_name = group_name + + session.commit() + self._controller.emit_event( + "products.group.changed", + { + "project_name": project_name, + "folder_ids": folder_ids, + "product_ids": product_ids, + "group_name": group_name, + }, + PRODUCTS_MODEL_SENDER + ) + + def _get_product_items_by_id(self, project_name, product_ids): + product_item_by_id = self._product_item_by_id[project_name] + missing_product_ids = set() + output = {} + for product_id in product_ids: + product_item = product_item_by_id.get(product_id) + if product_item is not None: + output[product_id] = product_item + else: + missing_product_ids.add(product_id) + + output.update( + self._query_product_items_by_ids( + project_name, missing_product_ids + ) + ) + return output + + def _get_version_items_by_id(self, project_name, version_ids): + version_item_by_id = self._version_item_by_id[project_name] + missing_version_ids = set() + output = {} + for version_id in version_ids: + version_item = version_item_by_id.get(version_id) + if version_item is not None: + output[version_id] = version_item + else: + missing_version_ids.add(version_id) + + output.update( + self._query_version_items_by_ids( + project_name, missing_version_ids + ) + ) + return output + + def _create_product_items( + self, + project_name, + products, + versions, + folder_items=None, + product_type_items=None, + ): + if folder_items is None: + folder_items = self._controller.get_folder_items(project_name) + + if product_type_items is None: + product_type_items = self.get_product_type_items(project_name) + + loaded_product_ids = self._controller.get_loaded_product_ids() + + versions_by_product_id = collections.defaultdict(list) + for version in versions: + versions_by_product_id[version["productId"]].append(version) + product_type_items_by_name = { + product_type_item.name: product_type_item + for product_type_item in product_type_items + } + output = {} + for product in products: + product_id = product["id"] + folder_id = product["folderId"] + folder_item = folder_items.get(folder_id) + if not folder_item: + continue + versions = versions_by_product_id[product_id] + if not versions: + continue + product_item = product_item_from_entity( + product, + versions, + product_type_items_by_name, + folder_item.label, + product_id in loaded_product_ids, + ) + output[product_id] = product_item + return output + + def _query_product_items_by_ids( + self, + project_name, + folder_ids=None, + product_ids=None, + folder_items=None + ): + """Query product items. + + This method does get from, or store to, cache attributes. + + One of 'product_ids' or 'folder_ids' must be passed to the method. + + Args: + project_name (str): Project name. + folder_ids (Optional[Iterable[str]]): Folder ids under which are + products. + product_ids (Optional[Iterable[str]]): Product ids to use. + folder_items (Optional[Dict[str, FolderItem]]): Prepared folder + items from controller. + + Returns: + dict[str, ProductItem]: Product items by product id. + """ + + if not folder_ids and not product_ids: + return {} + + kwargs = {} + if folder_ids is not None: + kwargs["folder_ids"] = folder_ids + + if product_ids is not None: + kwargs["product_ids"] = product_ids + + products = list(ayon_api.get_products(project_name, **kwargs)) + product_ids = {product["id"] for product in products} + + versions = ayon_api.get_versions( + project_name, product_ids=product_ids + ) + + return self._create_product_items( + project_name, products, versions, folder_items=folder_items + ) + + def _query_version_items_by_ids(self, project_name, version_ids): + versions = list(ayon_api.get_versions( + project_name, version_ids=version_ids + )) + product_ids = {version["productId"] for version in versions} + products = list(ayon_api.get_products( + project_name, product_ids=product_ids + )) + product_items = self._create_product_items( + project_name, products, versions + ) + version_items = {} + for product_item in product_items.values(): + version_items.update(product_item.version_items) + return version_items + + def _clear_product_version_items(self, project_name, folder_ids): + """Clear product and version items from memory. + + When products are re-queried for a folders, the old product and version + items in '_product_item_by_id' and '_version_item_by_id' should + be cleaned up from memory. And mapping in stored in + '_product_folder_ids_mapping' is not relevant either. + + Args: + project_name (str): Name of project. + folder_ids (Iterable[str]): Folder ids which are being refreshed. + """ + + project_mapping = self._product_folder_ids_mapping[project_name] + if not project_mapping: + return + + product_item_by_id = self._product_item_by_id[project_name] + version_item_by_id = self._version_item_by_id[project_name] + for folder_id in folder_ids: + product_ids = project_mapping.pop(folder_id, None) + if not product_ids: + continue + + for product_id in product_ids: + product_item = product_item_by_id.pop(product_id, None) + if product_item is None: + continue + for version_item in product_item.version_items.values(): + version_item_by_id.pop(version_item.version_id, None) + + def _refresh_product_items(self, project_name, folder_ids, sender): + """Refresh product items and store them in cache. + + Args: + project_name (str): Name of project. + folder_ids (Iterable[str]): Folder ids which are being refreshed. + sender (Union[str, None]): Who triggered the refresh. + """ + + if not project_name or not folder_ids: + return + + self._clear_product_version_items(project_name, folder_ids) + + project_mapping = self._product_folder_ids_mapping[project_name] + product_item_by_id = self._product_item_by_id[project_name] + version_item_by_id = self._version_item_by_id[project_name] + + for folder_id in folder_ids: + project_mapping[folder_id] = set() + + with self._product_refresh_event_manager( + project_name, folder_ids, sender + ): + folder_items = self._controller.get_folder_items(project_name) + items_by_folder_id = { + folder_id: {} + for folder_id in folder_ids + } + product_items_by_id = self._query_product_items_by_ids( + project_name, + folder_ids=folder_ids, + folder_items=folder_items + ) + for product_id, product_item in product_items_by_id.items(): + folder_id = product_item.folder_id + items_by_folder_id[product_item.folder_id][product_id] = ( + product_item + ) + + project_mapping[folder_id].add(product_id) + product_item_by_id[product_id] = product_item + for version_id, version_item in ( + product_item.version_items.items() + ): + version_item_by_id[version_id] = version_item + + project_cache = self._product_items_cache[project_name] + for folder_id, product_items in items_by_folder_id.items(): + project_cache[folder_id].update_data(product_items) + + @contextlib.contextmanager + def _product_refresh_event_manager( + self, project_name, folder_ids, sender + ): + self._controller.emit_event( + "products.refresh.started", + { + "project_name": project_name, + "folder_ids": folder_ids, + "sender": sender, + }, + PRODUCTS_MODEL_SENDER + ) + try: + yield + + finally: + self._controller.emit_event( + "products.refresh.finished", + { + "project_name": project_name, + "folder_ids": folder_ids, + "sender": sender, + }, + PRODUCTS_MODEL_SENDER + ) + + def refresh_representation_items( + self, project_name, version_ids, sender + ): + if not any((project_name, version_ids)): + return + self._controller.emit_event( + "model.representations.refresh.started", + { + "project_name": project_name, + "version_ids": version_ids, + "sender": sender, + }, + PRODUCTS_MODEL_SENDER + ) + failed = False + try: + self._refresh_representation_items(project_name, version_ids) + except Exception: + # TODO add more information about failed refresh + failed = True + + self._controller.emit_event( + "model.representations.refresh.finished", + { + "project_name": project_name, + "version_ids": version_ids, + "sender": sender, + "failed": failed, + }, + PRODUCTS_MODEL_SENDER + ) + + def _refresh_representation_items(self, project_name, version_ids): + representations = list(ayon_api.get_representations( + project_name, + version_ids=version_ids, + fields=["id", "name", "versionId"] + )) + + version_items_by_id = self._get_version_items_by_id( + project_name, version_ids + ) + product_ids = { + version_item.product_id + for version_item in version_items_by_id.values() + } + product_items_by_id = self._get_product_items_by_id( + project_name, product_ids + ) + repre_icon = { + "type": "awesome-font", + "name": "fa.file-o", + "color": get_default_entity_icon_color(), + } + repre_items_by_version_id = collections.defaultdict(dict) + for representation in representations: + version_id = representation["versionId"] + version_item = version_items_by_id.get(version_id) + if version_item is None: + continue + product_item = product_items_by_id.get(version_item.product_id) + if product_item is None: + continue + repre_id = representation["id"] + repre_item = RepreItem( + repre_id, + representation["name"], + repre_icon, + product_item.product_name, + product_item.folder_label, + ) + repre_items_by_version_id[version_id][repre_id] = repre_item + + project_cache = self._repre_items_cache[project_name] + for version_id, repre_items in repre_items_by_version_id.items(): + version_cache = project_cache[version_id] + version_cache.update_data(repre_items) diff --git a/openpype/tools/ayon_loader/models/selection.py b/openpype/tools/ayon_loader/models/selection.py new file mode 100644 index 0000000000..326ff835f6 --- /dev/null +++ b/openpype/tools/ayon_loader/models/selection.py @@ -0,0 +1,85 @@ +class SelectionModel(object): + """Model handling selection changes. + + Triggering events: + - "selection.project.changed" + - "selection.folders.changed" + - "selection.versions.changed" + """ + + event_source = "selection.model" + + def __init__(self, controller): + self._controller = controller + + self._project_name = None + self._folder_ids = set() + self._version_ids = set() + self._representation_ids = set() + + def get_selected_project_name(self): + return self._project_name + + def set_selected_project(self, project_name): + if self._project_name == project_name: + return + + self._project_name = project_name + self._controller.emit_event( + "selection.project.changed", + {"project_name": self._project_name}, + self.event_source + ) + + def get_selected_folder_ids(self): + return self._folder_ids + + def set_selected_folders(self, folder_ids): + if folder_ids == self._folder_ids: + return + + self._folder_ids = folder_ids + self._controller.emit_event( + "selection.folders.changed", + { + "project_name": self._project_name, + "folder_ids": folder_ids, + }, + self.event_source + ) + + def get_selected_version_ids(self): + return self._version_ids + + def set_selected_versions(self, version_ids): + if version_ids == self._version_ids: + return + + self._version_ids = version_ids + self._controller.emit_event( + "selection.versions.changed", + { + "project_name": self._project_name, + "folder_ids": self._folder_ids, + "version_ids": self._version_ids, + }, + self.event_source + ) + + def get_selected_representation_ids(self): + return self._representation_ids + + def set_selected_representations(self, repre_ids): + if repre_ids == self._representation_ids: + return + + self._representation_ids = repre_ids + self._controller.emit_event( + "selection.representations.changed", + { + "project_name": self._project_name, + "folder_ids": self._folder_ids, + "version_ids": self._version_ids, + "representation_ids": self._representation_ids, + } + ) diff --git a/openpype/tools/ayon_loader/ui/__init__.py b/openpype/tools/ayon_loader/ui/__init__.py new file mode 100644 index 0000000000..41e4418641 --- /dev/null +++ b/openpype/tools/ayon_loader/ui/__init__.py @@ -0,0 +1,6 @@ +from .window import LoaderWindow + + +__all__ = ( + "LoaderWindow", +) diff --git a/openpype/tools/ayon_loader/ui/actions_utils.py b/openpype/tools/ayon_loader/ui/actions_utils.py new file mode 100644 index 0000000000..a269b643dc --- /dev/null +++ b/openpype/tools/ayon_loader/ui/actions_utils.py @@ -0,0 +1,118 @@ +import uuid + +from qtpy import QtWidgets, QtGui +import qtawesome + +from openpype.lib.attribute_definitions import AbstractAttrDef +from openpype.tools.attribute_defs import AttributeDefinitionsDialog +from openpype.tools.utils.widgets import ( + OptionalMenu, + OptionalAction, + OptionDialog, +) +from openpype.tools.ayon_utils.widgets import get_qt_icon + + +def show_actions_menu(action_items, global_point, one_item_selected, parent): + selected_action_item = None + selected_options = None + + if not action_items: + menu = QtWidgets.QMenu(parent) + action = _get_no_loader_action(menu, one_item_selected) + menu.addAction(action) + menu.exec_(global_point) + return selected_action_item, selected_options + + menu = OptionalMenu(parent) + + action_items_by_id = {} + for action_item in action_items: + item_id = uuid.uuid4().hex + action_items_by_id[item_id] = action_item + item_options = action_item.options + icon = get_qt_icon(action_item.icon) + use_option = bool(item_options) + action = OptionalAction( + action_item.label, + icon, + use_option, + menu + ) + if use_option: + # Add option box tip + action.set_option_tip(item_options) + + tip = action_item.tooltip + if tip: + action.setToolTip(tip) + action.setStatusTip(tip) + + action.setData(item_id) + + menu.addAction(action) + + action = menu.exec_(global_point) + if action is not None: + item_id = action.data() + selected_action_item = action_items_by_id.get(item_id) + + if selected_action_item is not None: + selected_options = _get_options(action, selected_action_item, parent) + + return selected_action_item, selected_options + + +def _get_options(action, action_item, parent): + """Provides dialog to select value from loader provided options. + + Loader can provide static or dynamically created options based on + AttributeDefinitions, and for backwards compatibility qargparse. + + Args: + action (OptionalAction) - Action object in menu. + action_item (ActionItem) - Action item with context information. + parent (QtCore.QObject) - Parent object for dialog. + + Returns: + Union[dict[str, Any], None]: Selected value from attributes or + 'None' if dialog was cancelled. + """ + + # Pop option dialog + options = action_item.options + if not getattr(action, "optioned", False) or not options: + return {} + + if isinstance(options[0], AbstractAttrDef): + qargparse_options = False + dialog = AttributeDefinitionsDialog(options, parent) + else: + qargparse_options = True + dialog = OptionDialog(parent) + dialog.create(options) + + dialog.setWindowTitle(action.label + " Options") + + if not dialog.exec_(): + return None + + # Get option + if qargparse_options: + return dialog.parse() + return dialog.get_values() + + +def _get_no_loader_action(menu, one_item_selected): + """Creates dummy no loader option in 'menu'""" + + if one_item_selected: + submsg = "this version." + else: + submsg = "your selection." + msg = "No compatible loaders for {}".format(submsg) + icon = qtawesome.icon( + "fa.exclamation", + color=QtGui.QColor(255, 51, 0) + ) + return QtWidgets.QAction(icon, ("*" + msg), menu) diff --git a/openpype/tools/ayon_loader/ui/folders_widget.py b/openpype/tools/ayon_loader/ui/folders_widget.py new file mode 100644 index 0000000000..b911458546 --- /dev/null +++ b/openpype/tools/ayon_loader/ui/folders_widget.py @@ -0,0 +1,416 @@ +import qtpy +from qtpy import QtWidgets, QtCore, QtGui + +from openpype.tools.utils import ( + RecursiveSortFilterProxyModel, + DeselectableTreeView, +) +from openpype.style import get_objected_colors + +from openpype.tools.ayon_utils.widgets import ( + FoldersModel, + FOLDERS_MODEL_SENDER_NAME, +) +from openpype.tools.ayon_utils.widgets.folders_widget import ITEM_ID_ROLE + +if qtpy.API == "pyside": + from PySide.QtGui import QStyleOptionViewItemV4 +elif qtpy.API == "pyqt4": + from PyQt4.QtGui import QStyleOptionViewItemV4 + +UNDERLINE_COLORS_ROLE = QtCore.Qt.UserRole + 4 + + +class UnderlinesFolderDelegate(QtWidgets.QItemDelegate): + """Item delegate drawing bars under folder label. + + This is used in loader tool. Multiselection of folders + may group products by name under colored groups. Selected color groups are + then propagated back to selected folders as underlines. + """ + bar_height = 3 + + def __init__(self, *args, **kwargs): + super(UnderlinesFolderDelegate, self).__init__(*args, **kwargs) + colors = get_objected_colors("loader", "asset-view") + self._selected_color = colors["selected"].get_qcolor() + self._hover_color = colors["hover"].get_qcolor() + self._selected_hover_color = colors["selected-hover"].get_qcolor() + + def sizeHint(self, option, index): + """Add bar height to size hint.""" + result = super(UnderlinesFolderDelegate, self).sizeHint(option, index) + height = result.height() + result.setHeight(height + self.bar_height) + + return result + + def paint(self, painter, option, index): + """Replicate painting of an item and draw color bars if needed.""" + # Qt4 compat + if qtpy.API in ("pyside", "pyqt4"): + option = QStyleOptionViewItemV4(option) + + painter.save() + + item_rect = QtCore.QRect(option.rect) + item_rect.setHeight(option.rect.height() - self.bar_height) + + subset_colors = index.data(UNDERLINE_COLORS_ROLE) or [] + + subset_colors_width = 0 + if subset_colors: + subset_colors_width = option.rect.width() / len(subset_colors) + + subset_rects = [] + counter = 0 + for subset_c in subset_colors: + new_color = None + new_rect = None + if subset_c: + new_color = QtGui.QColor(subset_c) + + new_rect = QtCore.QRect( + option.rect.left() + (counter * subset_colors_width), + option.rect.top() + ( + option.rect.height() - self.bar_height + ), + subset_colors_width, + self.bar_height + ) + subset_rects.append((new_color, new_rect)) + counter += 1 + + # Background + if option.state & QtWidgets.QStyle.State_Selected: + if len(subset_colors) == 0: + item_rect.setTop(item_rect.top() + (self.bar_height / 2)) + + if option.state & QtWidgets.QStyle.State_MouseOver: + bg_color = self._selected_hover_color + else: + bg_color = self._selected_color + else: + item_rect.setTop(item_rect.top() + (self.bar_height / 2)) + if option.state & QtWidgets.QStyle.State_MouseOver: + bg_color = self._hover_color + else: + bg_color = QtGui.QColor() + bg_color.setAlpha(0) + + # When not needed to do a rounded corners (easier and without + # painter restore): + painter.fillRect( + option.rect, + QtGui.QBrush(bg_color) + ) + + if option.state & QtWidgets.QStyle.State_Selected: + for color, subset_rect in subset_rects: + if not color or not subset_rect: + continue + painter.fillRect(subset_rect, QtGui.QBrush(color)) + + # Icon + icon_index = index.model().index( + index.row(), index.column(), index.parent() + ) + # - Default icon_rect if not icon + icon_rect = QtCore.QRect( + item_rect.left(), + item_rect.top(), + # To make sure it's same size all the time + option.rect.height() - self.bar_height, + option.rect.height() - self.bar_height + ) + icon = index.model().data(icon_index, QtCore.Qt.DecorationRole) + + if icon: + mode = QtGui.QIcon.Normal + if not (option.state & QtWidgets.QStyle.State_Enabled): + mode = QtGui.QIcon.Disabled + elif option.state & QtWidgets.QStyle.State_Selected: + mode = QtGui.QIcon.Selected + + if isinstance(icon, QtGui.QPixmap): + icon = QtGui.QIcon(icon) + option.decorationSize = icon.size() / icon.devicePixelRatio() + + elif isinstance(icon, QtGui.QColor): + pixmap = QtGui.QPixmap(option.decorationSize) + pixmap.fill(icon) + icon = QtGui.QIcon(pixmap) + + elif isinstance(icon, QtGui.QImage): + icon = QtGui.QIcon(QtGui.QPixmap.fromImage(icon)) + option.decorationSize = icon.size() / icon.devicePixelRatio() + + elif isinstance(icon, QtGui.QIcon): + state = QtGui.QIcon.Off + if option.state & QtWidgets.QStyle.State_Open: + state = QtGui.QIcon.On + actual_size = option.icon.actualSize( + option.decorationSize, mode, state + ) + option.decorationSize = QtCore.QSize( + min(option.decorationSize.width(), actual_size.width()), + min(option.decorationSize.height(), actual_size.height()) + ) + + state = QtGui.QIcon.Off + if option.state & QtWidgets.QStyle.State_Open: + state = QtGui.QIcon.On + + icon.paint( + painter, icon_rect, + QtCore.Qt.AlignLeft, mode, state + ) + + # Text + text_rect = QtCore.QRect( + icon_rect.left() + icon_rect.width() + 2, + item_rect.top(), + item_rect.width(), + item_rect.height() + ) + + painter.drawText( + text_rect, QtCore.Qt.AlignVCenter, + index.data(QtCore.Qt.DisplayRole) + ) + + painter.restore() + + +class LoaderFoldersModel(FoldersModel): + def __init__(self, *args, **kwargs): + super(LoaderFoldersModel, self).__init__(*args, **kwargs) + + self._colored_items = set() + + def _fill_item_data(self, item, folder_item): + """ + + Args: + item (QtGui.QStandardItem): Item to fill data. + folder_item (FolderItem): Folder item. + """ + + super(LoaderFoldersModel, self)._fill_item_data(item, folder_item) + + def set_merged_products_selection(self, items): + changes = { + folder_id: None + for folder_id in self._colored_items + } + + all_folder_ids = set() + for item in items: + folder_ids = item["folder_ids"] + all_folder_ids.update(folder_ids) + + for folder_id in all_folder_ids: + changes[folder_id] = [] + + for item in items: + item_color = item["color"] + item_folder_ids = item["folder_ids"] + for folder_id in all_folder_ids: + folder_color = ( + item_color + if folder_id in item_folder_ids + else None + ) + changes[folder_id].append(folder_color) + + for folder_id, color_value in changes.items(): + item = self._items_by_id.get(folder_id) + if item is not None: + item.setData(color_value, UNDERLINE_COLORS_ROLE) + + self._colored_items = all_folder_ids + + +class LoaderFoldersWidget(QtWidgets.QWidget): + """Folders widget. + + Widget that handles folders view, model and selection. + + Expected selection handling is disabled by default. If enabled, the + widget will handle the expected in predefined way. Widget is listening + to event 'expected_selection_changed' with expected event data below, + the same data must be available when called method + 'get_expected_selection_data' on controller. + + { + "folder": { + "current": bool, # Folder is what should be set now + "folder_id": Union[str, None], # Folder id that should be selected + }, + ... + } + + Selection is confirmed by calling method 'expected_folder_selected' on + controller. + + + Args: + controller (AbstractWorkfilesFrontend): The control object. + parent (QtWidgets.QWidget): The parent widget. + handle_expected_selection (bool): If True, the widget will handle + the expected selection. Defaults to False. + """ + + refreshed = QtCore.Signal() + + def __init__(self, controller, parent, handle_expected_selection=False): + super(LoaderFoldersWidget, self).__init__(parent) + + folders_view = DeselectableTreeView(self) + folders_view.setHeaderHidden(True) + folders_view.setSelectionMode( + QtWidgets.QAbstractItemView.ExtendedSelection) + + folders_model = LoaderFoldersModel(controller) + folders_proxy_model = RecursiveSortFilterProxyModel() + folders_proxy_model.setSourceModel(folders_model) + folders_proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive) + + folders_label_delegate = UnderlinesFolderDelegate(folders_view) + + folders_view.setModel(folders_proxy_model) + folders_view.setItemDelegate(folders_label_delegate) + + main_layout = QtWidgets.QHBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.addWidget(folders_view, 1) + + controller.register_event_callback( + "selection.project.changed", + self._on_project_selection_change, + ) + 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._folders_label_delegate = folders_label_delegate + + self._handle_expected_selection = handle_expected_selection + self._expected_selection = None + + def set_name_filer(self, name): + """Set filter of folder name. + + Args: + name (str): The string filter. + """ + + self._folders_proxy_model.setFilterFixedString(name) + + def set_merged_products_selection(self, items): + """ + + Args: + items (list[dict[str, Any]]): List of merged items with folder + ids. + """ + + self._folders_model.set_merged_products_selection(items) + + def refresh(self): + self._folders_model.refresh() + + def _on_project_selection_change(self, event): + project_name = event["project_name"] + self._set_project_name(project_name) + + def _set_project_name(self, project_name): + self._folders_model.set_project_name(project_name) + + def _clear(self): + self._folders_model.clear() + + def _on_folders_refresh_finished(self, event): + if event["sender"] != FOLDERS_MODEL_SENDER_NAME: + self._set_project_name(event["project_name"]) + + def _on_controller_refresh(self): + self._update_expected_selection() + + def _on_model_refresh(self): + if self._expected_selection: + self._set_expected_selection() + self._folders_proxy_model.sort(0) + self.refreshed.emit() + + def _get_selected_item_ids(self): + selection_model = self._folders_view.selectionModel() + item_ids = [] + for index in selection_model.selectedIndexes(): + item_id = index.data(ITEM_ID_ROLE) + if item_id is not None: + item_ids.append(item_id) + return item_ids + + def _on_selection_change(self): + item_ids = self._get_selected_item_ids() + self._controller.set_selected_folders(item_ids) + + # Expected selection handling + def _on_expected_selection_change(self, event): + self._update_expected_selection(event.data) + + def _update_expected_selection(self, expected_data=None): + if not self._handle_expected_selection: + return + + if expected_data is None: + expected_data = self._controller.get_expected_selection_data() + + folder_data = expected_data.get("folder") + if not folder_data or not folder_data["current"]: + return + + folder_id = folder_data["id"] + self._expected_selection = folder_id + if not self._folders_model.is_refreshing: + self._set_expected_selection() + + def _set_expected_selection(self): + if not self._handle_expected_selection: + return + + folder_id = self._expected_selection + selected_ids = self._get_selected_item_ids() + self._expected_selection = None + skip_selection = ( + folder_id is None + or ( + folder_id in selected_ids + and len(selected_ids) == 1 + ) + ) + if not skip_selection: + 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) diff --git a/openpype/tools/ayon_loader/ui/info_widget.py b/openpype/tools/ayon_loader/ui/info_widget.py new file mode 100644 index 0000000000..b7d1b0811f --- /dev/null +++ b/openpype/tools/ayon_loader/ui/info_widget.py @@ -0,0 +1,141 @@ +import datetime + +from qtpy import QtWidgets + +from openpype.tools.utils.lib import format_version + + +class VersionTextEdit(QtWidgets.QTextEdit): + """QTextEdit that displays version specific information. + + This also overrides the context menu to add actions like copying + source path to clipboard or copying the raw data of the version + to clipboard. + + """ + def __init__(self, controller, parent): + super(VersionTextEdit, self).__init__(parent=parent) + + self._version_item = None + self._product_item = None + + self._controller = controller + + # Reset + self.set_current_item() + + def set_current_item(self, product_item=None, version_item=None): + """ + + Args: + product_item (Union[ProductItem, None]): Product item. + version_item (Union[VersionItem, None]): Version item to display. + """ + + self._product_item = product_item + self._version_item = version_item + + if version_item is None: + # Reset state to empty + self.setText("") + return + + version_label = format_version(abs(version_item.version)) + if version_item.version < 0: + version_label = "Hero version {}".format(version_label) + + # Define readable creation timestamp + created = version_item.published_time + created = datetime.datetime.strptime(created, "%Y%m%dT%H%M%SZ") + created = datetime.datetime.strftime(created, "%b %d %Y %H:%M") + + comment = version_item.comment or "No comment" + source = version_item.source or "No source" + + self.setHtml( + ( + "

{product_name}

" + "

{version_label}

" + "Comment
" + "{comment}

" + + "Created
" + "{created}

" + + "Source
" + "{source}" + ).format( + product_name=product_item.product_name, + version_label=version_label, + comment=comment, + created=created, + source=source, + ) + ) + + def contextMenuEvent(self, event): + """Context menu with additional actions""" + menu = self.createStandardContextMenu() + + # Add additional actions when any text, so we can assume + # the version is set. + source = None + if self._version_item is not None: + source = self._version_item.source + + if source: + menu.addSeparator() + action = QtWidgets.QAction( + "Copy source path to clipboard", menu + ) + action.triggered.connect(self._on_copy_source) + menu.addAction(action) + + menu.exec_(event.globalPos()) + + def _on_copy_source(self): + """Copy formatted source path to clipboard.""" + + source = self._version_item.source + if not source: + return + + filled_source = self._controller.fill_root_in_source(source) + clipboard = QtWidgets.QApplication.clipboard() + clipboard.setText(filled_source) + + +class InfoWidget(QtWidgets.QWidget): + """A Widget that display information about a specific version""" + def __init__(self, controller, parent): + super(InfoWidget, self).__init__(parent=parent) + + label_widget = QtWidgets.QLabel("Version Info", self) + info_text_widget = VersionTextEdit(controller, self) + info_text_widget.setReadOnly(True) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(label_widget, 0) + layout.addWidget(info_text_widget, 1) + + self._controller = controller + + self._info_text_widget = info_text_widget + self._label_widget = label_widget + + def set_selected_version_info(self, project_name, items): + if not items or not project_name: + self._info_text_widget.set_current_item() + return + first_item = next(iter(items)) + product_item = self._controller.get_product_item( + project_name, + first_item["product_id"], + ) + version_id = first_item["version_id"] + version_item = None + if product_item is not None: + version_item = product_item.version_items.get(version_id) + + self._info_text_widget.set_current_item(product_item, version_item) diff --git a/openpype/tools/ayon_loader/ui/product_group_dialog.py b/openpype/tools/ayon_loader/ui/product_group_dialog.py new file mode 100644 index 0000000000..5737ce58a4 --- /dev/null +++ b/openpype/tools/ayon_loader/ui/product_group_dialog.py @@ -0,0 +1,45 @@ +from qtpy import QtWidgets + +from openpype.tools.utils import PlaceholderLineEdit + + +class ProductGroupDialog(QtWidgets.QDialog): + def __init__(self, controller, parent): + super(ProductGroupDialog, self).__init__(parent) + self.setWindowTitle("Grouping products") + self.setMinimumWidth(250) + self.setModal(True) + + main_label = QtWidgets.QLabel("Group Name", self) + + group_name_input = PlaceholderLineEdit(self) + group_name_input.setPlaceholderText("Remain blank to ungroup..") + + group_btn = QtWidgets.QPushButton("Apply", self) + group_btn.setAutoDefault(True) + group_btn.setDefault(True) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(main_label, 0) + layout.addWidget(group_name_input, 0) + layout.addWidget(group_btn, 0) + + group_btn.clicked.connect(self._on_apply_click) + + self._project_name = None + self._product_ids = set() + + self._controller = controller + self._group_btn = group_btn + self._group_name_input = group_name_input + + def set_product_ids(self, project_name, product_ids): + self._project_name = project_name + self._product_ids = product_ids + + def _on_apply_click(self): + group_name = self._group_name_input.text().strip() or None + self._controller.change_products_group( + self._project_name, self._product_ids, group_name + ) + self.close() diff --git a/openpype/tools/ayon_loader/ui/product_types_widget.py b/openpype/tools/ayon_loader/ui/product_types_widget.py new file mode 100644 index 0000000000..a84a7ff846 --- /dev/null +++ b/openpype/tools/ayon_loader/ui/product_types_widget.py @@ -0,0 +1,220 @@ +from qtpy import QtWidgets, QtGui, QtCore + +from openpype.tools.ayon_utils.widgets import get_qt_icon + +PRODUCT_TYPE_ROLE = QtCore.Qt.UserRole + 1 + + +class ProductTypesQtModel(QtGui.QStandardItemModel): + refreshed = QtCore.Signal() + filter_changed = QtCore.Signal() + + def __init__(self, controller): + super(ProductTypesQtModel, self).__init__() + self._controller = controller + + self._refreshing = False + self._bulk_change = False + self._items_by_name = {} + + def is_refreshing(self): + return self._refreshing + + def get_filter_info(self): + """Product types filtering info. + + Returns: + dict[str, bool]: Filtering value by product type name. False value + means to hide product type. + """ + + return { + name: item.checkState() == QtCore.Qt.Checked + for name, item in self._items_by_name.items() + } + + def refresh(self, project_name): + self._refreshing = True + product_type_items = self._controller.get_product_type_items( + project_name) + + items_to_remove = set(self._items_by_name.keys()) + new_items = [] + for product_type_item in product_type_items: + name = product_type_item.name + items_to_remove.discard(name) + item = self._items_by_name.get(product_type_item.name) + if item is None: + item = QtGui.QStandardItem(name) + item.setData(name, PRODUCT_TYPE_ROLE) + item.setEditable(False) + item.setCheckable(True) + new_items.append(item) + self._items_by_name[name] = item + + item.setCheckState( + QtCore.Qt.Checked + if product_type_item.checked + else QtCore.Qt.Unchecked + ) + icon = get_qt_icon(product_type_item.icon) + item.setData(icon, QtCore.Qt.DecorationRole) + + root_item = self.invisibleRootItem() + if new_items: + root_item.appendRows(new_items) + + for name in items_to_remove: + item = self._items_by_name.pop(name) + root_item.removeRow(item.row()) + + self._refreshing = False + self.refreshed.emit() + + def setData(self, index, value, role=None): + checkstate_changed = False + if role is None: + role = QtCore.Qt.EditRole + elif role == QtCore.Qt.CheckStateRole: + checkstate_changed = True + output = super(ProductTypesQtModel, self).setData(index, value, role) + if checkstate_changed and not self._bulk_change: + self.filter_changed.emit() + return output + + def change_state_for_all(self, checked): + if self._items_by_name: + self.change_states(checked, self._items_by_name.keys()) + + def change_states(self, checked, product_types): + product_types = set(product_types) + if not product_types: + return + + if checked is None: + state = None + elif checked: + state = QtCore.Qt.Checked + else: + state = QtCore.Qt.Unchecked + + self._bulk_change = True + + changed = False + for product_type in product_types: + item = self._items_by_name.get(product_type) + if item is None: + continue + new_state = state + item_checkstate = item.checkState() + if new_state is None: + if item_checkstate == QtCore.Qt.Checked: + new_state = QtCore.Qt.Unchecked + else: + new_state = QtCore.Qt.Checked + elif item_checkstate == new_state: + continue + changed = True + item.setCheckState(new_state) + + self._bulk_change = False + + if changed: + self.filter_changed.emit() + + +class ProductTypesView(QtWidgets.QListView): + filter_changed = QtCore.Signal() + + def __init__(self, controller, parent): + super(ProductTypesView, self).__init__(parent) + + self.setSelectionMode( + QtWidgets.QAbstractItemView.ExtendedSelection + ) + self.setAlternatingRowColors(True) + self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + + product_types_model = ProductTypesQtModel(controller) + product_types_proxy_model = QtCore.QSortFilterProxyModel() + product_types_proxy_model.setSourceModel(product_types_model) + + self.setModel(product_types_proxy_model) + + product_types_model.refreshed.connect(self._on_refresh_finished) + product_types_model.filter_changed.connect(self._on_filter_change) + self.customContextMenuRequested.connect(self._on_context_menu) + + controller.register_event_callback( + "selection.project.changed", + self._on_project_change + ) + + self._controller = controller + + self._product_types_model = product_types_model + self._product_types_proxy_model = product_types_proxy_model + + def get_filter_info(self): + return self._product_types_model.get_filter_info() + + def _on_project_change(self, event): + project_name = event["project_name"] + self._product_types_model.refresh(project_name) + + def _on_refresh_finished(self): + self.filter_changed.emit() + + def _on_filter_change(self): + if not self._product_types_model.is_refreshing(): + self.filter_changed.emit() + + def _change_selection_state(self, checkstate): + selection_model = self.selectionModel() + product_types = { + index.data(PRODUCT_TYPE_ROLE) + for index in selection_model.selectedIndexes() + } + product_types.discard(None) + self._product_types_model.change_states(checkstate, product_types) + + def _on_enable_all(self): + self._product_types_model.change_state_for_all(True) + + def _on_disable_all(self): + self._product_types_model.change_state_for_all(False) + + def _on_context_menu(self, pos): + menu = QtWidgets.QMenu(self) + + # Add enable all action + action_check_all = QtWidgets.QAction(menu) + action_check_all.setText("Enable All") + action_check_all.triggered.connect(self._on_enable_all) + # Add disable all action + action_uncheck_all = QtWidgets.QAction(menu) + action_uncheck_all.setText("Disable All") + action_uncheck_all.triggered.connect(self._on_disable_all) + + menu.addAction(action_check_all) + menu.addAction(action_uncheck_all) + + # Get mouse position + global_pos = self.viewport().mapToGlobal(pos) + menu.exec_(global_pos) + + def event(self, event): + if event.type() == QtCore.QEvent.KeyPress: + if event.key() == QtCore.Qt.Key_Space: + self._change_selection_state(None) + return True + + if event.key() == QtCore.Qt.Key_Backspace: + self._change_selection_state(False) + return True + + if event.key() == QtCore.Qt.Key_Return: + self._change_selection_state(True) + return True + + return super(ProductTypesView, self).event(event) diff --git a/openpype/tools/ayon_loader/ui/products_delegates.py b/openpype/tools/ayon_loader/ui/products_delegates.py new file mode 100644 index 0000000000..6729468bfa --- /dev/null +++ b/openpype/tools/ayon_loader/ui/products_delegates.py @@ -0,0 +1,191 @@ +import numbers +from qtpy import QtWidgets, QtCore, QtGui + +from openpype.tools.utils.lib import format_version + +from .products_model import ( + PRODUCT_ID_ROLE, + VERSION_NAME_EDIT_ROLE, + VERSION_ID_ROLE, + PRODUCT_IN_SCENE_ROLE, +) + + +class VersionComboBox(QtWidgets.QComboBox): + value_changed = QtCore.Signal(str) + + def __init__(self, product_id, parent): + super(VersionComboBox, self).__init__(parent) + self._product_id = product_id + self._items_by_id = {} + + self._current_id = None + + self.currentIndexChanged.connect(self._on_index_change) + + def update_versions(self, version_items, current_version_id): + model = self.model() + root_item = model.invisibleRootItem() + version_items = list(reversed(version_items)) + version_ids = [ + version_item.version_id + for version_item in version_items + ] + if current_version_id not in version_ids and version_ids: + current_version_id = version_ids[0] + self._current_id = current_version_id + + to_remove = set(self._items_by_id.keys()) - set(version_ids) + for item_id in to_remove: + item = self._items_by_id.pop(item_id) + root_item.removeRow(item.row()) + + for idx, version_item in enumerate(version_items): + version_id = version_item.version_id + + item = self._items_by_id.get(version_id) + if item is None: + label = format_version( + abs(version_item.version), version_item.is_hero + ) + item = QtGui.QStandardItem(label) + item.setData(version_id, QtCore.Qt.UserRole) + self._items_by_id[version_id] = item + + if item.row() != idx: + root_item.insertRow(idx, item) + + index = version_ids.index(current_version_id) + if self.currentIndex() != index: + self.setCurrentIndex(index) + + def _on_index_change(self): + idx = self.currentIndex() + value = self.itemData(idx) + if value == self._current_id: + return + self._current_id = value + self.value_changed.emit(self._product_id) + + +class VersionDelegate(QtWidgets.QStyledItemDelegate): + """A delegate that display version integer formatted as version string.""" + + version_changed = QtCore.Signal() + + def __init__(self, *args, **kwargs): + super(VersionDelegate, self).__init__(*args, **kwargs) + self._editor_by_product_id = {} + + def displayText(self, value, locale): + if not isinstance(value, numbers.Integral): + return "N/A" + return format_version(abs(value), value < 0) + + def paint(self, painter, option, index): + fg_color = index.data(QtCore.Qt.ForegroundRole) + if fg_color: + if isinstance(fg_color, QtGui.QBrush): + fg_color = fg_color.color() + elif isinstance(fg_color, QtGui.QColor): + pass + else: + fg_color = None + + if not fg_color: + return super(VersionDelegate, self).paint(painter, option, index) + + if option.widget: + style = option.widget.style() + else: + style = QtWidgets.QApplication.style() + + style.drawControl( + style.CE_ItemViewItem, option, painter, option.widget + ) + + painter.save() + + text = self.displayText( + index.data(QtCore.Qt.DisplayRole), option.locale + ) + pen = painter.pen() + pen.setColor(fg_color) + painter.setPen(pen) + + text_rect = style.subElementRect(style.SE_ItemViewItemText, option) + text_margin = style.proxy().pixelMetric( + style.PM_FocusFrameHMargin, option, option.widget + ) + 1 + + painter.drawText( + text_rect.adjusted(text_margin, 0, - text_margin, 0), + option.displayAlignment, + text + ) + + painter.restore() + + def createEditor(self, parent, option, index): + product_id = index.data(PRODUCT_ID_ROLE) + if not product_id: + return + + editor = VersionComboBox(product_id, parent) + self._editor_by_product_id[product_id] = editor + editor.value_changed.connect(self._on_editor_change) + + return editor + + def _on_editor_change(self, product_id): + editor = self._editor_by_product_id[product_id] + + # Update model data + self.commitData.emit(editor) + # Display model data + self.version_changed.emit() + + def setEditorData(self, editor, index): + editor.clear() + + # Current value of the index + versions = index.data(VERSION_NAME_EDIT_ROLE) or [] + version_id = index.data(VERSION_ID_ROLE) + editor.update_versions(versions, version_id) + + def setModelData(self, editor, model, index): + """Apply the integer version back in the model""" + + version_id = editor.itemData(editor.currentIndex()) + model.setData(index, version_id, VERSION_NAME_EDIT_ROLE) + + +class LoadedInSceneDelegate(QtWidgets.QStyledItemDelegate): + """Delegate for Loaded in Scene state columns. + + Shows "Yes" or "No" for 1 or 0 values, or "N/A" for other values. + Colorizes green or dark grey based on values. + """ + + def __init__(self, *args, **kwargs): + super(LoadedInSceneDelegate, self).__init__(*args, **kwargs) + self._colors = { + 1: QtGui.QColor(80, 170, 80), + 0: QtGui.QColor(90, 90, 90), + } + self._default_color = QtGui.QColor(90, 90, 90) + + def displayText(self, value, locale): + if value == 0: + return "No" + elif value == 1: + return "Yes" + return "N/A" + + def initStyleOption(self, option, index): + super(LoadedInSceneDelegate, self).initStyleOption(option, index) + + # Colorize based on value + value = index.data(PRODUCT_IN_SCENE_ROLE) + color = self._colors.get(value, self._default_color) + option.palette.setBrush(QtGui.QPalette.Text, color) diff --git a/openpype/tools/ayon_loader/ui/products_model.py b/openpype/tools/ayon_loader/ui/products_model.py new file mode 100644 index 0000000000..741f15766b --- /dev/null +++ b/openpype/tools/ayon_loader/ui/products_model.py @@ -0,0 +1,590 @@ +import collections + +import qtawesome +from qtpy import QtGui, QtCore + +from openpype.style import get_default_entity_icon_color +from openpype.tools.ayon_utils.widgets import get_qt_icon + +PRODUCTS_MODEL_SENDER_NAME = "qt_products_model" + +GROUP_TYPE_ROLE = QtCore.Qt.UserRole + 1 +MERGED_COLOR_ROLE = QtCore.Qt.UserRole + 2 +FOLDER_LABEL_ROLE = QtCore.Qt.UserRole + 3 +FOLDER_ID_ROLE = QtCore.Qt.UserRole + 4 +PRODUCT_ID_ROLE = QtCore.Qt.UserRole + 5 +PRODUCT_NAME_ROLE = QtCore.Qt.UserRole + 6 +PRODUCT_TYPE_ROLE = QtCore.Qt.UserRole + 7 +PRODUCT_TYPE_ICON_ROLE = QtCore.Qt.UserRole + 8 +PRODUCT_IN_SCENE_ROLE = QtCore.Qt.UserRole + 9 +VERSION_ID_ROLE = QtCore.Qt.UserRole + 10 +VERSION_HERO_ROLE = QtCore.Qt.UserRole + 11 +VERSION_NAME_ROLE = QtCore.Qt.UserRole + 12 +VERSION_NAME_EDIT_ROLE = QtCore.Qt.UserRole + 13 +VERSION_PUBLISH_TIME_ROLE = QtCore.Qt.UserRole + 14 +VERSION_AUTHOR_ROLE = QtCore.Qt.UserRole + 15 +VERSION_FRAME_RANGE_ROLE = QtCore.Qt.UserRole + 16 +VERSION_DURATION_ROLE = QtCore.Qt.UserRole + 17 +VERSION_HANDLES_ROLE = QtCore.Qt.UserRole + 18 +VERSION_STEP_ROLE = QtCore.Qt.UserRole + 19 +VERSION_AVAILABLE_ROLE = QtCore.Qt.UserRole + 20 +VERSION_THUMBNAIL_ID_ROLE = QtCore.Qt.UserRole + 21 + + +class ProductsModel(QtGui.QStandardItemModel): + refreshed = QtCore.Signal() + version_changed = QtCore.Signal() + column_labels = [ + "Product name", + "Product type", + "Folder", + "Version", + "Time", + "Author", + "Frames", + "Duration", + "Handles", + "Step", + "In scene", + "Availability", + ] + merged_items_colors = [ + ("#{0:02x}{1:02x}{2:02x}".format(*c), QtGui.QColor(*c)) + for c in [ + (55, 161, 222), # Light Blue + (231, 176, 0), # Yellow + (154, 13, 255), # Purple + (130, 184, 30), # Light Green + (211, 79, 63), # Light Red + (179, 181, 182), # Grey + (194, 57, 179), # Pink + (0, 120, 215), # Dark Blue + (0, 204, 106), # Dark Green + (247, 99, 12), # Orange + ] + ] + + version_col = column_labels.index("Version") + published_time_col = column_labels.index("Time") + folders_label_col = column_labels.index("Folder") + in_scene_col = column_labels.index("In scene") + + def __init__(self, controller): + super(ProductsModel, self).__init__() + self.setColumnCount(len(self.column_labels)) + for idx, label in enumerate(self.column_labels): + self.setHeaderData(idx, QtCore.Qt.Horizontal, label) + self._controller = controller + + # Variables to store 'QStandardItem' + self._items_by_id = {} + self._group_items_by_name = {} + self._merged_items_by_id = {} + + # product item objects (they have version information) + self._product_items_by_id = {} + self._grouping_enabled = True + self._reset_merge_color = False + self._color_iterator = self._color_iter() + self._group_icon = None + + self._last_project_name = None + self._last_folder_ids = [] + + def get_product_item_indexes(self): + return [ + item.index() + for item in self._items_by_id.values() + ] + + def get_product_item_by_id(self, product_id): + """ + + Args: + product_id (str): Product id. + + Returns: + Union[ProductItem, None]: Product item with version information. + """ + + return self._product_items_by_id.get(product_id) + + def set_enable_grouping(self, enable_grouping): + if enable_grouping is self._grouping_enabled: + return + self._grouping_enabled = enable_grouping + # Ignore change if groups are not available + self.refresh(self._last_project_name, self._last_folder_ids) + + def flags(self, index): + # Make the version column editable + if index.column() == self.version_col and index.data(PRODUCT_ID_ROLE): + return ( + QtCore.Qt.ItemIsEnabled + | QtCore.Qt.ItemIsSelectable + | QtCore.Qt.ItemIsEditable + ) + if index.column() != 0: + index = self.index(index.row(), 0, index.parent()) + return super(ProductsModel, self).flags(index) + + def data(self, index, role=None): + if role is None: + role = QtCore.Qt.DisplayRole + + if not index.isValid(): + return None + + col = index.column() + if col == 0: + return super(ProductsModel, self).data(index, role) + + if role == QtCore.Qt.DecorationRole: + if col == 1: + role = PRODUCT_TYPE_ICON_ROLE + else: + return None + + if ( + role == VERSION_NAME_EDIT_ROLE + or (role == QtCore.Qt.EditRole and col == self.version_col) + ): + index = self.index(index.row(), 0, index.parent()) + product_id = index.data(PRODUCT_ID_ROLE) + product_item = self._product_items_by_id.get(product_id) + if product_item is None: + return None + return list(product_item.version_items.values()) + + if role == QtCore.Qt.EditRole: + return None + + if role == QtCore.Qt.DisplayRole: + if not index.data(PRODUCT_ID_ROLE): + return None + if col == self.version_col: + role = VERSION_NAME_ROLE + elif col == 1: + role = PRODUCT_TYPE_ROLE + elif col == 2: + role = FOLDER_LABEL_ROLE + elif col == 4: + role = VERSION_PUBLISH_TIME_ROLE + elif col == 5: + role = VERSION_AUTHOR_ROLE + elif col == 6: + role = VERSION_FRAME_RANGE_ROLE + elif col == 7: + role = VERSION_DURATION_ROLE + elif col == 8: + role = VERSION_HANDLES_ROLE + elif col == 9: + role = VERSION_STEP_ROLE + elif col == 10: + role = PRODUCT_IN_SCENE_ROLE + elif col == 11: + role = VERSION_AVAILABLE_ROLE + else: + return None + + index = self.index(index.row(), 0, index.parent()) + + return super(ProductsModel, self).data(index, role) + + def setData(self, index, value, role=None): + if not index.isValid(): + return False + + if role is None: + role = QtCore.Qt.EditRole + + col = index.column() + if col == self.version_col and role == QtCore.Qt.EditRole: + role = VERSION_NAME_EDIT_ROLE + + if role == VERSION_NAME_EDIT_ROLE: + if col != 0: + index = self.index(index.row(), 0, index.parent()) + product_id = index.data(PRODUCT_ID_ROLE) + product_item = self._product_items_by_id[product_id] + final_version_item = None + for v_id, version_item in product_item.version_items.items(): + if v_id == value: + final_version_item = version_item + break + + if final_version_item is None: + return False + if index.data(VERSION_ID_ROLE) == final_version_item.version_id: + return True + item = self.itemFromIndex(index) + self._set_version_data_to_product_item(item, final_version_item) + self.version_changed.emit() + return True + return super(ProductsModel, self).setData(index, value, role) + + def _get_next_color(self): + return next(self._color_iterator) + + def _color_iter(self): + while True: + for color in self.merged_items_colors: + if self._reset_merge_color: + self._reset_merge_color = False + break + yield color + + def _clear(self): + root_item = self.invisibleRootItem() + root_item.removeRows(0, root_item.rowCount()) + + self._items_by_id = {} + self._group_items_by_name = {} + self._merged_items_by_id = {} + self._product_items_by_id = {} + self._reset_merge_color = True + + def _get_group_icon(self): + if self._group_icon is None: + self._group_icon = qtawesome.icon( + "fa.object-group", + color=get_default_entity_icon_color() + ) + return self._group_icon + + def _get_group_model_item(self, group_name): + model_item = self._group_items_by_name.get(group_name) + if model_item is None: + model_item = QtGui.QStandardItem(group_name) + model_item.setData( + self._get_group_icon(), QtCore.Qt.DecorationRole + ) + model_item.setData(0, GROUP_TYPE_ROLE) + model_item.setEditable(False) + model_item.setColumnCount(self.columnCount()) + self._group_items_by_name[group_name] = model_item + return model_item + + def _get_merged_model_item(self, path, count, hex_color): + model_item = self._merged_items_by_id.get(path) + if model_item is None: + model_item = QtGui.QStandardItem() + model_item.setData(1, GROUP_TYPE_ROLE) + model_item.setData(hex_color, MERGED_COLOR_ROLE) + model_item.setEditable(False) + model_item.setColumnCount(self.columnCount()) + self._merged_items_by_id[path] = model_item + label = "{} ({})".format(path, count) + model_item.setData(label, QtCore.Qt.DisplayRole) + return model_item + + def _set_version_data_to_product_item(self, model_item, version_item): + """ + + Args: + model_item (QtGui.QStandardItem): Item which should have values + from version item. + version_item (VersionItem): Item from entities model with + information about version. + """ + + model_item.setData(version_item.version_id, VERSION_ID_ROLE) + model_item.setData(version_item.version, VERSION_NAME_ROLE) + model_item.setData(version_item.version_id, VERSION_ID_ROLE) + model_item.setData(version_item.is_hero, VERSION_HERO_ROLE) + model_item.setData( + version_item.published_time, VERSION_PUBLISH_TIME_ROLE + ) + model_item.setData(version_item.author, VERSION_AUTHOR_ROLE) + model_item.setData(version_item.frame_range, VERSION_FRAME_RANGE_ROLE) + model_item.setData(version_item.duration, VERSION_DURATION_ROLE) + model_item.setData(version_item.handles, VERSION_HANDLES_ROLE) + model_item.setData(version_item.step, VERSION_STEP_ROLE) + model_item.setData( + version_item.thumbnail_id, VERSION_THUMBNAIL_ID_ROLE) + + def _get_product_model_item(self, product_item): + model_item = self._items_by_id.get(product_item.product_id) + versions = list(product_item.version_items.values()) + versions.sort() + last_version = versions[-1] + if model_item is None: + product_id = product_item.product_id + model_item = QtGui.QStandardItem(product_item.product_name) + model_item.setEditable(False) + icon = get_qt_icon(product_item.product_icon) + product_type_icon = get_qt_icon(product_item.product_type_icon) + model_item.setColumnCount(self.columnCount()) + model_item.setData(icon, QtCore.Qt.DecorationRole) + model_item.setData(product_id, PRODUCT_ID_ROLE) + model_item.setData(product_item.product_name, PRODUCT_NAME_ROLE) + model_item.setData(product_item.product_type, PRODUCT_TYPE_ROLE) + model_item.setData(product_type_icon, PRODUCT_TYPE_ICON_ROLE) + model_item.setData(product_item.folder_id, FOLDER_ID_ROLE) + + self._product_items_by_id[product_id] = product_item + self._items_by_id[product_id] = model_item + + model_item.setData(product_item.folder_label, FOLDER_LABEL_ROLE) + in_scene = 1 if product_item.product_in_scene else 0 + model_item.setData(in_scene, PRODUCT_IN_SCENE_ROLE) + + self._set_version_data_to_product_item(model_item, last_version) + return model_item + + def get_last_project_name(self): + return self._last_project_name + + def refresh(self, project_name, folder_ids): + self._clear() + + self._last_project_name = project_name + self._last_folder_ids = folder_ids + + product_items = self._controller.get_product_items( + project_name, + folder_ids, + sender=PRODUCTS_MODEL_SENDER_NAME + ) + product_items_by_id = { + product_item.product_id: product_item + for product_item in product_items + } + + # Prepare product groups + product_name_matches_by_group = collections.defaultdict(dict) + for product_item in product_items_by_id.values(): + group_name = None + if self._grouping_enabled: + group_name = product_item.group_name + + product_name = product_item.product_name + group = product_name_matches_by_group[group_name] + if product_name not in group: + group[product_name] = [product_item] + continue + group[product_name].append(product_item) + + group_names = set(product_name_matches_by_group.keys()) + + root_item = self.invisibleRootItem() + new_root_items = [] + merged_paths = set() + for group_name in group_names: + key_parts = [] + if group_name: + key_parts.append(group_name) + + groups = product_name_matches_by_group[group_name] + merged_product_items = {} + top_items = [] + group_product_types = set() + for product_name, product_items in groups.items(): + group_product_types |= {p.product_type for p in product_items} + if len(product_items) == 1: + top_items.append(product_items[0]) + else: + path = "/".join(key_parts + [product_name]) + merged_paths.add(path) + merged_product_items[path] = ( + product_name, + product_items, + ) + + parent_item = None + if group_name: + parent_item = self._get_group_model_item(group_name) + parent_item.setData( + "|".join(group_product_types), PRODUCT_TYPE_ROLE) + + new_items = [] + if parent_item is not None and parent_item.row() < 0: + new_root_items.append(parent_item) + + for product_item in top_items: + item = self._get_product_model_item(product_item) + new_items.append(item) + + for path_info in merged_product_items.values(): + product_name, product_items = path_info + (merged_color_hex, merged_color_qt) = self._get_next_color() + merged_color = qtawesome.icon( + "fa.circle", color=merged_color_qt) + merged_item = self._get_merged_model_item( + product_name, len(product_items), merged_color_hex) + merged_item.setData(merged_color, QtCore.Qt.DecorationRole) + new_items.append(merged_item) + + merged_product_types = set() + new_merged_items = [] + for product_item in product_items: + item = self._get_product_model_item(product_item) + new_merged_items.append(item) + merged_product_types.add(product_item.product_type) + + merged_item.setData( + "|".join(merged_product_types), PRODUCT_TYPE_ROLE) + if new_merged_items: + merged_item.appendRows(new_merged_items) + + if not new_items: + continue + + if parent_item is None: + new_root_items.extend(new_items) + else: + parent_item.appendRows(new_items) + + if new_root_items: + root_item.appendRows(new_root_items) + + self.refreshed.emit() + # --------------------------------- + # This implementation does not call '_clear' at the start + # but is more complex and probably slower + # --------------------------------- + # def _remove_items(self, items): + # if not items: + # return + # root_item = self.invisibleRootItem() + # for item in items: + # row = item.row() + # if row < 0: + # continue + # parent = item.parent() + # if parent is None: + # parent = root_item + # parent.removeRow(row) + # + # def _remove_group_items(self, group_names): + # group_items = [ + # self._group_items_by_name.pop(group_name) + # for group_name in group_names + # ] + # self._remove_items(group_items) + # + # def _remove_merged_items(self, paths): + # merged_items = [ + # self._merged_items_by_id.pop(path) + # for path in paths + # ] + # self._remove_items(merged_items) + # + # def _remove_product_items(self, product_ids): + # product_items = [] + # for product_id in product_ids: + # self._product_items_by_id.pop(product_id) + # product_items.append(self._items_by_id.pop(product_id)) + # self._remove_items(product_items) + # + # def _add_to_new_items(self, item, parent_item, new_items, root_item): + # if item.row() < 0: + # new_items.append(item) + # else: + # item_parent = item.parent() + # if item_parent is not parent_item: + # if item_parent is None: + # item_parent = root_item + # item_parent.takeRow(item.row()) + # new_items.append(item) + + # def refresh(self, project_name, folder_ids): + # product_items = self._controller.get_product_items( + # project_name, + # folder_ids, + # sender=PRODUCTS_MODEL_SENDER_NAME + # ) + # product_items_by_id = { + # product_item.product_id: product_item + # for product_item in product_items + # } + # # Remove product items that are not available + # product_ids_to_remove = ( + # set(self._items_by_id.keys()) - set(product_items_by_id.keys()) + # ) + # self._remove_product_items(product_ids_to_remove) + # + # # Prepare product groups + # product_name_matches_by_group = collections.defaultdict(dict) + # for product_item in product_items_by_id.values(): + # group_name = None + # if self._grouping_enabled: + # group_name = product_item.group_name + # + # product_name = product_item.product_name + # group = product_name_matches_by_group[group_name] + # if product_name not in group: + # group[product_name] = [product_item] + # continue + # group[product_name].append(product_item) + # + # group_names = set(product_name_matches_by_group.keys()) + # + # root_item = self.invisibleRootItem() + # new_root_items = [] + # merged_paths = set() + # for group_name in group_names: + # key_parts = [] + # if group_name: + # key_parts.append(group_name) + # + # groups = product_name_matches_by_group[group_name] + # merged_product_items = {} + # top_items = [] + # for product_name, product_items in groups.items(): + # if len(product_items) == 1: + # top_items.append(product_items[0]) + # else: + # path = "/".join(key_parts + [product_name]) + # merged_paths.add(path) + # merged_product_items[path] = product_items + # + # parent_item = None + # if group_name: + # parent_item = self._get_group_model_item(group_name) + # + # new_items = [] + # if parent_item is not None and parent_item.row() < 0: + # new_root_items.append(parent_item) + # + # for product_item in top_items: + # item = self._get_product_model_item(product_item) + # self._add_to_new_items( + # item, parent_item, new_items, root_item + # ) + # + # for path, product_items in merged_product_items.items(): + # merged_item = self._get_merged_model_item(path) + # self._add_to_new_items( + # merged_item, parent_item, new_items, root_item + # ) + # + # new_merged_items = [] + # for product_item in product_items: + # item = self._get_product_model_item(product_item) + # self._add_to_new_items( + # item, merged_item, new_merged_items, root_item + # ) + # + # if new_merged_items: + # merged_item.appendRows(new_merged_items) + # + # if not new_items: + # continue + # + # if parent_item is not None: + # parent_item.appendRows(new_items) + # continue + # + # new_root_items.extend(new_items) + # + # root_item.appendRows(new_root_items) + # + # merged_item_ids_to_remove = ( + # set(self._merged_items_by_id.keys()) - merged_paths + # ) + # group_names_to_remove = ( + # set(self._group_items_by_name.keys()) - set(group_names) + # ) + # self._remove_merged_items(merged_item_ids_to_remove) + # self._remove_group_items(group_names_to_remove) diff --git a/openpype/tools/ayon_loader/ui/products_widget.py b/openpype/tools/ayon_loader/ui/products_widget.py new file mode 100644 index 0000000000..cfc18431a6 --- /dev/null +++ b/openpype/tools/ayon_loader/ui/products_widget.py @@ -0,0 +1,400 @@ +import collections + +from qtpy import QtWidgets, QtCore + +from openpype.tools.utils import ( + RecursiveSortFilterProxyModel, + DeselectableTreeView, +) +from openpype.tools.utils.delegates import PrettyTimeDelegate + +from .products_model import ( + ProductsModel, + PRODUCTS_MODEL_SENDER_NAME, + PRODUCT_TYPE_ROLE, + GROUP_TYPE_ROLE, + MERGED_COLOR_ROLE, + FOLDER_ID_ROLE, + PRODUCT_ID_ROLE, + VERSION_ID_ROLE, + VERSION_THUMBNAIL_ID_ROLE, +) +from .products_delegates import VersionDelegate, LoadedInSceneDelegate +from .actions_utils import show_actions_menu + + +class ProductsProxyModel(RecursiveSortFilterProxyModel): + def __init__(self, parent=None): + super(ProductsProxyModel, self).__init__(parent) + + self._product_type_filters = {} + self._ascending_sort = True + + def set_product_type_filters(self, product_type_filters): + self._product_type_filters = product_type_filters + self.invalidateFilter() + + def filterAcceptsRow(self, source_row, source_parent): + source_model = self.sourceModel() + index = source_model.index(source_row, 0, source_parent) + product_types_s = source_model.data(index, PRODUCT_TYPE_ROLE) + product_types = [] + if product_types_s: + product_types = product_types_s.split("|") + + for product_type in product_types: + if not self._product_type_filters.get(product_type, True): + return False + return super(ProductsProxyModel, self).filterAcceptsRow( + source_row, source_parent) + + def lessThan(self, left, right): + l_model = left.model() + r_model = right.model() + left_group_type = l_model.data(left, GROUP_TYPE_ROLE) + right_group_type = r_model.data(right, GROUP_TYPE_ROLE) + # Groups are always on top, merged product types are below + # and items without group at the bottom + # QUESTION Do we need to do it this way? + if left_group_type != right_group_type: + if left_group_type is None: + output = False + elif right_group_type is None: + output = True + else: + output = left_group_type < right_group_type + if not self._ascending_sort: + output = not output + return output + return super(ProductsProxyModel, self).lessThan(left, right) + + def sort(self, column, order=None): + if order is None: + order = QtCore.Qt.AscendingOrder + self._ascending_sort = order == QtCore.Qt.AscendingOrder + super(ProductsProxyModel, self).sort(column, order) + + +class ProductsWidget(QtWidgets.QWidget): + refreshed = QtCore.Signal() + merged_products_selection_changed = QtCore.Signal() + selection_changed = QtCore.Signal() + version_changed = QtCore.Signal() + default_widths = ( + 200, # Product name + 90, # Product type + 130, # Folder label + 60, # Version + 125, # Time + 75, # Author + 75, # Frames + 60, # Duration + 55, # Handles + 10, # Step + 25, # Loaded in scene + 65, # Site info (maybe?) + ) + + def __init__(self, controller, parent): + super(ProductsWidget, self).__init__(parent) + + self._controller = controller + + products_view = DeselectableTreeView(self) + # TODO - define custom object name in style + products_view.setObjectName("SubsetView") + products_view.setSelectionMode( + QtWidgets.QAbstractItemView.ExtendedSelection + ) + products_view.setAllColumnsShowFocus(True) + # TODO - add context menu + products_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + products_view.setSortingEnabled(True) + # Sort by product type + products_view.sortByColumn(1, QtCore.Qt.AscendingOrder) + products_view.setAlternatingRowColors(True) + + products_model = ProductsModel(controller) + products_proxy_model = ProductsProxyModel() + products_proxy_model.setSourceModel(products_model) + + products_view.setModel(products_proxy_model) + + for idx, width in enumerate(self.default_widths): + products_view.setColumnWidth(idx, width) + + version_delegate = VersionDelegate() + products_view.setItemDelegateForColumn( + products_model.version_col, version_delegate) + + time_delegate = PrettyTimeDelegate() + products_view.setItemDelegateForColumn( + products_model.published_time_col, time_delegate) + + in_scene_delegate = LoadedInSceneDelegate() + products_view.setItemDelegateForColumn( + products_model.in_scene_col, in_scene_delegate) + + main_layout = QtWidgets.QHBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.addWidget(products_view, 1) + + products_proxy_model.rowsInserted.connect(self._on_rows_inserted) + products_proxy_model.rowsMoved.connect(self._on_rows_moved) + products_model.refreshed.connect(self._on_refresh) + products_view.customContextMenuRequested.connect( + self._on_context_menu) + products_view.selectionModel().selectionChanged.connect( + self._on_selection_change) + products_model.version_changed.connect(self._on_version_change) + + controller.register_event_callback( + "selection.folders.changed", + self._on_folders_selection_change, + ) + controller.register_event_callback( + "products.refresh.finished", + self._on_products_refresh_finished + ) + controller.register_event_callback( + "products.group.changed", + self._on_group_changed + ) + + self._products_view = products_view + self._products_model = products_model + self._products_proxy_model = products_proxy_model + + self._version_delegate = version_delegate + self._time_delegate = time_delegate + + self._selected_project_name = None + self._selected_folder_ids = set() + + self._selected_merged_products = [] + self._selected_versions_info = [] + + # Set initial state of widget + # - Hide folders column + self._update_folders_label_visible() + # - Hide in scene column if is not supported (this won't change) + products_view.setColumnHidden( + products_model.in_scene_col, + not controller.is_loaded_products_supported() + ) + + def set_name_filer(self, name): + """Set filter of product name. + + Args: + name (str): The string filter. + """ + + self._products_proxy_model.setFilterFixedString(name) + + def set_product_type_filter(self, product_type_filters): + """ + + Args: + product_type_filters (dict[str, bool]): The filter of product + types. + """ + + self._products_proxy_model.set_product_type_filters( + product_type_filters + ) + + def set_enable_grouping(self, enable_grouping): + self._products_model.set_enable_grouping(enable_grouping) + + def get_selected_merged_products(self): + return self._selected_merged_products + + def get_selected_version_info(self): + return self._selected_versions_info + + def refresh(self): + self._refresh_model() + + def _fill_version_editor(self): + model = self._products_proxy_model + index_queue = collections.deque() + for row in range(model.rowCount()): + index_queue.append((row, None)) + + version_col = self._products_model.version_col + while index_queue: + (row, parent_index) = index_queue.popleft() + args = [row, 0] + if parent_index is not None: + args.append(parent_index) + index = model.index(*args) + rows = model.rowCount(index) + for row in range(rows): + index_queue.append((row, index)) + + product_id = model.data(index, PRODUCT_ID_ROLE) + if product_id is not None: + args[1] = version_col + v_index = model.index(*args) + self._products_view.openPersistentEditor(v_index) + + def _on_refresh(self): + self._fill_version_editor() + self.refreshed.emit() + + def _on_rows_inserted(self): + self._fill_version_editor() + + def _on_rows_moved(self): + self._fill_version_editor() + + def _refresh_model(self): + self._products_model.refresh( + self._selected_project_name, + self._selected_folder_ids + ) + + def _on_context_menu(self, point): + selection_model = self._products_view.selectionModel() + model = self._products_view.model() + project_name = self._products_model.get_last_project_name() + + version_ids = set() + indexes_queue = collections.deque() + indexes_queue.extend(selection_model.selectedIndexes()) + while indexes_queue: + index = indexes_queue.popleft() + for row in range(model.rowCount(index)): + child_index = model.index(row, 0, index) + indexes_queue.append(child_index) + version_id = model.data(index, VERSION_ID_ROLE) + if version_id is not None: + version_ids.add(version_id) + + action_items = self._controller.get_versions_action_items( + project_name, version_ids) + + # Prepare global point where to show the menu + global_point = self._products_view.mapToGlobal(point) + + result = show_actions_menu( + action_items, + global_point, + len(version_ids) == 1, + self + ) + action_item, options = result + if action_item is None or options is None: + return + + self._controller.trigger_action_item( + action_item.identifier, + options, + action_item.project_name, + version_ids=action_item.version_ids, + representation_ids=action_item.representation_ids, + ) + + def _on_selection_change(self): + selected_merged_products = [] + selection_model = self._products_view.selectionModel() + model = self._products_view.model() + indexes_queue = collections.deque() + indexes_queue.extend(selection_model.selectedIndexes()) + + # Helper for 'version_items' to avoid duplicated items + all_product_ids = set() + selected_version_ids = set() + # Version items contains information about selected version items + selected_versions_info = [] + while indexes_queue: + index = indexes_queue.popleft() + if index.column() != 0: + continue + + group_type = model.data(index, GROUP_TYPE_ROLE) + if group_type is None: + product_id = model.data(index, PRODUCT_ID_ROLE) + # Skip duplicates - when group and item are selected the item + # would be in the loop multiple times + if product_id in all_product_ids: + continue + + all_product_ids.add(product_id) + + version_id = model.data(index, VERSION_ID_ROLE) + selected_version_ids.add(version_id) + + thumbnail_id = model.data(index, VERSION_THUMBNAIL_ID_ROLE) + selected_versions_info.append({ + "folder_id": model.data(index, FOLDER_ID_ROLE), + "product_id": product_id, + "version_id": version_id, + "thumbnail_id": thumbnail_id, + }) + continue + + if group_type == 0: + for row in range(model.rowCount(index)): + child_index = model.index(row, 0, index) + indexes_queue.append(child_index) + continue + + if group_type != 1: + continue + + item_folder_ids = set() + for row in range(model.rowCount(index)): + child_index = model.index(row, 0, index) + indexes_queue.append(child_index) + + folder_id = model.data(child_index, FOLDER_ID_ROLE) + item_folder_ids.add(folder_id) + + if not item_folder_ids: + continue + + hex_color = model.data(index, MERGED_COLOR_ROLE) + item_data = { + "color": hex_color, + "folder_ids": item_folder_ids + } + selected_merged_products.append(item_data) + + prev_selected_merged_products = self._selected_merged_products + self._selected_merged_products = selected_merged_products + self._selected_versions_info = selected_versions_info + + if selected_merged_products != prev_selected_merged_products: + self.merged_products_selection_changed.emit() + self.selection_changed.emit() + self._controller.set_selected_versions(selected_version_ids) + + def _on_version_change(self): + self._on_selection_change() + + def _on_folders_selection_change(self, event): + self._selected_project_name = event["project_name"] + self._selected_folder_ids = event["folder_ids"] + self._refresh_model() + self._update_folders_label_visible() + + def _update_folders_label_visible(self): + folders_label_hidden = len(self._selected_folder_ids) <= 1 + self._products_view.setColumnHidden( + self._products_model.folders_label_col, + folders_label_hidden + ) + + def _on_products_refresh_finished(self, event): + if event["sender"] != PRODUCTS_MODEL_SENDER_NAME: + self._refresh_model() + + def _on_group_changed(self, event): + if event["project_name"] != self._selected_project_name: + return + folder_ids = event["folder_ids"] + if not set(folder_ids).intersection(set(self._selected_folder_ids)): + return + self.refresh() diff --git a/openpype/tools/ayon_loader/ui/repres_widget.py b/openpype/tools/ayon_loader/ui/repres_widget.py new file mode 100644 index 0000000000..7de582e629 --- /dev/null +++ b/openpype/tools/ayon_loader/ui/repres_widget.py @@ -0,0 +1,338 @@ +import collections + +from qtpy import QtWidgets, QtGui, QtCore +import qtawesome + +from openpype.style import get_default_entity_icon_color +from openpype.tools.ayon_utils.widgets import get_qt_icon +from openpype.tools.utils import DeselectableTreeView + +from .actions_utils import show_actions_menu + +REPRESENTAION_NAME_ROLE = QtCore.Qt.UserRole + 1 +REPRESENTATION_ID_ROLE = QtCore.Qt.UserRole + 2 +PRODUCT_NAME_ROLE = QtCore.Qt.UserRole + 3 +FOLDER_LABEL_ROLE = QtCore.Qt.UserRole + 4 +GROUP_TYPE_ROLE = QtCore.Qt.UserRole + 5 + + +class RepresentationsModel(QtGui.QStandardItemModel): + refreshed = QtCore.Signal() + colums_info = [ + ("Name", 120), + ("Product name", 125), + ("Folder", 125), + # ("Active site", 85), + # ("Remote site", 85) + ] + column_labels = [label for label, _ in colums_info] + column_widths = [width for _, width in colums_info] + folder_column = column_labels.index("Product name") + + def __init__(self, controller): + super(RepresentationsModel, self).__init__() + + self.setColumnCount(len(self.column_labels)) + + for idx, label in enumerate(self.column_labels): + self.setHeaderData(idx, QtCore.Qt.Horizontal, label) + + controller.register_event_callback( + "selection.project.changed", + self._on_project_change + ) + controller.register_event_callback( + "selection.versions.changed", + self._on_version_change + ) + self._selected_project_name = None + self._selected_version_ids = None + + self._group_icon = None + + self._items_by_id = {} + self._groups_items_by_name = {} + + self._controller = controller + + def refresh(self): + repre_items = self._controller.get_representation_items( + self._selected_project_name, self._selected_version_ids + ) + self._fill_items(repre_items) + self.refreshed.emit() + + def data(self, index, role=None): + if role is None: + role = QtCore.Qt.DisplayRole + + col = index.column() + if col != 0: + if role == QtCore.Qt.DecorationRole: + return None + + if role == QtCore.Qt.DisplayRole: + if col == 1: + role = PRODUCT_NAME_ROLE + elif col == 2: + role = FOLDER_LABEL_ROLE + index = self.index(index.row(), 0, index.parent()) + return super(RepresentationsModel, self).data(index, role) + + def setData(self, index, value, role=None): + if role is None: + role = QtCore.Qt.EditRole + return super(RepresentationsModel, self).setData(index, value, role) + + def _clear_items(self): + self._items_by_id = {} + root_item = self.invisibleRootItem() + root_item.removeRows(0, root_item.rowCount()) + + def _get_repre_item(self, repre_item): + repre_id = repre_item.representation_id + repre_name = repre_item.representation_name + repre_icon = repre_item.representation_icon + item = self._items_by_id.get(repre_id) + is_new_item = False + if item is None: + is_new_item = True + item = QtGui.QStandardItem() + self._items_by_id[repre_id] = item + item.setColumnCount(self.columnCount()) + item.setEditable(False) + + icon = get_qt_icon(repre_icon) + item.setData(repre_name, QtCore.Qt.DisplayRole) + item.setData(icon, QtCore.Qt.DecorationRole) + item.setData(repre_name, REPRESENTAION_NAME_ROLE) + item.setData(repre_id, REPRESENTATION_ID_ROLE) + item.setData(repre_item.product_name, PRODUCT_NAME_ROLE) + item.setData(repre_item.folder_label, FOLDER_LABEL_ROLE) + return is_new_item, item + + def _get_group_icon(self): + if self._group_icon is None: + self._group_icon = qtawesome.icon( + "fa.folder", + color=get_default_entity_icon_color() + ) + return self._group_icon + + def _get_group_item(self, repre_name): + item = self._groups_items_by_name.get(repre_name) + if item is not None: + return False, item + + # TODO add color + item = QtGui.QStandardItem() + item.setColumnCount(self.columnCount()) + item.setData(repre_name, QtCore.Qt.DisplayRole) + item.setData(self._get_group_icon(), QtCore.Qt.DecorationRole) + item.setData(0, GROUP_TYPE_ROLE) + item.setEditable(False) + self._groups_items_by_name[repre_name] = item + return True, item + + def _fill_items(self, repre_items): + items_to_remove = set(self._items_by_id.keys()) + repre_items_by_name = collections.defaultdict(list) + for repre_item in repre_items: + items_to_remove.discard(repre_item.representation_id) + repre_name = repre_item.representation_name + repre_items_by_name[repre_name].append(repre_item) + + root_item = self.invisibleRootItem() + for repre_id in items_to_remove: + item = self._items_by_id.pop(repre_id) + parent_item = item.parent() + if parent_item is None: + parent_item = root_item + parent_item.removeRow(item.row()) + + group_names = set() + new_root_items = [] + for repre_name, repre_name_items in repre_items_by_name.items(): + group_item = None + parent_is_group = False + if len(repre_name_items) > 1: + group_names.add(repre_name) + is_new_group, group_item = self._get_group_item(repre_name) + if is_new_group: + new_root_items.append(group_item) + parent_is_group = True + + new_group_items = [] + for repre_item in repre_name_items: + is_new_item, item = self._get_repre_item(repre_item) + item_parent = item.parent() + if item_parent is None: + item_parent = root_item + + if not is_new_item: + if parent_is_group: + if item_parent is group_item: + continue + elif item_parent is root_item: + continue + item_parent.takeRow(item.row()) + is_new_item = True + + if is_new_item: + new_group_items.append(item) + + if not new_group_items: + continue + + if group_item is not None: + group_item.appendRows(new_group_items) + else: + new_root_items.extend(new_group_items) + + if new_root_items: + root_item.appendRows(new_root_items) + + for group_name in set(self._groups_items_by_name) - group_names: + item = self._groups_items_by_name.pop(group_name) + parent_item = item.parent() + if parent_item is None: + parent_item = root_item + parent_item.removeRow(item.row()) + + def _on_project_change(self, event): + self._selected_project_name = event["project_name"] + + def _on_version_change(self, event): + self._selected_version_ids = event["version_ids"] + self.refresh() + + +class RepresentationsWidget(QtWidgets.QWidget): + def __init__(self, controller, parent): + super(RepresentationsWidget, self).__init__(parent) + + repre_view = DeselectableTreeView(self) + repre_view.setSelectionMode( + QtWidgets.QAbstractItemView.ExtendedSelection + ) + repre_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + repre_view.setSortingEnabled(True) + repre_view.setAlternatingRowColors(True) + + repre_model = RepresentationsModel(controller) + repre_proxy_model = QtCore.QSortFilterProxyModel() + repre_proxy_model.setSourceModel(repre_model) + repre_proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive) + repre_view.setModel(repre_proxy_model) + + for idx, width in enumerate(repre_model.column_widths): + repre_view.setColumnWidth(idx, width) + + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.addWidget(repre_view, 1) + + repre_view.customContextMenuRequested.connect( + self._on_context_menu) + repre_view.selectionModel().selectionChanged.connect( + self._on_selection_change) + repre_model.refreshed.connect(self._on_model_refresh) + + controller.register_event_callback( + "selection.project.changed", + self._on_project_change + ) + controller.register_event_callback( + "selection.folders.changed", + self._on_folder_change + ) + + self._controller = controller + self._selected_project_name = None + self._selected_multiple_folders = None + + self._repre_view = repre_view + self._repre_model = repre_model + self._repre_proxy_model = repre_proxy_model + + self._set_multiple_folders_selected(False) + + def refresh(self): + self._repre_model.refresh() + + def _on_folder_change(self, event): + self._set_multiple_folders_selected(len(event["folder_ids"]) > 1) + + def _on_project_change(self, event): + self._selected_project_name = event["project_name"] + + def _set_multiple_folders_selected(self, selected_multiple_folders): + if selected_multiple_folders == self._selected_multiple_folders: + return + self._selected_multiple_folders = selected_multiple_folders + self._repre_view.setColumnHidden( + self._repre_model.folder_column, + not self._selected_multiple_folders + ) + + def _on_model_refresh(self): + self._repre_proxy_model.sort(0) + + def _get_selected_repre_indexes(self): + selection_model = self._repre_view.selectionModel() + model = self._repre_view.model() + indexes_queue = collections.deque() + indexes_queue.extend(selection_model.selectedIndexes()) + + selected_indexes = [] + while indexes_queue: + index = indexes_queue.popleft() + if index.column() != 0: + continue + + group_type = model.data(index, GROUP_TYPE_ROLE) + if group_type is None: + selected_indexes.append(index) + + elif group_type == 0: + for row in range(model.rowCount(index)): + child_index = model.index(row, 0, index) + indexes_queue.append(child_index) + + return selected_indexes + + def _get_selected_repre_ids(self): + repre_ids = { + index.data(REPRESENTATION_ID_ROLE) + for index in self._get_selected_repre_indexes() + } + repre_ids.discard(None) + return repre_ids + + def _on_selection_change(self): + selected_repre_ids = self._get_selected_repre_ids() + self._controller.set_selected_representations(selected_repre_ids) + + def _on_context_menu(self, point): + repre_ids = self._get_selected_repre_ids() + action_items = self._controller.get_representations_action_items( + self._selected_project_name, repre_ids + ) + global_point = self._repre_view.mapToGlobal(point) + result = show_actions_menu( + action_items, + global_point, + len(repre_ids) == 1, + self + ) + action_item, options = result + if action_item is None or options is None: + return + + self._controller.trigger_action_item( + action_item.identifier, + options, + action_item.project_name, + version_ids=action_item.version_ids, + representation_ids=action_item.representation_ids, + ) diff --git a/openpype/tools/ayon_loader/ui/window.py b/openpype/tools/ayon_loader/ui/window.py new file mode 100644 index 0000000000..ca17e4b9fd --- /dev/null +++ b/openpype/tools/ayon_loader/ui/window.py @@ -0,0 +1,511 @@ +from qtpy import QtWidgets, QtCore, QtGui + +from openpype.resources import get_openpype_icon_filepath +from openpype.style import load_stylesheet +from openpype.tools.utils import ( + PlaceholderLineEdit, + ErrorMessageBox, + ThumbnailPainterWidget, + RefreshButton, + GoToCurrentButton, +) +from openpype.tools.utils.lib import center_window +from openpype.tools.ayon_utils.widgets import ProjectsCombobox +from openpype.tools.ayon_loader.control import LoaderController + +from .folders_widget import LoaderFoldersWidget +from .products_widget import ProductsWidget +from .product_types_widget import ProductTypesView +from .product_group_dialog import ProductGroupDialog +from .info_widget import InfoWidget +from .repres_widget import RepresentationsWidget + + +class LoadErrorMessageBox(ErrorMessageBox): + def __init__(self, messages, parent=None): + self._messages = messages + super(LoadErrorMessageBox, self).__init__("Loading failed", parent) + + def _create_top_widget(self, parent_widget): + label_widget = QtWidgets.QLabel(parent_widget) + label_widget.setText( + "Failed to load items" + ) + return label_widget + + def _get_report_data(self): + report_data = [] + for exc_msg, tb_text, repre, product, version in self._messages: + report_message = ( + "During load error happened on Product: \"{product}\"" + " Representation: \"{repre}\" Version: {version}" + "\n\nError message: {message}" + ).format( + product=product, + repre=repre, + version=version, + message=exc_msg + ) + if tb_text: + report_message += "\n\n{}".format(tb_text) + report_data.append(report_message) + return report_data + + def _create_content(self, content_layout): + item_name_template = ( + "Product: {}
" + "Version: {}
" + "Representation: {}
" + ) + exc_msg_template = "{}" + + for exc_msg, tb_text, repre, product, version in self._messages: + line = self._create_line() + content_layout.addWidget(line) + + item_name = item_name_template.format(product, version, repre) + item_name_widget = QtWidgets.QLabel( + item_name.replace("\n", "
"), self + ) + item_name_widget.setWordWrap(True) + content_layout.addWidget(item_name_widget) + + exc_msg = exc_msg_template.format(exc_msg.replace("\n", "
")) + message_label_widget = QtWidgets.QLabel(exc_msg, self) + message_label_widget.setWordWrap(True) + content_layout.addWidget(message_label_widget) + + if tb_text: + line = self._create_line() + tb_widget = self._create_traceback_widget(tb_text, self) + content_layout.addWidget(line) + content_layout.addWidget(tb_widget) + + +class RefreshHandler: + def __init__(self): + self._project_refreshed = False + self._folders_refreshed = False + self._products_refreshed = False + + @property + def project_refreshed(self): + return self._products_refreshed + + @property + def folders_refreshed(self): + return self._folders_refreshed + + @property + def products_refreshed(self): + return self._products_refreshed + + def reset(self): + self._project_refreshed = False + self._folders_refreshed = False + self._products_refreshed = False + + def set_project_refreshed(self): + self._project_refreshed = True + + def set_folders_refreshed(self): + self._folders_refreshed = True + + def set_products_refreshed(self): + self._products_refreshed = True + + +class LoaderWindow(QtWidgets.QWidget): + def __init__(self, controller=None, parent=None): + super(LoaderWindow, self).__init__(parent) + + icon = QtGui.QIcon(get_openpype_icon_filepath()) + self.setWindowIcon(icon) + self.setWindowTitle("AYON Loader") + self.setFocusPolicy(QtCore.Qt.StrongFocus) + self.setAttribute(QtCore.Qt.WA_DeleteOnClose, False) + self.setWindowFlags(self.windowFlags() | QtCore.Qt.Window) + + if controller is None: + controller = LoaderController() + + main_splitter = QtWidgets.QSplitter(self) + + context_splitter = QtWidgets.QSplitter(main_splitter) + context_splitter.setOrientation(QtCore.Qt.Vertical) + + # Context selection widget + context_widget = QtWidgets.QWidget(context_splitter) + + context_top_widget = QtWidgets.QWidget(context_widget) + projects_combobox = ProjectsCombobox( + controller, + context_top_widget, + handle_expected_selection=True + ) + projects_combobox.set_select_item_visible(True) + projects_combobox.set_libraries_separator_visible(True) + projects_combobox.set_standard_filter_enabled( + controller.is_standard_projects_filter_enabled() + ) + + go_to_current_btn = GoToCurrentButton(context_top_widget) + refresh_btn = RefreshButton(context_top_widget) + + context_top_layout = QtWidgets.QHBoxLayout(context_top_widget) + context_top_layout.setContentsMargins(0, 0, 0, 0,) + context_top_layout.addWidget(projects_combobox, 1) + context_top_layout.addWidget(go_to_current_btn, 0) + context_top_layout.addWidget(refresh_btn, 0) + + folders_filter_input = PlaceholderLineEdit(context_widget) + folders_filter_input.setPlaceholderText("Folder name filter...") + + folders_widget = LoaderFoldersWidget(controller, context_widget) + + product_types_widget = ProductTypesView(controller, context_splitter) + + context_layout = QtWidgets.QVBoxLayout(context_widget) + context_layout.setContentsMargins(0, 0, 0, 0) + context_layout.addWidget(context_top_widget, 0) + context_layout.addWidget(folders_filter_input, 0) + context_layout.addWidget(folders_widget, 1) + + context_splitter.addWidget(context_widget) + context_splitter.addWidget(product_types_widget) + context_splitter.setStretchFactor(0, 65) + context_splitter.setStretchFactor(1, 35) + + # Product + version selection item + products_wrap_widget = QtWidgets.QWidget(main_splitter) + + products_inputs_widget = QtWidgets.QWidget(products_wrap_widget) + + products_filter_input = PlaceholderLineEdit(products_inputs_widget) + products_filter_input.setPlaceholderText("Product name filter...") + product_group_checkbox = QtWidgets.QCheckBox( + "Enable grouping", products_inputs_widget) + product_group_checkbox.setChecked(True) + + products_widget = ProductsWidget(controller, products_wrap_widget) + + products_inputs_layout = QtWidgets.QHBoxLayout(products_inputs_widget) + products_inputs_layout.setContentsMargins(0, 0, 0, 0) + products_inputs_layout.addWidget(products_filter_input, 1) + products_inputs_layout.addWidget(product_group_checkbox, 0) + + products_wrap_layout = QtWidgets.QVBoxLayout(products_wrap_widget) + products_wrap_layout.setContentsMargins(0, 0, 0, 0) + products_wrap_layout.addWidget(products_inputs_widget, 0) + products_wrap_layout.addWidget(products_widget, 1) + + right_panel_splitter = QtWidgets.QSplitter(main_splitter) + right_panel_splitter.setOrientation(QtCore.Qt.Vertical) + + thumbnails_widget = ThumbnailPainterWidget(right_panel_splitter) + thumbnails_widget.set_use_checkboard(False) + + info_widget = InfoWidget(controller, right_panel_splitter) + + repre_widget = RepresentationsWidget(controller, right_panel_splitter) + + right_panel_splitter.addWidget(thumbnails_widget) + right_panel_splitter.addWidget(info_widget) + right_panel_splitter.addWidget(repre_widget) + + right_panel_splitter.setStretchFactor(0, 1) + right_panel_splitter.setStretchFactor(1, 1) + right_panel_splitter.setStretchFactor(2, 2) + + main_splitter.addWidget(context_splitter) + main_splitter.addWidget(products_wrap_widget) + main_splitter.addWidget(right_panel_splitter) + + main_splitter.setStretchFactor(0, 4) + main_splitter.setStretchFactor(1, 6) + main_splitter.setStretchFactor(2, 1) + + main_layout = QtWidgets.QHBoxLayout(self) + main_layout.addWidget(main_splitter) + + show_timer = QtCore.QTimer() + show_timer.setInterval(1) + + show_timer.timeout.connect(self._on_show_timer) + + projects_combobox.refreshed.connect(self._on_projects_refresh) + folders_widget.refreshed.connect(self._on_folders_refresh) + products_widget.refreshed.connect(self._on_products_refresh) + folders_filter_input.textChanged.connect( + self._on_folder_filter_change + ) + product_types_widget.filter_changed.connect( + self._on_product_type_filter_change + ) + products_filter_input.textChanged.connect( + self._on_product_filter_change + ) + product_group_checkbox.stateChanged.connect( + self._on_product_group_change + ) + products_widget.merged_products_selection_changed.connect( + self._on_merged_products_selection_change + ) + products_widget.selection_changed.connect( + self._on_products_selection_change + ) + go_to_current_btn.clicked.connect( + self._on_go_to_current_context_click + ) + refresh_btn.clicked.connect( + self._on_refresh_click + ) + controller.register_event_callback( + "load.finished", + self._on_load_finished, + ) + controller.register_event_callback( + "selection.project.changed", + self._on_project_selection_changed, + ) + controller.register_event_callback( + "selection.folders.changed", + self._on_folders_selection_changed, + ) + controller.register_event_callback( + "selection.versions.changed", + self._on_versions_selection_changed, + ) + controller.register_event_callback( + "controller.reset.started", + self._on_controller_reset_start, + ) + controller.register_event_callback( + "controller.reset.finished", + self._on_controller_reset_finish, + ) + + self._group_dialog = ProductGroupDialog(controller, self) + + self._main_splitter = main_splitter + + self._go_to_current_btn = go_to_current_btn + self._refresh_btn = refresh_btn + self._projects_combobox = projects_combobox + + self._folders_filter_input = folders_filter_input + self._folders_widget = folders_widget + + self._product_types_widget = product_types_widget + + self._products_filter_input = products_filter_input + self._product_group_checkbox = product_group_checkbox + self._products_widget = products_widget + + self._right_panel_splitter = right_panel_splitter + self._thumbnails_widget = thumbnails_widget + self._info_widget = info_widget + self._repre_widget = repre_widget + + self._controller = controller + self._refresh_handler = RefreshHandler() + self._first_show = True + self._reset_on_show = True + self._show_counter = 0 + self._show_timer = show_timer + self._selected_project_name = None + self._selected_folder_ids = set() + self._selected_version_ids = set() + + self._products_widget.set_enable_grouping( + self._product_group_checkbox.isChecked() + ) + + def refresh(self): + self._controller.reset() + + def showEvent(self, event): + super(LoaderWindow, self).showEvent(event) + + if self._first_show: + self._on_first_show() + + self._show_timer.start() + + def keyPressEvent(self, event): + modifiers = event.modifiers() + ctrl_pressed = QtCore.Qt.ControlModifier & modifiers + + # Grouping products on pressing Ctrl + G + if ( + ctrl_pressed + and event.key() == QtCore.Qt.Key_G + and not event.isAutoRepeat() + ): + self._show_group_dialog() + event.setAccepted(True) + return + + super(LoaderWindow, self).keyPressEvent(event) + + def _on_first_show(self): + self._first_show = False + # width, height = 1800, 900 + width, height = 1500, 750 + + self.resize(width, height) + + mid_width = int(width / 1.8) + sides_width = int((width - mid_width) * 0.5) + self._main_splitter.setSizes( + [sides_width, mid_width, sides_width] + ) + + thumbnail_height = int(height / 3.6) + info_height = int((height - thumbnail_height) * 0.5) + self._right_panel_splitter.setSizes( + [thumbnail_height, info_height, info_height] + ) + self.setStyleSheet(load_stylesheet()) + center_window(self) + + def _on_show_timer(self): + if self._show_counter < 2: + self._show_counter += 1 + return + + self._show_counter = 0 + self._show_timer.stop() + + if self._reset_on_show: + self._reset_on_show = False + self._controller.reset() + + def _show_group_dialog(self): + project_name = self._projects_combobox.get_current_project_name() + if not project_name: + return + + product_ids = { + i["product_id"] + for i in self._products_widget.get_selected_version_info() + } + if not product_ids: + return + + self._group_dialog.set_product_ids(project_name, product_ids) + self._group_dialog.show() + + def _on_folder_filter_change(self, text): + self._folders_widget.set_name_filer(text) + + def _on_product_group_change(self): + self._products_widget.set_enable_grouping( + self._product_group_checkbox.isChecked() + ) + + def _on_product_filter_change(self, text): + self._products_widget.set_name_filer(text) + + def _on_product_type_filter_change(self): + self._products_widget.set_product_type_filter( + self._product_types_widget.get_filter_info() + ) + + def _on_merged_products_selection_change(self): + items = self._products_widget.get_selected_merged_products() + self._folders_widget.set_merged_products_selection(items) + + def _on_products_selection_change(self): + items = self._products_widget.get_selected_version_info() + self._info_widget.set_selected_version_info( + self._projects_combobox.get_current_project_name(), + items + ) + + def _on_go_to_current_context_click(self): + context = self._controller.get_current_context() + self._controller.set_expected_selection( + context["project_name"], + context["folder_id"], + ) + + def _on_refresh_click(self): + self._controller.reset() + + def _on_controller_reset_start(self): + self._refresh_handler.reset() + + def _on_controller_reset_finish(self): + context = self._controller.get_current_context() + project_name = context["project_name"] + self._go_to_current_btn.setVisible(bool(project_name)) + self._projects_combobox.set_current_context_project(project_name) + if not self._refresh_handler.project_refreshed: + self._projects_combobox.refresh() + + def _on_load_finished(self, event): + error_info = event["error_info"] + if not error_info: + return + + box = LoadErrorMessageBox(error_info, self) + box.show() + + def _on_project_selection_changed(self, event): + self._selected_project_name = event["project_name"] + + def _on_folders_selection_changed(self, event): + self._selected_folder_ids = set(event["folder_ids"]) + self._update_thumbnails() + + def _on_versions_selection_changed(self, event): + self._selected_version_ids = set(event["version_ids"]) + self._update_thumbnails() + + def _update_thumbnails(self): + project_name = self._selected_project_name + thumbnail_ids = set() + if self._selected_version_ids: + thumbnail_id_by_entity_id = ( + self._controller.get_version_thumbnail_ids( + project_name, + self._selected_version_ids + ) + ) + thumbnail_ids = set(thumbnail_id_by_entity_id.values()) + elif self._selected_folder_ids: + thumbnail_id_by_entity_id = ( + self._controller.get_folder_thumbnail_ids( + project_name, + self._selected_folder_ids + ) + ) + thumbnail_ids = set(thumbnail_id_by_entity_id.values()) + + thumbnail_ids.discard(None) + + if not thumbnail_ids: + self._thumbnails_widget.set_current_thumbnails(None) + return + + thumbnail_paths = set() + for thumbnail_id in thumbnail_ids: + thumbnail_path = self._controller.get_thumbnail_path( + project_name, thumbnail_id) + thumbnail_paths.add(thumbnail_path) + thumbnail_paths.discard(None) + self._thumbnails_widget.set_current_thumbnail_paths(thumbnail_paths) + + def _on_projects_refresh(self): + self._refresh_handler.set_project_refreshed() + if not self._refresh_handler.folders_refreshed: + self._folders_widget.refresh() + + def _on_folders_refresh(self): + self._refresh_handler.set_folders_refreshed() + if not self._refresh_handler.products_refreshed: + self._products_widget.refresh() + + def _on_products_refresh(self): + self._refresh_handler.set_products_refreshed() diff --git a/openpype/tools/ayon_utils/models/__init__.py b/openpype/tools/ayon_utils/models/__init__.py index 1434282c5b..69722b5e21 100644 --- a/openpype/tools/ayon_utils/models/__init__.py +++ b/openpype/tools/ayon_utils/models/__init__.py @@ -12,6 +12,7 @@ from .hierarchy import ( HierarchyModel, HIERARCHY_MODEL_SENDER, ) +from .thumbnails import ThumbnailsModel __all__ = ( @@ -26,4 +27,6 @@ __all__ = ( "TaskItem", "HierarchyModel", "HIERARCHY_MODEL_SENDER", + + "ThumbnailsModel", ) diff --git a/openpype/tools/ayon_utils/models/hierarchy.py b/openpype/tools/ayon_utils/models/hierarchy.py index 93f4c48d98..6c30d22f3a 100644 --- a/openpype/tools/ayon_utils/models/hierarchy.py +++ b/openpype/tools/ayon_utils/models/hierarchy.py @@ -29,9 +29,8 @@ class FolderItem: 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. + label (Union[str, None]): Folder label. + icon (Union[dict[str, Any], None]): Icon definition. """ def __init__( @@ -240,23 +239,65 @@ class HierarchyModel(object): self._refresh_tasks_cache(project_name, folder_id, sender) return task_cache.get_data() + def get_folder_entities(self, project_name, folder_ids): + """Get folder entities by ids. + + Args: + project_name (str): Project name. + folder_ids (Iterable[str]): Folder ids. + + Returns: + dict[str, Any]: Folder entities by id. + """ + + output = {} + folder_ids = set(folder_ids) + if not project_name or not folder_ids: + return output + + folder_ids_to_query = set() + for folder_id in folder_ids: + cache = self._folders_by_id[project_name][folder_id] + if cache.is_valid: + output[folder_id] = cache.get_data() + elif folder_id: + folder_ids_to_query.add(folder_id) + else: + output[folder_id] = None + self._query_folder_entities(project_name, folder_ids_to_query) + for folder_id in folder_ids_to_query: + cache = self._folders_by_id[project_name][folder_id] + output[folder_id] = cache.get_data() + return output + def get_folder_entity(self, project_name, folder_id): - cache = self._folders_by_id[project_name][folder_id] - if not cache.is_valid: - entity = None - if folder_id: - entity = ayon_api.get_folder_by_id(project_name, folder_id) - cache.update_data(entity) - return cache.get_data() + output = self.get_folder_entities(project_name, {folder_id}) + return output[folder_id] + + def get_task_entities(self, project_name, task_ids): + output = {} + task_ids = set(task_ids) + if not project_name or not task_ids: + return output + + task_ids_to_query = set() + for task_id in task_ids: + cache = self._tasks_by_id[project_name][task_id] + if cache.is_valid: + output[task_id] = cache.get_data() + elif task_id: + task_ids_to_query.add(task_id) + else: + output[task_id] = None + self._query_task_entities(project_name, task_ids_to_query) + for task_id in task_ids_to_query: + cache = self._tasks_by_id[project_name][task_id] + output[task_id] = cache.get_data() + return output def get_task_entity(self, project_name, task_id): - cache = self._tasks_by_id[project_name][task_id] - if not cache.is_valid: - entity = None - if task_id: - entity = ayon_api.get_task_by_id(project_name, task_id) - cache.update_data(entity) - return cache.get_data() + output = self.get_task_entities(project_name, {task_id}) + return output[task_id] @contextlib.contextmanager def _folder_refresh_event_manager(self, project_name, sender): @@ -326,6 +367,25 @@ class HierarchyModel(object): hierachy_queue.extend(item["children"] or []) return folder_items + def _query_folder_entities(self, project_name, folder_ids): + if not project_name or not folder_ids: + return + project_cache = self._folders_by_id[project_name] + folders = ayon_api.get_folders(project_name, folder_ids=folder_ids) + for folder in folders: + folder_id = folder["id"] + project_cache[folder_id].update_data(folder) + + def _query_task_entities(self, project_name, task_ids): + if not project_name or not task_ids: + return + + project_cache = self._tasks_by_id[project_name] + tasks = ayon_api.get_tasks(project_name, task_ids=task_ids) + for task in tasks: + task_id = task["id"] + project_cache[task_id].update_data(task) + def _refresh_tasks_cache(self, project_name, folder_id, sender=None): if folder_id in self._tasks_refreshing: return diff --git a/openpype/tools/ayon_utils/models/projects.py b/openpype/tools/ayon_utils/models/projects.py index ae3eeecea4..4ad53fbbfa 100644 --- a/openpype/tools/ayon_utils/models/projects.py +++ b/openpype/tools/ayon_utils/models/projects.py @@ -29,13 +29,14 @@ class ProjectItem: is parent. """ - def __init__(self, name, active, icon=None): + def __init__(self, name, active, is_library, icon=None): self.name = name self.active = active + self.is_library = is_library if icon is None: icon = { "type": "awesome-font", - "name": "fa.map", + "name": "fa.book" if is_library else "fa.map", "color": get_default_entity_icon_color(), } self.icon = icon @@ -50,6 +51,7 @@ class ProjectItem: return { "name": self.name, "active": self.active, + "is_library": self.is_library, "icon": self.icon, } @@ -78,7 +80,7 @@ def _get_project_items_from_entitiy(projects): """ return [ - ProjectItem(project["name"], project["active"]) + ProjectItem(project["name"], project["active"], project["library"]) for project in projects ] @@ -141,5 +143,5 @@ class ProjectsModel(object): self._projects_cache.update_data(project_items) def _query_projects(self): - projects = ayon_api.get_projects(fields=["name", "active"]) + projects = ayon_api.get_projects(fields=["name", "active", "library"]) return _get_project_items_from_entitiy(projects) diff --git a/openpype/tools/ayon_utils/models/thumbnails.py b/openpype/tools/ayon_utils/models/thumbnails.py new file mode 100644 index 0000000000..40892338df --- /dev/null +++ b/openpype/tools/ayon_utils/models/thumbnails.py @@ -0,0 +1,118 @@ +import collections + +import ayon_api + +from openpype.client.server.thumbnails import AYONThumbnailCache + +from .cache import NestedCacheItem + + +class ThumbnailsModel: + entity_cache_lifetime = 240 # In seconds + + def __init__(self): + self._thumbnail_cache = AYONThumbnailCache() + self._paths_cache = collections.defaultdict(dict) + self._folders_cache = NestedCacheItem( + levels=2, lifetime=self.entity_cache_lifetime) + self._versions_cache = NestedCacheItem( + levels=2, lifetime=self.entity_cache_lifetime) + + def reset(self): + self._paths_cache = collections.defaultdict(dict) + self._folders_cache.reset() + self._versions_cache.reset() + + def get_thumbnail_path(self, project_name, thumbnail_id): + return self._get_thumbnail_path(project_name, thumbnail_id) + + def get_folder_thumbnail_ids(self, project_name, folder_ids): + project_cache = self._folders_cache[project_name] + output = {} + missing_cache = set() + for folder_id in folder_ids: + cache = project_cache[folder_id] + if cache.is_valid: + output[folder_id] = cache.get_data() + else: + missing_cache.add(folder_id) + self._query_folder_thumbnail_ids(project_name, missing_cache) + for folder_id in missing_cache: + cache = project_cache[folder_id] + output[folder_id] = cache.get_data() + return output + + def get_version_thumbnail_ids(self, project_name, version_ids): + project_cache = self._versions_cache[project_name] + output = {} + missing_cache = set() + for version_id in version_ids: + cache = project_cache[version_id] + if cache.is_valid: + output[version_id] = cache.get_data() + else: + missing_cache.add(version_id) + self._query_version_thumbnail_ids(project_name, missing_cache) + for version_id in missing_cache: + cache = project_cache[version_id] + output[version_id] = cache.get_data() + return output + + def _get_thumbnail_path(self, project_name, thumbnail_id): + if not thumbnail_id: + return None + + project_cache = self._paths_cache[project_name] + if thumbnail_id in project_cache: + return project_cache[thumbnail_id] + + filepath = self._thumbnail_cache.get_thumbnail_filepath( + project_name, thumbnail_id + ) + if filepath is not None: + project_cache[thumbnail_id] = filepath + return filepath + + # 'ayon_api' had a bug, public function + # 'get_thumbnail_by_id' did not return output of + # 'ServerAPI' method. + con = ayon_api.get_server_api_connection() + result = con.get_thumbnail_by_id(project_name, thumbnail_id) + if result is None: + pass + + elif result.is_valid: + filepath = self._thumbnail_cache.store_thumbnail( + project_name, + thumbnail_id, + result.content, + result.content_type + ) + project_cache[thumbnail_id] = filepath + return filepath + + def _query_folder_thumbnail_ids(self, project_name, folder_ids): + if not project_name or not folder_ids: + return + + folders = ayon_api.get_folders( + project_name, + folder_ids=folder_ids, + fields=["id", "thumbnailId"] + ) + project_cache = self._folders_cache[project_name] + for folder in folders: + project_cache[folder["id"]] = folder["thumbnailId"] + + def _query_version_thumbnail_ids(self, project_name, version_ids): + if not project_name or not version_ids: + return + + versions = ayon_api.get_versions( + project_name, + version_ids=version_ids, + fields=["id", "thumbnailId"] + ) + project_cache = self._versions_cache[project_name] + for version in versions: + project_cache[version["id"]] = version["thumbnailId"] diff --git a/openpype/tools/ayon_utils/widgets/__init__.py b/openpype/tools/ayon_utils/widgets/__init__.py index 59aef98faf..432a249a73 100644 --- a/openpype/tools/ayon_utils/widgets/__init__.py +++ b/openpype/tools/ayon_utils/widgets/__init__.py @@ -8,11 +8,13 @@ from .projects_widget import ( from .folders_widget import ( FoldersWidget, FoldersModel, + FOLDERS_MODEL_SENDER_NAME, ) from .tasks_widget import ( TasksWidget, TasksModel, + TASKS_MODEL_SENDER_NAME, ) from .utils import ( get_qt_icon, @@ -28,9 +30,11 @@ __all__ = ( "FoldersWidget", "FoldersModel", + "FOLDERS_MODEL_SENDER_NAME", "TasksWidget", "TasksModel", + "TASKS_MODEL_SENDER_NAME", "get_qt_icon", "RefreshThread", diff --git a/openpype/tools/ayon_utils/widgets/folders_widget.py b/openpype/tools/ayon_utils/widgets/folders_widget.py index 4f44881081..b57ffb126a 100644 --- a/openpype/tools/ayon_utils/widgets/folders_widget.py +++ b/openpype/tools/ayon_utils/widgets/folders_widget.py @@ -9,7 +9,7 @@ from openpype.tools.utils import ( from .utils import RefreshThread, get_qt_icon -SENDER_NAME = "qt_folders_model" +FOLDERS_MODEL_SENDER_NAME = "qt_folders_model" ITEM_ID_ROLE = QtCore.Qt.UserRole + 1 ITEM_NAME_ROLE = QtCore.Qt.UserRole + 2 @@ -112,7 +112,7 @@ class FoldersModel(QtGui.QStandardItemModel): project_name, self._controller.get_folder_items, project_name, - SENDER_NAME + FOLDERS_MODEL_SENDER_NAME ) self._current_refresh_thread = thread self._refresh_threads[thread.id] = thread @@ -142,6 +142,21 @@ class FoldersModel(QtGui.QStandardItemModel): self._fill_items(thread.get_result()) + def _fill_item_data(self, item, folder_item): + """ + + Args: + item (QtGui.QStandardItem): Item to fill data. + folder_item (FolderItem): Folder item. + """ + + icon = get_qt_icon(folder_item.icon) + item.setData(folder_item.entity_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) + + def _fill_items(self, folder_items_by_id): if not folder_items_by_id: if folder_items_by_id is not None: @@ -195,11 +210,7 @@ class FoldersModel(QtGui.QStandardItemModel): else: is_new = self._parent_id_by_id[item_id] != parent_id - icon = get_qt_icon(folder_item.icon) - 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) + self._fill_item_data(item, folder_item) if is_new: new_items.append(item) self._items_by_id[item_id] = item @@ -320,7 +331,7 @@ class FoldersWidget(QtWidgets.QWidget): self._folders_model.set_project_name(project_name) def _on_folders_refresh_finished(self, event): - if event["sender"] != SENDER_NAME: + if event["sender"] != FOLDERS_MODEL_SENDER_NAME: self._set_project_name(event["project_name"]) def _on_controller_refresh(self): diff --git a/openpype/tools/ayon_utils/widgets/projects_widget.py b/openpype/tools/ayon_utils/widgets/projects_widget.py index 818d574910..11bb5de51b 100644 --- a/openpype/tools/ayon_utils/widgets/projects_widget.py +++ b/openpype/tools/ayon_utils/widgets/projects_widget.py @@ -5,6 +5,9 @@ from .utils import RefreshThread, get_qt_icon PROJECT_NAME_ROLE = QtCore.Qt.UserRole + 1 PROJECT_IS_ACTIVE_ROLE = QtCore.Qt.UserRole + 2 +PROJECT_IS_LIBRARY_ROLE = QtCore.Qt.UserRole + 3 +PROJECT_IS_CURRENT_ROLE = QtCore.Qt.UserRole + 4 +LIBRARY_PROJECT_SEPARATOR_ROLE = QtCore.Qt.UserRole + 5 class ProjectsModel(QtGui.QStandardItemModel): @@ -15,10 +18,23 @@ class ProjectsModel(QtGui.QStandardItemModel): self._controller = controller self._project_items = {} + self._has_libraries = False self._empty_item = None self._empty_item_added = False + self._select_item = None + self._select_item_added = False + self._select_item_visible = None + + self._libraries_sep_item = None + self._libraries_sep_item_added = False + self._libraries_sep_item_visible = False + + self._current_context_project = None + + self._selected_project = None + self._is_refreshing = False self._refresh_thread = None @@ -32,21 +48,63 @@ class ProjectsModel(QtGui.QStandardItemModel): def has_content(self): return len(self._project_items) > 0 + def set_select_item_visible(self, visible): + if self._select_item_visible is visible: + return + self._select_item_visible = visible + + if self._selected_project is None: + self._add_select_item() + + def set_libraries_separator_visible(self, visible): + if self._libraries_sep_item_visible is visible: + return + self._libraries_sep_item_visible = visible + + def set_selected_project(self, project_name): + if not self._select_item_visible: + return + + self._selected_project = project_name + if project_name is None: + self._add_select_item() + else: + self._remove_select_item() + + def set_current_context_project(self, project_name): + if project_name == self._current_context_project: + return + self._unset_current_context_project(self._current_context_project) + self._current_context_project = project_name + self._set_current_context_project(project_name) + + def _set_current_context_project(self, project_name): + item = self._project_items.get(project_name) + if item is None: + return + item.setData(True, PROJECT_IS_CURRENT_ROLE) + + def _unset_current_context_project(self, project_name): + item = self._project_items.get(project_name) + if item is None: + return + item.setData(False, PROJECT_IS_CURRENT_ROLE) + def _add_empty_item(self): + if self._empty_item_added: + return + self._empty_item_added = True item = self._get_empty_item() - if not self._empty_item_added: - root_item = self.invisibleRootItem() - root_item.appendRow(item) - self._empty_item_added = True + root_item = self.invisibleRootItem() + root_item.appendRow(item) def _remove_empty_item(self): if not self._empty_item_added: return - + self._empty_item_added = False root_item = self.invisibleRootItem() item = self._get_empty_item() root_item.takeRow(item.row()) - self._empty_item_added = False def _get_empty_item(self): if self._empty_item is None: @@ -55,6 +113,61 @@ class ProjectsModel(QtGui.QStandardItemModel): self._empty_item = item return self._empty_item + def _get_library_sep_item(self): + if self._libraries_sep_item is not None: + return self._libraries_sep_item + + item = QtGui.QStandardItem() + item.setData("Libraries", QtCore.Qt.DisplayRole) + item.setData(True, LIBRARY_PROJECT_SEPARATOR_ROLE) + item.setFlags(QtCore.Qt.NoItemFlags) + self._libraries_sep_item = item + return item + + def _add_library_sep_item(self): + if ( + not self._libraries_sep_item_visible + or self._libraries_sep_item_added + ): + return + self._libraries_sep_item_added = True + item = self._get_library_sep_item() + root_item = self.invisibleRootItem() + root_item.appendRow(item) + + def _remove_library_sep_item(self): + if ( + not self._libraries_sep_item_added + ): + return + self._libraries_sep_item_added = False + item = self._get_library_sep_item() + root_item = self.invisibleRootItem() + root_item.takeRow(item.row()) + + def _add_select_item(self): + if self._select_item_added: + return + self._select_item_added = True + item = self._get_select_item() + root_item = self.invisibleRootItem() + root_item.appendRow(item) + + def _remove_select_item(self): + if not self._select_item_added: + return + self._select_item_added = False + root_item = self.invisibleRootItem() + item = self._get_select_item() + root_item.takeRow(item.row()) + + def _get_select_item(self): + if self._select_item is None: + item = QtGui.QStandardItem("< Select project >") + item.setEditable(False) + self._select_item = item + return self._select_item + def _refresh(self): if self._is_refreshing: return @@ -80,44 +193,118 @@ class ProjectsModel(QtGui.QStandardItemModel): self.refreshed.emit() def _fill_items(self, project_items): - items_to_remove = set(self._project_items.keys()) + new_project_names = { + project_item.name + for project_item in project_items + } + + # Handle "Select item" visibility + if self._select_item_visible: + # Add select project. if previously selected project is not in + # project items + if self._selected_project not in new_project_names: + self._add_select_item() + else: + self._remove_select_item() + + root_item = self.invisibleRootItem() + + items_to_remove = set(self._project_items.keys()) - new_project_names + for project_name in items_to_remove: + item = self._project_items.pop(project_name) + root_item.takeRow(item.row()) + + has_library_project = False new_items = [] for project_item in project_items: project_name = project_item.name - items_to_remove.discard(project_name) item = self._project_items.get(project_name) + if project_item.is_library: + has_library_project = True if item is None: item = QtGui.QStandardItem() + item.setEditable(False) new_items.append(item) icon = get_qt_icon(project_item.icon) item.setData(project_name, QtCore.Qt.DisplayRole) item.setData(icon, QtCore.Qt.DecorationRole) item.setData(project_name, PROJECT_NAME_ROLE) item.setData(project_item.active, PROJECT_IS_ACTIVE_ROLE) + item.setData(project_item.is_library, PROJECT_IS_LIBRARY_ROLE) + is_current = project_name == self._current_context_project + item.setData(is_current, PROJECT_IS_CURRENT_ROLE) self._project_items[project_name] = item - root_item = self.invisibleRootItem() + self._set_current_context_project(self._current_context_project) + + self._has_libraries = has_library_project + if new_items: root_item.appendRows(new_items) - for project_name in items_to_remove: - item = self._project_items.pop(project_name) - root_item.removeRow(item.row()) - if self.has_content(): + # Make sure "No projects" item is removed self._remove_empty_item() + if has_library_project: + self._add_library_sep_item() + else: + self._remove_library_sep_item() else: + # Keep only "No projects" item self._add_empty_item() + self._remove_select_item() + self._remove_library_sep_item() class ProjectSortFilterProxy(QtCore.QSortFilterProxyModel): def __init__(self, *args, **kwargs): super(ProjectSortFilterProxy, self).__init__(*args, **kwargs) self._filter_inactive = True + self._filter_standard = False + self._filter_library = False + self._sort_by_type = True # Disable case sensitivity self.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive) + def _type_sort(self, l_index, r_index): + if not self._sort_by_type: + return None + + l_is_library = l_index.data(PROJECT_IS_LIBRARY_ROLE) + r_is_library = r_index.data(PROJECT_IS_LIBRARY_ROLE) + # Both hare project items + if l_is_library is not None and r_is_library is not None: + if l_is_library is r_is_library: + return None + if l_is_library: + return False + return True + + if l_index.data(LIBRARY_PROJECT_SEPARATOR_ROLE): + if r_is_library is None: + return False + return r_is_library + + if r_index.data(LIBRARY_PROJECT_SEPARATOR_ROLE): + if l_is_library is None: + return True + return l_is_library + return None + def lessThan(self, left_index, right_index): + # Current project always on top + # - make sure this is always first, before any other sorting + # e.g. type sort would move the item lower + if left_index.data(PROJECT_IS_CURRENT_ROLE): + return True + if right_index.data(PROJECT_IS_CURRENT_ROLE): + return False + + # Library separator should be before library projects + result = self._type_sort(left_index, right_index) + if result is not None: + return result + if left_index.data(PROJECT_NAME_ROLE) is None: return True @@ -137,21 +324,43 @@ class ProjectSortFilterProxy(QtCore.QSortFilterProxyModel): def filterAcceptsRow(self, source_row, source_parent): index = self.sourceModel().index(source_row, 0, source_parent) + project_name = index.data(PROJECT_NAME_ROLE) + if project_name is None: + return True + string_pattern = self.filterRegularExpression().pattern() + if string_pattern: + return string_pattern.lower() in project_name.lower() + + # Current project keep always visible + default = super(ProjectSortFilterProxy, self).filterAcceptsRow( + source_row, source_parent + ) + if not default: + return default + + # Make sure current project is visible + if index.data(PROJECT_IS_CURRENT_ROLE): + return True + if ( self._filter_inactive and not index.data(PROJECT_IS_ACTIVE_ROLE) ): return False - if string_pattern: - project_name = index.data(PROJECT_IS_ACTIVE_ROLE) - if project_name is not None: - return string_pattern.lower() in project_name.lower() + if ( + self._filter_standard + and not index.data(PROJECT_IS_LIBRARY_ROLE) + ): + return False - return super(ProjectSortFilterProxy, self).filterAcceptsRow( - source_row, source_parent - ) + if ( + self._filter_library + and index.data(PROJECT_IS_LIBRARY_ROLE) + ): + return False + return True def _custom_index_filter(self, index): return bool(index.data(PROJECT_IS_ACTIVE_ROLE)) @@ -159,14 +368,34 @@ class ProjectSortFilterProxy(QtCore.QSortFilterProxyModel): def is_active_filter_enabled(self): return self._filter_inactive - def set_active_filter_enabled(self, value): - if self._filter_inactive == value: + def set_active_filter_enabled(self, enabled): + if self._filter_inactive == enabled: return - self._filter_inactive = value + self._filter_inactive = enabled self.invalidateFilter() + def set_library_filter_enabled(self, enabled): + if self._filter_library == enabled: + return + self._filter_library = enabled + self.invalidateFilter() + + def set_standard_filter_enabled(self, enabled): + if self._filter_standard == enabled: + return + self._filter_standard = enabled + self.invalidateFilter() + + def set_sort_by_type(self, enabled): + if self._sort_by_type is enabled: + return + self._sort_by_type = enabled + self.invalidate() + class ProjectsCombobox(QtWidgets.QWidget): + refreshed = QtCore.Signal() + def __init__(self, controller, parent, handle_expected_selection=False): super(ProjectsCombobox, self).__init__(parent) @@ -203,6 +432,7 @@ class ProjectsCombobox(QtWidgets.QWidget): self._controller = controller self._listen_selection_change = True + self._select_item_visible = False self._handle_expected_selection = handle_expected_selection self._expected_selection = None @@ -264,17 +494,56 @@ class ProjectsCombobox(QtWidgets.QWidget): return None return self._projects_combobox.itemData(idx, PROJECT_NAME_ROLE) + def set_current_context_project(self, project_name): + self._projects_model.set_current_context_project(project_name) + self._projects_proxy_model.invalidateFilter() + + def _update_select_item_visiblity(self, **kwargs): + if not self._select_item_visible: + return + if "project_name" not in kwargs: + project_name = self.get_current_project_name() + else: + project_name = kwargs.get("project_name") + + # Hide the item if a project is selected + self._projects_model.set_selected_project(project_name) + + def set_select_item_visible(self, visible): + self._select_item_visible = visible + self._projects_model.set_select_item_visible(visible) + self._update_select_item_visiblity() + + def set_libraries_separator_visible(self, visible): + self._projects_model.set_libraries_separator_visible(visible) + + def is_active_filter_enabled(self): + return self._projects_proxy_model.is_active_filter_enabled() + + def set_active_filter_enabled(self, enabled): + return self._projects_proxy_model.set_active_filter_enabled(enabled) + + def set_standard_filter_enabled(self, enabled): + return self._projects_proxy_model.set_standard_filter_enabled(enabled) + + def set_library_filter_enabled(self, enabled): + return self._projects_proxy_model.set_library_filter_enabled(enabled) + def _on_current_index_changed(self, idx): if not self._listen_selection_change: return project_name = self._projects_combobox.itemData( idx, PROJECT_NAME_ROLE) + self._update_select_item_visiblity(project_name=project_name) self._controller.set_selected_project(project_name) def _on_model_refresh(self): self._projects_proxy_model.sort(0) + self._projects_proxy_model.invalidateFilter() if self._expected_selection: self._set_expected_selection() + self._update_select_item_visiblity() + self.refreshed.emit() def _on_projects_refresh_finished(self, event): if event["sender"] != PROJECTS_MODEL_SENDER: diff --git a/openpype/tools/ayon_utils/widgets/tasks_widget.py b/openpype/tools/ayon_utils/widgets/tasks_widget.py index 0af506863a..da745bd810 100644 --- a/openpype/tools/ayon_utils/widgets/tasks_widget.py +++ b/openpype/tools/ayon_utils/widgets/tasks_widget.py @@ -5,7 +5,7 @@ from openpype.tools.utils import DeselectableTreeView from .utils import RefreshThread, get_qt_icon -SENDER_NAME = "qt_tasks_model" +TASKS_MODEL_SENDER_NAME = "qt_tasks_model" ITEM_ID_ROLE = QtCore.Qt.UserRole + 1 PARENT_ID_ROLE = QtCore.Qt.UserRole + 2 ITEM_NAME_ROLE = QtCore.Qt.UserRole + 3 @@ -362,7 +362,7 @@ class TasksWidget(QtWidgets.QWidget): # Refresh only if current folder id is the same if ( - event["sender"] == SENDER_NAME + event["sender"] == TASKS_MODEL_SENDER_NAME or event["folder_id"] != self._selected_folder_id ): return diff --git a/openpype/tools/utils/__init__.py b/openpype/tools/utils/__init__.py index 018088e916..ed41d93f0d 100644 --- a/openpype/tools/utils/__init__.py +++ b/openpype/tools/utils/__init__.py @@ -43,6 +43,7 @@ from .overlay_messages import ( MessageOverlayObject, ) from .multiselection_combobox import MultiSelectionComboBox +from .thumbnail_paint_widget import ThumbnailPainterWidget __all__ = ( @@ -90,4 +91,6 @@ __all__ = ( "MessageOverlayObject", "MultiSelectionComboBox", + + "ThumbnailPainterWidget", ) diff --git a/openpype/tools/utils/host_tools.py b/openpype/tools/utils/host_tools.py index 2ebc973a47..ca23945339 100644 --- a/openpype/tools/utils/host_tools.py +++ b/openpype/tools/utils/host_tools.py @@ -86,12 +86,22 @@ class HostToolsHelper: def get_loader_tool(self, parent): """Create, cache and return loader tool window.""" if self._loader_tool is None: - from openpype.tools.loader import LoaderWindow - host = registered_host() ILoadHost.validate_load_methods(host) + if AYON_SERVER_ENABLED: + from openpype.tools.ayon_loader.ui import LoaderWindow + from openpype.tools.ayon_loader import LoaderController - loader_window = LoaderWindow(parent=parent or self._parent) + controller = LoaderController(host=host) + loader_window = LoaderWindow( + controller=controller, + parent=parent or self._parent + ) + + else: + from openpype.tools.loader import LoaderWindow + + loader_window = LoaderWindow(parent=parent or self._parent) self._loader_tool = loader_window return self._loader_tool @@ -109,7 +119,7 @@ class HostToolsHelper: if use_context is None: use_context = False - if use_context: + if not AYON_SERVER_ENABLED and use_context: context = {"asset": get_current_asset_name()} loader_tool.set_context(context, refresh=True) else: @@ -187,6 +197,9 @@ class HostToolsHelper: def get_library_loader_tool(self, parent): """Create, cache and return library loader tool window.""" + if AYON_SERVER_ENABLED: + return self.get_loader_tool(parent) + if self._library_loader_tool is None: from openpype.tools.libraryloader import LibraryLoaderWindow @@ -199,6 +212,9 @@ class HostToolsHelper: def show_library_loader(self, parent=None): """Loader tool for loading representations from library project.""" + if AYON_SERVER_ENABLED: + return self.show_loader(parent) + with qt_app_context(): library_loader_tool = self.get_library_loader_tool(parent) library_loader_tool.show() diff --git a/openpype/tools/utils/images/__init__.py b/openpype/tools/utils/images/__init__.py new file mode 100644 index 0000000000..3f437fcc8c --- /dev/null +++ b/openpype/tools/utils/images/__init__.py @@ -0,0 +1,56 @@ +import os +from qtpy import QtGui + +IMAGES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__))) + + +def get_image_path(filename): + """Get image path from './images'. + + Returns: + Union[str, None]: Path to image file or None if not found. + """ + + path = os.path.join(IMAGES_DIR, filename) + if os.path.exists(path): + return path + return None + + +def get_image(filename): + """Load image from './images' as QImage. + + Returns: + Union[QtGui.QImage, None]: QImage or None if not found. + """ + + path = get_image_path(filename) + if path: + return QtGui.QImage(path) + return None + + +def get_pixmap(filename): + """Load image from './images' as QPixmap. + + Returns: + Union[QtGui.QPixmap, None]: QPixmap or None if not found. + """ + + path = get_image_path(filename) + if path: + return QtGui.QPixmap(path) + return None + + +def get_icon(filename): + """Load image from './images' as QIcon. + + Returns: + Union[QtGui.QIcon, None]: QIcon or None if not found. + """ + + pix = get_pixmap(filename) + if pix: + return QtGui.QIcon(pix) + return None diff --git a/openpype/tools/utils/images/thumbnail.png b/openpype/tools/utils/images/thumbnail.png new file mode 100644 index 0000000000000000000000000000000000000000..adea862e5b7169f8279bd398226a2fe6283e4925 GIT binary patch literal 5118 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 100: + width = int(width * 0.6) + height = int(height * 0.6) + + scaled_pix = self._get_default_pix().scaled( + width, + height, + QtCore.Qt.KeepAspectRatio, + QtCore.Qt.SmoothTransformation + ) + pos_x = int( + (pix_width - scaled_pix.width()) / 2 + ) + pos_y = int( + (pix_height - scaled_pix.height()) / 2 + ) + new_pix = QtGui.QPixmap(pix_width, pix_height) + new_pix.fill(QtCore.Qt.transparent) + pix_painter = QtGui.QPainter() + pix_painter.begin(new_pix) + render_hints = ( + QtGui.QPainter.Antialiasing + | QtGui.QPainter.SmoothPixmapTransform + ) + if hasattr(QtGui.QPainter, "HighQualityAntialiasing"): + render_hints |= QtGui.QPainter.HighQualityAntialiasing + + pix_painter.setRenderHints(render_hints) + pix_painter.drawPixmap(pos_x, pos_y, scaled_pix) + pix_painter.end() + return new_pix + + def _draw_thumbnails(self, thumbnails, pix_width, pix_height): + full_border_width = 2 * self.border_width + + checker_pix = self._paint_tile(pix_width, pix_height) + + backgrounded_images = [] + for src_pix in thumbnails: + scaled_pix = src_pix.scaled( + pix_width - full_border_width, + pix_height - full_border_width, + QtCore.Qt.KeepAspectRatio, + QtCore.Qt.SmoothTransformation + ) + pos_x = int( + (pix_width - scaled_pix.width()) / 2 + ) + pos_y = int( + (pix_height - scaled_pix.height()) / 2 + ) + + new_pix = QtGui.QPixmap(pix_width, pix_height) + new_pix.fill(QtCore.Qt.transparent) + pix_painter = QtGui.QPainter() + pix_painter.begin(new_pix) + render_hints = ( + QtGui.QPainter.Antialiasing + | QtGui.QPainter.SmoothPixmapTransform + ) + if hasattr(QtGui.QPainter, "HighQualityAntialiasing"): + render_hints |= QtGui.QPainter.HighQualityAntialiasing + pix_painter.setRenderHints(render_hints) + + tiled_rect = QtCore.QRectF( + pos_x, pos_y, scaled_pix.width(), scaled_pix.height() + ) + pix_painter.drawTiledPixmap( + tiled_rect, + checker_pix, + QtCore.QPointF(0.0, 0.0) + ) + pix_painter.drawPixmap(pos_x, pos_y, scaled_pix) + pix_painter.end() + backgrounded_images.append(new_pix) + return backgrounded_images + + def _paint_dash_line(self, painter, rect): + pen = QtGui.QPen() + pen.setWidth(1) + pen.setBrush(QtCore.Qt.darkGray) + pen.setStyle(QtCore.Qt.DashLine) + + new_rect = rect.adjusted(1, 1, -1, -1) + painter.setPen(pen) + painter.setBrush(QtCore.Qt.transparent) + # painter.drawRect(rect) + painter.drawRect(new_rect) + + def _cache_pix(self): + rect = self.rect() + rect_width = rect.width() + rect_height = rect.height() + + pix_x_offset = 0 + pix_y_offset = 0 + expected_height = int( + (rect_width / self.width_ratio) * self.height_ratio + ) + if expected_height > rect_height: + expected_height = rect_height + expected_width = int( + (rect_height / self.height_ratio) * self.width_ratio + ) + pix_x_offset = (rect_width - expected_width) / 2 + else: + expected_width = rect_width + pix_y_offset = (rect_height - expected_height) / 2 + + if self._current_pixes is None: + used_default_pix = True + pixes_to_draw = None + pixes_len = 1 + else: + used_default_pix = False + pixes_to_draw = self._current_pixes + if len(pixes_to_draw) > self.max_thumbnails: + pixes_to_draw = pixes_to_draw[:-self.max_thumbnails] + pixes_len = len(pixes_to_draw) + + width_offset, height_offset = self._get_pix_offset_size( + expected_width, expected_height, pixes_len + ) + pix_width = expected_width - width_offset + pix_height = expected_height - height_offset + + if used_default_pix: + thumbnail_images = [self._paint_default_pix(pix_width, pix_height)] + else: + thumbnail_images = self._draw_thumbnails( + pixes_to_draw, pix_width, pix_height + ) + + if pixes_len == 1: + width_offset_part = 0 + height_offset_part = 0 + else: + width_offset_part = int(float(width_offset) / (pixes_len - 1)) + height_offset_part = int(float(height_offset) / (pixes_len - 1)) + full_width_offset = width_offset + pix_x_offset + + final_pix = QtGui.QPixmap(rect_width, rect_height) + final_pix.fill(QtCore.Qt.transparent) + + bg_pen = QtGui.QPen() + bg_pen.setWidth(self.border_width) + bg_pen.setColor(self._border_color) + + final_painter = QtGui.QPainter() + final_painter.begin(final_pix) + render_hints = ( + QtGui.QPainter.Antialiasing + | QtGui.QPainter.SmoothPixmapTransform + ) + if hasattr(QtGui.QPainter, "HighQualityAntialiasing"): + render_hints |= QtGui.QPainter.HighQualityAntialiasing + + final_painter.setRenderHints(render_hints) + + final_painter.setBrush(QtGui.QBrush(self._thumbnail_bg_color)) + final_painter.setPen(bg_pen) + final_painter.drawRect(rect) + + for idx, pix in enumerate(thumbnail_images): + x_offset = full_width_offset - (width_offset_part * idx) + y_offset = (height_offset_part * idx) + pix_y_offset + final_painter.drawPixmap(x_offset, y_offset, pix) + + # Draw drop enabled dashes + if used_default_pix: + self._paint_dash_line(final_painter, rect) + + final_painter.end() + + self._cached_pix = final_pix + + def _get_pix_offset_size(self, width, height, image_count): + if image_count == 1: + return 0, 0 + + part_width = width / self.offset_sep + part_height = height / self.offset_sep + return part_width, part_height From 45b61c21711b92c5a59e73b1125c2da2696d62de Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 11 Oct 2023 23:12:42 +0300 Subject: [PATCH 0265/1233] update create and publish plugins --- .../houdini/server/settings/create.py | 146 +++++++++++------- .../houdini/server/settings/publish.py | 68 ++++---- 2 files changed, 124 insertions(+), 90 deletions(-) diff --git a/server_addon/houdini/server/settings/create.py b/server_addon/houdini/server/settings/create.py index a1f8d24c30..81b871e83f 100644 --- a/server_addon/houdini/server/settings/create.py +++ b/server_addon/houdini/server/settings/create.py @@ -34,52 +34,110 @@ class CreateStaticMeshModel(BaseSettingsModel): class CreatePluginsModel(BaseSettingsModel): - CreateArnoldAss: CreateArnoldAssModel = Field( - default_factory=CreateArnoldAssModel, - title="Create Arnold Ass") - # "-" is not compatible in the new model - CreateStaticMesh: CreateStaticMeshModel = Field( - default_factory=CreateStaticMeshModel, - title="Create Static Mesh" - ) CreateAlembicCamera: CreatorModel = Field( default_factory=CreatorModel, title="Create Alembic Camera") + CreateArnoldAss: CreateArnoldAssModel = Field( + default_factory=CreateArnoldAssModel, + title="Create Arnold Ass") + CreateArnoldRop: CreatorModel = Field( + default_factory=CreatorModel, + title="Create Arnold ROP") CreateCompositeSequence: CreatorModel = Field( default_factory=CreatorModel, - title="Create Composite Sequence") + title="Create Composite (Image Sequence)") + CreateHDA: CreatorModel = Field( + default_factory=CreatorModel, + title="Create Houdini Digital Asset") + CreateKarmaROP: CreatorModel = Field( + default_factory=CreatorModel, + title="Create Karma ROP") + CreateMantraROP: CreatorModel = Field( + default_factory=CreatorModel, + title="Create Mantra ROP") CreatePointCache: CreatorModel = Field( default_factory=CreatorModel, - title="Create Point Cache") + title="Create PointCache (Abc)") + CreateBGEO: CreatorModel = Field( + default_factory=CreatorModel, + title="Create PointCache (Bgeo)") + CreateRedshiftProxy: CreatorModel = Field( + default_factory=CreatorModel, + title="Create Redshift Proxy") CreateRedshiftROP: CreatorModel = Field( default_factory=CreatorModel, - title="Create RedshiftROP") - CreateRemotePublish: CreatorModel = Field( + title="Create Redshift ROP") + CreateReview: CreatorModel = Field( default_factory=CreatorModel, - title="Create Remote Publish") + title="Create Review") + # "-" is not compatible in the new model + CreateStaticMesh: CreateStaticMeshModel = Field( + default_factory=CreateStaticMeshModel, + title="Create Static Mesh") + CreateUSD: CreatorModel = Field( + default_factory=CreatorModel, + title="Create USD (experimental)") + CreateUSDRender: CreatorModel = Field( + default_factory=CreatorModel, + title="Create USD render (experimental)") CreateVDBCache: CreatorModel = Field( default_factory=CreatorModel, title="Create VDB Cache") - CreateUSD: CreatorModel = Field( + CreateVrayROP: CreatorModel = Field( default_factory=CreatorModel, - title="Create USD") - CreateUSDModel: CreatorModel = Field( - default_factory=CreatorModel, - title="Create USD model") - USDCreateShadingWorkspace: CreatorModel = Field( - default_factory=CreatorModel, - title="Create USD shading workspace") - CreateUSDRender: CreatorModel = Field( - default_factory=CreatorModel, - title="Create USD render") + title="Create VRay ROP") DEFAULT_HOUDINI_CREATE_SETTINGS = { + "CreateAlembicCamera": { + "enabled": True, + "default_variants": ["Main"] + }, "CreateArnoldAss": { "enabled": True, "default_variants": ["Main"], "ext": ".ass" }, + "CreateArnoldRop": { + "enabled": True, + "default_variants": ["Main"] + }, + "CreateCompositeSequence": { + "enabled": True, + "default_variants": ["Main"] + }, + "CreateHDA": { + "enabled": True, + "default_variants": ["Main"] + }, + "CreateKarmaROP": { + "enabled": True, + "default_variants": ["Main"] + }, + "CreateMantraROP": { + "enabled": True, + "default_variants": ["Main"] + }, + "CreatePointCache": { + "enabled": True, + "default_variants": ["Main"] + }, + "CreateBGEO": { + "enabled": True, + "default_variants": ["Main"] + }, + "CreateRedshiftProxy": { + "enabled": True, + "default_variants": ["Main"] + }, + "CreateRedshiftROP": { + "enabled": True, + "default_variants": ["Main"] + }, + "CreateReview": { + "enabled": True, + "default_variants": ["Main"] + }, "CreateStaticMesh": { "enabled": True, "default_variants": [ @@ -93,44 +151,20 @@ DEFAULT_HOUDINI_CREATE_SETTINGS = { "UCX" ] }, - "CreateAlembicCamera": { - "enabled": True, - "default_variants": ["Main"] - }, - "CreateCompositeSequence": { - "enabled": True, - "default_variants": ["Main"] - }, - "CreatePointCache": { - "enabled": True, - "default_variants": ["Main"] - }, - "CreateRedshiftROP": { - "enabled": True, - "default_variants": ["Main"] - }, - "CreateRemotePublish": { - "enabled": True, - "default_variants": ["Main"] - }, - "CreateVDBCache": { - "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"] }, + "CreateVDBCache": { + "enabled": True, + "default_variants": ["Main"] + }, + "CreateVrayROP": { + "enabled": True, + "default_variants": ["Main"] + }, } diff --git a/server_addon/houdini/server/settings/publish.py b/server_addon/houdini/server/settings/publish.py index 7612c446bf..5a1ee1fa07 100644 --- a/server_addon/houdini/server/settings/publish.py +++ b/server_addon/houdini/server/settings/publish.py @@ -23,27 +23,52 @@ class BasicValidateModel(BaseSettingsModel): class PublishPluginsModel(BaseSettingsModel): - ValidateWorkfilePaths: ValidateWorkfilePathsModel = Field( - default_factory=ValidateWorkfilePathsModel, - title="Validate workfile paths settings.") - ValidateReviewColorspace: BasicValidateModel = Field( - default_factory=BasicValidateModel, - title="Validate Review Colorspace.") ValidateContainers: BasicValidateModel = Field( default_factory=BasicValidateModel, title="Validate Latest Containers.") - ValidateSubsetName: BasicValidateModel = Field( - default_factory=BasicValidateModel, - title="Validate Subset Name.") ValidateMeshIsStatic: BasicValidateModel = Field( default_factory=BasicValidateModel, title="Validate Mesh is Static.") + ValidateReviewColorspace: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Review Colorspace.") + ValidateSubsetName: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Subset Name.") ValidateUnrealStaticMeshName: BasicValidateModel = Field( default_factory=BasicValidateModel, title="Validate Unreal Static Mesh Name.") + ValidateWorkfilePaths: ValidateWorkfilePathsModel = Field( + default_factory=ValidateWorkfilePathsModel, + title="Validate workfile paths settings.") DEFAULT_HOUDINI_PUBLISH_SETTINGS = { + "ValidateContainers": { + "enabled": True, + "optional": True, + "active": True + }, + "ValidateMeshIsStatic": { + "enabled": True, + "optional": True, + "active": True + }, + "ValidateReviewColorspace": { + "enabled": True, + "optional": True, + "active": True + }, + "ValidateSubsetName": { + "enabled": True, + "optional": True, + "active": True + }, + "ValidateUnrealStaticMeshName": { + "enabled": False, + "optional": True, + "active": True + }, "ValidateWorkfilePaths": { "enabled": True, "optional": True, @@ -55,30 +80,5 @@ DEFAULT_HOUDINI_PUBLISH_SETTINGS = { "$HIP", "$JOB" ] - }, - "ValidateReviewColorspace": { - "enabled": True, - "optional": True, - "active": True - }, - "ValidateContainers": { - "enabled": True, - "optional": True, - "active": True - }, - "ValidateSubsetName": { - "enabled": True, - "optional": True, - "active": True - }, - "ValidateMeshIsStatic": { - "enabled": True, - "optional": True, - "active": True - }, - "ValidateUnrealStaticMeshName": { - "enabled": False, - "optional": True, - "active": True } } From dc28a8d3d285b41556fa9185268e1c3df22b8f51 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 11 Oct 2023 22:28:31 +0200 Subject: [PATCH 0266/1233] adding backward compatibility apply_settings --- .../plugins/publish/validate_asset_context.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/validate_asset_context.py b/openpype/hosts/nuke/plugins/publish/validate_asset_context.py index 2a7b7a47d5..04592913f3 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_asset_context.py +++ b/openpype/hosts/nuke/plugins/publish/validate_asset_context.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- """Validate if instance asset is the same as context asset.""" from __future__ import absolute_import +from typing_extensions import deprecated import pyblish.api @@ -56,8 +57,20 @@ class ValidateCorrectAssetContext( ] optional = True - # TODO: apply_settigs to maintain backwards compatibility - # with `ValidateCorrectAssetName` + @classmethod + def apply_settings(cls, project_settings): + """Apply the settings from the deprecated + ExtractReviewDataMov plugin for backwards compatibility + """ + nuke_publish = project_settings["nuke"]["publish"] + if "ValidateCorrectAssetName" not in nuke_publish: + return + + deprecated_setting = nuke_publish["ValidateCorrectAssetName"] + cls.enabled = deprecated_setting["enabled"] + cls.optional = deprecated_setting["optional"] + cls.active = deprecated_setting["active"] + def process(self, instance): if not self.is_active(instance.data): return From 63d27aa331f639a2daa9982a41b4d0e3682c8f7a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 11 Oct 2023 22:32:14 +0200 Subject: [PATCH 0267/1233] updating docstrings --- .../nuke/plugins/publish/validate_asset_context.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/validate_asset_context.py b/openpype/hosts/nuke/plugins/publish/validate_asset_context.py index 04592913f3..3cd8704b76 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_asset_context.py +++ b/openpype/hosts/nuke/plugins/publish/validate_asset_context.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- """Validate if instance asset is the same as context asset.""" from __future__ import absolute_import -from typing_extensions import deprecated import pyblish.api @@ -17,6 +16,7 @@ from openpype.pipeline.publish import ( class SelectInvalidNodesAction(pyblish.api.Action): + """Select invalid nodes.""" label = "Select Failed Node" icon = "briefcase" @@ -45,8 +45,6 @@ class ValidateCorrectAssetContext( so it can be disabled when needed. Checking `asset` and `task` keys. - - Action on this validator will select invalid instances in Outliner. """ order = ValidateContentsOrder label = "Validate asset context" @@ -59,8 +57,7 @@ class ValidateCorrectAssetContext( @classmethod def apply_settings(cls, project_settings): - """Apply the settings from the deprecated - ExtractReviewDataMov plugin for backwards compatibility + """Apply deprecated settings from project settings. """ nuke_publish = project_settings["nuke"]["publish"] if "ValidateCorrectAssetName" not in nuke_publish: @@ -105,6 +102,7 @@ class ValidateCorrectAssetContext( @classmethod def get_invalid(cls, instance, compute=False): + """Get invalid keys from instance data and context data.""" invalid = instance.data.get("invalid_keys", []) if compute: @@ -122,6 +120,7 @@ class ValidateCorrectAssetContext( @classmethod def repair(cls, instance): + """Repair instance data with context data.""" invalid = cls.get_invalid(instance) create_context = instance.context.data["create_context"] @@ -138,6 +137,7 @@ class ValidateCorrectAssetContext( @classmethod def select(cls, instance): + """Select invalid node """ invalid = cls.get_invalid(instance) if not invalid: return From bf6303a90876a5234c335df386d4f4c99da3ec39 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 11 Oct 2023 22:40:04 +0200 Subject: [PATCH 0268/1233] hound --- openpype/hosts/nuke/plugins/publish/validate_asset_context.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/validate_asset_context.py b/openpype/hosts/nuke/plugins/publish/validate_asset_context.py index 3cd8704b76..3a5678d61d 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_asset_context.py +++ b/openpype/hosts/nuke/plugins/publish/validate_asset_context.py @@ -134,7 +134,6 @@ class ValidateCorrectAssetContext( create_context.save_changes() - @classmethod def select(cls, instance): """Select invalid node """ From 8ee57bd3a1e030f32f85b0c409e44d09e2e0c9bb Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 11 Oct 2023 23:57:42 +0300 Subject: [PATCH 0269/1233] bugfix update instance parameters values on update_instances --- openpype/hosts/houdini/api/lib.py | 1 - openpype/hosts/houdini/api/plugin.py | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 3b45914b19..6fa8b02735 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -932,7 +932,6 @@ def self_publish(): active = node_path in inputs_paths instance["active"] = active - hou.node(node_path).parm("active").set(active) context.save_changes() diff --git a/openpype/hosts/houdini/api/plugin.py b/openpype/hosts/houdini/api/plugin.py index c82ba11114..5102b64644 100644 --- a/openpype/hosts/houdini/api/plugin.py +++ b/openpype/hosts/houdini/api/plugin.py @@ -250,11 +250,14 @@ class HoudiniCreator(NewCreator, HoudiniCreatorBase): key: changes[key].new_value for key in changes.changed_keys } + # Update ParmTemplates self.imprint( instance_node, new_values, update=True ) + # Update values + instance_node.setParms(new_values) def imprint(self, node, values, update=False): # Never store instance node and instance id since that data comes From 7035e1e0145f11f832eee08754f681e3304e591d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 12 Oct 2023 13:50:31 +0800 Subject: [PATCH 0270/1233] clean up the codes in thummbnail and preview animation extractor --- openpype/hosts/max/api/lib.py | 32 ++++++++++++++++++- .../publish/extract_review_animation.py | 32 ++++--------------- .../max/plugins/publish/extract_thumbnail.py | 26 ++++++++++++--- 3 files changed, 59 insertions(+), 31 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 68baf720bc..fe2742cdb0 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -322,7 +322,7 @@ def is_headless(): @contextlib.contextmanager -def viewport_camera(camera): +def viewport_setup_updated(camera): original = rt.viewport.getCamera() has_autoplay = rt.preferences.playPreviewWhenDone if not original: @@ -339,6 +339,36 @@ def viewport_camera(camera): rt.preferences.playPreviewWhenDone = has_autoplay +@contextlib.contextmanager +def viewport_setup(viewport_setting, visual_style, camera): + """Function to set visual style options + + Args: + visual_style (str): visual style for active viewport + + Returns: + list: the argument which can set visual style + """ + original = rt.viewport.getCamera() + has_autoplay = rt.preferences.playPreviewWhenDone + if not original: + # if there is no original camera + # use the current camera as original + original = rt.getNodeByName(camera) + review_camera = rt.getNodeByName(camera) + current_setting = viewport_setting.VisualStyleMode + try: + rt.viewport.setCamera(review_camera) + viewport_setting.VisualStyleMode = rt.Name( + visual_style) + rt.preferences.playPreviewWhenDone = False + yield + finally: + rt.viewport.setCamera(original) + viewport_setting.VisualStyleMode = current_setting + rt.preferences.playPreviewWhenDone = has_autoplay + + def set_timeline(frameStart, frameEnd): """Set frame range for timeline editor in Max """ diff --git a/openpype/hosts/max/plugins/publish/extract_review_animation.py b/openpype/hosts/max/plugins/publish/extract_review_animation.py index 9c26ef7e7d..24e7785b2b 100644 --- a/openpype/hosts/max/plugins/publish/extract_review_animation.py +++ b/openpype/hosts/max/plugins/publish/extract_review_animation.py @@ -4,7 +4,8 @@ import pyblish.api from pymxs import runtime as rt from openpype.pipeline import publish from openpype.hosts.max.api.lib import ( - viewport_camera, + viewport_setup_updated, + viewport_setup, get_max_version, set_preview_arg ) @@ -38,7 +39,7 @@ class ExtractReviewAnimation(publish.Extractor): review_camera = instance.data["review_camera"] if get_max_version() >= 2024: - with viewport_camera(review_camera): + with viewport_setup_updated(review_camera): preview_arg = set_preview_arg( instance, filepath, start, end, fps) rt.execute(preview_arg) @@ -46,10 +47,10 @@ class ExtractReviewAnimation(publish.Extractor): visual_style_preset = instance.data.get("visualStyleMode") nitrousGraphicMgr = rt.NitrousGraphicsManager viewport_setting = nitrousGraphicMgr.GetActiveViewportSetting() - with viewport_camera(review_camera) and ( - self._visual_style_option( - viewport_setting, visual_style_preset) - ): + with viewport_setup( + viewport_setting, + visual_style_preset, + review_camera): viewport_setting.VisualStyleMode = rt.Name( visual_style_preset) preview_arg = set_preview_arg( @@ -87,22 +88,3 @@ class ExtractReviewAnimation(publish.Extractor): file_list.append(actual_name) return file_list - - @contextlib.contextmanager - def _visual_style_option(self, viewport_setting, visual_style): - """Function to set visual style options - - Args: - visual_style (str): visual style for active viewport - - Returns: - list: the argument which can set visual style - """ - current_setting = viewport_setting.VisualStyleMode - if visual_style != current_setting: - try: - viewport_setting.VisualStyleMode = rt.Name( - visual_style) - yield - finally: - viewport_setting.VisualStyleMode = current_setting diff --git a/openpype/hosts/max/plugins/publish/extract_thumbnail.py b/openpype/hosts/max/plugins/publish/extract_thumbnail.py index 22c45f3e11..731dac74e3 100644 --- a/openpype/hosts/max/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/max/plugins/publish/extract_thumbnail.py @@ -4,12 +4,14 @@ import pyblish.api from pymxs import runtime as rt from openpype.pipeline import publish from openpype.hosts.max.api.lib import ( - viewport_camera, + viewport_setup_updated, + viewport_setup, get_max_version, set_preview_arg ) + class ExtractThumbnail(publish.Extractor): """ Extract Thumbnail for Review @@ -39,10 +41,24 @@ class ExtractThumbnail(publish.Extractor): "Writing Thumbnail to" " '%s' to '%s'" % (filename, tmp_staging)) review_camera = instance.data["review_camera"] - with viewport_camera(review_camera): - preview_arg = set_preview_arg( - instance, filepath, fps, frame) - rt.execute(preview_arg) + if get_max_version() >= 2024: + with viewport_setup_updated(review_camera): + preview_arg = set_preview_arg( + instance, filepath, frame, frame, fps) + rt.execute(preview_arg) + else: + visual_style_preset = instance.data.get("visualStyleMode") + nitrousGraphicMgr = rt.NitrousGraphicsManager + viewport_setting = nitrousGraphicMgr.GetActiveViewportSetting() + with viewport_setup( + viewport_setting, + visual_style_preset, + review_camera): + viewport_setting.VisualStyleMode = rt.Name( + visual_style_preset) + preview_arg = set_preview_arg( + instance, filepath, frame, frame, fps) + rt.execute(preview_arg) representation = { "name": "thumbnail", From 7d98ddfbe143fb92f64bbf4aea37bba937a7127d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 12 Oct 2023 13:52:04 +0800 Subject: [PATCH 0271/1233] hound --- .../hosts/max/plugins/publish/extract_review_animation.py | 7 +++---- openpype/hosts/max/plugins/publish/extract_thumbnail.py | 6 +++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_review_animation.py b/openpype/hosts/max/plugins/publish/extract_review_animation.py index 24e7785b2b..acabd74958 100644 --- a/openpype/hosts/max/plugins/publish/extract_review_animation.py +++ b/openpype/hosts/max/plugins/publish/extract_review_animation.py @@ -1,5 +1,4 @@ import os -import contextlib import pyblish.api from pymxs import runtime as rt from openpype.pipeline import publish @@ -48,9 +47,9 @@ class ExtractReviewAnimation(publish.Extractor): nitrousGraphicMgr = rt.NitrousGraphicsManager viewport_setting = nitrousGraphicMgr.GetActiveViewportSetting() with viewport_setup( - viewport_setting, - visual_style_preset, - review_camera): + viewport_setting, + visual_style_preset, + review_camera): viewport_setting.VisualStyleMode = rt.Name( visual_style_preset) preview_arg = set_preview_arg( diff --git a/openpype/hosts/max/plugins/publish/extract_thumbnail.py b/openpype/hosts/max/plugins/publish/extract_thumbnail.py index 731dac74e3..f0f349cd77 100644 --- a/openpype/hosts/max/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/max/plugins/publish/extract_thumbnail.py @@ -51,9 +51,9 @@ class ExtractThumbnail(publish.Extractor): nitrousGraphicMgr = rt.NitrousGraphicsManager viewport_setting = nitrousGraphicMgr.GetActiveViewportSetting() with viewport_setup( - viewport_setting, - visual_style_preset, - review_camera): + viewport_setting, + visual_style_preset, + review_camera): viewport_setting.VisualStyleMode = rt.Name( visual_style_preset) preview_arg = set_preview_arg( From 32820f4bfa7574c366caae4295828f4efdcb0370 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 12 Oct 2023 16:24:13 +0800 Subject: [PATCH 0272/1233] add viewport Tetxure support for preview --- openpype/hosts/max/api/lib.py | 8 ++++++-- openpype/hosts/max/plugins/publish/collect_review.py | 12 ++++++++++-- .../hosts/max/plugins/publish/extract_thumbnail.py | 2 +- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index fe2742cdb0..26ca5ed1d8 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -578,8 +578,12 @@ def set_preview_arg(instance, filepath, preview_preset = "userdefined" else: preview_preset = preview_preset.lower() - preview_preset.option = f"vpPreset:#{visual_style_preset}" - job_args.append(preview_preset) + preview_preset_option = f"vpPreset:#{visual_style_preset}" + job_args.append(preview_preset_option) + viewport_texture = instance.data.get("vpTexture", True) + if viewport_texture: + viewport_texture_option = f"vpTexture:{viewport_texture}" + job_args.append(viewport_texture_option) job_str = " ".join(job_args) log.debug(job_str) diff --git a/openpype/hosts/max/plugins/publish/collect_review.py b/openpype/hosts/max/plugins/publish/collect_review.py index 1f0dca5329..8b9a777c63 100644 --- a/openpype/hosts/max/plugins/publish/collect_review.py +++ b/openpype/hosts/max/plugins/publish/collect_review.py @@ -59,6 +59,7 @@ class CollectReview(pyblish.api.InstancePlugin, instance.data["colorspaceConfig"] = colorspace_mgr.OCIOConfigPath instance.data["colorspaceDisplay"] = display instance.data["colorspaceView"] = view_transform + instance.data["vpTexture"] = attr_values.get("vpTexture") # Enable ftrack functionality instance.data.setdefault("families", []).append('ftrack') @@ -66,12 +67,19 @@ class CollectReview(pyblish.api.InstancePlugin, burnin_members = instance.data.setdefault("burninDataMembers", {}) burnin_members["focalLength"] = focal_length - self.log.debug(f"data:{data}") instance.data.update(data) + self.log.debug(f"data:{data}") @classmethod def get_attribute_defs(cls): - return [ + additional_attrs = [] + if int(get_max_version()) >= 2024: + additional_attrs.append( + BoolDef("vpTexture", + label="Viewport Texture", + default=True), + ) + return additional_attrs + [ BoolDef("dspGeometry", label="Geometry", default=True), diff --git a/openpype/hosts/max/plugins/publish/extract_thumbnail.py b/openpype/hosts/max/plugins/publish/extract_thumbnail.py index f0f349cd77..890ee24f8e 100644 --- a/openpype/hosts/max/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/max/plugins/publish/extract_thumbnail.py @@ -41,7 +41,7 @@ class ExtractThumbnail(publish.Extractor): "Writing Thumbnail to" " '%s' to '%s'" % (filename, tmp_staging)) review_camera = instance.data["review_camera"] - if get_max_version() >= 2024: + if int(get_max_version()) >= 2024: with viewport_setup_updated(review_camera): preview_arg = set_preview_arg( instance, filepath, frame, frame, fps) From 6d04bcd7acc8fb304163795f4f74b9d866add5ae Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 12 Oct 2023 16:40:31 +0800 Subject: [PATCH 0273/1233] make sure get max version is integer --- openpype/hosts/max/plugins/publish/extract_review_animation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/extract_review_animation.py b/openpype/hosts/max/plugins/publish/extract_review_animation.py index acabd74958..da3f4155c1 100644 --- a/openpype/hosts/max/plugins/publish/extract_review_animation.py +++ b/openpype/hosts/max/plugins/publish/extract_review_animation.py @@ -37,7 +37,7 @@ class ExtractReviewAnimation(publish.Extractor): " '%s' to '%s'" % (filename, staging_dir)) review_camera = instance.data["review_camera"] - if get_max_version() >= 2024: + if int(get_max_version()) >= 2024: with viewport_setup_updated(review_camera): preview_arg = set_preview_arg( instance, filepath, start, end, fps) From 68b281fdedad5df6339452418335e5f7f771ca07 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 12 Oct 2023 10:10:29 +0100 Subject: [PATCH 0274/1233] Improved how models abc extractor is implemented Co-authored-by: Kayla Man --- .../plugins/publish/collect_instances.py | 5 +---- .../blender/plugins/publish/extract_abc.py | 10 +++++++++- .../plugins/publish/extract_abc_model.py | 17 ----------------- 3 files changed, 10 insertions(+), 22 deletions(-) delete mode 100644 openpype/hosts/blender/plugins/publish/extract_abc_model.py diff --git a/openpype/hosts/blender/plugins/publish/collect_instances.py b/openpype/hosts/blender/plugins/publish/collect_instances.py index b4fc167638..c95d718187 100644 --- a/openpype/hosts/blender/plugins/publish/collect_instances.py +++ b/openpype/hosts/blender/plugins/publish/collect_instances.py @@ -50,10 +50,10 @@ class CollectInstances(pyblish.api.ContextPlugin): for group in asset_groups: instance = self.create_instance(context, group) - family = instance.data["family"] members = [] if type(group) == bpy.types.Collection: members = list(group.objects) + family = instance.data["family"] if family == "animation": for obj in group.objects: if obj.type == 'EMPTY' and obj.get(AVALON_PROPERTY): @@ -63,9 +63,6 @@ class CollectInstances(pyblish.api.ContextPlugin): else: members = group.children_recursive - if family == "pointcache": - instance.data["families"].append("abc.export") - members.append(group) instance[:] = members self.log.debug(json.dumps(instance.data, indent=4)) diff --git a/openpype/hosts/blender/plugins/publish/extract_abc.py b/openpype/hosts/blender/plugins/publish/extract_abc.py index b113685842..a603366f30 100644 --- a/openpype/hosts/blender/plugins/publish/extract_abc.py +++ b/openpype/hosts/blender/plugins/publish/extract_abc.py @@ -12,7 +12,7 @@ class ExtractABC(publish.Extractor): label = "Extract ABC" hosts = ["blender"] - families = ["abc.export"] + families = ["pointcache"] def process(self, instance): # Define extract output file path @@ -61,3 +61,11 @@ class ExtractABC(publish.Extractor): self.log.info("Extracted instance '%s' to: %s", instance.name, representation) + +class ExtractModelABC(ExtractABC): + """Extract model as ABC.""" + + label = "Extract Model ABC" + hosts = ["blender"] + families = ["model"] + optional = True diff --git a/openpype/hosts/blender/plugins/publish/extract_abc_model.py b/openpype/hosts/blender/plugins/publish/extract_abc_model.py deleted file mode 100644 index b31e36c681..0000000000 --- a/openpype/hosts/blender/plugins/publish/extract_abc_model.py +++ /dev/null @@ -1,17 +0,0 @@ -import pyblish.api -from openpype.pipeline import publish - - -class ExtractModelABC(publish.Extractor): - """Extract model as ABC.""" - - order = pyblish.api.ExtractorOrder - 0.1 - label = "Extract Model ABC" - hosts = ["blender"] - families = ["model"] - optional = True - - def process(self, instance): - # Add abc.export family to the instance, to allow the extraction - # as alembic of the asset. - instance.data["families"].append("abc.export") From 7cce128c2027f88cb5dc54d526ff3f944dc3a14f Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 12 Oct 2023 10:11:28 +0100 Subject: [PATCH 0275/1233] Hound fixes --- openpype/hosts/blender/plugins/publish/extract_abc.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/blender/plugins/publish/extract_abc.py b/openpype/hosts/blender/plugins/publish/extract_abc.py index a603366f30..7b6c4d7ae7 100644 --- a/openpype/hosts/blender/plugins/publish/extract_abc.py +++ b/openpype/hosts/blender/plugins/publish/extract_abc.py @@ -62,6 +62,7 @@ class ExtractABC(publish.Extractor): self.log.info("Extracted instance '%s' to: %s", instance.name, representation) + class ExtractModelABC(ExtractABC): """Extract model as ABC.""" From 6259687b32c9216f0f111e07b5c6228242d1d25e Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 12 Oct 2023 10:14:30 +0100 Subject: [PATCH 0276/1233] Increment workfile version when publishing pointcache --- .../hosts/blender/plugins/publish/increment_workfile_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/blender/plugins/publish/increment_workfile_version.py b/openpype/hosts/blender/plugins/publish/increment_workfile_version.py index 3d176f9c30..6ace14d77c 100644 --- a/openpype/hosts/blender/plugins/publish/increment_workfile_version.py +++ b/openpype/hosts/blender/plugins/publish/increment_workfile_version.py @@ -10,7 +10,7 @@ class IncrementWorkfileVersion(pyblish.api.ContextPlugin): optional = True hosts = ["blender"] families = ["animation", "model", "rig", "action", "layout", "blendScene", - "render"] + "pointcache", "render"] def process(self, context): From e7cd31f2dd5521e6bb8d30e908958f6c3706628a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 12 Oct 2023 11:22:53 +0200 Subject: [PATCH 0277/1233] Extended error message when getting subset name (#5649) * Modified KeyError message Basic KeyError exception was raised which didn't produce enough information. Now it should be more verbose. * Updated exception message * Changed to custom exception Custom exception can be handled in nicer way that default KeyError * Update openpype/pipeline/create/subset_name.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> * Renamed custom exception * Update openpype/pipeline/create/subset_name.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --------- Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/pipeline/create/subset_name.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/create/subset_name.py b/openpype/pipeline/create/subset_name.py index 3f0692b46a..00025b19b8 100644 --- a/openpype/pipeline/create/subset_name.py +++ b/openpype/pipeline/create/subset_name.py @@ -14,6 +14,13 @@ class TaskNotSetError(KeyError): super(TaskNotSetError, self).__init__(msg) +class TemplateFillError(Exception): + def __init__(self, msg=None): + if not msg: + msg = "Creator's subset name template is missing key value." + super(TemplateFillError, self).__init__(msg) + + def get_subset_name_template( project_name, family, @@ -112,6 +119,10 @@ def get_subset_name( for project. Settings are queried if not passed. family_filter (Optional[str]): Use different family for subset template filtering. Value of 'family' is used when not passed. + + Raises: + TemplateFillError: If filled template contains placeholder key which is not + collected. """ if not family: @@ -154,4 +165,10 @@ def get_subset_name( for key, value in dynamic_data.items(): fill_pairs[key] = value - return template.format(**prepare_template_data(fill_pairs)) + try: + return template.format(**prepare_template_data(fill_pairs)) + except KeyError as exp: + raise TemplateFillError( + "Value for {} key is missing in template '{}'." + " Available values are {}".format(str(exp), template, fill_pairs) + ) From 95fefaaa169a7404d3fae9ed5906b1227cde7c95 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 12 Oct 2023 10:45:16 +0100 Subject: [PATCH 0278/1233] Fix wrong hierarchy when loading --- .../hosts/blender/plugins/load/load_abc.py | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/blender/plugins/load/load_abc.py b/openpype/hosts/blender/plugins/load/load_abc.py index 9b3d940536..a7077f98f2 100644 --- a/openpype/hosts/blender/plugins/load/load_abc.py +++ b/openpype/hosts/blender/plugins/load/load_abc.py @@ -60,16 +60,30 @@ class CacheModelLoader(plugin.AssetLoader): imported = lib.get_selection() + empties = [obj for obj in imported if obj.type == 'EMPTY'] + + container = None + + for empty in empties: + if not empty.parent: + container = empty + break + + assert container, "No asset group found" + # Children must be linked before parents, # otherwise the hierarchy will break objects = [] + nodes = list(container.children) - for obj in imported: + for obj in nodes: obj.parent = asset_group - for obj in imported: + bpy.data.objects.remove(container) + + for obj in nodes: objects.append(obj) - imported.extend(list(obj.children)) + nodes.extend(list(obj.children)) objects.reverse() From 38427b5eecb38b6ffdc2494826907fe542c025b6 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Thu, 12 Oct 2023 11:07:09 +0100 Subject: [PATCH 0279/1233] Testing: Inject mongo_url argument earlier (#5706) * Inject mongo_url argument earlier * monkeypatch instead of os.environ --------- Co-authored-by: Petr Kalis --- tests/lib/testing_classes.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/lib/testing_classes.py b/tests/lib/testing_classes.py index e82e438e54..277b332e19 100644 --- a/tests/lib/testing_classes.py +++ b/tests/lib/testing_classes.py @@ -105,7 +105,7 @@ class ModuleUnitTest(BaseTest): yield path @pytest.fixture(scope="module") - def env_var(self, monkeypatch_session, download_test_data): + def env_var(self, monkeypatch_session, download_test_data, mongo_url): """Sets temporary env vars from json file.""" env_url = os.path.join(download_test_data, "input", "env_vars", "env_var.json") @@ -129,6 +129,9 @@ class ModuleUnitTest(BaseTest): monkeypatch_session.setenv(key, str(value)) #reset connection to openpype DB with new env var + if mongo_url: + monkeypatch_session.setenv("OPENPYPE_MONGO", mongo_url) + import openpype.settings.lib as sett_lib sett_lib._SETTINGS_HANDLER = None sett_lib._LOCAL_SETTINGS_HANDLER = None @@ -150,8 +153,7 @@ class ModuleUnitTest(BaseTest): request, mongo_url): """Restore prepared MongoDB dumps into selected DB.""" backup_dir = os.path.join(download_test_data, "input", "dumps") - - uri = mongo_url or os.environ.get("OPENPYPE_MONGO") + uri = os.environ.get("OPENPYPE_MONGO") db_handler = DBHandler(uri) db_handler.setup_from_dump(self.TEST_DB_NAME, backup_dir, overwrite=True, From 7589de5aa14cb595f7646f68e7f9d8eaf373a0b5 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 12 Oct 2023 11:33:18 +0100 Subject: [PATCH 0280/1233] Improved loop to get all loaded objects --- openpype/hosts/blender/plugins/load/load_abc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/blender/plugins/load/load_abc.py b/openpype/hosts/blender/plugins/load/load_abc.py index a7077f98f2..91d7356a2c 100644 --- a/openpype/hosts/blender/plugins/load/load_abc.py +++ b/openpype/hosts/blender/plugins/load/load_abc.py @@ -83,7 +83,7 @@ class CacheModelLoader(plugin.AssetLoader): for obj in nodes: objects.append(obj) - nodes.extend(list(obj.children)) + objects.extend(list(obj.children_recursive)) objects.reverse() From 251740891980ad37a7d3d326bbb833b36ced2f24 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 12 Oct 2023 11:40:21 +0100 Subject: [PATCH 0281/1233] Fixed handling of missing container in the abc file being loaded --- .../hosts/blender/plugins/load/load_abc.py | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/blender/plugins/load/load_abc.py b/openpype/hosts/blender/plugins/load/load_abc.py index 91d7356a2c..531a820436 100644 --- a/openpype/hosts/blender/plugins/load/load_abc.py +++ b/openpype/hosts/blender/plugins/load/load_abc.py @@ -69,23 +69,26 @@ class CacheModelLoader(plugin.AssetLoader): container = empty break - assert container, "No asset group found" - - # Children must be linked before parents, - # otherwise the hierarchy will break objects = [] - nodes = list(container.children) + if container: + # Children must be linked before parents, + # otherwise the hierarchy will break + nodes = list(container.children) - for obj in nodes: - obj.parent = asset_group + for obj in nodes: + obj.parent = asset_group - bpy.data.objects.remove(container) + bpy.data.objects.remove(container) - for obj in nodes: - objects.append(obj) - objects.extend(list(obj.children_recursive)) + for obj in nodes: + objects.append(obj) + objects.extend(list(obj.children_recursive)) - objects.reverse() + objects.reverse() + else: + for obj in imported: + obj.parent = asset_group + objects = imported for obj in objects: # Unlink the object from all collections From 5b3c6b8cfde60a9d053b093e1401b089aa6db8e0 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Thu, 12 Oct 2023 13:16:14 +0100 Subject: [PATCH 0282/1233] Update tests/integration/hosts/maya/input/startup/userSetup.py --- tests/integration/hosts/maya/input/startup/userSetup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/hosts/maya/input/startup/userSetup.py b/tests/integration/hosts/maya/input/startup/userSetup.py index 67352af63d..bb73ec7ee0 100644 --- a/tests/integration/hosts/maya/input/startup/userSetup.py +++ b/tests/integration/hosts/maya/input/startup/userSetup.py @@ -19,10 +19,10 @@ def setup_pyblish_logging(): def _run_publish_test_deferred(): try: + setup_pyblish_logging() pyblish.util.publish() finally: cmds.quit(force=True) -cmds.evalDeferred("setup_pyblish_logging()", evaluateNext=True) cmds.evalDeferred("_run_publish_test_deferred()", lowestPriority=True) From 73a88419d07316cf549decf3bb655554692e0d5d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 12 Oct 2023 14:43:09 +0200 Subject: [PATCH 0283/1233] Chore: AYON query functions arguments (#5752) * fixe get subsets to work as in mongo api * fixe get assets to work as in mongo api --- openpype/client/server/entities.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/client/server/entities.py b/openpype/client/server/entities.py index 3ee62a3172..16223d3d91 100644 --- a/openpype/client/server/entities.py +++ b/openpype/client/server/entities.py @@ -75,9 +75,9 @@ def _get_subsets( ): fields.add(key) - active = None + active = True if archived: - active = False + active = None for subset in con.get_products( project_name, @@ -196,7 +196,7 @@ def get_assets( active = True if archived: - active = False + active = None con = get_server_api_connection() fields = folder_fields_v3_to_v4(fields, con) From bc4c2e02004eceeafdb74ea68bb5f54cf411e063 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Thu, 12 Oct 2023 12:47:03 +0000 Subject: [PATCH 0284/1233] [Automated] Release --- CHANGELOG.md | 471 ++++++++++++++++++++++++++++++++++++++++++++ openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 473 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f14340348..7d5cf2c4d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,477 @@ # Changelog +## [3.17.2](https://github.com/ynput/OpenPype/tree/3.17.2) + + +[Full Changelog](https://github.com/ynput/OpenPype/compare/3.17.1...3.17.2) + +### **🆕 New features** + + +
+Maya: Add MayaPy application. #5705 + +This adds mayapy to the application to be launched from a task. + + +___ + +
+ + +
+Feature: Copy resources when downloading last workfile #4944 + +When the last published workfile is downloaded as a prelaunch hook, all resource files referenced in the workfile representation are copied to the `resources` folder, which is inside the local workfile folder. + + +___ + +
+ + +
+Blender: Deadline support #5438 + +Add Deadline support for Blender. + + +___ + +
+ + +
+Fusion: implement toggle to use Deadline plugin FusionCmd #5678 + +Fusion 17 doesn't work in DL 10.3, but FusionCmd does. It might be probably better option as headless variant.Fusion plugin seems to be closing and reopening application when worker is running on artist machine, not so with FusionCmdAdded configuration to Project Settings for admin to select appropriate Deadline plugin: + + +___ + +
+ + +
+Loader tool: Refactor loader tool (for AYON) #5729 + +Refactored loader tool to new tool. Separated backend and frontend logic. Refactored logic is AYON-centric and is used only in AYON mode, so it does not affect OpenPype. The tool is also replacing library loader. + + +___ + +
+ +### **🚀 Enhancements** + + +
+Maya: implement matchmove publishing #5445 + +Add possibility to export multiple cameras in single `matchmove` family instance, both in `abc` and `ma`.Exposed flag 'Keep image planes' to control export of image planes. + + +___ + +
+ + +
+Maya: Add optional Fbx extractors in Rig and Animation family #5589 + +This PR allows user to export control rigs(optionally with mesh) and animated rig in fbx optionally by attaching the rig objects to the two newly introduced sets. + + +___ + +
+ + +
+Maya: Optional Resolution Validator for Render #5693 + +Adding optional resolution validator for maya in render family, similar to the one in Max.It checks if the resolution in render setting aligns with that in setting from the db. + + +___ + +
+ + +
+Use host's node uniqueness for instance id in new publisher #5490 + +Instead of writing `instance_id` as parm or attributes on the publish instances we can, for some hosts, just rely on a unique name or path within the scene to refer to that particular instance. By doing so we fix #4820 because upon duplicating such a publish instance using the host's (DCC) functionality the uniqueness for the duplicate is then already ensured instead of attributes remaining exact same value as where to were duplicated from, making `instance_id` a non-unique value. + + +___ + +
+ + +
+Max: Implementation of OCIO configuration #5499 + +Resolve #5473 Implementation of OCIO configuration for Max 2024 regarding to the update of Max 2024 + + +___ + +
+ + +
+Nuke: Multiple format supports for ExtractReviewDataMov #5623 + +This PR would fix the bug of the plugin `ExtractReviewDataMov` not being able to support extensions other than `mov`. The plugin is also renamed to `ExtractReviewDataBakingStreams` as i provides multiple format supoort. + + +___ + +
+ + +
+Bugfix: houdini switching context doesnt update variables #5651 + +Allows admins to have a list of vars (e.g. JOB) with (dynamic) values that will be updated on context changes, e.g. when switching to another asset or task.Using template keys is supported but formatting keys capitalization variants is not, e.g. {Asset} and {ASSET} won't workDisabling Update Houdini vars on context change feature will leave all Houdini vars unmanaged and thus no context update changes will occur.Also, this PR adds a new button in menu to update vars on demand. + + +___ + +
+ + +
+Publisher: Fix report maker memory leak + optimize lookups using set #5667 + +Fixes a memory leak where resetting publisher does not clear the stored plugins for the Publish Report Maker.Also changes the stored plugins to a `set` to optimize the lookup speeds. + + +___ + +
+ + +
+Add openpype_mongo command flag for testing. #5676 + +Instead of changing the environment, this command flag allows for changing the database. + + +___ + +
+ + +
+Nuke: minor docstring and code tweaks for ExtractReviewMov #5695 + +Code and docstring tweaks on https://github.com/ynput/OpenPype/pull/5623 + + +___ + +
+ + +
+AYON: Small settings fixes #5699 + +Small changes/fixes related to AYON settings. All foundry apps variant `13-0` has label `13.0`. Key `"ExtractReviewIntermediates"` is not mandatory in settings. + + +___ + +
+ + +
+Blender: Alembic Animation loader #5711 + +Implemented loading Alembic Animations in Blender. + + +___ + +
+ +### **🐛 Bug fixes** + + +
+Maya: Missing "data" field and enabling of audio #5618 + +When updating audio containers, the field "data" was missing and the audio node was not enabled on the timeline. + + +___ + +
+ + +
+Maya: Bug in validate Plug-in Path Attribute #5687 + +Overwriting list with string is causing `TypeError: string indices must be integers` in subsequent iterations, crashing the validator plugin. + + +___ + +
+ + +
+General: Avoid fallback if value is 0 for handle start/end #5652 + +There's a bug on the `pyblish_functions.get_time_data_from_instance_or_context` where if `handleStart` or `handleEnd` on the instance are set to value 0 it's falling back to grabbing the handles from the instance context. Instead, the logic should be that it only falls back to the `instance.context` if the key doesn't exist.This change was only affecting me on the `handleStart`/`handleEnd` and it's unlikely it could cause issues on `frameStart`, `frameEnd` or `fps` but regardless, the `get` logic is wrong. + + +___ + +
+ + +
+Fusion: added missing env vars to Deadline submission #5659 + +Environment variables discerning type of job was missing. Without this injection of environment variables won't start. + + +___ + +
+ + +
+Nuke: workfile version synchronization settings fixed #5662 + +Settings for synchronizing workfile version to published products is fixed. + + +___ + +
+ + +
+AYON Workfiles Tool: Open workfile changes context #5671 + +Change context when workfile is opened. + + +___ + +
+ + +
+Blender: Fix remove/update in new layout instance #5679 + +Fixes an error that occurs when removing or updating an asset in a new layout instance. + + +___ + +
+ + +
+AYON Launcher tool: Fix refresh btn #5685 + +Refresh button does propagate refreshed content properly. Folders and tasks are cached for 60 seconds instead of 10 seconds. Auto-refresh in launcher will refresh only actions and related data which is project and project settings. + + +___ + +
+ + +
+Deadline: handle all valid paths in RenderExecutable #5694 + +This commit enhances the path resolution mechanism in the RenderExecutable function of the Ayon plugin. Previously, the function only considered paths starting with a tilde (~), ignoring other valid paths listed in exe_list. This limitation led to an empty expanded_paths list when none of the paths in exe_list started with a tilde, causing the function to fail in finding the Ayon executable.With this fix, the RenderExecutable function now correctly processes and includes all valid paths from exe_list, improving its reliability and preventing unnecessary errors related to Ayon executable location. + + +___ + +
+ + +
+AYON Launcher tool: Fix skip last workfile boolean #5700 + +Skip last workfile boolean works as expected. + + +___ + +
+ + +
+Chore: Explore here action can work without task #5703 + +Explore here action does not crash when task is not selected, and change error message a little. + + +___ + +
+ + +
+Testing: Inject mongo_url argument earlier #5706 + +Fix for https://github.com/ynput/OpenPype/pull/5676The Mongo url is used earlier in the execution. + + +___ + +
+ + +
+Blender: Add support to auto-install PySide2 in blender 4 #5723 + +Change version regex to support blender 4 subfolder. + + +___ + +
+ + +
+Fix: Hardcoded main site and wrongly copied workfile #5733 + +Fixing these two issues: +- Hardcoded main site -> Replaced by `anatomy.fill_root`. +- Workfiles can sometimes be copied while they shouldn't. + + +___ + +
+ + +
+Bugfix: ServerDeleteOperation asset -> folder conversion typo #5735 + +Fix ServerDeleteOperation asset -> folder conversion typo + + +___ + +
+ + +
+Nuke: loaders are filtering correctly #5739 + +Variable name for filtering by extensions were not correct - it suppose to be plural. It is fixed now and filtering is working as suppose to. + + +___ + +
+ + +
+Nuke: failing multiple thumbnails integration #5741 + +This handles the situation when `ExtractReviewIntermediates` (previously `ExtractReviewDataMov`) has multiple outputs, including thumbnails that need to be integrated. Previously, integrating the thumbnail representation was causing an issue in the integration process. However, we have now resolved this issue by no longer integrating thumbnails as loadable representations.NOW default is that thumbnail representation are NOT integrated (eg. they will not show up in DB > couldn't be Loaded in Loader) and no `_thumb.jpg` will be left in `render` (most likely) publish folder.IF there would be need to override this behavior, please use `project_settings/global/publish/PreIntegrateThumbnails` + + +___ + +
+ + +
+AYON Settings: Fix global overrides #5745 + +The `output` dictionary that gets passed into `ayon_settings._convert_global_project_settings` gets replaced when converting the settings for `ExtractOIIOTranscode`. This results in `global` not being in the output dictionary and thus the defaults being used and not the project overrides. + + +___ + +
+ + +
+Chore: AYON query functions arguments #5752 + +Fixed how `archived` argument is handled in get subsets/assets function. + + +___ + +
+ +### **🔀 Refactored code** + + +
+Publisher: Refactor Report Maker plugin data storage to be a dict by plugin.id #5668 + +Refactor Report Maker plugin data storage to be a dict by `plugin.id`Also fixes `_current_plugin_data` type on `__init__` + + +___ + +
+ + +
+Chore: Refactor Resolve into new style HostBase, IWorkfileHost, ILoadHost #5701 + +Refactor Resolve into new style HostBase, IWorkfileHost, ILoadHost + + +___ + +
+ +### **Merged pull requests** + + +
+Chore: Maya reduce get project settings calls #5669 + +Re-use system settings / project settings where we can instead of requerying. + + +___ + +
+ + +
+Extended error message when getting subset name #5649 + +Each Creator is using `get_subset_name` functions which collects context data and fills configured template with placeholders.If any key is missing in the template, non descriptive error is thrown.This should provide more verbose message: + + +___ + +
+ + +
+Tests: Remove checks for env var #5696 + +Env var will be filled in `env_var` fixture, here it is too early to check + + +___ + +
+ + + + ## [3.17.1](https://github.com/ynput/OpenPype/tree/3.17.1) diff --git a/openpype/version.py b/openpype/version.py index 1a316df989..b0a79162b2 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.17.2-nightly.4" +__version__ = "3.17.2" diff --git a/pyproject.toml b/pyproject.toml index 2460185bdd..ad93b70c0f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.17.1" # OpenPype +version = "3.17.2" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From f5150665bd76b0ca27118b90cd1ce136cd899f8b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 12 Oct 2023 12:48:17 +0000 Subject: [PATCH 0285/1233] 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 f74904f79d..25f36ebc9a 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.17.2 - 3.17.2-nightly.4 - 3.17.2-nightly.3 - 3.17.2-nightly.2 @@ -134,7 +135,6 @@ body: - 3.14.11-nightly.2 - 3.14.11-nightly.1 - 3.14.10 - - 3.14.10-nightly.9 validations: required: true - type: dropdown From b92bc4b20236d14e9a1ebaf6ed8250820b190319 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Thu, 12 Oct 2023 14:18:11 +0100 Subject: [PATCH 0286/1233] Update tests/integration/hosts/maya/input/startup/userSetup.py Co-authored-by: Roy Nieterau --- tests/integration/hosts/maya/input/startup/userSetup.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/integration/hosts/maya/input/startup/userSetup.py b/tests/integration/hosts/maya/input/startup/userSetup.py index bb73ec7ee0..eb6e2411b5 100644 --- a/tests/integration/hosts/maya/input/startup/userSetup.py +++ b/tests/integration/hosts/maya/input/startup/userSetup.py @@ -8,13 +8,13 @@ import pyblish.util def setup_pyblish_logging(): log = logging.getLogger("pyblish") - hnd = logging.StreamHandler(sys.stdout) - fmt = logging.Formatter( + handler = logging.StreamHandler(sys.stdout) + formatter = logging.Formatter( "pyblish (%(levelname)s) (line: %(lineno)d) %(name)s:" "\n%(message)s" ) - hnd.setFormatter(fmt) - log.addHandler(hnd) + handler.setFormatter(formatter) + log.addHandler(handler) def _run_publish_test_deferred(): From 61f381cb5cee14f9f2c85b6db04c9144f9818ac5 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 12 Oct 2023 15:20:51 +0200 Subject: [PATCH 0287/1233] resolve: make sure of file existence --- openpype/hosts/resolve/api/lib.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index 4066dd34fd..37410c9727 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -196,7 +196,7 @@ def create_media_pool_item( Create media pool item. Args: - fpath (str): absolute path to a file + files (list[str]): list of absolute paths to files root (resolve.Folder)[optional]: root folder / bin object Returns: @@ -206,8 +206,13 @@ def create_media_pool_item( media_pool = get_current_project().GetMediaPool() root_bin = root or media_pool.GetRootFolder() + # make sure files list is not empty and first available file exists + filepath = next((f for f in files if os.path.isfile(f)), None) + if not filepath: + raise FileNotFoundError("No file found in input files list") + # try to search in bin if the clip does not exist - existing_mpi = get_media_pool_item(files[0], root_bin) + existing_mpi = get_media_pool_item(filepath, root_bin) if existing_mpi: return existing_mpi From f03be42e9d62882a501303abfbee37b83463c946 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 12 Oct 2023 15:30:39 +0200 Subject: [PATCH 0288/1233] resolve: improving key calling from version data --- openpype/hosts/resolve/api/plugin.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py index a0dba6fd05..8381f81acb 100644 --- a/openpype/hosts/resolve/api/plugin.py +++ b/openpype/hosts/resolve/api/plugin.py @@ -386,12 +386,13 @@ class ClipLoader: """Load clip into timeline Arguments: - files (list): list of files to load into timeline + files (list[str]): list of files to load into timeline """ # create project bin for the media to be imported into self.active_bin = lib.create_bin(self.data["binPath"]) - handle_start = self.data["versionData"].get("handleStart", 0) - handle_end = self.data["versionData"].get("handleEnd", 0) + + handle_start = self.data["versionData"].get("handleStart") or 0 + handle_end = self.data["versionData"].get("handleEnd") or 0 media_pool_item = lib.create_media_pool_item( files, From dfbc11bca505fbc06de8868453e28ded2f2b6072 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 12 Oct 2023 16:41:20 +0200 Subject: [PATCH 0289/1233] wrong action name in exception --- openpype/hosts/nuke/plugins/publish/validate_asset_context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/validate_asset_context.py b/openpype/hosts/nuke/plugins/publish/validate_asset_context.py index 3a5678d61d..09cb5102a5 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_asset_context.py +++ b/openpype/hosts/nuke/plugins/publish/validate_asset_context.py @@ -24,7 +24,7 @@ class SelectInvalidNodesAction(pyblish.api.Action): def process(self, context, plugin): if not hasattr(plugin, "select"): - raise RuntimeError("Plug-in does not have repair method.") + raise RuntimeError("Plug-in does not have select method.") # Get the failed instances self.log.debug("Finding failed plug-ins..") From 848953f026493244a0de98bbf6df6f6d0f421e73 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 12 Oct 2023 18:01:12 +0300 Subject: [PATCH 0290/1233] add sections --- server_addon/houdini/server/settings/publish.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server_addon/houdini/server/settings/publish.py b/server_addon/houdini/server/settings/publish.py index 5a1ee1fa07..ab1b71c6bb 100644 --- a/server_addon/houdini/server/settings/publish.py +++ b/server_addon/houdini/server/settings/publish.py @@ -25,7 +25,8 @@ class BasicValidateModel(BaseSettingsModel): class PublishPluginsModel(BaseSettingsModel): ValidateContainers: BasicValidateModel = Field( default_factory=BasicValidateModel, - title="Validate Latest Containers.") + title="Validate Latest Containers.", + section="Validators") ValidateMeshIsStatic: BasicValidateModel = Field( default_factory=BasicValidateModel, title="Validate Mesh is Static.") From 05dc8f557da1edddf3d53991bb0d4f766a6dc9bd Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 12 Oct 2023 18:02:13 +0300 Subject: [PATCH 0291/1233] Align Openpype with Ayon --- .../defaults/project_settings/houdini.json | 160 +++++++++++------- .../schemas/schema_houdini_create.json | 92 ++++++---- .../schemas/schema_houdini_publish.json | 56 +++--- 3 files changed, 187 insertions(+), 121 deletions(-) diff --git a/openpype/settings/defaults/project_settings/houdini.json b/openpype/settings/defaults/project_settings/houdini.json index 4f57ee52c6..f28beac65d 100644 --- a/openpype/settings/defaults/project_settings/houdini.json +++ b/openpype/settings/defaults/project_settings/houdini.json @@ -24,6 +24,12 @@ }, "shelves": [], "create": { + "CreateAlembicCamera": { + "enabled": true, + "default_variants": [ + "Main" + ] + }, "CreateArnoldAss": { "enabled": true, "default_variants": [ @@ -31,6 +37,66 @@ ], "ext": ".ass" }, + "CreateArnoldRop": { + "enabled": true, + "default_variants": [ + "Main" + ] + }, + "CreateCompositeSequence": { + "enabled": true, + "default_variants": [ + "Main" + ] + }, + "CreateHDA": { + "enabled": true, + "default_variants": [ + "Main" + ] + }, + "CreateKarmaROP": { + "enabled": true, + "default_variants": [ + "Main" + ] + }, + "CreateMantraROP": { + "enabled": true, + "default_variants": [ + "Main" + ] + }, + "CreatePointCache": { + "enabled": true, + "default_variants": [ + "Main" + ] + }, + "CreateBGEO": { + "enabled": true, + "default_variants": [ + "Main" + ] + }, + "CreateRedshiftProxy": { + "enabled": true, + "default_variants": [ + "Main" + ] + }, + "CreateRedshiftROP": { + "enabled": true, + "default_variants": [ + "Main" + ] + }, + "CreateReview": { + "enabled": true, + "default_variants": [ + "Main" + ] + }, "CreateStaticMesh": { "enabled": true, "default_variants": [ @@ -44,31 +110,13 @@ "UCX" ] }, - "CreateAlembicCamera": { + "CreateUSD": { "enabled": true, "default_variants": [ "Main" ] }, - "CreateCompositeSequence": { - "enabled": true, - "default_variants": [ - "Main" - ] - }, - "CreatePointCache": { - "enabled": true, - "default_variants": [ - "Main" - ] - }, - "CreateRedshiftROP": { - "enabled": true, - "default_variants": [ - "Main" - ] - }, - "CreateRemotePublish": { + "CreateUSDRender": { "enabled": true, "default_variants": [ "Main" @@ -80,32 +128,39 @@ "Main" ] }, - "CreateUSD": { - "enabled": false, - "default_variants": [ - "Main" - ] - }, - "CreateUSDModel": { - "enabled": false, - "default_variants": [ - "Main" - ] - }, - "USDCreateShadingWorkspace": { - "enabled": false, - "default_variants": [ - "Main" - ] - }, - "CreateUSDRender": { - "enabled": false, + "CreateVrayROP": { + "enabled": true, "default_variants": [ "Main" ] } }, "publish": { + "ValidateContainers": { + "enabled": true, + "optional": true, + "active": true + }, + "ValidateMeshIsStatic": { + "enabled": true, + "optional": true, + "active": true + }, + "ValidateReviewColorspace": { + "enabled": true, + "optional": true, + "active": true + }, + "ValidateSubsetName": { + "enabled": true, + "optional": true, + "active": true + }, + "ValidateUnrealStaticMeshName": { + "enabled": false, + "optional": true, + "active": true + }, "ValidateWorkfilePaths": { "enabled": true, "optional": true, @@ -117,31 +172,6 @@ "$HIP", "$JOB" ] - }, - "ValidateReviewColorspace": { - "enabled": true, - "optional": true, - "active": true - }, - "ValidateContainers": { - "enabled": true, - "optional": true, - "active": true - }, - "ValidateSubsetName": { - "enabled": true, - "optional": true, - "active": true - }, - "ValidateMeshIsStatic": { - "enabled": true, - "optional": true, - "active": true - }, - "ValidateUnrealStaticMeshName": { - "enabled": false, - "optional": true, - "active": true } } } 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 cd8c260124..f37738c4ec 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 @@ -4,6 +4,16 @@ "key": "create", "label": "Creator plugins", "children": [ + { + "type": "schema_template", + "name": "template_create_plugin", + "template_data": [ + { + "key": "CreateAlembicCamera", + "label": "Create Alembic Camera" + } + ] + }, { "type": "dict", "collapsible": true, @@ -39,6 +49,52 @@ ] }, + { + "type": "schema_template", + "name": "template_create_plugin", + "template_data": [ + { + "key": "CreateArnoldRop", + "label": "Create Arnold ROP" + }, + { + "key": "CreateCompositeSequence", + "label": "Create Composite (Image Sequence)" + }, + { + "key": "CreateHDA", + "label": "Create Houdini Digital Asset" + }, + { + "key": "CreateKarmaROP", + "label": "Create Karma ROP" + }, + { + "key": "CreateMantraROP", + "label": "Create Mantra ROP" + }, + { + "key": "CreatePointCache", + "label": "Create PointCache (Abc)" + }, + { + "key": "CreateBGEO", + "label": "Create PointCache (Bgeo)" + }, + { + "key": "CreateRedshiftProxy", + "label": "Create Redshift Proxy" + }, + { + "key": "CreateRedshiftROP", + "label": "Create Redshift ROP" + }, + { + "key": "CreateReview", + "label": "Create Review" + } + ] + }, { "type": "dict", "collapsible": true, @@ -75,44 +131,20 @@ "name": "template_create_plugin", "template_data": [ { - "key": "CreateAlembicCamera", - "label": "Create Alembic Camera" + "key": "CreateUSD", + "label": "Create USD (experimental)" }, { - "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": "CreateUSDRender", + "label": "Create USD render (experimental)" }, { "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" + "key": "CreateVrayROP", + "label": "Create VRay ROP" } ] } 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 d5f70b0312..e202e7b615 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 @@ -4,6 +4,36 @@ "key": "publish", "label": "Publish plugins", "children": [ + { + "type": "label", + "label": "Validators" + }, + { + "type": "schema_template", + "name": "template_publish_plugin", + "template_data": [ + { + "key": "ValidateContainers", + "label": "Validate Containers" + }, + { + "key": "ValidateMeshIsStatic", + "label": "Validate Mesh is Static" + }, + { + "key": "ValidateReviewColorspace", + "label": "Validate Review Colorspace" + }, + { + "key": "ValidateSubsetName", + "label": "Validate Subset Name" + }, + { + "key": "ValidateUnrealStaticMeshName", + "label": "Validate Unreal Static Mesh Name" + } + ] + }, { "type": "dict", "collapsible": true, @@ -35,32 +65,6 @@ "object_type": "text" } ] - }, - { - "type": "schema_template", - "name": "template_publish_plugin", - "template_data": [ - { - "key": "ValidateReviewColorspace", - "label": "Validate Review Colorspace" - }, - { - "key": "ValidateContainers", - "label": "ValidateContainers" - }, - { - "key": "ValidateSubsetName", - "label": "Validate Subset Name" - }, - { - "key": "ValidateMeshIsStatic", - "label": "Validate Mesh is Static" - }, - { - "key": "ValidateUnrealStaticMeshName", - "label": "Validate Unreal Static Mesh Name" - } - ] } ] } From 0953dc65cc915bd78cd1b37ba4305ce4fc705aa8 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 12 Oct 2023 17:16:35 +0200 Subject: [PATCH 0292/1233] utilization of already created action in nuke api openpype.hosts.nuke.api.action.SelectInvalidAction --- openpype/hosts/nuke/api/__init__.py | 6 ++- openpype/hosts/nuke/api/actions.py | 38 +++++++++---------- .../plugins/publish/validate_asset_context.py | 38 ++----------------- 3 files changed, 26 insertions(+), 56 deletions(-) diff --git a/openpype/hosts/nuke/api/__init__.py b/openpype/hosts/nuke/api/__init__.py index 1af5ff365d..a01f5bda0a 100644 --- a/openpype/hosts/nuke/api/__init__.py +++ b/openpype/hosts/nuke/api/__init__.py @@ -50,6 +50,8 @@ from .utils import ( get_colorspace_list ) +from .actions import SelectInvalidAction + __all__ = ( "file_extensions", "has_unsaved_changes", @@ -92,5 +94,7 @@ __all__ = ( "create_write_node", "colorspace_exists_on_node", - "get_colorspace_list" + "get_colorspace_list", + + "SelectInvalidAction", ) diff --git a/openpype/hosts/nuke/api/actions.py b/openpype/hosts/nuke/api/actions.py index c955a85acc..ca3c8393ed 100644 --- a/openpype/hosts/nuke/api/actions.py +++ b/openpype/hosts/nuke/api/actions.py @@ -20,33 +20,31 @@ class SelectInvalidAction(pyblish.api.Action): def process(self, context, plugin): - try: - import nuke - except ImportError: - raise ImportError("Current host is not Nuke") - - errored_instances = get_errored_instances_from_context(context, - plugin=plugin) + # Get the errored instances for the plug-in + errored_instances = get_errored_instances_from_context( + context, plugin) # Get the invalid nodes for the plug-ins self.log.info("Finding invalid nodes..") - invalid = list() + invalid_nodes = set() for instance in errored_instances: - invalid_nodes = plugin.get_invalid(instance) + invalid = plugin.get_invalid(instance) - if invalid_nodes: - if isinstance(invalid_nodes, (list, tuple)): - invalid.append(invalid_nodes[0]) - else: - self.log.warning("Plug-in returned to be invalid, " - "but has no selectable nodes.") + if not invalid: + continue - # Ensure unique (process each node only once) - invalid = list(set(invalid)) + select_node = instance.data.get("transientData", {}).get("node") + if not select_node: + raise RuntimeError( + "No transientData['node'] found on instance: {}".format( + instance) + ) - if invalid: - self.log.info("Selecting invalid nodes: {}".format(invalid)) + invalid_nodes.add(select_node) + + if invalid_nodes: + self.log.info("Selecting invalid nodes: {}".format(invalid_nodes)) reset_selection() - select_nodes(invalid) + select_nodes(list(invalid_nodes)) else: self.log.info("No invalid nodes found.") diff --git a/openpype/hosts/nuke/plugins/publish/validate_asset_context.py b/openpype/hosts/nuke/plugins/publish/validate_asset_context.py index 09cb5102a5..aa96846799 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_asset_context.py +++ b/openpype/hosts/nuke/plugins/publish/validate_asset_context.py @@ -4,34 +4,13 @@ from __future__ import absolute_import import pyblish.api -import openpype.hosts.nuke.api.lib as nlib - from openpype.pipeline.publish import ( RepairAction, ValidateContentsOrder, PublishXmlValidationError, - OptionalPyblishPluginMixin, - get_errored_instances_from_context + OptionalPyblishPluginMixin ) - - -class SelectInvalidNodesAction(pyblish.api.Action): - """Select invalid nodes.""" - - label = "Select Failed Node" - icon = "briefcase" - on = "failed" - - def process(self, context, plugin): - if not hasattr(plugin, "select"): - raise RuntimeError("Plug-in does not have select method.") - - # Get the failed instances - self.log.debug("Finding failed plug-ins..") - failed_instance = get_errored_instances_from_context(context, plugin) - if failed_instance: - self.log.debug("Attempting selection ...") - plugin.select(failed_instance.pop()) +from openpype.hosts.nuke.api import SelectInvalidAction class ValidateCorrectAssetContext( @@ -51,7 +30,7 @@ class ValidateCorrectAssetContext( hosts = ["nuke"] actions = [ RepairAction, - SelectInvalidNodesAction, + SelectInvalidAction ] optional = True @@ -133,14 +112,3 @@ class ValidateCorrectAssetContext( created_instance[_key] = instance.context.data[_key] create_context.save_changes() - - @classmethod - def select(cls, instance): - """Select invalid node """ - invalid = cls.get_invalid(instance) - if not invalid: - return - - select_node = instance.data["transientData"]["node"] - nlib.reset_selection() - select_node["selected"].setValue(True) From 4c90065f43930c35e1ad37712b0ec74d49a0b4d2 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 12 Oct 2023 17:22:51 +0200 Subject: [PATCH 0293/1233] apply setting fix so it works in deprecated and new configuration --- .../nuke/plugins/publish/validate_asset_context.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/validate_asset_context.py b/openpype/hosts/nuke/plugins/publish/validate_asset_context.py index aa96846799..384cfab7b2 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_asset_context.py +++ b/openpype/hosts/nuke/plugins/publish/validate_asset_context.py @@ -39,13 +39,14 @@ class ValidateCorrectAssetContext( """Apply deprecated settings from project settings. """ nuke_publish = project_settings["nuke"]["publish"] - if "ValidateCorrectAssetName" not in nuke_publish: - return + if "ValidateCorrectAssetName" in nuke_publish: + settings = nuke_publish["ValidateCorrectAssetName"] + else: + settings = nuke_publish["ValidateCorrectAssetContext"] - deprecated_setting = nuke_publish["ValidateCorrectAssetName"] - cls.enabled = deprecated_setting["enabled"] - cls.optional = deprecated_setting["optional"] - cls.active = deprecated_setting["active"] + cls.enabled = settings["enabled"] + cls.optional = settings["optional"] + cls.active = settings["active"] def process(self, instance): if not self.is_active(instance.data): From ecf144993fb48c0e6d9d8e7c8c0512f805abafe6 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 12 Oct 2023 17:33:58 +0200 Subject: [PATCH 0294/1233] optimisation of validator and xml message --- .../publish/help/validate_asset_context.xml | 26 ++++++++++++----- .../plugins/publish/validate_asset_context.py | 29 +++++++++---------- 2 files changed, 31 insertions(+), 24 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/help/validate_asset_context.xml b/openpype/hosts/nuke/plugins/publish/help/validate_asset_context.xml index 85efef799a..6d3a9724db 100644 --- a/openpype/hosts/nuke/plugins/publish/help/validate_asset_context.xml +++ b/openpype/hosts/nuke/plugins/publish/help/validate_asset_context.xml @@ -3,19 +3,29 @@ Shot/Asset name -## Invalid node context keys and values +## Publishing to a different asset context -Following Node with name: \`{node_name}\` +There are publish instances present which are publishing into a different asset than your current context. -Context keys and values: \`{correct_values}\` +Usually this is not what you want but there can be cases where you might want to publish into another asset/shot or task. -Wrong keys and values: \`{wrong_values}\`. +If that's the case you can disable the validation on the instance to ignore it. -### How to repair? +Following Node with name is wrong: \`{node_name}\` -1. Either use Repair or Select button. -2. If you chose Select then rename asset knob to correct name. -3. Hit Reload button on the publisher. +### Correct context keys and values: + +\`{correct_values}\` + +### Wrong keys and values: + +\`{wrong_values}\`. + + +## How to repair? + +1. Use \"Repair\" button. +2. Hit Reload button on the publisher. diff --git a/openpype/hosts/nuke/plugins/publish/validate_asset_context.py b/openpype/hosts/nuke/plugins/publish/validate_asset_context.py index 384cfab7b2..ab62daeaeb 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_asset_context.py +++ b/openpype/hosts/nuke/plugins/publish/validate_asset_context.py @@ -52,7 +52,7 @@ class ValidateCorrectAssetContext( if not self.is_active(instance.data): return - invalid_keys = self.get_invalid(instance, compute=True) + invalid_keys = self.get_invalid(instance) if not invalid_keys: return @@ -81,27 +81,24 @@ class ValidateCorrectAssetContext( ) @classmethod - def get_invalid(cls, instance, compute=False): + def get_invalid(cls, instance): """Get invalid keys from instance data and context data.""" - invalid = instance.data.get("invalid_keys", []) - if compute: - testing_keys = ["asset", "task"] - for _key in testing_keys: - if _key not in instance.data: - invalid.append(_key) - continue - if instance.data[_key] != instance.context.data[_key]: - invalid.append(_key) + invalid_keys = [] + testing_keys = ["asset", "task"] + for _key in testing_keys: + if _key not in instance.data: + invalid_keys.append(_key) + continue + if instance.data[_key] != instance.context.data[_key]: + invalid_keys.append(_key) - instance.data["invalid_keys"] = invalid - - return invalid + return invalid_keys @classmethod def repair(cls, instance): """Repair instance data with context data.""" - invalid = cls.get_invalid(instance) + invalid_keys = cls.get_invalid(instance) create_context = instance.context.data["create_context"] @@ -109,7 +106,7 @@ class ValidateCorrectAssetContext( created_instance = create_context.get_instance_by_id( instance_id ) - for _key in invalid: + for _key in invalid_keys: created_instance[_key] = instance.context.data[_key] create_context.save_changes() From 21de693c17adaeebdf4ac30cd198347474a7efa2 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 12 Oct 2023 17:07:47 +0100 Subject: [PATCH 0295/1233] Implemented inventory plugin to update actors in current level --- openpype/hosts/unreal/api/pipeline.py | 4 ++ .../unreal/plugins/inventory/update_actors.py | 60 +++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 openpype/hosts/unreal/plugins/inventory/update_actors.py diff --git a/openpype/hosts/unreal/api/pipeline.py b/openpype/hosts/unreal/api/pipeline.py index 2893550325..60b4886e4f 100644 --- a/openpype/hosts/unreal/api/pipeline.py +++ b/openpype/hosts/unreal/api/pipeline.py @@ -13,8 +13,10 @@ from openpype.client import get_asset_by_name, get_assets from openpype.pipeline import ( register_loader_plugin_path, register_creator_plugin_path, + register_inventory_action_path, deregister_loader_plugin_path, deregister_creator_plugin_path, + deregister_inventory_action_path, AYON_CONTAINER_ID, legacy_io, ) @@ -127,6 +129,7 @@ def install(): pyblish.api.register_plugin_path(str(PUBLISH_PATH)) register_loader_plugin_path(str(LOAD_PATH)) register_creator_plugin_path(str(CREATE_PATH)) + register_inventory_action_path(str(INVENTORY_PATH)) _register_callbacks() _register_events() @@ -136,6 +139,7 @@ def uninstall(): pyblish.api.deregister_plugin_path(str(PUBLISH_PATH)) deregister_loader_plugin_path(str(LOAD_PATH)) deregister_creator_plugin_path(str(CREATE_PATH)) + deregister_inventory_action_path(str(INVENTORY_PATH)) def _register_callbacks(): diff --git a/openpype/hosts/unreal/plugins/inventory/update_actors.py b/openpype/hosts/unreal/plugins/inventory/update_actors.py new file mode 100644 index 0000000000..672bb2b32e --- /dev/null +++ b/openpype/hosts/unreal/plugins/inventory/update_actors.py @@ -0,0 +1,60 @@ +import unreal + +from openpype.hosts.unreal.api.pipeline import ( + ls, + replace_static_mesh_actors, + replace_skeletal_mesh_actors, + delete_previous_asset_if_unused, +) +from openpype.pipeline import InventoryAction + + +class UpdateActors(InventoryAction): + """Update Actors in level to this version. + """ + + label = "Update Actors in level to this version" + icon = "arrow-up" + + def process(self, containers): + allowed_families = ["model", "rig"] + + # Get all the containers in the Unreal Project + all_containers = ls() + + for container in containers: + container_dir = container.get("namespace") + if container.get("family") not in allowed_families: + unreal.log_warning( + f"Container {container_dir} is not supported.") + continue + + # Get all containers with same asset_name but different objectName. + # These are the containers that need to be updated in the level. + sa_containers = [ + i + for i in all_containers + if ( + i.get("asset_name") == container.get("asset_name") and + i.get("objectName") != container.get("objectName") + ) + ] + + asset_content = unreal.EditorAssetLibrary.list_assets( + container_dir, recursive=True, include_folder=False + ) + + # Update all actors in level + for sa_cont in sa_containers: + sa_dir = sa_cont.get("namespace") + old_content = unreal.EditorAssetLibrary.list_assets( + sa_dir, recursive=True, include_folder=False + ) + + if container.get("family") == "rig": + replace_skeletal_mesh_actors(old_content, asset_content) + replace_static_mesh_actors(old_content, asset_content) + + unreal.EditorLevelLibrary.save_current_level() + + delete_previous_asset_if_unused(sa_cont, old_content) From a6aada9c13cf97c70b4538fdb3c09afb33d960f9 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 12 Oct 2023 17:22:52 +0100 Subject: [PATCH 0296/1233] Fixed issue with the updater for GeometryCaches --- openpype/hosts/unreal/api/pipeline.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/unreal/api/pipeline.py b/openpype/hosts/unreal/api/pipeline.py index 60b4886e4f..760e052a3e 100644 --- a/openpype/hosts/unreal/api/pipeline.py +++ b/openpype/hosts/unreal/api/pipeline.py @@ -722,8 +722,8 @@ def replace_skeletal_mesh_actors(old_assets, new_assets): def replace_geometry_cache_actors(old_assets, new_assets): geometry_cache_comps, old_caches, new_caches = _get_comps_and_assets( - unreal.SkeletalMeshComponent, - unreal.SkeletalMesh, + unreal.GeometryCacheComponent, + unreal.GeometryCache, old_assets, new_assets ) From 5fa8a6c4eae338064d8c028fb4b7342ec9868fc2 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 12 Oct 2023 17:23:14 +0100 Subject: [PATCH 0297/1233] Added support for GeometryCaches to the new inventory plugin --- openpype/hosts/unreal/plugins/inventory/update_actors.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/plugins/inventory/update_actors.py b/openpype/hosts/unreal/plugins/inventory/update_actors.py index 672bb2b32e..37777114e2 100644 --- a/openpype/hosts/unreal/plugins/inventory/update_actors.py +++ b/openpype/hosts/unreal/plugins/inventory/update_actors.py @@ -4,6 +4,7 @@ from openpype.hosts.unreal.api.pipeline import ( ls, replace_static_mesh_actors, replace_skeletal_mesh_actors, + replace_geometry_cache_actors, delete_previous_asset_if_unused, ) from openpype.pipeline import InventoryAction @@ -53,7 +54,13 @@ class UpdateActors(InventoryAction): if container.get("family") == "rig": replace_skeletal_mesh_actors(old_content, asset_content) - replace_static_mesh_actors(old_content, asset_content) + replace_static_mesh_actors(old_content, asset_content) + elif container.get("family") == "model": + if container.get("loader") == "PointCacheAlembicLoader": + replace_geometry_cache_actors( + old_content, asset_content) + else: + replace_static_mesh_actors(old_content, asset_content) unreal.EditorLevelLibrary.save_current_level() From 1a492757fa454fbf12aace9dcede1fc16d3ccf0a Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 12 Oct 2023 17:25:57 +0100 Subject: [PATCH 0298/1233] Updating assets no longer updates actors in current level --- .../unreal/plugins/load/load_geometrycache_abc.py | 10 ---------- .../unreal/plugins/load/load_skeletalmesh_abc.py | 11 ----------- .../unreal/plugins/load/load_skeletalmesh_fbx.py | 11 ----------- .../hosts/unreal/plugins/load/load_staticmesh_abc.py | 10 ---------- .../hosts/unreal/plugins/load/load_staticmesh_fbx.py | 10 ---------- 5 files changed, 52 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py b/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py index 8ac2656bd7..3e128623a6 100644 --- a/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py +++ b/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py @@ -207,16 +207,6 @@ class PointCacheAlembicLoader(plugin.Loader): for a in asset_content: unreal.EditorAssetLibrary.save_asset(a) - old_assets = unreal.EditorAssetLibrary.list_assets( - container["namespace"], recursive=True, include_folder=False - ) - - replace_geometry_cache_actors(old_assets, asset_content) - - unreal.EditorLevelLibrary.save_current_level() - - delete_previous_asset_if_unused(container, old_assets) - def remove(self, container): path = container["namespace"] parent_path = os.path.dirname(path) diff --git a/openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py b/openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py index 2e6557bd2d..b7c09fc02b 100644 --- a/openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py +++ b/openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py @@ -182,17 +182,6 @@ class SkeletalMeshAlembicLoader(plugin.Loader): for a in asset_content: unreal.EditorAssetLibrary.save_asset(a) - old_assets = unreal.EditorAssetLibrary.list_assets( - container["namespace"], recursive=True, include_folder=False - ) - - replace_static_mesh_actors(old_assets, asset_content) - replace_skeletal_mesh_actors(old_assets, asset_content) - - unreal.EditorLevelLibrary.save_current_level() - - delete_previous_asset_if_unused(container, old_assets) - def remove(self, container): path = container["namespace"] parent_path = os.path.dirname(path) diff --git a/openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py b/openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py index 3c84f36399..112eba51ff 100644 --- a/openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py +++ b/openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py @@ -184,17 +184,6 @@ class SkeletalMeshFBXLoader(plugin.Loader): for a in asset_content: unreal.EditorAssetLibrary.save_asset(a) - old_assets = unreal.EditorAssetLibrary.list_assets( - container["namespace"], recursive=True, include_folder=False - ) - - replace_static_mesh_actors(old_assets, asset_content) - replace_skeletal_mesh_actors(old_assets, asset_content) - - unreal.EditorLevelLibrary.save_current_level() - - delete_previous_asset_if_unused(container, old_assets) - def remove(self, container): path = container["namespace"] parent_path = os.path.dirname(path) diff --git a/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py b/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py index cc7aed7b93..af098b3ec9 100644 --- a/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py +++ b/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py @@ -182,16 +182,6 @@ class StaticMeshAlembicLoader(plugin.Loader): for a in asset_content: unreal.EditorAssetLibrary.save_asset(a) - old_assets = unreal.EditorAssetLibrary.list_assets( - container["namespace"], recursive=True, include_folder=False - ) - - replace_static_mesh_actors(old_assets, asset_content) - - unreal.EditorLevelLibrary.save_current_level() - - delete_previous_asset_if_unused(container, old_assets) - def remove(self, container): path = container["namespace"] parent_path = os.path.dirname(path) diff --git a/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py b/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py index 0aac69b57b..4b20bb485c 100644 --- a/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py +++ b/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py @@ -171,16 +171,6 @@ class StaticMeshFBXLoader(plugin.Loader): for a in asset_content: unreal.EditorAssetLibrary.save_asset(a) - old_assets = unreal.EditorAssetLibrary.list_assets( - container["namespace"], recursive=True, include_folder=False - ) - - replace_static_mesh_actors(old_assets, asset_content) - - unreal.EditorLevelLibrary.save_current_level() - - delete_previous_asset_if_unused(container, old_assets) - def remove(self, container): path = container["namespace"] parent_path = os.path.dirname(path) From 27319255417e8865a54f1cfd68ac61e577c35340 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 12 Oct 2023 17:27:23 +0100 Subject: [PATCH 0299/1233] Hound fixes --- openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py | 2 -- openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py | 3 --- openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py | 3 --- openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py | 2 -- openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py | 2 -- 5 files changed, 12 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py b/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py index 3e128623a6..e64a5654a1 100644 --- a/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py +++ b/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py @@ -10,8 +10,6 @@ from openpype.hosts.unreal.api import plugin from openpype.hosts.unreal.api.pipeline import ( create_container, imprint, - replace_geometry_cache_actors, - delete_previous_asset_if_unused, ) import unreal # noqa diff --git a/openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py b/openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py index b7c09fc02b..03695bb93b 100644 --- a/openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py +++ b/openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py @@ -10,9 +10,6 @@ from openpype.hosts.unreal.api import plugin from openpype.hosts.unreal.api.pipeline import ( create_container, imprint, - replace_static_mesh_actors, - replace_skeletal_mesh_actors, - delete_previous_asset_if_unused, ) import unreal # noqa diff --git a/openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py b/openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py index 112eba51ff..7640ecfa9e 100644 --- a/openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py +++ b/openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py @@ -10,9 +10,6 @@ from openpype.hosts.unreal.api import plugin from openpype.hosts.unreal.api.pipeline import ( create_container, imprint, - replace_static_mesh_actors, - replace_skeletal_mesh_actors, - delete_previous_asset_if_unused, ) import unreal # noqa diff --git a/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py b/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py index af098b3ec9..a3ea2a2231 100644 --- a/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py +++ b/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py @@ -10,8 +10,6 @@ from openpype.hosts.unreal.api import plugin from openpype.hosts.unreal.api.pipeline import ( create_container, imprint, - replace_static_mesh_actors, - delete_previous_asset_if_unused, ) import unreal # noqa diff --git a/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py b/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py index 4b20bb485c..44d7ca631e 100644 --- a/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py +++ b/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py @@ -10,8 +10,6 @@ from openpype.hosts.unreal.api import plugin from openpype.hosts.unreal.api.pipeline import ( create_container, imprint, - replace_static_mesh_actors, - delete_previous_asset_if_unused, ) import unreal # noqa From 873c263587703db10872bbb16536413628cbfdc2 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 12 Oct 2023 21:15:23 +0300 Subject: [PATCH 0300/1233] add a TODO in shelves manager --- server_addon/houdini/server/settings/shelves.py | 1 + 1 file changed, 1 insertion(+) diff --git a/server_addon/houdini/server/settings/shelves.py b/server_addon/houdini/server/settings/shelves.py index 2319357f59..c8bda515f9 100644 --- a/server_addon/houdini/server/settings/shelves.py +++ b/server_addon/houdini/server/settings/shelves.py @@ -9,6 +9,7 @@ from ayon_server.settings import ( class ShelfToolsModel(BaseSettingsModel): name: str = Field(title="Name") help: str = Field(title="Help text") + # TODO: The following settings are not compatible with OP script: MultiplatformPathModel = Field( default_factory=MultiplatformPathModel, title="Script Path " From dcfad64320085041cf6b91577b3de605acde1f02 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 13 Oct 2023 16:32:34 +0800 Subject: [PATCH 0301/1233] add families with frame range back to the frame range validator --- openpype/hosts/max/plugins/publish/validate_frame_range.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/validate_frame_range.py b/openpype/hosts/max/plugins/publish/validate_frame_range.py index 21e847405e..43692d0401 100644 --- a/openpype/hosts/max/plugins/publish/validate_frame_range.py +++ b/openpype/hosts/max/plugins/publish/validate_frame_range.py @@ -27,7 +27,9 @@ class ValidateFrameRange(pyblish.api.InstancePlugin, label = "Validate Frame Range" order = ValidateContentsOrder - families = ["maxrender"] + families = ["camera", "maxrender", + "pointcache", "pointcloud", + "review", "redshiftproxy"] hosts = ["max"] optional = True actions = [RepairAction] From 9c53837c334acefb792972e3b4a28b414707583d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 13 Oct 2023 10:40:41 +0200 Subject: [PATCH 0302/1233] AYON: Tools enhancements (#5753) * moved universal 'TreeView' to utils * use 'TreeView' in folders widget * propagate 'set_deselectable' in 'FoldersWidget' * propagate more public functionality of 'FoldersWidget' * fix 'set_name_filer' typo * rename 'get_current_project_name' to 'get_selected_project_name' * added signals to task and project widgets * implemented more helper methods in hierarchy model * added more information to 'FolderItem' * add empty line after docstring * fix expected selection of folders in loader * keep only 'double_clicked' signal * pass full mouse event to signal --- .../tools/ayon_launcher/ui/hierarchy_page.py | 2 +- .../tools/ayon_loader/ui/folders_widget.py | 19 +- .../tools/ayon_loader/ui/products_widget.py | 2 +- openpype/tools/ayon_loader/ui/window.py | 8 +- openpype/tools/ayon_utils/models/hierarchy.py | 107 ++++++++++- .../ayon_utils/widgets/folders_widget.py | 172 +++++++++++++++--- .../ayon_utils/widgets/projects_widget.py | 8 +- .../tools/ayon_utils/widgets/tasks_widget.py | 5 + .../widgets/files_widget_published.py | 10 +- .../widgets/files_widget_workarea.py | 10 +- .../ayon_workfiles/widgets/folders_widget.py | 2 +- .../tools/ayon_workfiles/widgets/utils.py | 66 ------- .../tools/ayon_workfiles/widgets/window.py | 2 +- openpype/tools/utils/__init__.py | 6 +- openpype/tools/utils/views.py | 62 +++++++ 15 files changed, 350 insertions(+), 131 deletions(-) diff --git a/openpype/tools/ayon_launcher/ui/hierarchy_page.py b/openpype/tools/ayon_launcher/ui/hierarchy_page.py index 8c546b38ac..d56d43fdec 100644 --- a/openpype/tools/ayon_launcher/ui/hierarchy_page.py +++ b/openpype/tools/ayon_launcher/ui/hierarchy_page.py @@ -103,4 +103,4 @@ class HierarchyPage(QtWidgets.QWidget): self._controller.refresh() def _on_filter_text_changed(self, text): - self._folders_widget.set_name_filer(text) + self._folders_widget.set_name_filter(text) diff --git a/openpype/tools/ayon_loader/ui/folders_widget.py b/openpype/tools/ayon_loader/ui/folders_widget.py index b911458546..53351f76d9 100644 --- a/openpype/tools/ayon_loader/ui/folders_widget.py +++ b/openpype/tools/ayon_loader/ui/folders_widget.py @@ -11,14 +11,14 @@ from openpype.tools.ayon_utils.widgets import ( FoldersModel, FOLDERS_MODEL_SENDER_NAME, ) -from openpype.tools.ayon_utils.widgets.folders_widget import ITEM_ID_ROLE +from openpype.tools.ayon_utils.widgets.folders_widget import FOLDER_ID_ROLE if qtpy.API == "pyside": from PySide.QtGui import QStyleOptionViewItemV4 elif qtpy.API == "pyqt4": from PyQt4.QtGui import QStyleOptionViewItemV4 -UNDERLINE_COLORS_ROLE = QtCore.Qt.UserRole + 4 +UNDERLINE_COLORS_ROLE = QtCore.Qt.UserRole + 50 class UnderlinesFolderDelegate(QtWidgets.QItemDelegate): @@ -257,13 +257,11 @@ class LoaderFoldersWidget(QtWidgets.QWidget): Args: controller (AbstractWorkfilesFrontend): The control object. parent (QtWidgets.QWidget): The parent widget. - handle_expected_selection (bool): If True, the widget will handle - the expected selection. Defaults to False. """ refreshed = QtCore.Signal() - def __init__(self, controller, parent, handle_expected_selection=False): + def __init__(self, controller, parent): super(LoaderFoldersWidget, self).__init__(parent) folders_view = DeselectableTreeView(self) @@ -313,10 +311,9 @@ class LoaderFoldersWidget(QtWidgets.QWidget): self._folders_proxy_model = folders_proxy_model self._folders_label_delegate = folders_label_delegate - self._handle_expected_selection = handle_expected_selection self._expected_selection = None - def set_name_filer(self, name): + def set_name_filter(self, name): """Set filter of folder name. Args: @@ -365,7 +362,7 @@ class LoaderFoldersWidget(QtWidgets.QWidget): selection_model = self._folders_view.selectionModel() item_ids = [] for index in selection_model.selectedIndexes(): - item_id = index.data(ITEM_ID_ROLE) + item_id = index.data(FOLDER_ID_ROLE) if item_id is not None: item_ids.append(item_id) return item_ids @@ -379,9 +376,6 @@ class LoaderFoldersWidget(QtWidgets.QWidget): self._update_expected_selection(event.data) def _update_expected_selection(self, expected_data=None): - if not self._handle_expected_selection: - return - if expected_data is None: expected_data = self._controller.get_expected_selection_data() @@ -395,9 +389,6 @@ class LoaderFoldersWidget(QtWidgets.QWidget): self._set_expected_selection() def _set_expected_selection(self): - if not self._handle_expected_selection: - return - folder_id = self._expected_selection selected_ids = self._get_selected_item_ids() self._expected_selection = None diff --git a/openpype/tools/ayon_loader/ui/products_widget.py b/openpype/tools/ayon_loader/ui/products_widget.py index cfc18431a6..2d4959dc19 100644 --- a/openpype/tools/ayon_loader/ui/products_widget.py +++ b/openpype/tools/ayon_loader/ui/products_widget.py @@ -183,7 +183,7 @@ class ProductsWidget(QtWidgets.QWidget): not controller.is_loaded_products_supported() ) - def set_name_filer(self, name): + def set_name_filter(self, name): """Set filter of product name. Args: diff --git a/openpype/tools/ayon_loader/ui/window.py b/openpype/tools/ayon_loader/ui/window.py index ca17e4b9fd..a6d40d52e7 100644 --- a/openpype/tools/ayon_loader/ui/window.py +++ b/openpype/tools/ayon_loader/ui/window.py @@ -382,7 +382,7 @@ class LoaderWindow(QtWidgets.QWidget): self._controller.reset() def _show_group_dialog(self): - project_name = self._projects_combobox.get_current_project_name() + project_name = self._projects_combobox.get_selected_project_name() if not project_name: return @@ -397,7 +397,7 @@ class LoaderWindow(QtWidgets.QWidget): self._group_dialog.show() def _on_folder_filter_change(self, text): - self._folders_widget.set_name_filer(text) + self._folders_widget.set_name_filter(text) def _on_product_group_change(self): self._products_widget.set_enable_grouping( @@ -405,7 +405,7 @@ class LoaderWindow(QtWidgets.QWidget): ) def _on_product_filter_change(self, text): - self._products_widget.set_name_filer(text) + self._products_widget.set_name_filter(text) def _on_product_type_filter_change(self): self._products_widget.set_product_type_filter( @@ -419,7 +419,7 @@ class LoaderWindow(QtWidgets.QWidget): def _on_products_selection_change(self): items = self._products_widget.get_selected_version_info() self._info_widget.set_selected_version_info( - self._projects_combobox.get_current_project_name(), + self._projects_combobox.get_selected_project_name(), items ) diff --git a/openpype/tools/ayon_utils/models/hierarchy.py b/openpype/tools/ayon_utils/models/hierarchy.py index 6c30d22f3a..fc6b8e1eb7 100644 --- a/openpype/tools/ayon_utils/models/hierarchy.py +++ b/openpype/tools/ayon_utils/models/hierarchy.py @@ -29,16 +29,21 @@ class FolderItem: parent_id (Union[str, None]): Parent folder id. If 'None' then project is parent. name (str): Name of folder. + path (str): Folder path. + folder_type (str): Type of folder. label (Union[str, None]): Folder label. icon (Union[dict[str, Any], None]): Icon definition. """ def __init__( - self, entity_id, parent_id, name, label, icon + self, entity_id, parent_id, name, path, folder_type, label, icon ): self.entity_id = entity_id self.parent_id = parent_id self.name = name + self.path = path + self.folder_type = folder_type + self.label = label or name if not icon: icon = { "type": "awesome-font", @@ -46,7 +51,6 @@ class FolderItem: "color": get_default_entity_icon_color() } self.icon = icon - self.label = label or name def to_data(self): """Converts folder item to data. @@ -59,6 +63,8 @@ class FolderItem: "entity_id": self.entity_id, "parent_id": self.parent_id, "name": self.name, + "path": self.path, + "folder_type": self.folder_type, "label": self.label, "icon": self.icon, } @@ -90,8 +96,7 @@ class TaskItem: 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. + icon (Union[dict[str, Any], None]): Icon definitions. """ def __init__( @@ -183,12 +188,31 @@ def _get_task_items_from_tasks(tasks): def _get_folder_item_from_hierarchy_item(item): + name = item["name"] + path_parts = list(item["parents"]) + path_parts.append(name) + return FolderItem( item["id"], item["parentId"], - item["name"], + name, + "/".join(path_parts), + item["folderType"], item["label"], - None + None, + ) + + +def _get_folder_item_from_entity(entity): + name = entity["name"] + return FolderItem( + entity["id"], + entity["parentId"], + name, + entity["path"], + entity["folderType"], + entity["label"] or name, + None, ) @@ -223,13 +247,84 @@ class HierarchyModel(object): self._tasks_by_id.reset() def refresh_project(self, project_name): + """Force to refresh folder items for a project. + + Args: + project_name (str): Name of project to refresh. + """ + self._refresh_folders_cache(project_name) def get_folder_items(self, project_name, sender): + """Get folder items by project name. + + The folders are cached per project name. If the cache is not valid + then the folders are queried from server. + + Args: + project_name (str): Name of project where to look for folders. + sender (Union[str, None]): Who requested the folder ids. + + Returns: + dict[str, FolderItem]: Folder items by id. + """ + if not self._folders_items[project_name].is_valid: self._refresh_folders_cache(project_name, sender) return self._folders_items[project_name].get_data() + def get_folder_items_by_id(self, project_name, folder_ids): + """Get folder items by ids. + + This function will query folders if they are not in cache. But the + queried items are not added to cache back. + + Args: + project_name (str): Name of project where to look for folders. + folder_ids (Iterable[str]): Folder ids. + + Returns: + dict[str, Union[FolderItem, None]]: Folder items by id. + """ + + folder_ids = set(folder_ids) + if self._folders_items[project_name].is_valid: + cache_data = self._folders_items[project_name].get_data() + return { + folder_id: cache_data.get(folder_id) + for folder_id in folder_ids + } + folders = ayon_api.get_folders( + project_name, + folder_ids=folder_ids, + fields=["id", "name", "label", "parentId", "path", "folderType"] + ) + # Make sure all folder ids are in output + output = {folder_id: None for folder_id in folder_ids} + output.update({ + folder["id"]: _get_folder_item_from_entity(folder) + for folder in folders + }) + return output + + def get_folder_item(self, project_name, folder_id): + """Get folder items by id. + + This function will query folder if they is not in cache. But the + queried items are not added to cache back. + + Args: + project_name (str): Name of project where to look for folders. + folder_id (str): Folder id. + + Returns: + Union[FolderItem, None]: Folder item. + """ + items = self.get_folder_items_by_id( + project_name, [folder_id] + ) + return items.get(folder_id) + def get_task_items(self, project_name, folder_id, sender): if not project_name or not folder_id: return [] diff --git a/openpype/tools/ayon_utils/widgets/folders_widget.py b/openpype/tools/ayon_utils/widgets/folders_widget.py index b57ffb126a..322553c51c 100644 --- a/openpype/tools/ayon_utils/widgets/folders_widget.py +++ b/openpype/tools/ayon_utils/widgets/folders_widget.py @@ -4,14 +4,16 @@ from qtpy import QtWidgets, QtGui, QtCore from openpype.tools.utils import ( RecursiveSortFilterProxyModel, - DeselectableTreeView, + TreeView, ) from .utils import RefreshThread, get_qt_icon FOLDERS_MODEL_SENDER_NAME = "qt_folders_model" -ITEM_ID_ROLE = QtCore.Qt.UserRole + 1 -ITEM_NAME_ROLE = QtCore.Qt.UserRole + 2 +FOLDER_ID_ROLE = QtCore.Qt.UserRole + 1 +FOLDER_NAME_ROLE = QtCore.Qt.UserRole + 2 +FOLDER_PATH_ROLE = QtCore.Qt.UserRole + 3 +FOLDER_TYPE_ROLE = QtCore.Qt.UserRole + 4 class FoldersModel(QtGui.QStandardItemModel): @@ -84,6 +86,15 @@ class FoldersModel(QtGui.QStandardItemModel): return QtCore.QModelIndex() return self.indexFromItem(item) + def get_project_name(self): + """Project name which model currently use. + + Returns: + Union[str, None]: Currently used project name. + """ + + return self._last_project_name + def set_project_name(self, project_name): """Refresh folders items. @@ -151,12 +162,13 @@ class FoldersModel(QtGui.QStandardItemModel): """ icon = get_qt_icon(folder_item.icon) - item.setData(folder_item.entity_id, ITEM_ID_ROLE) - item.setData(folder_item.name, ITEM_NAME_ROLE) + item.setData(folder_item.entity_id, FOLDER_ID_ROLE) + item.setData(folder_item.name, FOLDER_NAME_ROLE) + item.setData(folder_item.path, FOLDER_PATH_ROLE) + item.setData(folder_item.folder_type, FOLDER_TYPE_ROLE) item.setData(folder_item.label, QtCore.Qt.DisplayRole) item.setData(icon, QtCore.Qt.DecorationRole) - def _fill_items(self, folder_items_by_id): if not folder_items_by_id: if folder_items_by_id is not None: @@ -193,7 +205,7 @@ class FoldersModel(QtGui.QStandardItemModel): folder_ids_to_add = set(folder_items) for row_idx in reversed(range(parent_item.rowCount())): child_item = parent_item.child(row_idx) - child_id = child_item.data(ITEM_ID_ROLE) + child_id = child_item.data(FOLDER_ID_ROLE) if child_id in ids_to_remove: removed_items.append(parent_item.takeRow(row_idx)) else: @@ -259,10 +271,14 @@ class FoldersWidget(QtWidgets.QWidget): the expected selection. Defaults to False. """ + double_clicked = QtCore.Signal(QtGui.QMouseEvent) + selection_changed = QtCore.Signal() + refreshed = QtCore.Signal() + def __init__(self, controller, parent, handle_expected_selection=False): super(FoldersWidget, self).__init__(parent) - folders_view = DeselectableTreeView(self) + folders_view = TreeView(self) folders_view.setHeaderHidden(True) folders_model = FoldersModel(controller) @@ -295,7 +311,7 @@ class FoldersWidget(QtWidgets.QWidget): selection_model = folders_view.selectionModel() selection_model.selectionChanged.connect(self._on_selection_change) - + folders_view.double_clicked.connect(self.double_clicked) folders_model.refreshed.connect(self._on_model_refresh) self._controller = controller @@ -306,7 +322,27 @@ class FoldersWidget(QtWidgets.QWidget): self._handle_expected_selection = handle_expected_selection self._expected_selection = None - def set_name_filer(self, name): + @property + def is_refreshing(self): + """Model is refreshing. + + Returns: + bool: True if model is refreshing. + """ + + return self._folders_model.is_refreshing + + @property + def has_content(self): + """Has at least one folder. + + Returns: + bool: True if model has at least one folder. + """ + + return self._folders_model.has_content + + def set_name_filter(self, name): """Set filter of folder name. Args: @@ -323,16 +359,108 @@ class FoldersWidget(QtWidgets.QWidget): self._folders_model.refresh() + def get_project_name(self): + """Project name in which folders widget currently is. + + Returns: + Union[str, None]: Currently used project name. + """ + + return self._folders_model.get_project_name() + + def set_project_name(self, project_name): + """Set project name. + + Do not use this method when controller is handling selection of + project using 'selection.project.changed' event. + + Args: + project_name (str): Project name. + """ + + self._folders_model.set_project_name(project_name) + + def get_selected_folder_id(self): + """Get selected folder id. + + Returns: + Union[str, None]: Folder id which is selected. + """ + + return self._get_selected_item_id() + + def get_selected_folder_label(self): + """Selected folder label. + + Returns: + Union[str, None]: Selected folder label. + """ + + item_id = self._get_selected_item_id() + return self.get_folder_label(item_id) + + def get_folder_label(self, folder_id): + """Folder label for a given folder id. + + Returns: + Union[str, None]: Folder label. + """ + + index = self._folders_model.get_index_by_id(folder_id) + if index.isValid(): + return index.data(QtCore.Qt.DisplayRole) + return None + + def set_selected_folder(self, folder_id): + """Change selection. + + Args: + folder_id (Union[str, None]): Folder id or None to deselect. + """ + + if folder_id is None: + self._folders_view.clearSelection() + return True + + if folder_id == self._get_selected_item_id(): + return True + index = self._folders_model.get_index_by_id(folder_id) + if not index.isValid(): + return False + + proxy_index = self._folders_proxy_model.mapFromSource(index) + if not proxy_index.isValid(): + return False + + selection_model = self._folders_view.selectionModel() + selection_model.setCurrentIndex( + proxy_index, QtCore.QItemSelectionModel.SelectCurrent + ) + return True + + def set_deselectable(self, enabled): + """Set deselectable mode. + + Items in view can be deselected. + + Args: + enabled (bool): Enable deselectable mode. + """ + + self._folders_view.set_deselectable(enabled) + + def _get_selected_index(self): + return self._folders_model.get_index_by_id( + self.get_selected_folder_id() + ) + def _on_project_selection_change(self, event): project_name = event["project_name"] - self._set_project_name(project_name) - - def _set_project_name(self, project_name): - self._folders_model.set_project_name(project_name) + self.set_project_name(project_name) def _on_folders_refresh_finished(self, event): if event["sender"] != FOLDERS_MODEL_SENDER_NAME: - self._set_project_name(event["project_name"]) + self.set_project_name(event["project_name"]) def _on_controller_refresh(self): self._update_expected_selection() @@ -341,11 +469,12 @@ class FoldersWidget(QtWidgets.QWidget): if self._expected_selection: self._set_expected_selection() self._folders_proxy_model.sort(0) + self.refreshed.emit() 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) + item_id = index.data(FOLDER_ID_ROLE) if item_id is not None: return item_id return None @@ -353,6 +482,7 @@ class FoldersWidget(QtWidgets.QWidget): def _on_selection_change(self): item_id = self._get_selected_item_id() self._controller.set_selected_folder(item_id) + self.selection_changed.emit() # Expected selection handling def _on_expected_selection_change(self, event): @@ -380,12 +510,6 @@ class FoldersWidget(QtWidgets.QWidget): 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) + if folder_id is not None: + self.set_selected_folder(folder_id) self._controller.expected_folder_selected(folder_id) diff --git a/openpype/tools/ayon_utils/widgets/projects_widget.py b/openpype/tools/ayon_utils/widgets/projects_widget.py index 11bb5de51b..be18cfe3ed 100644 --- a/openpype/tools/ayon_utils/widgets/projects_widget.py +++ b/openpype/tools/ayon_utils/widgets/projects_widget.py @@ -395,6 +395,7 @@ class ProjectSortFilterProxy(QtCore.QSortFilterProxyModel): class ProjectsCombobox(QtWidgets.QWidget): refreshed = QtCore.Signal() + selection_changed = QtCore.Signal() def __init__(self, controller, parent, handle_expected_selection=False): super(ProjectsCombobox, self).__init__(parent) @@ -482,7 +483,7 @@ class ProjectsCombobox(QtWidgets.QWidget): self._listen_selection_change = listen - def get_current_project_name(self): + def get_selected_project_name(self): """Name of selected project. Returns: @@ -502,7 +503,7 @@ class ProjectsCombobox(QtWidgets.QWidget): if not self._select_item_visible: return if "project_name" not in kwargs: - project_name = self.get_current_project_name() + project_name = self.get_selected_project_name() else: project_name = kwargs.get("project_name") @@ -536,6 +537,7 @@ class ProjectsCombobox(QtWidgets.QWidget): idx, PROJECT_NAME_ROLE) self._update_select_item_visiblity(project_name=project_name) self._controller.set_selected_project(project_name) + self.selection_changed.emit() def _on_model_refresh(self): self._projects_proxy_model.sort(0) @@ -561,7 +563,7 @@ class ProjectsCombobox(QtWidgets.QWidget): return project_name = self._expected_selection if project_name is not None: - if project_name != self.get_current_project_name(): + if project_name != self.get_selected_project_name(): self.set_selection(project_name) else: # Fake project change diff --git a/openpype/tools/ayon_utils/widgets/tasks_widget.py b/openpype/tools/ayon_utils/widgets/tasks_widget.py index da745bd810..d01b3a7917 100644 --- a/openpype/tools/ayon_utils/widgets/tasks_widget.py +++ b/openpype/tools/ayon_utils/widgets/tasks_widget.py @@ -296,6 +296,9 @@ class TasksWidget(QtWidgets.QWidget): handle_expected_selection (Optional[bool]): Handle expected selection. """ + refreshed = QtCore.Signal() + selection_changed = QtCore.Signal() + def __init__(self, controller, parent, handle_expected_selection=False): super(TasksWidget, self).__init__(parent) @@ -380,6 +383,7 @@ class TasksWidget(QtWidgets.QWidget): if not self._set_expected_selection(): self._on_selection_change() self._tasks_proxy_model.sort(0) + self.refreshed.emit() def _get_selected_item_ids(self): selection_model = self._tasks_view.selectionModel() @@ -400,6 +404,7 @@ class TasksWidget(QtWidgets.QWidget): parent_id, task_id, task_name = self._get_selected_item_ids() self._controller.set_selected_task(task_id, task_name) + self.selection_changed.emit() # Expected selection handling def _on_expected_selection_change(self, event): diff --git a/openpype/tools/ayon_workfiles/widgets/files_widget_published.py b/openpype/tools/ayon_workfiles/widgets/files_widget_published.py index bc59447777..576cf18d73 100644 --- a/openpype/tools/ayon_workfiles/widgets/files_widget_published.py +++ b/openpype/tools/ayon_workfiles/widgets/files_widget_published.py @@ -5,9 +5,10 @@ from openpype.style import ( get_default_entity_icon_color, get_disabled_entity_icon_color, ) +from openpype.tools.utils import TreeView from openpype.tools.utils.delegates import PrettyTimeDelegate -from .utils import TreeView, BaseOverlayFrame +from .utils import BaseOverlayFrame REPRE_ID_ROLE = QtCore.Qt.UserRole + 1 @@ -306,7 +307,7 @@ class PublishedFilesWidget(QtWidgets.QWidget): selection_model = view.selectionModel() selection_model.selectionChanged.connect(self._on_selection_change) - view.double_clicked_left.connect(self._on_left_double_click) + view.double_clicked.connect(self._on_mouse_double_click) controller.register_event_callback( "expected_selection_changed", @@ -350,8 +351,9 @@ class PublishedFilesWidget(QtWidgets.QWidget): 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_mouse_double_click(self, event): + if event.button() == QtCore.Qt.LeftButton: + self.save_as_requested.emit() def _on_expected_selection_change(self, event): if ( diff --git a/openpype/tools/ayon_workfiles/widgets/files_widget_workarea.py b/openpype/tools/ayon_workfiles/widgets/files_widget_workarea.py index e8ccd094d1..e59b319459 100644 --- a/openpype/tools/ayon_workfiles/widgets/files_widget_workarea.py +++ b/openpype/tools/ayon_workfiles/widgets/files_widget_workarea.py @@ -5,10 +5,9 @@ from openpype.style import ( get_default_entity_icon_color, get_disabled_entity_icon_color, ) +from openpype.tools.utils import TreeView 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 @@ -271,7 +270,7 @@ class WorkAreaFilesWidget(QtWidgets.QWidget): selection_model = view.selectionModel() selection_model.selectionChanged.connect(self._on_selection_change) - view.double_clicked_left.connect(self._on_left_double_click) + view.double_clicked.connect(self._on_mouse_double_click) view.customContextMenuRequested.connect(self._on_context_menu) controller.register_event_callback( @@ -333,8 +332,9 @@ class WorkAreaFilesWidget(QtWidgets.QWidget): 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_mouse_double_click(self, event): + if event.button() == QtCore.Qt.LeftButton: + self.save_as_requested.emit() def _on_context_menu(self, point): index = self._view.indexAt(point) diff --git a/openpype/tools/ayon_workfiles/widgets/folders_widget.py b/openpype/tools/ayon_workfiles/widgets/folders_widget.py index b35845f4b6..b04f8e4098 100644 --- a/openpype/tools/ayon_workfiles/widgets/folders_widget.py +++ b/openpype/tools/ayon_workfiles/widgets/folders_widget.py @@ -264,7 +264,7 @@ class FoldersWidget(QtWidgets.QWidget): self._expected_selection = None - def set_name_filer(self, name): + def set_name_filter(self, name): self._folders_proxy_model.setFilterFixedString(name) def _clear(self): diff --git a/openpype/tools/ayon_workfiles/widgets/utils.py b/openpype/tools/ayon_workfiles/widgets/utils.py index 6a61239f8d..9171638546 100644 --- a/openpype/tools/ayon_workfiles/widgets/utils.py +++ b/openpype/tools/ayon_workfiles/widgets/utils.py @@ -1,70 +1,4 @@ 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): diff --git a/openpype/tools/ayon_workfiles/widgets/window.py b/openpype/tools/ayon_workfiles/widgets/window.py index ef352c8b18..6218d2dd06 100644 --- a/openpype/tools/ayon_workfiles/widgets/window.py +++ b/openpype/tools/ayon_workfiles/widgets/window.py @@ -338,7 +338,7 @@ class WorkfilesToolWindow(QtWidgets.QWidget): self._side_panel.set_published_mode(published_mode) def _on_folder_filter_change(self, text): - self._folder_widget.set_name_filer(text) + self._folder_widget.set_name_filter(text) def _on_go_to_current_clicked(self): self._controller.go_to_current_context() diff --git a/openpype/tools/utils/__init__.py b/openpype/tools/utils/__init__.py index ed41d93f0d..50d50f467a 100644 --- a/openpype/tools/utils/__init__.py +++ b/openpype/tools/utils/__init__.py @@ -20,7 +20,10 @@ from .widgets import ( RefreshButton, GoToCurrentButton, ) -from .views import DeselectableTreeView +from .views import ( + DeselectableTreeView, + TreeView, +) from .error_dialog import ErrorMessageBox from .lib import ( WrappedCallbackItem, @@ -71,6 +74,7 @@ __all__ = ( "GoToCurrentButton", "DeselectableTreeView", + "TreeView", "ErrorMessageBox", diff --git a/openpype/tools/utils/views.py b/openpype/tools/utils/views.py index 01919d6745..596a47ede9 100644 --- a/openpype/tools/utils/views.py +++ b/openpype/tools/utils/views.py @@ -1,4 +1,6 @@ from openpype.resources import get_image_path +from openpype.tools.flickcharm import FlickCharm + from qtpy import QtWidgets, QtCore, QtGui, QtSvg @@ -57,3 +59,63 @@ class TreeViewSpinner(QtWidgets.QTreeView): self.paint_empty(event) else: super(TreeViewSpinner, self).paintEvent(event) + + +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 refresh animation. + """ + + double_clicked = QtCore.Signal(QtGui.QMouseEvent) + + 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): + self.double_clicked.emit(event) + + 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) From 8848d846975cef0013c1df18473d96da330c4418 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 13 Oct 2023 10:53:27 +0200 Subject: [PATCH 0303/1233] removed 'update_hierarchy' (#5756) --- openpype/hosts/blender/api/pipeline.py | 30 -------------------------- 1 file changed, 30 deletions(-) diff --git a/openpype/hosts/blender/api/pipeline.py b/openpype/hosts/blender/api/pipeline.py index 29339a512c..84af0904f0 100644 --- a/openpype/hosts/blender/api/pipeline.py +++ b/openpype/hosts/blender/api/pipeline.py @@ -460,36 +460,6 @@ def ls() -> Iterator: yield parse_container(container) -def update_hierarchy(containers): - """Hierarchical container support - - This is the function to support Scene Inventory to draw hierarchical - view for containers. - - We need both parent and children to visualize the graph. - - """ - - all_containers = set(ls()) # lookup set - - for container in containers: - # Find parent - # FIXME (jasperge): re-evaluate this. How would it be possible - # to 'nest' assets? Collections can have several parents, for - # now assume it has only 1 parent - parent = [ - coll for coll in bpy.data.collections if container in coll.children - ] - for node in parent: - if node in all_containers: - container["parent"] = node - break - - log.debug("Container: %s", container) - - yield container - - def publish(): """Shorthand to publish from within host.""" From f17ab23477fa1f48e905c7be62b5982a66bcd8f9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 13 Oct 2023 11:22:17 +0200 Subject: [PATCH 0304/1233] removing debug logging --- .../nuke/plugins/publish/validate_write_nodes.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py b/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py index 2a925fbeff..9aae53e59d 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py +++ b/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py @@ -82,12 +82,6 @@ class ValidateNukeWriteNode( correct_data = get_write_node_template_attr(write_group_node) check = [] - self.log.debug("__ write_node: {}".format( - write_node - )) - self.log.debug("__ correct_data: {}".format( - correct_data - )) # Collect key values of same type in a list. values_by_name = defaultdict(list) @@ -96,9 +90,6 @@ class ValidateNukeWriteNode( for knob_data in correct_data["knobs"]: knob_type = knob_data["type"] - self.log.debug("__ knob_type: {}".format( - knob_type - )) if ( knob_type == "__legacy__" @@ -134,9 +125,6 @@ class ValidateNukeWriteNode( fixed_values.append(value) - self.log.debug("__ key: {} | values: {}".format( - key, fixed_values - )) if ( node_value not in fixed_values and key != "file" @@ -144,8 +132,6 @@ class ValidateNukeWriteNode( ): check.append([key, value, write_node[key].value()]) - self.log.info(check) - if check: self._make_error(check) From b993cea40b1261dd78121c2bf39700cedb02c942 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 13 Oct 2023 17:36:57 +0800 Subject: [PATCH 0305/1233] rename validate max contents to validate container & add related families to check the container contents --- .../plugins/publish/validate_containers.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 openpype/hosts/max/plugins/publish/validate_containers.py diff --git a/openpype/hosts/max/plugins/publish/validate_containers.py b/openpype/hosts/max/plugins/publish/validate_containers.py new file mode 100644 index 0000000000..a5c0669a11 --- /dev/null +++ b/openpype/hosts/max/plugins/publish/validate_containers.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +import pyblish.api +from openpype.pipeline import PublishValidationError + + +class ValidateContainers(pyblish.api.InstancePlugin): + """Validates Containers. + + Check if MaxScene containers includes any contents underneath. + """ + + order = pyblish.api.ValidatorOrder + families = ["camera", + "model", + "maxScene", + "review", + "pointcache", + "pointcloud", + "redshiftproxy"] + hosts = ["max"] + label = "Container Contents" + + def process(self, instance): + if not instance.data["members"]: + raise PublishValidationError("No content found in the container") From 59dc6d2813554b3c19f69632ae8ff206d87b0c0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Fri, 13 Oct 2023 12:07:33 +0200 Subject: [PATCH 0306/1233] Update openpype/hosts/nuke/plugins/publish/help/validate_asset_context.xml Co-authored-by: Roy Nieterau --- .../hosts/nuke/plugins/publish/help/validate_asset_context.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/help/validate_asset_context.xml b/openpype/hosts/nuke/plugins/publish/help/validate_asset_context.xml index 6d3a9724db..d9394ae510 100644 --- a/openpype/hosts/nuke/plugins/publish/help/validate_asset_context.xml +++ b/openpype/hosts/nuke/plugins/publish/help/validate_asset_context.xml @@ -11,7 +11,7 @@ Usually this is not what you want but there can be cases where you might want to If that's the case you can disable the validation on the instance to ignore it. -Following Node with name is wrong: \`{node_name}\` +The wrong node's name is: \`{node_name}\` ### Correct context keys and values: From 6f8cc1c982f2378fd83430ab4fa640b8dcaffa09 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 13 Oct 2023 12:28:44 +0200 Subject: [PATCH 0307/1233] fixing settings for `ValidateScriptAttributes` --- openpype/settings/defaults/project_settings/nuke.json | 2 +- .../projects_schema/schemas/schema_nuke_publish.json | 4 ++-- server_addon/nuke/server/settings/publish_plugins.py | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index ad9f46c8ab..262381e15a 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -374,7 +374,7 @@ "optional": true, "active": true }, - "ValidateScript": { + "ValidateScriptAttributes": { "enabled": true, "optional": true, "active": true diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json index fa08e19c63..8877494053 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json @@ -113,8 +113,8 @@ "label": "Validate Gizmo (Group)" }, { - "key": "ValidateScript", - "label": "Validate script settings" + "key": "ValidateScriptAttributes", + "label": "Validate workfile attributes" } ] }, diff --git a/server_addon/nuke/server/settings/publish_plugins.py b/server_addon/nuke/server/settings/publish_plugins.py index 2d3a282c46..25a70b47ba 100644 --- a/server_addon/nuke/server/settings/publish_plugins.py +++ b/server_addon/nuke/server/settings/publish_plugins.py @@ -263,8 +263,8 @@ class PublishPuginsModel(BaseSettingsModel): title="Validate Backdrop", default_factory=OptionalPluginModel ) - ValidateScript: OptionalPluginModel = Field( - title="Validate Script", + ValidateScriptAttributes: OptionalPluginModel = Field( + title="Validate workfile attributes", default_factory=OptionalPluginModel ) ExtractThumbnail: ExtractThumbnailModel = Field( @@ -345,7 +345,7 @@ DEFAULT_PUBLISH_PLUGIN_SETTINGS = { "optional": True, "active": True }, - "ValidateScript": { + "ValidateScriptAttributes": { "enabled": True, "optional": True, "active": True From 70d8c72c96fab9cb7d1196a02ae3b1354fe86a9b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 13 Oct 2023 13:38:55 +0200 Subject: [PATCH 0308/1233] Scene Inventory tool: Refactor Scene Inventory tool (for AYON) (#5758) * propagate 'set_deselectable' in 'FoldersWidget' * propagate more public functionality of 'FoldersWidget' * initial commit duplicated current sceneinventory to ayon_sceneinventory * implemented basic controller helper * use the controller in UI * minor modifications * initial changes of switch dialog * moved 'get_containers' to controller * refresh scene inventory after show * fix passed argument to InventoryModel * removed vertical expanding * tweaks of folder input * initial changes in switch dialog * fix selection of folder * use new scene inventory in host tools * fix the size policy of FoldersField * fix current context folder id * fix current folder change * renamed asset > folder and subset > product * removed duplicated methods after rebase * removed unused import * formatting fix * try to query only valid UUID * query all containers documents at once * validate representation ids in view too * use 'container' variable instead of 'item' --- .../tools/ayon_sceneinventory/__init__.py | 6 + openpype/tools/ayon_sceneinventory/control.py | 134 ++ openpype/tools/ayon_sceneinventory/model.py | 622 ++++++++ .../ayon_sceneinventory/models/__init__.py | 6 + .../ayon_sceneinventory/models/site_sync.py | 176 +++ .../switch_dialog/__init__.py | 6 + .../switch_dialog/dialog.py | 1333 +++++++++++++++++ .../switch_dialog/folders_input.py | 307 ++++ .../switch_dialog/widgets.py | 94 ++ openpype/tools/ayon_sceneinventory/view.py | 825 ++++++++++ openpype/tools/ayon_sceneinventory/window.py | 200 +++ openpype/tools/utils/delegates.py | 7 +- openpype/tools/utils/host_tools.py | 19 +- 13 files changed, 3728 insertions(+), 7 deletions(-) create mode 100644 openpype/tools/ayon_sceneinventory/__init__.py create mode 100644 openpype/tools/ayon_sceneinventory/control.py create mode 100644 openpype/tools/ayon_sceneinventory/model.py create mode 100644 openpype/tools/ayon_sceneinventory/models/__init__.py create mode 100644 openpype/tools/ayon_sceneinventory/models/site_sync.py create mode 100644 openpype/tools/ayon_sceneinventory/switch_dialog/__init__.py create mode 100644 openpype/tools/ayon_sceneinventory/switch_dialog/dialog.py create mode 100644 openpype/tools/ayon_sceneinventory/switch_dialog/folders_input.py create mode 100644 openpype/tools/ayon_sceneinventory/switch_dialog/widgets.py create mode 100644 openpype/tools/ayon_sceneinventory/view.py create mode 100644 openpype/tools/ayon_sceneinventory/window.py diff --git a/openpype/tools/ayon_sceneinventory/__init__.py b/openpype/tools/ayon_sceneinventory/__init__.py new file mode 100644 index 0000000000..5412e2fea2 --- /dev/null +++ b/openpype/tools/ayon_sceneinventory/__init__.py @@ -0,0 +1,6 @@ +from .control import SceneInventoryController + + +__all__ = ( + "SceneInventoryController", +) diff --git a/openpype/tools/ayon_sceneinventory/control.py b/openpype/tools/ayon_sceneinventory/control.py new file mode 100644 index 0000000000..e98b0e307b --- /dev/null +++ b/openpype/tools/ayon_sceneinventory/control.py @@ -0,0 +1,134 @@ +import ayon_api + +from openpype.lib.events import QueuedEventSystem +from openpype.host import ILoadHost +from openpype.pipeline import ( + registered_host, + get_current_context, +) +from openpype.tools.ayon_utils.models import HierarchyModel + +from .models import SiteSyncModel + + +class SceneInventoryController: + """This is a temporary controller for AYON. + + Goal of this temporary controller is to provide a way to get current + context instead of using 'AvalonMongoDB' object (or 'legacy_io'). + + Also provides (hopefully) cleaner api for site sync. + """ + + def __init__(self, host=None): + if host is None: + host = registered_host() + self._host = host + self._current_context = None + self._current_project = None + self._current_folder_id = None + self._current_folder_set = False + + self._site_sync_model = SiteSyncModel(self) + # Switch dialog requirements + self._hierarchy_model = HierarchyModel(self) + self._event_system = self._create_event_system() + + def emit_event(self, topic, data=None, source=None): + 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 reset(self): + self._current_context = None + self._current_project = None + self._current_folder_id = None + self._current_folder_set = False + + self._site_sync_model.reset() + self._hierarchy_model.reset() + + def get_current_context(self): + if self._current_context is None: + if hasattr(self._host, "get_current_context"): + self._current_context = self._host.get_current_context() + else: + self._current_context = get_current_context() + return self._current_context + + def get_current_project_name(self): + if self._current_project is None: + self._current_project = self.get_current_context()["project_name"] + return self._current_project + + def get_current_folder_id(self): + if self._current_folder_set: + return self._current_folder_id + + context = self.get_current_context() + project_name = context["project_name"] + folder_path = context.get("folder_path") + folder_name = context.get("asset_name") + folder_id = None + if folder_path: + folder = ayon_api.get_folder_by_path(project_name, folder_path) + if folder: + folder_id = folder["id"] + elif folder_name: + for folder in ayon_api.get_folders( + project_name, folder_names=[folder_name] + ): + folder_id = folder["id"] + break + + self._current_folder_id = folder_id + self._current_folder_set = True + return self._current_folder_id + + def get_containers(self): + host = self._host + if isinstance(host, ILoadHost): + return host.get_containers() + elif hasattr(host, "ls"): + return host.ls() + return [] + + # Site Sync methods + def is_sync_server_enabled(self): + return self._site_sync_model.is_sync_server_enabled() + + def get_sites_information(self): + return self._site_sync_model.get_sites_information() + + def get_site_provider_icons(self): + return self._site_sync_model.get_site_provider_icons() + + def get_representations_site_progress(self, representation_ids): + return self._site_sync_model.get_representations_site_progress( + representation_ids + ) + + def resync_representations(self, representation_ids, site_type): + return self._site_sync_model.resync_representations( + representation_ids, site_type + ) + + # Switch dialog methods + def get_folder_items(self, project_name, sender=None): + return self._hierarchy_model.get_folder_items(project_name, sender) + + def get_folder_label(self, folder_id): + if not folder_id: + return None + project_name = self.get_current_project_name() + folder_item = self._hierarchy_model.get_folder_item( + project_name, folder_id) + if folder_item is None: + return None + return folder_item.label + + def _create_event_system(self): + return QueuedEventSystem() diff --git a/openpype/tools/ayon_sceneinventory/model.py b/openpype/tools/ayon_sceneinventory/model.py new file mode 100644 index 0000000000..16924b0a7e --- /dev/null +++ b/openpype/tools/ayon_sceneinventory/model.py @@ -0,0 +1,622 @@ +import collections +import re +import logging +import uuid +import copy + +from collections import defaultdict + +from qtpy import QtCore, QtGui +import qtawesome + +from openpype.client import ( + get_assets, + get_subsets, + get_versions, + get_last_version_by_subset_id, + get_representations, +) +from openpype.pipeline import ( + get_current_project_name, + schema, + HeroVersionType, +) +from openpype.style import get_default_entity_icon_color +from openpype.tools.utils.models import TreeModel, Item + + +def walk_hierarchy(node): + """Recursively yield group node.""" + for child in node.children(): + if child.get("isGroupNode"): + yield child + + for _child in walk_hierarchy(child): + yield _child + + +class InventoryModel(TreeModel): + """The model for the inventory""" + + Columns = [ + "Name", + "version", + "count", + "family", + "group", + "loader", + "objectName", + "active_site", + "remote_site", + ] + active_site_col = Columns.index("active_site") + remote_site_col = Columns.index("remote_site") + + OUTDATED_COLOR = QtGui.QColor(235, 30, 30) + CHILD_OUTDATED_COLOR = QtGui.QColor(200, 160, 30) + GRAYOUT_COLOR = QtGui.QColor(160, 160, 160) + + UniqueRole = QtCore.Qt.UserRole + 2 # unique label role + + def __init__(self, controller, parent=None): + super(InventoryModel, self).__init__(parent) + self.log = logging.getLogger(self.__class__.__name__) + + self._controller = controller + + self._hierarchy_view = False + + self._default_icon_color = get_default_entity_icon_color() + + site_icons = self._controller.get_site_provider_icons() + + self._site_icons = { + provider: QtGui.QIcon(icon_path) + for provider, icon_path in site_icons.items() + } + + def outdated(self, item): + value = item.get("version") + if isinstance(value, HeroVersionType): + return False + + if item.get("version") == item.get("highest_version"): + return False + return True + + def data(self, index, role): + if not index.isValid(): + return + + item = index.internalPointer() + + if role == QtCore.Qt.FontRole: + # Make top-level entries bold + if item.get("isGroupNode") or item.get("isNotSet"): # group-item + font = QtGui.QFont() + font.setBold(True) + return font + + if role == QtCore.Qt.ForegroundRole: + # Set the text color to the OUTDATED_COLOR when the + # collected version is not the same as the highest version + key = self.Columns[index.column()] + if key == "version": # version + if item.get("isGroupNode"): # group-item + if self.outdated(item): + return self.OUTDATED_COLOR + + if self._hierarchy_view: + # If current group is not outdated, check if any + # outdated children. + for _node in walk_hierarchy(item): + if self.outdated(_node): + return self.CHILD_OUTDATED_COLOR + else: + + if self._hierarchy_view: + # Although this is not a group item, we still need + # to distinguish which one contain outdated child. + for _node in walk_hierarchy(item): + if self.outdated(_node): + return self.CHILD_OUTDATED_COLOR.darker(150) + + return self.GRAYOUT_COLOR + + if key == "Name" and not item.get("isGroupNode"): + return self.GRAYOUT_COLOR + + # Add icons + if role == QtCore.Qt.DecorationRole: + if index.column() == 0: + # Override color + color = item.get("color", self._default_icon_color) + if item.get("isGroupNode"): # group-item + return qtawesome.icon("fa.folder", color=color) + if item.get("isNotSet"): + return qtawesome.icon("fa.exclamation-circle", color=color) + + return qtawesome.icon("fa.file-o", color=color) + + if index.column() == 3: + # Family icon + return item.get("familyIcon", None) + + column_name = self.Columns[index.column()] + + if column_name == "group" and item.get("group"): + return qtawesome.icon("fa.object-group", + color=get_default_entity_icon_color()) + + if item.get("isGroupNode"): + if column_name == "active_site": + provider = item.get("active_site_provider") + return self._site_icons.get(provider) + + if column_name == "remote_site": + provider = item.get("remote_site_provider") + return self._site_icons.get(provider) + + if role == QtCore.Qt.DisplayRole and item.get("isGroupNode"): + column_name = self.Columns[index.column()] + progress = None + if column_name == "active_site": + progress = item.get("active_site_progress", 0) + elif column_name == "remote_site": + progress = item.get("remote_site_progress", 0) + if progress is not None: + return "{}%".format(max(progress, 0) * 100) + + if role == self.UniqueRole: + return item["representation"] + item.get("objectName", "") + + return super(InventoryModel, self).data(index, role) + + def set_hierarchy_view(self, state): + """Set whether to display subsets in hierarchy view.""" + state = bool(state) + + if state != self._hierarchy_view: + self._hierarchy_view = state + + def refresh(self, selected=None, containers=None): + """Refresh the model""" + + # for debugging or testing, injecting items from outside + if containers is None: + containers = self._controller.get_containers() + + self.clear() + if not selected or not self._hierarchy_view: + self._add_containers(containers) + return + + # Filter by cherry-picked items + self._add_containers(( + container + for container in containers + if container["objectName"] in selected + )) + + def _add_containers(self, containers, parent=None): + """Add the items to the model. + + The items should be formatted similar to `api.ls()` returns, an item + is then represented as: + {"filename_v001.ma": [full/filename/of/loaded/filename_v001.ma, + full/filename/of/loaded/filename_v001.ma], + "nodetype" : "reference", + "node": "referenceNode1"} + + Note: When performing an additional call to `add_items` it will *not* + group the new items with previously existing item groups of the + same type. + + Args: + containers (generator): Container items. + parent (Item, optional): Set this item as parent for the added + items when provided. Defaults to the root of the model. + + Returns: + node.Item: root node which has children added based on the data + """ + + project_name = get_current_project_name() + + self.beginResetModel() + + # Group by representation + grouped = defaultdict(lambda: {"containers": list()}) + for container in containers: + repre_id = container["representation"] + grouped[repre_id]["containers"].append(container) + + ( + repres_by_id, + versions_by_id, + products_by_id, + folders_by_id, + ) = self._query_entities(project_name, set(grouped.keys())) + # Add to model + not_found = defaultdict(list) + not_found_ids = [] + for repre_id, group_dict in sorted(grouped.items()): + group_containers = group_dict["containers"] + representation = repres_by_id.get(repre_id) + if not representation: + not_found["representation"].extend(group_containers) + not_found_ids.append(repre_id) + continue + + version = versions_by_id.get(representation["parent"]) + if not version: + not_found["version"].extend(group_containers) + not_found_ids.append(repre_id) + continue + + product = products_by_id.get(version["parent"]) + if not product: + not_found["product"].extend(group_containers) + not_found_ids.append(repre_id) + continue + + folder = folders_by_id.get(product["parent"]) + if not folder: + not_found["folder"].extend(group_containers) + not_found_ids.append(repre_id) + continue + + group_dict.update({ + "representation": representation, + "version": version, + "subset": product, + "asset": folder + }) + + for _repre_id in not_found_ids: + grouped.pop(_repre_id) + + for where, group_containers in not_found.items(): + # create the group header + group_node = Item() + name = "< NOT FOUND - {} >".format(where) + group_node["Name"] = name + group_node["representation"] = name + group_node["count"] = len(group_containers) + group_node["isGroupNode"] = False + group_node["isNotSet"] = True + + self.add_child(group_node, parent=parent) + + for container in group_containers: + item_node = Item() + item_node.update(container) + item_node["Name"] = container.get("objectName", "NO NAME") + item_node["isNotFound"] = True + self.add_child(item_node, parent=group_node) + + # TODO Use product icons + family_icon = qtawesome.icon( + "fa.folder", color="#0091B2" + ) + # Prepare site sync specific data + progress_by_id = self._controller.get_representations_site_progress( + set(grouped.keys()) + ) + sites_info = self._controller.get_sites_information() + + for repre_id, group_dict in sorted(grouped.items()): + group_containers = group_dict["containers"] + representation = group_dict["representation"] + version = group_dict["version"] + subset = group_dict["subset"] + asset = group_dict["asset"] + + # Get the primary family + maj_version, _ = schema.get_schema_version(subset["schema"]) + if maj_version < 3: + src_doc = version + else: + src_doc = subset + + prim_family = src_doc["data"].get("family") + if not prim_family: + families = src_doc["data"].get("families") + if families: + prim_family = families[0] + + # Store the highest available version so the model can know + # whether current version is currently up-to-date. + highest_version = get_last_version_by_subset_id( + project_name, version["parent"] + ) + + # create the group header + group_node = Item() + group_node["Name"] = "{}_{}: ({})".format( + asset["name"], subset["name"], representation["name"] + ) + group_node["representation"] = repre_id + group_node["version"] = version["name"] + group_node["highest_version"] = highest_version["name"] + group_node["family"] = prim_family or "" + group_node["familyIcon"] = family_icon + group_node["count"] = len(group_containers) + group_node["isGroupNode"] = True + group_node["group"] = subset["data"].get("subsetGroup") + + # Site sync specific data + progress = progress_by_id[repre_id] + group_node.update(sites_info) + group_node["active_site_progress"] = progress["active_site"] + group_node["remote_site_progress"] = progress["remote_site"] + + self.add_child(group_node, parent=parent) + + for container in group_containers: + item_node = Item() + item_node.update(container) + + # store the current version on the item + item_node["version"] = version["name"] + + # Remapping namespace to item name. + # Noted that the name key is capital "N", by doing this, we + # can view namespace in GUI without changing container data. + item_node["Name"] = container["namespace"] + + self.add_child(item_node, parent=group_node) + + self.endResetModel() + + return self._root_item + + def _query_entities(self, project_name, repre_ids): + """Query entities for representations from containers. + + Returns: + tuple[dict, dict, dict, dict]: Representation, version, product + and folder documents by id. + """ + + repres_by_id = {} + versions_by_id = {} + products_by_id = {} + folders_by_id = {} + output = ( + repres_by_id, + versions_by_id, + products_by_id, + folders_by_id, + ) + + filtered_repre_ids = set() + for repre_id in repre_ids: + # Filter out invalid representation ids + # NOTE: This is added because scenes from OpenPype did contain + # ObjectId from mongo. + try: + uuid.UUID(repre_id) + filtered_repre_ids.add(repre_id) + except ValueError: + continue + if not filtered_repre_ids: + return output + + repre_docs = get_representations(project_name, repre_ids) + repres_by_id.update({ + repre_doc["_id"]: repre_doc + for repre_doc in repre_docs + }) + version_ids = { + repre_doc["parent"] for repre_doc in repres_by_id.values() + } + if not version_ids: + return output + + version_docs = get_versions(project_name, version_ids, hero=True) + versions_by_id.update({ + version_doc["_id"]: version_doc + for version_doc in version_docs + }) + hero_versions_by_subversion_id = collections.defaultdict(list) + for version_doc in versions_by_id.values(): + if version_doc["type"] != "hero_version": + continue + subversion = version_doc["version_id"] + hero_versions_by_subversion_id[subversion].append(version_doc) + + if hero_versions_by_subversion_id: + subversion_ids = set( + hero_versions_by_subversion_id.keys() + ) + subversion_docs = get_versions(project_name, subversion_ids) + for subversion_doc in subversion_docs: + subversion_id = subversion_doc["_id"] + subversion_ids.discard(subversion_id) + h_version_docs = hero_versions_by_subversion_id[subversion_id] + for version_doc in h_version_docs: + version_doc["name"] = HeroVersionType( + subversion_doc["name"] + ) + version_doc["data"] = copy.deepcopy( + subversion_doc["data"] + ) + + for subversion_id in subversion_ids: + h_version_docs = hero_versions_by_subversion_id[subversion_id] + for version_doc in h_version_docs: + versions_by_id.pop(version_doc["_id"]) + + product_ids = { + version_doc["parent"] + for version_doc in versions_by_id.values() + } + if not product_ids: + return output + product_docs = get_subsets(project_name, product_ids) + products_by_id.update({ + product_doc["_id"]: product_doc + for product_doc in product_docs + }) + folder_ids = { + product_doc["parent"] + for product_doc in products_by_id.values() + } + if not folder_ids: + return output + + folder_docs = get_assets(project_name, folder_ids) + folders_by_id.update({ + folder_doc["_id"]: folder_doc + for folder_doc in folder_docs + }) + return output + + +class FilterProxyModel(QtCore.QSortFilterProxyModel): + """Filter model to where key column's value is in the filtered tags""" + + def __init__(self, *args, **kwargs): + super(FilterProxyModel, self).__init__(*args, **kwargs) + self._filter_outdated = False + self._hierarchy_view = False + + def filterAcceptsRow(self, row, parent): + model = self.sourceModel() + source_index = model.index(row, self.filterKeyColumn(), parent) + + # Always allow bottom entries (individual containers), since their + # parent group hidden if it wouldn't have been validated. + rows = model.rowCount(source_index) + if not rows: + return True + + # Filter by regex + if hasattr(self, "filterRegExp"): + regex = self.filterRegExp() + else: + regex = self.filterRegularExpression() + pattern = regex.pattern() + if pattern: + pattern = re.escape(pattern) + + if not self._matches(row, parent, pattern): + return False + + if self._filter_outdated: + # When filtering to outdated we filter the up to date entries + # thus we "allow" them when they are outdated + if not self._is_outdated(row, parent): + return False + + return True + + def set_filter_outdated(self, state): + """Set whether to show the outdated entries only.""" + state = bool(state) + + if state != self._filter_outdated: + self._filter_outdated = bool(state) + self.invalidateFilter() + + def set_hierarchy_view(self, state): + state = bool(state) + + if state != self._hierarchy_view: + self._hierarchy_view = state + + def _is_outdated(self, row, parent): + """Return whether row is outdated. + + A row is considered outdated if it has "version" and "highest_version" + data and in the internal data structure, and they are not of an + equal value. + + """ + def outdated(node): + version = node.get("version", None) + highest = node.get("highest_version", None) + + # Always allow indices that have no version data at all + if version is None and highest is None: + return True + + # If either a version or highest is present but not the other + # consider the item invalid. + if not self._hierarchy_view: + # Skip this check if in hierarchy view, or the child item + # node will be hidden even it's actually outdated. + if version is None or highest is None: + return False + return version != highest + + index = self.sourceModel().index(row, self.filterKeyColumn(), parent) + + # The scene contents are grouped by "representation", e.g. the same + # "representation" loaded twice is grouped under the same header. + # Since the version check filters these parent groups we skip that + # check for the individual children. + has_parent = index.parent().isValid() + if has_parent and not self._hierarchy_view: + return True + + # Filter to those that have the different version numbers + node = index.internalPointer() + if outdated(node): + return True + + if self._hierarchy_view: + for _node in walk_hierarchy(node): + if outdated(_node): + return True + + return False + + def _matches(self, row, parent, pattern): + """Return whether row matches regex pattern. + + Args: + row (int): row number in model + parent (QtCore.QModelIndex): parent index + pattern (regex.pattern): pattern to check for in key + + Returns: + bool + + """ + model = self.sourceModel() + column = self.filterKeyColumn() + role = self.filterRole() + + def matches(row, parent, pattern): + index = model.index(row, column, parent) + key = model.data(index, role) + if re.search(pattern, key, re.IGNORECASE): + return True + + if matches(row, parent, pattern): + return True + + # Also allow if any of the children matches + source_index = model.index(row, column, parent) + rows = model.rowCount(source_index) + + if any( + matches(idx, source_index, pattern) + for idx in range(rows) + ): + return True + + if not self._hierarchy_view: + return False + + for idx in range(rows): + child_index = model.index(idx, column, source_index) + child_rows = model.rowCount(child_index) + return any( + self._matches(child_idx, child_index, pattern) + for child_idx in range(child_rows) + ) + + return True diff --git a/openpype/tools/ayon_sceneinventory/models/__init__.py b/openpype/tools/ayon_sceneinventory/models/__init__.py new file mode 100644 index 0000000000..c861d3c1a0 --- /dev/null +++ b/openpype/tools/ayon_sceneinventory/models/__init__.py @@ -0,0 +1,6 @@ +from .site_sync import SiteSyncModel + + +__all__ = ( + "SiteSyncModel", +) diff --git a/openpype/tools/ayon_sceneinventory/models/site_sync.py b/openpype/tools/ayon_sceneinventory/models/site_sync.py new file mode 100644 index 0000000000..b8c9443230 --- /dev/null +++ b/openpype/tools/ayon_sceneinventory/models/site_sync.py @@ -0,0 +1,176 @@ +from openpype.client import get_representations +from openpype.modules import ModulesManager + +NOT_SET = object() + + +class SiteSyncModel: + def __init__(self, controller): + self._controller = controller + + self._sync_server_module = NOT_SET + self._sync_server_enabled = None + self._active_site = NOT_SET + self._remote_site = NOT_SET + self._active_site_provider = NOT_SET + self._remote_site_provider = NOT_SET + + def reset(self): + self._sync_server_module = NOT_SET + self._sync_server_enabled = None + self._active_site = NOT_SET + self._remote_site = NOT_SET + self._active_site_provider = NOT_SET + self._remote_site_provider = NOT_SET + + def is_sync_server_enabled(self): + """Site sync is enabled. + + Returns: + bool: Is enabled or not. + """ + + self._cache_sync_server_module() + return self._sync_server_enabled + + def get_site_provider_icons(self): + """Icon paths per provider. + + Returns: + dict[str, str]: Path by provider name. + """ + + site_sync = self._get_sync_server_module() + if site_sync is None: + return {} + return site_sync.get_site_icons() + + def get_sites_information(self): + return { + "active_site": self._get_active_site(), + "active_site_provider": self._get_active_site_provider(), + "remote_site": self._get_remote_site(), + "remote_site_provider": self._get_remote_site_provider() + } + + def get_representations_site_progress(self, representation_ids): + """Get progress of representations sync.""" + + representation_ids = set(representation_ids) + output = { + repre_id: { + "active_site": 0, + "remote_site": 0, + } + for repre_id in representation_ids + } + if not self.is_sync_server_enabled(): + return output + + project_name = self._controller.get_current_project_name() + site_sync = self._get_sync_server_module() + repre_docs = get_representations(project_name, representation_ids) + active_site = self._get_active_site() + remote_site = self._get_remote_site() + + for repre_doc in repre_docs: + repre_output = output[repre_doc["_id"]] + result = site_sync.get_progress_for_repre( + repre_doc, active_site, remote_site + ) + repre_output["active_site"] = result[active_site] + repre_output["remote_site"] = result[remote_site] + + return output + + def resync_representations(self, representation_ids, site_type): + """ + + Args: + representation_ids (Iterable[str]): Representation ids. + site_type (Literal[active_site, remote_site]): Site type. + """ + + project_name = self._controller.get_current_project_name() + site_sync = self._get_sync_server_module() + active_site = self._get_active_site() + remote_site = self._get_remote_site() + progress = self.get_representations_site_progress( + representation_ids + ) + for repre_id in representation_ids: + repre_progress = progress.get(repre_id) + if not repre_progress: + continue + + if site_type == "active_site": + # check opposite from added site, must be 1 or unable to sync + check_progress = repre_progress["remote_site"] + site = active_site + else: + check_progress = repre_progress["active_site"] + site = remote_site + + if check_progress == 1: + site_sync.add_site( + project_name, repre_id, site, force=True + ) + + def _get_sync_server_module(self): + self._cache_sync_server_module() + return self._sync_server_module + + def _cache_sync_server_module(self): + if self._sync_server_module is not NOT_SET: + return self._sync_server_module + manager = ModulesManager() + site_sync = manager.modules_by_name.get("sync_server") + sync_enabled = site_sync is not None and site_sync.enabled + self._sync_server_module = site_sync + self._sync_server_enabled = sync_enabled + + def _get_active_site(self): + if self._active_site is NOT_SET: + self._cache_sites() + return self._active_site + + def _get_remote_site(self): + if self._remote_site is NOT_SET: + self._cache_sites() + return self._remote_site + + def _get_active_site_provider(self): + if self._active_site_provider is NOT_SET: + self._cache_sites() + return self._active_site_provider + + def _get_remote_site_provider(self): + if self._remote_site_provider is NOT_SET: + self._cache_sites() + return self._remote_site_provider + + def _cache_sites(self): + site_sync = self._get_sync_server_module() + active_site = None + remote_site = None + active_site_provider = None + remote_site_provider = None + if site_sync is not None: + project_name = self._controller.get_current_project_name() + active_site = site_sync.get_active_site(project_name) + remote_site = site_sync.get_remote_site(project_name) + active_site_provider = "studio" + remote_site_provider = "studio" + if active_site != "studio": + active_site_provider = site_sync.get_active_provider( + project_name, active_site + ) + if remote_site != "studio": + remote_site_provider = site_sync.get_active_provider( + project_name, remote_site + ) + + self._active_site = active_site + self._remote_site = remote_site + self._active_site_provider = active_site_provider + self._remote_site_provider = remote_site_provider diff --git a/openpype/tools/ayon_sceneinventory/switch_dialog/__init__.py b/openpype/tools/ayon_sceneinventory/switch_dialog/__init__.py new file mode 100644 index 0000000000..4c07832829 --- /dev/null +++ b/openpype/tools/ayon_sceneinventory/switch_dialog/__init__.py @@ -0,0 +1,6 @@ +from .dialog import SwitchAssetDialog + + +__all__ = ( + "SwitchAssetDialog", +) diff --git a/openpype/tools/ayon_sceneinventory/switch_dialog/dialog.py b/openpype/tools/ayon_sceneinventory/switch_dialog/dialog.py new file mode 100644 index 0000000000..2ebed7f89b --- /dev/null +++ b/openpype/tools/ayon_sceneinventory/switch_dialog/dialog.py @@ -0,0 +1,1333 @@ +import collections +import logging + +from qtpy import QtWidgets, QtCore +import qtawesome + +from openpype.client import ( + get_assets, + get_subset_by_name, + get_subsets, + get_versions, + get_hero_versions, + get_last_versions, + get_representations, +) +from openpype.pipeline.load import ( + discover_loader_plugins, + switch_container, + get_repres_contexts, + loaders_from_repre_context, + LoaderSwitchNotImplementedError, + IncompatibleLoaderError, + LoaderNotFoundError +) + +from .widgets import ( + ButtonWithMenu, + SearchComboBox +) +from .folders_input import FoldersField + +log = logging.getLogger("SwitchAssetDialog") + + +class ValidationState: + def __init__(self): + self.folder_ok = True + self.product_ok = True + self.repre_ok = True + + @property + def all_ok(self): + return ( + self.folder_ok + and self.product_ok + and self.repre_ok + ) + + +class SwitchAssetDialog(QtWidgets.QDialog): + """Widget to support asset switching""" + + MIN_WIDTH = 550 + + switched = QtCore.Signal() + + def __init__(self, controller, parent=None, items=None): + super(SwitchAssetDialog, self).__init__(parent) + + self.setWindowTitle("Switch selected items ...") + + # Force and keep focus dialog + self.setModal(True) + + folders_field = FoldersField(controller, self) + products_combox = SearchComboBox(self) + repres_combobox = SearchComboBox(self) + + products_combox.set_placeholder("") + repres_combobox.set_placeholder("") + + folder_label = QtWidgets.QLabel(self) + product_label = QtWidgets.QLabel(self) + repre_label = QtWidgets.QLabel(self) + + current_folder_btn = QtWidgets.QPushButton("Use current folder", self) + + accept_icon = qtawesome.icon("fa.check", color="white") + accept_btn = ButtonWithMenu(self) + accept_btn.setIcon(accept_icon) + + main_layout = QtWidgets.QGridLayout(self) + # Folder column + main_layout.addWidget(current_folder_btn, 0, 0) + main_layout.addWidget(folders_field, 1, 0) + main_layout.addWidget(folder_label, 2, 0) + # Product column + main_layout.addWidget(products_combox, 1, 1) + main_layout.addWidget(product_label, 2, 1) + # Representation column + main_layout.addWidget(repres_combobox, 1, 2) + main_layout.addWidget(repre_label, 2, 2) + # Btn column + main_layout.addWidget(accept_btn, 1, 3) + main_layout.setColumnStretch(0, 1) + main_layout.setColumnStretch(1, 1) + main_layout.setColumnStretch(2, 1) + main_layout.setColumnStretch(3, 0) + + show_timer = QtCore.QTimer() + show_timer.setInterval(0) + show_timer.setSingleShot(False) + + show_timer.timeout.connect(self._on_show_timer) + folders_field.value_changed.connect( + self._combobox_value_changed + ) + products_combox.currentIndexChanged.connect( + self._combobox_value_changed + ) + repres_combobox.currentIndexChanged.connect( + self._combobox_value_changed + ) + accept_btn.clicked.connect(self._on_accept) + current_folder_btn.clicked.connect(self._on_current_folder) + + self._show_timer = show_timer + self._show_counter = 0 + + self._current_folder_btn = current_folder_btn + + self._folders_field = folders_field + self._products_combox = products_combox + self._representations_box = repres_combobox + + self._folder_label = folder_label + self._product_label = product_label + self._repre_label = repre_label + + self._accept_btn = accept_btn + + self.setMinimumWidth(self.MIN_WIDTH) + + # Set default focus to accept button so you don't directly type in + # first asset field, this also allows to see the placeholder value. + accept_btn.setFocus() + + self._folder_docs_by_id = {} + self._product_docs_by_id = {} + self._version_docs_by_id = {} + self._repre_docs_by_id = {} + + self._missing_folder_ids = set() + self._missing_product_ids = set() + self._missing_version_ids = set() + self._missing_repre_ids = set() + self._missing_docs = False + + self._inactive_folder_ids = set() + self._inactive_product_ids = set() + self._inactive_repre_ids = set() + + self._init_folder_id = None + self._init_product_name = None + self._init_repre_name = None + + self._fill_check = False + + self._project_name = controller.get_current_project_name() + self._folder_id = controller.get_current_folder_id() + + self._current_folder_btn.setEnabled(self._folder_id is not None) + + self._controller = controller + + self._items = items + self._prepare_content_data() + + def showEvent(self, event): + super(SwitchAssetDialog, self).showEvent(event) + self._show_timer.start() + + def refresh(self, init_refresh=False): + """Build the need comboboxes with content""" + if not self._fill_check and not init_refresh: + return + + self._fill_check = False + + validation_state = ValidationState() + self._folders_field.refresh() + # Set other comboboxes to empty if any document is missing or + # any folder of loaded representations is archived. + self._is_folder_ok(validation_state) + if validation_state.folder_ok: + product_values = self._get_product_box_values() + self._fill_combobox(product_values, "product") + self._is_product_ok(validation_state) + + if validation_state.folder_ok and validation_state.product_ok: + repre_values = sorted(self._representations_box_values()) + self._fill_combobox(repre_values, "repre") + self._is_repre_ok(validation_state) + + # Fill comboboxes with values + self.set_labels() + + self.apply_validations(validation_state) + + self._build_loaders_menu() + + if init_refresh: + # pre select context if possible + self._folders_field.set_selected_item(self._init_folder_id) + self._products_combox.set_valid_value(self._init_product_name) + self._representations_box.set_valid_value(self._init_repre_name) + + self._fill_check = True + + def set_labels(self): + folder_label = self._folders_field.get_selected_folder_label() + product_label = self._products_combox.get_valid_value() + repre_label = self._representations_box.get_valid_value() + + default = "*No changes" + self._folder_label.setText(folder_label or default) + self._product_label.setText(product_label or default) + self._repre_label.setText(repre_label or default) + + def apply_validations(self, validation_state): + error_msg = "*Please select" + error_sheet = "border: 1px solid red;" + + product_sheet = None + repre_sheet = None + accept_state = "" + if validation_state.folder_ok is False: + self._folder_label.setText(error_msg) + elif validation_state.product_ok is False: + product_sheet = error_sheet + self._product_label.setText(error_msg) + elif validation_state.repre_ok is False: + repre_sheet = error_sheet + self._repre_label.setText(error_msg) + + if validation_state.all_ok: + accept_state = "1" + + self._folders_field.set_valid(validation_state.folder_ok) + self._products_combox.setStyleSheet(product_sheet or "") + self._representations_box.setStyleSheet(repre_sheet or "") + + self._accept_btn.setEnabled(validation_state.all_ok) + self._set_style_property(self._accept_btn, "state", accept_state) + + def find_last_versions(self, product_ids): + project_name = self._project_name + return get_last_versions( + project_name, + subset_ids=product_ids, + fields=["_id", "parent", "type"] + ) + + def _on_show_timer(self): + if self._show_counter == 2: + self._show_timer.stop() + self.refresh(True) + else: + self._show_counter += 1 + + def _prepare_content_data(self): + repre_ids = { + item["representation"] + for item in self._items + } + + project_name = self._project_name + repres = list(get_representations( + project_name, + representation_ids=repre_ids, + archived=True, + )) + repres_by_id = {str(repre["_id"]): repre for repre in repres} + + content_repre_docs_by_id = {} + inactive_repre_ids = set() + missing_repre_ids = set() + version_ids = set() + for repre_id in repre_ids: + repre_doc = repres_by_id.get(repre_id) + if repre_doc is None: + missing_repre_ids.add(repre_id) + elif repres_by_id[repre_id]["type"] == "archived_representation": + inactive_repre_ids.add(repre_id) + version_ids.add(repre_doc["parent"]) + else: + content_repre_docs_by_id[repre_id] = repre_doc + version_ids.add(repre_doc["parent"]) + + version_docs = get_versions( + project_name, + version_ids=version_ids, + hero=True + ) + content_version_docs_by_id = {} + for version_doc in version_docs: + version_id = version_doc["_id"] + content_version_docs_by_id[version_id] = version_doc + + missing_version_ids = set() + product_ids = set() + for version_id in version_ids: + version_doc = content_version_docs_by_id.get(version_id) + if version_doc is None: + missing_version_ids.add(version_id) + else: + product_ids.add(version_doc["parent"]) + + product_docs = get_subsets( + project_name, subset_ids=product_ids, archived=True + ) + product_docs_by_id = {sub["_id"]: sub for sub in product_docs} + + folder_ids = set() + inactive_product_ids = set() + missing_product_ids = set() + content_product_docs_by_id = {} + for product_id in product_ids: + product_doc = product_docs_by_id.get(product_id) + if product_doc is None: + missing_product_ids.add(product_id) + elif product_doc["type"] == "archived_subset": + folder_ids.add(product_doc["parent"]) + inactive_product_ids.add(product_id) + else: + folder_ids.add(product_doc["parent"]) + content_product_docs_by_id[product_id] = product_doc + + folder_docs = get_assets( + project_name, asset_ids=folder_ids, archived=True + ) + folder_docs_by_id = { + folder_doc["_id"]: folder_doc + for folder_doc in folder_docs + } + + missing_folder_ids = set() + inactive_folder_ids = set() + content_folder_docs_by_id = {} + for folder_id in folder_ids: + folder_doc = folder_docs_by_id.get(folder_id) + if folder_doc is None: + missing_folder_ids.add(folder_id) + elif folder_doc["type"] == "archived_asset": + inactive_folder_ids.add(folder_id) + else: + content_folder_docs_by_id[folder_id] = folder_doc + + # stash context values, works only for single representation + init_folder_id = None + init_product_name = None + init_repre_name = None + if len(repres) == 1: + init_repre_doc = repres[0] + init_version_doc = content_version_docs_by_id.get( + init_repre_doc["parent"]) + init_product_doc = None + init_folder_doc = None + if init_version_doc: + init_product_doc = content_product_docs_by_id.get( + init_version_doc["parent"] + ) + if init_product_doc: + init_folder_doc = content_folder_docs_by_id.get( + init_product_doc["parent"] + ) + if init_folder_doc: + init_repre_name = init_repre_doc["name"] + init_product_name = init_product_doc["name"] + init_folder_id = init_folder_doc["_id"] + + self._init_folder_id = init_folder_id + self._init_product_name = init_product_name + self._init_repre_name = init_repre_name + + self._folder_docs_by_id = content_folder_docs_by_id + self._product_docs_by_id = content_product_docs_by_id + self._version_docs_by_id = content_version_docs_by_id + self._repre_docs_by_id = content_repre_docs_by_id + + self._missing_folder_ids = missing_folder_ids + self._missing_product_ids = missing_product_ids + self._missing_version_ids = missing_version_ids + self._missing_repre_ids = missing_repre_ids + self._missing_docs = ( + bool(missing_folder_ids) + or bool(missing_version_ids) + or bool(missing_product_ids) + or bool(missing_repre_ids) + ) + + self._inactive_folder_ids = inactive_folder_ids + self._inactive_product_ids = inactive_product_ids + self._inactive_repre_ids = inactive_repre_ids + + def _combobox_value_changed(self, *args, **kwargs): + self.refresh() + + def _build_loaders_menu(self): + repre_ids = self._get_current_output_repre_ids() + loaders = self._get_loaders(repre_ids) + # Get and destroy the action group + self._accept_btn.clear_actions() + + if not loaders: + return + + # Build new action group + group = QtWidgets.QActionGroup(self._accept_btn) + + for loader in loaders: + # Label + label = getattr(loader, "label", None) + if label is None: + label = loader.__name__ + + action = group.addAction(label) + # action = QtWidgets.QAction(label) + action.setData(loader) + + # Support font-awesome icons using the `.icon` and `.color` + # attributes on plug-ins. + icon = getattr(loader, "icon", None) + if icon is not None: + try: + key = "fa.{0}".format(icon) + color = getattr(loader, "color", "white") + action.setIcon(qtawesome.icon(key, color=color)) + + except Exception as exc: + print("Unable to set icon for loader {}: {}".format( + loader, str(exc) + )) + + self._accept_btn.add_action(action) + + group.triggered.connect(self._on_action_clicked) + + def _on_action_clicked(self, action): + loader_plugin = action.data() + self._trigger_switch(loader_plugin) + + def _get_loaders(self, repre_ids): + repre_contexts = None + if repre_ids: + repre_contexts = get_repres_contexts(repre_ids) + + if not repre_contexts: + return list() + + available_loaders = [] + for loader_plugin in discover_loader_plugins(): + # Skip loaders without switch method + if not hasattr(loader_plugin, "switch"): + continue + + # Skip utility loaders + if ( + hasattr(loader_plugin, "is_utility") + and loader_plugin.is_utility + ): + continue + available_loaders.append(loader_plugin) + + loaders = None + for repre_context in repre_contexts.values(): + _loaders = set(loaders_from_repre_context( + available_loaders, repre_context + )) + if loaders is None: + loaders = _loaders + else: + loaders = _loaders.intersection(loaders) + + if not loaders: + break + + if loaders is None: + loaders = [] + else: + loaders = list(loaders) + + return loaders + + def _fill_combobox(self, values, combobox_type): + if combobox_type == "product": + combobox_widget = self._products_combox + elif combobox_type == "repre": + combobox_widget = self._representations_box + else: + return + selected_value = combobox_widget.get_valid_value() + + # Fill combobox + if values is not None: + combobox_widget.populate(list(sorted(values))) + if selected_value and selected_value in values: + index = None + for idx in range(combobox_widget.count()): + if selected_value == str(combobox_widget.itemText(idx)): + index = idx + break + if index is not None: + combobox_widget.setCurrentIndex(index) + + def _set_style_property(self, widget, name, value): + cur_value = widget.property(name) + if cur_value == value: + return + widget.setProperty(name, value) + widget.style().polish(widget) + + def _get_current_output_repre_ids(self): + # NOTE hero versions are not used because it is expected that + # hero version has same representations as latests + selected_folder_id = self._folders_field.get_selected_folder_id() + selected_product_name = self._products_combox.currentText() + selected_repre = self._representations_box.currentText() + + # Nothing is selected + # [ ] [ ] [ ] + if ( + not selected_folder_id + and not selected_product_name + and not selected_repre + ): + return list(self._repre_docs_by_id.keys()) + + # Everything is selected + # [x] [x] [x] + if selected_folder_id and selected_product_name and selected_repre: + return self._get_current_output_repre_ids_xxx( + selected_folder_id, selected_product_name, selected_repre + ) + + # [x] [x] [ ] + # If folder and product is selected + if selected_folder_id and selected_product_name: + return self._get_current_output_repre_ids_xxo( + selected_folder_id, selected_product_name + ) + + # [x] [ ] [x] + # If folder and repre is selected + if selected_folder_id and selected_repre: + return self._get_current_output_repre_ids_xox( + selected_folder_id, selected_repre + ) + + # [x] [ ] [ ] + # If folder and product is selected + if selected_folder_id: + return self._get_current_output_repre_ids_xoo(selected_folder_id) + + # [ ] [x] [x] + if selected_product_name and selected_repre: + return self._get_current_output_repre_ids_oxx( + selected_product_name, selected_repre + ) + + # [ ] [x] [ ] + if selected_product_name: + return self._get_current_output_repre_ids_oxo( + selected_product_name + ) + + # [ ] [ ] [x] + return self._get_current_output_repre_ids_oox(selected_repre) + + def _get_current_output_repre_ids_xxx( + self, folder_id, selected_product_name, selected_repre + ): + project_name = self._project_name + product_doc = get_subset_by_name( + project_name, + selected_product_name, + folder_id, + fields=["_id"] + ) + + product_id = product_doc["_id"] + last_versions_by_product_id = self.find_last_versions([product_id]) + version_doc = last_versions_by_product_id.get(product_id) + if not version_doc: + return [] + + repre_docs = get_representations( + project_name, + version_ids=[version_doc["_id"]], + representation_names=[selected_repre], + fields=["_id"] + ) + return [repre_doc["_id"] for repre_doc in repre_docs] + + def _get_current_output_repre_ids_xxo(self, folder_id, product_name): + project_name = self._project_name + product_doc = get_subset_by_name( + project_name, + product_name, + folder_id, + fields=["_id"] + ) + if not product_doc: + return [] + + repre_names = set() + for repre_doc in self._repre_docs_by_id.values(): + repre_names.add(repre_doc["name"]) + + # TODO where to take version ids? + version_ids = [] + repre_docs = get_representations( + project_name, + representation_names=repre_names, + version_ids=version_ids, + fields=["_id"] + ) + return [repre_doc["_id"] for repre_doc in repre_docs] + + def _get_current_output_repre_ids_xox(self, folder_id, selected_repre): + product_names = { + product_doc["name"] + for product_doc in self._product_docs_by_id.values() + } + + project_name = self._project_name + product_docs = get_subsets( + project_name, + asset_ids=[folder_id], + subset_names=product_names, + fields=["_id", "name"] + ) + product_name_by_id = { + product_doc["_id"]: product_doc["name"] + for product_doc in product_docs + } + product_ids = list(product_name_by_id.keys()) + last_versions_by_product_id = self.find_last_versions(product_ids) + last_version_id_by_product_name = {} + for product_id, last_version in last_versions_by_product_id.items(): + product_name = product_name_by_id[product_id] + last_version_id_by_product_name[product_name] = ( + last_version["_id"] + ) + + repre_docs = get_representations( + project_name, + version_ids=last_version_id_by_product_name.values(), + representation_names=[selected_repre], + fields=["_id"] + ) + return [repre_doc["_id"] for repre_doc in repre_docs] + + def _get_current_output_repre_ids_xoo(self, folder_id): + project_name = self._project_name + repres_by_product_name = collections.defaultdict(set) + for repre_doc in self._repre_docs_by_id.values(): + version_doc = self._version_docs_by_id[repre_doc["parent"]] + product_doc = self._product_docs_by_id[version_doc["parent"]] + product_name = product_doc["name"] + repres_by_product_name[product_name].add(repre_doc["name"]) + + product_docs = list(get_subsets( + project_name, + asset_ids=[folder_id], + subset_names=repres_by_product_name.keys(), + fields=["_id", "name"] + )) + product_name_by_id = { + product_doc["_id"]: product_doc["name"] + for product_doc in product_docs + } + product_ids = list(product_name_by_id.keys()) + last_versions_by_product_id = self.find_last_versions(product_ids) + last_version_id_by_product_name = {} + for product_id, last_version in last_versions_by_product_id.items(): + product_name = product_name_by_id[product_id] + last_version_id_by_product_name[product_name] = ( + last_version["_id"] + ) + + repre_names_by_version_id = {} + for product_name, repre_names in repres_by_product_name.items(): + version_id = last_version_id_by_product_name.get(product_name) + # This should not happen but why to crash? + if version_id is not None: + repre_names_by_version_id[version_id] = list(repre_names) + + repre_docs = get_representations( + project_name, + names_by_version_ids=repre_names_by_version_id, + fields=["_id"] + ) + return [repre_doc["_id"] for repre_doc in repre_docs] + + def _get_current_output_repre_ids_oxx( + self, product_name, selected_repre + ): + project_name = self._project_name + product_docs = get_subsets( + project_name, + asset_ids=self._folder_docs_by_id.keys(), + subset_names=[product_name], + fields=["_id"] + ) + product_ids = [product_doc["_id"] for product_doc in product_docs] + last_versions_by_product_id = self.find_last_versions(product_ids) + last_version_ids = [ + last_version["_id"] + for last_version in last_versions_by_product_id.values() + ] + repre_docs = get_representations( + project_name, + version_ids=last_version_ids, + representation_names=[selected_repre], + fields=["_id"] + ) + return [repre_doc["_id"] for repre_doc in repre_docs] + + def _get_current_output_repre_ids_oxo(self, product_name): + project_name = self._project_name + product_docs = get_subsets( + project_name, + asset_ids=self._folder_docs_by_id.keys(), + subset_names=[product_name], + fields=["_id", "parent"] + ) + product_docs_by_id = { + product_doc["_id"]: product_doc + for product_doc in product_docs + } + if not product_docs: + return list() + + last_versions_by_product_id = self.find_last_versions( + product_docs_by_id.keys() + ) + + product_id_by_version_id = {} + for product_id, last_version in last_versions_by_product_id.items(): + version_id = last_version["_id"] + product_id_by_version_id[version_id] = product_id + + if not product_id_by_version_id: + return list() + + repre_names_by_folder_id = collections.defaultdict(set) + for repre_doc in self._repre_docs_by_id.values(): + version_doc = self._version_docs_by_id[repre_doc["parent"]] + product_doc = self._product_docs_by_id[version_doc["parent"]] + folder_doc = self._folder_docs_by_id[product_doc["parent"]] + folder_id = folder_doc["_id"] + repre_names_by_folder_id[folder_id].add(repre_doc["name"]) + + repre_names_by_version_id = {} + for last_version_id, product_id in product_id_by_version_id.items(): + product_doc = product_docs_by_id[product_id] + folder_id = product_doc["parent"] + repre_names = repre_names_by_folder_id.get(folder_id) + if not repre_names: + continue + repre_names_by_version_id[last_version_id] = repre_names + + repre_docs = get_representations( + project_name, + names_by_version_ids=repre_names_by_version_id, + fields=["_id"] + ) + return [repre_doc["_id"] for repre_doc in repre_docs] + + def _get_current_output_repre_ids_oox(self, selected_repre): + project_name = self._project_name + repre_docs = get_representations( + project_name, + representation_names=[selected_repre], + version_ids=self._version_docs_by_id.keys(), + fields=["_id"] + ) + return [repre_doc["_id"] for repre_doc in repre_docs] + + def _get_product_box_values(self): + project_name = self._project_name + selected_folder_id = self._folders_field.get_selected_folder_id() + if selected_folder_id: + folder_ids = [selected_folder_id] + else: + folder_ids = list(self._folder_docs_by_id.keys()) + + product_docs = get_subsets( + project_name, + asset_ids=folder_ids, + fields=["parent", "name"] + ) + + product_names_by_parent_id = collections.defaultdict(set) + for product_doc in product_docs: + product_names_by_parent_id[product_doc["parent"]].add( + product_doc["name"] + ) + + possible_product_names = None + for product_names in product_names_by_parent_id.values(): + if possible_product_names is None: + possible_product_names = product_names + else: + possible_product_names = possible_product_names.intersection( + product_names) + + if not possible_product_names: + break + + if not possible_product_names: + return [] + return list(possible_product_names) + + def _representations_box_values(self): + # NOTE hero versions are not used because it is expected that + # hero version has same representations as latests + project_name = self._project_name + selected_folder_id = self._folders_field.get_selected_folder_id() + selected_product_name = self._products_combox.currentText() + + # If nothing is selected + # [ ] [ ] [?] + if not selected_folder_id and not selected_product_name: + # Find all representations of selection's products + possible_repres = get_representations( + project_name, + version_ids=self._version_docs_by_id.keys(), + fields=["parent", "name"] + ) + + possible_repres_by_parent = collections.defaultdict(set) + for repre in possible_repres: + possible_repres_by_parent[repre["parent"]].add(repre["name"]) + + output_repres = None + for repre_names in possible_repres_by_parent.values(): + if output_repres is None: + output_repres = repre_names + else: + output_repres = (output_repres & repre_names) + + if not output_repres: + break + + return list(output_repres or list()) + + # [x] [x] [?] + if selected_folder_id and selected_product_name: + product_doc = get_subset_by_name( + project_name, + selected_product_name, + selected_folder_id, + fields=["_id"] + ) + + product_id = product_doc["_id"] + last_versions_by_product_id = self.find_last_versions([product_id]) + version_doc = last_versions_by_product_id.get(product_id) + repre_docs = get_representations( + project_name, + version_ids=[version_doc["_id"]], + fields=["name"] + ) + return [ + repre_doc["name"] + for repre_doc in repre_docs + ] + + # [x] [ ] [?] + # If only folder is selected + if selected_folder_id: + # Filter products by names from content + product_names = { + product_doc["name"] + for product_doc in self._product_docs_by_id.values() + } + + product_docs = get_subsets( + project_name, + asset_ids=[selected_folder_id], + subset_names=product_names, + fields=["_id"] + ) + product_ids = { + product_doc["_id"] + for product_doc in product_docs + } + if not product_ids: + return list() + + last_versions_by_product_id = self.find_last_versions(product_ids) + product_id_by_version_id = {} + for product_id, last_version in ( + last_versions_by_product_id.items() + ): + version_id = last_version["_id"] + product_id_by_version_id[version_id] = product_id + + if not product_id_by_version_id: + return list() + + repre_docs = list(get_representations( + project_name, + version_ids=product_id_by_version_id.keys(), + fields=["name", "parent"] + )) + if not repre_docs: + return list() + + repre_names_by_parent = collections.defaultdict(set) + for repre_doc in repre_docs: + repre_names_by_parent[repre_doc["parent"]].add( + repre_doc["name"] + ) + + available_repres = None + for repre_names in repre_names_by_parent.values(): + if available_repres is None: + available_repres = repre_names + continue + + available_repres = available_repres.intersection(repre_names) + + return list(available_repres) + + # [ ] [x] [?] + product_docs = list(get_subsets( + project_name, + asset_ids=self._folder_docs_by_id.keys(), + subset_names=[selected_product_name], + fields=["_id", "parent"] + )) + if not product_docs: + return list() + + product_docs_by_id = { + product_doc["_id"]: product_doc + for product_doc in product_docs + } + last_versions_by_product_id = self.find_last_versions( + product_docs_by_id.keys() + ) + + product_id_by_version_id = {} + for product_id, last_version in last_versions_by_product_id.items(): + version_id = last_version["_id"] + product_id_by_version_id[version_id] = product_id + + if not product_id_by_version_id: + return list() + + repre_docs = list( + get_representations( + project_name, + version_ids=product_id_by_version_id.keys(), + fields=["name", "parent"] + ) + ) + if not repre_docs: + return list() + + repre_names_by_folder_id = collections.defaultdict(set) + for repre_doc in repre_docs: + product_id = product_id_by_version_id[repre_doc["parent"]] + folder_id = product_docs_by_id[product_id]["parent"] + repre_names_by_folder_id[folder_id].add(repre_doc["name"]) + + available_repres = None + for repre_names in repre_names_by_folder_id.values(): + if available_repres is None: + available_repres = repre_names + continue + + available_repres = available_repres.intersection(repre_names) + + return list(available_repres) + + def _is_folder_ok(self, validation_state): + selected_folder_id = self._folders_field.get_selected_folder_id() + if ( + selected_folder_id is None + and (self._missing_docs or self._inactive_folder_ids) + ): + validation_state.folder_ok = False + + def _is_product_ok(self, validation_state): + selected_folder_id = self._folders_field.get_selected_folder_id() + selected_product_name = self._products_combox.get_valid_value() + + # [?] [x] [?] + # If product is selected then must be ok + if selected_product_name is not None: + return + + # [ ] [ ] [?] + if selected_folder_id is None: + # If there were archived products and folder is not selected + if self._inactive_product_ids: + validation_state.product_ok = False + return + + # [x] [ ] [?] + project_name = self._project_name + product_docs = get_subsets( + project_name, asset_ids=[selected_folder_id], fields=["name"] + ) + + product_names = set( + product_doc["name"] + for product_doc in product_docs + ) + + for product_doc in self._product_docs_by_id.values(): + if product_doc["name"] not in product_names: + validation_state.product_ok = False + break + + def _is_repre_ok(self, validation_state): + selected_folder_id = self._folders_field.get_selected_folder_id() + selected_product_name = self._products_combox.get_valid_value() + selected_repre = self._representations_box.get_valid_value() + + # [?] [?] [x] + # If product is selected then must be ok + if selected_repre is not None: + return + + # [ ] [ ] [ ] + if selected_folder_id is None and selected_product_name is None: + if ( + self._inactive_repre_ids + or self._missing_version_ids + or self._missing_repre_ids + ): + validation_state.repre_ok = False + return + + # [x] [x] [ ] + project_name = self._project_name + if ( + selected_folder_id is not None + and selected_product_name is not None + ): + product_doc = get_subset_by_name( + project_name, + selected_product_name, + selected_folder_id, + fields=["_id"] + ) + product_id = product_doc["_id"] + last_versions_by_product_id = self.find_last_versions([product_id]) + last_version = last_versions_by_product_id.get(product_id) + if not last_version: + validation_state.repre_ok = False + return + + repre_docs = get_representations( + project_name, + version_ids=[last_version["_id"]], + fields=["name"] + ) + + repre_names = set( + repre_doc["name"] + for repre_doc in repre_docs + ) + for repre_doc in self._repre_docs_by_id.values(): + if repre_doc["name"] not in repre_names: + validation_state.repre_ok = False + break + return + + # [x] [ ] [ ] + if selected_folder_id is not None: + product_docs = list(get_subsets( + project_name, + asset_ids=[selected_folder_id], + fields=["_id", "name"] + )) + + product_name_by_id = {} + product_ids = set() + for product_doc in product_docs: + product_id = product_doc["_id"] + product_ids.add(product_id) + product_name_by_id[product_id] = product_doc["name"] + + last_versions_by_product_id = self.find_last_versions(product_ids) + + product_id_by_version_id = {} + for product_id, last_version in ( + last_versions_by_product_id.items() + ): + version_id = last_version["_id"] + product_id_by_version_id[version_id] = product_id + + repre_docs = get_representations( + project_name, + version_ids=product_id_by_version_id.keys(), + fields=["name", "parent"] + ) + repres_by_product_name = collections.defaultdict(set) + for repre_doc in repre_docs: + product_id = product_id_by_version_id[repre_doc["parent"]] + product_name = product_name_by_id[product_id] + repres_by_product_name[product_name].add(repre_doc["name"]) + + for repre_doc in self._repre_docs_by_id.values(): + version_doc = self._version_docs_by_id[repre_doc["parent"]] + product_doc = self._product_docs_by_id[version_doc["parent"]] + repre_names = repres_by_product_name[product_doc["name"]] + if repre_doc["name"] not in repre_names: + validation_state.repre_ok = False + break + return + + # [ ] [x] [ ] + # Product documents + product_docs = get_subsets( + project_name, + asset_ids=self._folder_docs_by_id.keys(), + subset_names=[selected_product_name], + fields=["_id", "name", "parent"] + ) + product_docs_by_id = {} + for product_doc in product_docs: + product_docs_by_id[product_doc["_id"]] = product_doc + + last_versions_by_product_id = self.find_last_versions( + product_docs_by_id.keys() + ) + product_id_by_version_id = {} + for product_id, last_version in last_versions_by_product_id.items(): + version_id = last_version["_id"] + product_id_by_version_id[version_id] = product_id + + repre_docs = get_representations( + project_name, + version_ids=product_id_by_version_id.keys(), + fields=["name", "parent"] + ) + repres_by_folder_id = collections.defaultdict(set) + for repre_doc in repre_docs: + product_id = product_id_by_version_id[repre_doc["parent"]] + folder_id = product_docs_by_id[product_id]["parent"] + repres_by_folder_id[folder_id].add(repre_doc["name"]) + + for repre_doc in self._repre_docs_by_id.values(): + version_doc = self._version_docs_by_id[repre_doc["parent"]] + product_doc = self._product_docs_by_id[version_doc["parent"]] + folder_id = product_doc["parent"] + repre_names = repres_by_folder_id[folder_id] + if repre_doc["name"] not in repre_names: + validation_state.repre_ok = False + break + + def _on_current_folder(self): + # Set initial folder as current. + folder_id = self._controller.get_current_folder_id() + if not folder_id: + return + + selected_folder_id = self._folders_field.get_selected_folder_id() + if folder_id == selected_folder_id: + return + + self._folders_field.set_selected_item(folder_id) + self._combobox_value_changed() + + def _on_accept(self): + self._trigger_switch() + + def _trigger_switch(self, loader=None): + # Use None when not a valid value or when placeholder value + selected_folder_id = self._folders_field.get_selected_folder_id() + selected_product_name = self._products_combox.get_valid_value() + selected_representation = self._representations_box.get_valid_value() + + project_name = self._project_name + if selected_folder_id: + folder_ids = {selected_folder_id} + else: + folder_ids = set(self._folder_docs_by_id.keys()) + + product_names = None + if selected_product_name: + product_names = [selected_product_name] + + product_docs = list(get_subsets( + project_name, + subset_names=product_names, + asset_ids=folder_ids + )) + product_ids = set() + product_docs_by_parent_and_name = collections.defaultdict(dict) + for product_doc in product_docs: + product_ids.add(product_doc["_id"]) + folder_id = product_doc["parent"] + name = product_doc["name"] + product_docs_by_parent_and_name[folder_id][name] = product_doc + + # versions + _version_docs = get_versions(project_name, subset_ids=product_ids) + version_docs = list(reversed( + sorted(_version_docs, key=lambda item: item["name"]) + )) + + hero_version_docs = list(get_hero_versions( + project_name, subset_ids=product_ids + )) + + version_ids = set() + version_docs_by_parent_id = {} + for version_doc in version_docs: + parent_id = version_doc["parent"] + if parent_id not in version_docs_by_parent_id: + version_ids.add(version_doc["_id"]) + version_docs_by_parent_id[parent_id] = version_doc + + hero_version_docs_by_parent_id = {} + for hero_version_doc in hero_version_docs: + version_ids.add(hero_version_doc["_id"]) + parent_id = hero_version_doc["parent"] + hero_version_docs_by_parent_id[parent_id] = hero_version_doc + + repre_docs = get_representations( + project_name, version_ids=version_ids + ) + repre_docs_by_parent_id_by_name = collections.defaultdict(dict) + for repre_doc in repre_docs: + parent_id = repre_doc["parent"] + name = repre_doc["name"] + repre_docs_by_parent_id_by_name[parent_id][name] = repre_doc + + for container in self._items: + self._switch_container( + container, + loader, + selected_folder_id, + selected_product_name, + selected_representation, + product_docs_by_parent_and_name, + version_docs_by_parent_id, + hero_version_docs_by_parent_id, + repre_docs_by_parent_id_by_name, + ) + + self.switched.emit() + + self.close() + + def _switch_container( + self, + container, + loader, + selected_folder_id, + product_name, + selected_representation, + product_docs_by_parent_and_name, + version_docs_by_parent_id, + hero_version_docs_by_parent_id, + repre_docs_by_parent_id_by_name, + ): + container_repre_id = container["representation"] + container_repre = self._repre_docs_by_id[container_repre_id] + container_repre_name = container_repre["name"] + container_version_id = container_repre["parent"] + + container_version = self._version_docs_by_id[container_version_id] + + container_product_id = container_version["parent"] + container_product = self._product_docs_by_id[container_product_id] + + if selected_folder_id: + folder_id = selected_folder_id + else: + folder_id = container_product["parent"] + + products_by_name = product_docs_by_parent_and_name[folder_id] + if product_name: + product_doc = products_by_name[product_name] + else: + product_doc = products_by_name[container_product["name"]] + + repre_doc = None + product_id = product_doc["_id"] + if container_version["type"] == "hero_version": + hero_version = hero_version_docs_by_parent_id.get( + product_id + ) + if hero_version: + _repres = repre_docs_by_parent_id_by_name.get( + hero_version["_id"] + ) + if selected_representation: + repre_doc = _repres.get(selected_representation) + else: + repre_doc = _repres.get(container_repre_name) + + if not repre_doc: + version_doc = version_docs_by_parent_id[product_id] + version_id = version_doc["_id"] + repres_by_name = repre_docs_by_parent_id_by_name[version_id] + if selected_representation: + repre_doc = repres_by_name[selected_representation] + else: + repre_doc = repres_by_name[container_repre_name] + + error = None + try: + switch_container(container, repre_doc, loader) + except ( + LoaderSwitchNotImplementedError, + IncompatibleLoaderError, + LoaderNotFoundError, + ) as exc: + error = str(exc) + except Exception: + error = ( + "Switch asset failed. " + "Search console log for more details." + ) + if error is not None: + log.warning(( + "Couldn't switch asset." + "See traceback for more information." + ), exc_info=True) + dialog = QtWidgets.QMessageBox(self) + dialog.setWindowTitle("Switch asset failed") + dialog.setText(error) + dialog.exec_() diff --git a/openpype/tools/ayon_sceneinventory/switch_dialog/folders_input.py b/openpype/tools/ayon_sceneinventory/switch_dialog/folders_input.py new file mode 100644 index 0000000000..699c62371a --- /dev/null +++ b/openpype/tools/ayon_sceneinventory/switch_dialog/folders_input.py @@ -0,0 +1,307 @@ +from qtpy import QtWidgets, QtCore +import qtawesome + +from openpype.tools.utils import ( + PlaceholderLineEdit, + BaseClickableFrame, + set_style_property, +) +from openpype.tools.ayon_utils.widgets import FoldersWidget + +NOT_SET = object() + + +class ClickableLineEdit(QtWidgets.QLineEdit): + """QLineEdit capturing left mouse click. + + Triggers `clicked` signal on mouse click. + """ + clicked = QtCore.Signal() + + def __init__(self, *args, **kwargs): + super(ClickableLineEdit, self).__init__(*args, **kwargs) + self.setReadOnly(True) + self._mouse_pressed = False + + def mousePressEvent(self, event): + if event.button() == QtCore.Qt.LeftButton: + self._mouse_pressed = True + event.accept() + + def mouseMoveEvent(self, event): + event.accept() + + def mouseReleaseEvent(self, event): + if self._mouse_pressed: + self._mouse_pressed = False + if self.rect().contains(event.pos()): + self.clicked.emit() + event.accept() + + def mouseDoubleClickEvent(self, event): + event.accept() + + +class ControllerWrap: + def __init__(self, controller): + self._controller = controller + self._selected_folder_id = None + + def emit_event(self, *args, **kwargs): + self._controller.emit_event(*args, **kwargs) + + def register_event_callback(self, *args, **kwargs): + self._controller.register_event_callback(*args, **kwargs) + + def get_current_project_name(self): + return self._controller.get_current_project_name() + + def get_folder_items(self, *args, **kwargs): + return self._controller.get_folder_items(*args, **kwargs) + + def set_selected_folder(self, folder_id): + self._selected_folder_id = folder_id + + def get_selected_folder_id(self): + return self._selected_folder_id + + +class FoldersDialog(QtWidgets.QDialog): + """Dialog to select asset for a context of instance.""" + + def __init__(self, controller, parent): + super(FoldersDialog, self).__init__(parent) + self.setWindowTitle("Select folder") + + filter_input = PlaceholderLineEdit(self) + filter_input.setPlaceholderText("Filter folders..") + + controller_wrap = ControllerWrap(controller) + folders_widget = FoldersWidget(controller_wrap, self) + folders_widget.set_deselectable(True) + + ok_btn = QtWidgets.QPushButton("OK", self) + cancel_btn = QtWidgets.QPushButton("Cancel", self) + + btns_layout = QtWidgets.QHBoxLayout() + btns_layout.addStretch(1) + btns_layout.addWidget(ok_btn) + btns_layout.addWidget(cancel_btn) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(filter_input, 0) + layout.addWidget(folders_widget, 1) + layout.addLayout(btns_layout, 0) + + folders_widget.double_clicked.connect(self._on_ok_clicked) + folders_widget.refreshed.connect(self._on_folders_refresh) + filter_input.textChanged.connect(self._on_filter_change) + ok_btn.clicked.connect(self._on_ok_clicked) + cancel_btn.clicked.connect(self._on_cancel_clicked) + + self._filter_input = filter_input + self._ok_btn = ok_btn + self._cancel_btn = cancel_btn + + self._folders_widget = folders_widget + self._controller_wrap = controller_wrap + + # Set selected folder only when user confirms the dialog + self._selected_folder_id = None + self._selected_folder_label = None + + self._folder_id_to_select = NOT_SET + + self._first_show = True + self._default_height = 500 + + def showEvent(self, event): + """Refresh asset model on show.""" + super(FoldersDialog, self).showEvent(event) + if self._first_show: + self._first_show = False + self._on_first_show() + + def refresh(self): + project_name = self._controller_wrap.get_current_project_name() + self._folders_widget.set_project_name(project_name) + + def _on_first_show(self): + center = self.rect().center() + size = self.size() + size.setHeight(self._default_height) + + self.resize(size) + new_pos = self.mapToGlobal(center) + new_pos.setX(new_pos.x() - int(self.width() / 2)) + new_pos.setY(new_pos.y() - int(self.height() / 2)) + self.move(new_pos) + + def _on_folders_refresh(self): + if self._folder_id_to_select is NOT_SET: + return + self._folders_widget.set_selected_folder(self._folder_id_to_select) + self._folder_id_to_select = NOT_SET + + def _on_filter_change(self, text): + """Trigger change of filter of folders.""" + + self._folders_widget.set_name_filter(text) + + def _on_cancel_clicked(self): + self.done(0) + + def _on_ok_clicked(self): + self._selected_folder_id = ( + self._folders_widget.get_selected_folder_id() + ) + self._selected_folder_label = ( + self._folders_widget.get_selected_folder_label() + ) + self.done(1) + + def set_selected_folder(self, folder_id): + """Change preselected folder before showing the dialog. + + This also resets model and clean filter. + """ + + if ( + self._folders_widget.is_refreshing + or self._folders_widget.get_project_name() is None + ): + self._folder_id_to_select = folder_id + else: + self._folders_widget.set_selected_folder(folder_id) + + def get_selected_folder_id(self): + """Get selected folder id. + + Returns: + Union[str, None]: Selected folder id or None if nothing + is selected. + """ + return self._selected_folder_id + + def get_selected_folder_label(self): + return self._selected_folder_label + + +class FoldersField(BaseClickableFrame): + """Field where asset name of selected instance/s is showed. + + Click on the field will trigger `FoldersDialog`. + """ + value_changed = QtCore.Signal() + + def __init__(self, controller, parent): + super(FoldersField, self).__init__(parent) + self.setObjectName("AssetNameInputWidget") + + # Don't use 'self' for parent! + # - this widget has specific styles + dialog = FoldersDialog(controller, parent) + + name_input = ClickableLineEdit(self) + name_input.setObjectName("AssetNameInput") + + icon = qtawesome.icon("fa.window-maximize", color="white") + icon_btn = QtWidgets.QPushButton(self) + icon_btn.setIcon(icon) + icon_btn.setObjectName("AssetNameInputButton") + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + layout.addWidget(name_input, 1) + layout.addWidget(icon_btn, 0) + + # Make sure all widgets are vertically extended to highest widget + for widget in ( + name_input, + icon_btn + ): + w_size_policy = widget.sizePolicy() + w_size_policy.setVerticalPolicy( + QtWidgets.QSizePolicy.MinimumExpanding) + widget.setSizePolicy(w_size_policy) + + size_policy = self.sizePolicy() + size_policy.setVerticalPolicy(QtWidgets.QSizePolicy.Maximum) + self.setSizePolicy(size_policy) + + name_input.clicked.connect(self._mouse_release_callback) + icon_btn.clicked.connect(self._mouse_release_callback) + dialog.finished.connect(self._on_dialog_finish) + + self._controller = controller + self._dialog = dialog + self._name_input = name_input + self._icon_btn = icon_btn + + self._selected_folder_id = None + self._selected_folder_label = None + self._selected_items = [] + self._is_valid = True + + def refresh(self): + self._dialog.refresh() + + def is_valid(self): + """Is asset valid.""" + return self._is_valid + + def get_selected_folder_id(self): + """Selected asset names.""" + return self._selected_folder_id + + def get_selected_folder_label(self): + return self._selected_folder_label + + def set_text(self, text): + """Set text in text field. + + Does not change selected items (assets). + """ + self._name_input.setText(text) + + def set_valid(self, is_valid): + state = "" + if not is_valid: + state = "invalid" + self._set_state_property(state) + + def set_selected_item(self, folder_id=None, folder_label=None): + """Set folder for selection. + + Args: + folder_id (Optional[str]): Folder id to select. + folder_label (Optional[str]): Folder label. + """ + + self._selected_folder_id = folder_id + if not folder_id: + folder_label = None + elif folder_id and not folder_label: + folder_label = self._controller.get_folder_label(folder_id) + self._selected_folder_label = folder_label + self.set_text(folder_label if folder_label else "") + + def _on_dialog_finish(self, result): + if not result: + return + + folder_id = self._dialog.get_selected_folder_id() + folder_label = self._dialog.get_selected_folder_label() + self.set_selected_item(folder_id, folder_label) + + self.value_changed.emit() + + def _mouse_release_callback(self): + self._dialog.set_selected_folder(self._selected_folder_id) + self._dialog.open() + + def _set_state_property(self, state): + set_style_property(self, "state", state) + set_style_property(self._name_input, "state", state) + set_style_property(self._icon_btn, "state", state) diff --git a/openpype/tools/ayon_sceneinventory/switch_dialog/widgets.py b/openpype/tools/ayon_sceneinventory/switch_dialog/widgets.py new file mode 100644 index 0000000000..50a49e0ce1 --- /dev/null +++ b/openpype/tools/ayon_sceneinventory/switch_dialog/widgets.py @@ -0,0 +1,94 @@ +from qtpy import QtWidgets, QtCore + +from openpype import style + + +class ButtonWithMenu(QtWidgets.QToolButton): + def __init__(self, parent=None): + super(ButtonWithMenu, self).__init__(parent) + + self.setObjectName("ButtonWithMenu") + + self.setPopupMode(QtWidgets.QToolButton.MenuButtonPopup) + menu = QtWidgets.QMenu(self) + + self.setMenu(menu) + + self._menu = menu + self._actions = [] + + def menu(self): + return self._menu + + def clear_actions(self): + if self._menu is not None: + self._menu.clear() + self._actions = [] + + def add_action(self, action): + self._actions.append(action) + self._menu.addAction(action) + + def _on_action_trigger(self): + action = self.sender() + if action not in self._actions: + return + action.trigger() + + +class SearchComboBox(QtWidgets.QComboBox): + """Searchable ComboBox with empty placeholder value as first value""" + + def __init__(self, parent): + super(SearchComboBox, self).__init__(parent) + + self.setEditable(True) + self.setInsertPolicy(QtWidgets.QComboBox.NoInsert) + + combobox_delegate = QtWidgets.QStyledItemDelegate(self) + self.setItemDelegate(combobox_delegate) + + completer = self.completer() + completer.setCompletionMode( + QtWidgets.QCompleter.PopupCompletion + ) + completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive) + + completer_view = completer.popup() + completer_view.setObjectName("CompleterView") + completer_delegate = QtWidgets.QStyledItemDelegate(completer_view) + completer_view.setItemDelegate(completer_delegate) + completer_view.setStyleSheet(style.load_stylesheet()) + + self._combobox_delegate = combobox_delegate + + self._completer_delegate = completer_delegate + self._completer = completer + + def set_placeholder(self, placeholder): + self.lineEdit().setPlaceholderText(placeholder) + + def populate(self, items): + self.clear() + self.addItems([""]) # ensure first item is placeholder + self.addItems(items) + + def get_valid_value(self): + """Return the current text if it's a valid value else None + + Note: The empty placeholder value is valid and returns as "" + + """ + + text = self.currentText() + lookup = set(self.itemText(i) for i in range(self.count())) + if text not in lookup: + return None + + return text or None + + def set_valid_value(self, value): + """Try to locate 'value' and pre-select it in dropdown.""" + index = self.findText(value) + if index > -1: + self.setCurrentIndex(index) diff --git a/openpype/tools/ayon_sceneinventory/view.py b/openpype/tools/ayon_sceneinventory/view.py new file mode 100644 index 0000000000..039b498b1b --- /dev/null +++ b/openpype/tools/ayon_sceneinventory/view.py @@ -0,0 +1,825 @@ +import uuid +import collections +import logging +import itertools +from functools import partial + +from qtpy import QtWidgets, QtCore +import qtawesome + +from openpype.client import ( + get_version_by_id, + get_versions, + get_hero_versions, + get_representation_by_id, + get_representations, +) +from openpype import style +from openpype.pipeline import ( + HeroVersionType, + update_container, + remove_container, + discover_inventory_actions, +) +from openpype.tools.utils.lib import ( + iter_model_rows, + format_version +) + +from .switch_dialog import SwitchAssetDialog +from .model import InventoryModel + + +DEFAULT_COLOR = "#fb9c15" + +log = logging.getLogger("SceneInventory") + + +class SceneInventoryView(QtWidgets.QTreeView): + data_changed = QtCore.Signal() + hierarchy_view_changed = QtCore.Signal(bool) + + def __init__(self, controller, parent): + super(SceneInventoryView, self).__init__(parent=parent) + + # view settings + self.setIndentation(12) + self.setAlternatingRowColors(True) + self.setSortingEnabled(True) + self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) + self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + + self.customContextMenuRequested.connect(self._show_right_mouse_menu) + + self._hierarchy_view = False + self._selected = None + + self._controller = controller + + def _set_hierarchy_view(self, enabled): + if enabled == self._hierarchy_view: + return + self._hierarchy_view = enabled + self.hierarchy_view_changed.emit(enabled) + + def _enter_hierarchy(self, items): + self._selected = set(i["objectName"] for i in items) + self._set_hierarchy_view(True) + self.data_changed.emit() + self.expandToDepth(1) + self.setStyleSheet(""" + QTreeView { + border-color: #fb9c15; + } + """) + + def _leave_hierarchy(self): + self._set_hierarchy_view(False) + self.data_changed.emit() + self.setStyleSheet("QTreeView {}") + + def _build_item_menu_for_selection(self, items, menu): + # Exclude items that are "NOT FOUND" since setting versions, updating + # and removal won't work for those items. + items = [item for item in items if not item.get("isNotFound")] + if not items: + return + + # An item might not have a representation, for example when an item + # is listed as "NOT FOUND" + repre_ids = set() + for item in items: + repre_id = item["representation"] + try: + uuid.UUID(repre_id) + repre_ids.add(repre_id) + except ValueError: + pass + + project_name = self._controller.get_current_project_name() + repre_docs = get_representations( + project_name, representation_ids=repre_ids, fields=["parent"] + ) + + version_ids = { + repre_doc["parent"] + for repre_doc in repre_docs + } + + loaded_versions = get_versions( + project_name, version_ids=version_ids, hero=True + ) + + loaded_hero_versions = [] + versions_by_parent_id = collections.defaultdict(list) + subset_ids = set() + for version in loaded_versions: + if version["type"] == "hero_version": + loaded_hero_versions.append(version) + else: + parent_id = version["parent"] + versions_by_parent_id[parent_id].append(version) + subset_ids.add(parent_id) + + all_versions = get_versions( + project_name, subset_ids=subset_ids, hero=True + ) + hero_versions = [] + versions = [] + for version in all_versions: + if version["type"] == "hero_version": + hero_versions.append(version) + else: + versions.append(version) + + has_loaded_hero_versions = len(loaded_hero_versions) > 0 + has_available_hero_version = len(hero_versions) > 0 + has_outdated = False + + for version in versions: + parent_id = version["parent"] + current_versions = versions_by_parent_id[parent_id] + for current_version in current_versions: + if current_version["name"] < version["name"]: + has_outdated = True + break + + if has_outdated: + break + + switch_to_versioned = None + if has_loaded_hero_versions: + def _on_switch_to_versioned(items): + repre_ids = { + item["representation"] + for item in items + } + + repre_docs = get_representations( + project_name, + representation_ids=repre_ids, + fields=["parent"] + ) + + version_ids = set() + version_id_by_repre_id = {} + for repre_doc in repre_docs: + version_id = repre_doc["parent"] + repre_id = str(repre_doc["_id"]) + version_id_by_repre_id[repre_id] = version_id + version_ids.add(version_id) + + hero_versions = get_hero_versions( + project_name, + version_ids=version_ids, + fields=["version_id"] + ) + + hero_src_version_ids = set() + for hero_version in hero_versions: + version_id = hero_version["version_id"] + hero_src_version_ids.add(version_id) + hero_version_id = hero_version["_id"] + for _repre_id, current_version_id in ( + version_id_by_repre_id.items() + ): + if current_version_id == hero_version_id: + version_id_by_repre_id[_repre_id] = version_id + + version_docs = get_versions( + project_name, + version_ids=hero_src_version_ids, + fields=["name"] + ) + version_name_by_id = {} + for version_doc in version_docs: + version_name_by_id[version_doc["_id"]] = \ + version_doc["name"] + + # Specify version per item to update to + update_items = [] + update_versions = [] + for item in items: + repre_id = item["representation"] + version_id = version_id_by_repre_id.get(repre_id) + version_name = version_name_by_id.get(version_id) + if version_name is not None: + update_items.append(item) + update_versions.append(version_name) + self._update_containers(update_items, update_versions) + + update_icon = qtawesome.icon( + "fa.asterisk", + color=DEFAULT_COLOR + ) + switch_to_versioned = QtWidgets.QAction( + update_icon, + "Switch to versioned", + menu + ) + switch_to_versioned.triggered.connect( + lambda: _on_switch_to_versioned(items) + ) + + update_to_latest_action = None + if has_outdated or has_loaded_hero_versions: + update_icon = qtawesome.icon( + "fa.angle-double-up", + color=DEFAULT_COLOR + ) + update_to_latest_action = QtWidgets.QAction( + update_icon, + "Update to latest", + menu + ) + update_to_latest_action.triggered.connect( + lambda: self._update_containers(items, version=-1) + ) + + change_to_hero = None + if has_available_hero_version: + # TODO change icon + change_icon = qtawesome.icon( + "fa.asterisk", + color="#00b359" + ) + change_to_hero = QtWidgets.QAction( + change_icon, + "Change to hero", + menu + ) + change_to_hero.triggered.connect( + lambda: self._update_containers(items, + version=HeroVersionType(-1)) + ) + + # set version + set_version_icon = qtawesome.icon("fa.hashtag", color=DEFAULT_COLOR) + set_version_action = QtWidgets.QAction( + set_version_icon, + "Set version", + menu + ) + set_version_action.triggered.connect( + lambda: self._show_version_dialog(items)) + + # switch folder + switch_folder_icon = qtawesome.icon("fa.sitemap", color=DEFAULT_COLOR) + switch_folder_action = QtWidgets.QAction( + switch_folder_icon, + "Switch Folder", + menu + ) + switch_folder_action.triggered.connect( + lambda: self._show_switch_dialog(items)) + + # remove + remove_icon = qtawesome.icon("fa.remove", color=DEFAULT_COLOR) + remove_action = QtWidgets.QAction(remove_icon, "Remove items", menu) + remove_action.triggered.connect( + lambda: self._show_remove_warning_dialog(items)) + + # add the actions + if switch_to_versioned: + menu.addAction(switch_to_versioned) + + if update_to_latest_action: + menu.addAction(update_to_latest_action) + + if change_to_hero: + menu.addAction(change_to_hero) + + menu.addAction(set_version_action) + menu.addAction(switch_folder_action) + + menu.addSeparator() + + menu.addAction(remove_action) + + self._handle_sync_server(menu, repre_ids) + + def _handle_sync_server(self, menu, repre_ids): + """Adds actions for download/upload when SyncServer is enabled + + Args: + menu (OptionMenu) + repre_ids (list) of object_ids + + Returns: + (OptionMenu) + """ + + if not self._controller.is_sync_server_enabled(): + return + + menu.addSeparator() + + download_icon = qtawesome.icon("fa.download", color=DEFAULT_COLOR) + download_active_action = QtWidgets.QAction( + download_icon, + "Download", + menu + ) + download_active_action.triggered.connect( + lambda: self._add_sites(repre_ids, "active_site")) + + upload_icon = qtawesome.icon("fa.upload", color=DEFAULT_COLOR) + upload_remote_action = QtWidgets.QAction( + upload_icon, + "Upload", + menu + ) + upload_remote_action.triggered.connect( + lambda: self._add_sites(repre_ids, "remote_site")) + + menu.addAction(download_active_action) + menu.addAction(upload_remote_action) + + def _add_sites(self, repre_ids, site_type): + """(Re)sync all 'repre_ids' to specific site. + + It checks if opposite site has fully available content to limit + accidents. (ReSync active when no remote >> losing active content) + + Args: + repre_ids (list) + site_type (Literal[active_site, remote_site]): Site type. + """ + + self._controller.resync_representations(repre_ids, site_type) + + self.data_changed.emit() + + def _build_item_menu(self, items=None): + """Create menu for the selected items""" + + if not items: + items = [] + + menu = QtWidgets.QMenu(self) + + # add the actions + self._build_item_menu_for_selection(items, menu) + + # These two actions should be able to work without selection + # expand all items + expandall_action = QtWidgets.QAction(menu, text="Expand all items") + expandall_action.triggered.connect(self.expandAll) + + # collapse all items + collapse_action = QtWidgets.QAction(menu, text="Collapse all items") + collapse_action.triggered.connect(self.collapseAll) + + menu.addAction(expandall_action) + menu.addAction(collapse_action) + + custom_actions = self._get_custom_actions(containers=items) + if custom_actions: + submenu = QtWidgets.QMenu("Actions", self) + for action in custom_actions: + color = action.color or DEFAULT_COLOR + icon = qtawesome.icon("fa.%s" % action.icon, color=color) + action_item = QtWidgets.QAction(icon, action.label, submenu) + action_item.triggered.connect( + partial(self._process_custom_action, action, items)) + + submenu.addAction(action_item) + + menu.addMenu(submenu) + + # go back to flat view + back_to_flat_action = None + if self._hierarchy_view: + back_to_flat_icon = qtawesome.icon("fa.list", color=DEFAULT_COLOR) + back_to_flat_action = QtWidgets.QAction( + back_to_flat_icon, + "Back to Full-View", + menu + ) + back_to_flat_action.triggered.connect(self._leave_hierarchy) + + # send items to hierarchy view + enter_hierarchy_icon = qtawesome.icon("fa.indent", color="#d8d8d8") + enter_hierarchy_action = QtWidgets.QAction( + enter_hierarchy_icon, + "Cherry-Pick (Hierarchy)", + menu + ) + enter_hierarchy_action.triggered.connect( + lambda: self._enter_hierarchy(items)) + + if items: + menu.addAction(enter_hierarchy_action) + + if back_to_flat_action is not None: + menu.addAction(back_to_flat_action) + + return menu + + def _get_custom_actions(self, containers): + """Get the registered Inventory Actions + + Args: + containers(list): collection of containers + + Returns: + list: collection of filter and initialized actions + """ + + def sorter(Plugin): + """Sort based on order attribute of the plugin""" + return Plugin.order + + # Fedd an empty dict if no selection, this will ensure the compat + # lookup always work, so plugin can interact with Scene Inventory + # reversely. + containers = containers or [dict()] + + # Check which action will be available in the menu + Plugins = discover_inventory_actions() + compatible = [p() for p in Plugins if + any(p.is_compatible(c) for c in containers)] + + return sorted(compatible, key=sorter) + + def _process_custom_action(self, action, containers): + """Run action and if results are returned positive update the view + + If the result is list or dict, will select view items by the result. + + Args: + action (InventoryAction): Inventory Action instance + containers (list): Data of currently selected items + + Returns: + None + """ + + result = action.process(containers) + if result: + self.data_changed.emit() + + if isinstance(result, (list, set)): + self._select_items_by_action(result) + + if isinstance(result, dict): + self._select_items_by_action( + result["objectNames"], result["options"] + ) + + def _select_items_by_action(self, object_names, options=None): + """Select view items by the result of action + + Args: + object_names (list or set): A list/set of container object name + options (dict): GUI operation options. + + Returns: + None + + """ + options = options or dict() + + if options.get("clear", True): + self.clearSelection() + + object_names = set(object_names) + if ( + self._hierarchy_view + and not self._selected.issuperset(object_names) + ): + # If any container not in current cherry-picked view, update + # view before selecting them. + self._selected.update(object_names) + self.data_changed.emit() + + model = self.model() + selection_model = self.selectionModel() + + select_mode = { + "select": QtCore.QItemSelectionModel.Select, + "deselect": QtCore.QItemSelectionModel.Deselect, + "toggle": QtCore.QItemSelectionModel.Toggle, + }[options.get("mode", "select")] + + for index in iter_model_rows(model, 0): + item = index.data(InventoryModel.ItemRole) + if item.get("isGroupNode"): + continue + + name = item.get("objectName") + if name in object_names: + self.scrollTo(index) # Ensure item is visible + flags = select_mode | QtCore.QItemSelectionModel.Rows + selection_model.select(index, flags) + + object_names.remove(name) + + if len(object_names) == 0: + break + + def _show_right_mouse_menu(self, pos): + """Display the menu when at the position of the item clicked""" + + globalpos = self.viewport().mapToGlobal(pos) + + if not self.selectionModel().hasSelection(): + print("No selection") + # Build menu without selection, feed an empty list + menu = self._build_item_menu() + menu.exec_(globalpos) + return + + active = self.currentIndex() # index under mouse + active = active.sibling(active.row(), 0) # get first column + + # move index under mouse + indices = self.get_indices() + if active in indices: + indices.remove(active) + + indices.append(active) + + # Extend to the sub-items + all_indices = self._extend_to_children(indices) + items = [dict(i.data(InventoryModel.ItemRole)) for i in all_indices + if i.parent().isValid()] + + if self._hierarchy_view: + # Ensure no group item + items = [n for n in items if not n.get("isGroupNode")] + + menu = self._build_item_menu(items) + menu.exec_(globalpos) + + def get_indices(self): + """Get the selected rows""" + selection_model = self.selectionModel() + return selection_model.selectedRows() + + def _extend_to_children(self, indices): + """Extend the indices to the children indices. + + Top-level indices are extended to its children indices. Sub-items + are kept as is. + + Args: + indices (list): The indices to extend. + + Returns: + list: The children indices + + """ + def get_children(i): + model = i.model() + rows = model.rowCount(parent=i) + for row in range(rows): + child = model.index(row, 0, parent=i) + yield child + + subitems = set() + for i in indices: + valid_parent = i.parent().isValid() + if valid_parent and i not in subitems: + subitems.add(i) + + if self._hierarchy_view: + # Assume this is a group item + for child in get_children(i): + subitems.add(child) + else: + # is top level item + for child in get_children(i): + subitems.add(child) + + return list(subitems) + + def _show_version_dialog(self, items): + """Create a dialog with the available versions for the selected file + + Args: + items (list): list of items to run the "set_version" for + + Returns: + None + """ + + active = items[-1] + + project_name = self._controller.get_current_project_name() + # Get available versions for active representation + repre_doc = get_representation_by_id( + project_name, + active["representation"], + fields=["parent"] + ) + + repre_version_doc = get_version_by_id( + project_name, + repre_doc["parent"], + fields=["parent"] + ) + + version_docs = list(get_versions( + project_name, + subset_ids=[repre_version_doc["parent"]], + hero=True + )) + hero_version = None + standard_versions = [] + for version_doc in version_docs: + if version_doc["type"] == "hero_version": + hero_version = version_doc + else: + standard_versions.append(version_doc) + versions = list(reversed( + sorted(standard_versions, key=lambda item: item["name"]) + )) + if hero_version: + _version_id = hero_version["version_id"] + for _version in versions: + if _version["_id"] != _version_id: + continue + + hero_version["name"] = HeroVersionType( + _version["name"] + ) + hero_version["data"] = _version["data"] + break + + # Get index among the listed versions + current_item = None + current_version = active["version"] + if isinstance(current_version, HeroVersionType): + current_item = hero_version + else: + for version in versions: + if version["name"] == current_version: + current_item = version + break + + all_versions = [] + if hero_version: + all_versions.append(hero_version) + all_versions.extend(versions) + + if current_item: + index = all_versions.index(current_item) + else: + index = 0 + + versions_by_label = dict() + labels = [] + for version in all_versions: + is_hero = version["type"] == "hero_version" + label = format_version(version["name"], is_hero) + labels.append(label) + versions_by_label[label] = version["name"] + + label, state = QtWidgets.QInputDialog.getItem( + self, + "Set version..", + "Set version number to", + labels, + current=index, + editable=False + ) + if not state: + return + + if label: + version = versions_by_label[label] + self._update_containers(items, version) + + def _show_switch_dialog(self, items): + """Display Switch dialog""" + dialog = SwitchAssetDialog(self._controller, self, items) + dialog.switched.connect(self.data_changed.emit) + dialog.show() + + def _show_remove_warning_dialog(self, items): + """Prompt a dialog to inform the user the action will remove items""" + + accept = QtWidgets.QMessageBox.Ok + buttons = accept | QtWidgets.QMessageBox.Cancel + + state = QtWidgets.QMessageBox.question( + self, + "Are you sure?", + "Are you sure you want to remove {} item(s)".format(len(items)), + buttons=buttons, + defaultButton=accept + ) + + if state != accept: + return + + for item in items: + remove_container(item) + self.data_changed.emit() + + def _show_version_error_dialog(self, version, items): + """Shows QMessageBox when version switch doesn't work + + Args: + version: str or int or None + """ + if version == -1: + version_str = "latest" + elif isinstance(version, HeroVersionType): + version_str = "hero" + elif isinstance(version, int): + version_str = "v{:03d}".format(version) + else: + version_str = version + + dialog = QtWidgets.QMessageBox(self) + dialog.setIcon(QtWidgets.QMessageBox.Warning) + dialog.setStyleSheet(style.load_stylesheet()) + dialog.setWindowTitle("Update failed") + + switch_btn = dialog.addButton( + "Switch Folder", + QtWidgets.QMessageBox.ActionRole + ) + switch_btn.clicked.connect(lambda: self._show_switch_dialog(items)) + + dialog.addButton(QtWidgets.QMessageBox.Cancel) + + msg = ( + "Version update to '{}' failed as representation doesn't exist." + "\n\nPlease update to version with a valid representation" + " OR \n use 'Switch Folder' button to change folder." + ).format(version_str) + dialog.setText(msg) + dialog.exec_() + + def update_all(self): + """Update all items that are currently 'outdated' in the view""" + # Get the source model through the proxy model + model = self.model().sourceModel() + + # Get all items from outdated groups + outdated_items = [] + for index in iter_model_rows(model, + column=0, + include_root=False): + item = index.data(model.ItemRole) + + if not item.get("isGroupNode"): + continue + + # Only the group nodes contain the "highest_version" data and as + # such we find only the groups and take its children. + if not model.outdated(item): + continue + + # Collect all children which we want to update + children = item.children() + outdated_items.extend(children) + + if not outdated_items: + log.info("Nothing to update.") + return + + # Trigger update to latest + self._update_containers(outdated_items, version=-1) + + def _update_containers(self, items, version): + """Helper to update items to given version (or version per item) + + If at least one item is specified this will always try to refresh + the inventory even if errors occurred on any of the items. + + Arguments: + items (list): Items to update + version (int or list): Version to set to. + This can be a list specifying a version for each item. + Like `update_container` version -1 sets the latest version + and HeroTypeVersion instances set the hero version. + + """ + + if isinstance(version, (list, tuple)): + # We allow a unique version to be specified per item. In that case + # the length must match with the items + assert len(items) == len(version), ( + "Number of items mismatches number of versions: " + "{} items - {} versions".format(len(items), len(version)) + ) + versions = version + else: + # Repeat the same version infinitely + versions = itertools.repeat(version) + + # Trigger update to latest + try: + for item, item_version in zip(items, versions): + try: + update_container(item, item_version) + except AssertionError: + self._show_version_error_dialog(item_version, [item]) + log.warning("Update failed", exc_info=True) + finally: + # Always update the scene inventory view, even if errors occurred + self.data_changed.emit() diff --git a/openpype/tools/ayon_sceneinventory/window.py b/openpype/tools/ayon_sceneinventory/window.py new file mode 100644 index 0000000000..427bf4c50d --- /dev/null +++ b/openpype/tools/ayon_sceneinventory/window.py @@ -0,0 +1,200 @@ +from qtpy import QtWidgets, QtCore, QtGui +import qtawesome + +from openpype import style, resources +from openpype.tools.utils.delegates import VersionDelegate +from openpype.tools.utils.lib import ( + preserve_expanded_rows, + preserve_selection, +) +from openpype.tools.ayon_sceneinventory import SceneInventoryController + +from .model import ( + InventoryModel, + FilterProxyModel +) +from .view import SceneInventoryView + + +class ControllerVersionDelegate(VersionDelegate): + """Version delegate that uses controller to get project. + + Original VersionDelegate is using 'AvalonMongoDB' object instead. Don't + worry about the variable name, object is stored to '_dbcon' attribute. + """ + + def get_project_name(self): + self._dbcon.get_current_project_name() + + +class SceneInventoryWindow(QtWidgets.QDialog): + """Scene Inventory window""" + + def __init__(self, controller=None, parent=None): + super(SceneInventoryWindow, self).__init__(parent) + + if controller is None: + controller = SceneInventoryController() + + project_name = controller.get_current_project_name() + icon = QtGui.QIcon(resources.get_openpype_icon_filepath()) + self.setWindowIcon(icon) + self.setWindowTitle("Scene Inventory - {}".format(project_name)) + self.setObjectName("SceneInventory") + + self.resize(1100, 480) + + # region control + + filter_label = QtWidgets.QLabel("Search", self) + text_filter = QtWidgets.QLineEdit(self) + + outdated_only_checkbox = QtWidgets.QCheckBox( + "Filter to outdated", self + ) + outdated_only_checkbox.setToolTip("Show outdated files only") + outdated_only_checkbox.setChecked(False) + + icon = qtawesome.icon("fa.arrow-up", color="white") + update_all_button = QtWidgets.QPushButton(self) + update_all_button.setToolTip("Update all outdated to latest version") + update_all_button.setIcon(icon) + + icon = qtawesome.icon("fa.refresh", color="white") + refresh_button = QtWidgets.QPushButton(self) + refresh_button.setToolTip("Refresh") + refresh_button.setIcon(icon) + + control_layout = QtWidgets.QHBoxLayout() + control_layout.addWidget(filter_label) + control_layout.addWidget(text_filter) + control_layout.addWidget(outdated_only_checkbox) + control_layout.addWidget(update_all_button) + control_layout.addWidget(refresh_button) + + model = InventoryModel(controller) + proxy = FilterProxyModel() + proxy.setSourceModel(model) + proxy.setDynamicSortFilter(True) + proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive) + + view = SceneInventoryView(controller, self) + view.setModel(proxy) + + sync_enabled = controller.is_sync_server_enabled() + view.setColumnHidden(model.active_site_col, not sync_enabled) + view.setColumnHidden(model.remote_site_col, not sync_enabled) + + # set some nice default widths for the view + view.setColumnWidth(0, 250) # name + view.setColumnWidth(1, 55) # version + view.setColumnWidth(2, 55) # count + view.setColumnWidth(3, 150) # family + view.setColumnWidth(4, 120) # group + view.setColumnWidth(5, 150) # loader + + # apply delegates + version_delegate = ControllerVersionDelegate(controller, self) + column = model.Columns.index("version") + view.setItemDelegateForColumn(column, version_delegate) + + layout = QtWidgets.QVBoxLayout(self) + layout.addLayout(control_layout) + layout.addWidget(view) + + show_timer = QtCore.QTimer() + show_timer.setInterval(0) + show_timer.setSingleShot(False) + + # signals + show_timer.timeout.connect(self._on_show_timer) + text_filter.textChanged.connect(self._on_text_filter_change) + outdated_only_checkbox.stateChanged.connect( + self._on_outdated_state_change + ) + view.hierarchy_view_changed.connect( + self._on_hierarchy_view_change + ) + view.data_changed.connect(self._on_refresh_request) + refresh_button.clicked.connect(self._on_refresh_request) + update_all_button.clicked.connect(self._on_update_all) + + self._show_timer = show_timer + self._show_counter = 0 + self._controller = controller + self._update_all_button = update_all_button + self._outdated_only_checkbox = outdated_only_checkbox + self._view = view + self._model = model + self._proxy = proxy + self._version_delegate = version_delegate + + self._first_show = True + self._first_refresh = True + + def showEvent(self, event): + super(SceneInventoryWindow, self).showEvent(event) + if self._first_show: + self._first_show = False + self.setStyleSheet(style.load_stylesheet()) + + self._show_counter = 0 + self._show_timer.start() + + 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. + + """ + + def _on_refresh_request(self): + """Signal callback to trigger 'refresh' without any arguments.""" + + self.refresh() + + def refresh(self, containers=None): + self._first_refresh = False + self._controller.reset() + with preserve_expanded_rows( + tree_view=self._view, + role=self._model.UniqueRole + ): + with preserve_selection( + tree_view=self._view, + role=self._model.UniqueRole, + current_index=False + ): + kwargs = {"containers": containers} + # TODO do not touch view's inner attribute + if self._view._hierarchy_view: + kwargs["selected"] = self._view._selected + self._model.refresh(**kwargs) + + def _on_show_timer(self): + if self._show_counter < 3: + self._show_counter += 1 + return + self._show_timer.stop() + self.refresh() + + def _on_hierarchy_view_change(self, enabled): + self._proxy.set_hierarchy_view(enabled) + self._model.set_hierarchy_view(enabled) + + def _on_text_filter_change(self, text_filter): + if hasattr(self._proxy, "setFilterRegExp"): + self._proxy.setFilterRegExp(text_filter) + else: + self._proxy.setFilterRegularExpression(text_filter) + + def _on_outdated_state_change(self): + self._proxy.set_filter_outdated( + self._outdated_only_checkbox.isChecked() + ) + + def _on_update_all(self): + self._view.update_all() diff --git a/openpype/tools/utils/delegates.py b/openpype/tools/utils/delegates.py index c71c87f9b0..c51323e556 100644 --- a/openpype/tools/utils/delegates.py +++ b/openpype/tools/utils/delegates.py @@ -24,9 +24,12 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate): lock = False def __init__(self, dbcon, *args, **kwargs): - self.dbcon = dbcon + self._dbcon = dbcon super(VersionDelegate, self).__init__(*args, **kwargs) + def get_project_name(self): + return self._dbcon.active_project() + def displayText(self, value, locale): if isinstance(value, HeroVersionType): return lib.format_version(value, True) @@ -120,7 +123,7 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate): "Version is not integer" ) - project_name = self.dbcon.active_project() + project_name = self.get_project_name() # Add all available versions to the editor parent_id = item["version_document"]["parent"] version_docs = [ diff --git a/openpype/tools/utils/host_tools.py b/openpype/tools/utils/host_tools.py index ca23945339..29c8c0ba8e 100644 --- a/openpype/tools/utils/host_tools.py +++ b/openpype/tools/utils/host_tools.py @@ -171,14 +171,23 @@ class HostToolsHelper: def get_scene_inventory_tool(self, parent): """Create, cache and return scene inventory tool window.""" if self._scene_inventory_tool is None: - from openpype.tools.sceneinventory import SceneInventoryWindow - host = registered_host() ILoadHost.validate_load_methods(host) - scene_inventory_window = SceneInventoryWindow( - parent=parent or self._parent - ) + if AYON_SERVER_ENABLED: + from openpype.tools.ayon_sceneinventory.window import ( + SceneInventoryWindow) + + scene_inventory_window = SceneInventoryWindow( + parent=parent or self._parent + ) + + else: + from openpype.tools.sceneinventory import SceneInventoryWindow + + scene_inventory_window = SceneInventoryWindow( + parent=parent or self._parent + ) self._scene_inventory_tool = scene_inventory_window return self._scene_inventory_tool From 08baaca5a339adc11ebfb4fc77ad1d163df759f6 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 13 Oct 2023 14:18:03 +0200 Subject: [PATCH 0309/1233] Refactor `SelectInvalidAction` to behave like other action for other host, create `SelectInstanceNodeAction` as dedicated action to select the instance node for a failed plugin. - Note: Selecting Instance Node will still select the instance node even if the user has currently 'fixed' the problem. --- openpype/hosts/nuke/api/__init__.py | 6 +- openpype/hosts/nuke/api/actions.py | 59 ++++++++++++++----- openpype/hosts/nuke/api/lib.py | 5 +- .../plugins/publish/validate_asset_context.py | 4 +- 4 files changed, 53 insertions(+), 21 deletions(-) diff --git a/openpype/hosts/nuke/api/__init__.py b/openpype/hosts/nuke/api/__init__.py index a01f5bda0a..c6ccd0baf1 100644 --- a/openpype/hosts/nuke/api/__init__.py +++ b/openpype/hosts/nuke/api/__init__.py @@ -50,7 +50,10 @@ from .utils import ( get_colorspace_list ) -from .actions import SelectInvalidAction +from .actions import ( + SelectInvalidAction, + SelectInstanceNodeAction +) __all__ = ( "file_extensions", @@ -97,4 +100,5 @@ __all__ = ( "get_colorspace_list", "SelectInvalidAction", + "SelectInstanceNodeAction" ) diff --git a/openpype/hosts/nuke/api/actions.py b/openpype/hosts/nuke/api/actions.py index ca3c8393ed..995e6427af 100644 --- a/openpype/hosts/nuke/api/actions.py +++ b/openpype/hosts/nuke/api/actions.py @@ -18,6 +18,38 @@ class SelectInvalidAction(pyblish.api.Action): on = "failed" # This action is only available on a failed plug-in icon = "search" # Icon from Awesome Icon + def process(self, context, plugin): + + errored_instances = get_errored_instances_from_context(context, + plugin=plugin) + + # Get the invalid nodes for the plug-ins + self.log.info("Finding invalid nodes..") + invalid = set() + for instance in errored_instances: + invalid_nodes = plugin.get_invalid(instance) + + if invalid_nodes: + if isinstance(invalid_nodes, (list, tuple)): + invalid.update(invalid_nodes) + else: + self.log.warning("Plug-in returned to be invalid, " + "but has no selectable nodes.") + + if invalid: + self.log.info("Selecting invalid nodes: {}".format(invalid)) + reset_selection() + select_nodes(invalid) + else: + self.log.info("No invalid nodes found.") + + +class SelectInstanceNodeAction(pyblish.api.Action): + """Select instance node for failed plugin.""" + label = "Select instance node" + on = "failed" # This action is only available on a failed plug-in + icon = "mdi.cursor-default-click" + def process(self, context, plugin): # Get the errored instances for the plug-in @@ -25,26 +57,21 @@ class SelectInvalidAction(pyblish.api.Action): context, plugin) # Get the invalid nodes for the plug-ins - self.log.info("Finding invalid nodes..") - invalid_nodes = set() + self.log.info("Finding instance nodes..") + nodes = set() for instance in errored_instances: - invalid = plugin.get_invalid(instance) - - if not invalid: - continue - - select_node = instance.data.get("transientData", {}).get("node") - if not select_node: + instance_node = instance.data.get("transientData", {}).get("node") + if not instance_node: raise RuntimeError( "No transientData['node'] found on instance: {}".format( - instance) + instance + ) ) + nodes.add(instance_node) - invalid_nodes.add(select_node) - - if invalid_nodes: - self.log.info("Selecting invalid nodes: {}".format(invalid_nodes)) + if nodes: + self.log.info("Selecting instance nodes: {}".format(nodes)) reset_selection() - select_nodes(list(invalid_nodes)) + select_nodes(nodes) else: - self.log.info("No invalid nodes found.") + self.log.info("No instance nodes found.") diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 390545b806..62f3a3c3ff 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -2833,9 +2833,10 @@ def select_nodes(nodes): """Selects all inputted nodes Arguments: - nodes (list): nuke nodes to be selected + nodes (Union[list, tuple, set]): nuke nodes to be selected """ - assert isinstance(nodes, (list, tuple)), "nodes has to be list or tuple" + assert isinstance(nodes, (list, tuple, set)), \ + "nodes has to be list, tuple or set" for node in nodes: node["selected"].setValue(True) diff --git a/openpype/hosts/nuke/plugins/publish/validate_asset_context.py b/openpype/hosts/nuke/plugins/publish/validate_asset_context.py index ab62daeaeb..731645a11c 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_asset_context.py +++ b/openpype/hosts/nuke/plugins/publish/validate_asset_context.py @@ -10,7 +10,7 @@ from openpype.pipeline.publish import ( PublishXmlValidationError, OptionalPyblishPluginMixin ) -from openpype.hosts.nuke.api import SelectInvalidAction +from openpype.hosts.nuke.api import SelectInstanceNodeAction class ValidateCorrectAssetContext( @@ -30,7 +30,7 @@ class ValidateCorrectAssetContext( hosts = ["nuke"] actions = [ RepairAction, - SelectInvalidAction + SelectInstanceNodeAction ] optional = True From 3bbf3c0db93a5cc5db99a929a0f7cefa2a17cf02 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 13 Oct 2023 14:32:21 +0200 Subject: [PATCH 0310/1233] Tweak logging for Nuke for artist facing reports --- .../nuke/plugins/publish/collect_backdrop.py | 2 +- .../nuke/plugins/publish/collect_context_data.py | 2 +- .../hosts/nuke/plugins/publish/collect_gizmo.py | 2 +- .../hosts/nuke/plugins/publish/collect_model.py | 2 +- .../nuke/plugins/publish/collect_slate_node.py | 2 +- .../nuke/plugins/publish/collect_workfile.py | 4 +++- .../nuke/plugins/publish/extract_backdrop.py | 4 +--- .../hosts/nuke/plugins/publish/extract_camera.py | 8 +++----- .../hosts/nuke/plugins/publish/extract_gizmo.py | 5 +---- .../hosts/nuke/plugins/publish/extract_model.py | 8 ++++---- .../nuke/plugins/publish/extract_ouput_node.py | 2 +- .../nuke/plugins/publish/extract_render_local.py | 4 ++-- .../plugins/publish/extract_review_data_lut.py | 4 ++-- .../publish/extract_review_intermediates.py | 15 ++++++++------- .../nuke/plugins/publish/extract_script_save.py | 5 ++--- .../nuke/plugins/publish/extract_slate_frame.py | 14 +++++++------- .../nuke/plugins/publish/extract_thumbnail.py | 4 ++-- .../nuke/plugins/publish/validate_backdrop.py | 4 ++-- .../plugins/publish/validate_output_resolution.py | 4 ++-- .../plugins/publish/validate_rendered_frames.py | 14 +++++++------- .../nuke/plugins/publish/validate_write_nodes.py | 2 +- 21 files changed, 53 insertions(+), 58 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/collect_backdrop.py b/openpype/hosts/nuke/plugins/publish/collect_backdrop.py index 7d51af7e9e..d04c1204e3 100644 --- a/openpype/hosts/nuke/plugins/publish/collect_backdrop.py +++ b/openpype/hosts/nuke/plugins/publish/collect_backdrop.py @@ -57,4 +57,4 @@ class CollectBackdrops(pyblish.api.InstancePlugin): if version: instance.data['version'] = version - self.log.info("Backdrop instance collected: `{}`".format(instance)) + self.log.debug("Backdrop instance collected: `{}`".format(instance)) diff --git a/openpype/hosts/nuke/plugins/publish/collect_context_data.py b/openpype/hosts/nuke/plugins/publish/collect_context_data.py index f1b4965205..b85e924f55 100644 --- a/openpype/hosts/nuke/plugins/publish/collect_context_data.py +++ b/openpype/hosts/nuke/plugins/publish/collect_context_data.py @@ -64,4 +64,4 @@ class CollectContextData(pyblish.api.ContextPlugin): context.data["scriptData"] = script_data context.data.update(script_data) - self.log.info('Context from Nuke script collected') + self.log.debug('Context from Nuke script collected') diff --git a/openpype/hosts/nuke/plugins/publish/collect_gizmo.py b/openpype/hosts/nuke/plugins/publish/collect_gizmo.py index e3c40a7a90..c410de7c32 100644 --- a/openpype/hosts/nuke/plugins/publish/collect_gizmo.py +++ b/openpype/hosts/nuke/plugins/publish/collect_gizmo.py @@ -43,4 +43,4 @@ class CollectGizmo(pyblish.api.InstancePlugin): "frameStart": first_frame, "frameEnd": last_frame }) - self.log.info("Gizmo instance collected: `{}`".format(instance)) + self.log.debug("Gizmo instance collected: `{}`".format(instance)) diff --git a/openpype/hosts/nuke/plugins/publish/collect_model.py b/openpype/hosts/nuke/plugins/publish/collect_model.py index 3fdf376d0c..a099f06be0 100644 --- a/openpype/hosts/nuke/plugins/publish/collect_model.py +++ b/openpype/hosts/nuke/plugins/publish/collect_model.py @@ -43,4 +43,4 @@ class CollectModel(pyblish.api.InstancePlugin): "frameStart": first_frame, "frameEnd": last_frame }) - self.log.info("Model instance collected: `{}`".format(instance)) + self.log.debug("Model instance collected: `{}`".format(instance)) diff --git a/openpype/hosts/nuke/plugins/publish/collect_slate_node.py b/openpype/hosts/nuke/plugins/publish/collect_slate_node.py index c7d65ffd24..3baa0cd9b5 100644 --- a/openpype/hosts/nuke/plugins/publish/collect_slate_node.py +++ b/openpype/hosts/nuke/plugins/publish/collect_slate_node.py @@ -39,7 +39,7 @@ class CollectSlate(pyblish.api.InstancePlugin): instance.data["slateNode"] = slate_node instance.data["slate"] = True instance.data["families"].append("slate") - self.log.info( + self.log.debug( "Slate node is in node graph: `{}`".format(slate.name())) self.log.debug( "__ instance.data: `{}`".format(instance.data)) diff --git a/openpype/hosts/nuke/plugins/publish/collect_workfile.py b/openpype/hosts/nuke/plugins/publish/collect_workfile.py index 852042e6e9..0f03572f8b 100644 --- a/openpype/hosts/nuke/plugins/publish/collect_workfile.py +++ b/openpype/hosts/nuke/plugins/publish/collect_workfile.py @@ -37,4 +37,6 @@ class CollectWorkfile(pyblish.api.InstancePlugin): # adding basic script data instance.data.update(script_data) - self.log.info("Collect script version") + self.log.debug( + "Collected current script version: {}".format(current_file) + ) diff --git a/openpype/hosts/nuke/plugins/publish/extract_backdrop.py b/openpype/hosts/nuke/plugins/publish/extract_backdrop.py index 5166fa4b2c..2a6a5dee2a 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_backdrop.py +++ b/openpype/hosts/nuke/plugins/publish/extract_backdrop.py @@ -56,8 +56,6 @@ class ExtractBackdropNode(publish.Extractor): # connect output node for n, output in connections_out.items(): opn = nuke.createNode("Output") - self.log.info(n.name()) - self.log.info(output.name()) output.setInput( next((i for i, d in enumerate(output.dependencies()) if d.name() in n.name()), 0), opn) @@ -102,5 +100,5 @@ class ExtractBackdropNode(publish.Extractor): } instance.data["representations"].append(representation) - self.log.info("Extracted instance '{}' to: {}".format( + self.log.debug("Extracted instance '{}' to: {}".format( instance.name, path)) diff --git a/openpype/hosts/nuke/plugins/publish/extract_camera.py b/openpype/hosts/nuke/plugins/publish/extract_camera.py index 33df6258ae..b0facd379a 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_camera.py +++ b/openpype/hosts/nuke/plugins/publish/extract_camera.py @@ -36,11 +36,11 @@ class ExtractCamera(publish.Extractor): step = 1 output_range = str(nuke.FrameRange(first_frame, last_frame, step)) - self.log.info("instance.data: `{}`".format( + self.log.debug("instance.data: `{}`".format( pformat(instance.data))) rm_nodes = [] - self.log.info("Crating additional nodes") + self.log.debug("Creating additional nodes for 3D Camera Extractor") subset = instance.data["subset"] staging_dir = self.staging_dir(instance) @@ -84,8 +84,6 @@ class ExtractCamera(publish.Extractor): for n in rm_nodes: nuke.delete(n) - self.log.info(file_path) - # create representation data if "representations" not in instance.data: instance.data["representations"] = [] @@ -112,7 +110,7 @@ class ExtractCamera(publish.Extractor): "frameEndHandle": last_frame, }) - self.log.info("Extracted instance '{0}' to: {1}".format( + self.log.debug("Extracted instance '{0}' to: {1}".format( instance.name, file_path)) diff --git a/openpype/hosts/nuke/plugins/publish/extract_gizmo.py b/openpype/hosts/nuke/plugins/publish/extract_gizmo.py index b0b1a9f7b7..ecec0d6f80 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_gizmo.py +++ b/openpype/hosts/nuke/plugins/publish/extract_gizmo.py @@ -85,8 +85,5 @@ class ExtractGizmo(publish.Extractor): } instance.data["representations"].append(representation) - self.log.info("Extracted instance '{}' to: {}".format( + self.log.debug("Extracted instance '{}' to: {}".format( instance.name, path)) - - self.log.info("Data {}".format( - instance.data)) diff --git a/openpype/hosts/nuke/plugins/publish/extract_model.py b/openpype/hosts/nuke/plugins/publish/extract_model.py index 00462f8035..a8b37fb173 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_model.py +++ b/openpype/hosts/nuke/plugins/publish/extract_model.py @@ -33,13 +33,13 @@ class ExtractModel(publish.Extractor): first_frame = int(nuke.root()["first_frame"].getValue()) last_frame = int(nuke.root()["last_frame"].getValue()) - self.log.info("instance.data: `{}`".format( + self.log.debug("instance.data: `{}`".format( pformat(instance.data))) rm_nodes = [] model_node = instance.data["transientData"]["node"] - self.log.info("Crating additional nodes") + self.log.debug("Creating additional nodes for Extract Model") subset = instance.data["subset"] staging_dir = self.staging_dir(instance) @@ -76,7 +76,7 @@ class ExtractModel(publish.Extractor): for n in rm_nodes: nuke.delete(n) - self.log.info(file_path) + self.log.debug("Filepath: {}".format(file_path)) # create representation data if "representations" not in instance.data: @@ -104,5 +104,5 @@ class ExtractModel(publish.Extractor): "frameEndHandle": last_frame, }) - self.log.info("Extracted instance '{0}' to: {1}".format( + self.log.debug("Extracted instance '{0}' to: {1}".format( instance.name, file_path)) diff --git a/openpype/hosts/nuke/plugins/publish/extract_ouput_node.py b/openpype/hosts/nuke/plugins/publish/extract_ouput_node.py index e66cfd9018..3fe1443bb3 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_ouput_node.py +++ b/openpype/hosts/nuke/plugins/publish/extract_ouput_node.py @@ -27,7 +27,7 @@ class CreateOutputNode(pyblish.api.ContextPlugin): if active_node: active_node = active_node.pop() - self.log.info(active_node) + self.log.debug("Active node: {}".format(active_node)) active_node['selected'].setValue(True) # select only instance render node diff --git a/openpype/hosts/nuke/plugins/publish/extract_render_local.py b/openpype/hosts/nuke/plugins/publish/extract_render_local.py index e2cf2addc5..ff04367e20 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_render_local.py +++ b/openpype/hosts/nuke/plugins/publish/extract_render_local.py @@ -119,7 +119,7 @@ class NukeRenderLocal(publish.Extractor, instance.data["representations"].append(repre) - self.log.info("Extracted instance '{0}' to: {1}".format( + self.log.debug("Extracted instance '{0}' to: {1}".format( instance.name, out_dir )) @@ -143,7 +143,7 @@ class NukeRenderLocal(publish.Extractor, instance.data["families"] = families collections, remainder = clique.assemble(filenames) - self.log.info('collections: {}'.format(str(collections))) + self.log.debug('collections: {}'.format(str(collections))) if collections: collection = collections[0] 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 2a26ed82fb..b007f90f6c 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,7 @@ class ExtractReviewDataLut(publish.Extractor): hosts = ["nuke"] def process(self, instance): - self.log.info("Creating staging dir...") + self.log.debug("Creating staging dir...") if "representations" in instance.data: staging_dir = instance.data[ "representations"][0]["stagingDir"].replace("\\", "/") @@ -33,7 +33,7 @@ class ExtractReviewDataLut(publish.Extractor): staging_dir = os.path.normpath(os.path.dirname(render_path)) instance.data["stagingDir"] = staging_dir - self.log.info( + self.log.debug( "StagingDir `{0}`...".format(instance.data["stagingDir"])) # generate data diff --git a/openpype/hosts/nuke/plugins/publish/extract_review_intermediates.py b/openpype/hosts/nuke/plugins/publish/extract_review_intermediates.py index 9730e3b61f..3ee166eb56 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_review_intermediates.py +++ b/openpype/hosts/nuke/plugins/publish/extract_review_intermediates.py @@ -52,7 +52,7 @@ class ExtractReviewIntermediates(publish.Extractor): task_type = instance.context.data["taskType"] subset = instance.data["subset"] - self.log.info("Creating staging dir...") + self.log.debug("Creating staging dir...") if "representations" not in instance.data: instance.data["representations"] = [] @@ -62,10 +62,10 @@ class ExtractReviewIntermediates(publish.Extractor): instance.data["stagingDir"] = staging_dir - self.log.info( + self.log.debug( "StagingDir `{0}`...".format(instance.data["stagingDir"])) - self.log.info(self.outputs) + self.log.debug("Outputs: {}".format(self.outputs)) # generate data with maintained_selection(): @@ -104,9 +104,10 @@ class ExtractReviewIntermediates(publish.Extractor): re.search(s, subset) for s in f_subsets): continue - self.log.info( + self.log.debug( "Baking output `{}` with settings: {}".format( - o_name, o_data)) + o_name, o_data) + ) # check if settings have more then one preset # so we dont need to add outputName to representation @@ -155,10 +156,10 @@ class ExtractReviewIntermediates(publish.Extractor): instance.data["useSequenceForReview"] = False else: instance.data["families"].remove("review") - self.log.info(( + self.log.debug( "Removing `review` from families. " "Not available baking profile." - )) + ) self.log.debug(instance.data["families"]) self.log.debug( diff --git a/openpype/hosts/nuke/plugins/publish/extract_script_save.py b/openpype/hosts/nuke/plugins/publish/extract_script_save.py index 0c8e561fd7..e44e5686b6 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_script_save.py +++ b/openpype/hosts/nuke/plugins/publish/extract_script_save.py @@ -3,13 +3,12 @@ import pyblish.api class ExtractScriptSave(pyblish.api.Extractor): - """ - """ + """Save current Nuke workfile script""" label = 'Script Save' order = pyblish.api.Extractor.order - 0.1 hosts = ['nuke'] def process(self, instance): - self.log.info('saving script') + self.log.debug('Saving current script') nuke.scriptSave() diff --git a/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py b/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py index 25262a7418..7befb7b7f3 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py +++ b/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py @@ -48,7 +48,7 @@ class ExtractSlateFrame(publish.Extractor): if instance.data.get("bakePresets"): for o_name, o_data in instance.data["bakePresets"].items(): - self.log.info("_ o_name: {}, o_data: {}".format( + self.log.debug("_ o_name: {}, o_data: {}".format( o_name, pformat(o_data))) self.render_slate( instance, @@ -65,14 +65,14 @@ class ExtractSlateFrame(publish.Extractor): def _create_staging_dir(self, instance): - self.log.info("Creating staging dir...") + self.log.debug("Creating staging dir...") staging_dir = os.path.normpath( os.path.dirname(instance.data["path"])) instance.data["stagingDir"] = staging_dir - self.log.info( + self.log.debug( "StagingDir `{0}`...".format(instance.data["stagingDir"])) def _check_frames_exists(self, instance): @@ -275,10 +275,10 @@ class ExtractSlateFrame(publish.Extractor): break if not matching_repre: - self.log.info(( - "Matching reresentaion was not found." + self.log.info( + "Matching reresentation was not found." " Representation files were not filled with slate." - )) + ) return # Add frame to matching representation files @@ -345,7 +345,7 @@ class ExtractSlateFrame(publish.Extractor): try: node[key].setValue(value) - self.log.info("Change key \"{}\" to value \"{}\"".format( + self.log.debug("Change key \"{}\" to value \"{}\"".format( key, value )) except NameError: diff --git a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py index 46288db743..de7567c1b1 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py @@ -69,7 +69,7 @@ class ExtractThumbnail(publish.Extractor): "bake_viewer_input_process"] node = instance.data["transientData"]["node"] # group node - self.log.info("Creating staging dir...") + self.log.debug("Creating staging dir...") if "representations" not in instance.data: instance.data["representations"] = [] @@ -79,7 +79,7 @@ class ExtractThumbnail(publish.Extractor): instance.data["stagingDir"] = staging_dir - self.log.info( + self.log.debug( "StagingDir `{0}`...".format(instance.data["stagingDir"])) temporary_nodes = [] diff --git a/openpype/hosts/nuke/plugins/publish/validate_backdrop.py b/openpype/hosts/nuke/plugins/publish/validate_backdrop.py index ad60089952..761b080caa 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_backdrop.py +++ b/openpype/hosts/nuke/plugins/publish/validate_backdrop.py @@ -43,8 +43,8 @@ class SelectCenterInNodeGraph(pyblish.api.Action): all_xC.append(xC) all_yC.append(yC) - self.log.info("all_xC: `{}`".format(all_xC)) - self.log.info("all_yC: `{}`".format(all_yC)) + self.log.debug("all_xC: `{}`".format(all_xC)) + self.log.debug("all_yC: `{}`".format(all_yC)) # zoom to nodes in node graph nuke.zoom(2, [min(all_xC), min(all_yC)]) diff --git a/openpype/hosts/nuke/plugins/publish/validate_output_resolution.py b/openpype/hosts/nuke/plugins/publish/validate_output_resolution.py index 39114c80c8..ff6d73c6ec 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_output_resolution.py +++ b/openpype/hosts/nuke/plugins/publish/validate_output_resolution.py @@ -104,9 +104,9 @@ class ValidateOutputResolution( _rfn["resize"].setValue(0) _rfn["black_outside"].setValue(1) - cls.log.info("I am adding reformat node") + cls.log.info("Adding reformat node") if cls.resolution_msg == invalid: reformat = cls.get_reformat(instance) reformat["format"].setValue(nuke.root()["format"].value()) - cls.log.info("I am fixing reformat to root.format") + cls.log.info("Fixing reformat to root.format") diff --git a/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py b/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py index 9a35b61a0e..64bf69b69b 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py +++ b/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py @@ -76,8 +76,8 @@ class ValidateRenderedFrames(pyblish.api.InstancePlugin): return collections, remainder = clique.assemble(repre["files"]) - self.log.info("collections: {}".format(str(collections))) - self.log.info("remainder: {}".format(str(remainder))) + self.log.debug("collections: {}".format(str(collections))) + self.log.debug("remainder: {}".format(str(remainder))) collection = collections[0] @@ -103,15 +103,15 @@ class ValidateRenderedFrames(pyblish.api.InstancePlugin): coll_start = min(collection.indexes) coll_end = max(collection.indexes) - self.log.info("frame_length: {}".format(frame_length)) - self.log.info("collected_frames_len: {}".format( + self.log.debug("frame_length: {}".format(frame_length)) + self.log.debug("collected_frames_len: {}".format( collected_frames_len)) - self.log.info("f_start_h-f_end_h: {}-{}".format( + self.log.debug("f_start_h-f_end_h: {}-{}".format( f_start_h, f_end_h)) - self.log.info( + self.log.debug( "coll_start-coll_end: {}-{}".format(coll_start, coll_end)) - self.log.info( + self.log.debug( "len(collection.indexes): {}".format(collected_frames_len) ) diff --git a/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py b/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py index 9aae53e59d..9c8bfae388 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py +++ b/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py @@ -39,7 +39,7 @@ class RepairNukeWriteNodeAction(pyblish.api.Action): set_node_knobs_from_settings(write_node, correct_data["knobs"]) - self.log.info("Node attributes were fixed") + self.log.debug("Node attributes were fixed") class ValidateNukeWriteNode( From 1f1676f5bb98aea465371641693b63ff2326abbd Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 13 Oct 2023 14:36:36 +0200 Subject: [PATCH 0311/1233] Refactor to use variable `node_value` instead of `value` - `value` only exists as the last variable value in the `for value in values` loop and might not be declared if `values` is an empty iterable. --- openpype/hosts/nuke/plugins/publish/validate_write_nodes.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py b/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py index 9aae53e59d..b882e240e6 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py +++ b/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py @@ -111,7 +111,6 @@ class ValidateNukeWriteNode( for value in values: if type(node_value) in (int, float): try: - if isinstance(value, list): value = color_gui_to_int(value) else: @@ -130,7 +129,7 @@ class ValidateNukeWriteNode( and key != "file" and key != "tile_color" ): - check.append([key, value, write_node[key].value()]) + check.append([key, node_value, write_node[key].value()]) if check: self._make_error(check) From ba804833cd42f7a78aa2095b68e0943dab7b81fc Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 13 Oct 2023 20:50:07 +0800 Subject: [PATCH 0312/1233] rename validate containers to validate instance has members --- .../plugins/publish/validate_containers.py | 4 ++-- .../publish/validate_no_max_content.py | 22 ------------------- 2 files changed, 2 insertions(+), 24 deletions(-) delete mode 100644 openpype/hosts/max/plugins/publish/validate_no_max_content.py diff --git a/openpype/hosts/max/plugins/publish/validate_containers.py b/openpype/hosts/max/plugins/publish/validate_containers.py index a5c0669a11..3c0039d5e0 100644 --- a/openpype/hosts/max/plugins/publish/validate_containers.py +++ b/openpype/hosts/max/plugins/publish/validate_containers.py @@ -3,8 +3,8 @@ import pyblish.api from openpype.pipeline import PublishValidationError -class ValidateContainers(pyblish.api.InstancePlugin): - """Validates Containers. +class ValidateInstanceHasMembers(pyblish.api.InstancePlugin): + """Validates Instance has members. Check if MaxScene containers includes any contents underneath. """ diff --git a/openpype/hosts/max/plugins/publish/validate_no_max_content.py b/openpype/hosts/max/plugins/publish/validate_no_max_content.py deleted file mode 100644 index 73e12e75c9..0000000000 --- a/openpype/hosts/max/plugins/publish/validate_no_max_content.py +++ /dev/null @@ -1,22 +0,0 @@ -# -*- coding: utf-8 -*- -import pyblish.api -from openpype.pipeline import PublishValidationError -from pymxs import runtime as rt - - -class ValidateMaxContents(pyblish.api.InstancePlugin): - """Validates Max contents. - - Check if MaxScene container includes any contents underneath. - """ - - order = pyblish.api.ValidatorOrder - families = ["camera", - "maxScene", - "review"] - hosts = ["max"] - label = "Max Scene Contents" - - def process(self, instance): - if not instance.data["members"]: - raise PublishValidationError("No content found in the container") From 5914f2e23ce6ffdf24e1ec044bccd7d7144bd626 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 13 Oct 2023 15:54:12 +0200 Subject: [PATCH 0313/1233] :recycle: remove restriction for "Shot" folder type --- openpype/hosts/maya/plugins/create/create_multishot_layout.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_multishot_layout.py b/openpype/hosts/maya/plugins/create/create_multishot_layout.py index c109a76a31..c39d1c3ee3 100644 --- a/openpype/hosts/maya/plugins/create/create_multishot_layout.py +++ b/openpype/hosts/maya/plugins/create/create_multishot_layout.py @@ -194,7 +194,7 @@ class CreateMultishotLayout(plugin.MayaCreator): parent_id = current_folder["id"] # get all child folders of the current one - child_folders = get_folders( + return get_folders( project_name=self.project_name, parent_ids=[parent_id], fields=[ @@ -203,7 +203,6 @@ class CreateMultishotLayout(plugin.MayaCreator): "name", "label", "path", "folderType", "id" ] ) - return [f for f in child_folders if f["folderType"] == "Shot"] # blast this creator if Ayon server is not enabled From 8f5a5341e000b8138ef6819cf42e238f3f57b8bf Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 13 Oct 2023 15:55:07 +0200 Subject: [PATCH 0314/1233] :recycle: improve error message --- openpype/hosts/maya/plugins/create/create_multishot_layout.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/create/create_multishot_layout.py b/openpype/hosts/maya/plugins/create/create_multishot_layout.py index c39d1c3ee3..232ddc4389 100644 --- a/openpype/hosts/maya/plugins/create/create_multishot_layout.py +++ b/openpype/hosts/maya/plugins/create/create_multishot_layout.py @@ -114,7 +114,9 @@ class CreateMultishotLayout(plugin.MayaCreator): # want to create a new shot folders by publishing the layouts # and shot defined in the sequencer. Sort of editorial publish # in side of Maya. - raise CreatorError("No shots found under the specified folder.") + raise CreatorError(( + "No shots found under the specified " + f"folder: {pre_create_data['shotParent']}.")) # Get layout creator layout_creator_id = "io.openpype.creators.maya.layout" From 86c4dec6d2314122ccdd372e8818c82023d78c2f Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 13 Oct 2023 16:04:06 +0200 Subject: [PATCH 0315/1233] :recycle: change warning message to debug --- openpype/hosts/maya/plugins/publish/extract_look.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index b2b3330df1..635c2c425c 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -185,9 +185,9 @@ class MakeRSTexBin(TextureProcessor): "{}".format(config_path)) if not os.getenv("OCIO"): - self.log.warning( + self.log.debug( "OCIO environment variable not set." - "Setting it with OCIO config from OpenPype/AYON Settings." + "Setting it with OCIO config from Maya." ) os.environ["OCIO"] = config_path From 1b79767e7bbd76f93ca8ba8bf0f2ef434239509c Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 13 Oct 2023 22:15:14 +0800 Subject: [PATCH 0316/1233] add families into frame range collector and improve the validation report in frame range validator --- .../plugins/publish/collect_frame_range.py | 23 +++++++++ .../max/plugins/publish/collect_review.py | 2 - .../max/plugins/publish/extract_camera_abc.py | 4 +- .../max/plugins/publish/extract_pointcache.py | 4 +- .../max/plugins/publish/extract_pointcloud.py | 4 +- .../plugins/publish/extract_redshift_proxy.py | 4 +- .../publish/validate_animation_timeline.py | 48 ------------------- .../plugins/publish/validate_frame_range.py | 45 +++++++++++------ 8 files changed, 62 insertions(+), 72 deletions(-) create mode 100644 openpype/hosts/max/plugins/publish/collect_frame_range.py delete mode 100644 openpype/hosts/max/plugins/publish/validate_animation_timeline.py diff --git a/openpype/hosts/max/plugins/publish/collect_frame_range.py b/openpype/hosts/max/plugins/publish/collect_frame_range.py new file mode 100644 index 0000000000..197ecff0b1 --- /dev/null +++ b/openpype/hosts/max/plugins/publish/collect_frame_range.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +"""Collect instance members.""" +import pyblish.api +from pymxs import runtime as rt + + +class CollectFrameRange(pyblish.api.InstancePlugin): + """Collect Set Members.""" + + order = pyblish.api.CollectorOrder + 0.01 + label = "Collect Frame Range" + hosts = ['max'] + families = ["camera", "maxrender", + "pointcache", "pointcloud", + "review"] + + def process(self, instance): + if instance.data["family"] == "maxrender": + instance.data["frameStart"] = int(rt.rendStart) + instance.data["frameEnd"] = int(rt.rendEnd) + else: + instance.data["frameStart"] = int(rt.animationRange.start) + instance.data["frameEnd"] = int(rt.animationRange.end) diff --git a/openpype/hosts/max/plugins/publish/collect_review.py b/openpype/hosts/max/plugins/publish/collect_review.py index 8e27a857d7..cc4caae497 100644 --- a/openpype/hosts/max/plugins/publish/collect_review.py +++ b/openpype/hosts/max/plugins/publish/collect_review.py @@ -29,8 +29,6 @@ class CollectReview(pyblish.api.InstancePlugin, attr_values = self.get_attr_values_from_data(instance.data) data = { "review_camera": camera_name, - "frameStart": instance.context.data["frameStart"], - "frameEnd": instance.context.data["frameEnd"], "fps": instance.context.data["fps"], "dspGeometry": attr_values.get("dspGeometry"), "dspShapes": attr_values.get("dspShapes"), diff --git a/openpype/hosts/max/plugins/publish/extract_camera_abc.py b/openpype/hosts/max/plugins/publish/extract_camera_abc.py index b1918c53e0..ea33bc67ed 100644 --- a/openpype/hosts/max/plugins/publish/extract_camera_abc.py +++ b/openpype/hosts/max/plugins/publish/extract_camera_abc.py @@ -19,8 +19,8 @@ class ExtractCameraAlembic(publish.Extractor, OptionalPyblishPluginMixin): def process(self, instance): if not self.is_active(instance.data): return - start = float(instance.data.get("frameStartHandle", 1)) - end = float(instance.data.get("frameEndHandle", 1)) + start = instance.data["frameStart"] + end = instance.data["frameEnd"] self.log.info("Extracting Camera ...") diff --git a/openpype/hosts/max/plugins/publish/extract_pointcache.py b/openpype/hosts/max/plugins/publish/extract_pointcache.py index c3de623bc0..a5480ff0dc 100644 --- a/openpype/hosts/max/plugins/publish/extract_pointcache.py +++ b/openpype/hosts/max/plugins/publish/extract_pointcache.py @@ -51,8 +51,8 @@ class ExtractAlembic(publish.Extractor): families = ["pointcache"] def process(self, instance): - start = float(instance.data.get("frameStartHandle", 1)) - end = float(instance.data.get("frameEndHandle", 1)) + start = instance.data["frameStart"] + end = instance.data["frameEnd"] self.log.debug("Extracting pointcache ...") diff --git a/openpype/hosts/max/plugins/publish/extract_pointcloud.py b/openpype/hosts/max/plugins/publish/extract_pointcloud.py index 583bbb6dbd..de90229c59 100644 --- a/openpype/hosts/max/plugins/publish/extract_pointcloud.py +++ b/openpype/hosts/max/plugins/publish/extract_pointcloud.py @@ -39,8 +39,8 @@ class ExtractPointCloud(publish.Extractor): def process(self, instance): self.settings = self.get_setting(instance) - start = int(instance.context.data.get("frameStart")) - end = int(instance.context.data.get("frameEnd")) + start = instance.data["frameStart"] + end = instance.data["frameEnd"] self.log.info("Extracting PRT...") stagingdir = 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 f67ed30c6b..4f64e88584 100644 --- a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py +++ b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py @@ -16,8 +16,8 @@ class ExtractRedshiftProxy(publish.Extractor): families = ["redshiftproxy"] def process(self, instance): - start = int(instance.context.data.get("frameStart")) - end = int(instance.context.data.get("frameEnd")) + start = instance.data["frameStart"] + end = instance.data["frameEnd"] self.log.debug("Extracting Redshift Proxy...") stagingdir = self.staging_dir(instance) diff --git a/openpype/hosts/max/plugins/publish/validate_animation_timeline.py b/openpype/hosts/max/plugins/publish/validate_animation_timeline.py deleted file mode 100644 index 2a9483c763..0000000000 --- a/openpype/hosts/max/plugins/publish/validate_animation_timeline.py +++ /dev/null @@ -1,48 +0,0 @@ -import pyblish.api - -from pymxs import runtime as rt -from openpype.pipeline.publish import ( - RepairAction, - ValidateContentsOrder, - PublishValidationError -) -from openpype.hosts.max.api.lib import get_frame_range, set_timeline - - -class ValidateAnimationTimeline(pyblish.api.InstancePlugin): - """ - Validates Animation Timeline for Preview Animation in Max - """ - - label = "Animation Timeline for Review" - order = ValidateContentsOrder - families = ["review"] - hosts = ["max"] - actions = [RepairAction] - - def process(self, instance): - frame_range = get_frame_range() - frame_start_handle = frame_range["frameStart"] - int( - frame_range["handleStart"] - ) - frame_end_handle = frame_range["frameEnd"] + int( - frame_range["handleEnd"] - ) - if rt.animationRange.start != frame_start_handle or ( - rt.animationRange.end != frame_end_handle - ): - raise PublishValidationError("Incorrect animation timeline " - "set for preview animation.. " - "\nYou can use repair action to " - "the correct animation timeline") - - @classmethod - def repair(cls, instance): - frame_range = get_frame_range() - frame_start_handle = frame_range["frameStart"] - int( - frame_range["handleStart"] - ) - frame_end_handle = frame_range["frameEnd"] + int( - frame_range["handleEnd"] - ) - set_timeline(frame_start_handle, frame_end_handle) diff --git a/openpype/hosts/max/plugins/publish/validate_frame_range.py b/openpype/hosts/max/plugins/publish/validate_frame_range.py index 43692d0401..a50a3910c7 100644 --- a/openpype/hosts/max/plugins/publish/validate_frame_range.py +++ b/openpype/hosts/max/plugins/publish/validate_frame_range.py @@ -9,6 +9,7 @@ from openpype.pipeline.publish import ( ValidateContentsOrder, PublishValidationError ) +from openpype.hosts.max.api.lib import get_frame_range, set_timeline class ValidateFrameRange(pyblish.api.InstancePlugin, @@ -29,7 +30,7 @@ class ValidateFrameRange(pyblish.api.InstancePlugin, order = ValidateContentsOrder families = ["camera", "maxrender", "pointcache", "pointcloud", - "review", "redshiftproxy"] + "review"] hosts = ["max"] optional = True actions = [RepairAction] @@ -38,29 +39,45 @@ class ValidateFrameRange(pyblish.api.InstancePlugin, if not self.is_active(instance.data): self.log.info("Skipping validation...") return - context = instance.context - frame_start = int(context.data.get("frameStart")) - frame_end = int(context.data.get("frameEnd")) - - inst_frame_start = int(instance.data.get("frameStart")) - inst_frame_end = int(instance.data.get("frameEnd")) + frame_range = get_frame_range() + inst_frame_start = instance.data.get("frameStart") + inst_frame_end = instance.data.get("frameEnd") + frame_start_handle = frame_range["frameStart"] - int( + frame_range["handleStart"] + ) + frame_end_handle = frame_range["frameEnd"] + int( + frame_range["handleEnd"] + ) errors = [] - if frame_start != inst_frame_start: + if frame_start_handle != inst_frame_start: errors.append( f"Start frame ({inst_frame_start}) on instance does not match " # noqa - f"with the start frame ({frame_start}) set on the asset data. ") # noqa - if frame_end != inst_frame_end: + f"with the start frame ({frame_start_handle}) set on the asset data. ") # noqa + if frame_end_handle != inst_frame_end: errors.append( f"End frame ({inst_frame_end}) on instance does not match " - f"with the end frame ({frame_start}) from the asset data. ") + f"with the end frame ({frame_end_handle}) from the asset data. ") if errors: errors.append("You can use repair action to fix it.") - raise PublishValidationError("\n".join(errors)) + report = "Frame range settings are incorrect.\n\n" + for error in errors: + report += "- {}\n\n".format(error) + raise PublishValidationError(report, title="Frame Range incorrect") @classmethod def repair(cls, instance): - rt.rendStart = instance.context.data.get("frameStart") - rt.rendEnd = instance.context.data.get("frameEnd") + frame_range = get_frame_range() + frame_start_handle = frame_range["frameStart"] - int( + frame_range["handleStart"] + ) + frame_end_handle = frame_range["frameEnd"] + int( + frame_range["handleEnd"] + ) + if instance.data["family"] == "maxrender": + rt.rendStart = frame_start_handle + rt.rendEnd = frame_end_handle + else: + set_timeline(frame_start_handle, frame_end_handle) From f45c603da29da954a44aade1e23d5ce304ccc8f1 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 13 Oct 2023 22:16:21 +0800 Subject: [PATCH 0317/1233] add redshift proxy family --- openpype/hosts/max/plugins/publish/collect_frame_range.py | 2 +- openpype/hosts/max/plugins/publish/validate_frame_range.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/collect_frame_range.py b/openpype/hosts/max/plugins/publish/collect_frame_range.py index 197ecff0b1..6e5f928a8e 100644 --- a/openpype/hosts/max/plugins/publish/collect_frame_range.py +++ b/openpype/hosts/max/plugins/publish/collect_frame_range.py @@ -12,7 +12,7 @@ class CollectFrameRange(pyblish.api.InstancePlugin): hosts = ['max'] families = ["camera", "maxrender", "pointcache", "pointcloud", - "review"] + "review", "redshiftproxy"] def process(self, instance): if instance.data["family"] == "maxrender": diff --git a/openpype/hosts/max/plugins/publish/validate_frame_range.py b/openpype/hosts/max/plugins/publish/validate_frame_range.py index a50a3910c7..cf4d02c830 100644 --- a/openpype/hosts/max/plugins/publish/validate_frame_range.py +++ b/openpype/hosts/max/plugins/publish/validate_frame_range.py @@ -30,7 +30,7 @@ class ValidateFrameRange(pyblish.api.InstancePlugin, order = ValidateContentsOrder families = ["camera", "maxrender", "pointcache", "pointcloud", - "review"] + "review", "redshiftproxy"] hosts = ["max"] optional = True actions = [RepairAction] From f9dcd4bce67dd35c111f184a07cf489b07ed3537 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 13 Oct 2023 22:18:22 +0800 Subject: [PATCH 0318/1233] hound --- openpype/hosts/max/plugins/publish/validate_frame_range.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/validate_frame_range.py b/openpype/hosts/max/plugins/publish/validate_frame_range.py index cf4d02c830..b1e8aafbb7 100644 --- a/openpype/hosts/max/plugins/publish/validate_frame_range.py +++ b/openpype/hosts/max/plugins/publish/validate_frame_range.py @@ -58,7 +58,8 @@ class ValidateFrameRange(pyblish.api.InstancePlugin, if frame_end_handle != inst_frame_end: errors.append( f"End frame ({inst_frame_end}) on instance does not match " - f"with the end frame ({frame_end_handle}) from the asset data. ") + f"with the end frame ({frame_end_handle}) " + "from the asset data. ") if errors: errors.append("You can use repair action to fix it.") From 4ba25d35d3b8c8848bced077b9cc0f20b084bb32 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 13 Oct 2023 22:20:33 +0800 Subject: [PATCH 0319/1233] rename the py script --- .../{validate_containers.py => validate_instance_has_members.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename openpype/hosts/max/plugins/publish/{validate_containers.py => validate_instance_has_members.py} (100%) diff --git a/openpype/hosts/max/plugins/publish/validate_containers.py b/openpype/hosts/max/plugins/publish/validate_instance_has_members.py similarity index 100% rename from openpype/hosts/max/plugins/publish/validate_containers.py rename to openpype/hosts/max/plugins/publish/validate_instance_has_members.py From 59b7c61b3da7cb95b6b62c731ea0496dc83bac8a Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 13 Oct 2023 22:28:45 +0800 Subject: [PATCH 0320/1233] docstring for collect frane rabge --- openpype/hosts/max/plugins/publish/collect_frame_range.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/collect_frame_range.py b/openpype/hosts/max/plugins/publish/collect_frame_range.py index 6e5f928a8e..2dd39b5b50 100644 --- a/openpype/hosts/max/plugins/publish/collect_frame_range.py +++ b/openpype/hosts/max/plugins/publish/collect_frame_range.py @@ -5,7 +5,7 @@ from pymxs import runtime as rt class CollectFrameRange(pyblish.api.InstancePlugin): - """Collect Set Members.""" + """Collect Frame Range.""" order = pyblish.api.CollectorOrder + 0.01 label = "Collect Frame Range" From ca915cc1957371e63f748af5cf00526460e0acd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Fri, 13 Oct 2023 16:40:10 +0200 Subject: [PATCH 0321/1233] Update openpype/hosts/nuke/plugins/publish/extract_camera.py --- openpype/hosts/nuke/plugins/publish/extract_camera.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_camera.py b/openpype/hosts/nuke/plugins/publish/extract_camera.py index b0facd379a..5f9b5f154e 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_camera.py +++ b/openpype/hosts/nuke/plugins/publish/extract_camera.py @@ -36,8 +36,6 @@ class ExtractCamera(publish.Extractor): step = 1 output_range = str(nuke.FrameRange(first_frame, last_frame, step)) - self.log.debug("instance.data: `{}`".format( - pformat(instance.data))) rm_nodes = [] self.log.debug("Creating additional nodes for 3D Camera Extractor") From 636c7e02fd98718e9ea2ee739c47b8714426f39c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 13 Oct 2023 16:40:33 +0200 Subject: [PATCH 0322/1233] removing empty row --- openpype/hosts/nuke/plugins/publish/extract_camera.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_camera.py b/openpype/hosts/nuke/plugins/publish/extract_camera.py index 5f9b5f154e..3ec85c1f11 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_camera.py +++ b/openpype/hosts/nuke/plugins/publish/extract_camera.py @@ -36,7 +36,6 @@ class ExtractCamera(publish.Extractor): step = 1 output_range = str(nuke.FrameRange(first_frame, last_frame, step)) - rm_nodes = [] self.log.debug("Creating additional nodes for 3D Camera Extractor") subset = instance.data["subset"] From 7dfc32f66ca5fcb0dbc76c56d7ac9448022fa53b Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 13 Oct 2023 23:19:48 +0800 Subject: [PATCH 0323/1233] bug fix on the project setting being errored out when passing through the validator and extractor --- openpype/hosts/max/plugins/publish/extract_pointcloud.py | 1 + openpype/hosts/max/plugins/publish/validate_pointcloud.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_pointcloud.py b/openpype/hosts/max/plugins/publish/extract_pointcloud.py index 583bbb6dbd..190f049d23 100644 --- a/openpype/hosts/max/plugins/publish/extract_pointcloud.py +++ b/openpype/hosts/max/plugins/publish/extract_pointcloud.py @@ -36,6 +36,7 @@ class ExtractPointCloud(publish.Extractor): label = "Extract Point Cloud" hosts = ["max"] families = ["pointcloud"] + settings = [] def process(self, instance): self.settings = self.get_setting(instance) diff --git a/openpype/hosts/max/plugins/publish/validate_pointcloud.py b/openpype/hosts/max/plugins/publish/validate_pointcloud.py index 295a23f1f6..a336cbd80c 100644 --- a/openpype/hosts/max/plugins/publish/validate_pointcloud.py +++ b/openpype/hosts/max/plugins/publish/validate_pointcloud.py @@ -100,8 +100,8 @@ class ValidatePointCloud(pyblish.api.InstancePlugin): selection_list = instance.data["members"] - project_setting = instance.data["project_setting"] - attr_settings = project_setting["max"]["PointCloud"]["attribute"] + project_settings = instance.context.data["project_settings"] + attr_settings = project_settings["max"]["PointCloud"]["attribute"] for sel in selection_list: obj = sel.baseobject anim_names = rt.GetSubAnimNames(obj) From 94032f0522b90dcd94c63dd1d58177ce49ca1062 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 13 Oct 2023 17:39:29 +0200 Subject: [PATCH 0324/1233] :bug: convert generator to list --- openpype/hosts/maya/plugins/create/create_multishot_layout.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_multishot_layout.py b/openpype/hosts/maya/plugins/create/create_multishot_layout.py index 232ddc4389..9aabe43d8c 100644 --- a/openpype/hosts/maya/plugins/create/create_multishot_layout.py +++ b/openpype/hosts/maya/plugins/create/create_multishot_layout.py @@ -105,8 +105,8 @@ class CreateMultishotLayout(plugin.MayaCreator): ] def create(self, subset_name, instance_data, pre_create_data): - shots = self.get_related_shots( - folder_path=pre_create_data["shotParent"] + shots = list( + self.get_related_shots(folder_path=pre_create_data["shotParent"]) ) if not shots: # There are no shot folders under the specified folder. From 971164cd7ff29a52d865dfe2f58084eb9adeb13b Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 13 Oct 2023 18:06:53 +0200 Subject: [PATCH 0325/1233] :bug: don't call cmds.ogs() if in headless mode --- openpype/hosts/maya/api/lib.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 510d4ecc85..0c571d41e0 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -146,13 +146,15 @@ def suspended_refresh(suspend=True): cmds.ogs(pause=True) is a toggle so we cant pass False. """ - original_state = cmds.ogs(query=True, pause=True) + original_state = None + if not IS_HEADLESS: + original_state = cmds.ogs(query=True, pause=True) try: - if suspend and not original_state: + if suspend and not original_state and not IS_HEADLESS: cmds.ogs(pause=True) yield finally: - if suspend and not original_state: + if suspend and not original_state and not IS_HEADLESS: cmds.ogs(pause=True) From adc60f19ca118194d73d8fae646f305de5c189b5 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Sat, 14 Oct 2023 03:24:39 +0000 Subject: [PATCH 0326/1233] [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 b0a79162b2..f98d4c1cf5 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.17.2" +__version__ = "3.17.3-nightly.1" From ac4ca2082fe342e1406d58f413df3479b59b4c16 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 14 Oct 2023 03:25:24 +0000 Subject: [PATCH 0327/1233] 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 25f36ebc9a..dba39ac36d 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.17.3-nightly.1 - 3.17.2 - 3.17.2-nightly.4 - 3.17.2-nightly.3 @@ -134,7 +135,6 @@ body: - 3.14.11-nightly.3 - 3.14.11-nightly.2 - 3.14.11-nightly.1 - - 3.14.10 validations: required: true - type: dropdown From cffe48fc205217c83bf0a402b325dfca11b30524 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 16 Oct 2023 09:57:22 +0200 Subject: [PATCH 0328/1233] :recycle: simplify the code --- openpype/hosts/maya/api/lib.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 0c571d41e0..7c49c837e9 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -146,15 +146,17 @@ def suspended_refresh(suspend=True): cmds.ogs(pause=True) is a toggle so we cant pass False. """ - original_state = None - if not IS_HEADLESS: - original_state = cmds.ogs(query=True, pause=True) + if IS_HEADLESS: + yield + return + + original_state = cmds.ogs(query=True, pause=True) try: - if suspend and not original_state and not IS_HEADLESS: + if suspend and not original_state: cmds.ogs(pause=True) yield finally: - if suspend and not original_state and not IS_HEADLESS: + if suspend and not original_state: cmds.ogs(pause=True) From 5bcbf80d127d81b785382f7398836950547ff244 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 16 Oct 2023 10:02:55 +0200 Subject: [PATCH 0329/1233] :recycle: remove unused code --- openpype/hosts/maya/plugins/create/create_multishot_layout.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/create/create_multishot_layout.py b/openpype/hosts/maya/plugins/create/create_multishot_layout.py index 9aabe43d8c..dae318512a 100644 --- a/openpype/hosts/maya/plugins/create/create_multishot_layout.py +++ b/openpype/hosts/maya/plugins/create/create_multishot_layout.py @@ -52,7 +52,6 @@ class CreateMultishotLayout(plugin.MayaCreator): current_path_parts = current_folder["path"].split("/") - items_with_label = [] # populate the list with parents of the current folder # this will create menu items like: # [ From 7431f6e9ef68b95ad0dc6c7ac0e9b1c1656672ce Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 16 Oct 2023 16:06:58 +0800 Subject: [PATCH 0330/1233] make sure original basename is used during publishing as the tyc export needs very strict naming --- openpype/hosts/max/plugins/load/load_tycache.py | 2 +- .../plugins/publish/collect_tycache_attributes.py | 4 +++- .../hosts/max/plugins/publish/extract_tycache.py | 15 ++++++++------- .../defaults/project_anatomy/templates.json | 6 ++++++ .../defaults/project_settings/global.json | 11 +++++++++++ server_addon/core/server/settings/tools.py | 11 +++++++++++ server_addon/core/server/version.py | 2 +- 7 files changed, 41 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_tycache.py b/openpype/hosts/max/plugins/load/load_tycache.py index 7eac0de3e5..ff3a26fbd6 100644 --- a/openpype/hosts/max/plugins/load/load_tycache.py +++ b/openpype/hosts/max/plugins/load/load_tycache.py @@ -13,7 +13,7 @@ from openpype.hosts.max.api.pipeline import ( from openpype.pipeline import get_representation_path, load -class PointCloudLoader(load.LoaderPlugin): +class TyCacheLoader(load.LoaderPlugin): """Point Cloud Loader.""" families = ["tycache"] diff --git a/openpype/hosts/max/plugins/publish/collect_tycache_attributes.py b/openpype/hosts/max/plugins/publish/collect_tycache_attributes.py index d735b2f2c0..56cf6614e2 100644 --- a/openpype/hosts/max/plugins/publish/collect_tycache_attributes.py +++ b/openpype/hosts/max/plugins/publish/collect_tycache_attributes.py @@ -42,6 +42,7 @@ class CollectTyCacheData(pyblish.api.InstancePlugin, "tycacheChanMaterials", "tycacheChanCustomFloat" "tycacheChanCustomVector", "tycacheChanCustomTM", "tycacheChanPhysX", "tycacheMeshBackup", + "tycacheCreateObject", "tycacheCreateObjectIfNotCreated", "tycacheAdditionalCloth", "tycacheAdditionalSkin", @@ -59,7 +60,8 @@ class CollectTyCacheData(pyblish.api.InstancePlugin, "tycacheChanRot", "tycacheChanScale", "tycacheChanVel", "tycacheChanShape", "tycacheChanMatID", "tycacheChanMapping", - "tycacheChanMaterials"] + "tycacheChanMaterials", "tycacheCreateObject", + "tycacheCreateObjectIfNotCreated"] return [ EnumDef("all_tyc_attrs", tyc_attr_enum, diff --git a/openpype/hosts/max/plugins/publish/extract_tycache.py b/openpype/hosts/max/plugins/publish/extract_tycache.py index 0327564b3a..a787080776 100644 --- a/openpype/hosts/max/plugins/publish/extract_tycache.py +++ b/openpype/hosts/max/plugins/publish/extract_tycache.py @@ -37,7 +37,7 @@ class ExtractTyCache(publish.Extractor): stagingdir = self.staging_dir(instance) filename = "{name}.tyc".format(**instance.data) path = os.path.join(stagingdir, filename) - filenames = self.get_file(instance, start, end) + filenames = self.get_files(instance, start, end) additional_attributes = instance.data.get("tyc_attrs", {}) with maintained_selection(): @@ -55,14 +55,14 @@ class ExtractTyCache(publish.Extractor): tycache_spline_enabled=has_tyc_spline) for job in job_args: rt.Execute(job) - + representations = instance.data.setdefault("representations", []) representation = { 'name': 'tyc', 'ext': 'tyc', 'files': filenames if len(filenames) > 1 else filenames[0], - "stagingDir": stagingdir + "stagingDir": stagingdir, } - instance.data["representations"].append(representation) + representations.append(representation) self.log.info(f"Extracted instance '{instance.name}' to: {filenames}") # Get the tyMesh filename for extraction @@ -71,13 +71,14 @@ class ExtractTyCache(publish.Extractor): 'name': 'tyMesh', 'ext': 'tyc', 'files': mesh_filename, - "stagingDir": stagingdir + "stagingDir": stagingdir, + "outputName": '__tyMesh' } - instance.data["representations"].append(mesh_repres) + representations.append(mesh_repres) self.log.info( f"Extracted instance '{instance.name}' to: {mesh_filename}") - def get_file(self, instance, start_frame, end_frame): + def get_files(self, instance, start_frame, end_frame): """Get file names for tyFlow in tyCache format. Set the filenames accordingly to the tyCache file diff --git a/openpype/settings/defaults/project_anatomy/templates.json b/openpype/settings/defaults/project_anatomy/templates.json index e5e535bf19..aa3f8d4843 100644 --- a/openpype/settings/defaults/project_anatomy/templates.json +++ b/openpype/settings/defaults/project_anatomy/templates.json @@ -53,6 +53,11 @@ "file": "{originalBasename}<.{@frame}><_{udim}>.{ext}", "path": "{@folder}/{@file}" }, + "tycache": { + "folder": "{root[work]}/{project[name]}/{hierarchy}/{asset}/publish/{family}/{subset}/{@version}", + "file": "{originalBasename}<_{@version}><_{@frame}>.{ext}", + "path": "{@folder}/{@file}" + }, "source": { "folder": "{root[work]}/{originalDirname}", "file": "{originalBasename}.{ext}", @@ -66,6 +71,7 @@ "simpleUnrealTextureHero": "Simple Unreal Texture - Hero", "simpleUnrealTexture": "Simple Unreal Texture", "online": "online", + "tycache": "tycache", "source": "source", "transient": "transient" } diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 06a595d1c5..9ccf5cae05 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -546,6 +546,17 @@ "task_types": [], "task_names": [], "template_name": "online" + }, + { + "families": [ + "tycache" + ], + "hosts": [ + "max" + ], + "task_types": [], + "task_names": [], + "template_name": "tycache" } ], "hero_template_name_profiles": [ diff --git a/server_addon/core/server/settings/tools.py b/server_addon/core/server/settings/tools.py index 7befc795e4..d7c7b367b7 100644 --- a/server_addon/core/server/settings/tools.py +++ b/server_addon/core/server/settings/tools.py @@ -487,6 +487,17 @@ DEFAULT_TOOLS_VALUES = { "task_types": [], "task_names": [], "template_name": "publish_online" + }, + { + "families": [ + "tycache" + ], + "hosts": [ + "max" + ], + "task_types": [], + "task_names": [], + "template_name": "publish_tycache" } ], "hero_template_name_profiles": [ diff --git a/server_addon/core/server/version.py b/server_addon/core/server/version.py index b3f4756216..ae7362549b 100644 --- a/server_addon/core/server/version.py +++ b/server_addon/core/server/version.py @@ -1 +1 @@ -__version__ = "0.1.2" +__version__ = "0.1.3" From 4a0f87c5179959e6375ace26c0307ff7c29c0e9e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 16 Oct 2023 16:09:08 +0800 Subject: [PATCH 0331/1233] updated the file naming convention --- openpype/settings/defaults/project_anatomy/templates.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_anatomy/templates.json b/openpype/settings/defaults/project_anatomy/templates.json index aa3f8d4843..5694693c97 100644 --- a/openpype/settings/defaults/project_anatomy/templates.json +++ b/openpype/settings/defaults/project_anatomy/templates.json @@ -55,7 +55,7 @@ }, "tycache": { "folder": "{root[work]}/{project[name]}/{hierarchy}/{asset}/publish/{family}/{subset}/{@version}", - "file": "{originalBasename}<_{@version}><_{@frame}>.{ext}", + "file": "{originalBasename}<_{@frame}>.{ext}", "path": "{@folder}/{@file}" }, "source": { From 4bb51b91de8a903cc3540f3582cc22440fda60a0 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 16 Oct 2023 10:09:39 +0200 Subject: [PATCH 0332/1233] :bulb: rewrite todo comment to make it more clear --- openpype/hosts/maya/plugins/create/create_multishot_layout.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/create/create_multishot_layout.py b/openpype/hosts/maya/plugins/create/create_multishot_layout.py index dae318512a..0b027c02ea 100644 --- a/openpype/hosts/maya/plugins/create/create_multishot_layout.py +++ b/openpype/hosts/maya/plugins/create/create_multishot_layout.py @@ -37,7 +37,7 @@ class CreateMultishotLayout(plugin.MayaCreator): # selected folder to create the Camera Sequencer. """ - Todo: get this needs to be switched to get_folder_by_path + Todo: `get_folder_by_name` should be switched to `get_folder_by_path` once the fork to pure AYON is done. Warning: this will not work for projects where the asset name From 84e77a970719618710e68bd17a9a69bdf5311442 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 16 Oct 2023 12:30:49 +0200 Subject: [PATCH 0333/1233] fixing unc paths on windows with backward slashes --- openpype/hosts/nuke/api/lib.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 62f3a3c3ff..b061271f5a 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -2222,7 +2222,6 @@ 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)) @@ -2303,7 +2302,7 @@ Reopening Nuke should synchronize these paths and resolve any discrepancies. if env_path in path: # with regsub we make sure path format of slashes is correct resub_expr = ( - "[regsub -all {{\\\\}} [getenv {}] \"/\"]").format(env_var) + "[regsub -all {{\\}} [getenv {}] \"/\"]").format(env_var) new_path = path.replace( env_path, resub_expr From 9cd8c864eb10a0d646259fc18e96d9f21315ff5e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 16 Oct 2023 17:37:50 +0200 Subject: [PATCH 0334/1233] fix default factory of tools --- server_addon/applications/server/settings.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/server_addon/applications/server/settings.py b/server_addon/applications/server/settings.py index fd481b6ce8..be9a2ea07e 100644 --- a/server_addon/applications/server/settings.py +++ b/server_addon/applications/server/settings.py @@ -115,9 +115,7 @@ class ToolGroupModel(BaseSettingsModel): name: str = Field("", title="Name") label: str = Field("", title="Label") environment: str = Field("{}", title="Environments", widget="textarea") - variants: list[ToolVariantModel] = Field( - default_factory=ToolVariantModel - ) + variants: list[ToolVariantModel] = Field(default_factory=list) @validator("environment") def validate_json(cls, value): From 2afc95f514e80c73027a183ec46d3d0b237cd322 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 16 Oct 2023 17:39:53 +0200 Subject: [PATCH 0335/1233] bump version --- server_addon/applications/server/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/applications/server/version.py b/server_addon/applications/server/version.py index 485f44ac21..b3f4756216 100644 --- a/server_addon/applications/server/version.py +++ b/server_addon/applications/server/version.py @@ -1 +1 @@ -__version__ = "0.1.1" +__version__ = "0.1.2" From 32ce0671323d053351d8f43aec0bd7e4f9e856a2 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 17 Oct 2023 10:11:47 +0200 Subject: [PATCH 0336/1233] OP-7134 - added missing OPENPYPE_VERSION --- .../deadline/plugins/publish/submit_fusion_deadline.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py index 0b97582d2a..9a718aa089 100644 --- a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py @@ -13,7 +13,8 @@ from openpype.pipeline.publish import ( ) from openpype.lib import ( BoolDef, - NumberDef + NumberDef, + is_running_from_build ) @@ -230,6 +231,11 @@ class FusionSubmitDeadline( "OPENPYPE_LOG_NO_COLORS", "IS_TEST" ] + + # Add OpenPype version if we are running from build. + if is_running_from_build(): + keys.append("OPENPYPE_VERSION") + environment = dict({key: os.environ[key] for key in keys if key in os.environ}, **legacy_io.Session) From 2e34fc444af468b7e46eb495346147f368be4542 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 17 Oct 2023 10:53:21 +0200 Subject: [PATCH 0337/1233] skip tasks when looking for asset entity --- .../modules/ftrack/plugins/publish/collect_ftrack_api.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/collect_ftrack_api.py b/openpype/modules/ftrack/plugins/publish/collect_ftrack_api.py index fe3275ce2c..aade709360 100644 --- a/openpype/modules/ftrack/plugins/publish/collect_ftrack_api.py +++ b/openpype/modules/ftrack/plugins/publish/collect_ftrack_api.py @@ -194,10 +194,11 @@ class CollectFtrackApi(pyblish.api.ContextPlugin): "TypedContext where project_id is \"{}\" and name in ({})" ).format(project_entity["id"], joined_asset_names)).all() - entities_by_name = { - entity["name"]: entity - for entity in entities - } + entities_by_name = {} + for entity in entities: + if entity.entity_type.lower() == "task": + continue + entities_by_name[entity["name"]] = entity for asset_name, by_task_data in instance_by_asset_and_task.items(): entity = entities_by_name.get(asset_name) From f12a2fb504d6ef449897e5f338f37d67b9b86215 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 17 Oct 2023 11:29:52 +0200 Subject: [PATCH 0338/1233] Nuke: gizmo loading representations fixed --- openpype/hosts/nuke/plugins/load/load_gizmo.py | 2 +- openpype/hosts/nuke/plugins/load/load_gizmo_ip.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/nuke/plugins/load/load_gizmo.py b/openpype/hosts/nuke/plugins/load/load_gizmo.py index ede05c422b..5d028fc2db 100644 --- a/openpype/hosts/nuke/plugins/load/load_gizmo.py +++ b/openpype/hosts/nuke/plugins/load/load_gizmo.py @@ -26,7 +26,7 @@ class LoadGizmo(load.LoaderPlugin): families = ["gizmo"] representations = ["*"] - extensions = {"gizmo"} + extensions = {"nk"} label = "Load Gizmo" order = 0 diff --git a/openpype/hosts/nuke/plugins/load/load_gizmo_ip.py b/openpype/hosts/nuke/plugins/load/load_gizmo_ip.py index d567aaf7b0..ba2de3d05d 100644 --- a/openpype/hosts/nuke/plugins/load/load_gizmo_ip.py +++ b/openpype/hosts/nuke/plugins/load/load_gizmo_ip.py @@ -28,7 +28,7 @@ class LoadGizmoInputProcess(load.LoaderPlugin): families = ["gizmo"] representations = ["*"] - extensions = {"gizmo"} + extensions = {"nk"} label = "Load Gizmo - Input Process" order = 0 From 9ce0ef1d9c2942bbaaf3920ce53ac217d3674bee Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 17 Oct 2023 11:31:40 +0200 Subject: [PATCH 0339/1233] use object type id to skip tasks --- .../plugins/publish/collect_ftrack_api.py | 58 ++++++++++++------- 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/collect_ftrack_api.py b/openpype/modules/ftrack/plugins/publish/collect_ftrack_api.py index aade709360..c78abbd1d6 100644 --- a/openpype/modules/ftrack/plugins/publish/collect_ftrack_api.py +++ b/openpype/modules/ftrack/plugins/publish/collect_ftrack_api.py @@ -44,19 +44,25 @@ class CollectFtrackApi(pyblish.api.ContextPlugin): self.log.debug("Project found: {0}".format(project_entity)) + task_object_type = session.query( + "ObjectType where name is 'Task'").one() + task_object_type_id = task_object_type["id"] asset_entity = None if asset_name: # Find asset entity entity_query = ( - 'TypedContext where project_id is "{0}"' - ' and name is "{1}"' - ).format(project_entity["id"], asset_name) + "TypedContext where project_id is '{}'" + " and name is '{}'" + " and object_type_id != '{}'" + ).format( + project_entity["id"], + asset_name, + task_object_type_id + ) self.log.debug("Asset entity query: < {0} >".format(entity_query)) asset_entities = [] for entity in session.query(entity_query).all(): - # Skip tasks - if entity.entity_type.lower() != "task": - asset_entities.append(entity) + asset_entities.append(entity) if len(asset_entities) == 0: raise AssertionError(( @@ -103,10 +109,19 @@ class CollectFtrackApi(pyblish.api.ContextPlugin): context.data["ftrackEntity"] = asset_entity context.data["ftrackTask"] = task_entity - self.per_instance_process(context, asset_entity, task_entity) + self.per_instance_process( + context, + asset_entity, + task_entity, + task_object_type_id + ) def per_instance_process( - self, context, context_asset_entity, context_task_entity + self, + context, + asset_entity, + task_entity, + task_object_type_id ): context_task_name = None context_asset_name = None @@ -182,24 +197,27 @@ class CollectFtrackApi(pyblish.api.ContextPlugin): session = context.data["ftrackSession"] project_entity = context.data["ftrackProject"] - asset_names = set() - for asset_name in instance_by_asset_and_task.keys(): - asset_names.add(asset_name) + asset_names = set(instance_by_asset_and_task.keys()) joined_asset_names = ",".join([ "\"{}\"".format(name) for name in asset_names ]) - entities = session.query(( - "TypedContext where project_id is \"{}\" and name in ({})" - ).format(project_entity["id"], joined_asset_names)).all() - - entities_by_name = {} - for entity in entities: - if entity.entity_type.lower() == "task": - continue - entities_by_name[entity["name"]] = entity + entities = session.query( + ( + "TypedContext where project_id is \"{}\" and name in ({})" + " and object_type_id != '{}'" + ).format( + project_entity["id"], + joined_asset_names, + task_object_type_id + ) + ).all() + entities_by_name = { + entity["name"]: entity + for entity in entities + } for asset_name, by_task_data in instance_by_asset_and_task.items(): entity = entities_by_name.get(asset_name) task_entity_by_name = {} From c8248dfc9ccc780c66c77bf8aa5d8ea23c257e33 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 17 Oct 2023 11:34:30 +0200 Subject: [PATCH 0340/1233] updating gizmo with maintained dependencies closes https://github.com/ynput/OpenPype/issues/5501 --- openpype/hosts/nuke/api/lib.py | 67 ++++++++++++++++--- .../hosts/nuke/plugins/load/load_gizmo.py | 42 ++++++------ .../hosts/nuke/plugins/load/load_gizmo_ip.py | 45 +++++++------ 3 files changed, 103 insertions(+), 51 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 62f3a3c3ff..0a5f772346 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -48,20 +48,15 @@ from openpype.pipeline import ( get_current_asset_name, ) from openpype.pipeline.context_tools import ( - get_current_project_asset, get_custom_workfile_template_from_session ) -from openpype.pipeline.colorspace import ( - get_imageio_config -) +from openpype.pipeline.colorspace import get_imageio_config from openpype.pipeline.workfile import BuildWorkfile from . import gizmo_menu from .constants import ASSIST -from .workio import ( - save_file, - open_file -) +from .workio import save_file +from .utils import get_node_outputs log = Logger.get_logger(__name__) @@ -2802,7 +2797,7 @@ def find_free_space_to_paste_nodes( @contextlib.contextmanager -def maintained_selection(): +def maintained_selection(exclude_nodes=None): """Maintain selection during context Example: @@ -2811,7 +2806,12 @@ def maintained_selection(): >>> print(node["selected"].value()) False """ + if exclude_nodes: + for node in exclude_nodes: + node["selected"].setValue(False) + previous_selection = nuke.selectedNodes() + try: yield finally: @@ -2823,6 +2823,51 @@ def maintained_selection(): select_nodes(previous_selection) +@contextlib.contextmanager +def swap_node_with_dependency(old_node, new_node): + """ Swap node with dependency + + Swap node with dependency and reconnect all inputs and outputs. + It removes old node. + + Arguments: + old_node (nuke.Node): node to be replaced + new_node (nuke.Node): node to replace with + + Example: + >>> old_node_name = old_node["name"].value() + >>> print(old_node_name) + old_node_name_01 + >>> with swap_node_with_dependency(old_node, new_node) as node_name: + ... new_node["name"].setValue(node_name) + >>> print(new_node["name"].value()) + old_node_name_01 + """ + # preserve position + xpos, ypos = old_node.xpos(), old_node.ypos() + # preserve selection after all is done + outputs = get_node_outputs(old_node) + inputs = old_node.dependencies() + node_name = old_node["name"].value() + + try: + nuke.delete(old_node) + + yield node_name + finally: + + # Reconnect inputs + for i, node in enumerate(inputs): + new_node.setInput(i, node) + # Reconnect outputs + if outputs: + for n, pipes in outputs.items(): + for i in pipes: + n.setInput(i, new_node) + # return to original position + new_node.setXYpos(xpos, ypos) + + def reset_selection(): """Deselect all selected nodes""" for node in nuke.selectedNodes(): @@ -2920,13 +2965,13 @@ def process_workfile_builder(): "workfile_builder", {}) # get settings - createfv_on = workfile_builder.get("create_first_version") or None + create_fv_on = workfile_builder.get("create_first_version") or None builder_on = workfile_builder.get("builder_on_start") or None last_workfile_path = os.environ.get("AVALON_LAST_WORKFILE") # generate first version in file not existing and feature is enabled - if createfv_on and not os.path.exists(last_workfile_path): + if create_fv_on and not os.path.exists(last_workfile_path): # get custom template path if any custom_template_path = get_custom_workfile_template_from_session( project_settings=project_settings diff --git a/openpype/hosts/nuke/plugins/load/load_gizmo.py b/openpype/hosts/nuke/plugins/load/load_gizmo.py index 5d028fc2db..23cf4d7741 100644 --- a/openpype/hosts/nuke/plugins/load/load_gizmo.py +++ b/openpype/hosts/nuke/plugins/load/load_gizmo.py @@ -12,7 +12,8 @@ from openpype.pipeline import ( from openpype.hosts.nuke.api.lib import ( maintained_selection, get_avalon_knob_data, - set_avalon_knob_data + set_avalon_knob_data, + swap_node_with_dependency, ) from openpype.hosts.nuke.api import ( containerise, @@ -45,7 +46,7 @@ class LoadGizmo(load.LoaderPlugin): data (dict): compulsory attribute > not used Returns: - nuke node: containerised nuke node object + nuke node: containerized nuke node object """ # get main variables @@ -83,12 +84,12 @@ class LoadGizmo(load.LoaderPlugin): # add group from nk nuke.nodePaste(file) - GN = nuke.selectedNode() + group_node = nuke.selectedNode() - GN["name"].setValue(object_name) + group_node["name"].setValue(object_name) return containerise( - node=GN, + node=group_node, name=name, namespace=namespace, context=context, @@ -110,7 +111,7 @@ class LoadGizmo(load.LoaderPlugin): version_doc = get_version_by_id(project_name, representation["parent"]) # get corresponding node - GN = nuke.toNode(container['objectName']) + group_node = nuke.toNode(container['objectName']) file = get_representation_path(representation).replace("\\", "/") name = container['name'] @@ -135,22 +136,24 @@ class LoadGizmo(load.LoaderPlugin): for k in add_keys: data_imprint.update({k: version_data[k]}) + # capture pipeline metadata + avalon_data = get_avalon_knob_data(group_node) + # adding nodes to node graph # just in case we are in group lets jump out of it nuke.endGroup() - with maintained_selection(): - xpos = GN.xpos() - ypos = GN.ypos() - avalon_data = get_avalon_knob_data(GN) - nuke.delete(GN) - # add group from nk + with maintained_selection([group_node]): + # insert nuke script to the script nuke.nodePaste(file) - - GN = nuke.selectedNode() - set_avalon_knob_data(GN, avalon_data) - GN.setXYpos(xpos, ypos) - GN["name"].setValue(object_name) + # convert imported to selected node + new_group_node = nuke.selectedNode() + # swap nodes with maintained connections + with swap_node_with_dependency( + group_node, new_group_node) as node_name: + new_group_node["name"].setValue(node_name) + # set updated pipeline metadata + set_avalon_knob_data(new_group_node, avalon_data) last_version_doc = get_last_version_by_subset_id( project_name, version_doc["parent"], fields=["_id"] @@ -161,11 +164,12 @@ class LoadGizmo(load.LoaderPlugin): color_value = self.node_color else: color_value = "0xd88467ff" - GN["tile_color"].setValue(int(color_value, 16)) + + new_group_node["tile_color"].setValue(int(color_value, 16)) self.log.info("updated to version: {}".format(version_doc.get("name"))) - return update_container(GN, data_imprint) + return update_container(new_group_node, data_imprint) def switch(self, container, representation): self.update(container, representation) diff --git a/openpype/hosts/nuke/plugins/load/load_gizmo_ip.py b/openpype/hosts/nuke/plugins/load/load_gizmo_ip.py index ba2de3d05d..ce0a1615f1 100644 --- a/openpype/hosts/nuke/plugins/load/load_gizmo_ip.py +++ b/openpype/hosts/nuke/plugins/load/load_gizmo_ip.py @@ -14,7 +14,8 @@ from openpype.hosts.nuke.api.lib import ( maintained_selection, create_backdrop, get_avalon_knob_data, - set_avalon_knob_data + set_avalon_knob_data, + swap_node_with_dependency, ) from openpype.hosts.nuke.api import ( containerise, @@ -47,7 +48,7 @@ class LoadGizmoInputProcess(load.LoaderPlugin): data (dict): compulsory attribute > not used Returns: - nuke node: containerised nuke node object + nuke node: containerized nuke node object """ # get main variables @@ -85,17 +86,17 @@ class LoadGizmoInputProcess(load.LoaderPlugin): # add group from nk nuke.nodePaste(file) - GN = nuke.selectedNode() + group_node = nuke.selectedNode() - GN["name"].setValue(object_name) + group_node["name"].setValue(object_name) # try to place it under Viewer1 - if not self.connect_active_viewer(GN): - nuke.delete(GN) + if not self.connect_active_viewer(group_node): + nuke.delete(group_node) return return containerise( - node=GN, + node=group_node, name=name, namespace=namespace, context=context, @@ -117,7 +118,7 @@ class LoadGizmoInputProcess(load.LoaderPlugin): version_doc = get_version_by_id(project_name, representation["parent"]) # get corresponding node - GN = nuke.toNode(container['objectName']) + group_node = nuke.toNode(container['objectName']) file = get_representation_path(representation).replace("\\", "/") name = container['name'] @@ -142,22 +143,24 @@ class LoadGizmoInputProcess(load.LoaderPlugin): for k in add_keys: data_imprint.update({k: version_data[k]}) + # capture pipeline metadata + avalon_data = get_avalon_knob_data(group_node) + # adding nodes to node graph # just in case we are in group lets jump out of it nuke.endGroup() - with maintained_selection(): - xpos = GN.xpos() - ypos = GN.ypos() - avalon_data = get_avalon_knob_data(GN) - nuke.delete(GN) - # add group from nk + with maintained_selection([group_node]): + # insert nuke script to the script nuke.nodePaste(file) - - GN = nuke.selectedNode() - set_avalon_knob_data(GN, avalon_data) - GN.setXYpos(xpos, ypos) - GN["name"].setValue(object_name) + # convert imported to selected node + new_group_node = nuke.selectedNode() + # swap nodes with maintained connections + with swap_node_with_dependency( + group_node, new_group_node) as node_name: + new_group_node["name"].setValue(node_name) + # set updated pipeline metadata + set_avalon_knob_data(new_group_node, avalon_data) last_version_doc = get_last_version_by_subset_id( project_name, version_doc["parent"], fields=["_id"] @@ -168,11 +171,11 @@ class LoadGizmoInputProcess(load.LoaderPlugin): color_value = self.node_color else: color_value = "0xd88467ff" - GN["tile_color"].setValue(int(color_value, 16)) + new_group_node["tile_color"].setValue(int(color_value, 16)) self.log.info("updated to version: {}".format(version_doc.get("name"))) - return update_container(GN, data_imprint) + return update_container(new_group_node, data_imprint) def connect_active_viewer(self, group_node): """ From 247451bb5871d1287dd20362081a2c09637f5688 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 17 Oct 2023 11:45:55 +0200 Subject: [PATCH 0341/1233] thumbnail extractor as last extractor --- openpype/plugins/publish/extract_thumbnail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/extract_thumbnail.py b/openpype/plugins/publish/extract_thumbnail.py index de101ac7ac..0ddbb3f40b 100644 --- a/openpype/plugins/publish/extract_thumbnail.py +++ b/openpype/plugins/publish/extract_thumbnail.py @@ -17,7 +17,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): """Create jpg thumbnail from sequence using ffmpeg""" label = "Extract Thumbnail" - order = pyblish.api.ExtractorOrder + order = pyblish.api.ExtractorOrder + 0.49 families = [ "imagesequence", "render", "render2d", "prerender", "source", "clip", "take", "online", "image" From 908b8e3fb69a2347d6b6208f32c546dc55ba061a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 17 Oct 2023 12:22:01 +0200 Subject: [PATCH 0342/1233] Add MayaUsdReferenceLoader to reference USD as Maya native geometry using `mayaUSDImport` file translator --- openpype/hosts/maya/api/plugin.py | 3 +- .../hosts/maya/plugins/load/load_reference.py | 100 +++++++++++++++++- 2 files changed, 101 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py index 3b54954c8a..07167a9a32 100644 --- a/openpype/hosts/maya/api/plugin.py +++ b/openpype/hosts/maya/api/plugin.py @@ -771,7 +771,8 @@ class ReferenceLoader(Loader): "ma": "mayaAscii", "mb": "mayaBinary", "abc": "Alembic", - "fbx": "FBX" + "fbx": "FBX", + "usd": "USD Import" }.get(representation["name"]) assert file_type, "Unsupported representation: %s" % representation diff --git a/openpype/hosts/maya/plugins/load/load_reference.py b/openpype/hosts/maya/plugins/load/load_reference.py index 4b704fa706..0d7f08d3c3 100644 --- a/openpype/hosts/maya/plugins/load/load_reference.py +++ b/openpype/hosts/maya/plugins/load/load_reference.py @@ -1,7 +1,9 @@ import os import difflib import contextlib + from maya import cmds +import qargparse from openpype.settings import get_project_settings import openpype.hosts.maya.api.plugin @@ -128,6 +130,12 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): if not attach_to_root: group_name = namespace + kwargs = {} + if "file_options" in options: + kwargs["options"] = options["file_options"] + if "file_type" in options: + kwargs["type"] = options["file_type"] + path = self.filepath_from_context(context) with maintained_selection(): cmds.loadPlugin("AbcImport.mll", quiet=True) @@ -139,7 +147,8 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): reference=True, returnNewNodes=True, groupReference=attach_to_root, - groupName=group_name) + groupName=group_name, + **kwargs) shapes = cmds.ls(nodes, shapes=True, long=True) @@ -251,3 +260,92 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): else: self.log.warning("This version of Maya does not support locking of" " transforms of cameras.") + + +class MayaUSDReferenceLoader(ReferenceLoader): + """Reference USD file to native Maya nodes using MayaUSDImport reference""" + + families = ["usd"] + representations = ["usd"] + extensions = {"usd", "usda", "usdc"} + + options = ReferenceLoader.options + [ + qargparse.Boolean( + "readAnimData", + label="Load anim data", + default=True, + help="Load animation data from USD file" + ), + qargparse.Boolean( + "useAsAnimationCache", + label="Use as animation cache", + default=True, + help=( + "Imports geometry prims with time-sampled point data using a " + "point-based deformer that references the imported " + "USD file.\n" + "This provides better import and playback performance when " + "importing time-sampled geometry from USD, and should " + "reduce the weight of the resulting Maya scene." + ) + ), + qargparse.Boolean( + "importInstances", + label="Import instances", + default=True, + help=( + "Import USD instanced geometries as Maya instanced shapes. " + "Will flatten the scene otherwise." + ) + ), + qargparse.String( + "primPath", + label="Prim Path", + default="/", + help=( + "Name of the USD scope where traversing will begin.\n" + "The prim at the specified primPath (including the prim) will " + "be imported.\n" + "Specifying the pseudo-root (/) means you want " + "to import everything in the file.\n" + "If the passed prim path is empty, it will first try to " + "import the defaultPrim for the rootLayer if it exists.\n" + "Otherwise, it will behave as if the pseudo-root was passed " + "in." + ) + ) + ] + + file_type = "USD Import" + + def process_reference(self, context, name, namespace, options): + cmds.loadPlugin("mayaUsdPlugin", quiet=True) + + def bool_option(key, default): + # Shorthand for getting optional boolean file option from options + value = int(bool(options.get(key, default))) + return "{}={}".format(key, value) + + def string_option(key, default): + # Shorthand for getting optional string file option from options + value = str(options.get(key, default)) + return "{}={}".format(key, value) + + options["file_options"] = ";".join([ + string_option("primPath", default="/"), + bool_option("importInstances", default=True), + bool_option("useAsAnimationCache", default=True), + bool_option("readAnimData", default=True), + # TODO: Expose more parameters + # "preferredMaterial=none", + # "importRelativeTextures=Automatic", + # "useCustomFrameRange=0", + # "startTime=0", + # "endTime=0", + # "importUSDZTextures=0" + ]) + options["file_type"] = self.file_type + + return super(MayaUSDReferenceLoader, self).process_reference( + context, name, namespace, options + ) From a1a4898a731547db13a884f40df5640b48d65b6a Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 17 Oct 2023 18:31:22 +0800 Subject: [PATCH 0343/1233] updated the publish review animation for 2023 and 2024 3dsMax respectively --- openpype/hosts/max/api/lib.py | 159 ++++++++++++++---- .../hosts/max/plugins/create/create_review.py | 8 +- .../max/plugins/publish/collect_review.py | 11 +- .../publish/extract_review_animation.py | 24 +-- .../publish/validate_resolution_setting.py | 2 +- 5 files changed, 143 insertions(+), 61 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 26ca5ed1d8..6817160ce7 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- """Library of functions useful for 3dsmax pipeline.""" +import os import contextlib import logging import json @@ -323,6 +324,11 @@ def is_headless(): @contextlib.contextmanager def viewport_setup_updated(camera): + """Function to set viewport camera during context + ***For 3dsMax 2024+ + Args: + camera (str): viewport camera for review render + """ original = rt.viewport.getCamera() has_autoplay = rt.preferences.playPreviewWhenDone if not original: @@ -340,32 +346,59 @@ def viewport_setup_updated(camera): @contextlib.contextmanager -def viewport_setup(viewport_setting, visual_style, camera): - """Function to set visual style options +def viewport_setup(instance, viewport_setting, camera): + """Function to set camera and other viewport options + during context + ****For Max Version < 2024 Args: - visual_style (str): visual style for active viewport + instance (str): instance + viewport_setting (str): active viewport setting + camera (str): viewport camera - Returns: - list: the argument which can set visual style """ original = rt.viewport.getCamera() + has_vp_btn = rt.ViewportButtonMgr.EnableButtons has_autoplay = rt.preferences.playPreviewWhenDone if not original: # if there is no original camera # use the current camera as original original = rt.getNodeByName(camera) review_camera = rt.getNodeByName(camera) - current_setting = viewport_setting.VisualStyleMode + + current_visualStyle = viewport_setting.VisualStyleMode + current_visualPreset = viewport_setting.ViewportPreset + current_useTexture = viewport_setting.UseTextureEnabled + orig_vp_grid = rt.viewport.getGridVisibility(1) + orig_vp_bkg = rt.viewport.IsSolidBackgroundColorMode() + + visualStyle = instance.data.get("visualStyleMode") + viewportPreset = instance.data.get("viewportPreset") + useTexture = instance.data.get("vpTexture") + has_grid_viewport = instance.data.get("dspGrid") + bkg_color_viewport = instance.data.get("dspBkg") + try: rt.viewport.setCamera(review_camera) - viewport_setting.VisualStyleMode = rt.Name( - visual_style) + rt.viewport.setGridVisibility(1, has_grid_viewport) rt.preferences.playPreviewWhenDone = False + rt.ViewportButtonMgr.EnableButtons = False + rt.viewport.EnableSolidBackgroundColorMode( + bkg_color_viewport) + viewport_setting.VisualStyleMode = rt.Name( + visualStyle) + viewport_setting.ViewportPreset = rt.Name( + viewportPreset) + viewport_setting.UseTextureEnabled = useTexture yield finally: rt.viewport.setCamera(original) - viewport_setting.VisualStyleMode = current_setting + rt.viewport.setGridVisibility(1, orig_vp_grid) + rt.viewport.EnableSolidBackgroundColorMode(orig_vp_bkg) + viewport_setting.VisualStyleMode = current_visualStyle + viewport_setting.ViewportPreset = current_visualPreset + viewport_setting.UseTextureEnabled = current_useTexture + rt.ViewportButtonMgr.EnableButtons = has_vp_btn rt.preferences.playPreviewWhenDone = has_autoplay @@ -532,9 +565,10 @@ def get_plugins() -> list: return plugin_info_list -def set_preview_arg(instance, filepath, - start, end, fps): +def publish_review_animation(instance, filepath, + start, end, fps): """Function to set up preview arguments in MaxScript. + ****For 3dsMax 2024+ Args: instance (str): instance @@ -561,31 +595,88 @@ def set_preview_arg(instance, filepath, enabled = instance.data.get(key) if enabled: job_args.append(f"{key}:{enabled}") - if get_max_version() >= 2024: - visual_style_preset = instance.data.get("visualStyleMode") - if visual_style_preset == "Realistic": - visual_style_preset = "defaultshading" - else: - visual_style_preset = visual_style_preset.lower() - # new argument exposed for Max 2024 for visual style - visual_style_option = f"vpStyle:#{visual_style_preset}" - job_args.append(visual_style_option) - # new argument for pre-view preset exposed in Max 2024 - preview_preset = instance.data.get("viewportPreset") - if preview_preset == "Quality": - preview_preset = "highquality" - elif preview_preset == "Customize": - preview_preset = "userdefined" - else: - preview_preset = preview_preset.lower() - preview_preset_option = f"vpPreset:#{visual_style_preset}" - job_args.append(preview_preset_option) - viewport_texture = instance.data.get("vpTexture", True) - if viewport_texture: - viewport_texture_option = f"vpTexture:{viewport_texture}" - job_args.append(viewport_texture_option) + + visual_style_preset = instance.data.get("visualStyleMode") + if visual_style_preset == "Realistic": + visual_style_preset = "defaultshading" + else: + visual_style_preset = visual_style_preset.lower() + # new argument exposed for Max 2024 for visual style + visual_style_option = f"vpStyle:#{visual_style_preset}" + job_args.append(visual_style_option) + # new argument for pre-view preset exposed in Max 2024 + preview_preset = instance.data.get("viewportPreset") + if preview_preset == "Quality": + preview_preset = "highquality" + elif preview_preset == "Customize": + preview_preset = "userdefined" + else: + preview_preset = preview_preset.lower() + preview_preset_option = f"vpPreset:#{preview_preset}" + job_args.append(preview_preset_option) + viewport_texture = instance.data.get("vpTexture", True) + if viewport_texture: + viewport_texture_option = f"vpTexture:{viewport_texture}" + job_args.append(viewport_texture_option) job_str = " ".join(job_args) log.debug(job_str) return job_str + +def publish_preview_sequences(staging_dir, filename, + startFrame, endFrame, ext): + """publish preview animation by creating bitmaps + ***For 3dsMax Version <2024 + + Args: + staging_dir (str): staging directory + filename (str): filename + startFrame (int): start frame + endFrame (int): end frame + ext (str): image extension + """ + # get the screenshot + rt.forceCompleteRedraw() + rt.enableSceneRedraw() + res_width = rt.renderWidth + res_height = rt.renderHeight + + viewportRatio = float(res_width / res_height) + + for i in range(startFrame, endFrame + 1): + rt.sliderTime = i + fname = "{}.{:04}.{}".format(filename, i, ext) + filepath = os.path.join(staging_dir, fname) + filepath = filepath.replace("\\", "/") + preview_res = rt.bitmap( + res_width, res_height, filename=filepath) + dib = rt.gw.getViewportDib() + dib_width = float(dib.width) + dib_height = float(dib.height) + renderRatio = float(dib_width / dib_height) + if viewportRatio <= renderRatio: + heightCrop = (dib_width / renderRatio) + topEdge = int((dib_height - heightCrop) / 2.0) + tempImage_bmp = rt.bitmap(dib_width, heightCrop) + src_box_value = rt.Box2(0, topEdge, dib_width, heightCrop) + else: + widthCrop = dib_height * renderRatio + leftEdge = int((dib_width - widthCrop) / 2.0) + tempImage_bmp = rt.bitmap(widthCrop, dib_height) + src_box_value = rt.Box2(0, leftEdge, dib_width, dib_height) + rt.pasteBitmap(dib, tempImage_bmp, src_box_value, rt.Point2(0, 0)) + + # copy the bitmap and close it + rt.copy(tempImage_bmp, preview_res) + rt.close(tempImage_bmp) + + rt.save(preview_res) + rt.close(preview_res) + + rt.close(dib) + + if rt.keyboard.escPressed: + rt.exit() + # clean up the cache + rt.gc() diff --git a/openpype/hosts/max/plugins/create/create_review.py b/openpype/hosts/max/plugins/create/create_review.py index e654783a33..ea56123c79 100644 --- a/openpype/hosts/max/plugins/create/create_review.py +++ b/openpype/hosts/max/plugins/create/create_review.py @@ -20,7 +20,8 @@ class CreateReview(plugin.MaxCreator): "keepImages", "percentSize", "visualStyleMode", - "viewportPreset"]: + "viewportPreset", + "vpTexture"]: if key in pre_create_data: creator_attributes[key] = pre_create_data[key] @@ -66,7 +67,10 @@ class CreateReview(plugin.MaxCreator): EnumDef("viewportPreset", preview_preset_enum, default="Quality", - label="Pre-View Preset") + label="Pre-View Preset"), + BoolDef("vpTexture", + label="Viewport Texture", + default=False) ] def get_pre_create_attr_defs(self): diff --git a/openpype/hosts/max/plugins/publish/collect_review.py b/openpype/hosts/max/plugins/publish/collect_review.py index 8b9a777c63..9ab1d6f3a8 100644 --- a/openpype/hosts/max/plugins/publish/collect_review.py +++ b/openpype/hosts/max/plugins/publish/collect_review.py @@ -34,6 +34,7 @@ class CollectReview(pyblish.api.InstancePlugin, "percentSize": creator_attrs["percentSize"], "visualStyleMode": creator_attrs["visualStyleMode"], "viewportPreset": creator_attrs["viewportPreset"], + "vpTexture": creator_attrs["vpTexture"], "frameStart": instance.context.data["frameStart"], "frameEnd": instance.context.data["frameEnd"], "fps": instance.context.data["fps"], @@ -59,7 +60,6 @@ class CollectReview(pyblish.api.InstancePlugin, instance.data["colorspaceConfig"] = colorspace_mgr.OCIOConfigPath instance.data["colorspaceDisplay"] = display instance.data["colorspaceView"] = view_transform - instance.data["vpTexture"] = attr_values.get("vpTexture") # Enable ftrack functionality instance.data.setdefault("families", []).append('ftrack') @@ -72,14 +72,7 @@ class CollectReview(pyblish.api.InstancePlugin, @classmethod def get_attribute_defs(cls): - additional_attrs = [] - if int(get_max_version()) >= 2024: - additional_attrs.append( - BoolDef("vpTexture", - label="Viewport Texture", - default=True), - ) - return additional_attrs + [ + return [ BoolDef("dspGeometry", label="Geometry", default=True), diff --git a/openpype/hosts/max/plugins/publish/extract_review_animation.py b/openpype/hosts/max/plugins/publish/extract_review_animation.py index da3f4155c1..a77f6213fa 100644 --- a/openpype/hosts/max/plugins/publish/extract_review_animation.py +++ b/openpype/hosts/max/plugins/publish/extract_review_animation.py @@ -6,7 +6,8 @@ from openpype.hosts.max.api.lib import ( viewport_setup_updated, viewport_setup, get_max_version, - set_preview_arg + publish_review_animation, + publish_preview_sequences ) @@ -37,22 +38,15 @@ class ExtractReviewAnimation(publish.Extractor): " '%s' to '%s'" % (filename, staging_dir)) review_camera = instance.data["review_camera"] - if int(get_max_version()) >= 2024: - with viewport_setup_updated(review_camera): - preview_arg = set_preview_arg( - instance, filepath, start, end, fps) - rt.execute(preview_arg) - else: - visual_style_preset = instance.data.get("visualStyleMode") + if int(get_max_version()) < 2024: nitrousGraphicMgr = rt.NitrousGraphicsManager viewport_setting = nitrousGraphicMgr.GetActiveViewportSetting() - with viewport_setup( - viewport_setting, - visual_style_preset, - review_camera): - viewport_setting.VisualStyleMode = rt.Name( - visual_style_preset) - preview_arg = set_preview_arg( + with viewport_setup(instance, viewport_setting, review_camera): + publish_preview_sequences( + staging_dir, instance.name, start, end, ext) + else: + with viewport_setup_updated(review_camera): + preview_arg = publish_review_animation( instance, filepath, start, end, fps) rt.execute(preview_arg) diff --git a/openpype/hosts/max/plugins/publish/validate_resolution_setting.py b/openpype/hosts/max/plugins/publish/validate_resolution_setting.py index 5ac41b10a0..969db0da2d 100644 --- a/openpype/hosts/max/plugins/publish/validate_resolution_setting.py +++ b/openpype/hosts/max/plugins/publish/validate_resolution_setting.py @@ -12,7 +12,7 @@ class ValidateResolutionSetting(pyblish.api.InstancePlugin, """Validate the resolution setting aligned with DB""" order = pyblish.api.ValidatorOrder - 0.01 - families = ["maxrender"] + families = ["maxrender", "review"] hosts = ["max"] label = "Validate Resolution Setting" optional = True From fcbe4616018c780102bd752f43a83956dcae6961 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 17 Oct 2023 18:34:50 +0800 Subject: [PATCH 0344/1233] hound fix for the last commit --- 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 6817160ce7..69dfd600a5 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -624,6 +624,7 @@ def publish_review_animation(instance, filepath, return job_str + def publish_preview_sequences(staging_dir, filename, startFrame, endFrame, ext): """publish preview animation by creating bitmaps From 7c035a157e801869ead6c56193f9aa34a323dfe8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 17 Oct 2023 12:52:46 +0200 Subject: [PATCH 0345/1233] fix args names --- openpype/modules/ftrack/plugins/publish/collect_ftrack_api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/collect_ftrack_api.py b/openpype/modules/ftrack/plugins/publish/collect_ftrack_api.py index c78abbd1d6..bea76718ca 100644 --- a/openpype/modules/ftrack/plugins/publish/collect_ftrack_api.py +++ b/openpype/modules/ftrack/plugins/publish/collect_ftrack_api.py @@ -119,8 +119,8 @@ class CollectFtrackApi(pyblish.api.ContextPlugin): def per_instance_process( self, context, - asset_entity, - task_entity, + context_asset_entity, + context_task_entity, task_object_type_id ): context_task_name = None From ab2241aebb62de3489c13458f2d118b5e49e9886 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 17 Oct 2023 19:11:23 +0800 Subject: [PATCH 0346/1233] fix the viewport setting issue when the first frame is flickering with different setups --- openpype/hosts/max/api/lib.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 69dfd600a5..736b0fb544 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -385,11 +385,14 @@ def viewport_setup(instance, viewport_setting, camera): rt.ViewportButtonMgr.EnableButtons = False rt.viewport.EnableSolidBackgroundColorMode( bkg_color_viewport) - viewport_setting.VisualStyleMode = rt.Name( - visualStyle) - viewport_setting.ViewportPreset = rt.Name( - viewportPreset) - viewport_setting.UseTextureEnabled = useTexture + if visualStyle != current_visualStyle: + viewport_setting.VisualStyleMode = rt.Name( + visualStyle) + elif viewportPreset != current_visualPreset: + viewport_setting.ViewportPreset = rt.Name( + viewportPreset) + elif useTexture != current_useTexture: + viewport_setting.UseTextureEnabled = useTexture yield finally: rt.viewport.setCamera(original) @@ -402,6 +405,7 @@ def viewport_setup(instance, viewport_setting, camera): rt.preferences.playPreviewWhenDone = has_autoplay + def set_timeline(frameStart, frameEnd): """Set frame range for timeline editor in Max """ From 27106bcc724b8db6bdf739d33d9fd9700dfc2f22 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 17 Oct 2023 14:12:57 +0200 Subject: [PATCH 0347/1233] moved show and publish logic to publisher window --- openpype/hosts/houdini/api/lib.py | 17 +++++-------- openpype/tools/publisher/window.py | 41 +++++++++++++++++++++++------- 2 files changed, 39 insertions(+), 19 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 6fa8b02735..8863570966 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -856,22 +856,19 @@ def update_houdini_vars_context_dialog(): dialog.show() -def publisher_show_and_publish(comment=""): - """Open publisher window and trigger publishing action.""" +def publisher_show_and_publish(comment=None): + """Open publisher window and trigger publishing action. + + Args: + comment (Optional[str]): Comment to set in publisher window. + """ main_window = get_main_window() publisher_window = get_tool_by_name( tool_name="publisher", parent=main_window, - reset_on_show=False ) - - publisher_window.set_current_tab("publish") - publisher_window.make_sure_is_visible() - publisher_window.reset_on_show = False - publisher_window.set_comment_input_text(comment) - publisher_window.reset() - publisher_window.click_publish() + publisher_window.show_and_pubish(comment) def find_rop_input_dependencies(input_tuple): diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index 9214c0a43f..af6d7371b1 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -388,20 +388,43 @@ class PublisherWindow(QtWidgets.QDialog): def controller(self): return self._controller - @property - def reset_on_show(self): - return self._reset_on_show + def show_and_publish(self, comment=None): + """Show the window and start publishing. - @reset_on_show.setter - def reset_on_show(self, value): - self._reset_on_show = value + The method will reset controller and start the publishing afterwards. - def set_comment_input_text(self, text=""): - self._comment_input.setText(text) + Todos: + Move validations from '_on_publish_clicked' and change of + 'comment' value in controller to controller so it can be + simplified. - def click_publish(self): + Args: + comment (Optional[str]): Comment to be set to publish. + If is set to 'None' a comment is not changed at all. + """ + + if comment is not None: + self.set_comment(comment) + self._reset_on_show = False + self.make_sure_is_visible() + # Reset controller + self._controller.reset() + # Fake publish click to trigger save validation and propagate + # comment to controller self._on_publish_clicked() + def set_comment(self, comment): + """Change comment text. + + Todos: + Be able to set the comment via controller. + + Args: + comment (str): Comment text. + """ + + self._comment_input.setText(comment) + def make_sure_is_visible(self): if self._window_is_visible: self.setWindowState(QtCore.Qt.WindowActive) From 0f9c30378ec49babc463b20e46bec07dca56020b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 17 Oct 2023 14:26:14 +0200 Subject: [PATCH 0348/1233] revert 'get_publisher_tool' and 'show_publisher_tool' arguments --- openpype/tools/utils/host_tools.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/openpype/tools/utils/host_tools.py b/openpype/tools/utils/host_tools.py index 5f538fe45b..cc20774349 100644 --- a/openpype/tools/utils/host_tools.py +++ b/openpype/tools/utils/host_tools.py @@ -286,9 +286,7 @@ class HostToolsHelper: dialog.activateWindow() dialog.showNormal() - def get_publisher_tool( - self, parent=None, controller=None, reset_on_show=None - ): + def get_publisher_tool(self, parent=None, controller=None): """Create, cache and return publisher window.""" if self._publisher_tool is None: @@ -299,18 +297,15 @@ class HostToolsHelper: publisher_window = PublisherWindow( controller=controller, - parent=parent or self._parent, - reset_on_show=reset_on_show + parent=parent or self._parent ) self._publisher_tool = publisher_window return self._publisher_tool - def show_publisher_tool( - self, parent=None, controller=None, reset_on_show=None, tab=None - ): + def show_publisher_tool(self, parent=None, controller=None, tab=None): with qt_app_context(): - window = self.get_publisher_tool(parent, controller, reset_on_show) + window = self.get_publisher_tool(parent, controller) if tab: window.set_current_tab(tab) window.make_sure_is_visible() From e6ac57b35fa4ea51bfcde3e844660e60b95cc97f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Tue, 17 Oct 2023 15:11:45 +0200 Subject: [PATCH 0349/1233] Update openpype/hosts/houdini/api/lib.py 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 8863570966..e4b9d70d57 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -868,7 +868,7 @@ def publisher_show_and_publish(comment=None): tool_name="publisher", parent=main_window, ) - publisher_window.show_and_pubish(comment) + publisher_window.show_and_publish(comment) def find_rop_input_dependencies(input_tuple): From b1ea0b099dd13e13515d3a4f32b2830702496488 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 17 Oct 2023 15:32:42 +0200 Subject: [PATCH 0350/1233] merging if conditions for handles exclusion also updating docstring --- openpype/hosts/resolve/api/lib.py | 5 +++-- openpype/hosts/resolve/api/plugin.py | 9 ++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index 22be929412..2d91609679 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -258,9 +258,10 @@ def create_timeline_item( Args: media_pool_item (resolve.MediaPoolItem): resolve's object + source_start (int): media source input frame (sequence frame) + source_end (int): media source output frame (sequence frame) + timeline_in (int): timeline input frame (sequence frame) timeline (resolve.Timeline)[optional]: resolve's object - source_start (int)[optional]: media source input frame (sequence frame) - source_end (int)[optional]: media source output frame (sequence frame) Returns: object: resolve.TimelineItem diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py index 1c817d8e0d..314d066890 100644 --- a/openpype/hosts/resolve/api/plugin.py +++ b/openpype/hosts/resolve/api/plugin.py @@ -418,12 +418,11 @@ class ClipLoader: source_in = int(_clip_property("Start")) source_out = int(_clip_property("End")) - if _clip_property("Type") == "Video": - source_in += handle_start - source_out -= handle_end - # include handles - if not self.with_handles: + if ( + not self.with_handles + or _clip_property("Type") == "Video" + ): source_in += handle_start source_out -= handle_end From b66a679ee2517849f173718eede45cf62585c4e1 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 17 Oct 2023 16:19:34 +0200 Subject: [PATCH 0351/1233] adding docstring for maintained selection --- openpype/hosts/nuke/api/lib.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 0a5f772346..bb8fbd01c4 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -2800,6 +2800,13 @@ def find_free_space_to_paste_nodes( def maintained_selection(exclude_nodes=None): """Maintain selection during context + Maintain selection during context and unselect + all nodes after context is done. + + Arguments: + exclude_nodes (list[nuke.Node]): list of nodes to be unselected + before context is done + Example: >>> with maintained_selection(): ... node["selected"].setValue(True) From 13159c48890e24734da1390edd87d578aa98f640 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Tue, 17 Oct 2023 17:29:27 +0300 Subject: [PATCH 0352/1233] Jakub comments --- openpype/hosts/houdini/api/lib.py | 8 ++------ openpype/hosts/houdini/api/plugin.py | 5 +++-- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index e4b9d70d57..f258dda36e 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -924,11 +924,7 @@ def self_publish(): for instance in context.instances: node_path = instance.data.get("instance_node") - if not node_path: - continue - - active = node_path in inputs_paths - instance["active"] = active + instance["active"] = node_path and node_path in inputs_paths context.save_changes() @@ -941,7 +937,7 @@ def add_self_publish_button(node): label = os.environ.get("AVALON_LABEL") or "OpenPype" button_parm = hou.ButtonParmTemplate( - "{}_publish".format(label.lower()), + "ayon_self_publish", "{} Publish".format(label), script_callback="from openpype.hosts.houdini.api.lib import " "self_publish; self_publish()", diff --git a/openpype/hosts/houdini/api/plugin.py b/openpype/hosts/houdini/api/plugin.py index 5102b64644..d79ccc71bd 100644 --- a/openpype/hosts/houdini/api/plugin.py +++ b/openpype/hosts/houdini/api/plugin.py @@ -325,8 +325,9 @@ class HoudiniCreator(NewCreator, HoudiniCreatorBase): """Method called on initialization of plugin to apply settings.""" # Apply General Settings - self.add_publish_button = \ - project_settings["houdini"]["general"]["add_self_publish_button"] + houdini_general_settings = project_settings["houdini"]["general"] + self.add_publish_button = houdini_general_settings.get( + "add_self_publish_button", False) # Apply Creator Settings settings_name = self.settings_name From f214751be375016d4464b1faf4e427c1851f36a4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 17 Oct 2023 16:29:55 +0200 Subject: [PATCH 0353/1233] modules can be loaded in dev mode correctly --- openpype/modules/base.py | 87 ++++++++++++++++++++++++++++------------ 1 file changed, 61 insertions(+), 26 deletions(-) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index a3c21718b9..e5741728d9 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -37,7 +37,6 @@ from openpype.lib import ( import_filepath, import_module_from_dirpath, ) -from openpype.lib.openpype_version import is_staging_enabled from .interfaces import ( OpenPypeInterface, @@ -317,21 +316,10 @@ def load_modules(force=False): time.sleep(0.1) -def _get_ayon_addons_information(): - """Receive information about addons to use from server. - - Todos: - Actually ask server for the information. - Allow project name as optional argument to be able to query information - about used addons for specific project. - Returns: - List[Dict[str, Any]]: List of addon information to use. - """ - - output = [] +def _get_ayon_bundle_data(): bundle_name = os.getenv("AYON_BUNDLE_NAME") bundles = ayon_api.get_bundles()["bundles"] - final_bundle = next( + return next( ( bundle for bundle in bundles @@ -339,10 +327,32 @@ def _get_ayon_addons_information(): ), None ) - if final_bundle is None: - return output - bundle_addons = final_bundle["addons"] + +def _is_dev_mode_enabled(): + """Dev mode is enabled in AYON. + + Returns: + bool: True if dev mode is enabled. + """ + + return os.getenv("AYON_DEV_MODE") == "1" + + +def _get_ayon_addons_information(bundle_info): + """Receive information about addons to use from server. + + Todos: + Actually ask server for the information. + Allow project name as optional argument to be able to query information + about used addons for specific project. + + Returns: + List[Dict[str, Any]]: List of addon information to use. + """ + + output = [] + bundle_addons = bundle_info["addons"] addons = ayon_api.get_addons_info()["addons"] for addon in addons: name = addon["name"] @@ -378,31 +388,56 @@ def _load_ayon_addons(openpype_modules, modules_key, log): v3_addons_to_skip = [] - addons_info = _get_ayon_addons_information() + bundle_info = _get_ayon_bundle_data() + addons_info = _get_ayon_addons_information(bundle_info) if not addons_info: return v3_addons_to_skip + 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): + + dev_mode_enabled = _is_dev_mode_enabled() + dev_addons_info = {} + if dev_mode_enabled: + # Get dev addons info only when dev mode is enabled + dev_addons_info = bundle_info.get("addonDevelopment", dev_addons_info) + + addons_dir_exists = os.path.exists(addons_dir) + if not addons_dir_exists: log.warning("Addons directory does not exists. Path \"{}\"".format( addons_dir )) - return v3_addons_to_skip for addon_info in addons_info: addon_name = addon_info["name"] addon_version = addon_info["version"] - folder_name = "{}_{}".format(addon_name, addon_version) - addon_dir = os.path.join(addons_dir, folder_name) - if not os.path.exists(addon_dir): - log.debug(( - "No localized client code found for addon {} {}." - ).format(addon_name, addon_version)) + dev_addon_info = dev_addons_info.get(addon_name, {}) + use_dev_path = dev_addon_info.get("enabled", False) + + addon_dir = None + if use_dev_path: + addon_dir = dev_addon_info["path"] + if not addon_dir or not os.path.exists(addon_dir): + log.warning(( + "Dev addon {} {} path does not exists. Path \"{}\"" + ).format(addon_name, addon_version, addon_dir)) + continue + + elif addons_dir_exists: + folder_name = "{}_{}".format(addon_name, addon_version) + addon_dir = os.path.join(addons_dir, folder_name) + if not os.path.exists(addon_dir): + log.debug(( + "No localized client code found for addon {} {}." + ).format(addon_name, addon_version)) + continue + + if not addon_dir: continue sys.path.insert(0, addon_dir) From 0b02a97e5fabbf34136c6ebb5957c4930aea971c Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Tue, 17 Oct 2023 17:40:45 +0300 Subject: [PATCH 0354/1233] bump houdini addon version --- server_addon/houdini/server/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/houdini/server/version.py b/server_addon/houdini/server/version.py index bbab0242f6..1276d0254f 100644 --- a/server_addon/houdini/server/version.py +++ b/server_addon/houdini/server/version.py @@ -1 +1 @@ -__version__ = "0.1.4" +__version__ = "0.1.5" From 74acdd63eee751eecfe2d92933d3d3752d89b4ce Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 17 Oct 2023 16:41:27 +0200 Subject: [PATCH 0355/1233] change env variable key --- openpype/modules/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index e5741728d9..355fee0e0a 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -336,7 +336,7 @@ def _is_dev_mode_enabled(): bool: True if dev mode is enabled. """ - return os.getenv("AYON_DEV_MODE") == "1" + return os.getenv("AYON_USE_DEV") == "1" def _get_ayon_addons_information(bundle_info): From a32e3996956b78db172f3a08989650fdeff9e58d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 17 Oct 2023 17:06:21 +0200 Subject: [PATCH 0356/1233] use dev variant in dev mode --- openpype/settings/ayon_settings.py | 40 +++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index 3ccb18111a..eb64480dc3 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -290,6 +290,16 @@ def _convert_modules_system( modules_settings[key] = value +def is_dev_mode_enabled(): + """Dev mode is enabled in AYON. + + Returns: + bool: True if dev mode is enabled. + """ + + return os.getenv("AYON_USE_DEV") == "1" + + def convert_system_settings(ayon_settings, default_settings, addon_versions): default_settings = copy.deepcopy(default_settings) output = { @@ -1400,15 +1410,39 @@ class _AyonSettingsCache: if _AyonSettingsCache.variant is None: from openpype.lib.openpype_version import is_staging_enabled - _AyonSettingsCache.variant = ( - "staging" if is_staging_enabled() else "production" - ) + variant = "production" + if is_dev_mode_enabled(): + variant = cls._get_dev_mode_settings_variant() + elif is_staging_enabled(): + variant = "staging" + _AyonSettingsCache.variant = variant return _AyonSettingsCache.variant @classmethod def _get_bundle_name(cls): return os.environ["AYON_BUNDLE_NAME"] + @classmethod + def _get_dev_mode_settings_variant(cls): + """Develop mode settings variant. + + Returns: + str: Name of settings variant. + """ + + bundles = ayon_api.get_bundles() + user = ayon_api.get_user() + username = user["name"] + for bundle in bundles: + if ( + bundle.get("isDev") + and bundle.get("activeUser") == username + ): + return bundle["name"] + # Return fake variant - distribution logic will tell user that he does not + # have set any dev bundle + return "dev" + @classmethod def get_value_by_project(cls, project_name): cache_item = _AyonSettingsCache.cache_by_project_name[project_name] From 2088c7d7e6109b589724a13905547cbb1d6aa28f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 17 Oct 2023 17:06:38 +0200 Subject: [PATCH 0357/1233] use 'is_dev_mode_enabled' from 'ayon_'settings' --- openpype/modules/base.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 355fee0e0a..6f3e4566f3 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -31,6 +31,7 @@ from openpype.settings.lib import ( get_studio_system_settings_overrides, load_json_file ) +from openpype.settings.ayon_settings import is_dev_mode_enabled from openpype.lib import ( Logger, @@ -329,16 +330,6 @@ def _get_ayon_bundle_data(): ) -def _is_dev_mode_enabled(): - """Dev mode is enabled in AYON. - - Returns: - bool: True if dev mode is enabled. - """ - - return os.getenv("AYON_USE_DEV") == "1" - - def _get_ayon_addons_information(bundle_info): """Receive information about addons to use from server. @@ -400,7 +391,7 @@ def _load_ayon_addons(openpype_modules, modules_key, log): "addons" ) - dev_mode_enabled = _is_dev_mode_enabled() + dev_mode_enabled = is_dev_mode_enabled() dev_addons_info = {} if dev_mode_enabled: # Get dev addons info only when dev mode is enabled From 41136557250175dcb829e62b12420b9316f7e2ae Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 17 Oct 2023 17:19:58 +0200 Subject: [PATCH 0358/1233] updating pr changes to latest changes --- openpype/lib/transcoding.py | 17 +++- openpype/plugins/publish/extract_thumbnail.py | 77 ++++++++++--------- 2 files changed, 54 insertions(+), 40 deletions(-) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index 1b75b96525..7cd3671dd1 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -1149,9 +1149,9 @@ def convert_colorspace( target_colorspace=None, view=None, display=None, + additional_input_args=None, additional_command_args=None, logger=None, - input_args=None ): """Convert source file from one color space to another. @@ -1170,6 +1170,8 @@ def convert_colorspace( view (str): name for viewer space (ocio valid) both 'view' and 'display' must be filled (if 'target_colorspace') display (str): name for display-referred reference space (ocio valid) + both 'view' and 'display' must be filled (if 'target_colorspace') + additional_input_args (list): input arguments for oiiotool additional_command_args (list): arguments for oiiotool (like binary depth for .dpx) logger (logging.Logger): Logger used for logging. @@ -1179,12 +1181,21 @@ def convert_colorspace( if logger is None: logger = logging.getLogger(__name__) - oiio_cmd = get_oiio_tool_args( - "oiiotool", + # prepare main oiio command args + args = [ input_path, # Don't add any additional attributes "--nosoftwareattrib", "--colorconfig", config_path + ] + + # prepand any additional args if available + if additional_input_args: + args = additional_input_args + args + + oiio_cmd = get_oiio_tool_args( + "oiiotool", + *args ) if all([target_colorspace, view, display]): diff --git a/openpype/plugins/publish/extract_thumbnail.py b/openpype/plugins/publish/extract_thumbnail.py index ff416ecb23..5b75a374ba 100644 --- a/openpype/plugins/publish/extract_thumbnail.py +++ b/openpype/plugins/publish/extract_thumbnail.py @@ -5,7 +5,6 @@ import tempfile import pyblish.api from openpype.lib import ( get_ffmpeg_tool_args, - get_oiio_tool_args, is_oiio_supported, run_subprocess, @@ -26,7 +25,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): hosts = ["shell", "fusion", "resolve", "traypublisher", "substancepainter"] enabled = False - # presetable attribute + # presentable attribute ffmpeg_args = None def process(self, instance): @@ -95,27 +94,26 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): filename = os.path.splitext(input_file)[0] jpeg_file = filename + "_thumb.jpg" full_output_path = os.path.join(dst_staging, jpeg_file) + colorspace_data = repre.get("colorspaceData") - if oiio_supported: - self.log.debug("Trying to convert with OIIO") + # only use OIIO if it is supported and representation has + # colorspace data + if oiio_supported and colorspace_data: + self.log.debug( + "Trying to convert with OIIO " + "with colorspace data: {}".format(colorspace_data) + ) # If the input can read by OIIO then use OIIO method for # conversion otherwise use ffmpeg - colorspace_data = repre["colorspaceData"] - source_colorspace = colorspace_data["colorspace"] - config_path = colorspace_data.get("config", {}).get("path") - display = colorspace_data["display"] - view = colorspace_data["view"] thumbnail_created = self.create_thumbnail_oiio( full_input_path, full_output_path, - config_path, - source_colorspace, - display, - view + colorspace_data ) # Try to use FFMPEG if OIIO is not supported or for cases when - # oiiotool isn't available + # oiiotool isn't available or representation is not having + # colorspace data if not thumbnail_created: if oiio_supported: self.log.debug( @@ -149,7 +147,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): break if not thumbnail_created: - self.log.warning("Thumbanil has not been created.") + self.log.warning("Thumbnail has not been created.") def _is_review_instance(self, instance): # TODO: We should probably handle "not creating" of thumbnail @@ -188,21 +186,36 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): self, src_path, dst_path, - config_path, - source_colorspace, - display, - view + colorspace_data, ): + """Create thumbnail using OIIO tool oiiotool + + Args: + src_path (str): path to source file + dst_path (str): path to destination file + colorspace_data (dict): colorspace data from representation + keys: + colorspace (str) + config (dict) + display (Optional[str]) + view (Optional[str]) + + Returns: + str: path to created thumbnail + """ self.log.info("Extracting thumbnail {}".format(dst_path)) - oiio_cmd = get_oiio_tool_args( - "oiiotool", - "-a", src_path, - "-o", dst_path - ) - self.log.debug("running: {}".format(" ".join(oiio_cmd))) + try: - run_subprocess(oiio_cmd, logger=self.log) - return True + convert_colorspace( + src_path, + dst_path, + colorspace_data["config"]["path"], + colorspace_data["colorspace"], + display=colorspace_data.get("display"), + view=colorspace_data.get("view"), + additional_input_args=["-i:ch=R,G,B"], + logger=self.log, + ) except Exception: self.log.warning( "Failed to create thumbnail using oiiotool", @@ -210,16 +223,6 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): ) return False - convert_colorspace( - src_path, - dst_path, - config_path, - source_colorspace, - view=view, - display=display, - input_args=["-i:ch=R,G,B"] - ) - return dst_path def create_thumbnail_ffmpeg(self, src_path, dst_path): From fc900780cd97fc15e4c91d24340dba02dd015aed Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 17 Oct 2023 17:33:38 +0200 Subject: [PATCH 0359/1233] typo --- 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 7cd3671dd1..07acc309d2 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -1189,7 +1189,7 @@ def convert_colorspace( "--colorconfig", config_path ] - # prepand any additional args if available + # prepend any additional args if available if additional_input_args: args = additional_input_args + args From 9e61d7e371d54a500cc1c67dd3c560d8cfcf98c1 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 17 Oct 2023 17:39:10 +0200 Subject: [PATCH 0360/1233] removing debug printing --- openpype/hosts/resolve/api/lib.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index ca79dd6e87..dfe4608a46 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -292,10 +292,6 @@ def create_timeline_item( "recordFrame": timeline_in, } - print("clip_data", "_" * 50) - print(media_pool_item.GetName()) - print(clip_data) - # add to timeline media_pool.AppendToTimeline([clip_data]) @@ -511,7 +507,7 @@ def imprint(timeline_item, data=None): Arguments: timeline_item (hiero.core.TrackItem): hiero track item object - data (dict): Any data which needst to be imprinted + data (dict): Any data which needs to be imprinted Examples: data = { From 3bbc3d0489db4ea0d785416e6459c79fee34701e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Tue, 17 Oct 2023 17:42:46 +0200 Subject: [PATCH 0361/1233] Update openpype/hosts/resolve/api/lib.py Co-authored-by: Roy Nieterau --- openpype/hosts/resolve/api/lib.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index dfe4608a46..20636f299b 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -251,10 +251,10 @@ def get_media_pool_item(filepath, root: object = None) -> object: def create_timeline_item( media_pool_item: object, - source_start: int, - source_end: int, - timeline_in: int, - timeline: object = None + timeline: object = None, + timeline_in: int = None, + source_start: int = None, + source_end: int = None, ) -> object: """ Add media pool item to current or defined timeline. From 435408dc142cb934424333257f8092db37974b01 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 17 Oct 2023 17:47:23 +0200 Subject: [PATCH 0362/1233] create_timeline_item with backward comapatibility --- openpype/hosts/resolve/api/lib.py | 30 ++++++++++++++++++---------- openpype/hosts/resolve/api/plugin.py | 4 ++-- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index 20636f299b..6f84d921e0 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -261,10 +261,10 @@ def create_timeline_item( Args: media_pool_item (resolve.MediaPoolItem): resolve's object - source_start (int): media source input frame (sequence frame) - source_end (int): media source output frame (sequence frame) - timeline_in (int): timeline input frame (sequence frame) - timeline (resolve.Timeline)[optional]: resolve's object + timeline (Optional[resolve.Timeline]): resolve's object + timeline_in (Optional[int]): timeline input frame (sequence frame) + source_start (Optional[int]): media source input frame (sequence frame) + source_end (Optional[int]): media source output frame (sequence frame) Returns: object: resolve.TimelineItem @@ -277,21 +277,29 @@ def create_timeline_item( timeline = timeline or get_current_timeline() # timing variables - fps = project.GetSetting("timelineFrameRate") - duration = source_end - source_start - timecode_in = frames_to_timecode(timeline_in, fps) - timecode_out = frames_to_timecode(timeline_in + duration, fps) + if all([timeline_in, source_start, source_end]): + fps = project.GetSetting("timelineFrameRate") + duration = source_end - source_start + timecode_in = frames_to_timecode(timeline_in, fps) + timecode_out = frames_to_timecode(timeline_in + duration, fps) + else: + timecode_in = None + timecode_out = None # if timeline was used then switch it to current timeline with maintain_current_timeline(timeline): # Add input mediaPoolItem to clip data clip_data = { "mediaPoolItem": media_pool_item, - "startFrame": source_start, - "endFrame": source_end, - "recordFrame": timeline_in, } + if source_start: + clip_data["startFrame"] = source_start + if source_end: + clip_data["endFrame"] = source_end + if timecode_in: + clip_data["recordFrame"] = timecode_in + # add to timeline media_pool.AppendToTimeline([clip_data]) diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py index 95f2fb2281..c88ed762ab 100644 --- a/openpype/hosts/resolve/api/plugin.py +++ b/openpype/hosts/resolve/api/plugin.py @@ -434,10 +434,10 @@ class ClipLoader: # make track item from source in bin as item timeline_item = lib.create_timeline_item( media_pool_item, + self.active_timeline, + timeline_in, source_in, source_out, - timeline_in, - self.active_timeline, ) print("Loading clips: `{}`".format(self.data["clip_name"])) From af9dbe5af2688c018ebdae9602beb0b3c0b89888 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 18 Oct 2023 03:25:10 +0000 Subject: [PATCH 0363/1233] [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 f98d4c1cf5..6f740d0c78 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.17.3-nightly.1" +__version__ = "3.17.3-nightly.2" From 1be774230b05d895d595a96993e577646c1d1207 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 18 Oct 2023 03:25:51 +0000 Subject: [PATCH 0364/1233] 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 dba39ac36d..2849a4951a 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.17.3-nightly.2 - 3.17.3-nightly.1 - 3.17.2 - 3.17.2-nightly.4 @@ -134,7 +135,6 @@ body: - 3.14.11-nightly.4 - 3.14.11-nightly.3 - 3.14.11-nightly.2 - - 3.14.11-nightly.1 validations: required: true - type: dropdown From 3461cbed58efeb3c901e66b7c677002543021f03 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 18 Oct 2023 12:17:58 +0800 Subject: [PATCH 0365/1233] use originalbasename entirely for the publishing tycache --- .../hosts/max/plugins/publish/collect_tycache_attributes.py | 2 +- openpype/settings/defaults/project_anatomy/templates.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/collect_tycache_attributes.py b/openpype/hosts/max/plugins/publish/collect_tycache_attributes.py index 56cf6614e2..fa27a9a9d6 100644 --- a/openpype/hosts/max/plugins/publish/collect_tycache_attributes.py +++ b/openpype/hosts/max/plugins/publish/collect_tycache_attributes.py @@ -60,7 +60,7 @@ class CollectTyCacheData(pyblish.api.InstancePlugin, "tycacheChanRot", "tycacheChanScale", "tycacheChanVel", "tycacheChanShape", "tycacheChanMatID", "tycacheChanMapping", - "tycacheChanMaterials", "tycacheCreateObject", + "tycacheChanMaterials", "tycacheCreateObjectIfNotCreated"] return [ EnumDef("all_tyc_attrs", diff --git a/openpype/settings/defaults/project_anatomy/templates.json b/openpype/settings/defaults/project_anatomy/templates.json index 5694693c97..5766a09100 100644 --- a/openpype/settings/defaults/project_anatomy/templates.json +++ b/openpype/settings/defaults/project_anatomy/templates.json @@ -55,7 +55,7 @@ }, "tycache": { "folder": "{root[work]}/{project[name]}/{hierarchy}/{asset}/publish/{family}/{subset}/{@version}", - "file": "{originalBasename}<_{@frame}>.{ext}", + "file": "{originalBasename}.{ext}", "path": "{@folder}/{@file}" }, "source": { From 863ed821cad4a8544e6d37c272b70acdf852683d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 18 Oct 2023 10:04:45 +0200 Subject: [PATCH 0366/1233] change '_reset_on_first_show' to 'False' on show and publish --- openpype/tools/publisher/window.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index af6d7371b1..312cf1dd5c 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -403,9 +403,11 @@ class PublisherWindow(QtWidgets.QDialog): If is set to 'None' a comment is not changed at all. """ + self._reset_on_show = False + self._reset_on_first_show = False + if comment is not None: self.set_comment(comment) - self._reset_on_show = False self.make_sure_is_visible() # Reset controller self._controller.reset() From 68f7826cf610b8160cb8ce21bc764f1964eb4559 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 18 Oct 2023 10:16:17 +0200 Subject: [PATCH 0367/1233] updated 'ayon_api' to '0.5.1' --- .../vendor/python/common/ayon_api/_api.py | 14 +- .../python/common/ayon_api/graphql_queries.py | 5 + .../python/common/ayon_api/server_api.py | 177 ++++++++++++------ .../vendor/python/common/ayon_api/utils.py | 83 ++++++-- .../vendor/python/common/ayon_api/version.py | 2 +- 5 files changed, 194 insertions(+), 87 deletions(-) diff --git a/openpype/vendor/python/common/ayon_api/_api.py b/openpype/vendor/python/common/ayon_api/_api.py index 22e137d6e5..9f89d3d59e 100644 --- a/openpype/vendor/python/common/ayon_api/_api.py +++ b/openpype/vendor/python/common/ayon_api/_api.py @@ -602,12 +602,12 @@ def delete_installer(*args, **kwargs): def download_installer(*args, **kwargs): con = get_server_api_connection() - con.download_installer(*args, **kwargs) + return con.download_installer(*args, **kwargs) def upload_installer(*args, **kwargs): con = get_server_api_connection() - con.upload_installer(*args, **kwargs) + return con.upload_installer(*args, **kwargs) # Dependency packages @@ -753,12 +753,12 @@ def get_secrets(*args, **kwargs): def get_secret(*args, **kwargs): con = get_server_api_connection() - return con.delete_secret(*args, **kwargs) + return con.get_secret(*args, **kwargs) def save_secret(*args, **kwargs): con = get_server_api_connection() - return con.delete_secret(*args, **kwargs) + return con.save_secret(*args, **kwargs) def delete_secret(*args, **kwargs): @@ -978,12 +978,14 @@ def 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) + return 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) + return con.get_thumbnail( + project_name, entity_type, entity_id, thumbnail_id + ) def get_folder_thumbnail(project_name, folder_id, thumbnail_id=None): diff --git a/openpype/vendor/python/common/ayon_api/graphql_queries.py b/openpype/vendor/python/common/ayon_api/graphql_queries.py index 2435fc8a17..cedb3ed2ac 100644 --- a/openpype/vendor/python/common/ayon_api/graphql_queries.py +++ b/openpype/vendor/python/common/ayon_api/graphql_queries.py @@ -144,6 +144,7 @@ def product_types_query(fields): query_queue.append((k, v, field)) return query + def project_product_types_query(fields): query = GraphQlQuery("ProjectProductTypes") project_query = query.add_field("project") @@ -175,6 +176,8 @@ def folders_graphql_query(fields): parent_folder_ids_var = query.add_variable("parentFolderIds", "[String!]") folder_paths_var = query.add_variable("folderPaths", "[String!]") folder_names_var = query.add_variable("folderNames", "[String!]") + folder_types_var = query.add_variable("folderTypes", "[String!]") + statuses_var = query.add_variable("folderStatuses", "[String!]") has_products_var = query.add_variable("folderHasProducts", "Boolean!") project_field = query.add_field("project") @@ -185,6 +188,8 @@ def folders_graphql_query(fields): folders_field.set_filter("parentIds", parent_folder_ids_var) folders_field.set_filter("names", folder_names_var) folders_field.set_filter("paths", folder_paths_var) + folders_field.set_filter("folderTypes", folder_types_var) + folders_field.set_filter("statuses", statuses_var) folders_field.set_filter("hasProducts", has_products_var) nested_fields = fields_to_dict(fields) diff --git a/openpype/vendor/python/common/ayon_api/server_api.py b/openpype/vendor/python/common/ayon_api/server_api.py index 511a239a83..3bac59c192 100644 --- a/openpype/vendor/python/common/ayon_api/server_api.py +++ b/openpype/vendor/python/common/ayon_api/server_api.py @@ -75,6 +75,7 @@ from .utils import ( TransferProgress, create_dependency_package_basename, ThumbnailContent, + get_default_timeout, ) PatternType = type(re.compile("")) @@ -351,7 +352,6 @@ class ServerAPI(object): 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__( @@ -500,20 +500,13 @@ class ServerAPI(object): 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'. + Utils function 'get_default_timeout' is used by default. Returns: float: Timeout value in seconds. """ - try: - return float(os.environ.get(SERVER_TIMEOUT_ENV_KEY)) - except (ValueError, TypeError): - pass - - return cls._default_timeout + return get_default_timeout() @classmethod def get_default_max_retries(cls): @@ -662,13 +655,10 @@ class ServerAPI(object): as default variant. Args: - variant (Literal['production', 'staging']): Settings variant name. + variant (str): Settings variant name. It is possible to use + 'production', 'staging' or name of dev bundle. """ - if variant not in ("production", "staging"): - raise ValueError(( - "Invalid variant name {}. Expected 'production' or 'staging'" - ).format(variant)) self._default_settings_variant = variant default_settings_variant = property( @@ -938,8 +928,8 @@ class ServerAPI(object): int(re_match.group("major")), int(re_match.group("minor")), int(re_match.group("patch")), - re_match.group("prerelease"), - re_match.group("buildmetadata") + re_match.group("prerelease") or "", + re_match.group("buildmetadata") or "", ) return self._server_version_tuple @@ -1140,31 +1130,41 @@ class ServerAPI(object): response = None new_response = None - for _ in range(max_retries): + for retry_idx in reversed(range(max_retries)): try: response = function(url, **kwargs) break except ConnectionRefusedError: + if retry_idx == 0: + self.log.warning( + "Connection error happened.", exc_info=True + ) + # 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 + # Log warning only on last attempt + if retry_idx == 0: + self.log.warning( + "Connection error happened.", exc_info=True + ) + new_response = RestApiResponse( None, {"detail": "Unable to connect the server. Connection error"} ) - break time.sleep(0.1) @@ -1349,7 +1349,9 @@ class ServerAPI(object): status=None, description=None, summary=None, - payload=None + payload=None, + progress=None, + retries=None ): kwargs = { key: value @@ -1360,9 +1362,27 @@ class ServerAPI(object): ("description", description), ("summary", summary), ("payload", payload), + ("progress", progress), + ("retries", retries), ) if value is not None } + # 'progress' and 'retries' are available since 0.5.x server version + major, minor, _, _, _ = self.server_version_tuple + if (major, minor) < (0, 5): + args = [] + if progress is not None: + args.append("progress") + if retries is not None: + args.append("retries") + fields = ", ".join("'{}'".format(f) for f in args) + ending = "s" if len(args) > 1 else "" + raise ValueError(( + "Your server version '{}' does not support update" + " of {} field{} on event. The fields are supported since" + " server version '0.5'." + ).format(self.get_server_version(), fields, ending)) + response = self.patch( "events/{}".format(event_id), **kwargs @@ -1434,6 +1454,7 @@ class ServerAPI(object): description=None, sequential=None, events_filter=None, + max_retries=None, ): """Enroll job based on events. @@ -1475,8 +1496,12 @@ 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. + events_filter (Optional[dict[str, Any]]): Filtering conditions + to filter the source event. For more technical specifications + look to server backed 'ayon_server.sqlfilter.Filter'. + TODO: Add example of filters. + max_retries (Optional[int]): How many times can be event retried. + Default value is based on server (3 at the time of this PR). Returns: Union[None, dict[str, Any]]: None if there is no event matching @@ -1487,6 +1512,7 @@ class ServerAPI(object): "sourceTopic": source_topic, "targetTopic": target_topic, "sender": sender, + "maxRetries": max_retries, } if sequential is not None: kwargs["sequential"] = sequential @@ -2236,6 +2262,34 @@ class ServerAPI(object): response.raise_for_status("Failed to create/update dependency") return response.data + def _get_dependency_package_route( + self, filename=None, platform_name=None + ): + major, minor, patch, _, _ = self.server_version_tuple + if (major, minor, patch) <= (0, 2, 0): + # Backwards compatibility for AYON server 0.2.0 and lower + self.log.warning(( + "Using deprecated dependency package route." + " Please update your AYON server to version 0.2.1 or higher." + " Backwards compatibility for this route will be removed" + " in future releases of ayon-python-api." + )) + if platform_name is None: + platform_name = platform.system().lower() + base = "dependencies" + if not filename: + return base + return "{}/{}/{}".format(base, filename, platform_name) + + if (major, minor) <= (0, 3): + endpoint = "desktop/dependency_packages" + else: + endpoint = "desktop/dependencyPackages" + + if filename: + return "{}/{}".format(endpoint, filename) + return endpoint + def get_dependency_packages(self): """Information about dependency packages on server. @@ -2263,33 +2317,11 @@ class ServerAPI(object): server. """ - endpoint = "desktop/dependencyPackages" - major, minor, _, _, _ = self.server_version_tuple - if major == 0 and minor <= 3: - endpoint = "desktop/dependency_packages" - + endpoint = self._get_dependency_package_route() result = self.get(endpoint) result.raise_for_status() return result.data - def _get_dependency_package_route( - self, filename=None, platform_name=None - ): - major, minor, patch, _, _ = self.server_version_tuple - if major == 0 and (minor > 2 or (minor == 2 and patch >= 1)): - base = "desktop/dependency_packages" - if not filename: - return base - return "{}/{}".format(base, filename) - - # Backwards compatibility for AYON server 0.2.0 and lower - if platform_name is None: - platform_name = platform.system().lower() - base = "dependencies" - if not filename: - return base - return "{}/{}/{}".format(base, filename, platform_name) - def create_dependency_package( self, filename, @@ -3515,7 +3547,9 @@ class ServerAPI(object): folder_ids=None, folder_paths=None, folder_names=None, + folder_types=None, parent_ids=None, + statuses=None, active=True, fields=None, own_attributes=False @@ -3536,8 +3570,12 @@ class ServerAPI(object): for filtering. folder_names (Optional[Iterable[str]]): Folder names used for filtering. + folder_types (Optional[Iterable[str]]): Folder types used + for filtering. parent_ids (Optional[Iterable[str]]): Ids of folder parents. Use 'None' if folder is direct child of project. + statuses (Optional[Iterable[str]]): Folder statuses used + for filtering. active (Optional[bool]): Filter active/inactive folders. Both are returned if is set to None. fields (Optional[Iterable[str]]): Fields to be queried for @@ -3574,6 +3612,18 @@ class ServerAPI(object): return filters["folderNames"] = list(folder_names) + if folder_types is not None: + folder_types = set(folder_types) + if not folder_types: + return + filters["folderTypes"] = list(folder_types) + + if statuses is not None: + statuses = set(statuses) + if not statuses: + return + filters["folderStatuses"] = list(statuses) + if parent_ids is not None: parent_ids = set(parent_ids) if not parent_ids: @@ -4312,9 +4362,6 @@ class ServerAPI(object): fields.remove("attrib") fields |= self.get_attributes_fields_for_type("version") - if active is not None: - fields.add("active") - # Make sure fields have minimum required fields fields |= {"id", "version"} @@ -4323,6 +4370,9 @@ class ServerAPI(object): use_rest = True fields = {"id"} + if active is not None: + fields.add("active") + if own_attributes: fields.add("ownAttrib") @@ -5845,19 +5895,22 @@ class ServerAPI(object): """Helper method to get links from server for entity types. Example output: - [ - { - "id": "59a212c0d2e211eda0e20242ac120002", - "linkType": "reference", - "description": "reference link between folders", - "projectName": "my_project", - "author": "frantadmin", - "entityId": "b1df109676db11ed8e8c6c9466b19aa8", - "entityType": "folder", - "direction": "out" - }, + { + "59a212c0d2e211eda0e20242ac120001": [ + { + "id": "59a212c0d2e211eda0e20242ac120002", + "linkType": "reference", + "description": "reference link between folders", + "projectName": "my_project", + "author": "frantadmin", + "entityId": "b1df109676db11ed8e8c6c9466b19aa8", + "entityType": "folder", + "direction": "out" + }, + ... + ], ... - ] + } Args: project_name (str): Project where links are. diff --git a/openpype/vendor/python/common/ayon_api/utils.py b/openpype/vendor/python/common/ayon_api/utils.py index 314d13faec..502d24f713 100644 --- a/openpype/vendor/python/common/ayon_api/utils.py +++ b/openpype/vendor/python/common/ayon_api/utils.py @@ -1,3 +1,4 @@ +import os import re import datetime import uuid @@ -15,6 +16,7 @@ except ImportError: import requests import unidecode +from .constants import SERVER_TIMEOUT_ENV_KEY from .exceptions import UrlError REMOVED_VALUE = object() @@ -27,6 +29,23 @@ RepresentationParents = collections.namedtuple( ) +def get_default_timeout(): + """Default value for requests timeout. + + First looks for environment variable SERVER_TIMEOUT_ENV_KEY which + can affect timeout value. If not available then use 10.0 s. + + Returns: + float: Timeout value in seconds. + """ + + try: + return float(os.environ.get(SERVER_TIMEOUT_ENV_KEY)) + except (ValueError, TypeError): + pass + return 10.0 + + class ThumbnailContent: """Wrapper for thumbnail content. @@ -231,30 +250,36 @@ def _try_parse_url(url): return None -def _try_connect_to_server(url): +def _try_connect_to_server(url, timeout=None): + if timeout is None: + timeout = get_default_timeout() try: # TODO add validation if the url lead to Ayon server - # - thiw won't validate if the url lead to 'google.com' - requests.get(url) + # - this won't validate if the url lead to 'google.com' + requests.get(url, timeout=timeout) except BaseException: return False return True -def login_to_server(url, username, password): +def login_to_server(url, username, password, timeout=None): """Use login to the server to receive token. Args: url (str): Server url. username (str): User's username. password (str): User's password. + timeout (Optional[float]): Timeout for request. Value from + 'get_default_timeout' is used if not specified. Returns: Union[str, None]: User's token if login was successfull. Otherwise 'None'. """ + if timeout is None: + timeout = get_default_timeout() headers = {"Content-Type": "application/json"} response = requests.post( "{}/api/auth/login".format(url), @@ -262,7 +287,8 @@ def login_to_server(url, username, password): json={ "name": username, "password": password - } + }, + timeout=timeout, ) token = None # 200 - success @@ -273,47 +299,67 @@ def login_to_server(url, username, password): return token -def logout_from_server(url, token): +def logout_from_server(url, token, timeout=None): """Logout from server and throw token away. Args: url (str): Url from which should be logged out. token (str): Token which should be used to log out. + timeout (Optional[float]): Timeout for request. Value from + 'get_default_timeout' is used if not specified. """ + if timeout is None: + timeout = get_default_timeout() headers = { "Content-Type": "application/json", "Authorization": "Bearer {}".format(token) } requests.post( url + "/api/auth/logout", - headers=headers + headers=headers, + timeout=timeout, ) -def is_token_valid(url, token): +def is_token_valid(url, token, timeout=None): """Check if token is valid. + Token can be a user token or service api key. + Args: url (str): Server url. token (str): User's token. + timeout (Optional[float]): Timeout for request. Value from + 'get_default_timeout' is used if not specified. Returns: bool: True if token is valid. """ - headers = { + if timeout is None: + timeout = get_default_timeout() + + base_headers = { "Content-Type": "application/json", - "Authorization": "Bearer {}".format(token) } - response = requests.get( - "{}/api/users/me".format(url), - headers=headers - ) - return response.status_code == 200 + for header_value in ( + {"Authorization": "Bearer {}".format(token)}, + {"X-Api-Key": token}, + ): + headers = base_headers.copy() + headers.update(header_value) + response = requests.get( + "{}/api/users/me".format(url), + headers=headers, + timeout=timeout, + ) + if response.status_code == 200: + return True + return False -def validate_url(url): +def validate_url(url, timeout=None): """Validate url if is valid and server is available. Validation checks if can be parsed as url and contains scheme. @@ -334,6 +380,7 @@ def validate_url(url): Args: url (str): Server url. + timeout (Optional[int]): Timeout in seconds for connection to server. Returns: Url which was used to connect to server. @@ -369,10 +416,10 @@ def validate_url(url): # - this will trigger UrlError if both will crash if not parsed_url.scheme: new_url = "https://" + modified_url - if _try_connect_to_server(new_url): + if _try_connect_to_server(new_url, timeout=timeout): return new_url - if _try_connect_to_server(modified_url): + if _try_connect_to_server(modified_url, timeout=timeout): return modified_url hints = [] diff --git a/openpype/vendor/python/common/ayon_api/version.py b/openpype/vendor/python/common/ayon_api/version.py index f3826a6407..ac4f32997f 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.4.1" +__version__ = "0.5.1" From 211d64c3dea458b18ba268a66fa2e292dcf0ed7d Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 18 Oct 2023 11:39:31 +0300 Subject: [PATCH 0368/1233] align houdini shelves manager in OP and Ayon --- .../schemas/schema_houdini_scriptshelf.json | 6 ++++- .../houdini/server/settings/shelves.py | 25 +++++++------------ 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_scriptshelf.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_scriptshelf.json index bab9b604b4..35d768843d 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_scriptshelf.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_scriptshelf.json @@ -40,6 +40,10 @@ "object_type": { "type": "dict", "children": [ + { + "type": "label", + "label": "Name and Script Path are mandatory." + }, { "type": "text", "key": "label", @@ -68,4 +72,4 @@ } ] } -} \ No newline at end of file +} diff --git a/server_addon/houdini/server/settings/shelves.py b/server_addon/houdini/server/settings/shelves.py index c8bda515f9..ac7922e058 100644 --- a/server_addon/houdini/server/settings/shelves.py +++ b/server_addon/houdini/server/settings/shelves.py @@ -1,23 +1,16 @@ from pydantic import Field from ayon_server.settings import ( BaseSettingsModel, - MultiplatformPathModel, - MultiplatformPathListModel, + MultiplatformPathModel ) class ShelfToolsModel(BaseSettingsModel): - name: str = Field(title="Name") - help: str = Field(title="Help text") - # TODO: The following settings are not compatible with OP - script: MultiplatformPathModel = Field( - default_factory=MultiplatformPathModel, - title="Script Path " - ) - icon: MultiplatformPathModel = Field( - default_factory=MultiplatformPathModel, - title="Icon Path " - ) + """Name and Script Path are mandatory.""" + label: str = Field(title="Name") + script: str = Field(title="Script Path") + icon: str = Field( "", title="Icon Path") + help: str = Field("", title="Help text") class ShelfDefinitionModel(BaseSettingsModel): @@ -31,10 +24,10 @@ class ShelfDefinitionModel(BaseSettingsModel): class ShelvesModel(BaseSettingsModel): _layout = "expanded" - shelf_set_name: str = Field(title="Shelfs set name") + shelf_set_name: str = Field("", title="Shelfs set name") - shelf_set_source_path: MultiplatformPathListModel = Field( - default_factory=MultiplatformPathListModel, + shelf_set_source_path: MultiplatformPathModel = Field( + default_factory=MultiplatformPathModel, title="Shelf Set Path (optional)" ) From a704fd44e8731cbe0ee17b042a5cc4808f87b2d9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 18 Oct 2023 10:45:52 +0200 Subject: [PATCH 0369/1233] ignore some predefined names to import --- openpype/modules/base.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 6f3e4566f3..080be251f3 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -434,8 +434,18 @@ def _load_ayon_addons(openpype_modules, modules_key, log): sys.path.insert(0, addon_dir) imported_modules = [] for name in os.listdir(addon_dir): + # Ignore of files is implemented to be able to run code from code + # where usually is more files than just the addon + # Ignore start and setup scripts + if name in ("setup.py", "start.py"): + continue + path = os.path.join(addon_dir, name) basename, ext = os.path.splitext(name) + # Ignore folders/files with dot in name + # - dot names cannot be imported in Python + if "." in basename: + continue is_dir = os.path.isdir(path) is_py_file = ext.lower() == ".py" if not is_py_file and not is_dir: From 6fb59e3085f7d621dcbde1d9b8ed8ed82081e51b Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 18 Oct 2023 12:36:43 +0300 Subject: [PATCH 0370/1233] resolve hound --- server_addon/houdini/server/settings/shelves.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/houdini/server/settings/shelves.py b/server_addon/houdini/server/settings/shelves.py index ac7922e058..8d0512bdeb 100644 --- a/server_addon/houdini/server/settings/shelves.py +++ b/server_addon/houdini/server/settings/shelves.py @@ -9,7 +9,7 @@ class ShelfToolsModel(BaseSettingsModel): """Name and Script Path are mandatory.""" label: str = Field(title="Name") script: str = Field(title="Script Path") - icon: str = Field( "", title="Icon Path") + icon: str = Field("", title="Icon Path") help: str = Field("", title="Help text") From b20f59e87ed1c98f678b136ee011918bb54e9b7b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 18 Oct 2023 11:53:50 +0200 Subject: [PATCH 0371/1233] formatting fix --- openpype/settings/ayon_settings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index eb64480dc3..7d4675c0f3 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -1439,8 +1439,8 @@ class _AyonSettingsCache: and bundle.get("activeUser") == username ): return bundle["name"] - # Return fake variant - distribution logic will tell user that he does not - # have set any dev bundle + # Return fake variant - distribution logic will tell user that he + # does not have set any dev bundle return "dev" @classmethod From 489a502550fdf610b0afeb91a33266b32793a344 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 18 Oct 2023 12:15:18 +0200 Subject: [PATCH 0372/1233] reverting backslash removal --- 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 b061271f5a..ab0d0f4971 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -2302,7 +2302,7 @@ Reopening Nuke should synchronize these paths and resolve any discrepancies. if env_path in path: # with regsub we make sure path format of slashes is correct resub_expr = ( - "[regsub -all {{\\}} [getenv {}] \"/\"]").format(env_var) + "[regsub -all {{\\\\}} [getenv {}] \"/\"]").format(env_var) new_path = path.replace( env_path, resub_expr From 65cfa7751c2b9805ff7e60bb24e6dae7c97e81be Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 18 Oct 2023 12:22:10 +0200 Subject: [PATCH 0373/1233] added disk mapping settings to core addon --- server_addon/core/server/settings/main.py | 25 +++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/server_addon/core/server/settings/main.py b/server_addon/core/server/settings/main.py index ca8f7e63ed..433d0ef2f0 100644 --- a/server_addon/core/server/settings/main.py +++ b/server_addon/core/server/settings/main.py @@ -12,6 +12,27 @@ from .publish_plugins import PublishPuginsModel, DEFAULT_PUBLISH_VALUES from .tools import GlobalToolsModel, DEFAULT_TOOLS_VALUES +class DiskMappingItemModel(BaseSettingsModel): + _layout = "expanded" + source: str = Field("", title="Source") + destination: str = Field("", title="Destination") + + +class DiskMappingModel(BaseSettingsModel): + windows: list[DiskMappingItemModel] = Field( + title="Windows", + default_factory=list, + ) + linux: list[DiskMappingItemModel] = Field( + title="Linux", + default_factory=list, + ) + darwin: list[DiskMappingItemModel] = Field( + title="MacOS", + default_factory=list, + ) + + class ImageIOFileRuleModel(BaseSettingsModel): name: str = Field("", title="Rule name") pattern: str = Field("", title="Regex pattern") @@ -97,6 +118,10 @@ class CoreSettings(BaseSettingsModel): widget="textarea", scope=["studio"], ) + disk_mapping: DiskMappingModel = Field( + default_factory=DiskMappingModel, + title="Disk mapping", + ) tools: GlobalToolsModel = Field( default_factory=GlobalToolsModel, title="Tools" From a810dfe78a97eb1467d3745ee786f2743f6faac2 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 18 Oct 2023 12:30:57 +0200 Subject: [PATCH 0374/1233] fixing assert message --- openpype/hosts/resolve/api/lib.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index 6f84d921e0..798c40a864 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -308,9 +308,9 @@ def create_timeline_item( assert output_timeline_item, AssertionError(( "Clip name '{}' was't created on the timeline: '{}' \n\n" - "Please check if the clip is in the media pool or if the timeline \n" - "is having activated correct track name, or if it is not already \n" - "having any clip add place on the timeline in: '{}' out: '{}'. \n\n" + "Please check if correct track position is activated, \n" + "or if a clip is not already at the timeline in \n" + "position: '{}' out: '{}'. \n\n" "Clip data: {}" ).format( clip_name, timeline.GetName(), timecode_in, timecode_out, clip_data From a11467883b5d1cec302f43361e7d939ff502ac07 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 18 Oct 2023 12:34:12 +0200 Subject: [PATCH 0375/1233] getting fps from timeline instead of project --- openpype/hosts/resolve/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index 798c40a864..aef9caca78 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -278,7 +278,7 @@ def create_timeline_item( # timing variables if all([timeline_in, source_start, source_end]): - fps = project.GetSetting("timelineFrameRate") + fps = timeline.GetSetting("timelineFrameRate") duration = source_end - source_start timecode_in = frames_to_timecode(timeline_in, fps) timecode_out = frames_to_timecode(timeline_in + duration, fps) From c61a601c78669d70c472c67016eeb77531f42bab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Wed, 18 Oct 2023 12:35:21 +0200 Subject: [PATCH 0376/1233] :bug: fix key in applicaiton json (#5787) `maya` was wrongly used instead of `mayapy`, breaking AYON defaults --- server_addon/applications/server/applications.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/applications/server/applications.json b/server_addon/applications/server/applications.json index 171bd709a6..db7f86e357 100644 --- a/server_addon/applications/server/applications.json +++ b/server_addon/applications/server/applications.json @@ -69,7 +69,7 @@ } ] }, - "maya": { + "mayapy": { "enabled": true, "label": "Maya", "icon": "{}/app_icons/maya.png", From fbc24f565415cc6cbb88547c9b333bf41a52a3e0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 18 Oct 2023 12:48:50 +0200 Subject: [PATCH 0377/1233] make sure handles on timeline are included only if available or demanded --- openpype/hosts/resolve/api/plugin.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py index c88ed762ab..9a09685bee 100644 --- a/openpype/hosts/resolve/api/plugin.py +++ b/openpype/hosts/resolve/api/plugin.py @@ -410,6 +410,18 @@ class ClipLoader: if handle_end is None: handle_end = int(self.data["assetData"]["handleEnd"]) + # check frame duration from versionData or assetData + frame_start = self.data["versionData"].get("frameStart") + if frame_start is None: + frame_start = self.data["assetData"]["frameStart"] + + # check frame duration from versionData or assetData + frame_end = self.data["versionData"].get("frameEnd") + if frame_end is None: + frame_end = self.data["assetData"]["frameEnd"] + + db_frame_duration = int(frame_end) - int(frame_start) + 1 + # get timeline in timeline_start = self.active_timeline.GetStartFrame() if self.sequential_load: @@ -423,10 +435,17 @@ class ClipLoader: source_in = int(_clip_property("Start")) source_out = int(_clip_property("End")) - # include handles + # check if source duration is shorter than db frame duration + source_with_handles = True + source_duration = source_out - source_in + 1 + if source_duration < db_frame_duration: + source_with_handles = False + + # only exclude handles if source has no handles or + # if user wants to load without handles if ( not self.with_handles - or _clip_property("Type") == "Video" + or not source_with_handles ): source_in += handle_start source_out -= handle_end From 105720ff0d3f999a735cfaeb0783997c13131b4e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 18 Oct 2023 12:55:17 +0200 Subject: [PATCH 0378/1233] add dev icons --- openpype/resources/__init__.py | 7 ++++++- openpype/resources/icons/AYON_icon_dev.png | Bin 0 -> 17344 bytes openpype/resources/icons/AYON_splash_dev.png | Bin 0 -> 21796 bytes 3 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 openpype/resources/icons/AYON_icon_dev.png create mode 100644 openpype/resources/icons/AYON_splash_dev.png diff --git a/openpype/resources/__init__.py b/openpype/resources/__init__.py index b8671f517a..b33d1bf023 100644 --- a/openpype/resources/__init__.py +++ b/openpype/resources/__init__.py @@ -55,6 +55,9 @@ def get_openpype_staging_icon_filepath(): def get_openpype_icon_filepath(staging=None): + if AYON_SERVER_ENABLED and os.getenv("AYON_USE_DEV") == "1": + return get_resource("icons", "AYON_icon_dev.png") + if staging is None: staging = is_running_staging() @@ -68,7 +71,9 @@ def get_openpype_splash_filepath(staging=None): staging = is_running_staging() if AYON_SERVER_ENABLED: - if staging: + if os.getenv("AYON_USE_DEV") == "1": + splash_file_name = "AYON_splash_dev.png" + elif staging: splash_file_name = "AYON_splash_staging.png" else: splash_file_name = "AYON_splash.png" diff --git a/openpype/resources/icons/AYON_icon_dev.png b/openpype/resources/icons/AYON_icon_dev.png new file mode 100644 index 0000000000000000000000000000000000000000..e99a64d475d1e5841e4994bcc7705aea8202b0fd GIT binary patch literal 17344 zcmc(H1zS|#*Y=r#p}SGap+QPo5KsmIl@vuJ6$PY2N@_->qy&|2kW?B3sUZZFE*a^N zlA%Pp-aY>Q?`L@Q@{&1o&faUUxYxbb*_&{ET}^6=a}*E+QEO|V3?T>(euP70B;dcl zKEsC)1bhBOU0vV9Q1cp(wuZW#l%kxR*d=i(2oj3*jO`O`d%)3YDVH1~z`?;0-|?+V zM@*cCF~>Dgxb5-Ldcs(Y%k&0cortkUc!VYCDZJ#ni7=AixVJrcXXn7VSgdFJudhn! zRNwZ&U*!$bi?fk&Bj>NY?qROdmz*&RyLj2D6u0u4t)1o0Q%@UbWOwuYDuzUQx~|9K zETdTI{v5eECd6jeKx2fW^-b%L17hTt`K&lUXB1@yC9R$N)mgvWG4Bp~ei;$H&173$+zVciy{mZ}Wc)pEyqaWf9Kx^cpew%%fLs9h>uyQFm3H z{pVgAHxyfL2#@r|CEVAz`_5D5u2yUw7F|$0c;$kk@Uv?+A)Z5L%}u|*U9hb4bAXa9 zsOEEM9hLMaZXK*hgR*P81Uv!Br2O8$wui`V| zzxa)>A>*nk@nx5mCZpXC6@4qbFXev~J6oMWhYJnX2dow?wsY)s9w)#kZ*}X>e{4ts?o^F+16asp%L8H9=T9k|xYnf*=j@(G$n-PlP8Un5L zhm43CEsl*<+iN_V;`lmsG|hX5#1YOy^Pb&O;IR5pFvCx zMip)eEz~xya9^x}ZNQDQ9;%^Gxg8bgA@rG*;P1Mo7GuG9STpJU&cn>GurR)2dU{K{;KnVdFDE)U$}L4#davqh^z^a0jBs7zyqVLy)oLM<^-l4}7m>&~ znL+ebtpCKcW8=B^*n62e)G_5i^88Wrw zvfhrDBojUT)=L&FqZ;|TOF>{{Df*^DF`P!O~` zb4rp-RZhni#zhLI2pu7DCasx2?MS+{Mece-c8U~;`0^V`Ah~1v3AHr)ywgl#*fce` zNUTL9uKqD_D>?zKOW!1jbSE2&zs>jT0six8vw8%s-Bb|2WfA7|ktz0^A&iHi(E>}B z`Dqme=OKRDvYxI$TmLLGY#2=e#V!w%27Z>mrYlVzZImfo#&i#_z)C+2o-y=z;OIBz zq}DWV_B?NDDhSBnniLjhSqRY;*5zPQ(7fh?4Ek&6k!1t4R#XAFnsg0ccBU`$cjJag zKgYf2=5qH?MOT3tRprzcZB6>!jL1maJAgSqL9=2I$acGNBIG^Xq{lm z5=%xHCVdS19^2R7-)x-waL;ldzCY$Bj%>>6kwq0PDa;_BuKVb)&fLljUqzvgudzzS zew8Kay>@fp*+{S&_fvDm@Gve|7)l_r@n9PMOQBbT<|1tYR)yl;23S|$?e`ZMl6tYe zxLwt)5*U_TEd-3xpvwy(LlY2G@RBdO3XB^!h}_GaFru7G$hjxyV04NN>A0n?q!Py1 zu)hdh#P3>NRC^Z)9FF_uncQCPN&D(-wZsZPJdsF?Jht~^H?Z(st&aBDRDqIYUUSiT*x+ON)M z4+Kx&*XyDl&M9Q7>h}@k$p-R-o`dqOKOMjOMc2AMRQp4eId-73FbX}LP90i3LX}9&VHM5HyqE=mNBPKZt<}Cp}g7Keugumsc?fe8sj%fs6&m~hp^nTK{Ojpj+ zyh_LE#)OnG(z|IR(8T8SUibF3fNHLpQ*gYH^NoXs_|(WN=)7=eck$K`dGviOm!s*7 zKxbW;LKZzlUa^byRkH>fVSG{Mgt@w9)9|K*k%cC>DV{81y;Xi=w`DSI{ zJXbyWT&29`xyefcaWr&S;Bi<37|&Is^9l&R*#xVbu#n6eHF6$ihym4D#u^ZCipUgo z6-cH;!g44xiFIz{DS16J5N)N%LAOK=( z`^nIJmAq(5%w?c`%X8>7`RqA^xHt+(`>vvzCB7&KDI1GPQyQEkjGhTbzvDgQUNusR z%9rowE?BxuAA1i7_^p8M2!F-~Ot2AL7Mwc<=U=g7$mC0*b46~R$JaC?2a1UB?Yd+W zzrmb|17OaSyZ?eN_Ff&C0*e^0m^O+Rin}$49s*LbM|;HniVHvS7bE_m2#h8GboMP* z+N<%*A2@~KWyW!nxK+Z;`e0@xEtvT!xITD;C?vZ^O`Aak;xl>LX_2~f@8(<~Yt2{3 z8E~&%A@urDKbewpv5QR{nL3E)F|!-~I@9s(8`p4GCN|XiPWh$mVf?^3Q$U~_T7y7u zcA%spIR`u+z5sVL&Agk;G(AX$-Zvp}CE0X%dLh33J9eF|7P_|z7Ih9RYTv{bZzHA% zfI;nd%$o8*j^GwQGc;#hjedi@i?5c1`tGuA{c76f6@ zO;)|F904qp`38Ifeyo|bTDTu60DCq4vRwVlJKOebj=D|_Q6uL126z`T@70~-yeqC3 z^q0Zv0#a!?Lglg;xCv5d3+(!?irJao-HMlSl*cMtMG@}LriZrPl;BK80dUbPqTlRY zcLL@@0cxUDPi% zAdB$`kuFiRXIhvRQy>Y)%4PCcq5(d&t!%n}(%SQ4Maa{;5FTQInF7Q8Ap(Yzu*aC2 zHU^K&0PHgamWwZC(F299ow1mds5RiE}f1rtZUe<(=oMG%3Ra2*J$eI+^W(J-X~rx5s_kf=87pj5

zO6|I-eO^WJ;efN3Jhn=>*~n>5$ImucJ_&iCJwZf303T|9(AUDNHMPr9X;QV zgtj{VJDRfm2oc8j_D3Fz@hdb;O>i`v?>*hFK+sdocq=B>--O|E{BPbxZEKXNiuLPO! zyN&nzn*~$7m8)FfD^IrkKI(kg{D$<|A=>v34{+oe7^6Ps@S(21j@G(X0KG0xaD(cI*o} zKN`M&*XD9+KlA!$j*wJ}9S1>`&?+OlQQ|{x1T}@#$aP$iucLuocmVMxLVpgdkc=6M z(?VdaW<+ETjRLY>r}gip+?@Sgne`;#=&bJTJFwC=;mHfU)OSC8${{i|zmTc88HA+( zwpJrZdih)}YXN|*X>Gk1l`+BN;x!srjLla4+XN>insYxeE~BlBYbDZua-QK%U@Y*< zXa;Y9WCqC(SydFfJdsVeMLTJBEWO2L{})U$ujxeL%fmL*B4xC_@1*>*|Pj@&+CDMH2WO)s0#WF=Q?bS znYXeW@!6`p{Yzr*oXIVl7_a!o)$l;scWDn;dB;rPmLFgKp%`U9(4g>A!CtibIB&)m zzViIy)(`4hMx{0rX-%iVKc7lAFd3r5Hfqn!eN^*1b|A_=G_o z*4rtII5nFEs{7g%5}_Az;l!~t;1c-xx+gvt%e-oCJ1_VVt*39A*&a>-g(uL}tZk@Z zTW-jzyiK>y*t7JFH(!uQVq(E*M|S;a3Hex3xfKD&i(@F!4}ozQ0Jq&5JlGHE&c&@1tsvH9x z;;Xcp)gEcu$1kt-e4T>*|$U1hk8zK8W;+_^RU*$rfBTJh}Qqo$3_IhoY4G ziuROm&jDg1J3v!^v9JUu8=LmP#pjP|oyl&kmQE@~DwUmKxDi0i!jdr@Wv_Ph_NGJ> zbbPPmI+RTZ4e~YVNm`GHIv-?6 z-H`E0XzgVfHSy_-o-Ff=S-lu{^|IF!=PP+{+CUV>+J!b2`DC1MxPG{VEI8R7m_!?Ee<81^fgNcCQbtC8NNdNlyzvsLXZrcj#l-H^Z5^& z-tKB5&@ZdOGS(PTGr_p0Uh&x7G~j(KJ+QFQ`?zY@eNdh@fD(n{mMA`Oeym3P#}6mW z0sx>{&hwvIl68{V*twUimNp&%(%1m#5{DLztFk0;Hti>qEc?%*eh?M>8M$$<@VSt# zA;zIRO&O^QI5kZ!rx#1P3ip=23AyNxmeFE!+d1l+cMGAgQVVPn?j2mCDW9?dU&`ew z*WU)la#|p{_W_RPte;J(E~~?N3Vd@`0A6zfj1drA&Kho6tIN0Yaf4`^Q9j+;)6)Le$@50f8;O5+lBG9>d$C7hQ@?Z zKnE6=`#IU>F2`c`pHW}j{{e!?aT;Oy0lgtt-^(ZDj&=Zuiw90$RMl$1F)aR82%u{x z<%5&Eqe-<}1Iqi3ksPblZ;r%j9Lv=>yGb-gEdWqeQxPZ^ zCP}vZ3y7Dz!+eXpUkLksM>62ThrwT}p@EpS{edeySVJ>G9^GI}I#wM=er>LG{mN^G z5IGI~=9*Q*h@|{@JII?^b6E$v0cf zt{m-!7~%h5rO{D<3X>W^#KwLzT$xd;s7Iw~{F{?31#d{5&eit{)YPoKY;JT4J?;8# zZp(W%!~i-R6k2~~1%GBxpsk7!siBjbnhlRUq#S?J=ul<}Y417G47Cg+e2CX*7Ihrtev*T%e9bANbo3!r!;VygeMK`5=9wS?|cWJ2phMJMl`lPT6QI#X{jze8b-;qo)*CyxVo&MOJK%F*># z7rxL$sFhU_JAc_djTpUiZr@AHzYb2}pk7IGx=e%n+}67rblBW7xrEKQ3_m0Fr?$(p zfeIk9=!==$u7dZ^S}Z|;`A*|lTD)}d*;g&9A6`*DmGJQ_TC=B9)oxKirqb9YbwK(s zI*v8WG*J)mGubjH!R~`|H*RD(F@F7n8x!lm;`ui_gIfJEuKEVAM&{Zi6Wzo}!0xze z>mt-7nIKlk7!(@W`i2yhtZk7RuhU>U6c>7k925fTS*|xD&NeK)rk>}$J}~#2xiZX> z_}2*HYbH>P7&V9`SAbfLE6~CwgXmZAjc6C@8 zQR1h)jJRBX$sDb0BjO2?2~d=VzxY6<&?HTzMD)2j#1&3~`KW|%Ld*+{v!5PzWJJEg zv|yQJE=WEpgtVNWU<1CRY?jk3T9 z=i_f1o{ppa@}Lae*N45HxNjYL+6M|3KK&bP$;{WLOI&YwFHbPIUn<^{j>J@_%+3dU zNoDjUw8}-1klsSLA~s3Ny4=JWNJXHy;r=Xjhn^&9+htZu|E>Iric7S6QQ8yl^RKRP z)W@qmXqgK75pr0)7Bv33ZFkiLci0@{jI$qID;*v^=5d&)-AcV*ZBK1pWjEkIS{J7E z(xxZb6OaE{88IDrvV`f4r8Z&mrgT?%o|GKV=s$W@;m#mwt>-l(J5~fSi(l z_IG*lji6rO0KVFH+h#kV`S^>B%hbd{utt`?k9_NF8M~wv`rz-7p3ajeg;E`f)efWa zJt^`V!=c=P_sp%47Q6lGwC}r9V=jBUyk#pt}jeiH}Z|CctG;^o5_`QpdPd2HvNx~43 zCc7%&kF&wk6W3pmPUWucYL6c86rX~*%T3PyhDwm|Cd+r#UKW34za1;>{q4EAEnGtxn z^2&S4H(v=kcZE?<7cb@TD9ib5Ywgn^k+p$ zmpzMwg@wcS?-LfAr<-D%-SK6%p_$XUvl=vlrbt6&QwpS`FOupgIchRgq)wAQ;_cs&P ziv~;@N7JwPb%Ws_9qw(oT+4dB9q|qfq60M8IpMpMX0!8qWWWp=Db;Z+S!U&3xpila z8fwDtcVYg0-E`#xOM{G`eoo^xJ<4at+j;7tG`$jL6v5exvGMUX;&s0mq+Ob*2%C4JPm{Wn|i#umN{WT^HuW6OZ*A>b_vf{V8M z77GgtHmAo2HeX*~5V&$>@*r3TxaZ~jRdx?f|0Fag>ZHhhS*@_TapOjJCwGwl2(WLi zP6x`&XE`hMPo7>zpw7rE^Q|g}QSt5d`ilPO2bt`!(-4&0!ZarRj zpoGcU{b`#Kus7wmQ7ES09xQx2?{DH2zugw&jj$JBmLw_bj>Nkk?<+RX2s2UKx^d$b zyMpTsZTyDLTkwp_FEH{HM6aC+q-dD^_3aJ!&E=m>#v>K2pC;TR3hxvHV`g5XA6}kn ztoL239;K=EcR~7Z_sG{Tbtie?uk20aq&?PPc!0;1XB})Waes3BS@G3)E9?b`ap0%7 zJf?h?l7@$kmRQw;kwOMWMvu4x_a9U`PhL()fUF~>-U#R%_6_N&PEu@fuC4!42LWK8 z78mzND~9XJSbaAy2hD9Gqjs=D9dO~^=cmFBqa~h6aax95y}dTLvIoiszur)>Nunpr z_B=L!4;Kyz@bX%_SK9Q%H=mszF_&!9Yv13S4hrh%PLl5X`PtK)=hV2N;(O);`rn zt7_&4#FPJGuTkn#bpP&tq;zk8O&f&dkjVk%zXG3>C#kMn*bQpOaym&eKU50iYoR)Vb9NVkKm$H*MT| zr07n#`2ueTfSB&%gYBM$1WRWCJ!wHhhC&StA5A~WS$9N+ul{KazvTPp*EbOD{toTi zGVmE95T8z*G@)n1A5G;g_r^l5U7(<(RNU^CUKz@#6}|hx>dPzUk|74SJg>E}V-PNn z{w@#vT^)`2@ZsX9u*S#0lS4645fK%~sfL43xASfZd#A;Oxm?-A5+zC7bh9-C9PRE) ztfyI}$T?lI?u?do8mn?UrM{J=^(Z$lFZt>Vk|dCdl7WYg=0EHPG2*nczcoJsG#*9H zDDd=je?F$!E#O+`(4U2_o-lgeTUW1Qwka#G(f1J`z_u@a5va+#1_XkUkx?QDxZ`KT z1qRjhvA_NP4ro$N1)ZIYGnQ8-I6Qv**yhIv6PwMM76CD_o`c}~Sz(?sPGiH79EyXC zU3&tpt-+L^dQuf{J7JKrTfoab&*mZ|UYTcn4X?89q}lE`?~<4kqjGwmp#NDT9H<9o2^M|t3Nl9op zwAwAzwDQidX4_c)Afm;4V$f$ z=^zLUK>AeiB>zFGYTb4w!GTt`dH(}LP=3++Y$r|~#gNJ#PBUQBvL(dRV0 z(hcIEn^8~s!RMzde=?4bk6FULUZ{pnDHESRhkF2&OMWM6{8=UD%43jp76G!#QWwe7 zH~#;<0O#eN2F@B2D}f}XxYf?sQ@_#V0MhZ=;qc~L;6Mb6eJ(1_XQ-w6dL2b0M7l4$ z`{>{lKJ>vvdK+gQla!|9{aGddN=|lm>&1IzSTnaT%mSjKT?bKW^bL07H6IUFEzgpf zy7oE-pCc%{gIO(nGS{$6@HM(vqTkTP8HhEds!C?4(pFD8@OU3v&A|B`gm_PTIAhN@ z!Hf?zS2eC>>!w|{X%C}c9CvBgiHuzTh{`n@TbF{mbHqFEIjeTWhh2X+Ai`_|0<9YJwH*VhS zk?vEr9;RkGt)1!?PV3dJJj~*E*6IF#v@31*;w15*h5vC(FAy0~+)G znxE>;1aVp5^wI|h2eOJK{8)Ci%Sc!P%`q`fom#?>ot-_;WxB~`Fju>zlrp`-V@Y>= zF~ubr_}g8OXGdN&G$<2Xo8aQ3+}3&^a}@h-wvg%=7#LJ74Wk@8-)Lo46Hf_}uzox~ z_LqLTtiL^CY+2W&>+}6^XSD~6V*_&i>~Tl-rWXNpDtj+?`R~t$=2cY5^!61RnU%@b+zM&xRn9Uq}Z1I_yaMCY*&L_!eMT-GG4D%DGJ84?oWeN3crBjav9Q z>4b_gYjaJ0_LK%dEj?c2Qf>ckH6}m*gIV36nfIh8 z;Cz?2lu!3yp7i92&#*7r59Z?Ey?abZmC{aQGNYr6dcxx3a)8>#DgOD&E33+r8Jm{o zR8l)fAsuw;|M6pa41u4jo~-EQf%pqoke7pXW(GXrQ!_-*qd)R3KQz|Yw+f-Z31rcA#si+u?!wUhrdN?z2|P zLcz1h*fgI@_}}TGh&dP9O9i0%e;in(Q| z^5gBrp2foa^h-b?Pfxjovm+uSsXST`tab-eCvV@rO$3blJ0N_$H@~dA^3cZey`wdY zIbvKRHE0eDkbc-s2E6^;=qSIQNsVJt-O27a?Wel%))({?!QF11oVHW`+d6C6bs68@ z2#!7i1g2dc*IPh`)D|FqN)kVRiaIiQ**3fJNq(eh@$9yN!2?i2Jr@7GjC_-nv~nC^ z$f0t-#_G}+b=Ho#EEad^>*gKu0-p-4o5su9@_SQevT#dQDHdftJJ!(1tFj;Ye&Cl|Mj-FKDo2jn1Df1V z28sjTzI&JCHs6sXZvGinp;MYLmr)?5AE#wK<2jLWZzLK-e$w>Gy0;AojFM8@^a>EX z9w$4aDFD@%HVpK60qOvxOi8eK_Kk^^wfB5DRQed;O7zTF9v(*ej4rr1 zHn6}v}4|5l5TdSIt9bNP|SapAfXmnKZU(B5CcdFh3r1p3-a%40%CB-3x z`g|Tx*lja2M}i%O07H9pNHrC?mddaNUe1wCWN1l65fW67>}X13`S}P8r*0|TLFMG{?a@(9y`L42BM!!= zPi>+V=E#;dVl9y%O`1I&&>*CuJdi$jOqbc~z19dMZ)Xhf%&;w-t_awW1wUrp6Vz?Z z0Yq^z-qcx2)VSnnK;z%HkGtZNKpNx~njW)f02>b zLa5juF7{S3`$$c7}e8S;G|L8dqM+*NF||Svr;)+=GEbv>?G9K5fV+yodkDZ zMLXkYMCtX&i6${owW>3&xi=f>6A?|c!(%@5G2h#JK2Rk)w*Fi%-lhgC;zZo4x0-e!Mu_FrW^&C87`q|8UnOX`tf(B zQBaT&S}E-E&Z&e9lh&+R{t7TULdLDMYJrc2j^3_up0qy&(IhZVP;)p=S2R2>{eAoO z3trD(6%CTTd;3=M$sk`ZC_hT-7SF|BG?}~1ulKd5=RMF86BARdF24;Z*zA5*@B{OD zro9pbw0^TR=p^FwADrdS#A zp5V(3URl`@4$onO#dpRQQt#itFDV_-Z8%s=$pa~>cRJ!8p&!$HydXhTuQvSso#?-s zuZ&F00G%}zJat)Z&Em-JENSSR56~+EjDVfj~*j*bZ4eSY3Xt&8EefCz2CKtMb`B219&4B7=G zScQoUNSiZD|7(@g_>bd*z96|TU%r%-uDXFH5Qnr~KhIL9(i8#uxcuB_6JamL;3p(A-$Q{2>lAGHe2@*Y_{#NY;JGOKc zT5hFX9@%*iQwIXL7cBJ3{U-UXtqb6V2C+TdKk|q+aOPjadA9Q2rZ-iwq;yY;;9x_b z@?GkbGl-3gd$``Ptqby%z-Xw70HHIp-KXmE*=wz6!Ef@*2rl|!Z0&bMVE7TOe`ZcW z!Pg1TQF9xR=}Jn^41QKUd2`U?63AG4UNe-2v-`YD!|dUvrV}Xn7eP-^J}D`wH|7a; z-qca^E?KMsIcyERPYvKoGRCdY?tJc*xy)e{|CdzLA>8%~IqQcKlsP^lageZjAmHp_vg)cfuE zB@s|p6S(?(u_7m-8s^oEmJz#Oyw@M_oD}5IUHk=QZ;8_lW4q-6yvy z_632m73`pf=p)+NImv+rUyp6jG))3Mwa-m19K#<>D!UbzQsS#Ybns_^0Qduc8c!qi04%lZGtNj)1eGz5MB?#|AZ;%t0(ivj zoc!_wou^Bk93GL|*7NVkLfaN=#+{{CM@zj=ey~|b5=bqmtmS}tZ;m}qBBGPGQmR2^ z$B`aW>~Y&^FcCPJ$B>SEl1uZ+kCmRsfQ~PYHlJ2IUp&$P-Oe6|(#FNVjl!TtVHCI} z2_ob9zZ&Hhlff5}%qu`mLNC$L*!vSfnal>)0b=SYZ@nbc=5JEEi%XmALAQJ1_@0jx zpaeDqy!sUY8Wz!@*Aw=wug^P>gGnl)6L1Su7R{#ZM9^zysYpUan6JkTDng&zpaWZ$ z(hW@Wn83rAnwyPt0FD30JwaEJ+)wXEiMhji(~+~-bV_PDwI92u&Agsp^1V(AbNvbBsxuO(;kBaJyhGwO-Q{2Q~-z`QM016&K9#a z!%!Gcf+d~^LNmKX1IY1MO7l*>Q+)T!T8OlD2bITSlI`U)^F}UEQ!dg5?Z5b0ZmnrH z6$-iu+o1jf8PDnqLQMck|D9O8ZjeSMoFDK+iUEMaLEtri-9YH{4!9 zmP6>qOnOg8t@v3*N?8UTyeIUa0e);gNrMTc!fr ztic!j^r(d#&o7sss{xw`WIFkLdz1;(V{nnEwPue?=frwH7 zKPuxl?r;e0LW1xoVR%7m1%y!B3F)2Lmao0NazrFllAsV&!q>Ecv_oJv0umB^zEgqG zAoXchMQ#H^(*rt*wqn!83TC%ZP8fHH8olRs2D{^<{4Yn;&XR0{ZW#C95Sf6(rvwyQ zsyngW^#&})ARfbn#BQPpqP(PQO(g8hMohUdJfV}1e|*cMgx z@A*C?NS}Z^brP7|v}$p>)!EuWF#47qQof&QS8Ws(WAMgncFHMMQg~ zfgS$}o`PPSiP)ZS$~iJMvU=HH(rRL0pL1B4X*FgNdkgR0f}wAxM=M(?^bdo5gUC5@ zdFUfKE@4jXqyjhnNqUnN&!+~?13Yy!#y!H@sqqjy5ogM6p6BWOcen5hj5T?GLL1oL zyrRZkHJ1?_Z!KyUHzQ%>?+eQ9M_nAV3X_#!L<1Ge#9?1cj1 zsrbv_D2eusX5Mfv3Be&UHKG~cOt9etoEmxi8*Yl}(|TTmeZ&s?3M=WL82}n-?;^_- z)^?hejfB%m$3NfOj3ceh9S9;F18-oZYTbYi5NC z)%0chu(G!^z?50oJVXk=Keoemg`?3z9QB;qTygMwO$uoFRC0F{{95#p;q8x`$bSj zY~wZT3!LW@|2CHx$eySV7WHrBiWsC7LXum^JM|{&SO)$&+&o|fZ1N{K3D#UKz<-cs zIe{KcSM&K9ml7#BGSu$y2O_%FM(TtK1nMJkZgD;k1#;gRg|BV4oQ%-&Lc(gcR#c~7 zf%=|{GAlhi;Z~kvSw5gZd~I?5{CfSLC}dQSZtJ`g#zF!bLxTcX)zN78 z18nLmr}xBFz|73@TWj6s*<|gRx)LP3(_V$KM!~K_^=5(<%|@f-94NUO_rV^^U(n#t zP@#FfoagUaRSi%slZ7@aCko9C-@#U5ZPkLujRL<$*gAndW^9Z97q$tCI&MX0gag@A1(;BoemR%iCqh0FAty0F*e$1E`t zDMG-T-gjZRe608_B$$AjC1wo&8^DCkhm1B7?vn+5ZRMhp_(kwLmjLv(+XTz)Mwi9gT=@$IM2(;wGE3RQ`ORcb$y2hSBYxb?eLP|G!wnCGs*qA zWea=}L}q<-;_~$GAsDpeF;R2dfDpPE&ck?g+;(tL3)~qhPW)^>Q>;BmbZb`Y0o{Br@WyzOes(SX^}b;Q84Z&gUJKk#QLL&CsP>jKrb;g*iGWbbTIJr zBZ&5OW(^j_46$RmwPyN5z(rbWNcRHF2@@^^8KO_IN#s6aK+tACws9#|V{p;Eto{Ue z_}5uacnPxC39}^*+^`rE?H0%hl=R4*)oajyVhlcS0=+DAqW13Ck_8rxGvLIZNIU01 zTNj~0%=Da3vo#nr<500bNnx^ho*-2Twr}q5Hh{G_bCFFlx8Io&#<17XI+thb&x7gs zVsB$A!`n_|xDaX-l5`5fG^&Kdd3Ic_Uc+CElqt~4Gc5|oWMUWQJb^17!ghpW)Kd{8WXT90Vdi48`Xk^#^E_)kRis>+V%`246TR96n0mcVy1G0p=)Y4 zXYzys2?j8-l-o5>i_(N;3j+%%p+Ee0c^IS*d3yy4{ULbf@A2lbQy|g=*#l#;4+ffQ zI+SQ8{Hq`-(^xHVxoAx}Q(Be{QJ@W@gD1n(9X7pzsC-_n!>&x30t5xZwMtv2ffvQ% zK0|hTvYN0WQ(&%odg|iEX#gj&`vqWIsV5~`(rXLTZGU0y)sFGGfc8zYz5q;g_LPuXJH$#M*n7l z>agsx-v~l7%=sf5K55(&$c3@7LKhtGTp$c`hnPc?I0l9k10DyCU;&tP zT4Kxr!56Sw`tyu%2P+s+ixRD3kk1P~hW8WK+xA`1xDP#zxC~s8Ui+;x$D)*4E<$ZI zG55ba-mx)3J?Q8E`$)G#hHg3`WbVEP+>@Gy(DFeu5VyIi-Fe1aADp=WW2junqzv_; zqWi{yj+VL!zy-zdzAfw2nh&A&k&PcriwwVgq<KPR< z{TwUF(HHZ7%R*n|f3CSVA`Ddtf>nm?^~^|4pI)edLspEEVnzRrG{y5=Vl9>hdWY>Y zcu%m@@fh`&`f}g=}7T%@l*=kfv;k;n`8QI$)jSL>z73%p51LJSn*U8AA8lWSH~s|n z@ z2GoHl8WBP-{v!-~ennCTr8jgapxvha7j{g9ME_nG`_$f5wd4uR3SEaal|4QJ%VTJ? zS3N2<90E%b!d^ku{sPBl!`g?NF);=e}A;T+~YtzpuAC)rbz_3%S zr@S?L`al^B*2HD6gcj#COd;qkq^hZe0B*zAs)VH+-uxN#?j9H+kyM%KMFzn$Nk387 zvIs@&l77jCpd+CfdMn+?KmzO^xLsUWTKz)$MG&3&rgK>aK zaP;V|Favim4jdeGV3SI9VeJBPM+Lz*i`mWyK%03VM%AHm7L#<36|!0)I%f7k{s+oh zuP|4!Hy$<|2P#uQW$H|yi00@2hwSW<#791j*Naa$Z!`dxHODP6XMFiDLyYL)($8wU ziz94dP#Hpjbf2%c6qqaSq6qptCO_<9;dEQLH_)amRu6sFaZn8eJ}x1y%$7&sexGGef-Nigs<}06j*(N6#}HW^9(CZ zV%?S+IvVH?=OS>vmI}J@mfcI6)LhSMxP_aUp(Bx&b;*6X*AblVis#04eVFu@#bp7$ zA{TJ)4sF(>R8G|3hQOnIt6LLk{?81ik@d8^hv+Asm7=Pt+?$%rsb4F zVlzSV$9?5jj&+ZAt;N_W&_spU8oI!U$-8cr!fIlfkSQG#KFX2jEtW&W{zGkT?bJ(h z#$>$=l`CDI;^0MynX!pU9fOjH;2bG4R?bS=tNu{T?<_!G+v^Rm>Hp=Q@E>QSb7n7d V7AFPXfnNdvX=~`BO0HQw|9|wzLIwZ; literal 0 HcmV?d00001 diff --git a/openpype/resources/icons/AYON_splash_dev.png b/openpype/resources/icons/AYON_splash_dev.png new file mode 100644 index 0000000000000000000000000000000000000000..3b366e755d841bde1b82b6e5c40457adff4d39dc GIT binary patch literal 21796 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 y7sY-0mXdy4=HZ|fKIJ9}0RQi Date: Wed, 18 Oct 2023 13:05:23 +0200 Subject: [PATCH 0379/1233] improving code readability --- openpype/hosts/resolve/api/plugin.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py index 9a09685bee..63da14b1c2 100644 --- a/openpype/hosts/resolve/api/plugin.py +++ b/openpype/hosts/resolve/api/plugin.py @@ -306,14 +306,18 @@ class ClipLoader: self.active_project = lib.get_current_project() # try to get value from options or evaluate key value for `handles` - self.with_handles = options.get("handles") or bool( - options.get("handles") is True) + self.with_handles = options.get("handles") is True + # try to get value from options or evaluate key value for `load_to` - self.new_timeline = options.get("newTimeline") or bool( - "New timeline" in options.get("load_to", "")) + self.new_timeline = ( + options.get("newTimeline") or + options.get("load_to") == "New timeline" + ) # try to get value from options or evaluate key value for `load_how` - self.sequential_load = options.get("sequentially") or bool( - "Sequentially in order" in options.get("load_how", "")) + self.sequential_load = ( + options.get("sequentially") or + options.get("load_how") == "Sequentially in order" + ) assert self._populate_data(), str( "Cannot Load selected data, look into database " From c49289bb8a30e22b020e2b35a7c60b73dddfa69e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 18 Oct 2023 19:20:31 +0800 Subject: [PATCH 0380/1233] add use selection back to the creator option & cleanup the viewport setting code --- openpype/hosts/max/api/lib.py | 96 +++++++++---------- .../hosts/max/plugins/create/create_review.py | 3 +- .../max/plugins/publish/collect_review.py | 57 +++++++---- .../publish/extract_review_animation.py | 18 ++-- 4 files changed, 98 insertions(+), 76 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 736b0fb544..48656842de 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -323,7 +323,7 @@ def is_headless(): @contextlib.contextmanager -def viewport_setup_updated(camera): +def viewport_camera(camera): """Function to set viewport camera during context ***For 3dsMax 2024+ Args: @@ -346,64 +346,55 @@ def viewport_setup_updated(camera): @contextlib.contextmanager -def viewport_setup(instance, viewport_setting, camera): - """Function to set camera and other viewport options - during context - ****For Max Version < 2024 - - Args: - instance (str): instance - viewport_setting (str): active viewport setting - camera (str): viewport camera - - """ - original = rt.viewport.getCamera() - has_vp_btn = rt.ViewportButtonMgr.EnableButtons - has_autoplay = rt.preferences.playPreviewWhenDone - if not original: +def viewport_preference_setting(camera, + general_viewport, + nitrous_viewport, + vp_button_mgr, + preview_preferences): + original_camera = rt.viewport.getCamera() + if not original_camera: # if there is no original camera # use the current camera as original - original = rt.getNodeByName(camera) + original_camera = rt.getNodeByName(camera) review_camera = rt.getNodeByName(camera) - - current_visualStyle = viewport_setting.VisualStyleMode - current_visualPreset = viewport_setting.ViewportPreset - current_useTexture = viewport_setting.UseTextureEnabled orig_vp_grid = rt.viewport.getGridVisibility(1) orig_vp_bkg = rt.viewport.IsSolidBackgroundColorMode() - visualStyle = instance.data.get("visualStyleMode") - viewportPreset = instance.data.get("viewportPreset") - useTexture = instance.data.get("vpTexture") - has_grid_viewport = instance.data.get("dspGrid") - bkg_color_viewport = instance.data.get("dspBkg") - + nitrousGraphicMgr = rt.NitrousGraphicsManager + viewport_setting = nitrousGraphicMgr.GetActiveViewportSetting() + vp_button_mgr_original = { + key: getattr(rt.ViewportButtonMgr, key) for key in vp_button_mgr + } + nitrous_viewport_original = { + key: getattr(viewport_setting, key) for key in nitrous_viewport + } + preview_preferences_original = { + key: getattr(rt.preferences, key) for key in preview_preferences + } try: rt.viewport.setCamera(review_camera) - rt.viewport.setGridVisibility(1, has_grid_viewport) - rt.preferences.playPreviewWhenDone = False - rt.ViewportButtonMgr.EnableButtons = False - rt.viewport.EnableSolidBackgroundColorMode( - bkg_color_viewport) - if visualStyle != current_visualStyle: - viewport_setting.VisualStyleMode = rt.Name( - visualStyle) - elif viewportPreset != current_visualPreset: - viewport_setting.ViewportPreset = rt.Name( - viewportPreset) - elif useTexture != current_useTexture: - viewport_setting.UseTextureEnabled = useTexture + rt.viewport.setGridVisibility(1, general_viewport["dspGrid"]) + rt.viewport.EnableSolidBackgroundColorMode(general_viewport["dspBkg"]) + for key, value in vp_button_mgr.items(): + setattr(rt.ViewportButtonMgr, key, value) + for key, value in nitrous_viewport.items(): + if nitrous_viewport[key] != nitrous_viewport_original[key]: + setattr(viewport_setting, key, value) + for key, value in preview_preferences.items(): + setattr(rt.preferences, key, value) yield + finally: - rt.viewport.setCamera(original) + rt.viewport.setCamera(review_camera) rt.viewport.setGridVisibility(1, orig_vp_grid) rt.viewport.EnableSolidBackgroundColorMode(orig_vp_bkg) - viewport_setting.VisualStyleMode = current_visualStyle - viewport_setting.ViewportPreset = current_visualPreset - viewport_setting.UseTextureEnabled = current_useTexture - rt.ViewportButtonMgr.EnableButtons = has_vp_btn - rt.preferences.playPreviewWhenDone = has_autoplay - + for key, value in vp_button_mgr_original.items(): + setattr(rt.ViewportButtonMgr, key, value) + for key, value in nitrous_viewport_original.items(): + setattr(viewport_setting, key, value) + for key, value in preview_preferences_original.items(): + setattr(rt.preferences, key, value) + rt.completeRedraw() def set_timeline(frameStart, frameEnd): @@ -630,7 +621,8 @@ def publish_review_animation(instance, filepath, def publish_preview_sequences(staging_dir, filename, - startFrame, endFrame, ext): + startFrame, endFrame, + percentSize, ext): """publish preview animation by creating bitmaps ***For 3dsMax Version <2024 @@ -639,13 +631,15 @@ def publish_preview_sequences(staging_dir, filename, filename (str): filename startFrame (int): start frame endFrame (int): end frame + percentSize (int): percentage of the resolution ext (str): image extension """ # get the screenshot rt.forceCompleteRedraw() rt.enableSceneRedraw() - res_width = rt.renderWidth - res_height = rt.renderHeight + resolution_percentage = float(percentSize) / 100 + res_width = rt.renderWidth * resolution_percentage + res_height = rt.renderHeight * resolution_percentage viewportRatio = float(res_width / res_height) @@ -684,4 +678,4 @@ def publish_preview_sequences(staging_dir, filename, if rt.keyboard.escPressed: rt.exit() # clean up the cache - rt.gc() + rt.gc(delayed=True) diff --git a/openpype/hosts/max/plugins/create/create_review.py b/openpype/hosts/max/plugins/create/create_review.py index ea56123c79..977c018f5c 100644 --- a/openpype/hosts/max/plugins/create/create_review.py +++ b/openpype/hosts/max/plugins/create/create_review.py @@ -75,4 +75,5 @@ class CreateReview(plugin.MaxCreator): def get_pre_create_attr_defs(self): # Use same attributes as for instance attributes - return self.get_instance_attr_defs() + attrs = super().get_pre_create_attr_defs() + return attrs + self.get_instance_attr_defs() diff --git a/openpype/hosts/max/plugins/publish/collect_review.py b/openpype/hosts/max/plugins/publish/collect_review.py index 9ab1d6f3a8..6e9a6c870e 100644 --- a/openpype/hosts/max/plugins/publish/collect_review.py +++ b/openpype/hosts/max/plugins/publish/collect_review.py @@ -27,28 +27,15 @@ class CollectReview(pyblish.api.InstancePlugin, focal_length = node.fov creator_attrs = instance.data["creator_attributes"] attr_values = self.get_attr_values_from_data(instance.data) - data = { + + general_preview_data = { "review_camera": camera_name, "imageFormat": creator_attrs["imageFormat"], "keepImages": creator_attrs["keepImages"], "percentSize": creator_attrs["percentSize"], - "visualStyleMode": creator_attrs["visualStyleMode"], - "viewportPreset": creator_attrs["viewportPreset"], - "vpTexture": creator_attrs["vpTexture"], "frameStart": instance.context.data["frameStart"], "frameEnd": instance.context.data["frameEnd"], "fps": instance.context.data["fps"], - "dspGeometry": attr_values.get("dspGeometry"), - "dspShapes": attr_values.get("dspShapes"), - "dspLights": attr_values.get("dspLights"), - "dspCameras": attr_values.get("dspCameras"), - "dspHelpers": attr_values.get("dspHelpers"), - "dspParticles": attr_values.get("dspParticles"), - "dspBones": attr_values.get("dspBones"), - "dspBkg": attr_values.get("dspBkg"), - "dspGrid": attr_values.get("dspGrid"), - "dspSafeFrame": attr_values.get("dspSafeFrame"), - "dspFrameNums": attr_values.get("dspFrameNums") } if int(get_max_version()) >= 2024: @@ -61,14 +48,50 @@ class CollectReview(pyblish.api.InstancePlugin, instance.data["colorspaceDisplay"] = display instance.data["colorspaceView"] = view_transform + preview_data = { + "visualStyleMode": creator_attrs["visualStyleMode"], + "viewportPreset": creator_attrs["viewportPreset"], + "vpTexture": creator_attrs["vpTexture"], + "dspGeometry": attr_values.get("dspGeometry"), + "dspShapes": attr_values.get("dspShapes"), + "dspLights": attr_values.get("dspLights"), + "dspCameras": attr_values.get("dspCameras"), + "dspHelpers": attr_values.get("dspHelpers"), + "dspParticles": attr_values.get("dspParticles"), + "dspBones": attr_values.get("dspBones"), + "dspBkg": attr_values.get("dspBkg"), + "dspGrid": attr_values.get("dspGrid"), + "dspSafeFrame": attr_values.get("dspSafeFrame"), + "dspFrameNums": attr_values.get("dspFrameNums") + } + else: + preview_data = {} + general_viewport = { + "dspBkg": attr_values.get("dspBkg"), + "dspGrid": attr_values.get("dspGrid") + } + nitrous_viewport = { + "VisualStyleMode": creator_attrs["visualStyleMode"], + "ViewportPreset": creator_attrs["viewportPreset"], + "UseTextureEnabled": creator_attrs["vpTexture"] + } + preview_data["general_viewport"] = general_viewport + preview_data["nitrous_viewport"] = nitrous_viewport + preview_data["vp_button_manager"] = { + "EnableButtons" : False + } + preview_data["preferences"] = { + "playPreviewWhenDone": False + } + # Enable ftrack functionality instance.data.setdefault("families", []).append('ftrack') burnin_members = instance.data.setdefault("burninDataMembers", {}) burnin_members["focalLength"] = focal_length - instance.data.update(data) - self.log.debug(f"data:{data}") + instance.data.update(general_preview_data) + instance.data.update(preview_data) @classmethod def get_attribute_defs(cls): diff --git a/openpype/hosts/max/plugins/publish/extract_review_animation.py b/openpype/hosts/max/plugins/publish/extract_review_animation.py index a77f6213fa..2fbcb157a3 100644 --- a/openpype/hosts/max/plugins/publish/extract_review_animation.py +++ b/openpype/hosts/max/plugins/publish/extract_review_animation.py @@ -3,8 +3,8 @@ import pyblish.api from pymxs import runtime as rt from openpype.pipeline import publish from openpype.hosts.max.api.lib import ( - viewport_setup_updated, - viewport_setup, + viewport_camera, + viewport_preference_setting, get_max_version, publish_review_animation, publish_preview_sequences @@ -39,13 +39,17 @@ class ExtractReviewAnimation(publish.Extractor): review_camera = instance.data["review_camera"] if int(get_max_version()) < 2024: - nitrousGraphicMgr = rt.NitrousGraphicsManager - viewport_setting = nitrousGraphicMgr.GetActiveViewportSetting() - with viewport_setup(instance, viewport_setting, review_camera): + with viewport_preference_setting(review_camera, + instance.data["general_viewport"], + instance.data["nitrous_viewport"], + instance.data["vp_button_manager"], + instance.data["preferences"]): + percentSize = instance.data.get("percentSize") publish_preview_sequences( - staging_dir, instance.name, start, end, ext) + staging_dir, instance.name, + start, end, percentSize, ext) else: - with viewport_setup_updated(review_camera): + with viewport_camera(review_camera): preview_arg = publish_review_animation( instance, filepath, start, end, fps) rt.execute(preview_arg) From d0b397f130b997660b843bd6d7fdd79fa2295b7a Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 18 Oct 2023 19:23:30 +0800 Subject: [PATCH 0381/1233] hound --- openpype/hosts/max/plugins/publish/collect_review.py | 6 +++--- .../hosts/max/plugins/publish/extract_review_animation.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/collect_review.py b/openpype/hosts/max/plugins/publish/collect_review.py index 6e9a6c870e..21f63a8c73 100644 --- a/openpype/hosts/max/plugins/publish/collect_review.py +++ b/openpype/hosts/max/plugins/publish/collect_review.py @@ -66,7 +66,7 @@ class CollectReview(pyblish.api.InstancePlugin, } else: preview_data = {} - general_viewport = { + general_viewport = { "dspBkg": attr_values.get("dspBkg"), "dspGrid": attr_values.get("dspGrid") } @@ -77,8 +77,8 @@ class CollectReview(pyblish.api.InstancePlugin, } preview_data["general_viewport"] = general_viewport preview_data["nitrous_viewport"] = nitrous_viewport - preview_data["vp_button_manager"] = { - "EnableButtons" : False + preview_data["vp_btn_mgr"] = { + "EnableButtons": False } preview_data["preferences"] = { "playPreviewWhenDone": False diff --git a/openpype/hosts/max/plugins/publish/extract_review_animation.py b/openpype/hosts/max/plugins/publish/extract_review_animation.py index 2fbcb157a3..ccd641f619 100644 --- a/openpype/hosts/max/plugins/publish/extract_review_animation.py +++ b/openpype/hosts/max/plugins/publish/extract_review_animation.py @@ -42,7 +42,7 @@ class ExtractReviewAnimation(publish.Extractor): with viewport_preference_setting(review_camera, instance.data["general_viewport"], instance.data["nitrous_viewport"], - instance.data["vp_button_manager"], + instance.data["vp_btn_mgr"], instance.data["preferences"]): percentSize = instance.data.get("percentSize") publish_preview_sequences( From 6f2718ee4d64532380ee56774f0c0339a9fa3465 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 18 Oct 2023 19:29:43 +0800 Subject: [PATCH 0382/1233] add missing docstrings --- openpype/hosts/max/api/lib.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 48656842de..8103eaecc5 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -351,6 +351,16 @@ def viewport_preference_setting(camera, nitrous_viewport, vp_button_mgr, preview_preferences): + """Function to set viewport setting during context + + Args: + camera (str): Viewport camera for review render + general_viewport (dict): General viewport setting + nitrous_viewport (dict): Nitrous setting for + preview animation + vp_button_mgr (dict): Viewport button manager Setting + preview_preferences (dict): Preview Preferences Setting + """ original_camera = rt.viewport.getCamera() if not original_camera: # if there is no original camera From a993a2999ed8d32dbcfa53c055e4bd73971fe2f7 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 18 Oct 2023 19:30:12 +0800 Subject: [PATCH 0383/1233] add missing docstrings --- openpype/hosts/max/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 8103eaecc5..8c0bacf792 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -352,7 +352,7 @@ def viewport_preference_setting(camera, vp_button_mgr, preview_preferences): """Function to set viewport setting during context - + ***For Max Version < 2024 Args: camera (str): Viewport camera for review render general_viewport (dict): General viewport setting From aa74d48836c990a66413e821bd09628e5480a00f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 18 Oct 2023 14:00:57 +0200 Subject: [PATCH 0384/1233] clip duration from Frames clip attributes --- openpype/hosts/resolve/api/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py index 63da14b1c2..5c4a92df89 100644 --- a/openpype/hosts/resolve/api/plugin.py +++ b/openpype/hosts/resolve/api/plugin.py @@ -438,10 +438,10 @@ class ClipLoader: source_in = int(_clip_property("Start")) source_out = int(_clip_property("End")) + source_duration = int(_clip_property("Frames")) # check if source duration is shorter than db frame duration source_with_handles = True - source_duration = source_out - source_in + 1 if source_duration < db_frame_duration: source_with_handles = False From 8653accdbaa15830b7cf02cde75d3dc13d1b96ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Wed, 18 Oct 2023 14:04:28 +0200 Subject: [PATCH 0385/1233] Update openpype/settings/ayon_settings.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/settings/ayon_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index f8ab067fca..f1e08a9fd1 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -843,7 +843,7 @@ def _convert_nuke_project_settings(ayon_settings, output): # nodes ayon_imageio_nodes = ayon_imageio["nodes"] - if ayon_imageio_nodes.get("required_nodes"): + if "required_nodes" in ayon_imageio_nodes: ayon_imageio_nodes["requiredNodes"] = ( ayon_imageio_nodes.pop("required_nodes")) if ayon_imageio_nodes.get("override_nodes"): From 512986ea677c8c8ce910b69efed9c17291fcae48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Wed, 18 Oct 2023 14:04:47 +0200 Subject: [PATCH 0386/1233] Update openpype/settings/ayon_settings.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/settings/ayon_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index f1e08a9fd1..1941f85655 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -846,7 +846,7 @@ def _convert_nuke_project_settings(ayon_settings, output): if "required_nodes" in ayon_imageio_nodes: ayon_imageio_nodes["requiredNodes"] = ( ayon_imageio_nodes.pop("required_nodes")) - if ayon_imageio_nodes.get("override_nodes"): + if "override_nodes" in ayon_imageio_nodes: ayon_imageio_nodes["overrideNodes"] = ( ayon_imageio_nodes.pop("override_nodes")) From 84e3c8c8ad8e26ae56f7b1325c0441d0c35f4e70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Wed, 18 Oct 2023 14:05:22 +0200 Subject: [PATCH 0387/1233] Update openpype/settings/ayon_settings.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/settings/ayon_settings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index 1941f85655..43c0a1483c 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -851,8 +851,8 @@ def _convert_nuke_project_settings(ayon_settings, output): ayon_imageio_nodes.pop("override_nodes")) for item in ayon_imageio_nodes["requiredNodes"]: - if item.get("nuke_node_class"): - item["nukeNodeClass"] = item["nuke_node_class"] + if "nuke_node_class" in item: + item["nukeNodeClass"] = item.pop("nuke_node_class") item["knobs"] = _convert_nuke_knobs(item["knobs"]) for item in ayon_imageio_nodes["overrideNodes"]: From 8acd3cc1277dfbc7f2f7df4480d9ad30ab3429e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Wed, 18 Oct 2023 14:05:41 +0200 Subject: [PATCH 0388/1233] Update openpype/settings/ayon_settings.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/settings/ayon_settings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index 43c0a1483c..4d7b5c46af 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -856,8 +856,8 @@ def _convert_nuke_project_settings(ayon_settings, output): item["knobs"] = _convert_nuke_knobs(item["knobs"]) for item in ayon_imageio_nodes["overrideNodes"]: - if item.get("nuke_node_class"): - item["nukeNodeClass"] = item["nuke_node_class"] + if "nuke_node_class" in item: + item["nukeNodeClass"] = item.pop("nuke_node_class") item["knobs"] = _convert_nuke_knobs(item["knobs"]) output["nuke"] = ayon_nuke From b7e30d8fa3504efa7090d27fedf2a86eee15e534 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Wed, 18 Oct 2023 14:08:44 +0200 Subject: [PATCH 0389/1233] Update openpype/settings/ayon_settings.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/settings/ayon_settings.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index 4d7b5c46af..88af517932 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -836,10 +836,8 @@ def _convert_nuke_project_settings(ayon_settings, output): imageio_workfile[dst] = imageio_workfile.pop(src) # regex inputs - regex_inputs = ayon_imageio.get("regex_inputs") - if regex_inputs: - ayon_imageio.pop("regex_inputs") - ayon_imageio["regexInputs"] = regex_inputs + if "regex_inputs" in ayon_imageio: + ayon_imageio["regexInputs"] = ayon_imageio.pop("regex_inputs") # nodes ayon_imageio_nodes = ayon_imageio["nodes"] From 4771377cd5b72e0be91ddfcebf0114990772bcc7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 18 Oct 2023 14:08:57 +0200 Subject: [PATCH 0390/1233] use white background icon --- openpype/resources/icons/AYON_icon_dev.png | Bin 17344 -> 15928 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/openpype/resources/icons/AYON_icon_dev.png b/openpype/resources/icons/AYON_icon_dev.png index e99a64d475d1e5841e4994bcc7705aea8202b0fd..3e867ef372db7e861663e672e1dd93e72ee8c7e1 100644 GIT binary patch literal 15928 zcmcIri93{0+kcRVNF@rBy<{zeFj=w>MfU7TjqF(`WT})SVhBShCi|AL@9oqeW8X%W zFt)Kz_U}yZ`(58(@O52b&Yb6*``pX#Ue5hQ-7wT;W8qBrcUlUyo0i?F3!X+gI1u0qSOAsU$?;qbOp=HeZUG4Dk?BiTqTrzBh zXSR$_a&ZYavqY~Xend?N!n3E_8%Sx$a`UPeCwQB=b{XAYSDI+)R~1FE1$Dq(MSQ(x z-gNGff(@w|tY_tm&je_nKaG8J>dX-dc$d}txp*F)FWW0$B6*7WPMZHJ zHs2m;&JMU*EiYEcb@taaiSt*)uQVmX?MR%SM3WDU<4)t@qTHNK0UHkG2I{fWOaeI`kQk}}`DaBD-IAtT8THVEs9Uo1XwxeLAI&~bbzc=$+{cIID> zt99hKQ=+Sg=C$)}`X%Q`cg{p>V{om+ZtY^`;k1_C34xzipl-tYvYHJ?@sj*93R6Sg;Z2 zUK2U7{P2*!@x6wK)LM1N3DqBGeXt#us8JA*^Qm7L<#pPTS_yeq8LhPL9A~Si7n+Cg>uWnsEpN;H zI{m1oy?cAM{lS5;n`Wc=O9fPNZb6|4Am#t_-=^dPCMeDd)px@Lj=WyIhaY4=Xcj3> zs0fJsU`QO|_>{Ad%NQrc38}|k#G4?Q4CWd?E8aB8wwCtE#-T;qSezthL^MNo8U#Huq~kCG3%yLS@fC@YE{dVU)2CP7w4J`t46{-(7_I(`Y$LWezd#eu%`mO&;n!L zEYSTss1T7oYx(BE&hy=(A(taAjv|=jN?95hpsiMde)~c$hGe#XeIMiZ99K(yO}SS~4?%5Kl4S1~)229BjL9;+6Iv*G#6z7QQyGBgF7_N4lI2o28ClFEA0m%3XSbkI% zFOAt3%CAQ!r34xgv4Rl?55PmG`XT7dZIrHls?fQEAw@l68Y&|@zQ2g$Y&w)KO| zO*Ci3ysM3g-hvV6h9^+%GkQo54j*r}8Tun3=x(X|ontih%kEkbub@)K#M9?bTP0b1i z=ej#OZTYB8I|_&T2<^;|u$6HY>L#od$-j?#F?G*4RM*()zc9ie0r~0|ZE7#9k=yZT zW&y)l2tvx@&tgz|6BZR+W}^Ad%;O7GPtA&d zQ?coPz}5_gbWrdu)P_jo`Lk=h+^ixS%z|b>gjp}yp)%*sU#9ra-^Dz85D8_)f&pGs zp9S$DGNu79AUbcphHo(fqBGWJf(9B9U#IwWuZxV1gGFVPp%&?sK}Sr^^F5K0YeYxE zhZUf~{h(mJ=dT`!Z@U&3VDCBS(#b)k1 z?tq|3>P5fD)IH_+0pw?_!36y+;Qcz4X`rmfLnEID?8HuRcR>0Ad+vB#F(gS2=lCQ9 z4sQky-!yyn($-9t2@)L)4iEtj5ChD@r5w$Erw8a=SP&QmDiL=F^%O&j@99Rc%DO(o zsR~8_U2Wmbg~E9(pSP*;uP3dm0GV+9qEjcq@zVu?k8KSolC3X#F+Vhz-2hz~Kmx>@ zV59q2G`m(`7i!Z39KLG# zI&y+a_6Y zu%?-JGv1s4M2P^Rn5clLHi_4M)j)N-XCZ-H?4UQsexu{jP33bD%c)@0R1Av46N=P{ z2ux6*^Hs@4;d}>X!Evy_AwxQWWIO3|VLhaG7-<@@EMPAYxLZ!@&R+Fu$K#te7xE<| z%18j7JpG`!!f#+R|LSC%4d4J4a7*bsq0S&xG=BiW>Z9?V#@8*Nn0{Fd;kI)2*Y@=? zZzHKdD~K`lkkVH|N1Pv!%hVe~uJqpbr7el2_Ytn@DF=@+iiY2u7HLxplP7yz5hT;{)48_687#;@JHl zj$l+k9^383G^x66Y3)@3}a$9J03Oo zzQM8qZRugmg*a?b7UC`$xLA@^TrwAO?ySZDV0RvyGV~lDe|t4D(-5Nr)Y3JY9lG5{ zP%fAr4$DV#O;!5PWM}|nm=9>Gsk6DZzud1*G-}j@Dz7p@ADmF%^|o`-?6;V7BRTA; z;8c(4p%1rFjF=pK?WY_Bz-&n%7|$k&b75LW{K#a$K@-{nj;QOR?a39% zXs%o5RpZULGzNad4(T0+L0{UWZm;V4RmQJt0=||5E#M*)72*alFB)H{A$|Qhbkf=Tz`D)AedfIQy4QP`2iC|v7%V&Gs{N&e}lPi{DE|1a7 zx11GQgnS4K^vLf)GY}C-ekTXb{QIXOW4PWW5W7$e<^T@pWxo=Eo&G1A0f44is0g40 z!JDt;M!;UeKv+lN^8!GOhe=l9M+Zdm-IyI~(H!bPHutvj!PI&l{korEDhvi`J;uUB z$?_CF^=DX|uqLx9I4dV~-x)ZTVa)aeXS(v=Qb^qhnrv19kZ~|3zi8v%n!Jci26`|H zwYFPefXD>C*1hv&p62^0t{n0KkZBIhcHKIFGlw<`l z*@(h8HEzzr4sC%@$CI$oiRoDHh(A_!v(m!Mqwht5k5uVU(I4|a*7oKlZ{G%Jzjw9l zQTIUqb#A;d3sZ8q!mFX!k2tgn0LC_DFdt*T+7VyKW$8)uOs=*va1!QT`g%`o`LP)#_?034TWeU#9rhgX8 zX0X0cg;YytFCH_Qk(3CE|-22FwHGOAD^q3E@1=w?3wVekfv zI~cC86}B|&CKQwRsxpg68#c2geIe_W&lYX_GM)PL#90OZHeg!~IH6W?_|8)tC3xmJ z-o-|>mrWwq#b=9!B8I^w@K*Jv7w_a5?In?nCzkd(KFN~=2 zE%4hoZ@OOZ-?iI2fKFX1-5KU9In1aH9x1Rkp>TY1rwFtj1N-p6(d;zMtTI+#iG(+rO=p=O@E#x}u-1aKU+8<2nmVId8ZvEFf9x75o%-%Bz?TcWV~ zfi6-%Ez9J(YuMxMT(_e6q#a*FlOooXTr*b9U)1N6V3`Uw##Jq1~VwnXS{=g2^(iZ+i zc#Mkzq2^F-uZ{mEu}$gnmI1L&>uHDew$G1aqCb4E62ExtoQMkmd`~nG*;r#g&TWq3 zDO)g?Dk603?W#Py@!=}Bnd1x_Wg0Jo~#Xw7V#$yb8C%Pr3bADoe@4smol*IVHT(d zi>*wQ>qF>{T){hjKp#SPy5HgL`BaSR0{}VkiXI~0FBDX7bt86sslB~$ zZH}A6MdM>k#al={J;mx+7v|caFK^NV-RCc76w0Ms$MxZp$D{L3g7tf~kWWd16VLjn zVcz06ix(m>08JibL+ZuY!9dJkE0Y&}7M8QGea@~mjZUqeQrd!WlC(g15k|iPm}q?o zAB0(2p;?WGeZKhQBgJ9!n;NCB!zN(DCEu`CQE28bK+LZ5%1FBg%`aWVg6pK?Le5s^ zwdjbNA%~G5-byXH@IzU#bAa4BU>3gUg$RpYju#^6Z~9cToS?I7#N|=R>G!vJ7quq@M6kBQ|d*tK?Ssj`)kLiUOG>VwA`Y=oZLMwU9G`Gp(YWki%C6 zWc^zEf+B_ExdIEcfWe;9PyL-P+<$Q}#N0+1!d~ zCmwGUv0DQB09mBV;-@j58y)e{n-ZQ2FhQS98OO9ctsba80^$%Y6R(%GFfpKT)aHee z?Op9#Puf%%e@xHq4)1@X6?l*^T@!qk*;sj~qwt);t40ch|d~gO-#?G(KtJhe7V>$dp}tf#cQq(4K;7^wG;sHXEqb z&Zcu1PoVhs-ZDT~D+i=kRHtU2IX3^hZl|mLS7nNrmA^XDw0(%m7rH+qEC^!naTrBl z8Dx1Fzs!_fJI<$X5!U>3FyZ;7k-}pM&6)JQ3u~>uoKRo~K^oIDhR{_-RIjKim;1>8 zIP$&rL}7%vZOj-I@D9!VHw$WY0u>*k9Z6}`F3yi?AJX7-r`^bg`fiky4UIixIV5QGHg#9n2g;_oKoQ ztn|&zV=9%70C`@X{#AqDAJ|u9SMDmeGNE#Ox((cW z=nuiHo8O=H3aFyhd|GPP+zIKrMNK~gI7jh48aKNgnJ|6@*yWK6tG$uemi z-MZ#4HsR+R0pr#9n~Y|@!wn0+;)Hry5IDpy+~K(GdX_ybxowaC+I|6dRN|)96!}3ZEB&L7{6#h^f5rRBZ^5cRWvHM z^vjLPYuT1f+4cQ|&De6A%M}anhHdx@G}xuQ%`cFDb9}l27R@HC*_R_1Ob(qH)f#a5B3FmW0eZ`Ox%%;;B$(5?c z`Soh)#ldQ6*{fRS7X71d-|Bs~;ls^?|K}LLTG?GLzn$S~$Y>VCbana$G^Gfyw;v%( zGb+>Di> zj|p%jyt=3h58~3ne{pGQg(UUYwv@kNcC2!)x9&z!|Kle*`!~ zFA=-xbOu!}bI%?S(%#!nlBY$&f6UV=vkD$2_p?=q7v%f_HNg>;*-o@ONiy! z8Ru_Gi<{E|YqsMi=H~C2&OT`2lD1X6zx#J3Y`NpfdN$O0{hej0{XDAU&0vl0tFx&W z9UmJilSL@kpxYCFk{$Yfz0;Hr-gMts8Vw$Oc?OnVZ%-mKNnA3T6 zcC^l8xXQdBzpweCzP`TuRBL=y=w1**_+nqFgO{*wYy}2`DdoE?VpSioD6SjB(@UJM zc}9545a+}td2@N{#VLt^Ald4yEDgYi@s^%FAU( z{rl|-^701A^GPxe_7(TXKb^<(9aAqaY2M%7-lR-*$QZa?IGEn3PoI zC8uu#IoHz~np2?G@at&`gDIDsiYh89`o14QZa(4P{XOT-*A0n`zmO;qTOu_E*Upuh zdhx>$0Tti*!$wX-MNn5*)Okcs90^|d@V!VglHFfbRj+($wBD<3;)TQmNQ?b9r#gFH zUf$pKky_twAE!-GgQQs+eV4xTC71+rsIOm3S2q97=lk+7J73;=gXEmMSl^irj~iB2 zTZI*pN7WZ&_>_Dylca1cB-mc>CF?SOlx|!%vH5WODK+^tbm1oW!!C&Yo=Zb-$NMU# z(zUl>`x&q2Y+U{%Px+JJuy3|MlcXFnE;;G!T$2j>_VP^7??+5#H*e;J?rld6voLg3 z`p;X$@Lo1#IDGV%jaTFA4;zWe$&nVt)-bpi>54L4c;tL(^L|BER@O_izX;x)#p=xK zsd7>C4B^j_A}17Fh$8dE_Xd5jOkcl#4FIA}$B?pl%A?$%+Y6Qb_QAM<0&%zb;n1{S zriMy?)`lbKmp7K`DWReDlw)eU7v1KIi;8*@FPNTfA7Y{FshKM%{3d4bdcQ;G>eWYf z{=Mc^uD#+E88vl#>q^(xc7cChTim$uJAg0lj-`FzpX+4B96G&E%j2JU1C~Yt3;EF7 zi`iCql!Et!R$*S=vg%p-z;{_$pUz!Nxz(O3?>lxNYLzT)SH;e&ASu2ycBoAVp*XFL0i zg$17(5_W(Ho*WQN(R1g-x9rx9LbV|-p%>DKT1 zcX@oSD?4(3HM#l0_rdqTS|SJW&T5P446Rn5pRw%({qG2QDs-}}bTGba!a~^~pVJM#)JJ_#=1q?VY?KtGL zR{$|KW8*J*HErd@Do!x7@yO``D_VI!NifXh?fHfA}2O;)%Mzy{pY5d90Ff59J_MAJDiSz<*D8T zr@EGueZ${=O7lS|1BLG_my|V*j^Xg^-^o|~z(q7nLY1;1+kP4I)j)CJUa2kzXi$j$SmobEE&D*`DW)>DX-jm<*fu$i%l$oA7 zb*gSABbYV{OsHQLPSJ~B9(2w;RpBwbsS1QMg|)(BJz}=In;Uejtgw4qonh14kd}^6 zd>MGI19*}V>Z`mMc!%O=n)#rrdI_7{;dXGm8s)+Lz?HAu#lh62C4^7UxBT1;SKn#E ziXw?2GY$B_be|s=mUc9Ibf`@D(#;Ir&bI^J!Q$r4*(QE17df{+0__2Cp!y-dT;>*TtW`X=vAO<9 z2jJX7L19VxNE9>dbjH-V2+qW$x z4XgS#0H8Az#@*pRqlQx%T%1sv2`bEhpr3|UR)$oqSBG3vtR&z&lCM?7;`IgquoM|2OD{2n=f(JTs`RV= z{r$vD;e43^E0<&$zEgF0&lgtN_1=IxP0q!{L%P+@2_?m-E{k{Q+x%T|_a|5VS_h&S z=*ALLgjfs)bE1Cy8LvAqy2{atR^R!)V*GJootxg?A9iMx^s~XdfSGjk5b#NJ<$+<+ z0%KS!8F*pI{72WXFHQvjwDehvmx)pE7#i>jUE?h*DOo$GlaW_YP)<{aD&Q1q!$Py= zG?-~qhgSdm>@LT)FWjQrA`tA6-$@mtz*&3fo(;t9d=zrcHp*1p1X6nMYbwmNx0ZQO zp~PUuxeg4}W+UHIp0sO17|8_4mqm{%UrZat*&?mO|6sqStVi zcC_q=!Q)QRTvFv;A@ep%nVoqtV8#H@^NcoLuMkc3rc3y3)TVLHrZ^n`e{n$1p8ARR z#@5!ZPTyXB0K<2E)`Sl$c#hmt+Z{hv3|#0^=1;L0$t7X_R4Wy_tuF-6j`a*xq-Y=n z@fp*40V+o)zD6$sfA6Q3r{&={`|IP`;Dt+`5^o|w?63!IAjZx<@BX!DHcFLOlmBUSD1Zw3T)#CP8|R!E>f#mWgDy?$i+#}EDbgZ-W2yFZhbmB{ZJ2bHf}zTW8JI|I7|R$MxQ((qG(`^& z?hAfZP^=~7*r|5V;J-N3yBE$?>HY1D^FTx38rY{{V}FM-OwP?*+E;o$G&@vTsB*Bo z=G`3bj|s1V<4qf=IKP29|3VP8b)?FSfo-n#ou^xjQ$wSq1A?8D4VHn zH1INgttYYou&t~k9Z}EEoUPEIcJOd#Hv1Le#efaSWuM)CxKROR*gvCfzd-;#^!DnLkz*KRUf|;J0EWKo zI7)oDVQ1$7VvDlHMk;BEV$<^tOmZg^-;=!JBgNtKf&%P>g}W4$Z`~l2T>#+*TJ4sO z2Vdyl@l8tD!SK#vRMBP9gU{O=^E3OZ+W)>#xch^XQvH2+j8Q8c8}d2WDC#lISV4GI zY*!z!#PlmKCR(OR(V@9*u?Hh+;QqVi(Gta=M+f9$3m~_7@4N0W!}44lUg(VC52A<~ zmxBnaerr0d7@T~GsYfj4FdNT+NA+lu*Nji|t?r!b3IPi@{{2yc+xER$ps!(nKl8h1hHQF|?Hlug zeBpP$aQ$U{^_~Ie!2{D2%{dp3Kt8r`Eq zAvG@YAN-A^4xeP=B@`8hi(SI&Hc(&I(A)zP|HEI_)t-x?zhj8Zf&(D?&4@<(;a{ok z4>awJQhb=&WMV)>IQ-<8YAZ~{bge8r?;`$lyJT2UjY~NQ+<8?W)|}$zuIGQAEmw!8 zWSDY?np;|~RW!-OOibLSeG6FI2ZWIwf!#RyyPfbD`)AL~Z|*j!AuwPX>s-g5m@OMh ztse_)7ImQlDb{=S=e@*tDF2-9%%tokeHm?TmtU^UGXS}6JGiQiudJlH`WN~1usnVH!@2zAt%7KEtX z3VvzZWc&-n!{bdMDmrV^?Tbx2OZXQrSSZy3{gQC}Gx{qjgD_ICdWm&oP^DeSMlUV> z357cQPRrekqoO)%fQ}4=Ae`MexwwXUEgd`xElW2^3$+X9HHy92-zO2k&5aVK=92c| z_6!vpke~MclxbdoiDvUCxaS3~P3MQ~Y?6#+T?fnW&H$^$Ocb%Ltr4vsgVAO7fPS{>JM?*lKgY z3{`uT(n5St4_p9W1p&^kS2yp)w5cKH4C=lkv~0WR@)+R=wiw)vgm?qUsi_ zhAX@$btsB1cuLcbYsVX<0*B@%bYc-eMLheJW%a!owS$#di~@kYzV^#wMF5!d>{Xh$ z6%{WGUmtFc)KpA9dSOq2=aVyy_ z09y>aU~s~8Jr-qAlRJ6&#(7jgCG|W^KF2WKRTt);xFd2S*?S`D{B*A`R0V?ZfZdhv z^cFF|eT2beV1cwxoC@|^F{?_nT`WJL985b2Rp4fwauuwI8AnAq4?!k3?>$am;>o>A0xNzTMoqtomN+*ohEZ;AX5~p zH2t-9(5|AZ&wO=kyRTXfy=1#LtE*<>;*yWX;X)2}#yW=qw}Unp@$6?kAdzXDe7FQM*dCD2nAzF_jXfbQA<8euI9VH<{4Lgt9OixhIsw!osqiPI;)iHq+Uk zXPj*Y;ushbBj@<((76K`CpWj-?%#V4T_i4C=mOu$h3!AI^Jx>kw!b3ZLCE^>S-6Sd zo;cZQ9jC*}EnNX3 z#8+Xa4O~Gp0T~{+dWTjw04nWi(QjjGTcqSWeGg;`opV8xw?95+RGxkN#0(@sy&GpA zE`Ll_^v*I=9FHg};XYG+)x?T<{3J!dSUb#(y2OzX)}gfW<>Ur9^eixiKns~KAela$ zaNwEv`nAjKuXk294j@b2h<4c30A#Tzgn7Car8+4FJYolZURG*IuNNMP-LwE928OVp zj)gTB6cor-WdM(60~lmE`7H)s6>D1tN`N3JG0%L6sg>~n@k8Bos+-?WscITroUu73 z^ic%HrI9iR6L~QUEks}$k<_R*wHbLYlI_WJ*M8MgnhLuO5mHX`wUe=kl--11IONGu zZ{6vpiuBNJau9VCEg!o3;M~D3hsgDH_T%Q)?jsyg+y+5C2&Qrh2%UbFIerGB*sYMl zgVI3ITKnbNtSiVUEsaQ6Knf_;B~{%GnD4s|#9!L9zfqd&fqniuT7&sjVB~WimT;L% z_-B}DWZKJ{&PJ@T4kZ=tP!6! z2=*;NE?xOxWx}p0$MN&CI&w}LjT614MtiX;%Bm5|$0!`e>P+h3ez0KLC9K7>x75J_ z1UuQlXOxE2k&B8zrk`o18hl|*WA9QJymUoo2f%NWo+TIqa#0pGb9i%G}` zUQ$Z;BO+Zvyou(VdUixG!jU9+<&Qtzt$(*Q`K<_6NT5($bmOQXVS{}_bWA29Mc`Z* z==b}I`4$|xoLJ2Wu3SXPZX_iL@-&3w;SHJ(+J2fsC<`H%Dx54u5`rvQML;W0-a0$R zV{3@+v=De)z1{ZW7eqv4!#tE?#oKH4@X8y|X^Q;U<_UV-$}jLEAX-BqwCn)dJeOcX z!>a`hw@@LXjqk7QjIluj*%7$Vf6csi44V9*bD-e}S~h1azzVPGkT@Zf!VO|PjiPw} z3T((QLaS=(YqcZX4KzH7ob6zT2GT`oZJr9jOl=hp$1bke5;kTnXr&de6lL7VhRAQNUm0+k(OSUW&R zYg>cVw>b+@o^2PD%%POQGZ@3Q4mzk+$>B1-yHOKo+VO_|BK|gLG4|i+podzKzBHMF zODtrh?Hptay6GVGX}@H@81!?$wkY;+?4qbk6E25#1SOAjTsg-laf*QdYYA9*CQUAJ zf!EF9Ghgf%5YWG7!q;#p)mf%%~L=yZ&Sy1 z`R4!!!(k2oZS@qf@HzAitxJH^_%1&nL(VAT;Fp)Ff zbI`~?75;BWVcQFBAjCTTR4H1cCq(Ng%-3ahe1wiui85fi)$ngwOChp7g`@5HLon4a zdHpe(9fC~mTQiTBk^}?J6M+q#qIHVaqq)HNauY5?Faof<{=|kU)K6nKhZ>V!-4No- zl2#Saf@09<*aMnEA*iy<0(e))N_1TMg;n-4UNx=+wKCIA)tw@mWm8ULaK0T%x-@5x zJ}U46bQ)eK0`m$uhXFfoU_hrapSs!gOjWyMD(i9Rqezon!*tNMZCC$^5PC)auFUk->A7DCJXlVkCP(;J%G;0{vZqmM-E7XL0qQ3>u(r6 z=<|0GBTG{_hPAVxi}-92yV-OGFvxokwb!VrcU%l2NYZ+cA!w>jz-6MUw)1r`odDSc z83NjFp*Yb!5xWW4r%LeXH_&-NfS~Xvh^qESo(%{{EwSgD5(5McR#oHDR2XUi$yLob zLHl_osAce%hwnSqPWF~Yj6PrgF(^Fu1ah*$V#x&9Qol(0pAVi8BsAp?M|^eqY9#|S zQSW$&>k-qjwpYKgK=64P za@I8|ekhy|{Q(Qh$bagL1MGa8nkgG`O%e=W0Wr^rLT-*-jxv<2H<$eGE&~)N!=5ZB zc9PXF7!VXAIvT(92Q<6qE?vayknQh*4)M##Aq2xuMo56aGQrQOjytuFzJ;)vASajp z^AH5pbr9-szK_UD(949xm3nSED9(8<9~FB&tpZ3Xpd9B*th@q&-j|uwK_uDwrZpf_ z!mPb}9|Ev-Z9rkB&#F!tveZ`=yZ#frafayEBvQLSyjA9gXerHr+#Nay3J#dBs;xF8 zcG1;bH%Wg2Gy{kG6OB%%omkN2z8z4X@Wh8>ggL*tMgS>M7^2T5u$G>6{M>_;kYZK< z_ScC~{#L-RB<&!)b`x!!HaKkT)KUm)9RIr z(DEW++P%EqH*b6ZbG{oW=O{=3HFQB2VPR=={Jl90E!9Auvqb;YD!m6>iB0m;D3AaY zpYg|FVb~x1IN&y4Pt1c|0$v*a`HG~S%EY8N%nR6+>|BLe#_>3Prr)-`BY|K*%P1b_ ziwW@-XKV@Y0pJxbsUK8gSO5+5ndJ4xfcNm!``);t+A)ESn`Y4ceZZv$LyH%7RZc-F#M3}4?sR^v-+2`*4q4}10w-(D80{DbpLlZr^GVt6 zP|dI!I8l7%u*`}dcz>+*`oEu=|M}@J4%&|)h{MQVnGyW<`IW&EZf=DHgb+QdeZi|W z5L64gw}3)x(I`J#U`kge8*>%>xw#O)o(n{z?pa{UbW59urYs9v56}nRMvN`%a))VW zS?gHZp9CvXMLe!K0Njpabe0y&N!E7P3HQ&BX1LTC&LeHzyEt6`a&`LnbV*0Vi4u5~pX)Si_k_i;Y&iz(c5F({{e} zibSyNCm!U1%sW=9Y4usBx`DKD9~I{{9FH~VA0%8qAsR zw{2KL^|8}n2vH>SmsNCL^>1|>VKvMOr%_ExuELB#U z$oK#0)h}iZ_pl2ublagq&|AZjqQ&a4d`}qklp`Z<{f=D{*u-Qrm*Z2nzm8}O+FT(g;w@U^R#Q69#zw2;l=n9!wiZF#8!rt(^15f_Bv z)JPE_Gb8c%Mw;83wxhN8XS8j;z}o|vR@I-`cr9o626(@z%m`e)=!O9mwvYK^LR-?& zjd?r>d0yXo>NW5xk~^<(CpDNaM{raHJ-shMBWG9B#0Yr|p$m@D9V$V4eJqo7b%Yf{ zvIrI(6SNg4`ysm<*MNecD0<2&5%)oO^8|hRf)Ha%fM7(j$cAyd3?s=mmH|??Yqw}; zTiNzZn-mf@VGI{}${`uCoak!EdNfgPObJkzpL(9Gfo#-ZHi`rCk;(e0lYB>@a24<_A)1z<{M9^1`Qu<2F9lT%2S|)ZYBDn+m9^y6(r(W2u|naC zNqXD)IFew*S;2_0yOS18{^nCKfm1YnK&1?dA-@+)bAH^yq}Cs1fTq$qMN|aImyic9 zZ%2n2-8P5ZtBc>S7T_$~7kH58^QV^{?^VTEgpz5#_%$vcLrRjVc%KMCwa zcbF8;UCjnkN&mjE=toTjwXtuX;%-Mihr<8n-Dt1gQ7XVLj3uoHFbmFj-sIk)zvZmK zd|=$3o|G}LGL~kg+51NN-iO7^VO`U&zk`@>)q`t7zoyx&=*^W5LFD3awS&mgbkqZ+ yo^@_&)IJ>PR&_8Y7;!!0BfzBp&wtNT4~_{wPGxO%IxI-r2dQnSRibh0@&5pL0`NQl literal 17344 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 V7AFPXfnNdvX=~`BO0HQw|9|wzLIwZ; From 620269c2fc56da2b629d6b9d65ff70234c7820ec Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 18 Oct 2023 14:21:28 +0200 Subject: [PATCH 0391/1233] hound --- openpype/hosts/nuke/plugins/load/load_ociolook.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/nuke/plugins/load/load_ociolook.py b/openpype/hosts/nuke/plugins/load/load_ociolook.py index 29503ef4de..a22b0ae1af 100644 --- a/openpype/hosts/nuke/plugins/load/load_ociolook.py +++ b/openpype/hosts/nuke/plugins/load/load_ociolook.py @@ -39,7 +39,6 @@ class LoadOcioLookNodes(load.LoaderPlugin): # json file variables schema_version = 1 - def load(self, context, name, namespace, data): """ Loading function to get the soft effects to particular read node @@ -66,10 +65,10 @@ class LoadOcioLookNodes(load.LoaderPlugin): group_node = self._create_group_node( object_name, filepath, json_f["data"]) - self._node_version_color(context["version"], group_node) - self.log.info("Loaded lut setup: `{}`".format(group_node["name"].value())) + self.log.info( + "Loaded lut setup: `{}`".format(group_node["name"].value())) return containerise( node=group_node, @@ -244,7 +243,6 @@ class LoadOcioLookNodes(load.LoaderPlugin): self.log.info("Updated lut setup: `{}`".format( group_node["name"].value())) - def _load_json_data(self, filepath): # getting data from json file with unicode conversion with open(filepath, "r") as _file: @@ -304,6 +302,7 @@ class LoadOcioLookNodes(load.LoaderPlugin): color_value = self.old_node_color node["tile_color"].setValue(int(color_value, 16)) + def _colorspace_name_by_type(colorspace_data): """ Returns colorspace name by type From 2445f6a1e4c6844bbc51c02093af8aac1360ba67 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 18 Oct 2023 15:19:46 +0200 Subject: [PATCH 0392/1233] fixing lost commit content - loading updating fixed and container data are imprinted with updated representation id - not failing due `::` split on NoneType object - workfile colorspace is separated key from ocio look items - validator treating workfile colorspace key separately --- .../hosts/nuke/plugins/load/load_ociolook.py | 13 +++-- .../publish/collect_colorspace_look.py | 21 +++++--- .../publish/extract_colorspace_look.py | 4 +- .../publish/validate_colorspace_look.py | 52 +++++++++++++------ openpype/pipeline/colorspace.py | 3 ++ 5 files changed, 65 insertions(+), 28 deletions(-) diff --git a/openpype/hosts/nuke/plugins/load/load_ociolook.py b/openpype/hosts/nuke/plugins/load/load_ociolook.py index a22b0ae1af..3413b85749 100644 --- a/openpype/hosts/nuke/plugins/load/load_ociolook.py +++ b/openpype/hosts/nuke/plugins/load/load_ociolook.py @@ -15,7 +15,8 @@ from openpype.pipeline import ( ) from openpype.hosts.nuke.api import ( containerise, - viewer_update_and_undo_stop + viewer_update_and_undo_stop, + update_container, ) @@ -122,9 +123,9 @@ class LoadOcioLookNodes(load.LoaderPlugin): for node in group_node.nodes(): if node.Class() not in ["Input", "Output"]: nuke.delete(node) - if node.Class() == "Input": + elif node.Class() == "Input": input_node = node - if node.Class() == "Output": + elif node.Class() == "Output": output_node = node else: group_node = nuke.createNode( @@ -178,13 +179,12 @@ class LoadOcioLookNodes(load.LoaderPlugin): ( file for file in all_files if file.endswith(extension) - and item_name in file ), None ) if not item_lut_file: raise ValueError( - "File with extension {} not found in directory".format( + "File with extension '{}' not found in directory".format( extension)) item_lut_path = os.path.join( @@ -243,6 +243,9 @@ class LoadOcioLookNodes(load.LoaderPlugin): self.log.info("Updated lut setup: `{}`".format( group_node["name"].value())) + return update_container( + group_node, {"representation": str(representation["_id"])}) + def _load_json_data(self, filepath): # getting data from json file with unicode conversion with open(filepath, "r") as _file: diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_colorspace_look.py b/openpype/hosts/traypublisher/plugins/publish/collect_colorspace_look.py index c7a886a619..f259b120e9 100644 --- a/openpype/hosts/traypublisher/plugins/publish/collect_colorspace_look.py +++ b/openpype/hosts/traypublisher/plugins/publish/collect_colorspace_look.py @@ -1,3 +1,4 @@ +from math import e import os from pprint import pformat import pyblish.api @@ -42,9 +43,18 @@ class CollectColorspaceLook(pyblish.api.InstancePlugin, "input_colorspace", "output_colorspace" ]: - color_data = colorspace.convert_colorspace_enumerator_item( - creator_attrs[colorspace_key], config_items) - converted_color_data[colorspace_key] = color_data + if creator_attrs[colorspace_key]: + color_data = colorspace.convert_colorspace_enumerator_item( + creator_attrs[colorspace_key], config_items) + converted_color_data[colorspace_key] = color_data + else: + converted_color_data[colorspace_key] = None + + # add colorspace to config data + if converted_color_data["working_colorspace"]: + config_data["colorspace"] = ( + converted_color_data["working_colorspace"]["name"] + ) # create lut representation data lut_repre = { @@ -58,12 +68,11 @@ class CollectColorspaceLook(pyblish.api.InstancePlugin, instance.data.update({ "representations": [lut_repre], "source": file_url, + "ocioLookWorkingSpace": converted_color_data["working_colorspace"], "ocioLookItems": [ { "name": lut_repre_name, "ext": ext.lstrip("."), - "working_colorspace": converted_color_data[ - "working_colorspace"], "input_colorspace": converted_color_data[ "input_colorspace"], "output_colorspace": converted_color_data[ @@ -72,7 +81,7 @@ class CollectColorspaceLook(pyblish.api.InstancePlugin, "interpolation": creator_attrs["interpolation"], "config_data": config_data } - ] + ], }) self.log.debug(pformat(instance.data)) diff --git a/openpype/hosts/traypublisher/plugins/publish/extract_colorspace_look.py b/openpype/hosts/traypublisher/plugins/publish/extract_colorspace_look.py index ffd877af1d..f94bbc7a49 100644 --- a/openpype/hosts/traypublisher/plugins/publish/extract_colorspace_look.py +++ b/openpype/hosts/traypublisher/plugins/publish/extract_colorspace_look.py @@ -16,6 +16,7 @@ class ExtractColorspaceLook(publish.Extractor, def process(self, instance): ociolook_items = instance.data["ocioLookItems"] + ociolook_working_color = instance.data["ocioLookWorkingSpace"] staging_dir = self.staging_dir(instance) # create ociolook file attributes @@ -23,7 +24,8 @@ class ExtractColorspaceLook(publish.Extractor, ociolook_file_content = { "version": 1, "data": { - "ocioLookItems": ociolook_items + "ocioLookItems": ociolook_items, + "ocioLookWorkingSpace": ociolook_working_color } } diff --git a/openpype/hosts/traypublisher/plugins/publish/validate_colorspace_look.py b/openpype/hosts/traypublisher/plugins/publish/validate_colorspace_look.py index ce7f8831fd..548ce9d15a 100644 --- a/openpype/hosts/traypublisher/plugins/publish/validate_colorspace_look.py +++ b/openpype/hosts/traypublisher/plugins/publish/validate_colorspace_look.py @@ -21,25 +21,54 @@ class ValidateColorspaceLook(pyblish.api.InstancePlugin, instance.data["instance_id"]) creator_defs = created_instance.creator_attribute_defs + ociolook_working_color = instance.data.get("ocioLookWorkingSpace") ociolook_items = instance.data.get("ocioLookItems", []) - for ociolook_item in ociolook_items: - self.validate_colorspace_set_attrs(ociolook_item, creator_defs) + creator_defs_by_key = {_def.key: _def.label for _def in creator_defs} - def validate_colorspace_set_attrs(self, ociolook_item, creator_defs): + not_set_keys = {} + if not ociolook_working_color: + not_set_keys["working_colorspace"] = creator_defs_by_key[ + "working_colorspace"] + + for ociolook_item in ociolook_items: + item_not_set_keys = self.validate_colorspace_set_attrs( + ociolook_item, creator_defs_by_key) + if item_not_set_keys: + not_set_keys[ociolook_item["name"]] = item_not_set_keys + + if not_set_keys: + message = ( + "Colorspace look attributes are not set: \n" + ) + for key, value in not_set_keys.items(): + if isinstance(value, list): + values_string = "\n\t- ".join(value) + message += f"\n\t{key}:\n\t- {values_string}" + else: + message += f"\n\t{value}" + + raise PublishValidationError( + title="Colorspace Look attributes", + message=message, + description=message + ) + + def validate_colorspace_set_attrs( + self, + ociolook_item, + creator_defs_by_key + ): """Validate colorspace look attributes""" self.log.debug(f"Validate colorspace look attributes: {ociolook_item}") - self.log.debug(f"Creator defs: {creator_defs}") check_keys = [ - "working_colorspace", "input_colorspace", "output_colorspace", "direction", "interpolation" ] - creator_defs_by_key = {_def.key: _def.label for _def in creator_defs} not_set_keys = [] for key in check_keys: @@ -57,13 +86,4 @@ class ValidateColorspaceLook(pyblish.api.InstancePlugin, ) not_set_keys.append(def_label) - if not_set_keys: - message = ( - "Colorspace look attributes are not set: " - f"{', '.join(not_set_keys)}" - ) - raise PublishValidationError( - title="Colorspace Look attributes", - message=message, - description=message - ) + return not_set_keys diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 82d9b17a37..9f720f6ae9 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -546,6 +546,9 @@ def convert_colorspace_enumerator_item( Returns: dict: colorspace data """ + if "::" not in colorspace_enum_item: + return None + # split string with `::` separator and set first as key and second as value item_type, item_name = colorspace_enum_item.split("::") From c56560283ab72735718cba46e263887efc7b7d99 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 18 Oct 2023 15:21:58 +0200 Subject: [PATCH 0393/1233] hound catches --- openpype/hosts/nuke/plugins/load/load_ociolook.py | 5 +++-- .../traypublisher/plugins/publish/collect_colorspace_look.py | 1 - 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/nuke/plugins/load/load_ociolook.py b/openpype/hosts/nuke/plugins/load/load_ociolook.py index 3413b85749..18c8cdba35 100644 --- a/openpype/hosts/nuke/plugins/load/load_ociolook.py +++ b/openpype/hosts/nuke/plugins/load/load_ociolook.py @@ -184,8 +184,9 @@ class LoadOcioLookNodes(load.LoaderPlugin): ) if not item_lut_file: raise ValueError( - "File with extension '{}' not found in directory".format( - extension)) + "File with extension '{}' not " + "found in directory".format(extension) + ) item_lut_path = os.path.join( dir_path, item_lut_file).replace("\\", "/") diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_colorspace_look.py b/openpype/hosts/traypublisher/plugins/publish/collect_colorspace_look.py index f259b120e9..6aede099bf 100644 --- a/openpype/hosts/traypublisher/plugins/publish/collect_colorspace_look.py +++ b/openpype/hosts/traypublisher/plugins/publish/collect_colorspace_look.py @@ -1,4 +1,3 @@ -from math import e import os from pprint import pformat import pyblish.api From 519a99adff9e7b5735be59cc62bf4f4dc75bd7ca Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 18 Oct 2023 21:39:42 +0800 Subject: [PATCH 0394/1233] bug fix some of the unsupported arguments --- openpype/hosts/max/api/lib.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 8c0bacf792..fc74d78f05 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -331,6 +331,9 @@ def viewport_camera(camera): """ original = rt.viewport.getCamera() has_autoplay = rt.preferences.playPreviewWhenDone + nitrousGraphicMgr = rt.NitrousGraphicsManager + viewport_setting = nitrousGraphicMgr.GetActiveViewportSetting() + orig_preset = viewport_setting.ViewportPreset if not original: # if there is no original camera # use the current camera as original @@ -343,6 +346,8 @@ def viewport_camera(camera): finally: rt.viewport.setCamera(original) rt.preferences.playPreviewWhenDone = has_autoplay + viewport_setting.ViewportPreset = orig_preset + rt.completeRedraw() @contextlib.contextmanager @@ -604,6 +609,15 @@ def publish_review_animation(instance, filepath, visual_style_preset = instance.data.get("visualStyleMode") if visual_style_preset == "Realistic": visual_style_preset = "defaultshading" + elif visual_style_preset == "Shaded": + visual_style_preset = "defaultshading" + log.warning( + "'Shaded' Mode not supported in " + "preview animation in Max 2024..\n\n" + "Using 'defaultshading' instead") + + elif visual_style_preset == "ConsistentColors": + visual_style_preset = "flatcolor" else: visual_style_preset = visual_style_preset.lower() # new argument exposed for Max 2024 for visual style From 1ecf502e9a25538d55d47c8af12d2053654279b3 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 18 Oct 2023 17:09:20 +0300 Subject: [PATCH 0395/1233] add collectors section in Houdini settings --- .../projects_schema/schemas/schema_houdini_publish.json | 4 ++++ server_addon/houdini/server/settings/publish_plugins.py | 6 ++++-- 2 files changed, 8 insertions(+), 2 deletions(-) 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 d030b3bdd3..7bdb643dbb 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 @@ -4,6 +4,10 @@ "key": "publish", "label": "Publish plugins", "children": [ + { + "type":"label", + "label":"Collectors" + }, { "type": "dict", "collapsible": true, diff --git a/server_addon/houdini/server/settings/publish_plugins.py b/server_addon/houdini/server/settings/publish_plugins.py index 76030bdeea..a19fb4bca9 100644 --- a/server_addon/houdini/server/settings/publish_plugins.py +++ b/server_addon/houdini/server/settings/publish_plugins.py @@ -158,7 +158,8 @@ class CollectRopFrameRangeModel(BaseSettingsModel): ignore start and end handles specified in the asset data for publish instances """ - use_asset_handles: bool = Field(title="Use asset handles") + use_asset_handles: bool = Field( + title="Use asset handles") class BasicValidateModel(BaseSettingsModel): @@ -170,7 +171,8 @@ class BasicValidateModel(BaseSettingsModel): class PublishPluginsModel(BaseSettingsModel): CollectRopFrameRange: CollectRopFrameRangeModel = Field( default_factory=CollectRopFrameRangeModel, - title="Collect Rop Frame Range." + title="Collect Rop Frame Range.", + section="Collectors" ) ValidateWorkfilePaths: ValidateWorkfilePathsModel = Field( default_factory=ValidateWorkfilePathsModel, From 520ff86cd044c15328fa936cc452301836626943 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 18 Oct 2023 17:32:10 +0300 Subject: [PATCH 0396/1233] bump houdini addon version --- server_addon/houdini/server/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/houdini/server/version.py b/server_addon/houdini/server/version.py index 1276d0254f..0a8da88258 100644 --- a/server_addon/houdini/server/version.py +++ b/server_addon/houdini/server/version.py @@ -1 +1 @@ -__version__ = "0.1.5" +__version__ = "0.1.6" From 6ed7ebebceb47fc2dc829b6b9cd96a8eff7fa509 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 18 Oct 2023 16:32:29 +0200 Subject: [PATCH 0397/1233] 'NumberAttrWidget' shows 'Multiselection' label on multiselection --- openpype/tools/attribute_defs/widgets.py | 66 ++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 4 deletions(-) diff --git a/openpype/tools/attribute_defs/widgets.py b/openpype/tools/attribute_defs/widgets.py index d9c55f4a64..738f036b80 100644 --- a/openpype/tools/attribute_defs/widgets.py +++ b/openpype/tools/attribute_defs/widgets.py @@ -20,6 +20,7 @@ from openpype.tools.utils import ( FocusSpinBox, FocusDoubleSpinBox, MultiSelectionComboBox, + ClickableFrame, ) from openpype.widgets.nice_checkbox import NiceCheckbox @@ -251,6 +252,30 @@ class LabelAttrWidget(_BaseAttrDefWidget): self.main_layout.addWidget(input_widget, 0) +class ClickableLineEdit(QtWidgets.QLineEdit): + clicked = QtCore.Signal() + + def __init__(self, text, parent): + super(ClickableLineEdit, self).__init__(parent) + self.setText(text) + self.setReadOnly(True) + + self._mouse_pressed = False + + def mousePressEvent(self, event): + if event.button() == QtCore.Qt.LeftButton: + self._mouse_pressed = True + super(ClickableLineEdit, self).mousePressEvent(event) + + def mouseReleaseEvent(self, event): + if self._mouse_pressed: + self._mouse_pressed = False + if self.rect().contains(event.pos()): + self.clicked.emit() + + super(ClickableLineEdit, self).mouseReleaseEvent(event) + + class NumberAttrWidget(_BaseAttrDefWidget): def _ui_init(self): decimals = self.attr_def.decimals @@ -271,19 +296,23 @@ class NumberAttrWidget(_BaseAttrDefWidget): QtWidgets.QAbstractSpinBox.ButtonSymbols.NoButtons ) + multisel_widget = ClickableLineEdit("< Multiselection >", self) + input_widget.valueChanged.connect(self._on_value_change) + multisel_widget.clicked.connect(self._on_multi_click) self._input_widget = input_widget + self._multisel_widget = multisel_widget + self._last_multivalue = None self.main_layout.addWidget(input_widget, 0) - - def _on_value_change(self, new_value): - self.value_changed.emit(new_value, self.attr_def.id) + self.main_layout.addWidget(multisel_widget, 0) def current_value(self): return self._input_widget.value() def set_value(self, value, multivalue=False): + self._last_multivalue = None if multivalue: set_value = set(value) if None in set_value: @@ -291,13 +320,42 @@ class NumberAttrWidget(_BaseAttrDefWidget): set_value.add(self.attr_def.default) if len(set_value) > 1: - self._input_widget.setSpecialValueText("Multiselection") + self._last_multivalue = next(iter(set_value), None) + self._set_multiselection_visible(True) return value = tuple(set_value)[0] + self._set_multiselection_visible(False, False) + if self.current_value != value: self._input_widget.setValue(value) + def _on_value_change(self, new_value): + self.value_changed.emit(new_value, self.attr_def.id) + + def _on_multi_click(self): + self._set_multiselection_visible(False) + + def _set_multiselection_visible(self, visible, change_focus=True): + self._input_widget.setVisible(not visible) + self._multisel_widget.setVisible(visible) + if visible: + return + + # Change value once user clicked on the input field + if self._last_multivalue is None: + value = self.attr_def.default + else: + value = self._last_multivalue + self._input_widget.setValue(value) + if not change_focus: + return + # Change focus to input field and move cursor to the end + self._input_widget.setFocus(QtCore.Qt.MouseFocusReason) + line_edit = self._input_widget.lineEdit() + if line_edit is not None: + line_edit.setCursorPosition(len(line_edit.text())) + class TextAttrWidget(_BaseAttrDefWidget): def _ui_init(self): From 559750c07ce69e30a0d398a039a88bbe3f3d787c Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 18 Oct 2023 17:42:51 +0300 Subject: [PATCH 0398/1233] bump houdini addon version --- server_addon/houdini/server/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/houdini/server/version.py b/server_addon/houdini/server/version.py index 1276d0254f..0a8da88258 100644 --- a/server_addon/houdini/server/version.py +++ b/server_addon/houdini/server/version.py @@ -1 +1 @@ -__version__ = "0.1.5" +__version__ = "0.1.6" From 52a086c2b19b9d6137a291827a67d29dc4942a43 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 18 Oct 2023 23:10:37 +0800 Subject: [PATCH 0399/1233] integrate both functions into a function for extract preview action --- openpype/hosts/max/api/lib.py | 72 +++++++++++++------ .../max/plugins/publish/collect_review.py | 2 +- .../publish/extract_review_animation.py | 29 ++------ 3 files changed, 55 insertions(+), 48 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index fc74d78f05..4266db1e7f 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -347,15 +347,22 @@ def viewport_camera(camera): rt.viewport.setCamera(original) rt.preferences.playPreviewWhenDone = has_autoplay viewport_setting.ViewportPreset = orig_preset - rt.completeRedraw() @contextlib.contextmanager -def viewport_preference_setting(camera, - general_viewport, +def play_preview_when_done(has_autoplay): + current_playback = rt.preferences.playPreviewWhenDone + try: + rt.preferences.playPreviewWhenDone = has_autoplay + yield + finally: + rt.preferences.playPreviewWhenDone = current_playback + + +@contextlib.contextmanager +def viewport_preference_setting(general_viewport, nitrous_viewport, - vp_button_mgr, - preview_preferences): + vp_button_mgr): """Function to set viewport setting during context ***For Max Version < 2024 Args: @@ -366,12 +373,6 @@ def viewport_preference_setting(camera, vp_button_mgr (dict): Viewport button manager Setting preview_preferences (dict): Preview Preferences Setting """ - original_camera = rt.viewport.getCamera() - if not original_camera: - # if there is no original camera - # use the current camera as original - original_camera = rt.getNodeByName(camera) - review_camera = rt.getNodeByName(camera) orig_vp_grid = rt.viewport.getGridVisibility(1) orig_vp_bkg = rt.viewport.IsSolidBackgroundColorMode() @@ -383,11 +384,8 @@ def viewport_preference_setting(camera, nitrous_viewport_original = { key: getattr(viewport_setting, key) for key in nitrous_viewport } - preview_preferences_original = { - key: getattr(rt.preferences, key) for key in preview_preferences - } + try: - rt.viewport.setCamera(review_camera) rt.viewport.setGridVisibility(1, general_viewport["dspGrid"]) rt.viewport.EnableSolidBackgroundColorMode(general_viewport["dspBkg"]) for key, value in vp_button_mgr.items(): @@ -395,21 +393,15 @@ def viewport_preference_setting(camera, for key, value in nitrous_viewport.items(): if nitrous_viewport[key] != nitrous_viewport_original[key]: setattr(viewport_setting, key, value) - for key, value in preview_preferences.items(): - setattr(rt.preferences, key, value) yield finally: - rt.viewport.setCamera(review_camera) rt.viewport.setGridVisibility(1, orig_vp_grid) rt.viewport.EnableSolidBackgroundColorMode(orig_vp_bkg) for key, value in vp_button_mgr_original.items(): setattr(rt.ViewportButtonMgr, key, value) for key, value in nitrous_viewport_original.items(): setattr(viewport_setting, key, value) - for key, value in preview_preferences_original.items(): - setattr(rt.preferences, key, value) - rt.completeRedraw() def set_timeline(frameStart, frameEnd): @@ -638,6 +630,9 @@ def publish_review_animation(instance, filepath, viewport_texture_option = f"vpTexture:{viewport_texture}" job_args.append(viewport_texture_option) + auto_play_option = "autoPlay:false" + job_args.append(auto_play_option) + job_str = " ".join(job_args) log.debug(job_str) @@ -659,8 +654,6 @@ def publish_preview_sequences(staging_dir, filename, ext (str): image extension """ # get the screenshot - rt.forceCompleteRedraw() - rt.enableSceneRedraw() resolution_percentage = float(percentSize) / 100 res_width = rt.renderWidth * resolution_percentage res_height = rt.renderHeight * resolution_percentage @@ -703,3 +696,36 @@ def publish_preview_sequences(staging_dir, filename, rt.exit() # clean up the cache rt.gc(delayed=True) + +def publish_preview_animation(instance, staging_dir, filepath, + startFrame, endFrame, review_camera): + """Publish Reivew Animation + + Args: + instance (pyblish.api.instance): Instance + staging_dir (str): staging directory + filepath (str): filepath + startFrame (int): start frame + endFrame (int): end frame + review_camera (str): viewport camera for + preview render + """ + with play_preview_when_done(False): + with viewport_camera(review_camera): + if int(get_max_version()) < 2024: + with viewport_preference_setting( + instance.data["general_viewport"], + instance.data["nitrous_viewport"], + instance.data["vp_btn_mgr"]): + percentSize = instance.data.get("percentSize") + ext = instance.data.get("imageFormat") + rt.completeRedraw() + publish_preview_sequences( + staging_dir, instance.name, + startFrame, endFrame, percentSize, ext) + else: + fps = instance.data["fps"] + rt.completeRedraw() + preview_arg = publish_review_animation( + instance, filepath, startFrame, endFrame, fps) + rt.execute(preview_arg) diff --git a/openpype/hosts/max/plugins/publish/collect_review.py b/openpype/hosts/max/plugins/publish/collect_review.py index 21f63a8c73..c3985a2ded 100644 --- a/openpype/hosts/max/plugins/publish/collect_review.py +++ b/openpype/hosts/max/plugins/publish/collect_review.py @@ -35,7 +35,7 @@ class CollectReview(pyblish.api.InstancePlugin, "percentSize": creator_attrs["percentSize"], "frameStart": instance.context.data["frameStart"], "frameEnd": instance.context.data["frameEnd"], - "fps": instance.context.data["fps"], + "fps": instance.context.data["fps"] } if int(get_max_version()) >= 2024: diff --git a/openpype/hosts/max/plugins/publish/extract_review_animation.py b/openpype/hosts/max/plugins/publish/extract_review_animation.py index ccd641f619..27a86323eb 100644 --- a/openpype/hosts/max/plugins/publish/extract_review_animation.py +++ b/openpype/hosts/max/plugins/publish/extract_review_animation.py @@ -1,14 +1,7 @@ import os import pyblish.api -from pymxs import runtime as rt from openpype.pipeline import publish -from openpype.hosts.max.api.lib import ( - viewport_camera, - viewport_preference_setting, - get_max_version, - publish_review_animation, - publish_preview_sequences -) +from openpype.hosts.max.api.lib import publish_preview_animation class ExtractReviewAnimation(publish.Extractor): @@ -27,7 +20,6 @@ class ExtractReviewAnimation(publish.Extractor): filename = "{0}..{1}".format(instance.name, ext) start = int(instance.data["frameStart"]) end = int(instance.data["frameEnd"]) - fps = float(instance.data["fps"]) filepath = os.path.join(staging_dir, filename) filepath = filepath.replace("\\", "/") filenames = self.get_files( @@ -38,21 +30,10 @@ class ExtractReviewAnimation(publish.Extractor): " '%s' to '%s'" % (filename, staging_dir)) review_camera = instance.data["review_camera"] - if int(get_max_version()) < 2024: - with viewport_preference_setting(review_camera, - instance.data["general_viewport"], - instance.data["nitrous_viewport"], - instance.data["vp_btn_mgr"], - instance.data["preferences"]): - percentSize = instance.data.get("percentSize") - publish_preview_sequences( - staging_dir, instance.name, - start, end, percentSize, ext) - else: - with viewport_camera(review_camera): - preview_arg = publish_review_animation( - instance, filepath, start, end, fps) - rt.execute(preview_arg) + publish_preview_animation( + instance, staging_dir, + filepath, start, end, + review_camera) tags = ["review"] if not instance.data.get("keepImages"): From 3e75b9ec796791cdd19617e1dddf68db31420063 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 18 Oct 2023 23:13:03 +0800 Subject: [PATCH 0400/1233] hound --- openpype/hosts/max/api/lib.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 4266db1e7f..2027c88214 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -697,6 +697,7 @@ def publish_preview_sequences(staging_dir, filename, # clean up the cache rt.gc(delayed=True) + def publish_preview_animation(instance, staging_dir, filepath, startFrame, endFrame, review_camera): """Publish Reivew Animation @@ -707,22 +708,21 @@ def publish_preview_animation(instance, staging_dir, filepath, filepath (str): filepath startFrame (int): start frame endFrame (int): end frame - review_camera (str): viewport camera for - preview render + review_camera (str): viewport camera for preview render """ with play_preview_when_done(False): with viewport_camera(review_camera): if int(get_max_version()) < 2024: - with viewport_preference_setting( + with viewport_preference_setting( instance.data["general_viewport"], instance.data["nitrous_viewport"], instance.data["vp_btn_mgr"]): - percentSize = instance.data.get("percentSize") - ext = instance.data.get("imageFormat") - rt.completeRedraw() - publish_preview_sequences( - staging_dir, instance.name, - startFrame, endFrame, percentSize, ext) + percentSize = instance.data.get("percentSize") + ext = instance.data.get("imageFormat") + rt.completeRedraw() + publish_preview_sequences( + staging_dir, instance.name, + startFrame, endFrame, percentSize, ext) else: fps = instance.data["fps"] rt.completeRedraw() From bb4778e71bff7ba54e017eb35f252c7a916c132b Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 18 Oct 2023 18:17:08 +0300 Subject: [PATCH 0401/1233] MinKiu Comments --- .../publish/collect_rop_frame_range.py | 27 ++++++++++++++----- .../plugins/publish/validate_frame_range.py | 2 +- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py b/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py index c9bc1766cb..c610deba40 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py +++ b/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py @@ -41,11 +41,17 @@ class CollectRopFrameRange(pyblish.api.InstancePlugin, return # Log artist friendly message about the collected frame range + frame_start=frame_data["frameStart"] + frame_end=frame_data["frameEnd"] + if attr_values.get("use_handles"): self.log.info( "Full Frame range with Handles " - "{0[frameStartHandle]} - {0[frameEndHandle]}\n" - .format(frame_data) + "[{frame_start_handle} - {frame_end_handle}]\n" + .format( + frame_start_handle=frame_data["frameStartHandle"], + frame_end_handle=frame_data["frameEndHandle"] + ) ) else: self.log.info( @@ -54,20 +60,27 @@ class CollectRopFrameRange(pyblish.api.InstancePlugin, ) self.log.info( - "Frame range {0[frameStart]} - {0[frameEnd]}" - .format(frame_data) + "Frame range [{frame_start} - {frame_end}]" + .format( + frame_start=frame_start, + frame_end=frame_end + ) ) if frame_data.get("byFrameStep", 1.0) != 1.0: - self.log.info("Frame steps {0[byFrameStep]}".format(frame_data)) + self.log.info("Frame steps {}".format(frame_data["byFrameStep"])) instance.data.update(frame_data) # Add frame range to label if the instance has a frame range. label = instance.data.get("label", instance.data["name"]) instance.data["label"] = ( - "{0} [{1[frameStart]} - {1[frameEnd]}]" - .format(label, frame_data) + "{label} [{frame_start} - {frame_end}]" + .format( + label=label, + frame_start=frame_start, + frame_end=frame_end + ) ) @classmethod diff --git a/openpype/hosts/houdini/plugins/publish/validate_frame_range.py b/openpype/hosts/houdini/plugins/publish/validate_frame_range.py index 2411d29e3e..b35ab62002 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_frame_range.py +++ b/openpype/hosts/houdini/plugins/publish/validate_frame_range.py @@ -36,7 +36,7 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): "The frame range for the instance is invalid because " "the start frame is higher than the end frame.\n\nThis " "is likely due to asset handles being applied to your " - "instance or may be because the ROP node's start frame " + "instance or the ROP node's start frame " "is set higher than the end frame.\n\nIf your ROP frame " "range is correct and you do not want to apply asset " "handles make sure to disable Use asset handles on the " From f2ad5ee2536c8492363bc64b1a8110c37bd27ec3 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 18 Oct 2023 18:19:07 +0300 Subject: [PATCH 0402/1233] resolve hound --- .../hosts/houdini/plugins/publish/collect_rop_frame_range.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py b/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py index c610deba40..91d5a5ef74 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py +++ b/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py @@ -41,8 +41,8 @@ class CollectRopFrameRange(pyblish.api.InstancePlugin, return # Log artist friendly message about the collected frame range - frame_start=frame_data["frameStart"] - frame_end=frame_data["frameEnd"] + frame_start = frame_data["frameStart"] + frame_end = frame_data["frameEnd"] if attr_values.get("use_handles"): self.log.info( From eae470977570aecb8cd26d248d56f4a5ebd4cdcd Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 18 Oct 2023 23:39:47 +0800 Subject: [PATCH 0403/1233] refactored the preview animation publish --- openpype/hosts/max/api/lib.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 2027c88214..23a6ab0717 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -567,8 +567,8 @@ def get_plugins() -> list: return plugin_info_list -def publish_review_animation(instance, filepath, - start, end, fps): +def publish_review_animation(instance, staging_dir, start, + end, ext, fps): """Function to set up preview arguments in MaxScript. ****For 3dsMax 2024+ @@ -583,6 +583,9 @@ def publish_review_animation(instance, filepath, list: job arguments """ job_args = list() + filename = "{0}..{1}".format(instance.name, ext) + filepath = os.path.join(staging_dir, filename) + filepath = filepath.replace("\\", "/") default_option = f'CreatePreview filename:"{filepath}"' job_args.append(default_option) frame_option = f"outputAVI:false start:{start} end:{end} fps:{fps}" # noqa @@ -698,13 +701,14 @@ def publish_preview_sequences(staging_dir, filename, rt.gc(delayed=True) -def publish_preview_animation(instance, staging_dir, filepath, - startFrame, endFrame, review_camera): - """Publish Reivew Animation +def publish_preview_animation( + instance, staging_dir, + startFrame, endFrame, + ext, review_camera): + """Render camera review animation Args: instance (pyblish.api.instance): Instance - staging_dir (str): staging directory filepath (str): filepath startFrame (int): start frame endFrame (int): end frame @@ -718,7 +722,6 @@ def publish_preview_animation(instance, staging_dir, filepath, instance.data["nitrous_viewport"], instance.data["vp_btn_mgr"]): percentSize = instance.data.get("percentSize") - ext = instance.data.get("imageFormat") rt.completeRedraw() publish_preview_sequences( staging_dir, instance.name, @@ -726,6 +729,7 @@ def publish_preview_animation(instance, staging_dir, filepath, else: fps = instance.data["fps"] rt.completeRedraw() - preview_arg = publish_review_animation( - instance, filepath, startFrame, endFrame, fps) + preview_arg = publish_review_animation(instance, staging_dir, + startFrame, endFrame, + ext, fps) rt.execute(preview_arg) From f650ecc20e0892ee9c72a1ae6160e443e9d0b726 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 18 Oct 2023 23:58:33 +0800 Subject: [PATCH 0404/1233] refactored the preview animation publish --- openpype/hosts/max/api/lib.py | 8 ++++++-- .../hosts/max/plugins/publish/extract_review_animation.py | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 23a6ab0717..9c5eccb215 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -703,8 +703,8 @@ def publish_preview_sequences(staging_dir, filename, def publish_preview_animation( instance, staging_dir, - startFrame, endFrame, - ext, review_camera): + ext, review_camera, + startFrame=None, endFrame=None): """Render camera review animation Args: @@ -714,6 +714,10 @@ def publish_preview_animation( endFrame (int): end frame review_camera (str): viewport camera for preview render """ + if start_frame is None: + start_frame = int(rt.animationRange.start) + if end_frame is None: + end_frame = int(rt.animationRange.end) with play_preview_when_done(False): with viewport_camera(review_camera): if int(get_max_version()) < 2024: diff --git a/openpype/hosts/max/plugins/publish/extract_review_animation.py b/openpype/hosts/max/plugins/publish/extract_review_animation.py index 27a86323eb..d57ed44d65 100644 --- a/openpype/hosts/max/plugins/publish/extract_review_animation.py +++ b/openpype/hosts/max/plugins/publish/extract_review_animation.py @@ -32,8 +32,8 @@ class ExtractReviewAnimation(publish.Extractor): review_camera = instance.data["review_camera"] publish_preview_animation( instance, staging_dir, - filepath, start, end, - review_camera) + ext, review_camera, + startFrame=start, endFrame=end) tags = ["review"] if not instance.data.get("keepImages"): From f96b7dbb198f0b5ca23abf5470ca72829039bc5b Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 18 Oct 2023 23:59:39 +0800 Subject: [PATCH 0405/1233] hound --- openpype/hosts/max/api/lib.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 9c5eccb215..eb6cd3cabc 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -714,10 +714,10 @@ def publish_preview_animation( endFrame (int): end frame review_camera (str): viewport camera for preview render """ - if start_frame is None: - start_frame = int(rt.animationRange.start) - if end_frame is None: - end_frame = int(rt.animationRange.end) + if startFrame is None: + startFrame = int(rt.animationRange.start) + if endFrame is None: + endFrame = int(rt.animationRange.end) with play_preview_when_done(False): with viewport_camera(review_camera): if int(get_max_version()) < 2024: From 0e22a2ef3cdb32ee013bd0ea51c4685768d53997 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 18 Oct 2023 19:25:32 +0200 Subject: [PATCH 0406/1233] Remove `setParms` call since it's responsibility of `self.imprint` to set the values --- openpype/hosts/houdini/api/plugin.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/hosts/houdini/api/plugin.py b/openpype/hosts/houdini/api/plugin.py index d79ccc71bd..72565f7211 100644 --- a/openpype/hosts/houdini/api/plugin.py +++ b/openpype/hosts/houdini/api/plugin.py @@ -250,14 +250,12 @@ class HoudiniCreator(NewCreator, HoudiniCreatorBase): key: changes[key].new_value for key in changes.changed_keys } - # Update ParmTemplates + # Update parm templates and values self.imprint( instance_node, new_values, update=True ) - # Update values - instance_node.setParms(new_values) def imprint(self, node, values, update=False): # Never store instance node and instance id since that data comes From a2c5934a1e2c9b88d445762a623647d8c80fdc08 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 18 Oct 2023 22:14:13 +0200 Subject: [PATCH 0407/1233] Fix updating parms of same name that user had taken off of default value manually + refactor deprecated `Node.replaceSpareParmTuple` to use `ParmTemplateGroup.replace` instead --- openpype/hosts/houdini/api/lib.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index f258dda36e..2440ded6ad 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -369,16 +369,18 @@ def imprint(node, data, update=False): # is for some reason lost every call to `appendToFolder()` parm_folder = parm_group.findFolder("Extra") + for parm_template in update_parms: + parm_group.replace(parm_template.name(), parm_template) + + # When replacing a parm with a parm of the same name it preserves its + # value if before the replacement the parm was not at the default, + # because it has a value override set. Since we're trying to update the + # parm by using the new value as `default` we enforce the parm is at + # default state + node.parm(parm_template.name()).revertToDefaults() + node.setParmTemplateGroup(parm_group) - # TODO: Updating is done here, by calling probably deprecated functions. - # This needs to be addressed in the future. - if not update_parms: - return - - for parm in update_parms: - node.replaceSpareParmTuple(parm.name(), parm) - def lsattr(attr, value=None, root="/"): """Return nodes that have `attr` From e001b2632a69a32bcc07dbdf5d16c5ed70927bbd Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 18 Oct 2023 22:16:18 +0200 Subject: [PATCH 0408/1233] Refactor `parm` to `parm_template` to clarify variable refers to a parm template --- openpype/hosts/houdini/api/lib.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 2440ded6ad..a777d15581 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -339,7 +339,7 @@ def imprint(node, data, update=False): if value is None: continue - parm = get_template_from_value(key, value) + parm_template = get_template_from_value(key, value) if key in current_parms: if node.evalParm(key) == data[key]: @@ -348,10 +348,10 @@ def imprint(node, data, update=False): log.debug(f"{key} already exists on {node}") else: log.debug(f"replacing {key}") - update_parms.append(parm) + update_parms.append(parm_template) continue - templates.append(parm) + templates.append(parm_template) parm_group = node.parmTemplateGroup() parm_folder = parm_group.findFolder("Extra") From f93e27ac41fbcbe20e4cb1d14034ef781eaedbc4 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 18 Oct 2023 22:23:10 +0200 Subject: [PATCH 0409/1233] Simplify logic --- openpype/hosts/houdini/api/lib.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index a777d15581..7263a79e53 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -363,11 +363,10 @@ def imprint(node, data, update=False): parm_folder.setParmTemplates(templates) parm_group.append(parm_folder) else: + # Add to parm folder instance, then replace with updated one in group for template in templates: - parm_group.appendToFolder(parm_folder, template) - # this is needed because the pointer to folder - # is for some reason lost every call to `appendToFolder()` - parm_folder = parm_group.findFolder("Extra") + parm_folder.addParmTemplate(template) + parm_group.replace(parm_folder.name(), parm_folder) for parm_template in update_parms: parm_group.replace(parm_template.name(), parm_template) From b2a86e22d053986630fb7173ede638bc4ff8d57d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 18 Oct 2023 22:26:48 +0200 Subject: [PATCH 0410/1233] Simplify --- 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 7263a79e53..a91da396ec 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -342,7 +342,7 @@ def imprint(node, data, update=False): parm_template = get_template_from_value(key, value) if key in current_parms: - if node.evalParm(key) == data[key]: + if node.evalParm(key) == value: continue if not update: log.debug(f"{key} already exists on {node}") From 3506c810b17916ef93c12e84d69737773f1939d8 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 18 Oct 2023 22:31:49 +0200 Subject: [PATCH 0411/1233] Refactor variable names to refer to parm templates as opposed to parms + do nothing if both no new and no update parms to process --- openpype/hosts/houdini/api/lib.py | 42 ++++++++++++++++++------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index a91da396ec..3031e2d2bd 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -332,8 +332,8 @@ def imprint(node, data, update=False): return current_parms = {p.name(): p for p in node.spareParms()} - update_parms = [] - templates = [] + update_parm_templates = [] + new_parm_templates = [] for key, value in data.items(): if value is None: @@ -348,27 +348,35 @@ def imprint(node, data, update=False): log.debug(f"{key} already exists on {node}") else: log.debug(f"replacing {key}") - update_parms.append(parm_template) + update_parm_templates.append(parm_template) continue - templates.append(parm_template) + new_parm_templates.append(parm_template) + + if not new_parm_templates and not update_parm_templates: + return parm_group = node.parmTemplateGroup() - parm_folder = parm_group.findFolder("Extra") - # if folder doesn't exist yet, create one and append to it, - # else append to existing one - if not parm_folder: - parm_folder = hou.FolderParmTemplate("folder", "Extra") - parm_folder.setParmTemplates(templates) - parm_group.append(parm_folder) - else: - # Add to parm folder instance, then replace with updated one in group - for template in templates: - parm_folder.addParmTemplate(template) - parm_group.replace(parm_folder.name(), parm_folder) + # Add new parm templates + if new_parm_templates: + parm_folder = parm_group.findFolder("Extra") - for parm_template in update_parms: + # if folder doesn't exist yet, create one and append to it, + # else append to existing one + if not parm_folder: + parm_folder = hou.FolderParmTemplate("folder", "Extra") + parm_folder.setParmTemplates(new_parm_templates) + parm_group.append(parm_folder) + else: + # Add to parm template folder instance then replace with updated + # one in parm template group + for template in new_parm_templates: + parm_folder.addParmTemplate(template) + parm_group.replace(parm_folder.name(), parm_folder) + + # Update existing parm templates + for parm_template in update_parm_templates: parm_group.replace(parm_template.name(), parm_template) # When replacing a parm with a parm of the same name it preserves its From aee1ac1be9690a0bcf4eb4ea62a35342edd90dc8 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 19 Oct 2023 13:39:14 +0800 Subject: [PATCH 0412/1233] clean up the code in collector and extractor --- .../max/plugins/create/create_tycache.py | 22 ---------- .../publish/collect_tycache_attributes.py | 35 ++++++++-------- .../max/plugins/publish/extract_tycache.py | 41 ++++++++----------- .../plugins/publish/validate_tyflow_data.py | 2 +- 4 files changed, 33 insertions(+), 67 deletions(-) diff --git a/openpype/hosts/max/plugins/create/create_tycache.py b/openpype/hosts/max/plugins/create/create_tycache.py index c48094028a..92d12e012f 100644 --- a/openpype/hosts/max/plugins/create/create_tycache.py +++ b/openpype/hosts/max/plugins/create/create_tycache.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- """Creator plugin for creating TyCache.""" from openpype.hosts.max.api import plugin -from openpype.lib import EnumDef class CreateTyCache(plugin.MaxCreator): @@ -10,24 +9,3 @@ class CreateTyCache(plugin.MaxCreator): label = "TyCache" family = "tycache" icon = "gear" - - def create(self, subset_name, instance_data, pre_create_data): - instance_data["tycache_type"] = pre_create_data.get( - "tycache_type") - super(CreateTyCache, self).create( - subset_name, - instance_data, - pre_create_data) - - def get_pre_create_attr_defs(self): - attrs = super(CreateTyCache, self).get_pre_create_attr_defs() - - tycache_format_enum = ["tycache", "tycachespline"] - - return attrs + [ - - EnumDef("tycache_type", - tycache_format_enum, - default="tycache", - label="TyCache Type") - ] diff --git a/openpype/hosts/max/plugins/publish/collect_tycache_attributes.py b/openpype/hosts/max/plugins/publish/collect_tycache_attributes.py index fa27a9a9d6..779b4c1b7e 100644 --- a/openpype/hosts/max/plugins/publish/collect_tycache_attributes.py +++ b/openpype/hosts/max/plugins/publish/collect_tycache_attributes.py @@ -14,22 +14,19 @@ class CollectTyCacheData(pyblish.api.InstancePlugin, families = ["tycache"] def process(self, instance): - all_tyc_attributes_dict = {} attr_values = self.get_attr_values_from_data(instance.data) - tycache_boolean_attributes = attr_values.get("all_tyc_attrs") - if tycache_boolean_attributes: - for attrs in tycache_boolean_attributes: - all_tyc_attributes_dict[attrs] = True - tyc_layer_attr = attr_values.get("tycache_layer") - if tyc_layer_attr: - all_tyc_attributes_dict["tycacheLayer"] = ( - tyc_layer_attr) - tyc_objname_attr = attr_values.get("tycache_objname") - if tyc_objname_attr: - all_tyc_attributes_dict["tycache_objname"] = ( - tyc_objname_attr) + attributes = {} + for attr_key in attr_values.get("tycacheAttributes", []): + attributes[attr_key] = True + + for key in ["tycacheLayer", "tycacheObjectName"]: + attributes[key] = attr_values.get(key, "") + + # Collect the selected channel data before exporting + instance.data["tyc_attrs"] = attributes self.log.debug( - f"Found tycache attributes: {all_tyc_attributes_dict}") + f"Found tycache attributes: {attributes}" + ) @classmethod def get_attribute_defs(cls): @@ -63,17 +60,17 @@ class CollectTyCacheData(pyblish.api.InstancePlugin, "tycacheChanMaterials", "tycacheCreateObjectIfNotCreated"] return [ - EnumDef("all_tyc_attrs", + EnumDef("tycacheAttributes", tyc_attr_enum, default=tyc_default_attrs, multiselection=True, label="TyCache Attributes"), - TextDef("tycache_layer", + TextDef("tycacheLayer", label="TyCache Layer", tooltip="Name of tycache layer", - default=""), - TextDef("tycache_objname", + default="$(tyFlowLayer)"), + TextDef("tycacheObjectName", label="TyCache Object Name", tooltip="TyCache Object Name", - default="") + default="$(tyFlowName)_tyCache") ] diff --git a/openpype/hosts/max/plugins/publish/extract_tycache.py b/openpype/hosts/max/plugins/publish/extract_tycache.py index a787080776..9262219b7a 100644 --- a/openpype/hosts/max/plugins/publish/extract_tycache.py +++ b/openpype/hosts/max/plugins/publish/extract_tycache.py @@ -13,9 +13,9 @@ class ExtractTyCache(publish.Extractor): Notes: - TyCache only works for TyFlow Pro Plugin. - Args: - self.export_particle(): sets up all job arguments for attributes - to be exported in MAXscript + Methods: + self.get_export_particles_job_args(): sets up all job arguments + for attributes to be exported in MAXscript self.get_operators(): get the export_particle operator @@ -30,7 +30,7 @@ class ExtractTyCache(publish.Extractor): def process(self, instance): # TODO: let user decide the param - start = int(instance.context.data.get("frameStart")) + start = int(instance.context.data["frameStart"]) end = int(instance.context.data.get("frameEnd")) self.log.info("Extracting Tycache...") @@ -42,17 +42,11 @@ class ExtractTyCache(publish.Extractor): with maintained_selection(): job_args = None - has_tyc_spline = ( - True - if instance.data["tycache_type"] == "tycachespline" - else False - ) if instance.data["tycache_type"] == "tycache": - job_args = self.export_particle( + job_args = self.get_export_particles_job_args( instance.data["members"], start, end, path, - additional_attributes, - tycache_spline_enabled=has_tyc_spline) + additional_attributes) for job in job_args: rt.Execute(job) representations = instance.data.setdefault("representations", []) @@ -66,7 +60,7 @@ class ExtractTyCache(publish.Extractor): self.log.info(f"Extracted instance '{instance.name}' to: {filenames}") # Get the tyMesh filename for extraction - mesh_filename = "{}__tyMesh.tyc".format(instance.name) + mesh_filename = f"{instance.name}__tyMesh.tyc" mesh_repres = { 'name': 'tyMesh', 'ext': 'tyc', @@ -90,7 +84,7 @@ class ExtractTyCache(publish.Extractor): e.g. tycacheMain__tyPart_00000.tyc Args: - instance (str): instance. + instance (pyblish.api.Instance): instance. start_frame (int): Start frame. end_frame (int): End frame. @@ -101,13 +95,12 @@ class ExtractTyCache(publish.Extractor): filenames = [] # should we include frame 0 ? for frame in range(int(start_frame), int(end_frame) + 1): - filename = "{}__tyPart_{:05}.tyc".format(instance.name, frame) + filename = f"{instance.name}__tyPart_{frame:05}.tyc" filenames.append(filename) return filenames - def export_particle(self, members, start, end, - filepath, additional_attributes, - tycache_spline_enabled=False): + def get_export_particles_job_args(self, members, start, end, + filepath, additional_attributes): """Sets up all job arguments for attributes. Those attributes are to be exported in MAX Script. @@ -117,6 +110,8 @@ class ExtractTyCache(publish.Extractor): start (int): Start frame. end (int): End frame. filepath (str): Output path of the TyCache file. + additional_attributes (dict): channel attributes data + which needed to be exported Returns: list of arguments for MAX Script. @@ -125,12 +120,7 @@ class ExtractTyCache(publish.Extractor): job_args = [] opt_list = self.get_operators(members) for operator in opt_list: - if tycache_spline_enabled: - export_mode = f'{operator}.exportMode=3' - job_args.append(export_mode) - else: - export_mode = f'{operator}.exportMode=2' - job_args.append(export_mode) + job_args.append(f"{operator}.exportMode=2") start_frame = f"{operator}.frameStart={start}" job_args.append(start_frame) end_frame = f"{operator}.frameEnd={end}" @@ -192,6 +182,7 @@ class ExtractTyCache(publish.Extractor): if isinstance(value, bool): tyc_attribute = f"{operator}.{key}=True" elif isinstance(value, str): - tyc_attribute = f"{operator}.{key}={value}" + tyc_attribute = f'{operator}.{key}="{value}"' additional_args.append(tyc_attribute) + self.log.debug(additional_args) return additional_args diff --git a/openpype/hosts/max/plugins/publish/validate_tyflow_data.py b/openpype/hosts/max/plugins/publish/validate_tyflow_data.py index a359100e6e..4b2bf975ee 100644 --- a/openpype/hosts/max/plugins/publish/validate_tyflow_data.py +++ b/openpype/hosts/max/plugins/publish/validate_tyflow_data.py @@ -40,7 +40,7 @@ class ValidateTyFlowData(pyblish.api.InstancePlugin): and editable mesh(es) Args: - instance (str): instance node + instance (pyblish.api.Instance): instance Returns: invalid(list): list of invalid nodes which are not From 96cc3dd778d3b1b7f3c4aba01f5b2c64ee290b6c Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 19 Oct 2023 13:45:50 +0800 Subject: [PATCH 0413/1233] clean up the code in the extractor --- openpype/hosts/max/plugins/publish/extract_tycache.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_tycache.py b/openpype/hosts/max/plugins/publish/extract_tycache.py index 9262219b7a..33dde03667 100644 --- a/openpype/hosts/max/plugins/publish/extract_tycache.py +++ b/openpype/hosts/max/plugins/publish/extract_tycache.py @@ -41,12 +41,10 @@ class ExtractTyCache(publish.Extractor): additional_attributes = instance.data.get("tyc_attrs", {}) with maintained_selection(): - job_args = None - if instance.data["tycache_type"] == "tycache": - job_args = self.get_export_particles_job_args( - instance.data["members"], - start, end, path, - additional_attributes) + job_args = self.get_export_particles_job_args( + instance.data["members"], + start, end, path, + additional_attributes) for job in job_args: rt.Execute(job) representations = instance.data.setdefault("representations", []) From 3797ad5bb7cf2cfc578534e423ac956c5994a0a5 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Thu, 19 Oct 2023 11:14:05 +0300 Subject: [PATCH 0414/1233] BigRoy's comments - better logging and removing unnecessary logic --- openpype/hosts/houdini/api/lib.py | 17 ++++------ .../publish/collect_rop_frame_range.py | 34 +++++++++---------- 2 files changed, 23 insertions(+), 28 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 58be3c3836..70bc107d7f 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -559,7 +559,7 @@ def get_template_from_value(key, value): return parm -def get_frame_data(node, asset_data=None, log=None): +def get_frame_data(node, handle_start=0, handle_end=0, log=None): """Get the frame data: start frame, end frame and steps. Args: @@ -569,8 +569,6 @@ def get_frame_data(node, asset_data=None, log=None): dict: frame data for start, end and steps. """ - if asset_data is None: - asset_data = {} if log is None: log = self.log @@ -587,21 +585,20 @@ def get_frame_data(node, asset_data=None, log=None): data["frameStartHandle"] = hou.intFrame() data["frameEndHandle"] = hou.intFrame() data["byFrameStep"] = 1.0 - data["handleStart"] = 0 - data["handleEnd"] = 0 + log.info( - "Node '{}' has 'Render current frame' set. \n" - "Asset Handles are ignored. \n" + "Node '{}' has 'Render current frame' set.\n" + "Asset Handles are ignored.\n" "frameStart and frameEnd are set to the " - "current frame".format(node.path()) + "current frame.".format(node.path()) ) else: data["frameStartHandle"] = int(node.evalParm("f1")) data["frameEndHandle"] = int(node.evalParm("f2")) data["byFrameStep"] = node.evalParm("f3") - data["handleStart"] = asset_data.get("handleStart", 0) - data["handleEnd"] = asset_data.get("handleEnd", 0) + data["handleStart"] = handle_start + data["handleEnd"] = handle_end data["frameStart"] = data["frameStartHandle"] + data["handleStart"] data["frameEnd"] = data["frameEndHandle"] - data["handleEnd"] diff --git a/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py b/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py index 91d5a5ef74..f34b1faa77 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py +++ b/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py @@ -4,11 +4,11 @@ import hou # noqa import pyblish.api from openpype.lib import BoolDef from openpype.hosts.houdini.api import lib -from openpype.pipeline import OptionalPyblishPluginMixin +from openpype.pipeline import OpenPypePyblishPluginMixin class CollectRopFrameRange(pyblish.api.InstancePlugin, - OptionalPyblishPluginMixin): + OpenPypePyblishPluginMixin): """Collect all frames which would be saved from the ROP nodes""" @@ -28,14 +28,20 @@ class CollectRopFrameRange(pyblish.api.InstancePlugin, return ropnode = hou.node(node_path) - asset_data = instance.context.data["assetEntity"]["data"] attr_values = self.get_attr_values_from_data(instance.data) - if not attr_values.get("use_handles"): - asset_data["handleStart"] = 0 - asset_data["handleEnd"] = 0 - frame_data = lib.get_frame_data(ropnode, asset_data, self.log) + if attr_values.get("use_handles", self.use_asset_handles): + asset_data = instance.context.data["assetEntity"]["data"] + handle_start = asset_data.get("handleStart", 0) + handle_end = asset_data.get("handleEnd", 0) + else: + handle_start = 0 + handle_end = 0 + + frame_data = lib.get_frame_data( + ropnode, handle_start, handle_end, self.log + ) if not frame_data: return @@ -47,26 +53,18 @@ class CollectRopFrameRange(pyblish.api.InstancePlugin, if attr_values.get("use_handles"): self.log.info( "Full Frame range with Handles " - "[{frame_start_handle} - {frame_end_handle}]\n" + "[{frame_start_handle} - {frame_end_handle}]" .format( frame_start_handle=frame_data["frameStartHandle"], frame_end_handle=frame_data["frameEndHandle"] ) ) else: - self.log.info( + self.log.debug( "Use handles is deactivated for this instance, " - "start and end handles are set to 0.\n" + "start and end handles are set to 0." ) - self.log.info( - "Frame range [{frame_start} - {frame_end}]" - .format( - frame_start=frame_start, - frame_end=frame_end - ) - ) - if frame_data.get("byFrameStep", 1.0) != 1.0: self.log.info("Frame steps {}".format(frame_data["byFrameStep"])) From e3111239a0e3e0b6489f1d9daac546e49ba0c849 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 19 Oct 2023 10:48:57 +0200 Subject: [PATCH 0415/1233] trigger 'selection_changed' signal to propagate selection change (#5793) --- openpype/tools/publisher/widgets/card_view_widgets.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/tools/publisher/widgets/card_view_widgets.py b/openpype/tools/publisher/widgets/card_view_widgets.py index eae8e0420a..5cdd429cd4 100644 --- a/openpype/tools/publisher/widgets/card_view_widgets.py +++ b/openpype/tools/publisher/widgets/card_view_widgets.py @@ -797,6 +797,7 @@ class InstanceCardView(AbstractInstanceView): widget.set_active(value) else: self._select_item_clear(instance_id, group_name, instance_widget) + self.selection_changed.emit() self.active_changed.emit() def _on_widget_selection(self, instance_id, group_name, selection_type): From e17ffd1c3d01a42f2655791d2263edf516393f04 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Thu, 19 Oct 2023 12:05:49 +0300 Subject: [PATCH 0416/1233] BigRoy's comment - better logging --- .../plugins/publish/collect_rop_frame_range.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py b/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py index f34b1faa77..875dd2da8a 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py +++ b/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py @@ -51,7 +51,7 @@ class CollectRopFrameRange(pyblish.api.InstancePlugin, frame_end = frame_data["frameEnd"] if attr_values.get("use_handles"): - self.log.info( + self.log.debug( "Full Frame range with Handles " "[{frame_start_handle} - {frame_end_handle}]" .format( @@ -65,6 +65,17 @@ class CollectRopFrameRange(pyblish.api.InstancePlugin, "start and end handles are set to 0." ) + message = "Frame range [{frame_start} - {frame_end}]".format( + frame_start=frame_start, + frame_end=frame_end + ) + if handle_start or handle_end: + message += " with handles [{handle_start}]-[{handle_end}]".format( + handle_start=handle_start, + handle_end=handle_end + ) + self.log.info(message) + if frame_data.get("byFrameStep", 1.0) != 1.0: self.log.info("Frame steps {}".format(frame_data["byFrameStep"])) From b3078ed40f737b3b191898f34303df3b632de8e4 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Thu, 19 Oct 2023 12:08:00 +0300 Subject: [PATCH 0417/1233] resolve hound --- .../houdini/plugins/publish/collect_rop_frame_range.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py b/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py index 875dd2da8a..6a1871afdc 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py +++ b/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py @@ -66,9 +66,9 @@ class CollectRopFrameRange(pyblish.api.InstancePlugin, ) message = "Frame range [{frame_start} - {frame_end}]".format( - frame_start=frame_start, - frame_end=frame_end - ) + frame_start=frame_start, + frame_end=frame_end + ) if handle_start or handle_end: message += " with handles [{handle_start}]-[{handle_end}]".format( handle_start=handle_start, From cd11029665ad96f6d42b260d56b8d165b4aed299 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 19 Oct 2023 11:09:00 +0200 Subject: [PATCH 0418/1233] do not trigger value change signal when hiding multiselection label --- openpype/tools/attribute_defs/widgets.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/tools/attribute_defs/widgets.py b/openpype/tools/attribute_defs/widgets.py index 738f036b80..e05db6bed0 100644 --- a/openpype/tools/attribute_defs/widgets.py +++ b/openpype/tools/attribute_defs/widgets.py @@ -347,7 +347,9 @@ class NumberAttrWidget(_BaseAttrDefWidget): value = self.attr_def.default else: value = self._last_multivalue + self._input_widget.blockSignals(True) self._input_widget.setValue(value) + self._input_widget.blockSignals(False) if not change_focus: return # Change focus to input field and move cursor to the end From e3a8050ced649fb8935190ec25cd265cda507fd5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 19 Oct 2023 11:10:11 +0200 Subject: [PATCH 0419/1233] show multiselection label back on lost focus --- openpype/tools/attribute_defs/widgets.py | 28 +++++++++++++++++++----- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/openpype/tools/attribute_defs/widgets.py b/openpype/tools/attribute_defs/widgets.py index e05db6bed0..46f8da317d 100644 --- a/openpype/tools/attribute_defs/widgets.py +++ b/openpype/tools/attribute_defs/widgets.py @@ -295,6 +295,8 @@ class NumberAttrWidget(_BaseAttrDefWidget): input_widget.setButtonSymbols( QtWidgets.QAbstractSpinBox.ButtonSymbols.NoButtons ) + input_line_edit = input_widget.lineEdit() + input_widget.installEventFilter(self) multisel_widget = ClickableLineEdit("< Multiselection >", self) @@ -302,12 +304,23 @@ class NumberAttrWidget(_BaseAttrDefWidget): multisel_widget.clicked.connect(self._on_multi_click) self._input_widget = input_widget + self._input_line_edit = input_line_edit self._multisel_widget = multisel_widget self._last_multivalue = None + self._multivalue = False self.main_layout.addWidget(input_widget, 0) self.main_layout.addWidget(multisel_widget, 0) + def eventFilter(self, obj, event): + if ( + self._multivalue + and obj is self._input_widget + and event.type() == QtCore.QEvent.FocusOut + ): + self._set_multiselection_visible(True) + return False + def current_value(self): return self._input_widget.value() @@ -322,21 +335,24 @@ class NumberAttrWidget(_BaseAttrDefWidget): if len(set_value) > 1: self._last_multivalue = next(iter(set_value), None) self._set_multiselection_visible(True) + self._multivalue = True return value = tuple(set_value)[0] - self._set_multiselection_visible(False, False) + self._multivalue = False + self._set_multiselection_visible(False) if self.current_value != value: self._input_widget.setValue(value) def _on_value_change(self, new_value): + self._multivalue = False self.value_changed.emit(new_value, self.attr_def.id) def _on_multi_click(self): - self._set_multiselection_visible(False) + self._set_multiselection_visible(False, True) - def _set_multiselection_visible(self, visible, change_focus=True): + def _set_multiselection_visible(self, visible, change_focus=False): self._input_widget.setVisible(not visible) self._multisel_widget.setVisible(visible) if visible: @@ -354,9 +370,9 @@ class NumberAttrWidget(_BaseAttrDefWidget): return # Change focus to input field and move cursor to the end self._input_widget.setFocus(QtCore.Qt.MouseFocusReason) - line_edit = self._input_widget.lineEdit() - if line_edit is not None: - line_edit.setCursorPosition(len(line_edit.text())) + self._input_line_edit.setCursorPosition( + len(self._input_line_edit.text()) + ) class TextAttrWidget(_BaseAttrDefWidget): From 63e983412cc2e438153c093539a679c76486c0b9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 19 Oct 2023 11:10:19 +0200 Subject: [PATCH 0420/1233] removed unused import --- openpype/tools/attribute_defs/widgets.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/tools/attribute_defs/widgets.py b/openpype/tools/attribute_defs/widgets.py index 46f8da317d..91b5b229de 100644 --- a/openpype/tools/attribute_defs/widgets.py +++ b/openpype/tools/attribute_defs/widgets.py @@ -20,7 +20,6 @@ from openpype.tools.utils import ( FocusSpinBox, FocusDoubleSpinBox, MultiSelectionComboBox, - ClickableFrame, ) from openpype.widgets.nice_checkbox import NiceCheckbox From 4418d1116477add6da68e33abeca492c669bba73 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 19 Oct 2023 11:18:59 +0100 Subject: [PATCH 0421/1233] Code improvements from suggestions Co-authored-by: Roy Nieterau --- .../hosts/blender/plugins/load/load_abc.py | 21 +++++++------------ .../plugins/publish/collect_instances.py | 2 +- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/openpype/hosts/blender/plugins/load/load_abc.py b/openpype/hosts/blender/plugins/load/load_abc.py index 531a820436..af28cff7fe 100644 --- a/openpype/hosts/blender/plugins/load/load_abc.py +++ b/openpype/hosts/blender/plugins/load/load_abc.py @@ -60,19 +60,14 @@ class CacheModelLoader(plugin.AssetLoader): imported = lib.get_selection() - empties = [obj for obj in imported if obj.type == 'EMPTY'] - - container = None - - for empty in empties: - if not empty.parent: - container = empty - break + # Use first EMPTY without parent as container + container = next( + (obj for obj in imported if obj.type == "EMPTY" and not obj.parent), + None + ) objects = [] if container: - # Children must be linked before parents, - # otherwise the hierarchy will break nodes = list(container.children) for obj in nodes: @@ -80,11 +75,9 @@ class CacheModelLoader(plugin.AssetLoader): bpy.data.objects.remove(container) + objects.extend(nodes) for obj in nodes: - objects.append(obj) - objects.extend(list(obj.children_recursive)) - - objects.reverse() + objects.extend(obj.children_recursive) else: for obj in imported: obj.parent = asset_group diff --git a/openpype/hosts/blender/plugins/publish/collect_instances.py b/openpype/hosts/blender/plugins/publish/collect_instances.py index c95d718187..ad2ce54147 100644 --- a/openpype/hosts/blender/plugins/publish/collect_instances.py +++ b/openpype/hosts/blender/plugins/publish/collect_instances.py @@ -51,7 +51,7 @@ class CollectInstances(pyblish.api.ContextPlugin): for group in asset_groups: instance = self.create_instance(context, group) members = [] - if type(group) == bpy.types.Collection: + if isinstance(group, bpy.types.Collection): members = list(group.objects) family = instance.data["family"] if family == "animation": From 0b69ce120a23cb2393f4a281030b8696d357d780 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 19 Oct 2023 11:21:13 +0100 Subject: [PATCH 0422/1233] Hound fixes --- openpype/hosts/blender/plugins/load/load_abc.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/blender/plugins/load/load_abc.py b/openpype/hosts/blender/plugins/load/load_abc.py index af28cff7fe..73f08fcc98 100644 --- a/openpype/hosts/blender/plugins/load/load_abc.py +++ b/openpype/hosts/blender/plugins/load/load_abc.py @@ -62,7 +62,8 @@ class CacheModelLoader(plugin.AssetLoader): # Use first EMPTY without parent as container container = next( - (obj for obj in imported if obj.type == "EMPTY" and not obj.parent), + (obj for obj in imported + if obj.type == "EMPTY" and not obj.parent), None ) From 2c460ed64701a51812d02f67e27529978f8b4ad4 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 19 Oct 2023 18:23:55 +0800 Subject: [PATCH 0423/1233] move functions to preview_animation.py --- openpype/hosts/max/api/lib.py | 136 +++++--- openpype/hosts/max/api/preview_animation.py | 306 ++++++++++++++++++ .../max/plugins/publish/collect_review.py | 15 +- .../publish/extract_review_animation.py | 8 +- 4 files changed, 406 insertions(+), 59 deletions(-) create mode 100644 openpype/hosts/max/api/preview_animation.py diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index eb6cd3cabc..5e55daceb2 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -568,7 +568,7 @@ def get_plugins() -> list: def publish_review_animation(instance, staging_dir, start, - end, ext, fps): + end, ext, fps, viewport_options): """Function to set up preview arguments in MaxScript. ****For 3dsMax 2024+ @@ -578,6 +578,7 @@ def publish_review_animation(instance, staging_dir, start, start (int): startFrame end (int): endFrame fps (float): fps value + viewport_options (dict): viewport setting options Returns: list: job arguments @@ -590,48 +591,34 @@ def publish_review_animation(instance, staging_dir, start, job_args.append(default_option) frame_option = f"outputAVI:false start:{start} end:{end} fps:{fps}" # noqa job_args.append(frame_option) - options = [ - "percentSize", "dspGeometry", "dspShapes", - "dspLights", "dspCameras", "dspHelpers", "dspParticles", - "dspBones", "dspBkg", "dspGrid", "dspSafeFrame", "dspFrameNums" - ] - for key in options: - enabled = instance.data.get(key) - if enabled: - job_args.append(f"{key}:{enabled}") + for key, value in viewport_options.items(): + if isinstance(value, bool): + if value: + job_args.append(f"{key}:{value}") - visual_style_preset = instance.data.get("visualStyleMode") - if visual_style_preset == "Realistic": - visual_style_preset = "defaultshading" - elif visual_style_preset == "Shaded": - visual_style_preset = "defaultshading" - log.warning( - "'Shaded' Mode not supported in " - "preview animation in Max 2024..\n\n" - "Using 'defaultshading' instead") - - elif visual_style_preset == "ConsistentColors": - visual_style_preset = "flatcolor" - else: - visual_style_preset = visual_style_preset.lower() - # new argument exposed for Max 2024 for visual style - visual_style_option = f"vpStyle:#{visual_style_preset}" - job_args.append(visual_style_option) - # new argument for pre-view preset exposed in Max 2024 - preview_preset = instance.data.get("viewportPreset") - if preview_preset == "Quality": - preview_preset = "highquality" - elif preview_preset == "Customize": - preview_preset = "userdefined" - else: - preview_preset = preview_preset.lower() - preview_preset_option = f"vpPreset:#{preview_preset}" - job_args.append(preview_preset_option) - viewport_texture = instance.data.get("vpTexture", True) - if viewport_texture: - viewport_texture_option = f"vpTexture:{viewport_texture}" - job_args.append(viewport_texture_option) + elif isinstance(value, str): + if key == "vpStyle": + if viewport_options[key] == "Realistic": + value = "defaultshading" + elif viewport_options[key] == "Shaded": + log.warning( + "'Shaded' Mode not supported in " + "preview animation in Max 2024..\n" + "Using 'defaultshading' instead") + value = "defaultshading" + elif viewport_options[key] == "ConsistentColors": + value = "flatcolor" + else: + value = value.lower() + elif key == "vpPreset": + if viewport_options[key] == "Quality": + value = "highquality" + elif viewport_options[key] == "Customize": + value = "userdefined" + else: + value = value.lower() + job_args.append(f"{key}: #{value}") auto_play_option = "autoPlay:false" job_args.append(auto_play_option) @@ -704,29 +691,34 @@ def publish_preview_sequences(staging_dir, filename, def publish_preview_animation( instance, staging_dir, ext, review_camera, - startFrame=None, endFrame=None): + startFrame=None, endFrame=None, + viewport_options=None): """Render camera review animation Args: instance (pyblish.api.instance): Instance filepath (str): filepath + review_camera (str): viewport camera for preview render startFrame (int): start frame endFrame (int): end frame - review_camera (str): viewport camera for preview render + viewport_options (dict): viewport setting options """ + if startFrame is None: startFrame = int(rt.animationRange.start) if endFrame is None: endFrame = int(rt.animationRange.end) + if viewport_options is None: + viewport_options = viewport_options_for_preview_animation() with play_preview_when_done(False): with viewport_camera(review_camera): if int(get_max_version()) < 2024: with viewport_preference_setting( - instance.data["general_viewport"], - instance.data["nitrous_viewport"], - instance.data["vp_btn_mgr"]): - percentSize = instance.data.get("percentSize") - rt.completeRedraw() + viewport_options["general_viewport"], + viewport_options["nitrous_viewport"], + viewport_options["vp_btn_mgr"]): + percentSize = viewport_options.get("percentSize", 100) + publish_preview_sequences( staging_dir, instance.name, startFrame, endFrame, percentSize, ext) @@ -735,5 +727,51 @@ def publish_preview_animation( rt.completeRedraw() preview_arg = publish_review_animation(instance, staging_dir, startFrame, endFrame, - ext, fps) + ext, fps, viewport_options) rt.execute(preview_arg) + + rt.completeRedraw() + +def viewport_options_for_preview_animation(): + """ + Function to store the default data of viewport options + Returns: + dict: viewport setting options + + """ + # viewport_options should be the dictionary + if int(get_max_version()) < 2024: + return { + "visualStyleMode": "defaultshading", + "viewportPreset": "highquality", + "percentSize": 100, + "vpTexture": False, + "dspGeometry": True, + "dspShapes": False, + "dspLights": False, + "dspCameras": False, + "dspHelpers": False, + "dspParticles": True, + "dspBones": False, + "dspBkg": True, + "dspGrid": False, + "dspSafeFrame":False, + "dspFrameNums": False + } + else: + viewport_options = {} + viewport_options.update({"percentSize": 100}) + general_viewport = { + "dspBkg": True, + "dspGrid": False + } + nitrous_viewport = { + "VisualStyleMode": "defaultshading", + "ViewportPreset": "highquality", + "UseTextureEnabled": False + } + viewport_options["general_viewport"] = general_viewport + viewport_options["nitrous_viewport"] = nitrous_viewport + viewport_options["vp_btn_mgr"] = { + "EnableButtons": False} + return viewport_options diff --git a/openpype/hosts/max/api/preview_animation.py b/openpype/hosts/max/api/preview_animation.py new file mode 100644 index 0000000000..bb2f1410d4 --- /dev/null +++ b/openpype/hosts/max/api/preview_animation.py @@ -0,0 +1,306 @@ +import os +import logging +import contextlib +from pymxs import runtime as rt +from .lib import get_max_version + +log = logging.getLogger("openpype.hosts.max") + + +@contextlib.contextmanager +def play_preview_when_done(has_autoplay): + """Function to set preview playback option during + context + + Args: + has_autoplay (bool): autoplay during creating + preview animation + """ + current_playback = rt.preferences.playPreviewWhenDone + try: + rt.preferences.playPreviewWhenDone = has_autoplay + yield + finally: + rt.preferences.playPreviewWhenDone = current_playback + + +@contextlib.contextmanager +def viewport_camera(camera): + """Function to set viewport camera during context + ***For 3dsMax 2024+ + Args: + camera (str): viewport camera for review render + """ + original = rt.viewport.getCamera() + has_autoplay = rt.preferences.playPreviewWhenDone + nitrousGraphicMgr = rt.NitrousGraphicsManager + viewport_setting = nitrousGraphicMgr.GetActiveViewportSetting() + orig_preset = viewport_setting.ViewportPreset + if not original: + # if there is no original camera + # use the current camera as original + original = rt.getNodeByName(camera) + review_camera = rt.getNodeByName(camera) + try: + rt.viewport.setCamera(review_camera) + rt.preferences.playPreviewWhenDone = False + yield + finally: + rt.viewport.setCamera(original) + rt.preferences.playPreviewWhenDone = has_autoplay + viewport_setting.ViewportPreset = orig_preset + + +@contextlib.contextmanager +def viewport_preference_setting(general_viewport, + nitrous_viewport, + vp_button_mgr): + """Function to set viewport setting during context + ***For Max Version < 2024 + Args: + camera (str): Viewport camera for review render + general_viewport (dict): General viewport setting + nitrous_viewport (dict): Nitrous setting for + preview animation + vp_button_mgr (dict): Viewport button manager Setting + preview_preferences (dict): Preview Preferences Setting + """ + orig_vp_grid = rt.viewport.getGridVisibility(1) + orig_vp_bkg = rt.viewport.IsSolidBackgroundColorMode() + + nitrousGraphicMgr = rt.NitrousGraphicsManager + viewport_setting = nitrousGraphicMgr.GetActiveViewportSetting() + vp_button_mgr_original = { + key: getattr(rt.ViewportButtonMgr, key) for key in vp_button_mgr + } + nitrous_viewport_original = { + key: getattr(viewport_setting, key) for key in nitrous_viewport + } + + try: + rt.viewport.setGridVisibility(1, general_viewport["dspGrid"]) + rt.viewport.EnableSolidBackgroundColorMode(general_viewport["dspBkg"]) + for key, value in vp_button_mgr.items(): + setattr(rt.ViewportButtonMgr, key, value) + for key, value in nitrous_viewport.items(): + if nitrous_viewport[key] != nitrous_viewport_original[key]: + setattr(viewport_setting, key, value) + yield + + finally: + rt.viewport.setGridVisibility(1, orig_vp_grid) + rt.viewport.EnableSolidBackgroundColorMode(orig_vp_bkg) + for key, value in vp_button_mgr_original.items(): + setattr(rt.ViewportButtonMgr, key, value) + for key, value in nitrous_viewport_original.items(): + setattr(viewport_setting, key, value) + +def publish_review_animation(instance, staging_dir, start, + end, ext, fps, viewport_options): + """Function to set up preview arguments in MaxScript. + ****For 3dsMax 2024+ + + Args: + instance (str): instance + filepath (str): output of the preview animation + start (int): startFrame + end (int): endFrame + fps (float): fps value + viewport_options (dict): viewport setting options + + Returns: + list: job arguments + """ + job_args = list() + filename = "{0}..{1}".format(instance.name, ext) + filepath = os.path.join(staging_dir, filename) + filepath = filepath.replace("\\", "/") + default_option = f'CreatePreview filename:"{filepath}"' + job_args.append(default_option) + frame_option = f"outputAVI:false start:{start} end:{end} fps:{fps}" # noqa + job_args.append(frame_option) + + for key, value in viewport_options.items(): + if isinstance(value, bool): + if value: + job_args.append(f"{key}:{value}") + + elif isinstance(value, str): + if key == "vpStyle": + if viewport_options[key] == "Realistic": + value = "defaultshading" + elif viewport_options[key] == "Shaded": + log.warning( + "'Shaded' Mode not supported in " + "preview animation in Max 2024..\n" + "Using 'defaultshading' instead") + value = "defaultshading" + elif viewport_options[key] == "ConsistentColors": + value = "flatcolor" + else: + value = value.lower() + elif key == "vpPreset": + if viewport_options[key] == "Quality": + value = "highquality" + elif viewport_options[key] == "Customize": + value = "userdefined" + else: + value = value.lower() + job_args.append(f"{key}: #{value}") + + auto_play_option = "autoPlay:false" + job_args.append(auto_play_option) + + job_str = " ".join(job_args) + log.debug(job_str) + + return job_str + + +def publish_preview_sequences(staging_dir, filename, + startFrame, endFrame, + percentSize, ext): + """publish preview animation by creating bitmaps + ***For 3dsMax Version <2024 + + Args: + staging_dir (str): staging directory + filename (str): filename + startFrame (int): start frame + endFrame (int): end frame + percentSize (int): percentage of the resolution + ext (str): image extension + """ + # get the screenshot + resolution_percentage = float(percentSize) / 100 + res_width = rt.renderWidth * resolution_percentage + res_height = rt.renderHeight * resolution_percentage + + viewportRatio = float(res_width / res_height) + + for i in range(startFrame, endFrame + 1): + rt.sliderTime = i + fname = "{}.{:04}.{}".format(filename, i, ext) + filepath = os.path.join(staging_dir, fname) + filepath = filepath.replace("\\", "/") + preview_res = rt.bitmap( + res_width, res_height, filename=filepath) + dib = rt.gw.getViewportDib() + dib_width = float(dib.width) + dib_height = float(dib.height) + renderRatio = float(dib_width / dib_height) + if viewportRatio <= renderRatio: + heightCrop = (dib_width / renderRatio) + topEdge = int((dib_height - heightCrop) / 2.0) + tempImage_bmp = rt.bitmap(dib_width, heightCrop) + src_box_value = rt.Box2(0, topEdge, dib_width, heightCrop) + else: + widthCrop = dib_height * renderRatio + leftEdge = int((dib_width - widthCrop) / 2.0) + tempImage_bmp = rt.bitmap(widthCrop, dib_height) + src_box_value = rt.Box2(0, leftEdge, dib_width, dib_height) + rt.pasteBitmap(dib, tempImage_bmp, src_box_value, rt.Point2(0, 0)) + + # copy the bitmap and close it + rt.copy(tempImage_bmp, preview_res) + rt.close(tempImage_bmp) + + rt.save(preview_res) + rt.close(preview_res) + + rt.close(dib) + + if rt.keyboard.escPressed: + rt.exit() + # clean up the cache + rt.gc(delayed=True) + + +def publish_preview_animation( + instance, staging_dir, + ext, review_camera, + startFrame=None, endFrame=None, + viewport_options=None): + """Render camera review animation + + Args: + instance (pyblish.api.instance): Instance + filepath (str): filepath + review_camera (str): viewport camera for preview render + startFrame (int): start frame + endFrame (int): end frame + viewport_options (dict): viewport setting options + """ + + if startFrame is None: + startFrame = int(rt.animationRange.start) + if endFrame is None: + endFrame = int(rt.animationRange.end) + if viewport_options is None: + viewport_options = viewport_options_for_preview_animation() + with play_preview_when_done(False): + with viewport_camera(review_camera): + if int(get_max_version()) < 2024: + with viewport_preference_setting( + viewport_options["general_viewport"], + viewport_options["nitrous_viewport"], + viewport_options["vp_btn_mgr"]): + percentSize = viewport_options.get("percentSize", 100) + + publish_preview_sequences( + staging_dir, instance.name, + startFrame, endFrame, percentSize, ext) + else: + fps = instance.data["fps"] + rt.completeRedraw() + preview_arg = publish_review_animation(instance, staging_dir, + startFrame, endFrame, + ext, fps, viewport_options) + rt.execute(preview_arg) + + rt.completeRedraw() + + +def viewport_options_for_preview_animation(): + """ + Function to store the default data of viewport options + Returns: + dict: viewport setting options + + """ + # viewport_options should be the dictionary + if int(get_max_version()) < 2024: + return { + "visualStyleMode": "defaultshading", + "viewportPreset": "highquality", + "percentSize": 100, + "vpTexture": False, + "dspGeometry": True, + "dspShapes": False, + "dspLights": False, + "dspCameras": False, + "dspHelpers": False, + "dspParticles": True, + "dspBones": False, + "dspBkg": True, + "dspGrid": False, + "dspSafeFrame":False, + "dspFrameNums": False + } + else: + viewport_options = {} + viewport_options.update({"percentSize": 100}) + general_viewport = { + "dspBkg": True, + "dspGrid": False + } + nitrous_viewport = { + "VisualStyleMode": "defaultshading", + "ViewportPreset": "highquality", + "UseTextureEnabled": False + } + viewport_options["general_viewport"] = general_viewport + viewport_options["nitrous_viewport"] = nitrous_viewport + viewport_options["vp_btn_mgr"] = { + "EnableButtons": False} + return viewport_options diff --git a/openpype/hosts/max/plugins/publish/collect_review.py b/openpype/hosts/max/plugins/publish/collect_review.py index c3985a2ded..904a4eab0f 100644 --- a/openpype/hosts/max/plugins/publish/collect_review.py +++ b/openpype/hosts/max/plugins/publish/collect_review.py @@ -32,7 +32,6 @@ class CollectReview(pyblish.api.InstancePlugin, "review_camera": camera_name, "imageFormat": creator_attrs["imageFormat"], "keepImages": creator_attrs["keepImages"], - "percentSize": creator_attrs["percentSize"], "frameStart": instance.context.data["frameStart"], "frameEnd": instance.context.data["frameEnd"], "fps": instance.context.data["fps"] @@ -49,9 +48,10 @@ class CollectReview(pyblish.api.InstancePlugin, instance.data["colorspaceView"] = view_transform preview_data = { - "visualStyleMode": creator_attrs["visualStyleMode"], - "viewportPreset": creator_attrs["viewportPreset"], - "vpTexture": creator_attrs["vpTexture"], + "vpStyle": creator_attrs["visualStyleMode"], + "vpPreset": creator_attrs["viewportPreset"], + "percentSize": creator_attrs["percentSize"], + "vpTextures": creator_attrs["vpTexture"], "dspGeometry": attr_values.get("dspGeometry"), "dspShapes": attr_values.get("dspShapes"), "dspLights": attr_values.get("dspLights"), @@ -66,6 +66,8 @@ class CollectReview(pyblish.api.InstancePlugin, } else: preview_data = {} + preview_data.update({ + "percentSize": creator_attrs["percentSize"]}) general_viewport = { "dspBkg": attr_values.get("dspBkg"), "dspGrid": attr_values.get("dspGrid") @@ -80,9 +82,6 @@ class CollectReview(pyblish.api.InstancePlugin, preview_data["vp_btn_mgr"] = { "EnableButtons": False } - preview_data["preferences"] = { - "playPreviewWhenDone": False - } # Enable ftrack functionality instance.data.setdefault("families", []).append('ftrack') @@ -91,7 +90,7 @@ class CollectReview(pyblish.api.InstancePlugin, burnin_members["focalLength"] = focal_length instance.data.update(general_preview_data) - instance.data.update(preview_data) + instance.data["viewport_options"] = preview_data @classmethod def get_attribute_defs(cls): diff --git a/openpype/hosts/max/plugins/publish/extract_review_animation.py b/openpype/hosts/max/plugins/publish/extract_review_animation.py index d57ed44d65..d2de981236 100644 --- a/openpype/hosts/max/plugins/publish/extract_review_animation.py +++ b/openpype/hosts/max/plugins/publish/extract_review_animation.py @@ -1,7 +1,9 @@ import os import pyblish.api from openpype.pipeline import publish -from openpype.hosts.max.api.lib import publish_preview_animation +from openpype.hosts.max.api.preview_animation import ( + publish_preview_animation +) class ExtractReviewAnimation(publish.Extractor): @@ -30,10 +32,12 @@ class ExtractReviewAnimation(publish.Extractor): " '%s' to '%s'" % (filename, staging_dir)) review_camera = instance.data["review_camera"] + viewport_options = instance.data.get("viewport_options", {}) publish_preview_animation( instance, staging_dir, ext, review_camera, - startFrame=start, endFrame=end) + startFrame=start, endFrame=end, + viewport_options=viewport_options) tags = ["review"] if not instance.data.get("keepImages"): From 0aa5b59384992ffc9f97554a57c606436f34fb3c Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 19 Oct 2023 18:24:15 +0800 Subject: [PATCH 0424/1233] move functions to preview_animation.py --- openpype/hosts/max/api/lib.py | 292 ---------------------------------- 1 file changed, 292 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 5e55daceb2..166a66ce48 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -322,88 +322,6 @@ def is_headless(): return rt.maxops.isInNonInteractiveMode() -@contextlib.contextmanager -def viewport_camera(camera): - """Function to set viewport camera during context - ***For 3dsMax 2024+ - Args: - camera (str): viewport camera for review render - """ - original = rt.viewport.getCamera() - has_autoplay = rt.preferences.playPreviewWhenDone - nitrousGraphicMgr = rt.NitrousGraphicsManager - viewport_setting = nitrousGraphicMgr.GetActiveViewportSetting() - orig_preset = viewport_setting.ViewportPreset - if not original: - # if there is no original camera - # use the current camera as original - original = rt.getNodeByName(camera) - review_camera = rt.getNodeByName(camera) - try: - rt.viewport.setCamera(review_camera) - rt.preferences.playPreviewWhenDone = False - yield - finally: - rt.viewport.setCamera(original) - rt.preferences.playPreviewWhenDone = has_autoplay - viewport_setting.ViewportPreset = orig_preset - - -@contextlib.contextmanager -def play_preview_when_done(has_autoplay): - current_playback = rt.preferences.playPreviewWhenDone - try: - rt.preferences.playPreviewWhenDone = has_autoplay - yield - finally: - rt.preferences.playPreviewWhenDone = current_playback - - -@contextlib.contextmanager -def viewport_preference_setting(general_viewport, - nitrous_viewport, - vp_button_mgr): - """Function to set viewport setting during context - ***For Max Version < 2024 - Args: - camera (str): Viewport camera for review render - general_viewport (dict): General viewport setting - nitrous_viewport (dict): Nitrous setting for - preview animation - vp_button_mgr (dict): Viewport button manager Setting - preview_preferences (dict): Preview Preferences Setting - """ - orig_vp_grid = rt.viewport.getGridVisibility(1) - orig_vp_bkg = rt.viewport.IsSolidBackgroundColorMode() - - nitrousGraphicMgr = rt.NitrousGraphicsManager - viewport_setting = nitrousGraphicMgr.GetActiveViewportSetting() - vp_button_mgr_original = { - key: getattr(rt.ViewportButtonMgr, key) for key in vp_button_mgr - } - nitrous_viewport_original = { - key: getattr(viewport_setting, key) for key in nitrous_viewport - } - - try: - rt.viewport.setGridVisibility(1, general_viewport["dspGrid"]) - rt.viewport.EnableSolidBackgroundColorMode(general_viewport["dspBkg"]) - for key, value in vp_button_mgr.items(): - setattr(rt.ViewportButtonMgr, key, value) - for key, value in nitrous_viewport.items(): - if nitrous_viewport[key] != nitrous_viewport_original[key]: - setattr(viewport_setting, key, value) - yield - - finally: - rt.viewport.setGridVisibility(1, orig_vp_grid) - rt.viewport.EnableSolidBackgroundColorMode(orig_vp_bkg) - for key, value in vp_button_mgr_original.items(): - setattr(rt.ViewportButtonMgr, key, value) - for key, value in nitrous_viewport_original.items(): - setattr(viewport_setting, key, value) - - def set_timeline(frameStart, frameEnd): """Set frame range for timeline editor in Max """ @@ -565,213 +483,3 @@ def get_plugins() -> list: plugin_info_list.append(plugin_info) return plugin_info_list - - -def publish_review_animation(instance, staging_dir, start, - end, ext, fps, viewport_options): - """Function to set up preview arguments in MaxScript. - ****For 3dsMax 2024+ - - Args: - instance (str): instance - filepath (str): output of the preview animation - start (int): startFrame - end (int): endFrame - fps (float): fps value - viewport_options (dict): viewport setting options - - Returns: - list: job arguments - """ - job_args = list() - filename = "{0}..{1}".format(instance.name, ext) - filepath = os.path.join(staging_dir, filename) - filepath = filepath.replace("\\", "/") - default_option = f'CreatePreview filename:"{filepath}"' - job_args.append(default_option) - frame_option = f"outputAVI:false start:{start} end:{end} fps:{fps}" # noqa - job_args.append(frame_option) - - for key, value in viewport_options.items(): - if isinstance(value, bool): - if value: - job_args.append(f"{key}:{value}") - - elif isinstance(value, str): - if key == "vpStyle": - if viewport_options[key] == "Realistic": - value = "defaultshading" - elif viewport_options[key] == "Shaded": - log.warning( - "'Shaded' Mode not supported in " - "preview animation in Max 2024..\n" - "Using 'defaultshading' instead") - value = "defaultshading" - elif viewport_options[key] == "ConsistentColors": - value = "flatcolor" - else: - value = value.lower() - elif key == "vpPreset": - if viewport_options[key] == "Quality": - value = "highquality" - elif viewport_options[key] == "Customize": - value = "userdefined" - else: - value = value.lower() - job_args.append(f"{key}: #{value}") - - auto_play_option = "autoPlay:false" - job_args.append(auto_play_option) - - job_str = " ".join(job_args) - log.debug(job_str) - - return job_str - - -def publish_preview_sequences(staging_dir, filename, - startFrame, endFrame, - percentSize, ext): - """publish preview animation by creating bitmaps - ***For 3dsMax Version <2024 - - Args: - staging_dir (str): staging directory - filename (str): filename - startFrame (int): start frame - endFrame (int): end frame - percentSize (int): percentage of the resolution - ext (str): image extension - """ - # get the screenshot - resolution_percentage = float(percentSize) / 100 - res_width = rt.renderWidth * resolution_percentage - res_height = rt.renderHeight * resolution_percentage - - viewportRatio = float(res_width / res_height) - - for i in range(startFrame, endFrame + 1): - rt.sliderTime = i - fname = "{}.{:04}.{}".format(filename, i, ext) - filepath = os.path.join(staging_dir, fname) - filepath = filepath.replace("\\", "/") - preview_res = rt.bitmap( - res_width, res_height, filename=filepath) - dib = rt.gw.getViewportDib() - dib_width = float(dib.width) - dib_height = float(dib.height) - renderRatio = float(dib_width / dib_height) - if viewportRatio <= renderRatio: - heightCrop = (dib_width / renderRatio) - topEdge = int((dib_height - heightCrop) / 2.0) - tempImage_bmp = rt.bitmap(dib_width, heightCrop) - src_box_value = rt.Box2(0, topEdge, dib_width, heightCrop) - else: - widthCrop = dib_height * renderRatio - leftEdge = int((dib_width - widthCrop) / 2.0) - tempImage_bmp = rt.bitmap(widthCrop, dib_height) - src_box_value = rt.Box2(0, leftEdge, dib_width, dib_height) - rt.pasteBitmap(dib, tempImage_bmp, src_box_value, rt.Point2(0, 0)) - - # copy the bitmap and close it - rt.copy(tempImage_bmp, preview_res) - rt.close(tempImage_bmp) - - rt.save(preview_res) - rt.close(preview_res) - - rt.close(dib) - - if rt.keyboard.escPressed: - rt.exit() - # clean up the cache - rt.gc(delayed=True) - - -def publish_preview_animation( - instance, staging_dir, - ext, review_camera, - startFrame=None, endFrame=None, - viewport_options=None): - """Render camera review animation - - Args: - instance (pyblish.api.instance): Instance - filepath (str): filepath - review_camera (str): viewport camera for preview render - startFrame (int): start frame - endFrame (int): end frame - viewport_options (dict): viewport setting options - """ - - if startFrame is None: - startFrame = int(rt.animationRange.start) - if endFrame is None: - endFrame = int(rt.animationRange.end) - if viewport_options is None: - viewport_options = viewport_options_for_preview_animation() - with play_preview_when_done(False): - with viewport_camera(review_camera): - if int(get_max_version()) < 2024: - with viewport_preference_setting( - viewport_options["general_viewport"], - viewport_options["nitrous_viewport"], - viewport_options["vp_btn_mgr"]): - percentSize = viewport_options.get("percentSize", 100) - - publish_preview_sequences( - staging_dir, instance.name, - startFrame, endFrame, percentSize, ext) - else: - fps = instance.data["fps"] - rt.completeRedraw() - preview_arg = publish_review_animation(instance, staging_dir, - startFrame, endFrame, - ext, fps, viewport_options) - rt.execute(preview_arg) - - rt.completeRedraw() - -def viewport_options_for_preview_animation(): - """ - Function to store the default data of viewport options - Returns: - dict: viewport setting options - - """ - # viewport_options should be the dictionary - if int(get_max_version()) < 2024: - return { - "visualStyleMode": "defaultshading", - "viewportPreset": "highquality", - "percentSize": 100, - "vpTexture": False, - "dspGeometry": True, - "dspShapes": False, - "dspLights": False, - "dspCameras": False, - "dspHelpers": False, - "dspParticles": True, - "dspBones": False, - "dspBkg": True, - "dspGrid": False, - "dspSafeFrame":False, - "dspFrameNums": False - } - else: - viewport_options = {} - viewport_options.update({"percentSize": 100}) - general_viewport = { - "dspBkg": True, - "dspGrid": False - } - nitrous_viewport = { - "VisualStyleMode": "defaultshading", - "ViewportPreset": "highquality", - "UseTextureEnabled": False - } - viewport_options["general_viewport"] = general_viewport - viewport_options["nitrous_viewport"] = nitrous_viewport - viewport_options["vp_btn_mgr"] = { - "EnableButtons": False} - return viewport_options From c93c26462d9bda7bc87937d109c535691b06da37 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 19 Oct 2023 18:51:32 +0800 Subject: [PATCH 0425/1233] clean up the code for the tycache publishing --- .../hosts/max/plugins/load/load_tycache.py | 3 +- .../publish/collect_tycache_attributes.py | 2 +- .../max/plugins/publish/extract_tycache.py | 35 +++++++++---------- .../plugins/publish/validate_tyflow_data.py | 33 ++++++++--------- 4 files changed, 32 insertions(+), 41 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_tycache.py b/openpype/hosts/max/plugins/load/load_tycache.py index ff3a26fbd6..f878ed9f1c 100644 --- a/openpype/hosts/max/plugins/load/load_tycache.py +++ b/openpype/hosts/max/plugins/load/load_tycache.py @@ -46,8 +46,7 @@ class TyCacheLoader(load.LoaderPlugin): path = get_representation_path(representation) node = rt.GetNodeByName(container["instance_node"]) node_list = get_previous_loaded_object(node) - update_custom_attribute_data( - node, node_list) + update_custom_attribute_data(node, node_list) with maintained_selection(): for prt in node_list: prt.filename = path diff --git a/openpype/hosts/max/plugins/publish/collect_tycache_attributes.py b/openpype/hosts/max/plugins/publish/collect_tycache_attributes.py index 779b4c1b7e..0351ca45c5 100644 --- a/openpype/hosts/max/plugins/publish/collect_tycache_attributes.py +++ b/openpype/hosts/max/plugins/publish/collect_tycache_attributes.py @@ -6,7 +6,7 @@ from openpype.pipeline.publish import OpenPypePyblishPluginMixin class CollectTyCacheData(pyblish.api.InstancePlugin, OpenPypePyblishPluginMixin): - """Collect Review Data for Preview Animation""" + """Collect Channel Attributes for TyCache Export""" order = pyblish.api.CollectorOrder + 0.02 label = "Collect tyCache attribute Data" diff --git a/openpype/hosts/max/plugins/publish/extract_tycache.py b/openpype/hosts/max/plugins/publish/extract_tycache.py index 33dde03667..49721f47fe 100644 --- a/openpype/hosts/max/plugins/publish/extract_tycache.py +++ b/openpype/hosts/max/plugins/publish/extract_tycache.py @@ -115,26 +115,23 @@ class ExtractTyCache(publish.Extractor): list of arguments for MAX Script. """ - job_args = [] - opt_list = self.get_operators(members) - for operator in opt_list: - job_args.append(f"{operator}.exportMode=2") - start_frame = f"{operator}.frameStart={start}" - job_args.append(start_frame) - end_frame = f"{operator}.frameEnd={end}" - job_args.append(end_frame) - filepath = filepath.replace("\\", "/") - tycache_filename = f'{operator}.tyCacheFilename="{filepath}"' - job_args.append(tycache_filename) - # TODO: add the additional job args for tycache attributes - if additional_attributes: - additional_args = self.get_additional_attribute_args( - operator, additional_attributes - ) - job_args.extend(additional_args) - tycache_export = f"{operator}.exportTyCache()" - job_args.append(tycache_export) + settings = { + "exportMode": 2, + "frameStart": start, + "frameEnd": end, + "tyCacheFilename": filepath.replace("\\", "/") + } + settings.update(additional_attributes) + job_args = [] + for operator in self.get_operators(members): + for key, value in settings.items(): + if isinstance(value, str): + # embed in quotes + value = f'"{value}"' + + job_args.append(f"{operator}.{key}={value}") + job_args.append(f"{operator}.exportTyCache()") return job_args @staticmethod diff --git a/openpype/hosts/max/plugins/publish/validate_tyflow_data.py b/openpype/hosts/max/plugins/publish/validate_tyflow_data.py index 4b2bf975ee..67c35ec01c 100644 --- a/openpype/hosts/max/plugins/publish/validate_tyflow_data.py +++ b/openpype/hosts/max/plugins/publish/validate_tyflow_data.py @@ -52,12 +52,7 @@ class ValidateTyFlowData(pyblish.api.InstancePlugin): selection_list = instance.data["members"] for sel in selection_list: - sel_tmp = str(sel) - if rt.ClassOf(sel) in [rt.tyFlow, - rt.Editable_Mesh]: - if "tyFlow" not in sel_tmp: - invalid.append(sel) - else: + if rt.ClassOf(sel) not in [rt.tyFlow, rt.Editable_Mesh]: invalid.append(sel) return invalid @@ -75,22 +70,22 @@ class ValidateTyFlowData(pyblish.api.InstancePlugin): of the node connections """ invalid = [] - container = instance.data["instance_node"] - self.log.debug(f"Validating tyFlow object for {container}") - selection_list = instance.data["members"] - bool_list = [] - for sel in selection_list: - obj = sel.baseobject + members = instance.data["members"] + for member in members: + obj = member.baseobject + + # There must be at least one animation with export + # particles enabled + has_export_particles = False anim_names = rt.GetSubAnimNames(obj) for anim_name in anim_names: - # get all the names of the related tyFlow nodes + # get name of the related tyFlow node sub_anim = rt.GetSubAnim(obj, anim_name) # check if there is export particle operator - boolean = rt.IsProperty(sub_anim, "Export_Particles") - bool_list.append(str(boolean)) - # if the export_particles property is not there - # it means there is not a "Export Particle" operator - if "True" not in bool_list: - invalid.append(sel) + if rt.IsProperty(sub_anim, "Export_Particles"): + has_export_particles = True + break + if not has_export_particles: + invalid.append(member) return invalid From 9868b09c9bbd546d98148c7a80c087b87f84a766 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 10 Oct 2023 15:58:34 +0200 Subject: [PATCH 0426/1233] prepared context dialog using AYON calls --- openpype/tools/context_dialog/_ayon_window.py | 783 ++++++++++++++++++ .../tools/context_dialog/_openpype_window.py | 396 +++++++++ openpype/tools/context_dialog/window.py | 402 +-------- 3 files changed, 1188 insertions(+), 393 deletions(-) create mode 100644 openpype/tools/context_dialog/_ayon_window.py create mode 100644 openpype/tools/context_dialog/_openpype_window.py diff --git a/openpype/tools/context_dialog/_ayon_window.py b/openpype/tools/context_dialog/_ayon_window.py new file mode 100644 index 0000000000..6514780236 --- /dev/null +++ b/openpype/tools/context_dialog/_ayon_window.py @@ -0,0 +1,783 @@ +import os +import json + +import ayon_api +from qtpy import QtWidgets, QtCore, QtGui + +from openpype import style +from openpype.lib.events import QueuedEventSystem +from openpype.tools.ayon_utils.models import ( + ProjectsModel, + HierarchyModel, +) +from openpype.tools.ayon_utils.widgets import ( + ProjectsCombobox, + FoldersWidget, + TasksWidget, +) +from openpype.tools.utils.lib import ( + center_window, + get_openpype_qt_app, +) + + +class SelectionModel(object): + """Model handling selection changes. + + Triggering events: + - "selection.project.changed" + - "selection.folder.changed" + - "selection.task.changed" + """ + + event_source = "selection.model" + + def __init__(self, controller): + self._controller = controller + + self._project_name = None + self._folder_id = None + self._task_id = None + self._task_name = None + + def get_selected_project_name(self): + return self._project_name + + def set_selected_project(self, project_name): + self._project_name = project_name + self._controller.emit_event( + "selection.project.changed", + {"project_name": project_name}, + self.event_source + ) + + 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", + { + "project_name": self._project_name, + "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, task_id, task_name): + if task_id == self._task_id: + return + + self._task_name = task_name + self._task_id = task_id + self._controller.emit_event( + "selection.task.changed", + { + "project_name": self._project_name, + "folder_id": self._folder_id, + "task_name": task_name, + "task_id": task_id, + }, + self.event_source + ) + + +class ExpectedSelection: + def __init__(self, controller): + self._project_name = None + self._folder_id = None + + self._project_selected = True + self._folder_selected = True + + self._controller = controller + + def _emit_change(self): + self._controller.emit_event( + "expected_selection_changed", + self.get_expected_selection_data(), + ) + + def set_expected_selection(self, project_name, folder_id): + self._project_name = project_name + self._folder_id = folder_id + + self._project_selected = False + self._folder_selected = False + self._emit_change() + + def get_expected_selection_data(self): + project_current = False + folder_current = False + if not self._project_selected: + project_current = True + elif not self._folder_selected: + folder_current = True + return { + "project": { + "name": self._project_name, + "current": project_current, + "selected": self._project_selected, + }, + "folder": { + "id": self._folder_id, + "current": folder_current, + "selected": self._folder_selected, + }, + } + + def is_expected_project_selected(self, project_name): + return project_name == self._project_name and self._project_selected + + def is_expected_folder_selected(self, folder_id): + return folder_id == self._folder_id and self._folder_selected + + def expected_project_selected(self, project_name): + if project_name != self._project_name: + return False + self._project_selected = True + self._emit_change() + return True + + def expected_folder_selected(self, folder_id): + if folder_id != self._folder_id: + return False + self._folder_selected = True + self._emit_change() + return True + + +class ContextDialogController: + def __init__(self): + self._event_system = None + + self._projects_model = ProjectsModel(self) + self._hierarchy_model = HierarchyModel(self) + self._selection_model = SelectionModel(self) + self._expected_selection = ExpectedSelection(self) + + self._confirmed = False + self._is_strict = False + self._output_path = None + + self._initial_project_name = None + self._initial_folder_id = None + self._initial_folder_label = None + self._initial_project_found = True + self._initial_folder_found = True + self._initial_tasks_found = True + + def reset(self): + self._emit_event("controller.reset.started") + + self._confirmed = False + self._output_path = None + + self._initial_project_name = None + self._initial_folder_id = None + self._initial_folder_label = None + self._initial_project_found = True + self._initial_folder_found = True + self._initial_tasks_found = True + + self._projects_model.reset() + self._hierarchy_model.reset() + + self._emit_event("controller.reset.finished") + + def refresh(self): + self._emit_event("controller.refresh.started") + + self._projects_model.reset() + self._hierarchy_model.reset() + + self._emit_event("controller.refresh.finished") + + # Event handling + def emit_event(self, topic, data=None, source=None): + """Use implemented event system to trigger event.""" + + if data is None: + data = {} + self._get_event_system().emit(topic, data, source) + + def register_event_callback(self, topic, callback): + self._get_event_system().add_callback(topic, callback) + + def set_output_json_path(self, output_path): + self._output_path = output_path + + def is_strict(self): + return self._is_strict + + def set_strict(self, enabled): + if self._is_strict is enabled: + return + self._is_strict = enabled + self._emit_event("strict.changed", {"strict": enabled}) + + # Data model functions + def get_project_items(self, sender=None): + return self._projects_model.get_project_items(sender) + + def get_folder_items(self, project_name, sender=None): + return self._hierarchy_model.get_folder_items(project_name, sender) + + def get_task_items(self, project_name, folder_id, sender=None): + return self._hierarchy_model.get_task_items( + project_name, folder_id, sender + ) + + # Expected selection helpers + def set_expected_selection(self, project_name, folder_id): + return self._expected_selection.set_expected_selection( + project_name, folder_id + ) + + def get_expected_selection_data(self): + return self._expected_selection.get_expected_selection_data() + + def expected_project_selected(self, project_name): + self._expected_selection.expected_project_selected(project_name) + + def expected_folder_selected(self, folder_id): + self._expected_selection.expected_folder_selected(folder_id) + + # Selection handling + def get_selected_project_name(self): + return self._selection_model.get_selected_project_name() + + def set_selected_project(self, project_name): + self._selection_model.set_selected_project(project_name) + + 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_name(self): + return self._selection_model.get_selected_task_name() + + def get_selected_task_id(self): + return self._selection_model.get_selected_task_id() + + def set_selected_task(self, task_id, task_name): + self._selection_model.set_selected_task(task_id, task_name) + + def is_initial_context_valid(self): + return self._initial_folder_found and self._initial_project_found + + def set_initial_context( + self, project_name=None, asset_name=None, folder_path=None + ): + if project_name is None: + project_found = True + asset_name = None + folder_path = None + + else: + project = ayon_api.get_project(project_name) + project_found = project is not None + + folder_id = None + folder_found = True + folder_label = None + if folder_path: + folder_label = folder_path + folder = ayon_api.get_folder_by_path(project_name, folder_path) + if folder: + folder_id = folder["id"] + else: + folder_found = False + elif asset_name: + folder_label = asset_name + for folder in ayon_api.get_folders( + project_name, folder_names=[asset_name] + ): + folder_id = folder["id"] + break + if not folder_id: + folder_found = False + + tasks_found = True + if folder_found and (folder_path or asset_name): + tasks = list(ayon_api.get_tasks( + project_name, folder_ids=[folder_id], fields=["id"] + )) + if not tasks: + tasks_found = False + + self._initial_project_name = project_name + self._initial_folder_id = folder_id + self._initial_folder_label = folder_label + self._initial_folder_found = project_found + self._initial_folder_found = folder_found + self._initial_tasks_found = tasks_found + self._emit_event( + "initial.context.changed", + self.get_initial_context() + ) + + def get_initial_context(self): + return { + "project_name": self._initial_project_name, + "folder_id": self._initial_folder_id, + "folder_label": self._initial_folder_label, + "project_found": self._initial_project_found, + "folder_found": self._initial_folder_found, + "tasks_found": self._initial_tasks_found, + "valid": ( + self._initial_project_found + and self._initial_folder_found + and self._initial_tasks_found + ) + } + + # Result of this tool + def get_selected_context(self): + return { + "project": None, + "project_name": None, + "asset": None, + "folder_id": None, + "folder_path": None, + "task": None, + "task_id": None, + "task_name": None, + } + + def window_closed(self): + if not self._confirmed and not self._is_strict: + return + + self._store_output() + + def confirm_selection(self): + self._confirmed = True + self._emit_event( + "selection.confirmed", + {"confirmed": True} + ) + + def _store_output(self): + if not self._output_path: + return + + dirpath = os.path.dirname(self._output_path) + os.makedirs(dirpath, exist_ok=True) + with open(self._output_path, "w") as stream: + json.dump(self.get_selected_context(), stream) + + def _get_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 + + def _emit_event(self, topic, data=None): + self.emit_event(topic, data, "controller") + + +class InvalidContextOverlay(QtWidgets.QFrame): + confirmed = QtCore.Signal() + + def __init__(self, parent): + super(InvalidContextOverlay, self).__init__(parent) + self.setObjectName("OverlayFrame") + + mid_widget = QtWidgets.QWidget(self) + label_widget = QtWidgets.QLabel( + "Requested context was not found...", + mid_widget + ) + + confirm_btn = QtWidgets.QPushButton("Close", mid_widget) + + mid_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) + label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + mid_layout = QtWidgets.QVBoxLayout(mid_widget) + mid_layout.setContentsMargins(0, 0, 0, 0) + mid_layout.addWidget(label_widget, 0) + mid_layout.addSpacing(30) + mid_layout.addWidget(confirm_btn, 0) + + main_layout = QtWidgets.QGridLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.addWidget(mid_widget, 1, 1) + main_layout.setRowStretch(0, 1) + main_layout.setRowStretch(1, 0) + main_layout.setRowStretch(2, 1) + main_layout.setColumnStretch(0, 1) + main_layout.setColumnStretch(1, 0) + main_layout.setColumnStretch(2, 1) + + confirm_btn.clicked.connect(self.confirmed) + + self._label_widget = label_widget + self._confirm_btn = confirm_btn + + def set_context( + self, + project_name, + folder_label, + project_found, + folder_found, + tasks_found, + ): + lines = [] + if not project_found: + lines.extend([ + "Requested project {} was not found...".format(project_name), + ]) + + elif not folder_found: + lines.extend([ + "Requested folder was not found...", + "", + "Project: {}".format(project_name), + "Folder: {}".format(folder_label), + ]) + elif not tasks_found: + lines.extend([ + "Requested folder does not have any tasks...", + "", + "Project: {}".format(project_name), + "Folder: {}".format(folder_label), + ]) + else: + lines.append("Requested context was not found...") + self._label_widget.setText("
".join(lines)) + + +class ContextDialog(QtWidgets.QDialog): + """Dialog to select a context. + + Context has 3 parts: + - Project + - Asset + - Task + + It is possible to predefine project and asset. In that case their widgets + will have passed preselected values and will be disabled. + """ + def __init__(self, controller=None, parent=None): + super(ContextDialog, self).__init__(parent) + + self.setWindowTitle("Select Context") + self.setWindowIcon(QtGui.QIcon(style.app_icon_path())) + + if controller is None: + controller = ContextDialogController() + + # Enable minimize and maximize for app + window_flags = QtCore.Qt.Window + if not parent: + window_flags |= QtCore.Qt.WindowStaysOnTopHint + self.setWindowFlags(window_flags) + self.setFocusPolicy(QtCore.Qt.StrongFocus) + + # UI initialization + main_splitter = QtWidgets.QSplitter(self) + + # Left side widget contains project combobox and asset widget + left_side_widget = QtWidgets.QWidget(main_splitter) + + project_combobox = ProjectsCombobox( + controller, + parent=left_side_widget, + handle_expected_selection=True + ) + + # Assets widget + folders_widget = FoldersWidget( + controller, + parent=left_side_widget, + handle_expected_selection=True + ) + + left_side_layout = QtWidgets.QVBoxLayout(left_side_widget) + left_side_layout.setContentsMargins(0, 0, 0, 0) + left_side_layout.addWidget(project_combobox, 0) + left_side_layout.addWidget(folders_widget, 1) + + # Right side of window contains only tasks + tasks_widget = TasksWidget(controller, parent=main_splitter) + + # Add widgets to main splitter + main_splitter.addWidget(left_side_widget) + main_splitter.addWidget(tasks_widget) + + # Set stretch of both sides + main_splitter.setStretchFactor(0, 7) + main_splitter.setStretchFactor(1, 3) + + # Add confimation button to bottom right + ok_btn = QtWidgets.QPushButton("OK", self) + + buttons_layout = QtWidgets.QHBoxLayout() + buttons_layout.setContentsMargins(0, 0, 0, 0) + buttons_layout.addStretch(1) + buttons_layout.addWidget(ok_btn, 0) + + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.addWidget(main_splitter, 1) + main_layout.addLayout(buttons_layout, 0) + + overlay_widget = InvalidContextOverlay(self) + overlay_widget.setVisible(False) + + ok_btn.clicked.connect(self._on_ok_click) + project_combobox.refreshed.connect(self._on_projects_refresh) + overlay_widget.confirmed.connect(self._on_overlay_confirm) + + controller.register_event_callback( + "selection.project.changed", + self._on_project_selection_change + ) + controller.register_event_callback( + "selection.folder.changed", + self._on_folder_selection_change + ) + controller.register_event_callback( + "selection.task.changed", + self._on_task_selection_change + ) + controller.register_event_callback( + "initial.context.changed", + self._on_init_context_change + ) + controller.register_event_callback( + "strict.changed", + self._on_strict_changed + ) + controller.register_event_callback( + "controller.reset.finished", + self._on_controller_reset + ) + controller.register_event_callback( + "controller.refresh.finished", + self._on_controller_refresh + ) + + # Set stylehseet and resize window on first show + self._first_show = True + self._visible = False + + self._controller = controller + + self._project_combobox = project_combobox + self._folders_widget = folders_widget + self._tasks_widget = tasks_widget + + self._ok_btn = ok_btn + + self._overlay_widget = overlay_widget + + self._apply_strict_changes(self.is_strict()) + + def is_strict(self): + return self._controller.is_strict() + + def showEvent(self, event): + """Override show event to do some callbacks.""" + super(ContextDialog, self).showEvent(event) + self._visible = True + + if self._first_show: + self._first_show = False + # Set stylesheet and resize + self.setStyleSheet(style.load_stylesheet()) + self.resize(600, 700) + center_window(self) + self._controller.refresh() + + initial_context = self._controller.get_initial_context() + self._set_init_context(initial_context) + self._overlay_widget.resize(self.size()) + + def resizeEvent(self, event): + super(ContextDialog, self).resizeEvent(event) + self._overlay_widget.resize(self.size()) + + def closeEvent(self, event): + """Ignore close event if is in strict state and context is not done.""" + if self.is_strict() and not self._ok_btn.isEnabled(): + # Allow to close window when initial context is not valid + if self._controller.is_initial_context_valid(): + event.ignore() + return + + if self.is_strict(): + self._controller.confirm_selection() + self._visible = False + super(ContextDialog, self).closeEvent(event) + + def set_strict(self, enabled): + """Change strictness of dialog.""" + + self._controller.set_strict(enabled) + + def refresh(self): + """Refresh all widget one by one. + + When asset refresh is triggered we have to wait when is done so + this method continues with `_on_asset_widget_refresh_finished`. + """ + + self._controller.reset() + + def get_context(self): + """Result of dialog.""" + return self._controller.get_selected_context() + + def set_context(self, project_name=None, asset_name=None): + """Set context which will be used and locked in dialog.""" + + self._controller.set_initial_context(project_name, asset_name) + + def _on_projects_refresh(self): + initial_context = self._controller.get_initial_context() + self._controller.set_expected_selection( + initial_context["project_name"], + initial_context["folder_id"] + ) + + def _on_overlay_confirm(self): + self.close() + + def _on_ok_click(self): + # Store values to output + self._controller.confirm_selection() + # Close dialog + self.accept() + + def _on_project_selection_change(self, event): + self._on_selection_change( + event["project_name"], + ) + + def _on_folder_selection_change(self, event): + self._on_selection_change( + event["project_name"], + event["folder_id"], + ) + + def _on_task_selection_change(self, event): + self._on_selection_change( + event["project_name"], + event["folder_id"], + event["task_name"], + ) + + def _on_selection_change( + self, project_name, folder_id=None, task_name=None + ): + self._validate_strict(project_name, folder_id, task_name) + + def _on_init_context_change(self, event): + self._set_init_context(event.data) + if self._visible: + self._controller.set_expected_selection( + event["project_name"], event["folder_id"] + ) + + def _set_init_context(self, init_context): + project_name = init_context["project_name"] + if not init_context["valid"]: + self._overlay_widget.setVisible(True) + self._overlay_widget.set_context( + project_name, + init_context["folder_label"], + init_context["project_found"], + init_context["folder_found"], + init_context["tasks_found"] + ) + return + + self._overlay_widget.setVisible(False) + if project_name: + self._project_combobox.setEnabled(False) + if init_context["folder_id"]: + self._folders_widget.setEnabled(False) + else: + self._project_combobox.setEnabled(True) + self._folders_widget.setEnabled(True) + + def _on_strict_changed(self, event): + self._apply_strict_changes(event["strict"]) + + def _on_controller_reset(self): + self._apply_strict_changes(self.is_strict()) + self._project_combobox.refresh() + + def _on_controller_refresh(self): + self._project_combobox.refresh() + + def _apply_strict_changes(self, is_strict): + if not is_strict: + if not self._ok_btn.isEnabled(): + self._ok_btn.setEnabled(True) + return + context = self._controller.get_selected_context() + self._validate_strict( + context["project_name"], + context["folder_id"], + context["task_name"] + ) + + def _validate_strict(self, project_name, folder_id, task_name): + if not self.is_strict(): + return + + enabled = True + if not project_name or not folder_id or not task_name: + enabled = False + self._ok_btn.setEnabled(enabled) + + +def main( + path_to_store, + project_name=None, + asset_name=None, + strict=True +): + # Run Qt application + app = get_openpype_qt_app() + controller = ContextDialogController() + controller.set_strict(strict) + controller.set_initial_context(project_name, asset_name) + controller.set_output_json_path(path_to_store) + window = ContextDialog(controller=controller) + window.show() + app.exec_() + + # Get result from window + data = window.get_context() + + # Make sure json filepath directory exists + file_dir = os.path.dirname(path_to_store) + if not os.path.exists(file_dir): + os.makedirs(file_dir) + + # Store result into json file + with open(path_to_store, "w") as stream: + json.dump(data, stream) diff --git a/openpype/tools/context_dialog/_openpype_window.py b/openpype/tools/context_dialog/_openpype_window.py new file mode 100644 index 0000000000..d370772a7f --- /dev/null +++ b/openpype/tools/context_dialog/_openpype_window.py @@ -0,0 +1,396 @@ +import os +import json + +from qtpy import QtWidgets, QtCore, QtGui + +from openpype import style +from openpype.pipeline import AvalonMongoDB +from openpype.tools.utils.lib import center_window, get_openpype_qt_app +from openpype.tools.utils.assets_widget import SingleSelectAssetsWidget +from openpype.tools.utils.constants import ( + PROJECT_NAME_ROLE +) +from openpype.tools.utils.tasks_widget import TasksWidget +from openpype.tools.utils.models import ( + ProjectModel, + ProjectSortFilterProxy +) + + +class ContextDialog(QtWidgets.QDialog): + """Dialog to select a context. + + Context has 3 parts: + - Project + - Asset + - Task + + It is possible to predefine project and asset. In that case their widgets + will have passed preselected values and will be disabled. + """ + def __init__(self, parent=None): + super(ContextDialog, self).__init__(parent) + + self.setWindowTitle("Select Context") + self.setWindowIcon(QtGui.QIcon(style.app_icon_path())) + + # Enable minimize and maximize for app + window_flags = QtCore.Qt.Window + if not parent: + window_flags |= QtCore.Qt.WindowStaysOnTopHint + self.setWindowFlags(window_flags) + self.setFocusPolicy(QtCore.Qt.StrongFocus) + + dbcon = AvalonMongoDB() + + # UI initialization + main_splitter = QtWidgets.QSplitter(self) + + # Left side widget contains project combobox and asset widget + left_side_widget = QtWidgets.QWidget(main_splitter) + + project_combobox = QtWidgets.QComboBox(left_side_widget) + # Styled delegate to propagate stylessheet + project_delegate = QtWidgets.QStyledItemDelegate(project_combobox) + project_combobox.setItemDelegate(project_delegate) + # Project model with only active projects without default item + project_model = ProjectModel( + dbcon, + only_active=True, + add_default_project=False + ) + # Sorting proxy model + project_proxy = ProjectSortFilterProxy() + project_proxy.setSourceModel(project_model) + project_combobox.setModel(project_proxy) + + # Assets widget + assets_widget = SingleSelectAssetsWidget( + dbcon, parent=left_side_widget + ) + + left_side_layout = QtWidgets.QVBoxLayout(left_side_widget) + left_side_layout.setContentsMargins(0, 0, 0, 0) + left_side_layout.addWidget(project_combobox) + left_side_layout.addWidget(assets_widget) + + # Right side of window contains only tasks + tasks_widget = TasksWidget(dbcon, main_splitter) + + # Add widgets to main splitter + main_splitter.addWidget(left_side_widget) + main_splitter.addWidget(tasks_widget) + + # Set stretch of both sides + main_splitter.setStretchFactor(0, 7) + main_splitter.setStretchFactor(1, 3) + + # Add confimation button to bottom right + ok_btn = QtWidgets.QPushButton("OK", self) + + buttons_layout = QtWidgets.QHBoxLayout() + buttons_layout.setContentsMargins(0, 0, 0, 0) + buttons_layout.addStretch(1) + buttons_layout.addWidget(ok_btn, 0) + + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.addWidget(main_splitter, 1) + main_layout.addLayout(buttons_layout, 0) + + # Timer which will trigger asset refresh + # - this is needed because asset widget triggers + # finished refresh before hides spin box so we need to trigger + # refreshing in small offset if we want re-refresh asset widget + assets_timer = QtCore.QTimer() + assets_timer.setInterval(50) + assets_timer.setSingleShot(True) + + assets_timer.timeout.connect(self._on_asset_refresh_timer) + + project_combobox.currentIndexChanged.connect( + self._on_project_combo_change + ) + assets_widget.selection_changed.connect(self._on_asset_change) + assets_widget.refresh_triggered.connect(self._on_asset_refresh_trigger) + assets_widget.refreshed.connect(self._on_asset_widget_refresh_finished) + tasks_widget.task_changed.connect(self._on_task_change) + ok_btn.clicked.connect(self._on_ok_click) + + self._dbcon = dbcon + + self._project_combobox = project_combobox + self._project_model = project_model + self._project_proxy = project_proxy + self._project_delegate = project_delegate + + self._assets_widget = assets_widget + + self._tasks_widget = tasks_widget + + self._ok_btn = ok_btn + + self._strict = False + + # Values set by `set_context` method + self._set_context_project = None + self._set_context_asset = None + + # Requirements for asset widget refresh + self._assets_timer = assets_timer + self._rerefresh_assets = True + self._assets_refreshing = False + + # Set stylehseet and resize window on first show + self._first_show = True + + # Helper attributes for handling of refresh + self._ignore_value_changes = False + self._refresh_on_next_show = True + + # Output of dialog + self._context_to_store = { + "project": None, + "asset": None, + "task": None + } + + def closeEvent(self, event): + """Ignore close event if is in strict state and context is not done.""" + if self._strict and not self._ok_btn.isEnabled(): + event.ignore() + return + + if self._strict: + self._confirm_values() + super(ContextDialog, self).closeEvent(event) + + def set_strict(self, strict): + """Change strictness of dialog.""" + self._strict = strict + self._validate_strict() + + def _set_refresh_on_next_show(self): + """Refresh will be called on next showEvent. + + If window is already visible then just execute refresh. + """ + self._refresh_on_next_show = True + if self.isVisible(): + self.refresh() + + def _refresh_assets(self): + """Trigger refreshing of asset widget. + + This will set mart to rerefresh asset when current refreshing is done + or do it immidietely if asset widget is not refreshing at the time. + """ + if self._assets_refreshing: + self._rerefresh_assets = True + else: + self._on_asset_refresh_timer() + + def showEvent(self, event): + """Override show event to do some callbacks.""" + super(ContextDialog, self).showEvent(event) + if self._first_show: + self._first_show = False + # Set stylesheet and resize + self.setStyleSheet(style.load_stylesheet()) + self.resize(600, 700) + center_window(self) + + if self._refresh_on_next_show: + self.refresh() + + def refresh(self): + """Refresh all widget one by one. + + When asset refresh is triggered we have to wait when is done so + this method continues with `_on_asset_widget_refresh_finished`. + """ + # Change state of refreshing (no matter how refresh was called) + self._refresh_on_next_show = False + + # Ignore changes of combobox and asset widget + self._ignore_value_changes = True + + # Get current project name to be able set it afterwards + select_project_name = self._dbcon.Session.get("AVALON_PROJECT") + # Trigger project refresh + self._project_model.refresh() + # Sort projects + self._project_proxy.sort(0) + + # Disable combobox if project was passed to `set_context` + if self._set_context_project: + select_project_name = self._set_context_project + self._project_combobox.setEnabled(False) + else: + # Find new project to select + self._project_combobox.setEnabled(True) + if ( + select_project_name is None + and self._project_proxy.rowCount() > 0 + ): + index = self._project_proxy.index(0, 0) + select_project_name = index.data(PROJECT_NAME_ROLE) + + self._ignore_value_changes = False + + idx = self._project_combobox.findText(select_project_name) + if idx >= 0: + self._project_combobox.setCurrentIndex(idx) + self._dbcon.Session["AVALON_PROJECT"] = ( + self._project_combobox.currentText() + ) + + # Trigger asset refresh + self._refresh_assets() + + def _on_asset_refresh_timer(self): + """This is only way how to trigger refresh asset widget. + + Use `_refresh_assets` method to refresh asset widget. + """ + self._assets_widget.refresh() + + def _on_asset_widget_refresh_finished(self): + """Catch when asset widget finished refreshing.""" + # If should refresh again then skip all other callbacks and trigger + # assets timer directly. + self._assets_refreshing = False + if self._rerefresh_assets: + self._rerefresh_assets = False + self._assets_timer.start() + return + + self._ignore_value_changes = True + if self._set_context_asset: + self._dbcon.Session["AVALON_ASSET"] = self._set_context_asset + self._assets_widget.setEnabled(False) + self._assets_widget.select_asset_by_name(self._set_context_asset) + self._set_asset_to_tasks_widget() + else: + self._assets_widget.setEnabled(True) + self._assets_widget.set_current_asset_btn_visibility(False) + + # Refresh tasks + self._tasks_widget.refresh() + + self._ignore_value_changes = False + + self._validate_strict() + + def _on_project_combo_change(self): + if self._ignore_value_changes: + return + project_name = self._project_combobox.currentText() + + if self._dbcon.Session.get("AVALON_PROJECT") == project_name: + return + + self._dbcon.Session["AVALON_PROJECT"] = project_name + + self._refresh_assets() + self._validate_strict() + + def _on_asset_refresh_trigger(self): + self._assets_refreshing = True + self._on_asset_change() + + def _on_asset_change(self): + """Selected assets have changed""" + if self._ignore_value_changes: + return + self._set_asset_to_tasks_widget() + + def _on_task_change(self): + self._validate_strict() + + def _set_asset_to_tasks_widget(self): + asset_id = self._assets_widget.get_selected_asset_id() + + self._tasks_widget.set_asset_id(asset_id) + + def _confirm_values(self): + """Store values to output.""" + self._context_to_store["project"] = self.get_selected_project() + self._context_to_store["asset"] = self.get_selected_asset() + self._context_to_store["task"] = self.get_selected_task() + + def _on_ok_click(self): + # Store values to output + self._confirm_values() + # Close dialog + self.accept() + + def get_selected_project(self): + """Get selected project.""" + return self._project_combobox.currentText() + + def get_selected_asset(self): + """Currently selected asset in asset widget.""" + return self._assets_widget.get_selected_asset_name() + + def get_selected_task(self): + """Currently selected task.""" + return self._tasks_widget.get_selected_task_name() + + def _validate_strict(self): + if not self._strict: + if not self._ok_btn.isEnabled(): + self._ok_btn.setEnabled(True) + return + + enabled = True + if not self._set_context_project and not self.get_selected_project(): + enabled = False + elif not self._set_context_asset and not self.get_selected_asset(): + enabled = False + elif not self.get_selected_task(): + enabled = False + self._ok_btn.setEnabled(enabled) + + def set_context(self, project_name=None, asset_name=None): + """Set context which will be used and locked in dialog.""" + if project_name is None: + asset_name = None + + self._set_context_project = project_name + self._set_context_asset = asset_name + + self._context_to_store["project"] = project_name + self._context_to_store["asset"] = asset_name + + self._set_refresh_on_next_show() + + def get_context(self): + """Result of dialog.""" + return self._context_to_store + + +def main( + path_to_store, + project_name=None, + asset_name=None, + strict=True +): + # Run Qt application + app = get_openpype_qt_app() + window = ContextDialog() + window.set_strict(strict) + window.set_context(project_name, asset_name) + window.show() + app.exec_() + + # Get result from window + data = window.get_context() + + # Make sure json filepath directory exists + file_dir = os.path.dirname(path_to_store) + if not os.path.exists(file_dir): + os.makedirs(file_dir) + + # Store result into json file + with open(path_to_store, "w") as stream: + json.dump(data, stream) diff --git a/openpype/tools/context_dialog/window.py b/openpype/tools/context_dialog/window.py index 4fe41c9949..15b90463da 100644 --- a/openpype/tools/context_dialog/window.py +++ b/openpype/tools/context_dialog/window.py @@ -1,396 +1,12 @@ -import os -import json +from openpype import AYON_SERVER_ENABLED -from qtpy import QtWidgets, QtCore, QtGui +if AYON_SERVER_ENABLED: + from ._ayon_window import ContextDialog, main +else: + from ._openpype_window import ContextDialog, main -from openpype import style -from openpype.pipeline import AvalonMongoDB -from openpype.tools.utils.lib import center_window, get_openpype_qt_app -from openpype.tools.utils.assets_widget import SingleSelectAssetsWidget -from openpype.tools.utils.constants import ( - PROJECT_NAME_ROLE + +__all__ = ( + "ContextDialog", + "main", ) -from openpype.tools.utils.tasks_widget import TasksWidget -from openpype.tools.utils.models import ( - ProjectModel, - ProjectSortFilterProxy -) - - -class ContextDialog(QtWidgets.QDialog): - """Dialog to select a context. - - Context has 3 parts: - - Project - - Aseet - - Task - - It is possible to predefine project and asset. In that case their widgets - will have passed preselected values and will be disabled. - """ - def __init__(self, parent=None): - super(ContextDialog, self).__init__(parent) - - self.setWindowTitle("Select Context") - self.setWindowIcon(QtGui.QIcon(style.app_icon_path())) - - # Enable minimize and maximize for app - window_flags = QtCore.Qt.Window - if not parent: - window_flags |= QtCore.Qt.WindowStaysOnTopHint - self.setWindowFlags(window_flags) - self.setFocusPolicy(QtCore.Qt.StrongFocus) - - dbcon = AvalonMongoDB() - - # UI initialization - main_splitter = QtWidgets.QSplitter(self) - - # Left side widget contains project combobox and asset widget - left_side_widget = QtWidgets.QWidget(main_splitter) - - project_combobox = QtWidgets.QComboBox(left_side_widget) - # Styled delegate to propagate stylessheet - project_delegate = QtWidgets.QStyledItemDelegate(project_combobox) - project_combobox.setItemDelegate(project_delegate) - # Project model with only active projects without default item - project_model = ProjectModel( - dbcon, - only_active=True, - add_default_project=False - ) - # Sorting proxy model - project_proxy = ProjectSortFilterProxy() - project_proxy.setSourceModel(project_model) - project_combobox.setModel(project_proxy) - - # Assets widget - assets_widget = SingleSelectAssetsWidget( - dbcon, parent=left_side_widget - ) - - left_side_layout = QtWidgets.QVBoxLayout(left_side_widget) - left_side_layout.setContentsMargins(0, 0, 0, 0) - left_side_layout.addWidget(project_combobox) - left_side_layout.addWidget(assets_widget) - - # Right side of window contains only tasks - tasks_widget = TasksWidget(dbcon, main_splitter) - - # Add widgets to main splitter - main_splitter.addWidget(left_side_widget) - main_splitter.addWidget(tasks_widget) - - # Set stretch of both sides - main_splitter.setStretchFactor(0, 7) - main_splitter.setStretchFactor(1, 3) - - # Add confimation button to bottom right - ok_btn = QtWidgets.QPushButton("OK", self) - - buttons_layout = QtWidgets.QHBoxLayout() - buttons_layout.setContentsMargins(0, 0, 0, 0) - buttons_layout.addStretch(1) - buttons_layout.addWidget(ok_btn, 0) - - main_layout = QtWidgets.QVBoxLayout(self) - main_layout.addWidget(main_splitter, 1) - main_layout.addLayout(buttons_layout, 0) - - # Timer which will trigger asset refresh - # - this is needed because asset widget triggers - # finished refresh before hides spin box so we need to trigger - # refreshing in small offset if we want re-refresh asset widget - assets_timer = QtCore.QTimer() - assets_timer.setInterval(50) - assets_timer.setSingleShot(True) - - assets_timer.timeout.connect(self._on_asset_refresh_timer) - - project_combobox.currentIndexChanged.connect( - self._on_project_combo_change - ) - assets_widget.selection_changed.connect(self._on_asset_change) - assets_widget.refresh_triggered.connect(self._on_asset_refresh_trigger) - assets_widget.refreshed.connect(self._on_asset_widget_refresh_finished) - tasks_widget.task_changed.connect(self._on_task_change) - ok_btn.clicked.connect(self._on_ok_click) - - self._dbcon = dbcon - - self._project_combobox = project_combobox - self._project_model = project_model - self._project_proxy = project_proxy - self._project_delegate = project_delegate - - self._assets_widget = assets_widget - - self._tasks_widget = tasks_widget - - self._ok_btn = ok_btn - - self._strict = False - - # Values set by `set_context` method - self._set_context_project = None - self._set_context_asset = None - - # Requirements for asset widget refresh - self._assets_timer = assets_timer - self._rerefresh_assets = True - self._assets_refreshing = False - - # Set stylehseet and resize window on first show - self._first_show = True - - # Helper attributes for handling of refresh - self._ignore_value_changes = False - self._refresh_on_next_show = True - - # Output of dialog - self._context_to_store = { - "project": None, - "asset": None, - "task": None - } - - def closeEvent(self, event): - """Ignore close event if is in strict state and context is not done.""" - if self._strict and not self._ok_btn.isEnabled(): - event.ignore() - return - - if self._strict: - self._confirm_values() - super(ContextDialog, self).closeEvent(event) - - def set_strict(self, strict): - """Change strictness of dialog.""" - self._strict = strict - self._validate_strict() - - def _set_refresh_on_next_show(self): - """Refresh will be called on next showEvent. - - If window is already visible then just execute refresh. - """ - self._refresh_on_next_show = True - if self.isVisible(): - self.refresh() - - def _refresh_assets(self): - """Trigger refreshing of asset widget. - - This will set mart to rerefresh asset when current refreshing is done - or do it immidietely if asset widget is not refreshing at the time. - """ - if self._assets_refreshing: - self._rerefresh_assets = True - else: - self._on_asset_refresh_timer() - - def showEvent(self, event): - """Override show event to do some callbacks.""" - super(ContextDialog, self).showEvent(event) - if self._first_show: - self._first_show = False - # Set stylesheet and resize - self.setStyleSheet(style.load_stylesheet()) - self.resize(600, 700) - center_window(self) - - if self._refresh_on_next_show: - self.refresh() - - def refresh(self): - """Refresh all widget one by one. - - When asset refresh is triggered we have to wait when is done so - this method continues with `_on_asset_widget_refresh_finished`. - """ - # Change state of refreshing (no matter how refresh was called) - self._refresh_on_next_show = False - - # Ignore changes of combobox and asset widget - self._ignore_value_changes = True - - # Get current project name to be able set it afterwards - select_project_name = self._dbcon.Session.get("AVALON_PROJECT") - # Trigger project refresh - self._project_model.refresh() - # Sort projects - self._project_proxy.sort(0) - - # Disable combobox if project was passed to `set_context` - if self._set_context_project: - select_project_name = self._set_context_project - self._project_combobox.setEnabled(False) - else: - # Find new project to select - self._project_combobox.setEnabled(True) - if ( - select_project_name is None - and self._project_proxy.rowCount() > 0 - ): - index = self._project_proxy.index(0, 0) - select_project_name = index.data(PROJECT_NAME_ROLE) - - self._ignore_value_changes = False - - idx = self._project_combobox.findText(select_project_name) - if idx >= 0: - self._project_combobox.setCurrentIndex(idx) - self._dbcon.Session["AVALON_PROJECT"] = ( - self._project_combobox.currentText() - ) - - # Trigger asset refresh - self._refresh_assets() - - def _on_asset_refresh_timer(self): - """This is only way how to trigger refresh asset widget. - - Use `_refresh_assets` method to refresh asset widget. - """ - self._assets_widget.refresh() - - def _on_asset_widget_refresh_finished(self): - """Catch when asset widget finished refreshing.""" - # If should refresh again then skip all other callbacks and trigger - # assets timer directly. - self._assets_refreshing = False - if self._rerefresh_assets: - self._rerefresh_assets = False - self._assets_timer.start() - return - - self._ignore_value_changes = True - if self._set_context_asset: - self._dbcon.Session["AVALON_ASSET"] = self._set_context_asset - self._assets_widget.setEnabled(False) - self._assets_widget.select_assets(self._set_context_asset) - self._set_asset_to_tasks_widget() - else: - self._assets_widget.setEnabled(True) - self._assets_widget.set_current_asset_btn_visibility(False) - - # Refresh tasks - self._tasks_widget.refresh() - - self._ignore_value_changes = False - - self._validate_strict() - - def _on_project_combo_change(self): - if self._ignore_value_changes: - return - project_name = self._project_combobox.currentText() - - if self._dbcon.Session.get("AVALON_PROJECT") == project_name: - return - - self._dbcon.Session["AVALON_PROJECT"] = project_name - - self._refresh_assets() - self._validate_strict() - - def _on_asset_refresh_trigger(self): - self._assets_refreshing = True - self._on_asset_change() - - def _on_asset_change(self): - """Selected assets have changed""" - if self._ignore_value_changes: - return - self._set_asset_to_tasks_widget() - - def _on_task_change(self): - self._validate_strict() - - def _set_asset_to_tasks_widget(self): - asset_id = self._assets_widget.get_selected_asset_id() - - self._tasks_widget.set_asset_id(asset_id) - - def _confirm_values(self): - """Store values to output.""" - self._context_to_store["project"] = self.get_selected_project() - self._context_to_store["asset"] = self.get_selected_asset() - self._context_to_store["task"] = self.get_selected_task() - - def _on_ok_click(self): - # Store values to output - self._confirm_values() - # Close dialog - self.accept() - - def get_selected_project(self): - """Get selected project.""" - return self._project_combobox.currentText() - - def get_selected_asset(self): - """Currently selected asset in asset widget.""" - return self._assets_widget.get_selected_asset_name() - - def get_selected_task(self): - """Currently selected task.""" - return self._tasks_widget.get_selected_task_name() - - def _validate_strict(self): - if not self._strict: - if not self._ok_btn.isEnabled(): - self._ok_btn.setEnabled(True) - return - - enabled = True - if not self._set_context_project and not self.get_selected_project(): - enabled = False - elif not self._set_context_asset and not self.get_selected_asset(): - enabled = False - elif not self.get_selected_task(): - enabled = False - self._ok_btn.setEnabled(enabled) - - def set_context(self, project_name=None, asset_name=None): - """Set context which will be used and locked in dialog.""" - if project_name is None: - asset_name = None - - self._set_context_project = project_name - self._set_context_asset = asset_name - - self._context_to_store["project"] = project_name - self._context_to_store["asset"] = asset_name - - self._set_refresh_on_next_show() - - def get_context(self): - """Result of dialog.""" - return self._context_to_store - - -def main( - path_to_store, - project_name=None, - asset_name=None, - strict=True -): - # Run Qt application - app = get_openpype_qt_app() - window = ContextDialog() - window.set_strict(strict) - window.set_context(project_name, asset_name) - window.show() - app.exec_() - - # Get result from window - data = window.get_context() - - # Make sure json filepath directory exists - file_dir = os.path.dirname(path_to_store) - if not os.path.exists(file_dir): - os.makedirs(file_dir) - - # Store result into json file - with open(path_to_store, "w") as stream: - json.dump(data, stream) From b374bf7eaebcb151a38447593ab54864e4cc65ba Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 13 Oct 2023 14:47:21 +0200 Subject: [PATCH 0427/1233] fix storing of data --- openpype/tools/context_dialog/_ayon_window.py | 34 +++++-------------- 1 file changed, 8 insertions(+), 26 deletions(-) diff --git a/openpype/tools/context_dialog/_ayon_window.py b/openpype/tools/context_dialog/_ayon_window.py index 6514780236..04fd3495e1 100644 --- a/openpype/tools/context_dialog/_ayon_window.py +++ b/openpype/tools/context_dialog/_ayon_window.py @@ -356,27 +356,17 @@ class ContextDialogController: "task_name": None, } - def window_closed(self): - if not self._confirmed and not self._is_strict: - return - - self._store_output() - def confirm_selection(self): self._confirmed = True - self._emit_event( - "selection.confirmed", - {"confirmed": True} - ) - def _store_output(self): + def store_output(self): if not self._output_path: return dirpath = os.path.dirname(self._output_path) os.makedirs(dirpath, exist_ok=True) with open(self._output_path, "w") as stream: - json.dump(self.get_selected_context(), stream) + json.dump(self.get_selected_context(), stream, indent=4) def _get_event_system(self): """Inner event system for workfiles tool controller. @@ -627,7 +617,7 @@ class ContextDialog(QtWidgets.QDialog): return if self.is_strict(): - self._controller.confirm_selection() + self._confirm_selection() self._visible = False super(ContextDialog, self).closeEvent(event) @@ -666,10 +656,13 @@ class ContextDialog(QtWidgets.QDialog): def _on_ok_click(self): # Store values to output - self._controller.confirm_selection() + self._confirm_selection() # Close dialog self.accept() + def _confirm_selection(self): + self._controller.confirm_selection() + def _on_project_selection_change(self, event): self._on_selection_change( event["project_name"], @@ -769,15 +762,4 @@ def main( window = ContextDialog(controller=controller) window.show() app.exec_() - - # Get result from window - data = window.get_context() - - # Make sure json filepath directory exists - file_dir = os.path.dirname(path_to_store) - if not os.path.exists(file_dir): - os.makedirs(file_dir) - - # Store result into json file - with open(path_to_store, "w") as stream: - json.dump(data, stream) + controller.store_output() From f86874df3de22841587697cc8246eda14ddb4978 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 13 Oct 2023 14:47:46 +0200 Subject: [PATCH 0428/1233] add select item to projects combobox --- openpype/tools/context_dialog/_ayon_window.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/tools/context_dialog/_ayon_window.py b/openpype/tools/context_dialog/_ayon_window.py index 04fd3495e1..07495b7674 100644 --- a/openpype/tools/context_dialog/_ayon_window.py +++ b/openpype/tools/context_dialog/_ayon_window.py @@ -496,6 +496,7 @@ class ContextDialog(QtWidgets.QDialog): parent=left_side_widget, handle_expected_selection=True ) + project_combobox.set_select_item_visible(True) # Assets widget folders_widget = FoldersWidget( From 28bcbc8053133a1a257078052e9c2115167fea0d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 13 Oct 2023 14:48:07 +0200 Subject: [PATCH 0429/1233] implemented helper function to prepare initial context data --- openpype/tools/context_dialog/_ayon_window.py | 136 +++++++++++------- 1 file changed, 84 insertions(+), 52 deletions(-) diff --git a/openpype/tools/context_dialog/_ayon_window.py b/openpype/tools/context_dialog/_ayon_window.py index 07495b7674..73f9ed139c 100644 --- a/openpype/tools/context_dialog/_ayon_window.py +++ b/openpype/tools/context_dialog/_ayon_window.py @@ -277,52 +277,15 @@ class ContextDialogController: def is_initial_context_valid(self): return self._initial_folder_found and self._initial_project_found - def set_initial_context( - self, project_name=None, asset_name=None, folder_path=None - ): - if project_name is None: - project_found = True - asset_name = None - folder_path = None - - else: - project = ayon_api.get_project(project_name) - project_found = project is not None - - folder_id = None - folder_found = True - folder_label = None - if folder_path: - folder_label = folder_path - folder = ayon_api.get_folder_by_path(project_name, folder_path) - if folder: - folder_id = folder["id"] - else: - folder_found = False - elif asset_name: - folder_label = asset_name - for folder in ayon_api.get_folders( - project_name, folder_names=[asset_name] - ): - folder_id = folder["id"] - break - if not folder_id: - folder_found = False - - tasks_found = True - if folder_found and (folder_path or asset_name): - tasks = list(ayon_api.get_tasks( - project_name, folder_ids=[folder_id], fields=["id"] - )) - if not tasks: - tasks_found = False + def set_initial_context(self, project_name=None, asset_name=None): + result = self._prepare_initial_context(project_name, asset_name) self._initial_project_name = project_name - self._initial_folder_id = folder_id - self._initial_folder_label = folder_label - self._initial_folder_found = project_found - self._initial_folder_found = folder_found - self._initial_tasks_found = tasks_found + self._initial_folder_id = result["folder_id"] + self._initial_folder_label = result["folder_label"] + self._initial_project_found = result["project_found"] + self._initial_folder_found = result["folder_found"] + self._initial_tasks_found = result["tasks_found"] self._emit_event( "initial.context.changed", self.get_initial_context() @@ -345,15 +308,36 @@ class ContextDialogController: # Result of this tool def get_selected_context(self): + project_name = None + folder_id = None + task_id = None + task_name = None + folder_path = None + folder_name = None + if self._confirmed: + project_name = self.get_selected_project_name() + folder_id = self.get_selected_folder_id() + task_id = self.get_selected_task_id() + task_name = self.get_selected_task_name() + + folder_item = None + if folder_id: + folder_item = self._hierarchy_model.get_folder_item( + project_name, folder_id) + + if folder_item: + folder_path = folder_item.path + folder_name = folder_item.name return { - "project": None, - "project_name": None, - "asset": None, - "folder_id": None, - "folder_path": None, - "task": None, - "task_id": None, - "task_name": None, + "project": project_name, + "project_name": project_name, + "asset": folder_name, + "folder_id": folder_id, + "folder_path": folder_path, + "task": task_name, + "task_name": task_name, + "task_id": task_id, + "initial_context_valid": self.is_initial_context_valid(), } def confirm_selection(self): @@ -368,6 +352,54 @@ class ContextDialogController: with open(self._output_path, "w") as stream: json.dump(self.get_selected_context(), stream, indent=4) + def _prepare_initial_context(self, project_name, asset_name): + project_found = True + output = { + "project_found": project_found, + "folder_id": None, + "folder_label": None, + "folder_found": True, + "tasks_found": True, + } + if project_name is None: + asset_name = None + else: + project = ayon_api.get_project(project_name) + project_found = project is not None + output["project_found"] = project_found + if not project_found or not asset_name: + return output + + output["folder_label"] = asset_name + + folder_id = None + folder_found = False + # First try to find by path + folder = ayon_api.get_folder_by_path(project_name, asset_name) + # Try to find by name if folder was not found by path + # - prevent to query by name if 'asset_name' contains '/' + if not folder and "/" not in asset_name: + folder = next( + ayon_api.get_folders( + project_name, folder_names=[asset_name], fields=["id"]), + None + ) + + if folder: + folder_id = folder["id"] + folder_found = True + + output["folder_id"] = folder_id + output["folder_found"] = folder_found + if not folder_found: + return output + + tasks = list(ayon_api.get_tasks( + project_name, folder_ids=[folder_id], fields=["id"] + )) + output["tasks_found"] = bool(tasks) + return output + def _get_event_system(self): """Inner event system for workfiles tool controller. From a62718dc72b62a1823513795695a55b0601bb5a4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 13 Oct 2023 14:48:24 +0200 Subject: [PATCH 0430/1233] project name is in quotes --- openpype/tools/context_dialog/_ayon_window.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/tools/context_dialog/_ayon_window.py b/openpype/tools/context_dialog/_ayon_window.py index 73f9ed139c..f347978392 100644 --- a/openpype/tools/context_dialog/_ayon_window.py +++ b/openpype/tools/context_dialog/_ayon_window.py @@ -468,7 +468,8 @@ class InvalidContextOverlay(QtWidgets.QFrame): lines = [] if not project_found: lines.extend([ - "Requested project {} was not found...".format(project_name), + "Requested project '{}' was not found...".format( + project_name), ]) elif not folder_found: From 2f15dca3f50bbf687efd86bfb8339c284fd6aa7a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 13 Oct 2023 14:51:59 +0200 Subject: [PATCH 0431/1233] removed 'window.py' --- openpype/tools/context_dialog/__init__.py | 12 +++++++----- openpype/tools/context_dialog/window.py | 12 ------------ 2 files changed, 7 insertions(+), 17 deletions(-) delete mode 100644 openpype/tools/context_dialog/window.py diff --git a/openpype/tools/context_dialog/__init__.py b/openpype/tools/context_dialog/__init__.py index 9b10baf903..15b90463da 100644 --- a/openpype/tools/context_dialog/__init__.py +++ b/openpype/tools/context_dialog/__init__.py @@ -1,10 +1,12 @@ -from .window import ( - ContextDialog, - main -) +from openpype import AYON_SERVER_ENABLED + +if AYON_SERVER_ENABLED: + from ._ayon_window import ContextDialog, main +else: + from ._openpype_window import ContextDialog, main __all__ = ( "ContextDialog", - "main" + "main", ) diff --git a/openpype/tools/context_dialog/window.py b/openpype/tools/context_dialog/window.py deleted file mode 100644 index 15b90463da..0000000000 --- a/openpype/tools/context_dialog/window.py +++ /dev/null @@ -1,12 +0,0 @@ -from openpype import AYON_SERVER_ENABLED - -if AYON_SERVER_ENABLED: - from ._ayon_window import ContextDialog, main -else: - from ._openpype_window import ContextDialog, main - - -__all__ = ( - "ContextDialog", - "main", -) From 15532b06c5277b78c54ace0829445d08c3050987 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 19 Oct 2023 19:06:01 +0800 Subject: [PATCH 0432/1233] remove the unused function --- .../max/plugins/publish/extract_tycache.py | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_tycache.py b/openpype/hosts/max/plugins/publish/extract_tycache.py index 49721f47fe..03a6b55f93 100644 --- a/openpype/hosts/max/plugins/publish/extract_tycache.py +++ b/openpype/hosts/max/plugins/publish/extract_tycache.py @@ -159,25 +159,3 @@ class ExtractTyCache(publish.Extractor): opt_list.append(opt) return opt_list - - def get_additional_attribute_args(self, operator, attrs): - """Get Additional args with the attributes pre-set by user - - Args: - operator (str): export particle operator - attrs (dict): a dict which stores the additional attributes - added by user - - Returns: - additional_args(list): a list of additional args for MAX script - """ - additional_args = [] - for key, value in attrs.items(): - tyc_attribute = None - if isinstance(value, bool): - tyc_attribute = f"{operator}.{key}=True" - elif isinstance(value, str): - tyc_attribute = f'{operator}.{key}="{value}"' - additional_args.append(tyc_attribute) - self.log.debug(additional_args) - return additional_args From eb28f19fcfd7380ed9cc7b855263ca03ea53ade5 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 19 Oct 2023 19:17:29 +0800 Subject: [PATCH 0433/1233] change info to debug --- openpype/hosts/max/plugins/publish/extract_tycache.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_tycache.py b/openpype/hosts/max/plugins/publish/extract_tycache.py index 03a6b55f93..d9d7c17cff 100644 --- a/openpype/hosts/max/plugins/publish/extract_tycache.py +++ b/openpype/hosts/max/plugins/publish/extract_tycache.py @@ -32,7 +32,7 @@ class ExtractTyCache(publish.Extractor): # TODO: let user decide the param start = int(instance.context.data["frameStart"]) end = int(instance.context.data.get("frameEnd")) - self.log.info("Extracting Tycache...") + self.log.debug("Extracting Tycache...") stagingdir = self.staging_dir(instance) filename = "{name}.tyc".format(**instance.data) @@ -55,7 +55,6 @@ class ExtractTyCache(publish.Extractor): "stagingDir": stagingdir, } representations.append(representation) - self.log.info(f"Extracted instance '{instance.name}' to: {filenames}") # Get the tyMesh filename for extraction mesh_filename = f"{instance.name}__tyMesh.tyc" @@ -67,8 +66,7 @@ class ExtractTyCache(publish.Extractor): "outputName": '__tyMesh' } representations.append(mesh_repres) - self.log.info( - f"Extracted instance '{instance.name}' to: {mesh_filename}") + self.log.debug(f"Extracted instance '{instance.name}' to: {filenames}") def get_files(self, instance, start_frame, end_frame): """Get file names for tyFlow in tyCache format. From f695ea72848d2df538ccbdcc14a3db482dd711f8 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 19 Oct 2023 19:24:13 +0800 Subject: [PATCH 0434/1233] cleanup the code --- openpype/hosts/max/api/preview_animation.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/openpype/hosts/max/api/preview_animation.py b/openpype/hosts/max/api/preview_animation.py index bb2f1410d4..c6dd8737a7 100644 --- a/openpype/hosts/max/api/preview_animation.py +++ b/openpype/hosts/max/api/preview_animation.py @@ -33,9 +33,6 @@ def viewport_camera(camera): """ original = rt.viewport.getCamera() has_autoplay = rt.preferences.playPreviewWhenDone - nitrousGraphicMgr = rt.NitrousGraphicsManager - viewport_setting = nitrousGraphicMgr.GetActiveViewportSetting() - orig_preset = viewport_setting.ViewportPreset if not original: # if there is no original camera # use the current camera as original @@ -48,7 +45,6 @@ def viewport_camera(camera): finally: rt.viewport.setCamera(original) rt.preferences.playPreviewWhenDone = has_autoplay - viewport_setting.ViewportPreset = orig_preset @contextlib.contextmanager @@ -95,6 +91,7 @@ def viewport_preference_setting(general_viewport, for key, value in nitrous_viewport_original.items(): setattr(viewport_setting, key, value) + def publish_review_animation(instance, staging_dir, start, end, ext, fps, viewport_options): """Function to set up preview arguments in MaxScript. From 100ba33cb296dd1265ad461e3dea85c7f97fe3a2 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Thu, 19 Oct 2023 14:34:06 +0300 Subject: [PATCH 0435/1233] update doc string --- openpype/hosts/houdini/api/lib.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index ed06ec539e..eab77ca19a 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -569,13 +569,23 @@ def get_template_from_value(key, value): def get_frame_data(node, handle_start=0, handle_end=0, log=None): - """Get the frame data: start frame, end frame and steps. + """Get the frame data: start frame, end frame, steps, + start frame with start handle and end frame with end handle. + + This function uses Houdini node as the source of truth + therefore users are allowed to publish their desired frame range. + + It also calculates frame start and end with handles. Args: node(hou.Node) + handle_start(int) + handle_end(int) + log(logging.Logger) Returns: - dict: frame data for start, end and steps. + dict: frame data for start, end, steps, + start with handle and end with handle """ From a37c7539bb6477ce26f7f5c816230a4783e2ccf0 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 19 Oct 2023 12:52:48 +0100 Subject: [PATCH 0436/1233] Changed empty type to Single Arrow --- openpype/hosts/blender/plugins/load/load_abc.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/blender/plugins/load/load_abc.py b/openpype/hosts/blender/plugins/load/load_abc.py index 73f08fcc98..8d1863d4d5 100644 --- a/openpype/hosts/blender/plugins/load/load_abc.py +++ b/openpype/hosts/blender/plugins/load/load_abc.py @@ -148,6 +148,7 @@ class CacheModelLoader(plugin.AssetLoader): bpy.context.scene.collection.children.link(containers) asset_group = bpy.data.objects.new(group_name, object_data=None) + asset_group.empty_display_type = 'SINGLE_ARROW' containers.objects.link(asset_group) objects = self._process(libpath, asset_group, group_name) From 35c2a8328860c671d76e3f77279c5340da7c4e2d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 19 Oct 2023 14:07:49 +0200 Subject: [PATCH 0437/1233] store version contexts by version id --- openpype/tools/ayon_loader/models/actions.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/tools/ayon_loader/models/actions.py b/openpype/tools/ayon_loader/models/actions.py index 3edb04e9eb..177335a933 100644 --- a/openpype/tools/ayon_loader/models/actions.py +++ b/openpype/tools/ayon_loader/models/actions.py @@ -447,11 +447,12 @@ class LoaderActionsModel: project_doc["code"] = project_doc["data"]["code"] for version_doc in version_docs: + version_id = version_doc["_id"] product_id = version_doc["parent"] product_doc = product_docs_by_id[product_id] folder_id = product_doc["parent"] folder_doc = folder_docs_by_id[folder_id] - version_context_by_id[product_id] = { + version_context_by_id[version_id] = { "project": project_doc, "asset": folder_doc, "subset": product_doc, From 100d1d9ca67883bdb956bfc7ac1cb7471d1c9a5a Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Thu, 19 Oct 2023 15:23:16 +0300 Subject: [PATCH 0438/1233] BigRoy's comments: add a commetn and a default value for dictionary get --- .../hosts/houdini/plugins/publish/collect_rop_frame_range.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py b/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py index 6a1871afdc..a368e77ff7 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py +++ b/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py @@ -46,11 +46,11 @@ class CollectRopFrameRange(pyblish.api.InstancePlugin, if not frame_data: return - # Log artist friendly message about the collected frame range + # Log debug message about the collected frame range frame_start = frame_data["frameStart"] frame_end = frame_data["frameEnd"] - if attr_values.get("use_handles"): + if attr_values.get("use_handles", self.use_asset_handles): self.log.debug( "Full Frame range with Handles " "[{frame_start_handle} - {frame_end_handle}]" @@ -65,6 +65,7 @@ class CollectRopFrameRange(pyblish.api.InstancePlugin, "start and end handles are set to 0." ) + # Log collected frame range to the user message = "Frame range [{frame_start} - {frame_end}]".format( frame_start=frame_start, frame_end=frame_end From 349cf6d35d8544b84d28e4f90e771557dc25da6b Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 19 Oct 2023 20:25:16 +0800 Subject: [PATCH 0439/1233] support resolution settings --- openpype/hosts/max/api/preview_animation.py | 58 ++++++++++++++----- .../hosts/max/plugins/create/create_review.py | 12 ++++ .../max/plugins/publish/collect_review.py | 4 +- .../publish/extract_review_animation.py | 2 + 4 files changed, 59 insertions(+), 17 deletions(-) diff --git a/openpype/hosts/max/api/preview_animation.py b/openpype/hosts/max/api/preview_animation.py index c6dd8737a7..601ff65c81 100644 --- a/openpype/hosts/max/api/preview_animation.py +++ b/openpype/hosts/max/api/preview_animation.py @@ -24,6 +24,26 @@ def play_preview_when_done(has_autoplay): rt.preferences.playPreviewWhenDone = current_playback +@contextlib.contextmanager +def render_resolution(width, height): + """Function to set render resolution option during + context + + Args: + width (int): render width + height (int): render height + """ + current_renderWidth = rt.renderWidth + current_renderHeight = rt.renderHeight + try: + rt.renderWidth = width + rt.renderHeight = height + yield + finally: + rt.renderWidth = current_renderWidth + rt.renderHeight = current_renderHeight + + @contextlib.contextmanager def viewport_camera(camera): """Function to set viewport camera during context @@ -217,6 +237,7 @@ def publish_preview_animation( instance, staging_dir, ext, review_camera, startFrame=None, endFrame=None, + resolution=None, viewport_options=None): """Render camera review animation @@ -235,25 +256,30 @@ def publish_preview_animation( endFrame = int(rt.animationRange.end) if viewport_options is None: viewport_options = viewport_options_for_preview_animation() + if resolution is None: + resolution = (1920, 1080) with play_preview_when_done(False): with viewport_camera(review_camera): - if int(get_max_version()) < 2024: - with viewport_preference_setting( - viewport_options["general_viewport"], - viewport_options["nitrous_viewport"], - viewport_options["vp_btn_mgr"]): - percentSize = viewport_options.get("percentSize", 100) + width, height = resolution + with render_resolution(width, height): + if int(get_max_version()) < 2024: + with viewport_preference_setting( + viewport_options["general_viewport"], + viewport_options["nitrous_viewport"], + viewport_options["vp_btn_mgr"]): + percentSize = viewport_options.get("percentSize", 100) - publish_preview_sequences( - staging_dir, instance.name, - startFrame, endFrame, percentSize, ext) - else: - fps = instance.data["fps"] - rt.completeRedraw() - preview_arg = publish_review_animation(instance, staging_dir, - startFrame, endFrame, - ext, fps, viewport_options) - rt.execute(preview_arg) + publish_preview_sequences( + staging_dir, instance.name, + startFrame, endFrame, percentSize, ext) + else: + fps = instance.data["fps"] + rt.completeRedraw() + preview_arg = publish_review_animation( + instance, staging_dir, + startFrame, endFrame, + ext, fps, viewport_options) + rt.execute(preview_arg) rt.completeRedraw() diff --git a/openpype/hosts/max/plugins/create/create_review.py b/openpype/hosts/max/plugins/create/create_review.py index 977c018f5c..bbcdce90b7 100644 --- a/openpype/hosts/max/plugins/create/create_review.py +++ b/openpype/hosts/max/plugins/create/create_review.py @@ -18,6 +18,8 @@ class CreateReview(plugin.MaxCreator): "creator_attributes", dict()) for key in ["imageFormat", "keepImages", + "review_width", + "review_height", "percentSize", "visualStyleMode", "viewportPreset", @@ -48,6 +50,16 @@ class CreateReview(plugin.MaxCreator): "DXMode", "Customize"] return [ + NumberDef("review_width", + label="Review width", + decimals=0, + minimum=0, + default=1920), + NumberDef("review_height", + label="Review height", + decimals=0, + minimum=0, + default=1080), BoolDef("keepImages", label="Keep Image Sequences", default=False), diff --git a/openpype/hosts/max/plugins/publish/collect_review.py b/openpype/hosts/max/plugins/publish/collect_review.py index 904a4eab0f..cfd48edb15 100644 --- a/openpype/hosts/max/plugins/publish/collect_review.py +++ b/openpype/hosts/max/plugins/publish/collect_review.py @@ -34,7 +34,9 @@ class CollectReview(pyblish.api.InstancePlugin, "keepImages": creator_attrs["keepImages"], "frameStart": instance.context.data["frameStart"], "frameEnd": instance.context.data["frameEnd"], - "fps": instance.context.data["fps"] + "fps": instance.context.data["fps"], + "resolution": (creator_attrs["review_width"], + creator_attrs["review_height"]) } if int(get_max_version()) >= 2024: diff --git a/openpype/hosts/max/plugins/publish/extract_review_animation.py b/openpype/hosts/max/plugins/publish/extract_review_animation.py index d2de981236..c308aadfdb 100644 --- a/openpype/hosts/max/plugins/publish/extract_review_animation.py +++ b/openpype/hosts/max/plugins/publish/extract_review_animation.py @@ -33,10 +33,12 @@ class ExtractReviewAnimation(publish.Extractor): review_camera = instance.data["review_camera"] viewport_options = instance.data.get("viewport_options", {}) + resolution = instance.data.get("resolution", ()) publish_preview_animation( instance, staging_dir, ext, review_camera, startFrame=start, endFrame=end, + resolution=resolution, viewport_options=viewport_options) tags = ["review"] From 0016f8aa3d4dea931ac89ce4c977bff9a442cf45 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 13 Oct 2023 15:33:00 +0200 Subject: [PATCH 0440/1233] created copy of push to project tool --- .../tools/ayon_push_to_project/__init__.py | 0 openpype/tools/ayon_push_to_project/app.py | 28 + .../ayon_push_to_project/control_context.py | 678 +++++++++ .../ayon_push_to_project/control_integrate.py | 1210 +++++++++++++++++ openpype/tools/ayon_push_to_project/window.py | 829 +++++++++++ 5 files changed, 2745 insertions(+) create mode 100644 openpype/tools/ayon_push_to_project/__init__.py create mode 100644 openpype/tools/ayon_push_to_project/app.py create mode 100644 openpype/tools/ayon_push_to_project/control_context.py create mode 100644 openpype/tools/ayon_push_to_project/control_integrate.py create mode 100644 openpype/tools/ayon_push_to_project/window.py diff --git a/openpype/tools/ayon_push_to_project/__init__.py b/openpype/tools/ayon_push_to_project/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openpype/tools/ayon_push_to_project/app.py b/openpype/tools/ayon_push_to_project/app.py new file mode 100644 index 0000000000..b3ec33f353 --- /dev/null +++ b/openpype/tools/ayon_push_to_project/app.py @@ -0,0 +1,28 @@ +import click + +from openpype.tools.utils import get_openpype_qt_app +from openpype.tools.push_to_project.window import PushToContextSelectWindow + + +@click.command() +@click.option("--project", help="Source project name") +@click.option("--version", help="Source version id") +def main(project, version): + """Run PushToProject tool to integrate version in different project. + + Args: + project (str): Source project name. + version (str): Version id. + """ + + app = get_openpype_qt_app() + + window = PushToContextSelectWindow() + window.show() + window.controller.set_source(project, version) + + app.exec_() + + +if __name__ == "__main__": + main() diff --git a/openpype/tools/ayon_push_to_project/control_context.py b/openpype/tools/ayon_push_to_project/control_context.py new file mode 100644 index 0000000000..e4058893d5 --- /dev/null +++ b/openpype/tools/ayon_push_to_project/control_context.py @@ -0,0 +1,678 @@ +import re +import collections +import threading + +from openpype.client import ( + get_projects, + get_assets, + get_asset_by_id, + get_subset_by_id, + get_version_by_id, + get_representations, +) +from openpype.settings import get_project_settings +from openpype.lib import prepare_template_data +from openpype.lib.events import EventSystem +from openpype.pipeline.create import ( + SUBSET_NAME_ALLOWED_SYMBOLS, + get_subset_name_template, +) + +from .control_integrate import ( + ProjectPushItem, + ProjectPushItemProcess, + ProjectPushItemStatus, +) + + +class AssetItem: + def __init__( + self, + entity_id, + name, + icon_name, + icon_color, + parent_id, + has_children + ): + self.id = entity_id + self.name = name + self.icon_name = icon_name + self.icon_color = icon_color + self.parent_id = parent_id + self.has_children = has_children + + @classmethod + def from_doc(cls, asset_doc, has_children=True): + parent_id = asset_doc["data"].get("visualParent") + if parent_id is not None: + parent_id = str(parent_id) + return cls( + str(asset_doc["_id"]), + asset_doc["name"], + asset_doc["data"].get("icon"), + asset_doc["data"].get("color"), + parent_id, + has_children + ) + + +class TaskItem: + def __init__(self, asset_id, name, task_type, short_name): + self.asset_id = asset_id + self.name = name + self.task_type = task_type + self.short_name = short_name + + @classmethod + def from_asset_doc(cls, asset_doc, project_doc): + asset_tasks = asset_doc["data"].get("tasks") or {} + project_task_types = project_doc["config"]["tasks"] + output = [] + for task_name, task_info in asset_tasks.items(): + task_type = task_info.get("type") + task_type_info = project_task_types.get(task_type) or {} + output.append(cls( + asset_doc["_id"], + task_name, + task_type, + task_type_info.get("short_name") + )) + return output + + +class EntitiesModel: + def __init__(self, event_system): + self._event_system = event_system + self._project_names = None + self._project_docs_by_name = {} + self._assets_by_project = {} + self._tasks_by_asset_id = collections.defaultdict(dict) + + def has_cached_projects(self): + return self._project_names is None + + def has_cached_assets(self, project_name): + if not project_name: + return True + return project_name in self._assets_by_project + + def has_cached_tasks(self, project_name): + return self.has_cached_assets(project_name) + + def get_projects(self): + if self._project_names is None: + self.refresh_projects() + return list(self._project_names) + + def get_assets(self, project_name): + if project_name not in self._assets_by_project: + self.refresh_assets(project_name) + return dict(self._assets_by_project[project_name]) + + def get_asset_by_id(self, project_name, asset_id): + return self._assets_by_project[project_name].get(asset_id) + + def get_tasks(self, project_name, asset_id): + if not project_name or not asset_id: + return [] + + if project_name not in self._tasks_by_asset_id: + self.refresh_assets(project_name) + + all_task_items = self._tasks_by_asset_id[project_name] + asset_task_items = all_task_items.get(asset_id) + if not asset_task_items: + return [] + return list(asset_task_items) + + def refresh_projects(self, force=False): + self._event_system.emit( + "projects.refresh.started", {}, "entities.model" + ) + if force or self._project_names is None: + project_names = [] + project_docs_by_name = {} + for project_doc in get_projects(): + library_project = project_doc["data"].get("library_project") + if not library_project: + continue + project_name = project_doc["name"] + project_names.append(project_name) + project_docs_by_name[project_name] = project_doc + self._project_names = project_names + self._project_docs_by_name = project_docs_by_name + self._event_system.emit( + "projects.refresh.finished", {}, "entities.model" + ) + + def _refresh_assets(self, project_name): + asset_items_by_id = {} + task_items_by_asset_id = {} + self._assets_by_project[project_name] = asset_items_by_id + self._tasks_by_asset_id[project_name] = task_items_by_asset_id + if not project_name: + return + + project_doc = self._project_docs_by_name[project_name] + asset_docs_by_parent_id = collections.defaultdict(list) + for asset_doc in get_assets(project_name): + parent_id = asset_doc["data"].get("visualParent") + asset_docs_by_parent_id[parent_id].append(asset_doc) + + hierarchy_queue = collections.deque() + for asset_doc in asset_docs_by_parent_id[None]: + hierarchy_queue.append(asset_doc) + + while hierarchy_queue: + asset_doc = hierarchy_queue.popleft() + children = asset_docs_by_parent_id[asset_doc["_id"]] + asset_item = AssetItem.from_doc(asset_doc, len(children) > 0) + asset_items_by_id[asset_item.id] = asset_item + task_items_by_asset_id[asset_item.id] = ( + TaskItem.from_asset_doc(asset_doc, project_doc) + ) + for child in children: + hierarchy_queue.append(child) + + def refresh_assets(self, project_name, force=False): + self._event_system.emit( + "assets.refresh.started", + {"project_name": project_name}, + "entities.model" + ) + + if force or project_name not in self._assets_by_project: + self._refresh_assets(project_name) + + self._event_system.emit( + "assets.refresh.finished", + {"project_name": project_name}, + "entities.model" + ) + + +class SelectionModel: + def __init__(self, event_system): + self._event_system = event_system + + self.project_name = None + self.asset_id = None + self.task_name = None + + def select_project(self, project_name): + if self.project_name == project_name: + return + + self.project_name = project_name + self._event_system.emit( + "project.changed", + {"project_name": project_name}, + "selection.model" + ) + + def select_asset(self, asset_id): + if self.asset_id == asset_id: + return + self.asset_id = asset_id + self._event_system.emit( + "asset.changed", + { + "project_name": self.project_name, + "asset_id": asset_id + }, + "selection.model" + ) + + def select_task(self, task_name): + if self.task_name == task_name: + return + self.task_name = task_name + self._event_system.emit( + "task.changed", + { + "project_name": self.project_name, + "asset_id": self.asset_id, + "task_name": task_name + }, + "selection.model" + ) + + +class UserPublishValues: + """Helper object to validate values required for push to different project. + + Args: + event_system (EventSystem): Event system to catch and emit events. + new_asset_name (str): Name of new asset name. + variant (str): Variant for new subset name in new project. + """ + + asset_name_regex = re.compile("^[a-zA-Z0-9_.]+$") + variant_regex = re.compile("^[{}]+$".format(SUBSET_NAME_ALLOWED_SYMBOLS)) + + def __init__(self, event_system): + self._event_system = event_system + self._new_asset_name = None + self._variant = None + self._comment = None + self._is_variant_valid = False + self._is_new_asset_name_valid = False + + self.set_new_asset("") + self.set_variant("") + self.set_comment("") + + @property + def new_asset_name(self): + return self._new_asset_name + + @property + def variant(self): + return self._variant + + @property + def comment(self): + return self._comment + + @property + def is_variant_valid(self): + return self._is_variant_valid + + @property + def is_new_asset_name_valid(self): + return self._is_new_asset_name_valid + + @property + def is_valid(self): + return self.is_variant_valid and self.is_new_asset_name_valid + + def set_variant(self, variant): + if variant == self._variant: + return + + old_variant = self._variant + old_is_valid = self._is_variant_valid + + self._variant = variant + is_valid = False + if variant: + is_valid = self.variant_regex.match(variant) is not None + self._is_variant_valid = is_valid + + changes = { + key: {"new": new, "old": old} + for key, old, new in ( + ("variant", old_variant, variant), + ("is_valid", old_is_valid, is_valid) + ) + } + + self._event_system.emit( + "variant.changed", + { + "variant": variant, + "is_valid": self._is_variant_valid, + "changes": changes + }, + "user_values" + ) + + def set_new_asset(self, asset_name): + if self._new_asset_name == asset_name: + return + old_asset_name = self._new_asset_name + old_is_valid = self._is_new_asset_name_valid + self._new_asset_name = asset_name + is_valid = True + if asset_name: + is_valid = ( + self.asset_name_regex.match(asset_name) is not None + ) + self._is_new_asset_name_valid = is_valid + changes = { + key: {"new": new, "old": old} + for key, old, new in ( + ("new_asset_name", old_asset_name, asset_name), + ("is_valid", old_is_valid, is_valid) + ) + } + + self._event_system.emit( + "new_asset_name.changed", + { + "new_asset_name": self._new_asset_name, + "is_valid": self._is_new_asset_name_valid, + "changes": changes + }, + "user_values" + ) + + def set_comment(self, comment): + if comment == self._comment: + return + old_comment = self._comment + self._comment = comment + self._event_system.emit( + "comment.changed", + { + "comment": comment, + "changes": { + "comment": {"new": comment, "old": old_comment} + } + }, + "user_values" + ) + + +class PushToContextController: + def __init__(self, project_name=None, version_id=None): + self._src_project_name = None + self._src_version_id = None + self._src_asset_doc = None + self._src_subset_doc = None + self._src_version_doc = None + + event_system = EventSystem() + entities_model = EntitiesModel(event_system) + selection_model = SelectionModel(event_system) + user_values = UserPublishValues(event_system) + + self._event_system = event_system + self._entities_model = entities_model + self._selection_model = selection_model + self._user_values = user_values + + event_system.add_callback("project.changed", self._on_project_change) + event_system.add_callback("asset.changed", self._invalidate) + event_system.add_callback("variant.changed", self._invalidate) + event_system.add_callback("new_asset_name.changed", self._invalidate) + + self._submission_enabled = False + self._process_thread = None + self._process_item = None + + self.set_source(project_name, version_id) + + def _get_task_info_from_repre_docs(self, asset_doc, repre_docs): + asset_tasks = asset_doc["data"].get("tasks") or {} + found_comb = [] + for repre_doc in repre_docs: + context = repre_doc["context"] + task_info = context.get("task") + if task_info is None: + continue + + task_name = None + task_type = None + if isinstance(task_info, str): + task_name = task_info + asset_task_info = asset_tasks.get(task_info) or {} + task_type = asset_task_info.get("type") + + elif isinstance(task_info, dict): + task_name = task_info.get("name") + task_type = task_info.get("type") + + if task_name and task_type: + return task_name, task_type + + if task_name: + found_comb.append((task_name, task_type)) + + for task_name, task_type in found_comb: + return task_name, task_type + return None, None + + def _get_src_variant(self): + project_name = self._src_project_name + version_doc = self._src_version_doc + asset_doc = self._src_asset_doc + repre_docs = get_representations( + project_name, version_ids=[version_doc["_id"]] + ) + task_name, task_type = self._get_task_info_from_repre_docs( + asset_doc, repre_docs + ) + + project_settings = get_project_settings(project_name) + subset_doc = self.src_subset_doc + family = subset_doc["data"].get("family") + if not family: + family = subset_doc["data"]["families"][0] + template = get_subset_name_template( + self._src_project_name, + family, + task_name, + task_type, + None, + project_settings=project_settings + ) + template_low = template.lower() + variant_placeholder = "{variant}" + if ( + variant_placeholder not in template_low + or (not task_name and "{task" in template_low) + ): + return "" + + idx = template_low.index(variant_placeholder) + template_s = template[:idx] + template_e = template[idx + len(variant_placeholder):] + fill_data = prepare_template_data({ + "family": family, + "task": task_name + }) + try: + subset_s = template_s.format(**fill_data) + subset_e = template_e.format(**fill_data) + except Exception as exc: + print("Failed format", exc) + return "" + + subset_name = self.src_subset_doc["name"] + if ( + (subset_s and not subset_name.startswith(subset_s)) + or (subset_e and not subset_name.endswith(subset_e)) + ): + return "" + + if subset_s: + subset_name = subset_name[len(subset_s):] + if subset_e: + subset_name = subset_name[:len(subset_e)] + return subset_name + + def set_source(self, project_name, version_id): + if ( + project_name == self._src_project_name + and version_id == self._src_version_id + ): + return + + self._src_project_name = project_name + self._src_version_id = version_id + asset_doc = None + subset_doc = None + version_doc = None + if project_name and version_id: + version_doc = get_version_by_id(project_name, version_id) + + if version_doc: + subset_doc = get_subset_by_id(project_name, version_doc["parent"]) + + if subset_doc: + asset_doc = get_asset_by_id(project_name, subset_doc["parent"]) + + self._src_asset_doc = asset_doc + self._src_subset_doc = subset_doc + self._src_version_doc = version_doc + if asset_doc: + self.user_values.set_new_asset(asset_doc["name"]) + variant = self._get_src_variant() + if variant: + self.user_values.set_variant(variant) + + comment = version_doc["data"].get("comment") + if comment: + self.user_values.set_comment(comment) + + self._event_system.emit( + "source.changed", { + "project_name": project_name, + "version_id": version_id + }, + "controller" + ) + + @property + def src_project_name(self): + return self._src_project_name + + @property + def src_version_id(self): + return self._src_version_id + + @property + def src_label(self): + if not self._src_project_name or not self._src_version_id: + return "Source is not defined" + + asset_doc = self.src_asset_doc + if not asset_doc: + return "Source is invalid" + + asset_path_parts = list(asset_doc["data"]["parents"]) + asset_path_parts.append(asset_doc["name"]) + asset_path = "/".join(asset_path_parts) + subset_doc = self.src_subset_doc + version_doc = self.src_version_doc + return "Source: {}/{}/{}/v{:0>3}".format( + self._src_project_name, + asset_path, + subset_doc["name"], + version_doc["name"] + ) + + @property + def src_version_doc(self): + return self._src_version_doc + + @property + def src_subset_doc(self): + return self._src_subset_doc + + @property + def src_asset_doc(self): + return self._src_asset_doc + + @property + def event_system(self): + return self._event_system + + @property + def model(self): + return self._entities_model + + @property + def selection_model(self): + return self._selection_model + + @property + def user_values(self): + return self._user_values + + @property + def submission_enabled(self): + return self._submission_enabled + + def _on_project_change(self, event): + project_name = event["project_name"] + self.model.refresh_assets(project_name) + self._invalidate() + + def _invalidate(self): + submission_enabled = self._check_submit_validations() + if submission_enabled == self._submission_enabled: + return + self._submission_enabled = submission_enabled + self._event_system.emit( + "submission.enabled.changed", + {"enabled": submission_enabled}, + "controller" + ) + + def _check_submit_validations(self): + if not self._user_values.is_valid: + return False + + if not self.selection_model.project_name: + return False + + if ( + not self._user_values.new_asset_name + and not self.selection_model.asset_id + ): + return False + + return True + + def get_selected_asset_name(self): + project_name = self._selection_model.project_name + asset_id = self._selection_model.asset_id + if not project_name or not asset_id: + return None + asset_item = self._entities_model.get_asset_by_id( + project_name, asset_id + ) + if asset_item: + return asset_item.name + return None + + def submit(self, wait=True): + if not self.submission_enabled: + return + + if self._process_thread is not None: + return + + item = ProjectPushItem( + self.src_project_name, + self.src_version_id, + self.selection_model.project_name, + self.selection_model.asset_id, + self.selection_model.task_name, + self.user_values.variant, + comment=self.user_values.comment, + new_asset_name=self.user_values.new_asset_name, + dst_version=1 + ) + + status_item = ProjectPushItemStatus(event_system=self._event_system) + process_item = ProjectPushItemProcess(item, status_item) + self._process_item = process_item + self._event_system.emit("submit.started", {}, "controller") + if wait: + self._submit_callback() + self._process_item = None + return process_item + + thread = threading.Thread(target=self._submit_callback) + self._process_thread = thread + thread.start() + return process_item + + def wait_for_process_thread(self): + if self._process_thread is None: + return + self._process_thread.join() + self._process_thread = None + + def _submit_callback(self): + process_item = self._process_item + if process_item is None: + return + process_item.process() + self._event_system.emit("submit.finished", {}, "controller") + if process_item is self._process_item: + self._process_item = None diff --git a/openpype/tools/ayon_push_to_project/control_integrate.py b/openpype/tools/ayon_push_to_project/control_integrate.py new file mode 100644 index 0000000000..a822339ccf --- /dev/null +++ b/openpype/tools/ayon_push_to_project/control_integrate.py @@ -0,0 +1,1210 @@ +import os +import re +import copy +import socket +import itertools +import datetime +import sys +import traceback + +from bson.objectid import ObjectId + +from openpype.client import ( + get_project, + get_assets, + get_asset_by_id, + get_subset_by_id, + get_subset_by_name, + get_version_by_id, + get_last_version_by_subset_id, + get_version_by_name, + get_representations, +) +from openpype.client.operations import ( + OperationsSession, + new_asset_document, + new_subset_document, + new_version_doc, + new_representation_doc, + prepare_version_update_data, + prepare_representation_update_data, +) +from openpype.modules import ModulesManager +from openpype.lib import ( + StringTemplate, + get_openpype_username, + get_formatted_current_time, + source_hash, +) + +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 + +UNKNOWN = object() + + +class PushToProjectError(Exception): + pass + + +class FileItem(object): + def __init__(self, path): + self.path = path + + @property + def is_valid_file(self): + return os.path.exists(self.path) and os.path.isfile(self.path) + + +class SourceFile(FileItem): + def __init__(self, path, frame=None, udim=None): + super(SourceFile, self).__init__(path) + self.frame = frame + self.udim = udim + + def __repr__(self): + subparts = [self.__class__.__name__] + if self.frame is not None: + subparts.append("frame: {}".format(self.frame)) + if self.udim is not None: + subparts.append("UDIM: {}".format(self.udim)) + + return "<{}> '{}'".format(" - ".join(subparts), self.path) + + +class ResourceFile(FileItem): + def __init__(self, path, relative_path): + super(ResourceFile, self).__init__(path) + self.relative_path = relative_path + + def __repr__(self): + return "<{}> '{}'".format(self.__class__.__name__, self.relative_path) + + @property + def is_valid_file(self): + if not self.relative_path: + return False + return super(ResourceFile, self).is_valid_file + + +class ProjectPushItem: + def __init__( + self, + src_project_name, + src_version_id, + dst_project_name, + dst_asset_id, + dst_task_name, + variant, + comment=None, + new_asset_name=None, + dst_version=None + ): + self.src_project_name = src_project_name + self.src_version_id = src_version_id + self.dst_project_name = dst_project_name + self.dst_asset_id = dst_asset_id + self.dst_task_name = dst_task_name + self.dst_version = dst_version + self.variant = variant + self.new_asset_name = new_asset_name + self.comment = comment or "" + self._id = "|".join([ + src_project_name, + src_version_id, + dst_project_name, + str(dst_asset_id), + str(new_asset_name), + str(dst_task_name), + str(dst_version) + ]) + + @property + def id(self): + return self._id + + def __repr__(self): + return "<{} - {}>".format(self.__class__.__name__, self.id) + + +class StatusMessage: + def __init__(self, message, level): + self.message = message + self.level = level + + def __str__(self): + return "{}: {}".format(self.level.upper(), self.message) + + def __repr__(self): + return "<{} - {}> {}".format( + self.__class__.__name__, self.level.upper, self.message + ) + + +class ProjectPushItemStatus: + def __init__( + self, + failed=False, + finished=False, + fail_reason=None, + formatted_traceback=None, + messages=None, + event_system=None + ): + if messages is None: + messages = [] + self._failed = failed + self._finished = finished + self._fail_reason = fail_reason + self._traceback = formatted_traceback + self._messages = messages + self._event_system = event_system + + def emit_event(self, topic, data=None): + if self._event_system is None: + return + + self._event_system.emit(topic, data or {}, "push.status") + + def get_finished(self): + """Processing of push to project finished. + + Returns: + bool: Finished. + """ + + return self._finished + + def set_finished(self, finished=True): + """Mark status as finished. + + Args: + finished (bool): Processing finished (failed or not). + """ + + if finished != self._finished: + self._finished = finished + self.emit_event("push.finished.changed", {"finished": finished}) + + finished = property(get_finished, set_finished) + + def set_failed(self, fail_reason, exc_info=None): + """Set status as failed. + + Attribute 'fail_reason' can change automatically based on passed value. + Reason is unset if 'failed' is 'False' and is set do default reason if + is set to 'True' and reason is not set. + + Args: + failed (bool): Push to project failed. + fail_reason (str): Reason why failed. + """ + + failed = True + if not fail_reason and not exc_info: + failed = False + + full_traceback = None + if exc_info is not None: + full_traceback = "".join(traceback.format_exception(*exc_info)) + if not fail_reason: + fail_reason = "Failed without specified reason" + + if ( + self._failed == failed + and self._traceback == full_traceback + and self._fail_reason == fail_reason + ): + return + + self._failed = failed + self._fail_reason = fail_reason or None + self._traceback = full_traceback + + self.emit_event( + "push.failed.changed", + { + "failed": failed, + "reason": fail_reason, + "traceback": full_traceback + } + ) + + @property + def failed(self): + """Processing failed. + + Returns: + bool: Processing failed. + """ + + return self._failed + + @property + def fail_reason(self): + """Reason why push to process failed. + + Returns: + Union[str, None]: Reason why push failed or None. + """ + + return self._fail_reason + + @property + def traceback(self): + """Traceback of failed process. + + Traceback is available only if unhandled exception happened. + + Returns: + Union[str, None]: Formatted traceback. + """ + + return self._traceback + + # Loggin helpers + # TODO better logging + def add_message(self, message, level): + message_obj = StatusMessage(message, level) + self._messages.append(message_obj) + self.emit_event( + "push.message.added", + {"message": message, "level": level} + ) + print(message_obj) + return message_obj + + def debug(self, message): + return self.add_message(message, "debug") + + def info(self, message): + return self.add_message(message, "info") + + def warning(self, message): + return self.add_message(message, "warning") + + def error(self, message): + return self.add_message(message, "error") + + def critical(self, message): + return self.add_message(message, "critical") + + +class ProjectPushRepreItem: + """Representation item. + + Representation item based on representation document and project roots. + + Representation document may have reference to: + - source files: Files defined with publish template + - resource files: Files that should be in publish directory + but filenames are not template based. + + Args: + repre_doc (Dict[str, Ant]): Representation document. + roots (Dict[str, str]): Project roots (based on project anatomy). + """ + + def __init__(self, repre_doc, roots): + self._repre_doc = repre_doc + self._roots = roots + self._src_files = None + self._resource_files = None + self._frame = UNKNOWN + + @property + def repre_doc(self): + return self._repre_doc + + @property + def src_files(self): + if self._src_files is None: + self.get_source_files() + return self._src_files + + @property + def resource_files(self): + if self._resource_files is None: + self.get_source_files() + return self._resource_files + + @staticmethod + def _clean_path(path): + new_value = path.replace("\\", "/") + while "//" in new_value: + new_value = new_value.replace("//", "/") + return new_value + + @staticmethod + def _get_relative_path(path, src_dirpath): + dirpath, basename = os.path.split(path) + if not dirpath.lower().startswith(src_dirpath.lower()): + return None + + relative_dir = dirpath[len(src_dirpath):].lstrip("/") + if relative_dir: + relative_path = "/".join([relative_dir, basename]) + else: + relative_path = basename + return relative_path + + @property + def frame(self): + """First frame of representation files. + + This value will be in representation document context if is sequence. + + Returns: + Union[int, None]: First frame in representation files based on + source files or None if frame is not part of filename. + """ + + if self._frame is UNKNOWN: + frame = None + for src_file in self.src_files: + src_frame = src_file.frame + if ( + src_frame is not None + and (frame is None or src_frame < frame) + ): + frame = src_frame + self._frame = frame + return self._frame + + @staticmethod + def validate_source_files(src_files, resource_files): + if not src_files: + raise AssertionError(( + "Couldn't figure out source files from representation." + " Found resource files {}" + ).format(", ".join(str(i) for i in resource_files))) + + invalid_items = [ + item + for item in itertools.chain(src_files, resource_files) + if not item.is_valid_file + ] + if invalid_items: + raise AssertionError(( + "Source files that were not found on disk: {}" + ).format(", ".join(str(i) for i in invalid_items))) + + def get_source_files(self): + if self._src_files is not None: + return self._src_files, self._resource_files + + repre_context = self._repre_doc["context"] + if "frame" in repre_context or "udim" in repre_context: + src_files, resource_files = self._get_source_files_with_frames() + else: + src_files, resource_files = self._get_source_files() + + self.validate_source_files(src_files, resource_files) + + self._src_files = src_files + self._resource_files = resource_files + return self._src_files, self._resource_files + + def _get_source_files_with_frames(self): + frame_placeholder = "__frame__" + udim_placeholder = "__udim__" + src_files = [] + resource_files = [] + template = self._repre_doc["data"]["template"] + # Remove padding from 'udim' and 'frame' formatting keys + # - "{frame:0>4}" -> "{frame}" + for key in ("udim", "frame"): + sub_part = "{" + key + "[^}]*}" + replacement = "{{{}}}".format(key) + template = re.sub(sub_part, replacement, template) + + repre_context = self._repre_doc["context"] + fill_repre_context = copy.deepcopy(repre_context) + if "frame" in fill_repre_context: + fill_repre_context["frame"] = frame_placeholder + + if "udim" in fill_repre_context: + fill_repre_context["udim"] = udim_placeholder + + fill_roots = fill_repre_context["root"] + for root_name in tuple(fill_roots.keys()): + fill_roots[root_name] = "{{root[{}]}}".format(root_name) + repre_path = StringTemplate.format_template( + template, fill_repre_context) + repre_path = self._clean_path(repre_path) + src_dirpath, src_basename = os.path.split(repre_path) + src_basename = ( + re.escape(src_basename) + .replace(frame_placeholder, "(?P[0-9]+)") + .replace(udim_placeholder, "(?P[0-9]+)") + ) + src_basename_regex = re.compile("^{}$".format(src_basename)) + for file_info in self._repre_doc["files"]: + filepath_template = self._clean_path(file_info["path"]) + filepath = self._clean_path( + filepath_template.format(root=self._roots) + ) + dirpath, basename = os.path.split(filepath_template) + if ( + dirpath.lower() != src_dirpath.lower() + or not src_basename_regex.match(basename) + ): + relative_path = self._get_relative_path(filepath, src_dirpath) + resource_files.append(ResourceFile(filepath, relative_path)) + continue + + filepath = os.path.join(src_dirpath, basename) + frame = None + udim = None + for item in src_basename_regex.finditer(basename): + group_name = item.lastgroup + value = item.group(group_name) + if group_name == "frame": + frame = int(value) + elif group_name == "udim": + udim = value + + src_files.append(SourceFile(filepath, frame, udim)) + + return src_files, resource_files + + def _get_source_files(self): + src_files = [] + resource_files = [] + template = self._repre_doc["data"]["template"] + repre_context = self._repre_doc["context"] + fill_repre_context = copy.deepcopy(repre_context) + fill_roots = fill_repre_context["root"] + for root_name in tuple(fill_roots.keys()): + fill_roots[root_name] = "{{root[{}]}}".format(root_name) + repre_path = StringTemplate.format_template(template, + fill_repre_context) + repre_path = self._clean_path(repre_path) + src_dirpath = os.path.dirname(repre_path) + for file_info in self._repre_doc["files"]: + filepath_template = self._clean_path(file_info["path"]) + filepath = self._clean_path( + filepath_template.format(root=self._roots)) + + if filepath_template.lower() == repre_path.lower(): + src_files.append( + SourceFile(repre_path.format(root=self._roots)) + ) + else: + relative_path = self._get_relative_path( + filepath_template, src_dirpath + ) + resource_files.append( + ResourceFile(filepath, relative_path) + ) + return src_files, resource_files + + +class ProjectPushItemProcess: + """ + Args: + item (ProjectPushItem): Item which is being processed. + item_status (ProjectPushItemStatus): Object to store status. + """ + + # TODO where to get host?!!! + host_name = "republisher" + + def __init__(self, item, item_status=None): + self._item = item + + self._src_project_doc = None + self._src_asset_doc = None + self._src_subset_doc = None + self._src_version_doc = None + self._src_repre_items = None + self._src_anatomy = None + + self._project_doc = None + self._anatomy = None + self._asset_doc = None + self._created_asset_doc = None + self._task_info = None + self._subset_doc = None + self._version_doc = None + + self._family = None + self._subset_name = None + + self._project_settings = None + self._template_name = None + + if item_status is None: + item_status = ProjectPushItemStatus() + self._status = item_status + self._operations = OperationsSession() + self._file_transaction = FileTransaction() + + @property + def status(self): + return self._status + + @property + def src_project_doc(self): + return self._src_project_doc + + @property + def src_anatomy(self): + return self._src_anatomy + + @property + def src_asset_doc(self): + return self._src_asset_doc + + @property + def src_subset_doc(self): + return self._src_subset_doc + + @property + def src_version_doc(self): + return self._src_version_doc + + @property + def src_repre_items(self): + return self._src_repre_items + + @property + def project_doc(self): + return self._project_doc + + @property + def anatomy(self): + return self._anatomy + + @property + def project_settings(self): + return self._project_settings + + @property + def asset_doc(self): + return self._asset_doc + + @property + def task_info(self): + return self._task_info + + @property + def subset_doc(self): + return self._subset_doc + + @property + def version_doc(self): + return self._version_doc + + @property + def variant(self): + return self._item.variant + + @property + def family(self): + return self._family + + @property + def subset_name(self): + return self._subset_name + + @property + def template_name(self): + return self._template_name + + def fill_source_variables(self): + src_project_name = self._item.src_project_name + src_version_id = self._item.src_version_id + + project_doc = get_project(src_project_name) + if not project_doc: + self._status.set_failed( + f"Source project \"{src_project_name}\" was not found" + ) + raise PushToProjectError(self._status.fail_reason) + + self._status.debug(f"Project '{src_project_name}' found") + + version_doc = get_version_by_id(src_project_name, src_version_id) + if not version_doc: + self._status.set_failed(( + f"Source version with id \"{src_version_id}\"" + f" was not found in project \"{src_project_name}\"" + )) + raise PushToProjectError(self._status.fail_reason) + + subset_id = version_doc["parent"] + subset_doc = get_subset_by_id(src_project_name, subset_id) + if not subset_doc: + self._status.set_failed(( + f"Could find subset with id \"{subset_id}\"" + f" in project \"{src_project_name}\"" + )) + raise PushToProjectError(self._status.fail_reason) + + asset_id = subset_doc["parent"] + asset_doc = get_asset_by_id(src_project_name, asset_id) + if not asset_doc: + self._status.set_failed(( + f"Could find asset with id \"{asset_id}\"" + f" in project \"{src_project_name}\"" + )) + raise PushToProjectError(self._status.fail_reason) + + anatomy = Anatomy(src_project_name) + + repre_docs = get_representations( + src_project_name, + version_ids=[src_version_id] + ) + repre_items = [ + ProjectPushRepreItem(repre_doc, anatomy.roots) + for repre_doc in repre_docs + ] + self._status.debug(( + f"Found {len(repre_items)} representations on" + f" version {src_version_id} in project '{src_project_name}'" + )) + if not repre_items: + self._status.set_failed( + "Source version does not have representations" + f" (Version id: {src_version_id})" + ) + raise PushToProjectError(self._status.fail_reason) + + self._src_anatomy = anatomy + self._src_project_doc = project_doc + self._src_asset_doc = asset_doc + self._src_subset_doc = subset_doc + self._src_version_doc = version_doc + self._src_repre_items = repre_items + + def fill_destination_project(self): + # --- Destination entities --- + dst_project_name = self._item.dst_project_name + # Validate project existence + dst_project_doc = get_project(dst_project_name) + if not dst_project_doc: + self._status.set_failed( + f"Destination project '{dst_project_name}' was not found" + ) + raise PushToProjectError(self._status.fail_reason) + + self._status.debug( + f"Destination project '{dst_project_name}' found" + ) + self._project_doc = dst_project_doc + self._anatomy = Anatomy(dst_project_name) + self._project_settings = get_project_settings( + self._item.dst_project_name + ) + + def _create_asset( + self, + src_asset_doc, + project_doc, + parent_asset_doc, + asset_name + ): + parent_id = None + parents = [] + tools = [] + if parent_asset_doc: + parent_id = parent_asset_doc["_id"] + parents = list(parent_asset_doc["data"]["parents"]) + parents.append(parent_asset_doc["name"]) + _tools = parent_asset_doc["data"].get("tools_env") + if _tools: + tools = list(_tools) + + asset_name_low = asset_name.lower() + other_asset_docs = get_assets( + project_doc["name"], fields=["_id", "name", "data.visualParent"] + ) + for other_asset_doc in other_asset_docs: + other_name = other_asset_doc["name"] + other_parent_id = other_asset_doc["data"].get("visualParent") + if other_name.lower() != asset_name_low: + continue + + if other_parent_id != parent_id: + self._status.set_failed(( + f"Asset with name \"{other_name}\" already" + " exists in different hierarchy." + )) + raise PushToProjectError(self._status.fail_reason) + + self._status.debug(( + f"Found already existing asset with name \"{other_name}\"" + f" which match requested name \"{asset_name}\"" + )) + return get_asset_by_id(project_doc["name"], other_asset_doc["_id"]) + + data_keys = ( + "clipIn", + "clipOut", + "frameStart", + "frameEnd", + "handleStart", + "handleEnd", + "resolutionWidth", + "resolutionHeight", + "fps", + "pixelAspect", + ) + asset_data = { + "visualParent": parent_id, + "parents": parents, + "tasks": {}, + "tools_env": tools + } + src_asset_data = src_asset_doc["data"] + for key in data_keys: + if key in src_asset_data: + asset_data[key] = src_asset_data[key] + + asset_doc = new_asset_document( + asset_name, + project_doc["_id"], + parent_id, + parents, + data=asset_data + ) + self._operations.create_entity( + project_doc["name"], + asset_doc["type"], + asset_doc + ) + self._status.info( + f"Creating new asset with name \"{asset_name}\"" + ) + self._created_asset_doc = asset_doc + return asset_doc + + def fill_or_create_destination_asset(self): + dst_project_name = self._item.dst_project_name + dst_asset_id = self._item.dst_asset_id + dst_task_name = self._item.dst_task_name + new_asset_name = self._item.new_asset_name + if not dst_asset_id and not new_asset_name: + self._status.set_failed( + "Push item does not have defined destination asset" + ) + raise PushToProjectError(self._status.fail_reason) + + # Get asset document + parent_asset_doc = None + if dst_asset_id: + parent_asset_doc = get_asset_by_id( + self._item.dst_project_name, self._item.dst_asset_id + ) + if not parent_asset_doc: + self._status.set_failed( + f"Could find asset with id \"{dst_asset_id}\"" + f" in project \"{dst_project_name}\"" + ) + raise PushToProjectError(self._status.fail_reason) + + if not new_asset_name: + asset_doc = parent_asset_doc + else: + asset_doc = self._create_asset( + self.src_asset_doc, + self.project_doc, + parent_asset_doc, + new_asset_name + ) + self._asset_doc = asset_doc + if not dst_task_name: + self._task_info = {} + return + + asset_path_parts = list(asset_doc["data"]["parents"]) + asset_path_parts.append(asset_doc["name"]) + asset_path = "/".join(asset_path_parts) + asset_tasks = asset_doc.get("data", {}).get("tasks") or {} + task_info = asset_tasks.get(dst_task_name) + if not task_info: + self._status.set_failed( + f"Could find task with name \"{dst_task_name}\"" + f" on asset \"{asset_path}\"" + f" in project \"{dst_project_name}\"" + ) + raise PushToProjectError(self._status.fail_reason) + + # Create copy of task info to avoid changing data in asset document + task_info = copy.deepcopy(task_info) + task_info["name"] = dst_task_name + # Fill rest of task information based on task type + task_type = task_info["type"] + task_type_info = self.project_doc["config"]["tasks"].get(task_type, {}) + task_info.update(task_type_info) + self._task_info = task_info + + def determine_family(self): + subset_doc = self.src_subset_doc + family = subset_doc["data"].get("family") + families = subset_doc["data"].get("families") + if not family and families: + family = families[0] + + if not family: + self._status.set_failed( + "Couldn't figure out family from source subset" + ) + raise PushToProjectError(self._status.fail_reason) + + self._status.debug( + f"Publishing family is '{family}' (Based on source subset)" + ) + self._family = family + + def determine_publish_template_name(self): + template_name = get_publish_template_name( + self._item.dst_project_name, + self.host_name, + self.family, + self.task_info.get("name"), + self.task_info.get("type"), + project_settings=self.project_settings + ) + self._status.debug( + f"Using template '{template_name}' for integration" + ) + self._template_name = template_name + + def determine_subset_name(self): + family = self.family + asset_doc = self.asset_doc + task_info = self.task_info + subset_name = get_subset_name( + family, + self.variant, + task_info.get("name"), + asset_doc, + project_name=self._item.dst_project_name, + host_name=self.host_name, + project_settings=self.project_settings + ) + self._status.info( + f"Push will be integrating to subset with name '{subset_name}'" + ) + self._subset_name = subset_name + + def make_sure_subset_exists(self): + project_name = self._item.dst_project_name + asset_id = self.asset_doc["_id"] + subset_name = self.subset_name + family = self.family + subset_doc = get_subset_by_name(project_name, subset_name, asset_id) + if subset_doc: + self._subset_doc = subset_doc + return subset_doc + + data = { + "families": [family] + } + subset_doc = new_subset_document( + subset_name, family, asset_id, data + ) + self._operations.create_entity(project_name, "subset", subset_doc) + self._subset_doc = subset_doc + + def make_sure_version_exists(self): + """Make sure version document exits in database.""" + + project_name = self._item.dst_project_name + version = self._item.dst_version + src_version_doc = self.src_version_doc + subset_doc = self.subset_doc + subset_id = subset_doc["_id"] + src_data = src_version_doc["data"] + families = subset_doc["data"].get("families") + if not families: + families = [subset_doc["data"]["family"]] + + version_data = { + "families": list(families), + "fps": src_data.get("fps"), + "source": src_data.get("source"), + "machine": socket.gethostname(), + "comment": self._item.comment or "", + "author": get_openpype_username(), + "time": get_formatted_current_time(), + } + if version is None: + last_version_doc = get_last_version_by_subset_id( + project_name, subset_id + ) + if last_version_doc: + 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 + ) + # Update existing version + if existing_version_doc: + version_doc = new_version_doc( + version, subset_id, version_data, existing_version_doc["_id"] + ) + update_data = prepare_version_update_data( + existing_version_doc, version_doc + ) + if update_data: + self._operations.update_entity( + project_name, + "version", + existing_version_doc["_id"], + update_data + ) + self._version_doc = version_doc + + return + + version_doc = new_version_doc( + version, subset_id, version_data + ) + self._operations.create_entity(project_name, "version", version_doc) + + self._version_doc = version_doc + + def integrate_representations(self): + try: + self._integrate_representations() + except Exception: + self._operations.clear() + self._file_transaction.rollback() + raise + + def _integrate_representations(self): + version_doc = self.version_doc + version_id = version_doc["_id"] + existing_repres = get_representations( + self._item.dst_project_name, + version_ids=[version_id] + ) + existing_repres_by_low_name = { + repre_doc["name"].lower(): repre_doc + for repre_doc in existing_repres + } + template_name = self.template_name + anatomy = self.anatomy + formatting_data = get_template_data( + self.project_doc, + self.asset_doc, + self.task_info.get("name"), + self.host_name + ) + formatting_data.update({ + "subset": self.subset_name, + "family": self.family, + "version": version_doc["name"] + }) + + path_template = anatomy.templates[template_name]["path"].replace( + "\\", "/" + ) + file_template = StringTemplate( + anatomy.templates[template_name]["file"] + ) + self._status.info("Preparing files to transfer") + processed_repre_items = self._prepare_file_transactions( + anatomy, template_name, formatting_data, file_template + ) + self._file_transaction.process() + self._status.info("Preparing database changes") + self._prepare_database_operations( + version_id, + processed_repre_items, + path_template, + existing_repres_by_low_name + ) + self._status.info("Finalization") + self._operations.commit() + self._file_transaction.finalize() + + def _prepare_file_transactions( + self, anatomy, template_name, formatting_data, file_template + ): + processed_repre_items = [] + for repre_item in self.src_repre_items: + repre_doc = repre_item.repre_doc + repre_name = repre_doc["name"] + repre_format_data = copy.deepcopy(formatting_data) + repre_format_data["representation"] = repre_name + for src_file in repre_item.src_files: + ext = os.path.splitext(src_file.path)[-1] + repre_format_data["ext"] = ext[1:] + break + + template_obj = anatomy.templates_obj[template_name]["folder"] + folder_path = template_obj.format_strict(formatting_data) + repre_context = folder_path.used_values + folder_path_rootless = folder_path.rootless + repre_filepaths = [] + published_path = None + for src_file in repre_item.src_files: + file_data = copy.deepcopy(repre_format_data) + frame = src_file.frame + if frame is not None: + file_data["frame"] = frame + + udim = src_file.udim + if udim is not None: + file_data["udim"] = udim + + filename = file_template.format_strict(file_data) + dst_filepath = os.path.normpath( + os.path.join(folder_path, filename) + ) + dst_rootless_path = os.path.normpath( + os.path.join(folder_path_rootless, filename) + ) + if published_path is None or frame == repre_item.frame: + published_path = dst_filepath + repre_context.update(filename.used_values) + + repre_filepaths.append((dst_filepath, dst_rootless_path)) + self._file_transaction.add(src_file.path, dst_filepath) + + for resource_file in repre_item.resource_files: + dst_filepath = os.path.normpath( + os.path.join(folder_path, resource_file.relative_path) + ) + dst_rootless_path = os.path.normpath( + os.path.join( + folder_path_rootless, resource_file.relative_path + ) + ) + repre_filepaths.append((dst_filepath, dst_rootless_path)) + self._file_transaction.add(resource_file.path, dst_filepath) + processed_repre_items.append( + (repre_item, repre_filepaths, repre_context, published_path) + ) + return processed_repre_items + + def _prepare_database_operations( + self, + version_id, + processed_repre_items, + path_template, + existing_repres_by_low_name + ): + modules_manager = ModulesManager() + sync_server_module = modules_manager.get("sync_server") + if sync_server_module is None or not sync_server_module.enabled: + sites = [{ + "name": "studio", + "created_dt": datetime.datetime.now() + }] + else: + sites = sync_server_module.compute_resource_sync_sites( + project_name=self._item.dst_project_name + ) + + added_repre_names = set() + for item in processed_repre_items: + (repre_item, repre_filepaths, repre_context, published_path) = item + repre_name = repre_item.repre_doc["name"] + added_repre_names.add(repre_name.lower()) + new_repre_data = { + "path": published_path, + "template": path_template + } + new_repre_files = [] + for (path, rootless_path) in repre_filepaths: + new_repre_files.append({ + "_id": ObjectId(), + "path": rootless_path, + "size": os.path.getsize(path), + "hash": source_hash(path), + "sites": sites + }) + + existing_repre = existing_repres_by_low_name.get( + repre_name.lower() + ) + entity_id = None + if existing_repre: + entity_id = existing_repre["_id"] + new_repre_doc = new_representation_doc( + repre_name, + version_id, + repre_context, + data=new_repre_data, + entity_id=entity_id + ) + new_repre_doc["files"] = new_repre_files + if not existing_repre: + self._operations.create_entity( + self._item.dst_project_name, + new_repre_doc["type"], + new_repre_doc + ) + else: + update_data = prepare_representation_update_data( + existing_repre, new_repre_doc + ) + if update_data: + self._operations.update_entity( + self._item.dst_project_name, + new_repre_doc["type"], + new_repre_doc["_id"], + update_data + ) + + existing_repre_names = set(existing_repres_by_low_name.keys()) + for repre_name in (existing_repre_names - added_repre_names): + repre_doc = existing_repres_by_low_name[repre_name] + self._operations.update_entity( + self._item.dst_project_name, + repre_doc["type"], + repre_doc["_id"], + {"type": "archived_representation"} + ) + + def process(self): + try: + self._status.info("Process started") + self.fill_source_variables() + self._status.info("Source entities were found") + self.fill_destination_project() + self._status.info("Destination project was found") + self.fill_or_create_destination_asset() + self._status.info("Destination asset was determined") + self.determine_family() + self.determine_publish_template_name() + self.determine_subset_name() + self.make_sure_subset_exists() + self.make_sure_version_exists() + self._status.info("Prerequirements were prepared") + self.integrate_representations() + self._status.info("Integration finished") + + except PushToProjectError as exc: + if not self._status.failed: + self._status.set_failed(str(exc)) + + except Exception as exc: + _exc, _value, _tb = sys.exc_info() + self._status.set_failed( + "Unhandled error happened: {}".format(str(exc)), + (_exc, _value, _tb) + ) + + finally: + self._status.set_finished() diff --git a/openpype/tools/ayon_push_to_project/window.py b/openpype/tools/ayon_push_to_project/window.py new file mode 100644 index 0000000000..dc5eab5787 --- /dev/null +++ b/openpype/tools/ayon_push_to_project/window.py @@ -0,0 +1,829 @@ +import collections + +from qtpy import QtWidgets, QtGui, QtCore + +from openpype.style import load_stylesheet, get_app_icon_path +from openpype.tools.utils import ( + PlaceholderLineEdit, + SeparatorWidget, + get_asset_icon_by_name, + set_style_property, +) +from openpype.tools.utils.views import DeselectableTreeView + +from .control_context import PushToContextController + +PROJECT_NAME_ROLE = QtCore.Qt.UserRole + 1 +ASSET_NAME_ROLE = QtCore.Qt.UserRole + 2 +ASSET_ID_ROLE = QtCore.Qt.UserRole + 3 +TASK_NAME_ROLE = QtCore.Qt.UserRole + 4 +TASK_TYPE_ROLE = QtCore.Qt.UserRole + 5 + + +class ProjectsModel(QtGui.QStandardItemModel): + empty_text = "< Empty >" + select_project_text = "< Select Project >" + + refreshed = QtCore.Signal() + + def __init__(self, controller): + super(ProjectsModel, self).__init__() + self._controller = controller + + self.event_system.add_callback( + "projects.refresh.finished", self._on_refresh_finish + ) + + placeholder_item = QtGui.QStandardItem(self.empty_text) + + root_item = self.invisibleRootItem() + root_item.appendRows([placeholder_item]) + items = {None: placeholder_item} + + self._placeholder_item = placeholder_item + self._items = items + + @property + def event_system(self): + return self._controller.event_system + + def _on_refresh_finish(self): + root_item = self.invisibleRootItem() + project_names = self._controller.model.get_projects() + + if not project_names: + placeholder_text = self.empty_text + else: + placeholder_text = self.select_project_text + self._placeholder_item.setData(placeholder_text, QtCore.Qt.DisplayRole) + + new_items = [] + if None not in self._items: + new_items.append(self._placeholder_item) + + current_project_names = set(self._items.keys()) + for project_name in current_project_names - set(project_names): + if project_name is None: + continue + item = self._items.pop(project_name) + root_item.takeRow(item.row()) + + for project_name in project_names: + if project_name in self._items: + continue + item = QtGui.QStandardItem(project_name) + item.setData(project_name, PROJECT_NAME_ROLE) + new_items.append(item) + + if new_items: + root_item.appendRows(new_items) + self.refreshed.emit() + + +class ProjectProxyModel(QtCore.QSortFilterProxyModel): + def __init__(self): + super(ProjectProxyModel, self).__init__() + self._filter_empty_projects = False + + def set_filter_empty_project(self, filter_empty_projects): + if filter_empty_projects == self._filter_empty_projects: + return + self._filter_empty_projects = filter_empty_projects + self.invalidate() + + def filterAcceptsRow(self, row, parent): + if not self._filter_empty_projects: + return True + model = self.sourceModel() + source_index = model.index(row, self.filterKeyColumn(), parent) + if model.data(source_index, PROJECT_NAME_ROLE) is None: + return False + return True + + +class AssetsModel(QtGui.QStandardItemModel): + items_changed = QtCore.Signal() + empty_text = "< Empty >" + + def __init__(self, controller): + super(AssetsModel, self).__init__() + self._controller = controller + + placeholder_item = QtGui.QStandardItem(self.empty_text) + placeholder_item.setFlags(QtCore.Qt.ItemIsEnabled) + + root_item = self.invisibleRootItem() + root_item.appendRows([placeholder_item]) + + self.event_system.add_callback( + "project.changed", self._on_project_change + ) + self.event_system.add_callback( + "assets.refresh.started", self._on_refresh_start + ) + self.event_system.add_callback( + "assets.refresh.finished", self._on_refresh_finish + ) + + self._items = {None: placeholder_item} + + self._placeholder_item = placeholder_item + self._last_project = None + + @property + def event_system(self): + return self._controller.event_system + + def _clear(self): + placeholder_in = False + root_item = self.invisibleRootItem() + for row in reversed(range(root_item.rowCount())): + item = root_item.child(row) + asset_id = item.data(ASSET_ID_ROLE) + if asset_id is None: + placeholder_in = True + continue + root_item.removeRow(item.row()) + + for key in tuple(self._items.keys()): + if key is not None: + self._items.pop(key) + + if not placeholder_in: + root_item.appendRows([self._placeholder_item]) + self._items[None] = self._placeholder_item + + def _on_project_change(self, event): + project_name = event["project_name"] + if project_name == self._last_project: + return + + self._last_project = project_name + self._clear() + self.items_changed.emit() + + def _on_refresh_start(self, event): + pass + + def _on_refresh_finish(self, event): + event_project_name = event["project_name"] + project_name = self._controller.selection_model.project_name + if event_project_name != project_name: + return + + self._last_project = event["project_name"] + if project_name is None: + if None not in self._items: + self._clear() + self.items_changed.emit() + return + + asset_items_by_id = self._controller.model.get_assets(project_name) + if not asset_items_by_id: + self._clear() + self.items_changed.emit() + return + + assets_by_parent_id = collections.defaultdict(list) + for asset_item in asset_items_by_id.values(): + assets_by_parent_id[asset_item.parent_id].append(asset_item) + + root_item = self.invisibleRootItem() + if None in self._items: + self._items.pop(None) + root_item.takeRow(self._placeholder_item.row()) + + items_to_remove = set(self._items) - set(asset_items_by_id.keys()) + hierarchy_queue = collections.deque() + hierarchy_queue.append((None, root_item)) + while hierarchy_queue: + parent_id, parent_item = hierarchy_queue.popleft() + new_items = [] + for asset_item in assets_by_parent_id[parent_id]: + item = self._items.get(asset_item.id) + if item is None: + item = QtGui.QStandardItem() + item.setFlags( + QtCore.Qt.ItemIsSelectable + | QtCore.Qt.ItemIsEnabled + ) + new_items.append(item) + self._items[asset_item.id] = item + + elif item.parent() is not parent_item: + new_items.append(item) + + icon = get_asset_icon_by_name( + asset_item.icon_name, asset_item.icon_color + ) + item.setData(asset_item.name, QtCore.Qt.DisplayRole) + item.setData(icon, QtCore.Qt.DecorationRole) + item.setData(asset_item.id, ASSET_ID_ROLE) + + hierarchy_queue.append((asset_item.id, item)) + + if new_items: + parent_item.appendRows(new_items) + + for item_id in items_to_remove: + item = self._items.pop(item_id, None) + if item is None: + continue + row = item.row() + if row < 0: + continue + parent = item.parent() + if parent is None: + parent = root_item + parent.takeRow(row) + + self.items_changed.emit() + + +class TasksModel(QtGui.QStandardItemModel): + items_changed = QtCore.Signal() + empty_text = "< Empty >" + + def __init__(self, controller): + super(TasksModel, self).__init__() + self._controller = controller + + placeholder_item = QtGui.QStandardItem(self.empty_text) + placeholder_item.setFlags(QtCore.Qt.ItemIsEnabled) + + root_item = self.invisibleRootItem() + root_item.appendRows([placeholder_item]) + + self.event_system.add_callback( + "project.changed", self._on_project_change + ) + self.event_system.add_callback( + "assets.refresh.finished", self._on_asset_refresh_finish + ) + self.event_system.add_callback( + "asset.changed", self._on_asset_change + ) + + self._items = {None: placeholder_item} + + self._placeholder_item = placeholder_item + self._last_project = None + + @property + def event_system(self): + return self._controller.event_system + + def _clear(self): + placeholder_in = False + root_item = self.invisibleRootItem() + for row in reversed(range(root_item.rowCount())): + item = root_item.child(row) + task_name = item.data(TASK_NAME_ROLE) + if task_name is None: + placeholder_in = True + continue + root_item.removeRow(item.row()) + + for key in tuple(self._items.keys()): + if key is not None: + self._items.pop(key) + + if not placeholder_in: + root_item.appendRows([self._placeholder_item]) + self._items[None] = self._placeholder_item + + def _on_project_change(self, event): + project_name = event["project_name"] + if project_name == self._last_project: + return + + self._last_project = project_name + self._clear() + self.items_changed.emit() + + def _on_asset_refresh_finish(self, event): + self._refresh(event["project_name"]) + + def _on_asset_change(self, event): + self._refresh(event["project_name"]) + + def _refresh(self, new_project_name): + project_name = self._controller.selection_model.project_name + if new_project_name != project_name: + return + + self._last_project = project_name + if project_name is None: + if None not in self._items: + self._clear() + self.items_changed.emit() + return + + asset_id = self._controller.selection_model.asset_id + task_items = self._controller.model.get_tasks( + project_name, asset_id + ) + if not task_items: + self._clear() + self.items_changed.emit() + return + + root_item = self.invisibleRootItem() + if None in self._items: + self._items.pop(None) + root_item.takeRow(self._placeholder_item.row()) + + new_items = [] + task_names = set() + for task_item in task_items: + task_name = task_item.name + item = self._items.get(task_name) + if item is None: + item = QtGui.QStandardItem() + item.setFlags( + QtCore.Qt.ItemIsSelectable + | QtCore.Qt.ItemIsEnabled + ) + new_items.append(item) + self._items[task_name] = item + + item.setData(task_name, QtCore.Qt.DisplayRole) + item.setData(task_name, TASK_NAME_ROLE) + item.setData(task_item.task_type, TASK_TYPE_ROLE) + + if new_items: + root_item.appendRows(new_items) + + items_to_remove = set(self._items) - task_names + for item_id in items_to_remove: + item = self._items.pop(item_id, None) + if item is None: + continue + parent = item.parent() + if parent is not None: + parent.removeRow(item.row()) + + self.items_changed.emit() + + +class PushToContextSelectWindow(QtWidgets.QWidget): + def __init__(self, controller=None): + super(PushToContextSelectWindow, self).__init__() + if controller is None: + controller = PushToContextController() + self._controller = controller + + self.setWindowTitle("Push to project (select context)") + self.setWindowIcon(QtGui.QIcon(get_app_icon_path())) + + main_context_widget = QtWidgets.QWidget(self) + + header_widget = QtWidgets.QWidget(main_context_widget) + + header_label = QtWidgets.QLabel(controller.src_label, header_widget) + + header_layout = QtWidgets.QHBoxLayout(header_widget) + header_layout.setContentsMargins(0, 0, 0, 0) + header_layout.addWidget(header_label) + + main_splitter = QtWidgets.QSplitter( + QtCore.Qt.Horizontal, main_context_widget + ) + + context_widget = QtWidgets.QWidget(main_splitter) + + project_combobox = QtWidgets.QComboBox(context_widget) + project_model = ProjectsModel(controller) + project_proxy = ProjectProxyModel() + project_proxy.setSourceModel(project_model) + project_proxy.setDynamicSortFilter(True) + project_delegate = QtWidgets.QStyledItemDelegate() + project_combobox.setItemDelegate(project_delegate) + project_combobox.setModel(project_proxy) + + asset_task_splitter = QtWidgets.QSplitter( + QtCore.Qt.Vertical, context_widget + ) + + asset_view = DeselectableTreeView(asset_task_splitter) + asset_view.setHeaderHidden(True) + asset_model = AssetsModel(controller) + asset_proxy = QtCore.QSortFilterProxyModel() + asset_proxy.setSourceModel(asset_model) + asset_proxy.setDynamicSortFilter(True) + asset_view.setModel(asset_proxy) + + task_view = QtWidgets.QListView(asset_task_splitter) + task_proxy = QtCore.QSortFilterProxyModel() + task_model = TasksModel(controller) + task_proxy.setSourceModel(task_model) + task_proxy.setDynamicSortFilter(True) + task_view.setModel(task_proxy) + + asset_task_splitter.addWidget(asset_view) + asset_task_splitter.addWidget(task_view) + + context_layout = QtWidgets.QVBoxLayout(context_widget) + context_layout.setContentsMargins(0, 0, 0, 0) + context_layout.addWidget(project_combobox, 0) + context_layout.addWidget(asset_task_splitter, 1) + + # --- Inputs widget --- + inputs_widget = QtWidgets.QWidget(main_splitter) + + asset_name_input = PlaceholderLineEdit(inputs_widget) + asset_name_input.setPlaceholderText("< Name of new asset >") + asset_name_input.setObjectName("ValidatedLineEdit") + + variant_input = PlaceholderLineEdit(inputs_widget) + variant_input.setPlaceholderText("< Variant >") + variant_input.setObjectName("ValidatedLineEdit") + + comment_input = PlaceholderLineEdit(inputs_widget) + comment_input.setPlaceholderText("< Publish comment >") + + inputs_layout = QtWidgets.QFormLayout(inputs_widget) + inputs_layout.setContentsMargins(0, 0, 0, 0) + inputs_layout.addRow("New asset name", asset_name_input) + inputs_layout.addRow("Variant", variant_input) + inputs_layout.addRow("Comment", comment_input) + + main_splitter.addWidget(context_widget) + main_splitter.addWidget(inputs_widget) + + # --- Buttons widget --- + btns_widget = QtWidgets.QWidget(self) + cancel_btn = QtWidgets.QPushButton("Cancel", btns_widget) + publish_btn = QtWidgets.QPushButton("Publish", btns_widget) + + btns_layout = QtWidgets.QHBoxLayout(btns_widget) + btns_layout.setContentsMargins(0, 0, 0, 0) + btns_layout.addStretch(1) + btns_layout.addWidget(cancel_btn, 0) + btns_layout.addWidget(publish_btn, 0) + + sep_1 = SeparatorWidget(parent=main_context_widget) + sep_2 = SeparatorWidget(parent=main_context_widget) + main_context_layout = QtWidgets.QVBoxLayout(main_context_widget) + main_context_layout.addWidget(header_widget, 0) + main_context_layout.addWidget(sep_1, 0) + main_context_layout.addWidget(main_splitter, 1) + main_context_layout.addWidget(sep_2, 0) + main_context_layout.addWidget(btns_widget, 0) + + # NOTE This was added in hurry + # - should be reorganized and changed styles + overlay_widget = QtWidgets.QFrame(self) + overlay_widget.setObjectName("OverlayFrame") + + overlay_label = QtWidgets.QLabel(overlay_widget) + overlay_label.setAlignment(QtCore.Qt.AlignCenter) + + overlay_btns_widget = QtWidgets.QWidget(overlay_widget) + overlay_btns_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + # Add try again button (requires changes in controller) + overlay_try_btn = QtWidgets.QPushButton( + "Try again", overlay_btns_widget + ) + overlay_close_btn = QtWidgets.QPushButton( + "Close", overlay_btns_widget + ) + + overlay_btns_layout = QtWidgets.QHBoxLayout(overlay_btns_widget) + overlay_btns_layout.addStretch(1) + overlay_btns_layout.addWidget(overlay_try_btn, 0) + overlay_btns_layout.addWidget(overlay_close_btn, 0) + overlay_btns_layout.addStretch(1) + + overlay_layout = QtWidgets.QVBoxLayout(overlay_widget) + overlay_layout.addWidget(overlay_label, 0) + overlay_layout.addWidget(overlay_btns_widget, 0) + overlay_layout.setAlignment(QtCore.Qt.AlignCenter) + + main_layout = QtWidgets.QStackedLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.addWidget(main_context_widget) + main_layout.addWidget(overlay_widget) + main_layout.setStackingMode(QtWidgets.QStackedLayout.StackAll) + main_layout.setCurrentWidget(main_context_widget) + + show_timer = QtCore.QTimer() + show_timer.setInterval(1) + + main_thread_timer = QtCore.QTimer() + main_thread_timer.setInterval(10) + + user_input_changed_timer = QtCore.QTimer() + user_input_changed_timer.setInterval(200) + user_input_changed_timer.setSingleShot(True) + + main_thread_timer.timeout.connect(self._on_main_thread_timer) + show_timer.timeout.connect(self._on_show_timer) + user_input_changed_timer.timeout.connect(self._on_user_input_timer) + asset_name_input.textChanged.connect(self._on_new_asset_change) + variant_input.textChanged.connect(self._on_variant_change) + comment_input.textChanged.connect(self._on_comment_change) + project_model.refreshed.connect(self._on_projects_refresh) + project_combobox.currentIndexChanged.connect(self._on_project_change) + asset_view.selectionModel().selectionChanged.connect( + self._on_asset_change + ) + asset_model.items_changed.connect(self._on_asset_model_change) + task_view.selectionModel().selectionChanged.connect( + self._on_task_change + ) + task_model.items_changed.connect(self._on_task_model_change) + publish_btn.clicked.connect(self._on_select_click) + cancel_btn.clicked.connect(self._on_close_click) + overlay_close_btn.clicked.connect(self._on_close_click) + overlay_try_btn.clicked.connect(self._on_try_again_click) + + controller.event_system.add_callback( + "new_asset_name.changed", self._on_controller_new_asset_change + ) + controller.event_system.add_callback( + "variant.changed", self._on_controller_variant_change + ) + controller.event_system.add_callback( + "comment.changed", self._on_controller_comment_change + ) + controller.event_system.add_callback( + "submission.enabled.changed", self._on_submission_change + ) + controller.event_system.add_callback( + "source.changed", self._on_controller_source_change + ) + controller.event_system.add_callback( + "submit.started", self._on_controller_submit_start + ) + controller.event_system.add_callback( + "submit.finished", self._on_controller_submit_end + ) + controller.event_system.add_callback( + "push.message.added", self._on_push_message + ) + + self._main_layout = main_layout + + self._main_context_widget = main_context_widget + + self._header_label = header_label + self._main_splitter = main_splitter + + self._project_combobox = project_combobox + self._project_model = project_model + self._project_proxy = project_proxy + self._project_delegate = project_delegate + + self._asset_view = asset_view + self._asset_model = asset_model + self._asset_proxy_model = asset_proxy + + self._task_view = task_view + self._task_proxy_model = task_proxy + + self._variant_input = variant_input + self._asset_name_input = asset_name_input + self._comment_input = comment_input + + self._publish_btn = publish_btn + + self._overlay_widget = overlay_widget + self._overlay_close_btn = overlay_close_btn + self._overlay_try_btn = overlay_try_btn + self._overlay_label = overlay_label + + self._user_input_changed_timer = user_input_changed_timer + # Store current value on input text change + # The value is unset when is passed to controller + # The goal is to have controll over changes happened during user change + # in UI and controller auto-changes + self._variant_input_text = None + self._new_asset_name_input_text = None + self._comment_input_text = None + self._show_timer = show_timer + self._show_counter = 2 + self._first_show = True + + self._main_thread_timer = main_thread_timer + self._main_thread_timer_can_stop = True + self._last_submit_message = None + self._process_item = None + + publish_btn.setEnabled(False) + overlay_close_btn.setVisible(False) + overlay_try_btn.setVisible(False) + + if controller.user_values.new_asset_name: + asset_name_input.setText(controller.user_values.new_asset_name) + if controller.user_values.variant: + variant_input.setText(controller.user_values.variant) + self._invalidate_variant() + self._invalidate_new_asset_name() + + @property + def controller(self): + return self._controller + + def showEvent(self, event): + super(PushToContextSelectWindow, self).showEvent(event) + if self._first_show: + self._first_show = False + self.setStyleSheet(load_stylesheet()) + self._invalidate_variant() + self._show_timer.start() + + def _on_show_timer(self): + if self._show_counter == 0: + self._show_timer.stop() + return + + self._show_counter -= 1 + if self._show_counter == 1: + width = 740 + height = 640 + inputs_width = 360 + self.resize(width, height) + self._main_splitter.setSizes([width - inputs_width, inputs_width]) + + if self._show_counter > 0: + return + + self._controller.model.refresh_projects() + + def _on_new_asset_change(self, text): + self._new_asset_name_input_text = text + self._user_input_changed_timer.start() + + def _on_variant_change(self, text): + self._variant_input_text = text + self._user_input_changed_timer.start() + + def _on_comment_change(self, text): + self._comment_input_text = text + self._user_input_changed_timer.start() + + def _on_user_input_timer(self): + asset_name = self._new_asset_name_input_text + if asset_name is not None: + self._new_asset_name_input_text = None + self._controller.user_values.set_new_asset(asset_name) + + variant = self._variant_input_text + if variant is not None: + self._variant_input_text = None + self._controller.user_values.set_variant(variant) + + comment = self._comment_input_text + if comment is not None: + self._comment_input_text = None + self._controller.user_values.set_comment(comment) + + def _on_controller_new_asset_change(self, event): + asset_name = event["changes"]["new_asset_name"]["new"] + if ( + self._new_asset_name_input_text is None + and asset_name != self._asset_name_input.text() + ): + self._asset_name_input.setText(asset_name) + + self._invalidate_new_asset_name() + + def _on_controller_variant_change(self, event): + is_valid_changes = event["changes"]["is_valid"] + variant = event["changes"]["variant"]["new"] + if ( + self._variant_input_text is None + and variant != self._variant_input.text() + ): + self._variant_input.setText(variant) + + if is_valid_changes["old"] != is_valid_changes["new"]: + self._invalidate_variant() + + def _on_controller_comment_change(self, event): + comment = event["comment"] + if ( + self._comment_input_text is None + and comment != self._comment_input.text() + ): + self._comment_input.setText(comment) + + def _on_controller_source_change(self): + self._header_label.setText(self._controller.src_label) + + def _invalidate_new_asset_name(self): + asset_name = self._controller.user_values.new_asset_name + self._task_view.setVisible(not asset_name) + + valid = None + if asset_name: + valid = self._controller.user_values.is_new_asset_name_valid + + state = "" + if valid is True: + state = "valid" + elif valid is False: + state = "invalid" + set_style_property(self._asset_name_input, "state", state) + + def _invalidate_variant(self): + valid = self._controller.user_values.is_variant_valid + state = "invalid" + if valid is True: + state = "valid" + set_style_property(self._variant_input, "state", state) + + def _on_projects_refresh(self): + self._project_proxy.sort(0, QtCore.Qt.AscendingOrder) + + def _on_project_change(self): + idx = self._project_combobox.currentIndex() + if idx < 0: + self._project_proxy.set_filter_empty_project(False) + return + + project_name = self._project_combobox.itemData(idx, PROJECT_NAME_ROLE) + self._project_proxy.set_filter_empty_project(project_name is not None) + self._controller.selection_model.select_project(project_name) + + def _on_asset_change(self): + indexes = self._asset_view.selectedIndexes() + index = next(iter(indexes), None) + asset_id = None + if index is not None: + model = self._asset_view.model() + asset_id = model.data(index, ASSET_ID_ROLE) + self._controller.selection_model.select_asset(asset_id) + + def _on_asset_model_change(self): + self._asset_proxy_model.sort(0, QtCore.Qt.AscendingOrder) + + def _on_task_model_change(self): + self._task_proxy_model.sort(0, QtCore.Qt.AscendingOrder) + + def _on_task_change(self): + indexes = self._task_view.selectedIndexes() + index = next(iter(indexes), None) + task_name = None + if index is not None: + model = self._task_view.model() + task_name = model.data(index, TASK_NAME_ROLE) + self._controller.selection_model.select_task(task_name) + + def _on_submission_change(self, event): + self._publish_btn.setEnabled(event["enabled"]) + + def _on_close_click(self): + self.close() + + def _on_select_click(self): + self._process_item = self._controller.submit(wait=False) + + def _on_try_again_click(self): + self._process_item = None + self._last_submit_message = None + + self._overlay_close_btn.setVisible(False) + self._overlay_try_btn.setVisible(False) + self._main_layout.setCurrentWidget(self._main_context_widget) + + def _on_main_thread_timer(self): + if self._last_submit_message: + self._overlay_label.setText(self._last_submit_message) + self._last_submit_message = None + + process_status = self._process_item.status + push_failed = process_status.failed + fail_traceback = process_status.traceback + if self._main_thread_timer_can_stop: + self._main_thread_timer.stop() + self._overlay_close_btn.setVisible(True) + if push_failed and not fail_traceback: + self._overlay_try_btn.setVisible(True) + + if push_failed: + message = "Push Failed:\n{}".format(process_status.fail_reason) + if fail_traceback: + message += "\n{}".format(fail_traceback) + self._overlay_label.setText(message) + set_style_property(self._overlay_close_btn, "state", "error") + + if self._main_thread_timer_can_stop: + # Join thread in controller + self._controller.wait_for_process_thread() + # Reset process item to None + self._process_item = None + + def _on_controller_submit_start(self): + self._main_thread_timer_can_stop = False + self._main_thread_timer.start() + self._main_layout.setCurrentWidget(self._overlay_widget) + self._overlay_label.setText("Submittion started") + + def _on_controller_submit_end(self): + self._main_thread_timer_can_stop = True + + def _on_push_message(self, event): + self._last_submit_message = event["message"] From 065ebc389c3d8377903f814b0c76d5cc15b4429a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 13 Oct 2023 15:35:56 +0200 Subject: [PATCH 0441/1233] renamed 'app.py' to 'main.py' and use it in loader action --- openpype/plugins/load/push_to_library.py | 24 +++++++++++++------ .../ayon_push_to_project/{app.py => main.py} | 0 2 files changed, 17 insertions(+), 7 deletions(-) rename openpype/tools/ayon_push_to_project/{app.py => main.py} (100%) diff --git a/openpype/plugins/load/push_to_library.py b/openpype/plugins/load/push_to_library.py index dd7291e686..5befc5eb9d 100644 --- a/openpype/plugins/load/push_to_library.py +++ b/openpype/plugins/load/push_to_library.py @@ -1,6 +1,6 @@ import os -from openpype import PACKAGE_DIR +from openpype import PACKAGE_DIR, AYON_SERVER_ENABLED from openpype.lib import get_openpype_execute_args, run_detached_process from openpype.pipeline import load from openpype.pipeline.load import LoadError @@ -32,12 +32,22 @@ class PushToLibraryProject(load.SubsetLoaderPlugin): raise LoadError("Please select only one item") context = tuple(filtered_contexts)[0] - push_tool_script_path = os.path.join( - PACKAGE_DIR, - "tools", - "push_to_project", - "app.py" - ) + + if AYON_SERVER_ENABLED: + push_tool_script_path = os.path.join( + PACKAGE_DIR, + "tools", + "ayon_push_to_project", + "main.py" + ) + else: + push_tool_script_path = os.path.join( + PACKAGE_DIR, + "tools", + "push_to_project", + "app.py" + ) + project_doc = context["project"] version_doc = context["version"] project_name = project_doc["name"] diff --git a/openpype/tools/ayon_push_to_project/app.py b/openpype/tools/ayon_push_to_project/main.py similarity index 100% rename from openpype/tools/ayon_push_to_project/app.py rename to openpype/tools/ayon_push_to_project/main.py From 178ab5d77a2e9f34ee5766e0d8a7d1bcd0cae8da Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 13 Oct 2023 18:08:16 +0200 Subject: [PATCH 0442/1233] moved 'window.py' to subfolder 'ui' --- openpype/tools/ayon_push_to_project/main.py | 20 +++++++++++-------- .../tools/ayon_push_to_project/ui/__init__.py | 6 ++++++ .../ayon_push_to_project/{ => ui}/window.py | 0 3 files changed, 18 insertions(+), 8 deletions(-) create mode 100644 openpype/tools/ayon_push_to_project/ui/__init__.py rename openpype/tools/ayon_push_to_project/{ => ui}/window.py (100%) diff --git a/openpype/tools/ayon_push_to_project/main.py b/openpype/tools/ayon_push_to_project/main.py index b3ec33f353..e36940e488 100644 --- a/openpype/tools/ayon_push_to_project/main.py +++ b/openpype/tools/ayon_push_to_project/main.py @@ -1,7 +1,17 @@ import click from openpype.tools.utils import get_openpype_qt_app -from openpype.tools.push_to_project.window import PushToContextSelectWindow +from openpype.tools.ayon_push_to_project.ui import PushToContextSelectWindow + + +def main_show(project_name, version_id): + app = get_openpype_qt_app() + + window = PushToContextSelectWindow() + window.show() + window.set_source(project_name, version_id) + + app.exec_() @click.command() @@ -15,13 +25,7 @@ def main(project, version): version (str): Version id. """ - app = get_openpype_qt_app() - - window = PushToContextSelectWindow() - window.show() - window.controller.set_source(project, version) - - app.exec_() + main_show(project, version) if __name__ == "__main__": diff --git a/openpype/tools/ayon_push_to_project/ui/__init__.py b/openpype/tools/ayon_push_to_project/ui/__init__.py new file mode 100644 index 0000000000..1e86475530 --- /dev/null +++ b/openpype/tools/ayon_push_to_project/ui/__init__.py @@ -0,0 +1,6 @@ +from .window import PushToContextSelectWindow + + +__all__ = ( + "PushToContextSelectWindow", +) diff --git a/openpype/tools/ayon_push_to_project/window.py b/openpype/tools/ayon_push_to_project/ui/window.py similarity index 100% rename from openpype/tools/ayon_push_to_project/window.py rename to openpype/tools/ayon_push_to_project/ui/window.py From 4481c7590b1c66fa9f36c08311adfc246f4bed6e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 13 Oct 2023 18:11:06 +0200 Subject: [PATCH 0443/1233] renamed 'control_context.py' to 'control.py' --- openpype/tools/ayon_push_to_project/__init__.py | 6 ++++++ .../ayon_push_to_project/{control_context.py => control.py} | 0 openpype/tools/ayon_push_to_project/ui/window.py | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) rename openpype/tools/ayon_push_to_project/{control_context.py => control.py} (100%) diff --git a/openpype/tools/ayon_push_to_project/__init__.py b/openpype/tools/ayon_push_to_project/__init__.py index e69de29bb2..83df110c96 100644 --- a/openpype/tools/ayon_push_to_project/__init__.py +++ b/openpype/tools/ayon_push_to_project/__init__.py @@ -0,0 +1,6 @@ +from .control import PushToContextController + + +__all__ = ( + "PushToContextController", +) diff --git a/openpype/tools/ayon_push_to_project/control_context.py b/openpype/tools/ayon_push_to_project/control.py similarity index 100% rename from openpype/tools/ayon_push_to_project/control_context.py rename to openpype/tools/ayon_push_to_project/control.py diff --git a/openpype/tools/ayon_push_to_project/ui/window.py b/openpype/tools/ayon_push_to_project/ui/window.py index dc5eab5787..a1fff2d27d 100644 --- a/openpype/tools/ayon_push_to_project/ui/window.py +++ b/openpype/tools/ayon_push_to_project/ui/window.py @@ -11,7 +11,7 @@ from openpype.tools.utils import ( ) from openpype.tools.utils.views import DeselectableTreeView -from .control_context import PushToContextController +from openpype.tools.ayon_push_to_project import PushToContextController PROJECT_NAME_ROLE = QtCore.Qt.UserRole + 1 ASSET_NAME_ROLE = QtCore.Qt.UserRole + 2 From edf5c525415961fa079e3e7d2772cd0fb06a87f7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 13 Oct 2023 18:12:00 +0200 Subject: [PATCH 0444/1233] initial modification of controller --- .../tools/ayon_push_to_project/control.py | 662 ++++++------------ .../ayon_push_to_project/models/__init__.py | 6 + .../ayon_push_to_project/models/selection.py | 72 ++ 3 files changed, 288 insertions(+), 452 deletions(-) create mode 100644 openpype/tools/ayon_push_to_project/models/__init__.py create mode 100644 openpype/tools/ayon_push_to_project/models/selection.py diff --git a/openpype/tools/ayon_push_to_project/control.py b/openpype/tools/ayon_push_to_project/control.py index e4058893d5..4aef09156f 100644 --- a/openpype/tools/ayon_push_to_project/control.py +++ b/openpype/tools/ayon_push_to_project/control.py @@ -1,10 +1,7 @@ import re -import collections import threading from openpype.client import ( - get_projects, - get_assets, get_asset_by_id, get_subset_by_id, get_version_by_id, @@ -12,260 +9,47 @@ from openpype.client import ( ) from openpype.settings import get_project_settings from openpype.lib import prepare_template_data -from openpype.lib.events import EventSystem +from openpype.lib.events import QueuedEventSystem from openpype.pipeline.create import ( SUBSET_NAME_ALLOWED_SYMBOLS, get_subset_name_template, ) +from openpype.tools.ayon_utils.models import ProjectsModel, HierarchyModel from .control_integrate import ( ProjectPushItem, ProjectPushItemProcess, ProjectPushItemStatus, ) - - -class AssetItem: - def __init__( - self, - entity_id, - name, - icon_name, - icon_color, - parent_id, - has_children - ): - self.id = entity_id - self.name = name - self.icon_name = icon_name - self.icon_color = icon_color - self.parent_id = parent_id - self.has_children = has_children - - @classmethod - def from_doc(cls, asset_doc, has_children=True): - parent_id = asset_doc["data"].get("visualParent") - if parent_id is not None: - parent_id = str(parent_id) - return cls( - str(asset_doc["_id"]), - asset_doc["name"], - asset_doc["data"].get("icon"), - asset_doc["data"].get("color"), - parent_id, - has_children - ) - - -class TaskItem: - def __init__(self, asset_id, name, task_type, short_name): - self.asset_id = asset_id - self.name = name - self.task_type = task_type - self.short_name = short_name - - @classmethod - def from_asset_doc(cls, asset_doc, project_doc): - asset_tasks = asset_doc["data"].get("tasks") or {} - project_task_types = project_doc["config"]["tasks"] - output = [] - for task_name, task_info in asset_tasks.items(): - task_type = task_info.get("type") - task_type_info = project_task_types.get(task_type) or {} - output.append(cls( - asset_doc["_id"], - task_name, - task_type, - task_type_info.get("short_name") - )) - return output - - -class EntitiesModel: - def __init__(self, event_system): - self._event_system = event_system - self._project_names = None - self._project_docs_by_name = {} - self._assets_by_project = {} - self._tasks_by_asset_id = collections.defaultdict(dict) - - def has_cached_projects(self): - return self._project_names is None - - def has_cached_assets(self, project_name): - if not project_name: - return True - return project_name in self._assets_by_project - - def has_cached_tasks(self, project_name): - return self.has_cached_assets(project_name) - - def get_projects(self): - if self._project_names is None: - self.refresh_projects() - return list(self._project_names) - - def get_assets(self, project_name): - if project_name not in self._assets_by_project: - self.refresh_assets(project_name) - return dict(self._assets_by_project[project_name]) - - def get_asset_by_id(self, project_name, asset_id): - return self._assets_by_project[project_name].get(asset_id) - - def get_tasks(self, project_name, asset_id): - if not project_name or not asset_id: - return [] - - if project_name not in self._tasks_by_asset_id: - self.refresh_assets(project_name) - - all_task_items = self._tasks_by_asset_id[project_name] - asset_task_items = all_task_items.get(asset_id) - if not asset_task_items: - return [] - return list(asset_task_items) - - def refresh_projects(self, force=False): - self._event_system.emit( - "projects.refresh.started", {}, "entities.model" - ) - if force or self._project_names is None: - project_names = [] - project_docs_by_name = {} - for project_doc in get_projects(): - library_project = project_doc["data"].get("library_project") - if not library_project: - continue - project_name = project_doc["name"] - project_names.append(project_name) - project_docs_by_name[project_name] = project_doc - self._project_names = project_names - self._project_docs_by_name = project_docs_by_name - self._event_system.emit( - "projects.refresh.finished", {}, "entities.model" - ) - - def _refresh_assets(self, project_name): - asset_items_by_id = {} - task_items_by_asset_id = {} - self._assets_by_project[project_name] = asset_items_by_id - self._tasks_by_asset_id[project_name] = task_items_by_asset_id - if not project_name: - return - - project_doc = self._project_docs_by_name[project_name] - asset_docs_by_parent_id = collections.defaultdict(list) - for asset_doc in get_assets(project_name): - parent_id = asset_doc["data"].get("visualParent") - asset_docs_by_parent_id[parent_id].append(asset_doc) - - hierarchy_queue = collections.deque() - for asset_doc in asset_docs_by_parent_id[None]: - hierarchy_queue.append(asset_doc) - - while hierarchy_queue: - asset_doc = hierarchy_queue.popleft() - children = asset_docs_by_parent_id[asset_doc["_id"]] - asset_item = AssetItem.from_doc(asset_doc, len(children) > 0) - asset_items_by_id[asset_item.id] = asset_item - task_items_by_asset_id[asset_item.id] = ( - TaskItem.from_asset_doc(asset_doc, project_doc) - ) - for child in children: - hierarchy_queue.append(child) - - def refresh_assets(self, project_name, force=False): - self._event_system.emit( - "assets.refresh.started", - {"project_name": project_name}, - "entities.model" - ) - - if force or project_name not in self._assets_by_project: - self._refresh_assets(project_name) - - self._event_system.emit( - "assets.refresh.finished", - {"project_name": project_name}, - "entities.model" - ) - - -class SelectionModel: - def __init__(self, event_system): - self._event_system = event_system - - self.project_name = None - self.asset_id = None - self.task_name = None - - def select_project(self, project_name): - if self.project_name == project_name: - return - - self.project_name = project_name - self._event_system.emit( - "project.changed", - {"project_name": project_name}, - "selection.model" - ) - - def select_asset(self, asset_id): - if self.asset_id == asset_id: - return - self.asset_id = asset_id - self._event_system.emit( - "asset.changed", - { - "project_name": self.project_name, - "asset_id": asset_id - }, - "selection.model" - ) - - def select_task(self, task_name): - if self.task_name == task_name: - return - self.task_name = task_name - self._event_system.emit( - "task.changed", - { - "project_name": self.project_name, - "asset_id": self.asset_id, - "task_name": task_name - }, - "selection.model" - ) +from .models import PushToProjectSelectionModel class UserPublishValues: """Helper object to validate values required for push to different project. Args: - event_system (EventSystem): Event system to catch and emit events. - new_asset_name (str): Name of new asset name. - variant (str): Variant for new subset name in new project. + controller (PushToContextController): Event system to catch + and emit events. """ - asset_name_regex = re.compile("^[a-zA-Z0-9_.]+$") + folder_name_regex = re.compile("^[a-zA-Z0-9_.]+$") variant_regex = re.compile("^[{}]+$".format(SUBSET_NAME_ALLOWED_SYMBOLS)) - def __init__(self, event_system): - self._event_system = event_system - self._new_asset_name = None + def __init__(self, controller): + self._controller = controller + self._new_folder_name = None self._variant = None self._comment = None self._is_variant_valid = False - self._is_new_asset_name_valid = False + self._is_new_folder_name_valid = False - self.set_new_asset("") + self.set_new_folder_name("") self.set_variant("") self.set_comment("") @property - def new_asset_name(self): - return self._new_asset_name + def new_folder_name(self): + return self._new_folder_name @property def variant(self): @@ -280,70 +64,58 @@ class UserPublishValues: return self._is_variant_valid @property - def is_new_asset_name_valid(self): - return self._is_new_asset_name_valid + def is_new_folder_name_valid(self): + return self._is_new_folder_name_valid @property def is_valid(self): - return self.is_variant_valid and self.is_new_asset_name_valid + return self.is_variant_valid and self.is_new_folder_name_valid + + def get_data(self): + return { + "new_folder_name": self._new_folder_name, + "variant": self._variant, + "comment": self._comment, + "is_variant_valid": self._is_variant_valid, + "is_new_folder_name_valid": self._is_new_folder_name_valid, + "is_valid": self.is_valid + } def set_variant(self, variant): if variant == self._variant: return - old_variant = self._variant - old_is_valid = self._is_variant_valid - self._variant = variant is_valid = False if variant: is_valid = self.variant_regex.match(variant) is not None self._is_variant_valid = is_valid - changes = { - key: {"new": new, "old": old} - for key, old, new in ( - ("variant", old_variant, variant), - ("is_valid", old_is_valid, is_valid) - ) - } - - self._event_system.emit( + self._controller.emit_event( "variant.changed", { "variant": variant, "is_valid": self._is_variant_valid, - "changes": changes }, "user_values" ) - def set_new_asset(self, asset_name): - if self._new_asset_name == asset_name: + def set_new_folder_name(self, folder_name): + if self._new_folder_name == folder_name: return - old_asset_name = self._new_asset_name - old_is_valid = self._is_new_asset_name_valid - self._new_asset_name = asset_name - is_valid = True - if asset_name: - is_valid = ( - self.asset_name_regex.match(asset_name) is not None - ) - self._is_new_asset_name_valid = is_valid - changes = { - key: {"new": new, "old": old} - for key, old, new in ( - ("new_asset_name", old_asset_name, asset_name), - ("is_valid", old_is_valid, is_valid) - ) - } - self._event_system.emit( - "new_asset_name.changed", + self._new_folder_name = folder_name + is_valid = True + if folder_name: + is_valid = ( + self.folder_name_regex.match(folder_name) is not None + ) + self._is_new_folder_name_valid = is_valid + self._controller.emit_event( + "new_folder_name.changed", { - "new_asset_name": self._new_asset_name, - "is_valid": self._is_new_asset_name_valid, - "changes": changes + "new_folder_name": self._new_folder_name, + "is_valid": self._is_new_folder_name_valid, }, "user_values" ) @@ -351,42 +123,30 @@ class UserPublishValues: def set_comment(self, comment): if comment == self._comment: return - old_comment = self._comment self._comment = comment - self._event_system.emit( + self._controller.emit_event( "comment.changed", - { - "comment": comment, - "changes": { - "comment": {"new": comment, "old": old_comment} - } - }, + {"comment": comment}, "user_values" ) class PushToContextController: def __init__(self, project_name=None, version_id=None): + self._event_system = self._create_event_system() + + self._projects_model = ProjectsModel(self) + self._hierarchy_model = HierarchyModel(self) + + self._selection_model = PushToProjectSelectionModel(self) + self._user_values = UserPublishValues(self) + self._src_project_name = None self._src_version_id = None self._src_asset_doc = None self._src_subset_doc = None self._src_version_doc = None - - event_system = EventSystem() - entities_model = EntitiesModel(event_system) - selection_model = SelectionModel(event_system) - user_values = UserPublishValues(event_system) - - self._event_system = event_system - self._entities_model = entities_model - self._selection_model = selection_model - self._user_values = user_values - - event_system.add_callback("project.changed", self._on_project_change) - event_system.add_callback("asset.changed", self._invalidate) - event_system.add_callback("variant.changed", self._invalidate) - event_system.add_callback("new_asset_name.changed", self._invalidate) + self._src_label = None self._submission_enabled = False self._process_thread = None @@ -394,6 +154,157 @@ class PushToContextController: self.set_source(project_name, version_id) + # Events system + 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 set_source(self, project_name, version_id): + if ( + project_name == self._src_project_name + and version_id == self._src_version_id + ): + return + + self._src_project_name = project_name + self._src_version_id = version_id + self._src_label = None + asset_doc = None + subset_doc = None + version_doc = None + if project_name and version_id: + version_doc = get_version_by_id(project_name, version_id) + + if version_doc: + subset_doc = get_subset_by_id(project_name, version_doc["parent"]) + + if subset_doc: + asset_doc = get_asset_by_id(project_name, subset_doc["parent"]) + + self._src_asset_doc = asset_doc + self._src_subset_doc = subset_doc + self._src_version_doc = version_doc + if asset_doc: + self._user_values.set_new_folder_name(asset_doc["name"]) + variant = self._get_src_variant() + if variant: + self._user_values.set_variant(variant) + + comment = version_doc["data"].get("comment") + if comment: + self._user_values.set_comment(comment) + + self._event_system.emit( + "source.changed", { + "project_name": project_name, + "version_id": version_id + }, + "controller" + ) + + def get_source_label(self): + if self._src_label is None: + self._src_label = self._get_source_label() + return self._src_label + + def get_project_items(self, sender=None): + return self._projects_model.get_project_items(sender) + + def get_folder_items(self, project_name, sender=None): + return self._hierarchy_model.get_folder_items(project_name, sender) + + def get_task_items(self, project_name, folder_id, sender=None): + return self._hierarchy_model.get_task_items( + project_name, folder_id, sender + ) + + def get_user_values(self): + return self._user_values.get_data() + + def set_user_value_folder_name(self, folder_name): + self._user_values.set_new_folder_name(folder_name) + + def set_user_value_variant(self, variant): + self._user_values.set_variant(variant) + + def set_user_value_comment(self, comment): + self._user_values.set_comment(comment) + + def set_selected_project(self, project_name): + self._selection_model.set_selected_project(project_name) + + def set_selected_folder(self, folder_id): + self._selection_model.set_selected_folder(folder_id) + + def set_selected_task(self, task_id, task_name): + self._selection_model.set_selected_task(task_id, task_name) + + # Processing methods + def submit(self, wait=True): + if not self._submission_enabled: + return + + if self._process_thread is not None: + return + + item = ProjectPushItem( + self._src_project_name, + self._src_version_id, + self._selection_model.get_selected_project_name(), + self._selection_model.get_selected_folder_id(), + self._selection_model.get_selected_task_name(), + self._user_values.variant, + comment=self._user_values.comment, + new_folder_name=self._user_values.new_folder_name, + dst_version=1 + ) + + status_item = ProjectPushItemStatus(event_system=self._event_system) + process_item = ProjectPushItemProcess(item, status_item) + self._process_item = process_item + self._event_system.emit("submit.started", {}, "controller") + if wait: + self._submit_callback() + self._process_item = None + return process_item + + thread = threading.Thread(target=self._submit_callback) + self._process_thread = thread + thread.start() + return process_item + + def wait_for_process_thread(self): + if self._process_thread is None: + return + self._process_thread.join() + self._process_thread = None + + def _get_source_label(self): + if not self._src_project_name or not self._src_version_id: + return "Source is not defined" + + asset_doc = self._src_asset_doc + if not asset_doc: + return "Source is invalid" + + asset_path_parts = list(asset_doc["data"]["parents"]) + asset_path_parts.append(asset_doc["name"]) + asset_path = "/".join(asset_path_parts) + subset_doc = self._src_subset_doc + version_doc = self._src_version_doc + return "Source: {}/{}/{}/v{:0>3}".format( + self._src_project_name, + asset_path, + subset_doc["name"], + version_doc["name"] + ) + def _get_task_info_from_repre_docs(self, asset_doc, repre_docs): asset_tasks = asset_doc["data"].get("tasks") or {} found_comb = [] @@ -436,7 +347,7 @@ class PushToContextController: ) project_settings = get_project_settings(project_name) - subset_doc = self.src_subset_doc + subset_doc = self._src_subset_doc family = subset_doc["data"].get("family") if not family: family = subset_doc["data"]["families"][0] @@ -470,7 +381,7 @@ class PushToContextController: print("Failed format", exc) return "" - subset_name = self.src_subset_doc["name"] + subset_name = self._src_subset_doc["name"] if ( (subset_s and not subset_name.startswith(subset_s)) or (subset_e and not subset_name.endswith(subset_e)) @@ -483,112 +394,7 @@ class PushToContextController: subset_name = subset_name[:len(subset_e)] return subset_name - def set_source(self, project_name, version_id): - if ( - project_name == self._src_project_name - and version_id == self._src_version_id - ): - return - - self._src_project_name = project_name - self._src_version_id = version_id - asset_doc = None - subset_doc = None - version_doc = None - if project_name and version_id: - version_doc = get_version_by_id(project_name, version_id) - - if version_doc: - subset_doc = get_subset_by_id(project_name, version_doc["parent"]) - - if subset_doc: - asset_doc = get_asset_by_id(project_name, subset_doc["parent"]) - - self._src_asset_doc = asset_doc - self._src_subset_doc = subset_doc - self._src_version_doc = version_doc - if asset_doc: - self.user_values.set_new_asset(asset_doc["name"]) - variant = self._get_src_variant() - if variant: - self.user_values.set_variant(variant) - - comment = version_doc["data"].get("comment") - if comment: - self.user_values.set_comment(comment) - - self._event_system.emit( - "source.changed", { - "project_name": project_name, - "version_id": version_id - }, - "controller" - ) - - @property - def src_project_name(self): - return self._src_project_name - - @property - def src_version_id(self): - return self._src_version_id - - @property - def src_label(self): - if not self._src_project_name or not self._src_version_id: - return "Source is not defined" - - asset_doc = self.src_asset_doc - if not asset_doc: - return "Source is invalid" - - asset_path_parts = list(asset_doc["data"]["parents"]) - asset_path_parts.append(asset_doc["name"]) - asset_path = "/".join(asset_path_parts) - subset_doc = self.src_subset_doc - version_doc = self.src_version_doc - return "Source: {}/{}/{}/v{:0>3}".format( - self._src_project_name, - asset_path, - subset_doc["name"], - version_doc["name"] - ) - - @property - def src_version_doc(self): - return self._src_version_doc - - @property - def src_subset_doc(self): - return self._src_subset_doc - - @property - def src_asset_doc(self): - return self._src_asset_doc - - @property - def event_system(self): - return self._event_system - - @property - def model(self): - return self._entities_model - - @property - def selection_model(self): - return self._selection_model - - @property - def user_values(self): - return self._user_values - - @property - def submission_enabled(self): - return self._submission_enabled - def _on_project_change(self, event): - project_name = event["project_name"] - self.model.refresh_assets(project_name) self._invalidate() def _invalidate(self): @@ -606,68 +412,17 @@ class PushToContextController: if not self._user_values.is_valid: return False - if not self.selection_model.project_name: + if not self._selection_model.get_selected_project_name(): return False if ( - not self._user_values.new_asset_name - and not self.selection_model.asset_id + not self._user_values.new_folder_name + and not self._selection_model.get_selected_folder_id() ): return False return True - def get_selected_asset_name(self): - project_name = self._selection_model.project_name - asset_id = self._selection_model.asset_id - if not project_name or not asset_id: - return None - asset_item = self._entities_model.get_asset_by_id( - project_name, asset_id - ) - if asset_item: - return asset_item.name - return None - - def submit(self, wait=True): - if not self.submission_enabled: - return - - if self._process_thread is not None: - return - - item = ProjectPushItem( - self.src_project_name, - self.src_version_id, - self.selection_model.project_name, - self.selection_model.asset_id, - self.selection_model.task_name, - self.user_values.variant, - comment=self.user_values.comment, - new_asset_name=self.user_values.new_asset_name, - dst_version=1 - ) - - status_item = ProjectPushItemStatus(event_system=self._event_system) - process_item = ProjectPushItemProcess(item, status_item) - self._process_item = process_item - self._event_system.emit("submit.started", {}, "controller") - if wait: - self._submit_callback() - self._process_item = None - return process_item - - thread = threading.Thread(target=self._submit_callback) - self._process_thread = thread - thread.start() - return process_item - - def wait_for_process_thread(self): - if self._process_thread is None: - return - self._process_thread.join() - self._process_thread = None - def _submit_callback(self): process_item = self._process_item if process_item is None: @@ -676,3 +431,6 @@ class PushToContextController: self._event_system.emit("submit.finished", {}, "controller") if process_item is self._process_item: self._process_item = None + + def _create_event_system(self): + return QueuedEventSystem() diff --git a/openpype/tools/ayon_push_to_project/models/__init__.py b/openpype/tools/ayon_push_to_project/models/__init__.py new file mode 100644 index 0000000000..0123fc9355 --- /dev/null +++ b/openpype/tools/ayon_push_to_project/models/__init__.py @@ -0,0 +1,6 @@ +from .selection import PushToProjectSelectionModel + + +__all__ = ( + "PushToProjectSelectionModel", +) diff --git a/openpype/tools/ayon_push_to_project/models/selection.py b/openpype/tools/ayon_push_to_project/models/selection.py new file mode 100644 index 0000000000..19f1c6d37d --- /dev/null +++ b/openpype/tools/ayon_push_to_project/models/selection.py @@ -0,0 +1,72 @@ +class PushToProjectSelectionModel(object): + """Model handling selection changes. + + Triggering events: + - "selection.project.changed" + - "selection.folder.changed" + - "selection.task.changed" + """ + + event_source = "push-to-project.selection.model" + + def __init__(self, controller): + self._controller = controller + + self._project_name = None + self._folder_id = None + self._task_name = None + self._task_id = None + + def get_selected_project_name(self): + return self._project_name + + def set_selected_project(self, project_name): + if project_name == self._project_name: + return + + self._project_name = project_name + self._controller.emit_event( + "selection.project.changed", + {"project_name": project_name}, + self.event_source + ) + + 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", + { + "project_name": self._project_name, + "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, task_id, task_name): + if task_id == self._task_id: + return + + self._task_name = task_name + self._task_id = task_id + self._controller.emit_event( + "selection.task.changed", + { + "project_name": self._project_name, + "folder_id": self._folder_id, + "task_name": task_name, + "task_id": task_id, + }, + self.event_source + ) From 78eebdaca177543235dafbda26763365678532d3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 13 Oct 2023 18:12:21 +0200 Subject: [PATCH 0445/1233] initial changes of window --- .../tools/ayon_push_to_project/ui/window.py | 650 ++++-------------- 1 file changed, 126 insertions(+), 524 deletions(-) diff --git a/openpype/tools/ayon_push_to_project/ui/window.py b/openpype/tools/ayon_push_to_project/ui/window.py index a1fff2d27d..d5b2823490 100644 --- a/openpype/tools/ayon_push_to_project/ui/window.py +++ b/openpype/tools/ayon_push_to_project/ui/window.py @@ -1,369 +1,19 @@ -import collections - from qtpy import QtWidgets, QtGui, QtCore from openpype.style import load_stylesheet, get_app_icon_path from openpype.tools.utils import ( PlaceholderLineEdit, SeparatorWidget, - get_asset_icon_by_name, set_style_property, ) -from openpype.tools.utils.views import DeselectableTreeView - -from openpype.tools.ayon_push_to_project import PushToContextController - -PROJECT_NAME_ROLE = QtCore.Qt.UserRole + 1 -ASSET_NAME_ROLE = QtCore.Qt.UserRole + 2 -ASSET_ID_ROLE = QtCore.Qt.UserRole + 3 -TASK_NAME_ROLE = QtCore.Qt.UserRole + 4 -TASK_TYPE_ROLE = QtCore.Qt.UserRole + 5 - - -class ProjectsModel(QtGui.QStandardItemModel): - empty_text = "< Empty >" - select_project_text = "< Select Project >" - - refreshed = QtCore.Signal() - - def __init__(self, controller): - super(ProjectsModel, self).__init__() - self._controller = controller - - self.event_system.add_callback( - "projects.refresh.finished", self._on_refresh_finish - ) - - placeholder_item = QtGui.QStandardItem(self.empty_text) - - root_item = self.invisibleRootItem() - root_item.appendRows([placeholder_item]) - items = {None: placeholder_item} - - self._placeholder_item = placeholder_item - self._items = items - - @property - def event_system(self): - return self._controller.event_system - - def _on_refresh_finish(self): - root_item = self.invisibleRootItem() - project_names = self._controller.model.get_projects() - - if not project_names: - placeholder_text = self.empty_text - else: - placeholder_text = self.select_project_text - self._placeholder_item.setData(placeholder_text, QtCore.Qt.DisplayRole) - - new_items = [] - if None not in self._items: - new_items.append(self._placeholder_item) - - current_project_names = set(self._items.keys()) - for project_name in current_project_names - set(project_names): - if project_name is None: - continue - item = self._items.pop(project_name) - root_item.takeRow(item.row()) - - for project_name in project_names: - if project_name in self._items: - continue - item = QtGui.QStandardItem(project_name) - item.setData(project_name, PROJECT_NAME_ROLE) - new_items.append(item) - - if new_items: - root_item.appendRows(new_items) - self.refreshed.emit() - - -class ProjectProxyModel(QtCore.QSortFilterProxyModel): - def __init__(self): - super(ProjectProxyModel, self).__init__() - self._filter_empty_projects = False - - def set_filter_empty_project(self, filter_empty_projects): - if filter_empty_projects == self._filter_empty_projects: - return - self._filter_empty_projects = filter_empty_projects - self.invalidate() - - def filterAcceptsRow(self, row, parent): - if not self._filter_empty_projects: - return True - model = self.sourceModel() - source_index = model.index(row, self.filterKeyColumn(), parent) - if model.data(source_index, PROJECT_NAME_ROLE) is None: - return False - return True - - -class AssetsModel(QtGui.QStandardItemModel): - items_changed = QtCore.Signal() - empty_text = "< Empty >" - - def __init__(self, controller): - super(AssetsModel, self).__init__() - self._controller = controller - - placeholder_item = QtGui.QStandardItem(self.empty_text) - placeholder_item.setFlags(QtCore.Qt.ItemIsEnabled) - - root_item = self.invisibleRootItem() - root_item.appendRows([placeholder_item]) - - self.event_system.add_callback( - "project.changed", self._on_project_change - ) - self.event_system.add_callback( - "assets.refresh.started", self._on_refresh_start - ) - self.event_system.add_callback( - "assets.refresh.finished", self._on_refresh_finish - ) - - self._items = {None: placeholder_item} - - self._placeholder_item = placeholder_item - self._last_project = None - - @property - def event_system(self): - return self._controller.event_system - - def _clear(self): - placeholder_in = False - root_item = self.invisibleRootItem() - for row in reversed(range(root_item.rowCount())): - item = root_item.child(row) - asset_id = item.data(ASSET_ID_ROLE) - if asset_id is None: - placeholder_in = True - continue - root_item.removeRow(item.row()) - - for key in tuple(self._items.keys()): - if key is not None: - self._items.pop(key) - - if not placeholder_in: - root_item.appendRows([self._placeholder_item]) - self._items[None] = self._placeholder_item - - def _on_project_change(self, event): - project_name = event["project_name"] - if project_name == self._last_project: - return - - self._last_project = project_name - self._clear() - self.items_changed.emit() - - def _on_refresh_start(self, event): - pass - - def _on_refresh_finish(self, event): - event_project_name = event["project_name"] - project_name = self._controller.selection_model.project_name - if event_project_name != project_name: - return - - self._last_project = event["project_name"] - if project_name is None: - if None not in self._items: - self._clear() - self.items_changed.emit() - return - - asset_items_by_id = self._controller.model.get_assets(project_name) - if not asset_items_by_id: - self._clear() - self.items_changed.emit() - return - - assets_by_parent_id = collections.defaultdict(list) - for asset_item in asset_items_by_id.values(): - assets_by_parent_id[asset_item.parent_id].append(asset_item) - - root_item = self.invisibleRootItem() - if None in self._items: - self._items.pop(None) - root_item.takeRow(self._placeholder_item.row()) - - items_to_remove = set(self._items) - set(asset_items_by_id.keys()) - hierarchy_queue = collections.deque() - hierarchy_queue.append((None, root_item)) - while hierarchy_queue: - parent_id, parent_item = hierarchy_queue.popleft() - new_items = [] - for asset_item in assets_by_parent_id[parent_id]: - item = self._items.get(asset_item.id) - if item is None: - item = QtGui.QStandardItem() - item.setFlags( - QtCore.Qt.ItemIsSelectable - | QtCore.Qt.ItemIsEnabled - ) - new_items.append(item) - self._items[asset_item.id] = item - - elif item.parent() is not parent_item: - new_items.append(item) - - icon = get_asset_icon_by_name( - asset_item.icon_name, asset_item.icon_color - ) - item.setData(asset_item.name, QtCore.Qt.DisplayRole) - item.setData(icon, QtCore.Qt.DecorationRole) - item.setData(asset_item.id, ASSET_ID_ROLE) - - hierarchy_queue.append((asset_item.id, item)) - - if new_items: - parent_item.appendRows(new_items) - - for item_id in items_to_remove: - item = self._items.pop(item_id, None) - if item is None: - continue - row = item.row() - if row < 0: - continue - parent = item.parent() - if parent is None: - parent = root_item - parent.takeRow(row) - - self.items_changed.emit() - - -class TasksModel(QtGui.QStandardItemModel): - items_changed = QtCore.Signal() - empty_text = "< Empty >" - - def __init__(self, controller): - super(TasksModel, self).__init__() - self._controller = controller - - placeholder_item = QtGui.QStandardItem(self.empty_text) - placeholder_item.setFlags(QtCore.Qt.ItemIsEnabled) - - root_item = self.invisibleRootItem() - root_item.appendRows([placeholder_item]) - - self.event_system.add_callback( - "project.changed", self._on_project_change - ) - self.event_system.add_callback( - "assets.refresh.finished", self._on_asset_refresh_finish - ) - self.event_system.add_callback( - "asset.changed", self._on_asset_change - ) - - self._items = {None: placeholder_item} - - self._placeholder_item = placeholder_item - self._last_project = None - - @property - def event_system(self): - return self._controller.event_system - - def _clear(self): - placeholder_in = False - root_item = self.invisibleRootItem() - for row in reversed(range(root_item.rowCount())): - item = root_item.child(row) - task_name = item.data(TASK_NAME_ROLE) - if task_name is None: - placeholder_in = True - continue - root_item.removeRow(item.row()) - - for key in tuple(self._items.keys()): - if key is not None: - self._items.pop(key) - - if not placeholder_in: - root_item.appendRows([self._placeholder_item]) - self._items[None] = self._placeholder_item - - def _on_project_change(self, event): - project_name = event["project_name"] - if project_name == self._last_project: - return - - self._last_project = project_name - self._clear() - self.items_changed.emit() - - def _on_asset_refresh_finish(self, event): - self._refresh(event["project_name"]) - - def _on_asset_change(self, event): - self._refresh(event["project_name"]) - - def _refresh(self, new_project_name): - project_name = self._controller.selection_model.project_name - if new_project_name != project_name: - return - - self._last_project = project_name - if project_name is None: - if None not in self._items: - self._clear() - self.items_changed.emit() - return - - asset_id = self._controller.selection_model.asset_id - task_items = self._controller.model.get_tasks( - project_name, asset_id - ) - if not task_items: - self._clear() - self.items_changed.emit() - return - - root_item = self.invisibleRootItem() - if None in self._items: - self._items.pop(None) - root_item.takeRow(self._placeholder_item.row()) - - new_items = [] - task_names = set() - for task_item in task_items: - task_name = task_item.name - item = self._items.get(task_name) - if item is None: - item = QtGui.QStandardItem() - item.setFlags( - QtCore.Qt.ItemIsSelectable - | QtCore.Qt.ItemIsEnabled - ) - new_items.append(item) - self._items[task_name] = item - - item.setData(task_name, QtCore.Qt.DisplayRole) - item.setData(task_name, TASK_NAME_ROLE) - item.setData(task_item.task_type, TASK_TYPE_ROLE) - - if new_items: - root_item.appendRows(new_items) - - items_to_remove = set(self._items) - task_names - for item_id in items_to_remove: - item = self._items.pop(item_id, None) - if item is None: - continue - parent = item.parent() - if parent is not None: - parent.removeRow(item.row()) - - self.items_changed.emit() +from openpype.tools.ayon_utils.widgets import ( + ProjectsCombobox, + FoldersWidget, + TasksWidget, +) +from openpype.tools.ayon_push_to_project.control import ( + PushToContextController, +) class PushToContextSelectWindow(QtWidgets.QWidget): @@ -380,7 +30,10 @@ class PushToContextSelectWindow(QtWidgets.QWidget): header_widget = QtWidgets.QWidget(main_context_widget) - header_label = QtWidgets.QLabel(controller.src_label, header_widget) + header_label = QtWidgets.QLabel( + controller.get_source_label(), + header_widget + ) header_layout = QtWidgets.QHBoxLayout(header_widget) header_layout.setContentsMargins(0, 0, 0, 0) @@ -392,48 +45,32 @@ class PushToContextSelectWindow(QtWidgets.QWidget): context_widget = QtWidgets.QWidget(main_splitter) - project_combobox = QtWidgets.QComboBox(context_widget) - project_model = ProjectsModel(controller) - project_proxy = ProjectProxyModel() - project_proxy.setSourceModel(project_model) - project_proxy.setDynamicSortFilter(True) - project_delegate = QtWidgets.QStyledItemDelegate() - project_combobox.setItemDelegate(project_delegate) - project_combobox.setModel(project_proxy) + projects_combobox = ProjectsCombobox(controller, context_widget) + projects_combobox.set_select_item_visible(True) + projects_combobox.set_standard_filter_enabled(True) - asset_task_splitter = QtWidgets.QSplitter( + context_splitter = QtWidgets.QSplitter( QtCore.Qt.Vertical, context_widget ) - asset_view = DeselectableTreeView(asset_task_splitter) - asset_view.setHeaderHidden(True) - asset_model = AssetsModel(controller) - asset_proxy = QtCore.QSortFilterProxyModel() - asset_proxy.setSourceModel(asset_model) - asset_proxy.setDynamicSortFilter(True) - asset_view.setModel(asset_proxy) + folders_widget = FoldersWidget(controller, context_splitter) + folders_widget.set_deselectable(True) + tasks_widget = TasksWidget(controller, context_splitter) - task_view = QtWidgets.QListView(asset_task_splitter) - task_proxy = QtCore.QSortFilterProxyModel() - task_model = TasksModel(controller) - task_proxy.setSourceModel(task_model) - task_proxy.setDynamicSortFilter(True) - task_view.setModel(task_proxy) - - asset_task_splitter.addWidget(asset_view) - asset_task_splitter.addWidget(task_view) + context_splitter.addWidget(folders_widget) + context_splitter.addWidget(tasks_widget) context_layout = QtWidgets.QVBoxLayout(context_widget) context_layout.setContentsMargins(0, 0, 0, 0) - context_layout.addWidget(project_combobox, 0) - context_layout.addWidget(asset_task_splitter, 1) + context_layout.addWidget(projects_combobox, 0) + context_layout.addWidget(context_splitter, 1) # --- Inputs widget --- inputs_widget = QtWidgets.QWidget(main_splitter) - asset_name_input = PlaceholderLineEdit(inputs_widget) - asset_name_input.setPlaceholderText("< Name of new asset >") - asset_name_input.setObjectName("ValidatedLineEdit") + folder_name_input = PlaceholderLineEdit(inputs_widget) + folder_name_input.setPlaceholderText("< Name of new folder >") + folder_name_input.setObjectName("ValidatedLineEdit") variant_input = PlaceholderLineEdit(inputs_widget) variant_input.setPlaceholderText("< Variant >") @@ -444,7 +81,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget): inputs_layout = QtWidgets.QFormLayout(inputs_widget) inputs_layout.setContentsMargins(0, 0, 0, 0) - inputs_layout.addRow("New asset name", asset_name_input) + inputs_layout.addRow("New folder name", folder_name_input) inputs_layout.addRow("Variant", variant_input) inputs_layout.addRow("Comment", comment_input) @@ -509,7 +146,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget): main_layout.setCurrentWidget(main_context_widget) show_timer = QtCore.QTimer() - show_timer.setInterval(1) + show_timer.setInterval(0) main_thread_timer = QtCore.QTimer() main_thread_timer.setInterval(10) @@ -521,46 +158,38 @@ class PushToContextSelectWindow(QtWidgets.QWidget): main_thread_timer.timeout.connect(self._on_main_thread_timer) show_timer.timeout.connect(self._on_show_timer) user_input_changed_timer.timeout.connect(self._on_user_input_timer) - asset_name_input.textChanged.connect(self._on_new_asset_change) + folder_name_input.textChanged.connect(self._on_new_asset_change) variant_input.textChanged.connect(self._on_variant_change) comment_input.textChanged.connect(self._on_comment_change) - project_model.refreshed.connect(self._on_projects_refresh) - project_combobox.currentIndexChanged.connect(self._on_project_change) - asset_view.selectionModel().selectionChanged.connect( - self._on_asset_change - ) - asset_model.items_changed.connect(self._on_asset_model_change) - task_view.selectionModel().selectionChanged.connect( - self._on_task_change - ) - task_model.items_changed.connect(self._on_task_model_change) + publish_btn.clicked.connect(self._on_select_click) cancel_btn.clicked.connect(self._on_close_click) overlay_close_btn.clicked.connect(self._on_close_click) overlay_try_btn.clicked.connect(self._on_try_again_click) - controller.event_system.add_callback( - "new_asset_name.changed", self._on_controller_new_asset_change + controller.register_event_callback( + "new_folder_name.changed", + self._on_controller_new_asset_change ) - controller.event_system.add_callback( + controller.register_event_callback( "variant.changed", self._on_controller_variant_change ) - controller.event_system.add_callback( + controller.register_event_callback( "comment.changed", self._on_controller_comment_change ) - controller.event_system.add_callback( + controller.register_event_callback( "submission.enabled.changed", self._on_submission_change ) - controller.event_system.add_callback( + controller.register_event_callback( "source.changed", self._on_controller_source_change ) - controller.event_system.add_callback( + controller.register_event_callback( "submit.started", self._on_controller_submit_start ) - controller.event_system.add_callback( + controller.register_event_callback( "submit.finished", self._on_controller_submit_end ) - controller.event_system.add_callback( + controller.register_event_callback( "push.message.added", self._on_push_message ) @@ -571,20 +200,12 @@ class PushToContextSelectWindow(QtWidgets.QWidget): self._header_label = header_label self._main_splitter = main_splitter - self._project_combobox = project_combobox - self._project_model = project_model - self._project_proxy = project_proxy - self._project_delegate = project_delegate - - self._asset_view = asset_view - self._asset_model = asset_model - self._asset_proxy_model = asset_proxy - - self._task_view = task_view - self._task_proxy_model = task_proxy + self._projects_combobox = projects_combobox + self._folders_widget = folders_widget + self._tasks_widget = tasks_widget self._variant_input = variant_input - self._asset_name_input = asset_name_input + self._folder_name_input = folder_name_input self._comment_input = comment_input self._publish_btn = publish_btn @@ -600,60 +221,78 @@ class PushToContextSelectWindow(QtWidgets.QWidget): # The goal is to have controll over changes happened during user change # in UI and controller auto-changes self._variant_input_text = None - self._new_asset_name_input_text = None + self._new_folder_name_input_text = None self._comment_input_text = None - self._show_timer = show_timer - self._show_counter = 2 + self._first_show = True + self._show_timer = show_timer + self._show_counter = 0 self._main_thread_timer = main_thread_timer self._main_thread_timer_can_stop = True self._last_submit_message = None self._process_item = None + self._variant_is_valid = None + self._folder_is_valid = None + publish_btn.setEnabled(False) overlay_close_btn.setVisible(False) overlay_try_btn.setVisible(False) - if controller.user_values.new_asset_name: - asset_name_input.setText(controller.user_values.new_asset_name) - if controller.user_values.variant: - variant_input.setText(controller.user_values.variant) - self._invalidate_variant() - self._invalidate_new_asset_name() + # Support of public api function of controller + def set_source(self, project_name, version_id): + """Set source project and version. - @property - def controller(self): - return self._controller + Call the method on controller. + + Args: + project_name (Union[str, None]): Name of project. + version_id (Union[str, None]): Version id. + """ + + self._controller.set_source(project_name, version_id) def showEvent(self, event): super(PushToContextSelectWindow, self).showEvent(event) if self._first_show: self._first_show = False - self.setStyleSheet(load_stylesheet()) - self._invalidate_variant() - self._show_timer.start() + self._on_first_show() + + def refresh(self): + user_values = self._controller.get_user_values() + new_folder_name = user_values["new_folder_name"] + variant = user_values["variant"] + self._folder_name_input.setText(new_folder_name or "") + self._variant_input.setText(variant or "") + self._invalidate_variant(user_values["is_variant_valid"]) + self._invalidate_new_folder_name( + new_folder_name, user_values["is_new_folder_name_valid"] + ) + + self._projects_combobox.refresh() + + def _on_first_show(self): + width = 740 + height = 640 + inputs_width = 360 + self.setStyleSheet(load_stylesheet()) + self.resize(width, height) + self._main_splitter.setSizes([width - inputs_width, inputs_width]) + self._show_timer.start() def _on_show_timer(self): - if self._show_counter == 0: - self._show_timer.stop() + if self._show_counter < 3: + self._show_counter += 1 return + self._show_timer.stop() - self._show_counter -= 1 - if self._show_counter == 1: - width = 740 - height = 640 - inputs_width = 360 - self.resize(width, height) - self._main_splitter.setSizes([width - inputs_width, inputs_width]) + self._show_counter = 0 - if self._show_counter > 0: - return - - self._controller.model.refresh_projects() + self.refresh() def _on_new_asset_change(self, text): - self._new_asset_name_input_text = text + self._new_folder_name_input_text = text self._user_input_changed_timer.start() def _on_variant_change(self, text): @@ -665,42 +304,41 @@ class PushToContextSelectWindow(QtWidgets.QWidget): self._user_input_changed_timer.start() def _on_user_input_timer(self): - asset_name = self._new_asset_name_input_text - if asset_name is not None: - self._new_asset_name_input_text = None - self._controller.user_values.set_new_asset(asset_name) + folder_name = self._new_folder_name_input_text + if folder_name is not None: + self._new_folder_name_input_text = None + self._controller.set_user_value_folder_name(folder_name) variant = self._variant_input_text if variant is not None: self._variant_input_text = None - self._controller.user_values.set_variant(variant) + self._controller.set_user_value_variant(variant) comment = self._comment_input_text if comment is not None: self._comment_input_text = None - self._controller.user_values.set_comment(comment) + self._controller.set_user_value_comment(comment) def _on_controller_new_asset_change(self, event): - asset_name = event["changes"]["new_asset_name"]["new"] + folder_name = event["new_folder_name"] if ( - self._new_asset_name_input_text is None - and asset_name != self._asset_name_input.text() + self._new_folder_name_input_text is None + and folder_name != self._folder_name_input.text() ): - self._asset_name_input.setText(asset_name) + self._folder_name_input.setText(folder_name) - self._invalidate_new_asset_name() + self._invalidate_new_folder_name(folder_name, event["is_valid"]) def _on_controller_variant_change(self, event): - is_valid_changes = event["changes"]["is_valid"] - variant = event["changes"]["variant"]["new"] + is_valid = event["is_valid"] + variant = event["variant"] if ( self._variant_input_text is None and variant != self._variant_input.text() ): self._variant_input.setText(variant) - if is_valid_changes["old"] != is_valid_changes["new"]: - self._invalidate_variant() + self._invalidate_variant(is_valid) def _on_controller_comment_change(self, event): comment = event["comment"] @@ -711,66 +349,30 @@ class PushToContextSelectWindow(QtWidgets.QWidget): self._comment_input.setText(comment) def _on_controller_source_change(self): - self._header_label.setText(self._controller.src_label) + self._header_label.setText(self._controller.get_source_label()) - def _invalidate_new_asset_name(self): - asset_name = self._controller.user_values.new_asset_name - self._task_view.setVisible(not asset_name) - - valid = None - if asset_name: - valid = self._controller.user_values.is_new_asset_name_valid - - state = "" - if valid is True: - state = "valid" - elif valid is False: - state = "invalid" - set_style_property(self._asset_name_input, "state", state) - - def _invalidate_variant(self): - valid = self._controller.user_values.is_variant_valid - state = "invalid" - if valid is True: - state = "valid" - set_style_property(self._variant_input, "state", state) - - def _on_projects_refresh(self): - self._project_proxy.sort(0, QtCore.Qt.AscendingOrder) - - def _on_project_change(self): - idx = self._project_combobox.currentIndex() - if idx < 0: - self._project_proxy.set_filter_empty_project(False) + def _invalidate_new_folder_name(self, folder_name, is_valid): + print(folder_name) + self._tasks_widget.setVisible(not folder_name) + if self._folder_is_valid is is_valid: return + self._folder_is_valid = is_valid + state = "" + if folder_name: + if is_valid is True: + state = "valid" + elif is_valid is False: + state = "invalid" + set_style_property( + self._folder_name_input, "state", state + ) - project_name = self._project_combobox.itemData(idx, PROJECT_NAME_ROLE) - self._project_proxy.set_filter_empty_project(project_name is not None) - self._controller.selection_model.select_project(project_name) - - def _on_asset_change(self): - indexes = self._asset_view.selectedIndexes() - index = next(iter(indexes), None) - asset_id = None - if index is not None: - model = self._asset_view.model() - asset_id = model.data(index, ASSET_ID_ROLE) - self._controller.selection_model.select_asset(asset_id) - - def _on_asset_model_change(self): - self._asset_proxy_model.sort(0, QtCore.Qt.AscendingOrder) - - def _on_task_model_change(self): - self._task_proxy_model.sort(0, QtCore.Qt.AscendingOrder) - - def _on_task_change(self): - indexes = self._task_view.selectedIndexes() - index = next(iter(indexes), None) - task_name = None - if index is not None: - model = self._task_view.model() - task_name = model.data(index, TASK_NAME_ROLE) - self._controller.selection_model.select_task(task_name) + def _invalidate_variant(self, is_valid): + if self._variant_is_valid is is_valid: + return + self._variant_is_valid = is_valid + state = "valid" if is_valid else "invalid" + set_style_property(self._variant_input, "state", state) def _on_submission_change(self, event): self._publish_btn.setEnabled(event["enabled"]) From 2ac76ad4afc402963e435fe1a25eae643ae526b6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 13 Oct 2023 18:22:56 +0200 Subject: [PATCH 0446/1233] moved user values model to different file --- .../tools/ayon_push_to_project/control.py | 128 ++---------------- .../ayon_push_to_project/models/__init__.py | 2 + .../models/user_values.py | 110 +++++++++++++++ 3 files changed, 123 insertions(+), 117 deletions(-) create mode 100644 openpype/tools/ayon_push_to_project/models/user_values.py diff --git a/openpype/tools/ayon_push_to_project/control.py b/openpype/tools/ayon_push_to_project/control.py index 4aef09156f..4cba437553 100644 --- a/openpype/tools/ayon_push_to_project/control.py +++ b/openpype/tools/ayon_push_to_project/control.py @@ -1,4 +1,3 @@ -import re import threading from openpype.client import ( @@ -10,10 +9,7 @@ from openpype.client import ( from openpype.settings import get_project_settings from openpype.lib import prepare_template_data from openpype.lib.events import QueuedEventSystem -from openpype.pipeline.create import ( - SUBSET_NAME_ALLOWED_SYMBOLS, - get_subset_name_template, -) +from openpype.pipeline.create import get_subset_name_template from openpype.tools.ayon_utils.models import ProjectsModel, HierarchyModel from .control_integrate import ( @@ -21,114 +17,10 @@ from .control_integrate import ( ProjectPushItemProcess, ProjectPushItemStatus, ) -from .models import PushToProjectSelectionModel - - -class UserPublishValues: - """Helper object to validate values required for push to different project. - - Args: - controller (PushToContextController): Event system to catch - and emit events. - """ - - folder_name_regex = re.compile("^[a-zA-Z0-9_.]+$") - variant_regex = re.compile("^[{}]+$".format(SUBSET_NAME_ALLOWED_SYMBOLS)) - - def __init__(self, controller): - self._controller = controller - self._new_folder_name = None - self._variant = None - self._comment = None - self._is_variant_valid = False - self._is_new_folder_name_valid = False - - self.set_new_folder_name("") - self.set_variant("") - self.set_comment("") - - @property - def new_folder_name(self): - return self._new_folder_name - - @property - def variant(self): - return self._variant - - @property - def comment(self): - return self._comment - - @property - def is_variant_valid(self): - return self._is_variant_valid - - @property - def is_new_folder_name_valid(self): - return self._is_new_folder_name_valid - - @property - def is_valid(self): - return self.is_variant_valid and self.is_new_folder_name_valid - - def get_data(self): - return { - "new_folder_name": self._new_folder_name, - "variant": self._variant, - "comment": self._comment, - "is_variant_valid": self._is_variant_valid, - "is_new_folder_name_valid": self._is_new_folder_name_valid, - "is_valid": self.is_valid - } - - def set_variant(self, variant): - if variant == self._variant: - return - - self._variant = variant - is_valid = False - if variant: - is_valid = self.variant_regex.match(variant) is not None - self._is_variant_valid = is_valid - - self._controller.emit_event( - "variant.changed", - { - "variant": variant, - "is_valid": self._is_variant_valid, - }, - "user_values" - ) - - def set_new_folder_name(self, folder_name): - if self._new_folder_name == folder_name: - return - - self._new_folder_name = folder_name - is_valid = True - if folder_name: - is_valid = ( - self.folder_name_regex.match(folder_name) is not None - ) - self._is_new_folder_name_valid = is_valid - self._controller.emit_event( - "new_folder_name.changed", - { - "new_folder_name": self._new_folder_name, - "is_valid": self._is_new_folder_name_valid, - }, - "user_values" - ) - - def set_comment(self, comment): - if comment == self._comment: - return - self._comment = comment - self._controller.emit_event( - "comment.changed", - {"comment": comment}, - "user_values" - ) +from .models import ( + PushToProjectSelectionModel, + UserPublishValuesModel, +) class PushToContextController: @@ -139,7 +31,7 @@ class PushToContextController: self._hierarchy_model = HierarchyModel(self) self._selection_model = PushToProjectSelectionModel(self) - self._user_values = UserPublishValues(self) + self._user_values = UserPublishValuesModel(self) self._src_project_name = None self._src_version_id = None @@ -229,18 +121,23 @@ class PushToContextController: def set_user_value_folder_name(self, folder_name): self._user_values.set_new_folder_name(folder_name) + self._invalidate() def set_user_value_variant(self, variant): self._user_values.set_variant(variant) + self._invalidate() def set_user_value_comment(self, comment): self._user_values.set_comment(comment) + self._invalidate() def set_selected_project(self, project_name): self._selection_model.set_selected_project(project_name) + self._invalidate() def set_selected_folder(self, folder_id): self._selection_model.set_selected_folder(folder_id) + self._invalidate() def set_selected_task(self, task_id, task_name): self._selection_model.set_selected_task(task_id, task_name) @@ -394,9 +291,6 @@ class PushToContextController: subset_name = subset_name[:len(subset_e)] return subset_name - def _on_project_change(self, event): - self._invalidate() - def _invalidate(self): submission_enabled = self._check_submit_validations() if submission_enabled == self._submission_enabled: diff --git a/openpype/tools/ayon_push_to_project/models/__init__.py b/openpype/tools/ayon_push_to_project/models/__init__.py index 0123fc9355..48eb5e9f14 100644 --- a/openpype/tools/ayon_push_to_project/models/__init__.py +++ b/openpype/tools/ayon_push_to_project/models/__init__.py @@ -1,6 +1,8 @@ from .selection import PushToProjectSelectionModel +from .user_values import UserPublishValuesModel __all__ = ( "PushToProjectSelectionModel", + "UserPublishValuesModel", ) diff --git a/openpype/tools/ayon_push_to_project/models/user_values.py b/openpype/tools/ayon_push_to_project/models/user_values.py new file mode 100644 index 0000000000..2a4faeb136 --- /dev/null +++ b/openpype/tools/ayon_push_to_project/models/user_values.py @@ -0,0 +1,110 @@ +import re + +from openpype.pipeline.create import SUBSET_NAME_ALLOWED_SYMBOLS + + +class UserPublishValuesModel: + """Helper object to validate values required for push to different project. + + Args: + controller (PushToContextController): Event system to catch + and emit events. + """ + + folder_name_regex = re.compile("^[a-zA-Z0-9_.]+$") + variant_regex = re.compile("^[{}]+$".format(SUBSET_NAME_ALLOWED_SYMBOLS)) + + def __init__(self, controller): + self._controller = controller + self._new_folder_name = None + self._variant = None + self._comment = None + self._is_variant_valid = False + self._is_new_folder_name_valid = False + + self.set_new_folder_name("") + self.set_variant("") + self.set_comment("") + + @property + def new_folder_name(self): + return self._new_folder_name + + @property + def variant(self): + return self._variant + + @property + def comment(self): + return self._comment + + @property + def is_variant_valid(self): + return self._is_variant_valid + + @property + def is_new_folder_name_valid(self): + return self._is_new_folder_name_valid + + @property + def is_valid(self): + return self.is_variant_valid and self.is_new_folder_name_valid + + def get_data(self): + return { + "new_folder_name": self._new_folder_name, + "variant": self._variant, + "comment": self._comment, + "is_variant_valid": self._is_variant_valid, + "is_new_folder_name_valid": self._is_new_folder_name_valid, + "is_valid": self.is_valid + } + + def set_variant(self, variant): + if variant == self._variant: + return + + self._variant = variant + is_valid = False + if variant: + is_valid = self.variant_regex.match(variant) is not None + self._is_variant_valid = is_valid + + self._controller.emit_event( + "variant.changed", + { + "variant": variant, + "is_valid": self._is_variant_valid, + }, + "user_values" + ) + + def set_new_folder_name(self, folder_name): + if self._new_folder_name == folder_name: + return + + self._new_folder_name = folder_name + is_valid = True + if folder_name: + is_valid = ( + self.folder_name_regex.match(folder_name) is not None + ) + self._is_new_folder_name_valid = is_valid + self._controller.emit_event( + "new_folder_name.changed", + { + "new_folder_name": self._new_folder_name, + "is_valid": self._is_new_folder_name_valid, + }, + "user_values" + ) + + def set_comment(self, comment): + if comment == self._comment: + return + self._comment = comment + self._controller.emit_event( + "comment.changed", + {"comment": comment}, + "user_values" + ) From 7951c95f095e5418d0deb2d19e934868f6686238 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 13 Oct 2023 18:23:45 +0200 Subject: [PATCH 0447/1233] renamed '_get_source_label' to '_prepare_source_label' --- openpype/tools/ayon_push_to_project/control.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/tools/ayon_push_to_project/control.py b/openpype/tools/ayon_push_to_project/control.py index 4cba437553..1e6cbd55d4 100644 --- a/openpype/tools/ayon_push_to_project/control.py +++ b/openpype/tools/ayon_push_to_project/control.py @@ -102,7 +102,7 @@ class PushToContextController: def get_source_label(self): if self._src_label is None: - self._src_label = self._get_source_label() + self._src_label = self._prepare_source_label() return self._src_label def get_project_items(self, sender=None): @@ -182,7 +182,7 @@ class PushToContextController: self._process_thread.join() self._process_thread = None - def _get_source_label(self): + def _prepare_source_label(self): if not self._src_project_name or not self._src_version_id: return "Source is not defined" @@ -190,14 +190,14 @@ class PushToContextController: if not asset_doc: return "Source is invalid" - asset_path_parts = list(asset_doc["data"]["parents"]) - asset_path_parts.append(asset_doc["name"]) - asset_path = "/".join(asset_path_parts) + folder_path_parts = list(asset_doc["data"]["parents"]) + folder_path_parts.append(asset_doc["name"]) + folder_path = "/".join(folder_path_parts) subset_doc = self._src_subset_doc version_doc = self._src_version_doc return "Source: {}/{}/{}/v{:0>3}".format( self._src_project_name, - asset_path, + folder_path, subset_doc["name"], version_doc["name"] ) From 37da54b438a3d109b6bfab21f49d046ef89aa38c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 13 Oct 2023 18:24:07 +0200 Subject: [PATCH 0448/1233] implemented helper method to trigger controller events --- .../tools/ayon_push_to_project/control.py | 39 ++++++++++--------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/openpype/tools/ayon_push_to_project/control.py b/openpype/tools/ayon_push_to_project/control.py index 1e6cbd55d4..d07b915bf7 100644 --- a/openpype/tools/ayon_push_to_project/control.py +++ b/openpype/tools/ayon_push_to_project/control.py @@ -92,12 +92,12 @@ class PushToContextController: if comment: self._user_values.set_comment(comment) - self._event_system.emit( - "source.changed", { + self._emit_event( + "source.changed", + { "project_name": project_name, "version_id": version_id - }, - "controller" + } ) def get_source_label(self): @@ -165,7 +165,7 @@ class PushToContextController: status_item = ProjectPushItemStatus(event_system=self._event_system) process_item = ProjectPushItemProcess(item, status_item) self._process_item = process_item - self._event_system.emit("submit.started", {}, "controller") + self._emit_event("submit.started") if wait: self._submit_callback() self._process_item = None @@ -291,17 +291,6 @@ class PushToContextController: subset_name = subset_name[:len(subset_e)] return subset_name - def _invalidate(self): - submission_enabled = self._check_submit_validations() - if submission_enabled == self._submission_enabled: - return - self._submission_enabled = submission_enabled - self._event_system.emit( - "submission.enabled.changed", - {"enabled": submission_enabled}, - "controller" - ) - def _check_submit_validations(self): if not self._user_values.is_valid: return False @@ -314,17 +303,31 @@ class PushToContextController: and not self._selection_model.get_selected_folder_id() ): return False - return True + def _invalidate(self): + submission_enabled = self._check_submit_validations() + if submission_enabled == self._submission_enabled: + return + self._submission_enabled = submission_enabled + self._emit_event( + "submission.enabled.changed", + {"enabled": submission_enabled} + ) + def _submit_callback(self): process_item = self._process_item if process_item is None: return process_item.process() - self._event_system.emit("submit.finished", {}, "controller") + self._emit_event("submit.finished", {}) if process_item is self._process_item: self._process_item = None + def _emit_event(self, topic, data=None): + if data is None: + data = {} + self.emit_event(topic, data, "controller") + def _create_event_system(self): return QueuedEventSystem() From faadf3582c43ef19126d6d351501a4e8ccdbdc3f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 13 Oct 2023 18:24:16 +0200 Subject: [PATCH 0449/1233] removed debug print --- openpype/tools/ayon_push_to_project/ui/window.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/tools/ayon_push_to_project/ui/window.py b/openpype/tools/ayon_push_to_project/ui/window.py index d5b2823490..57c4c2619f 100644 --- a/openpype/tools/ayon_push_to_project/ui/window.py +++ b/openpype/tools/ayon_push_to_project/ui/window.py @@ -352,7 +352,6 @@ class PushToContextSelectWindow(QtWidgets.QWidget): self._header_label.setText(self._controller.get_source_label()) def _invalidate_new_folder_name(self, folder_name, is_valid): - print(folder_name) self._tasks_widget.setVisible(not folder_name) if self._folder_is_valid is is_valid: return From e729cc1964cfd351ba47057a48df143c4379f2b5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 13 Oct 2023 18:26:16 +0200 Subject: [PATCH 0450/1233] moved 'control_integrate.py' to models as 'integrate.py' --- openpype/tools/ayon_push_to_project/control.py | 10 +++++----- openpype/tools/ayon_push_to_project/models/__init__.py | 10 ++++++++++ .../{control_integrate.py => models/integrate.py} | 0 3 files changed, 15 insertions(+), 5 deletions(-) rename openpype/tools/ayon_push_to_project/{control_integrate.py => models/integrate.py} (100%) diff --git a/openpype/tools/ayon_push_to_project/control.py b/openpype/tools/ayon_push_to_project/control.py index d07b915bf7..4fc011da09 100644 --- a/openpype/tools/ayon_push_to_project/control.py +++ b/openpype/tools/ayon_push_to_project/control.py @@ -12,15 +12,15 @@ from openpype.lib.events import QueuedEventSystem from openpype.pipeline.create import get_subset_name_template from openpype.tools.ayon_utils.models import ProjectsModel, HierarchyModel -from .control_integrate import ( +from .models import ( + PushToProjectSelectionModel, + + UserPublishValuesModel, + ProjectPushItem, ProjectPushItemProcess, ProjectPushItemStatus, ) -from .models import ( - PushToProjectSelectionModel, - UserPublishValuesModel, -) class PushToContextController: diff --git a/openpype/tools/ayon_push_to_project/models/__init__.py b/openpype/tools/ayon_push_to_project/models/__init__.py index 48eb5e9f14..e8c0fae02e 100644 --- a/openpype/tools/ayon_push_to_project/models/__init__.py +++ b/openpype/tools/ayon_push_to_project/models/__init__.py @@ -1,8 +1,18 @@ from .selection import PushToProjectSelectionModel from .user_values import UserPublishValuesModel +from .integrate import ( + ProjectPushItem, + ProjectPushItemProcess, + ProjectPushItemStatus, +) __all__ = ( "PushToProjectSelectionModel", + "UserPublishValuesModel", + + "ProjectPushItem", + "ProjectPushItemProcess", + "ProjectPushItemStatus", ) diff --git a/openpype/tools/ayon_push_to_project/control_integrate.py b/openpype/tools/ayon_push_to_project/models/integrate.py similarity index 100% rename from openpype/tools/ayon_push_to_project/control_integrate.py rename to openpype/tools/ayon_push_to_project/models/integrate.py From ed4c306c43907f5ea8bbf0860aa08d5abb032dcc Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 16 Oct 2023 12:16:44 +0200 Subject: [PATCH 0451/1233] modified integration to avoid direct access to integrate objects --- .../tools/ayon_push_to_project/control.py | 47 +- .../ayon_push_to_project/models/__init__.py | 2 + .../ayon_push_to_project/models/integrate.py | 572 +++++++++--------- .../tools/ayon_push_to_project/ui/window.py | 18 +- 4 files changed, 328 insertions(+), 311 deletions(-) diff --git a/openpype/tools/ayon_push_to_project/control.py b/openpype/tools/ayon_push_to_project/control.py index 4fc011da09..0a19136701 100644 --- a/openpype/tools/ayon_push_to_project/control.py +++ b/openpype/tools/ayon_push_to_project/control.py @@ -14,12 +14,8 @@ from openpype.tools.ayon_utils.models import ProjectsModel, HierarchyModel from .models import ( PushToProjectSelectionModel, - UserPublishValuesModel, - - ProjectPushItem, - ProjectPushItemProcess, - ProjectPushItemStatus, + IntegrateModel, ) @@ -29,6 +25,7 @@ class PushToContextController: self._projects_model = ProjectsModel(self) self._hierarchy_model = HierarchyModel(self) + self._integrate_model = IntegrateModel(self) self._selection_model = PushToProjectSelectionModel(self) self._user_values = UserPublishValuesModel(self) @@ -42,7 +39,7 @@ class PushToContextController: self._submission_enabled = False self._process_thread = None - self._process_item = None + self._process_item_id = None self.set_source(project_name, version_id) @@ -58,6 +55,13 @@ class PushToContextController: self._event_system.add_callback(topic, callback) def set_source(self, project_name, version_id): + """Set source project and version. + + Args: + project_name (Union[str, None]): Source project name. + version_id (Union[str, None]): Source version id. + """ + if ( project_name == self._src_project_name and version_id == self._src_version_id @@ -101,6 +105,12 @@ class PushToContextController: ) def get_source_label(self): + """Get source label. + + Returns: + str: Label describing source project and version as path. + """ + if self._src_label is None: self._src_label = self._prepare_source_label() return self._src_label @@ -142,6 +152,9 @@ class PushToContextController: def set_selected_task(self, task_id, task_name): self._selection_model.set_selected_task(task_id, task_name) + def get_process_item_status(self, item_id): + return self._integrate_model.get_item_status(item_id) + # Processing methods def submit(self, wait=True): if not self._submission_enabled: @@ -150,7 +163,7 @@ class PushToContextController: if self._process_thread is not None: return - item = ProjectPushItem( + item_id = self._integrate_model.create_process_item( self._src_project_name, self._src_version_id, self._selection_model.get_selected_project_name(), @@ -162,19 +175,17 @@ class PushToContextController: dst_version=1 ) - status_item = ProjectPushItemStatus(event_system=self._event_system) - process_item = ProjectPushItemProcess(item, status_item) - self._process_item = process_item + self._process_item_id = item_id self._emit_event("submit.started") if wait: self._submit_callback() - self._process_item = None - return process_item + self._process_item_id = None + return item_id thread = threading.Thread(target=self._submit_callback) self._process_thread = thread thread.start() - return process_item + return item_id def wait_for_process_thread(self): if self._process_thread is None: @@ -316,13 +327,13 @@ class PushToContextController: ) def _submit_callback(self): - process_item = self._process_item - if process_item is None: + process_item_id = self._process_item_id + if process_item_id is None: return - process_item.process() + self._integrate_model.integrate_item(process_item_id) self._emit_event("submit.finished", {}) - if process_item is self._process_item: - self._process_item = None + if process_item_id == self._process_item_id: + self._process_item_id = None def _emit_event(self, topic, data=None): if data is None: diff --git a/openpype/tools/ayon_push_to_project/models/__init__.py b/openpype/tools/ayon_push_to_project/models/__init__.py index e8c0fae02e..5f909437a7 100644 --- a/openpype/tools/ayon_push_to_project/models/__init__.py +++ b/openpype/tools/ayon_push_to_project/models/__init__.py @@ -4,6 +4,7 @@ from .integrate import ( ProjectPushItem, ProjectPushItemProcess, ProjectPushItemStatus, + IntegrateModel, ) @@ -15,4 +16,5 @@ __all__ = ( "ProjectPushItem", "ProjectPushItemProcess", "ProjectPushItemStatus", + "IntegrateModel", ) diff --git a/openpype/tools/ayon_push_to_project/models/integrate.py b/openpype/tools/ayon_push_to_project/models/integrate.py index a822339ccf..b3de69c79a 100644 --- a/openpype/tools/ayon_push_to_project/models/integrate.py +++ b/openpype/tools/ayon_push_to_project/models/integrate.py @@ -6,6 +6,7 @@ import itertools import datetime import sys import traceback +import uuid from bson.objectid import ObjectId @@ -98,38 +99,62 @@ class ProjectPushItem: src_project_name, src_version_id, dst_project_name, - dst_asset_id, + dst_folder_id, dst_task_name, variant, - comment=None, - new_asset_name=None, - dst_version=None + comment, + new_folder_name, + dst_version, + item_id=None, ): + if not item_id: + item_id = uuid.uuid4().hex self.src_project_name = src_project_name self.src_version_id = src_version_id self.dst_project_name = dst_project_name - self.dst_asset_id = dst_asset_id + self.dst_folder_id = dst_folder_id self.dst_task_name = dst_task_name self.dst_version = dst_version self.variant = variant - self.new_asset_name = new_asset_name + self.new_folder_name = new_folder_name self.comment = comment or "" - self._id = "|".join([ - src_project_name, - src_version_id, - dst_project_name, - str(dst_asset_id), - str(new_asset_name), - str(dst_task_name), - str(dst_version) - ]) + self.item_id = item_id + self._repr_value = None @property - def id(self): - return self._id + def _repr(self): + if not self._repr_value: + self._repr_value = "|".join([ + self.src_project_name, + self.src_version_id, + self.dst_project_name, + str(self.dst_folder_id), + str(self.new_folder_name), + str(self.dst_task_name), + str(self.dst_version) + ]) + return self._repr_value def __repr__(self): - return "<{} - {}>".format(self.__class__.__name__, self.id) + return "<{} - {}>".format(self.__class__.__name__, self._repr) + + def to_data(self): + return { + "src_project_name": self.src_project_name, + "src_version_id": self.src_version_id, + "dst_project_name": self.dst_project_name, + "dst_folder_id": self.dst_folder_id, + "dst_task_name": self.dst_task_name, + "dst_version": self.dst_version, + "variant": self.variant, + "comment": self.comment, + "new_folder_name": self.new_folder_name, + "item_id": self.item_id, + } + + @classmethod + def from_data(cls, data): + return cls(**data) class StatusMessage: @@ -149,49 +174,17 @@ class StatusMessage: class ProjectPushItemStatus: def __init__( self, + started=False, failed=False, finished=False, fail_reason=None, - formatted_traceback=None, - messages=None, - event_system=None + full_traceback=None ): - if messages is None: - messages = [] - self._failed = failed - self._finished = finished - self._fail_reason = fail_reason - self._traceback = formatted_traceback - self._messages = messages - self._event_system = event_system - - def emit_event(self, topic, data=None): - if self._event_system is None: - return - - self._event_system.emit(topic, data or {}, "push.status") - - def get_finished(self): - """Processing of push to project finished. - - Returns: - bool: Finished. - """ - - return self._finished - - def set_finished(self, finished=True): - """Mark status as finished. - - Args: - finished (bool): Processing finished (failed or not). - """ - - if finished != self._finished: - self._finished = finished - self.emit_event("push.finished.changed", {"finished": finished}) - - finished = property(get_finished, set_finished) + self.started = started + self.failed = failed + self.finished = finished + self.fail_reason = fail_reason + self.full_traceback = full_traceback def set_failed(self, fail_reason, exc_info=None): """Set status as failed. @@ -201,8 +194,8 @@ class ProjectPushItemStatus: is set to 'True' and reason is not set. Args: - failed (bool): Push to project failed. fail_reason (str): Reason why failed. + exc_info(tuple): Exception info. """ failed = True @@ -215,84 +208,22 @@ class ProjectPushItemStatus: if not fail_reason: fail_reason = "Failed without specified reason" - if ( - self._failed == failed - and self._traceback == full_traceback - and self._fail_reason == fail_reason - ): - return + self.failed = failed + self.fail_reason = fail_reason or None + self.full_traceback = full_traceback - self._failed = failed - self._fail_reason = fail_reason or None - self._traceback = full_traceback + def to_data(self): + return { + "started": self.started, + "failed": self.failed, + "finished": self.finished, + "fail_reason": self.fail_reason, + "full_traceback": self.full_traceback, + } - self.emit_event( - "push.failed.changed", - { - "failed": failed, - "reason": fail_reason, - "traceback": full_traceback - } - ) - - @property - def failed(self): - """Processing failed. - - Returns: - bool: Processing failed. - """ - - return self._failed - - @property - def fail_reason(self): - """Reason why push to process failed. - - Returns: - Union[str, None]: Reason why push failed or None. - """ - - return self._fail_reason - - @property - def traceback(self): - """Traceback of failed process. - - Traceback is available only if unhandled exception happened. - - Returns: - Union[str, None]: Formatted traceback. - """ - - return self._traceback - - # Loggin helpers - # TODO better logging - def add_message(self, message, level): - message_obj = StatusMessage(message, level) - self._messages.append(message_obj) - self.emit_event( - "push.message.added", - {"message": message, "level": level} - ) - print(message_obj) - return message_obj - - def debug(self, message): - return self.add_message(message, "debug") - - def info(self, message): - return self.add_message(message, "info") - - def warning(self, message): - return self.add_message(message, "warning") - - def error(self, message): - return self.add_message(message, "error") - - def critical(self, message): - return self.add_message(message, "critical") + @classmethod + def from_data(cls, data): + return cls(**data) class ProjectPushRepreItem: @@ -508,22 +439,21 @@ class ProjectPushRepreItem: class ProjectPushItemProcess: """ Args: + model (IntegrateModel): Model which is processing item. item (ProjectPushItem): Item which is being processed. - item_status (ProjectPushItemStatus): Object to store status. """ # TODO where to get host?!!! host_name = "republisher" - def __init__(self, item, item_status=None): + def __init__(self, model, item): + self._model = model self._item = item - self._src_project_doc = None self._src_asset_doc = None self._src_subset_doc = None self._src_version_doc = None self._src_repre_items = None - self._src_anatomy = None self._project_doc = None self._anatomy = None @@ -539,85 +469,98 @@ class ProjectPushItemProcess: self._project_settings = None self._template_name = None - if item_status is None: - item_status = ProjectPushItemStatus() - self._status = item_status + self._status = ProjectPushItemStatus() self._operations = OperationsSession() self._file_transaction = FileTransaction() - @property - def status(self): - return self._status + self._messages = [] @property - def src_project_doc(self): - return self._src_project_doc + def item_id(self): + return self._item.item_id @property - def src_anatomy(self): - return self._src_anatomy + def started(self): + return self._status.started - @property - def src_asset_doc(self): - return self._src_asset_doc + def get_status_data(self): + return self._status.to_data() - @property - def src_subset_doc(self): - return self._src_subset_doc + def integrate(self): + self._status.started = True + try: + self._log_info("Process started") + self._fill_source_variables() + self._log_info("Source entities were found") + self._fill_destination_project() + self._log_info("Destination project was found") + self._fill_or_create_destination_asset() + self._log_info("Destination asset was determined") + self._determine_family() + self._determine_publish_template_name() + self._determine_subset_name() + self._make_sure_subset_exists() + self._make_sure_version_exists() + self._log_info("Prerequirements were prepared") + self._integrate_representations() + self._log_info("Integration finished") - @property - def src_version_doc(self): - return self._src_version_doc + except PushToProjectError as exc: + if not self._status.failed: + self._status.set_failed(str(exc)) - @property - def src_repre_items(self): - return self._src_repre_items + except Exception as exc: + _exc, _value, _tb = sys.exc_info() + self._status.set_failed( + "Unhandled error happened: {}".format(str(exc)), + (_exc, _value, _tb) + ) - @property - def project_doc(self): - return self._project_doc + finally: + self._status.finished = True + self._emit_event( + "push.finished.changed", + { + "finished": True, + "item_id": self.item_id, + } + ) - @property - def anatomy(self): - return self._anatomy + def _emit_event(self, topic, data): + self._model.emit_event(topic, data) - @property - def project_settings(self): - return self._project_settings + # Loggin helpers + # TODO better logging + def _add_message(self, message, level): + message_obj = StatusMessage(message, level) + self._messages.append(message_obj) + self._emit_event( + "push.message.added", + { + "message": message, + "level": level, + "item_id": self.item_id, + } + ) + print(message_obj) + return message_obj - @property - def asset_doc(self): - return self._asset_doc + def _log_debug(self, message): + return self._add_message(message, "debug") - @property - def task_info(self): - return self._task_info + def _log_info(self, message): + return self._add_message(message, "info") - @property - def subset_doc(self): - return self._subset_doc + def _log_warning(self, message): + return self._add_message(message, "warning") - @property - def version_doc(self): - return self._version_doc + def _log_error(self, message): + return self._add_message(message, "error") - @property - def variant(self): - return self._item.variant + def _log_critical(self, message): + return self._add_message(message, "critical") - @property - def family(self): - return self._family - - @property - def subset_name(self): - return self._subset_name - - @property - def template_name(self): - return self._template_name - - def fill_source_variables(self): + def _fill_source_variables(self): src_project_name = self._item.src_project_name src_version_id = self._item.src_version_id @@ -626,9 +569,14 @@ class ProjectPushItemProcess: self._status.set_failed( f"Source project \"{src_project_name}\" was not found" ) + + self._emit_event( + "push.failed.changed", + {"item_id": self.item_id} + ) raise PushToProjectError(self._status.fail_reason) - self._status.debug(f"Project '{src_project_name}' found") + self._log_debug(f"Project '{src_project_name}' found") version_doc = get_version_by_id(src_project_name, src_version_id) if not version_doc: @@ -666,7 +614,7 @@ class ProjectPushItemProcess: ProjectPushRepreItem(repre_doc, anatomy.roots) for repre_doc in repre_docs ] - self._status.debug(( + self._log_debug(( f"Found {len(repre_items)} representations on" f" version {src_version_id} in project '{src_project_name}'" )) @@ -677,14 +625,12 @@ class ProjectPushItemProcess: ) raise PushToProjectError(self._status.fail_reason) - self._src_anatomy = anatomy - self._src_project_doc = project_doc self._src_asset_doc = asset_doc self._src_subset_doc = subset_doc self._src_version_doc = version_doc self._src_repre_items = repre_items - def fill_destination_project(self): + def _fill_destination_project(self): # --- Destination entities --- dst_project_name = self._item.dst_project_name # Validate project existence @@ -695,7 +641,7 @@ class ProjectPushItemProcess: ) raise PushToProjectError(self._status.fail_reason) - self._status.debug( + self._log_debug( f"Destination project '{dst_project_name}' found" ) self._project_doc = dst_project_doc @@ -739,7 +685,7 @@ class ProjectPushItemProcess: )) raise PushToProjectError(self._status.fail_reason) - self._status.debug(( + self._log_debug(( f"Found already existing asset with name \"{other_name}\"" f" which match requested name \"{asset_name}\"" )) @@ -780,18 +726,18 @@ class ProjectPushItemProcess: asset_doc["type"], asset_doc ) - self._status.info( + self._log_info( f"Creating new asset with name \"{asset_name}\"" ) self._created_asset_doc = asset_doc return asset_doc - def fill_or_create_destination_asset(self): + def _fill_or_create_destination_asset(self): dst_project_name = self._item.dst_project_name - dst_asset_id = self._item.dst_asset_id + dst_folder_id = self._item.dst_folder_id dst_task_name = self._item.dst_task_name - new_asset_name = self._item.new_asset_name - if not dst_asset_id and not new_asset_name: + new_folder_name = self._item.new_folder_name + if not dst_folder_id and not new_folder_name: self._status.set_failed( "Push item does not have defined destination asset" ) @@ -799,25 +745,25 @@ class ProjectPushItemProcess: # Get asset document parent_asset_doc = None - if dst_asset_id: + if dst_folder_id: parent_asset_doc = get_asset_by_id( - self._item.dst_project_name, self._item.dst_asset_id + self._item.dst_project_name, self._item.dst_folder_id ) if not parent_asset_doc: self._status.set_failed( - f"Could find asset with id \"{dst_asset_id}\"" + f"Could find asset with id \"{dst_folder_id}\"" f" in project \"{dst_project_name}\"" ) raise PushToProjectError(self._status.fail_reason) - if not new_asset_name: + if not new_folder_name: asset_doc = parent_asset_doc else: asset_doc = self._create_asset( - self.src_asset_doc, - self.project_doc, + self._src_asset_doc, + self._project_doc, parent_asset_doc, - new_asset_name + new_folder_name ) self._asset_doc = asset_doc if not dst_task_name: @@ -842,12 +788,13 @@ class ProjectPushItemProcess: task_info["name"] = dst_task_name # Fill rest of task information based on task type task_type = task_info["type"] - task_type_info = self.project_doc["config"]["tasks"].get(task_type, {}) + task_type_info = self._project_doc["config"]["tasks"].get( + task_type, {}) task_info.update(task_type_info) self._task_info = task_info - def determine_family(self): - subset_doc = self.src_subset_doc + def _determine_family(self): + subset_doc = self._src_subset_doc family = subset_doc["data"].get("family") families = subset_doc["data"].get("families") if not family and families: @@ -859,48 +806,48 @@ class ProjectPushItemProcess: ) raise PushToProjectError(self._status.fail_reason) - self._status.debug( + self._log_debug( f"Publishing family is '{family}' (Based on source subset)" ) self._family = family - def determine_publish_template_name(self): + def _determine_publish_template_name(self): template_name = get_publish_template_name( self._item.dst_project_name, self.host_name, - self.family, - self.task_info.get("name"), - self.task_info.get("type"), - project_settings=self.project_settings + self._family, + self._task_info.get("name"), + self._task_info.get("type"), + project_settings=self._project_settings ) - self._status.debug( + self._log_debug( f"Using template '{template_name}' for integration" ) self._template_name = template_name - def determine_subset_name(self): - family = self.family - asset_doc = self.asset_doc - task_info = self.task_info + def _determine_subset_name(self): + family = self._family + asset_doc = self._asset_doc + task_info = self._task_info subset_name = get_subset_name( family, - self.variant, + self._item.variant, task_info.get("name"), asset_doc, project_name=self._item.dst_project_name, host_name=self.host_name, - project_settings=self.project_settings + project_settings=self._project_settings ) - self._status.info( + self._log_info( f"Push will be integrating to subset with name '{subset_name}'" ) self._subset_name = subset_name - def make_sure_subset_exists(self): + def _make_sure_subset_exists(self): project_name = self._item.dst_project_name - asset_id = self.asset_doc["_id"] - subset_name = self.subset_name - family = self.family + asset_id = self._asset_doc["_id"] + subset_name = self._subset_name + family = self._family subset_doc = get_subset_by_name(project_name, subset_name, asset_id) if subset_doc: self._subset_doc = subset_doc @@ -915,13 +862,13 @@ class ProjectPushItemProcess: self._operations.create_entity(project_name, "subset", subset_doc) self._subset_doc = subset_doc - def make_sure_version_exists(self): + def _make_sure_version_exists(self): """Make sure version document exits in database.""" project_name = self._item.dst_project_name version = self._item.dst_version - src_version_doc = self.src_version_doc - subset_doc = self.subset_doc + src_version_doc = self._src_version_doc + subset_doc = self._subset_doc subset_id = subset_doc["_id"] src_data = src_version_doc["data"] families = subset_doc["data"].get("families") @@ -947,8 +894,8 @@ class ProjectPushItemProcess: version = get_versioning_start( project_name, self.host_name, - task_name=self.task_info["name"], - task_type=self.task_info["type"], + task_name=self._task_info["name"], + task_type=self._task_info["type"], family=families[0], subset=subset_doc["name"] ) @@ -982,16 +929,16 @@ class ProjectPushItemProcess: self._version_doc = version_doc - def integrate_representations(self): + def _integrate_representations(self): try: - self._integrate_representations() + self._real_integrate_representations() except Exception: self._operations.clear() self._file_transaction.rollback() raise - def _integrate_representations(self): - version_doc = self.version_doc + def _real_integrate_representations(self): + version_doc = self._version_doc version_id = version_doc["_id"] existing_repres = get_representations( self._item.dst_project_name, @@ -1001,17 +948,17 @@ class ProjectPushItemProcess: repre_doc["name"].lower(): repre_doc for repre_doc in existing_repres } - template_name = self.template_name - anatomy = self.anatomy + template_name = self._template_name + anatomy = self._anatomy formatting_data = get_template_data( - self.project_doc, - self.asset_doc, - self.task_info.get("name"), + self._project_doc, + self._asset_doc, + self._task_info.get("name"), self.host_name ) formatting_data.update({ - "subset": self.subset_name, - "family": self.family, + "subset": self._subset_name, + "family": self._family, "version": version_doc["name"] }) @@ -1021,19 +968,19 @@ class ProjectPushItemProcess: file_template = StringTemplate( anatomy.templates[template_name]["file"] ) - self._status.info("Preparing files to transfer") + self._log_info("Preparing files to transfer") processed_repre_items = self._prepare_file_transactions( anatomy, template_name, formatting_data, file_template ) self._file_transaction.process() - self._status.info("Preparing database changes") + self._log_info("Preparing database changes") self._prepare_database_operations( version_id, processed_repre_items, path_template, existing_repres_by_low_name ) - self._status.info("Finalization") + self._log_info("Finalization") self._operations.commit() self._file_transaction.finalize() @@ -1041,7 +988,7 @@ class ProjectPushItemProcess: self, anatomy, template_name, formatting_data, file_template ): processed_repre_items = [] - for repre_item in self.src_repre_items: + for repre_item in self._src_repre_items: repre_doc = repre_item.repre_doc repre_name = repre_doc["name"] repre_format_data = copy.deepcopy(formatting_data) @@ -1050,6 +997,9 @@ class ProjectPushItemProcess: ext = os.path.splitext(src_file.path)[-1] repre_format_data["ext"] = ext[1:] break + repre_output_name = repre_doc["context"].get("output") + if repre_output_name is not None: + repre_format_data["output"] = repre_output_name template_obj = anatomy.templates_obj[template_name]["folder"] folder_path = template_obj.format_strict(formatting_data) @@ -1177,34 +1127,86 @@ class ProjectPushItemProcess: {"type": "archived_representation"} ) - def process(self): - try: - self._status.info("Process started") - self.fill_source_variables() - self._status.info("Source entities were found") - self.fill_destination_project() - self._status.info("Destination project was found") - self.fill_or_create_destination_asset() - self._status.info("Destination asset was determined") - self.determine_family() - self.determine_publish_template_name() - self.determine_subset_name() - self.make_sure_subset_exists() - self.make_sure_version_exists() - self._status.info("Prerequirements were prepared") - self.integrate_representations() - self._status.info("Integration finished") - except PushToProjectError as exc: - if not self._status.failed: - self._status.set_failed(str(exc)) +class IntegrateModel: + def __init__(self, controller): + self._controller = controller + self._process_items = {} - except Exception as exc: - _exc, _value, _tb = sys.exc_info() - self._status.set_failed( - "Unhandled error happened: {}".format(str(exc)), - (_exc, _value, _tb) - ) + def reset(self): + self._process_items = {} - finally: - self._status.set_finished() + def emit_event(self, topic, data=None, source=None): + self._controller.emit_event(topic, data, source) + + def create_process_item( + self, + src_project_name, + src_version_id, + dst_project_name, + dst_folder_id, + dst_task_name, + variant, + comment, + new_folder_name, + dst_version, + ): + """Create new item for integration. + + Args: + src_project_name (str): Source project name. + src_version_id (str): Source version id. + dst_project_name (str): Destination project name. + dst_folder_id (str): Destination folder id. + dst_task_name (str): Destination task name. + variant (str): Variant name. + comment (Union[str, None]): Comment. + new_folder_name (Union[str, None]): New folder name. + dst_version (int): Destination version number. + + Returns: + str: Item id. The id can be used to trigger integration or get + status information. + """ + + item = ProjectPushItem( + src_project_name, + src_version_id, + dst_project_name, + dst_folder_id, + dst_task_name, + variant, + comment=comment, + new_folder_name=new_folder_name, + dst_version=dst_version + ) + process_item = ProjectPushItemProcess(self, item) + self._process_items[item.item_id] = process_item + return item.item_id + + def integrate_item(self, item_id): + """Start integration of item. + + Args: + item_id (str): Item id which should be integrated. + """ + + item = self._process_items.get(item_id) + if item is None or item.started: + return + item.integrate() + + def get_item_status(self, item_id): + """Status of an item. + + Args: + item_id (str): Item id for which status should be returned. + + Returns: + dict[str, Any]: Status data. + """ + + item = self._process_items.get(item_id) + if item is not None: + return item.get_status_data() + return None diff --git a/openpype/tools/ayon_push_to_project/ui/window.py b/openpype/tools/ayon_push_to_project/ui/window.py index 57c4c2619f..535c01c643 100644 --- a/openpype/tools/ayon_push_to_project/ui/window.py +++ b/openpype/tools/ayon_push_to_project/ui/window.py @@ -231,7 +231,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget): self._main_thread_timer = main_thread_timer self._main_thread_timer_can_stop = True self._last_submit_message = None - self._process_item = None + self._process_item_id = None self._variant_is_valid = None self._folder_is_valid = None @@ -380,10 +380,10 @@ class PushToContextSelectWindow(QtWidgets.QWidget): self.close() def _on_select_click(self): - self._process_item = self._controller.submit(wait=False) + self._process_item_id = self._controller.submit(wait=False) def _on_try_again_click(self): - self._process_item = None + self._process_item_id = None self._last_submit_message = None self._overlay_close_btn.setVisible(False) @@ -395,9 +395,11 @@ class PushToContextSelectWindow(QtWidgets.QWidget): self._overlay_label.setText(self._last_submit_message) self._last_submit_message = None - process_status = self._process_item.status - push_failed = process_status.failed - fail_traceback = process_status.traceback + process_status = self._controller.get_process_item_status( + self._process_item_id + ) + push_failed = process_status["failed"] + fail_traceback = process_status["full_traceback"] if self._main_thread_timer_can_stop: self._main_thread_timer.stop() self._overlay_close_btn.setVisible(True) @@ -405,7 +407,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget): self._overlay_try_btn.setVisible(True) if push_failed: - message = "Push Failed:\n{}".format(process_status.fail_reason) + message = "Push Failed:\n{}".format(process_status["fail_reason"]) if fail_traceback: message += "\n{}".format(fail_traceback) self._overlay_label.setText(message) @@ -415,7 +417,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget): # Join thread in controller self._controller.wait_for_process_thread() # Reset process item to None - self._process_item = None + self._process_item_id = None def _on_controller_submit_start(self): self._main_thread_timer_can_stop = False From 74b73648180b06966fbd44dc7fef995f073c080c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 16 Oct 2023 12:24:18 +0200 Subject: [PATCH 0452/1233] fix re-use of 'output' for representation --- openpype/tools/ayon_push_to_project/models/integrate.py | 2 ++ openpype/tools/push_to_project/control_integrate.py | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/openpype/tools/ayon_push_to_project/models/integrate.py b/openpype/tools/ayon_push_to_project/models/integrate.py index b3de69c79a..976d8cb4f0 100644 --- a/openpype/tools/ayon_push_to_project/models/integrate.py +++ b/openpype/tools/ayon_push_to_project/models/integrate.py @@ -997,6 +997,8 @@ class ProjectPushItemProcess: ext = os.path.splitext(src_file.path)[-1] repre_format_data["ext"] = ext[1:] break + + # Re-use 'output' from source representation repre_output_name = repre_doc["context"].get("output") if repre_output_name is not None: repre_format_data["output"] = repre_output_name diff --git a/openpype/tools/push_to_project/control_integrate.py b/openpype/tools/push_to_project/control_integrate.py index a822339ccf..9f083d8eb7 100644 --- a/openpype/tools/push_to_project/control_integrate.py +++ b/openpype/tools/push_to_project/control_integrate.py @@ -1051,6 +1051,11 @@ class ProjectPushItemProcess: repre_format_data["ext"] = ext[1:] break + # Re-use 'output' from source representation + repre_output_name = repre_doc["context"].get("output") + if repre_output_name is not None: + repre_format_data["output"] = repre_output_name + template_obj = anatomy.templates_obj[template_name]["folder"] folder_path = template_obj.format_strict(formatting_data) repre_context = folder_path.used_values From 38883e4bddd699db3edd33b393adf0da347b5ffd Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 16 Oct 2023 14:04:16 +0200 Subject: [PATCH 0453/1233] removed unnecessary imports --- .../tools/ayon_push_to_project/models/__init__.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/openpype/tools/ayon_push_to_project/models/__init__.py b/openpype/tools/ayon_push_to_project/models/__init__.py index 5f909437a7..99355b4296 100644 --- a/openpype/tools/ayon_push_to_project/models/__init__.py +++ b/openpype/tools/ayon_push_to_project/models/__init__.py @@ -1,20 +1,10 @@ from .selection import PushToProjectSelectionModel from .user_values import UserPublishValuesModel -from .integrate import ( - ProjectPushItem, - ProjectPushItemProcess, - ProjectPushItemStatus, - IntegrateModel, -) +from .integrate import IntegrateModel __all__ = ( "PushToProjectSelectionModel", - "UserPublishValuesModel", - - "ProjectPushItem", - "ProjectPushItemProcess", - "ProjectPushItemStatus", "IntegrateModel", ) From c0ab1c368e281ba21b2b9d1133475055524220b5 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Thu, 19 Oct 2023 13:00:16 +0000 Subject: [PATCH 0454/1233] [Automated] Release --- CHANGELOG.md | 373 ++++++++++++++++++++++++++++++++++++++++++++ openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 375 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d5cf2c4d2..58428ab4d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,379 @@ # Changelog +## [3.17.3](https://github.com/ynput/OpenPype/tree/3.17.3) + + +[Full Changelog](https://github.com/ynput/OpenPype/compare/3.17.2...3.17.3) + +### **🆕 New features** + + +

+Maya: Multi-shot Layout Creator #5710 + +New Multi-shot Layout creator is a way of automating creation of the new Layout instances in Maya, associated with correct shots, frame ranges and Camera Sequencer in Maya. + + +___ + +
+ + +
+Colorspace: ociolook file product type workflow #5541 + +Traypublisher support for publishing of colorspace look files (ociolook) which are json files holding any LUT files. This new product is available for loading in Nuke host at the moment.Added colorspace selector to publisher attribute with better labeling. We are supporting also Roles and Alias (only v2 configs). + + +___ + +
+ + +
+Scene Inventory tool: Refactor Scene Inventory tool (for AYON) #5758 + +Modified scene inventory tool for AYON. The main difference is in how project name is defined and replacement of assets combobox with folders dialog. + + +___ + +
+ + +
+AYON: Support dev bundles #5783 + +Modules can be loaded in AYON dev mode from different location. + + +___ + +
+ +### **🚀 Enhancements** + + +
+Testing: Ingest Maya userSetup #5734 + +Suggesting to ingest `userSetup.py` startup script for easier collaboration and transparency of testing. + + +___ + +
+ + +
+Fusion: Work with pathmaps #5329 + +Path maps are a big part of our Fusion workflow. We map the project folder to a path map within Fusion so all loaders and savers point to the path map variable. This way any computer on any OS can open any comp no matter where the project folder is located. + + +___ + +
+ + +
+Maya: Add Maya 2024 and remove pre 2022. #5674 + +Adding Maya 2024 as default application variant.Removing Maya 2020 and older, as these are not supported anymore. + + +___ + +
+ + +
+Enhancement: Houdini: Allow using template keys in Houdini shelves manager #5727 + +Allow using Template keys in Houdini shelves manager. + + +___ + +
+ + +
+Houdini: Fix Show in usdview loader action #5737 + +Fix the "Show in USD View" loader to show up in Houdini + + +___ + +
+ + +
+Nuke: validator of asset context with repair actions #5749 + +Instance nodes with different context of asset and task can be now validated and repaired via repair action. + + +___ + +
+ + +
+AYON: Tools enhancements #5753 + +Few enhancements and tweaks of AYON related tools. + + +___ + +
+ + +
+Max: Tweaks on ValidateMaxContents #5759 + +This PR provides enhancements on ValidateMaxContent as follow: +- Rename `ValidateMaxContents` to `ValidateContainers` +- Add related families which are required to pass the validation(All families except `Render` as the render instance is the one which only allows empty container) + + +___ + +
+ + +
+Enhancement: Nuke refactor `SelectInvalidAction` #5762 + +Refactor `SelectInvalidAction` to behave like other action for other host, create `SelectInstanceNodeAction` as dedicated action to select the instance node for a failed plugin. +- Note: Selecting Instance Node will still select the instance node even if the user has currently 'fixed' the problem. + + +___ + +
+ + +
+Enhancement: Tweak logging for Nuke for artist facing reports #5763 + +Tweak logs that are not artist-facing to debug level + in some cases clarify what the logged value is. + + +___ + +
+ + +
+AYON Settings: Disk mapping #5786 + +Added disk mapping settings to core addon settings. + + +___ + +
+ +### **🐛 Bug fixes** + + +
+Maya: add colorspace argument to redshiftTextureProcessor #5645 + +In color managed Maya, texture processing during Look Extraction wasn't passing texture colorspaces set on textures to `redshiftTextureProcessor` tool. This in effect caused this tool to produce non-zero exit code (even though the texture was converted into wrong colorspace) and therefor crash of the extractor. This PR is passing colorspace to that tool if color management is enabled. + + +___ + +
+ + +
+Maya: don't call `cmds.ogs()` in headless mode #5769 + +`cmds.ogs()` is a call that will crash if Maya is running in headless mode (mayabatch, mayapy). This is handling that case. + + +___ + +
+ + +
+Resolve: inventory management fix #5673 + +Loaded Timeline item containers are now updating correctly and version management is working as it suppose to. +- [x] updating loaded timeline items +- [x] Removing of loaded timeline items + + +___ + +
+ + +
+Blender: Remove 'update_hierarchy' #5756 + +Remove `update_hierarchy` function which is causing crashes in scene inventory tool. + + +___ + +
+ + +
+Max: bug fix on the settings in pointcloud family #5768 + +Bug fix on the settings being errored out in validate point cloud(see links:https://github.com/ynput/OpenPype/pull/5759#pullrequestreview-1676681705) and passibly in point cloud extractor. + + +___ + +
+ + +
+AYON settings: Fix default factory of tools #5773 + +Fix default factory of application tools. + + +___ + +
+ + +
+Fusion: added missing OPENPYPE_VERSION #5776 + +Fusion submission to Deadline was missing OPENPYPE_VERSION env var when submitting from build (not source code directly). This missing env var might break rendering on DL if path to OP executable (openpype_console.exe) is not set explicitly and might cause an issue when different versions of OP are deployed.This PR adds this environment variable. + + +___ + +
+ + +
+Ftrack: Skip tasks when looking for asset equivalent entity #5777 + +Skip tasks when looking for asset equivalent entity. + + +___ + +
+ + +
+Nuke: loading gizmos fixes #5779 + +Gizmo product is not offered in Loader as plugin. It is also updating as expected. + + +___ + +
+ + +
+General: thumbnail extractor as last extractor #5780 + +Fixing issue with the order of the `ExtractOIIOTranscode` and `ExtractThumbnail` plugins. The problem was that the `ExtractThumbnail` plugin was processed before the `ExtractOIIOTranscode` plugin. As a result, the `ExtractThumbnail` plugin did not inherit the `review` tag into the representation data. This caused the `ExtractThumbnail` plugin to fail in processing and creating thumbnails. + + +___ + +
+ + +
+Bug: fix key in application json #5787 + +In PR #5705 `maya` was wrongly used instead of `mayapy`, breaking AYON defaults in AYON Application Addon. + + +___ + +
+ + +
+'NumberAttrWidget' shows 'Multiselection' label on multiselection #5792 + +Attribute definition widget 'NumberAttrWidget' shows `< Multiselection >` label on multiselection. + + +___ + +
+ + +
+Publisher: Selection change by enabled checkbox on instance update attributes #5793 + +Change of instance by clicking on enabled checkbox will actually update attributes on right side to match the selection. + + +___ + +
+ + +
+Houdini: Remove `setParms` call since it's responsibility of `self.imprint` to set the values #5796 + +Revert a recent change made in #5621 due to this comment. However the change is faulty as can be seen mentioned here + + +___ + +
+ + +
+AYON loader: Fix SubsetLoader functionality #5799 + +Fix SubsetLoader plugin processing in AYON loader tool. + + +___ + +
+ +### **Merged pull requests** + + +
+Houdini: Add self publish button #5621 + +This PR allows single publishing by adding a publish button to created rop nodes in HoudiniAdmins are much welcomed to enable it from houdini general settingsPublish Button also includes all input publish instances. in this screen shot the alembic instance is ignored because the switch is turned off + + +___ + +
+ + +
+Nuke: fixing UNC support for OCIO path #5771 + +UNC paths were broken on windows for custom OCIO path and this is solving the issue with removed double slash at start of path + + +___ + +
+ + + + ## [3.17.2](https://github.com/ynput/OpenPype/tree/3.17.2) diff --git a/openpype/version.py b/openpype/version.py index 6f740d0c78..ec09c45abb 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.17.3-nightly.2" +__version__ = "3.17.3" diff --git a/pyproject.toml b/pyproject.toml index ad93b70c0f..3803e4714e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.17.2" # OpenPype +version = "3.17.3" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 2962b0ae436f4d75f99bfe2c9f6f372e33effc4b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 19 Oct 2023 13:01:19 +0000 Subject: [PATCH 0455/1233] 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 2849a4951a..d63d05f477 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.17.3 - 3.17.3-nightly.2 - 3.17.3-nightly.1 - 3.17.2 @@ -134,7 +135,6 @@ body: - 3.15.0-nightly.1 - 3.14.11-nightly.4 - 3.14.11-nightly.3 - - 3.14.11-nightly.2 validations: required: true - type: dropdown From 977d0144d83f5ae3f0f49a4052db022c4cdd6024 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 19 Oct 2023 21:24:50 +0800 Subject: [PATCH 0456/1233] move render resolution function to lib --- openpype/hosts/max/api/lib.py | 20 +++++++++++++++++++ openpype/hosts/max/api/preview_animation.py | 22 +-------------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 166a66ce48..e6b669f82f 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -483,3 +483,23 @@ def get_plugins() -> list: plugin_info_list.append(plugin_info) return plugin_info_list + + +@contextlib.contextmanager +def render_resolution(width, height): + """Function to set render resolution option during + context + + Args: + width (int): render width + height (int): render height + """ + current_renderWidth = rt.renderWidth + current_renderHeight = rt.renderHeight + try: + rt.renderWidth = width + rt.renderHeight = height + yield + finally: + rt.renderWidth = current_renderWidth + rt.renderHeight = current_renderHeight diff --git a/openpype/hosts/max/api/preview_animation.py b/openpype/hosts/max/api/preview_animation.py index 601ff65c81..3d66d278f0 100644 --- a/openpype/hosts/max/api/preview_animation.py +++ b/openpype/hosts/max/api/preview_animation.py @@ -2,7 +2,7 @@ import os import logging import contextlib from pymxs import runtime as rt -from .lib import get_max_version +from .lib import get_max_version, render_resolution log = logging.getLogger("openpype.hosts.max") @@ -24,26 +24,6 @@ def play_preview_when_done(has_autoplay): rt.preferences.playPreviewWhenDone = current_playback -@contextlib.contextmanager -def render_resolution(width, height): - """Function to set render resolution option during - context - - Args: - width (int): render width - height (int): render height - """ - current_renderWidth = rt.renderWidth - current_renderHeight = rt.renderHeight - try: - rt.renderWidth = width - rt.renderHeight = height - yield - finally: - rt.renderWidth = current_renderWidth - rt.renderHeight = current_renderHeight - - @contextlib.contextmanager def viewport_camera(camera): """Function to set viewport camera during context From f674b7b10835a1bd75ad91f76082f14afd4a9f20 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 19 Oct 2023 21:35:33 +0800 Subject: [PATCH 0457/1233] hound --- openpype/hosts/max/api/lib.py | 1 - openpype/hosts/max/api/preview_animation.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index e6b669f82f..5a54abd141 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- """Library of functions useful for 3dsmax pipeline.""" -import os import contextlib import logging import json diff --git a/openpype/hosts/max/api/preview_animation.py b/openpype/hosts/max/api/preview_animation.py index 3d66d278f0..caa4f60475 100644 --- a/openpype/hosts/max/api/preview_animation.py +++ b/openpype/hosts/max/api/preview_animation.py @@ -287,7 +287,7 @@ def viewport_options_for_preview_animation(): "dspBones": False, "dspBkg": True, "dspGrid": False, - "dspSafeFrame":False, + "dspSafeFrame": False, "dspFrameNums": False } else: From 71e9d6cc13c1b147abd67213d4d1ae234842a1f8 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 19 Oct 2023 22:53:56 +0800 Subject: [PATCH 0458/1233] rename render_preview_animation --- openpype/hosts/max/api/preview_animation.py | 2 +- .../hosts/max/plugins/publish/extract_review_animation.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/max/api/preview_animation.py b/openpype/hosts/max/api/preview_animation.py index caa4f60475..260d18893e 100644 --- a/openpype/hosts/max/api/preview_animation.py +++ b/openpype/hosts/max/api/preview_animation.py @@ -213,7 +213,7 @@ def publish_preview_sequences(staging_dir, filename, rt.gc(delayed=True) -def publish_preview_animation( +def render_preview_animation( instance, staging_dir, ext, review_camera, startFrame=None, endFrame=None, diff --git a/openpype/hosts/max/plugins/publish/extract_review_animation.py b/openpype/hosts/max/plugins/publish/extract_review_animation.py index c308aadfdb..979cbc828c 100644 --- a/openpype/hosts/max/plugins/publish/extract_review_animation.py +++ b/openpype/hosts/max/plugins/publish/extract_review_animation.py @@ -2,7 +2,7 @@ import os import pyblish.api from openpype.pipeline import publish from openpype.hosts.max.api.preview_animation import ( - publish_preview_animation + render_preview_animation ) @@ -34,7 +34,7 @@ class ExtractReviewAnimation(publish.Extractor): review_camera = instance.data["review_camera"] viewport_options = instance.data.get("viewport_options", {}) resolution = instance.data.get("resolution", ()) - publish_preview_animation( + render_preview_animation( instance, staging_dir, ext, review_camera, startFrame=start, endFrame=end, From e24140715b386e34bbefdf6350d6f75c0c388e8e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 20 Oct 2023 00:30:24 +0800 Subject: [PATCH 0459/1233] clean up code for preview animation --- openpype/hosts/max/api/preview_animation.py | 157 +++++++++--------- .../publish/extract_review_animation.py | 26 ++- 2 files changed, 90 insertions(+), 93 deletions(-) diff --git a/openpype/hosts/max/api/preview_animation.py b/openpype/hosts/max/api/preview_animation.py index 260d18893e..171d335ba4 100644 --- a/openpype/hosts/max/api/preview_animation.py +++ b/openpype/hosts/max/api/preview_animation.py @@ -92,96 +92,92 @@ def viewport_preference_setting(general_viewport, setattr(viewport_setting, key, value) -def publish_review_animation(instance, staging_dir, start, - end, ext, fps, viewport_options): +def _render_preview_animation_max_2024( + filepath, start, end, ext, viewport_options): """Function to set up preview arguments in MaxScript. ****For 3dsMax 2024+ - Args: - instance (str): instance - filepath (str): output of the preview animation + filepath (str): filepath for render output without frame number and + extension, for example: /path/to/file start (int): startFrame end (int): endFrame - fps (float): fps value - viewport_options (dict): viewport setting options - + viewport_options (dict): viewport setting options, e.g. + {"vpStyle": "defaultshading", "vpPreset": "highquality"} Returns: - list: job arguments + list: Created files """ - job_args = list() - filename = "{0}..{1}".format(instance.name, ext) - filepath = os.path.join(staging_dir, filename) filepath = filepath.replace("\\", "/") + filepath = f"{filepath}..{ext}" + frame_template = f"{filepath}.{{:04d}}.{ext}" + job_args = list() default_option = f'CreatePreview filename:"{filepath}"' job_args.append(default_option) - frame_option = f"outputAVI:false start:{start} end:{end} fps:{fps}" # noqa + frame_option = f"outputAVI:false start:{start} end:{end}" job_args.append(frame_option) - for key, value in viewport_options.items(): if isinstance(value, bool): if value: job_args.append(f"{key}:{value}") - elif isinstance(value, str): if key == "vpStyle": - if viewport_options[key] == "Realistic": + if value == "Realistic": value = "defaultshading" - elif viewport_options[key] == "Shaded": + elif value == "Shaded": log.warning( "'Shaded' Mode not supported in " - "preview animation in Max 2024..\n" - "Using 'defaultshading' instead") + "preview animation in Max 2024.\n" + "Using 'defaultshading' instead.") value = "defaultshading" - elif viewport_options[key] == "ConsistentColors": + elif value == "ConsistentColors": value = "flatcolor" else: value = value.lower() elif key == "vpPreset": - if viewport_options[key] == "Quality": + if value == "Quality": value = "highquality" - elif viewport_options[key] == "Customize": + elif value == "Customize": value = "userdefined" else: value = value.lower() job_args.append(f"{key}: #{value}") - auto_play_option = "autoPlay:false" job_args.append(auto_play_option) - job_str = " ".join(job_args) log.debug(job_str) - - return job_str + rt.completeRedraw() + rt.execute(job_str) + # Return the created files + return [frame_template.format(frame) for frame in range(start, end + 1)] -def publish_preview_sequences(staging_dir, filename, - startFrame, endFrame, - percentSize, ext): - """publish preview animation by creating bitmaps +def _render_preview_animation_max_pre_2024( + filepath, startFrame, endFrame, percentSize, ext): + """Render viewport animation by creating bitmaps ***For 3dsMax Version <2024 - Args: - staging_dir (str): staging directory - filename (str): filename + filepath (str): filepath without frame numbers and extension startFrame (int): start frame endFrame (int): end frame percentSize (int): percentage of the resolution ext (str): image extension + Returns: + list: Created filepaths """ # get the screenshot resolution_percentage = float(percentSize) / 100 res_width = rt.renderWidth * resolution_percentage res_height = rt.renderHeight * resolution_percentage - viewportRatio = float(res_width / res_height) - - for i in range(startFrame, endFrame + 1): - rt.sliderTime = i - fname = "{}.{:04}.{}".format(filename, i, ext) - filepath = os.path.join(staging_dir, fname) - filepath = filepath.replace("\\", "/") + frame_template = "{}.{{:04}}.{}".format(filepath, ext) + frame_template.replace("\\", "/") + files = [] + user_cancelled = False + for frame in range(startFrame, endFrame + 1): + rt.sliderTime = frame + filepath = frame_template.format(frame) preview_res = rt.bitmap( - res_width, res_height, filename=filepath) + res_width, res_height, filename=filepath + ) dib = rt.gw.getViewportDib() dib_width = float(dib.width) dib_height = float(dib.height) @@ -197,71 +193,78 @@ def publish_preview_sequences(staging_dir, filename, tempImage_bmp = rt.bitmap(widthCrop, dib_height) src_box_value = rt.Box2(0, leftEdge, dib_width, dib_height) rt.pasteBitmap(dib, tempImage_bmp, src_box_value, rt.Point2(0, 0)) - # copy the bitmap and close it rt.copy(tempImage_bmp, preview_res) rt.close(tempImage_bmp) - rt.save(preview_res) rt.close(preview_res) - rt.close(dib) - - if rt.keyboard.escPressed: - rt.exit() + files.append(filepath) # clean up the cache + if rt.keyboard.escPressed: + user_cancelled = True + break rt.gc(delayed=True) + if user_cancelled: + raise RuntimeError("User cancelled rendering of viewport animation.") + return files def render_preview_animation( - instance, staging_dir, - ext, review_camera, - startFrame=None, endFrame=None, - resolution=None, + filepath, + ext, + review_camera, + start_frame=None, + end_frame=None, + width=1920, + height=1080, viewport_options=None): """Render camera review animation - Args: - instance (pyblish.api.instance): Instance - filepath (str): filepath + filepath (str): filepath to render to, without frame number and + extension + ext (str): output file extension review_camera (str): viewport camera for preview render - startFrame (int): start frame - endFrame (int): end frame + start_frame (int): start frame + end_frame (int): end frame + width (int): render resolution width + height (int): render resolution height viewport_options (dict): viewport setting options + Returns: + list: Rendered output files """ + if start_frame is None: + start_frame = int(rt.animationRange.start) + if end_frame is None: + end_frame = int(rt.animationRange.end) - if startFrame is None: - startFrame = int(rt.animationRange.start) - if endFrame is None: - endFrame = int(rt.animationRange.end) if viewport_options is None: viewport_options = viewport_options_for_preview_animation() - if resolution is None: - resolution = (1920, 1080) with play_preview_when_done(False): with viewport_camera(review_camera): - width, height = resolution with render_resolution(width, height): if int(get_max_version()) < 2024: with viewport_preference_setting( viewport_options["general_viewport"], viewport_options["nitrous_viewport"], - viewport_options["vp_btn_mgr"]): + viewport_options["vp_btn_mgr"] + ): percentSize = viewport_options.get("percentSize", 100) - - publish_preview_sequences( - staging_dir, instance.name, - startFrame, endFrame, percentSize, ext) + return _render_preview_animation_max_pre_2024( + filepath, + start_frame, + end_frame, + percentSize, + ext + ) else: - fps = instance.data["fps"] - rt.completeRedraw() - preview_arg = publish_review_animation( - instance, staging_dir, - startFrame, endFrame, - ext, fps, viewport_options) - rt.execute(preview_arg) - - rt.completeRedraw() + return _render_preview_animation_max_2024( + filepath, + start_frame, + end_frame, + ext, + viewport_options + ) def viewport_options_for_preview_animation(): diff --git a/openpype/hosts/max/plugins/publish/extract_review_animation.py b/openpype/hosts/max/plugins/publish/extract_review_animation.py index 979cbc828c..df1f2b4182 100644 --- a/openpype/hosts/max/plugins/publish/extract_review_animation.py +++ b/openpype/hosts/max/plugins/publish/extract_review_animation.py @@ -24,8 +24,6 @@ class ExtractReviewAnimation(publish.Extractor): end = int(instance.data["frameEnd"]) filepath = os.path.join(staging_dir, filename) filepath = filepath.replace("\\", "/") - filenames = self.get_files( - instance.name, start, end, ext) self.log.debug( "Writing Review Animation to" @@ -34,13 +32,18 @@ class ExtractReviewAnimation(publish.Extractor): review_camera = instance.data["review_camera"] viewport_options = instance.data.get("viewport_options", {}) resolution = instance.data.get("resolution", ()) - render_preview_animation( - instance, staging_dir, - ext, review_camera, - startFrame=start, endFrame=end, - resolution=resolution, + files = render_preview_animation( + os.path.join(staging_dir, instance.name), + ext, + review_camera, + start, + end, + width=resolution[0], + height=resolution[1], viewport_options=viewport_options) + filenames = [os.path.basename(path) for path in files] + tags = ["review"] if not instance.data.get("keepImages"): tags.append("delete") @@ -63,12 +66,3 @@ class ExtractReviewAnimation(publish.Extractor): if "representations" not in instance.data: instance.data["representations"] = [] instance.data["representations"].append(representation) - - def get_files(self, filename, start, end, ext): - file_list = [] - for frame in range(int(start), int(end) + 1): - actual_name = "{}.{:04}.{}".format( - filename, frame, ext) - file_list.append(actual_name) - - return file_list From 4c390a62391c65ec9dd8e403686708e5ee1d3ebd Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 20 Oct 2023 00:32:27 +0800 Subject: [PATCH 0460/1233] hound --- openpype/hosts/max/api/preview_animation.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/max/api/preview_animation.py b/openpype/hosts/max/api/preview_animation.py index 171d335ba4..1d7211443a 100644 --- a/openpype/hosts/max/api/preview_animation.py +++ b/openpype/hosts/max/api/preview_animation.py @@ -1,4 +1,3 @@ -import os import logging import contextlib from pymxs import runtime as rt From 25311caace08d2b663cd07293bb8266d88685ea3 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Thu, 19 Oct 2023 19:42:17 +0300 Subject: [PATCH 0461/1233] add repait action to turn off use handles for the failed instance --- .../plugins/publish/validate_frame_range.py | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_frame_range.py b/openpype/hosts/houdini/plugins/publish/validate_frame_range.py index b35ab62002..343cf3c5e4 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_frame_range.py +++ b/openpype/hosts/houdini/plugins/publish/validate_frame_range.py @@ -1,11 +1,17 @@ # -*- coding: utf-8 -*- import pyblish.api from openpype.pipeline import PublishValidationError +from openpype.pipeline.publish import RepairAction from openpype.hosts.houdini.api.action import SelectInvalidAction import hou +class DisableUseAssetHandlesAction(RepairAction): + label = "Disable use asset handles" + icon = "mdi.toggle-switch-off" + + class ValidateFrameRange(pyblish.api.InstancePlugin): """Validate Frame Range. @@ -17,7 +23,7 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): order = pyblish.api.ValidatorOrder - 0.1 hosts = ["houdini"] label = "Validate Frame Range" - actions = [SelectInvalidAction] + actions = [DisableUseAssetHandlesAction, SelectInvalidAction] def process(self, instance): @@ -60,3 +66,32 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): .format(instance.data) ) return [rop_node] + + @classmethod + def repair(cls, instance): + + if not cls.get_invalid(instance): + # Already fixed + print ("Not working") + return + + # Disable use asset handles + context = instance.context + create_context = context.data["create_context"] + instance_id = instance.data.get("instance_id") + if not instance_id: + cls.log.debug("'{}' must have instance id" + .format(instance)) + return + + created_instance = create_context.get_instance_by_id(instance_id) + if not instance_id: + cls.log.debug("Unable to find instance '{}' by id" + .format(instance)) + return + + created_instance.publish_attributes["CollectRopFrameRange"]["use_handles"] = False # noqa + + create_context.save_changes() + cls.log.debug("use asset handles is turned off for '{}'" + .format(instance)) From 82edc833390d686fcaf6a8827615a22034c0d3fa Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Thu, 19 Oct 2023 19:43:11 +0300 Subject: [PATCH 0462/1233] resolve hound --- openpype/hosts/houdini/plugins/publish/validate_frame_range.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_frame_range.py b/openpype/hosts/houdini/plugins/publish/validate_frame_range.py index 343cf3c5e4..6a66f3de9f 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_frame_range.py +++ b/openpype/hosts/houdini/plugins/publish/validate_frame_range.py @@ -72,7 +72,6 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): if not cls.get_invalid(instance): # Already fixed - print ("Not working") return # Disable use asset handles From 32501fbb2bbb44fe9751c6420a3c37f440b98620 Mon Sep 17 00:00:00 2001 From: jmichael7 <148431692+jmichael7@users.noreply.github.com> Date: Thu, 19 Oct 2023 22:46:54 +0530 Subject: [PATCH 0463/1233] Corrected a typo in Readme.md (Top -> To) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ce98f845e6..ed3e058002 100644 --- a/README.md +++ b/README.md @@ -279,7 +279,7 @@ arguments and it will create zip file that OpenPype can use. Building documentation ---------------------- -Top build API documentation, run `.\tools\make_docs(.ps1|.sh)`. It will create html documentation +To build API documentation, run `.\tools\make_docs(.ps1|.sh)`. It will create html documentation from current sources in `.\docs\build`. **Note that it needs existing virtual environment.** From 404a4dedfcfd798cbcdbab8dc3637d36b5e059e8 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 20 Oct 2023 11:20:58 +0800 Subject: [PATCH 0464/1233] clean up the code across the tycache families --- .../hosts/max/plugins/load/load_tycache.py | 2 +- .../max/plugins/publish/extract_tycache.py | 6 ++--- .../plugins/publish/validate_tyflow_data.py | 25 ++++++++----------- 3 files changed, 14 insertions(+), 19 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_tycache.py b/openpype/hosts/max/plugins/load/load_tycache.py index f878ed9f1c..ff9598b33f 100644 --- a/openpype/hosts/max/plugins/load/load_tycache.py +++ b/openpype/hosts/max/plugins/load/load_tycache.py @@ -25,7 +25,7 @@ class TyCacheLoader(load.LoaderPlugin): def load(self, context, name=None, namespace=None, data=None): """Load tyCache""" from pymxs import runtime as rt - filepath = os.path.normpath(self.filepath_from_context(context)) + filepath = self.filepath_from_context(context) obj = rt.tyCache() obj.filename = filepath diff --git a/openpype/hosts/max/plugins/publish/extract_tycache.py b/openpype/hosts/max/plugins/publish/extract_tycache.py index d9d7c17cff..baed8a9e44 100644 --- a/openpype/hosts/max/plugins/publish/extract_tycache.py +++ b/openpype/hosts/max/plugins/publish/extract_tycache.py @@ -8,8 +8,7 @@ from openpype.pipeline import publish class ExtractTyCache(publish.Extractor): - """ - Extract tycache format with tyFlow operators. + """Extract tycache format with tyFlow operators. Notes: - TyCache only works for TyFlow Pro Plugin. @@ -89,7 +88,6 @@ class ExtractTyCache(publish.Extractor): """ filenames = [] - # should we include frame 0 ? for frame in range(int(start_frame), int(end_frame) + 1): filename = f"{instance.name}__tyPart_{frame:05}.tyc" filenames.append(filename) @@ -146,7 +144,7 @@ class ExtractTyCache(publish.Extractor): opt_list = [] for member in members: obj = member.baseobject - # TODO: to see if it can be used maxscript instead + # TODO: see if it can use maxscript instead anim_names = rt.GetSubAnimNames(obj) for anim_name in anim_names: sub_anim = rt.GetSubAnim(obj, anim_name) diff --git a/openpype/hosts/max/plugins/publish/validate_tyflow_data.py b/openpype/hosts/max/plugins/publish/validate_tyflow_data.py index 67c35ec01c..c0f29422ec 100644 --- a/openpype/hosts/max/plugins/publish/validate_tyflow_data.py +++ b/openpype/hosts/max/plugins/publish/validate_tyflow_data.py @@ -4,8 +4,7 @@ from pymxs import runtime as rt class ValidateTyFlowData(pyblish.api.InstancePlugin): - """Validate that TyFlow plugins or - relevant operators being set correctly.""" + """Validate TyFlow plugins or relevant operators are set correctly.""" order = pyblish.api.ValidatorOrder families = ["pointcloud", "tycache"] @@ -31,9 +30,9 @@ class ValidateTyFlowData(pyblish.api.InstancePlugin): if invalid_object or invalid_operator: raise PublishValidationError( "issues occurred", - description="Container should only include tyFlow object\n " - "and tyflow operator 'Export Particle' should be in \n" - "the tyFlow editor") + description="Container should only include tyFlow object " + "and tyflow operator 'Export Particle' should be in " + "the tyFlow editor.") def get_tyflow_object(self, instance): """Get the nodes which are not tyFlow object(s) @@ -43,19 +42,17 @@ class ValidateTyFlowData(pyblish.api.InstancePlugin): instance (pyblish.api.Instance): instance Returns: - invalid(list): list of invalid nodes which are not - tyFlow object(s) and editable mesh(es). + list: invalid nodes which are not tyFlow + object(s) and editable mesh(es). """ - invalid = [] container = instance.data["instance_node"] self.log.debug(f"Validating tyFlow container for {container}") - selection_list = instance.data["members"] - for sel in selection_list: - if rt.ClassOf(sel) not in [rt.tyFlow, rt.Editable_Mesh]: - invalid.append(sel) - - return invalid + allowed_classes = [rt.tyFlow, rt.Editable_Mesh] + return [ + member for member in instance.data["members"] + if rt.ClassOf(member) not in allowed_classes + ] def get_tyflow_operator(self, instance): """Check if the Export Particle Operators in the node From ec70706ab5ef53d8d71a4e0802d77ac28f9707d2 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 20 Oct 2023 11:22:26 +0800 Subject: [PATCH 0465/1233] hound --- openpype/hosts/max/plugins/load/load_tycache.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_tycache.py b/openpype/hosts/max/plugins/load/load_tycache.py index ff9598b33f..a860ecd357 100644 --- a/openpype/hosts/max/plugins/load/load_tycache.py +++ b/openpype/hosts/max/plugins/load/load_tycache.py @@ -1,5 +1,3 @@ -import os - from openpype.hosts.max.api import lib, maintained_selection from openpype.hosts.max.api.lib import ( unique_namespace, From 8369dfddc98ec289c2daf5fa39b3f1af70532221 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 20 Oct 2023 11:43:30 +0800 Subject: [PATCH 0466/1233] use asset entity data for get_frame_range --- openpype/hosts/max/api/lib.py | 18 ++++++++++++------ .../plugins/publish/validate_frame_range.py | 5 +++-- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 8b70b3ced7..fcd21111fa 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -234,22 +234,28 @@ def reset_scene_resolution(): set_scene_resolution(width, height) -def get_frame_range() -> Union[Dict[str, Any], None]: +def get_frame_range(asset_doc=None) -> Union[Dict[str, Any], None]: """Get the current assets frame range and handles. + Args: + asset_doc (dict): Asset Entity Data + Returns: dict: with frame start, frame end, handle start, handle end. """ # Set frame start/end - asset = get_current_project_asset() - frame_start = asset["data"].get("frameStart") - frame_end = asset["data"].get("frameEnd") + if asset_doc is None: + asset_doc = get_current_project_asset() + + data = asset_doc["data"] + frame_start = data.get("frameStart") + frame_end = data.get("frameEnd") if frame_start is None or frame_end is None: return - handle_start = asset["data"].get("handleStart", 0) - handle_end = asset["data"].get("handleEnd", 0) + handle_start = data.get("handleStart", 0) + handle_end = data.get("handleEnd", 0) return { "frameStart": frame_start, "frameEnd": frame_end, diff --git a/openpype/hosts/max/plugins/publish/validate_frame_range.py b/openpype/hosts/max/plugins/publish/validate_frame_range.py index b1e8aafbb7..1ca9761da6 100644 --- a/openpype/hosts/max/plugins/publish/validate_frame_range.py +++ b/openpype/hosts/max/plugins/publish/validate_frame_range.py @@ -37,10 +37,11 @@ class ValidateFrameRange(pyblish.api.InstancePlugin, def process(self, instance): if not self.is_active(instance.data): - self.log.info("Skipping validation...") + self.log.debug("Skipping Validate Frame Range...") return - frame_range = get_frame_range() + frame_range = get_frame_range( + asset_doc=instance.data["assetEntity"]) inst_frame_start = instance.data.get("frameStart") inst_frame_end = instance.data.get("frameEnd") From 151881e1b3b5b13dab81a360d297bdf4491b73d2 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 20 Oct 2023 16:29:42 +0800 Subject: [PATCH 0467/1233] clean up code for review families --- openpype/hosts/max/api/preview_animation.py | 11 ++-- .../max/plugins/publish/collect_review.py | 16 +++-- .../publish/extract_review_animation.py | 15 ++--- .../max/plugins/publish/extract_thumbnail.py | 60 ++++++------------- .../publish/validate_resolution_setting.py | 4 +- 5 files changed, 37 insertions(+), 69 deletions(-) diff --git a/openpype/hosts/max/api/preview_animation.py b/openpype/hosts/max/api/preview_animation.py index 1d7211443a..4c878cc33a 100644 --- a/openpype/hosts/max/api/preview_animation.py +++ b/openpype/hosts/max/api/preview_animation.py @@ -106,10 +106,10 @@ def _render_preview_animation_max_2024( list: Created files """ filepath = filepath.replace("\\", "/") - filepath = f"{filepath}..{ext}" + preview_output = f"{filepath}..{ext}" frame_template = f"{filepath}.{{:04d}}.{ext}" job_args = list() - default_option = f'CreatePreview filename:"{filepath}"' + default_option = f'CreatePreview filename:"{preview_output}"' job_args.append(default_option) frame_option = f"outputAVI:false start:{start} end:{end}" job_args.append(frame_option) @@ -199,10 +199,10 @@ def _render_preview_animation_max_pre_2024( rt.close(preview_res) rt.close(dib) files.append(filepath) - # clean up the cache if rt.keyboard.escPressed: user_cancelled = True break + # clean up the cache rt.gc(delayed=True) if user_cancelled: raise RuntimeError("User cancelled rendering of viewport animation.") @@ -267,11 +267,10 @@ def render_preview_animation( def viewport_options_for_preview_animation(): - """ - Function to store the default data of viewport options + """Function to store the default data of viewport options + Returns: dict: viewport setting options - """ # viewport_options should be the dictionary if int(get_max_version()) < 2024: diff --git a/openpype/hosts/max/plugins/publish/collect_review.py b/openpype/hosts/max/plugins/publish/collect_review.py index cfd48edb15..8b782344eb 100644 --- a/openpype/hosts/max/plugins/publish/collect_review.py +++ b/openpype/hosts/max/plugins/publish/collect_review.py @@ -35,8 +35,8 @@ class CollectReview(pyblish.api.InstancePlugin, "frameStart": instance.context.data["frameStart"], "frameEnd": instance.context.data["frameEnd"], "fps": instance.context.data["fps"], - "resolution": (creator_attrs["review_width"], - creator_attrs["review_height"]) + "review_width": creator_attrs["review_width"], + "review_height": creator_attrs["review_height"], } if int(get_max_version()) >= 2024: @@ -67,9 +67,6 @@ class CollectReview(pyblish.api.InstancePlugin, "dspFrameNums": attr_values.get("dspFrameNums") } else: - preview_data = {} - preview_data.update({ - "percentSize": creator_attrs["percentSize"]}) general_viewport = { "dspBkg": attr_values.get("dspBkg"), "dspGrid": attr_values.get("dspGrid") @@ -79,10 +76,11 @@ class CollectReview(pyblish.api.InstancePlugin, "ViewportPreset": creator_attrs["viewportPreset"], "UseTextureEnabled": creator_attrs["vpTexture"] } - preview_data["general_viewport"] = general_viewport - preview_data["nitrous_viewport"] = nitrous_viewport - preview_data["vp_btn_mgr"] = { - "EnableButtons": False + preview_data = { + "percentSize": creator_attrs["percentSize"], + "general_viewport": general_viewport, + "nitrous_viewport": nitrous_viewport, + "vp_btn_mgr": {"EnableButtons": False} } # Enable ftrack functionality diff --git a/openpype/hosts/max/plugins/publish/extract_review_animation.py b/openpype/hosts/max/plugins/publish/extract_review_animation.py index df1f2b4182..8391346e40 100644 --- a/openpype/hosts/max/plugins/publish/extract_review_animation.py +++ b/openpype/hosts/max/plugins/publish/extract_review_animation.py @@ -19,27 +19,22 @@ class ExtractReviewAnimation(publish.Extractor): def process(self, instance): staging_dir = self.staging_dir(instance) ext = instance.data.get("imageFormat") - filename = "{0}..{1}".format(instance.name, ext) start = int(instance.data["frameStart"]) end = int(instance.data["frameEnd"]) - filepath = os.path.join(staging_dir, filename) - filepath = filepath.replace("\\", "/") - + filepath = os.path.join(staging_dir, instance.name) self.log.debug( - "Writing Review Animation to" - " '%s' to '%s'" % (filename, staging_dir)) + "Writing Review Animation to '{}'".format(filepath)) review_camera = instance.data["review_camera"] viewport_options = instance.data.get("viewport_options", {}) - resolution = instance.data.get("resolution", ()) files = render_preview_animation( - os.path.join(staging_dir, instance.name), + filepath, ext, review_camera, start, end, - width=resolution[0], - height=resolution[1], + width=instance.data["review_width"], + height=instance.data["review_height"], viewport_options=viewport_options) filenames = [os.path.basename(path) for path in files] diff --git a/openpype/hosts/max/plugins/publish/extract_thumbnail.py b/openpype/hosts/max/plugins/publish/extract_thumbnail.py index 890ee24f8e..fdedb3d0fc 100644 --- a/openpype/hosts/max/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/max/plugins/publish/extract_thumbnail.py @@ -1,20 +1,12 @@ import os import tempfile import pyblish.api -from pymxs import runtime as rt from openpype.pipeline import publish -from openpype.hosts.max.api.lib import ( - viewport_setup_updated, - viewport_setup, - get_max_version, - set_preview_arg -) - +from openpype.hosts.max.api.preview_animation import render_preview_animation class ExtractThumbnail(publish.Extractor): - """ - Extract Thumbnail for Review + """Extract Thumbnail for Review """ order = pyblish.api.ExtractorOrder @@ -29,36 +21,26 @@ class ExtractThumbnail(publish.Extractor): self.log.debug( f"Create temp directory {tmp_staging} for thumbnail" ) - fps = float(instance.data["fps"]) + ext = instance.data.get("imageFormat") frame = int(instance.data["frameStart"]) instance.context.data["cleanupFullPaths"].append(tmp_staging) - filename = "{name}_thumbnail..png".format(**instance.data) - filepath = os.path.join(tmp_staging, filename) - filepath = filepath.replace("\\", "/") - thumbnail = self.get_filename(instance.name, frame) + filepath = os.path.join(tmp_staging, instance.name) + + self.log.debug("Writing Thumbnail to '{}'".format(filepath)) - self.log.debug( - "Writing Thumbnail to" - " '%s' to '%s'" % (filename, tmp_staging)) review_camera = instance.data["review_camera"] - if int(get_max_version()) >= 2024: - with viewport_setup_updated(review_camera): - preview_arg = set_preview_arg( - instance, filepath, frame, frame, fps) - rt.execute(preview_arg) - else: - visual_style_preset = instance.data.get("visualStyleMode") - nitrousGraphicMgr = rt.NitrousGraphicsManager - viewport_setting = nitrousGraphicMgr.GetActiveViewportSetting() - with viewport_setup( - viewport_setting, - visual_style_preset, - review_camera): - viewport_setting.VisualStyleMode = rt.Name( - visual_style_preset) - preview_arg = set_preview_arg( - instance, filepath, frame, frame, fps) - rt.execute(preview_arg) + viewport_options = instance.data.get("viewport_options", {}) + files = render_preview_animation( + filepath, + ext, + review_camera, + frame, + frame, + width=instance.data["review_width"], + height=instance.data["review_height"], + viewport_options=viewport_options) + + thumbnail = next(os.path.basename(path) for path in files) representation = { "name": "thumbnail", @@ -73,9 +55,3 @@ class ExtractThumbnail(publish.Extractor): if "representations" not in instance.data: instance.data["representations"] = [] instance.data["representations"].append(representation) - - def get_filename(self, filename, target_frame): - thumbnail_name = "{}_thumbnail.{:04}.png".format( - filename, target_frame - ) - return thumbnail_name diff --git a/openpype/hosts/max/plugins/publish/validate_resolution_setting.py b/openpype/hosts/max/plugins/publish/validate_resolution_setting.py index 969db0da2d..7d91a7b991 100644 --- a/openpype/hosts/max/plugins/publish/validate_resolution_setting.py +++ b/openpype/hosts/max/plugins/publish/validate_resolution_setting.py @@ -12,7 +12,7 @@ class ValidateResolutionSetting(pyblish.api.InstancePlugin, """Validate the resolution setting aligned with DB""" order = pyblish.api.ValidatorOrder - 0.01 - families = ["maxrender", "review"] + families = ["maxrender"] hosts = ["max"] label = "Validate Resolution Setting" optional = True @@ -21,7 +21,7 @@ class ValidateResolutionSetting(pyblish.api.InstancePlugin, if not self.is_active(instance.data): return width, height = self.get_db_resolution(instance) - current_width = rt.renderwidth + current_width = rt.renderWidth current_height = rt.renderHeight if current_width != width and current_height != height: raise PublishValidationError("Resolution Setting " From 2b335af1080f4ad1357ca55ed4c936a332be9d3e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 20 Oct 2023 17:22:51 +0800 Subject: [PATCH 0468/1233] make the calculation of the resolution with precent size more accurate and clean up the code on thumbnail extractor --- openpype/hosts/max/api/preview_animation.py | 13 ++++++------- .../max/plugins/publish/extract_thumbnail.py | 18 ++++++------------ 2 files changed, 12 insertions(+), 19 deletions(-) diff --git a/openpype/hosts/max/api/preview_animation.py b/openpype/hosts/max/api/preview_animation.py index 4c878cc33a..15fef1b428 100644 --- a/openpype/hosts/max/api/preview_animation.py +++ b/openpype/hosts/max/api/preview_animation.py @@ -157,15 +157,14 @@ def _render_preview_animation_max_pre_2024( filepath (str): filepath without frame numbers and extension startFrame (int): start frame endFrame (int): end frame - percentSize (int): percentage of the resolution ext (str): image extension Returns: list: Created filepaths """ # get the screenshot - resolution_percentage = float(percentSize) / 100 - res_width = rt.renderWidth * resolution_percentage - res_height = rt.renderHeight * resolution_percentage + percent = percentSize / 100.0 + res_width = int(round(rt.renderWidth * percent)) + res_height = int(round(rt.renderHeight * percent)) viewportRatio = float(res_width / res_height) frame_template = "{}.{{:04}}.{}".format(filepath, ext) frame_template.replace("\\", "/") @@ -212,7 +211,7 @@ def _render_preview_animation_max_pre_2024( def render_preview_animation( filepath, ext, - review_camera, + camera, start_frame=None, end_frame=None, width=1920, @@ -223,7 +222,7 @@ def render_preview_animation( filepath (str): filepath to render to, without frame number and extension ext (str): output file extension - review_camera (str): viewport camera for preview render + camera (str): viewport camera for preview render start_frame (int): start frame end_frame (int): end frame width (int): render resolution width @@ -240,7 +239,7 @@ def render_preview_animation( if viewport_options is None: viewport_options = viewport_options_for_preview_animation() with play_preview_when_done(False): - with viewport_camera(review_camera): + with viewport_camera(camera): with render_resolution(width, height): if int(get_max_version()) < 2024: with viewport_preference_setting( diff --git a/openpype/hosts/max/plugins/publish/extract_thumbnail.py b/openpype/hosts/max/plugins/publish/extract_thumbnail.py index fdedb3d0fc..05a5156cd3 100644 --- a/openpype/hosts/max/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/max/plugins/publish/extract_thumbnail.py @@ -15,17 +15,11 @@ class ExtractThumbnail(publish.Extractor): families = ["review"] def process(self, instance): - # TODO: Create temp directory for thumbnail - # - this is to avoid "override" of source file - tmp_staging = tempfile.mkdtemp(prefix="pyblish_tmp_") - self.log.debug( - f"Create temp directory {tmp_staging} for thumbnail" - ) ext = instance.data.get("imageFormat") frame = int(instance.data["frameStart"]) - instance.context.data["cleanupFullPaths"].append(tmp_staging) - filepath = os.path.join(tmp_staging, instance.name) - + staging_dir = self.staging_dir(instance) + filepath = os.path.join( + staging_dir, f"{instance.name}_thumbnail") self.log.debug("Writing Thumbnail to '{}'".format(filepath)) review_camera = instance.data["review_camera"] @@ -34,8 +28,8 @@ class ExtractThumbnail(publish.Extractor): filepath, ext, review_camera, - frame, - frame, + start_frame=frame, + end_frame=frame, width=instance.data["review_width"], height=instance.data["review_height"], viewport_options=viewport_options) @@ -46,7 +40,7 @@ class ExtractThumbnail(publish.Extractor): "name": "thumbnail", "ext": "png", "files": thumbnail, - "stagingDir": tmp_staging, + "stagingDir": staging_dir, "thumbnail": True } From 6583525e429cc98e4ac3d81cf3bc0a397990cdd5 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 20 Oct 2023 11:58:51 +0100 Subject: [PATCH 0469/1233] Add option to replace only selected actors --- openpype/hosts/unreal/api/pipeline.py | 33 ++++-- .../unreal/plugins/inventory/update_actors.py | 112 +++++++++++------- 2 files changed, 88 insertions(+), 57 deletions(-) diff --git a/openpype/hosts/unreal/api/pipeline.py b/openpype/hosts/unreal/api/pipeline.py index 760e052a3e..35b218e629 100644 --- a/openpype/hosts/unreal/api/pipeline.py +++ b/openpype/hosts/unreal/api/pipeline.py @@ -654,13 +654,21 @@ def generate_sequence(h, h_dir): def _get_comps_and_assets( - component_class, asset_class, old_assets, new_assets + component_class, asset_class, old_assets, new_assets, selected ): eas = unreal.get_editor_subsystem(unreal.EditorActorSubsystem) - comps = eas.get_all_level_actors_components() - components = [ - c for c in comps if isinstance(c, component_class) - ] + + components = [] + if selected: + sel_actors = eas.get_selected_level_actors() + for actor in sel_actors: + comps = actor.get_components_by_class(component_class) + components.extend(comps) + else: + comps = eas.get_all_level_actors_components() + components = [ + c for c in comps if isinstance(c, component_class) + ] # Get all the static meshes among the old assets in a dictionary with # the name as key @@ -681,14 +689,15 @@ def _get_comps_and_assets( return components, selected_old_assets, selected_new_assets -def replace_static_mesh_actors(old_assets, new_assets): +def replace_static_mesh_actors(old_assets, new_assets, selected): smes = unreal.get_editor_subsystem(unreal.StaticMeshEditorSubsystem) static_mesh_comps, old_meshes, new_meshes = _get_comps_and_assets( unreal.StaticMeshComponent, unreal.StaticMesh, old_assets, - new_assets + new_assets, + selected ) for old_name, old_mesh in old_meshes.items(): @@ -701,12 +710,13 @@ def replace_static_mesh_actors(old_assets, new_assets): static_mesh_comps, old_mesh, new_mesh) -def replace_skeletal_mesh_actors(old_assets, new_assets): +def replace_skeletal_mesh_actors(old_assets, new_assets, selected): skeletal_mesh_comps, old_meshes, new_meshes = _get_comps_and_assets( unreal.SkeletalMeshComponent, unreal.SkeletalMesh, old_assets, - new_assets + new_assets, + selected ) for old_name, old_mesh in old_meshes.items(): @@ -720,12 +730,13 @@ def replace_skeletal_mesh_actors(old_assets, new_assets): comp.set_skeletal_mesh_asset(new_mesh) -def replace_geometry_cache_actors(old_assets, new_assets): +def replace_geometry_cache_actors(old_assets, new_assets, selected): geometry_cache_comps, old_caches, new_caches = _get_comps_and_assets( unreal.GeometryCacheComponent, unreal.GeometryCache, old_assets, - new_assets + new_assets, + selected ) for old_name, old_mesh in old_caches.items(): diff --git a/openpype/hosts/unreal/plugins/inventory/update_actors.py b/openpype/hosts/unreal/plugins/inventory/update_actors.py index 37777114e2..2b012cf22c 100644 --- a/openpype/hosts/unreal/plugins/inventory/update_actors.py +++ b/openpype/hosts/unreal/plugins/inventory/update_actors.py @@ -10,58 +10,78 @@ from openpype.hosts.unreal.api.pipeline import ( from openpype.pipeline import InventoryAction -class UpdateActors(InventoryAction): - """Update Actors in level to this version. +def update_assets(containers, selected): + allowed_families = ["model", "rig"] + + # Get all the containers in the Unreal Project + all_containers = ls() + + for container in containers: + container_dir = container.get("namespace") + if container.get("family") not in allowed_families: + unreal.log_warning( + f"Container {container_dir} is not supported.") + continue + + # Get all containers with same asset_name but different objectName. + # These are the containers that need to be updated in the level. + sa_containers = [ + i + for i in all_containers + if ( + i.get("asset_name") == container.get("asset_name") and + i.get("objectName") != container.get("objectName") + ) + ] + + asset_content = unreal.EditorAssetLibrary.list_assets( + container_dir, recursive=True, include_folder=False + ) + + # Update all actors in level + for sa_cont in sa_containers: + sa_dir = sa_cont.get("namespace") + old_content = unreal.EditorAssetLibrary.list_assets( + sa_dir, recursive=True, include_folder=False + ) + + if container.get("family") == "rig": + replace_skeletal_mesh_actors( + old_content, asset_content, selected) + replace_static_mesh_actors( + old_content, asset_content, selected) + elif container.get("family") == "model": + if container.get("loader") == "PointCacheAlembicLoader": + replace_geometry_cache_actors( + old_content, asset_content, selected) + else: + replace_static_mesh_actors( + old_content, asset_content, selected) + + unreal.EditorLevelLibrary.save_current_level() + + delete_previous_asset_if_unused(sa_cont, old_content) + + +class UpdateAllActors(InventoryAction): + """Update all the Actors in the current level to the version of the asset + selected in the scene manager. """ - label = "Update Actors in level to this version" + label = "Replace all Actors in level to this version" icon = "arrow-up" def process(self, containers): - allowed_families = ["model", "rig"] + update_assets(containers, False) - # Get all the containers in the Unreal Project - all_containers = ls() - for container in containers: - container_dir = container.get("namespace") - if container.get("family") not in allowed_families: - unreal.log_warning( - f"Container {container_dir} is not supported.") - continue +class UpdateSelectedActors(InventoryAction): + """Update only the selected Actors in the current level to the version + of the asset selected in the scene manager. + """ - # Get all containers with same asset_name but different objectName. - # These are the containers that need to be updated in the level. - sa_containers = [ - i - for i in all_containers - if ( - i.get("asset_name") == container.get("asset_name") and - i.get("objectName") != container.get("objectName") - ) - ] + label = "Replace selected Actors in level to this version" + icon = "arrow-up" - asset_content = unreal.EditorAssetLibrary.list_assets( - container_dir, recursive=True, include_folder=False - ) - - # Update all actors in level - for sa_cont in sa_containers: - sa_dir = sa_cont.get("namespace") - old_content = unreal.EditorAssetLibrary.list_assets( - sa_dir, recursive=True, include_folder=False - ) - - if container.get("family") == "rig": - replace_skeletal_mesh_actors(old_content, asset_content) - replace_static_mesh_actors(old_content, asset_content) - elif container.get("family") == "model": - if container.get("loader") == "PointCacheAlembicLoader": - replace_geometry_cache_actors( - old_content, asset_content) - else: - replace_static_mesh_actors(old_content, asset_content) - - unreal.EditorLevelLibrary.save_current_level() - - delete_previous_asset_if_unused(sa_cont, old_content) + def process(self, containers): + update_assets(containers, True) From 465101c6398fcc1fcd6cca0e5ecd278dae2a4640 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 20 Oct 2023 12:22:44 +0100 Subject: [PATCH 0470/1233] Added another action to delete unused assets --- openpype/hosts/unreal/api/pipeline.py | 2 +- .../plugins/inventory/delete_unused_assets.py | 31 +++++++++++++++++++ .../unreal/plugins/inventory/update_actors.py | 4 +-- 3 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 openpype/hosts/unreal/plugins/inventory/delete_unused_assets.py diff --git a/openpype/hosts/unreal/api/pipeline.py b/openpype/hosts/unreal/api/pipeline.py index 35b218e629..0bb19ec601 100644 --- a/openpype/hosts/unreal/api/pipeline.py +++ b/openpype/hosts/unreal/api/pipeline.py @@ -750,7 +750,7 @@ def replace_geometry_cache_actors(old_assets, new_assets, selected): comp.set_geometry_cache(new_mesh) -def delete_previous_asset_if_unused(container, asset_content): +def delete_asset_if_unused(container, asset_content): ar = unreal.AssetRegistryHelpers.get_asset_registry() references = set() diff --git a/openpype/hosts/unreal/plugins/inventory/delete_unused_assets.py b/openpype/hosts/unreal/plugins/inventory/delete_unused_assets.py new file mode 100644 index 0000000000..f63476b3c7 --- /dev/null +++ b/openpype/hosts/unreal/plugins/inventory/delete_unused_assets.py @@ -0,0 +1,31 @@ +import unreal + +from openpype.hosts.unreal.api.pipeline import delete_asset_if_unused +from openpype.pipeline import InventoryAction + + + +class DeleteUnusedAssets(InventoryAction): + """Delete all the assets that are not used in any level. + """ + + label = "Delete Unused Assets" + icon = "trash" + color = "red" + order = 1 + + def process(self, containers): + allowed_families = ["model", "rig"] + + for container in containers: + container_dir = container.get("namespace") + if container.get("family") not in allowed_families: + unreal.log_warning( + f"Container {container_dir} is not supported.") + continue + + asset_content = unreal.EditorAssetLibrary.list_assets( + container_dir, recursive=True, include_folder=False + ) + + delete_asset_if_unused(container, asset_content) diff --git a/openpype/hosts/unreal/plugins/inventory/update_actors.py b/openpype/hosts/unreal/plugins/inventory/update_actors.py index 2b012cf22c..6bc576716c 100644 --- a/openpype/hosts/unreal/plugins/inventory/update_actors.py +++ b/openpype/hosts/unreal/plugins/inventory/update_actors.py @@ -5,7 +5,7 @@ from openpype.hosts.unreal.api.pipeline import ( replace_static_mesh_actors, replace_skeletal_mesh_actors, replace_geometry_cache_actors, - delete_previous_asset_if_unused, + delete_asset_if_unused, ) from openpype.pipeline import InventoryAction @@ -60,7 +60,7 @@ def update_assets(containers, selected): unreal.EditorLevelLibrary.save_current_level() - delete_previous_asset_if_unused(sa_cont, old_content) + delete_asset_if_unused(sa_cont, old_content) class UpdateAllActors(InventoryAction): From ea1c9bf70722ef075a02c446df2685e4dd50e00f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 20 Oct 2023 15:12:54 +0200 Subject: [PATCH 0471/1233] Removed redundant copy of extension.zxp (#5802) --- .../hosts/photoshop/api/extension/extension.zxp | Bin 54056 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 openpype/hosts/photoshop/api/extension/extension.zxp diff --git a/openpype/hosts/photoshop/api/extension/extension.zxp b/openpype/hosts/photoshop/api/extension/extension.zxp deleted file mode 100644 index 39b766cd0d354e63c5b0e5b874be47131860e3c6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 54056 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 Date: Fri, 20 Oct 2023 21:34:48 +0800 Subject: [PATCH 0472/1233] rename frame range data --- openpype/hosts/max/api/lib.py | 23 +++++++++--------- .../plugins/publish/collect_frame_range.py | 8 +++---- .../max/plugins/publish/collect_render.py | 4 ++-- .../max/plugins/publish/collect_review.py | 2 ++ .../max/plugins/publish/extract_camera_abc.py | 4 ++-- .../max/plugins/publish/extract_pointcache.py | 4 ++-- .../max/plugins/publish/extract_pointcloud.py | 4 ++-- .../plugins/publish/extract_redshift_proxy.py | 4 ++-- .../publish/extract_review_animation.py | 4 ++-- .../max/plugins/publish/extract_thumbnail.py | 2 +- .../plugins/publish/validate_frame_range.py | 24 +++++++++---------- 11 files changed, 42 insertions(+), 41 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index fcd21111fa..979665d892 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -248,19 +248,19 @@ def get_frame_range(asset_doc=None) -> Union[Dict[str, Any], None]: asset_doc = get_current_project_asset() data = asset_doc["data"] - frame_start = data.get("frameStart") - frame_end = data.get("frameEnd") + frame_start = data.get("frameStart", 0) + frame_end = data.get("frameEnd", 0) if frame_start is None or frame_end is None: return handle_start = data.get("handleStart", 0) handle_end = data.get("handleEnd", 0) + frame_start_handle = int(frame_start) - int(handle_start) + frame_end_handle = int(frame_end) + int(handle_end) return { - "frameStart": frame_start, - "frameEnd": frame_end, - "handleStart": handle_start, - "handleEnd": handle_end + "frame_start_handle": frame_start_handle, + "frame_end_handle": frame_end_handle, } @@ -280,12 +280,11 @@ def reset_frame_range(fps: bool = True): fps_number = float(data_fps["data"]["fps"]) rt.frameRate = fps_number frame_range = get_frame_range() - frame_start_handle = frame_range["frameStart"] - int( - frame_range["handleStart"] - ) - frame_end_handle = frame_range["frameEnd"] + int(frame_range["handleEnd"]) - set_timeline(frame_start_handle, frame_end_handle) - set_render_frame_range(frame_start_handle, frame_end_handle) + + set_timeline( + frame_range["frame_start_handle"], frame_range["frame_end_handle"]) + set_render_frame_range( + frame_range["frame_start_handle"], frame_range["frame_end_handle"]) def set_context_setting(): diff --git a/openpype/hosts/max/plugins/publish/collect_frame_range.py b/openpype/hosts/max/plugins/publish/collect_frame_range.py index 2dd39b5b50..e83733e4f6 100644 --- a/openpype/hosts/max/plugins/publish/collect_frame_range.py +++ b/openpype/hosts/max/plugins/publish/collect_frame_range.py @@ -16,8 +16,8 @@ class CollectFrameRange(pyblish.api.InstancePlugin): def process(self, instance): if instance.data["family"] == "maxrender": - instance.data["frameStart"] = int(rt.rendStart) - instance.data["frameEnd"] = int(rt.rendEnd) + instance.data["frameStartHandle"] = int(rt.rendStart) + instance.data["frameEndHandle"] = int(rt.rendEnd) else: - instance.data["frameStart"] = int(rt.animationRange.start) - instance.data["frameEnd"] = int(rt.animationRange.end) + instance.data["frameStartHandle"] = int(rt.animationRange.start) + instance.data["frameEndHandle"] = int(rt.animationRange.end) diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index a359e61921..fe580aafc8 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -97,8 +97,8 @@ class CollectRender(pyblish.api.InstancePlugin): "renderer": renderer, "source": filepath, "plugin": "3dsmax", - "frameStart": int(rt.rendStart), - "frameEnd": int(rt.rendEnd), + "frameStart": instance.data.get("frameStartHandle"), + "frameEnd": instance.data.get("frameEndHandle"), "version": version_int, "farm": True } diff --git a/openpype/hosts/max/plugins/publish/collect_review.py b/openpype/hosts/max/plugins/publish/collect_review.py index cc4caae497..fd5bfddf20 100644 --- a/openpype/hosts/max/plugins/publish/collect_review.py +++ b/openpype/hosts/max/plugins/publish/collect_review.py @@ -29,6 +29,8 @@ class CollectReview(pyblish.api.InstancePlugin, attr_values = self.get_attr_values_from_data(instance.data) data = { "review_camera": camera_name, + "frameStart": instance.data.get("frameStartHandle"), + "frameEnd": instance.data.get("frameEndHandle"), "fps": instance.context.data["fps"], "dspGeometry": attr_values.get("dspGeometry"), "dspShapes": attr_values.get("dspShapes"), diff --git a/openpype/hosts/max/plugins/publish/extract_camera_abc.py b/openpype/hosts/max/plugins/publish/extract_camera_abc.py index ea33bc67ed..a42f27be6e 100644 --- a/openpype/hosts/max/plugins/publish/extract_camera_abc.py +++ b/openpype/hosts/max/plugins/publish/extract_camera_abc.py @@ -19,8 +19,8 @@ class ExtractCameraAlembic(publish.Extractor, OptionalPyblishPluginMixin): def process(self, instance): if not self.is_active(instance.data): return - start = instance.data["frameStart"] - end = instance.data["frameEnd"] + start = instance.data["frameStartHandle"] + end = instance.data["frameEndHandle"] self.log.info("Extracting Camera ...") diff --git a/openpype/hosts/max/plugins/publish/extract_pointcache.py b/openpype/hosts/max/plugins/publish/extract_pointcache.py index a5480ff0dc..f6a8500c08 100644 --- a/openpype/hosts/max/plugins/publish/extract_pointcache.py +++ b/openpype/hosts/max/plugins/publish/extract_pointcache.py @@ -51,8 +51,8 @@ class ExtractAlembic(publish.Extractor): families = ["pointcache"] def process(self, instance): - start = instance.data["frameStart"] - end = instance.data["frameEnd"] + start = instance.data["frameStartHandle"] + end = instance.data["frameEndHandle"] self.log.debug("Extracting pointcache ...") diff --git a/openpype/hosts/max/plugins/publish/extract_pointcloud.py b/openpype/hosts/max/plugins/publish/extract_pointcloud.py index 79b4301377..d9fbe5e9dd 100644 --- a/openpype/hosts/max/plugins/publish/extract_pointcloud.py +++ b/openpype/hosts/max/plugins/publish/extract_pointcloud.py @@ -40,8 +40,8 @@ class ExtractPointCloud(publish.Extractor): def process(self, instance): self.settings = self.get_setting(instance) - start = instance.data["frameStart"] - end = instance.data["frameEnd"] + start = instance.data["frameStartHandle"] + end = instance.data["frameEndHandle"] self.log.info("Extracting PRT...") stagingdir = 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 4f64e88584..47ed85977b 100644 --- a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py +++ b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py @@ -16,8 +16,8 @@ class ExtractRedshiftProxy(publish.Extractor): families = ["redshiftproxy"] def process(self, instance): - start = instance.data["frameStart"] - end = instance.data["frameEnd"] + start = instance.data["frameStartHandle"] + end = instance.data["frameEndHandle"] self.log.debug("Extracting Redshift Proxy...") stagingdir = self.staging_dir(instance) diff --git a/openpype/hosts/max/plugins/publish/extract_review_animation.py b/openpype/hosts/max/plugins/publish/extract_review_animation.py index 8e06e52b5c..af86ed7694 100644 --- a/openpype/hosts/max/plugins/publish/extract_review_animation.py +++ b/openpype/hosts/max/plugins/publish/extract_review_animation.py @@ -48,8 +48,8 @@ class ExtractReviewAnimation(publish.Extractor): "ext": instance.data["imageFormat"], "files": filenames, "stagingDir": staging_dir, - "frameStart": instance.data["frameStart"], - "frameEnd": instance.data["frameEnd"], + "frameStart": instance.data["frameStartHandle"], + "frameEnd": instance.data["frameEndHandle"], "tags": tags, "preview": True, "camera_name": review_camera diff --git a/openpype/hosts/max/plugins/publish/extract_thumbnail.py b/openpype/hosts/max/plugins/publish/extract_thumbnail.py index 82f4fc7a8b..0e7da89fa2 100644 --- a/openpype/hosts/max/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/max/plugins/publish/extract_thumbnail.py @@ -24,7 +24,7 @@ class ExtractThumbnail(publish.Extractor): f"Create temp directory {tmp_staging} for thumbnail" ) fps = int(instance.data["fps"]) - frame = int(instance.data["frameStart"]) + frame = int(instance.data["frameStartHandle"]) instance.context.data["cleanupFullPaths"].append(tmp_staging) filename = "{name}_thumbnail..png".format(**instance.data) filepath = os.path.join(tmp_staging, filename) diff --git a/openpype/hosts/max/plugins/publish/validate_frame_range.py b/openpype/hosts/max/plugins/publish/validate_frame_range.py index 1ca9761da6..fa1ff7e380 100644 --- a/openpype/hosts/max/plugins/publish/validate_frame_range.py +++ b/openpype/hosts/max/plugins/publish/validate_frame_range.py @@ -43,14 +43,10 @@ class ValidateFrameRange(pyblish.api.InstancePlugin, frame_range = get_frame_range( asset_doc=instance.data["assetEntity"]) - inst_frame_start = instance.data.get("frameStart") - inst_frame_end = instance.data.get("frameEnd") - frame_start_handle = frame_range["frameStart"] - int( - frame_range["handleStart"] - ) - frame_end_handle = frame_range["frameEnd"] + int( - frame_range["handleEnd"] - ) + inst_frame_start = instance.data.get("frameStartHandle") + inst_frame_end = instance.data.get("frameEndHandle") + frame_start_handle = frame_range["frame_start_handle"] + frame_end_handle = frame_range["frame_end_handle"] errors = [] if frame_start_handle != inst_frame_start: errors.append( @@ -63,10 +59,14 @@ class ValidateFrameRange(pyblish.api.InstancePlugin, "from the asset data. ") if errors: - errors.append("You can use repair action to fix it.") - report = "Frame range settings are incorrect.\n\n" - for error in errors: - report += "- {}\n\n".format(error) + bullet_point_errors = "\n".join( + "- {}".format(error) for error in errors + ) + report = ( + "Frame range settings are incorrect.\n\n" + f"{bullet_point_errors}\n\n" + "You can use repair action to fix it." + ) raise PublishValidationError(report, title="Frame Range incorrect") @classmethod From 25c0c1996f7dccefcc6016c364d5d87e5cd65bd4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 20 Oct 2023 15:41:34 +0200 Subject: [PATCH 0473/1233] removed 'shotgun_api3' from openpype dependencies --- server_addon/openpype/client/pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/server_addon/openpype/client/pyproject.toml b/server_addon/openpype/client/pyproject.toml index 6d5ac92ca7..40da8f6716 100644 --- a/server_addon/openpype/client/pyproject.toml +++ b/server_addon/openpype/client/pyproject.toml @@ -8,7 +8,6 @@ aiohttp_json_rpc = "*" # TVPaint server aiohttp-middlewares = "^2.0.0" wsrpc_aiohttp = "^3.1.1" # websocket server clique = "1.6.*" -shotgun_api3 = {git = "https://github.com/shotgunsoftware/python-api.git", rev = "v3.3.3"} gazu = "^0.9.3" google-api-python-client = "^1.12.8" # sync server google support (should be separate?) jsonschema = "^2.6.0" From 6453de982326f086e1d8dd1fda4d17ab0fc0fc7e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 20 Oct 2023 22:11:11 +0800 Subject: [PATCH 0474/1233] align the frame range data with other hosts like Maya --- openpype/hosts/max/api/lib.py | 12 +++++++---- .../plugins/publish/validate_frame_range.py | 21 +++++++++++-------- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 979665d892..8aa38b013a 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -259,8 +259,12 @@ def get_frame_range(asset_doc=None) -> Union[Dict[str, Any], None]: frame_start_handle = int(frame_start) - int(handle_start) frame_end_handle = int(frame_end) + int(handle_end) return { - "frame_start_handle": frame_start_handle, - "frame_end_handle": frame_end_handle, + "frameStart": frame_start, + "frameEnd": frame_end, + "handleStart": handle_start, + "handleEnd": handle_end, + "frameStartHandle": frame_start_handle, + "frameEndHandle": frame_end_handle, } @@ -282,9 +286,9 @@ def reset_frame_range(fps: bool = True): frame_range = get_frame_range() set_timeline( - frame_range["frame_start_handle"], frame_range["frame_end_handle"]) + frame_range["frameStartHandle"], frame_range["frameEndHandle"]) set_render_frame_range( - frame_range["frame_start_handle"], frame_range["frame_end_handle"]) + frame_range["frameStartHandle"], frame_range["frameEndHandle"]) def set_context_setting(): diff --git a/openpype/hosts/max/plugins/publish/validate_frame_range.py b/openpype/hosts/max/plugins/publish/validate_frame_range.py index fa1ff7e380..0e8316e844 100644 --- a/openpype/hosts/max/plugins/publish/validate_frame_range.py +++ b/openpype/hosts/max/plugins/publish/validate_frame_range.py @@ -7,7 +7,8 @@ from openpype.pipeline import ( from openpype.pipeline.publish import ( RepairAction, ValidateContentsOrder, - PublishValidationError + PublishValidationError, + KnownPublishError ) from openpype.hosts.max.api.lib import get_frame_range, set_timeline @@ -45,8 +46,13 @@ class ValidateFrameRange(pyblish.api.InstancePlugin, inst_frame_start = instance.data.get("frameStartHandle") inst_frame_end = instance.data.get("frameEndHandle") - frame_start_handle = frame_range["frame_start_handle"] - frame_end_handle = frame_range["frame_end_handle"] + if inst_frame_start is None or inst_frame_end is None: + raise KnownPublishError( + "Missing frame start and frame end on " + "instance to to validate." + ) + frame_start_handle = frame_range["frameStartHandle"] + frame_end_handle = frame_range["frameEndHandle"] errors = [] if frame_start_handle != inst_frame_start: errors.append( @@ -72,12 +78,9 @@ class ValidateFrameRange(pyblish.api.InstancePlugin, @classmethod def repair(cls, instance): frame_range = get_frame_range() - frame_start_handle = frame_range["frameStart"] - int( - frame_range["handleStart"] - ) - frame_end_handle = frame_range["frameEnd"] + int( - frame_range["handleEnd"] - ) + frame_start_handle = frame_range["frameStartHandle"] + frame_end_handle = frame_range["frameEndHandle"] + if instance.data["family"] == "maxrender": rt.rendStart = frame_start_handle rt.rendEnd = frame_end_handle From c5b63b241d199c87766f5a7d8a7c59946b8c9dd3 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 20 Oct 2023 22:12:20 +0800 Subject: [PATCH 0475/1233] check if frame start and frame end is None, if yes it will return empty dict --- openpype/hosts/max/api/lib.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 8aa38b013a..f62f580e83 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -248,11 +248,11 @@ def get_frame_range(asset_doc=None) -> Union[Dict[str, Any], None]: asset_doc = get_current_project_asset() data = asset_doc["data"] - frame_start = data.get("frameStart", 0) - frame_end = data.get("frameEnd", 0) + frame_start = data.get("frameStart") + frame_end = data.get("frameEnd") if frame_start is None or frame_end is None: - return + return {} handle_start = data.get("handleStart", 0) handle_end = data.get("handleEnd", 0) From de2a6c33248b51f19faa51c74ab86fc935be6454 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 20 Oct 2023 16:03:44 +0100 Subject: [PATCH 0476/1233] Added dialog to confirm before deleting files --- .../plugins/inventory/delete_unused_assets.py | 39 ++++++++++++++++++- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/unreal/plugins/inventory/delete_unused_assets.py b/openpype/hosts/unreal/plugins/inventory/delete_unused_assets.py index f63476b3c7..8320e3c92d 100644 --- a/openpype/hosts/unreal/plugins/inventory/delete_unused_assets.py +++ b/openpype/hosts/unreal/plugins/inventory/delete_unused_assets.py @@ -1,10 +1,10 @@ import unreal +from openpype.hosts.unreal.api.tools_ui import qt_app_context from openpype.hosts.unreal.api.pipeline import delete_asset_if_unused from openpype.pipeline import InventoryAction - class DeleteUnusedAssets(InventoryAction): """Delete all the assets that are not used in any level. """ @@ -14,7 +14,9 @@ class DeleteUnusedAssets(InventoryAction): color = "red" order = 1 - def process(self, containers): + dialog = None + + def _delete_unused_assets(self, containers): allowed_families = ["model", "rig"] for container in containers: @@ -29,3 +31,36 @@ class DeleteUnusedAssets(InventoryAction): ) delete_asset_if_unused(container, asset_content) + + def _show_confirmation_dialog(self, containers): + from qtpy import QtCore + from openpype.widgets import popup + from openpype.style import load_stylesheet + + dialog = popup.Popup() + dialog.setWindowFlags( + QtCore.Qt.Window + | QtCore.Qt.WindowStaysOnTopHint + ) + dialog.setFocusPolicy(QtCore.Qt.StrongFocus) + dialog.setWindowTitle("Delete all unused assets") + dialog.setMessage( + "You are about to delete all the assets in the project that \n" + "are not used in any level. Are you sure you want to continue?" + ) + dialog.setButtonText("Delete") + + dialog.on_clicked.connect( + lambda: self._delete_unused_assets(containers) + ) + + dialog.show() + dialog.raise_() + dialog.activateWindow() + dialog.setStyleSheet(load_stylesheet()) + + self.dialog = dialog + + def process(self, containers): + with qt_app_context(): + self._show_confirmation_dialog(containers) From 69d665fd7d7809e9cab1941433627215956dd5fb Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 20 Oct 2023 23:03:52 +0800 Subject: [PATCH 0477/1233] make sure the collectorcorder fro collect render is later than collect frame range --- openpype/hosts/max/plugins/publish/collect_render.py | 6 +++--- openpype/hosts/max/plugins/publish/collect_review.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index fe580aafc8..7765b3b924 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -14,7 +14,7 @@ from openpype.client import get_last_version_by_subset_name class CollectRender(pyblish.api.InstancePlugin): """Collect Render for Deadline""" - order = pyblish.api.CollectorOrder + 0.01 + order = pyblish.api.CollectorOrder + 0.02 label = "Collect 3dsmax Render Layers" hosts = ['max'] families = ["maxrender"] @@ -97,8 +97,8 @@ class CollectRender(pyblish.api.InstancePlugin): "renderer": renderer, "source": filepath, "plugin": "3dsmax", - "frameStart": instance.data.get("frameStartHandle"), - "frameEnd": instance.data.get("frameEndHandle"), + "frameStart": instance.data["frameStartHandle"], + "frameEnd": instance.data["frameEndHandle"], "version": version_int, "farm": True } diff --git a/openpype/hosts/max/plugins/publish/collect_review.py b/openpype/hosts/max/plugins/publish/collect_review.py index fd5bfddf20..531521fa38 100644 --- a/openpype/hosts/max/plugins/publish/collect_review.py +++ b/openpype/hosts/max/plugins/publish/collect_review.py @@ -29,8 +29,8 @@ class CollectReview(pyblish.api.InstancePlugin, attr_values = self.get_attr_values_from_data(instance.data) data = { "review_camera": camera_name, - "frameStart": instance.data.get("frameStartHandle"), - "frameEnd": instance.data.get("frameEndHandle"), + "frameStart": instance.data["frameStartHandle"], + "frameEnd": instance.data["frameEndHandle"], "fps": instance.context.data["fps"], "dspGeometry": attr_values.get("dspGeometry"), "dspShapes": attr_values.get("dspShapes"), From 0988f4a9a87d0f78f810b4a2dc6444784c50371c Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 20 Oct 2023 16:09:54 +0100 Subject: [PATCH 0478/1233] Do not remove automatically unused assets --- openpype/hosts/unreal/plugins/inventory/update_actors.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/unreal/plugins/inventory/update_actors.py b/openpype/hosts/unreal/plugins/inventory/update_actors.py index 6bc576716c..d32887b6f3 100644 --- a/openpype/hosts/unreal/plugins/inventory/update_actors.py +++ b/openpype/hosts/unreal/plugins/inventory/update_actors.py @@ -60,8 +60,6 @@ def update_assets(containers, selected): unreal.EditorLevelLibrary.save_current_level() - delete_asset_if_unused(sa_cont, old_content) - class UpdateAllActors(InventoryAction): """Update all the Actors in the current level to the version of the asset From 28c1c2dbb66a852850d351a4ebc9a35620846460 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 20 Oct 2023 16:10:43 +0100 Subject: [PATCH 0479/1233] Hound fixes --- openpype/hosts/unreal/plugins/inventory/update_actors.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/unreal/plugins/inventory/update_actors.py b/openpype/hosts/unreal/plugins/inventory/update_actors.py index d32887b6f3..b0d941ba80 100644 --- a/openpype/hosts/unreal/plugins/inventory/update_actors.py +++ b/openpype/hosts/unreal/plugins/inventory/update_actors.py @@ -5,7 +5,6 @@ from openpype.hosts.unreal.api.pipeline import ( replace_static_mesh_actors, replace_skeletal_mesh_actors, replace_geometry_cache_actors, - delete_asset_if_unused, ) from openpype.pipeline import InventoryAction From 0240b2665d0a8ae446334d660cae9afaa64e7b74 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 20 Oct 2023 17:17:35 +0200 Subject: [PATCH 0480/1233] use context data instead of 'legacy_io' --- .../modules/timers_manager/plugins/publish/start_timer.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/openpype/modules/timers_manager/plugins/publish/start_timer.py b/openpype/modules/timers_manager/plugins/publish/start_timer.py index 6408327ca1..19a67292f5 100644 --- a/openpype/modules/timers_manager/plugins/publish/start_timer.py +++ b/openpype/modules/timers_manager/plugins/publish/start_timer.py @@ -6,8 +6,6 @@ Requires: import pyblish.api -from openpype.pipeline import legacy_io - class StartTimer(pyblish.api.ContextPlugin): label = "Start Timer" @@ -25,9 +23,9 @@ class StartTimer(pyblish.api.ContextPlugin): self.log.debug("Publish is not affecting running timers.") return - project_name = legacy_io.active_project() - asset_name = legacy_io.Session.get("AVALON_ASSET") - task_name = legacy_io.Session.get("AVALON_TASK") + project_name = context.data["projectName"] + asset_name = context.data.get("asset") + task_name = context.data.get("task") if not project_name or not asset_name or not task_name: self.log.info(( "Current context does not contain all" From b5fc212933fa0e13a60f11a0baaf3eb4ebeae283 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 20 Oct 2023 17:23:29 +0200 Subject: [PATCH 0481/1233] removed unused `_asset` variable from `RenderInstance` --- openpype/pipeline/publish/abstract_collect_render.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/pipeline/publish/abstract_collect_render.py b/openpype/pipeline/publish/abstract_collect_render.py index 8a26402bd8..764532cadb 100644 --- a/openpype/pipeline/publish/abstract_collect_render.py +++ b/openpype/pipeline/publish/abstract_collect_render.py @@ -11,7 +11,6 @@ import six import pyblish.api -from openpype.pipeline import legacy_io from .publish_plugins import AbstractMetaContextPlugin @@ -31,7 +30,7 @@ class RenderInstance(object): label = attr.ib() # label to show in GUI subset = attr.ib() # subset name task = attr.ib() # task name - asset = attr.ib() # asset name (AVALON_ASSET) + asset = attr.ib() # asset name attachTo = attr.ib() # subset name to attach render to setMembers = attr.ib() # list of nodes/members producing render output publish = attr.ib() # bool, True to publish instance @@ -129,7 +128,6 @@ class AbstractCollectRender(pyblish.api.ContextPlugin): """Constructor.""" super(AbstractCollectRender, self).__init__(*args, **kwargs) self._file_path = None - self._asset = legacy_io.Session["AVALON_ASSET"] self._context = None def process(self, context): From bf38b2bbefa47614af271ae7c68ee265c84701e2 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 20 Oct 2023 23:25:37 +0800 Subject: [PATCH 0482/1233] clean up the codes for collect frame range and get frame range function --- openpype/hosts/max/api/lib.py | 11 +++++++---- .../hosts/max/plugins/publish/collect_frame_range.py | 1 - 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index f62f580e83..edbd14bb8b 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -254,10 +254,13 @@ def get_frame_range(asset_doc=None) -> Union[Dict[str, Any], None]: if frame_start is None or frame_end is None: return {} - handle_start = data.get("handleStart", 0) - handle_end = data.get("handleEnd", 0) - frame_start_handle = int(frame_start) - int(handle_start) - frame_end_handle = int(frame_end) + int(handle_end) + frame_start = int(frame_start) + frame_end = int(frame_end) + handle_start = int(data.get("handleStart", 0)) + handle_end = int(data.get("handleEnd", 0)) + frame_start_handle = frame_start - handle_start + frame_end_handle = frame_end + handle_end + return { "frameStart": frame_start, "frameEnd": frame_end, diff --git a/openpype/hosts/max/plugins/publish/collect_frame_range.py b/openpype/hosts/max/plugins/publish/collect_frame_range.py index e83733e4f6..86fb6e856c 100644 --- a/openpype/hosts/max/plugins/publish/collect_frame_range.py +++ b/openpype/hosts/max/plugins/publish/collect_frame_range.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -"""Collect instance members.""" import pyblish.api from pymxs import runtime as rt From caf1cc5cc258fedd2dbcc3fc58e79a7f6acf489c Mon Sep 17 00:00:00 2001 From: Ynbot Date: Sat, 21 Oct 2023 03:24:31 +0000 Subject: [PATCH 0483/1233] [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 ec09c45abb..e2e3c663af 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.17.3" +__version__ = "3.17.4-nightly.1" From 085398d14c87f3f1882e0fbc3ca40191f6b3241f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 21 Oct 2023 03:25:11 +0000 Subject: [PATCH 0484/1233] 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 d63d05f477..3c126048da 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.17.4-nightly.1 - 3.17.3 - 3.17.3-nightly.2 - 3.17.3-nightly.1 @@ -134,7 +135,6 @@ body: - 3.15.0 - 3.15.0-nightly.1 - 3.14.11-nightly.4 - - 3.14.11-nightly.3 validations: required: true - type: dropdown From 39b44e126450f5c12ba18f3db6a13485a507b259 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 23 Oct 2023 16:07:18 +0800 Subject: [PATCH 0485/1233] fix the indentation --- openpype/hosts/max/plugins/publish/extract_tycache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/extract_tycache.py b/openpype/hosts/max/plugins/publish/extract_tycache.py index baed8a9e44..9bfe74f679 100644 --- a/openpype/hosts/max/plugins/publish/extract_tycache.py +++ b/openpype/hosts/max/plugins/publish/extract_tycache.py @@ -144,7 +144,7 @@ class ExtractTyCache(publish.Extractor): opt_list = [] for member in members: obj = member.baseobject - # TODO: see if it can use maxscript instead + # TODO: see if it can use maxscript instead anim_names = rt.GetSubAnimNames(obj) for anim_name in anim_names: sub_anim = rt.GetSubAnim(obj, anim_name) From d9dd36d77aebf2714719dfeb64992cdb206243a5 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 23 Oct 2023 17:24:18 +0800 Subject: [PATCH 0486/1233] clean up code & make sure the ext of thumbnail representation aligns with the image format data setting --- openpype/hosts/max/api/preview_animation.py | 3 --- openpype/hosts/max/plugins/publish/extract_thumbnail.py | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/openpype/hosts/max/api/preview_animation.py b/openpype/hosts/max/api/preview_animation.py index 15fef1b428..bb3ad4a7af 100644 --- a/openpype/hosts/max/api/preview_animation.py +++ b/openpype/hosts/max/api/preview_animation.py @@ -31,7 +31,6 @@ def viewport_camera(camera): camera (str): viewport camera for review render """ original = rt.viewport.getCamera() - has_autoplay = rt.preferences.playPreviewWhenDone if not original: # if there is no original camera # use the current camera as original @@ -39,11 +38,9 @@ def viewport_camera(camera): review_camera = rt.getNodeByName(camera) try: rt.viewport.setCamera(review_camera) - rt.preferences.playPreviewWhenDone = False yield finally: rt.viewport.setCamera(original) - rt.preferences.playPreviewWhenDone = has_autoplay @contextlib.contextmanager diff --git a/openpype/hosts/max/plugins/publish/extract_thumbnail.py b/openpype/hosts/max/plugins/publish/extract_thumbnail.py index fb391d09e1..e9d37d0be5 100644 --- a/openpype/hosts/max/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/max/plugins/publish/extract_thumbnail.py @@ -37,7 +37,7 @@ class ExtractThumbnail(publish.Extractor): representation = { "name": "thumbnail", - "ext": "png", + "ext": ext, "files": thumbnail, "stagingDir": staging_dir, "thumbnail": True From cc970effbc02f32bddff5f9c5f618c3ed7699a70 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 23 Oct 2023 12:12:19 +0200 Subject: [PATCH 0487/1233] moved content of 'contants.py' to 'constants.py' --- openpype/pipeline/publish/constants.py | 4 ++++ openpype/pipeline/publish/contants.py | 3 --- openpype/pipeline/publish/lib.py | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) delete mode 100644 openpype/pipeline/publish/contants.py diff --git a/openpype/pipeline/publish/constants.py b/openpype/pipeline/publish/constants.py index dcd3445200..92e3fb089f 100644 --- a/openpype/pipeline/publish/constants.py +++ b/openpype/pipeline/publish/constants.py @@ -5,3 +5,7 @@ ValidatePipelineOrder = pyblish.api.ValidatorOrder + 0.05 ValidateContentsOrder = pyblish.api.ValidatorOrder + 0.1 ValidateSceneOrder = pyblish.api.ValidatorOrder + 0.2 ValidateMeshOrder = pyblish.api.ValidatorOrder + 0.3 + +DEFAULT_PUBLISH_TEMPLATE = "publish" +DEFAULT_HERO_PUBLISH_TEMPLATE = "hero" +TRANSIENT_DIR_TEMPLATE = "transient" diff --git a/openpype/pipeline/publish/contants.py b/openpype/pipeline/publish/contants.py deleted file mode 100644 index c5296afe9a..0000000000 --- a/openpype/pipeline/publish/contants.py +++ /dev/null @@ -1,3 +0,0 @@ -DEFAULT_PUBLISH_TEMPLATE = "publish" -DEFAULT_HERO_PUBLISH_TEMPLATE = "hero" -TRANSIENT_DIR_TEMPLATE = "transient" diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index 4d9443f635..4ea2f932f1 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -25,7 +25,7 @@ from openpype.pipeline import ( ) from openpype.pipeline.plugin_discover import DiscoverResult -from .contants import ( +from .constants import ( DEFAULT_PUBLISH_TEMPLATE, DEFAULT_HERO_PUBLISH_TEMPLATE, TRANSIENT_DIR_TEMPLATE From 0b0e359632a9b89f804560e868aa9f15a2720281 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 23 Oct 2023 18:24:40 +0800 Subject: [PATCH 0488/1233] docstring tweak --- openpype/hosts/max/api/preview_animation.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/max/api/preview_animation.py b/openpype/hosts/max/api/preview_animation.py index bb3ad4a7af..b8564a9bd4 100644 --- a/openpype/hosts/max/api/preview_animation.py +++ b/openpype/hosts/max/api/preview_animation.py @@ -8,8 +8,7 @@ log = logging.getLogger("openpype.hosts.max") @contextlib.contextmanager def play_preview_when_done(has_autoplay): - """Function to set preview playback option during - context + """Set preview playback option during context Args: has_autoplay (bool): autoplay during creating @@ -25,10 +24,10 @@ def play_preview_when_done(has_autoplay): @contextlib.contextmanager def viewport_camera(camera): - """Function to set viewport camera during context + """Set viewport camera during context ***For 3dsMax 2024+ Args: - camera (str): viewport camera for review render + camera (str): viewport camera """ original = rt.viewport.getCamera() if not original: @@ -90,7 +89,7 @@ def viewport_preference_setting(general_viewport, def _render_preview_animation_max_2024( filepath, start, end, ext, viewport_options): - """Function to set up preview arguments in MaxScript. + """Render viewport preview with MaxScript using `CreateAnimation`. ****For 3dsMax 2024+ Args: filepath (str): filepath for render output without frame number and @@ -263,7 +262,7 @@ def render_preview_animation( def viewport_options_for_preview_animation(): - """Function to store the default data of viewport options + """Get default viewport options for `render_preview_animation`. Returns: dict: viewport setting options From 113b0664ad7f86709b402de5b778df2f0b31050a Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 23 Oct 2023 18:37:29 +0800 Subject: [PATCH 0489/1233] docstring tweak --- 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 4515133d50..cbaf8a0c33 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -498,8 +498,7 @@ def get_plugins() -> list: @contextlib.contextmanager def render_resolution(width, height): - """Function to set render resolution option during - context + """Set render resolution option during context Args: width (int): render width From 8848f5ca2c5b7b554626e28a716aab8546bcb432 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 23 Oct 2023 15:26:07 +0200 Subject: [PATCH 0490/1233] removed unused 'get_render_path' function --- openpype/hosts/nuke/api/lib.py | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 734a565541..8b1ba0ab0d 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -40,7 +40,6 @@ from openpype.settings import ( from openpype.modules import ModulesManager from openpype.pipeline.template_data import get_template_data_with_names from openpype.pipeline import ( - get_current_project_name, discover_legacy_creator_plugins, Anatomy, get_current_host_name, @@ -1099,26 +1098,6 @@ def check_subsetname_exists(nodes, subset_name): False) -def get_render_path(node): - ''' Generate Render path from presets regarding avalon knob data - ''' - avalon_knob_data = read_avalon_data(node) - - nuke_imageio_writes = get_imageio_node_setting( - node_class=avalon_knob_data["families"], - plugin_name=avalon_knob_data["creator"], - subset=avalon_knob_data["subset"] - ) - - data = { - "avalon": avalon_knob_data, - "nuke_imageio_writes": nuke_imageio_writes - } - - anatomy_filled = format_anatomy(data) - return anatomy_filled["render"]["path"].replace("\\", "/") - - def format_anatomy(data): ''' Helping function for formatting of anatomy paths From 473e09761bd5bb8b3b3de4677cb773a9189f57aa Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 23 Oct 2023 21:56:31 +0800 Subject: [PATCH 0491/1233] make sure percentSize works for adjusting resolution of preview animation --- openpype/hosts/max/api/preview_animation.py | 27 ++++++++++++------- .../max/plugins/publish/collect_review.py | 3 +-- .../publish/extract_review_animation.py | 1 + .../max/plugins/publish/extract_thumbnail.py | 1 + 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/max/api/preview_animation.py b/openpype/hosts/max/api/preview_animation.py index b8564a9bd4..eb832c1d1c 100644 --- a/openpype/hosts/max/api/preview_animation.py +++ b/openpype/hosts/max/api/preview_animation.py @@ -88,7 +88,7 @@ def viewport_preference_setting(general_viewport, def _render_preview_animation_max_2024( - filepath, start, end, ext, viewport_options): + filepath, start, end, percentSize, ext, viewport_options): """Render viewport preview with MaxScript using `CreateAnimation`. ****For 3dsMax 2024+ Args: @@ -96,19 +96,25 @@ def _render_preview_animation_max_2024( extension, for example: /path/to/file start (int): startFrame end (int): endFrame + percentSize (float): render resolution multiplier by 100 + e.g. 100.0 is 1x, 50.0 is 0.5x, 150.0 is 1.5x viewport_options (dict): viewport setting options, e.g. {"vpStyle": "defaultshading", "vpPreset": "highquality"} Returns: list: Created files """ + # the percentSize argument must be integer + percent = int(percentSize) filepath = filepath.replace("\\", "/") preview_output = f"{filepath}..{ext}" frame_template = f"{filepath}.{{:04d}}.{ext}" job_args = list() default_option = f'CreatePreview filename:"{preview_output}"' job_args.append(default_option) - frame_option = f"outputAVI:false start:{start} end:{end}" - job_args.append(frame_option) + output_res_option = f"outputAVI:false percentSize:{percent}" + job_args.append(output_res_option) + frame_range_options = f"start:{start} end:{end}" + job_args.append(frame_range_options) for key, value in viewport_options.items(): if isinstance(value, bool): if value: @@ -153,6 +159,8 @@ def _render_preview_animation_max_pre_2024( filepath (str): filepath without frame numbers and extension startFrame (int): start frame endFrame (int): end frame + percentSize (float): render resolution multiplier by 100 + e.g. 100.0 is 1x, 50.0 is 0.5x, 150.0 is 1.5x ext (str): image extension Returns: list: Created filepaths @@ -210,6 +218,7 @@ def render_preview_animation( camera, start_frame=None, end_frame=None, + percentSize=100.0, width=1920, height=1080, viewport_options=None): @@ -221,6 +230,8 @@ def render_preview_animation( camera (str): viewport camera for preview render start_frame (int): start frame end_frame (int): end frame + percentSize (float): render resolution multiplier by 100 + e.g. 100.0 is 1x, 50.0 is 0.5x, 150.0 is 1.5x width (int): render resolution width height (int): render resolution height viewport_options (dict): viewport setting options @@ -243,7 +254,6 @@ def render_preview_animation( viewport_options["nitrous_viewport"], viewport_options["vp_btn_mgr"] ): - percentSize = viewport_options.get("percentSize", 100) return _render_preview_animation_max_pre_2024( filepath, start_frame, @@ -256,6 +266,7 @@ def render_preview_animation( filepath, start_frame, end_frame, + percentSize, ext, viewport_options ) @@ -272,7 +283,6 @@ def viewport_options_for_preview_animation(): return { "visualStyleMode": "defaultshading", "viewportPreset": "highquality", - "percentSize": 100, "vpTexture": False, "dspGeometry": True, "dspShapes": False, @@ -288,18 +298,15 @@ def viewport_options_for_preview_animation(): } else: viewport_options = {} - viewport_options.update({"percentSize": 100}) - general_viewport = { + viewport_options["general_viewport"] = { "dspBkg": True, "dspGrid": False } - nitrous_viewport = { + viewport_options["nitrous_viewport"] = { "VisualStyleMode": "defaultshading", "ViewportPreset": "highquality", "UseTextureEnabled": False } - viewport_options["general_viewport"] = general_viewport - viewport_options["nitrous_viewport"] = nitrous_viewport viewport_options["vp_btn_mgr"] = { "EnableButtons": False} return viewport_options diff --git a/openpype/hosts/max/plugins/publish/collect_review.py b/openpype/hosts/max/plugins/publish/collect_review.py index ea606f8b9e..1f488f8180 100644 --- a/openpype/hosts/max/plugins/publish/collect_review.py +++ b/openpype/hosts/max/plugins/publish/collect_review.py @@ -32,6 +32,7 @@ class CollectReview(pyblish.api.InstancePlugin, "review_camera": camera_name, "frameStart": instance.data["frameStartHandle"], "frameEnd": instance.data["frameEndHandle"], + "percentSize": creator_attrs["percentSize"], "imageFormat": creator_attrs["imageFormat"], "keepImages": creator_attrs["keepImages"], "fps": instance.context.data["fps"], @@ -52,7 +53,6 @@ class CollectReview(pyblish.api.InstancePlugin, preview_data = { "vpStyle": creator_attrs["visualStyleMode"], "vpPreset": creator_attrs["viewportPreset"], - "percentSize": creator_attrs["percentSize"], "vpTextures": creator_attrs["vpTexture"], "dspGeometry": attr_values.get("dspGeometry"), "dspShapes": attr_values.get("dspShapes"), @@ -77,7 +77,6 @@ class CollectReview(pyblish.api.InstancePlugin, "UseTextureEnabled": creator_attrs["vpTexture"] } preview_data = { - "percentSize": creator_attrs["percentSize"], "general_viewport": general_viewport, "nitrous_viewport": nitrous_viewport, "vp_btn_mgr": {"EnableButtons": False} diff --git a/openpype/hosts/max/plugins/publish/extract_review_animation.py b/openpype/hosts/max/plugins/publish/extract_review_animation.py index ae7744ac19..99dc5c5cdc 100644 --- a/openpype/hosts/max/plugins/publish/extract_review_animation.py +++ b/openpype/hosts/max/plugins/publish/extract_review_animation.py @@ -33,6 +33,7 @@ class ExtractReviewAnimation(publish.Extractor): review_camera, start, end, + percentSize=instance.data["percentSize"], width=instance.data["review_width"], height=instance.data["review_height"], viewport_options=viewport_options) diff --git a/openpype/hosts/max/plugins/publish/extract_thumbnail.py b/openpype/hosts/max/plugins/publish/extract_thumbnail.py index e9d37d0be5..02fa75e032 100644 --- a/openpype/hosts/max/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/max/plugins/publish/extract_thumbnail.py @@ -29,6 +29,7 @@ class ExtractThumbnail(publish.Extractor): review_camera, start_frame=frame, end_frame=frame, + percentSize=instance.data["percentSize"], width=instance.data["review_width"], height=instance.data["review_height"], viewport_options=viewport_options) From e00e2ec271dafaf60ae8d9f37efc659231745db6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 23 Oct 2023 16:10:56 +0200 Subject: [PATCH 0492/1233] better origin data handling --- openpype/pipeline/create/context.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index f9e3f86652..3624c7155e 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -912,6 +912,12 @@ class CreatedInstance: # Create a copy of passed data to avoid changing them on the fly data = copy.deepcopy(data or {}) + + # Pop dictionary values that will be converted to objects to be able + # catch changes + orig_creator_attributes = data.pop("creator_attributes", None) or {} + orig_publish_attributes = data.pop("publish_attributes", None) or {} + # Store original value of passed data self._orig_data = copy.deepcopy(data) @@ -919,10 +925,6 @@ class CreatedInstance: data.pop("family", None) data.pop("subset", None) - # Pop dictionary values that will be converted to objects to be able - # catch changes - orig_creator_attributes = data.pop("creator_attributes", None) or {} - orig_publish_attributes = data.pop("publish_attributes", None) or {} # QUESTION Does it make sense to have data stored as ordered dict? self._data = collections.OrderedDict() @@ -1039,7 +1041,10 @@ class CreatedInstance: @property def origin_data(self): - return copy.deepcopy(self._orig_data) + output = copy.deepcopy(self._orig_data) + output["creator_attributes"] = self.creator_attributes.origin_data + output["publish_attributes"] = self.publish_attributes.origin_data + return output @property def creator_identifier(self): @@ -1095,7 +1100,7 @@ class CreatedInstance: def changes(self): """Calculate and return changes.""" - return TrackChangesItem(self._orig_data, self.data_to_store()) + return TrackChangesItem(self.origin_data, self.data_to_store()) def mark_as_stored(self): """Should be called when instance data are stored. @@ -1211,7 +1216,7 @@ class CreatedInstance: publish_attributes = self.publish_attributes.serialize_attributes() return { "data": self.data_to_store(), - "orig_data": copy.deepcopy(self._orig_data), + "orig_data": self.origin_data, "creator_attr_defs": creator_attr_defs, "publish_attributes": publish_attributes, "creator_label": self._creator_label, @@ -1251,7 +1256,7 @@ class CreatedInstance: creator_identifier=creator_identifier, creator_label=creator_label, group_label=group_label, - creator_attributes=creator_attr_defs + creator_attr_defs=creator_attr_defs ) obj._orig_data = serialized_data["orig_data"] obj.publish_attributes.deserialize_attributes(publish_attributes) From d16cb8767122f04372d2aaa8b9bafb94bbee099d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 23 Oct 2023 16:11:13 +0200 Subject: [PATCH 0493/1233] mark instances as saved after save changes --- openpype/pipeline/create/context.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 3624c7155e..fcc3c248e9 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -2336,6 +2336,10 @@ class CreateContext: identifier, label, exc_info, add_traceback ) ) + else: + for update_data in update_list: + instance = update_data.instance + instance.mark_as_stored() if failed_info: raise CreatorsSaveFailed(failed_info) From 31b4b579763dd81ad02c5d33bb662176291d2ef0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 23 Oct 2023 16:47:52 +0200 Subject: [PATCH 0494/1233] fix 'mark_as_stored' on 'PublishAttributes' --- openpype/pipeline/create/context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index fcc3c248e9..25f03ddd3b 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -758,7 +758,7 @@ class PublishAttributes: yield name def mark_as_stored(self): - self._origin_data = copy.deepcopy(self._data) + self._origin_data = copy.deepcopy(self.data_to_store()) def data_to_store(self): """Convert attribute values to "data to store".""" From 171dedb4f30d20507d67ac62c7272dffe0555432 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Mon, 23 Oct 2023 18:46:53 +0300 Subject: [PATCH 0495/1233] update collector order --- .../houdini/plugins/publish/collect_rop_frame_range.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py b/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py index a368e77ff7..23717561e2 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py +++ b/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py @@ -13,7 +13,9 @@ class CollectRopFrameRange(pyblish.api.InstancePlugin, """Collect all frames which would be saved from the ROP nodes""" hosts = ["houdini"] - order = pyblish.api.CollectorOrder + # This specific order value is used so that + # this plugin runs after CollectAnatomyInstanceData + order = pyblish.api.CollectorOrder + 0.5 label = "Collect RopNode Frame Range" use_asset_handles = True @@ -32,7 +34,7 @@ class CollectRopFrameRange(pyblish.api.InstancePlugin, attr_values = self.get_attr_values_from_data(instance.data) if attr_values.get("use_handles", self.use_asset_handles): - asset_data = instance.context.data["assetEntity"]["data"] + asset_data = instance.data["assetEntity"]["data"] handle_start = asset_data.get("handleStart", 0) handle_end = asset_data.get("handleEnd", 0) else: From 7bb81f7f6d712524c86c7bb414a0e8040132e104 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Mon, 23 Oct 2023 19:17:35 +0300 Subject: [PATCH 0496/1233] update collector order, follow BigRoys recommendation --- .../hosts/houdini/plugins/publish/collect_rop_frame_range.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py b/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py index 23717561e2..186244fedd 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py +++ b/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py @@ -15,7 +15,7 @@ class CollectRopFrameRange(pyblish.api.InstancePlugin, hosts = ["houdini"] # This specific order value is used so that # this plugin runs after CollectAnatomyInstanceData - order = pyblish.api.CollectorOrder + 0.5 + order = pyblish.api.CollectorOrder + 0.499 label = "Collect RopNode Frame Range" use_asset_handles = True From 218c7f61c647159df119b19db381ed25b983de58 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Mon, 23 Oct 2023 19:23:00 +0300 Subject: [PATCH 0497/1233] update collector order for render product-types --- openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py | 4 +++- openpype/hosts/houdini/plugins/publish/collect_karma_rop.py | 4 +++- openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py | 4 +++- .../hosts/houdini/plugins/publish/collect_redshift_rop.py | 4 +++- openpype/hosts/houdini/plugins/publish/collect_vray_rop.py | 4 +++- 5 files changed, 15 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py index 28389c3b31..b489f83b29 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py @@ -20,7 +20,9 @@ class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin): """ label = "Arnold ROP Render Products" - order = pyblish.api.CollectorOrder + 0.4 + # This specific order value is used so that + # this plugin runs after CollectRopFrameRange + order = pyblish.api.CollectorOrder + 0.4999 hosts = ["houdini"] families = ["arnold_rop"] diff --git a/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py b/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py index b66dcde13f..fe0b8711fc 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py @@ -24,7 +24,9 @@ class CollectKarmaROPRenderProducts(pyblish.api.InstancePlugin): """ label = "Karma ROP Render Products" - order = pyblish.api.CollectorOrder + 0.4 + # This specific order value is used so that + # this plugin runs after CollectRopFrameRange + order = pyblish.api.CollectorOrder + 0.4999 hosts = ["houdini"] families = ["karma_rop"] diff --git a/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py b/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py index 3b7cf59f32..cc412f30a1 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py @@ -24,7 +24,9 @@ class CollectMantraROPRenderProducts(pyblish.api.InstancePlugin): """ label = "Mantra ROP Render Products" - order = pyblish.api.CollectorOrder + 0.4 + # This specific order value is used so that + # this plugin runs after CollectRopFrameRange + order = pyblish.api.CollectorOrder + 0.4999 hosts = ["houdini"] families = ["mantra_rop"] diff --git a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py index ca171a91f9..deb9eac971 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py @@ -24,7 +24,9 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin): """ label = "Redshift ROP Render Products" - order = pyblish.api.CollectorOrder + 0.4 + # This specific order value is used so that + # this plugin runs after CollectRopFrameRange + order = pyblish.api.CollectorOrder + 0.4999 hosts = ["houdini"] families = ["redshift_rop"] diff --git a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py index b1ff4c1886..53072aebc6 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py @@ -24,7 +24,9 @@ class CollectVrayROPRenderProducts(pyblish.api.InstancePlugin): """ label = "VRay ROP Render Products" - order = pyblish.api.CollectorOrder + 0.4 + # This specific order value is used so that + # this plugin runs after CollectRopFrameRange + order = pyblish.api.CollectorOrder + 0.4999 hosts = ["houdini"] families = ["vray_rop"] From 18eedd478eb14d9405ff4a22d32654189c6016e6 Mon Sep 17 00:00:00 2001 From: Mustafa Taher Date: Mon, 23 Oct 2023 23:04:59 +0300 Subject: [PATCH 0498/1233] Update doc string Co-authored-by: Roy Nieterau --- openpype/hosts/houdini/api/lib.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index eab77ca19a..41cb4c76ee 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -572,10 +572,14 @@ def get_frame_data(node, handle_start=0, handle_end=0, log=None): """Get the frame data: start frame, end frame, steps, start frame with start handle and end frame with end handle. - This function uses Houdini node as the source of truth - therefore users are allowed to publish their desired frame range. + This function uses Houdini node's `trange`, `t1, `t2` and `t3` + parameters as the source of truth for the full inclusive frame + range to render, as such these are considered as the frame + range including the handles. - It also calculates frame start and end with handles. + The non-inclusive frame start and frame end without handles + are computed by subtracting the handles from the inclusive + frame range. Args: node(hou.Node) From 3761e3fc9a05a2fc978b6e34d696887fcf9e7a21 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Mon, 23 Oct 2023 23:06:43 +0300 Subject: [PATCH 0499/1233] resolve hound --- openpype/hosts/houdini/api/lib.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 41cb4c76ee..115f4f3c17 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -572,9 +572,9 @@ def get_frame_data(node, handle_start=0, handle_end=0, log=None): """Get the frame data: start frame, end frame, steps, start frame with start handle and end frame with end handle. - This function uses Houdini node's `trange`, `t1, `t2` and `t3` - parameters as the source of truth for the full inclusive frame - range to render, as such these are considered as the frame + This function uses Houdini node's `trange`, `t1, `t2` and `t3` + parameters as the source of truth for the full inclusive frame + range to render, as such these are considered as the frame range including the handles. The non-inclusive frame start and frame end without handles From a65a275c5fb06d411482577da825b0eeb1bfc7f3 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Mon, 23 Oct 2023 23:10:43 +0300 Subject: [PATCH 0500/1233] update doc string --- openpype/hosts/houdini/api/lib.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 115f4f3c17..cadeaa8ed4 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -582,10 +582,12 @@ def get_frame_data(node, handle_start=0, handle_end=0, log=None): frame range. Args: - node(hou.Node) - handle_start(int) - handle_end(int) - log(logging.Logger) + node (hou.Node): ROP node to retrieve frame range from, + the frame range is assumed to be the frame range + *including* the start and end handles. + handle_start (int): Start handles. + handle_end (int): End handles. + log (logging.Logger): Logger to log to. Returns: dict: frame data for start, end, steps, From 75864eee2130068092eac5da6cb8a08ed373819c Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 24 Oct 2023 12:55:18 +0800 Subject: [PATCH 0501/1233] clean up the job_argument code for max 2024 --- openpype/hosts/max/api/preview_animation.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/max/api/preview_animation.py b/openpype/hosts/max/api/preview_animation.py index eb832c1d1c..1bf99b86d0 100644 --- a/openpype/hosts/max/api/preview_animation.py +++ b/openpype/hosts/max/api/preview_animation.py @@ -108,13 +108,7 @@ def _render_preview_animation_max_2024( filepath = filepath.replace("\\", "/") preview_output = f"{filepath}..{ext}" frame_template = f"{filepath}.{{:04d}}.{ext}" - job_args = list() - default_option = f'CreatePreview filename:"{preview_output}"' - job_args.append(default_option) - output_res_option = f"outputAVI:false percentSize:{percent}" - job_args.append(output_res_option) - frame_range_options = f"start:{start} end:{end}" - job_args.append(frame_range_options) + job_args = [] for key, value in viewport_options.items(): if isinstance(value, bool): if value: @@ -141,10 +135,13 @@ def _render_preview_animation_max_2024( else: value = value.lower() job_args.append(f"{key}: #{value}") - auto_play_option = "autoPlay:false" - job_args.append(auto_play_option) - job_str = " ".join(job_args) - log.debug(job_str) + + job_str = ( + f'CreatePreview filename:"{preview_output}" outputAVI:false ' + f"percentSize:{percent} start:{start} end:{end} " + f"{' '.join(job_args)} " + "autoPlay:false" + ) rt.completeRedraw() rt.execute(job_str) # Return the created files From 4c837a6a9e0b859626cafe0e688572a5bb57175c Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 24 Oct 2023 15:15:18 +0800 Subject: [PATCH 0502/1233] restore the os.path.normpath in loader for test --- openpype/hosts/max/plugins/load/load_tycache.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_tycache.py b/openpype/hosts/max/plugins/load/load_tycache.py index a860ecd357..858297dd8e 100644 --- a/openpype/hosts/max/plugins/load/load_tycache.py +++ b/openpype/hosts/max/plugins/load/load_tycache.py @@ -1,3 +1,4 @@ +import os from openpype.hosts.max.api import lib, maintained_selection from openpype.hosts.max.api.lib import ( unique_namespace, @@ -23,7 +24,7 @@ class TyCacheLoader(load.LoaderPlugin): def load(self, context, name=None, namespace=None, data=None): """Load tyCache""" from pymxs import runtime as rt - filepath = self.filepath_from_context(context) + filepath = os.path.normpath(self.filepath_from_context(context)) obj = rt.tyCache() obj.filename = filepath @@ -46,8 +47,8 @@ class TyCacheLoader(load.LoaderPlugin): node_list = get_previous_loaded_object(node) update_custom_attribute_data(node, node_list) with maintained_selection(): - for prt in node_list: - prt.filename = path + for tyc in node_list: + tyc.filename = path lib.imprint(container["instance_node"], { "representation": str(representation["_id"]) }) From 8882eaf730ae91189ceae61b071187ba0e60d508 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 24 Oct 2023 10:08:29 +0200 Subject: [PATCH 0503/1233] skip before window show in AYON mode --- openpype/hosts/blender/api/ops.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openpype/hosts/blender/api/ops.py b/openpype/hosts/blender/api/ops.py index 0eb90eeff9..208c11cfe8 100644 --- a/openpype/hosts/blender/api/ops.py +++ b/openpype/hosts/blender/api/ops.py @@ -284,6 +284,8 @@ class LaunchLoader(LaunchQtApp): _tool_name = "loader" def before_window_show(self): + if AYON_SERVER_ENABLED: + return self._window.set_context( {"asset": get_current_asset_name()}, refresh=True @@ -309,6 +311,8 @@ class LaunchManager(LaunchQtApp): _tool_name = "sceneinventory" def before_window_show(self): + if AYON_SERVER_ENABLED: + return self._window.refresh() @@ -320,6 +324,8 @@ class LaunchLibrary(LaunchQtApp): _tool_name = "libraryloader" def before_window_show(self): + if AYON_SERVER_ENABLED: + return self._window.refresh() @@ -340,6 +346,8 @@ class LaunchWorkFiles(LaunchQtApp): return result def before_window_show(self): + if AYON_SERVER_ENABLED: + return self._window.root = str(Path( os.environ.get("AVALON_WORKDIR", ""), os.environ.get("AVALON_SCENEDIR", ""), From 35a53598542d3233bbd2f3d46b37e1b92c02ed5a Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 24 Oct 2023 16:18:14 +0800 Subject: [PATCH 0504/1233] docstring edit --- openpype/hosts/max/plugins/load/load_tycache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/load/load_tycache.py b/openpype/hosts/max/plugins/load/load_tycache.py index 858297dd8e..41ea267c3d 100644 --- a/openpype/hosts/max/plugins/load/load_tycache.py +++ b/openpype/hosts/max/plugins/load/load_tycache.py @@ -13,7 +13,7 @@ from openpype.pipeline import get_representation_path, load class TyCacheLoader(load.LoaderPlugin): - """Point Cloud Loader.""" + """TyCache Loader.""" families = ["tycache"] representations = ["tyc"] From d66709791b4e663e8d2b6a3c435913eafc8d4245 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 24 Oct 2023 10:02:57 +0100 Subject: [PATCH 0505/1233] Include grease pencil in review and thumbnails --- openpype/hosts/blender/api/capture.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/blender/api/capture.py b/openpype/hosts/blender/api/capture.py index 849f8ee629..67c1523393 100644 --- a/openpype/hosts/blender/api/capture.py +++ b/openpype/hosts/blender/api/capture.py @@ -148,13 +148,14 @@ def applied_view(window, camera, isolate=None, options=None): area.ui_type = "VIEW_3D" - meshes = [obj for obj in window.scene.objects if obj.type == "MESH"] + types = ["MESH", "GPENCIL"] + objects = [obj for obj in window.scene.objects if obj.type in types] if camera == "AUTO": space.region_3d.view_perspective = "ORTHO" - isolate_objects(window, isolate or meshes) + isolate_objects(window, isolate or objects) else: - isolate_objects(window, isolate or meshes) + isolate_objects(window, isolate or objects) space.camera = window.scene.objects.get(camera) space.region_3d.view_perspective = "CAMERA" From bd545bce39b7d9925866f4d52fcd1f508770658c Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 24 Oct 2023 10:27:25 +0100 Subject: [PATCH 0506/1233] Use set instead of list to check types Co-authored-by: Roy Nieterau --- openpype/hosts/blender/api/capture.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/blender/api/capture.py b/openpype/hosts/blender/api/capture.py index 67c1523393..bad6831143 100644 --- a/openpype/hosts/blender/api/capture.py +++ b/openpype/hosts/blender/api/capture.py @@ -148,7 +148,7 @@ def applied_view(window, camera, isolate=None, options=None): area.ui_type = "VIEW_3D" - types = ["MESH", "GPENCIL"] + types = {"MESH", "GPENCIL"} objects = [obj for obj in window.scene.objects if obj.type in types] if camera == "AUTO": From 1dfdeacd04721434231e2ab3f993a4715e8015da Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 24 Oct 2023 13:23:10 +0200 Subject: [PATCH 0507/1233] use 'open_current_requested' signal instead of missing 'save_as_requested' --- openpype/tools/ayon_workfiles/widgets/files_widget_workarea.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/ayon_workfiles/widgets/files_widget_workarea.py b/openpype/tools/ayon_workfiles/widgets/files_widget_workarea.py index e59b319459..3a8e90f933 100644 --- a/openpype/tools/ayon_workfiles/widgets/files_widget_workarea.py +++ b/openpype/tools/ayon_workfiles/widgets/files_widget_workarea.py @@ -334,7 +334,7 @@ class WorkAreaFilesWidget(QtWidgets.QWidget): def _on_mouse_double_click(self, event): if event.button() == QtCore.Qt.LeftButton: - self.save_as_requested.emit() + self.open_current_requested.emit() def _on_context_menu(self, point): index = self._view.indexAt(point) From 038cdfa80e84052f303bc7c0739c5986c0dc7b04 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 24 Oct 2023 19:34:56 +0800 Subject: [PATCH 0508/1233] allows users to enable custom attributes for abc depending on their preferences and supsended refresh for abc & obj extractor for optimization --- openpype/hosts/max/api/lib.py | 13 ++++++ .../hosts/max/plugins/create/create_camera.py | 18 ++++++++ .../hosts/max/plugins/create/create_model.py | 18 ++++++++ .../max/plugins/create/create_pointcache.py | 18 ++++++++ .../max/plugins/publish/extract_camera_abc.py | 39 ++++++++---------- .../max/plugins/publish/extract_camera_fbx.py | 3 -- .../plugins/publish/extract_max_scene_raw.py | 1 - .../max/plugins/publish/extract_model.py | 41 +++++++++---------- .../max/plugins/publish/extract_model_fbx.py | 4 -- .../max/plugins/publish/extract_model_obj.py | 27 ++++++------ .../max/plugins/publish/extract_pointcache.py | 37 ++++++++--------- 11 files changed, 134 insertions(+), 85 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 8b70b3ced7..f02e3372b0 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -497,3 +497,16 @@ def get_plugins() -> list: plugin_info_list.append(plugin_info) return plugin_info_list + + +@contextlib.contextmanager +def suspended_refresh(): + rt.disableSceneRedraw() + rt.suspendEditing() + + try: + yield + + finally: + rt.enableSceneRedraw() + rt.resumeEditing() diff --git a/openpype/hosts/max/plugins/create/create_camera.py b/openpype/hosts/max/plugins/create/create_camera.py index 804d629ec7..8277581d31 100644 --- a/openpype/hosts/max/plugins/create/create_camera.py +++ b/openpype/hosts/max/plugins/create/create_camera.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- """Creator plugin for creating camera.""" from openpype.hosts.max.api import plugin +from openpype.lib import BoolDef class CreateCamera(plugin.MaxCreator): @@ -9,3 +10,20 @@ class CreateCamera(plugin.MaxCreator): label = "Camera" family = "camera" icon = "gear" + + def create(self, subset_name, instance_data, pre_create_data): + instance_data["custom_attrs"] = pre_create_data.get( + "custom_attrs") + + super(CreateCamera, self).create( + subset_name, + instance_data, + pre_create_data) + + def get_pre_create_attr_defs(self): + attrs = super().get_pre_create_attr_defs() + return attrs + [ + BoolDef("custom_attrs", + label="Custom Attributes", + default=False), + ] diff --git a/openpype/hosts/max/plugins/create/create_model.py b/openpype/hosts/max/plugins/create/create_model.py index fc09d475ef..9c459a184e 100644 --- a/openpype/hosts/max/plugins/create/create_model.py +++ b/openpype/hosts/max/plugins/create/create_model.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- """Creator plugin for model.""" from openpype.hosts.max.api import plugin +from openpype.lib import BoolDef class CreateModel(plugin.MaxCreator): @@ -9,3 +10,20 @@ class CreateModel(plugin.MaxCreator): label = "Model" family = "model" icon = "gear" + + def create(self, subset_name, instance_data, pre_create_data): + instance_data["custom_attrs"] = pre_create_data.get( + "custom_attrs") + + super(CreateModel, self).create( + subset_name, + instance_data, + pre_create_data) + + def get_pre_create_attr_defs(self): + attrs = super().get_pre_create_attr_defs() + return attrs + [ + BoolDef("custom_attrs", + label="Custom Attributes", + default=False), + ] diff --git a/openpype/hosts/max/plugins/create/create_pointcache.py b/openpype/hosts/max/plugins/create/create_pointcache.py index c2d11f4c32..2790ceeefc 100644 --- a/openpype/hosts/max/plugins/create/create_pointcache.py +++ b/openpype/hosts/max/plugins/create/create_pointcache.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- """Creator plugin for creating pointcache alembics.""" from openpype.hosts.max.api import plugin +from openpype.lib import BoolDef class CreatePointCache(plugin.MaxCreator): @@ -9,3 +10,20 @@ class CreatePointCache(plugin.MaxCreator): label = "Point Cache" family = "pointcache" icon = "gear" + + def create(self, subset_name, instance_data, pre_create_data): + instance_data["custom_attrs"] = pre_create_data.get( + "custom_attrs") + + super(CreatePointCache, self).create( + subset_name, + instance_data, + pre_create_data) + + def get_pre_create_attr_defs(self): + attrs = super().get_pre_create_attr_defs() + return attrs + [ + BoolDef("custom_attrs", + label="Custom Attributes", + default=False), + ] diff --git a/openpype/hosts/max/plugins/publish/extract_camera_abc.py b/openpype/hosts/max/plugins/publish/extract_camera_abc.py index b1918c53e0..d7c2e6a557 100644 --- a/openpype/hosts/max/plugins/publish/extract_camera_abc.py +++ b/openpype/hosts/max/plugins/publish/extract_camera_abc.py @@ -4,6 +4,7 @@ import pyblish.api from pymxs import runtime as rt from openpype.hosts.max.api import maintained_selection +from openpype.hosts.max.api.lib import suspended_refresh from openpype.pipeline import OptionalPyblishPluginMixin, publish @@ -22,33 +23,29 @@ class ExtractCameraAlembic(publish.Extractor, OptionalPyblishPluginMixin): start = float(instance.data.get("frameStartHandle", 1)) end = float(instance.data.get("frameEndHandle", 1)) - self.log.info("Extracting Camera ...") - stagingdir = self.staging_dir(instance) filename = "{name}.abc".format(**instance.data) path = os.path.join(stagingdir, filename) - # We run the render - self.log.info(f"Writing alembic '{filename}' to '{stagingdir}'") + with suspended_refresh(): + rt.AlembicExport.ArchiveType = rt.Name("ogawa") + rt.AlembicExport.CoordinateSystem = rt.Name("maya") + rt.AlembicExport.StartFrame = start + rt.AlembicExport.EndFrame = end + rt.AlembicExport.CustomAttributes = instance.data.get( + "custom_attrs", False) - rt.AlembicExport.ArchiveType = rt.Name("ogawa") - rt.AlembicExport.CoordinateSystem = rt.Name("maya") - rt.AlembicExport.StartFrame = start - rt.AlembicExport.EndFrame = end - rt.AlembicExport.CustomAttributes = True + with maintained_selection(): + # select and export + node_list = instance.data["members"] + rt.Select(node_list) + rt.ExportFile( + path, + rt.Name("noPrompt"), + selectedOnly=True, + using=rt.AlembicExport, + ) - with maintained_selection(): - # select and export - node_list = instance.data["members"] - rt.Select(node_list) - rt.ExportFile( - path, - rt.Name("noPrompt"), - selectedOnly=True, - using=rt.AlembicExport, - ) - - self.log.info("Performing Extraction ...") if "representations" not in instance.data: instance.data["representations"] = [] diff --git a/openpype/hosts/max/plugins/publish/extract_camera_fbx.py b/openpype/hosts/max/plugins/publish/extract_camera_fbx.py index 537c88eb4d..4b5631b05f 100644 --- a/openpype/hosts/max/plugins/publish/extract_camera_fbx.py +++ b/openpype/hosts/max/plugins/publish/extract_camera_fbx.py @@ -20,13 +20,10 @@ class ExtractCameraFbx(publish.Extractor, OptionalPyblishPluginMixin): if not self.is_active(instance.data): return - self.log.debug("Extracting Camera ...") stagingdir = self.staging_dir(instance) filename = "{name}.fbx".format(**instance.data) filepath = os.path.join(stagingdir, filename) - self.log.info(f"Writing fbx file '{filename}' to '{filepath}'") - rt.FBXExporterSetParam("Animation", True) rt.FBXExporterSetParam("Cameras", True) rt.FBXExporterSetParam("AxisConversionMethod", "Animation") 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 a7a889c587..791cc65fcd 100644 --- a/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py +++ b/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py @@ -26,7 +26,6 @@ class ExtractMaxSceneRaw(publish.Extractor, OptionalPyblishPluginMixin): filename = "{name}.max".format(**instance.data) max_path = os.path.join(stagingdir, filename) - self.log.info("Writing max file '%s' to '%s'" % (filename, max_path)) if "representations" not in instance.data: instance.data["representations"] = [] diff --git a/openpype/hosts/max/plugins/publish/extract_model.py b/openpype/hosts/max/plugins/publish/extract_model.py index 38f4848c5e..36e789d180 100644 --- a/openpype/hosts/max/plugins/publish/extract_model.py +++ b/openpype/hosts/max/plugins/publish/extract_model.py @@ -3,6 +3,7 @@ import pyblish.api from openpype.pipeline import publish, OptionalPyblishPluginMixin from pymxs import runtime as rt from openpype.hosts.max.api import maintained_selection +from openpype.hosts.max.api.lib import suspended_refresh class ExtractModel(publish.Extractor, OptionalPyblishPluginMixin): @@ -20,34 +21,30 @@ class ExtractModel(publish.Extractor, OptionalPyblishPluginMixin): if not self.is_active(instance.data): return - self.log.debug("Extracting Geometry ...") - stagingdir = self.staging_dir(instance) filename = "{name}.abc".format(**instance.data) filepath = os.path.join(stagingdir, filename) - # We run the render - self.log.info("Writing alembic '%s' to '%s'" % (filename, stagingdir)) + with suspended_refresh(): + rt.AlembicExport.ArchiveType = rt.name("ogawa") + rt.AlembicExport.CoordinateSystem = rt.name("maya") + rt.AlembicExport.CustomAttributes = instance.data.get( + "custom_attrs", False) + rt.AlembicExport.UVs = True + rt.AlembicExport.VertexColors = True + rt.AlembicExport.PreserveInstances = True - rt.AlembicExport.ArchiveType = rt.name("ogawa") - rt.AlembicExport.CoordinateSystem = rt.name("maya") - rt.AlembicExport.CustomAttributes = True - rt.AlembicExport.UVs = True - rt.AlembicExport.VertexColors = True - rt.AlembicExport.PreserveInstances = True + with maintained_selection(): + # select and export + node_list = instance.data["members"] + rt.Select(node_list) + rt.exportFile( + filepath, + rt.name("noPrompt"), + selectedOnly=True, + using=rt.AlembicExport, + ) - with maintained_selection(): - # select and export - node_list = instance.data["members"] - rt.Select(node_list) - rt.exportFile( - filepath, - rt.name("noPrompt"), - selectedOnly=True, - using=rt.AlembicExport, - ) - - self.log.info("Performing Extraction ...") if "representations" not in instance.data: instance.data["representations"] = [] diff --git a/openpype/hosts/max/plugins/publish/extract_model_fbx.py b/openpype/hosts/max/plugins/publish/extract_model_fbx.py index fd48ed5007..6c42fd5364 100644 --- a/openpype/hosts/max/plugins/publish/extract_model_fbx.py +++ b/openpype/hosts/max/plugins/publish/extract_model_fbx.py @@ -20,12 +20,9 @@ class ExtractModelFbx(publish.Extractor, OptionalPyblishPluginMixin): if not self.is_active(instance.data): return - self.log.debug("Extracting Geometry ...") - stagingdir = self.staging_dir(instance) filename = "{name}.fbx".format(**instance.data) filepath = os.path.join(stagingdir, filename) - self.log.info("Writing FBX '%s' to '%s'" % (filepath, stagingdir)) rt.FBXExporterSetParam("Animation", False) rt.FBXExporterSetParam("Cameras", False) @@ -46,7 +43,6 @@ class ExtractModelFbx(publish.Extractor, OptionalPyblishPluginMixin): using=rt.FBXEXP, ) - self.log.info("Performing Extraction ...") if "representations" not in instance.data: instance.data["representations"] = [] diff --git a/openpype/hosts/max/plugins/publish/extract_model_obj.py b/openpype/hosts/max/plugins/publish/extract_model_obj.py index a5d9ad6597..8464353164 100644 --- a/openpype/hosts/max/plugins/publish/extract_model_obj.py +++ b/openpype/hosts/max/plugins/publish/extract_model_obj.py @@ -3,6 +3,7 @@ import pyblish.api from openpype.pipeline import publish, OptionalPyblishPluginMixin from pymxs import runtime as rt from openpype.hosts.max.api import maintained_selection +from openpype.hosts.max.api.lib import suspended_refresh from openpype.pipeline.publish import KnownPublishError @@ -21,25 +22,21 @@ class ExtractModelObj(publish.Extractor, OptionalPyblishPluginMixin): if not self.is_active(instance.data): return - self.log.debug("Extracting Geometry ...") - stagingdir = self.staging_dir(instance) filename = "{name}.obj".format(**instance.data) filepath = os.path.join(stagingdir, filename) - self.log.info("Writing OBJ '%s' to '%s'" % (filepath, stagingdir)) - - self.log.info("Performing Extraction ...") - with maintained_selection(): - # select and export - node_list = instance.data["members"] - rt.Select(node_list) - rt.exportFile( - filepath, - rt.name("noPrompt"), - selectedOnly=True, - using=rt.ObjExp, - ) + with suspended_refresh(): + with maintained_selection(): + # select and export + node_list = instance.data["members"] + rt.Select(node_list) + rt.exportFile( + filepath, + rt.name("noPrompt"), + selectedOnly=True, + using=rt.ObjExp, + ) if not os.path.exists(filepath): raise KnownPublishError( "File {} wasn't produced by 3ds max, please check the logs.") diff --git a/openpype/hosts/max/plugins/publish/extract_pointcache.py b/openpype/hosts/max/plugins/publish/extract_pointcache.py index c3de623bc0..71dc667d7e 100644 --- a/openpype/hosts/max/plugins/publish/extract_pointcache.py +++ b/openpype/hosts/max/plugins/publish/extract_pointcache.py @@ -42,6 +42,7 @@ import pyblish.api from openpype.pipeline import publish from pymxs import runtime as rt from openpype.hosts.max.api import maintained_selection +from openpype.hosts.max.api.lib import suspended_refresh class ExtractAlembic(publish.Extractor): @@ -54,30 +55,28 @@ class ExtractAlembic(publish.Extractor): start = float(instance.data.get("frameStartHandle", 1)) end = float(instance.data.get("frameEndHandle", 1)) - self.log.debug("Extracting pointcache ...") - parent_dir = self.staging_dir(instance) file_name = "{name}.abc".format(**instance.data) path = os.path.join(parent_dir, file_name) - # We run the render - self.log.info("Writing alembic '%s' to '%s'" % (file_name, parent_dir)) + with suspended_refresh(): + rt.AlembicExport.ArchiveType = rt.name("ogawa") + rt.AlembicExport.CoordinateSystem = rt.name("maya") + rt.AlembicExport.StartFrame = start + rt.AlembicExport.EndFrame = end + rt.AlembicExport.CustomAttributes = instance.data.get( + "custom_attrs", False) - rt.AlembicExport.ArchiveType = rt.name("ogawa") - rt.AlembicExport.CoordinateSystem = rt.name("maya") - rt.AlembicExport.StartFrame = start - rt.AlembicExport.EndFrame = end - - with maintained_selection(): - # select and export - node_list = instance.data["members"] - rt.Select(node_list) - rt.exportFile( - path, - rt.name("noPrompt"), - selectedOnly=True, - using=rt.AlembicExport, - ) + with maintained_selection(): + # select and export + node_list = instance.data["members"] + rt.Select(node_list) + rt.exportFile( + path, + rt.name("noPrompt"), + selectedOnly=True, + using=rt.AlembicExport, + ) if "representations" not in instance.data: instance.data["representations"] = [] From a320c30d437b59c0d2cd83c07e5b477f9cdb8291 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 24 Oct 2023 19:52:54 +0800 Subject: [PATCH 0509/1233] update docstring --- openpype/hosts/max/api/lib.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index f02e3372b0..051076c418 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -501,6 +501,8 @@ def get_plugins() -> list: @contextlib.contextmanager def suspended_refresh(): + """Suspended refresh for scene and modify panel redraw. + """ rt.disableSceneRedraw() rt.suspendEditing() From 507adfa9368f24d9eaff47e5d386cad08671ab9e Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Mon, 5 Jun 2023 18:39:32 +0200 Subject: [PATCH 0510/1233] Refactor: Blender new publisher --- openpype/hosts/blender/api/ops.py | 6 ++++- openpype/hosts/blender/api/plugin.py | 25 ++++++++++++++++--- .../blender/plugins/create/create_action.py | 2 +- .../plugins/create/create_animation.py | 2 +- .../blender/plugins/create/create_camera.py | 2 +- .../blender/plugins/create/create_layout.py | 2 +- .../blender/plugins/create/create_model.py | 2 +- .../plugins/create/create_pointcache.py | 2 +- .../blender/plugins/create/create_review.py | 2 +- .../blender/plugins/create/create_rig.py | 2 +- .../blender/plugins/publish/extract_blend.py | 5 +++- 11 files changed, 39 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/blender/api/ops.py b/openpype/hosts/blender/api/ops.py index 208c11cfe8..22c590d4bd 100644 --- a/openpype/hosts/blender/api/ops.py +++ b/openpype/hosts/blender/api/ops.py @@ -275,6 +275,10 @@ class LaunchCreator(LaunchQtApp): def before_window_show(self): self._window.refresh() + def execute(self, context): + host_tools.show_publisher(tab="create") + return {"FINISHED"} + class LaunchLoader(LaunchQtApp): """Launch Avalon Loader.""" @@ -299,7 +303,7 @@ class LaunchPublisher(LaunchQtApp): bl_label = "Publish..." def execute(self, context): - host_tools.show_publish() + host_tools.show_publisher(tab="publish") return {"FINISHED"} diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index fb87d08cce..221c8d8936 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -6,7 +6,8 @@ from typing import Dict, List, Optional import bpy from openpype.pipeline import ( - LegacyCreator, + Creator, + CreatedInstance, LoaderPlugin, ) from .pipeline import AVALON_CONTAINERS @@ -134,10 +135,11 @@ def deselect_all(): bpy.context.view_layer.objects.active = active -class Creator(LegacyCreator): - """Base class for Creator plug-ins.""" +class BlenderCreator(Creator): + """Base class for Blender Creator plug-ins.""" defaults = ['Main'] + # Deprecated? def process(self): collection = bpy.data.collections.new(name=self.data["subset"]) bpy.context.scene.collection.children.link(collection) @@ -150,6 +152,23 @@ class Creator(LegacyCreator): return collection + def create( + self, subset_name: str, instance_data: dict, pre_create_data: dict + ): + """Override abstract method from Creator. + Create new instance and store it. + + Args: + subset_name(str): Subset name of created instance. + instance_data(dict): Base data for instance. + pre_create_data(dict): Data based on pre creation attributes. + Those may affect how creator works. + """ + instance = CreatedInstance( + self.family, subset_name, instance_data + ) + + class Loader(LoaderPlugin): """Base class for Loader plug-ins.""" diff --git a/openpype/hosts/blender/plugins/create/create_action.py b/openpype/hosts/blender/plugins/create/create_action.py index 0203ba74c0..effbccd430 100644 --- a/openpype/hosts/blender/plugins/create/create_action.py +++ b/openpype/hosts/blender/plugins/create/create_action.py @@ -7,7 +7,7 @@ import openpype.hosts.blender.api.plugin from openpype.hosts.blender.api import lib -class CreateAction(openpype.hosts.blender.api.plugin.Creator): +class CreateAction(openpype.hosts.blender.api.plugin.BlenderCreator): """Action output for character rigs""" name = "actionMain" diff --git a/openpype/hosts/blender/plugins/create/create_animation.py b/openpype/hosts/blender/plugins/create/create_animation.py index bc2840952b..1b9bbcacd9 100644 --- a/openpype/hosts/blender/plugins/create/create_animation.py +++ b/openpype/hosts/blender/plugins/create/create_animation.py @@ -7,7 +7,7 @@ from openpype.hosts.blender.api import plugin, lib, ops from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES -class CreateAnimation(plugin.Creator): +class CreateAnimation(plugin.BlenderCreator): """Animation output for character rigs""" name = "animationMain" diff --git a/openpype/hosts/blender/plugins/create/create_camera.py b/openpype/hosts/blender/plugins/create/create_camera.py index 7a770a3e77..c72f2b92ff 100644 --- a/openpype/hosts/blender/plugins/create/create_camera.py +++ b/openpype/hosts/blender/plugins/create/create_camera.py @@ -7,7 +7,7 @@ from openpype.hosts.blender.api import plugin, lib, ops from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES -class CreateCamera(plugin.Creator): +class CreateCamera(plugin.BlenderCreator): """Polygonal static geometry""" name = "cameraMain" diff --git a/openpype/hosts/blender/plugins/create/create_layout.py b/openpype/hosts/blender/plugins/create/create_layout.py index 73ed683256..ba75df6735 100644 --- a/openpype/hosts/blender/plugins/create/create_layout.py +++ b/openpype/hosts/blender/plugins/create/create_layout.py @@ -7,7 +7,7 @@ from openpype.hosts.blender.api import plugin, lib, ops from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES -class CreateLayout(plugin.Creator): +class CreateLayout(plugin.BlenderCreator): """Layout output for character rigs""" name = "layoutMain" diff --git a/openpype/hosts/blender/plugins/create/create_model.py b/openpype/hosts/blender/plugins/create/create_model.py index 51fc6683f6..a7e71622ea 100644 --- a/openpype/hosts/blender/plugins/create/create_model.py +++ b/openpype/hosts/blender/plugins/create/create_model.py @@ -7,7 +7,7 @@ from openpype.hosts.blender.api import plugin, lib, ops from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES -class CreateModel(plugin.Creator): +class CreateModel(plugin.BlenderCreator): """Polygonal static geometry""" name = "modelMain" diff --git a/openpype/hosts/blender/plugins/create/create_pointcache.py b/openpype/hosts/blender/plugins/create/create_pointcache.py index 65cf18472d..0555d956de 100644 --- a/openpype/hosts/blender/plugins/create/create_pointcache.py +++ b/openpype/hosts/blender/plugins/create/create_pointcache.py @@ -7,7 +7,7 @@ from openpype.hosts.blender.api import plugin, lib, ops from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES -class CreatePointcache(plugin.Creator): +class CreatePointcache(plugin.BlenderCreator): """Polygonal static geometry""" name = "pointcacheMain" diff --git a/openpype/hosts/blender/plugins/create/create_review.py b/openpype/hosts/blender/plugins/create/create_review.py index 914f249891..58c26e0324 100644 --- a/openpype/hosts/blender/plugins/create/create_review.py +++ b/openpype/hosts/blender/plugins/create/create_review.py @@ -7,7 +7,7 @@ from openpype.hosts.blender.api import plugin, lib, ops from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES -class CreateReview(plugin.Creator): +class CreateReview(plugin.BlenderCreator): """Single baked camera""" name = "reviewDefault" diff --git a/openpype/hosts/blender/plugins/create/create_rig.py b/openpype/hosts/blender/plugins/create/create_rig.py index 08cc46ee3e..7a0393f0ba 100644 --- a/openpype/hosts/blender/plugins/create/create_rig.py +++ b/openpype/hosts/blender/plugins/create/create_rig.py @@ -7,7 +7,7 @@ from openpype.hosts.blender.api import plugin, lib, ops from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES -class CreateRig(plugin.Creator): +class CreateRig(plugin.BlenderCreator): """Artist-friendly rig with controls to direct motion""" name = "rigMain" diff --git a/openpype/hosts/blender/plugins/publish/extract_blend.py b/openpype/hosts/blender/plugins/publish/extract_blend.py index d4f26b4f3c..fba9a861a0 100644 --- a/openpype/hosts/blender/plugins/publish/extract_blend.py +++ b/openpype/hosts/blender/plugins/publish/extract_blend.py @@ -5,7 +5,7 @@ import bpy from openpype.pipeline import publish -class ExtractBlend(publish.Extractor): +class ExtractBlend(publish.Extractor, publish.OptionalPyblishPluginMixin): """Extract a blend file.""" label = "Extract Blend" @@ -16,6 +16,9 @@ class ExtractBlend(publish.Extractor): def process(self, instance): # Define extract output file path + if not self.is_active(instance.data): + return + stagingdir = self.staging_dir(instance) filename = f"{instance.name}.blend" filepath = os.path.join(stagingdir, filename) From e06dfbb8e8dbf1a92f2164a0b929968384d1aaff Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Tue, 6 Jun 2023 16:09:09 +0200 Subject: [PATCH 0511/1233] draft implementation for blender creator refactor --- openpype/hosts/blender/api/plugin.py | 75 ++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index 221c8d8936..39d0f5e662 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -152,6 +152,29 @@ class BlenderCreator(Creator): return collection + @staticmethod + def cache_subsets(shared_data): + """Cache instances for Creators shared data. + + Create `blender_cached_subsets` key when needed in shared data and + fill it with all collected instances from the scene under its + respective creator identifiers. + + If legacy instances are detected in the scene, create + `blender_cached_legacy_subsets` key and fill it with + all legacy subsets from this family as a value. # key or value? + + Args: + shared_data(Dict[str, Any]): Shared data. + + Return: + Dict[str, Any]: Shared data with cached subsets. + """ + if not shared_data.get('blender_cached_subsets'): + cache = {} + cache_legacy = {} + + def create( self, subset_name: str, instance_data: dict, pre_create_data: dict ): @@ -168,6 +191,58 @@ class BlenderCreator(Creator): self.family, subset_name, instance_data ) + collection = bpy.data.collections.new(name=self.data['subset']) + bpy.context.scene.collection.children.link(collection) + + if (self.options or {}).get("useSelection"): + for obj in get_selection(): + collection.objects.link(obj) + + + def collect_instances(self): + """Override abstract method from BaseCreator. + Collect existing instances related to this creator plugin.""" + for ( + instance_data in self.cache_subsets( + self.collection_shared_data + ).get('blender_cached_subsets') + ): + # Process only instances that were created by this creator + creator_id = instance_data.get('creator_identifier') + + if creator_id == self.identifier: + # Create instance object from existing data + instance = CreatedInstance.from_existing( + instance_data, self + ) + + # Add instance to create context + self.add_instance_to_context(instance) + + + def update_instances(self, update_list): + """Override abstract method from BaseCreator. + Store changes of existing instances so they can be recollected. + + Args: + update_list(List[UpdateData]): Changed instances + and their changes, as a list of tuples.""" + for created_instance, _changes in update_list: + data = created_instance.data_to_store() + + # TODO + + + def remove_instances(self, instances: List[CreatedInstance]): + """Override abstract method from BaseCreator. + Method called when instances are removed. + + Args: + instance(List[CreatedInstance]): Instance objects to remove. + """ + for instance in instances: + self._remove_instance_from_context(instance) + class Loader(LoaderPlugin): """Base class for Loader plug-ins.""" From 3c198694a9be7dace95e5e027b604f23e3f2bdfe Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Tue, 6 Jun 2023 18:03:02 +0200 Subject: [PATCH 0512/1233] blender creator cache subsets --- openpype/hosts/blender/api/plugin.py | 59 +++++++++++++++++++++++++--- 1 file changed, 53 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index 39d0f5e662..9a982c45e7 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -10,7 +10,11 @@ from openpype.pipeline import ( CreatedInstance, LoaderPlugin, ) -from .pipeline import AVALON_CONTAINERS +from .pipeline import ( + AVALON_CONTAINERS, + AVALON_INSTANCES, + AVALON_PROPERTY, +) from .ops import ( MainThreadItem, execute_in_main_thread @@ -174,6 +178,44 @@ class BlenderCreator(Creator): cache = {} cache_legacy = {} + avalon_instances = bpy.data.collections.get(AVALON_INSTANCES) + if avalon_instances: + for obj in bpy.data.collections.get(AVALON_INSTANCES).objects: + avalon_prop = obj.get(AVALON_PROPERTY, {}) + if avalon_prop.get('id') == 'pyblish.avalon.instance': + creator_id = avalon_prop.get('creator_identifier') + + if creator_id: + # Creator instance + cache.setdefault(creator_id, []).append( + avalon_prop + ) + else: + family = avalon_prop.get('family') + if family: + # Legacy creator instance + cache_legacy.setdefault(family, []).append( + avalon_prop + ) + + for col in bpy.data.collections: + avalon_prop = col.get(AVALON_PROPERTY, {}) + if avalon_prop.get('id') == 'pyblish.avalon.instance': + creaor_id = avalon_prop.get('creator_identifier') + + if creator_id: + # Creator instance + cache.setdefault(creator_id, []).append(avalon_prop) + else: + family = avalon_prop.get('family') + if family: + cache_legacy.setdefault(family, []) + if family: + # Legacy creator instance + cache_legacy.setdefault(family, []).append( + avalon_prop + ) + def create( self, subset_name: str, instance_data: dict, pre_create_data: dict @@ -202,11 +244,16 @@ class BlenderCreator(Creator): def collect_instances(self): """Override abstract method from BaseCreator. Collect existing instances related to this creator plugin.""" - for ( - instance_data in self.cache_subsets( - self.collection_shared_data - ).get('blender_cached_subsets') - ): + + # Cache subsets in shared data + self.cache_subsets(self.collection_shared_data) + + # Get cached subsets + cached_subsets = self.collection_shared_data.get('blender_cached_subsets') + if not cached_subsets: + return + + for instance_data in cached_subsets: # Process only instances that were created by this creator creator_id = instance_data.get('creator_identifier') From d4c030e77d38b7db3fc50c98d0d43ca6d6beae8e Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Fri, 16 Jun 2023 11:16:42 +0200 Subject: [PATCH 0513/1233] Add identifiers to blender creators. Fix wrong method name --- openpype/hosts/blender/api/plugin.py | 2 +- openpype/hosts/blender/plugins/create/create_action.py | 1 + openpype/hosts/blender/plugins/create/create_animation.py | 1 + openpype/hosts/blender/plugins/create/create_camera.py | 1 + openpype/hosts/blender/plugins/create/create_layout.py | 1 + openpype/hosts/blender/plugins/create/create_model.py | 1 + openpype/hosts/blender/plugins/create/create_pointcache.py | 1 + openpype/hosts/blender/plugins/create/create_review.py | 1 + openpype/hosts/blender/plugins/create/create_rig.py | 1 + 9 files changed, 9 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index 9a982c45e7..c13af363c5 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -264,7 +264,7 @@ class BlenderCreator(Creator): ) # Add instance to create context - self.add_instance_to_context(instance) + self._add_instance_to_context(instance) def update_instances(self, update_list): diff --git a/openpype/hosts/blender/plugins/create/create_action.py b/openpype/hosts/blender/plugins/create/create_action.py index effbccd430..5f4ded3688 100644 --- a/openpype/hosts/blender/plugins/create/create_action.py +++ b/openpype/hosts/blender/plugins/create/create_action.py @@ -10,6 +10,7 @@ from openpype.hosts.blender.api import lib class CreateAction(openpype.hosts.blender.api.plugin.BlenderCreator): """Action output for character rigs""" + identifier = "io.openpype.creators.blender.action" name = "actionMain" label = "Action" family = "action" diff --git a/openpype/hosts/blender/plugins/create/create_animation.py b/openpype/hosts/blender/plugins/create/create_animation.py index 1b9bbcacd9..277c588610 100644 --- a/openpype/hosts/blender/plugins/create/create_animation.py +++ b/openpype/hosts/blender/plugins/create/create_animation.py @@ -10,6 +10,7 @@ from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES class CreateAnimation(plugin.BlenderCreator): """Animation output for character rigs""" + identifier = "io.openpype.creators.blender.animation" name = "animationMain" label = "Animation" family = "animation" diff --git a/openpype/hosts/blender/plugins/create/create_camera.py b/openpype/hosts/blender/plugins/create/create_camera.py index c72f2b92ff..9086c44c5f 100644 --- a/openpype/hosts/blender/plugins/create/create_camera.py +++ b/openpype/hosts/blender/plugins/create/create_camera.py @@ -10,6 +10,7 @@ from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES class CreateCamera(plugin.BlenderCreator): """Polygonal static geometry""" + identifier = "io.openpype.creators.blender.camera" name = "cameraMain" label = "Camera" family = "camera" diff --git a/openpype/hosts/blender/plugins/create/create_layout.py b/openpype/hosts/blender/plugins/create/create_layout.py index ba75df6735..ae567e6495 100644 --- a/openpype/hosts/blender/plugins/create/create_layout.py +++ b/openpype/hosts/blender/plugins/create/create_layout.py @@ -10,6 +10,7 @@ from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES class CreateLayout(plugin.BlenderCreator): """Layout output for character rigs""" + identifier = "io.openpype.creators.blender.layout" name = "layoutMain" label = "Layout" family = "layout" diff --git a/openpype/hosts/blender/plugins/create/create_model.py b/openpype/hosts/blender/plugins/create/create_model.py index a7e71622ea..46196ab383 100644 --- a/openpype/hosts/blender/plugins/create/create_model.py +++ b/openpype/hosts/blender/plugins/create/create_model.py @@ -10,6 +10,7 @@ from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES class CreateModel(plugin.BlenderCreator): """Polygonal static geometry""" + identifier = "io.openpype.creators.blender.model" name = "modelMain" label = "Model" family = "model" diff --git a/openpype/hosts/blender/plugins/create/create_pointcache.py b/openpype/hosts/blender/plugins/create/create_pointcache.py index 0555d956de..4c434202c7 100644 --- a/openpype/hosts/blender/plugins/create/create_pointcache.py +++ b/openpype/hosts/blender/plugins/create/create_pointcache.py @@ -10,6 +10,7 @@ from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES class CreatePointcache(plugin.BlenderCreator): """Polygonal static geometry""" + identifier = "io.openpype.creators.blender.pointcache" name = "pointcacheMain" label = "Point Cache" family = "pointcache" diff --git a/openpype/hosts/blender/plugins/create/create_review.py b/openpype/hosts/blender/plugins/create/create_review.py index 58c26e0324..87774aed7a 100644 --- a/openpype/hosts/blender/plugins/create/create_review.py +++ b/openpype/hosts/blender/plugins/create/create_review.py @@ -10,6 +10,7 @@ from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES class CreateReview(plugin.BlenderCreator): """Single baked camera""" + identifier = "io.openpype.creators.blender.review" name = "reviewDefault" label = "Review" family = "review" diff --git a/openpype/hosts/blender/plugins/create/create_rig.py b/openpype/hosts/blender/plugins/create/create_rig.py index 7a0393f0ba..84924a659b 100644 --- a/openpype/hosts/blender/plugins/create/create_rig.py +++ b/openpype/hosts/blender/plugins/create/create_rig.py @@ -10,6 +10,7 @@ from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES class CreateRig(plugin.BlenderCreator): """Artist-friendly rig with controls to direct motion""" + identifier = "io.openpype.creators.blender.rig" name = "rigMain" label = "Rig" family = "rig" From efa294defcc625acdd36e7948bde3f75b15beb88 Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Fri, 16 Jun 2023 17:54:28 +0200 Subject: [PATCH 0514/1233] fixed wrong variable name --- openpype/hosts/blender/api/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index c13af363c5..02436e3583 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -201,7 +201,7 @@ class BlenderCreator(Creator): for col in bpy.data.collections: avalon_prop = col.get(AVALON_PROPERTY, {}) if avalon_prop.get('id') == 'pyblish.avalon.instance': - creaor_id = avalon_prop.get('creator_identifier') + creator_id = avalon_prop.get('creator_identifier') if creator_id: # Creator instance From b681173dc5b2ee34862ffa41dbc3fc452c682a56 Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Mon, 3 Jul 2023 17:44:36 +0200 Subject: [PATCH 0515/1233] draft blenderhost class implementation --- openpype/hosts/blender/api/__init__.py | 2 + openpype/hosts/blender/api/pipeline.py | 41 ++++++++++++++++++- .../blender/blender_addon/startup/init.py | 4 +- 3 files changed, 44 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/blender/api/__init__.py b/openpype/hosts/blender/api/__init__.py index e15f1193a5..ce2b444997 100644 --- a/openpype/hosts/blender/api/__init__.py +++ b/openpype/hosts/blender/api/__init__.py @@ -10,6 +10,7 @@ from .pipeline import ( ls, publish, containerise, + BlenderHost, ) from .plugin import ( @@ -47,6 +48,7 @@ __all__ = [ "ls", "publish", "containerise", + "BlenderHost", "Creator", "Loader", diff --git a/openpype/hosts/blender/api/pipeline.py b/openpype/hosts/blender/api/pipeline.py index 84af0904f0..935981da86 100644 --- a/openpype/hosts/blender/api/pipeline.py +++ b/openpype/hosts/blender/api/pipeline.py @@ -6,10 +6,14 @@ from typing import Callable, Dict, Iterator, List, Optional import bpy from . import lib -from . import ops +from . import ops, properties import pyblish.api +from openpype.host import( + HostBase, + IPublishHost, +) from openpype.client import get_asset_by_name from openpype.pipeline import ( schema, @@ -47,6 +51,39 @@ IS_HEADLESS = bpy.app.background log = Logger.get_logger(__name__) +class BlenderHost(HostBase, IPublishHost): + name = "blender" + + def install(self): + """Override install method from HostBase. + Install Blender host functionality.""" + install() + + def ls(self) -> Iterator: + """List containers from active Blender scene.""" + return ls() + + def get_context_data(self): + """Override abstract method from IPublishHost. + Get global data related to creation-publishing from workfile. + + Returns: + dict: Context data stored using 'update_context_data'. + """ + return bpy.context.scene.openpype_context + + def update_context_data(self, data, changes): + """Override abstract method from IPublishHost. + Store global context data to workfile. + + Args: + data (dict): New data as are. + changes (dict): Only data that has been changed. Each value has + tuple with '(, )' value. + """ + bpy.context.scene.openpype_context.update(data) + + def pype_excepthook_handler(*args): traceback.print_exception(*args) @@ -72,6 +109,7 @@ def install(): if not IS_HEADLESS: ops.register() + properties.register() def uninstall(): @@ -86,6 +124,7 @@ def uninstall(): if not IS_HEADLESS: ops.unregister() + properties.unregister() def show_message(title, message): diff --git a/openpype/hosts/blender/blender_addon/startup/init.py b/openpype/hosts/blender/blender_addon/startup/init.py index 8dbff8a91d..603691675d 100644 --- a/openpype/hosts/blender/blender_addon/startup/init.py +++ b/openpype/hosts/blender/blender_addon/startup/init.py @@ -1,9 +1,9 @@ from openpype.pipeline import install_host -from openpype.hosts.blender import api +from openpype.hosts.blender.api import BlenderHost def register(): - install_host(api) + install_host(BlenderHost()) def unregister(): From 3b7ed1d71f86f6bd04029cc6ec0e369aff94c365 Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Tue, 4 Jul 2023 10:55:04 +0200 Subject: [PATCH 0516/1233] Fix update_container_data error --- openpype/hosts/blender/api/pipeline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/blender/api/pipeline.py b/openpype/hosts/blender/api/pipeline.py index 935981da86..968c70089b 100644 --- a/openpype/hosts/blender/api/pipeline.py +++ b/openpype/hosts/blender/api/pipeline.py @@ -81,7 +81,7 @@ class BlenderHost(HostBase, IPublishHost): changes (dict): Only data that has been changed. Each value has tuple with '(, )' value. """ - bpy.context.scene.openpype_context.update(data) + bpy.context.scene.openpype_context |= data def pype_excepthook_handler(*args): From 7bdbf0252211167c065aa8f3e622d48e725c0bf8 Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Tue, 4 Jul 2023 16:07:44 +0200 Subject: [PATCH 0517/1233] Added properties to blender host --- openpype/hosts/blender/api/properties.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 openpype/hosts/blender/api/properties.py diff --git a/openpype/hosts/blender/api/properties.py b/openpype/hosts/blender/api/properties.py new file mode 100644 index 0000000000..ffc1dea733 --- /dev/null +++ b/openpype/hosts/blender/api/properties.py @@ -0,0 +1,23 @@ +import bpy +from bpy.utils import register_classes_factory + +class OpenpypeContext(bpy.types.PropertyGroup): + pass + +classes = [OpenpypeContext] + +factory_register, factory_unregister = register_classes_factory(classes) + +def register(): + """Register the properties.""" + factory_register() + + bpy.types.Scene.openpype_context = bpy.props.CollectionProperty( + name="OpenPype Context", type=OpenpypeContext, options={"HIDDEN"} + ) + +def unregister(): + """Unregister the properties.""" + factory_unregister() + + del bpy.types.Scene.openpype_context From d9960e84d44dab94ac5fec6bf45315ce79ab5d37 Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Tue, 4 Jul 2023 17:54:40 +0200 Subject: [PATCH 0518/1233] Fixed errors during create --- openpype/hosts/blender/api/pipeline.py | 2 +- openpype/hosts/blender/api/plugin.py | 6 +++--- openpype/hosts/blender/api/properties.py | 9 +++++---- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/blender/api/pipeline.py b/openpype/hosts/blender/api/pipeline.py index 968c70089b..935981da86 100644 --- a/openpype/hosts/blender/api/pipeline.py +++ b/openpype/hosts/blender/api/pipeline.py @@ -81,7 +81,7 @@ class BlenderHost(HostBase, IPublishHost): changes (dict): Only data that has been changed. Each value has tuple with '(, )' value. """ - bpy.context.scene.openpype_context |= data + bpy.context.scene.openpype_context.update(data) def pype_excepthook_handler(*args): diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index 02436e3583..91068244c5 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -230,13 +230,13 @@ class BlenderCreator(Creator): Those may affect how creator works. """ instance = CreatedInstance( - self.family, subset_name, instance_data + self.family, subset_name, instance_data, self ) - collection = bpy.data.collections.new(name=self.data['subset']) + collection = bpy.data.collections.new(name=subset_name) bpy.context.scene.collection.children.link(collection) - if (self.options or {}).get("useSelection"): + if pre_create_data.get("useSelection"): for obj in get_selection(): collection.objects.link(obj) diff --git a/openpype/hosts/blender/api/properties.py b/openpype/hosts/blender/api/properties.py index ffc1dea733..c6b5ffe011 100644 --- a/openpype/hosts/blender/api/properties.py +++ b/openpype/hosts/blender/api/properties.py @@ -4,7 +4,7 @@ from bpy.utils import register_classes_factory class OpenpypeContext(bpy.types.PropertyGroup): pass -classes = [OpenpypeContext] +classes = [] # [OpenpypeContext] factory_register, factory_unregister = register_classes_factory(classes) @@ -12,9 +12,10 @@ def register(): """Register the properties.""" factory_register() - bpy.types.Scene.openpype_context = bpy.props.CollectionProperty( - name="OpenPype Context", type=OpenpypeContext, options={"HIDDEN"} - ) + bpy.types.Scene.openpype_context = {} + # bpy.types.Scene.openpype_context = bpy.props.CollectionProperty( + # name="OpenPype Context", type=OpenpypeContext, options={"HIDDEN"} + # ) def unregister(): """Unregister the properties.""" From cf66d12ef20feb2e1dcf326d4888434a3c506bdb Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Wed, 5 Jul 2023 17:36:42 +0200 Subject: [PATCH 0519/1233] Fixes to base creator, creators compatibility --- openpype/hosts/blender/api/plugin.py | 3 + .../blender/plugins/create/create_action.py | 27 +++++++++ .../plugins/create/create_animation.py | 47 +++++++++++++++- .../blender/plugins/create/create_camera.py | 56 ++++++++++++++++++- .../blender/plugins/create/create_layout.py | 45 ++++++++++++++- .../blender/plugins/create/create_model.py | 45 ++++++++++++++- .../plugins/create/create_pointcache.py | 22 ++++++++ .../blender/plugins/create/create_review.py | 43 +++++++++++++- .../blender/plugins/create/create_rig.py | 45 ++++++++++++++- 9 files changed, 315 insertions(+), 18 deletions(-) diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index 91068244c5..153897cb9a 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -216,6 +216,8 @@ class BlenderCreator(Creator): avalon_prop ) + return shared_data + def create( self, subset_name: str, instance_data: dict, pre_create_data: dict @@ -232,6 +234,7 @@ class BlenderCreator(Creator): instance = CreatedInstance( self.family, subset_name, instance_data, self ) + self._add_instance_to_context(instance) collection = bpy.data.collections.new(name=subset_name) bpy.context.scene.collection.children.link(collection) diff --git a/openpype/hosts/blender/plugins/create/create_action.py b/openpype/hosts/blender/plugins/create/create_action.py index 5f4ded3688..32e924d758 100644 --- a/openpype/hosts/blender/plugins/create/create_action.py +++ b/openpype/hosts/blender/plugins/create/create_action.py @@ -16,6 +16,33 @@ class CreateAction(openpype.hosts.blender.api.plugin.BlenderCreator): family = "action" icon = "male" + def create( + self, subset_name: str, instance_data: dict, pre_create_data: dict + ): + + name = openpype.hosts.blender.api.plugin.asset_name( + instance_data["asset"], subset_name + ) + collection = bpy.data.collections.new(name=name) + bpy.context.scene.collection.children.link(collection) + instance_data['task'] = get_current_task_name() + lib.imprint(collection, instance_data) + + if pre_create_data.get("useSelection"): + for obj in lib.get_selection(): + if (obj.animation_data is not None + and obj.animation_data.action is not None): + + empty_obj = bpy.data.objects.new(name=name, + object_data=None) + empty_obj.animation_data_create() + empty_obj.animation_data.action = obj.animation_data.action + empty_obj.animation_data.action.name = name + collection.objects.link(empty_obj) + + return collection + + # Deprecated def process(self): asset = self.data["asset"] diff --git a/openpype/hosts/blender/plugins/create/create_animation.py b/openpype/hosts/blender/plugins/create/create_animation.py index 277c588610..12478fd7e5 100644 --- a/openpype/hosts/blender/plugins/create/create_animation.py +++ b/openpype/hosts/blender/plugins/create/create_animation.py @@ -16,12 +16,53 @@ class CreateAnimation(plugin.BlenderCreator): family = "animation" icon = "male" - def process(self): + def create( + self, subset_name: str, instance_data: dict, pre_create_data: dict + ): """ Run the creator on Blender main thread""" - mti = ops.MainThreadItem(self._process) + mti = ops.MainThreadItem( + self._process, subset_name, instance_data, pre_create_data + ) ops.execute_in_main_thread(mti) - def _process(self): + def _process( + self, subset_name: str, instance_data: dict, pre_create_data: dict + ): + # Get Instance Container or create it if it does not exist + instances = bpy.data.collections.get(AVALON_INSTANCES) + if not instances: + instances = bpy.data.collections.new(name=AVALON_INSTANCES) + bpy.context.scene.collection.children.link(instances) + + # Create instance object + # name = self.name + # if not name: + name = plugin.asset_name(instance_data["asset"], subset_name) + # asset_group = bpy.data.objects.new(name=name, object_data=None) + # asset_group.empty_display_type = 'SINGLE_ARROW' + asset_group = bpy.data.collections.new(name=name) + instances.children.link(asset_group) + instance_data['task'] = get_current_task_name() + lib.imprint(asset_group, instance_data) + + if pre_create_data.get("useSelection"): + selected = lib.get_selection() + for obj in selected: + asset_group.objects.link(obj) + elif pre_create_data.get("asset_group"): + obj = (self.options or {}).get("asset_group") + asset_group.objects.link(obj) + + return asset_group + + # Deprecated + def process(self): + """ Run the creator on Blender main thread""" + mti = ops.MainThreadItem(self._process_legacy) + ops.execute_in_main_thread(mti) + + # Deprecated + def _process_legacy(self): # Get Instance Container or create it if it does not exist instances = bpy.data.collections.get(AVALON_INSTANCES) if not instances: diff --git a/openpype/hosts/blender/plugins/create/create_camera.py b/openpype/hosts/blender/plugins/create/create_camera.py index 9086c44c5f..6b9d1b7f73 100644 --- a/openpype/hosts/blender/plugins/create/create_camera.py +++ b/openpype/hosts/blender/plugins/create/create_camera.py @@ -16,12 +16,62 @@ class CreateCamera(plugin.BlenderCreator): family = "camera" icon = "video-camera" - def process(self): + def create( + self, subset_name: str, instance_data: dict, pre_create_data: dict + ): """ Run the creator on Blender main thread""" - mti = ops.MainThreadItem(self._process) + mti = ops.MainThreadItem( + self._process, subset_name, instance_data, pre_create_data + ) ops.execute_in_main_thread(mti) - def _process(self): + def _process( + self, subset_name: str, instance_data: dict, pre_create_data: dict + ): + # Get Instance Container or create it if it does not exist + instances = bpy.data.collections.get(AVALON_INSTANCES) + if not instances: + instances = bpy.data.collections.new(name=AVALON_INSTANCES) + bpy.context.scene.collection.children.link(instances) + + # Create instance object + name = plugin.asset_name(instance_data["asset"], subset_name) + + asset_group = bpy.data.objects.new(name=name, object_data=None) + asset_group.empty_display_type = 'SINGLE_ARROW' + instances.objects.link(asset_group) + instance_data['task'] = get_current_task_name() + lib.imprint(asset_group, instance_data) + + if pre_create_data.get("useSelection"): + bpy.context.view_layer.objects.active = asset_group + selected = lib.get_selection() + for obj in selected: + obj.select_set(True) + selected.append(asset_group) + bpy.ops.object.parent_set(keep_transform=True) + else: + plugin.deselect_all() + camera = bpy.data.cameras.new(subset_name) + camera_obj = bpy.data.objects.new(subset_name, camera) + + instances.objects.link(camera_obj) + + camera_obj.select_set(True) + asset_group.select_set(True) + bpy.context.view_layer.objects.active = asset_group + bpy.ops.object.parent_set(keep_transform=True) + + return asset_group + + # Deprecated + def process(self): + """ Run the creator on Blender main thread""" + mti = ops.MainThreadItem(self._process_legacy) + ops.execute_in_main_thread(mti) + + # Deprecated + def _process_legacy(self): # Get Instance Container or create it if it does not exist instances = bpy.data.collections.get(AVALON_INSTANCES) if not instances: diff --git a/openpype/hosts/blender/plugins/create/create_layout.py b/openpype/hosts/blender/plugins/create/create_layout.py index ae567e6495..24aa277349 100644 --- a/openpype/hosts/blender/plugins/create/create_layout.py +++ b/openpype/hosts/blender/plugins/create/create_layout.py @@ -16,12 +16,51 @@ class CreateLayout(plugin.BlenderCreator): family = "layout" icon = "cubes" - def process(self): + def create( + self, subset_name: str, instance_data: dict, pre_create_data: dict + ): """ Run the creator on Blender main thread""" - mti = ops.MainThreadItem(self._process) + mti = ops.MainThreadItem( + self._process, subset_name, instance_data, pre_create_data + ) ops.execute_in_main_thread(mti) - def _process(self): + def _process( + self, subset_name: str, instance_data: dict, pre_create_data: dict + ): + # Get Instance Container or create it if it does not exist + instances = bpy.data.collections.get(AVALON_INSTANCES) + if not instances: + instances = bpy.data.collections.new(name=AVALON_INSTANCES) + bpy.context.scene.collection.children.link(instances) + + # Create instance object + name = plugin.asset_name(instance_data["asset"], subset_name) + asset_group = bpy.data.objects.new(name=name, object_data=None) + asset_group.empty_display_type = 'SINGLE_ARROW' + instances.objects.link(asset_group) + instance_data['task'] = get_current_task_name() + lib.imprint(asset_group, instance_data) + + # Add selected objects to instance + if pre_create_data.get("useSelection"): + bpy.context.view_layer.objects.active = asset_group + selected = lib.get_selection() + for obj in selected: + obj.select_set(True) + selected.append(asset_group) + bpy.ops.object.parent_set(keep_transform=True) + + return asset_group + + # Deprecated + def process(self): + """ Run the creator on Blender main thread""" + mti = ops.MainThreadItem(self._process_legacy) + ops.execute_in_main_thread(mti) + + # Deprecated + def _process_legacy(self): # Get Instance Container or create it if it does not exist instances = bpy.data.collections.get(AVALON_INSTANCES) if not instances: diff --git a/openpype/hosts/blender/plugins/create/create_model.py b/openpype/hosts/blender/plugins/create/create_model.py index 46196ab383..2e713dd661 100644 --- a/openpype/hosts/blender/plugins/create/create_model.py +++ b/openpype/hosts/blender/plugins/create/create_model.py @@ -16,12 +16,51 @@ class CreateModel(plugin.BlenderCreator): family = "model" icon = "cube" - def process(self): + def create( + self, subset_name: str, instance_data: dict, pre_create_data: dict + ): """ Run the creator on Blender main thread""" - mti = ops.MainThreadItem(self._process) + mti = ops.MainThreadItem( + self._process, subset_name, instance_data, pre_create_data + ) ops.execute_in_main_thread(mti) - def _process(self): + def _process( + self, subset_name: str, instance_data: dict, pre_create_data: dict + ): + # Get Instance Container or create it if it does not exist + instances = bpy.data.collections.get(AVALON_INSTANCES) + if not instances: + instances = bpy.data.collections.new(name=AVALON_INSTANCES) + bpy.context.scene.collection.children.link(instances) + + # Create instance object + name = plugin.asset_name(instance_data["asset"], subset_name) + asset_group = bpy.data.objects.new(name=name, object_data=None) + asset_group.empty_display_type = 'SINGLE_ARROW' + instances.objects.link(asset_group) + instance_data['task'] = get_current_task_name() + lib.imprint(asset_group, instance_data) + + # Add selected objects to instance + if pre_create_data.get("useSelection"): + bpy.context.view_layer.objects.active = asset_group + selected = lib.get_selection() + for obj in selected: + obj.select_set(True) + selected.append(asset_group) + bpy.ops.object.parent_set(keep_transform=True) + + return asset_group + + # Deprecated + def process(self): + """ Run the creator on Blender main thread""" + mti = ops.MainThreadItem(self._process_legacy) + ops.execute_in_main_thread(mti) + + # Deprecated + def _process_legacy(self): # Get Instance Container or create it if it does not exist instances = bpy.data.collections.get(AVALON_INSTANCES) if not instances: diff --git a/openpype/hosts/blender/plugins/create/create_pointcache.py b/openpype/hosts/blender/plugins/create/create_pointcache.py index 4c434202c7..5932315bc8 100644 --- a/openpype/hosts/blender/plugins/create/create_pointcache.py +++ b/openpype/hosts/blender/plugins/create/create_pointcache.py @@ -16,6 +16,28 @@ class CreatePointcache(plugin.BlenderCreator): family = "pointcache" icon = "gears" + def create( + self, subset_name: str, instance_data: dict, pre_create_data: dict + ): + + name = openpype.hosts.blender.api.plugin.asset_name( + instance_data["asset"], subset_name + ) + collection = bpy.data.collections.new(name=name) + bpy.context.scene.collection.children.link(collection) + instance_data['task'] = get_current_task_name() + lib.imprint(collection, instance_data) + + if pre_create_data.get("useSelection"): + objects = lib.get_selection() + for obj in objects: + collection.objects.link(obj) + if obj.type == 'EMPTY': + objects.extend(obj.children) + + return collection + + # Deprecated def process(self): """ Run the creator on Blender main thread""" mti = ops.MainThreadItem(self._process) diff --git a/openpype/hosts/blender/plugins/create/create_review.py b/openpype/hosts/blender/plugins/create/create_review.py index 87774aed7a..4b5cfb4c35 100644 --- a/openpype/hosts/blender/plugins/create/create_review.py +++ b/openpype/hosts/blender/plugins/create/create_review.py @@ -16,12 +16,49 @@ class CreateReview(plugin.BlenderCreator): family = "review" icon = "video-camera" - def process(self): + def create( + self, subset_name: str, instance_data: dict, pre_create_data: dict + ): """ Run the creator on Blender main thread""" - mti = ops.MainThreadItem(self._process) + mti = ops.MainThreadItem( + self._process, subset_name, instance_data, pre_create_data + ) ops.execute_in_main_thread(mti) - def _process(self): + def _process( + self, subset_name: str, instance_data: dict, pre_create_data: dict + ): + # Get Instance Container or create it if it does not exist + instances = bpy.data.collections.get(AVALON_INSTANCES) + if not instances: + instances = bpy.data.collections.new(name=AVALON_INSTANCES) + bpy.context.scene.collection.children.link(instances) + + # Create instance object + name = plugin.asset_name(instance_data["asset"], subset_name) + asset_group = bpy.data.collections.new(name=name) + instances.children.link(asset_group) + instance_data['task'] = get_current_task_name() + lib.imprint(asset_group, instance_data) + + if pre_create_data.get("useSelection"): + selected = lib.get_selection() + for obj in selected: + asset_group.objects.link(obj) + elif pre_create_data.get("asset_group"): + obj = (self.options or {}).get("asset_group") + asset_group.objects.link(obj) + + return asset_group + + # Deprecated + def process(self): + """ Run the creator on Blender main thread""" + mti = ops.MainThreadItem(self._process_legacy) + ops.execute_in_main_thread(mti) + + # Deprecated + def _process_legacy(self): # Get Instance Container or create it if it does not exist instances = bpy.data.collections.get(AVALON_INSTANCES) if not instances: diff --git a/openpype/hosts/blender/plugins/create/create_rig.py b/openpype/hosts/blender/plugins/create/create_rig.py index 84924a659b..5ebf952d4a 100644 --- a/openpype/hosts/blender/plugins/create/create_rig.py +++ b/openpype/hosts/blender/plugins/create/create_rig.py @@ -16,12 +16,51 @@ class CreateRig(plugin.BlenderCreator): family = "rig" icon = "wheelchair" - def process(self): + def create( + self, subset_name: str, instance_data: dict, pre_create_data: dict + ): """ Run the creator on Blender main thread""" - mti = ops.MainThreadItem(self._process) + mti = ops.MainThreadItem( + self._process, subset_name, instance_data, pre_create_data + ) ops.execute_in_main_thread(mti) - def _process(self): + def _process( + self, subset_name: str, instance_data: dict, pre_create_data: dict + ): + # Get Instance Container or create it if it does not exist + instances = bpy.data.collections.get(AVALON_INSTANCES) + if not instances: + instances = bpy.data.collections.new(name=AVALON_INSTANCES) + bpy.context.scene.collection.children.link(instances) + + # Create instance object + name = plugin.asset_name(instance_data["asset"], subset_name) + asset_group = bpy.data.objects.new(name=name, object_data=None) + asset_group.empty_display_type = 'SINGLE_ARROW' + instances.objects.link(asset_group) + instance_data['task'] = get_current_task_name() + lib.imprint(asset_group, instance_data) + + # Add selected objects to instance + if pre_create_data.get("useSelection"): + bpy.context.view_layer.objects.active = asset_group + selected = lib.get_selection() + for obj in selected: + obj.select_set(True) + selected.append(asset_group) + bpy.ops.object.parent_set(keep_transform=True) + + return asset_group + + # Deprecated + def process(self): + """ Run the creator on Blender main thread""" + mti = ops.MainThreadItem(self._process_legacy) + ops.execute_in_main_thread(mti) + + # Deprecated + def _process_legacy(self): # Get Instance Container or create it if it does not exist instances = bpy.data.collections.get(AVALON_INSTANCES) if not instances: From e0e7c965856795776e86cfab83a582b2fd4c134d Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Mon, 10 Jul 2023 14:59:31 +0200 Subject: [PATCH 0520/1233] blender creator instances added to context --- openpype/hosts/blender/plugins/create/create_action.py | 5 ++++- openpype/hosts/blender/plugins/create/create_animation.py | 6 +++++- openpype/hosts/blender/plugins/create/create_camera.py | 6 +++++- openpype/hosts/blender/plugins/create/create_layout.py | 6 +++++- openpype/hosts/blender/plugins/create/create_model.py | 6 +++++- openpype/hosts/blender/plugins/create/create_pointcache.py | 7 +++++-- openpype/hosts/blender/plugins/create/create_review.py | 6 +++++- openpype/hosts/blender/plugins/create/create_rig.py | 6 +++++- 8 files changed, 39 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/blender/plugins/create/create_action.py b/openpype/hosts/blender/plugins/create/create_action.py index 32e924d758..6c2c6d98ce 100644 --- a/openpype/hosts/blender/plugins/create/create_action.py +++ b/openpype/hosts/blender/plugins/create/create_action.py @@ -2,7 +2,7 @@ import bpy -from openpype.pipeline import get_current_task_name +from openpype.pipeline import get_current_task_name, CreatedInstance import openpype.hosts.blender.api.plugin from openpype.hosts.blender.api import lib @@ -19,6 +19,9 @@ class CreateAction(openpype.hosts.blender.api.plugin.BlenderCreator): def create( self, subset_name: str, instance_data: dict, pre_create_data: dict ): + self._add_instance_to_context( + CreatedInstance(self.family, subset_name, instance_data, self) + ) name = openpype.hosts.blender.api.plugin.asset_name( instance_data["asset"], subset_name diff --git a/openpype/hosts/blender/plugins/create/create_animation.py b/openpype/hosts/blender/plugins/create/create_animation.py index 12478fd7e5..ec77569889 100644 --- a/openpype/hosts/blender/plugins/create/create_animation.py +++ b/openpype/hosts/blender/plugins/create/create_animation.py @@ -2,7 +2,7 @@ import bpy -from openpype.pipeline import get_current_task_name +from openpype.pipeline import get_current_task_name, CreatedInstance from openpype.hosts.blender.api import plugin, lib, ops from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES @@ -20,6 +20,10 @@ class CreateAnimation(plugin.BlenderCreator): self, subset_name: str, instance_data: dict, pre_create_data: dict ): """ Run the creator on Blender main thread""" + self._add_instance_to_context( + CreatedInstance(self.family, subset_name, instance_data, self) + ) + mti = ops.MainThreadItem( self._process, subset_name, instance_data, pre_create_data ) diff --git a/openpype/hosts/blender/plugins/create/create_camera.py b/openpype/hosts/blender/plugins/create/create_camera.py index 6b9d1b7f73..55ea07d90f 100644 --- a/openpype/hosts/blender/plugins/create/create_camera.py +++ b/openpype/hosts/blender/plugins/create/create_camera.py @@ -2,7 +2,7 @@ import bpy -from openpype.pipeline import get_current_task_name +from openpype.pipeline import get_current_task_name, CreatedInstance from openpype.hosts.blender.api import plugin, lib, ops from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES @@ -20,6 +20,10 @@ class CreateCamera(plugin.BlenderCreator): self, subset_name: str, instance_data: dict, pre_create_data: dict ): """ Run the creator on Blender main thread""" + self._add_instance_to_context( + CreatedInstance(self.family, subset_name, instance_data, self) + ) + mti = ops.MainThreadItem( self._process, subset_name, instance_data, pre_create_data ) diff --git a/openpype/hosts/blender/plugins/create/create_layout.py b/openpype/hosts/blender/plugins/create/create_layout.py index 24aa277349..60812d7fc9 100644 --- a/openpype/hosts/blender/plugins/create/create_layout.py +++ b/openpype/hosts/blender/plugins/create/create_layout.py @@ -2,7 +2,7 @@ import bpy -from openpype.pipeline import get_current_task_name +from openpype.pipeline import get_current_task_name, CreatedInstance from openpype.hosts.blender.api import plugin, lib, ops from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES @@ -20,6 +20,10 @@ class CreateLayout(plugin.BlenderCreator): self, subset_name: str, instance_data: dict, pre_create_data: dict ): """ Run the creator on Blender main thread""" + self._add_instance_to_context( + CreatedInstance(self.family, subset_name, instance_data, self) + ) + mti = ops.MainThreadItem( self._process, subset_name, instance_data, pre_create_data ) diff --git a/openpype/hosts/blender/plugins/create/create_model.py b/openpype/hosts/blender/plugins/create/create_model.py index 2e713dd661..59bbae5bc4 100644 --- a/openpype/hosts/blender/plugins/create/create_model.py +++ b/openpype/hosts/blender/plugins/create/create_model.py @@ -2,7 +2,7 @@ import bpy -from openpype.pipeline import get_current_task_name +from openpype.pipeline import get_current_task_name, CreatedInstance from openpype.hosts.blender.api import plugin, lib, ops from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES @@ -20,6 +20,10 @@ class CreateModel(plugin.BlenderCreator): self, subset_name: str, instance_data: dict, pre_create_data: dict ): """ Run the creator on Blender main thread""" + self._add_instance_to_context( + CreatedInstance(self.family, subset_name, instance_data, self) + ) + mti = ops.MainThreadItem( self._process, subset_name, instance_data, pre_create_data ) diff --git a/openpype/hosts/blender/plugins/create/create_pointcache.py b/openpype/hosts/blender/plugins/create/create_pointcache.py index 5932315bc8..bb843c5a22 100644 --- a/openpype/hosts/blender/plugins/create/create_pointcache.py +++ b/openpype/hosts/blender/plugins/create/create_pointcache.py @@ -2,7 +2,7 @@ import bpy -from openpype.pipeline import get_current_task_name +from openpype.pipeline import get_current_task_name, CreatedInstance from openpype.hosts.blender.api import plugin, lib, ops from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES @@ -19,8 +19,11 @@ class CreatePointcache(plugin.BlenderCreator): def create( self, subset_name: str, instance_data: dict, pre_create_data: dict ): + self._add_instance_to_context( + CreatedInstance(self.family, subset_name, instance_data, self) + ) - name = openpype.hosts.blender.api.plugin.asset_name( + name = plugin.asset_name( instance_data["asset"], subset_name ) collection = bpy.data.collections.new(name=name) diff --git a/openpype/hosts/blender/plugins/create/create_review.py b/openpype/hosts/blender/plugins/create/create_review.py index 4b5cfb4c35..1191fb95f1 100644 --- a/openpype/hosts/blender/plugins/create/create_review.py +++ b/openpype/hosts/blender/plugins/create/create_review.py @@ -2,7 +2,7 @@ import bpy -from openpype.pipeline import get_current_task_name +from openpype.pipeline import get_current_task_name, CreatedInstance from openpype.hosts.blender.api import plugin, lib, ops from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES @@ -20,6 +20,10 @@ class CreateReview(plugin.BlenderCreator): self, subset_name: str, instance_data: dict, pre_create_data: dict ): """ Run the creator on Blender main thread""" + self._add_instance_to_context( + CreatedInstance(self.family, subset_name, instance_data, self) + ) + mti = ops.MainThreadItem( self._process, subset_name, instance_data, pre_create_data ) diff --git a/openpype/hosts/blender/plugins/create/create_rig.py b/openpype/hosts/blender/plugins/create/create_rig.py index 5ebf952d4a..752f3f7b18 100644 --- a/openpype/hosts/blender/plugins/create/create_rig.py +++ b/openpype/hosts/blender/plugins/create/create_rig.py @@ -2,7 +2,7 @@ import bpy -from openpype.pipeline import get_current_task_name +from openpype.pipeline import get_current_task_name, CreatedInstance from openpype.hosts.blender.api import plugin, lib, ops from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES @@ -20,6 +20,10 @@ class CreateRig(plugin.BlenderCreator): self, subset_name: str, instance_data: dict, pre_create_data: dict ): """ Run the creator on Blender main thread""" + self._add_instance_to_context( + CreatedInstance(self.family, subset_name, instance_data, self) + ) + mti = ops.MainThreadItem( self._process, subset_name, instance_data, pre_create_data ) From 6eae2c264d57b4ed5395de03950e09bc54ae7acb Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Wed, 12 Jul 2023 15:54:41 +0200 Subject: [PATCH 0521/1233] Add dict to imprint, type hints in blenderhost, imprint in create --- openpype/hosts/blender/api/lib.py | 2 +- openpype/hosts/blender/api/pipeline.py | 4 ++-- openpype/hosts/blender/api/plugin.py | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/blender/api/lib.py b/openpype/hosts/blender/api/lib.py index 9bb560c364..76bef09ceb 100644 --- a/openpype/hosts/blender/api/lib.py +++ b/openpype/hosts/blender/api/lib.py @@ -188,7 +188,7 @@ def imprint(node: bpy.types.bpy_struct_meta_idprop, data: Dict): # Support values evaluated at imprint value = value() - if not isinstance(value, (int, float, bool, str, list)): + if not isinstance(value, (int, float, bool, str, list, dict)): raise TypeError(f"Unsupported type: {type(value)}") imprint_data[key] = value diff --git a/openpype/hosts/blender/api/pipeline.py b/openpype/hosts/blender/api/pipeline.py index 935981da86..4c06b1959e 100644 --- a/openpype/hosts/blender/api/pipeline.py +++ b/openpype/hosts/blender/api/pipeline.py @@ -63,7 +63,7 @@ class BlenderHost(HostBase, IPublishHost): """List containers from active Blender scene.""" return ls() - def get_context_data(self): + def get_context_data(self) -> dict: """Override abstract method from IPublishHost. Get global data related to creation-publishing from workfile. @@ -72,7 +72,7 @@ class BlenderHost(HostBase, IPublishHost): """ return bpy.context.scene.openpype_context - def update_context_data(self, data, changes): + def update_context_data(self, data: dict, changes: dict): """Override abstract method from IPublishHost. Store global context data to workfile. diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index 153897cb9a..fe1e4ce81c 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -238,6 +238,7 @@ class BlenderCreator(Creator): collection = bpy.data.collections.new(name=subset_name) bpy.context.scene.collection.children.link(collection) + imprint(collection, instance_data) if pre_create_data.get("useSelection"): for obj in get_selection(): From 7617d1e0ad63042d39a3c626f6e614100d2bd879 Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Wed, 19 Jul 2023 20:28:45 +0200 Subject: [PATCH 0522/1233] Set shared data, edit loop in cached_subsets --- openpype/hosts/blender/api/plugin.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index fe1e4ce81c..97a3dfa6a6 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -216,6 +216,9 @@ class BlenderCreator(Creator): avalon_prop ) + shared_data["blender_cached_subsets"] = cache + shared_data["blender_cached_legacy_subsets"] = cache_legacy + return shared_data @@ -257,14 +260,17 @@ class BlenderCreator(Creator): if not cached_subsets: return - for instance_data in cached_subsets: + for instance_data in cached_subsets.get(self.identifier, []): # Process only instances that were created by this creator + data = dict() + for key, value in instance_data.items(): + data[key] = value creator_id = instance_data.get('creator_identifier') if creator_id == self.identifier: # Create instance object from existing data instance = CreatedInstance.from_existing( - instance_data, self + data, self ) # Add instance to create context From aeebd78cc71fb3edbe51157c60da849f62e727f3 Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Thu, 20 Jul 2023 15:31:23 +0200 Subject: [PATCH 0523/1233] Collect instances functional --- openpype/hosts/blender/api/plugin.py | 4 ++-- openpype/hosts/blender/plugins/create/create_action.py | 3 +++ openpype/hosts/blender/plugins/create/create_animation.py | 3 +++ openpype/hosts/blender/plugins/create/create_camera.py | 3 +++ openpype/hosts/blender/plugins/create/create_layout.py | 3 +++ openpype/hosts/blender/plugins/create/create_model.py | 3 +++ openpype/hosts/blender/plugins/create/create_pointcache.py | 3 +++ openpype/hosts/blender/plugins/create/create_review.py | 3 +++ openpype/hosts/blender/plugins/create/create_rig.py | 3 +++ 9 files changed, 26 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index 97a3dfa6a6..5dd352ff6c 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -262,10 +262,10 @@ class BlenderCreator(Creator): for instance_data in cached_subsets.get(self.identifier, []): # Process only instances that were created by this creator - data = dict() + data = {} for key, value in instance_data.items(): data[key] = value - creator_id = instance_data.get('creator_identifier') + creator_id = data.get('creator_identifier') if creator_id == self.identifier: # Create instance object from existing data diff --git a/openpype/hosts/blender/plugins/create/create_action.py b/openpype/hosts/blender/plugins/create/create_action.py index 6c2c6d98ce..2c0d248994 100644 --- a/openpype/hosts/blender/plugins/create/create_action.py +++ b/openpype/hosts/blender/plugins/create/create_action.py @@ -29,6 +29,9 @@ class CreateAction(openpype.hosts.blender.api.plugin.BlenderCreator): collection = bpy.data.collections.new(name=name) bpy.context.scene.collection.children.link(collection) instance_data['task'] = get_current_task_name() + instance_data['id'] = 'pyblish.avalon.instance' + instance_data['creator_identifier'] = self.identifier + instance_data['label'] = self.label lib.imprint(collection, instance_data) if pre_create_data.get("useSelection"): diff --git a/openpype/hosts/blender/plugins/create/create_animation.py b/openpype/hosts/blender/plugins/create/create_animation.py index ec77569889..538c0455ac 100644 --- a/openpype/hosts/blender/plugins/create/create_animation.py +++ b/openpype/hosts/blender/plugins/create/create_animation.py @@ -47,6 +47,9 @@ class CreateAnimation(plugin.BlenderCreator): asset_group = bpy.data.collections.new(name=name) instances.children.link(asset_group) instance_data['task'] = get_current_task_name() + instance_data['id'] = 'pyblish.avalon.instance' + instance_data['creator_identifier'] = self.identifier + instance_data['label'] = self.label lib.imprint(asset_group, instance_data) if pre_create_data.get("useSelection"): diff --git a/openpype/hosts/blender/plugins/create/create_camera.py b/openpype/hosts/blender/plugins/create/create_camera.py index 55ea07d90f..ae85273353 100644 --- a/openpype/hosts/blender/plugins/create/create_camera.py +++ b/openpype/hosts/blender/plugins/create/create_camera.py @@ -45,6 +45,9 @@ class CreateCamera(plugin.BlenderCreator): asset_group.empty_display_type = 'SINGLE_ARROW' instances.objects.link(asset_group) instance_data['task'] = get_current_task_name() + instance_data['id'] = 'pyblish.avalon.instance' + instance_data['creator_identifier'] = self.identifier + instance_data['label'] = self.label lib.imprint(asset_group, instance_data) if pre_create_data.get("useSelection"): diff --git a/openpype/hosts/blender/plugins/create/create_layout.py b/openpype/hosts/blender/plugins/create/create_layout.py index 60812d7fc9..cce422b229 100644 --- a/openpype/hosts/blender/plugins/create/create_layout.py +++ b/openpype/hosts/blender/plugins/create/create_layout.py @@ -44,6 +44,9 @@ class CreateLayout(plugin.BlenderCreator): asset_group.empty_display_type = 'SINGLE_ARROW' instances.objects.link(asset_group) instance_data['task'] = get_current_task_name() + instance_data['id'] = 'pyblish.avalon.instance' + instance_data['creator_identifier'] = self.identifier + instance_data['label'] = self.label lib.imprint(asset_group, instance_data) # Add selected objects to instance diff --git a/openpype/hosts/blender/plugins/create/create_model.py b/openpype/hosts/blender/plugins/create/create_model.py index 59bbae5bc4..57f9e79aa1 100644 --- a/openpype/hosts/blender/plugins/create/create_model.py +++ b/openpype/hosts/blender/plugins/create/create_model.py @@ -44,6 +44,9 @@ class CreateModel(plugin.BlenderCreator): asset_group.empty_display_type = 'SINGLE_ARROW' instances.objects.link(asset_group) instance_data['task'] = get_current_task_name() + instance_data['id'] = 'pyblish.avalon.instance' + instance_data['creator_identifier'] = self.identifier + instance_data['label'] = self.label lib.imprint(asset_group, instance_data) # Add selected objects to instance diff --git a/openpype/hosts/blender/plugins/create/create_pointcache.py b/openpype/hosts/blender/plugins/create/create_pointcache.py index bb843c5a22..f95f79ae78 100644 --- a/openpype/hosts/blender/plugins/create/create_pointcache.py +++ b/openpype/hosts/blender/plugins/create/create_pointcache.py @@ -29,6 +29,9 @@ class CreatePointcache(plugin.BlenderCreator): collection = bpy.data.collections.new(name=name) bpy.context.scene.collection.children.link(collection) instance_data['task'] = get_current_task_name() + instance_data['id'] = 'pyblish.avalon.instance' + instance_data['creator_identifier'] = self.identifier + instance_data['label'] = self.label lib.imprint(collection, instance_data) if pre_create_data.get("useSelection"): diff --git a/openpype/hosts/blender/plugins/create/create_review.py b/openpype/hosts/blender/plugins/create/create_review.py index 1191fb95f1..8472600c2f 100644 --- a/openpype/hosts/blender/plugins/create/create_review.py +++ b/openpype/hosts/blender/plugins/create/create_review.py @@ -43,6 +43,9 @@ class CreateReview(plugin.BlenderCreator): asset_group = bpy.data.collections.new(name=name) instances.children.link(asset_group) instance_data['task'] = get_current_task_name() + instance_data['id'] = 'pyblish.avalon.instance' + instance_data['creator_identifier'] = self.identifier + instance_data['label'] = self.label lib.imprint(asset_group, instance_data) if pre_create_data.get("useSelection"): diff --git a/openpype/hosts/blender/plugins/create/create_rig.py b/openpype/hosts/blender/plugins/create/create_rig.py index 752f3f7b18..a60f2a72ee 100644 --- a/openpype/hosts/blender/plugins/create/create_rig.py +++ b/openpype/hosts/blender/plugins/create/create_rig.py @@ -44,6 +44,9 @@ class CreateRig(plugin.BlenderCreator): asset_group.empty_display_type = 'SINGLE_ARROW' instances.objects.link(asset_group) instance_data['task'] = get_current_task_name() + instance_data['id'] = 'pyblish.avalon.instance' + instance_data['creator_identifier'] = self.identifier + instance_data['label'] = self.label lib.imprint(asset_group, instance_data) # Add selected objects to instance From 4d03c1c0e60b8e32248a4bb96a326c7c3a3762fb Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Thu, 20 Jul 2023 18:26:59 +0200 Subject: [PATCH 0524/1233] Set subset in inst data, use update to set it --- .../hosts/blender/plugins/create/create_action.py | 13 +++++++++---- .../blender/plugins/create/create_animation.py | 13 +++++++++---- .../hosts/blender/plugins/create/create_camera.py | 13 +++++++++---- .../hosts/blender/plugins/create/create_layout.py | 13 +++++++++---- .../hosts/blender/plugins/create/create_model.py | 13 +++++++++---- .../blender/plugins/create/create_pointcache.py | 13 +++++++++---- .../hosts/blender/plugins/create/create_review.py | 13 +++++++++---- openpype/hosts/blender/plugins/create/create_rig.py | 13 +++++++++---- 8 files changed, 72 insertions(+), 32 deletions(-) diff --git a/openpype/hosts/blender/plugins/create/create_action.py b/openpype/hosts/blender/plugins/create/create_action.py index 2c0d248994..6951e86c46 100644 --- a/openpype/hosts/blender/plugins/create/create_action.py +++ b/openpype/hosts/blender/plugins/create/create_action.py @@ -28,10 +28,15 @@ class CreateAction(openpype.hosts.blender.api.plugin.BlenderCreator): ) collection = bpy.data.collections.new(name=name) bpy.context.scene.collection.children.link(collection) - instance_data['task'] = get_current_task_name() - instance_data['id'] = 'pyblish.avalon.instance' - instance_data['creator_identifier'] = self.identifier - instance_data['label'] = self.label + instance_data.update( + { + "id": "pyblish.avalon.instance", + "creator_identifier": self.identifier, + "label": self.label, + "task": get_current_task_name(), + "subset": subset_name, + } + ) lib.imprint(collection, instance_data) if pre_create_data.get("useSelection"): diff --git a/openpype/hosts/blender/plugins/create/create_animation.py b/openpype/hosts/blender/plugins/create/create_animation.py index 538c0455ac..9e7dfbaf84 100644 --- a/openpype/hosts/blender/plugins/create/create_animation.py +++ b/openpype/hosts/blender/plugins/create/create_animation.py @@ -46,10 +46,15 @@ class CreateAnimation(plugin.BlenderCreator): # asset_group.empty_display_type = 'SINGLE_ARROW' asset_group = bpy.data.collections.new(name=name) instances.children.link(asset_group) - instance_data['task'] = get_current_task_name() - instance_data['id'] = 'pyblish.avalon.instance' - instance_data['creator_identifier'] = self.identifier - instance_data['label'] = self.label + instance_data.update( + { + "id": "pyblish.avalon.instance", + "creator_identifier": self.identifier, + "label": self.label, + "task": get_current_task_name(), + "subset": subset_name, + } + ) lib.imprint(asset_group, instance_data) if pre_create_data.get("useSelection"): diff --git a/openpype/hosts/blender/plugins/create/create_camera.py b/openpype/hosts/blender/plugins/create/create_camera.py index ae85273353..c5987779c0 100644 --- a/openpype/hosts/blender/plugins/create/create_camera.py +++ b/openpype/hosts/blender/plugins/create/create_camera.py @@ -44,10 +44,15 @@ class CreateCamera(plugin.BlenderCreator): asset_group = bpy.data.objects.new(name=name, object_data=None) asset_group.empty_display_type = 'SINGLE_ARROW' instances.objects.link(asset_group) - instance_data['task'] = get_current_task_name() - instance_data['id'] = 'pyblish.avalon.instance' - instance_data['creator_identifier'] = self.identifier - instance_data['label'] = self.label + instance_data.update( + { + "id": "pyblish.avalon.instance", + "creator_identifier": self.identifier, + "label": self.label, + "task": get_current_task_name(), + "subset": subset_name, + } + ) lib.imprint(asset_group, instance_data) if pre_create_data.get("useSelection"): diff --git a/openpype/hosts/blender/plugins/create/create_layout.py b/openpype/hosts/blender/plugins/create/create_layout.py index cce422b229..cb61799f4a 100644 --- a/openpype/hosts/blender/plugins/create/create_layout.py +++ b/openpype/hosts/blender/plugins/create/create_layout.py @@ -43,10 +43,15 @@ class CreateLayout(plugin.BlenderCreator): asset_group = bpy.data.objects.new(name=name, object_data=None) asset_group.empty_display_type = 'SINGLE_ARROW' instances.objects.link(asset_group) - instance_data['task'] = get_current_task_name() - instance_data['id'] = 'pyblish.avalon.instance' - instance_data['creator_identifier'] = self.identifier - instance_data['label'] = self.label + instance_data.update( + { + "id": "pyblish.avalon.instance", + "creator_identifier": self.identifier, + "label": self.label, + "task": get_current_task_name(), + "subset": subset_name, + } + ) lib.imprint(asset_group, instance_data) # Add selected objects to instance diff --git a/openpype/hosts/blender/plugins/create/create_model.py b/openpype/hosts/blender/plugins/create/create_model.py index 57f9e79aa1..ccf3668d98 100644 --- a/openpype/hosts/blender/plugins/create/create_model.py +++ b/openpype/hosts/blender/plugins/create/create_model.py @@ -43,10 +43,15 @@ class CreateModel(plugin.BlenderCreator): asset_group = bpy.data.objects.new(name=name, object_data=None) asset_group.empty_display_type = 'SINGLE_ARROW' instances.objects.link(asset_group) - instance_data['task'] = get_current_task_name() - instance_data['id'] = 'pyblish.avalon.instance' - instance_data['creator_identifier'] = self.identifier - instance_data['label'] = self.label + instance_data.update( + { + "id": "pyblish.avalon.instance", + "creator_identifier": self.identifier, + "label": self.label, + "task": get_current_task_name(), + "subset": subset_name, + } + ) lib.imprint(asset_group, instance_data) # Add selected objects to instance diff --git a/openpype/hosts/blender/plugins/create/create_pointcache.py b/openpype/hosts/blender/plugins/create/create_pointcache.py index f95f79ae78..e27cb22389 100644 --- a/openpype/hosts/blender/plugins/create/create_pointcache.py +++ b/openpype/hosts/blender/plugins/create/create_pointcache.py @@ -28,10 +28,15 @@ class CreatePointcache(plugin.BlenderCreator): ) collection = bpy.data.collections.new(name=name) bpy.context.scene.collection.children.link(collection) - instance_data['task'] = get_current_task_name() - instance_data['id'] = 'pyblish.avalon.instance' - instance_data['creator_identifier'] = self.identifier - instance_data['label'] = self.label + instance_data.update( + { + "id": "pyblish.avalon.instance", + "creator_identifier": self.identifier, + "label": self.label, + "task": get_current_task_name(), + "subset": subset_name, + } + ) lib.imprint(collection, instance_data) if pre_create_data.get("useSelection"): diff --git a/openpype/hosts/blender/plugins/create/create_review.py b/openpype/hosts/blender/plugins/create/create_review.py index 8472600c2f..afeaea951b 100644 --- a/openpype/hosts/blender/plugins/create/create_review.py +++ b/openpype/hosts/blender/plugins/create/create_review.py @@ -42,10 +42,15 @@ class CreateReview(plugin.BlenderCreator): name = plugin.asset_name(instance_data["asset"], subset_name) asset_group = bpy.data.collections.new(name=name) instances.children.link(asset_group) - instance_data['task'] = get_current_task_name() - instance_data['id'] = 'pyblish.avalon.instance' - instance_data['creator_identifier'] = self.identifier - instance_data['label'] = self.label + instance_data.update( + { + "id": "pyblish.avalon.instance", + "creator_identifier": self.identifier, + "label": self.label, + "task": get_current_task_name(), + "subset": subset_name, + } + ) lib.imprint(asset_group, instance_data) if pre_create_data.get("useSelection"): diff --git a/openpype/hosts/blender/plugins/create/create_rig.py b/openpype/hosts/blender/plugins/create/create_rig.py index a60f2a72ee..2db766f7ed 100644 --- a/openpype/hosts/blender/plugins/create/create_rig.py +++ b/openpype/hosts/blender/plugins/create/create_rig.py @@ -43,10 +43,15 @@ class CreateRig(plugin.BlenderCreator): asset_group = bpy.data.objects.new(name=name, object_data=None) asset_group.empty_display_type = 'SINGLE_ARROW' instances.objects.link(asset_group) - instance_data['task'] = get_current_task_name() - instance_data['id'] = 'pyblish.avalon.instance' - instance_data['creator_identifier'] = self.identifier - instance_data['label'] = self.label + instance_data.update( + { + "id": "pyblish.avalon.instance", + "creator_identifier": self.identifier, + "label": self.label, + "task": get_current_task_name(), + "subset": subset_name, + } + ) lib.imprint(asset_group, instance_data) # Add selected objects to instance From 8236e46ca93b562feb66df51a81ec74c6785586f Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Mon, 31 Jul 2023 17:16:40 +0200 Subject: [PATCH 0525/1233] implement IWorkfileHost for Blender host --- openpype/hosts/blender/api/pipeline.py | 70 +++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/blender/api/pipeline.py b/openpype/hosts/blender/api/pipeline.py index 4c06b1959e..c3f8f06694 100644 --- a/openpype/hosts/blender/api/pipeline.py +++ b/openpype/hosts/blender/api/pipeline.py @@ -12,6 +12,7 @@ import pyblish.api from openpype.host import( HostBase, + IWorkfileHost, IPublishHost, ) from openpype.client import get_asset_by_name @@ -33,6 +34,14 @@ from openpype.lib import ( ) import openpype.hosts.blender from openpype.settings import get_project_settings +from .workio import ( + open_file, + save_file, + current_file, + has_unsaved_changes, + file_extensions, + work_root, +) HOST_DIR = os.path.dirname(os.path.abspath(openpype.hosts.blender.__file__)) @@ -51,7 +60,7 @@ IS_HEADLESS = bpy.app.background log = Logger.get_logger(__name__) -class BlenderHost(HostBase, IPublishHost): +class BlenderHost(HostBase, IWorkfileHost, IPublishHost): name = "blender" def install(self): @@ -63,6 +72,65 @@ class BlenderHost(HostBase, IPublishHost): """List containers from active Blender scene.""" return ls() + def get_workfile_extensions(self) -> List[str]: + """Override get_workfile_extensions method from IWorkfileHost. + Get workfile possible extensions. + + Returns: + List[str]: Workfile extensions. + """ + return file_extensions() + + def save_workfile(self, dst_path: str = None): + """Override save_workfile method from IWorkfileHost. + Save currently opened workfile. + + Args: + dst_path (str): Where the current scene should be saved. Or use + current path if `None` is passed. + """ + save_file(dst_path if dst_path else bpy.data.filepath) + + def open_workfile(self, filepath: str): + """Override open_workfile method from IWorkfileHost. + Open workfile at specified filepath in the host. + + Args: + filepath (str): Path to workfile. + """ + open_file(filepath) + + def get_current_workfile(self) -> str: + """Override get_current_workfile method from IWorkfileHost. + Retrieve currently opened workfile path. + + Returns: + str: Path to currently opened workfile. + """ + return current_file() + + def workfile_has_unsaved_changes(self) -> bool: + """Override wokfile_has_unsaved_changes method from IWorkfileHost. + Returns True if opened workfile has no unsaved changes. + + Returns: + bool: True if scene is saved and False if it has unsaved + modifications. + """ + return has_unsaved_changes() + + def work_root(self, session) -> str: + """Override work_root method from IWorkfileHost. + Modify workdir per host. + + Args: + session (dict): Session context data. + + Returns: + str: Path to new workdir. + """ + return work_root(session) + def get_context_data(self) -> dict: """Override abstract method from IPublishHost. Get global data related to creation-publishing from workfile. From e9d022bab37f67391267788c3de940c1db0f955d Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Wed, 2 Aug 2023 16:13:34 +0200 Subject: [PATCH 0526/1233] Handling optional plugins --- openpype/hosts/blender/plugins/publish/extract_abc.py | 2 +- .../blender/plugins/publish/extract_abc_animation.py | 5 ++++- .../blender/plugins/publish/extract_blend_animation.py | 5 ++++- .../blender/plugins/publish/extract_camera_abc.py | 2 +- .../blender/plugins/publish/extract_camera_fbx.py | 2 +- openpype/hosts/blender/plugins/publish/extract_fbx.py | 2 +- .../blender/plugins/publish/extract_fbx_animation.py | 5 ++++- .../hosts/blender/plugins/publish/extract_layout.py | 2 +- .../hosts/blender/plugins/publish/extract_playblast.py | 2 +- .../plugins/publish/increment_workfile_version.py | 6 +++++- .../blender/plugins/publish/integrate_animation.py | 6 +++++- .../blender/plugins/publish/validate_mesh_has_uv.py | 10 ++++++++-- .../blender/plugins/publish/validate_object_mode.py | 6 +++++- 13 files changed, 41 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/extract_abc.py b/openpype/hosts/blender/plugins/publish/extract_abc.py index 7b6c4d7ae7..5af8104344 100644 --- a/openpype/hosts/blender/plugins/publish/extract_abc.py +++ b/openpype/hosts/blender/plugins/publish/extract_abc.py @@ -7,7 +7,7 @@ from openpype.hosts.blender.api import plugin from openpype.hosts.blender.api.pipeline import AVALON_PROPERTY -class ExtractABC(publish.Extractor): +class ExtractABC(publish.Extractor, publish.OptionalPyblishPluginMixin): """Extract as ABC.""" label = "Extract ABC" diff --git a/openpype/hosts/blender/plugins/publish/extract_abc_animation.py b/openpype/hosts/blender/plugins/publish/extract_abc_animation.py index 44b2ba3761..0b6b93b7a5 100644 --- a/openpype/hosts/blender/plugins/publish/extract_abc_animation.py +++ b/openpype/hosts/blender/plugins/publish/extract_abc_animation.py @@ -6,7 +6,10 @@ from openpype.pipeline import publish from openpype.hosts.blender.api import plugin -class ExtractAnimationABC(publish.Extractor): +class ExtractAnimationABC( + publish.Extractor, + publish.OptionalPyblishPluginMixin, +): """Extract as ABC.""" label = "Extract Animation ABC" diff --git a/openpype/hosts/blender/plugins/publish/extract_blend_animation.py b/openpype/hosts/blender/plugins/publish/extract_blend_animation.py index 477411b73d..3a5b788c9e 100644 --- a/openpype/hosts/blender/plugins/publish/extract_blend_animation.py +++ b/openpype/hosts/blender/plugins/publish/extract_blend_animation.py @@ -5,7 +5,10 @@ import bpy from openpype.pipeline import publish -class ExtractBlendAnimation(publish.Extractor): +class ExtractBlendAnimation( + publish.Extractor, + publish.OptionalPyblishPluginMixin, +): """Extract a blend file.""" label = "Extract Blend" diff --git a/openpype/hosts/blender/plugins/publish/extract_camera_abc.py b/openpype/hosts/blender/plugins/publish/extract_camera_abc.py index 036be7bf3c..2a327f4d65 100644 --- a/openpype/hosts/blender/plugins/publish/extract_camera_abc.py +++ b/openpype/hosts/blender/plugins/publish/extract_camera_abc.py @@ -7,7 +7,7 @@ from openpype.hosts.blender.api import plugin from openpype.hosts.blender.api.pipeline import AVALON_PROPERTY -class ExtractCameraABC(publish.Extractor): +class ExtractCameraABC(publish.Extractor, publish.OptionalPyblishPluginMixin): """Extract camera as ABC.""" label = "Extract Camera (ABC)" diff --git a/openpype/hosts/blender/plugins/publish/extract_camera_fbx.py b/openpype/hosts/blender/plugins/publish/extract_camera_fbx.py index 315994140e..8e5e4d37d4 100644 --- a/openpype/hosts/blender/plugins/publish/extract_camera_fbx.py +++ b/openpype/hosts/blender/plugins/publish/extract_camera_fbx.py @@ -6,7 +6,7 @@ from openpype.pipeline import publish from openpype.hosts.blender.api import plugin -class ExtractCamera(publish.Extractor): +class ExtractCamera(publish.Extractor, publish.OptionalPyblishPluginMixin): """Extract as the camera as FBX.""" label = "Extract Camera (FBX)" diff --git a/openpype/hosts/blender/plugins/publish/extract_fbx.py b/openpype/hosts/blender/plugins/publish/extract_fbx.py index 0ad797c226..8ace6a43a7 100644 --- a/openpype/hosts/blender/plugins/publish/extract_fbx.py +++ b/openpype/hosts/blender/plugins/publish/extract_fbx.py @@ -7,7 +7,7 @@ from openpype.hosts.blender.api import plugin from openpype.hosts.blender.api.pipeline import AVALON_PROPERTY -class ExtractFBX(publish.Extractor): +class ExtractFBX(publish.Extractor, publish.OptionalPyblishPluginMixin): """Extract as FBX.""" label = "Extract FBX" diff --git a/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py b/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py index 062b42e99d..04f50f8207 100644 --- a/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py +++ b/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py @@ -10,7 +10,10 @@ from openpype.hosts.blender.api import plugin from openpype.hosts.blender.api.pipeline import AVALON_PROPERTY -class ExtractAnimationFBX(publish.Extractor): +class ExtractAnimationFBX( + publish.Extractor, + publish.OptionalPyblishPluginMixin, +): """Extract as animation.""" label = "Extract FBX" diff --git a/openpype/hosts/blender/plugins/publish/extract_layout.py b/openpype/hosts/blender/plugins/publish/extract_layout.py index f2d04f1178..8445560bba 100644 --- a/openpype/hosts/blender/plugins/publish/extract_layout.py +++ b/openpype/hosts/blender/plugins/publish/extract_layout.py @@ -11,7 +11,7 @@ from openpype.hosts.blender.api import plugin from openpype.hosts.blender.api.pipeline import AVALON_PROPERTY -class ExtractLayout(publish.Extractor): +class ExtractLayout(publish.Extractor, publish.OptionalPyblishPluginMixin): """Extract a layout.""" label = "Extract Layout" diff --git a/openpype/hosts/blender/plugins/publish/extract_playblast.py b/openpype/hosts/blender/plugins/publish/extract_playblast.py index 196e75b8cc..82ddbc1fc2 100644 --- a/openpype/hosts/blender/plugins/publish/extract_playblast.py +++ b/openpype/hosts/blender/plugins/publish/extract_playblast.py @@ -9,7 +9,7 @@ from openpype.hosts.blender.api import capture from openpype.hosts.blender.api.lib import maintained_time -class ExtractPlayblast(publish.Extractor): +class ExtractPlayblast(publish.Extractor, publish.OptionalPyblishPluginMixin): """ Extract viewport playblast. diff --git a/openpype/hosts/blender/plugins/publish/increment_workfile_version.py b/openpype/hosts/blender/plugins/publish/increment_workfile_version.py index 6ace14d77c..176668f366 100644 --- a/openpype/hosts/blender/plugins/publish/increment_workfile_version.py +++ b/openpype/hosts/blender/plugins/publish/increment_workfile_version.py @@ -1,8 +1,12 @@ import pyblish.api +from openpype.pipeline.publish import OptionalPyblishPluginMixin from openpype.hosts.blender.api.workio import save_file -class IncrementWorkfileVersion(pyblish.api.ContextPlugin): +class IncrementWorkfileVersion( + pyblish.api.ContextPlugin, + OptionalPyblishPluginMixin +): """Increment current workfile version.""" order = pyblish.api.IntegratorOrder + 0.9 diff --git a/openpype/hosts/blender/plugins/publish/integrate_animation.py b/openpype/hosts/blender/plugins/publish/integrate_animation.py index d9a85bc79b..b7e5423fa8 100644 --- a/openpype/hosts/blender/plugins/publish/integrate_animation.py +++ b/openpype/hosts/blender/plugins/publish/integrate_animation.py @@ -1,9 +1,13 @@ import json import pyblish.api +from openpype.pipeline.publish import OptionalPyblishPluginMixin -class IntegrateAnimation(pyblish.api.InstancePlugin): +class IntegrateAnimation( + pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin, +): """Generate a JSON file for animation.""" label = "Integrate Animation" diff --git a/openpype/hosts/blender/plugins/publish/validate_mesh_has_uv.py b/openpype/hosts/blender/plugins/publish/validate_mesh_has_uv.py index edf47193be..687371b362 100644 --- a/openpype/hosts/blender/plugins/publish/validate_mesh_has_uv.py +++ b/openpype/hosts/blender/plugins/publish/validate_mesh_has_uv.py @@ -4,11 +4,17 @@ import bpy import pyblish.api -from openpype.pipeline.publish import ValidateContentsOrder +from openpype.pipeline.publish import ( + ValidateContentsOrder, + OptionalPyblishPluginMixin, +) import openpype.hosts.blender.api.action -class ValidateMeshHasUvs(pyblish.api.InstancePlugin): +class ValidateMeshHasUvs( + pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin, +): """Validate that the current mesh has UV's.""" order = ValidateContentsOrder diff --git a/openpype/hosts/blender/plugins/publish/validate_object_mode.py b/openpype/hosts/blender/plugins/publish/validate_object_mode.py index ac60e00f89..d8d2e3c8bf 100644 --- a/openpype/hosts/blender/plugins/publish/validate_object_mode.py +++ b/openpype/hosts/blender/plugins/publish/validate_object_mode.py @@ -3,10 +3,14 @@ from typing import List import bpy import pyblish.api +from openpype.pipeline.publish import OptionalPyblishPluginMixin import openpype.hosts.blender.api.action -class ValidateObjectIsInObjectMode(pyblish.api.InstancePlugin): +class ValidateObjectIsInObjectMode( + pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin, +): """Validate that the objects in the instance are in Object Mode.""" order = pyblish.api.ValidatorOrder - 0.01 From 656a42dd525317f83b22a232f8dc68756d2c389a Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Wed, 9 Aug 2023 11:45:58 +0200 Subject: [PATCH 0527/1233] Handling instance noce --- openpype/hosts/blender/api/plugin.py | 13 ++++++++++--- .../hosts/blender/plugins/create/create_action.py | 8 ++++++++ .../blender/plugins/create/create_animation.py | 6 ++++++ .../hosts/blender/plugins/create/create_camera.py | 6 ++++++ .../hosts/blender/plugins/create/create_layout.py | 6 ++++++ .../hosts/blender/plugins/create/create_model.py | 6 ++++++ .../blender/plugins/create/create_pointcache.py | 6 ++++++ .../hosts/blender/plugins/create/create_review.py | 6 ++++++ openpype/hosts/blender/plugins/create/create_rig.py | 6 ++++++ 9 files changed, 60 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index 5dd352ff6c..fe0d53f84e 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -234,13 +234,20 @@ class BlenderCreator(Creator): pre_create_data(dict): Data based on pre creation attributes. Those may affect how creator works. """ + collection = bpy.data.collections.new(name=subset_name) + bpy.context.scene.collection.children.link(collection) + + instance_node = {} + for key, value in collection.items(): + instance_node[key] = value + + instance_data["instance_node"] = instance_node + instance = CreatedInstance( self.family, subset_name, instance_data, self ) self._add_instance_to_context(instance) - collection = bpy.data.collections.new(name=subset_name) - bpy.context.scene.collection.children.link(collection) imprint(collection, instance_data) if pre_create_data.get("useSelection"): @@ -287,7 +294,7 @@ class BlenderCreator(Creator): for created_instance, _changes in update_list: data = created_instance.data_to_store() - # TODO + imprint(data.get("instance_node", {}), data) def remove_instances(self, instances: List[CreatedInstance]): diff --git a/openpype/hosts/blender/plugins/create/create_action.py b/openpype/hosts/blender/plugins/create/create_action.py index 6951e86c46..a43258082c 100644 --- a/openpype/hosts/blender/plugins/create/create_action.py +++ b/openpype/hosts/blender/plugins/create/create_action.py @@ -28,6 +28,11 @@ class CreateAction(openpype.hosts.blender.api.plugin.BlenderCreator): ) collection = bpy.data.collections.new(name=name) bpy.context.scene.collection.children.link(collection) + + instance_node = {} + for key, value in collection.items(): + instance_node[key] = value + instance_data.update( { "id": "pyblish.avalon.instance", @@ -35,8 +40,11 @@ class CreateAction(openpype.hosts.blender.api.plugin.BlenderCreator): "label": self.label, "task": get_current_task_name(), "subset": subset_name, + "instance_node": instance_node, } ) + from pprint import pprint + pprint(instance_data) lib.imprint(collection, instance_data) if pre_create_data.get("useSelection"): diff --git a/openpype/hosts/blender/plugins/create/create_animation.py b/openpype/hosts/blender/plugins/create/create_animation.py index 9e7dfbaf84..842292f0f9 100644 --- a/openpype/hosts/blender/plugins/create/create_animation.py +++ b/openpype/hosts/blender/plugins/create/create_animation.py @@ -46,6 +46,11 @@ class CreateAnimation(plugin.BlenderCreator): # asset_group.empty_display_type = 'SINGLE_ARROW' asset_group = bpy.data.collections.new(name=name) instances.children.link(asset_group) + + instance_node = {} + for key, value in asset_group.items(): + instance_node[key] = value + instance_data.update( { "id": "pyblish.avalon.instance", @@ -53,6 +58,7 @@ class CreateAnimation(plugin.BlenderCreator): "label": self.label, "task": get_current_task_name(), "subset": subset_name, + "instance_node": instance_node, } ) lib.imprint(asset_group, instance_data) diff --git a/openpype/hosts/blender/plugins/create/create_camera.py b/openpype/hosts/blender/plugins/create/create_camera.py index c5987779c0..8360abbc7d 100644 --- a/openpype/hosts/blender/plugins/create/create_camera.py +++ b/openpype/hosts/blender/plugins/create/create_camera.py @@ -44,6 +44,11 @@ class CreateCamera(plugin.BlenderCreator): asset_group = bpy.data.objects.new(name=name, object_data=None) asset_group.empty_display_type = 'SINGLE_ARROW' instances.objects.link(asset_group) + + instance_node = {} + for key, value in asset_group.items(): + instance_node[key] = value + instance_data.update( { "id": "pyblish.avalon.instance", @@ -51,6 +56,7 @@ class CreateCamera(plugin.BlenderCreator): "label": self.label, "task": get_current_task_name(), "subset": subset_name, + "instance_node": instance_node, } ) lib.imprint(asset_group, instance_data) diff --git a/openpype/hosts/blender/plugins/create/create_layout.py b/openpype/hosts/blender/plugins/create/create_layout.py index cb61799f4a..b4b127f32c 100644 --- a/openpype/hosts/blender/plugins/create/create_layout.py +++ b/openpype/hosts/blender/plugins/create/create_layout.py @@ -43,6 +43,11 @@ class CreateLayout(plugin.BlenderCreator): asset_group = bpy.data.objects.new(name=name, object_data=None) asset_group.empty_display_type = 'SINGLE_ARROW' instances.objects.link(asset_group) + + instance_node = {} + for key, value in asset_group.items(): + instance_node[key] = value + instance_data.update( { "id": "pyblish.avalon.instance", @@ -50,6 +55,7 @@ class CreateLayout(plugin.BlenderCreator): "label": self.label, "task": get_current_task_name(), "subset": subset_name, + "instance_node": instance_node, } ) lib.imprint(asset_group, instance_data) diff --git a/openpype/hosts/blender/plugins/create/create_model.py b/openpype/hosts/blender/plugins/create/create_model.py index ccf3668d98..5cb2de0fae 100644 --- a/openpype/hosts/blender/plugins/create/create_model.py +++ b/openpype/hosts/blender/plugins/create/create_model.py @@ -43,6 +43,11 @@ class CreateModel(plugin.BlenderCreator): asset_group = bpy.data.objects.new(name=name, object_data=None) asset_group.empty_display_type = 'SINGLE_ARROW' instances.objects.link(asset_group) + + instance_node = {} + for key, value in asset_group.items(): + instance_node[key] = value + instance_data.update( { "id": "pyblish.avalon.instance", @@ -50,6 +55,7 @@ class CreateModel(plugin.BlenderCreator): "label": self.label, "task": get_current_task_name(), "subset": subset_name, + "instance_node": instance_node, } ) lib.imprint(asset_group, instance_data) diff --git a/openpype/hosts/blender/plugins/create/create_pointcache.py b/openpype/hosts/blender/plugins/create/create_pointcache.py index e27cb22389..a95ae547c8 100644 --- a/openpype/hosts/blender/plugins/create/create_pointcache.py +++ b/openpype/hosts/blender/plugins/create/create_pointcache.py @@ -28,6 +28,11 @@ class CreatePointcache(plugin.BlenderCreator): ) collection = bpy.data.collections.new(name=name) bpy.context.scene.collection.children.link(collection) + + instance_node = {} + for key, value in collection.items(): + instance_node[key] = value + instance_data.update( { "id": "pyblish.avalon.instance", @@ -35,6 +40,7 @@ class CreatePointcache(plugin.BlenderCreator): "label": self.label, "task": get_current_task_name(), "subset": subset_name, + "instance_node": instance_node, } ) lib.imprint(collection, instance_data) diff --git a/openpype/hosts/blender/plugins/create/create_review.py b/openpype/hosts/blender/plugins/create/create_review.py index afeaea951b..1bcafbc265 100644 --- a/openpype/hosts/blender/plugins/create/create_review.py +++ b/openpype/hosts/blender/plugins/create/create_review.py @@ -42,6 +42,11 @@ class CreateReview(plugin.BlenderCreator): name = plugin.asset_name(instance_data["asset"], subset_name) asset_group = bpy.data.collections.new(name=name) instances.children.link(asset_group) + + instance_node = {} + for key, value in asset_group.items(): + instance_node[key] = value + instance_data.update( { "id": "pyblish.avalon.instance", @@ -49,6 +54,7 @@ class CreateReview(plugin.BlenderCreator): "label": self.label, "task": get_current_task_name(), "subset": subset_name, + "instance_node": instance_node, } ) lib.imprint(asset_group, instance_data) diff --git a/openpype/hosts/blender/plugins/create/create_rig.py b/openpype/hosts/blender/plugins/create/create_rig.py index 2db766f7ed..e93f4a171f 100644 --- a/openpype/hosts/blender/plugins/create/create_rig.py +++ b/openpype/hosts/blender/plugins/create/create_rig.py @@ -43,6 +43,11 @@ class CreateRig(plugin.BlenderCreator): asset_group = bpy.data.objects.new(name=name, object_data=None) asset_group.empty_display_type = 'SINGLE_ARROW' instances.objects.link(asset_group) + + instance_node = {} + for key, value in asset_group.items(): + instance_node[key] = value + instance_data.update( { "id": "pyblish.avalon.instance", @@ -50,6 +55,7 @@ class CreateRig(plugin.BlenderCreator): "label": self.label, "task": get_current_task_name(), "subset": subset_name, + "instance_node": instance_node, } ) lib.imprint(asset_group, instance_data) From 5f7847c6973d378779e3b27a0ef25ed3fa5a9e1c Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Wed, 9 Aug 2023 12:04:06 +0200 Subject: [PATCH 0528/1233] Removed useless pprint --- openpype/hosts/blender/plugins/create/create_action.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/blender/plugins/create/create_action.py b/openpype/hosts/blender/plugins/create/create_action.py index a43258082c..ee7ca092b5 100644 --- a/openpype/hosts/blender/plugins/create/create_action.py +++ b/openpype/hosts/blender/plugins/create/create_action.py @@ -43,8 +43,6 @@ class CreateAction(openpype.hosts.blender.api.plugin.BlenderCreator): "instance_node": instance_node, } ) - from pprint import pprint - pprint(instance_data) lib.imprint(collection, instance_data) if pre_create_data.get("useSelection"): From f52c9f0a38de8c9d3f50d6a1a721f20afdfa1b5f Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Thu, 10 Aug 2023 10:37:46 +0200 Subject: [PATCH 0529/1233] Enhance instance_node, wip remove_inst, rm extra empty lines --- openpype/hosts/blender/api/plugin.py | 22 ++++++++++++++----- .../blender/plugins/create/create_action.py | 8 ++++--- .../plugins/create/create_animation.py | 12 ++++++---- .../blender/plugins/create/create_camera.py | 12 ++++++---- .../blender/plugins/create/create_layout.py | 12 ++++++---- .../blender/plugins/create/create_model.py | 12 ++++++---- .../plugins/create/create_pointcache.py | 8 ++++--- .../blender/plugins/create/create_review.py | 12 ++++++---- .../blender/plugins/create/create_rig.py | 12 ++++++---- 9 files changed, 75 insertions(+), 35 deletions(-) diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index fe0d53f84e..9967f9479c 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -155,7 +155,6 @@ class BlenderCreator(Creator): return collection - @staticmethod def cache_subsets(shared_data): """Cache instances for Creators shared data. @@ -221,7 +220,6 @@ class BlenderCreator(Creator): return shared_data - def create( self, subset_name: str, instance_data: dict, pre_create_data: dict ): @@ -254,7 +252,6 @@ class BlenderCreator(Creator): for obj in get_selection(): collection.objects.link(obj) - def collect_instances(self): """Override abstract method from BaseCreator. Collect existing instances related to this creator plugin.""" @@ -283,7 +280,6 @@ class BlenderCreator(Creator): # Add instance to create context self._add_instance_to_context(instance) - def update_instances(self, update_list): """Override abstract method from BaseCreator. Store changes of existing instances so they can be recollected. @@ -296,7 +292,6 @@ class BlenderCreator(Creator): imprint(data.get("instance_node", {}), data) - def remove_instances(self, instances: List[CreatedInstance]): """Override abstract method from BaseCreator. Method called when instances are removed. @@ -305,6 +300,23 @@ class BlenderCreator(Creator): instance(List[CreatedInstance]): Instance objects to remove. """ for instance in instances: + outliner_entity = instance.data.get("instance_node", {}).get( + "datablock" + ) + if not outliner_entity: + continue + + if isinstance(outliner_entity, bpy.types.Collection): + for children in outliner_entity.children_recursive: + if isinstance(children, bpy.types.Collection): + bpy.data.collections.remove(children) + else: + bpy.data.objects.remove(children) + + bpy.data.collections.remove(outliner_entity) + elif isinstance(outliner_entity, bpy.types.Object): + bpy.data.objects.remove(outliner_entity) + self._remove_instance_from_context(instance) diff --git a/openpype/hosts/blender/plugins/create/create_action.py b/openpype/hosts/blender/plugins/create/create_action.py index ee7ca092b5..7404e7e037 100644 --- a/openpype/hosts/blender/plugins/create/create_action.py +++ b/openpype/hosts/blender/plugins/create/create_action.py @@ -5,6 +5,7 @@ import bpy from openpype.pipeline import get_current_task_name, CreatedInstance import openpype.hosts.blender.api.plugin from openpype.hosts.blender.api import lib +from openpype.hosts.blender.api.pipeline import AVALON_PROPERTY class CreateAction(openpype.hosts.blender.api.plugin.BlenderCreator): @@ -29,9 +30,10 @@ class CreateAction(openpype.hosts.blender.api.plugin.BlenderCreator): collection = bpy.data.collections.new(name=name) bpy.context.scene.collection.children.link(collection) - instance_node = {} - for key, value in collection.items(): - instance_node[key] = value + collection[AVALON_PROPERTY] = instance_node = { + "name": collection.name, + "datablock": collection, + } instance_data.update( { diff --git a/openpype/hosts/blender/plugins/create/create_animation.py b/openpype/hosts/blender/plugins/create/create_animation.py index 842292f0f9..c2f24250e1 100644 --- a/openpype/hosts/blender/plugins/create/create_animation.py +++ b/openpype/hosts/blender/plugins/create/create_animation.py @@ -4,7 +4,10 @@ import bpy from openpype.pipeline import get_current_task_name, CreatedInstance from openpype.hosts.blender.api import plugin, lib, ops -from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES +from openpype.hosts.blender.api.pipeline import ( + AVALON_INSTANCES, + AVALON_PROPERTY, +) class CreateAnimation(plugin.BlenderCreator): @@ -47,9 +50,10 @@ class CreateAnimation(plugin.BlenderCreator): asset_group = bpy.data.collections.new(name=name) instances.children.link(asset_group) - instance_node = {} - for key, value in asset_group.items(): - instance_node[key] = value + asset_group[AVALON_PROPERTY] = instance_node = { + "name": asset_group.name, + "datablock": asset_group, + } instance_data.update( { diff --git a/openpype/hosts/blender/plugins/create/create_camera.py b/openpype/hosts/blender/plugins/create/create_camera.py index 8360abbc7d..a83124cbe7 100644 --- a/openpype/hosts/blender/plugins/create/create_camera.py +++ b/openpype/hosts/blender/plugins/create/create_camera.py @@ -4,7 +4,10 @@ import bpy from openpype.pipeline import get_current_task_name, CreatedInstance from openpype.hosts.blender.api import plugin, lib, ops -from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES +from openpype.hosts.blender.api.pipeline import ( + AVALON_INSTANCES, + AVALON_PROPERTY, +) class CreateCamera(plugin.BlenderCreator): @@ -45,9 +48,10 @@ class CreateCamera(plugin.BlenderCreator): asset_group.empty_display_type = 'SINGLE_ARROW' instances.objects.link(asset_group) - instance_node = {} - for key, value in asset_group.items(): - instance_node[key] = value + asset_group[AVALON_PROPERTY] = instance_node = { + "name": asset_group.name, + "datablock": asset_group, + } instance_data.update( { diff --git a/openpype/hosts/blender/plugins/create/create_layout.py b/openpype/hosts/blender/plugins/create/create_layout.py index b4b127f32c..4fb76ef41e 100644 --- a/openpype/hosts/blender/plugins/create/create_layout.py +++ b/openpype/hosts/blender/plugins/create/create_layout.py @@ -4,7 +4,10 @@ import bpy from openpype.pipeline import get_current_task_name, CreatedInstance from openpype.hosts.blender.api import plugin, lib, ops -from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES +from openpype.hosts.blender.api.pipeline import ( + AVALON_INSTANCES, + AVALON_PROPERTY, +) class CreateLayout(plugin.BlenderCreator): @@ -44,9 +47,10 @@ class CreateLayout(plugin.BlenderCreator): asset_group.empty_display_type = 'SINGLE_ARROW' instances.objects.link(asset_group) - instance_node = {} - for key, value in asset_group.items(): - instance_node[key] = value + asset_group[AVALON_PROPERTY] = instance_node = { + "name": asset_group.name, + "datablock": asset_group, + } instance_data.update( { diff --git a/openpype/hosts/blender/plugins/create/create_model.py b/openpype/hosts/blender/plugins/create/create_model.py index 5cb2de0fae..45f1d66ad9 100644 --- a/openpype/hosts/blender/plugins/create/create_model.py +++ b/openpype/hosts/blender/plugins/create/create_model.py @@ -4,7 +4,10 @@ import bpy from openpype.pipeline import get_current_task_name, CreatedInstance from openpype.hosts.blender.api import plugin, lib, ops -from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES +from openpype.hosts.blender.api.pipeline import ( + AVALON_INSTANCES, + AVALON_PROPERTY, +) class CreateModel(plugin.BlenderCreator): @@ -44,9 +47,10 @@ class CreateModel(plugin.BlenderCreator): asset_group.empty_display_type = 'SINGLE_ARROW' instances.objects.link(asset_group) - instance_node = {} - for key, value in asset_group.items(): - instance_node[key] = value + asset_group[AVALON_PROPERTY] = instance_node = { + "name": asset_group.name, + "datablock": asset_group, + } instance_data.update( { diff --git a/openpype/hosts/blender/plugins/create/create_pointcache.py b/openpype/hosts/blender/plugins/create/create_pointcache.py index a95ae547c8..7aa3b22466 100644 --- a/openpype/hosts/blender/plugins/create/create_pointcache.py +++ b/openpype/hosts/blender/plugins/create/create_pointcache.py @@ -5,6 +5,7 @@ import bpy from openpype.pipeline import get_current_task_name, CreatedInstance from openpype.hosts.blender.api import plugin, lib, ops from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES +from openpype.hosts.blender.api.pipeline import AVALON_PROPERTY class CreatePointcache(plugin.BlenderCreator): @@ -29,9 +30,10 @@ class CreatePointcache(plugin.BlenderCreator): collection = bpy.data.collections.new(name=name) bpy.context.scene.collection.children.link(collection) - instance_node = {} - for key, value in collection.items(): - instance_node[key] = value + collection[AVALON_PROPERTY] = instance_node = { + "name": collection.name, + "datablock": collection, + } instance_data.update( { diff --git a/openpype/hosts/blender/plugins/create/create_review.py b/openpype/hosts/blender/plugins/create/create_review.py index 1bcafbc265..a6ca5b1b92 100644 --- a/openpype/hosts/blender/plugins/create/create_review.py +++ b/openpype/hosts/blender/plugins/create/create_review.py @@ -4,7 +4,10 @@ import bpy from openpype.pipeline import get_current_task_name, CreatedInstance from openpype.hosts.blender.api import plugin, lib, ops -from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES +from openpype.hosts.blender.api.pipeline import ( + AVALON_INSTANCES, + AVALON_PROPERTY, +) class CreateReview(plugin.BlenderCreator): @@ -43,9 +46,10 @@ class CreateReview(plugin.BlenderCreator): asset_group = bpy.data.collections.new(name=name) instances.children.link(asset_group) - instance_node = {} - for key, value in asset_group.items(): - instance_node[key] = value + asset_group[AVALON_PROPERTY] = instance_node = { + "name": asset_group.name, + "datablock": asset_group, + } instance_data.update( { diff --git a/openpype/hosts/blender/plugins/create/create_rig.py b/openpype/hosts/blender/plugins/create/create_rig.py index e93f4a171f..f7c99b0b03 100644 --- a/openpype/hosts/blender/plugins/create/create_rig.py +++ b/openpype/hosts/blender/plugins/create/create_rig.py @@ -4,7 +4,10 @@ import bpy from openpype.pipeline import get_current_task_name, CreatedInstance from openpype.hosts.blender.api import plugin, lib, ops -from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES +from openpype.hosts.blender.api.pipeline import ( + AVALON_INSTANCES, + AVALON_PROPERTY, +) class CreateRig(plugin.BlenderCreator): @@ -44,9 +47,10 @@ class CreateRig(plugin.BlenderCreator): asset_group.empty_display_type = 'SINGLE_ARROW' instances.objects.link(asset_group) - instance_node = {} - for key, value in asset_group.items(): - instance_node[key] = value + asset_group[AVALON_PROPERTY] = instance_node = { + "name": asset_group.name, + "datablock": asset_group, + } instance_data.update( { From 371d7405ec10257fe52c1259c383604862b080f7 Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Thu, 10 Aug 2023 11:19:05 +0200 Subject: [PATCH 0530/1233] Apply creator change to bl base creator --- openpype/hosts/blender/api/plugin.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index 9967f9479c..060d229eb5 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -235,9 +235,10 @@ class BlenderCreator(Creator): collection = bpy.data.collections.new(name=subset_name) bpy.context.scene.collection.children.link(collection) - instance_node = {} - for key, value in collection.items(): - instance_node[key] = value + collection["instance_node"] = instance_node = { + "name": collection.name, + "datablock": collection, + } instance_data["instance_node"] = instance_node From e832cf59ccb2bbf6a436184f1143ebf53231c5df Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Tue, 12 Sep 2023 15:49:33 +0200 Subject: [PATCH 0531/1233] Fix Cannot pickle object error --- openpype/hosts/blender/api/plugin.py | 5 +---- openpype/hosts/blender/plugins/create/create_action.py | 1 - openpype/hosts/blender/plugins/create/create_animation.py | 1 - openpype/hosts/blender/plugins/create/create_camera.py | 1 - openpype/hosts/blender/plugins/create/create_layout.py | 1 - openpype/hosts/blender/plugins/create/create_model.py | 1 - openpype/hosts/blender/plugins/create/create_pointcache.py | 1 - openpype/hosts/blender/plugins/create/create_review.py | 1 - openpype/hosts/blender/plugins/create/create_rig.py | 1 - 9 files changed, 1 insertion(+), 12 deletions(-) diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index 060d229eb5..73d8fc0ed5 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -237,7 +237,6 @@ class BlenderCreator(Creator): collection["instance_node"] = instance_node = { "name": collection.name, - "datablock": collection, } instance_data["instance_node"] = instance_node @@ -267,9 +266,7 @@ class BlenderCreator(Creator): for instance_data in cached_subsets.get(self.identifier, []): # Process only instances that were created by this creator - data = {} - for key, value in instance_data.items(): - data[key] = value + data = instance_data.to_dict() creator_id = data.get('creator_identifier') if creator_id == self.identifier: diff --git a/openpype/hosts/blender/plugins/create/create_action.py b/openpype/hosts/blender/plugins/create/create_action.py index 7404e7e037..d766fce038 100644 --- a/openpype/hosts/blender/plugins/create/create_action.py +++ b/openpype/hosts/blender/plugins/create/create_action.py @@ -32,7 +32,6 @@ class CreateAction(openpype.hosts.blender.api.plugin.BlenderCreator): collection[AVALON_PROPERTY] = instance_node = { "name": collection.name, - "datablock": collection, } instance_data.update( diff --git a/openpype/hosts/blender/plugins/create/create_animation.py b/openpype/hosts/blender/plugins/create/create_animation.py index c2f24250e1..88ae9e5996 100644 --- a/openpype/hosts/blender/plugins/create/create_animation.py +++ b/openpype/hosts/blender/plugins/create/create_animation.py @@ -52,7 +52,6 @@ class CreateAnimation(plugin.BlenderCreator): asset_group[AVALON_PROPERTY] = instance_node = { "name": asset_group.name, - "datablock": asset_group, } instance_data.update( diff --git a/openpype/hosts/blender/plugins/create/create_camera.py b/openpype/hosts/blender/plugins/create/create_camera.py index a83124cbe7..026b5739d6 100644 --- a/openpype/hosts/blender/plugins/create/create_camera.py +++ b/openpype/hosts/blender/plugins/create/create_camera.py @@ -50,7 +50,6 @@ class CreateCamera(plugin.BlenderCreator): asset_group[AVALON_PROPERTY] = instance_node = { "name": asset_group.name, - "datablock": asset_group, } instance_data.update( diff --git a/openpype/hosts/blender/plugins/create/create_layout.py b/openpype/hosts/blender/plugins/create/create_layout.py index 4fb76ef41e..f46ae58a43 100644 --- a/openpype/hosts/blender/plugins/create/create_layout.py +++ b/openpype/hosts/blender/plugins/create/create_layout.py @@ -49,7 +49,6 @@ class CreateLayout(plugin.BlenderCreator): asset_group[AVALON_PROPERTY] = instance_node = { "name": asset_group.name, - "datablock": asset_group, } instance_data.update( diff --git a/openpype/hosts/blender/plugins/create/create_model.py b/openpype/hosts/blender/plugins/create/create_model.py index 45f1d66ad9..069b78626b 100644 --- a/openpype/hosts/blender/plugins/create/create_model.py +++ b/openpype/hosts/blender/plugins/create/create_model.py @@ -49,7 +49,6 @@ class CreateModel(plugin.BlenderCreator): asset_group[AVALON_PROPERTY] = instance_node = { "name": asset_group.name, - "datablock": asset_group, } instance_data.update( diff --git a/openpype/hosts/blender/plugins/create/create_pointcache.py b/openpype/hosts/blender/plugins/create/create_pointcache.py index 7aa3b22466..3054b81ef5 100644 --- a/openpype/hosts/blender/plugins/create/create_pointcache.py +++ b/openpype/hosts/blender/plugins/create/create_pointcache.py @@ -32,7 +32,6 @@ class CreatePointcache(plugin.BlenderCreator): collection[AVALON_PROPERTY] = instance_node = { "name": collection.name, - "datablock": collection, } instance_data.update( diff --git a/openpype/hosts/blender/plugins/create/create_review.py b/openpype/hosts/blender/plugins/create/create_review.py index a6ca5b1b92..10a96c94fd 100644 --- a/openpype/hosts/blender/plugins/create/create_review.py +++ b/openpype/hosts/blender/plugins/create/create_review.py @@ -48,7 +48,6 @@ class CreateReview(plugin.BlenderCreator): asset_group[AVALON_PROPERTY] = instance_node = { "name": asset_group.name, - "datablock": asset_group, } instance_data.update( diff --git a/openpype/hosts/blender/plugins/create/create_rig.py b/openpype/hosts/blender/plugins/create/create_rig.py index f7c99b0b03..8daffe638d 100644 --- a/openpype/hosts/blender/plugins/create/create_rig.py +++ b/openpype/hosts/blender/plugins/create/create_rig.py @@ -49,7 +49,6 @@ class CreateRig(plugin.BlenderCreator): asset_group[AVALON_PROPERTY] = instance_node = { "name": asset_group.name, - "datablock": asset_group, } instance_data.update( From 78b33370dc3418083d323d465332ff28aa32fbeb Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Mon, 18 Sep 2023 17:59:33 +0200 Subject: [PATCH 0532/1233] Fix collectreview --- openpype/hosts/blender/plugins/publish/collect_review.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/blender/plugins/publish/collect_review.py b/openpype/hosts/blender/plugins/publish/collect_review.py index 3bf2e39e24..66a3d7b5e8 100644 --- a/openpype/hosts/blender/plugins/publish/collect_review.py +++ b/openpype/hosts/blender/plugins/publish/collect_review.py @@ -16,10 +16,14 @@ class CollectReview(pyblish.api.InstancePlugin): self.log.debug(f"instance: {instance}") + datablock = bpy.data.collections.get( + instance.data.get("instance_node", {}).get("name", "") + ) + # get cameras cameras = [ obj - for obj in instance + for obj in datablock.all_objects if isinstance(obj, bpy.types.Object) and obj.type == "CAMERA" ] From d11b00510388a797f5e0229d13a2f3bf6b837460 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 24 Oct 2023 21:08:54 +0800 Subject: [PATCH 0533/1233] make sure the path is normalized during the update for the loaders --- openpype/hosts/max/plugins/load/load_tycache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/load/load_tycache.py b/openpype/hosts/max/plugins/load/load_tycache.py index 41ea267c3d..1373404274 100644 --- a/openpype/hosts/max/plugins/load/load_tycache.py +++ b/openpype/hosts/max/plugins/load/load_tycache.py @@ -42,7 +42,7 @@ class TyCacheLoader(load.LoaderPlugin): """update the container""" from pymxs import runtime as rt - path = get_representation_path(representation) + path = os.path.normpath(get_representation_path(representation)) node = rt.GetNodeByName(container["instance_node"]) node_list = get_previous_loaded_object(node) update_custom_attribute_data(node, node_list) From 92bab3491afa798dfd646555286af1031b225e84 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 24 Oct 2023 21:44:36 +0800 Subject: [PATCH 0534/1233] add to check if the max is in headless mode for suspend refreshing and optimized the abc extractors --- openpype/hosts/max/api/lib.py | 4 +- .../hosts/max/plugins/create/create_camera.py | 18 ----- .../hosts/max/plugins/create/create_model.py | 18 ----- .../max/plugins/create/create_pointcache.py | 18 ----- .../max/plugins/publish/extract_camera_abc.py | 61 ---------------- .../max/plugins/publish/extract_model.py | 60 ---------------- .../max/plugins/publish/extract_pointcache.py | 70 ++++++++++++++++--- 7 files changed, 62 insertions(+), 187 deletions(-) delete mode 100644 openpype/hosts/max/plugins/publish/extract_camera_abc.py delete mode 100644 openpype/hosts/max/plugins/publish/extract_model.py diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 0c0aa6ad2c..61a2d938a1 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -517,7 +517,9 @@ def suspended_refresh(): """ rt.disableSceneRedraw() rt.suspendEditing() - + if is_headless(): + yield + return try: yield diff --git a/openpype/hosts/max/plugins/create/create_camera.py b/openpype/hosts/max/plugins/create/create_camera.py index 8277581d31..804d629ec7 100644 --- a/openpype/hosts/max/plugins/create/create_camera.py +++ b/openpype/hosts/max/plugins/create/create_camera.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- """Creator plugin for creating camera.""" from openpype.hosts.max.api import plugin -from openpype.lib import BoolDef class CreateCamera(plugin.MaxCreator): @@ -10,20 +9,3 @@ class CreateCamera(plugin.MaxCreator): label = "Camera" family = "camera" icon = "gear" - - def create(self, subset_name, instance_data, pre_create_data): - instance_data["custom_attrs"] = pre_create_data.get( - "custom_attrs") - - super(CreateCamera, self).create( - subset_name, - instance_data, - pre_create_data) - - def get_pre_create_attr_defs(self): - attrs = super().get_pre_create_attr_defs() - return attrs + [ - BoolDef("custom_attrs", - label="Custom Attributes", - default=False), - ] diff --git a/openpype/hosts/max/plugins/create/create_model.py b/openpype/hosts/max/plugins/create/create_model.py index 9c459a184e..fc09d475ef 100644 --- a/openpype/hosts/max/plugins/create/create_model.py +++ b/openpype/hosts/max/plugins/create/create_model.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- """Creator plugin for model.""" from openpype.hosts.max.api import plugin -from openpype.lib import BoolDef class CreateModel(plugin.MaxCreator): @@ -10,20 +9,3 @@ class CreateModel(plugin.MaxCreator): label = "Model" family = "model" icon = "gear" - - def create(self, subset_name, instance_data, pre_create_data): - instance_data["custom_attrs"] = pre_create_data.get( - "custom_attrs") - - super(CreateModel, self).create( - subset_name, - instance_data, - pre_create_data) - - def get_pre_create_attr_defs(self): - attrs = super().get_pre_create_attr_defs() - return attrs + [ - BoolDef("custom_attrs", - label="Custom Attributes", - default=False), - ] diff --git a/openpype/hosts/max/plugins/create/create_pointcache.py b/openpype/hosts/max/plugins/create/create_pointcache.py index 2790ceeefc..c2d11f4c32 100644 --- a/openpype/hosts/max/plugins/create/create_pointcache.py +++ b/openpype/hosts/max/plugins/create/create_pointcache.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- """Creator plugin for creating pointcache alembics.""" from openpype.hosts.max.api import plugin -from openpype.lib import BoolDef class CreatePointCache(plugin.MaxCreator): @@ -10,20 +9,3 @@ class CreatePointCache(plugin.MaxCreator): label = "Point Cache" family = "pointcache" icon = "gear" - - def create(self, subset_name, instance_data, pre_create_data): - instance_data["custom_attrs"] = pre_create_data.get( - "custom_attrs") - - super(CreatePointCache, self).create( - subset_name, - instance_data, - pre_create_data) - - def get_pre_create_attr_defs(self): - attrs = super().get_pre_create_attr_defs() - return attrs + [ - BoolDef("custom_attrs", - label="Custom Attributes", - default=False), - ] diff --git a/openpype/hosts/max/plugins/publish/extract_camera_abc.py b/openpype/hosts/max/plugins/publish/extract_camera_abc.py deleted file mode 100644 index 934c6b9d99..0000000000 --- a/openpype/hosts/max/plugins/publish/extract_camera_abc.py +++ /dev/null @@ -1,61 +0,0 @@ -import os - -import pyblish.api -from pymxs import runtime as rt - -from openpype.hosts.max.api import maintained_selection -from openpype.hosts.max.api.lib import suspended_refresh -from openpype.pipeline import OptionalPyblishPluginMixin, publish - - -class ExtractCameraAlembic(publish.Extractor, OptionalPyblishPluginMixin): - """Extract Camera with AlembicExport.""" - - order = pyblish.api.ExtractorOrder - 0.1 - label = "Extract Alembic Camera" - hosts = ["max"] - families = ["camera"] - optional = True - - def process(self, instance): - if not self.is_active(instance.data): - return - start = instance.data["frameStartHandle"] - end = instance.data["frameEndHandle"] - - stagingdir = self.staging_dir(instance) - filename = "{name}.abc".format(**instance.data) - path = os.path.join(stagingdir, filename) - - with suspended_refresh(): - rt.AlembicExport.ArchiveType = rt.Name("ogawa") - rt.AlembicExport.CoordinateSystem = rt.Name("maya") - rt.AlembicExport.StartFrame = start - rt.AlembicExport.EndFrame = end - rt.AlembicExport.CustomAttributes = instance.data.get( - "custom_attrs", False) - - with maintained_selection(): - # select and export - node_list = instance.data["members"] - rt.Select(node_list) - rt.ExportFile( - path, - rt.Name("noPrompt"), - selectedOnly=True, - using=rt.AlembicExport, - ) - - if "representations" not in instance.data: - instance.data["representations"] = [] - - representation = { - "name": "abc", - "ext": "abc", - "files": filename, - "stagingDir": stagingdir, - "frameStart": start, - "frameEnd": end, - } - instance.data["representations"].append(representation) - self.log.info(f"Extracted instance '{instance.name}' to: {path}") diff --git a/openpype/hosts/max/plugins/publish/extract_model.py b/openpype/hosts/max/plugins/publish/extract_model.py deleted file mode 100644 index 36e789d180..0000000000 --- a/openpype/hosts/max/plugins/publish/extract_model.py +++ /dev/null @@ -1,60 +0,0 @@ -import os -import pyblish.api -from openpype.pipeline import publish, OptionalPyblishPluginMixin -from pymxs import runtime as rt -from openpype.hosts.max.api import maintained_selection -from openpype.hosts.max.api.lib import suspended_refresh - - -class ExtractModel(publish.Extractor, OptionalPyblishPluginMixin): - """ - Extract Geometry in Alembic Format - """ - - order = pyblish.api.ExtractorOrder - 0.1 - label = "Extract Geometry (Alembic)" - hosts = ["max"] - families = ["model"] - optional = True - - def process(self, instance): - if not self.is_active(instance.data): - return - - stagingdir = self.staging_dir(instance) - filename = "{name}.abc".format(**instance.data) - filepath = os.path.join(stagingdir, filename) - - with suspended_refresh(): - rt.AlembicExport.ArchiveType = rt.name("ogawa") - rt.AlembicExport.CoordinateSystem = rt.name("maya") - rt.AlembicExport.CustomAttributes = instance.data.get( - "custom_attrs", False) - rt.AlembicExport.UVs = True - rt.AlembicExport.VertexColors = True - rt.AlembicExport.PreserveInstances = True - - with maintained_selection(): - # select and export - node_list = instance.data["members"] - rt.Select(node_list) - rt.exportFile( - filepath, - rt.name("noPrompt"), - selectedOnly=True, - using=rt.AlembicExport, - ) - - if "representations" not in instance.data: - instance.data["representations"] = [] - - representation = { - "name": "abc", - "ext": "abc", - "files": filename, - "stagingDir": stagingdir, - } - instance.data["representations"].append(representation) - self.log.info( - "Extracted instance '%s' to: %s" % (instance.name, filepath) - ) diff --git a/openpype/hosts/max/plugins/publish/extract_pointcache.py b/openpype/hosts/max/plugins/publish/extract_pointcache.py index fca318a43f..4b3407274c 100644 --- a/openpype/hosts/max/plugins/publish/extract_pointcache.py +++ b/openpype/hosts/max/plugins/publish/extract_pointcache.py @@ -39,34 +39,31 @@ Note: """ import os import pyblish.api -from openpype.pipeline import publish +from openpype.pipeline import publish, OptionalPyblishPluginMixin from pymxs import runtime as rt from openpype.hosts.max.api import maintained_selection from openpype.hosts.max.api.lib import suspended_refresh +from openpype.lib import BoolDef -class ExtractAlembic(publish.Extractor): +class ExtractAlembic(publish.Extractor, + OptionalPyblishPluginMixin): order = pyblish.api.ExtractorOrder label = "Extract Pointcache" hosts = ["max"] families = ["pointcache"] + optional = False def process(self, instance): - start = instance.data["frameStartHandle"] - end = instance.data["frameEndHandle"] + if not self.is_active(instance.data): + return parent_dir = self.staging_dir(instance) file_name = "{name}.abc".format(**instance.data) path = os.path.join(parent_dir, file_name) with suspended_refresh(): - rt.AlembicExport.ArchiveType = rt.name("ogawa") - rt.AlembicExport.CoordinateSystem = rt.name("maya") - rt.AlembicExport.StartFrame = start - rt.AlembicExport.EndFrame = end - rt.AlembicExport.CustomAttributes = instance.data.get( - "custom_attrs", False) - + self._set_abc_attributes(instance) with maintained_selection(): # select and export node_list = instance.data["members"] @@ -88,3 +85,54 @@ class ExtractAlembic(publish.Extractor): "stagingDir": parent_dir, } instance.data["representations"].append(representation) + + def _set_abc_attributes(self, instance): + start = instance.data["frameStartHandle"] + end = instance.data["frameEndHandle"] + attr_values = self.get_attr_values_from_data(instance.data) + custom_attrs = attr_values.get("custom_attrs", False) + rt.AlembicExport.ArchiveType = rt.Name("ogawa") + rt.AlembicExport.CoordinateSystem = rt.Name("maya") + rt.AlembicExport.StartFrame = start + rt.AlembicExport.EndFrame = end + rt.AlembicExport.CustomAttributes = custom_attrs + + @classmethod + def get_attribute_defs(cls): + return [ + BoolDef("custom_attrs", + label="Custom Attributes", + default=False), + ] + + +class ExtractCameraAlembic(ExtractAlembic): + """Extract Camera with AlembicExport.""" + + order = pyblish.api.ExtractorOrder - 0.1 + label = "Extract Alembic Camera" + hosts = ["max"] + families = ["camera"] + optional = True + + +class ExtractModel(ExtractAlembic): + """ + Extract Geometry in Alembic Format + """ + + order = pyblish.api.ExtractorOrder - 0.1 + label = "Extract Geometry (Alembic)" + hosts = ["max"] + families = ["model"] + optional = True + + def _set_abc_attributes(self, instance): + attr_values = self.get_attr_values_from_data(instance.data) + custom_attrs = attr_values.get("custom_attrs", False) + rt.AlembicExport.ArchiveType = rt.name("ogawa") + rt.AlembicExport.CoordinateSystem = rt.name("maya") + rt.AlembicExport.CustomAttributes = custom_attrs + rt.AlembicExport.UVs = True + rt.AlembicExport.VertexColors = True + rt.AlembicExport.PreserveInstances = True From a526260b461a85e951a1fc87697a56df70afa7d3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 24 Oct 2023 17:57:09 +0200 Subject: [PATCH 0535/1233] 'get_assets' function can find folders by path and by name --- openpype/client/server/entities.py | 57 ++++++++++++++++++++++++++---- 1 file changed, 50 insertions(+), 7 deletions(-) diff --git a/openpype/client/server/entities.py b/openpype/client/server/entities.py index 16223d3d91..c1e27eabb9 100644 --- a/openpype/client/server/entities.py +++ b/openpype/client/server/entities.py @@ -183,6 +183,20 @@ def get_asset_by_name(project_name, asset_name, fields=None): return None + +def _folders_query(project_name, con, fields, **kwargs): + if fields is None or "tasks" in fields: + folders = get_folders_with_tasks( + con, project_name, fields=fields, **kwargs + ) + + else: + folders = con.get_folders(project_name, fields=fields, **kwargs) + + for folder in folders: + yield folder + + def get_assets( project_name, asset_ids=None, @@ -199,22 +213,51 @@ def get_assets( active = None con = get_server_api_connection() + fields = folder_fields_v3_to_v4(fields, con) kwargs = dict( folder_ids=asset_ids, - folder_names=asset_names, parent_ids=parent_ids, active=active, - fields=fields ) + if not asset_names: + for folder in _folders_query(project_name, con, fields, **kwargs): + yield convert_v4_folder_to_v3(folder, project_name) + return - if fields is None or "tasks" in fields: - folders = get_folders_with_tasks(con, project_name, **kwargs) + new_asset_names = set() + folder_paths = set() + if asset_names: + for name in asset_names: + if "/" in name: + folder_paths.add(name) + else: + new_asset_names.add(name) - else: - folders = con.get_folders(project_name, **kwargs) + if folder_paths: + for folder in _folders_query( + project_name, con, fields, folder_paths=folder_paths, **kwargs + ): + yield convert_v4_folder_to_v3(folder, project_name) - for folder in folders: + if not new_asset_names: + return + + folders_by_name = collections.defaultdict(list) + for folder in _folders_query( + project_name, con, fields, folder_names=new_asset_names, **kwargs + ): + folders_by_name[folder["name"]].append(folder) + + for name, folders in folders_by_name.items(): + folder = next( + ( + folder + for folder in folders + if folder["path"] == name + ), + folders[0] + ) yield convert_v4_folder_to_v3(folder, project_name) From 331c4833ade7acf5ab0db7952e9821b817354d2d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 24 Oct 2023 17:58:34 +0200 Subject: [PATCH 0536/1233] added helper function 'get_asset_name_identifier' to receive folder path in AYON mode and asset name in openpype --- openpype/client/__init__.py | 4 ++++ openpype/client/entities.py | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/openpype/client/__init__.py b/openpype/client/__init__.py index 7831afd8ad..a313d6b3cc 100644 --- a/openpype/client/__init__.py +++ b/openpype/client/__init__.py @@ -43,6 +43,8 @@ from .entities import ( get_thumbnail_id_from_source, get_workfile_info, + + get_asset_name_identifier, ) from .entity_links import ( @@ -105,4 +107,6 @@ __all__ = ( "get_linked_representation_id", "create_project", + + "get_asset_name_identifier", ) diff --git a/openpype/client/entities.py b/openpype/client/entities.py index 5d9654c611..d085f90028 100644 --- a/openpype/client/entities.py +++ b/openpype/client/entities.py @@ -4,3 +4,22 @@ if not AYON_SERVER_ENABLED: from .mongo.entities import * else: from .server.entities import * + + +def get_asset_name_identifier(asset_doc): + """Get asset name identifier by asset document. + + This function is added because of AYON implementation where name + identifier is not just a name but full path. + + Asset document must have "name" key, and "data.parents" when in AYON mode. + + Args: + asset_doc (dict[str, Any]): Asset document. + """ + + if not AYON_SERVER_ENABLED: + return asset_doc["name"] + parents = list(asset_doc["data"]["parents"]) + parents.append(asset_doc["name"]) + return "/".join(parents) From 4c6ec4b9bcb245892421b14bee26d7564f33f971 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 24 Oct 2023 18:00:58 +0200 Subject: [PATCH 0537/1233] instances in AYON mode have 'folderPath' instead of 'asset' --- openpype/pipeline/create/context.py | 37 ++++++++++++++++--- .../publish/collect_from_create_context.py | 3 ++ 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 25f03ddd3b..333ab25f54 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -11,7 +11,12 @@ from contextlib import contextmanager import pyblish.logic import pyblish.api -from openpype.client import get_assets, get_asset_by_name +from openpype import AYON_SERVER_ENABLED +from openpype.client import ( + get_assets, + get_asset_by_name, + get_asset_name_identifier, +) from openpype.settings import ( get_system_settings, get_project_settings @@ -922,9 +927,19 @@ class CreatedInstance: self._orig_data = copy.deepcopy(data) # Pop family and subset to prevent unexpected changes + # TODO change to 'productType' and 'productName' in AYON data.pop("family", None) data.pop("subset", None) + if AYON_SERVER_ENABLED: + asset_name = data.pop("asset", None) + if "folderPath" not in data: + data["folderPath"] = asset_name + + elif "folderPath" in data: + asset_name = data.pop("folderPath").split("/")[-1] + if "asset" not in data: + data["asset"] = asset_name # QUESTION Does it make sense to have data stored as ordered dict? self._data = collections.OrderedDict() @@ -1268,6 +1283,8 @@ class CreatedInstance: def has_set_asset(self): """Asset name is set in data.""" + if AYON_SERVER_ENABLED: + return "folderPath" in self._data return "asset" in self._data @property @@ -2229,7 +2246,10 @@ class CreateContext: task_names_by_asset_name = {} for instance in instances: task_name = instance.get("task") - asset_name = instance.get("asset") + if AYON_SERVER_ENABLED: + asset_name = instance.get("folderPath") + else: + asset_name = instance.get("asset") if asset_name: task_names_by_asset_name[asset_name] = set() if task_name: @@ -2240,15 +2260,18 @@ class CreateContext: for asset_name in task_names_by_asset_name.keys() if asset_name is not None ] + fields = {"name", "data.tasks"} + if AYON_SERVER_ENABLED: + fields |= {"data.parents"} asset_docs = list(get_assets( self.project_name, asset_names=asset_names, - fields=["name", "data.tasks"] + fields=fields )) task_names_by_asset_name = {} for asset_doc in asset_docs: - asset_name = asset_doc["name"] + asset_name = get_asset_name_identifier(asset_doc) tasks = asset_doc.get("data", {}).get("tasks") or {} task_names_by_asset_name[asset_name] = set(tasks.keys()) @@ -2256,7 +2279,11 @@ class CreateContext: if not instance.has_valid_asset or not instance.has_valid_task: continue - asset_name = instance["asset"] + if AYON_SERVER_ENABLED: + asset_name = instance["folderPath"] + else: + asset_name = instance["asset"] + if asset_name not in task_names_by_asset_name: instance.set_asset_invalid(True) continue diff --git a/openpype/plugins/publish/collect_from_create_context.py b/openpype/plugins/publish/collect_from_create_context.py index 8806a13ca0..84f6141069 100644 --- a/openpype/plugins/publish/collect_from_create_context.py +++ b/openpype/plugins/publish/collect_from_create_context.py @@ -4,6 +4,7 @@ import os import pyblish.api +from openpype import AYON_SERVER_ENABLED from openpype.host import IPublishHost from openpype.pipeline import legacy_io, registered_host from openpype.pipeline.create import CreateContext @@ -38,6 +39,8 @@ class CollectFromCreateContext(pyblish.api.ContextPlugin): for created_instance in create_context.instances: instance_data = created_instance.data_to_store() + if AYON_SERVER_ENABLED: + instance_data["asset"] = instance_data.pop("folderPath") if instance_data["active"]: thumbnail_path = thumbnail_paths_by_instance_id.get( created_instance.id From 0ddd95eacf0fda0b058fccc3388d0816aa9d4145 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 24 Oct 2023 18:02:41 +0200 Subject: [PATCH 0538/1233] use 'get_asset_name_identifier' in pipeline logic --- openpype/pipeline/context_tools.py | 5 ++--- openpype/pipeline/create/utils.py | 11 ++++++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/openpype/pipeline/context_tools.py b/openpype/pipeline/context_tools.py index 13630ae7ca..e20099759a 100644 --- a/openpype/pipeline/context_tools.py +++ b/openpype/pipeline/context_tools.py @@ -17,6 +17,7 @@ from openpype.client import ( get_asset_by_id, get_asset_by_name, version_is_latest, + get_asset_name_identifier, ) from openpype.lib.events import emit_event from openpype.modules import load_modules, ModulesManager @@ -568,14 +569,12 @@ def compute_session_changes( Dict[str, str]: Changes in the Session dictionary. """ - changes = {} - # Get asset document and asset if not asset_doc: task_name = None asset_name = None else: - asset_name = asset_doc["name"] + asset_name = get_asset_name_identifier(asset_doc) # Detect any changes compared session mapping = { diff --git a/openpype/pipeline/create/utils.py b/openpype/pipeline/create/utils.py index 2ef1f02bd6..ce4af8f474 100644 --- a/openpype/pipeline/create/utils.py +++ b/openpype/pipeline/create/utils.py @@ -1,6 +1,11 @@ import collections -from openpype.client import get_assets, get_subsets, get_last_versions +from openpype.client import ( + get_assets, + get_subsets, + get_last_versions, + get_asset_name_identifier, +) def get_last_versions_for_instances( @@ -52,10 +57,10 @@ def get_last_versions_for_instances( asset_docs = get_assets( project_name, asset_names=subset_names_by_asset_name.keys(), - fields=["name", "_id"] + fields=["name", "_id", "data.parents"] ) asset_names_by_id = { - asset_doc["_id"]: asset_doc["name"] + asset_doc["_id"]: get_asset_name_identifier(asset_doc) for asset_doc in asset_docs } if not asset_names_by_id: From 922c481d95b6e377a8d7433819360c45179fcffa Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 24 Oct 2023 18:03:02 +0200 Subject: [PATCH 0539/1233] use 'get_asset_name_identifier' for 'AVALON_ASSET' on app start --- openpype/lib/applications.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index ff5e27c122..4d75a01e1d 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -12,6 +12,7 @@ from abc import ABCMeta, abstractmethod import six from openpype import AYON_SERVER_ENABLED, PACKAGE_DIR +from openpype.client import get_asset_name_identifier from openpype.settings import ( get_system_settings, get_project_settings, @@ -1728,7 +1729,9 @@ def prepare_context_environments(data, env_group=None, modules_manager=None): "AVALON_APP_NAME": app.full_name } if asset_doc: - context_env["AVALON_ASSET"] = asset_doc["name"] + asset_name = get_asset_name_identifier(asset_doc) + context_env["AVALON_ASSET"] = asset_name + if task_name: context_env["AVALON_TASK"] = task_name From f38c3f395e68df0a33a3b1ba135dbbcf0cf7f899 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 24 Oct 2023 18:03:20 +0200 Subject: [PATCH 0540/1233] use 'get_asset_name_identifier' in global plugins --- .../publish/collect_anatomy_instance_data.py | 20 +++++++++++-------- openpype/plugins/publish/collect_audio.py | 10 ++++++---- .../publish/extract_hierarchy_to_ayon.py | 4 ++-- .../publish/validate_editorial_asset_name.py | 9 ++++++--- 4 files changed, 26 insertions(+), 17 deletions(-) diff --git a/openpype/plugins/publish/collect_anatomy_instance_data.py b/openpype/plugins/publish/collect_anatomy_instance_data.py index b4f4d6a16a..cc6da9b2c3 100644 --- a/openpype/plugins/publish/collect_anatomy_instance_data.py +++ b/openpype/plugins/publish/collect_anatomy_instance_data.py @@ -30,7 +30,8 @@ import pyblish.api from openpype.client import ( get_assets, get_subsets, - get_last_versions + get_last_versions, + get_asset_name_identifier, ) from openpype.pipeline.version_start import get_versioning_start @@ -60,6 +61,9 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): self.log.debug("Querying asset documents for instances.") context_asset_doc = context.data.get("assetEntity") + context_asset_name = None + if context_asset_doc: + context_asset_name = get_asset_name_identifier(context_asset_doc) instances_with_missing_asset_doc = collections.defaultdict(list) for instance in context: @@ -68,15 +72,15 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): # There is possibility that assetEntity on instance is already set # which can happen in standalone publisher - if ( - instance_asset_doc - and instance_asset_doc["name"] == _asset_name - ): - continue + if instance_asset_doc: + instance_asset_name = get_asset_name_identifier( + instance_asset_doc) + if instance_asset_name == _asset_name: + continue # Check if asset name is the same as what is in context # - they may be different, e.g. in NukeStudio - if context_asset_doc and context_asset_doc["name"] == _asset_name: + if context_asset_name and context_asset_name == _asset_name: instance.data["assetEntity"] = context_asset_doc else: @@ -93,7 +97,7 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): asset_docs = get_assets(project_name, asset_names=asset_names) asset_docs_by_name = { - asset_doc["name"]: asset_doc + get_asset_name_identifier(asset_doc): asset_doc for asset_doc in asset_docs } diff --git a/openpype/plugins/publish/collect_audio.py b/openpype/plugins/publish/collect_audio.py index 6aaadfc568..734a625852 100644 --- a/openpype/plugins/publish/collect_audio.py +++ b/openpype/plugins/publish/collect_audio.py @@ -6,6 +6,7 @@ from openpype.client import ( get_subsets, get_last_versions, get_representations, + get_asset_name_identifier, ) from openpype.pipeline.load import get_representation_path_with_anatomy @@ -121,12 +122,13 @@ class CollectAudio(pyblish.api.ContextPlugin): asset_docs = get_assets( project_name, asset_names=asset_names, - fields=["_id", "name"] + fields=["_id", "name", "data.parents"] ) - asset_id_by_name = {} - for asset_doc in asset_docs: - asset_id_by_name[asset_doc["name"]] = asset_doc["_id"] + asset_id_by_name = { + get_asset_name_identifier(asset_doc): asset_doc["_id"] + for asset_doc in asset_docs + } asset_ids = set(asset_id_by_name.values()) # Query subsets with name define by 'audio_subset_name' attr diff --git a/openpype/plugins/publish/extract_hierarchy_to_ayon.py b/openpype/plugins/publish/extract_hierarchy_to_ayon.py index 0d9131718b..fe8cb40ad2 100644 --- a/openpype/plugins/publish/extract_hierarchy_to_ayon.py +++ b/openpype/plugins/publish/extract_hierarchy_to_ayon.py @@ -8,7 +8,7 @@ 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.client import get_assets, get_asset_name_identifier from openpype.pipeline.template_data import ( get_asset_template_data, get_task_template_data, @@ -58,7 +58,7 @@ class ExtractHierarchyToAYON(pyblish.api.ContextPlugin): project_name, asset_names=instances_by_asset_name.keys() ) asset_docs_by_name = { - asset_doc["name"]: asset_doc + get_asset_name_identifier(asset_doc): asset_doc for asset_doc in asset_docs } for asset_name, instances in instances_by_asset_name.items(): diff --git a/openpype/plugins/publish/validate_editorial_asset_name.py b/openpype/plugins/publish/validate_editorial_asset_name.py index fca0d8e7f5..b5afc49f2e 100644 --- a/openpype/plugins/publish/validate_editorial_asset_name.py +++ b/openpype/plugins/publish/validate_editorial_asset_name.py @@ -2,7 +2,7 @@ from pprint import pformat import pyblish.api -from openpype.client import get_assets +from openpype.client import get_assets, get_asset_name_identifier class ValidateEditorialAssetName(pyblish.api.ContextPlugin): @@ -34,8 +34,11 @@ class ValidateEditorialAssetName(pyblish.api.ContextPlugin): self.log.debug("__ db_assets: {}".format(db_assets)) asset_db_docs = { - str(e["name"]): [str(p) for p in e["data"]["parents"]] - for e in db_assets} + get_asset_name_identifier(asset_doc): list( + asset_doc["data"]["parents"] + ) + for asset_doc in db_assets + } self.log.debug("__ project_entities: {}".format( pformat(asset_db_docs))) From c505513b056e092ac8dd8c3652776a238046980c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 24 Oct 2023 18:04:31 +0200 Subject: [PATCH 0541/1233] use folder path in ayon tools to define current context --- openpype/tools/ayon_launcher/models/actions.py | 6 +++--- openpype/tools/ayon_loader/control.py | 2 +- openpype/tools/ayon_sceneinventory/control.py | 11 ++--------- openpype/tools/ayon_workfiles/control.py | 2 +- 4 files changed, 7 insertions(+), 14 deletions(-) diff --git a/openpype/tools/ayon_launcher/models/actions.py b/openpype/tools/ayon_launcher/models/actions.py index 93ec115734..d7c4219dc2 100644 --- a/openpype/tools/ayon_launcher/models/actions.py +++ b/openpype/tools/ayon_launcher/models/actions.py @@ -402,12 +402,12 @@ class ActionsModel: ) def _prepare_session(self, project_name, folder_id, task_id): - folder_name = None + folder_path = None if folder_id: folder = self._controller.get_folder_entity( project_name, folder_id) if folder: - folder_name = folder["name"] + folder_path = folder["path"] task_name = None if task_id: @@ -417,7 +417,7 @@ class ActionsModel: return { "AVALON_PROJECT": project_name, - "AVALON_ASSET": folder_name, + "AVALON_ASSET": folder_path, "AVALON_TASK": task_name, } diff --git a/openpype/tools/ayon_loader/control.py b/openpype/tools/ayon_loader/control.py index 2b779f5c2e..d2fae35f32 100644 --- a/openpype/tools/ayon_loader/control.py +++ b/openpype/tools/ayon_loader/control.py @@ -289,7 +289,7 @@ class LoaderController(BackendLoaderController, FrontendLoaderController): project_name = context.get("project_name") asset_name = context.get("asset_name") if project_name and asset_name: - folder = ayon_api.get_folder_by_name( + folder = ayon_api.get_folder_by_path( project_name, asset_name, fields=["id"] ) if folder: diff --git a/openpype/tools/ayon_sceneinventory/control.py b/openpype/tools/ayon_sceneinventory/control.py index e98b0e307b..6111d7e43b 100644 --- a/openpype/tools/ayon_sceneinventory/control.py +++ b/openpype/tools/ayon_sceneinventory/control.py @@ -70,19 +70,12 @@ class SceneInventoryController: context = self.get_current_context() project_name = context["project_name"] - folder_path = context.get("folder_path") folder_name = context.get("asset_name") folder_id = None - if folder_path: - folder = ayon_api.get_folder_by_path(project_name, folder_path) + if folder_name: + folder = ayon_api.get_folder_by_path(project_name, folder_name) if folder: folder_id = folder["id"] - elif folder_name: - for folder in ayon_api.get_folders( - project_name, folder_names=[folder_name] - ): - folder_id = folder["id"] - break self._current_folder_id = folder_id self._current_folder_set = True diff --git a/openpype/tools/ayon_workfiles/control.py b/openpype/tools/ayon_workfiles/control.py index 3784959caf..d86b04badb 100644 --- a/openpype/tools/ayon_workfiles/control.py +++ b/openpype/tools/ayon_workfiles/control.py @@ -427,7 +427,7 @@ class BaseWorkfileController( task_name = context["task_name"] folder_id = None if folder_name: - folder = ayon_api.get_folder_by_name(project_name, folder_name) + folder = ayon_api.get_folder_by_path(project_name, folder_name) if folder: folder_id = folder["id"] From 1ac66764d49af599915a592de0917a4b67977358 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 24 Oct 2023 18:05:16 +0200 Subject: [PATCH 0542/1233] use folder path in publisher --- openpype/tools/publisher/control.py | 6 ++- .../tools/publisher/widgets/assets_widget.py | 42 ++++++++++++++++--- openpype/tools/publisher/widgets/widgets.py | 17 ++++++-- openpype/tools/utils/assets_widget.py | 1 + 4 files changed, 56 insertions(+), 10 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index a6264303d5..ad87bdf607 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -12,10 +12,12 @@ from abc import ABCMeta, abstractmethod import six import pyblish.api +from openpype import AYON_SERVER_ENABLED from openpype.client import ( get_assets, get_asset_by_id, get_subsets, + get_asset_name_identifier, ) from openpype.lib.events import EventSystem from openpype.lib.attribute_definitions import ( @@ -73,6 +75,8 @@ class AssetDocsCache: "data.visualParent": True, "data.tasks": True } + if AYON_SERVER_ENABLED: + projection["data.parents"] = True def __init__(self, controller): self._controller = controller @@ -105,7 +109,7 @@ class AssetDocsCache: elif "tasks" not in asset_doc["data"]: asset_doc["data"]["tasks"] = {} - asset_name = asset_doc["name"] + asset_name = get_asset_name_identifier(asset_doc) asset_tasks = asset_doc["data"]["tasks"] task_names_by_asset_name[asset_name] = list(asset_tasks.keys()) asset_docs_by_name[asset_name] = asset_doc diff --git a/openpype/tools/publisher/widgets/assets_widget.py b/openpype/tools/publisher/widgets/assets_widget.py index c536f93c9b..5f74b79c99 100644 --- a/openpype/tools/publisher/widgets/assets_widget.py +++ b/openpype/tools/publisher/widgets/assets_widget.py @@ -11,7 +11,8 @@ from openpype.tools.utils import ( from openpype.tools.utils.assets_widget import ( SingleSelectAssetsWidget, ASSET_ID_ROLE, - ASSET_NAME_ROLE + ASSET_NAME_ROLE, + ASSET_PATH_ROLE, ) @@ -31,6 +32,15 @@ class CreateWidgetAssetsWidget(SingleSelectAssetsWidget): self._last_filter_height = None + def get_selected_asset_name(self): + if AYON_SERVER_ENABLED: + selection_model = self._view.selectionModel() + indexes = selection_model.selectedRows() + for index in indexes: + return index.data(ASSET_PATH_ROLE) + return None + return super(CreateWidgetAssetsWidget, self).get_selected_asset_name() + def _check_header_height(self): """Catch header height changes. @@ -100,21 +110,24 @@ class AssetsHierarchyModel(QtGui.QStandardItemModel): self._controller = controller self._items_by_name = {} + self._items_by_path = {} self._items_by_asset_id = {} def reset(self): self.clear() self._items_by_name = {} + self._items_by_path = {} self._items_by_asset_id = {} assets_by_parent_id = self._controller.get_asset_hierarchy() items_by_name = {} + items_by_path = {} items_by_asset_id = {} _queue = collections.deque() - _queue.append((self.invisibleRootItem(), None)) + _queue.append((self.invisibleRootItem(), None, None)) while _queue: - parent_item, parent_id = _queue.popleft() + parent_item, parent_id, parent_path = _queue.popleft() children = assets_by_parent_id.get(parent_id) if not children: continue @@ -127,6 +140,9 @@ class AssetsHierarchyModel(QtGui.QStandardItemModel): for name in sorted(children_by_name.keys()): child = children_by_name[name] child_id = child["_id"] + child_path = name + if parent_path: + child_path = "{}/{}".format(parent_path, child_path) has_children = bool(assets_by_parent_id.get(child_id)) icon = get_asset_icon(child, has_children) @@ -138,15 +154,18 @@ class AssetsHierarchyModel(QtGui.QStandardItemModel): item.setData(icon, QtCore.Qt.DecorationRole) item.setData(child_id, ASSET_ID_ROLE) item.setData(name, ASSET_NAME_ROLE) + item.setData(child_path, ASSET_PATH_ROLE) items_by_name[name] = item + items_by_path[child_path] = item items_by_asset_id[child_id] = item items.append(item) - _queue.append((item, child_id)) + _queue.append((item, child_id, child_path)) parent_item.appendRows(items) self._items_by_name = items_by_name + self._items_by_path = items_by_path self._items_by_asset_id = items_by_asset_id def get_index_by_asset_id(self, asset_id): @@ -156,12 +175,20 @@ class AssetsHierarchyModel(QtGui.QStandardItemModel): return QtCore.QModelIndex() def get_index_by_asset_name(self, asset_name): - item = self._items_by_name.get(asset_name) + item = None + if AYON_SERVER_ENABLED: + item = self._items_by_path.get(asset_name) + + if item is None: + item = self._items_by_name.get(asset_name) + if item is None: return QtCore.QModelIndex() return item.index() def name_is_valid(self, item_name): + if AYON_SERVER_ENABLED and item_name in self._items_by_path: + return True return item_name in self._items_by_name @@ -296,7 +323,10 @@ class AssetsDialog(QtWidgets.QDialog): index = self._asset_view.currentIndex() asset_name = None if index.isValid(): - asset_name = index.data(ASSET_NAME_ROLE) + if AYON_SERVER_ENABLED: + asset_name = index.data(ASSET_PATH_ROLE) + else: + asset_name = index.data(ASSET_NAME_ROLE) self._selected_asset = asset_name self.done(1) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index 1bbe73381f..cc0f7a9f97 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -538,6 +538,7 @@ class AssetsField(BaseClickableFrame): Does not change selected items (assets). """ self._name_input.setText(text) + self._name_input.end(False) def set_selected_items(self, asset_names=None): """Set asset names for selection of instances. @@ -1162,7 +1163,10 @@ class GlobalAttrsWidget(QtWidgets.QWidget): invalid_tasks = False for instance in self._current_instances: new_variant_value = instance.get("variant") - new_asset_name = instance.get("asset") + if AYON_SERVER_ENABLED: + new_asset_name = instance.get("folderPath") + else: + new_asset_name = instance.get("asset") new_task_name = instance.get("task") if variant_value is not None: new_variant_value = variant_value @@ -1193,7 +1197,11 @@ class GlobalAttrsWidget(QtWidgets.QWidget): instance["variant"] = variant_value if asset_name is not None: - instance["asset"] = asset_name + if AYON_SERVER_ENABLED: + instance["folderPath"] = asset_name + else: + instance["asset"] = asset_name + instance.set_asset_invalid(False) if task_name is not None: @@ -1282,7 +1290,10 @@ class GlobalAttrsWidget(QtWidgets.QWidget): variants.add(instance.get("variant") or self.unknown_value) families.add(instance.get("family") or self.unknown_value) - asset_name = instance.get("asset") or self.unknown_value + if AYON_SERVER_ENABLED: + asset_name = instance.get("folderPath") or self.unknown_value + else: + asset_name = instance.get("asset") or self.unknown_value task_name = instance.get("task") or "" asset_names.add(asset_name) asset_task_combinations.append((asset_name, task_name)) diff --git a/openpype/tools/utils/assets_widget.py b/openpype/tools/utils/assets_widget.py index a45d762c73..b83f4dfcaf 100644 --- a/openpype/tools/utils/assets_widget.py +++ b/openpype/tools/utils/assets_widget.py @@ -36,6 +36,7 @@ ASSET_ID_ROLE = QtCore.Qt.UserRole + 1 ASSET_NAME_ROLE = QtCore.Qt.UserRole + 2 ASSET_LABEL_ROLE = QtCore.Qt.UserRole + 3 ASSET_UNDERLINE_COLORS_ROLE = QtCore.Qt.UserRole + 4 +ASSET_PATH_ROLE = QtCore.Qt.UserRole + 5 class AssetsView(TreeViewSpinner, DeselectableTreeView): From 69792a5c64446bf4c5b17333f2da14a2b9368e3a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 24 Oct 2023 18:07:06 +0200 Subject: [PATCH 0543/1233] use 'get_asset_name_identifier' in aftereffecs --- .../plugins/create/workfile_creator.py | 22 ++++++++++++++++--- .../plugins/publish/collect_workfile.py | 6 ++++- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/create/workfile_creator.py b/openpype/hosts/aftereffects/plugins/create/workfile_creator.py index 2e7b9d4a7e..5dc3d6592d 100644 --- a/openpype/hosts/aftereffects/plugins/create/workfile_creator.py +++ b/openpype/hosts/aftereffects/plugins/create/workfile_creator.py @@ -1,3 +1,4 @@ +from openpype import AYON_SERVER_ENABLED import openpype.hosts.aftereffects.api as api from openpype.client import get_asset_by_name from openpype.pipeline import ( @@ -43,6 +44,14 @@ class AEWorkfileCreator(AutoCreator): task_name = context.get_current_task_name() host_name = context.host_name + existing_asset_name = None + if existing_instance is not None: + if AYON_SERVER_ENABLED: + existing_asset_name = existing_instance.get("folderPath") + + if existing_asset_name is None: + existing_asset_name = existing_instance["asset"] + if existing_instance is None: asset_doc = get_asset_by_name(project_name, asset_name) subset_name = self.get_subset_name( @@ -50,10 +59,13 @@ class AEWorkfileCreator(AutoCreator): project_name, host_name ) data = { - "asset": asset_name, "task": task_name, "variant": self.default_variant } + if AYON_SERVER_ENABLED: + data["folderPath"] = asset_name + else: + data["asset"] = asset_name data.update(self.get_dynamic_data( self.default_variant, task_name, asset_doc, project_name, host_name, None @@ -68,7 +80,7 @@ class AEWorkfileCreator(AutoCreator): new_instance.data_to_store()) elif ( - existing_instance["asset"] != asset_name + existing_asset_name != asset_name or existing_instance["task"] != task_name ): asset_doc = get_asset_by_name(project_name, asset_name) @@ -76,6 +88,10 @@ class AEWorkfileCreator(AutoCreator): self.default_variant, task_name, asset_doc, project_name, host_name ) - existing_instance["asset"] = asset_name + if AYON_SERVER_ENABLED: + existing_instance["folderPath"] = asset_name + else: + existing_instance["asset"] = asset_name + existing_instance["task"] = task_name existing_instance["subset"] = subset_name diff --git a/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py b/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py index dc557f67fc..58d2757840 100644 --- a/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py +++ b/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py @@ -1,6 +1,8 @@ import os import pyblish.api + +from openpype.client import get_asset_name_identifier from openpype.pipeline.create import get_subset_name @@ -48,9 +50,11 @@ class CollectWorkfile(pyblish.api.ContextPlugin): asset_entity = context.data["assetEntity"] project_entity = context.data["projectEntity"] + asset_name = get_asset_name_identifier(asset_entity) + instance_data = { "active": True, - "asset": asset_entity["name"], + "asset": asset_name, "task": task, "frameStart": context.data['frameStart'], "frameEnd": context.data['frameEnd'], From 18fcfa4a4133ac71873ed5ac67e498891b514f96 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 24 Oct 2023 18:07:17 +0200 Subject: [PATCH 0544/1233] use 'get_asset_name_identifier' in fusion --- .../fusion/plugins/create/create_workfile.py | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_workfile.py b/openpype/hosts/fusion/plugins/create/create_workfile.py index 8acaaa172f..8063e56413 100644 --- a/openpype/hosts/fusion/plugins/create/create_workfile.py +++ b/openpype/hosts/fusion/plugins/create/create_workfile.py @@ -1,6 +1,7 @@ from openpype.hosts.fusion.api import ( get_current_comp ) +from openpype import AYON_SERVER_ENABLED from openpype.client import get_asset_by_name from openpype.pipeline import ( AutoCreator, @@ -68,6 +69,15 @@ class FusionWorkfileCreator(AutoCreator): task_name = self.create_context.get_current_task_name() host_name = self.create_context.host_name + existing_instance_asset = None + if existing_instance is not None: + if AYON_SERVER_ENABLED: + existing_instance_asset = existing_instance.data.get( + "folderPath") + + if not existing_instance_asset: + existing_instance_asset = existing_instance.data.get("asset") + if existing_instance is None: asset_doc = get_asset_by_name(project_name, asset_name) subset_name = self.get_subset_name( @@ -75,10 +85,13 @@ class FusionWorkfileCreator(AutoCreator): project_name, host_name ) data = { - "asset": asset_name, "task": task_name, "variant": self.default_variant } + if AYON_SERVER_ENABLED: + data["folderPath"] = asset_name + else: + data["asset"] = asset_name data.update(self.get_dynamic_data( self.default_variant, task_name, asset_doc, project_name, host_name, None @@ -91,7 +104,7 @@ class FusionWorkfileCreator(AutoCreator): self._add_instance_to_context(new_instance) elif ( - existing_instance["asset"] != asset_name + existing_instance_asset != asset_name or existing_instance["task"] != task_name ): asset_doc = get_asset_by_name(project_name, asset_name) @@ -99,6 +112,9 @@ class FusionWorkfileCreator(AutoCreator): self.default_variant, task_name, asset_doc, project_name, host_name ) - existing_instance["asset"] = asset_name + if AYON_SERVER_ENABLED: + existing_instance["folderPath"] = asset_name + else: + existing_instance["asset"] = asset_name existing_instance["task"] = task_name existing_instance["subset"] = subset_name From 279ab08dfacf6f0916d0162f35c11e72567565e1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 24 Oct 2023 18:07:27 +0200 Subject: [PATCH 0545/1233] use 'get_asset_name_identifier' in celaction --- .../plugins/publish/collect_celaction_instances.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/celaction/plugins/publish/collect_celaction_instances.py b/openpype/hosts/celaction/plugins/publish/collect_celaction_instances.py index c815c1edd4..875f15fcc5 100644 --- a/openpype/hosts/celaction/plugins/publish/collect_celaction_instances.py +++ b/openpype/hosts/celaction/plugins/publish/collect_celaction_instances.py @@ -1,6 +1,8 @@ import os import pyblish.api +from openpype.client import get_asset_name_identifier + class CollectCelactionInstances(pyblish.api.ContextPlugin): """ Adds the celaction render instances """ @@ -17,8 +19,10 @@ class CollectCelactionInstances(pyblish.api.ContextPlugin): asset_entity = context.data["assetEntity"] project_entity = context.data["projectEntity"] + asset_name = get_asset_name_identifier(asset_entity) + shared_instance_data = { - "asset": asset_entity["name"], + "asset": asset_name, "frameStart": asset_entity["data"]["frameStart"], "frameEnd": asset_entity["data"]["frameEnd"], "handleStart": asset_entity["data"]["handleStart"], From a5056ea3fbca99c74f63ccafe409bbc650ec612b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 24 Oct 2023 18:09:43 +0200 Subject: [PATCH 0546/1233] modified maya to follow new usage of 'asset' value --- openpype/hosts/maya/api/plugin.py | 11 ++++++---- .../maya/plugins/create/create_review.py | 7 +++++- .../maya/plugins/create/create_workfile.py | 22 +++++++++++++++---- .../validate_unreal_staticmesh_naming.py | 3 ++- 4 files changed, 33 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py index 3b54954c8a..6c8b5b6b78 100644 --- a/openpype/hosts/maya/api/plugin.py +++ b/openpype/hosts/maya/api/plugin.py @@ -7,6 +7,7 @@ import six from maya import cmds from maya.app.renderSetup.model import renderSetup +from openpype import AYON_SERVER_ENABLED from openpype.lib import BoolDef, Logger from openpype.settings import get_project_settings from openpype.pipeline import ( @@ -463,14 +464,16 @@ class RenderlayerCreator(NewCreator, MayaCreatorBase): # this instance will not have the `instance_node` data yet # until it's been saved/persisted at least once. project_name = self.create_context.get_current_project_name() - + asset_name = self.create_context.get_current_asset_name() instance_data = { - "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"]) + if AYON_SERVER_ENABLED: + instance_data["folderPath"] = asset_name + else: + instance_data["asset"] = asset_name + asset_doc = get_asset_by_name(project_name, asset_name) subset_name = self.get_subset_name( layer.name(), instance_data["task"], diff --git a/openpype/hosts/maya/plugins/create/create_review.py b/openpype/hosts/maya/plugins/create/create_review.py index f60e2406bc..18d661b186 100644 --- a/openpype/hosts/maya/plugins/create/create_review.py +++ b/openpype/hosts/maya/plugins/create/create_review.py @@ -2,6 +2,7 @@ import json from maya import cmds +from openpype import AYON_SERVER_ENABLED from openpype.hosts.maya.api import ( lib, plugin @@ -43,7 +44,11 @@ class CreateReview(plugin.MayaCreator): members = cmds.ls(selection=True) project_name = self.project_name - asset_doc = get_asset_by_name(project_name, instance_data["asset"]) + if AYON_SERVER_ENABLED: + asset_name = instance_data["folderPath"] + else: + asset_name = instance_data["asset"] + asset_doc = get_asset_by_name(project_name, asset_name) task_name = instance_data["task"] preset = lib.get_capture_preset( task_name, diff --git a/openpype/hosts/maya/plugins/create/create_workfile.py b/openpype/hosts/maya/plugins/create/create_workfile.py index d84753cd7f..74629776af 100644 --- a/openpype/hosts/maya/plugins/create/create_workfile.py +++ b/openpype/hosts/maya/plugins/create/create_workfile.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- """Creator plugin for creating workfiles.""" +from openpype import AYON_SERVER_ENABLED from openpype.pipeline import CreatedInstance, AutoCreator -from openpype.client import get_asset_by_name +from openpype.client import get_asset_by_name, get_asset_name_identifier from openpype.hosts.maya.api import plugin from maya import cmds @@ -29,16 +30,27 @@ class CreateWorkfile(plugin.MayaCreatorBase, AutoCreator): task_name = self.create_context.get_current_task_name() host_name = self.create_context.host_name + current_instance_asset = None + if current_instance is not None: + if AYON_SERVER_ENABLED: + current_instance_asset = current_instance.get("folderPath") + if not current_instance_asset: + current_instance_asset = current_instance.get("asset") + if current_instance is None: asset_doc = get_asset_by_name(project_name, asset_name) subset_name = self.get_subset_name( variant, task_name, asset_doc, project_name, host_name ) data = { - "asset": asset_name, "task": task_name, "variant": variant } + if AYON_SERVER_ENABLED: + data["folderPath"] = asset_name + else: + data["asset"] = asset_name + data.update( self.get_dynamic_data( variant, task_name, asset_doc, @@ -50,14 +62,16 @@ class CreateWorkfile(plugin.MayaCreatorBase, AutoCreator): ) self._add_instance_to_context(current_instance) elif ( - current_instance["asset"] != asset_name - or current_instance["task"] != task_name + current_instance_asset != asset_name + or current_instance["task"] != task_name ): # Update instance context if is not the same asset_doc = get_asset_by_name(project_name, asset_name) subset_name = self.get_subset_name( variant, task_name, asset_doc, project_name, host_name ) + asset_name = get_asset_name_identifier(asset_doc) + current_instance["asset"] = asset_name current_instance["task"] = task_name current_instance["subset"] = subset_name 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 58fa9d02bd..42d3dc3ac8 100644 --- a/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py +++ b/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py @@ -102,7 +102,8 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin, cl_r = re.compile(regex_collision) - mesh_name = "{}{}".format(instance.data["asset"], + asset_name = instance.data["assetEntity"]["name"] + mesh_name = "{}{}".format(asset_name, instance.data.get("variant", [])) for obj in collision_set: From 82b1e0b205428dd0496be71bb269bd7d179b67c1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 24 Oct 2023 18:12:08 +0200 Subject: [PATCH 0547/1233] modified houdini to follow new asset naming --- .../houdini/plugins/create/create_workfile.py | 23 +++++++++--- .../plugins/publish/collect_usd_bootstrap.py | 35 ++++++++++++------- .../plugins/publish/validate_subset_name.py | 10 +++--- 3 files changed, 47 insertions(+), 21 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_workfile.py b/openpype/hosts/houdini/plugins/create/create_workfile.py index cc45a6c2a8..04a844bdf5 100644 --- a/openpype/hosts/houdini/plugins/create/create_workfile.py +++ b/openpype/hosts/houdini/plugins/create/create_workfile.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- """Creator plugin for creating workfiles.""" +from openpype import AYON_SERVER_ENABLED from openpype.hosts.houdini.api import plugin from openpype.hosts.houdini.api.lib import read, imprint from openpype.hosts.houdini.api.pipeline import CONTEXT_CONTAINER @@ -30,16 +31,27 @@ class CreateWorkfile(plugin.HoudiniCreatorBase, AutoCreator): task_name = self.create_context.get_current_task_name() host_name = self.host_name + current_instance_asset = None + if current_instance is not None: + if AYON_SERVER_ENABLED: + current_instance_asset = current_instance.get("folderPath") + if not current_instance_asset: + current_instance_asset = current_instance.get("asset") + if current_instance is None: asset_doc = get_asset_by_name(project_name, asset_name) subset_name = self.get_subset_name( variant, task_name, asset_doc, project_name, host_name ) data = { - "asset": asset_name, "task": task_name, "variant": variant } + if AYON_SERVER_ENABLED: + data["folderpath"] = asset_name + else: + data["asset"] = asset_name + data.update( self.get_dynamic_data( variant, task_name, asset_doc, @@ -51,15 +63,18 @@ class CreateWorkfile(plugin.HoudiniCreatorBase, AutoCreator): ) self._add_instance_to_context(current_instance) elif ( - current_instance["asset"] != asset_name - or current_instance["task"] != task_name + current_instance_asset != asset_name + or current_instance["task"] != task_name ): # Update instance context if is not the same asset_doc = get_asset_by_name(project_name, asset_name) subset_name = self.get_subset_name( variant, task_name, asset_doc, project_name, host_name ) - current_instance["asset"] = asset_name + if AYON_SERVER_ENABLED: + current_instance["folderPath"] = asset_name + else: + current_instance["asset"] = asset_name current_instance["task"] = task_name current_instance["subset"] = subset_name diff --git a/openpype/hosts/houdini/plugins/publish/collect_usd_bootstrap.py b/openpype/hosts/houdini/plugins/publish/collect_usd_bootstrap.py index 14a8e3c056..462cf99b9c 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_usd_bootstrap.py +++ b/openpype/hosts/houdini/plugins/publish/collect_usd_bootstrap.py @@ -1,6 +1,10 @@ import pyblish.api -from openpype.client import get_subset_by_name, get_asset_by_name +from openpype.client import ( + get_subset_by_name, + get_asset_by_name, + get_asset_name_identifier, +) import openpype.lib.usdlib as usdlib @@ -51,8 +55,9 @@ class CollectUsdBootstrap(pyblish.api.InstancePlugin): self.log.debug("Add bootstrap for: %s" % bootstrap) project_name = instance.context.data["projectName"] - asset = get_asset_by_name(project_name, instance.data["asset"]) - assert asset, "Asset must exist: %s" % asset + asset_name = instance.data["asset"] + asset_doc = get_asset_by_name(project_name, asset_name) + assert asset_doc, "Asset must exist: %s" % asset_name # Check which are not about to be created and don't exist yet required = {"shot": ["usdShot"], "asset": ["usdAsset"]}.get(bootstrap) @@ -67,19 +72,21 @@ class CollectUsdBootstrap(pyblish.api.InstancePlugin): required += list(layers) self.log.debug("Checking required bootstrap: %s" % required) - for subset in required: - if self._subset_exists(project_name, instance, subset, asset): + for subset_name in required: + if self._subset_exists( + project_name, instance, subset_name, asset_doc + ): continue self.log.debug( "Creating {0} USD bootstrap: {1} {2}".format( - bootstrap, asset["name"], subset + bootstrap, asset_name, subset_name ) ) - new = instance.context.create_instance(subset) - new.data["subset"] = subset - new.data["label"] = "{0} ({1})".format(subset, asset["name"]) + new = instance.context.create_instance(subset_name) + new.data["subset"] = subset_name + new.data["label"] = "{0} ({1})".format(subset_name, asset_name) new.data["family"] = "usd.bootstrap" new.data["comment"] = "Automated bootstrap USD file." new.data["publishFamilies"] = ["usd"] @@ -91,21 +98,23 @@ class CollectUsdBootstrap(pyblish.api.InstancePlugin): for key in ["asset"]: new.data[key] = instance.data[key] - def _subset_exists(self, project_name, instance, subset, asset): + def _subset_exists(self, project_name, instance, subset_name, asset_doc): """Return whether subset exists in current context or in database.""" # Allow it to be created during this publish session context = instance.context + + asset_doc_name = get_asset_name_identifier(asset_doc) for inst in context: if ( - inst.data["subset"] == subset - and inst.data["asset"] == asset["name"] + inst.data["subset"] == subset_name + and inst.data["asset"] == asset_doc_name ): return True # Or, if they already exist in the database we can # skip them too. if get_subset_by_name( - project_name, subset, asset["_id"], fields=["_id"] + project_name, subset_name, asset_doc["_id"], fields=["_id"] ): return True return False diff --git a/openpype/hosts/houdini/plugins/publish/validate_subset_name.py b/openpype/hosts/houdini/plugins/publish/validate_subset_name.py index bb3648f361..7bed74ebb1 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_subset_name.py +++ b/openpype/hosts/houdini/plugins/publish/validate_subset_name.py @@ -54,12 +54,13 @@ class ValidateSubsetName(pyblish.api.InstancePlugin, rop_node = hou.node(instance.data["instance_node"]) # Check subset name + asset_doc = instance.data["assetEntity"] subset_name = get_subset_name( family=instance.data["family"], variant=instance.data["variant"], task_name=instance.data["task"], - asset_doc=instance.data["assetEntity"], - dynamic_data={"asset": instance.data["asset"]} + asset_doc=asset_doc, + dynamic_data={"asset": asset_doc["name"]} ) if instance.data.get("subset") != subset_name: @@ -76,12 +77,13 @@ class ValidateSubsetName(pyblish.api.InstancePlugin, rop_node = hou.node(instance.data["instance_node"]) # Check subset name + asset_doc = instance.data["assetEntity"] subset_name = get_subset_name( family=instance.data["family"], variant=instance.data["variant"], task_name=instance.data["task"], - asset_doc=instance.data["assetEntity"], - dynamic_data={"asset": instance.data["asset"]} + asset_doc=asset_doc, + dynamic_data={"asset": asset_doc["name"]} ) instance.data["subset"] = subset_name From 23c41fe12aedeedd567cd9d290e243a5871c07bf Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 24 Oct 2023 18:13:00 +0200 Subject: [PATCH 0548/1233] modified photoshop to follow new 'asset' name usage --- openpype/hosts/photoshop/lib.py | 22 ++++++++++++++++--- .../plugins/create/create_flatten_image.py | 22 +++++++++++++++---- .../plugins/publish/collect_auto_image.py | 3 ++- .../plugins/publish/collect_auto_review.py | 4 +++- .../plugins/publish/collect_auto_workfile.py | 3 ++- 5 files changed, 44 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/photoshop/lib.py b/openpype/hosts/photoshop/lib.py index 9f603a70d2..654528410a 100644 --- a/openpype/hosts/photoshop/lib.py +++ b/openpype/hosts/photoshop/lib.py @@ -1,5 +1,6 @@ import re +from openpype import AYON_SERVER_ENABLED import openpype.hosts.photoshop.api as api from openpype.client import get_asset_by_name from openpype.lib import prepare_template_data @@ -43,6 +44,15 @@ class PSAutoCreator(AutoCreator): asset_name = context.get_current_asset_name() task_name = context.get_current_task_name() host_name = context.host_name + + existing_instance_asset = None + if existing_instance is not None: + if AYON_SERVER_ENABLED: + existing_instance_asset = existing_instance.get("folderPath") + + if not existing_instance_asset: + existing_instance_asset = existing_instance.get("asset") + if existing_instance is None: asset_doc = get_asset_by_name(project_name, asset_name) subset_name = self.get_subset_name( @@ -50,10 +60,13 @@ class PSAutoCreator(AutoCreator): project_name, host_name ) data = { - "asset": asset_name, "task": task_name, "variant": self.default_variant } + if AYON_SERVER_ENABLED: + data["folderPath"] = asset_name + else: + data["asset"] = asset_name data.update(self.get_dynamic_data( self.default_variant, task_name, asset_doc, project_name, host_name, None @@ -70,7 +83,7 @@ class PSAutoCreator(AutoCreator): new_instance.data_to_store()) elif ( - existing_instance["asset"] != asset_name + existing_instance_asset != asset_name or existing_instance["task"] != task_name ): asset_doc = get_asset_by_name(project_name, asset_name) @@ -78,7 +91,10 @@ class PSAutoCreator(AutoCreator): self.default_variant, task_name, asset_doc, project_name, host_name ) - existing_instance["asset"] = asset_name + if AYON_SERVER_ENABLED: + existing_instance["folderPath"] = asset_name + else: + existing_instance["asset"] = asset_name existing_instance["task"] = task_name existing_instance["subset"] = subset_name diff --git a/openpype/hosts/photoshop/plugins/create/create_flatten_image.py b/openpype/hosts/photoshop/plugins/create/create_flatten_image.py index afde77fdb4..942f8f4989 100644 --- a/openpype/hosts/photoshop/plugins/create/create_flatten_image.py +++ b/openpype/hosts/photoshop/plugins/create/create_flatten_image.py @@ -1,5 +1,6 @@ from openpype.pipeline import CreatedInstance +from openpype import AYON_SERVER_ENABLED from openpype.lib import BoolDef import openpype.hosts.photoshop.api as api from openpype.hosts.photoshop.lib import PSAutoCreator, clean_subset_name @@ -37,6 +38,14 @@ class AutoImageCreator(PSAutoCreator): host_name = context.host_name asset_doc = get_asset_by_name(project_name, asset_name) + existing_instance_asset = None + if existing_instance is not None: + if AYON_SERVER_ENABLED: + existing_instance_asset = existing_instance.get("folderPath") + + if not existing_instance_asset: + existing_instance_asset = existing_instance.get("asset") + if existing_instance is None: subset_name = self.get_subset_name( self.default_variant, task_name, asset_doc, @@ -44,9 +53,12 @@ class AutoImageCreator(PSAutoCreator): ) data = { - "asset": asset_name, "task": task_name, } + if AYON_SERVER_ENABLED: + data["folderPath"] = asset_name + else: + data["asset"] = asset_name if not self.active_on_create: data["active"] = False @@ -62,15 +74,17 @@ class AutoImageCreator(PSAutoCreator): new_instance.data_to_store()) elif ( # existing instance from different context - existing_instance["asset"] != asset_name + existing_instance_asset != asset_name or existing_instance["task"] != task_name ): subset_name = self.get_subset_name( self.default_variant, task_name, asset_doc, project_name, host_name ) - - existing_instance["asset"] = asset_name + if AYON_SERVER_ENABLED: + existing_instance["folderPath"] = asset_name + else: + existing_instance["asset"] = asset_name existing_instance["task"] = task_name existing_instance["subset"] = subset_name diff --git a/openpype/hosts/photoshop/plugins/publish/collect_auto_image.py b/openpype/hosts/photoshop/plugins/publish/collect_auto_image.py index 77f1a3e91f..038ae9ff6c 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_auto_image.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_auto_image.py @@ -1,5 +1,6 @@ import pyblish.api +from openpype.client import get_asset_name_identifier from openpype.hosts.photoshop import api as photoshop from openpype.pipeline.create import get_subset_name @@ -27,7 +28,7 @@ class CollectAutoImage(pyblish.api.ContextPlugin): task_name = context.data["anatomyData"]["task"]["name"] host_name = context.data["hostName"] asset_doc = context.data["assetEntity"] - asset_name = asset_doc["name"] + asset_name = get_asset_name_identifier(asset_doc) auto_creator = proj_settings.get( "photoshop", {}).get( diff --git a/openpype/hosts/photoshop/plugins/publish/collect_auto_review.py b/openpype/hosts/photoshop/plugins/publish/collect_auto_review.py index 82ba0ac09c..37e9e8bae8 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_auto_review.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_auto_review.py @@ -7,6 +7,7 @@ Provides: """ import pyblish.api +from openpype.client import get_asset_name_identifier from openpype.hosts.photoshop import api as photoshop from openpype.pipeline.create import get_subset_name @@ -65,7 +66,8 @@ class CollectAutoReview(pyblish.api.ContextPlugin): task_name = context.data["anatomyData"]["task"]["name"] host_name = context.data["hostName"] asset_doc = context.data["assetEntity"] - asset_name = asset_doc["name"] + + asset_name = get_asset_name_identifier(asset_doc) subset_name = get_subset_name( family, diff --git a/openpype/hosts/photoshop/plugins/publish/collect_auto_workfile.py b/openpype/hosts/photoshop/plugins/publish/collect_auto_workfile.py index 01dc50af40..be5a641d51 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_auto_workfile.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_auto_workfile.py @@ -1,6 +1,7 @@ import os import pyblish.api +from openpype.client import get_asset_name_identifier from openpype.hosts.photoshop import api as photoshop from openpype.pipeline.create import get_subset_name @@ -69,8 +70,8 @@ class CollectAutoWorkfile(pyblish.api.ContextPlugin): task_name = context.data["anatomyData"]["task"]["name"] host_name = context.data["hostName"] asset_doc = context.data["assetEntity"] - asset_name = asset_doc["name"] + asset_name = get_asset_name_identifier(asset_doc) subset_name = get_subset_name( family, variant, From 7c7f9f175c041292e8a11306763a0863cc6bae95 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 24 Oct 2023 18:13:23 +0200 Subject: [PATCH 0549/1233] modified substance to follow new 'asset' usage --- .../plugins/create/create_workfile.py | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/substancepainter/plugins/create/create_workfile.py b/openpype/hosts/substancepainter/plugins/create/create_workfile.py index d7f31f9dcf..8aa696f11d 100644 --- a/openpype/hosts/substancepainter/plugins/create/create_workfile.py +++ b/openpype/hosts/substancepainter/plugins/create/create_workfile.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- """Creator plugin for creating workfiles.""" +from openpype import AYON_SERVER_ENABLED from openpype.pipeline import CreatedInstance, AutoCreator from openpype.client import get_asset_by_name @@ -41,6 +42,13 @@ class CreateWorkfile(AutoCreator): if instance.creator_identifier == self.identifier ), None) + current_instance_asset = None + if current_instance is not None: + if AYON_SERVER_ENABLED: + current_instance_asset = current_instance.get("folderPath") + if not current_instance_asset: + current_instance_asset = current_instance.get("asset") + if current_instance is None: self.log.info("Auto-creating workfile instance...") asset_doc = get_asset_by_name(project_name, asset_name) @@ -48,22 +56,28 @@ class CreateWorkfile(AutoCreator): variant, task_name, asset_doc, project_name, host_name ) data = { - "asset": asset_name, "task": task_name, "variant": variant } + if AYON_SERVER_ENABLED: + data["folderPath"] = asset_name + else: + data["asset"] = asset_name current_instance = self.create_instance_in_context(subset_name, data) elif ( - current_instance["asset"] != asset_name - or current_instance["task"] != task_name + current_instance_asset != asset_name + or current_instance["task"] != task_name ): # Update instance context if is not the same asset_doc = get_asset_by_name(project_name, asset_name) subset_name = self.get_subset_name( variant, task_name, asset_doc, project_name, host_name ) - current_instance["asset"] = asset_name + if AYON_SERVER_ENABLED: + current_instance["folderPath"] = asset_name + else: + current_instance["asset"] = asset_name current_instance["task"] = task_name current_instance["subset"] = subset_name From 7d366371a5eec266083721b1ac651813103978fb Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 24 Oct 2023 18:17:35 +0200 Subject: [PATCH 0550/1233] modified most of code in traypublisher to follow new asset usage --- openpype/hosts/traypublisher/api/plugin.py | 11 ++++++++--- .../plugins/create/create_colorspace_look.py | 7 ++++++- .../traypublisher/plugins/create/create_editorial.py | 6 +++++- .../plugins/create/create_movie_batch.py | 9 ++++++++- 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/traypublisher/api/plugin.py b/openpype/hosts/traypublisher/api/plugin.py index 36e041a32c..14c66fa08f 100644 --- a/openpype/hosts/traypublisher/api/plugin.py +++ b/openpype/hosts/traypublisher/api/plugin.py @@ -1,7 +1,9 @@ +from openpype import AYON_SERVER_ENABLED from openpype.client import ( get_assets, get_subsets, get_last_versions, + get_asset_name_identifier, ) from openpype.lib.attribute_definitions import ( FileDef, @@ -114,7 +116,10 @@ class SettingsCreator(TrayPublishCreator): # Fill 'version_to_use' if version control is enabled if self.allow_version_control: - asset_name = data["asset"] + if AYON_SERVER_ENABLED: + asset_name = data["folderPath"] + else: + asset_name = data["asset"] subset_docs_by_asset_id = self._prepare_next_versions( [asset_name], [subset_name]) version = subset_docs_by_asset_id[asset_name].get(subset_name) @@ -162,10 +167,10 @@ class SettingsCreator(TrayPublishCreator): asset_docs = get_assets( self.project_name, asset_names=asset_names, - fields=["_id", "name"] + fields=["_id", "name", "data.parents"] ) asset_names_by_id = { - asset_doc["_id"]: asset_doc["name"] + asset_doc["_id"]: get_asset_name_identifier(asset_doc) for asset_doc in asset_docs } subset_docs = list(get_subsets( diff --git a/openpype/hosts/traypublisher/plugins/create/create_colorspace_look.py b/openpype/hosts/traypublisher/plugins/create/create_colorspace_look.py index 5628d0973f..ac4c72a0ce 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_colorspace_look.py +++ b/openpype/hosts/traypublisher/plugins/create/create_colorspace_look.py @@ -6,6 +6,7 @@ production type `ociolook`. All files are published as representation. """ from pathlib import Path +from openpype import AYON_SERVER_ENABLED from openpype.client import get_asset_by_name from openpype.lib.attribute_definitions import ( FileDef, EnumDef, TextDef, UISeparatorDef @@ -54,8 +55,12 @@ This creator publishes color space look file (LUT). # this should never happen raise CreatorError("Missing files from representation") + if AYON_SERVER_ENABLED: + asset_name = instance_data["folderPath"] + else: + asset_name = instance_data["asset"] asset_doc = get_asset_by_name( - self.project_name, instance_data["asset"]) + self.project_name, asset_name) subset_name = self.get_subset_name( variant=instance_data["variant"], diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py index 8640500b18..23cf066362 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_editorial.py +++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py @@ -1,6 +1,7 @@ import os from copy import deepcopy import opentimelineio as otio +from openpype import AYON_SERVER_ENABLED from openpype.client import ( get_asset_by_name, get_project @@ -215,7 +216,10 @@ or updating already created. Publishing will create OTIO file. ] } # Create otio editorial instance - asset_name = instance_data["asset"] + if AYON_SERVER_ENABLED: + asset_name = instance_data["folderPath"] + else: + asset_name = instance_data["asset"] asset_doc = get_asset_by_name(self.project_name, asset_name) if pre_create_data["fps"] == "from_selection": diff --git a/openpype/hosts/traypublisher/plugins/create/create_movie_batch.py b/openpype/hosts/traypublisher/plugins/create/create_movie_batch.py index 3454b6e135..8fa65c7fff 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_movie_batch.py +++ b/openpype/hosts/traypublisher/plugins/create/create_movie_batch.py @@ -2,6 +2,8 @@ import copy import os import re +from openpype import AYON_SERVER_ENABLED +from openpype.client import get_asset_name_identifier from openpype.lib import ( FileDef, BoolDef, @@ -64,8 +66,13 @@ class BatchMovieCreator(TrayPublishCreator): subset_name, task_name = self._get_subset_and_task( asset_doc, data["variant"], self.project_name) + asset_name = get_asset_name_identifier(asset_doc) + instance_data["task"] = task_name - instance_data["asset"] = asset_doc["name"] + if AYON_SERVER_ENABLED: + instance_data["folderPath"] = asset_name + else: + instance_data["asset"] = asset_name # Create new instance new_instance = CreatedInstance(self.family, subset_name, From 8360c321bdf3713df75a7b0225d4cb59ac22b02f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 24 Oct 2023 18:39:04 +0200 Subject: [PATCH 0551/1233] modified tvpaint to use new 'asset' handling --- .../tvpaint/plugins/create/create_render.py | 54 +++++++++++++++---- .../tvpaint/plugins/create/create_review.py | 21 ++++++-- .../tvpaint/plugins/create/create_workfile.py | 20 +++++-- .../plugins/publish/validate_asset_name.py | 12 ++++- 4 files changed, 89 insertions(+), 18 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render.py b/openpype/hosts/tvpaint/plugins/create/create_render.py index b7a7c208d9..667103432e 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render.py @@ -37,7 +37,8 @@ Todos: import collections from typing import Any, Optional, Union -from openpype.client import get_asset_by_name +from openpype import AYON_SERVER_ENABLED +from openpype.client import get_asset_by_name, get_asset_name_identifier from openpype.lib import ( prepare_template_data, AbstractAttrDef, @@ -784,18 +785,25 @@ class TVPaintAutoDetectRenderCreator(TVPaintCreator): project_name, host_name=self.create_context.host_name, ) + asset_name = get_asset_name_identifier(asset_doc) if existing_instance is not None: - existing_instance["asset"] = asset_doc["name"] + if AYON_SERVER_ENABLED: + existing_instance["folderPath"] = asset_name + else: + existing_instance["asset"] = asset_name existing_instance["task"] = task_name existing_instance["subset"] = subset_name return existing_instance instance_data: dict[str, str] = { - "asset": asset_doc["name"], "task": task_name, "family": creator.family, "variant": variant } + if AYON_SERVER_ENABLED: + instance_data["folderPath"] = asset_name + else: + instance_data["asset"] = asset_name pre_create_data: dict[str, str] = { "group_id": group_id, "mark_for_review": mark_for_review @@ -820,6 +828,8 @@ class TVPaintAutoDetectRenderCreator(TVPaintCreator): for layer_name in render_pass["layer_names"]: render_pass_by_layer_name[layer_name] = render_pass + asset_name = get_asset_name_identifier(asset_doc) + for layer in layers: layer_name = layer["name"] variant = layer_name @@ -838,17 +848,25 @@ class TVPaintAutoDetectRenderCreator(TVPaintCreator): ) if render_pass is not None: - render_pass["asset"] = asset_doc["name"] + if AYON_SERVER_ENABLED: + render_pass["folderPath"] = asset_name + else: + render_pass["asset"] = asset_name + render_pass["task"] = task_name render_pass["subset"] = subset_name continue instance_data: dict[str, str] = { - "asset": asset_doc["name"], "task": task_name, "family": creator.family, "variant": variant } + if AYON_SERVER_ENABLED: + instance_data["folderPath"] = asset_name + else: + instance_data["asset"] = asset_name + pre_create_data: dict[str, Any] = { "render_layer_instance_id": render_layer_instance.id, "layer_names": [layer_name], @@ -882,9 +900,13 @@ class TVPaintAutoDetectRenderCreator(TVPaintCreator): def create(self, subset_name, instance_data, pre_create_data): project_name: str = self.create_context.get_current_project_name() - asset_name: str = instance_data["asset"] + if AYON_SERVER_ENABLED: + asset_name: str = instance_data["folderPath"] + else: + asset_name: str = instance_data["asset"] task_name: str = instance_data["task"] - asset_doc: dict[str, Any] = get_asset_by_name(project_name, asset_name) + asset_doc: dict[str, Any] = get_asset_by_name( + project_name, asset_name) render_layers_by_group_id: dict[int, CreatedInstance] = {} render_passes_by_render_layer_id: dict[int, list[CreatedInstance]] = ( @@ -1061,7 +1083,6 @@ class TVPaintSceneRenderCreator(TVPaintAutoCreator): host_name ) data = { - "asset": asset_name, "task": task_name, "variant": self.default_variant, "creator_attributes": { @@ -1073,6 +1094,10 @@ class TVPaintSceneRenderCreator(TVPaintAutoCreator): self.default_pass_name ) } + if AYON_SERVER_ENABLED: + data["folderPath"] = asset_name + else: + data["asset"] = asset_name if not self.active_on_create: data["active"] = False @@ -1101,8 +1126,14 @@ class TVPaintSceneRenderCreator(TVPaintAutoCreator): asset_name = create_context.get_current_asset_name() task_name = create_context.get_current_task_name() + existing_name = None + if AYON_SERVER_ENABLED: + existing_name = existing_instance.get("folderPath") + if existing_name is None: + existing_name = existing_instance["asset"] + if ( - existing_instance["asset"] != asset_name + existing_name != asset_name or existing_instance["task"] != task_name ): asset_doc = get_asset_by_name(project_name, asset_name) @@ -1114,7 +1145,10 @@ class TVPaintSceneRenderCreator(TVPaintAutoCreator): host_name, existing_instance ) - existing_instance["asset"] = asset_name + if AYON_SERVER_ENABLED: + existing_instance["folderPath"] = asset_name + else: + existing_instance["asset"] = asset_name existing_instance["task"] = task_name existing_instance["subset"] = subset_name diff --git a/openpype/hosts/tvpaint/plugins/create/create_review.py b/openpype/hosts/tvpaint/plugins/create/create_review.py index 7bb7510a8e..265cef00ef 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_review.py +++ b/openpype/hosts/tvpaint/plugins/create/create_review.py @@ -1,3 +1,4 @@ +from openpype import AYON_SERVER_ENABLED from openpype.client import get_asset_by_name from openpype.pipeline import CreatedInstance from openpype.hosts.tvpaint.api.plugin import TVPaintAutoCreator @@ -33,6 +34,13 @@ class TVPaintReviewCreator(TVPaintAutoCreator): asset_name = create_context.get_current_asset_name() task_name = create_context.get_current_task_name() + existing_asset_name = None + if existing_instance is not None: + if AYON_SERVER_ENABLED: + existing_asset_name = existing_instance.get("folderPath") + if existing_asset_name is None: + existing_asset_name = existing_instance.get("asset") + if existing_instance is None: asset_doc = get_asset_by_name(project_name, asset_name) subset_name = self.get_subset_name( @@ -43,10 +51,14 @@ class TVPaintReviewCreator(TVPaintAutoCreator): host_name ) data = { - "asset": asset_name, "task": task_name, "variant": self.default_variant } + if AYON_SERVER_ENABLED: + data["folderPath"] = asset_name + else: + data["asset"] = asset_name + if not self.active_on_create: data["active"] = False @@ -59,7 +71,7 @@ class TVPaintReviewCreator(TVPaintAutoCreator): self._add_instance_to_context(new_instance) elif ( - existing_instance["asset"] != asset_name + existing_asset_name != asset_name or existing_instance["task"] != task_name ): asset_doc = get_asset_by_name(project_name, asset_name) @@ -71,6 +83,9 @@ class TVPaintReviewCreator(TVPaintAutoCreator): host_name, existing_instance ) - existing_instance["asset"] = asset_name + if AYON_SERVER_ENABLED: + existing_instance["folderPath"] = asset_name + else: + existing_instance["asset"] = asset_name existing_instance["task"] = task_name existing_instance["subset"] = subset_name diff --git a/openpype/hosts/tvpaint/plugins/create/create_workfile.py b/openpype/hosts/tvpaint/plugins/create/create_workfile.py index c3982c0eca..eec0f8483f 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_workfile.py +++ b/openpype/hosts/tvpaint/plugins/create/create_workfile.py @@ -1,3 +1,4 @@ +from openpype import AYON_SERVER_ENABLED from openpype.client import get_asset_by_name from openpype.pipeline import CreatedInstance from openpype.hosts.tvpaint.api.plugin import TVPaintAutoCreator @@ -29,6 +30,13 @@ class TVPaintWorkfileCreator(TVPaintAutoCreator): asset_name = create_context.get_current_asset_name() task_name = create_context.get_current_task_name() + existing_asset_name = None + if existing_instance is not None: + if AYON_SERVER_ENABLED: + existing_asset_name = existing_instance.get("folderPath") + if existing_asset_name is None: + existing_asset_name = existing_instance.get("asset") + if existing_instance is None: asset_doc = get_asset_by_name(project_name, asset_name) subset_name = self.get_subset_name( @@ -39,10 +47,13 @@ class TVPaintWorkfileCreator(TVPaintAutoCreator): host_name ) data = { - "asset": asset_name, "task": task_name, "variant": self.default_variant } + if AYON_SERVER_ENABLED: + data["folderPath"] = asset_name + else: + data["asset"] = asset_name new_instance = CreatedInstance( self.family, subset_name, data, self @@ -53,7 +64,7 @@ class TVPaintWorkfileCreator(TVPaintAutoCreator): self._add_instance_to_context(new_instance) elif ( - existing_instance["asset"] != asset_name + existing_asset_name != asset_name or existing_instance["task"] != task_name ): asset_doc = get_asset_by_name(project_name, asset_name) @@ -65,6 +76,9 @@ class TVPaintWorkfileCreator(TVPaintAutoCreator): host_name, existing_instance ) - existing_instance["asset"] = asset_name + if AYON_SERVER_ENABLED: + existing_instance["folderPath"] = asset_name + else: + existing_instance["asset"] = asset_name existing_instance["task"] = task_name existing_instance["subset"] = subset_name diff --git a/openpype/hosts/tvpaint/plugins/publish/validate_asset_name.py b/openpype/hosts/tvpaint/plugins/publish/validate_asset_name.py index 9347960d3f..dc29e6c278 100644 --- a/openpype/hosts/tvpaint/plugins/publish/validate_asset_name.py +++ b/openpype/hosts/tvpaint/plugins/publish/validate_asset_name.py @@ -1,4 +1,5 @@ import pyblish.api +from openpype import AYON_SERVER_ENABLED from openpype.pipeline import ( PublishXmlValidationError, OptionalPyblishPluginMixin, @@ -24,12 +25,19 @@ class FixAssetNames(pyblish.api.Action): old_instance_items = list_instances() new_instance_items = [] for instance_item in old_instance_items: - instance_asset_name = instance_item.get("asset") + if AYON_SERVER_ENABLED: + instance_asset_name = instance_item.get("folderPath") + else: + instance_asset_name = instance_item.get("asset") + if ( instance_asset_name and instance_asset_name != context_asset_name ): - instance_item["asset"] = context_asset_name + if AYON_SERVER_ENABLED: + instance_item["folderPath"] = context_asset_name + else: + instance_item["asset"] = context_asset_name new_instance_items.append(instance_item) write_instances(new_instance_items) From 5ada46f2a05dc71b621a7cffe077cb9dcff0e997 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 24 Oct 2023 18:41:37 +0200 Subject: [PATCH 0552/1233] modified some parts of hiero to follow new 'asset' handling --- openpype/hosts/hiero/api/plugin.py | 6 ++---- .../publish/collect_frame_tag_instances.py | 7 ++++++- .../plugins/publish/precollect_instances.py | 1 - .../plugins/publish/precollect_workfile.py | 17 ++++++++++------- .../collect_assetbuilds.py | 18 ++++++++++-------- 5 files changed, 28 insertions(+), 21 deletions(-) diff --git a/openpype/hosts/hiero/api/plugin.py b/openpype/hosts/hiero/api/plugin.py index 52f96261b2..0e0632e032 100644 --- a/openpype/hosts/hiero/api/plugin.py +++ b/openpype/hosts/hiero/api/plugin.py @@ -11,7 +11,6 @@ import qargparse from openpype.settings import get_current_project_settings from openpype.lib import Logger from openpype.pipeline import LoaderPlugin, LegacyCreator -from openpype.pipeline.context_tools import get_current_project_asset from openpype.pipeline.load import get_representation_path_from_context from . import lib @@ -494,9 +493,8 @@ class ClipLoader: joint `data` key with asset.data dict into the representation """ - asset_name = self.context["representation"]["context"]["asset"] - asset_doc = get_current_project_asset(asset_name) - log.debug("__ asset_doc: {}".format(pformat(asset_doc))) + + asset_doc = self.context["asset"] self.data["assetData"] = asset_doc["data"] def _make_track_item(self, source_bin_item, audio=False): diff --git a/openpype/hosts/hiero/plugins/publish/collect_frame_tag_instances.py b/openpype/hosts/hiero/plugins/publish/collect_frame_tag_instances.py index 982a34efd6..79bf67b336 100644 --- a/openpype/hosts/hiero/plugins/publish/collect_frame_tag_instances.py +++ b/openpype/hosts/hiero/plugins/publish/collect_frame_tag_instances.py @@ -5,6 +5,8 @@ import json import pyblish.api +from openpype.client import get_asset_name_identifier + class CollectFrameTagInstances(pyblish.api.ContextPlugin): """Collect frames from tags. @@ -99,6 +101,9 @@ class CollectFrameTagInstances(pyblish.api.ContextPlugin): # first collect all available subset tag frames subset_data = {} + context_asset_doc = context.data["assetEntity"] + context_asset_name = get_asset_name_identifier(context_asset_doc) + for tag_data in sequence_tags: frame = int(tag_data["start"]) @@ -115,7 +120,7 @@ class CollectFrameTagInstances(pyblish.api.ContextPlugin): subset_data[subset] = { "frames": [frame], "format": tag_data["format"], - "asset": context.data["assetEntity"]["name"] + "asset": context_asset_name } return subset_data diff --git a/openpype/hosts/hiero/plugins/publish/precollect_instances.py b/openpype/hosts/hiero/plugins/publish/precollect_instances.py index 3f9da2cf60..65b8fed49c 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_instances.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_instances.py @@ -178,7 +178,6 @@ class PrecollectInstances(pyblish.api.ContextPlugin): def create_shot_instance(self, context, **data): master_layer = data.get("heroTrack") hierarchy_data = data.get("hierarchyData") - asset = data.get("asset") item = data.get("item") clip_name = item.name() diff --git a/openpype/hosts/hiero/plugins/publish/precollect_workfile.py b/openpype/hosts/hiero/plugins/publish/precollect_workfile.py index 5a66581531..1d6bdc0257 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_workfile.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_workfile.py @@ -7,6 +7,7 @@ from qtpy.QtGui import QPixmap import hiero.ui +from openpype import AYON_SERVER_ENABLED from openpype.hosts.hiero.api.otio import hiero_export @@ -17,9 +18,10 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin): order = pyblish.api.CollectorOrder - 0.491 def process(self, context): + asset_name = context.data["asset"] + if AYON_SERVER_ENABLED: + asset_name = asset_name.split("/")[-1] - asset = context.data["asset"] - subset = "workfile" active_timeline = hiero.ui.activeSequence() project = active_timeline.project() fps = active_timeline.framerate().toFloat() @@ -59,13 +61,14 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin): 'files': base_name, "stagingDir": staging_dir, } - + family = "workfile" instance_data = { - "name": "{}_{}".format(asset, subset), - "asset": asset, - "subset": "{}{}".format(asset, subset.capitalize()), + "name": "{}_{}".format(asset_name, family), + "asset": context.data["asset"], + # TODO use 'get_subset_name' + "subset": "{}{}".format(asset_name, family.capitalize()), "item": project, - "family": "workfile", + "family": family, "families": [], "representations": [workfile_representation, thumb_representation] } diff --git a/openpype/hosts/hiero/plugins/publish_old_workflow/collect_assetbuilds.py b/openpype/hosts/hiero/plugins/publish_old_workflow/collect_assetbuilds.py index 767f7c30f7..37370497a5 100644 --- a/openpype/hosts/hiero/plugins/publish_old_workflow/collect_assetbuilds.py +++ b/openpype/hosts/hiero/plugins/publish_old_workflow/collect_assetbuilds.py @@ -1,5 +1,6 @@ from pyblish import api -from openpype.client import get_assets + +from openpype.client import get_assets, get_asset_name_identifier class CollectAssetBuilds(api.ContextPlugin): @@ -19,10 +20,13 @@ class CollectAssetBuilds(api.ContextPlugin): def process(self, context): project_name = context.data["projectName"] asset_builds = {} - for asset in get_assets(project_name): - if asset["data"]["entityType"] == "AssetBuild": - self.log.debug("Found \"{}\" in database.".format(asset)) - asset_builds[asset["name"]] = asset + for asset_doc in get_assets(project_name): + if asset_doc["data"].get("entityType") != "AssetBuild": + continue + + asset_name = get_asset_name_identifier(asset_doc) + self.log.debug("Found \"{}\" in database.".format(asset_doc)) + asset_builds[asset_name] = asset_doc for instance in context: if instance.data["family"] != "clip": @@ -50,9 +54,7 @@ class CollectAssetBuilds(api.ContextPlugin): # Collect asset builds. data = {"assetbuilds": []} for name in asset_names: - data["assetbuilds"].append( - asset_builds[name] - ) + data["assetbuilds"].append(asset_builds[name]) self.log.debug( "Found asset builds: {}".format(data["assetbuilds"]) ) From 51bebd0f1e24264e72358d58ef55f77062aaba3b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 24 Oct 2023 18:42:14 +0200 Subject: [PATCH 0553/1233] modified part of flame to follow new 'asset' naming --- .../hosts/flame/plugins/publish/collect_timeline_otio.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/flame/plugins/publish/collect_timeline_otio.py b/openpype/hosts/flame/plugins/publish/collect_timeline_otio.py index f8cfa9e963..20ac048986 100644 --- a/openpype/hosts/flame/plugins/publish/collect_timeline_otio.py +++ b/openpype/hosts/flame/plugins/publish/collect_timeline_otio.py @@ -1,5 +1,6 @@ import pyblish.api +from openpype.client import get_asset_name_identifier import openpype.hosts.flame.api as opfapi from openpype.hosts.flame.otio import flame_export from openpype.pipeline.create import get_subset_name @@ -33,13 +34,15 @@ class CollecTimelineOTIO(pyblish.api.ContextPlugin): project_settings=context.data["project_settings"] ) + asset_name = get_asset_name_identifier(asset_doc) + # adding otio timeline to context with opfapi.maintained_segment_selection(sequence) as selected_seg: otio_timeline = flame_export.create_otio_timeline(sequence) instance_data = { "name": subset_name, - "asset": asset_doc["name"], + "asset": asset_name, "subset": subset_name, "family": "workfile", "families": [] From 9d617db64cac3e06ea373e05511bce4fc0728c19 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 24 Oct 2023 18:46:35 +0200 Subject: [PATCH 0554/1233] use only folder name to create instance name --- .../hosts/resolve/plugins/publish/precollect_workfile.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/resolve/plugins/publish/precollect_workfile.py b/openpype/hosts/resolve/plugins/publish/precollect_workfile.py index a2f3eaed7a..28b2350f01 100644 --- a/openpype/hosts/resolve/plugins/publish/precollect_workfile.py +++ b/openpype/hosts/resolve/plugins/publish/precollect_workfile.py @@ -15,6 +15,8 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin): def process(self, context): asset = get_current_asset_name() + # AYON compatibility split name and use last piece + _asset_name = asset.split("/")[-1] subset = "workfile" project = rapi.get_current_project() fps = project.GetSetting("timelineFrameRate") @@ -24,9 +26,9 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin): otio_timeline = davinci_export.create_otio_timeline(project) instance_data = { - "name": "{}_{}".format(asset, subset), + "name": "{}_{}".format(_asset_name, subset), "asset": asset, - "subset": "{}{}".format(asset, subset.capitalize()), + "subset": "{}{}".format(_asset_name, subset.capitalize()), "item": project, "family": "workfile", "families": [] From e34c0b9ecd1505657969676bb0d9a062498c6c52 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 24 Oct 2023 18:50:54 +0200 Subject: [PATCH 0555/1233] avoid unnecessary call to database --- openpype/hosts/resolve/api/plugin.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py index 8381f81acb..a3d533d3d7 100644 --- a/openpype/hosts/resolve/api/plugin.py +++ b/openpype/hosts/resolve/api/plugin.py @@ -1,10 +1,11 @@ import re import uuid +import copy + import qargparse from qtpy import QtWidgets, QtCore from openpype.settings import get_current_project_settings -from openpype.pipeline.context_tools import get_current_project_asset from openpype.pipeline import ( LegacyCreator, LoaderPlugin, @@ -379,8 +380,8 @@ class ClipLoader: joint `data` key with asset.data dict into the representation """ - asset_name = self.context["representation"]["context"]["asset"] - self.data["assetData"] = get_current_project_asset(asset_name)["data"] + + self.data["assetData"] = copy.deepcopy(self.context["asset"]["data"]) def load(self, files): """Load clip into timeline From 24e20ee12a3bbebff4f7896cdefe201d5b14c279 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 25 Oct 2023 03:25:13 +0000 Subject: [PATCH 0556/1233] [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 e2e3c663af..0bdf2d278a 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.17.4-nightly.1" +__version__ = "3.17.4-nightly.2" From a22d1baa15c03e9889dbf836a5f975c05b8751e7 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 25 Oct 2023 12:31:54 +0800 Subject: [PATCH 0557/1233] clean up the codes --- openpype/hosts/max/api/lib.py | 4 ++-- openpype/hosts/max/plugins/publish/extract_pointcache.py | 9 +-------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 61a2d938a1..e44cea42ad 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -515,11 +515,11 @@ def get_plugins() -> list: def suspended_refresh(): """Suspended refresh for scene and modify panel redraw. """ - rt.disableSceneRedraw() - rt.suspendEditing() if is_headless(): yield return + rt.disableSceneRedraw() + rt.suspendEditing() try: yield diff --git a/openpype/hosts/max/plugins/publish/extract_pointcache.py b/openpype/hosts/max/plugins/publish/extract_pointcache.py index 4b3407274c..d15cd676f2 100644 --- a/openpype/hosts/max/plugins/publish/extract_pointcache.py +++ b/openpype/hosts/max/plugins/publish/extract_pointcache.py @@ -109,21 +109,14 @@ class ExtractAlembic(publish.Extractor, class ExtractCameraAlembic(ExtractAlembic): """Extract Camera with AlembicExport.""" - order = pyblish.api.ExtractorOrder - 0.1 label = "Extract Alembic Camera" - hosts = ["max"] families = ["camera"] optional = True class ExtractModel(ExtractAlembic): - """ - Extract Geometry in Alembic Format - """ - - order = pyblish.api.ExtractorOrder - 0.1 + """Extract Geometry in Alembic Format""" label = "Extract Geometry (Alembic)" - hosts = ["max"] families = ["model"] optional = True From d108641aa7d9575d03de750945cdd4608c857361 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 25 Oct 2023 12:48:33 +0800 Subject: [PATCH 0558/1233] remove optional=ture in children class of abc extractor --- openpype/hosts/max/plugins/publish/extract_pointcache.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_pointcache.py b/openpype/hosts/max/plugins/publish/extract_pointcache.py index d15cd676f2..0097432f43 100644 --- a/openpype/hosts/max/plugins/publish/extract_pointcache.py +++ b/openpype/hosts/max/plugins/publish/extract_pointcache.py @@ -52,7 +52,7 @@ class ExtractAlembic(publish.Extractor, label = "Extract Pointcache" hosts = ["max"] families = ["pointcache"] - optional = False + optional = True def process(self, instance): if not self.is_active(instance.data): @@ -111,14 +111,12 @@ class ExtractCameraAlembic(ExtractAlembic): label = "Extract Alembic Camera" families = ["camera"] - optional = True class ExtractModel(ExtractAlembic): """Extract Geometry in Alembic Format""" label = "Extract Geometry (Alembic)" families = ["model"] - optional = True def _set_abc_attributes(self, instance): attr_values = self.get_attr_values_from_data(instance.data) From 439afe4adbbd524b252184cd7c4033b7ad817cb2 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 25 Oct 2023 15:21:27 +0800 Subject: [PATCH 0559/1233] narrow down to the 4 image formats support for review --- openpype/hosts/max/plugins/create/create_review.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/openpype/hosts/max/plugins/create/create_review.py b/openpype/hosts/max/plugins/create/create_review.py index bbcdce90b7..4b1149faa1 100644 --- a/openpype/hosts/max/plugins/create/create_review.py +++ b/openpype/hosts/max/plugins/create/create_review.py @@ -33,10 +33,7 @@ class CreateReview(plugin.MaxCreator): pre_create_data) def get_instance_attr_defs(self): - image_format_enum = [ - "bmp", "cin", "exr", "jpg", "hdr", "rgb", "png", - "rla", "rpf", "dds", "sgi", "tga", "tif", "vrimg" - ] + image_format_enum = ["exr", "jpg", "png", "tif"] visual_style_preset_enum = [ "Realistic", "Shaded", "Facets", From 76864ad8f5c886804153496e8f49a02ce86b0cd3 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 25 Oct 2023 16:56:34 +0800 Subject: [PATCH 0560/1233] raise publish error in collect review due to the loaded abc camera doesn't support fov attribute properties and narrow down the image format to 3 for reviews --- openpype/hosts/max/plugins/create/create_review.py | 2 +- .../hosts/max/plugins/publish/collect_review.py | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/max/plugins/create/create_review.py b/openpype/hosts/max/plugins/create/create_review.py index 4b1149faa1..8052b74f06 100644 --- a/openpype/hosts/max/plugins/create/create_review.py +++ b/openpype/hosts/max/plugins/create/create_review.py @@ -33,7 +33,7 @@ class CreateReview(plugin.MaxCreator): pre_create_data) def get_instance_attr_defs(self): - image_format_enum = ["exr", "jpg", "png", "tif"] + image_format_enum = ["exr", "jpg", "png"] visual_style_preset_enum = [ "Realistic", "Shaded", "Facets", diff --git a/openpype/hosts/max/plugins/publish/collect_review.py b/openpype/hosts/max/plugins/publish/collect_review.py index 1f488f8180..beecd391a5 100644 --- a/openpype/hosts/max/plugins/publish/collect_review.py +++ b/openpype/hosts/max/plugins/publish/collect_review.py @@ -5,7 +5,10 @@ import pyblish.api from pymxs import runtime as rt from openpype.lib import BoolDef from openpype.hosts.max.api.lib import get_max_version -from openpype.pipeline.publish import OpenPypePyblishPluginMixin +from openpype.pipeline.publish import ( + OpenPypePyblishPluginMixin, + KnownPublishError +) class CollectReview(pyblish.api.InstancePlugin, @@ -24,7 +27,13 @@ class CollectReview(pyblish.api.InstancePlugin, for node in nodes: if rt.classOf(node) in rt.Camera.classes: camera_name = node.name - focal_length = node.fov + if rt.isProperty(node, "fov"): + focal_length = node.fov + else: + raise KnownPublishError( + "Invalid object found in 'Review' container." + " Only native max Camera supported" + ) creator_attrs = instance.data["creator_attributes"] attr_values = self.get_attr_values_from_data(instance.data) From dfd239172cb324d4bff8e5fa89cf39c672abb5d0 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 25 Oct 2023 17:20:06 +0800 Subject: [PATCH 0561/1233] do not do normapath in update function --- openpype/hosts/max/plugins/load/load_tycache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/load/load_tycache.py b/openpype/hosts/max/plugins/load/load_tycache.py index 1373404274..41ea267c3d 100644 --- a/openpype/hosts/max/plugins/load/load_tycache.py +++ b/openpype/hosts/max/plugins/load/load_tycache.py @@ -42,7 +42,7 @@ class TyCacheLoader(load.LoaderPlugin): """update the container""" from pymxs import runtime as rt - path = os.path.normpath(get_representation_path(representation)) + path = get_representation_path(representation) node = rt.GetNodeByName(container["instance_node"]) node_list = get_previous_loaded_object(node) update_custom_attribute_data(node, node_list) From 26a575d5941df35cb3e08d45275321cab8cc0819 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 25 Oct 2023 17:34:53 +0800 Subject: [PATCH 0562/1233] improve the code for camera check on the node --- .../max/plugins/publish/collect_review.py | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/collect_review.py b/openpype/hosts/max/plugins/publish/collect_review.py index beecd391a5..8bf41c13ab 100644 --- a/openpype/hosts/max/plugins/publish/collect_review.py +++ b/openpype/hosts/max/plugins/publish/collect_review.py @@ -22,18 +22,26 @@ class CollectReview(pyblish.api.InstancePlugin, def process(self, instance): nodes = instance.data["members"] - focal_length = None - camera_name = None - for node in nodes: - if rt.classOf(node) in rt.Camera.classes: - camera_name = node.name - if rt.isProperty(node, "fov"): - focal_length = node.fov - else: - raise KnownPublishError( - "Invalid object found in 'Review' container." - " Only native max Camera supported" - ) + def is_camera(node): + is_camera_class = rt.classOf(node) in rt.Camera.classes + return is_camera_class and rt.isProperty(node, "fov") + + # Use first camera in instance + cameras = [node for node in nodes if is_camera(node)] + if cameras: + if len(cameras) > 1: + self.log.warning( + "Found more than one camera in instance, using first " + f"one found: {cameras[0]}" + ) + camera = cameras[0] + camera_name = camera.name + focal_length = camera.fov + else: + raise KnownPublishError( + "Invalid object found in 'Review' container." + " Only native max Camera supported" + ) creator_attrs = instance.data["creator_attributes"] attr_values = self.get_attr_values_from_data(instance.data) From 8a2e9af88662ef96daec2906ccd9caa5559f0e96 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 25 Oct 2023 17:35:48 +0800 Subject: [PATCH 0563/1233] hound --- openpype/hosts/max/plugins/publish/collect_review.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/max/plugins/publish/collect_review.py b/openpype/hosts/max/plugins/publish/collect_review.py index 8bf41c13ab..0d48cc1ff3 100644 --- a/openpype/hosts/max/plugins/publish/collect_review.py +++ b/openpype/hosts/max/plugins/publish/collect_review.py @@ -22,6 +22,7 @@ class CollectReview(pyblish.api.InstancePlugin, def process(self, instance): nodes = instance.data["members"] + def is_camera(node): is_camera_class = rt.classOf(node) in rt.Camera.classes return is_camera_class and rt.isProperty(node, "fov") From 053cc44891343624d73de6a03ed3c560103ff118 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 25 Oct 2023 11:40:26 +0200 Subject: [PATCH 0564/1233] removed unnecessary line --- openpype/client/server/entities.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/client/server/entities.py b/openpype/client/server/entities.py index c1e27eabb9..c735c558d5 100644 --- a/openpype/client/server/entities.py +++ b/openpype/client/server/entities.py @@ -183,7 +183,6 @@ def get_asset_by_name(project_name, asset_name, fields=None): return None - def _folders_query(project_name, con, fields, **kwargs): if fields is None or "tasks" in fields: folders = get_folders_with_tasks( From 7942e33a117e487d5d86c15e8d919fc5dc947ff6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 25 Oct 2023 12:01:58 +0200 Subject: [PATCH 0565/1233] be expicit about source of asset and folder path --- .../hosts/fusion/plugins/create/create_workfile.py | 14 ++++++-------- .../houdini/plugins/create/create_workfile.py | 12 ++++++------ .../hosts/maya/plugins/create/create_workfile.py | 12 ++++++------ .../plugins/create/create_flatten_image.py | 13 ++++++------- .../plugins/create/create_workfile.py | 12 ++++++------ 5 files changed, 30 insertions(+), 33 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_workfile.py b/openpype/hosts/fusion/plugins/create/create_workfile.py index 8063e56413..4092086ea4 100644 --- a/openpype/hosts/fusion/plugins/create/create_workfile.py +++ b/openpype/hosts/fusion/plugins/create/create_workfile.py @@ -69,14 +69,12 @@ class FusionWorkfileCreator(AutoCreator): task_name = self.create_context.get_current_task_name() host_name = self.create_context.host_name - existing_instance_asset = None - if existing_instance is not None: - if AYON_SERVER_ENABLED: - existing_instance_asset = existing_instance.data.get( - "folderPath") - - if not existing_instance_asset: - existing_instance_asset = existing_instance.data.get("asset") + if existing_instance is None: + existing_instance_asset = None + elif AYON_SERVER_ENABLED: + existing_instance_asset = existing_instance["folderPath"] + else: + existing_instance_asset = existing_instance["asset"] if existing_instance is None: asset_doc = get_asset_by_name(project_name, asset_name) diff --git a/openpype/hosts/houdini/plugins/create/create_workfile.py b/openpype/hosts/houdini/plugins/create/create_workfile.py index 04a844bdf5..f8ee68ebc9 100644 --- a/openpype/hosts/houdini/plugins/create/create_workfile.py +++ b/openpype/hosts/houdini/plugins/create/create_workfile.py @@ -31,12 +31,12 @@ class CreateWorkfile(plugin.HoudiniCreatorBase, AutoCreator): task_name = self.create_context.get_current_task_name() host_name = self.host_name - current_instance_asset = None - if current_instance is not None: - if AYON_SERVER_ENABLED: - current_instance_asset = current_instance.get("folderPath") - if not current_instance_asset: - current_instance_asset = current_instance.get("asset") + if current_instance is None: + current_instance_asset = None + elif AYON_SERVER_ENABLED: + current_instance_asset = current_instance["folderPath"] + else: + current_instance_asset = current_instance["asset"] if current_instance is None: asset_doc = get_asset_by_name(project_name, asset_name) diff --git a/openpype/hosts/maya/plugins/create/create_workfile.py b/openpype/hosts/maya/plugins/create/create_workfile.py index 74629776af..7282fc6b8b 100644 --- a/openpype/hosts/maya/plugins/create/create_workfile.py +++ b/openpype/hosts/maya/plugins/create/create_workfile.py @@ -30,12 +30,12 @@ class CreateWorkfile(plugin.MayaCreatorBase, AutoCreator): task_name = self.create_context.get_current_task_name() host_name = self.create_context.host_name - current_instance_asset = None - if current_instance is not None: - if AYON_SERVER_ENABLED: - current_instance_asset = current_instance.get("folderPath") - if not current_instance_asset: - current_instance_asset = current_instance.get("asset") + if current_instance is None: + current_instance_asset = None + elif AYON_SERVER_ENABLED: + current_instance_asset = current_instance["folderPath"] + else: + current_instance_asset = current_instance["asset"] if current_instance is None: asset_doc = get_asset_by_name(project_name, asset_name) diff --git a/openpype/hosts/photoshop/plugins/create/create_flatten_image.py b/openpype/hosts/photoshop/plugins/create/create_flatten_image.py index 942f8f4989..24be9df0e0 100644 --- a/openpype/hosts/photoshop/plugins/create/create_flatten_image.py +++ b/openpype/hosts/photoshop/plugins/create/create_flatten_image.py @@ -38,13 +38,12 @@ class AutoImageCreator(PSAutoCreator): host_name = context.host_name asset_doc = get_asset_by_name(project_name, asset_name) - existing_instance_asset = None - if existing_instance is not None: - if AYON_SERVER_ENABLED: - existing_instance_asset = existing_instance.get("folderPath") - - if not existing_instance_asset: - existing_instance_asset = existing_instance.get("asset") + if existing_instance is None: + existing_instance_asset = None + elif AYON_SERVER_ENABLED: + existing_instance_asset = existing_instance["folderPath"] + else: + existing_instance_asset = existing_instance["asset"] if existing_instance is None: subset_name = self.get_subset_name( diff --git a/openpype/hosts/substancepainter/plugins/create/create_workfile.py b/openpype/hosts/substancepainter/plugins/create/create_workfile.py index 8aa696f11d..c73277e405 100644 --- a/openpype/hosts/substancepainter/plugins/create/create_workfile.py +++ b/openpype/hosts/substancepainter/plugins/create/create_workfile.py @@ -42,12 +42,12 @@ class CreateWorkfile(AutoCreator): if instance.creator_identifier == self.identifier ), None) - current_instance_asset = None - if current_instance is not None: - if AYON_SERVER_ENABLED: - current_instance_asset = current_instance.get("folderPath") - if not current_instance_asset: - current_instance_asset = current_instance.get("asset") + if current_instance is None: + current_instance_asset = None + elif AYON_SERVER_ENABLED: + current_instance_asset = current_instance["folderPath"] + else: + current_instance_asset = current_instance["asset"] if current_instance is None: self.log.info("Auto-creating workfile instance...") From a12d599b442540a46b88212fcdc873ad1c85db46 Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Wed, 25 Oct 2023 14:04:00 +0200 Subject: [PATCH 0566/1233] change creator labels --- openpype/hosts/blender/plugins/create/create_action.py | 2 +- openpype/hosts/blender/plugins/create/create_animation.py | 2 +- openpype/hosts/blender/plugins/create/create_camera.py | 2 +- openpype/hosts/blender/plugins/create/create_layout.py | 2 +- openpype/hosts/blender/plugins/create/create_model.py | 2 +- openpype/hosts/blender/plugins/create/create_pointcache.py | 2 +- openpype/hosts/blender/plugins/create/create_review.py | 2 +- openpype/hosts/blender/plugins/create/create_rig.py | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/blender/plugins/create/create_action.py b/openpype/hosts/blender/plugins/create/create_action.py index d766fce038..e7b689c54e 100644 --- a/openpype/hosts/blender/plugins/create/create_action.py +++ b/openpype/hosts/blender/plugins/create/create_action.py @@ -38,7 +38,7 @@ class CreateAction(openpype.hosts.blender.api.plugin.BlenderCreator): { "id": "pyblish.avalon.instance", "creator_identifier": self.identifier, - "label": self.label, + "label": subset_name, "task": get_current_task_name(), "subset": subset_name, "instance_node": instance_node, diff --git a/openpype/hosts/blender/plugins/create/create_animation.py b/openpype/hosts/blender/plugins/create/create_animation.py index 88ae9e5996..8b4214ceda 100644 --- a/openpype/hosts/blender/plugins/create/create_animation.py +++ b/openpype/hosts/blender/plugins/create/create_animation.py @@ -58,7 +58,7 @@ class CreateAnimation(plugin.BlenderCreator): { "id": "pyblish.avalon.instance", "creator_identifier": self.identifier, - "label": self.label, + "label": subset_name, "task": get_current_task_name(), "subset": subset_name, "instance_node": instance_node, diff --git a/openpype/hosts/blender/plugins/create/create_camera.py b/openpype/hosts/blender/plugins/create/create_camera.py index 026b5739d6..4747e50b2e 100644 --- a/openpype/hosts/blender/plugins/create/create_camera.py +++ b/openpype/hosts/blender/plugins/create/create_camera.py @@ -56,7 +56,7 @@ class CreateCamera(plugin.BlenderCreator): { "id": "pyblish.avalon.instance", "creator_identifier": self.identifier, - "label": self.label, + "label": subset_name, "task": get_current_task_name(), "subset": subset_name, "instance_node": instance_node, diff --git a/openpype/hosts/blender/plugins/create/create_layout.py b/openpype/hosts/blender/plugins/create/create_layout.py index f46ae58a43..0c97d57af3 100644 --- a/openpype/hosts/blender/plugins/create/create_layout.py +++ b/openpype/hosts/blender/plugins/create/create_layout.py @@ -55,7 +55,7 @@ class CreateLayout(plugin.BlenderCreator): { "id": "pyblish.avalon.instance", "creator_identifier": self.identifier, - "label": self.label, + "label": subset_name, "task": get_current_task_name(), "subset": subset_name, "instance_node": instance_node, diff --git a/openpype/hosts/blender/plugins/create/create_model.py b/openpype/hosts/blender/plugins/create/create_model.py index 069b78626b..3c8e9c4900 100644 --- a/openpype/hosts/blender/plugins/create/create_model.py +++ b/openpype/hosts/blender/plugins/create/create_model.py @@ -55,7 +55,7 @@ class CreateModel(plugin.BlenderCreator): { "id": "pyblish.avalon.instance", "creator_identifier": self.identifier, - "label": self.label, + "label": subset_name, "task": get_current_task_name(), "subset": subset_name, "instance_node": instance_node, diff --git a/openpype/hosts/blender/plugins/create/create_pointcache.py b/openpype/hosts/blender/plugins/create/create_pointcache.py index 3054b81ef5..a40bd5af61 100644 --- a/openpype/hosts/blender/plugins/create/create_pointcache.py +++ b/openpype/hosts/blender/plugins/create/create_pointcache.py @@ -38,7 +38,7 @@ class CreatePointcache(plugin.BlenderCreator): { "id": "pyblish.avalon.instance", "creator_identifier": self.identifier, - "label": self.label, + "label": subset_name, "task": get_current_task_name(), "subset": subset_name, "instance_node": instance_node, diff --git a/openpype/hosts/blender/plugins/create/create_review.py b/openpype/hosts/blender/plugins/create/create_review.py index 10a96c94fd..8c9a8d5927 100644 --- a/openpype/hosts/blender/plugins/create/create_review.py +++ b/openpype/hosts/blender/plugins/create/create_review.py @@ -54,7 +54,7 @@ class CreateReview(plugin.BlenderCreator): { "id": "pyblish.avalon.instance", "creator_identifier": self.identifier, - "label": self.label, + "label": subset_name, "task": get_current_task_name(), "subset": subset_name, "instance_node": instance_node, diff --git a/openpype/hosts/blender/plugins/create/create_rig.py b/openpype/hosts/blender/plugins/create/create_rig.py index 8daffe638d..110a9f5c8e 100644 --- a/openpype/hosts/blender/plugins/create/create_rig.py +++ b/openpype/hosts/blender/plugins/create/create_rig.py @@ -55,7 +55,7 @@ class CreateRig(plugin.BlenderCreator): { "id": "pyblish.avalon.instance", "creator_identifier": self.identifier, - "label": self.label, + "label": subset_name, "task": get_current_task_name(), "subset": subset_name, "instance_node": instance_node, From 40fe7391b2c8470b0880852bf09e95b706e37ed3 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 25 Oct 2023 20:46:28 +0800 Subject: [PATCH 0567/1233] knownPublishError comment tweaks --- openpype/hosts/max/plugins/publish/collect_review.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/collect_review.py b/openpype/hosts/max/plugins/publish/collect_review.py index 0d48cc1ff3..2e3df5b116 100644 --- a/openpype/hosts/max/plugins/publish/collect_review.py +++ b/openpype/hosts/max/plugins/publish/collect_review.py @@ -40,8 +40,9 @@ class CollectReview(pyblish.api.InstancePlugin, focal_length = camera.fov else: raise KnownPublishError( - "Invalid object found in 'Review' container." - " Only native max Camera supported" + "Unable to find a valid camera in 'Review' container." + " Only native max Camera supported. " + f"Found objects: {cameras}" ) creator_attrs = instance.data["creator_attributes"] attr_values = self.get_attr_values_from_data(instance.data) From cd6a2941d2410c2ca314589bae4f79182ccd6f41 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 25 Oct 2023 21:15:52 +0800 Subject: [PATCH 0568/1233] comment update for knownpublisherror --- openpype/hosts/max/plugins/publish/collect_review.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/collect_review.py b/openpype/hosts/max/plugins/publish/collect_review.py index 2e3df5b116..b1d9c2d25e 100644 --- a/openpype/hosts/max/plugins/publish/collect_review.py +++ b/openpype/hosts/max/plugins/publish/collect_review.py @@ -42,7 +42,7 @@ class CollectReview(pyblish.api.InstancePlugin, raise KnownPublishError( "Unable to find a valid camera in 'Review' container." " Only native max Camera supported. " - f"Found objects: {cameras}" + f"Found objects: {nodes}" ) creator_attrs = instance.data["creator_attributes"] attr_values = self.get_attr_values_from_data(instance.data) From c3ae2a3a09bdd72824f950682faf418931651ff7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 25 Oct 2023 15:20:28 +0200 Subject: [PATCH 0569/1233] be expicit about source of asset and folder path in photoshop --- openpype/hosts/photoshop/lib.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/photoshop/lib.py b/openpype/hosts/photoshop/lib.py index 654528410a..5c8dff947d 100644 --- a/openpype/hosts/photoshop/lib.py +++ b/openpype/hosts/photoshop/lib.py @@ -45,13 +45,12 @@ class PSAutoCreator(AutoCreator): task_name = context.get_current_task_name() host_name = context.host_name - existing_instance_asset = None - if existing_instance is not None: - if AYON_SERVER_ENABLED: - existing_instance_asset = existing_instance.get("folderPath") - - if not existing_instance_asset: - existing_instance_asset = existing_instance.get("asset") + if existing_instance is None: + existing_instance_asset = None + elif AYON_SERVER_ENABLED: + existing_instance_asset = existing_instance["folderPath"] + else: + existing_instance_asset = existing_instance["asset"] if existing_instance is None: asset_doc = get_asset_by_name(project_name, asset_name) From 803fb616492f5e34feec7a3a3bdd7e7599dcc8d8 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 25 Oct 2023 22:31:27 +0800 Subject: [PATCH 0570/1233] validate loaded plugins tweaks in 3dsmax --- .../plugins/publish/validate_loaded_plugin.py | 69 +++++++++++++++++++ .../plugins/publish/validate_usd_plugin.py | 49 ------------- .../defaults/project_settings/max.json | 5 ++ .../schemas/schema_max_publish.json | 25 +++++++ .../max/server/settings/publishers.py | 18 ++++- server_addon/max/server/version.py | 2 +- 6 files changed, 117 insertions(+), 51 deletions(-) create mode 100644 openpype/hosts/max/plugins/publish/validate_loaded_plugin.py delete mode 100644 openpype/hosts/max/plugins/publish/validate_usd_plugin.py diff --git a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py new file mode 100644 index 0000000000..10cbdf22fb --- /dev/null +++ b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +"""Validator for USD plugin.""" +from pyblish.api import InstancePlugin, ValidatorOrder +from pymxs import runtime as rt + +from openpype.pipeline.publish import ( + RepairAction, + OptionalPyblishPluginMixin, + PublishValidationError +) +from openpype.hosts.max.api.lib import get_plugins + + +class ValidateLoadedPlugin(OptionalPyblishPluginMixin, + InstancePlugin): + """Validates if the specific plugin is loaded in 3ds max. + User can add the plugins they want to check through""" + + order = ValidatorOrder + hosts = ["max"] + label = "Validate Loaded Plugin" + optional = True + actions = [RepairAction] + + def get_invalid(self, instance): + """Plugin entry point.""" + invalid = [] + # display all DLL loaded plugins in Max + plugin_info = get_plugins() + project_settings = instance.context.data[ + "project_settings"]["max"]["publish"] + target_plugins = project_settings[ + "ValidateLoadedPlugin"]["plugins_for_check"] + for plugin in target_plugins: + if plugin.lower() not in plugin_info: + invalid.append( + f"Plugin {plugin} not exists in 3dsMax Plugin List.") + for i, _ in enumerate(plugin_info): + if plugin.lower() == rt.pluginManager.pluginDllName(i): + if not rt.pluginManager.isPluginDllLoaded(i): + invalid.append( + f"Plugin {plugin} not loaded.") + return invalid + + def process(self, instance): + invalid_plugins = self.get_invalid(instance) + if invalid_plugins: + bullet_point_invalid_statement = "\n".join( + "- {}".format(invalid) for invalid in invalid_plugins + ) + report = ( + "Required plugins fails to load.\n\n" + f"{bullet_point_invalid_statement}\n\n" + "You can use repair action to load the plugin." + ) + raise PublishValidationError(report, title="Required Plugins unloaded") + + @classmethod + def repair(cls, instance): + plugin_info = get_plugins() + project_settings = instance.context.data[ + "project_settings"]["max"]["publish"] + target_plugins = project_settings[ + "ValidateLoadedPlugin"]["plugins_for_check"] + for plugin in target_plugins: + for i, _ in enumerate(plugin_info): + if plugin == rt.pluginManager.pluginDllName(i): + if not rt.pluginManager.isPluginDllLoaded(i): + rt.pluginManager.loadPluginDll(i) diff --git a/openpype/hosts/max/plugins/publish/validate_usd_plugin.py b/openpype/hosts/max/plugins/publish/validate_usd_plugin.py deleted file mode 100644 index 36c4291925..0000000000 --- a/openpype/hosts/max/plugins/publish/validate_usd_plugin.py +++ /dev/null @@ -1,49 +0,0 @@ -# -*- coding: utf-8 -*- -"""Validator for USD plugin.""" -from pyblish.api import InstancePlugin, ValidatorOrder -from pymxs import runtime as rt - -from openpype.pipeline import ( - OptionalPyblishPluginMixin, - PublishValidationError -) - - -def get_plugins() -> list: - """Get plugin list from 3ds max.""" - manager = rt.PluginManager - count = manager.pluginDllCount - plugin_info_list = [] - for p in range(1, count + 1): - plugin_info = manager.pluginDllName(p) - plugin_info_list.append(plugin_info) - - return plugin_info_list - - -class ValidateUSDPlugin(OptionalPyblishPluginMixin, - InstancePlugin): - """Validates if USD plugin is installed or loaded in 3ds max.""" - - order = ValidatorOrder - 0.01 - families = ["model"] - hosts = ["max"] - label = "Validate USD Plugin loaded" - optional = True - - def process(self, instance): - """Plugin entry point.""" - - for sc in ValidateUSDPlugin.__subclasses__(): - self.log.info(sc) - - if not self.is_active(instance.data): - return - - plugin_info = get_plugins() - usd_import = "usdimport.dli" - if usd_import not in plugin_info: - raise PublishValidationError(f"USD Plugin {usd_import} not found") - usd_export = "usdexport.dle" - if usd_export not in plugin_info: - raise PublishValidationError(f"USD Plugin {usd_export} not found") diff --git a/openpype/settings/defaults/project_settings/max.json b/openpype/settings/defaults/project_settings/max.json index bfb1aa4aeb..45246fdf2b 100644 --- a/openpype/settings/defaults/project_settings/max.json +++ b/openpype/settings/defaults/project_settings/max.json @@ -36,6 +36,11 @@ "enabled": true, "optional": true, "active": true + }, + "ValidateLoadedPlugin": { + "enabled": false, + "optional": true, + "plugins_for_check": [] } } } diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json index ea08c735a6..4490c5353d 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json @@ -28,6 +28,31 @@ "label": "Active" } ] + }, + { + "type": "dict", + "collapsible": true, + "key": "ValidateLoadedPlugin", + "label": "Validate Loaded Plugin", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "boolean", + "key": "optional", + "label": "Optional" + }, + { + "type": "list", + "key": "plugins_for_check", + "label": "Plugins Needed For Check", + "object_type": "text" + } + ] } ] } diff --git a/server_addon/max/server/settings/publishers.py b/server_addon/max/server/settings/publishers.py index a695b85e89..8a28224a07 100644 --- a/server_addon/max/server/settings/publishers.py +++ b/server_addon/max/server/settings/publishers.py @@ -3,6 +3,14 @@ from pydantic import Field from ayon_server.settings import BaseSettingsModel +class ValidateLoadedPluginModel(BaseSettingsModel): + enabled: bool = Field(title="ValidateLoadedPlugin") + optional: bool = Field(title="Optional") + plugins_for_check: list[str] = Field( + default_factory=list, title="Plugins Needed For Check" + ) + + class BasicValidateModel(BaseSettingsModel): enabled: bool = Field(title="Enabled") optional: bool = Field(title="Optional") @@ -15,12 +23,20 @@ class PublishersModel(BaseSettingsModel): title="Validate Frame Range", section="Validators" ) - + ValidateLoadedPlugin: ValidateLoadedPluginModel = Field( + default_factory=ValidateLoadedPluginModel, + title="Validate Loaded Plugin" + ) DEFAULT_PUBLISH_SETTINGS = { "ValidateFrameRange": { "enabled": True, "optional": True, "active": True + }, + "ValidateLoadedPlugin": { + "enabled": False, + "optional": True, + "plugins_for_check": [] } } diff --git a/server_addon/max/server/version.py b/server_addon/max/server/version.py index 3dc1f76bc6..485f44ac21 100644 --- a/server_addon/max/server/version.py +++ b/server_addon/max/server/version.py @@ -1 +1 @@ -__version__ = "0.1.0" +__version__ = "0.1.1" From 77776d0943ae8750876f98521a9e493dbcdf584e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 25 Oct 2023 22:53:28 +0800 Subject: [PATCH 0571/1233] hound --- openpype/hosts/max/plugins/publish/validate_loaded_plugin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py index 10cbdf22fb..44343bada2 100644 --- a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py +++ b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py @@ -53,7 +53,8 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, f"{bullet_point_invalid_statement}\n\n" "You can use repair action to load the plugin." ) - raise PublishValidationError(report, title="Required Plugins unloaded") + raise PublishValidationError( + report, title="Required Plugins unloaded") @classmethod def repair(cls, instance): From f35b3508ea737877b71226615fc9747ec0fdbe17 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 25 Oct 2023 17:45:23 +0200 Subject: [PATCH 0572/1233] Fix of no_of_frames (#5819) If it throws exception, no_of_frames wont be available. This approach is used to limit need to decide if published file is image or video-like. Hopefully exception is fast enough and would be still necessary for rare cases of weird video-likes files. --- .../webpublisher/plugins/publish/collect_published_files.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py index 1416255083..6bb67ef260 100644 --- a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py +++ b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py @@ -156,8 +156,7 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin): self.log.debug("frameEnd:: {}".format( instance.data["frameEnd"])) except Exception: - self.log.warning("Unable to count frames " - "duration {}".format(no_of_frames)) + self.log.warning("Unable to count frames duration.") instance.data["handleStart"] = asset_doc["data"]["handleStart"] instance.data["handleEnd"] = asset_doc["data"]["handleEnd"] From 84abccce4ea26ce45fb0e86d3a895e20356c833f Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Thu, 26 Oct 2023 09:49:23 +0300 Subject: [PATCH 0573/1233] retrieve settings that missed by merge --- .../houdini/server/settings/publish.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/server_addon/houdini/server/settings/publish.py b/server_addon/houdini/server/settings/publish.py index ab1b71c6bb..6615e34ca5 100644 --- a/server_addon/houdini/server/settings/publish.py +++ b/server_addon/houdini/server/settings/publish.py @@ -3,6 +3,16 @@ from ayon_server.settings import BaseSettingsModel # Publish Plugins +class CollectRopFrameRangeModel(BaseSettingsModel): + """Collect Frame Range + Disable this if you want the publisher to + ignore start and end handles specified in the + asset data for publish instances + """ + use_asset_handles: bool = Field( + title="Use asset handles") + + class ValidateWorkfilePathsModel(BaseSettingsModel): enabled: bool = Field(title="Enabled") optional: bool = Field(title="Optional") @@ -23,6 +33,11 @@ class BasicValidateModel(BaseSettingsModel): class PublishPluginsModel(BaseSettingsModel): + CollectRopFrameRange: CollectRopFrameRangeModel = Field( + default_factory=CollectRopFrameRangeModel, + title="Collect Rop Frame Range.", + section="Collectors" + ) ValidateContainers: BasicValidateModel = Field( default_factory=BasicValidateModel, title="Validate Latest Containers.", @@ -45,6 +60,9 @@ class PublishPluginsModel(BaseSettingsModel): DEFAULT_HOUDINI_PUBLISH_SETTINGS = { + "CollectRopFrameRange": { + "use_asset_handles": True + }, "ValidateContainers": { "enabled": True, "optional": True, From 64be3b09830f4a793ae9219d071ea044a46e1d2c Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Thu, 26 Oct 2023 09:49:57 +0300 Subject: [PATCH 0574/1233] bump minor version --- server_addon/houdini/server/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/houdini/server/version.py b/server_addon/houdini/server/version.py index 0a8da88258..01ef12070d 100644 --- a/server_addon/houdini/server/version.py +++ b/server_addon/houdini/server/version.py @@ -1 +1 @@ -__version__ = "0.1.6" +__version__ = "0.2.6" From a43b842097b48924345cb8be98f8ca380a2b73a5 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 26 Oct 2023 15:49:28 +0800 Subject: [PATCH 0575/1233] add missing codes for switching on/off the loaded plugin validator --- openpype/hosts/max/plugins/publish/validate_loaded_plugin.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py index 44343bada2..0090c69269 100644 --- a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py +++ b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py @@ -24,6 +24,9 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, def get_invalid(self, instance): """Plugin entry point.""" + if not self.is_active(instance.data): + self.log.debug("Skipping Validate Loaded Plugin...") + return invalid = [] # display all DLL loaded plugins in Max plugin_info = get_plugins() From cb4dd2559b9d51469870a3962cf69435f7d290b4 Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Thu, 26 Oct 2023 10:44:34 +0200 Subject: [PATCH 0576/1233] wip create blend scene create render --- .../plugins/create/create_blendScene.py | 41 +++++++++++++++---- .../blender/plugins/create/create_render.py | 35 ++++++++++++---- 2 files changed, 59 insertions(+), 17 deletions(-) diff --git a/openpype/hosts/blender/plugins/create/create_blendScene.py b/openpype/hosts/blender/plugins/create/create_blendScene.py index 63bcf212ff..ee8e52d3c5 100644 --- a/openpype/hosts/blender/plugins/create/create_blendScene.py +++ b/openpype/hosts/blender/plugins/create/create_blendScene.py @@ -4,7 +4,10 @@ import bpy from openpype.pipeline import get_current_task_name from openpype.hosts.blender.api import plugin, lib, ops -from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES +from openpype.hosts.blender.api.pipeline import ( + AVALON_INSTANCES, + AVALON_PROPERTY, +) class CreateBlendScene(plugin.Creator): @@ -15,12 +18,18 @@ class CreateBlendScene(plugin.Creator): family = "blendScene" icon = "cubes" - def process(self): + def create( + self, subset_name: str, instance_data: dict, pre_create_data: dict + ): """ Run the creator on Blender main thread""" - mti = ops.MainThreadItem(self._process) + mti = ops.MainThreadItem( + self._process, subset_name, instance_data, pre_create_data + ) ops.execute_in_main_thread(mti) - def _process(self): + def _process( + self, subset_name: str, instance_data: dict, pre_create_data: dict + ): # Get Instance Container or create it if it does not exist instances = bpy.data.collections.get(AVALON_INSTANCES) if not instances: @@ -28,14 +37,28 @@ class CreateBlendScene(plugin.Creator): bpy.context.scene.collection.children.link(instances) # Create instance object - asset = self.data["asset"] - subset = self.data["subset"] - name = plugin.asset_name(asset, subset) + asset = instance_data.get("asset") + name = plugin.asset_name(asset, subset_name) asset_group = bpy.data.objects.new(name=name, object_data=None) asset_group.empty_display_type = 'SINGLE_ARROW' instances.objects.link(asset_group) - self.data['task'] = get_current_task_name() - lib.imprint(asset_group, self.data) + + asset_group[AVALON_PROPERTY] = instance_node = { + "name": asset_group.name + } + + instance_data.update( + { + "id": "publish.avalon.instance", + "creator_identifier": self.identifier, + "label": subset_name, + "task": get_current_task_name(), + "subset": subset_name, + "instance_node": instance_node, + } + ) + + lib.imprint(asset_group, instance_data) # Add selected objects to instance if (self.options or {}).get("useSelection"): diff --git a/openpype/hosts/blender/plugins/create/create_render.py b/openpype/hosts/blender/plugins/create/create_render.py index f938a21808..ab3119b32e 100644 --- a/openpype/hosts/blender/plugins/create/create_render.py +++ b/openpype/hosts/blender/plugins/create/create_render.py @@ -4,10 +4,13 @@ import bpy from openpype.pipeline import get_current_task_name from openpype.hosts.blender.api import plugin, lib from openpype.hosts.blender.api.render_lib import prepare_rendering -from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES +from openpype.hosts.blender.api.pipeline import ( + AVALON_INSTANCES, + AVALON_PROPERTY, +) -class CreateRenderlayer(plugin.Creator): +class CreateRenderlayer(plugin.BlenderCreator): """Single baked camera""" name = "renderingMain" @@ -15,7 +18,9 @@ class CreateRenderlayer(plugin.Creator): family = "render" icon = "eye" - def process(self): + def create( + self, subset_name: str, instance_data: dict, pre_create_data: dict + ): # Get Instance Container or create it if it does not exist instances = bpy.data.collections.get(AVALON_INSTANCES) if not instances: @@ -23,15 +28,29 @@ class CreateRenderlayer(plugin.Creator): bpy.context.scene.collection.children.link(instances) # Create instance object - asset = self.data["asset"] - subset = self.data["subset"] - name = plugin.asset_name(asset, subset) + asset = instance_data.get("asset") + name = plugin.asset_name(asset, subset_name) asset_group = bpy.data.collections.new(name=name) try: instances.children.link(asset_group) - self.data['task'] = get_current_task_name() - lib.imprint(asset_group, self.data) + + asset_group[AVALON_PROPERTY] = instance_node = { + "name": asset_group.name + } + + instance_data.update( + { + "id": "pyblish.avalon.instance", + "creator_identifier": self.identifier, + "label": subset_name, + "task": get_current_task_name(), + "subset": subset_name, + "instance_node": instance_node, + } + ) + + lib.imprint(asset_group, instance_data) prepare_rendering(asset_group) except Exception: From 5d87d08ab83b81815e6bc47bddcd7300a30fcc60 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 26 Oct 2023 17:04:00 +0800 Subject: [PATCH 0577/1233] clean up the code of validate loaded plugins --- .../plugins/publish/validate_loaded_plugin.py | 77 +++++++++++-------- 1 file changed, 45 insertions(+), 32 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py index 0090c69269..a8bdf7f903 100644 --- a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py +++ b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- -"""Validator for USD plugin.""" -from pyblish.api import InstancePlugin, ValidatorOrder +"""Validator for Loaded Plugin.""" +from pyblish.api import ContextPlugin, ValidatorOrder from pymxs import runtime as rt from openpype.pipeline.publish import ( - RepairAction, + RepairContextAction, OptionalPyblishPluginMixin, PublishValidationError ) @@ -12,7 +12,7 @@ from openpype.hosts.max.api.lib import get_plugins class ValidateLoadedPlugin(OptionalPyblishPluginMixin, - InstancePlugin): + ContextPlugin): """Validates if the specific plugin is loaded in 3ds max. User can add the plugins they want to check through""" @@ -20,29 +20,38 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, hosts = ["max"] label = "Validate Loaded Plugin" optional = True - actions = [RepairAction] + actions = [RepairContextAction] - def get_invalid(self, instance): + def get_invalid(self, context): """Plugin entry point.""" - if not self.is_active(instance.data): + if not self.is_active(context.data): self.log.debug("Skipping Validate Loaded Plugin...") return invalid = [] - # display all DLL loaded plugins in Max - plugin_info = get_plugins() - project_settings = instance.context.data[ - "project_settings"]["max"]["publish"] - target_plugins = project_settings[ - "ValidateLoadedPlugin"]["plugins_for_check"] - for plugin in target_plugins: - if plugin.lower() not in plugin_info: + # get all DLL loaded plugins in Max and their plugin index + available_plugins = { + plugin_name.lower(): index for index, plugin_name in enumerate(\ + get_plugins()) + } + required_plugins = ( + context.data["project_settings"]["max"]["publish"] + ["ValidateLoadedPlugin"]["plugins_for_check"] + ) + for plugin in required_plugins: + plugin_name = plugin.lower() + + plugin_index = available_plugins.get(plugin_name) + + if plugin_index is None: invalid.append( - f"Plugin {plugin} not exists in 3dsMax Plugin List.") - for i, _ in enumerate(plugin_info): - if plugin.lower() == rt.pluginManager.pluginDllName(i): - if not rt.pluginManager.isPluginDllLoaded(i): - invalid.append( - f"Plugin {plugin} not loaded.") + f"Plugin {plugin} not exists in 3dsMax Plugin List." + ) + continue + + if not rt.pluginManager.isPluginDllLoaded(plugin_index): + invalid.append( + f"Plugin {plugin} not loaded.") + return invalid def process(self, instance): @@ -60,14 +69,18 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, report, title="Required Plugins unloaded") @classmethod - def repair(cls, instance): - plugin_info = get_plugins() - project_settings = instance.context.data[ - "project_settings"]["max"]["publish"] - target_plugins = project_settings[ - "ValidateLoadedPlugin"]["plugins_for_check"] - for plugin in target_plugins: - for i, _ in enumerate(plugin_info): - if plugin == rt.pluginManager.pluginDllName(i): - if not rt.pluginManager.isPluginDllLoaded(i): - rt.pluginManager.loadPluginDll(i) + def repair(cls, context): + # get all DLL loaded plugins in Max and their plugin index + available_plugins = { + plugin_name.lower(): index for index, plugin_name in enumerate( + get_plugins()) + } + required_plugins = ( + context.data["project_settings"]["max"]["publish"] + ["ValidateLoadedPlugin"]["plugins_for_check"] + ) + for plugin in required_plugins: + plugin_name = plugin.lower() + plugin_index = available_plugins.get(plugin_name) + if not rt.pluginManager.isPluginDllLoaded(plugin_index): + rt.pluginManager.loadPluginDll(plugin_index) From 6c24e55d9697bd28d6d7e7cac813f2d0756aa6a8 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 26 Oct 2023 17:05:10 +0800 Subject: [PATCH 0578/1233] hound --- openpype/hosts/max/plugins/publish/validate_loaded_plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py index a8bdf7f903..564cfd0e67 100644 --- a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py +++ b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py @@ -30,7 +30,7 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, invalid = [] # get all DLL loaded plugins in Max and their plugin index available_plugins = { - plugin_name.lower(): index for index, plugin_name in enumerate(\ + plugin_name.lower(): index for index, plugin_name in enumerate( get_plugins()) } required_plugins = ( From b0a12848b92a825e7d979fa9c63416310ec6d528 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 26 Oct 2023 17:47:40 +0800 Subject: [PATCH 0579/1233] clean up code and add condition to make sure the plugin not erroring out during validation --- .../plugins/publish/validate_loaded_plugin.py | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py index 564cfd0e67..49f0f3041b 100644 --- a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py +++ b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py @@ -18,7 +18,7 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, order = ValidatorOrder hosts = ["max"] - label = "Validate Loaded Plugin" + label = "Validate Loaded Plugins" optional = True actions = [RepairContextAction] @@ -27,16 +27,23 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, if not self.is_active(context.data): self.log.debug("Skipping Validate Loaded Plugin...") return + + required_plugins = ( + context.data["project_settings"]["max"]["publish"] + ["ValidateLoadedPlugin"]["plugins_for_check"] + ) + + if not required_plugins: + return + invalid = [] + # get all DLL loaded plugins in Max and their plugin index available_plugins = { plugin_name.lower(): index for index, plugin_name in enumerate( get_plugins()) } - required_plugins = ( - context.data["project_settings"]["max"]["publish"] - ["ValidateLoadedPlugin"]["plugins_for_check"] - ) + for plugin in required_plugins: plugin_name = plugin.lower() @@ -49,8 +56,7 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, continue if not rt.pluginManager.isPluginDllLoaded(plugin_index): - invalid.append( - f"Plugin {plugin} not loaded.") + invalid.append(f"Plugin {plugin} not loaded.") return invalid @@ -82,5 +88,10 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, for plugin in required_plugins: plugin_name = plugin.lower() plugin_index = available_plugins.get(plugin_name) + + if plugin_index is None: + cls.log.warning(f"Can't enable missing plugin: {plugin}") + continue + if not rt.pluginManager.isPluginDllLoaded(plugin_index): rt.pluginManager.loadPluginDll(plugin_index) From a8c4c05b7329b6ccf22309f14aa3990f18b96844 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 26 Oct 2023 17:58:03 +0800 Subject: [PATCH 0580/1233] Docstring edit --- openpype/hosts/max/plugins/publish/validate_loaded_plugin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py index 49f0f3041b..9602d0f313 100644 --- a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py +++ b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py @@ -14,7 +14,8 @@ from openpype.hosts.max.api.lib import get_plugins class ValidateLoadedPlugin(OptionalPyblishPluginMixin, ContextPlugin): """Validates if the specific plugin is loaded in 3ds max. - User can add the plugins they want to check through""" + Studio Admin(s) can add the plugins they want to check in validation + via studio defined project settings""" order = ValidatorOrder hosts = ["max"] From ca2ff805910510a6ecf75e0ae233b8b818665924 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 26 Oct 2023 12:43:13 +0200 Subject: [PATCH 0581/1233] nuke: updating colorspace defaults --- .../defaults/project_settings/nuke.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index 1cadedd797..20df0ad5c2 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -19,16 +19,16 @@ "rules": {} }, "viewer": { - "viewerProcess": "sRGB" + "viewerProcess": "sRGB (default)" }, "baking": { - "viewerProcess": "rec709" + "viewerProcess": "rec709 (default)" }, "workfile": { - "colorManagement": "Nuke", + "colorManagement": "OCIO", "OCIO_config": "nuke-default", - "workingSpaceLUT": "linear", - "monitorLut": "sRGB" + "workingSpaceLUT": "scene_linear", + "monitorLut": "sRGB (default)" }, "nodes": { "requiredNodes": [ @@ -76,7 +76,7 @@ { "type": "text", "name": "colorspace", - "value": "linear" + "value": "scene_linear" }, { "type": "bool", @@ -129,7 +129,7 @@ { "type": "text", "name": "colorspace", - "value": "linear" + "value": "scene_linear" }, { "type": "bool", @@ -177,7 +177,7 @@ { "type": "text", "name": "colorspace", - "value": "sRGB" + "value": "texture_paint" }, { "type": "bool", @@ -193,7 +193,7 @@ "inputs": [ { "regex": "(beauty).*(?=.exr)", - "colorspace": "linear" + "colorspace": "scene_linear" } ] } From 71014fca0b42e490c2ceedf94fc90648ca16808a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 26 Oct 2023 13:34:18 +0200 Subject: [PATCH 0582/1233] hide multivalue widget by default --- openpype/tools/attribute_defs/widgets.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/tools/attribute_defs/widgets.py b/openpype/tools/attribute_defs/widgets.py index 91b5b229de..8957f2b19d 100644 --- a/openpype/tools/attribute_defs/widgets.py +++ b/openpype/tools/attribute_defs/widgets.py @@ -298,6 +298,7 @@ class NumberAttrWidget(_BaseAttrDefWidget): input_widget.installEventFilter(self) multisel_widget = ClickableLineEdit("< Multiselection >", self) + multisel_widget.setVisible(False) input_widget.valueChanged.connect(self._on_value_change) multisel_widget.clicked.connect(self._on_multi_click) From 2f4844613e2e8051bc54a76154e2a7abf211306d Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 26 Oct 2023 13:13:17 +0100 Subject: [PATCH 0583/1233] Use constant to define Asset path --- openpype/hosts/unreal/api/pipeline.py | 1 + openpype/hosts/unreal/plugins/load/load_alembic_animation.py | 2 +- openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py | 3 ++- openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py | 3 ++- openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py | 3 ++- openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py | 3 ++- openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py | 3 ++- openpype/hosts/unreal/plugins/load/load_uasset.py | 2 +- openpype/hosts/unreal/plugins/load/load_yeticache.py | 2 +- 9 files changed, 14 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/unreal/api/pipeline.py b/openpype/hosts/unreal/api/pipeline.py index 0bb19ec601..f2d7b5f73e 100644 --- a/openpype/hosts/unreal/api/pipeline.py +++ b/openpype/hosts/unreal/api/pipeline.py @@ -30,6 +30,7 @@ import unreal # noqa logger = logging.getLogger("openpype.hosts.unreal") AYON_CONTAINERS = "AyonContainers" +AYON_ASSET_DIR = "/Game/Ayon/Assets" CONTEXT_CONTAINER = "Ayon/context.json" UNREAL_VERSION = semver.VersionInfo( *os.getenv("AYON_UNREAL_VERSION").split(".") diff --git a/openpype/hosts/unreal/plugins/load/load_alembic_animation.py b/openpype/hosts/unreal/plugins/load/load_alembic_animation.py index 1d60b63f9a..0328d2ae9f 100644 --- a/openpype/hosts/unreal/plugins/load/load_alembic_animation.py +++ b/openpype/hosts/unreal/plugins/load/load_alembic_animation.py @@ -69,7 +69,7 @@ class AnimationAlembicLoader(plugin.Loader): """ # Create directory for asset and ayon container - root = "/Game/Ayon/Assets" + root = unreal_pipeline.AYON_ASSET_DIR asset = context.get('asset').get('name') suffix = "_CON" if asset: diff --git a/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py b/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py index e64a5654a1..ec9c52b9fb 100644 --- a/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py +++ b/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py @@ -8,6 +8,7 @@ from openpype.pipeline import ( ) from openpype.hosts.unreal.api import plugin from openpype.hosts.unreal.api.pipeline import ( + AYON_ASSET_DIR, create_container, imprint, ) @@ -24,7 +25,7 @@ class PointCacheAlembicLoader(plugin.Loader): icon = "cube" color = "orange" - root = "/Game/Ayon/Assets" + root = AYON_ASSET_DIR @staticmethod def get_task( diff --git a/openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py b/openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py index 03695bb93b..8ebd9a82b6 100644 --- a/openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py +++ b/openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py @@ -8,6 +8,7 @@ from openpype.pipeline import ( ) from openpype.hosts.unreal.api import plugin from openpype.hosts.unreal.api.pipeline import ( + AYON_ASSET_DIR, create_container, imprint, ) @@ -23,7 +24,7 @@ class SkeletalMeshAlembicLoader(plugin.Loader): icon = "cube" color = "orange" - root = "/Game/Ayon/Assets" + root = AYON_ASSET_DIR @staticmethod def get_task(filename, asset_dir, asset_name, replace, default_conversion): diff --git a/openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py b/openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py index 7640ecfa9e..a5a8730732 100644 --- a/openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py +++ b/openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py @@ -8,6 +8,7 @@ from openpype.pipeline import ( ) from openpype.hosts.unreal.api import plugin from openpype.hosts.unreal.api.pipeline import ( + AYON_ASSET_DIR, create_container, imprint, ) @@ -23,7 +24,7 @@ class SkeletalMeshFBXLoader(plugin.Loader): icon = "cube" color = "orange" - root = "/Game/Ayon/Assets" + root = AYON_ASSET_DIR @staticmethod def get_task(filename, asset_dir, asset_name, replace): diff --git a/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py b/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py index a3ea2a2231..019a95a9bf 100644 --- a/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py +++ b/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py @@ -8,6 +8,7 @@ from openpype.pipeline import ( ) from openpype.hosts.unreal.api import plugin from openpype.hosts.unreal.api.pipeline import ( + AYON_ASSET_DIR, create_container, imprint, ) @@ -23,7 +24,7 @@ class StaticMeshAlembicLoader(plugin.Loader): icon = "cube" color = "orange" - root = "/Game/Ayon/Assets" + root = AYON_ASSET_DIR @staticmethod def get_task(filename, asset_dir, asset_name, replace, 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 44d7ca631e..66088d793c 100644 --- a/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py +++ b/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py @@ -8,6 +8,7 @@ from openpype.pipeline import ( ) from openpype.hosts.unreal.api import plugin from openpype.hosts.unreal.api.pipeline import ( + AYON_ASSET_DIR, create_container, imprint, ) @@ -23,7 +24,7 @@ class StaticMeshFBXLoader(plugin.Loader): icon = "cube" color = "orange" - root = "/Game/Ayon/Assets" + root = AYON_ASSET_DIR @staticmethod def get_task(filename, asset_dir, asset_name, replace): diff --git a/openpype/hosts/unreal/plugins/load/load_uasset.py b/openpype/hosts/unreal/plugins/load/load_uasset.py index 88aaac41e8..dfd92d2fe5 100644 --- a/openpype/hosts/unreal/plugins/load/load_uasset.py +++ b/openpype/hosts/unreal/plugins/load/load_uasset.py @@ -41,7 +41,7 @@ class UAssetLoader(plugin.Loader): """ # Create directory for asset and Ayon container - root = "/Game/Ayon/Assets" + root = unreal_pipeline.AYON_ASSET_DIR asset = context.get('asset').get('name') suffix = "_CON" asset_name = f"{asset}_{name}" if asset else f"{name}" diff --git a/openpype/hosts/unreal/plugins/load/load_yeticache.py b/openpype/hosts/unreal/plugins/load/load_yeticache.py index 22f5029bac..780ed7c484 100644 --- a/openpype/hosts/unreal/plugins/load/load_yeticache.py +++ b/openpype/hosts/unreal/plugins/load/load_yeticache.py @@ -86,7 +86,7 @@ class YetiLoader(plugin.Loader): raise RuntimeError("Groom plugin is not activated.") # Create directory for asset and Ayon container - root = "/Game/Ayon/Assets" + root = unreal_pipeline.AYON_ASSET_DIR asset = context.get('asset').get('name') suffix = "_CON" asset_name = f"{asset}_{name}" if asset else f"{name}" From 3b1b59916662b528ba7fee1e952511f32a615284 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 26 Oct 2023 14:18:11 +0200 Subject: [PATCH 0584/1233] updating create ayon addon script - adding argparse module - adding `output` for targeting to specfic folder (ayon-docker/addons) - adding `dont-clear-output` for avoiding clearing already created folders and versions in addons --- server_addon/create_ayon_addons.py | 64 +++++++++++++++++++++++++----- 1 file changed, 55 insertions(+), 9 deletions(-) diff --git a/server_addon/create_ayon_addons.py b/server_addon/create_ayon_addons.py index 61dbd5c8d9..47711244c3 100644 --- a/server_addon/create_ayon_addons.py +++ b/server_addon/create_ayon_addons.py @@ -3,6 +3,7 @@ import sys import re import json import shutil +import argparse import zipfile import platform import collections @@ -185,8 +186,8 @@ def create_openpype_package( addon_output_dir = output_dir / "openpype" / addon_version private_dir = addon_output_dir / "private" # Make sure dir exists - addon_output_dir.mkdir(parents=True) - private_dir.mkdir(parents=True) + addon_output_dir.mkdir(parents=True, exist_ok=True) + private_dir.mkdir(parents=True, exist_ok=True) # Copy version shutil.copy(str(version_path), str(addon_output_dir)) @@ -268,19 +269,29 @@ def create_addon_package( ) -def main(create_zip=True, keep_source=False): +def main(output_dir=None, skip_zip=True, keep_source=False, clear_output_dir=False): current_dir = Path(os.path.dirname(os.path.abspath(__file__))) root_dir = current_dir.parent - output_dir = current_dir / "packages" + create_zip = not skip_zip + + output_dir = Path(output_dir) if output_dir else current_dir / "packages" + print("Current directory:", current_dir) + print("Root directory:", root_dir) + print("Skip zip:", skip_zip) + print("Clear output dir:", clear_output_dir) + print("Keep source:", keep_source) print("Package creation started...") + print(f"Output directory: {output_dir}") # Make sure package dir is empty - if output_dir.exists(): + if output_dir.exists() and clear_output_dir: shutil.rmtree(str(output_dir)) + # Make sure output dir is created - output_dir.mkdir(parents=True) + output_dir.mkdir(parents=True, exist_ok=True) for addon_dir in current_dir.iterdir(): + print(f"Processing addon: {addon_dir.name}") if not addon_dir.is_dir(): continue @@ -303,6 +314,41 @@ def main(create_zip=True, keep_source=False): if __name__ == "__main__": - create_zip = "--skip-zip" not in sys.argv - keep_sources = "--keep-sources" in sys.argv - main(create_zip, keep_sources) + parser = argparse.ArgumentParser() + parser.add_argument( + "--skip-zip", + dest="skip_zip", + action="store_true", + help=( + "Skip zipping server package and create only" + " server folder structure." + ) + ) + parser.add_argument( + "--keep-sources", + dest="keep_sources", + action="store_true", + help=( + "Keep folder structure when server package is created." + ) + ) + parser.add_argument( + "-o", "--output", + dest="output_dir", + default=None, + help=( + "Directory path where package will be created" + " (Will be purged if already exists!)" + ) + ) + parser.add_argument( + "-c", "--dont-clear-output", + dest="clear_output_dir", + action="store_false", + help=( + "Clear output directory before creating packages." + ) + ) + + args = parser.parse_args(sys.argv[1:]) + main(args.output_dir, args.skip_zip, args.keep_sources, args.clear_output_dir) From ad27d4b1cd6b331e3a0f1200440d73d2bae11efc Mon Sep 17 00:00:00 2001 From: Ynbot Date: Thu, 26 Oct 2023 12:26:01 +0000 Subject: [PATCH 0585/1233] [Automated] Release --- CHANGELOG.md | 268 ++++++++++++++++++++++++++++++++++++++++++++ openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 270 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58428ab4d3..7432b33e24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,274 @@ # Changelog +## [3.17.4](https://github.com/ynput/OpenPype/tree/3.17.4) + + +[Full Changelog](https://github.com/ynput/OpenPype/compare/3.17.3...3.17.4) + +### **🆕 New features** + + +
+Add Support for Husk-AYON Integration #5816 + +This draft pull request introduces support for integrating Husk with AYON within the OpenPype repository. + + +___ + +
+ + +
+Push to project tool: Prepare push to project tool for AYON #5770 + +Cloned Push to project tool for AYON and modified it. + + +___ + +
+ +### **🚀 Enhancements** + + +
+Max: tycache family support #5624 + +Tycache family supports for Tyflow Plugin in Max + + +___ + +
+ + +
+Unreal: Changed behaviour for updating assets #5670 + +Changed how assets are updated in Unreal. + + +___ + +
+ + +
+Unreal: Improved error reporting for Sequence Frame Validator #5730 + +Improved error reporting for Sequence Frame Validator. + + +___ + +
+ + +
+Max: Setting tweaks on Review Family #5744 + +- Bug fix of not being able to publish the preferred visual style when creating preview animation +- Exposes the parameters after creating instance +- Add the Quality settings and viewport texture settings for preview animation +- add use selection for create review + + +___ + +
+ + +
+Max: Add families with frame range extractions back to the frame range validator #5757 + +In 3dsMax, there are some instances which exports the files in frame range but not being added to the optional frame range validator. In this PR, these instances would have the optional frame range validators to allow users to check if frame range aligns with the context data from DB.The following families have been added to have optional frame range validator: +- maxrender +- review +- camera +- redshift proxy +- pointcache +- point cloud(tyFlow PRT) + + +___ + +
+ + +
+TimersManager: Use available data to get context info #5804 + +Get context information from pyblish context data instead of using `legacy_io`. + + +___ + +
+ + +
+Chore: Removed unused variable from `AbstractCollectRender` #5805 + +Removed unused `_asset` variable from `RenderInstance`. + + +___ + +
+ +### **🐛 Bug fixes** + + +
+Bugfix/houdini: wrong frame calculation with handles #5698 + +This PR make collect plugins to consider `handleStart` and `handleEnd` when collecting frame range it affects three parts: +- get frame range in collect plugins +- expected file in render plugins +- submit houdini job deadline plugin + + +___ + +
+ + +
+Nuke: ayon server settings improvements #5746 + +Nuke settings were not aligned with OpenPype settings. Also labels needed to be improved. + + +___ + +
+ + +
+Blender: Fix pointcache family and fix alembic extractor #5747 + +Fixed `pointcache` family and fixed behaviour of the alembic extractor. + + +___ + +
+ + +
+AYON: Remove 'shotgun_api3' from dependencies #5803 + +Removed `shotgun_api3` dependency from openpype dependencies for AYON launcher. The dependency is already defined in shotgrid addon and change of version causes clashes. + + +___ + +
+ + +
+Chore: Fix typo in filename #5807 + +Move content of `contants.py` into `constants.py`. + + +___ + +
+ + +
+Chore: Create context respects instance changes #5809 + +Fix issue with unrespected change propagation in `CreateContext`. All successfully saved instances are marked as saved so they have no changes. Origin data of an instance are explicitly not handled directly by the object but by the attribute wrappers. + + +___ + +
+ + +
+Blender: Fix tools handling in AYON mode #5811 + +Skip logic in `before_window_show` in blender when in AYON mode. Most of the stuff called there happes on show automatically. + + +___ + +
+ + +
+Blender: Include Grease Pencil in review and thumbnails #5812 + +Include Grease Pencil in review and thumbnails. + + +___ + +
+ + +
+Workfiles tool AYON: Fix double click of workfile #5813 + +Fix double click on workfiles in workfiles tool to open the file. + + +___ + +
+ + +
+Webpublisher: removal of usage of no_of_frames in error message #5819 + +If it throws exception, `no_of_frames` value wont be available, so it doesn't make sense to log it. + + +___ + +
+ + +
+Attribute Defs: Hide multivalue widget in Number by default #5821 + +Fixed default look of `NumberAttrWidget` by hiding its multiselection widget. + + +___ + +
+ +### **Merged pull requests** + + +
+Corrected a typo in Readme.md (Top -> To) #5800 + + +___ + +
+ + +
+Photoshop: Removed redundant copy of extension.zxp #5802 + +`extension.zxp` shouldn't be inside of extension folder. + + +___ + +
+ + + + ## [3.17.3](https://github.com/ynput/OpenPype/tree/3.17.3) diff --git a/openpype/version.py b/openpype/version.py index 0bdf2d278a..4c58a5098f 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.17.4-nightly.2" +__version__ = "3.17.4" diff --git a/pyproject.toml b/pyproject.toml index 3803e4714e..633dafece1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.17.3" # OpenPype +version = "3.17.4" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 9c7ce75eea842ed60e53b70ba89a466c10049116 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 26 Oct 2023 12:27:07 +0000 Subject: [PATCH 0586/1233] 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 3c126048da..b92061f39a 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.17.4 + - 3.17.4-nightly.2 - 3.17.4-nightly.1 - 3.17.3 - 3.17.3-nightly.2 @@ -133,8 +135,6 @@ body: - 3.15.1-nightly.2 - 3.15.1-nightly.1 - 3.15.0 - - 3.15.0-nightly.1 - - 3.14.11-nightly.4 validations: required: true - type: dropdown From 1ecd96acf6acb98f9fb27aa70a345ad5014343b9 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 26 Oct 2023 21:18:28 +0800 Subject: [PATCH 0587/1233] use context.data instead of instance data --- openpype/hosts/max/plugins/publish/validate_loaded_plugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py index 9602d0f313..69f72ccf1d 100644 --- a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py +++ b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py @@ -61,8 +61,8 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, return invalid - def process(self, instance): - invalid_plugins = self.get_invalid(instance) + def process(self, context): + invalid_plugins = self.get_invalid(context) if invalid_plugins: bullet_point_invalid_statement = "\n".join( "- {}".format(invalid) for invalid in invalid_plugins From 5b18acadc0c76e7bd51f5fe8a6b1b93486a0e79e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 26 Oct 2023 21:33:31 +0800 Subject: [PATCH 0588/1233] Implement ValidateAttributes in 3dsMax --- .../plugins/publish/validate_attributes.py | 62 +++++++++++++++++++ openpype/settings/ayon_settings.py | 9 +++ .../defaults/project_settings/max.json | 4 ++ .../schemas/schema_max_publish.json | 19 ++++++ .../max/server/settings/publishers.py | 36 ++++++++++- 5 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 openpype/hosts/max/plugins/publish/validate_attributes.py diff --git a/openpype/hosts/max/plugins/publish/validate_attributes.py b/openpype/hosts/max/plugins/publish/validate_attributes.py new file mode 100644 index 0000000000..e98e73de06 --- /dev/null +++ b/openpype/hosts/max/plugins/publish/validate_attributes.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +"""Validator for Attributes.""" +from pyblish.api import ContextPlugin, ValidatorOrder +from pymxs import runtime as rt + +from openpype.pipeline.publish import ( + OptionalPyblishPluginMixin, + PublishValidationError, + RepairContextAction +) + + +class ValidateAttributes(OptionalPyblishPluginMixin, + ContextPlugin): + """Validates attributes are consistent in 3ds max.""" + + order = ValidatorOrder + hosts = ["max"] + label = "Attributes" + actions = [RepairContextAction] + optional = True + + @classmethod + def get_invalid(cls, context): + attributes = ( + context.data["project_settings"]["max"]["publish"] + ["ValidateAttributes"]["attributes"] + ) + if not attributes: + return + + invalid_attributes = [key for key, value in attributes.items() + if rt.Execute(attributes[key]) != value] + + return invalid_attributes + + def process(self, context): + if not self.is_active(context.data): + self.log.debug("Skipping Validate Attributes...") + return + invalid_attributes = self.get_invalid(context) + if invalid_attributes: + bullet_point_invalid_statement = "\n".join( + "- {}".format(invalid) for invalid in invalid_attributes + ) + report = ( + "Required Attribute(s) have invalid value(s).\n\n" + f"{bullet_point_invalid_statement}\n\n" + "You can use repair action to fix it." + ) + raise PublishValidationError( + report, title="Invalid Value(s) for Required Attribute(s)") + + @classmethod + def repair(cls, context): + attributes = ( + context.data["project_settings"]["max"]["publish"] + ["ValidateAttributes"]["attributes"] + ) + invalid_attribute_keys = cls.get_invalid(context) + for key in invalid_attribute_keys: + attributes[key] = rt.Execute(attributes[key]) diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index 8d4683490b..a31c8a04e0 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -639,6 +639,15 @@ def _convert_3dsmax_project_settings(ayon_settings, output): for item in point_cloud_attribute } ayon_max["PointCloud"]["attribute"] = new_point_cloud_attribute + # --- Publish (START) --- + ayon_publish = ayon_max["publish"] + try: + attributes = json.loads( + ayon_publish["ValidateAttributes"]["attributes"] + ) + except ValueError: + attributes = {} + ayon_publish["ValidateAttributes"]["attributes"] = attributes output["max"] = ayon_max diff --git a/openpype/settings/defaults/project_settings/max.json b/openpype/settings/defaults/project_settings/max.json index bfb1aa4aeb..24a87020bb 100644 --- a/openpype/settings/defaults/project_settings/max.json +++ b/openpype/settings/defaults/project_settings/max.json @@ -36,6 +36,10 @@ "enabled": true, "optional": true, "active": true + }, + "ValidateAttributes": { + "enabled": false, + "attributes": {} } } } diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json index ea08c735a6..c3b56bae5e 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json @@ -28,6 +28,25 @@ "label": "Active" } ] + }, + { + "type": "dict", + "collapsible": true, + "key": "ValidateAttributes", + "label": "ValidateAttributes", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "raw-json", + "key": "attributes", + "label": "Attributes" + } + ] } ] } diff --git a/server_addon/max/server/settings/publishers.py b/server_addon/max/server/settings/publishers.py index a695b85e89..df8412391a 100644 --- a/server_addon/max/server/settings/publishers.py +++ b/server_addon/max/server/settings/publishers.py @@ -1,6 +1,30 @@ -from pydantic import Field +import json +from pydantic import Field, validator from ayon_server.settings import BaseSettingsModel +from ayon_server.exceptions import BadRequestException + + +class ValidateAttributesModel(BaseSettingsModel): + enabled: bool = Field(title="ValidateAttributes") + attributes: str = Field( + "{}", title="Attributes", widget="textarea") + + @validator("attributes") + def validate_json(cls, value): + if not value.strip(): + return "{}" + try: + converted_value = json.loads(value) + success = isinstance(converted_value, dict) + except json.JSONDecodeError: + success = False + + if not success: + raise BadRequestException( + "The attibutes can't be parsed as json object" + ) + return value class BasicValidateModel(BaseSettingsModel): @@ -15,6 +39,10 @@ class PublishersModel(BaseSettingsModel): title="Validate Frame Range", section="Validators" ) + ValidateAttributes: ValidateAttributesModel = Field( + default_factory=ValidateAttributesModel, + title="Validate Attributes" + ) DEFAULT_PUBLISH_SETTINGS = { @@ -22,5 +50,9 @@ DEFAULT_PUBLISH_SETTINGS = { "enabled": True, "optional": True, "active": True - } + }, + "ValidateAttributes": { + "enabled": False, + "attributes": "{}" + }, } From a6e72e708b5853f42aa99a18a57554ee2494d5ec Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 26 Oct 2023 17:03:42 +0200 Subject: [PATCH 0589/1233] removed debugging prints --- server_addon/create_ayon_addons.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/server_addon/create_ayon_addons.py b/server_addon/create_ayon_addons.py index 47711244c3..711dc5d00a 100644 --- a/server_addon/create_ayon_addons.py +++ b/server_addon/create_ayon_addons.py @@ -274,24 +274,17 @@ def main(output_dir=None, skip_zip=True, keep_source=False, clear_output_dir=Fal root_dir = current_dir.parent create_zip = not skip_zip - output_dir = Path(output_dir) if output_dir else current_dir / "packages" - print("Current directory:", current_dir) - print("Root directory:", root_dir) - print("Skip zip:", skip_zip) - print("Clear output dir:", clear_output_dir) - print("Keep source:", keep_source) - print("Package creation started...") - print(f"Output directory: {output_dir}") # Make sure package dir is empty if output_dir.exists() and clear_output_dir: shutil.rmtree(str(output_dir)) + print("Package creation started...") + print(f"Output directory: {output_dir}") + # Make sure output dir is created output_dir.mkdir(parents=True, exist_ok=True) - for addon_dir in current_dir.iterdir(): - print(f"Processing addon: {addon_dir.name}") if not addon_dir.is_dir(): continue From afc980efb30207f510416ab3b8be2a7938a8a1d6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 26 Oct 2023 17:07:24 +0200 Subject: [PATCH 0590/1233] small cleanup of the code and add option to limit addons creation --- server_addon/create_ayon_addons.py | 40 +++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/server_addon/create_ayon_addons.py b/server_addon/create_ayon_addons.py index 711dc5d00a..2f7be760f3 100644 --- a/server_addon/create_ayon_addons.py +++ b/server_addon/create_ayon_addons.py @@ -185,6 +185,9 @@ def create_openpype_package( addon_output_dir = output_dir / "openpype" / addon_version private_dir = addon_output_dir / "private" + if addon_output_dir.exists(): + shutil.rmtree(str(addon_output_dir)) + # Make sure dir exists addon_output_dir.mkdir(parents=True, exist_ok=True) private_dir.mkdir(parents=True, exist_ok=True) @@ -269,13 +272,22 @@ def create_addon_package( ) -def main(output_dir=None, skip_zip=True, keep_source=False, clear_output_dir=False): +def main( + output_dir=None, + skip_zip=True, + keep_source=False, + clear_output_dir=False, + addons=None, +): current_dir = Path(os.path.dirname(os.path.abspath(__file__))) root_dir = current_dir.parent create_zip = not skip_zip + if output_dir: + output_dir = Path(output_dir) + else: + output_dir = current_dir / "packages" - # Make sure package dir is empty if output_dir.exists() and clear_output_dir: shutil.rmtree(str(output_dir)) @@ -288,6 +300,9 @@ def main(output_dir=None, skip_zip=True, keep_source=False, clear_output_dir=Fal if not addon_dir.is_dir(): continue + if addons and addon_dir.name not in addons: + continue + server_dir = addon_dir / "server" if not server_dir.exists(): continue @@ -335,13 +350,26 @@ if __name__ == "__main__": ) ) parser.add_argument( - "-c", "--dont-clear-output", + "-c", "--clear-output-dir", dest="clear_output_dir", - action="store_false", + action="store_true", help=( - "Clear output directory before creating packages." + "Clear output directory before package creation." ) ) + parser.add_argument( + "-a", + "--addon", + dest="addons", + action="append", + help="Limit addon creation to given addon name", + ) args = parser.parse_args(sys.argv[1:]) - main(args.output_dir, args.skip_zip, args.keep_sources, args.clear_output_dir) + main( + args.output_dir, + args.skip_zip, + args.keep_sources, + args.clear_output_dir, + args.addons, + ) From ae2c4bd5548c6ba81e7937320b0b91554ea614cd Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 26 Oct 2023 17:09:34 +0200 Subject: [PATCH 0591/1233] nuke: aligning server addon settings with openpype --- server_addon/nuke/server/settings/imageio.py | 16 ++++++++-------- server_addon/nuke/server/version.py | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/server_addon/nuke/server/settings/imageio.py b/server_addon/nuke/server/settings/imageio.py index 15ccd4e89a..19ad5ff24a 100644 --- a/server_addon/nuke/server/settings/imageio.py +++ b/server_addon/nuke/server/settings/imageio.py @@ -213,16 +213,16 @@ class ImageIOSettings(BaseSettingsModel): DEFAULT_IMAGEIO_SETTINGS = { "viewer": { - "viewerProcess": "sRGB" + "viewerProcess": "sRGB (default)" }, "baking": { - "viewerProcess": "rec709" + "viewerProcess": "rec709 (default)" }, "workfile": { - "color_management": "Nuke", + "color_management": "OCIO", "native_ocio_config": "nuke-default", - "working_space": "linear", - "thumbnail_space": "sRGB", + "working_space": "scene_linear", + "thumbnail_space": "sRGB (default)", }, "nodes": { "required_nodes": [ @@ -269,7 +269,7 @@ DEFAULT_IMAGEIO_SETTINGS = { { "type": "text", "name": "colorspace", - "text": "linear" + "text": "scene_linear" }, { "type": "boolean", @@ -321,7 +321,7 @@ DEFAULT_IMAGEIO_SETTINGS = { { "type": "text", "name": "colorspace", - "text": "linear" + "text": "scene_linear" }, { "type": "boolean", @@ -368,7 +368,7 @@ DEFAULT_IMAGEIO_SETTINGS = { { "type": "text", "name": "colorspace", - "text": "sRGB" + "text": "texture_paint" }, { "type": "boolean", diff --git a/server_addon/nuke/server/version.py b/server_addon/nuke/server/version.py index bbab0242f6..1276d0254f 100644 --- a/server_addon/nuke/server/version.py +++ b/server_addon/nuke/server/version.py @@ -1 +1 @@ -__version__ = "0.1.4" +__version__ = "0.1.5" From b6694606876eba68f3e0cb24928f93f7093e1c70 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 26 Oct 2023 18:06:47 +0200 Subject: [PATCH 0592/1233] Removed unnecessary condition. --- openpype/client/server/entities.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/openpype/client/server/entities.py b/openpype/client/server/entities.py index c735c558d5..fcb5ec2383 100644 --- a/openpype/client/server/entities.py +++ b/openpype/client/server/entities.py @@ -226,12 +226,11 @@ def get_assets( new_asset_names = set() folder_paths = set() - if asset_names: - for name in asset_names: - if "/" in name: - folder_paths.add(name) - else: - new_asset_names.add(name) + for name in asset_names: + if "/" in name: + folder_paths.add(name) + else: + new_asset_names.add(name) if folder_paths: for folder in _folders_query( From 130693b798be8c53e18ec02bcacde7b8b6f45fdd Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 26 Oct 2023 18:24:42 +0200 Subject: [PATCH 0593/1233] use 'AYON_SERVER_ENABLED' in resolve plugin --- .../plugins/publish/precollect_workfile.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/resolve/plugins/publish/precollect_workfile.py b/openpype/hosts/resolve/plugins/publish/precollect_workfile.py index 28b2350f01..39c28e29f5 100644 --- a/openpype/hosts/resolve/plugins/publish/precollect_workfile.py +++ b/openpype/hosts/resolve/plugins/publish/precollect_workfile.py @@ -1,6 +1,7 @@ import pyblish.api from pprint import pformat +from openpype import AYON_SERVER_ENABLED from openpype.pipeline import get_current_asset_name from openpype.hosts.resolve import api as rapi from openpype.hosts.resolve.otio import davinci_export @@ -13,10 +14,13 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin): order = pyblish.api.CollectorOrder - 0.5 def process(self, context): + current_asset = get_current_asset_name() + if AYON_SERVER_ENABLED: + # AYON compatibility split name and use last piece + asset_name = current_asset.split("/")[-1] + else: + asset_name = current_asset - asset = get_current_asset_name() - # AYON compatibility split name and use last piece - _asset_name = asset.split("/")[-1] subset = "workfile" project = rapi.get_current_project() fps = project.GetSetting("timelineFrameRate") @@ -26,9 +30,9 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin): otio_timeline = davinci_export.create_otio_timeline(project) instance_data = { - "name": "{}_{}".format(_asset_name, subset), - "asset": asset, - "subset": "{}{}".format(_asset_name, subset.capitalize()), + "name": "{}_{}".format(asset_name, subset), + "asset": current_asset, + "subset": "{}{}".format(asset_name, subset.capitalize()), "item": project, "family": "workfile", "families": [] From e920c014307e0fcb543f129b6e72b4ba49433cfb Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 26 Oct 2023 18:41:47 +0200 Subject: [PATCH 0594/1233] unify conditions --- .../hosts/tvpaint/plugins/create/create_review.py | 12 ++++++------ .../hosts/tvpaint/plugins/create/create_workfile.py | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/create/create_review.py b/openpype/hosts/tvpaint/plugins/create/create_review.py index 265cef00ef..5caf20f27d 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_review.py +++ b/openpype/hosts/tvpaint/plugins/create/create_review.py @@ -34,12 +34,12 @@ class TVPaintReviewCreator(TVPaintAutoCreator): asset_name = create_context.get_current_asset_name() task_name = create_context.get_current_task_name() - existing_asset_name = None - if existing_instance is not None: - if AYON_SERVER_ENABLED: - existing_asset_name = existing_instance.get("folderPath") - if existing_asset_name is None: - existing_asset_name = existing_instance.get("asset") + if existing_instance is None: + existing_asset_name = None + elif AYON_SERVER_ENABLED: + existing_asset_name = existing_instance["folderPath"] + else: + existing_asset_name = existing_instance["asset"] if existing_instance is None: asset_doc = get_asset_by_name(project_name, asset_name) diff --git a/openpype/hosts/tvpaint/plugins/create/create_workfile.py b/openpype/hosts/tvpaint/plugins/create/create_workfile.py index eec0f8483f..4ce5d7fc96 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_workfile.py +++ b/openpype/hosts/tvpaint/plugins/create/create_workfile.py @@ -30,12 +30,12 @@ class TVPaintWorkfileCreator(TVPaintAutoCreator): asset_name = create_context.get_current_asset_name() task_name = create_context.get_current_task_name() - existing_asset_name = None - if existing_instance is not None: - if AYON_SERVER_ENABLED: - existing_asset_name = existing_instance.get("folderPath") - if existing_asset_name is None: - existing_asset_name = existing_instance.get("asset") + if existing_instance is None: + existing_asset_name = None + elif AYON_SERVER_ENABLED: + existing_asset_name = existing_instance["folderPath"] + else: + existing_asset_name = existing_instance["asset"] if existing_instance is None: asset_doc = get_asset_by_name(project_name, asset_name) From 8a9a1e66f3fe007367d7895fcbcb452a88c4a366 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 26 Oct 2023 19:08:08 +0200 Subject: [PATCH 0595/1233] comparison of UILabelDef is comparing label value --- openpype/lib/attribute_definitions.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/lib/attribute_definitions.py b/openpype/lib/attribute_definitions.py index a71d6cc72a..b8faae8f4c 100644 --- a/openpype/lib/attribute_definitions.py +++ b/openpype/lib/attribute_definitions.py @@ -240,6 +240,11 @@ class UILabelDef(UIDef): def __init__(self, label): super(UILabelDef, self).__init__(label=label) + def __eq__(self, other): + if not super(UILabelDef, self).__eq__(other): + return False + return self.label == other.label: + # --------------------------------------- # Attribute defintioins should hold value From bbfff158b055b4fcaf520396bdea0d5544321d08 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 26 Oct 2023 19:08:25 +0200 Subject: [PATCH 0596/1233] it is possible to define key of label to differentiate --- openpype/lib/attribute_definitions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/lib/attribute_definitions.py b/openpype/lib/attribute_definitions.py index b8faae8f4c..658a3fe581 100644 --- a/openpype/lib/attribute_definitions.py +++ b/openpype/lib/attribute_definitions.py @@ -237,8 +237,8 @@ class UISeparatorDef(UIDef): class UILabelDef(UIDef): type = "label" - def __init__(self, label): - super(UILabelDef, self).__init__(label=label) + def __init__(self, label, key=None): + super(UILabelDef, self).__init__(label=label, key=key) def __eq__(self, other): if not super(UILabelDef, self).__eq__(other): From 576ac94e3168568e1a6e5bc136bc338fac19a8f4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 26 Oct 2023 19:12:15 +0200 Subject: [PATCH 0597/1233] Remove semicolon --- openpype/lib/attribute_definitions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/lib/attribute_definitions.py b/openpype/lib/attribute_definitions.py index 658a3fe581..3dd284b8e4 100644 --- a/openpype/lib/attribute_definitions.py +++ b/openpype/lib/attribute_definitions.py @@ -243,7 +243,7 @@ class UILabelDef(UIDef): def __eq__(self, other): if not super(UILabelDef, self).__eq__(other): return False - return self.label == other.label: + return self.label == other.label # --------------------------------------- From 0b0cfb4116ed6067b0416bb34ecd8a3dd7e19805 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 27 Oct 2023 14:24:37 +0200 Subject: [PATCH 0598/1233] add slash at the beginning of path --- openpype/client/entities.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/client/entities.py b/openpype/client/entities.py index d085f90028..cbaa943743 100644 --- a/openpype/client/entities.py +++ b/openpype/client/entities.py @@ -22,4 +22,4 @@ def get_asset_name_identifier(asset_doc): return asset_doc["name"] parents = list(asset_doc["data"]["parents"]) parents.append(asset_doc["name"]) - return "/".join(parents) + return "/" + "/".join(parents) From dd72d45ce7881b46c90c6972f27cc101b02f7696 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 27 Oct 2023 14:36:22 +0200 Subject: [PATCH 0599/1233] fix path in assets widget --- openpype/tools/publisher/widgets/assets_widget.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/tools/publisher/widgets/assets_widget.py b/openpype/tools/publisher/widgets/assets_widget.py index 5f74b79c99..32be514dd7 100644 --- a/openpype/tools/publisher/widgets/assets_widget.py +++ b/openpype/tools/publisher/widgets/assets_widget.py @@ -140,9 +140,11 @@ class AssetsHierarchyModel(QtGui.QStandardItemModel): for name in sorted(children_by_name.keys()): child = children_by_name[name] child_id = child["_id"] - child_path = name if parent_path: - child_path = "{}/{}".format(parent_path, child_path) + child_path = "{}/{}".format(parent_path, name) + else: + child_path = "/{}".format(name) + has_children = bool(assets_by_parent_id.get(child_id)) icon = get_asset_icon(child, has_children) From 6a0decab459db0b83b777289521584f2eaca02a2 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 27 Oct 2023 21:09:29 +0800 Subject: [PATCH 0600/1233] make sure to check invalid properties --- .../plugins/publish/validate_attributes.py | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_attributes.py b/openpype/hosts/max/plugins/publish/validate_attributes.py index e98e73de06..2d3f09f972 100644 --- a/openpype/hosts/max/plugins/publish/validate_attributes.py +++ b/openpype/hosts/max/plugins/publish/validate_attributes.py @@ -29,10 +29,20 @@ class ValidateAttributes(OptionalPyblishPluginMixin, if not attributes: return - invalid_attributes = [key for key, value in attributes.items() - if rt.Execute(attributes[key]) != value] + for wrap_object, property_name in attributes.items(): + invalid_properties = [key for key in property_name.keys() + if not rt.Execute( + f'isProperty {wrap_object} "{key}"')] + if invalid_properties: + cls.log.error( + "Unknown Property Values:{}".format(invalid_properties)) + return invalid_properties + # TODO: support multiple varaible types in maxscript + invalid_attributes = [key for key, value in property_name.items() + if rt.Execute("{}.{}".format( + wrap_object, property_name[key]))!=value] - return invalid_attributes + return invalid_attributes def process(self, context): if not self.is_active(context.data): @@ -57,6 +67,10 @@ class ValidateAttributes(OptionalPyblishPluginMixin, context.data["project_settings"]["max"]["publish"] ["ValidateAttributes"]["attributes"] ) - invalid_attribute_keys = cls.get_invalid(context) - for key in invalid_attribute_keys: - attributes[key] = rt.Execute(attributes[key]) + for wrap_object, property_name in attributes.items(): + invalid_attributes = [key for key, value in property_name.items() + if rt.Execute("{}.{}".format( + wrap_object, property_name[key]))!=value] + for attrs in invalid_attributes: + rt.Execute("{}.{}={}".format( + wrap_object, attrs, attributes[wrap_object][attrs])) From 4f658d2f51cd3357a3d31ba7d607354debabaa19 Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Fri, 27 Oct 2023 16:05:01 +0200 Subject: [PATCH 0601/1233] update blender creator --- openpype/hosts/blender/api/plugin.py | 14 ++++++++++++-- .../blender/plugins/create/create_blendScene.py | 2 +- .../hosts/blender/plugins/create/create_model.py | 2 +- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index 73d8fc0ed5..dbd9f25d68 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -9,6 +9,7 @@ from openpype.pipeline import ( Creator, CreatedInstance, LoaderPlugin, + get_current_task_name, ) from .pipeline import ( AVALON_CONTAINERS, @@ -235,11 +236,20 @@ class BlenderCreator(Creator): collection = bpy.data.collections.new(name=subset_name) bpy.context.scene.collection.children.link(collection) - collection["instance_node"] = instance_node = { + collection[AVALON_PROPERTY] = instance_node = { "name": collection.name, } - instance_data["instance_node"] = instance_node + instance_data.update( + { + "id": "pyblish.avalon.instance", + "creator_identifier": self.identifier, + "label": subset_name, + "task": get_current_task_name(), + "subset": subset_name, + "instance_node": instance_node, + } + ) instance = CreatedInstance( self.family, subset_name, instance_data, self diff --git a/openpype/hosts/blender/plugins/create/create_blendScene.py b/openpype/hosts/blender/plugins/create/create_blendScene.py index ee8e52d3c5..23ff991654 100644 --- a/openpype/hosts/blender/plugins/create/create_blendScene.py +++ b/openpype/hosts/blender/plugins/create/create_blendScene.py @@ -21,7 +21,7 @@ class CreateBlendScene(plugin.Creator): def create( self, subset_name: str, instance_data: dict, pre_create_data: dict ): - """ Run the creator on Blender main thread""" + """Run the creator on Blender main thread.""" mti = ops.MainThreadItem( self._process, subset_name, instance_data, pre_create_data ) diff --git a/openpype/hosts/blender/plugins/create/create_model.py b/openpype/hosts/blender/plugins/create/create_model.py index 3c8e9c4900..761d9fca9f 100644 --- a/openpype/hosts/blender/plugins/create/create_model.py +++ b/openpype/hosts/blender/plugins/create/create_model.py @@ -22,7 +22,7 @@ class CreateModel(plugin.BlenderCreator): def create( self, subset_name: str, instance_data: dict, pre_create_data: dict ): - """ Run the creator on Blender main thread""" + """Run the creator on Blender main thread.""" self._add_instance_to_context( CreatedInstance(self.family, subset_name, instance_data, self) ) From ec5bc71eb3529ba096e00d5261dfdf0608a53280 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 27 Oct 2023 16:42:56 +0200 Subject: [PATCH 0602/1233] skip kitsu module when creating ayon addons --- server_addon/create_ayon_addons.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server_addon/create_ayon_addons.py b/server_addon/create_ayon_addons.py index 61dbd5c8d9..86139a65f8 100644 --- a/server_addon/create_ayon_addons.py +++ b/server_addon/create_ayon_addons.py @@ -205,7 +205,8 @@ def create_openpype_package( "shotgrid", "sync_server", "example_addons", - "slack" + "slack", + "kitsu", ] # Subdirs that won't be added to output zip file ignored_subpaths = [ From 3f8b250510e1cb0e6f8edae6afde87e5d2718c23 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 27 Oct 2023 17:05:00 +0200 Subject: [PATCH 0603/1233] removed unncessary path filtering --- openpype/client/server/entities.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/openpype/client/server/entities.py b/openpype/client/server/entities.py index fcb5ec2383..9e86dfdd63 100644 --- a/openpype/client/server/entities.py +++ b/openpype/client/server/entities.py @@ -241,21 +241,9 @@ def get_assets( if not new_asset_names: return - folders_by_name = collections.defaultdict(list) for folder in _folders_query( project_name, con, fields, folder_names=new_asset_names, **kwargs ): - folders_by_name[folder["name"]].append(folder) - - for name, folders in folders_by_name.items(): - folder = next( - ( - folder - for folder in folders - if folder["path"] == name - ), - folders[0] - ) yield convert_v4_folder_to_v3(folder, project_name) From 4bde7b7fd94ab4ca183f90d83ef347e738d8b843 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 27 Oct 2023 18:01:04 +0200 Subject: [PATCH 0604/1233] hiero: adding folderPath to creator - some minor typos fixes - modules sorting --- openpype/hosts/hiero/api/plugin.py | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/hiero/api/plugin.py b/openpype/hosts/hiero/api/plugin.py index 0e0632e032..dc90012b0f 100644 --- a/openpype/hosts/hiero/api/plugin.py +++ b/openpype/hosts/hiero/api/plugin.py @@ -31,7 +31,7 @@ def load_stylesheet(): class CreatorWidget(QtWidgets.QDialog): # output items - items = dict() + items = {} def __init__(self, name, info, ui_inputs, parent=None): super(CreatorWidget, self).__init__(parent) @@ -642,8 +642,8 @@ class PublishClip: Returns: hiero.core.TrackItem: hiero track item object with pype tag """ - vertical_clip_match = dict() - tag_data = dict() + vertical_clip_match = {} + tag_data = {} types = { "shot": "shot", "folder": "folder", @@ -705,9 +705,10 @@ class PublishClip: self._create_parents() def convert(self): - # solve track item data and add them to tag data - self._convert_to_tag_data() + tag_hierarchy_data = self._convert_to_tag_data() + + self.tag_data.update(tag_hierarchy_data) # if track name is in review track name and also if driving track name # is not in review track name: skip tag creation @@ -721,16 +722,28 @@ class PublishClip: if self.rename: # rename track item self.track_item.setName(new_name) - self.tag_data["asset"] = new_name + self.tag_data["asset_name"] = new_name else: - self.tag_data["asset"] = self.ti_name + self.tag_data["asset_name"] = self.ti_name self.tag_data["hierarchyData"]["shot"] = self.ti_name + # AYON unique identifier + folder_path = "/{}/{}".format( + tag_hierarchy_data["hierarchy"], + self.tag_data["asset_name"] + ) + self.tag_data["folderPath"] = folder_path + + # TODO: remove debug print + log.debug("___ folder_path: {}".format( + folder_path)) + if self.tag_data["heroTrack"] and self.review_layer: self.tag_data.update({"reviewTrack": self.review_layer}) else: self.tag_data.update({"reviewTrack": None}) + # TODO: remove debug print log.debug("___ self.tag_data: {}".format( pformat(self.tag_data) )) @@ -889,7 +902,7 @@ class PublishClip: tag_hierarchy_data = hero_data # add data to return data dict - self.tag_data.update(tag_hierarchy_data) + return tag_hierarchy_data def _solve_tag_hierarchy_data(self, hierarchy_formatting_data): """ Solve tag data from hierarchy data and templates. """ From 4e9173fa71b9e6b4c19994356ce704a7d3bd29f5 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 27 Oct 2023 18:02:11 +0200 Subject: [PATCH 0605/1233] Hiero: adding asset_name and processing folderPath - refactor labels --- .../plugins/publish/precollect_instances.py | 60 +++++++++++++------ .../plugins/publish/precollect_workfile.py | 15 +++-- 2 files changed, 52 insertions(+), 23 deletions(-) diff --git a/openpype/hosts/hiero/plugins/publish/precollect_instances.py b/openpype/hosts/hiero/plugins/publish/precollect_instances.py index 65b8fed49c..1acbbb3d88 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_instances.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_instances.py @@ -1,9 +1,12 @@ import pyblish + +from openpype import AYON_SERVER_ENABLED from openpype.pipeline.editorial import is_overlapping_otio_ranges + from openpype.hosts.hiero import api as phiero from openpype.hosts.hiero.api.otio import hiero_export -import hiero +import hiero # # developer reload modules from pprint import pformat @@ -80,25 +83,24 @@ class PrecollectInstances(pyblish.api.ContextPlugin): if k not in ("id", "applieswhole", "label") }) - asset = tag_data["asset"] + asset, asset_name = self._get_asset_data(tag_data) + subset = tag_data["subset"] # insert family into families - family = tag_data["family"] families = [str(f) for f in tag_data["families"]] - families.insert(0, str(family)) # form label - label = asset - if asset != clip_name: + label = "{} -".format(asset) + if asset_name != clip_name: label += " ({})".format(clip_name) label += " {}".format(subset) - label += " {}".format("[" + ", ".join(families) + "]") data.update({ "name": "{}_{}".format(asset, subset), "label": label, "asset": asset, + "asset_name": asset_name, "item": track_item, "families": families, "publish": tag_data["publish"], @@ -176,6 +178,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin): }) def create_shot_instance(self, context, **data): + subset = "shotMain" master_layer = data.get("heroTrack") hierarchy_data = data.get("hierarchyData") item = data.get("item") @@ -188,23 +191,21 @@ class PrecollectInstances(pyblish.api.ContextPlugin): return asset = data["asset"] - subset = "shotMain" + asset_name = data["asset_name"] # insert family into families family = "shot" # form label - label = asset - if asset != clip_name: + label = "{} -".format(asset) + if asset_name != clip_name: label += " ({}) ".format(clip_name) label += " {}".format(subset) - label += " [{}]".format(family) data.update({ "name": "{}_{}".format(asset, subset), "label": label, "subset": subset, - "asset": asset, "family": family, "families": [] }) @@ -214,7 +215,34 @@ class PrecollectInstances(pyblish.api.ContextPlugin): self.log.debug( "_ instance.data: {}".format(pformat(instance.data))) + def _get_asset_data(self, data): + folder_path = ( + data.pop("folderPath") if data.get("folderPath") else None) + + if data.get("asset_name"): + asset_name = data["asset_name"] + else: + asset_name = data["asset"] + + # backward compatibility for clip tags + # which are missing folderPath key + # TODO remove this in future versions + if not folder_path: + hierarchy_path = data["hierarchy"] + folder_path = "/{}/{}".format( + hierarchy_path, + asset_name + ) + + if AYON_SERVER_ENABLED: + asset = folder_path + else: + asset = asset_name + + return asset, asset_name + def create_audio_instance(self, context, **data): + subset = "audioMain" master_layer = data.get("heroTrack") if not master_layer: @@ -229,23 +257,21 @@ class PrecollectInstances(pyblish.api.ContextPlugin): return asset = data["asset"] - subset = "audioMain" + asset_name = data["asset_name"] # insert family into families family = "audio" # form label - label = asset - if asset != clip_name: + label = "{} -".format(asset) + if asset_name != clip_name: label += " ({}) ".format(clip_name) label += " {}".format(subset) - label += " [{}]".format(family) data.update({ "name": "{}_{}".format(asset, subset), "label": label, "subset": subset, - "asset": asset, "family": family, "families": ["clip"] }) diff --git a/openpype/hosts/hiero/plugins/publish/precollect_workfile.py b/openpype/hosts/hiero/plugins/publish/precollect_workfile.py index 1d6bdc0257..8abb0885c6 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_workfile.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_workfile.py @@ -18,7 +18,8 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin): order = pyblish.api.CollectorOrder - 0.491 def process(self, context): - asset_name = context.data["asset"] + asset = context.data["asset"] + asset_name = asset if AYON_SERVER_ENABLED: asset_name = asset_name.split("/")[-1] @@ -29,7 +30,7 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin): # adding otio timeline to context otio_timeline = hiero_export.create_otio_timeline() - # get workfile thumnail paths + # get workfile thumbnail paths tmp_staging = tempfile.mkdtemp(prefix="pyblish_tmp_") thumbnail_name = "workfile_thumbnail.png" thumbnail_path = os.path.join(tmp_staging, thumbnail_name) @@ -51,8 +52,8 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin): } # get workfile paths - curent_file = project.path() - staging_dir, base_name = os.path.split(curent_file) + current_file = project.path() + staging_dir, base_name = os.path.split(current_file) # creating workfile representation workfile_representation = { @@ -63,10 +64,12 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin): } family = "workfile" instance_data = { + "label": "{} - {}Main".format( + asset, family), "name": "{}_{}".format(asset_name, family), "asset": context.data["asset"], # TODO use 'get_subset_name' - "subset": "{}{}".format(asset_name, family.capitalize()), + "subset": "{}{}Main".format(asset_name, family.capitalize()), "item": project, "family": family, "families": [], @@ -81,7 +84,7 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin): "activeProject": project, "activeTimeline": active_timeline, "otioTimeline": otio_timeline, - "currentFile": curent_file, + "currentFile": current_file, "colorspace": self.get_colorspace(project), "fps": fps } From efef9e2fd35abc0e8fe7989776bbaeeef87b8336 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 27 Oct 2023 18:06:54 +0200 Subject: [PATCH 0606/1233] small fix in timers manager --- openpype/modules/timers_manager/timers_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/timers_manager/timers_manager.py b/openpype/modules/timers_manager/timers_manager.py index 43286f7da4..674d834a1d 100644 --- a/openpype/modules/timers_manager/timers_manager.py +++ b/openpype/modules/timers_manager/timers_manager.py @@ -247,7 +247,7 @@ class TimersManager( return { "project_name": project_name, "asset_id": str(asset_doc["_id"]), - "asset_name": asset_doc["name"], + "asset_name": asset_name, "task_name": task_name, "task_type": task_type, "hierarchy": hierarchy_items From d9a35e7804c5b747d2316d8b6a91b8ee0866809e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 27 Oct 2023 22:51:08 +0200 Subject: [PATCH 0607/1233] fixing extract hierarchy to ayon --- .../publish/extract_hierarchy_to_ayon.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/openpype/plugins/publish/extract_hierarchy_to_ayon.py b/openpype/plugins/publish/extract_hierarchy_to_ayon.py index fe8cb40ad2..ef69369d67 100644 --- a/openpype/plugins/publish/extract_hierarchy_to_ayon.py +++ b/openpype/plugins/publish/extract_hierarchy_to_ayon.py @@ -191,15 +191,15 @@ class ExtractHierarchyToAYON(pyblish.api.ContextPlugin): """ # filter only the active publishing instances - active_folder_names = set() + active_folder_paths = set() for instance in context: if instance.data.get("publish") is not False: - active_folder_names.add(instance.data.get("asset")) + active_folder_paths.add(instance.data.get("asset")) - active_folder_names.discard(None) + active_folder_paths.discard(None) - self.log.debug("Active folder names: {}".format(active_folder_names)) - if not active_folder_names: + self.log.debug("Active folder paths: {}".format(active_folder_paths)) + if not active_folder_paths: return None project_item = None @@ -230,12 +230,13 @@ class ExtractHierarchyToAYON(pyblish.api.ContextPlugin): if not children_context: continue - for asset_name, asset_info in children_context.items(): + for asset, asset_info in children_context.items(): if ( - asset_name not in active_folder_names + asset not in active_folder_paths and not asset_info.get("childs") ): continue + asset_name = asset.split("/")[-1] item_id = uuid.uuid4().hex new_item = copy.deepcopy(asset_info) new_item["name"] = asset_name @@ -252,7 +253,7 @@ class ExtractHierarchyToAYON(pyblish.api.ContextPlugin): items_by_id[item_id] = new_item parent_id_by_item_id[item_id] = parent_id - if asset_name in active_folder_names: + if asset in active_folder_paths: valid_ids.add(item_id) hierarchy_queue.append((item_id, new_children_context)) From dcf5855a477f35fd9fc160c8830974ec7cd2b8ed Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 27 Oct 2023 22:51:24 +0200 Subject: [PATCH 0608/1233] hound --- openpype/hosts/hiero/api/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/hiero/api/plugin.py b/openpype/hosts/hiero/api/plugin.py index dc90012b0f..f72d27fed5 100644 --- a/openpype/hosts/hiero/api/plugin.py +++ b/openpype/hosts/hiero/api/plugin.py @@ -902,7 +902,7 @@ class PublishClip: tag_hierarchy_data = hero_data # add data to return data dict - return tag_hierarchy_data + return tag_hierarchy_data def _solve_tag_hierarchy_data(self, hierarchy_formatting_data): """ Solve tag data from hierarchy data and templates. """ From 7a156e8499c4be6e7e505fc9a9641008c7e2f829 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Sat, 28 Oct 2023 03:24:38 +0000 Subject: [PATCH 0609/1233] [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 4c58a5098f..d6839c9b70 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.17.4" +__version__ = "3.17.5-nightly.1" From 0443fa0a290b84413be0bd15a10921789d06a68f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 28 Oct 2023 03:25:22 +0000 Subject: [PATCH 0610/1233] 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 b92061f39a..73505368dd 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.17.5-nightly.1 - 3.17.4 - 3.17.4-nightly.2 - 3.17.4-nightly.1 @@ -134,7 +135,6 @@ body: - 3.15.1-nightly.3 - 3.15.1-nightly.2 - 3.15.1-nightly.1 - - 3.15.0 validations: required: true - type: dropdown From fe4e38190feaea158e077c8075330c69e9f10b4b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 30 Oct 2023 10:00:37 +0100 Subject: [PATCH 0611/1233] skip 'kitsu' addon in openpype modules load --- openpype/modules/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 080be251f3..e8b85d0e93 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -66,6 +66,7 @@ IGNORED_FILENAMES_IN_AYON = { "shotgrid", "sync_server", "slack", + "kitsu", } From c029fa632489529e36dbdaec5b74d2e938f46847 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 30 Oct 2023 17:21:29 +0800 Subject: [PATCH 0612/1233] support invalid checks on different variable types of attributes in Maxscript --- .../max/plugins/publish/validate_attributes.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_attributes.py b/openpype/hosts/max/plugins/publish/validate_attributes.py index 2d3f09f972..fa9912de07 100644 --- a/openpype/hosts/max/plugins/publish/validate_attributes.py +++ b/openpype/hosts/max/plugins/publish/validate_attributes.py @@ -38,9 +38,20 @@ class ValidateAttributes(OptionalPyblishPluginMixin, "Unknown Property Values:{}".format(invalid_properties)) return invalid_properties # TODO: support multiple varaible types in maxscript - invalid_attributes = [key for key, value in property_name.items() - if rt.Execute("{}.{}".format( - wrap_object, property_name[key]))!=value] + invalid_attributes = [] + for key, value in property_name.items(): + property_key = rt.Execute("{}.{}".format( + wrap_object, key)) + if isinstance(value, str) and "#" not in value: + if property_key != '"{}"'.format(value): + invalid_attributes.append(key) + + elif isinstance(value, bool): + if property_key != value: + invalid_attributes.append(key) + else: + if property_key != '{}'.format(value): + invalid_attributes.append(key) return invalid_attributes @@ -71,6 +82,7 @@ class ValidateAttributes(OptionalPyblishPluginMixin, invalid_attributes = [key for key, value in property_name.items() if rt.Execute("{}.{}".format( wrap_object, property_name[key]))!=value] + for attrs in invalid_attributes: rt.Execute("{}.{}={}".format( wrap_object, attrs, attributes[wrap_object][attrs])) From 0550668d3d3356a91f6b9c0fc47d6ebc27000ce0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Mon, 30 Oct 2023 10:24:01 +0100 Subject: [PATCH 0613/1233] Update openpype/hosts/hiero/api/plugin.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/hiero/api/plugin.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/openpype/hosts/hiero/api/plugin.py b/openpype/hosts/hiero/api/plugin.py index f72d27fed5..b0c73e41fb 100644 --- a/openpype/hosts/hiero/api/plugin.py +++ b/openpype/hosts/hiero/api/plugin.py @@ -733,11 +733,6 @@ class PublishClip: self.tag_data["asset_name"] ) self.tag_data["folderPath"] = folder_path - - # TODO: remove debug print - log.debug("___ folder_path: {}".format( - folder_path)) - if self.tag_data["heroTrack"] and self.review_layer: self.tag_data.update({"reviewTrack": self.review_layer}) else: From 05ab5e2e0fe53f4d585db7d4050ab6f6051b4f7b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 30 Oct 2023 12:14:55 +0100 Subject: [PATCH 0614/1233] use 'asset' from context instead of from anatomy data --- .../plugins/publish/create_publish_royalrender_job.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py index 3eb49a39ee..e13bf97e54 100644 --- a/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py +++ b/openpype/modules/royalrender/plugins/publish/create_publish_royalrender_job.py @@ -189,7 +189,7 @@ class CreatePublishRoyalRenderJob(pyblish.api.InstancePlugin, environment = RREnvList({ "AVALON_PROJECT": anatomy_data["project"]["name"], - "AVALON_ASSET": anatomy_data["asset"], + "AVALON_ASSET": instance.context.data["asset"], "AVALON_TASK": anatomy_data["task"]["name"], "OPENPYPE_USERNAME": anatomy_data["user"] }) From f27a25d2440671b19293bfba0c84180657178753 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 30 Oct 2023 12:15:27 +0100 Subject: [PATCH 0615/1233] add 'folder' key to template data in harmony collectors --- .../plugins/publish/collect_harmony_scenes.py | 3 +++ .../plugins/publish/collect_harmony_zips.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_harmony_scenes.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_harmony_scenes.py index 48c36aa067..c435ca2096 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_harmony_scenes.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_harmony_scenes.py @@ -60,6 +60,9 @@ class CollectHarmonyScenes(pyblish.api.InstancePlugin): # updating hierarchy data anatomy_data_new.update({ "asset": asset_data["name"], + "folder": { + "name": asset_data["name"], + }, "task": { "name": task, "type": task_type, diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_harmony_zips.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_harmony_zips.py index 40a969f8df..d90215e767 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_harmony_zips.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_harmony_zips.py @@ -56,6 +56,9 @@ class CollectHarmonyZips(pyblish.api.InstancePlugin): anatomy_data_new.update( { "asset": asset_data["name"], + "folder": { + "name": asset_data["name"], + }, "task": { "name": task, "type": task_type, From 4c55f515dd8513382fc8b9b7ac7710716708f6c6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 30 Oct 2023 12:16:07 +0100 Subject: [PATCH 0616/1233] do not use 'instance.data["asset"]' to prepare template data --- .../publish/collect_anatomy_instance_data.py | 30 ++++++++----------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/openpype/plugins/publish/collect_anatomy_instance_data.py b/openpype/plugins/publish/collect_anatomy_instance_data.py index cc6da9b2c3..1b4b44e40e 100644 --- a/openpype/plugins/publish/collect_anatomy_instance_data.py +++ b/openpype/plugins/publish/collect_anatomy_instance_data.py @@ -187,35 +187,29 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): self.log.debug("Storing anatomy data to instance data.") project_doc = context.data["projectEntity"] - context_asset_doc = context.data.get("assetEntity") - project_task_types = project_doc["config"]["tasks"] for instance in context: + asset_doc = instance.data.get("assetEntity") anatomy_updates = { - "asset": instance.data["asset"], - "folder": { - "name": instance.data["asset"], - }, "family": instance.data["family"], "subset": instance.data["subset"], } - - # Hierarchy - asset_doc = instance.data.get("assetEntity") - if ( - asset_doc - and ( - not context_asset_doc - or asset_doc["_id"] != context_asset_doc["_id"] - ) - ): + if asset_doc: parents = asset_doc["data"].get("parents") or list() parent_name = project_doc["name"] if parents: parent_name = parents[-1] - anatomy_updates["hierarchy"] = "/".join(parents) - anatomy_updates["parent"] = parent_name + + hierarchy = "/".join(parents) + anatomy_updates.update({ + "asset": asset_doc["name"], + "hierarchy": hierarchy, + "parent": parent_name, + "folder": { + "name": asset_doc["name"], + }, + }) # Task task_type = None From 6c7e5c66a6b85e2ee8e42c2c9b38ea6639d9dd42 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 30 Oct 2023 21:13:42 +0800 Subject: [PATCH 0617/1233] support invalid checks on different variable types of attributes in Maxscript & repair actions --- .../plugins/publish/validate_attributes.py | 45 +++++++++++-------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_attributes.py b/openpype/hosts/max/plugins/publish/validate_attributes.py index fa9912de07..f266b2bca1 100644 --- a/openpype/hosts/max/plugins/publish/validate_attributes.py +++ b/openpype/hosts/max/plugins/publish/validate_attributes.py @@ -42,16 +42,16 @@ class ValidateAttributes(OptionalPyblishPluginMixin, for key, value in property_name.items(): property_key = rt.Execute("{}.{}".format( wrap_object, key)) - if isinstance(value, str) and "#" not in value: - if property_key != '"{}"'.format(value): - invalid_attributes.append(key) - - elif isinstance(value, bool): - if property_key != value: - invalid_attributes.append(key) + if isinstance(value, str) and ( + value.startswith("#") and not value.endswith(")") + ): + # not applicable for #() array value type + # and only applicable for enum i.e. #bob, #sally + if "#{}".format(property_key) != value: + invalid_attributes.append((wrap_object, key)) else: - if property_key != '{}'.format(value): - invalid_attributes.append(key) + if property_key != value: + invalid_attributes.append((wrap_object, key)) return invalid_attributes @@ -62,12 +62,14 @@ class ValidateAttributes(OptionalPyblishPluginMixin, invalid_attributes = self.get_invalid(context) if invalid_attributes: bullet_point_invalid_statement = "\n".join( - "- {}".format(invalid) for invalid in invalid_attributes + "- {}".format(invalid) for invalid + in invalid_attributes ) report = ( "Required Attribute(s) have invalid value(s).\n\n" f"{bullet_point_invalid_statement}\n\n" - "You can use repair action to fix it." + "You can use repair action to fix them if they are not\n" + "unknown property value(s)" ) raise PublishValidationError( report, title="Invalid Value(s) for Required Attribute(s)") @@ -78,11 +80,16 @@ class ValidateAttributes(OptionalPyblishPluginMixin, context.data["project_settings"]["max"]["publish"] ["ValidateAttributes"]["attributes"] ) - for wrap_object, property_name in attributes.items(): - invalid_attributes = [key for key, value in property_name.items() - if rt.Execute("{}.{}".format( - wrap_object, property_name[key]))!=value] - - for attrs in invalid_attributes: - rt.Execute("{}.{}={}".format( - wrap_object, attrs, attributes[wrap_object][attrs])) + invalid_attributes = cls.get_invalid(context) + for attrs in invalid_attributes: + prop, attr = attrs + value = attributes[prop][attr] + if isinstance(value, str) and not value.startswith("#"): + attribute_fix = '{}.{}="{}"'.format( + prop, attr, value + ) + else: + attribute_fix = "{}.{}={}".format( + prop, attr, value + ) + rt.Execute(attribute_fix) From 319a236bb2bfaf59dcaa2568685fb30bc1a04e8c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 30 Oct 2023 16:07:14 +0100 Subject: [PATCH 0618/1233] do not strip asset name --- openpype/tools/creator/window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/creator/window.py b/openpype/tools/creator/window.py index 47f27a262a..117519e1d7 100644 --- a/openpype/tools/creator/window.py +++ b/openpype/tools/creator/window.py @@ -214,7 +214,7 @@ class CreatorWindow(QtWidgets.QDialog): asset_name = self._asset_name_input.text() # Early exit if no asset name - if not asset_name.strip(): + if not asset_name: self._build_menu() self.echo("Asset name is required ..") self._set_valid_state(False) From 027cced5f58bb6af9b476f96744bf0909c5b6ebc Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 30 Oct 2023 16:40:46 +0100 Subject: [PATCH 0619/1233] show full folder path in look assigner --- openpype/hosts/maya/tools/mayalookassigner/commands.py | 8 +++++--- openpype/hosts/maya/tools/mayalookassigner/widgets.py | 3 ++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/tools/mayalookassigner/commands.py b/openpype/hosts/maya/tools/mayalookassigner/commands.py index 5cc4f84931..86df502ecd 100644 --- a/openpype/hosts/maya/tools/mayalookassigner/commands.py +++ b/openpype/hosts/maya/tools/mayalookassigner/commands.py @@ -4,7 +4,7 @@ from collections import defaultdict import maya.cmds as cmds -from openpype.client import get_assets +from openpype.client import get_assets, get_asset_name_identifier from openpype.pipeline import ( remove_container, registered_host, @@ -128,7 +128,8 @@ def create_items_from_nodes(nodes): project_name = get_current_project_name() asset_ids = set(id_hashes.keys()) - asset_docs = get_assets(project_name, asset_ids, fields=["name"]) + fields = {"_id", "name", "data.parents"} + asset_docs = get_assets(project_name, asset_ids, fields=fields) asset_docs_by_id = { str(asset_doc["_id"]): asset_doc for asset_doc in asset_docs @@ -156,8 +157,9 @@ def create_items_from_nodes(nodes): namespace = get_namespace_from_node(node) namespaces.add(namespace) + label = get_asset_name_identifier(asset_doc) asset_view_items.append({ - "label": asset_doc["name"], + "label": label, "asset": asset_doc, "looks": looks, "namespaces": namespaces diff --git a/openpype/hosts/maya/tools/mayalookassigner/widgets.py b/openpype/hosts/maya/tools/mayalookassigner/widgets.py index 82c37e2104..ef29a4c726 100644 --- a/openpype/hosts/maya/tools/mayalookassigner/widgets.py +++ b/openpype/hosts/maya/tools/mayalookassigner/widgets.py @@ -3,6 +3,7 @@ from collections import defaultdict from qtpy import QtWidgets, QtCore +from openpype.client import get_asset_name_identifier from openpype.tools.utils.models import TreeModel from openpype.tools.utils.lib import ( preserve_expanded_rows, @@ -126,7 +127,7 @@ class AssetOutliner(QtWidgets.QWidget): asset_namespaces = defaultdict(set) for item in items: asset_id = str(item["asset"]["_id"]) - asset_name = item["asset"]["name"] + asset_name = get_asset_name_identifier(item["asset"]) asset_namespaces[asset_name].add(item.get("namespace")) if asset_name in assets: From 246c408ce7036de973b1dc6ba1d918f5670d495c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 30 Oct 2023 22:21:46 +0100 Subject: [PATCH 0620/1233] Maya: Remove RenderSetup layer observers that are not needed since new publisher --- openpype/hosts/maya/api/lib.py | 153 ---------------------------- openpype/hosts/maya/api/pipeline.py | 10 -- 2 files changed, 163 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 7c49c837e9..6d785234c5 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -3069,159 +3069,6 @@ def _get_render_instances(): return instances -renderItemObserverList = [] - - -class RenderSetupListObserver: - """Observer to catch changes in render setup layers.""" - - def listItemAdded(self, item): - print("--- adding ...") - self._add_render_layer(item) - - def listItemRemoved(self, item): - print("--- removing ...") - self._remove_render_layer(item.name()) - - def _add_render_layer(self, item): - render_sets = _get_render_instances() - layer_name = item.name() - - for render_set in render_sets: - members = cmds.sets(render_set, query=True) or [] - - namespace_name = "_{}".format(render_set) - if not cmds.namespace(exists=namespace_name): - index = 1 - namespace_name = "_{}".format(render_set) - try: - cmds.namespace(rm=namespace_name) - except RuntimeError: - # namespace is not empty, so we leave it untouched - pass - orignal_namespace_name = namespace_name - while(cmds.namespace(exists=namespace_name)): - namespace_name = "{}{}".format( - orignal_namespace_name, index) - index += 1 - - namespace = cmds.namespace(add=namespace_name) - - if members: - # if set already have namespaced members, use the same - # namespace as others. - namespace = members[0].rpartition(":")[0] - else: - namespace = namespace_name - - render_layer_set_name = "{}:{}".format(namespace, layer_name) - if render_layer_set_name in members: - continue - print(" - creating set for {}".format(layer_name)) - maya_set = cmds.sets(n=render_layer_set_name, empty=True) - cmds.sets(maya_set, forceElement=render_set) - rio = RenderSetupItemObserver(item) - print("- adding observer for {}".format(item.name())) - item.addItemObserver(rio.itemChanged) - renderItemObserverList.append(rio) - - def _remove_render_layer(self, layer_name): - render_sets = _get_render_instances() - - for render_set in render_sets: - members = cmds.sets(render_set, query=True) - if not members: - continue - - # all sets under set should have the same namespace - namespace = members[0].rpartition(":")[0] - render_layer_set_name = "{}:{}".format(namespace, layer_name) - - if render_layer_set_name in members: - print(" - removing set for {}".format(layer_name)) - cmds.delete(render_layer_set_name) - - -class RenderSetupItemObserver: - """Handle changes in render setup items.""" - - def __init__(self, item): - self.item = item - self.original_name = item.name() - - def itemChanged(self, *args, **kwargs): - """Item changed callback.""" - if self.item.name() == self.original_name: - return - - render_sets = _get_render_instances() - - for render_set in render_sets: - members = cmds.sets(render_set, query=True) - if not members: - continue - - # all sets under set should have the same namespace - namespace = members[0].rpartition(":")[0] - render_layer_set_name = "{}:{}".format( - namespace, self.original_name) - - if render_layer_set_name in members: - print(" <> renaming {} to {}".format(self.original_name, - self.item.name())) - cmds.rename(render_layer_set_name, - "{}:{}".format( - namespace, self.item.name())) - self.original_name = self.item.name() - - -renderListObserver = RenderSetupListObserver() - - -def add_render_layer_change_observer(): - import maya.app.renderSetup.model.renderSetup as renderSetup - - rs = renderSetup.instance() - render_sets = _get_render_instances() - - layers = rs.getRenderLayers() - for render_set in render_sets: - members = cmds.sets(render_set, query=True) - if not members: - continue - # all sets under set should have the same namespace - namespace = members[0].rpartition(":")[0] - for layer in layers: - render_layer_set_name = "{}:{}".format(namespace, layer.name()) - if render_layer_set_name not in members: - continue - rio = RenderSetupItemObserver(layer) - print("- adding observer for {}".format(layer.name())) - layer.addItemObserver(rio.itemChanged) - renderItemObserverList.append(rio) - - -def add_render_layer_observer(): - import maya.app.renderSetup.model.renderSetup as renderSetup - - print("> adding renderSetup observer ...") - rs = renderSetup.instance() - rs.addListObserver(renderListObserver) - pass - - -def remove_render_layer_observer(): - import maya.app.renderSetup.model.renderSetup as renderSetup - - print("< removing renderSetup observer ...") - rs = renderSetup.instance() - try: - rs.removeListObserver(renderListObserver) - except ValueError: - # no observer set yet - pass - - def update_content_on_context_change(): """ This will update scene content to match new asset on context change diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py index 6b791c9665..1ecfdfaa40 100644 --- a/openpype/hosts/maya/api/pipeline.py +++ b/openpype/hosts/maya/api/pipeline.py @@ -580,20 +580,11 @@ def on_save(): lib.set_id(node, new_id, overwrite=False) -def _update_render_layer_observers(): - # Helper to trigger update for all renderlayer observer logic - lib.remove_render_layer_observer() - lib.add_render_layer_observer() - lib.add_render_layer_change_observer() - - def on_open(): """On scene open let's assume the containers have changed.""" from openpype.widgets import popup - utils.executeDeferred(_update_render_layer_observers) - # Validate FPS after update_task_from_path to # ensure it is using correct FPS for the asset lib.validate_fps() @@ -630,7 +621,6 @@ def on_new(): with lib.suspended_refresh(): lib.set_context_settings() - utils.executeDeferred(_update_render_layer_observers) _remove_workfile_lock() From 90c8922a8a53f0e2265ca8037d1593d9da6559d3 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 30 Oct 2023 22:54:54 +0100 Subject: [PATCH 0621/1233] Remove remaining unused function --- openpype/hosts/maya/api/lib.py | 37 ---------------------------------- 1 file changed, 37 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 6d785234c5..2d394893cd 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -115,8 +115,6 @@ _alembic_options = { INT_FPS = {15, 24, 25, 30, 48, 50, 60, 44100, 48000} FLOAT_FPS = {23.98, 23.976, 29.97, 47.952, 59.94} -RENDERLIKE_INSTANCE_FAMILIES = ["rendering", "vrayscene"] - DISPLAY_LIGHTS_ENUM = [ {"label": "Use Project Settings", "value": "project_settings"}, @@ -3034,41 +3032,6 @@ class shelf(): cmds.shelfLayout(self.name, p="ShelfLayout") -def _get_render_instances(): - """Return all 'render-like' instances. - - This returns list of instance sets that needs to receive information - about render layer changes. - - Returns: - list: list of instances - - """ - objectset = cmds.ls("*.id", long=True, exactType="objectSet", - recursive=True, objectsOnly=True) - - instances = [] - for objset in objectset: - if not cmds.attributeQuery("id", node=objset, exists=True): - continue - - id_attr = "{}.id".format(objset) - if cmds.getAttr(id_attr) != "pyblish.avalon.instance": - continue - - has_family = cmds.attributeQuery("family", - node=objset, - exists=True) - if not has_family: - continue - - if cmds.getAttr( - "{}.family".format(objset)) in RENDERLIKE_INSTANCE_FAMILIES: - instances.append(objset) - - return instances - - def update_content_on_context_change(): """ This will update scene content to match new asset on context change From 32e88ebc9663431dcc58a62c8e4a4bb784848966 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 30 Oct 2023 23:47:53 +0100 Subject: [PATCH 0622/1233] Show workfiles tool if current file is not set to make it clear there's user interaction needed to save --- openpype/hosts/resolve/api/menu.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/resolve/api/menu.py b/openpype/hosts/resolve/api/menu.py index 160cc44fdb..9c6fe4957c 100644 --- a/openpype/hosts/resolve/api/menu.py +++ b/openpype/hosts/resolve/api/menu.py @@ -118,6 +118,9 @@ class OpenPypeMenu(QtWidgets.QWidget): host = registered_host() current_file = host.get_current_workfile() if not current_file: + print("Current project is not saved. " + "Please save once first via workfiles tool.") + host_tools.show_workfiles() return print(f"Saving current file to: {current_file}") From f0b8d8d79826df7c27650dfbe68046e2d2d63d9d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 31 Oct 2023 14:19:37 +0800 Subject: [PATCH 0623/1233] add docstrings and clean up the codes on the validator --- .../plugins/publish/validate_attributes.py | 80 +++++++++++++------ 1 file changed, 55 insertions(+), 25 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_attributes.py b/openpype/hosts/max/plugins/publish/validate_attributes.py index f266b2bca1..f603934eed 100644 --- a/openpype/hosts/max/plugins/publish/validate_attributes.py +++ b/openpype/hosts/max/plugins/publish/validate_attributes.py @@ -10,9 +10,47 @@ from openpype.pipeline.publish import ( ) +def has_property(object_name, property_name): + """Return whether an object has a property with given name""" + return rt.Execute(f'isProperty {object_name} "{property_name}"') + +def is_matching_value(object_name, property_name, value): + """Return whether an existing property matches value `value""" + property_value = rt.Execute(f"{object_name}.{property_name}") + + # Wrap property value if value is a string valued attributes + # starting with a `#` + if ( + isinstance(value, str) and + value.startswith("#") and + not value.endswith(")") + ): + # prefix value with `#` + # not applicable for #() array value type + # and only applicable for enum i.e. #bob, #sally + property_value = f"#{property_value}" + + return property_value == value + + class ValidateAttributes(OptionalPyblishPluginMixin, ContextPlugin): - """Validates attributes are consistent in 3ds max.""" + """Validates attributes in the project setting are consistent + with the nodes from MaxWrapper Class in 3ds max. + E.g. "renderers.current.separateAovFiles", + "renderers.production.PrimaryGIEngine" + Admin(s) need to put json below and enable this validator for a check: + { + "renderers.current":{ + "separateAovFiles" : True + } + "renderers.production":{ + "PrimaryGIEngine": "#RS_GIENGINE_BRUTE_FORCE", + } + .... + } + + """ order = ValidatorOrder hosts = ["max"] @@ -28,32 +66,24 @@ class ValidateAttributes(OptionalPyblishPluginMixin, ) if not attributes: return + invalid = [] + for object_name, required_properties in attributes.items(): + if not rt.Execute(f"isValidValue {object_name}"): + # Skip checking if the node does not + # exist in MaxWrapper Class + continue - for wrap_object, property_name in attributes.items(): - invalid_properties = [key for key in property_name.keys() - if not rt.Execute( - f'isProperty {wrap_object} "{key}"')] - if invalid_properties: - cls.log.error( - "Unknown Property Values:{}".format(invalid_properties)) - return invalid_properties - # TODO: support multiple varaible types in maxscript - invalid_attributes = [] - for key, value in property_name.items(): - property_key = rt.Execute("{}.{}".format( - wrap_object, key)) - if isinstance(value, str) and ( - value.startswith("#") and not value.endswith(")") - ): - # not applicable for #() array value type - # and only applicable for enum i.e. #bob, #sally - if "#{}".format(property_key) != value: - invalid_attributes.append((wrap_object, key)) - else: - if property_key != value: - invalid_attributes.append((wrap_object, key)) + for property_name, value in required_properties.items(): + if not has_property(object_name, property_name): + cls.log.error(f"Non-existing property: {object_name}.{property_name}") + invalid.append((object_name, property_name)) - return invalid_attributes + if not is_matching_value(object_name, property_name, value): + cls.log.error( + f"Invalid value for: {object_name}.{property_name}. Should be: {value}") + invalid.append((object_name, property_name)) + + return invalid def process(self, context): if not self.is_active(context.data): From 4c204a87a917ef05bf5b23e3f180d4f95650a3fb Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 31 Oct 2023 14:20:25 +0800 Subject: [PATCH 0624/1233] hound --- openpype/hosts/max/plugins/publish/validate_attributes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/max/plugins/publish/validate_attributes.py b/openpype/hosts/max/plugins/publish/validate_attributes.py index f603934eed..44d6c64139 100644 --- a/openpype/hosts/max/plugins/publish/validate_attributes.py +++ b/openpype/hosts/max/plugins/publish/validate_attributes.py @@ -14,6 +14,7 @@ def has_property(object_name, property_name): """Return whether an object has a property with given name""" return rt.Execute(f'isProperty {object_name} "{property_name}"') + def is_matching_value(object_name, property_name, value): """Return whether an existing property matches value `value""" property_value = rt.Execute(f"{object_name}.{property_name}") From 33a21674c5e752c19be8636ab6587f38f91f8f59 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 31 Oct 2023 14:21:37 +0800 Subject: [PATCH 0625/1233] hound --- openpype/hosts/max/plugins/publish/validate_attributes.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_attributes.py b/openpype/hosts/max/plugins/publish/validate_attributes.py index 44d6c64139..5697237c95 100644 --- a/openpype/hosts/max/plugins/publish/validate_attributes.py +++ b/openpype/hosts/max/plugins/publish/validate_attributes.py @@ -76,12 +76,14 @@ class ValidateAttributes(OptionalPyblishPluginMixin, for property_name, value in required_properties.items(): if not has_property(object_name, property_name): - cls.log.error(f"Non-existing property: {object_name}.{property_name}") + cls.log.error( + f"Non-existing property: {object_name}.{property_name}") invalid.append((object_name, property_name)) if not is_matching_value(object_name, property_name, value): cls.log.error( - f"Invalid value for: {object_name}.{property_name}. Should be: {value}") + f"Invalid value for: {object_name}.{property_name}" + f". Should be: {value}") invalid.append((object_name, property_name)) return invalid From ce80ca2397c7ed9b609f0358a9c0595289a8e260 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 31 Oct 2023 16:10:22 +0800 Subject: [PATCH 0626/1233] debug message --- openpype/hosts/max/plugins/publish/validate_attributes.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/max/plugins/publish/validate_attributes.py b/openpype/hosts/max/plugins/publish/validate_attributes.py index 5697237c95..00b9d34c06 100644 --- a/openpype/hosts/max/plugins/publish/validate_attributes.py +++ b/openpype/hosts/max/plugins/publish/validate_attributes.py @@ -72,6 +72,8 @@ class ValidateAttributes(OptionalPyblishPluginMixin, if not rt.Execute(f"isValidValue {object_name}"): # Skip checking if the node does not # exist in MaxWrapper Class + cls.log.debug(f"Unable to find '{object_name}'." + f" Skipping validation of attributes") continue for property_name, value in required_properties.items(): From 3218b8064cdd00f7efab87bab935e1c8cb130c16 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 31 Oct 2023 16:11:57 +0800 Subject: [PATCH 0627/1233] hound & docstring tweak --- openpype/hosts/max/plugins/publish/validate_attributes.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_attributes.py b/openpype/hosts/max/plugins/publish/validate_attributes.py index 00b9d34c06..75d3f05d07 100644 --- a/openpype/hosts/max/plugins/publish/validate_attributes.py +++ b/openpype/hosts/max/plugins/publish/validate_attributes.py @@ -46,7 +46,7 @@ class ValidateAttributes(OptionalPyblishPluginMixin, "separateAovFiles" : True } "renderers.production":{ - "PrimaryGIEngine": "#RS_GIENGINE_BRUTE_FORCE", + "PrimaryGIEngine": "#RS_GIENGINE_BRUTE_FORCE" } .... } @@ -79,7 +79,8 @@ class ValidateAttributes(OptionalPyblishPluginMixin, for property_name, value in required_properties.items(): if not has_property(object_name, property_name): cls.log.error( - f"Non-existing property: {object_name}.{property_name}") + "Non-existing property: " + f"{object_name}.{property_name}") invalid.append((object_name, property_name)) if not is_matching_value(object_name, property_name, value): From 3606f312b0f671404cea584dfdca2d9af749e18c Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 31 Oct 2023 16:33:06 +0800 Subject: [PATCH 0628/1233] fix the wrong aspect ratio and viewport doesn't maximize to 1 during context --- openpype/hosts/max/api/preview_animation.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/max/api/preview_animation.py b/openpype/hosts/max/api/preview_animation.py index 1bf99b86d0..bef5741343 100644 --- a/openpype/hosts/max/api/preview_animation.py +++ b/openpype/hosts/max/api/preview_animation.py @@ -23,8 +23,8 @@ def play_preview_when_done(has_autoplay): @contextlib.contextmanager -def viewport_camera(camera): - """Set viewport camera during context +def viewport_layout_and_camera(camera): + """Set viewport layout and camera during context ***For 3dsMax 2024+ Args: camera (str): viewport camera @@ -36,9 +36,12 @@ def viewport_camera(camera): original = rt.getNodeByName(camera) review_camera = rt.getNodeByName(camera) try: + if rt.viewport.getLayout() != rt.Name("layout_1"): + rt.viewport.setLayout(rt.Name("layout_1")) rt.viewport.setCamera(review_camera) yield finally: + rt.viewport.ResetAllViews() rt.viewport.setCamera(original) @@ -162,6 +165,7 @@ def _render_preview_animation_max_pre_2024( Returns: list: Created filepaths """ + # get the screenshot percent = percentSize / 100.0 res_width = int(round(rt.renderWidth * percent)) @@ -190,7 +194,7 @@ def _render_preview_animation_max_pre_2024( widthCrop = dib_height * renderRatio leftEdge = int((dib_width - widthCrop) / 2.0) tempImage_bmp = rt.bitmap(widthCrop, dib_height) - src_box_value = rt.Box2(0, leftEdge, dib_width, dib_height) + src_box_value = rt.Box2(0, leftEdge, widthCrop, dib_height) rt.pasteBitmap(dib, tempImage_bmp, src_box_value, rt.Point2(0, 0)) # copy the bitmap and close it rt.copy(tempImage_bmp, preview_res) @@ -243,7 +247,7 @@ def render_preview_animation( if viewport_options is None: viewport_options = viewport_options_for_preview_animation() with play_preview_when_done(False): - with viewport_camera(camera): + with viewport_layout_and_camera(camera): with render_resolution(width, height): if int(get_max_version()) < 2024: with viewport_preference_setting( From e120b55fa47d7b716aa71122f4ecc3aa93ff6a06 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 31 Oct 2023 17:13:24 +0800 Subject: [PATCH 0629/1233] setLayout in regards to original layout --- openpype/hosts/max/api/preview_animation.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/max/api/preview_animation.py b/openpype/hosts/max/api/preview_animation.py index bef5741343..22de298175 100644 --- a/openpype/hosts/max/api/preview_animation.py +++ b/openpype/hosts/max/api/preview_animation.py @@ -29,11 +29,12 @@ def viewport_layout_and_camera(camera): Args: camera (str): viewport camera """ - original = rt.viewport.getCamera() - if not original: + original_camera = rt.viewport.getCamera() + original_layout = rt.viewport.getLayout() + if not original_camera: # if there is no original camera # use the current camera as original - original = rt.getNodeByName(camera) + original_camera = rt.getNodeByName(camera) review_camera = rt.getNodeByName(camera) try: if rt.viewport.getLayout() != rt.Name("layout_1"): @@ -41,8 +42,8 @@ def viewport_layout_and_camera(camera): rt.viewport.setCamera(review_camera) yield finally: - rt.viewport.ResetAllViews() - rt.viewport.setCamera(original) + rt.viewport.setLayout(original_layout) + rt.viewport.setCamera(original_camera) @contextlib.contextmanager From 009cda005227158561d898aa59c28ca16ae4166c Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 31 Oct 2023 17:18:06 +0800 Subject: [PATCH 0630/1233] update the debug message with dots --- openpype/hosts/max/plugins/publish/validate_attributes.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_attributes.py b/openpype/hosts/max/plugins/publish/validate_attributes.py index 75d3f05d07..172b65e955 100644 --- a/openpype/hosts/max/plugins/publish/validate_attributes.py +++ b/openpype/hosts/max/plugins/publish/validate_attributes.py @@ -73,7 +73,7 @@ class ValidateAttributes(OptionalPyblishPluginMixin, # Skip checking if the node does not # exist in MaxWrapper Class cls.log.debug(f"Unable to find '{object_name}'." - f" Skipping validation of attributes") + " Skipping validation of attributes.") continue for property_name, value in required_properties.items(): @@ -86,7 +86,7 @@ class ValidateAttributes(OptionalPyblishPluginMixin, if not is_matching_value(object_name, property_name, value): cls.log.error( f"Invalid value for: {object_name}.{property_name}" - f". Should be: {value}") + f" Should be: {value}") invalid.append((object_name, property_name)) return invalid @@ -105,7 +105,7 @@ class ValidateAttributes(OptionalPyblishPluginMixin, "Required Attribute(s) have invalid value(s).\n\n" f"{bullet_point_invalid_statement}\n\n" "You can use repair action to fix them if they are not\n" - "unknown property value(s)" + "unknown property value(s)." ) raise PublishValidationError( report, title="Invalid Value(s) for Required Attribute(s)") From dadd258cf1f1938ef609ecec689d08bb19815198 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 31 Oct 2023 17:26:25 +0800 Subject: [PATCH 0631/1233] make the viewport_layout_and_camera reuseable --- openpype/hosts/max/api/preview_animation.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/max/api/preview_animation.py b/openpype/hosts/max/api/preview_animation.py index 22de298175..dcf243d31e 100644 --- a/openpype/hosts/max/api/preview_animation.py +++ b/openpype/hosts/max/api/preview_animation.py @@ -23,11 +23,13 @@ def play_preview_when_done(has_autoplay): @contextlib.contextmanager -def viewport_layout_and_camera(camera): +def viewport_layout_and_camera(camera, layout="layout_1"): """Set viewport layout and camera during context ***For 3dsMax 2024+ Args: camera (str): viewport camera + layout (str): layout to use in viewport, defaults to `layout_1` + Use None to not change viewport layout during context. """ original_camera = rt.viewport.getCamera() original_layout = rt.viewport.getLayout() @@ -37,8 +39,10 @@ def viewport_layout_and_camera(camera): original_camera = rt.getNodeByName(camera) review_camera = rt.getNodeByName(camera) try: - if rt.viewport.getLayout() != rt.Name("layout_1"): - rt.viewport.setLayout(rt.Name("layout_1")) + if layout is not None: + layout = rt.Name(layout) + if rt.viewport.getLayout() != layout: + rt.viewport.setLayout(layout) rt.viewport.setCamera(review_camera) yield finally: From ad0b941475c67196ad7e09beada2bd81b2d51a63 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 31 Oct 2023 17:28:42 +0800 Subject: [PATCH 0632/1233] lowercase invalid msg for the condition of not is_maching_value function --- openpype/hosts/max/plugins/publish/validate_attributes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/validate_attributes.py b/openpype/hosts/max/plugins/publish/validate_attributes.py index 172b65e955..0cd405aebd 100644 --- a/openpype/hosts/max/plugins/publish/validate_attributes.py +++ b/openpype/hosts/max/plugins/publish/validate_attributes.py @@ -86,7 +86,7 @@ class ValidateAttributes(OptionalPyblishPluginMixin, if not is_matching_value(object_name, property_name, value): cls.log.error( f"Invalid value for: {object_name}.{property_name}" - f" Should be: {value}") + f" should be: {value}") invalid.append((object_name, property_name)) return invalid From 6daa1b898a2cfe13509c71861816fad221778816 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 31 Oct 2023 10:47:23 +0000 Subject: [PATCH 0633/1233] Use collections as asset group for blendscene family --- .../plugins/create/create_blendScene.py | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/blender/plugins/create/create_blendScene.py b/openpype/hosts/blender/plugins/create/create_blendScene.py index 63bcf212ff..96e63924d3 100644 --- a/openpype/hosts/blender/plugins/create/create_blendScene.py +++ b/openpype/hosts/blender/plugins/create/create_blendScene.py @@ -31,21 +31,11 @@ class CreateBlendScene(plugin.Creator): asset = self.data["asset"] subset = self.data["subset"] name = plugin.asset_name(asset, subset) - asset_group = bpy.data.objects.new(name=name, object_data=None) - asset_group.empty_display_type = 'SINGLE_ARROW' - instances.objects.link(asset_group) + + # Create the new asset group as collection + asset_group = bpy.data.collections.new(name=name) + instances.children.link(asset_group) self.data['task'] = get_current_task_name() lib.imprint(asset_group, self.data) - # Add selected objects to instance - if (self.options or {}).get("useSelection"): - bpy.context.view_layer.objects.active = asset_group - selected = lib.get_selection() - for obj in selected: - if obj.parent in selected: - obj.select_set(False) - continue - selected.append(asset_group) - bpy.ops.object.parent_set(keep_transform=True) - return asset_group From 1c45fa139b38ea8f2c9055613a5f536f2b9fa40e Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 31 Oct 2023 10:48:05 +0000 Subject: [PATCH 0634/1233] Include collections asset group to get unique number --- openpype/hosts/blender/api/plugin.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index fb87d08cce..45b0c60b3b 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -9,7 +9,10 @@ from openpype.pipeline import ( LegacyCreator, LoaderPlugin, ) -from .pipeline import AVALON_CONTAINERS +from .pipeline import ( + AVALON_CONTAINERS, + AVALON_PROPERTY, +) from .ops import ( MainThreadItem, execute_in_main_thread @@ -40,9 +43,16 @@ def get_unique_number( avalon_container = bpy.data.collections.get(AVALON_CONTAINERS) if not avalon_container: return "01" - asset_groups = avalon_container.all_objects - - container_names = [c.name for c in asset_groups if c.type == 'EMPTY'] + # Check the names of both object and collection containers + obj_asset_groups = avalon_container.all_objects + obj_group_names = [ + c.name for c in obj_asset_groups + if c.type == 'EMPTY' and c.get(AVALON_PROPERTY)] + coll_asset_groups = avalon_container.children_recursive + coll_group_names = [ + c.name for c in coll_asset_groups + if c.get(AVALON_PROPERTY)] + container_names = obj_group_names + coll_group_names count = 1 name = f"{asset}_{count:0>2}_{subset}" while name in container_names: From a180a276a3838dd9b433e03c7e52b46bd39d5886 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 31 Oct 2023 10:49:17 +0000 Subject: [PATCH 0635/1233] Add check that instance is object to pack images --- .../hosts/blender/plugins/publish/extract_blend.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/extract_blend.py b/openpype/hosts/blender/plugins/publish/extract_blend.py index c8eeef7fd7..4b6d9e7c69 100644 --- a/openpype/hosts/blender/plugins/publish/extract_blend.py +++ b/openpype/hosts/blender/plugins/publish/extract_blend.py @@ -25,11 +25,11 @@ class ExtractBlend(publish.Extractor): data_blocks = set() - for obj in instance: - data_blocks.add(obj) - # Pack used images in the blend files. - if obj.type == 'MESH': - for material_slot in obj.material_slots: + for data in instance: + data_blocks.add(data) + if isinstance(data, bpy.types.Object) and data.type == 'MESH': + # Pack used images in the blend files. + for material_slot in data.material_slots: mat = material_slot.material if mat and mat.use_nodes: tree = mat.node_tree From 371f9a52755a6c761b87ff1e6a07fe59384acbc1 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 31 Oct 2023 11:49:17 +0100 Subject: [PATCH 0636/1233] Bugfix: Collect Rendered Files only collecting first instance (#5832) * Bugfix: Collect all instances from the metadata file - don't return on first iteration * Fix initial state of variable --- .../plugins/publish/collect_rendered_files.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/publish/collect_rendered_files.py b/openpype/plugins/publish/collect_rendered_files.py index 8a5a5a83f1..a249b3acda 100644 --- a/openpype/plugins/publish/collect_rendered_files.py +++ b/openpype/plugins/publish/collect_rendered_files.py @@ -56,6 +56,17 @@ class CollectRenderedFiles(pyblish.api.ContextPlugin): data_object["stagingDir"] = anatomy.fill_root(staging_dir) def _process_path(self, data, anatomy): + """Process data of a single JSON publish metadata file. + + Args: + data: The loaded metadata from the JSON file + anatomy: Anatomy for the current context + + Returns: + bool: Whether any instance of this particular metadata file + has a persistent staging dir. + + """ # validate basic necessary data data_err = "invalid json file - missing data" required = ["asset", "user", "comment", @@ -89,6 +100,7 @@ class CollectRenderedFiles(pyblish.api.ContextPlugin): os.environ["FTRACK_SERVER"] = ftrack["FTRACK_SERVER"] # now we can just add instances from json file and we are done + any_staging_dir_persistent = False for instance_data in data.get("instances"): self.log.debug(" - processing instance for {}".format( @@ -106,6 +118,9 @@ class CollectRenderedFiles(pyblish.api.ContextPlugin): staging_dir_persistent = instance.data.get( "stagingDir_persistent", False ) + if staging_dir_persistent: + any_staging_dir_persistent = True + representations = [] for repre_data in instance_data.get("representations") or []: self._fill_staging_dir(repre_data, anatomy) @@ -127,7 +142,7 @@ class CollectRenderedFiles(pyblish.api.ContextPlugin): self.log.debug( f"Adding audio to instance: {instance.data['audio']}") - return staging_dir_persistent + return any_staging_dir_persistent def process(self, context): self._context = context From fd30a0426cb2a10b93384e8a6ce898e57b7d5114 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 31 Oct 2023 10:49:58 +0000 Subject: [PATCH 0637/1233] Separate blendscene loader from blend loader --- .../hosts/blender/plugins/load/load_blend.py | 2 +- .../blender/plugins/load/load_blendscene.py | 215 ++++++++++++++++++ 2 files changed, 216 insertions(+), 1 deletion(-) create mode 100644 openpype/hosts/blender/plugins/load/load_blendscene.py diff --git a/openpype/hosts/blender/plugins/load/load_blend.py b/openpype/hosts/blender/plugins/load/load_blend.py index 25d6568889..0719c5c97d 100644 --- a/openpype/hosts/blender/plugins/load/load_blend.py +++ b/openpype/hosts/blender/plugins/load/load_blend.py @@ -20,7 +20,7 @@ from openpype.hosts.blender.api.pipeline import ( class BlendLoader(plugin.AssetLoader): """Load assets from a .blend file.""" - families = ["model", "rig", "layout", "camera", "blendScene"] + families = ["model", "rig", "layout", "camera"] representations = ["blend"] label = "Append Blend" diff --git a/openpype/hosts/blender/plugins/load/load_blendscene.py b/openpype/hosts/blender/plugins/load/load_blendscene.py new file mode 100644 index 0000000000..8c43f40bdd --- /dev/null +++ b/openpype/hosts/blender/plugins/load/load_blendscene.py @@ -0,0 +1,215 @@ +from typing import Dict, List, Optional +from pathlib import Path + +import bpy + +from openpype.pipeline import ( + get_representation_path, + AVALON_CONTAINER_ID, +) +from openpype.pipeline.create import get_legacy_creator_by_name +from openpype.hosts.blender.api import plugin +from openpype.hosts.blender.api.lib import imprint +from openpype.hosts.blender.api.pipeline import ( + AVALON_CONTAINERS, + AVALON_PROPERTY, +) + + +class BlendSceneLoader(plugin.AssetLoader): + """Load assets from a .blend file.""" + + families = ["blendScene"] + representations = ["blend"] + + label = "Append Blend" + icon = "code-fork" + color = "orange" + + @staticmethod + def _get_asset_container(collections): + for coll in collections: + parents = [c for c in collections if c.user_of_id(coll)] + if coll.get(AVALON_PROPERTY) and not parents: + return coll + + return None + + def _process_data(self, libpath, group_name, family): + # Append all the data from the .blend file + with bpy.data.libraries.load( + libpath, link=False, relative=False + ) as (data_from, data_to): + for attr in dir(data_to): + setattr(data_to, attr, getattr(data_from, attr)) + + members = [] + + # Rename the object to add the asset name + for attr in dir(data_to): + for data in getattr(data_to, attr): + data.name = f"{group_name}:{data.name}" + members.append(data) + + container = self._get_asset_container( + data_to.collections) + assert container, "No asset group found" + + container.name = group_name + + # Link the group to the scene + bpy.context.scene.collection.children.link(container) + + # Remove the library from the blend file + library = bpy.data.libraries.get(bpy.path.basename(libpath)) + bpy.data.libraries.remove(library) + + return container, members + + def process_asset( + self, context: dict, name: str, namespace: Optional[str] = None, + options: Optional[Dict] = None + ) -> Optional[List]: + """ + Arguments: + name: Use pre-defined name + namespace: Use pre-defined namespace + context: Full parenthood of representation to load + options: Additional settings dictionary + """ + libpath = self.filepath_from_context(context) + asset = context["asset"]["name"] + subset = context["subset"]["name"] + + try: + family = context["representation"]["context"]["family"] + except ValueError: + family = "model" + + asset_name = plugin.asset_name(asset, subset) + unique_number = plugin.get_unique_number(asset, subset) + group_name = plugin.asset_name(asset, subset, unique_number) + namespace = namespace or f"{asset}_{unique_number}" + + avalon_container = bpy.data.collections.get(AVALON_CONTAINERS) + if not avalon_container: + avalon_container = bpy.data.collections.new(name=AVALON_CONTAINERS) + bpy.context.scene.collection.children.link(avalon_container) + + container, members = self._process_data(libpath, group_name, family) + + avalon_container.children.link(container) + + data = { + "schema": "openpype:container-2.0", + "id": AVALON_CONTAINER_ID, + "name": name, + "namespace": namespace or '', + "loader": str(self.__class__.__name__), + "representation": str(context["representation"]["_id"]), + "libpath": libpath, + "asset_name": asset_name, + "parent": str(context["representation"]["parent"]), + "family": context["representation"]["context"]["family"], + "objectName": group_name, + "members": members, + } + + container[AVALON_PROPERTY] = data + + objects = [ + obj for obj in bpy.data.objects + if obj.name.startswith(f"{group_name}:") + ] + + self[:] = objects + return objects + + def exec_update(self, container: Dict, representation: Dict): + """ + Update the loaded asset. + """ + group_name = container["objectName"] + asset_group = bpy.data.collections.get(group_name) + libpath = Path(get_representation_path(representation)).as_posix() + + assert asset_group, ( + f"The asset is not loaded: {container['objectName']}" + ) + + collection_parents = {} + members = asset_group.get(AVALON_PROPERTY).get("members", []) + loaded_collections = {c for c in bpy.data.collections if c in members} + loaded_collections.add(bpy.data.collections.get(AVALON_CONTAINERS)) + for member in members: + if isinstance(member, bpy.types.Object): + member_parents = set(member.users_collection) + elif isinstance(member, bpy.types.Collection): + member_parents = { + c for c in bpy.data.collections if c.user_of_id(member)} + else: + continue + + member_parents = member_parents.difference(loaded_collections) + if member_parents: + collection_parents[member.name] = list(member_parents) + + old_data = dict(asset_group.get(AVALON_PROPERTY)) + + self.exec_remove(container) + + family = container["family"] + asset_group, members = self._process_data(libpath, group_name, family) + + for member in members: + if member.name in collection_parents: + for parent in collection_parents[member.name]: + if isinstance(member, bpy.types.Object): + parent.objects.link(member) + elif isinstance(member, bpy.types.Collection): + parent.children.link(member) + + avalon_container = bpy.data.collections.get(AVALON_CONTAINERS) + avalon_container.children.link(asset_group) + + # Restore the old data, but reset memebers, as they don't exist anymore + # This avoids a crash, because the memory addresses of those members + # are not valid anymore + old_data["members"] = [] + asset_group[AVALON_PROPERTY] = old_data + + new_data = { + "libpath": libpath, + "representation": str(representation["_id"]), + "parent": str(representation["parent"]), + "members": members, + } + + imprint(asset_group, new_data) + + def exec_remove(self, container: Dict) -> bool: + """ + Remove an existing container from a Blender scene. + """ + group_name = container["objectName"] + asset_group = bpy.data.collections.get(group_name) + + attrs = [ + attr for attr in dir(bpy.data) + if isinstance( + getattr(bpy.data, attr), + bpy.types.bpy_prop_collection + ) + ] + + members = asset_group.get(AVALON_PROPERTY).get("members", []) + + for attr in attrs: + for data in getattr(bpy.data, attr): + if data in members: + # Skip the asset group + if data == asset_group: + continue + getattr(bpy.data, attr).remove(data) + + bpy.data.collections.remove(asset_group) From 6ec87aa06df1dbb7f9ae28fae286e61e73ba3355 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 31 Oct 2023 11:10:20 +0000 Subject: [PATCH 0638/1233] Hound fixes --- openpype/hosts/blender/plugins/load/load_blendscene.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/blender/plugins/load/load_blendscene.py b/openpype/hosts/blender/plugins/load/load_blendscene.py index 8c43f40bdd..fe7afb3119 100644 --- a/openpype/hosts/blender/plugins/load/load_blendscene.py +++ b/openpype/hosts/blender/plugins/load/load_blendscene.py @@ -7,7 +7,6 @@ from openpype.pipeline import ( get_representation_path, AVALON_CONTAINER_ID, ) -from openpype.pipeline.create import get_legacy_creator_by_name from openpype.hosts.blender.api import plugin from openpype.hosts.blender.api.lib import imprint from openpype.hosts.blender.api.pipeline import ( From e39689ba8e1a7a9de754ecf5becb487d5fda7665 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 31 Oct 2023 20:28:15 +0800 Subject: [PATCH 0639/1233] add docs --- website/docs/artist_hosts_3dsmax.md | 24 ++++++++++++++++++ .../assets/3dsmax_validate_attributes.png | Bin 0 -> 35154 bytes 2 files changed, 24 insertions(+) create mode 100644 website/docs/assets/3dsmax_validate_attributes.png diff --git a/website/docs/artist_hosts_3dsmax.md b/website/docs/artist_hosts_3dsmax.md index fffab8ca5d..bc79094746 100644 --- a/website/docs/artist_hosts_3dsmax.md +++ b/website/docs/artist_hosts_3dsmax.md @@ -118,4 +118,28 @@ Current OpenPype integration (ver 3.15.0) supports only ```PointCache```, ```Ca This part of documentation is still work in progress. ::: +## Validators + +Current Openpype integration supports different validators such as Frame Range and Attributes. +Some validators are mandatory while some are optional and user can choose to enable them in the setting. + +**Validate Frame Range**: Optional Validator for checking Frame Range + +**Validate Attributes**: Optional Validator for checking if object properties' attributes are valid + in MaxWrapper Class. +:::note + Users can write the properties' attributes they want to check in dict format in the setting + before validation. + E.g. ```renderers.current.separateAovFiles``` and ```renderers.current.PrimaryGIEngine``` + User can put the attributes in the dict format below + ``` + { + "renderer.current":{ + "separateAovFiles" : True + "PrimaryGIEngine": "#RS_GIENGINE_BRUTE_FORCE" + } + } + ``` + ![Validate Attribute Setting](assets/3dsmax_validate_attributes.png) +::: ## ...to be added diff --git a/website/docs/assets/3dsmax_validate_attributes.png b/website/docs/assets/3dsmax_validate_attributes.png new file mode 100644 index 0000000000000000000000000000000000000000..5af82361887f47fda96607886ec10e15eb355df1 GIT binary patch literal 35154 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 FpM9ou89oV;b5Eca*vLo#<6{~%iTY)O)Z@( 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 sPpdcV0Zhn($&0bNe4%m4rY literal 0 HcmV?d00001 From 1795d501d995f037af9ad81d8731f14b67a7f19e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 31 Oct 2023 20:29:34 +0800 Subject: [PATCH 0640/1233] add docs --- website/docs/artist_hosts_3dsmax.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/artist_hosts_3dsmax.md b/website/docs/artist_hosts_3dsmax.md index bc79094746..6f23a19103 100644 --- a/website/docs/artist_hosts_3dsmax.md +++ b/website/docs/artist_hosts_3dsmax.md @@ -129,7 +129,7 @@ Some validators are mandatory while some are optional and user can choose to ena in MaxWrapper Class. :::note Users can write the properties' attributes they want to check in dict format in the setting - before validation. + before validation. The attributes are then to be converted into Maxscript and do a check. E.g. ```renderers.current.separateAovFiles``` and ```renderers.current.PrimaryGIEngine``` User can put the attributes in the dict format below ``` From cfd9f0f06c26c0d47340f9baf42239674e2cebc8 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 31 Oct 2023 20:32:28 +0800 Subject: [PATCH 0641/1233] edit docstring --- openpype/hosts/max/plugins/publish/validate_attributes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_attributes.py b/openpype/hosts/max/plugins/publish/validate_attributes.py index 0cd405aebd..0632ee38f0 100644 --- a/openpype/hosts/max/plugins/publish/validate_attributes.py +++ b/openpype/hosts/max/plugins/publish/validate_attributes.py @@ -40,11 +40,11 @@ class ValidateAttributes(OptionalPyblishPluginMixin, with the nodes from MaxWrapper Class in 3ds max. E.g. "renderers.current.separateAovFiles", "renderers.production.PrimaryGIEngine" - Admin(s) need to put json below and enable this validator for a check: + Admin(s) need to put the dict below and enable this validator for a check: { "renderers.current":{ "separateAovFiles" : True - } + }, "renderers.production":{ "PrimaryGIEngine": "#RS_GIENGINE_BRUTE_FORCE" } From 00fb722089a80dae83fe89b387ddcc481053f053 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 31 Oct 2023 13:55:22 +0100 Subject: [PATCH 0642/1233] use AYON username for user in template data --- openpype/lib/local_settings.py | 6 ++++++ openpype/plugins/publish/collect_current_pype_user.py | 8 +++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/openpype/lib/local_settings.py b/openpype/lib/local_settings.py index dae6e074af..9b780fd88a 100644 --- a/openpype/lib/local_settings.py +++ b/openpype/lib/local_settings.py @@ -611,6 +611,12 @@ def get_openpype_username(): settings and last option is to use `getpass.getuser()` which returns machine username. """ + + if AYON_SERVER_ENABLED: + import ayon_api + + return ayon_api.get_user()["name"] + username = os.environ.get("OPENPYPE_USERNAME") if not username: local_settings = get_local_settings() diff --git a/openpype/plugins/publish/collect_current_pype_user.py b/openpype/plugins/publish/collect_current_pype_user.py index 2d507ba292..5c0c4fc82e 100644 --- a/openpype/plugins/publish/collect_current_pype_user.py +++ b/openpype/plugins/publish/collect_current_pype_user.py @@ -1,4 +1,6 @@ import pyblish.api + +from openpype import AYON_SERVER_ENABLED from openpype.lib import get_openpype_username @@ -7,7 +9,11 @@ class CollectCurrentUserPype(pyblish.api.ContextPlugin): # Order must be after default pyblish-base CollectCurrentUser order = pyblish.api.CollectorOrder + 0.001 - label = "Collect Pype User" + label = ( + "Collect AYON User" + if AYON_SERVER_ENABLED + else "Collect OpenPype User" + ) def process(self, context): user = get_openpype_username() From 98f91ce932c4b544f0cb6d54f6010f8d99d493c4 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 31 Oct 2023 15:37:15 +0100 Subject: [PATCH 0643/1233] Fix typo `actions_dir` -> `path` to fix register launcher actions from OpenPypeModule --- openpype/modules/launcher_action.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/launcher_action.py b/openpype/modules/launcher_action.py index 5e14f25f76..4f0674c94f 100644 --- a/openpype/modules/launcher_action.py +++ b/openpype/modules/launcher_action.py @@ -40,7 +40,7 @@ class LauncherAction(OpenPypeModule, ITrayAction): actions_paths = self.manager.collect_plugin_paths()["actions"] for path in actions_paths: if path and os.path.exists(path): - register_launcher_action_path(actions_dir) + register_launcher_action_path(path) paths_str = os.environ.get("AVALON_ACTIONS") or "" if paths_str: From 711976e68586a6110b04a0b7d650effca08dcd30 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Tue, 31 Oct 2023 18:05:20 +0200 Subject: [PATCH 0644/1233] fix bug when loading shelf files --- openpype/hosts/houdini/api/shelves.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/api/shelves.py b/openpype/hosts/houdini/api/shelves.py index 4b5ebd4202..0afc737665 100644 --- a/openpype/hosts/houdini/api/shelves.py +++ b/openpype/hosts/houdini/api/shelves.py @@ -44,7 +44,7 @@ def generate_shelves(): "{}".format(shelf_set_os_filepath)) continue - hou.shelves.newShelfSet(file_path=shelf_set_os_filepath) + hou.shelves.loadFile(shelf_set_os_filepath) continue shelf_set_name = shelf_set_config.get('shelf_set_name') From dd070c6fcc0ded177fefebcf64f5272bb4d007d3 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Tue, 31 Oct 2023 18:06:06 +0200 Subject: [PATCH 0645/1233] Enhance shelves settings visual appeal --- .../schemas/schema_houdini_scriptshelf.json | 24 ++++++++++++------- .../houdini/server/settings/shelves.py | 14 +++++++---- server_addon/houdini/server/version.py | 2 +- 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_scriptshelf.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_scriptshelf.json index 35d768843d..f45377c8b4 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_scriptshelf.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_scriptshelf.json @@ -7,22 +7,30 @@ "object_type": { "type": "dict", "children": [ + { + "type": "label", + "label": "Option 1: Add a .shelf file" + }, + { + "type": "path", + "key": "shelf_set_source_path", + "label": "Shelf Set Path", + "multipath": false, + "multiplatform": true + }, + { + "type": "label", + "label": "OR Option 2: Add Shelf Set Name and Shelves Definitions" + }, { "type": "text", "key": "shelf_set_name", "label": "Shelf Set Name" }, - { - "type": "path", - "key": "shelf_set_source_path", - "label": "Shelf Set Path (optional)", - "multipath": false, - "multiplatform": true - }, { "type": "list", "key": "shelf_definition", - "label": "Shelves", + "label": "Shelves Definitions", "use_label_wrap": true, "object_type": { "type": "dict", diff --git a/server_addon/houdini/server/settings/shelves.py b/server_addon/houdini/server/settings/shelves.py index 8d0512bdeb..e02ddf1c34 100644 --- a/server_addon/houdini/server/settings/shelves.py +++ b/server_addon/houdini/server/settings/shelves.py @@ -24,14 +24,18 @@ class ShelfDefinitionModel(BaseSettingsModel): class ShelvesModel(BaseSettingsModel): _layout = "expanded" - shelf_set_name: str = Field("", title="Shelfs set name") - shelf_set_source_path: MultiplatformPathModel = Field( default_factory=MultiplatformPathModel, - title="Shelf Set Path (optional)" + title="Shelf Set Path", + section="Option 1: Add a .shelf file." + ) + shelf_set_name: str = Field( + "", + title="Shelf Set Name", + section=("OR Option 2: Add Shelf Set Name " + "and Shelves Definitions.") ) - shelf_definition: list[ShelfDefinitionModel] = Field( default_factory=list, - title="Shelf Definitions" + title="Shelves Definitions" ) diff --git a/server_addon/houdini/server/version.py b/server_addon/houdini/server/version.py index 01ef12070d..6cd38b7465 100644 --- a/server_addon/houdini/server/version.py +++ b/server_addon/houdini/server/version.py @@ -1 +1 @@ -__version__ = "0.2.6" +__version__ = "0.2.7" From 918770f817e5ab1a1c930bae81a441609b4d5ce2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 31 Oct 2023 18:57:28 +0100 Subject: [PATCH 0646/1233] 'get_current_context_template_data' returns same values as base function 'get_template_data' --- openpype/pipeline/context_tools.py | 93 +++++++----------------------- 1 file changed, 22 insertions(+), 71 deletions(-) diff --git a/openpype/pipeline/context_tools.py b/openpype/pipeline/context_tools.py index 13630ae7ca..5afdb30f7b 100644 --- a/openpype/pipeline/context_tools.py +++ b/openpype/pipeline/context_tools.py @@ -25,10 +25,7 @@ from openpype.tests.lib import is_in_tests from .publish.lib import filter_pyblish_plugins from .anatomy import Anatomy -from .template_data import ( - get_template_data_with_names, - get_template_data -) +from .template_data import get_template_data_with_names from .workfile import ( get_workfile_template_key, get_custom_workfile_template_by_string_context, @@ -483,6 +480,27 @@ def get_template_data_from_session(session=None, system_settings=None): ) +def get_current_context_template_data(system_settings=None): + """Prepare template data for current context. + + Args: + system_settings (Optional[Dict[str, Any]]): Prepared system settings. + + Returns: + Dict[str, Any] Template data for current context. + """ + + context = get_current_context() + project_name = context["project_name"] + asset_name = context["asset_name"] + task_name = context["task_name"] + host_name = get_current_host_name() + + return get_template_data_with_names( + project_name, asset_name, task_name, host_name, system_settings + ) + + def get_workdir_from_session(session=None, template_key=None): """Template data for template fill from session keys. @@ -661,70 +679,3 @@ def get_process_id(): if _process_id is None: _process_id = str(uuid.uuid4()) return _process_id - - -def get_current_context_template_data(): - """Template data for template fill from current context - - Returns: - Dict[str, Any] of the following tokens and their values - Supported Tokens: - - Regular Tokens - - app - - user - - asset - - parent - - hierarchy - - folder[name] - - root[work, ...] - - studio[code, name] - - project[code, name] - - task[type, name, short] - - - Context Specific Tokens - - assetData[frameStart] - - assetData[frameEnd] - - assetData[handleStart] - - assetData[handleEnd] - - assetData[frameStartHandle] - - assetData[frameEndHandle] - - assetData[resolutionHeight] - - assetData[resolutionWidth] - - """ - - # pre-prepare get_template_data args - current_context = get_current_context() - project_name = current_context["project_name"] - asset_name = current_context["asset_name"] - anatomy = Anatomy(project_name) - - # prepare get_template_data args - project_doc = get_project(project_name) - asset_doc = get_asset_by_name(project_name, asset_name) - task_name = current_context["task_name"] - host_name = get_current_host_name() - - # get regular template data - template_data = get_template_data( - project_doc, asset_doc, task_name, host_name - ) - - template_data["root"] = anatomy.roots - - # get context specific vars - asset_data = asset_doc["data"].copy() - - # compute `frameStartHandle` and `frameEndHandle` - if "frameStart" in asset_data and "handleStart" in asset_data: - asset_data["frameStartHandle"] = \ - asset_data["frameStart"] - asset_data["handleStart"] - - if "frameEnd" in asset_data and "handleEnd" in asset_data: - asset_data["frameEndHandle"] = \ - asset_data["frameEnd"] + asset_data["handleEnd"] - - # add assetData - template_data["assetData"] = asset_data - - return template_data From 4a11eed09ba8936351ce8acf1fa06fdd0ef904fb Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 31 Oct 2023 18:59:06 +0100 Subject: [PATCH 0647/1233] implemented new function which does what houdini requires --- openpype/hosts/houdini/api/lib.py | 56 +++++++++++++++++++++++---- openpype/hosts/houdini/api/shelves.py | 5 ++- 2 files changed, 51 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index cadeaa8ed4..ac375c56d6 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -11,20 +11,21 @@ import json import six from openpype.lib import StringTemplate -from openpype.client import get_asset_by_name +from openpype.client import get_project, get_asset_by_name from openpype.settings import get_current_project_settings from openpype.pipeline import ( + Anatomy, get_current_project_name, get_current_asset_name, - registered_host -) -from openpype.pipeline.context_tools import ( - get_current_context_template_data, - get_current_project_asset + registered_host, + get_current_context, + get_current_host_name, ) +from openpype.pipeline.create import CreateContext +from openpype.pipeline.template_data import get_template_data +from openpype.pipeline.context_tools import get_current_project_asset from openpype.widgets import popup from openpype.tools.utils.host_tools import get_tool_by_name -from openpype.pipeline.create import CreateContext import hou @@ -804,6 +805,45 @@ def get_camera_from_container(container): return cameras[0] +def get_current_context_template_data_with_asset_data(): + """ + TODOs: + Support both 'assetData' and 'folderData' in future. + """ + + context = get_current_context() + project_name = context["project_name"] + asset_name = context["asset_name"] + task_name = context["task_name"] + host_name = get_current_host_name() + + anatomy = Anatomy(project_name) + project_doc = get_project(project_name) + asset_doc = get_asset_by_name(project_name, asset_name) + + # get context specific vars + asset_data = asset_doc["data"] + + # compute `frameStartHandle` and `frameEndHandle` + frame_start = asset_data.get("frameStart") + frame_end = asset_data.get("frameEnd") + handle_start = asset_data.get("handleStart") + handle_end = asset_data.get("handleEnd") + if frame_start is not None and handle_start is not None: + asset_data["frameStartHandle"] = frame_start - handle_start + + if frame_end is not None and handle_end is not None: + asset_data["frameEndHandle"] = frame_end + handle_end + + template_data = get_template_data( + project_doc, asset_doc, task_name, host_name + ) + template_data["root"] = anatomy.roots + template_data["assetData"] = asset_data + + return template_data + + def get_context_var_changes(): """get context var changes.""" @@ -823,7 +863,7 @@ def get_context_var_changes(): return houdini_vars_to_update # Get Template data - template_data = get_current_context_template_data() + template_data = get_current_context_template_data_with_asset_data() # Set Houdini Vars for item in houdini_vars: diff --git a/openpype/hosts/houdini/api/shelves.py b/openpype/hosts/houdini/api/shelves.py index 4b5ebd4202..5df45a1f72 100644 --- a/openpype/hosts/houdini/api/shelves.py +++ b/openpype/hosts/houdini/api/shelves.py @@ -7,10 +7,11 @@ from openpype.settings import get_project_settings from openpype.pipeline import get_current_project_name from openpype.lib import StringTemplate -from openpype.pipeline.context_tools import get_current_context_template_data import hou +from .lib import get_current_context_template_data_with_asset_data + log = logging.getLogger("openpype.hosts.houdini.shelves") @@ -30,7 +31,7 @@ def generate_shelves(): return # Get Template data - template_data = get_current_context_template_data() + template_data = get_current_context_template_data_with_asset_data() for shelf_set_config in shelves_set_config: shelf_set_filepath = shelf_set_config.get('shelf_set_source_path') From 9e85cc17a989fa88d4df8a5a8644ccb30470270f Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 1 Nov 2023 03:25:26 +0000 Subject: [PATCH 0648/1233] [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 d6839c9b70..4865fcfb31 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.17.5-nightly.1" +__version__ = "3.17.5-nightly.2" From 15414809828905c24f89ce67547b101817d1309d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 1 Nov 2023 03:26:11 +0000 Subject: [PATCH 0649/1233] 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 73505368dd..249da3da0e 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.17.5-nightly.2 - 3.17.5-nightly.1 - 3.17.4 - 3.17.4-nightly.2 @@ -134,7 +135,6 @@ body: - 3.15.1-nightly.4 - 3.15.1-nightly.3 - 3.15.1-nightly.2 - - 3.15.1-nightly.1 validations: required: true - type: dropdown From f330f87993d6826b519c7ee949c8c1a2c21e197b Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 1 Nov 2023 10:37:41 +0200 Subject: [PATCH 0650/1233] Add options to Houdini shelves manager settings --- .../schemas/schema_houdini_scriptshelf.json | 141 +++++++++--------- .../houdini/server/settings/shelves.py | 11 ++ 2 files changed, 85 insertions(+), 67 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_scriptshelf.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_scriptshelf.json index f45377c8b4..2dfce906b7 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_scriptshelf.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_scriptshelf.json @@ -5,78 +5,85 @@ "is_group": true, "use_label_wrap": true, "object_type": { - "type": "dict", - "children": [ + "type": "dict-conditional", + "enum_key": "options", + "enum_label": "Options", + "enum_children": [ { - "type": "label", - "label": "Option 1: Add a .shelf file" + + "key": "add_shelf_file", + "label": "Add a .shelf file", + "children": [ + { + "type": "path", + "key": "shelf_set_source_path", + "label": "Shelf Set Path", + "multipath": false, + "multiplatform": true + } + ] }, { - "type": "path", - "key": "shelf_set_source_path", - "label": "Shelf Set Path", - "multipath": false, - "multiplatform": true - }, - { - "type": "label", - "label": "OR Option 2: Add Shelf Set Name and Shelves Definitions" - }, - { - "type": "text", - "key": "shelf_set_name", - "label": "Shelf Set Name" - }, - { - "type": "list", - "key": "shelf_definition", - "label": "Shelves Definitions", - "use_label_wrap": true, - "object_type": { - "type": "dict", - "children": [ - { - "type": "text", - "key": "shelf_name", - "label": "Shelf Name" - }, - { - "type": "list", - "key": "tools_list", - "label": "Tools", - "use_label_wrap": true, - "object_type": { - "type": "dict", - "children": [ - { - "type": "label", - "label": "Name and Script Path are mandatory." - }, - { - "type": "text", - "key": "label", - "label": "Name" - }, - { - "type": "path", - "key": "script", - "label": "Script" - }, - { - "type": "path", - "key": "icon", - "label": "Icon" - }, - { - "type": "text", - "key": "help", - "label": "Help" + "key": "add_set_and_definitions", + "label": "Add Shelf Set Name and Shelves Definitions", + "children": [ + { + "type": "text", + "key": "shelf_set_name", + "label": "Shelf Set Name" + }, + { + "type": "list", + "key": "shelf_definition", + "label": "Shelves Definitions", + "use_label_wrap": true, + "object_type": { + "type": "dict", + "children": [ + { + "type": "text", + "key": "shelf_name", + "label": "Shelf Name" + }, + { + "type": "list", + "key": "tools_list", + "label": "Tools", + "use_label_wrap": true, + "object_type": { + "type": "dict", + "children": [ + { + "type": "label", + "label": "Name and Script Path are mandatory." + }, + { + "type": "text", + "key": "label", + "label": "Name" + }, + { + "type": "path", + "key": "script", + "label": "Script" + }, + { + "type": "path", + "key": "icon", + "label": "Icon" + }, + { + "type": "text", + "key": "help", + "label": "Help" + } + ] } - ] - } + } + ] } - ] - } + } + ] } ] } diff --git a/server_addon/houdini/server/settings/shelves.py b/server_addon/houdini/server/settings/shelves.py index e02ddf1c34..651af27537 100644 --- a/server_addon/houdini/server/settings/shelves.py +++ b/server_addon/houdini/server/settings/shelves.py @@ -21,9 +21,20 @@ class ShelfDefinitionModel(BaseSettingsModel): title="Shelf Tools" ) +def shelves_enum_options(): + return [ + {"value": "add_shelf_file", "label": "Add a .shelf file"}, + {"value": "add_set_and_definitions", "label": "Add Shelf Set Name and Shelves Definitions"} + ] class ShelvesModel(BaseSettingsModel): _layout = "expanded" + options: str = Field( + title="Options", + description="Switch between shelves manager options", + enum_resolver=shelves_enum_options, + conditionalEnum=True + ) shelf_set_source_path: MultiplatformPathModel = Field( default_factory=MultiplatformPathModel, title="Shelf Set Path", From 0e331db93a0de805a55e88711d5d880f6281715e Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 1 Nov 2023 11:24:48 +0200 Subject: [PATCH 0651/1233] Adjust Houdini Shelves Ayon settings --- .../houdini/server/settings/shelves.py | 47 ++++++++++++------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/server_addon/houdini/server/settings/shelves.py b/server_addon/houdini/server/settings/shelves.py index 651af27537..a0acc90505 100644 --- a/server_addon/houdini/server/settings/shelves.py +++ b/server_addon/houdini/server/settings/shelves.py @@ -21,32 +21,47 @@ class ShelfDefinitionModel(BaseSettingsModel): title="Shelf Tools" ) + +class AddShelfFileModel(BaseSettingsModel): + shelf_set_source_path: MultiplatformPathModel = Field( + default_factory=MultiplatformPathModel, + title="Shelf Set Path" + ) + + +class AddSetAndDefinitionsModel(BaseSettingsModel): + shelf_set_name: str = Field(title="Shelf Set Name") + shelf_definition: list[ShelfDefinitionModel] = Field( + default_factory=list, + title="Shelves Definitions" + ) + + def shelves_enum_options(): return [ - {"value": "add_shelf_file", "label": "Add a .shelf file"}, - {"value": "add_set_and_definitions", "label": "Add Shelf Set Name and Shelves Definitions"} + { + "value": "add_shelf_file", + "label": "Add a .shelf file" + }, + { + "value": "add_set_and_definitions", + "label": "Add Shelf Set Name and Shelves Definitions" + } ] + class ShelvesModel(BaseSettingsModel): - _layout = "expanded" options: str = Field( title="Options", description="Switch between shelves manager options", enum_resolver=shelves_enum_options, conditionalEnum=True ) - shelf_set_source_path: MultiplatformPathModel = Field( - default_factory=MultiplatformPathModel, - title="Shelf Set Path", - section="Option 1: Add a .shelf file." + add_shelf_file: AddShelfFileModel = Field( + title="Add a .shelf file", + default_factory=AddShelfFileModel ) - shelf_set_name: str = Field( - "", - title="Shelf Set Name", - section=("OR Option 2: Add Shelf Set Name " - "and Shelves Definitions.") - ) - shelf_definition: list[ShelfDefinitionModel] = Field( - default_factory=list, - title="Shelves Definitions" + add_set_and_definitions: AddSetAndDefinitionsModel = Field( + title="Add Shelf Set Name and Shelves Definitions", + default_factory=AddSetAndDefinitionsModel ) From 381c00c3342193442e46b3e2475db64dda129beb Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 1 Nov 2023 12:29:41 +0200 Subject: [PATCH 0652/1233] Align Openpype and Ayon settings, quick fix in shelves loader script --- openpype/hosts/houdini/api/shelves.py | 34 +++-- .../schemas/schema_houdini_scriptshelf.json | 128 ++++++++++-------- .../houdini/server/settings/shelves.py | 2 +- 3 files changed, 91 insertions(+), 73 deletions(-) diff --git a/openpype/hosts/houdini/api/shelves.py b/openpype/hosts/houdini/api/shelves.py index 0afc737665..6fb3967be8 100644 --- a/openpype/hosts/houdini/api/shelves.py +++ b/openpype/hosts/houdini/api/shelves.py @@ -23,29 +23,33 @@ def generate_shelves(): # load configuration of houdini shelves project_name = get_current_project_name() project_settings = get_project_settings(project_name) - shelves_set_config = project_settings["houdini"]["shelves"] + shelves_configs = project_settings["houdini"]["shelves"] - if not shelves_set_config: + if not shelves_configs: log.debug("No custom shelves found in project settings.") return # Get Template data template_data = get_current_context_template_data() - for shelf_set_config in shelves_set_config: - shelf_set_filepath = shelf_set_config.get('shelf_set_source_path') - shelf_set_os_filepath = shelf_set_filepath[current_os] - if shelf_set_os_filepath: - shelf_set_os_filepath = get_path_using_template_data( - shelf_set_os_filepath, template_data - ) - if not os.path.isfile(shelf_set_os_filepath): - log.error("Shelf path doesn't exist - " - "{}".format(shelf_set_os_filepath)) - continue + for config in shelves_configs: + selected_option = config["options"] + shelf_set_config = config[selected_option] - hou.shelves.loadFile(shelf_set_os_filepath) - continue + shelf_set_filepath = shelf_set_config.get('shelf_set_source_path') + if shelf_set_filepath: + shelf_set_os_filepath = shelf_set_filepath[current_os] + if shelf_set_os_filepath: + shelf_set_os_filepath = get_path_using_template_data( + shelf_set_os_filepath, template_data + ) + if not os.path.isfile(shelf_set_os_filepath): + log.error("Shelf path doesn't exist - " + "{}".format(shelf_set_os_filepath)) + continue + + hou.shelves.loadFile(shelf_set_os_filepath) + continue shelf_set_name = shelf_set_config.get('shelf_set_name') if not shelf_set_name: diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_scriptshelf.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_scriptshelf.json index 2dfce906b7..cee04b73e5 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_scriptshelf.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_scriptshelf.json @@ -15,11 +15,18 @@ "label": "Add a .shelf file", "children": [ { - "type": "path", - "key": "shelf_set_source_path", - "label": "Shelf Set Path", - "multipath": false, - "multiplatform": true + "type": "dict", + "key": "add_shelf_file", + "label": "Add a .shelf file", + "children": [ + { + "type": "path", + "key": "shelf_set_source_path", + "label": "Shelf Set Path", + "multipath": false, + "multiplatform": true + } + ] } ] }, @@ -28,60 +35,67 @@ "label": "Add Shelf Set Name and Shelves Definitions", "children": [ { - "type": "text", - "key": "shelf_set_name", - "label": "Shelf Set Name" - }, - { - "type": "list", - "key": "shelf_definition", - "label": "Shelves Definitions", - "use_label_wrap": true, - "object_type": { - "type": "dict", - "children": [ - { - "type": "text", - "key": "shelf_name", - "label": "Shelf Name" - }, - { - "type": "list", - "key": "tools_list", - "label": "Tools", - "use_label_wrap": true, - "object_type": { - "type": "dict", - "children": [ - { - "type": "label", - "label": "Name and Script Path are mandatory." - }, - { - "type": "text", - "key": "label", - "label": "Name" - }, - { - "type": "path", - "key": "script", - "label": "Script" - }, - { - "type": "path", - "key": "icon", - "label": "Icon" - }, - { - "type": "text", - "key": "help", - "label": "Help" + "key": "add_set_and_definitions", + "label": "Add Shelf Set Name and Shelves Definitions", + "type": "dict", + "children": [ + { + "type": "text", + "key": "shelf_set_name", + "label": "Shelf Set Name" + }, + { + "type": "list", + "key": "shelf_definition", + "label": "Shelves Definitions", + "use_label_wrap": true, + "object_type": { + "type": "dict", + "children": [ + { + "type": "text", + "key": "shelf_name", + "label": "Shelf Name" + }, + { + "type": "list", + "key": "tools_list", + "label": "Tools", + "use_label_wrap": true, + "object_type": { + "type": "dict", + "children": [ + { + "type": "label", + "label": "Name and Script Path are mandatory." + }, + { + "type": "text", + "key": "label", + "label": "Name" + }, + { + "type": "path", + "key": "script", + "label": "Script" + }, + { + "type": "path", + "key": "icon", + "label": "Icon" + }, + { + "type": "text", + "key": "help", + "label": "Help" + } + ] } - ] - } + } + ] } - ] - } + } + ] } ] } diff --git a/server_addon/houdini/server/settings/shelves.py b/server_addon/houdini/server/settings/shelves.py index a0acc90505..133c18f77c 100644 --- a/server_addon/houdini/server/settings/shelves.py +++ b/server_addon/houdini/server/settings/shelves.py @@ -30,7 +30,7 @@ class AddShelfFileModel(BaseSettingsModel): class AddSetAndDefinitionsModel(BaseSettingsModel): - shelf_set_name: str = Field(title="Shelf Set Name") + shelf_set_name: str = Field("", title="Shelf Set Name") shelf_definition: list[ShelfDefinitionModel] = Field( default_factory=list, title="Shelves Definitions" From c185d2cab8b9cb1a21d17c26acfeb8180eedcb15 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 1 Nov 2023 12:35:41 +0200 Subject: [PATCH 0653/1233] resolve hound --- openpype/hosts/houdini/api/shelves.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/api/shelves.py b/openpype/hosts/houdini/api/shelves.py index 8b27ecd67e..5093a90988 100644 --- a/openpype/hosts/houdini/api/shelves.py +++ b/openpype/hosts/houdini/api/shelves.py @@ -46,7 +46,7 @@ def generate_shelves(): ) if not os.path.isfile(shelf_set_os_filepath): log.error("Shelf path doesn't exist - " - "{}".format(shelf_set_os_filepath)) + "{}".format(shelf_set_os_filepath)) continue hou.shelves.loadFile(shelf_set_os_filepath) From 82f3b5e07f9d5b2bbe2a7679f11c52adf5ad51ff Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 1 Nov 2023 18:40:28 +0800 Subject: [PATCH 0654/1233] avoid using asset from context in collect render and also clean up unncessary code from the collector --- .../max/plugins/publish/collect_render.py | 19 +------------------ .../plugins/publish/collect_scene_version.py | 1 + 2 files changed, 2 insertions(+), 18 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index 7765b3b924..38194a0735 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -4,11 +4,9 @@ import os import pyblish.api from pymxs import runtime as rt -from openpype.pipeline import get_current_asset_name from openpype.hosts.max.api import colorspace from openpype.hosts.max.api.lib import get_max_version, get_current_renderer from openpype.hosts.max.api.lib_renderproducts import RenderProducts -from openpype.client import get_last_version_by_subset_name class CollectRender(pyblish.api.InstancePlugin): @@ -27,7 +25,6 @@ class CollectRender(pyblish.api.InstancePlugin): filepath = current_file.replace("\\", "/") context.data['currentFile'] = current_file - asset = get_current_asset_name() files_by_aov = RenderProducts().get_beauty(instance.name) aovs = RenderProducts().get_aovs(instance.name) @@ -49,19 +46,6 @@ class CollectRender(pyblish.api.InstancePlugin): instance.data["files"].append(files_by_aov) img_format = RenderProducts().image_format() - project_name = context.data["projectName"] - asset_doc = context.data["assetEntity"] - asset_id = asset_doc["_id"] - version_doc = get_last_version_by_subset_name(project_name, - instance.name, - asset_id) - self.log.debug("version_doc: {0}".format(version_doc)) - version_int = 1 - if version_doc: - version_int += int(version_doc["name"]) - - self.log.debug(f"Setting {version_int} to context.") - context.data["version"] = version_int # OCIO config not support in # most of the 3dsmax renderers # so this is currently hard coded @@ -87,7 +71,7 @@ class CollectRender(pyblish.api.InstancePlugin): renderer = str(renderer_class).split(":")[0] # also need to get the render dir for conversion data = { - "asset": asset, + "asset": instance.data["asset"], "subset": str(instance.name), "publish": True, "maxversion": str(get_max_version()), @@ -99,7 +83,6 @@ class CollectRender(pyblish.api.InstancePlugin): "plugin": "3dsmax", "frameStart": instance.data["frameStartHandle"], "frameEnd": instance.data["frameEndHandle"], - "version": version_int, "farm": True } instance.data.update(data) diff --git a/openpype/plugins/publish/collect_scene_version.py b/openpype/plugins/publish/collect_scene_version.py index 7920c1e82b..f870ae9ad7 100644 --- a/openpype/plugins/publish/collect_scene_version.py +++ b/openpype/plugins/publish/collect_scene_version.py @@ -24,6 +24,7 @@ class CollectSceneVersion(pyblish.api.ContextPlugin): "hiero", "houdini", "maya", + "max", "nuke", "photoshop", "resolve", From 424a0d6f2fdb9ab866aeb7bbf2c8d552e7a19a95 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 1 Nov 2023 11:14:38 +0000 Subject: [PATCH 0655/1233] Fix missing grease pencils in thumbnails and playblasts --- openpype/hosts/blender/plugins/publish/collect_review.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/collect_review.py b/openpype/hosts/blender/plugins/publish/collect_review.py index 3bf2e39e24..2760ab9811 100644 --- a/openpype/hosts/blender/plugins/publish/collect_review.py +++ b/openpype/hosts/blender/plugins/publish/collect_review.py @@ -31,11 +31,12 @@ class CollectReview(pyblish.api.InstancePlugin): focal_length = cameras[0].data.lens - # get isolate objects list from meshes instance members . + # get isolate objects list from meshes instance members. + types = {"MESH", "GPENCIL"} isolate_objects = [ obj for obj in instance - if isinstance(obj, bpy.types.Object) and obj.type == "MESH" + if isinstance(obj, bpy.types.Object) and obj.type in types ] if not instance.data.get("remove"): From e4aa43e91bdc9e8f81055d7357bfb219cdd98a68 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 1 Nov 2023 11:53:49 +0000 Subject: [PATCH 0656/1233] Fix Blender Render Settings in Ayon --- server_addon/blender/server/settings/main.py | 2 +- server_addon/blender/server/version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server_addon/blender/server/settings/main.py b/server_addon/blender/server/settings/main.py index 4476ea709b..374b2fafa2 100644 --- a/server_addon/blender/server/settings/main.py +++ b/server_addon/blender/server/settings/main.py @@ -41,7 +41,7 @@ class BlenderSettings(BaseSettingsModel): default_factory=BlenderImageIOModel, title="Color Management (ImageIO)" ) - render_settings: RenderSettingsModel = Field( + RenderSettings: RenderSettingsModel = Field( default_factory=RenderSettingsModel, title="Render Settings") workfile_builder: TemplateWorkfileBaseOptions = Field( default_factory=TemplateWorkfileBaseOptions, diff --git a/server_addon/blender/server/version.py b/server_addon/blender/server/version.py index ae7362549b..bbab0242f6 100644 --- a/server_addon/blender/server/version.py +++ b/server_addon/blender/server/version.py @@ -1 +1 @@ -__version__ = "0.1.3" +__version__ = "0.1.4" From 0916db5fa0c98e239e65ff5a95fa76184fec613f Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 1 Nov 2023 14:35:22 +0200 Subject: [PATCH 0657/1233] set f1 and f2 to $FSTART and $FEND respectively --- openpype/hosts/houdini/plugins/create/create_composite.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/hosts/houdini/plugins/create/create_composite.py b/openpype/hosts/houdini/plugins/create/create_composite.py index 9d4f7969bb..52ea6fa054 100644 --- a/openpype/hosts/houdini/plugins/create/create_composite.py +++ b/openpype/hosts/houdini/plugins/create/create_composite.py @@ -45,6 +45,11 @@ class CreateCompositeSequence(plugin.HoudiniCreator): instance_node.setParms(parms) + # Manually set f1 & f2 to $FSTART and $FEND respectively + # to match other Houdini nodes default. + instance_node.parm("f1").setExpression("$FSTART") + instance_node.parm("f2").setExpression("$FEND") + # Lock any parameters in this list to_lock = ["prim_to_detail_pattern"] self.lock_parameters(instance_node, to_lock) From 41d9cf65b0d8d70affefe47e6feaca71c406a4d0 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 1 Nov 2023 15:05:33 +0200 Subject: [PATCH 0658/1233] make tab menu name change according to the app whether OpenPype or AYON --- openpype/hosts/houdini/api/creator_node_shelves.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/houdini/api/creator_node_shelves.py b/openpype/hosts/houdini/api/creator_node_shelves.py index 1f9fef7417..085642a277 100644 --- a/openpype/hosts/houdini/api/creator_node_shelves.py +++ b/openpype/hosts/houdini/api/creator_node_shelves.py @@ -173,6 +173,7 @@ def install(): os.remove(filepath) icon = get_openpype_icon_filepath() + tab_menu_label = os.environ.get("AVALON_LABEL") or "OpenPype" # Create context only to get creator plugins, so we don't reset and only # populate what we need to retrieve the list of creator plugins @@ -197,14 +198,14 @@ def install(): if not network_categories: continue - key = "openpype_create.{}".format(identifier) + key = "ayon_create.{}".format(identifier) log.debug(f"Registering {key}") script = CREATE_SCRIPT.format(identifier=identifier) data = { "script": script, "language": hou.scriptLanguage.Python, "icon": icon, - "help": "Create OpenPype publish instance for {}".format( + "help": "Create Ayon publish instance for {}".format( creator.label ), "help_url": None, @@ -213,7 +214,7 @@ def install(): "cop_viewer_categories": [], "network_op_type": None, "viewer_op_type": None, - "locations": ["OpenPype"] + "locations": [tab_menu_label] } label = "Create {}".format(creator.label) tool = hou.shelves.tool(key) From c577d2bc84854382ecf157b22f293d4f23c38298 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 1 Nov 2023 13:09:57 +0000 Subject: [PATCH 0659/1233] Fix default settings --- server_addon/blender/server/settings/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/blender/server/settings/main.py b/server_addon/blender/server/settings/main.py index 374b2fafa2..5eff276ef5 100644 --- a/server_addon/blender/server/settings/main.py +++ b/server_addon/blender/server/settings/main.py @@ -61,7 +61,7 @@ DEFAULT_VALUES = { }, "set_frames_startup": True, "set_resolution_startup": True, - "render_settings": DEFAULT_RENDER_SETTINGS, + "RenderSettings": DEFAULT_RENDER_SETTINGS, "publish": DEFAULT_BLENDER_PUBLISH_SETTINGS, "workfile_builder": { "create_first_version": False, From eb242e78a5afeef40c814bc27a140dbfdc0e7f85 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 1 Nov 2023 15:13:40 +0200 Subject: [PATCH 0660/1233] add get_network_categories --- openpype/hosts/houdini/plugins/create/create_bgeo.py | 8 +++++++- openpype/hosts/houdini/plugins/create/create_hda.py | 7 ++++++- .../houdini/plugins/create/create_redshift_proxy.py | 12 +++++++++--- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_bgeo.py b/openpype/hosts/houdini/plugins/create/create_bgeo.py index a3f31e7e94..0f629cf9c9 100644 --- a/openpype/hosts/houdini/plugins/create/create_bgeo.py +++ b/openpype/hosts/houdini/plugins/create/create_bgeo.py @@ -3,6 +3,7 @@ from openpype.hosts.houdini.api import plugin from openpype.pipeline import CreatedInstance, CreatorError from openpype.lib import EnumDef +import hou class CreateBGEO(plugin.HoudiniCreator): @@ -13,7 +14,6 @@ class CreateBGEO(plugin.HoudiniCreator): icon = "gears" def create(self, subset_name, instance_data, pre_create_data): - import hou instance_data.pop("active", None) @@ -90,3 +90,9 @@ class CreateBGEO(plugin.HoudiniCreator): return attrs + [ EnumDef("bgeo_type", bgeo_enum, label="BGEO Options"), ] + + def get_network_categories(self): + return [ + hou.ropNodeTypeCategory(), + hou.sopNodeTypeCategory() + ] diff --git a/openpype/hosts/houdini/plugins/create/create_hda.py b/openpype/hosts/houdini/plugins/create/create_hda.py index c4093bfbc6..ac075d2072 100644 --- a/openpype/hosts/houdini/plugins/create/create_hda.py +++ b/openpype/hosts/houdini/plugins/create/create_hda.py @@ -5,6 +5,7 @@ from openpype.client import ( get_subsets, ) from openpype.hosts.houdini.api import plugin +import hou class CreateHDA(plugin.HoudiniCreator): @@ -35,7 +36,6 @@ class CreateHDA(plugin.HoudiniCreator): def create_instance_node( self, node_name, parent, node_type="geometry"): - import hou parent_node = hou.node("/obj") if self.selected_nodes: @@ -81,3 +81,8 @@ class CreateHDA(plugin.HoudiniCreator): pre_create_data) # type: plugin.CreatedInstance return instance + + def get_network_categories(self): + return [ + hou.objNodeTypeCategory() + ] diff --git a/openpype/hosts/houdini/plugins/create/create_redshift_proxy.py b/openpype/hosts/houdini/plugins/create/create_redshift_proxy.py index b814dd9d57..3a4ab7008b 100644 --- a/openpype/hosts/houdini/plugins/create/create_redshift_proxy.py +++ b/openpype/hosts/houdini/plugins/create/create_redshift_proxy.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """Creator plugin for creating Redshift proxies.""" from openpype.hosts.houdini.api import plugin -from openpype.pipeline import CreatedInstance +import hou class CreateRedshiftProxy(plugin.HoudiniCreator): @@ -12,7 +12,7 @@ class CreateRedshiftProxy(plugin.HoudiniCreator): icon = "magic" def create(self, subset_name, instance_data, pre_create_data): - import hou # noqa + # Remove the active, we are checking the bypass flag of the nodes instance_data.pop("active", None) @@ -28,7 +28,7 @@ class CreateRedshiftProxy(plugin.HoudiniCreator): instance = super(CreateRedshiftProxy, self).create( subset_name, instance_data, - pre_create_data) # type: CreatedInstance + pre_create_data) instance_node = hou.node(instance.get("instance_node")) @@ -44,3 +44,9 @@ class CreateRedshiftProxy(plugin.HoudiniCreator): # Lock some Avalon attributes to_lock = ["family", "id", "prim_to_detail_pattern"] self.lock_parameters(instance_node, to_lock) + + def get_network_categories(self): + return [ + hou.ropNodeTypeCategory(), + hou.sopNodeTypeCategory() + ] From aed9b13e0048951f9cc8605415ea7d5142ca11cb Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 1 Nov 2023 15:15:09 +0200 Subject: [PATCH 0661/1233] update ids to 'ayon' instead of 'openpype' --- openpype/hosts/houdini/startup/MainMenuCommon.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/houdini/startup/MainMenuCommon.xml b/openpype/hosts/houdini/startup/MainMenuCommon.xml index b2e32a70f9..c875cac0f5 100644 --- a/openpype/hosts/houdini/startup/MainMenuCommon.xml +++ b/openpype/hosts/houdini/startup/MainMenuCommon.xml @@ -16,7 +16,7 @@ return label - + - + - + Date: Wed, 1 Nov 2023 15:35:09 +0200 Subject: [PATCH 0662/1233] update get_network_categories --- openpype/hosts/houdini/plugins/create/create_pointcache.py | 1 + openpype/hosts/houdini/plugins/create/create_staticmesh.py | 1 + openpype/hosts/houdini/plugins/create/create_vbd_cache.py | 1 + 3 files changed, 3 insertions(+) diff --git a/openpype/hosts/houdini/plugins/create/create_pointcache.py b/openpype/hosts/houdini/plugins/create/create_pointcache.py index 7eaf2aff2b..8fe8052e0a 100644 --- a/openpype/hosts/houdini/plugins/create/create_pointcache.py +++ b/openpype/hosts/houdini/plugins/create/create_pointcache.py @@ -83,6 +83,7 @@ class CreatePointCache(plugin.HoudiniCreator): def get_network_categories(self): return [ hou.ropNodeTypeCategory(), + hou.objNodeTypeCategory(), hou.sopNodeTypeCategory() ] diff --git a/openpype/hosts/houdini/plugins/create/create_staticmesh.py b/openpype/hosts/houdini/plugins/create/create_staticmesh.py index ea0b36f03f..d0985198bd 100644 --- a/openpype/hosts/houdini/plugins/create/create_staticmesh.py +++ b/openpype/hosts/houdini/plugins/create/create_staticmesh.py @@ -54,6 +54,7 @@ class CreateStaticMesh(plugin.HoudiniCreator): def get_network_categories(self): return [ hou.ropNodeTypeCategory(), + hou.objNodeTypeCategory(), hou.sopNodeTypeCategory() ] diff --git a/openpype/hosts/houdini/plugins/create/create_vbd_cache.py b/openpype/hosts/houdini/plugins/create/create_vbd_cache.py index 9c96e48e3a..69418f9575 100644 --- a/openpype/hosts/houdini/plugins/create/create_vbd_cache.py +++ b/openpype/hosts/houdini/plugins/create/create_vbd_cache.py @@ -40,6 +40,7 @@ class CreateVDBCache(plugin.HoudiniCreator): def get_network_categories(self): return [ hou.ropNodeTypeCategory(), + hou.objNodeTypeCategory(), hou.sopNodeTypeCategory() ] From 25c8a424ec65330777bc3968c41d9fe6f59d470c Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 1 Nov 2023 15:40:29 +0200 Subject: [PATCH 0663/1233] skip check if node has no 'trange' parameter --- .../hosts/houdini/plugins/publish/validate_frame_range.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openpype/hosts/houdini/plugins/publish/validate_frame_range.py b/openpype/hosts/houdini/plugins/publish/validate_frame_range.py index 6a66f3de9f..2264372549 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_frame_range.py +++ b/openpype/hosts/houdini/plugins/publish/validate_frame_range.py @@ -57,6 +57,14 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): return rop_node = hou.node(instance.data["instance_node"]) + + if rop_node.parm("trange") is None: + cls.log.debug( + "Skipping Check, Node has no 'trange' parameter: {}" + .format(rop_node.path()) + ) + return + if instance.data["frameStart"] > instance.data["frameEnd"]: cls.log.info( "The ROP node render range is set to " From 60438ab4a8bed39a8ee681f03e995e88a8b17943 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 1 Nov 2023 17:00:41 +0200 Subject: [PATCH 0664/1233] BigRoy's comments - Better conditional and debug message --- .../houdini/plugins/publish/validate_frame_range.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_frame_range.py b/openpype/hosts/houdini/plugins/publish/validate_frame_range.py index 2264372549..5d3866cfdb 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_frame_range.py +++ b/openpype/hosts/houdini/plugins/publish/validate_frame_range.py @@ -57,15 +57,17 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): return rop_node = hou.node(instance.data["instance_node"]) + frame_start = instance.data.get("frameStart") + frame_end = instance.data.get("frameEnd") - if rop_node.parm("trange") is None: + if not (frame_start or frame_end): cls.log.debug( - "Skipping Check, Node has no 'trange' parameter: {}" - .format(rop_node.path()) + "Skipping frame range validation for " + "instance without frame data: {}".format(rop_node.path()) ) return - if instance.data["frameStart"] > instance.data["frameEnd"]: + if frame_start > frame_end: cls.log.info( "The ROP node render range is set to " "{0[frameStartHandle]} - {0[frameEndHandle]} " From 5a873368ee9b2544ea2c19401354ec5f94712537 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 1 Nov 2023 17:56:23 +0200 Subject: [PATCH 0665/1233] fix loading bug --- openpype/hosts/houdini/plugins/load/load_image.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/load/load_image.py b/openpype/hosts/houdini/plugins/load/load_image.py index 663a93e48b..cff2b74e52 100644 --- a/openpype/hosts/houdini/plugins/load/load_image.py +++ b/openpype/hosts/houdini/plugins/load/load_image.py @@ -119,7 +119,8 @@ class ImageLoader(load.LoaderPlugin): if not parent.children(): parent.destroy() - def _get_file_sequence(self, root): + def _get_file_sequence(self, file_path): + root = os.path.dirname(file_path) files = sorted(os.listdir(root)) first_fname = files[0] From dca872e1fce9f1735063769d17e8256f9c003125 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 1 Nov 2023 17:57:00 +0200 Subject: [PATCH 0666/1233] fix collector order to fix the missing frames --- openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py | 4 ++-- openpype/hosts/houdini/plugins/publish/collect_frames.py | 4 +++- openpype/hosts/houdini/plugins/publish/collect_karma_rop.py | 4 ++-- openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py | 4 ++-- .../hosts/houdini/plugins/publish/collect_redshift_rop.py | 4 ++-- openpype/hosts/houdini/plugins/publish/collect_vray_rop.py | 4 ++-- 6 files changed, 13 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py index b489f83b29..420a8324fe 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py @@ -21,8 +21,8 @@ class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin): label = "Arnold ROP Render Products" # This specific order value is used so that - # this plugin runs after CollectRopFrameRange - order = pyblish.api.CollectorOrder + 0.4999 + # this plugin runs after CollectFrames + order = pyblish.api.CollectorOrder + 0.49999 hosts = ["houdini"] families = ["arnold_rop"] diff --git a/openpype/hosts/houdini/plugins/publish/collect_frames.py b/openpype/hosts/houdini/plugins/publish/collect_frames.py index 01df809d4c..79cfcc6139 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_frames.py +++ b/openpype/hosts/houdini/plugins/publish/collect_frames.py @@ -11,7 +11,9 @@ from openpype.hosts.houdini.api import lib class CollectFrames(pyblish.api.InstancePlugin): """Collect all frames which would be saved from the ROP nodes""" - order = pyblish.api.CollectorOrder + 0.01 + # This specific order value is used so that + # this plugin runs after CollectRopFrameRange + order = pyblish.api.CollectorOrder + 0.4999 label = "Collect Frames" families = ["vdbcache", "imagesequence", "ass", "redshiftproxy", "review", "bgeo"] diff --git a/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py b/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py index fe0b8711fc..a477529df9 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py @@ -25,8 +25,8 @@ class CollectKarmaROPRenderProducts(pyblish.api.InstancePlugin): label = "Karma ROP Render Products" # This specific order value is used so that - # this plugin runs after CollectRopFrameRange - order = pyblish.api.CollectorOrder + 0.4999 + # this plugin runs after CollectFrames + order = pyblish.api.CollectorOrder + 0.49999 hosts = ["houdini"] families = ["karma_rop"] diff --git a/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py b/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py index cc412f30a1..9f0ae8d33c 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py @@ -25,8 +25,8 @@ class CollectMantraROPRenderProducts(pyblish.api.InstancePlugin): label = "Mantra ROP Render Products" # This specific order value is used so that - # this plugin runs after CollectRopFrameRange - order = pyblish.api.CollectorOrder + 0.4999 + # this plugin runs after CollectFrames + order = pyblish.api.CollectorOrder + 0.49999 hosts = ["houdini"] families = ["mantra_rop"] diff --git a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py index deb9eac971..0bd7b41641 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py @@ -25,8 +25,8 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin): label = "Redshift ROP Render Products" # This specific order value is used so that - # this plugin runs after CollectRopFrameRange - order = pyblish.api.CollectorOrder + 0.4999 + # this plugin runs after CollectFrames + order = pyblish.api.CollectorOrder + 0.49999 hosts = ["houdini"] families = ["redshift_rop"] diff --git a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py index 53072aebc6..519c12aede 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py @@ -25,8 +25,8 @@ class CollectVrayROPRenderProducts(pyblish.api.InstancePlugin): label = "VRay ROP Render Products" # This specific order value is used so that - # this plugin runs after CollectRopFrameRange - order = pyblish.api.CollectorOrder + 0.4999 + # this plugin runs after CollectFrames + order = pyblish.api.CollectorOrder + 0.49999 hosts = ["houdini"] families = ["vray_rop"] From c15adfa327cdb30f13c21f9e0f6c11d18c73ca9e Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 1 Nov 2023 18:05:23 +0200 Subject: [PATCH 0667/1233] BigRoys' commit - fallback to AYON --- openpype/hosts/houdini/api/creator_node_shelves.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/api/creator_node_shelves.py b/openpype/hosts/houdini/api/creator_node_shelves.py index 085642a277..14662dc419 100644 --- a/openpype/hosts/houdini/api/creator_node_shelves.py +++ b/openpype/hosts/houdini/api/creator_node_shelves.py @@ -173,7 +173,7 @@ def install(): os.remove(filepath) icon = get_openpype_icon_filepath() - tab_menu_label = os.environ.get("AVALON_LABEL") or "OpenPype" + tab_menu_label = os.environ.get("AVALON_LABEL") or "AYON" # Create context only to get creator plugins, so we don't reset and only # populate what we need to retrieve the list of creator plugins From c64af8ddef70412de33b8c04ff048cfbdc41d77d Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 1 Nov 2023 18:06:36 +0200 Subject: [PATCH 0668/1233] BigRoy's comment - remove Obj from network_categories to avoid possible confusion --- openpype/hosts/houdini/plugins/create/create_pointcache.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/create/create_pointcache.py b/openpype/hosts/houdini/plugins/create/create_pointcache.py index 8fe8052e0a..7eaf2aff2b 100644 --- a/openpype/hosts/houdini/plugins/create/create_pointcache.py +++ b/openpype/hosts/houdini/plugins/create/create_pointcache.py @@ -83,7 +83,6 @@ class CreatePointCache(plugin.HoudiniCreator): def get_network_categories(self): return [ hou.ropNodeTypeCategory(), - hou.objNodeTypeCategory(), hou.sopNodeTypeCategory() ] From bd8638caa10524dca1554208d4b301413729983b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 1 Nov 2023 17:16:37 +0100 Subject: [PATCH 0669/1233] ftrack events are not processed if project is not available in OpenPype database --- .../event_first_version_status.py | 45 ++++++++++++++----- .../event_next_task_update.py | 6 +++ .../event_push_frame_values_to_task.py | 6 +++ .../event_task_to_parent_status.py | 6 +++ .../event_task_to_version_status.py | 6 +++ .../event_thumbnail_updates.py | 6 +++ .../event_version_to_task_statuses.py | 5 +++ 7 files changed, 70 insertions(+), 10 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_server/event_first_version_status.py b/openpype/modules/ftrack/event_handlers_server/event_first_version_status.py index 8ef333effd..2ac02f233e 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_first_version_status.py +++ b/openpype/modules/ftrack/event_handlers_server/event_first_version_status.py @@ -1,3 +1,6 @@ +import collections + +from openpype.client import get_project from openpype_modules.ftrack.lib import BaseEvent @@ -73,8 +76,21 @@ class FirstVersionStatus(BaseEvent): if not self.task_status_map: return - entities_info = self.filter_event_ents(event) - if not entities_info: + filtered_entities_info = self.filter_entities_info(event) + if not filtered_entities_info: + return + + for project_id, entities_info in filtered_entities_info.items(): + self.process_by_project(session, event, project_id, entities_info) + + def process_by_project(self, session, event, project_id, entities_info): + project_name = self.get_project_name_from_event( + session, event, project_id + ) + if get_project(project_name) is None: + self.log.debug( + f"Project '{project_name}' not found in OpenPype. Skipping" + ) return entity_ids = [] @@ -154,18 +170,18 @@ class FirstVersionStatus(BaseEvent): exc_info=True ) - def filter_event_ents(self, event): - filtered_ents = [] - for entity in event["data"].get("entities", []): + def filter_entities_info(self, event): + filtered_entities_info = collections.defaultdict(list) + for entity_info in event["data"].get("entities", []): # Care only about add actions - if entity.get("action") != "add": + if entity_info.get("action") != "add": continue # Filter AssetVersions - if entity["entityType"] != "assetversion": + if entity_info["entityType"] != "assetversion": continue - entity_changes = entity.get("changes") or {} + entity_changes = entity_info.get("changes") or {} # Check if version of Asset Version is `1` version_num = entity_changes.get("version", {}).get("new") @@ -177,9 +193,18 @@ class FirstVersionStatus(BaseEvent): if not task_id: continue - filtered_ents.append(entity) + project_id = None + for parent_item in reversed(entity_info["parents"]): + if parent_item["entityType"] == "show": + project_id = parent_item["entityId"] + break - return filtered_ents + if project_id is None: + continue + + filtered_entities_info[project_id].append(entity_info) + + return filtered_entities_info def register(session): diff --git a/openpype/modules/ftrack/event_handlers_server/event_next_task_update.py b/openpype/modules/ftrack/event_handlers_server/event_next_task_update.py index a100c34f67..07a8ff433e 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_next_task_update.py +++ b/openpype/modules/ftrack/event_handlers_server/event_next_task_update.py @@ -1,4 +1,6 @@ import collections + +from openpype.client import get_project from openpype_modules.ftrack.lib import BaseEvent @@ -99,6 +101,10 @@ class NextTaskUpdate(BaseEvent): project_name = self.get_project_name_from_event( session, event, project_id ) + if get_project(project_name) is None: + self.log.debug("Project not found in OpenPype. Skipping") + return + # Load settings project_settings = self.get_project_settings_from_event( event, project_name diff --git a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py index ed630ad59d..65c3c1a69a 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py +++ b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py @@ -3,6 +3,8 @@ import copy from typing import Any import ftrack_api + +from openpype.client import get_project from openpype_modules.ftrack.lib import ( BaseEvent, query_custom_attributes, @@ -139,6 +141,10 @@ class PushHierValuesToNonHierEvent(BaseEvent): project_name: str = self.get_project_name_from_event( session, event, project_id ) + if get_project(project_name) is None: + self.log.debug("Project not found in OpenPype. Skipping") + return set(), set() + # Load settings project_settings: dict[str, Any] = ( self.get_project_settings_from_event(event, project_name) diff --git a/openpype/modules/ftrack/event_handlers_server/event_task_to_parent_status.py b/openpype/modules/ftrack/event_handlers_server/event_task_to_parent_status.py index 25fa3b0535..d2b395a1a3 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_task_to_parent_status.py +++ b/openpype/modules/ftrack/event_handlers_server/event_task_to_parent_status.py @@ -1,4 +1,6 @@ import collections + +from openpype.client import get_project from openpype_modules.ftrack.lib import BaseEvent @@ -60,6 +62,10 @@ class TaskStatusToParent(BaseEvent): project_name = self.get_project_name_from_event( session, event, project_id ) + if get_project(project_name) is None: + self.log.debug("Project not found in OpenPype. Skipping") + return + # Load settings project_settings = self.get_project_settings_from_event( event, project_name diff --git a/openpype/modules/ftrack/event_handlers_server/event_task_to_version_status.py b/openpype/modules/ftrack/event_handlers_server/event_task_to_version_status.py index b77849c678..91ee2410d7 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_task_to_version_status.py +++ b/openpype/modules/ftrack/event_handlers_server/event_task_to_version_status.py @@ -1,4 +1,6 @@ import collections + +from openpype.client import get_project from openpype_modules.ftrack.lib import BaseEvent @@ -102,6 +104,10 @@ class TaskToVersionStatus(BaseEvent): project_name = self.get_project_name_from_event( session, event, project_id ) + if get_project(project_name) is None: + self.log.debug("Project not found in OpenPype. Skipping") + return + # Load settings project_settings = self.get_project_settings_from_event( event, project_name diff --git a/openpype/modules/ftrack/event_handlers_server/event_thumbnail_updates.py b/openpype/modules/ftrack/event_handlers_server/event_thumbnail_updates.py index 64673f792c..318e69f414 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_thumbnail_updates.py +++ b/openpype/modules/ftrack/event_handlers_server/event_thumbnail_updates.py @@ -1,4 +1,6 @@ import collections + +from openpype.client import get_project from openpype_modules.ftrack.lib import BaseEvent @@ -22,6 +24,10 @@ class ThumbnailEvents(BaseEvent): project_name = self.get_project_name_from_event( session, event, project_id ) + if get_project(project_name) is None: + self.log.debug("Project not found in OpenPype. Skipping") + return + # Load settings project_settings = self.get_project_settings_from_event( event, project_name diff --git a/openpype/modules/ftrack/event_handlers_server/event_version_to_task_statuses.py b/openpype/modules/ftrack/event_handlers_server/event_version_to_task_statuses.py index fb40fd6417..fbe44bcba7 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_version_to_task_statuses.py +++ b/openpype/modules/ftrack/event_handlers_server/event_version_to_task_statuses.py @@ -1,3 +1,4 @@ +from openpype.client import get_project from openpype_modules.ftrack.lib import BaseEvent @@ -50,6 +51,10 @@ class VersionToTaskStatus(BaseEvent): project_name = self.get_project_name_from_event( session, event, project_id ) + if get_project(project_name) is None: + self.log.debug("Project not found in OpenPype. Skipping") + return + # Load settings project_settings = self.get_project_settings_from_event( event, project_name From d4b75797c69a59725e3d8348f44aefedb5136455 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 1 Nov 2023 20:49:05 +0100 Subject: [PATCH 0670/1233] nuke: making sure duplicated loader is not removed --- openpype/hosts/nuke/api/plugin.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index c39e3c339d..301b9533a9 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -537,6 +537,7 @@ class NukeLoader(LoaderPlugin): node.addKnob(knob) def clear_members(self, parent_node): + parent_class = parent_node.Class() members = self.get_members(parent_node) dependent_nodes = None @@ -549,6 +550,8 @@ class NukeLoader(LoaderPlugin): break for member in members: + if member.Class() == parent_class: + continue self.log.info("removing node: `{}".format(member.name())) nuke.delete(member) From e3eea5a8e35fedd12bebfdc0da77338850b22457 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 1 Nov 2023 20:49:36 +0100 Subject: [PATCH 0671/1233] Nuke: updating without node renameing --- openpype/hosts/nuke/plugins/load/load_clip.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/openpype/hosts/nuke/plugins/load/load_clip.py b/openpype/hosts/nuke/plugins/load/load_clip.py index 19038b168d..737da3746d 100644 --- a/openpype/hosts/nuke/plugins/load/load_clip.py +++ b/openpype/hosts/nuke/plugins/load/load_clip.py @@ -299,9 +299,6 @@ class LoadClip(plugin.NukeLoader): "Representation id `{}` is failing to load".format(repre_id)) return - read_name = self._get_node_name(representation) - - read_node["name"].setValue(read_name) read_node["file"].setValue(filepath) # to avoid multiple undo steps for rest of process From 83798a7b5e3daa3c71bb34d0849e70817730b311 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 1 Nov 2023 22:15:41 +0200 Subject: [PATCH 0672/1233] BigRoy's comment - fallback to 'AYON' --- 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 ac375c56d6..db7f0886c3 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -1018,7 +1018,7 @@ def self_publish(): def add_self_publish_button(node): """Adds a self publish button to the rop node.""" - label = os.environ.get("AVALON_LABEL") or "OpenPype" + label = os.environ.get("AVALON_LABEL") or "AYON" button_parm = hou.ButtonParmTemplate( "ayon_self_publish", From 3a78230ba4c8fc69285d9b660499891245479765 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 1 Nov 2023 22:16:57 +0200 Subject: [PATCH 0673/1233] BigRoy's comment - fallback to 'AYON' --- openpype/hosts/houdini/startup/MainMenuCommon.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/startup/MainMenuCommon.xml b/openpype/hosts/houdini/startup/MainMenuCommon.xml index c875cac0f5..0903aef7bc 100644 --- a/openpype/hosts/houdini/startup/MainMenuCommon.xml +++ b/openpype/hosts/houdini/startup/MainMenuCommon.xml @@ -4,7 +4,7 @@ Date: Wed, 1 Nov 2023 22:20:42 +0200 Subject: [PATCH 0674/1233] BigRoy's comment - Update COnditional --- openpype/hosts/houdini/plugins/publish/validate_frame_range.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_frame_range.py b/openpype/hosts/houdini/plugins/publish/validate_frame_range.py index 5d3866cfdb..90a079217b 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_frame_range.py +++ b/openpype/hosts/houdini/plugins/publish/validate_frame_range.py @@ -60,7 +60,7 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): frame_start = instance.data.get("frameStart") frame_end = instance.data.get("frameEnd") - if not (frame_start or frame_end): + if frame_start is None or frame_end is None: cls.log.debug( "Skipping frame range validation for " "instance without frame data: {}".format(rop_node.path()) From 978ec89f6562c82d2cb9d878cc3563448a21aca3 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 1 Nov 2023 21:35:20 +0100 Subject: [PATCH 0675/1233] Nuke: updating ls method to have full name and node --- openpype/hosts/nuke/api/pipeline.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/nuke/api/pipeline.py b/openpype/hosts/nuke/api/pipeline.py index a1d290646c..f6ba33f00f 100644 --- a/openpype/hosts/nuke/api/pipeline.py +++ b/openpype/hosts/nuke/api/pipeline.py @@ -478,8 +478,6 @@ def parse_container(node): """ data = read_avalon_data(node) - # (TODO) Remove key validation when `ls` has re-implemented. - # # If not all required data return the empty container required = ["schema", "id", "name", "namespace", "loader", "representation"] @@ -487,7 +485,10 @@ def parse_container(node): return # Store the node's name - data["objectName"] = node["name"].value() + data.update({ + "objectName": node.fullName(), + "node": node, + }) return data From 16aad9928823a10034d6d9bfc1ba7cb25fd24a53 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 1 Nov 2023 21:36:48 +0100 Subject: [PATCH 0676/1233] nuke: updating loaders so they are using nodes rather then objectName --- .../hosts/nuke/plugins/load/load_backdrop.py | 16 ++++---- .../nuke/plugins/load/load_camera_abc.py | 24 ++++++----- openpype/hosts/nuke/plugins/load/load_clip.py | 6 +-- .../hosts/nuke/plugins/load/load_effects.py | 30 +++++++------- .../nuke/plugins/load/load_effects_ip.py | 40 ++++++++----------- .../hosts/nuke/plugins/load/load_gizmo.py | 28 +++++++------ .../hosts/nuke/plugins/load/load_gizmo_ip.py | 28 +++++++------ .../hosts/nuke/plugins/load/load_image.py | 6 +-- .../hosts/nuke/plugins/load/load_model.py | 29 ++++++++------ .../hosts/nuke/plugins/load/load_ociolook.py | 25 +++++------- .../nuke/plugins/load/load_script_precomp.py | 7 +--- 11 files changed, 117 insertions(+), 122 deletions(-) diff --git a/openpype/hosts/nuke/plugins/load/load_backdrop.py b/openpype/hosts/nuke/plugins/load/load_backdrop.py index 0cbd380697..54d37da203 100644 --- a/openpype/hosts/nuke/plugins/load/load_backdrop.py +++ b/openpype/hosts/nuke/plugins/load/load_backdrop.py @@ -64,8 +64,7 @@ class LoadBackdropNodes(load.LoaderPlugin): data_imprint = { "version": vname, - "colorspaceInput": colorspace, - "objectName": object_name + "colorspaceInput": colorspace } for k in add_keys: @@ -194,7 +193,7 @@ class LoadBackdropNodes(load.LoaderPlugin): version_doc = get_version_by_id(project_name, representation["parent"]) # get corresponding node - GN = nuke.toNode(container['objectName']) + GN = container["node"] file = get_representation_path(representation).replace("\\", "/") @@ -207,10 +206,11 @@ class LoadBackdropNodes(load.LoaderPlugin): add_keys = ["source", "author", "fps"] - data_imprint = {"representation": str(representation["_id"]), - "version": vname, - "colorspaceInput": colorspace, - "objectName": object_name} + data_imprint = { + "representation": str(representation["_id"]), + "version": vname, + "colorspaceInput": colorspace, + } for k in add_keys: data_imprint.update({k: version_data[k]}) @@ -252,6 +252,6 @@ class LoadBackdropNodes(load.LoaderPlugin): self.update(container, representation) def remove(self, container): - node = nuke.toNode(container['objectName']) + node = container["node"] with viewer_update_and_undo_stop(): nuke.delete(node) diff --git a/openpype/hosts/nuke/plugins/load/load_camera_abc.py b/openpype/hosts/nuke/plugins/load/load_camera_abc.py index e245b0cb5e..898c5e4e7b 100644 --- a/openpype/hosts/nuke/plugins/load/load_camera_abc.py +++ b/openpype/hosts/nuke/plugins/load/load_camera_abc.py @@ -48,10 +48,11 @@ class AlembicCameraLoader(load.LoaderPlugin): # add additional metadata from the version to imprint to Avalon knob add_keys = ["source", "author", "fps"] - data_imprint = {"frameStart": first, - "frameEnd": last, - "version": vname, - "objectName": object_name} + data_imprint = { + "frameStart": first, + "frameEnd": last, + "version": vname, + } for k in add_keys: data_imprint.update({k: version_data[k]}) @@ -111,7 +112,7 @@ class AlembicCameraLoader(load.LoaderPlugin): project_name = get_current_project_name() version_doc = get_version_by_id(project_name, representation["parent"]) - object_name = container['objectName'] + object_name = container["node"] # get main variables version_data = version_doc.get("data", {}) @@ -124,11 +125,12 @@ class AlembicCameraLoader(load.LoaderPlugin): # add additional metadata from the version to imprint to Avalon knob add_keys = ["source", "author", "fps"] - data_imprint = {"representation": str(representation["_id"]), - "frameStart": first, - "frameEnd": last, - "version": vname, - "objectName": object_name} + data_imprint = { + "representation": str(representation["_id"]), + "frameStart": first, + "frameEnd": last, + "version": vname + } for k in add_keys: data_imprint.update({k: version_data[k]}) @@ -194,6 +196,6 @@ class AlembicCameraLoader(load.LoaderPlugin): self.update(container, representation) def remove(self, container): - node = nuke.toNode(container['objectName']) + node = container["node"] with viewer_update_and_undo_stop(): nuke.delete(node) diff --git a/openpype/hosts/nuke/plugins/load/load_clip.py b/openpype/hosts/nuke/plugins/load/load_clip.py index 737da3746d..3a2ec3dbee 100644 --- a/openpype/hosts/nuke/plugins/load/load_clip.py +++ b/openpype/hosts/nuke/plugins/load/load_clip.py @@ -189,8 +189,6 @@ class LoadClip(plugin.NukeLoader): value_ = value_.replace("\\", "/") data_imprint[key] = value_ - data_imprint["objectName"] = read_name - if add_retime and version_data.get("retime", None): data_imprint["addRetime"] = True @@ -254,7 +252,7 @@ class LoadClip(plugin.NukeLoader): is_sequence = len(representation["files"]) > 1 - read_node = nuke.toNode(container['objectName']) + read_node = container["node"] if is_sequence: representation = self._representation_with_hash_in_frame( @@ -353,7 +351,7 @@ class LoadClip(plugin.NukeLoader): self.set_as_member(read_node) def remove(self, container): - read_node = nuke.toNode(container['objectName']) + read_node = container["node"] assert read_node.Class() == "Read", "Must be Read" with viewer_update_and_undo_stop(): diff --git a/openpype/hosts/nuke/plugins/load/load_effects.py b/openpype/hosts/nuke/plugins/load/load_effects.py index cacc00854e..cc048372d4 100644 --- a/openpype/hosts/nuke/plugins/load/load_effects.py +++ b/openpype/hosts/nuke/plugins/load/load_effects.py @@ -62,11 +62,12 @@ class LoadEffects(load.LoaderPlugin): add_keys = ["frameStart", "frameEnd", "handleStart", "handleEnd", "source", "author", "fps"] - data_imprint = {"frameStart": first, - "frameEnd": last, - "version": vname, - "colorspaceInput": colorspace, - "objectName": object_name} + data_imprint = { + "frameStart": first, + "frameEnd": last, + "version": vname, + "colorspaceInput": colorspace, + } for k in add_keys: data_imprint.update({k: version_data[k]}) @@ -159,7 +160,7 @@ class LoadEffects(load.LoaderPlugin): version_doc = get_version_by_id(project_name, representation["parent"]) # get corresponding node - GN = nuke.toNode(container['objectName']) + GN = container["node"] file = get_representation_path(representation).replace("\\", "/") name = container['name'] @@ -175,12 +176,13 @@ class LoadEffects(load.LoaderPlugin): add_keys = ["frameStart", "frameEnd", "handleStart", "handleEnd", "source", "author", "fps"] - data_imprint = {"representation": str(representation["_id"]), - "frameStart": first, - "frameEnd": last, - "version": vname, - "colorspaceInput": colorspace, - "objectName": object_name} + data_imprint = { + "representation": str(representation["_id"]), + "frameStart": first, + "frameEnd": last, + "version": vname, + "colorspaceInput": colorspace + } for k in add_keys: data_imprint.update({k: version_data[k]}) @@ -212,7 +214,7 @@ class LoadEffects(load.LoaderPlugin): pre_node = nuke.createNode("Input") pre_node["name"].setValue("rgb") - for ef_name, ef_val in nodes_order.items(): + for _, ef_val in nodes_order.items(): node = nuke.createNode(ef_val["class"]) for k, v in ef_val["node"].items(): if k in self.ignore_attr: @@ -346,6 +348,6 @@ class LoadEffects(load.LoaderPlugin): self.update(container, representation) def remove(self, container): - node = nuke.toNode(container['objectName']) + node = container["node"] with viewer_update_and_undo_stop(): nuke.delete(node) diff --git a/openpype/hosts/nuke/plugins/load/load_effects_ip.py b/openpype/hosts/nuke/plugins/load/load_effects_ip.py index bdf3cd6965..cdfdfef3b8 100644 --- a/openpype/hosts/nuke/plugins/load/load_effects_ip.py +++ b/openpype/hosts/nuke/plugins/load/load_effects_ip.py @@ -63,11 +63,12 @@ class LoadEffectsInputProcess(load.LoaderPlugin): add_keys = ["frameStart", "frameEnd", "handleStart", "handleEnd", "source", "author", "fps"] - data_imprint = {"frameStart": first, - "frameEnd": last, - "version": vname, - "colorspaceInput": colorspace, - "objectName": object_name} + data_imprint = { + "frameStart": first, + "frameEnd": last, + "version": vname, + "colorspaceInput": colorspace, + } for k in add_keys: data_imprint.update({k: version_data[k]}) @@ -98,7 +99,7 @@ class LoadEffectsInputProcess(load.LoaderPlugin): pre_node = nuke.createNode("Input") pre_node["name"].setValue("rgb") - for ef_name, ef_val in nodes_order.items(): + for _, ef_val in nodes_order.items(): node = nuke.createNode(ef_val["class"]) for k, v in ef_val["node"].items(): if k in self.ignore_attr: @@ -164,28 +165,26 @@ class LoadEffectsInputProcess(load.LoaderPlugin): version_doc = get_version_by_id(project_name, representation["parent"]) # get corresponding node - GN = nuke.toNode(container['objectName']) + GN = container["node"] file = get_representation_path(representation).replace("\\", "/") - name = container['name'] version_data = version_doc.get("data", {}) vname = version_doc.get("name", None) first = version_data.get("frameStart", None) last = version_data.get("frameEnd", None) workfile_first_frame = int(nuke.root()["first_frame"].getValue()) - namespace = container['namespace'] colorspace = version_data.get("colorspace", None) - object_name = "{}_{}".format(name, namespace) add_keys = ["frameStart", "frameEnd", "handleStart", "handleEnd", "source", "author", "fps"] - data_imprint = {"representation": str(representation["_id"]), - "frameStart": first, - "frameEnd": last, - "version": vname, - "colorspaceInput": colorspace, - "objectName": object_name} + data_imprint = { + "representation": str(representation["_id"]), + "frameStart": first, + "frameEnd": last, + "version": vname, + "colorspaceInput": colorspace, + } for k in add_keys: data_imprint.update({k: version_data[k]}) @@ -217,7 +216,7 @@ class LoadEffectsInputProcess(load.LoaderPlugin): pre_node = nuke.createNode("Input") pre_node["name"].setValue("rgb") - for ef_name, ef_val in nodes_order.items(): + for _, ef_val in nodes_order.items(): node = nuke.createNode(ef_val["class"]) for k, v in ef_val["node"].items(): if k in self.ignore_attr: @@ -251,11 +250,6 @@ class LoadEffectsInputProcess(load.LoaderPlugin): output = nuke.createNode("Output") output.setInput(0, pre_node) - # # try to place it under Viewer1 - # if not self.connect_active_viewer(GN): - # nuke.delete(GN) - # return - # get all versions in list last_version_doc = get_last_version_by_subset_id( project_name, version_doc["parent"], fields=["_id"] @@ -365,6 +359,6 @@ class LoadEffectsInputProcess(load.LoaderPlugin): self.update(container, representation) def remove(self, container): - node = nuke.toNode(container['objectName']) + node = container["node"] with viewer_update_and_undo_stop(): nuke.delete(node) diff --git a/openpype/hosts/nuke/plugins/load/load_gizmo.py b/openpype/hosts/nuke/plugins/load/load_gizmo.py index 23cf4d7741..19b5cca74e 100644 --- a/openpype/hosts/nuke/plugins/load/load_gizmo.py +++ b/openpype/hosts/nuke/plugins/load/load_gizmo.py @@ -64,11 +64,12 @@ class LoadGizmo(load.LoaderPlugin): add_keys = ["frameStart", "frameEnd", "handleStart", "handleEnd", "source", "author", "fps"] - data_imprint = {"frameStart": first, - "frameEnd": last, - "version": vname, - "colorspaceInput": colorspace, - "objectName": object_name} + data_imprint = { + "frameStart": first, + "frameEnd": last, + "version": vname, + "colorspaceInput": colorspace + } for k in add_keys: data_imprint.update({k: version_data[k]}) @@ -111,7 +112,7 @@ class LoadGizmo(load.LoaderPlugin): version_doc = get_version_by_id(project_name, representation["parent"]) # get corresponding node - group_node = nuke.toNode(container['objectName']) + group_node = container["node"] file = get_representation_path(representation).replace("\\", "/") name = container['name'] @@ -126,12 +127,13 @@ class LoadGizmo(load.LoaderPlugin): add_keys = ["frameStart", "frameEnd", "handleStart", "handleEnd", "source", "author", "fps"] - data_imprint = {"representation": str(representation["_id"]), - "frameStart": first, - "frameEnd": last, - "version": vname, - "colorspaceInput": colorspace, - "objectName": object_name} + data_imprint = { + "representation": str(representation["_id"]), + "frameStart": first, + "frameEnd": last, + "version": vname, + "colorspaceInput": colorspace + } for k in add_keys: data_imprint.update({k: version_data[k]}) @@ -175,6 +177,6 @@ class LoadGizmo(load.LoaderPlugin): self.update(container, representation) def remove(self, container): - node = nuke.toNode(container['objectName']) + node = container["node"] with viewer_update_and_undo_stop(): nuke.delete(node) diff --git a/openpype/hosts/nuke/plugins/load/load_gizmo_ip.py b/openpype/hosts/nuke/plugins/load/load_gizmo_ip.py index ce0a1615f1..5b4877678a 100644 --- a/openpype/hosts/nuke/plugins/load/load_gizmo_ip.py +++ b/openpype/hosts/nuke/plugins/load/load_gizmo_ip.py @@ -66,11 +66,12 @@ class LoadGizmoInputProcess(load.LoaderPlugin): add_keys = ["frameStart", "frameEnd", "handleStart", "handleEnd", "source", "author", "fps"] - data_imprint = {"frameStart": first, - "frameEnd": last, - "version": vname, - "colorspaceInput": colorspace, - "objectName": object_name} + data_imprint = { + "frameStart": first, + "frameEnd": last, + "version": vname, + "colorspaceInput": colorspace + } for k in add_keys: data_imprint.update({k: version_data[k]}) @@ -118,7 +119,7 @@ class LoadGizmoInputProcess(load.LoaderPlugin): version_doc = get_version_by_id(project_name, representation["parent"]) # get corresponding node - group_node = nuke.toNode(container['objectName']) + group_node = container["node"] file = get_representation_path(representation).replace("\\", "/") name = container['name'] @@ -133,12 +134,13 @@ class LoadGizmoInputProcess(load.LoaderPlugin): add_keys = ["frameStart", "frameEnd", "handleStart", "handleEnd", "source", "author", "fps"] - data_imprint = {"representation": str(representation["_id"]), - "frameStart": first, - "frameEnd": last, - "version": vname, - "colorspaceInput": colorspace, - "objectName": object_name} + data_imprint = { + "representation": str(representation["_id"]), + "frameStart": first, + "frameEnd": last, + "version": vname, + "colorspaceInput": colorspace + } for k in add_keys: data_imprint.update({k: version_data[k]}) @@ -256,6 +258,6 @@ class LoadGizmoInputProcess(load.LoaderPlugin): self.update(container, representation) def remove(self, container): - node = nuke.toNode(container['objectName']) + node = container["node"] with viewer_update_and_undo_stop(): nuke.delete(node) diff --git a/openpype/hosts/nuke/plugins/load/load_image.py b/openpype/hosts/nuke/plugins/load/load_image.py index 6bffb97e6f..411a61d77b 100644 --- a/openpype/hosts/nuke/plugins/load/load_image.py +++ b/openpype/hosts/nuke/plugins/load/load_image.py @@ -146,8 +146,6 @@ class LoadImage(load.LoaderPlugin): data_imprint.update( {k: context["version"]['data'].get(k, str(None))}) - data_imprint.update({"objectName": read_name}) - r["tile_color"].setValue(int("0x4ecd25ff", 16)) return containerise(r, @@ -168,7 +166,7 @@ class LoadImage(load.LoaderPlugin): inputs: """ - node = nuke.toNode(container["objectName"]) + node = container["node"] frame_number = node["first"].value() assert node.Class() == "Read", "Must be Read" @@ -237,7 +235,7 @@ class LoadImage(load.LoaderPlugin): self.log.info("updated to version: {}".format(version_doc.get("name"))) def remove(self, container): - node = nuke.toNode(container['objectName']) + node = container["node"] assert node.Class() == "Read", "Must be Read" with viewer_update_and_undo_stop(): diff --git a/openpype/hosts/nuke/plugins/load/load_model.py b/openpype/hosts/nuke/plugins/load/load_model.py index b9b8a0f4c0..3fe92b74d0 100644 --- a/openpype/hosts/nuke/plugins/load/load_model.py +++ b/openpype/hosts/nuke/plugins/load/load_model.py @@ -46,10 +46,11 @@ class AlembicModelLoader(load.LoaderPlugin): # add additional metadata from the version to imprint to Avalon knob add_keys = ["source", "author", "fps"] - data_imprint = {"frameStart": first, - "frameEnd": last, - "version": vname, - "objectName": object_name} + data_imprint = { + "frameStart": first, + "frameEnd": last, + "version": vname + } for k in add_keys: data_imprint.update({k: version_data[k]}) @@ -114,9 +115,9 @@ class AlembicModelLoader(load.LoaderPlugin): # Get version from io project_name = get_current_project_name() version_doc = get_version_by_id(project_name, representation["parent"]) - object_name = container['objectName'] + # get corresponding node - model_node = nuke.toNode(object_name) + model_node = container["node"] # get main variables version_data = version_doc.get("data", {}) @@ -129,11 +130,12 @@ class AlembicModelLoader(load.LoaderPlugin): # add additional metadata from the version to imprint to Avalon knob add_keys = ["source", "author", "fps"] - data_imprint = {"representation": str(representation["_id"]), - "frameStart": first, - "frameEnd": last, - "version": vname, - "objectName": object_name} + data_imprint = { + "representation": str(representation["_id"]), + "frameStart": first, + "frameEnd": last, + "version": vname + } for k in add_keys: data_imprint.update({k: version_data[k]}) @@ -142,7 +144,6 @@ class AlembicModelLoader(load.LoaderPlugin): file = get_representation_path(representation).replace("\\", "/") with maintained_selection(): - model_node = nuke.toNode(object_name) model_node['selected'].setValue(True) # collect input output dependencies @@ -163,8 +164,10 @@ class AlembicModelLoader(load.LoaderPlugin): ypos = model_node.ypos() nuke.nodeCopy("%clipboard%") nuke.delete(model_node) + + # paste the node back and set the position nuke.nodePaste("%clipboard%") - model_node = nuke.toNode(object_name) + model_node = nuke.selectedNode() model_node.setXYpos(xpos, ypos) # link to original input nodes diff --git a/openpype/hosts/nuke/plugins/load/load_ociolook.py b/openpype/hosts/nuke/plugins/load/load_ociolook.py index 18c8cdba35..c0f8235253 100644 --- a/openpype/hosts/nuke/plugins/load/load_ociolook.py +++ b/openpype/hosts/nuke/plugins/load/load_ociolook.py @@ -55,7 +55,7 @@ class LoadOcioLookNodes(load.LoaderPlugin): """ namespace = namespace or context['asset']['name'] suffix = secrets.token_hex(nbytes=4) - object_name = "{}_{}_{}".format( + node_name = "{}_{}_{}".format( name, namespace, suffix) # getting file path @@ -64,7 +64,9 @@ class LoadOcioLookNodes(load.LoaderPlugin): json_f = self._load_json_data(filepath) group_node = self._create_group_node( - object_name, filepath, json_f["data"]) + filepath, json_f["data"]) + # renaming group node + group_node["name"].setValue(node_name) self._node_version_color(context["version"], group_node) @@ -76,17 +78,14 @@ class LoadOcioLookNodes(load.LoaderPlugin): name=name, namespace=namespace, context=context, - loader=self.__class__.__name__, - data={ - "objectName": object_name, - } + loader=self.__class__.__name__ ) def _create_group_node( self, - object_name, filepath, - data + data, + group_node=None ): """Creates group node with all the nodes inside. @@ -94,9 +93,9 @@ class LoadOcioLookNodes(load.LoaderPlugin): in between - in case those are needed. Arguments: - object_name (str): name of the group node filepath (str): path to json file data (dict): data from json file + group_node (Optional[nuke.Node]): group node or None Returns: nuke.Node: group node with all the nodes inside @@ -117,7 +116,6 @@ class LoadOcioLookNodes(load.LoaderPlugin): input_node = None output_node = None - group_node = nuke.toNode(object_name) if group_node: # remove all nodes between Input and Output nodes for node in group_node.nodes(): @@ -130,7 +128,6 @@ class LoadOcioLookNodes(load.LoaderPlugin): else: group_node = nuke.createNode( "Group", - "name {}_1".format(object_name), inpanel=False ) @@ -227,16 +224,16 @@ class LoadOcioLookNodes(load.LoaderPlugin): project_name = get_current_project_name() version_doc = get_version_by_id(project_name, representation["parent"]) - object_name = container['objectName'] + group_node = container["node"] filepath = get_representation_path(representation) json_f = self._load_json_data(filepath) group_node = self._create_group_node( - object_name, filepath, - json_f["data"] + json_f["data"], + group_node ) self._node_version_color(version_doc, group_node) diff --git a/openpype/hosts/nuke/plugins/load/load_script_precomp.py b/openpype/hosts/nuke/plugins/load/load_script_precomp.py index d5f9d24765..cbe19d217b 100644 --- a/openpype/hosts/nuke/plugins/load/load_script_precomp.py +++ b/openpype/hosts/nuke/plugins/load/load_script_precomp.py @@ -46,8 +46,6 @@ class LinkAsGroup(load.LoaderPlugin): file = self.filepath_from_context(context).replace("\\", "/") self.log.info("file: {}\n".format(file)) - precomp_name = context["representation"]["context"]["subset"] - self.log.info("versionData: {}\n".format(context["version"]["data"])) # add additional metadata from the version to imprint to Avalon knob @@ -62,7 +60,6 @@ class LinkAsGroup(load.LoaderPlugin): } for k in add_keys: data_imprint.update({k: context["version"]['data'][k]}) - data_imprint.update({"objectName": precomp_name}) # group context is set to precomp, so back up one level. nuke.endGroup() @@ -118,7 +115,7 @@ class LinkAsGroup(load.LoaderPlugin): inputs: """ - node = nuke.toNode(container['objectName']) + node = container["node"] root = get_representation_path(representation).replace("\\", "/") @@ -159,6 +156,6 @@ class LinkAsGroup(load.LoaderPlugin): self.log.info("updated to version: {}".format(version_doc.get("name"))) def remove(self, container): - node = nuke.toNode(container['objectName']) + node = container["node"] with viewer_update_and_undo_stop(): nuke.delete(node) From 8bf570ceef7823a2bf8615a30ac15dfc59c8eaf7 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 2 Nov 2023 12:24:25 +0800 Subject: [PATCH 0677/1233] up version for the max bundle --- server_addon/max/server/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/max/server/version.py b/server_addon/max/server/version.py index 3dc1f76bc6..485f44ac21 100644 --- a/server_addon/max/server/version.py +++ b/server_addon/max/server/version.py @@ -1 +1 @@ -__version__ = "0.1.0" +__version__ = "0.1.1" From 881340b60a1dd2eba9d76331082679b9e26e6df9 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 2 Nov 2023 12:27:22 +0800 Subject: [PATCH 0678/1233] supports families check before the validation of loaded plugins --- .../plugins/publish/validate_loaded_plugin.py | 59 +++++++++++++------ .../plugins/publish/collect_scene_version.py | 1 + openpype/settings/ayon_settings.py | 13 ++++ .../defaults/project_settings/max.json | 2 +- .../schemas/schema_max_publish.json | 12 ++-- .../max/server/settings/publishers.py | 12 +++- 6 files changed, 74 insertions(+), 25 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py index 69f72ccf1d..e8284aeedd 100644 --- a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py +++ b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py @@ -1,10 +1,11 @@ # -*- coding: utf-8 -*- """Validator for Loaded Plugin.""" -from pyblish.api import ContextPlugin, ValidatorOrder +import os +from pyblish.api import InstancePlugin, ValidatorOrder from pymxs import runtime as rt from openpype.pipeline.publish import ( - RepairContextAction, + RepairAction, OptionalPyblishPluginMixin, PublishValidationError ) @@ -12,7 +13,7 @@ from openpype.hosts.max.api.lib import get_plugins class ValidateLoadedPlugin(OptionalPyblishPluginMixin, - ContextPlugin): + InstancePlugin): """Validates if the specific plugin is loaded in 3ds max. Studio Admin(s) can add the plugins they want to check in validation via studio defined project settings""" @@ -21,17 +22,17 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, hosts = ["max"] label = "Validate Loaded Plugins" optional = True - actions = [RepairContextAction] + actions = [RepairAction] - def get_invalid(self, context): + def get_invalid(self, instance): """Plugin entry point.""" - if not self.is_active(context.data): + if not self.is_active(instance.data): self.log.debug("Skipping Validate Loaded Plugin...") return required_plugins = ( - context.data["project_settings"]["max"]["publish"] - ["ValidateLoadedPlugin"]["plugins_for_check"] + instance.context.data["project_settings"]["max"]["publish"] + ["ValidateLoadedPlugin"]["family_plugins_mapping"] ) if not required_plugins: @@ -45,9 +46,21 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, get_plugins()) } - for plugin in required_plugins: - plugin_name = plugin.lower() + for families, plugin in required_plugins.items(): + families_list = families.split(",") + excluded_families = [family for family in families_list + if instance.data["family"]!=family + and family!="_"] + if excluded_families: + self.log.debug("The {} instance is not part of {}.".format( + instance.data["family"], excluded_families + )) + return + if not plugin: + return + + plugin_name = plugin.format(**os.environ).lower() plugin_index = available_plugins.get(plugin_name) if plugin_index is None: @@ -61,8 +74,8 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, return invalid - def process(self, context): - invalid_plugins = self.get_invalid(context) + def process(self, instance): + invalid_plugins = self.get_invalid(instance) if invalid_plugins: bullet_point_invalid_statement = "\n".join( "- {}".format(invalid) for invalid in invalid_plugins @@ -76,18 +89,30 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, report, title="Required Plugins unloaded") @classmethod - def repair(cls, context): + def repair(cls, instance): # get all DLL loaded plugins in Max and their plugin index available_plugins = { plugin_name.lower(): index for index, plugin_name in enumerate( get_plugins()) } required_plugins = ( - context.data["project_settings"]["max"]["publish"] - ["ValidateLoadedPlugin"]["plugins_for_check"] + instance.context.data["project_settings"]["max"]["publish"] + ["ValidateLoadedPlugin"]["family_plugins_mapping"] ) - for plugin in required_plugins: - plugin_name = plugin.lower() + for families, plugin in required_plugins.items(): + families_list = families.split(",") + excluded_families = [family for family in families_list + if instance.data["family"]!=family + and family!="_"] + if excluded_families: + cls.log.debug("The {} instance is not part of {}.".format( + instance.data["family"], excluded_families + )) + continue + if not plugin: + continue + + plugin_name = plugin.format(**os.environ).lower() plugin_index = available_plugins.get(plugin_name) if plugin_index is None: diff --git a/openpype/plugins/publish/collect_scene_version.py b/openpype/plugins/publish/collect_scene_version.py index 7920c1e82b..f870ae9ad7 100644 --- a/openpype/plugins/publish/collect_scene_version.py +++ b/openpype/plugins/publish/collect_scene_version.py @@ -24,6 +24,7 @@ class CollectSceneVersion(pyblish.api.ContextPlugin): "hiero", "houdini", "maya", + "max", "nuke", "photoshop", "resolve", diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index 8d4683490b..0cc2abdda4 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -640,6 +640,19 @@ def _convert_3dsmax_project_settings(ayon_settings, output): } ayon_max["PointCloud"]["attribute"] = new_point_cloud_attribute + ayon_publish = ayon_max["publish"] + if "ValidateLoadedPlugin" in ayon_publish: + family_plugin_mapping = ( + ayon_publish["ValidateLoadedPlugin"]["family_plugins_mapping"] + ) + new_family_plugin_mapping = { + item["families"]: item["plugins"] + for item in family_plugin_mapping + } + ayon_max["ValidateLoadedPlugin"]["family_plugins_mapping"] = ( + new_family_plugin_mapping + ) + output["max"] = ayon_max diff --git a/openpype/settings/defaults/project_settings/max.json b/openpype/settings/defaults/project_settings/max.json index 45246fdf2b..78eba08750 100644 --- a/openpype/settings/defaults/project_settings/max.json +++ b/openpype/settings/defaults/project_settings/max.json @@ -40,7 +40,7 @@ "ValidateLoadedPlugin": { "enabled": false, "optional": true, - "plugins_for_check": [] + "family_plugins_mapping": {} } } } diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json index 4490c5353d..74c06f8156 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json @@ -47,10 +47,14 @@ "label": "Optional" }, { - "type": "list", - "key": "plugins_for_check", - "label": "Plugins Needed For Check", - "object_type": "text" + "type": "dict-modifiable", + "collapsible": true, + "key": "family_plugins_mapping", + "label": "Family Plugins Mapping", + "use_label_wrap": true, + "object_type": { + "type": "text" + } } ] } diff --git a/server_addon/max/server/settings/publishers.py b/server_addon/max/server/settings/publishers.py index 8a28224a07..3cf3ecf2a5 100644 --- a/server_addon/max/server/settings/publishers.py +++ b/server_addon/max/server/settings/publishers.py @@ -3,11 +3,17 @@ from pydantic import Field from ayon_server.settings import BaseSettingsModel +class FamilyPluginsMappingModel(BaseSettingsModel): + _layout = "compact" + families: str = Field(title="Families") + plugins: str = Field(title="Plugins") + + class ValidateLoadedPluginModel(BaseSettingsModel): enabled: bool = Field(title="ValidateLoadedPlugin") optional: bool = Field(title="Optional") - plugins_for_check: list[str] = Field( - default_factory=list, title="Plugins Needed For Check" + family_plugins_mapping: list[FamilyPluginsMappingModel] = Field( + default_factory=list, title="Family Plugins Mapping" ) @@ -37,6 +43,6 @@ DEFAULT_PUBLISH_SETTINGS = { "ValidateLoadedPlugin": { "enabled": False, "optional": True, - "plugins_for_check": [] + "family_plugins_mapping": {} } } From 8d727a9b80922bb07b468f694ab4f57f69945926 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 2 Nov 2023 12:30:38 +0800 Subject: [PATCH 0679/1233] hound --- .../max/plugins/publish/validate_loaded_plugin.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py index e8284aeedd..dc82c7ed65 100644 --- a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py +++ b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py @@ -32,7 +32,8 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, required_plugins = ( instance.context.data["project_settings"]["max"]["publish"] - ["ValidateLoadedPlugin"]["family_plugins_mapping"] + ["ValidateLoadedPlugin"] + ["family_plugins_mapping"] ) if not required_plugins: @@ -49,8 +50,8 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, for families, plugin in required_plugins.items(): families_list = families.split(",") excluded_families = [family for family in families_list - if instance.data["family"]!=family - and family!="_"] + if instance.data["family"] != family + and family != "_"] if excluded_families: self.log.debug("The {} instance is not part of {}.".format( instance.data["family"], excluded_families @@ -97,13 +98,14 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, } required_plugins = ( instance.context.data["project_settings"]["max"]["publish"] - ["ValidateLoadedPlugin"]["family_plugins_mapping"] + ["ValidateLoadedPlugin"] + ["family_plugins_mapping"] ) for families, plugin in required_plugins.items(): families_list = families.split(",") excluded_families = [family for family in families_list - if instance.data["family"]!=family - and family!="_"] + if instance.data["family"] != family + and family != "_"] if excluded_families: cls.log.debug("The {} instance is not part of {}.".format( instance.data["family"], excluded_families From 1aef9dc449b525313760db6e3d7e86378e6f1ab9 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 2 Nov 2023 12:45:48 +0800 Subject: [PATCH 0680/1233] make sure the validator can be loaded in AYON --- openpype/settings/ayon_settings.py | 2 +- server_addon/max/server/settings/publishers.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index 0cc2abdda4..4fe19c95a2 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -649,7 +649,7 @@ def _convert_3dsmax_project_settings(ayon_settings, output): item["families"]: item["plugins"] for item in family_plugin_mapping } - ayon_max["ValidateLoadedPlugin"]["family_plugins_mapping"] = ( + ayon_publish["ValidateLoadedPlugin"]["family_plugins_mapping"] = ( new_family_plugin_mapping ) diff --git a/server_addon/max/server/settings/publishers.py b/server_addon/max/server/settings/publishers.py index 3cf3ecf2a5..d0fbb3d552 100644 --- a/server_addon/max/server/settings/publishers.py +++ b/server_addon/max/server/settings/publishers.py @@ -43,6 +43,6 @@ DEFAULT_PUBLISH_SETTINGS = { "ValidateLoadedPlugin": { "enabled": False, "optional": True, - "family_plugins_mapping": {} + "family_plugins_mapping": [] } } From 3da4e1c8e7eafec01b4a501234c6a78cf7a1c686 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 2 Nov 2023 08:33:08 +0000 Subject: [PATCH 0681/1233] Add Nuke 11.0 --- .../system_settings/applications.json | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index 6a0ddb398e..a5283751e9 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -344,13 +344,30 @@ }, "environment": {} }, + "11-0": { + "use_python_2": true, + "executables": { + "windows": [ + "C:\\Program Files\\Nuke11.0v4\\Nuke11.0.exe" + ], + "darwin": [], + "linux": [] + }, + "arguments": { + "windows": [], + "darwin": [], + "linux": [] + }, + "environment": {} + }, "__dynamic_keys_labels__": { "13-2": "13.2", "13-0": "13.0", "12-2": "12.2", "12-0": "12.0", "11-3": "11.3", - "11-2": "11.2" + "11-2": "11.2", + "11-0": "11.0" } } }, From 58c9664f7e8ac2082a44279e652a1fb82674769d Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 2 Nov 2023 10:39:28 +0000 Subject: [PATCH 0682/1233] Use sets and don't check container children when getting unique number --- openpype/hosts/blender/api/plugin.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index 45b0c60b3b..2f940011ba 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -44,15 +44,15 @@ def get_unique_number( if not avalon_container: return "01" # Check the names of both object and collection containers - obj_asset_groups = avalon_container.all_objects - obj_group_names = [ + obj_asset_groups = avalon_container.objects + obj_group_names = { c.name for c in obj_asset_groups - if c.type == 'EMPTY' and c.get(AVALON_PROPERTY)] - coll_asset_groups = avalon_container.children_recursive - coll_group_names = [ + if c.type == 'EMPTY' and c.get(AVALON_PROPERTY)} + coll_asset_groups = avalon_container.children + coll_group_names = { c.name for c in coll_asset_groups - if c.get(AVALON_PROPERTY)] - container_names = obj_group_names + coll_group_names + if c.get(AVALON_PROPERTY)} + container_names = obj_group_names.union(coll_group_names) count = 1 name = f"{asset}_{count:0>2}_{subset}" while name in container_names: From 59bb86dc3337cb340d3ed9bf1c842167f98fa6b0 Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Thu, 2 Nov 2023 11:42:39 +0100 Subject: [PATCH 0683/1233] Rename BlenderCreator into BaseCreator --- openpype/hosts/blender/api/plugin.py | 2 +- openpype/hosts/blender/plugins/create/create_action.py | 2 +- openpype/hosts/blender/plugins/create/create_animation.py | 2 +- openpype/hosts/blender/plugins/create/create_camera.py | 2 +- openpype/hosts/blender/plugins/create/create_layout.py | 2 +- openpype/hosts/blender/plugins/create/create_model.py | 2 +- openpype/hosts/blender/plugins/create/create_pointcache.py | 2 +- openpype/hosts/blender/plugins/create/create_render.py | 2 +- openpype/hosts/blender/plugins/create/create_review.py | 2 +- openpype/hosts/blender/plugins/create/create_rig.py | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index dbd9f25d68..3ddc375670 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -140,7 +140,7 @@ def deselect_all(): bpy.context.view_layer.objects.active = active -class BlenderCreator(Creator): +class BaseCreator(Creator): """Base class for Blender Creator plug-ins.""" defaults = ['Main'] diff --git a/openpype/hosts/blender/plugins/create/create_action.py b/openpype/hosts/blender/plugins/create/create_action.py index e7b689c54e..7d00aa1dcb 100644 --- a/openpype/hosts/blender/plugins/create/create_action.py +++ b/openpype/hosts/blender/plugins/create/create_action.py @@ -8,7 +8,7 @@ from openpype.hosts.blender.api import lib from openpype.hosts.blender.api.pipeline import AVALON_PROPERTY -class CreateAction(openpype.hosts.blender.api.plugin.BlenderCreator): +class CreateAction(openpype.hosts.blender.api.plugin.BaseCreator): """Action output for character rigs""" identifier = "io.openpype.creators.blender.action" diff --git a/openpype/hosts/blender/plugins/create/create_animation.py b/openpype/hosts/blender/plugins/create/create_animation.py index 8b4214ceda..6cfd054e74 100644 --- a/openpype/hosts/blender/plugins/create/create_animation.py +++ b/openpype/hosts/blender/plugins/create/create_animation.py @@ -10,7 +10,7 @@ from openpype.hosts.blender.api.pipeline import ( ) -class CreateAnimation(plugin.BlenderCreator): +class CreateAnimation(plugin.BaseCreator): """Animation output for character rigs""" identifier = "io.openpype.creators.blender.animation" diff --git a/openpype/hosts/blender/plugins/create/create_camera.py b/openpype/hosts/blender/plugins/create/create_camera.py index 4747e50b2e..5d9682e575 100644 --- a/openpype/hosts/blender/plugins/create/create_camera.py +++ b/openpype/hosts/blender/plugins/create/create_camera.py @@ -10,7 +10,7 @@ from openpype.hosts.blender.api.pipeline import ( ) -class CreateCamera(plugin.BlenderCreator): +class CreateCamera(plugin.BaseCreator): """Polygonal static geometry""" identifier = "io.openpype.creators.blender.camera" diff --git a/openpype/hosts/blender/plugins/create/create_layout.py b/openpype/hosts/blender/plugins/create/create_layout.py index 0c97d57af3..ed47b0632f 100644 --- a/openpype/hosts/blender/plugins/create/create_layout.py +++ b/openpype/hosts/blender/plugins/create/create_layout.py @@ -10,7 +10,7 @@ from openpype.hosts.blender.api.pipeline import ( ) -class CreateLayout(plugin.BlenderCreator): +class CreateLayout(plugin.BaseCreator): """Layout output for character rigs""" identifier = "io.openpype.creators.blender.layout" diff --git a/openpype/hosts/blender/plugins/create/create_model.py b/openpype/hosts/blender/plugins/create/create_model.py index 761d9fca9f..949fae0f76 100644 --- a/openpype/hosts/blender/plugins/create/create_model.py +++ b/openpype/hosts/blender/plugins/create/create_model.py @@ -10,7 +10,7 @@ from openpype.hosts.blender.api.pipeline import ( ) -class CreateModel(plugin.BlenderCreator): +class CreateModel(plugin.BaseCreator): """Polygonal static geometry""" identifier = "io.openpype.creators.blender.model" diff --git a/openpype/hosts/blender/plugins/create/create_pointcache.py b/openpype/hosts/blender/plugins/create/create_pointcache.py index a40bd5af61..2ad12caa9c 100644 --- a/openpype/hosts/blender/plugins/create/create_pointcache.py +++ b/openpype/hosts/blender/plugins/create/create_pointcache.py @@ -8,7 +8,7 @@ from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES from openpype.hosts.blender.api.pipeline import AVALON_PROPERTY -class CreatePointcache(plugin.BlenderCreator): +class CreatePointcache(plugin.BaseCreator): """Polygonal static geometry""" identifier = "io.openpype.creators.blender.pointcache" diff --git a/openpype/hosts/blender/plugins/create/create_render.py b/openpype/hosts/blender/plugins/create/create_render.py index ab3119b32e..45570f3491 100644 --- a/openpype/hosts/blender/plugins/create/create_render.py +++ b/openpype/hosts/blender/plugins/create/create_render.py @@ -10,7 +10,7 @@ from openpype.hosts.blender.api.pipeline import ( ) -class CreateRenderlayer(plugin.BlenderCreator): +class CreateRenderlayer(plugin.BaseCreator): """Single baked camera""" name = "renderingMain" diff --git a/openpype/hosts/blender/plugins/create/create_review.py b/openpype/hosts/blender/plugins/create/create_review.py index 8c9a8d5927..e8b893b4c0 100644 --- a/openpype/hosts/blender/plugins/create/create_review.py +++ b/openpype/hosts/blender/plugins/create/create_review.py @@ -10,7 +10,7 @@ from openpype.hosts.blender.api.pipeline import ( ) -class CreateReview(plugin.BlenderCreator): +class CreateReview(plugin.BaseCreator): """Single baked camera""" identifier = "io.openpype.creators.blender.review" diff --git a/openpype/hosts/blender/plugins/create/create_rig.py b/openpype/hosts/blender/plugins/create/create_rig.py index 110a9f5c8e..6223e64174 100644 --- a/openpype/hosts/blender/plugins/create/create_rig.py +++ b/openpype/hosts/blender/plugins/create/create_rig.py @@ -10,7 +10,7 @@ from openpype.hosts.blender.api.pipeline import ( ) -class CreateRig(plugin.BlenderCreator): +class CreateRig(plugin.BaseCreator): """Artist-friendly rig with controls to direct motion""" identifier = "io.openpype.creators.blender.rig" From 7a1099b57e351e47d57e500c9184d2183527e61d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 2 Nov 2023 12:08:49 +0100 Subject: [PATCH 0684/1233] fix access to bundles in dev mode --- openpype/settings/ayon_settings.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index 8d4683490b..5b179158f0 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -1436,7 +1436,10 @@ class _AyonSettingsCache: def _use_bundles(cls): if _AyonSettingsCache.use_bundles is None: major, minor, _, _, _ = ayon_api.get_server_version_tuple() - _AyonSettingsCache.use_bundles = major == 0 and minor >= 3 + use_bundles = True + if (major, minor) < (0, 3): + use_bundles = False + _AyonSettingsCache.use_bundles = use_bundles return _AyonSettingsCache.use_bundles @classmethod @@ -1467,7 +1470,7 @@ class _AyonSettingsCache: bundles = ayon_api.get_bundles() user = ayon_api.get_user() username = user["name"] - for bundle in bundles: + for bundle in bundles["bundles"]: if ( bundle.get("isDev") and bundle.get("activeUser") == username From 13ec4d9a537296e648a4cf33ab5e9da865e78145 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 2 Nov 2023 12:31:58 +0100 Subject: [PATCH 0685/1233] fix formatting order --- openpype/modules/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index e8b85d0e93..15bde39f68 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -106,7 +106,7 @@ class _ModuleClass(object): if attr_name in self.__attributes__: self.log.warning( "Duplicated name \"{}\" in {}. Overriding.".format( - self.name, attr_name + attr_name, self.name ) ) self.__attributes__[attr_name] = value From 36f928151dc1e9a9996654ff8d698b7f1b51058a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 2 Nov 2023 12:32:18 +0100 Subject: [PATCH 0686/1233] safe call of get plugins path --- openpype/modules/base.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 15bde39f68..f47baa0e4d 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -997,7 +997,17 @@ class ModulesManager: continue method = getattr(module, method_name) - paths = method(*args, **kwargs) + try: + paths = method(*args, **kwargs) + except Exception: + self.log.warning( + "Failed to get plugin paths from module {}.".format( + module.__class__.__name__ + ), + exc_info=True + ) + continue + if paths: # Convert to list if value is not list if not isinstance(paths, (list, tuple, set)): From f912c2c69c9743be16705781d4a388c355ecf68c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 2 Nov 2023 12:32:35 +0100 Subject: [PATCH 0687/1233] change if conditions order --- openpype/modules/base.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index f47baa0e4d..1a3280a6e5 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -467,19 +467,19 @@ def _load_ayon_addons(openpype_modules, modules_key, log): )) continue - if len(imported_modules) == 1: - mod = imported_modules[0] - addon_alias = getattr(mod, "V3_ALIAS", None) - if not addon_alias: - addon_alias = addon_name - v3_addons_to_skip.append(addon_alias) - new_import_str = "{}.{}".format(modules_key, addon_alias) + if len(imported_modules) > 1: + log.info("More then one module '{}' was imported.".format(name)) + continue - sys.modules[new_import_str] = mod - setattr(openpype_modules, addon_alias, mod) + mod = imported_modules[0] + addon_alias = getattr(mod, "V3_ALIAS", None) + if not addon_alias: + addon_alias = addon_name + v3_addons_to_skip.append(addon_alias) + new_import_str = "{}.{}".format(modules_key, addon_alias) - else: - log.info("More then one module was imported") + sys.modules[new_import_str] = mod + setattr(openpype_modules, addon_alias, mod) return v3_addons_to_skip From d55ac7aff7cd7d470ddac5aa9d8f9d2b43cf2cd2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 2 Nov 2023 12:33:23 +0100 Subject: [PATCH 0688/1233] removed unused import --- openpype/hosts/tvpaint/api/pipeline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/tvpaint/api/pipeline.py b/openpype/hosts/tvpaint/api/pipeline.py index 58fbd09545..a84f196f09 100644 --- a/openpype/hosts/tvpaint/api/pipeline.py +++ b/openpype/hosts/tvpaint/api/pipeline.py @@ -7,7 +7,7 @@ import requests import pyblish.api -from openpype.client import get_project, get_asset_by_name +from openpype.client import get_asset_by_name from openpype.host import HostBase, IWorkfileHost, ILoadHost, IPublishHost from openpype.hosts.tvpaint import TVPAINT_ROOT_DIR from openpype.settings import get_current_project_settings From 9d77421d9de2b2a7cee1d8c9e32290a632f6ac80 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 2 Nov 2023 12:33:36 +0100 Subject: [PATCH 0689/1233] use AYON label when in AYON mode --- openpype/hosts/tvpaint/api/communication_server.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/tvpaint/api/communication_server.py b/openpype/hosts/tvpaint/api/communication_server.py index d67ef8f798..34302eef6e 100644 --- a/openpype/hosts/tvpaint/api/communication_server.py +++ b/openpype/hosts/tvpaint/api/communication_server.py @@ -21,6 +21,7 @@ from aiohttp_json_rpc.protocol import ( ) from aiohttp_json_rpc.exceptions import RpcError +from openpype import AYON_SERVER_ENABLED from openpype.lib import emit_event from openpype.hosts.tvpaint.tvpaint_plugin import get_plugin_files_path @@ -834,8 +835,9 @@ class BaseCommunicator: class QtCommunicator(BaseCommunicator): + title = "AYON Tools" if AYON_SERVER_ENABLED else "OpenPype Tools" menu_definitions = { - "title": "OpenPype Tools", + "title": title, "menu_items": [ { "callback": "workfiles_tool", From 0cc90ceb081a5988192a993374b488ea0051618f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 2 Nov 2023 12:33:46 +0100 Subject: [PATCH 0690/1233] removed unused 'previous_context' data --- openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py b/openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py index 95a5cd77bd..56b51c812a 100644 --- a/openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py +++ b/openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py @@ -69,7 +69,6 @@ class CollectWorkfileData(pyblish.api.ContextPlugin): "asset_name": context.data["asset"], "task_name": context.data["task"] } - context.data["previous_context"] = current_context self.log.debug("Current context is: {}".format(current_context)) # Collect context from workfile metadata From add4a1566d9dde015ac9b4422c0497f1c8710ab0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 2 Nov 2023 13:40:41 +0100 Subject: [PATCH 0691/1233] Use 'AVALON_LABEL' for label --- openpype/hosts/tvpaint/api/communication_server.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/tvpaint/api/communication_server.py b/openpype/hosts/tvpaint/api/communication_server.py index 34302eef6e..2c4d8160a6 100644 --- a/openpype/hosts/tvpaint/api/communication_server.py +++ b/openpype/hosts/tvpaint/api/communication_server.py @@ -835,7 +835,10 @@ class BaseCommunicator: class QtCommunicator(BaseCommunicator): - title = "AYON Tools" if AYON_SERVER_ENABLED else "OpenPype Tools" + label = os.getenv("AVALON_LABEL") + if not label: + label = "AYON" if AYON_SERVER_ENABLED else "OpenPype" + title = "{} Tools".format(label) menu_definitions = { "title": title, "menu_items": [ From e9699d2cef653a00b185ed04d7874c458ae18f94 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 2 Nov 2023 13:43:27 +0100 Subject: [PATCH 0692/1233] fix grammar and use warning Co-authored-by: Roy Nieterau --- openpype/modules/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 1a3280a6e5..2d3f0d4bc1 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -468,7 +468,7 @@ def _load_ayon_addons(openpype_modules, modules_key, log): continue if len(imported_modules) > 1: - log.info("More then one module '{}' was imported.".format(name)) + log.warning("More than one module '{}' was imported.".format(name)) continue mod = imported_modules[0] From 88116be4c63079598ec8a38b45ad3cc329155a87 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 2 Nov 2023 20:44:21 +0800 Subject: [PATCH 0693/1233] allows users to preset the settings before the creator setting --- .../hosts/max/plugins/create/create_review.py | 58 ++++++++--- openpype/plugins/publish/extract_review.py | 2 +- .../defaults/project_settings/max.json | 10 ++ .../projects_schema/schema_project_max.json | 98 +++++++++++++++++++ 4 files changed, 154 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/max/plugins/create/create_review.py b/openpype/hosts/max/plugins/create/create_review.py index 8052b74f06..67dc158001 100644 --- a/openpype/hosts/max/plugins/create/create_review.py +++ b/openpype/hosts/max/plugins/create/create_review.py @@ -12,6 +12,32 @@ class CreateReview(plugin.MaxCreator): family = "review" icon = "video-camera" + review_width = 1920 + review_height = 1080 + percentSize = 100 + keep_images = False + image_format = "png" + visual_style = "Realistic" + viewport_preset = "Quality" + vp_texture = True + anti_aliasing = None + + + @classmethod + def apply_settings(cls, project_settings): + settings = project_settings["max"]["PreviewAnimation"] # noqa + + # Take some defaults from settings + cls.review_width = settings.get("review_width", cls.review_width) + cls.review_height = settings.get("review_height", cls.review_height) + cls.percentSize = settings.get("percentSize", cls.percentSize) + cls.keep_images = settings.get("keep_images", cls.keep_images) + cls.image_format = settings.get("image_format", cls.image_format) + cls.visual_style = settings.get("visual_style", cls.visual_style) + cls.viewport_preset = settings.get("viewport_preset", cls.viewport_preset) + cls.vp_texture = settings.get("vp_texture", cls.vp_texture) + cls.anti_aliasing = settings.get("anti_aliasing", cls.anti_aliasing) + def create(self, subset_name, instance_data, pre_create_data): # Transfer settings from pre create to instance creator_attributes = instance_data.setdefault( @@ -23,6 +49,7 @@ class CreateReview(plugin.MaxCreator): "percentSize", "visualStyleMode", "viewportPreset", + "anti_aliasing", "vpTexture"]: if key in pre_create_data: creator_attributes[key] = pre_create_data[key] @@ -33,7 +60,7 @@ class CreateReview(plugin.MaxCreator): pre_create_data) def get_instance_attr_defs(self): - image_format_enum = ["exr", "jpg", "png"] + image_format_enum = ["exr", "jpg", "png", "tga"] visual_style_preset_enum = [ "Realistic", "Shaded", "Facets", @@ -45,41 +72,46 @@ class CreateReview(plugin.MaxCreator): preview_preset_enum = [ "Quality", "Standard", "Performance", "DXMode", "Customize"] + anti_aliasing_enum = ["None", "2X", "4X", "8X"] return [ NumberDef("review_width", label="Review width", decimals=0, minimum=0, - default=1920), + default=self.review_width), NumberDef("review_height", label="Review height", decimals=0, minimum=0, - default=1080), - BoolDef("keepImages", - label="Keep Image Sequences", - default=False), - EnumDef("imageFormat", - image_format_enum, - default="png", - label="Image Format Options"), + default=self.review_height), NumberDef("percentSize", label="Percent of Output", default=100, minimum=1, decimals=0), + BoolDef("keepImages", + label="Keep Image Sequences", + default=self.keep_images), + EnumDef("imageFormat", + image_format_enum, + default=self.image_format, + label="Image Format Options"), EnumDef("visualStyleMode", visual_style_preset_enum, - default="Realistic", + default=self.visual_style, label="Preference"), EnumDef("viewportPreset", preview_preset_enum, - default="Quality", + default=self.viewport_preset, label="Pre-View Preset"), + EnumDef("anti_aliasing", + anti_aliasing_enum, + default=self.anti_aliasing, + label="Anti-aliasing Quality"), BoolDef("vpTexture", label="Viewport Texture", - default=False) + default=self.vp_texture) ] def get_pre_create_attr_defs(self): diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index 0ae941511c..db8a030dfa 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -68,7 +68,7 @@ class ExtractReview(pyblish.api.InstancePlugin): ] # Supported extensions - image_exts = ["exr", "jpg", "jpeg", "png", "dpx"] + image_exts = ["exr", "jpg", "jpeg", "png", "dpx", "tga"] video_exts = ["mov", "mp4"] supported_exts = image_exts + video_exts diff --git a/openpype/settings/defaults/project_settings/max.json b/openpype/settings/defaults/project_settings/max.json index bfb1aa4aeb..c610a963d4 100644 --- a/openpype/settings/defaults/project_settings/max.json +++ b/openpype/settings/defaults/project_settings/max.json @@ -16,6 +16,16 @@ "image_format": "exr", "multipass": true }, + "PreviewAnimation": { + "review_width": 1920, + "review_height": 1080, + "percentSize": 100, + "keep_images": false, + "image_format": "png", + "visual_style": "Realistic", + "viewport_preset": "Quality", + "vp_texture": true + }, "PointCloud": { "attribute": { "Age": "age", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_max.json b/openpype/settings/entities/schemas/projects_schema/schema_project_max.json index e314174dff..b012e73fc4 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_max.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_max.json @@ -65,6 +65,104 @@ } ] }, + { + "type": "dict", + "collapsible": true, + "key": "PreviewAnimation", + "label": "Preview Animation", + "children": [ + { + "type": "number", + "key": "review_width", + "label": "Review Width" + }, + { + "type": "number", + "key": "review_height", + "label": "Review Height" + }, + { + "type": "number", + "key": "percentSize", + "label": "Percent of Output" + }, + { + "type": "boolean", + "key": "keep_images", + "label": "Keep Image Sequences" + }, + { + "key": "image_format", + "label": "Image Format Options", + "type": "enum", + "multiselection": false, + "defaults": "exr", + "enum_items": [ + {"exr": "exr"}, + {"jpg": "jpg"}, + {"png": "png"}, + {"tga": "tga"} + ] + }, + { + "key": "visual_style", + "label": "Preference", + "type": "enum", + "multiselection": false, + "defaults": "Realistic", + "enum_items": [ + {"Realistic": "Realistic"}, + {"Shaded": "Shaded"}, + {"Facets": "Facets"}, + {"ConsistentColors": "ConsistentColors"}, + {"HiddenLine": "HiddenLine"}, + {"Wireframe": "Wireframe"}, + {"BoundingBox": "BoundingBox"}, + {"Ink": "Ink"}, + {"ColorInk": "ColorInk"}, + {"Acrylic": "Acrylic"}, + {"Tech": "Tech"}, + {"Graphite": "Graphite"}, + {"ColorPencil": "ColorPencil"}, + {"Pastel": "Pastel"}, + {"Clay": "Clay"}, + {"ModelAssist": "ModelAssist"} + ] + }, + { + "key": "viewport_preset", + "label": "Pre-View Preset", + "type": "enum", + "multiselection": false, + "defaults": "Quality", + "enum_items": [ + {"Quality": "Quality"}, + {"Standard": "Standard"}, + {"Performance": "Performance"}, + {"DXMode": "DXMode"}, + {"Customize": "Customize"} + ] + }, + { + "key": "anti_aliasing", + "label": "Anti-aliasing Quality", + "type": "enum", + "multiselection": false, + "defaults": "None", + "enum_items": [ + {"None": "None"}, + {"2X": "2X"}, + {"4X": "4X"}, + {"8X": "8X"} + ] + }, + { + "type": "boolean", + "key": "vp_texture", + "label": "Viewport Texture" + } + ] + }, { "type": "dict", "collapsible": true, From 3428ec08a5592e54586ea8f0126e4907a8f4eeee Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 2 Nov 2023 13:46:05 +0100 Subject: [PATCH 0694/1233] log which method was used to get plugins --- openpype/modules/base.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 2d3f0d4bc1..03ec3d271a 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -1001,9 +1001,10 @@ class ModulesManager: paths = method(*args, **kwargs) except Exception: self.log.warning( - "Failed to get plugin paths from module {}.".format( - module.__class__.__name__ - ), + ( + "Failed to get plugin paths from module" + " '{}' using '{}'." + ).format(module.__class__.__name__, method_name), exc_info=True ) continue From 597260ad520393f2942b5685a817210d18544f45 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 2 Nov 2023 20:49:40 +0800 Subject: [PATCH 0695/1233] cosmetic fix --- openpype/settings/defaults/project_settings/max.json | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/settings/defaults/project_settings/max.json b/openpype/settings/defaults/project_settings/max.json index c610a963d4..ac04c60b54 100644 --- a/openpype/settings/defaults/project_settings/max.json +++ b/openpype/settings/defaults/project_settings/max.json @@ -24,6 +24,7 @@ "image_format": "png", "visual_style": "Realistic", "viewport_preset": "Quality", + "anti_aliasing": "None", "vp_texture": true }, "PointCloud": { From 026aae1d0d9ebc6409acedc545c9d20950b5365a Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 2 Nov 2023 21:35:18 +0800 Subject: [PATCH 0696/1233] add AA quality setting options & add the correct aspect ratio --- openpype/hosts/max/api/preview_animation.py | 4 +++- openpype/hosts/max/plugins/create/create_review.py | 8 +++++++- openpype/hosts/max/plugins/publish/collect_review.py | 1 + openpype/plugins/publish/extract_review.py | 2 +- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/max/api/preview_animation.py b/openpype/hosts/max/api/preview_animation.py index dcf243d31e..5f36b12edb 100644 --- a/openpype/hosts/max/api/preview_animation.py +++ b/openpype/hosts/max/api/preview_animation.py @@ -189,7 +189,8 @@ def _render_preview_animation_max_pre_2024( dib = rt.gw.getViewportDib() dib_width = float(dib.width) dib_height = float(dib.height) - renderRatio = float(dib_width / dib_height) + # aspect ratio + renderRatio = rt.getRendImageAspect() if viewportRatio <= renderRatio: heightCrop = (dib_width / renderRatio) topEdge = int((dib_height - heightCrop) / 2.0) @@ -311,6 +312,7 @@ def viewport_options_for_preview_animation(): viewport_options["nitrous_viewport"] = { "VisualStyleMode": "defaultshading", "ViewportPreset": "highquality", + "AntialiasingQuality": "None", "UseTextureEnabled": False } viewport_options["vp_btn_mgr"] = { diff --git a/openpype/hosts/max/plugins/create/create_review.py b/openpype/hosts/max/plugins/create/create_review.py index 8052b74f06..331d2f30ea 100644 --- a/openpype/hosts/max/plugins/create/create_review.py +++ b/openpype/hosts/max/plugins/create/create_review.py @@ -23,6 +23,7 @@ class CreateReview(plugin.MaxCreator): "percentSize", "visualStyleMode", "viewportPreset", + "antialiasingQuality", "vpTexture"]: if key in pre_create_data: creator_attributes[key] = pre_create_data[key] @@ -33,7 +34,7 @@ class CreateReview(plugin.MaxCreator): pre_create_data) def get_instance_attr_defs(self): - image_format_enum = ["exr", "jpg", "png"] + image_format_enum = ["exr", "jpg", "png", "tga"] visual_style_preset_enum = [ "Realistic", "Shaded", "Facets", @@ -45,6 +46,7 @@ class CreateReview(plugin.MaxCreator): preview_preset_enum = [ "Quality", "Standard", "Performance", "DXMode", "Customize"] + anti_aliasing_enum = ["None", "2X", "4X", "8X"] return [ NumberDef("review_width", @@ -77,6 +79,10 @@ class CreateReview(plugin.MaxCreator): preview_preset_enum, default="Quality", label="Pre-View Preset"), + EnumDef("antialiasingQuality", + anti_aliasing_enum, + default="None", + label="Anti-aliasing Quality"), BoolDef("vpTexture", label="Viewport Texture", default=False) diff --git a/openpype/hosts/max/plugins/publish/collect_review.py b/openpype/hosts/max/plugins/publish/collect_review.py index b1d9c2d25e..a579b3f4b0 100644 --- a/openpype/hosts/max/plugins/publish/collect_review.py +++ b/openpype/hosts/max/plugins/publish/collect_review.py @@ -93,6 +93,7 @@ class CollectReview(pyblish.api.InstancePlugin, nitrous_viewport = { "VisualStyleMode": creator_attrs["visualStyleMode"], "ViewportPreset": creator_attrs["viewportPreset"], + "AntialiasingQuality": creator_attrs["antialiasingQuality"], "UseTextureEnabled": creator_attrs["vpTexture"] } preview_data = { diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index 0ae941511c..db8a030dfa 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -68,7 +68,7 @@ class ExtractReview(pyblish.api.InstancePlugin): ] # Supported extensions - image_exts = ["exr", "jpg", "jpeg", "png", "dpx"] + image_exts = ["exr", "jpg", "jpeg", "png", "dpx", "tga"] video_exts = ["mov", "mp4"] supported_exts = image_exts + video_exts From 2d940227b1144e67c0dc37384575aea067a37d25 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 2 Nov 2023 14:54:29 +0100 Subject: [PATCH 0697/1233] skip openpype addon --- openpype/modules/base.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 03ec3d271a..aa3deff475 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -408,6 +408,10 @@ def _load_ayon_addons(openpype_modules, modules_key, log): addon_name = addon_info["name"] addon_version = addon_info["version"] + # OpenPype addon does not have any addon object + if addon_name == "openpype": + continue + dev_addon_info = dev_addons_info.get(addon_name, {}) use_dev_path = dev_addon_info.get("enabled", False) From 27dd549c7dae21960f38e548598da4d884b61cd8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 2 Nov 2023 14:54:40 +0100 Subject: [PATCH 0698/1233] ignore pycahce folders --- openpype/modules/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index aa3deff475..df7286e7b7 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -442,7 +442,7 @@ def _load_ayon_addons(openpype_modules, modules_key, log): # Ignore of files is implemented to be able to run code from code # where usually is more files than just the addon # Ignore start and setup scripts - if name in ("setup.py", "start.py"): + if name in ("setup.py", "start.py", "__pycache__"): continue path = os.path.join(addon_dir, name) From 87c3682d61392b1828e6b4c09060a77400f14b00 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 2 Nov 2023 14:55:06 +0100 Subject: [PATCH 0699/1233] imported modules must have Module class --- openpype/modules/base.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index df7286e7b7..eb6e7d6b73 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -458,7 +458,15 @@ def _load_ayon_addons(openpype_modules, modules_key, log): try: mod = __import__(basename, fromlist=("",)) - imported_modules.append(mod) + for attr_name in dir(mod): + attr = getattr(mod, attr_name) + if ( + inspect.isclass(attr) + and issubclass(attr, OpenPypeModule) + ): + imported_modules.append(mod) + break + except BaseException: log.warning( "Failed to import \"{}\"".format(basename), From db26cdd6e35a08d0025dcf8a54024b17af484099 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 2 Nov 2023 21:55:22 +0800 Subject: [PATCH 0700/1233] hound --- openpype/hosts/max/plugins/create/create_review.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/max/plugins/create/create_review.py b/openpype/hosts/max/plugins/create/create_review.py index 67dc158001..ed0359ebd7 100644 --- a/openpype/hosts/max/plugins/create/create_review.py +++ b/openpype/hosts/max/plugins/create/create_review.py @@ -22,7 +22,6 @@ class CreateReview(plugin.MaxCreator): vp_texture = True anti_aliasing = None - @classmethod def apply_settings(cls, project_settings): settings = project_settings["max"]["PreviewAnimation"] # noqa @@ -34,9 +33,11 @@ class CreateReview(plugin.MaxCreator): cls.keep_images = settings.get("keep_images", cls.keep_images) cls.image_format = settings.get("image_format", cls.image_format) cls.visual_style = settings.get("visual_style", cls.visual_style) - cls.viewport_preset = settings.get("viewport_preset", cls.viewport_preset) + cls.viewport_preset = settings.get( + "viewport_preset", cls.viewport_preset) cls.vp_texture = settings.get("vp_texture", cls.vp_texture) - cls.anti_aliasing = settings.get("anti_aliasing", cls.anti_aliasing) + cls.anti_aliasing = settings.get( + "anti_aliasing", cls.anti_aliasing) def create(self, subset_name, instance_data, pre_create_data): # Transfer settings from pre create to instance @@ -49,7 +50,7 @@ class CreateReview(plugin.MaxCreator): "percentSize", "visualStyleMode", "viewportPreset", - "anti_aliasing", + "antialiasingQuality", "vpTexture"]: if key in pre_create_data: creator_attributes[key] = pre_create_data[key] @@ -105,7 +106,7 @@ class CreateReview(plugin.MaxCreator): preview_preset_enum, default=self.viewport_preset, label="Pre-View Preset"), - EnumDef("anti_aliasing", + EnumDef("antialiasingQuality", anti_aliasing_enum, default=self.anti_aliasing, label="Anti-aliasing Quality"), From 541d333ab8f2ee0b92bab36d96b8c07e7a2457dc Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 2 Nov 2023 14:56:33 +0100 Subject: [PATCH 0701/1233] more specific message when loaded multiple modules in addon dir --- openpype/modules/base.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index eb6e7d6b73..457e29905d 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -480,7 +480,14 @@ def _load_ayon_addons(openpype_modules, modules_key, log): continue if len(imported_modules) > 1: - log.warning("More than one module '{}' was imported.".format(name)) + log.warning(( + "Skipping addon '{}'." + " Multiple modules were found ({}) in dir {}." + ).format( + addon_name, + ", ".join([m.__name__ for m in imported_modules]), + addon_dir, + )) continue mod = imported_modules[0] From 487d8dfde72a9af0b2964a15d4b57a07954ce767 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 2 Nov 2023 22:34:20 +0800 Subject: [PATCH 0702/1233] make sure the code doesn't break the extractor --- openpype/hosts/max/api/preview_animation.py | 14 +++++++++++++- .../hosts/max/plugins/publish/collect_review.py | 5 ++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/api/preview_animation.py b/openpype/hosts/max/api/preview_animation.py index 5f36b12edb..0754fa61c4 100644 --- a/openpype/hosts/max/api/preview_animation.py +++ b/openpype/hosts/max/api/preview_animation.py @@ -52,6 +52,7 @@ def viewport_layout_and_camera(camera, layout="layout_1"): @contextlib.contextmanager def viewport_preference_setting(general_viewport, + nitrous_manager, nitrous_viewport, vp_button_mgr): """Function to set viewport setting during context @@ -59,6 +60,7 @@ def viewport_preference_setting(general_viewport, Args: camera (str): Viewport camera for review render general_viewport (dict): General viewport setting + nitrous_manager (dict): Nitrous graphic manager nitrous_viewport (dict): Nitrous setting for preview animation vp_button_mgr (dict): Viewport button manager Setting @@ -72,6 +74,9 @@ def viewport_preference_setting(general_viewport, vp_button_mgr_original = { key: getattr(rt.ViewportButtonMgr, key) for key in vp_button_mgr } + nitrous_manager_original = { + key: getattr(nitrousGraphicMgr, key) for key in nitrous_manager + } nitrous_viewport_original = { key: getattr(viewport_setting, key) for key in nitrous_viewport } @@ -81,6 +86,8 @@ def viewport_preference_setting(general_viewport, rt.viewport.EnableSolidBackgroundColorMode(general_viewport["dspBkg"]) for key, value in vp_button_mgr.items(): setattr(rt.ViewportButtonMgr, key, value) + for key, value in nitrous_manager.items(): + setattr(nitrousGraphicMgr, key, value) for key, value in nitrous_viewport.items(): if nitrous_viewport[key] != nitrous_viewport_original[key]: setattr(viewport_setting, key, value) @@ -91,6 +98,8 @@ def viewport_preference_setting(general_viewport, rt.viewport.EnableSolidBackgroundColorMode(orig_vp_bkg) for key, value in vp_button_mgr_original.items(): setattr(rt.ViewportButtonMgr, key, value) + for key, value in nitrous_manager_original.items(): + setattr(nitrousGraphicMgr, key, value) for key, value in nitrous_viewport_original.items(): setattr(viewport_setting, key, value) @@ -258,6 +267,7 @@ def render_preview_animation( if int(get_max_version()) < 2024: with viewport_preference_setting( viewport_options["general_viewport"], + viewport_options["nitrous_manager"], viewport_options["nitrous_viewport"], viewport_options["vp_btn_mgr"] ): @@ -309,10 +319,12 @@ def viewport_options_for_preview_animation(): "dspBkg": True, "dspGrid": False } + viewport_options["nitrous_manager"] = { + "AntialiasingQuality": "None" + } viewport_options["nitrous_viewport"] = { "VisualStyleMode": "defaultshading", "ViewportPreset": "highquality", - "AntialiasingQuality": "None", "UseTextureEnabled": False } viewport_options["vp_btn_mgr"] = { diff --git a/openpype/hosts/max/plugins/publish/collect_review.py b/openpype/hosts/max/plugins/publish/collect_review.py index a579b3f4b0..e7e957e6f1 100644 --- a/openpype/hosts/max/plugins/publish/collect_review.py +++ b/openpype/hosts/max/plugins/publish/collect_review.py @@ -90,14 +90,17 @@ class CollectReview(pyblish.api.InstancePlugin, "dspBkg": attr_values.get("dspBkg"), "dspGrid": attr_values.get("dspGrid") } + nitrous_manager = { + "AntialiasingQuality": creator_attrs["antialiasingQuality"], + } nitrous_viewport = { "VisualStyleMode": creator_attrs["visualStyleMode"], "ViewportPreset": creator_attrs["viewportPreset"], - "AntialiasingQuality": creator_attrs["antialiasingQuality"], "UseTextureEnabled": creator_attrs["vpTexture"] } preview_data = { "general_viewport": general_viewport, + "nitrous_manager": nitrous_manager, "nitrous_viewport": nitrous_viewport, "vp_btn_mgr": {"EnableButtons": False} } From ae536f2409cf9b622b617150dbd9778b6c180e91 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 2 Nov 2023 16:15:03 +0100 Subject: [PATCH 0703/1233] traypublisher: folder path implementation --- openpype/hosts/traypublisher/api/editorial.py | 6 +++++- .../hosts/traypublisher/plugins/create/create_editorial.py | 4 +++- .../traypublisher/plugins/publish/collect_shot_instances.py | 1 - 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/traypublisher/api/editorial.py b/openpype/hosts/traypublisher/api/editorial.py index e8f76bd314..2f5e709ffc 100644 --- a/openpype/hosts/traypublisher/api/editorial.py +++ b/openpype/hosts/traypublisher/api/editorial.py @@ -319,8 +319,12 @@ class ShotMetadataSolver: tasks = self._generate_tasks_from_settings( project_doc) + # generate hierarchy path from parents + hierarchy_path = self._create_hierarchy_path(parents) + return shot_name, { - "hierarchy": self._create_hierarchy_path(parents), + "hierarchy": hierarchy_path, + "folderPath": f"{hierarchy_path}/{shot_name}", "parents": parents, "tasks": tasks } diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py index 23cf066362..5dc3893697 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_editorial.py +++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py @@ -217,9 +217,10 @@ or updating already created. Publishing will create OTIO file. } # Create otio editorial instance if AYON_SERVER_ENABLED: - asset_name = instance_data["folderPath"] + asset_name = instance_data.pop("folderPath") else: asset_name = instance_data["asset"] + asset_doc = get_asset_by_name(self.project_name, asset_name) if pre_create_data["fps"] == "from_selection": @@ -682,6 +683,7 @@ or updating already created. Publishing will create OTIO file. # create creator attributes creator_attributes = { "asset_name": shot_name, + "Folder path": shot_metadata["folderPath"], "Parent hierarchy path": shot_metadata["hierarchy"], "workfile_start_frame": workfile_start_frame, "fps": fps, diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py b/openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py index 78c1f14e4e..b08397caf7 100644 --- a/openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py +++ b/openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py @@ -120,7 +120,6 @@ class CollectShotInstance(pyblish.api.InstancePlugin): frame_dur = frame_end - frame_start return { - "asset": _cr_attrs["asset_name"], "fps": float(_cr_attrs["fps"]), "handleStart": _cr_attrs["handle_start"], "handleEnd": _cr_attrs["handle_end"], From 976ff308e97abed1efd6b87f09c477ad16c8f329 Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Thu, 2 Nov 2023 16:18:51 +0100 Subject: [PATCH 0704/1233] Add Create workfile plugin --- .../blender/plugins/create/create_workfile.py | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 openpype/hosts/blender/plugins/create/create_workfile.py diff --git a/openpype/hosts/blender/plugins/create/create_workfile.py b/openpype/hosts/blender/plugins/create/create_workfile.py new file mode 100644 index 0000000000..d1529f75f6 --- /dev/null +++ b/openpype/hosts/blender/plugins/create/create_workfile.py @@ -0,0 +1,100 @@ +import bpy + +from openpype.pipeline import CreatedInstance, AutoCreator +from openpype.client import get_asset_by_name +from openpype.hosts.blender.api.plugin import BaseCreator +from openpype.hosts.blender.api.lib import imprint +from openpype.hosts.blender.api.pipeline import AVALON_PROPERTY + + +class CreateWorkfile(BaseCreator, AutoCreator): + """Workfile auto-creator.""" + identifier = "io.openpype.creators.blender.workfile" + label = "Workfile" + family = "workfile" + icon = "fa5.file" + + def create(self): + """Create workfile instances.""" + current_instance = next( + ( + instance for instance in self.create_context.instances + if instance.creator_identifier == self.identifier + ), + None, + ) + + project_name = self.project_name + asset_name = self.create_context.get_current_asset_name() + task_name = self.create_context.get_current_task_name() + host_name = self.create_context.host_name + + if not current_instance: + asset_doc = get_asset_by_name(project_name, asset_name) + subset_name = self.get_subset_name( + task_name, task_name, asset_doc, project_name, host_name + ) + data = { + "asset": asset_name, + "task": task_name, + "variant": task_name, + } + data.update( + self.get_dynamic_data( + task_name, + task_name, + asset_doc, + project_name, + host_name, + current_instance, + ) + ) + self.log.info("Auto-creating workfile instance...") + current_instance = CreatedInstance( + self.family, subset_name, data, self + ) + self._add_instance_to_context(current_instance) + elif ( + current_instance["asset"] != asset_name + or current_instance["task"] != task_name + ): + # Update instance context if it's different + asset_doc = get_asset_by_name(project_name, asset_name) + subset_name = self.get_subset_name( + task_name, task_name, asset_doc, project_name, host_name + ) + + current_instance.update( + { + "asset": asset_name, + "task": task_name, + "subset": subset_name, + } + ) + + def collect_instances(self): + """Collect workfile instances.""" + self.cache_subsets(self.collection_shared_data) + cached_subsets = self.collection_shared_data["blender_cached_subsets"] + for node in cached_subsets.get(self.identifier, []): + created_instance = CreatedInstance.from_existing( + self.read_instance_node(node), self + ) + self._add_instance_to_context(created_instance) + + def update_instances(self, update_list): + """Update workfile instances.""" + for created_inst, _changes in update_list: + data = created_inst.data_to_store() + node = data.get("instance_node") + if not node: + task_name = self.create_context.get_current_task_name() + + bpy.context.scene[AVALON_PROPERTY] = node = { + "name": f"workfile{task_name}" + } + + created_inst["instance_node"] = node + data = created_inst.data_to_store() + + imprint(node, data) From 6a619d023b4f4bac7d48883bd16081193950d950 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 3 Nov 2023 00:53:37 +0100 Subject: [PATCH 0705/1233] Remove on instance toggled callback which isn't relevant to the new publisher --- openpype/hosts/houdini/api/pipeline.py | 56 ------------------- .../publish/collect_instances_usd_layered.py | 4 -- 2 files changed, 60 deletions(-) diff --git a/openpype/hosts/houdini/api/pipeline.py b/openpype/hosts/houdini/api/pipeline.py index f8db45c56b..11135e20b2 100644 --- a/openpype/hosts/houdini/api/pipeline.py +++ b/openpype/hosts/houdini/api/pipeline.py @@ -3,7 +3,6 @@ import os import sys import logging -import contextlib import hou # noqa @@ -66,10 +65,6 @@ class HoudiniHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): register_event_callback("open", on_open) register_event_callback("new", on_new) - pyblish.api.register_callback( - "instanceToggled", on_pyblish_instance_toggled - ) - self._has_been_setup = True # add houdini vendor packages hou_pythonpath = os.path.join(HOUDINI_HOST_DIR, "vendor") @@ -406,54 +401,3 @@ def _set_context_settings(): lib.reset_framerange() lib.update_houdini_vars_context() - - -def on_pyblish_instance_toggled(instance, new_value, old_value): - """Toggle saver tool passthrough states on instance toggles.""" - @contextlib.contextmanager - def main_take(no_update=True): - """Enter root take during context""" - original_take = hou.takes.currentTake() - original_update_mode = hou.updateModeSetting() - root = hou.takes.rootTake() - has_changed = False - try: - if original_take != root: - has_changed = True - if no_update: - hou.setUpdateMode(hou.updateMode.Manual) - hou.takes.setCurrentTake(root) - yield - finally: - if has_changed: - if no_update: - hou.setUpdateMode(original_update_mode) - hou.takes.setCurrentTake(original_take) - - if not instance.data.get("_allowToggleBypass", True): - return - - nodes = instance[:] - if not nodes: - return - - # Assume instance node is first node - instance_node = nodes[0] - - if not hasattr(instance_node, "isBypassed"): - # Likely not a node that can actually be bypassed - log.debug("Can't bypass node: %s", instance_node.path()) - return - - if instance_node.isBypassed() != (not old_value): - print("%s old bypass state didn't match old instance state, " - "updating anyway.." % instance_node.path()) - - try: - # Go into the main take, because when in another take changing - # the bypass state of a note cannot be done due to it being locked - # by default. - with main_take(no_update=True): - instance_node.bypass(not new_value) - except hou.PermissionError as exc: - log.warning("%s - %s", instance_node.path(), exc) diff --git a/openpype/hosts/houdini/plugins/publish/collect_instances_usd_layered.py b/openpype/hosts/houdini/plugins/publish/collect_instances_usd_layered.py index 0600730d00..d154cdc7c0 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_instances_usd_layered.py +++ b/openpype/hosts/houdini/plugins/publish/collect_instances_usd_layered.py @@ -122,10 +122,6 @@ class CollectInstancesUsdLayered(pyblish.api.ContextPlugin): instance.data.update(save_data) instance.data["usdLayer"] = layer - # Don't allow the Pyblish `instanceToggled` we have installed - # to set this node to bypass. - instance.data["_allowToggleBypass"] = False - instances.append(instance) # Store the collected ROP node dependencies From d94c19d0ba44a5d48b8964fe3fa6d98a29b3f0da Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 3 Nov 2023 00:59:42 +0100 Subject: [PATCH 0706/1233] Remove non-working LOPs USD output processors - These do not show up in recent Houdini versions - The recent output processor API is very different and processors should be registered differently - These are non-functional in current OpenPype code --- openpype/hosts/houdini/api/pipeline.py | 4 - .../vendor/husdoutputprocessors/__init__.py | 1 - .../avalon_uri_processor.py | 152 ------------------ .../stagingdir_processor.py | 90 ----------- 4 files changed, 247 deletions(-) delete mode 100644 openpype/hosts/houdini/vendor/husdoutputprocessors/__init__.py delete mode 100644 openpype/hosts/houdini/vendor/husdoutputprocessors/avalon_uri_processor.py delete mode 100644 openpype/hosts/houdini/vendor/husdoutputprocessors/stagingdir_processor.py diff --git a/openpype/hosts/houdini/api/pipeline.py b/openpype/hosts/houdini/api/pipeline.py index f8db45c56b..095004ea6f 100644 --- a/openpype/hosts/houdini/api/pipeline.py +++ b/openpype/hosts/houdini/api/pipeline.py @@ -71,10 +71,6 @@ class HoudiniHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): ) self._has_been_setup = True - # add houdini vendor packages - hou_pythonpath = os.path.join(HOUDINI_HOST_DIR, "vendor") - - sys.path.append(hou_pythonpath) # Set asset settings for the empty scene directly after launch of # Houdini so it initializes into the correct scene FPS, diff --git a/openpype/hosts/houdini/vendor/husdoutputprocessors/__init__.py b/openpype/hosts/houdini/vendor/husdoutputprocessors/__init__.py deleted file mode 100644 index 69e3be50da..0000000000 --- a/openpype/hosts/houdini/vendor/husdoutputprocessors/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__path__ = __import__('pkgutil').extend_path(__path__, __name__) diff --git a/openpype/hosts/houdini/vendor/husdoutputprocessors/avalon_uri_processor.py b/openpype/hosts/houdini/vendor/husdoutputprocessors/avalon_uri_processor.py deleted file mode 100644 index 310d057a11..0000000000 --- a/openpype/hosts/houdini/vendor/husdoutputprocessors/avalon_uri_processor.py +++ /dev/null @@ -1,152 +0,0 @@ -import os -import hou -import husdoutputprocessors.base as base - -import colorbleed.usdlib as usdlib - -from openpype.client import get_asset_by_name -from openpype.pipeline import Anatomy, get_current_project_name - - -class AvalonURIOutputProcessor(base.OutputProcessorBase): - """Process Avalon URIs into their full path equivalents. - - """ - - _parameters = None - _param_prefix = 'avalonurioutputprocessor_' - _parms = { - "use_publish_paths": _param_prefix + "use_publish_paths" - } - - def __init__(self): - """ There is only one object of each output processor class that is - ever created in a Houdini session. Therefore be very careful - about what data gets put in this object. - """ - self._use_publish_paths = False - self._cache = dict() - - def displayName(self): - return 'Avalon URI Output Processor' - - def parameters(self): - - if not self._parameters: - parameters = hou.ParmTemplateGroup() - use_publish_path = hou.ToggleParmTemplate( - name=self._parms["use_publish_paths"], - label='Resolve Reference paths to publish paths', - default_value=False, - help=("When enabled any paths for Layers, References or " - "Payloads are resolved to published master versions.\n" - "This is usually only used by the publishing pipeline, " - "but can be used for testing too.")) - parameters.append(use_publish_path) - self._parameters = parameters.asDialogScript() - - return self._parameters - - def beginSave(self, config_node, t): - parm = self._parms["use_publish_paths"] - self._use_publish_paths = config_node.parm(parm).evalAtTime(t) - self._cache.clear() - - def endSave(self): - self._use_publish_paths = None - self._cache.clear() - - def processAsset(self, - asset_path, - asset_path_for_save, - referencing_layer_path, - asset_is_layer, - for_save): - """ - Args: - asset_path (str): The incoming file path you want to alter or not. - asset_path_for_save (bool): Whether the current path is a - referenced path in the USD file. When True, return the path - you want inside USD file. - referencing_layer_path (str): ??? - asset_is_layer (bool): Whether this asset is a USD layer file. - If this is False, the asset is something else (for example, - a texture or volume file). - for_save (bool): Whether the asset path is for a file to be saved - out. If so, then return actual written filepath. - - Returns: - The refactored asset path. - - """ - - # Retrieve from cache if this query occurred before (optimization) - cache_key = (asset_path, asset_path_for_save, asset_is_layer, for_save) - if cache_key in self._cache: - return self._cache[cache_key] - - relative_template = "{asset}_{subset}.{ext}" - uri_data = usdlib.parse_avalon_uri(asset_path) - if uri_data: - - if for_save: - # Set save output path to a relative path so other - # processors can potentially manage it easily? - path = relative_template.format(**uri_data) - - print("Avalon URI Resolver: %s -> %s" % (asset_path, path)) - self._cache[cache_key] = path - return path - - if self._use_publish_paths: - # Resolve to an Avalon published asset for embedded paths - path = self._get_usd_master_path(**uri_data) - else: - path = relative_template.format(**uri_data) - - print("Avalon URI Resolver: %s -> %s" % (asset_path, path)) - self._cache[cache_key] = path - return path - - self._cache[cache_key] = asset_path - return asset_path - - def _get_usd_master_path(self, - asset, - subset, - ext): - """Get the filepath for a .usd file of a subset. - - This will return the path to an unversioned master file generated by - `usd_master_file.py`. - - """ - - PROJECT = get_current_project_name() - anatomy = Anatomy(PROJECT) - asset_doc = get_asset_by_name(PROJECT, asset) - if not asset_doc: - raise RuntimeError("Invalid asset name: '%s'" % asset) - - template_obj = anatomy.templates_obj["publish"]["path"] - path = template_obj.format_strict({ - "project": PROJECT, - "asset": asset_doc["name"], - "subset": subset, - "representation": ext, - "version": 0 # stub version zero - }) - - # Remove the version folder - subset_folder = os.path.dirname(os.path.dirname(path)) - master_folder = os.path.join(subset_folder, "master") - fname = "{0}.{1}".format(subset, ext) - - return os.path.join(master_folder, fname).replace("\\", "/") - - -output_processor = AvalonURIOutputProcessor() - - -def usdOutputProcessor(): - return output_processor diff --git a/openpype/hosts/houdini/vendor/husdoutputprocessors/stagingdir_processor.py b/openpype/hosts/houdini/vendor/husdoutputprocessors/stagingdir_processor.py deleted file mode 100644 index d8e36d5aa8..0000000000 --- a/openpype/hosts/houdini/vendor/husdoutputprocessors/stagingdir_processor.py +++ /dev/null @@ -1,90 +0,0 @@ -import hou -import husdoutputprocessors.base as base -import os - - -class StagingDirOutputProcessor(base.OutputProcessorBase): - """Output all USD Rop file nodes into the Staging Directory - - Ignore any folders and paths set in the Configured Layers - and USD Rop node, just take the filename and save into a - single directory. - - """ - theParameters = None - parameter_prefix = "stagingdiroutputprocessor_" - stagingdir_parm_name = parameter_prefix + "stagingDir" - - def __init__(self): - self.staging_dir = None - - def displayName(self): - return 'StagingDir Output Processor' - - def parameters(self): - if not self.theParameters: - parameters = hou.ParmTemplateGroup() - rootdirparm = hou.StringParmTemplate( - self.stagingdir_parm_name, - 'Staging Directory', 1, - string_type=hou.stringParmType.FileReference, - file_type=hou.fileType.Directory - ) - parameters.append(rootdirparm) - self.theParameters = parameters.asDialogScript() - return self.theParameters - - def beginSave(self, config_node, t): - - # Use the Root Directory parameter if it is set. - root_dir_parm = config_node.parm(self.stagingdir_parm_name) - if root_dir_parm: - self.staging_dir = root_dir_parm.evalAtTime(t) - - if not self.staging_dir: - out_file_parm = config_node.parm('lopoutput') - if out_file_parm: - self.staging_dir = out_file_parm.evalAtTime(t) - if self.staging_dir: - (self.staging_dir, filename) = os.path.split(self.staging_dir) - - def endSave(self): - self.staging_dir = None - - def processAsset(self, asset_path, - asset_path_for_save, - referencing_layer_path, - asset_is_layer, - for_save): - """ - Args: - asset_path (str): The incoming file path you want to alter or not. - asset_path_for_save (bool): Whether the current path is a - referenced path in the USD file. When True, return the path - you want inside USD file. - referencing_layer_path (str): ??? - asset_is_layer (bool): Whether this asset is a USD layer file. - If this is False, the asset is something else (for example, - a texture or volume file). - for_save (bool): Whether the asset path is for a file to be saved - out. If so, then return actual written filepath. - - Returns: - The refactored asset path. - - """ - - # Treat save paths as being relative to the output path. - if for_save and self.staging_dir: - # Whenever we're processing a Save Path make sure to - # resolve it to the Staging Directory - filename = os.path.basename(asset_path) - return os.path.join(self.staging_dir, filename) - - return asset_path - - -output_processor = StagingDirOutputProcessor() -def usdOutputProcessor(): - return output_processor - From 1e4005f4454efa4ea78f3bb14c0f96de400b8734 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 3 Nov 2023 16:01:44 +0800 Subject: [PATCH 0707/1233] add AYON settings support and finalize the settings --- .../hosts/max/plugins/create/create_review.py | 2 +- server_addon/max/server/settings/main.py | 8 ++ .../max/server/settings/preview_animation.py | 92 +++++++++++++++++++ server_addon/max/server/version.py | 2 +- 4 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 server_addon/max/server/settings/preview_animation.py diff --git a/openpype/hosts/max/plugins/create/create_review.py b/openpype/hosts/max/plugins/create/create_review.py index ed0359ebd7..7aeea39b64 100644 --- a/openpype/hosts/max/plugins/create/create_review.py +++ b/openpype/hosts/max/plugins/create/create_review.py @@ -35,9 +35,9 @@ class CreateReview(plugin.MaxCreator): cls.visual_style = settings.get("visual_style", cls.visual_style) cls.viewport_preset = settings.get( "viewport_preset", cls.viewport_preset) - cls.vp_texture = settings.get("vp_texture", cls.vp_texture) cls.anti_aliasing = settings.get( "anti_aliasing", cls.anti_aliasing) + cls.vp_texture = settings.get("vp_texture", cls.vp_texture) def create(self, subset_name, instance_data, pre_create_data): # Transfer settings from pre create to instance diff --git a/server_addon/max/server/settings/main.py b/server_addon/max/server/settings/main.py index 7f4561cbb1..0280fcebb9 100644 --- a/server_addon/max/server/settings/main.py +++ b/server_addon/max/server/settings/main.py @@ -4,6 +4,9 @@ from .imageio import ImageIOSettings from .render_settings import ( RenderSettingsModel, DEFAULT_RENDER_SETTINGS ) +from .preview_animation import ( + PreviewAnimationModel, DEFAULT_PREVIEW_ANIMATION_SETTINGS +) from .publishers import ( PublishersModel, DEFAULT_PUBLISH_SETTINGS ) @@ -29,6 +32,10 @@ class MaxSettings(BaseSettingsModel): default_factory=RenderSettingsModel, title="Render Settings" ) + PreviewAnimation: PreviewAnimationModel = Field( + default_factory=PreviewAnimationModel, + title="Preview Animation" + ) PointCloud: PointCloudSettings = Field( default_factory=PointCloudSettings, title="Point Cloud" @@ -40,6 +47,7 @@ class MaxSettings(BaseSettingsModel): DEFAULT_VALUES = { "RenderSettings": DEFAULT_RENDER_SETTINGS, + "PreviewAnimation": DEFAULT_PREVIEW_ANIMATION_SETTINGS, "PointCloud": { "attribute": [ {"name": "Age", "value": "age"}, diff --git a/server_addon/max/server/settings/preview_animation.py b/server_addon/max/server/settings/preview_animation.py new file mode 100644 index 0000000000..2496e8e548 --- /dev/null +++ b/server_addon/max/server/settings/preview_animation.py @@ -0,0 +1,92 @@ +from pydantic import Field + +from ayon_server.settings import BaseSettingsModel + + +def image_format_enum(): + """Return enumerator for image output formats.""" + return [ + {"label": "exr", "value": "exr"}, + {"label": "jpg", "value": "jpg"}, + {"label": "png", "value": "png"}, + {"label": "tga", "value": "tga"} + ] + + +def visual_style_enum(): + """Return enumerator for viewport visual style.""" + return [ + {"label": "Realistic", "value": "Realistic"}, + {"label": "Shaded", "value": "Shaded"}, + {"label": "Facets", "value": "Facets"}, + {"label": "ConsistentColors", + "value": "ConsistentColors"}, + {"label": "Wireframe", "value": "Wireframe"}, + {"label": "BoundingBox", "value": "BoundingBox"}, + {"label": "Ink", "value": "Ink"}, + {"label": "ColorInk", "value": "ColorInk"}, + {"label": "Acrylic", "value": "Acrylic"}, + {"label": "Tech", "value": "Tech"}, + {"label": "Graphite", "value": "Graphite"}, + {"label": "ColorPencil", "value": "ColorPencil"}, + {"label": "Pastel", "value": "Pastel"}, + {"label": "Clay", "value": "Clay"}, + {"label": "ModelAssist", "value": "ModelAssist"} + ] + +def visual_preset_enum(): + """Return enumerator for viewport visual preset.""" + return [ + {"label": "Quality", "value": "Quality"}, + {"label": "Standard", "value": "Standard"}, + {"label": "Performance", "value": "Performance"}, + {"label": "DXMode", "value": "DXMode"}, + {"label": "Customize", "value": "Customize"}, + ] + + +def anti_aliasing_enum(): + """Return enumerator for viewport anti-aliasing.""" + return [ + {"label": "None", "value": "None"}, + {"label": "2X", "value": "2X"}, + {"label": "4X", "value": "4X"}, + {"label": "8X", "value": "8X"} + ] + + +class PreviewAnimationModel(BaseSettingsModel): + review_width: int = Field(1920, title="Review Width") + review_height: int = Field(1080, title="Review Height") + percentSize: float = Field(100.0, title="Percent of Output") + keep_images: bool = Field(False, title="Keep Image Sequences") + image_format: str = Field( + enum_resolver=image_format_enum, + title="Image Format Options" + ) + visual_style: str = Field( + enum_resolver=visual_style_enum, + title="Preference" + ) + viewport_preset: str = Field( + enum_resolver=visual_preset_enum, + title="Pre-View Preset" + ) + anti_aliasing: str = Field( + enum_resolver=anti_aliasing_enum, + title="Anti-aliasing Quality" + ) + vp_texture: bool = Field(True, title="Viewport Texture") + + +DEFAULT_PREVIEW_ANIMATION_SETTINGS = { + "review_width": 1920, + "review_height": 1080, + "percentSize": 100.0, + "keep_images": False, + "image_format": "png", + "visual_style": "Realistic", + "viewport_preset": "Quality", + "anti_aliasing": "None", + "vp_texture": True +} diff --git a/server_addon/max/server/version.py b/server_addon/max/server/version.py index 3dc1f76bc6..485f44ac21 100644 --- a/server_addon/max/server/version.py +++ b/server_addon/max/server/version.py @@ -1 +1 @@ -__version__ = "0.1.0" +__version__ = "0.1.1" From 2728379bbc0810155447cae35c01c65341ec12bd Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 3 Nov 2023 16:04:39 +0800 Subject: [PATCH 0708/1233] hound --- server_addon/max/server/settings/preview_animation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/server_addon/max/server/settings/preview_animation.py b/server_addon/max/server/settings/preview_animation.py index 2496e8e548..759ce78291 100644 --- a/server_addon/max/server/settings/preview_animation.py +++ b/server_addon/max/server/settings/preview_animation.py @@ -34,6 +34,7 @@ def visual_style_enum(): {"label": "ModelAssist", "value": "ModelAssist"} ] + def visual_preset_enum(): """Return enumerator for viewport visual preset.""" return [ From 8fb7266ff8f0ea9c3b3f387ca120280093993294 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Fri, 3 Nov 2023 11:18:09 +0200 Subject: [PATCH 0709/1233] Add CollectAssetHandles and modify associated files --- openpype/hosts/houdini/api/lib.py | 20 +-- .../plugins/publish/collect_arnold_rop.py | 2 +- .../plugins/publish/collect_asset_handles.py | 125 ++++++++++++++++++ .../houdini/plugins/publish/collect_frames.py | 10 +- .../plugins/publish/collect_karma_rop.py | 2 +- .../plugins/publish/collect_mantra_rop.py | 2 +- .../plugins/publish/collect_redshift_rop.py | 2 +- .../publish/collect_rop_frame_range.py | 79 +---------- .../plugins/publish/collect_vray_rop.py | 2 +- .../plugins/publish/validate_frame_range.py | 2 +- .../defaults/project_settings/houdini.json | 2 +- .../schemas/schema_houdini_publish.json | 4 +- .../houdini/server/settings/publish.py | 2 +- 13 files changed, 151 insertions(+), 103 deletions(-) create mode 100644 openpype/hosts/houdini/plugins/publish/collect_asset_handles.py diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index ac375c56d6..c6722fb1bb 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -569,9 +569,9 @@ def get_template_from_value(key, value): return parm -def get_frame_data(node, handle_start=0, handle_end=0, log=None): - """Get the frame data: start frame, end frame, steps, - start frame with start handle and end frame with end handle. +def get_frame_data(node, log=None): + """Get the frame data: `frameStartHandle`, `frameEndHandle` + and `byFrameStep`. This function uses Houdini node's `trange`, `t1, `t2` and `t3` parameters as the source of truth for the full inclusive frame @@ -579,20 +579,17 @@ def get_frame_data(node, handle_start=0, handle_end=0, log=None): range including the handles. The non-inclusive frame start and frame end without handles - are computed by subtracting the handles from the inclusive + can be computed by subtracting the handles from the inclusive frame range. Args: node (hou.Node): ROP node to retrieve frame range from, the frame range is assumed to be the frame range *including* the start and end handles. - handle_start (int): Start handles. - handle_end (int): End handles. - log (logging.Logger): Logger to log to. Returns: - dict: frame data for start, end, steps, - start with handle and end with handle + dict: frame data for `frameStartHandle`, `frameEndHandle` + and `byFrameStep`. """ @@ -623,11 +620,6 @@ def get_frame_data(node, handle_start=0, handle_end=0, log=None): data["frameEndHandle"] = int(node.evalParm("f2")) data["byFrameStep"] = node.evalParm("f3") - data["handleStart"] = handle_start - data["handleEnd"] = handle_end - data["frameStart"] = data["frameStartHandle"] + data["handleStart"] - data["frameEnd"] = data["frameEndHandle"] - data["handleEnd"] - return data diff --git a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py index 420a8324fe..d95f763826 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py @@ -22,7 +22,7 @@ class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin): label = "Arnold ROP Render Products" # This specific order value is used so that # this plugin runs after CollectFrames - order = pyblish.api.CollectorOrder + 0.49999 + order = pyblish.api.CollectorOrder + 0.11 hosts = ["houdini"] families = ["arnold_rop"] diff --git a/openpype/hosts/houdini/plugins/publish/collect_asset_handles.py b/openpype/hosts/houdini/plugins/publish/collect_asset_handles.py new file mode 100644 index 0000000000..5c11948608 --- /dev/null +++ b/openpype/hosts/houdini/plugins/publish/collect_asset_handles.py @@ -0,0 +1,125 @@ +# -*- coding: utf-8 -*- +"""Collector plugin for frames data on ROP instances.""" +import hou # noqa +import pyblish.api +from openpype.lib import BoolDef +from openpype.pipeline import OpenPypePyblishPluginMixin + + +class CollectAssetHandles(pyblish.api.InstancePlugin, + OpenPypePyblishPluginMixin): + """Apply asset handles. + + If instance does not have: + - frameStart + - frameEnd + - handleStart + - handleEnd + But it does have: + - frameStartHandle + - frameEndHandle + + Then we will retrieve the asset's handles to compute + the exclusive frame range and actual handle ranges. + """ + + hosts = ["houdini"] + + # This specific order value is used so that + # this plugin runs after CollectAnatomyInstanceData + order = pyblish.api.CollectorOrder + 0.499 + + label = "Collect Asset Handles" + use_asset_handles = True + + + def process(self, instance): + # Only process instances without already existing handles data + # but that do have frameStartHandle and frameEndHandle defined + # like the data collected from CollectRopFrameRange + if "frameStartHandle" not in instance.data: + return + if "frameEndHandle" not in instance.data: + return + + has_existing_data = { + "handleStart", + "handleEnd", + "frameStart", + "frameEnd" + }.issubset(instance.data) + if has_existing_data: + return + + attr_values = self.get_attr_values_from_data(instance.data) + if attr_values.get("use_handles", self.use_asset_handles): + asset_data = instance.data["assetEntity"]["data"] + handle_start = asset_data.get("handleStart", 0) + handle_end = asset_data.get("handleEnd", 0) + else: + handle_start = 0 + handle_end = 0 + + frame_start = instance.data["frameStartHandle"] + handle_start + frame_end = instance.data["frameEndHandle"] - handle_end + + instance.data.update({ + "handleStart": handle_start, + "handleEnd": handle_end, + "frameStart": frame_start, + "frameEnd": frame_end + }) + + # Log debug message about the collected frame range + if attr_values.get("use_handles", self.use_asset_handles): + self.log.debug( + "Full Frame range with Handles " + "[{frame_start_handle} - {frame_end_handle}]" + .format( + frame_start_handle=instance.data["frameStartHandle"], + frame_end_handle=instance.data["frameEndHandle"] + ) + ) + else: + self.log.debug( + "Use handles is deactivated for this instance, " + "start and end handles are set to 0." + ) + + # Log collected frame range to the user + message = "Frame range [{frame_start} - {frame_end}]".format( + frame_start=frame_start, + frame_end=frame_end + ) + if handle_start or handle_end: + message += " with handles [{handle_start}]-[{handle_end}]".format( + handle_start=handle_start, + handle_end=handle_end + ) + self.log.info(message) + + if instance.data.get("byFrameStep", 1.0) != 1.0: + self.log.info( + "Frame steps {}".format(instance.data["byFrameStep"])) + + # Add frame range to label if the instance has a frame range. + label = instance.data.get("label", instance.data["name"]) + instance.data["label"] = ( + "{label} [{frame_start} - {frame_end}]" + .format( + label=label, + frame_start=frame_start, + frame_end=frame_end + ) + ) + + @classmethod + def get_attribute_defs(cls): + return [ + BoolDef("use_handles", + tooltip="Disable this if you want the publisher to" + " ignore start and end handles specified in the" + " asset data for this publish instance", + default=cls.use_asset_handles, + label="Use asset handles") + ] diff --git a/openpype/hosts/houdini/plugins/publish/collect_frames.py b/openpype/hosts/houdini/plugins/publish/collect_frames.py index 79cfcc6139..f6f538f5a5 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_frames.py +++ b/openpype/hosts/houdini/plugins/publish/collect_frames.py @@ -13,7 +13,7 @@ class CollectFrames(pyblish.api.InstancePlugin): # This specific order value is used so that # this plugin runs after CollectRopFrameRange - order = pyblish.api.CollectorOrder + 0.4999 + order = pyblish.api.CollectorOrder + 0.1 label = "Collect Frames" families = ["vdbcache", "imagesequence", "ass", "redshiftproxy", "review", "bgeo"] @@ -22,8 +22,8 @@ class CollectFrames(pyblish.api.InstancePlugin): ropnode = hou.node(instance.data["instance_node"]) - start_frame = instance.data.get("frameStart", None) - end_frame = instance.data.get("frameEnd", None) + start_frame = instance.data.get("frameStartHandle", None) + end_frame = instance.data.get("frameEndHandle", None) output_parm = lib.get_output_parameter(ropnode) if start_frame is not None: @@ -53,7 +53,7 @@ class CollectFrames(pyblish.api.InstancePlugin): # Check if frames are bigger than 1 (file collection) # override the result if end_frame - start_frame > 0: - result = self.create_file_list( + result = self.create_file_list(self, match, int(start_frame), int(end_frame) ) @@ -62,7 +62,7 @@ class CollectFrames(pyblish.api.InstancePlugin): instance.data.update({"frames": result}) @staticmethod - def create_file_list(match, start_frame, end_frame): + def create_file_list(self,match, start_frame, end_frame): """Collect files based on frame range and `regex.match` Args: diff --git a/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py b/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py index a477529df9..dac350a6ef 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py @@ -26,7 +26,7 @@ class CollectKarmaROPRenderProducts(pyblish.api.InstancePlugin): label = "Karma ROP Render Products" # This specific order value is used so that # this plugin runs after CollectFrames - order = pyblish.api.CollectorOrder + 0.49999 + order = pyblish.api.CollectorOrder + 0.11 hosts = ["houdini"] families = ["karma_rop"] diff --git a/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py b/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py index 9f0ae8d33c..a3e7927807 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py @@ -26,7 +26,7 @@ class CollectMantraROPRenderProducts(pyblish.api.InstancePlugin): label = "Mantra ROP Render Products" # This specific order value is used so that # this plugin runs after CollectFrames - order = pyblish.api.CollectorOrder + 0.49999 + order = pyblish.api.CollectorOrder + 0.11 hosts = ["houdini"] families = ["mantra_rop"] diff --git a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py index 0bd7b41641..0acddab011 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py @@ -26,7 +26,7 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin): label = "Redshift ROP Render Products" # This specific order value is used so that # this plugin runs after CollectFrames - order = pyblish.api.CollectorOrder + 0.49999 + order = pyblish.api.CollectorOrder + 0.11 hosts = ["houdini"] families = ["redshift_rop"] diff --git a/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py b/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py index 186244fedd..1e6bc3b16e 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py +++ b/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py @@ -2,22 +2,15 @@ """Collector plugin for frames data on ROP instances.""" import hou # noqa import pyblish.api -from openpype.lib import BoolDef from openpype.hosts.houdini.api import lib -from openpype.pipeline import OpenPypePyblishPluginMixin -class CollectRopFrameRange(pyblish.api.InstancePlugin, - OpenPypePyblishPluginMixin): - +class CollectRopFrameRange(pyblish.api.InstancePlugin): """Collect all frames which would be saved from the ROP nodes""" hosts = ["houdini"] - # This specific order value is used so that - # this plugin runs after CollectAnatomyInstanceData - order = pyblish.api.CollectorOrder + 0.499 + order = pyblish.api.CollectorOrder label = "Collect RopNode Frame Range" - use_asset_handles = True def process(self, instance): @@ -30,78 +23,16 @@ class CollectRopFrameRange(pyblish.api.InstancePlugin, return ropnode = hou.node(node_path) - - attr_values = self.get_attr_values_from_data(instance.data) - - if attr_values.get("use_handles", self.use_asset_handles): - asset_data = instance.data["assetEntity"]["data"] - handle_start = asset_data.get("handleStart", 0) - handle_end = asset_data.get("handleEnd", 0) - else: - handle_start = 0 - handle_end = 0 - frame_data = lib.get_frame_data( - ropnode, handle_start, handle_end, self.log + ropnode, self.log ) if not frame_data: return # Log debug message about the collected frame range - frame_start = frame_data["frameStart"] - frame_end = frame_data["frameEnd"] - - if attr_values.get("use_handles", self.use_asset_handles): - self.log.debug( - "Full Frame range with Handles " - "[{frame_start_handle} - {frame_end_handle}]" - .format( - frame_start_handle=frame_data["frameStartHandle"], - frame_end_handle=frame_data["frameEndHandle"] - ) - ) - else: - self.log.debug( - "Use handles is deactivated for this instance, " - "start and end handles are set to 0." - ) - - # Log collected frame range to the user - message = "Frame range [{frame_start} - {frame_end}]".format( - frame_start=frame_start, - frame_end=frame_end + self.log.debug( + "Collected frame_data: {}".format(frame_data) ) - if handle_start or handle_end: - message += " with handles [{handle_start}]-[{handle_end}]".format( - handle_start=handle_start, - handle_end=handle_end - ) - self.log.info(message) - - if frame_data.get("byFrameStep", 1.0) != 1.0: - self.log.info("Frame steps {}".format(frame_data["byFrameStep"])) instance.data.update(frame_data) - - # Add frame range to label if the instance has a frame range. - label = instance.data.get("label", instance.data["name"]) - instance.data["label"] = ( - "{label} [{frame_start} - {frame_end}]" - .format( - label=label, - frame_start=frame_start, - frame_end=frame_end - ) - ) - - @classmethod - def get_attribute_defs(cls): - return [ - BoolDef("use_handles", - tooltip="Disable this if you want the publisher to" - " ignore start and end handles specified in the" - " asset data for this publish instance", - default=cls.use_asset_handles, - label="Use asset handles") - ] diff --git a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py index 519c12aede..64de2079cd 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py @@ -26,7 +26,7 @@ class CollectVrayROPRenderProducts(pyblish.api.InstancePlugin): label = "VRay ROP Render Products" # This specific order value is used so that # this plugin runs after CollectFrames - order = pyblish.api.CollectorOrder + 0.49999 + order = pyblish.api.CollectorOrder + 0.11 hosts = ["houdini"] families = ["vray_rop"] diff --git a/openpype/hosts/houdini/plugins/publish/validate_frame_range.py b/openpype/hosts/houdini/plugins/publish/validate_frame_range.py index 6a66f3de9f..b49cfae901 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_frame_range.py +++ b/openpype/hosts/houdini/plugins/publish/validate_frame_range.py @@ -89,7 +89,7 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): .format(instance)) return - created_instance.publish_attributes["CollectRopFrameRange"]["use_handles"] = False # noqa + created_instance.publish_attributes["CollectAssetHandles"]["use_handles"] = False # noqa create_context.save_changes() cls.log.debug("use asset handles is turned off for '{}'" diff --git a/openpype/settings/defaults/project_settings/houdini.json b/openpype/settings/defaults/project_settings/houdini.json index 87983620ec..0dd8443e44 100644 --- a/openpype/settings/defaults/project_settings/houdini.json +++ b/openpype/settings/defaults/project_settings/houdini.json @@ -137,7 +137,7 @@ } }, "publish": { - "CollectRopFrameRange": { + "CollectAssetHandles": { "use_asset_handles": true }, "ValidateContainers": { 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 0de9f21c9f..324cfd8d58 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 @@ -11,8 +11,8 @@ { "type": "dict", "collapsible": true, - "key": "CollectRopFrameRange", - "label": "Collect Rop Frame Range", + "key": "CollectAssetHandles", + "label": "Collect Asset Handles", "children": [ { "type": "label", diff --git a/server_addon/houdini/server/settings/publish.py b/server_addon/houdini/server/settings/publish.py index 6615e34ca5..342bf957c1 100644 --- a/server_addon/houdini/server/settings/publish.py +++ b/server_addon/houdini/server/settings/publish.py @@ -3,7 +3,7 @@ from ayon_server.settings import BaseSettingsModel # Publish Plugins -class CollectRopFrameRangeModel(BaseSettingsModel): +class CollectAssetHandlesModel(BaseSettingsModel): """Collect Frame Range Disable this if you want the publisher to ignore start and end handles specified in the From 38d71e7c73ef5716b2964c867fe81d1b26656aed Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Fri, 3 Nov 2023 11:19:08 +0200 Subject: [PATCH 0710/1233] Bump Houdini addon version --- server_addon/houdini/server/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/houdini/server/version.py b/server_addon/houdini/server/version.py index 6cd38b7465..c49a95c357 100644 --- a/server_addon/houdini/server/version.py +++ b/server_addon/houdini/server/version.py @@ -1 +1 @@ -__version__ = "0.2.7" +__version__ = "0.2.8" From 3d74095521a45337d764a70eaf794fa7f98dc226 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Fri, 3 Nov 2023 11:21:10 +0200 Subject: [PATCH 0711/1233] Resolve Hound and Remove debug code --- .../hosts/houdini/plugins/publish/collect_asset_handles.py | 1 - openpype/hosts/houdini/plugins/publish/collect_frames.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_asset_handles.py b/openpype/hosts/houdini/plugins/publish/collect_asset_handles.py index 5c11948608..6474d64765 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_asset_handles.py +++ b/openpype/hosts/houdini/plugins/publish/collect_asset_handles.py @@ -32,7 +32,6 @@ class CollectAssetHandles(pyblish.api.InstancePlugin, label = "Collect Asset Handles" use_asset_handles = True - def process(self, instance): # Only process instances without already existing handles data # but that do have frameStartHandle and frameEndHandle defined diff --git a/openpype/hosts/houdini/plugins/publish/collect_frames.py b/openpype/hosts/houdini/plugins/publish/collect_frames.py index f6f538f5a5..cdef642174 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_frames.py +++ b/openpype/hosts/houdini/plugins/publish/collect_frames.py @@ -53,7 +53,7 @@ class CollectFrames(pyblish.api.InstancePlugin): # Check if frames are bigger than 1 (file collection) # override the result if end_frame - start_frame > 0: - result = self.create_file_list(self, + result = self.create_file_list( match, int(start_frame), int(end_frame) ) @@ -62,7 +62,7 @@ class CollectFrames(pyblish.api.InstancePlugin): instance.data.update({"frames": result}) @staticmethod - def create_file_list(self,match, start_frame, end_frame): + def create_file_list(match, start_frame, end_frame): """Collect files based on frame range and `regex.match` Args: From 382ad931c670adbdd52fdf8996bb1d4115d37744 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 3 Nov 2023 14:13:08 +0100 Subject: [PATCH 0712/1233] resolve: frame duration and handles operation --- openpype/hosts/resolve/api/plugin.py | 53 ++++++++++++++++------------ 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py index 5c4a92df89..0f6f4d14bd 100644 --- a/openpype/hosts/resolve/api/plugin.py +++ b/openpype/hosts/resolve/api/plugin.py @@ -405,26 +405,41 @@ class ClipLoader: self.active_bin ) _clip_property = media_pool_item.GetClipProperty + source_in = int(_clip_property("Start")) + source_out = int(_clip_property("End")) + source_duration = int(_clip_property("Frames")) - # get handles + # get version data frame data from db + frame_start = self.data["versionData"].get("frameStart") + frame_end = self.data["versionData"].get("frameEnd") 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"]) - # check frame duration from versionData or assetData - frame_start = self.data["versionData"].get("frameStart") - if frame_start is None: - frame_start = self.data["assetData"]["frameStart"] + # check if source duration is shorter than db frame duration + source_with_handles = True + # make sure all frame data is available + if ( + frame_start is None or + frame_end is None or + handle_start is None or + handle_end is None + ): + # if not then rather assume that source has no handles + source_with_handles = False + else: + # calculate db frame duration + db_frame_duration = ( + # include handles + int(handle_start) + int(handle_end) + + # include frame duration + (int(frame_end) - int(frame_start) + 1) + ) - # check frame duration from versionData or assetData - frame_end = self.data["versionData"].get("frameEnd") - if frame_end is None: - frame_end = self.data["assetData"]["frameEnd"] - - db_frame_duration = int(frame_end) - int(frame_start) + 1 + # compare source duration with db frame duration + # and assume that source has no handles if source duration + # is shorter than db frame duration + if source_duration < db_frame_duration: + source_with_handles = False # get timeline in timeline_start = self.active_timeline.GetStartFrame() @@ -436,14 +451,6 @@ class ClipLoader: timeline_in = int( timeline_start + self.data["assetData"]["clipIn"]) - source_in = int(_clip_property("Start")) - source_out = int(_clip_property("End")) - source_duration = int(_clip_property("Frames")) - - # check if source duration is shorter than db frame duration - source_with_handles = True - if source_duration < db_frame_duration: - source_with_handles = False # only exclude handles if source has no handles or # if user wants to load without handles From 69bf065851a7b82db1779a72022abf941d5c56bd Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Fri, 3 Nov 2023 15:41:37 +0200 Subject: [PATCH 0713/1233] BigRoy's Comment - Update label --- .../hosts/houdini/plugins/publish/collect_asset_handles.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_asset_handles.py b/openpype/hosts/houdini/plugins/publish/collect_asset_handles.py index 6474d64765..67a281639d 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_asset_handles.py +++ b/openpype/hosts/houdini/plugins/publish/collect_asset_handles.py @@ -104,11 +104,11 @@ class CollectAssetHandles(pyblish.api.InstancePlugin, # Add frame range to label if the instance has a frame range. label = instance.data.get("label", instance.data["name"]) instance.data["label"] = ( - "{label} [{frame_start} - {frame_end}]" + "{label} [{frame_start_handle} - {frame_end_handle}]" .format( label=label, - frame_start=frame_start, - frame_end=frame_end + frame_start_handle=instance.data["frameStartHandle"], + frame_end_handle=instance.data["frameEndHandle"] ) ) From 2d3ae5a0d346a95586abde0a11632291e8c83467 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 3 Nov 2023 22:02:13 +0800 Subject: [PATCH 0714/1233] supports checking the required plugins with families type * --- .../plugins/publish/validate_loaded_plugin.py | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py index dc82c7ed65..d6f849a57e 100644 --- a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py +++ b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py @@ -16,7 +16,11 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, InstancePlugin): """Validates if the specific plugin is loaded in 3ds max. Studio Admin(s) can add the plugins they want to check in validation - via studio defined project settings""" + via studio defined project settings + If families = ["*"], all the required plugins would be validated + If families + + """ order = ValidatorOrder hosts = ["max"] @@ -48,15 +52,17 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, } for families, plugin in required_plugins.items(): - families_list = families.split(",") - excluded_families = [family for family in families_list - if instance.data["family"] != family - and family != "_"] - if excluded_families: - self.log.debug("The {} instance is not part of {}.".format( - instance.data["family"], excluded_families - )) - return + # Out of for loop build the instance family lookup + instance_families = {instance.data["family"]} + instance_families.update(instance.data.get("families", [])) + self.log.debug(f"{instance_families}") + # In the for loop check whether any family matches + match_families = {fam.strip() for fam in families.split(",") if fam.strip()} + self.log.debug(f"match_families: {match_families}") + has_match = "*" in match_families or match_families.intersection( + instance_families) or families == "_" + if not has_match: + continue if not plugin: return @@ -66,7 +72,7 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, if plugin_index is None: invalid.append( - f"Plugin {plugin} not exists in 3dsMax Plugin List." + f"Plugin {plugin} does not exist in 3dsMax Plugin List." ) continue @@ -82,12 +88,12 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, "- {}".format(invalid) for invalid in invalid_plugins ) report = ( - "Required plugins fails to load.\n\n" + "Required plugins are not loaded.\n\n" f"{bullet_point_invalid_statement}\n\n" "You can use repair action to load the plugin." ) raise PublishValidationError( - report, title="Required Plugins unloaded") + report, title="Missing Required Plugins") @classmethod def repair(cls, instance): From e61d03556a1a7d915a6cd421c6a0ec45d40c8481 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 3 Nov 2023 22:03:00 +0800 Subject: [PATCH 0715/1233] hound --- openpype/hosts/max/plugins/publish/validate_loaded_plugin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py index d6f849a57e..e58685cc4d 100644 --- a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py +++ b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py @@ -57,7 +57,8 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, instance_families.update(instance.data.get("families", [])) self.log.debug(f"{instance_families}") # In the for loop check whether any family matches - match_families = {fam.strip() for fam in families.split(",") if fam.strip()} + match_families = {fam.strip() for fam in + families.split(",") if fam.strip()} self.log.debug(f"match_families: {match_families}") has_match = "*" in match_families or match_families.intersection( instance_families) or families == "_" From 6e9c3b227815e7e8cbd158a8110549b2f095443e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 3 Nov 2023 15:06:59 +0100 Subject: [PATCH 0716/1233] deadline: settings are not blocking extension input --- server_addon/deadline/server/settings/publish_plugins.py | 2 +- server_addon/deadline/server/version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server_addon/deadline/server/settings/publish_plugins.py b/server_addon/deadline/server/settings/publish_plugins.py index 8d48695a9c..54b7ff57c1 100644 --- a/server_addon/deadline/server/settings/publish_plugins.py +++ b/server_addon/deadline/server/settings/publish_plugins.py @@ -267,7 +267,7 @@ class ProcessSubmittedJobOnFarmModel(BaseSettingsModel): title="Reviewable products filter", ) - @validator("aov_filter", "skip_integration_repre_list") + @validator("aov_filter") def validate_unique_names(cls, value): ensure_unique_names(value) return value diff --git a/server_addon/deadline/server/version.py b/server_addon/deadline/server/version.py index b3f4756216..ae7362549b 100644 --- a/server_addon/deadline/server/version.py +++ b/server_addon/deadline/server/version.py @@ -1 +1 @@ -__version__ = "0.1.2" +__version__ = "0.1.3" From 44a0b0e6284a73180dccdef244fe92c5761caaf0 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 3 Nov 2023 22:42:44 +0800 Subject: [PATCH 0717/1233] make sure the aspect ratio correct --- openpype/hosts/max/api/preview_animation.py | 51 +++++++++++---------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/openpype/hosts/max/api/preview_animation.py b/openpype/hosts/max/api/preview_animation.py index 0754fa61c4..d6becd499e 100644 --- a/openpype/hosts/max/api/preview_animation.py +++ b/openpype/hosts/max/api/preview_animation.py @@ -166,13 +166,16 @@ def _render_preview_animation_max_2024( def _render_preview_animation_max_pre_2024( - filepath, startFrame, endFrame, percentSize, ext): + filepath, startFrame, endFrame, + width, height, percentSize, ext): """Render viewport animation by creating bitmaps ***For 3dsMax Version <2024 Args: filepath (str): filepath without frame numbers and extension startFrame (int): start frame endFrame (int): end frame + width (int): render resolution width + height (int): render resolution height percentSize (float): render resolution multiplier by 100 e.g. 100.0 is 1x, 50.0 is 0.5x, 150.0 is 1.5x ext (str): image extension @@ -182,9 +185,8 @@ def _render_preview_animation_max_pre_2024( # get the screenshot percent = percentSize / 100.0 - res_width = int(round(rt.renderWidth * percent)) - res_height = int(round(rt.renderHeight * percent)) - viewportRatio = float(res_width / res_height) + res_width = int(round(width * percent)) + res_height = int(round(height * percent)) frame_template = "{}.{{:04}}.{}".format(filepath, ext) frame_template.replace("\\", "/") files = [] @@ -196,10 +198,11 @@ def _render_preview_animation_max_pre_2024( res_width, res_height, filename=filepath ) dib = rt.gw.getViewportDib() - dib_width = float(dib.width) - dib_height = float(dib.height) + dib_width = rt.renderWidth + dib_height = rt.renderHeight # aspect ratio - renderRatio = rt.getRendImageAspect() + viewportRatio = dib_width /dib_height + renderRatio = float(res_width / res_height) if viewportRatio <= renderRatio: heightCrop = (dib_width / renderRatio) topEdge = int((dib_height - heightCrop) / 2.0) @@ -263,22 +266,24 @@ def render_preview_animation( viewport_options = viewport_options_for_preview_animation() with play_preview_when_done(False): with viewport_layout_and_camera(camera): - with render_resolution(width, height): - if int(get_max_version()) < 2024: - with viewport_preference_setting( - viewport_options["general_viewport"], - viewport_options["nitrous_manager"], - viewport_options["nitrous_viewport"], - viewport_options["vp_btn_mgr"] - ): - return _render_preview_animation_max_pre_2024( - filepath, - start_frame, - end_frame, - percentSize, - ext - ) - else: + if int(get_max_version()) < 2024: + with viewport_preference_setting( + viewport_options["general_viewport"], + viewport_options["nitrous_manager"], + viewport_options["nitrous_viewport"], + viewport_options["vp_btn_mgr"] + ): + return _render_preview_animation_max_pre_2024( + filepath, + start_frame, + end_frame, + width, + height, + percentSize, + ext + ) + else: + with render_resolution(width, height): return _render_preview_animation_max_2024( filepath, start_frame, From 1132d1c9f3eb7d512b548988e717b16563dde119 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 3 Nov 2023 15:43:33 +0100 Subject: [PATCH 0718/1233] avoid double slashes in context title path --- openpype/host/host.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/host/host.py b/openpype/host/host.py index 630fb873a8..afe06d1f55 100644 --- a/openpype/host/host.py +++ b/openpype/host/host.py @@ -170,7 +170,7 @@ class HostBase(object): if project_name: items.append(project_name) if asset_name: - items.append(asset_name) + items.append(asset_name.lstrip("/")) if task_name: items.append(task_name) if items: From 01c965eb9cb701eee833cb735dcec0daf4f6f61a Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 3 Nov 2023 22:43:34 +0800 Subject: [PATCH 0719/1233] hound --- openpype/hosts/max/api/preview_animation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/api/preview_animation.py b/openpype/hosts/max/api/preview_animation.py index d6becd499e..bd0fee3658 100644 --- a/openpype/hosts/max/api/preview_animation.py +++ b/openpype/hosts/max/api/preview_animation.py @@ -201,7 +201,7 @@ def _render_preview_animation_max_pre_2024( dib_width = rt.renderWidth dib_height = rt.renderHeight # aspect ratio - viewportRatio = dib_width /dib_height + viewportRatio = dib_width / dib_height renderRatio = float(res_width / res_height) if viewportRatio <= renderRatio: heightCrop = (dib_width / renderRatio) From e5e278201b359772e51ae7dafae188753de06a3e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 3 Nov 2023 16:18:04 +0100 Subject: [PATCH 0720/1233] better code explanation --- openpype/hosts/resolve/api/plugin.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py index 0f6f4d14bd..09c5a69d73 100644 --- a/openpype/hosts/resolve/api/plugin.py +++ b/openpype/hosts/resolve/api/plugin.py @@ -415,6 +415,19 @@ class ClipLoader: handle_start = self.data["versionData"].get("handleStart") handle_end = self.data["versionData"].get("handleEnd") + """ + There are cases where representation could be published without + handles if the "Extract review output tags" is set to "no_handles". + This would result in a shorter source duration compared to the + db frame-range. In such cases, we need to assume that the source + has no handles. + + To address this, we should compare the duration of the source + frame with the db frame-range. The duration of the db frame-range + should be calculated from the version data. If, for any reason, + the frame data is missing in the version data, we should again + assume that the source has no handles. + """ # check if source duration is shorter than db frame duration source_with_handles = True # make sure all frame data is available @@ -771,7 +784,7 @@ class PublishClip: # increasing steps by index of rename iteration self.count_steps *= self.rename_index - hierarchy_formatting_data = dict() + hierarchy_formatting_data = {} _data = self.timeline_item_default_data.copy() if self.ui_inputs: # adding tag metadata from ui @@ -867,7 +880,7 @@ class PublishClip: def _convert_to_entity(self, key): """ Converting input key to key with type. """ # convert to entity type - entity_type = self.types.get(key, None) + entity_type = self.types.get(key) assert entity_type, "Missing entity type for `{}`".format( key From 49154c0750ab45ffeb7bd413ab99b79a6f6aadef Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 3 Nov 2023 16:45:12 +0100 Subject: [PATCH 0721/1233] fix create multishot layout --- .../maya/plugins/create/create_multishot_layout.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_multishot_layout.py b/openpype/hosts/maya/plugins/create/create_multishot_layout.py index 0b027c02ea..7cd3fdbd17 100644 --- a/openpype/hosts/maya/plugins/create/create_multishot_layout.py +++ b/openpype/hosts/maya/plugins/create/create_multishot_layout.py @@ -45,10 +45,14 @@ class CreateMultishotLayout(plugin.MayaCreator): above is done. """ - current_folder = get_folder_by_name( - project_name=get_current_project_name(), - folder_name=get_current_asset_name(), - ) + project_name = get_current_project_name() + folder_path = get_current_asset_name() + if "/" in folder_path: + current_folder = get_folder_by_path(project_name, folder_path) + else: + current_folder = get_folder_by_name( + project_name, folder_name=folder_path + ) current_path_parts = current_folder["path"].split("/") From 9154dbab05ec7f5b921874b5d523720df7c9409a Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 3 Nov 2023 15:45:15 +0000 Subject: [PATCH 0722/1233] Fix loading of blend layout --- openpype/hosts/blender/plugins/load/load_blend.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/blender/plugins/load/load_blend.py b/openpype/hosts/blender/plugins/load/load_blend.py index 25d6568889..3d6b634916 100644 --- a/openpype/hosts/blender/plugins/load/load_blend.py +++ b/openpype/hosts/blender/plugins/load/load_blend.py @@ -32,7 +32,7 @@ class BlendLoader(plugin.AssetLoader): empties = [obj for obj in objects if obj.type == 'EMPTY'] for empty in empties: - if empty.get(AVALON_PROPERTY): + if empty.get(AVALON_PROPERTY) and empty.parent is None: return empty return None @@ -90,6 +90,7 @@ class BlendLoader(plugin.AssetLoader): members.append(data) container = self._get_asset_container(data_to.objects) + print(container) assert container, "No asset group found" container.name = group_name @@ -100,8 +101,11 @@ class BlendLoader(plugin.AssetLoader): # Link all the container children to the collection for obj in container.children_recursive: + print(obj) bpy.context.scene.collection.objects.link(obj) + print("") + # Remove the library from the blend file library = bpy.data.libraries.get(bpy.path.basename(libpath)) bpy.data.libraries.remove(library) From dbd1fcb98912616d03c456718baf9b3f2e65a03c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 3 Nov 2023 17:03:54 +0100 Subject: [PATCH 0723/1233] make sure all QThread objects are always removed from python memory --- .../ayon_utils/widgets/folders_widget.py | 3 +- .../ayon_utils/widgets/projects_widget.py | 13 +++-- .../tools/ayon_utils/widgets/tasks_widget.py | 49 ++++++++++--------- 3 files changed, 35 insertions(+), 30 deletions(-) diff --git a/openpype/tools/ayon_utils/widgets/folders_widget.py b/openpype/tools/ayon_utils/widgets/folders_widget.py index 322553c51c..b72a992858 100644 --- a/openpype/tools/ayon_utils/widgets/folders_widget.py +++ b/openpype/tools/ayon_utils/widgets/folders_widget.py @@ -104,8 +104,8 @@ class FoldersModel(QtGui.QStandardItemModel): if not project_name: self._last_project_name = project_name - self._current_refresh_thread = None self._fill_items({}) + self._current_refresh_thread = None return self._is_refreshing = True @@ -152,6 +152,7 @@ class FoldersModel(QtGui.QStandardItemModel): return self._fill_items(thread.get_result()) + self._current_refresh_thread = None def _fill_item_data(self, item, folder_item): """ diff --git a/openpype/tools/ayon_utils/widgets/projects_widget.py b/openpype/tools/ayon_utils/widgets/projects_widget.py index be18cfe3ed..05347faca4 100644 --- a/openpype/tools/ayon_utils/widgets/projects_widget.py +++ b/openpype/tools/ayon_utils/widgets/projects_widget.py @@ -35,12 +35,11 @@ class ProjectsModel(QtGui.QStandardItemModel): self._selected_project = None - self._is_refreshing = False self._refresh_thread = None @property def is_refreshing(self): - return self._is_refreshing + return self._refresh_thread is not None def refresh(self): self._refresh() @@ -169,15 +168,16 @@ class ProjectsModel(QtGui.QStandardItemModel): return self._select_item def _refresh(self): - if self._is_refreshing: + if self._refresh_thread is not None: return - self._is_refreshing = True + refresh_thread = RefreshThread( "projects", self._query_project_items ) refresh_thread.refresh_finished.connect(self._refresh_finished) - refresh_thread.start() + self._refresh_thread = refresh_thread + refresh_thread.start() def _query_project_items(self): return self._controller.get_project_items() @@ -185,11 +185,10 @@ class ProjectsModel(QtGui.QStandardItemModel): def _refresh_finished(self): # TODO check if failed result = self._refresh_thread.get_result() - self._refresh_thread = None self._fill_items(result) - self._is_refreshing = False + self._refresh_thread = None self.refreshed.emit() def _fill_items(self, project_items): diff --git a/openpype/tools/ayon_utils/widgets/tasks_widget.py b/openpype/tools/ayon_utils/widgets/tasks_widget.py index d01b3a7917..a6375c6ae6 100644 --- a/openpype/tools/ayon_utils/widgets/tasks_widget.py +++ b/openpype/tools/ayon_utils/widgets/tasks_widget.py @@ -185,28 +185,7 @@ class TasksModel(QtGui.QStandardItemModel): 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. - """ - - # Make sure to remove thread from '_refresh_threads' dict - thread = self._refresh_threads.pop(thread_id) - if ( - self._current_refresh_thread is None - or thread_id != self._current_refresh_thread.id - ): - return - + def _fill_data_from_thread(self, thread): task_items = thread.get_result() # Task items are refreshed if task_items is None: @@ -247,7 +226,33 @@ class TasksModel(QtGui.QStandardItemModel): if new_items: root_item.appendRows(new_items) + 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. + """ + + # Make sure to remove thread from '_refresh_threads' dict + thread = self._refresh_threads.pop(thread_id) + if ( + self._current_refresh_thread is None + or thread_id != self._current_refresh_thread.id + ): + return + + self._fill_data_from_thread(thread) + + root_item = self.invisibleRootItem() self._has_content = root_item.rowCount() > 0 + self._current_refresh_thread = None self._is_refreshing = False self.refreshed.emit() From 10aea1088a68f49218255e4988febe4bc1f0f462 Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Fri, 3 Nov 2023 17:06:44 +0100 Subject: [PATCH 0724/1233] Add collect workfile to blender host --- .../plugins/publish/collect_workfile.py | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 openpype/hosts/blender/plugins/publish/collect_workfile.py diff --git a/openpype/hosts/blender/plugins/publish/collect_workfile.py b/openpype/hosts/blender/plugins/publish/collect_workfile.py new file mode 100644 index 0000000000..e431405e80 --- /dev/null +++ b/openpype/hosts/blender/plugins/publish/collect_workfile.py @@ -0,0 +1,38 @@ +from pathlib import Path + +import bpy +from pyblish.api import InstancePlugin, CollectorOrder + + +class CollectWorkfile(InstancePlugin): + """Inject workfile data into its instance.""" + + order = CollectorOrder + label = "Collect Workfile" + hosts = ["blender"] + families = ["workfile"] + + def process(self, instance): + """Process collector.""" + + context = instance.context + filepath = Path(context.data["currentFile"]) + ext = filepath.suffix + + instance.data.update( + { + "setMembers": [filepath.as_posix()], + "frameStart": context.data.get("frameStart", 1), + "frameEnd": context.data.get("frameEnd", 1), + "handleStart": context.data.get("handleStart", 1), + "handledEnd": context.data.get("handleEnd", 1), + "representations": [ + { + "name": ext.lstrip("."), + "ext": ext, + "files": filepath.name, # TODO resources + "stagingDir": filepath.parent, + } + ], + } + ) From f18d0d9f8f36fcdc7193aaea5588db391fc5acec Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Fri, 3 Nov 2023 18:09:48 +0200 Subject: [PATCH 0725/1233] include full frame range in representation dictionary --- openpype/hosts/houdini/plugins/publish/extract_ass.py | 4 ++-- openpype/hosts/houdini/plugins/publish/extract_bgeo.py | 4 ++-- openpype/hosts/houdini/plugins/publish/extract_composite.py | 4 ++-- openpype/hosts/houdini/plugins/publish/extract_fbx.py | 6 +++--- openpype/hosts/houdini/plugins/publish/extract_opengl.py | 4 ++-- .../hosts/houdini/plugins/publish/extract_redshift_proxy.py | 6 +++--- openpype/hosts/houdini/plugins/publish/extract_vdb_cache.py | 4 ++-- 7 files changed, 16 insertions(+), 16 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/extract_ass.py b/openpype/hosts/houdini/plugins/publish/extract_ass.py index 0d246625ba..be60217055 100644 --- a/openpype/hosts/houdini/plugins/publish/extract_ass.py +++ b/openpype/hosts/houdini/plugins/publish/extract_ass.py @@ -56,7 +56,7 @@ class ExtractAss(publish.Extractor): 'ext': ext, "files": files, "stagingDir": staging_dir, - "frameStart": instance.data["frameStart"], - "frameEnd": instance.data["frameEnd"], + "frameStart": instance.data["frameStartHandle"], + "frameEnd": instance.data["frameEndHandle"], } instance.data["representations"].append(representation) diff --git a/openpype/hosts/houdini/plugins/publish/extract_bgeo.py b/openpype/hosts/houdini/plugins/publish/extract_bgeo.py index c9625ec880..d13141b426 100644 --- a/openpype/hosts/houdini/plugins/publish/extract_bgeo.py +++ b/openpype/hosts/houdini/plugins/publish/extract_bgeo.py @@ -47,7 +47,7 @@ class ExtractBGEO(publish.Extractor): "ext": ext.lstrip("."), "files": output, "stagingDir": staging_dir, - "frameStart": instance.data["frameStart"], - "frameEnd": instance.data["frameEnd"] + "frameStart": instance.data["frameStartHandle"], + "frameEnd": instance.data["frameEndHandle"] } instance.data["representations"].append(representation) diff --git a/openpype/hosts/houdini/plugins/publish/extract_composite.py b/openpype/hosts/houdini/plugins/publish/extract_composite.py index 7a1ab36b93..11cf83a46d 100644 --- a/openpype/hosts/houdini/plugins/publish/extract_composite.py +++ b/openpype/hosts/houdini/plugins/publish/extract_composite.py @@ -41,8 +41,8 @@ class ExtractComposite(publish.Extractor): "ext": ext, "files": output, "stagingDir": staging_dir, - "frameStart": instance.data["frameStart"], - "frameEnd": instance.data["frameEnd"], + "frameStart": instance.data["frameStartHandle"], + "frameEnd": instance.data["frameEndHandle"], } from pprint import pformat diff --git a/openpype/hosts/houdini/plugins/publish/extract_fbx.py b/openpype/hosts/houdini/plugins/publish/extract_fbx.py index 7993b3352f..1be61ecce1 100644 --- a/openpype/hosts/houdini/plugins/publish/extract_fbx.py +++ b/openpype/hosts/houdini/plugins/publish/extract_fbx.py @@ -40,9 +40,9 @@ class ExtractFBX(publish.Extractor): } # A single frame may also be rendered without start/end frame. - if "frameStart" in instance.data and "frameEnd" in instance.data: - representation["frameStart"] = instance.data["frameStart"] - representation["frameEnd"] = instance.data["frameEnd"] + if "frameStartHandle" in instance.data and "frameEndHandle" in instance.data: + representation["frameStart"] = instance.data["frameStartHandle"] + representation["frameEnd"] = instance.data["frameEndHandle"] # set value type for 'representations' key to list if "representations" not in instance.data: diff --git a/openpype/hosts/houdini/plugins/publish/extract_opengl.py b/openpype/hosts/houdini/plugins/publish/extract_opengl.py index 6c36dec5f5..38808089ac 100644 --- a/openpype/hosts/houdini/plugins/publish/extract_opengl.py +++ b/openpype/hosts/houdini/plugins/publish/extract_opengl.py @@ -39,8 +39,8 @@ class ExtractOpenGL(publish.Extractor): "ext": instance.data["imageFormat"], "files": output, "stagingDir": staging_dir, - "frameStart": instance.data["frameStart"], - "frameEnd": instance.data["frameEnd"], + "frameStart": instance.data["frameStartHandle"], + "frameEnd": instance.data["frameEndHandle"], "tags": tags, "preview": True, "camera_name": instance.data.get("review_camera") diff --git a/openpype/hosts/houdini/plugins/publish/extract_redshift_proxy.py b/openpype/hosts/houdini/plugins/publish/extract_redshift_proxy.py index 1d99ac665c..18cbd5712e 100644 --- a/openpype/hosts/houdini/plugins/publish/extract_redshift_proxy.py +++ b/openpype/hosts/houdini/plugins/publish/extract_redshift_proxy.py @@ -44,8 +44,8 @@ class ExtractRedshiftProxy(publish.Extractor): } # A single frame may also be rendered without start/end frame. - if "frameStart" in instance.data and "frameEnd" in instance.data: - representation["frameStart"] = instance.data["frameStart"] - representation["frameEnd"] = instance.data["frameEnd"] + if "frameStartHandle" in instance.data and "frameEndHandle" in instance.data: + representation["frameStart"] = instance.data["frameStartHandle"] + representation["frameEnd"] = instance.data["frameEndHandle"] instance.data["representations"].append(representation) diff --git a/openpype/hosts/houdini/plugins/publish/extract_vdb_cache.py b/openpype/hosts/houdini/plugins/publish/extract_vdb_cache.py index 4bca758f08..89af8e1756 100644 --- a/openpype/hosts/houdini/plugins/publish/extract_vdb_cache.py +++ b/openpype/hosts/houdini/plugins/publish/extract_vdb_cache.py @@ -40,7 +40,7 @@ class ExtractVDBCache(publish.Extractor): "ext": "vdb", "files": output, "stagingDir": staging_dir, - "frameStart": instance.data["frameStart"], - "frameEnd": instance.data["frameEnd"], + "frameStart": instance.data["frameStartHandle"], + "frameEnd": instance.data["frameEndHandle"], } instance.data["representations"].append(representation) From 821b478830f2f75f733d82374bd06195db01e9d8 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 3 Nov 2023 16:50:11 +0000 Subject: [PATCH 0726/1233] Get the selection when creating the instance --- .../plugins/create/create_blendScene.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/openpype/hosts/blender/plugins/create/create_blendScene.py b/openpype/hosts/blender/plugins/create/create_blendScene.py index 96e63924d3..970be157b9 100644 --- a/openpype/hosts/blender/plugins/create/create_blendScene.py +++ b/openpype/hosts/blender/plugins/create/create_blendScene.py @@ -15,6 +15,8 @@ class CreateBlendScene(plugin.Creator): family = "blendScene" icon = "cubes" + maintain_selection = False + def process(self): """ Run the creator on Blender main thread""" mti = ops.MainThreadItem(self._process) @@ -38,4 +40,29 @@ class CreateBlendScene(plugin.Creator): self.data['task'] = get_current_task_name() lib.imprint(asset_group, self.data) + try: + area = next( + area for area in bpy.context.window.screen.areas + if area.type == 'OUTLINER') + region = next( + region for region in area.regions + if region.type == 'WINDOW') + except StopIteration as e: + raise RuntimeError("Could not find outliner. An outliner space " + "must be in the main Blender window.") from e + + with bpy.context.temp_override( + window=bpy.context.window, + area=area, + region=region, + screen=bpy.context.window.screen + ): + ids = bpy.context.selected_ids + + for id in ids: + if isinstance(id, bpy.types.Collection): + asset_group.children.link(id) + elif isinstance(id, bpy.types.Object): + asset_group.objects.link(id) + return asset_group From b5ebe86b1482d5790296283323e96aab060dbad6 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 3 Nov 2023 16:51:21 +0000 Subject: [PATCH 0727/1233] Hound fixes --- openpype/hosts/blender/plugins/create/create_blendScene.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/blender/plugins/create/create_blendScene.py b/openpype/hosts/blender/plugins/create/create_blendScene.py index 970be157b9..791e741ca7 100644 --- a/openpype/hosts/blender/plugins/create/create_blendScene.py +++ b/openpype/hosts/blender/plugins/create/create_blendScene.py @@ -41,7 +41,7 @@ class CreateBlendScene(plugin.Creator): lib.imprint(asset_group, self.data) try: - area = next( + area = next( area for area in bpy.context.window.screen.areas if area.type == 'OUTLINER') region = next( From e6d13db010609fabe648e43a9745f94e2c355a14 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 3 Nov 2023 17:27:25 +0000 Subject: [PATCH 0728/1233] Keeps the transform when updating --- openpype/hosts/blender/plugins/load/load_blendscene.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openpype/hosts/blender/plugins/load/load_blendscene.py b/openpype/hosts/blender/plugins/load/load_blendscene.py index fe7afb3119..34030d9d84 100644 --- a/openpype/hosts/blender/plugins/load/load_blendscene.py +++ b/openpype/hosts/blender/plugins/load/load_blendscene.py @@ -136,13 +136,18 @@ class BlendSceneLoader(plugin.AssetLoader): f"The asset is not loaded: {container['objectName']}" ) + # Get the parents of the members of the asset group, so we can + # re-link them after the update. + # Also gets the transform for each object to reapply after the update. collection_parents = {} + member_transforms = {} members = asset_group.get(AVALON_PROPERTY).get("members", []) loaded_collections = {c for c in bpy.data.collections if c in members} loaded_collections.add(bpy.data.collections.get(AVALON_CONTAINERS)) for member in members: if isinstance(member, bpy.types.Object): member_parents = set(member.users_collection) + member_transforms[member.name] = member.matrix_basis.copy() elif isinstance(member, bpy.types.Collection): member_parents = { c for c in bpy.data.collections if c.user_of_id(member)} @@ -167,6 +172,9 @@ class BlendSceneLoader(plugin.AssetLoader): parent.objects.link(member) elif isinstance(member, bpy.types.Collection): parent.children.link(member) + if (member.name in member_transforms and + isinstance(member, bpy.types.Object)): + member.matrix_basis = member_transforms[member.name] avalon_container = bpy.data.collections.get(AVALON_CONTAINERS) avalon_container.children.link(asset_group) From 4a9f1e7d785298b4df94a1a61bc983cdc73c37b9 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 3 Nov 2023 17:29:12 +0000 Subject: [PATCH 0729/1233] Hound fixes --- openpype/hosts/blender/plugins/load/load_blendscene.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/blender/plugins/load/load_blendscene.py b/openpype/hosts/blender/plugins/load/load_blendscene.py index 34030d9d84..28dcf4fc70 100644 --- a/openpype/hosts/blender/plugins/load/load_blendscene.py +++ b/openpype/hosts/blender/plugins/load/load_blendscene.py @@ -174,7 +174,7 @@ class BlendSceneLoader(plugin.AssetLoader): parent.children.link(member) if (member.name in member_transforms and isinstance(member, bpy.types.Object)): - member.matrix_basis = member_transforms[member.name] + member.matrix_basis = member_transforms[member.name] avalon_container = bpy.data.collections.get(AVALON_CONTAINERS) avalon_container.children.link(asset_group) From 8f4d490af25ad8765c6f7ae056a57e00b97a228f Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 3 Nov 2023 17:30:25 +0000 Subject: [PATCH 0730/1233] Hound fixes --- openpype/hosts/blender/plugins/load/load_blendscene.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/blender/plugins/load/load_blendscene.py b/openpype/hosts/blender/plugins/load/load_blendscene.py index 28dcf4fc70..b1b2c3ba79 100644 --- a/openpype/hosts/blender/plugins/load/load_blendscene.py +++ b/openpype/hosts/blender/plugins/load/load_blendscene.py @@ -172,9 +172,10 @@ class BlendSceneLoader(plugin.AssetLoader): parent.objects.link(member) elif isinstance(member, bpy.types.Collection): parent.children.link(member) - if (member.name in member_transforms and - isinstance(member, bpy.types.Object)): - member.matrix_basis = member_transforms[member.name] + if member.name in member_transforms and isinstance( + member, bpy.types.Object + ): + member.matrix_basis = member_transforms[member.name] avalon_container = bpy.data.collections.get(AVALON_CONTAINERS) avalon_container.children.link(asset_group) From 2a19d5cc548e55bd526f24a70494717fbe5f23c7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 3 Nov 2023 18:57:15 +0100 Subject: [PATCH 0731/1233] fix default type of projects model cache --- openpype/tools/ayon_utils/models/projects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/ayon_utils/models/projects.py b/openpype/tools/ayon_utils/models/projects.py index 4ad53fbbfa..383f676c64 100644 --- a/openpype/tools/ayon_utils/models/projects.py +++ b/openpype/tools/ayon_utils/models/projects.py @@ -87,7 +87,7 @@ def _get_project_items_from_entitiy(projects): class ProjectsModel(object): def __init__(self, controller): - self._projects_cache = CacheItem(default_factory=dict) + self._projects_cache = CacheItem(default_factory=list) self._project_items_by_name = {} self._projects_by_name = {} From 3bffe3b31b3b367e4a24f0446d2b217e3623ecbe Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 3 Nov 2023 18:58:29 +0100 Subject: [PATCH 0732/1233] renamed 'ProjectsModel' to 'ProjectsQtModel' --- openpype/tools/ayon_launcher/ui/projects_widget.py | 4 ++-- openpype/tools/ayon_utils/widgets/__init__.py | 4 ++-- openpype/tools/ayon_utils/widgets/projects_widget.py | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/openpype/tools/ayon_launcher/ui/projects_widget.py b/openpype/tools/ayon_launcher/ui/projects_widget.py index 7dbaec5147..31c36719a6 100644 --- a/openpype/tools/ayon_launcher/ui/projects_widget.py +++ b/openpype/tools/ayon_launcher/ui/projects_widget.py @@ -3,7 +3,7 @@ from qtpy import QtWidgets, QtCore from openpype.tools.flickcharm import FlickCharm from openpype.tools.utils import PlaceholderLineEdit, RefreshButton from openpype.tools.ayon_utils.widgets import ( - ProjectsModel, + ProjectsQtModel, ProjectSortFilterProxy, ) from openpype.tools.ayon_utils.models import PROJECTS_MODEL_SENDER @@ -95,7 +95,7 @@ class ProjectsWidget(QtWidgets.QWidget): projects_view.setSelectionMode(QtWidgets.QListView.NoSelection) flick = FlickCharm(parent=self) flick.activateOn(projects_view) - projects_model = ProjectsModel(controller) + projects_model = ProjectsQtModel(controller) projects_proxy_model = ProjectSortFilterProxy() projects_proxy_model.setSourceModel(projects_model) diff --git a/openpype/tools/ayon_utils/widgets/__init__.py b/openpype/tools/ayon_utils/widgets/__init__.py index 432a249a73..1ef7dfe482 100644 --- a/openpype/tools/ayon_utils/widgets/__init__.py +++ b/openpype/tools/ayon_utils/widgets/__init__.py @@ -1,7 +1,7 @@ from .projects_widget import ( # ProjectsWidget, ProjectsCombobox, - ProjectsModel, + ProjectsQtModel, ProjectSortFilterProxy, ) @@ -25,7 +25,7 @@ from .utils import ( __all__ = ( # "ProjectsWidget", "ProjectsCombobox", - "ProjectsModel", + "ProjectsQtModel", "ProjectSortFilterProxy", "FoldersWidget", diff --git a/openpype/tools/ayon_utils/widgets/projects_widget.py b/openpype/tools/ayon_utils/widgets/projects_widget.py index 05347faca4..9f0f839281 100644 --- a/openpype/tools/ayon_utils/widgets/projects_widget.py +++ b/openpype/tools/ayon_utils/widgets/projects_widget.py @@ -10,11 +10,11 @@ PROJECT_IS_CURRENT_ROLE = QtCore.Qt.UserRole + 4 LIBRARY_PROJECT_SEPARATOR_ROLE = QtCore.Qt.UserRole + 5 -class ProjectsModel(QtGui.QStandardItemModel): +class ProjectsQtModel(QtGui.QStandardItemModel): refreshed = QtCore.Signal() def __init__(self, controller): - super(ProjectsModel, self).__init__() + super(ProjectsQtModel, self).__init__() self._controller = controller self._project_items = {} @@ -402,7 +402,7 @@ class ProjectsCombobox(QtWidgets.QWidget): projects_combobox = QtWidgets.QComboBox(self) combobox_delegate = QtWidgets.QStyledItemDelegate(projects_combobox) projects_combobox.setItemDelegate(combobox_delegate) - projects_model = ProjectsModel(controller) + projects_model = ProjectsQtModel(controller) projects_proxy_model = ProjectSortFilterProxy() projects_proxy_model.setSourceModel(projects_model) projects_combobox.setModel(projects_proxy_model) From 264e3cac79b863c5eb45841860da7789a13e714f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 3 Nov 2023 18:59:02 +0100 Subject: [PATCH 0733/1233] renamed other qt models to contain 'Qt' --- openpype/tools/ayon_loader/ui/folders_widget.py | 4 ++-- openpype/tools/ayon_utils/widgets/__init__.py | 8 ++++---- openpype/tools/ayon_utils/widgets/folders_widget.py | 6 +++--- openpype/tools/ayon_utils/widgets/tasks_widget.py | 8 ++++---- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/openpype/tools/ayon_loader/ui/folders_widget.py b/openpype/tools/ayon_loader/ui/folders_widget.py index 53351f76d9..eaaf7ca617 100644 --- a/openpype/tools/ayon_loader/ui/folders_widget.py +++ b/openpype/tools/ayon_loader/ui/folders_widget.py @@ -8,7 +8,7 @@ from openpype.tools.utils import ( from openpype.style import get_objected_colors from openpype.tools.ayon_utils.widgets import ( - FoldersModel, + FoldersQtModel, FOLDERS_MODEL_SENDER_NAME, ) from openpype.tools.ayon_utils.widgets.folders_widget import FOLDER_ID_ROLE @@ -182,7 +182,7 @@ class UnderlinesFolderDelegate(QtWidgets.QItemDelegate): painter.restore() -class LoaderFoldersModel(FoldersModel): +class LoaderFoldersModel(FoldersQtModel): def __init__(self, *args, **kwargs): super(LoaderFoldersModel, self).__init__(*args, **kwargs) diff --git a/openpype/tools/ayon_utils/widgets/__init__.py b/openpype/tools/ayon_utils/widgets/__init__.py index 1ef7dfe482..f58de17c4a 100644 --- a/openpype/tools/ayon_utils/widgets/__init__.py +++ b/openpype/tools/ayon_utils/widgets/__init__.py @@ -7,13 +7,13 @@ from .projects_widget import ( from .folders_widget import ( FoldersWidget, - FoldersModel, + FoldersQtModel, FOLDERS_MODEL_SENDER_NAME, ) from .tasks_widget import ( TasksWidget, - TasksModel, + TasksQtModel, TASKS_MODEL_SENDER_NAME, ) from .utils import ( @@ -29,11 +29,11 @@ __all__ = ( "ProjectSortFilterProxy", "FoldersWidget", - "FoldersModel", + "FoldersQtModel", "FOLDERS_MODEL_SENDER_NAME", "TasksWidget", - "TasksModel", + "TasksQtModel", "TASKS_MODEL_SENDER_NAME", "get_qt_icon", diff --git a/openpype/tools/ayon_utils/widgets/folders_widget.py b/openpype/tools/ayon_utils/widgets/folders_widget.py index b72a992858..44323a192c 100644 --- a/openpype/tools/ayon_utils/widgets/folders_widget.py +++ b/openpype/tools/ayon_utils/widgets/folders_widget.py @@ -16,7 +16,7 @@ FOLDER_PATH_ROLE = QtCore.Qt.UserRole + 3 FOLDER_TYPE_ROLE = QtCore.Qt.UserRole + 4 -class FoldersModel(QtGui.QStandardItemModel): +class FoldersQtModel(QtGui.QStandardItemModel): """Folders model which cares about refresh of folders. Args: @@ -26,7 +26,7 @@ class FoldersModel(QtGui.QStandardItemModel): refreshed = QtCore.Signal() def __init__(self, controller): - super(FoldersModel, self).__init__() + super(FoldersQtModel, self).__init__() self._controller = controller self._items_by_id = {} @@ -282,7 +282,7 @@ class FoldersWidget(QtWidgets.QWidget): folders_view = TreeView(self) folders_view.setHeaderHidden(True) - folders_model = FoldersModel(controller) + folders_model = FoldersQtModel(controller) folders_proxy_model = RecursiveSortFilterProxyModel() folders_proxy_model.setSourceModel(folders_model) folders_proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive) diff --git a/openpype/tools/ayon_utils/widgets/tasks_widget.py b/openpype/tools/ayon_utils/widgets/tasks_widget.py index a6375c6ae6..f27711acdd 100644 --- a/openpype/tools/ayon_utils/widgets/tasks_widget.py +++ b/openpype/tools/ayon_utils/widgets/tasks_widget.py @@ -12,7 +12,7 @@ ITEM_NAME_ROLE = QtCore.Qt.UserRole + 3 TASK_TYPE_ROLE = QtCore.Qt.UserRole + 4 -class TasksModel(QtGui.QStandardItemModel): +class TasksQtModel(QtGui.QStandardItemModel): """Tasks model which cares about refresh of tasks by folder id. Args: @@ -22,7 +22,7 @@ class TasksModel(QtGui.QStandardItemModel): refreshed = QtCore.Signal() def __init__(self, controller): - super(TasksModel, self).__init__() + super(TasksQtModel, self).__init__() self._controller = controller @@ -285,7 +285,7 @@ class TasksModel(QtGui.QStandardItemModel): if section == 0: return "Tasks" - return super(TasksModel, self).headerData( + return super(TasksQtModel, self).headerData( section, orientation, role ) @@ -310,7 +310,7 @@ class TasksWidget(QtWidgets.QWidget): tasks_view = DeselectableTreeView(self) tasks_view.setIndentation(0) - tasks_model = TasksModel(controller) + tasks_model = TasksQtModel(controller) tasks_proxy_model = QtCore.QSortFilterProxyModel() tasks_proxy_model.setSourceModel(tasks_model) tasks_proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive) From 86e4bed1514a1506200ac3ca9c51956c95414b2b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 3 Nov 2023 18:59:52 +0100 Subject: [PATCH 0734/1233] validate that item on which is clicked is enabled --- openpype/tools/ayon_launcher/ui/projects_widget.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/openpype/tools/ayon_launcher/ui/projects_widget.py b/openpype/tools/ayon_launcher/ui/projects_widget.py index 31c36719a6..38c7f62bd5 100644 --- a/openpype/tools/ayon_launcher/ui/projects_widget.py +++ b/openpype/tools/ayon_launcher/ui/projects_widget.py @@ -133,9 +133,14 @@ class ProjectsWidget(QtWidgets.QWidget): return self._projects_model.has_content() def _on_view_clicked(self, index): - if index.isValid(): - project_name = index.data(QtCore.Qt.DisplayRole) - self._controller.set_selected_project(project_name) + if not index.isValid(): + return + model = index.model() + flags = model.flags(index) + if not flags & QtCore.Qt.ItemIsEnabled: + return + project_name = index.data(QtCore.Qt.DisplayRole) + self._controller.set_selected_project(project_name) def _on_project_filter_change(self, text): self._projects_proxy_model.setFilterFixedString(text) From 05748bbb9291424ed82495a63bc581b2437fe772 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 3 Nov 2023 19:00:51 +0100 Subject: [PATCH 0735/1233] projects model pass sender value --- openpype/tools/ayon_utils/widgets/projects_widget.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/tools/ayon_utils/widgets/projects_widget.py b/openpype/tools/ayon_utils/widgets/projects_widget.py index 9f0f839281..804b7a05ac 100644 --- a/openpype/tools/ayon_utils/widgets/projects_widget.py +++ b/openpype/tools/ayon_utils/widgets/projects_widget.py @@ -180,7 +180,9 @@ class ProjectsQtModel(QtGui.QStandardItemModel): refresh_thread.start() def _query_project_items(self): - return self._controller.get_project_items() + return self._controller.get_project_items( + sender=PROJECTS_MODEL_SENDER + ) def _refresh_finished(self): # TODO check if failed From 42c32f81969399bc900fdfe2900d0da0e8c3edea Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 3 Nov 2023 19:01:26 +0100 Subject: [PATCH 0736/1233] projects model returns 'None' if is in middle of refreshing --- openpype/tools/ayon_utils/models/projects.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/openpype/tools/ayon_utils/models/projects.py b/openpype/tools/ayon_utils/models/projects.py index 383f676c64..36d53edc24 100644 --- a/openpype/tools/ayon_utils/models/projects.py +++ b/openpype/tools/ayon_utils/models/projects.py @@ -103,8 +103,18 @@ class ProjectsModel(object): self._refresh_projects_cache() def get_project_items(self, sender): + """ + + Args: + sender (str): Name of sender who asked for items. + + Returns: + Union[list[ProjectItem], None]: List of project items, or None + if model is refreshing. + """ + if not self._projects_cache.is_valid: - self._refresh_projects_cache(sender) + return self._refresh_projects_cache(sender) return self._projects_cache.get_data() def get_project_entity(self, project_name): @@ -136,11 +146,12 @@ class ProjectsModel(object): def _refresh_projects_cache(self, sender=None): if self._is_refreshing: - return + return None with self._project_refresh_event_manager(sender): project_items = self._query_projects() self._projects_cache.update_data(project_items) + return self._projects_cache.get_data() def _query_projects(self): projects = ayon_api.get_projects(fields=["name", "active", "library"]) From 5a0b2f69153f71469b60acf78d588c3583493764 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 3 Nov 2023 19:01:40 +0100 Subject: [PATCH 0737/1233] projects model handle cases when model is refreshing --- openpype/tools/ayon_utils/widgets/projects_widget.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/openpype/tools/ayon_utils/widgets/projects_widget.py b/openpype/tools/ayon_utils/widgets/projects_widget.py index 804b7a05ac..2beee29cb9 100644 --- a/openpype/tools/ayon_utils/widgets/projects_widget.py +++ b/openpype/tools/ayon_utils/widgets/projects_widget.py @@ -1,3 +1,5 @@ +import uuid + from qtpy import QtWidgets, QtCore, QtGui from openpype.tools.ayon_utils.models import PROJECTS_MODEL_SENDER @@ -187,11 +189,14 @@ class ProjectsQtModel(QtGui.QStandardItemModel): def _refresh_finished(self): # TODO check if failed result = self._refresh_thread.get_result() - - self._fill_items(result) + if result is not None: + self._fill_items(result) self._refresh_thread = None - self.refreshed.emit() + if result is None: + self._refresh() + else: + self.refreshed.emit() def _fill_items(self, project_items): new_project_names = { From 779cea668862a9354816f5fb9c62bf0d71496951 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Fri, 3 Nov 2023 20:04:16 +0200 Subject: [PATCH 0738/1233] update dictionary keys to be frames with handles --- .../hosts/houdini/plugins/publish/collect_review_data.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_review_data.py b/openpype/hosts/houdini/plugins/publish/collect_review_data.py index 3efb75e66c..9671945b9a 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_review_data.py +++ b/openpype/hosts/houdini/plugins/publish/collect_review_data.py @@ -6,6 +6,8 @@ class CollectHoudiniReviewData(pyblish.api.InstancePlugin): """Collect Review Data.""" label = "Collect Review Data" + # This specific order value is used so that + # this plugin runs after CollectRopFrameRange order = pyblish.api.CollectorOrder + 0.1 hosts = ["houdini"] families = ["review"] @@ -41,8 +43,8 @@ class CollectHoudiniReviewData(pyblish.api.InstancePlugin): return if focal_length_parm.isTimeDependent(): - start = instance.data["frameStart"] - end = instance.data["frameEnd"] + 1 + start = instance.data["frameStartHandle"] + end = instance.data["frameEndHandle"] + 1 focal_length = [ focal_length_parm.evalAsFloatAtFrame(t) for t in range(int(start), int(end)) From 15a52469354238165236ed890d10679e932f4439 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Fri, 3 Nov 2023 20:07:15 +0200 Subject: [PATCH 0739/1233] Resolve Hound --- openpype/hosts/houdini/plugins/publish/extract_fbx.py | 2 +- .../hosts/houdini/plugins/publish/extract_redshift_proxy.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/extract_fbx.py b/openpype/hosts/houdini/plugins/publish/extract_fbx.py index 1be61ecce1..7dc193c6a9 100644 --- a/openpype/hosts/houdini/plugins/publish/extract_fbx.py +++ b/openpype/hosts/houdini/plugins/publish/extract_fbx.py @@ -40,7 +40,7 @@ class ExtractFBX(publish.Extractor): } # A single frame may also be rendered without start/end frame. - if "frameStartHandle" in instance.data and "frameEndHandle" in instance.data: + if "frameStartHandle" in instance.data and "frameEndHandle" in instance.data: # noqa representation["frameStart"] = instance.data["frameStartHandle"] representation["frameEnd"] = instance.data["frameEndHandle"] diff --git a/openpype/hosts/houdini/plugins/publish/extract_redshift_proxy.py b/openpype/hosts/houdini/plugins/publish/extract_redshift_proxy.py index 18cbd5712e..ef5991924f 100644 --- a/openpype/hosts/houdini/plugins/publish/extract_redshift_proxy.py +++ b/openpype/hosts/houdini/plugins/publish/extract_redshift_proxy.py @@ -44,7 +44,7 @@ class ExtractRedshiftProxy(publish.Extractor): } # A single frame may also be rendered without start/end frame. - if "frameStartHandle" in instance.data and "frameEndHandle" in instance.data: + if "frameStartHandle" in instance.data and "frameEndHandle" in instance.data: # noqa representation["frameStart"] = instance.data["frameStartHandle"] representation["frameEnd"] = instance.data["frameEndHandle"] From d87506f8af15123cdc0d07176dba26bce262a434 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 3 Nov 2023 19:12:17 +0100 Subject: [PATCH 0740/1233] removed unused import --- openpype/tools/ayon_utils/widgets/projects_widget.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/tools/ayon_utils/widgets/projects_widget.py b/openpype/tools/ayon_utils/widgets/projects_widget.py index 2beee29cb9..f98bfcdf8a 100644 --- a/openpype/tools/ayon_utils/widgets/projects_widget.py +++ b/openpype/tools/ayon_utils/widgets/projects_widget.py @@ -1,5 +1,3 @@ -import uuid - from qtpy import QtWidgets, QtCore, QtGui from openpype.tools.ayon_utils.models import PROJECTS_MODEL_SENDER From bd94b8dcc972fe8f954b9b10de46350d1515864c Mon Sep 17 00:00:00 2001 From: Ynbot Date: Sat, 4 Nov 2023 03:25:15 +0000 Subject: [PATCH 0741/1233] [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 4865fcfb31..c6ebd65e9c 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.17.5-nightly.2" +__version__ = "3.17.5-nightly.3" From 2ab07ec7ede740c13d98e8abeca9c02f73db4182 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 4 Nov 2023 03:25:51 +0000 Subject: [PATCH 0742/1233] 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 249da3da0e..7a1fe9d83e 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.17.5-nightly.3 - 3.17.5-nightly.2 - 3.17.5-nightly.1 - 3.17.4 @@ -134,7 +135,6 @@ body: - 3.15.1-nightly.5 - 3.15.1-nightly.4 - 3.15.1-nightly.3 - - 3.15.1-nightly.2 validations: required: true - type: dropdown From b079ca8d0f76ede60866dcb26f506189cbfcea9e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 6 Nov 2023 15:41:20 +0800 Subject: [PATCH 0743/1233] clean up the duplicated variable --- openpype/settings/ayon_settings.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index 88fbbd5124..fa73199269 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -649,7 +649,6 @@ def _convert_3dsmax_project_settings(ayon_settings, output): attributes = {} ayon_publish["ValidateAttributes"]["attributes"] = attributes - ayon_publish = ayon_max["publish"] if "ValidateLoadedPlugin" in ayon_publish: family_plugin_mapping = ( ayon_publish["ValidateLoadedPlugin"]["family_plugins_mapping"] From 7b9c6c3b99900dde8ea841a2bfe25ea02f4cbeff Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 6 Nov 2023 10:30:13 +0000 Subject: [PATCH 0744/1233] Added validator to verify that the instance is not empty --- .../publish/validate_instance_empty.py | 18 ++++++++++ .../defaults/project_settings/blender.json | 5 +++ .../schemas/schema_blender_publish.json | 16 +++++++++ .../server/settings/publish_plugins.py | 35 ++++++++++++------- server_addon/blender/server/version.py | 2 +- 5 files changed, 62 insertions(+), 14 deletions(-) create mode 100644 openpype/hosts/blender/plugins/publish/validate_instance_empty.py diff --git a/openpype/hosts/blender/plugins/publish/validate_instance_empty.py b/openpype/hosts/blender/plugins/publish/validate_instance_empty.py new file mode 100644 index 0000000000..66d8b45e1e --- /dev/null +++ b/openpype/hosts/blender/plugins/publish/validate_instance_empty.py @@ -0,0 +1,18 @@ +import bpy + +import pyblish.api + + +class ValidateInstanceEmpty(pyblish.api.InstancePlugin): + """Validator to verify that the instance is not empty""" + + order = pyblish.api.ValidatorOrder - 0.01 + hosts = ["blender"] + families = ["blendScene"] + label = "Validate Instance is not Empty" + optional = False + + def process(self, instance): + collection = bpy.data.collections[instance.name] + if not (collection.objects or collection.children): + raise RuntimeError(f"Instance {instance.name} is empty.") diff --git a/openpype/settings/defaults/project_settings/blender.json b/openpype/settings/defaults/project_settings/blender.json index 7fb8c333a6..385e97ef91 100644 --- a/openpype/settings/defaults/project_settings/blender.json +++ b/openpype/settings/defaults/project_settings/blender.json @@ -71,6 +71,11 @@ "optional": false, "active": true }, + "ValidateInstanceEmpty": { + "enabled": true, + "optional": false, + "active": true + }, "ExtractBlend": { "enabled": true, "optional": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json index b84c663e6c..e4f1096223 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json @@ -79,6 +79,22 @@ } ] }, + { + "type": "collapsible-wrap", + "label": "BlendScene", + "children": [ + { + "type": "schema_template", + "name": "template_publish_plugin", + "template_data": [ + { + "key": "ValidateInstanceEmpty", + "label": "Validate Instance is not Empty" + } + ] + } + ] + }, { "type": "collapsible-wrap", "label": "Render", diff --git a/server_addon/blender/server/settings/publish_plugins.py b/server_addon/blender/server/settings/publish_plugins.py index 27dc0b232f..bb68b40cbb 100644 --- a/server_addon/blender/server/settings/publish_plugins.py +++ b/server_addon/blender/server/settings/publish_plugins.py @@ -61,26 +61,16 @@ class PublishPuginsModel(BaseSettingsModel): ValidateCameraZeroKeyframe: ValidatePluginModel = Field( default_factory=ValidatePluginModel, title="Validate Camera Zero Keyframe", - section="Validators" + section="General Validators" ) ValidateFileSaved: ValidateFileSavedModel = Field( default_factory=ValidateFileSavedModel, title="Validate File Saved", - section="Validators" - ) - ValidateRenderCameraIsSet: ValidatePluginModel = Field( - default_factory=ValidatePluginModel, - title="Validate Render Camera Is Set", - section="Validators" - ) - ValidateDeadlinePublish: ValidatePluginModel = Field( - default_factory=ValidatePluginModel, - title="Validate Render Output for Deadline", - section="Validators" ) ValidateMeshHasUvs: ValidatePluginModel = Field( default_factory=ValidatePluginModel, - title="Validate Mesh Has Uvs" + title="Validate Mesh Has Uvs", + section="Model Validators" ) ValidateMeshNoNegativeScale: ValidatePluginModel = Field( default_factory=ValidatePluginModel, @@ -94,6 +84,20 @@ class PublishPuginsModel(BaseSettingsModel): default_factory=ValidatePluginModel, title="Validate No Colons In Name" ) + ValidateInstanceEmpty: ValidatePluginModel = Field( + default_factory=ValidatePluginModel, + title="Validate Instance is not Empty", + section="BlendScene Validators" + ) + ValidateRenderCameraIsSet: ValidatePluginModel = Field( + default_factory=ValidatePluginModel, + title="Validate Render Camera Is Set", + section="Render Validators" + ) + ValidateDeadlinePublish: ValidatePluginModel = Field( + default_factory=ValidatePluginModel, + title="Validate Render Output for Deadline", + ) ExtractBlend: ExtractBlendModel = Field( default_factory=ExtractBlendModel, title="Extract Blend", @@ -179,6 +183,11 @@ DEFAULT_BLENDER_PUBLISH_SETTINGS = { "optional": False, "active": True }, + "ValidateInstanceEmpty": { + "enabled": True, + "optional": False, + "active": True + }, "ExtractBlend": { "enabled": True, "optional": True, diff --git a/server_addon/blender/server/version.py b/server_addon/blender/server/version.py index ae7362549b..1276d0254f 100644 --- a/server_addon/blender/server/version.py +++ b/server_addon/blender/server/version.py @@ -1 +1 @@ -__version__ = "0.1.3" +__version__ = "0.1.5" From 026fa6567680e4079c20158f16edad466b777999 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 6 Nov 2023 21:19:30 +0800 Subject: [PATCH 0745/1233] value tweaks on aspect ratio --- openpype/hosts/max/api/preview_animation.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/max/api/preview_animation.py b/openpype/hosts/max/api/preview_animation.py index bd0fee3658..bbf05f4ca9 100644 --- a/openpype/hosts/max/api/preview_animation.py +++ b/openpype/hosts/max/api/preview_animation.py @@ -185,8 +185,8 @@ def _render_preview_animation_max_pre_2024( # get the screenshot percent = percentSize / 100.0 - res_width = int(round(width * percent)) - res_height = int(round(height * percent)) + res_width = width * percent + res_height = height * percent frame_template = "{}.{{:04}}.{}".format(filepath, ext) frame_template.replace("\\", "/") files = [] @@ -212,7 +212,7 @@ def _render_preview_animation_max_pre_2024( widthCrop = dib_height * renderRatio leftEdge = int((dib_width - widthCrop) / 2.0) tempImage_bmp = rt.bitmap(widthCrop, dib_height) - src_box_value = rt.Box2(0, leftEdge, widthCrop, dib_height) + src_box_value = rt.Box2(leftEdge, 0, widthCrop, dib_height) rt.pasteBitmap(dib, tempImage_bmp, src_box_value, rt.Point2(0, 0)) # copy the bitmap and close it rt.copy(tempImage_bmp, preview_res) From 8f21d653e079662eef75a435ea97b26d101d7567 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 6 Nov 2023 21:32:43 +0800 Subject: [PATCH 0746/1233] use product_types instead of families --- server_addon/core/server/settings/tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/core/server/settings/tools.py b/server_addon/core/server/settings/tools.py index d7c7b367b7..0dd9d396ae 100644 --- a/server_addon/core/server/settings/tools.py +++ b/server_addon/core/server/settings/tools.py @@ -489,7 +489,7 @@ DEFAULT_TOOLS_VALUES = { "template_name": "publish_online" }, { - "families": [ + "product_types": [ "tycache" ], "hosts": [ From 1d34d5b1ae48ef69e12d30585b03a9352203f498 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 6 Nov 2023 18:04:08 +0100 Subject: [PATCH 0747/1233] use "asset" value instead of name from assetEntity --- .../hosts/maya/plugins/publish/validate_instance_in_context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 4ded57137c..edfb002278 100644 --- a/openpype/hosts/maya/plugins/publish/validate_instance_in_context.py +++ b/openpype/hosts/maya/plugins/publish/validate_instance_in_context.py @@ -74,4 +74,4 @@ class ValidateInstanceInContext(pyblish.api.InstancePlugin, @staticmethod def get_context_asset(instance): - return instance.context.data["assetEntity"]["name"] + return instance.context.data["asset"] From f2e14e7f4aa4c9d1557e3fdacc05cc564c21d7bb Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 6 Nov 2023 18:05:37 +0100 Subject: [PATCH 0748/1233] use asset and project from instance and context data --- openpype/hosts/maya/plugins/publish/collect_review.py | 8 ++++---- .../hosts/maya/plugins/publish/validate_model_name.py | 6 ++++-- .../hosts/maya/plugins/publish/validate_shader_name.py | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_review.py b/openpype/hosts/maya/plugins/publish/collect_review.py index 586939a3b8..0930da8f27 100644 --- a/openpype/hosts/maya/plugins/publish/collect_review.py +++ b/openpype/hosts/maya/plugins/publish/collect_review.py @@ -3,7 +3,7 @@ from maya import cmds, mel import pyblish.api from openpype.client import get_subset_by_name -from openpype.pipeline import legacy_io, KnownPublishError +from openpype.pipeline import KnownPublishError from openpype.hosts.maya.api import lib @@ -116,10 +116,10 @@ class CollectReview(pyblish.api.InstancePlugin): instance.data['remove'] = True else: - task = legacy_io.Session["AVALON_TASK"] - legacy_subset_name = task + 'Review' + project_name = instance.context.data["projectName"] asset_doc = instance.context.data['assetEntity'] - project_name = legacy_io.active_project() + task = instance.context.data["task"] + legacy_subset_name = task + 'Review' subset_doc = get_subset_by_name( project_name, legacy_subset_name, diff --git a/openpype/hosts/maya/plugins/publish/validate_model_name.py b/openpype/hosts/maya/plugins/publish/validate_model_name.py index f4c1aa39c7..11f59bb439 100644 --- a/openpype/hosts/maya/plugins/publish/validate_model_name.py +++ b/openpype/hosts/maya/plugins/publish/validate_model_name.py @@ -67,13 +67,15 @@ class ValidateModelName(pyblish.api.InstancePlugin, regex = cls.top_level_regex r = re.compile(regex) m = r.match(top_group) + project_name = instance.context.data["projectName"] + current_asset_name = instance.context.data["asset"] if m is None: cls.log.error("invalid name on: {}".format(top_group)) cls.log.error("name doesn't match regex {}".format(regex)) invalid.append(top_group) else: if "asset" in r.groupindex: - if m.group("asset") != legacy_io.Session["AVALON_ASSET"]: + if m.group("asset") != current_asset_name: cls.log.error("Invalid asset name in top level group.") return top_group if "subset" in r.groupindex: @@ -81,7 +83,7 @@ class ValidateModelName(pyblish.api.InstancePlugin, cls.log.error("Invalid subset name in top level group.") return top_group if "project" in r.groupindex: - if m.group("project") != legacy_io.Session["AVALON_PROJECT"]: + if m.group("project") != project_name: cls.log.error("Invalid project name in top level group.") return top_group diff --git a/openpype/hosts/maya/plugins/publish/validate_shader_name.py b/openpype/hosts/maya/plugins/publish/validate_shader_name.py index 36bb2c1fee..d6486dea7f 100644 --- a/openpype/hosts/maya/plugins/publish/validate_shader_name.py +++ b/openpype/hosts/maya/plugins/publish/validate_shader_name.py @@ -51,7 +51,7 @@ class ValidateShaderName(pyblish.api.InstancePlugin, descendants = cmds.ls(descendants, noIntermediate=True, long=True) shapes = cmds.ls(descendants, type=["nurbsSurface", "mesh"], long=True) - asset_name = instance.data.get("asset", None) + asset_name = instance.data.get("asset") # Check the number of connected shadingEngines per shape regex_compile = re.compile(cls.regex) From b7bec4b4e4fea704b0706013e76ac03de325ebee Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 6 Nov 2023 18:28:51 +0100 Subject: [PATCH 0749/1233] use correct label for follow workfile version --- server_addon/core/server/settings/publish_plugins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/core/server/settings/publish_plugins.py b/server_addon/core/server/settings/publish_plugins.py index 69a759465e..93d8db964d 100644 --- a/server_addon/core/server/settings/publish_plugins.py +++ b/server_addon/core/server/settings/publish_plugins.py @@ -21,7 +21,7 @@ class ValidateBaseModel(BaseSettingsModel): class CollectAnatomyInstanceDataModel(BaseSettingsModel): _isGroup = True follow_workfile_version: bool = Field( - True, title="Collect Anatomy Instance Data" + True, title="Follow workfile version" ) From efa9b8fe4c1b22af0aa561f6ea9132687b703f5e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 6 Nov 2023 18:39:58 +0100 Subject: [PATCH 0750/1233] Remove unused `instanceToggled` callbacks (#5862) --- openpype/hosts/aftereffects/api/pipeline.py | 10 ---------- openpype/hosts/nuke/api/pipeline.py | 22 --------------------- openpype/hosts/photoshop/api/pipeline.py | 10 ---------- openpype/hosts/tvpaint/api/pipeline.py | 4 ---- 4 files changed, 46 deletions(-) diff --git a/openpype/hosts/aftereffects/api/pipeline.py b/openpype/hosts/aftereffects/api/pipeline.py index 8fc7a70dd8..e059f7c272 100644 --- a/openpype/hosts/aftereffects/api/pipeline.py +++ b/openpype/hosts/aftereffects/api/pipeline.py @@ -74,11 +74,6 @@ class AfterEffectsHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): register_loader_plugin_path(LOAD_PATH) register_creator_plugin_path(CREATE_PATH) - log.info(PUBLISH_PATH) - - pyblish.api.register_callback( - "instanceToggled", on_pyblish_instance_toggled - ) register_event_callback("application.launched", application_launch) @@ -186,11 +181,6 @@ def application_launch(): check_inventory() -def on_pyblish_instance_toggled(instance, old_value, new_value): - """Toggle layer visibility on instance toggles.""" - instance[0].Visible = new_value - - def ls(): """Yields containers from active AfterEffects document. diff --git a/openpype/hosts/nuke/api/pipeline.py b/openpype/hosts/nuke/api/pipeline.py index f6ba33f00f..ba4d66ab63 100644 --- a/openpype/hosts/nuke/api/pipeline.py +++ b/openpype/hosts/nuke/api/pipeline.py @@ -129,9 +129,6 @@ class NukeHost( register_event_callback("workio.open_file", check_inventory_versions) register_event_callback("taskChanged", change_context_label) - pyblish.api.register_callback( - "instanceToggled", on_pyblish_instance_toggled) - _install_menu() # add script menu @@ -402,25 +399,6 @@ def add_shortcuts_from_presets(): log.error(e) -def on_pyblish_instance_toggled(instance, old_value, new_value): - """Toggle node passthrough states on instance toggles.""" - - log.info("instance toggle: {}, old_value: {}, new_value:{} ".format( - instance, old_value, new_value)) - - # Whether instances should be passthrough based on new value - - with viewer_update_and_undo_stop(): - n = instance[0] - try: - n["publish"].value() - except ValueError: - n = add_publish_knob(n) - log.info(" `Publish` knob was added to write node..") - - n["publish"].setValue(new_value) - - def containerise(node, name, namespace, diff --git a/openpype/hosts/photoshop/api/pipeline.py b/openpype/hosts/photoshop/api/pipeline.py index 56ae2a4c25..4e0dbcad06 100644 --- a/openpype/hosts/photoshop/api/pipeline.py +++ b/openpype/hosts/photoshop/api/pipeline.py @@ -48,11 +48,6 @@ class PhotoshopHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): pyblish.api.register_plugin_path(PUBLISH_PATH) register_loader_plugin_path(LOAD_PATH) register_creator_plugin_path(CREATE_PATH) - log.info(PUBLISH_PATH) - - pyblish.api.register_callback( - "instanceToggled", on_pyblish_instance_toggled - ) register_event_callback("application.launched", on_application_launch) @@ -177,11 +172,6 @@ def on_application_launch(): check_inventory() -def on_pyblish_instance_toggled(instance, old_value, new_value): - """Toggle layer visibility on instance toggles.""" - instance[0].Visible = new_value - - def ls(): """Yields containers from active Photoshop document diff --git a/openpype/hosts/tvpaint/api/pipeline.py b/openpype/hosts/tvpaint/api/pipeline.py index a84f196f09..c125da1533 100644 --- a/openpype/hosts/tvpaint/api/pipeline.py +++ b/openpype/hosts/tvpaint/api/pipeline.py @@ -84,10 +84,6 @@ class TVPaintHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): register_loader_plugin_path(load_dir) register_creator_plugin_path(create_dir) - registered_callbacks = ( - pyblish.api.registered_callbacks().get("instanceToggled") or [] - ) - register_event_callback("application.launched", self.initial_launch) register_event_callback("application.exit", self.application_exit) From 3fdccc886f4d8f785c0d9297264b15a6904d496d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 7 Nov 2023 15:46:12 +0800 Subject: [PATCH 0751/1233] make sure the extraction would be skipped if there is empty list of animated_skeleton --- openpype/hosts/maya/plugins/publish/extract_fbx_animation.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py index 8288bc9329..ad9ca385dc 100644 --- a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py +++ b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py @@ -42,6 +42,11 @@ class ExtractFBXAnimation(publish.Extractor): # Export from the rig's namespace so that the exported # FBX does not include the namespace but preserves the node # names as existing in the rig workfile + if not out_members: + self.log.debug( + "Top group of animated skeleton not found...skipping extraction") + return + namespace = get_namespace(out_members[0]) relative_out_members = [ strip_namespace(node, namespace) for node in out_members From 69c33f9c8f560c3c92407ae13f471cba36b9790e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 7 Nov 2023 15:52:19 +0800 Subject: [PATCH 0752/1233] hound --- openpype/hosts/maya/plugins/publish/extract_fbx_animation.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py index ad9ca385dc..c6f8029e7d 100644 --- a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py +++ b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py @@ -44,7 +44,8 @@ class ExtractFBXAnimation(publish.Extractor): # names as existing in the rig workfile if not out_members: self.log.debug( - "Top group of animated skeleton not found...skipping extraction") + "Top group of animated skeleton not found.." + "skipping extraction") return namespace = get_namespace(out_members[0]) From 93101debd87d1d72cbeec69f2131f093f06c27a7 Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Tue, 7 Nov 2023 10:23:55 +0100 Subject: [PATCH 0753/1233] collect workfile remove unused import and comment --- openpype/hosts/blender/plugins/publish/collect_workfile.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/collect_workfile.py b/openpype/hosts/blender/plugins/publish/collect_workfile.py index e431405e80..01c033084b 100644 --- a/openpype/hosts/blender/plugins/publish/collect_workfile.py +++ b/openpype/hosts/blender/plugins/publish/collect_workfile.py @@ -1,6 +1,5 @@ from pathlib import Path -import bpy from pyblish.api import InstancePlugin, CollectorOrder @@ -30,7 +29,7 @@ class CollectWorkfile(InstancePlugin): { "name": ext.lstrip("."), "ext": ext, - "files": filepath.name, # TODO resources + "files": filepath.name, "stagingDir": filepath.parent, } ], From 7f5986a683299a10d65789cc9ac86362d97262d4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 7 Nov 2023 11:43:08 +0100 Subject: [PATCH 0754/1233] fix maya workfile creator --- openpype/hosts/maya/plugins/create/create_workfile.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/create/create_workfile.py b/openpype/hosts/maya/plugins/create/create_workfile.py index 7282fc6b8b..198f9c4a36 100644 --- a/openpype/hosts/maya/plugins/create/create_workfile.py +++ b/openpype/hosts/maya/plugins/create/create_workfile.py @@ -72,7 +72,10 @@ class CreateWorkfile(plugin.MayaCreatorBase, AutoCreator): ) asset_name = get_asset_name_identifier(asset_doc) - current_instance["asset"] = asset_name + if AYON_SERVER_ENABLED: + current_instance["folderPath"] = asset_name + else: + current_instance["asset"] = asset_name current_instance["task"] = task_name current_instance["subset"] = subset_name From 1e819a26838570aaf93cf89e23adf5f673413a4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Tue, 7 Nov 2023 13:28:26 +0100 Subject: [PATCH 0755/1233] Update openpype/hosts/traypublisher/api/editorial.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/traypublisher/api/editorial.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/traypublisher/api/editorial.py b/openpype/hosts/traypublisher/api/editorial.py index 2f5e709ffc..ddb369468f 100644 --- a/openpype/hosts/traypublisher/api/editorial.py +++ b/openpype/hosts/traypublisher/api/editorial.py @@ -321,7 +321,8 @@ class ShotMetadataSolver: # generate hierarchy path from parents hierarchy_path = self._create_hierarchy_path(parents) - + if hierarchy_path: + hierarchy_path = f"/{hierarchy_path}" return shot_name, { "hierarchy": hierarchy_path, "folderPath": f"{hierarchy_path}/{shot_name}", From ce413045130184c6da299d1f3a4a441a550a146d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 7 Nov 2023 13:29:15 +0100 Subject: [PATCH 0756/1233] ignore if passed icon definition is None --- openpype/tools/ayon_utils/widgets/utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/tools/ayon_utils/widgets/utils.py b/openpype/tools/ayon_utils/widgets/utils.py index 8bc3b1ea9b..2817b5efc0 100644 --- a/openpype/tools/ayon_utils/widgets/utils.py +++ b/openpype/tools/ayon_utils/widgets/utils.py @@ -54,6 +54,8 @@ class _IconsCache: @classmethod def get_icon(cls, icon_def): + if not icon_def: + return None icon_type = icon_def["type"] cache_key = cls._get_cache_key(icon_def) cache = cls._cache.get(cache_key) From 46761a028f6a6c91815f6b8c7705abf706f48380 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 7 Nov 2023 13:31:04 +0100 Subject: [PATCH 0757/1233] traypublisher: editorial better handling hierarchy for folder path --- openpype/hosts/traypublisher/api/editorial.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/traypublisher/api/editorial.py b/openpype/hosts/traypublisher/api/editorial.py index ddb369468f..1a83fcecbd 100644 --- a/openpype/hosts/traypublisher/api/editorial.py +++ b/openpype/hosts/traypublisher/api/editorial.py @@ -322,10 +322,13 @@ class ShotMetadataSolver: # generate hierarchy path from parents hierarchy_path = self._create_hierarchy_path(parents) if hierarchy_path: - hierarchy_path = f"/{hierarchy_path}" + folder_path = f"/{hierarchy_path}/{shot_name}" + else: + folder_path = f"/{shot_name}" + return shot_name, { "hierarchy": hierarchy_path, - "folderPath": f"{hierarchy_path}/{shot_name}", + "folderPath": folder_path, "parents": parents, "tasks": tasks } From 62af7138e11cc11509b91ec2f1ee7cc1995fb10b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 7 Nov 2023 13:38:43 +0100 Subject: [PATCH 0758/1233] traypublisher: editorial ayon attribute only https://github.com/ynput/OpenPype/pull/5873#discussion_r1384655823 --- .../hosts/traypublisher/plugins/create/create_editorial.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py index 5dc3893697..59f24a2a2b 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_editorial.py +++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py @@ -683,13 +683,15 @@ or updating already created. Publishing will create OTIO file. # create creator attributes creator_attributes = { "asset_name": shot_name, - "Folder path": shot_metadata["folderPath"], "Parent hierarchy path": shot_metadata["hierarchy"], "workfile_start_frame": workfile_start_frame, "fps": fps, "handle_start": int(handle_start), "handle_end": int(handle_end) } + if AYON_SERVER_ENABLED: + creator_attributes["folderPath"] = shot_metadata["folderPath"] + creator_attributes.update(timing_data) # create shared new instance data From bf96b15b90e04b0721af853adeb161707ebd5b8c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 7 Nov 2023 13:50:44 +0100 Subject: [PATCH 0759/1233] center publisher window on first show --- openpype/tools/publisher/window.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index 312cf1dd5c..2416763c27 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -15,6 +15,7 @@ from openpype.tools.utils import ( MessageOverlayObject, PixmapLabel, ) +from openpype.tools.utils.lib import center_window from .constants import ResetKeySequence from .publish_report_viewer import PublishReportViewerWidget @@ -529,6 +530,7 @@ class PublisherWindow(QtWidgets.QDialog): def _on_first_show(self): self.resize(self.default_width, self.default_height) self.setStyleSheet(style.load_stylesheet()) + center_window(self) self._reset_on_show = self._reset_on_first_show def _on_show_timer(self): From 414df2370346aabbb555212f8123bbcb35817ea2 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Tue, 7 Nov 2023 13:07:26 +0000 Subject: [PATCH 0760/1233] Introduce app_group flag (#5869) --- openpype/cli.py | 7 +++++-- openpype/pype_commands.py | 5 ++++- tests/conftest.py | 10 ++++++++++ tests/lib/testing_classes.py | 9 ++++++--- 4 files changed, 25 insertions(+), 6 deletions(-) diff --git a/openpype/cli.py b/openpype/cli.py index 7422f32f13..f0fe550a1f 100644 --- a/openpype/cli.py +++ b/openpype/cli.py @@ -282,6 +282,9 @@ def run(script): "--app_variant", help="Provide specific app variant for test, empty for latest", default=None) +@click.option("--app_group", + help="Provide specific app group for test, empty for default", + default=None) @click.option("-t", "--timeout", help="Provide specific timeout value for test case", @@ -294,11 +297,11 @@ def run(script): help="MongoDB for testing.", default=None) def runtests(folder, mark, pyargs, test_data_folder, persist, app_variant, - timeout, setup_only, mongo_url): + timeout, setup_only, mongo_url, app_group): """Run all automatic tests after proper initialization via start.py""" PypeCommands().run_tests(folder, mark, pyargs, test_data_folder, persist, app_variant, timeout, setup_only, - mongo_url) + mongo_url, app_group) @main.command(help="DEPRECATED - run sync server") diff --git a/openpype/pype_commands.py b/openpype/pype_commands.py index 071ecfffd2..b5828d3dfe 100644 --- a/openpype/pype_commands.py +++ b/openpype/pype_commands.py @@ -214,7 +214,7 @@ class PypeCommands: def run_tests(self, folder, mark, pyargs, test_data_folder, persist, app_variant, timeout, setup_only, - mongo_url): + mongo_url, app_group): """ Runs tests from 'folder' @@ -260,6 +260,9 @@ class PypeCommands: if persist: args.extend(["--persist", persist]) + if app_group: + args.extend(["--app_group", app_group]) + if app_variant: args.extend(["--app_variant", app_variant]) diff --git a/tests/conftest.py b/tests/conftest.py index 6e82c9917d..a862030fff 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,6 +14,11 @@ def pytest_addoption(parser): help="True - keep test_db, test_openpype, outputted test files" ) + parser.addoption( + "--app_group", action="store", default=None, + help="Keep empty to use default application or explicit" + ) + parser.addoption( "--app_variant", action="store", default=None, help="Keep empty to locate latest installed variant or explicit" @@ -45,6 +50,11 @@ def persist(request): return request.config.getoption("--persist") +@pytest.fixture(scope="module") +def app_group(request): + return request.config.getoption("--app_group") + + @pytest.fixture(scope="module") def app_variant(request): return request.config.getoption("--app_variant") diff --git a/tests/lib/testing_classes.py b/tests/lib/testing_classes.py index 277b332e19..e8e338e434 100644 --- a/tests/lib/testing_classes.py +++ b/tests/lib/testing_classes.py @@ -248,19 +248,22 @@ class PublishTest(ModuleUnitTest): SETUP_ONLY = False @pytest.fixture(scope="module") - def app_name(self, app_variant): + def app_name(self, app_variant, app_group): """Returns calculated value for ApplicationManager. Eg.(nuke/12-2)""" from openpype.lib import ApplicationManager app_variant = app_variant or self.APP_VARIANT + app_group = app_group or self.APP_GROUP application_manager = ApplicationManager() if not app_variant: variant = ( application_manager.find_latest_available_variant_for_group( - self.APP_GROUP)) + app_group + ) + ) app_variant = variant.name - yield "{}/{}".format(self.APP_GROUP, app_variant) + yield "{}/{}".format(app_group, app_variant) @pytest.fixture(scope="module") def app_args(self, download_test_data): From 82c3442f6192d2f4e26268a89483e55f4cebfbaa Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 7 Nov 2023 14:46:05 +0100 Subject: [PATCH 0761/1233] convert 'ValidateAttributes' settings only if are available --- openpype/settings/ayon_settings.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index a31c8a04e0..b56249bbc2 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -641,13 +641,14 @@ def _convert_3dsmax_project_settings(ayon_settings, output): ayon_max["PointCloud"]["attribute"] = new_point_cloud_attribute # --- Publish (START) --- ayon_publish = ayon_max["publish"] - try: - attributes = json.loads( - ayon_publish["ValidateAttributes"]["attributes"] - ) - except ValueError: - attributes = {} - ayon_publish["ValidateAttributes"]["attributes"] = attributes + if "ValidateAttributes" in ayon_publish: + try: + attributes = json.loads( + ayon_publish["ValidateAttributes"]["attributes"] + ) + except ValueError: + attributes = {} + ayon_publish["ValidateAttributes"]["attributes"] = attributes output["max"] = ayon_max From e3c28bd55775ebb0d8b1d412dc7155ba0aa8d152 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Tue, 7 Nov 2023 13:46:43 +0000 Subject: [PATCH 0762/1233] [Automated] Release --- CHANGELOG.md | 457 ++++++++++++++++++++++++++++++++++++++++++++ openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 459 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7432b33e24..b3daf581ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,463 @@ # Changelog +## [3.17.5](https://github.com/ynput/OpenPype/tree/3.17.5) + + +[Full Changelog](https://github.com/ynput/OpenPype/compare/3.17.4...3.17.5) + +### **🆕 New features** + + +
+Fusion: Add USD loader #4896 + +Add an OpenPype managed USD loader (`uLoader`) for Fusion. + + +___ + +
+ + +
+Fusion: Resolution validator #5325 + +Added a resolution validator.The code is from my old PR (https://github.com/ynput/OpenPype/pull/4921) that I closed because the PR also contained a frame range validator that no longer is needed. + + +___ + +
+ + +
+Context Selection tool: Refactor Context tool (for AYON) #5766 + +Context selection tool has AYON variant. + + +___ + +
+ + +
+AYON: Use AYON username for user in template data #5842 + +Use ayon username for template data in AYON mode. + + +___ + +
+ + +
+Testing: app_group flag #5869 + +`app_group` command flag. This is for changing which flavour of the host to launch. In the case of Maya, you can launch Maya and MayaPy, but it can be used for the Nuke family as well.Split from #5644 + + +___ + +
+ +### **🚀 Enhancements** + + +
+Enhancement: Fusion fix saver creation + minor Blender/Fusion logging tweaks #5558 + +- Blender change logs to `debug` level in preparation for new publisher artist facing reports (note that it currently still uses the old publisher) +- Fusion: Create Saver fix redeclaration of default_variants +- Fusion: Fix saver being created in incorrect state without saving directly after create +- Fusion: Allow reset frame range on render family +- Fusion: Tweak logging level for artist-facing report + + +___ + +
+ + +
+Resolve: load clip to timeline at set time #5665 + +It is possible to load clip to correct place on timeline. + + +___ + +
+ + +
+Nuke: Optional Deadline workfile dependency. #5732 + +Adds option to add the workfile as dependency for the Deadline job.Think it used to have something like this, but it disappeared. Usecase is for remote workflow where the Nuke script needs to be synced before the job can start. + + +___ + +
+ + +
+Enhancement/houdini rearrange ayon houdini settings files #5748 + +Rearranging Houdini Settings to be more readable, easier to edit, update settings (include all families/product types)This PR is mainly for Ayon Settings to have more organized files. For Openpype, I'll make sure that each Houdini setting in Ayon has an equivalent in Openpype. +- [x] update Ayon settings, fix typos and remove deprecated settings. +- [x] Sync with Openpype +- [x] Test in Openpype +- [x] Test in Ayon + + +___ + +
+ + +
+Chore: updating create ayon addon script #5822 + +Adding developers environment options. + + +___ + +
+ + +
+Max: Implement Validator for Properties/Attributes Value Check #5824 + +Add optional validator which can check if the property attributes are valid in Max + + +___ + +
+ + +
+Nuke: Remove unused 'get_render_path' function #5826 + +Remove unused function `get_render_path` from nuke integration. + + +___ + +
+ + +
+Chore: Limit current context template data function #5845 + +Current implementation of `get_current_context_template_data` does return the same values as base template data function `get_template_data`. + + +___ + +
+ + +
+Max: Make sure Collect Render not ignoring instance asset #5847 + +- Make sure Collect Render is not always using asset from context. +- Make sure Scene version being collected +- Clean up unnecessary uses of code in the collector. + + +___ + +
+ + +
+Ftrack: Events are not processed if project is not available in OpenPype #5853 + +Events that happened on project which is not in OpenPype is not processed. + + +___ + +
+ + +
+Nuke: Add Nuke 11.0 as default setting #5855 + +Found I needed Nuke 11.0 in the default settings to help with unit testing. + + +___ + +
+ + +
+TVPaint: Code cleanup #5857 + +Removed unused import. Use `AYON` label in ayon mode. Removed unused data in publish context `"previous_context"`. + + +___ + +
+ + +
+AYON settings: Use correct label for follow workfile version #5874 + +Follow workfile version label was marked as Collect Anatomy Instance Data label. + + +___ + +
+ +### **🐛 Bug fixes** + + +
+Nuke: Fix workfile template builder so representations get loaded next to each other #5061 + +Refactor when the cleanup of the placeholder happens for the cases where multiple representations are loaded by a single placeholder.The existing code didn't take into account the case where a template placeholder can load multiple representations so it was trying to do the cleanup of the placeholder node and the re-arrangement of the imported nodes too early. I assume this was designed only for the cases where a single representation can load multiple nodes. + + +___ + +
+ + +
+Nuke: Dont update node name on update #5704 + +When updating `Image` containers the code is trying to set the name of the node. This results in a warning message from Nuke shown below;Suggesting to not change the node name when updating. + + +___ + +
+ + +
+UIDefLabel can be unique #5827 + +`UILabelDef` have implemented comparison and uniqueness. + + +___ + +
+ + +
+AYON: Skip kitsu module when creating ayon addons #5828 + +Create AYON packages is skipping kitsu module in creation of modules/addons and kitsu module is not loaded from modules on start. The addon already has it's repository https://github.com/ynput/ayon-kitsu. + + +___ + +
+ + +
+Bugfix: Collect Rendered Files only collecting first instance #5832 + +Collect all instances from the metadata file - don't return on first instance iteration. + + +___ + +
+ + +
+Houdini: set frame range for the created composite ROP #5833 + +Quick bug fix for created composite ROP, set its frame range to the frame range of the playbar. + + +___ + +
+ + +
+Fix registering launcher actions from OpenPypeModules #5843 + +Fix typo `actions_dir` -> `path` to fix register launcher actions fromm OpenPypeModule + + +___ + +
+ + +
+Bugfix in houdini shelves manager and beautify settings #5844 + +This PR fixes the problem in this PR https://github.com/ynput/OpenPype/issues/5457 by using the right function to load a pre-made houdini `.shelf` fileAlso, it beautifies houdini shelves settings to provide better guidance for users which helps with other issue https://github.com/ynput/OpenPype/issues/5458 , Rather adding default shelf and set names, I'll educate users how to use the tool correctly.Users now are able to select between the two options.| OpenPype | Ayon || -- | -- || | | + + +___ + +
+ + +
+Blender: Fix missing Grease Pencils in review #5848 + +Fix Grease Pencil missing in review when isolating objects. + + +___ + +
+ + +
+Blender: Fix Render Settings in Ayon #5849 + +Fix Render Settings in Ayon for Blender. + + +___ + +
+ + +
+Bugfix: houdini tab menu working as expected #5850 + +This PR:Tab menu name changes to Ayon when using ayon get_network_categories is checked in all creator plugins. | Product | Network Category | | -- | -- | | Alembic camera | rop, obj | | Arnold Ass | rop | | Arnold ROP | rop | | Bgeo | rop, sop | | composite sequence | cop2, rop | | hda | obj | | Karma ROP | rop | | Mantra ROP | rop | | ABC | rop, sop | | RS proxy | rop, sop| | RS ROP | rop | | Review | rop | | Static mesh | rop, obj, sop | | USD | lop, rop | | USD Render | rop | | VDB | rop, obj, sop | | V Ray | rop | + + +___ + +
+ + +
+Bigfix: Houdini skip frame_range_validator if node has no 'trange' parameter #5851 + +I faced a bug when publishing HDA instance as it has no `trange` parameter. As this PR title says : skip frame_range_validator if node has no 'trange' parameter + + +___ + +
+ + +
+Bugfix: houdini image sequence loading and missing frames #5852 + +I made this PR in to fix issues mentioned here https://github.com/ynput/OpenPype/pull/5833#issuecomment-1789207727in short: +- image load doesn't work +- publisher only publish one frame + + +___ + +
+ + +
+Nuke: loaders' containers updating as nodes #5854 + +Nuke loaded containers are updating correctly even they have been duplicating of originally loaded nodes. This had previously been removed duplicated nodes. + + +___ + +
+ + +
+deadline: settings are not blocking extension input #5864 + +Settings are not blocking user input. + + +___ + +
+ + +
+Blender: Fix loading of blend layouts #5866 + +Fix a problem with loading blend layouts. + + +___ + +
+ + +
+AYON: Launcher refresh issues #5867 + +Fixed refresh of projects issue in launcher tool. And renamed Qt models to contain `Qt` in their name (it was really hard to find out where were used). It is not possible to click on disabled item in launcher's projects view. + + +___ + +
+ + +
+Fix the Wrong key words for tycache workfile template settings in AYON #5870 + +Fix the wrong key words for the tycache workfile template settings in AYON(i.e. Instead of families, product_types should be used) + + +___ + +
+ + +
+AYON tools: Handle empty icon definition #5876 + +Ignore if passed icon definition is `None`. + + +___ + +
+ +### **🔀 Refactored code** + + +
+Houdini: Remove on instance toggled callback #5860 + +Remove on instance toggled callback which isn't relevant to the new publisher + + +___ + +
+ + +
+Chore: Remove unused `instanceToggled` callbacks #5862 + +The `instanceToggled` callbacks should be irrelevant for new publisher. + + +___ + +
+ + + + ## [3.17.4](https://github.com/ynput/OpenPype/tree/3.17.4) diff --git a/openpype/version.py b/openpype/version.py index c6ebd65e9c..9832c77291 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.17.5-nightly.3" +__version__ = "3.17.5" diff --git a/pyproject.toml b/pyproject.toml index 633dafece1..c6f4880cdd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.17.4" # OpenPype +version = "3.17.5" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 246a4af3ef2185b2ad924bd89c8b4f2bd4404678 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 7 Nov 2023 13:47:43 +0000 Subject: [PATCH 0763/1233] 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 7a1fe9d83e..bdfc2ad46f 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.17.5 - 3.17.5-nightly.3 - 3.17.5-nightly.2 - 3.17.5-nightly.1 @@ -134,7 +135,6 @@ body: - 3.15.1-nightly.6 - 3.15.1-nightly.5 - 3.15.1-nightly.4 - - 3.15.1-nightly.3 validations: required: true - type: dropdown From 7aa5b09ef26dd2da96cccb0eb28d79a6e3d20844 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 7 Nov 2023 14:56:46 +0100 Subject: [PATCH 0764/1233] improving code --- openpype/hosts/traypublisher/api/editorial.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/traypublisher/api/editorial.py b/openpype/hosts/traypublisher/api/editorial.py index 1a83fcecbd..613f1de768 100644 --- a/openpype/hosts/traypublisher/api/editorial.py +++ b/openpype/hosts/traypublisher/api/editorial.py @@ -53,11 +53,11 @@ class ShotMetadataSolver: try: # format to new shot name return shot_rename_template.format(**data) - except KeyError as _E: + except KeyError as _error: raise CreatorError(( "Make sure all keys in settings are correct:: \n\n" f"From template string {shot_rename_template} > " - f"`{_E}` has no equivalent in \n" + f"`{_error}` has no equivalent in \n" f"{list(data.keys())} input formatting keys!" )) @@ -100,7 +100,7 @@ class ShotMetadataSolver: "at your project settings..." )) - # QUESTION:how to refactory `match[-1]` to some better way? + # QUESTION:how to refactor `match[-1]` to some better way? output_data[token_key] = match[-1] return output_data @@ -130,10 +130,10 @@ class ShotMetadataSolver: parent_token["name"]: parent_token["value"].format(**data) for parent_token in hierarchy_parents } - except KeyError as _E: + except KeyError as _error: raise CreatorError(( "Make sure all keys in settings are correct : \n" - f"`{_E}` has no equivalent in \n{list(data.keys())}" + f"`{_error}` has no equivalent in \n{list(data.keys())}" )) _parent_tokens_type = { @@ -147,10 +147,10 @@ class ShotMetadataSolver: try: parent_name = _parent.format( **_parent_tokens_formatting_data) - except KeyError as _E: + except KeyError as _error: raise CreatorError(( "Make sure all keys in settings are correct : \n\n" - f"`{_E}` from template string " + f"`{_error}` from template string " f"{shot_hierarchy['parents_path']}, " f" has no equivalent in \n" f"{list(_parent_tokens_formatting_data.keys())} parents" From 638c1c65e78cdf4fc9327820a265878f0a5e13a6 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 7 Nov 2023 14:57:44 +0100 Subject: [PATCH 0765/1233] ayon distributed folder path from create to publish --- .../plugins/create/create_editorial.py | 69 +++++++++++-------- .../plugins/publish/collect_shot_instances.py | 8 ++- 2 files changed, 49 insertions(+), 28 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py index 59f24a2a2b..8c5083bcb2 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_editorial.py +++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py @@ -102,14 +102,23 @@ class EditorialShotInstanceCreator(EditorialClipInstanceCreatorBase): label = "Editorial Shot" def get_instance_attr_defs(self): - attr_defs = [ - TextDef( - "asset_name", - label="Asset name", + instance_attributes = [] + if AYON_SERVER_ENABLED: + instance_attributes.append( + TextDef( + "folderPath", + label="Folder path" + ) ) - ] - attr_defs.extend(CLIP_ATTR_DEFS) - return attr_defs + else: + instance_attributes.append( + TextDef( + "shotName", + label="Shot name" + ) + ) + instance_attributes.extend(CLIP_ATTR_DEFS) + return instance_attributes class EditorialPlateInstanceCreator(EditorialClipInstanceCreatorBase): @@ -216,10 +225,7 @@ or updating already created. Publishing will create OTIO file. ] } # Create otio editorial instance - if AYON_SERVER_ENABLED: - asset_name = instance_data.pop("folderPath") - else: - asset_name = instance_data["asset"] + asset_name = instance_data["asset"] asset_doc = get_asset_by_name(self.project_name, asset_name) @@ -671,7 +677,10 @@ or updating already created. Publishing will create OTIO file. } ) - self._validate_name_uniqueness(shot_name) + # It should be validated only in openpype since we are supporting + # publishing to AYON with folder path and uniqueness is not an issue + if not AYON_SERVER_ENABLED: + self._validate_name_uniqueness(shot_name) timing_data = self._get_timing_data( otio_clip, @@ -682,36 +691,42 @@ or updating already created. Publishing will create OTIO file. # create creator attributes creator_attributes = { - "asset_name": shot_name, - "Parent hierarchy path": shot_metadata["hierarchy"], + "workfile_start_frame": workfile_start_frame, "fps": fps, "handle_start": int(handle_start), "handle_end": int(handle_end) } - if AYON_SERVER_ENABLED: - creator_attributes["folderPath"] = shot_metadata["folderPath"] - + # add timing data creator_attributes.update(timing_data) - # create shared new instance data + # create base instance data base_instance_data = { "shotName": shot_name, "variant": variant_name, - - # HACK: just for temporal bug workaround - # TODO: should loockup shot name for update - "asset": parent_asset_name, "task": "", - "newAssetPublishing": True, - - # parent time properties "trackStartFrame": track_start_frame, "timelineOffset": timeline_offset, - # creator_attributes - "creator_attributes": creator_attributes } + # update base instance data with context data + # and also update creator attributes with context data + if AYON_SERVER_ENABLED: + # TODO: this is here just to be able to publish + # to AYON with folder path + creator_attributes["folderPath"] = shot_metadata.pop("folderPath") + base_instance_data["folderPath"] = parent_asset_name + else: + creator_attributes.update({ + "shotName": shot_name, + "Parent hierarchy path": shot_metadata["hierarchy"] + }) + + base_instance_data["asset"] = parent_asset_name + + + # add creator attributes to shared instance data + base_instance_data["creator_attributes"] = creator_attributes # add hierarchy shot metadata base_instance_data.update(shot_metadata) diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py b/openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py index b08397caf7..65dd35782f 100644 --- a/openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py +++ b/openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py @@ -2,6 +2,8 @@ from pprint import pformat import pyblish.api import opentimelineio as otio +from openpype import AYON_SERVER_ENABLED + class CollectShotInstance(pyblish.api.InstancePlugin): """ Collect shot instances @@ -119,7 +121,7 @@ class CollectShotInstance(pyblish.api.InstancePlugin): frame_end = _cr_attrs["frameEnd"] frame_dur = frame_end - frame_start - return { + data = { "fps": float(_cr_attrs["fps"]), "handleStart": _cr_attrs["handle_start"], "handleEnd": _cr_attrs["handle_end"], @@ -132,6 +134,10 @@ class CollectShotInstance(pyblish.api.InstancePlugin): "sourceOut": _cr_attrs["sourceOut"], "workfileFrameStart": workfile_start_frame } + if AYON_SERVER_ENABLED: + data["asset"] = _cr_attrs["asset"] + + return data def _solve_hierarchy_context(self, instance): """ Adding hierarchy data to context shared data. From cf356e7ecd66521f91996024bac4e911a4ce09ac Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Tue, 7 Nov 2023 16:00:50 +0100 Subject: [PATCH 0766/1233] add create render identifier --- openpype/hosts/blender/plugins/create/create_render.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/blender/plugins/create/create_render.py b/openpype/hosts/blender/plugins/create/create_render.py index 45570f3491..e036ae7df3 100644 --- a/openpype/hosts/blender/plugins/create/create_render.py +++ b/openpype/hosts/blender/plugins/create/create_render.py @@ -13,6 +13,7 @@ from openpype.hosts.blender.api.pipeline import ( class CreateRenderlayer(plugin.BaseCreator): """Single baked camera""" + identifier = "io.openpype.creators.blender.render" name = "renderingMain" label = "Render" family = "render" @@ -28,8 +29,7 @@ class CreateRenderlayer(plugin.BaseCreator): bpy.context.scene.collection.children.link(instances) # Create instance object - asset = instance_data.get("asset") - name = plugin.asset_name(asset, subset_name) + name = plugin.asset_name(instance_data.get("asset"), subset_name) asset_group = bpy.data.collections.new(name=name) try: From 44578b2121f1eee9fa6daca3e72dac047f41ebf2 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 7 Nov 2023 16:15:16 +0100 Subject: [PATCH 0767/1233] traypublisher: editorial collector asset to instance with ayon exception for folderPath --- .../traypublisher/plugins/publish/collect_shot_instances.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py b/openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py index 65dd35782f..0b7e022658 100644 --- a/openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py +++ b/openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py @@ -135,7 +135,9 @@ class CollectShotInstance(pyblish.api.InstancePlugin): "workfileFrameStart": workfile_start_frame } if AYON_SERVER_ENABLED: - data["asset"] = _cr_attrs["asset"] + data["asset"] = _cr_attrs["folderPath"] + else: + data["asset"] = _cr_attrs["shotName"] return data From f3370c0229da5ff9c323a7277f8711122b25a4b7 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 7 Nov 2023 15:21:10 +0000 Subject: [PATCH 0768/1233] Changed how extractors name the output files --- openpype/hosts/blender/plugins/publish/extract_abc.py | 9 ++++++--- .../blender/plugins/publish/extract_abc_animation.py | 10 +++++++--- .../hosts/blender/plugins/publish/extract_blend.py | 9 ++++++--- .../plugins/publish/extract_blend_animation.py | 9 ++++++--- .../blender/plugins/publish/extract_camera_abc.py | 9 ++++++--- .../blender/plugins/publish/extract_camera_fbx.py | 9 ++++++--- openpype/hosts/blender/plugins/publish/extract_fbx.py | 9 ++++++--- .../blender/plugins/publish/extract_fbx_animation.py | 11 +++++++---- .../hosts/blender/plugins/publish/extract_layout.py | 10 +++++++--- .../blender/plugins/publish/extract_playblast.py | 5 ++++- .../blender/plugins/publish/extract_thumbnail.py | 5 ++++- 11 files changed, 65 insertions(+), 30 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/extract_abc.py b/openpype/hosts/blender/plugins/publish/extract_abc.py index b17d7cc6e4..59035d8f61 100644 --- a/openpype/hosts/blender/plugins/publish/extract_abc.py +++ b/openpype/hosts/blender/plugins/publish/extract_abc.py @@ -17,7 +17,10 @@ class ExtractABC(publish.Extractor): def process(self, instance): # Define extract output file path stagingdir = self.staging_dir(instance) - filename = f"{instance.name}.abc" + asset_name = instance.data["assetEntity"]["name"] + subset = instance.data["subset"] + instance_name = f"{asset_name}_{subset}" + filename = f"{instance_name}.abc" filepath = os.path.join(stagingdir, filename) # Perform extraction @@ -59,8 +62,8 @@ class ExtractABC(publish.Extractor): } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s", - instance.name, representation) + self.log.info( + f"Extracted instance '{instance_name}' to: {representation}") class ExtractModelABC(ExtractABC): diff --git a/openpype/hosts/blender/plugins/publish/extract_abc_animation.py b/openpype/hosts/blender/plugins/publish/extract_abc_animation.py index 6866b05fea..0ac6f12de5 100644 --- a/openpype/hosts/blender/plugins/publish/extract_abc_animation.py +++ b/openpype/hosts/blender/plugins/publish/extract_abc_animation.py @@ -17,7 +17,11 @@ class ExtractAnimationABC(publish.Extractor): def process(self, instance): # Define extract output file path stagingdir = self.staging_dir(instance) - filename = f"{instance.name}.abc" + asset_name = instance.data["assetEntity"]["name"] + subset = instance.data["subset"] + instance_name = f"{asset_name}_{subset}" + filename = f"{instance_name}.abc" + filepath = os.path.join(stagingdir, filename) # Perform extraction @@ -66,5 +70,5 @@ class ExtractAnimationABC(publish.Extractor): } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s", - instance.name, representation) + self.log.info( + f"Extracted instance '{instance_name}' to: {representation}") diff --git a/openpype/hosts/blender/plugins/publish/extract_blend.py b/openpype/hosts/blender/plugins/publish/extract_blend.py index c8eeef7fd7..0a9fb74f7b 100644 --- a/openpype/hosts/blender/plugins/publish/extract_blend.py +++ b/openpype/hosts/blender/plugins/publish/extract_blend.py @@ -17,7 +17,10 @@ class ExtractBlend(publish.Extractor): # Define extract output file path stagingdir = self.staging_dir(instance) - filename = f"{instance.name}.blend" + asset_name = instance.data["assetEntity"]["name"] + subset = instance.data["subset"] + instance_name = f"{asset_name}_{subset}" + filename = f"{instance_name}.blend" filepath = os.path.join(stagingdir, filename) # Perform extraction @@ -52,5 +55,5 @@ class ExtractBlend(publish.Extractor): } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s", - instance.name, representation) + self.log.info( + f"Extracted instance '{instance_name}' to: {representation}") diff --git a/openpype/hosts/blender/plugins/publish/extract_blend_animation.py b/openpype/hosts/blender/plugins/publish/extract_blend_animation.py index 661cecce81..3d36ee7ec3 100644 --- a/openpype/hosts/blender/plugins/publish/extract_blend_animation.py +++ b/openpype/hosts/blender/plugins/publish/extract_blend_animation.py @@ -17,7 +17,10 @@ class ExtractBlendAnimation(publish.Extractor): # Define extract output file path stagingdir = self.staging_dir(instance) - filename = f"{instance.name}.blend" + asset_name = instance.data["assetEntity"]["name"] + subset = instance.data["subset"] + instance_name = f"{asset_name}_{subset}" + filename = f"{instance_name}.blend" filepath = os.path.join(stagingdir, filename) # Perform extraction @@ -50,5 +53,5 @@ class ExtractBlendAnimation(publish.Extractor): } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s", - instance.name, representation) + self.log.info( + f"Extracted instance '{instance_name}' to: {representation}") diff --git a/openpype/hosts/blender/plugins/publish/extract_camera_abc.py b/openpype/hosts/blender/plugins/publish/extract_camera_abc.py index 5916564ac0..b6b38b41ff 100644 --- a/openpype/hosts/blender/plugins/publish/extract_camera_abc.py +++ b/openpype/hosts/blender/plugins/publish/extract_camera_abc.py @@ -18,7 +18,10 @@ class ExtractCameraABC(publish.Extractor): def process(self, instance): # Define extract output file path stagingdir = self.staging_dir(instance) - filename = f"{instance.name}.abc" + asset_name = instance.data["assetEntity"]["name"] + subset = instance.data["subset"] + instance_name = f"{asset_name}_{subset}" + filename = f"{instance_name}.abc" filepath = os.path.join(stagingdir, filename) # Perform extraction @@ -64,5 +67,5 @@ class ExtractCameraABC(publish.Extractor): } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s", - instance.name, representation) + self.log.info( + f"Extracted instance '{instance_name}' to: {representation}") diff --git a/openpype/hosts/blender/plugins/publish/extract_camera_fbx.py b/openpype/hosts/blender/plugins/publish/extract_camera_fbx.py index a541f5b375..be9f178d1b 100644 --- a/openpype/hosts/blender/plugins/publish/extract_camera_fbx.py +++ b/openpype/hosts/blender/plugins/publish/extract_camera_fbx.py @@ -17,7 +17,10 @@ class ExtractCamera(publish.Extractor): def process(self, instance): # Define extract output file path stagingdir = self.staging_dir(instance) - filename = f"{instance.name}.fbx" + asset_name = instance.data["assetEntity"]["name"] + subset = instance.data["subset"] + instance_name = f"{asset_name}_{subset}" + filename = f"{instance_name}.fbx" filepath = os.path.join(stagingdir, filename) # Perform extraction @@ -73,5 +76,5 @@ class ExtractCamera(publish.Extractor): } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s", - instance.name, representation) + self.log.info( + f"Extracted instance '{instance_name}' to: {representation}") diff --git a/openpype/hosts/blender/plugins/publish/extract_fbx.py b/openpype/hosts/blender/plugins/publish/extract_fbx.py index f2ce117dcd..c21dc35ff6 100644 --- a/openpype/hosts/blender/plugins/publish/extract_fbx.py +++ b/openpype/hosts/blender/plugins/publish/extract_fbx.py @@ -18,7 +18,10 @@ class ExtractFBX(publish.Extractor): def process(self, instance): # Define extract output file path stagingdir = self.staging_dir(instance) - filename = f"{instance.name}.fbx" + asset_name = instance.data["assetEntity"]["name"] + subset = instance.data["subset"] + instance_name = f"{asset_name}_{subset}" + filename = f"{instance_name}.fbx" filepath = os.path.join(stagingdir, filename) # Perform extraction @@ -84,5 +87,5 @@ class ExtractFBX(publish.Extractor): } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s", - instance.name, representation) + self.log.info( + f"Extracted instance '{instance_name}' to: {representation}") diff --git a/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py b/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py index 5fe5931e65..ed4e7ecc6a 100644 --- a/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py +++ b/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py @@ -86,7 +86,10 @@ class ExtractAnimationFBX(publish.Extractor): asset_group.select_set(True) armature.select_set(True) - fbx_filename = f"{instance.name}_{armature.name}.fbx" + asset_name = instance.data["assetEntity"]["name"] + subset = instance.data["subset"] + instance_name = f"{asset_name}_{subset}" + fbx_filename = f"{instance_name}_{armature.name}.fbx" filepath = os.path.join(stagingdir, fbx_filename) override = plugin.create_blender_context( @@ -119,7 +122,7 @@ class ExtractAnimationFBX(publish.Extractor): pair[1].user_clear() bpy.data.actions.remove(pair[1]) - json_filename = f"{instance.name}.json" + json_filename = f"{instance_name}.json" json_path = os.path.join(stagingdir, json_filename) json_dict = { @@ -158,5 +161,5 @@ class ExtractAnimationFBX(publish.Extractor): instance.data["representations"].append(fbx_representation) instance.data["representations"].append(json_representation) - self.log.info("Extracted instance '{}' to: {}".format( - instance.name, fbx_representation)) + self.log.info( + f"Extracted instance '{instance_name}' to: {fbx_representation}") diff --git a/openpype/hosts/blender/plugins/publish/extract_layout.py b/openpype/hosts/blender/plugins/publish/extract_layout.py index 05f86b8370..8e820ee84e 100644 --- a/openpype/hosts/blender/plugins/publish/extract_layout.py +++ b/openpype/hosts/blender/plugins/publish/extract_layout.py @@ -212,7 +212,11 @@ class ExtractLayout(publish.Extractor): json_data.append(json_element) - json_filename = "{}.json".format(instance.name) + asset_name = instance.data["assetEntity"]["name"] + subset = instance.data["subset"] + instance_name = f"{asset_name}_{subset}" + json_filename = f"{instance_name}.json" + json_path = os.path.join(stagingdir, json_filename) with open(json_path, "w+") as file: @@ -245,5 +249,5 @@ class ExtractLayout(publish.Extractor): } instance.data["representations"].append(fbx_representation) - self.log.info("Extracted instance '%s' to: %s", - instance.name, json_representation) + self.log.info( + f"Extracted instance '{instance_name}' to: {json_representation}") diff --git a/openpype/hosts/blender/plugins/publish/extract_playblast.py b/openpype/hosts/blender/plugins/publish/extract_playblast.py index b0099cce85..805aacc5f4 100644 --- a/openpype/hosts/blender/plugins/publish/extract_playblast.py +++ b/openpype/hosts/blender/plugins/publish/extract_playblast.py @@ -50,7 +50,10 @@ class ExtractPlayblast(publish.Extractor): # get output path stagingdir = self.staging_dir(instance) - filename = instance.name + asset_name = instance.data["assetEntity"]["name"] + subset = instance.data["subset"] + filename = f"{asset_name}_{subset}" + path = os.path.join(stagingdir, filename) self.log.debug(f"Outputting images to {path}") diff --git a/openpype/hosts/blender/plugins/publish/extract_thumbnail.py b/openpype/hosts/blender/plugins/publish/extract_thumbnail.py index 52e5d98fc4..e8a9c68dd1 100644 --- a/openpype/hosts/blender/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/blender/plugins/publish/extract_thumbnail.py @@ -27,7 +27,10 @@ class ExtractThumbnail(publish.Extractor): self.log.debug("Extracting capture..") stagingdir = self.staging_dir(instance) - filename = instance.name + asset_name = instance.data["assetEntity"]["name"] + subset = instance.data["subset"] + filename = f"{asset_name}_{subset}" + path = os.path.join(stagingdir, filename) self.log.debug(f"Outputting images to {path}") From f7d76617c0ea5635f9ae8ad0e6a18454da24c2be Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Tue, 7 Nov 2023 15:51:54 +0000 Subject: [PATCH 0769/1233] Testing: Validate Maya Logs (#5775) * Working version * Improve launched app communication * Move imports to methods. * Update tests/integration/hosts/maya/test_publish_in_maya.py Co-authored-by: Roy Nieterau * Collect errors from process * fix startup scripts arguments * Update openpype/lib/applications.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> * Fix application polling * Docstring * Revert stdout and stderr * Revert subprocess.PIPE * Added missed imports If we are moving these because of testing, lets move all of them --------- Co-authored-by: Roy Nieterau Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Co-authored-by: kalisp --- openpype/hosts/maya/api/lib_rendersettings.py | 6 ++-- .../collect_deadline_server_from_instance.py | 5 ++-- .../publish/submit_blender_deadline.py | 5 ++-- .../publish/submit_houdini_remote_publish.py | 3 +- .../publish/submit_houdini_render_deadline.py | 5 ++-- .../plugins/publish/submit_max_deadline.py | 14 +++++---- .../plugins/publish/submit_maya_deadline.py | 8 ++--- .../submit_maya_remote_publish_deadline.py | 5 ++-- .../plugins/publish/submit_nuke_deadline.py | 5 ++-- .../publish/collect_otio_frame_ranges.py | 18 +++++++----- .../plugins/publish/collect_otio_review.py | 7 +++-- .../publish/collect_otio_subset_resources.py | 17 ++++++----- .../publish/extract_otio_audio_tracks.py | 8 +++-- openpype/plugins/publish/extract_otio_file.py | 5 +++- .../plugins/publish/extract_otio_review.py | 24 ++++++++++----- .../publish/extract_otio_trimming_video.py | 3 +- tests/integration/hosts/maya/lib.py | 7 ++++- .../hosts/maya/test_publish_in_maya.py | 29 +++++++++++++++++++ tests/lib/testing_classes.py | 3 +- 19 files changed, 121 insertions(+), 56 deletions(-) diff --git a/openpype/hosts/maya/api/lib_rendersettings.py b/openpype/hosts/maya/api/lib_rendersettings.py index 42cf29d0a7..20264c2cdf 100644 --- a/openpype/hosts/maya/api/lib_rendersettings.py +++ b/openpype/hosts/maya/api/lib_rendersettings.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- """Class for handling Render Settings.""" -from maya import cmds # noqa -import maya.mel as mel import six import sys @@ -63,6 +61,10 @@ class RenderSettings(object): def set_default_renderer_settings(self, renderer=None): """Set basic settings based on renderer.""" + # Not all hosts can import this module. + from maya import cmds + import maya.mel as mel + if not renderer: renderer = cmds.getAttr( 'defaultRenderGlobals.currentRenderer').lower() 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 9b4f89c129..1d3dad769f 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 @@ -5,8 +5,6 @@ This is resolving index of server lists stored in `deadlineServers` instance attribute or using default server if that attribute doesn't exists. """ -from maya import cmds - import pyblish.api from openpype.pipeline.publish import KnownPublishError @@ -44,7 +42,8 @@ class CollectDeadlineServerFromInstance(pyblish.api.InstancePlugin): str: Selected Deadline Webservice URL. """ - + # Not all hosts can import this module. + from maya import cmds deadline_settings = ( render_instance.context.data ["system_settings"] diff --git a/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py b/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py index 4a7497b075..094f2b1821 100644 --- a/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py @@ -6,8 +6,6 @@ import getpass import attr from datetime import datetime -import bpy - from openpype.lib import is_running_from_build from openpype.pipeline import legacy_io from openpype.pipeline.farm.tools import iter_expected_files @@ -142,6 +140,9 @@ class BlenderSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): return job_info def get_plugin_info(self): + # Not all hosts can import this module. + import bpy + plugin_info = BlenderPluginInfo( SceneFile=self.scene_path, Version=bpy.app.version_string, 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 39c0c3afe4..0bee42c4cb 100644 --- a/openpype/modules/deadline/plugins/publish/submit_houdini_remote_publish.py +++ b/openpype/modules/deadline/plugins/publish/submit_houdini_remote_publish.py @@ -3,7 +3,6 @@ import json from datetime import datetime import requests -import hou import pyblish.api @@ -31,6 +30,8 @@ class HoudiniSubmitPublishDeadline(pyblish.api.ContextPlugin): targets = ["deadline"] def process(self, context): + # Not all hosts can import this module. + import hou # Ensure no errors so far assert all( 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 6f885c578a..abc650204b 100644 --- a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py @@ -1,9 +1,8 @@ -import hou - import os import attr import getpass from datetime import datetime + import pyblish.api from openpype.pipeline import legacy_io @@ -119,6 +118,8 @@ class HoudiniSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): return job_info def get_plugin_info(self): + # Not all hosts can import this module. + import hou instance = self._instance context = instance.context diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index 073da3019a..23d4183132 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -1,8 +1,8 @@ import os import getpass import copy - import attr + from openpype.lib import ( TextDef, BoolDef, @@ -15,11 +15,6 @@ from openpype.pipeline import ( from openpype.pipeline.publish.lib import ( replace_with_published_scene_path ) -from openpype.hosts.max.api.lib import ( - get_current_renderer, - get_multipass_setting -) -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 @@ -191,6 +186,13 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, self.submit(self.assemble_payload(job_info, plugin_info)) def _use_published_name(self, data, project_settings): + # Not all hosts can import these modules. + from openpype.hosts.max.api.lib import ( + get_current_renderer, + get_multipass_setting + ) + from openpype.hosts.max.api.lib_rendersettings import RenderSettings + instance = self._instance job_info = copy.deepcopy(self.job_info) plugin_info = copy.deepcopy(self.plugin_info) diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py index 7775191b12..7d532923ff 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py @@ -28,8 +28,6 @@ from collections import OrderedDict import attr -from maya import cmds - from openpype.pipeline import ( legacy_io, OpenPypePyblishPluginMixin @@ -246,6 +244,8 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, return job_info def get_plugin_info(self): + # Not all hosts can import this module. + from maya import cmds instance = self._instance context = instance.context @@ -288,7 +288,7 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, return plugin_payload def process_submission(self): - + from maya import cmds instance = self._instance filepath = self.scene_path # publish if `use_publish` else workfile @@ -675,7 +675,7 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, str """ - + from maya import cmds # "vrayscene//_/" vray_settings = cmds.ls(type="VRaySettingsNode") node = vray_settings[0] diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py index 0d23f44333..41a2a64ab5 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py @@ -2,8 +2,6 @@ import os import attr from datetime import datetime -from maya import cmds - from openpype import AYON_SERVER_ENABLED from openpype.pipeline import legacy_io, PublishXmlValidationError from openpype.tests.lib import is_in_tests @@ -127,7 +125,8 @@ class MayaSubmitRemotePublishDeadline( job_info.EnvironmentKeyValue[key] = value def get_plugin_info(self): - + # Not all hosts can import this module. + from maya import cmds scene = self._instance.context.data["currentFile"] plugin_info = MayaPluginInfo() diff --git a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py index 0e57c54959..fb3ab2710d 100644 --- a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py @@ -7,8 +7,6 @@ from datetime import datetime import requests import pyblish.api -import nuke - from openpype import AYON_SERVER_ENABLED from openpype.pipeline import legacy_io from openpype.pipeline.publish import ( @@ -498,6 +496,9 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin, Returning: list: captured groups list """ + # Not all hosts can import this module. + import nuke + captured_groups = [] for lg_name, list_node_class in self.limit_groups.items(): for node_class in list_node_class: diff --git a/openpype/plugins/publish/collect_otio_frame_ranges.py b/openpype/plugins/publish/collect_otio_frame_ranges.py index 9a68b6e43d..4b130b0e03 100644 --- a/openpype/plugins/publish/collect_otio_frame_ranges.py +++ b/openpype/plugins/publish/collect_otio_frame_ranges.py @@ -5,15 +5,9 @@ Requires: masterLayer -> instance data attribute otioClipRange -> instance data attribute """ -# import os -import opentimelineio as otio -import pyblish.api from pprint import pformat -from openpype.pipeline.editorial import ( - get_media_range_with_retimes, - otio_range_to_frame_range, - otio_range_with_handles -) + +import pyblish.api class CollectOtioFrameRanges(pyblish.api.InstancePlugin): @@ -27,6 +21,14 @@ class CollectOtioFrameRanges(pyblish.api.InstancePlugin): hosts = ["resolve", "hiero", "flame", "traypublisher"] def process(self, instance): + # Not all hosts can import these modules. + import opentimelineio as otio + from openpype.pipeline.editorial import ( + get_media_range_with_retimes, + otio_range_to_frame_range, + otio_range_with_handles + ) + # get basic variables otio_clip = instance.data["otioClip"] workfile_start = instance.data["workfileFrameStart"] diff --git a/openpype/plugins/publish/collect_otio_review.py b/openpype/plugins/publish/collect_otio_review.py index f0157282a1..0e4d596213 100644 --- a/openpype/plugins/publish/collect_otio_review.py +++ b/openpype/plugins/publish/collect_otio_review.py @@ -11,10 +11,10 @@ Provides: instance -> families (adding ["review", "ftrack"]) """ -import opentimelineio as otio -import pyblish.api from pprint import pformat +import pyblish.api + class CollectOtioReview(pyblish.api.InstancePlugin): """Get matching otio track from defined review layer""" @@ -25,6 +25,9 @@ class CollectOtioReview(pyblish.api.InstancePlugin): hosts = ["resolve", "hiero", "flame"] def process(self, instance): + # Not all hosts can import this module. + import opentimelineio as otio + # get basic variables otio_review_clips = [] otio_timeline = instance.context.data["otioTimeline"] diff --git a/openpype/plugins/publish/collect_otio_subset_resources.py b/openpype/plugins/publish/collect_otio_subset_resources.py index f659791d95..739f5bb726 100644 --- a/openpype/plugins/publish/collect_otio_subset_resources.py +++ b/openpype/plugins/publish/collect_otio_subset_resources.py @@ -6,18 +6,15 @@ Provides: instance -> otioReviewClips """ import os + import clique -import opentimelineio as otio import pyblish.api -from openpype.pipeline.editorial import ( - get_media_range_with_retimes, - range_from_frames, - make_sequence_collection -) + from openpype.pipeline.publish import ( get_publish_template_name ) + class CollectOtioSubsetResources(pyblish.api.InstancePlugin): """Get Resources for a subset version""" @@ -26,8 +23,14 @@ class CollectOtioSubsetResources(pyblish.api.InstancePlugin): families = ["clip"] hosts = ["resolve", "hiero", "flame"] - def process(self, instance): + # Not all hosts can import these modules. + import opentimelineio as otio + from openpype.pipeline.editorial import ( + get_media_range_with_retimes, + range_from_frames, + make_sequence_collection + ) if "audio" in instance.data["family"]: return diff --git a/openpype/plugins/publish/extract_otio_audio_tracks.py b/openpype/plugins/publish/extract_otio_audio_tracks.py index 4f17731452..4b73321f02 100644 --- a/openpype/plugins/publish/extract_otio_audio_tracks.py +++ b/openpype/plugins/publish/extract_otio_audio_tracks.py @@ -1,11 +1,12 @@ import os +import tempfile + import pyblish + from openpype.lib import ( get_ffmpeg_tool_args, run_subprocess ) -import tempfile -import opentimelineio as otio class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): @@ -155,6 +156,9 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): Returns: list: list of audio clip dictionaries """ + # Not all hosts can import this module. + import opentimelineio as otio + output = [] # go trough all audio tracks for otio_track in otio_timeline.tracks: diff --git a/openpype/plugins/publish/extract_otio_file.py b/openpype/plugins/publish/extract_otio_file.py index 1a6a82117d..7f1cac33d7 100644 --- a/openpype/plugins/publish/extract_otio_file.py +++ b/openpype/plugins/publish/extract_otio_file.py @@ -1,6 +1,6 @@ import os + import pyblish.api -import opentimelineio as otio from openpype.pipeline import publish @@ -16,6 +16,9 @@ class ExtractOTIOFile(publish.Extractor): hosts = ["resolve", "hiero", "traypublisher"] def process(self, instance): + # Not all hosts can import this module. + import opentimelineio as otio + if not instance.context.data.get("otioTimeline"): return # create representation data diff --git a/openpype/plugins/publish/extract_otio_review.py b/openpype/plugins/publish/extract_otio_review.py index 699207df8a..ad4c807091 100644 --- a/openpype/plugins/publish/extract_otio_review.py +++ b/openpype/plugins/publish/extract_otio_review.py @@ -15,8 +15,8 @@ Provides: """ import os + import clique -import opentimelineio as otio from pyblish import api from openpype.lib import ( @@ -24,13 +24,6 @@ from openpype.lib import ( run_subprocess, ) from openpype.pipeline import publish -from openpype.pipeline.editorial import ( - otio_range_to_frame_range, - trim_media_range, - range_from_frames, - frames_to_seconds, - make_sequence_collection -) class ExtractOTIOReview(publish.Extractor): @@ -62,6 +55,13 @@ class ExtractOTIOReview(publish.Extractor): output_ext = ".jpg" def process(self, instance): + # Not all hosts can import these modules. + import opentimelineio as otio + from openpype.pipeline.editorial import ( + otio_range_to_frame_range, + make_sequence_collection + ) + # TODO: convert resulting image sequence to mp4 # get otio clip and other time info from instance clip @@ -281,6 +281,12 @@ class ExtractOTIOReview(publish.Extractor): Returns: otio.time.TimeRange: trimmed available range """ + # Not all hosts can import these modules. + from openpype.pipeline.editorial import ( + trim_media_range, + range_from_frames + ) + avl_start = int(avl_range.start_time.value) src_start = int(avl_start + start) avl_durtation = int(avl_range.duration.value) @@ -338,6 +344,8 @@ class ExtractOTIOReview(publish.Extractor): Returns: otio.time.TimeRange: trimmed available range """ + # Not all hosts can import this module. + from openpype.pipeline.editorial import frames_to_seconds # create path and frame start to destination output_path, out_frame_start = self._get_ffmpeg_output() diff --git a/openpype/plugins/publish/extract_otio_trimming_video.py b/openpype/plugins/publish/extract_otio_trimming_video.py index 67ff6c538c..2020fcde93 100644 --- a/openpype/plugins/publish/extract_otio_trimming_video.py +++ b/openpype/plugins/publish/extract_otio_trimming_video.py @@ -15,7 +15,6 @@ from openpype.lib import ( run_subprocess, ) from openpype.pipeline import publish -from openpype.pipeline.editorial import frames_to_seconds class ExtractOTIOTrimmingVideo(publish.Extractor): @@ -75,6 +74,8 @@ class ExtractOTIOTrimmingVideo(publish.Extractor): otio_range (opentime.TimeRange): range to trim to """ + # Not all hosts can import this module. + from openpype.pipeline.editorial import frames_to_seconds # create path to destination output_path = self._get_ffmpeg_output(input_file_path) diff --git a/tests/integration/hosts/maya/lib.py b/tests/integration/hosts/maya/lib.py index f27d516605..04ddb765a4 100644 --- a/tests/integration/hosts/maya/lib.py +++ b/tests/integration/hosts/maya/lib.py @@ -33,7 +33,7 @@ class MayaHostFixtures(HostFixtures): yield dest_path @pytest.fixture(scope="module") - def startup_scripts(self, monkeypatch_session): + def startup_scripts(self, monkeypatch_session, download_test_data): """Points Maya to userSetup file from input data""" startup_path = os.path.join( os.path.dirname(__file__), "input", "startup" @@ -44,6 +44,11 @@ class MayaHostFixtures(HostFixtures): "{}{}{}".format(startup_path, os.pathsep, original_pythonpath) ) + monkeypatch_session.setenv( + "MAYA_CMD_FILE_OUTPUT", + os.path.join(download_test_data, "output.log") + ) + @pytest.fixture(scope="module") def skip_compare_folders(self): yield [] diff --git a/tests/integration/hosts/maya/test_publish_in_maya.py b/tests/integration/hosts/maya/test_publish_in_maya.py index b7ee228aae..be8c74e0b8 100644 --- a/tests/integration/hosts/maya/test_publish_in_maya.py +++ b/tests/integration/hosts/maya/test_publish_in_maya.py @@ -1,3 +1,6 @@ +import re +import os + from tests.lib.assert_classes import DBAssert from tests.integration.hosts.maya.lib import MayaLocalPublishTestClass @@ -35,6 +38,32 @@ class TestPublishInMaya(MayaLocalPublishTestClass): TIMEOUT = 120 # publish timeout + def test_publish( + self, + dbcon, + publish_finished, + download_test_data + ): + """Testing Pyblish and Python logs within Maya.""" + + # All maya output via MAYA_CMD_FILE_OUTPUT env var during test run + logging_path = os.path.join(download_test_data, "output.log") + with open(logging_path, "r") as f: + logging_output = f.read() + + print(("-" * 50) + "LOGGING" + ("-" * 50)) + print(logging_output) + + # Check for pyblish errors. + error_regex = r"pyblish \(ERROR\)((.|\n)*?)((pyblish \())" + matches = re.findall(error_regex, logging_output) + assert not matches, matches[0][0] + + # Check for python errors. + error_regex = r"// Error((.|\n)*)" + matches = re.findall(error_regex, logging_output) + assert not matches, matches[0][0] + def test_db_asserts(self, dbcon, publish_finished): """Host and input data dependent expected results in DB.""" print("test_db_asserts") diff --git a/tests/lib/testing_classes.py b/tests/lib/testing_classes.py index e8e338e434..3b0611e2a0 100644 --- a/tests/lib/testing_classes.py +++ b/tests/lib/testing_classes.py @@ -10,6 +10,7 @@ import glob import platform import requests import re +import time from tests.lib.db_handler import DBHandler from tests.lib.file_handler import RemoteFileHandler @@ -334,7 +335,7 @@ class PublishTest(ModuleUnitTest): print("Creating only setup for test, not launching app") yield False return - import time + time_start = time.time() timeout = timeout or self.TIMEOUT timeout = float(timeout) From 63af150dd8f08b280bb6cb1ebe918dd4025e50a5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 7 Nov 2023 17:22:02 +0100 Subject: [PATCH 0770/1233] confirm of instance context changes reset origin of input fields --- openpype/tools/publisher/widgets/widgets.py | 29 +++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index 1bbe73381f..77ebc3f0bb 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -579,6 +579,10 @@ class AssetsField(BaseClickableFrame): """Change to asset names set with last `set_selected_items` call.""" self.set_selected_items(self._origin_value) + def confirm_value(self): + self._origin_value = copy.deepcopy(self._selected_items) + self._has_value_changed = False + class TasksComboboxProxy(QtCore.QSortFilterProxyModel): def __init__(self, *args, **kwargs): @@ -785,6 +789,18 @@ class TasksCombobox(QtWidgets.QComboBox): self._set_is_valid(is_valid) + def confirm_value(self): + new_task_name = self._selected_items[0] + origin_value = copy.deepcopy(self._origin_value) + new_origin_value = [ + (asset_name, new_task_name) + for (asset_name, task_name) in origin_value + ] + + self._origin_value = new_origin_value + self._origin_selection = copy.deepcopy(self._selected_items) + self._has_value_changed = False + def set_selected_items(self, asset_task_combinations=None): """Set items for selected instances. @@ -919,6 +935,10 @@ class VariantInputWidget(PlaceholderLineEdit): """Change text of multiselection.""" self._multiselection_text = text + def confirm_value(self): + self._origin_value = copy.deepcopy(self._current_value) + self._has_value_changed = False + def _set_is_valid(self, valid): if valid == self._is_valid: return @@ -1210,6 +1230,15 @@ class GlobalAttrsWidget(QtWidgets.QWidget): self._set_btns_enabled(False) self._set_btns_visible(invalid_tasks) + if variant_value is not None: + self.variant_input.confirm_value() + + if asset_name is not None: + self.asset_value_widget.confirm_value() + + if task_name is not None: + self.task_value_widget.confirm_value() + self.instance_context_changed.emit() def _on_cancel(self): From e32263916c4856d3d65c4b1c31aeeeaf3047f018 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 7 Nov 2023 17:26:29 +0100 Subject: [PATCH 0771/1233] traypublisher: label of instances with folder path --- .../traypublisher/plugins/create/create_editorial.py | 12 ++++++++---- .../plugins/publish/collect_shot_instances.py | 4 ++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py index 8c5083bcb2..128010cef9 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_editorial.py +++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py @@ -606,19 +606,23 @@ or updating already created. Publishing will create OTIO file. Returns: str: label string """ - shot_name = instance_data["shotName"] + if AYON_SERVER_ENABLED: + asset_name = instance_data["creator_attributes"]["folderPath"] + else: + asset_name = instance_data["creator_attributes"]["shotName"] + variant_name = instance_data["variant"] family = preset["family"] - # get variant name from preset or from inharitance + # get variant name from preset or from inheritance _variant_name = preset.get("variant") or variant_name # subset name subset_name = "{}{}".format( family, _variant_name.capitalize() ) - label = "{}_{}".format( - shot_name, + label = "{} {}".format( + asset_name, subset_name ) diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py b/openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py index 0b7e022658..e00ac64244 100644 --- a/openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py +++ b/openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py @@ -155,7 +155,7 @@ class CollectShotInstance(pyblish.api.InstancePlugin): else {} ) - name = instance.data["asset"] + asset_name = instance.data["asset"] # get handles handle_start = int(instance.data["handleStart"]) @@ -177,7 +177,7 @@ class CollectShotInstance(pyblish.api.InstancePlugin): parents = instance.data.get('parents', []) - actual = {name: in_info} + actual = {asset_name: in_info} for parent in reversed(parents): parent_name = parent["entity_name"] From c6d81edc346bf233c65f8c96e69597afcd1f2c6e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 7 Nov 2023 17:28:37 +0100 Subject: [PATCH 0772/1233] ayon settings: trypublisher editorial add task model conversion --- openpype/settings/ayon_settings.py | 6 +++++- .../server/settings/editorial_creators.py | 16 +++++++--------- server_addon/traypublisher/server/version.py | 2 +- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index 5eb68e3972..efad3ee27b 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -1021,10 +1021,14 @@ def _convert_traypublisher_project_settings(ayon_settings, output): item["family"] = item.pop("product_type") shot_add_tasks = ayon_editorial_simple["shot_add_tasks"] + + # TODO: backward compatibility and remove in future if isinstance(shot_add_tasks, dict): shot_add_tasks = [] + + # aggregate shot_add_tasks items new_shot_add_tasks = { - item["name"]: item["task_type"] + item["name"]: {"type": item["task_type"]} for item in shot_add_tasks } ayon_editorial_simple["shot_add_tasks"] = new_shot_add_tasks diff --git a/server_addon/traypublisher/server/settings/editorial_creators.py b/server_addon/traypublisher/server/settings/editorial_creators.py index 4111f22576..ac0ff0afc7 100644 --- a/server_addon/traypublisher/server/settings/editorial_creators.py +++ b/server_addon/traypublisher/server/settings/editorial_creators.py @@ -5,19 +5,17 @@ from ayon_server.settings import BaseSettingsModel, task_types_enum class ClipNameTokenizerItem(BaseSettingsModel): _layout = "expanded" - # TODO was 'dict-modifiable', is list of dicts now, must be fixed in code - name: str = Field("#TODO", title="Tokenizer name") + name: str = Field("", title="Tokenizer name") regex: str = Field("", title="Tokenizer regex") class ShotAddTasksItem(BaseSettingsModel): _layout = "expanded" - # TODO was 'dict-modifiable', is list of dicts now, must be fixed in code name: str = Field('', title="Key") - task_type: list[str] = Field( + task_type: str = Field( title="Task type", - default_factory=list, - enum_resolver=task_types_enum) + enum_resolver=task_types_enum + ) class ShotRenameSubmodel(BaseSettingsModel): @@ -54,7 +52,7 @@ class TokenToParentConvertorItem(BaseSettingsModel): ) -class ShotHierchySubmodel(BaseSettingsModel): +class ShotHierarchySubmodel(BaseSettingsModel): enabled: bool = True parents_path: str = Field( "", @@ -102,9 +100,9 @@ class EditorialSimpleCreatorPlugin(BaseSettingsModel): title="Shot Rename", default_factory=ShotRenameSubmodel ) - shot_hierarchy: ShotHierchySubmodel = Field( + shot_hierarchy: ShotHierarchySubmodel = Field( title="Shot Hierarchy", - default_factory=ShotHierchySubmodel + default_factory=ShotHierarchySubmodel ) shot_add_tasks: list[ShotAddTasksItem] = Field( title="Add tasks to shot", diff --git a/server_addon/traypublisher/server/version.py b/server_addon/traypublisher/server/version.py index df0c92f1e2..e57ad00718 100644 --- a/server_addon/traypublisher/server/version.py +++ b/server_addon/traypublisher/server/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring addon version.""" -__version__ = "0.1.2" +__version__ = "0.1.3" From e4ed21623a57618fa6889750c5cf97ec9cc1872d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 7 Nov 2023 17:35:48 +0100 Subject: [PATCH 0773/1233] fix task combinations --- openpype/tools/publisher/widgets/widgets.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index 77ebc3f0bb..9b31697749 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -789,15 +789,12 @@ class TasksCombobox(QtWidgets.QComboBox): self._set_is_valid(is_valid) - def confirm_value(self): + def confirm_value(self, asset_names): new_task_name = self._selected_items[0] - origin_value = copy.deepcopy(self._origin_value) - new_origin_value = [ + self._origin_value = [ (asset_name, new_task_name) - for (asset_name, task_name) in origin_value + for asset_name in asset_names ] - - self._origin_value = new_origin_value self._origin_selection = copy.deepcopy(self._selected_items) self._has_value_changed = False @@ -1180,6 +1177,7 @@ class GlobalAttrsWidget(QtWidgets.QWidget): subset_names = set() invalid_tasks = False + asset_names = [] for instance in self._current_instances: new_variant_value = instance.get("variant") new_asset_name = instance.get("asset") @@ -1193,6 +1191,7 @@ class GlobalAttrsWidget(QtWidgets.QWidget): if task_name is not None: new_task_name = task_name + asset_names.append(new_asset_name) try: new_subset_name = self._controller.get_subset_name( instance.creator_identifier, @@ -1237,7 +1236,7 @@ class GlobalAttrsWidget(QtWidgets.QWidget): self.asset_value_widget.confirm_value() if task_name is not None: - self.task_value_widget.confirm_value() + self.task_value_widget.confirm_value(asset_names) self.instance_context_changed.emit() From b8ed125569bdfd7c343d08483bfd70431d38f11d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 7 Nov 2023 17:44:33 +0100 Subject: [PATCH 0774/1233] set spacing between buttons --- openpype/tools/publisher/widgets/widgets.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index 9b31697749..6dbeaad821 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -1127,6 +1127,7 @@ class GlobalAttrsWidget(QtWidgets.QWidget): btns_layout = QtWidgets.QHBoxLayout() btns_layout.setContentsMargins(0, 0, 0, 0) btns_layout.addStretch(1) + btns_layout.setSpacing(5) btns_layout.addWidget(submit_btn) btns_layout.addWidget(cancel_btn) From 8c8c083395b77e920254e8dd5d83ad06eb0c636d Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 8 Nov 2023 03:24:51 +0000 Subject: [PATCH 0775/1233] [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 9832c77291..8500b78966 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.17.5" +__version__ = "3.17.6-nightly.1" From 3cd8fc6a0a8162a73d647f17a66294b0c79b2724 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 8 Nov 2023 03:25:29 +0000 Subject: [PATCH 0776/1233] 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 bdfc2ad46f..5d4db81a77 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.17.6-nightly.1 - 3.17.5 - 3.17.5-nightly.3 - 3.17.5-nightly.2 @@ -134,7 +135,6 @@ body: - 3.15.1 - 3.15.1-nightly.6 - 3.15.1-nightly.5 - - 3.15.1-nightly.4 validations: required: true - type: dropdown From cfc54439095140f2b6e2d84adc9ebfa09e96a4d3 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 8 Nov 2023 16:35:28 +0800 Subject: [PATCH 0777/1233] clean up code tweaks for OP settings(not suitable for ayon yet) --- .../plugins/publish/validate_loaded_plugin.py | 82 ++++++++++--------- openpype/settings/ayon_settings.py | 3 + .../schemas/schema_max_publish.json | 10 ++- .../max/server/settings/publishers.py | 5 +- 4 files changed, 59 insertions(+), 41 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py index e58685cc4d..8d59bbc120 100644 --- a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py +++ b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py @@ -51,34 +51,36 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, get_plugins()) } - for families, plugin in required_plugins.items(): - # Out of for loop build the instance family lookup - instance_families = {instance.data["family"]} - instance_families.update(instance.data.get("families", [])) - self.log.debug(f"{instance_families}") - # In the for loop check whether any family matches + # Build instance families lookup + instance_families = {instance.data["family"]} + instance_families.update(instance.data.get("families", [])) + self.log.debug(f"Checking plug-in validation for instance families: {instance_families}") + for families in required_plugins.keys(): + # Check for matching families match_families = {fam.strip() for fam in families.split(",") if fam.strip()} - self.log.debug(f"match_families: {match_families}") + self.log.debug(f"Plug-in family requirements: {match_families}") has_match = "*" in match_families or match_families.intersection( - instance_families) or families == "_" + instance_families) + if not has_match: continue - if not plugin: - return + plugins = [plugin for plugin in required_plugins[families]["plugins"]] + for plugin in plugins: + if not plugin: + return + plugin_name = plugin.format(**os.environ).lower() + plugin_index = available_plugins.get(plugin_name) - plugin_name = plugin.format(**os.environ).lower() - plugin_index = available_plugins.get(plugin_name) + if plugin_index is None: + invalid.append( + f"Plugin {plugin} does not exist in 3dsMax Plugin List." + ) + continue - if plugin_index is None: - invalid.append( - f"Plugin {plugin} does not exist in 3dsMax Plugin List." - ) - continue - - if not rt.pluginManager.isPluginDllLoaded(plugin_index): - invalid.append(f"Plugin {plugin} not loaded.") + if not rt.pluginManager.isPluginDllLoaded(plugin_index): + invalid.append(f"Plugin {plugin} not loaded.") return invalid @@ -108,25 +110,29 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, ["ValidateLoadedPlugin"] ["family_plugins_mapping"] ) - for families, plugin in required_plugins.items(): - families_list = families.split(",") - excluded_families = [family for family in families_list - if instance.data["family"] != family - and family != "_"] - if excluded_families: - cls.log.debug("The {} instance is not part of {}.".format( - instance.data["family"], excluded_families - )) - continue - if not plugin: + instance_families = {instance.data["family"]} + instance_families.update(instance.data.get("families", [])) + cls.log.debug(f"Checking plug-in validation for instance families: {instance_families}") + for families in required_plugins.keys(): + match_families = {fam.strip() for fam in + families.split(",") if fam.strip()} + cls.log.debug(f"Plug-in family requirements: {match_families}") + has_match = "*" in match_families or match_families.intersection( + instance_families) + + if not has_match: continue - plugin_name = plugin.format(**os.environ).lower() - plugin_index = available_plugins.get(plugin_name) + plugins = [plugin for plugin in required_plugins[families]["plugins"]] + for plugin in plugins: + if not plugin: + return + plugin_name = plugin.format(**os.environ).lower() + plugin_index = available_plugins.get(plugin_name) - if plugin_index is None: - cls.log.warning(f"Can't enable missing plugin: {plugin}") - continue + if plugin_index is None: + cls.log.warning(f"Can't enable missing plugin: {plugin}") + continue - if not rt.pluginManager.isPluginDllLoaded(plugin_index): - rt.pluginManager.loadPluginDll(plugin_index) + if not rt.pluginManager.isPluginDllLoaded(plugin_index): + rt.pluginManager.loadPluginDll(plugin_index) diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index fa73199269..0cefd047b1 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -653,6 +653,9 @@ def _convert_3dsmax_project_settings(ayon_settings, output): family_plugin_mapping = ( ayon_publish["ValidateLoadedPlugin"]["family_plugins_mapping"] ) + for item in family_plugin_mapping: + if "product_types" in item: + item["families"] = item.pop("product_types") new_family_plugin_mapping = { item["families"]: item["plugins"] for item in family_plugin_mapping diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json index b48ce20f5d..c44c7525da 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json @@ -72,7 +72,15 @@ "label": "Family Plugins Mapping", "use_label_wrap": true, "object_type": { - "type": "text" + "type": "dict", + "children": [ + { + "key": "plugins", + "label": "plugins", + "type": "list", + "object_type": "text" + } + ] } } ] diff --git a/server_addon/max/server/settings/publishers.py b/server_addon/max/server/settings/publishers.py index d23acc6dd7..d7169f8b96 100644 --- a/server_addon/max/server/settings/publishers.py +++ b/server_addon/max/server/settings/publishers.py @@ -29,8 +29,9 @@ class ValidateAttributesModel(BaseSettingsModel): class FamilyPluginsMappingModel(BaseSettingsModel): _layout = "compact" - families: str = Field(title="Families") - plugins: str = Field(title="Plugins") + product_types: str = Field(title="Product Types") + plugins: list[str] = Field( + default_factory=list,title="Plugins") class ValidateLoadedPluginModel(BaseSettingsModel): From 105cf5c116ab755ddc315956b63145d6b511e7db Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 8 Nov 2023 12:31:31 +0200 Subject: [PATCH 0778/1233] fix a typo in maya ayon settings --- server_addon/maya/server/settings/creators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/maya/server/settings/creators.py b/server_addon/maya/server/settings/creators.py index 84e873589d..34a54832af 100644 --- a/server_addon/maya/server/settings/creators.py +++ b/server_addon/maya/server/settings/creators.py @@ -27,7 +27,7 @@ class CreateUnrealStaticMeshModel(BaseSettingsModel): default_factory=list, title="Default Products" ) - static_mesh_prefixes: str = Field("S", title="Static Mesh Prefix") + static_mesh_prefix: str = Field("S", title="Static Mesh Prefix") collision_prefixes: list[str] = Field( default_factory=list, title="Collision Prefixes" From 60b775a4f410aec128627a8697004be5a45527cb Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 8 Nov 2023 12:31:49 +0200 Subject: [PATCH 0779/1233] fix some typos in Houdini ayon settings --- server_addon/houdini/server/settings/create.py | 2 +- server_addon/houdini/server/settings/publish.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/server_addon/houdini/server/settings/create.py b/server_addon/houdini/server/settings/create.py index 81b871e83f..e8db917849 100644 --- a/server_addon/houdini/server/settings/create.py +++ b/server_addon/houdini/server/settings/create.py @@ -26,7 +26,7 @@ class CreateStaticMeshModel(BaseSettingsModel): default_factory=list, title="Default Products" ) - static_mesh_prefixes: str = Field("S", title="Static Mesh Prefix") + static_mesh_prefix: str = Field("S", title="Static Mesh Prefix") collision_prefixes: list[str] = Field( default_factory=list, title="Collision Prefixes" diff --git a/server_addon/houdini/server/settings/publish.py b/server_addon/houdini/server/settings/publish.py index 342bf957c1..92a676b0d0 100644 --- a/server_addon/houdini/server/settings/publish.py +++ b/server_addon/houdini/server/settings/publish.py @@ -33,9 +33,9 @@ class BasicValidateModel(BaseSettingsModel): class PublishPluginsModel(BaseSettingsModel): - CollectRopFrameRange: CollectRopFrameRangeModel = Field( - default_factory=CollectRopFrameRangeModel, - title="Collect Rop Frame Range.", + CollectAssetHandles: CollectAssetHandlesModel = Field( + default_factory=CollectAssetHandlesModel, + title="Collect Asset Handles.", section="Collectors" ) ValidateContainers: BasicValidateModel = Field( @@ -60,7 +60,7 @@ class PublishPluginsModel(BaseSettingsModel): DEFAULT_HOUDINI_PUBLISH_SETTINGS = { - "CollectRopFrameRange": { + "CollectAssetHandles": { "use_asset_handles": True }, "ValidateContainers": { From 4311f4840dd3b39115e5ddd244c025d86df1f1c8 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 8 Nov 2023 12:38:21 +0200 Subject: [PATCH 0780/1233] bump addon versions --- server_addon/houdini/server/version.py | 2 +- server_addon/maya/server/version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server_addon/houdini/server/version.py b/server_addon/houdini/server/version.py index c49a95c357..75cf7831c4 100644 --- a/server_addon/houdini/server/version.py +++ b/server_addon/houdini/server/version.py @@ -1 +1 @@ -__version__ = "0.2.8" +__version__ = "0.2.9" diff --git a/server_addon/maya/server/version.py b/server_addon/maya/server/version.py index 90ce344d3e..805897cda3 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.5" +__version__ = "0.1.6" From d15dfaaf849d51c49d9a0e14d5be8f1c6f85ea2d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 8 Nov 2023 18:47:42 +0800 Subject: [PATCH 0781/1233] hound & code tweak regarding to OP setting --- .../plugins/publish/validate_loaded_plugin.py | 44 +++++++++---------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py index 8d59bbc120..d348e37abc 100644 --- a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py +++ b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """Validator for Loaded Plugin.""" import os -from pyblish.api import InstancePlugin, ValidatorOrder +import pyblish.api from pymxs import runtime as rt from openpype.pipeline.publish import ( @@ -13,7 +13,7 @@ from openpype.hosts.max.api.lib import get_plugins class ValidateLoadedPlugin(OptionalPyblishPluginMixin, - InstancePlugin): + pyblish.api.InstancePlugin): """Validates if the specific plugin is loaded in 3ds max. Studio Admin(s) can add the plugins they want to check in validation via studio defined project settings @@ -22,24 +22,21 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, """ - order = ValidatorOrder + order = pyblish.api.ValidatorOrder hosts = ["max"] label = "Validate Loaded Plugins" optional = True actions = [RepairAction] + family_plugins_mapping = {} + def get_invalid(self, instance): """Plugin entry point.""" if not self.is_active(instance.data): self.log.debug("Skipping Validate Loaded Plugin...") return - required_plugins = ( - instance.context.data["project_settings"]["max"]["publish"] - ["ValidateLoadedPlugin"] - ["family_plugins_mapping"] - ) - + required_plugins = self.family_plugins_mapping if not required_plugins: return @@ -54,11 +51,12 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, # Build instance families lookup instance_families = {instance.data["family"]} instance_families.update(instance.data.get("families", [])) - self.log.debug(f"Checking plug-in validation for instance families: {instance_families}") - for families in required_plugins.keys(): + self.log.debug("Checking plug-in validation " + f"for instance families: {instance_families}") + for family in required_plugins: # Check for matching families match_families = {fam.strip() for fam in - families.split(",") if fam.strip()} + family.split(",") if fam.strip()} self.log.debug(f"Plug-in family requirements: {match_families}") has_match = "*" in match_families or match_families.intersection( instance_families) @@ -66,7 +64,8 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, if not has_match: continue - plugins = [plugin for plugin in required_plugins[families]["plugins"]] + plugins = [plugin for plugin in + required_plugins[family]["plugins"]] for plugin in plugins: if not plugin: return @@ -75,7 +74,8 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, if plugin_index is None: invalid.append( - f"Plugin {plugin} does not exist in 3dsMax Plugin List." + f"Plugin {plugin} does not exist" + " in 3dsMax Plugin List." ) continue @@ -105,17 +105,14 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, plugin_name.lower(): index for index, plugin_name in enumerate( get_plugins()) } - required_plugins = ( - instance.context.data["project_settings"]["max"]["publish"] - ["ValidateLoadedPlugin"] - ["family_plugins_mapping"] - ) + required_plugins = cls.family_plugins_mapping instance_families = {instance.data["family"]} instance_families.update(instance.data.get("families", [])) - cls.log.debug(f"Checking plug-in validation for instance families: {instance_families}") - for families in required_plugins.keys(): + cls.log.debug("Checking plug-in validation " + f"for instance families: {instance_families}") + for family in required_plugins.keys(): match_families = {fam.strip() for fam in - families.split(",") if fam.strip()} + family.split(",") if fam.strip()} cls.log.debug(f"Plug-in family requirements: {match_families}") has_match = "*" in match_families or match_families.intersection( instance_families) @@ -123,7 +120,8 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, if not has_match: continue - plugins = [plugin for plugin in required_plugins[families]["plugins"]] + plugins = [plugin for plugin in + required_plugins[family]["plugins"]] for plugin in plugins: if not plugin: return From 1207ef3bbbcfe1b5db9bf0b5107d6e67eb5f6c53 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 8 Nov 2023 12:00:59 +0100 Subject: [PATCH 0782/1233] autofix folder path on older instances --- openpype/pipeline/create/context.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 333ab25f54..e4dcedda2c 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -2255,11 +2255,11 @@ class CreateContext: if task_name: task_names_by_asset_name[asset_name].add(task_name) - asset_names = [ + asset_names = { asset_name for asset_name in task_names_by_asset_name.keys() if asset_name is not None - ] + } fields = {"name", "data.tasks"} if AYON_SERVER_ENABLED: fields |= {"data.parents"} @@ -2270,10 +2270,12 @@ class CreateContext: )) task_names_by_asset_name = {} + asset_docs_by_name = collections.defaultdict(list) for asset_doc in asset_docs: asset_name = get_asset_name_identifier(asset_doc) tasks = asset_doc.get("data", {}).get("tasks") or {} task_names_by_asset_name[asset_name] = set(tasks.keys()) + asset_docs_by_name[asset_doc["name"]].append(asset_doc) for instance in instances: if not instance.has_valid_asset or not instance.has_valid_task: @@ -2281,6 +2283,11 @@ class CreateContext: if AYON_SERVER_ENABLED: asset_name = instance["folderPath"] + if "/" not in asset_name: + asset_docs = asset_docs_by_name.get(asset_name) + if len(asset_docs) == 1: + asset_name = get_asset_name_identifier(asset_docs[0]) + instance["folderPath"] = asset_name else: asset_name = instance["asset"] From d3804564f26b3ba1a3d304195ec42b408c2b3dff Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 8 Nov 2023 12:01:20 +0100 Subject: [PATCH 0783/1233] do not yield same asset multiple times --- openpype/client/server/entities.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/client/server/entities.py b/openpype/client/server/entities.py index 9e86dfdd63..becf4abda3 100644 --- a/openpype/client/server/entities.py +++ b/openpype/client/server/entities.py @@ -232,10 +232,12 @@ def get_assets( else: new_asset_names.add(name) + yielded_ids = set() if folder_paths: for folder in _folders_query( project_name, con, fields, folder_paths=folder_paths, **kwargs ): + yielded_ids.add(folder["id"]) yield convert_v4_folder_to_v3(folder, project_name) if not new_asset_names: @@ -244,7 +246,9 @@ def get_assets( for folder in _folders_query( project_name, con, fields, folder_names=new_asset_names, **kwargs ): - yield convert_v4_folder_to_v3(folder, project_name) + if folder["id"] not in yielded_ids: + yielded_ids.add(folder["id"]) + yield convert_v4_folder_to_v3(folder, project_name) def get_archived_assets( From 2f23b83481de1d7cad5a5139ff0955091e980da0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 8 Nov 2023 13:38:45 +0100 Subject: [PATCH 0784/1233] traypublisher: failed validator in editorial not necessary --- .../traypublisher/plugins/publish/validate_frame_ranges.py | 7 ++++++- 1 file changed, 6 insertions(+), 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 09de2d8db2..7a5a3c7fc1 100644 --- a/openpype/hosts/traypublisher/plugins/publish/validate_frame_ranges.py +++ b/openpype/hosts/traypublisher/plugins/publish/validate_frame_ranges.py @@ -30,12 +30,17 @@ class ValidateFrameRange(OptionalPyblishPluginMixin, if not self.is_active(instance.data): return + # Skip the instance if does not have asset entity in database + asset_doc = instance.data.get("assetEntity") + if not asset_doc: + self.log.warning("No asset data found, skipping.") + return + if (self.skip_timelines_check and any(re.search(pattern, instance.data["task"]) for pattern in self.skip_timelines_check)): self.log.info("Skipping for {} task".format(instance.data["task"])) - asset_doc = instance.data["assetEntity"] asset_data = asset_doc["data"] frame_start = asset_data["frameStart"] frame_end = asset_data["frameEnd"] From 8f1648f87a917538b8ab139f0c1af5286aa19988 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 8 Nov 2023 14:04:56 +0100 Subject: [PATCH 0785/1233] duplicated session 3 > session 4 --- openpype/pipeline/schema/session-4.0.json | 81 +++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 openpype/pipeline/schema/session-4.0.json diff --git a/openpype/pipeline/schema/session-4.0.json b/openpype/pipeline/schema/session-4.0.json new file mode 100644 index 0000000000..9f785939e4 --- /dev/null +++ b/openpype/pipeline/schema/session-4.0.json @@ -0,0 +1,81 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + + "title": "openpype:session-3.0", + "description": "The Avalon environment", + + "type": "object", + + "additionalProperties": true, + + "required": [ + "AVALON_PROJECT", + "AVALON_ASSET" + ], + + "properties": { + "AVALON_PROJECTS": { + "description": "Absolute path to root of project directories", + "type": "string", + "example": "/nas/projects" + }, + "AVALON_PROJECT": { + "description": "Name of project", + "type": "string", + "pattern": "^\\w*$", + "example": "Hulk" + }, + "AVALON_ASSET": { + "description": "Name of asset", + "type": "string", + "pattern": "^\\w*$", + "example": "Bruce" + }, + "AVALON_TASK": { + "description": "Name of task", + "type": "string", + "pattern": "^\\w*$", + "example": "modeling" + }, + "AVALON_APP": { + "description": "Name of host", + "type": "string", + "pattern": "^\\w*$", + "example": "maya2016" + }, + "AVALON_DB": { + "description": "Name of database", + "type": "string", + "pattern": "^\\w*$", + "example": "avalon", + "default": "avalon" + }, + "AVALON_LABEL": { + "description": "Nice name of Avalon, used in e.g. graphical user interfaces", + "type": "string", + "example": "Mindbender", + "default": "Avalon" + }, + "AVALON_TIMEOUT": { + "description": "Wherever there is a need for a timeout, this is the default value.", + "type": "string", + "pattern": "^[0-9]*$", + "default": "1000", + "example": "1000" + }, + "AVALON_INSTANCE_ID": { + "description": "Unique identifier for instances in a working file", + "type": "string", + "pattern": "^[\\w.]*$", + "default": "avalon.instance", + "example": "avalon.instance" + }, + "AVALON_CONTAINER_ID": { + "description": "Unique identifier for a loaded representation in a working file", + "type": "string", + "pattern": "^[\\w.]*$", + "default": "avalon.container", + "example": "avalon.container" + } + } +} From e85504b80c5665d6ddf73fa3b98855db7504b18c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 8 Nov 2023 14:08:03 +0100 Subject: [PATCH 0786/1233] use new schema in 'legacy_io' --- openpype/pipeline/legacy_io.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/legacy_io.py b/openpype/pipeline/legacy_io.py index 60fa035c22..864102dff9 100644 --- a/openpype/pipeline/legacy_io.py +++ b/openpype/pipeline/legacy_io.py @@ -30,7 +30,7 @@ def install(): session = session_data_from_environment(context_keys=True) - session["schema"] = "openpype:session-3.0" + session["schema"] = "openpype:session-4.0" try: schema.validate(session) except schema.ValidationError as e: From 2bc41f53bfa696ff6fe04bd1e840fd85f5fa48ed Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 8 Nov 2023 14:06:17 +0100 Subject: [PATCH 0787/1233] chnaged title of schema --- openpype/pipeline/schema/session-4.0.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/schema/session-4.0.json b/openpype/pipeline/schema/session-4.0.json index 9f785939e4..dc4791994e 100644 --- a/openpype/pipeline/schema/session-4.0.json +++ b/openpype/pipeline/schema/session-4.0.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-04/schema#", - "title": "openpype:session-3.0", + "title": "openpype:session-4.0", "description": "The Avalon environment", "type": "object", From d3fc80f9055b4c6b5d2e4d95ef6a489db465169c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 8 Nov 2023 14:06:35 +0100 Subject: [PATCH 0788/1233] allow forward slash in AVALON_ASSET --- openpype/pipeline/schema/session-4.0.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/schema/session-4.0.json b/openpype/pipeline/schema/session-4.0.json index dc4791994e..9610d8ec64 100644 --- a/openpype/pipeline/schema/session-4.0.json +++ b/openpype/pipeline/schema/session-4.0.json @@ -28,7 +28,7 @@ "AVALON_ASSET": { "description": "Name of asset", "type": "string", - "pattern": "^\\w*$", + "pattern": "^[\\/\\w]*$", "example": "Bruce" }, "AVALON_TASK": { From 3cade9a288db81c4eb6b8684641eafb947b5a183 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 8 Nov 2023 14:06:50 +0100 Subject: [PATCH 0789/1233] removed unused keys --- openpype/pipeline/schema/session-4.0.json | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/openpype/pipeline/schema/session-4.0.json b/openpype/pipeline/schema/session-4.0.json index 9610d8ec64..54a6323e8f 100644 --- a/openpype/pipeline/schema/session-4.0.json +++ b/openpype/pipeline/schema/session-4.0.json @@ -14,11 +14,6 @@ ], "properties": { - "AVALON_PROJECTS": { - "description": "Absolute path to root of project directories", - "type": "string", - "example": "/nas/projects" - }, "AVALON_PROJECT": { "description": "Name of project", "type": "string", @@ -62,20 +57,6 @@ "pattern": "^[0-9]*$", "default": "1000", "example": "1000" - }, - "AVALON_INSTANCE_ID": { - "description": "Unique identifier for instances in a working file", - "type": "string", - "pattern": "^[\\w.]*$", - "default": "avalon.instance", - "example": "avalon.instance" - }, - "AVALON_CONTAINER_ID": { - "description": "Unique identifier for a loaded representation in a working file", - "type": "string", - "pattern": "^[\\w.]*$", - "default": "avalon.container", - "example": "avalon.container" } } } From e5d3a1aeb0d9d9320eb1b201f73aab3fa383cc65 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 8 Nov 2023 14:07:06 +0100 Subject: [PATCH 0790/1233] 'AVALON_ASSET' is not required --- openpype/pipeline/schema/session-4.0.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/pipeline/schema/session-4.0.json b/openpype/pipeline/schema/session-4.0.json index 54a6323e8f..088156af85 100644 --- a/openpype/pipeline/schema/session-4.0.json +++ b/openpype/pipeline/schema/session-4.0.json @@ -9,8 +9,7 @@ "additionalProperties": true, "required": [ - "AVALON_PROJECT", - "AVALON_ASSET" + "AVALON_PROJECT" ], "properties": { From 389b568f6b8f356a9be4ab89496b37fa26706337 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 8 Nov 2023 14:10:06 +0100 Subject: [PATCH 0791/1233] removed AVALON_PROJECTS --- openpype/pipeline/context_tools.py | 6 +----- openpype/pipeline/mongodb.py | 2 -- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/openpype/pipeline/context_tools.py b/openpype/pipeline/context_tools.py index 38c80c87bb..fe46bd1558 100644 --- a/openpype/pipeline/context_tools.py +++ b/openpype/pipeline/context_tools.py @@ -88,11 +88,7 @@ def registered_root(): root = _registered_root["_"] if root: return root - - root = legacy_io.Session.get("AVALON_PROJECTS") - if root: - return os.path.normpath(root) - return "" + return {} def install_host(host): diff --git a/openpype/pipeline/mongodb.py b/openpype/pipeline/mongodb.py index 41a44c7373..c948983c3d 100644 --- a/openpype/pipeline/mongodb.py +++ b/openpype/pipeline/mongodb.py @@ -62,8 +62,6 @@ def auto_reconnect(func): SESSION_CONTEXT_KEYS = ( - # Root directory of projects on disk - "AVALON_PROJECTS", # Name of current Project "AVALON_PROJECT", # Name of current Asset From eda0afc26052f1ac1d5a6088aed71db94e8c3010 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 8 Nov 2023 14:27:47 +0100 Subject: [PATCH 0792/1233] traypublisher: adding exceptions for editorial instances --- .../traypublisher/plugins/create/create_editorial.py | 2 ++ .../plugins/publish/collect_sequence_frame_data.py | 6 ++++++ .../plugins/publish/validate_frame_ranges.py | 8 ++++---- openpype/plugins/publish/collect_resources_path.py | 6 ++++++ 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py index 8640500b18..a2746f115f 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_editorial.py +++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py @@ -701,6 +701,8 @@ or updating already created. Publishing will create OTIO file. # parent time properties "trackStartFrame": track_start_frame, "timelineOffset": timeline_offset, + "isEditorial": True, + # creator_attributes "creator_attributes": creator_attributes } diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_sequence_frame_data.py b/openpype/hosts/traypublisher/plugins/publish/collect_sequence_frame_data.py index db70d4fe0a..92cedf6b5b 100644 --- a/openpype/hosts/traypublisher/plugins/publish/collect_sequence_frame_data.py +++ b/openpype/hosts/traypublisher/plugins/publish/collect_sequence_frame_data.py @@ -27,6 +27,12 @@ class CollectSequenceFrameData( if not self.is_active(instance.data): return + # editorial would fail since they might not be in database yet + is_editorial = instance.data.get("isEditorial") + if is_editorial: + self.log.debug("Instance is Editorial. Skipping.") + return + frame_data = self.get_frame_data_from_repre_sequence(instance) if not frame_data: diff --git a/openpype/hosts/traypublisher/plugins/publish/validate_frame_ranges.py b/openpype/hosts/traypublisher/plugins/publish/validate_frame_ranges.py index 7a5a3c7fc1..4977a13374 100644 --- a/openpype/hosts/traypublisher/plugins/publish/validate_frame_ranges.py +++ b/openpype/hosts/traypublisher/plugins/publish/validate_frame_ranges.py @@ -30,10 +30,10 @@ class ValidateFrameRange(OptionalPyblishPluginMixin, if not self.is_active(instance.data): return - # Skip the instance if does not have asset entity in database - asset_doc = instance.data.get("assetEntity") - if not asset_doc: - self.log.warning("No asset data found, skipping.") + # editorial would fail since they might not be in database yet + is_editorial = instance.data.get("isEditorial") + if is_editorial: + self.log.debug("Instance is Editorial. Skipping.") return if (self.skip_timelines_check and diff --git a/openpype/plugins/publish/collect_resources_path.py b/openpype/plugins/publish/collect_resources_path.py index cfb4d63c1b..14c13310df 100644 --- a/openpype/plugins/publish/collect_resources_path.py +++ b/openpype/plugins/publish/collect_resources_path.py @@ -68,6 +68,12 @@ class CollectResourcesPath(pyblish.api.InstancePlugin): ] def process(self, instance): + # editorial would fail since they might not be in database yet + is_editorial = instance.data.get("isEditorial") + if is_editorial: + self.log.debug("Instance is Editorial. Skipping.") + return + anatomy = instance.context.data["anatomy"] template_data = copy.deepcopy(instance.data["anatomyData"]) From ea69e9943ec21cbcb0c5094ed6f063dd393cccb8 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 8 Nov 2023 15:56:37 +0200 Subject: [PATCH 0793/1233] update houdini license validator --- .../validate_houdini_license_category.py | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_houdini_license_category.py b/openpype/hosts/houdini/plugins/publish/validate_houdini_license_category.py index f1c52f22c1..e0e06e37c8 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_houdini_license_category.py +++ b/openpype/hosts/houdini/plugins/publish/validate_houdini_license_category.py @@ -3,30 +3,29 @@ import pyblish.api from openpype.pipeline import PublishValidationError -class ValidateHoudiniCommercialLicense(pyblish.api.InstancePlugin): - """Validate the Houdini instance runs a Commercial license. +class ValidateHoudiniNotApprenticeLicense(pyblish.api.InstancePlugin): + """Validate the Houdini instance runs a non Apprentice license. - When extracting USD files from a non-commercial Houdini license, even with - Houdini Indie license, the resulting files will get "scrambled" with - a license protection and get a special .usdnc or .usdlc suffix. + When extracting USD files from an apprentice Houdini license, + the resulting files will get "scrambled" with a license protection + and get a special .usdnc or .usdlc suffix. This currently breaks the Subset/representation pipeline so we disallow - any publish with those licenses. Only the commercial license is valid. + any publish with apprentice license. """ order = pyblish.api.ValidatorOrder families = ["usd"] hosts = ["houdini"] - label = "Houdini Commercial License" + label = "Houdini Apprentice License" def process(self, instance): import hou - license = hou.licenseCategory() - if license != hou.licenseCategoryType.Commercial: + if hou.isApprentice(): raise PublishValidationError( - ("USD Publishing requires a full Commercial " - "license. You are on: {}").format(license), + ("USD Publishing requires a non apprentice " + "license."), title=self.label) From d410899714cffcf75ab4be154b78af6acd99a601 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 8 Nov 2023 22:05:33 +0800 Subject: [PATCH 0794/1233] add ayon settings support for validate loaded plugins --- .../plugins/publish/validate_loaded_plugin.py | 11 ++++---- openpype/settings/ayon_settings.py | 19 ++++++-------- .../max/server/settings/publishers.py | 25 +++++++++++++------ 3 files changed, 30 insertions(+), 25 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py index d348e37abc..a681dc507f 100644 --- a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py +++ b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py @@ -63,12 +63,12 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, if not has_match: continue - - plugins = [plugin for plugin in - required_plugins[family]["plugins"]] + plugins = [plugin for plugin in required_plugins[family]["plugins"]] for plugin in plugins: if not plugin: return + # make sure the validation applied for + # plugins with different Max version plugin_name = plugin.format(**os.environ).lower() plugin_index = available_plugins.get(plugin_name) @@ -110,7 +110,7 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, instance_families.update(instance.data.get("families", [])) cls.log.debug("Checking plug-in validation " f"for instance families: {instance_families}") - for family in required_plugins.keys(): + for family in required_plugins: match_families = {fam.strip() for fam in family.split(",") if fam.strip()} cls.log.debug(f"Plug-in family requirements: {match_families}") @@ -120,8 +120,7 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, if not has_match: continue - plugins = [plugin for plugin in - required_plugins[family]["plugins"]] + plugins = [plugin for plugin in family["plugins"]] for plugin in plugins: if not plugin: return diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index 0cefd047b1..8fd7f990c4 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -650,19 +650,14 @@ def _convert_3dsmax_project_settings(ayon_settings, output): ayon_publish["ValidateAttributes"]["attributes"] = attributes if "ValidateLoadedPlugin" in ayon_publish: - family_plugin_mapping = ( - ayon_publish["ValidateLoadedPlugin"]["family_plugins_mapping"] - ) - for item in family_plugin_mapping: - if "product_types" in item: - item["families"] = item.pop("product_types") - new_family_plugin_mapping = { - item["families"]: item["plugins"] - for item in family_plugin_mapping - } - ayon_publish["ValidateLoadedPlugin"]["family_plugins_mapping"] = ( - new_family_plugin_mapping + new_plugin_mapping = {} + loaded_plugin = ( + ayon_publish["ValidateLoadedPlugin"] ) + for item in loaded_plugin["family_plugins_mapping"]: + name = item.pop("name") + new_plugin_mapping[name] = item + loaded_plugin["family_plugins_mapping"] = new_plugin_mapping output["max"] = ayon_max diff --git a/server_addon/max/server/settings/publishers.py b/server_addon/max/server/settings/publishers.py index d7169f8b96..cf482d59d8 100644 --- a/server_addon/max/server/settings/publishers.py +++ b/server_addon/max/server/settings/publishers.py @@ -1,7 +1,7 @@ import json from pydantic import Field, validator -from ayon_server.settings import BaseSettingsModel +from ayon_server.settings import BaseSettingsModel, ensure_unique_names from ayon_server.exceptions import BadRequestException @@ -27,20 +27,31 @@ class ValidateAttributesModel(BaseSettingsModel): return value -class FamilyPluginsMappingModel(BaseSettingsModel): +class FamilyMappingItemModel(BaseSettingsModel): _layout = "compact" - product_types: str = Field(title="Product Types") + name: str = Field("", title="Product type") plugins: list[str] = Field( - default_factory=list,title="Plugins") + default_factory=list, + title="Plugins" + ) class ValidateLoadedPluginModel(BaseSettingsModel): - enabled: bool = Field(title="ValidateLoadedPlugin") + enabled: bool = Field(title="Enabled") optional: bool = Field(title="Optional") - family_plugins_mapping: list[FamilyPluginsMappingModel] = Field( - default_factory=list, title="Family Plugins Mapping" + family_plugins_mapping: list[FamilyMappingItemModel] = ( + Field( + default_factory=list, + title="Family Plugins Mapping" + ) ) + # This is to validate unique names (like in dict) + @validator("family_plugins_mapping") + def validate_unique_outputs(cls, value): + ensure_unique_names(value) + return value + class BasicValidateModel(BaseSettingsModel): enabled: bool = Field(title="Enabled") From 57131a8b40bb6633aaeda75528a58bb3967298b3 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 8 Nov 2023 22:07:36 +0800 Subject: [PATCH 0795/1233] hound --- openpype/hosts/max/plugins/publish/validate_loaded_plugin.py | 3 ++- server_addon/max/server/settings/publishers.py | 4 +--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py index a681dc507f..06486e94a6 100644 --- a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py +++ b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py @@ -63,7 +63,8 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, if not has_match: continue - plugins = [plugin for plugin in required_plugins[family]["plugins"]] + plugins = [plugin for plugin in + required_plugins[family]["plugins"]] for plugin in plugins: if not plugin: return diff --git a/server_addon/max/server/settings/publishers.py b/server_addon/max/server/settings/publishers.py index cf482d59d8..a752d8cb74 100644 --- a/server_addon/max/server/settings/publishers.py +++ b/server_addon/max/server/settings/publishers.py @@ -39,11 +39,9 @@ class FamilyMappingItemModel(BaseSettingsModel): class ValidateLoadedPluginModel(BaseSettingsModel): enabled: bool = Field(title="Enabled") optional: bool = Field(title="Optional") - family_plugins_mapping: list[FamilyMappingItemModel] = ( - Field( + family_plugins_mapping: list[FamilyMappingItemModel] = Field( default_factory=list, title="Family Plugins Mapping" - ) ) # This is to validate unique names (like in dict) From af9718f753b836542aa55a837123caa655beb369 Mon Sep 17 00:00:00 2001 From: Mustafa Taher Date: Wed, 8 Nov 2023 16:25:17 +0200 Subject: [PATCH 0796/1233] BigRoy's comment - update doc string Co-authored-by: Roy Nieterau --- .../plugins/publish/validate_houdini_license_category.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_houdini_license_category.py b/openpype/hosts/houdini/plugins/publish/validate_houdini_license_category.py index e0e06e37c8..fd6ad9e3be 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_houdini_license_category.py +++ b/openpype/hosts/houdini/plugins/publish/validate_houdini_license_category.py @@ -8,7 +8,7 @@ class ValidateHoudiniNotApprenticeLicense(pyblish.api.InstancePlugin): When extracting USD files from an apprentice Houdini license, the resulting files will get "scrambled" with a license protection - and get a special .usdnc or .usdlc suffix. + and get a special .usdnc suffix. This currently breaks the Subset/representation pipeline so we disallow any publish with apprentice license. From 81d054799c5761180baca44dba95a4f00a0f32de Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 8 Nov 2023 15:26:21 +0100 Subject: [PATCH 0797/1233] Apply suggestions from code review Co-authored-by: Roy Nieterau --- openpype/pipeline/schema/session-4.0.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/pipeline/schema/session-4.0.json b/openpype/pipeline/schema/session-4.0.json index 088156af85..0dab48aa46 100644 --- a/openpype/pipeline/schema/session-4.0.json +++ b/openpype/pipeline/schema/session-4.0.json @@ -35,7 +35,7 @@ "description": "Name of host", "type": "string", "pattern": "^\\w*$", - "example": "maya2016" + "example": "maya" }, "AVALON_DB": { "description": "Name of database", @@ -47,7 +47,7 @@ "AVALON_LABEL": { "description": "Nice name of Avalon, used in e.g. graphical user interfaces", "type": "string", - "example": "Mindbender", + "example": "MyLabel", "default": "Avalon" }, "AVALON_TIMEOUT": { From 1676f37d3aa98633ff9ac8531285b850d7c5ba28 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 8 Nov 2023 16:32:33 +0100 Subject: [PATCH 0798/1233] change default value of registered root --- openpype/pipeline/context_tools.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/openpype/pipeline/context_tools.py b/openpype/pipeline/context_tools.py index fe46bd1558..33eb335ab9 100644 --- a/openpype/pipeline/context_tools.py +++ b/openpype/pipeline/context_tools.py @@ -43,7 +43,7 @@ from . import ( _is_installed = False _process_id = None -_registered_root = {"_": ""} +_registered_root = {"_": {}} _registered_host = {"_": None} # Keep modules manager (and it's modules) in memory # - that gives option to register modules' callbacks @@ -85,10 +85,7 @@ def register_root(path): def registered_root(): """Return currently registered root""" - root = _registered_root["_"] - if root: - return root - return {} + return _registered_root["_"] def install_host(host): From 74d0f944afd6ce2e719204c0505981f5fd77d952 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 8 Nov 2023 15:44:32 +0000 Subject: [PATCH 0799/1233] Do not pack image if it is already packed --- .../blender/plugins/publish/extract_blend.py | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/extract_blend.py b/openpype/hosts/blender/plugins/publish/extract_blend.py index c8eeef7fd7..645314e50e 100644 --- a/openpype/hosts/blender/plugins/publish/extract_blend.py +++ b/openpype/hosts/blender/plugins/publish/extract_blend.py @@ -28,16 +28,22 @@ class ExtractBlend(publish.Extractor): for obj in instance: data_blocks.add(obj) # Pack used images in the blend files. - if obj.type == 'MESH': - for material_slot in obj.material_slots: - mat = material_slot.material - if mat and mat.use_nodes: - tree = mat.node_tree - if tree.type == 'SHADER': - for node in tree.nodes: - if node.bl_idname == 'ShaderNodeTexImage': - if node.image: - node.image.pack() + if obj.type != 'MESH': + continue + for material_slot in obj.material_slots: + mat = material_slot.material + if not(mat and mat.use_nodes): + continue + tree = mat.node_tree + if tree.type != 'SHADER': + continue + for node in tree.nodes: + if node.bl_idname != 'ShaderNodeTexImage': + continue + # Check if image is not packed already + # and pack it if not. + if node.image and node.image.packed_file is None: + node.image.pack() bpy.data.libraries.write(filepath, data_blocks) From 06856644d160155dee3b378ad2326cd44babf5b9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 8 Nov 2023 17:07:48 +0100 Subject: [PATCH 0800/1233] modify 'registered_root' function docstring --- openpype/pipeline/context_tools.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/context_tools.py b/openpype/pipeline/context_tools.py index 33eb335ab9..71f41fd234 100644 --- a/openpype/pipeline/context_tools.py +++ b/openpype/pipeline/context_tools.py @@ -84,7 +84,21 @@ def register_root(path): def registered_root(): - """Return currently registered root""" + """Return registered roots from current project anatomy. + + Consider this does return roots only for current project and current + platforms, only if host was installer using 'install_host'. + + Deprecated: + Please use project 'Anatomy' to get roots. This function is still used + at current core functions of load logic, but that will change + in future and this function will be removed eventually. Using this + function at new places can cause problems in the future. + + Returns: + dict[str, str]: Root paths. + """ + return _registered_root["_"] From b3649b5c5cd1167acb6de9d63ec3c50263be2408 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 9 Nov 2023 13:29:48 +0800 Subject: [PATCH 0801/1233] improve debug message & use debug instead of info for artist-facing report --- openpype/hosts/maya/api/fbx.py | 4 ++-- .../hosts/maya/plugins/publish/extract_fbx_animation.py | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/api/fbx.py b/openpype/hosts/maya/api/fbx.py index dbb3578f08..c8f4050bc1 100644 --- a/openpype/hosts/maya/api/fbx.py +++ b/openpype/hosts/maya/api/fbx.py @@ -156,7 +156,7 @@ class FBXExtractor: # 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)) # Collect the start and end including handles start = instance.data.get("frameStartHandle") or \ @@ -186,7 +186,7 @@ class FBXExtractor: template = "FBXExport{0} {1}" if key == "UpAxis" else \ "FBXExport{0} -v {1}" # noqa cmd = template.format(key, value) - self.log.info(cmd) + self.log.debug(cmd) mel.eval(cmd) # Never show the UI or generate a log diff --git a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py index c6f8029e7d..c13d349394 100644 --- a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py +++ b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py @@ -43,9 +43,13 @@ class ExtractFBXAnimation(publish.Extractor): # FBX does not include the namespace but preserves the node # names as existing in the rig workfile if not out_members: + skeleton_set = [ + i for i in instance + if i.endswith("skeletonAnim_SET") + ] self.log.debug( - "Top group of animated skeleton not found.." - "skipping extraction") + "Top group of animated skeleton not found in " + "{}.\nSkipping fbx animation extraction".format(skeleton_set)) return namespace = get_namespace(out_members[0]) From ed60c361bab707bdd06d6de8fc09e828ff57c639 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 9 Nov 2023 13:30:31 +0800 Subject: [PATCH 0802/1233] hound --- openpype/hosts/maya/plugins/publish/extract_fbx_animation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py index c13d349394..e88b8b1e16 100644 --- a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py +++ b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py @@ -44,8 +44,8 @@ class ExtractFBXAnimation(publish.Extractor): # names as existing in the rig workfile if not out_members: skeleton_set = [ - i for i in instance - if i.endswith("skeletonAnim_SET") + i for i in instance + if i.endswith("skeletonAnim_SET") ] self.log.debug( "Top group of animated skeleton not found in " From 0403af298e795cdc7a206b88485f40bd6e07072b Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 9 Nov 2023 17:21:37 +0800 Subject: [PATCH 0803/1233] tweaks the codes to use list instead of dict for OP settings --- .../plugins/publish/validate_loaded_plugin.py | 141 +++++++++--------- .../defaults/project_settings/max.json | 2 +- .../schemas/schema_max_publish.json | 10 +- 3 files changed, 77 insertions(+), 76 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py index 06486e94a6..7450c8f971 100644 --- a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py +++ b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py @@ -17,9 +17,6 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, """Validates if the specific plugin is loaded in 3ds max. Studio Admin(s) can add the plugins they want to check in validation via studio defined project settings - If families = ["*"], all the required plugins would be validated - If families - """ order = pyblish.api.ValidatorOrder @@ -30,66 +27,77 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, family_plugins_mapping = {} - def get_invalid(self, instance): + @classmethod + def get_invalid(cls, instance): """Plugin entry point.""" - if not self.is_active(instance.data): - self.log.debug("Skipping Validate Loaded Plugin...") - return - - required_plugins = self.family_plugins_mapping - if not required_plugins: + family_plugins_mapping = cls.family_plugins_mapping + if not family_plugins_mapping: return invalid = [] + # Find all plug-in requirements for current instance + instance_families = {instance.data["family"]} + instance_families.update(instance.data.get("families", [])) + cls.log.debug("Checking plug-in validation " + f"for instance families: {instance_families}") + all_required_plugins = set() + + for mapping in family_plugins_mapping: + # Check for matching families + if not mapping: + return + + match_families = {fam for fam in mapping["families"] if fam.strip()} + has_match = "*" in match_families or match_families.intersection( + instance_families) + + if not has_match: + continue + + cls.log.debug(f"Found plug-in family requirements: {match_families}") + required_plugins = [ + # match lowercase and format with os.environ to allow + # plugin names defined by max version, e.g. {3DSMAX_VERSION} + plugin.format(**os.environ).lower() + for plugin in mapping["plugins"] + # ignore empty fields in settings + if plugin.strip() + ] + + all_required_plugins.update(required_plugins) + + if not all_required_plugins: + # Instance has no plug-in requirements + return # get all DLL loaded plugins in Max and their plugin index available_plugins = { plugin_name.lower(): index for index, plugin_name in enumerate( get_plugins()) } - - # Build instance families lookup - instance_families = {instance.data["family"]} - instance_families.update(instance.data.get("families", [])) - self.log.debug("Checking plug-in validation " - f"for instance families: {instance_families}") - for family in required_plugins: - # Check for matching families - match_families = {fam.strip() for fam in - family.split(",") if fam.strip()} - self.log.debug(f"Plug-in family requirements: {match_families}") - has_match = "*" in match_families or match_families.intersection( - instance_families) - - if not has_match: + # validate the required plug-ins + for plugin in sorted(all_required_plugins): + plugin_index = available_plugins.get(plugin) + if plugin_index is None: + debug_msg = ( + f"Plugin {plugin} does not exist" + " in 3dsMax Plugin List." + ) + invalid.append((plugin, debug_msg)) continue - plugins = [plugin for plugin in - required_plugins[family]["plugins"]] - for plugin in plugins: - if not plugin: - return - # make sure the validation applied for - # plugins with different Max version - plugin_name = plugin.format(**os.environ).lower() - plugin_index = available_plugins.get(plugin_name) - - if plugin_index is None: - invalid.append( - f"Plugin {plugin} does not exist" - " in 3dsMax Plugin List." - ) - continue - - if not rt.pluginManager.isPluginDllLoaded(plugin_index): - invalid.append(f"Plugin {plugin} not loaded.") - + if not rt.pluginManager.isPluginDllLoaded(plugin_index): + debug_msg = f"Plugin {plugin} not loaded." + invalid.append((plugin, debug_msg)) return invalid def process(self, instance): - invalid_plugins = self.get_invalid(instance) - if invalid_plugins: + if not self.is_active(instance.data): + self.log.debug("Skipping Validate Loaded Plugin...") + return + invalid = self.get_invalid(instance) + if invalid: bullet_point_invalid_statement = "\n".join( - "- {}".format(invalid) for invalid in invalid_plugins + "- {}".format(message) for _, message in invalid ) report = ( "Required plugins are not loaded.\n\n" @@ -101,36 +109,23 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, @classmethod def repair(cls, instance): + # get all DLL loaded plugins in Max and their plugin index + invalid = cls.get_invalid(instance) + if not invalid: + return + # get all DLL loaded plugins in Max and their plugin index available_plugins = { plugin_name.lower(): index for index, plugin_name in enumerate( get_plugins()) } - required_plugins = cls.family_plugins_mapping - instance_families = {instance.data["family"]} - instance_families.update(instance.data.get("families", [])) - cls.log.debug("Checking plug-in validation " - f"for instance families: {instance_families}") - for family in required_plugins: - match_families = {fam.strip() for fam in - family.split(",") if fam.strip()} - cls.log.debug(f"Plug-in family requirements: {match_families}") - has_match = "*" in match_families or match_families.intersection( - instance_families) - if not has_match: + for invalid_plugin, _ in invalid: + plugin_index = available_plugins.get(invalid_plugin) + + if plugin_index is None: + cls.log.warning(f"Can't enable missing plugin: {invalid_plugin}") continue - plugins = [plugin for plugin in family["plugins"]] - for plugin in plugins: - if not plugin: - return - plugin_name = plugin.format(**os.environ).lower() - plugin_index = available_plugins.get(plugin_name) - - if plugin_index is None: - cls.log.warning(f"Can't enable missing plugin: {plugin}") - continue - - if not rt.pluginManager.isPluginDllLoaded(plugin_index): - rt.pluginManager.loadPluginDll(plugin_index) + if not rt.pluginManager.isPluginDllLoaded(plugin_index): + rt.pluginManager.loadPluginDll(plugin_index) diff --git a/openpype/settings/defaults/project_settings/max.json b/openpype/settings/defaults/project_settings/max.json index 57927b48c7..92049cdbe9 100644 --- a/openpype/settings/defaults/project_settings/max.json +++ b/openpype/settings/defaults/project_settings/max.json @@ -44,7 +44,7 @@ "ValidateLoadedPlugin": { "enabled": false, "optional": true, - "family_plugins_mapping": {} + "family_plugins_mapping": [] } } } diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json index c44c7525da..c6d37ae993 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json @@ -66,7 +66,7 @@ "label": "Optional" }, { - "type": "dict-modifiable", + "type": "list", "collapsible": true, "key": "family_plugins_mapping", "label": "Family Plugins Mapping", @@ -74,9 +74,15 @@ "object_type": { "type": "dict", "children": [ + { + "key": "families", + "label": "Famiies", + "type": "list", + "object_type": "text" + }, { "key": "plugins", - "label": "plugins", + "label": "Plugins", "type": "list", "object_type": "text" } From f52af5bcc464621066d1db19a29c0e31bb6f7773 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 9 Nov 2023 17:23:11 +0800 Subject: [PATCH 0804/1233] add the full stop --- openpype/hosts/maya/plugins/publish/extract_fbx_animation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py index e88b8b1e16..756158d4f0 100644 --- a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py +++ b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py @@ -49,7 +49,7 @@ class ExtractFBXAnimation(publish.Extractor): ] self.log.debug( "Top group of animated skeleton not found in " - "{}.\nSkipping fbx animation extraction".format(skeleton_set)) + "{}.\nSkipping fbx animation extraction.".format(skeleton_set)) return namespace = get_namespace(out_members[0]) From 0d6e728552d0a42c6338b07dd761e981111d6b6e Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 9 Nov 2023 10:42:38 +0000 Subject: [PATCH 0805/1233] Added function to get collections Also added a flag in old function to get selection to include collections. --- openpype/hosts/blender/api/lib.py | 54 +++++++++++++++++-- .../plugins/create/create_blendScene.py | 30 +++-------- 2 files changed, 58 insertions(+), 26 deletions(-) diff --git a/openpype/hosts/blender/api/lib.py b/openpype/hosts/blender/api/lib.py index 9bb560c364..2f33fd25ad 100644 --- a/openpype/hosts/blender/api/lib.py +++ b/openpype/hosts/blender/api/lib.py @@ -266,9 +266,57 @@ def read(node: bpy.types.bpy_struct_meta_idprop): return data -def get_selection() -> List[bpy.types.Object]: - """Return the selected objects from the current scene.""" - return [obj for obj in bpy.context.scene.objects if obj.select_get()] +def get_selected_collections(): + """ + Returns a list of the currently selected collections in the outliner. + + Raises: + RuntimeError: If the outliner cannot be found in the main Blender + window. + + Returns: + list: A list of `bpy.types.Collection` objects that are currently + selected in the outliner. + """ + try: + area = next( + area for area in bpy.context.window.screen.areas + if area.type == 'OUTLINER') + region = next( + region for region in area.regions + if region.type == 'WINDOW') + except StopIteration as e: + raise RuntimeError("Could not find outliner. An outliner space " + "must be in the main Blender window.") from e + + with bpy.context.temp_override( + window=bpy.context.window, + area=area, + region=region, + screen=bpy.context.window.screen + ): + ids = bpy.context.selected_ids + + return [id for id in ids if isinstance(id, bpy.types.Collection)] + + +def get_selection(include_collections: bool = False) -> List[bpy.types.Object]: + """ + Returns a list of selected objects in the current Blender scene. + + Args: + include_collections (bool, optional): Whether to include selected + collections in the result. Defaults to False. + + Returns: + List[bpy.types.Object]: A list of selected objects. + """ + selection = [obj for obj in bpy.context.scene.objects if obj.select_get()] + + if include_collections: + selection.extend(get_selected_collections()) + + return selection @contextlib.contextmanager diff --git a/openpype/hosts/blender/plugins/create/create_blendScene.py b/openpype/hosts/blender/plugins/create/create_blendScene.py index 791e741ca7..bb57a16888 100644 --- a/openpype/hosts/blender/plugins/create/create_blendScene.py +++ b/openpype/hosts/blender/plugins/create/create_blendScene.py @@ -40,29 +40,13 @@ class CreateBlendScene(plugin.Creator): self.data['task'] = get_current_task_name() lib.imprint(asset_group, self.data) - try: - area = next( - area for area in bpy.context.window.screen.areas - if area.type == 'OUTLINER') - region = next( - region for region in area.regions - if region.type == 'WINDOW') - except StopIteration as e: - raise RuntimeError("Could not find outliner. An outliner space " - "must be in the main Blender window.") from e + if (self.options or {}).get("useSelection"): + selection = lib.get_selection(include_collections=True) - with bpy.context.temp_override( - window=bpy.context.window, - area=area, - region=region, - screen=bpy.context.window.screen - ): - ids = bpy.context.selected_ids - - for id in ids: - if isinstance(id, bpy.types.Collection): - asset_group.children.link(id) - elif isinstance(id, bpy.types.Object): - asset_group.objects.link(id) + for data in selection: + if isinstance(data, bpy.types.Collection): + asset_group.children.link(data) + elif isinstance(data, bpy.types.Object): + asset_group.objects.link(data) return asset_group From f3de6175bcb1bbbd5105602023be05fac3f717dc Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 9 Nov 2023 10:44:37 +0000 Subject: [PATCH 0806/1233] Hound fixes --- openpype/hosts/blender/api/lib.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/blender/api/lib.py b/openpype/hosts/blender/api/lib.py index 2f33fd25ad..1f68dd0839 100644 --- a/openpype/hosts/blender/api/lib.py +++ b/openpype/hosts/blender/api/lib.py @@ -287,7 +287,7 @@ def get_selected_collections(): if region.type == 'WINDOW') except StopIteration as e: raise RuntimeError("Could not find outliner. An outliner space " - "must be in the main Blender window.") from e + "must be in the main Blender window.") from e with bpy.context.temp_override( window=bpy.context.window, @@ -311,7 +311,7 @@ def get_selection(include_collections: bool = False) -> List[bpy.types.Object]: Returns: List[bpy.types.Object]: A list of selected objects. """ - selection = [obj for obj in bpy.context.scene.objects if obj.select_get()] + selection = [obj for obj in bpy.context.scene.objects if obj.select_get()] if include_collections: selection.extend(get_selected_collections()) From a023183ca2f24a634b3377fde17ea394ec46a38f Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 9 Nov 2023 11:08:36 +0000 Subject: [PATCH 0807/1233] Fix potential problem when removing data --- .../blender/plugins/load/load_blendscene.py | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/openpype/hosts/blender/plugins/load/load_blendscene.py b/openpype/hosts/blender/plugins/load/load_blendscene.py index b1b2c3ba79..2c955af9e8 100644 --- a/openpype/hosts/blender/plugins/load/load_blendscene.py +++ b/openpype/hosts/blender/plugins/load/load_blendscene.py @@ -180,7 +180,7 @@ class BlendSceneLoader(plugin.AssetLoader): avalon_container = bpy.data.collections.get(AVALON_CONTAINERS) avalon_container.children.link(asset_group) - # Restore the old data, but reset memebers, as they don't exist anymore + # Restore the old data, but reset members, as they don't exist anymore # This avoids a crash, because the memory addresses of those members # are not valid anymore old_data["members"] = [] @@ -202,22 +202,20 @@ class BlendSceneLoader(plugin.AssetLoader): group_name = container["objectName"] asset_group = bpy.data.collections.get(group_name) - attrs = [ - attr for attr in dir(bpy.data) - if isinstance( - getattr(bpy.data, attr), - bpy.types.bpy_prop_collection - ) - ] + members = set(asset_group.get(AVALON_PROPERTY).get("members", [])) - members = asset_group.get(AVALON_PROPERTY).get("members", []) + if members: + for attr_name in dir(bpy.data): + attr = getattr(bpy.data, attr_name) + if not isinstance(attr, bpy.types.bpy_prop_collection): + continue - for attr in attrs: - for data in getattr(bpy.data, attr): - if data in members: - # Skip the asset group - if data == asset_group: + # ensure to make a list copy because we + # we remove members as we iterate + for data in list(attr): + if data not in members or data == asset_group: continue - getattr(bpy.data, attr).remove(data) + + attr.remove(data) bpy.data.collections.remove(asset_group) From 8a040ad0a4d88b99eca7423e6bea2ea04e183a29 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 9 Nov 2023 12:23:46 +0100 Subject: [PATCH 0808/1233] pass in variant to 'get_addons_settings' --- openpype/settings/ayon_settings.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index 5eb68e3972..67ef109d8b 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -1497,7 +1497,8 @@ class _AyonSettingsCache: if cls._use_bundles(): value = ayon_api.get_addons_settings( bundle_name=cls._get_bundle_name(), - project_name=project_name + project_name=project_name, + variant=cls._get_variant() ) else: value = ayon_api.get_addons_settings(project_name) From 2c0d1b5c2bd3efb9e0740b0d5324a588533459e1 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 9 Nov 2023 13:56:08 +0100 Subject: [PATCH 0809/1233] improving the logic for handles operation --- openpype/hosts/resolve/api/plugin.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py index 09c5a69d73..886615c7aa 100644 --- a/openpype/hosts/resolve/api/plugin.py +++ b/openpype/hosts/resolve/api/plugin.py @@ -410,10 +410,16 @@ class ClipLoader: source_duration = int(_clip_property("Frames")) # get version data frame data from db - frame_start = self.data["versionData"].get("frameStart") - frame_end = self.data["versionData"].get("frameEnd") - handle_start = self.data["versionData"].get("handleStart") - handle_end = self.data["versionData"].get("handleEnd") + version_data = self.data["versionData"] + frame_start = version_data.get("frameStart") + frame_end = version_data.get("frameEnd") + + if self.with_handles: + handle_start = version_data.get("handleStart") or 0 + handle_end = version_data.get("handleEnd") or 0 + else: + handle_start = 0 + handle_end = 0 """ There are cases where representation could be published without @@ -434,8 +440,8 @@ class ClipLoader: if ( frame_start is None or frame_end is None or - handle_start is None or - handle_end is None + handle_start is 0 or + handle_end is 0 ): # if not then rather assume that source has no handles source_with_handles = False @@ -464,12 +470,11 @@ class ClipLoader: timeline_in = int( timeline_start + self.data["assetData"]["clipIn"]) - # only exclude handles if source has no handles or # if user wants to load without handles if ( - not self.with_handles - or not source_with_handles + not self.with_handles # set by user + or not source_with_handles # result of source duration check ): source_in += handle_start source_out -= handle_end From a4dbc1958011cb0fbdeef68499797606c5fabc51 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 9 Nov 2023 12:56:34 +0000 Subject: [PATCH 0810/1233] Changed validator to work with other families as well --- .../plugins/publish/validate_instance_empty.py | 17 +++++++++++++---- .../blender/server/settings/publish_plugins.py | 9 ++++----- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/validate_instance_empty.py b/openpype/hosts/blender/plugins/publish/validate_instance_empty.py index 66d8b45e1e..5abfd6dee8 100644 --- a/openpype/hosts/blender/plugins/publish/validate_instance_empty.py +++ b/openpype/hosts/blender/plugins/publish/validate_instance_empty.py @@ -8,11 +8,20 @@ class ValidateInstanceEmpty(pyblish.api.InstancePlugin): order = pyblish.api.ValidatorOrder - 0.01 hosts = ["blender"] - families = ["blendScene"] + families = ["model", "pointcache", "rig", "camera" "layout", "blendScene"] label = "Validate Instance is not Empty" optional = False def process(self, instance): - collection = bpy.data.collections[instance.name] - if not (collection.objects or collection.children): - raise RuntimeError(f"Instance {instance.name} is empty.") + self.log.debug(instance) + self.log.debug(instance.data) + if instance.data["family"] == "blendScene": + # blendScene instances are collections + collection = bpy.data.collections[instance.name] + if not (collection.objects or collection.children): + raise RuntimeError(f"Instance {instance.name} is empty.") + else: + # All other instances are objects + asset_group = bpy.data.objects[instance.name] + if not asset_group.children: + raise RuntimeError(f"Instance {instance.name} is empty.") diff --git a/server_addon/blender/server/settings/publish_plugins.py b/server_addon/blender/server/settings/publish_plugins.py index bb68b40cbb..1c4ad0c6fd 100644 --- a/server_addon/blender/server/settings/publish_plugins.py +++ b/server_addon/blender/server/settings/publish_plugins.py @@ -67,6 +67,10 @@ class PublishPuginsModel(BaseSettingsModel): default_factory=ValidateFileSavedModel, title="Validate File Saved", ) + ValidateInstanceEmpty: ValidatePluginModel = Field( + default_factory=ValidatePluginModel, + title="Validate Instance is not Empty" + ) ValidateMeshHasUvs: ValidatePluginModel = Field( default_factory=ValidatePluginModel, title="Validate Mesh Has Uvs", @@ -84,11 +88,6 @@ class PublishPuginsModel(BaseSettingsModel): default_factory=ValidatePluginModel, title="Validate No Colons In Name" ) - ValidateInstanceEmpty: ValidatePluginModel = Field( - default_factory=ValidatePluginModel, - title="Validate Instance is not Empty", - section="BlendScene Validators" - ) ValidateRenderCameraIsSet: ValidatePluginModel = Field( default_factory=ValidatePluginModel, title="Validate Render Camera Is Set", From cab0a6a3ee661828d694297d2229721dc0a2b969 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 9 Nov 2023 13:03:46 +0000 Subject: [PATCH 0811/1233] Improved formatting --- openpype/hosts/blender/plugins/publish/extract_blend.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/extract_blend.py b/openpype/hosts/blender/plugins/publish/extract_blend.py index f29dae7f69..17e574c1be 100644 --- a/openpype/hosts/blender/plugins/publish/extract_blend.py +++ b/openpype/hosts/blender/plugins/publish/extract_blend.py @@ -28,11 +28,13 @@ class ExtractBlend(publish.Extractor): for data in instance: data_blocks.add(data) # Pack used images in the blend files. - if not(isinstance(data, bpy.types.Object) and data.type == 'MESH'): + if not ( + isinstance(data, bpy.types.Object) and data.type == 'MESH' + ): continue for material_slot in data.material_slots: mat = material_slot.material - if not(mat and mat.use_nodes): + if not (mat and mat.use_nodes): continue tree = mat.node_tree if tree.type != 'SHADER': From 12c8d3d2f8d79dbadca22efe8cc691c998def4a1 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 9 Nov 2023 21:17:29 +0800 Subject: [PATCH 0812/1233] add supports for ayon settings --- openpype/settings/ayon_settings.py | 9 +++------ server_addon/max/server/settings/publishers.py | 11 ++++------- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index 8fd7f990c4..eb7e3a2d0f 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -650,14 +650,11 @@ def _convert_3dsmax_project_settings(ayon_settings, output): ayon_publish["ValidateAttributes"]["attributes"] = attributes if "ValidateLoadedPlugin" in ayon_publish: - new_plugin_mapping = {} loaded_plugin = ( - ayon_publish["ValidateLoadedPlugin"] + ayon_publish["ValidateLoadedPlugin"]["family_plugins_mapping"] ) - for item in loaded_plugin["family_plugins_mapping"]: - name = item.pop("name") - new_plugin_mapping[name] = item - loaded_plugin["family_plugins_mapping"] = new_plugin_mapping + for item in loaded_plugin: + item["families"] = item.pop("product_types") output["max"] = ayon_max diff --git a/server_addon/max/server/settings/publishers.py b/server_addon/max/server/settings/publishers.py index a752d8cb74..eeb6478216 100644 --- a/server_addon/max/server/settings/publishers.py +++ b/server_addon/max/server/settings/publishers.py @@ -29,7 +29,10 @@ class ValidateAttributesModel(BaseSettingsModel): class FamilyMappingItemModel(BaseSettingsModel): _layout = "compact" - name: str = Field("", title="Product type") + product_types: list[str] = Field( + default_factory=list, + title="Product Types" + ) plugins: list[str] = Field( default_factory=list, title="Plugins" @@ -44,12 +47,6 @@ class ValidateLoadedPluginModel(BaseSettingsModel): title="Family Plugins Mapping" ) - # This is to validate unique names (like in dict) - @validator("family_plugins_mapping") - def validate_unique_outputs(cls, value): - ensure_unique_names(value) - return value - class BasicValidateModel(BaseSettingsModel): enabled: bool = Field(title="Enabled") From e0fba84c9293fa6fe7d9ddb5aa5e1783faaa56d7 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 9 Nov 2023 21:17:42 +0800 Subject: [PATCH 0813/1233] add supports for ayon settings --- server_addon/max/server/settings/publishers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/max/server/settings/publishers.py b/server_addon/max/server/settings/publishers.py index eeb6478216..4b6429250f 100644 --- a/server_addon/max/server/settings/publishers.py +++ b/server_addon/max/server/settings/publishers.py @@ -1,7 +1,7 @@ import json from pydantic import Field, validator -from ayon_server.settings import BaseSettingsModel, ensure_unique_names +from ayon_server.settings import BaseSettingsModel from ayon_server.exceptions import BadRequestException From 25be7762f18518ffacc3f0ac511f647025b6fbb0 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 9 Nov 2023 21:19:14 +0800 Subject: [PATCH 0814/1233] hound --- .../max/plugins/publish/validate_loaded_plugin.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py index 7450c8f971..ea2fee353d 100644 --- a/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py +++ b/openpype/hosts/max/plugins/publish/validate_loaded_plugin.py @@ -39,7 +39,7 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, instance_families = {instance.data["family"]} instance_families.update(instance.data.get("families", [])) cls.log.debug("Checking plug-in validation " - f"for instance families: {instance_families}") + f"for instance families: {instance_families}") all_required_plugins = set() for mapping in family_plugins_mapping: @@ -47,14 +47,16 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, if not mapping: return - match_families = {fam for fam in mapping["families"] if fam.strip()} + match_families = {fam for fam in mapping["families"] + if fam.strip()} has_match = "*" in match_families or match_families.intersection( instance_families) if not has_match: continue - cls.log.debug(f"Found plug-in family requirements: {match_families}") + cls.log.debug( + f"Found plug-in family requirements: {match_families}") required_plugins = [ # match lowercase and format with os.environ to allow # plugin names defined by max version, e.g. {3DSMAX_VERSION} @@ -124,7 +126,8 @@ class ValidateLoadedPlugin(OptionalPyblishPluginMixin, plugin_index = available_plugins.get(invalid_plugin) if plugin_index is None: - cls.log.warning(f"Can't enable missing plugin: {invalid_plugin}") + cls.log.warning( + f"Can't enable missing plugin: {invalid_plugin}") continue if not rt.pluginManager.isPluginDllLoaded(plugin_index): From 1e0f44923909de38a76b58e59588d3c417240a7f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 9 Nov 2023 14:32:57 +0100 Subject: [PATCH 0815/1233] implemented 'get_ayon_server_api_connection' to help create connection --- openpype/client/__init__.py | 3 +++ openpype/client/server/utils.py | 25 +++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/openpype/client/__init__.py b/openpype/client/__init__.py index 7831afd8ad..fe6dc97877 100644 --- a/openpype/client/__init__.py +++ b/openpype/client/__init__.py @@ -1,6 +1,7 @@ from .mongo import ( OpenPypeMongoConnection, ) +from .server.utils import get_ayon_server_api_connection from .entities import ( get_projects, @@ -59,6 +60,8 @@ from .operations import ( __all__ = ( "OpenPypeMongoConnection", + "get_ayon_server_api_connection", + "get_projects", "get_project", "get_whole_project", diff --git a/openpype/client/server/utils.py b/openpype/client/server/utils.py index ed128cfad9..a9dcf539bd 100644 --- a/openpype/client/server/utils.py +++ b/openpype/client/server/utils.py @@ -1,8 +1,33 @@ +import os import uuid +import ayon_api + from openpype.client.operations_base import REMOVED_VALUE +class _GlobalCache: + initialized = False + + +def get_ayon_server_api_connection(): + if _GlobalCache.initialized: + con = ayon_api.get_server_api_connection() + else: + from openpype.lib.local_settings import get_local_site_id + + _GlobalCache.initialized = True + site_id = get_local_site_id() + version = os.getenv("AYON_VERSION") + if ayon_api.is_connection_created(): + con = ayon_api.get_server_api_connection() + con.set_site_id(site_id) + con.set_client_version(version) + else: + con = ayon_api.create_connection(site_id, version) + return con + + def create_entity_id(): return uuid.uuid1().hex From 8e0513fe548b15aed1edcda6f83a38c919758241 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 9 Nov 2023 14:33:19 +0100 Subject: [PATCH 0816/1233] use 'get_ayon_server_api_connection' in server functions --- openpype/client/server/entities.py | 27 +++++++++++++------------- openpype/client/server/entity_links.py | 14 ++++++------- openpype/client/server/operations.py | 11 +++++------ 3 files changed, 25 insertions(+), 27 deletions(-) diff --git a/openpype/client/server/entities.py b/openpype/client/server/entities.py index 16223d3d91..b41727a797 100644 --- a/openpype/client/server/entities.py +++ b/openpype/client/server/entities.py @@ -1,9 +1,8 @@ import collections -from ayon_api import get_server_api_connection - from openpype.client.mongo.operations import CURRENT_THUMBNAIL_SCHEMA +from .utils import get_ayon_server_api_connection from .openpype_comp import get_folders_with_tasks from .conversion_utils import ( project_fields_v3_to_v4, @@ -37,7 +36,7 @@ def get_projects(active=True, inactive=False, library=None, fields=None): elif inactive: active = False - con = get_server_api_connection() + con = get_ayon_server_api_connection() fields = project_fields_v3_to_v4(fields, con) for project in con.get_projects(active, library, fields=fields): yield convert_v4_project_to_v3(project) @@ -45,7 +44,7 @@ def get_projects(active=True, inactive=False, library=None, fields=None): def get_project(project_name, active=True, inactive=False, fields=None): # Skip if both are disabled - con = get_server_api_connection() + con = get_ayon_server_api_connection() fields = project_fields_v3_to_v4(fields, con) return convert_v4_project_to_v3( con.get_project(project_name, fields=fields) @@ -66,7 +65,7 @@ def _get_subsets( fields=None ): # Convert fields and add minimum required fields - con = get_server_api_connection() + con = get_ayon_server_api_connection() fields = subset_fields_v3_to_v4(fields, con) if fields is not None: for key in ( @@ -102,7 +101,7 @@ def _get_versions( active=None, fields=None ): - con = get_server_api_connection() + con = get_ayon_server_api_connection() fields = version_fields_v3_to_v4(fields, con) @@ -198,7 +197,7 @@ def get_assets( if archived: active = None - con = get_server_api_connection() + con = get_ayon_server_api_connection() fields = folder_fields_v3_to_v4(fields, con) kwargs = dict( folder_ids=asset_ids, @@ -236,7 +235,7 @@ def get_archived_assets( def get_asset_ids_with_subsets(project_name, asset_ids=None): - con = get_server_api_connection() + con = get_ayon_server_api_connection() return con.get_folder_ids_with_products(project_name, asset_ids) @@ -282,7 +281,7 @@ def get_subsets( def get_subset_families(project_name, subset_ids=None): - con = get_server_api_connection() + con = get_ayon_server_api_connection() return con.get_product_type_names(project_name, subset_ids) @@ -430,7 +429,7 @@ def get_output_link_versions(project_name, version_id, fields=None): if not version_id: return [] - con = get_server_api_connection() + con = get_ayon_server_api_connection() version_links = con.get_version_links( project_name, version_id, link_direction="out") @@ -446,7 +445,7 @@ def get_output_link_versions(project_name, version_id, fields=None): def version_is_latest(project_name, version_id): - con = get_server_api_connection() + con = get_ayon_server_api_connection() return con.version_is_latest(project_name, version_id) @@ -501,7 +500,7 @@ def get_representations( else: active = None - con = get_server_api_connection() + con = get_ayon_server_api_connection() fields = representation_fields_v3_to_v4(fields, con) if fields and active is not None: fields.add("active") @@ -535,7 +534,7 @@ def get_representations_parents(project_name, representations): repre["_id"] for repre in representations } - con = get_server_api_connection() + con = get_ayon_server_api_connection() parents_by_repre_id = con.get_representations_parents(project_name, repre_ids) folder_ids = set() @@ -677,7 +676,7 @@ def get_workfile_info( if not asset_id or not task_name or not filename: return None - con = get_server_api_connection() + con = get_ayon_server_api_connection() task = con.get_task_by_name( project_name, asset_id, task_name, fields=["id", "name", "folderId"] ) diff --git a/openpype/client/server/entity_links.py b/openpype/client/server/entity_links.py index d8395aabe7..368dcdcb9d 100644 --- a/openpype/client/server/entity_links.py +++ b/openpype/client/server/entity_links.py @@ -1,6 +1,4 @@ -import ayon_api -from ayon_api import get_folder_links, get_versions_links - +from .utils import get_ayon_server_api_connection from .entities import get_assets, get_representation_by_id @@ -28,7 +26,8 @@ def get_linked_asset_ids(project_name, asset_doc=None, asset_id=None): if not asset_id: asset_id = asset_doc["_id"] - links = get_folder_links(project_name, asset_id, link_direction="in") + con = get_ayon_server_api_connection() + links = con.get_folder_links(project_name, asset_id, link_direction="in") return [ link["entityId"] for link in links @@ -115,6 +114,7 @@ def get_linked_representation_id( if link_type: link_types = [link_type] + con = get_ayon_server_api_connection() # Store already found version ids to avoid recursion, and also to store # output -> Don't forget to remove 'version_id' at the end!!! linked_version_ids = {version_id} @@ -124,7 +124,7 @@ def get_linked_representation_id( if not versions_to_check: break - links = get_versions_links( + links = con.get_versions_links( project_name, versions_to_check, link_types=link_types, @@ -145,8 +145,8 @@ def get_linked_representation_id( linked_version_ids.remove(version_id) if not linked_version_ids: return [] - - representations = ayon_api.get_representations( + con = get_ayon_server_api_connection() + representations = con.get_representations( project_name, version_ids=linked_version_ids, fields=["id"]) diff --git a/openpype/client/server/operations.py b/openpype/client/server/operations.py index 5b38405c34..eddc1eaf60 100644 --- a/openpype/client/server/operations.py +++ b/openpype/client/server/operations.py @@ -5,7 +5,6 @@ import uuid import datetime from bson.objectid import ObjectId -from ayon_api import get_server_api_connection from openpype.client.operations_base import ( REMOVED_VALUE, @@ -41,7 +40,7 @@ from .conversion_utils import ( convert_update_representation_to_v4, convert_update_workfile_info_to_v4, ) -from .utils import create_entity_id +from .utils import create_entity_id, get_ayon_server_api_connection def _create_or_convert_to_id(entity_id=None): @@ -680,7 +679,7 @@ class OperationsSession(BaseOperationsSession): def __init__(self, con=None, *args, **kwargs): super(OperationsSession, self).__init__(*args, **kwargs) if con is None: - con = get_server_api_connection() + con = get_ayon_server_api_connection() self._con = con self._project_cache = {} self._nested_operations = collections.defaultdict(list) @@ -858,7 +857,7 @@ def create_project( """ if con is None: - con = get_server_api_connection() + con = get_ayon_server_api_connection() return con.create_project( project_name, @@ -870,12 +869,12 @@ def create_project( def delete_project(project_name, con=None): if con is None: - con = get_server_api_connection() + con = get_ayon_server_api_connection() return con.delete_project(project_name) def create_thumbnail(project_name, src_filepath, thumbnail_id=None, con=None): if con is None: - con = get_server_api_connection() + con = get_ayon_server_api_connection() return con.create_thumbnail(project_name, src_filepath, thumbnail_id) From 14ddd5cb51e8785f27b5be5406b358792bbe6328 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 9 Nov 2023 14:36:42 +0100 Subject: [PATCH 0817/1233] use 'get_ayon_server_api_connection' in core functions --- openpype/lib/local_settings.py | 6 +++--- openpype/modules/base.py | 10 +++++++--- openpype/pipeline/anatomy.py | 6 +++--- openpype/pipeline/context_tools.py | 6 ++++++ openpype/pipeline/thumbnail.py | 6 ++---- openpype/settings/ayon_settings.py | 28 +++++++++++++++++++--------- 6 files changed, 40 insertions(+), 22 deletions(-) diff --git a/openpype/lib/local_settings.py b/openpype/lib/local_settings.py index 9b780fd88a..ea42d2f0b5 100644 --- a/openpype/lib/local_settings.py +++ b/openpype/lib/local_settings.py @@ -36,6 +36,7 @@ from openpype.settings import ( ) from openpype.client.mongo import validate_mongo_connection +from openpype.client import get_ayon_server_api_connection _PLACEHOLDER = object() @@ -613,9 +614,8 @@ def get_openpype_username(): """ if AYON_SERVER_ENABLED: - import ayon_api - - return ayon_api.get_user()["name"] + con = get_ayon_server_api_connection() + return con.get_user()["name"] username = os.environ.get("OPENPYPE_USERNAME") if not username: diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 457e29905d..4636906cec 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -16,9 +16,9 @@ from abc import ABCMeta, abstractmethod import six import appdirs -import ayon_api from openpype import AYON_SERVER_ENABLED +from openpype.client import get_ayon_server_api_connection from openpype.settings import ( get_system_settings, SYSTEM_SETTINGS_KEY, @@ -319,8 +319,11 @@ def load_modules(force=False): def _get_ayon_bundle_data(): + con = get_ayon_server_api_connection() + bundles = con.get_bundles()["bundles"] + bundle_name = os.getenv("AYON_BUNDLE_NAME") - bundles = ayon_api.get_bundles()["bundles"] + return next( ( bundle @@ -345,7 +348,8 @@ def _get_ayon_addons_information(bundle_info): output = [] bundle_addons = bundle_info["addons"] - addons = ayon_api.get_addons_info()["addons"] + con = get_ayon_server_api_connection() + addons = con.get_addons_info()["addons"] for addon in addons: name = addon["name"] versions = addon.get("versions") diff --git a/openpype/pipeline/anatomy.py b/openpype/pipeline/anatomy.py index 029b5cc1ff..0e5ab1d42e 100644 --- a/openpype/pipeline/anatomy.py +++ b/openpype/pipeline/anatomy.py @@ -5,7 +5,6 @@ import platform import collections import numbers -import ayon_api import six import time @@ -16,7 +15,7 @@ from openpype.settings.lib import ( from openpype.settings.constants import ( DEFAULT_PROJECT_KEY ) -from openpype.client import get_project +from openpype.client import get_project, get_ayon_server_api_connection from openpype.lib import Logger, get_local_site_id from openpype.lib.path_templates import ( TemplateUnsolved, @@ -479,7 +478,8 @@ class Anatomy(BaseAnatomy): if AYON_SERVER_ENABLED: if not project_name: return - return ayon_api.get_project_roots_for_site( + con = get_ayon_server_api_connection() + return con.get_project_roots_for_site( project_name, get_local_site_id() ) diff --git a/openpype/pipeline/context_tools.py b/openpype/pipeline/context_tools.py index 5afdb30f7b..034bbc0070 100644 --- a/openpype/pipeline/context_tools.py +++ b/openpype/pipeline/context_tools.py @@ -11,12 +11,14 @@ import pyblish.api from pyblish.lib import MessageHandler import openpype +from openpype import AYON_SERVER_ENABLED from openpype.host import HostBase from openpype.client import ( get_project, get_asset_by_id, get_asset_by_name, version_is_latest, + get_ayon_server_api_connection, ) from openpype.lib.events import emit_event from openpype.modules import load_modules, ModulesManager @@ -105,6 +107,10 @@ def install_host(host): _is_installed = True + # Make sure global AYON connection has set site id and version + if AYON_SERVER_ENABLED: + get_ayon_server_api_connection() + legacy_io.install() modules_manager = _get_modules_manager() diff --git a/openpype/pipeline/thumbnail.py b/openpype/pipeline/thumbnail.py index 63c55d0c19..14fb8b06fc 100644 --- a/openpype/pipeline/thumbnail.py +++ b/openpype/pipeline/thumbnail.py @@ -4,7 +4,7 @@ import logging from openpype import AYON_SERVER_ENABLED from openpype.lib import Logger -from openpype.client import get_project +from openpype.client import get_project, get_ayon_server_api_connection from . import legacy_io from .anatomy import Anatomy from .plugin_discover import ( @@ -153,8 +153,6 @@ class ServerThumbnailResolver(ThumbnailResolver): if not entity_type or not entity_id: return None - import ayon_api - project_name = self.dbcon.active_project() thumbnail_id = thumbnail_entity["_id"] @@ -169,7 +167,7 @@ class ServerThumbnailResolver(ThumbnailResolver): # NOTE Use 'get_server_api_connection' because public function # 'get_thumbnail_by_id' does not return output of 'ServerAPI' # method. - con = ayon_api.get_server_api_connection() + con = get_ayon_server_api_connection() if hasattr(con, "get_thumbnail_by_id"): result = con.get_thumbnail_by_id(thumbnail_id) if result.is_valid: diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index 67ef109d8b..745cadfc6e 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -20,7 +20,8 @@ import copy import time import six -import ayon_api + +from openpype.client import get_ayon_server_api_connection def _convert_color(color_value): @@ -1445,7 +1446,8 @@ class _AyonSettingsCache: @classmethod def _use_bundles(cls): if _AyonSettingsCache.use_bundles is None: - major, minor, _, _, _ = ayon_api.get_server_version_tuple() + con = get_ayon_server_api_connection() + major, minor, _, _, _ = con.get_server_version_tuple() use_bundles = True if (major, minor) < (0, 3): use_bundles = False @@ -1462,6 +1464,8 @@ class _AyonSettingsCache: variant = cls._get_dev_mode_settings_variant() elif is_staging_enabled(): variant = "staging" + + # Cache variant _AyonSettingsCache.variant = variant return _AyonSettingsCache.variant @@ -1477,8 +1481,9 @@ class _AyonSettingsCache: str: Name of settings variant. """ - bundles = ayon_api.get_bundles() - user = ayon_api.get_user() + con = get_ayon_server_api_connection() + bundles = con.get_bundles() + user = con.get_user() username = user["name"] for bundle in bundles["bundles"]: if ( @@ -1494,21 +1499,23 @@ class _AyonSettingsCache: def get_value_by_project(cls, project_name): cache_item = _AyonSettingsCache.cache_by_project_name[project_name] if cache_item.is_outdated: + con = get_ayon_server_api_connection() if cls._use_bundles(): - value = ayon_api.get_addons_settings( + value = con.get_addons_settings( bundle_name=cls._get_bundle_name(), project_name=project_name, variant=cls._get_variant() ) else: - value = ayon_api.get_addons_settings(project_name) + value = con.get_addons_settings(project_name) cache_item.update_value(value) return cache_item.get_value() @classmethod def _get_addon_versions_from_bundle(cls): + con = get_ayon_server_api_connection() expected_bundle = cls._get_bundle_name() - bundles = ayon_api.get_bundles()["bundles"] + bundles = con.get_bundles()["bundles"] bundle = next( ( bundle @@ -1528,8 +1535,11 @@ class _AyonSettingsCache: if cls._use_bundles(): addons = cls._get_addon_versions_from_bundle() else: - settings_data = ayon_api.get_addons_settings( - only_values=False, variant=cls._get_variant()) + con = get_ayon_server_api_connection() + settings_data = con.get_addons_settings( + only_values=False, + variant=cls._get_variant() + ) addons = settings_data["versions"] cache_item.update_value(addons) From e23a2b5e09dfb13c5ed1587a450bd5de89b7ef35 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 9 Nov 2023 14:36:55 +0100 Subject: [PATCH 0818/1233] set default variant in ayon_settings --- openpype/settings/ayon_settings.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index 745cadfc6e..f0b4528802 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -1467,6 +1467,10 @@ class _AyonSettingsCache: # Cache variant _AyonSettingsCache.variant = variant + + # Set the variant to global ayon api connection + con = get_ayon_server_api_connection() + con.set_default_settings_variant(variant) return _AyonSettingsCache.variant @classmethod From 6fa92ebc14204c3c723a0b2a1d42ba492d10ff30 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 9 Nov 2023 14:40:33 +0100 Subject: [PATCH 0819/1233] resolve: clip data should be integer rather then timecode --- openpype/hosts/resolve/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index aef9caca78..3866477c77 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -298,7 +298,7 @@ def create_timeline_item( if source_end: clip_data["endFrame"] = source_end if timecode_in: - clip_data["recordFrame"] = timecode_in + clip_data["recordFrame"] = timeline_in # add to timeline media_pool.AppendToTimeline([clip_data]) From 85bad0ae3d6f43732b9087583af2e09de55fed40 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 9 Nov 2023 14:42:30 +0100 Subject: [PATCH 0820/1233] hound --- openpype/hosts/resolve/api/plugin.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py index 886615c7aa..7cf5913b67 100644 --- a/openpype/hosts/resolve/api/plugin.py +++ b/openpype/hosts/resolve/api/plugin.py @@ -440,8 +440,8 @@ class ClipLoader: if ( frame_start is None or frame_end is None or - handle_start is 0 or - handle_end is 0 + handle_start == 0 or + handle_end == 0 ): # if not then rather assume that source has no handles source_with_handles = False @@ -473,8 +473,8 @@ class ClipLoader: # only exclude handles if source has no handles or # if user wants to load without handles if ( - not self.with_handles # set by user - or not source_with_handles # result of source duration check + not self.with_handles # set by user + or not source_with_handles # result of source duration check ): source_in += handle_start source_out -= handle_end From 0b8dde268708594e23d1f305a7bfbd3da99a55d9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 9 Nov 2023 15:47:14 +0100 Subject: [PATCH 0821/1233] resolve: creating clips with folder path also converting dict() to {} --- openpype/hosts/resolve/api/plugin.py | 29 +++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py index 49c3b484d2..197f288150 100644 --- a/openpype/hosts/resolve/api/plugin.py +++ b/openpype/hosts/resolve/api/plugin.py @@ -19,7 +19,7 @@ from .menu import load_stylesheet class CreatorWidget(QtWidgets.QDialog): # output items - items = dict() + items = {} def __init__(self, name, info, ui_inputs, parent=None): super(CreatorWidget, self).__init__(parent) @@ -101,7 +101,7 @@ class CreatorWidget(QtWidgets.QDialog): self.close() def value(self, data, new_data=None): - new_data = new_data or dict() + new_data = new_data or {} for k, v in data.items(): new_data[k] = { "target": None, @@ -290,7 +290,7 @@ class Spacer(QtWidgets.QWidget): class ClipLoader: active_bin = None - data = dict() + data = {} def __init__(self, loader_obj, context, **options): """ Initialize object @@ -588,8 +588,8 @@ class PublishClip: Returns: hiero.core.TrackItem: hiero track item object with openpype tag """ - vertical_clip_match = dict() - tag_data = dict() + vertical_clip_match = {} + tag_data = {} types = { "shot": "shot", "folder": "folder", @@ -665,15 +665,23 @@ class PublishClip: new_name = self.tag_data.pop("newClipName") if self.rename: - self.tag_data["asset"] = new_name + self.tag_data["asset_name"] = new_name else: - self.tag_data["asset"] = self.ti_name + self.tag_data["asset_name"] = self.ti_name + # AYON unique identifier + folder_path = "/{}/{}".format( + self.tag_data["hierarchy"], + self.tag_data["asset_name"] + ) + self.tag_data["folder_path"] = folder_path + + # create new name for track item if not lib.pype_marker_workflow: # create compound clip workflow lib.create_compound_clip( self.timeline_item_data, - self.tag_data["asset"], + self.tag_data["asset_name"], self.mp_folder ) @@ -765,7 +773,7 @@ class PublishClip: # increasing steps by index of rename iteration self.count_steps *= self.rename_index - hierarchy_formatting_data = dict() + hierarchy_formatting_data = {} _data = self.timeline_item_default_data.copy() if self.ui_inputs: # adding tag metadata from ui @@ -854,8 +862,7 @@ class PublishClip: "parents": self.parents, "hierarchyData": hierarchy_formatting_data, "subset": self.subset, - "family": self.subset_family, - "families": ["clip"] + "family": self.subset_family } def _convert_to_entity(self, key): From d07898a3286b139c0d48bc14fb16b1417adcd8e5 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 9 Nov 2023 15:47:50 +0100 Subject: [PATCH 0822/1233] resolve: collect instances with folder path --- .../plugins/publish/precollect_instances.py | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/resolve/plugins/publish/precollect_instances.py b/openpype/hosts/resolve/plugins/publish/precollect_instances.py index 8ec169ad65..58c1c85276 100644 --- a/openpype/hosts/resolve/plugins/publish/precollect_instances.py +++ b/openpype/hosts/resolve/plugins/publish/precollect_instances.py @@ -9,6 +9,7 @@ from openpype.hosts.resolve.api.lib import ( get_publish_attribute, get_otio_clip_instance_data, ) +from openpype import AYON_SERVER_ENABLED class PrecollectInstances(pyblish.api.ContextPlugin): @@ -29,7 +30,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin): for timeline_item_data in selected_timeline_items: - data = dict() + data = {} timeline_item = timeline_item_data["clip"]["item"] # get pype tag data @@ -60,24 +61,25 @@ class PrecollectInstances(pyblish.api.ContextPlugin): if k not in ("id", "applieswhole", "label") }) - asset = tag_data["asset"] + if AYON_SERVER_ENABLED: + asset = tag_data["folder_path"] + else: + asset = tag_data["asset_name"] + subset = tag_data["subset"] - # insert family into families - family = tag_data["family"] - families = [str(f) for f in tag_data["families"]] - families.insert(0, str(family)) - data.update({ - "name": "{} {} {}".format(asset, subset, families), + "name": "{}_{}".format(asset, subset), + "label": "{} {}".format(asset, subset), "asset": asset, "item": timeline_item, - "families": families, "publish": get_publish_attribute(timeline_item), "fps": context.data["fps"], "handleStart": handle_start, "handleEnd": handle_end, - "newAssetPublishing": True + "newAssetPublishing": True, + "families": ["clip"], + "isEditorial": True }) # otio clip data @@ -135,7 +137,8 @@ class PrecollectInstances(pyblish.api.ContextPlugin): family = "shot" data.update({ - "name": "{} {} {}".format(asset, subset, family), + "name": "{}_{}".format(asset, subset), + "label": "{} {}".format(asset, subset), "subset": subset, "asset": asset, "family": family, From 7cd98fe9033c17b4897208ab7c54bda5e7e53c45 Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Thu, 9 Nov 2023 15:52:36 +0100 Subject: [PATCH 0823/1233] Remove deprecated code, use avalon instances in basecreator --- openpype/hosts/blender/api/plugin.py | 25 +++++----- .../blender/plugins/create/create_action.py | 25 ---------- .../plugins/create/create_animation.py | 37 -------------- .../blender/plugins/create/create_camera.py | 49 ------------------- .../blender/plugins/create/create_layout.py | 37 -------------- .../blender/plugins/create/create_model.py | 37 -------------- .../plugins/create/create_pointcache.py | 36 -------------- .../blender/plugins/create/create_review.py | 33 ------------- .../blender/plugins/create/create_rig.py | 37 -------------- 9 files changed, 11 insertions(+), 305 deletions(-) diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index 3ddc375670..629cb4dac9 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -144,18 +144,6 @@ class BaseCreator(Creator): """Base class for Blender Creator plug-ins.""" defaults = ['Main'] - # Deprecated? - def process(self): - collection = bpy.data.collections.new(name=self.data["subset"]) - bpy.context.scene.collection.children.link(collection) - imprint(collection, self.data) - - if (self.options or {}).get("useSelection"): - for obj in get_selection(): - collection.objects.link(obj) - - return collection - @staticmethod def cache_subsets(shared_data): """Cache instances for Creators shared data. @@ -233,8 +221,17 @@ class BaseCreator(Creator): pre_create_data(dict): Data based on pre creation attributes. Those may affect how creator works. """ - collection = bpy.data.collections.new(name=subset_name) - bpy.context.scene.collection.children.link(collection) + # Get Instance Container or create it if it does not exist + instances = bpy.data.collections.get(AVALON_INSTANCES) + if not instances: + instances = bpy.data.collections.new(name=AVALON_INSTANCES) + bpy.context.scene.collection.children.link(instances) + + # Create instance collection + collection = bpy.data.collections.new( + name=asset_name(instance_data["asset"], subset_name) + ) + instances.children.link(collection) collection[AVALON_PROPERTY] = instance_node = { "name": collection.name, diff --git a/openpype/hosts/blender/plugins/create/create_action.py b/openpype/hosts/blender/plugins/create/create_action.py index 7d00aa1dcb..9267fc0765 100644 --- a/openpype/hosts/blender/plugins/create/create_action.py +++ b/openpype/hosts/blender/plugins/create/create_action.py @@ -59,28 +59,3 @@ class CreateAction(openpype.hosts.blender.api.plugin.BaseCreator): collection.objects.link(empty_obj) return collection - - # Deprecated - def process(self): - - asset = self.data["asset"] - subset = self.data["subset"] - name = openpype.hosts.blender.api.plugin.asset_name(asset, subset) - collection = bpy.data.collections.new(name=name) - bpy.context.scene.collection.children.link(collection) - self.data['task'] = get_current_task_name() - lib.imprint(collection, self.data) - - if (self.options or {}).get("useSelection"): - for obj in lib.get_selection(): - if (obj.animation_data is not None - and obj.animation_data.action is not None): - - empty_obj = bpy.data.objects.new(name=name, - object_data=None) - empty_obj.animation_data_create() - empty_obj.animation_data.action = obj.animation_data.action - empty_obj.animation_data.action.name = name - collection.objects.link(empty_obj) - - return collection diff --git a/openpype/hosts/blender/plugins/create/create_animation.py b/openpype/hosts/blender/plugins/create/create_animation.py index 6cfd054e74..89567061b6 100644 --- a/openpype/hosts/blender/plugins/create/create_animation.py +++ b/openpype/hosts/blender/plugins/create/create_animation.py @@ -75,40 +75,3 @@ class CreateAnimation(plugin.BaseCreator): asset_group.objects.link(obj) return asset_group - - # Deprecated - def process(self): - """ Run the creator on Blender main thread""" - mti = ops.MainThreadItem(self._process_legacy) - ops.execute_in_main_thread(mti) - - # Deprecated - def _process_legacy(self): - # Get Instance Container or create it if it does not exist - instances = bpy.data.collections.get(AVALON_INSTANCES) - if not instances: - instances = bpy.data.collections.new(name=AVALON_INSTANCES) - bpy.context.scene.collection.children.link(instances) - - # Create instance object - # name = self.name - # if not name: - asset = self.data["asset"] - subset = self.data["subset"] - name = plugin.asset_name(asset, subset) - # asset_group = bpy.data.objects.new(name=name, object_data=None) - # asset_group.empty_display_type = 'SINGLE_ARROW' - asset_group = bpy.data.collections.new(name=name) - instances.children.link(asset_group) - self.data['task'] = get_current_task_name() - lib.imprint(asset_group, self.data) - - if (self.options or {}).get("useSelection"): - selected = lib.get_selection() - for obj in selected: - asset_group.objects.link(obj) - elif (self.options or {}).get("asset_group"): - obj = (self.options or {}).get("asset_group") - asset_group.objects.link(obj) - - return asset_group diff --git a/openpype/hosts/blender/plugins/create/create_camera.py b/openpype/hosts/blender/plugins/create/create_camera.py index 5d9682e575..125514ae01 100644 --- a/openpype/hosts/blender/plugins/create/create_camera.py +++ b/openpype/hosts/blender/plugins/create/create_camera.py @@ -84,52 +84,3 @@ class CreateCamera(plugin.BaseCreator): bpy.ops.object.parent_set(keep_transform=True) return asset_group - - # Deprecated - def process(self): - """ Run the creator on Blender main thread""" - mti = ops.MainThreadItem(self._process_legacy) - ops.execute_in_main_thread(mti) - - # Deprecated - def _process_legacy(self): - # Get Instance Container or create it if it does not exist - instances = bpy.data.collections.get(AVALON_INSTANCES) - if not instances: - instances = bpy.data.collections.new(name=AVALON_INSTANCES) - bpy.context.scene.collection.children.link(instances) - - # Create instance object - asset = self.data["asset"] - subset = self.data["subset"] - name = plugin.asset_name(asset, subset) - - asset_group = bpy.data.objects.new(name=name, object_data=None) - asset_group.empty_display_type = 'SINGLE_ARROW' - instances.objects.link(asset_group) - self.data['task'] = get_current_task_name() - print(f"self.data: {self.data}") - lib.imprint(asset_group, self.data) - - if (self.options or {}).get("useSelection"): - bpy.context.view_layer.objects.active = asset_group - selected = lib.get_selection() - for obj in selected: - if obj.parent in selected: - obj.select_set(False) - continue - selected.append(asset_group) - bpy.ops.object.parent_set(keep_transform=True) - else: - plugin.deselect_all() - camera = bpy.data.cameras.new(subset) - camera_obj = bpy.data.objects.new(subset, camera) - - instances.objects.link(camera_obj) - - camera_obj.select_set(True) - asset_group.select_set(True) - bpy.context.view_layer.objects.active = asset_group - bpy.ops.object.parent_set(keep_transform=True) - - return asset_group diff --git a/openpype/hosts/blender/plugins/create/create_layout.py b/openpype/hosts/blender/plugins/create/create_layout.py index ed47b0632f..a6c7053cb2 100644 --- a/openpype/hosts/blender/plugins/create/create_layout.py +++ b/openpype/hosts/blender/plugins/create/create_layout.py @@ -73,40 +73,3 @@ class CreateLayout(plugin.BaseCreator): bpy.ops.object.parent_set(keep_transform=True) return asset_group - - # Deprecated - def process(self): - """ Run the creator on Blender main thread""" - mti = ops.MainThreadItem(self._process_legacy) - ops.execute_in_main_thread(mti) - - # Deprecated - def _process_legacy(self): - # Get Instance Container or create it if it does not exist - instances = bpy.data.collections.get(AVALON_INSTANCES) - if not instances: - instances = bpy.data.collections.new(name=AVALON_INSTANCES) - bpy.context.scene.collection.children.link(instances) - - # Create instance object - asset = self.data["asset"] - subset = self.data["subset"] - name = plugin.asset_name(asset, subset) - asset_group = bpy.data.objects.new(name=name, object_data=None) - asset_group.empty_display_type = 'SINGLE_ARROW' - instances.objects.link(asset_group) - self.data['task'] = get_current_task_name() - lib.imprint(asset_group, self.data) - - # Add selected objects to instance - if (self.options or {}).get("useSelection"): - bpy.context.view_layer.objects.active = asset_group - selected = lib.get_selection() - for obj in selected: - if obj.parent in selected: - obj.select_set(False) - continue - selected.append(asset_group) - bpy.ops.object.parent_set(keep_transform=True) - - return asset_group diff --git a/openpype/hosts/blender/plugins/create/create_model.py b/openpype/hosts/blender/plugins/create/create_model.py index 949fae0f76..8beb8025c4 100644 --- a/openpype/hosts/blender/plugins/create/create_model.py +++ b/openpype/hosts/blender/plugins/create/create_model.py @@ -73,40 +73,3 @@ class CreateModel(plugin.BaseCreator): bpy.ops.object.parent_set(keep_transform=True) return asset_group - - # Deprecated - def process(self): - """ Run the creator on Blender main thread""" - mti = ops.MainThreadItem(self._process_legacy) - ops.execute_in_main_thread(mti) - - # Deprecated - def _process_legacy(self): - # Get Instance Container or create it if it does not exist - instances = bpy.data.collections.get(AVALON_INSTANCES) - if not instances: - instances = bpy.data.collections.new(name=AVALON_INSTANCES) - bpy.context.scene.collection.children.link(instances) - - # Create instance object - asset = self.data["asset"] - subset = self.data["subset"] - name = plugin.asset_name(asset, subset) - asset_group = bpy.data.objects.new(name=name, object_data=None) - asset_group.empty_display_type = 'SINGLE_ARROW' - instances.objects.link(asset_group) - self.data['task'] = get_current_task_name() - lib.imprint(asset_group, self.data) - - # Add selected objects to instance - if (self.options or {}).get("useSelection"): - bpy.context.view_layer.objects.active = asset_group - selected = lib.get_selection() - for obj in selected: - if obj.parent in selected: - obj.select_set(False) - continue - selected.append(asset_group) - bpy.ops.object.parent_set(keep_transform=True) - - return asset_group diff --git a/openpype/hosts/blender/plugins/create/create_pointcache.py b/openpype/hosts/blender/plugins/create/create_pointcache.py index 2ad12caa9c..aa8b297d16 100644 --- a/openpype/hosts/blender/plugins/create/create_pointcache.py +++ b/openpype/hosts/blender/plugins/create/create_pointcache.py @@ -54,39 +54,3 @@ class CreatePointcache(plugin.BaseCreator): objects.extend(obj.children) return collection - - # Deprecated - def process(self): - """ Run the creator on Blender main thread""" - mti = ops.MainThreadItem(self._process) - ops.execute_in_main_thread(mti) - - def _process(self): - # Get Instance Container or create it if it does not exist - instances = bpy.data.collections.get(AVALON_INSTANCES) - if not instances: - instances = bpy.data.collections.new(name=AVALON_INSTANCES) - bpy.context.scene.collection.children.link(instances) - - # Create instance object - asset = self.data["asset"] - subset = self.data["subset"] - name = plugin.asset_name(asset, subset) - asset_group = bpy.data.objects.new(name=name, object_data=None) - asset_group.empty_display_type = 'SINGLE_ARROW' - instances.objects.link(asset_group) - self.data['task'] = get_current_task_name() - lib.imprint(asset_group, self.data) - - # Add selected objects to instance - if (self.options or {}).get("useSelection"): - bpy.context.view_layer.objects.active = asset_group - selected = lib.get_selection() - for obj in selected: - if obj.parent in selected: - obj.select_set(False) - continue - selected.append(asset_group) - bpy.ops.object.parent_set(keep_transform=True) - - return asset_group diff --git a/openpype/hosts/blender/plugins/create/create_review.py b/openpype/hosts/blender/plugins/create/create_review.py index e8b893b4c0..13fa3b621f 100644 --- a/openpype/hosts/blender/plugins/create/create_review.py +++ b/openpype/hosts/blender/plugins/create/create_review.py @@ -71,36 +71,3 @@ class CreateReview(plugin.BaseCreator): asset_group.objects.link(obj) return asset_group - - # Deprecated - def process(self): - """ Run the creator on Blender main thread""" - mti = ops.MainThreadItem(self._process_legacy) - ops.execute_in_main_thread(mti) - - # Deprecated - def _process_legacy(self): - # Get Instance Container or create it if it does not exist - instances = bpy.data.collections.get(AVALON_INSTANCES) - if not instances: - instances = bpy.data.collections.new(name=AVALON_INSTANCES) - bpy.context.scene.collection.children.link(instances) - - # Create instance object - asset = self.data["asset"] - subset = self.data["subset"] - name = plugin.asset_name(asset, subset) - asset_group = bpy.data.collections.new(name=name) - instances.children.link(asset_group) - self.data['task'] = get_current_task_name() - lib.imprint(asset_group, self.data) - - if (self.options or {}).get("useSelection"): - selected = lib.get_selection() - for obj in selected: - asset_group.objects.link(obj) - elif (self.options or {}).get("asset_group"): - obj = (self.options or {}).get("asset_group") - asset_group.objects.link(obj) - - return asset_group diff --git a/openpype/hosts/blender/plugins/create/create_rig.py b/openpype/hosts/blender/plugins/create/create_rig.py index 6223e64174..6682162f4b 100644 --- a/openpype/hosts/blender/plugins/create/create_rig.py +++ b/openpype/hosts/blender/plugins/create/create_rig.py @@ -73,40 +73,3 @@ class CreateRig(plugin.BaseCreator): bpy.ops.object.parent_set(keep_transform=True) return asset_group - - # Deprecated - def process(self): - """ Run the creator on Blender main thread""" - mti = ops.MainThreadItem(self._process_legacy) - ops.execute_in_main_thread(mti) - - # Deprecated - def _process_legacy(self): - # Get Instance Container or create it if it does not exist - instances = bpy.data.collections.get(AVALON_INSTANCES) - if not instances: - instances = bpy.data.collections.new(name=AVALON_INSTANCES) - bpy.context.scene.collection.children.link(instances) - - # Create instance object - asset = self.data["asset"] - subset = self.data["subset"] - name = plugin.asset_name(asset, subset) - asset_group = bpy.data.objects.new(name=name, object_data=None) - asset_group.empty_display_type = 'SINGLE_ARROW' - instances.objects.link(asset_group) - self.data['task'] = get_current_task_name() - lib.imprint(asset_group, self.data) - - # Add selected objects to instance - if (self.options or {}).get("useSelection"): - bpy.context.view_layer.objects.active = asset_group - selected = lib.get_selection() - for obj in selected: - if obj.parent in selected: - obj.select_set(False) - continue - selected.append(asset_group) - bpy.ops.object.parent_set(keep_transform=True) - - return asset_group From 42c61fa348ca6036cef594f0f6fe6932b3c41b5a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 9 Nov 2023 16:22:17 +0100 Subject: [PATCH 0824/1233] do not check for "/" if asset name is empty --- openpype/pipeline/create/context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index e4dcedda2c..f32477dfb7 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -2283,7 +2283,7 @@ class CreateContext: if AYON_SERVER_ENABLED: asset_name = instance["folderPath"] - if "/" not in asset_name: + if asset_name and "/" not in asset_name: asset_docs = asset_docs_by_name.get(asset_name) if len(asset_docs) == 1: asset_name = get_asset_name_identifier(asset_docs[0]) From 573da36d4b12d58ab84110bbc46105be3f4e9e90 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 9 Nov 2023 16:32:20 +0100 Subject: [PATCH 0825/1233] workfile instance with support of folder path --- .../plugins/publish/extract_workfile.py | 1 + .../plugins/publish/precollect_workfile.py | 18 +++++++++--------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/resolve/plugins/publish/extract_workfile.py b/openpype/hosts/resolve/plugins/publish/extract_workfile.py index 535f879b58..db63487405 100644 --- a/openpype/hosts/resolve/plugins/publish/extract_workfile.py +++ b/openpype/hosts/resolve/plugins/publish/extract_workfile.py @@ -26,6 +26,7 @@ class ExtractWorkfile(publish.Extractor): resolve_workfile_ext = ".drp" drp_file_name = name + resolve_workfile_ext + drp_file_path = os.path.normpath( os.path.join(staging_dir, drp_file_name)) diff --git a/openpype/hosts/resolve/plugins/publish/precollect_workfile.py b/openpype/hosts/resolve/plugins/publish/precollect_workfile.py index 39c28e29f5..ccc5fd86ff 100644 --- a/openpype/hosts/resolve/plugins/publish/precollect_workfile.py +++ b/openpype/hosts/resolve/plugins/publish/precollect_workfile.py @@ -3,6 +3,7 @@ from pprint import pformat from openpype import AYON_SERVER_ENABLED from openpype.pipeline import get_current_asset_name + from openpype.hosts.resolve import api as rapi from openpype.hosts.resolve.otio import davinci_export @@ -14,14 +15,12 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin): order = pyblish.api.CollectorOrder - 0.5 def process(self, context): - current_asset = get_current_asset_name() - if AYON_SERVER_ENABLED: - # AYON compatibility split name and use last piece - asset_name = current_asset.split("/")[-1] - else: - asset_name = current_asset + current_asset_name = asset_name = get_current_asset_name() - subset = "workfile" + if AYON_SERVER_ENABLED: + asset_name = current_asset_name.split("/")[-1] + + subset = "workfileMain" project = rapi.get_current_project() fps = project.GetSetting("timelineFrameRate") video_tracks = rapi.get_video_track_names() @@ -31,8 +30,9 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin): instance_data = { "name": "{}_{}".format(asset_name, subset), - "asset": current_asset, - "subset": "{}{}".format(asset_name, subset.capitalize()), + "label": "{} {}".format(current_asset_name, subset), + "asset": current_asset_name, + "subset": subset, "item": project, "family": "workfile", "families": [] From a0da4cd17f2a0809eb2d4904a48ec8a5f5c04ab1 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 9 Nov 2023 15:35:58 +0000 Subject: [PATCH 0826/1233] Changed how we get instance group in the validator --- .../blender/plugins/publish/collect_instances.py | 4 ++-- .../plugins/publish/validate_instance_empty.py | 14 +++++--------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/collect_instances.py b/openpype/hosts/blender/plugins/publish/collect_instances.py index ad2ce54147..2d56e5fd7b 100644 --- a/openpype/hosts/blender/plugins/publish/collect_instances.py +++ b/openpype/hosts/blender/plugins/publish/collect_instances.py @@ -1,4 +1,3 @@ -import json from typing import Generator import bpy @@ -50,6 +49,7 @@ class CollectInstances(pyblish.api.ContextPlugin): for group in asset_groups: instance = self.create_instance(context, group) + instance.data["instance_group"] = group members = [] if isinstance(group, bpy.types.Collection): members = list(group.objects) @@ -65,6 +65,6 @@ class CollectInstances(pyblish.api.ContextPlugin): members.append(group) instance[:] = members - self.log.debug(json.dumps(instance.data, indent=4)) + self.log.debug(instance.data) for obj in instance: self.log.debug(obj) diff --git a/openpype/hosts/blender/plugins/publish/validate_instance_empty.py b/openpype/hosts/blender/plugins/publish/validate_instance_empty.py index 5abfd6dee8..3ebc6515d3 100644 --- a/openpype/hosts/blender/plugins/publish/validate_instance_empty.py +++ b/openpype/hosts/blender/plugins/publish/validate_instance_empty.py @@ -13,15 +13,11 @@ class ValidateInstanceEmpty(pyblish.api.InstancePlugin): optional = False def process(self, instance): - self.log.debug(instance) - self.log.debug(instance.data) - if instance.data["family"] == "blendScene": - # blendScene instances are collections - collection = bpy.data.collections[instance.name] - if not (collection.objects or collection.children): + asset_group = instance.data["instance_group"] + + if isinstance(asset_group, bpy.types.Collection): + if not (asset_group.objects or asset_group.children): raise RuntimeError(f"Instance {instance.name} is empty.") - else: - # All other instances are objects - asset_group = bpy.data.objects[instance.name] + elif isinstance(asset_group, bpy.types.Object): if not asset_group.children: raise RuntimeError(f"Instance {instance.name} is empty.") From 9607db50f2fce387860d94bf7563a3e841ac1089 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Thu, 9 Nov 2023 15:58:27 +0000 Subject: [PATCH 0827/1233] Testing: Ingest expected files and input workfile (#5840) * Ingest input workfile * Ingest input workfile * Ingested expected files, workfile Implemented LocalFileHandler. Test name added to structure to separate files for each test. Removed superfluous `files` to keep other Maya test working * Missing time import * Hound * Skip directories when checking folder structure. * Update tests/lib/testing_classes.py Co-authored-by: Roy Nieterau * Updated integration tests documentation * Ingested test files for Deadline test in maya * Removed unneeded files * Refactored name --------- Co-authored-by: kalisp Co-authored-by: Roy Nieterau --- tests/integration/README.md | 63 +- tests/integration/hosts/maya/lib.py | 31 +- .../maya/test_deadline_publish_in_maya.py | 3 +- .../test_deadline_publish_in_maya/README.md | 17 + ...test_project_test_asset_modelMain_hero.abc | Bin 0 -> 24787 bytes .../test_project_test_asset_modelMain_hero.ma | 1208 +++++++++++++++++ ...test_project_test_asset_modelMain_v001.abc | Bin 0 -> 24787 bytes .../test_project_test_asset_modelMain_v001.ma | 1208 +++++++++++++++++ ..._asset_renderTest_taskMain_beauty_v001.exr | Bin 0 -> 3608096 bytes ..._asset_renderTest_taskMain_beauty_v001.jpg | Bin 0 -> 77035 bytes ...et_renderTest_taskMain_beauty_v001_png.png | Bin 0 -> 563725 bytes ...oject_test_asset_workfileTest_task_v001.ma | 525 +++++++ .../test_project_test_asset_test_task_v001.ma | 525 +++++++ .../test_project_test_asset_test_task_v002.ma | 525 +++++++ .../test_asset/work/test_task/workspace.mel | 10 + .../dumps/avalon_tests/test_project.bson | Bin 0 -> 4899 bytes .../avalon_tests/test_project.metadata.json | 1 + .../input/dumps/openpype_tests/settings.bson | Bin 0 -> 914 bytes .../openpype_tests/settings.metadata.json | 1 + .../input/env_vars/env_var.json | 11 + .../input/startup/userSetup.py | 13 + .../test_project_test_asset_test_task_v001.ma | 525 +++++++ .../hosts/maya/test_publish_in_maya.py | 4 +- ...test_project_test_asset_modelMain_hero.abc | Bin 0 -> 24647 bytes .../test_project_test_asset_modelMain_hero.ma | 1186 ++++++++++++++++ ...test_project_test_asset_modelMain_v001.abc | Bin 0 -> 24647 bytes .../test_project_test_asset_modelMain_v001.ma | 1186 ++++++++++++++++ ...oject_test_asset_workfileTest_task_v001.ma | 476 +++++++ .../Main/renderMain_metadata.json | 275 ++++ .../test_project_test_asset_test_task_v001.ma | 476 +++++++ .../test_project_test_asset_test_task_v002.ma | 480 +++++++ .../test_asset/work/test_task/workspace.mel | 12 + .../dumps/avalon_tests/test_project.bson | Bin 0 -> 4899 bytes .../avalon_tests/test_project.metadata.json | 1 + .../input/dumps/openpype_tests/settings.bson | 0 .../openpype_tests/settings.metadata.json | 1 + .../input/env_vars/env_var.json | 11 + .../input/startup/userSetup.py | 0 .../test_project_test_asset_test_task_v001.ma | 476 +++++++ tests/lib/file_handler.py | 122 +- tests/lib/testing_classes.py | 70 +- 41 files changed, 9346 insertions(+), 96 deletions(-) create mode 100644 tests/integration/hosts/maya/test_deadline_publish_in_maya/README.md create mode 100644 tests/integration/hosts/maya/test_deadline_publish_in_maya/expected/test_project/test_asset/publish/model/modelMain/hero/test_project_test_asset_modelMain_hero.abc create mode 100644 tests/integration/hosts/maya/test_deadline_publish_in_maya/expected/test_project/test_asset/publish/model/modelMain/hero/test_project_test_asset_modelMain_hero.ma create mode 100644 tests/integration/hosts/maya/test_deadline_publish_in_maya/expected/test_project/test_asset/publish/model/modelMain/v001/test_project_test_asset_modelMain_v001.abc create mode 100644 tests/integration/hosts/maya/test_deadline_publish_in_maya/expected/test_project/test_asset/publish/model/modelMain/v001/test_project_test_asset_modelMain_v001.ma create mode 100644 tests/integration/hosts/maya/test_deadline_publish_in_maya/expected/test_project/test_asset/publish/render/renderTest_taskMain_beauty/v001/test_project_test_asset_renderTest_taskMain_beauty_v001.exr create mode 100644 tests/integration/hosts/maya/test_deadline_publish_in_maya/expected/test_project/test_asset/publish/render/renderTest_taskMain_beauty/v001/test_project_test_asset_renderTest_taskMain_beauty_v001.jpg create mode 100644 tests/integration/hosts/maya/test_deadline_publish_in_maya/expected/test_project/test_asset/publish/render/renderTest_taskMain_beauty/v001/test_project_test_asset_renderTest_taskMain_beauty_v001_png.png create mode 100644 tests/integration/hosts/maya/test_deadline_publish_in_maya/expected/test_project/test_asset/publish/workfile/workfileTest_task/v001/test_project_test_asset_workfileTest_task_v001.ma create mode 100644 tests/integration/hosts/maya/test_deadline_publish_in_maya/expected/test_project/test_asset/work/test_task/test_project_test_asset_test_task_v001.ma create mode 100644 tests/integration/hosts/maya/test_deadline_publish_in_maya/expected/test_project/test_asset/work/test_task/test_project_test_asset_test_task_v002.ma create mode 100644 tests/integration/hosts/maya/test_deadline_publish_in_maya/expected/test_project/test_asset/work/test_task/workspace.mel create mode 100644 tests/integration/hosts/maya/test_deadline_publish_in_maya/input/dumps/avalon_tests/test_project.bson create mode 100644 tests/integration/hosts/maya/test_deadline_publish_in_maya/input/dumps/avalon_tests/test_project.metadata.json create mode 100644 tests/integration/hosts/maya/test_deadline_publish_in_maya/input/dumps/openpype_tests/settings.bson create mode 100644 tests/integration/hosts/maya/test_deadline_publish_in_maya/input/dumps/openpype_tests/settings.metadata.json create mode 100644 tests/integration/hosts/maya/test_deadline_publish_in_maya/input/env_vars/env_var.json create mode 100644 tests/integration/hosts/maya/test_deadline_publish_in_maya/input/startup/userSetup.py create mode 100644 tests/integration/hosts/maya/test_deadline_publish_in_maya/input/workfile/test_project_test_asset_test_task_v001.ma create mode 100644 tests/integration/hosts/maya/test_publish_in_maya/expected/test_project/test_asset/publish/model/modelMain/hero/test_project_test_asset_modelMain_hero.abc create mode 100644 tests/integration/hosts/maya/test_publish_in_maya/expected/test_project/test_asset/publish/model/modelMain/hero/test_project_test_asset_modelMain_hero.ma create mode 100644 tests/integration/hosts/maya/test_publish_in_maya/expected/test_project/test_asset/publish/model/modelMain/v001/test_project_test_asset_modelMain_v001.abc create mode 100644 tests/integration/hosts/maya/test_publish_in_maya/expected/test_project/test_asset/publish/model/modelMain/v001/test_project_test_asset_modelMain_v001.ma create mode 100644 tests/integration/hosts/maya/test_publish_in_maya/expected/test_project/test_asset/publish/workfile/workfileTest_task/v001/test_project_test_asset_workfileTest_task_v001.ma create mode 100644 tests/integration/hosts/maya/test_publish_in_maya/expected/test_project/test_asset/work/test_task/renders/maya/test_project_test_asset_workfileTest_task_v001/Main/renderMain_metadata.json create mode 100644 tests/integration/hosts/maya/test_publish_in_maya/expected/test_project/test_asset/work/test_task/test_project_test_asset_test_task_v001.ma create mode 100644 tests/integration/hosts/maya/test_publish_in_maya/expected/test_project/test_asset/work/test_task/test_project_test_asset_test_task_v002.ma create mode 100644 tests/integration/hosts/maya/test_publish_in_maya/expected/test_project/test_asset/work/test_task/workspace.mel create mode 100644 tests/integration/hosts/maya/test_publish_in_maya/input/dumps/avalon_tests/test_project.bson create mode 100644 tests/integration/hosts/maya/test_publish_in_maya/input/dumps/avalon_tests/test_project.metadata.json create mode 100644 tests/integration/hosts/maya/test_publish_in_maya/input/dumps/openpype_tests/settings.bson create mode 100644 tests/integration/hosts/maya/test_publish_in_maya/input/dumps/openpype_tests/settings.metadata.json create mode 100644 tests/integration/hosts/maya/test_publish_in_maya/input/env_vars/env_var.json rename tests/integration/hosts/maya/{ => test_publish_in_maya}/input/startup/userSetup.py (100%) create mode 100644 tests/integration/hosts/maya/test_publish_in_maya/input/workfile/test_project_test_asset_test_task_v001.ma diff --git a/tests/integration/README.md b/tests/integration/README.md index eef8141127..7b9b7cd706 100644 --- a/tests/integration/README.md +++ b/tests/integration/README.md @@ -14,23 +14,52 @@ How to run - run in cmd `{OPENPYPE_ROOT}/.venv/Scripts/python.exe {OPENPYPE_ROOT}/start.py runtests {OPENPYPE_ROOT}/tests/integration` - add `hosts/APP_NAME` after integration part to limit only on specific app (eg. `{OPENPYPE_ROOT}/tests/integration/hosts/maya`) - -OR can use built executables + +OR can use built executables `openpype_console runtests {ABS_PATH}/tests/integration` +Command line arguments +---------------------- + - "--mark" - "Run tests marked by", + - "--pyargs" - "Run tests from package", + - "--test_data_folder" - "Unzipped directory path of test file", + - "--persist" - "Persist test DB and published files after test end", + - "--app_variant" - "Provide specific app variant for test, empty for latest", + - "--app_group" - "Provide specific app group for test, empty for default", + - "--timeout" - "Provide specific timeout value for test case", + - "--setup_only" - "Only create dbs, do not run tests", + - "--mongo_url" - "MongoDB for testing.", + +Run Tray for test +----------------- +In case of failed test you might want to run it manually and visually debug what happened. +For that: +- run tests that is failing +- add environment variables (to command line process or your IDE) + - OPENPYPE_DATABASE_NAME = openpype_tests + - AVALON_DB = avalon_tests +- run tray as usual + - `{OPENPYPE_ROOT}/.venv/Scripts/python.exe {OPENPYPE_ROOT}/start.py run tray --debug` + +You should see only test asset and state of databases for that particular use case. + How to check logs/errors from app -------------------------------- -Keep PERSIST to True in the class and check `test_openpype.logs` collection. +Keep PERSIST to True in the class and check `test_openpype.logs` collection. How to create test for publishing from host ------------------------------------------ - Extend PublishTest in `tests/lib/testing_classes.py` - Use `resources\test_data.zip` skeleton file as a template for testing input data -- Put workfile into `test_data.zip/input/workfile` -- If you require other than base DB dumps provide them to `test_data.zip/input/dumps` +- Create subfolder `test_data` with matching name to your test file containing you test class + - (see `tests/integration/hosts/maya/test_publish_in_maya` and `test_publish_in_maya.py`) +- Put this subfolder name into TEST_FILES [(HASH_ID, FILE_NAME, MD5_OPTIONAL)] + - at first position, all others may be "" +- Put workfile into `test_data/input/workfile` +- If you require other than base DB dumps provide them to `test_data/input/dumps` -- (Check commented code in `db_handler.py` how to dump specific DB. Currently all collections will be dumped.) -- Implement `last_workfile_path` -- `startup_scripts` - must contain pointing host to startup script saved into `test_data.zip/input/startup` +- Implement `last_workfile_path` +- `startup_scripts` - must contain pointing host to startup script saved into `test_data/input/startup` -- Script must contain something like (pseudocode) ``` import openpype @@ -39,7 +68,7 @@ from avalon import api, HOST from openpype.api import Logger log = Logger().get_logger(__name__) - + api.install(HOST) log_lines = [] for result in pyblish.util.publish_iter(): @@ -54,18 +83,20 @@ for result in pyblish.util.publish_iter(): EXIT_APP (command to exit host) ``` (Install and publish methods must be triggered only AFTER host app is fully initialized!) -- If you would like add any command line arguments for your host app add it to `test_data.zip/input/app_args/app_args.json` (as a json list) -- Provide any required environment variables to `test_data.zip/input/env_vars/env_vars.json` (as a json dictionary) -- Zip `test_data.zip`, named it with descriptive name, upload it to Google Drive, right click - `Get link`, copy hash id (file must be accessible to anyone with a link!) -- Put this hash id and zip file name into TEST_FILES [(HASH_ID, FILE_NAME, MD5_OPTIONAL)]. If you want to check MD5 of downloaded -file, provide md5 value of zipped file. +- If you would like add any command line arguments for your host app add it to `test_data/input/app_args/app_args.json` (as a json list) +- Provide any required environment variables to `test_data/input/env_vars/env_vars.json` (as a json dictionary) - Implement any assert checks you need in extended class - Run test class manually (via Pycharm or pytest runner (TODO)) - If you want test to visually compare expected files to published one, set PERSIST to True, run test manually -- Locate temporary `publish` subfolder of temporary folder (found in debugging console log) - -- Copy whole folder content into .zip file into `expected` subfolder + -- Copy whole folder content into .zip file into `expected` subfolder -- By default tests are comparing only structure of `expected` and published format (eg. if you want to save space, replace published files with empty files, but with expected names!) -- Zip and upload again, change PERSIST to False - + - Use `TEST_DATA_FOLDER` variable in your class to reuse existing downloaded and unzipped test data (for faster creation of tests) -- Keep `APP_VARIANT` empty if you want to trigger test on latest version of app, or provide explicit value (as '2022' for Photoshop for example) \ No newline at end of file +- Keep `APP_VARIANT` empty if you want to trigger test on latest version of app, or provide explicit value (as '2022' for Photoshop for example) + +For storing test zip files on Google Drive: +- Zip `test_data.zip`, named it with descriptive name, upload it to Google Drive, right click - `Get link`, copy hash id (file must be accessible to anyone with a link!) +- Put this hash id and zip file name into TEST_FILES [(HASH_ID, FILE_NAME, MD5_OPTIONAL)]. If you want to check MD5 of downloaded +file, provide md5 value of zipped file. diff --git a/tests/integration/hosts/maya/lib.py b/tests/integration/hosts/maya/lib.py index 04ddb765a4..ab5bcbbd36 100644 --- a/tests/integration/hosts/maya/lib.py +++ b/tests/integration/hosts/maya/lib.py @@ -16,18 +16,25 @@ class MayaHostFixtures(HostFixtures): Maya expects workfile in proper folder, so copy is done first. """ - src_path = os.path.join(download_test_data, - "input", - "workfile", - "test_project_test_asset_test_task_v001.mb") - dest_folder = os.path.join(output_folder_url, - self.PROJECT, - self.ASSET, - "work", - self.TASK) + src_path = os.path.join( + download_test_data, + "input", + "workfile", + "test_project_test_asset_test_task_v001.ma" + ) + dest_folder = os.path.join( + output_folder_url, + self.PROJECT, + self.ASSET, + "work", + self.TASK + ) + os.makedirs(dest_folder) - dest_path = os.path.join(dest_folder, - "test_project_test_asset_test_task_v001.mb") + + dest_path = os.path.join( + dest_folder, "test_project_test_asset_test_task_v001.ma" + ) shutil.copy(src_path, dest_path) yield dest_path @@ -36,7 +43,7 @@ class MayaHostFixtures(HostFixtures): def startup_scripts(self, monkeypatch_session, download_test_data): """Points Maya to userSetup file from input data""" startup_path = os.path.join( - os.path.dirname(__file__), "input", "startup" + download_test_data, "input", "startup" ) original_pythonpath = os.environ.get("PYTHONPATH") monkeypatch_session.setenv( diff --git a/tests/integration/hosts/maya/test_deadline_publish_in_maya.py b/tests/integration/hosts/maya/test_deadline_publish_in_maya.py index 9332177944..c2ef342600 100644 --- a/tests/integration/hosts/maya/test_deadline_publish_in_maya.py +++ b/tests/integration/hosts/maya/test_deadline_publish_in_maya.py @@ -24,8 +24,7 @@ class TestDeadlinePublishInMaya(MayaDeadlinePublishTestClass): PERSIST = True TEST_FILES = [ - ("1dDY7CbdFXfRksGVoiuwjhnPoTRCCf5ea", - "test_maya_deadline_publish.zip", "") + ("test_deadline_publish_in_maya", "", "") ] APP_GROUP = "maya" diff --git a/tests/integration/hosts/maya/test_deadline_publish_in_maya/README.md b/tests/integration/hosts/maya/test_deadline_publish_in_maya/README.md new file mode 100644 index 0000000000..929bfcd666 --- /dev/null +++ b/tests/integration/hosts/maya/test_deadline_publish_in_maya/README.md @@ -0,0 +1,17 @@ +Test data +--------- +Each class implementing `TestCase` can provide test file(s) by adding them to +TEST_FILES ('GDRIVE_FILE_ID', 'ACTUAL_FILE_NAME', 'MD5HASH') + +GDRIVE_FILE_ID can be pulled from shareable link from Google Drive app. + +Currently it is expected that test file will be zip file with structure: +- expected - expected files (not implemented yet) +- input + - data - test data (workfiles, images etc) + - dumps - folder for BSOn dumps from (`mongodump`) + - env_vars + env_vars.json - dictionary with environment variables {key:value} + + - sql - sql files to load with `mongoimport` (human readable) + - startup - scripts that should run in the host on its startup diff --git a/tests/integration/hosts/maya/test_deadline_publish_in_maya/expected/test_project/test_asset/publish/model/modelMain/hero/test_project_test_asset_modelMain_hero.abc b/tests/integration/hosts/maya/test_deadline_publish_in_maya/expected/test_project/test_asset/publish/model/modelMain/hero/test_project_test_asset_modelMain_hero.abc new file mode 100644 index 0000000000000000000000000000000000000000..d9e54259960efb658348c78adaa0f9ae0958083e GIT binary patch literal 24787 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 MH=R?xes^8}8yK4hrvLx| literal 0 HcmV?d00001 diff --git a/tests/integration/hosts/maya/test_deadline_publish_in_maya/expected/test_project/test_asset/publish/model/modelMain/hero/test_project_test_asset_modelMain_hero.ma b/tests/integration/hosts/maya/test_deadline_publish_in_maya/expected/test_project/test_asset/publish/model/modelMain/hero/test_project_test_asset_modelMain_hero.ma new file mode 100644 index 0000000000..9ee588337e --- /dev/null +++ b/tests/integration/hosts/maya/test_deadline_publish_in_maya/expected/test_project/test_asset/publish/model/modelMain/hero/test_project_test_asset_modelMain_hero.ma @@ -0,0 +1,1208 @@ +//Maya ASCII 2020 scene +//Name: modelMain.ma +//Last modified: Mon, Oct 24, 2022 02:57:47 PM +//Codeset: 1252 +requires maya "2020"; +requires "mtoa" "4.1.1"; +currentUnit -l centimeter -a degree -t pal; +fileInfo "application" "maya"; +fileInfo "product" "Maya 2020"; +fileInfo "version" "2020"; +fileInfo "cutIdentifier" "202011110415-b1e20b88e2"; +fileInfo "osv" "Microsoft Windows 10 Technical Preview (Build 19044)\n"; +fileInfo "UUID" "A787A358-4FE7-6E55-0C81-61BFEB0C2726"; +createNode transform -n "model_GRP"; + rename -uid "445FDC20-4A9D-2C5B-C7BD-F98F6E660B5C"; + setAttr ".rlio[0]" 1 yes 0; +createNode transform -n "pSphere1_GEO" -p "model_GRP"; + rename -uid "7445A43F-444F-B2D3-4315-2AA013D2E0B6"; + addAttr -ci true -sn "cbId" -ln "cbId" -dt "string"; + setAttr ".cbId" -type "string" "60df31e2be2b48bd3695c056:302a4c6123a4"; +createNode mesh -n "pSphere1_GEOShape1" -p "pSphere1_GEO"; + rename -uid "7C731260-45C6-339E-07BF-359446B08EA1"; + addAttr -ci true -sn "cbId" -ln "cbId" -dt "string"; + setAttr -k off ".v"; + setAttr ".vir" yes; + setAttr ".vif" yes; + setAttr ".uvst[0].uvsn" -type "string" "map1"; + setAttr -s 439 ".uvst[0].uvsp"; + setAttr ".uvst[0].uvsp[0:249]" -type "float2" 0 0.050000001 0.050000001 0.050000001 + 0.1 0.050000001 0.15000001 0.050000001 0.2 0.050000001 0.25 0.050000001 0.30000001 + 0.050000001 0.35000002 0.050000001 0.40000004 0.050000001 0.45000005 0.050000001 + 0.50000006 0.050000001 0.55000007 0.050000001 0.60000008 0.050000001 0.6500001 0.050000001 + 0.70000011 0.050000001 0.75000012 0.050000001 0.80000013 0.050000001 0.85000014 0.050000001 + 0.90000015 0.050000001 0.95000017 0.050000001 1.000000119209 0.050000001 0 0.1 0.050000001 + 0.1 0.1 0.1 0.15000001 0.1 0.2 0.1 0.25 0.1 0.30000001 0.1 0.35000002 0.1 0.40000004 + 0.1 0.45000005 0.1 0.50000006 0.1 0.55000007 0.1 0.60000008 0.1 0.6500001 0.1 0.70000011 + 0.1 0.75000012 0.1 0.80000013 0.1 0.85000014 0.1 0.90000015 0.1 0.95000017 0.1 1.000000119209 + 0.1 0 0.15000001 0.050000001 0.15000001 0.1 0.15000001 0.15000001 0.15000001 0.2 + 0.15000001 0.25 0.15000001 0.30000001 0.15000001 0.35000002 0.15000001 0.40000004 + 0.15000001 0.45000005 0.15000001 0.50000006 0.15000001 0.55000007 0.15000001 0.60000008 + 0.15000001 0.6500001 0.15000001 0.70000011 0.15000001 0.75000012 0.15000001 0.80000013 + 0.15000001 0.85000014 0.15000001 0.90000015 0.15000001 0.95000017 0.15000001 1.000000119209 + 0.15000001 0 0.2 0.050000001 0.2 0.1 0.2 0.15000001 0.2 0.2 0.2 0.25 0.2 0.30000001 + 0.2 0.35000002 0.2 0.40000004 0.2 0.45000005 0.2 0.50000006 0.2 0.55000007 0.2 0.60000008 + 0.2 0.6500001 0.2 0.70000011 0.2 0.75000012 0.2 0.80000013 0.2 0.85000014 0.2 0.90000015 + 0.2 0.95000017 0.2 1.000000119209 0.2 0 0.25 0.050000001 0.25 0.1 0.25 0.15000001 + 0.25 0.2 0.25 0.25 0.25 0.30000001 0.25 0.35000002 0.25 0.40000004 0.25 0.45000005 + 0.25 0.50000006 0.25 0.55000007 0.25 0.60000008 0.25 0.6500001 0.25 0.70000011 0.25 + 0.75000012 0.25 0.80000013 0.25 0.85000014 0.25 0.90000015 0.25 0.95000017 0.25 1.000000119209 + 0.25 0 0.30000001 0.050000001 0.30000001 0.1 0.30000001 0.15000001 0.30000001 0.2 + 0.30000001 0.25 0.30000001 0.30000001 0.30000001 0.35000002 0.30000001 0.40000004 + 0.30000001 0.45000005 0.30000001 0.50000006 0.30000001 0.55000007 0.30000001 0.60000008 + 0.30000001 0.6500001 0.30000001 0.70000011 0.30000001 0.75000012 0.30000001 0.80000013 + 0.30000001 0.85000014 0.30000001 0.90000015 0.30000001 0.95000017 0.30000001 1.000000119209 + 0.30000001 0 0.35000002 0.050000001 0.35000002 0.1 0.35000002 0.15000001 0.35000002 + 0.2 0.35000002 0.25 0.35000002 0.30000001 0.35000002 0.35000002 0.35000002 0.40000004 + 0.35000002 0.45000005 0.35000002 0.50000006 0.35000002 0.55000007 0.35000002 0.60000008 + 0.35000002 0.6500001 0.35000002 0.70000011 0.35000002 0.75000012 0.35000002 0.80000013 + 0.35000002 0.85000014 0.35000002 0.90000015 0.35000002 0.95000017 0.35000002 1.000000119209 + 0.35000002 0 0.40000004 0.050000001 0.40000004 0.1 0.40000004 0.15000001 0.40000004 + 0.2 0.40000004 0.25 0.40000004 0.30000001 0.40000004 0.35000002 0.40000004 0.40000004 + 0.40000004 0.45000005 0.40000004 0.50000006 0.40000004 0.55000007 0.40000004 0.60000008 + 0.40000004 0.6500001 0.40000004 0.70000011 0.40000004 0.75000012 0.40000004 0.80000013 + 0.40000004 0.85000014 0.40000004 0.90000015 0.40000004 0.95000017 0.40000004 1.000000119209 + 0.40000004 0 0.45000005 0.050000001 0.45000005 0.1 0.45000005 0.15000001 0.45000005 + 0.2 0.45000005 0.25 0.45000005 0.30000001 0.45000005 0.35000002 0.45000005 0.40000004 + 0.45000005 0.45000005 0.45000005 0.50000006 0.45000005 0.55000007 0.45000005 0.60000008 + 0.45000005 0.6500001 0.45000005 0.70000011 0.45000005 0.75000012 0.45000005 0.80000013 + 0.45000005 0.85000014 0.45000005 0.90000015 0.45000005 0.95000017 0.45000005 1.000000119209 + 0.45000005 0 0.50000006 0.050000001 0.50000006 0.1 0.50000006 0.15000001 0.50000006 + 0.2 0.50000006 0.25 0.50000006 0.30000001 0.50000006 0.35000002 0.50000006 0.40000004 + 0.50000006 0.45000005 0.50000006 0.50000006 0.50000006 0.55000007 0.50000006 0.60000008 + 0.50000006 0.6500001 0.50000006 0.70000011 0.50000006 0.75000012 0.50000006 0.80000013 + 0.50000006 0.85000014 0.50000006 0.90000015 0.50000006 0.95000017 0.50000006 1.000000119209 + 0.50000006 0 0.55000007 0.050000001 0.55000007 0.1 0.55000007 0.15000001 0.55000007 + 0.2 0.55000007 0.25 0.55000007 0.30000001 0.55000007 0.35000002 0.55000007 0.40000004 + 0.55000007 0.45000005 0.55000007 0.50000006 0.55000007 0.55000007 0.55000007 0.60000008 + 0.55000007 0.6500001 0.55000007 0.70000011 0.55000007 0.75000012 0.55000007 0.80000013 + 0.55000007 0.85000014 0.55000007 0.90000015 0.55000007 0.95000017 0.55000007 1.000000119209 + 0.55000007 0 0.60000008 0.050000001 0.60000008 0.1 0.60000008 0.15000001 0.60000008 + 0.2 0.60000008 0.25 0.60000008 0.30000001 0.60000008 0.35000002 0.60000008 0.40000004 + 0.60000008 0.45000005 0.60000008 0.50000006 0.60000008 0.55000007 0.60000008 0.60000008 + 0.60000008 0.6500001 0.60000008 0.70000011 0.60000008 0.75000012 0.60000008 0.80000013 + 0.60000008 0.85000014 0.60000008 0.90000015 0.60000008; + setAttr ".uvst[0].uvsp[250:438]" 0.95000017 0.60000008 1.000000119209 0.60000008 + 0 0.6500001 0.050000001 0.6500001 0.1 0.6500001 0.15000001 0.6500001 0.2 0.6500001 + 0.25 0.6500001 0.30000001 0.6500001 0.35000002 0.6500001 0.40000004 0.6500001 0.45000005 + 0.6500001 0.50000006 0.6500001 0.55000007 0.6500001 0.60000008 0.6500001 0.6500001 + 0.6500001 0.70000011 0.6500001 0.75000012 0.6500001 0.80000013 0.6500001 0.85000014 + 0.6500001 0.90000015 0.6500001 0.95000017 0.6500001 1.000000119209 0.6500001 0 0.70000011 + 0.050000001 0.70000011 0.1 0.70000011 0.15000001 0.70000011 0.2 0.70000011 0.25 0.70000011 + 0.30000001 0.70000011 0.35000002 0.70000011 0.40000004 0.70000011 0.45000005 0.70000011 + 0.50000006 0.70000011 0.55000007 0.70000011 0.60000008 0.70000011 0.6500001 0.70000011 + 0.70000011 0.70000011 0.75000012 0.70000011 0.80000013 0.70000011 0.85000014 0.70000011 + 0.90000015 0.70000011 0.95000017 0.70000011 1.000000119209 0.70000011 0 0.75000012 + 0.050000001 0.75000012 0.1 0.75000012 0.15000001 0.75000012 0.2 0.75000012 0.25 0.75000012 + 0.30000001 0.75000012 0.35000002 0.75000012 0.40000004 0.75000012 0.45000005 0.75000012 + 0.50000006 0.75000012 0.55000007 0.75000012 0.60000008 0.75000012 0.6500001 0.75000012 + 0.70000011 0.75000012 0.75000012 0.75000012 0.80000013 0.75000012 0.85000014 0.75000012 + 0.90000015 0.75000012 0.95000017 0.75000012 1.000000119209 0.75000012 0 0.80000013 + 0.050000001 0.80000013 0.1 0.80000013 0.15000001 0.80000013 0.2 0.80000013 0.25 0.80000013 + 0.30000001 0.80000013 0.35000002 0.80000013 0.40000004 0.80000013 0.45000005 0.80000013 + 0.50000006 0.80000013 0.55000007 0.80000013 0.60000008 0.80000013 0.6500001 0.80000013 + 0.70000011 0.80000013 0.75000012 0.80000013 0.80000013 0.80000013 0.85000014 0.80000013 + 0.90000015 0.80000013 0.95000017 0.80000013 1.000000119209 0.80000013 0 0.85000014 + 0.050000001 0.85000014 0.1 0.85000014 0.15000001 0.85000014 0.2 0.85000014 0.25 0.85000014 + 0.30000001 0.85000014 0.35000002 0.85000014 0.40000004 0.85000014 0.45000005 0.85000014 + 0.50000006 0.85000014 0.55000007 0.85000014 0.60000008 0.85000014 0.6500001 0.85000014 + 0.70000011 0.85000014 0.75000012 0.85000014 0.80000013 0.85000014 0.85000014 0.85000014 + 0.90000015 0.85000014 0.95000017 0.85000014 1.000000119209 0.85000014 0 0.90000015 + 0.050000001 0.90000015 0.1 0.90000015 0.15000001 0.90000015 0.2 0.90000015 0.25 0.90000015 + 0.30000001 0.90000015 0.35000002 0.90000015 0.40000004 0.90000015 0.45000005 0.90000015 + 0.50000006 0.90000015 0.55000007 0.90000015 0.60000008 0.90000015 0.6500001 0.90000015 + 0.70000011 0.90000015 0.75000012 0.90000015 0.80000013 0.90000015 0.85000014 0.90000015 + 0.90000015 0.90000015 0.95000017 0.90000015 1.000000119209 0.90000015 0 0.95000017 + 0.050000001 0.95000017 0.1 0.95000017 0.15000001 0.95000017 0.2 0.95000017 0.25 0.95000017 + 0.30000001 0.95000017 0.35000002 0.95000017 0.40000004 0.95000017 0.45000005 0.95000017 + 0.50000006 0.95000017 0.55000007 0.95000017 0.60000008 0.95000017 0.6500001 0.95000017 + 0.70000011 0.95000017 0.75000012 0.95000017 0.80000013 0.95000017 0.85000014 0.95000017 + 0.90000015 0.95000017 0.95000017 0.95000017 1.000000119209 0.95000017 0.025 0 0.075000003 + 0 0.125 0 0.17500001 0 0.22500001 0 0.27500001 0 0.32500002 0 0.375 0 0.42500001 + 0 0.47500002 0 0.52499998 0 0.57499999 0 0.625 0 0.67500001 0 0.72499996 0 0.77499998 + 0 0.82499999 0 0.875 0 0.92500001 0 0.97499996 0 0.025 1 0.075000003 1 0.125 1 0.17500001 + 1 0.22500001 1 0.27500001 1 0.32500002 1 0.375 1 0.42500001 1 0.47500002 1 0.52499998 + 1 0.57499999 1 0.625 1 0.67500001 1 0.72499996 1 0.77499998 1 0.82499999 1 0.875 + 1 0.92500001 1 0.97499996 1; + setAttr ".cuvs" -type "string" "map1"; + setAttr ".dcc" -type "string" "Ambient+Diffuse"; + setAttr ".covm[0]" 0 1 1; + setAttr ".cdvm[0]" 0 1 1; + setAttr -s 382 ".vt"; + setAttr ".vt[0:165]" 0.14877813 -0.98768836 -0.048340943 0.12655823 -0.98768836 -0.091949932 + 0.091949932 -0.98768836 -0.12655823 0.048340935 -0.98768836 -0.14877811 0 -0.98768836 -0.15643455 + -0.048340935 -0.98768836 -0.1487781 -0.091949917 -0.98768836 -0.1265582 -0.12655818 -0.98768836 -0.091949902 + -0.14877807 -0.98768836 -0.048340924 -0.15643452 -0.98768836 0 -0.14877807 -0.98768836 0.048340924 + -0.12655818 -0.98768836 0.091949895 -0.091949895 -0.98768836 0.12655817 -0.048340924 -0.98768836 0.14877805 + -4.6621107e-09 -0.98768836 0.15643449 0.048340909 -0.98768836 0.14877804 0.09194988 -0.98768836 0.12655815 + 0.12655815 -0.98768836 0.091949888 0.14877804 -0.98768836 0.048340913 0.15643448 -0.98768836 0 + 0.29389283 -0.95105654 -0.095491566 0.25000018 -0.95105654 -0.18163574 0.18163574 -0.95105654 -0.25000015 + 0.095491551 -0.95105654 -0.2938928 0 -0.95105654 -0.30901715 -0.095491551 -0.95105654 -0.29389277 + -0.18163571 -0.95105654 -0.25000009 -0.25000009 -0.95105654 -0.18163569 -0.29389271 -0.95105654 -0.095491529 + -0.30901706 -0.95105654 0 -0.29389271 -0.95105654 0.095491529 -0.25000006 -0.95105654 0.18163568 + -0.18163568 -0.95105654 0.25000006 -0.095491529 -0.95105654 0.29389268 -9.2094243e-09 -0.95105654 0.30901703 + 0.095491499 -0.95105654 0.29389265 0.18163563 -0.95105654 0.25000003 0.25 -0.95105654 0.18163565 + 0.29389265 -0.95105654 0.095491506 0.309017 -0.95105654 0 0.43177092 -0.89100653 -0.14029087 + 0.36728629 -0.89100653 -0.2668491 0.2668491 -0.89100653 -0.36728626 0.14029086 -0.89100653 -0.43177086 + 0 -0.89100653 -0.45399073 -0.14029086 -0.89100653 -0.43177083 -0.26684904 -0.89100653 -0.36728618 + -0.36728615 -0.89100653 -0.26684901 -0.43177077 -0.89100653 -0.14029081 -0.45399064 -0.89100653 0 + -0.43177077 -0.89100653 0.14029081 -0.36728612 -0.89100653 0.26684898 -0.26684898 -0.89100653 0.36728612 + -0.14029081 -0.89100653 0.43177071 -1.3529972e-08 -0.89100653 0.45399058 0.14029078 -0.89100653 0.43177068 + 0.26684892 -0.89100653 0.36728609 0.36728606 -0.89100653 0.26684895 0.43177065 -0.89100653 0.1402908 + 0.45399052 -0.89100653 0 0.55901736 -0.809017 -0.18163574 0.47552857 -0.809017 -0.34549171 + 0.34549171 -0.809017 -0.47552854 0.18163572 -0.809017 -0.5590173 0 -0.809017 -0.58778554 + -0.18163572 -0.809017 -0.55901724 -0.34549165 -0.809017 -0.47552842 -0.47552839 -0.809017 -0.34549159 + -0.55901712 -0.809017 -0.18163566 -0.58778536 -0.809017 0 -0.55901712 -0.809017 0.18163566 + -0.47552836 -0.809017 0.34549156 -0.34549156 -0.809017 0.47552833 -0.18163566 -0.809017 0.55901706 + -1.7517365e-08 -0.809017 0.5877853 0.18163562 -0.809017 0.55901706 0.3454915 -0.809017 0.4755283 + 0.47552827 -0.809017 0.34549153 0.559017 -0.809017 0.18163563 0.58778524 -0.809017 0 + 0.67249894 -0.70710677 -0.21850814 0.57206178 -0.70710677 -0.41562718 0.41562718 -0.70710677 -0.57206172 + 0.21850812 -0.70710677 -0.67249888 0 -0.70710677 -0.70710713 -0.21850812 -0.70710677 -0.67249882 + -0.41562709 -0.70710677 -0.5720616 -0.57206154 -0.70710677 -0.41562706 -0.6724987 -0.70710677 -0.21850805 + -0.70710695 -0.70710677 0 -0.6724987 -0.70710677 0.21850805 -0.57206154 -0.70710677 0.415627 + -0.415627 -0.70710677 0.57206148 -0.21850805 -0.70710677 0.67249858 -2.1073424e-08 -0.70710677 0.70710683 + 0.21850799 -0.70710677 0.67249858 0.41562691 -0.70710677 0.57206142 0.57206142 -0.70710677 0.41562697 + 0.67249852 -0.70710677 0.21850802 0.70710677 -0.70710677 0 0.7694214 -0.58778524 -0.25000015 + 0.65450895 -0.58778524 -0.47552854 0.47552854 -0.58778524 -0.65450889 0.25000012 -0.58778524 -0.76942128 + 0 -0.58778524 -0.80901736 -0.25000012 -0.58778524 -0.76942122 -0.47552845 -0.58778524 -0.65450877 + -0.65450871 -0.58778524 -0.47552839 -0.7694211 -0.58778524 -0.25000006 -0.80901718 -0.58778524 0 + -0.7694211 -0.58778524 0.25000006 -0.65450865 -0.58778524 0.47552836 -0.47552836 -0.58778524 0.65450859 + -0.25000006 -0.58778524 0.76942098 -2.4110586e-08 -0.58778524 0.80901712 0.24999999 -0.58778524 0.76942098 + 0.47552827 -0.58778524 0.65450853 0.65450853 -0.58778524 0.4755283 0.76942092 -0.58778524 0.25 + 0.809017 -0.58778524 0 0.8473981 -0.45399052 -0.27533633 0.72083992 -0.45399052 -0.5237208 + 0.5237208 -0.45399052 -0.72083986 0.2753363 -0.45399052 -0.84739798 0 -0.45399052 -0.89100695 + -0.2753363 -0.45399052 -0.84739798 -0.52372068 -0.45399052 -0.72083968 -0.72083962 -0.45399052 -0.52372062 + -0.8473978 -0.45399052 -0.27533621 -0.89100677 -0.45399052 0 -0.8473978 -0.45399052 0.27533621 + -0.72083962 -0.45399052 0.52372062 -0.52372062 -0.45399052 0.72083956 -0.27533621 -0.45399052 0.84739769 + -2.6554064e-08 -0.45399052 0.89100665 0.27533615 -0.45399052 0.84739763 0.5237205 -0.45399052 0.7208395 + 0.72083944 -0.45399052 0.52372056 0.84739757 -0.45399052 0.27533618 0.89100653 -0.45399052 0 + 0.90450913 -0.30901697 -0.2938928 0.7694214 -0.30901697 -0.55901736 0.55901736 -0.30901697 -0.76942134 + 0.29389277 -0.30901697 -0.90450901 0 -0.30901697 -0.95105702 -0.29389277 -0.30901697 -0.90450895 + -0.55901724 -0.30901697 -0.76942122 -0.76942116 -0.30901697 -0.55901718 -0.90450877 -0.30901697 -0.29389271 + -0.95105678 -0.30901697 0 -0.90450877 -0.30901697 0.29389271 -0.7694211 -0.30901697 0.55901712 + -0.55901712 -0.30901697 0.76942104 -0.29389271 -0.30901697 0.90450865 -2.8343694e-08 -0.30901697 0.95105666 + 0.29389262 -0.30901697 0.90450859 0.559017 -0.30901697 0.76942098 0.76942092 -0.30901697 0.55901706 + 0.90450853 -0.30901697 0.29389265 0.95105654 -0.30901697 0 0.93934804 -0.15643437 -0.30521268 + 0.79905719 -0.15643437 -0.580549 0.580549 -0.15643437 -0.79905713 0.30521265 -0.15643437 -0.93934792 + 0 -0.15643437 -0.98768884 -0.30521265 -0.15643437 -0.93934786; + setAttr ".vt[166:331]" -0.58054888 -0.15643437 -0.79905695 -0.79905689 -0.15643437 -0.58054882 + -0.93934768 -0.15643437 -0.30521256 -0.9876886 -0.15643437 0 -0.93934768 -0.15643437 0.30521256 + -0.79905683 -0.15643437 0.58054876 -0.58054876 -0.15643437 0.79905677 -0.30521256 -0.15643437 0.93934757 + -2.9435407e-08 -0.15643437 0.98768848 0.30521247 -0.15643437 0.93934757 0.58054864 -0.15643437 0.79905671 + 0.79905665 -0.15643437 0.5805487 0.93934751 -0.15643437 0.3052125 0.98768836 -0.15643437 0 + 0.95105714 0 -0.30901718 0.80901754 0 -0.5877856 0.5877856 0 -0.80901748 0.30901715 0 -0.95105702 + 0 0 -1.000000476837 -0.30901715 0 -0.95105696 -0.58778548 0 -0.8090173 -0.80901724 0 -0.58778542 + -0.95105678 0 -0.30901706 -1.000000238419 0 0 -0.95105678 0 0.30901706 -0.80901718 0 0.58778536 + -0.58778536 0 0.80901712 -0.30901706 0 0.95105666 -2.9802322e-08 0 1.000000119209 + 0.30901697 0 0.9510566 0.58778524 0 0.80901706 0.809017 0 0.5877853 0.95105654 0 0.309017 + 1 0 0 0.93934804 0.15643437 -0.30521268 0.79905719 0.15643437 -0.580549 0.580549 0.15643437 -0.79905713 + 0.30521265 0.15643437 -0.93934792 0 0.15643437 -0.98768884 -0.30521265 0.15643437 -0.93934786 + -0.58054888 0.15643437 -0.79905695 -0.79905689 0.15643437 -0.58054882 -0.93934768 0.15643437 -0.30521256 + -0.9876886 0.15643437 0 -0.93934768 0.15643437 0.30521256 -0.79905683 0.15643437 0.58054876 + -0.58054876 0.15643437 0.79905677 -0.30521256 0.15643437 0.93934757 -2.9435407e-08 0.15643437 0.98768848 + 0.30521247 0.15643437 0.93934757 0.58054864 0.15643437 0.79905671 0.79905665 0.15643437 0.5805487 + 0.93934751 0.15643437 0.3052125 0.98768836 0.15643437 0 0.90450913 0.30901697 -0.2938928 + 0.7694214 0.30901697 -0.55901736 0.55901736 0.30901697 -0.76942134 0.29389277 0.30901697 -0.90450901 + 0 0.30901697 -0.95105702 -0.29389277 0.30901697 -0.90450895 -0.55901724 0.30901697 -0.76942122 + -0.76942116 0.30901697 -0.55901718 -0.90450877 0.30901697 -0.29389271 -0.95105678 0.30901697 0 + -0.90450877 0.30901697 0.29389271 -0.7694211 0.30901697 0.55901712 -0.55901712 0.30901697 0.76942104 + -0.29389271 0.30901697 0.90450865 -2.8343694e-08 0.30901697 0.95105666 0.29389262 0.30901697 0.90450859 + 0.559017 0.30901697 0.76942098 0.76942092 0.30901697 0.55901706 0.90450853 0.30901697 0.29389265 + 0.95105654 0.30901697 0 0.8473981 0.45399052 -0.27533633 0.72083992 0.45399052 -0.5237208 + 0.5237208 0.45399052 -0.72083986 0.2753363 0.45399052 -0.84739798 0 0.45399052 -0.89100695 + -0.2753363 0.45399052 -0.84739798 -0.52372068 0.45399052 -0.72083968 -0.72083962 0.45399052 -0.52372062 + -0.8473978 0.45399052 -0.27533621 -0.89100677 0.45399052 0 -0.8473978 0.45399052 0.27533621 + -0.72083962 0.45399052 0.52372062 -0.52372062 0.45399052 0.72083956 -0.27533621 0.45399052 0.84739769 + -2.6554064e-08 0.45399052 0.89100665 0.27533615 0.45399052 0.84739763 0.5237205 0.45399052 0.7208395 + 0.72083944 0.45399052 0.52372056 0.84739757 0.45399052 0.27533618 0.89100653 0.45399052 0 + 0.7694214 0.58778524 -0.25000015 0.65450895 0.58778524 -0.47552854 0.47552854 0.58778524 -0.65450889 + 0.25000012 0.58778524 -0.76942128 0 0.58778524 -0.80901736 -0.25000012 0.58778524 -0.76942122 + -0.47552845 0.58778524 -0.65450877 -0.65450871 0.58778524 -0.47552839 -0.7694211 0.58778524 -0.25000006 + -0.80901718 0.58778524 0 -0.7694211 0.58778524 0.25000006 -0.65450865 0.58778524 0.47552836 + -0.47552836 0.58778524 0.65450859 -0.25000006 0.58778524 0.76942098 -2.4110586e-08 0.58778524 0.80901712 + 0.24999999 0.58778524 0.76942098 0.47552827 0.58778524 0.65450853 0.65450853 0.58778524 0.4755283 + 0.76942092 0.58778524 0.25 0.809017 0.58778524 0 0.67249894 0.70710677 -0.21850814 + 0.57206178 0.70710677 -0.41562718 0.41562718 0.70710677 -0.57206172 0.21850812 0.70710677 -0.67249888 + 0 0.70710677 -0.70710713 -0.21850812 0.70710677 -0.67249882 -0.41562709 0.70710677 -0.5720616 + -0.57206154 0.70710677 -0.41562706 -0.6724987 0.70710677 -0.21850805 -0.70710695 0.70710677 0 + -0.6724987 0.70710677 0.21850805 -0.57206154 0.70710677 0.415627 -0.415627 0.70710677 0.57206148 + -0.21850805 0.70710677 0.67249858 -2.1073424e-08 0.70710677 0.70710683 0.21850799 0.70710677 0.67249858 + 0.41562691 0.70710677 0.57206142 0.57206142 0.70710677 0.41562697 0.67249852 0.70710677 0.21850802 + 0.70710677 0.70710677 0 0.55901736 0.809017 -0.18163574 0.47552857 0.809017 -0.34549171 + 0.34549171 0.809017 -0.47552854 0.18163572 0.809017 -0.5590173 0 0.809017 -0.58778554 + -0.18163572 0.809017 -0.55901724 -0.34549165 0.809017 -0.47552842 -0.47552839 0.809017 -0.34549159 + -0.55901712 0.809017 -0.18163566 -0.58778536 0.809017 0 -0.55901712 0.809017 0.18163566 + -0.47552836 0.809017 0.34549156 -0.34549156 0.809017 0.47552833 -0.18163566 0.809017 0.55901706 + -1.7517365e-08 0.809017 0.5877853 0.18163562 0.809017 0.55901706 0.3454915 0.809017 0.4755283 + 0.47552827 0.809017 0.34549153 0.559017 0.809017 0.18163563 0.58778524 0.809017 0 + 0.43177092 0.89100653 -0.14029087 0.36728629 0.89100653 -0.2668491 0.2668491 0.89100653 -0.36728626 + 0.14029086 0.89100653 -0.43177086 0 0.89100653 -0.45399073 -0.14029086 0.89100653 -0.43177083 + -0.26684904 0.89100653 -0.36728618 -0.36728615 0.89100653 -0.26684901 -0.43177077 0.89100653 -0.14029081 + -0.45399064 0.89100653 0 -0.43177077 0.89100653 0.14029081 -0.36728612 0.89100653 0.26684898; + setAttr ".vt[332:381]" -0.26684898 0.89100653 0.36728612 -0.14029081 0.89100653 0.43177071 + -1.3529972e-08 0.89100653 0.45399058 0.14029078 0.89100653 0.43177068 0.26684892 0.89100653 0.36728609 + 0.36728606 0.89100653 0.26684895 0.43177065 0.89100653 0.1402908 0.45399052 0.89100653 0 + 0.29389283 0.95105654 -0.095491566 0.25000018 0.95105654 -0.18163574 0.18163574 0.95105654 -0.25000015 + 0.095491551 0.95105654 -0.2938928 0 0.95105654 -0.30901715 -0.095491551 0.95105654 -0.29389277 + -0.18163571 0.95105654 -0.25000009 -0.25000009 0.95105654 -0.18163569 -0.29389271 0.95105654 -0.095491529 + -0.30901706 0.95105654 0 -0.29389271 0.95105654 0.095491529 -0.25000006 0.95105654 0.18163568 + -0.18163568 0.95105654 0.25000006 -0.095491529 0.95105654 0.29389268 -9.2094243e-09 0.95105654 0.30901703 + 0.095491499 0.95105654 0.29389265 0.18163563 0.95105654 0.25000003 0.25 0.95105654 0.18163565 + 0.29389265 0.95105654 0.095491506 0.309017 0.95105654 0 0.14877813 0.98768836 -0.048340943 + 0.12655823 0.98768836 -0.091949932 0.091949932 0.98768836 -0.12655823 0.048340935 0.98768836 -0.14877811 + 0 0.98768836 -0.15643455 -0.048340935 0.98768836 -0.1487781 -0.091949917 0.98768836 -0.1265582 + -0.12655818 0.98768836 -0.091949902 -0.14877807 0.98768836 -0.048340924 -0.15643452 0.98768836 0 + -0.14877807 0.98768836 0.048340924 -0.12655818 0.98768836 0.091949895 -0.091949895 0.98768836 0.12655817 + -0.048340924 0.98768836 0.14877805 -4.6621107e-09 0.98768836 0.15643449 0.048340909 0.98768836 0.14877804 + 0.09194988 0.98768836 0.12655815 0.12655815 0.98768836 0.091949888 0.14877804 0.98768836 0.048340913 + 0.15643448 0.98768836 0 0 -1 0 0 1 0; + setAttr -s 780 ".ed"; + setAttr ".ed[0:165]" 0 1 1 1 2 1 2 3 1 3 4 1 4 5 1 5 6 1 6 7 1 7 8 1 8 9 1 + 9 10 1 10 11 1 11 12 1 12 13 1 13 14 1 14 15 1 15 16 1 16 17 1 17 18 1 18 19 1 19 0 1 + 20 21 1 21 22 1 22 23 1 23 24 1 24 25 1 25 26 1 26 27 1 27 28 1 28 29 1 29 30 1 30 31 1 + 31 32 1 32 33 1 33 34 1 34 35 1 35 36 1 36 37 1 37 38 1 38 39 1 39 20 1 40 41 1 41 42 1 + 42 43 1 43 44 1 44 45 1 45 46 1 46 47 1 47 48 1 48 49 1 49 50 1 50 51 1 51 52 1 52 53 1 + 53 54 1 54 55 1 55 56 1 56 57 1 57 58 1 58 59 1 59 40 1 60 61 1 61 62 1 62 63 1 63 64 1 + 64 65 1 65 66 1 66 67 1 67 68 1 68 69 1 69 70 1 70 71 1 71 72 1 72 73 1 73 74 1 74 75 1 + 75 76 1 76 77 1 77 78 1 78 79 1 79 60 1 80 81 1 81 82 1 82 83 1 83 84 1 84 85 1 85 86 1 + 86 87 1 87 88 1 88 89 1 89 90 1 90 91 1 91 92 1 92 93 1 93 94 1 94 95 1 95 96 1 96 97 1 + 97 98 1 98 99 1 99 80 1 100 101 1 101 102 1 102 103 1 103 104 1 104 105 1 105 106 1 + 106 107 1 107 108 1 108 109 1 109 110 1 110 111 1 111 112 1 112 113 1 113 114 1 114 115 1 + 115 116 1 116 117 1 117 118 1 118 119 1 119 100 1 120 121 1 121 122 1 122 123 1 123 124 1 + 124 125 1 125 126 1 126 127 1 127 128 1 128 129 1 129 130 1 130 131 1 131 132 1 132 133 1 + 133 134 1 134 135 1 135 136 1 136 137 1 137 138 1 138 139 1 139 120 1 140 141 1 141 142 1 + 142 143 1 143 144 1 144 145 1 145 146 1 146 147 1 147 148 1 148 149 1 149 150 1 150 151 1 + 151 152 1 152 153 1 153 154 1 154 155 1 155 156 1 156 157 1 157 158 1 158 159 1 159 140 1 + 160 161 1 161 162 1 162 163 1 163 164 1 164 165 1 165 166 1; + setAttr ".ed[166:331]" 166 167 1 167 168 1 168 169 1 169 170 1 170 171 1 171 172 1 + 172 173 1 173 174 1 174 175 1 175 176 1 176 177 1 177 178 1 178 179 1 179 160 1 180 181 1 + 181 182 1 182 183 1 183 184 1 184 185 1 185 186 1 186 187 1 187 188 1 188 189 1 189 190 1 + 190 191 1 191 192 1 192 193 1 193 194 1 194 195 1 195 196 1 196 197 1 197 198 1 198 199 1 + 199 180 1 200 201 1 201 202 1 202 203 1 203 204 1 204 205 1 205 206 1 206 207 1 207 208 1 + 208 209 1 209 210 1 210 211 1 211 212 1 212 213 1 213 214 1 214 215 1 215 216 1 216 217 1 + 217 218 1 218 219 1 219 200 1 220 221 1 221 222 1 222 223 1 223 224 1 224 225 1 225 226 1 + 226 227 1 227 228 1 228 229 1 229 230 1 230 231 1 231 232 1 232 233 1 233 234 1 234 235 1 + 235 236 1 236 237 1 237 238 1 238 239 1 239 220 1 240 241 1 241 242 1 242 243 1 243 244 1 + 244 245 1 245 246 1 246 247 1 247 248 1 248 249 1 249 250 1 250 251 1 251 252 1 252 253 1 + 253 254 1 254 255 1 255 256 1 256 257 1 257 258 1 258 259 1 259 240 1 260 261 1 261 262 1 + 262 263 1 263 264 1 264 265 1 265 266 1 266 267 1 267 268 1 268 269 1 269 270 1 270 271 1 + 271 272 1 272 273 1 273 274 1 274 275 1 275 276 1 276 277 1 277 278 1 278 279 1 279 260 1 + 280 281 1 281 282 1 282 283 1 283 284 1 284 285 1 285 286 1 286 287 1 287 288 1 288 289 1 + 289 290 1 290 291 1 291 292 1 292 293 1 293 294 1 294 295 1 295 296 1 296 297 1 297 298 1 + 298 299 1 299 280 1 300 301 1 301 302 1 302 303 1 303 304 1 304 305 1 305 306 1 306 307 1 + 307 308 1 308 309 1 309 310 1 310 311 1 311 312 1 312 313 1 313 314 1 314 315 1 315 316 1 + 316 317 1 317 318 1 318 319 1 319 300 1 320 321 1 321 322 1 322 323 1 323 324 1 324 325 1 + 325 326 1 326 327 1 327 328 1 328 329 1 329 330 1 330 331 1 331 332 1; + setAttr ".ed[332:497]" 332 333 1 333 334 1 334 335 1 335 336 1 336 337 1 337 338 1 + 338 339 1 339 320 1 340 341 1 341 342 1 342 343 1 343 344 1 344 345 1 345 346 1 346 347 1 + 347 348 1 348 349 1 349 350 1 350 351 1 351 352 1 352 353 1 353 354 1 354 355 1 355 356 1 + 356 357 1 357 358 1 358 359 1 359 340 1 360 361 1 361 362 1 362 363 1 363 364 1 364 365 1 + 365 366 1 366 367 1 367 368 1 368 369 1 369 370 1 370 371 1 371 372 1 372 373 1 373 374 1 + 374 375 1 375 376 1 376 377 1 377 378 1 378 379 1 379 360 1 0 20 1 1 21 1 2 22 1 + 3 23 1 4 24 1 5 25 1 6 26 1 7 27 1 8 28 1 9 29 1 10 30 1 11 31 1 12 32 1 13 33 1 + 14 34 1 15 35 1 16 36 1 17 37 1 18 38 1 19 39 1 20 40 1 21 41 1 22 42 1 23 43 1 24 44 1 + 25 45 1 26 46 1 27 47 1 28 48 1 29 49 1 30 50 1 31 51 1 32 52 1 33 53 1 34 54 1 35 55 1 + 36 56 1 37 57 1 38 58 1 39 59 1 40 60 1 41 61 1 42 62 1 43 63 1 44 64 1 45 65 1 46 66 1 + 47 67 1 48 68 1 49 69 1 50 70 1 51 71 1 52 72 1 53 73 1 54 74 1 55 75 1 56 76 1 57 77 1 + 58 78 1 59 79 1 60 80 1 61 81 1 62 82 1 63 83 1 64 84 1 65 85 1 66 86 1 67 87 1 68 88 1 + 69 89 1 70 90 1 71 91 1 72 92 1 73 93 1 74 94 1 75 95 1 76 96 1 77 97 1 78 98 1 79 99 1 + 80 100 1 81 101 1 82 102 1 83 103 1 84 104 1 85 105 1 86 106 1 87 107 1 88 108 1 + 89 109 1 90 110 1 91 111 1 92 112 1 93 113 1 94 114 1 95 115 1 96 116 1 97 117 1 + 98 118 1 99 119 1 100 120 1 101 121 1 102 122 1 103 123 1 104 124 1 105 125 1 106 126 1 + 107 127 1 108 128 1 109 129 1 110 130 1 111 131 1 112 132 1 113 133 1 114 134 1 115 135 1 + 116 136 1 117 137 1; + setAttr ".ed[498:663]" 118 138 1 119 139 1 120 140 1 121 141 1 122 142 1 123 143 1 + 124 144 1 125 145 1 126 146 1 127 147 1 128 148 1 129 149 1 130 150 1 131 151 1 132 152 1 + 133 153 1 134 154 1 135 155 1 136 156 1 137 157 1 138 158 1 139 159 1 140 160 1 141 161 1 + 142 162 1 143 163 1 144 164 1 145 165 1 146 166 1 147 167 1 148 168 1 149 169 1 150 170 1 + 151 171 1 152 172 1 153 173 1 154 174 1 155 175 1 156 176 1 157 177 1 158 178 1 159 179 1 + 160 180 1 161 181 1 162 182 1 163 183 1 164 184 1 165 185 1 166 186 1 167 187 1 168 188 1 + 169 189 1 170 190 1 171 191 1 172 192 1 173 193 1 174 194 1 175 195 1 176 196 1 177 197 1 + 178 198 1 179 199 1 180 200 1 181 201 1 182 202 1 183 203 1 184 204 1 185 205 1 186 206 1 + 187 207 1 188 208 1 189 209 1 190 210 1 191 211 1 192 212 1 193 213 1 194 214 1 195 215 1 + 196 216 1 197 217 1 198 218 1 199 219 1 200 220 1 201 221 1 202 222 1 203 223 1 204 224 1 + 205 225 1 206 226 1 207 227 1 208 228 1 209 229 1 210 230 1 211 231 1 212 232 1 213 233 1 + 214 234 1 215 235 1 216 236 1 217 237 1 218 238 1 219 239 1 220 240 1 221 241 1 222 242 1 + 223 243 1 224 244 1 225 245 1 226 246 1 227 247 1 228 248 1 229 249 1 230 250 1 231 251 1 + 232 252 1 233 253 1 234 254 1 235 255 1 236 256 1 237 257 1 238 258 1 239 259 1 240 260 1 + 241 261 1 242 262 1 243 263 1 244 264 1 245 265 1 246 266 1 247 267 1 248 268 1 249 269 1 + 250 270 1 251 271 1 252 272 1 253 273 1 254 274 1 255 275 1 256 276 1 257 277 1 258 278 1 + 259 279 1 260 280 1 261 281 1 262 282 1 263 283 1 264 284 1 265 285 1 266 286 1 267 287 1 + 268 288 1 269 289 1 270 290 1 271 291 1 272 292 1 273 293 1 274 294 1 275 295 1 276 296 1 + 277 297 1 278 298 1 279 299 1 280 300 1 281 301 1 282 302 1 283 303 1; + setAttr ".ed[664:779]" 284 304 1 285 305 1 286 306 1 287 307 1 288 308 1 289 309 1 + 290 310 1 291 311 1 292 312 1 293 313 1 294 314 1 295 315 1 296 316 1 297 317 1 298 318 1 + 299 319 1 300 320 1 301 321 1 302 322 1 303 323 1 304 324 1 305 325 1 306 326 1 307 327 1 + 308 328 1 309 329 1 310 330 1 311 331 1 312 332 1 313 333 1 314 334 1 315 335 1 316 336 1 + 317 337 1 318 338 1 319 339 1 320 340 1 321 341 1 322 342 1 323 343 1 324 344 1 325 345 1 + 326 346 1 327 347 1 328 348 1 329 349 1 330 350 1 331 351 1 332 352 1 333 353 1 334 354 1 + 335 355 1 336 356 1 337 357 1 338 358 1 339 359 1 340 360 1 341 361 1 342 362 1 343 363 1 + 344 364 1 345 365 1 346 366 1 347 367 1 348 368 1 349 369 1 350 370 1 351 371 1 352 372 1 + 353 373 1 354 374 1 355 375 1 356 376 1 357 377 1 358 378 1 359 379 1 380 0 1 380 1 1 + 380 2 1 380 3 1 380 4 1 380 5 1 380 6 1 380 7 1 380 8 1 380 9 1 380 10 1 380 11 1 + 380 12 1 380 13 1 380 14 1 380 15 1 380 16 1 380 17 1 380 18 1 380 19 1 360 381 1 + 361 381 1 362 381 1 363 381 1 364 381 1 365 381 1 366 381 1 367 381 1 368 381 1 369 381 1 + 370 381 1 371 381 1 372 381 1 373 381 1 374 381 1 375 381 1 376 381 1 377 381 1 378 381 1 + 379 381 1; + setAttr -s 400 -ch 1560 ".fc[0:399]" -type "polyFaces" + f 4 0 381 -21 -381 + mu 0 4 0 1 22 21 + f 4 1 382 -22 -382 + mu 0 4 1 2 23 22 + f 4 2 383 -23 -383 + mu 0 4 2 3 24 23 + f 4 3 384 -24 -384 + mu 0 4 3 4 25 24 + f 4 4 385 -25 -385 + mu 0 4 4 5 26 25 + f 4 5 386 -26 -386 + mu 0 4 5 6 27 26 + f 4 6 387 -27 -387 + mu 0 4 6 7 28 27 + f 4 7 388 -28 -388 + mu 0 4 7 8 29 28 + f 4 8 389 -29 -389 + mu 0 4 8 9 30 29 + f 4 9 390 -30 -390 + mu 0 4 9 10 31 30 + f 4 10 391 -31 -391 + mu 0 4 10 11 32 31 + f 4 11 392 -32 -392 + mu 0 4 11 12 33 32 + f 4 12 393 -33 -393 + mu 0 4 12 13 34 33 + f 4 13 394 -34 -394 + mu 0 4 13 14 35 34 + f 4 14 395 -35 -395 + mu 0 4 14 15 36 35 + f 4 15 396 -36 -396 + mu 0 4 15 16 37 36 + f 4 16 397 -37 -397 + mu 0 4 16 17 38 37 + f 4 17 398 -38 -398 + mu 0 4 17 18 39 38 + f 4 18 399 -39 -399 + mu 0 4 18 19 40 39 + f 4 19 380 -40 -400 + mu 0 4 19 20 41 40 + f 4 20 401 -41 -401 + mu 0 4 21 22 43 42 + f 4 21 402 -42 -402 + mu 0 4 22 23 44 43 + f 4 22 403 -43 -403 + mu 0 4 23 24 45 44 + f 4 23 404 -44 -404 + mu 0 4 24 25 46 45 + f 4 24 405 -45 -405 + mu 0 4 25 26 47 46 + f 4 25 406 -46 -406 + mu 0 4 26 27 48 47 + f 4 26 407 -47 -407 + mu 0 4 27 28 49 48 + f 4 27 408 -48 -408 + mu 0 4 28 29 50 49 + f 4 28 409 -49 -409 + mu 0 4 29 30 51 50 + f 4 29 410 -50 -410 + mu 0 4 30 31 52 51 + f 4 30 411 -51 -411 + mu 0 4 31 32 53 52 + f 4 31 412 -52 -412 + mu 0 4 32 33 54 53 + f 4 32 413 -53 -413 + mu 0 4 33 34 55 54 + f 4 33 414 -54 -414 + mu 0 4 34 35 56 55 + f 4 34 415 -55 -415 + mu 0 4 35 36 57 56 + f 4 35 416 -56 -416 + mu 0 4 36 37 58 57 + f 4 36 417 -57 -417 + mu 0 4 37 38 59 58 + f 4 37 418 -58 -418 + mu 0 4 38 39 60 59 + f 4 38 419 -59 -419 + mu 0 4 39 40 61 60 + f 4 39 400 -60 -420 + mu 0 4 40 41 62 61 + f 4 40 421 -61 -421 + mu 0 4 42 43 64 63 + f 4 41 422 -62 -422 + mu 0 4 43 44 65 64 + f 4 42 423 -63 -423 + mu 0 4 44 45 66 65 + f 4 43 424 -64 -424 + mu 0 4 45 46 67 66 + f 4 44 425 -65 -425 + mu 0 4 46 47 68 67 + f 4 45 426 -66 -426 + mu 0 4 47 48 69 68 + f 4 46 427 -67 -427 + mu 0 4 48 49 70 69 + f 4 47 428 -68 -428 + mu 0 4 49 50 71 70 + f 4 48 429 -69 -429 + mu 0 4 50 51 72 71 + f 4 49 430 -70 -430 + mu 0 4 51 52 73 72 + f 4 50 431 -71 -431 + mu 0 4 52 53 74 73 + f 4 51 432 -72 -432 + mu 0 4 53 54 75 74 + f 4 52 433 -73 -433 + mu 0 4 54 55 76 75 + f 4 53 434 -74 -434 + mu 0 4 55 56 77 76 + f 4 54 435 -75 -435 + mu 0 4 56 57 78 77 + f 4 55 436 -76 -436 + mu 0 4 57 58 79 78 + f 4 56 437 -77 -437 + mu 0 4 58 59 80 79 + f 4 57 438 -78 -438 + mu 0 4 59 60 81 80 + f 4 58 439 -79 -439 + mu 0 4 60 61 82 81 + f 4 59 420 -80 -440 + mu 0 4 61 62 83 82 + f 4 60 441 -81 -441 + mu 0 4 63 64 85 84 + f 4 61 442 -82 -442 + mu 0 4 64 65 86 85 + f 4 62 443 -83 -443 + mu 0 4 65 66 87 86 + f 4 63 444 -84 -444 + mu 0 4 66 67 88 87 + f 4 64 445 -85 -445 + mu 0 4 67 68 89 88 + f 4 65 446 -86 -446 + mu 0 4 68 69 90 89 + f 4 66 447 -87 -447 + mu 0 4 69 70 91 90 + f 4 67 448 -88 -448 + mu 0 4 70 71 92 91 + f 4 68 449 -89 -449 + mu 0 4 71 72 93 92 + f 4 69 450 -90 -450 + mu 0 4 72 73 94 93 + f 4 70 451 -91 -451 + mu 0 4 73 74 95 94 + f 4 71 452 -92 -452 + mu 0 4 74 75 96 95 + f 4 72 453 -93 -453 + mu 0 4 75 76 97 96 + f 4 73 454 -94 -454 + mu 0 4 76 77 98 97 + f 4 74 455 -95 -455 + mu 0 4 77 78 99 98 + f 4 75 456 -96 -456 + mu 0 4 78 79 100 99 + f 4 76 457 -97 -457 + mu 0 4 79 80 101 100 + f 4 77 458 -98 -458 + mu 0 4 80 81 102 101 + f 4 78 459 -99 -459 + mu 0 4 81 82 103 102 + f 4 79 440 -100 -460 + mu 0 4 82 83 104 103 + f 4 80 461 -101 -461 + mu 0 4 84 85 106 105 + f 4 81 462 -102 -462 + mu 0 4 85 86 107 106 + f 4 82 463 -103 -463 + mu 0 4 86 87 108 107 + f 4 83 464 -104 -464 + mu 0 4 87 88 109 108 + f 4 84 465 -105 -465 + mu 0 4 88 89 110 109 + f 4 85 466 -106 -466 + mu 0 4 89 90 111 110 + f 4 86 467 -107 -467 + mu 0 4 90 91 112 111 + f 4 87 468 -108 -468 + mu 0 4 91 92 113 112 + f 4 88 469 -109 -469 + mu 0 4 92 93 114 113 + f 4 89 470 -110 -470 + mu 0 4 93 94 115 114 + f 4 90 471 -111 -471 + mu 0 4 94 95 116 115 + f 4 91 472 -112 -472 + mu 0 4 95 96 117 116 + f 4 92 473 -113 -473 + mu 0 4 96 97 118 117 + f 4 93 474 -114 -474 + mu 0 4 97 98 119 118 + f 4 94 475 -115 -475 + mu 0 4 98 99 120 119 + f 4 95 476 -116 -476 + mu 0 4 99 100 121 120 + f 4 96 477 -117 -477 + mu 0 4 100 101 122 121 + f 4 97 478 -118 -478 + mu 0 4 101 102 123 122 + f 4 98 479 -119 -479 + mu 0 4 102 103 124 123 + f 4 99 460 -120 -480 + mu 0 4 103 104 125 124 + f 4 100 481 -121 -481 + mu 0 4 105 106 127 126 + f 4 101 482 -122 -482 + mu 0 4 106 107 128 127 + f 4 102 483 -123 -483 + mu 0 4 107 108 129 128 + f 4 103 484 -124 -484 + mu 0 4 108 109 130 129 + f 4 104 485 -125 -485 + mu 0 4 109 110 131 130 + f 4 105 486 -126 -486 + mu 0 4 110 111 132 131 + f 4 106 487 -127 -487 + mu 0 4 111 112 133 132 + f 4 107 488 -128 -488 + mu 0 4 112 113 134 133 + f 4 108 489 -129 -489 + mu 0 4 113 114 135 134 + f 4 109 490 -130 -490 + mu 0 4 114 115 136 135 + f 4 110 491 -131 -491 + mu 0 4 115 116 137 136 + f 4 111 492 -132 -492 + mu 0 4 116 117 138 137 + f 4 112 493 -133 -493 + mu 0 4 117 118 139 138 + f 4 113 494 -134 -494 + mu 0 4 118 119 140 139 + f 4 114 495 -135 -495 + mu 0 4 119 120 141 140 + f 4 115 496 -136 -496 + mu 0 4 120 121 142 141 + f 4 116 497 -137 -497 + mu 0 4 121 122 143 142 + f 4 117 498 -138 -498 + mu 0 4 122 123 144 143 + f 4 118 499 -139 -499 + mu 0 4 123 124 145 144 + f 4 119 480 -140 -500 + mu 0 4 124 125 146 145 + f 4 120 501 -141 -501 + mu 0 4 126 127 148 147 + f 4 121 502 -142 -502 + mu 0 4 127 128 149 148 + f 4 122 503 -143 -503 + mu 0 4 128 129 150 149 + f 4 123 504 -144 -504 + mu 0 4 129 130 151 150 + f 4 124 505 -145 -505 + mu 0 4 130 131 152 151 + f 4 125 506 -146 -506 + mu 0 4 131 132 153 152 + f 4 126 507 -147 -507 + mu 0 4 132 133 154 153 + f 4 127 508 -148 -508 + mu 0 4 133 134 155 154 + f 4 128 509 -149 -509 + mu 0 4 134 135 156 155 + f 4 129 510 -150 -510 + mu 0 4 135 136 157 156 + f 4 130 511 -151 -511 + mu 0 4 136 137 158 157 + f 4 131 512 -152 -512 + mu 0 4 137 138 159 158 + f 4 132 513 -153 -513 + mu 0 4 138 139 160 159 + f 4 133 514 -154 -514 + mu 0 4 139 140 161 160 + f 4 134 515 -155 -515 + mu 0 4 140 141 162 161 + f 4 135 516 -156 -516 + mu 0 4 141 142 163 162 + f 4 136 517 -157 -517 + mu 0 4 142 143 164 163 + f 4 137 518 -158 -518 + mu 0 4 143 144 165 164 + f 4 138 519 -159 -519 + mu 0 4 144 145 166 165 + f 4 139 500 -160 -520 + mu 0 4 145 146 167 166 + f 4 140 521 -161 -521 + mu 0 4 147 148 169 168 + f 4 141 522 -162 -522 + mu 0 4 148 149 170 169 + f 4 142 523 -163 -523 + mu 0 4 149 150 171 170 + f 4 143 524 -164 -524 + mu 0 4 150 151 172 171 + f 4 144 525 -165 -525 + mu 0 4 151 152 173 172 + f 4 145 526 -166 -526 + mu 0 4 152 153 174 173 + f 4 146 527 -167 -527 + mu 0 4 153 154 175 174 + f 4 147 528 -168 -528 + mu 0 4 154 155 176 175 + f 4 148 529 -169 -529 + mu 0 4 155 156 177 176 + f 4 149 530 -170 -530 + mu 0 4 156 157 178 177 + f 4 150 531 -171 -531 + mu 0 4 157 158 179 178 + f 4 151 532 -172 -532 + mu 0 4 158 159 180 179 + f 4 152 533 -173 -533 + mu 0 4 159 160 181 180 + f 4 153 534 -174 -534 + mu 0 4 160 161 182 181 + f 4 154 535 -175 -535 + mu 0 4 161 162 183 182 + f 4 155 536 -176 -536 + mu 0 4 162 163 184 183 + f 4 156 537 -177 -537 + mu 0 4 163 164 185 184 + f 4 157 538 -178 -538 + mu 0 4 164 165 186 185 + f 4 158 539 -179 -539 + mu 0 4 165 166 187 186 + f 4 159 520 -180 -540 + mu 0 4 166 167 188 187 + f 4 160 541 -181 -541 + mu 0 4 168 169 190 189 + f 4 161 542 -182 -542 + mu 0 4 169 170 191 190 + f 4 162 543 -183 -543 + mu 0 4 170 171 192 191 + f 4 163 544 -184 -544 + mu 0 4 171 172 193 192 + f 4 164 545 -185 -545 + mu 0 4 172 173 194 193 + f 4 165 546 -186 -546 + mu 0 4 173 174 195 194 + f 4 166 547 -187 -547 + mu 0 4 174 175 196 195 + f 4 167 548 -188 -548 + mu 0 4 175 176 197 196 + f 4 168 549 -189 -549 + mu 0 4 176 177 198 197 + f 4 169 550 -190 -550 + mu 0 4 177 178 199 198 + f 4 170 551 -191 -551 + mu 0 4 178 179 200 199 + f 4 171 552 -192 -552 + mu 0 4 179 180 201 200 + f 4 172 553 -193 -553 + mu 0 4 180 181 202 201 + f 4 173 554 -194 -554 + mu 0 4 181 182 203 202 + f 4 174 555 -195 -555 + mu 0 4 182 183 204 203 + f 4 175 556 -196 -556 + mu 0 4 183 184 205 204 + f 4 176 557 -197 -557 + mu 0 4 184 185 206 205 + f 4 177 558 -198 -558 + mu 0 4 185 186 207 206 + f 4 178 559 -199 -559 + mu 0 4 186 187 208 207 + f 4 179 540 -200 -560 + mu 0 4 187 188 209 208 + f 4 180 561 -201 -561 + mu 0 4 189 190 211 210 + f 4 181 562 -202 -562 + mu 0 4 190 191 212 211 + f 4 182 563 -203 -563 + mu 0 4 191 192 213 212 + f 4 183 564 -204 -564 + mu 0 4 192 193 214 213 + f 4 184 565 -205 -565 + mu 0 4 193 194 215 214 + f 4 185 566 -206 -566 + mu 0 4 194 195 216 215 + f 4 186 567 -207 -567 + mu 0 4 195 196 217 216 + f 4 187 568 -208 -568 + mu 0 4 196 197 218 217 + f 4 188 569 -209 -569 + mu 0 4 197 198 219 218 + f 4 189 570 -210 -570 + mu 0 4 198 199 220 219 + f 4 190 571 -211 -571 + mu 0 4 199 200 221 220 + f 4 191 572 -212 -572 + mu 0 4 200 201 222 221 + f 4 192 573 -213 -573 + mu 0 4 201 202 223 222 + f 4 193 574 -214 -574 + mu 0 4 202 203 224 223 + f 4 194 575 -215 -575 + mu 0 4 203 204 225 224 + f 4 195 576 -216 -576 + mu 0 4 204 205 226 225 + f 4 196 577 -217 -577 + mu 0 4 205 206 227 226 + f 4 197 578 -218 -578 + mu 0 4 206 207 228 227 + f 4 198 579 -219 -579 + mu 0 4 207 208 229 228 + f 4 199 560 -220 -580 + mu 0 4 208 209 230 229 + f 4 200 581 -221 -581 + mu 0 4 210 211 232 231 + f 4 201 582 -222 -582 + mu 0 4 211 212 233 232 + f 4 202 583 -223 -583 + mu 0 4 212 213 234 233 + f 4 203 584 -224 -584 + mu 0 4 213 214 235 234 + f 4 204 585 -225 -585 + mu 0 4 214 215 236 235 + f 4 205 586 -226 -586 + mu 0 4 215 216 237 236 + f 4 206 587 -227 -587 + mu 0 4 216 217 238 237 + f 4 207 588 -228 -588 + mu 0 4 217 218 239 238 + f 4 208 589 -229 -589 + mu 0 4 218 219 240 239 + f 4 209 590 -230 -590 + mu 0 4 219 220 241 240 + f 4 210 591 -231 -591 + mu 0 4 220 221 242 241 + f 4 211 592 -232 -592 + mu 0 4 221 222 243 242 + f 4 212 593 -233 -593 + mu 0 4 222 223 244 243 + f 4 213 594 -234 -594 + mu 0 4 223 224 245 244 + f 4 214 595 -235 -595 + mu 0 4 224 225 246 245 + f 4 215 596 -236 -596 + mu 0 4 225 226 247 246 + f 4 216 597 -237 -597 + mu 0 4 226 227 248 247 + f 4 217 598 -238 -598 + mu 0 4 227 228 249 248 + f 4 218 599 -239 -599 + mu 0 4 228 229 250 249 + f 4 219 580 -240 -600 + mu 0 4 229 230 251 250 + f 4 220 601 -241 -601 + mu 0 4 231 232 253 252 + f 4 221 602 -242 -602 + mu 0 4 232 233 254 253 + f 4 222 603 -243 -603 + mu 0 4 233 234 255 254 + f 4 223 604 -244 -604 + mu 0 4 234 235 256 255 + f 4 224 605 -245 -605 + mu 0 4 235 236 257 256 + f 4 225 606 -246 -606 + mu 0 4 236 237 258 257 + f 4 226 607 -247 -607 + mu 0 4 237 238 259 258 + f 4 227 608 -248 -608 + mu 0 4 238 239 260 259 + f 4 228 609 -249 -609 + mu 0 4 239 240 261 260 + f 4 229 610 -250 -610 + mu 0 4 240 241 262 261 + f 4 230 611 -251 -611 + mu 0 4 241 242 263 262 + f 4 231 612 -252 -612 + mu 0 4 242 243 264 263 + f 4 232 613 -253 -613 + mu 0 4 243 244 265 264 + f 4 233 614 -254 -614 + mu 0 4 244 245 266 265 + f 4 234 615 -255 -615 + mu 0 4 245 246 267 266 + f 4 235 616 -256 -616 + mu 0 4 246 247 268 267 + f 4 236 617 -257 -617 + mu 0 4 247 248 269 268 + f 4 237 618 -258 -618 + mu 0 4 248 249 270 269 + f 4 238 619 -259 -619 + mu 0 4 249 250 271 270 + f 4 239 600 -260 -620 + mu 0 4 250 251 272 271 + f 4 240 621 -261 -621 + mu 0 4 252 253 274 273 + f 4 241 622 -262 -622 + mu 0 4 253 254 275 274 + f 4 242 623 -263 -623 + mu 0 4 254 255 276 275 + f 4 243 624 -264 -624 + mu 0 4 255 256 277 276 + f 4 244 625 -265 -625 + mu 0 4 256 257 278 277 + f 4 245 626 -266 -626 + mu 0 4 257 258 279 278 + f 4 246 627 -267 -627 + mu 0 4 258 259 280 279 + f 4 247 628 -268 -628 + mu 0 4 259 260 281 280 + f 4 248 629 -269 -629 + mu 0 4 260 261 282 281 + f 4 249 630 -270 -630 + mu 0 4 261 262 283 282 + f 4 250 631 -271 -631 + mu 0 4 262 263 284 283 + f 4 251 632 -272 -632 + mu 0 4 263 264 285 284 + f 4 252 633 -273 -633 + mu 0 4 264 265 286 285 + f 4 253 634 -274 -634 + mu 0 4 265 266 287 286 + f 4 254 635 -275 -635 + mu 0 4 266 267 288 287 + f 4 255 636 -276 -636 + mu 0 4 267 268 289 288 + f 4 256 637 -277 -637 + mu 0 4 268 269 290 289 + f 4 257 638 -278 -638 + mu 0 4 269 270 291 290 + f 4 258 639 -279 -639 + mu 0 4 270 271 292 291 + f 4 259 620 -280 -640 + mu 0 4 271 272 293 292 + f 4 260 641 -281 -641 + mu 0 4 273 274 295 294 + f 4 261 642 -282 -642 + mu 0 4 274 275 296 295 + f 4 262 643 -283 -643 + mu 0 4 275 276 297 296 + f 4 263 644 -284 -644 + mu 0 4 276 277 298 297 + f 4 264 645 -285 -645 + mu 0 4 277 278 299 298 + f 4 265 646 -286 -646 + mu 0 4 278 279 300 299 + f 4 266 647 -287 -647 + mu 0 4 279 280 301 300 + f 4 267 648 -288 -648 + mu 0 4 280 281 302 301 + f 4 268 649 -289 -649 + mu 0 4 281 282 303 302 + f 4 269 650 -290 -650 + mu 0 4 282 283 304 303 + f 4 270 651 -291 -651 + mu 0 4 283 284 305 304 + f 4 271 652 -292 -652 + mu 0 4 284 285 306 305 + f 4 272 653 -293 -653 + mu 0 4 285 286 307 306 + f 4 273 654 -294 -654 + mu 0 4 286 287 308 307 + f 4 274 655 -295 -655 + mu 0 4 287 288 309 308 + f 4 275 656 -296 -656 + mu 0 4 288 289 310 309 + f 4 276 657 -297 -657 + mu 0 4 289 290 311 310 + f 4 277 658 -298 -658 + mu 0 4 290 291 312 311 + f 4 278 659 -299 -659 + mu 0 4 291 292 313 312 + f 4 279 640 -300 -660 + mu 0 4 292 293 314 313 + f 4 280 661 -301 -661 + mu 0 4 294 295 316 315 + f 4 281 662 -302 -662 + mu 0 4 295 296 317 316 + f 4 282 663 -303 -663 + mu 0 4 296 297 318 317 + f 4 283 664 -304 -664 + mu 0 4 297 298 319 318 + f 4 284 665 -305 -665 + mu 0 4 298 299 320 319 + f 4 285 666 -306 -666 + mu 0 4 299 300 321 320 + f 4 286 667 -307 -667 + mu 0 4 300 301 322 321 + f 4 287 668 -308 -668 + mu 0 4 301 302 323 322 + f 4 288 669 -309 -669 + mu 0 4 302 303 324 323 + f 4 289 670 -310 -670 + mu 0 4 303 304 325 324 + f 4 290 671 -311 -671 + mu 0 4 304 305 326 325 + f 4 291 672 -312 -672 + mu 0 4 305 306 327 326 + f 4 292 673 -313 -673 + mu 0 4 306 307 328 327 + f 4 293 674 -314 -674 + mu 0 4 307 308 329 328 + f 4 294 675 -315 -675 + mu 0 4 308 309 330 329 + f 4 295 676 -316 -676 + mu 0 4 309 310 331 330 + f 4 296 677 -317 -677 + mu 0 4 310 311 332 331 + f 4 297 678 -318 -678 + mu 0 4 311 312 333 332 + f 4 298 679 -319 -679 + mu 0 4 312 313 334 333 + f 4 299 660 -320 -680 + mu 0 4 313 314 335 334 + f 4 300 681 -321 -681 + mu 0 4 315 316 337 336 + f 4 301 682 -322 -682 + mu 0 4 316 317 338 337 + f 4 302 683 -323 -683 + mu 0 4 317 318 339 338 + f 4 303 684 -324 -684 + mu 0 4 318 319 340 339 + f 4 304 685 -325 -685 + mu 0 4 319 320 341 340 + f 4 305 686 -326 -686 + mu 0 4 320 321 342 341 + f 4 306 687 -327 -687 + mu 0 4 321 322 343 342 + f 4 307 688 -328 -688 + mu 0 4 322 323 344 343 + f 4 308 689 -329 -689 + mu 0 4 323 324 345 344 + f 4 309 690 -330 -690 + mu 0 4 324 325 346 345 + f 4 310 691 -331 -691 + mu 0 4 325 326 347 346 + f 4 311 692 -332 -692 + mu 0 4 326 327 348 347 + f 4 312 693 -333 -693 + mu 0 4 327 328 349 348 + f 4 313 694 -334 -694 + mu 0 4 328 329 350 349 + f 4 314 695 -335 -695 + mu 0 4 329 330 351 350 + f 4 315 696 -336 -696 + mu 0 4 330 331 352 351 + f 4 316 697 -337 -697 + mu 0 4 331 332 353 352 + f 4 317 698 -338 -698 + mu 0 4 332 333 354 353 + f 4 318 699 -339 -699 + mu 0 4 333 334 355 354 + f 4 319 680 -340 -700 + mu 0 4 334 335 356 355 + f 4 320 701 -341 -701 + mu 0 4 336 337 358 357 + f 4 321 702 -342 -702 + mu 0 4 337 338 359 358 + f 4 322 703 -343 -703 + mu 0 4 338 339 360 359 + f 4 323 704 -344 -704 + mu 0 4 339 340 361 360 + f 4 324 705 -345 -705 + mu 0 4 340 341 362 361 + f 4 325 706 -346 -706 + mu 0 4 341 342 363 362 + f 4 326 707 -347 -707 + mu 0 4 342 343 364 363 + f 4 327 708 -348 -708 + mu 0 4 343 344 365 364 + f 4 328 709 -349 -709 + mu 0 4 344 345 366 365 + f 4 329 710 -350 -710 + mu 0 4 345 346 367 366 + f 4 330 711 -351 -711 + mu 0 4 346 347 368 367 + f 4 331 712 -352 -712 + mu 0 4 347 348 369 368 + f 4 332 713 -353 -713 + mu 0 4 348 349 370 369 + f 4 333 714 -354 -714 + mu 0 4 349 350 371 370 + f 4 334 715 -355 -715 + mu 0 4 350 351 372 371 + f 4 335 716 -356 -716 + mu 0 4 351 352 373 372 + f 4 336 717 -357 -717 + mu 0 4 352 353 374 373 + f 4 337 718 -358 -718 + mu 0 4 353 354 375 374 + f 4 338 719 -359 -719 + mu 0 4 354 355 376 375 + f 4 339 700 -360 -720 + mu 0 4 355 356 377 376 + f 4 340 721 -361 -721 + mu 0 4 357 358 379 378 + f 4 341 722 -362 -722 + mu 0 4 358 359 380 379 + f 4 342 723 -363 -723 + mu 0 4 359 360 381 380 + f 4 343 724 -364 -724 + mu 0 4 360 361 382 381 + f 4 344 725 -365 -725 + mu 0 4 361 362 383 382 + f 4 345 726 -366 -726 + mu 0 4 362 363 384 383 + f 4 346 727 -367 -727 + mu 0 4 363 364 385 384 + f 4 347 728 -368 -728 + mu 0 4 364 365 386 385 + f 4 348 729 -369 -729 + mu 0 4 365 366 387 386 + f 4 349 730 -370 -730 + mu 0 4 366 367 388 387 + f 4 350 731 -371 -731 + mu 0 4 367 368 389 388 + f 4 351 732 -372 -732 + mu 0 4 368 369 390 389 + f 4 352 733 -373 -733 + mu 0 4 369 370 391 390 + f 4 353 734 -374 -734 + mu 0 4 370 371 392 391 + f 4 354 735 -375 -735 + mu 0 4 371 372 393 392 + f 4 355 736 -376 -736 + mu 0 4 372 373 394 393 + f 4 356 737 -377 -737 + mu 0 4 373 374 395 394 + f 4 357 738 -378 -738 + mu 0 4 374 375 396 395 + f 4 358 739 -379 -739 + mu 0 4 375 376 397 396 + f 4 359 720 -380 -740 + mu 0 4 376 377 398 397 + f 3 -1 -741 741 + mu 0 3 1 0 399 + f 3 -2 -742 742 + mu 0 3 2 1 400 + f 3 -3 -743 743 + mu 0 3 3 2 401 + f 3 -4 -744 744 + mu 0 3 4 3 402 + f 3 -5 -745 745 + mu 0 3 5 4 403 + f 3 -6 -746 746 + mu 0 3 6 5 404 + f 3 -7 -747 747 + mu 0 3 7 6 405 + f 3 -8 -748 748 + mu 0 3 8 7 406 + f 3 -9 -749 749 + mu 0 3 9 8 407 + f 3 -10 -750 750 + mu 0 3 10 9 408 + f 3 -11 -751 751 + mu 0 3 11 10 409 + f 3 -12 -752 752 + mu 0 3 12 11 410 + f 3 -13 -753 753 + mu 0 3 13 12 411 + f 3 -14 -754 754 + mu 0 3 14 13 412 + f 3 -15 -755 755 + mu 0 3 15 14 413 + f 3 -16 -756 756 + mu 0 3 16 15 414 + f 3 -17 -757 757 + mu 0 3 17 16 415 + f 3 -18 -758 758 + mu 0 3 18 17 416 + f 3 -19 -759 759 + mu 0 3 19 18 417 + f 3 -20 -760 740 + mu 0 3 20 19 418 + f 3 360 761 -761 + mu 0 3 378 379 419 + f 3 361 762 -762 + mu 0 3 379 380 420 + f 3 362 763 -763 + mu 0 3 380 381 421 + f 3 363 764 -764 + mu 0 3 381 382 422 + f 3 364 765 -765 + mu 0 3 382 383 423 + f 3 365 766 -766 + mu 0 3 383 384 424 + f 3 366 767 -767 + mu 0 3 384 385 425 + f 3 367 768 -768 + mu 0 3 385 386 426 + f 3 368 769 -769 + mu 0 3 386 387 427 + f 3 369 770 -770 + mu 0 3 387 388 428 + f 3 370 771 -771 + mu 0 3 388 389 429 + f 3 371 772 -772 + mu 0 3 389 390 430 + f 3 372 773 -773 + mu 0 3 390 391 431 + f 3 373 774 -774 + mu 0 3 391 392 432 + f 3 374 775 -775 + mu 0 3 392 393 433 + f 3 375 776 -776 + mu 0 3 393 394 434 + f 3 376 777 -777 + mu 0 3 394 395 435 + f 3 377 778 -778 + mu 0 3 395 396 436 + f 3 378 779 -779 + mu 0 3 396 397 437 + f 3 379 760 -780 + mu 0 3 397 398 438; + setAttr ".cd" -type "dataPolyComponent" Index_Data Edge 0 ; + setAttr ".cvd" -type "dataPolyComponent" Index_Data Vertex 0 ; + setAttr ".pd[0]" -type "dataPolyComponent" Index_Data UV 0 ; + setAttr ".hfd" -type "dataPolyComponent" Index_Data Face 0 ; + setAttr ".dr" 1; + setAttr ".ai_translator" -type "string" "polymesh"; + setAttr ".cbId" -type "string" "60df31e2be2b48bd3695c056:6c77a15a98a9"; +select -ne :time1; + setAttr ".o" 1001; + setAttr ".unw" 1001; +select -ne :hardwareRenderingGlobals; + setAttr ".otfna" -type "stringArray" 22 "NURBS Curves" "NURBS Surfaces" "Polygons" "Subdiv Surface" "Particles" "Particle Instance" "Fluids" "Strokes" "Image Planes" "UI" "Lights" "Cameras" "Locators" "Joints" "IK Handles" "Deformers" "Motion Trails" "Components" "Hair Systems" "Follicles" "Misc. UI" "Ornaments" ; + setAttr ".otfva" -type "Int32Array" 22 0 1 1 1 1 1 + 1 1 1 0 0 0 0 0 0 0 0 0 + 0 0 0 0 ; + setAttr ".fprt" yes; +select -ne :renderPartition; + setAttr -s 2 ".st"; +select -ne :renderGlobalsList1; +select -ne :defaultShaderList1; + setAttr -s 5 ".s"; +select -ne :postProcessList1; + setAttr -s 2 ".p"; +select -ne :defaultRenderingList1; + setAttr -s 2 ".r"; +select -ne :lightList1; +select -ne :initialShadingGroup; + setAttr -s 2 ".dsm"; + setAttr ".ro" yes; +select -ne :initialParticleSE; + setAttr ".ro" yes; +select -ne :defaultRenderGlobals; + addAttr -ci true -h true -sn "dss" -ln "defaultSurfaceShader" -dt "string"; + setAttr ".ren" -type "string" "arnold"; + setAttr ".outf" 51; + setAttr ".imfkey" -type "string" "exr"; + setAttr ".an" yes; + setAttr ".fs" 1001; + setAttr ".ef" 1001; + setAttr ".oft" -type "string" ""; + setAttr ".pff" yes; + setAttr ".ifp" -type "string" "//_"; + setAttr ".rv" -type "string" ""; + setAttr ".pram" -type "string" ""; + setAttr ".poam" -type "string" ""; + setAttr ".prlm" -type "string" ""; + setAttr ".polm" -type "string" ""; + setAttr ".prm" -type "string" ""; + setAttr ".pom" -type "string" ""; + setAttr ".dss" -type "string" "lambert1"; +select -ne :defaultResolution; + setAttr ".w" 1920; + setAttr ".h" 1080; + setAttr ".pa" 1; + setAttr ".dar" 1.7777777910232544; +select -ne :defaultLightSet; +select -ne :hardwareRenderGlobals; + setAttr ".ctrs" 256; + setAttr ".btrs" 512; +connectAttr "pSphere1_GEOShape1.iog" ":initialShadingGroup.dsm" -na; +// End of modelMain.ma diff --git a/tests/integration/hosts/maya/test_deadline_publish_in_maya/expected/test_project/test_asset/publish/model/modelMain/v001/test_project_test_asset_modelMain_v001.abc b/tests/integration/hosts/maya/test_deadline_publish_in_maya/expected/test_project/test_asset/publish/model/modelMain/v001/test_project_test_asset_modelMain_v001.abc new file mode 100644 index 0000000000000000000000000000000000000000..d9e54259960efb658348c78adaa0f9ae0958083e GIT binary patch literal 24787 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 MH=R?xes^8}8yK4hrvLx| literal 0 HcmV?d00001 diff --git a/tests/integration/hosts/maya/test_deadline_publish_in_maya/expected/test_project/test_asset/publish/model/modelMain/v001/test_project_test_asset_modelMain_v001.ma b/tests/integration/hosts/maya/test_deadline_publish_in_maya/expected/test_project/test_asset/publish/model/modelMain/v001/test_project_test_asset_modelMain_v001.ma new file mode 100644 index 0000000000..9ee588337e --- /dev/null +++ b/tests/integration/hosts/maya/test_deadline_publish_in_maya/expected/test_project/test_asset/publish/model/modelMain/v001/test_project_test_asset_modelMain_v001.ma @@ -0,0 +1,1208 @@ +//Maya ASCII 2020 scene +//Name: modelMain.ma +//Last modified: Mon, Oct 24, 2022 02:57:47 PM +//Codeset: 1252 +requires maya "2020"; +requires "mtoa" "4.1.1"; +currentUnit -l centimeter -a degree -t pal; +fileInfo "application" "maya"; +fileInfo "product" "Maya 2020"; +fileInfo "version" "2020"; +fileInfo "cutIdentifier" "202011110415-b1e20b88e2"; +fileInfo "osv" "Microsoft Windows 10 Technical Preview (Build 19044)\n"; +fileInfo "UUID" "A787A358-4FE7-6E55-0C81-61BFEB0C2726"; +createNode transform -n "model_GRP"; + rename -uid "445FDC20-4A9D-2C5B-C7BD-F98F6E660B5C"; + setAttr ".rlio[0]" 1 yes 0; +createNode transform -n "pSphere1_GEO" -p "model_GRP"; + rename -uid "7445A43F-444F-B2D3-4315-2AA013D2E0B6"; + addAttr -ci true -sn "cbId" -ln "cbId" -dt "string"; + setAttr ".cbId" -type "string" "60df31e2be2b48bd3695c056:302a4c6123a4"; +createNode mesh -n "pSphere1_GEOShape1" -p "pSphere1_GEO"; + rename -uid "7C731260-45C6-339E-07BF-359446B08EA1"; + addAttr -ci true -sn "cbId" -ln "cbId" -dt "string"; + setAttr -k off ".v"; + setAttr ".vir" yes; + setAttr ".vif" yes; + setAttr ".uvst[0].uvsn" -type "string" "map1"; + setAttr -s 439 ".uvst[0].uvsp"; + setAttr ".uvst[0].uvsp[0:249]" -type "float2" 0 0.050000001 0.050000001 0.050000001 + 0.1 0.050000001 0.15000001 0.050000001 0.2 0.050000001 0.25 0.050000001 0.30000001 + 0.050000001 0.35000002 0.050000001 0.40000004 0.050000001 0.45000005 0.050000001 + 0.50000006 0.050000001 0.55000007 0.050000001 0.60000008 0.050000001 0.6500001 0.050000001 + 0.70000011 0.050000001 0.75000012 0.050000001 0.80000013 0.050000001 0.85000014 0.050000001 + 0.90000015 0.050000001 0.95000017 0.050000001 1.000000119209 0.050000001 0 0.1 0.050000001 + 0.1 0.1 0.1 0.15000001 0.1 0.2 0.1 0.25 0.1 0.30000001 0.1 0.35000002 0.1 0.40000004 + 0.1 0.45000005 0.1 0.50000006 0.1 0.55000007 0.1 0.60000008 0.1 0.6500001 0.1 0.70000011 + 0.1 0.75000012 0.1 0.80000013 0.1 0.85000014 0.1 0.90000015 0.1 0.95000017 0.1 1.000000119209 + 0.1 0 0.15000001 0.050000001 0.15000001 0.1 0.15000001 0.15000001 0.15000001 0.2 + 0.15000001 0.25 0.15000001 0.30000001 0.15000001 0.35000002 0.15000001 0.40000004 + 0.15000001 0.45000005 0.15000001 0.50000006 0.15000001 0.55000007 0.15000001 0.60000008 + 0.15000001 0.6500001 0.15000001 0.70000011 0.15000001 0.75000012 0.15000001 0.80000013 + 0.15000001 0.85000014 0.15000001 0.90000015 0.15000001 0.95000017 0.15000001 1.000000119209 + 0.15000001 0 0.2 0.050000001 0.2 0.1 0.2 0.15000001 0.2 0.2 0.2 0.25 0.2 0.30000001 + 0.2 0.35000002 0.2 0.40000004 0.2 0.45000005 0.2 0.50000006 0.2 0.55000007 0.2 0.60000008 + 0.2 0.6500001 0.2 0.70000011 0.2 0.75000012 0.2 0.80000013 0.2 0.85000014 0.2 0.90000015 + 0.2 0.95000017 0.2 1.000000119209 0.2 0 0.25 0.050000001 0.25 0.1 0.25 0.15000001 + 0.25 0.2 0.25 0.25 0.25 0.30000001 0.25 0.35000002 0.25 0.40000004 0.25 0.45000005 + 0.25 0.50000006 0.25 0.55000007 0.25 0.60000008 0.25 0.6500001 0.25 0.70000011 0.25 + 0.75000012 0.25 0.80000013 0.25 0.85000014 0.25 0.90000015 0.25 0.95000017 0.25 1.000000119209 + 0.25 0 0.30000001 0.050000001 0.30000001 0.1 0.30000001 0.15000001 0.30000001 0.2 + 0.30000001 0.25 0.30000001 0.30000001 0.30000001 0.35000002 0.30000001 0.40000004 + 0.30000001 0.45000005 0.30000001 0.50000006 0.30000001 0.55000007 0.30000001 0.60000008 + 0.30000001 0.6500001 0.30000001 0.70000011 0.30000001 0.75000012 0.30000001 0.80000013 + 0.30000001 0.85000014 0.30000001 0.90000015 0.30000001 0.95000017 0.30000001 1.000000119209 + 0.30000001 0 0.35000002 0.050000001 0.35000002 0.1 0.35000002 0.15000001 0.35000002 + 0.2 0.35000002 0.25 0.35000002 0.30000001 0.35000002 0.35000002 0.35000002 0.40000004 + 0.35000002 0.45000005 0.35000002 0.50000006 0.35000002 0.55000007 0.35000002 0.60000008 + 0.35000002 0.6500001 0.35000002 0.70000011 0.35000002 0.75000012 0.35000002 0.80000013 + 0.35000002 0.85000014 0.35000002 0.90000015 0.35000002 0.95000017 0.35000002 1.000000119209 + 0.35000002 0 0.40000004 0.050000001 0.40000004 0.1 0.40000004 0.15000001 0.40000004 + 0.2 0.40000004 0.25 0.40000004 0.30000001 0.40000004 0.35000002 0.40000004 0.40000004 + 0.40000004 0.45000005 0.40000004 0.50000006 0.40000004 0.55000007 0.40000004 0.60000008 + 0.40000004 0.6500001 0.40000004 0.70000011 0.40000004 0.75000012 0.40000004 0.80000013 + 0.40000004 0.85000014 0.40000004 0.90000015 0.40000004 0.95000017 0.40000004 1.000000119209 + 0.40000004 0 0.45000005 0.050000001 0.45000005 0.1 0.45000005 0.15000001 0.45000005 + 0.2 0.45000005 0.25 0.45000005 0.30000001 0.45000005 0.35000002 0.45000005 0.40000004 + 0.45000005 0.45000005 0.45000005 0.50000006 0.45000005 0.55000007 0.45000005 0.60000008 + 0.45000005 0.6500001 0.45000005 0.70000011 0.45000005 0.75000012 0.45000005 0.80000013 + 0.45000005 0.85000014 0.45000005 0.90000015 0.45000005 0.95000017 0.45000005 1.000000119209 + 0.45000005 0 0.50000006 0.050000001 0.50000006 0.1 0.50000006 0.15000001 0.50000006 + 0.2 0.50000006 0.25 0.50000006 0.30000001 0.50000006 0.35000002 0.50000006 0.40000004 + 0.50000006 0.45000005 0.50000006 0.50000006 0.50000006 0.55000007 0.50000006 0.60000008 + 0.50000006 0.6500001 0.50000006 0.70000011 0.50000006 0.75000012 0.50000006 0.80000013 + 0.50000006 0.85000014 0.50000006 0.90000015 0.50000006 0.95000017 0.50000006 1.000000119209 + 0.50000006 0 0.55000007 0.050000001 0.55000007 0.1 0.55000007 0.15000001 0.55000007 + 0.2 0.55000007 0.25 0.55000007 0.30000001 0.55000007 0.35000002 0.55000007 0.40000004 + 0.55000007 0.45000005 0.55000007 0.50000006 0.55000007 0.55000007 0.55000007 0.60000008 + 0.55000007 0.6500001 0.55000007 0.70000011 0.55000007 0.75000012 0.55000007 0.80000013 + 0.55000007 0.85000014 0.55000007 0.90000015 0.55000007 0.95000017 0.55000007 1.000000119209 + 0.55000007 0 0.60000008 0.050000001 0.60000008 0.1 0.60000008 0.15000001 0.60000008 + 0.2 0.60000008 0.25 0.60000008 0.30000001 0.60000008 0.35000002 0.60000008 0.40000004 + 0.60000008 0.45000005 0.60000008 0.50000006 0.60000008 0.55000007 0.60000008 0.60000008 + 0.60000008 0.6500001 0.60000008 0.70000011 0.60000008 0.75000012 0.60000008 0.80000013 + 0.60000008 0.85000014 0.60000008 0.90000015 0.60000008; + setAttr ".uvst[0].uvsp[250:438]" 0.95000017 0.60000008 1.000000119209 0.60000008 + 0 0.6500001 0.050000001 0.6500001 0.1 0.6500001 0.15000001 0.6500001 0.2 0.6500001 + 0.25 0.6500001 0.30000001 0.6500001 0.35000002 0.6500001 0.40000004 0.6500001 0.45000005 + 0.6500001 0.50000006 0.6500001 0.55000007 0.6500001 0.60000008 0.6500001 0.6500001 + 0.6500001 0.70000011 0.6500001 0.75000012 0.6500001 0.80000013 0.6500001 0.85000014 + 0.6500001 0.90000015 0.6500001 0.95000017 0.6500001 1.000000119209 0.6500001 0 0.70000011 + 0.050000001 0.70000011 0.1 0.70000011 0.15000001 0.70000011 0.2 0.70000011 0.25 0.70000011 + 0.30000001 0.70000011 0.35000002 0.70000011 0.40000004 0.70000011 0.45000005 0.70000011 + 0.50000006 0.70000011 0.55000007 0.70000011 0.60000008 0.70000011 0.6500001 0.70000011 + 0.70000011 0.70000011 0.75000012 0.70000011 0.80000013 0.70000011 0.85000014 0.70000011 + 0.90000015 0.70000011 0.95000017 0.70000011 1.000000119209 0.70000011 0 0.75000012 + 0.050000001 0.75000012 0.1 0.75000012 0.15000001 0.75000012 0.2 0.75000012 0.25 0.75000012 + 0.30000001 0.75000012 0.35000002 0.75000012 0.40000004 0.75000012 0.45000005 0.75000012 + 0.50000006 0.75000012 0.55000007 0.75000012 0.60000008 0.75000012 0.6500001 0.75000012 + 0.70000011 0.75000012 0.75000012 0.75000012 0.80000013 0.75000012 0.85000014 0.75000012 + 0.90000015 0.75000012 0.95000017 0.75000012 1.000000119209 0.75000012 0 0.80000013 + 0.050000001 0.80000013 0.1 0.80000013 0.15000001 0.80000013 0.2 0.80000013 0.25 0.80000013 + 0.30000001 0.80000013 0.35000002 0.80000013 0.40000004 0.80000013 0.45000005 0.80000013 + 0.50000006 0.80000013 0.55000007 0.80000013 0.60000008 0.80000013 0.6500001 0.80000013 + 0.70000011 0.80000013 0.75000012 0.80000013 0.80000013 0.80000013 0.85000014 0.80000013 + 0.90000015 0.80000013 0.95000017 0.80000013 1.000000119209 0.80000013 0 0.85000014 + 0.050000001 0.85000014 0.1 0.85000014 0.15000001 0.85000014 0.2 0.85000014 0.25 0.85000014 + 0.30000001 0.85000014 0.35000002 0.85000014 0.40000004 0.85000014 0.45000005 0.85000014 + 0.50000006 0.85000014 0.55000007 0.85000014 0.60000008 0.85000014 0.6500001 0.85000014 + 0.70000011 0.85000014 0.75000012 0.85000014 0.80000013 0.85000014 0.85000014 0.85000014 + 0.90000015 0.85000014 0.95000017 0.85000014 1.000000119209 0.85000014 0 0.90000015 + 0.050000001 0.90000015 0.1 0.90000015 0.15000001 0.90000015 0.2 0.90000015 0.25 0.90000015 + 0.30000001 0.90000015 0.35000002 0.90000015 0.40000004 0.90000015 0.45000005 0.90000015 + 0.50000006 0.90000015 0.55000007 0.90000015 0.60000008 0.90000015 0.6500001 0.90000015 + 0.70000011 0.90000015 0.75000012 0.90000015 0.80000013 0.90000015 0.85000014 0.90000015 + 0.90000015 0.90000015 0.95000017 0.90000015 1.000000119209 0.90000015 0 0.95000017 + 0.050000001 0.95000017 0.1 0.95000017 0.15000001 0.95000017 0.2 0.95000017 0.25 0.95000017 + 0.30000001 0.95000017 0.35000002 0.95000017 0.40000004 0.95000017 0.45000005 0.95000017 + 0.50000006 0.95000017 0.55000007 0.95000017 0.60000008 0.95000017 0.6500001 0.95000017 + 0.70000011 0.95000017 0.75000012 0.95000017 0.80000013 0.95000017 0.85000014 0.95000017 + 0.90000015 0.95000017 0.95000017 0.95000017 1.000000119209 0.95000017 0.025 0 0.075000003 + 0 0.125 0 0.17500001 0 0.22500001 0 0.27500001 0 0.32500002 0 0.375 0 0.42500001 + 0 0.47500002 0 0.52499998 0 0.57499999 0 0.625 0 0.67500001 0 0.72499996 0 0.77499998 + 0 0.82499999 0 0.875 0 0.92500001 0 0.97499996 0 0.025 1 0.075000003 1 0.125 1 0.17500001 + 1 0.22500001 1 0.27500001 1 0.32500002 1 0.375 1 0.42500001 1 0.47500002 1 0.52499998 + 1 0.57499999 1 0.625 1 0.67500001 1 0.72499996 1 0.77499998 1 0.82499999 1 0.875 + 1 0.92500001 1 0.97499996 1; + setAttr ".cuvs" -type "string" "map1"; + setAttr ".dcc" -type "string" "Ambient+Diffuse"; + setAttr ".covm[0]" 0 1 1; + setAttr ".cdvm[0]" 0 1 1; + setAttr -s 382 ".vt"; + setAttr ".vt[0:165]" 0.14877813 -0.98768836 -0.048340943 0.12655823 -0.98768836 -0.091949932 + 0.091949932 -0.98768836 -0.12655823 0.048340935 -0.98768836 -0.14877811 0 -0.98768836 -0.15643455 + -0.048340935 -0.98768836 -0.1487781 -0.091949917 -0.98768836 -0.1265582 -0.12655818 -0.98768836 -0.091949902 + -0.14877807 -0.98768836 -0.048340924 -0.15643452 -0.98768836 0 -0.14877807 -0.98768836 0.048340924 + -0.12655818 -0.98768836 0.091949895 -0.091949895 -0.98768836 0.12655817 -0.048340924 -0.98768836 0.14877805 + -4.6621107e-09 -0.98768836 0.15643449 0.048340909 -0.98768836 0.14877804 0.09194988 -0.98768836 0.12655815 + 0.12655815 -0.98768836 0.091949888 0.14877804 -0.98768836 0.048340913 0.15643448 -0.98768836 0 + 0.29389283 -0.95105654 -0.095491566 0.25000018 -0.95105654 -0.18163574 0.18163574 -0.95105654 -0.25000015 + 0.095491551 -0.95105654 -0.2938928 0 -0.95105654 -0.30901715 -0.095491551 -0.95105654 -0.29389277 + -0.18163571 -0.95105654 -0.25000009 -0.25000009 -0.95105654 -0.18163569 -0.29389271 -0.95105654 -0.095491529 + -0.30901706 -0.95105654 0 -0.29389271 -0.95105654 0.095491529 -0.25000006 -0.95105654 0.18163568 + -0.18163568 -0.95105654 0.25000006 -0.095491529 -0.95105654 0.29389268 -9.2094243e-09 -0.95105654 0.30901703 + 0.095491499 -0.95105654 0.29389265 0.18163563 -0.95105654 0.25000003 0.25 -0.95105654 0.18163565 + 0.29389265 -0.95105654 0.095491506 0.309017 -0.95105654 0 0.43177092 -0.89100653 -0.14029087 + 0.36728629 -0.89100653 -0.2668491 0.2668491 -0.89100653 -0.36728626 0.14029086 -0.89100653 -0.43177086 + 0 -0.89100653 -0.45399073 -0.14029086 -0.89100653 -0.43177083 -0.26684904 -0.89100653 -0.36728618 + -0.36728615 -0.89100653 -0.26684901 -0.43177077 -0.89100653 -0.14029081 -0.45399064 -0.89100653 0 + -0.43177077 -0.89100653 0.14029081 -0.36728612 -0.89100653 0.26684898 -0.26684898 -0.89100653 0.36728612 + -0.14029081 -0.89100653 0.43177071 -1.3529972e-08 -0.89100653 0.45399058 0.14029078 -0.89100653 0.43177068 + 0.26684892 -0.89100653 0.36728609 0.36728606 -0.89100653 0.26684895 0.43177065 -0.89100653 0.1402908 + 0.45399052 -0.89100653 0 0.55901736 -0.809017 -0.18163574 0.47552857 -0.809017 -0.34549171 + 0.34549171 -0.809017 -0.47552854 0.18163572 -0.809017 -0.5590173 0 -0.809017 -0.58778554 + -0.18163572 -0.809017 -0.55901724 -0.34549165 -0.809017 -0.47552842 -0.47552839 -0.809017 -0.34549159 + -0.55901712 -0.809017 -0.18163566 -0.58778536 -0.809017 0 -0.55901712 -0.809017 0.18163566 + -0.47552836 -0.809017 0.34549156 -0.34549156 -0.809017 0.47552833 -0.18163566 -0.809017 0.55901706 + -1.7517365e-08 -0.809017 0.5877853 0.18163562 -0.809017 0.55901706 0.3454915 -0.809017 0.4755283 + 0.47552827 -0.809017 0.34549153 0.559017 -0.809017 0.18163563 0.58778524 -0.809017 0 + 0.67249894 -0.70710677 -0.21850814 0.57206178 -0.70710677 -0.41562718 0.41562718 -0.70710677 -0.57206172 + 0.21850812 -0.70710677 -0.67249888 0 -0.70710677 -0.70710713 -0.21850812 -0.70710677 -0.67249882 + -0.41562709 -0.70710677 -0.5720616 -0.57206154 -0.70710677 -0.41562706 -0.6724987 -0.70710677 -0.21850805 + -0.70710695 -0.70710677 0 -0.6724987 -0.70710677 0.21850805 -0.57206154 -0.70710677 0.415627 + -0.415627 -0.70710677 0.57206148 -0.21850805 -0.70710677 0.67249858 -2.1073424e-08 -0.70710677 0.70710683 + 0.21850799 -0.70710677 0.67249858 0.41562691 -0.70710677 0.57206142 0.57206142 -0.70710677 0.41562697 + 0.67249852 -0.70710677 0.21850802 0.70710677 -0.70710677 0 0.7694214 -0.58778524 -0.25000015 + 0.65450895 -0.58778524 -0.47552854 0.47552854 -0.58778524 -0.65450889 0.25000012 -0.58778524 -0.76942128 + 0 -0.58778524 -0.80901736 -0.25000012 -0.58778524 -0.76942122 -0.47552845 -0.58778524 -0.65450877 + -0.65450871 -0.58778524 -0.47552839 -0.7694211 -0.58778524 -0.25000006 -0.80901718 -0.58778524 0 + -0.7694211 -0.58778524 0.25000006 -0.65450865 -0.58778524 0.47552836 -0.47552836 -0.58778524 0.65450859 + -0.25000006 -0.58778524 0.76942098 -2.4110586e-08 -0.58778524 0.80901712 0.24999999 -0.58778524 0.76942098 + 0.47552827 -0.58778524 0.65450853 0.65450853 -0.58778524 0.4755283 0.76942092 -0.58778524 0.25 + 0.809017 -0.58778524 0 0.8473981 -0.45399052 -0.27533633 0.72083992 -0.45399052 -0.5237208 + 0.5237208 -0.45399052 -0.72083986 0.2753363 -0.45399052 -0.84739798 0 -0.45399052 -0.89100695 + -0.2753363 -0.45399052 -0.84739798 -0.52372068 -0.45399052 -0.72083968 -0.72083962 -0.45399052 -0.52372062 + -0.8473978 -0.45399052 -0.27533621 -0.89100677 -0.45399052 0 -0.8473978 -0.45399052 0.27533621 + -0.72083962 -0.45399052 0.52372062 -0.52372062 -0.45399052 0.72083956 -0.27533621 -0.45399052 0.84739769 + -2.6554064e-08 -0.45399052 0.89100665 0.27533615 -0.45399052 0.84739763 0.5237205 -0.45399052 0.7208395 + 0.72083944 -0.45399052 0.52372056 0.84739757 -0.45399052 0.27533618 0.89100653 -0.45399052 0 + 0.90450913 -0.30901697 -0.2938928 0.7694214 -0.30901697 -0.55901736 0.55901736 -0.30901697 -0.76942134 + 0.29389277 -0.30901697 -0.90450901 0 -0.30901697 -0.95105702 -0.29389277 -0.30901697 -0.90450895 + -0.55901724 -0.30901697 -0.76942122 -0.76942116 -0.30901697 -0.55901718 -0.90450877 -0.30901697 -0.29389271 + -0.95105678 -0.30901697 0 -0.90450877 -0.30901697 0.29389271 -0.7694211 -0.30901697 0.55901712 + -0.55901712 -0.30901697 0.76942104 -0.29389271 -0.30901697 0.90450865 -2.8343694e-08 -0.30901697 0.95105666 + 0.29389262 -0.30901697 0.90450859 0.559017 -0.30901697 0.76942098 0.76942092 -0.30901697 0.55901706 + 0.90450853 -0.30901697 0.29389265 0.95105654 -0.30901697 0 0.93934804 -0.15643437 -0.30521268 + 0.79905719 -0.15643437 -0.580549 0.580549 -0.15643437 -0.79905713 0.30521265 -0.15643437 -0.93934792 + 0 -0.15643437 -0.98768884 -0.30521265 -0.15643437 -0.93934786; + setAttr ".vt[166:331]" -0.58054888 -0.15643437 -0.79905695 -0.79905689 -0.15643437 -0.58054882 + -0.93934768 -0.15643437 -0.30521256 -0.9876886 -0.15643437 0 -0.93934768 -0.15643437 0.30521256 + -0.79905683 -0.15643437 0.58054876 -0.58054876 -0.15643437 0.79905677 -0.30521256 -0.15643437 0.93934757 + -2.9435407e-08 -0.15643437 0.98768848 0.30521247 -0.15643437 0.93934757 0.58054864 -0.15643437 0.79905671 + 0.79905665 -0.15643437 0.5805487 0.93934751 -0.15643437 0.3052125 0.98768836 -0.15643437 0 + 0.95105714 0 -0.30901718 0.80901754 0 -0.5877856 0.5877856 0 -0.80901748 0.30901715 0 -0.95105702 + 0 0 -1.000000476837 -0.30901715 0 -0.95105696 -0.58778548 0 -0.8090173 -0.80901724 0 -0.58778542 + -0.95105678 0 -0.30901706 -1.000000238419 0 0 -0.95105678 0 0.30901706 -0.80901718 0 0.58778536 + -0.58778536 0 0.80901712 -0.30901706 0 0.95105666 -2.9802322e-08 0 1.000000119209 + 0.30901697 0 0.9510566 0.58778524 0 0.80901706 0.809017 0 0.5877853 0.95105654 0 0.309017 + 1 0 0 0.93934804 0.15643437 -0.30521268 0.79905719 0.15643437 -0.580549 0.580549 0.15643437 -0.79905713 + 0.30521265 0.15643437 -0.93934792 0 0.15643437 -0.98768884 -0.30521265 0.15643437 -0.93934786 + -0.58054888 0.15643437 -0.79905695 -0.79905689 0.15643437 -0.58054882 -0.93934768 0.15643437 -0.30521256 + -0.9876886 0.15643437 0 -0.93934768 0.15643437 0.30521256 -0.79905683 0.15643437 0.58054876 + -0.58054876 0.15643437 0.79905677 -0.30521256 0.15643437 0.93934757 -2.9435407e-08 0.15643437 0.98768848 + 0.30521247 0.15643437 0.93934757 0.58054864 0.15643437 0.79905671 0.79905665 0.15643437 0.5805487 + 0.93934751 0.15643437 0.3052125 0.98768836 0.15643437 0 0.90450913 0.30901697 -0.2938928 + 0.7694214 0.30901697 -0.55901736 0.55901736 0.30901697 -0.76942134 0.29389277 0.30901697 -0.90450901 + 0 0.30901697 -0.95105702 -0.29389277 0.30901697 -0.90450895 -0.55901724 0.30901697 -0.76942122 + -0.76942116 0.30901697 -0.55901718 -0.90450877 0.30901697 -0.29389271 -0.95105678 0.30901697 0 + -0.90450877 0.30901697 0.29389271 -0.7694211 0.30901697 0.55901712 -0.55901712 0.30901697 0.76942104 + -0.29389271 0.30901697 0.90450865 -2.8343694e-08 0.30901697 0.95105666 0.29389262 0.30901697 0.90450859 + 0.559017 0.30901697 0.76942098 0.76942092 0.30901697 0.55901706 0.90450853 0.30901697 0.29389265 + 0.95105654 0.30901697 0 0.8473981 0.45399052 -0.27533633 0.72083992 0.45399052 -0.5237208 + 0.5237208 0.45399052 -0.72083986 0.2753363 0.45399052 -0.84739798 0 0.45399052 -0.89100695 + -0.2753363 0.45399052 -0.84739798 -0.52372068 0.45399052 -0.72083968 -0.72083962 0.45399052 -0.52372062 + -0.8473978 0.45399052 -0.27533621 -0.89100677 0.45399052 0 -0.8473978 0.45399052 0.27533621 + -0.72083962 0.45399052 0.52372062 -0.52372062 0.45399052 0.72083956 -0.27533621 0.45399052 0.84739769 + -2.6554064e-08 0.45399052 0.89100665 0.27533615 0.45399052 0.84739763 0.5237205 0.45399052 0.7208395 + 0.72083944 0.45399052 0.52372056 0.84739757 0.45399052 0.27533618 0.89100653 0.45399052 0 + 0.7694214 0.58778524 -0.25000015 0.65450895 0.58778524 -0.47552854 0.47552854 0.58778524 -0.65450889 + 0.25000012 0.58778524 -0.76942128 0 0.58778524 -0.80901736 -0.25000012 0.58778524 -0.76942122 + -0.47552845 0.58778524 -0.65450877 -0.65450871 0.58778524 -0.47552839 -0.7694211 0.58778524 -0.25000006 + -0.80901718 0.58778524 0 -0.7694211 0.58778524 0.25000006 -0.65450865 0.58778524 0.47552836 + -0.47552836 0.58778524 0.65450859 -0.25000006 0.58778524 0.76942098 -2.4110586e-08 0.58778524 0.80901712 + 0.24999999 0.58778524 0.76942098 0.47552827 0.58778524 0.65450853 0.65450853 0.58778524 0.4755283 + 0.76942092 0.58778524 0.25 0.809017 0.58778524 0 0.67249894 0.70710677 -0.21850814 + 0.57206178 0.70710677 -0.41562718 0.41562718 0.70710677 -0.57206172 0.21850812 0.70710677 -0.67249888 + 0 0.70710677 -0.70710713 -0.21850812 0.70710677 -0.67249882 -0.41562709 0.70710677 -0.5720616 + -0.57206154 0.70710677 -0.41562706 -0.6724987 0.70710677 -0.21850805 -0.70710695 0.70710677 0 + -0.6724987 0.70710677 0.21850805 -0.57206154 0.70710677 0.415627 -0.415627 0.70710677 0.57206148 + -0.21850805 0.70710677 0.67249858 -2.1073424e-08 0.70710677 0.70710683 0.21850799 0.70710677 0.67249858 + 0.41562691 0.70710677 0.57206142 0.57206142 0.70710677 0.41562697 0.67249852 0.70710677 0.21850802 + 0.70710677 0.70710677 0 0.55901736 0.809017 -0.18163574 0.47552857 0.809017 -0.34549171 + 0.34549171 0.809017 -0.47552854 0.18163572 0.809017 -0.5590173 0 0.809017 -0.58778554 + -0.18163572 0.809017 -0.55901724 -0.34549165 0.809017 -0.47552842 -0.47552839 0.809017 -0.34549159 + -0.55901712 0.809017 -0.18163566 -0.58778536 0.809017 0 -0.55901712 0.809017 0.18163566 + -0.47552836 0.809017 0.34549156 -0.34549156 0.809017 0.47552833 -0.18163566 0.809017 0.55901706 + -1.7517365e-08 0.809017 0.5877853 0.18163562 0.809017 0.55901706 0.3454915 0.809017 0.4755283 + 0.47552827 0.809017 0.34549153 0.559017 0.809017 0.18163563 0.58778524 0.809017 0 + 0.43177092 0.89100653 -0.14029087 0.36728629 0.89100653 -0.2668491 0.2668491 0.89100653 -0.36728626 + 0.14029086 0.89100653 -0.43177086 0 0.89100653 -0.45399073 -0.14029086 0.89100653 -0.43177083 + -0.26684904 0.89100653 -0.36728618 -0.36728615 0.89100653 -0.26684901 -0.43177077 0.89100653 -0.14029081 + -0.45399064 0.89100653 0 -0.43177077 0.89100653 0.14029081 -0.36728612 0.89100653 0.26684898; + setAttr ".vt[332:381]" -0.26684898 0.89100653 0.36728612 -0.14029081 0.89100653 0.43177071 + -1.3529972e-08 0.89100653 0.45399058 0.14029078 0.89100653 0.43177068 0.26684892 0.89100653 0.36728609 + 0.36728606 0.89100653 0.26684895 0.43177065 0.89100653 0.1402908 0.45399052 0.89100653 0 + 0.29389283 0.95105654 -0.095491566 0.25000018 0.95105654 -0.18163574 0.18163574 0.95105654 -0.25000015 + 0.095491551 0.95105654 -0.2938928 0 0.95105654 -0.30901715 -0.095491551 0.95105654 -0.29389277 + -0.18163571 0.95105654 -0.25000009 -0.25000009 0.95105654 -0.18163569 -0.29389271 0.95105654 -0.095491529 + -0.30901706 0.95105654 0 -0.29389271 0.95105654 0.095491529 -0.25000006 0.95105654 0.18163568 + -0.18163568 0.95105654 0.25000006 -0.095491529 0.95105654 0.29389268 -9.2094243e-09 0.95105654 0.30901703 + 0.095491499 0.95105654 0.29389265 0.18163563 0.95105654 0.25000003 0.25 0.95105654 0.18163565 + 0.29389265 0.95105654 0.095491506 0.309017 0.95105654 0 0.14877813 0.98768836 -0.048340943 + 0.12655823 0.98768836 -0.091949932 0.091949932 0.98768836 -0.12655823 0.048340935 0.98768836 -0.14877811 + 0 0.98768836 -0.15643455 -0.048340935 0.98768836 -0.1487781 -0.091949917 0.98768836 -0.1265582 + -0.12655818 0.98768836 -0.091949902 -0.14877807 0.98768836 -0.048340924 -0.15643452 0.98768836 0 + -0.14877807 0.98768836 0.048340924 -0.12655818 0.98768836 0.091949895 -0.091949895 0.98768836 0.12655817 + -0.048340924 0.98768836 0.14877805 -4.6621107e-09 0.98768836 0.15643449 0.048340909 0.98768836 0.14877804 + 0.09194988 0.98768836 0.12655815 0.12655815 0.98768836 0.091949888 0.14877804 0.98768836 0.048340913 + 0.15643448 0.98768836 0 0 -1 0 0 1 0; + setAttr -s 780 ".ed"; + setAttr ".ed[0:165]" 0 1 1 1 2 1 2 3 1 3 4 1 4 5 1 5 6 1 6 7 1 7 8 1 8 9 1 + 9 10 1 10 11 1 11 12 1 12 13 1 13 14 1 14 15 1 15 16 1 16 17 1 17 18 1 18 19 1 19 0 1 + 20 21 1 21 22 1 22 23 1 23 24 1 24 25 1 25 26 1 26 27 1 27 28 1 28 29 1 29 30 1 30 31 1 + 31 32 1 32 33 1 33 34 1 34 35 1 35 36 1 36 37 1 37 38 1 38 39 1 39 20 1 40 41 1 41 42 1 + 42 43 1 43 44 1 44 45 1 45 46 1 46 47 1 47 48 1 48 49 1 49 50 1 50 51 1 51 52 1 52 53 1 + 53 54 1 54 55 1 55 56 1 56 57 1 57 58 1 58 59 1 59 40 1 60 61 1 61 62 1 62 63 1 63 64 1 + 64 65 1 65 66 1 66 67 1 67 68 1 68 69 1 69 70 1 70 71 1 71 72 1 72 73 1 73 74 1 74 75 1 + 75 76 1 76 77 1 77 78 1 78 79 1 79 60 1 80 81 1 81 82 1 82 83 1 83 84 1 84 85 1 85 86 1 + 86 87 1 87 88 1 88 89 1 89 90 1 90 91 1 91 92 1 92 93 1 93 94 1 94 95 1 95 96 1 96 97 1 + 97 98 1 98 99 1 99 80 1 100 101 1 101 102 1 102 103 1 103 104 1 104 105 1 105 106 1 + 106 107 1 107 108 1 108 109 1 109 110 1 110 111 1 111 112 1 112 113 1 113 114 1 114 115 1 + 115 116 1 116 117 1 117 118 1 118 119 1 119 100 1 120 121 1 121 122 1 122 123 1 123 124 1 + 124 125 1 125 126 1 126 127 1 127 128 1 128 129 1 129 130 1 130 131 1 131 132 1 132 133 1 + 133 134 1 134 135 1 135 136 1 136 137 1 137 138 1 138 139 1 139 120 1 140 141 1 141 142 1 + 142 143 1 143 144 1 144 145 1 145 146 1 146 147 1 147 148 1 148 149 1 149 150 1 150 151 1 + 151 152 1 152 153 1 153 154 1 154 155 1 155 156 1 156 157 1 157 158 1 158 159 1 159 140 1 + 160 161 1 161 162 1 162 163 1 163 164 1 164 165 1 165 166 1; + setAttr ".ed[166:331]" 166 167 1 167 168 1 168 169 1 169 170 1 170 171 1 171 172 1 + 172 173 1 173 174 1 174 175 1 175 176 1 176 177 1 177 178 1 178 179 1 179 160 1 180 181 1 + 181 182 1 182 183 1 183 184 1 184 185 1 185 186 1 186 187 1 187 188 1 188 189 1 189 190 1 + 190 191 1 191 192 1 192 193 1 193 194 1 194 195 1 195 196 1 196 197 1 197 198 1 198 199 1 + 199 180 1 200 201 1 201 202 1 202 203 1 203 204 1 204 205 1 205 206 1 206 207 1 207 208 1 + 208 209 1 209 210 1 210 211 1 211 212 1 212 213 1 213 214 1 214 215 1 215 216 1 216 217 1 + 217 218 1 218 219 1 219 200 1 220 221 1 221 222 1 222 223 1 223 224 1 224 225 1 225 226 1 + 226 227 1 227 228 1 228 229 1 229 230 1 230 231 1 231 232 1 232 233 1 233 234 1 234 235 1 + 235 236 1 236 237 1 237 238 1 238 239 1 239 220 1 240 241 1 241 242 1 242 243 1 243 244 1 + 244 245 1 245 246 1 246 247 1 247 248 1 248 249 1 249 250 1 250 251 1 251 252 1 252 253 1 + 253 254 1 254 255 1 255 256 1 256 257 1 257 258 1 258 259 1 259 240 1 260 261 1 261 262 1 + 262 263 1 263 264 1 264 265 1 265 266 1 266 267 1 267 268 1 268 269 1 269 270 1 270 271 1 + 271 272 1 272 273 1 273 274 1 274 275 1 275 276 1 276 277 1 277 278 1 278 279 1 279 260 1 + 280 281 1 281 282 1 282 283 1 283 284 1 284 285 1 285 286 1 286 287 1 287 288 1 288 289 1 + 289 290 1 290 291 1 291 292 1 292 293 1 293 294 1 294 295 1 295 296 1 296 297 1 297 298 1 + 298 299 1 299 280 1 300 301 1 301 302 1 302 303 1 303 304 1 304 305 1 305 306 1 306 307 1 + 307 308 1 308 309 1 309 310 1 310 311 1 311 312 1 312 313 1 313 314 1 314 315 1 315 316 1 + 316 317 1 317 318 1 318 319 1 319 300 1 320 321 1 321 322 1 322 323 1 323 324 1 324 325 1 + 325 326 1 326 327 1 327 328 1 328 329 1 329 330 1 330 331 1 331 332 1; + setAttr ".ed[332:497]" 332 333 1 333 334 1 334 335 1 335 336 1 336 337 1 337 338 1 + 338 339 1 339 320 1 340 341 1 341 342 1 342 343 1 343 344 1 344 345 1 345 346 1 346 347 1 + 347 348 1 348 349 1 349 350 1 350 351 1 351 352 1 352 353 1 353 354 1 354 355 1 355 356 1 + 356 357 1 357 358 1 358 359 1 359 340 1 360 361 1 361 362 1 362 363 1 363 364 1 364 365 1 + 365 366 1 366 367 1 367 368 1 368 369 1 369 370 1 370 371 1 371 372 1 372 373 1 373 374 1 + 374 375 1 375 376 1 376 377 1 377 378 1 378 379 1 379 360 1 0 20 1 1 21 1 2 22 1 + 3 23 1 4 24 1 5 25 1 6 26 1 7 27 1 8 28 1 9 29 1 10 30 1 11 31 1 12 32 1 13 33 1 + 14 34 1 15 35 1 16 36 1 17 37 1 18 38 1 19 39 1 20 40 1 21 41 1 22 42 1 23 43 1 24 44 1 + 25 45 1 26 46 1 27 47 1 28 48 1 29 49 1 30 50 1 31 51 1 32 52 1 33 53 1 34 54 1 35 55 1 + 36 56 1 37 57 1 38 58 1 39 59 1 40 60 1 41 61 1 42 62 1 43 63 1 44 64 1 45 65 1 46 66 1 + 47 67 1 48 68 1 49 69 1 50 70 1 51 71 1 52 72 1 53 73 1 54 74 1 55 75 1 56 76 1 57 77 1 + 58 78 1 59 79 1 60 80 1 61 81 1 62 82 1 63 83 1 64 84 1 65 85 1 66 86 1 67 87 1 68 88 1 + 69 89 1 70 90 1 71 91 1 72 92 1 73 93 1 74 94 1 75 95 1 76 96 1 77 97 1 78 98 1 79 99 1 + 80 100 1 81 101 1 82 102 1 83 103 1 84 104 1 85 105 1 86 106 1 87 107 1 88 108 1 + 89 109 1 90 110 1 91 111 1 92 112 1 93 113 1 94 114 1 95 115 1 96 116 1 97 117 1 + 98 118 1 99 119 1 100 120 1 101 121 1 102 122 1 103 123 1 104 124 1 105 125 1 106 126 1 + 107 127 1 108 128 1 109 129 1 110 130 1 111 131 1 112 132 1 113 133 1 114 134 1 115 135 1 + 116 136 1 117 137 1; + setAttr ".ed[498:663]" 118 138 1 119 139 1 120 140 1 121 141 1 122 142 1 123 143 1 + 124 144 1 125 145 1 126 146 1 127 147 1 128 148 1 129 149 1 130 150 1 131 151 1 132 152 1 + 133 153 1 134 154 1 135 155 1 136 156 1 137 157 1 138 158 1 139 159 1 140 160 1 141 161 1 + 142 162 1 143 163 1 144 164 1 145 165 1 146 166 1 147 167 1 148 168 1 149 169 1 150 170 1 + 151 171 1 152 172 1 153 173 1 154 174 1 155 175 1 156 176 1 157 177 1 158 178 1 159 179 1 + 160 180 1 161 181 1 162 182 1 163 183 1 164 184 1 165 185 1 166 186 1 167 187 1 168 188 1 + 169 189 1 170 190 1 171 191 1 172 192 1 173 193 1 174 194 1 175 195 1 176 196 1 177 197 1 + 178 198 1 179 199 1 180 200 1 181 201 1 182 202 1 183 203 1 184 204 1 185 205 1 186 206 1 + 187 207 1 188 208 1 189 209 1 190 210 1 191 211 1 192 212 1 193 213 1 194 214 1 195 215 1 + 196 216 1 197 217 1 198 218 1 199 219 1 200 220 1 201 221 1 202 222 1 203 223 1 204 224 1 + 205 225 1 206 226 1 207 227 1 208 228 1 209 229 1 210 230 1 211 231 1 212 232 1 213 233 1 + 214 234 1 215 235 1 216 236 1 217 237 1 218 238 1 219 239 1 220 240 1 221 241 1 222 242 1 + 223 243 1 224 244 1 225 245 1 226 246 1 227 247 1 228 248 1 229 249 1 230 250 1 231 251 1 + 232 252 1 233 253 1 234 254 1 235 255 1 236 256 1 237 257 1 238 258 1 239 259 1 240 260 1 + 241 261 1 242 262 1 243 263 1 244 264 1 245 265 1 246 266 1 247 267 1 248 268 1 249 269 1 + 250 270 1 251 271 1 252 272 1 253 273 1 254 274 1 255 275 1 256 276 1 257 277 1 258 278 1 + 259 279 1 260 280 1 261 281 1 262 282 1 263 283 1 264 284 1 265 285 1 266 286 1 267 287 1 + 268 288 1 269 289 1 270 290 1 271 291 1 272 292 1 273 293 1 274 294 1 275 295 1 276 296 1 + 277 297 1 278 298 1 279 299 1 280 300 1 281 301 1 282 302 1 283 303 1; + setAttr ".ed[664:779]" 284 304 1 285 305 1 286 306 1 287 307 1 288 308 1 289 309 1 + 290 310 1 291 311 1 292 312 1 293 313 1 294 314 1 295 315 1 296 316 1 297 317 1 298 318 1 + 299 319 1 300 320 1 301 321 1 302 322 1 303 323 1 304 324 1 305 325 1 306 326 1 307 327 1 + 308 328 1 309 329 1 310 330 1 311 331 1 312 332 1 313 333 1 314 334 1 315 335 1 316 336 1 + 317 337 1 318 338 1 319 339 1 320 340 1 321 341 1 322 342 1 323 343 1 324 344 1 325 345 1 + 326 346 1 327 347 1 328 348 1 329 349 1 330 350 1 331 351 1 332 352 1 333 353 1 334 354 1 + 335 355 1 336 356 1 337 357 1 338 358 1 339 359 1 340 360 1 341 361 1 342 362 1 343 363 1 + 344 364 1 345 365 1 346 366 1 347 367 1 348 368 1 349 369 1 350 370 1 351 371 1 352 372 1 + 353 373 1 354 374 1 355 375 1 356 376 1 357 377 1 358 378 1 359 379 1 380 0 1 380 1 1 + 380 2 1 380 3 1 380 4 1 380 5 1 380 6 1 380 7 1 380 8 1 380 9 1 380 10 1 380 11 1 + 380 12 1 380 13 1 380 14 1 380 15 1 380 16 1 380 17 1 380 18 1 380 19 1 360 381 1 + 361 381 1 362 381 1 363 381 1 364 381 1 365 381 1 366 381 1 367 381 1 368 381 1 369 381 1 + 370 381 1 371 381 1 372 381 1 373 381 1 374 381 1 375 381 1 376 381 1 377 381 1 378 381 1 + 379 381 1; + setAttr -s 400 -ch 1560 ".fc[0:399]" -type "polyFaces" + f 4 0 381 -21 -381 + mu 0 4 0 1 22 21 + f 4 1 382 -22 -382 + mu 0 4 1 2 23 22 + f 4 2 383 -23 -383 + mu 0 4 2 3 24 23 + f 4 3 384 -24 -384 + mu 0 4 3 4 25 24 + f 4 4 385 -25 -385 + mu 0 4 4 5 26 25 + f 4 5 386 -26 -386 + mu 0 4 5 6 27 26 + f 4 6 387 -27 -387 + mu 0 4 6 7 28 27 + f 4 7 388 -28 -388 + mu 0 4 7 8 29 28 + f 4 8 389 -29 -389 + mu 0 4 8 9 30 29 + f 4 9 390 -30 -390 + mu 0 4 9 10 31 30 + f 4 10 391 -31 -391 + mu 0 4 10 11 32 31 + f 4 11 392 -32 -392 + mu 0 4 11 12 33 32 + f 4 12 393 -33 -393 + mu 0 4 12 13 34 33 + f 4 13 394 -34 -394 + mu 0 4 13 14 35 34 + f 4 14 395 -35 -395 + mu 0 4 14 15 36 35 + f 4 15 396 -36 -396 + mu 0 4 15 16 37 36 + f 4 16 397 -37 -397 + mu 0 4 16 17 38 37 + f 4 17 398 -38 -398 + mu 0 4 17 18 39 38 + f 4 18 399 -39 -399 + mu 0 4 18 19 40 39 + f 4 19 380 -40 -400 + mu 0 4 19 20 41 40 + f 4 20 401 -41 -401 + mu 0 4 21 22 43 42 + f 4 21 402 -42 -402 + mu 0 4 22 23 44 43 + f 4 22 403 -43 -403 + mu 0 4 23 24 45 44 + f 4 23 404 -44 -404 + mu 0 4 24 25 46 45 + f 4 24 405 -45 -405 + mu 0 4 25 26 47 46 + f 4 25 406 -46 -406 + mu 0 4 26 27 48 47 + f 4 26 407 -47 -407 + mu 0 4 27 28 49 48 + f 4 27 408 -48 -408 + mu 0 4 28 29 50 49 + f 4 28 409 -49 -409 + mu 0 4 29 30 51 50 + f 4 29 410 -50 -410 + mu 0 4 30 31 52 51 + f 4 30 411 -51 -411 + mu 0 4 31 32 53 52 + f 4 31 412 -52 -412 + mu 0 4 32 33 54 53 + f 4 32 413 -53 -413 + mu 0 4 33 34 55 54 + f 4 33 414 -54 -414 + mu 0 4 34 35 56 55 + f 4 34 415 -55 -415 + mu 0 4 35 36 57 56 + f 4 35 416 -56 -416 + mu 0 4 36 37 58 57 + f 4 36 417 -57 -417 + mu 0 4 37 38 59 58 + f 4 37 418 -58 -418 + mu 0 4 38 39 60 59 + f 4 38 419 -59 -419 + mu 0 4 39 40 61 60 + f 4 39 400 -60 -420 + mu 0 4 40 41 62 61 + f 4 40 421 -61 -421 + mu 0 4 42 43 64 63 + f 4 41 422 -62 -422 + mu 0 4 43 44 65 64 + f 4 42 423 -63 -423 + mu 0 4 44 45 66 65 + f 4 43 424 -64 -424 + mu 0 4 45 46 67 66 + f 4 44 425 -65 -425 + mu 0 4 46 47 68 67 + f 4 45 426 -66 -426 + mu 0 4 47 48 69 68 + f 4 46 427 -67 -427 + mu 0 4 48 49 70 69 + f 4 47 428 -68 -428 + mu 0 4 49 50 71 70 + f 4 48 429 -69 -429 + mu 0 4 50 51 72 71 + f 4 49 430 -70 -430 + mu 0 4 51 52 73 72 + f 4 50 431 -71 -431 + mu 0 4 52 53 74 73 + f 4 51 432 -72 -432 + mu 0 4 53 54 75 74 + f 4 52 433 -73 -433 + mu 0 4 54 55 76 75 + f 4 53 434 -74 -434 + mu 0 4 55 56 77 76 + f 4 54 435 -75 -435 + mu 0 4 56 57 78 77 + f 4 55 436 -76 -436 + mu 0 4 57 58 79 78 + f 4 56 437 -77 -437 + mu 0 4 58 59 80 79 + f 4 57 438 -78 -438 + mu 0 4 59 60 81 80 + f 4 58 439 -79 -439 + mu 0 4 60 61 82 81 + f 4 59 420 -80 -440 + mu 0 4 61 62 83 82 + f 4 60 441 -81 -441 + mu 0 4 63 64 85 84 + f 4 61 442 -82 -442 + mu 0 4 64 65 86 85 + f 4 62 443 -83 -443 + mu 0 4 65 66 87 86 + f 4 63 444 -84 -444 + mu 0 4 66 67 88 87 + f 4 64 445 -85 -445 + mu 0 4 67 68 89 88 + f 4 65 446 -86 -446 + mu 0 4 68 69 90 89 + f 4 66 447 -87 -447 + mu 0 4 69 70 91 90 + f 4 67 448 -88 -448 + mu 0 4 70 71 92 91 + f 4 68 449 -89 -449 + mu 0 4 71 72 93 92 + f 4 69 450 -90 -450 + mu 0 4 72 73 94 93 + f 4 70 451 -91 -451 + mu 0 4 73 74 95 94 + f 4 71 452 -92 -452 + mu 0 4 74 75 96 95 + f 4 72 453 -93 -453 + mu 0 4 75 76 97 96 + f 4 73 454 -94 -454 + mu 0 4 76 77 98 97 + f 4 74 455 -95 -455 + mu 0 4 77 78 99 98 + f 4 75 456 -96 -456 + mu 0 4 78 79 100 99 + f 4 76 457 -97 -457 + mu 0 4 79 80 101 100 + f 4 77 458 -98 -458 + mu 0 4 80 81 102 101 + f 4 78 459 -99 -459 + mu 0 4 81 82 103 102 + f 4 79 440 -100 -460 + mu 0 4 82 83 104 103 + f 4 80 461 -101 -461 + mu 0 4 84 85 106 105 + f 4 81 462 -102 -462 + mu 0 4 85 86 107 106 + f 4 82 463 -103 -463 + mu 0 4 86 87 108 107 + f 4 83 464 -104 -464 + mu 0 4 87 88 109 108 + f 4 84 465 -105 -465 + mu 0 4 88 89 110 109 + f 4 85 466 -106 -466 + mu 0 4 89 90 111 110 + f 4 86 467 -107 -467 + mu 0 4 90 91 112 111 + f 4 87 468 -108 -468 + mu 0 4 91 92 113 112 + f 4 88 469 -109 -469 + mu 0 4 92 93 114 113 + f 4 89 470 -110 -470 + mu 0 4 93 94 115 114 + f 4 90 471 -111 -471 + mu 0 4 94 95 116 115 + f 4 91 472 -112 -472 + mu 0 4 95 96 117 116 + f 4 92 473 -113 -473 + mu 0 4 96 97 118 117 + f 4 93 474 -114 -474 + mu 0 4 97 98 119 118 + f 4 94 475 -115 -475 + mu 0 4 98 99 120 119 + f 4 95 476 -116 -476 + mu 0 4 99 100 121 120 + f 4 96 477 -117 -477 + mu 0 4 100 101 122 121 + f 4 97 478 -118 -478 + mu 0 4 101 102 123 122 + f 4 98 479 -119 -479 + mu 0 4 102 103 124 123 + f 4 99 460 -120 -480 + mu 0 4 103 104 125 124 + f 4 100 481 -121 -481 + mu 0 4 105 106 127 126 + f 4 101 482 -122 -482 + mu 0 4 106 107 128 127 + f 4 102 483 -123 -483 + mu 0 4 107 108 129 128 + f 4 103 484 -124 -484 + mu 0 4 108 109 130 129 + f 4 104 485 -125 -485 + mu 0 4 109 110 131 130 + f 4 105 486 -126 -486 + mu 0 4 110 111 132 131 + f 4 106 487 -127 -487 + mu 0 4 111 112 133 132 + f 4 107 488 -128 -488 + mu 0 4 112 113 134 133 + f 4 108 489 -129 -489 + mu 0 4 113 114 135 134 + f 4 109 490 -130 -490 + mu 0 4 114 115 136 135 + f 4 110 491 -131 -491 + mu 0 4 115 116 137 136 + f 4 111 492 -132 -492 + mu 0 4 116 117 138 137 + f 4 112 493 -133 -493 + mu 0 4 117 118 139 138 + f 4 113 494 -134 -494 + mu 0 4 118 119 140 139 + f 4 114 495 -135 -495 + mu 0 4 119 120 141 140 + f 4 115 496 -136 -496 + mu 0 4 120 121 142 141 + f 4 116 497 -137 -497 + mu 0 4 121 122 143 142 + f 4 117 498 -138 -498 + mu 0 4 122 123 144 143 + f 4 118 499 -139 -499 + mu 0 4 123 124 145 144 + f 4 119 480 -140 -500 + mu 0 4 124 125 146 145 + f 4 120 501 -141 -501 + mu 0 4 126 127 148 147 + f 4 121 502 -142 -502 + mu 0 4 127 128 149 148 + f 4 122 503 -143 -503 + mu 0 4 128 129 150 149 + f 4 123 504 -144 -504 + mu 0 4 129 130 151 150 + f 4 124 505 -145 -505 + mu 0 4 130 131 152 151 + f 4 125 506 -146 -506 + mu 0 4 131 132 153 152 + f 4 126 507 -147 -507 + mu 0 4 132 133 154 153 + f 4 127 508 -148 -508 + mu 0 4 133 134 155 154 + f 4 128 509 -149 -509 + mu 0 4 134 135 156 155 + f 4 129 510 -150 -510 + mu 0 4 135 136 157 156 + f 4 130 511 -151 -511 + mu 0 4 136 137 158 157 + f 4 131 512 -152 -512 + mu 0 4 137 138 159 158 + f 4 132 513 -153 -513 + mu 0 4 138 139 160 159 + f 4 133 514 -154 -514 + mu 0 4 139 140 161 160 + f 4 134 515 -155 -515 + mu 0 4 140 141 162 161 + f 4 135 516 -156 -516 + mu 0 4 141 142 163 162 + f 4 136 517 -157 -517 + mu 0 4 142 143 164 163 + f 4 137 518 -158 -518 + mu 0 4 143 144 165 164 + f 4 138 519 -159 -519 + mu 0 4 144 145 166 165 + f 4 139 500 -160 -520 + mu 0 4 145 146 167 166 + f 4 140 521 -161 -521 + mu 0 4 147 148 169 168 + f 4 141 522 -162 -522 + mu 0 4 148 149 170 169 + f 4 142 523 -163 -523 + mu 0 4 149 150 171 170 + f 4 143 524 -164 -524 + mu 0 4 150 151 172 171 + f 4 144 525 -165 -525 + mu 0 4 151 152 173 172 + f 4 145 526 -166 -526 + mu 0 4 152 153 174 173 + f 4 146 527 -167 -527 + mu 0 4 153 154 175 174 + f 4 147 528 -168 -528 + mu 0 4 154 155 176 175 + f 4 148 529 -169 -529 + mu 0 4 155 156 177 176 + f 4 149 530 -170 -530 + mu 0 4 156 157 178 177 + f 4 150 531 -171 -531 + mu 0 4 157 158 179 178 + f 4 151 532 -172 -532 + mu 0 4 158 159 180 179 + f 4 152 533 -173 -533 + mu 0 4 159 160 181 180 + f 4 153 534 -174 -534 + mu 0 4 160 161 182 181 + f 4 154 535 -175 -535 + mu 0 4 161 162 183 182 + f 4 155 536 -176 -536 + mu 0 4 162 163 184 183 + f 4 156 537 -177 -537 + mu 0 4 163 164 185 184 + f 4 157 538 -178 -538 + mu 0 4 164 165 186 185 + f 4 158 539 -179 -539 + mu 0 4 165 166 187 186 + f 4 159 520 -180 -540 + mu 0 4 166 167 188 187 + f 4 160 541 -181 -541 + mu 0 4 168 169 190 189 + f 4 161 542 -182 -542 + mu 0 4 169 170 191 190 + f 4 162 543 -183 -543 + mu 0 4 170 171 192 191 + f 4 163 544 -184 -544 + mu 0 4 171 172 193 192 + f 4 164 545 -185 -545 + mu 0 4 172 173 194 193 + f 4 165 546 -186 -546 + mu 0 4 173 174 195 194 + f 4 166 547 -187 -547 + mu 0 4 174 175 196 195 + f 4 167 548 -188 -548 + mu 0 4 175 176 197 196 + f 4 168 549 -189 -549 + mu 0 4 176 177 198 197 + f 4 169 550 -190 -550 + mu 0 4 177 178 199 198 + f 4 170 551 -191 -551 + mu 0 4 178 179 200 199 + f 4 171 552 -192 -552 + mu 0 4 179 180 201 200 + f 4 172 553 -193 -553 + mu 0 4 180 181 202 201 + f 4 173 554 -194 -554 + mu 0 4 181 182 203 202 + f 4 174 555 -195 -555 + mu 0 4 182 183 204 203 + f 4 175 556 -196 -556 + mu 0 4 183 184 205 204 + f 4 176 557 -197 -557 + mu 0 4 184 185 206 205 + f 4 177 558 -198 -558 + mu 0 4 185 186 207 206 + f 4 178 559 -199 -559 + mu 0 4 186 187 208 207 + f 4 179 540 -200 -560 + mu 0 4 187 188 209 208 + f 4 180 561 -201 -561 + mu 0 4 189 190 211 210 + f 4 181 562 -202 -562 + mu 0 4 190 191 212 211 + f 4 182 563 -203 -563 + mu 0 4 191 192 213 212 + f 4 183 564 -204 -564 + mu 0 4 192 193 214 213 + f 4 184 565 -205 -565 + mu 0 4 193 194 215 214 + f 4 185 566 -206 -566 + mu 0 4 194 195 216 215 + f 4 186 567 -207 -567 + mu 0 4 195 196 217 216 + f 4 187 568 -208 -568 + mu 0 4 196 197 218 217 + f 4 188 569 -209 -569 + mu 0 4 197 198 219 218 + f 4 189 570 -210 -570 + mu 0 4 198 199 220 219 + f 4 190 571 -211 -571 + mu 0 4 199 200 221 220 + f 4 191 572 -212 -572 + mu 0 4 200 201 222 221 + f 4 192 573 -213 -573 + mu 0 4 201 202 223 222 + f 4 193 574 -214 -574 + mu 0 4 202 203 224 223 + f 4 194 575 -215 -575 + mu 0 4 203 204 225 224 + f 4 195 576 -216 -576 + mu 0 4 204 205 226 225 + f 4 196 577 -217 -577 + mu 0 4 205 206 227 226 + f 4 197 578 -218 -578 + mu 0 4 206 207 228 227 + f 4 198 579 -219 -579 + mu 0 4 207 208 229 228 + f 4 199 560 -220 -580 + mu 0 4 208 209 230 229 + f 4 200 581 -221 -581 + mu 0 4 210 211 232 231 + f 4 201 582 -222 -582 + mu 0 4 211 212 233 232 + f 4 202 583 -223 -583 + mu 0 4 212 213 234 233 + f 4 203 584 -224 -584 + mu 0 4 213 214 235 234 + f 4 204 585 -225 -585 + mu 0 4 214 215 236 235 + f 4 205 586 -226 -586 + mu 0 4 215 216 237 236 + f 4 206 587 -227 -587 + mu 0 4 216 217 238 237 + f 4 207 588 -228 -588 + mu 0 4 217 218 239 238 + f 4 208 589 -229 -589 + mu 0 4 218 219 240 239 + f 4 209 590 -230 -590 + mu 0 4 219 220 241 240 + f 4 210 591 -231 -591 + mu 0 4 220 221 242 241 + f 4 211 592 -232 -592 + mu 0 4 221 222 243 242 + f 4 212 593 -233 -593 + mu 0 4 222 223 244 243 + f 4 213 594 -234 -594 + mu 0 4 223 224 245 244 + f 4 214 595 -235 -595 + mu 0 4 224 225 246 245 + f 4 215 596 -236 -596 + mu 0 4 225 226 247 246 + f 4 216 597 -237 -597 + mu 0 4 226 227 248 247 + f 4 217 598 -238 -598 + mu 0 4 227 228 249 248 + f 4 218 599 -239 -599 + mu 0 4 228 229 250 249 + f 4 219 580 -240 -600 + mu 0 4 229 230 251 250 + f 4 220 601 -241 -601 + mu 0 4 231 232 253 252 + f 4 221 602 -242 -602 + mu 0 4 232 233 254 253 + f 4 222 603 -243 -603 + mu 0 4 233 234 255 254 + f 4 223 604 -244 -604 + mu 0 4 234 235 256 255 + f 4 224 605 -245 -605 + mu 0 4 235 236 257 256 + f 4 225 606 -246 -606 + mu 0 4 236 237 258 257 + f 4 226 607 -247 -607 + mu 0 4 237 238 259 258 + f 4 227 608 -248 -608 + mu 0 4 238 239 260 259 + f 4 228 609 -249 -609 + mu 0 4 239 240 261 260 + f 4 229 610 -250 -610 + mu 0 4 240 241 262 261 + f 4 230 611 -251 -611 + mu 0 4 241 242 263 262 + f 4 231 612 -252 -612 + mu 0 4 242 243 264 263 + f 4 232 613 -253 -613 + mu 0 4 243 244 265 264 + f 4 233 614 -254 -614 + mu 0 4 244 245 266 265 + f 4 234 615 -255 -615 + mu 0 4 245 246 267 266 + f 4 235 616 -256 -616 + mu 0 4 246 247 268 267 + f 4 236 617 -257 -617 + mu 0 4 247 248 269 268 + f 4 237 618 -258 -618 + mu 0 4 248 249 270 269 + f 4 238 619 -259 -619 + mu 0 4 249 250 271 270 + f 4 239 600 -260 -620 + mu 0 4 250 251 272 271 + f 4 240 621 -261 -621 + mu 0 4 252 253 274 273 + f 4 241 622 -262 -622 + mu 0 4 253 254 275 274 + f 4 242 623 -263 -623 + mu 0 4 254 255 276 275 + f 4 243 624 -264 -624 + mu 0 4 255 256 277 276 + f 4 244 625 -265 -625 + mu 0 4 256 257 278 277 + f 4 245 626 -266 -626 + mu 0 4 257 258 279 278 + f 4 246 627 -267 -627 + mu 0 4 258 259 280 279 + f 4 247 628 -268 -628 + mu 0 4 259 260 281 280 + f 4 248 629 -269 -629 + mu 0 4 260 261 282 281 + f 4 249 630 -270 -630 + mu 0 4 261 262 283 282 + f 4 250 631 -271 -631 + mu 0 4 262 263 284 283 + f 4 251 632 -272 -632 + mu 0 4 263 264 285 284 + f 4 252 633 -273 -633 + mu 0 4 264 265 286 285 + f 4 253 634 -274 -634 + mu 0 4 265 266 287 286 + f 4 254 635 -275 -635 + mu 0 4 266 267 288 287 + f 4 255 636 -276 -636 + mu 0 4 267 268 289 288 + f 4 256 637 -277 -637 + mu 0 4 268 269 290 289 + f 4 257 638 -278 -638 + mu 0 4 269 270 291 290 + f 4 258 639 -279 -639 + mu 0 4 270 271 292 291 + f 4 259 620 -280 -640 + mu 0 4 271 272 293 292 + f 4 260 641 -281 -641 + mu 0 4 273 274 295 294 + f 4 261 642 -282 -642 + mu 0 4 274 275 296 295 + f 4 262 643 -283 -643 + mu 0 4 275 276 297 296 + f 4 263 644 -284 -644 + mu 0 4 276 277 298 297 + f 4 264 645 -285 -645 + mu 0 4 277 278 299 298 + f 4 265 646 -286 -646 + mu 0 4 278 279 300 299 + f 4 266 647 -287 -647 + mu 0 4 279 280 301 300 + f 4 267 648 -288 -648 + mu 0 4 280 281 302 301 + f 4 268 649 -289 -649 + mu 0 4 281 282 303 302 + f 4 269 650 -290 -650 + mu 0 4 282 283 304 303 + f 4 270 651 -291 -651 + mu 0 4 283 284 305 304 + f 4 271 652 -292 -652 + mu 0 4 284 285 306 305 + f 4 272 653 -293 -653 + mu 0 4 285 286 307 306 + f 4 273 654 -294 -654 + mu 0 4 286 287 308 307 + f 4 274 655 -295 -655 + mu 0 4 287 288 309 308 + f 4 275 656 -296 -656 + mu 0 4 288 289 310 309 + f 4 276 657 -297 -657 + mu 0 4 289 290 311 310 + f 4 277 658 -298 -658 + mu 0 4 290 291 312 311 + f 4 278 659 -299 -659 + mu 0 4 291 292 313 312 + f 4 279 640 -300 -660 + mu 0 4 292 293 314 313 + f 4 280 661 -301 -661 + mu 0 4 294 295 316 315 + f 4 281 662 -302 -662 + mu 0 4 295 296 317 316 + f 4 282 663 -303 -663 + mu 0 4 296 297 318 317 + f 4 283 664 -304 -664 + mu 0 4 297 298 319 318 + f 4 284 665 -305 -665 + mu 0 4 298 299 320 319 + f 4 285 666 -306 -666 + mu 0 4 299 300 321 320 + f 4 286 667 -307 -667 + mu 0 4 300 301 322 321 + f 4 287 668 -308 -668 + mu 0 4 301 302 323 322 + f 4 288 669 -309 -669 + mu 0 4 302 303 324 323 + f 4 289 670 -310 -670 + mu 0 4 303 304 325 324 + f 4 290 671 -311 -671 + mu 0 4 304 305 326 325 + f 4 291 672 -312 -672 + mu 0 4 305 306 327 326 + f 4 292 673 -313 -673 + mu 0 4 306 307 328 327 + f 4 293 674 -314 -674 + mu 0 4 307 308 329 328 + f 4 294 675 -315 -675 + mu 0 4 308 309 330 329 + f 4 295 676 -316 -676 + mu 0 4 309 310 331 330 + f 4 296 677 -317 -677 + mu 0 4 310 311 332 331 + f 4 297 678 -318 -678 + mu 0 4 311 312 333 332 + f 4 298 679 -319 -679 + mu 0 4 312 313 334 333 + f 4 299 660 -320 -680 + mu 0 4 313 314 335 334 + f 4 300 681 -321 -681 + mu 0 4 315 316 337 336 + f 4 301 682 -322 -682 + mu 0 4 316 317 338 337 + f 4 302 683 -323 -683 + mu 0 4 317 318 339 338 + f 4 303 684 -324 -684 + mu 0 4 318 319 340 339 + f 4 304 685 -325 -685 + mu 0 4 319 320 341 340 + f 4 305 686 -326 -686 + mu 0 4 320 321 342 341 + f 4 306 687 -327 -687 + mu 0 4 321 322 343 342 + f 4 307 688 -328 -688 + mu 0 4 322 323 344 343 + f 4 308 689 -329 -689 + mu 0 4 323 324 345 344 + f 4 309 690 -330 -690 + mu 0 4 324 325 346 345 + f 4 310 691 -331 -691 + mu 0 4 325 326 347 346 + f 4 311 692 -332 -692 + mu 0 4 326 327 348 347 + f 4 312 693 -333 -693 + mu 0 4 327 328 349 348 + f 4 313 694 -334 -694 + mu 0 4 328 329 350 349 + f 4 314 695 -335 -695 + mu 0 4 329 330 351 350 + f 4 315 696 -336 -696 + mu 0 4 330 331 352 351 + f 4 316 697 -337 -697 + mu 0 4 331 332 353 352 + f 4 317 698 -338 -698 + mu 0 4 332 333 354 353 + f 4 318 699 -339 -699 + mu 0 4 333 334 355 354 + f 4 319 680 -340 -700 + mu 0 4 334 335 356 355 + f 4 320 701 -341 -701 + mu 0 4 336 337 358 357 + f 4 321 702 -342 -702 + mu 0 4 337 338 359 358 + f 4 322 703 -343 -703 + mu 0 4 338 339 360 359 + f 4 323 704 -344 -704 + mu 0 4 339 340 361 360 + f 4 324 705 -345 -705 + mu 0 4 340 341 362 361 + f 4 325 706 -346 -706 + mu 0 4 341 342 363 362 + f 4 326 707 -347 -707 + mu 0 4 342 343 364 363 + f 4 327 708 -348 -708 + mu 0 4 343 344 365 364 + f 4 328 709 -349 -709 + mu 0 4 344 345 366 365 + f 4 329 710 -350 -710 + mu 0 4 345 346 367 366 + f 4 330 711 -351 -711 + mu 0 4 346 347 368 367 + f 4 331 712 -352 -712 + mu 0 4 347 348 369 368 + f 4 332 713 -353 -713 + mu 0 4 348 349 370 369 + f 4 333 714 -354 -714 + mu 0 4 349 350 371 370 + f 4 334 715 -355 -715 + mu 0 4 350 351 372 371 + f 4 335 716 -356 -716 + mu 0 4 351 352 373 372 + f 4 336 717 -357 -717 + mu 0 4 352 353 374 373 + f 4 337 718 -358 -718 + mu 0 4 353 354 375 374 + f 4 338 719 -359 -719 + mu 0 4 354 355 376 375 + f 4 339 700 -360 -720 + mu 0 4 355 356 377 376 + f 4 340 721 -361 -721 + mu 0 4 357 358 379 378 + f 4 341 722 -362 -722 + mu 0 4 358 359 380 379 + f 4 342 723 -363 -723 + mu 0 4 359 360 381 380 + f 4 343 724 -364 -724 + mu 0 4 360 361 382 381 + f 4 344 725 -365 -725 + mu 0 4 361 362 383 382 + f 4 345 726 -366 -726 + mu 0 4 362 363 384 383 + f 4 346 727 -367 -727 + mu 0 4 363 364 385 384 + f 4 347 728 -368 -728 + mu 0 4 364 365 386 385 + f 4 348 729 -369 -729 + mu 0 4 365 366 387 386 + f 4 349 730 -370 -730 + mu 0 4 366 367 388 387 + f 4 350 731 -371 -731 + mu 0 4 367 368 389 388 + f 4 351 732 -372 -732 + mu 0 4 368 369 390 389 + f 4 352 733 -373 -733 + mu 0 4 369 370 391 390 + f 4 353 734 -374 -734 + mu 0 4 370 371 392 391 + f 4 354 735 -375 -735 + mu 0 4 371 372 393 392 + f 4 355 736 -376 -736 + mu 0 4 372 373 394 393 + f 4 356 737 -377 -737 + mu 0 4 373 374 395 394 + f 4 357 738 -378 -738 + mu 0 4 374 375 396 395 + f 4 358 739 -379 -739 + mu 0 4 375 376 397 396 + f 4 359 720 -380 -740 + mu 0 4 376 377 398 397 + f 3 -1 -741 741 + mu 0 3 1 0 399 + f 3 -2 -742 742 + mu 0 3 2 1 400 + f 3 -3 -743 743 + mu 0 3 3 2 401 + f 3 -4 -744 744 + mu 0 3 4 3 402 + f 3 -5 -745 745 + mu 0 3 5 4 403 + f 3 -6 -746 746 + mu 0 3 6 5 404 + f 3 -7 -747 747 + mu 0 3 7 6 405 + f 3 -8 -748 748 + mu 0 3 8 7 406 + f 3 -9 -749 749 + mu 0 3 9 8 407 + f 3 -10 -750 750 + mu 0 3 10 9 408 + f 3 -11 -751 751 + mu 0 3 11 10 409 + f 3 -12 -752 752 + mu 0 3 12 11 410 + f 3 -13 -753 753 + mu 0 3 13 12 411 + f 3 -14 -754 754 + mu 0 3 14 13 412 + f 3 -15 -755 755 + mu 0 3 15 14 413 + f 3 -16 -756 756 + mu 0 3 16 15 414 + f 3 -17 -757 757 + mu 0 3 17 16 415 + f 3 -18 -758 758 + mu 0 3 18 17 416 + f 3 -19 -759 759 + mu 0 3 19 18 417 + f 3 -20 -760 740 + mu 0 3 20 19 418 + f 3 360 761 -761 + mu 0 3 378 379 419 + f 3 361 762 -762 + mu 0 3 379 380 420 + f 3 362 763 -763 + mu 0 3 380 381 421 + f 3 363 764 -764 + mu 0 3 381 382 422 + f 3 364 765 -765 + mu 0 3 382 383 423 + f 3 365 766 -766 + mu 0 3 383 384 424 + f 3 366 767 -767 + mu 0 3 384 385 425 + f 3 367 768 -768 + mu 0 3 385 386 426 + f 3 368 769 -769 + mu 0 3 386 387 427 + f 3 369 770 -770 + mu 0 3 387 388 428 + f 3 370 771 -771 + mu 0 3 388 389 429 + f 3 371 772 -772 + mu 0 3 389 390 430 + f 3 372 773 -773 + mu 0 3 390 391 431 + f 3 373 774 -774 + mu 0 3 391 392 432 + f 3 374 775 -775 + mu 0 3 392 393 433 + f 3 375 776 -776 + mu 0 3 393 394 434 + f 3 376 777 -777 + mu 0 3 394 395 435 + f 3 377 778 -778 + mu 0 3 395 396 436 + f 3 378 779 -779 + mu 0 3 396 397 437 + f 3 379 760 -780 + mu 0 3 397 398 438; + setAttr ".cd" -type "dataPolyComponent" Index_Data Edge 0 ; + setAttr ".cvd" -type "dataPolyComponent" Index_Data Vertex 0 ; + setAttr ".pd[0]" -type "dataPolyComponent" Index_Data UV 0 ; + setAttr ".hfd" -type "dataPolyComponent" Index_Data Face 0 ; + setAttr ".dr" 1; + setAttr ".ai_translator" -type "string" "polymesh"; + setAttr ".cbId" -type "string" "60df31e2be2b48bd3695c056:6c77a15a98a9"; +select -ne :time1; + setAttr ".o" 1001; + setAttr ".unw" 1001; +select -ne :hardwareRenderingGlobals; + setAttr ".otfna" -type "stringArray" 22 "NURBS Curves" "NURBS Surfaces" "Polygons" "Subdiv Surface" "Particles" "Particle Instance" "Fluids" "Strokes" "Image Planes" "UI" "Lights" "Cameras" "Locators" "Joints" "IK Handles" "Deformers" "Motion Trails" "Components" "Hair Systems" "Follicles" "Misc. UI" "Ornaments" ; + setAttr ".otfva" -type "Int32Array" 22 0 1 1 1 1 1 + 1 1 1 0 0 0 0 0 0 0 0 0 + 0 0 0 0 ; + setAttr ".fprt" yes; +select -ne :renderPartition; + setAttr -s 2 ".st"; +select -ne :renderGlobalsList1; +select -ne :defaultShaderList1; + setAttr -s 5 ".s"; +select -ne :postProcessList1; + setAttr -s 2 ".p"; +select -ne :defaultRenderingList1; + setAttr -s 2 ".r"; +select -ne :lightList1; +select -ne :initialShadingGroup; + setAttr -s 2 ".dsm"; + setAttr ".ro" yes; +select -ne :initialParticleSE; + setAttr ".ro" yes; +select -ne :defaultRenderGlobals; + addAttr -ci true -h true -sn "dss" -ln "defaultSurfaceShader" -dt "string"; + setAttr ".ren" -type "string" "arnold"; + setAttr ".outf" 51; + setAttr ".imfkey" -type "string" "exr"; + setAttr ".an" yes; + setAttr ".fs" 1001; + setAttr ".ef" 1001; + setAttr ".oft" -type "string" ""; + setAttr ".pff" yes; + setAttr ".ifp" -type "string" "//_"; + setAttr ".rv" -type "string" ""; + setAttr ".pram" -type "string" ""; + setAttr ".poam" -type "string" ""; + setAttr ".prlm" -type "string" ""; + setAttr ".polm" -type "string" ""; + setAttr ".prm" -type "string" ""; + setAttr ".pom" -type "string" ""; + setAttr ".dss" -type "string" "lambert1"; +select -ne :defaultResolution; + setAttr ".w" 1920; + setAttr ".h" 1080; + setAttr ".pa" 1; + setAttr ".dar" 1.7777777910232544; +select -ne :defaultLightSet; +select -ne :hardwareRenderGlobals; + setAttr ".ctrs" 256; + setAttr ".btrs" 512; +connectAttr "pSphere1_GEOShape1.iog" ":initialShadingGroup.dsm" -na; +// End of modelMain.ma diff --git a/tests/integration/hosts/maya/test_deadline_publish_in_maya/expected/test_project/test_asset/publish/render/renderTest_taskMain_beauty/v001/test_project_test_asset_renderTest_taskMain_beauty_v001.exr b/tests/integration/hosts/maya/test_deadline_publish_in_maya/expected/test_project/test_asset/publish/render/renderTest_taskMain_beauty/v001/test_project_test_asset_renderTest_taskMain_beauty_v001.exr new file mode 100644 index 0000000000000000000000000000000000000000..b137a46dfd7d763017b172f9b8df014f7c82f281 GIT binary patch literal 3608096 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 ?ASAMn5K#SWdl_B$>_K>>jd%zl?2<+o_1+f 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

taJK*HA)OcIKdSn@)n%l&uon{)NnnO9E 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⋘ z))C~`Iuvo(~5!AC2>Jw?-Si!_X3+$rR^bTi9W>$v(fSp9y0K|0#^C_x0ww3b8u6d^KiwSttN_=2 zEaP;;Oz68nmvW_KGp7%hiNkCmj<`ETnkkQcJ;LnGl1Esr<2eR>J(kD`Hiy~fsOPAC zJ-GUNc=L69JwhDGm%iV-gKiQ>IhCo^%RWKH-ZM_$!9_Yhz*PD1f5yuG^HQh;>}j)@ z!57n1;Wjt_I%%7zW3&t-*8ux4^Q0bL_92*Cz_7N6+I=Kj-om$y^mh|!wfJNmuT=g# z8spIE1L8#wZ@jfphCpYcC0L!PCj^_92;6;Ol?u20ld1pjZ-8V2+43k0>xe`n$|=@9 z+c4n!00+!DAlbk+412@d1ADVf27L#-e41HcA%JHp*}ypfECz;2a7KuAGpUm}eA=Navp{U~lX`jxoFY zeZ4Q*vX2FLfZLWS#uea}?#wwM=-mr`Ui?l`b`@M3d0__{v7SR_Gb<)&xwK8qOk5i7(LLB3s zK*B#mK0(Dee3)CRO1!p*J;scF1o$n2&1^mPq3`#CoWb8)_*Mx->rdjU1NH{QOHfZh?>FK#z}ASi0H5y9yZ^5XJV%B};Ng?3k}Zq03U-jJ z;_QN*UbqcwpSOo|0O|Y?gK+}>>Kut|nPyg`RjOXJ1?9Aln|22NieZwkn|Tc3lBbt$ z+TU#t8}tr8Mfqg0M*RM*a}XqKgv&MJR*?j&bmJEhe(u2Q%zap*wuAfjJ*Y&j{;h{s zx$GECtU1sn$=v%5i1r|#$yF~gys32N-q4^B6*X(mhLAb#NK#W^s*8E+lvvOo!OSYa^^A!CMyT6-6UVL3;cZE9^p3e zCbnVpBeG?&X0c|DLAYz`Iqp8%8Hz-{tnylvG|yzP&6o?g{*{aldH!OX6bVC|nF)66bXSS5h^l{K;;|8S0)ZP?3G ztt9;AIuXSp=r00&2V%o&CCp$9^km@c z=IQ;WE>-`2^Oy(P1^mE_FGf9iLj$I2sn0JabGDAOqZe(&vj>^<_OelE=-@uo?J09WL*4^Du63vlfeXcbZU zK{x$8`V^&1d5C?8{aMWKc;|n;xcc9{1FMi}e3mBe3E6U|z!3Y32-zB){A|5Imr8|r zYpHsm{c-a*%HZw6I0R2$t>gkR(zaSoH%BMGMD0Zc_1ycn6O6eJ2RWecygUP3K{SeT z0lYB<+K}*p{;5H758LjeRYHd#(mDE(Vdey*eORjLGFi+s`!MA)pVvkiVfI4pY{P(A zw~c%e5oL1+jdo1AutrQZuU4X10pbRW6m`<5CqM&(c1*Dds<-}(*h0eR?Lj^ReBU7k z!6tu~coXFU(mDB(K&NmU#v#`JB?8ty>j>L0$|+D2@b*y8UBgk&0Sl0RievBy{RS3T zfxyoI>aiKxTA9~Apznlsfp}B+OT)|qOi1|nTiJS{4o_p{t2PM7n1Q`y@ri7ibVaV( z!&j|@VsVK59H~O_26mk30x`)PMARf2AE&7NcpjZ#mGU^shOfJy3#ep<*stK*g&HI~ z1aWqe&j>aZDuI$^3m+0b&{Uxv8)k|%qnyS)@$w|tBw6j^XB&R)bBf~Wb%;Sd2c9PM zU8jIf{uF(PBkIZX=zN_w-ZipT2`!>6q8x)+`sv1>9|6m=SffxY%4Uu_M>oI~JOkaF zF=j~kRq`Ib4xy&0f*ohbZvOc?_*?K-Il5f^KsjOe5ybjUQ|GC4bDl?+DVrsKzZd+< zBuy`Sl*Qd|g|tvBOFzij;L9QfZ_ggKQ3g=^!CkYCAfL4h0UaOf2;}uIT?cG^$p(_u zRTBCs#)%nPf=$W=!0B8d0=|)bNTgM)nRbS58VU;VOSdq91qA8=iAMAz@Rv~g&($x~ zUZjb$i+aK`+AM~*jdl!HZ{7n)=%uQ?JcAqwRtT3Ur|d%@n&lV*)>Wx$fp)zV_Fk!K ziCU>D+;y=A;RfOb;l?Hb(fZ49pV!1|h!+H#`&g3AA>6fan^vJ(iB@5`(li~%Ab5{44hc85@ye8i+fq$!BZFUQ7R5Z<#v5W!HGp>qQEhS-|B$UjL#kG_ybFk9iF62e&X7 zhd@)nFiAZJm|?tapd+Q42mCNNSy)H7`UN^c#89}6uN!bh;A{eMAm9(DnE;y<@K8Cr zIl7b!zmzKf{Ss^bVz7&oYWl&6YCg$)n2oP{oQZ6ChIWzx^q?SJiGDuD+{5b{Fh~9Q zk)L}XH}u^#9N-r0!~EQJa~^%JmC4eFyLJpIQ%1PN+E00Gni^&ELq)ZeV~}YaZ`&jd z625s7@|jDZYe1R_;EfM3#hX063^QY%?P0GGTYO?4B3lNkRGTQPgfM%5x0q*o*;)E? z)cst*LQXRQ=ko?(%4636x#}S2MhV3Vs`+a;;7c}03b!?j*?okBFWFF{Hbp_arO z7l_nzh!-1#So^p;Xvb&Bf*n62N_^d-E&N?T^Tg9jyoP)Rv_#+U#aoLtzwv$&_asiO z1h86H@R0ClXqCz@5p;79FMx+XO^3fV#2)>8g3&0$&HqVUt4Ot+PW}$=CIP4vU8qH| z==~=5Ay@w{PK&6;r(-m$giJ%ggieCg)n91!f_9Ox7vT_M=?^lB1^gs)Ky z*cR;q){!p;t^st@t^q;LmWk7J%wspO^i#pFGK`#qW@!{FlFXfhf}FJqiPwM%2;ow` zs$B^2;uHmNXAkm-*ItHW?Z1eyjjWTtKj+=BSTp(&#UkJeh1yw1n8tyU4)w%2=xJ<|_zLM6 za+HmI7$p2%9La{a?}4H#{H1dc=rKUPM7RNaQ!8@=`|h1izEuLw?)Q7`LP+PE1fMK= zc>-Muv{^@<#2I|a)|;d5<(Xv2*Ezs+3530wWYEd~p|XcfwC)nf*WJahRS4K)NciC| zK@7D)(ltOoE6l!K2zWVP4EC{dbVD57{bHU?(PJJcmlbK6rp7#b8rv?UT~H_8A*fk2 zM?Fo)-B+dz)TCeUlP$XhK8c&6e;$o;nx(H;afU3>2%=BPW{~hOa{N2F+^YVP0^8W2LTo12))+|lL3!!#j5A&q( zmmtzfwhVKNaLG1Iw9Ytza>_K$(+k-H1lBx%7wX9+!af$=G+#IR5m;$#!&CIIH+0kR zS4ijBd#oeySLjC@gjevG2W-O#m#iaF^*|HAF?fjpGy`nIzz4pC$U5m@grV{wAy`~q6a;vBVOd_wu&HJGEUG>RVn}rwpgQwH`Nr>Gw$J? zWDsj!AzdQhB%q%PvmasyeQ9ZDn8!#~*@woMJ-tfQI0rI}DilXqY$L;8*2<)r(9S@@ z=NyQ%g}Jp4```p~n{G_9dWpa?`c1u30Yu$(b3oKzp;9*oc-uffZxi*}C(;%uBsK_z z+5=s1cEew;5#K>~2-e6Ba?DdXeUPf3V0@hN^}b1(Ol|P1By&jkpj)?bx@!RD0dEgb zKpms45yRfh(g5|7L}R%U&8&98Dv3@$=(9<>QY|N2GfQKVhP7X!*3UIVoAo%mWbT0XqYT0||5jT^aZ!?F{Y?);|3d;Raxxd3sM!lg!_EV;=B#S;uz>LO~s& z`+7ja=jlZ{KS5<04|L(~BU`qKS|#~nP$N6We1>ctFWN%3oM~wHv4@vq5bb!5x`!9x zQmsTc2jQ}de+!>x*3F+{k!1Cox^fxr4&3!6!ZBKn>^v23k3%TyNV6DMKm8PB{vk)V zK@$C_PI?O;@d8Agv;IEp+^sH+5a zw>`YM5AT1q`?yT@=riq1r$DpV+xN$4oC82HQK~9cukr))pjr+RKG`zDWtpK?Y2r*n`*quocE zS<;nKRlu4|(a%wH4gg)&E)L2m6qGm{|(Pn87E?Gvi^g#v=@Ik#iKR;Bh643!`DVZ>`0MmAC2Z-8E8 zf^m?ei=SZo*`bqUu_Vof0M1jJOIp}Yz>IKxCD|dGmnY1GLNm0 zUcukMk}U%U9}+&$RRZPcJQe$pa2x#;$QR)12dYu#F`)fDMIl`QF#(B2+8OjC-X4${ zBG~aWTMVd+Z(-Pn;I0`bz#HKjfPAKt59+YL@itARpAv7%(S?L>9|lx1ecV9V6=~bY zEzww_CR<|@<>8BVOte1A0yt&)Dz1Kj5pR8jZW}4kd4??BjDwz$8dKr4$qJ|2HA#@&!F!>1dwhT#C=6td3$hnF%Pi!HVE&aLHq|OOn>S{ zfr?nL17wDD@q>BjHh=IfxI$X5k94{$=lXPihj)6c3@K)iS!{n1Xi4QDsfHvDBb zU#Y4?Xr`fMV$3sN51{MOEJ`!k!{+MG*I6Rx>c4}oP)xKe(zJ;>MX^lGd+;ha%pTZ| zixg-!Xfkkvs1t! z^m%l#M%uUy{zOZKN|2$lO426$!71gj`Zq}UA&xt^k3PfPg8l*^j|8wdts`>1G-f8Zy%g|J-GWGeO@C5D?&dDFi|U{$7m1o z9;b{kgIyKa29WRp^TXA@jW^1&h3^vhFjqI{7|qKQXt)nB$(J7H+I?i4hb3XSo<=ySo;!VZDK^P|Pp_j9|gs+>EX0k{D zvc(KD$(Kf0Ek0=%EKvG-%+UI|=j%`|)JZE=q?&#)08u!bs7`??`erePNf1{@xI{jC z5s_i!5Gqyw?j6Xm1CczWbIbz}t@)|xhRpv;)f24)`;TfK?HK60fz5=s4K)1%oy=o& z)2Jsry&%3P&`G*NJ97gIDyIL`|BJK&W0WtoIX^`Q7ooB=IXah9Aal02N{sA0cs_K9HT6k2zAnlmKLA%vW40~ z1fOVq51VuaFhrmS7$kh@I>6$A*e}rYk*>hrAY4+<;cYWa0$o3dvx8az0-e}0%nk#y~Xc`E3;T^z!V5!NPg{;tOPr^9TsG%TaRuUrFM0`0@rh^6ZurzBX}MvAr&ZphX&Na|&O zvKVKAy@9*VFbZ-Ge~EfB#{9|RPuk<3*SmK%QJ`J{+pu9K;P!R$ffclcAK=P1e1s12 zd*!QENeZ<6-Aq$URl!LC?A6$(VCUfKH~0eTa=Qk+30nrwa%ezQ%h75Lf?mp~25bXu(9{Q;rT()7bqY1YmUR)vo4cISUiKmxwf^~#? zj;EJ(1Ss`Em3Yz>rE;JoF49ChhP?lMvH&@Epa!)1w^k9_nOPd$9Os~9vqZ~!sTx^@ z%De|4N5jv3AIm(6eW+EWmq)dfcIKz9%F_$zp(_S}SveK1ZEqqLVM&mS%!+_|Z89wxd-CTi$y}&i zt1$d!n0>0LUUq}zCBijarvTOb9QBhp%7t`e)l%WMQxvVjAZO$=nc8+CkShe982Bvo zoo>z{Mz&su;P-piaLrPP{)C{=YR44z&=hjFWe^F2J(QxYyyk_3Lg1P zpp#+|F#OkWT>T)f0qlICcJU^l_Wt#G5FO<20;)lV$z`%Dc<`qByq>0W_e(Z=5dr!4 z?_p0cE|CwjnWj$BOVvM(1-0kzegCFzl#yXXv<}p~YsB}zB3y0|S|)-BSDFd1eKHNf zXZyH$dq5l{)pQppTMy&VIv!+~Un1;btNg$?#5ml?x`Bm+Pdfvm=sNiugnQV1+^{#H z4hCODTh@q=&>JLKM=U;tI8rRGkUoh+ym0f6vS|`GOGZ7hiF%j|K5O@}MbzI-rq&^} zRpdp)IFnd&sp=Ix6jY9`b$qBpreTy#vlt|N_94+0o2U&!u;NHp0RR6gc$LJ>zf@JE z)iH!=yiFLqn_x8@ViamcKX(lAbC0t5sVne#Jw^ZMbEctrQh_$fs%k0BE$ayI%g>Nu zZckA_^^X4~0wCA{d`h%qjzQ8D5HS>Op`GFFp_v6S1dtC7dvgmzKLw)7fcN{U{`fWj zKp_a?e=MUkvj~@W(9S{UNR$gGrv=&#k}|dILr-HNp=0e!HnfU(_`+Z1K5Q17rNKCq zukv$0M?yK>AiPBQdLOW^j1yGzbkn>&nTEYQLhXCl@>LCzKR=ClA>Oo&=kuC;iGIpH zOtwb3OuT7?6?X@)3XBs&>}AT3@bR`S6Swf+zW-!#jOOn5$-+D-!^k8p_G$P_qIHlr z39?KQtU$e*K$jKLDtSNmCvmGJzuxPw_rDl;d4d`M@7~c*Ste4=e{jmz;TRm`Q2%z0 zq?-eof4ukIIQ<4zu*1Vwuww`J{#Rd*By;TorE(B!oTCQb(I$ao2#CrZqXoL$`wsl> z4#85@pP!C?-XR$NGUYMVe9W^%ORRm=6QOqDHi|`{1OcA;|FZuBj}JskK_(zqzi5kO z18)!Dd^iUPHt(Q8eD4|#M1+8K2_ikf^8a;T@U{T{7xzo76V(xw4!KIr2Xjk+9wRh&xQ1`&fNF?xj&!l zdcR+{PNeJ01GP%cGA|Ae@H@sHdu$&+&yZ;<)~Q#p?{&QbsKtx5@11UzS^sb6Xi)3; z1ti$9Po!ByJD<8wyAIULn!Wo}n|$ zyhD+$pSLF3u2EX0v`c7}eAwX>v-zt}kZM(%q+{$D=lXwhj^WTswWD0Y-cc_8{(b@8 z!OSy>wnv_pYk)U5$B2j^S096QjI~Mf_czYAOEAtB>P@pS%ZPCTT|SZ^(C->avy8%n zi=s<(h_^xh)_El1;tjFR?|lv1#IKtP$Qot!3f;ozhhRR3!pk)xh_X#ewSs-{HXwif zLCMWHN4tc^QLk{1@C<{G2mIXJBeP6Eg8+pfJb$ZHbBrw0GR>e2{E=6yM7;u>kl-S` za1)0Hl~9vxy@E{=D5fx2a@E-_I-2Y8>Ox%b{E&SO41O zaQA%u9Utfyc!w_BG|B-l&>AKCglXo5n+o-4+}k}>sx!yc}C>H4=#uvYFYHOR{|P^%Q{6zGq14fLg2ZIUIgKbCI@@)_g2e+sB11N`rIfo2jx zq+Urk2le{1!{Fe0yXWvT@bjow0JY%qwfm>2SHAy0I05tupl^2QIoBw7cDP4??y>C? z*C=#q$khiQ&m5z055v4dJt#W2d|{bZsaC9`S%whoTcZT32&j%3<^}o(_~Gj>Kc0UG z)hd`BDAlT0@DB{}j&Q?#sz}En?#02kdv0DJS07F^#~%UFfOYEhJ^T1_6@Y?Gv0Xq2O$Kg9dv{@e9F!)$~b z)-AP43|6@Yg9X|H?B&4${x|*)wn=af!gH@tPDHFy;Zd;WbB)4O2i$-#*^_8zoSS4r z64od;eOsUY{G3Cr5;S1Ifp~nZM9V5gsx`#RE}>ilhqg{8SKltdJD7dKGxX(wV~=5X zdF*kK4i=MP#xmV1rBV$$!BEc&HwXEcXP~>UTi7PqB?@l29~nBtb&7;~!FjtryPILY ze2sGqy2HS5Ay*&B3;}*T!{DieJ>1(p6cnReN1mR!Tdoo8ZJP+LfX`c{S|i*nQyJ!U zitG|#$Nsaf*C}R2LD);G{6s8z~4ILrfBHF^cEk>u)Qu&$BYKYretWdiDf zS|uRP#5!Uz2qKU_{QXy{80O;~%+vb>!O@XsVU@x$?iKR$@2XQ2?hgE)lMi6sP^ed` zRjvUw2atBium8~D?-u~aDD?{Ta!`2Qp=p*$wnaLcWzhTp^$&P4a*fn0a*ZP0tWzB0 z!29|+G|ME~saKGMfBy9dGtS1N?ww|zV3-%_W|{uFnOyys2U?}PLMqhV!>d)gMH=Mw z3f}H{yT?2od;z+Jk*;-$IJ8;D0Ka_0Fi(+=d-ynqQ_RQh|JBtJ5e0gOp640#2&bF- z27cM1QmtFaG^F>(b{jc~3=oU>g z1N`#!f#h-Tbc6h<`v_u>@XtA9o5DPfJwEXeh0ihpKQ`@ByF{-LP-E7;vi~c9JF)jo zl#5Z0O%m-A^UMf$wsEp`oP$B$o4w}g*~Ukn5|H`&-oX}Wd)~%6(l691sZ{^_Pp#6= zIr;=~Xy{b1OjEBw=U=l-saC90z2a}wpIRko+$t5{;53UsKWJ;^8#u<11nBp2jKeG$ z`f4!U5$lB0;RHMI-+q6qz!`#JKF}B3D}DiZltG?*xJvcQ13!o>e~K6!?@o4gu@IFCg3<)_OQ&R;z6K<`M-8@c_S5_rDX+zDJk`^$OMM(<32XFeiGq zn|~m}jcpRH7Ww}=oX50f#(L)h*PWyc8YZv?7Q7tzO+b_tB*(Z2rtrUl~Ar7 zerA&87vL1bHENX-?Hua)@I`vkp0$}~~VSQf9e%H!y?_F_fx4>t^wVzY!i~eJ0#erSf^IG?Gy8iLOq=K zK_#^LE9ik?cldiAaF1z}mudP0JwF6K%vi1yo31%e$A6)L$fT}HOdv^bn=09a-HH7YpUhB+vMlpu-gKj&y%Ci z56v-xa!$1h$e(MJX%_zePBAAQ=IgtM=NewUvgOOsC(x@>sRlp#uX(Ot672%|Cdc?Z zL!Y2RJ#_znoYVgtJ4@lzGsC>)%eGIPWAJu|$-#~fu-^p7_X53W7m@%opsk;`e358J z5Yaf&a|P{UU^sz`;p!EI`gHRTJ8G1_pVR&vHOlt!onj_g4e~w0pf=+jLJ(n=15^~f zg3_IBlDE&H@b7npc_`N1ydcpY=xdq@{jlFNfe)ZwAsFf2CeQzhID8skZlxP7&-SD%C7gKkP8dIsT|$0BA%6B&obxKRd<5 zIu__nu}ZXm+mvF3B>4NEx=*fttYd^*i}Z_w$<~}>xrR-$aSnL~>E`Sc26?PgUpI4( z-MHu%;1;1-R;S1@{%$w>M7zY5t96Q``kFk$9OIzg0VS7ID}o42mA`rZImft1K;sKK zI)!@8G7MI(5g1Y;-2@~Ct6C-3$Tf<_;n018-6E200{wK0Qmv%#E8d}1D#5-vM)rw5 z!LOS=!pb#%kZS)F*DmjUbLZ?h#~P(w0tLmc_tPw$Vp%574>ifsFW4p~TMPF9lej`1 zl+?pK*Dnq7!S0&0uWE=_vhC;-#<^~hL(e_JfIY!JL9RZmSYJ1rWXUzkH5%n`k4&?G zld@U`W=xi;#oE!%#@SDf3iYD!pSA%DQa}IfoeMWf>$)DC9OSc2Ja?OO4A`al`Yh9= zR?1(0f2!4Qn@&Cm^0{=qRjNgrYqVH9*cJ>=0q??a&?R!n$|7xA=?+<-k^@?I`qZ}L> zRs$e50lTMIn`Rl5$KXv$vOy4cmZn;=O@cZIk0O-|rnKM>dv=(6q*Ltf8Ov0cDDuC* zO;V-Wz0=eyFAko)(SWJplPa1Nw>&0xkcPPL$1C?xp}&JMXY0>F9A8h&M|)UiF|`e7Vprf zZ2^7E|zPAcMEA<^Y)K$9k+cF?-1``m{ikz9kdIB$wacSim#zaP zsB|ZEugKMYdEmp2TtnZ$et|_g_y+aM1Cg%c9gK68YGxT&Osb_$(d&KY=_yv3rs5s> z`nrWzuIdyy#K|_1%9UtqnrV@C@rFiugFFFQq3-Wb{`?0N?yFatWk7pSuaIa9SA3Z! z*RWipLfs~5h_^?WZBnEgkMat^U|Xa+#lU@=ZCtKFDp$5~w(-8#RI8G0VII5R0Re>c zbNX|@%+fd;I)}#D?{@EhCD9I(_!(xqgcv6rx?6;T;^B!oMuvI0#!9siFUwTlK#Mfc zrjp7f(Jt8r)7nKk6kfRoSb%?b{XvB_%fvnb6mXg5&0jHCpzK!PdzVX_ecRkSlC@y{>H=KkzLJ)jc~ zrzgXV#;-!W|=zb8;507N4+c-leOj_&_Hs}=`a*=U1;cmvWHx+fI$|5mrKbmK# zqYSD)cj*g6AHkT8l)~8^HsU%pM!810NTi!cB%g>QHCe zC8vlM720V-^RwcM`q(0;Kowjm55Lo6u%ise*ol?vEHVLq)EkXL)GOsOg_dW?Ors|l z#7pZ$P&~jOZrpV{FR{i2e$mEZK!|ndU%5wmI^M4B3F~GY-GH9|3_pl4Pwj5{dF+oV zX@J~O-a{+454U>L>*gB7wdR_zj7K;bW}sax(nUMnK~^g1<;OZWBg8pmny6G8Wi(6D z%);Dx^;T+Z5XUst)`)w?I$oGRLEpg>x=%9&0jKKt=Jhhn)hGq2!)E1ug50A0s!%(| zx{EbSccsVOB-teo;Nm$Q|8AsCaZrlO}j;E<}oaTzp&zy&Cc*U?&piQ+wGBN8fANXWDRC^ zPcfTeRjBC^xle1A9AEqp-u~d){>^P2fc7{C@WGuz(=2&}jVxvZBS}@OXJWE|lvW7UtdNdo`<)&09iNHI{pv5#N8155`_i!8%br%(g-5?$(~ zT5+WC6(Zo5Sx#&}`e~p*xXu^(nj`$JGW!_dEQO>H3;ssBdW3a~7UCt}V44;R0OzJX`|Mg3lk9QEN+tY#x@GpMM>y0Qk7oT=##szZlWt*lsr(!QxA+?U0@G~V zb3hpP6=bvp5bRvaHkx2PJm(pfZxCzm(@O{i+c^z6t zqks)%H$v_M`xaS8EThs)xa8B9>-1d(ZRP3|T3}Qi`b+!(iKlsiF#>MZ5Yz!0NCkM; zBx&3raJVrbM?W3MI6YLq?39$#!QWh9WZa)&G>TtA&rS**pPz+i(cAX}}}f&f9U&oR~%`x@y8dyamb zjhA{@s!Y8UGKwhqV#hx~62xy-XYiN6TbR$6{)bDiL`}Y`bzGf$o%18<#&InpT!j+) zMYNMokO{ z$L_VJ{z?7^LddUI?M7JnCV%4bFwbyTuMn{g;3wwwHFn4FRG%LVNW^Go(Ga7pJBik5 z0iLb{kSk=yWWWaJaed?eIMynMWi~yBpkR?=nx56k_fJMhsaCL~*6$hqQeL>rD?|j4 z#W=$M=J?o|&!$<7i|yTRJ(7t;Q>>Eb4|lCnDbg?d4YWOseDw#fPe%|jv0A1H^<2n4 z%ew9RD^Opg6>-$%_A8A}-+;bGTT<@_%9o^gz117nIPl>0KEMfX^40*j{Y9>;+nX~! zU!%l22|*y(z1+A;1lLCexMob;=HLJH`Sg$Y0ck7A?126C>lY;8`9Dn)@Zynm)ei&& z=no*QZSJy$18i3RbIZe1`=6Wd&#x&$kbwUf1N*VMT3+<(*9e?|uW-x}F}j6wZ3 zjY0gkj3EIG{nr-Ae~kUt<_!M7IWolmrm_G0jQ{rn`Nj7Cdys53RXy#L7Dso4Ye18a z&jUiH)o_3#?I!|-tm)r^0(8~{3OWQ`=zC{5if5l+SMckq`M*s0<8>l*?_^IQ7IMk1OxwfU&?-HJV93`M7{|;H~v{Bjj;*{$2i@;B-rZ^{@8zT$WSdq zJ>l+};4F=?Or4^C+RuO-RJlBGH8{Bz8Am){ur@CK{KS%FBvxUOGrq(le)-@U^9dhb zP+%5i#xwxff_@ifrAk&NB~`{F^7~Qm5E5dW66Ak(v)qHO(M|Hpx9*-~e*HD$nm$Ix z+MB1N9XCu`AoICNm(7-Ey5u{dJOlp`%$F~;Nv$)v=v~=iALi~+i!`cMH%Ood5C#qq z+V5wQb4C32x37QJ0}{|L8*!@_+@Vh9dRKAzQ^pRJ)9+s^Frk|B&4IR0M<6it4*ccH z+^Bl5wwc=`bCIMTZpCIE?g}loUbBac*95~SOp=9ci);LYzmwaxM{keJE~Qp!w_APS z_#(GZeQ;xtS?mf!xy=gA%*sC943%vC>QAJF*-u}Y>GFz3{roEGXjhJDAKDt5r zuM>DnW=V_mz-EWk$M>l=c83t-X$4y>)uKJj7<4XfawW#;YXDVSUoPD|Weo1Y(rjXB zg>wH=A>P*bYjbf!v!ntw-wwTOuSUD5H9W&Kn&n5$fA1Qohbl!(D{&G-Od@S-z+yz> zgof5k3i`>HHn4|zO0$HZDG+6o)n$AU6?qcmcSN*DlVaTBcsoDmp*v2KfNh89>?Q6_nr{ zW4B6IUdeR@O>4qgtPy2!li?uD36K#1LmA&$KAXm!~>K3;QfIC1s$en{K>@!OfS9X)f>K z55Emm=@+M&I>e-o>F2$IhPDX^a*2JSJR|O3m7ZSbH^eMXNWM5IM7fAoitC4#jxEF2 zt&F)oM9+`;vr4l{PqQd68WLO~_4O@EORSn|c8F_BSf11=5bNFSE7BG9R;gT~Y8HQ* z46=ZiJd16_Bgr$@ple5xJWg@+Iw8{%<0i;7w(pJ z4L6SwZeQOy0S$Iz8UmCcs5Yu20vlzXdaf~*)b0>H=l-A%ts~nKUB89RJX4xC(w3v= zPFHa-B=-ymB%|=JwOpIUfc>-A22 z#E>p}3pnJ?v5ptg6bv_5AeG6d8ayQ;8uVJ-kk@=Tgu8btS79ydCB3G=*sC!}MIW1y z|LF!G4I2r7ZnVg-%qMaHg>Q;T*Ba!_l0(|57FT;S%=m^0wyZLPn=_2fQ5<8@jTqK2 zPngGo0KusfOm>;f67@2FH}nfa{TuY>2eB`q656q+>Ebi%E|8RvxCx&_nmeb=+mon6674z16lc(|9#v?bK(5&AL#{mCOK_c}vsqDCb^~)kd;%-Xf$+=L7vh`Z zWDZR>W*ZP+W$%`*kE_x$=JQQ22RX|I4eXzxTZ#x!v1pOLJXEM4 z+SxA-aB_xLsrU1XbYdUISNYwj+uZe6X3EvGjeYX;1Ji8{2t1c+9%79%Mz~xe-#xI6 zUn8v(YZj)Ne%{e8O>+AY0OSKhD>S`g_6PrXy>W#B>)yFNs^sGn_!mt`H*EqtW|`DK>5(qZ+#*S> zYLssIxQFoy9btL>rPB3|yiHOfE6_hr|JV5+<-&866W1I54d|tDNvu|a^cMRw>P6Wq zl~ec+dH-OxO{W_0VWHF5pFH?AaIVNRxq+8!`MKD7D>VAt?`?O3?4(-rv;|c` zznb}M(tWuwP7ktbwfqu}FLbQy#5A+b(X~kgN$aHi62{jAs5wTiQBL6n*$0?*Nd9hr zU%irltis zyzwzO!kFuXzj$;ljow2U>Y?J_-d!DL?lICjMdsyXAKUJ+kNaoyDl|&HzkC*Fc!#i# zPvrFeq!|h~qTbSvFs?ozog+aXbzQc5@EqU4E?{A=ukqI?x#hj2wlcXnv{wHO;R)^< zL(eP{)SF=A6>^J&cGoBN_3=kgDL+Ok$Zd_A;Nk3Frsp3(+joS3arce|2*_7(O?~DX z&r(+YKEsQ?gSl~vEEfSjUN1v@o@BJn+(oC}!oRTyD;M9Ju#WBoAXFO=)noZ%czX>J z;3s8T5&x_*hZ_D)o#n%R14rmJsI5|`2dWCbpv4cwkc@Zm)KZzmxD^Q3@X|QZ*~ccs zHwbSljX_p6yRxp9YblN{hyU_`j}!^tf*Prt;@)5kOydep6ES1=ADUs?UMZtYv3 z8&FB0TBcfI9DKyEKgnxFQi)cR{-ZIzQ?UI#a!0cadneiC3En8{>#1F>Q$V&}uBKek zF488MX={=;xbq`t8YxyF*RE9*WhB>#dO^9pzGDB^HQ6a4#W~5OQtJ(Oi0AgA5+&O6 z9ojXe-0+(3lSZ^6S)qo>ZDP>|<(vQUflYd5B8%7zE0LB@_!M23_xl+q-o_K+3~i8~ zFlU3jM1!zz_Y!8s)0-kw!mCS;dbu_F9?}XRFB;J}yy1=WMWk9X#irWaB>k0%J-}cu z(2pN{zZB^X$)hLW=ZBlrlx}8uYv)K(O-rZ;x2D$}-1wj`$}Lo|2=dPQ zT8p$aYo+{V^5=P;ZX;Z9>3#>3tz2a1`vM(y&;Bb-SI^!yA?Fo!sOEIuPN<)F$Q#T7 zmEoGnX*3vxT3x`~eJx~#=FjsjITGML;M=_q#ctHh`xU&uSM$PntrSF84^Qn$wG8eW zWcy8^1scngjN=ph^l;aTLT7OE+rj}r+j%8mAD?WYQQS2i(ZoM6>BZG|bK@H#(Os_O z5?!{sM2d6>=BQ4WV=C8LCkg3J#uMN&!aGkX)&1Fe&>Jk(5^tGd#{Vz2(Gg~ld!9P5 zeTDiA4fd`r&>rIktw<}-3$UUuCs_yiWa~?GdH*HWx(A?K7pSlfE0xYt?4qewatC?{Lp;3Sw2~T4@*h__NHs2`mU?_~5`^@McyhMyGhG77R0YLGO^w0k}C-7iotlgmQ8YoClg%_yPu(Z!%N=7F+5#ig3pE&na?t`o}59 zGP-kym1Q{4k$I#}iu>OJm9&8U-#xrCwRsZ2JeIcza*R{WFHs<#ex1|AgOFp%ux^O{ z+YxAwbjvV#goeMRo0V$L%MuYL%w3@d)zcEFdo23WA;Tq05R7C3RO}t4Oa%Cz)?J|V7D<)gw@{Cz z+jO9cO%?$(oA}xU>gJ&TlYL@@%MHfX;MsvCMohD3kaFb`C5|zzZ_VmG_1sRa5Y|_a z#R&!VzDSzuXeT;XN`zo4R)!^g^SJt&Y-8$edF_*P*ec%u23Akvav8HcG2arUhMz7u z)pBPbjAM?GelgVJv@){-BDS0tim`oqlh6luaY}0wF--Brg-|#ps(t&j0Cy-wt(R708CGHlvc)4iB)IqmUkK85l0{k~$lj*+x zh(ewsB}B`qHd#V0p0X5?h(d0T7h9~?+k0BiFG8`)&PZi z^Ee^HqJ9Vui!2uT`j87uj~|&Y7M(wuJI{a zlIy7>>;nns;U%@wA0 zcbVQ2Ntrt3>Yc8|ymrJoUR5nP5mA0ktHYHv`q*VKM%f#;D1_S`KG4(ONxs1#y7_M6 z;4T}QO45OtOO^yT=BbaF0-c%oCFud(n1%+qQ4VK)UIt*VrQQguQr&BSCi41Mqg{@9 ze4Kma8bTE{*ARWIroO-tbyhHmhiJIzJ5fJKk}GlfH4;({&=l@ARHWtiohdG5I2nAj zZ}7MX?n#`h6JiKTQEL}0f5$P#NN)<@ z7G@NG@`Kb^{^MDBshd4RN2g-4FZ(_BtXC|+5ABYVjBM{2*7H@QOS)l{*dzdBZ}iNoodw{XrDsA#5(<166DG{ z6y#X&`wY29CQr*12E%-i@daguPql+<@oPyv5w>xWJJei&dWkn!$=_Gy3_0E8=^>^$ zrqxexnCt!ffI)RDNR=g{xGGgcVrw8((%d3d60BoK)dc5{ht2?+wXL!L3&C!1@cX+} zVw+;Ac7U5>ia$3y)Wp>Ic{g42W+33lI>2 z$<)#yZ;eWk9?#&m(9&e%)IH>BF8oXMZ}dATf0=sIKLyeiiT*j}Ip>gPP%YZB4e!6? zno-_8q852?klKYh)OHUfUPh^g9XPgJ&T5iUu|sd&Z(Doc1`Rg1(Jtbg9OOg2sE;wu zJi#l~=@4@erx-K*#oB+D-+MAFtWxIMKRTdYoxjxQ>N+UDXR7$*=UX-j{&77gqQ+*` zKS5h$_05Lu=c8TZxYIm`zY~E?$v;^NdZ0ydJPvCIDVQ>PonWlv9I-WO8%eZTcvN9! zyYJ1F$R%7Qzbuq$!O!aqZH)lnHrK^DrCM4f`GntISExHeg@iZze0>mng4}2M*Lh7d zxOR2dFJ`7OMzad(l!@?fId&qxe6dZ}M&O*dhWUAF6~Vd&xiEKWeCkD6CSzVu4*qd( z^RELWS*03Xk`@SGfZhioHdY5PXjFSm`#2}CzZ!MtQBs5k&`PTDx)kjTiZ#8OGKJ`) zWJ}(JY7i&T*{0s%_rxvBy`mm94yeD-6J}~$tuB%XHm{XzhxV1`GOX0eAuDHldgC1J$xp$6dQ%?nXX}@MwsVf~Ncd1(?Dg@xNX9zs zpTf8t;{A%7uPyKucEX^fCgKt1O%ZX@r- z$%KH&Zv5k7Pxg5($3&=X;~UTkc9w1V_Vl{vE zDwM+=Z`Xw#e}h(wKwprpe@5HKvq{TzbBq9pG$YG3fDO1V%Pc&uSYnK(NDZeu!BL(V z(=(TMH1`n;jRu3Y^qe3sq+frnAR%8-$%Nn7i*D?160wv1pNY*sn-T`jSX`6 zhhAuw}?2T&~Uq&lXph0l5~|>!b%|0c zSYT-z1@*J{I{siE^YU&P%GYU$)lE*%;u&{K6=a76ZVlxUMYwDbA7#k(BdGD$&vLNPSmdkmYL^NGaXf_5HbwQaoIg?cy3 zEKbNa@_^YJ{vOM4F`i72ms00GI^;e%O_5}e(0mscTf&~Rll{9UUSJy*N@&mq{=nAJ zj%S=UERxO0z-#0atXHft#1y;LuV#UT?!zY3p<1O!;U3N-npw17{AM-1#WJ@01AqJS z#w^n)BE|;`Mz;6^3i9-rngIp!jJj7Ns!$=GPrp1!>6l(MO1cQMm*ilc7~oO;%(b7a zRAQxcD)H^L?fFI?EW3}AQ?r64j>S*Zi^-Tommq>d?kS-B#yryoMb?Saj zY~T~c)?@NWh9lf~hj@WWa1Cx=7K&_bi41MTurxrQ z#=1LZ3L}A~RMoFFKzL8yJVATcHZdy)igmJmBvG}-n@4ciCOSteC!e2+p12|o)N5+Y z#idnQcq|ZD6`h_Ne>_JIZ$+|&VTF8AWZ>}=>MGS6Xa1DRQ>~dzvN%C&7Cc3dH-B}x zNqeP+@xAf%O$Q!3&jf{pe8}RE@-1K+KmAam&v+|4@%_}tou&8BhjyD)NI&suhJMC0 zMrcI0MsSDpDV-Z&rr&ClhH<7`+K+#1Xwq5{`11YQ;%~rDHFx zdBffFe&rB`6^m3$sV`;fnd$U<`qj(kVSQbq-%zewC9X2u-lATL+zwL5){c;)+dqhS zjE?Xvx)Y#ecYnb)h-Ab4)jUyH%`kpcz&ImHihZpT3EK{`e=YR~?#{;Zq zh-{r}_}e>_dv3u49H5IvU8Xs@G1tH$2Gldg;SBZb#~sxk;{x#-oS1%-Zn>Y0cz%eZ zkB4%dYJF^nVVP)qdS_+_0RZzt8IAIHH=|q(?OddEjFxH<-{_V92t5aE#~Dxe;QYhW z{O{mBk|$z@(OcYal%*o52USjNA8m>=z21)n*&sf}8HHJTLgH&s_TeozhKz9vb1@Af zyrkL()!Icywu8F(`C;vR!CyYbS|Oc3-QVL&L+ff4XC?G|hITuuuuiU#yumOoAl)d{ zyY@a`|F~fq?(1BrdUW< zCw69dMtQ<}N0^8BqWz@XNM=Vk`nbk-5KJI%1%ABH!(BAVfD6A~w~3@n%XCCqcQL51 z3Y6j2hErSZ3?~+?k`rQ$@SNZ#76tCsNvCH~t+zzBL?|>NUg_fpbq_B9si{!z2h_x9 zlPGMmdiY)O!WRNIncyz;Q=5Ix;R;o{zYBT_bwjIr*8(4g8TerEkz69% z#FxjIJ$z{50=BuKE2 zwiRT+Ft|wuU>&<5-1#*PbU-@|bVFDt4G`LucZ$7-`;~q>V;X2<96o}DeO~JXS0C%E zvn(}`%^uOTq8={H_wJck?|#YL@f6W|=nG z8qx#9_yigMbc*q1W?HD~3jdq)1B`huRxZoTGEA!0p!*Y#c4-`qcyIcFxgC5Un>brf zC=qrrOM)`JWX1Asc{=x?T4jn|N{mNo8s~6zL~cP>;LBMl-jaDtBVVg9AfQauJKZ%_ zxrk&P>#$h$f!r`F`_;S7yzGOt(oQH|Jx#kf!^k#ql5K$ch_f@!Ke<9gHIRNfP@~W3 zu4jgGl5duGnq6RTu3@Zoh$*ytt_99Ph)bd=^k~4(S}Y-;URkr~q01)i1O9jQJ=8rs z`nPzEVQPR?sJUeKKJXiWXZMZ*|AEzC;Ow~bS68kh`@&>q5v@vBaG(D95rRWx^OaE# z%Egg2#>p9vSeLxW7uo!iz+G#c+xH}&Z09JyU!YE*XdO(|Esn{*9f;S+=0RO(eNItR zoN8rYj+b9ASmP@sdNEE$`0g+goKMig0uC`gZaJktB?GLDfAlNfE=Smc?D~I9J{w}V z#RMnuNP5fvt*KU+VuNs)F-?ZH-@y~>V;-WPwS|`dCes>PSFS-n8)Y-WPPOm^BhxBZ z7vv|?^n!?W66cy^9%mb6`?pr+1LM3v$svPw@%oVP!)G+PP5p~=J7#v<3Ci?CODm-Gk%16 zOw*BW-@my&u}HOT_ut>7TAm#vdJ*o@inj;U$~*q74`CkmOcrQMHWX>^;>6oF#r24C z_s82!ducvz&a4ow(8&taDo@c>DX}Z~XJtFGPhJ6KI6s2>W%&}gXJ@(IW2)4xP=3t3 zM!BvIEmG4kbO%Iy{R$Ua75o_Hk}YRfcot4DAf`Ru`b5m z0&Wj$I3d-3_w;^Ew%QQAIHWx31U)V7>#hvr;tE4VM3OJaHnf9ty3QBUquxoX{CJCD zQJIuueh1Yk;gksG+p~ZDcyT1jQl9GR`D^Kw{$y^%usFCg!#c<$UN^z=k7ty1a0lS( zSc6|s%5D&AUTz)XLEl%&0nfzSj_cO|5?OJ^yuQ z0Cy-h+4<4$F%_3D-e?p9f=k2+wo-J)Fkzh_AIkY|X6x~TRm^lp)(3#~CRP5}YG zs7+30a{9gAepdBs3=@`FGukW60D1I1y=HutA%~T zDdwch%(K4m#T%M+21!6~N>xA)Rm#nB`}nVE3wz`A_}|`P7XV&Kea5dXRG0ANf=m4P zY=)_e@^j>Seu^thLc^gox-qc|NxrtH=RL!6liVS4m6=UPf39D469iPK`6lm5=ayLRluMX6aTq`=FdJ5EtlX>90?AMzReHaz{9k z>~qcBpdj3=`}Hz8E6}P)+QC2h(!4*r)yBEPOwVcl8C{tRe19jFrZQ~mmZRJ5kb?LZ zDndMY#1?2oxs>OQu$Z8@NA^gKGqwu31=HM@H9>vsoy1KYTJLpFuU1Wdr z^nL6=AL6YdEg?WH%pQJ;o?lRwkr2-T9_o3%R{zHsviA!v)TdizZ*4XUAP$HxPy;U}u5R zkEdjZ7x#VNO;4|dM>*x_M3$yh&F48)8WbDRji7F;WXoDz0jZ{;RZa3Q_3y~$aCKu` zZZS4#o{)hpR+)nA9=~$4IK^-N+Maa@Z89T*NVg6@>6Xz2h-Js%O+P=#aM(l|(X(SE z`E>GuZ0aTSa^Fow_vUe?*Z_An>8$~Ci$Z`zHl6ZmDf$=$CwRLxYPfo?&IH@QW|Gx+ zW3QFkA56v`ynzso9zSQ;uHd$h5FTZ^%L7%K-w}F3cgyzH8?t()MRWDa!{ZijyR#|Yss(j?a{+a^*c(=FRR&gvW<)2YxA;p7J) zQ#Zi_S#>A6kyR+n$12k&9pL=SpJY|0Q?fb8FVc6E%_-0a0^kq1$9SihhFL(pCE5o0 zV7(#ygB{?i;D4Bahto&W!qsCFh;_3Inj`nuRRior^=iC~G zXb(t|Y%j^?(Z^6or-d3HX$!5E{3m^jGqA{O2?cfx?aI+#GaXXp<{0JZu_ zR#s?i;|UDIoU8!mdrWgE_vi+-O5pB7+z!#m=D@CA;jB{gOvG~`9-KX`V%&YA%`w&% zX%_@AJA-7K?9xA-M|dGV_jfaFj0#dMZJ{LJ*Y9UyMDOKZLp$#V!WS`pxzJh0m4BGiUV4Q6PgFs+mGHSf}CbE@0L}s_(vkgNu)_$^IQjL2_LQ=~j)=`4$}6LXB)A5u6n3aa)v0;- zdcs;F{Ads)U8Gm13fCG&4SBP`G&E0d9f>V?!D1HmOhs&t2Faua9*@eB;;N_{+2kn3E`~WJJ-+wJT zgrl3$F8a7Tf{S%|2cYj1%P!DpXTj{!0ke@pzG0E9TGcJ=0@W+(6q#mfgF&%ziURq3 zi465fzGahS8xP`kL!UtNPsJsn#_Wv2KDqMcwl%^k$pJph!^k!eUMmxHEllf(U}T0( znW6T6iunw7iv1a=BUfP2H~C(-kH{@cE5h;tBd{AHH6@4U^{|&DtT`zU(WW3z?f4|wQ#nrQKTt>diOpw(<0x%7;mx6znxO8 zH{Cwg2ZEWSkG7@pmiol19n~xWV>~$)3GN1Yr)aV+tUc=GSf{d71r9nNLp*(k8!R6X z`wY8eOk0BE7BS(*f4o;WASS^6KbR1Ug-&cw_Z(noQ1n*JA9J2nKr? zm%&Xhu!vrqA6ZS!Y);%+f9KdwNQw*%Gcc>gz;(Ajirfsz|n{CexBk>-f7ck3FIQf7b%YE_{UA9Dp7m zYS;f}5CG6yaE{!e%(1g?7OT%v8U1yQD$?|dCYdH5ID=0ysunL-LO-R> zDtv@ollXB3SM5U*apY_XQH{4YX=Sm@lY%bI=CmKPyGMb+b@LM>W1RbIjzFyIW*wS2gD2QoBa|{0O zk~8~WNsc%NUne=X%HPAh_(rcSd+_-TnX44Qs9_ ze17%##U@bitdh$zA7a0Q*2N-M@7fXN&dW)0TpT?-efkD)AQorT!txzp9(;sbu z^y=l}T#O1o09ddO_AZULd@JI9psP!aK@RaOhCSZ-cb83|JOcse5QMjU9i56Z!iaXG zd`;rV2khM;cD!v-MzXbH)f4paI=BDwOlKg$QI(>7qGDy1nO_Jrpf)LA5C70KyDOM& zY?V;tYl!g#*)J6G337A2TZD2QYwr;G+uI)b3~*zB4}Q7_WG?oo(hx(3Twr@`_{le| zwCf4Zea9=(YYM*e3(+yJh6GSg8EgdS{)0iiNp}yXct@iDD;Jy}O!b!yiVg|o2`5BV zVpEiR9jvp4gk&n?i(*`yqPay(f9+Q+M2#~a6wE^u_BrCA5q zDMZY@ZGo>2fq≤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

8p_hAq^pmi_*$M&^;8Zt@TRJF}PPSs}&& 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∋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

%5(%0 zctsrGNBB&#VcaHNbk97Yco}j`0xuk*>cw{P@gO&9J4{XeEQK%0m+51?G&nh6Fv|oT zw|UM8-(nRw`3c!G`nMrO^Av|>eQCn1;2j)nzO=J!X_O(GpnN;+PTyo-_yP?>Kt21z z!1Ox652RgFs_P#+$?2G5oMo4kYz5j&FzwNpqkH1Xq3q@b`ip1CW~DRTnd!GMED;pG z?B$`xLc=gNFW8o^l&}0;ui*#*=5P)Nmi|uchfI^LKct;gow7+=NQ%6B=-v=`&H}7O|+x?_vR2xPUjc3oDuupxh&()`8yPT0p;bD_b2iTieYsSD;}x z^H<&-((u5Ik`ufQDq(V3zh*I{jd#k`AFXKXzJdcOj+j>|_VX;!9MXi(l@*C@pn2W2M=q9Z16gBq%J1*gQHe$-Jclx?^3g5_$ViF`Y4*KY%tlX1G^zPH-`S;qbxuN2xY-YJW!EVyZRIarBE2 zUO-glS!k}FG1F{zNm&+Mf0eDgfGNJ}f8P9`M(UF<+eXdwMQEcei^NBu+jyq}$0#$3&46)y^aQjjvBKlQQoeCXyqta_cyUfS(LSq<>o{t$JRnu&? z;qm=IZy4@~ua2z*h=Lj+Rd~YAB)TQ~I z4i^lLe0i2F@XtDGRN_z&2A1WV9S_{O{PimMd{rrR1eIhd*}3=uztO0y(?)@&^IfEL z36bb&75}K9t8|{^Iv445duO?SOTo8_G|IkSpx}roDe;Jm0#PT9Q4S+?zuaAZ1LE?5 zcd6dsB3$hAx4!v?599K!EfRB#z)sk$GSqO)^{M?+Ror}=HVMeyJ)6cBzcD-PIW zu1RnxB><@%@$ZiyTAl~_22{M+eC-UYHp$4ppBNbw+GjWuT$@HkTjM;3_@=q1xZo$2 z6E8jyK#sYjb+U#BdgR|kuE~zI}=lG8cB?RvP4zB&AZ`MFusDQFRfQCGS_=*esb{OGQFNtuD3c|8mW?>C- z74Lw`8xTGujy#kM7647OjB>Yq?_reb6dA1g$2*v9ne3?081%!yn3dTrnvJo@Xq=}k z+cjZe>wCCos!gv1bg);1e(@3=6d=KEl?wd^xMxOI{*f9wA+o~MIr>-RMc7w#$NW_g zdy#);^cyfoJTMUPXF7o41s)(g%`Ck60EI~_yrk1@koa8wH{+0_X2Q)euq->nrZar7 zNz8Lbx70RmmgP47D6?(sDv1zhjS2(NxT6vYeP>A|3s>6^0f^&KO|z!>rqyInB2%Sk z9OQ_|AMOs{b!nQNak7)SLs+a^z7ceZ4hJ7(HqpY!%0CqE{D@>yko_~)BOYhi5Za|;Y#Vm`eumDQ`jpcNjU!s_wGzl`zU z;IhdSIE255TPe+6DGxM!mq2t~W(=QtZO~lzOR-J14mR(@$J-bTnh=rybBjT-S-jyC zhk5OyfQX9ts8WCXtI(*(5Cv7aewS>nW}vAj`7-%Zkg~y*?7Be`!Ko`S3-^481gqR? z{^5)6P-I_;$yLTl*U&&u0MuBz=M3x22b#UhB|~=dp0St+=j;!!?{XA(rH#hUol(Cb zB4knx6Rcv_OW!Oa)c&ZW|5n@~< zrUg^O4fMgo#Yg){T^CZ=ca$+w&U{W^<*wBJ(4VlyCtrquYBib{8CsMFvUW@J%H0cQfshl>_1oyJM1w9_C-3 znN|t*+0IbyB!5|8YMSBUma;TYS%iHh9d@lPE|W7(&>NGOEv(g)gQ3#4y?wd|b)@UZ zPtq3|{6eW?RHb;q#R*oFyR)fM(k!qg%!_XILQHyip+_WS_Xe{Pc@QRIS>Zd4r@< z&l80 z)f1{*FV)32Qn@4{`1SMD99_l7AQ-?$z1u4}A_80Z!ZKlE+oiosF5-s#G!3jLJI&|~ z&@acjxW;mgnhGG>r(Ies{KsgUeA{gi^^pt>M7Wq2{>{Ky_|n1v$A6KcYR}NJdW~JX zN~1V`xu0!btx>t0zoSptITYnN)~XRut-&EXg+8418NBC0#kq_0eK~d{Z>(+kul#VE3@TC0D@tTa8l|r9O9w2a@`Ub}(|68r# zL%C5ux8jF=IV|83b&07`rB`%J%rj=J?`0^_>K{KQOvn0Z!cV+hfqf_c?!h6+q>B=y zf2xTHOWUiBcL=|oD2V_e--mvp8=GzxSfSHyDDcJMzW(@oq7T{xO|zK@Ko9s|sa&X) z_#W}?+(B}1E|bN!NPm0Wg2BZb2{$HJgWX_o=WA)Uk`pc+MwI-+Aqy2h`RDFpeGsp2 zC3o-F@i;D<6@ioB#_?;6?OigHYwl@Qx_?>b{Q|l_y`IgYW?0LWta5(&tJcbO(JtnC zX*C+wo_!V>*QuD{JHy)KhN2A!v#mgw_e+pX7)2jr36*z?j<+f&yI!gY_r2VYcgDk) z>3F;=%0s_(NB&nwY?i;=@;$3`lR}Rg=A+GVNQ`$HTr1K&7=;n}^05SZ6>kXk&$9eB zKte@)9yo&@YlDSvA8wewiO=6rcA(`3bO!CI)%MYGDi-{dd3k=^AE{?z?-@AG6!^9! zmNlGR%DsKDr)9MbM!j*3Np}EQ+*eIf04Pf3YeYwwh-rmulBd6N4{&J(MrbsG+r|Y` z`s=kLabl_7#K6|v9c2zGXMc!r#ymOrqD~bGhj7h3V2m3t4J;fp3inak#s%2l( z=)J0LlFjfG;iNAsgbJ9kjq)tmJjBPk+$=meTc+#3BR$Z&S18`-H!M5t88}xr^1F!- z+B?U20V2FiH6TBc?b30g-s@7!x5c=Bn3tUj4YjIc9wyl+H}5g%IX`s%-_h=y6z?${ zQrM=RkzoKbT_KNkQZbrr_ZyoWY1^Z11^>GuWK?|zC0&`E_qi8M@#JHcc6te{bkfeN z*2cKNY=G5DB2AL4N$>TIMLW|hVV^TUWPy?Y)%&jzQBMs@ZKFw7zt!4Djx!t7z5=Lg z2vdI|w7+ieEVI`C4RWcqdE{S&F859^sAN{#K8QebiA;#zW^LiR^~F?Gz88>XA9aBKn-UL%uz}OmbMZfxrKD0~+*< z#VHaxU81x+VwlHanMQY6&Ho4-55h}#xyC*11CS($w#={ezHgZAXx?l77r_j3Q*2$}pLX~3qLMyer1X+;q2>y|Dx z*d-Jg?3BU6-yu0767Qkj4*3D41qlw+uI&^%{j$O06^&j!RO!9-_4bxw1ol4HGD75+RQq&xFV#c zowWf7S8P|eT=d{wNxW#Ds@2k$T&`Lgy({gY$c|L9BTsXyVKkN4e0?V)K&%yhio7c-^$DR4-yY5%V_rGRbMP}Sd zV4>mv$6iMeHrrG&hvRkU&-)dAOl{=EVHRSMtmvYB7A7nO3Ljclg&wzTkDuapVff}u|^y6~=6Xa}}g*0L<|O=j-*lLG%raEgjRvnh!Lt5!VSzfh-G<6mS~ z>lMK_fxYmf$>e{Uk`z3^ug0k+*8v5U>EJ44WH~RBG{$ce)L3E{b*S@5goQ?UT2TIN zGrQ0PTzy(<&5y=_rh#IZ$2C8eG6|7s_Ap)Kdd+W z-R7aQ_Q<|1Y34XxfYeG3b5bB~ua;uZ_5uS_TbA9R17soR;*PG6?h8{7IYKk-qW-+3~MdBB4vHHOj&_jdoX(Lb6~)H?vGgi2-GS z^(a4q{|MF3Sk#}Ogj>>~wVsE&>(6I6cr}*#7P`WM@H&ADZdqa(qOBg4&@-YPSc~Xe zEcpS6;JFE5mMHG*5~oB`!=E{b`rRQ^uQEn8?J{H_9Uc6pB5G1mwHsB7~KQR4;h*bEtDDunj2{TtYpxMcJ!t|M;1^ zRWh`b-ODraS73S;fe-9udtIKqO>m?kKi2BzdR`jIXDA4(1Nq28A*#N>T!8fthPv#u z+guvMiqi-Rh-10V8+6bcIwq1{CnZ}RALqhy47z}*PGJ)--QZPB$>;mV{4um@Q&69Q zJ-rr^=@6c2kem=yY{Ne>EW6v!wPF~3t4_rxaZJ8dsI+XkwkC)b)&jP76G45it;o5QZvFrG}mjpdM`ESxci5Q~7tI+Mz;U!PPL zG18?7+f1{E0`p;Y0w0qB&ArS_&?ubhBZk3k+gf82%gitj`^2`Xf}SJWGJDRGskWW) zQCzazAs3Jb_OaHwa!gRkX5fi^a<8O&@A4Gh#rupa3k%u0wnzf7UdyUie$Q0UUuP3`u%EvJzV|stE0q;qlCKi8vK+_78hj>jco@7J?wo5zgUjC<5&I^Dn#z zPx&;#znRJF1Hbv^&T}#}K0V@{;L&*!mWq?)i+nnTd`KUL+4KEDJ=WR~>OArUxP1AV8PL*u7d(KCjo(JYeZ}!o* z0$$0_Nnm;v{2hA%yc6fWs8662+=C|>#b5WE-nL{U2X^UVt=&qAr^50GL62?*9brR+Sk91aR#2l>QzgKclVfZj{@s? ziTd+(&12A>*lLTSa`Y)_(C2EnKfP&Lamu$@ORlS;KHOXIB4|TzI>mc_dQ(&QHP4Rb z{RYtG4@c&bU#P(%+5VIV@$c!k%6*Ha-DC&QYQ_0vluIyR9K4UP8?0))T0E*o^s5UU z@#WzAnI3s3-eU+sSHK<4prA@=1twK3@FI9P1+t33pU}KW_1z37xqyPm=UI1VQ;y>u zqn<>U`oyxYjtO5u4SxQPff#*Tu8Pp-G-6MXQ);d1m56+xddx#{{7H7Wpdo+XCq77^ zg?l%@==!gBL{+7mSA+gwEy!3;p$PAJIcc+P|T6AP!)`!3sT| z6rY#w9*?N5(iV2DQ87cvn$#!;RgP<%m(Odb9-QZom2Y{xE6^tp7pi{r=nP}`u!iUk zyv4CAoF82}k@-Mit$T>Oc-+UrAaeV?TPma^FNr?Gp5$jn72h^9 zE8^W7%WS)1vFiM=gwtqKU7BM16RUP7(ZOS%Y^d#nexGx&RLD_=RFlCS{A;Q$`ACB3 z$w5HR!{b~%w!pV0_sRKKxA8;r^KI&w$b?Lf4$`i@>j}aza*SR(WQlz+V)@Sb<}`Ma1QP7_vpJMoaCy)j+6V!slVR-R_y?J+ok;+)qTQV^1Y|O*OrHCg=ERuC zA3b?Uw!8hv=bqlcJXt=c{^Ic_Wd!|oh`z0jBVZP?9rC?efpBn}T!Vt)#L4mOOnmcK zUS4uzGLY5MU;-%NCGuUUJY5&?Ywjg*17)3hY0Of)MNX2m+s`U_j(P`j@#wFw5CO(6 zkpBMr$1eKw-+-&YXQ4N*@Bd;R=TU#ur%c&X_Kq(yXv*z?nm1VIN6w?$v>BHF<K)_^>4OTm_G$+&J-_$S2V^kL$+oy4gFN>={U}!j0jBw7s!oRt ztNmM0vk^Z9Jicin-7Y^zEyhw_F|%&O{iDaRMsc#tugp07h<&D9M{YxLxB3!z$vZ`O zvgH9T=u?XS1M&oSsh~`jIj(=B^~A}ZXKIT+_fEDOuv-~cRnQw+k)cuMc3v-m-h|$i zZP5gG<*XGHU3{eBw>kjA_~V-9nFz}j3tR4*QJp?de=Vp#um!#L0EfryK>C~N*hM}5 z;sCkj9f#mAgy1cAxzHM%c_Qb|0tTG7aDBr+NMp;bC@6oT0+iZpcHQC;Z=WAH6z}{_ zj_uP9j(;@g+NqRr0>;@6j&IBNAw^fD-P0ux?xCp985u?;1cTXap2<25>tl+^Ag}WP z!>3!AgT-Wix`ZGko2IyGt{AlAbz*TZA1!=J`bQTeDW|avZTHG&2q`T@>qNLK>BxY(>UcP#zRX<#EY_c5EQ*9zmI+PtF zTXMQIBYK|2uXzW*5@22KTYwD9j=<+eVKSpR*I>7ICOqjEI_^jIEQ^=GC{IoK8xLKo z!fIx5^3RB{(7~er-yew(i38gbLY%6`0<5?@F1%`-szV#Gw_0puE=wgIS-Ola1Y|dk zX_9^8J~ZAHnUNLN_RC|AIw3FX*M9C+!)!DBl5P51@#Y_-jz(E>`9HE7)vRvKfB#4f zh?ee@x}^@cX~YeInbi!p8M{6X2zqcu_irHH6CA2fIm90ZbUSz7@ePocKuk6M7VCXv zv&B1}m7d~dvkV!{EW%lPqSd8w$Ys>gY~o&J3iqO?cyQ4D?r zv7}e|yzZEaz73K*ApvwV%QKYVy6zr;J}#)Cn~`kNfgwuYRk0$Is}R&;LOiA?8XNoD zu+(Vh8tN49B;Cp`5!qR%n2>LssX>fwJiZ_96yjr@hXt&D_U6}tU*7*CLZz!i6ZB9{ zM6&;r@G4Pql}olG%_U(AlL|tD2 z&@ayM=5U&!GB1|}|oswlT7r-r}T?M+Nz&$wtDo%l$ zY^PUOw5Li1$G=5j{kDa1e( zEyTe1x-ZKD5#2(muZwT!b@;ZJ;4=A#VGWaPpT%!es@eR=?Yt9RzA5otbbwsLbKh0) z%*U`7!^J+i0JuxYc(2%P9SK`Pwhl2V_!PynYs%X0C38_M2qFhv6!UC%gmw2dDxQ7r z^^5s=?Bbk-EZb)-w<5GOR=zbrvii1fxwU1HKg|hyIx~N|TfUN5OJv?Adv@rHV@w3R zqL%IR!hWT`OjZx|i!;VsCbtRk(&|R~_`!DvN^Rsl#7V8ZMCHmIy0w0r`ibE3E#|MC z9T&xQ{dN6VaoDs<+kzyC0d@Tt=>;uReb?A?%zvU!4pSMb1ED?&TdK7fAz&;|9>F%H zX+HJVAu+Ty*%A3Rs#}2tx`$MewI;>IAPIlC=v#(WEw;M6wPw{9_3^tuA3H|bzfXPG zv=bN+SwHm#NGhU7yRa)}w9=J=AIu7#O}hebsi%L_;l`lBnnvT61ZL}iCWuDRcWnt^ zHU>y;mt$Ghhk_q0&)cGT`zUxnpBR?AAthP;YA84Qbb5Twq}7XUPj-9PUypr{`pcEc zv_+mAq>5G%J>UF*vR;CE-|e3(u?fwgKn^+_U#Ql79pM$v6MB!s`X(Trhx?rz#P8kO zh(mNZZbOw)gJD#X8N=h>3q9wUDAiJ45GNU^(wDvqS>aB10i|)siUiJ#>a0<13D=2@ zKcY+}Z+F0VAc(%nYgB)EaB=A7QH<{3`fE_pArABO8ftS&{fdtC#3MxpjQvAC`^UxV zn=Ird>s#yuU>E%X1-pW+6$ZKz9*CG2kmV@*kmWq!KX)laQAY6X4L|ZHLWvOrWN!JE zBRsp?Ki=_Yug*6#XVv3@7x*o8}+X+9)0-fuL?lv=XC2fknwb3llV(6^8ty zwHj_w*cb09b|k=x1J5-oFzEv9WwI!b5zERx%R;pCG^=Y`wq13B3@}AezqQwtN>)Va zgD%z{D*DWQddyIh;uh;5_C<~yr2E?B4wc|1gn_s=L$H%##E7N5K&B9QY0udtE72|iRLi@V z@+1c(Ky$|YJRe@^tq4qc^(z&po25U(m4yK4PknadQKs;+;kurXGL&u~Lh~RU+KXSi z_7 zT`swj%#a7OYtaba=%>u`*-_+DIjM-(0vfTk3H7tmDd93A|G6?lDa9JKtvJ~AKw5R8 zpC(T3uJ>*=SoI^0DOkiomym8hVQZjIvLkcBc>B`CF^*3o{nX2|?fCNO;>UZLR{bNS zrsU9e8brQcWLT}1FuQODB>jEeNDaz$Ub~N=E>rd?%gt8 z-mZ=G=eal37EzjHI@Zb)_u&hiP;w$)B)^|pp69)EX_O<_?e$9ePVd3jI*z>9h z4z*BkNmnL3A&-Z9?sBtB zdlRS9?oGfjP6ljR$Vhm086Cvq{Z&`YVb;^s-un%^R&L=+dQGZU)hr{nMWw#C*D+~< zntk+--#Fa^Kq8cJAxxji=$bTXk|#M&PD&s3l^WhNJi-amFJJ7I))aW`i843y^jKoZ zq=J2A^<`?YR&#|W^);flN1w@&?7l^mM@a;Df=LSg>PQBFKR>`FSjWFimMoTC54UK< z9p(d*UaUbjjlf+>?*zz;??Pd~a!VfCFn;si-_+c+C%ENVIy9C_$ESr!^|}7tXFu86 zA5gNx%PmtF-^m2Rkv$%7g?%;aHmFu6j$el9lUxzwE{=7IYSlNWLHGM}%;Wt1gGw!r zpN)Vi8bwYc+^5U(+2P^YGx@LH8!>E3y}IOUozjo*NRKv&dXu$STvG#zE_QwC)dJ%I zG(NxL+<>18>NC8S=~8#dr=^Q-l|VgyK72axvJWyGEComlpUZ8Xk|H#UtdU2YLKi6V zq>~O28q>AcBU$d;e$Ay!(Jk-S_w_pF*Qnxpdj_rZi}85=Vj8d0fBhWzKFJpEyHjut zcR0Shzg)46MCo)n~Lh_`S6=Vo+?)Zh4GZ`HJ)} z)Ja_1CSaS|u#A58fY=?3f7B{ZzINO18>3Sf@BD@?Jc#q^5``ctRHDkafp&R;SS`)A z*{$v$ud;WH?47QXiapOc_d&sC& zF5etquf+euG2xH~JD1Q^ua{^c44CBL1Z>d1Cdh5O#&eG|t0-}}qcn?ZFl>mkC`{OJ8*U0fp@sapHDFkyRa@I}jV)bi#ch3^gg8!zg<<${U%;n#-JNB;6qq*9{TDwk7lQ0E*eDsoIJDze)l=JNadhke5-Q6^{9 zb5k0SqjxlmXT2(o!$2wq)V}ub%Y?3oTws_ND_=#UrzB}g3-RL&WJT>9DIvngt5?zA zAo>}Hymw)73f%!&3pWhb1$4p6FWkEci4#zxvr!YY-fE8FR%*#A*$|rBewkPdstN3j z0G4aBO#ddDEPs`E3jxja+N*fM9uSpiN4#>!ZWZ<1%W=WFK&*Lsh2|@xJX+*IHoTL= zJmOx)*t0cZ9_Sct(Q3bMsCRT;$q^F4%PQ_`xuMqa3Y1jin%1g9ek6xV7khk1A^sbJ zFq@{tjPvclPYV)m$ZxMavi_i?IydBu3LTn})m`#dXY;e+Z7ieko-Y@r2E4=2P2hm9 zA1Imn@>Ly~x3EMtDdn3qho+6(JsTVkUsTCK76g3U0Qv%^QOlMIxOI<8zin*C#aIMbq^B%u zlMNJ)or|xzP^XreS*F7M@9%fc5;-sT7bvL|m*RQ$6k|(@$=X1*Uvx(-EVx#|HGL1G zNbmz+SD$3c|30^Wn6MCNQPpTYvF$EgKeszv;f8-F?64tU-_9LHP-^omLFn>wFww_G z`L-3zH*e)D62Nh{N|IWe>LpcD-!kfj#9MOdKUs}^KRY7pyawh7^8B+PI085gpp!!jW8?) z;v*`kh-2b*Zvuw5ohVpk@{wiD$Pgzd(|qxab1ZuCOO@R|%xT;B&<2SGi65t!lGhUD zqav9c&vb#Q3QeE1Xop{jaqcB<+%1Z%$a;x)t3;~UDM67&h@nPMo2XvOiJ;KwG_k=8 zQPZFA(noF99Z^Vk+Vt56|C4+4v}XbOD~L22GsVnt%*@Qp?l_K_nVFfH5;HSX#~d@W z+r90~o3pcTzjohDeRQ95Z=aS*rIJ*VN+Jc#M;w~B!$PcM-T12TrmI^_e}vo6Ca@&Q zPOCT@n-vX4ldq%wFinK2Sbm;VJNI!d8 zTHHE!zp5z(biLx}JoF@$q2BEfG6K*caj01)iH2Zdx%$ES<*6mw89It}@mAzr?yse{ zACv5)n--Wej4UxLyuWlW(J2Y4UoC?uM(k^)nr2~as+qrgOD9&yQrr?Q`j2POyO9{Y z3U#!8;sf>a@47#_RyjI^so&1R$&j*$NNKuV=?7qyGbG+R4{4r23YqcrB=)#OUfzea zwuDW6{>EfL{@&|F!W=n1jtp`-@TCtb8Wm2&l4F>+u>62rLp-WskT0qwlY3h)@-+0Q zYdn{uQEnGUsYzl~s;V+Md5!Q=sMjsBN?MquVy8=8m(#W#1a6Yq z`D&ns5Pzz!mFFJW0Pj?f*M9GS&n3>!Ala$?@~A?-ejDea zE4N0|9r`m^FbhV2imMmU>IE@5|J^#OxCt-DHPqmaoW@3`9HJjAkFK4VCUMj4hN_C+HOK zS;i7|%D4USz2Onxw>cn$tA`HNU`3go5T@c2cSBPAe0i_$e}_RqnT+Coi}dMEqa6TX%eE~;}gi4{-yxc z=rlS78X^^tqT)$13p0Mg(j`WH(3Gr5+Q{v`hJ?~7r5vDzJ6n`kRNN3Ne2sGwjfF;J zc*9?O0^<#B0;_hd@^8r@!2(wIyll@k78X+i1K;0#)SFLH(LQKL?)RkA40E*oM31wi z2YVKz$0Wp+nuHeM-wuJ6*(>Bu;Sv?Odf)ApyMh3w1PSi#dd^54SBSB%lsTx(WyPBT zkS^nKcp6(|aA^o7v&aFWPiZwK+?I(E%kc0#Q1fE5z(m?|4f{9?ysoIv@?x!8116bU z_)BB{7*ZX&rVooXYFB8m!%qO#ka}OQxaZ0z5vS@PX|6w5qJL#67Sq8~&Ach}?+2@F~e zw6-=I888?dOs%kqws9>Y7?(pD=eTLxjv^Y}XTGDnBNIK$>&%*ss{#6qx|bz=bfnA} zbq%|}L>1?!CIDlmRF5sd&7R}l(mVY`8illVLIRQ(Y1NC2@g3qNnkv*r^hnh|w*KbK zbitfHs7V*$UNK)iq-Uz$0;V1}MvFpI(6L<4m*Py;9$FBPtVy1lKujJ4R3Jr*T%)6* z!73%9TTBBkpi87gcZKH8&!DG1WB}0vGmObg2OX4v#7p(a}2CUhwQ&T>^{~gEAg|BT&Yk_ zW;@u1jgoUhl(-3MhJKj-85_n~w4+P97Ylc+e*o|8hoxA!Ee6cJS@*|tdjIb8&E%tk z><_ni=8TS5!gjHT2hb3$pKOW#IP3jZvPM2=dEdB+o{#qSpgJqMv-ltXy ztp$cI@DI=%eFq7bQ{{j2D;TE_L(o{^+t_AS&ZB zASdFR0%#B&HV$f}OFq$Fx>C^4*bVXL-qxK*Gn0dBMC)ND2X9>Ik?0tUf2!4KsU?${W zCv*Qf1UDZmnCx{%=`3xEiVs&ZBWP2C2Y`N5hx)CGeC6HM15jvwv}6_GR+q}o-#OBr zfNn9?ifJ~?1h&w?@k^I+fcwv#3gpviv>fZb4LcH+Q=ZA&(u;`s zDVAx))16JYcZcT(>>%qTLJRI*tdlJ~7(j*E3js)3qhuHvdc8{|$GA!bbA>PV7FZDK z>G4BgI1G^b^1*qna=s5VX@kQ(lkBmLU8XX8aSS3F5cuwmAh18NhkaPUmB2PtE2{mB z8DAuH>43~P6Der!2R;_|2;7RePF>AP@ZNEAoUE&LHa4E)I69$% za>B%QB#%GLEXy>*X7iV_mnQ}jKf z21CN%TzT$Y&P0v8B2~$*eO#LL0S3pUURGItF~sT15Ha*~)PgVgEA@J^!tI)u0A7hU zsTkG^6#s=Kx{@`;M$O+CCor?ZQ_m|m7kTF$Fa`KsShEWBVeOynT|=EOKUQchQugr< z(hyWAi8KRlRAx}FIQx(LHJ&{&Y`q_6C+;1+p8TK#I~A3KwFnALkMoHD7GQu8j!l>B z&%kxDqg@NUR}hV>Y;=7JKJ$(bY^?1V$I}yEVIr-Vm*2~b1O0A`OoH8CxGj5dbdGy~ zlXjVR@TJoZ<~8nf5BR4-u}rk`pwit=Crd(9z0fMtbPKKg*93d_(9Y1RMXqtqVRiav z{vqL2r!4tOqSxjxoU@PDV*s$vZzxQk<}K_TXZgPeY7wUieYP2iQZNrXavyP<2XPx3 zlj#`a!NJ7jE2F?3nIEq{aptiesOP}*9x1eu;=?7Sn)kjB&nrViJgfbBelg( z2TJ^XgBx?qk{wcO9*9>Dg;@z^W;o~Plb@T#I)aodW*GA|_wZje?zNi82e;lW(U7hh zrQzQj21c3^9m1h(MIioGul1;1!(^X;9?))E6?T{-&oYwl1O$)q0KB;AzA`76q+1TW zL%xe{8QL()WK!u6OMM)>2c%0Z%Dld+g=1SSg!BJYbUNa_Hh1At1v7&+mCU(}4D11G3*6#|Le} zDlBpKnTB_WkUBELV1RJlumGep)bx}?#EC+wiFGvnkY*(ELA%RegcN=1CBuRr;z++2 z0e2*Vok1Gqi}ZL~g96Ic^y*ugg$ABkqcaw1LVW@44mbgk?mnYXO%ch~?4#2oB-<g-mXFd zCq-wsJ@3v9kKr`QbT5)D0dn9^s`QFPE^9qfL&8Ys#Bz8~wNfl=@L|dsxVR?*@WThn z>$2$3DkSwAS92cel^qHN#FIcA7%|``(6K zEbr_7ijR`8R|{*Yh$!1t$msZcz>h&4Z6Q^!2kEnn$k6|7;|dUIS|MFx%5ak8nITi*H`h6O4Jg*leb zdk61whLB-8+F>m$S%lXHoKX4u8I}89pN%_G74!7g8+1uQr3WOPkmH^}px{Sm)AW~@ zlWkRo@i}i;EA0D=QF4S@;HPG$9uP^V%M?%5SVn6pjB4GPV!kJ4Zmot*EkEkuqhAOtAMLPT4M=^+ZL=~r4=iZDsgVGI^M;w;(0P9i*sGkjmzqj6!Cq~1NFa09(IV+u(7{@AUmT8vh2Km{+ z5RXC~mMbZbn<;v#mE7UU4 zqLp20&YeuL{yzz_df6MDn@FeR3l!r_D%oxo;*n<|&Qw+P5l9&VWAzdCNRg+3RV^BB z86K&}XGD)>YD%4fHJB%ur?1SdA!#MJ77uWL55)NbZkERM$hrV661H*aB?c@@QwHS$ z?#U%RA?KJ_XYDfNb3Z-p5+QLv<0RKWTfOZAzO=6~_wch(2r0oswirqkY*DftexQ^x z+ns^6?>8-BHQzFgHK}}H>qX|qPq(A%Ag#RGv~uBF^NHr={QYe)9Pc5Btu0eeDj)6- zto`(MM8&nR#QEbe15l}swLd|xOfbr)kV`W6CS4-_?%X$CQI~YcS6bBwb%}AqCBLsa86DzE)^f-3a)^?Gq+hrQ7T0 zI*`EV;mj1jsRWJ|b_Khg_)TD;g!vA!r06ZgiT3>(Ivf2WxGL1_+%QbwmBu zfA#N%N<<0}$|Ekes{Ygoa#39z5tf4e-b=AezCiV*BGtz;Te1CdC&Pe8QTfpu4Pm8S zW)QvUn_JT?Dl91orXz_S+K(z-bm>OQUsZR^phmdJLti=sErUBzbqFuH=x}`~a!e2j6JAZ_Ak*r<5Nd&1-cnobvwXFTf zs?sl`)mtlbBZ7q?u@Px9ZYDa5oY3TN4|0RRkARy#;4JfJr7%}E1q!{HDVb)WqaPhT zB1QopZnn<1P8$>uJl^}jU3_c{`IX&xwq;7-^V2#zvFv<@bEHY#cH+Kma<|q&Ln6>%Npw0qiT$`gp_Vz0816*duXdm#VnM^URT;yia*dC{Ds-<8xaGiCdWa1xoKA z;CrJScY^(TpBLN${Wqo@!_B@VYmSj+%AXxq_bk&wt#aH1JFOw_n?Xs|zXm(LF=6`u z#e`(ZUYOxWHC*5G_YZS5OT&7FyC+(j3&YP&K~F3tSQ};S5>l;Oc?$FvU6^G2)v~W& zI7oqHNv4z@)JHk=L6VQ6LYw58rkM@1YZXG9`Ud@REtxFX6z{^AU>VdbxO9w->UXda*KhL-g!^}k z_%0u(Cr`HB5pwplX)f;@L;_9$9-;fkZ9(*EEn(o^fQ}xYKiH#Oq8*=mIR`3Lv&_3i zpY|k5Ai?!Jr#Qs`A?&{_g#E`MQZFFm({YtV211j}g7v=Iv^-&R=ztwihgLAJca!rq zN7I$^qzU8UzE!bz8{IzgEZRM<_c>}R%{dV2@Ep@?KFE!6{S>z=<^V*t*(}aJXBlT6 zc*EVELvkYE>8NpwDGg>*%hg!cX=Hy`v!Y(QFsbAwQDXHB-t;vM2zT$4m2YMnp`C}C zMfFhXc;^x0D$zLrO=S=`0j`Zo}vV5wc(pl?ID@JbkP6RL_;T~n2*P^+;FVen4q+JYeTB2=|A6c@AU!k`4 zjJ#&JOw!_ueRXBur+eZJS6jMpZD4Q2lgzi9oFP>@C7;SgyPslthVh1l_X@bdaL5A} zYyA|NR9>O>gqPs2jb>G^DZGL=)G_zMSba^lGWRsnlmt{x~@CZva!u(6Hy+US6 z0@0M6LQ;O7n0u6K52Y2-1kyb+gY*(r`4(u2R71HMwPH7z@2@F0)ywz1?s$A6`x5Xg zO`4oZK_lwE?+Hz)(i@RBNho#Vsv9HjE*d`e;Ne*L07Tkybswu=8b3%2U(b_qJ)PU- z)0r1C#4AiHFKdak2RMbh4gjY(CioD2_}dCYGp#7i?6#SqWzv&cOV_n`U^c-QAs-n1P2*SF*CKzEq$HL0ijdC!}Q_Ldz zV51$HX19xeza3(ZQ)1Q3tu6WT2klh4KHE@C5a%!ZDCSvpf<=0_`2M*E(jM*_^(uLA zhX<-m9n5Uc7psJ?<-Y}ra&T;FRHWrlxvJwBDEn>N zy>{i!jB+Ji&Z)=|T2Ml5P$3!*w9A0So99TZJB6LlS6y@(-uD zT`kJoK`MR0w|2kQ2;%|R{;HH0Ya-vZiU&u@y_X8@(DLpgd~$dZJn1uKD3Fhz zAPn?4XHdg6&rwHqsYohGE69R644HWOR$Z$|uJ>Eg_W^Er>1T%(4s)EtmRE4AkTh%QWbjkky+jeeOIJA(m_})TLB2Og5akkyEs%4oa z5w6L0;;n5WUXB`tRo@OlP|%21wsG6%_a9(p!HgOm2q2H(FuxPZW2|qHee|%`^IXbt z?&)5o1TeD{H>Jmc5RcU|JINf!9a-ffOh;{Zt04>U9a5r|b}bGd#xTX6c=<_qD%(iF zyH$Z>6p`RogXP)!2wk*$k@-&V5l*zb`iojoyagmcs?du?_+X$90I*H>NCdz96NF&}LM}JVugEC02h~w6jd(@wqh9 zcl=;vk16P)9I;O7jX!wCsb(3ux}xnrx^@V$4hwZfJBxE5DMf%V{9A#3P{V#Oe!3 zH2b0@pXiIAk~N&y<1@E3g>EimQ#in3idJKI3C@c~YwrlmEo>Vklbk1M6){g)Ayw0SrP$23_Q15%Zy*d$h^xJ17Iy&V^171)kVvCEVy_74Sq ztPHs!W$$I@-6E7F0XQf8s4uT^qJ4&em&cfkBO2v|#yCfoSYU_eDS-i2>iIq{ryI1z z_rC;0+w--4_7Ls*IePq%>4XK0eSTg#M$Hh6f1q0>n}*Ne{24{^+Zy%R2YR_e*#EUA zr&;VTo_M!;RzGLH{vb5b7X3nN5Xlz)9;Lw?BkQPdHc0Og`xR_a^Zbb9S{MENSe8=% zu`GFHks?cmOW1R0%^FL%s$Ik>>|sgU6E;7K$fOG(RYyPV5gqO-QOiF@y4Wp0z`95Q zlMykxbc_b7a4)hBB%F=n_3pRHfLVq8nGqsYhFxo8o$eLBM?ZiYVjW}=dvy=kl&pBz zc$QHQUpODF?e11*Jy}*Obq^n2Xb@TJx4|-E?*lA=TemWtn!asUpeyXkMYF~$>^LOT z!SO!$03+?tR|nEfv*&rF=oeqFxoVqHbj~OEMz}9ZBYJY|e)vWi-{QylK|I;+f)6$x z08O(XZ$ku+gqq)y=h9q&f`*vBP4-E{Y=PDtg72Vc!y40L;TRgO(QL!Xxm$!J>(-D` z_5M{8B6uIt^?89qtVq}3s?hqFCdH>I|HpQXFlxM8XB5`T1iIwOILMK(<8EwfCi>Vk zi#(r*8V`~n223sCkQMw9Q)lS&a*%97$o%QGTQr*{b*uW>bLS+yDWOs;cwB`RhbsMJ zgMmUq)$8a(iE))~mx*h@85!5fxfZ0DN#FfNkhgr#)1w5xGpY|@nohMUHv>rdXwJAX z&Pu)UBJ5W#k&YpsX;3@Uts0R|bMI)kO_d(H6uQ*)Hc2{{@2kbm4Q{-5=4j> z&0(=i$Tu zmMPQuAMFw6|5tlN_`~e_8RL$LetHj1yS|TuMa>b~WHN0{nw7Q^=%1K& z(pT39Q;jku$?2q!H%d(ztQhj`6fG1R0Pc!cQwO&~Q<=OyUi*)e?AHQfZ!Ng~e~$|< z{fYI=vBo`Ri`TIdY5Crldvqt!!6u&s<2&ITZp581y`k;*kBI~K$r~ds*^!U7eU!pY zlh-JL*CzLXt}^iOAl!XyuqpjG!R>9jt}~aeuCvEINtfILpN)LP3H2+VC)lXqVy&rN zRjM$fZ1RzSB}6>M?t@_d!!M>a8n?%)ux!A?cO~nD%@Lxo*LmvZxRVAivHGcSTfIrz zTUI*_*KZFPAmrAZ=uk-U*D}tkbfNV9u11Yp$#hgv!VfVhEBaZ5DaK z4}~vc0~7F7&TCEdwY02=v3D{HfStP|$&5qc689Ls>1w86y4~>0V_)dt24bMw@a3vj32!V$BPh_ys-S5B_=H;oQP8jVM!ASLQ%L&L*() zzDp(HJ`?~hP^n6t>aJs=Mm&iSx1n0(u-8RPaaS=}T{Dkr@Rvs_+Y${*eLpde?bddH zP7%cs(z~U_82nuxjeY|Qo5=c65q&2;YtW4!e4`KtVB{|K+IdmxDWp1AMEJ<}^p$RI zBk!-_?(_-%+%nqXr@b?<-aEI!|Gv)BU;Z{2*Wxl3cyCbza^hW2l%FCYZf_c+Y!rJs zPXbKLL4* zJO206+ym?d)>V=rQs$n!$9aaXvH&^6JwET{Z3o+r@Lli9PI zoN!9J9c3D8lx&I^&G`FB-K1W$Qe9&aDT6qSwf~I%kT}sMShzN-_{g{9FW6ppB{#up zI@d~aZg**r^gG|FtkcY6o=-;56(y^{U#!gGH%40t%Raq8{>5RY(lh=70SE5FTx;(= zLXMxe#7#{eQ2GF&WSZ9|U07df`&z{puM)?Fgptv)(JXv%QID(4kJV#fV(#oNC{KAS z&RqhUElBC;a#;RW%l~+MacgBOZjs6dfOBS}()ko_rj_60z7{KwYBhKqYgzIl=@$qG z7Oyv4D}#DuUeKy}z#RJVXW4&%?tGkK(O}jdBZq~wrlXUlO>K!m&l4pd=-)TMXCl_j z}R8P}00e|M@mD?~iO73ca-h1D4 zi-gRl$09ZEVFo3F3$;fY(Y?5es%5Uj%E(;Q4iF0$zcApTv`H8iYi&I44-D_z-;}RO zzciFFU!8j*H1lrfSUh^T%_|qfYV7L@p|MEV|B-~7L_9fpq0ZGyxO*NGJjav-V5jff zt{4cBbvivv@2D<)pun39AIDHE!a%S=z>bvpt$e8_?+k^%#}a<=Wk77V7*G8JO}W;!EbT`c)joKw$; z7`iXZMdvdRE&Hjh1cPP~1-&Yq!=N*1s=jT{{lGIV>~Y6asriS`tn-~3A7+PFkHM`C z6_wY#ioEOuNlh z=8@K)d?fYJib98Lu%`EOgrd(wJO>r+8VgtRYlC{9v*K-9SpV>hzzmC>wGIz}SV5SI zV}gV9E+y5SLGPhpIX^l6%(D*^Y{5T5SGE4I7EDLB;XUCU_WCDuW5Ru)k6@P2jlh0R zXWz( zB|TF5VrrJ1U(MBqvW?4Y^){lU4!!!}S)T?ab`?bc38{L2cH`B!9zs^bxRr-s#U2B@axxQtoPd>wHXT$JsgghI6CguzgIg<#8vi| zV`w|&m|}R6sMCo*_MK}T&z&vgSXyNAlKh@c$GVM?^2bcyR(C%=e(S4kwuwg70Y-}z zkH9crk4egnp>(NVvskd~f%9DqCZ+f1@#-u&C%x+~$@?v%Z#va>IkrK{nW$C$6s#k9 zrkNBN#JfqK+h2v}^yy-d{x=zK3G*+zea??#!Qu}zxBXd%@Y$*++u)bjxriR9btVux-kmP=| zXY+hXxTnWI2Y7xjD9pq}{(msMyRwhErq8%;CzV!q^{;{lmqKW8KU&8`lhEC)dVix7 z{*-q}(^@MuHDow#IPGXSQDh6|oxsmtDtFfQWMH1nKkbO(LRKEC)aI;Pu@NY;7>P<$ zAesw-iF49ZsUX@RpZnn|34CIIcu^X2d71Zv!?v>!OX!RCOK%b|V9P(&HLBui@H*={ zl>r=6+qLbO&)<)!7bVx{ z8x@_!A~PlxpvD7_$posRS{cDqh^`);!ppV|%j4y+R}={PuuN36yXE(U!4cg;`T+$BLPY&(KKp ziT6jGipyuZRx8wTf}csAi7Ilh@-ErCWY@Yv zbQ#dyIkkWp9pGB}yAn5bUV0!vdno0Z4Lar|Vxx^&@BA@03~)BH0gj5V^@HYV`^ThJIZq%L(m{uI3pVusm*d=@CQocSFH9e7D}Oyk9X~Bf zB?F%e!ye_Y>H9e%j7YpKd-Gz~k-vV1*3P8^zyq+eyL=sPIL}e)dQL2##yIL8uNozs zO|#dJGzIV4zO=Ht zD>gjcNjUdpjy|#~&+ZcThz344tT7UgYkT{NS-+qJIqs9}qat;w>s^kh4AoyL;f&zC zP^Q~ef%|yfl~BZ_uBVc5@US0!lNRk{^WGgXfv*PU%j3* zk`7rfoP=b(k_Z(~e}A(t-+TuDIsQlcXQM-)yaD{r5eR9h?U{M1i{u%pf$y%9qKdB8 z{@i%9I&r41+-R&N-e|98+|&_O9xAuThPYtNdJUw(C!QPu~X*warBKJ2%b0;mZ4??o6cTy9Dp~6A|EY04+N)pfsftizTo*XlT@Q%JoEqO zhdRIAf?S(e3*R4$3wHCkJE4_hzRFYo3p|{fWYn4L{y^ zVU(Q=O>K6ZAvjn=K*4xyZ2h#7*MXz7X>_slZ4BwK)6Taa<6M3lwj@m8@qJ7< z%i70RZL6&@0y`Ny;T3WnaUn)2RZclV7H4$bPlRzX62VKHdtl9j5}T2&f}!yv&ou{P z8U&v&)Mk`?^UmniQ2PB??{=ITlqbdki;q^Tf5(H!Zn+kx_%|5>0@LhcK+k*(A>K|* zzdn4|$h^6u1Rh&tPxl~9Uox(Dp#R? z`f+(FhQ8dI4d~Ppc=?Ai)c;Qwjbf$)Y|d4bKV(iij{8$uRqmpi5wb>$3Buu8Xpknw z5q=lxy{N!`xj$e-`9JM7Hw}|YOLrb#R-U!fjiy6}qFEb3N;25Wy|f|o{Nh6FY#W~t z8740SJd7HB^K#t$+wYPOZ%PJWbKv#t>U>DYJ=gC=3X%`dm0GyY4KX7n&}tWvWw+{1wYD*~_;hWY%Dit;7atE-J5x!EvN8+H!(?JKQSi>n z?hwt{uA4AeQqMb6*|!~0gXR=_DXXvRj{d&=f0IzwMlCQ^M$VKo=g{-TmY~9fQ0Ry{ zgX$9n>3Fd89zXwyBkD>VvXOb@r>}0?BoJz^S$HUQ;j~);f#PZ7H}kDkVr==QP<%SS zUSvIlX)e*F@<~H~jM#@_IYyjxL6+xml^v`P-kE~;CVW0(eN4e4wgZoajUyg9Q7nxe zdHOD~oog-c8Ittl;|lB`@&vRzX+fCzxnubyLUBSxF!N8OzhjPEn1lNOF^;F&ma@xS zyxx1NgNHu|g=xeu`dh58aXja5tGia6)%`U2v8h*-9&Z(xq8R(Iw{!6#w%cQq2F!64 zh#MN0E=|azxlx2{N>?TUo1G}`Kr@SvayNlosn-|W%ZIgBQAsGB^aw~h_AsH){c@UQ zI;@ifpvvpHX3o4Icu8KHHBZQeOKqNl26Ee(`t<8~z9n)MtK33!nW~<6o+}w$)Xg`l zHtR8UH+xc}K2FS3j}itM+*)hP|A5tiD(%);A}%OxAYKeMw)cQ*`m*?*ALTCc zUbq&*p8EQ2?USK(=rKOT>K#2NxjYQ%f8JA7w^mgip^nAPS8L$n4A{oW?2bkJMeRBS zm_b0n?$x}WVIiq=@|^EJ~=JjNJ3`Y$YiQm63Y3>iq$%l zB44Txugg-lfuUZ+oLfIc?FS97J%f;)0Vx&%Y&0$2nRdd5!7KvSzt9RWG1v z7#h}H)Kn(bi+Lbc?GpY=#m{bdDlWd;^H9JIQhKA%YuUZ|CHkbVsB}d4CsKYTo`O@* zos7U>7ur@V3c@-@(`% zdx|m{jHtT{-FtchLb%5cXb|ndr8nXDxkVx=_k#t8Wn@!~9vLF;*7i6}99lGHS(;hM zg!Ic3tKS>5*%eS^RLJH|$!w>uFY_XJ2;X?~%;h!cp>qUY&Nx%V39qW{ng*y!-8>4v zv2*-zSe_h+YLl2>fX23AT+g*5cHjYyU2i&E5Va5^y0b#*jr4Yht@*up zzdyIs)d$=vI&(AdB0?D;XgF;q{6(-ae26@T$+`W6Ma})fBZVltcjh(QZw|oV=dVo0FgAE){3fZI98eh2q{V4XydnZPsk@KcxdT@qL#o%Gva>XnqV0vi#pPhE}>uH z-!a&m%5*q01z98pPfN!X-p<99cX2E?^|{oQ*0~THb|4gIQ)8U&a)O0H0FUz<(hSa< zd|&HksqWL_W!u?Wskyn?IaKykIwqg1Dpqe0gtLG!%`USAZiv|qRuj%*4dmu=m{N@T zypQ3{KSH_b<6Fw2%`6!$x*X=cyq2u4DY#h3>8>^8VxIZN^S%_ZJ9C8}=u;ARFO&BO zvq-KzpQX6&_q;LbMpgAh4xMp!#YW$#Ozo7B@W=g*&n>LmqBRIws9p+^0g;i!|7gWf zv`A9oSQ=v5u1)oRs%y);QIOIAZA2)v$*D5d$W8n>VU`>GMxHQMcp?1JJ{Aob=rYsz z1(9+R9%$;dR{0`m2627e2l-KDlQ$Y8-N!U^=+hxP>gAXd(K)gA`>1O8u^wF9)n z_Tnmr-hK36{JLzXX@AY|-s5%N(Y^NsCNPJHF$1mML-TA-Dp_GT6or--5A!SfS9FMR z;=LO*nT1vFF&*7;yWjKJ1I#-6EQFDG5cI13w&OxNr;XaSYzWpzA|oebnOf2XZb$fQ z0*=1N<6Q^u*%J#<6`8*&cMO$paHp1zG``43ZXr_4lnOPxZ`>qt)p*g=fc}-noA17q z?%$Ig5+CfwP99%&+Rg>>ZF&_zhw{W5b5qBcDCoA^c7ku2hZ?gKRdn5%1e|~0zabHE z{7R%K?8@uXDY&@d%fgQhMJ18tTLrN|(o^GSPga$X34qp)G#4TD?Lf_oehaf2N~OS0 zA11Dg-XyxQulaf45MPcrVWY8~nAi}8SaTc6C*a{5%;LgIpE8SpU*Ihr5ABTr-%;*2 zcu|u%{uw-XB`uJ6HeOK&X9e_D(3CSmm1i-KKPfXEiT&X>)ZGt@-u241rdPc74(yH;4Y z8tWizg{tg-vzrg*EI3}LV8^>&d43eF^lHuD3HxW^FhA|BnQ-@r9BEVLxRE@!Yt&R@ z27J!cJi@| z?6Ln*Cg}VpE*}hp=bD|Puxv8Zt%37B5Q-yDJR3Eu=wA}rtSn)v zX}qZqS5|hXsqepR%+wNVyknQ{k9}{|-(<;UP0N@2SO){IruTo&Ivbw;Em(2U;SAP2 zkI2gS{wpas(ElH&od@a(-*s$NPa<3B?>%q6J3G$a1x){=x+dSuZ8Y_g?;t2mL+#(A zcHHE(2E!5Y)3uH>hcmx?hqsj@z#-A+xs~Vl;3Gx9s)Uz(pP6H4QiJHm}b|O>dzR?+yRKnKLM=*XTAtkHEpJ7Mkkt=%Uy^ zHNfTm5x;iG;MNa|e}&Uh>#K4J*TLx0(3r;Xf4pXNPEoqh@o9ec`u1tA;El=ts(a?P z(6n#l)i-?lCorSTF!pK9db%b!+&-$w+mP1_acpM4Us$Rj_ znIadq^IgAf(-Q}uO1d41BgnV09zIityGh95g^jgj|Y}aHG;G*m@q9z8L z?YN`t)+}w;npb#`m0a3^HaW*9psv#wpNU(aGp8ww=&;M12H>wBo#HF>$)D`ghu%Z^ zw|t@=hF{Es6!!{fRwjmGAW4(@OEVc4l|V$E*~{~UH8zg^mj-$RW4t%>go23t&QWnX zTvchh-K!SGv*Y8e0Cchp{PGxP&x&S$P^;(To+WDCrdZTx_L$F+@1Z`-Kk(@VDovv+ z2v^di53YEGhA-v(#<1Of{iVkk2c8J&;_W}$mTYT4xJRbc(qPEXxM<2WZJdCC-?!YP z5aE~fRpbs|_+mRjZYkS+rzaSgDa#L6X`t?c_)3;(`&e6 zSeAj65e`|H6TV#{r8swIc=bj`0pb8!}KQXD^7ya6@iR#5{iNthP8 z`V=kqFy<0d>T-d2_VHF@BleGevG0U6@4j-(yRJsWa&0Y^8ZOb?46{3{ColAAiwQ=O z92EPcRXf`q+R-MGi=M}aHHem&$6X8m*w}E7P_c_I?xTt1n%?hVc(sejl5ktGJIl1> z^tAHJ;UG;hk6>UJTkt*4lOdGE5m$iI;auX-MYVE^OR3c8&%`2l-gk%JH@~c_ zA;piA>DQ%T9`{EW0(zlWQ#al_da$C(C9&PA3%%|Ly{io*I665*eJR(ie}y{A?hzCA z3zRNZtCZoEW5L+Reqmka9F%Rg4jZT1#cTXF33Lf`i_w6!bD3-nTtzGlQcQ%nA2 z_W=@cTop+;7NMWSz+HL`npX>)>h2h(xOMugC)1@ta_o}Qb*mgp-j%R_42S*OVB0`f zZd^sW^htY_xJG)S-mf1w1adPW-+dDJgglF3D?8sRx!x&m-IcGi{J@`Hm>vY}_bx`& zFF^o3YjtHk>&VN}LcBr6mby(@{ovP@kQ4%@kMQqLF)TC41!3H%2sCuCKEojr87y%_ zhbWG1Pq5mI+x(BE$@8)wFf;w2``l*hTJjt`5QzGYVUIZH+%~_D=Wl-mJfmLWZ%?ot zoN*}F5G}K0S`EyIvfj3NhtaQ3@@*rWBf_1r4F++5?syhU3JRB#95Oa)J(Y%UnF{$6u+E8fAynK8aB zaO!5w!^Mfee{EKiFVf9Qw5j!CmQ$*@pts9|n`w2YUYX@5da9N@>Y#=B{G?UX`^h_5 zG8IT+nO^Qi54z!mUNp)rlQ1s&Qm9lVQi-uV;StB24QJFz@dSI9XN$bsp>z$Z{8#{e zI(f`Mje(z&xd@ci zzPSus*!ktbng4syu3bA5f)F^;Y{P7Wp7f!~nQT%6!>6_^a;~unbjdcEd1|fr8e*~e zuqAqyIHzd9vdpw!pduH!c3qSRUj&t=x9ivoI+ zZrpj50RQr@i=Ex*t-$qPN{}ltR+a}o(%w|IfkzSuLp}&1xs57ZK$Ke9MCD2IGVPy0 z5KBr)_RO$UyCwSAERosOjR85sH$lmgbP%F)^3xhy>zmQ?-6}((!4|kR9%ExauBh1%ubdzW&$mefBzTR_`ySQVFS-LS+7f7u_ zIojflm2#$uL#)W>>J8E6$B4=WZBk4#)FX6*@9}pH7VhL2M>`qdg1BsyAX!B`w-5O+ zqtv2Sa)l{Y=jAC~9dD&r&)%?2jlYm+>=xQjPjC_A7_Ti{HN{K1wvJ1(m7*on1OiyW zs}}-*XjZ<0uY80)AEtKvc8*x6$2P81)JfYU0)H3hMSF7O~J%-OVDwmdQb_&GYCEg{Q7Hs3_#ytWC(2ZTfmuQyDWf>)Zfw(@xNi{Y~ z)T~^=ldVyx-9`D56zf7aUZ_YjSE#X#2XPJnX_r*a?q{u4)K6U`;+mqG%r%g%{AL4t zqno;o6&Uzw#5hU0>VU*Ku$OUw<(1rdw_Lqh zbRRw0cn?#&E!bl1Q<|Z#yJOTQR);KeH}Q6g<(I@blSC`LwN9>dJ%&m5K+lMeOH{L) z9~JZ2dk+cm)vhPKLU752Tyy;D^7Oq{BSKI3nEK!Se`c7hoQdsYx}oipkT2zgqwBCF2>C zVxdKNm}QB~K5`o$2vGEE-ZxAgVofvH$Hv~rI2vX_KKJ*mk$GomXIILVE2tEIJdAYE z%>4=C6ncak?9|BruHQL~8HYRqQO}6hX(t3bYvmAMg zQ6}A#SrQ~*A0f~w$nO@;JW43 zV%7a(wIZq7(k)Wepcg+OH1gR7Z(u9rnMS*L-o2To{$}$C4u2(GYaYLkZ6EnzU9NH) zpLi`>XAftF`n#w=`w3c>u5q&H_j@QGAI`oyDIcFoIWNB&nFKRW{{;%m*gc%zd8CV{ zP4E!2ScPry1kE&&zhj71qZ3nR}1+Z{YEn<_cQA>{524ezqdx_8^AEN zNWDpRkQIAFGY9=zynucc^GvxE=sC!30hjogw>8X3pv^q)l!(5UZW8khduNqQDL=+= zn`)L-BT2NWlVgGM35k2a)c+^f~j?tO0Db(%pfh;3I5U1zfCMxzng(* zVVvU_>j>|25#b)>4>;gBL+fDEMCWf`vX9Ui`#+ai#VnAQEBxl?X+q!TYinf!f$QXd zn!Z2}vxI)jG8Alg3Ur8uyReFJ^~Sgcec|qowuAycL6|2Bvwj2cbc?q51=t5SOKPX_ zcM|THCLZIdWM}L4aQ{HZ+9=hyg%<4pY6^XuY%fz%rNGp?iwg=7W1^TFYh;<{5SV0` zr%t|D@n7rcpAg`WH}G?xR2ys(jKU?WH$T`$t2Bre0mCR4;OxoPjj%~&@pY;eZ4fDz z@pfhDzPlFUd<}OE*Udva6=j=XrcyjbyGTyDe2Uu36>XhhHcivReS)?{a1W)I343Ff zutX+P!#;2gw~en_H6o$a+M1d2UuwaID1WE>mNX$ui@5- zFdi=80(?4H=W*{b?;zfOt$nc%i#3IMwPV-O!vkkNZBk}yHwo{cAl@J1$bIi-J%{fV zpdQq$TqCiJ%2lqG&r)-U7HE}2)wCK_;kq^|6~ZG5oy2)s>QS3-&yd|*`l+E#f$smm{zyB`{6cl3 ztpJ1h&Ng$ z8RTFc7^Ewd-oVs%^WBH9_lfKg@1ocX@SS9No+S`|Mt($d? z4Dr;r8zw=p!wR2ggaF6c5ctR_!<`XsIa|O$mWj2pOk=am zS8>7b0RW>ER{um?WOsF^Vg-PZdCE9^ie5R&xe65B|U8Qs{_aRb- z3B;pv$raEs@~`Y$gnt_0vVraH9qsHKegs!8CsXrjE72NfQ$0d|bpktVF zoliiPre!2==jW04ddR_f{zh@PoP`88Xa_^^~qK&F8D_ zVwt9RfDrE_p9KI)6lY0utjsefX!7I|oeHId$^(LYL(5eB-07xLO=4|Sa>QG9@sO^o zWw%h9#Jxfzbq9pG8$A5ce{2%`XMU1NfCqnLpQKu0kut-?GsqzZj06?nYZwHx1vUH17hgioM_&btJMwzbQVQ;nw5PzH_+eXRMq#0=C8m1m$IEITf zs}-K1tPxAq3AVx9o}&Jn|L?nsLN$&2TbKrkFelRGWaDFW*{Ws{-)~x3JdK`FID0tz zSxVd;06@*>7r0)*9sB?%nhB{&nA-~^s7JnDH~&{~vc)p#Z4!jbFL~;vIXdVEdx#E^ z(@c}}Df*Qn2C@0NA6Ng4?-u0mOE(kZ#M_sz8SbGUg?Hc{_5yYZ8||*EX`ZxZUOySm0_hH@{{8bv3ewu}9kz^~s07rks$1m|Z`KRdOd5!Wz zi~vB6Hspg;Wl#`TPZzUg@=v%@xiyM8I{T1u=58LTnifHhcC-hKBhz%Q?nVLi;%N%j zZtoz8ddYgzm=VT*bD%>09`?i5I)Pr<=StNqj*)mv$&yItO{`#7;TC^S?Hq@QWzygM z_pmkd-p}(kh~Te`lPzPDjr~23(T!5d6r-)1#DHHJ#vj4YQ0ybMbAnwzS9kF;jCb?& zbNsve06>I2XWtMj&>O|VDoKruV4GTD7cb#voyu2BscO-tF*eL%_A?~NB2-iR0b`XnIwUZAC=0DoSwen}_ z{yX2tr&>1LeUBhR1s>SnHO?6F76L@8pr6&%eT-E*PpsYF*(vY<9RM(kN50H7>1SW2 z)l4x+?-r{PZjl`2&`CZ)PBhNfBtBBjCYu|hvrV}JU&pThtX%ZDs#&sw+a$4v?ce#j znMP@Sf(*U30$9$&0-f;LNa zjBXLv$yX`IJi^x*?%2&kKXD5~wxpFiLH`C6<}^Vs)VD)6T)%vyJ*(rJ0jwh;>ve7w$|qk@Mvz5O3EsOO!6($Q|qz z7{io7Eb>XGEYg`$-Yk8R_ARnqRI+)N+7P2e!=L!_<--gRuQV%m(ZC?)5u6j%{HNFe z=tWGP38A)b>I+QN1A&$p%W#)g#Y!o@jx7vOK(TtW81*FF1;VXpGvsr*Se0t4m{yWr zmSL7z@C9Crm}3C(qE0#JqjZ&zdz{&y93b3`wf(s2>p#NsCCxl%4G(+eOWHIg>glI> z^@44Dy-cp&XHXyj>^0ubcTu?OBGpB5nVM@jw;a%sHdwWfA*hwc>dEPILRv1eTq@4A?D!%#Rbwlr(yio2MFK>@qDFu ze3;7zs0$RjRg1WD)KBy7A@`ubKg(rMjueaP6&j_KN*};+eh{tsT1!@v95l%;yrO8g@ppj2iTcMsjqO}L2r1L)b`R=G?$^Y~MwbGradr)&cl&;ZjZVuD$S z2gFOgRIEj#==`TTe#Dno@Gkyb70vi-+-rnpjwKq$C=Yj-SIGw8r$|TalbTc9~tHK_w@6n8@UI&N3N1u!Tsvv>UN8QzaUomjDou@Trb)rUz1~*uG1ih zeEa}Gy=nQ)CJgJ)G(oBfI&uY5tc+nT%dkwIX&!gIi5GwC&-$b6 z0(}X023XN9tfF1N4$!yoy@I4`DwMMh^>JqFm?boeNYw!Wkk35*sb($W7{-&0KUep1 z@pRWo8K%Ag%}{goVjU1{(M_@qa`g9b6R#DkNY$ApZW02$6{vIc|5<;fEZd-SXa~P} ze74Rcopz2TK&q%{jg76T#ZuBP-AqB zN?@*}nq?cba@xe1+jVjVSxOYj6r^i?1D}v!?~?ToQDRJ@ZMcWS?QCNOzklcP4i0hb z;06K{YoZ>81CMawXoEXlptei;G;1Eh*7s+AFDK;lDnpLa5@jbxk=hL+@SD}wdNs4S z=U1S|Z`SO?Qk8_um2z7jtWqB#Iohg~7zP;^BaP92Ot2oIo#6uC@;5NIdPg7tKF(St zpdFij?G}`1AlY(^QY~7XkYe--#xtawxk7#kYn4EOE(1B%~N!99pKE8P|Vaw;~b9h z+=4CA9N>2|&oFWK#2Mn9FB2MMt`oPilxy|VDK*fI?%>EaItLu%_*B3GQB`d-)QL;xbu_5cS*-8_bjEm;%j++d|_qRg<*0P%9M3r{5*O zfuik-Wjs8SjnD9a?k8z$6dl4kg}CcnqeXl26akNsR)51+EN_)T-{EPQV#C`t$~{M9 z9E>u)gT~!uADE>|Hr~cBQ|#b(4K_=71-^$uJ@xSceJ)TJYf>mrGmxwg@Zjl&yRMO8 z9+{zDCjDm9CJ6EZbm!Z;ddn4#wFDp&X}3ihO2O}ZBDDctgl z>)Rj#ah+fqZ@EmSoo$`i!xL}9-Otn4EP{3!Y-gArA?*cIZnRO)DkZr`lmv&612JSAy0Q)G~SiA-9 zu~zCD&etDp_Zm^5=m!27yjAcJ$0hp1>=Bw-CjEqY;sR-^&ORE*^C4P_;otDPxw8$Z zm&{|GW8y6)J^?=*6P$pTD33FYGbol-e}=uGSa`p!V;kJZCSUC1EK(I|rkX8Q(avER zPBFTMoB#B=x|=6c59*$~XP9LdSFr6KO03B=u|ocz^WHx-5C1)!BojZcO9cK7%DKv4 zJ8QL?Cg}nN!o6vVA+{1Z*|J~zJ|ClGy_|NwI<*mct@K!vX(rX`T|E5tC-5D_LJh_q z#-(na1B?`1sEcJ*oI|4w*YG3AIFrA^1L!9h79jz=bu?2K=rhFfwMnL0ReC8!YG%=C zdQvSl63)J_0EmZi7O%)tR5v%+t0Z%kX6n%R z{6*+CU9qgj^t?&~eqHcRp)#WjRu zD99t)$<=q0c!9_~!7+N6J!pQD$nT!FVMM|+jzZ?gaImak5}n5C;; zKs5{Y#L=Inn_;P+5#gXx8)>DKE?*w*fVcJ4(Ig*xx0{7`q)FmCC*gdVMz5e~SBw!j z0BaZi4rMmUnrpg7CdM?@ShjPS$ljNHw@{-<`tRZ&=}<5479v?I(i>+Rt#^bT>>90K zqoQ0-JA4CKtCDC4e<)e!5Os`IuAi?QZmpfdH#AQBYj>EVRubUh?pG&6w4he_3XrXo zE3eeD2{^{1o2!tzz+C#s)A)CMxyoZ)NYG96boEuLR9m3SK`!DI))C#5dif`aRsrWg z-j+0jadzz@iOM`Z)}dM@gEWV5!8+wK{dkoM*#_wfr;s5ok1&pI&1}*c@Rw=&Nd}FA z3+QqMiK@SgevS^?b-2E)X~OY z9pX%9n53HcJMANesQ+&KfjwjX=;BM#5Ac*|@C<1Z8=#G{wu(_GUn8!N4|l{k0s`=K z?czc{Owsyzb@0p7vzP|XUrlqeUg=w$PE zD3$)5|EDOuTn1^lyGo@AX4Er&Ub0o9gHB;$->=~MIYpXRv3T1`m3aGgDuC}<2Tu@J zsZfsk>BZadPg3B4Qg`AW5h7^RySMk+<<7gcM*t)0W21FOaVF8;GrGR>kr zJUvbU_aHlnxM%F+gj;T3M;MxVyr(Ug+`P7v4Svf5>8IX@6HO#S^FBT=`{QQSi1Dn#o8-PS2>WAik|>92vc(r=Mj zr)SC5NqRW_JOvx+XEvy@_HmYn>HkhY_CbT(6Zj!|KidY`GmJ${k<1pRN5B)9Liw+^ z3$>ePhPqI#UUL3Z#+NI2@D1e_bMN=1iP;UTa(E?laSb zeSD>~tDi!xLrj!jjcS;iX7)9#aC3{0OtrtqE&LkQAVVY1I;lf~zk7h|E65;Eu=&eh z@ly?rGM4F3k0xjgvnL6x;%<@0S$4>fE{;$VjcmdUlgMZDG>Eo9!F>W!wP_Y{R;yHD z?o*5^6zYZ9J8Tkg&fcEW49?Ku4o?se?tQ$ZT4<&VRn!ZLl&)a@F8)2-vJHqY*vGe! zPJ#8}!QL$050DQa3pA$)4>0DjLQNLYisgC*Yb3D_W8_{D(&etc@uvG|bqdjLV~i<= zE`G{I67_R*!rgq$Bg}`GXgly{{k$!5#p=nbfA{|Xga~t2FPr`dd3TRtoDpdkVS~8! zUiBf^2K>rAB3H@XV~_@YCs)bQKgv`iGeIBZrdB9hrIU?%>KgoAlxeh!w^kbQ#~N{h z8QUP+AP~Sbp;GP&{uq51_wW3_gWAEf`}z!;YB)vP$LSb8!W`)rW>2uqIJ|-#Z&NM% z0~YnPlaIOCIZd^WXl{{il6e{D79iGiK;<6F*{q*5%We_1LOw=EypgR|prKUuB}1^y zE(Ga0$m;L>NBog!4`HdWQhT$SNV#tbFCCC8cUl zuO)impL9!>5$2H@<{bho{qHhU{%ucEp8$56;lKQT<+f2h_8T3nQR^4zv6fB zFH+M_2L>UYh*htV3U%$`UqR2%inW#~mP^}shq)=287HG%=x5o4w+Pa#BV6|}#~IvV zsOQ&83AW7AMeEIzx+MO<%raWR_&fb)sHqm|Mzqr@I@Pk} z@)R4W=MzL2M^0g83D9?s;ABfc0K=3+%u=OW7{bjq!Ei^4g;3`WqAh|i8DUOSv_Nkg zL~VlZft!TljR|I2xn>D(K$LSXA!Uk1s*`koGXkV*ARltIrfJv*EMpB*@1cIp|0B&} zHIlDDFK=;H=)0k|gRK1BSO>{w+hjg&A>Jb2u@9yw=z46w-Jw>irWmdh>ZGL^j8bZp z^@u>#=z5VC@J;-9+Y;>{ z>jEY0gKhz#PU|Gbo)rc+Kl3oJK)wN*1-yOI*;>vkY>e|Bu0)e*mQxt!=_KVw(QYni z5S)WsUn#7+aThd9k$Q5aZKLL4yzQ#QK z8~zksn5RjmWJ#1mxQjtzqD?<n&M-3$uqqyasnoxLKl1Gd)91xEbzPqWm}hs}_3souIvd z^m3^d%}_gsdipDNi&a{FGfHCZmTE9bxq~>s)Gla~K|AyEW*hVgw1|@^>lMk-rXO1Q zBwus~(#sD0jK7nn)hHw0jlJ^%#nUxPhju_T-zZuk_gDI9`+4H@zKe$Vc?PMJGLFGq ztP`hLq-e1X6E6<%l__v_QP1S+U0_nqE`E@0`w46v#5v5{NwD|!Rwq*}26MMdd5W@! z)*w1S6&k#YWRWJ+wEC%B3F&Z#Vjt^o_T632N)UzEu#NS zcMc78eMYDipJ2L#)k~db4Y#;M-Xbmf+{Fcd*~Lyf{UzrRMK2u~pjVi$A8CDn#W-9q zZJzMWpikT+X_S18@EIl0F;hFrcIej}RW6}c+9OOm+(+G{;TC=crke9-eykk^4`fMN;ouPmCc00#OD4sil|eSxBpk9F)GdVwsPFW*+8-o`OZrO@^Y zVIN@}g1=R!b%OR`^$y|fO}^SEaFnT5O5nSFJlW(BwR^yy<8PXvpZ$WuGZ1S3aqI%2 zNR54bo3xZK(X>{&k8^>-IkZyl{Xe>R8YFtS2{)-`arTr;idEjfb&xiacqkAZ|;9K2w`s^AJC5($D*x?*B&5$LKvnFOLZ{n zChSo6@K=5os9hlf0NMn%NbMtcFe0rt8Eiuu3uPrI#NumLYl=7D5yr-xHxADCdq%&{{um-2;oJ&7-#zd)Wv_B zY@8ue_q$Mvrf|zC9?D^*65v&$_6?wy^>HuAUA1C{vXx!5saM!Cq*fdn4CJoqbG|}~ z0pYemF8nbR;1=S?(>#k)+%~pa?jv|Nr(m1eA8$av+4l3bex9bW4-sqYV->EyfPVw0 z83=Ma#8fT%3DF`9@$(G`2(Utut@8{TYbn$!T`S($%SEveBOuv>YvQ3dJcC+{LH3*_ziZlxLR*8b& zul}0<6v{nAQ_Qx=Ht{mmUtaVIq^lS^(VrGR679im2L(XyfB4ZX*(_s}Fo#^KB-01< zNc1b0YNZ#c3^V^bALK>6k!@P7_W5y?l;BXapp#FlT%o4-BmD&PNT@T=n`#kfAMopY z412f`f3OXv8A#WTG8L+UyzFCZp& zB-ALP{OIINGEyo;xIRMP#2jKZ&It5+eTIM0DSQR45~-7l@UHoM3t^n#9#|vAGf>Z~ zmG$rX;jT&7pk6RePhl$+dpT@E?qI10yyILW%+s!bE9G~Hrm1?_FQMN6Y=h8G23h?A zXd8Kk!z6LmvW+W*eu2?u-(~!~FgAAy5nph(#EVd`G^-z<$)_inW@s2DNoZ z?-77-`+z{RB3>&}(IRqzhj5T;;1rRi4uF!a&Qkw@?jF$29c2yxz}$6-U1FB1Y-9BD z#TXMUGEXc})T*SZ(vEkt1H+gGvQO1Y&hV|{7t42Z8zhkK7Ap7BI7JX|Jc9i@KVO5l zuTD*(wS^~F+9I7`^^ho28xjy`5^1ki_;H19GR&!)$1oKL@DrSQgukPkXP8ByeE!oF ze1&|jmVG3{c!4_7MHjDfi9)%z?<|$Ccb+C^pXhh&eU}iTjWh$!e(cH9m*QH~L&z&vOF-04$O(P)*UUQ5xj3wd3rN z&pU)RNnIg(hNEt3C2D28A$3SC5&r~(ei&!4_ucpi|B|i3+8ysIQ+*6`1NEQvgFOa% z>6B~}NY>IUaMtO@eX03o2MUIH^wpGZY7JkmTB79v5$y5^{teL2(JVg3^8l_?FvGHf zwS$NHFh|-TdWs482)Mq%E|C5-$=s{jb%)5_dILwYTCI%rD{qH*rfch_kS_gazGci9 zQ=#gI{Vd}L*mX?AUA#>+kRE2+Z&X7A?D7>=vRecW5qzER=lJ_r;f_qBrAlLrlJ&Y- z?t!#Z!LB%aM;Op|dpP!yM;Jf={4K>Y+9~ly=sT8ShlpIQYdFX7Vioem_WxLeF-6lZ zfVcCx+A3xjH`P$|yHe>UA?~hvL4?CTj#Y+1x?`wkI8!g$wpu;_u!}3w+VMHSag>gH z=@dEA1mkR!OSo=|Q8sUzoa6%TvV-XiJ;>n&Y7c#pk+U~PeeuIGIra_qdZvb-``U;9 zoo|+45$Efhu2G|L0W-mleQ=5@RzJx!%;Dz#2;L(M^?3KDQyyg$YSAJN3@B4%9a5~2 zYpqZQxoj6)CFx=jZ>O1LUZfm}Gxqaj>S5X%=4urZDi{B57k7hV`3>};Q6R`Q#*S%> zxhK}*f9HGo(#+^&&wgT=;ph%?VQbULMtu6PBUjtc9B(GtD%RM~tCrQnOt3Lc3j_#s zr&!u1;u$BM`Red1TVjAeg5GOo;H^5uyaBs?y?@*$ZWrJa;d^+!pSWq})yNd6^LJ1z zygR6$Rws3cshO9q|JANh+%)n1Cg8o&KjX_WdIvv$2l{cbN`wPX_brUSr?0nha-rG_ z2-3wlvS3>ur%Lhv90OW8hrj0UJ)A>KiUpl)(B~H633}3HZ{H$SbpcC0ZulIfYiKyo7TMs+Q{%GR`DAI0e6k zdx9a@2ETW+;~uh1eD9rVSS@dnVjaEw5#pI)T(a^M?Hu_4_Z4jB(}$hdFENHBhu3>Ea{Bf0px3^sVOCs%fr(m3|Q0*Fdob`u^ zN}XFc-r+6e0rqy0Nm{IBqW$zQ@4s&rk**gUYGcJarXPv1u9b8P&edg`#N3Rw$rbl1Ov7*Pmc3 z)Hfi^-A>w#b2>pTE}IE&xWLpz4Mb_(m@*URMVG)jrG1$uJ|8(=5g zjJB4nw~r)SB3X?$r=HQvl&;;zhkO74UV#@V_&dfJKdhH3{m=NDBo4FaW?97H?$S-l zS6If%R=t3Te(&KXTk`i*YGmnQ?+OZqyRMQdlXeVw@4bVso4<*jt#wGy$rI?ZNd#$CN7%_z@!2K7IU>}uS+GlJlYnDv>jUweMj~fF-#F9656~U_ZtlVVrxLeS zXb&gZgnkVAldU zZ_jS7K!%C+Sp*ZKLqF1=?Rg0zBR;2P;+X!RDzT0TI@w83|uN9^UDhdi;Ih;C8;A zLCr!90;@Ewf@f%Hi$ZSS|m5nFQHyBbmJdE>|^NWaJCex9-zE^NoQRG zfB^Opw+Pp8Jgx7$Xu&pwyAs__`ZDExeA^_v`n_CV?eVvTJC^W#To@)e2e9|gPzbj0c23bsl|91uh{{#{d@N${ zRwArM*>2%{0^2#)aYtB{3iomH^{Ql_068kO^SjuVG4IidpoezWEh_GbO#sbwjV#^{PmiAiUuQet zB;7R}@fzu}d3>2tsT#>nuoc-_lI0<~N$M?Vq$_uCvv3a=<4CoTc~ZW{DAOq_>Zx94 zz2p|bZ~q0_Y`&eKQ7nA5tCZ8rB-pBw75(n%Zxz!Zk*kG%Y#TK}zeqmLkY>=sJwr{r z_Ff><}wYcR$A= zX1(M&a;-F1?>W{r_&(kP+{ZoOpUK*UYrjXWW&DT$@NKUU-H2f#5cm=f05rl5>}iFC zcIul2?W9Q467}Oty~sMHM@SC~fBPn3o(jnZ{k&pHh4KcvUHICE3skz{YQ<_w0lpSrjWQM)L>q$~fL^O)9sHu4f$x8xq7d%* zhd%yPsLawqeF?Q40v6-!|A?GXv+GY3v@- z&(|e*54)eCm0!L-)_R2m{AUlhS186F@-ES;?>GKV?UE}StYiebGx(aJH}Z~a#YWt!3EFHd6KlneVGnpwO;&#>nyLo7n=y9N9Be0`P5 zpFm1f%j8BFhqwfqZR4VyRSI}o!`yIp8wByz#hPb7k<9aUBVR0#8DxS#3G|1!*+u!d zOV%J=z&}+;8^o`X&C%=-_j7*E0)6D~xQ3&jU>K*LxI{od{_Q{Pb%R8NL#Yz|#8Nlj7~rPjL4@-`&6tu=92)mhBPHO{9In-`gQQ zLKUgs`L9!S^ph$zC6RI>u#RSFBW zYb3)Q3RNz^Ow-pW#yNRAw=mne{XMVY?%)~6i06$nuy(d6e?moCIE1FDqTiM3Z4)^M zLqFJuiq^QfL4lwh*L{w#h;STbGY?BOdjN*ECzRz$B4l zDBd_xOTBva;~iR_%CE&ynp73ip`VY3^B`Bt=R@q>kGxG`u2b}>2EsKH9PAB-DMp3t zQ%_L4$VhXpiR`_m>8~IsXkCIahAMRwGtH7l>62uf!#V0;x4g}RR17OzLwoUz}C7EI5E=avyntl4$^Czf84RF2z8$M!N3q*Z!4dwwne3 zXcKq>gS*zsCD_8=|NR9p01)hwsb`j8^G&KQ$s|kn76u6L0{jGaib}LG#A+UIn)v=j zIOCW@#1j~I&mDBLNU;j;Zk8@*pF~4~S*>)Qrnj$3u~p26^=Hr|6QDQ2HuLxfi9Ai? z!32G)+;&0RQ08g5yuSZB!*B}o322syvhU<#@3RS; zWim^#NdUcT;qwd(vkLShT0^{qyhwCzcq1MjT%kCDAwKfh{%5jR`Bu}y)AX@Q)ByH~tR5a`PS zOPqeOq+BCMPo~+&HO~<05!wa)L~l==P2?NNno1?w^%P^Rm?V2==N;TF@@jeV1%KA!d_`apE7@4B7GtcQ3xMqs6lhG+RxQ)Z#5~15 zm2UZx0)NUn3H)Fm)6SPKdxsR@lBy@#D%4nL*vsJ=kY;}Sv`OI{nrM6htyt!i!1W#K zty+n_m8lo%G13+M9{oq9%sAZ`eT`U>l~$=*hMOD~Vea9^+kv`IGjI>oue1(B z_?fT1K#a1lmlbLK&nb46UbeAHX^os>^(`>@!YPVT<^pZm=gQ9-NhX>6&7DFUSf?a` z4r6QyR)#T?3>rE5dCw3lgjDk>dL%0v#b&Wa$xi^}w_UurFCnI5)YdtmU@94NRAG+Z zeqg`>&XHD7uY7|#F$i}J5;XJ2s4cu|nN%B95~0rj=;!El4A##ZW$KXL#~x=amuit| z6>gHuR64>-GLAN58ubd$EIEb?wk^{8QmB=okqL6mJ0jlU;cfo)5l%mIh(1=A@KmI~ zg*(=Ub^;0{Ly4ya2ckt**n!iM><)c9R z7~>ozSMM-Ol`QPdGU*Vjr+>1sb`DQ>2S4Q;{M9k~93{eKh5Ql5G!4_}CZSW<`|47x zY5tQ-h<45hGez4QoP|m{!`7KVMMz`*{W1(C)&kw zcPNzyge((eO8^7#E~pnUR_TZ1oe6g;WN$He+FU{gm;!ythN?wwG2eh5p&Nug&1jWF z{^aV*(=f<#@)hW~1IE15%m8`f==+iz=4z6#K)#J1YdJ-Rc*WOEa@x+{Bt+JypHr?{ zCG`)VaSY*Hpm~9YZz5j{^2{pg6|`19+|}P5{#bZ~y}d!CzyReWOQZU;eT;S-UmN{` zei7{iT>VCP}9wGhU6^>x-9F1KhTfcByGyf&5ziYElxyB3V1=cv* z(#KJ%5nAu?FjK4_pGMEo2saSV{_%eYuUuKaa*1S&GtNw|-ZU)9=mN7vEY-MNlWL4) zTcljN7zCzFu~KD~=ngW#^9c^+utenyKE}n*^BQR5V~z3;Amx$?26un;a>`NRcDS1* za@-4N{|c!J*+7S779g-EFpH236|t@a!*qR$1=b;qqet*)YpjF+-nnFJuYVX~RVrO1 zw~qvU=IuiK5$HZk1^E!@9%Ci?-6KGx831^%1&lZUu%4sc$(LZ3rE8P|04$TzOlN!< zW_dqNXc7x^lB;}x)XBE_#@9)<RP{Rtu^b8YhHKW zlV0usC*oDWc6SbUiZYIgG5KuF+pd-QyXR7@{Kb%OG|VnT^%AL4^0PD1s&V|oa?R2? zGFLyEdBo#4ewXm8Sh0>&qdv}$YxKXo^2?KBTxgbnzDM5~PIGs@*E3Ylu^1?#?}eY{gEYUF=h0i%^?S%Y>Mhm|M|? z0^>@BnNKU^#+bF!`+1;lG7P#o=V<($DP}yq305F)pT}nW^$Qok>=`boNS2CG1BO3 zs$tOs+457gB|`gR7R*5qbWMW*;bx}J zg9@*=zvC~F3Ae?W;GGnxU%++IM?&JYSV+%sG^PCd0tp zZJWA>dz#8Lu2|u_%nJSv&N;kP1BA;W>>4=Eh`m`)BcYC{b9NR=D`3*lufdUoj=2hNg~|@{cOB$k+N}2 zp)}5|dBhHtX0F3W*y~TPSclx)&7$6UCzvHa{orSqY#c$dF)r97Fi1bmaDtku@8)@R zU#QMFlVFUqlJIJhW`G0X+}Gh{Y`Wey&KBM*wRmHMwMjyb#%Ievjz8|!Ei_PnvJb)9 zBU*#Gu?Z(%q*$;EC0bJ{9N;)ZGD&acLEjWJjK{>jwcvqgk+Vh#5Y5gL}KlX)u8iL3Etib|Pq-3c+}l)Gc45^slY+89lq zZiJIeMZS*PJA-6~X|dX;DSyOw_wE+<@Ohjc?A*`q;udYZi_|M*owkQo`uMXkG(w6A z<34pEN_@OR># z1-aOV-QHyCT1Lp!nxvG;GY_V`c$%e~qn%>$NiQ23rjAdpuvcJ#S+bO7=0SaqT7#5B zY!@5lkpFx8Fon`d>ZBLONs7f~8p^qp3#f+~>YVj)HZH-;!!#49#}-jkbDQ`uH`BB} zuKymr)BOTX!OG>u_~oIVH+jn$E#BQgw2n7G}wf!b!T}uU{ls#KRqHXRni@Zx6FdRWB2pC4JKhH`1wM z9fms$wM{kaW*w$^TI3M(sFZF(u=_!s>NmlT*S`PGqCmWK_D_AvygI_kJc+$V(O$0j zE;jAyCZ&U)Y2ru6Bn=-Y>A_jtEcF5{1o&5e4q=udS(>!d)vBHRN2u1JP%!lm;cxm` z%#za770RoWv@$F|L0^lt5-&;B9-yliT*DfqM8Du}hl5QpV(8}>-9_8M{P*8K$}Gm* zI!vgwPbk$mS36VJIL<1VZuJiC0MXk?rs)oLkan8F+sWSx0%o z6qInUal!~E>8zW7@aqlyCT{EKDN3W{T2-{e4Mh6o$A$IWdfx~R%)>q0FX8>(hT7SL ze{{y&|L@rC1^idtc}l!3l|tfG)MKKxO?LAI{ac0G#7_v0;ML1FFppv1yY~v&20zQ1q_O=}D=yS|1D~f< zF9&=1MK@XN9+qa+#|>{@x=AO;*?WN1F6jGX)SXtb5ASMak=EaOo)9D4Cabn{G$<+; za5byvzswNseB)WDctyNQs29)G=N>Fxhq@~M{oj89iH2vH^mDKfjgp;wCkXk9o^Ko? z-g(cF?2^(=_tNd-7d$dbm8}?Oo1;OzqnfHxGE9uOy1fa0#W(b@@?kCZZnP!yDC!~K z*btqIZ=xwfy|4QRU)-%5D2*!Y#dA2q4ZJP0WTf*?b{stzi(|Yr(`N|(j&BtyQ;m5% z%*5Yk8Wn3^u0}dz^3^mL<0RGOs|M4cyRS@To}NNwl8$8}M<@B(9QDm@t=uhCz6>;y zcA=e@S*&>5!-@*UZ0%_#yp8n7Aufv5dl<2e(9RTUvk6DINYfu>j(gE2bOqNW*2^7fCD8zMK#0~j2DGv|`8_-% ztm!BH->?lO8k-~>qZ+0>sFSNOPE^ZR%YPkAJRf!G5p?oKX}b8MU&iT`Kc<=+<80+|^r%*lsXxc4lS>3RTVH6xl6~b>eTeNmlhB*ai{~{(68x zy)ejq3+F-I80|OR0QWN(s@ZPl?~?!d{rS2V==2I&1x`?8i~G1koJ$^JZ*3BFNWb{h z!D*HzUG8t)%Qnp)?#$M2<;OO=f#>B%HrK&JfU^e2BuzA zq+^+kemYA{8yII9U`IYdwTycn`f`?7tmnNC z(NeukKPT?0NbMzZA6p9_-~>_a9PkWZ^0>6R*-ftd2En zd9Z~|x2vtK(yH>+OY>z~}CS)^K_4YY3)pChT4wh#Yk?Gidca|O-UX6F_1 zewfoL)a!ktlxFV3+6@w`VA`o3-g&xx*ee9GZL&q>BJ?fiAcX4^tP$2Sg-Ri<%yrB* zQKvw|tb(Tl9G1bIVqc#Ll`9v+Up9&Py08zDtYGi{Sq8sVh;GU%QlNuz{0xb1igG^H zz$i(%_(hyhn{guU_FKPvHHRRQ6|tr-de^X}ir=219W{#Qsz;kMja|Y6rEsLPQpM!w zhv=bp(lv|Zhv<1v2sTQT(2mv#KRPqaI)5HzmCfNAgS(WjDtz>3EXHQ>2aQ4_;Z}yp zSetyU`1`doR$;DQV&Ai0baN&fFiz1-$kzp1DU~T_6d10O93$F$e025nfA2Fwp_B9e zJ=@s%HUF4pSeT7qXNt~?Xyj9=M(k6@0ro+rN%S-Qs7%ceTl|9!YLWUz(H_=6frsel z>=Qt|rk#R58{t8}m>@S!4z*jR;qM7{S|o}!RLirB`T7*@=5?5>XQzIz#%U4HP zBAs`$#97+=Wf(w0Y~gr06)QcgFETLu%-xK;C04tOZIm+3=K5iVTPGvdv`)yyqe3p` zWj|;7vjvJXq-N1SXK>6=>traF;_N*99&Uqv_*FMee}H3PP1l zk{W4mw{G5P+HX&-B4f?sUSRLSU1?{w35m3jFILKK5QsPSaX~_)>otjS_o@~gq2X_a z*bj&tqI&r@bGU?{ZMpe$aQ``rb&_$E$0Pyq)Fp&|3ido+cZ~fERiL(87XIAc@8fH> zj?WHQC;94y55w#+&jtD;jP9Ts9ycX z2$yX3Hau5thH?dWmU0h$iX+OjLSmHt&%b|#9AD3?m|9uN z%JUhvUM`Zk6r({t>u4w6^e5uY^3{pPq~nY;R*`@H{rP&DMP-{9244qQhdYI?63JE~ zo+cakxm_VBmXxUL$3H52Qm9$L-t)!i3R0=4ReXVZiTVg(lx~MG)=Di?ClmhuQ8B^+ z#iENJ1Vo^S&Kc0PnzK{9Vk71%rfxFi!Ai=3pgl)7^-v0CDwU?Vl=3l@=6snmfxATwlG>dt; zk=MdbUbrk+ei>hyv-dSpwl?jo`2FQI9I+@Hg$FOB5@m z#5*scjT6Nyg5B<6dA`4jEYxck`uH}%NUYu6<8|<7GxY+3oiz-cgHoCHM|BTBTVgKF zv58jHN{Ih|`fy%umncVA#54JtN#-8!5zfP{n%No0HmMHaE#uln{_6Lyl8v+4!ZFC^ z9I^?Ax`R6#W9I5->rpEWvLB-(UKMJloLr*YMKb(0#&vUzc;(@ullEQd4r+;di$K08 zN7Fr!x1MAc-0%LEMTgq>NW9bKO^kVhd7E@IBVc2w<)=TJrG~lrWBl(wTceoXmk9>7 z{6%t-m0M_sAgn!%gNNVYFOSe*uQv#`aplXiHK*v@ymK_l<*g%7PQ2Y%hpi%8M2J_j zw3x=gouVJj`css}1;U4SjpFBsC@0)=su_*E+IdRfojqDbEh7FB9zwES>l^a@7Or^& z)2MP$yp3r}ouH4?!^U2oAd56B&D<7#h7qSQg4t3mHMb%zb>nFn4fE=(IIrAWyk_dUFPBj(XpCtm)U%FzGvfR>3R ztC3EB`~m^asU}|UVE(l?cW?&Ty95mJe9?1yA8DVbk!0}R^{?^wu=1aTIF!oc?~O1R zrYqz%iw|=YYgxsQPzBpGifE^#KgT@Ay@0&;e-rdNS7n~6Od-^sb7-2x-+hUUtJ668 z2KvWy4AOff`*~gl4RIV}q?*Wo_4JUcounsSmaaKL0jk}9-JYe8Y-dU(0h zPA3`*bS#obTH);AZY!0Dv?v!Bs#HCqn>xT)pcrIdB9mxXBPy3az;N{$Wxn{|o{fu- zL(m36iIVPD$G~u#=c)c5xO(3C$rp97!$1$y|J^MH>RG$6F_vnjWwJ=)5q_5sN7%u( zDJFa9@H=G3ynU=474oEmNS7A5Q!JtlBb11jl@hlwl#{`3?R??ZCJDbiMW9PBd!9Du zaW|iMol2>fv%ttYk^M)yDfUq(pH=L?!#_mF-d!dSbh1dsU$YI-`BtiEn4q7PtMe=( z_c_e%77D@9G!@p-Bss$QA#Au0{Zx=irx5Yt_XimJJlzNvzdZ%*#2}Yx=R^a zr|Sp1bO<_m(yZMeVa=mos1%8nItAat2)CLgg+586S|Jl`$$oNs>i^!!Ki%Nn zhd(~SB%x9z%DkVuSi#SE2YV08D1&P-OO;`QX(UU7VeDt$zmuP7EYt7~)-ljA+`*q= zNG)rg{i_DqMn88Ki*-tY8uiRJiuPCYh;e2}h&nMhuQG*fja{riKEcz?!;57`vRSOj zB)&;#lAdvzY&Pw=NnE_SYR)X#&u;M)DRcK(L?_r@BktgOdF*3^SZoq1Rs_9FHji}N z#Kzgy%4O*pW*+4Yw#-#w9+E3BP?oLa@A%`dx=Qk(j$ta?Db~O?WRd;?;(5~R;6O)6 z2!!)r)o_un2Cshl?R9X>%Mb6AN>+&zjVb5ZhJcRv8In@T1;W2h$`GI?@kRFl3E+ck>Z%d{{HcCeifsTVsFK?jRo* zb@L{eP|jb&o+Apkn#Nzlx_Q%22EP{QXcwHM|JB*_f7Xl>ui$aEiB~+l1Uery6e_9Y z_Om`KQ#F=u$Cbp@Hd|1rRf6&(f`@jAnq9C@nMLKbops^lbA)6x7+_L{yIU8v_*8d z%_*|wXYQVlZ&-)_XYmm(WGfv5xqDDg^3>q2xq7>K|EEXbpY~7tr~T9ZY5%l;+CS}| z_D}n#{nP$w|FnPFKkc9P|8CnFC0fHcU>s*0XP6Ldm#qKMB^tz0$-MOq~51v*$puy?6v!3{9H z3qSCK9BQTXzMvhEEk#(r`V?>Kc%#C&?>9U*m z({H)zLu@A~c~9&@Wh>#X`Z>v#u3-_+3srK}2ia+6<|&)RhS-Q#gI^<`=BqVG%u=i8 zGL4zV9-_}tLEkQsB|n#}Um{~227iJcV!uo}%3Lj@pO$DG;8gX<^79f|(PNk!{!Van zAxq0RQMh%4rRZ^ubffqzwOG?EwMoLe57@hu^B>-A5YSB(s&w(Oj1((>wxpd-f0m`i zI9{wAYle0N>gPIt>=1~un571KIEd%$gRedzTu9bm!@hstD(n#S13&$ldah9t&JM|n zY$fe9-P8z+mpjdjY^6gG$_ZdqRLDWyA)ePu&QaNfH;c+vatxrHAf8)A0#$CKB-m@s z>{j6flS6drnkRX0{R~q5-^@}cKaYE{K*7`VGR8V0z^OxkXf4R$5*~~uV0NJ%a}4}8 z`jYkHjhy|o)5xb}OOW^2yNu%~CkGfvXM*jZE*ShJ*^*#8Ul(^T(izGLZ#VwtB|Oax z+mKk3cq7{oZ#TsP>^1YKaI0`D?KJ5!S08^T*%FvVLTwZaTz!J=U?#8*<8R_`vJHXS z;~F`_t>}lC`5cL>CqHHj6ffa=pDEW6mps2B&^xSuTvHnOxP>2IMi4nV!J8<=XD z=gBQ1*RaphHM6B_l#6$89D;H*ij^y6L+v~~!HoNX@9IOb;O5OZPO>sZXBwYsaDV}- z)qQ`Mp)*G{%nEY@>PZ5H1GW4Xk!qQz-=J@oN#DO8Wws4dDslDc;bE9?3}hYN!%EZd z=hVn4R>s-s5Rfl#6Z&k)*Oh48!$Y#->(b3@l!SD~H0J*%)b9QJ2OZa9{gbSgz?+#{kO+;X2kH#{kCwZ?|AOemO*2C^hrNrxiE@IuPd`bqKs}4QO*>7rMmL3gO1{Y5 zOEUv|O*MB3PrA%80ClJPm9yX5Z4+OiEYa8~381UzULcgqPts%Vck@EuZsRUc*oJio zoFWT!Wava#Cz#MrW^0NydAnimgZZnK<>j9Ktm+Z@qK|8o1;Ivvs*CR}^d)?xm7BLp z;e$H<&Us40b>8l2+EZk}aD=*Bpzv~kl2@SmRdYfXX%9NJvj}wkoWhH1*#y^)hufHxlQN< zB~2f1Ynpb0fPAq`VTp{p*FNOcC!-{doO1bGbuV}2V(A*=!~#|IT+{e@%5lcGesff` z(}V2qKG=t3Ych@(sHPgchy(bKi^}EaDGgI(sw?C`Jz!6dF1|2Zye;bp%6ao>Z#V5s zkcCz#V;ZvwPcRu_(abiFcJZ|j$-b?bi~aP<#}(jr@x_}O zCE;#&^YV2)F49jM;5bD-M;zb)b%wWbDd!c+fL3L#)cf}`)s~+bCT^i)UX~~ku7meb z&aaTH6N4Lcc%HmO2F7oQ4V(^d z{oeS?Rj>{#mhNMQ+Z3oef1IZMqW3&`m3WOP$ie^349$bO7jb@Xf$H534%$(sc7sH$ znPcGhhoHjhF{*7?fohP0d9-pd<^03%0R7>2fpsZv^=Ko!Gl5 zC*TgZKnMLK+!au41B%bM+pNRniwqO^n;ZjFb6~7lM(8J@Z*g`w`yn6z1Aw=iX^f{w zuwA@Sv{|wqJezfxZAhSlzmuzvr$?xbev)X7zw-wM+YqoFxcUlJUVVbQ`u4P568&(H z9sJ)a@uR9i_LnhJbmWUp0i??x-d)2cn7Dh1H32(ACnMIZUeY+x|IO>*5~W^l>RC7M zDLTme2xDC!u506Z>VM>LZS?oC?$G|Rjn-J_Z$%ra|yz#$-k}t28xq=&GDpNo_4{-AE41Ueq4aV>q z7V;ihgg$D!;j|xJ5rdSx6!jQuPq>x8ldlW;lyz9N znXfBfjbx=-rdH}joNbtJYcIECy>hX;SB-SFjAT9Pv2t-YZzt6kIpbR2p6-|NM{k&zu7VDgAEc85L|uBq?GeA z)%TFEKJ8 zDbLbdMAY&p7@mH6l6MOYs$PZKWT0s0P1 zW6-xOBR^KZM1yEEPY3d{-P()G0td|gmSV*bcoK~>+7PP8)kbC`Jm3%Wfxnz zrb&!p0`h)1_W1KP3xLBEZ^$aOn(>&V$4fQPTbfJo4 zAmTZ&2h#Lur$K#m!FJiokIwpO&L8o%9@OclN!R=*{w_984|i`jFZ1XvbfPiAhUMo- zs|$o}T%oob7`ssHU9(tV%ZRjyw3xch32Xgtre;upQ;o>-zpb}*%qo$ z&VRN9_hG6YUBdTq%~3f9a`l}e&r?R5>wN*%*b*7aNr?S#Yw!0iTgf!WHdLknbbe9| zMwtU0w{ZzJ5YKmTc)Mw*fffV(SIG!H@-zD&_`TKgffq%zc7}8f+sSX7 zNVIl=z&H+n3C?fi)8yxRUtq7nzC*E~Q1<8efsXr_bW;!NuHcT)a@DEk3RGJ}+`U+b zm5WDM7AOo;An((kSws=8ry5Aszl=%Kr=J8?Zrlr%Lg$aoq7l|l^5~`vQfa5v^692n zMj9lVMc0T>PJkt4k}yl1YVgJ%?dU(-a|5t9bn|E-9ws3E0w@sPSUdt zwTPe}vW)CuB^kYcZ~2*d6z(eAMzY?``~7>2g9eERYgeCBX8OV>1uo+6hj7OEr~CmDenLT~*j7Ld+- zTzR{X(6Tkx3I7^j?+bTtjWnoK1?m>l%sl(lJ^f}L-N%)qL9x&#Bv}tM z>TY0YW@M_NZ=vq;o~RVoNQc@<)<39Y99PQ+Yg4uo;o{wgY1$JMj05Wk^Jus$)q-gA zYME#0e~mv)``NNdOs3i~Fw%;%zg+%NRlXX|j#3Hh@BoKFD)uhwu~<{lV-SJ4g;p(a z4Ajb^ozB)wGIH@XPJ9uEwFh%EPno49&`~3eeA>_X>XYBwTj;zeL~AnDAeI4rdxTaa z{lk0s^ZqQIaXeGoD2aCZaZ#A+{!jYwj|Jj zbOr%|yS+lfFhQ}v*TpsjEIsB?h6%vxMLEISB3T(@>fyoPL_Y*Oz>nGU*Im4kuZyP# zZ|e*R>5OHBahzrb*a0^%XGk+N4HBrw^OPk@!LKFjYo*|>UVR#5*T~`QFI8M8+{gUr zT=j@z!8+mqBf=VZOIle}b1&k89HeTkB8`$Xvs*-7eVV7-AW$m#JA8x-soEnn&FlcD zi@2!KMNe0iF_R+d5P79RT` z>9U6>h%cGN?qYZG9i#fXWNA&)Mp|iRYvc^FBc97v{vAHWLbXh_j71dI-Vs`}sAB0f z?G_&VWx4zvRJ`c`2ROsQnv$#!eyvz)7b;z|jSGLtJlfA`7wQlcYG)l0=;-FXNSi$KRaD2u2GhUdv#eSe3452;$<<65AaXzc64I1YKwH1_Q&sKd%O zgttYyjB-M>_S*^;X#xH_`ykTU1;P>;{wCEN#RB0v!vy0v+7ZnRUl&&&;yLy%!vy9& z#lkH#%?#WX)*jG|{`(Hj2hnC=D?{C(oZLgQjIa%198}2BPvUNCW@l-6xmP_JVUer{ zu_>wA*TLT(BA@PKx_eR0VI1J>^m8Vd0Q=(_wnC0{nPI~6Gk336)&K|AUZ`E5W1ypp zuU+WhnG9BB=$0na_dyN2D2r`ehahmnNwOX&p#ELw z?1#P`W>qe}g${Ityzl0Pxp4@pmFf^+9<>d7>-Qw@0K+2c6j`#~-HUeG)yFX93@O#% z`$Luygo{MuHX+FS3xo_E=v(Ad!FHsxdP$U%6vG{yL-aMGr{B__z57t8VjuGN_$q}; zB{J2qW;8Rvayv(STr|W6YQLYLkS?E~u#9l_bN7lhfprRe0-PP18KyDd;WLeG;)B=; z@+la7o*teaoSi)^)?vYR?p~n#cnObw$UgYrR>yyL;a19d_CcVs!!p7;Of?729N9__ zPssaFyEJ{^S%N2 z;&H}Cad4-JebB|1V_*wUphLVdQ(LYA_#VJwXch(9#i+-=F6W3EIYMniY?|34EF1%m zi++ah7zl1azwuuqY7}1~0W~+g-I~O5)fXsAl-|GJ!}|0p=A}r>IbuKOEp)Avd^xC- zEz&Z;5oX&agmKU$)-1}~J<3eBTMJcaEdkiZ@c$Dd$F=+_VeUE zF8IqXKAIWI`5Br?dQgvk!1wf^z@UQ{Xg6NK^Jc%TSB-1HHOg-vG!O-c)JBUV$I4F zJUl^-d2oJR!&*h^eyxyW8F?Lya?;0T7iu4(P=<1{i4USj%EbvL+L@qU-6-=mu0V%r zyl`t5-x?9!RE3=G*Pr9N_)gP?*e{av^ce%4>ab@A0p7N|l%y!#OM;ue}=BIYIh>*dSg4J7m35eD>=v48j&zxN_8gIZ669-`#>kd{mm<(X zGb7l}FafIR5v{R|VD57aFiiZl?f-ihYvS)@9H&?yS)rNP$0S^59EZL|KOADaf*WSF z4Wpa#ah0kqmw)%+7COp8BPYcWe^aR>(hB)>fFtjTho?k?|C1H4cH&p;*&ArBcZ}rFi4J4?8%Ht znmQRDo#8KiT_zYP=RvF+dCeC!Lpd*3fpM@x;^R8Znqqhfe}uM7 z8g1_4dxoTzUnwhBaSI)5W|HvfRgw{?*;FfqvlDLfMX&17I^h7v&+%hk203gHe0bN# z1$X857Hf~QzvwZ=0>K8t1^y=19>~UkQRMALIzu}Gxh&>U{7qmBiZyWzaQ5SE;cWry z9at$sZNjaj%RD^-9iq+bgCK+Ob9B%J+lkibCj~mdOrltzSis&rM8`M?a0;`1mY$)r zK#^obyn2cZst>3ZXl1$h_Hjj79HL7!46`zhyZT%p0Q>t{dX044i!-FB-w4;M9zoxJ zct^M%Zj)&IEBNWpKD>*2LAHc(kfV`m&@On4innE)sQc9_@&IF$IZMkhWsOL>Ml;(q zUa16R7%mWQV8)oB?n;y%Rb^_=Q~J7G!e?sZ>?9i3N_~38I_%}{7${YH4;gC)B1b>( z+qm9tiN?*M9-fQj_mG;|HPTj*bxCmITKkxXP6AZ+wAadsa25*aD zf@+RthF}Azv49LB@+pV_0)LRRALZlucLqKg#sBMCQWCihDrkby-R8hJ{wIEZwpR-xC zn|B9Cq-B+uzms}aq-BjL-Za$i)2j)FY1(^8s=1djFgM1DIvG`uY{Nb}gM2LY?62(q zAhPQ1*2gu-?&8bU=i_RWgnn2dcLU@6k#6c7(em>&?E*!tnMG90OZ3Aii$-yvXGXsG z?nCl(!u9VDts}rG^(5~OYK7!c)j1-?0#LNiRc{sU=4BaCE%+6D5QkGKl&%3WWu~zk zm=lys_{T*wGjmiX3H6e^-NUToi~=2m>+qLO0ZB$t7M=VeEkIc%#qf>)7d^+oShIY! z0kHP4cR2?Sfia{GG?B zss)7W%%f@gt-^`MdsvT)d|aIZl#83h^3^`Ql4xKWLp%re{IB6-?t@u$f$-Mvi{2vn z8B(m-r&ni4t-=Bwb5sc?F1{6V55EH?i(}L<+Y^*(nIO(%bom{sJbS5S9LWVT^1V_>i8 zClgFSJkcnLrw3%O90P5`QVhqKyxhSaFw7eK+V8D?T9ep0BH0qt7xN%~! z@~`cFlJ!!x!1{O*N3s%aUZ&8^i?s*rc!mkGr8_A4N$y@?#R;{s58`ekosBZ%Z3(w> z^x9js0Zw+Ijp73wNk+ehzCx1sM5M(bh`&?0_z109=BuuF;{^iJ*$9hzZn#a7 zQ7`v3Y_al_Je5MDq)hEvDfYoDIIBqQ%yCBLVuTBg91ycwA_IQp79RC1!-PRjU#SxWL%~ySPvr-qryI-qsNs=`wdO?KH6MIr~AJ?*;}a{e%4o^_XdleUNr~ z7n^<(Z|fcsX#e~gzC?p?E3g^a2dQUiX6_-$7uE7X>`wP%<*(tF%PW=w zWipl#pj^L;-OF99%sRYE{QkXpbflF+8O%+!OuHcTE!xov3F}!V*dbu~`P~PDRPjcnGvrhB!!10ZxdQa_w{fdwgxW9;K=zt; zI#>PI@Uu0+{_)lih6FfY z*t_(TjN>3$Al}H|c?}ELIKU?Wu`=?-Lv;4RU&9A6WVkEpS%M9=A;EUCCC>h0W$>-E zGxffJSUAvzyMlXE1*&i|Oo%p@DP(GcO6PSCbibk^9qwtp?o)5A;;PmJ3 z#MuGa9I83ABeo$>)ro%c2S-f25yXeY8^I0$xIo}l3=;$!(6_)V{55=m4up$+%xzqt zCkvhq?#ia=n@2kYY2>*060h!KQq2vqjWMwet&mJGfU0BxPGd}+{GJ{wB>0>8YUGPw zb-VcVzKpO`Jt|a5(?>dM6Y_f-=m=Kiuk>|7t4LQL^<0dDM^!jGZ~Pf1LhR2Gm5V{f zrCR0!0c1qoyx;nD^Y(IAJ@R!ii=Cx*3K(NLNA!CuS$~EEvTiXiFX4A^;I7b)7{{Y5 zY{F+~;!S^b{KIX0Tu)HuDIXWXUERQJ;)9uJl=Rv1t)IIWS0C|eq?J>ENkXKRLy+#* z28koI5c>fRko#B9^>HOx5oyWNDpX+^K|Wm}X%$93Ema(6?Ba`gX&d${(HGFeY*P#i zQ~@t9M`MO&4@;tfbr{5YZlPhXxqC%gK<1vim$w^ppK6Y73PkvCVD6w!P?$%7mk)S< z0v-69^po_HcTj9Yz@Ovn2aL2IoQ|J&p*GBY>e)kd*lX%pu0EC#@OSpXO4%_cz$ZaJ zoTXkSg}QivoB)gQLET4Zv8HJAHlYRyuopeaQ_s!P0-D0V zhF|v}$!L;3)c|CZ;V*O5>m@<`!W@kb0i6t#6OMs>OfPqmm5B%j;cez+s!uY8ulKtSrq!# z+pSgDFvZIq^*CD-{?g4`v6OzYMdac4H2oKGT3MbR_954>$Ef;glJyE@^5t?ByVz() zNk-mo1**N=2p1MnAVL82bU(fNHGI0MAP2s#kIs2dpzfZf%T;J)AzXlL_>brg$N+;F z4dDKA_cBZfbntbtjNorFk8<_FU((F*c7uHY>h1>S6nTRHdzY&Zu))Nd0H^a;;=kbO zfF%TXg>;6!i+)JF3aW_5nq_J?i%Qi7zkc_@G=7oX%^T|O1O?O_RLiHDJ3|VwS19Y^ z1DW4fpLlvY1ZJs^&~BjvoIbqse?v3VDy&=_=(vgh8{^^c`wfgqLazD*gWp@fw;!Ev zU>e1n#4>a~TY`G02C1=Tx$4WL^poIp1yNO$lh?sla8z?%?w}4vgM?f~u`=YnW8ggH zJtW6~QVHh%3Q5u9xEC5ZO=5qad-|T8ry$&8?ldYViLOxY0 zX%mt!{}udY(hz&V(s}>>WlXerxXm2ZB00?r>oD08;yK9hg7q)j40aRVZh{TK)Dvj| zxqhlS;0dw~AzZKy1J)PI2w?nyOaX5EbhsEmNSHa|i+vs0GX0xs&WWAL3#RZ zl756`nY4!|%=U57UuC}ky<_i2Sl_^W(W{pPRVziC=crIlYNSt*zdrQ;@?9 z4ePMG7yKpgd<;@Ebh>%#9w-+tldckrH4U-@ULN{k-jlp1ilyR>o*tC*)3ke7zAl9- zzk*+)L^bE(d5A99ehGgGpRYE-08}zzZrX$jRm$bp2?1vdaGd8UA@3Q-*$3|-Pf$Rd zFxp(D(Em+_&MmZTW%{#q!ZL+v+Hji;9r<#@lxbR;8Bo9NSMbXeluEwnT_C6wZsBQV z?O{DjM?a*V-NQmYJx3&6W*ir416exCd6W~d_IY|lTEN*4IAC;BVB7$=8+d@gAEcZI z>lwV~hjs8P_?-RMups)+G={&qK(R&yeOoD8pek2k75VhrEwp%Jq?J|{(OSOR7?X8` zZCHr?x2NUuv1Wtp5!UkM{%@iz3RSrJG_!?UL5zZ6L$Lhm#fby;OE&Fz=AkM z9%Mg8-N6CgrfK{L%MDDv8pnW}_X+G&|;7vFZla(TzVTB(QMeO$pv(@(OD zjIj8+G>bm`F3>@~2t2`9Go=#T?Vsa|w16C@UFb1tt1#V^apJ2_T3H~MO0fXq@*tWE zUM1cLPJ6)ZzJjBf0ee4S>YyJYoq-bqf0JTijfj2{@BsumI0g_dn8rw#spfvg0szb+ z-fj>91ndvw(-9Wf>z6TfQ~GJyn#IbQ+RUSXH<$jbOo3xSu@nL#$%tyML*U&9jU2!O z$KBd zd?4qHy(?Mo_g1;s)#nNhM1MF2dbzQ8gB-AT104z1U&IZuP15gT*GhfSqnnzfCtR7#SxYt%;CT9&L5A^luBl3Y{LMncUdBeO%_z zGS#HZoA`iv$=z!e`7(xnQn+=Lxu5grF=)|dFLw`5SD$U%QbpOyX!A~fa9f07!vBpz zS*|+#CI03Gf@FP)A^IW30?@JZcAH>;x}%);a_8gUn7DStg)^1?@-U5yWh=bv61vq`S6m8b~VjDI^cLleJ|F{VKkbd&# zGXT_U5NSa_v%;J1F8!cCx+V?C0TxU2-?Eq?`)93 z+SAV5#NWgR_kgS;gj@d}U!^cj{}dT}mw8mC8t_W%CFLr*_~Kpw)~Z=-iXn)F1OCb5 zB9jE?k08ILTA=r(Mw)D?QWnI8W6hxMK>Z@f`?wdQ%&G;a$hKjm%UW5S{gU-*`gBu& z#r^xO1J$`cE?*bAsSSc3x6W>0&<|r?0@fF}YgQ{|8vn*$vi=aAWaSQOfufUNu0pN? z@IOHPUD4)7aR>;*6uZzbdK(0vUNwmcwVfkITD^bI)rWS3aPcJXXBMbTHE;JG)(*}T zol}5D&cp8v6Dozk8Uvg@&VI1!`MQ2%0J9ICBcdMypOC*3M2Nw-fn5P)e@Rv#AlQaj zhZ)E5wt$}?)W$IYUh^{#0L&}VW}2A;48S7e>N`f|=>fNHVqPkh_`d;}ltpsD`Vemf zzF&y_%a~nkuqOQ91ivO*Dp0izlP~A#>EkjOB(FsTuweVR&X7=#K|DH3%gvj5wpdxR9_h@(Q?NbK$|_Pl zH(w3;)cGUI$sQKm)h52)7mFz3Rov}xn_cV-onh8X_(Sv*!$(yLWl!?ly`CrkJ$|$~ z+t63tc+(Dn4grpVKCUgi5c^NBCh3cnL+p!{&k;)%&0>KEmZ9?^?r{;QU^&Dl+Wh`K z;kvt*YC*iIV<5rCSKV7^ye+J~&z7si^5vP@_?zd5kE-lK`?xv;QVjndKUdu->2>fr zA^IWean+-6o8;%ZU+1XcFTreM9;KRtzWs6k2dq=T4PhE1Spji6xT|B-YuF0}sJmNe za0e9F3Sg!Pwu4&&+`agl;NL)1&%YmiFxH?-3^+^C4^L2_?!ddmn!x=9$@(ok=^BMH zP+dIS25+lI`bi%ArK=CfYlqnmvx4k|P524Q7!&d-SR*4WYeXCat-{IA4=^qe?jes+ zNmd@zfr!LkbhTL|V{}@}2v<;l`{zc;my$koXld8*E=OMre>eDMSQkkfw#w!0 zPo(t>V_C6Cqgv!=H-`nbCswu~S)f^BRwWf>fa7^L69eUYN8b#GzD7YrHfJa4;+19s zF}!}nI*YT1x+CsE_?tqU@kTU;v47S)b@Sq#KV9cubhTXY7pl67m=se9i>gzJsfZ|N zBnhjFh838I<1{OqT&H(Er)=Ny;NiP$yKT-qKjY0j_ryFCeh#>_w^z@2bZ^(V^iM%D zmMF&yQ*XZXWP`fP6_IcSNn)ji-fU_^RZvxA*VMDKH&~IeHX#Z03s7qy z9!j-ebyPvpjD-NJqe%I)if3hC8g`!8_BN{a!usx!Jo!N*e7-uol`Gun{8 z&+uo(PHw9qj0uy9{kV^g8cpbanU1^P+?O9|Ly)`#U2d-=jRtLJ+S`}Dee;@(Pe#w1 z#@?4G98%&?)B)3r%8Ib+Qa#%wnym5~l(rZcrcHu8lWgNen)gP}k(+9BA!BLzew@M7 z82qa}Pv^)DmcG$se(M7w7M|zb&JH7)&G+GK%G07+pF8;}afumavm(c4DvB`2U{u|$fH0QcUz6sRN23vV`kQNdWZY5DesH(LT(8Ey3 zHoRMI`X@k%V`Wxz=YHdzPPP)9K0@H$c}Z_iK!}tXX21R}K|nyF6{6J=t>0EF->LLh z$M(~F{l+X`CiSBKWZ0ZKFw1vHG;Kr}pGI$F-ecz$xxwZo`JO!io9DnOc-$)|w@qMj zfljs58wji2W6bnHsl5%!^WNs+0mZE}DX-Lkm^M72eo}N;W^vF6^JJewV{$^5z{dk) zM%OP+3)fvLY|5xm$|b+){1_Uvf9-M;uZ+^U{$bOE>J|gwYkW>uW&iMOLKRFx;uG*V zKV*o;8!?!FVfOs!^raai`CLhPCPtL&|FFSg^xJU1SIy8%T3olW?`(MNco$#cf?|zn zeXBqx!{&w(DU6Z-tC(K(rGazfqdi(K`2(Kzp&Gyu=YA^4VXsGJjD3HR<%~J-~*N zRL5PI{fg{Av=9)B$^p|NlM+|j678?I&Sxcq%M|EvyTUanKlY1=A z&hv>;jrG2jRoU2PpFD*{ z5+O9cz%VLG7r8x~9_EDcYVGWwV47YdC(`(9qZ45K2BiV%* z-u>kNK;i;rbobLX#;wjhJHNUfU;K1jp7$|G1Js0Ws}^u*qgg)6UYdIa3}+4r5j#KX zKwtmtQf<;(TeSl8_!W^{$_|)>Yi(>Lmiv894v5J0cjDW%{9u3Y2#nr6ODP{#MTGpx z^R~FX_TrEZW6t)^Zk>+{V72Mrm^t{CCL$nqrT67Pht*1=wUFy%|*2r|jmT zr*mR)10sz-Wo-sbhKvH1h)TdPPE4VG&FGP8lI3dOU*3Q>v?6Z(y4T?K;d|oys$tc-`dr{(7p7X9nN6N+zRQ5*&UHdinLwkn!TDG>5_u6PyZ4 za*-VHwau_eG;uRFZ%lH*J@zyAkGU9;Q^uzwVJWfOr^cM}2Nw>HoU#IbhdYI!>TcVh z>Tk?%%uRf)92vn(V-T&PT7*6OyNL#1-dNMB;aYFZ-ot>LU)@jA*rlJlaA0#48A!_< z@@7{I_AT&*cc$aPD;W0cywlbpO5s`;kBY8+u|MT4bWatOeTlISsY}M zRy}tuW680z@9hnbnIppH2n>!cK1%^&s7-vAlPKZ|Lx#nl3Ye0a!5lu4Y~4;elmi2y zMjQ*>>R_CSjB(17)A6*Jp=-v3IE6i4m5HI2I8Mst-Im=-%AKdX(A+qRnuMNV)xZn( zL*WhKdo!=dJc~|EJUi$QKCmkLWNq>HcE4+)ssecBoJ;%?TGeK_%=}K*C2so>tHlpH zE8AT6CY5Fq1^R#34ZG3vsSHJ@1@t0#cWTTISDs^Ap|9YJbO)$6J2#ugZ)FQA zA!9=k>sw%<)mRh%PWJ>H-q$o6T8WSBtl$&BhmYRg#jVSXT5O{&>e9GpbE<8pqQ{8U z8*|Py`!@blA0`!fb0>6hA0L@YxE<7y%(>*G+KU&DN6v-5?$=Qn@sal;vnNx` z&3YxW_{vH!H>Bp^sJi4rKfQil{a3h_UV5kJ)tL6C4x=kzv*ox=(J7Dhg?6uq_Bb>x zo;P3;qoI}lI`^^)>;9o2n@MWrCEgbAIU%=WZRW}LeAeqZc7scHXbOp!Aj$8ch{T=coT!cw?Bi^@HC^KhZzf_0zAfLyGh$a9e0 zPr8h(O#wZ57b1^y_5;=hw-dUa7LtbmkBMcU_`gC6+;Ogf_Co@VnkN@cn~dF6@ugEL zR>;9%slM=fH`a2YaXK#ZFCeL4$snn2+Tp3gC%9s3FH?Jnd}imVRikq}ZtVK(YTG=h zqiTQT8Wd{_-E|Xh4kPWFL)T=MDyxxlo3wIlUcgSf3eUScw*uDXyZ9u2PVMDEU{%C6 zN7dz}n-J881q8NBap`JwN14R81eqeYtlhYNPvmHCKlIUEePGnR(O*kp$!|~)`CrKt z^-_vizPz9(1H~}rpZ?@HZ?DH?zudFw3c0O(=yZ8JOSLY@=+%+kbYyPJ) zfX9ob)DA6(_mySubs)tY&UdXkmh=6*!e^}%qs?=qR`@dS@X5qsMAE_djPB;m=|Hh9 z!_XdA1Lgbc`fm!>CMUL)w{1H^oNhM=;vNoU?#6&cF`O$tl(QrGp^zf2MXZMs$cjZBdop6igwO%a2 zf~L%}&eOho6eOVvB*lN;WHZ%_c#;TNwd?#R`=D#9yR}#La7zW z%KOe^A@8|dkh=u9WoMM{+cxxSM45hVw9`@kAT2t>>Cv;`WuetFv=x~ZVoXCjC;SD6 zI8tlj%$`9>tjhrFGh@I5G8Os2@5EYp@n|xA-nnE)HcRsU3pU3<{IX&Vm_?`em3TpA ziZ7o}4;I2$XWfFs#1?pEmL`RboIDImSJ<;1G0$jCyeEVrlB&v?bA=6c2JhzQBkB~P z3MybTkHftCQ9ibGGMN4|rqJK=Z+m||hAGAR>1Ip=qoO9M@8Q+#9gjP1e*{_*3r55L zav(l5{0=$>sffV&ZpUXgFphEmqR*p7xp#4DQI}Fc9BDGm1tg6q2``Wy0^$#?`&vAh zD6bHpSTW)rA`J8ttQZr>(fY{k6$j2Drb|4*w=gZi>+rhwNxBrzjM*2npknA+@p)~D z)GCT+zSO5p33RcE0PVct#=+~lxs%bbFt=rB^QBO@e2h6ax6Ouk7Kc)fHiAA^PC9I`zI!fCG_1 zh$~3Z;eDh*OSht`WuQIUDm3{EF^z9`@lAEA0ft?+^Ydq)5fnFM>q@BnKOA!Y?L;H{3kKy0M8Lmso`<-?Wv*Fb z(fXr*1A^vzMNAzssZ?hC-i~Zf0>;?Z=;@T&L{l23Kfmb--CxhwxLEf^KUHs>SAKlz z%5bPm-!v^tBP0Jdf3jDX!JrwqhXm$S;OcJ7%=5}yrU-Sr0y%a#&>Jck{M=hCD0Z=T z?bda`7~p|O2E4FEdoJ!roZfpUrucDHag5-cFr6Tf*Uupet@N^$1S^1hgQ(>o+;Tu545drJ?tQMW5*mPZVfdzA=M7a!$s84OIxE=D}x-gznB`08`?2h+`t zDvfCgt(o}Tb_*N@W|>asbFywR#Wion{$#NP&?P;@N1gUtrVgx*Ydk_zCTU?7{3~wn zuAqwbkpb?Tr*KW!Qj6nJFRslGX$+muz7%oo1cnX%=EKjJW#K83=uZ4cqdU{1x|t`; z(q6!FpG+g40)@UhC4+`7uE3=8v3rJI^g-q&++RS$Xc4T!^CmYUBk-12N&P)X{-53r zL*nsOK@N%L#m?q?R7a$9pTGjc#Y6PBVaqTp>v~6p`P`r+M~M?OpGVjbYa$c24A81}9rkPIm+e}*wzaz5D-z>q_I|(%1((mxr{zyiHSjl9uA=Cqb zRU?v!@2||DVz>w)T+_HAzHB}tCNiw{E+81;$@t#3Um_!eD9+VOKR zq)8eY(;X1@E|j>K)ak=LTeEb3b26*2_jk0bUZq# z#|muUPH}y04P86&BZdqd%Kst({>jBQ5*Ub`O+K?kcKA{Z3%A*)oxWr{w2wfqyd_4B z9@n!ov-5A7 zqJT|a_4;%|%|-ItPCf)|tz zhisukba=UQrA7R3gJQ{`E*i1aT_SOH-*jG5e%!r%xAQHFigP}CfL_lF27TVsK${n$ zNbzZq3dIJWlVT*vT1mF2TPpR9S6E(4%2;Vod*(5myjIDp*Yjhf1t_06mRT}6+#Z9- z-vdl&`!Hi9bIfsw0XT)w^8M=p+M8HX|4@h?Wtzd>W~FeR1SX*rqe>5jlnRxJ<|Nw% z1teI*Jy5$q*y_w2>M^`8*SC}vK05`T+zZLJV!)Dxz8@ivz0<)nCD4pz>|{Pf3TG}@ z*mM%D6W`JUtMi&riJ{Y}Q><3&Y)x}RMx6m`heQv{(r<^^^%1krWPo$sLB*#WNQN+x z=Tqu(z_;2b;gt)@3wiH%>)3|Zxdo^pyg2b8?t=iz=g1%E%|;XUG@Oc5d&bSrda)<8 zDE$2yHGk>ldF;-q5z*Jw(0%2;tfYQiZTk@xE(%~D8|CPfJe_=m&wkbxs9{{axLO~P zZ``0EQaV36KE*IA(fMh-d!$i{XVty$QWRwX$Fl9KM+oGK#>)&-hJ4g{R|Im*LFOO* zmf0Vyu~LI&a3}PQE1^*Hxpi#6n)exsFn+-AoK2C`3eqP$q3$f=)tAyB$BAXS@^O9K zxI{}|Gjzr3jIT7uXp$6e2Wb{63+t#zHO{ei3U}NEsPQST_2ScQ3@**AvQCN$jLb{1 zPi;dHB3(i>?r-R-&X3CUPi@f$A{5N2-I?B-Sby3l(_q!Z2WlqJVp;c@(<{+#Kz!qc zz^=y91(8GxPmNYsqK4F~RA-sqT)*&psxtZ19mxxwwu?(8vs8j>YN0l1Y3@6l-Yuxy zo3JHXct#A_`cgxYL_ZkoFF|tqXcww}-^$kV)Zy+t5sqcgu}7Uzp=5fe;(d2XA^gmf z0M9Q9JA#hWfv!UQP8vvwtI##pn8EbyuA?)?g%39@)4T?iIAi;?&FFcXj{_&YoKyy> z1x#8VsC#D;)7ee8gyCiU?dMor2e6%-dpv~dD@m(n-)Oww323rB?vE;==}8extO5dp zoqtnWjy-~?>YL>^x_wBNI;G#9!f7l_sPhQXYu?Y_pb6vY%q&~(eKes<81F<*hr9Tu z_B#1ybb8#JBXH8=QJcm>rUQnOe1!_ti~b0HImzS?8M>${;I?!(mf{Z`pYpLfQ^S8jNnpoyhJ6rEDzu*p%21o% z&~K`a-%POR02@D*z@H2co-RfzOgQ#^GyY_-w>raOk9rl(K0ZBOuNNU` zyVB2;=a@d(0V(l-X-wwa<;>w0SbH~OdZlXb_isnmuG#I*d(D4dq;s11uWBC?_v+2P z=K+MP!*he2O4a8ZEdtiB_QzaJsG@R6t$bv(-{d;Pxj`(VjY5A>ei#PQ^JNRh2Rzk1 z-?}m(45?IB`%3o*u>^ZzqB6shtjQ4xwZw3F>0?2; zn*nN>-btF*N7P5D(+N1?d($bHHx~Q4Pd&#b(YU{av6-Eml*G&vvvc-}@w;VRt?2HN|1Aq%!=)5n)h0(6@=0dX_X?)d z{#v*3)8Bp%Of4~V%&-5|6^eweXz3v{C^M+L5lOb^56(L#ijuHr#T3 zL_KN#ls~U}T#d^3+6Kw%>L8l&n1!1G@Bvlwc(>q~oDs6U?Z zn!&a4&!U{YtNv%`FF#2vi2ce8#(y*_r&o~=c-inxofBBJCLL|Swr(Tl#fP6h-Q_C( zxO5{8;^vzg64*IV++yZsdH#rM$t?J;8DQfd{LPW!uuJQ_H7@TqWjmBjv=6=jJ8i3w zxc2aPYnzIvO(JlY?KC08v3<9tO^{o8kJ;ROi7!kwnWeK0|H-fhIfpXLeWzci0Y&X= z%Y%LL2mhM^r@2$2nCXq~eAJWK{trey6!nGAfUM#JX?RABZr#gHA?q@w1&v9(d|9Hy z`(Ss8yihD+@i``ScE{Pd??JZ?_dbj=WE7f1?j6b4miZrAG3k4g3!g8Y?QhFwA7(#t zQ%sr}Q?i)PXMp(^YlpJgS0Y-=`b%hr=|54lcNhG&~-J)9xy_yl{Es9^i~?0t15OOIYFb1Vvk9KO{tVA%8=+O-Bv-NB&AON51A7CK+L7p$<^pfk0^cqX(|;pM})aM*Ag%SmOAa zErA*c|7&?_kXk56)Oo~GQK);aI_kT3Mr56eKdw+8)#HEm9ATM--K|;9&8J(P7jsi~*D4-6nm7O=rY~ z(J?jsDd;~(-;*e%uS2#eeeUy$?#2{TWg9zai@1}=ADbE?gcyw-iIuXc{n zGO09An!bZK?1Dqh2>R>8iPnyj;oK{ctu`TPT)zV5vAvw*4n2yr3lHDVC@C;EjToo| z@f*Fot<}c_BlY(3T)9euYJx`{qL+%-j~<2>uq9tp0$IZd6I0Z?-rMo0@c1{{%Lq`RjlW>il=9c!`w@FVEA2 z?<7@rHcTn;im~rFzM<PMT8Ra@ij_1-k{_Z zZ~#9Q(n6xqx)^U<1qX;$_I5?sefl~Ci%c41{FPB=>m^C2{T^0upDr9dncwYMfz9bLdRML3A&Z`d&Gjf0i{1`8#E ziu;8mIQ;RAat6KXQ-Ufmy=%*jH!UK`7?Z*YQTi()v-E3lB_y|%Uj6GuaO_C-(og0V zRA!OrY~SHVsm`b@&|SAaU#h<%yAqr&vHi6BO45!_ozKfA;b`beE8MfrT5c(Ti1H*- z4t3{OL-@;di>82Y_LkwM(~IsOOKO}U6cTI5NyAj;p#8=Sk89o6Ifp&8A}hVoaCQM&m z5ev;N1`|D1nq=5UTlaj^7bpCi&SW*DH7Snb#r0|+)z~IMksJ^_>me<$bZC9#ICjzk zEyu6V8rUUpu7h2~doG{W?zVVA%f2@IZZJv=e_HmpmyWFf4%)?cx8F(Q@+#S_UBYQq zWyxxPA?(2BYmMz=#M`T#3~>E;bJbMyoT9|afrUZ!7^G3>-EmW2qoKDCYdUj7(PXJD z-|?V>@sd(0%=lAr*4GBxQ@5fFq$(tP&a9&ysQhy=2BvQOig^-MVguM-U?44!w!BpJ zj~CIBkI-9-b8<&b5AD?7P=)9NCNwspulcv(Z~_618GgRM$wjHn`!3B#!?F5HMW@lT z8snA7Yoh;RH3z?gGN(b})9^*2^!NXEp>y+;qv~5&m-xinH z@o`PAgK|PEBE*ZoaL^ZizY7*=es-~`Z3_L6&oPR&vDXF zdAwJXLj|H?p?T|I^YqecLg|8mWd~)wth;vCBbnk96N>PR?t?$&TXhr18^`~Z_r6@# zukw4yarvvHTD{wsa+vqHGw{fX0gp8@m)_h2FqeDlR2f@e~` zInX&@T8sXApqSXK^ypE=uW0N$9RO{4da%La{NRzbmHBUEX2m-3!DVx9vR6-yfyARu z5c)|?d@+Xv(7ML9-L~D#wV=*GCL>1~Wg^d)hAl;8GVKcZ`i|Ck>0SP+yU`*K#IXDN zqII^H1lM%&6Q&HL28|#tB6$G)de;CdjPtPkYm8}`Mo(2^W4 zXv(;b)F2nj;-lK|O~vm_;j;|zSOQ$g{XY*Z&pW=LH%p-)e}I61Kzy(NZJOUZ?pc?8 zpr9ZiF#bd}O`TQK&^6|5(1R!GJKZ%A{i4Gv7d@w~@XHi0Gwkw{JXSGqV|OotwjeC_k?6 zbbt0pc>CVXIEI0ZaJB$`xF&E6q&c#B;RyRjYRo&s$UNy3lS@YQa5;+DT)&<39mBx^=7-E@&UOB%F{IWHs5!g2E zG_T*o%rUtJcc2(ZETb7Ub1y(-(*&T%EJ%rxR?ZR{*Ijr6KtJ}eg(lsr^cURDU8>Z2 zFhF8L{l>*OYtODD1lIK@*Wh=vVHIoe@%W`}rOf!D5t)A6}s@Ki#fzI0kKT z?V=151xqhO9jX=}2FQhz>Fi%KU$Q?C4V#pocD1A@%gd({umDH=mm}z++5| zQ0x1T+xcL9rJFV@g6=L*i8bWOV;-(iMeY2lk!zph6DsB38s)-neQJTT!muL_(e#}b z;&Cp+9eYOL<&fe9tu+^E=x+paC|Z>rIaq> z=OdO!j=jUEy&Zc#Y%DR!B$Kv2(1qs`LWm!GBOaW87Gvu zf2{XYC6|d-p<3)!S%V2z#AkM8$OF#(h8=*_ViCW256eqZ6__F3yDG!Q8S_rHu-pTx z-+wZhZ_D{-0_Rnc{;^bD>V&mq>!>#9h=@!EAlfglyO)L3exrE5dxEUL6A?uewPiG@ z-X}p>>^^mE%(3g}iWPHpRIHrxkW>j=1zGogf>qct5fS9e43@x%(Od>H&UjGZIB=DO zN^S!}wJ!24&{<=_Fpv_8?NUSq6j-G?970k`1V$1nI;Bd}37%&eUtZhPI@5!HL$*FC zFy-kG0h}GP@>Ql1V|9&3lZz}B=E6A~9&cSqb4^d3VL*7H7bI83Npj`pW@TwpF85x1 zW@#R3-#UOTV*euDh-n?+27*jxaLsa;~Y;xG%6-(o@fy7 ze%Qb2`>;8&Mgxkcl(A&OoO4iSk&vqi3Sc-&RupFcP3iNmL4AuZiQ4+a`df^}ufm8u zEb}0Xd=m+Nufi3o`x3h=AX!DYnTQ~R9}!UigHiN9w*^){{$eSJOk#P8bd(rjd}F#F zm~el>q3)7#!L@P-clS*L8%zoi)t?e4-u&n2 zcNm-A3l$#a-B+s_M)>$Tflc#`dB}H*5~T&ny*7T*$=>`*)p|q#3R|O+Oj}glf~Gmn z$P43W774$QE+snf=I32T2UlXk+9xDtK_LPnb*VqvVjaAqim_*d0AJRbJZ#+o@aoSz zBKUhAO}c&XN+X08WTQhONnx9IF$Mzt&(!c6*U17jI{!?tB3SN>MqIOPe%-nn{BF8^ zl5XeG(gPkS=R4s*ksKOzc_ol#pta3gx(V86lvgR0`3Blx5Uu+wo||7x0Lu{X?^oDc zD1-Me*y_kw?=gmcj=Oh>v~{GR9fiz6ggGj&BCYV|>OMQ@j6ePpNG=N#%GDVK*9CR* z!d)@;4|mq6qwE4f{c9pUN)ylaNDZJ2eo@@oZW+5!StXf9*Qe5D9BVA2*%nbf^WUC^ z4xma3#s;a?8xTvgNNRG{BxH7fL*}FbShQ@<;fq!}60f7fZxDUz^Pw|1_Ai~uV~pDO zz==a@P^B2lXtP^x9;3n z+x7Y5eWB@*PR%Y)mS{dcmnJp9pjfB>z1**hxICm*`Z(K1ZH$H0@lALo)2KQK&H8l= zeZ$?3TtPMf-@sEXHm90NSqr8%gxY7eb5|%Zqik~WuWHla=g3f|rw6r$_dNLupTykQ z-4nWPbbVbom@Q&yN_FCgqAa&xpmbfWR93HCvwxo28DxW0P`eGT|oZ?pW^USegn%}y%e-mvtPn~BB-8l0Z zXhrlUUzH|tK|rwNxd?rx-|s@hedh74jb@#bVJ<}rpLETW4ncaNH5yG*C6U}Bl$In; zLZu=+K^wt$?#{^#ax}K_3XV>`nL~U>G}DPJ5|c_LIt96lvW|b2CU%N~FaSiJK@vSf zTwag|7=(*{Lu*tjRaT@9u+0+D>SKR;M)}?$*iZPigWghX!;Ck%SXU~wU4Yf;@hp}* z2rMM)(d-kwz&+Nqs0a=PNiOznoSPo}Y=^!oEYMpBKdwlz`ZcMhCh^58 z!nh$YDbyw0s?;1{U=i3YC)s`n|D)A=M_2-1#1m#{=>`n6C# zo)N#!d%Az-SGg&ogEF9BuO-sNYEXMKzzN>Qcuj&A%KT@$$FSy#Z$|(KgMK&IDd=u| z$02&MseOzRFVd|bwO5=^K%nCm-R$!IzMH`_h+7@&1MGsUgD`%RBgc>7v=B`>)}^ux z(PMjtxZGVBEQ+!KtDawIi_&?irW-rjt&~fkF!nF!R`z2@_RsQ59kGyopw#RCXg(WazI)PKV;|TmiFb zCC*N88iGNhLY+@gqu5D~jG~z}6>rJ<<_MeUGI`gS2+K7AMShJujMk@XW5Qi#_%n=u z43qm8@il0C{2CQ$AuA?KvBuW+jna+78Q+5!)E4&fB-9-j(vzAnpD}pdoQi-T%C8X) zwdU1_yZ2k}pdCLF4L9iOL_p4RJc~6={y?7Tto*v)$cL$)*n-8}8}BK06{Jp|q(OVN@UkmS>!rEpl0Bq% z)&LUJ8#u2(O2n=en53Nrb1Fqh0{DPCmvW5^38ufKW^eBC4(OC0f84tCt+jTSY(a#U zqj!Y(c!E}cb-Ye1;cQpMADu(3e=RV}*+1GIy8Gi9D$%h51H7L=Uh{vp0zT(_MkcWrQUQ6PvxJr$w3IwqI_+*2!9pbMu;CB^5{qYFUZm+-C)5 zKW1H{!0kAxB%z&>$?Pw`@Z{=AR|mHSxWwxaKTG4EUHSXC24DP7P&jnma7XB-r_fe_ zHK`JS^}#PQ!rxV?C`Vf4mq)f}L)&94Br4gKFJ|#JFs)CF3AkTGx>8K$1XXdV4EwD7YUn59(-%`jBfZ+za8-iOvQhkvThE|?eT3a^ zbluSfGPV@wxVn0IO!J%BV|Df(mOgHu*#RZz_7^A;*zGUcBE=(AQFiDz)X&#zUuvZm z$y*rOHPrW40-Vc-%qnN4@C$m`Xtr_cmwBd&l9uT&RiHAwU-y?-kLdI_L|i}@)=7Ep z)<8ov<-a9Nbv44kHwK_GywLYc1r?HR(OQKq^6&g6j(_%U zYLY9_u*&fDzThQV5UDf?nq}>MvahUe`w(W@*oS1)%x{0{&*wS`b}rKI#4+^`vyKv* zWMy>Bk9A0MOc`9!Kgw1v!n*{l`R#ux6=VG~NxUZP@!=6+P%cYEx6+}cQL0WM-dO2} z_5$MBq_hfgNokNy&6BEJ9=iMij__I(hiL9rbNZl1~ zT$!cNs=J6aXTP)EIV>;A>~K$bSb^F?z&v@dZ@yEQT@mx8l41LsCCvhyS(4CB_5M!@ zyeUpY#3?#~(KU)S`m@_C145l2=;`jtkk*K`r(OEa#X0iam@eZO-`o8s;S3LSFwZ?g zER(Gff}%mUEX&2tQMuQv_eX%U>mN>WY}1P8eZW0`SEFP6OpW1VeKj(N6xVy!N48N{ zm!GfX?1P(Qm=-ZYlfql$pD$N}LrG4jh>(gJ$-~$OPRXK7#5+2bU!>>gwh3d)QO)%Z zXf}au2rhK0$yJWeb{N1JpvFXKdu~xgZAD^dz~`B9T(jub2)lwuNT<9ixi&ej1)Nhw zjzVX%@HmshpX&JIU0jQI;PJH`xgNWM0p9g)KsUH(z)~0_PsS_f&zOIKHgVOO<`!>3 z7hgFFysJYssq&oxF3Oc@RRs&GrF{YkUjYq-e&-^67!jU7rd|JDBJN?6e6UJH$_aOg z_K&zluWA|0I41X$_$9n@obuzW*UCz@Q(Q*5X6SWyB>I8OXIU8pmMIHu`-Oc$w)2Dt z2!AkL?H`@4_S;}EEaLB4qS|5!a$lf`uWyUt8X9Lr*6EF?GTz<`h%S7H_Z*xvsLfAo zPQ9<+?>(l%)EZ!~J?&0;d!DX&pv&YI_&GQsl$!)~R~qS)$Si&E0~X>-juDT3k{(PD zuc3&iFkf8}?}3?C4uFbh(^(xe?BR9k9sQhuP$AS#wk$xA>Cp-I6~FH=R3Av%o_i#q z_>d_vs8H;VurZk74fdo2REaXyfV8k4Ksf};tRkId5hf~|{VS^4maHP2DC1Ry+;t?|j}m0ldnv2KwNsUW<7P1u3)N2oQ; z{SFnWoZMG=Fp73S{L{dbKJlQc68a4vB|70}^9j#B(#!5mn7c8_AfGq-KEjSFvA@9I%po`E2Wtx$r#01h4*&Z#AV6P0_aZg{aQWsDmoEP z4aogT`U`mP2s1dZq&8*SK%wFc`vK~Mnl3xSZ>Z(lvQ}%(C%6sAlfO(nwRnT zUZ7};Vu$iIG{O&Mr2s&9>*fC)8urF`tUvb%y;`;J{dwoCpMy2Jc!zXjj9_+}R$|y9 zm?yHCr_j}jdr-{1!iy9~VuN20nOt0!X6D5mICZ=#Bp70m4fe#u95aQg#Na6kpcp;a zHbAKTX|6lqE2~LFv4vyK5!)1%pDoC%Ob4VDxQI4m05Y6Iz?^{E^71N_L=wI+EKM;8 z<(-3ey9R_xR1YI%^I^1GD3&S>!`pNZJHdB;y3gzI-<{H`GNq-SE2l$`%Jti1Q9;jw z_Q|Ihu6Cj~(b0Yq4&iv2)aAl1pu=(VK&|5Z`HEmvxx}m$%X?Dk+~PNgsn>A{{&mbd z%Ff4i$31k$T`8Z@#krW{7@YZyX70l^;2tTOM!S(cvaHFX(Bs;T-S4wO!8}|inCe?C zCB8I-5y&9mw$0My9&LQ!aHJ?v`py}7qp_oKqv6*DE(oMNua$Dl2oP@T_~K4zp_~}y zPEdgv0s~_96-%0^STE?&n2swP+ISKO&b7vHd0hvCrZYhgr-KLTw)XL3%m@}P$XnXzTXMA%19Ob|T%^%o0kXABB=ZL zNgl$jIv&k(ZFpXSRc^cgqshZ-TCvC9cDDm+Sd%81B$r~qVEB%2}B0QlD70y$NguFoZhky5%IG0jucmag2tN)vtCmpiWTrw6sM#$RHM z=g5`uo)JdJD#b76Ktkh(2M5?|vvg4vuX`blMmtOjCDF zT(2IX1pOXf3*CMYsnuDOPS0Pg*V;qgxB8S6YxYQ0$~Z=d4G485+Ei+6!09`7ndirWmQ8?+b1D=a`VNopPpub@wy(Ee*ABW6L5V=i7 z5w6nsbj?tmup~Fa`5p>zFuwGIMXnAB!izvi{nqiJ6f@-H5AG3m_qko1DmC0f1{tv9 zSq6i?$K&lB%i>sgYe*BK&G;ggsR_pS5g@8wO`e@dAN@+U^60%%&1qL zBV!-Mi)KC3*1MKl0hn}z?-7D=F~s0dsQWa_bPpUSdh{<9Gl_Q$4|Lf(7~>1@6mO)~ zXi$#vEY>>OzI=AWH53vc)uUC)$>&hmIS=f(IxxrnVK2hnCD<4Ka?H^LI=xrdHwTa)<6tyMb3CzQ`OM44C#gRd4l(2?{?oUN~9_) z7j~vrh(JGRJBAF~^2qP4K%DlNQhH?E6(hV4h}I zkH~|Q4rQ!6K?(Dim4=-srEWsO;m$0 zAJ*mCKsmn8vLRh%zHIx-*d=yCF^+|99k`QAXjw*+IIhFxb$4uXtv)5#n@Ju(%m~dZ z&M}}ug?3x0yF^Wd!y^>-`GdJbA7lvigzWJn5k|5|ZtJ3;N zt!E%?Apb8Mz1{P6Ee!gHum81@Ibvt`Pr;sm84g7brtR0BGoO^7`i+;KaK$H z1inP$97SpPN1`&HRFi44A)0gK;`BXwj4RZVT6J1r2Wx`c7en(;CCI)B2%GENozFUZ z@%8ov-p8Ecdx~{LT#Rg$miX;f!#FOGOEyS8yMitk8vf}Y7$?XT(=*ZHt`wxi^#13b zUq43Snw)ZfpK|sJcLZ+VkvYds07-v+Kuf>h_STyhoD0Y4=`E2-bxOB?w|Lu9X_dn-g5P%;i2OF{jXNtaqgdWBaxB1* zvl-vu#UmS{s)nn+9lTny*$aWi23^RFK`ROyuk z@gqmMA`#EOOun_jYEtM#qzjhvz`(4uy%X%UzYlP`v1kpV+i&-0NnR+G;SBnf=q%9j z3N^)2W+1qE2Drc-I=qMr{F7)#__QNrjfU~kD07OiPEjquH2TNs8>o`U=j)p*;9mV%x>98WW`F-(8+bWC96*NO5UAl;!~{)cBod@n8ISz*g#Cs29-l zYf_8A*7IvgB!HBl=_cx{m7tl;mrt@SO_Ha^5+OuknvL77r`snXqB?bV^6jR~&>VSo zo@Sk9RlhYQHph8+{mse$nSbB`Pih#~B-%lptiU<6ip-vg2?m(bputx3;2!AL6%x>- z{<07JqgVLB%{{=pUo1;hN$uiIloTpV!sZf)TTcA$oX!YXD?^+m1$Vy8-?fK0~6^n!nJoO-Gpjd zlU}VE(N4D7AiMy)Qf1q`;bDd)8^l~J(S|<)ZmcT}U-QJGYm8zL@Lxo};T-QW(5}v3 z82T|>`{3Ni3Y6ZtD=D60PIpnPzV8J$=G8;Ec!$5cg6z{$$mc^`FU;4F$FT&fVIxkYEnNG?VpOd7 z{1ZIQp?`{T&gDMR@di%kf+CP3)p?B>l_^5jDnX?5XBX0gBk!1c)4=uH)E2<3m-j>- zA{^jaM&7w1v6;(&>d*`Ga*8d#?})O#g*KI8zIT1M?9$Rs&Y&V}0Ov+140yjdXO$y7 zORy_XEX{)PO0~EkG{Jw1Pa8xBKwEqvyQKkPnP<%)Et5`h{;Do4KcOaPdO7px$dEc#Dgs1mMB@W!E#{7ezf)e^tm1=7~{nrU$LIcn83EmZvG6xV~ z^W9&pZx)|!~O?fS{1W=O!MBEdDi2{ zTrWG>9%f7hL&c9N2G|I(PhudIp&nIZ+dsxQRzgFP0h zZduOqggCc6hu3nEUXc!H)Bjx_PBqJ_^og7y=@xF2DblG(FU(V9Me_KBEzHDs06~Mc z&;a!kXCzL=N~Nh)Ln=5Q)siGTbO+|%#KLDNgA)0+d$9FUeu-rJoa#EQ8_6paXK-k3 zuWF@d3*@gE_Ls?!ljb>5v3XhzqFR9EI4* zOlsg#O1lfcYc)O|{e~|Mt{F*ZhtG5_IlrSI_qlhtZ{|=wJ3J{j!XkKY;#v=rMjzg-hx;M{tgZ+>b^76?7Tu7~#L5(YJJ_Sqd&wZO%&F5ON5 zXPeBddxT!`TmqcK-oTV80^_lm=xMQ-042DN+c)1+Cx zTJl*>HEIntT?a_}6v&D3kxIQ`5*d(J_ zk93Y>@Z@p>s{i`*`p_E9uuiGUjF{-c4wrK*$AoPJIpUH(Fr169kSa5;z<&QXax|Nd zcR2TiQZ&cEE$Y-W^99)woI|TYU7RurfP`RDtqqniKaH}zmu6K-w)B^6q}~q{piLBq z;yxDTDwKmRW3XAX^uznH>`Db=geL5I@dI9aPx%+Rm!Ly%n+V$3E6Y4=a5>Bv^gGU@ z+UYydm18!3gWrEEwm7hxC4&O*N>*Xr9fLVZF8 zj47CaLPR@M)C^zSud*Td zZ`yB}yP&jft@ZN(cAMCuE`E#>#?}n%YB^Ok&vCLtgG6~UxU$NY^AMb2DW8*j)RFQC z^6{2g=BF)&zDqXMQQ}0#5*e(KF!2{}Tj*y03=7lCY~QB%`~vN=BHTPPlDr;a7Ww3# z9*Ccq`t;M=-Qm_bMd=@%U!R+abvMWl&I45o@Uo?ij|hKy9Xg?u;{?_E1bS?p?j0~) z5v{Sfg%pWqDDBWXRQ3n)1W<0(aUpRZ(igLs}LSPz{W{nPTTY}{z~!IYvI0v z17ba=CyVtYN=oHon|}$J;q@3ykjgbP4S7c`&Es1>$Irz#%~L|TPau$9>+d~2q%elpxrfvC=m;C(wt*nxW=x$NSb|pK_qoe&!eXe$>l6;;{)S=9s z#<(7aOJ3zR{gzX%QJAD|hq`PoHMY@b#kim>qt3W~cZf#ip^v~jbM{(bIKLIX=d8N> zf8qI>Rxc0*#aXIW$XCR+>NeHEH1F7+y^wB6bqUXU{%V^S9}LcS8S|Gg>3tba<5Xc&q;K9 z+4nfl;I*po31(}v?O)KLF)HtI=eUT~+GowMOVy*eU1Qey3-yZrPBM=(3^orMaZq~d z>&9QPbBE)Vu#q%r%@r7FjY><4%8nQE^Nb+|RN0jDQ4zvGkUJ=;wmFeG?(F!^#qn8n znlZukQEKhojXvon`OfL+VwVsULyw+tDeTo6Ly@RZp>K|IQ?Qj~b zj3Fh(oqCxRCwCO4_}Pc~@D{BG=UZfu!f(_Q|8% z1bY={vn!xcN4Z&IPigsuY#+nE-=<=nL$Tx^4*FJKEmP~;Y4Ak>W6r1IPGQ_{4LMV# z=Ma3?WQSv`vUZ=qOTezilz-8qF3tu2-1i};G_*CE^9&DqGytGDKY&TmOVu zqm5}sSs|6<9OaSY)czn{Y=~C;6KUj<3wwdv_CW7jxkgY+k`5Op&9f<&OP5C!{6|i| zchuErdb6wK=hEb@s`T`vs>Hr-5BJ&sQ%VUq@&vT}YM~Fdg^xba*AdlQ^@ugkL z*=Ion_3`iM6Uqv#Zh^A-rFh1;=0jT;4h^xG*1(s)5;{fQN^Vz|d=#47$0t;dTlPD5 z{RFTae*Bo@Gx0Hi6Z4#+42m+mGF(reBWuJ)%M;REz%3*L@kOSMdno3c0>5F^6P`k) zD@wLA5F*hj2b9MFuLKOd1^^Y_3}g1~M7uW0s|Re~wQ@d-iu5%)Wb+>pRBc9AW}#HCc6a8|_iD-Qn9lw0>5t;(#u!qF;O3pPe!6y1%%^@lW1hg}a6pVIYDWBYOrH zVf5Y{_5s~>+d6nl#tRq`!{EqG0}#!aKkiCZQyBe&FM1EpEqKCcQ`RNM)LfqL+24Y9{6wfCUO-i<1L zG^`7*;8*Ijf>o`;j7UbH48k3eO~R%h$Pz%}ga_Q>+^hqx3>wXI^!t!PWT*U}gvY#s zCV56($fG@D`5dNgGBMPQ;sN0+Sz^4el@|N-bo&iYtPe2&rr6SR*+rt_&!Y4S6sI?1 zL_PnCAA6w^1{M31f#uIExF3sbNM~@0)3l@S`I}wW0QhEb-miIi{|{iHR&4+<-TnyO ziwaPSx%ET=u?Kz5^pA3r+@s!z*YP!gkxO`xt1HxtH@rvuE84Fix?Td$l5-0E@Q(tE zCWa+;+XGP^X!H008LM@nW zzN2Z5Q`#AnTNs{Wk$n*N+pGZAMXWQ313Y3$_X7cwpD!SrQw_n`-ot69PS_A<7#vNy z%{ECrk7(x(9F$P9EzK$20M`bW<)&3C)qH~veoxX@r!vJRO@+{Q!EZO^|4>Y+T;ej$ z)rZtz+4XCWy%lUyuW3N6NvoQ0LmhqRZV)eWi3TfdrNRQd{BT}As9R`LcW_BoY-LiZ z38mxR(iSqEab_3i3@6GH9GGH+e1dRdk;I5*$0ElUOsco{%jFp&9a$-lsL?zQdeL5av5<>C)B*E+^T-3^F;+I zH#l${5P)g>@i0jZZJYlK_SG(jSu@#0xSQ8>jTOi7mACB(ju4_U>?7A0 zxk!HZ>0OIQ8}29|aT=pd>xUusAcW(qNi4U{iDXGVPajpXQJI5H(r}>A;&4+2Yo78 zit2b9k95X5t@hmaRI<=J4|+T=5)yCRW|iI+KxU1GOi`YVMwP~I#G@Ad8G51G0wH>R z0LnWms9#_C`~pXQg^%qr)`)z=J)_m00wM?UTE&Rw=(iNMy7=l(6!$WIsWmaRuJ}-w z$LxRPZu||vH|y%!fT%KOapQhhM+nsal3b%rg*XcMK5pdD=$a+Y^ZG9j(EaRTNeIG} zYO~5ROCcBp`xsVB4Ep*xm#VTgYr*cofmIqdF_j7|Q+EW?f}3pCDY$JPjQ+YW-9&8j zQRa4{n_c}ixgQS4BWcF=;4Y0d{eM(@p9BF^{ZO0ahS>cB+2T*aeMm#MB8yIPrM1x#(K$4}C!^g$paoaGwyFin|_@6WCRqim@phd&b z?da3P__Jh@r9Df!40PnKf!zk#WxBmgk`061?0*F$3a+CTY+7dYZtmh3;j1uMqf%+h zbAac3*Q@#yilWyo?%O0)yaIm2=v_JoGr|95{V0#X6#&kbN`612OawL(Z5c>?_dd0qm2m^Ii#7hjCRiEr+`nD@#mo4=WY;x zCCtb^Tde&JmXiT(4O%`q@84E4*hgY*Z09|&+?~Vm^%}6%E3(=1m1#o0B-OzpAMBN2 zkzmKt7u3o6{4`ng%PbMz@l}D;9vtgd<9BAquvJy)jEB@QbvajO8*dwL1U4cDCITqk z?a<}fdV>$} z{TT)bTSL}mT8)ZdIm|0wL4g){ZJ^4G97vRB_#kR`2!9BF_8KrUoIObYp;i5T?#BaK znmokL-61U}Ec@NVJSj0U^BF}qV1pcNVyGKr##^c-#3L!VcLBqY-TY!%7w4toG%INf-NY`^;9no07&Fl>_<{DiUHCIyAc zvq{(XQb@JHD?#QI5fmDd8C}mRd^+3$de58+IeMe6z83spTU;LDniFdTgo+5ady5EJ z!4{+dJc>g)ZEDa9|y!5Op{!TC(TU;JT0zT9oUi-HTdxkJ*u8!+7l4`cg+=0E` z@-Ei6Kf%h@9npP%FHjx>@fd|`3-VP9OoL}Ozdk|6@c*DXzO}82(WDOPmf&sm?OY7$ zDAV=oLuv#j)O7`zTz)w`_9$f=@y_FXG74>{`6X1x_oy}y&ZK`lZ;^X20C_l3z*4Z*@`>s?Vs!V%!7LjaP>GULkFum@!3G&yWU`d?QbA zLQ@fLKZx*-<;g!$hv$8pP6#g!r|^f1EQN8}Jcb{3MMNak=68#K`6to&|9)=1cI5pN z%eo{p5c=|CpJ&P3#;*A!Nf*-0tufYEqxp+?mTAkCq1$NQ%L7*N{>AWx&;ltD(^HF$ zZi8_GHgP7(2eI@p^98Z-F7~^ZxtHnR{QdgZ7dQ5edoiKvw9q!V#5Uaz2P_Ap-MYf- z0laha>4bg)ya9|!AZu>DY~3B^;Ie25?x}CiEd=vinp@l9CXGzVuTGAM*!dXDgDguB?vRCkm+8S)TQB+POsK3xw}kuVHyU5jYv;77y=w2 zG;0r?n!Cc5;LxN&cgQR^AW>vq8HaPf_|a&-0B2ud`X~AB3pvRo6qVl5?xSEU%Qf<#IF zdkZvxZ8F#%+66A~tjGEb+=Z)+7ZHQ*M}R(GzzFz3KQhA}^duXAN|V@9@Dg(vjATlb zdz$Ir=J@vObTzzJCo@WhX-I;vNCD%oRwddL7?< z6o1rfL%q^zs`B?J*GOb}VcrS14{QPg_L0}9nxz-F zL}}9b0`rUhzz6f&T2^BvF?xCN2cMdSa($Nn0W71R!M5?@qH?}J{K}HxeJ6-?g}z+b z-0puy3o<94af|HIM@@iaL=%wwPx^A}!4faC{INwF#csd9Dxtsx#rq>S9mHGodNH}Y zcSmrYk)KoeXZSnjdJ0mJhZwU8qs7yOPHzFt3q|xsFisoRCyfSTvmYZFZ0J7_M zk&aME%69KTeSbrCd^T(IYloO*Tlj0cqnXcZ#aNFF-XZ-Og>&vT#=eKt$09D2-=aY~ zDAi7`iE)%>MR*g(f1zC^86mVBza2j#pX~04#?bQy+x+VLsia5Wq`>z4bbSl}=lzxw zoGSvcL5)Rr@r?^-m=TTZn}TwcS>kktWVX03KUnp7Uaw#GlGMAm*h($hu+C{YapCjL zjT=bGwN;Q!qgtDf?6FC?Ky5+1MOu&y?YBH>n-}1S>tOt0ny^WIc>Qt(+)>1iFqNC} zxY68w3P>ZmTp^7qhdFveY?sE|z5C{#+{?kD+NyS{FB^ezH3dhzv(>Etgt`Zh+rBfZ zqa>ND$dqll!0P4s;O>Y=XuuD5hi|}wX`np1_MP6QnGyaI=ltf5tTY7f!G~Te%IEwn zvK`B!I_-!kv<*6ix4_OYI;6XP3Gqe#0tt0mA4>r>^v74?yhB_4+dLwGrjcc6pjpNk zmjXA+n+gT~Q7=b|&5NdQi3u^8`Ja*@2bh|k@RXQUA7P;Q73*-I` z6QTyBSp_&(rr22p;calT=w-(j-HGBApBv4dM^N1GXxhI#6lNwABZn23*|>FRO(~?g zx*hONGW{S>LCI0*`sUUnhHO>72v2^d8dJARef+EF+bYd*>+JwODg$WTeW=*)(nin7 zi|w}P`nK32ljtFjR+?OC53UR55f6TnpM!ZUR$|39Ky}!oc59bt1$0RAw!|6Z%}<4O za>yb&R;P1`0$l{A81AXpXK2QbPX<9D@(QG!4U7ewUEi!*5@kw}_Cl+g)a=0rZ29=H zBh(vLe8g$;~*D${6GAD&WbY%$>7>@ao71cg(pUEzX!r8sDm zw@9FTpXuc~cQ7yW?_YWQF9rSZO`77+e%-xxgif=lFy4aEXjUeMH&rK_e;=P`{nro; z`FeAS@t~i2fx2Gqu!qF52zNyHW5iN|#wiUoezPmdEd-x9>Tw>NRj?zIZQdA}Q~}rf zA~=AM+hc}pK0xL3_!H1u5$l%mE5aCJKfws20f3NTxkX1*T$>=mSCrF;^cx9WBi{IW zQ@Gy{Tw`h1VfV_2KxNwK`m?Af5ZVtM5Y&E%vvUgm2kIl&Y<$Lp?gyyP-z2UkM{{>d z;6BeL6JG0XgptV%q(G$8#Ow%$Lb*$R?ON^t)T?hYr@CN#T7 z1zY&(xgPM!U&vAR8%8P(t*)Dy^3%0+jxLv>n`hLg!b@gY>3}uT8x^&dpcO2S$S|HW zdd7o!=?XYXDR$1s4=?~~hu^kpNNY)SyzJC7p>V+U=3VxeTRXG=%QLMdyXbfc)4ncj zxw-5IKj&GBOp=q->hJ=$otTNcE(GgD7{s?J#oAtwHM;iu@Q)DwbA^6zxjLxHDBQR< z?G@x5;1vNr^4^ho0wT70A6y~1s?(kC&5!o20eGoqn5EN=7*zR(QtA}H&f$SLu}#Dc z(Ga&LSx1)y1;1pNK%&~#r{>tQu8ClkmSsn?<2jwI=j7b4MT?$+a z3f-a2!0sQ~6xz@}WqGencZ@4j0ulZp-hDiXE3C4d()_{R5I(^@&`({#(aoTH_SYNt zD_q2j%o@o@9yR4T)u@N$LFNPYv|wwo2BBQ?C*4azg}d%uN*2{CYeu=TyOMRXW$Z zX(fkU{IV>{OsW|=0lx+O(Qby^%#5zX6lCKhN%aDvwQ5;L$Zssq zSeH1cKG6;L?@4An(!(rC7DDXfED#>@-L5el1K%P@)W33pig$sv87qTH&5fSpf{tKI46a-NeP(W5N7maCcqFExH zXrLyfhN=XGsWt|E`uCZP0yDS}Do_b=`(opDh~+hVwA<@BXR~R#X*zFQV75%0VN}Vy zmPPR<9jA#=7s3iJh*#-=C#Yp#j@9@E^n;zWI3usGgNtde*qd zs$Vg<33;{$YVQ`?hMA+zet;B(MSd&T;$f*e%4QH2fsj|Hf4zv`ChqCgvO*>Nip!jC ziPqSew~9o~76y++iY2>8Jiwl4c}li!o?nTpJd|LcrS9cTZs~qzx%no~a|JqK;zoy0 zxeaox(T@tGTGZ;qdMn-DO?+&Wuas>-G=rPLA_})3_M&XjR3Lr%xHRgUrTB)AFfsjG ze;Qz+^DupBl-w8L6Cw#9dt#Wt{AcaMAivW3}Sn}ge&mcCVk2< zBekf(VV?sX^A(3#BB1wcrLuJlC%EWeTfIH}LyXUIzfAifb|{fn8!6ZDGU31ayN9nD z6u?M`drI|jzFY9{hlXAb@z5uHlou-d?O!UtRAcVD{Fd9y@Sblv)UR@R85vS=x7w4h zP%lM$yZGrjMp`gIKp9G_^RV}-IZyeC#-%bE6E({T6BE>HT+b(p4PgcnxPx8`g6K}R znJzjpDsI}DIL0fb(|n_TjBUbbs!rf2_no*|hAH?Bf4c&_N{g&7*opvoXoP{~PTpaj zxpv}pz_s0s=*3J82C)!HhE%F|s6}`WOU<}@VI-Bn$0&g4eV}Ea%ljx+kIG7|?TG96 zo-UAE@a^^7IQH!r+w^ag-1Vl8X^4NFK_%8<_A~Fnbl2~ZZCI%9l5ANeLq5}fj+WTG1l$t?^FNiKho75i3tn0Gw>bgoU(8%KJfBKxpz6(FK20=;ukiFPw9 z7a-%yxGdKnWHY{#p$Zq?py;G%YFW9vR`(7Osi)Px2z7_r%oC~Sm9CI%QichcWDQ9_ zf2@8XYzaR63L+NAB3>yHS6RX}GJv_K{en7YnV3PvOFcc(>fOuKY_-bNq<9DW#K0F* zX|rs$`Mw5@k#ZJ_k!ePp)Hu{8DK2*XUb$7EjG_fu<6E&T7G}(elTy!Y;5kd<@pyav zYFJE!x+)-Duhq*6HDvqeoPCdP5wFl$*?zS3B#rx3V3~IKZ6I8$9y%_5IA34I%FwKQ zvrj&_``}SK^U5hOdb-pHbM2CakzdyRA8O093!T<+g0fdyc()(VQ3CMhZGFCAB=rgo znfoT#rCY%z`Rci=XJqR~uH|Xk3AS|WE0BDl0XF2N_dePplsFzBNX^3*>*GqlSSi!G zK{CfUr`SG7^ZjmOb3MlP5s&(Abr~wgA3CYA51pWB!}&N)gm*ed(@v>EcaH}Wi;=HU znv+4vXp72gA3U;B1dy8Pm+15K3^d%N-A8}?_6(JJM7J)wEfbTi_DG8~9woQWht0Le zXB;O}jc%{gA=xy=NlX1l+)Jru#!d`p4_%H$$<~Z46vmeq6Xz(+^D@zYFcf>InP<|a z-m0|^B3vAOBQb?eH{0$I3m#pRq*%$UU14OW=iRXIvlj(1&?}BK` zYss9BC>mz4(nPm(>%7U*&}UHRX!kcOU(Y@u&8t%3y)_XtF4Q#Nx3~(VMw2ALS#Gqu zT^ZIy0ff5LIz_t2x*@+8*{V2>jNk8qXS%O)oFVDCP!5U|hxhn%J73NsD*jQ-Ci|4g zEEH}Zxq%pN*VO%^+#P*^^?LZNpA5x500`M3l&x0m>tV%qo}}-;%E?I0fs5wvpXiCr}%e2cdky`jFG7a;1bQT1uBi0$Ja22ym#~)7;ohK`*(DV0_mr~zhA>}y2d9~nrxe$vhHE4Ba2LA?cEWk8?*cb()%fG|-VfMB)I-)`6} zZKPL)Z39)RQMvU!4cQAl**DyR(B3;-!c40hl2(%IOZMSsa}=`lgJ>u$=LBmFerh-W zCTYH{cUD5ct4(}Ls)1y9P1+8T#L*?fLM%&k(SX)rN?iBLg8-ZFJSO+7&LrL zD-C?ic2A}LevXMA1VO^DC33?`GpC#kPJ!wL9M3E|4$;)x-&L%>NRM$av-ze2tVQcq|ih zZ4~Vqq}_rAm=NQU5cl(O(7JDeBHR_ZUi0iRisrLfV{Jr=rDQ_BkYlZt!+mqhjI}d& zH`mGfo$_D@PFvM{;;b*>b^q)`6p7YJ3m0}P_pI z%TM1pPP5d%i4nh=q_2{GU8zOiE%yf+1JNV}0fH4v6>EP6IOtT2rWzP{1~^aAuCd{g z6+V7ou9bT_Y_6BqEx!3HHIM$-V!mA9Jxmj6a)k+Y?ge~bkMmWw{>JgN3=FSQY>c;0 zj@ipgy@dFHMnyGx5#p!f;F^ZBh3kMZT8>7PeW+%K5b)dy7wJ^_dCb?(GvHG(v4~gs zBqisXUGNr|=P)Y3BGV$<0VBUrVyewdpL9-Ml&PtjD;I?@p_&VH5ZvzLgm2?ts}*a7 zdRDg>buWYqR5Izy0aU#%C10BmjBj;W*7vp?S(vR?3@#Z*;4f zXE(Fm)ibq&F)4VvE9NNGB=Z{%tKP6s{e5SMQa_d9yixl0J$a+)5i z*OpPKicF@;2qz0XC8?Mp8EKQ|&PR#Vurc zDO$;|;A#R!TBo`ty~0XEkgcfWt&kGbiR6#>&| z4Ogqc{u-s8y$joohe8LL+A}2DNj(=i^3^)SPU01*-fzYRNjo?Xwz5ym^$<@8D}wzz zVZGf_5^yo&?z0XNDAcVZZjJPp@2gaB)EO#2eM`K>!;$?h_0ulV34RoZNU)!&XT|%@ zRzb@N{YJ6?+{oW49%vc*>DjQwVh~0>gAJVso=cQ|qTld6+j|XHu=PuiQLRzEeM+BX z3*HYgH0l2n_(6A*55U;WkP0*J)un2CmZ9iyn&j^YqkNKCw8hNG#*Byk`A71lf7Dp6 zLEl*Ietw@T^~m3bX039$M?GH`Gnkn+3#yl&8|iype+^rs01(j* zO7RctJ${#H$9$2I7BKR9J8chi)M|D&SHChioIZ*TN^Q|UzxDG#6s}-l>exKIoW(<8 znzbP9uQApo0U(OzF|X&x#miXv5D2L6GC42d)qDsLM2g!^X9|$Lx}~=(2(xH65q)0c z&QZWm6V6=~PSaj}*_NXMq@BL$PeTl&>ju3?H26T+gT^V)X$H?{Fb`m@1blk9?Bru@ z*7<@=PHV~i&+zfv|1f;~KX;de{2w1%6aWA2l4_cpAjMvJh^vQ(2WOH>P=FMtN>Gq1 zb*w9A5IV#h$XjxAlhCr>Dc6b;gmy1B@&(o-W6*U9$I_4Lqge$Gfrq}vp}@|#)F?tT z#B;Mg3J9n-U>j4R8sxD{8e%od!ZM$yxs!$de1N@o8ztqt00an%W}8v2O7Xfxj?7!8 zlxlpCdZrOV1QtIMUBN$o;I@Y|3IaBVgF-xmJOk{}K1)OYhI=JcSL={z`h{AhElii@ zGU`9)JW=U8MR19$SJWi-2(e06DVq>9Mw0LxU0dLMRtmR9{H`*XVD)~C@eNccZ$Mvk zF8#pT+^rmmbZvND}F(}k7EFZtAwveH~70oaa)u$cRB9aK0kzb2ha3t-Yxfd z!?;8JKFuBZ-eIlG$b22XS^j-@kwG_Z5l~nuQl% z(3XF7;NJXWXvNcaih7M=>@#QBFSxtEw5qE#Ei1@kAtxBXkK5E~y*l8QoFpL0n2GVk zE3w7bitPF)GAW;7<@VmW^7r2@iRMS6e@8aQx{G+Wc|XL-`4E3ju~o}xS8*Q^e#(@t zAkD#A7NT$XWxkaMWLxg|yt%^X6xyM;L6hkVh-eSXEPwj`*eEkNI99F*bmAK7l1{ME zt9xNMKRJY;Sb4&>=N0z(?9@xObPhe~{1w_I?q1^+#}gu7c}x0$4?%|ev-EjJ{AYh{`sFg+R9 z+u+}?jnoaIEvte%PPiTWa$NIl$`x1tUITG0%u~AA2^BM4v6Tc6ROdI6Q6|+YAuy0} znNF!qwY4y|`04~h3xH`AYgS%cPj^!(9N6Zc%rP!_TI4qg4RR47RuycL?pEa*=*Qk> zg1HdA$62r~w(BHeS_U+h`q5=+ryGH&o3FxgQtzg&d(C1lk41zVP|X-9p_z_lS6gv?#EQkr+0|D{x@fhw*0>anQ4jA~l6=dx_lN z`Xxhy8`3BP!>q!L<=YpA5nfNO;3i%rIW!}*Uvr{|{SJFSQSOKelj zR*#<3AbsBSG8TP!w!_(T-S?Zkfr0SOVfmP^fVDZHI3xexv*aL=738JXzhttq9 z>~KZLV7)3d{y=9@S5}C)PFCq)6{7PJHFsDTu9oC*MzeoFEAZV*NWl0C_gF{VBkFp` z{f5w3Yw$0vsUd1*b@I*gTP6KQj||h`ji`0n4|&T3%Pjkh;bAzBGiWQ{Q23})eUY_O z+_xZ)^OZ}o+b3gUavQzi8;|i4EUP{}7bL5)m2llq!k*O<=(V2&!jY;oUjt1ue-gv zW$iH-X9-OCpdt+mg}U-g+#wO*Vx1>gB0G7fFuxL=-O)_5w+%m(9CCUXFlHxzKd{yWK7N4P&!9wOZu$oBM;nIo4C{`mW7~N8)F&8_ z9-T7DnOy%y5np4C{h1EkE+P`bi&=?bcjv|$8*Kh1)`TPy^hwY6=SLk)G-8Kx;@Eb5 z4`6`*uZRL0w`539{O>&nkTONbkes_O&FYVwBZ_YjopR=nPN>LuZ=1{uth)4Yx2#(0 zR1tm`otnVo+ddi>3Y2>CW4d9+d|m_y*;A5dd+57x-Z#om74FD zkv47!0$VfVx~?mGKH)bW*k!kZbLQ~fKm$Yhc@M`Ry;*lhWLJRIcRA*eRnd)#bl*qb zZ;^nYEPJl8Mlpr%l3d1xEz%QA3v7zn3JpC*P+Ub#S$eS&nq{bCt&;E`1>3P0|<@Ed} zo3bQDjm7TY!#6X{GS(4ThlfJ6 zvqKsz+%9&6bD`3`NoTxA;^^eQpL0*dOfMhDs(qzNLbc9s(+ZPEiWI-GZ!NY~wK00~ zNMD?S(G_j+I}<&If?qQ~mAi!pI*{FmxH3snDgj<(EA3*k4IjskuuYC0Ekv(l?02Lc z>SLrjtSFlk^PB5-pBpd(^?5rNU&9$1()PREfSs#O30OD(`zF-@yG*|db*RS$uI9T% zmQ`H%Pqh%yg6#5on@E|{*>SF)AGPujF`~qbOc&Ru*=9t0wix;}zmoFmGz%qo@GZG} z(+dns|AgZoTzvgXD3*~E9A^t|V%&7h;68~~rXAGi0-u8(+x&5W3;RK>TTAwe^{-BP z4Q2qPFyYzl?)C_iwN?aW<(s$o46?F-DDqH!$zmhL4RobAyMG5I-uwwRBjI?7PU(Tx?0 z9s;@9CnLRh$1(_m`m%lL?M0GLjxP|VMUGP@DD>_rzJ9d>oZG|sf24g=aAa-Qc5KYV zHYT=h+jcU^#CFHV#F^MOC$=ZHZ95%){rpG&`MYbM?5eKn+Pv4g)`j<5&N17pNMp8{ zY8n1fjYi|d-8a~L_mW$wGS!sAv{7vJd*c;ik8lgx6``d&EM7hISN^S2gH*ei)>hR} z75Dm#Lyv6HSxF$o*(q{6hwp3)6Znti{e_eUFI{HoGCp-S<9}boVrUGU<;Zt*McUM& z+2zO3ys|~s$Kt%+ZHU`=LmK6)!+Q?h3Md|pGRg0kmr?I0r@8hqT|M$Y7;RS|}I$Z5QBK zC<^Y91ZYw=OLdekQdgl{=3|z+Pg}yR66L}1-Lik$t<5=3kH+vBb=R5N)^wt1-*xyL zh2;Z09+YcVOY+VB5Rf`LnP#`$nP-r#i)wiEI3&y`UY;@`f&oX}MU!>hc?@kN#*?fH zFV?83QHk)bmey-p{1NIJ={+tu$MO^%uc?xtA3%M&Mban{WJe~ItIshu$V0nz)KluP z`-ci7)1w~NyL+O@jAN^ht3zf|?*9CM(kU`53v_-O+NPj1dV@mVSFYGv_lfWeS!8)f z8>6CH!^&#@f+2EvCJ>qz?|iv`25+a&=4es(JCl(13@)O9$-g3{%3>(`HBWIynNH1= zi>ZHhn3R^-jN_wHa#+i{9oW|sKDMk`7Tx+clusn9@I2`cxxx|(5~Ejj0<|ICmV8}5 zn?P@#sxda@0{SkusDA#(e(c9H%5AHVhCgRSaGw<}!ve*&Qd1G35gE$U+OS_hEhg3# zC?yk7wEgu~v)cr^IzHTTlhGQtR8+ahC9_B~%q7`Lkq`QEW&#$eTeMBQFn0tuO1jb% z2t<4vC3Qb-_omrj@SKqLDP-SY(BlsI#gzLCD&*H<;x)0Rbm$r#h_a;F4Z%8Ke3FK> z)Q506;+XlfIgy=VBS~)1s{wBjoHUlbnd|^@|SCd>g_wrF!34nC0pZL_`nS;76(|2x16>8JDy;wcNykNmfe1toKecXN8Om4AY9+mnfxtr4* z5=;iIiWpbgbV=4XaK+k+AmhKY{lncfWAMkH_}Mo4)E`jrnwPKx;-RJs)O}lck$e;} zm&+D!KdRJ@nHKMlMlL>oHV0TIOLE_iY|t6k9Rs>U&cCda4fCBd1m5K;K(P&IZxe<0 zQs>t@{9l5a0kB8#)1#=6Wq#AbQEl9OC9_T?*TmC11ZbYHz>JVz{HC-i`T6XCPj5=C zRXQ(tJw~}sS^f!r7zCn%w6_txj|(Eh`$2T}1+^oVcI(S`%sEK;gJ=!3N3vV0RFxs; z5y!&@{%k?}Qsxs_U#Sw|$e4}_ z|I!$(+S98->${R31qC;L(U5O@a7EJC!aC>yg9`5v-=cwlzdV<1*uzBj@egC3>=Fim ztC0!yI)Oj$f1r{#(_dZjXx<*#1)*-Ytu;OZ&z0a64WF3ti-HH3X88O5vB=Gv5r=h9 z>rbkY@WDBu)uz;#{*mNmnTT-g4&KR1v@gzZxpj{DnM}E6XX5c{kKVs*Py>as&4P5* zlF76$a4H)mlh7_seqoS>d8LS2tA?{S21J~MSkBw7)sk`au>2{dqbt!4++$dbYWAl| z3$y?fRa!)ei^U2dC9QZQN247w$N$lF#-!lmx?yOz^)7Fzxq7`-Y}2f_c3_#dF^A}c z<9duHc)z;Zwg?(1&Adg@rGBgFP%L)cYA;ix#_c(*X=JF#k2bjopd-p-$?^r^!qw98YUNP(B-<4qjFMFu)v(u|2G)o(_5UC%rbF(!@8fZ+oMY0|xJxLb{ z)l|B0P8el?%#pf3g+uH2Qgd6=q!K?LTz(wq4S6I=G&ae^p^~Bj<{tZYTj8l!5Rld3 zv-5Hb#CZ{1mwLwM0D4VmcU-b{h?fUn;+zJVJfeldyy%OhRku{|0E6`AKaasE}h zWE;cm-NE>F=UD00*92#m$W!<;6oz@Fns}yI@3%2vXcd|uN+=s2bAbE5F{W{eipWaI zO1P&rj;oMwmteQ1uxxW%Gz3pF3|8p^YilHsNT1}%wa@fygPVfjXEc)wjGCnfIE3pI z4+=5RcW$XAmNlvq9?_OVBpt%jwgVSj@RAiAhJsF5t#It*8V$zp5Op3U)*+QIAQamE zYPdWa<5ZH8_i&E-Z&c{uIpg>5Pm;Lw*-<@Ge-~x(%&E$JySQ#Q`-kvYABl*d*86rG zzRgT=hx>nl!LtlnX1s${XLiUogxeAR!t_8KS!S3UUr&0x`X1#OYl3E#X2QDf7bO!% zbSN4HzvW)Th~sGKW;C$-DeE&c>pe+*Huf?Y;QuMW5W9;PgO=?fv;3TyX+p>`9w#_Q~f*r-j2dr;fw5U)2D1&?Hq) z&_=7HK=)~hXU7Crk`f-}69RV6B&Y3;6z?=g&dBX&KhMoIu!8a^^0FxGV4ULS@m;01s*PyShmX4L4}@#>s`iR`4K)v=sFay zocc(*)z8Yq&kuYs5Bx*%j`zi_>3R*z*wofqWwVg0rvwksb=hA=%|96N`B^5_lZ;0J zl9g?D^Xn7^gpeAB@)?v^5@0&WbG$wSuT@jTHq75_F(d8Q%XsTO+H_u}a3%{b(R^dI z((e{y$Za>DrGqb8rL%p}q0Th&uP)H#k|zck`lY$3Sb8u&{oWPw`$=E%a1XhM;Q?yR z;`O4Pf1H6BONW_GN0_WUoK5<<<2>P(ndFD&9h_`7(I58uZr)2;QJ4vB`D zyA*jB+QPY%U}JE5FDvC1Xn`05NHmiW<8A7jw{n5BVT+3Je;du%gWvHF1TFu{1)@BO zTy^`DxvT6M)VC-fW07WAj8}V{8ZT^bk-oEnXcpcqW6V-p^rUms^Bs|5yaV&aGk+eZ z$Qa+*=GvUtjg?@n_ZbQ&E)jDM-&=0#IO2K0EqW@UU@Fes>Lh! za4X`U&o{hu18X4rX@>p2y=)1-hA!WnXqhq`IkB1ZdG^^8bOpkI3?S2?6=5tyDQSXGN}43awwOed}_5uCx8&K?yJnWS+CTO{lcyA!{+mZ8{=BP zmF)V9Y4_zZ5jOlru?d6Vv2=SpeX_*V#73yqqXqJ3DLcMsESjLVI$F`rS421MN zccP7|ka2)q2QE4O;SpfD-WrLRWr!aSNb77b3{8@41~JMb?ltl7hj>IpJsjiaJmHx| zzZETU{eQvmtx96rP_H#H2L^;k)IUB43Z&VbCTvhV+d^ElgogwOn{ zeYQ`%Gnj$_vd9Yk3P_Wv+foW!%%wfPkuRL5H(u!x*@IZ+m$@`20nI!;Sj{=Pf zQ_`KsHn0|uW`bd@^ta-Ef^2FTJ?owtg0PF9FbMa0F`64Da%mGXYEfDqSUU*F#AS8>=@RreJB^=!TjqBwb{2CuegI6 zcxF`b?dL_I6X%#WDdQ=YK|j3rqO+#8UzU=RK<95Q|0_$U%Z9&vpaK*^W6S3WK4XY7 z#)0wH9pUh|GCbCl$b2%J>v@qW!*~zw@jW~5;Rd8lNIp^N%B__?B<*1kx8osevXt>1+@^>bN zTz?@x{LaaIMjWuFgxX_}#d@ffLga4*^7Y@jazr{hO{YSwM|8z1-yRs$!z*=%!z-0# z-x~0Im0S}OesIFJ0ecMCS@~M0UZV-}>#qNen$XhtkTsAPyU1$!hsd9Z$OlJq%uN4R zy63t*X|R5JIE|!fc8nStpz~vg3FmdHuD2Y}C7Esv%&#Iw`0lf3%fKivQgtHB@h~48K;&tOYdY`{*0gdb{*Fkpz#GjeA|7y zUN#ci?j6_`7Nt*g_~wj6)Wt8U#MZw>r$T7j8Y#kazK>-GqW@Cn=aT981&Ir7jX*FV z?tBwz=#1qWrCV5J^n#{SiFa2m7ir6=Se>ZV*Q9*BZ1fXt;y9{Tencs;?eGs%_@{-v z;`KR|dP2{}|UYver@X4gF&8((I}~$0TQ9<6*86gMI9>>!nz> z$DkbN_=fZq&I=vA>J_lk!6qLV=&lkyL#{r75n|Q~l3=&&@{WBETlNaYqLo;Po~NL) zzJ&?HRhAyN=!A4|hWq-ZL%#r3`>9kG>j>$9f98}r%qUyJISk|A^QTIeaW2Pgkm1`W z%&97uN1$_JA4!sBs$II1DJ)lSwm$+8qVx5O1|}$*ZxAICe%SB9!WraSYMC7Db*D%= zxX$z`5e4u5a~Z|R2)nib*560%{XW(vuoQMxx0KoQ@rx{L-7SrSY#5~v3 z9|h4vw2fp%pV9%XJ6Mh_BOm*xw+5-fTv0D`FPC3Lh*y{w*cK3fZB?uJ;eOY$660Nr z=?YLWhGm(dYguS}b~rg(n0@r?#j_c~Fwe)!6N)yC7Mo*Ct=8Y;>>Jqq`|_J>`zbT^ zD0P@j{u6h0jKseFuR`a*xAcZ1(b9o%e9){@W6*KDS?^k_i_Ho}nspRJq` zEfJyZWZCK!X&}CSLJM@qD`n|VV@4lnrfVctJ&XCR<;pk}d~*Yn@sxk_`9x>O{e;0@ zpZ1BX0vDN^T3jPHDlGz8*5eHC+t;F5`MG%{PVPLf>5|?4gX?P}UIDsfceg*Z&cFGE z7?iF-CK%BuaS@*~YV2b@NL`k8mfk}TlgbnzpW$ju1wN> zIx)LNen>M<15#)a-LnclRqpP0aBR`62v_^RibETtoqlHc24Nl8{02hF2w?9R=8ds{ zdg@#MB;jJ$;BWG8YK+O9srF}wu`drTGn@;{K!T|}&w$ARx_@HlR#`gbzSv6TaTbZ9 zw{OI1>M7$qvO)@n(RsoLUz(3_{QWCK*5CQWO#XBR$JIT>h6vdlY1+-uMuS*Q)LK3P zXTWMv!KpKN3*t6eWMG9bWYs-(z)J@nMYie1o-xTc`wby8JAQtZD-wRZBi=Ag=^}jl zB$AxnEK#H7F+k(T#jk|Jqf+%t3^2)$4mFA+nbbJ(U~!iqbUEy+61^EFLdWTCmuKA4*n5FKGWM|jhXejaM0@?=-dE7%!u8t@=@k7p8af2cA#qHOh8Kqp>p z?aVNKNfI54VQ|L~Nh8YvWgrzE#?p zcAPl5cffT~bc6`zf0^DNRyIsc{w~q}^oQK~Jnqq`qM99MwaWg-y?ZZHmE-!kE)`Yn zZ?Cs)O70-km%)lW=yGj*NlaORqH@t?zukM?6VNKwK(n@aL;DYD-!^N5mG@|XisNpT zjBwSt&ZPz%!!=7Hj`Pl%I7vaj{Apd3c`x_N$od$!xETM_^H+{EANdl&5rqcvj$p5K zmTn2b&J?>pWUaP+MyIq&!P!@+=`GKz(2WOjqnJntUnOT&9`amnZ*^jshMeno8g+X2 z*gw`W*8Cg~ptKGW8E)(H6;0WNI#{n`HH3??j1q8Q$YOaCeEW(LK~#RKt5- zuU{Kt1KY1J{QM*$cdjlT*Sp!q-AcvTjdH7EPjK{_;(SuR&InHrO)`Fe{o_9Ih55~M zDl}2>oI}rc5guVa{>roM%rkD$_7PJ$Y@=D+w$r4-S9omqz*MmXqJIH}XnJEU)P>%V zhFJY`qP%QFQta_wNwtp$ZPNDF?{4>v+132wWfVXBS5 z^`}R!NaL?Ur%aPXQ)o}L-3#z^PeJ&&+@18nY$*XFiJ$PI3t2R0ODhF7{WkSZ;|so9 z_x1-OFwsE0=;M5QPhlC3#~TZtzED`GQ+@vn%Ba{i252VD6z^vqx5bTqkzGqYt`mcN zVGw+Pe&FH^?i0-s%?01;^w=0vkkf$df?`Y{D7xza-wNNXMqiLcYQ#@%RB-!vEgEa) z+PsIRvCA2X5FJ8!$4yr(6htV{DR&@tA-{hqIz5(F;SsTe!mrrAdJsTuwAu(B`>^s{mfd57QcAKt`tqbW9mMsGtrkNWwNaYU!G-A@AYzt1Ve z3I|>8By`)}LO}2OOFIt?Q$op!%2eGmGYShz`4O-k*zwa+M+%23H@aFswa_E5>T!a> zG5d#}kP+X!aB-99E|^Qo0ZezA6xT8><5pr1UEf~YOAPu^WAY2kEQQI@hq0}<^_+(J1)Yk)@Dh?7DW9LK#pPb@Z(Cudww7OYGBGeABTuZt| z9RFmxBCOw0G%Nh0h(1;Zrq_LvDf>n030B|(`3@@kvm%z*kZZoxKiimld3OMRS~T%z9?=KQxF+^SIT3As(yE_1V&<-7}@ zX!hG4?+N8F#x2D5k8zTPdL85ncpYk>z*i;ayaHq;;w1f{Xz&5pF*e||T^IL>bU0E0 zqOr({Ds2Y=I%v|@E^8A{r&tIc3;b&5uKAt^HUO7v$|h)*Vu)dep-v{t9`7E^>iG>e zP_b-;^$-XDCDX1GPv-=7E5{+185NX$jJ<2OZ^sqAdqI^!+$G*lVJIdrIUDV0Oz0KT z9-g0Fo|5qM-@J7)x8zr$dVy}fBPt{n=(W#Fa)N+>uu5|dEKRd`fI_}S@Cf%tdAfo} zdm#&02mAYGn6>Y~q6p`zN}tGNhZgYm(AJI--Ny(TfG!b>)jbcVd->o|vLbI;VU(K@ zc>6rN%qrCW*8CyPU4(CXiC&vRr#lifW`&nq+AfY}6W>6mg6W`ImZ$&CF0ujrM4pXo z=h6P*d0V_hHxv=!%?%Ef6UZ2EMlp{_M@n4pq_pn&rzI}c zv{5IwRaRd1?rCw38PWO(ugJjW?kUJ}(y${KI-Y4kvHk9DlxKl(##fmMqvW`(7R)q$K6 zHrc{l1Q%6CvG&ezuONO2H{^!+jlPuf?1FG#JhP*-)|g`g<<~}Oa3W&u$Ivw|@6h#- zbx&_-K5<=Wz8~1`j?>C{MDScQiYsCD&x$kBnmUrFr2*F^R6ArgJN`2a`%I&nH| zPgBcnlRu06nw06K)H-h%xgk#&QvzSuywZ1j^G+_MgR?K-(a1WfyOB~J`5_~Uw(lWp zwb8x$>ndT;zP#sp2tI#THbhG8j(zJ55A2Qu>z3LyL(<`?_HY_aINzFt#so*?@!6h3 zC~xo(_Q}Fjnyoe^=|+IBGa`ZI8xGl8!Q(j?+rsKZcqJf|eZeaP^JM1gIA0g-!Zf2( zWMx9F$}$59%cx4H2(ymnW;uuuCtTRWV;nkcOakEmF}aa?R1E2ir%0|J`N1s5u$K+v zVut{X?Nh9iN(1%d_ic{pEEza(&uo;X-M_{EX?fbQb>t-BXG*W%ozt=lq zyz5d8F|x};E9sPhL)W|2iKuQU&Xb-@TA+$6OSkNH=xe@JZ2|v92k*ATrY+`~yb8Fs zEd+AkSs!1p`3Ukxa-%Y=On{Cy%4eQZuQ0{moJgufbX{5Nqg%4`t|-v{UfpeO(Rs+mJ`f&D^wRitHQdpiaG-k6-Ld)J z<>Ag1hl&L?c|tG@jEM2=_fNz+a!S20@AqPk{xuq~s`l9yjXSqFhJu}&KvEEV>!K}n z)B4hGZ3XtGHfcoeh;;W*Kz#3p*y@A=W+brl==3Zil$W#Tu-U6N9XO!Kp-H2_{(Lt( z#@8z@5Za+oSC+t_wnZ`OX;fxHygEX7l4|J+>43dBlv~{szexIHja8~kwn5mM7#=0J z79U7w7n&aNhjObgn8G$PO+&=`@L?$Pcx;W<8c(z%k`nYe^jWKr*cTZqj zq?_!I1e@%)c;|3hLT5A{#5k^FVc#|H2krPD$xdKL94mm|HDmjA|EOXvs1Y-rzY5-qj5_)Rz1pmZF3^fqO|)_*dz<0 zvf?!f!+T^tPBAzJ)n_WT*+zEi*URfLjgY9dQ0@E0ZIQroe^MU{A69Chebz92Mjn|? zHgJTUT1%dcG;nNfiPTh)ckvU8A+UQm4rSyMQ{iIK?AZS@FSOGWhvt)2C%^35iRM6b zpHZ5eNn|o1A~C9q12c>;hCQP>hudlqE)}L;XWm-`M*HCGMhK z@YVR&e423XFI*E}ki&ERePc(|mU49O+d*{y%~X#%tKfpP=iu~+=)QB|{0#0#bla+| zWNj6(bXwx&^8*`>lLjl$4eUker2~#@CH$pBT(5U$L{DDcjY$Y+2pkEC6$xoUQaqKUMP`2f0y=K zcTS*vB}FXS;IBSLs~^j7!ds;V5sqR^#|Eh`W%_Q|U$l|JrSc39kOyi^N=cr9R5-r3 zCFty9FhFJCPy=6(&c3z-{9RS#5n2ywz1z2Li0+QhPR}fgjwp4=ji~uqEb-r~* zZXTQ;@1GozqMCy4FH|3+F`zP$lF(vf(Jn>xUNr97+w?!eaVjZF#+Kpi`o~Iy)yew$ znX_#^A-u9qv8KMFZILH_^f$+2bNkkOatVoLlN0<)T#A6_YIqDbpv6F&d~D1o4l?+M z{#^e9iv?9QyYK=9WB}6HMT1R+qa~d-YD9!v9C(iy(L`o+a^(M6q0_rrs@)wvDR{8U zHMKCIMjq|U(mS@^s92PAyi09#NTgg8=PS~=M0SS9{twHdR2dwGuukv}dhT$7oTLV^ z>g(M=ejg&6zY8ie3zEA-_zL^(6ylHZJqp$<_$k^4&MqDhVg)(i{^2Nc-gQrb`B^&c zb@f25f>Q5gPa$3zgrIQkI@F`4RQSGhht}#JeRB$=*KkN}3g#1gyGLFiU%!E5=brV* zcTZI*8iR-c%SZ@Vj1YW$UwjiD!z~ErOJ>+ce?hk$yO*KTH$^W&a?h?JI_jTn zO~OxqeBocP{tP>Lyma(704i?5spbv|9x=M4={DKd>nwbOLR(uc-Z2D#bCt57SYq0D zh6>;_E=F`|nd|&%*mZ*-K!btQzzH5E{7Z0vL%KNGxsMWO#VnJu7@s$6|6)xdp$f(E zQzfySLn)>S)fw3l+djB0w`5|rBUpnm)7UovX5#t&!49o7XMmevN1m%Hb$(2Zp4f7o zM4iF<`{VtnC-Kz@5!gm^#p47|lLk}-G^6Uy#gmH(K}J!)0PJ2jsHr7qivRoO>3v$d z;~=eh+p-!~euPUEC;&T53I%lsLOtjI8pBTfIEOn1`vwnYbqEXduWOE&DVNkeau|F{ zBo0q!D~GvMBu*qw1}0{J)V*aFZxuvXCo_)s4Sj*qp{`E5XDO2XM3ZSpA^60V4L5Gj zm#-EswFGx;g}?k)DO>rM=~L6mV*FAx6DkW5I%BM)miB{xC9K+Py8s}J+7`kNj)wgY{P0-e^9_#uD!PDYrYd*gf~1h$+bJ2ei#=4llRRH{9* ze+&%{pnhmq;}Ep_Y9#)N$3B(Khxx5U4}#5U)Tj*2vc6q#4>BuojuvGeUE`TB%v2?C zDE(V*dqU9mNMTl*v_>J%8eT&mqvgXw!`a#U1(FLQn72?8Wl{0?e|&(Qp-*z&5rQE zwy#3hKwIKA5pJ={G`6vuAt9++c_3De0o>nQn+QSz|Hc&o`8qjbu04$7z>ti17)3=x zMZ^3&=cXKAz@QcJ!>W8(m>!NmProWMQJs>?C8(PO889tSX$jvKCz>F zAP)0Tfim9?<((U7ML|(!1l`kfa2(&GdP4n7oSmanN%-VOKJDa2jHDQl_jugyGTd$) zYjdD|)M1ZNEBw3?M2iu~Q+pP5#~ufcTKBDxs#UX%ncq)(Vbq>pqbh)bvleozY}ywMxnmsTCw zNUK$q^4;O}J;f616m3_xbBtNwJX`x7?wln2D@_EY#((V(TQc3?e@ZBEmrLJ*M;o3p zsdH_|yXk@m;U+%A!qqf#HR#ZLPgImhTS?*!XeZI=p`cZHldd;1b_ z5NtUwh^3syJ4!lZjb4~Gu$*lT1_NrcT>@M~J5cWfY(8N?5Yyj}G&}7&$d|RibmmK> zJUrb^bS`p2xLR;-nU%qC)y<(M1kTyh6V1)YfMNMLH9v?>ZNS2sp|BBMC z?wrzX46{gs2(GeR$b*o+olkY7xr3R!P#g%A2dy{ZE6WjGzP)2T?XaU-mwQFHq=p|V zx}-zvM`v0LfgKzQFjcohxwg13_vw@-1fl{6t&evRExjAm+MN~2)d|)4f1ra=JHa~z z$f|p{P;TCpr^=4r*SYqM5)g|_bL0%}d zMSgWE1zRAWf1yMh*&*P-dDluAE1@fLY`s?(+IB?%b1UBPV{NAXSz5N~3-Yk!JsH`J zi&ysB$bxhL@C##?FOQxb00H&Us;^>gU@*itV5cQ*uM)a}CxYuY=oFVo%HR=B5zW){ za(s~d0?N2SJ#Mf*uyR}q?V@aNcMV;nTm76?Mx%$~CBOV-@zSm(JwTW?8ofY%8HhtMM5fI`QUUbA`LJaoPEoD! z3WsxWC}n;ukcg6q(nxe2p!G^Ke!=g)qVW#?$8yBCLg^ga5BU#8WyL7>zx0%{H&?5cCL(2Br@`-o1jGNf5iAes@BW%GY>9GL%q$=cFDrj ze&;Y9Ek%GnWQ`6Hce(2R(E*|GSP)OjP>?^h=__omU;SbR_s?%sD!cRNx@A4~G2F81 zBXRe=TksH*YHJLa(!zuja$>m#V{qFx2|r+$Xodv-UuZ2rZ|GxgZW@M_Jp^CHeFn9F z;kS~djC2}EJNAW?pg9g*<3A2P4F3rV0WWYPfk0V!1Jn5IZH;U7L*(jT8<6XSxfuIa z`>hs=S4OjMAKNCG=Gp|*$=*nEWBlZd99dbBQk~`rO<;(xOD!8;7}o9iygVG3U#Cqp ze|q)ETc757(>7k*1|OKmz6bj}8sgb}U?)6KYu1lm8d| z6ip}ueP5z-+G~a%wr*$izj@Jg8n7L*S$$PB5!RolK>{E~j2-{NFM)K}KHu*3qDW>N zEj3q*lzgwYWQiI0t_d3Pv&51ccJX@$73qW7bO!KWl*ux6%Ce5yVo=Tx zDor!Fi~eFKyh73D83hb?R7sUFe#*cM(vfU~XTb`$o8uc6_40#+en`|h&9N|d%%IeE znhx%#1lM^`OZ*S-AGkWe>Cub`3=;sA`RyA)a1q@MY6o|#qxM@2`pJke#rvTOG1oU_ z-=M)YSTZtWlzH<<6CZiF&^{$6&uySwrf8CFC$O`0xoUWx3I=vID!)2Hd5 z7<0@{+7JND{0lG|O=HAy{pW?i+vs=AMvCVE^?iMVA!ibpgxXD#u|*I>J-Ed<$P8~z z+3W8=2D8#3e*X)~Xxi5AU$D|P7ER*6g9^3S@KteL7F2E+Se{N_NgV>RJ6Bs%lH8XL z7@pokNU^`M7RipmwzyAMNQdAVb}u-Dz`a}pp?#=RgjbfnBWTuRFvnL=9k6j<`h^rZ ztb?~xM7r21Z`X85E0f}zmucM~Hi*?-OMRf0Y`~SP?NW}gdKc{i2Uqvr^Ci&jC(FU8 zR+EO{%BG0<%QCB%-_xmE#ALkHA1ISxPsxL~fQLh%BNJ{NY` z?fCrZ5xO0V5|X19y%bMlqzRrD0}uxy3yyN-;c=tS4jm@<|5YCkYGtJhF5>6R&!{sP z(I+FL*unxNchD0*l%c2AAzvUCTz0a1-nPxco}zo)=obn#vRt!$z>GYyFdfg?4@Iju zJ~azNl;4hPhiqO~`|ZppjB@}Vx%t_|7O<`xe*?q2T##+h!+LNJUT_-188(LbHJJ`~ zsnzH-PzQf@cBM?@1m~Kk{MC1weB3*b7KkREG+uSlbd2(DhF- zDj_(*asaJdyx)&|DF*I+0bgw5x?bK+9)?7q#R6phx^D-mqg*7n^gVT!V^5DeO5&y> z;PY#M*`UZRV4e!*#68IU4hjf72kGMx997Mua-2k)#k+otUrkOQ@Ap(-T+B>Fwir;U z(Om9nmE~nDj}`ub<`?S)S=+CW5Ll?Z-ur^i_$5>GdyvBAyRy~JLyDepV2Qcdu+JFG z7t`P`P8MN;70%Z$+{=q_;kV$HaFKH~;f^BVH}T}_ifwitGhfa3;$h^oT>l_=tj{$~20+t2OU!RXi(an(+&hDA@z_ zkFh1CAEo~u7(;7dE>UbNe3)<=sN8Rfu)WuW5Ttg05QG*wV#M{UaQ_bnzkT7ryxztg z|Ff2X`2XBq$f^Hxd%4WEP}baSe?GmsstLbhgol(lO}t9P5aGp0NL1GqE-n%)>Huv; za-PpFPgy+IoyY&Oo(FKx8gmD?< zNPPj>aR``L)D&oxK0U7XWL!aZdG7VbS=Eik7*8P*nQehkNbhi;4S#xjQ(%+hw8Fa z&@N4i|BW)=*TQ4)_z+?KGC4Qy7;B=+jMc; z$JODtBZ?T+(x?yA%WIHtS?bslC{0o_z0~G#N@Z;6(1uI8r@M3d75$vRL3w*{c16eS} z1V3;|ANe{U)5j^dzzs2{fZT{`MHMuX5wHrBh$xPc6U3daqf(8ND=10A>9= z<-U~z^4pET#x&D@W@XEGC+SDC|9j+VAJ-bo5ed{jj^_;X`i^ptnO(pzeX;+1HFS+O z#yiD>cTAy6d?x2=o7l&S{c?olC4ch`2-hqdXEl0}sfjzk@U6qGf41u-yC8{SeW-*4 ze(Gkl>@EKMb@di?>7=Xc5A`H#h#pkCbCY%Jc&?aaquW&}xZq%_jVeP+yjePfdXI3M zsPo$iA=-X&Wvr8Ei{Ri8k8fPN9LWs%%I35W@uLwnou;1(>kxcAd#`Uqf^}!Oj`0pw zPKivLtjh+e0+03={o0;au>=8ur0~kFz{{j+u9kA`YLViMfBn13_u~-%2j0Pv4p}5@ zmUGI1-=0#&B$v(=3>OR@Qemfl8((tj(M_L$3AF$rJyPUHw3C_gzaklVb&3N9&4DuX)7v^;Rg%ui#vc|jE#A`FZpFiWcb6PT!KOHVzgm0lw3fgqPjpRf~ zf#Z_=;}9&h$|oLB`-KqdxC~n>hjg!`Zq??U9=`qHUm?AawX5K$OBKPjKB6mbN?307 zIC1}gdXHZMCiM9R#X9Nh{Be74fnxA;pVg$dQUtbk4$okPj4OFvj1Ht#&Isn4Loo>Bvy~;6&UFJm}R5bBS3VS19 zZ{gg#Us27o^l^@kD6Nq)P4aBezyli>)T!C{hJjvfKYx9fs4M*R+CTQ94Zt#!EXP77iS#q|OaEp5V=3Me&+F8>m72_gRO09VsnI=2Ja8QN^$SvUpUmVJK zL}-tp%R#G*_|mOG0a5TJ!~e;vRC9$RHcRbB;0IQtJ$6Prwnlf2!3r z>N{IC)bIDe7{L;e`S}yTYm7(G+(d4M(dF;e$jX8$mNMT@&_`GS8bK$ami&%W5PshvQws(rEqVf zI(I~2Uah?52;!QJZT{!d6ffR4 z$Wxrlq#mGArXkis@WCM|I1hd21MU(IsrQM%7NhnCL<~5})$7MR^YhmJoqXgL9M_X< z%ry;2>t{F4o}4p%&JSRjtQDb@4Gj6jiEZK>fNP)alSuFb`wkvfqEVug^^5k=w^Mpt zqB0>mz%6=iy8n*%sys#PvI;M}safhb%5*yLZqzP^yT{-A#$tsY{C$P#4&5d*1_Owx zv_uYrUj+~J{1L;)0I0n;E@RrHroBNY84UUtdX8q>p>OdU2-~~~&45;R>(Hre&ti7s|lq%k?SwoGgm<1?CgjZx*Ngf_7-#5BXoA&rw7&b7my0uCBLMqLq z<^|;jru@|{ZJn&dBs_?E?U{K-ht5^e;Cqmv3GfRUm z`0usx%8d|~sOZkVEj`Op&h?u8(B(R>L?_;h=@9qWr{1!gE7QVxO znk^*U`7_Ht$*M^NPVdvTMc*Xx_JQqgwORJq#wv4^D`a;jS@)RFI;q;ua_W~<1Kgb+ z$ugtHJLcs8Pr)ZE^X+k)=nu{xxEAs*4k$p#o%cy~AAg)8I+a+@KVAzXwt4b*V;s-O z0f*$rdXc3D4u$w`q+Tr=VB!R{1?KfX)UJfpZ*);ZGk2uncClPP|`ms^B1MAKsXF0bM%_IpU( zrX|V${vHfC%BJ&%V@}~(%+8UZ7u;x%*`sBbtH+uH=uC{}>|F~wH zh*NHyi_?Qpdg#OWRc^i5?1sL5V`()~4Z%l1PRvJOqR7mgXnQ=Zg6zEGYjYx4Gr9bX z)FD))^#n(&(GzN)F})03{e{ph_b{1`MU!LrlVsfUTw;sdKp8R@P5#|W5YYqm?u%X- zm8d!`pcNloj)kjVq`f_KbL`{joDgy8BhsfzagNC@d5gbO|Evjh!nizL26f)#?g63r z#leu_5BjZV4h6)g)Jiyz6SiJ~r-R?*2>(RdNg>$hs}fUqR8#6aV~9V-EzfkmHHGl; zejar1mNX++8|~iw7Sqc)f^OX<9}_^T5$k6Ec6<`%;+XM*_Fwx`ArxM(KowO1*!(kH zDema3#3F*4QNz!U*noR&xE{ijZ5OXPUQx!MPxSKLRZG@Fp4V zer2-1#uN4o3DbaFvv?IWtJ}-VkHa(zm{W6$&@&o|iY>fy_Z+!M@k{VnX(0H2O1ptA zCh4>@!pDPLic^S(?S?W*>w-9WLq&}FCW0;PKHfl7lrSBpn`sy9Gt|03>4M$7!@vf< zu{9D1n-5TxLsDkhRQ;>yskHvYV&MjRii~?j7~-!rT^V(QfU;Rm*jV}MLj57BRp_p3 z0tufVO<(OYjB`!0b5n+u2DMqnaUSJ5ay>`ba}?s8&hcyHN>wB~z=si( zOdaLOaa!>p!_p}MqOB{*HiiRJIirg{kz@yl!bV}E+&9*IFY8FYrBq+~0kyJzL6YUk z)!C;ZB$`b&fLCmmpK42^zgfyUw1DEThdkq2b5;_cEM%e#8cWEg$Ga7Bf;e(sHiWp{ z77u6I0!`ZA+tw$9WOV7-6^Sfb6vq8M^>*mPJOOt#nw7!?S$7rUdEOQ;#H;;m zn8y?o0&BW0KUl5td1rF0KY*sixw`dEl=DR|T~dl=tj`m(D?ZXIhC4mNJz_$Pf$(yl zigRzi%8Y3CZhd_*Sm)GgH>mZ}an8V6-a%Br?TAW4o`XU2HaGlni0>`*0H1l%JK|L+ z%;0~sKa@N{kY?#7k$Hak)@AyS&o=3!lqaF9HT?!V;M)Z1G5YPgqquqGT}-~OY_fwb zU;;2^`YNxRT~ZyuPVWB4Kq*a)iHPvnxIXbC-Cr#^W#4WJ@-;v&%dl=B(|)Z+Ra--t zj~+RF?|&@FEXvnMHufk@it4oce~%%WK@trcj5a{|DwAZSmbTeCKoH zMh{tTQ_ig_-V4hj{yA_sVS+fDk16(jX@=t4dhgz*#1n4Fu4}{T(rDM{4iJZiMP_D5UK|T&ok>KT`L(U;E%ULJHvl+)q<6#Zlf6x>ysxL zpU{~6BRDM2e(emp`>s|4X4n?qEpq4R;Sh${y}o6D`hR3v^ynf3Vm?m^)OlZePHWBr z3@*yX*catC#bS=B&L7<1T7{NDT?&aL=O=s!8@MA8TJIbMR`@GBa2WvK8QlFDh`5WN zt6ZXbzaF7{=uN+=*(GbICxt3F)|MlnN( z5Sx1KbmybEwWjUvA{j0@|ER9OqO}&WM&Tiz+J$%M_5ac{Ym#Vo$wga#TOj*vgY`Oh z+03y^WEdw+(|?V8T(oLC#gPy0l3*{CWX*E0i+V;BJ(SGdJ{LwvSi8diWdIZ;AeLFT*q~%rUQ?n&-NAXJq4WrzwPIgItaJ~~q_U}-x zN%pzK2gwl}P-yG=?GqJiZIXp_l@*T9Np}C5jCU?LTAB^OG|w7cG%Py9()i!LLGCb9YM0lu)N~)Rll~4@;KCmZf`~Y_ z7>LuKYx>1IW548|Uhq#)ZTTf@z2m$>-KpkNj5y^TGjR7T@hR6OJh~M+g!G<{Du4tO zo3OS+AO@!|&DNB7zQI1lQP++p`N)D~{CFlW z1yDOvO0KC?*J1whdm{XDuE;`s5aaxa_yGUc&mr9``SCJSy;@=)DIm-@>*YVYzj+!n z-(6WC$P(6rN}C8c4GO#~%`tH=*!h+sWEktzzwZYkms^KV%7hm(wMIpD1QGax0-Y0V zP&sGVWSuMlizpwE=I*6?m=~~oW3Q&%vQnht~MtkJ9E9NT{1hyB50&t4( z2wR@2d;c^nj$Egr)mWysO8K}27X6z_%N(y~&hZ|~BO@Hv?Joem?Izi{AQzTY=%w4l zG;LD(=(q-zXu1>zg6=`AQ%ZD}Pj5^gnkBqAo5SidnNdQ8FiS&5L{*rYw9|{mUsYzWk!+JlLnC_5f7hv@gTPUjcUm{{-bQ^)m zz5`Sj@dnfSA2p6aoo4mWZfM~2^l~LR>*4m;(hmW4nE#%|xudPt8(`-^4 zFfLEec~DP(INb-77uZuoaod=dE%P@3VbHo4jM#y)hTJv z4s+3@6Pi|I78M}7QmwU0`MIgmNOM}JeIW!E2E6SS8H8-FMtyxrX(ls{RDXTN{c;R_ ze+7CAa-Z19HdkT}fO-@KoA3Sn2lKl>VL3hPeP7$0+#cFtj%+z$!+62@=YS7S+lAX) z5Wwqm#6wHMgP_KfRRnb9qm}uQL(R=&o_Itmir5H6GAI&qnloU zj{nN%4#n}Cz)XQ_mP0_s0Pm6gEj;D%vEJADevZ%<34&D33z(KY>ZMf6TJIRBQ6xBi!`M zZh`?FI-6miO?Z`yr1O`P~lF;kNF~oc)=gqFIJZ zPB4cQD@8)0?Y}PFyQrtkT7#7>y0>8^g9;6W9_eQG&8y96F9lx?Gi~7S%C(LOIOHFY zCYA#XNHyPei9Xf-*r!rx@^1F?g;tCW?`1h-TK2D0|G<0vQYh=R~Dx^nPQ7(X@qrx?H~Ivcbuo_0f97O(`G{-M_bjPB6RQo2mm^N_7 z3Vo@1alwD{hUBML$oJBX4%qtT8x+xPQ>)z)^qSpb8Z_&q1g1l~04%XiA3axM%|7u# zD&@*4l3eQ?qnFSzcCyJ4801IQ3Y@>)LWXUL64{CY=N~wx??u1*Q|-XIcbR4XQjctL z?873kvJBxocZ!xCbSNr~xE2I6WvocgC~t+HR??jGe4y51)O>dLzZaQyYUO&>7_}L3 zPC^0C4PPPjV$|>1rw@6xR-AhU3cx=2fgP?(i*>YSDGK|KFs%|-I$EyrxE|w0w*Y0;=d%wNA?F_~(NwCmO8stG<1nBP zo{V(0<<-S-pu?SvbR>(Dg)7m(5tj;~mp8;<^7~Uo9-P1UYR(f?v`a8!f>&!C@(TRm z2@~yY0~KpwnGtM?Vx(FL+YFqwL0+sC>5qPqO8Hk)y7k64I8m=Ud*{1$Uo~El#+x^oqf9$O z$3lM$$3kPQ!!Y9v?-=uY*jW2Gn`~bcE#K^ghnQIne?ORZk2~8VO)0rWeUVtU_yvI3 zorhvTbejR`u`Lifrp1`B=Oe~juXdaV=?_RdT`N~nDWTVe#_Rwo!+haRH8;zpYT0t) z#8SQB#`n!MuM~f`JJl-Ngi$k0?)JOR1P8pljPXpe`4|-C1MY&IA{`3c4l(#g^cwVf zoU&9Ky_33xhB%!P3QUKu4rzRnbEJ7Dq3Q#wsCosWvCcjfTl|7q{|&QFa1AlbwjdJT z6)6VXX=F&(k`6dbSaf=~vh)g^(2!j52tM{gy@q$4+6eyLp&->299(!)E_VyEP41TX znzv6b7atU0-2Cy5e1~=69k*ynS8Z;zN=%>faYXN zDXa2lg957>6n=_hoF&gG+_^!z^lp(?l#ftq*#2vTZ)u9#CbOSiq#pnL0+L)Y)@zEf zS6rks*=&*WzN=RD2+ZN%A!`{}LCon#Pa+;XThOB{=l9;!65S$fE7$uSgyc)IMYA%> z65`x(e9$+QyT2bZT`tHJ9Ttnt<6xVLHa(Ss||Qf%Z3rvm(a7Yia z=}^wG|07*p*p>!UyqDz61~L{=RmqWWLF|2iF5gEs+9SqS?p|)6 zt+74e+9M!+&Q7$)yx+B{a4n=cSQ0_&FR~mDh(emMUeG~Jx<7loBi$b##G6WGq%(@_ z^4-Yqj~%_g5jZ|TZb3Hz?)xW(xeQuP8Cg!Ez3MdPnY2nfR0wZS6;_>6r^M~c2F2() z%$r?Ge}DhZzeVx(m=>tl4u3>X#}u^7R;hBG0yA98PliNyx1d~$>U5aA-Jj#!wDZuN zb;6xs5UQnEhr_#c8wC|lq7|7S_2wPU1BTN|NZog?F0YIU^tfMa)28#!F5-J1bC!aR zg0n$VFmD^|WLpdm7}vo{*o$nZ%tM#l7tBBBTea|?7KjA?6>H%?g6^N-+?5$1EXR0c ze)WX=-cQj#X=5J7^#zt9xGjTHOQgnNVJ^Hw7}kAixB_o#tRl7MJsuZd3NUA9=YsM; zu#eQ~!k*eDgi@S5;jWK#m}Gj}0_9mh*N6_5CeW_6ey&py>~YAt{FhE=gAvzo9>6K< zjtny+IO-8Zx1D8XpYz<@siDxZM2-IRp}pKx{Zc?ge0eC~<<%S6<^N(4s&*g zR2nctNPobtrx8rlD{#+#DM*cdS&}!THq{Q({b>f+3gwEdIQtdH3A-fQ<`lU9 za_9A4mScted`GRZ?+|=Xmm&CudDbPePH~J04|+j?VMV>bJU2-@$IPiX##VZGJzjNs zVKBMV$Q-Am7$3%R&M=PQb_`7X;ZztH@0S_WR_M@cFiOU4m}+a48s?AjoM5#{?ONcF z6K%r2<)1UbT@5@Z@r<6Nyq~HY%c}PjbPKwl0<|XUl8^107Icb@a&J-jXdGjZFS$nz zamzLED85t%yFpyZ)n=HQv!NEVOck$+dyj z*F>=$+70#Cm&kT|5ZhFn`nEv&kgfFyz5lUE5pU?$U|f`HWxRZ%idIBP>SCHv7SX>A z6ns`k&|O45p(ve_!Yo8qkh%9?j9RAf;lN{KwH( ztXirO=RC(g(lPwV1tZu$vUh4j;!8px%@f*nuvd-QuOa#YMl+FSLe_y@1pT*EyAwQc zhfl%{@A+Bn0B+GuiU#RN_;fpuc$Bkjb1y=PUg+&}gh&6E2>&I%GCk*fIbO|Hg>Lus z5Yr=dHl?qN3f*DVX*?+7y8IFXLHm^71rLaGjNQJe=2>+U+<6sH*;zq)WQRmn7~@Nx z@NF^g&_oADL^p?E$GcQ`rX@NFE(XOjiz2-EmLohHR4R1U-f~Utk%rZAUW8|Kx~pT7 z-QjJ9pK3)56x(bJ!@3PyoG3WF&6()u{X7Q%(`+%paJha^nReJ0eqf@LWOII>W}|i` z!aMRO!92$*%HV&?tjnx!pM~~_l~0aU0N=BM*HrqiuM9)_6l<(+ z*tBk_UyS<1s@B-m!QoZ6s00_8KAN2}qr|$gQPADgG7ysgO`H??bB1XF@^zmm&nU^k zZxQ{1M;wGed!9Ax-tsL><_{ zRt-x2dw6q&^@voYMQF*OfoTgs;~AKHf4&FMhKo&cB)h7mwCJEdrI`J?H?Pc;>yeqj zY7i+CQtY+Jp;5w2vI)7qWs#3r2Izx7E5G(JGoMxPg9FU?Am*OH zW{ex%6Hu$ogNc4$&ec8urof0|)Sm{qd`EFF1^-L*fHnN^BcuZ9{;p8Ng| zTVk$5&}~G!6e4hz@_rTl0pn~E_90Evs&Rz)(jXw8xc{{SMx2MXAoZ{9F}t*LTzjM@ zU*HW5NVg%cFVpr665^$wcbEqHZGkS$v{cPIKh7-G8i=#_eU?M0{Dc|$Bc3A@j(CVE z)0X(Xhp9)ns&#n{+qQ4=uH}r86HwA>N60PKV0hMh>-hV%!>GtVy5Td7|I0SNho?r=M=6KV}qq((0-n$d~Abw0J#} zd}F!hUeLIg5j&rsp2~dNkFgs#s;E-rXJ0s?5t`?2emtpk!X;HWIwQ+`M|#<#fsA6^ zrkPDZ*%eJ6#mYu04oUgekN0#>3rNNh4Q?gQ5(OHiu_oMK2E>KZJZ9ireYRmf#$9Kr zhP+{tS}^Tr6{^!2hCE`e(NS$$RO&JYAJZI4v;eFyx#no@0FiEqb~s>@Q179Yhg=Cz*ZT{AQ`(Zy^P^NLyH1;Km7W_+x_+Bvgs(i;vOuOjy=H{(`gnD=L$Ob}JYWW^ zL2I*#_G$2@laLI8ui(ZSdk1<76WRUUK=w3c)w0T1AnId;|0UI!&PKw^O*gcP`Ky z2)%)0GMz{JqAWrZga=ABqV?U`rxgKdCbA`z9EcQ8-jkst1;UBO7~lti^X_-eT}lUO)BNownu?N z-){T;90-97>4Nztr+In?zI0Ffds4j2d}OiT%)a%GXvd0s2KLzARBtR-oc#w54v8Z4 z1MGkCHVg`+S0X414G~*YpNQc|WLcM3mkjzsUKS+`)oXZ3lQF)&ZB3kOcutc&t(r>! zc>+c3i~rqW<$qYf)t%sveDHs#2?1K}zdV85JS!|1u>nN#bqH($QK)wC&FFT$pTy@) z$d|^PQq1vpo>*0*0TgR`KSg+MP8ihC&!oP7zz;Gj)W0E)u2L>RM@6zoM2&$w^n?Un zy^n2n`nQL|xFUJ1{Udu#aa0*-)#;U;k^D0^s7JT^;+w+J_k;YEN@HANT=4e3c9)6G zd&7%zLOoHcs^d=OilJ9I@+T*UO0}M-%KckgDl9GWN@6|TR(BGf2;1gJ{{Xu zV&xZ$ZG2jK4(uA^N^(NMtCWIs88p*8^$5XS{$oJg<2>8+rQToRzt?P%)1S+v-XpD5 zTwRb?Vq#GMXsM=Yy%+kqASNeBc;$8Hyk*jyS;+TwR|$+75zUVuUhockvA-0aO=tX zX?*I|C^ayCB?wLNA7E&Y!t^4gf56PCnF4SI*~<>}x5(7vKR{xJrR$qx^xk2|@`M{K z7Y$DMNBRkj`xY>P;eMgRJPmebzWopi_4dT7NlhBc75!x9<&5imgZ?n&E0{Yh6KP{;&hAPj+Fl_=fS#I{P&Dq|l3NfyKKd&M^yQ)v1S9hbX9*X88V5R2zc= zNgu&=n^ULDejy!pE!H7*ni&NKSeO5u1Q(t{kxfaJe-XO1$RW>ym7A=yW=jZcFQIj) zR2fI8oD)7GU9%IwO0|o&)+#6zYbVTX)$}DmToJSkO2RgqPC`w!Lr0@#=w@M|=30H* zW2o0@BX9lUacpD%aH&q7tJznVxDA=3qaE=-FuAn2lr27jAz5lc zS6zsw{YtS1`-%pVK4VAC&bF`O@Y-@oM4RX6wv)*>9>0qt+5zy^2gB#W}V?+G2X7E@+4GW$nd!ViDfQvCh3qtC2lbuW2&IP z#=O^hRhBWi4gT$l1!2A*+Z*WyT~@in=Tswmk0?hdJsL2hSwMCeBH6c|}fki5kPsW2=Z<*$=!h51xrzq>#toB3D?K#{(p? zMJ5H6w+u_7<s18*a1bf+L|Ck+8%5ws|{c-{OI#oj50K&BL-I16c*zjtl2u~4~E*Q%~3v|3a zYeE4@8Qv#Ej^Vfa#!!3WuhVCQHWmd#QW7F~51J+QSe?>4q(--&J@M{C?X>8HCt(8E zID8R(LIlU_LL>TMQ1Q+oKCQ?Iw>?4IL-+0snqr&%8xP2RFtcpA zqmMQ?bZ0sy(NRa$RTNgpG#HOEAbzbNPzU}z5KsFn6)PbyB$ocCFVgKF0`-UbrDV;l z^$iYHjQa1J6gl^%Ap%Y~B$lv^IMwI+z-?#`)RZR^K_`#-#2oQl@qt1dDzxcXHD532 zb!X4o{fvpI?^cLguPKqoUEnL){`pax43Dh#-@GDr1=*ehdl)dqMz%?_FUo}?u^9^I zY@>w5WydI@2Ps-m&>&k=xMZtWyh)UE+yGCG`|Bz&sR8&u!fsh-R#<0^ zG9$~R*_QTYO{*9V&}E!kCe+W#!?igavLp^}C9sZhi>=KoDF0PaXpQpqdGjSVEGV9x zEBFo9Ht*Ih5hBFVr~Db=M!Pn{JIv-2es-MuI>5-<|LBgtxCOBV>G~69^qKVz7l;?F ze^mE)OsD#%Jj<)|EP|ZidQX<^mA5K`)~vrSv+`tuUqlM)Ux|+x9frUtYy3S(>D8jp zIlF5z#%(A{`hxdPz`xWY;+kbZix1dgO|WsS#Iw6=&b8;ET?yN^JowQS z7xq=R>{@0nkQ0{&;mDx3Zy~bF4ViYeeE_`2i~t-ewjSA;c`~(@Sw@&N2kLnO|le*M(umQWksIf>@{$*Md9nWL+H zaq>)=4epqu@k+E#_$S}Xw?GVi`j9@btPa$2{)Fxo&3X*}sv7=5-=cpMdE_I#ajtzK z3HwNX*&_+dTyH$XV?7g)!oFS&VRx-ushUm@8HUNaAE9p3DIJA=c4|VdTpZ{|5fET(bV@L*P2f%>mB>Z_OxG-QBKxkVyx0G zlnOVpQWH6>Lc3F~>F{!$2g%r6tn>zVc3T&AOJYM ze?)S9coz4g&_B4sL9@UkD9-?x;kF=m{`fTEAB}R&D@ir}kf-cVaGL$qEj_%7>f5J1 z$AfiDy%OUV@4`PcK4?fTSu;9hi#@~}V>QCGhjfYcffZWvQ`9lu4;|U}7z6G^r|i}6 zs?bQRi;HSuf;|dwxZS@#I?YbwD=LuK3I%ExotuDxMz=QqW|EZXjB-p~JTT5s1;072 zldQR>{gq+)EVak@t;v*fbD%aJ+8Y#Pw=c?fVP1t*)YHTWnJk4r zcFQw^!?{r|C$MW(hBz@PxI5Ivb`G=^+nKjL`>$AIrujuf`6)&-3Rt!Mp;6UU*@U4; zata$0Kc9hQ{A?`2?bg#Ir(V}%DxJaIr5>~HQ@HQuvfM{7w6FK_0Nn5&Cp1^5)*ER` zFeAp@8(V+6k8$f1DlC0?$2mSlqt}UO(PVm?eddfwbmDq{_aPP@Gq^e; z(}#(y#b$>$Ej+_8&ql37aK1(I&gB;yvw^z$EAxKXOv6YM8Wd|#@YegoR^cO=kidY?MODlPnlu;5K( znp5hI=J1pw@l0|Uo11E2`` z-3-+kEYjyb5ojn^i(>c^<3?!2CtP*p)cm%&N$H$8lFNh|>xLUR+Q}^E0he6E=!4+G zE#sKaH?lIZIWx+oTkH*0^T0kL^&y$I`lCew{$4u)^;l%oH#3Rn4prsi3f{ebjE-XG z`j?jCEOnu01B1!D$H*kgYG3^P5~$LU=?z?&22^gPFQ zM!!>Jpgh_jfLEZr0w$;!VqN<4e%S|i9jLknF${H&n-jP+wtaGGk0BWlAR@&YESPPy zcTH@Be-zcMH73L(9xzvlgJxG8+m=*?kz8P zwT5wxb;<@77(Kx0bxL%ESx=AU`lH+j`#a^V><;mhEY#*|*6Vk(ybbYT?$I3V9uhQ2 zu}>%4W%sG}`~cQ!T;SLz@lT_EoVzcAAH&(SIz->2D0Wuezre~qB6Q)P%#n7^B}&(t zx{b1k(`M{ujL$n7auSs!Ppc?KeUJzwS@l2^WkK|WM&y)A>~@8>B<7UdSZI&5$O`X_ z`=4Tg`HKDeN_<9k5?en#pVS~HPQ7%GCL@;3n7}U44;Aq7maAt*_WSi#>mMA_Mr`~y z`wC^>GT}bVBdzWnXL47S?n%E!WQV&_LqOJ$yC(J%72kZ5Y?&d?*$F}1=%_|IO*^DR zo%zwU_zjone1S+M(<(npFItW8cv|q{-FI!6i8%(lDWO7J>I20_ffe@g;$zbq+2^wt!wx z>m${F6J0h+OtaoI{*~aFR5Y-96mx zKYgZilnXSp{c5$5cT@Z^T7{pdDBQCA)UTCE$Gt@N(CRqt)1^&}q zL;NAu)BhCfL*0O;7e6w+dfXPNNC(n9@`S}3GQ8n8CTKO{;Vp@;{@BhDNZ&_pwSk8| zyw0sYEQ#2|X6C-iF4^-HGuSmPo1pz=6CH{>#^cj}QQUn>te-fgTFZ2Lgd$^+%ki@PZr`;ueMR8R`jiKj3@45n-wF*UcpM{Sj~b;< zpzudzU4>A1Eb@D`q*E2y$-pUcZQ%?Gs6BLpQR3GPn;UE9+&ik92v*c8Sj2Fe^d4J#}gEI5z%4(##fhhytC(6-DGyg`SEAp05w z(h*p7c$-|Xu}OTAb)}qc!yx?#gB>xtDF9JmnsWLLqgg`Y27f(5RIRmF6!zivr{GC& z+wm7Msl#2SE+#lb%VJi>m^S4`k3VEr?>TWvslOG*{jK{;z8SRl$wgd~Uis%8-hs?l zq?UOSza@wmNJpU>r`oNx8RE`t83}oBUQJ2-i`DHt%={t)wkAyYx4_$xELcGqQez2m zbhFaGTuP4zpf5_D^%3AmiM|~F%5+_9wMW|5Gp<#kIri5}Ow%r@UmEx13h4(Dum()i zut+DwJKn8mgRoi8e1lghNokC18Dd&+gZN_fbYol3E2 zg};za&mShMl_fIqBN5WKTKnQyY!RYT096U+<5RWNznIywUbhps zgs*gn(|v7IMKI>*?nS`+9dcA>L(KP;kAF~*DtBh_f}%MVliDi~I1~WDxo2P0I>60b zLSUc zx{3;ylZr<5aeMo^esPC?w?vP71Uk?!2<^-##IV?i_{ffFVR|vg2+R7S+4%t40r0)TPsy%+OaeMkH%?KF=cOpnaLp?qg!WW0|f)(ES*-P)igiR#~_(USbt2cMKmv19!hJJZ}X zB8{*nDe3T!iy8~#gLt1Zfe88HPR`>C^dsco{CsWW-j1Y>ej!H02G%4qT}rVEl{&fR+)o^f3?uPo?(v|mn?t%edUfykXup^?V|@MrLUVK@?}(85_INBy z8pC4@YGrX2tgGWBa3H<974B1{fMbQ7c1oz$6cO4b`bxBIKM72rWvbQRR~n2PtuDDN z)p8(8OJSc8`*pmHat6}9{vY*20HsLtGTIPUgE8592JFj(=H}Yuag_vPusiKvG6P1g zO$0YlzZSClbYekYU?~4q{}k3nx#;e~3_)=q$Ho2#7ne9tw9s7b+lLn6$~DQmDx)VFpmn>5lrm*ep0~ zSE3U4#v7~Nddq^u|LNors}`$<%9uFk?vgQ%%<|YM;E%U=`=a%8a>)@RFz8QabL`T; zTHgmr$TS+}1emTRskW#k8RdUtT3t{9i$o^xc2D}L7AO7}nBC5K>Z1r&`FOgH{Ek4F ze~wU-UTu|@^<%@bVE`*^SNe?kHWK_&lHYRvjt@JQwAdsDR>vN(5_p24*#Yt_q}uSV zwHZumbV@TEK>Lw<2~Nr_AWnzSm8W}!QcKD&xtdDl9J~&vz%1L0d4m(uYsM2eqUA02 z1&SuIF}992aqH25UMN~^A0jY<@8qI1bok}(igMExk&fbT_iDZR!Uoh$N;+L{wYAEkk~K)9<6pM2v6soA6aU%g82h-wXjkARS4B)j}Q&-c`LPG_ci_&6a4 z{Sar3HPdyUp9f@>MpbJxTv@A71m8r$l4IL;yOEhwLBW0L0;Nv&=` zbC!h&{U)%p*d~K_-61?jSubNBwn4Q%_EU44`LHY5?fXoDu?YK#DiV8%+Ua7D_{zmT zdbf>m|3$ET1C%cBfc}o-?7Rqv03X4dOdq#w$VeY7B-~9qEF>!g_%*>Vd;mv|pdJxB zgvmxXSnR@^RTA7rm`zgxf?0K6L|u|>oBvztG0>=&EzoF@OS5Nc>w~#s119^6)zq8o zRi+l6@-35%V51K_A0cDJUV{-pWCKs7;E($NzHdU`6Si|pG6SwK$@K^>2p93QE{%Pp zV3Jk8c!m#O>I8#?b1E9cpWAsP_-#UQ2<_i^o&|;&XQe2FW<2LssJStBJ;6RYvG``S zsS4h4{A%{>2;&5iY@a(efEM5tncA}JEV>*>BX>AzDBxX_&XU*+zas>?2K`6`*T1uf zooz#5EUHdN^?2UDQOO@_R@pt3Tq7;esC61B+^WprhF$yX-qQ~%!<`;Ak+NX#=j z-}{fizf!z+G2&Y{z<`1nUp8R}{^>@q1+MJf34=}zGJ(-SAG5;(* zx(T_5j8x!P@-Kxw#Tv*Is5{)goT zhf|tf>)oCt$MPDJo4?9!554^;Oo5A5DP!BPfn)4P1nTrJT>r(Bay| zm6n-zQ^z%>)T`Q&k>i2Xr1`OIn-c1=;XEfb`9<|e^2(xyGt53fN7Kxyw^z%HM3yV# z0e%tvUZEPS=+zv`HmSLI3~Ku%GhF9*AwAykyx*vQ#XIZ{qg&oPC40sU*%bb&zdB&p zuaLcVeX(Xs12^-a@0^V*Q2q^j%(ou)grwX>fyH+fZD{Ca3Sf<9;;kXuhES?THG&>`ys;yER68$}`N z8!6v0s8{Ag%&=gI<_VMW$uT%X`aP?=xPIXs+hkM%Jx1FvaCPX^sx zKiLyY{?%!`Lvs#WoSK3c4`sgQ7?qAO$SLwE^8)-g%-HLC`|sDp%C^j$f8$26#ugUMuxP5E1Z5w7`~ZE=FGH_U7q7R?kZv`bYg< z^3?`x23IOcgqsu8$=SZC<;w%*Hssq*0kfz^z)v#Ag|AS^?|*dBX{F$d>M(6er4^Bt z{A^VbU_kk_jaQ|hTp}e%{6Z4wn{1Y`Ldrd(jFtQLTm`t2O0T5ylW54qI3So7@!;ptej;Idw+%DV^%|>(A90PZW5Cj>6p&R zGer*6PP9y5l+HEq!;@ce`CpgBAC2*C-5076oI|IaR<)|kcQom8Ahbp5-YU@g=Au+& zyUqDH2d6XtvtZ9;OFm_z-LVMG&fs?q)M}JhQd&WrsvdZ^Z#(l(kHP>D>fLIdT*(`WYrN~jdPr=JS|Kj zxXLa4DLS{pq$xXbLZdt*BmhjvM=}x`m#DVMU|TD+jnc`n6{+>;Mt{=p_%OOOA~7;7u*GKmigtx_X^|HZ zAX%qe&^Mk{Vp?U?(<0*;PP0gRGD&+`3YO0GFVtvM|2$t-X1hSosmZf)dvu2@F!a3F zC$q5<>GNH>xx+Z=h`8Fjy_MiiWa#@h!*Njg@o-vqf&7ykkoMgmjbY;*{1O}4vxfui zv4^@$E!pb=Q?XW_5z~f#AhJEg*&yiwwoxuEXqJZML~nSyd%I9~oAw?`wx#cY=yY}n zo-0Vr7=PJ1rb4{M zSsOG|r{jA{bwE5GG^IMENW+g;_(CcnPsz7B#!b&~`hnS@%Q71kbBlgI$W(e{JVxMX z8C^(yPKvW7GZBF6`*p6-SOwI4gU|vl@WStY!hUMd_EX7y6BVdQH(cPT6oNWgpd&x_ zs_&#cT1Bl*+v~SX}ooz z)+#cN^IX)(yY)x+E%$}<4dv_H#F6ME(h0YJR%7qCi`9!>UYO-v;+5!Oq*Jn>Z0fGO9Ck-YFx!vhJ#T z`}~Q`98ePw+ABFObaB2k-7U*7_owbj!;bJAwV(XS0BlV>Un%SZ9^#z&0`sy^Tc#ZH zv_hs*^brlyNv+^xV*85dyj^^VneuOuqTp11Hn@|_&l3ayhhkxv*PpISh>pDHr|lPF zh=V2jTPHj6Cy`~7BbBU7q#&Gg6su$=mgOZ@*HrOy(5pad5!9!g&mp=Ksm!-K%(u*5 zL(K0wrLYYq7h? z)l@(dl~e2N-1Md1q-k~A$e-%>!XXqJ-BXl?9Py+60iRBfq<=2aCzDC?8Xv)an23-FI}%G} ztcN=7)}V)MMYckntk4W?NYNDhx#en?YGZlf1uEyy_5%Tb&e1}>wTr^LzQ4{<5$^3X zHo!7B7#@_8Iuor}x{Mc9Uv?}fmC_6oX%1wckRBy}P0>2?%IxRE8 zM=%FsebTaY{5=)gXWIr%^JvbkMvV+F9n!Y+2~sQq{?WTK!AXN~nYj&gge+VqkdxtV8;A;8TR_gstm_ z{hsf75jL_f`da_4xE@O{J=ri48<5Ze1VMI@=|^*x;xMR8vWauQwj;7()AX#r;kY2~ z6QLQS+Wd-NyW?FJDbePUdUfh@bMZ*5H9zk?=X;ZDo?ESL8n#28Vezqy0ZvK+oghpIjWUCQ*q;osyG2WNm zHecAR603qFcNHe{53_4nPh#?@WnjpnMS%^=nlX`Iz_tW8osg8s#0?>YAEy+1LiEpO z5zXR{1-#*phk*NY1=9k_dg?61i+v3AvyuTW#H4WA;&U85{ZDj+xVZ+!0Kv~nOyw!? zr?WNjXO}fe)S7r!tq1fWFui&>P?do$lwHp}{w^jsI1u zNU<<4;FV^d1=!w)6RE~{Jws7yt`H8ZyZ$<@QYe=~MwhA+z6XQ6t1%lLS>?W0O>N{7 z+UV1eZZORwu!6tXh5K#q7F(%8BSyX+R&$M46ccSyI*PG-x2Nx-60mu5IAsQi`?!0Xs;>9^K;HKm0KRZ1t~t&1N5&w;uX2x6rTZ06Ghc^UJb~%e_Hv!-q^{vCgW@f*zBi?$YySiz&Ci=S0ko8dutCjT-IM)~$U083&vEus7|80&>aOW68lE*+=~7Vw@MDfEJ}%7z+M#$MSmo{K71esD zs?;&-W)9ye@U^-9&hs=CNNlIiAXIg5F5STR-qe^F#T2H9-Wm40enYS|24vj`Lx23 z<^%^;Cn<7lNTkgelnRiG3%m+^>rf;7EXvlUq}7W2gjvmw>1&akDN zRu{hZ$fE|>MJpEMG!Z{p2Hg>j|F{)no%1fv)iDh>N&Rsudoh9pdX6JB)AtSKoVv;j zg>m2)omVF2Rq7ww6VElg%dgRb@Up<}fSqT$b}a?h3f@bZWgc1Y2CGBaBHt{_JXT1c zUWIE}kp%A|%NpEntLm**mMbV#spA{9G=g#c6S+~^CAfbP_pCL9ZM0a!6g|#mZLkHo zPQ#io-Spg{*bZf6g2dAKa%qC-oJMbYCB-)=FmdSf zq6f`R#~Ho#t&rFBjSBWCPVW@*m#S0|3IY#;e;(Bb@iJcr_0%~>wQyx^klYrV@Rn}v z1p@N%0pb!iw1#0T*xw|07Y4ZDKc0AQyFvNs#_UP0ti?2T#1vqs*;K5eI1JmAV*1Kd zsw1f|kSprXSSx~LvcgREJC5Z!^2Zi3$rKq0$$sob$P?;32YqqQ=T9}PTO;|a0+1%F z2;p9+2f|gipiWaff5e;AQho@0PRe$uSRSH9j!1VAo2s;G-5kO#HcgZE0=7AF#qfsM z9>w&vXyye;47)d5;S0CIx_adxw?iO02JNwx%Z0mcTu5|KFZOM|Z}Dzr-!?F9^F|SW z=IiZAr6QCup<)^4A-HkEkmmhlO`fb7#FBCQ>AxNjRIVA`b$obz+9aw{aCcXpbB!{v z<&=xHi{S+7_bCW@H_BWi*(svhpx(hgW*XZf4`nk#w?RHhm1fJL1$pt*WYKOQL_Zl* z_q|bV|L75hnnsz!1J@C4GL7ll|HigRR-|YBAeKuIBffX41+TobvctYlQRT<4R`spKEhN#FQ6l$LB6~rYoz)ets+(u;f_CPyGqv!b>XMYD`i*=2^ zpUg70SM)hK!W42aMezuS;nTGiZ{4IQP#$@z-Q$ToCmsKUVV5oT zcnp+ux0GF^X;Qi&@pIhpT9M0O?yi4!1mL0$Dez8idSPuh)Hopb&)J>Lp6lDgxOx5c z&w!HCgIUG%1MYBzg<5n5WvWcTz^W8R1!pvyd|)uYFo7{z%{QPbLfftcf)``_w3HLp zwA#wr+Hw=za0)oaX;WvlMB48v&0BSc^m2@5S?xH@ee_CWolh>&YUvhpk8J%|o(`+$ z8fTt(iG7*M&nr=7Ue~PkixchUk#j?aLZ#5-TMD)Oa7c9*ZfDyBw|mBBYk->YO2%+@ zPHj|HYkJ9h>#LE6s74#owWM`>*p~Xu(moXG?Z% zCd=g$x*6!VP`pJ@rc0tm8`m7d;{%0xX813=OxC&DJH*2ya7MtEm{mHa%_De_`w_t* z!Tygnd5>_X=-dw#dR93osHqiI`rmHj>}Za+YTNH>$ry)>~k{f%Zc$6csrll&ZY-w%6{+E`u^yU_nXu-?rGA z#DLL|&Yr`D6uUG`D|KdR@A37+EtH=P26XIx3cQXt=v4}Qs_QZfBAWR5vGiMJ&ZpN# ztIEB4paJMVJ0nC8KDYqMK+{v41q#0g*?c`At5}X<6E3E!X%J{Dp@~TI9#fstFOZQD zjBF4H=TG1EDU(h?(jnentVQ&>Qak%L<0A9T8ezSO&=mn>X9i!EYtw4H;&@mvLIbfH zFrVW0mn#u)0vXMjUH*`N{s|wP$GN1JriNlYj*1*xVw|#HYhSt@pWRO~_3>LjP~s=V zDePuF{rg?6!>!44f4$?qz-YoN%mczY3u?M}{U$p?7JuBq2?YD;sozqqeL<0pFS2#! zZu9pl+obL|KrJNY34PRB{222tNOX{|C{|%y*%4^h#5&qNR~&*{J44Q!>3@Wuz|>%C zgB{}xw)bxpTJ%fyI>tvv&b)K@5QHi)Y3~x#U%v&bAu8n{yP=Ltv_4nRpFl{GICB|) zRqvwg2IIMXA}~l9BuD5q5t<>GYNNalh2j|y(74m@k;L_TxEor-JEWVPk;X<-I6D1J zz@*I0myym4En;{$j)%&7aA%nVL zzW#_pJ#5SUEtg!oJt`=gG)WM||KrEqH(!`um>y_e@MR$J-{Wu9TtAe6D(M#S4h0&u zKJ_Sf?MB05iRRaUU(m889Zr}NFMK7ov{>+qLc_KP-%O7h(F*{H4fFG5>2$iOS?T3G z@>`GZx(7&JuP#u*TDK5&@=`H$2s`-I!+C{2WBrjr=oz*;*yKTyI z=gv8*Aooddicg9?$XmQO)Bvj}R=|@LeW;vp3rbYDFXCUmlkKA@^^2-qA;GXhkpU_ zi%sI^1mMA!IiJ9)$IrT>*|>&hVIz=KKOQKYk=;7$vavK1N1pdz-XN$0+(szAj9Hl7G^l^-XJ|f%_D;&`$ z)iLnn5j)u{#;jTK3&$?=O#YMlhO!HH(CQVygXR9$I`)Bdztmi*&9{LH$ zONx7jqb+=%Ip%(Xf0BEKUxFEkXM}yGEzxVNeWnY+9_q~zfXzG{7U1|-8uaF%FCIJy zY1pNg@yf9B*I({CTcp=mS74v3I}Y;*2!J8dnRZohfc4)nKkr|aQf(%iyAlJG585H^ zUa-evl@Bxf;#@EC_k5G#s*dX{;v|Q{-}CTGoxf}>@rHo-Apmz+BjRUEuS%z%c#cIC z^JI;Di`^{36k)R{kCstVh08dieRr5O+nQ^uV>99&6yR(7$RkZm2WB!%Yeeed?Ljx`gB!Q4vSGf0VQ8tRkC8eQ0(qW^vhm%CqXeDNy%lC# zOdpRzQ^ql`Sdjd{+*Ws#BC8XTI0I5cvpLTBj~~umspBvIIPZsTTvvKuvOCTf+-s~U zs$HJ58S*K9xqW#2eDyEiS?##@88E zxW8(Ch<}npq?bc-U{$<@cr%X?`Lt?Hg1LJFaKf8GSM~${jI_tS9bk3*?S+rbtmw2$ zoe?4+6hxh=xBxcofEnH!(kUeJNl)AkZ5=SUEy@QV`L_Y(SCQ%pZ;#r)omE_xXO9UO zCf_8{Dal+LCfD-^eZ0>p_Cy|Clo{yRF-Fx zwK!3eO?ZNJ_HSD!zcv}&3io1w_v84DWf}8M$NM4qTpSTC+M=AAoeer8DHi>!6UfW-xI?~Gncn#CUq-LWLgh$^|p6^T}v*hDAa z9@PboLve0&Wc`~|lN|9&y>ykNK=_`(X`6dx>hx_Xc)pR0b<%6WyNO8Xrn=BL_@5=3?zbMwLv@&shB4Vp6Uu51Y8 zgbZlonJNl)w#BA)zPh6qzE?g_)Eg@n5F9O>k^VoswXdBSQWZzjs+@hI~$E^Vw2tZ%2x-kL~Ote!q zA4np;-aY4-ZkH`gM0Q7ZcOqmP6`aI#7!;K5T_ovWW75;06EGpIoYaI=1y6L@se?Uv0bcr{Ka#X&y zbx@9%Q$L}a#05F#ei+Y)*{8Z*tCgL2IP*YpLxF8sR)BA24igNN*yi<%XFoP7D?)Ss z*k6V2c&mT@5Kxa9AXMGvk~Tl*>=6_*q>O4seZRH)w9QmYq1l+x^x9}%s1?ci49v98 ztd{t9P&%CmGnuZ*{s{4Rl7qQw2)<7ZxFSuy1A{3x?L zxH&)BEjPGzd*2)-$$Nbe(G}haeUMQzkPd?r!NbdxAAG6zg0!peS4Pu79Afa)5W4=x zBbOo~%n6e@JR2{mO?S8oeO(}#DVH)x5TO&b!u#suwF(WnkvRJ*Bn9-J)F?Y&&YGF&KXtPp?x_bn~So?S73gEFXF^B zXP17~M!A$H_Q{HJAmbEeODH6R*kR3L3kw>Shxeox6&c>t0W;U_d^XomYpHT~&DDa* zOj`rO+Z@r@1v>z2HXviT*p3_Of^46G?82t3ZbvP4{|EYH!Sx%@ zK7(aNk6-hhvxnPHO=-35E=W~I?O_GxhZ}ij9LxNi+Jq~!S09K@RQs>0FC@o|{p?On zla_(S=Y{5CdXqQ-exFtNl>OLc7R#t1x^})i!6`LdONuPo)Y2X|7-Cbtzum#mi&|kF zkm?#)#eG2%0;H?#3UpdKSHPx@%bT3baP5r5u%@k0&5z9wN0e)9-AbnJN{9^tqA3U zcBp;v7KBC0L{5oC_oa?We{8B%KB`0_@8M6d(Yr6{y%aF9(TKhf(J*4Z ztFKXNHAgwk@!bQ*_}^@ily~Q;-k?09d}Cp+-rN_;l#(93Ha>E@w}_KKHKN^{*1|np zDDZkSK;C?!T;mjpwYtOn#6v$f>2D>Cm>$7eNrxJ3GnuG3=4(Za5DkSTvX|gu)bE?S z{WxLmi<X_~x)@~BCC^0U%wD^lFx{6bOQ`&mG=e@nB6@>-maYs@PQ`9Iu4 z_`$k6@YbE>XkeRC3jhSk9)5WmO(r7e_`FzoM%GCm((Y-REk=t+>=O(+7g*?DaPIpgCgDH*Dcab zs<(($gx`HqDK%r*|6*XnJw##$3H2x1O;d^f=;0hEICQTlsQt7a!F$CGCpL@F;pmHL zKC!|oCpL!an(P_a(xEEKPO99ZP-dK)BC>>xz3?@7LjBH(KeiDNo>ypz{E^5PT0{)l z@%D>1BA>V+R(&F&mOtqEW>K~S+Us3=#CJtpfYhLWE2szc1Y~mx-K#-12Ik3-x=&n; zi7S720FL$y*7LfG=Q&Wdgkk{}>bLLN8k&U@afj@g&IZjPnh{f@Me}}uW57T7>=Wvn z(iP~833H7`*^PO*S9H7SVoNpT1^KzRVZEZ9%=fw|9Z)u)WV#Q%?cnDlQtaDw~@v^@G|_rMg^ z@z_dBxw;BZzfuH64M9;!OhTc6RF}0;mW8B%gr!wMg#|}VxSCGCx`L|kd1glN|Mu+r z?sButN&J;(buT!>^IqMv%xk}^Sx)|qXH*c}m+XP(qg5?a9c3Ei0J9H*lIKggqxix( zP%~D_L=1uy)h5s3fi$%B^;2LSYoFxb5Lu9Yg=mmPu}+H3B<&=Ne)-gj4?6MJ)dvpx zz87-5VOn~g?d`SOeaf{Xen}>^?6T9Y%o2z6$MHRvOjJv2v>1un@0m|3y{v*?AM)i> zGj`Z~{bX8Qif6Y?d`^h0mKBLG$I4waRqdjWwmMmvTFBh*VckR`gBCB+KTH8fi&H**NT736~<(L{KR3Qf_Iiw2jdjc}bh!lc>_ z3rLJ!ubu|68+;Ji@zVVb@X8SF( z;`)Scf8&|NL}Z-6uzNA9Fr}(Brofy}4E7z;HN^~yOK8ix6N)i0l;zx}6@^PBosLUY zeD=i)jxj-bUSdai9z>5~uT_p>3(gtXcaK771^qSuLCX76e|AE)e&;l^Y?GuswfAF6 zFv)k0wO0YgXNAf#N2P^(lwp-)5!X9j`1?fQM{EczlrgPANR63+=u!D9^bo#8c8wV_ zbFJU-W?S4Rn?LaiX{U0wyaIIq)AJyV?xufbU9YgY2ZVD)-O<{J^{ zMDgD5YsYL#gU`ED?_`|IpGEB=!{&T7B8p zhtoF}+g_?ch%vbP6G5su-AA{k>8W4bw+hBK7xLx3A>!ZG5g_)&7Sl=%4|d6w(fn`vfzy%wd^~+suytcUV;6E+m~$AjO$dTp6OUA z?GQD&jcygur`Fl3jcGN)zP5dT$JIz#!MkP~j2I1hvgr~!yrIxO&B8qDA1cY$^BX?X zBr4YOXw#{a7T_2MTESbu36WH1mzz*$hkU;2bY-}1qdgUP1Bw~V<@zYI@`Y;&tyvZ7ZpNizlPY|QR! zGHg`YT)~e@s430SDtAUot(;g|o+-}Mp=^!bAhAxc#yZ4FsPnyJAF~hn%fY?A%s9>{ z+k5vza3Q0OiP<=Amx;I?k|nMR)8h2jvlewt3aPOPaZGbU3$J8q*0-A!@bZx;Jkm76 z!8$|S(`$umRNN}NNt$3C=dy#=7*eEKS2a2LTNPO@KKva9|@oP6Y7ge*1@fa5?@&>shY&c8QdrPo*zV%e8v?*ZekSHAp9XZ>vcCS>2bP1+b8iU`Qhu&?38pj%UpN-1W&W@{uh024rm-kG7-_S zazU%vA|7t=Z&;}JQHU4`*YYqEE&{6$|EMm{!2RhBCM~y?LBd1&?d?6B7mwHZXQSg^ z-U%?dIZZE#Pc;Kbs0sUstVqnZ=HaVpGLXEkKYyU5jKA;|IV{1KoEh?C*s>`-JOGQd zJq&~v$VnD)c^#;OdrC4LXx<+tWkh(vn_nvPEijo*(I7e26>rzdYVZ(ju=HkS4CAF`5GfTHd*k!@> zf9NsjO>K9`kMq+lhWkeft4^y;i$MzWP4UX}#QRGNbj5c_{^M4x@86mbgh5#A*hWBq z$woj|#)WB9oswA@;0K}jyF{&aZv}f;9_~W2NVN&)4Q`|r zCEHT13^bb#)FeJZ;|MWXAH%gVD!=%gS=kLN&cXK>7Q z%UMZ9+N{ncnZ>@{w~rgNCjz!^)%scWbvIIw($|ipt0SVu!TrKL9j~z~_r7t-lE$7yeO7%>?@}`_Z)xKB1Z0;rXMIR`2xe#m=mPh`|DlBA?%_RIYk` zmFgOSMpSe>@~v(CIWE4(xxfR%!>tLP9(h(FyeqyH@NZYCDi3{?s+0C~-A}%Pcc)cc z7ikWh-#=H2!IGfw?eMkF1bb(}F^VMd;VQH@Ro^+cNM z$1I7#m;V@xe(O4Ep~A@*{gUZZzs)*$tj1oGF3zJ@_%IKKSG)OwBY=pgi$=KMmo6P; z=39N!ohJ||^40~VHpL@uzbC~l&0dXaaZ0PcDxtvK49gO)IR@ETg%!}wBX)n)Y6%R| z-CNh6zZz-v>A&(ym8d9J5?ts~Qm-oV^hr{z^GiLRymo9WH~hH_^wXaz)Nu9OrqIs$ zUi!g1ZlBaBT%ar8e1!G$@SKM2<}Lp+l7l6_7QGQ^LJLe!PJmvANoR)TFRwhG-xb#m zM*(Lg&%h%C|FTI1FHo8(0!L^r!bFP&FJ);Z71sI-z3Whe8ss6|JiPs@6tggLl&Q_36F{O(3Mt4>ZfZ&mcVjnGY$`b*`{ z#iTSt@i~YcdOle+Sgq|tOQm4yrAJAZa{u>Z?_%?VdAdEUP5cNCVVh}QP|c{s{Eurw zkNhdSe-98ErPb@-A=`j*b#nOpq*%!^7VY@N$m!&njDw~^-6C(BV~ab(l2A6ZmRE9o ze0V&xaf#=9Tq8O(0j=R`FP7fx^J;VKxI3o^-`W;As?pQAO#u~K>QO5bEh8Ht+_~E{ zQazaUitXPD|KzqVd2P}H#}*IpbHm$SAg+g+ngoduj!=-VV*avhI3wc;C^Ja0_&}Iq zcPM9@ij{-8FV{2-9Yl(g>>t55lW*S$)Z>L<{AzzNsJwbBqiC{S^K1)hQz<{$d-o!j zg0`#6<^rg8KEXJoY75;z^X#jS|5`i4&9dJzS+>zNx<&|L`=Fi=?i&+)IpJ|?(@QY# zQH*o9&sNSt1$M3zuLwnPrO`(>OS3b_$*sV((58j_Dbq%x$MbLmIIi0yV%&xX-vt@_mMiBjLI3`_}XgR>3ul%SdyH;-Qsy zcEP!w{0y;W^iieA4^22b&OSbMWD%xH^)~lf4 ze9K#A@kGz2yQ+%ES|<_cRnYglF2-88afAu+!<`%(s|=|A-|Z+DlJ#ms>4yB&wusb< zI+dMshD~0XnI$PchZGOIfH*kdpbhP_!r~hg+;_iz>FbMDDyKa zaEei0@ahU8Ev4TW*lF zv09p#LUy`x+_%Lr$2mE$-}UQDDFO%Ndrg>VCSvU_n=W-Zb^GZ^MlKTxi3tcwE=_Ka z$g_`ZFHTzFlj(_ybi5rNFk->lN*qOHzZ6VQ6)rbu^+I(J8b7#&^KM8+@MTdBt#*59 zGo2E2#`BEZXZQ#1;`rYYDAHMNU1&W!n2PH@G;+U}oMF*4x;y!P{1PdzQ5Br7lrWD% z2vTRNPD`y|QFKQNW6WF;-1f;WOErhe6Vy0vdgwJstHd{}On&noVkX?rS18w6BopDo zj{3W1gYNy*o_D|WDeC?t5+BIei_CRdIPHN{)y`c7jzFA8!#Cx-!vokY$S)5};Ffur}gGooJbCSJPxPOaY?-Ii@(HQqz>2?Zz<~zFe z(+{b#kfpRvYZ!M5eI;@-32$f%JX@m5LQUS$m%<1FTcnWZzTAUxdU}b8g8nod6{*bW z_A-~hM%7~2MLChUH!ySe5c@!rQj3~nTAFW*!sFDZlhk_a+tat3VfdDmGZ70T1ul%c z%6CfJE;`IqqoNZF^{HRtPQmviUAj}Atx399^zI4i=?V#HJ<*}rzC~g3RFA;i6EUr- z^K+V8qt!cbkaL|p@E7~+7RM?}j`1Mf)!>x&IVtDg_ixV+Otj5Q)HMA|z3aMw&PT(V zavgT1^Faj&RUtODZ@{_f5=y%9N;dI7{HlXZ@D{hNV4~hY$7)5BzcB+hFgLezfpOa-a4Nn;V z5F1fiE^R#TJWwy4R5))uQOg)~8N<-Ctw?i+pC4x%#o6Ju z2=X_CxnWjZqG28d>IBBp_0||nnk{iP$~czp@VfaKg)xoqKr}Ysb8K|sY`5cXX@Qu| zE*SBBl&Zl`o~h4#!F6b61+YbA!KvWy*=0~KOm1{Ps4yC$^e2s;F8vQ3C=~8^iK;s* z$40xg6b5`?7%q?!{K6-rnGiX#y$qWz@qPITd_h1LuJ2z09zh-uG>Mi_IL1LpPyO4+ z(wa0_!W?7*C>mYT%g3 z|5Wo(NqPh_qkR{f?8OvJ|F(T~=j-a{-=2S98Fualh+g_a_2OqeoFlcq1-{^9B7toM zNYfBvS#wKFzxOEUJpqJy6d6>5 zY;K6HF>f9`^Iz*$e*^MieBi_nr-M>cTd+Plp%R z2W{~=MdQl?9j(IPPjL+VH)#J>35v3)qq)YlvWhb(4L(30#go?hMufiQN}=e?AzahE zgMw`p3qd#gb3Vx!5|RVoff7YEwhgEfNdPtXKcGgJBVei!e0dQvQM}ls>fXJT{|40j z*g?$df6MWGUe5(t5L-&{sWNqc)MRr_odE2Dkjh7|`|ciQRv=n6si~(T+xW&V?%L>$ z+FaJ4OYFwd6lplC7 zyx&62B?|Uli{9&|zwhqx%&!kMe^Bc?q50e`&YW*IA|o`G$or)w(sX&Yb@+S=1n6)6 z!?p{~`)bdq*%(wSNH9UUpaY!X5bc@%dAYo+dG6U%G%0)U^tTKbDd2T#Q(Y6Z4DTm- zN68k2+*%~Se95&fSqq0-`u4FeVSSbDpc`Fw)!0Envqe7jdmWWJ#y@>YCGoHPb{gMqr+OW>e zfs7_+Z_BUK(hu=;CcDfNVwEK~lpx6y^8UD3eplmCZoW*fO1n+FB?4XVu$B00?YL+3 z-5fH=ZkYj?pD(T$_KdW?`^Perj1V+jNGtziT=kZXri1 znlpSh)r#ERLpf$pURaY6+csrr9lg?dEbZHj%V)AobbP$Mdsn_}luWb(`{hH)wVC`z zJ0jXnUTTdYWm-jMLC3@A(BOGqt4n`!dkmokv|#AS4EwziKYxcDms6==JA`O%KhxKR zO9aSTlGNWJVpaFmuHSoDuwQ?^F^pQbj-qTNZ6&mQ7)kx~ z`M94Fk&W6zBRK^8spgE+*d?Ily%Mrj)`>JD;dkH048}3}ULPBokrtz|Xn;L527Pyr zd-29v>!&NvtKt568~rwD@9)tO9Cg}A?MltKIKW8XfcvsZX9YR?&BUE2n!T`*3BYmQ z0^s3%Zkf4A7G!?dKOyo&nBz5pnx?sXp7*!RKXjRbYJPs~d(R|DGHgKbh@1SpzRij0 zIk!zY52}MQvw$d>@!Vo=GG~)P=^SQ?#4HRVi2pB)JC;pDv>NT&V5M??R@YZWOuWm- z&pdAY%ApmZo{ot%rp0AO9IMBxR&De(G$0)LY65zqct3WwL%WsN2z_V9s{8P4 zV@Q%rUAFy=^)Y_-FPpp&kJ2x1S&Aj@wTHJdktgM2oinl)@%t_(<@j8&4xbvc%wY&# zRr#;4FN-RTn1EdD6KP<-@1GJyuGrR3f1A`*(g6~&&Zkd?*KvYedZPeCZEAZ=Miro_ zs_hjZ*C^OereqfmwaLmaFkZ4l6yGe=v9ZO4gv!R=GB0Jl-tm!ZDHCym1q+2cS%hqe zRaunZ-RpUz&FgYI0K%wTZ&06W{oB|q_XfrxI?fk0M~{NOdT{4C)jAMCSm;+|oUo`n zjNp@l{@#+}7be=G*^|gB=yWgg3;3>)GG4J=l$MMp&GE0Uv9w67F>IH%TP`Ib&)u?g z;>+f~W7KsN{D9+-bQOwPs!F1g&2vOHXeD&3qKJ{e-C zisXeKcov>m+q4LfLo0krF)o`_!`vG6pGQDvP&&${LljhiQX1;;?R#CchL!b)K&}`$ zL4H=p2Vyhp#EaO2(Q(6*W|VsqIYF=f()1Mb=t^@8Z--r1zVHZ0gkf6z4C50)s!o!n zfA`@K@n!G~F{1P2?14=ORNjkk1ogg`%IqTDiZ5+0~U_!y1z;#Les z61H{Mr69! z7kOgnmbhalujtga>!QiZ)Vo$u25iN(UhcvFOt&6gX3z+yd)bXa=Blzll@5wilN@7T z<9#{=KV4_&7W>I9CC-;y0o)$Pzdp*QY_VVFw#~KXJ21yN?W5TjFVOCdsZjwrHV1589zlNVbdd~-C$A0> zOIgpg#_j5PT?LihTG&bgvv4{Q*khgzF^lZ?3aS@{aVic)9dG`*5xSxs>|4q$+|%Ri z9r5Q=2A#BJ^(M;*;H3@b3y5H~f;~T;L?Gs8ibVAu377(5ugz`Xk=KUkKglxV(%`4L=xOspLp#HM_jW9HjTt*0{UusXMnW*w$Zc+qP}4*j%w~+sTS;+qP}nw(aDNPR_pE*Lz%yA28c&twvS# z-XEffV`es55<^)~oIN3MI>my0+RXYWe=Ecpl>V!OtJGRhe2I>fP7JxMeY*_u+PF3G z5H7OJ5S|(&ou4K#mAv}s$o8M3a(y1?3ChQ7H@6+%E{8DRbFad;$=`(y1Hs*sKeHK@ z-{se%I2HM3n~9XEBp5aw@kg3{L4K$>MX2-p%x2=--ZnVh$4Mw{cdMJH`xu6Dw3YEm zS%(K3bd}L%k!El}xK!QPJeFBgcz9!hI_J;jj@8IL8T0&!Yd;wD*A!)DiqeJfggwrYai1T^?Y9T$3ws5r?1?n?6~I| zQQ64^TBPcE$w=A&UbRuJuLaWl`2AC@f;uq>8J=A0yO-_3rkL{dwwEkRqG4cbb2!6X zz3>61T$5{0nhl~{-!v;Pl9DvP>_gsmk`VEq+AYue1iRk_W9I` zgw|W9b1mU{x&RNDnax%xZZb>_V|+VM?%l1E@>w`0K=EXB_Kss7A$h{KxAQmR;B)1J zov8oCQ_}w6+|=$cqz%=aR>A#)IBpidx2H8&yy4*X_ci#1JP5k8a%jfRWUp(Ng8jGM zYYzyV)272?K4v8howCJYIU+*!zQwUwZ?`gqP1+DY+P>|TdxL!i@bvx*H1t-alCP_m zxWR{)y({dq9@2A!DWgvGF3h_PkZ?X zbe9|OAB|e5qd}Ze;@uDVE0kfdO1=`$Z7AH~5FhPinZ#vo;q3f-uObZj_{}%}?n*cW zR~$o}-IA0h*ij`IHypD}0x6D|2JqHT=Q|5)+^pj=!QzLz@x>Ofd*^k57T8#7_-d&I z+@ghPf%Q-4rG8qq#uWPm7v)xteW^;gGuujmBH+Jnm1-5qwrgd}q1B(ouS=7FK#>ip z}W;QVktPr>a9D)Pd7D#>ykGFSs1H2+X(zOS-*T+6iV6gCJ zNC&{I%O37O2je8ycX(9e1l^Vs%x=X%SMsmJT$`i`-Tp3bY*Gx$;mq%(BdcGlxYqio zITF^<1>)7d1+rP%rpS!Dg^*;^Z+Gct(+>amDdc zL=iTgFcSh0EQ4Mt;bIXcO>%?MG7V_q5>AmtNAH{U;ImRdjdYUbaQ_XJDL&CX`7v^S z&1Df1zG{P94@cZL{lxKC)}*nFQ66Cg8q~>7j#zq&;{8cU_yigRlwTQgC;dU`*Zm9kx<9s zb#>MN55lJ*>N+{E0i6AM}P<%PlC*+9O)M8WVJgh<2J%uTrX?>CW{mS0+5v%LE5Kojv{<$%$PXg6GuJTQWa%Ze)vd!I8g__<5V(75ex-v;GS_Y& zg41sQxB{v6C+TM%#2MihNvz2wvCUGliR~QvOQk)b)UK{k+od|nK2!Y>2;Gg~P$apl zm4h-&u|hg77Q-&qAjAfBvdrfV4|l(0I@a83&8x@u2X%p$Ya7U)KcPnk(E&37ngwBfhI`0`^7DBjuDF^+`_o8rPC>B;eORH+e+FeERMN zTi0-85yX^!?P~CQPpB-s=HJQJ-KtnHHhG>lg;-`A z0c%+jLOmF`vJ!~2gDK9E96iy@|B~#8#K){inb4uwP3sSq?cySmoqsC7`nO2*Af2n{ z=oEx^+@M+`B-m#dMKsTjzw4-0TcWUw5pVd!*(8-1)Coz})lzX$oBJO=4a`PbYzqu zI0|*Bjl;Q;>b(sT7~+^xY#iZEGfFX4sn9E9?ZE&5T*>FjcK;b8?p(C;bL{-87F+m3v|PA4#d;g3)w1*n46f1 zHvcSl5PAH8zppSa(CefMls1NAz3WAGrdMa5)js7FVc1Zqxgexeo*OfU+dkCA<#s82 z`?<}Lbc))7G7HRbTwizJ@)Wqn@>8#IWq5+R1^z_3|>tksZcmnuWm`9-Bg$t1SGzdp|hdWtiS`gSJ`TDknRjE3Kf$PYyp;~rw zp`UUJ3UwS@#+m`%%j4&0%af0=SrpOrWvKOU>4w0-`E#5?rf8jls^qVb;devxp*|rU z7D;xQWyx4qG2TklWOGqol6Asu$!3~l7-rNP1SV=_6<;j!gR^|Hxz2cfw+SBZ9|@W? ziI(SQ_!gwQk5pLqz= z=w>$A*v6R7pNEjva!sEA|4*77`^!Nz<_VMotYgxnT6C&*;Uqe{T---np=@A8g@kGG zI54RS$<8xG@oVe=0 z?x=FjHwF=&oBjeWvQ3PWu~#YAohyW@f^G9dIS$D7;aNdP!~&fv=24L_Ikz-Y;4!Hm zM0>;{i0mS@DE(R^<#UkV1B}XSL}K4q(fo`!^m<18q8$EIklml`_S7N;1!m==T2`TK z5B!7+*tr!CAY>A~yq3}y4q0)xU;Sxd8?cc%gfgA$711QfXNKbjd$I5GcD>_-XanGo z4&uJle{DZEf^$izO(7@ndwHeOa6v4}<`&W_)x{9%+`9zz9$nxTmSUM;^9t#be5V5m zXA|uhTI&m@hg2J854bthsrC5*t#BjD#?FOdD5Aqr-^L2%E>CjY`Fc0VU1OMP;+B?Z zbO96T&aN&~*&(0UM!m!U3~{`p1zSHs<+@i!AAm==t90xCTp**GBU#ZcSYw?c4zS@K z@Jr&KNU@|;PcnJIjk8C!?@ohpO6}hxkLyGsxprjdZoEnJ??fv}8xs=tr4$iZpq!jfszN;dKq7Pnc~lsb zct2;lHE5jwMQYJ%%Om91{Y|308G8X=$NQ>Cx(Xz@>eH*Mp*vvcyJv6Lf*`#pihLs4 z_0#~@knu&nc6bTd*t0qHjbSI>$ui^;e|UNP;!8O?(+=3pQ>qc%34e)7OfL~4aEzvD zWtZ3>WrQ0`rbYT_5Bxe=m8Mn+;igW`9EE5j+A8t|@M(?s2im`09EEVo&MVIr`^hjK zUX*6so=P!#`E>}cPc}*zAeAaXKevT8$`vI>dK2vo?SkFIsuO^Bz&o0w7hwf+aRyr- z`pIb$L-H^#`Z3~&o}5CdYrOV!@$>msuqdo+M=?%_jh zv0Y{_Gj)pGojN(?Z;@36R#~cIJ6+tP@XQ4KaF(W_$iJ)+R}%$i}P|I$fq*oB*xo3b30m0a&-&60dmE|{5AX%`~n%m7{i_tJV_EJvdR7dcwZH@;xq{a|1z#5p)F02=&Iy@_N>tb};`PC5SZ6$-i! z1M%z(b&hh27|}AeS%#5BC%_vROp%I88UB|Z5bRE`#yq8;#rUOBb#e`-K2L_z3FZ!X zad3Mi^Kpe_iG6)K#vNm|hXVzw4~wjSc!q5~y*b3xrWoKP*0Mz=FbU`u=fN`C8Bt?U zqKd8;<_T|EnTc#{6FWLrWdN{D=nI^k$RT}n`aV+Zycrx`w+l7|X_X7MxrOIv++JGX zx5_9}3~qw~O)RUCms&_2Suyb{_4UWV8vwe`lQ`U1XIvMj9aB{z7Yi8IxT%=dD4T({4ff zaR>V`@UHL-$~XJ34;#eQif33|LhRxwCanFcWxS#u(MI?;=QbJKvL4}NYiYG0ul}B- zdW~`tbp{Ed4R22bXo8&@B~5n$9%Brk0Av@ZAZ{Iu!!<(bih4bT67bt4CUcZ%{nTD@ z8j58Vch(^w0P1m;b#f)k0usPC=ITER`C3`pJDl$ZIk5&4X6Te(Ek{D8UD;E4Zx!${VdSAZ@wf{t#HP9M;! zw4v<8%eobTU%rhxTHOjos)`N3r)eLCK@ah?I}zsr&=&0piW!{(don^ayW0ZOXIKYt z&dH#t#uyY6=m3njNuU+@omV^Zaqx7wgm&&Kk|vfal9YPX$SywP$#(8vmtz9GP!>ou zvfqJU1kvI*n@ZO{<}@b+>IA7l1f?2ZzhOqS*V`xFHs(C`?tibCXVoTA>L_1LE@3KK z5%ETGf9q8i@i4a`R}mjTj#)9oa(MQMjz-QOB?I?as0pog|b#%1I?gG6>S@7H*IR&k^JMe@r#8{ zW*Dz9sT1FOWtW`&h54-rR2w?FpvZ`MFvV1T!c1KS&b!vn<`TXE`ujv^C{x7Ga{6`m zNwSMB`vpxZ!%7ObH`cFE6y->I2eL-j+TL&V85TV~vx(n=^>tCp7hFm*5~uE5g96q> zS1hy(s}E)7TEZ>KHptIXsL;=hc?aIWi_?0$qZYADRj9NJWSO5HxIH9W5EzBFNpkTM zeqsU+#=S~_D&0@+gw$4JA$yxSmY;VI)R=ZJ30jfR_II8!&;iD69k+p60i)9jSY{U9KV9|n^)NQ z!pKI5cM+D$TmF8^Imrg-FRQ5cO}DyLQ>J945{JPkFZD0lj&qtKBWHy;JniTNa zWCXZ`t3(i9k|!q-J>+QReY>_2VeQJIx>rJ7UT+KubyG^yo=%D8j^GA4|A4S&=$1*d zSI59X!YnQ@6CYLyG+PZax7c`E90Tb(Pd9O%`3V6+1v)SQ_kd1G0tLech}R3(&T4Hc z`&z5&XJ@E&pF|3IpUbsQ*?f<*|IHA!sVaMbpm>C5Jx!k;nB%UGfY_>`=>U<#(-h$~<{<>)mQCMhr#iof_zqm@t7E2VC zi3Mr-#ZwE`SQ=F{yAsaoWZ7>{X>d19h8f1p`*6b@-WU_hfAQ`XNu4?eKuQ&XzkKvf zE>`K?!BG7IC8RI-ca^#iuoQERd|@88d5aK}w1vq+%@VO0B5_9i~iNaFw$+x zxmn^ZMuMXpgX!PU=VKtylL=1f>kRWlpbM<^>D*?RYl%klP}V;XuS49L0zRQCy>qNB z3XQoX+Vx_c+9k107(H_mb+`N5RIK+6x-)nop+BhST)!S(s03wFomxepDUVpj{eE`W zHa5WT(%n1RgZTJ?(5|gfx5tQadW``QAo>f#s7Lx+yrnqeyy#OPWdRBK@N9T)Vwq%A zxTGHW+6j>c_4mAE(V4rQ#~nKPQsh%dGWVm+0G}Po#c?zZ?~jcyt^r~-L_4k0TPKN* z=!OZVE2P-Q{0yltm1=lXr|`u-p3w+rL^G(!4`#Fj@HME}E#@}kpCYz{OWtKT;WoZ| z&7^Cj5g_yrg`SjHj7|G;DUMtR=%YoFrO;@nB%Lj}06j`T`qCg;h1;!fu1(uXd<=c4&gb(}`-s?^`5RQHBW%Tz6V4sQ`uq02ssbSu5jSBN< ziwA^N@Lo9gri-CJGT<>V2=ULrgE*TmJrN1GW`NbqQ$ zoRht)O)G%o5UTtbZ6o_rE#o%|puavSu0#+XxDOaCCg z;9oFJ8H9ft0l4Sch?fp`S0nc9JU#v^`~fCg32=}KCH!`+o|0EiScd)cU88?$ZdGrZ zzJXbZrP=*!BmEoiQDM=6uN%`({;nznje#@srd}?7@?UOY zEM&#|m0=B1%y=VQhs8*mmwUT-FgEx4ZG3(nlNwnk`C3cd;okSxSx}NjBCAr&#y4K!0q!B-;*2JJ{8pDrMsA8S0>p z1uCkQ16zm>Q4i_HE9Aj>Ksy-7cp}V;V;C(8IcG@Q&$BIrPf(RugQ!WSej3q0Q{pTa z*_dX7qk13t(j^Y$7&=jZjU?=1m#a{-cUrR78rB%9PV5%?Jg!M_g+WT?EZg6lzvVJZ ztHNxbgCZQ(X#Vk$dV>u>n@`-f({ZY-;hsiW!9R_!~L0Muw z{y}rdDxpcUnk&eYYE`0^r^+aLuen4ljdR2*PX44R*9b139t-r@Bsd22F}+bW3ntof zA5@AM5pe9E@Kq^GBvo7O;m@Fu4jU%X4HIuW2a%3nU{#n3ap*B1j8}Mg_oO&LkIaui zoYeOo5LiUI=qGeVUY{Monxyrn4m3At5v1E9Yg-1z@QIm|VhQSso!a5b_8pPFE*4K#Q zh;f9YXACq+%Tj{_m!tw$2LiK5{C|vbl%6$9hSw{XQ>ZYgNU_P%@&&SpFe%DXvj;-F zU?J;bvJDRzx_Qiz=#|Cg0jZEz=FrB>05d!vA}I4t4&`-d5Zo{2KK>L#b&& zFLv+b4zRev%Tha|yo4Se#Tk5J2~=MsXF`nIHFNK6?FC6Uu|TgyL%v0+rO2_3|Fy-7 zAX#|+C@{Pq(rjOmO*~hs>vdrrT9-TX%yJH5d+>-?SO7+42Yjx0I3q;;UJaW?p_>mp zsN?^(TNYK`?m9Zl`q3*8>oLlyiwE@t^q`g3Ch-1&be>S-3i*v~(34sGLh}oP9aRuO z_^_|U9E=jD3^SWt6I>Oi6o|#w-P%T0?Nc3aES_|;8K@v*YbY}_fM;n!(vzRt$!(y= z+rMj_m$OL}-nrwDtGB>di1u(N-3b1*gS;p44uE;hve}~K7D+c#>dvyw4W8ew(>Dui zQjzZhLs_CNk;NBJEWZ<+GSQ0{wMF6sDZZRI=q!wTPFuTTZDmgd_1xGovPy$`jd_Lj zF7RU&k!Gq7fp27%Hbt2Ea0~5S`vs2hS{^P+ii2k7Z;QD`X%Pl?EL0ufIXzCPcm(Vc zg*SPDu9H4}&oC9Fo+Df4bcER42N@;QjBf9exQ1vJ5ow8+0rlxod?y6B1az{_fIj?t zqFInElP#gTnFMLGIlvE5jWUNZ6Sx|_UB|uY+&(>$G4iVw+*DU??bE_tXcmJiLMeD> zJ0J#0Y!c%dP|c1aqfW;@ECO1AYSB^;Ez_GjV2kPJ;%>d;mqPSfxd@t4Qb-zCeAW`F2(=qVeLKGCH~6e=(5+4_PEjne>?-!# zmRdB+DGM*MXaO!tSvb2W((_!bJckGk+PP4;8MX3$rCNy!J)eEo`>y4nI`XK>_p@Es zJGw&G8=~@W&EGt|tg4!Xn%$!Vw|}-dK#m|jkpSEyMroR@r)?)VfkWxKOi!}IHdm%M zkc$@WN`=nh)NqGrW?{1p3Oy4+ULAv}WR1bo0_mpmbOzU% zS&jiD2SB~oScOKyg()UE{0m>A6NSz>g4_!^bE{`IlR%fpav-)=O03i+wgy0GYSEmXHpL)@?` zOSs_~o$6N>cHc^lMVR~0{M0y>`p}#*eo?d<9PIP0J1Sn^2v7Z;$c zR{nMlAq{`E(FkIZ{2p|jRgogwu1vu%`40Uew9}xayVQnwXfw48wox&}@t8K- zn=Z$oJi`gH7jc`IM&M!?@={>5S9FeWm56E%>Jv8vM7V(->eoK)|`~5`(IEmoHC(L7W9ouL-Et zpXtv4%K*Ej|KeptcHzIP?eWrmwkycz`A644sUtb6^*djFcIp8=66?Sn2?jZ)xw z=_$R%x*!HSF{nD0UsOUn(_?KMurZoqZk|PK=Y~yGews2WGm}2D%S)5Ld4VIQo_=g{ zX;7>Cje~7A(90ZtvG>b|T|BK|o^p8d0YCxM?Xb&!fG6B57J8?w?8fwR94klkh@$p) zDdV0`5O`*m=~&mGlNMESywN=ZpnWMe;}F+PkN!s*N)mbQ;v=|SDp;9y# zz#RSU5Kdu>y+Q85{cvV#`p%ev&H=P&=NE{P$+c$c$+kSbx+`ECbhKKxaGEQlz|Df8 zo(@-yBvnvS;il8thf8i|U>z~>GTBB^es7IQ6J%4$)XkrK?-?dizfn&326`_$m1ZOB za||+by2O&p01vZr+XQfT{vJ}_ejZ5Xq_bnT;B%n;hwF|%oXQyB*q;Mjd*H5u1bGPZH z;TS2BJrG3N&`p!7Vi}}ba1Y6ne^Z~o*O959o}^f zFiiRH8`&ti@^_iW=N4m#WtA1^tV*I)LaTOt9OQ13#?&XtAuC`KN2RRw)%oMIM!9t7 zmRr33#waV!!y^5d>-*tnl;!VPl`O=AWC7-vp9ydOhs!K0Pg|XcOh>50Gem+VqW%pTKBX>(VsZ@zy(z;SzmZ&ArDUyKo!RaD}Q(Y(SSt zE+Ew@+9p=6lc$1lg=w`ldTDuUT1{0`yCg?)3gn0HIEY9VB>jTVGH!T+0Rh_yquJA*(k!}>X5omcrHh*Z2l$-8t zZHN=6*?z%H^oYh*Lzexzw(%S7zWrN7h_{VTCTH|mD!mor=EiD>5oga$XE>s07A@lEeI|C-2em0R)bs_hQD=-! zut%yp%(z(5Y<|k+$**Y-XQyc`+kx&niyzip*lY& zMG-ZV4%mq_LX<{CZ?Gz%Wc_AWlJ%74`he1`mzY~Dc@lf169VPd`K}3OSc^z!aVD-h zl>48Sv}+S&8`y23tr5WcBXX8WqC2Ag_EfeR&r+v|dI9Syzab1642|_h9ci^&n%cWK~xS46;9r0aCdvHLv1nft@_KlI-o9WytNiO?~#~wiOF-lmWUVPq!8$k4n*~fg|=Bbhm zPoO`CVXc>2lt=`wONDlr0@=R0y`eF7mnTScL-OGx!iQPz9BP8jBA(6T4i?cYZ9`xp z%4KtUb$G4cqX*-@8YowDbjJI_q(~D}M~_~VF!Sa^h&vnosQ^+yyoTN&V(=>^jXb0B zMhsK_bx&+hA>brh3(VJRg}MgH$@-Wo|uQd7}*5{|7fS)(bs*L z>)$e0b9o+L#M>eBE5f-%Xo%S+_>PY&JHwR4Q^mtOpk1k!TOdY$C76nFxP=)JSfFo_ zs^q<|-e*$KvuHqIP_>0tE}xVSaY?9%Yd*FvzErYx_41Bn7dJfq*~KfpQ8K$I)I`4m zg}DEm;hNbUS;RG*+;obPYna$P+;5dMC%D0ee8#+RkA8zyq=R_ooKY#bKs`Vg>;Zj$ zx>4$|g^*|1-270czQ-P7S*ujs*uAe!5)jfNsfn*n)}W|JFiT6S*(BZS|0B>6J;43; z9c`Vi5A*#3obEk7h=3~6lVFfw;~xA3oL=dd^8qbapj!`WH_W|BOQx3TE7&whpXLa) zZvj^tAYKOXbA`yga1Nd?4!K_?^^D)3Tp}!7P^}5_c;-~5a=dFaOPA&Hl**b9bj_N{ zc#V8&*Yr^k-H6J&4MU5|m)ImQ?N?#CUiZWuEXkgR+62W6mqcX`Ppo5BrhL0DT3%$c zFv}P}tsE=O2HE<~)!Un|-V|wyJ;Zx$BHb{vFtUSryfIp(q)BFl<_Zt)mqS#!a(4&b zOOgAL8SCNx1oYme9pe2pID+C{&OAKPdOJ|v5iH#l1p-R5b_x|@flhI&`3r>>ActD< z8~@cm2HPMdTVv)Q*df_iDryfL=NRM3uC~g+|76@jxtV33l-76}L_C)=7Cy< zIZ>CFISqKboL;@e6{{xhdk2p(5Gx#7wF!WJCp>qGa7g6*PzWyYaSu2_?RHGpbO_W) z_AVnip8#4xQ|kGfME9I!K8aH%`S9BYVsAg4l0K0A{yp47=a5V1^FYDn}XKT5RRL6>OS zrH`&N%AwvlM@#b9sBraW*WZ|GHR2tdLQXKb#kC6Ymc$weA8qdez0&PRPEyUrK}WmJ z(a6ym*2hR8gtuw2)-V$$C+mjGLN{+T3wXx~kErDL?HwHIgtGzokZM|}>m3+lJ-e78 zu*z9MtWu{?R|SIkQZ66r__>u!LD?mg(#zPz$CW-?&eB4JmB(J`MH8FBa&sf;N>Nx8 z+n+c`ySY?o%@fq8+Js-zW;k#UCAr>i9YT4kEMr1U!Sw`IJ#zg0xHqig=zD?AwVHE8 zpRoPhF!vGe*;K8h&AJtu)Pw%M;jQ&@bPMm`1)>TaGqWh~{_eW9 z<{@&$>K%XUyR(d*+WrmGqTnlw{MquLe`C(gFXdXSwv-7_k_|dFLCom#YhH2wzQucTH z@46_Id}p0*)GME=Vqvly`f*Fae+qa@n{+Zo zXcH#M&RP37pUCTZXIO#07pKnGPj)+40EZLmVn3&|QdStQkM3?rSLB{gtINi)Q9hdl ze!&=fO;I(5fBx$E0-}Q&W;w$=El@<-dLwK37bxCdw9Ae!uPz-DpNG>}L2|)E}8b z43CM2`JQ?o)W$ZC{rYbXn(i6mv?!S6U+SM4DF41T+9`l>wLl{>RLAuTY!LxG3~ze^ z`}o)+2tSdn;GKGdr4>Uw`go^StdLw_-s*%}vZ}?qOtXZW_{R*eE>R2A6^p+0w)b(b zP|fat7GUhPmy)IqiN;wn;WH%3D|Ka)KF-2=;UM0>{@ zT4yDgvJUB*Bkzf^X;eN@*(6l`)hWvt=9iylELHzNrXIt;4{#k}{Oaus;te|e{B5!| zg;_x07D)EcdqmRdHEio7TE*xUR!7O#Ipj}q^>NPd>*T1mE>M~K$}{#a23wAPrkMiG z4Wl#~4WbFBPH@GVJ^xMT*=L(WsI`0NWOybxC{&0xb4oKPTU`(O4HKAd68m@mtRin{ zL{HdjO+n<$fPz$a84V~Wt*#BB0@DHj-gdEc7dG}wTv}JpAxH(RTurdzv^~KAN20wn0x(rgg#ai)a-{! z-D!fJ48hVzoSr!BpOiD_9R61Q*_N?GST-!J0~+xq$_8CrgSHvxo_xxy1r=u`WM`uD zt~gHzFQ%>{3BtW=r5CCq@mwdX;OYQqWPwzX1k_*$|J;0Cqd`mN>h+DLmz6oGVgq_@_EyAs8uT{^I@Z%~m#AFq925iCZvFqx--yF0k)=M>Uy8n7|<(=9(LPUR~k)cv!|5!)xcf0*i3QRdtIpOZU(3+(iFVEKjJ}c4yG|rb7g*_D%|n zl>N0vxj=S>S|d~{XP5oi4yskY1K&4r_9Rb$#@BD^EiI1>@{pMKh(AA^*Olzdz(T^zo+@QLLlubV;yH-2t9n zku4(Y2RFVSH@LJ*H#fB^J4EXF67BzeO-`PI6HU0LM>+Oy3#_Gg#F(gtq}GSi2W$;n1I$x3@!nY?8$CmX+hH6kl$OT-*0=OWG|4U*t0I;tmq&c*6(e0| zSi6F<$a9JV0nfK9{-V}%96k9&o}&^|I{N@#=_tMr#&gX|0uD@SE>Upx4_+M_0&}&a zNyrl|u zkAszl^auN5?=PaOm=fT+PYzLEW9FRxSKUeE1UTFmE8UP3MT%~Pb{oB>B zUrASX<*%lEG@TpNZr{T*NP8mrp~=B*GWnCY|ug{gG^d^K?gG5fp-FVUyerNTp8y z{Cs_nFkvS`j01m%ZvI*V8VP-B;t#}Zx{dE!uyEJ)K9lqu zjad?Z9}qmaM~ZoXi?4q_6Ysb>`7;^RsZ5h=O1i}!uxrvTcZFc#pEk)Pdxs3{2dJm& z)X>TwIOo86P5S;TM3isWkQFw@zU$oK-b8^#eFCq;K}f5{HKdR~>czsv!gVYkb1a|- z&&=tuYS~W+nVgXdqvn?|#@X5cs+%4;pp^MQ`91vxwyA0?Qv&wOe|iekBGD7ZlXSw! zQ|RO0PkonZ$1p2v7@Zg3u>kMK}&d5n&~o+r=7FsxxR*1 zjZx1J2{wT}OA^S|=BJ@=(yS7H;Tow_XpTy;Bb%uyg1roIQEWd^_;*(DZ^-yZ6jKxl zF8Skq;n)AEa=&|b<-1Ul7CF@1$k$mluXtL-18V2f#QR%hSfE|-d(e=_#1VV3$v7nO zh$$?(vj=p7x?=Wg?U7}UFPoHxE`OkYFdNkXUp<~RiomC&?;ERcs}v_WPE$@%ruPbF z7xU^U6>tsZ9t%K1v-ZhdL@&lH1JBp5aMZ}jdIu59rWXG_)@wgQVIQzPVh$Q-Kf&6x zFUT+Yq5r@Ka9to$3|YmmPytYl6yiK3BZhJQp&r_Q_|_1x*QW?yPnh)2cyW@Cr-g!Q zxh#lTh+Hx^1N-n1r_?_Vu{0G~@l}nII+(6~Bt8kn(EO_q{rP8yS2~Ap2l`C7ad|s8 zD9y7&Nxr#66~h7ZzW05IeX4u_{{s7tc}e9RzJuh-GfG_?+bFcoZ4rM&(`5a~jx>|M zKT9Vwz2L!|h+6KME>po5#TWeG=lCRKt4Qz3g#f7z0aDc(=Eb>EU+t}Rq}ni$t>4Pu zZ><9)=b1cD-iWG^JCtb;UtaRQk@=1{PzRs1ktb(zal6A5qa01rg?s2L=?o*SNU(Qk zdAavN;jK4oN;R5?RzbQbC=g{cN}!bdjoz~_ncM8Rv*)4|v9unt+t;^aPC=5@1}+&2 zAu8_xgN9L-SJTB8y0-ll&ros}q@6SY#lZ#c@FJplYmhwot?}g3gICxXwKHgW8q5XC zV}!>lky7UPJiE}xt3tUjbz@|OF0q=Q1C&GH469S5P64QkuWl8!NBF4Eu#srO_ahmJ z>E@WneN?G9ke>L+6vb_uZ1o%BkW{wFDdH2<8RqG`NOE?lRi{A9L2)KU5OC|I_fW;j}K0BoX zfT-AUisZ;@ns)-NoLr5f_jZw0dPzba^7XcLLS>il*=YbzMIxXH1_+LIP7vh(@cQ}* z1LFbZ2IKxIJ(D_4hE9Zy$=_qJ9{fS^UWumHHPr z`Nqo%B_Mp%jQ5P21c@rG?d^2l{yF~!QbavFy57G$IX}ilJVU*Dqv`n%#`VtSH{&BF zuBrhczRL0W@#*2^zlpUFp!Gf=t?$$8AMcP#f;xge-}ern>d@BE*3;S_pzq0Lgtg1u z1h>@IyYCy+NdK1M?b5$hnS~1Pt2rWC)`gf91=U)jXQflfN0obahqOC_HiaGU=4K=5 zb@1+7#JHqRa0Z<$d;X@Ul@?q6&}q3TV*~NkQ}v zWYzSIKU06>7hnCiS_u}_B)7hRT+Yt5%K8^SzO)c-QTeu=L-S0kq~}=i9@nre7tD5Y zT=R)t{9aLwzN)~aMxd^~@OTGg3t9go(TnskPrOs6K4`XtIvzmw0Ej0q^j{ zxO|2)qZXjq8lDiC`t^kI$guxp8C&V&_lQxZ7W;Mt1$BNb`h~A>fv1mBqD%g@FuQAQ z2LRoFt*-R*iggG#%`DIIgSv^gg?G=g&TRgve}ffbfeB;QH{QDiEP|_x^IPA!I6XH; z0W*hyOmISsrE-E(p@*-8ue5()QvBB*U}3hCORT3b{bRu=e9ZxQDWda9F9ed+lcQe? zl2xCwGVj;=B^68|v4Rva%vxyMqxBY6>n~$0j`^fadAjI~q$is|gF2etzW<}^tu5nQA){)hHi9Lk|5-BTfo8s{g>gV49u zFU?|!4r1IP1+XU2tvJ|!{n{kpJ{%$j8@DWvu=*7Z#dFW#6CzmpXL=_~G85in9sX1J zfN2vVABgVtI!HCQYU9}~Q%r#!B)`|hN?yR%eIScp0cD91>SG6JIG}63hpiSClH&Y_x`EaA@tQjJwJkqq@1{6w|FRMMePD$?wIIQ&G^^9YGG!iDlpEMf zxAgW+HyG!nAAx^UBY_7{3$OT-CU{z^@?2o_BwwBk9joBK#XD$iIh+9J?+xd%!SvUA zI~sZ0+u!ZpXYKBRO4R@x!uV0M2+E{g#T<_Hli)C zCz%_8n!o_uIrI(v_X{)D4gDqL4E`bLB&8jK{4GI@V5y2v+Tbc=@PtJN%M$Z34rnT& zV7tMH{G!dLW3hbs{>xqKYaNoFjjz8yYxswU^`Q}d4U!kY65TsGO<#s3(8C<5I5VdH z!Obpdt=}s!ibJmm)3h<#62lX0bHoi=@^gUI7N=QCxa(!mG%DRzv#?IPTmkp z#b%o*%ynQ}jo}#A+usBO%Wj7Vl5%z8^^Qkes4J#TkfsU(hVxcxQ!Fntq z*vkj3G8On}Fs;}kDNkL69>g9$KVwKk9I^$_K7dz*uNHrmVfQ+y0VKDcinzV|}4qf4nZfKVL-A(Zn z87fp1=m$9uFj=S6iS>#IbtCHwa&XL$t$YKMD&g&=0bO#Sdvj9>zif=bT}N2x=AU1{ z?zUgS?Ba(32qN7P%n8#8(*{<}EB3vY+TuV3C5rCPK=JUaz zIx7j@5Ze7K@w3$Hsyno5(f}>`Wb_ZG{UW!-7vDn6f899J-=RRZO&9tL^TZ$t44@WZ ziqV@3F%O`@96q3@CuFg$&v2NaQ4igNwn(xGG2qyR;avG$afVYbx;J7jzM+tAv-bb( z;_62~QsyAoKk1y59pJJ|-+&&**ZsWyBQrXONb#RvH0H%T`vsazG3qz^H~WEA@=je&S6tv_kD6P9M{65WasyJ6R^XgUizA z@2Zz#9r@G8F%RKyQO;RL*@w9M-d$2I0A^;v4*o8xd8DfsaH4g(Y0d%mA(~m<9=d6i zbLu&&d6rS;G1d|6ebh6A8`>GdO}1fyPR;@3iz5t(=QC8cVb&3*aqd3yCH!r`XIMtr zhPnC~CdrqG)&Z*wjzPe@K(Y9z|3AP`ECMo07ytXy4;yvT zfOd|*-5^P^c!;T142b*yCxC`@72?z>Al?+}n5$!$VUP*nqn%_L6saeE(ai~S;2h}U zSFDIP=jvY}t(A$fVjm)2TPJE233SEXjk73LW0-saCtazQ+rfi+l&t~u39f#o@m?OG z_7YW+)m4(8rtdBrBxh;bg}x-h-b*&{^r{uZ--5kDJg0x8Tqss^3)siT-35D{rbD_) zGs@Ej@IxKl0-|ieUPGKhoMNmRBr6q!+8HJVI^SKwJUD~_-pmik&B))?A&9fLMc^1J zT~{slCDA?{^=unosJ)wSm84dNeaIx)J{bX}daepamvAjcl-$S!V!B;jTs zcc#G@^DEd4E%Yz;A)~Y)mn?ns<7P1cpM4172I-1&;lsuw6x}q&5&Mu(J8uuoEaSvE z3h3KCEbR=-D9#?j%?=)*->?k>R)AdnWXtfkP>*EGv@`TmG_#xo{9XKA*!y(Tbkk?3 zG_&kO zj{*4bx1=lbRpd(*isvXd2pWa^*h=MV#OEkEy4`%Cjt*h&{!{cyRpX%l`i@1jNW05-F;=Oc zQa|PD_`7!sSjNKL;q93Iw2T#Lou#pj`Wj)F@nIuZ2T<>2%5B0&7=)VuKIH=9HT4|J zDBRr{D&!NyGshr*7sKQ&JYc408s`|~=|wwvcS$`5^8ko6?F@GxU-yTNRTA(Q$_4yw z>bXa#XNdQwfVu>1i?eqJ$28791o$Ic=ILb`XB|N~XB`3L0m0u!yr!SR-F*QkUjobo zf8y^LiocC=-YoVx$=_YLjc~I;vX5J%kZ8SLYKUF9EygP1Yq`=50^Rht@AXnokjiEG zTF}4frfX&5EG8Jmn+Ue@v~_ZWT-b*UGcMmXiObe3Qq0m^zPm;QWaN@)1jJvhST8$U zuUU+Gu33y`R=$dDShC^&tL&?zs@l4~>F)0C?rx-85CoePBqS720RfeekPsvl0|Y@* zQo6glySw}Q=5RQ?&->o-y!VdvC+CdGIcx7Vf3?qyw|?>}!Ah}GsH0Ob-r6aGb<`v9 z%Fi5qt^wV=`|seFsz2U_35VW(&EoRa_0kSe%H>TG1=@hTU?PrbTDfwPMTbzUs9@&~ zUb%9J8|R=wc9X>O=PhCx#^lS_z6o}|c~~PS)S+EqkR9%fc{oD{^A2A;qnWdhMn2~p zg#S&k(lOG{yGwv;;L+P3nx4Kuz0gT*DNg{C&0qLcN6Yyx*4Jk zo<6{o-2G=L?8ErmVl6ODK&aywU93gC8D>WTvXp9oK0~$=;e>j+jlW7-FFnMOXsMe= zxY;7MjStg@Jp<@wc5%gkC5_p2uv#vMZR(|9{g z60)_Ws!v{V4o2GxcGAv%yuC!xB6f<}&Hp&qD6>{J$zl%&x^A*L(Z<*7=_a>7u8|10 zU-{W4f^js;3iGd(%TG}$7d--hXk{BHRnX49yYCtB`Vq!ascMeCa=BxqQiVelOgMtc z4-rm$JyZ(>TNX*_CLiunE*fU3mXa^Gh@qV*muDC&m&2S?jUx2pYI&5iI+1|bo2Z8ZV_x{ z*hxSVa23eb(#?>pardL0oTB1y_wm3hB%07pkS^GVk1(j`>1OzPFb`SA;MH(7{LsSL zd-1G9twHJtBgT$kD_8I3Qv?L71eiZfwXltUjE=iopnZa-o3~6}r2sskpF7!{X(G&} zhtD%0!`LYz)RnL2{yT}LpH~jB2sa-G+r}=Fw~8)NJPtla&($kYldm4+eEQNgG|1gO z=nREmOD}hd!74#F@7?_dsRJy%+yks838Tz?>|%A8Y}q7Hq!IksBB@4>YrrM!^G)XQ z1gl)VDuo?9>v)v29=`8ah1;)wS1GHOS1*43D9!W`GgmL$Aky&|owxVid&Eo5!6XYG zU*UF!sc`2Ov0?UdWyi=dmQ*vDx3YKiu8dk4RVdv1#Vu*Yd|$ z5D={6Q|`>ssFn(LYW*-zmZ^JpKgLd@=mf1r%sSp7s!+31G0X7l^;%hyHJGq+^*hd9 zv$$C*(#1M4#S-3R8y75J`@a=;a&qi5I(p^J8 z-MW*qjSrJO;;mttC)WVc2G;=7glH?n6zo5DzepQzFY738uV|}6m3Z?n1xTnvxSe7N zFyS)!B_hk1WTQlrY%R+e+KEtyWFzc8{BAUVw^)l@jcl!8C(9Vp1;Z5SD*CZVn?#dj zBX_@WJ4}2dU1gY}Tx1^x6-}ZE=82XoOV>k3Y!?=8M?e6UGEMIodFvB&Y>&VOsYplO zUSR$%VbS*9et`8%(|mot+^ACU2n_IqNhK&}HF8Q7RtdGTpKc{uN;P-{ZW5;4p`MSi z3D=O!8A|p8hZcTr(5yX9)V*l zm5Src0iL9*Y$H<)@9)9cz&x&6nxpUIE7pRy!#YZ}uuhD<&pAlAiF%5B&N|9G&f6>5 z$Tfhw%Q7a`0{o9-2xpILfN~M|0Dm{}I_DsF{|p_+&>yWxpz9J5`CPJ5v=!@sVG8w> za1;J7p$-HDxDNPw5D<9!5HB$g>8F{;x%+8ni8hWf@V8CV-aOnQ$TEES6#4v43ZS?a zv2K2erhFZhGU|Ch?*Pw!Za_+=Y3@O2C%!%g+3dr(yX4EKs1r;$d&=b)N2sTOq+#l; zPCop9Kd*#4`}%18(9eO-zDoN1`OVL(r0B@(9IL-NVHrhE><7peDJnhIn3qCPvZ=>5uN-( zO}iND_$UXV4ya!vw~gN=fOKJ!KEz>?o^r=MsGA@9AH0)oM7mzS`UDN>!Z7RS70bkj zZw9!o{ETqIJnZK8@&^tl+>W;++@4`9R|B)rj581~z5ON`1-iQVGmOhrmdS_Nz5KBb zEEAU~2sb$g&yk%XjxeSe=BXJcL);WAjWZN0L)@|rHi!temdVFh)QhiwHOh=|vW-o$ zXcbMgeEO1j-94yS{q2M2&pC#+@r^P)0+}XS#OSBvtwDF?8W3+rJHa{t{f1)*?L@E> z_J?lf5c3$FZA7jH*gbzY#}IcvZ!g{s)&aNxpcN2o{o;_nlwGkFIQK#A1w25xc#h05 zB+w<+g1=3&MmBNMrWGv^+7p1!ob~qe-G)xGhl#g zfJ?NMV(Hqq*N;xn5-ouhf`UpjhkQ=4^yLQK4CSJ0sB}G?qG_hu1;9***T-Y*Ke=qHV8M+cRu7PCpXD?nno2KXPC)&989(Q*KuR*F_+Ac=3Fx!A@K(>}<&L-{x zNwoC<%iHe)30#OLuZY)|D6W5P7KbyoSluqIKlsgzllZ-Ga+c3*1LO&lfPem{@lx7s)1-D%!OgfF|u6wPKtcBWujXM@_8Rm zm%!7P*T2@v26`w|HA*r~u#7EIMmYp}#Mm8SJbDc?*=^$1NJd!`Et@3JPV8a`wwlFF z(#4yB>5H~jDctz7NeJh^TgV1cqU8h=eD`Ll*#^|};?0tc_}h|=jFZRcXeZqLEMt_5 zqOHK$rRzC{n8&#WfZg-<4sl!{v5bkflCF}ifT{=6I}s56IAQYD?87kaSG<{Fieyc+ z75$ico^1psxM1(|cf%C}_bvMICLz`V*1;hr+;MU>5>5Bti8n_%Fpql%oFktj6c$9cA6ewn{ih9%f%5+aT)UJ4W~Ra}8af+93LL%Q-y5xIzhd zz#?U%q(@+c)6MIqaWlxPaXXD0Lv~WU&lO|X~H6@N`Yx& zj>bAZU&k#Z+}Sz&<86y1p$?dcQ>?C+dx~lupJp2A_~s$g#MSTN&QlB#PW;_8bBUJu zI&35PIx=-S`GcH*>^F!a9i#2vJe;NNqh_mIHwZJS+Q06&?E)k)d z1ESx>CEVoiCfpQj;prpXlxUJ@nxomn;qK=eU>gx_g=-3S8h7`P6D!rgIS7B1sS|Al zj7Kv!M}u)hu*EQibpWrWS`g~MIAR;&8bG|HSX!VWT}3&Ay7=4T&8cRfPfImm@0Tl= zsC~Za>jRv&NMoE?sUqGQqqG(>%@FL2HC(8lg)d1eSFtQ zI)qHqtP*(oa`lwUe`w|EwTU=~_j3dPefl!Wp;i{0pI<&wiJD?%nF_r3#utnuho}Ur zR?%z&%0;J$mru6{iq*IAZ+<3SRV$&MZ4-0kh_JS@%v;othUBceo z|9IOmGR(y;Cdz?f>g7|nkY(~fk9ujUg%+_bf}5Wgs9rvurG=@HiI!FgZy$uYkS|9$ z{Hk8AJj%LAxj+Ro#X$Go!>Lp(SMK2>*itTk_QEA>n0=l)PxtxrDh0q^jgo%eZXxE$ zF?O=GdpJcJg_;Cg+69T06-wRw;?3YN-2K4Uvq)(l{rZt@Y|5Q;WWy}b-l4BpBqf_~ z5*lU+w_p2Kpv~R?tG;;itxxF3P(|=kNY}Gv$tWvvPT=8FzoFYLtU|G0Rwlv* zeNc|J2vAQk4~06wCzPoZ?BpB-mdP~1G_i++dg|wGkX@`E?hGd_0>ZNwID6&Fn}kXg zZyywDI!0P0n55grwuoUJj5BZJV;pG|&Ct0AJ$=bFz&hG4+{e=?_azg=8^FPL-*{!y2JcEJ|GIIa^`=qODBM6;Fv?WsspGBTQ)v|8?9I?Ue4|*oKy`*P zLr1<0=T@A}ulia)Dixo-5bD^&>EqGK-^JC*2NZq(9Vp3yoqD->x}XINageP9Kh82# zDPtb@^{G%AVS}CL7y|bW6d&|swh`+2IU3@1wh@k@L(ED=aQJ|^kuRU2fQ!RCPPqsg zFeCyvhW@($fC0G%PEkqLB${}8$(NzyBVK~;FWL%vpJ*$_kx++J1E_+GlYBiy8!#2x zCN9HxgpF<{%3+RXh{HCvNrG-hpo?(R&$~u0(b6yr<0#Ekvk-QkboKhz$HCwwZx9J~ zg6|+#vx}=%@~igU4{sips(!rf=l$}jc{0Tk(}ZzGtt{3-fG4;orK;DzO4n}@s267% zRVv^7(VdNrP;w1m4~t_YR?6UhaO`THrZlD*j$4Xh#^} zZUjGe39C@DNJ_9G*jl1^@ys=pYv34Nsp41t7O^e?+F87vBnzRAK~9ILM9UL2!p(5! zB8^E#lry=SNk;bJ$HB|wb2Mit_0nZ3O%iFQ)$(L3!tF+xps{lITP1wGPP!WCu|@*A zQj&$2KX4XMR5I4rj zBMhsA4|mZ{UOZc(0NvUwRjMIh$0f`-L#cvg%-`$H!yNrS9*U)#pN%sdBgt0Shv}!= zg@fGtxz&pi5KPiRX=a=R)Z!53@1;?sldoLv89+TB<^p%uB4w!S29ar+da-`apR5SU z8rh0Q(W@tzhXh*zp8j5*0oT5{2Vov2S=>okpc-e!-L;9+$xpIin0j~r425o{Q!w25 z`d7oOC$9uMUp=8(U>>KPE!5n`-z211y8lkDW(UvDn{9-3bd+_I5ale|evO1|rB(C* zYnuN0*F4>I;sh)7W4xV|JHP9bt{P?~Smo=~$TdkMSXC(`SSeK$XfIIp^78f;YDPMS zJBv5pdp}3RGS(!KWTEvV$%18UjRaUc^mf@=*3lh2zM_HT2AH3Zl^7leJNi)6wPQ7@TJ=&gTF2+u-#w1;-LZC}Or(HO} z^Vy3_MC*9w@mSjtw&2I#t}%}*mp4dd8=xP@+P=S6rZUCA-A}mb7V-!GKqdlm9vnlD zUdLP8M|%dy)*5AoxYf!&eDmNfFxU#EXD_%0y!{XmUOW@(_;SN4;l`JD_w#j*FgOPX zxL|79IJ0|DyKucU_I^KihHUqjV;dbG6z<$i*L>uhG#OpM36iXy)%;O|$Bx|7EL4Ie34($Z8 zLVrA*-$^0VQ|58mTE>h+88H6U&1&7Brj7Z)Bzm@{g`D; zwN$x$2k*ug?EP;3P}e{Y%fubLaOZt&{T!Gki+})~)g=8+%BNch2xuqf$N1mf=2OOLVG#&Qkh=&zD~xsj^}ED!#oRcy0c)ASfe&Ehaw zlX21_$;THkYKhty%LrR9@Aa?wI+uugx$o}l<#q@mpL+&y4g!uX z3$6u7YVr2M-3!Su@n%SYvW)Td!0c_|_WwNnA?ZOq56;d762TVT49?yT-WCDQ9?2S< z`4>oSBJ|TdeUJsj-ltpyotS10<;*BE)|P$v`&IDN&rn=Ld-(9TLtQ%sZ+@Pog)|_X zC8er*xd~Rx<1};1iLaAN2 zPG+7u<<1ht_p9o~fgXdLg_;H0x_Pf34YN0k+r>mU*~DEULf0eQ9N@b9;nC|_**smk z8M9Q0CikEgF@i0-m{HaM&oY%`^dDNt=SGlI43KZ>@-Tel0}N$KH3K!9sYyWrIm zlC=&Y$H*UA0$oEKZy&sR5^dis&Nc#>jwB1!QUrvr*U461J+X;v6Y=wow!iU(b##@~ zJ;);vGHAb7KX;ssb^HR=mm4T&ko5vCuTW)~m~zJ^j<3f(s7pYt1OY*$O}TuPly)}4 z$t;y~&@7d12I+!e>lht-e}ia~5S&1+0kReF@!{?k>_j^eY5UJaAnY?-54^qqiTyyq zC z63XRB!S#onvrHYs6i?sZr@a4n>KF2#LLC%Kd_ABX^7Zibu#V#Fu@57kgM$eA zFVP01K6(0%FxE(bb3)?e48<`LoD-bAX!``KPC<^Lac1yv_`8?MYve-Qq8y$-|GWA; zeaYtknf>0!rdk+hW*aF{TPN=0p_!Yd)%w9W*(3p~X_yOSDf+pG*-_5EUgz#tsB((n z9CV1HS};omR(XzWl~AbJ!zWh*87@e9K%Vj2HG(aYwZE@#6X);spV@1gxpU-kX6WpM zo94;K=&Pg@ON*2Np6~8YFabKiJcPS9%{0jU7a|Mr9N+>ETE4nM>GdPU%6e)0=o7Rt z7Sz*jen|gKGG-gFjZ`W=dG&YoEt2A`LG}K#{{OKm102uS2Y-8njdc_p&2eTp&3^IQ z11yWA6SM{?oge0GQH6(oh zuKM3CsRnR;NLM+APSC(lpjseV1Mfhj4RVAWLlRA_qjNOK=WsqOQj#x=v>{&Z;=&x@ zSX<@tN3Ut-^mBUnsuYIVckm>dUO#e(0wv?GC+vS;n}nH0hnOs5n}lDl)6NQXfrb?5 zafw(X*Ux>1Qm)K2Q6~eAvRSH0I@829K7aSU_fiev&I(oY)Ez=5>DKX|ZVhrmDwuwH zl5vQ`GV$(*K#yM;@qe;}I@*OP7uAaws06x}C@K{xmgZ;%xXw^S+U#OfOLy>W;<61k z3B{XZZN2;(q!2G7oJQF2w}FlO_|ncs+p8BFWVea*@_x9BcnR)IltaoLUmy3Ne_DTl zi+1(^%P}(9yh_113Ws2UfwqkZQ^czE>YXVaSK5`jkaeQtCMjJ zNT3Ra$z5?0GJavd$fG7Av#Osg*WEpD{aSvK2 zc8WMfm2AY`fAu8I6y?m{>$ke_f3E`VR?$wuDun<~nYvH60zB19x&)Xejxd081i6Ph z+r#JC>I@~suY$eT*6$!60DFeQq90MN;PweAY0Ka+{Qmgmaop$1GWdbz5vgEe)TC< zq8}q(LRQq@3p^!1??8`f`cc+GP1n!_E2B(6Ws;5lUYmqHd~`FAckv9MpB8KJ47l<$ z!*@%k9cz4tP8qpS>5nuSK0{}W;Vx0Pk+5M`QHCqp;$?b^>Pl#4&KhS{eW z1iBzS`St+RU*x*5=UckmddAVC6L1Mw2=1dxSTi%cEkB-emkO}H~( z51=IIS~>dOerBnVbzdcI6M28{-&B`qx|8zxCa8${I&w9iZ`#F7(_1Athl9!vDf}jh zTc0$FAXzHh&eLZf{r+CKbDfNQ_1zDUAwxO4M4Y8fvQVmkeT%gnV__NlmCyaBYnoo5 zYnndP_1=4%xIs?H86#e@jnGeP6vfy%L?Ix2xXV2L;VyqS{x;4Y?(Qh-^{;d@LGF)U z$JlxMY5njBtWp3)_r@1ELB*Ttr^}U%GXG6|cs4}4pr0Fm+aMeL7aWDTFI@Zw;w+C;H zGqMb^_lY({+JfAN*|`S(P4xyT=5eD;{B70JOT=Vzw-8XMogxw~zg~9<URw84+pES#aEpPr$E)4N5?EOnZ`N})*t{rIn}F3PRn4}B06KH2N_NXLKo?JHJ>y2{jb2;uJjyrNN5sM*K!LyN!r<85GK_0prPGId%%XlIAnMcS@@ zhv&v1ee~gOhtN54y>y(7T?{<0@%iQ=Wu@XM>$4Z_!i|z`BLA*_sj6FNoZx1X24bbXb=%cpH316=sqKEBQ3gqsH0J$$}CrfI#rDrKo=O%gZ0ARyRBf4mJo zcC@{8eTmw`H!EcBL5G-_hySjAw0)q5zgLbvxQ+pyIr@)Y8)rm12D#_z0p5UYQKF?r z5noTLnNjBbcfGt7O3mU@4SYTS53fXn_YD9500IC2000000000008MHD0C=42y=7Qk zOA@HPad!*B-QC@SYaloTcMopCgFAr)x8N4s-Q7L71$PPdoqgwx_Wh2ch~9$0Dj3Y`6a*Pm;91n@=JcnFZm_EJA~KAD4ofd0ws5R-W0O>woHYRNMQ_$*^nj4q+cCL?Sr<)Gld*gL|QqNiF&GyZG} ziuwlt3e?$$S;pw6zvF;{PqI4B;T$8}_>Ii!^DPMYN?+d=(vRE3!rgxGy#WE=F*4BG z)9c^``b~wx6A1VKz}u`a7yV2hzX>K#_`gvK`*sVLdcrd}%Ha+K{9Bj?8I9r+14}DMl?Ex;-DcM503>5v3L<81AjV#Jpf;9jD3jdpb z-Xix5O0wr3aDD#+1bm8l@u~^_FREs_)6Aggf9oZWQ8Ud(cu3}Zgg`O=r5R!!U>woT zVjNY;gTnv8?qAhbXb(}__*}z4;eWSKn$>k`j1AWh7Fp*Y;5$a~jeaAw4W(aTo&rVu z>lRG3F4-_mzeow1_}?yOfaeI$B#~j}2@Vwf|2{^#iAW!7*8u?(02J}Z44qGqdlbSA z#E&AS2N3X)&uHc}^U+SA?)iE^;r~ZSsm?V@ljvn?tUXXX|Mk)~DdL?avhsD24xpI- zO)?T~sTk9&xBRx?xL>l5q`Fi<@I06@4?fcKk{d#E-MQ21ZGHNkZsS+2T4ih3Co z@fZE{Bq#n3(=NGIP95{)@JFD}ZJk3qnfuEV_4DZht8+@zm*1OfjVR<<pi1lUhC~=Kq>` z&tM{LA#P3L?!ln&KhD10=Ze?gk;6g)T|g24f6^ts$ z_y>_DP|W|^gVqy~3Ee(qG zw;O~@oCON(@8xPPzM$Cuv5z!M+NBi#>f_^@0t)-jkd@1B;KZBSMB;5g;r}17@@->G z5}l^O6Fi`p|3%y5?IWG{2&-0By#mGfV;3jgYLn;`_BL)z0u=sVC8nL7p|gt(ckbl{ zh5zlNFi)j=t&>{?$VNdC|GwODjCKnG2iqj4ngT`qA7_7xSEBgIzC@-_1r+vMB@nMY zgYDu@GUD!n!vD8$q3;jy7{+d4Ifg;;{B!q1K2EdIPrF7ek%J=s;2snkCf;m%73mmk z1B&=ltl9iin9JMjD}*v-knI0?Y*3ve&oXS_Ut)k_{$DGtk!zR-1$ByeiVceRt699} zb%!X!R6q#qDJbT@0Kg&z=OE?cF?zZoDEtq5yF_L4SvR+fOD_i$@z)l9n&})n-iBy3 z=^7~de}PVm1mZp0lp;09Fi`YA({$JH7D-(Kx;YA!pz!}34f^$)x)nmFP)}D-?El=r z-y&Nj(ab|$El`4D{5{5Gok}pDX2)M*o%y5v$8>{y&2eVjU8e}WERBNyrKfm%@->Cq zlPru=|EN^fQTS`uP}k7UW;}iGqTv5$P4%my)5l}Hewr4^c>6ypR4>cNZ-^tOZW2-~9bobGL^}Sj_&7V+I;~221&{Zz zfPc)-Q zn|-VY$XL4`zED@ki2v>z)X3U>Y>}6!*U9RT_F4$@vr%^D?Xd8Ekeug~CYMe^F@Za$RJ>7%e z)Q5WsbYvR-z5U@%Qw;VoxjH3p205L>lPtiW{NJ+=vk#N5I7LAJKt0vX@$=co9%uIT zg?zY&%Ga!w3G^yb;~4r+y)eq+6rue^Czonr3m^Dd=sSqVB}%&KUVikGGj!(=-TdlT z;Lx5?v}4oMFAq_!luEzRpCBQw(|lu{%6&UdFVe&{z%-SoKE`a466*S&_Y04ZAK&%x zUZddbvP}c`|2hS|1NLy$3n>>N9{GC>Q=q}bo3D^Wn)=z`@BRJf$<~N>2pWZ7T)a(t zT}JXP(%CD7d6c{B96_~Yov>VKgYZA)D^*5$Yp3^fLp{Jg1Lxl~b0}x)gkx-SRfjk! zh5>=T@AV4e?OEHvkzZE|^(q${2e-;bI6p!{J>hN{=Wz9Nw0H5EBq>&D=Qq8YWC-&7 zPwW2-g?XB}2N|mKm3CT?{T&SWlWDSAkxHpoAkxJV#t_FO<0Ay^Y>n(UvIr;hPr#L0 z$cIe*L`$>JlZ``|S^?481(I+((#02T{vPGB zPi6q{MJm|4ZVA~M!^CPujY5O84YGg_J>1dn4lw-R8O7IVH_8^jit&_glr7cxin6`` zvR)qXQYTBkg1s-?nR8IAWf%A7>HOVnGl1t6+$U(JxhTsf372rRbASK$LDX}!<2{0~ z_jy_cZ+J#(WiH{}Ln+2vRl0eFYHK7`i1Byd#VUVIGlO^B#rjIMB4z#DW28^IOBBrGw6i*2=4reFRH~xhbx77q63vOU^Ecq1m1+kC zL!S$l#<;kL-k>1d+#;=$V6HzyhdCtb^a;5q2sB07)6CY&j5BBJ{yhJSF4vS>pk4F< z@yd%pC-A2!2JZfkRiI#=BDEt7w-AdY&HMt5A~lR7 z$H)n$e9e0(?0u;F&t^qxugj+xr0N%__49RpW03_$pT!rl(u-_HozE zvN%WQ8f56O40MXfS1Xt2tD5H7L>0)DYNPzHjT)t8u^iqJ?@N4Vt9OUgI6wL?&2#)m$MoN!}-XxL&8(!GXOeTK0KQzQ{S?6!A0s zZM+C?{M|h~=tuPR0%eRNr-&TgYk1oqSO;>| zZXs9)_fVceQuXlHR|t17!|deCnfecqQuVSm+F!QuFb~IA+=H*-f2IXMJggCK;X8-# z;`Z`RvEQMo<))ZKxKA>csKi_JGcVz1+6;+$`3W^Hl78^R+@GPz*M3vk%UUb(HU%6( zH-lpm=^S@^jhK0Un*3EQ)|F0yXt{p~{eW~e&HOeV;l`iy+g_0o6yX?Tty-Gs;QwAT zC&MsPw^EjPu}Yh9jC+!!sY1DrZ=9}9>TTAa*Fc!t0~*>vyvi$fp#`uhD(|>V-<>B1kA)xFJv33P+}V) zSp@^U*p;b`v-S93nC=~bxBcGVCf@v$Qv~AW59lNdwh>=n$%b3l0u7m3^ke$z7O^5V z!i^)0E&+rqsd}RIKj#~!Q7xn!jNx1G%9>Ch;-P*iE!3Q2Y+Uq^zlP_z}Sy* zSIaL}i?Na{%GRS;;v7n{Z563_gZKT%jZ%wH+ZC!=dbO-mP=pK4PPxW4w0eHjJHhq{ z$3==EcCP@=!9V89R}me~vz5zR#gCFNFsYTUkobBgThYxQ;&68?6Bp~WbC{>_wzB4Ha!KZnj$xeBNGDhuFtMmZizTDPH%hrvv z4)FS8AJoWydWU}S2s^;Jj{ZeUxvWNZjaaRSYe1^rDD4}WRQ;P0lr#CN0uAQzdZ}Y{ ztBH&UZmE``!-vt z;*a?yDs|FW2N?!?FJkSG0BHN(;bQel)ss|@NLJw#oAIXKam8!%jCC_87aAm0zI>y; zLiKe|(dQXgtEHUFQTn39H?cv%JTgP2myN#_;SBfO&Q87%>=$KwgN=0L=?iur>@vpk zM||FXjJrE%j(+R-I|!O3&dyN(Ia-+}vIX`Df~^jgIr0vv1d9nKtQ*e3&u>+0O2qYRP z{BB;mPf$=o9olIH>a0UV#~-bCafIuALYVsHDio_yOt+|_oO-y&scNLZn9K#|OB84)?ZStc1{q^4OcS)T&fx|b;m(4c1{wO9B5f{Vj*&_g@ix&8)bqdJ z287$T@m}swEo|fMy$~9;3bD#cd|R#3FE+smf3!TF8Q;%ZCNI{)+7I`zO7jF! zBOPVw8LXQhW$hgySm_>hjk}7^+7b}W)1h2A$ceE($vIDTh#VM>y+1;~iJxNFDqtH! zvif`cDMtFWAy(YwTZCHaLj50)b;>Yjn)$kUb1b6$&EkOT7;Du6^OQDW@}*2up<##{ zsX~q^xgxKB!{T`ogu3Jo@>I%^+vQv;?Te^OX?+k8|mVWVzW}+DYK2II~ewk1&`%Sh>RE4}$ zlYCu}YmBXUo!|)ScgP3U$$BO0NP!-hliD{4_7K;x)_#8aS)|)hHjFba5f1+Kl431( zAEB;KP~v`fZ2AA5!L0kTtYfvQUWgU8;B zI~d3Z`KUbzm2MhzwdvJ!C-gsrZr;YRQT&GOUWA12A^R0+Ec7(SG&+6P5vs&^!GtfkM$B{ z3q0K$G?@ldB&-u>m;#5Pu`kZ3%`JaiLEHEz*p>lUHS zuY&^$R9!>;18hDb?>;@R5p4X<3JLj&Pt*b-<`!Fbn#D2);0bqIzVZ&ONIh3oJ>56t z2$5xeP~a4tV4Zy;&U}lQzvl)53@XEvWp)?4nLqulTbw~6#eAxTa3}6XrJ{NY(LtkV ziW$R<#uv89eDyT7W)ZEDI@#5i*Kn!76OU)i5NpxOTcpg?FH}!8 zzJtlqQz^wf3~_6IC0&QRYxVIvj&rz8{3z>t|9{UnNOukO^C?wfoK&bxwZqv}Ey6x+ zkfEHWTS`}_{zkA?sX{wJv2}UBtHKIv0Pf@0QhV~2^ z0?IdlzJCMHH3t4hq*k$7rWWOzam+MjK(s}Yq1QMOZxiF+rq{~U+I+$}enMO%>*7&Q z#n~8?6>pEQb?^I)+cNIzxD|W06j|-Yj(kwM;;;MYAB%4RP=2 z*CB#)s-54)U7?<4(8bN&L%J>8&ohvxi@ml&RQ4v;UcP~H@&x__xj^IJ@xhnmZ6WMM)Ubby0lNs&;Jm0oP}U+g2njDHqjzJ>hTnPkmtYS zuMvqhH%S&M$N54(!)@#2E|YT%#oC3s^7ev1VIJPX+Q&pVVH}OIbP0SiFH)nO-Nm)~ zm}(sDV4Pa2f^<b1zYO(4ix6uU;)zhlzw%we`6h&VaJGw8v$X<(4dU$l2YC1C{M;+$ zw7zO(7RfR%O126#;%{Ffr<%2kD|~eeoTmS(B3X~Ro2gT+Hozv@;v8q1>=~q!a|2#0 z0|(AAOtCOQGeMc7PrHp134dyOKmukT}LAi`+BF?r>x<$+(8veRc0RR|dk*hYysFxyI z|KQOe33n6fTB)E~419xkfK#?+3;#_?v;*R$_7|GD21(MDb7Z45l~V5jh{u2Bw+m4& zzA0HD?dRqih;*q_l&HVJjWf8wC0U%Jbq#QjndU&>FObDJ!rMvJSfTBfog@nk;u+M* zqFr2}^zhyxi7+Qzz}_txr%pmA9Jd)Y@JyO*5)C>Jp#d=V5>yc5o)@2xn8M)ZXV{jd*CpYX+fz( zi@4y#4DI8+V|1#<0#&md(S}CR7XAeW%}j|H%5|gs64?hg`Nn(58;Ghm^z&G2w{S`3 ze!=P5C2!n9N9hOI(T)`=|JnX(*>dGnvrkT_XQ^+O>8!(B1T7Qjm#S4W-`t_Clk0uX z`z;PgGrB?T;L|80{AL$voHERsW5hU7Elx9afwN0u9fopSD}{Dykf~XMyM^|hZ8B4n zX9#!xbv5>7FE4lZ8BUa0&YLZg9AmmM+G+OTf9Cgb$`%oxkRR^9uuAZ9oMI?{J;H{! z-NP5*6mPSSEmLchW|YP}?ik5DUZ^hCV*W{^0PK00oO<3ZB*2?)<^(NQ2kO2|`5K;f z)-{xB0Ss`2et>liKfrZ~*eGEecY!3{WRfz@9N;b7UZ4SWZy&Qx^v@pnqLpOvVo$L1 zi}pPx+3Y2Xiq)>+NB9ksg?pO?lFb|=XURj~OBakWFwPX{&`(pZjxh-~f1@z_#5hj& z?H}nVr){r(vnz3mgt@yZ>nWkj4^tgBZXSG#n-_HcYRS`RBDsoxCq5=5bj@gZy+$GnkW%n1|3mZs1x) zHS?7!w((?Z_FkN!z}~tCKSKDw7i+0h(9D-?AYH-UM?SBX`$ncw@X^XEP%n#XfOKV@ z=oD2yGr~!|>Ixt-Qf5TE$RT;?2$6YVBV| z6YOK(33VJG7wJ8MV;^Ac3=2`ttdjR}2o4r$XnvNd=NKtax`rd#VV>K>+(-Is)yGq& z{Ezngd*4E48pEHi(oOTCtGE_RZ120t*gP`ODl$Suw62}-5N zELyczy7drIxFOX*KjR(-fXO4_&Jy$={Hq_$**(~WBo^D>9B=b;;g;&(olU)eqrq&mW zk5tq5h_$L~=)O)&tF_`$-uIA8L^aCOB>%j}ELT0s;}O6#-!63vu9K)ya04gW8tJH) z^&RID(K86R3t*AdCi2;Ao|<~TNHt*hU9>Zcs3eIz}?i)(U68O>vx}0o+}}?cyid(9RRi zT*4K+E;IKEIfGZord&t*A=Jq>_=xNsR4x0eX%|tVVSyF(yXD7yOraj;VVhXXxN@bx z=Swz;b#r%_r+t1~qK9(q>nYm);xhX!(s!Rgu3nu|&Jl$Yqz#;7?%`gpJi|N#mhl?N z9)7VJw+PHVp|4`?DbAf-WwPK{VXj}r_E0KRDd%LGBW=a2XjiW=luH7<9|2d0C)jbu zI_2M}7b%QmakkrpNtge=1`0LH^m;{y=mQ-x4JF#7>Wh^gVX9=lm<9OiWKmC&tVlIu z>^%We%&O!!36qUs&z+;Xc}2QP)cOS%Nj*bnsd>5sy_-ZiCwEa*icIqnE~3roM!qAV z0eboK)PtSl93j9)+533}8dXdBSfYI!dB>RjegEG6*X1$|pKTptX4xkgq;fA2-$iBV z8D#9es8G_%n_>ua;TV#w`Kss;T_-)lmi;!(DACf#FWfm-ho>*u^mX|uszRkw#VU!P zPoib^+iE%W!UjptplF9u72wEVkO7P$B^%U=SVuDqV(s*@VD7FE%s(Ar{H+Ggkt&po zQ^Q<>-9y|yz6aclvOR&>#C+9wygSBTB9FBOK!X92^&4M3LBzgO`U-fyM31)X;uLA` z<%7DJWDoE<0mC}7$fdZ~7v zU4TXE1pO6^aNFPPe+KvUU%{7X*U3jeVHs7f5Ne}a&@P9+NiqTZfq29?pI{}}lWdWr zB3(tiU7_ae4|5;x1bO%JjA*f1in|}|w`cZk#sN0=&5Qkh<_#Q}TifVqCfAT6J@RS( z`br~TuN_?dH1+JD4|2^x&U;AS{zIJglJ6q_Wwyu5}M?c%dI-XvF@QAlp zuOz}1#O=#vEeWFQ_T8+3A=A{--!**gX0t3I2_S9J)9wFG$6%)Qvl*VO)kd6;A|$cb{c z^HM)E-X_#FPb%C5#yVIdu9B}%5*!&29$_S#4{?FHz+J<*Rje?{J%^iM2ywZ< z_~7T`ZjwShjPoPV-R;8|XA`$%7vq$9Jjr~zIm5Rk!wuXGQiC*yWcMIPf39KlGlXly zY>K5`4!w+8fo0ZJ3e{59;aIZ{?qbEbY89^(9tYD!3cG2kYpeAH@-s*7%bDU zucxn{Lju(__K87Gp0;h|`}cEPv*d~;6pLjl(#6YU1|?6hH4^DISVw9F`SM+YP*3*B zz_ZKNa01?^nVjFbCRS*N*@53fz*8>2s_mu3y)=%#d|@9I?8CHxbuf!p`)Y)vL-YZm zM}%~`T0)`XZ+zs-L7_RiY%KuH9SqrUt;{a2T`c)B=HVzS{uaxaeAOrOOntp9=pXIE z$LLEG+jxx<-T{>gIl3#POcTkboP$&g978j76-q5)Dy1QAP7(dw83wMQTlgVvBW%)j z>4pGVz5%m1!Rw-+jpQ1cJo5rLYkZ*mo#MmK0}8SFXEL7!;4M!3fcWr+gmB-~@UtY9 zw#jGXkEY3%7&`@TE>Ze;n#D!BW1S_c2p04I?^;B=h+MN$g>-e1Mxq^Kw`nruL!$)e z;4SPUM3~E)k|heL`$~l_0sYM8S3#a#0gpH4CJBHUFI+Yv7Fim$720_wAJv2|38L#sfB3E-K3U=iyKDZ{*6k$!%Oe)T2# zZHtI$!ZdS%UZ_R0$Y1U6=cJvQ=6!(YYfrS6tti!*Vu`ZsW@B95B}lMpkoIGqg2bAq%C{B zNS^2zW50?Q}xXd7pgCegTs|JV2PGWBL@ z8K&HWjZ;Tiy#qqr8YLE}=Bc;v@wT(ydIn|b-9rUu7*0FJ{6=a0#Uw7wqv!v_3>SOJMID$dg z$DH8~Fx$nph;nx3n3|@k6?gKRg;vTj&1jS^5qkx-io{#b(%r!7*Tc>sfPY;k4Wtz zDLT9@SO5R+IP3T)n-O~JB(;VPfg{v2m~$w&A6lg^j$UpBeem%(!c|SpHM)l;S?J=4 zv)RKT`xfXWSx+~MeUN52OCRbP>1!F+&KGPA1+SHMfn**11T8pV6G6Cp372Vpj-029 zwO1=@kc)bWa^o5p?$Y?`z2^c|*t?Gwn}kZ`f3d$!@D??}wq1yEWQGC!M-Ly#Dql~d z1pKvkz#R;vRKpU%<6-zWhFn4`XiU!+7nzlIlSJ4gO(CR0ni_PU&Y zx%cXX zY^PYQNU>G=khqo~{?sOAmRTu>Vo|ZSQaa7RH2EL-?!nXabThI|RjTP0`SLZ2LoCRr zR-YOqq{&bZ@L(O`?K4UK2RvXGH&>Troooy8 zDO(-txmm;~%rS^*q)Lrs85-gNLZ+C%75_BG1?HG}gnoUHZ4Nuwdx5P?G2HeJ5$+0q zk8HM0_%G#b5jZCFb6g{haEf$$c%h$q#Bj7jo{;VN2H}3M7G5XQ&DbJ}wjX9o{il6o zsd}t~E>XPiYT4INHNtc|@^wnJBNR=quFy5UylO){ndX^cK>R^JNImQwKTpXzB2_oU zo}+z)KEOe~<>$Oi&eivqa_-<3n6Wpz#K;!X)wK&JnC57R*Mi*xyydEe+jnuD!^NA> zPJVCqW|Dz$qgrmANU(E-lwk8a4)VE6nAOJ`*-%%lyiiyAX}6Fs+POMtCq(OPBhKNH z4d}-=aOJNp6Bj8f75-Z05xQxzPzS>l*FgDeqpU882s@>UQBJ|GHt}?0p(>|1hQSHO zDTaT~Kg7I71Ry>^rYa^`OLiV2bIfEK<1LiSvoBvkQ%+8^Xk;pMeWSDqq*}x}Fi+Ra zk9pT1Of$caH_Ww%m}vHwGGJ~i6>UEyTA?16so%mvUtGeS$6`Q%rc5 z`Xnoy9qUy1bK*_=aK=I8YviN9@GU-uczh$~AA51Mh$r{;0_h5YaD!zGIKQWz#oLB_ zXcGUU{rd(9@TYNR=pVkm&fzMh!S3PCv$VQ7fnG=#DJDr4iI&Uc`I^V*$)-~E$LL;x zhnNkLySRs#7)Q9fw6lL50Y3gVThRXE|Gt;^8ijE!!K#JRGPYHYeV|C4cuAoe=_JhM z_xUAnBHhw#UalX)XV~09$~B2p@8b-xGzeIxqF${~Uqj(s@wK3yC0OlZ%GEtUk$zt% z3VKJiPBxP(ZJs664J_gL32()uFv{imYM1(}`s*y%A8#7$f}!pg$ZG|V;1r73dk!&v zpD)yoar&k{#wpx=m`tX%l{?bBSZ|c)5*g~AvwMz4rH*-0xzH`_JL)FIIKwboC-*GX z7WMU$a_t_LN*-Siu!K5kpU_F>ZGuvjTxH`jwbT!OpEb=sc}Ej33UxfdnP_owy>T%|6yl-SWx$!nuAJvLZQT4J;&{9lr zzK1*e`TVu)TbM0;=5g-+eX=#`5c?RjV^sYwg54X$Hway9(lvj~KSfok#JGkz<>?Wr zaZL0KVqDP6(n?1EF-}{hiMt`w6yVasXP%1vJ>I(X^*Z@?tQW_bs?EHi_JSiz6u{5> z_?0UAM&CpG+fgpvqa9#nn6i%Zw4)p_3==LftuYL@3;i+w086m1M%tv9VbUkS#b@GW zyFCAF_&c-D;bsFoKj2jIpf5^LtQ8u&#`Fg7H5!!x1nKtVb*@8)iV+_qN=$DNW z0$p`dIVOXwc&jhhfS=~;``|58e*S`R^TmFi9@PLeIMsq`#WT25=q;>N8RNvC^YM0D z71l`*j|DrHs9ZznW-*SYX?u9!pR}^y#l6YVFWAIYF7oj6|3I>gwe1$xBBxi-$Xlv( zja2r3{7*Z3jtY1rT?loT8g61=Ct{q;Fn9(-KBt*;4(I9nnGDXRSFv_j2S*rn(pB$Q0MqmUK#uM#?GXmWl1Q6pP?$@Qr)#K2LH1kHl_Iq_C0TlZIqzMzxNygTV$W%VjW7}7R!~%0za)?AUZ;@^SaC_)FDW* z%_WehG0<<4!`Cy}I?lX8s!Hz9?Z-HJ1`G4(l;&!Rb2G^1?KVy29N)ko_$J&jP7!Cf zL6c+^;PV~*`C*jGw1i`faYnE{)D`)>UcQeN*#5Q&TIG2AZlMG_s$YN6`Cu*F=i{&P z74kv6CRKNZgk|i{?Js$wQR0~J(Q=B$!v_k2dP2HYtsM1;c5<36z_0aHh0G_5G;OkP zo)N*`0!^=rH1oZK@HexqC>EYCfqMbu%iWUigVb~Gp>dblM}WCnG4_NTbTi6juAw=) zKbOOO13OELzvUIkIjH@`KE^p*zKXZkC{3_arIda;(y>5ekuv)&NY8nmK4oGG9uYJfiLVgAafK`X}fx=T}`lbZD2CC`jkm&}k-MfDc}u9T#bG-oTud zYnP~0$P}w?;F+Yw+r)ZMPIU179G`3%=Fu}C!UN*!lZ`{XX^M6H0E=ABguo0{tZfe` zSHD{f#k!CCXZw3_#jI3|D3@A^vbW0lY+bFw007RRW12yhc$s6=2`t?8Hr^c!$B+j0OwGVk#vij`x$DQkxj%V0m@mE zxOD=;Rr%{d&i~{so-260EdFk;fnD71IK`?RLI+qWCfRQrC8*~;K6rdcGb&Z7P+}b& z;1cS12Gh@Eno#_Tw+$@+lUep#lGP1DtB)kB8l_FjzHX8=T4k?GswE_9ewJ^M^aR^2 ze1Id}9AaY|%P{`^gj8kbIq^=0$*8BFte1&3s%GeWSqhaRd@rB_Ja(~vmY?-juC7EU#I1`X%w4{wNWJyd z5EpOT46k}#mO-o)SJxtmR2SN=WS?VHtt3xxm~|^xn&}1h(|zx6?f(fJ`!wF+6mt)s zx^IDvt)ubH8S*5HLP?Fda?#J82RF%Rls(9ku9a;;I`89ujOrbnVbm$o!xRuy@fzl) zS2)_PlNat8XYUj+U-AvD5f8+U?coGRJSDF}71R^;b{e(dMdCep`qg7%H9{Pi(9!_+0BMUqrK z^bg=W0-E^*n_z$_hi5R^nql@ShBFk2wrF>#d&oz$L*CXWX!gFJKM(I3f#7?AHr&0h zx9ZoIt10@bMNSDkBhc5`Z-iTkPAsEe9O`C2JXt0S_e|1DS9$o?$x$umDlSri0pw~6 z4Lto~-@Q0s>(nkJ*+ki2!|+Y0ejRVRh57S*mho*W;l^=hybaYn{QZ96Zf>EL2lP8I zzIMd>Dw&Vg+#O?VGaQ=P;P)~$U{Ek;509Q<#p+HUo&k^#kl?ANJ)9Y~8QK^Z*O1G! z&XH~Wy=(!VN+myE|K0(^ToWv~XYc*KO5kiKTdq_3gg&2=O>Z-@j?_snk=DpkkMs9; zN^o?km8NS6cl!it7en2-dp`rF=!d!FDhW5SZcQ_e+C+ZoFa4#z^q2n9U;0ab=`a1I zzx0>>(qH;Zf9Ws%rN8ulsKV{QHD}>=!A?-nnI>NBiM80oa|{J~fr4)n@9SHju}V@U zpJDU1G`UIK>LV!lsHachHL`EBp?{FBfPyd5CeTGU1Ah(i2)qaE|GlaginXK}Stne> zdj*1mA8XhAs#6f*YLj4=78Lvj$$ZUqq8YkZ&AYgu;0w2N4X}-nFVoH7Z-Ih8$w;(* z4@I%$91e2_3jP-U5(VBi?JV5Q3MnY~j*&NT3D&QgVQ)WpfP&B84LnT%hus~&!0t!BFFZwac8Os>i zH{4xN@Y9Td=a+`Kxd-$1f`UKFO0xPcO04Aot4#zH{7FX7pkVi2-hOV-)W1L%**CNk zwh_E-(8S+5xz6Eo)kN#4ry2#I;M2}Z)!##Te8Am3!~_N3C5&~nR>mvvO-a2JDEPd+ zMC(`wTm$sepc#MRZkEa8ZM*|=bubS>!8b|4Jp7Jx3p>hc{s|QPQWf;$3nZ}TM9Xy| zQ1F5CfA(R%hn8VN!3@$`}$VPfr5XAV)ap~-ZGJ|XPF!n{C823 z4b8709xIeU6aVt|;%$>J)67vUF^_|SpK5%LyhfaCI?Rr}4+{R?i(T9?7M$-CORb`y z;3HnN&fOo!TzUCq3IWj2tB5hO)KcH!6&yk7NK|vqlAYZ1P zO*dR42F?DPbX}RUcYxXFbVJZQ|IFi92V*Qcxr3Y*Nuc2K^gV;!!14599KGBD1wX?e z$>JI81nm&BP#qNf?6-#Lz**pX|4jl=@F5;i&LAHu6?}coK7)eKIrszm9*TLKVhJ?s z&jlJy;xu#74h9+DaX`V3wX=zL4J}p$p0W)Jem^(L8Gm<%LDW0Y#J|GrWZ#~^Y32|w zf#-z$zt>sXd#EIfF&651wIWdP#abjA93$nctP{dqK*8U}>*dAWg?x}`gnR%6U!aS5 zoMaW{jAWH_5EOidDWMM4Vqag1CF=xG@L%lB(h_XyXWGSX#r&bIj# zDEPqjU(P}1an@1LtiP2hgFg|i15Yq^4+aHatR>hTe=E)w;fk*Z6nyErD}?gbhUvOF zSO=isa}Hu1pq$}t3v_V|fr2mCNwLH}Y>+`c51Rd#M~GD80j@%I;2qnb;47E04@=hx zbosxJxA~*L1LkJ~v$TcVg*t@W|Cir@dHfNAdY*HTyZ?{=O136fXPi0Cwn#11RiRS6 z>3>Csx<@@_m;&Bh@W)ok)+C#@h=n`9_pg^)A^qRw3Y5QgjBFS73LIyyk^Q5;Q~Vm} z#W@Jvt(&7cM*rXON7x)AXXr>)b9Gco|M_=99sJ$ES`p~t?*@+3JbldLd_6pU!1a3m zZlMmY0btHJus!_uEdrSThr3HN2Y2(2@!zXvy)58e1GlicISf| zUzf+){eVueet=vdje0l9*uyu)P%AUQ1w1ReNKL#6^RQUeIXu+WIUIk>E|zORwDrH^ zXBcz}whPxwg}A8}|NZ|9bn*7mPg5-M^l=UnY?6Ir8AClK*aYqp@$~U`ldNJM(oa8w zv5g3Jat&||@b&QZ2zBuF{O9d}uZL(I=>mHn^N?~8_%nCEP)GLLbVJnBEquMK9Nl%I zM+p5))YEjsd1{t1&3x|uc4368ZvI$1(N^NMWAtc;LiG#-nmOFvY5D-~|GZzYk69z$ zBg4{Xe8LUveVN(>>sHZTUf|C%wd#e8lX+UeJ6F5| zwDM$Yc3y500ME}4a|!gyFc512-j@7gFWi}Rlwqn+UHc2tg?v@A=_zWGg=7QC>VL{d zKJO6PAWSj2M3iX!%Ud`?9YP)S(*&CgQ$PaqIP*Bw!VMhE9pxhGDe)S2KiUa*Kidf7 zB>Xkj0Z$*p6z3rJKFKQa8pjZ@wFq|pr>%gek7G3y8;QdW0*IRhqdQ zIMmZ53#*SWcSc#uU*AKaofxOe)AEg~0Pl9;US7{2<+3D;82ey%A+hq z$iAr+6=?i`eg>1S;}{BdZ+;c+@EynF1Kf>6bhHD)RhNK6bgSqJ>A&`*zppyEJ$wjP z9YQ84l?slLR|wf}JwDt+0dH7%7uCl@u|zZ1AvDZB${J^jx9t$^7%AMYpGmT+m1iIG z1RiILzx4z9J5C=D!_+Birv4f+`tb>xYz^@C-Jey-hAh1R?-kN?!w@&lL8b}3ZLR^1 zA~U+w^V0HF@rS;R}?HRvBi>qi*W^Tcbw z94^{fx*5hv;P~*Lv;mGGsC%k~B?^oqw3A$&6Ev;7k~giQYs3{w_A&BRn1^U51e+B~ z3set~E@4G#Rq`8zHt}H3)pE*ZyuIYhB5k&Ds>M8gv$Q5DRq`LL9wF$bo5UF>f38PV zsKnnAZnuro%3CLLh=#gHy2#U_TIl74{vp|by$?K@6nEFp2Y+jlQKj_dPJnl=&OY`d zL_hZ>;y!l0l%Ee@k9`cr(J;GPh-}R`v&V-4F4WUT3CzQP?9G2)l`51f+C*k)2e{6W z5iiABE|3T}s21d^!JdWNJ%c8gz@BO5JcF1fZeam{1Z#-LaA)T6L(FOVOnvAdK7Jk_ z0=+f~&`vH9S;m+qfSIhoST5?FZjOD-&)NTt%>RA9<_00d)Ce2g4b(l!D)};uoL(@_F>vt zl2z`0-d>VbhAE!DpWy??f4Ui%JMMmx)d8+deaA?AFAXJ48z~B3^oYSS3NcWSD9b332;qrJt!OKJfP2 zmpj0FaKoKxXTNB3_t(laO58&krFjNfCmdql!a7ClV-sw~+x&puB;fD ztb-&Afv)CPt)dE*3D&8`sm7zMV=RU0qOHlMQSSo1z@Kd5tv)ut`i{ds40Goh#4+@8 zXBSts*gmF4wpdlNfoWo$XqcV5e~^=Y+9XA|eS`3ewp+;0WHhUkGE8{~n4}DG;B6zH zqn_@)AYDPaKs_a0K|f|20Y1g-!-$tGW55gzv=gX%xEt78{4L;_p6JKO=hX8zaP-r_ zNDvqou#R5Czu04(WE-KI0RV7!f98!&j-hLKVE!TL3e4R+HR2`x^gQ(gq@PcyYpOAD zRaC5Hm1L2!Ug`kr2xFSQhfk?OruMVhGWj#uCV@dlp4JMfO?rD!B$D;sqbKb?;;qdZW8$TEm6SSwTezL zq#4mqPcX5J6{;s%($3P%l_>+~EG2IkCzC7;)3fv(qR~zy8vT4`=qQ$MVKeo+1mbNB z)9a-FF{=9iD!b~Ss@LaDcXxNkp*tm2LIDFoT2K)TLK>t&!T?c0S`Y(JLM5d;58d6} z4Zqo8?%aF*dhhi&?;p;b;hgoH&-?DX&$G|&{_i@CHF$b?dUJI7yQb+osv1XZC1d@J=w+CmJV~}(O=00%@B9Md|^i!AzRP$oZfTg+mx%#p8A!fke66h3Z zJ>5b7wq$Fl=Z?@h2D$pF=ic0Sap%$}{Vb0_zV1x|?Ys;_y6G<$5-pO=T184#SVkuq z8YJbb9z01hUnV18TBNxDv_sH2Y@SNG?#c&&&Uh>9xQ~}P2Pzdj0%00KKNnALwVY^+ zX2HMNe|h`SBbbSqV40`w?I+yk>yvDD|LKc6_n$^O$ksTAX%tQ{;%)o*+C}&Bgu2X8 z(@k5&ryE(veLDwLiwBsOJ|Pi7?%H`fxKj0RuglgLr8`D=2E21j z@9PufE?bji-Y8+5rcn9j#xj{v`Z&`pO{`6w^a`n$Kfz|BgidadyHve)9;mg|ie+o| zv6#o=toymxhSiG>F@oJH6w!_gG=to~o-bC5aJ=>|$lWP?3tzNll0h$*_DI(WwgWGb@Auu-hVp4nQBrejX>7O`g_?$pP2~4pv zPm-!HS9<)gn{R<~lm+v^Hsh8sU_G9cQQ{3@RX@_5BhXTyeZo5!WXq-!4B3D zIQ9Kp8HV;z{$9E{swL0wA`#E;u99TyF-%s;m#VZ02f0Vt8>jvAO%!W`dP1_gOlB4T z?wV)|SHD0f>j?Q0;BkRYqV-vteJrx&X*xKc5{>*_{9Ox_RP$2xygj@J%B>DTrJI!9QI(u>tdR|+&= z-Ab@5(Cp&p7^I%d)e-3I6ev<3V>V5Ru}!pqxU){WRYbF(hgYX} zrB6@qV;oY=o1`9MsFcDiHP(?F-Dae+?mz87~ceAyth zNIdhy%YTz#kz$aeLUD$cyRTVnAB%OwGNDX4-l~sVs$RY5(*H<~`*3BD!#&6@nqbq{N3!A9s5DQzNL?p)h`mA5&->D+ zJna$ILJfZ}mQf^PlEK#p_AklmG@WURVFq9K_p=`_gD(ayz$J7WZsb}pWnUlMy>c)_C;HGdS_|8{hY)0 zu~1OhhXgvsngQ|S>~i-}E-;Tt)dLfxSOGKP87Ejrgxk3KHwo~!$d>7*B^pIqx%*I1 zxcV^*m2hDMAzP#rYM*F>K4E}5NcPfC{tD~*~Ouo zW*)Ol(9U~)*Cn)~VliMNq^gijzG+m4>U;?A`M~~8tI0ipn*3J1}`){wmy&moq z=#gk~?OiXAeUwKa-E^Jw?U(FB36_N#4U%uKQ!e0bFOmCu$yNV2!`EG?;T9ZkHOiuu zpI}+4vPco&ImUd5p_3cpnybUrpRYBy2^5F`^ zXg@EleC9Ek)x!(}qDW~Fk70;%T&J9v927I}Kv zhT-ps*APgCN%W&l0+LnH7V##oewtZe@D(dy9{@|tI3dtUvO3DLM9w}W(I{6fTl1^u zuTXi6wuj9*02K*IY5eN@QWcHDQ5K0the(suDEmQ@ixk^<2ASZsDijAfCK#vb9zJfAz}}0sX%XGWDp7Ta zOti?--^co2AK(;jk!YCF&8MB`97a8N|0(vKXbXRraT?q3t6N{s*T{19zq=Obfwx_v z>g&TimT6EeclNt-*)*MHLby|`O_cpO(+q8nF5!k_#P_ph$_1J-wRX{d-fwQS3k7=k zd$|SUZGS(jQ3$_?ewJy9e%88YH*y9V6tbB^$W<(T6l)f0Kg7V=$KQf?!W{=5AAvl?AYI|= zCt8OXK%i5;O0-43O1ci1!T;R@fD$AcSw{D<;1`>s_Y7bk@(6_c@X3Q$x0cBDvI#e6 zW-SxC`OuFl721U|3^5P9{8>gFB1=_bY;$$+x7LXBwQ6O+FX`kyc%oE3$}+&&%i|cq zI5Ef(ZHIA)KoV?PB$g`~W<0<9Ga8U+jInhNt5o3a`EbQHhGbPgt3Wf=M7oY}gLWpy zwu|2}LZc9W3ntvdHzC;c2;>-)XcTLPUsAK+$%8--+#SXVyzL`&-ku!YYwr+9fzBrJ zkC!ucw^ zgf>c$EvuAv2yzZIiOHgk3ia)pBeHI)&k8OUCaZ6OvMVt3W-L7 zO`Kh>ewaS3w zpK1~9;|_JHlXeQ{=}oXCUAgp$c7|%cTCRt8kb|#VvLV4T+K#IqcZYcG_DhYzFBd!l zZDW+mQ%#K1oWgItI>cxbHc1^}ZIF~`^!L)sb`Sb^In_k4Bi*Q4PNel`l`YIHrCjg~ zKp@{L7)SE0B`pjx6Ks!0t=rWXqfbvos5oa3e2~_i+!i33iCI zvX0QqVjMD$;cdg*BjPpA0h(F3^8pu%v~J-OuL*YWb+ZqRGx2o;vxmJ$v@TUI(kj>i zb^iQaGPOT@V5M@-0k&ZTQoc&QN+*|SJzq<*fn*i)0DtQk?bWSo@63~o(lHOZ`IyG* zrM&zp7RQ*czOR*uw$smQkSx#y?Z62t`BIWOWM*h)XJ~PEL|dSm*DN{E!#v3~gmxxh z>&=ZU{UG;Fflrr2TB+v$o;#hRKL0h=hN~ZRz|ZbMFA$Pd;x!Z$l~RkuA$A{M=CMkJ zkC$6TbaI`;@V1T9?mc^UPouCv(<&YiIp;vA%asp$+3{94UsuT!uU-0No^*%-e0i$~ z>IvDhaGPX9lKB??_p>{=U(Yv4@^@+H{k_lCD9b!mz82R05PP}O0OyZ0=|)rZi57<# z_nsB25pLXlE?sAo?inCg?eA5tv_uYEgl$ZMrCc@L^u^Cx__tp+i|yiQ6vAz}NRh8K zPqj(VDAC0)-qgk4%Y(Il_j!sbPj9VEh^tO+ls$hJbeZ`3_kVW{X1!1>>Sc>JQO`l7 zu2jx8jI%4+B3I2ZD9|b11irbOuSJw{0q$_Y4$cAMHQ_d%Ub1DZec<_k^AYI8*%fXB z4p6)aYhR#~eu{2du!DSwa07pfYJQZ3ZCJ8Fv;}ItB^v*(ACRpfU*hY=Jm4IF`0~n! z8QKj(`6|$2(oI887Q5&K%UiEthXlJFp*uu!3|1;gG{)MXpd4Tluc4lNx^(tCci$#~ zuTQQ{s)>CR);{2%Q5KibeXJ;Z*fkfvL^|Aj-NoN76y}^@IZvft^j9DF@^+l{4DI)` z)^T*x?m>M0nn~ufG{bD01p8Qn9L!_) zpK9lgvivyX9EQ7d`6J$Th9O_KYsesna+ynLsS0{5U82$c@U}4z=%;XZ z=%yJa87B@f*NFMLAsdLhBUKMwYpCYQm!N|SU^u*OV2E~b0gFQR5P=kG$2@?_?mg@S zOtxXjD9hIT{rSsPldMWKa`n?s5pJv#0lUyCpk5@@o~QlvK2)S7nU6E&=!Q8{%{xQ} zxwnes=(>gwZ02cK%b}o1Ht=_?kjm6%7y?#6BG!n}j~XOLS!!kWv69VLM_%3zc6;=Q zVACx51hrUgfb$s5I&PS4fb*|DuuR!Dre2C*Gv3NMEWooyRe-m`isi^NC= zqV*tmk=Ash8rf8nr}r5rTtkA~h1<}MEfe;zfusKG2~?~|Hsk6)#BhwTOo+1g_ww=| zPLVo$LJ3yorwEq-~ z>_gRZFlqk#S;>YJ(>3B|v0^pgcIT-g9CvZ1>5|O%un9M=e7O4F%m2)eSGQtqVA>?) zVcLW#7kYSILMa!-n(sazWl<@`+vXe?vt5NsvS}mVE%7ti1JLYOonjiy2>z}p^j(FYFQsjMT8nIY{>5iVw;Dxs(Jrw= zH@$Di<$Ej`M7vw zr+Pk^*JG_dfQ=+>F(Sux$SZty6|XQ7L6hQKI9e7 zORP;x&c0iYoi!-39BN}>+)$nZ{#3vR3k@=0WT{6=a4(z%|Fsryi2-VuK^?= zsx!_G`%Ih_lYG{vxV1Udc*AQxr z`9|1rLsTHcNTndb3;ynBkhoXF3vym!BEL}Nkm-r^w}wh{4e6E5Z;Gn?b zTEXz8m1)(1r(b`>VN~g9m>RU1J>cuBDkrOMu&qYj{h{1Ku~a0I;2rCGx=_rq>oY@z zS$w`2`PwlTTX_J*y)zltK&R9a-U9RNG^2cN1o(pXyRtTW>DZ-sx&P1|;tly-${g31 z+~G|fJrI?<6vY7@wODybf&JMH%MJuU9DMJNCqWg_VnnhvW0MS zfS^OaczWzg6U-ASc$Cd`b=H=I<{Zu-`(1E(QVP!7ax0+)*kqQ+*|P{L{N z%486x!UuWlfh5FaknpMU0FCap<+9+todrQm2ZJ_vBYDEq+REZjXL086HYi)I+LvZ+ zwHhLhLA`mo_ZVccbG`X1O@_`i$ao05aX5m|1e6SNjens0zX02TJ$bS76G>-OyO36 zvec8kZKAG+G4=y!{;e{t>NwpxQAXZ>XcS8W_a%Ccu&t5Lw_8K)Pc^x;V|wKuzwQ^v z*Krb;J~LEg4RZ2VCrj=<_rk!5w5&+8^^y;ZI~?tRy!t=G=@y2}!UT@m#pEB|sI@=(7B z>`Ec$={RcLD;36cB2ADbMw2Eh@9D?i^Xr%?P<}o|CInY| zG6;XuoEbEO!Q9Gvdd?8UnB!@*nqsEgayd`?GV25(e*tlMHJt`4fMAfCaxJb z+i5I6I;_|Yc6_8wfopa^6*50odw*rM57PWKGoeWw&(=6QAlf6-0(mC{O*Y5lI2aFs zdJ$iVc@i95;rP1_HXZ6yZ^N^0;w$|77k7Az)KVi7e>dS7>V-tzDRPF9=T)YUlHdxu zUxst(KlD$Uv6(jMW-@?H$OE)rLV>niyKSm@_!c{vtl-+@iUrc|eaGL7ThJ_qa{Inp z$3653CYMOgWEla18$*P9Gyu0h-2HEo8t`g@HN*?97XHZPt_F#VWGhyL>QHosRgsME^gqG%_SzPYhWEf&iZI~nj8QA*LOXUTZh`a)=Gkz)F zo_?Y(@n49+Jr>wpf7x%(WEwjK&QZ8Wx2usa{CYWTU71R9bw*;|3~YSV5r^bLVEOD$ zAMy2Ve6X-&hZE{8ON;S#zHRhFzsSPdTKGXl+F*IRMfuar7E0p^gde0v}Gc-K@~obK7_?(3ip=qc z_tGw#=R_Lb4`A#!A@=##kUY*{xw!iI!m+)#IrM_#Q?B0<{Ct;SS(@7mia4dpbi=DT z{-LsE5=NDyNl=R}tW9}W`PsYIiPc7g{p)F#Vnjdl@igYobI|X&R)n({1A|QgnW|iN5lgsvKDjXXEtAb#frG8_cbe7-A+Xl=qnA}LX41|)4`MDkP z-j&u+9IM;yw=TeR`ak9)X~@tqBaXucHj^@w=jXkvg{jkH?-%U*o%f%y14$E|h8G-; zz2v0U7uy_6eTYdnrOO0-`pSwUPYHf_wTMH!9CWkn$1TkDH){Z(z7)$G5VjpbpmD0^zw z+f$yKV(Wgb-Id}!X9eFT_VsFLa?aSZahNKP7B`@GJ{k~s{-{~c^O zk_3+z6MgTzL^Cm%1FQ%r0ChJTvN8R?6$58ego zw44MLhFwDaWnek7;YnSR&meL7Z7iXzd^S24syE4V1qvA#)js z=H#?CrAK{i$lcx=Iz?WbX^3ixsgFpp90V@VaXprmQ&{wd@wNwtc4$$w1?!9m11Yyg zS4K#c^6pPR75@7^8`q$13=!;KC&oVTzrBaHN$^e;I1~vcrAmFSXF`HyJ1$p|_(U(I!V_I zlfM8@y5cXLv9rC|stmNVbecROE8~Pa50C9{1UIOs5yB<4GYr!q zjNqAY$%is;+NB0lEX4mBrTBp~!yN0eopJt=cG!=>Mu|o$J+Q`o0fL{UvMr%TiOwL% zBokB*%r7pCPx+$JU~)b7S^7B!2^x!NcjPCJ!ad+zHxHl4(nRr_eod^- zO9o$3;I}_?Qa&*IZj#lH?7s)c9{D~O`(b!NQRv=l6y?Mk>5%+4C5qLokweLP_gGVO~u|)B$1~}&NjLqR^7RBi~c@$c>rTKZT_xSmm zG=`Th&yROZ3PdQSpEV5+FdT7n)BYA&L;!DZ!RdAKWRqEvdaamN%JH1aZLZ7SOI|Sn z&qP~ue987%BA8WyA@o(n3a>6Z!E0)7!foUgeY|mqTZ$v*cI(O)EDt5c)v&ztS*u>R z<~{lkUPea`DGlN80S-Cd0gg#Nip4KUw~+wGA=tSek@<=Eiy>9H!0m~6R**rd0clR& zW(kh*eflN%uj}^cNp=B!1=*q3i>B{V$o*V$NpUc3EPZ08z zK!#4#pcPNVuoBi)k8&zvpCjeXzZKx?TYL?AxhMz27+!Lt?#qq+D^1xi-unTLqdv6o z8E&Y&Ww3O;vC!@8(R?l8_Svac1u~!*Yp22@5sbBn5{MLA4N6=cOAJ^bcsABSh_E=r zJk30fdcfOh-gn>j-ZiS75&qn?k59JU%dO}L^mvFG+E{3ShTW$hO10$1&^mT;?)%dP zFa@cMQuwNyubiUKp0{nu`8l@)2k@#?W`uQ#nN|FK1UX{ZK7X0^({o}wU|ZK&_rwdn z5zG&2boCm-cp$s7-P5IC_4Lr=xAc4D0pNa6+9^s;9^6DpU3Zo_?XtHpXcka(7@v;#B($y2x5O+AfQ|kKzgV z%c2FCK?s!|h@T4cp>Ob|_X(B0Gi6~30G4Py+^RFYZ}Fu5b|^dA2oa)|Yxy{mXjsE* zHiQ~74l&M?>`3rZY5VbO)NM5S$Q%M@M%IgPY>wB&CzkcTJipEn8JM8Q%%ujg?`g(h zsrLs3mbuW*A{>7o=za%$*6pq>Ooul)!{0sJJ^ebTo%M=_>^%gbSnlym(qhf>dZkg) zmR=DKcB%+Rvj&h*0s@~M5wpG{{Y_J|%;6v70Sq7hf?W(>a$u)CnXAgFC>mV+7XD0e zTmBJZ!5yQ^YBX8jG(SaO{VXzVb2Dz&IG>A2Q0{ZoB~!+mezQ)9JTtbtSmoORZ%;fP zgh2Oj+TmTk6m`CZm+L`+(kRPpSMlZ|B`_9c=_4e!8LW-*hb*MG@B)s_rr&Efi@*_N;w&U>hRILHu8v}{81(_v!hB#(~>qGxp34mu_h~^<5okLzi-r{Ns@|o;% zd@-DHZ{ioYC1~8BIrf-YBQZI-6RS2Uu#NU@r`Q+iX;fie4KicfJ-GTtfA6;V9V~ibGw8Jrn*FA&L+=(Rpz`wg|UF#QbwzW`I!fd3DeQqw!_sEO(Pe;WjXy)bCs77Pd~SsO2S;Lk<0 z%`NuT6}PwZ#uc5;&a{i}WiG+9o`A)wzOs%Z-$O4mg)PBKW)X``lJw_araIh{Qa6^iS%=dO)_Meg1z60KS-lnciE-FY&STX6f)gl-tUDE zwR<+!;ZwCn#@J8a)G=Pq?+je6JV=NL*BoPS2zt%Wj~;z=^df^$t4KrL2CtN8qY_=m zKawT8+a44*oOK>_Np20uGxuzoQV)%!c>URK&N|zRy}|;D%-9yn^pVZv+MgRYL@fZt zxjMwG!VU5C5?S^Z=+94!^p)^yBQ{xHF_;$M&;TGepGy7n{iW&N#8wL`p=RZVMLsND zeVkx7cCMC){-?^>0oLj1$*C^~?89H=v-4!|T=EBLz>o{+Nu!oS>FJ23gUxtOb2|MvrA!GT%lIsRBxvS z;vQ4%UXoyV5N-R`^7$eN9=P8zsLhE*{ZOA>HsuodLeQe3v79qr; zSF5CESVN@UfIY~rim-oNBA^bP{oLev4wo)A!PeM&%>XiovKy(@t18tK`vYfEsh6nn z3kvs17Oj(Ezf|iGk5^1A_Oyxn_su{EKKN+TnkOA<%fFTNQ@ZIMItD&Ltnr6%HOgE2 zgLijYQmVd%DbB*Nq}*LOBNR4;tMg|%E=|iN{#mQIFpcD-O|db;L3@XUY#;I#&oI<+ z$FWEiJNrE0u#;@vIcUG<6(-T9tT?3#^PDk~ZET0E$Ye0!2K#;fA$I=F6Y7n9kgx3< zOt%pF!Y#YP0P~7=cMM}x!QGFtB-N_c0dZhc=vhoNeM!>n_&$-{`b?4F^8gpF(DzM- z+7|Y`;uKt|pq$Fw*YaK}BOg^eM{rAs8?q_F?7mNzVe#4jtkjN4*LxQu-hG*BiRl!@ zM7MLEVA~<4MJy=#x$;_S+1o>4gne|e|LbfIYKZN}y>Nze+2wIVlcB8>9prUJty9o! z+$zHow?RX5mQ3crt5ok25|KyehGVM)$77`HRfOU-aF!%ZRjn_N1hm&sFBDbF&%f8VE z<+Zn2rc#lLc;}ra#qSm)+=aUb;nt~`VB`s-QLt8UH`XKU9R8IlFo1XTpOL_lNc3lK zKhl%?OM&Wx&1+nv81K;f@o8?s<$hi^i=qa#9WvHQVy$U6zvLME2|C9#+Zfd>#+mbL zu(w8jpqE-+ly7|yFB2 zWIJ9VVZjNO71^b#!&9WEg(-6VE7VTW8jarn^oYGPGprJ=_c8DEm*_!XgH6_XfXreQ z%p*6sM(?jP^Q?-*zz0~q?>mM~e2ZR#EA$|TVCQ2|p`royKm#BVB^T8}h8N1v-+(4( z^1U9F&?3Khlc5&bKA@KvvrfYya&7O%ac2Isr~cN^JvU9W=rQ`Tc>zzbQ=#;jzh8w0v058*=uU7u+a^h)C-g`FV!O z)nzJCc5y|D{zKH(tHr{Tx6R#YZ8!$C+F4YMLY!s^J@d$_gIm-#Dw&t8-2AhPWQyH- z($8+MYimD?u25}*aVN*1u_-lT?EA*IEZ5`jZR3LCDn*~&9ag-nYGjJgH{7Hx*m94- zEnApdpdWnaOhj*23HCj_-(A7iVQn*Pu09*ZuzJ$-@uwTT+(bHqn-TT-=aIGpX_iNij#W`uj83pJO`Kq$u|p>hSnEYB7OONJxbL z)`KD=tnFWJ7&ob4u=nyrrvV(d_oDo_?H|9g54uDrK0oeus6qW)=_=>m9ZfK z<-#^G(Kc$}6yGvMg!3iJ;ChSr9KDw9$#IQ=G2tO*z5L7b*jC`zS7UU$M9SIJ#~1Mh z95s)Rr?`WF7X!1iD{1ye-c@3emSHxZxTs*4Fo!%GG`WtCkf+}vo&<1#8uR-4h*Ib8 z!8i-Tjr$AsQJinOk#(|a`G-J<`Y^jAp-uL|^?ee(E?IaB+XN1U30+~9A@TYvgK}Vy z5`*4;S=zj7$a|vP0v(mbh!%e!TLpgstvX>2Z%D6Ag(mal+0mxRA}!9)oA}(+Y3d7< z`&DIm4X`Z)Q_`iNNp+8y2M`xEyBvrU*u7C`Ru+21udN4ajm$TskEPC3HEu((9U zKVFk-k?a>eM^Y~(-*$*{^t1Sdy(ivudPlv=JHm7)`oWt^v=Z)dYg$Qr5T=DRE2k>0 z@!9oXkBRLGrHXt&u79+D2)0eG?Hb+M1faM`HQ9KM)HNp6ra_)Z7T=*el$V%EnwgG7 zN3}+x-b-xfa)4Q@Jn%+@Q-&4twjmDUTc`GItVh}OuUUAu>lEpD9`un{T)c%)vsT0W zM6F7>#P0R2`~vJl4zV|x?E(p&xX!Xvq8+A`ohoJ-iC;=Z367`a@G45}tFBr_xumH# zA(3~6yOXwge~|XOpXju*-=}J|`%QQMF*oqXz$fr$gRCA&inC;OctdFW%9vl!EJKI# zo4-dvISQQ0GKS=$dhgjH+0v)4g%tikH*r80dqHi#0GTq;qK z7dW#d)?SKbw@j#`ewBPr|Mmpa0+CDT-^XPt-cibB|8&(Rzdsg#=h!}HULRRPxi`SM zL+duD-(uTMIm84) zNTaui;c3#TVEl-qYBZ2u``Pk$mXeA)pCbGM?o`pLke!M2Mgpn|m8sM#=D0wjBP+a} z#^_HL4Png+D-7LIxF*dp+O%TqGB!JuH443;kxo40Hs6+4GqKNm%oo{n7?jJ8H-SZ+ z_Slmwvq)#z_6vE6Ezplj!&XtL`c!v=>NAdfod_SXm|+&XZ1m4?`y55bK)ak4g`v7X z`UiCQ=iy4v$hop@8DS1P@^kuP}8uWoKuyz<_=!XQ01 z+V)WBVE!h}nyT{*yKT8L9$90F8S>RCM8(T4LAR__4(U#*rT}jKo0|J36*!)SFZ5Oq zdfC{ckPe{CHlIYh*cUJ0EY>$R`0qL10y@-8WjlNYa`pH$YP+t6NwGq6IYLO_k7$!i z`Et{!j4R!K#o}#Tj{satt`QSbg+^_>M^q4>c+W=B#|Ps6>Mku1NVN~`lD$co;{TJi z@UMIbmkI$LML-W$Pqjw8Y||U1p&gK33^BZ=e zpOtF*rE!Qxm)&n!`s2FTa(4>cvNP5VmRKvbv@BH;Orw_I65$sHbV76nmtleUgaj1n z`E}_ZQ!O^K{p=dIhzV@0E6};=m{w(IwY8;GjL#oixGAAHsPv?Ia#8E2;H?<2Wv#syJXjKnz$N;7|1!Z*EG)K4?&=2>7_}Y=xBd(s)26%DqsfB*Pw?MCS zqSu+<&svs;dieVZ5>^{TM>VPf5R3#d_t3^eD$cQ<=1EAqco#Hz zxNr@4XQ)wQ9j=#eP@Dw0B3@=L`d25DZ91?fnmbOBBKKp3d;b~c6<|Im=~rn^-*4jl zCE7DRE4I14*)x%hde0|Wto#{g7ZrQ|C6&Jmzo6a1vM=|DF7BH!qK!zU4uP_7p2Iv5 zYb1kh8ij9M-AQOS3Y3Y`5eQxa_s?pz`H&l__u#2GjND4ZqDQ{O}P z`_DGT_0QG_))fQr&F3&6_Cvo~b(IzU>Z$Xu=Q+-qVi^N*Bip{*^zOy8L+>DuC0ks* z_j`0h+zn8;HIixOqELawlcEThclf8*X4MA8fxUeF;dP3w+lwCQ5PyLV z>3l!WCN-)Z{*_|;>9urMw-l>%lKtKG>wOdN3#7Qo2lhFJ%!+5t?oMvyrbUVeQivh9syJYYAF5yn6C`hMIXhbX1^yqw052*mg z^$BV)mtx~BY(}0~w>IUGga180aESMy5r1luEPL+yB9utqe?hCjKcrLg1!jy-sq$-0 z=CjjUHmEBz$%k!zU((_)PCj$z3?+D^I>bDhZN)Ygc5Qs9mw?X=r)W>X1vSz7aw7UYn1vpPZ^sTP%PlB_NP40meglvQs?6JQT7}FVMg(iplN% ziGg{@XKA7(vh;UsgA}JV=0LWKyv*|8UmPr%XFHR22#=^sltR6a2KPS;)XKF&z~gkQ zcsC?79OK`wx;$4oA_%t{JtzM0$DUksa9&nVa%H`u6SC}X0XDDwa^7Ra_TeL3=BWk2;72I!R zou=nFn3d?IqkIU@N}GNW6e~(^A%tqnW4O())+9me?d;>qvB_|&X;&y+ez!+zE-)?F zrJ*=``gJRXu3!ar1-@IEWXV3})Ch?g=JH*%WC0U1-W$|(TUopDHcS$&!PP+Tkkz4; zs{XlLd+RL2j5c}X{hMEQ$v-`5unj?+LxX%7Hiqe{g*z|gBgl7iq;#_$p}XjpkOIs8 zG3fVY%DC@&8a;CS3#)8f#KVUR;Ih#{;*o%VWcNHURM4AK@Il6pfR%6VJIPj!TjW}n zN9QGVmdHbfpGmDE0|E=c?$lynYbwg-0D8G1IkYXFaZ5mxYCjv*;nIZT{`g!_h^)6( zDW0}gZR;nhI=+N)Le>WJh_@%g7Y>heWP?$N>mFg1VwS!{ceNkcY@E$JmYW~vymLZn z;P*-EU)QRJHjeP1_@DVy3!Ta0rMldJ%sfIs9Iuw8)~3n(KjAm{p9aebv30K}=4Mm? z;ddRh!ybKxL^kMuu{2A#IMW(yHGZJSR5@4puBm%WptRg%bSPOKC%qO5n(~6qhQt~&~H)>yT-UjH zuB6lV>!zEhP$cLg=fGsHT5cEy46^pe*gPQRbBw`7w{^a2fm{WWwo3h%g9B+msE%t? z)8WNg#_3py-@_%&n_A9NWpnrt<@CH!l=GkS^OxHzr!ju>r?73qC&j$4D zH>qBLl6m@O{}>zmjc15uazR#WRAe)P^T_)B6V?fhW@_d1yr9taz8=ZvA-EviYec~2A$?vqG^yP=@zNVQ1?e< z;SP|)W!7L@b#~Uidt8~C9DCC+%zJE)NBtb5SY@91HG~4Y^>cNkpStAe73d9G&qmYC zi_Zy`e5GT+oeAEK!)L$DA_iPg;C;31qC^}1UPrK-3PFB7q8skm$iLc7j7T-BG&xkX zEN)1BL4J}-47956ywk%dXv?42Aad1QwQb5(d~_45^!ows9{02;_XdMWe4IC=n;qd& z-tC%K>L)VJ2jyM4#D8;~Wxop#3~PJo0?tAxPXvf-BBW%K!$7+`c?(${HjuC3l5O=L zlKeGF!QV18m}8uqy|;Hjr|7eMw~R24P~KgK90bE5S8eYzhw_P+dL_Fvm~Fb*T(KXnj7JYl&mLSeQ1lN?oA)ug4sqw(+x?YP~7UTs^)tAVhNgEFJjO6rp952l(ankC#N>Kj~Tio8S>orwYU*K+88L%T(Z< z@u{CPd}w`mwTPECg?I2%AeO0IZBfdvKFBM+W@E$%01xl#6xN<*RTqbG!abaq{k;eM zxWKscX%S8tZL%$N0@WjMzbH!Arn?pB@Rts47Rjfr2F0R`4Gwz*j*SiOuDL(kknz>K zpNm?_o_S8AC!XHd8JuP+#L+>Skl zwOk}PPw!73NZ4DNrLcZlm)!xULU;eqBWqkEAqE7tf8TJ=iZ(m{t&HN_$V-y!?d-GgqgnwH!Hqya9!oymkV9vG(&$sE5_e+ zGS{jUU*0l4xVPY_ZtQ*dJsc z#0=5q8C$1T$Waz-H5;cUTPx8{HAWfhqaS7lvNXGGo=*&HWnN1_(6%YoC1s0rqhtlKK3p5q-^JZTC#BttoMQYY;gfH8b{bnbGe!G> z9sx+a-)Wk#&Z2l?mm3eRGWr<-UK&?IRV$GtAA`As^!M4Oc7Q zIhSP*_~Vb2Yn)*0gjD$C7geXf$W*F=^GF9k@|a;=B!PI{>(t43bNFyG{^zU0_>C%E z$kw0YuTi7WiG2a_Y8-iro3D_z zI-yvmT8U88nPfq&a&4^VtY9-Q^OP7_)jed$6BbsnOT`ZKy(Y{jdRr~A;`1B!OwQyj zsx|x&6zfo$k#04~!P|R(32sNcufWPE{n%dpZJhI;SEtko-WA3M!@EAmazfz@m2&By z9hOBeq-`47r9IsLyzB$Hfv|6oc=Pm3lWt(G;#OI9XzCS}suERJ_*m$hoCl~P?I`D% ze%Bz;j=T%-7fTyqCOZ(IySqs%nGc3gj@V_CCHepA*!h;a;#c+cf)_CJw_mv4`39il z#KM>~-DC=;$j^@Sdak+7e`M{VFF=+aLSpx}#JRv5R%~1eh zKQMU~^V8l+828-#)bjo7)e(UpMHX33232HxW(7@Jceq%`k6@PZ(C&qaYjlDmSETKs zbClH~n`DEMoy+g)FY)#4LU3<|IfYr28k|xWc$gNCn{9JtdXOu+(Is}-nj|987}Z1C zq|31OLETIYt}vkR@wxe5G(2)1g*pkY@g-mAhEd-(3W4T`+WEf)+(f%qA)T;yU}04W z)f&+E#o9bUzxV1D;#}-e9p4_pgPotg3WZ4LUu!>RVku8xAG4h|g3DH5QXSt5=<5xr zDS*VKTAf_7YUV$wUeIiT+>YQ4PlWr-9~;=ie4Atn?*8NyYdB}+;u(6(Ld^V@$O0WC zwoM8}dOB9{2J|%Ql;}3b=~l0fX{pv2hXqbdBQ!je*SsR6ConIf8N*VSGTS(C`+NOs zR0GX6zSdcuM(wv)e>X7IerG$cPyeD2V_f!`Ul_AjoEyoxR`cRGum|y>OXxhMM1NHn zA_zDD<8k9M#Cv?2coFIU%%D-J(GF@M*{1o*dy%9PBL)=!2ecRLRpVb7Bq07&s+*uq zwn?_XqT7;G9GchLaQ3P;P8tNfj-0@`jv;~$WTWDJ-d+{S+Pj zG-29^Fpo)I+|Z#z%rP3vGTha?#|07Lc4<<#fJ_I=9_isL0{_|Q&%{lXD%Y`^ z(h18l49C=WfMAb-L{Ex7n%#ZkRR@4ko^LpYI5bU=UI&T%t!c>Z<1Di~e4s zi0}o3$2mH~B~+}E+#o%|)F!-J|IL?dUzrN?Kf^&eu}JuYmtdb-PqQx9U1dN$5R?$_ zXcj%gC*LGmH_E79T_ovaGf%7wzL!_@ME;8H?UWX7KJjaE+XSiiIwHt%VSA7F2v_N= z{g5e^Xouy`MBn}cA7txtN%A8*wUE;FarBwjWA4tWEm)=rzabH~LxS>iz{CR3tzozS z1l~NFck}=A54KI^lN9E~-&Lw9%48kCI&_GCJqYZ4mQQlit5InFK&})e>v#L_2)6zv z#?Lk+(rB8N>I;!bMT;V=voPmV=ex5ldf`u}botRkBL|GJUAX35^D5Lrv>+8oK&?`a zIhpoICmb}2Wn5zx@tgHl(SQK@-Yxk4i*yV`AIT#Hp+BV5>k z#ZEla&>o0hc;WJ8nGoGr4B!@?r3&jM*KrHSK@kEt!S@a=&#~I|1el3{~gt{@d>vvm* zQS8NCvU)Pxy#2lxi`1|3{QVp)w~sEF+^pE7p;PjTUMf@UuhYqOV3t5|9Pazbzlf`! z=a=Qs8t<^fMnCD6CZ$C5pfBhWW*W+VcgqBJ-&Jc-xQ8>&k*`QLH_mPx?UGVvg}Ti( z#xpH&k>O@gMY?uGtyhS=R-x}*GDky?KFUtF6?|EWEhz+}Soe>gOh=>(4-G(MR~T?t zp^R38ben7q3KruI3f9*%>L<%b`8-7syKzbl+@*7J$y%h z@qr}}LjUB-AUzF!NOc({Ieq;7_PcrleojqMU=VXJm{$s&cHa@cswKrr!rkbHESpQ@ z4Y@Ns!bJnD=Klfdw5v%@w;+<;$BSAkMvhxpbs1@lFk=|8EQPh-Ws}@E znZmLa2x`|{!86pxtM|D#<+5nkKc|r0Uu80?X^#>H9x=RO>C_W_9~7-`%yJpudSvw+ zPTRIV#;;O9DxDhWr{4dN$z}6^mg)KO9KJ$xsHpf9p-u6WkdZ1GZYfDb6d%_o5LxZQ zcG{8rehHercZXr8vIe<)YI+aInnzfHk>Hwi=6yNNXp>O?O^cxxsRgO1YPbMuW`EN6V-+6ohmKR_wi5}gxNSz_w>eTk4$PZT5Zij9XP9@8Df!|YBd%Ys zP}IXoXP)~l%o~Dl^Z?)*SLjQR*n%*`SFG&{*(s*&KgRhC)7Zws?2!$Oi!bX=@iN_; zA?3nF#$Qaiaqj(m)@Z7Xs_!+rdCF%vf9+vcWzY7r3-@tJHs6q`H&36GyQ@S8S-9tP z1mj$P-cN>c5Z9P><{#f7-eecsY>vS-GB3hXp=VB>SC(s~DEOr~e37?e7uSBb{nhw* zpLE?-{3;%=mITr)n`B?VD6!~D1`xmuD%b%09Htag$eHpH8hSM82HScBC_QpK5n&c- zHP6@`zR1CZ+q^JLrl(*jO3Uw;oj)r0UsqD!;^X9W-PfxdaY{j1SmBV`t&ZWEy|+=W z3<*OUhJ+o(Mm0`PR+RBM$kTflT_QH7E)Sg0CVUwZe!iMz_i}?9(+N2Q=Gx|`Q0?lE!onJ(+vNC4a&_`bX4uP?=+;Kpv}`VJGf7uFkYNx|bn!xg>QSLk zOpK0?)jJy_T>=AJwrC&2jB{1$4~Y8Y(ya|r=gF&stx3wv9|E?r^NPGTIai_bhwJuQnw@{ z=_lCl8~l2Zy7R|(oz`3}K7XCO44`m7Kv||HT6IQ1vt1#QQXA8&Zh0`l`Owdx{rDB;U4=|xpQF=I2xPT-N_yws9kv-nbMI^$BjY|ldJ)2 zEY+`aSzpefz@F}5paK|C$NtTcwF#0G< zOz+x&6Oux`^&|JzJjoCp40y0>iaE@&z#7*I+g!L|hC@*%qusT1sVl$@T8^9J!7O{&24k$o{!aGU7wAmN|ZDu6$Fhb4%Q(wQlvx zYxnvyN-W?=5U=neXcMx1ziI{A-RZtbPSxv8MqOWo>pf$b0w*UwVmWIdUD6*!U*xg? zX_oS&)ANA#Z3r}b9-&teP;?u6frVWCip?hKAI6|coG{XBn>@rWSI#o2pxbXR1o!iZWW>KAX=2Y)!1Ecn4cDZm`TL)~Ke^ zrb{{r4?Nw8Vi9Zxc8LkF@T4)%(~4Z+)*=WLUdR{>rj zDx2`lrx*#Wu8dL&+kc>a3EXJvXCVjNpm(Y?8=wRn!)qo`T-6(auBXgWy@-Yudo{>6 z2wP`yKiWo3qO-njadq11mE&%oV1TZh=UfRO_S^JC`h=^h#W4*j6mc&9RUKD!teLhg zgw6PMZI5%_B$02`V%6d`Z-f}p!QVg#9`F>vKZ&*&l`-7-kR~NaRgEUOhTx(zWOS8Q z2I;9VrzcRI;NMKYY@U@`sY`r~3GUY&BFh=lU8q%)MnBsvG5hiK>+%KSGTHn;(@%Je zvr>gQmLXiL#1vE3gMHc)=#lpst*{~+HQLWj$JSoOl^I^~Y?DbQ_ekxLza~W6cP;?q zO67XliRC>!b_EB_@r9*IicG@epO%W-$hv=>qfKy+yh0gfJRv%yS*AH=FEDT(K5!co z1_gD4HkmSSL9dcfNN-;4uK9hEfa=X$wM#;gOyIz#LhsqMV6 z1!BN|-0^2=wKzxR>dDsTdO!5NzTxc*Lv{Oc4vF={^y|GZF{!eSY1i;~{r1w`)jf zaa&hvCBa|@*sQ8?{y?sc1w=?rNdZ;;JN+@$@i z@#NkppxGslzYksQfa?Oub&6f@hPkzh3$CEy->!cUEuluaw&WQKY?#nY; z1uHXVc(m$z1s-vExfw-{k;H4#jt${(h-U|7%H;fGOic2@eokj9H{`ZtX$b z5`7)Wc$Ue^)XY<5%6<@s4%LgMc#$4tnMGSLi1}G^EjB5=K~hZh-*+RL+Y|<-uK)cL z=zH0R)vozK%?|=}($P7h$$$U;(2t9E(kM@~LUjOCq3sUupZ_`Wm{RkbzD?xxF6V7a z8&#+e+$yJl_OLB{OuSK)gM;`BHF~aQf+x_&FCwcZzc*kjJ#DT{zE^$}Fxe|XwXE0- z49Eo)~*>fAn7{ zewbGbS>}rqBLX|;CRqwCct@sL!(1)WLS2#W-kE0ULGIRnfz)(>?>9=%geOjM7C$V* zVKW2bdJXo5%HVU?s{PrtY|za@Vc?F3Of%()2Qc1t6aBSwkd(hlSLY!#5~73SEe)8 zuPF#nG|kr{^9F74jdO9^s0#)7lO{35a0|f$K-#GD5435}Cwne)UqkHh?bOE}>N+3s zh(G~>E7~?A?T+9$ju0~NArA_ou*@T3A7_1ZgFDQe2c}j`F^TJZ;RsGm($8}h%l=f; zx~>VtzBK7&xSinn!DSm6g~l9>q&*K=J2yjem$!tKt+)Hf%GgP!xBr!4_Z*phUX(pt zud6+<*_Q)k8vuT|m~xw~yTtM9n#bBquppJ3ZiE(qC1df!%$weg3zL;zP)w%lX zT%Mb*>Z{#VYj>~p>Uo&D`CDSQ0v4jC|77)hyjdHu!Hi=1V)%Nzaf?hmI^XLYG~TPw zzCn%_w|!t?9G7~C+Pjvif_e-1r0sqlzJMGZ{ZclK^A$=leBOJIPvaw-Gc8*G6Hlr= zq*!(Dok^~EOkHMq(fC_u1gfq3yG zA^k!kY=z9^-Sv%WV5N<@r^3j|tx6=}_k|MfPBBNg?|EH#M|`Z4nO^Z!qMH9x?GaX_ ziLlz>3E>-bc(X*)$DL_v>}m3Eo-*^OPo-38|?$sWp^nb(kIwovPb?w?8T0g$JCeb2e&-wmu8 zUWvcG_e_T!PE+}HV~O5kaIwnkNH@9WrRak_N+e!|x9r$pSY(|KN3 zB~ot!Wd26%eCPIr$}h9g;2DgwyhGEjKY0LtCbrg+?QsLBgl384z|KzlH+R|te1|ci zLG-*VN*>Y$8RPU8s213%8Vs9v0f)WX*pMZg<>P(`xZ(C280Ci4Gn%DRY=SLI)yspG zF;}Q_q^u&Fq@ICi5MBJIhzOUHv@LQP$qx(vsdW4_H~irYzkhBRB+~g1TLN?h)3atW zWf}&5@UNzuCYr3xz3hc3o6qqg;&1y{STvVEiygYUX&v*?6*3}|M_p&)=oMq@f-Sm^Mg1Xg%ya)QFWwEA$J3vTBO1qZY@ zIXu@O$AN;2{yqBnrPtjolVva6_yr!-!!cy| zFJCCc5c{)Dm45^35&g1A4EQU^ZQy9Zl`XFt8zRBEgEKtk=L!eux==%~dzavlz!;Zw zwfK{Gc7R>175x1jtdHrxsr!kI8O|@5cNnQw@>TlT0Y0kDH-Wq_XI$oS+c?Qui#T^B zs@Vl{)@huhN|k+7w8H}3XQ+Rc1cwcB%u^CwRGZjuLfxuu^~%DXOOz4?kG}J$-^|$$ zPh-o{9P4C_;eDG8F~a)@ z$Pxua7#P}wdZ?^o@;D9p&%?!!O%miIP>|Ij>^*uv{k2dE^eb+VRS0%iiA?~hOgVX` z_p(EuZ87y{mZ;^qs6Ro3$tq6@ba%HeJql1*PZL3JyoP3&T6VWmqXjq7c&J-d!rhA@ zenRENiAmQK<39teJ*s8Z(ls*0(z63C;vCb;WMA+vORKmHQ`3aIn&d7e*2^usqh0KD z{eo16xdg*GGLz(_nio`1Q74UlLeYg z%lxxy-OqY^kxlMFYd&9B+WlL!$#3sKYe~d}JR*Zy-k?KTb8?c);%gQs3$j9jlk=*h zJR&085-W;RTLVpzUthp?dtiLS!~=uOZ+aEp*Fs?>da*$~ZIIv{Z|`6oT%l)X4lkTy ztHZCJZ=Pi-r^l<}|0w6=>~;-qR)cE4Q^6O!`RzGR)=$azJq@A8Ku9g<@?!} z=ySXf?S?sLy4MS~NLj>vUGHbi2UF(?~aVOjZntcDWPaU^%!EZC|7}kLz25>{Kd)dS3o?iEu$IP;`LA zH4_)0UBAW#^og26ypm6#|E(a4K|YXaX8I2t^1aOw++t5if@+;!s4#<8@!?*TV2x6U z8{GcQ)osZ&5T&pJ{a&-8S7x1ghTHnPS{LFuz+14Ly`O1%o%#}Qh)twZsOvQ_!Dx`N zM5tK%30SjQuKo=R^z)y`++C#d0>L$RxZ4GtciITxA<@nlWyJ-UMw2T(nkazqvVju- zOt2uUbnsft6zEIP(UBYl+`%BNnq1=wYokHj$^$yofGukyYa|3rYXwv7)dV`ayc2bBkYJE=YyY}1?lN@n1P|v(jwecQ~`35YGt)^Ao;^O1ZS_)v1L0LtJ!PIrTcBt09~yUhW_ zI%Avwuf^`40;{#^$IhZi4ahJN;x`Z|38ya?Gm zvXy)n*4{Y|@#+=9_DE<0;ngD56^?@+yl;b$Hp9n|LzZ!TyO3$RgI}lxOq}oTgJ+uO zn`T%E3jOQ~sC8{Bi!xH2@r00Vds`2l?9b|)95#iMW1&^x<|^cJxT%E@e~Udlvn`<~ z7+A+rw=nP8kO4uR4Cf%&o0rqaewa&ns=y%n<<>~5O_GIIz%@F_vTFGrx?$EU)Y}lU z8R0weRFr3}beP$0>Sr84t) zh0--50<*7T>;wB~h5RS5c+=)s;pTBRvdys#UqAexYVqBUbo&#)IzMoS8?*F1laj5` z4Bexk``!r5Uu>gg7eKiP3#1h}z^jx(IRl^nGk~~CvWK-xHS;s`>q-3&ylS1?LdXah zI;xk+%>gnbd}UdbFDGvXAUk@)X87Dd1*Rlw;jtJLrY%mw*?*9^bF1#)?BVNTK*zD3 zW4y=*FVGi%AzX@1AnjXZHwzy^=(hi<;~oud@eStHldr6jjBw^1J)vCu zn9(IhrOUr!eBpUjmfoFt^H_ljwnM5G-KaokllmdZan>$72g1Fd<3)eIhCD(BczgPv z)Hd)x@x3y=18kxW#Ll?5(>$vZWJ#9pEyNDRRgYBFmo*kHF>bK=sxJ3m7VV&s} zTOuJ|`?hdQQZ2m}CRs5~%M{*Di{E5BpKk7w%ZD}O=YSr#1Z!MkxW$FqH;FJ#`?f%V z%Mw97^D;cak8aRUsh4tf2RF7ztP^mIjIx7#sFuPS3N-Iw@-%;Qsg;v#mnL2uF{rVP z%hle!oLuD;%`jS^+o6J1;(c#g8&P1RO&~DM$@y)gUWqi1vpU1N#(>;`Im&m1dG~U` zg(2XPP9~0Gie!vn6*ruf^U><;hYBit=&}*S-gB0il z=Rw-_w;m1%5L0(N(o@P>S3k~JZ$ED!&rtg=*iBlA@r4=I zXJ`;$>RA^Qv}ET8=(9Aql>CI$X6Xw!LLn^rGW=k&Q}t4y6@hf42CzF~*TF55mfkRfIwy$nnD(vzCHx7bO} zE?LF_eaw@a9?3y&0<)KImQ&iVOFpICO=QNd+~dX*rna`H)1L6Kc-ph~A( zSGNz15nvXfZvJMb;WJOH6zIHP+dcs{^K6CE?;7W#=R-WJR$w9j1PDQa8f5O1VGkkA zQ=pd}B20p;PD9CQRy*7^+uN{0sVSZSS=H)14UZ3XpgvabM2UKhf+YJ$3(K?yNtier z!7jKfhB!J$7hqVFrCag2#{J8u&?Y28%OtdxGl0=i1aczPAnB=NO624>y?*Do>MMTrYc60 zPC6&RB-^DI;jlxWQu=xGWdo;qKB9i>W&>~gq(C1_W&U|6sl2-qR)?1)k$)}fc1&MVPMKONx<={m+~jfkZmnDd7B zi>3(j)M6O)5*y-7EUW}9!$&vmg8=w_&AI>_=QB%y=8J7!-Sc@)JCLU6?-Cj9%`UA- z-mgZbW~RAk1G(uv73ShHN3?C9ssvzyefY6saGZcE;B1gBjdyX0onEtBaGWc{I`(XT zm}Hz_(=ky?!7HG@MAT|9uG_s2&l4-{UQ)H#CWyBm1|@%6Msc2?4BOn})r#>3 z{ftJ5x$0Sl$k*4Xk2>(R2}<B7CIVuSIPGJ$_7+KrVi6 z5at-8YA&}zdhrbUH`9NpQlOjvQ93;5`2+|YT%lOQK7JB>tu*nkAWea6;q|e9(d9DK z$`I-4;~$@ay0|J#J4Z0aY>4idjxDE_EzBVNjkPaLLHwI)&Lr!AfNI?$A;F$u?B^Hc zL12Jkm1?d*9$hEPa{ufZ^aKTW3-$`kjCHl^>$EU)arD!rL6n=NK$lbSU(jic%O+`q zA|ybh)4$mgX-I%?)cMmX@)URv#G{88S9P8UdlZWi4uO3N`Nca}VFh+9Il|!l*AHK! zehIVIWzJGG=z^Q8DnV_QtO7bk@)+I_pv>-}?}t*$$|uU<@7~dZ4sWtSQll}#8{tf{ zbb?l)2F5~QT7kL|=m|)u9$Rt_%Pg3jr_@ZWl4H5v(;1H$4x*?P=HxI{Zw?|Sw<vO-S}z}9vtqjUT=}^ejs4p# z3gs)@>EAQJ4zX&bk58%L8pbs&Q4ez~+`c(7#{OQ6eG0oOA7KuNIJRN zXdWSkx$%YC*f!cY>nFWlw`xl-olQ2zS%N``J#204G$*`ck7$~WQv}80(+|4KvXDx( z4sJ<@lE=bU);tY$ZW0JdZlNDYKv!t#g3}5Fc^2a+UBjJx-+7)IS$nsTTU#Xy^ObemC>%#MgkA z7RmH%SmkfmrZs4E0zdzg;?^f3$DF(Y%gY_+`8fvNVhGQk3sgvJx1J$xq>EF8CV8s$ zHBuk}ooUP~? zu}h&_W*tqjq**M^YLVj=MSKP`9OAN0e%vMBCIGPV+to9E!yigi%CC7VZ@fN%np z^46{~$vr$V$?jdF+DtLpM+u&!R;g8{)C9Z7*hCqG7>U$s<%27e<^nTPem-oa+9p}V zRfQr5c1bific!px&I>iMPL8qE#%fislc@lV>#-t@5f4vKclZYJ)`kDRI_=Lo{vIXQ zs6B{qYmlwYEglCLzMr( zYbseuO13}IM8r^0*wf?B3X4Gv%i8T@X5R4l(3n1isE1}XPuJ;Is=_)d+tn(IWhLKk zoAr9`(?WFg*MFMRDrN6WEn+D|qdzM|1Ijog+_GFnnE?f(D=>^BOuPOPa|ZLDoh9IeDkb}L8ybTClr`r ze6v?70(qt=rjADl$Cx%KCtn5EFA(TU)8`UBxxzd*#80&jK^SBsTOQ`_mYUw7oelJf zaHm@UO7_<`i>Vg(v&V%3c`Fx0{Jus`(SPyf_g#G@&j2K5%nYuL@I&1LOajby!9c&9 zBEr2{2M->cW07Wde_xr&cWcwCtjx4Mo1`fu#x>b&etcU>&L8UCI={>e>rLxW_k6po2rK8+f@V`9a{fi#O!wKn<8N^(VH1FSQq z4W&-c{nH+7dm9nM<-kh|NTN3n;F@Ul&GaRP9t8T&l%R!kOyM)7xEBuBf%-rK);d9(nW=zY4Y6;;Z(t(Rv% z3^0+-T7Sk_yF?0PYywO|{j6n6Cz#jx0i9iw%#*AEHU-*EA_L^Vo4ZvBDDuH^>YEr= zJiw-Bza2r^zu&Nwy4i1$zip!iQmxQwR3$G8TXw>mIi&aTsMHjHIU;6gldU1`;G*26m=}k(KDBbn2XISq;{ROoAH2Qau=5qJZ7!uZMcamq#WLG`?cjkAtD0=0--P26|g zbN^#?4#_Pd;2Gl)M!zX3FtH-u))@*Z02sC9J0*DkyzAJ&A-~FJhkmMe^Mq#}OJRkr ze@g9a%z4={VNlsmX@UTwqjerqZyYr>14Ye_hUON$Z$n9J=&(@W9D8vzWIp>8XkY^ zYyqas%ioO@3zbSke$~(6IBBY2vB)hz&xs@E$unp zg_`qx(am8zkPSsBVD`nh(JFgC|0$+^{&PwI=%M$#W%*hAA^!O59IzF%L5bBHmR3xr z`RbWkx+?zW3GKotC&yTdi9D*!SKs8d<*?jY@y$P+gJ7>n=LV5Le_%taOp?{3^K0=K zJIsMmHdIT?U)Lq<^)Tq`%(G0us9x%La^(me#j>gNGuS|}n|EzZTD%`!Fak7<)(1?P%FbbtW{ zO1$MY!TVdggm_H1mraLxW>##E+U$|t3}HozG>2SmW#ahK)!x*+-cP1Qqb${eeL$YN z_d}qgannArONCo=U#eBlenqkhO=PogmtwJLB9U6C_bMH>FZPT=H~Ys2Gk4vZ64$&;hsuRrwICNT}! z`n;a~EVt)AA|T7(C=>V-@*X6d3W zEaTrV&_C44emCJgX?J5BdvE*1$DqQc0sEcj=lYF+He!Te6B$h=rj2L|$BDmdjG0~w zWrB9{&AFW7T@-4G(4h(f^)Y~HkzMoa?dVPC>Lupra|}h8cSwYqcUX6MRVky^lI}_b zY~_Av7WUd6prId$a6oQwti;)$6bL3qz@T?+>H`Fjz2X^zcg;oTGD?e)2DVgG2bDVv z$rT{Thj`z{lT5#xLs}qt^t$M-yh`lDhruG9^Q~TMUgx8!sMYEvy5Q?YBaOZtwR2F9my@l^#tpzM=YvJA?-qcd*iw^pe?Gk4{KbYMCfEtgG&n z=WV?t%rlCB-lxv0+p)!kNhDj1cb!DMwJ9KI_kQhXY8TtL=Mirvm@nEM{}+&!&J66~5b-BibgU-Motj88 zvOX`uA>RhZAt%RSq*kbms#l}^i$G|k`zchd#_A$Q)pSQ(!W%to}o^-u`^_#Fq$H z3Qdgx!so8`N8!3_o9y^PD|ZjeA>#h!3}v3qB`U;y{ucsglt-Dz8I611b8=1Du}9l~ zvVp(OQ42S=q3thsLd$psS)}d|7-f)cuuVH#7&a#an?tSR}A)=xLf-Lqp9lAR%b zU9|tl-*_Bm1eE{}2mcU9tagE^0Ev+yLJ&S!RuH~~smp>~m-PJ{?sZFKbwX&{(0Y$oFN=LxRt02+ zcAaTHTW22ycJ3OqtL{gAD5P#Gcbk#Si$ecZDx$5Imv4oFXN+9=7`sox8(Nmx?s2Hd zg>9eM88yYmD=W*wqsh3(8gaFbSGNDki@s4-QA>1aS7mCCDuErt(&h}upm5&t$dIPH zy*SJxo}m@yI@#ocX5r%lNO)bWN;&iQ;D%lL_Za5}(R6ickH$TcR?{Z8L!>z-z>^zM zrrWIKM#wSOE=~ksfF#;ZQ=jievNq7_ot#RnaEa~P`0AGau}5SZ^|?dJCj7c2cFAut zNb!mBiP6?6e*==wbBPcQjB*3ec`Ugm2C3g^xE7R~NJb>;k3utY4?*ut2+4IQH-9Yy zIKOTOXnYIiX^Ay8>MTomwtAvM8pV?xCBe03 zxo+P5r$f+0XvVIeFrTlZYvkz2&k^X;JbAg3(zZ_|S|Z&25_=sZM-fdRz``_TSVdAn z7Kt`&aWTl)@>eB3<`*%c?}+)miZ9{OxRoAU1B9E%Mlvm>I+ZGf+xOr5BK2K7bIk1g zf~=lRCM7H8aG)aWDEb=k!5R4*vFSC&;ku{t@oHc}n$-HCk8KbUNBh!*UP zb#R;NBh#r-xJ1GvFWt^RmT6t4l&cESb@T9q0UeE!3KfmA_<$BkT1is`pP(-v8|N}d zH(=8bhx)NfT6vM<4q@BHA+1!7vulwM(S&)fShkCkPz5>6G5ODscu};KPuiDFzMxR4 zRTsn5qm07-`G>)>LSXBc6HNxT_aa6bPf@#V;+8f00E&^8)0D_un(b_Bbg)7 zLbN2f>U5(Wh8}}hnPs4ivgV@__H^3}Jgn$V9@LtX|1TwoWtzKTAC;L8`^Y$nXS6;9 zaH4E{xBS&8_qhwz-eN9Nn?=I>oW>`i#F|VLS5KOdRMiMkdjRt|Ka6OEaq9g6C8%^< z6uK5Y$Me1z)b)A(Q$*vN1BP2e)YcAUxAb>k_DS(UdD3X&ke`3pqNUbYTCe99@u^(# znUHSG+Fek~sj)Ocp@m_AZ8E;{ z7oj7B(BPl(?u{*~Pgg%Tft^*Neb^M^0I$2B5oU>!ev)J>yl%9DYfSt_PLCe*XiQU0 z^2kn+2IsIg>Bb28c9ebF7s4sS@*J;C_pj27g{VJrv}n@Ggtp%|uj=BPfrd7SdY60X zr+5Z^!%i_$JcO43F6rUZ`}P2)rs!UEYB%cT;960}IvJ~El>!m+WdYy-uUD?z{X9e+ zVGipB5epL9nox;i-3&dY^z;|eK80G8yAyQ!&y(Y)H=SaXKwkDwhA-=sb#i8nU5LNC zV&xRq{TF>S%ATCPuOz!2%FW)@UR|sl)6uCXM4(w8)dCX^74G^!p=RfN-D-uEgA@N) z3UyPga&+txN%mSq=Z68Ejlt>JOj5qU36(8jfFTMKZ0ls_fnhnsXPg`1JjR}KVvvqc z<~?Gm^GimF4+M+yvY`Hn7nBTlG7%ticAPzUd+%KUP4u_!(&vzY>Cf6l?b`s=feNeh zqQBxDhF(DXgg?H60e{YOKkwmcg>SHbIWtq5`fx|L-1UT)lFMU%&a>iNNV4JmFz=vx zFH@cS&{~YNH9%m3Wu-?!rQ6LNU8+dDzz8xU-o`lqx=xqgADU+s08lZ_Gf8v!`Fj89&rK{%;O`Nw&duay zT%a(IQ7tah(9EwBm%^)ot#U|fzdHb@~PDkq_q8vPT?)dBodP$1oU+}vU! zyxl(VXL^%sevWYuaNg}7-pw{%O8}T!_<9qZ@+AA0C%_BPCdwgA8I z0562yX+f{@1KyXrCK_5@GgmvFcVmO*+eHp|$CG1NbN?otViN7ar9Fc3gutd#c&)@9 z)*Ovxyng2Q4=)c0U(kkNhCeO7A*$`e%ikz{0tTf3N}$hN7KRG???Be=eQV>}@JRvIK=56H&G zDe^>&{EKBTziCTK9hSMceRH_m$Z2BYsVT}RB9fV#axh~6Q2lvX4GNismN45chDF`H z-kmRiepsghs(h4_c1bZ#ExP~0Q_wxvWycd<`AFW3lK@=z(R5`}s)p(Ns{W(TZzBt~V^6eaYn1acd!5VOI8OXFXu)lfh z*f6kVg*~Vo%(%|0JoqQJ5M_6*`<-HQTf#!;==9p*{1=ilY9VGWWlTu7{)rES8ywTO zSEJQi`)!?wAr?} z1$8Nce2!0>OVAJ->(JxL<^bC0Myw!$qal7D3q%g39^tIvDiD;59}b~WKV6tJ*!VWZ z9Szq3D+u(AWM{HLh;ycOv=JE9%na;mXz2+W?PYr@tOjVuLDmr zdez=Nje^3hS6Ex5`h*=KDhyhsvK*L4hbJFz@0?bRMs4N7GA+AM;?ey%`;cygMg&Ib z#-~Ts3bRu}2n^mmckxbJ^wx=-i(Mk51ICy?C&4o-_yxXa&?#f3Cp749?O&Y`YdZ{g zPiJTn93dTIt%n!=cCVjxW{hsv)(65{0(y3F)QhU6l8xU=pSFGQ31AtPqG(C7`|L~; zhum#gkVQ1ByYh^g`O~w@qy3u{|GYoTFdN|d{`vTk>cJLj<&Lb(Q1p?CG)FmHcMpHAEWi8n^K`{W!y^wEDc(V4^7u(<$*F=o?{BFkYSrC;ij{DGF|AZfJH2uh zj1uO^53%?r-Cw34pnVyR>rCr@!@pZJlHQpHPQndCA{l~?#Mt5MzYi*(89eN+07w(wK}34Z$cbt! z_aSn<*fE|((%U)z**lsNpno1-|-kl*$>0dZnr^NAE!YY z;JRb-&rb=61pl{=r<;{=?Tj>AxD${Nnw8N%{q%i;LS3*bwK^OsgzsM$uUNPTtaBJ| z=M+rW{IBDE^yB}TVmo#wVFuV!-#cqOdF#G23?|xb)y_4 zSXf3e&h#@)aT6_wHdAW4?w{oE1hz*R{}mXe5wDgd7O1=5%hYBV1vzK72u$2w;ZGTs z75`_#_E>|!ZmM_NfE7@hqE_ABbdH&1MziVpq~An`Qr;ykp(6b0unTD&SJ~>n+%HX` zP}d&d`%R;)05H$i!~J-9R4HPcc58T?4O@zm_45$vy&MJV`snpwh0d(VF68jFrN8VO zq-7|qu|~Yx5Li#QT|@l;0RHJ_{{!$>z}KG54gVj2KTn@dpfuDq4pA$3C{f?m7JX|6 z80BlXYxlo(U#+I0FSg`sDWahLJbA)1ei)E@`R_4-H_D&GoXOfB) z^3chzQY_36><`?%(lV8+n__H`Mxj`=MWPvdq*dChQ@XlL9Bo~+ z$L||4+o1YS(>cZ&eS-77n*I2aC7XV7kEN`ee)SkB+<|o-!Jb_H6h*XpsIE!4TL>?z2-$|{`i>wHgAOVw-;W3(f$$-~hHJO%Q5abyqkd`0D&O;rvI(3-=3jxalv!062jK!uh;8(;nmp(5jP{}AfgC`_X50S*@!FS~`kAlt-MfJL$cqR%u-`^&cEl0ZLLqZ{u` zGrYy!r9HuK5=q8*ni?RhnbLMHzlE|J4V;dicH*Gx4k7UZXSGG5*Xc!4*q*gNqhNW6 zjd7~ha@&JT@4zR_PVE(ga&Durj~ybAgTxs;42<)G;S(d-Aa%V0)3g@rH9_jsskGW5 z*KJX)Wqy*xxif=|g^6ijxUoqp@(@tqVzIA-fgOBYel#iEd;bIDocK2)QST6E59|fP zJvhR--rxwN%u>EUJp}o20mt#1vhc#lEs&%@F+STJV*qa;eWGY>+zMKsT50W3_abA<9^*1#0#c z!G7i>NYUA1`RQCGVht}8E;}f-bmXhJK&X@r;)|AKk4N!=#HAV9N&c0RdrjMXNtSRr0l zw8<{{!yqO=wgI8~Biyr#nt0wBns7eF2IBw=0^{u&3jqTD{QW8T@PUWv$li|e zjk6Wb3`aL_si+%y--6A{08+}hT$2Nt2 z1(NcGcEQHI!fA>%{S;NBgWM-5)Q4OI;Dbgikz{RuS$r29r|pH{21%kPNu~; z#~Z;iQ=Xb}h)|nulS^F23FaR06iVFfk7RM1sc5!j(J%veBWx7Q!6)%H;rn-Ay{yCv z-9m7PF|H4Bl;hv%X=vstm2(=lFplB3yI&OUz4dy{q7^Q&g(|A;)He$>cieAiq|<-e zx~&7PPH7h@zBoAA?_i1q)MM>eJwdtxJ6mpzH~#ZSx?uP2mvFZF3cDMoiFDV8wM>J1 z#yXi~M?SAmpY5+u?G!4!{WBeBzq{yp9r-0%#NO{518*h3x{F0Rf+gXGwAXDitSH+Y zEvwx$&V6KSQLw+`^acT)^A4`=%K@`&QL>(ZubZ2PA76bLcWAX_8C@53_nn|M0*OHg zR;Vk~dIDz@H$!WfT(8sZq0@bbA=!p;y(h1d(dZH`ZvO3WCb5;^ z$@$8opX}R0HHJK>Rj$%Xc}n_3vQ@442A?l2=BChWnn*V++ToQal$CT^>qs)4E{wBY z_JX-4muwl9!9<`R-Q(SV91Pt}Wlx3;3#W~oZOGdr$p$p>?d=swk|cdd8N^bf@HxQSU@Msi zhhMGe7}Ce&6Je2pe{GX^oa7b&{;jZ&j&8|>A4eNR;{Ue)VEzH*$doe5{3n(O7uG9pu_bqV~vC_SdU ze*;qS{i6nguOJ_v9qEKvJC|ed9pfn2nRMS1aE-BFMi$FV|q6Ji!{+!#tx< zMYUv-u~rOX2N7o$WHw$ZQKBEkGxd#c8M7&%U6TJh+zatK)`xmhDf^D!q2Zi@z-$Bp zd26}k5S$_&x!>lh9L`8T`G*=~Mx;lf+F$pM*rX#@sLrKG9PRMPuH6O)@kcQc=8M81 z$g|P&Gi#?Y6d>iGzCOZf&La_aZ0y|+>(^AH7@yc$UYyMQsxyR6n#z zI{P|;y+C9g13Op%lb_@4MY=n~$abKfRw`=){FF>%e1Z$(`x$z}(Qg1oDik=vGWFoj z1s*WnhmfHD4GZ1gHPdwYQsV?l=07uAq`qy8v%maj=nKEFF<_p${hcGuegLe~cinHj z<%O9aK=(L^+qXDRiN?l|{QViSL-dc5FmwXnEVm2p1f z{wGb7N7|i1gI^~SP2*&a{s@Vd^(y*zXP67xj}5v)?d12D8(>`vL+ekDJsOv&KbTe= z_f5}IadwO`#aicT$)|G++#;zsx<)u98z!fRKr=tX?qHq|aQ;Fcuahh~`SACsmlKO1 zU(nMCV^0yj3VMBN)NC~WOxGw6aT~|qAo}!P?ykD8P(UgmS_eIXJBfDk0{CFz>1$h{ z;OiD@!r2Pfe8#u>en4;HpP$w9?G*O26w8uKV(vnFCg^rZ4Ws_)qpBbMP>Z)YL`GOi zv^a$u_~$DXWdJ16%Id8JI{S`;jX+z>eK(A_2QV1{hJ5$lE4edy9`LLU4XkFPfb)K) z(P?V2=10){48g({QD-2wvHZh>>p{->i65_-7Y%cs9{XFdLa4h|71C89-m16Naf3IqVO0Y(V176g?eDrcVmq4%Fo5Kse?n*_Fi61qnyXvHmNyo#A#|vP8 zH&1D})G#!y4|TquCy`5UVB)^H8hP)2^75thVRGez(pYa^Ye$9~^Af*$OCSR1QO8nKK_%R*kV<$0JFVso5IL5!-GD*^UJ3*_EEmw8OLaF_% zQ71(DlZSe~mv)UjH&Z8eLx_*YIk3q$ML|wC8)huheGv9crdga| z!}vzO{OWMX=UA03_pWLbB&>bC{=?}B9M6&VS2jeYrPlM+yHoUh21>gi@DsJt;aQcM zc5~$Rc}!I_l+){gI7hy&F>IXxE1E9Zb}$1 z)TEaO(5SFVy>902p|(cvl$&N#=#;%jngM~yR6waeA%YEZ6pCOTGwA!g;IqzA@6uj^ zzS3UJ!;_3*oUF1GO0-y1n}i564`Y9B0TV1HdL&D3mt_9Uo$_UvgA7Hsb{YML=F4(+ zvPpt{REV7uZy(+!r1bv)=|C30X%ug*(D3`B(zJ`pIB%Wn65pT{?{t6X7rshasST|7 z+o%BYZHt29WSC=s`!}h2?I}@^hjj||8r$3j@*zC=_Bs{USc;8Ek88pn{Q%<}-7dab zes~1f3$V)n35sL-0{iK0mGZlzL(V)^yI{2}_ZZ>nH)qZPm(;*7LB9RGs?8ozHS!}8 z*9a2jG>e9%48P4&fW;T-QmU)a#5t-~lWR21{Vy8KpJs{ve2p~29OoMFm2EQ7Df;sd zeVepeWrbfjN7T!ywqS2+)x1MMTYp8QU#pk&kMc@4&wxKG(I_@)kT}A;|m($32$lv|R%657K>rUzO}%WWqlryRXoh z#=k_Txi?61jY(Iex*uO2&?h*TS)9W3%d1skFZA*u-hpNRH+zpnP%xY`VdWc!z(mZpjt0dezqeVA&7( zJjF-8*2W*0knDbmaEhj%U25Jgtn|w$dz>TRs7^_#Ri-t~Y=|?`Gr`V2g?=v1L#A22 zL9HBE^}k*zG-N`Mab}4W{5saVR|MyHgGjIZFWd!Et1Qh*uUP7*G9!|C$Czf#Z+5@w zWZSqWvrR^5;~jy72PxNq12fCT-Z9Ff+{iaK$fDf_e|vvjBP`W)PcJd#8r;U}6^QrA z)2>teX7_?bKlToWd_Zy{*aa+pi_|sJIr=7PldN)WhdAVOrACMNJ7}MvN!~QeBXpWg zs;yXOsP8$tV^XP(Pnd10T;mA$1Ut(#u>3#CTBAXDD$>H&dx`B7i}kdQbA_r^FVO*W z!Q1}~f%a#FcaNY$=zz?y#4h0xe4Sjj%_Pe+YKaV3{y)ayT#)P!{9+sF8RrxB_FSeO zMk*8#~d?Dw1>oGE7h{@39T}a&#jVwup{h&m;aHiEBsof zUZS;4?Gprl-6S32CERsN9OpR1J;Iq_$2ys0Q~gQ0iFjj{O}dG6ZJCi}&Nf-B2Q2@u zQ#hyV6ok94QxP4`a>;gdaIMmHiR9{wwLe2MtwlQ%9$Kbq{N$P-UuuvNoVY*+eNkvW z#+jr7mi;prwG>w8CG-$jZ!-l z%~B4rp2XqEOoh;uyE`U6_1%OP6)XXi}8zEX`?n|`@nQp2=>X!fCD zPL*1fl4nT#n^w_Px@jijMPTs-dt0P)J`M4nVF>py&E6v1!OJ!U``0Tf{0jKGMq!qX zah7LNt8AJJ{u&shR43hXfTvZSX#oNPEdPVO2Zwm17!ywRSyOEyT_{I8IMhO3;xUGuh!Tlt34E%pl4sAO2OOg};49Ptk%ILEq`=yHz`?lR1Sf;7pzg0D~+6#bVi z0EvH#00P%6Kg+m?d2t{4>hwmr^NLt7Mid)77YW4*T@CLymTdZ1sn^V_23=p?#;o3ZBtV zsK1o^7qF$8UjuAY40C~1|3-M6za0~AQFe%$$C%`xUqC{d6=;{x4+n-r-&bpZC|C1b-1if777dxaI6bm$_IiG$MSjg3Z zJ|`P>iN$!;XhvJU!`G{8)h^Pv^Q4>JL-#0Sp8I~m*aw#XUE-MMxF-$EAuhyv%5+y) zol{bEp>7E-FYvlW@i*(Fy2K!$pFvnVy2KaQepu<1B)P+%rQ1}?0E>@#3j5wElVYz{ zzDwX2bNd_^4i4uWwSf%+Gse9_1A3ETt@+a-tyJAT)G|YQz%pCx*8uM+;V1*}^8aaO z$0Vs%vCdX0vW+tg`xMExd4@RG9U|pg;U4AMHpza04)Jl0ShscZPO*6gsaD!0+(Y2S z|5o1*$Y)qjk=AKvsavJF$5*IYWe2%BWT)vWH8JBD90LgtGR3CWgtyZ!6!?{L75$uk5AnezSijmX8RMu>*QmfCw?VjBQLP#7CDkTN z5A7c1>XbCyM6`u|3RvaOHKx+SJmZ>*w+Z<`yQ^II3`Toeswvb#wX;Y{ykwX~dR$?W zY{xJ?&LY_Hhk$;Leir83te{>2NO%yZ1e~KdSJo-3??3ccDc2}mLrQWxqq&&oIcnEy#OJOB3yj&`XVe8`sz7^T`i?nxGt zVxj}JpP7aaD3Grcf=>|n#@}q>Ep~7@=0_!9?=Ux?!ABT*rqdkG5kFS`_*HsRg+}4Nl*yE{us{~he3v2Zl8#9RjQeEt5Z^|*k2f}7M1!G!$^O@!9R#Aj3cah zdIMY^<3qf>fd7yEsWx2Gyt4#bK|Wst(p|I)JB4-2WPfhZ$@jg#3{m?;;~pL0t5?c2 zFs@K7x+Yv8M0xpt&T>UN`xt-fzufMB`f%(c!@@eH?^dAi_9+B=fb(8soI!yu@o!+L zf9~P3%}%gv(rnU%dgUAL;B`xusp0N_DLlY_wDpI18yEEcuQ?3-1LETZ=SCrjS5T;T z5b8CFX2k0)sxx@nd5K<^)Jb9N<91%0!vWqK4CVR-6815kp>EDRtB1mff#VA&%T_5cAo5rY^W~M>&04>^Uf&6pA z6nCt1qlRcZ;l>DGj2-pw5dRz4kNID&;hWtH1b|m8JLh;;Km-@h-=vrc`i{J91v1svj4tBUmT5BbJD zQn>~LeL6%uqho!bA4EEcj%)+*ucSH#Bt`{PD{31Zw|AfBQodLbXdoOwrn zEdH)CVjX0j8sRS2@`zeyR&3|%8W3ZdwoOPi!#rf%4hYCGa1FmfB|ozdmFh`z+rf2; z{#2k$Y!^%AKyOV!-qd47KtT)EFW@??*DkbT1 zqPu-=xy~9T{JDIUOq+Bs;W`A^E?JZV_0u2P?6f_kD~pjtV@7~$R}M7Uj}pk0b_mv6X5SfN^>(kf?~9pw9%|HGru zPsiw~7aCp}kgnEiH{UxE(sNS5*U8|0!r{-OtdA>27d7!+M6=#-~f&eI!V*RJ-7 z%(M0LR_vT(&DK&W1%G`+{NgRNV4K(~TCVIA_c8k;gQV(W+$&VcH*b*CnhTBpKvQjz zuC^;>>+<&dhNRhjj-x!PQ`XDj9OD~&flRVxp5Dg~`9iy>U2Tz$dB8hTsqnG-=N9t^ z)jA#RqCx&sj(;$q-BYf{8Nm`wiHdVngg2Sy2rq`;#M|TS$G8F`w6ilT>ty16Lo8i_;%#EwLx#@VC-XObx0q$N zdo=O!<7=WT{X~Oiwt-yR3RA6ud3w5~M0>T?2u-oR;Ji~9Bt)}Bp!dhx{{;oeGtTc8 zod{>vu|b|jy?Lg%Phb7i+M=Aww25}>RXqH>BiP1g`A-n0xdn#C_~l#9z>yyU!^K;F zYFT7kM${;OZ2WyT2@MtQK|lA8tk%DVnr2!gmLJHlIzuZlO|Z1i*U33UQD{`I z4i0aZ*2w{ZN4_o4kMLe1B;T;i2n;`m{aE`q&lK)4Ec6a1+mLLt`i^ySgP3L$?5|Pz z4qBz2VQG?w`sWs5i^MAe^-re7E4WNE_HnK91Uue=SA=tdN!|^jS@y^L-=P}oNj_d^ zaDq4?$hcvidWZ1CI^DET73pDIWR_B{gJ7vyn`x$4O|dTiGuvd6W44ZAX|y-mpB+re zpB);JUb|Qy^FPwxn1Cu}#QQkM6BMZ+P33yUqz)_&j4OO*v-zS!vF1_bgND^$p6wE+6wh)v{VO>YrG* z5$<80LPMv-W|=pzZDPi`HHv3Q{sHD0^z&o~vW-*BEOUdRR;f`gyKngWOtVi=%C-7G zxZ*gd?lO^7zHbA!KgOso+@p1(h&I?MGHscY7E3RIel^;Cjw{#v3oF)@Zz$DP zXrfs0G5##0Q11x$G|MrzVBb$w`i0NYX*Te8*GOgB_i!keA%1q>$TqH#zS%9%E7r?4 zA>P=eEzqB0+NTh2BVDUkZIXU8|C<*C1#1_SZ=hM7WRz%pK=h9|N9*A|MGE^oE3BV= zNr=D2HK$P!^o4Z*94g-J@x4`=cB_+L;fL6OT07Bd56{pC$3Ns-*W?jSgIu=77$>tr zt9+}(Q69HQ-}rBix9@1Tt&&&B!d*YLf?eXgds)MSkBOz~J7l%;IYy815$@$mBLcm` zKGy#Lgu}&t~(`<%$ z<`~0#80O<$WSb<~9g@G<(Jj6}jB~6}9O44@gC;xd5Phr$T;W%k=4*5HOLe(?&DUYv zW*@{qxxknDg?9FYJi^>Avq_9{QDlU@Z<*6Bk?7!J=65$~RUe+^!w$^)de+lXNR^4=@ic&^)7mH#f}` z>`@OK49;vEjjL%bjJKk~~uLl<9<&>J|F=tN!!l`xE=L zQXPMvN%1e8csq};!6B)p!C@Y;7Re8PVL(d_mgtLgm$}p`LjoHVu=c}kzq>CHeXRU- z>gg7qA(yEmy{ZjjolRmx1Jud_qcN`LXl6LX>w>~gQMjg@61{`*_t~bB-K3ght=)nU z9!tzsD;Bt*FFBW-!av3@)n=LY4ky@Soa+%*tOxAtAl}yhG0xE;Zj%<{4)p}}#5P%{ zgmQ^+Yng$2cn43g_ZNnKp;E&q4CXJ)U+BkQ3Lo=-h2{~}6kD=mxc3u;c&}F09ODv$ zM>6Ra)e`KrNr7?tHM)8BDowtLXrEkNH#^pjYHO|y&wt8|^4TEvjRU<^FE&XcygDb4ul0%u_}a~{ zUs<9z$Pab*saU=6W93i1cZ#$|`y~?nXpY{XLMLaRG|S)wMYY*8evC@)=a;l;mP10q zoe8Enx@;SYtw!nBCz)Sa7QB6E%T~p%-{M`DDL;eg?#WsORqu4Rr}s8xO+=O#J9O1@Q+`w3yChFh#cRk~HM|2Q}ESh$PBw-=~cZq|`` zDxK@wP`@t2NDIlgKgP6GE_|S>}-x?-A?TE{T0DUoGA&*8+b& zOE1xLgZACY$5*o7Cqlhwm>YLXp`}xFi9xaQW8?1)_65$jPq-%<hRC+LBCs@ z=RbqVRrd0xI|hc5o=h{$Ft;H;yg=p?30Yu@~^OdVGI+uU_65#>wg}R`ox*% zG7KcUMfQ=oR{Ak#5(bK(wLWI8U}tS*%sA zzf6AzSu2Nj{IU86d#_Up^H;L%FAU?HcerV8iEgeb%B5mG)2za;pukHMqMcCRSMUl= z%2og1MLN<=k?wexCE9mT+f;#m*aythkLG{mdn%2zbCIrR*oqyM%H! z^78a47PIZHFv0I%VXKvvDKE%l?4#{{!f3Zh_5}timALy^C;Y#Rv3-o6>1Cc_o%ut3 zknayB`}h(C(^Q$FO9agc%H1Wn=%8_Nv2LVCgzpB?3L*BzIBVb+?{t|K%DG>X?n#~U zJUt`KHpw63x2Zrq*vENAzQcUCU|!M83kp)Jj&`b7qCT|x&f9he$~-PK9~e5vb_WXv zTB8i{7wUb4u}%u%JR*3Kd5hKT`ykWD#vi+s2iP4V?BhncNjA{;ShpvrSMU(OJp$S# z_ZX}19pZ0b21UXC+r%#M!T#MM+r$I>Ji|l0*GQls^1l#o8s)e~KUM>Na}0_y&5|!K z&|E{Z%}ue5u-Qk?vyO=_Ga#I}#O`3LHuBC;&&Paz1BZT1w3}o(h9x?e>=EdkrC?ud zl>`6l75pJQh*B5ny7l*A?jnmVB7ZlW!%SDX<_yybcJv$EwF3&@&?t`<>0~GVj%2$_ z)I7sX+YC#WWSn)Cny+E`2F4{0QP4LZ;}0m-$jH^MGBL~_;!ZMkaE69j7T3wl&|DJ? z@+;o>J`&@kZGx|K(x=ZO`CPU;-0WsvmhcYI zEt=-GO3g6;QUL4@H_m?r|M=doP``2m@@1FUd@HQ$8`yDHv{UBaGjz2oI429F0s~=T zUV(|ObQ_7zp1y5Dykl3`sg7`$L;PQYR*CYoD38WiG|E0U{!Y?L_C`ghmF!VPJ1SLE zFA^TKC^2;&5lA&$VIC2i6~!5LsF@Zou(pe9=DCMs{n;a=8*bA09H-b+FWaCDf4=a+ z_&X$IkcIG=Z)AZP|3tOg-G845{w?2lo|SpBS4=bK95=@K9OL;t%|6!#?o_?VEp3V? z$O{t6B~tt6Ia-5~QI2Nb$Le3Npw;(AxhN0I4AHJ7+B4RNJ=*l?I{Sp*BBN zG5;c;wF@mWKR_wevMkaq5+AiFkge|EHc2o~28K&ETV@b#mZ`3g{2}iXzJZ7Q;Qgz+ z#K+h?llw$UEhCIx2|x5M;s2td?dcUT^zqLhVsweuDoqP(7jaGJ8gWg#geN%W>$OTa zXL&{E7?f%TyUcTA?SG6P>5cf}8DW!njo}waw%RGOMl>skw-5J-c4q&*NGsU)4Qif# zjX1@sQlEWLttmKAq;DVBIs8k=r_3+@u1WD;Q#2pz|9nI4;l?;I&I*iFe_kP9qRcV= zg_&n)l68$E+F_fdT7h^8^O0`=oB`n-(x{YZe}-(5eS!i72@0TC8|TP1H7NR+|5vCa zyOml8c_vtn@zyD>ku5FUhU^c6DSSfrC-DbkI73GXn*ZHfL%M7q^4A)$6KXtqVv>*rUa zyk;fZ#Y;r)k#RQaMUyiA5urY-n1EoAmvds_hSv}A8)cK7M0yulePe{%W8Gq%=kVNe zagO3$jB*c%Vq9%9ZeiF*5nsy{D^x;VIOg=4u3;|VG=3hC;GgyLDpd~&)=GYK|1rW% zs_io*-yrYMF9p%AV1I{Xmw4iBy2YTtW$Ld1q?=u$!#wI$tx~rLLcK`WzMd)3&&YBt1#| zj7ySpbc&Tx=?l2hFN4BNJMfo$9fxm1ZKqgZZ`l@ZLA1X!+$K0Fr$1W$|HYaYsDFjK z&>lnl*+ye5OU!0zE%Pd+Rcn!7+T^;}tP5Tsvb2WzSSMQqS+=gw+#+0(49mR3OjEn$ zPmxKMKWaZfmS_^4AfHu#@(yu~3-%52ndFt~#Jg~hJVNV~F3_X?=@4g}i*aY4fOwH< zu2X7|cS`(&YM#|AWSR?%8=xpxkZyQq4v4*CmZ;`fyV#%Lq*`601HU>as#lBj)G2S` zRx67RdB-Q4y2USYndS0yKR~TfC)?Bgrkc5f`ndJj$NTtU{sJStVzhG%Te}3*qh;EK zwleL68{KTgJ8KjdIDb(Ueyvl)TD?P~UZt3L`r$k%)Z-m#)+gHb@O*s#zi1c!evHQ+ zb&GPEdAVAx_5~#CtY!WY3gY>YV3p1^PcOe<(-S1h^&b8zoni3-cDJ-n=?2LLR)gX% z1-d!loIhZhl5eO|xB3qDhIjDgON|Qo`U1UHxptXomuuV+&L~%uN4tbjuS_$`v__@E zFQn`5R%td|qjgH3qk*yizsT?rMR+*Lnr$*T;OPnaR=bX2y3`_9D=@H85&aD6@^eOT z6va}3@^2O}h$5{kc%NY5jvGRw{2E2H17PgGH!hfALpg+a!MtKw#XS{hP;A#KtJT-d zsnnvG`)2zk^b}XILgVKRl6=bn#w{q<^iLJag${X^`C~M;VIbf^hI#Z$Lxb*+tJI5) zc5%5{mKhJopP?&s1-daWuy*$FczP)pMuadfxMxP$IfppMQcX&X*8cNPol>$DF!&p! zrn!GmJtOmt#X2`hr&udBz+bJ?f2tDgKt40gPP522e98d5)TajgT1-NlWa-#jdDM{85fdm_ehhkz~8-ra*xka&(mo%?O{>uNOU!d`-USv z26&3s)2z8i&hry)@OO0!0)vlwVV8Oab4_?aQlYU$1?*E4E{p7IR!gklC5?XOw7TBAey9jw|Q#W>>Y5x!!zOH78N z@7KRznq}YZPOx}SXof7Kh7_U-ioI|{ccd^W<)Bx7$ADm+d zun4aZ|1OptmU&i~*U!PDoQ5UVna^);(2sDUjo&}1{u1mB3PC&Wk*btWa6vxtj}&V? z!Q~nN4*#dBUj+M{Qu!rTw8t?L@u6bJIdZqC`#t~0$pEyfx{Q=79WW8vCZOX&b1HnH7S&8jq$e&5As5MEHuuyFwU|}BHro| zyF$^axq~P&#oI5`zk>S`%rVzUhBf8Q(Ds8q*0#4`*m|Nk%`Ivx@c8dT2#0bgXoI?^i{9D zUdLpISp2hb_DQ;Pq!d?}>o$oDM~zCqpaYzHWX72vW}3x@xfTTlDnX(A-M~5jRi!>C zAlVWSE?4t8KF&G9MXv7#XBgPWxu#nrI7c>ViuHR0O|oe=e__hC+9gVLe$za_igXA2;~Zs~ z+oy1h5Az82Rcgos>HcHdrCX#K?smCgCxfC!og*ySM*pu;ZD*L-THax4=vJlLf{@gOPggj%FVFS$S=`0DTs7Qx8(f6^?#5q(SQ0+|LH&d zr~mYy{?mW@PygvZ{ipx*pZ?Q-`cMDq|Me=;E!Zd0Ez&L2E7S}4`~Q4hxQBm0xJRfL z@P}s@@VQ_g;9tT$BHh5`n`Mi2;vE3q8_zc`(-!QLY%?mTQ_?AAn`EE3LhcisVhs#p zm=Ey_@iWiTEoqX~|G_sX(GI-dL8v#wGS`%9CC}s@F5cx8T(crDDA$yAD%{&RU#d0I zQ=tE+s$cEviqmYakk=@hWa00GdnVZ` zHBPW9HEs}H<7`tI=2d@YS{M~juIiM=xYI3?ZRr017N2jBc$;;KcZgz*bdzq8cw3;K zcL-1q1o|mgd4}00nPzF1NH-bg`3LA1$k*8>d4{=105t_z{8U@zT7mu@BF%~pagOm% zIYtE=WSJK74a&9EYLjgF#)P}AQa|(u_*tgiVq@GtM=RFv5FKN8hy#o76P9nhLFN_V z5`Tz0%OunbI29|}MYJQ*?UV@i78vvZ>mEbD5b4P_`9q(4eTOK_XPbDBAk-IFeDx~8 z*;8 z0gKNwjCe!7j=z74K)8!>2K~q|kABWE&N9tEz&OV_M!XHU8HjU?et~@g>IwFNeF9K3 zc!vMqe&VCLOx-5gCAvuW0>M4fAkR6rM4M`RgIK53E%E>>)QfSZTe3;2Snm@i*hjfK z%hW065e4?<8EKyaEdCh=*$iO_SOH{Zg zD9}D7#(jpFay9z1Ua?YLq^JH5{sGx0VDSa|33my1aSw@h;O^1SS*Dq1AYSke*d{s0 zNVeD}k*}C$`3701pdUHMDAuT#k*?_$G0r%~g?oU-FEY45`;=2;Almf~s#7|~`4DpuiBnTZ9RAVDXP|3XHx609HA_2AF60 zg$VX#o2^lZb*j`PI6pxx(Rzo|E_p@NDnCOyCBA{NO`7IfWX8IkVgif*-D-fp$RO5D zu|D8ygctvSQ)14iANom-wyA+ZskUertW%40fR+&>steBmCt zMdT~mCF*6KVS#>@X^J(#NC4>X0slw4#54=_^Z-k`$urD1$TfNg&p1cB1lTPIEPkJ$ zO`3g*desXA2*@&Zw}@rNI{D{lzCphbu}+q0(oKY0vW*^LvW-%maBqSBVEQ%1-7Qb1BYK3Q*WBd=QZz$>J8Ah;wm=DUOSA=d!xt2hGn`DCX0p1#gcR1#$ zN!|iI-=J{MIXc|^H4?D+`~yhWw+KwL7$vQLn&AL0`2aEt?U{MRYXu;da|Jh7O7SK91Sdf^yf0|pQ?;=UJ;yQun!aLZ(#YxCAvihW!m+M zc_x|_)oMpLx+ObAe_?*;Lp^nhU7+m|ctl~I0*jAu3wU?;5!xq=VIJp*V(kTDo*~~D z?Cl;d%?ABE;OnOx_n3KxYb3M_;hswrk#620_K7?A7jeme`l5~@66mVbA9X$I4u;@fP#d^~$WE%*#=jiHH6l(@WRq8j08)We= zT%+5>=2;2Oa*bygej%kgYSk(=R4ZbgU80<02YA5ZEBxXgc!mV@1KMRyiE@oYyi2r& zhB!y0n^xaxe*aQ{dMeZ2$Ny&M67P^qy7^P}0B?^V(TQ#mSo~q0oKMp%fGlX7qg^7` z)Gc<4WSGY?Bh>dV3?L6)Cx^R_aesmu=YV~~pP*=$1o{Q~g?qS0v5!&zaE!B0pj`mQ1nj2YF z-6G>$qud!roa-3|u=oW=tW!+04#|LfSR=f0KD9_Q%}Tayk$jGZzjKLyfrxWetoIG| ziZIO$_9xthcp+ae*AnSgtOpifq?>+$VIKPN4UB99FuNnt4VdZW8bv?n9OE8gn!SU^ zJ)~cNyN7xL?AB$NCtnAD{J;^b~{TWiWDf)Aw6VLD?H0%S( z)*IL*N~hQ}WQTaRS-gu&ymr|jAL7j#1@Pj(3HAst$E0etH44+*SU0)GWQSrs>g7B5 zYosyGZQ?1`C=b!D65T6gg1urr{U4eYNY~9WA%4KizX5gvW)&j50DT+MHNjq`hShh? z3c}q>lmus+G^nQ<6~Ox}!TvsBkk3&bej&U=Ez-#j!1I7$AHd_hL)6P`lY)Kx1GG!T z+f*wLuuQWgTMYAHZ&WLor}+ENkhDvHsGoTT{1uQL0!!iVVE!K9xy7zhdPb6Nwo2g~ zc|};KA7dvw;O`e1%rFb~)hUT};vRO39N;}crP#Mh1$@mjA=>~Jzec6d@Dvm9)+P5y zlWc*}DAyrwxHr=E4pFRIo1|f(b$XZ&>_f5x;07GRUHuV%0g| zTf{vCtO=290nh(e$kfXymnT>Vw{=RBY|S#4D7DJdEc^rABE3RgqAw7Feb-2JN~2tJ zjF}dvm|Eo!FIna_D!Hb>;tTgQ$@+vf%GrHuk?t38Ou9h~3WR=al`<&m7pPJPv_Z9M zr$oxtTZ9X=ShszAf&OeWk}cr1|10FbFaclBFvL5&!~H^1ZCj-27h0u0<+#KH-o|@| z{7nPM$kJ?VQ=y*Tz+S;ONgb2Iy*bB##Rn+IIP3$*IKw=_-ZLciGV=`15!^lA!5bK0 zT?qUY{hW4biv;=+>6&{4Fbhn+PO=5O_@853z5#IK*c_vH$18Zei`_SZy;!$eWtnEc zYm`_gD3>UggM3jQ7MXg**vCXWiuHicXxN^r5gm16cH6E5dlF#P$Wd8K~zGNQV%pKX5abFVP{qIRi8mUt)YbefH6F7L2wjA22s?im)_RHeogTc|I`gl0v7A@8tV z%I$ZQ>l&5goS4FwYo=cWj+bqUeUfcTV1R8(x=p24sE>S;e3NpWY3>jovOqb0u`DA z0y-r zbxPW0(QeUhm0Hk`1|!w-p^BQV_YA;Ji*Z^!9E@3-k?yb$2wE1 zZcx-M?iF66GR<9~*(0J~WS;$S(>?0#ok4#2CQRW&=6RAsjO!lJ7Fn~LSeIks3e5>l zhlFcPt%_*(AV1mGcjHs+5WhL5Yo9sC>y&>P00)RU{Y9~Ek4T@;E-}vt@$Lyut%`J8 zqryC+c4?KmYm8<^yQE2;L`Rl|Tr<<$!%P1FD0%d^M&g;nYr zm8rHPJQ5ulR?hL)K4S{sB^H5r`EZk5yt7AyeUfgeQ#`|Jlv}3+a`xfdh!cdt<2A?fnBxO;vf zbW6L$0YUwO7k|dOz-*MDfSVtVad(N8>jnfcg^zbsq=$9p7ZUBJTxXsQHMnx!m9LQ= zjS6-ti}a6POtNp12L*h&D$}}0RIBp%S)Pez#koJ2>;HlROs<)0jDL`5Pq-I>0QR46 zh;^ENQKnVCNw6P@67Hp3N1{OW2YL{dTIn{)cFa|P!1Q;D--}DN9q0UTQ?9vIWtZ3| zY@cw4M6P*(p<1m)c7{cv@$$EaFJoN48(U=UkkqOe{Q7eaQ~3VDlI;fsiH>GDzYHuh zu}_=iSE)0sRBP&#C)x8%-rb&K!apQ980O`eNOTnJAL8>3J;trjoMFKfJ_7ORMWezj zbC$(8=k-q!o}aHheS&*TwAUh==;#%0nfdfdp?;HmwA-7z&mQj*hx%@ke>ZlI!W{o7 z-^4x%6Q7jp;GE}}0M>_j7WY`B2Z>Uy12&Lk2Z4aTfLgU`4JbwBnpJ8scmBIY`-b)k z|GuDIYW&?LHrZaiO6u>|E3z&5MiyC0_2Qj4C#%$UDPG}j(bnm&9tic(E-TeKHgAheDG?Ko_AQhGtwg< z2=54U45(&Bg=UUfnvG(^DEFUp`-FH$iVfb+Ki=3PX&Tu zL;P&>6`D!*0Rgz9V+#M>ZRI+a8QSG;(Jxou-?B{w-+zyYQ-WM`o(av0eexuGq5iYSU1F)WzM)F>mwvcK^A4At|8!lf%lJFY zB)I*qNYAiff}>5^EC*Bg4_=k%9OBb18x|l@X*NWAj)|+(t}%@B`-Fr18x#iw;+>Fx zlx$D4(kTu~#6F?FQlP=a6h6U0yt7e$*?E|%LU4dWIwtarbcx{|^$YHhP_O!i&ajMf zyT-80G|9icGsE)b>M1r76%;_ciz$3i0LwRVj&G9V9}4!}i zsMIDp67J_4DKuXFQlol;vrgHlaN)OTw@Dt!4(9lGm0CzXqu8KX$+RTdfqs=IM0eY|n)^R>^01ySx7e!IpbIgD}9tkA5WkX>TH31?Yg3SYj7cUZoOV}f#> zYE7bpZAzh0ypwtr*kIKf$a_PgpaUe)0pC5`{KY%DN4ZBq9gNPiN&eseaEu$`_vc)= zSGIY$7wt099^5|Fs&9U{_BqryC?MAqg>;WHEP&oeuIUcRx67BmaZIexRA{1yFV_rJ zG(6enfq^Qu#kwyaPP2ykW?1bJy}jcbYLm7{lw+1-_T;HcY=RTl7~BGPNZO>69AFBf zV#B-J=;1rZ|5rf=1U-ED;!&M)l0#5{U5ZDfQ9i*zh85P?qZh)xpRPxE%Cz2#BRHs6 ziuAxbv&f>@IK$!`Z&ZkJMGqewU;p-SjY{!O(Qb|jp+2d<5*@75(rujM(CJgIgHz!c z7w1H#m3?xLh;v-MN~Kn=S+xdqLFl1_>K*s^Uk@wM@ep6WsY<<3VT(+uevqGZyX<_D z1JNGbNh#Nz=EG|o@2WD#Ud-k&MM>fy8(d~S>x}rEjK@uYNy(YbxpEg`tkE3 z={CCjH|WU!t-nGe<2>cMP@ilIOeN-+V4j7aRcfXFf*BrqS3k+dCnU>n6CV6H#y)J0 zK6v%$1>KTW2Iu%M1F!H!`Wn?7Gpe;PAF?g-O^J@TcQ(n-mpLU22)IQD1Vy>8QbTSk zy7xbU0r{r?YB;%O&hZ5XuCXOLfdTa@z9HOW={Cg%=2_qapfdzjp;|Tg|0Fu#OqgS0 zokq9+yG4e39PSn2*(6W5M7cg7z&`05?-gFHcK6N48~TO#hip@=GHTVPxuAbF_*J87 zpZxWT!LRqX?2`}i_X*FJ-TZ(azE?QucA5?SV!ZRC7r!syoJ_I(z98P2VfE#zY3>5U zfB>*S`-B@5(rxpMNe-gj*6G(j{gpDw(gE_SY&m$*a^L-@N(Zztih~eIz=B zd%?*e(V^JDGJ`;zVet>*9f9^wrB z9RILfE5S*!9ryUgM}hh3}Rj>5u1(dyI zRGZHiEsVQMai@53FYd0TxVux_DPD@ZySrO)hoZ$jxI^$@ft&yDUH9Yr;byI5RBl+Typw8oi0O)bB_sA z`$@@oBRn_Gc*=Q4mg~xP76JmF5kvAqgl^6t?Z&! zdxJ@>p0mI69*zRF^$KX61v;7MZ@>MN;G7xjkm(Y?buiiCxFUya%^dAwnD^g?(h=e0 zAObn=*@_peKk_CXRE>5EVnr#Rd^na52bk}#n^~SBHj8T(M2(pjDUJ@wm0bo!z&|El zpOG<6+boZ^`0iqGNwW{ss45oYToK{n;2n}ys?d;3a(!fkeAYid0x#YH6lz2i`-4ty z=RHB}F#nj}c8`$be3#14o2Q(Ci&b#11ZVBzd9R^4L)_WT`(6= zbllvWBj_4)ATIv+^VS|dSb`XH(_TevEsQD%d1v7V$(fzMC zU&|Q#be)=NRiwqOm^+|zF5A~p`1a$Tj_*Y0_tL&DB8OR3T3oYrbF_kiJt5>GIPfaX zurPVpjdha#*E)OT&WU9EAt*Vvakg=Gu`WR|(H4<*=t5$1us_)-wuMH0t|N+G!N*xn zi31ArUoLTtp2)72sHo;c-CV)RMu|?1x-*37?j$!?lppEUf}bB0wr3~WZexePW?ED9 zvMdXadPE5i&+cTe*?mhau$nYoS_mg5mtKZWiuQPD7aVUtA$-3=J|VM@?iW6>zs5a+{-fk zBFN%thZI0BG&m$|rK4tZK>T%RP+&j+`&^@5!y&|M-V3`gB4;4rIIQJ99jr(ry#on~uY zu;)9x;RjIgaNjzWIDy?26l;vMO!QdkY?IyMO^*`gm0~*~==c_p0b1}V>{VR}1y8j7 zZGDr-+2{HqYOLrw$Ea>2MK#+nHYLYG92>`)atZq>@#)iEORj) zu1hIScKyPGdZ-^j58z2aU7&6W!If5hkx{?{<(6LaAZN1%&C=sP)Z-_8pkk}2Vg2`W zJWP-T6+J%;EHcR4V53o;idz$z~xu7t)G;&t~FeTf4*%vsfUCTu4Geiej&pR1+P@h4b?9q-TjE2;MkyD zVTIokeNxI7Zd1f2v{5V3w@df;8>7Ft%*`jT*Lp&nZobqg+7uXrc+s5#!$3E$%rvuN zeLUee;w7t2ADO`bfLeEP0o3{-Irh7FM&I4Vk6vs5*6~x~5e&!)N z%W7F~-y!ZNLYrLH`TdMvb?;DrIwb;9&XX>8jw8vT0CRoWxe_!Bq%hC`Qk(kt>ytD} zR0LNjN*bt-gdav__<~_4yU|`S=g3~9sd^U+W3vcwfkulpCOIq2>jl4lD7(Xjt9F%0 zjlm%vd;i`NDt11>KrWFG^07;bl#m$tN&fBz)Uumv^^>{4U1rQaC|#5CWtormRBX>Y zBHthVii&2T7s#jLVy9-I;}ToBXp>ww$Ngma2A;><-bEPkP;du(c{28)X*eQ-su%sF zZk13Tq`RhF^ZY~u%Z#6nH@WSjS-?fQ7zcGfDIo4K=(>|{g>X`-+pTCHh&tHQGk9_& ziu^EO7-t=39%8Ffr(3ArL68Da!)98MZ*xl$P=eX8R0`_v3s;x39L|>gQz}<#8+jWA6zNMtiIs|tYIAj2|3Snaa z-$b!)C-~|d|HC9Pk2VsMuT}BikaKeH%jbn#EYU>*Aj98WyUTa`EA?8}F{!dnOg=4p z6f^)puZ)L9&0&?MYH^%T!-4R~GUvlry0B4CxjWf?gPXwcJ>6Toew@lSTB-1#h)?<| z13*N#GEj_i!Zhz6?JPurq*FG`k!jV~M9c*0QSzVD3Skw`b6I~sl4^B~p&1;E;XBy4 zH(sIE`s5)pzsZNSZf_IU;Q8I^WoefjZ?gD+X z8`{M%batBT)h;lGjRctc{3o7oCLJ9K?^M1W!ufKnTqF*vSC75!elY5s5%rF)Q7~+V zi3$P!+etV4ZAtg%J?Q*CTGY^tMyOoZ&^*wn`yG-Ke*0cePRpOmwoex5wnBw`Z}mwx zF6wwPPx#ZhYh1qN43}x}?C=9R&2n+FoZ!oY9n&px zdq4hYZtiGDUks%`D5&Pw%)N2`^5l9tB>Br#q{lVEx=>}@q1vI!uu!E53HKML-9tO4 zk#DGF0>9B8<$I|H0&{Tlus6-)kjH~fauIltX2-E89vOGvc`N1OeN!c94uT2*A9ueN z84LCH$&AXDDyEv`T+VP%K{#j8u0fBhbkl57-FE}klKyE#F5=igL0ZdfJ|?-g*Kjdo zeg>0NVhzfFh99gP9h%5!gl{k&z)}hpF5=Y4r}a{qXJ??nYq$76x-BYfLc-I+)j1~f z{0rR2i$=u@{$t9I?5?<|^uPkY_ZLJ=B$NCk$C0njiC_UlCm_NI%n;t!P~-3yUfaD~ zXJCdk!MPwEWFCP;{2cOGw&lAFD&m8F`JoA!Ml9vFpX?3^#Eb2aiPuqVzLji1(h`nP z+p*qW@kpH>3G(*#~SCbb>{Zs_hopRmS7O zRfaEDZSvc!KPo*#7C4;3=x4``D<=WIQf>0h(=@`}`S$55F6`4phx!)5?=aot$CE|? zwnbUKpalCGmFLcOTc_MxDiMI%nonGmp;>hdgI8}Md1ARL<&A1D<0kV2)molchC_4! z=Qh!Cm?eS}nG0K{srbqV9e{R&dQGKej90o9VM_E{5L2s?TL>5p)j<;x4he2IFdxfB zc;1l|7Y5c(ikrD*QX^=08v8WSQQYf;Z8_Dc>+aK;9Nz7^b*#d;VXdZ9diTTo)}^7@ zs1M%lT`M+ueghiw`eERJ9{wy+#*%Cs&tS2E*KuuNp*FwolN=t1;lS~PV4ltH+k8!l ze%G+w_k;`oj#J{h+g)<(Z>Dikc1wxm6OYQb#&t|flYH>6m^VJ*nhpn9Oe=+xeI@HM zov?`W^!KkJ)>19m%2|pvfxF4&nQ0fv#&scs$M5IpC|xOp+KsGpl6_suE!s6-2a8wP z$L>#GNOAwX)f;ft(4BOL`H6cW#PsU%Y+lg{#{FoQ_mMGSqTRq-3@Tqe$WFi%@5Jym z*fmnJ>GO(mo2pvktnrjXLxtQGBhf<)RO)m}{Q*+0{fh;F8~Z61m#KJ>T>i}-qA(t1K@_fm8{8S5S-?rBK?K#UuC-{iG z74&7(?^L!{q0+O6#v4MKKS?`FF&4T-?Ieg4tI;Tq42mJay5HA`W|@&!`{}wx4QAkT zZ$-j5OQC>NR}TotcS07zUZ_bWX68;0x1YV`2UbG z3B$^lg_p3z3vbbKeZ8_))yH2JnLd7q-q3Sz>!o6c9An%AiB@-qOx~1%M%>eog9Pm$ z{^{RR!ahuZ7Nv7iOnG{UraRqdL2w9YuR)c7P~y#X>TheW;Wlkhz(@MPC*8D>2UTun zGegkcugA?|oSdsZ8BDTofQ#fR9yml~mCmxZ%BVPQ<_LJ1XY}8*r7Y|}&y8C2 z&*~K&>KmMX)G9RRsQ31Z#sAWV97Z8Qh)gf<6tN1WzPm#> zGtRN3+emWWBQeVldz0({hq8$^idVkpzZw@wR$y-ReRPV}ap)Z)4JVfuFsoHx;jMg$ z=-xt__4}ckm*lushH;1Rnrb9W%)?_Qf}qm`QPR3a<+%7eCAWoFY9>Sj7e9LD`jm$Ti9%-Y$`c#ruBm z=TGw@-K(FJ8{N9BgKlX=hVOTPVhz;J-YTA^FAYuILz~4guwxzB2Q>?3hEI_e`jJoF zLyOh@`05VqEG}_F-(1!{x%@Jf_`t;Yu+OLHNpL#H15iPalp6je)lBx!S5t$U9l!KK zMRSJ`JdlQ=p&!mz+^VPCfC$oZVMz{b%MXEZB7xOti})|$m2Lf;sxD}^d=tj6Xq!cz zwu#%&1L1)80bl6^IFILSklW1+MifR=v?vdgM|AGK`AxI5@0irTEAvf8 z-an;eN!%oIrsEA7Rcv2X8?o`Qu9g_LsZseF^-@ukca7?=NhrQ>Y1z;9e0+fVg%B&Q z2SBZ|w2QKkfIi#MAU*nZN`Q4?%BQcOImU(MX$l3E@MQ=1#`Fpd0yq;EKf{}gSZ zJZyYkqqj}x2%hCxrkQ6!tXff0JX#YE+y}CeD>qr|3I*T*4eE!v+vQa{<0J&fCR+yW z9Fo#=t+RcfILDOA;kbf=GVUnXFiwqzx~msSac`BS{C1uQF3R%|+(lo(rs(2~W(e%67R%6%-c~r|O=) z{c1JQrn`Hy4tJmbzzpG%!tH@;kGC`W({0IiZIjdSedvIw0a0U80*7sM8Lsd*A6p6cW)0TB_=l zRdu0+b%;mm2t0X~Af~+lsnNSAHB9wSMFx>_Y~dVj#!LE>;pl#n$3Pk$$;Zy?zi?5a z#3I4lLGE}ES%yzaEBxA^Y?oqeg4C1Go$aX6Oj-^%wWI3Npkl4FnH zVv+Wh^?sZ7etIKqP$Idr`m=O zQ^>uAaY4PyyQ|d(v%&p&@5c!}k}XUO!g?9VPy}#IW}0!m&+w+&E40IrjY-L_5VoIC z3#aQW5f8k>!rV@4Ui?tGE_F;60nPAI>?^o7sN|bxS&g6tHs>f4kbY?!n2Kjk2m`ep)am(%nQ8`^0$y6hzQ|k|-^o01}-EI6^Ts^DOk` zJjK=^B8r@T23o6N&LZR268fXO`iX5UXhw}7(NH}6i*E|ZZ#p`#{yT3eNv=kKl$MGBoym|BgIp5`ao;1KM-ZIq;*lO`=zU&lFU#$Wd%j&P?t z#FJlrKu<)NfN&wzdQc$G|N97nyz$Pt)JJvEwESdc>V?e`44+68en=Ic7(z*-mS!4E zO{t_@tc@h81(`?VwzpLx}B|FVGFYkmWO5f z*ew$KYOp0QJ$;b#LW%0+W|&{Ea}pgN-`!!(Gtg(N&vD%*y7|YaX0tSDT?-hIYk?u3 z9OR^ujL*nA-~$cz)GHxc$qY0~n8k^MVty=H?i9UJ0P&)XRf05yFr%z2Rgfwk&e zNM{X}C7q6T@U&9H$Z%=A=%Ca{&L{mX^|U!=B`C$+DG}=yTFNB-4_V$6ID~wwsgbh__4HNXLM3{K03?9;qlQ81#}U85e#XBY{3prj z7}cC$Z6O>p1F5b7dKdrmx2Z!Vr#p@G$ z^m$G9uRZnO=Oj8rg%sDmP~RUEHvx`PWlab!CBfSR?-|nASo;V zB7wq^!#|(a??|k(EKmj$iKXm9<{oMu6Khp^d&%SyCE_|Ga%Esd7A0?FhK|cgCQqy1 zW5$otA0F35dMdVw2>1YYaCT#&=sCBKH5)x9SzTK;nl0MAp;KxtTn?U{<7%=o49C~> zo%#a1jks&*d^Il~WVcaMnZEtdDpUE>kahJ}XB;w#QS7F@-&G@2K8n4!QZ*=GaSq zWJ1T8_}gDXkFPr#-=5ZuN+{GAJoFOD0S`?}0oHemP|*q4E6-I^J6TYDIajke+G0-M zj$Es`D9q~N7F2W%^2kz$N)Fdh+t|wyukN?{>Yp?J8=xtV4>Pxx3!^Q5QXbtGvqgUh2 z`tZ_h7MMQ7zg}Izd`}`e@`570zIt;R#v8!1|BY2gErOm>#~IC|XJ0~BKhrJ7FQA?J zelY_Z%SIwsizMPari&1-0nQidE3b%|(N7EYe>2Z>c*|%drMPt|w}rJlA&_SQ3YHck zuc=92NF^{LAJKM=kzz={n%3)r{ChhEvZ69j8|fm}K!Qv0DVPTq?N0l=cSz;1*n0}fl#W-@5i}4x70&YeEM0@)C*ZxFC+%Ao zu$)A&EwJZ*zwE~hk{4g$@-A+|z##yGg*2U7Cw$25ueEXoih!{|&s{*Z`$Jtl4*~>! z-?eqqr@e#1&tB@C!uXosJQD^~Md|gtym`$&zlU5?Mps5}F9B|I!q&`HQG)<`MpJPafEvzV(fRZJ)jL@9CXIi#vNX?+fcI zgl{Jgw55l5bkya~&rR?3lh^$1qsirn%Yp5zjLb3tpvdY?x1Jy+L;08YX%&$b7n@k^ z!P|4=(^lJP^t$V}s{&0;jKto)|cF#`@MhD_Bs7z&yQnQ<}xU{R}!*;_kuu=xdms+y>{%vwQ!< z`Q!l~YYkwrSv~E;398?n@6M&4lL9Fls9u-6kF$$^3j?ZoTo#J+Afu+#Ee^6p_n zV~<5=UW!e(wn1|#__ie!gbtg5Jk{bY_M57w%z-D$FTsPOm)hEVQeOebCZcHI+w5wP zbVA@xgXyb%eSovCs>B#+uG(t%f3B{K?R7HJv+tYNCFEeCdUxTMC6JfzKX>N49*;X4 zf1Dz;?^9w+`nvQ>V;++w1l;Oplws@r*w24>eTaDC{@gt9ujxO35_R6^G^-C1UgYyA zH$;$i8EJuidzOgjkp42^4+%wC%D|w2L}Nz7`j(deyu^K5-)>er>y3i#^MCGOVdFi* z-pKfk2Nx6Kv!_*Jw+};D+bLUmCsyo%Xu{7AHuIR@c=J96Zvi^l6i0ry5C5V?K{2g~ zfjjfS*@Dm!ej>pw&;xE=#vA#AFW0)h)Avz;SAFJ#5>`{5k1q_PZ1D$3VnD^)|MQ34 z`ySfoiv=|MDnjsD|E3G36{$r6o`+s-J0a$9Bv;?1=a)D8JG00BE=hm_vu?Ac<&W^G z=(s`pu#d~%adVn@!;_mqdM=O?yOW;6ixhRD94$`N|4XB8{3fb<_NsvaS&PE_-}NY@ zMUMKffMmD1d+B-FSeF?|>Tk#9y%MvHHp51y?;~xOza2LqFPiODAYqVu{`RYTJUNZ= z)#{z*sE++1Xy!SHAMh*$)em@V2RjCB{Qox}L9K{L$5@jcTzKFZ9|g*Oj5g^v)QY~y z?@pCCbM>kl&y2^};ik^TjwO8r6|g<9A4C_#d3{4nb7`-oujY{cSPAPxKNgMRxO-k$9|Ml`_0Z_##+! zglTYkJlbas^Dkm)e2yQtJg)Ye^OpzCURo-!hhIjIJ`fQ$G zzP{Uo1)Ki-yM(Ws9P(tg#owxOW1IWrcYx|F`DV66ePKc2+_H??j~L!R{hwh*kWlAT zqrFdoVxmBkJ(eXD$Ko?u-_9HfKBOe&6h*7b#>avykLI|4fT0g(&zvFK(Wc86#sQUO zDpht9HXl1{xubvW6?U1T+i-tOjk8NBx9jq!xQc24(3m^K_5{CpOLYj3g(E#tJEV4r zR|iz*zq5_5PtT_^eh3|qhVwb&iYh%oIpy4wzp(*U5rx222Ui8@j8-~RL+We?Rf%<| zJc+8X2H#vyjuuhM_>I{bF{IZU+X8!Gjms1|p56R<_=or5HHuGQXX8VLfx4TqzQB3npP^^9E1Hgs#8tx()}g-gi&zQaSHrhJof_hFNi?& zty;zj@eU!hkJ)clpiCjyooC>YyVP@?J>9HTDm*&=$9(#U%YtPVNd;)fI-rmYwT0;Q z;(UH2*i)i0thHQYn3sK+W;Mr5g~uN?C{l&X{X?T>>s>&5U-(IHq$}(>YvKNBa;DcE zoVrf23`__%-C_Gw>E9#OB~M`g404GilFVg)Jdy)Q2HgmzG^>bY5oOxlg^`g?fjOl& z7_Rcvt@iPfEh=8W6$>I)5*L1Y$58D9{rlKg&Z_j}+&ZCkZgmhvd|W1no(gwkSngG( zU6HGk`33fRwnXdCw!DYpo4fP7eqh#2l_nv`a7P_bDKP5&;|7vjdGX0Bu1KfP{<(Dr z!sNo8Qm|UH(jN^M>{a}rh;lC>VNLNupJ-Se7!7>vZ=Hnh7<}k4m?T>v+)lc>^MgupaQcJtM z{d&?T{_n3U9|n}@s6#T=MM0jBWJcjH-nD^B^*c0$iVfx3vcog^%*LxuzVG=9l_`sP|`ZTj1?bU`B@oMxZ2D zOLWdvheW#7cc#y}c=pPTi!)r5f7iwq+a^9;oVzzD)FP!&;C+eE^KVwRL!HTT9VEK5sz+9k{ZQea&Ti9p0L-ZSBQ0w+vQg@v(oZ<(9BkgtwvZ^)HtsJFLh7AaFgNyPz(P4Sil?Fwi& zg8(WGF!>z?+%HL~ssUt~l7L3DkX~KELxbS71-^*gp9K$?MN9gt45|PF>xw-VV)#2e zK&W$$Q338jPvkZY_2P;17G!&*-1-7xg2yWA^i-%H+|xhg_$J!7$gWCE_@{z)#`I=1 zaO;Y5IF!(uHD>6uAZ?%>4z!;UjVlr&6yBsnPP)8xrDDpilT{^TR_wbRD%|^t%4T<1 zhsi5Uc#ejXta|q_MR5oG$z;5x?UK3^5qS|wP4bVmyYcFWC8kX~t;1%t(tWW%_`4Xz z8&73V+v=tnb1_7l%`%i(c0?2$RJU_}(1!FA>>dAHdzl~s&9jp(p|r0Dnqc8E!_SODfE zt-uVYWKh^=W7wOf$XDWRx^>!T;rn*Ewa=f8hjM#@opHm0YSLTfes+ZmUs;YCBl>C8 z_e%cUTA3j_U`}O+ zAij61$r|62X!1z@-W=7CsFC=pYZ-sT9IZi4?SNG~Gsg!U-s?XC-hUvWl#;&!r)?bj?1eR~>)-G(**%h91nvB7 zXRLpg*$31!^vf~HcCVCW;VsMv*^}S(VRXZcJg@vp3KQ z=WCbGgX;tVRo=DLLQLu%faZP93((6Sp*{oLVMuX%L~rg5dCR+GK4eXZ(B(rS9E08J z@b&8u23u6aY;A{%PWYlCK-H#0N6f{YW3@()={93JC)~LU_tWad^Tb@wtmM;0ym2Tp z>NU!|ko(8|SmNCMp8Cc+_dFBtUPa4`pF7Cl;r{FgAb%9D5%6dr$j^#wpB~Hds-9LUhI36LjoJ+-1z=1Krs>CB8sio92 zw8sj|Fu#0nB7LuHk>>cC5i9V`9;{VybZL|1^HW&}+S?DaS4lIwL0+kC7fLe*)eCX- zx7#pbv6)}Y7WdMmc|pA9HNSKk(e6BqR!6M*AjG=D^g}l*|12o&O`FGv8zIp%T4i*_ z7AfuXsvCq}l5Y8zY#hlGXHwPWucx4HhqA45N)sIE)Nu9(h9ErV*nF>b9ZzM<}!wWoiHQ6FUR9 z$ghrQd@eQq6Lx+CCI`oRx2A7Qd*#-A^T{6<5mHs=!?YD0tj@)~;+&|8)NioL~2CnMdD zGCLpOfET(F_r_={gxMX}MYcEfuii+lrb&fgeio4EWJ1v>)h5vtU7MI+eH zK7%RdHD@{#6JVT#OkfI(!M#PF-j80A*7NamkZ^5jX*&Nb6ms*ac>55{w)B@UAO&|n zLB7hO)ol@4qDFYhj*D$TCaF~d{%RLzTcE7#t7`t97eRcD*Te_tj^qfdej+7~V{n_{ z$o5zE35shoe{XiQ8gAb3h(Fb~+GgRVr z&%|Z&jo;@11Plkvv}Yj(V4*FTF`~*N!6!g5dVz3f_(*me%0JL_0&bmOM9-iUk=Lz{ zcMvhh#2Rf*(T(f$azwXfmM?;YP!Q%jpH_@n1v`sG5 zVM~3!3iQ4jYP)x&2Da=N#9ZbL>5!|#`XP$-X6VNS2?q3OQZ*bOA30|@<+w5KfGPh}*#ZjvY2Jzy}{JCC+1 z1=9!HrzzH@Q;YXF7MZJ?BqM*P^nCk8X$~=)HVJ=Y)iw#Adb3A{d`aRF`Aw_t3Um#i z{ea>#^!la3_ej5rqnyS%a0|P~DAD^Uq*Yj@Q0kv&599WAHQatLy&`ZFGzZwquTO4| zwtzo)7)sPU!L*s_CBOR8?Pigb`a>oS`-DQeibq4Cl>h7Xicc1Bx6gTwrsBJ8Ta~!r z;OBj*I*tk727&8K4)qN4ed{9ljCOHs0wDg>w|%7^<|jxvRY;ZfS#slG&FNN7LWyxI zOsLQ4>*;-hyEM0`JR2R_jPMKYHYyq>Y7fZsPk(9Vl`f?QPw`I@i%c%sLy}%DNGI*? zJ;q8{9jHxQ)h>}P``n`KhJ*4bQB|NFio!8H^;x5#b|jydMvQZ7!m=oyDfL2%1r@*7 z47$Qx+Kg=4p)I9(W6Dm%?#OrKIJZSup^WNxmF^x6R@*hl#9+ z!{cMebE5s0f6+A{ySwY>HaYa?hRCq?*4tn+<2s%8gmwVk5f<*79P0=D6Eog;wLjuT zZLKrC^Uq1Y>*xn-+q{93GSrj;r!Xd+au2%-`UUP;Q&=JN|pvII6RWyzY((ULno z_-&f^E*&K8CC*J@9*waRC39M4f%QaB?8lV5?~R+nPQmj4vLP{Joyb}<+wx3VKxk0x z`4u8mpg-X*eQsR}vs$i6^7G7=bux?}z0m^0pJ?QFnr#$Q*+P`}HnCITQ3!+qU~)Z% zjT6I_e8NK1D$57)8>HJ zPp0i(_cW_-i0cBId?b%~pdCKNwI|6CCarn&Op6d0^!c?gEV4&eTqk^6Bgrqj%qe&1 z3(8}VQl)7V%fGu3MMs$=gTfEG+%t;ha1Z@6_R-Kcl^anG{SieMG8C_I)`%0!DK6dr zi06ixu74;8vE{pmer_VmA-@uR1(UCNK`*eJp--4?=5m6+j{j)N2feOR3&%m*Ti~Gc z;K+KzDj`%xr>GwpkbR`T8?&X^9PNs#YSF-Z7=OM4)T7+AuPm zZqcZ}L+qc?D29HTXtRrP3YBZuq)Qqkefz=vF|XqD+zJ2pyVj%u<;cebMw>(c1y7*)EWvri2p6sU%r=4)vKoT?+4?@!q5`+;!ptM*U^K0 zGV5o6DBJfyglrQXYFrys4{F`up39wllY*S~sJ35xDn1C+shh*v%$t<`00Sy)$2Y(^ zU{aR~6OPal$DV(9ZaSn7!Dnegg`+L5Gx!cfXg0JK)%-U0k#yz_MZJmOqy8n#PrhA8 zHkBD7HN9m^sCo-pmZv!P-xY@t$ zXdaYalhhUZ<=evSFTVWZlC0N#WVNdr8odtsu#?Du+%|awSnsZ0U;WaJXMwQ*V#-yF zmN6l12Ir$L2_cyRvJXI8pWqFYSMcL!&L-(hyH21oHH_gt8YWikpc?3f$Rb)#;1q%U zofI78-##}8z&*(FFnQ7}r*w{wpCuMAlZlP_;gkVBl6`=wJTk5O&U!=@zBxKePw~@#ELMRJzA;>99BJ}VXrpR4 z#{7&$zP{oV=E5Er?gso>{ zlvVTrgEeMCu+tUv3z1=W_S0ql8shJ7(j@GxrAMR|4DmU=0Z*Pbj~S)*3xc_y)Q zsf;_0SPsQxuwt>rqE3ErsE;lW=}i3a5{8~1r%yK!dHzA+oqh4nL4LQS2e!Vo`hgma z7&8U5E&-K)(IXTIROF>+vk7imEzLE>8*moYRyM0;lBD+DW;r68EVzRPMkYp-I1mc< zp2Kx}F6Ht>KRCmRGtLwJ@5;diiswuIb6xOFTGuK%PE~l``@%hU@(a=0cX$X$9-L$6 zlkYqF^gXI+X|1cJxy(jB&95V4RLnS~55kT0+F?^p+S!Kd!enp8vdHewp-GpU2(E5GTwhrgnXntXTIK!@i{-EJx%I^!WXQ4QS+OTY6 zB@|M^wB$CFlNE0EB|CiOmv09pg+p&baTYOj*2Uc{Y(5$+pqK|Y%F^$8N1;jqDX z!H^QYef$$jXEdz^JdzXv?!fv%BTyU&g<^nt1jDlv$hIkW9IPu`AE!C`4ShPBmCY{7 zZIjvF{5~9u1YW-Ezjb%|Y1!j3_X8i2YpBa~U*K~ko4Fl&*JM@^JtnAu*Gn1>KcQHt zF_AFUDa2T@vrEjU6N480A6;$KOg5#ueG!;6?95fz_lc%?0b_`PnkruHs znqfk+Q<*sNQ&SdK9fI{LE0b9 zAB|F(KXP1p3`|oxjSOgvtB&AXB=Sr{?BZN{WKf`f`iqISMX`Q|4r00_u_i?J z9g&nVrdZqiy|&%ae&u-Ze@-){U#B(1GM5z!`i1Z_;OEzn++q#3>6NKv0f{ZjA2BI4 z?olN~ySA;-1%~S~OKeA{zvo?15hB9;{a|!U88n#*2(FNuwP!HhB6|yRzP@8QX|XBw zzOCF)SW5h{!TUFBi@P@VaCdfi2a%LDBSQ07m=KnU?55URnF{s0et*1yx)M_6ctm{? zzO~Q$`}v;IufQDDIF0q}lxBE`J}1{|mwQN0{ymP0ciA-ef@Mpp+1`6kYfmfVYY=8i zGwnj((S}qiLlCjh%XQcEqCAV*oa*-AdmX{aZc?>7ssa7f{KHLI=Gwr&DL0IX#jc2z z@%m(_AUm9Ae4YhD%qrxWz$f$T{Hz=IA)$SjpRoLAx}hCv<#uaod@}%*whk`W2Ry>p82&C0*=v;4X&BTr zM?^N>Tt+*ql|voqW`)YTw&MBO=asnz{F>&S5E(@;@#wZpibtH?VO97=v+po{9M1=V z)q`*G-~Ybi9CFZ!V&06V6QoiPW}{6ruH#;ixwlHxRHHDix*eEbVLRuO3_s4!w`$-w z(>Ar@e4$mm-^ZJDGPTdhuyz);urs9I_ds7*xfY;fk<`ybcJ7_zn6*EkC|U`cpG|YZ za68>mS<+&9VeZX8CQ_*JsHnDxb?dOd_IeiB)28g{7Ew9b*V&LPl5$I9HC7#X#M6)L z#|We(yn1KkY%kCxJVBJ(p<{ihaWtsRGJcIw$;ti9`2Z7EO?d7KZ_L(Rt9wRziSjJ_ z%kTove26=Jr!Vm16>WW5l240jmbXQGZt`$9-V?`snzzIN2CjcSw{&@yLA^J;H0Ky- zgHLLD^Ej}L?uF%$T1&mkE?<|}q&&F-B^14abdOu_>&Jli{ft%fle`++6%AwhI%0L& zUVeh}Tr9VoS0gsd+w;pg-V!TJkp6C$JiCE0cfFFhtAdue`D_f6_Qy&&uU7W~0pg0v+G}aVu4I%6w=2 zGAb~rE6O}H$0T1;7D+#B8AZMRCpLbc?J-|?lcm_prS0amL_}ol7xGJN6B2!S#d@a> zNpnhtY=|MNX(mD?ot>`U8s+SUMVf9#moG=2v2@nZ=B-xw$r)V5QIq>Qt(vNBIoEyr z^(KryJg9;CGVlSDE!6$OFxBixeQ(Ju6;rljBdApLF;1~T`~`myC8|DMXXr~!d%Uh0 z0g%%+=a4kjGtLcZ+&~K=4f(O{ZDNvrS zmz;SRFZ(WI!$xV6g-pXyrwgOd3Y*?LhwC9}U7nJSGnO%;CHDgC8XE%Zf7$eTkj#G3 zbU8Rq=PUj#k3w|?1;uY}AuwZEQS4@Dp7?R)Xy*;@cXkSs$A2F&#%12Ww^@plc_fuv z_D_?mIwfGh{QX}gCpL@pWxnDb+Ymt5{_@%o32MAUIvrDT#ZJ?VINuZtSU=I|d`n9L z3oN!3uJJfumzTomcoWZ*aEqS21x}I8dBz)zL4IGc7P)4b5I0KIOLDoI_t|Ap#S-}= z;=^IZW#sL`5XWBQzu}xy2+w9pj4P3u?Jmp*PNtxQRy%A=qL-9v<>JHN`J&$82BF_4 zcdiaHu08&I-F0vQ9oWk)i%8&ADK7|y6PKX}u8&~2M>iSLwZ;lc<2$~wUgA|{YSXKi zJz2j@zUfKQ7~Z=?^3Tq-1gWqC#umr>B4h=aHKl}8E9|jIOh9*}mNQE)+#T_@SuH9I zb8X^c0<-@?adO%RBRW0(Q5B{Mi$JvTdFO28z^vp**tBL|&{o=n5 ziR3BUM4cVvF9zw2a>lyd%XFx-*6%&|x5dv*=a)3aO)UehF&mZn@ExIhFA``sx}k4D zlxw(^$&M{CVcyi)68z~OMy&P}mAcksSoV^lO$rnI?l90GrAF*?sDLH9SQpy|t<;r%0+*PQX0 zrZXE~$GIsbgkBAuFjoZ8_Tv1H5Xuy<`^H44O`=Gj{|dKB*kzO(ij^e!W>vSR=LZtK zBfVhk?!xpM*(H6ai}V>}>CA+E7~pJ^64Zt!&+0mdqESD_2ndJy;S8@@Cb+$aEHs?y zz$i)Kw;~km?GZZ;kSsyocgdTl6kekpZp@F^F_Qafk!7m7 zL)`I62G|SDw09D<>0|W2)>5aVEA4&^m+p-(J`z>ajw~kxSJkJpablp;l7}kEmqLldYkz-Hg1rlgj=ZcM|6n7`}yfbK852#gYEwS zzd%60XypMtAY3PEI)BqiQmp#d_+KLSarN*GacLK~bMm&^#kdEu_hebyhBnI1lBeoi zqM8SiZ9O4gfe~)hNa#m7B;27u{fM@_h3^+!AR`>axsNkHLpp#5eiLe`7iS#b#D~4z zAc(f{^=cJFJ9!21_b64;%I)Dsyqu**IeP~C@BJTZH`dC_w@$(?l62WFl5NO6All{! zG}trvlUD8qbgps@Feq1ds-A819~<|r(5>${|WL8zeL{7oS_%u zOt^l6sg>y-oNQh!3;YxPN3e}@Rf(ouU>l2e={@}5r)qJ6m0TUWZ=oJWh4W;sp9Jgw ztN!^K-CR}TlGRzZ?L1}b0j}Txj;U$l7>hwx)66?4`{X#Q)sJe)4oO9_i7q^yp;r1C z9bBmny6K@`5U!L;8$~BL`dCY3n}l}AkoN4pm8)tLP%dEX_pxP~%NFD99l(qB=9_F` zoq!T;N!7;Mh}O}M{8#=S<`%wJfMwJI7uH4@Z?vOc-XSr=kVNYSnqZ%+Z@XZWtwRXb zeztiN2hE(Dd$(}2Fx*Y0;vxPLZk~y9`lsfx&&N3Bn!IJ+K0rV1B1M{h0(tmH+Mypy z)crp9c@O33cMp}R$JHy=a1D>WYnsF~_OJT047&$p>pVcJ7JZl2%QQ&;)gCz$&GIyt zNq=WHY@%tWzkUV!shedJ9qLq~?BjimzJ=%OwL!3rOEu>jlww>Z8{~?2UMpRw0RHp< zx=undmtumq`_+TL!#MpFLZKwk`Iy)$xqN9Z?y*VA zDrt~C(SlIJGMhqWnnpkS70LqMEb~2NtMCxVSAV3#G;QYbM)6}fx$o_=+yfkq7UBPy zFWe+o)xoWu5gzOj#@UU(CfHOb<{mvq2Kqp|xQiCx?C<*kOts(~I?PEhu2WGVu38}4 zqg*CdiEx*slxN6yY)e(R4vOONwIp8E!JM4{JVr<<_Zz*@&S^0xOh>ntx4+3 zEA4=3CKRy0^D*iP;(z9^5k7(id^IZc3Lss6eqF~;HU@t2_QcwGh3gO&tSgmSCF&G@ zh9lU7dn6ur^fyl)%`LRe=tPBNV7^+lCJ*$%q00g+BocVa$ zMzIdh&^U+aXHYBzyIdh8m^+6Mul8_5-D~_y{V{(#XWuovLpb9&*^-}Uo4_@Et`_?M z-d3m+M?cbqc)e2TGU))jX5P0iUA!*AS-LUS3l#X9`WcsqH*j-QI6IA^&LLL_l{)s{ zFn7Dzeyy?1mPzHS{XAMZkFl*oUx7w>yZO;}i;ePB?E*VLTgDj1|C840k$Bo~Js<#@n|^DqWqczC~=5h&Q)`cY^>0QK+C_1dxkzrBr=38kZO!@qmyfaCQz!x(SM8{=vJm^ zmgpK3>U4(EDoZIJ({um|~ahzG#i zKk2K)UIFbgp$^3|tg}6z5==1dSM);rpH;5^fat!%~8>XB?_3;6N z!TxCHlP`z7nW7ls!Z`fYldVX&e~8Q3{Pw6-N3_}{R4p^f?Cuq6Xq9gtO}bhkK1WM6 z(Ikj{DBiI7p-5G+jHCZ|-Z;$=@*&NTeIUuShubB%{qr?^wM>9J)!ZQlPq#)s;^jZ| zA7T|~3wIb{5ojY>(al;XBwF+Kp`JxKa}N3A8@pKS0|V@fyO@H5AJ@Tt+Jx=klg}F` ziMI6espK+^v`JJ6xkM<`Ji>z9*UCoOY9_^L$C?+ZpP-ZPlqnYINH!omL0u3|w|;6+ z_XzWHEmcr0B;3+2YT*@Xe*tO|R;qIN7Hc!b;_UTL{x=7innZ^!V%`?QBaV)J43Vu1f7Fiz1~En?9}wB+K|mFvX%TQLz?*-kja*1oKo&v`JQJ zdfOyHqgJWzUd6Ih>)4l7g2|?azsB(bt!81= zBmFF^ApD(YSljp#HK3P=SLOvE5QAii9MvMWp&guOurrh~rc@)NRG}8ZR?9fHA&f(# z)DUOcDx2sBNX|Zx*KOQ?=x2 z5Tf0}pF-c^@15X)+y|TE?v~2P)b+B1L%M(6`*;J_Ab5!+U44KX?kryMCDzw#lse4* zAN)Pc6thU5W>KK?eNwVH%st+A)SE)JXbbeCKK7q4ElT-nK##>5u_mrDh-YG@YPmfE zg1wn)RdUkR$yOi$7cY^HSD1F*=X-&Evyfc7kK03xus_PxNLJ5K**mKwE|8)vB7Mz5 zc?Zpsad-NeXvV%Pg8jHc+{FF|e~ONHD?^{JcMS{TTwntB7U`b5YZv=VFwjp=??M&S zAExnT%F{HiQE(@(PyJjLX>nFm;|a!-tQ*9n^G-2mh~^2x?SrhiTLkOE%?6Pk{=&_5 z605{0XPJ86rN!$__u8X(pH?$Lq z*%ae)MXIR`oejcKcFQ*lc zW-y8w`6OT7Eao1--`X#zl&>4_6rHcJOy=#wIP&__ICF}vTHG{9rWyT~cD_b7UlVI* zf-=IMx5GKwCI;;G31$O_akx+w>QbzMc(q;}bH721w@ac?<%cQrhOdYqDI~X;~#8;^f}rdVXdNZ+Atd+@EyEv9;37` zfnB`6Mz+9WIqGex?^9IvQT~qYqG)?UU6=6V?7Y3W%kxA>hN(inPyu^CT{Ffu*lmWghwBeM z%dlGU9CM>Y6~9<_u(MqP{B4ScM_`4FPWA#V^C-)>Y@I_$j9G~55+UKLw_lEFE9wxN8J3-&=q9nZ??A?xoc`bYFRWrK4qQl%q zy6+O=Yy>#iBzp!FDd|UM8xHW$jz5BD87Px(Ho{B>=r3A&1UI_ zI(6|PpF4)xMc%=HyiU>CM5h~2EX-3*(S7rw9C-{r~X|$Z36h4iDm)r z6brl9z;B3`R_!JlAnXQ|5- z_&bD~-+%?$>czKlndZpmStlaD$dqx{b4@N05-j7dCpk&hsg^+88|N4#u#G=IUcpJV zdbpD={=xqeKEbj`*~=5*ILYwz6ZCnO(I!->#3j%sM!(!6pofoc1pJC@wOIZYdYY+P z#Oa%M(N|wjAEHI<1*VmA0Jq3N?~d4^7%pkBradaU&VGs8Ue$>+{HD90$hXvrFh3b_u+7VC&g@dPvX2kGAodM|-(6^DN?l-@+UZG3+A_G1@;M1|k9?g$t}4a9`}^it{z<#bv4`>bCIeB)0Ird<$~r?nyDWiuLes z7XH066l#&Hdy%YF2RH;qDt0`c|_ zJG+F3L{R5QmtJmK1to?F=JEx1FvRPQk(Wq*<|dgXiir*~xe!m>gE|Gf$d#&!C8jA3 zvCvoB|B?TPyvN=14zCvk{gL3x+#l%xarFYGR<@3zT)jetvGFC$HRK3gC!K7av(L+` z`SU7`pRdEW-4BNOmyoF{;_dL4Ii^u|w`fNgvXu~Le94-h7B=zK zGSw;!BeXMT8Dgy|R*N-!JYXNwjTT4)?BXqXMivQb6<&bBZwBc;O_a;k$u5$qrURh# zO0M7#?=-Tf2phyqlX|&s;97)^(TUcm<`~C$yVMG|aS1k#K8Q34wG^t!R{em6y1zpB zt$&=w30j$Ak!pnfK4zKXD)9vp@LQQ8%2|fKamp?1I3wuGFzdH3sztQZ-8>ok&B9Kh zVUA^rl=Ii{Xeap^?Vn?oBPzu9;69us*o?A-xK~KX6ic-e?jRrI9%?6BWF(o8 zPU{z9?Hk6^EZ8On+VHiAwzvfHj0m^J>Yn2EGtiH<%dkwxTIhu*I0iapX#S3Wwag%A zjWYb%+nZTNus!OJ^^YeQ=L9yt##*Cn`K%l*k)Bhmb!3AK&!8jJFHvMG_Q6SZ8X5J< z<3#m5cQ}RmTX@z13Kbx5yYB^~ZH}GU@K~}IgqRnhm z%Efs~Gi18C{UT?`-^^ppe)H?aGPQ;KvvdfTH;6%iR*Am31b@*hxoTI|rW}5nX+54jHUm)GW z)=0tJp`A=H+`y$6Z{bbSBV90$whFoh(oZ&sdHRVqGLQb__KzV}-mYqy1aqZQAOP}N zj5X!FQK~^Y>S?jcH9Wz_6rEh<64@9N*fZ(!r$Lqxvqbdcas`#*cnkJ{N;!g!C|lDc zsC(He^8~Smbb}3oQD(;J&;1Qj0v&h@er`M1XcubLtYe9qUjzBN+}&KhxdbAei}m$! z^)WdHJpq3EpJ_0}D_5jaiuNQ{e-Fgl?h!z}LNOU^4*=E41_RI0Bv@P{8)kC%moKl9 zpZ@gm5c<*~c$S7~Ai$A%p-7{f)iV0S5&lLOAL2RssX$kthkjOz1a}AWo=SC`Wj`Cj zRknT)=N#2OvPaw+A>d({GsbC=tdHf_{+Xmg=C}U>Z6d8-gS(l%La?{bknxXsyA5NB zjy@i-46G5?@)m2rUs@(tO6ld~Xv`296oS0&BcEXsFJL?b+F3_*ehzi17n-CtPUi3O z2q;$L?CIcNrQ_*g>>J=kzLcy3MXRlp4*{{9%HR$(^}UrUS~#oPuV{>~D4l^V%P z-zT$>9zLiaUqZ}s_mHBjZ=vU@E#n@+VeX?GOH?YvBJB**ZecGGMH}ZSw{b(9kuKnF zHVCX@YbBY+I{6SUpTHf$H}QM9{?K2zNk3zq@b}+ED%iVYOz8&EHX|&g%gE=r+k>Ap z@&nyYQIXF{m(fm4k`2;74LXMiH%HoS;|96~xR)wb$p5p7NUZ<}(80$yTA+Ok@diq; zh_eL%3UpOyOVzLCMY~S1Lfg7Q+Q*4E@d!iQpQRi46l44oFiH1aj_5qZ+0N4k2G5yvRbG4}20sNs*6?f~82yph%O+h|d<#_v&j~}BP;>{EA^^NceHzb)P zSn#w>Q&lR4*zw1D3wnI2;N;Xlf0p4JQ4 zHLOB8Zx`H+KwFG8$Sd2BW?qo%996Z9W***_S|QEMC1QQ5k$*J>HqTeK7Xj6yYs@G4oO z)bG;&{R|=IT?gYfR9o7RwxKKS%o<%Mj}=Ec2{(BK)oWuLkt{ zw9*j!CS9tLaohw0^)l|-Gm#-mNGRkWGyh@#=d5#Ks=N<6zNUTDlC0{F75$`z4Zh$Ra z-6wQ`<`@Qd82~u=SfK#*MzG1=%QQ7hWg4Yc^7M8Os?pxdYLajR{RF9=4i4lVa`EvA zqEKo9ALe-x!Ep3JwFvr$c8-rX+0ySb;1l$rP7(G*YmQ+!J8!@}-0B6t z-x`%D*Gc@Y=DUZ&+z0xpP%hleG&at-N<2q(g68Mh%hkh8H|6EK{HOPK9AK&Dr5PTP z%rcG8rdgE9AngmZmZ&$%0)Zgz5l&Yt5w5z1AAOLlI7ea`1_D$|53{V&O)$|+&r*eY zQBLG48>h|D>y&n~mnh?{bM}y|c?6_d&rnp!^%*KeN9f*+Pf*@E1O=H|(N?RT{huEAg6 zA9>sI4FbMqY2j_TxlS^mpT7NYkGw%SLEz|i{nZX2?mD@!PusZWnJkh-%8uc1w$8EU zDQ5ok{Gdtl$76}MWg1T}!gb^1Ng}n{I~a%;AW)cVZx@;|%M7z9nk}mFc!LeR&#eGJ zj7_zYLN!mboUwfHIA0)r)#-{ zpThWjq1*&{`U&v)$~RD`u}D#)n6Aawbb#p;^8>6-DO)d6_X!#1!Ow|)Of8>b^a8g` z3T0cgeVjGK!8DV8bQAvqPORon@qn*!UmaN|O5`;15r6H4#yFS;Ft-I7V1REC<*FM6 z1zJ8gMw^&q0Yi%wS4#r}v~zw!NY|IDS_SbA9-;pLcS>9(6fWUvtCwn)?+`?}i?Euf zSi$e-`Wm26Ji*K~OFbKJmZ)nOGQtb^i{hyTT9}J)KOgT8o0AmJ&*EY%`o_z5N;R2~nBGzi2a*{sI z(k)P?I#2Tf(kb*D`RGHGE$03yD%p~Kgk2<0_XFf#)sC^vlP=1p003YB$;wYCmI;YQ+wdggI>Bxp-@s*ZsirZ? zP&>D8Paw@gwW4SjBlMQ3?!M1JF9@HzV)UM&=jqba^wX~Y;`DI5mrAC7hHD`25KG4e z!l+QlmoEBrQ>O^B#chIPj0Wx=HpCyFJE+$5e(Q< zS`g+%Cr>69{{G{wK{5LfW3N`auj@SfH&?sxa0iMRm;RL0?phARowQ!iXk3>09`z>R>$bmwC(|) z+rgh4!+5&=J@B>;KAI&anE$>j&Lnw_D8{-`v{`tR8F%{;;s)*oG}7+hRSxS^#GCZ7 zXxCWA+6Cb+g?Ub}Wf?3Hr|OL}?_gr=9$=y{6*}$UGOlyWIOE?%++7{ zk9S6}i@hn{%Gan-s#yqs?Cw*l8Ra0@h`apu7H0$cQloJ6VG~~=C(V>_j%t$t23Z$EdugM2RW@-;$>ykX2WLaNC#M1;v- z^WFVNSX7G!*jI`7F=6h0=U1ra;w*pHgh$zK<2r_~)v{@Zw?7?Vu#DV|54VXihd(D-KTV5%XdSVISD?O1 zyg*?VM>mDJze1vwTP-_7lX2ti<4XB|*Ed1Ae1P>)|NT>s=d5FS8d$ru8@7STw}+T4 zlFKd=Oe<8*Q&BFY8(}V|>gQe6%wr!)ux^#29x7IJj~JuP(3PmATj&w7@|&XF#{FWr ziy~X0n_Z-~hF345UTc~X^2i~Urv+`Br8nibvukb?EmE4sJ$-X8atSNPNI!Fvc#@fL zu|$=6&@z0V+VC^e7|JsHILpivAB1D>o)%$^f_}jc!4>p6^>BOK9orWeM{`V3HXZzX z2u{JxLX|4=4cI%Xc_WNbc8$VO_9TlRZ%|K$Tkm20?(?fvo*!4KGR{et{FGdwxA^Gr zLcR$1+$#EUFkg@B>&I{9%V((ZwkD`?j`*61SFrbq_e3i{JhFSULaXtOtM}!LXRk_B zJ^k%`&rl*CJr4V#jfz09rxoQGj6#C5|Hf<^-#C4Zn0zw$H>W=WLhjYO@sjmQW$c6S ztk?(36+OLovBw!nm!Zxo+!}5JeIYwIkKZC9j4*A}Ck*+1!pqr|d zb_z61o1~wlA7%}3hQ85OVYH)j#CMN;-Nu++J~vHBHvat^pj^>(X&I~FdbIHd4f<}N z%l&so(QRV8gssvKlCI>8)0=-Zi1+uAE6=&&_&U{4J9mRJfFoFt?K|Y7~|7u73@(S|sP_{(a{jGO|$nJY$4is@^1q zZt=(aQjPKrtN4alMoG`yB|DXi$yRhTFwVWap1XImCz;eqo1}(5Q7>m~+$H~^el>Rq zKim=%{nPDOi`%Ib3&rBf^$4dTbq~`J&Mm_of<^>5`eci9l}DI<3y(0%Og$sqIme<> zw1)XO2IYiyP`o+%;k{43Pw+Q7`Bo{1X@*$en^A4I^N~-u`}nzkHDMetlVj*>70p)D z{qe@jAylncv_8nORmeCh+mL^>R`i=zl*u_tv1*g-yGNU(KP6gub+Ui|{VSD9*Vang zg%Yp!ap`4C)$#P;>_l0PF`37x7Tu4vh}Ou*+I#cl45?iZ{tlI5Pw%VWwQ`%pw7)8s zj<9g`og&}+m}KJq!X$}s-6p(`tDDz2xn9yS2x>#I_g(yHr@g#>F~@)XpKKKx`X07i zrM(~Xg3hPN$K(s~rcL6{f~m%z`)1r-#s7b!H^NVky`IyKq#3Leat>8qLc)6-dObbZ z{y|X4Gur8}2M3fl(pn_0WLZUaaodGQJqmEDk-d{hvn>BJT_@=l7)?G-vXrUX2JuX+k?=N+(^>{Amfy=JSQYN56LJh8+7f7Z`GjOeq4N5-baR%G zCRVD|$1ejthFOVr?th3ieeg~{BgP(or|?>~_8ta*$EDKaZK*a?>`1F(xf~6|li%ap zMk^Insw6*f@$2B`Z`H`r&JFeOdD_jjK@se#P?WFP!Sdexr+AC}0h+s4sdABeq3*5J zE-sPQhcS=BH1qgJ0~|JpUOM#fER*sK?2zE?Jb88d+kjV`g{Go{$78tzt`XE@Adckd;PusUVpE@*Wc^!_5af)Uny0mP$5$z z(I8)W>8C;kxIs(R$yUqM!0*D(yID(h$6`#4W|hGd*vDbF!*5%^lUGc-_nyO}P~@%U|yPL*7yK`-|i(lBedO`wyD z|Az=Jou$e*(=P&FypeXAV}PrVeGuLOsXDnT@kX*GsyVhH$$FU@*=pfdu0Fzb z(q)c;UvI>Jd)-L{{~3mfUhWYVf(@az6BO=V(;~L>8(;?8O1($id-Xej4IKPdTSr^BJe@wPqfCai?5ouu_9pG3YIYD8VkgdK5d{;k*V3<0{*$+-*1RM4tCn!cK zqs(nW2nZ-A$;MHZ6bspUOJviuU3?dTuTnC>5ac*dsr}X8CC*H+J<#a@gMN}=;}AX6 z4&|guj$~zlViSKC8{WZ-z}Nm7=wzAzep%2>8E3YI$Jrlg_4sWUAMNzzVt*IWW|+Z@ zyRDh$;d2rAJ2lurIFyjRaoP=;AsL^}B!#mkfk*Jr6ko2h5J_+0%i0^crlk^J-Z zBeXa(fewF{o9W|>fleqV&He?d|j==7lDtx`&B#kKEs4vDC_Y1hg5Uqi>gJ`v*L{l6sfl~^E|y3 z%YEI-6%$NRkF~#E1in}kUzb=D;yKJGK=VOM($qqf^XMu zrCtO+INs)IUj6=6dx^}=H`qb0ih1-LahbGEihb|^V~r@;STk>q>dliI8E8irfqx^z zG{ODF79RcN$9qR;A0DU|$W^)dwus1A_Hu{Vi?oE>NY}o5R3&GeteJNa_)KGx^|IB> zqr|ItTcgZ$Q=-jsRnoOGHR6qu^?Y4NXzYW6?eI28)m;SorJqrjkKfiyo}lbwzI*iS z70ym4{}Ecd;M>Ov6=O_sW_?_dR&WR1{xm}aeWU;V)h^g98g7GnJWt6uK25tu^zwO` zl2OVrs#Bou7oj$d{1KLJUYVK=f+$O}rHjB%HZE5LjXl!YF7_b1WfZ8Xr|4knvQY}{ zw0PsAcTZo#bS_sv$@+`H7jL}uQ?wcJoWE19il;}aj&)e5jdGr1fu{$56ZM#3f^wc{ zO{h({Rj~bH^1t}IPJwY|jpDKQv-P&{>LrI+sb{O?@--yu&k;*hl*%;oETfK5O_C-U zK!N<$@(!S)gp*RF9N?_ zGSVv4Zk`ex@}Iu8j(GdHO0G;P%r@F$ji^SZi;sTNDUf~e<@0aXuy-#4pR-@GUcQpI zTdWD5uR?`D2lFWYCfg9xm}oQA+y(*C8R;^`0^vH{)Wt-Aa#g(Dr^r99XyltDL0_; zj4Dy7mmFfNlrLALnu{?{F=`dgG^m!v+1baelB1nA$W$q56S@d|zAl~~h6(Cf*zu6_ z33Q+zPB0L!O4o`t;cl}IQ_gesP0^tq^L1U!^N+C3(~L3C)@zV(3~Ch5)=Ru|?HkhB z7Tyr-JPhWd^ z1v}(vYUF3@^>PogUkv}#!-K!+6!`vOgG9Iu;rh$x@pqZVcCmM{5zqHA%N5%NSw==! zH1cI?LhKO`E&^YyiGGrCoWE17NxYH2lcz_vnqlGu1@;{KAmaHZKGPU??+OX!Ja6~K z@IOBUYBRd=2zNx+#tUhhW8W=~@JYLN(^mc0ut* zf{j6To*wGiJ*VO-HOYFirROhie|q>{w0RRBPTgn=FRu{$ddU#`KCUc% zgG`Eri@*mL7U5Q*Hi!bH>i9d)5zmltcDQ?KW(3b1IjP zF)37>BAdrtO#TyMPd|C(2gU)?nPB@@?HrvWG#7u(yzf^v^C}cB{lwY1mAZwuKoMcB zQgSi%hjB)SV6!;j0q3Z6av{qCOlYn4Dw3(-euM3=ASVk`D>0kPZ zxqtmL^QiV$tz4%-ti9`>Wom*PjguXMlT7Z%zJIudS0zU^mwppxXOO*z=OXZJ!#D=8 z_M$B+6igFxbbefkw4zu@HYQv*i<_n$X4T8i(aF-^#>F^5Ja-Pb2>kS$4#AD$_hXey zpS*eg5_fx>^_81mZ`zrrkX=Pgc`ubJb&q0ye+yZ z`pJwNCn#5bARsseq93LidU^xzgM9k*wOQN&#`BkFNN^`z1pXQk(OQ({&r4=;90TQw z$fuO^Gc>XH3)I0A!_O6S-_!d};*}pwV%xYE!#}yd$kxL+$kXKPhhAHXg%|E6Dnx4o z9I3Y+d{8dkAjs8%x;XKwr*}W+Mc_-;vktEj?cmT)%2vZumaP_P;psWRKs^@dkgpVK zJ4fUgAXyP=;ZpYnER8XTjh7}dzIjIa(HrI0T|$0c2R z5%?`4ey&SoIXaW{A@(&g->-()L+AVkfvaB^-}9H;y{*DSY!wRglyg*HbT0xQ^nH;3 zNi;~;@^*`~a101^aQ34fvkx+kvkhVHbM_0iWAAeJ3b$TN{y)O<=-nc@VcI+;#sStI zPfz;IBeWi#+n@S5KVKhY?-0n+*UySEzZ>5yI>dGn__EdhF6D~J#y8X3gj7oQumYXl zK3*gT^%eFmc$xZmOw;yp$yEh9h1g#N{t=p0q+KY@j8I#pe1SUoVlVgG$LoX(6yT~a zSN$%8HIKfZINVh5%{9bvej%uqRrs=f_{j*E#4^JNW4lr&DlS|afAl3A5?(w zH$ew*5%~EUb5x?uszso>p;%a;XpoquWg1g1nppR>e>E6eSz_*J0 z@L-buMh5mS>+mdf;vK2FKCS|F^O)P8Zl)Khjx&bXs~0qimn*^q!)i(cjIltE~fq@T?^Bl>85yk5YI99Sw_T~ zWNL`ku=d!7R*5O+g%{+jYNaCqG|bA$jltbDv{il3u#@axwDh>!+RkisdkU#V*v- zn{j-d@M7{miUsLf;a2fRp|&G5q%)2IP=Lx-bN6!hGLOQ~#hNG<2sYsM|G&n6pdA0T z{_1xr=RteH+0WYzQHMeW)f~)JyqWInCfZCh^W#eV-FywmK1f!czW%?U3v|>=)=E1B zU;26V`(J68{9JLjZ-3IrpQg17bq;Xzjk2tfNinLEB3?}}ee&kXn>HbXOq?Abk8Rv4 zx&I5kM8mUJQ*<2ygY5n;f2H6tN(r&QnQoMF<;RByFWlRNYNZb_aJPZ$9AeAUeDp5k zMuP<1)EC_YjQ?ByAzeGhG{NBOcKx$M@Rc9``ujs{+`Yg6h&M8i3UpA-3AW2s!L#P? zq@LyK1E(LnEu0ZUJDF<*I;JkgkRQ%T+-y0Yn^sz6+o&Ofn%^5$NFS zDp2R@OS}W>cQ;>(h3lVf!&XSrZ^oIOB5USd|E!V!zeN{mi8G6^zW%w3Pog1PkFVIJW!{crL4J5f%;Z5kv!ZuU04|-dQ4p9tgIfN%|9%hwocNZhwk1ldi3nrJerY;wzQO zRn1XpozjFPG=Qe7ohDKU$irM{DB z9r2&>Rf}FepQS!TPrqrHHo?F;oOtKq`$qAfmyl1ZWuq-DqATThvDpVf-6UB*!t(s3 zN(p!GF>0Eja;a$Z?N5n!9D)rp&k?I-^|Hs9{9TaF4loMT!)@?4HwaFVM_B$dzHGHh zNuZN+fNb?9euAl1uD7RL)i7(Cp}z~;P@LK4>p!jxv6;m+id#kQV$V@&Uj440 zHAPn`uaR$@{O(bNwNjZ$l1R%A&Kl9jdre{l8>q)WFX?{ycn@|I%?$D>>oDASQ0o_O zg!7kUfUgVbjADU(5d7dtmkHN72L7}D1Jon(m7pHv>?d0iZI-Wus1n`;l9hErppw0;Aj(p|)Sm%Hk^8jb2u?mGYp)|vC#rv^_X+9o-P68eHo5-j68d!UG z5<~6M4F8kS=Id=2?Z{LNWvi|A42Y1$hZ z`dQ&N;WlzrBP`JtubzFnZ5?rhmS8$f8)F`6<^Ce|*87JH6ATk_RW{*GVuI~wNQ3Ny z?B1UL9$%&8PNIGm-d397gAeyU#@?@&Y!qi2%hQao7HU%|dHFopA=GXM2mO#?;)T0y z*xh*eb}tvl-+lZx!IXJ4$dP@pT6TqGfkHD6@%-aG+c3wVo9WPb0Sah%tNxSzT`RYX z?*zrwuStwr5 z+=m+P3JF&qbbfI6g4Z9>ns6&$msB0-!j8~Rk*Q}-k!#1gc=jd$+K7Ih)iPv&OAL#V`oSo z9;_33dh2CBeI0ur=E;EiHAm-e{2_WD*T4JpYK8h)yVxfv=!dyl?;be^__~32?S}_e zk&ZzInHUGpU&fdRIexpgK%rV>7Kd`OMkLtI)%WzZTA`b-W03m`%>6Z@RbrtwKi4{` zF1{I>tKY>NpTBGt?dPnQtde6Ix6Su6bs9wNhT8v8u@Uaw+Z3we7u)zJVV1VKs!CmO12c_xI)6+>*8;a z`Ru*>_7$%}EZl!*@?GU_=8D@Kmd@J=7S)rnjE5v?;g<@e7e}LoV^J7$!l~rQw zU7Q`3kx)D6v!Yl~D6G~l&COF{Jiw|t&hhl@!Q9Q>uH8$OX)WseE57_vOe}c+%v%ri{#~s z!46sa&H*PVVoldSE0!Ol`niVN=x0&Swh3MR{_0t@Y*qAy6%A8|L9tEqjdm zMHl5{o)V^V)k-&tqnrT8!_xz&GG{+$KkYQ*INg*;3;Q6^8e~DZ+sLP4P166i|5L7t zzmu;Eb6=xN7ih`_+fh!iccHfoBFz~Zog9e< z^2H@G(*)fwlJ(DCd3kB(^>U*guaJ;0LJY_;utHKVxkd!%;9o7(B4CW*y?XU5SL?xt ze2rf2dCE_>nMc1}`+haba+LW-#yKKHaL-;neSPm^9~avY^i%5OFps+WDO4;_ERorV z3~)qRbqG9plYSF&Er(#n@yo>qnJ)gyr6?z+2{z%nUs^=|8s9oXxYZ^c6eHPs3Kc9P zsK=mUT_UTLZxJEbc>i#MA@vq$sS4EfvXx8qv(j&d*^(`7;Tb2dkjPeBN02U4&Fx~d z4z~#naOCK82(XNhErr``;uEe9a4eC@S2B;X4W-}g=KUT2`|$nGOM>mRopnrHQMm1K z*W$(9-6^i6KyfJU?(PgO#ob+tmg4SOoZ>RLyTbtU=HA?!+`QzyIv|(L z2)gnWb~?-hDM<@u+`}W7v-YBXPJT%wpI4qnY||f}oj1rUOQNt*^$e*lXBMUfGnxl8MxV(v_P$wXQZ{ zyeGG)PjXjG2gEczM&Xm|i}fn#R)|h^NpG?vd~C0j8|;8^C%j*V#^lMyLQ-byq8h>PRUOGB_o__y%fD`g`9wXqr<$S`N zyW`1{i(W!oXnReUr^Fv$;{K*X9TI)|J=iw=|?}85xPKd_273Iq8)^k}tj=Pww zVry+=YA(r|V~j;zsP6}@C(1sd3j6kn&8JRc%%|;6Ic6F(oUw}Qc9!BNPJ(ev4ho~- z?!U`2f-^#BUDQSd|K5m3cYek2LJYU23zKgiVD=3V7cURDegG~i3$?I_wHzU0+d2#N zP_>!|39O17+~wCA!WM!$1 znsk1#OeNZWFAIaHL92hHWDtz@)&A?P>g9?Dbn?mf%HHpzudDoq?T^q;^HCf@7kH0T z<9M>Lbp*_hONeKd0{i8=U{f0RtMJDQy0VH)h)?$uTH5X#B?j1JJF$L>e*nZ(={i;^8S!g*YXv8iJmIXdFJNn#~^`?JPtb( z5yrZkn{>aa{glFHMuOA=do-{f*9iN~IqUP-{s-?zsGC8hQr)*UJiiPZG)sgAjC!11 z#}ZPBqe2bR6R!JI7qnR=iN~o$#JqCIJc8flQF%eqBG`7CszdrfHbQ)qrdxvUvc;{B zvO~f$28p3=4Zd3>PObD**xyie`MvmuA^pS5Ge$oWQpH&HkieH&_QyAi-8@yx65LG9 zV%`0-5#iUpZ;jTGeQD%{B|RxW3dtO>%?$m0eol{4_us==gux53A68oavZzL1xaG02 zoJid;A~cU!8`lgbYgMTge}7SUuMuGhM`^Hmwj1V{kJ|TVJZ6_)5%XHb2OE&Z^ zMm9En4Lp#`O3u03e2%=@n}o?roW2~Lp4~s*_%^k`Gp51bx7iYpWy7gdn2lrG7&W~t zAgRzh4usriuDd6-`bIffcJ>e6A{-$)y2E!KQ;~ostwe!*urdM>;BTc&?{`40= z|3^v;fZZ#*NLniDosVrAWA&5M69e~qsmy(++_KoRd_c#$*xVvY5b5w$xO!QpdZoQjGD&;x7YZOpcbjl4R*BR z-kv+BiuYovSf`5Hbz}B_wHk`biHnPTRBciQxw^)FHoe-rxl^mtD*Ej7MA;Do76SyX zp5kL))c^3qK|6}-6)%!-g&^s@25N85)k&*ClLeyRwNY85F#~rDlp~IDJmt z{%mtW%};k~Nw!VH>!U(~k`9Z10`xb!yEk~at-jhz;i6KpI^PZ=%ZiP!qx+Bm=(q3> zgf)QRst+k{*e$59R;hQP=Y7tZ{*9FaNlZIm zMJAF>V7UH%&XT$ZHN@=C`bD{?+sC#SRIHAcsSPaLZNr}2rtq&Bg^~`~qc2>Jj%58J zP@L0|7V|N~x59oG17Yqn7g5v79l5&f=0UuzVc%Z#elgP!@l(|X8mh4h$o6@v5%KZ46LVE%09(dZc+LChF!|9@M;P# zIOD4W#Sps1SfA+DZm@QFO7wf~tC#%&3>}@B5+Qqv3;Nu%O=>+U{QA?2S~+4|g)6NV zvtKb*Q2$rc<1Vm+)Kae;_Z-WLZ{YS?WJDrTJa=`Ho6OmVK$L+;Y^kE1Gsr6 zOKJvZC8YmI@{X>@xg!X3F(^sch<8l@<$p``2q>RiKu%x5;#5pyO3B{d&Aw96T6e-= zP1qT*PHDmP!sL=a=GA*rZ8XaP>n%E<{YPn;<_~J{B1Fi;SA>sk{M)PHrlpUQcKB=6 z<%>degfnqG^r~%DI|lUW>Sv;Gl*+xuZ(;%O_q>Bc0(*;Bhn#T4GlrXrPIy{O>jP$= zQ>vNNl(@Sdm5O&JFyoKU&NqM*#N8&224zg_77*Kmv@+u8?XC zlRK9=zj9y2upRifke%GFkDkKU1+KT1BZpj!kDP@iKlEeB(+LXcdB5lWYCV_ws-=kI z=}hK7JO1KgfvG?(wq_bX!BQtvs-aUHY##3ZvT#94GKMbVzY_5(=rCxkmT!Gm@H~rt zf3r2CU?;8TEhscpZKjx{C1=ePd9WMn_uCZ}ukT0f&Pn+L3f$I0?FO+LmB$4Vy9r#p2+oIuXZGPaRkI1gv zMxz>Jw}J|}VrW2$Sx_r6VTq9+>{cv(l7s%G;kfUvW}WD~#ffeNaxz{AV-cS*(~n3U z+0s**GeHnP633HV7DzBfao$N+7O(0T{z+hPCD_upDO`LMpKQ{BQg;x@6O&{7jAH;g zqX6Ye?<~=ArqE0W4mLRxr+rFG)~}^&z)uR8JS)(Q`AmZ~Q%mhH++$lp9@qsj>dsO8 zOShRFq}GgV+}la1U1qV(#5V50AXDtJMWVrPF&E&aP~rcwBDrI;8r=|U#z zZ!jw}vU-IT+nHD5b`Ytf--e$1e8}mcmW7b}zS*U+3ANc@s{zO$CK{+ciuz_@)U1%^ z=8mwcCmo@4Aw|}6wdec#N)8mO=#y+dD|q;0Q60TBfoDR;A$<|;Mf3UdaHI}t=nA5Z zrC}Sfxe#IvHe-4>62T)PQLvH)7v{!{|KsA8CMd1IcGG&@k6LUd_AgH zjxT;OZ!ktIY}C4rY#|fW7SvERU<81I$3aSVLvra;c{=uwBYy83s}7u0^p(f>3euU! zSops19kxRH&w%Jxet1Q+_fic~E3re-3FJYzxzBeD7d+D#&U{v14}K~eEstf=3}+g# zS(0g1Zl8rz*=Hq7(CrE;W8Uu{*uDn?E{JbzGg+-uqO_Nh<+9Xcm!fvBxDrkSgNb`} zQUkE;X4u?udR5;p_GnV88xy^G2{>`0`MD6-?(tqs5$B&dEX%9c26 z)P543SC-Yqt6EgBJq+-k9iI`3ZRDPAli(POZ)_BAh4AU%k1M=`L}7E0b>5_#--u1X!R* zsjp7>OA~EUtb#6-$AB;Fp9`aqCbk*)rd5efs28eOl8S>nXW4BZ=RY*?w6G;|H=7Fm5ho!>)@n+Gw>e6ORvh2n1JaM9IFzP2*F=)kD6ky|a{67rQwXEE?-Wy~KbBT$?oG z?D~f9W2-aEn#|}aR#DpYgwu&aFINF*%?sm6^>C4x?NuHFUJ2imAgf1fU0of|XbYD# zzRyEI89_m*KbXkwe`2#sjS8(lQ5&7_Hb&vwmgIcvQfG&#{vwAL_D)3P@Q&d)Kk65h z>P`nkrQC018{42U>UuPHjU)LeZ)MJq8rulnmUg~Gdwr_jUqKYlb&=9^=q5Bj%Bhrk&KwnyIW)*!KNr%r^>_&Hh#B#?FL_w(%WaEkLmjy zZ#~DJ9X_9IOPAs)GezX`-s`jpt!SdrJDz#~oq-lx+AkAg6z7QC3Yq<_QU5qy8*Q=b zF(kVZ+}O}>J{uQ*hqHYKYp%$~|9SiN`XcB9$+Uv*TJ5xZLEe?A3~9*j*Pf`YAuyRH zFg&8$9|N1HN6@z?Y>$jww~UvMxTKel?0`No{Ee&MkUIVRrOeL8aw#ftx=@ludzcUI zn@KpzrC}-etxOT$h)WUQFChp+66cua=pA{8Bn$2H^%0NT5X`bU<$jZF=)6ML}d-%m(gG6Epw+#K-+cRVcV!El-$TWC8 zPm>L#?{$On|8`6q^l<_mZyn>D!y|Po1IU1mcQhQifm45gx%Mp1`_A`xQfHhm%g_@d{1oA&v|8iMY% zmx|r13!H`)671gCjqBQ+tuB1`Kuu`%BUkU}PbWLUr_Lbql9Ybc3f%!pLU$>9%&60JZQGA>j+vcWZX|)JX1nEiekYD9gr=eaLM(@Kh%~39t_?J*7B{%*=Pu@50_Qj{!UQ!@h!LcWd*-Jnkn>>5(gA3&8y3pc*n3haV!@N9OAi0r$xX`8}}AS1DCWLHSCP_i8e_ z+;u;>WZkbacd@SAaUSw7tVSHuqjT+D(GiDwGT&hIkMKA|y>nDQYIn{GG&&Q{E`?sj zevQd-D)El&qYLbjO-8ZzWnuqpcgvboMHuT4c1o|+J1LT=BeLmfw`PxHxrnkM3w** zzRfX?Zznt0PX8wD@bT=5^TUth?4ylCt1)q{+t&AE3O`Ig53ZV!ipuSu!bT)`JZry! z6@U_7rbNe6gc#3NlS`9aNdo`I#m&x*u!5u|Wik zXMsgoCS-AcNUS#c<+&BP#^(VKibYg?FI%HBw=1Cht-q!3xngx~y-`geIn#?*J0Mq? zJ3+m(YliuTLH9y}k@E8pNkXT=j_)N<-#ca#xbIm^u4l7785aRJ3 zVM_h_`e=auA!mI7S3-KF2E$OMRDswcWS;y&^+E-xu-u~Su^H%2;M>`cF>-HsoW_RD z<-Prj2k}aXXh(Bm$o@#3K=~&Ka&yn`G9hPCc?<-&29C=l3;nmRWs-W=? zrC$)Ohw_P_92u5`^Dy}P$aw8Q#TcfjxCYc}eh#~lcA5W3oX@vn-j@lo{C|3F>rAwT zq7;VVJ<64SlHIah*pJ5$urS><9S=7yYQ-2R^C(KvVHW3^`6Htq_qg!K%HY6SiuLvx zl~LFiSZ||SnOS#dw4fO3c_L8Z4;(Lc29zaa?3wX&h+^C!D5bzG$Tig|fE_A?DkZgX$M>Z^Aw*NkdfWg=T^l<}qAe$H?}FUs>N{6$FZ zz-*Q(;|%cS*Z^~;VtpHEMbsSgeoVTU>rcCsZ%GC?djGLza|8t-*!~>D?zD?2BvFgw zq|nqZeSpR|(4q5>$~Uxc&?=-z!zb?eW?hi!x(I^4_s_G%aR1>9Q%LT4ms~?gzaF^} zr_A+|20pOO8Wn#$J1ft!!pS!s;+&;}OTqxqdtGT?m@L7mkNt3N4cNq(o5Pe?zz7)h z$u39Ujq7m?&=;#aHwAPe)cH=9lp`NS;h>$u-=&`L2cNZ*`$h2d2Q`MeTwi1t*max# za*Y*yEpo55c1y{$Z!TcouEul3npCU*ltXiKzwyzOAUG)`wTHzrpIk5T1^xV&i)*<= ze7td|$(oQ!c2U3>*9pdSiC6xXp&`d|R)rip=tV(WY~#Y2SWNo}>IlmCT>un(U`9T) z4s$TV$;5Hz+)+YWMCyV@kvFI0@(7*_fP^uLFt1JWAlTAmeO|NaU_8dOB%;(wvZgSw zB()^PcMR80GBb`qc-v2x@0U;S#le-BJz_9o)O)X{Vws0&F7ry*FbZ4AV2tK<*e#%f z7|<1+9|coP-2gPO~aJ0+76X*%5x?cQ>qe2*BsRj zKVn}{y%yk7N<9WvKu-B+jT73ky3khfog#hM=hw#uVn-~^Kh((y4*XLOj?Mz>?4`Et zxh5t#IOSwI?D4(Y2?5-b>8+4_$FF$$#;qwa1h4yruWE=@3_jBP4=jxv#^y;VEwLR=*-^1 zu0W%z<*`MI_3_ch;H_KuG*>$Is_n=PH_bn@I?-8Zs(d#HP6Rp<;vadVSj-hibPfh= zylJ@gW-_SwA-8hH4?5!8!UkSz-$U`j?PwJ@vkFJOh3geXu;^flNN=;&gZN96k zzGR}?4g;bB@?sOhW17B(M90vnqZ+k+NJH=l)XqckqS=eqj9t%I|!3B z+S2gNpmUZ(?`9<7sqJ+hEmFrwlzxSw=j}qb**^UPOD|NqMBw;kzlz~#!D(7e3LIk$ zIy&3z;@eR$#{_CmNGSyca%~)br-BKwc6&2;XMu1(2vpPO@XIEYw{oczmMzaX<2-KS zxKh}N=@*n3)39sXpTVkf+m{|l5yZ%U0QZCFFl#pBzWHQZj@R2^gfXrk8T2Gh{JF@9 zi|;~UK@jq?1T&ni>Z8PT;GAVNLI-0MrudOUgLBvj4+0a^wolpV5HTftYHdW+c0JGb zdYdfcWXX)t)>vB#;c=>YaT1_C)7CKz{#07QT(^C@6W7dKhnC@!RHr1U##Gj}UD+*ej7g3DXpLXHmzangk~ZGocMv$#JRlkQ zp(a7DZ3-D>ZjI>%ZAz5xu*crxr)nMkwORfad%>0IkquKuIq=_K3|BN)4QBnqJ^`qA z$ZCClx$PVTt3V#(&Nqb6U7+En=iRxIC|5u&DGGIJu&-VvxMXs_3`G`IAJdu>_=yYpM)px6HN`{nm^R33yZ*tm5kfC^zqjJ-95l)QC3zwxIO$%HH=Y%umtUSrDXres zBKKl3F;T-)D9O~WyQ^(k9&F;YRy5}mJ)y3&v6a<^@#nkS(;X8MF!LK02+MB)?k-cb z&Kwd+=1g2CdIVKH28{vf&|+2C6m%gs<2WjDbiX6Ng70`I@GNXG<`gtoN`zkEh5n(0 zl3Yk)&Z#Dpoffm`g$EC_sQ^H28tu{++399bz8J}hYw!(^lIP%W0hmujuO?y5mjqq4 zPneYK{?`)CWAE_hDv%xrKh;QZ0Mgzw0(5_b!EsqZ0`Sm=Ey-aA-I<1X`CD}=+!x_z z@dg#f0!sn8vW)lJ_DJP8#Zc*dI*ubb>$u(f6luG@NZ>aI?1tD^X8(BT%xAQ^v+egN zZwz{54Y66wo0i{RFJB<-MT|--iE8Ax0u9m+O9Jxe@FMb%3Dl|~Qzd(;qo`RysGc2n z_1!o$oZ=}giFj@d=W`R6hv=p;E%0}dRC6kWPT7Cs;DNmQYS|ItL#?WSuc2^&BZ^J~ zhT#D;^euep-0DR!y$AsaN23POJr)d&yH`pCIi$T ziHvYBwVgmo-M1d{(+c95zC}VE36{C9wu;?rF&V6huu)C74M+dfCoS^)J0ZF^x{-SR zPh53cXBtVgy9Qr&S_lR$IK0=$`<@%{19=9YA%jQua05E;&(TaX67v(W=UKMnv&TIY4jOqn zu{!5R{+|kqS)&Z;m<`c<;A-EUY z7Ss&R8qtb|Uw`?ctipg_Xa{Qvi&b}bKdT(ionIK_i1Y>{og=SRINccu&&1VI1bo8C zV2t}sSk9ORO|SfWj^*@}e;Uc@{;XoYxy9rneMgvl#r8MKdKm)dO66xfq3#Q8hM_$OhsQ9JBxs@Po}6Bzn2JA)@g)8zU$mE!g2 zY<`T~+zX>HFXH?aDb6?DcADYf<*%D`F)tk4C`FA(6q!i3OwOkNA`19(uN1{coiyx2 zjjZV0S|s8&Y?X#5_1@fhu=a@6QfB2!mnba@D^$GI8FV;*-s5eWZIyBzEivZvp0FmE1RX9 z)GZQ9N`e1&#@jzTp3u2D<;UDCX@crR?w0=f&!F3jAbcMNA+hQ3L~pC#eFDT@OP& zBoym8#KkotEy6qx^Ub7^RQ!HMm}y?JL-9+T|AfHzhU=?<7@$~~&~O@g!WL`F$d;HS zY~IJ>)pHIaVa{{P{om4sraAn!b*FraJc&}yKIm-G$}<-<78Mm1#Rbp19_N|19TEz4 zH)cG$dhviH{dOL?3N2k~ASGeyl2g9Q^pVZS$6No#)#1B0`v%>FYsAQ3yIgF*o$Lq? z>zQ-)@5lr#$BitSX#3;EqJh?kPQukuaQW5^voK(Sa(z_TIyI*|6gyf;RT$m{B< zx=)}MhwoXa;P4nb3n8_jy8n*tYi$&VcEYj_31SRd*adpT zTb^Op50HgG!mI9IUZ1MB2WEh);{g7i9R7#&9QJ)|qvpm33SR^zkI=A;3`4$SG@#hV zD6W0vSD9buX!7m%dm^1G?<|$uUza^^Al`K5kc>``pg;f#V_IMKx^g5q?2_fHr# za2(%@K_{L~;W1*?`USoBUXWHmkFfHKpMMvt@#A$%`SEf2bfTxT%J&t-;2GHbcwc4FlMMMvaDZ$_TmCTIHLQt+bK~QRN!7c0%OP^Mbdx7bkP+AaES+2t~ zB-oK@n&l?Go>-b*mQjTrg1z|oY+Ipr>-Ppml#x7i!iIu^f`L5#d((lu@Xo#HhlYlN zLJFd&Z|<@~M`rl%cmEI1g`}GQ_bmVnHAMj%^a=9$|9P+}xc@v@Rs#337s+Vvz8pQ? z&pWdmoTao>!k*g)=HgTv-Ao~AxdsDXDpJaHYBmEa0*g7%2rljyqL&BFz#+ml{lJaG zeLwraeYvk|Js@h|EsvSeJxuS7qq~loqxbe@ISPs}EJ-BF|Hs|!$DB1a>_xSr2?!7X zH-*}QW&*;=X?PN2Maw4OSu+KhVz(3oA_#)N(66y>!GJ(DtgKYxFO)8v$kQg6I*OKyz;3^@_c*z0PY(~fKU zULEkg0`>+|P2Qmc+S5L2+g^^I0awem5NtZC9!i(~X}U?k1;LS{epJ5*7)GLgZW^d^ zmmpucRo}7!x}mD9=M*j4vTcV1nkz@ErWI6-e%h@zUQ~h#K5yC9{wDF&g#IU1knL)tc;^0yRtdJQ zcW_g&?DMKF{7B571l(Qm3{L{A6`sDgnr5{|iXYXVSEr20e6~p6aO|& ziN6|ATr*7E*))DdADtAV>zCEv_7FDm!fFIJ+C7`;rKF9*{$-nd0wxz*blM$UUhak_ zzJW_4;4LF93wyvCSSZwHeKSyb}>>jE0nQzyXZ_a1x1n>gQz%L0DPqDR~&QukqMG}%8wkIDNSI!{* zO2rOI?%&!@Tk+NFe2+w$Bz+Hg)ueXZl1cAz+^`63JqDqRIE@(k0C`TJ-*=*AF+lTz zy9DxbLQJQdDzWBSZ{aI&+3Aj72_znkkd!O5Sr1E~tsE62g zrrE~H^8kPmr+#nyIt-t3+>2fj|Aw(nFPiCA_Gp;Id{9)07|)t>S8-P+oO)$$e$l;r zl_Xwt;@8eU+q`imuVP;;cDMspKIV@uPY2xWWt~+9M(V_=Ys~-3sqC>i`l+AL;6Po) zzWMfpzxun$DTwT+%+?EtIYw;oKF<~|m_Y=GH0#TIz-VO0L~p00}yl7(9aFsi@np*?nZ0+vy`*t^?vQD)^l2E!dbZD zVf9!b!i!#{=Xmc;eXGh2ogLwthw{d|eIx;%iXl z8>{+WTDkI!*FI#eQSe_E!b#_Yk#fL%5Z3=z;Qqf>Q(VZwVE;!&wlVmRYI@$Ssc#_C zD$?chA5<}~NXZ!dNc^9jcnp_iZFa#_!<$7aU2UCyE)17oD~L_w%hB!T%&^z+tCTFhXCxR65tn48V3FMt zxk6|5g=-8x?|TVR`xrQN0e-Fv%-_q`s6#VgiHgjPo^v(yYgfhNbX{4H4+W6P6ZG=7 zpKmw=;h~+ftG= zh$JAAaDRbD;ipW!aqBgi;V;>gT={Z$=F1PlP8ZMeahO^c&nm&pHL%&m?h{9v6Yg2H z$FU_p!>pJ1yA%03;{yE#olcF#L-X4rtYS~P>Jm5Z1n+CKJ?WdlPn!gu`DTTCcCEj$ zQUeCLL>;6&|4?hVfy&v#M0DHl2$9wi7Dq$&uv?x#)iZPWeJBD)R_Wk3fL;v0$`dlL zxDFI>ZGqbQR7!ILz(5p$`dC|&1lIXqftdxZYWL9l9ICY%{cf2JCVJp?wPg&P_ghp( zaR0DSZ(BW5C4J06R|q9I^pD~;xiDpJ@e3kjhPZfb3ZVwqz1)`xkEuB!X&>q4s3LNB z?L+S>@o=skRCIQ`KKm%_O9x!GLszR z8bQNA`aThNRJoQ9G;FJ<7wUbSy-B8UOXNq&Q}P6r28<_;rpAB`+!)n(n{tVK>rtnO z?RcouVVm-Vf*IB1T)Zlp*BdQ~z24&srU$-YMcTdBwmw4s2mQz{pIbgrId(YrQ1OhY zgo3LK($7VKVVmd^N*$;g4FwMKiOoR%LM9xdq`mu#KuDN+lwCqjd4Am-loWY=VM*}|Ym0;ms#-1~(T?)^-G?pSIL2w4%s6k6wEKy2RjZ`t?i~iw zYYWBo$Gm>n#7;lUc=EY4GR>O{6#GEro+R4qkvp`H2*-H|cj*(txd?@L+04k)E4?DV~5hsQ2wx_?vGf|L9CY@ZNc*Vx8qm ztF^K8-&>YyuII8S+{nhIYMTKQ+bHddy4$bIcmE5koK#kYmT6KB;evpHrJ z>wRst)IYv}_RBE0&x?151q^a8vP`W$bUfm^#X@g9#Afy z2(0veBf3y*{sc<)ZIMrRrygcoFwPx5O9?o|-lFW;@(TY}KMMuKG%9jBU^>u6ZH5Q2 z&88@RGYXXlD=T~{yHmtSDxmx@Zbfx{2vvE)Dk15gW8=(zLH^$94F)VVe=(1|wy@1Y z=yxG+#oiKj%Q;rqw=JtU5{xj-V?@cDu(K}5yGl^N-G1yf43?13ZdRk!13LLbeP z!P?C9D-fVEX@erlgLjx3E5R}T@wX<3E-_;NgkJYAe~Us}_!k*YrS)N29pncFp0>z_ z1dB{p%x3wJznC{ok&)naI*DKJg!~j=>yUWB)tX$B@hVnCd_VdL-EOW;7&2@l5>Mo23+8*$h*caL5`e{uX7qe#sBamr|Wu zxR>%zpx=fKI`~eQ`tKGwOhArlW72RCS+U^om+;_2K(|_xUudd@(5_~Tb-YEeY|L!e zHpkg#%^0)?*?fsM&DuY9VP4{4cgP5G+WEEiX>MK4^@AC9-g}D9>)=(HQlIE?9)0%>D^RiJ zfN-rG>nhg6$NO^Ek`y7MS3~`e5EWy-(V1dW*Xi9t)rmlhHAu|90~yo3YQqd{H`e9MEVZ^Fz0*dQJvvR zvs%?Ox+%4xYvAE<2mOW*r80#fykbMqn#M!;r?*I6{l; zIx81Vna-8&q8$fpSt!{Sx=2Zn+fNt2~!Di#_za-13UW5f2cciq+2e z$>1Ph_nP;L^XgBH-sDcsFJghe=7I&2Pw!BMiMK{uXM*0y1E(3Gie=nG)&8WYL|W)} z1%$C)u`jAcgfBlRN>x-YOwK#-W|wu`r3TmWsGm)iH)K9-i1_H1R&U?tR2Xfy$WL>) zzQ0+~esTbViwUCnMzYK%KOdN|jULl~{V-~Eh6%{{+8u({D7{_+jt$7WVp{C^;9?a( z98X^hkDsHio`r7< zRvP3kpC@!26}eySmLYLkIJ?7LN2Xca!d-y5qn1nxQGhum-Np~lKKs&kL4F`WL#wCo zdv$!y=t#stGbG zwc!9~mXk*`M7S`0O5hKxEG9ZYVBI+k2bvwVO5Vk-Sv%YpW8B9-ezFNchIAZ&H|f%d zJ|noO1tHu@gjOG?%gk7IKo-3^{M}W$Rc7x9x;e+(S`GRKsdlc!KEe6x zbtJ3UP$|X5GbDnuArk6qE3jC@^Hjj<~B2jnV~tq2+GR5&V4^kD>?s^JN>(4QhaZQhV&BS!6sXW7s%MG zAJ?OOzl^fTs+LgcVcIz|>OX{owk& zhBrHN7zl2kkazqB1MT{dkX3zkbZfAy?q};1pKKSCuR`ZGQL(1* zfP9B)d7YMF#So7Ju~88upPn13=ObNyDS9bdcNpDuLd14!IACyE2)I^HNyESBr6tm8cijga29tH63hy6&v|fl86L%-S^6jmtj?cS={z z|Gb~%>NmzN z!$se#?q3s-YSq7`b>n(LW|ESrq3--*MI;NqyTKPVVk7$!vH^$+Q5mY{nj&e0Uo)|c zv9B>}xe*Q+xKP~04*Ywbn2=R&2Y1EIN_DvMrOX(rAWz9H4;DXb@l^JBfRwPJzZCI& zPx<-*$)L!snZ$H~<>Nx^cF`6uiLFxZ@E6CEa=)w~lz1Wk!>2W(VWvIB{13rzm7^31 z0X4EVb^Vl&aMjPsHw7-9s78`)EbrrvEi$H@z>?<+h>+V$E-GK$(DrP zfFQAzWDt#N$ofD&H#~h(yp}BKRj2rhby+70=>D%VTfPxc5$etVVz*;xHC^)-?T^IAG9=Ry2{F`Dw@0RHvoOfkuyBSt z+vFB&@#KzFHq|PeWc?UjKTh@(P*?yIq}!E=y`%2%e*l>e{xPE&{)5k3V!Gq`r=3M=zE%YgUu2>+5Kw#}TLuWB z$U+a{0BS#c^4PW{hl1)@C9l`TOO4q`rXc5J51b3#HHswMC^4a|zSh4p+ed)EZiPB*bA8d>%IX`ZZ8|4X z^H;jfnt*_q?7Kj5jupM)xK^SkuBl=LBr|X))wu0lj?|}^SZL_Fz|2U~_XfboCY4Oo z(L=L5HqXl2TKH7`b&JZ!HhBEyw^C3i%x6G-^Cz|cBfk&aM5)k5+|upg4f zAon(w-f#Ym3pgouv5i{AO*Xa-rS=HhRn9d^ggx_EgVt>{^xgJHyjR9`@9Zq|oNv4r zFgHuRs^q6d@@(tg8Qah=og<<}v&xAf0U>TRdZi0I_ikbIh~7JV(sq9~D>IsyS=FNB zzbq%@EGxD=(|)%OIz`~1sMMf+Wtk8`ogK`r@#BJdaA z#~iKhyE3z%{Fp>EHaT8HNx<2KrbR(SQwlXc8U!~%c z2obye#d?u4J}DvfaUJmB>_HGp&P&V8RT?->Cb=~#k4RH087 z$>vg@*7cOGeUsqW8uH?x#@DUlwB_f7Ag5^pAuR^N6JIu1St_@$R~>R%Neu*jvK6r;yK`JFn%^I7LFof#sKP1y{8Y}5W}AS$B~ z%AZ3~il0NV>&~DmCP-3HNVIF7WftG?2prsgMVew+X3{GoS=%Q4;->Fsqx@3n%@%y zYxZuFNAQ+rBMLQhRtSV~X_1$A%{A`}#t$y6}rFJoD#Kw z4QBVq(sX@1r_wy*2Fc3A`$3!^)z&txW41R8`abV?wN7?*Tpyy4@L-FtbWgaV@%We! z=zFvf63WHxDtWq<&~&Gi3bS-~Ku}`cyBx-&R@X_jf5sf|0#A+#8--1*8oux{-aobD}j=&Jh?9j*KuRh5qKh+q$qB1eRmdwixCiAIprhNS`Hus4Y zI?20J;)Q2g(K50DN03snO@d2iokW1yrTpU=c4!wv6ZU_}*Xn8+5Nw=k4BC8FpOE1k zw$3k=V^~qhFo+L!)i@P~yR~zKrcW+)tT@zx!oW8utgPrb8IS+comZGuaHs&(!0kn< zG44ML?Sae-of2XGF)qAH1M7zcTBY%BKmH-w3griU`jO|1exgy~olDDT1>vR_?$y$S zpuNX{FTJ86;c#xviX(#ez6)bw&(KZ@0rASTp`8()++tg7y4C;MYLyq5%(9)3Ne$ei zXgJm=_O5Y{MmNX}OSMX~&QFu-Xjg#v->WiX>vo4(Cw~vqa#mqQ8UL&A6@@`S|13+9 zt``~{;XOe04yK>CzDx5m56-efI-iAGhWvL4b>2z92x8JHnaR18<%n`0B}@DEQKx*I z<$}0Er+(SXm}7xe196_4xl=h>MqR3Fa! zQsSAQ*lCctDB7*i?nn&(_6@}E(`{aZfxCdZN2`^Y;sOhOflI%*P`63<(-f2< z&q7ANL32fPx*zHMWt~;DR1NnM$#r_QWkkAh`r!_$)TUfE+zA2y-l@(_=99Oc#^ugYAO1^GszztayLKsQ&T>H7;_0R9A5V32MF?w(<;B_^ap zx>u{*1-V4eITq`>IR@M?;1p)ak)VVJy2O`dl`l*_g(S5N2pet@W07zDJM$F*-9xi0 z&}xy>0v+)(+U3~YHAcOMV=_=3c{Ovz<6g08n0Nt+)-Hu9h&LM;PW<@5?HtzWmZM^G z5E0E?h*z8o4mP=Ln|9|x;a@~F!DWeSnc3%1nnS*Ijp=X_623-xy9a--@P%^d88T06 zkzc5uS`h-`4utUk5}a`k5}Fp8QMRv4t4sR4W`uE%)GrALhW$OnPolp_gL;_^gl)G? zbB%#>IU(MlM!Vg(ek0$U$Im_%A2ndMmA z1#|^{emUoivrE*%-bY*U?`YJwiGtoo`@np%j0X8muNJtJY034f^fpLknJuzrTPB)6 zJWA(;k~-Xd21JNlj8RPxs|R0FMRA%mQljXT=UWw+6P>?&6{>0$|55%`{f|X(+%;yY zvUu)b|A(v_1Ze#L0pi)@EYai~ao=RSLVYm~6#s|*_6P(y#k0?Mjd_9#46sSn`U@w9 zTHEK@JE>VY!4?{xcJcOyb0pCYf6qJ>2{Fay^po~+o2^Ew!3_GJLgY51VlV0X7B%q` zeKuvI_T6cLibJVmkxo^yVv>PDd{!TIC1gISknz-GhH`F5!M5dk_ux79q@iGcS7?ma;;IXi2>G6v&WrTkhGnDaq&A0`D4o1~~ol z2{nz;YU}59$whoWKwuo88l;@}&Xg+;v)d*umyUe($=skL@gZL@BW#I>ocMN5~@cQHJtMYe%)< zXhyEiD({-&V27>`2)X~;|9kaQL(AmUT}4LR8wxE9nD*wT80OXclVVn`kL$RP2Xe4V z~g4>k^-3T{lVYP^8GNP@kXp;r$Hn^aa*p*s&rQTn&G%a~0t^TfM@R2bCA zvhaFUZuA=H1qndX5|SxJUAiK)ju4{z*ix=r8P98tNnWk_8%{df{PlXyi8l zG|))u4*M4Fo`&0%8zWbn0LDJ?l*nh(GZxgf?j_Cy66_v_cb(vF>M_I>3Jmf)(t>sU zqQb5$S#f?--h=tuds-(4r^DiDD2Mv)eVIPYqM;2|BmF;XtmEwA0DvLnuRk~zIp#xK z9x#`w3&ODtri3cY9bvlziuHfe*p*mkLc3s}cqNa|eM0^JiW`w|!@I)!0;nr3d7jAMJFP8d zU~i1)u~4F4eUgZv-cgI?kKypnJM?Um%p&#>inLW4Wr`>HL@VU0FCgNb_{a1FeF!zX z1{pgvjI(eK(e`nCfs5pH^+svqE8Jo^&rCy4!2G=<97T4o&|YBYer|^4*EqIHovi)3Pw7q-zq0+~&6-5_)tmy6=U@7?fuJC@q{4kxV`{!{(9vNM?1_NH z*?4EYlIyw;FYNJuPpN&E$3zHc@JUsY>!d{we5tCvt{ zuXBtDzAtPwV(<}}DAK`_*K{Ce#k7^s!r=k;nWp~1<~oU0OZ5Br+hoSq5rG|DRvEvl zBh19AO@f@EoG|vs>XK_X4sDa#PL3xp5ECZomTHYN7A~6jjc! z`$DnDLkMY+?i9mvd_x|QC!CK35ws`@o@dCorBmyo_2``D0_zDdiw zb;mv1(4RVI% zWolo@Nx`R)TjZ%jwF>z}ACdaYjQ>((hx}hS|2b~_!rL4^oD!O6|I5&~74jCqh0`o+ zS`%2;=VMjpRMIGQf0&}nGbQ+`)x@AYgrxGfq~L%d1(pk%$6-ZG^%oVM(viB};QMyDSfv0d)l0!lSPUH*jk&qI* z0e%^d_n3Du_qk_*jdi*V_lsNud<$gjRESUmtm<7P%WQ)OsQE7IJdW7Atb38kUR$)! z3KLj(Z5%E`4h|921f zj}`A^;T-^=pEPSW{o@sG9ri~he#Ya_ZVz4Lzl<)3z1IL?WkOY{=dd3fiyTJ`@$njP7BSZ&Mfk*8WM zcsF!{i&&NX`WIbvs)x)6#N#u=!;@>1d)63v?k#FNG_o>a`Vm@G)L?&@eS!;2?QL^* z)dGkc0Nd(_mzs@L3Ee?TL}F}8*a-l^>v1dCeJXk@UMrEX1Ga6GwSSle496bp6V-@6rj;Ge5VsJ-gI|AwO|H!NeZ&Gu(5F zoMeXAqMIz&mrG_K0jN^zYl@PvTfsjfIIsA(7 z=#XfQ_kUBuL}-G%we+OG(+g>aH{`5SeR!R6no%9qE!d}5-y-dT+>9^*dSz^reXyS6 z?dzKaHT;JBIwm6hhx7&jLJbIW{9|OH*}qzDyOtqtnE;!9Me6SFfE|jPNEn zQDq+Dxrf~&;v1&g66KBP>XEvG{f~W<)Gqau5NF>l(n~+gIT--z`;FM^9LuJ(J$S5@ z#Jt$3nqC~!Y>Wtex?5~M5_pXvk-d1Ho zJ}I9UWqIfN&$fk&xMUO;RjJ=Gpn8k@l2bO1E#5o&SNuGsjj(WHI0n7=mZwkL!%PxI z4m1x$mx1W$IzdS%BsI>JX*Okl3emHwuy8K`VUfLA5^wX3Q0&ld%JlA;Tfwkg>^sG_ zaLq3Lk3o+FxU2EE5=M~M)6>y)j62$m61!aQ@k^Nq_ojNIKk_!YgVuk&@n*B64#~gR zPfgWxZCe&NhUh>)#B;+cX2E#huIv0$tlJbx)LO(j>BhNpP43*3_@O~X+Wj*ulXI+g z3!RYNpCi9S+J7;Qb$NsF|0`S>-0}F2f=3S^+xk0wou)#O>`ko)=?if(wxC3k?CmRt zW7)A6A1>A-TDP3nk2R*#$0X|mx~x{Y$_w)dNSa|uHe0WO2Qoj2Zf&3Rj!^fjRa})C z2veehv#;?fTXmNBuih!`zw&Zb#swywC0?I0*c7nR4sG@V74A zVM#{?HnnMcwDDD@q|EYHXO$Uofyd|8D5uw`HUt-RI|fwfP*y0pwt%uKCJE7ORh|(1O)wD-Z#E_ zUaYgA%sWT3d>Iqw*!->=Wq&nDy_|m+n_Uu-rr-C7NyF^j3S;7pb3^mO3+??SGL>y! z?G5%PS%d5yVV_>3f(6kk9p2ca2<@WOvCO_Y)TnHX+Z^-%@(rWe{jY0}=;hh3xuoxQ zZ1%HViWm3>u$)G_`xGAZ3gQ~whw2qw+??bslEFI}rD#boEOb;xxm}=zIoDuj*1Y7N z*u0R^?sW)ZnBR9R(6b2ZJ}KpKg+S|lFaVYO!;vx z}3~u)aIQK|goPxh2-Pr}5C>+Bb^XN8u)L2#19o+L7H+B?oPab&) zzBFK}=Cl4cjGK-?9tRdw;LK(5?q4)x82ebe7m2i!dQ=5aMr9W7k#l}>{<|pK)DlOj z1r`82Jk79VSigGJ{zEU<^<-~FvW(`VSqZlT zm@QIh)y*+L2?5bsq!FH@T`1?1eNB^>5e}iO%VC#Hi#aE#HrDyp`LZm>=_EttuIZzf z<1u;02htHqJM@Ggwt5H85v^G{#> zT@_P%AcWPp8p6G;R_{L@Djl zpa)+|RN7>&QR!0wQ@V(pmDCCyIraEo!Ze!3dZ&446Zye*_ZGShWj&A#i1g|dlVhyP z>=sXafOUEO4pewOlzARcL%lrCQv(WdB*FFd4oth;jTmPd`#Oy0wA&IC*pBKkKX z>?x-X``ehvML`K`Ez=+B?pyd~pG-4hkKypqx&v>(p+sOsW!W|ZrscshlA>BD z$a8b`7i8y2mCfN_{Gev(E zv4uMd7N>xA5oIqSQLm<6U%iAJ)>(J^+u0_Kz=TkB#QRSwZiKK@yX5kJYFDrKP6-VN z4Y9$q3BQR5d=XJi9koz!M7Z%SQ{IK<-SU~n#s#l3&RAag{Qs0GxKuUDn#OeZ)!ATv zyhlhx6|`D6V0rHz*7ST`{j|Wo*2C}muD-&_HGh6S@$qo=M>tOlTM@HPJwaoF)k|{p zZ_<4O=phP!!GSpi#976lmk!P|X80^%tJi+*c5%RXdHIo@`=)r=W9imK6G5Blv zp8xeT5k0eA)z}Sz^~fD@Igfr?S}nRAZ#WDD3^A<}9ay6$_*o?#U>0r#`u*3cQwIWC zFFUxqIhJMg3^Tdf`)z4|iDr}Jm7&lsI6lPZkm?%y&BQ;i(+X&kZpyVp0UAwLixe;p zN{X8p<`J)l720xhc*Aqgrn{<$`7<-C}B!leApZV!6D!#Ar6 zuZ%0OCo;vfD9+!%Y*6NtBr#u^zJEY_9%sJ=gn3KwL3=|79N@*b2={yGkFaIcy*>+i zerd>L zqMf7NV0|V6Gn}}m$&TS(6P1zMH$pPo!_QCG#`q*n3CQvk*hHJrJzNUju9OUs){W6r17jt%#Cr_)_jspand|pVGTbe@~O6SGYlzN z1XpCW zMverW;NcvPb8Gan%B9%SE#06V;1KRD(}nvN8o)iU{o5y8BSCacaA{OzSlJjW%K_3N z5e1~C^f3>!-zgY$dKD4f7X&!(JG43n>vszV2?gu#weJxPZkiI4Zx!i_^cEVJ;GYtZ zt{>r+>hA$ZSU+GSo5uM>o2S`e0Wc`nX1PHF%M|EBst5*(1U{*&;npKm0Sz=3iAr*MP7{=VCqZTEI*a z+I0t%&GCoVKTbjul4ea5kAQI0gLn63M!n-4o-W%^dWq6WSdE1^jdQ~<@vVq8*C(is zg5c%J;ebCHcBt64klvTa)f#`5|EPzI&GIMOMR_h$S?5Q)&XH3rfr|kPH^!ve6YIh{ zaw`14xoGW^D2`}Xn5^$T;PyZJ_a1IzVA zx=rxMHV<*lh=+KMvO+z7-qXT72h$87!G6S8y^DBKb8Co~M@ONW(|pc0`Y|_wj4>fy zBRsBg9Ad@0lxerh4GUmi!kn`$yh9mug!{k%PI8L;cbEn^gLQe9N-Sa8cQlLr!kr^G z**YYeE}n69;#I=W4ZjuG_8qo=CTv7|MV4OQ+E`~8`;-as&QQIg5n)5z*hY04z9skM z%Omdb6&i)E1Nz8@W#$18KnlquJmuGY{gdgKfE~qRy~@)=hpADX=9pM4Fw!puyk&X8 zT{Dave6QMulHMS(MlTbqdcOzl%%V_b!Eznh5*2koNjesg7%rn-d4~d?4S{~v$t7B8 z)}a3D-?wYvSo;oBhlr}l3R`}S&-_|5sI|roNTaw(Oalfhu+8kv>~Hultp6}CC_`<- zwf~Z@z!njuy8b19X#hSG1MN|-j|hr%EV7ZF6zhxh-ub;Vs=_|A#+2A&2@5%J)5eSGxJ%}@CD ztFJLGs{&96c59Yxgn7<4Dqrs$yF&@%dW#0?A0fPDAEwGotOqdv1NUpl5qlh(r{5h= z2gJHC&OFA$x-!of>unE$287iWSf~^tJ0@5pS)g8DK@q*&5J5d7KY?FD81{(}--5j3 z4P5~NzyL#tk3yoLT?~)-qf6`?rX{E%UMVrbiS#Se|1#WV-DPVQcHLDOzC_e2c%>2gjET{1N&~ct9a2)Q8{&`w98-`?0n4__(NK`_^G)l}IASW!{;2qb``m=!Yi*yEgj&M!^V{C8_AwE%_k*B_C`JTW}Sg#0Q z6RoJ*zr+OlpF`bD{3EsTokXxWS z{apb6aHg4RZK9oQGd)M(98VYFAfEa4Aic!M2TvCfY3)7^^fcNrUKmsi#x&ZJ@G8s? zyh%y&yFh3~2e>kcO|*OGKXbUA8GBvow|%fYrWl ziH~DXscA{~6H+gcudMjSE1=qDyr4I|@5N4|7hj=)Q;h z&|Kz;GQr5P;~#vn@@{HNeDi=vdc3KPEj_OVH!Po~Q7^(7!Sa7?`wpwF5uQ5$7_)iO~BBja3*i0F3j!zh^fTenx`<-(g^##yAt4800|o&@HY~KRs=e z(Eo%x0U+3CN7-~}-r)`K=yu(Kd8G|fi&Vv%xn@?2{VG33!Pk3HoJT&tG)tfO@DgDJ z+z5*cu8aq**Ks~zWl>&;*)PT#erZY9+1wf*Th{x-Ac@ zzZzab1UjIK^XhDZaH%J_V$t(O&!C5^ZPJ{%o*N0poZclnkMM;dk?$H4K+^#Hz@T~I zTab-)9R5RuRTig+nbleLaW16X>)@U%Q|>#7&5ARO>f*fMp8js^A2TZW<5J%Q5agTx zHc*84JGU;N!s?nH1rDTAL%q8H$4a_22x2SE=Yne9D-5hF)2PCbK~3*PFC0yp&%1w10^l$%Cu^i zCe{V9tXm}2%BLWzj#s#}nAG5zCo!FlYqrpwj;=q%F`>S)Or-mMOKWA0*&RCnW_j)R zu|c;>y*R=juiKAJ|@c%5bYB_#a5UbGACCm zO7zOI)`hCL9gO;OP&$i}cHz}&gLy;xV?KYE@=Cj4dlu}5$*>K@i1FfEiLzK1t}G&w zU)+0?=J$r?q5)y|-V(120L&2YKl-QxCXR;_P6ZsUIRqzit`-y@3>_&T%SnH(u?#5gs({igt0rcK{K@@+VQ&%&HK zWyqp_)P&#ZHb}8AU13=j(Jh@4=~XxuQEhI$k3crL#dm{prtLq!5SGs1oP#t<+=7?c zA-1;2>qP{AWJ4f@;pc&D3&S({68Mj+0^)`b*HXu9YcPQ|qQe_jM8AJ9rZ^*ow?96I zw+FSQsrT{@f)#v%;T~xBFpPp;o)Jh^zkRRveItYX*UG9;ku|}&5Bz~$hHean_&&h0 zO!M?RNB{ACd^ern1R-v{e?m^SFUJ+B`=o7u!Mw+M`*M5afu_burB6^c5KU12?M(6n zH_x`txW)Sb5n}Vs?ce6~*DnU)g1FcJQ!CVV>`bl{^B;S&31hE+S36b~oBQO?T+|Qt z@9mrC=P?h=a0_vP1cX~>*A=)(79xj6yCv0tuL|dj?(w2HG1Dwa3PVqM%`=Gz{wQia zfkwk_;!SU$J@|e=~6*jK-_nj zlSN<{+=lo$fl*G#9Lpu|$INyQc7^a9LOw*e1sS2c$NDB<`SgK?--h6KIN}dMYAr$Ls*RV$U=#2K*!+9hykC-Fz$T_?8F?uS}MmyphVc2BYv`br} znPp$2r`hO`MEIh6<`#dK8gf*d+OzBn`Vbbc%n7}A!|v8??$#+;9^{o>k*hdJ)qYdUdglF2ZYQrubSm$ zRuCV%A*&U?lYZa6OE#9twpfqy=tfK4QFr%1~$NBHAFM&0Mae5(e_Y9(~Syr<*%Y2;GB-5NYq~nz6BjynS?Iy7X+>h@r-lNyw zC_UUf*-ySQ!XwZJ<}}a<`y1mM!|@gg%Q3;_hg9W|?0dyHnB zO**$qZIlhVh41&D$9J$B?Y0fEZ5qe)lgBHVqIMzV$DMz#`lH{EgxmKhyu~;G^@aiV zK|A~i=@MyKXOyj-ybMw&3Abhc0eu!>p0wEek_ctge@mjR<0dIn zbYT=-T{pB{^)xl^9`X^y1A*T!LPFgI`G;S>3ElPbPyPL0{q#@03|!9P}EA9Sr#^n8(qshwC0+aUV zdBHc3m@Z&nHTQ_@{_l02C@O%%+|hrUuTGg`$2Jt%W*5UcJkTRLBFI!5Ji|xVkhi0# z;mXi|_s`#mJqxmATLqE`o0hDXXR33);`z0qto2kc54Sy0B7d0c~bKP zaaPreJ7c^2be#qG;LRP0lHRzJ7{HaWmV_zo;-WZ|_ zGNxP8|7)JRPC3QqOq^p37sT-q>g}K0qBvmvVf?!+8n|-9{PhDUx{2r{;2>?pY{bkL z>cq|JgztmwtVcqsAI~Vmt4E^2cJ`3bJhu5y6PJ23j2BvI6)3|wrxep%mGI5{uOC*)ZnjnBPQL2|q~Jl+eq$OMJ!uB(%phd{r0-`%Rv8{YMQc>;ld)1x>z;)i{Jc`_p= z+RLsmC{<@%rV%5_m?*>J3tfv4##LAxs@b;p(*(AtlTd>qRR{U*A&0ZQ(4azynO(=-EJ8#-dU0DP1zAQB%EGUr;4wY-d=Ty zQy30vv`0s^a)&sh9sGR14z7FnvRuqpmB0T^3KJCXY=8S74_$j*cz9fw9M;SN=NZ)L zy?m6)5T3taHCTV&#Y5fzIMCyYiFyyu5+>n(&A%8r$S+o8FH9eubHq?S_MzCJLiMmKN21$+C4SN$hl90ynC?{Ld;`3l67bYs6?;c|Xj49U$8vZfMd3h3&L za19OWqF=6$7~VfbOlZ~RqOyhv~O=@SI0Jr^9w9$mDO6Y9Hkqb@eK>*O3rvX z<%Q&Qt8k7`Pr!OG9d8-zTl2NxZ*E|6WU}pQ3Y2R8x(k4<(^h8a|8+?83Vc5CPaNy& zg3zrzb;R!d4)W?&g!0Jf!|(h~d4|@cp2-)%zC44PCqkOUX*bjwU5oJg$Q%Z~Im2s?E#p8SwHm8Z9dxRykwv`fwLt2A_K#+iKa zT(Vf!5M4@aCPb=C`Mlzt*;eO7Q>)^gI*iO4os#vEPFrqL!uuA?KfTqN<{-(4c6w?QbPolJ; zHw&0Ow)KFEZ&YP`d#A~mT%@QcbN!r7Y|t(D zvyJj~O1XpviTu-SLOyha|9!J(j}pWj=}FjLwfGPaot7LIownz+Im9Z*8^0aUB$i$_ z%eF$6Ubaj3apGTmoOVkf?#_lCfNDpZ@p^}9|JD9=Rwkb61>k0~yLTS((1QcElEMbs^9%^nUnyNMOY}_YR|L zK_l+-ww@BtthY+WvGjkZkps?pec5ksts+NqSy)?JPH1G0oK}H%|2?-ZFefkk#nafw{r=~VEu-kEPK z(Z;&?m}usadThRKRZDtc)A`h|`mCu!sX+hp<8=#lG-fPdW_8>PN|819vpQvrMUvYz zi)Ke)m{h;u2e7J9Epb8&`Fusb$`#H1g92XP>FT7G2lP`y>65udRGwy3nc^qaVp~5xvVL$(!0-zc$`)z)Wdrum4!`q|+iK7npMT$(T(KdJYbdVis)IN-U!7vv zBmP#Z&o)DF7mQE;Q=1gqkXY-05;RD(=l)V)S*WKFtsRIBCQ}coCpdyO54GfrE}s;S zFJVU>mJL(t%5(94T9v{Oy?j_Rps7=MjN(qPJdsmkooIsVbLWk%*~FsRroK$FD4^Yf z5!C&V=Z?|+Cn8f7(S(u3@#(6{`T|9|sa*qf0`KBIZ)ik^3irKBbVP8FXFlNAW(H`S zyEPOPg#>W;C%OL{BH1p?+zvUStH3qUF3c~}1eQ)@|M!dWgj6O}kf@W zVI8d7`O)$OO@=tb*82_r#3*g}Ecdc38)7qD`{#RC-6?{L>g??N(!a)dI*kjBS}SDJ!34)yP3#WY2v21HRA`tKe<^ zP>ZvCp%|C{IQ^c`c4A~&O>zOxIweO8*VPd}0vMadq_8)v1LD%-=Q~ft?nkBx6roZ~ zrtxS4_#Dd+Pml5<1EuaP{oaVFpMHWiH!X&9{=#4joy_WId6Y)PEu_IukgCgxpysSQ9LJFFyo8y zipn3zf092CCdg>YR#fF%qU<`Lk?ZdMfUS!Kx?;d7h3N-7Z*+0p<(!J}{ zs?0Kxg6abE^oQecS?x@@&L8%gLv7#dE4`pEv=*#}p#w%Wwf6XObM%T_N&ffaG!p5( zMQ%?_U+256`Ojk#AdVrI9LJJ0w3t!BK3c3I(Mw<}*Ku~ssF`AyZ=7jyfQoU1>iB*1 z4u^Yf*TCnL%H|%NN4-o=XNAqQ^mEZkwy2-Z>$C?Tms&&KPpYji7u*f$8Y*pxF@5=h ze$Vy)4UsIe-<_7ZADBI2vaQY~v}IBO^8B8X+qnDXI)yY}ullq%K$(^vT@nA|H2Yy= z(WwW`=Cr-O;c8T(U4SU%i)84V11&E9S%$q1;Y0)vmHYH?$5gE0HhtHeC=qp(EoH>4HBi(dliWu z-)`Z5v&vyWWx2C48S}1!3x;eHcT?%ElZXihge-0PIFN$r=Y-yk%7s@^&ywmCYcSqo zJv=UuQhBO1KW>JZUJ2nwaeF+5tTw7TCnwo}Hd{wiMwL^*?-d}kdVMIC0CRy&B5B5J zWEw?g#DYpa63$m5+)5|6FRy^#Y2Lk(R{;Fj5~AMR%g;4M)bc8FyOE2D69u&S7jrr^ zg_Zl?tQ@X7ka@wQ=cDJWzbj7@nq=x9UVEQa+Vi{!JiiVtwf=zm6qv=CM*dFs(kqNH zg16xhqkFd*ozT{aC%yB7ahsoJQABfaL6}!MbiCbDnSFf8KY2gBB{Dw5Nw)HSAn<5Y z40ld8M=0*>*m&OotlZODth(}qH$SFLu1h5|fOXTPv&08RM7M?GB1}qTLWXN2&J3_% zcV#ZV1%P$eBdtWa*2Zwlt?v5JuX$bVg+H#4$uNHc@G}{pGTd|it1uFNy@jW>m^}JZ zR#aP64zBmNUud$K|~yp}Mq> zsWZ&zmOmn<*_d$a1IHBm+$xcGyh_^>IKAoPv71$Dez|}1`|8ersUy1 zvw+~2)CKx@KLL@-;8s*loFRTJ8*9gITSK);rP*{DZINk}UZ27N#p=fRT_GMfx2v{z zlEas`x5|ur*O9G4{tYr(e{{+cjWBKb6@=w5F&^4kd8@4{k)3(wA6m8aYvr8&BlStd}6J9|xh>p#Og{)cfI z1X-SUOaQUUy$&ZII?D0rHZtc-M#RPo7x6=!9_p1nFI#DajKA zWl7v4asLVS?Azs(741+fu}Ess@xam3$NlM)2DTF~zYU?5`ajmrDM*(tSkrCW_U`V# zZQHhO+qSLUw(Z@vZQHi}O`n*Xxt=(6kx_S5QL$oGX0A7%H&_^LHNf?2dY0D6?7utP z?>4-X->T)#b5ebDy*jb0yG&X#tkx^FERWS*I#pc0R6#N1DgGJd3jzl(WY7 z5yC49UR%KHGkE35R#*d=d3d2xBjDcKFUGt;vV+9r7)%jC<#UEOj4*-U z@;9N*AHI7|sEKF}=8RzSY#yxhpwoVW)F7jlW+KXkzv|^K#U;+1HlYDzm zsA&shlN#BGtM~26u(`SAo%#BOpKP3F8Pd(>>V0-dQYx3_nPNJhQ(WA*|3X$iag5VU zhzHJ= zJtbT3UGYC0n>_lPORi)NE6)C=F|*%$tnU18ZOJrY>(DKfsRI6f&kPHRmW5fpS%CX! zvS@qt-R)_XyT^CG5?zk*gZt=D`<8oVIA_<+_e&Ev=H^Gvuu3$j7CS#N-d<&qV&lk- z>Dhzn`dScOhGWoQ=bB&_-kljr>Or)1n0bhGnDQSb_`R0kNcl6!*9L5geU?3}ft_&8 zkVP55;Lq&p;A&Gi>Yi%#%=YbJS=J?5feykeLV1cL3$NEY8SsUEewmhJ&r9&?-UN}Z zC=n;5x2GY7a<@lLn^J>v!tXwqiNm?{2s+E&H_U=;Z<}q0k4R^e#_*SEamn+-#dv+g z_6xdn7ZayAipA!D?pvmDY~l1A6hfmt+X^@`qV>m3wZRFOe}!!#unh4K>iJxMFHcjP zHi@7+#RA6|+ptI@se)8rjednH!U=1S=>yF5mguy-x1bhOXWc8$-VNH~^!2VJGl-3T z%Ps?|)}VhzNcNj1yqpYHUw`RQe|CP-?|E(((#J;!6h@U{7x)s9-7kV&d6r$02zlud zPnZE3fr5`=z=<^O&ck!)jeHm9^3$O0T}U-5qEl-5@P^ReUtwY9 ztUYe0ph%l!n`NdI>gu1ndyMI9*DhhSajlMfa$=9TRLYy~Z%5C#FxwDY6GZV!;^n8+ z;ZgdZ+{?pvk17q`t#_cyAKW>Aw8{Y;g}k5kH&}b~sQuSaRi)QGWVX#Uu&S)Q#0YsY zEb;FxW16;3Net|;Iop00s_{$<>-0&mMGohPO#%e_TNdAcUsM$mxc;xvqjJcFHEPgV zA?2s$1glhQqboCzi2q%d%j5{5Z|xVU07qpTP^f_0kj{ak*gC*x13*=YKkHq(O^S44 zHKHYOw{Sta^CfV%r}$nh-~7+Lvb#O4)<8Ezgmig9wMDIuP^w9?{d8TRvrKeIk?m^} zx%-@AR4Sp|R;Oi~Lpb*?Ik&#aEtTQI)7v`P`Sm~AW$e-&&nYI?EWgr(@gA2>e-Ioa zn&F$A;rhSvomiJ1JKN4j5EP}*B{BHv6RS-x&yV`c=$3Q}4G8?-F1EMPnrK0iSl6|3 zx-36nd;Rqa^Iq<~z2nVpSbBS156Gffl0B_n_ioiER7|XYU(Ym;-x5QdWFRMb`nm|Z zWd?ALwnbK*wnF8AusSKnPGCB{OsnP&zdX(?=ML|C*P;71^NGyXgjNN%^f*WsFQZ#P zz7403cuG{?t^&)jO}$d6Alefnz$7E6`^yMl9A$!N6`)j&_@_WelJK>?@cgtWf?I#m z_%9}T4~#PhfEXB(1xnD}^%d;;^n*Jh*^$AK1yiDqSa;eMW_Awxs#1!wXPUY3r(RAc z|CpdFx=QPJ-Ox?(Z$!Xt1aptUJH*}^kKkyO0?`IP6;BLVW_z3vptH|QL6SjqtqfI__tbjqR9pAVQ& z7uKl&dD=?`fJ*UyQAd)4J+z%9*Qi$W;D16YRCnFsdgs|e+_bgl4zhvp9l#d~%e)#4 z$dZBF^iC{4zd*{LZF=9Y=z=UiI1mc-wCJ%N7dm$IvcESvbIcG-UF48=$`QQ$Fnn?~ zIK4By530NG1Sf$zYpOx7IwwFSH)F|^Po{nFG;BH?kdvye#975$&HZPLE1+d zMv#sV5Iv4RhSv${s*lMf@A9mlDgO;}=CEjk$beVk@nskd>-BvXudZ==hs$yJBqOz` zA`pa8MV8A7dEfZYtf;!7#X&e$du-cX8|-ijTuZ+$qjbmLIr9rv#fWybC)e$IP5>01 z<^MOl0r~+z4!VI8{{vtDf2}P382w-PI=Dj4WD9GQumk+f?}b97K?LR=zV~~JLyX;Qv59}wHwx6u$ss@oMZxWgz*uD^Hl z^P$I`=H`@Pg6;FMOr!Trbt}3`Yo#YKcu3>Opj)B;uBrmy?QQD`|U6E1n zL6{23DB}-ijZR?b23MA@OLTtV2&+w;pV7ZPH->BWa{ng}K(Tsuz&Mstfp4rq^f{hn z&@Ki9^T61VR}`#&`N*!SfE3#37iVY|@rmHqr?W9Ml?>o4lO-TCzc{X&T6|?%klB&Beef6;gw5Bo~}kYs8)??_g0M42=n4-?ok<^ zT*)RvY&fEK@fD|(H;65?9HAaMdMy1f?3|9c*ZbA!+CX%jB-a(1 zz_^?14m#$aFi+F3o7*6V!t~0g(!&UcXUYz#K`!1g7Fe;KQh`wr+oOGhBK_p%E5rq! zUjncEA2o3 zeie>=n&buhGD8_5N^QSB3qe28O$|ALlgR1Bg#WZh{A{5Ruu3QL3qtHj#cMn!@$e#{8pD>m(C56w04#stbl}1shV47VgI+ zgF5N@;#fX8?OdeQgHxb7ux+h-v%V!48sQ%tuZ)O}U>g+aYW29q)6Opn)}eWN-8FWm z_NbYqlFV+8CFuqDSw#xeWV*GgB0IcdUYwut2JH3YANl!{OYw+{Q_D(0y7pEoP9{`+ z{iWDxl1~4^HP{{G=ga=n3C^!8$VH(QxA@!Z@;#ZS6%xCcGTGyoQ2X;x@U;yCztrtp z?7T#ynR$aS0tdVq;ED0}O)t`g9@pbA+IpeSu|#JdSj&}VEbvf&?rf6xk5ss%UK9)@!Q#X88Ff76}&^7t6@Mr@4(`X0hf zr@NYOfZQ9$J#mE1BJce!MCfiECE3#~XBVK}z_VtG+bXq12k{YQ!$h(NW0iV=mTA-Y z#=DQR@K<;X8f}%Ke}PiDUN#BZ0HNmahS;DE1 zCjFFaNtZX#CDOiCx^mb)Z;0;^a;4i1`3=_(TCBil6Z|tX_Z#GToI<>UZBcJUF3Ezd z2D!(_qFfN)yc`NWd!(Q8@eQ$_95bC4q&K#GXfK~Av%Fl#+rkg`+RHY$}76s-;)o}$0-i(Qqi0sX2HqtYzK_7VJ*a%>^F7R?mL z9?YIY+yW)O3DynO9>Vo&OrfMlomI@o7vuQanWfinK!j^xi)e-25@3%e$`0jPlp)SW z@2*N6-pVPi@f2pnG#%Z&2hyVm^~khpl=29Y>5yQ2UHAdPrxqbkoT4teN+CTbZ)iD| z>egAc*cFXZ2~XGJoF+BR`r`Qxj$OnvVq(o2mQUPSF`6w$TaZJhM|5D3*b;V&Y>vD+ zEI#`?W}1Ovp$S&yhi*APIlAEvwK3ec8`ecom}WK00QDlsqd>_pb8_Ag;fD<{E2mw^ z9Ksymx4&C~!U)eG5ZHX*uMJuVQF znIu3SJ7u3sZyFg_dQNa>VD8{EsaQ)($Y8ot23Ng;=J*v7zomFV}bxx!ZgEPXpPNhWsqnC9Db zeH>m9sV2FAK*Om?dFFVxMx8*5P{WwAP0l6WIS$&vyOM9r@u!FKQCQXVQ8vpzRZ{5Z zBhJn2k+>f1pMM_;`b#6RPcOjM#1!QhWIrwF6ldo$!bZo?rK(IfMIG4qv+{)ZZ7ogI zddC-=q(FQ%Mq3a$#c31qk(%OGr(l}c$3KZ>7wzvYF}U@cRQ4&1^YuePv_GYNgIH_6 z*j2Z36p(E3Aiy_6q`*Jx^{2wuaXjIaF({XN$v42fIX&2;S18q~=i^Z6iFCTSrdEt_ zi*wW_<6aZ2-binVhj3|;eHvr`dFWH_WEx=H>>aOCU0}BXA7_qnYF0}yog{>hYL_^UUAn5P-sc2XW4c1oouTRKIQu{g8O z(r-kTqDiwye6bckl1&O-_tOnoE92SnY+!c@t0-TCltFoHQj}+%4aS6Q#w1@_IP}Uq zD#^ZO?W7Uur2pyVGQgTxkZMK3`vHGI+0n`0L*moseN zFo9iV0KfRh1fANY+p&HCpVby!JWYcCbqxZ45M&!=N=MwK%dom1M@+Z5M)&}^{?#>v z!|u2V4zrvh9j38*|IX#z&h`Gr&C=uXL7K-9n@u9N*^>?Ml8ix}Nu+S7{&P>z)TTd{ zy?x<16G#HT7B0FV?!hJb>d;V%p4nQD8bdh=0$QEpCpac9I`Pcp1plkezTfhtHG<0W z7Wv*v4}TU)fgSj(#BYSRG}#m*WgL#fpn!XYj=2VPHHcc2>BT>Akx|FaGY6@g8pAG7iul(hh%r9=CiIxD$|;2Hs6L4 zX`K`X7}*fWeF@(KyKK~-4#%4tBJL>YQsgh&6z7>9D$iQQrC>&cbD=}HcAQX{pBEQn z`c~Zoe7LKe<<-o*qAQI5RHYl@a7rq~4rso3S{gXKZ4dTuUHIV}X?|y%1qHJE5@%!T zZH&0diE_4iUGK)w2lKFfonC{3Jg@=B)Gv$?zYIo3ckm7L$rr|2LnA)7yGd2(f4M|W zh}Fn@b&4e_1rf$EQ9U4kEz3SEr47^gSdqP zyH)R|m^t%2*F8GLI7K;~fdScg8e{T0O_-LPCbv zwIQ)}*#*7;)S97toHWmX$yigHPEc|>kw zDPskj$w*LktJJn+vm#TeIiR0U zbno`PA%Z*`NuN$uK-A2Cs*`V4O%*-w}LNF zZec-oOMtKdhA#i)N&nF&A_P0woC0iP0_Z@&QGnO_gj2fCs`*)`Q^7y z=h=A}s!9s3!m7FiOvwpypr5YL^1?@C0#V_$o8#o#X~Jp(BjF_a00`r&*joJUPX~xR za+WDq$mo4WX|eW)M&~=?oON+Jxm3xOvTfo1t2n2HdArs#DvV6o7#A4NAnT(oh?%88 z|7S>^9i=!Yxhg+10_8HE0Y33RSgsKE=;5_Qy35l{akeO{GGYF$L2{h!-7O-{p&Q~) zPp?leh)!>pDEB6SDgargTY}y;o<%?0fK|Ur2He*=b7jXqt4(QxjBN`WO{S525$4y0 zPsD;~-SK?^vWWh%Cn-yJ+zdO)+dkkfd9loH~*xn;-;C(!T~ zQ-c=q<{nzC;0f;#{t?>?lVh_jB>zdR_a07QF9l_UjcP`r@){}D6ukLv-beB;Nl?{l zl!4br)a&Nv(_`sTXBR&SDh*ZzxmMN5PSKUg`I&n6w>oM&$-FWM>N~P{Z__7Mmn1k$ zh%w*b3tIn-OCHqQA-Oyy({y7n#vjnn?k~)Rb4H@SI^62JDuQwk;2A^L6ABK1ar|IkFrQAiNdF~+p;O{Oc{V3%x= z7}pu{a(Tx(0r92U)aF+zrPvCzf}+m6@C|i{%s948T5PfiQsBntJ5FA5-Eo_Cg3dMTQ>RZqU$Jm@ zhJgR2%hfkr4bL;-0_F|Mu-k)hDatzl11a&4bKyWeZ^2tK^cW+WAwwT3%nSo8DT&VFtDdGs_80?QXw1+(*)Av zJ<1CZ?-;EA@$7vX<%7FxkL>b4=m8Gbn6twYoo!K2Uyd-e3liOjyAHAA3!q*^hnQcc z3e(KPLy%84F@8z(Kii~;W_uLgAxb5%Zro$FNqaPF)B^l$6D~nD8Yb!9Fby(XGvciU zNovKL1fXtPr1C@vV8vPeKUQew+0==JneWbD$}B_)IHq3bsMYfMd*@Yn3L# zKD}j|Q1Jav&598H@Ce2WwKa5>p)9&nK(IoNI?ZfqkwV@%DZ@CxCD-ljn~=IzlV&`z zq)GzPahv7$Q$MygeVJeh^Z0_qzI((Z8BNR+?h7xhvN&Os^3+XROgh zIj!)a9JhsY??icN)n8Lc|EpF`uY2VMdmE%Il5p-o9@d%f6nNFE6>L#gAF-S%$R}cC zJA5*fb5$Y+JbQI(qWD8zA}vO!L@NyDaVmuJOeA`(W5)8@ER-nyHwF5fwBR1sv#{&| znQWuXA#4MM=unnH1sq817p3Z{by`fwcK=XvqR8c3=`(_vMp`yAqZqVcbPoPWghXr0C>kYyP0 zaEuwslhx-lh4iXGkZ9#H=o)^K6nX`hy#Ja0s=NP@J_}a3eWoPFUF*7+z25rp+0lSr zqKqSJ4;S-nufVT7t`tPLIY#487@vO!#&toaXxk?i$iu|;^=2oiNq|xh1e`?bIe|`U zpzmGYp*=drnV;9ML4i4HRpwD9S)dCm6>2fc#lhi!g6;ED zA7hCPs-OgD5yiG^6y2IOA;iA;^8QtE)*S4jJ3f7Sj;j;oZw@`U%qFK%?c3hD7+69fGD(NEuS_aYp;dXEpOKDg!+YZnR;j+?QkJ>OssK_>~->N-vUsOwXlg3QzIQM@Afg}pHSeAX$` zJnW8>GrTNECEid-OoM(0Q>TF9fpeFiYOSrAhhSpWA%NM^Z6{d`5JXvl*{+tM*h$qhYQULVWdEz$g%POmDUB9g%wrotofthGTA@Gwrsog`Wce3>9}dGOj4Lqh8n*{kQiP(3%fnor0ED;yBjY~O=&!z}R?YLNml zCtpvZzdm|xhJLXj?f~`8`$Op+#mb&n=uTL~E95`6365pfA#z@}Hrh-7_vfCkF3&NH zXt7j>b9#Vh>OZ64B_h96W87%FhZDX5%Eo+tybj+6W5kr-V~bJVQH4jFkeW zzYe`@5@+P+6hi!dOfQbi(jdPwAVWQkQgiDoLHvS7_@5Pfq^i^$3idG;DMa^T!qu67 z5;R^En!Ek>3DE8Z_u$_FMZMAPA>&khW7E7ycbN*w+%daI?dA0g*US#7 z1S^U5*3j{}If_+6XsZd4&DnRkkDEm@sV0tpTQ^;T45M7p`S6+)!z2XSRCGhAM zQ3&B2=Au~|(`u9|Q4i}~p!2kUNry1-!;T*=Q8EFp6E!Y#D5JYG!*#!#2ky}=-yPXL z$a!QR+qpsWp{@TA?rIe$m~hGJ6Ld^fnauxmNX~Z{U_<)3#I}Ot7a|^vcK*Z#vh#sO zy@+)=b`8EG7P5&n3U@ef{hEMX3BF(n(>>Km(bcC#NOEsn(Acd%i93oF zYrk^;MhY!FYs24=v<`NBb{ATf-1_|6!^ONquBv+4S!HUK5o{k zkoaQk5s=&TV&?ii!{%hPOT9AvI+wSZ65AhDim|^TTsVyX=%@R4V zM`RuUz{~Xoh+MrQSFj!y0?~hoW@zQ{s|ISC}rzbnC zdvpN*Q{z&kwEE_s&~*hw)=6tLh)zqt9kSbFH+Z64Z{MfV6{4r*8s#~N$3H1e{I9bl z%W$?`&#VWn=-MLPG1`;?G9R8l+C!){WL9SwN6~HE{+j?Ch)1xG7hs+hr{J&i8(q5? z+8yK{pU@@7kpL*`UR|_{9X2~e=_<li)c;$8U7CmL7}?P|z!k!Xwuaeeh_+vJAMli#v41M#PhV2pb*isH$Zx;@G`kq*lP>eTVr|*B{r6O( z0E_kXj=!}?DPCZfGj@CpOs7tGg@=Z?ncEBuzErMKYaI+}Q7S0(ssCnaBy*Do($gVzU_kLka7ux{mJ2$=&+8WeL z{V$cucR`+cX7T7so6Hu;9@PewMOv=e0vW*#ylXbx0m4JxDVxAryj1Oyc^zc!5cmW7 z0shIpg!al^G^-8DJR6pwwq`gp_Iq=~8v>ozx)`^J$Lq4Z1%3kR07byuY*^xBrH? z$NM%sv0ac2;f$hc-&I4ljCzN1y+rQYCS6Il0(m*Zt4wZ^f%`E?zt`+C za}1MD6kN@_)%!PQq0$s|8*+UJQf>Z##x6fk`|=N{&pmsN@PQ4x0~ps1Ch1jb?|WtC zx9wjTYw$<3s~aRpPiFmZ;{bn+xB>y|m}T>S;%C@iZPs6$?@ade77mwft~Q(H;%Fbp zseu^xj!~p4@eK19ez~~ys3zSuOS%&oyN9h(JcA{<0d3B$hqtF+miW(Ipl5yq`pT6r z56=(q&ozHJf?*vi673Q$PKb0Ezm2k?{P&WqQnsct&B0t_9ibowcT%eug)_|V8QHWk z9|UU+krvQ)9u*ymHrON6Q>!>2+mjHFYwPu^L}y=_6%!Uki)uG zqDQhrdguxC=W3IiTc}L#-DH7RE#spY)x{+{#}F2XZywHEmhca-Mu@NbtAliZf_uY9 zw@q{1oyrM!&yl=ET-dsX1^DHET(8q%dH3vi-~!#&q}pOYyNmEgysu5V2E_RVxSbsu zXS+dP5@4OtZG<>oVev`7UrhW7u}yG%LP@QAeBPlmYX>>B=mh%gV~x>Srtp36ybGVz zdP&?|!~gopC6lJtKd1HB9evmJLdE=bre9hOF8gd>-h8#4MhlfQe9khO{_V=|vmTj{ z4t0B++6+QS{=K#Tb)duZF``vdRIMuRcw*W|RFxvz5T~5;xAc8}Orsv$*c84t4&)K< z;1&?+k*L4gg9xNi<_RvlgkeAILTJjMBiQEJ<4 z*N~^(kaQJ~e3HP=Grp@-?)?|R?;C4;SGW5JBh2U?qgu};vqw0L~4*OYmdRztWpOAVIg|en-{Lh z#Zgx?pi27U+$~U>Qeh76QpGp&;#BRg(Zeu8AjUg$y-XEU7SyYv(@;*-EA)#k$mm$F z-X+i1^@HN)2-xa2HX2?HK zch-R;6sD<$=zTnaE+Nj(GQSw!lahF6Qu>u^hHc7c^rOcwYu}q=O>!-=f}B~Fm9e>R zreT|0lmklTbM&yD!QJ|B;jZLIx~+BMdl;}osGAP0C=*c%{;fs@+7%d2adzhZVKU>0 zVUh=wRFei3&%b?>d0tu<-Z?T2TB%m){KFx(M|sY3gWQ8o2x0CjwGpih{QT81Fcl7bLvECDaqxZ%uD$U7vMaywnim8mQ zLrtm=6cImu+dP?`l$V=p5SDOgJgrVI$5>(SGzF~^!n|}>Q~$xquARdC{{j|DHWwpxzG58 zM*WAg^3xj!@ckjm((Ve6Z@PtMdc50=+U4c5Cdmy7N|d^r6?J?iE>K$boQe|)T!k8KvswIv+i`3$vxA7xS`L`_uFCXySz(GBs z*#+MgO_4xExEcW+6%MfK4swmHRU^(qsE2uuWp0RGm7Ax6dmvu>pUM8KQd8^-MHI`( z_qL!zWSu{MR_bMlx9n06Pi%2pBpsrHdWASp?Wnb%cend_!5+rRZ}he05+0v$+I!N8 z@QHF3>3oxYTLA9Z53a{iR295F6$fgJrv~Yluy8|*x<^=9YX5ykfRbfaxD~XDQuh9g z`Aj!Qne=`oOe!Z@m=K&{U>4T;^SV3T>vqG{<9DKeYZJQ0YZAD{5a`#a_W+h*cJXSg z)gDn+F5a0gTe!9fdZo>Q_uVotcCOP6f55YC5bv9P0sfMRZr|=LAUJl~=H1MPSdUum z8`G~}mQJnu+o*m1Da)%n{W#=Fv@s*B$h*7^?^OQp-Ui?l>Iw4-v>gQEdN_pwe&2%1 zG0*mC@fjg`AU^cTog6|$zhvF?^q&J0jx;%ptQ$OxA|utv6h+Iq!cUoo5hGPQ>ag|R%UT731QjJz&f!ux$a<6 z`8W%LyGs3ME=%f6zYa{QV>1C;=+k9ooTBqnm z?%^)*2Wz3lB$;JU)mglAx$>CczS1#1%{fF|pqAl54US^eu{Kgo9hE-H^nYp~ISRGE z1A|7GG6o)9DB|JQKl8-^7%OWN0nh3$nyJiV4GOt*y5LeJ~>yQst?#5 z0Tn$Kq-V)SnhBevXW(u{ZQZwzZ-+IHaOM^m5$_$BWI;TtQFuZ=pw@riH_kri+nrfI#~qD47xWskEQZh@=*UUw0oI*F~B-)8RBlw$02M zsXutUuV;wsu|C>u46pB0D7%1ncH5PR36_=mnj{UgMHd{=wEEmt0O%Y17RM_O@%aSu z6$bGq(1lplHO-@?VAT6 zUY;U7R`!RAu!sOj1h;3RbBGFK+-G*7RVI`h&31Od8M-nd!KPM$Bu4@;M-L2hfscKa zX$9m;wM4FpX&Kr6J-e(eQkwpFT*zy1v-ar@W!lR(>tBGgW0tkYNF}dN!M|yd8K%L! z@JGK&JK~)p2ELA{$mIGUZ)q+1J&eh<2(G;BCGo7Y-e44J&f8$lz7iG7P$Jx<;u7;D5*1n%{KUJ+KcCVhlBSMd>E9+)ey@G%a% zB#988KYZJ4RFfQb{<-eq?Ip^O9i)Fyy(IbV{LQ~`KFPvsuRr+a1pXW(@M?akC`+M{ zN;R(9uPd@~dX_@9E*8zs5cl9xcL@7WX}TFkQJy{#tL)k+v-Hm0XN+HzM=-a@0%g0u zBj6M8*j?%OV!}MHH;(dZF*9@e9^M8h%`mXfmg0KUe8jMdF-t&q*JnpO4zj9{vjDbE zH405SrnrA_kCda^EU#Lv@9g3S9a!6FXd$|ZN#g1L`Oysn&}WmxH)Mtr@s?ydz-5~9 zYpKw~KCs|AS7%Z$gk%#|y^n<`>%;;Rd$$-y{kA$0hu58v{Vz zqH+ee4p46^jb0r!hdo{QYf&_Z3b#Pq=!aM&pvH!o?3BbwmJ*@WS>xn{$}#_ zB+%U^v_!MfAMaF}VVZ$_#4%8+Q=0Zn%&)si{e~viRwQEs`nfyr4#PTq2krLi=M@qd z(8&_%qvt;(dVMtE3!F=_##oA7$B#sXv4cgFn=5$scUOPNN(%U<8a z0)^=bktA~oMzv&F+7msh{loo1*rQa(@XFKws)$U#DBBFnqfNhdj5bfiJXWLED3jPU z%Ocu>^MmKznO7t;&$mDd^ujcq?rRVdX<(c50<_K7sMHgmY|F4hE-nsGZkXsb_m9$y z9s+$bOVmY~VjV8c$}<0CkGM9bOD_M*gotYz`A;lUe0`2CtnHUbvWJFGp_Dcl%+RU{?+@&1@Wwm{T3JvY|^6V}Nd@c+L2;-4DgL)yqI7U8z4Soipb(VFygTwr(k6-5! zSBM9s{$Ld;VE(pUhv3`9m(aPU_j(WFpUm+)BWRd|YMdg`8>>cXqqx`9O0mNr+R1Yr zZoL>UkUN$=8>bNMGhdM^j~`W~T!L&`^lt}-I$9!wzO_Gce;WUF!?kLp(GCcJK$9i> zZ-yUz=>oNWwt^KvOp8oS8UJS=BJ?3k;0qvx&;0jqiV^1_9N1yB?k@(wBX}>CRj5#@ zzxG#FFGm;H$LMFWl86(|9v9a0kf zGpp+~AikiUtE144bvfL!w5k*Z+TUi^9-ST?;2XgHkFFp- zID6{>r+k|q?4erk+#DakFAzL4+oVfWb2D5a9usU5yweOL>yT{&lc&8Z81`e!Y-2v! zf}FVfb2LuSlY9!*11w24X6cC~qdYqIE0cAaAujjOXy5s&7JnTun-pX#QXHS$?Hj~C zC0>A$kVfyM*EDcHCE6<)A+ER33puP)jQQ)F-^3bdx{=9W1tJLtXjzosE`bSX2k4(O z!mEz-bS3>;qwF!U%wArOZxU<7c)qLu=v$^BIMSNxsF1MH^>VTvlylNYiW!Ce8DWSLZ6^IA-r9P ze`5gO$QbjzuHVBnLARJM*p*hLiItB*aVxIi{1oRq(Vd;vZ&d~<#QegUq=Um}h1P5RR!_=C_0-T~c58qf>; zWh)#|1HLhS%J^bPUt!kfl_5rX;?eQWt>M=Q@AUlO?(mXGd*|l(3Y7cY1@p%J#-(3L zpR^-1FH?gwFLUfa#sc&ub;&1Il&+2RB%8HIW*ZW$WZ5)^HqGT%f$||ZtUcCAr>B1_ z^?uspq}pFwC|kKJhFxIq(K&)8^>+wo63zJ^_y+5vm8n2pd09Up-e8XK{%nQW2Dk(J z`6%%6h2Dksu=NRgxi;BAUU*vwCu3|EzzftjV1^~A=@@ArC)*C~ic7p9PQTUweRe(E z<3kt($=HaG@6}fudB1N}lJONb(h$yil*thE7Nj<-EFAZHFB;-O67dy@`umLt_i38k zj<75(Pqa`PY7y%DP1?6y`&FP>xGR=33T;tWrlnv0qchzFwLb0WG4CJUUWzo(U$!h! z6dKbi;T(U76zZ>pxfxkj&zmX*KI!)8j2i1)w7tM5Y4%TofyG_A30jMk#8&Cz^-1}1 z)@dx~)^P4^$}!q?nyq^RqtpjLjChwW)@N17+NMr7m^BUmI7UPo_DGx8I^@_X_6XDp z5F?Pccf>OY!Znh2mSK!1*4}w9DWW^#-s-zWsR4aO**qeYI`qmKgr=X-RTF~a3$PLN zHQ6}IqREn@pX+?MjVPDPR3=-9GZ_nv9mjEQN zCV6d|2@atLntMk9#G-mXATw*)t5 z(EHPFpC&Nu;Z*3K9^Jdt=$-8q zsY>6);YCS~3Z*+dx$f1e1zPgY4^)`P2GPyAea79@XWTxAcF#^x46Ml+F;mG8U8xuFR;bT0ROHUWISKMHxC(pJ}O>YAES-zbl zrm`&gnN~<|5BpTT1e^=MFqUCb-PB6#zl@^%Ok|s?BM1J{1G8NPDuD9#zaIKN8lYW&nZOb3JbrA7K@T2vFOnnMsC zj*psyJb~%t`c`K6i{d-Bvr+qk@-o7HIb_VSh%|f>C7DhScgFw2+BpSR60Ti1$;6pV zY}>YN+n(6Y#I|kQwr$(C?VSBz{5R+N?CY-DRbACzzrEhIo+ZE^zHM;L83ff5$~v@; zonQAS59iq)x`jUKQvcsOQKrK5e$9dmb}mdyDa!cbybN|e@N(TSTAiq^UT_^wB~Xld zM#{S>q)?@H*IGNMC6-$-&qYpBKbg$kJ8r2YHt8v zNj9q-qRrlQ-zJa9OW4Z$G<_L{|gX`N2-9gRXbsq-e;8D)aduhKmY z8KOmAHA5`rtk`3;VoMwM>Q0nX`$uHG!aJf_hZNOpWjZVWpOykmhSf3r-qpnso7^`f zc&qAnNDB(YX?35ALK9;@P5e{&&ghAfbVyp3P?wi!Dfntg+5;bGn}kowt9|(C!LQN< zwfxPACs0Z)8VxxXqD?`bDOPm~fM%ko2WY8Js7YvnxkHv;O0t%(jdFMf#&$)zL$>|( zevBSu4hj!b>q75JcIr|a3xJ`1o&9icv1@@^YIjWzT>kQPo4H5w_`p2=y$2}o#o4`* zZ(*AqCG+(F z^?>>5f~UeQ+B-jqyn|Fh&Zis$4{xa6wj3%MBiMeK?nxxQiFZK;wB!BN?3l-o5e}{% zww}kY9(Ej%K3$Y*hA}&)NWwJ1&U3kaaJ}q0zS1N{v4yutDr1o((=E=HWks@%FAM6d zNb+l6dd>ei1Bte-Re?NHtBC!lNQQSrJ?K>zuE99$O_rlhw4op7;Kw<%DdokVMY6K1 zAX?#eiWM-K&NgO(3oLOAw~PJ7Vv9MH+28K=!Z05-jxq~Z?@V?Iz35uY4MKXFC<(KK zIVh5DZ8(7}qcjaKoIZhkP{rIWyzy*L8&8=$Cry!f(>1)2oB8z2BBMe2e9tBN_U?_Q zL?%Xda`QdI9oaCnF|;Jb2JASs1T%VaIufQz@0w>@YO)Fri~;rij&P0$waey{XQSZ_ zQVF;E7Nq&F{RUqxG*uJ`MWEF^ETs<*|K^$ih&8x7FAH=rnm|L4H!qV!{nsGUa-J4> zeNa!#4sX0$nmw|-xgrc09V)G)YRG`rM}_$RZF&=R=aE$)TIFYGasoX4F-vtylJur^=|q|eyh zORT?CN|6ny<}RVRnOMcX z?pK6pJO_S;3b`$!7myk);L!c-?o>3N%D~162~`@5VF)b=uyWVX>8uE}5r})j4ckK49aN8J~SlF6MFu6pZUZBy^ zsU)1$eGsj>ZBPxBs*Ez1EQ>cE?;xxlp)bKGw|@tMizlgY3z#IE&3Dc6{0s9OSKW7V z``d_s);CkG*d?-$a+?;~(iF+nZ*oVe8hVeqUm%ZL?c?Ty&lPSORgP@~UykjH*areX znJ6<4M^R2`DwaBq7B{cE-4K%?hfKr0E2KlBGqP=Bj&2ov^lc5|l?z?$Y$PothhOhT zK@Q-b%2|jpvQ?V8bwQv*qAS4DzuXa$p9Wcm+M^WXd$?tQUYgvQF{Jshw-4 z5>{sNs8NzxhCC%@wFmylcHU$NcQi{NKdv?pH}*H~hjr>Y%xoJyXRzEco`#ajGd#j8h#45m?G`tQ7un20Wiv+6!zp3S|WB98RJ<#Lx zMzFR8njHg|7YS?AzE0`FOc5f99AJT5I! z5USlsd7YcTJ|Hi+Rfx-V=!$#`bzvWWJ%^6<0wPQ|dgbUeV&9|0XLEsqa z_tq|6rsN&4OGJ1(yt&Kq0y;{q0f0$&f0&hgv2$GFogt0zuKhi;y|Evm&QX1`To7%4 z$&)1;T*ln8LzW&$^1|I(@r#bKLOOnB(#^^MJh_8BOTRb4@ZtXA5UK}sfoeZ+To~Yz zYV!ztl8$gck;9u(qde^_YlWC5s2KYuUSf2c0LaGX7&M0=e z1L@3Yu!XQpoMW#_nPA)_gPBIv2WTz1c)omzRPu7aytg{lN&4bzkj~TsT8-TNkI>mYtaZ01d(qQ*zEmy`;>#PNCqAJqjIu_U2$p~G;LS;O|2WCj9$sRV z)Fka)O0h0XU>Z35&dL4t+0VH-B;|FQ(Y4}>IX>4cS%h~VqW;40&JpWf=52}W2&at? z<9vL>|BkY6nE3r|iB=;(x-iN!xbpIWjth(nJioI9qZxR&GLTtZwgL9cpc&MUBb3#L z3(Euu6EDNdalPv4OevUoVwh|X%QDs^H${L5*MUy-Y~)xRBPP2b+OfJjH%b3yKLd8~ zeg=*&l2TtJ^8HE`rGG9nQ4dK4VEuvD@O$3}CP6KIv;4h$%|VYbpF&;YO~TEBN-`SY z98>I(k1=DP2r_|XV>4RiTRL@S;=s>VDEFkk+$#Qj%koyuBtyv>oZKopuh1_p6h;h8 z>M^^ue|6rjwAcY@(BvLFO=Y-`86{#+@;k2~EAiv1SIg&4FL;HzKuv5s6(2_gJ3vjc zoFW~VvBA9#VD=%Eiur0UA(ZE&30U?_erT8zU`{Gz|(nH zZn6d@V>q8g(zPiNr)8wC7vaVahYl)gHJiVHF$f=O#ehD5-UZ-X!_N@Tq2nA&ZCnyc z4KdVe42GAe4w!o_inEv{f6$p^t`PIJ=ZQ6GJ|I;~i&aOtCjmXJN7(N#uWTZ}*k68? zll`j4E(a*AOSTJ(`1p)XVCSJFv;@E7*Y=wfLhVJX0InKekUYYb6<-KsTy_Png%=0eOu%z+g{r1gIH z_Q5H{<>%$EA30++Tp8n!rl|++86lJCf83+!ap9AQf{emtf$@rp;zgse@bMbTgp9#P zxG`yyfU7)vx~A(NyDvVvcRs4Wc63*l9r<3mYrJ$;8f@5?_KNkXsfR3rFi(*y59pB? zWgR2Q)xw>`S&dc&#!T{Pm+ebR`7Tb*Yvg0AJpv2WC+8ca*~M5TrPxSU=_lzYGvBf; zrg_UVW>||=UG9*+ubi&0L`n$9j_w7rN$S~=&p)XyWLr5t*U*a%jcmW37-GY9~0IJoVhW zrxz&kMsUqw*qoSQ276i7e%&N}{ut!8Za1&=YCo$;2?Cs(m#yP1B!8X1su8Lt2-r626-fDDdZl<3rSDj|NT$yB{m#0-wfvQI)RJbk* z$|b_7H*ke!lokBNSDp^h2FW_i$u9EtrC%u!i`eZWs6CdvoFtF-6~;~!2&qzCrtP=u zFWK79F$qr5ns_~rj*t>Jw>=pkg zFW6UNd+077Rqt&22&)Qg0>ivE#ZSO6pA=6!Vw!#wO3b`r<;igQP zy(w4!ZkJ|lm8er8-q0##mf9c{YFR2ITzPds(rR?FuJxpNAf4?;K-<;XbKf$al!Dc02*F~i*^$|@J(*vn0^M65=(B-}>541N*aA=f6|Jj_A8 zM7#?7JIAm|IO1=EHqHp-7Pb$aT___gAx{0%VFf*m;WJ+?n3d6S8TvOUQs%&5#&>u9 z(_HjmhNDyOeo{G-byYmz*=nWH`?yc`wyES91BZsX7-70SCFq;R)QJ3*895ziZh}mO zKr44`WKfor^$r#3QT{JQK9BY69XH#KOb_tq-qq%u8|d`8z37AMRBG9u#&P$i5a>n; z6-w2QkZ<LF0s0YGavD9Y@RZ#b4C}21kS3%f0mw4EAjx*8UdO^Z> zzsVOvn|Iu<(7m+vnsI|ZU0kx4=|7IC?v}A?0y=4ZLY;Jp7O1~fX_bu95iLLdgIo>F2B9@2XD+ps|iY($F%e2iNPM`5qx0mFN)+>XuW;m4jP>ap9nWn zt-QKz@>u7Vt5URtKU(vzYHYkY_@-88CRSF+b#jpoRmf6IP!CC#pI`mdbD2vX+N#im<2K)9`D_g5uQ^VKv?Ly!t4WHt;nIaMBtD9S;>plfzCkJFR!na(N~YW zg#rW#)wXwB0xP2}|FPu43v-k7A-U_G9>hzHs!>*xWYtQI%C~C(hA#C?j4r07M-0ez zh6Ou_d)d_JN{Sf>57b?(6@T`;=(3>x35hykvUgphFtIETVZSQx8qz7RIVb0@V{@Lx zTh>YY8!ZCU@4oEzw_YF#)&VHt2KoFtiOiaclo6(09!RBh-R*`BWU9yoYeO<`p(6g9 z2|UuYCKzK(AP$_I9|vbA$I1gmnf)qHFG8GxOD<4PulOlM8u3IxZ~|-Kw`~tU+zb8; z&>H3&r8Efr3h;$g?Eq)BI*pcGJunycYfnNCv8LTiTQF$H(YfzkNtS6UFwX9&43gym z2A~?1dYZmYoTK~(G0vb~K(R8-rd+jHo@h~}oOiHD!}G$p_|)iRD%YZNciKiBtud$)%M@mdNFaoBf7Y1Dn%6V+%sLBpDebY~rP80X(*yq6A^h zpI#5nchC8mPSB0viRHO3LaRe>Z$WJY3KfT*UN#0|PdQsvs-QM6YdQhg-`mO$F)8|j z-YzLNvPJn4l=B+-B-1vjj(9HzB>q`Eof0OWTj(JfHOJ;GlAHbUmDSx7E3o(LgbuJT zP`$i|H$g9I-OKC?w0~C7_NFO|Akjk5rAa_F3j=yy9>CPo4qoJ%u<3c15J95&9btjr zR|#UxW6cb*6U}=kAA5mw5sa+t#9D@}_qp@Di_neMxs&72PX%MI-`nVl*3rf?G6-kN3f?rOAp@bC5$8 zW5?15z}X9Y;wxcnBdg(!h3Vbd9zI|}!nrhx*E}*TK3+m~>6WF!zfN*>pPwTTq8Ioe zTY55A$oJ60Trv&EnNe+Vj3OEU2KHXyYb?Ryy-PO!1rBiX!y!4bC%v6pD7(mI+C%K_ zpF)lMe$(hUTtAz>2fg=yVa@KKhF1Zd)7~H6qj`FYw%hw~E@Yu8W(s(}qlqy>sq($3qrOSbVN<^~M#{V6RvhNOFESO?vmP#5kO zl#koDtb+Hi&g;C1#DmJ9^{af}uQOAF5K=8eyz(RigS6rwcAcU@?lUC%QJQ&!Y{@34 z7Y)9fv>Q9Q7!}1xp$u`K>g_`>}Tcp~B+XU#w5Q}iFdaViG$sVy@%GI)^mI6zN42jQyLjD>h zZ<88CQOn(khj@PCZ=(3QMAFXXW_-9wRMLz_cqZ9vmX|5KKfyBZuWPN?9Z37=#l%9P z5<@HBs@&^(rw5U}_+;W9Csq`wmnE@HcfLHnKm}w%qYzs}l!_3_#TXrqoQ=TgZU*b_ zVR}DSuaF0}ldVcKj4dZrn_>VH`a?cPHt{{+OmpA#GO`R@lD%O4t&h~dr*V0y53S_|v&S;cy2QX&3CC zbei|R$h^G?aCdop**^fmoxSM|I^T*4RZwB*KAu$L#)upAtiMTSIOpBMLHr9dnxu}f z93m2pnV&}pg7pMiat(IZ6@Y|kM!}`ucJNy8nuw-=E;bPc>0e7^s>uX1(3#$j3PPMM z|N5uMr0lDy=j`~)_mGW^#*^y0wjD#9B>S*UZD?sSx`DXcC6p4u^C^zMsb8ga3@QgAy zi8PAy^b0gE6UFK)6_YL>qkwkAxz{Q4tUkPB&FC-N6GN}wyQ(=pnV$9JNU*@&*Gqf5 zsMWtsx_)V23&>L|Q7ccxJn1HfDe*B$Ki_H+(|R$FOR|jo9nz9ns6r~jB1`-F6lb;R z_Af)Sp-8H+ju#{%PbtXs&JkxK(n7LilJa;ZUvLWonSP4M32YZ#Gf2^YkFj)1M7riJ z747;&qJD4l^rA#jmbxs6!4+-AUZR@=LWwfF_`qnP4@xia3>EW*n*rM1K7uQ}0O2vP z>GkO8L^*yw1#_~zK)@=;=1`k2EmT%`68uKg#r2yFcJ2c%pj*|)JUIQo?e1Fy`V;Th6Z6y zwbHZG0{}c2kS?M6!+|6{1ne!n%oM#R-vOfi-~jHfGKB`o*@ zRhnmwGVG%ynuxYd@}ySCHKN#lyb03zlzOzAm$@e-Iul+ppNmuzd~NHmc`JisICS+ZL63$PsBz&ZC+^ zy8L$V&c-yAsRf1r+RhdSg#00yW3^cYq&gQNvYSlVknSL|FC`%ny98?-vO(t-F=g^)bzrNW=a=WWnV_MQJH_N#FKt1QF&IODV zo#zTzAs*bc3lnQ5RDJOZ)Zaa)o7hJQR(>tHf^+QZaa7YS*xe}=GK~#qTwoK0E%#-M zJ*kmbA_cW;5?}KHzR*~|x0P~yu>%OJfO4>E5Kc|T#7)mdmhWs$Wf*z&b}>qm^vJ%P z8MmKL%5=Y#`1)*55auUp6aX);jEj}+bz*emEjOJ)=e-8kTqmNXc!gz#X|V6TEtQy9 zSj0Y?u1@Z*g6|N=US3TqJ~6;Z3a?ZM#vRaO8=s`HPWi9(0(1g-0YoMhYfQA_m6Kvj zw3wmND^sVVR9h$CIR@z9am5jjHU_}z?4Loj5Lg1;Q&UZWXZS;2SI=G%6O zARo>X6=;k1R4F`I9RT}=>*l>voi>cNQ)}T5;-4-DGI?%?*}LcDp~W*dwYQo)SWPEO$^RX_6PFW z3Jum3>`!zVg=D^U5qi7QCctW*MqiEHli-n)#aLikiin1i9M5N^{gS8}eo2 zWV=UI_Nobyo?9SU7q+#vl*+yYAzT>1Y(wjgk-Imnl5$KAk*{xkS(^uyLfTNQ)`tFD zxCEjaBTsMd^Dh<5#p~}p)Jv1w`jz$hoPNkm>DK&g@|XUMYin`KE^3Rp|Zdsl4 z@z(!EwjMN)nL|6g=EYYa|L`VQ;g+#l60sC+Vva|?X~?BKh$0eP$7FwM-jWMlp6?qbXI^{_ngjM^R=ed@&+ql}*o}zWr?=({Foar%=5y z?CdD1;uxJ>)HQUFy|c~xK1EAC-?`H|@>i<|v1TCG#UZL$r++ZV)qW(?$SSS(KIZs> z5)A~8Zuwbjw0XOMOBBeQYJ_m{UCxuliqKYFt=Y+)s|ct5QuGSgDo_z+HgG#f8EH1~ zndqvDl@T<{?uKGH=Us+Tpku0&PJ5iOVFtXPNqW96@>y=y4gvch>iK&Q_qJH}?E#?G zdZb9xFnxW%!@ES?F$_9Ergn}p-U4LAyV)SuH8DYIkg!QHN$59Xl9hS<-pvK(_p4uz zZDfGzT*St%m3FQ{svoXy_50}_FKu76+eMDe>VdDRYhWEiohYhlilP1aNlAp0qyH8K zYU<^PRO<)R?}tV{SqP8&XTCPRp+i)Y*k9#cI@oU(S(!CIBT^tdN#h=FyuiNN@icu% zR15rEIe(M%iPkbpT7+(UmLNiA@77=lmXI&cuqEpJ1;d?APjF>=p;!kq4k|ldqvmUD zc)L>cg_@UXT0_Lzz4shr)mnBBAZQWL_@?F%E+g$O54yWhcN1*RQ5Nx;2947^kG?*r zq?NFUe?A^uL+2>pFVujl_HhX=3)X^LKNMwr_8n4Z6RD0Y!8(f43)4%Jdk4stgM$E* zANA~1So6RJ5L3Q;nK;B4_z~l!r~YG0e;>$4;K%SSA1xFBRLSXc{>?gxvOV}pO zlaNit_$F9{+kQu@_ggnGf2{g^7m4{#-5{+6^MDXz(XZjX$s1HBn5~U$kp`xL-D0=8 z3!tc$#hL-*#VRB1is*Z(P=a>cx&8zHV>h^7nC^S`@qzpE1L8@TOWz)(<8^|rljlE% zYOyi=t0LV5JloigLX0av_OAJ|pzVDk5s5mhUsC0w5Q*B;Yciq!$$ty5q@%5ggK|}* z?def0_rAKCbNbo|Fx_3AXjzdL4@tG|x9%sxGfPPHrfuCR!1)Jm)1yvOqqsf*a~o$i zM&I^SuaNGnoiB36SF+YtPMAds;sw-Yq!Wh< z&&m(p+t4Uw#UpUgO?S_kL_RsM|0o9!25&Un-`1&#Hoqu}`R&?h|x;h5f2sDxG5F>q+`F1NFStPe1cG|{H@HIu*b9ia7X1T*a*ngQefi_i z5%T_iaslny9ASb1>*(Jc|5v4uWXlW>>w29sTKJ1MPadEnpBkjd7u`p#{9JF8tA7Yy zv2%T>_qx&98sHh$qw#rlf=2gX|LSerC5>)^01GyYFr%97;D_Mk+d!Z86A8C3mJ|sd z1C7wZZq=hsP#ZHv|CaH(?gPfXQ@t3JG!9R4SVDo8etwGTm=Rr*`@z>^kpg^ywtO%b zxEO?ku>-^}sFsaEE5y!U2i9L+b+wW8$`gJ=HHz(>?nUKd3Ow<|dYZZ$EOU z@Sb3a!=1X6&`CA+WZ6+xhfC2!Hc%|YSJ=YX!cNT9#^CDukO=4dn1?n1B%cksO^T&L zbs07?t)H7^-6Fwm)Xc*iI_@sAdHW+C(hbT#m8ms+n0LqT;1YomYOx~W1};J4B@vGe zlqfoax2oB1yaTtN(FV;XcS$c*`_xDYlhR6g*C0I4bw91F6=hqbA;5wNocDxq26uM@2A;~rxrU5oUB487e6 zR75!)08~6|N3_T?c=3dcX_62qD9D0wUgzNut_M{dQldOIxc|c?AoKC+rXQDW8r--+ z*hl|*)Xm!LsPOVskae+-TnN~#C1TJ6{(3M=#x_c*>H6#=3T*k=(765NPc6YUY>4g% z5#@-hR-{$=V^xK+6M!o9xXcV{_=j;){o@LxSYf3p%>cDU>UjneA`f{j4YCnxZ5c@?RM zR-v483@y-%Q|)0(H1B`w=EhY#KmqNH^n}HvlqTzL@;*KI0HxZWuOaOky)N7=^DfI2xKXBQgEoZ=gf0;Jak!;%swc;RowX!jwj% z{%?-0mx>44WjSY)0RR8GG{X1Q2e#qH)V%nt8sXll$BVN!yX5)z^TWkH#24OzPaueo zLZuZ_l+&MQix96 z$i$DLg{U(%&cMsW!_G7OxjAun^Kk3(H@y1(al8ez@G6%`bS&B@CR#R# z)@93)#Q2a;IP58rtJR@1!ZOVi=(fU=YhTBIfPUwg=Y2=*mbvX{9-Ki~#+O@Yq~tgc z`eo&#_lDVeAr@+HvJnWIub6~JzxGZUL>ToGea=yTOBZMvm($6Ui!X{1fW1qncLiGy z#rEX*-d2dkFW_`vjWDhp%`mC>n_Jy8&AOGzEtaDyDnZ~!H28%X1@i@^E z_-Bbmn9Bq5CsKPZx;vNL0TP=wQmGbu%=KSGjmF%MFi*dKhc35R*l+i_y*Sr{$HAKp zVP3o{FSrm~{))<{8tf_O5sUH^VAG-LiCMt^f?S$-0J%oE2Vb8`v5GTJwIN(iH$yqM zLHigYS%5VCIf$|P<+DR9weK9=*Y-ll2h+&6s7VI;+Ua|7G`umYrbd}uN+Yj8!z@Lv zlAEPS^VtU18160U6>JB`GTtqm=2deiR9=_$0eXX-Wh>3GL(#5Po+HJ?G?wP}FWx=r z5Hw0ZrYFK{n0xM4t31sN%``*NKCjtWv>;TK2|g97Q> zd`_1@5TcxI6Zcf63+{zo;0aE?vSng~v0{~eGR=Z|(dk(SFpXGHr3cH~1G>^aKI zH)?Bn@k)Y^TqsV1^`3P!i>+QNRb0cp zn@S`i-M6=+Gmy@cjS|oc0+-A$xt&5R3%*@IiY2vpXk&5IEBaLj(2CA9KFRnu)gYT& z_~8$5fXU|>A}jIfna1B)0$L!l;Nyr5B*CCgHTw57E#)lc z4_{FUrW$+iq)m!tjCXb=GoX8c{9pz9Ho=5PXp+5SLX9?%jact#KHIR<5scwn{6@!~ zz$>Kq$+Hny$sJuoZE@BtPljfZ4>k(Ux5IL=)RNTsNtuKN8D8VF5tq7Dl((RB}${j+RS=m#hV#-DMHy_+W4eXLaqfC(x73v`+T*uye)XyIe#(cqds z8lk3FcS;Gfu;kRk?W1)=SZCl@*GF4^=I3W43@4bf)RW_*V>Er2KXbTt_baI9r$D_) zoK?~*bh3d}x+q(e^J|`JQKUEJ`W@mJk?>*3Z4IxDkKQ{)-=C=)@)P-(NF+%Vrv)5; zEa9J1h)1PUGExo57ip;`#>^}CfAFy*kW=xpz>byM2!Sy2bU1`f^A&|b=@iSWQlDMm zWlY-YSbUF(jv;Q&l1JkfiIa+DWUr)StZQ1r>*UWLuVp8;eYCsyx?Uims$s4Qvc(j8 z*;>N9i-F$LOCo%4VqUi=IfQ&7BkYE}9$*{9xO=qmjbSX}1D%S~Jp7@|B5m|Bdw9WV z%cDbWDpYFYc{fzd3{#@(PS9p5nS`j`(v0YbR*IJg5^Tk~`sw;v>w-aEa}zhmx?~Ae z3sjUz|2ml{5v;LHw)^+abO~#cK1S9{aykY7rrRW~4s{DYKtkT@Ssq9eIn9+2jI+W zDi`&!{`voL!SIIjLV~yR_X#&a$*@AZs?3nBwTYc0Wb5tmYx$WbRBgCN40boq)=#7v z9V1exGf5bvj58)46@E?`dNEPplN$kzl(CyVS=rS;v@C^ygb#0WCcZS-_UYijZ!Ue$~#`6hRln%@G<>tu?L{vS7A(Dx~Hf?x)Fo!=WBBAQY5}=TaLZSw2 zouE(84sd7nw36Xl$2^cHULV|jFaG#-sa5rJYEqReu}A?sKY{Z0IYX=v)(6Bi%q;bU zl*E~&K;8+~`h@b0jV~Y|BOl;m$FExsc0|H8EZtU+d+O`5)E z#2|Ic5d1sN;X4erF~?-x!(B8lTh+SWmbcf@Jjy z`(W2xi!;O^*Ib)zZkFq88E#j+twssXXMvF2UyVwMl4&;Ca{P31-OMpk^R5Xf|%4MwQfi62o7^kT7)LmK?@y~yB^Ro-jV!uoznfKmA%81ENpUhLZ2bk4L z1lvTC^L7?&)4N5ccY?eVFNLYJ3Jtvd<=JYh*Zq@jdQ%Am4-yIVn%nB{%t`TgukW*7gDPF0f1==XT{oqutp|ZW- zEW(WwECgp2v6!fga*t|TGC7_#h_4$;Ma8V~>6yyLsu4+`lYnsGt z6qg6Q0qz~n;foSnBItiPhN|XgVRy6q_}PP

JPa+o4;y1;494sS8HXQO2Q~VVr0b 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

aL94}%;mj|lnC&q&3pv8g!KXs~t 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

y* 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

3auTu&QapR|R ztL6kPbi+Y#3%Nu(WR8{Z-@}x)?!-aS&Sck&?=SIMVXw7=jo-gNWT(pAlo(O)M z%cpnp5Y35bHgQfv&TnXOm7Mk;ARr)!z~_HAZQz?fSy%m_pdcVHe#EuST{eiY)c^hV z|9H(KQ2+loZveo-&j?-0!0X%+-1R)y|Rm#_+D z5Z}g#*p}b0p&nDr1AHTV-#SHVM-LB>4RQ?25*1EF7J;j5FbEJ+l6AjOPSSE}a_xnB z8WmJZnj<`-_fc5;?Xg7Lr+&hn(dHSwYWu8oYK!3~uC2&xWD zd9TOP>eg&c2EsWcBix`lAGs4x2JtZ{dI5WREwC2jgh~(}Y=}$uUz-MbyNdGgbdz@h$GG$l_%z6XCyh(KVIi9c0@I`n^j6>^4b@4Tf@xSl7z|GJ0 zt39-UWFI>klr?UiN3>8j)o{bzEBHV`s zJ~d?u>_U+b5*&yJ7{?OLQ+!;rISqkuXhdS47M~C^KgFO-`32@d(rKiaC!)8%&Bgow2U>)NuFF z1oW!ELekGSi58>|b8-x$-x*YnQAXM0-xR4-#^#R^Zxl%ZGgS7G&Ql!DA@U98At9Eb zmKex<)N4`R=w}@L1q$)bXrJa;PVpsj6oZywv6cD_CkT|o1JsI*Jj)tCbt?jlJ{H@> z615(V7{uPD#rZ5^pWA_+oCbRa_d?mO}8`iUN1q(ND=ORx%&~aT}*$wO< z^^hd8KP@oV`qb>kkzLa4tc=aiw1v*jPwhAMfCWG|1Hsh2eJ>Bay-%=@5%?skRhXyS zgI9S0BobODm60#$0G9C?R%0C3yA>w*r|P(cSm`G4Kwy*36l4gI-45e(b%3Ww{I?WsEo<=s@nOw{^d$p8H%O1OI6(sW=(eXYe z?b;&@IFvQoFc*#HKWe84uYlt#&*0O8?!XrW9?7SNPN8zO8nu=PLo&Cp_baW68G!(I z$_1pT?+8REbLyYUNDj3Tb`2bM_epF%Bt1Tb^j+YKVj^4m1;cRQm*|-n=o0PI1>7Mz z;xpOq3v9BQp~?vU61 zMixQUN*1L>nMA%X2oEeg>+|0hMz0+nN=zH*UZVTaC^7jd))>f+w8i-`$VM*g@CYU8 zKpx##fR82mEX#Z}poLTZFnv~Y;$J_X8)9N5#0bFG%pS%0$^F*1{lv?JAh2_EC*IbW zXq?>(x-hdi`@_}chh~LA*)Y3y0g4TqEc6%pp>$KYqih@G69R(Za&hL3r$)JRZk_`7 zXq*z;)dC{^YvQkGG}1GXDvljyJ0h46rxfdCW3~bDHi6Z_t5V8*I-AL19CR zlN%@m!8xKYy%LJGs>o4BpA@lLm!@zy zm`@2P*SO{8>8|=lujsOmY!yd(foM}2cN-91-Jkr+PEN~BE~TeG>R4I^@f~*0cAlK4 z6Sw=S4>o-{Wa4X?<#Z{vMnO5gRjVyE7pIeK$kQjVlxu%wxx*keuv94_eY zDKWfb$Z@lb|BiY3K0Ob0Z4CS!e23ry>iZ3w2Iz-0C}6p?2}HR5=BE6tmnc?++14qV zglwsuU5S8EynClA!+~n2JOv+F`$N@4=R0d7)}|SHOIBo}X7s7piDB=Mq*w>U|7uNb zeSomY{v;+|H(=1L65%a2s5Vq87way_eT5Qk6YF^b(WMv_csc73HcqyTN5}pk%R;v2 z4Zd}6hx+jFY1gIHBOP5W$WdirRP}m<>F_YRNgKWWjW+~rrr|g#BjWz$*IFyM`Y{h2 zT9R!@^U?R$z3rKah+(z(qzYE;@oKK3{yr#ZSKMe8Qn3gQyO!Jqe%JLi1z#<54)yPTrDwW`V3pbo0skEVeGE68}O|pvesyA6dE7nOi zYqSf=1~uwk3Oy3i0cKIvj?mMe{P@1Je9^j=X>`o*4+z&Jp@0dtPO!=p>d%pt z`iVJ4z2dv)F3|#I`8LeUql@>~@mZ`Z#{ic^_6e`hJoT9u=%GS&ks<=(KXD$k?1wy@ z!|NY|3{cZ0GDr07UeWd=sMEfjz#AO7hr@Cz9>RRmt#rz@&>rFEyB(4(QT=||=BY%G z#cz`?B_vWk5@N0q$LDd()e!6xJCS5SvL&r1jRrwpBfE=SMat~LEj?5lWt-AWvTR?# zcApN#+q=C`-;iN)LGDf|Ue5O~q^d!;1lVIa(E|DO_gn#Avq62JF$QBxM0(lxJ*yDO zS)P^qJH?($(hQHn&;xvB99qJ6`};3TI^ly_O^EJCJix6zk#VNCWmGZh=+O0H1`RM^z~Y)G$l}cW zlsm-xYgl(32;WYp#3~czbACFs;gHG6`$d>c*A871*99I|gYSEABh+<%q(N(g0p|`{ z=v##M1u~QGqSjOIdcjw$)~%^Eo@$SiMRt&O)6Sh@0Ge}+#6k>l05QJ2Vlh(>l-_)r zN!0sxSMC1v+Z9gL0vrZj4RohSr$}psn(9vUaQMzMSbni82K;pk5o$=x7o3u>uTnQf ziFN)Sr8%JUB{~|3-NlCAr!e~Ll}j~+2k((4SD)b7n#nglk~ z50pc6ygfMBiKWIUfQEfs^V=K5Jfls54<5#WbdyOS%sb8c`*HRo!d;Lx@?gZrbee=L$E>f4@0QoU$GG3U5;Dd~b3n|e7{ zKmWksBGiNF7c}-aq;raJk3zOyFx)DzVPNKGqiMI^?awyPPC<>k-?uTnh-_q8(X3^d z^E2jWL+hs&1^5*hH+!F-2E_jJxv!M^2R6k{`$W*$_+^?&qoJ*2OEMmJ%bKf%G^J7n z;#Gkrmqfzv?Tb}uoiE6RQAxbUb~caR2v{J)fIRdmn&}8VtEO&sujSw23K_!V1_$gl z^91OBq$aEugV=4_do4G9OHr|Z55zPdmT@j!49@7f;h7Aw&U@cKodTO!1dbqXxU1%H8~pJ$%9MTl?( zlK{-c8PgoN7CI-ge=}}@{Y&7JK4R^HpFoVhHyV|;|7t=gbmA;zKR^gJUrb_kntAgz z48pycs~Ql^0_hJt?3Cst*O4rDjck@(e^qwqSRMKE%iuJ%$QmqbV;C+9AfqN_RYda) z@ot!xU`IfPU7TG=t6rs|DFa?8INoB?1PmQQ`{t5%*~dlN^eEmG4c8{fgO6Xv|#Or&ZW2Y0Z^bdiJfY&})O=#{srLOS=wxH~=NWe%&X z5FZb{U`l?$$3t#0w$60zth78ic)hn*q(@X_PLTpP`l0nftDVbBbXhq3+$GEMhwtpKcQ(>VQzoi_M#db)+L?HU%?5@X56wdzE7e4a~E(JGk^L;uc97O^Fp6uM2(KU;M zP~8f0VU-v(-fG`1eD7Z<6x&Ry@Ybk$6?z-2RY^LH&JZh%nT6>^2oD}fC3@MPj))~0 zP2t{?9w7^i#JbTfFEG+9ZUD%r?-^K`VtLjsedjH-$13oC!gR`8Pk@<^`MujzpeUFb zt)s=+Gs(4~svG0pdS%{39=}3EJD+j*2g7Lb2!%aA)MI`|=AK*lMq*bz6WEC zf$aQEU#PYJh0fO4BzJ(C%rw`x>Z^d@e)B@NM!H;}51ge#@_@u+dzF^heT*UQ_Q2fA z55K-O3}Am}w|tb305$tSL3XG+wy|pY01G67;Q9=&pCync(q)EKtH3xxa*}Ca9~NqF zP?Ts-*w6Xmr-O46=UgoGJ6s}Mf{Ey90F8aL?l;H5r&y$XPOXZ+cJ^>e$21GBuuqP|EiC=^DpwX~0rk+G zbZW8AHc2Q1e|#g^x`M^JHOjh&I)>tEjB#Y@zHp*ipPLixjIvnz5-l@b_kxBNFAzYn zeG+R-VefLBoU<>%S?`>aJ`f%L*-Y4l0nSGvQo)Ge?eU7I2->iOr=(?NWcnQ%-Ti2{HD@z>{& zF0n!46i1{3O5cu`4_L9_CEi|-Yh$WSYnM{&Fj0bheooU(8=d?7w!n?jDV8rs2o6Eu zApiEZ;jiA5n1;S>hWWB&V3U7G8VPs^({hWrLB;!-u~3s#ugpB(m5soYt>5TIrf`L? zQQiy62>u|kOTjcEt>*7!+|#RA(P)Wa=bCv+o>v0qPjZ+ zGp$KvqA%BQ&Ntf2YEYjty6|fy=vw4fp-!%N&h`-OL}>OMkawE12@Hw!387L6^@A)S zhjH^|d!u`zV>6;1dbFc`f+|bR0Jg_TaneQdY1*d-{UWR~8TE06QQ}JS1J_ly{yjds zgeTr6N6obcMB`8HXJtpB*T)M4)p-Ti#u{OH)*WZcGNb+;^A_o};+Xq?u!dxd(>K1? zsG__NAMIk{jVG_28s^arnpF~)m_Ggx$e+INE^KG7JK zn_?B}sx-R91SG%G?(%by9PvyEyq}*Wdmuaw3vA<{Bmn5G=!?|4S^CovP*%skMuI_V zk%U9`$n+cp8=`OOAwsJ!5iXu^SCCB*1bmws?%baRZ;(m($#Tw%LC^pR`tXl3PmQLV z1Q;@~%eTn+Gx>r;mD(l$v52HmUKK{EjsTH~rP7-Pd)ICevg#m|mWv!CsN#EIBg566%N{dKuB(I{q67llx6(kH{Q1=eSt0KFhMdBEb zLNVAg=rPLG6fNN%r{Zrc#ll0AE|H^wI_ z3sC5k>&W=kzX%E*QBSU~Goa9dX0vyNNuW5dW8?%Yyx}gjQ_LrZVwq40|9oSC1YnUD z75D)|=o&$?1@)C@Xqwi?MYA)svO#~2Ny5B3PP1f@Q6GDa=L{C(dVn2{faySB;R1bj z%Eam%kzf_k(F&ympySL_*}Dn$p5RQmzd|DXd`|N7%B+Qc=A7pnnx+Xj-Gx9sXS@z| z_je}P>q&(_Mgeds@;-f55!l3gMx<4E+K8devqas@}a8Rt}Mn#Yr9u9MiNiPvw= ze1+wh&rf=SVeG|QDXIEJ$ehq~Zf)vjWjY)mgQf&7M>dUZ%pE(X@M5ad?o>ti4H z#xUrrQigu|xiYRe1x5Bc26;qK@~M$z8|6G-#!J?x0>f(W|CfpbWaLtk`NvqX$Xq#2hS=CKt zfvcFJ%3S;1Yv?vr`R)Eqksoy^_PV{mGCWdQ&gq4-BTgwMRjtwSZ*S<%(2d~(+^)dc ze;brA_w=d;xKdkJA{fL)*eemjodjo^~g5@Q|<60n^HI{9DD!u4W zh^ggp#t#_$16X(O$!pZK;_XozRHIzL8I>xi93b|OnUWZPsghMjV?eK%Z;*(fS;9Wr z(8>j-jMo6)BOxrZ?(AR@R(uUrMI21Pvg^v1Q^ zqT9!Y0+I~LS4cz z?vaikey5)opkTmo#ys3rKTkiSMn2z2lb&~?ZSX1``on9eX5j(>%n{{)S!RaoFvXvg z4*eLPzj$`JhhHp<2>Xzq9N>BHg6~!}{a)F+K~$$G?)5E#?%iHvsgUcPe+>McmR}qt zaCR28M=uMR;uEK(mjB6?Vq=vtpeEDpU6*3}&--H4O2xWkd^67wAV6LIO86CwZx&k# zkX*$E@Ex&Y4Cv=!z3(~jwzS(DoF|LHFH%4I-h_FPp(yBT^{pD#80Tmh**{D64A6%B z!MYbOn&j?M`z3aYO13G@jkedV0Ia-^#J=L2n&l8CdnBmGa`TpXN#!LJ5%`#J> zo`MF9(WVq%U@Y*Gt9EKVrOi>V?Jy|X#0=iYs_yevi7r9dC6%@^{eyT`4y4A$Ka7<5 z)r7(Pyf~FyK0lRMb+gB)tV6kbd0|1eiRvK7W{Nwq&Mvz;vU?@L^KXJopGAIo^7c7D zH=`t~QHyf-x*Al?w@pmC!8;Js#wn^*(wYG0^ktM!^9m!qH_~FOi++Nw4%us`t|JqCcS2@$*KM@)tgSdh0+zP z!Ow;#%B@NYI!tn?(fpHDwiPmDQq43wwDIf&9sJ=I>29kg1Z-;O3Qex^2#R!xs;51I<|+)H70_w$#-khirBxyEH{__*jj2*V=;VhGacLO(B5Jf-0$maI zhD5nH{;>6VFv9?ZY~`PvZz{`k$igVlYg4vZrDqFyYnwzzTQqWQsN4>j3>BcN#t4KnylE*w%|< zddLQZcUP3EOH=RWq&ka&z!tfVD-8IN?}O=d1Ndd+Y?&wcXCl7U&SB*lCd^CwC~A*B z@%oT5BRo>oZWQdGUzvGUfk~4COJbCF7ZhZumOqcLbiK9G+#n>FQ_S5nl@5@sM_DiT z@NB=>E%*)N_8Qm@8bDw&z!%nPK=JS$?nbFO%z1F&@kN7>_YRJHDtSw18sl&FH{@)E zz14BvAZJEfqWN5Jx|X7(9c4K!^+)b{FLiQWtrq1=q&PP+K@sj>4>tFlU65km%CukR zrN}b^mxE9&hq8f>@I^Y>%>~L5OorK;3?hv|f6pWdPFG07k9~5)D+Jwh?Nflp8b1{x zh;?#vozo-kk?BuHRnXu3{mcwn6^Gv+7<{smvrL=XR74Q!R2jyQP^v}m40wAJJb}(l zT9TEk|A?r+Y155WLyB`#r5CtcM8#^h>DvwSq*b~&E=_W3WP&YrVL?4T`(j;8Yv-I9 zq8FUSw|@m*LR%Jtay62PQ9+4Y)e#e3xU>t#uFX zWATca;aj4H^mag#Y{9o9V>bdOaG93QjvhwU*(@R|L=IrKc)1p|3x!U19Pog{KfO03 zonimHSlqCGCyR_xi821FPXnaYR{2MLWS&PaE;1*223Hr8=;v7xAnXg`^Hq3gB z51RBna#jJflQE=Kuk-84np4jJMMOlgyKt|Z+|u#H95SlA8_wBxT$YU zCq4!5nqcJMx6dZ$8M;K5M3-@_qR=r4$@}(K-+z=B{^iE_htuS8cbC7gmsQmg@s`A2 zcKJU6UOxPTMz~59imZ?@Kdh5v>QU^dHl#TkW8RL|W+dyrN4JSTY|>RXDInfV{k0oO z=NCB8m+*Wi;Y`AG^Y;)Nge3}vB4L#7et-qH{c!$I5iGt5szXpGwkfw*Iz=adbkq7P z!CbBz1S+Fcp|0tJP0=LIEyO$>dG`thNSyusBMA`5hll@@&?JR%xjqE^!RM$$0@f+7XWfC4u36{E z9|XT*-BP@C8}pPrGg@*#ASl-&N-s`ob+VD{hin{S=2L)5bn2>wT~Hoh5HN1Z6zkf9 zH46*8Gs=jsMtRp5YplB$oA}}!4Mkqz4dZuLc$be))N8_P`v>UX={}$klRIqU7O3#Z zgpW>8S!R5jkndI*{w8T|2w!SFm>0+vM0XvcZ5rP;^Bl&lLiHuHvxvqm%CDWh4bAtE z>jwS>0KWwQ;S61s{yUUffqpsp=D@#P>kkP1BGu}9m~<;+^erkd?_48D&!ZF({xd>Z z9^RRhnkNYF==pv~wNFJBFM)L`TQs5G_R%)si@n40lC0!6s+H5L6}o#U=nv)@Goo|! zsArL%cL!A8Gr0O)_BDhz$m6U^?+;IKkIFNR^iU8eim7xr`2o8LwoJJFxUGf^*!u4qlK6Dc%pv{m=dwa0+Xev-Zbxv5s#}>D zUFDQbsu1n+Sql9r_3?JKXN__H6jN_Zgl?n2I=ui|-0Oq+Z=WqdzDn%j%e#35u^%T! zZ4#*;sL`AqxA;yG61>ow`zY;JnFI=o$9fAia|Lc{+4Ke{Q$ULW{P^R`l;S1hFc{cT zqGP!5!w^Sa!4oH;D1_7d99dr3ktAOnFCTt1$= zS?}$rcG<>mFowpKC+nXf{_Ik%-}+UrG1#`eBO;x}I;_L_iPI~>U0+DzT~S^x&*K}= z)IOb#IgEQLze-fpn~eh>IsK(+`=1GGRQ^pl?DjFG3i1@IK#kdA8Q4T`Hcjyqn-Cb+D8H)U|3YnxxYw>!{yw6$ocF8LJf=qbul4Jx}pAaH5 z26c_1+~;XFS3lh%JAz(SZNVelJ^X_wSX}b?1}TbrCeg@Wf5X7*zNuE|0i{&m_TkRM zE4VoQm}``Sf40+k{$WS*=?Bpr3?T+U7vbxNB@%^eoK+Y6`CvAZ3&i1>6a(247SYc^ z!7zm8zK-Vy8$Db5_;~%>j+cb&0C1YSq&oX}J1;RYz^1rUH1oDeT&5q`5N+2Q{)MX# zTK0_yVI7xYx;{9)!rd3*qm!3nt;VEJ^|lbgD-SX01-k9dy;+uw&x2RQt7DikzNOAu9cdRFOilI{IFO0*H^~f%VKucVjf9d> zrz0}V?&U8FJt~SKD}wum0Hv9T4d{~Xw3SkyZqw@p4&Yw&;0y)1wa4Qt)Ey9OAMmc* z#C8BjciwlC%>=uNmOhcog3vZyKsp6<2mox2R@vwlqEp&AEYR5q10uvU?O{>B)-_o` ze`8~XmXh4hwOV|DYnUy`a-QBhq)DE2-dYOz(a#k@?Ssg~|4*{zG5$N9O^R)ja>1_z z9XbcRhkCll_$O5cB*)x>mC4m1+9f$wJCq)wn#8bX8g<~gs!BfGVX2r@tW~Of{6c?! z1ScL_X3G#*gyVi;{K8p?@TKsjxl z;tvLEyj+}7KU`lS;_NO+>r}@e-SUxHNh!@7;Z1Pl+5`Hu(KnucK@mR2_>Vv7lsF}4 z7VI*ZWgma&l2@d+`}`{aL0;mW0U%h|%+gl}HOb>FpI0SX3j7^%MVicvz(7Z;_OnB< zYi*tc$M`$?B&l1uPLqDDR?{0rlN25>{i_z}OMDV$#WPPcI_5Cgd7=gQ+s?a6Otb(w zjQNm6o{hZ2;rtwrXj{HaC)Q=sMD|bTecaH1(Z3la8$kbEVYtLdzHX2=ybP$ekE)Sm ze%T<`@5bLU>VjXuyF#7?IYA$wQEWBxtd{!z-Myem{h4go;|ear(YKTv(5+7@D15U= zXl#cMSn zGbt7FZEq*>21XhFB}6vIiLJvL%cOW;i*qQnBNAMLme|NKUKP(d_yZ}-pHHZGdtkpT zi&86z!?Xy}_+6drCgmKhS_9*Lg^6MD%#hmMD^ad0Wh7QRM8VomtKY7~zqra5>gsRt z`WAl6qpMg0_x$d6m>c&8gZXNfx>Kf9r=JVu9qGX|Q;zorYA@C$F0gh7f^JJAsPl{I zB{$ch2@cjheFPoO3mgl(au}yn?w%Tp#NKc3#72`A&0%$9NLQMZz_>i6G1?~zia1V3 zyCPi9yGy-Cp)Auwh3E!oyXjK@M5UNAi5@W2ZdH2IF?y8j;V0?KK}0yY%Gc%=t4>5Y z)iWMjJd1079|Y8jKwgd2NfnqUvMZ8l_R2}Odjw5V7^6*dd8c8SU}1kKGK=ujv)0aa z>y@mIuUH=cktDa~qSSr0Xtx{e5`DSFHt%V2a^n{*G~BaQxSd?m{tV&$9sQo-0;=?7 zmsI#CRv|{lqpa}Ku&_(mC+8c1ExE!p(6Uk^oUv!NbHo>_*vy7yHbtw1I*Cri>mU5~ zjpK|?5qX9Go}jQY)7T1SdS&X&g14G4|A3I7KFQhf7mWMo7g9^CNxEa$zsVOQ_u!^L z0GM<;@P_m*<5!ae+|rle_=w z%}HQ?Uaq)q;g*!fkA~)O5&^+J%n7hU-%7A`Ge6w`ig(dE=iEI|w-UAl zkGNn`b*ItfVqqzt(CyWHkEDEFe72)MU~vlGwIR+PDa-DD$BM`=Qum_;rnku_3k+ia z;Jj!vUz=FlExJ;}8l8-;^01vn9GpV{-8tR_m69gS@jbjB&1wq2cLZF|^ zBBNPKbPJeBWr_DLf*EB)s9tB?(v7os>a;Aktop|`0fV;g9oFgEZ5L&SFS*0;X}E?b z_i2{Oys;{ufVFC{Y^^PHOFT6wAs>cUpnie=TQQq<(YE>BletNBl-`=a*j7NvN?{=|Aa)U}|X+n3HD*a{``tRNI-6P!+g46!x z#nH$*vHsgzc0p}Cs%>F5fl=hQOH7^I>)aOsgxWXJwOXD{Jm?3qO@y0%jS>Sd`R@0_ zV`HpCN44!1Vwm|F&vV=@_9%I~6xrrKo&tp#s>dsl(kj^~TNLabTSKftjEd1(iy>3V ztYq$q``V?!*xUf{2iHz5D`0c%jnPXu$+cx_M^Bx?466Ns$@M~&wc0EyT{8HAzdF#A z?zIJ)qE^`8FeI5IJHFGQm1ku3jp3zU26v0SC(}5xCcX4>Pqt&5@OVGT@(7b@Uaqdf zcw-7q`8}^ti{c9bRGOx54B?t_NqTj7fd7;}x>+O6DPE+`*L&A-jZkRTsv75G8L>_R zeORU6Lzix_C*tozjQXnJzROAeJMeE->5&eBtnx7EBp(x0K$84+1x7NBN-<7^tdiX$ zBdUv4`tSW&049QQ@_!Z>Jk8m@Z=+mjd=%rhls?=e}W zkX?Ot?3N8Dl`Tbw&+Y`%VuULoLOf0)=BkO;5}JT5-o zdR#`^IW@ zzk}vKBZoPgh+q0H1It5!LU7CZvea+4R?TY@dA+tV%BzgWE*bRRwB+Zq|(x;i8% z>HdlcL%2jd%!6s?8#TV-8RZd@VwzJ8(Bz!h!&S~NQPF+`;!rNq%$+J=Pv#(H65jA$ zsqyww)J#%$F;trPncq)ve@^rcR3@p!6zE?mo6=6t%`q6?q@xdY+8qqx(xDIb;go)- zKC#`)y|s(>pIA1*Nv&KQKf=6&Hvxig7l_dgAbbr={{)!hMLml(Tz;1Z`T`L<7j!#`D8y!;VNZZLE@st@t5M@Y^|2z_HkV42@N-y3Fb=8mn<&*v4fM+*$EQEAj@ zmb&<+Tp&CmRuEyjg{S7;-^(z{zQhIL-&n?iJrgSbLf#+`vqHfTqF+}TWS9!{Z@!D? zAE$OIoj}S6##*=QtO?HR2JyaX^E9zt`_qG`I$nV#xcTnL7sTooWRsm*2-qWn?&Ix! zX)F+1vBW)sK2F_9rwDj5HmB zc8uxZ_Pg_qU8R{3cqbM8LB%E3Cg1Sy*+L2?xgxg)Y7K!***;=xr{U~`l6tW>mAVF1 z`e~iY8r?^JlndpGuoS8Uut%6cyiH00u$t#f$P}biAK!=AJoGQ;%(lCpuzzxLN7POoV0`oG0fW} zlP$F90G5Y0X^+P+N0?HL>QvFTOFR{ZFTZvvz7f5On(T7bS1?Q)3+&8up!@X`6uZZn zWRdojUSSmO0?ej^`2DjFmrPuHqMJRrKqvyp9^ir%{8U6Mc_Clk_kzGAj)s< z9ePwl5)r;l-?L*-b-+9m>GMtNqytqX!rk9b#2&&{NAM`^_qQyhXW^ zEwF1jLOd=Vg<(k*4!A7Rq2A6@$f z|H9%Qu#FFC5%1#fQ_UCYDNW)Z6cM;d$WxIP^mj2JtI-eXE=++&*qOz;_Q^4a_D#M7 z6R)mPxzkqCZ%I9U)JY2-iL3*DH1=s+AFM1-UO)hT1r##@qa2g;aU~u-X+w z>*>G0(FmNElR(wGCrN%Q45)u-Ex})`ax`Fc>3=fjOQ!v?DKYDySgK}Op5?V%L8@mY z5vlGw-6=c2pnqVJH3?5~KOjVTcaC2s)hImoh6?A|*Hjn%+*{(w#>nhn*uHB*VeQ!Y4D|(GBBiPTT zS3{*S-N8N;UwDp@Q!3pZXHKsUysjGR*cyj(3R-tlr^u_Wi=#5kDEIFyM8P*NP1vKP zQL!Q#k}UM*7BZ?hA-{0J9H9qcbWCifb=TB?%OqO zVOzZ4p+Oktx`~#D z=2%c~@=P)h-QcSi5nLH16peKS{+u9h!vQf%Pnk(nO;D2=8#flOs;fB zu)w+8jcf%;20Vp_w=yY{@(ra^FLeTWl(k7k*$yg`ORX|w+Vl(Bz!d?QuL;3W8xOIh zKqwy#3)cXXT2$*sUt$#$1Te08j%um2z*MeUg~qBlz!UzP(1?7?K2D@}e)jTLeh$gb zAF--;S`G0IyCjYg;%$w594o6h?lHJ$;I%D1hBVrcjgspd_9G~vR%hg=GysiO_2rIE z)duq!F6N_W=m@`WN3{wXHp9GDgGCY9hIK}$?d=Q~a_)T*JJKw``;jtU<=?3Ek4x}R zf-$TY!+t^6`&nfN6gic0EMN=^#>5uIKDnX{Yr7AKWBBglBQXv$yVX2L)W$ zYdB>jp9iNO|82J?S%_YOe@T5*zxpMd&foXc5JU>`qou=!k1 zfhXD7>f+s#{67HiKoGxb^bQtk5NQUe{^z})C;hv2fp&qf|2|HfHQsK6^yi}#Q?0_B zZ@YNVPYu$ubiV$?8~1S4ir0v7*1{c_$8h(S2|(Qd7vgdLIh&yDU!GGCjeIwq;`5OHLXP>*LpJSC643e)Mp!_Q; z*vsrsKO4%EW+6+L@6U7>vBqH@#~|Sr)7%x37U}!XPvBlY(auMxs-+BbIch4E)RUB( z3FiDAfZ#z+5t~GD)~;cnk3LTRhq-zN8K*f!ouSdrk*+yJ8K)I$NjI#J=jsh{u96LL z?&2X|GECe3fqnvN{0Dlyd|RxJ@OYGUg|1l9J4&oG+#2fy@n)I~{t5opJQ5iDBOMR1?4wOm+eOG% zXy@Xre*_z3xrG0fT_fEfma9wup0D%UQKQ5=0O3}s)8F^z(;$~pRky$&JD~KJ|4X&v z@97oXEgNS;HSr8*=L`3s+DNdqL0vCo7h{=TBg8zFq0eye=^XB&Tm|X!dx3Bx%UFf_ zAR93D6Ysx4jL{3VW1S+rG0rPg)65sBJBIZLGAyMU@-?~yuTY=h6=`e}BR#6+Fpm@N zlxw1#3Us&ecn1Naze4#sm1oc+>>~Rg>olVrb+3?p@;?@tX38}hnY-v1TSsI+!b1Zt zpjl@HhUN(|&&*RFAZSM3z?vig!Gpq`^K^f?gMnVbO;gTMt>UK`Ki2svmvZ&I1G4oo zk4;j&0=Eg=LWejXVDof?e@rtNW`0i(@+{E+CIYTeG7NrtcZj(snnz%qCt2pG|Ksrs zWtx<2EztOuuU2QBkZO#&N3_>0zK(N)Q7*qo)z2-`w~gu;0gV3}q_GdrP&-7P5wyxB z8wA?npyaEzFp|FOWo}WD@9?#YS6|>H8tasB&*W*ihWmPeKv3_BReSj70KGqvYBJ9B z482J_#`QgSh5>Z{0_ha_J+)3nr!vku!Wd=WKTLFpaW2MkgU}f2EM0)KnJ+eJ=fqfO!7^7Nll8!=#EjEB}SPSuuZb=aly`O_zW|0 z#m-SX^p2sgU|%aG>guH}q5yjT0;NXbga3*`wD|*WLJ{m=iM$Ia4PC(4(p*EY-W zv}YL!bqkDndmG2D(0cn(E>(T|d?L~8{0rh%yKI4+X?zQhb>bBF3LF^!ldgW)F~j%? z^!kXlFWWQC<`zOTw~LppyFn;fo&Jq{Ym_(K3ww=qg0I&slVnXlAL8{4c7|~jp!Y{- z|92l1`8vskbpr0XRnk0Jp)|tDKIR-pqrfPoLOI@Cvd;Bar`!oV+F7nyt~TemO#*AL zPaqyZ_rLxBclepF*CV7*E86}I{I}x)_B@S3%@m_myn5+0L!^_h{|52*bf-x4({DM< z6P`hCp}?L0`S?mTn^ZrY=YD0}I7ICh5^cVNC7NLwFVk0U%+|;>%++>{qgjT%*u#ys zG0MLO|0_W^O$Dg^-9n38e{6)BM4N3=g8fj=uVK|{bM>mFYs8rrlI&rg&k!2;?of;} zZU5NEjd1Jls9|h(I^y@e?%X5XJI4 z81@0>64xO2fIv4-KjRep2;w#O0LSph-v;J)g_LUYQCHr}C)|;%cZSv>{IUL=X(--O zDnni6N7WDXSHGJ#>bk5{`O%~+z@G^I-E0!6r^ zM1y?AG-a6M1+rN}xWngXt-M8|=)W2Th}SppKX!Y#KZ29Zfa0@`K|NQfn5Ovqb_#g~ z^7kBLQZ8L1x`d;i_wkw~6K;KJRWE&k1b?%NpQFV-XcOxcdWKT0E>{JL&pm)~0{z51 z!8OR+M?b?pLcT)0!8(S&gM7(3#5_T~@do}1K|jMdMZJK#_i-oi8#vu8=a6s*P<(>z z6AbDF$tL48)0B_9U@4ch3NMilu|6N2p`D|04%xzf=UOw5sGWD)urRw-Q?ILE$PLUhL{5`8= zDW(`FLY+MQluO;uLL<*WfzI zPoH1z#2XvLS4e7QyLfuJA1h-0JQ^imS}#z#1>o+1;miK*<&&vbt}Ij2Ds=x9;*nuQ zwYZBHZ$-9h75@lBym5&t!1y z+T!euEg{RH3T1asZTM+S>x<~oVIfR72*5<8}4G1(IHGbhr0)N4}aG!a1V!dm|--| zLb8s1K(Or@M72n|&ZE-hh63g`k^-yT{#wd|4$!xnQ0^ zyk;Hyh>RCV>ILQrhH3aamT{oQf3swyt1qoDklq0U+!!aae}4pHADAcc^)^e`M>k6G z_aI*1eA3I^CQvVhdUpTCI_B#iW1C=^s~2kn4Bs&G9qbWCvgza0!vr(=ie4^XFX`GD z+9QlvvTC`%uW$!%UyXub*AgY!s%;GRL4752de$}oqgNWJjibnd?x zV48${`Dp8~4_t#B!+gEGeYA5Qy#oEr$Dg5{k*y+KLAyFqLpO*5ab6K5^m z&@2HAzgdEDs+UizP^>M{!Zc-;POA{%mTfe^n{GD4NUCv=i)a)3;1XG_jHmxc@HE3B z727EOj(n|r?JEQ@eC~mdc1^m5eZV+{b%=ccch51*HY(7~GLCr7I7Pbl4o0HObe>)gxa4hL3*QDb)Jq)6Ev)$G_RbAK=|4hH+A;_3h_#5~e-tOlkszsPbyj_mrkKa8U9LRlq5AtNIpn~;o*P=u6K2vL-Vz4zXG@4er?zx#a7?>~6Xb55soK7G!0 zc%J9=-1l`~*LDBAc8n+4VN@`}i+xtAOL!pORj!RfU;gv(9qgN?+?e3ZGw}^AG^|yT z>=5p=O|?z!khIF^kPP-m2A^rZQQ-vl?kC4Y(81TJ(k%xBtx=D2`G$H#!DbA*ls#gV z+8I`}oNV(tW#|^_6Phr05adq_9u!6UIKf{HI#2 zSLA>Yh5meJgA!DkOY{qj^h<;X_dj?=g!(SgBZE)7!nh#XbAn5=x=l9EB-sJg0Cf8T zgRt@86o+S&b_Mrji=1_aVex>FV-jY>#-P z#V#e!TIETTNV+}ib1hWEY5fMar-jCCf#$vOVYZ|XJ7i@TqY!N2s6b>{7( zBm7EDo3tC>1A;<*OLbH1a!q?hBD{WHBiTivGpukB+)3S_L=6D>v%_~*N*TnFqP_MTSR+Adl(i42Sj=W2Pig$ z`XxKqrWG5I!M97nKI;_c?MZZe|A2Di{evX1aL|JAB6C{n7IX%*{+3I~lU*FGRL%{?Tz_4W3bHEPwGBYdg$ zS(Zx8b}5s*CiyGB4T`wO6dNuuAH78eKid86qe+er_k;cOjbq#_GnMM6*oXKNocM-e z%Sy6+gJP0{RQnF8RC|RM)jA}xMS0M!6zhljB7@I0F4{x7Gr=j`r_e~V%CLxeA>GD4 z#WcT62aFFI1&kl-jBJZUC-s`ZAk+pTy{t3rQ%K!^(VkJB1x6IQLW_BBP=N42yFA+6 zEXOBIuDMl)VbL!n!KujLfbi$FLcW@VaUD zAJe>CGcx#@mRT0#T-;+{Z@I+5UP-GA#)Shy{o*11C$BXtx5;!%7Z~kQGOTD;0|V_+ z9=uO-km#IdgI$!!;LotWeKgJH9IsjV zn~3&Bd*qsFS9VBAcL)zyXD%>__Bkg62gJJRmgSp}!M9Dd%IFdG3&}B4|2rU9V8k@P zPGyqEHExq8+5>5WDz&sLRT@bS+U5O1lN_J!Qm?Vj-2PIojeUj;ey~6N68V;KKF+Cb zsZ#x`r`hK9O1`0%nSa&Urr$h{aZ~?Gy=I+mm62=@cLaL#V*RJDzTD0;K?Yx_->BgJ z2fF1PvpwQaU&lo6aOe17|DV^o#R`njs5Bd&umqU;LwV>mq|ZlN;ab zm5{+#Y!K;{X@%}iwMMR4qLX)oZkcFAag8I)>{`40+>4E`8*p`l(0#pW`dN7O3KozK{3c_!NBZy)6wv&?dh6YVEB zG0n5gy2aF}n&jD}waCi0WLZEyFaHoS_;Sr+-M|0(Nx?rvx&sqH`X%u$>NUv@Wbl>h-J@|%pT3f9G0Pbg5bqk{E!8DBy!?k^ zlWW{Dag007hHP80A=UQRRi7~26XLxK%st|4^I(6<4P@}`lQ}1b`D#@vHM7m_lM|et zy)-Nw5d3goaA1+aIsVISrv#s{Q=DAWDfVwSX;;d%#k-j2`h_?qk>Ve$)1STEq%h4r zAhb`WU&6d#TXbU<*}*igQ$}zY@7yO$wyplxH*}aU%R;`%ET_PTb;d5mHWd=+UE(hNOS6If z!=91J_Tk>h;8$s+TL%T`7gMcEwbQO7I*xId=m`$o`8>iK>uQ_o9FIb;&^~!B+VkZ$ z+q8AMYR#j!%60ohYSl>n{Kt4f0m=4#!j6fb?_ge(YbQ8~b!${{PIyMfxD^^sa^Ref z@GjBQt~kf*7tb>d^D!=*;>5UdkA?XlgD=quKMDU3(Y{QpYR!Lh9_+Ib-W^h$Q-Q&0 zwr(-uKJhN#2WeNh$6!tj|6aKcDgM7j_RH-7L8ASyw;C1xs!wnl7WxMd@r!r$h{`pm zTR(ph<7Sp~O+ zmgwc0r`d8%sn&x66dNMEKHlRSF44n26YUA}dHzDDY>nD76B+zRZ;c8tFaBNHA-(>C zas#US6g&B*BYdj$7FnOLMTQ&S)&CM6JbQ_Mq+FNeaQ(;g7cZX?@A-ymR3U?}S_AW6 z;XbK$;C%3pNOqO$7#FD5XjXy!rC#fhq+6D0{RWA%zCHndCa5z{UEgvGghx+~f7=`QX`sIO4}Fdw)cEwX&VIwX@E#<=a1g93U) z*{6`fuT@d29_2A8676~YB*x7-zC!EYr8a5VmK)!rJc9ivI4>}#*c%kRB5cw=+%MCy z%2;F=&8GM03;Xd9G;XaB@rg{1$;8;~^#k!g2;Hx0hD$&U^D&GY2VZi~p zX7Mh;0ofLqiYPQ9gAUKXVWDlRbQ|eTnNETe&1$a**SK(Bha~^d61{v=i>yqmTTG_q zIu$UZ8P`vFi!_@(;!MjYufgdy&IP|2 z*%tXGp3!h`EtpVX>|e zV5%MC)~K++_~@;4Tc)KZh+@_g25q1ZTYSI9HLu8Z|Qb z#Cz;h@C{I{fm)wuRHT=0n0ig7Rl1F6AOGkK8}uMDt;Bl*gJjzjn}P#p*pLNGu_@J# z4E{M5OkJAgp1jUBhr4?Dk7Z_w9@9MYT&v9c2ctYj1sE8Wnu7vS9@^!iAALfN?EZ6ulonnK1vTx|qSCO9m zLYM!X;2z@%44z@Xd$>Z23_c1?v@g_u?dRt^{6jUW-*3Eleu8VCY?qR4euhoA{O)0x zk5`0y^gL6gW|c-j(6g6&#K4L!GF<r?WLs!gfCravlI)OflWkF}7VW`1 z<{Fo80`&)|KY{m^>^Q?lqp;4A>i&U$4e^U{d-@7E_b<2Csdh+5crz_ED`{7*{8p;J z@x4tt#LqMrc1}NjBhpK`u}{=473!N}ht&D!lpx*a6XqEi>G}1RYK?JztZRl9-m!3> zclclR&v#g6jS4EXG%Gc#y2Y61&akyAdPT1Oigq{2+apE>zfQSbYMKrBJjMmkU)=gy zq2(T3r6Jvhc@gf7buQA2ckC8ZtCDLP7#Qg((i`eKC}5H08jF#kO;Tn81uWCyU01cxlM<6OjhppHPQ0!VOr`OGxeKDl1WKRDUm zHZ{p1$E-v0`VY*DI^{QyMS91%p1eN6pJ!R3-SbRT zYYqtSehLWsc`eGrKUk*>sh=NunMMVziaO;-ZxtF#^pxs>m0(!>d?(r+&fW*_6&w15 z=a}z*2>0e29^oz42cHyD{LAYn8CF#5-r-Rm#C!ch8dXUSfq{M@{X(iWP4c#>!hLTZ z|GEnN7uC9P{;#VHi!Yw1TcgmUJV^21%5|zWaQ;iTonf=i@Qw)e3-_&2v&`<2i1tXe zOLPMB%RjV6O?UvzFzJqPpHe-r1W4&W=$H71P-ye)8{eJdJ0u^!AvlBt`a;8cCB_Bq z@;zedHnwTVrFM^ozE`*O`weiQO|g&gB7@I6qFG74_2ji@S8(8v;QEg<>}oarV)N__E8;zkD&5jPVWjk*YSr>h(Ba9pknFBe zuTufD3A103Uas*ia?k?;H_SW2xPWyIegK6=sdm{GoAfzWp&{?cC=W~o&?uNbM7z63OSQu^oN?jfJ=a*I^rxPY-r?M1&~HN> zP^mf2HNy%S%usF2u@%#Mkr){DN~OBjeok`4J9mjsu_J>Iy}d-IO07~o zy!&j^Sm!gW+hp+Gk8w}2uT$Y4&9I7fgDO%`3CEt?hoZt)ylI)mcKE(<1!N7=fx&GtjGtEk-`D!&tprYKcO^x)dRzn7#bAo*; z#tn1_<=PQm$@V^B(eAIGJbw}8p;S+_U!(f=(HgZ@1@j#1OtAmMcLaynXP@s7?@e&_ ziXekeycZAzwS0~lu)Z~_%XD55pYA?=6%>$e{p=-lU$ApA*YwAAx@GJ1fS@z%hwtoC z#J(m2*O-RbX&|k#>c659=Jbe(3f^dX?)$dxZO#=OjBsdst_P z_AxIwCy;voGc7kLPjSe%VqLA%&9egoTV*aVo8{!1MS9(0sx$}>?tU`MDbbT{8R7>& z$QZY1Po(EOQu(VD#(iIG20$?Jx1pSAIifbcAnEbpJ!1 z$qBAZYmeyfEAJi-3J@GVezQ&$=YkBrdvuRzw;1LH#b%*ll*juAzpvc?V4wWsdcHC9 z+zRdEH;N7K9@4D-x@wa5>}6n}T=PCrzOhpMJd=4gGWbvd2=@W6C*Q<6L%k+AfO#R_ z#XrO`Dc%M1C-^!j)eHA6(``^vtT9w6#9iB9Ag;1It7!CI9tpGZ&Ocd6G72%#5p zjlKHo(c25mhwo-tf&$95y~91CdPPKfm+53%YE{^$VqKBKU#Bw9woPT8JHe&cEY)S1 zCE4{0G0wNhN^}IrNuuM`U$?(tV3_7AHu!~5t#^q-U1pae)Q=SYFrRz0Q^Myvfq|*E zD0G!Zh+m!ZkL%gyEwWk_&}Z7D@ek#in&vVtSf_hM6zlhhPO-u~Qa@;%}ZKZeDh*9Hafj`xWFs&kD~Y(hVV6h8jZ4rz{Ao{4)j z$!?$U-A_+mGc5kO4AZXrAHsa>lf}A+_?_dMdAM`;<&;l*gaTg90D#eY?psIw062uU|aEOR`&{C)R!a$1>g1SL9plQ@6hu z=j)U)E+B=k(8xW;Izzj1`%AqNunw#<0)vDHE3_3_H@;7BHY%8B;~xe4Tc>}!DcomJ z^!iCqK!@Zu8B+cqP+(B4mc*vOwXiRq^jquL#q;P``h0sPFY3yCmXW9Fr`w zMEf(WrMe&Q1qEPURB9rHFVdUnIK>W5%QPF9u9oR6FrMI2uLT9%_#Wa1JJCRE{N%M% zdyHF#Rf!(S?ht>kNQoX&__w~ww9>9nZruL>+?rbq&FTSRyOdD>2=5H5LsF*YCPhGy zZt3$EMFvdsO!F6*gw@uUyAGC(^6fAm0Q^I8gZFp74yyww&VNoWkE!Yh+qk zW+gl1npJ9{7LaOZT!1MtQurV5mFaXya*wUhrdxyF5%R^G3#N6uLKlIC%9og#`#emNa4fV_3~M$Z?_ot*f~~+pKmDM@dz*JPOZv~@4O@Z zLVLu<`6swLqzsEf{VKIMrx{l5QsP~&o+5=0vp=qJnO22HP=YBmGAyD|!hLehlpD-* zJfpz$%QunkP;82I3-wd2gA)M!SjfHq-)^$Z9^=V1b52aN@ej$iM7w{#;T&INU{F-A zlxbO^MY?m2<&=~51i zeKpPY{YIf7->^idM5j;K)nCAfT4bf!(5x;pw8-B5lxm9; z8mnI%5H!d9<#v<&5Wj2eM!tsi}nZ%;+&Fi zL9T;HFXn}4k8+(*Ki@FLrf3h>xKh1%*B&t%g>y>1#<&38A^j3^_kWF=;PA_B(Vk&G zzTs3`&q$pzvz%kR0YOk+U|!&!$Tt-kaF3x;OZ4v_%rS!sPrj*4hjrQc&>BzUR&%hNVH~=aUr0{=TEzx`X zXpDP6aG0-EhIfQ~>)pef-{hNI;#y_UsGHvc1K&S@{(y7h)l-g1<9zBhtBiIjGzuyF zpVxFt;Y<7O;lp=I^=}{fgbnfkyHv0A^wk039`PCWkL!#J_($I1Dz#}gv@18hhkNUk z@s8-0B84C0)}V-e7U#k|N4IR5xl3Z1*(_J8t5-737V4XAKEz+A{QSk8&rEHb$M zqfEy=x>v+JdxG=LW8}_%x@D#MWjfWGpVulie_chR`h;(MH_p$rly8diK%;(M)2I^a z-zLL4-zN(7wam;m?-$Z9&ax;nKnh>2T3`?=JMJ;=F_l`iYEXQmQNRMg86eyz)=jm} zGYTnCPCcst_8+T3Yt|6jA1^VvMKiOH(F(m@mynZ zPtt5OD=#p0OA{Rn4XM_9MOtNs`H;dF>SvuP)lG7^_R~4OQ32dQRvBm5?NYQWoD<7* zdL^qgbjt=seZplr#`zc+<6JwWAMWQHBQ<|k|9gtFMQ)J=Q}84FeWJIIR%izV>y<=$ zBfS1yy7n{0F3l#*W{#QQutE!k-k_ve#XGi6m+bHfLkeH4dxH{GzdWOR#OGKHi;N49 z1ESCEXpcq5uf>iOqK+nhvOohgNA&E}GfeTFA z)LIp*jGN#5LT-JHa}gM9lYaS3q49t))AHsw*hr{R_4-MvE;9JW`Pt^+fN7Vq$bt>W zL;Rt>-r*P+;K4q}D$$eZ{Bv2U-Xu@ARIJ+~Ym2-^Hq+88!a3d{2^su5pY@BE>FAeM zX!A^Ne_3F}J6>SC^v@)3o2**RF;S}`)|G5~ff1@c<+?YI2@bo(s?|b$4GZm3kh%kE z)y#8P=a6&2F-drEgbx`rGOdywFzprZQmKUuFnH>?$G{KGxInkOM=a5a6uMtXmBto% zwc6_^@P+wtU99`=rv}9+5953kT3|5EW{LjJ=XC2QufN^gB2TwwpL+EaRN!eg%yY=# zH!5&U)+@Qi_=aMgYgJtP`R!(u2i-E(Iro@$IT{6uC!&3`oHvgpIy)uQ{|fhEp9u_> z=rt-Jg`a0)nF(I+WP9*jn&!$k8Rrum`UhY8Szx47rr2=%%cr~eN5p#^le{B3Wf@kM zna6m+{XHvFx|4?0K=ld zpx}UflW-p>K}mNwCY9@e7wi=Q?J@lla`XRKSJPbJRYB_(?9Vl>RnaeGmm=A*z!>kG zXOeBcLd!dHjWZO;&$l$+v`uNS&Uj&CUtZdWp zTR@{;K8y0;8qc!8z*we>^n{6+T}q5wu|DR7Q2)P6lN^1*E^$v@Bc=c38wTAOWIwXZ z3Jh{ia7+pg5beYCPpljKKv4aG>W_JjZ2if z&?>_|xmzsDBGGY;+NhvS8t*tLK&d|06cnd(%%nROS!OxwRN>yeB1nDzW>^ai-C~Ri zO7z;Lg8eTrVGd)RuKw4c2)s8Gn@aV+ugEu{QJ?}}rLjx-cn^)DUCB2_N`EKQO1=e- zuQqA?qdXIw(*;JMeyxfz?tq{=Wzn7zJ)f`|Rs18b2!|x_0*3l>OtMX%W0~YJEFy!i zTnB0sNIAnghxr!6BEccsw0IZ(5zXp~Q-*WeD`HfTV#hiY=R&^KsDN|&<9e*?9&v(GrX|U4hSh+eQ-Vtz@HE!x z;1@^Y+9E&0Hpzo|$0!d>)lP6DymZPI8Gc_W*Uqva+isJd;0z30p=Fw{Q!X@&cMkRa zt4?^(F4ZoD4F2akaW3}B&|jD6%`?S2v&^Pj&okj2J0wAewM7nnVUzr)yDy*pxb7B{ zZe6BBu_@e_W`k7ztWv#vlWdDhEu_Hl4`E&~&GU>>txLCo8f24VP+*Jv0#m4;ehCs0 z#k*K%I3^Vvd&Ke1o{sZxuR7nbXwTb6B)iZPZc>!# zpi%E1g!uUf(=GRimgs3#?vhliK}y~4E9+Fq;CD#UFGYH0T4tL|x7DaRB#rX;2MhPX zR2dkMXM2{#@oB`*Zo)&k#S%3rMVQR6vS<=a|Gjp<5mhgo@{o;L7j7K$=y7 z!MBfoUV~&zk=`e-&#)~sp&AJceDnDGji3OtoN(`26{P%6;$0XRxF_(w6YoK{!!mn{ zL$(bYUdgsOCO0WKC#crB$3%M+8&qqcPo~_EY6oQ$QqTW7m0=;>vRjO4u3icAT&AVK zAl|We_`8QHwc*|bhrk8rni}W7eKg85#4k9I+g-#ZzdsPA@bR>voN0ohi{n>h}-dc-|4H z^?~0}XoSt5@=YSWa0*bb!P~z}vQH%0p;Ql!Fu7))QJB=hoERB=sJfKuXIX4hm+142 z*{0{1{evI9-6c`3lW&3q@(E6xG^hYQqMQ;yhZN%mPrq|~s|+&uGOa`WbjxJh6P({~ zWLn<)_*b1~m2LXprRzVs#51gp@EI0WYj{V*yPD-f{IV>9{WGir16|@UFp#Q0G%NXs zct(F-W1mWLfV^+fo_-m*TH*_e+2G8`6h4*3HL#-ik$wJeX3kL$$@V; z+uSc?kNDj~>-1(h%!^FR=P%51YE{g$AHTW&L!_5z-#T4z;K%h%iU=>V?dLCOR*edf z!N2wO*-M$$cMrw78x_{5r`fv1r`Ze(#k&FnyT#(2<6IOQ9=&CnfAPFd`NRExm*|&N zYaA0@V|PgkjF7?KA%!%Dr?1$iLF48T1vw}7$(Q~O@yEH$uu8X0augZ7c?{gc+ed#c zGtb@rA~+D^<{y0TBU1RRGceb?z!V&Sba%0CicQ`T@Hqe@#5x20A?*s+_!2!hkLx*L)jJ-x?ag8&Zt1IQ{8T?e+P~U%-3XF;jB0c|IGAtD7U1SIfK%?M1NU^iX%CK5w zm}1Agxcuh=bBJHQDcAJZRdA^xl|SnroalIpvrqKz5@@~Jr2nt6Y=8d3F;TOUZy0o$ z%ySEjeZrBRhXjU&sx@vg58tW(z4wvufMpgLe5m)C=Xgg*c6mowXJ%L#7O~DLHv|SR zFgYjijzJBmQVaS-npM~hLbA&@EZrv7eMo?m0u)YL{G&c$@EsrFk8y8M78rs4V~PID zZNh_LzQ5`lluijB?>&EUjAxZ`<2&U_)jrV$W`U7%U8!!T1Sm7fw+xE1&F_AKcQ46-cVv-4qBG3~8GPW>2s)p+%N|@ELZJgL0j5{w4*0IG$l&)0m+JNl zm1`FmH7Z>B{rC;2ABzmoC=3jzgiK5Bv3{X{m!{d6=Z^7uL>-baFx;ay^#6$d$ z!6!IGqk!E-q4|bUXuM<59`FFkH46@icS&|Y;*ogQ4r!wT@gBt{Yy}kW0&h5S)BiSU z>@&wihDE)S=Px!X?tMJPfs^O=6^pD?TjITF_cQD-xA}*jzmRJV_qIzhDqx?o%5X}k zRzu2vmuBM|dg-4<7V8WsEt}~&omEdz@Yx7S=}P%8dt6p7?f_K*u*-=J^4=y z20CD1hmc}_)&G|0agSAMg4;aA4+8`8HtUovvT9Wt6>L*)elyM0E$tUl|NHz!xcB8h z42xM7d&H0jg53U<9J5b%PjC+j42s@7KE*N3^$&jeY>NHW(+SQ+2A#5WYpQjs^{=;{ zzEW(6bD>$S(rA*O&!gU1gBn!RQoB; z95d&HaNi7Ti!2)D6BZO;nu~ey`~}JG95XWb(EC85FYNpf?}7>71t$LxTmR zHp40)Xp-aEORWmg9=DiCPsW8ej}Hiu>OWsRfB3G@utv4eP`NJBvsK2haE+R1pLIsR zIMi3J8RwLC#V+O3T?~vjj|mU9$WL%kLMs0ia(*H8W2tt~eo1ynbj~wvlkp96k5O#$4}mv;ed+>}X+1fDg*y0%K|FXcxR4Druh+Gj>!Q*aHAtt|CQ?gzLH}W z=HnVmwhhiNwrQWRCizW@kM}wyPH-_VW?1Q#O>^^&!J}`N!Zv;9bF8bt;1c}|D>C>J zo#5?Os}}12ud>5FBRCWogsc~=bHW4pCibZCqf3lS46D)&10(dMg@bSZ#M@8*rx)5^h)NKcSz}%!2c29)hFy5D&LfB zuT}BxVT6}cf?Lc6CEu`dJ~H^=>Vd2u#)T7Hv2NJ;1!?eN-Ro488?X}+Tw!v}a?PSW zlpBx}$UX&b0OZ^Uj){;m+bKc5wLvM|*D291Rjnr7W}a=5XPSF~dFQi#@WXfFT@8vS zxN$B|U&%LlM9H>5m)s_e3_i~&aDbB>u=f~^0*%Fw>%Xtqq+N5xc`Kw-~F~!~?*{FaFKEvW3@iZIHs8xnuNvx}U6VB=T2YMy@MCIDmYPP8- zxS;iZ@|t%f)i%d$jQj2<=lBLisrDP+k@6o?ufePnG(h0_f)k!+RA5lG2EG8$?+f(< z2Pn}g*9_^O>r|K*P$wW)|4VjIuhFgy2x^!Axh&ff=h7{+I5RLX z1_UWK$GIjrpS`4BYnSp5KEWN~Mausy-1q9~8FrYDelhgBb}5ODe8V-Wv@3cgC%6oY zUJ>15`Ns81MFxDs4T{ujEwa>W&2r0hp}t7XAL*CgJ>(s!)D#?Wi}`m+sXosH>-^cv zLPPCx@vZ^E2E{M8=a{+2nCAV1U1O`&F8>MlKE;6@4M^qxF)k=Jz*!C%U}W23-LUsp zq!;cs-2eY}0fOoS14E({vcE}oVGkH`39wLYXqON3eZDiq|Kd5>Hq9#TiAi3L8U2!f zFvB7wP>FThr8FoO89@F7q_Ck;_deFBB9%Zl!TIr?Wu|`d3@g$8J`u%cfstN`LD64z zfx&9EcxU7MS5Nbe%XF$VMtP>$WLu&j2WQR;Eq<<0baZbp#h;<`X{|O9&yS7G^Xg|cSP5RH}FrOM#(E7T@ zu2I)1$2)@y4LYAbVYz0jj0Hy3nq|6evM?W{{I51?`o-WuncxKdi%A~UdWl|=!-Mw< zjSfkWi;!ZcR6oKy&PBhpz@$=D7>ZI2RN;&jdU|xu&oq0P_MF{QoYgb`%;|{%x{;A$aSs z&t_QphT;DImjVM@0tgOAc=?C8$3TgEfr(WAWs?Sf8WcTwE!JIV=o@;3FVR`4S)&^7 zyiJzqI4D50e~btEf7uqA)eFpId(4X%H})y=tz8mi@c-MRP^#-0S*ud3Vv}Z^pKjeF zD%RaA!a9>{>KKj)bDiK0B@o9=&j z{eer~EU2%`rDTB>eIH%#>c_v)r)oO7rKd&V^7VAs37aBq?J93w9=`vk~*3(yzDhM^8 zSH#O_khk;UzGO$Xxk@eBwp$G3)U`>c+1&VUkrm|ub08$H{{dSa!6Xd;000dD00000 z000000095c004NLjGSduT}jin5AKBE8X&m4ySux)LvVt-ySuv++}+)s1P|`+U-)J{ z>ydY6GR5K-oPC|DySsN)ckNxO1apJ4jc_he-zub1g0WXAZyPnl8t%}|(<&6;mTEjh z{ha^^@b~s5CdlOt2;sb1Mx+_)PAiXYavh&_$To^@@(ATSL6azV&ri%k^&6Odbj^IX zfLW>->uMS4nrqni>x%Wd`Z+|`3n^6_=CtzG^BLrvK}>TY9g|KXpP7cvGi~A4Nd7&a zWZ5{CVNA3I=4O+tJB%3E0O0&u{&?zjDmT4BPya$B>wDG0raSkEex3g}e|GoD;-0+t# zAiBArkEQAY?T0urm6FZAj@tx5o}m^$(5lqzQ%#dAg+V~-q_K`zJ2dk-x_@Hg{m{*J z3wZx@P>^YuL6LklbLS$#4+{IJezuG+W3)Y7m|Ny4M`&KYO1bOUf6tHoatRM{&oYMp zGv2mR?(8Am$T_D$8uGM@3-@3XO`@$)YK?$k(KNw1s*jPcwVfa47W(=bG{RoI{@wo| zXU4H$=XLxkTC2DQahquR3F(>!ans~z8^lwTBj%B15~|t1j{gplnL?l5 zK#;!z=w|x`IfO;owMuT|H}D{zez?WhhPyxbpj5m_5oS9|^#s+%|JVEqg;}avY1ETW z{?D6S{rbs|2z;%`CzVp9%TZR(;1O=bGkn8grkRG%2tZeiv(4g6op6`jeIPHV2sBfZ zbdt3|fLBPHgjAD2H|(t*ezCSODuw(nNu`R*6w8ENoC{Rv$VKwb{(l{N{`PVu*y|+A zO67YPKW88iu*U$W1m|!UvKi%U+Qk67kgp>zljOes}#dMX_lMD zDwe2~6spRWv-gQLrrQKN8>Mu!F%Nf2PSdq=h&5fpsTYYhGxu7gDA#S_LtIdfz5NA0 z(ACoy{h&_5G$Y1#Kmg{7c?fM=u>|X+K%ZneN5M0+Nl>n`k5#0NXeG$~1p5~325N|z zbr^G1u1TvD>WHJ4Vf-0bbV?&%Hy!-eGMR9xpGDU7t!hr-8hp=z}s7wv}e!?sf&xJlU63l9_k7FeX5?Dw{FG+Bi4>u1^nqOE#8_! zRi}_m3UiNUI!`m~oonzp!ZfXYIMpor{sRP8?=vXTD&7WvM~$>dGvvKfXo|@_WFMz+ z%creBkH2{WPq$vy6&%j?msqaeUam$lgN%4f<^&Q#{{zz-ScBg=w4+>RQVLmQ4k&UYM z!5V35Rp#kVp*k5xF@MgVrAjf0w$3nS8ukblXcX<7q|qv^7M*9HA0b`EKA@V|#=rP@ z`AW3LKB}A1E2vUF!u0e)Jr`rYi_y)-K0@?^YF;~M1xK*|6-2%y!<=}fi&LP%DBm@P zyAx}Jy9ex?x4)Z5xcLm;!|Tua-w9s8bn__|L0-Y0arTOI43Zng#%VEkK%d4~*;<2K zn}l@oxM%z6@s~@q2Uzg8+eFd!@4;)tnMUY_Z$bP6>1T@N8^!ie^)pqA&62u!r)X=X zScYM*dAsH*U~UT4^)s7ACh59(*YW>c|5&SP8Tm>dFW}ce*Gak;5Vk=tAEH&)z)&ab z9jDMyrU?6aiX)VEfhpQJt3J+Uk{}nRQBPl!lnjF=QMsxh7yZmmK0l9eSJyzv2C@a) zDDzy+6lWj9Qh%3lbEs#p;7{AU&D&_aJtGW%MrRu=Q&ulQ->+14^AoPqE1+4p0rGZL zu3?{^VPF_Lhc8sVhJJvNsRFx$I<1hb(VSu+oR_JFe@!*QSZh`&Q3YH$1x~Z%YH#2O zw(<-*Cf5o#3Y;Rlgnm3sH28cXR)cbuWX0K}m$gPzp!jF;XCH{QDOIPKZIOq$FpMo$ zA7$t75BcKh)*@Ray1*#hV;<4P73b&}>KGJjCosRI6ON zjy}%PC{Zb|mx_E9;8`hwe0&BA{g7!gK?(92;u-E%C)vWoFqCd8&~*>?2=(#=_Usny z6LJ8bqfx4rr~hd^PK&pzR2l5)ef1V?S|YvpAXS@ctWqLTuUr~utqFLsoKL_&^NV;q)SY_ z?Ht1G87A8KrYUKLOZW~UZb6Ywn^+Ony~1kc^Q0(;A76M|*!s$q4^gA-khkKD3f1UV z4?krq4Y9ty2{ip*{scYt&sf_ES&OVuZr)<0oHJslIQ_Ie#5?F1hZ@mYR*h`y5T>ae z!cgmNe3094ryaCBHJyB(-W;tHlvnT?sZeW^OzLsm9gMA7jit{wA9D2<2xnWB-|Ng%8_s8f&mg9Xb}Xw#aq(M zde|vfb9aM)m5b|SNDTr##u^QYg57p71$r@#33g-dldm`gp26NB-a(Kqrzu6)lP=xB zNY+K#arYFd9U~v0fj!ApxdmLpPtv6s8K$2g{rUXQ)zT;!WvY^kvBujFZ7NejI-#H7 z>--vGAMq3O{r3$h+NMo#fPL!|K!z z6ssAg-QBwevGot|$ks(z;~X(gCt5ZOb#eN-x&~EB{qOi!EIUT#?i=AM)NB{i%HrrZ zPN16n8oG^r{oxQD@w8mUBEdYmm%UERJu1Q;^CVYUvIw0hFSVWX@^T>xOztT_&ew5ZXkt9(U0~pB^!{g zg3Z!QcCk^eU!TCw;$1F~s@ zlE&HQSUNcuFoJFNNlcTUp$XPmhBH;B>B-l=1Xf9pF**m|LU;3E?;s!hy9YZXoU0Zq zm+oPt8NL6^Q4+8GU;R^zqio3*Z(t15?;$1YmPmu01=_A+qYnwbl_h~a*&mwk&-7Vw^ z(7~mV>6A#mdJE{{gMMh|djgvy{ckqPQHOwP`39a{f%xw zNPqVx@j_ic_c)Vm?R7M`C(+g}PQ!GvCE?Bs5Wdz-<1|aQDQADJBG6Za(;KK3W2404=IJ4d}hxNnf1rq41ZPt_>`b@v+S6cJ(5)eqzB0(}qv5`=2BT3oNF zkKZmxuRt&B4e0Cx>oD^`sTL^kEGa0cMxIT4k%C9q8oqSd11#88lF=-wUgm%MuTXLM zfvb1zbE`PpV7404t-y$D0MsdGhhFh8)eg=yJ$Fx@v07T4LWHeDxLLS!Oskl9wQ;_D zS)zWBcY)q3m}!D!-RoU1mwP~qILPytpb2{C;MgyBP$x(d^}e1}aux{|3GD*ri4yez zZnEECZXh21Q-3dSnQD%Hk!Gzt;8(iIJT3Z(OZZ3dB(oiCiv*V8E4Uu+8yM6>{mda& zrqM~dZ?ZMgZUOHVBHWc@n6IaKVjrh+=_-+V;>WWIdb1>dce&n86xJ?CFo!VRT#7kg zuM zIXrU@J@58RT!pUeUSe)O4}>W)ufaIcO_B# z-)j&1(C%m6Cg5k^aFj!wWz$%x4$e`SJHqKlXoAITT|XaCpezF)_i*zqOgbX^2y%L9`-W*E0IweDQkA1lG|g zCzc81Bhk7ZLHm$HJ)&M(Av=Nn{3F7JeuQDHQNkc=hWr*-pl=;_mb6Gsv0R}7`559s zrQ`|hI|0!u^-Kr<6m7bpT_oM)f2-flA7zWP?eA_IRjQ1??&HPPYnYC^=^VUF@(eo0 zNV43^1^RpmZ;=r4^#sYsi=&@@!Z2O2yh5Q;ejVRFqFQF0p-L`OALiz3h;39a*9x&o zN}8sxKgD;hMRj*Pu~2Bn$X%Nu76bD3el-j!tO5L_$8I1wB`KQ%!2{N~~i8Ytb=CKhb7i{j@c zU;gj-$5=atVI7>KJOdx1DCan*^f1y*qMvVKd<|ZpW}1k$V;qpKFpt(QwTeGM{<;-`UF(3a*7mhR~f@uK$Hp##-65;{+D#-EQ{(n5HP^6vRA!z2Vld6*d0GCV6u=sdkUDEeJoD;9! z!gdR+f6g_JvW4GDF$wY&?X2gNuU(;uG2Z-?W@M65qTVLn&WLevfy%Jp;}Y!HAaQ}b z|7)fjrjsoDdOm?!#VwPVB~>U?Ct-*L zT*4QsdwbhOo*)Ue7ONthgWkyWvP{HS6{)A{g<49qMfx7X9et>gq@2mM=x16aY3BEH z{TJUV4(KgkC*DS=s#*u^jeO!5HOWk{mT09~{cHFxx=jpgcdfz>UaI~+e5nT2Jkd`d z#{rH?v3;^a?Md=$Xxa$oS)aw9h|spj~}=P$7t}jGE8Fh zP%a66%+O7fG>K=Ko`LU?Rf{7YXcZPI5U+&T4zYRoF-={-wDJGT{~qpi!+IIUIpotJ z7O`)KXaR1MG?LBm`>EFJxX6bKByd*;m}GN}YMi~GXVZ+b#a1a~-#G>cINaUAFZufi z1ZBU$T@+|& zC;l}8_}k$x=W>n`YhS@&TuAxi@41IIMz5E-iJhn85u{$= zV{4k!ET>UgA`{}=Bd~*%V-;<3h}a~xL0BOU0_N{!A5x(Ne5zW9w^*$q_)RaDVV=H! zhV2a)>dGz<^L(9p`5Oi%F|8K?V~iSd;zZ$FHqnZo}_HxSfxq)Vi_Cc>Eqg@Qlb3t z?CQTjnk#3M8|ag+8{{@a`}#yXfAE25HNx@)8v9r2XOL0ZeFWNpT-6(pL55n9YVk30o)+m+i*TmCKs#4&v&aIubPfFF1bsJ; zRINdVpT`Z1R$7J077ozc2H^&}QykKHjl5BOj&`*1D3wF3O2HMXPR82jKjt6IjdO66 z12{0r>=K%BIq7$f(JAs4KG%R7aO`8pC?CH`${#*YZskknI&W<%`Z77-4-gGF+WFf9_$SKnm6W2t^4n;~ESwD^5ijvqIoeKIi%knI`N*V8%JH2D!6chf$?B*iRAttj1)zvC5nnIzgK+6L^2bScg%M@O~z zJHf{@nd$-!%%2nVGSwb_UqcEsNS6CJ4^fd$EaSh)(oFMoLp*r+ogho}>f|twI)&2r znk99zrhm;*(96v-Ow}(}e1q0ZnIf@C0RI1cmmsq=Fu)H?jY7n;Rg!$&eN60iixm37 zL5@?%a-(zDCj|cX46S3-3rMS2{O>Y#DkY@r6Qp&*#AhY4UjxS3PEZ&I@wSpp?jVzF zcOh+q7TM*hW4}@jp`MY=lh1buCVr7ERWGrLUZ7>@zei#kGK)M$U;Z^WNM`?tzgxft zUZi7~TZ7mbbDL66HdrOqn|KU_S%YLxy3Qq*V&gE5|t9 zWQ5ffScy8+t4I?^n^e0=b~odv^=eITm(9<`N)cW!558XUCO|M_lv=spG`cu!WAWD9 z+!x3$Aq$OxAA{T-!-H-9;CJzss5nHyzrEgCCDY7S$d)PLujgw|Q$K;#N%^`lPS`~k z$*Gq5_@$codTC`}LUDCsul_1f6nF5@FAS4OR*2^g(E9nLnmT!aKqe@(qr`iaYfnJ{ zFB%1?hc6(i#S-;lj>0Vr<8_ipC`x5sKAHNIb002jqZY~jwf=beIEB9dm1Wq+OR5(5 zbpsFhm3n4`**tL*w}T)2(I)yDmTH!D=zaC)=$EeH>s%tegwN8O`Sl&^C8TQ2l8}$R zy>qo+fsc_j3JBNO2aHn<@*=H_^IZG~nOBIVi=v%|gmlsx#LW{o2(WinDeDCUs;%Sy z&>!g(_SGvutpNGBQ}pf8(HnEi%WsJ9JD#T(;RXPZYz2U5<1~)NxErLDsAnBu9nemZ zuOwd@r~i`a?#D44?3JeH<~2_{#$*vo_9ND~kCA`IB33<@Z0rRz-p)0AkVmVaKn>^# zb>;O#7iXH`EgaGOKvm)F={wdl7c2BY>(1*T#g5n;N zt@8E;bGbx{aXiDYiEWTZJ&v;ud9PWOXv$STMH_8XC1)2|C3guAbCYDIof~6atO|MG zD5jlTC+X+$FZILiYot|563spQY@=KQ5zbwLo;nq3)hR;jRkR8^pPK z9mAk+Kb}p|z5Ev+tq?j;`60Ihgogj%fR48QWl`3!I8l@$gUt(n`6lqQ} z*DA)_P0}^;Hi}p#Nwgv!4|9ysYNw2_)QZ=Nz0>YqSl9$v5x=>;etRs%SIWdYID_{368*h$AfP z|tM9xW5LCu{H_KlVfjc7NMVnTc}jA^uaw^L?8mF+cm3}Y4zeCpbc{Y z_qV8~u`T@i8Dv}I)Oho1#kNuC+wXk#{&D72S~uT8YS||Fq7OF{Gy+59bEJ!(4qe^S zjp;|#D{#MqU9^bz@WoottyD^mGI8{07{;669u=tg`z7kn(;Y**g@J&g9E>m>fwA|X zT^JP(vKVG^HUzq&9LZNAA9wHkj1I|7aW6K!vg^gik-n_SMTc`i5vIr~6sk#Mru&8h?3ytz0t1 z@=YmSr%lb?KB0{(GLC_^ev!5=AEBx!rwi@k26uChkV(` zi?eN(`V*#J!X*$4K)jr#zd>T0TQ9=cHOYc{(INu;47lpz6>n204Ry(nvZ+>{ zMFLNElGzxe?6-Y%+o&4pkGEe^FX8nIo?g}nxw{Ur6pLu4ckr0HQ%t5s#m;(w~W`vZIse26!XQ`i+s6FkYb6eZcyw3X`HfF0c$(h#x@-K z;Q)zw+9tA24DLdr6Y*TO@xT`lQ@1ce%xW{4~BArIa0&Gjvz=3UZQPv|Yy!V{An08YR^8L!CJWScYR=E)YDwd^&Xr#QNdwE!Z~9(#QD-?(g2ko2w0V zw@9X1d{=-MiTc$%pX_LOL@Q2+pJ;K@|WS910K=S~vtM!qKcez~Gs0R1@Q*e+4A z4%pog8_zJq7|sgzu8qIH&ji~H+YkH(!B(C^`8R-Jntm4L>NPy{%Oul1xN3%*kHA2R z;W;$+eufe1<>$pLGq%Qa^g;FoQqDG?K%70fO17~va=R3F_YnIa=Vg*k?m7vH`ajs; z3D^e$UA;Wm1_yZ_QEYrcz}Ti?j=8$Ly_sgqmDY&rIJQyr%GpLobB%&OXK<8S^MO%pT4ja;Cw}3s-xo zpj^l-lS=bB{4{}c^cFh7 z&qf}JhGJRYaMi*Pt4euLXpXLCUXl7G7GLKzGKAem(M2+m0j^*x6#nlC z63jKhcc3@PI<~Q80`0mVI7@_;%8;+gCX^E=n6}%C{(Rg0J->ZjsVb22Lg%Kw@+;pw@l#Z4{~ke zV(lm}$WJ$)I+Scj00CF=3lPmoTKfdBvLvuY7XKSw`j-z{{N+z2zwO*c=Au!kSn;uLK^ z2l+y!e506I(j|PV@dE_Ni>EK!pm56$w!ixukZ6-=(_eS*F^%+azaq&ORB7U_8>E{h zCmO%JuH(?m^Ry0eY352dSj1r*&XX_>y#ZQ;)haeAd~IUw+Xcv0u{Q6(f2}QfW+~-L zc&kR`@-;YX4N_sQEo>Bvd`%Bv+`aR}#gaM|`{+j~5q2Q2e@+K_GfeMc>{bK0Z{wkv zM7?6HhI!thA13)#$k9tyC0fJ0sArZbJNm2=008z8Lu_Rl9J2(ehBme>Mf1D%mcIjpHat*SH zHI6b7t+EZKn0zOwQ1J2EM-On@#^LLDIvzny>0GdU-y@B3{S3aI~@nZjl_wQyI zM>Q+oEmdX}h45$jm-wXqsB0p_R8v-YCo0UAW}|0%slK?l19x zB2~`bFPRl;_K{cMnI^!y9>7jPc!#dhx8YvkhnZxHJGlRv z?+{)iD=-Xm2Y%}2(Ind>p;xAnb%wfxBi^D=+#tp=;u#OUjlS;aKZjZ7$N&KO%2=CRbtpix zak#ZbqGg7AAmi8|>kxaswwEix<}sW^m2Iq8FXTy~2Ky-bfpRhPL>ISA=`Q97;~2x= z@o6WABxOr3U}}_h(Cfs{K{m{>fKpYs=X~`MUT=4% zzDSQ)%W^r{D)Kpoq3zGXR);IW#h7sDf=H?EVH1 zu!}rET_%aNQz?nF;_gW^nx_B)oPWUl$v$8m1O6yc|1~7a_81um@clFDVTuX)LX+q{ z9u;{Xb?*_3HgdLc^pCQB zICJy-8p+wLo7W(=_Zj+yz0WMnBM9N?0)cqmDE;G6jzN$m=)>RhorAGt;BFzMVF3-GU%y;w`3 zs*lqkd6`J9^Z|^&J~AaAQ?PCxta_?qRTOjb#HIhRlghS82dkoO?g zk?31SX?<)Ft_Fo(9=h>3TO+K(Eo~BB?k>@u9+~DYUTmWs!e1k61y3MEI$)n#1aY?_ z>_6Tj9yAK#|L73*_rcrj5#(#mQVFqZ6Zjo}h@WVkVTAZc6+hjQlm9Cu!8X;RWlV_z z;hIT0!dapP#W?MVUgi%%!X2?zyU0z#F8WpSH8SPmOoJesLp+%V>d{a;U;xgZaV*~V zc?zU!%dBLRZ(?^an#m|9I%$8Ed;HtBv3IcyG91H(SV@;ui(f!+HxbSc(B3;clFU$! zgj&0J!yJJCd7AI{`g)0CRneyR6F>8{)4NC<}II73V9m4CrTd z3f`c~RDeD8(t?6gudNcAWwW=J^P!xm)J{^^CMbL}i$nQo8<%bJ2wun6D-iBeFLDmW z(mhW1dw!{*QM6*YzdOcZh6Uyp&7@j+fSYdC1R2zsWV=)F2w%RcQJhWG4g4AA7%2b> zXDQ7%!8pZXiH3aIFn*cF+v5#hHUA1KTb*lIxE^iaJ_z@Sbo~LkU2ufDKw;xIPwu>% zblW|=Xe0SbwK&jKp-z#pQCg|2PI`p%cXF81UdAY!m@mOj$|Y2Dh{w@x=4sM3F;=5A zGj1TcBO0`VLC9*eUAAA(X>V~z@=1yf3QT+(Qg|o%qi0O8Hsf;+Mrp8e@L)x9b=f?&r7_~ED8Cr znJ3b*MeM8pPwaJavAPp9o#YO|1|`{&D@cJRyzjq{KkMw%OSqk%w@hWCqfoP0y-P^7 zM6jEE9O{8xC~LvJJ8p!9$!_(;Gy>=?1A(EsOZy`+tNx%(aAvzbDpgm@!W|%q(7uJD;hSU~vog zgZ$uwYq)hpBfm@)?f5jqFgN2o`{3*S9>xv4$~Tq{vKho1iJB;D#ET4z!1tZh$Viu7 z&Sp7hk4TFbc<3WfzkAT%`~Lt=HQOrW7I1{pCwH;03ub zjJpOx-Tm=|-`|^F4ltNijFT_TZsOq0|aWs$aLm^~~UUDk;(FCqw9zt7tV6Eq08^n%xcdxTt_u7v>~C|gKP=Fx zRgP`kA^!OdYny0Iy5#LO%$?{*rqUwm1cy+ok4L6Hw;2S>YvZKQjYW>znoaN7{QK(l^9zLH>flcI41{TS}|-^ag)%P0-<9%GMb zbew^9s#Rzg3x7SytV~IyIn)XI);_}5^ZgpRS_I$S{XKnqxoo2<6mU0P12YVKJ$19A zZ1FZ~rC_gvonJtPS;!Z@$)=ca_W-?d^(vQU82p|N06ao#7k~pBhX;6zwmXL5tgcWh zRCh6}m5(y`y1zc3puasr26?5tBq5=0nyIM7gEoK*xezxL=CXs z|H_c0suFD&+dl3`g-%m}-tbHgQY4uEp8xss8q^^S`bDrYSLYOtXP`hH?&6#LBK3ES z9wDW|G0sBOcq5lUhsZ zq*^3vx_Iy3oI)()!0yBBf6piQu92-@BGrnv{}bCZ&L-vusbX%jxk8CZ;~s%an3eAw zl~|o#&M{_!b+O_l3ij3`$QkM`rdaI+8P0)^n?nS_UK^iwP6sd3fM$h$UMJ@crbU8A z=?_X<|0t8{-#j_{Znlx_0<1#;Zg5xb{siA;zwvgZ8ehSs8xF8T-OW)-*T_^iioIW@ zNL9Gy2!&}>qFy&!p~5)TFg?Z^_;rH5mrJEYwRnYCu{>K>Cr7QwDD4GA_8Y@^f(2ja zKhdA37xu7Lvx~DrBh65&(APc2S-N5mmURmLMzPc)s$HO7^c-HT*e&b~&dnp!O0bu7 zG{kd=u|<%4E7gc$M55sYSwF3tO*a?#VV$g4uZLj=d5rW@(XT8x)|Iye$YP7&~Vu#wlD~KN#pIN~C|yi};g| zE1#rmWm>m!V9yJ5ouW@cLOcsK-Gc)>J9s)+!5@CrflmJb5BSccnD=>&Whv2EF9mf^ zylI>@*t}IrrWEZE`y@>lbqDem<|5KV8`SkW@lnoAZHxwKj7`CI%uTsU=NO9FZl=F_Zt*`Y!p=EZC#OozFkQ4srR1CJ8xUV7 z{(6FibMP3WW7r|;J!B8J>^Fb+BDMFf?nSa1=|wW?84tg2vfMox1|EK1J~-R-6FE99 z!WIededLpL@s_(-oPCMr|3rNN-~y`5aG4}qOSXl0CRd4mJ;IrK(lu}w>3^H2Dt8ZveC(^R93gBzdmezJ^!-RftiIURn`DndW@M zW`_jkn2`B~vkeY(54?%PHUa#|F|>du*eG9ff}LW++5>%uc*4+q{lPBABwwX8T^C{F z_t_T!00g5@JWtatGE7fDN;5B3vPAa^o}(*Q0(KSZjs%Q7kEBJIi;3kX=btWt$&-7XQ_KI~1dX1zG#?mzMW2Yw%? zx3^6+)Lohp;~3rK`+8p?uaom7wwp(_81*p5`VNUdl%^%p|9ry1aH?r(|>>!@g~%scvUZV3r)B18ZAQ| z928^Y9^5LGV3us@3A{n%3JTm< zU;f`JmhR(zyv;Jy%>)6E%z?aleElc!Kl#kvnW=-dNj5i0wLn>+s+L=)xJROx6zMj? z5$MX@LN%+LH^Lm|s!~=gwM6p*o?(*c(8s8mnqvDU9`ugBze_Cq%PJ}A=^gkA^((ku zqG-n|<`jc{>I1|A`afBJRtO!#S|qiz-yp#N_OTjC2dDrr$ma;-Z}N`G)dHSji2L!z zU{AA58r2qI1#(9yqMZ(*2dJz=Y(tT57EuMNvoydr)w0s%okC30ow5^T1x5&aw;!Fn z3^Q#){>l80ws!YtANY7S!i=|Z3BN$jIA)O1!H;~bpUFDJJn{-GRU7U=zL09Hng31J zCFtYX7~?9Dmk-*(IK$Q_p6*DyQ$&b|Fh_`oKF(w0E4Wr6t-KlP2Ju&54?nTSe=-8T z#`SXYG-sM4p9Fb8w|v?j;5AB~r(2`BgYk9T zz;pC#5f<&h-N0S?blN2F2yT|t!#BW)vkn3BX&Yk|{%VTEuuQ7rpXiVG>n{2R&eI)l zUn%hb3j91r2?f$3-c20pM856iMmtNg*v07@_%+-p?HYK29_O61jeX&p^gMh&SGisf zKTALQ?fo<4CCcLlHr1qXXT;YK>s1```A{FGp%bWo;(w&|B>4-%1&mOKQ*?ljR9Ug& zA;tx&L;%kC!Cp*T* zJ_7rsdIjAA&2WSGA_eiv8m4=obOrD~;4_ZaOZ0MGd*GsjWgF!BpW0fpP-L13l%S6sw5E4B3x$~!B0PJ8YO2M zjnZGj^|8M~8>L@Cs#oz2XeQ<8B3{M1eA)nds*q_DD^|LOlCNDM9Hj{K&M<9~Vd(L2 zO){{_x&EtzpZ~I*&;8uYahF82@=OX`_&MY)E>ns;E?+Fo8kE~G1y z6c6yHnAw(@I(S=osvwschR7!ySmLcU3h732--c+AuPSBIEuy~0n&XZ&ilE(7&ruKl z6Mk@DJ43UApGT&~8g+<+QG$FG!G=T?IOq_MWJQu`f<-Ue>+=(sbrRCy0XontmP}gogXgmJ^4=_o-_^|Z} z^QX5r+2RnZmk;*NE%YK8?bHU|)+ZmYLiIYypP28xhqh5}0cRhJRn-e!17p7cy(OCS zcYL_W)2fwv-|0O9wW7_BJVVazCAt^j zATQowe04u@b_n_eY=dR9WeC0MPfyN7t!_NQ;k^iw#iiAF{# z2I-7biwqQ#k6?|$R8t_}*U%#Enq@;&F_tuQBixS=P^XzjgEURjT_S;QW7HweeN4eY za};Al|7862v(rwY?=y}yh$EiTOp`2U>w5YMwoTC@A1js@s%PoZOk?aZkDMS;%)15H zMwKZQtKx0cOWZ--LhIx>2SeYIEbm}T*Qk_m^$NB*hLJ6vB6jjs%X|%K7x<%c{GFA_ z8&-VYL_G6!KEV9``4Z*_2H2~oXM$1Amn$f;%|7mZSi3ONKwL*i)ol~^!NN|Q?I~tj6p8? z$vr%|YMhN>4#j+o<6(BZ^;W@R8Tk^X@kIiW##-q%DZ!Q-aO87>wY?9oaB3C#>f9ri zVY~R2L0RT@A%Gjrno-^b!ckU`ySMMWx>%e zTdLGlC?{WLmvjl0rJ7~_0@}y9kCLabhxvQ{J{ka!sV`k)nw+CE!W{BdxTS~NAv{Ni zaZIsXrG%%uLP5F)2*5V*-rc2{?;QLF)XAq(f_nH(c7@n79_i!=rCy>@Y=+u8Cdzh@ zjb!=a!>B{T_NOYfP*>eFlhg`%>y)3^hEdd0EmG8TF5ZIebYnV1v=fhD!kw?7FE@-s zzvl}~GLCuq^s&p9csp|rs1=iKH;PuvE|d9uwTW(#tLKig9;1f45^ow{*UPSvE!AW1 zjduci%2K1B)ydw&D3vJIbN985QmlhLvx(ipM&ButwMk@n;|3fS~Spy5HXk!Zb>{ zRHAb6VUP{!B-J?H66r*`W`_D2_6DX+@D7T%t5)h3`k&~pmz8cjPD49`y8#K(&i`?* zM-cp$U}J<%tq2$#|A%`3$RqwrpmVE8a6lJ3_vrIe3oqPL@2{Fbq)II-Uk?bDa*BHB zo4U7eyh#tgpT~z?(AP}eLNza6_c);IUe@39TjU==fu7Y1z(MO|xVxw)ETWr*iw%*E zLYy>n5>(v1dnBf4(u{n39n#ArB5dY~4RUYM=7}chvh;e{h}K9pcCc!tSv&ZeTf~Ar zX=jF+kKwe^YZc($9-(Uaz;1ree?koKs+C4OpqRIe-NvmIX%U^IyoNK%`++x3Zz0*HhWp#Yu>S7<6r}6T?fhpF{)cz$TS9O8LJUT~e zA3ez0FH|EF_?2Zc(gN!3t2f=`*MR?xZ<73eGu%PsSE=gf*a-Vw+-^Rp2C0TZP09tu zO0<)8qB9gA|96>mgcSg-KvKW_CCebwuvXT?AL9tf0$3;V_2TbLu#~M0aIaF}>d)2f zwEK8_MfA_erY~x9~@K@2FeS*tnr)i;|e+qcG=2&7K1vtKe zXcf?{na3Iyr5HHJ_VX-|+`=&qG)jheYG<>JO4T2u-+^Nt2=;f0GWInK46|FtJ;FFd zi8t}K{%?Hi0?n)=T&%4ihd>Y3DV=nac-~>>(DxfdTz>9eUf}OYrj?4`K-P(Tg8W@; zwPnhrE2yW{Yf4qH_d5ic`&~onM>HC~e6P|BbirA+PVV8UROA^6vnJfn*V;yX`TsEi z&yjB6czPk8ZDK;5unvmVQ%s1~vkWE}4Kvq?2(}g}PS7Ikfsgqbh;>k*q@P_QhrbQ^ zv_+s&R;YQ3S}m_u@_w^WQ>e`_vs%7Mf_-R?+Q)}rt4blt&M~Y^WrOg4y+VWw+>=rz z)8rBv*RWjaSHEkFQF`S%&D1`TB#SF#?NqH?+%*u8IytUMsC&yF@V6Uen?xeDRLjp` z5HDAtZlPV`L5|#gN@f0D&`)jr_JJ?&hB-%RQLc}0)icxeb9Me#`&A36*FWZfy#_nZ zFoiiQRPwgS|H5BlSa9`1Jdbe1KEHt;r+ot%WqpHv0`2D(X>U~c8e)*TjpOX|&Hw!| z#U$H&v22u$WjgYK#82uCcPITsx6lR^q_YKb&LQ8goBuNgH6iHh?ED@24t0lfNTvD& zg?+3{&>%%M9AMAg3FgbdehCRG_IwY&ze9L6Pu2KzZB}ryrucpaj&C#~aVh_+p z86v;rYP`SEPx*R9+xGD0=$)bbul~2lGmIDM!n@E*AYHqM4RJKe{2UYURiRL=LZ=A# z!0daYoTuj-$Q>;H;T8o?H{19cKK3!^FQgZ5U$#V6%%!wxJsoYCl2EGEO#zfjWnau~?w2lttgT!)9FQWw?d{ z0eN@RDLBX6AT`M86tw+*nBP8JCw&7u!R`#MPOd|EjI~lF(Z(R5S^)xbi6B>%VRoM^ zTLb9=<}A+gpYfT;?x8ybeu+eWiMJwO!Z>P>!rm8dP5)>cO+WQ1744)$;P?J>4iV@W zXL1U)jn*h)9`p3eFgiioAk@wmZvA0FxRGQQ>eMWjV!}GY(|d{v_6m2SQuf2bDO9+1 zne3nY{}SN|leYzbRlKuIYnxcKu7|ykjlY|(E7)U};Zu6HX4i)tourQ-AgQ0S4IN@N z3pqxm%KiMaKdup;5VwErbSqtH6eKagqc0NLB^4dfN<0U^gUR^KIZ zoy0Wf5yUMF?4EkX`hex_qjcAPNKleYz z5#q%Lp+r-oq{SYFLaxDb zHH;IQg-hgF21#Ztqm&CYv)0*CHxReLb5yjOH>mVa&c08OS$bGI z#A~LRO^T7$9>Ip$Og$9}=Bbm^c*~zkBEB$05|kw>Y;IlzsDbEM81;g z#ymtlC>J5xuhPoWKg2SNyMuWID^??4=bGqZuabyxeEDq%s~5Y6_=LcpeNNUZ_#FEw zyi>AWK(~N?+BqcLrCk0LA;gVxLaY_!G02~xKk2it5B>Q1d){7mFO-v0)W65i)0V6c zb@`M!!Pxa7%V3oR6vR4~Y24R~aD%s}NdoC2%iwF!1N0>#C`gp;3pnct?45by6D0ca z7Qx5#0wXz(8ik0L`_}jX<>cyMyarOY4@fgi2zK8dpM!+P!XbbuN zBz3itS_$^Peb^MMdLhwfftqA|?Lza!0Iysf-u`m6mq+GN<=h5^Q8uNLQfc7aMeN20S!yq8|InRPPG(l*ZBKfw77q)=11vrA%U+jeONKBi&G=xP0|r zcKhzwaK2)EWt6Knw|KlD>-C7yn5LOXa!7mF0(4mf*a&2iQeP4_TH85c;_aRgh4 zmo1`yH3pnRBJ5%Ba4&e<_leEFQ_T_{>=6=eEfaQrD3?pIlxir@{Ls}Zu#L|!0{rg@ z=I_44oFFgdnypL?5`o|RgUVFz&Wns#M?a?)sc<*PJAIA<1>3|@Xx+naX8+K>N?u@k zfj~OHiyv#UO8npG=84Q>RzK!w3M}F+1D%pB(lvK*jS&+(GY3}c^O z2Kee^fjz_~wnlw{GSG%^P(IEo$$R!f-+%j#6EZUZ*Gt|3QfWNy)N3!A}8U7pp2t&I79JEWqECK0+d*G-1HRL$^)0=jhL`xrUsXoh0 zfkujvdUCQ2)dJK7fG^N#neqa&L)7}g$^RPGG4TSKZ-Bk`hh~FvnXXESUg{bV;TGF` z#wY0-jCIhPdHNg!++%?@sD~&!%`BAD|HhBDeT2e3CO$m``(XhBat%*AQ>GGcrJWyR zerVr7Q_-p&f?r&J)*zeN;SC!QgYF5;};Z#ss> z*t80(mL{6F3-b0J&QC)-&8Bae$V3Mzf4;7460(s#cS3AlR{3)%{<5VZH22Brm@Uv~m^I ze6yk^VTAh({V(6ex|0mP#LAWy7?mmdxe0b>YZ(=HaBiX6!~$>s03GevpziOmhWUu5 zRS5Y;I>)epaxC83%cGJ71wO_)Nfqr-p?ZlJ{fTy-;KbD@`O^vd`(4pCjjTO1_CA_f z?Yuv~g9QrL5KxdGHt^S7Ocmzn2iXQ6>TaN7-LB9k7zHa@WK45{{HAFhAS3KZCj{G! zlY#cr%*@hx#PSW4$-x|Qb~`5BLQAwrGzj*mYircsAn0actqib(fv8oT!NT7(C{@es z5~`NfiK`c4A74Q$7Dd^%3jK*M+B{2pjE;HqC0e1Sp7#c>NCWZ-$mL$7fV+8yIK(7h z$}lp>3Uvx})5)WrLpuSUL_2L_OWd&ew*y$K9=#XPBg#hkn33)GS0f)6G3W zL%3Qcd3QTQ8)v;lPQJuCg1I*8R!=DwLr@z#wNx- zBFnN(Osv`{t46(2BIxTd&v*AiF^Z*3b9bLwS<$w%Pu0?(&%ir>i9APf3pk|Q!PY8H zGeA01uiU|)o8QO(?8h=yq7mj0VLi>q)JL`~+~VdBcitxO0>$?e;T`KZ1gKezV~9#_ zl??OPJk>7lF76OVwS2fkh4wP_5y}P*#+h3A0H>czu0g)>Aw1U6Eo=wRIE%+O@eaEm zjiO{5G=o6?Jy5L^ZI63%g#>#dSDI!DVL2zhUUs^}x(a)a0n z7}chKK)ho&@7w({(F%RB&LV-Y3;GT9lwi~K|2;>fSVX;;W$O?~GW7k*GFPGWIr^uB zUfNHUGQ}dp9Zal4nS8UHCgBZSFi@_ME@s5*Hfh76F?x$|_h7LWolN~~(#2Ecc!Ob-#n{ys3ulPjxb19`nUnESB}9Ya*ZO;H<)|bTCrx#LyRNTQ_g|k zM^1p-0ald)#IuM0E0|5p0hUkvDGB;0)W z`1+EqQLU6_#MP@@{)pn~J5K&N=68J3)jO0*ec}b=qjvsQ39dfQcBWC25|O4-*+>_# z`)$HK@*}ikXvhc8@Lo2f{5t95cd3>zZw(*X1z4xvpd5nN@!kAEUndxx!nX-Ohn5;G zP|8)1Z9QFI!*TX*5PeRS=xpQh_Q?38RK?eVz3=besD24^hzWMSO!7NE?JUmOJXJe) zrnyROw6#T|i}y53wq`FMPj{q!skW0}gDhvCWj6eA3p4HU4~sZ6&Mw;o!JYyY!rca` z5L@!)5f=6Vu3;~)dTD>Z8A23r`_cOi=#r!~7DBahzt>N-xsf!58e8DJ#}YGiVj5m7&{A(lp7=)jh%KU%5Xax+623^|mw1QpEF;D?_D0+n!(`wj zcZ@mSN}=k91@bw{nU}AR5AwNa3(ai272J(P(;q(EB01t^t!$$t(nX7CC%-`!!B(A& zd^OY~f2U*EG#$};jEzCo4O}O`pXVW_dE!0HZ_D&cq2|YQ^F)&r>N&~A8CtCZr_fW> zOT-QV(v?nrhDpu=5C3U~cu#N;vV}d&auM=9oHZ|xC)ggLPC=y#(}V~o{fr&V=l|~* z+cqZ6vRPOwsYmKdxOkVpmz!@l$1k}Nt`%IE`$a5_yDN+)kxJ!NeENCeW`$~;ZOS>K zbr=71!zO9U{Zh36=U6NBJ&6vVuS0aBqPqxJD5k0R_udgKBh;%BC9*A_$EPWe0^l!f<*+v_6i-tJSP4f1;dNVIX zJNS5O<{9RN+NBr>4fAwqqzJU#!D$uOO5}Z{SnlR-5PEt?vb{n@HkoKI+F*Y-ijDw(HoSiw^ z33}|EGo&QzQT7IrL(KPE*D!RGdOyO0yh2PuvCo$1S-S|fD`Xi)q3$QS63sdL`q|)M zbaM(ctE53e`xy0dm+7?fm5Sw>`0L(Y`Fc4AGK^5p7$zT~g1&C!2fGflee)J>rJVsz zbLObuZ({HN-v5Gauy>V;g1@a}6-v0f8bxHwETi?(2Uz|d3RMbKFW@zD<;u_x4&fxL z36^8b%VcIrmI*K5NoK1g>c!jm08^;N54@coUbGXSAGloFj* z$guR$Zg{$Uj_VQ^Zq~@R4MBihC;4GWHEx`dWmv;M$?I&n8)G?wzD6Qj@^}62RjfS4=k0^Js*@YxhGJ$@fc&AHcv~YT)xIGCB;CYTBLcNZj39=oW1RHszpSP2+Z@wdtZ+l z(R%R=BmF}1jcY`^P_Sp~=->O_IIT?8Bp>xuv1*qPWp4`y43cx?Yaq;N)aQ0_sQVBX z`Pw;Z$G|qR93%GLCZ$OGaL1R20FPx#>oD-6fEb?s7KtR|G$Xm9bF?|ydbM2jR#6xK zQ96L94gNaH4(v7P>n?7Xqd>)1d8i3COZqC5F5y z)cnJ4K|5`fFVXS}V;(k%!Q1=pn5m6(8exaOlVVq*%-#<1<`F7fF5dYynrJc6TD8zT zj&6y$#UgW`$Tpg;9~OjfM7G>2ypvy{v_V&*1?rl7x=hL`wT|x$TktomFX6XHtsm0u zV(rN%q$*rP9it$@6{}i=_mR~z|A--d=;R(qx8d$3SZ1F5l?u07kx{S0-Ei?#SD3nz5E&s!gF9_9ot{ zSt7;iyTdiSY>8p^B<(cIHDZj}77E+CL%=ok12Su$QnA8MxynDQ7?3YOAh+@3Tv-S0 zLu-Y<1^$%6*$r}O`9QPt!v^WRg#+cNQB1OfVb&l$!^rz9D9jPucaJwHtjz>-qXLCe zv;&VIvN^u4T8(i*shTo5;KZMF<-0TbalZB_iJDop+u8b z0pZ5)3;@4hM(L7`iRMe>{vPpG_7R4eCMhJVJ-m=lR`GA3%VfpsCMmkPTLkZJjnfRW zLR_}-p&y3uoOb2_>znrthG|lrG}w!MRI>2` zsdzgW#2`2LTcvoqj-R7X!~{*A7N8*V`$aX6a#d)!O}t2=mTR0f$U!p?d$CB~Cm3rN zWe;{~7Zv50r{2$vda^`jlr_eQw}yQVbyKdhM2Wt&hoM`LYWe(NnmNiPRdI`Y1u@DW zZlxFcEl8x#JXxkG(*W_}6l)7xv2KAvJ+%R2f0K0d>Hnqi*~IwzyukUp zWa{&`)TxXLxd-s{Hi>Lw3AL_~-=bW>Op^w=eTgzoQ7Ddc%rz3M%T>XGFveNVtP?Cfd#{afsy+I?6WN&?{hqzDk8`nPehcH_Z7<@R0C4 zu2U2UqDJ~C{U7-G+8o17bHN^HJIE*QAub8{TlZkjL21T{wRX`2tL3s{oiA_mblIvx z12N{Rl}=$wWoMXT9W@fxQK5F&+v{YuDe*S#-0!z0*hd*Rkm4N@9LOhKedDd7>|Vet zl$gdDCdZh!@rl>YP@W*Yzum*wM=X%G_&MONoE2a7f1o_r>I{d@0bKHH#33%mY1#Twc6{A=I>+e#9)J!A z)Z+z`Ku3~ujTHeZORZSf=vb@&CbtMB4A+@eJT^HA()q`d<6HqMoE#n@7i3 zyn;2$m?nFLD&#qaLp}}jnTGlKiZ^8&zCdJ~;w=aG`?^fyBR!VsL4cf~e)Vk@pQJf? zq1Z9c@C?>YT_!`kL^+GG>EX4G+rjhoLbz%X9cA(Gm#>cf%s643Mm7Iu|A*VFl|f>xm+Lf_ipYO z^F-4hCWQaOe{b=_GBM2MLnHg(9g%p|Pcig!SP;C;ASbn|Z|*0s)`_?1U@w^S$VZ|B zFgL4|40Et2-X6ree&4V+3rt=>=$04<#;AUADb*=A3b!Aj3%4^26HHvDZIob}RG>*W^k@GYX71tE$h$^>fox-Qb;5$&fr>ZR=(_}S z^@O|D@?F5}QQ)4{N~G(?ek`!KMWpUms>swXSM&4R!7DZrXi+XT$dj$LOzq|vXPhAy zZxQSqq9C|6Nv-8I$-G8j=xO|vr?NnaerX-An3tz^Ksrm-AY__u_AmVBfnTj-ULn<* zvo&L^!kza?;ci4~9Q3iX(yw~cR- zf^h`-1b7n)RW>o$`>3ar3?I4}COi3y)yq^?NK;H;@4Uak-ix+x;|6$*F)7yOnYHqR zKAEL#Ve4me4~4o`%JcsUx5nH07kCh>xFpjE=M7T&6^-H?)oR5gOFuV();*#CSF|(n z`X(vek`u@brPPlF`i==&#nViss+LJIO<`Z|k*4uau|mHZrNn;OLL@oHJeZ=WjO?GmFnZ(!Khb|{Mjy?_>pF#Mh5KqU(b+udgE+7eD9IRXlvvd zR^fNI39YkVKngUnKfzs*P3CK$pX-%S4L-hNUmc+RRLD_#ze=)&a;aTOwe+DA;?+F> zYrje4W2kH^*=m(qk<`_zQ;C;Xi1oXRJ>qQrZrM}Br+X;qM%fxk@n*h`MM9^@@Xyqf z|Jr`nVEw|6Wf;3ei^e}3<5tL%jSg`ugc;}Os7ZHZ8zb#jh^ClT(#iHi+zwG)!ZCIY zQlG$^m9~(zYakAJN1woyYY=w{Pw`f46o+ z)(o@Lj+>T9*AT3~zy`XXyvsJ$&ASI9UBz1mdHOeakS1}YhZmU0&;I^qQQZ;@gZ7bJ z!&-T)J&5;E_pcysv8nnFKB3OXc)!HauihRXo^kinbRCn~=1*ZiG+;hPnt6N5m!?|% zRQB{ZMip!QVOJv<=qXiEtH`iSvdP@V(-q{JZW-$sVT-?re!BmjWzgI0-|-PIj*+YM zqFis^X=T`loPhC>0Ag5A%HJb_glbi`9pG>gJ`JIKj4!`6a4ep_!#woNS14 ziFc}z9qei!K1cHk^#mE|)*u|IJtKs8#5*9=mS=>y^#qry1@f{+o2x&^NWFl%w~vN$ zJw{hA^6zJu^|HkK#z_-QFc%g-stme03D@LHQBGl@jk0J4bh4cym8&p!*~Yn>o0OSm z^R#%ntfJBH?m=&mX2|uj93mM+7Xalxe6|qE!IYgW5*_Cx^oVML!q$QkdgDc7c|08sSEov1P*TJNpRtfEqcIlqX1= z7=ey>E3}hfSCbUYLcw;l6Y?d=Mwole!ai=75S_el-k>0h7IH8%TU4o zBP6@;XqTC~#X9{QR&ho-1-jg=8s%TZp)VH*|GoVzqYZMAPNs1e81bL_1q;;W>!#V_ zKHK_LDUGpqet-hCjf=469Lv`z)8*{@^35pk8qDguT1$)*<#MVP?x}5nkFSsS9KBh_ z)4LtKFCiOv!;CWZy?p9bBJE1WMk(GN_JO)}-@e9Jw@6D>v?zpre)^9izo`A13^L*Vg7P|Q&jUMiI z__fL&ZUqWo;?fjeGfmfNT*=)1{aYZv1ETeul;*CCU!*@)a!Dm41qBIZwyWa}P(dQN1|Iu3eC7 zzDl7{QoYzH1K|q%WKoio`I9Z8+Rju6YNi zjSuna6j^WxZO1eV^ks(P9#Xb?i^ML1cjWcaI#MHhn*`-`ngs5@PETfO+eEZ;={8r0 zG>X#ou@B!L_OKzGh+IKe)^ zXOtvem1p7SB9~LA*2O}8Q!bsQcL&$US|w|p2!BgE3w`YoAX9XU5ow!m(!rUp(Z%zh z_;;`|cA{POkueS_I(QcgRD7)=4i(DeOI5O-z5(CR&+Q{5f3gfr0a#c4X+-Bw}t zp?Is!_o6K|ax9~0CzXn;Bs{&Sr}R@^zD*L73{iIF%I;CKOy-Fy8B9|j^Qab`0_9r? z*GQ&ZeUEU}%7_kPjLf3X|KbEY#k5ywf@O}N;Nu)gyYMvc3O3YlR;y~+rwonCkNHn9 zQ1_*(+1lY2eXNCgd8U@%WwZPu>lAcK)$>#eJ%X^;)<^;(j?jc_+JqvVFHjABgndge z4tEo;5o-MMV~K)niKRE&6!rKK%+K#0oNV^L@vD@Qt&cG}KH%&z4N)y$qZlQ)d#o}X z;@if~5DN{$p4DoM(Q6gusWXnyF3c059ff?!(<42$k6t7j=GjFI*KigEN;r?+HF z0q%6uSO)-4EaQY&a}Tdl#T_jE_Apz!Al^>w=SE4~-F)pq4&pU{UxKGsr3`290%?&# zr40D|Wik(c`l%IC`0Jm_OC*V(J3jmp_6*O}eZF!In`GhcW*@spsgs|jMBJ6Hu!#K& zc#s$y+IiW=pdi^2@dlejl~Rx!xJ%mQQk`bO7%S5(jEgC%BIRwuZvO8U;NZW6?fmGM zeO%cm!+qR6=$9;`X;vSg>V+ug3^G!jox>Wr&yfdMEiyHWJObm*lfFz)+XoTOdqgP} zLA~Oxi~S{o-4V8PRK6C+xJLTNqW8=8LEpWgo((g5_#Yq0cTQlDe@)7=}UM$|kI%-mA6OOs!8d{`-aHU?ZP(4R$ z6jxwUBLDp>!LEEknB8Bs{|(eS>1+5I68_pF_@``*D(JflSh#(r(H(NVQu?VeTIGtb z!Cc)UEjPGtj|TZ=YTSKf!!A+Qam>SlRj1gjOO*<5P*32J4Mwq(3^p;06En1p%+V#p zG&#W%>~)2dc1EBh(~y3OWHsDDu7+qm^%KJ++c1CUhc2F8iyunA{9FbE&fnF`L^#5M zaQ~!Oq?2uUS~)4UL>hRe0g*GsUu@;7(x6=Pbr!5OPb@Ly z>Dj~N>uUR8T;LQcpIKzoDt3&`-Kn2OvOPz80WQ_&8uUwAzJ$Bq)hFRgm4>^on`@3v zyan9`+9A#{PdCb`dkDz$3N6Yp^`cIWWh~WPjf!#9Ak$xe`+^%m_DF zM{ZF2dEOruNPqVnXP#p2Q?fj|6k+wuj2P7+`9h^p41P8F^TNonEvyA%L?Yui=AJfWIj1wfP ztF(^LITp^LB)d@dINJ5fQ17Hlhj^r#igkTzETHitRXMY|-@=KWEZ zaFMEYsHYF^5%J;{j$&IMn};XW_UnsjqIj#f9m(b`))^|rO#G)oF<0Lt(=D7(FV2xT z6T7$u;Y=;^l>+%EWPdlV@g*v<#d|QXaK2XRIl|oqGV`bsqgq+z+|K{n|Npq+tqIrl zD~!@;$Fxh$Bfj}7RiJMxG&D=8HQYmmeZB>c`7+3f|g;_$sgu;h$+yKr<1FzRN>{TR>CnzvI_A3{nLfJ zT49huJpY z!(U?_f`T;3bqXOL%U7vYC7I=#QcZ86TZY7Yp`GwF%Qr{ZWf~_Nrs@4X_6Ff0Hq8q4 zEb^XK1@wIn8~gADnpCrC#ufC3o?BSS>LH#$SHw&FOXb2KhYmK2oKvh}7P3Xyd%gk6 z37@YWf?_`p$k$0;z|P>YS9^r>jMPeR!14BO;db!SwY-C#p%iQ5b#?OpzK*t5PPJ@< z8uiG}n`Oi`sDtkcp8`=0qg7)<{st=)HF>aU#r?G8c0v+r_ z^iwEj?>FhD`MNm=*oN`9;cfs<8QfjwF|K}^S)eiq>wtdh6^v<|eTcu4zf-XN_s<<> zqnVAd`5epL=i@`P4s)+jWEG!mD*p2nbq^=e{1Ix9qg?r)(bbFFgf4IR(`y1&B*el5@pl6h6_|N#VwLoNBq6u%OL@nMb#e`zf``Z)b zBb0ja19YB_T@=gcD#OMOT8U^2@U&6|&8$R|PF|vUn-Klf zE-vN5Bm@0aoy-p21S8aAlSHNB0OvpBtCh5f2Dlq$c750)sFMkFQ!ZziY!g~0bN(Lt zImxV9Og~$-RH;Iy&ik8m{Rpd5=oSIv1m}QN{D&^!X`Wu{IhgxLD3VpOWvm0XVYXqi zyYm)LcC|~>b9d{qyG~+~;L9i>ys&(85>mY|s%oAjz z5ed< zA3)zOkfLp0z!xaF`oBcx>b45w>}eJ{hEdI9?^7(=M|=(P@Xs(}8AZ4%QPa!8-{u&k zn$Om66Z(`oL%T|nuN~&t$6c&G$-q3;$Nk+IU^t$p(<-yJ{tWjqecd>GwG{ z#0B;)!LpNIvT=(5;&}%z#>P4>^;49cSaXaG+KF(hP0TX{z~3{j3i*;u+{2(iNON%)>ia*gN!NfJdHv2=j3B z{rgS08$ZuM4z!coch~TtP6${29$Ez-(|vq2icrp ^!2zB!Ewf_qJQ!4liV7Ksf z8)f_wAz5vdJixk!@8iCMC0P|}yFw8E33u~Lr0c^xRk1qj$Thrd?LKxd57BzFm}DdE zj9$(+@5nC>;b^M`X(_i5` zhVA0gPbFAR(+Rc@a=d{KvFqgkd7NOcmI*Yo0q%aD&0+!_F!z8)C((M>2fL^!JAmg4 zV0rZM5o@lKiT&*9C;k(#$p8)?ibc#r#)ZLNEfZb zO%geJxVv^y1zIL4cd!eT0P7u4_jHWj!+VISlb2=S6q;d_WcCEfI#MlPFFi$1H~ruE zo_<5@GIbS7fW38vFhy?{^#V?|e1azO>tp&QBH>1zj9yNYgk4mf4C*QM9P?PRsZ;~a z>=gppvQ~jo#Sz8}xJuax8ueVEChJJ#7mTArOpT&sQ?-&98=*FT589bU3P3YSJ0Eu! zsL1+ne6nRP-!R8D;#aUA7Hh;x74uYODkx{I!ur{}xY_#85Into+yz=cEEXxG>)itJ zcHpnG44xq*8^xM|`0nO=>3Sdrhj_X9o@Er~o@ySb-FbJ5zfC(sw2p8E_yN>&NEc+w zNEd(D$$vEh=%!!5uMj8~sONt3SbtmZd3&HAVDC5v0UMfgfWH�MLhncqZ6lA7Y$9 zJE59?17(~5b{dXBhDof0Yj}pq4gtyqyqzrqUoX%%-ku%2dzb-Ei03AWOhc}IgDi&0 zXj{FU|3v2L9ptc%V;ioK;}{Hc40AM2JHQHY@&2Y*iFI&*^?vh*MXBl@jzQKU1@r^K z)(9)?9f0#OeS#6-w&U+yCxX4(#@EX^L*eT8@xeMMQ?dAAl!3kfesh9xik^547&#|s z#%U4uzeGI!{(JvtobdHRJ#CU`7W49@n{JRoJ0aXyCbN$yRxi|~n^rIG=Bt%`fMy>8 ze*vm$b#r@pxcegPKXh>n;_miyA)m+C5Nu%`EKtJS-@a>-;OqAAZ;;B>)z27>2v+5;Dn85YsunsVe=%%3_ z$(Dhb0n!EgkU$4_A5d4oIzqAv_!Y?Kv@>`+G_yz-fOP>_pV<5O+ejB)zG=o8MnJw9 z^7$KRhrkbu3C5tW>cx>?u=k-KqHWQS{~g;l+A*wB5r|Wvo|-4Z-ON)tgx|o8vMiCi z2LQjtTLJI!1Ub$`yrx)5vDhwHFMSQq)jvnATG}Mx9sv5*EY>IqR6Wbplq-Y1*2+FZ zXcqc;BA-7&lB@zXOj!oaV%PA{4^ehg^z&4Au>ZO560gDCq<;F8Y9Dcqyo<}*BiXn{ z`~)fTE6NUMkGCgJ2mA%|5bh?;80D-*^c4*5W{f%139w|`0|eU*GrtD?5}Be0ydULq zlN9G}*Kibb3~z$&d$u#4jA|IGxW15jLp>!n#oBpd%7-z3F2%_?3eZ<4_^_y8+k z+dd-6jCN*_gKiq;9`eaHTA?bzl6*;^L#D1#@RJQ&bD1Rs5(~q#q)?UNI+_Q}K zao@mg5!}P%>avWIEprYqjWbT*?J!OtT~IFY^pY>l(ySA~-B2uY4zP^=?kE0n3$_C> z0?q;CbBv=GaF$Wza}W@QNs?8{1=Lg45sF3pZPpQvLFzfKe)b{mKEQ(n_7Cnps7J(0 z))De0(iPqwAX)�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