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 001/300] 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 002/300] 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 003/300] 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 004/300] 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 005/300] 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 08d17e527443fab05731eb0428806568e8e297aa Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Thu, 20 Jul 2023 15:44:19 +0200 Subject: [PATCH 006/300] 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 007/300] 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 008/300] 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 fb4567560ec9973aab354c566bd1e00674190e5b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 30 Aug 2023 14:53:04 +0200 Subject: [PATCH 009/300] 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 010/300] 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 011/300] 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 012/300] 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 013/300] 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 014/300] 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 015/300] 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 016/300] 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 017/300] 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 018/300] 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 019/300] 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 020/300] 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 021/300] 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 022/300] 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 023/300] 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 024/300] 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 025/300] 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 5f0ce4f88dd09caee68a04e94db672620c6d0416 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 15 Sep 2023 18:38:40 +0800 Subject: [PATCH 026/300] 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 027/300] 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 028/300] 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 029/300] 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 28a3cf943dec1320b070381982cf44f20cc6fd1e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 15 Sep 2023 22:26:08 +0800 Subject: [PATCH 030/300] 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 031/300] 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 fd6c6f3b3cbd6c4d820f1725c117ef43e9cde89d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 25 Sep 2023 21:59:03 +0800 Subject: [PATCH 032/300] 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 bf16f8492f39cce8c6b5c7cf39714a459180f94e Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 28 Sep 2023 15:05:13 +0100 Subject: [PATCH 033/300] 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 034/300] 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 035/300] 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 036/300] 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 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 037/300] 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 ffeb0282b93b05a1d345bfa39928c195d5f1ff74 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Mon, 2 Oct 2023 14:39:27 +0200 Subject: [PATCH 038/300] 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 039/300] 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 be09038e222f833a2eb3290006f08d02ea88d9a3 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 3 Oct 2023 14:54:54 +0800 Subject: [PATCH 040/300] 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 041/300] 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 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 042/300] 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 043/300] 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 044/300] 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 1d0e55aa833d99180b99cbbd718954933c5103b2 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 4 Oct 2023 13:00:42 +0200 Subject: [PATCH 045/300] 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 046/300] 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 047/300] 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 048/300] 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 049/300] 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 050/300] 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 051/300] 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 052/300] 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 053/300] 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 054/300] 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 055/300] 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 c8f6fb209bafc06dd12f536e64979e244af61c17 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 5 Oct 2023 11:31:30 +0300 Subject: [PATCH 056/300] 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 057/300] 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 058/300] 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 059/300] 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 060/300] 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 061/300] 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 062/300] 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 063/300] 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 064/300] 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 065/300] 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 066/300] 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 9d6340a8a18a0e41651580a38fedbb9dec732f5c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 5 Oct 2023 17:03:54 +0200 Subject: [PATCH 067/300] 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 068/300] 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 af67b4780f2daf62bafea4f227aea8e541d83dc6 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 6 Oct 2023 16:33:27 +0300 Subject: [PATCH 069/300] 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 0d47f4f57a5f472adbef5ef18c5f80b5c773d250 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 6 Oct 2023 16:41:56 +0300 Subject: [PATCH 070/300] 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 071/300] 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 4b02645618f1b31ec35452ceaa41dbcd5b623df6 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 6 Oct 2023 17:27:35 +0300 Subject: [PATCH 072/300] 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 073/300] 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 47425e3aa460a012a4fc6265ff5b4fdfcc345fc5 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 6 Oct 2023 22:54:00 +0300 Subject: [PATCH 074/300] 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 075/300] 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 076/300] 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 847f73deadd24f35b9f7567ea844cbc23db192fb Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Mon, 9 Oct 2023 10:52:13 +0300 Subject: [PATCH 077/300] 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 521707340af3e2357c7764d906da3659dec95e9d Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Mon, 9 Oct 2023 12:30:58 +0300 Subject: [PATCH 078/300] 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 079/300] 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 080/300] 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 081/300] 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 082/300] 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 083/300] 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 084/300] 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 9ff279d52f2a92471bf530a2664dafcbf9018c03 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 9 Oct 2023 16:07:32 +0200 Subject: [PATCH 085/300] 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 086/300] 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 087/300] 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 088/300] 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 4ff71554d3c7f76c753f3c6e0796f367b3548dcb Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 9 Oct 2023 17:16:40 +0200 Subject: [PATCH 089/300] 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 090/300] 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 2f2e100231089b3bb321ed1e72962b929f75621f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 10 Oct 2023 09:43:28 +0200 Subject: [PATCH 091/300] 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 58e5cf20b3023ea0c440304b2ba5184af6110312 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 10 Oct 2023 14:34:35 +0200 Subject: [PATCH 092/300] 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 093/300] 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 094/300] 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 519db56b8769a71f18083a1cb972c2e9ae0f567c Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 11 Oct 2023 09:38:48 +0300 Subject: [PATCH 095/300] 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 096/300] 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 097/300] 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 098/300] 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 099/300] 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 100/300] 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 101/300] 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 102/300] 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 103/300] 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 104/300] 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 105/300] 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 106/300] 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 91c37916cb82f75515f81a54b922769dab5b1816 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 11 Oct 2023 15:23:26 +0200 Subject: [PATCH 107/300] 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 108/300] 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 109/300] 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 110/300] 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 111/300] 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 112/300] 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 113/300] 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 114/300] 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 115/300] 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 45b61c21711b92c5a59e73b1125c2da2696d62de Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 11 Oct 2023 23:12:42 +0300 Subject: [PATCH 116/300] 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 7035e1e0145f11f832eee08754f681e3304e591d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 12 Oct 2023 13:50:31 +0800 Subject: [PATCH 117/300] 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 118/300] 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 119/300] 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 120/300] 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 121/300] 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 122/300] 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 123/300] 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 95fefaaa169a7404d3fae9ed5906b1227cde7c95 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 12 Oct 2023 10:45:16 +0100 Subject: [PATCH 124/300] 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 7589de5aa14cb595f7646f68e7f9d8eaf373a0b5 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 12 Oct 2023 11:33:18 +0100 Subject: [PATCH 125/300] 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 126/300] 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 848953f026493244a0de98bbf6df6f6d0f421e73 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 12 Oct 2023 18:01:12 +0300 Subject: [PATCH 127/300] 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 128/300] 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 21de693c17adaeebdf4ac30cd198347474a7efa2 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 12 Oct 2023 17:07:47 +0100 Subject: [PATCH 129/300] 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 130/300] 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 131/300] 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 132/300] 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 133/300] 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 134/300] 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 135/300] 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 6f8cc1c982f2378fd83430ab4fa640b8dcaffa09 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 13 Oct 2023 12:28:44 +0200 Subject: [PATCH 136/300] 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 1b79767e7bbd76f93ca8ba8bf0f2ef434239509c Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 13 Oct 2023 22:15:14 +0800 Subject: [PATCH 137/300] 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 138/300] 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 139/300] 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 59b7c61b3da7cb95b6b62c731ea0496dc83bac8a Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 13 Oct 2023 22:28:45 +0800 Subject: [PATCH 140/300] 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 7431f6e9ef68b95ad0dc6c7ac0e9b1c1656672ce Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 16 Oct 2023 16:06:58 +0800 Subject: [PATCH 141/300] 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 142/300] 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 a1a4898a731547db13a884f40df5640b48d65b6a Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 17 Oct 2023 18:31:22 +0800 Subject: [PATCH 143/300] 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 144/300] 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 ab2241aebb62de3489c13458f2d118b5e49e9886 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 17 Oct 2023 19:11:23 +0800 Subject: [PATCH 145/300] 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 f214751be375016d4464b1faf4e427c1851f36a4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 17 Oct 2023 16:29:55 +0200 Subject: [PATCH 146/300] 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 74acdd63eee751eecfe2d92933d3d3752d89b4ce Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 17 Oct 2023 16:41:27 +0200 Subject: [PATCH 147/300] 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 148/300] 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 149/300] 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 3461cbed58efeb3c901e66b7c677002543021f03 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 18 Oct 2023 12:17:58 +0800 Subject: [PATCH 150/300] 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 68f7826cf610b8160cb8ce21bc764f1964eb4559 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 18 Oct 2023 10:16:17 +0200 Subject: [PATCH 151/300] 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 152/300] 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 153/300] 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 154/300] 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 155/300] 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 65cfa7751c2b9805ff7e60bb24e6dae7c97e81be Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 18 Oct 2023 12:22:10 +0200 Subject: [PATCH 156/300] 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 105720ff0d3f999a735cfaeb0783997c13131b4e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 18 Oct 2023 12:55:17 +0200 Subject: [PATCH 157/300] 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 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 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 19:20:31 +0800 Subject: [PATCH 158/300] 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 159/300] 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 160/300] 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 161/300] 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 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 162/300] 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 163/300] 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 164/300] 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 165/300] 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 166/300] 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 167/300] 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 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 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 168/300] 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 169/300] 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 170/300] 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 171/300] 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 172/300] 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 173/300] 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 174/300] '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 175/300] 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 176/300] 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 177/300] 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 178/300] 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 179/300] 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 180/300] 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 181/300] 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 182/300] 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 183/300] 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 184/300] 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 185/300] 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 186/300] 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 187/300] 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 188/300] 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 189/300] 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 190/300] 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 191/300] 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 e17ffd1c3d01a42f2655791d2263edf516393f04 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Thu, 19 Oct 2023 12:05:49 +0300 Subject: [PATCH 192/300] 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 193/300] 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 194/300] 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 195/300] 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 196/300] 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 197/300] 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 198/300] 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 199/300] 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 200/300] 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 201/300] 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 15532b06c5277b78c54ace0829445d08c3050987 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 19 Oct 2023 19:06:01 +0800 Subject: [PATCH 202/300] 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 203/300] 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 204/300] 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 205/300] 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 206/300] 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 207/300] 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 208/300] 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 209/300] 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 210/300] 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 211/300] 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 212/300] 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 213/300] 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 214/300] 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 215/300] 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 216/300] 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 217/300] 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 218/300] 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 219/300] 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 220/300] 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 221/300] 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 222/300] 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 223/300] 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 224/300] [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 225/300] 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 226/300] 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 227/300] 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 228/300] 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 229/300] 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 230/300] 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 231/300] 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 232/300] 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 233/300] 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 234/300] 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 235/300] 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 236/300] 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 237/300] 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 238/300] 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 239/300] 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 240/300] 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 241/300] 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 242/300] 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 243/300] 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 244/300] 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 245/300] 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 246/300] 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 247/300] 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 248/300] 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 249/300] 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 250/300] 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 251/300] 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 252/300] 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 253/300] [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 254/300] 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 255/300] 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 256/300] 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 257/300] 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 258/300] 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 259/300] 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 260/300] 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 261/300] 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 262/300] 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 263/300] 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 264/300] 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 265/300] 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 266/300] 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 267/300] 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 268/300] 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 269/300] 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 270/300] 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 271/300] 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 272/300] 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 273/300] 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 274/300] 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 275/300] 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 276/300] 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 277/300] 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 d11b00510388a797f5e0229d13a2f3bf6b837460 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 24 Oct 2023 21:08:54 +0800 Subject: [PATCH 278/300] 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 24e20ee12a3bbebff4f7896cdefe201d5b14c279 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 25 Oct 2023 03:25:13 +0000 Subject: [PATCH 279/300] [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 439afe4adbbd524b252184cd7c4033b7ad817cb2 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 25 Oct 2023 15:21:27 +0800 Subject: [PATCH 280/300] 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 281/300] 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 282/300] 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 283/300] 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 284/300] 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 40fe7391b2c8470b0880852bf09e95b706e37ed3 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 25 Oct 2023 20:46:28 +0800 Subject: [PATCH 285/300] 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 286/300] 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 f35b3508ea737877b71226615fc9747ec0fdbe17 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 25 Oct 2023 17:45:23 +0200 Subject: [PATCH 287/300] 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 288/300] 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 289/300] 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 71014fca0b42e490c2ceedf94fc90648ca16808a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 26 Oct 2023 13:34:18 +0200 Subject: [PATCH 290/300] 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 291/300] 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 292/300] 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 293/300] [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 294/300] 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 a6e72e708b5853f42aa99a18a57554ee2494d5ec Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 26 Oct 2023 17:03:42 +0200 Subject: [PATCH 295/300] 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 296/300] 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 ec5bc71eb3529ba096e00d5261dfdf0608a53280 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 27 Oct 2023 16:42:56 +0200 Subject: [PATCH 297/300] 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 7a156e8499c4be6e7e505fc9a9641008c7e2f829 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Sat, 28 Oct 2023 03:24:38 +0000 Subject: [PATCH 298/300] [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 299/300] 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 300/300] 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", }