From 5b18acadc0c76e7bd51f5fe8a6b1b93486a0e79e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 26 Oct 2023 21:33:31 +0800 Subject: [PATCH 01/54] Implement ValidateAttributes in 3dsMax --- .../plugins/publish/validate_attributes.py | 62 +++++++++++++++++++ openpype/settings/ayon_settings.py | 9 +++ .../defaults/project_settings/max.json | 4 ++ .../schemas/schema_max_publish.json | 19 ++++++ .../max/server/settings/publishers.py | 36 ++++++++++- 5 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 openpype/hosts/max/plugins/publish/validate_attributes.py diff --git a/openpype/hosts/max/plugins/publish/validate_attributes.py b/openpype/hosts/max/plugins/publish/validate_attributes.py new file mode 100644 index 0000000000..e98e73de06 --- /dev/null +++ b/openpype/hosts/max/plugins/publish/validate_attributes.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +"""Validator for Attributes.""" +from pyblish.api import ContextPlugin, ValidatorOrder +from pymxs import runtime as rt + +from openpype.pipeline.publish import ( + OptionalPyblishPluginMixin, + PublishValidationError, + RepairContextAction +) + + +class ValidateAttributes(OptionalPyblishPluginMixin, + ContextPlugin): + """Validates attributes are consistent in 3ds max.""" + + order = ValidatorOrder + hosts = ["max"] + label = "Attributes" + actions = [RepairContextAction] + optional = True + + @classmethod + def get_invalid(cls, context): + attributes = ( + context.data["project_settings"]["max"]["publish"] + ["ValidateAttributes"]["attributes"] + ) + if not attributes: + return + + invalid_attributes = [key for key, value in attributes.items() + if rt.Execute(attributes[key]) != value] + + return invalid_attributes + + def process(self, context): + if not self.is_active(context.data): + self.log.debug("Skipping Validate Attributes...") + return + invalid_attributes = self.get_invalid(context) + if invalid_attributes: + bullet_point_invalid_statement = "\n".join( + "- {}".format(invalid) for invalid in invalid_attributes + ) + report = ( + "Required Attribute(s) have invalid value(s).\n\n" + f"{bullet_point_invalid_statement}\n\n" + "You can use repair action to fix it." + ) + raise PublishValidationError( + report, title="Invalid Value(s) for Required Attribute(s)") + + @classmethod + def repair(cls, context): + attributes = ( + context.data["project_settings"]["max"]["publish"] + ["ValidateAttributes"]["attributes"] + ) + invalid_attribute_keys = cls.get_invalid(context) + for key in invalid_attribute_keys: + attributes[key] = rt.Execute(attributes[key]) diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index 8d4683490b..a31c8a04e0 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -639,6 +639,15 @@ def _convert_3dsmax_project_settings(ayon_settings, output): for item in point_cloud_attribute } ayon_max["PointCloud"]["attribute"] = new_point_cloud_attribute + # --- Publish (START) --- + ayon_publish = ayon_max["publish"] + try: + attributes = json.loads( + ayon_publish["ValidateAttributes"]["attributes"] + ) + except ValueError: + attributes = {} + ayon_publish["ValidateAttributes"]["attributes"] = attributes output["max"] = ayon_max diff --git a/openpype/settings/defaults/project_settings/max.json b/openpype/settings/defaults/project_settings/max.json index bfb1aa4aeb..24a87020bb 100644 --- a/openpype/settings/defaults/project_settings/max.json +++ b/openpype/settings/defaults/project_settings/max.json @@ -36,6 +36,10 @@ "enabled": true, "optional": true, "active": true + }, + "ValidateAttributes": { + "enabled": false, + "attributes": {} } } } diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json index ea08c735a6..c3b56bae5e 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json @@ -28,6 +28,25 @@ "label": "Active" } ] + }, + { + "type": "dict", + "collapsible": true, + "key": "ValidateAttributes", + "label": "ValidateAttributes", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "raw-json", + "key": "attributes", + "label": "Attributes" + } + ] } ] } diff --git a/server_addon/max/server/settings/publishers.py b/server_addon/max/server/settings/publishers.py index a695b85e89..df8412391a 100644 --- a/server_addon/max/server/settings/publishers.py +++ b/server_addon/max/server/settings/publishers.py @@ -1,6 +1,30 @@ -from pydantic import Field +import json +from pydantic import Field, validator from ayon_server.settings import BaseSettingsModel +from ayon_server.exceptions import BadRequestException + + +class ValidateAttributesModel(BaseSettingsModel): + enabled: bool = Field(title="ValidateAttributes") + attributes: str = Field( + "{}", title="Attributes", widget="textarea") + + @validator("attributes") + def validate_json(cls, value): + if not value.strip(): + return "{}" + try: + converted_value = json.loads(value) + success = isinstance(converted_value, dict) + except json.JSONDecodeError: + success = False + + if not success: + raise BadRequestException( + "The attibutes can't be parsed as json object" + ) + return value class BasicValidateModel(BaseSettingsModel): @@ -15,6 +39,10 @@ class PublishersModel(BaseSettingsModel): title="Validate Frame Range", section="Validators" ) + ValidateAttributes: ValidateAttributesModel = Field( + default_factory=ValidateAttributesModel, + title="Validate Attributes" + ) DEFAULT_PUBLISH_SETTINGS = { @@ -22,5 +50,9 @@ DEFAULT_PUBLISH_SETTINGS = { "enabled": True, "optional": True, "active": True - } + }, + "ValidateAttributes": { + "enabled": False, + "attributes": "{}" + }, } From 6a0decab459db0b83b777289521584f2eaca02a2 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 27 Oct 2023 21:09:29 +0800 Subject: [PATCH 02/54] make sure to check invalid properties --- .../plugins/publish/validate_attributes.py | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_attributes.py b/openpype/hosts/max/plugins/publish/validate_attributes.py index e98e73de06..2d3f09f972 100644 --- a/openpype/hosts/max/plugins/publish/validate_attributes.py +++ b/openpype/hosts/max/plugins/publish/validate_attributes.py @@ -29,10 +29,20 @@ class ValidateAttributes(OptionalPyblishPluginMixin, if not attributes: return - invalid_attributes = [key for key, value in attributes.items() - if rt.Execute(attributes[key]) != value] + for wrap_object, property_name in attributes.items(): + invalid_properties = [key for key in property_name.keys() + if not rt.Execute( + f'isProperty {wrap_object} "{key}"')] + if invalid_properties: + cls.log.error( + "Unknown Property Values:{}".format(invalid_properties)) + return invalid_properties + # TODO: support multiple varaible types in maxscript + invalid_attributes = [key for key, value in property_name.items() + if rt.Execute("{}.{}".format( + wrap_object, property_name[key]))!=value] - return invalid_attributes + return invalid_attributes def process(self, context): if not self.is_active(context.data): @@ -57,6 +67,10 @@ class ValidateAttributes(OptionalPyblishPluginMixin, context.data["project_settings"]["max"]["publish"] ["ValidateAttributes"]["attributes"] ) - invalid_attribute_keys = cls.get_invalid(context) - for key in invalid_attribute_keys: - attributes[key] = rt.Execute(attributes[key]) + for wrap_object, property_name in attributes.items(): + invalid_attributes = [key for key, value in property_name.items() + if rt.Execute("{}.{}".format( + wrap_object, property_name[key]))!=value] + for attrs in invalid_attributes: + rt.Execute("{}.{}={}".format( + wrap_object, attrs, attributes[wrap_object][attrs])) From c029fa632489529e36dbdaec5b74d2e938f46847 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 30 Oct 2023 17:21:29 +0800 Subject: [PATCH 03/54] support invalid checks on different variable types of attributes in Maxscript --- .../max/plugins/publish/validate_attributes.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_attributes.py b/openpype/hosts/max/plugins/publish/validate_attributes.py index 2d3f09f972..fa9912de07 100644 --- a/openpype/hosts/max/plugins/publish/validate_attributes.py +++ b/openpype/hosts/max/plugins/publish/validate_attributes.py @@ -38,9 +38,20 @@ class ValidateAttributes(OptionalPyblishPluginMixin, "Unknown Property Values:{}".format(invalid_properties)) return invalid_properties # TODO: support multiple varaible types in maxscript - invalid_attributes = [key for key, value in property_name.items() - if rt.Execute("{}.{}".format( - wrap_object, property_name[key]))!=value] + invalid_attributes = [] + for key, value in property_name.items(): + property_key = rt.Execute("{}.{}".format( + wrap_object, key)) + if isinstance(value, str) and "#" not in value: + if property_key != '"{}"'.format(value): + invalid_attributes.append(key) + + elif isinstance(value, bool): + if property_key != value: + invalid_attributes.append(key) + else: + if property_key != '{}'.format(value): + invalid_attributes.append(key) return invalid_attributes @@ -71,6 +82,7 @@ class ValidateAttributes(OptionalPyblishPluginMixin, invalid_attributes = [key for key, value in property_name.items() if rt.Execute("{}.{}".format( wrap_object, property_name[key]))!=value] + for attrs in invalid_attributes: rt.Execute("{}.{}={}".format( wrap_object, attrs, attributes[wrap_object][attrs])) From 6c7e5c66a6b85e2ee8e42c2c9b38ea6639d9dd42 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 30 Oct 2023 21:13:42 +0800 Subject: [PATCH 04/54] support invalid checks on different variable types of attributes in Maxscript & repair actions --- .../plugins/publish/validate_attributes.py | 45 +++++++++++-------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_attributes.py b/openpype/hosts/max/plugins/publish/validate_attributes.py index fa9912de07..f266b2bca1 100644 --- a/openpype/hosts/max/plugins/publish/validate_attributes.py +++ b/openpype/hosts/max/plugins/publish/validate_attributes.py @@ -42,16 +42,16 @@ class ValidateAttributes(OptionalPyblishPluginMixin, for key, value in property_name.items(): property_key = rt.Execute("{}.{}".format( wrap_object, key)) - if isinstance(value, str) and "#" not in value: - if property_key != '"{}"'.format(value): - invalid_attributes.append(key) - - elif isinstance(value, bool): - if property_key != value: - invalid_attributes.append(key) + if isinstance(value, str) and ( + value.startswith("#") and not value.endswith(")") + ): + # not applicable for #() array value type + # and only applicable for enum i.e. #bob, #sally + if "#{}".format(property_key) != value: + invalid_attributes.append((wrap_object, key)) else: - if property_key != '{}'.format(value): - invalid_attributes.append(key) + if property_key != value: + invalid_attributes.append((wrap_object, key)) return invalid_attributes @@ -62,12 +62,14 @@ class ValidateAttributes(OptionalPyblishPluginMixin, invalid_attributes = self.get_invalid(context) if invalid_attributes: bullet_point_invalid_statement = "\n".join( - "- {}".format(invalid) for invalid in invalid_attributes + "- {}".format(invalid) for invalid + in invalid_attributes ) report = ( "Required Attribute(s) have invalid value(s).\n\n" f"{bullet_point_invalid_statement}\n\n" - "You can use repair action to fix it." + "You can use repair action to fix them if they are not\n" + "unknown property value(s)" ) raise PublishValidationError( report, title="Invalid Value(s) for Required Attribute(s)") @@ -78,11 +80,16 @@ class ValidateAttributes(OptionalPyblishPluginMixin, context.data["project_settings"]["max"]["publish"] ["ValidateAttributes"]["attributes"] ) - for wrap_object, property_name in attributes.items(): - invalid_attributes = [key for key, value in property_name.items() - if rt.Execute("{}.{}".format( - wrap_object, property_name[key]))!=value] - - for attrs in invalid_attributes: - rt.Execute("{}.{}={}".format( - wrap_object, attrs, attributes[wrap_object][attrs])) + invalid_attributes = cls.get_invalid(context) + for attrs in invalid_attributes: + prop, attr = attrs + value = attributes[prop][attr] + if isinstance(value, str) and not value.startswith("#"): + attribute_fix = '{}.{}="{}"'.format( + prop, attr, value + ) + else: + attribute_fix = "{}.{}={}".format( + prop, attr, value + ) + rt.Execute(attribute_fix) From f0b8d8d79826df7c27650dfbe68046e2d2d63d9d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 31 Oct 2023 14:19:37 +0800 Subject: [PATCH 05/54] add docstrings and clean up the codes on the validator --- .../plugins/publish/validate_attributes.py | 80 +++++++++++++------ 1 file changed, 55 insertions(+), 25 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_attributes.py b/openpype/hosts/max/plugins/publish/validate_attributes.py index f266b2bca1..f603934eed 100644 --- a/openpype/hosts/max/plugins/publish/validate_attributes.py +++ b/openpype/hosts/max/plugins/publish/validate_attributes.py @@ -10,9 +10,47 @@ from openpype.pipeline.publish import ( ) +def has_property(object_name, property_name): + """Return whether an object has a property with given name""" + return rt.Execute(f'isProperty {object_name} "{property_name}"') + +def is_matching_value(object_name, property_name, value): + """Return whether an existing property matches value `value""" + property_value = rt.Execute(f"{object_name}.{property_name}") + + # Wrap property value if value is a string valued attributes + # starting with a `#` + if ( + isinstance(value, str) and + value.startswith("#") and + not value.endswith(")") + ): + # prefix value with `#` + # not applicable for #() array value type + # and only applicable for enum i.e. #bob, #sally + property_value = f"#{property_value}" + + return property_value == value + + class ValidateAttributes(OptionalPyblishPluginMixin, ContextPlugin): - """Validates attributes are consistent in 3ds max.""" + """Validates attributes in the project setting are consistent + with the nodes from MaxWrapper Class in 3ds max. + E.g. "renderers.current.separateAovFiles", + "renderers.production.PrimaryGIEngine" + Admin(s) need to put json below and enable this validator for a check: + { + "renderers.current":{ + "separateAovFiles" : True + } + "renderers.production":{ + "PrimaryGIEngine": "#RS_GIENGINE_BRUTE_FORCE", + } + .... + } + + """ order = ValidatorOrder hosts = ["max"] @@ -28,32 +66,24 @@ class ValidateAttributes(OptionalPyblishPluginMixin, ) if not attributes: return + invalid = [] + for object_name, required_properties in attributes.items(): + if not rt.Execute(f"isValidValue {object_name}"): + # Skip checking if the node does not + # exist in MaxWrapper Class + continue - for wrap_object, property_name in attributes.items(): - invalid_properties = [key for key in property_name.keys() - if not rt.Execute( - f'isProperty {wrap_object} "{key}"')] - if invalid_properties: - cls.log.error( - "Unknown Property Values:{}".format(invalid_properties)) - return invalid_properties - # TODO: support multiple varaible types in maxscript - invalid_attributes = [] - for key, value in property_name.items(): - property_key = rt.Execute("{}.{}".format( - wrap_object, key)) - if isinstance(value, str) and ( - value.startswith("#") and not value.endswith(")") - ): - # not applicable for #() array value type - # and only applicable for enum i.e. #bob, #sally - if "#{}".format(property_key) != value: - invalid_attributes.append((wrap_object, key)) - else: - if property_key != value: - invalid_attributes.append((wrap_object, key)) + for property_name, value in required_properties.items(): + if not has_property(object_name, property_name): + cls.log.error(f"Non-existing property: {object_name}.{property_name}") + invalid.append((object_name, property_name)) - return invalid_attributes + if not is_matching_value(object_name, property_name, value): + cls.log.error( + f"Invalid value for: {object_name}.{property_name}. Should be: {value}") + invalid.append((object_name, property_name)) + + return invalid def process(self, context): if not self.is_active(context.data): From 4c204a87a917ef05bf5b23e3f180d4f95650a3fb Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 31 Oct 2023 14:20:25 +0800 Subject: [PATCH 06/54] hound --- openpype/hosts/max/plugins/publish/validate_attributes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/max/plugins/publish/validate_attributes.py b/openpype/hosts/max/plugins/publish/validate_attributes.py index f603934eed..44d6c64139 100644 --- a/openpype/hosts/max/plugins/publish/validate_attributes.py +++ b/openpype/hosts/max/plugins/publish/validate_attributes.py @@ -14,6 +14,7 @@ def has_property(object_name, property_name): """Return whether an object has a property with given name""" return rt.Execute(f'isProperty {object_name} "{property_name}"') + def is_matching_value(object_name, property_name, value): """Return whether an existing property matches value `value""" property_value = rt.Execute(f"{object_name}.{property_name}") From 33a21674c5e752c19be8636ab6587f38f91f8f59 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 31 Oct 2023 14:21:37 +0800 Subject: [PATCH 07/54] hound --- openpype/hosts/max/plugins/publish/validate_attributes.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_attributes.py b/openpype/hosts/max/plugins/publish/validate_attributes.py index 44d6c64139..5697237c95 100644 --- a/openpype/hosts/max/plugins/publish/validate_attributes.py +++ b/openpype/hosts/max/plugins/publish/validate_attributes.py @@ -76,12 +76,14 @@ class ValidateAttributes(OptionalPyblishPluginMixin, for property_name, value in required_properties.items(): if not has_property(object_name, property_name): - cls.log.error(f"Non-existing property: {object_name}.{property_name}") + cls.log.error( + f"Non-existing property: {object_name}.{property_name}") invalid.append((object_name, property_name)) if not is_matching_value(object_name, property_name, value): cls.log.error( - f"Invalid value for: {object_name}.{property_name}. Should be: {value}") + f"Invalid value for: {object_name}.{property_name}" + f". Should be: {value}") invalid.append((object_name, property_name)) return invalid From ce80ca2397c7ed9b609f0358a9c0595289a8e260 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 31 Oct 2023 16:10:22 +0800 Subject: [PATCH 08/54] debug message --- openpype/hosts/max/plugins/publish/validate_attributes.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/max/plugins/publish/validate_attributes.py b/openpype/hosts/max/plugins/publish/validate_attributes.py index 5697237c95..00b9d34c06 100644 --- a/openpype/hosts/max/plugins/publish/validate_attributes.py +++ b/openpype/hosts/max/plugins/publish/validate_attributes.py @@ -72,6 +72,8 @@ class ValidateAttributes(OptionalPyblishPluginMixin, if not rt.Execute(f"isValidValue {object_name}"): # Skip checking if the node does not # exist in MaxWrapper Class + cls.log.debug(f"Unable to find '{object_name}'." + f" Skipping validation of attributes") continue for property_name, value in required_properties.items(): From 3218b8064cdd00f7efab87bab935e1c8cb130c16 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 31 Oct 2023 16:11:57 +0800 Subject: [PATCH 09/54] hound & docstring tweak --- openpype/hosts/max/plugins/publish/validate_attributes.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_attributes.py b/openpype/hosts/max/plugins/publish/validate_attributes.py index 00b9d34c06..75d3f05d07 100644 --- a/openpype/hosts/max/plugins/publish/validate_attributes.py +++ b/openpype/hosts/max/plugins/publish/validate_attributes.py @@ -46,7 +46,7 @@ class ValidateAttributes(OptionalPyblishPluginMixin, "separateAovFiles" : True } "renderers.production":{ - "PrimaryGIEngine": "#RS_GIENGINE_BRUTE_FORCE", + "PrimaryGIEngine": "#RS_GIENGINE_BRUTE_FORCE" } .... } @@ -79,7 +79,8 @@ class ValidateAttributes(OptionalPyblishPluginMixin, for property_name, value in required_properties.items(): if not has_property(object_name, property_name): cls.log.error( - f"Non-existing property: {object_name}.{property_name}") + "Non-existing property: " + f"{object_name}.{property_name}") invalid.append((object_name, property_name)) if not is_matching_value(object_name, property_name, value): From 009cda005227158561d898aa59c28ca16ae4166c Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 31 Oct 2023 17:18:06 +0800 Subject: [PATCH 10/54] update the debug message with dots --- openpype/hosts/max/plugins/publish/validate_attributes.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_attributes.py b/openpype/hosts/max/plugins/publish/validate_attributes.py index 75d3f05d07..172b65e955 100644 --- a/openpype/hosts/max/plugins/publish/validate_attributes.py +++ b/openpype/hosts/max/plugins/publish/validate_attributes.py @@ -73,7 +73,7 @@ class ValidateAttributes(OptionalPyblishPluginMixin, # Skip checking if the node does not # exist in MaxWrapper Class cls.log.debug(f"Unable to find '{object_name}'." - f" Skipping validation of attributes") + " Skipping validation of attributes.") continue for property_name, value in required_properties.items(): @@ -86,7 +86,7 @@ class ValidateAttributes(OptionalPyblishPluginMixin, if not is_matching_value(object_name, property_name, value): cls.log.error( f"Invalid value for: {object_name}.{property_name}" - f". Should be: {value}") + f" Should be: {value}") invalid.append((object_name, property_name)) return invalid @@ -105,7 +105,7 @@ class ValidateAttributes(OptionalPyblishPluginMixin, "Required Attribute(s) have invalid value(s).\n\n" f"{bullet_point_invalid_statement}\n\n" "You can use repair action to fix them if they are not\n" - "unknown property value(s)" + "unknown property value(s)." ) raise PublishValidationError( report, title="Invalid Value(s) for Required Attribute(s)") From ad0b941475c67196ad7e09beada2bd81b2d51a63 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 31 Oct 2023 17:28:42 +0800 Subject: [PATCH 11/54] lowercase invalid msg for the condition of not is_maching_value function --- openpype/hosts/max/plugins/publish/validate_attributes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/validate_attributes.py b/openpype/hosts/max/plugins/publish/validate_attributes.py index 172b65e955..0cd405aebd 100644 --- a/openpype/hosts/max/plugins/publish/validate_attributes.py +++ b/openpype/hosts/max/plugins/publish/validate_attributes.py @@ -86,7 +86,7 @@ class ValidateAttributes(OptionalPyblishPluginMixin, if not is_matching_value(object_name, property_name, value): cls.log.error( f"Invalid value for: {object_name}.{property_name}" - f" Should be: {value}") + f" should be: {value}") invalid.append((object_name, property_name)) return invalid From e39689ba8e1a7a9de754ecf5becb487d5fda7665 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 31 Oct 2023 20:28:15 +0800 Subject: [PATCH 12/54] add docs --- website/docs/artist_hosts_3dsmax.md | 24 ++++++++++++++++++ .../assets/3dsmax_validate_attributes.png | Bin 0 -> 35154 bytes 2 files changed, 24 insertions(+) create mode 100644 website/docs/assets/3dsmax_validate_attributes.png diff --git a/website/docs/artist_hosts_3dsmax.md b/website/docs/artist_hosts_3dsmax.md index fffab8ca5d..bc79094746 100644 --- a/website/docs/artist_hosts_3dsmax.md +++ b/website/docs/artist_hosts_3dsmax.md @@ -118,4 +118,28 @@ Current OpenPype integration (ver 3.15.0) supports only ```PointCache```, ```Ca This part of documentation is still work in progress. ::: +## Validators + +Current Openpype integration supports different validators such as Frame Range and Attributes. +Some validators are mandatory while some are optional and user can choose to enable them in the setting. + +**Validate Frame Range**: Optional Validator for checking Frame Range + +**Validate Attributes**: Optional Validator for checking if object properties' attributes are valid + in MaxWrapper Class. +:::note + Users can write the properties' attributes they want to check in dict format in the setting + before validation. + E.g. ```renderers.current.separateAovFiles``` and ```renderers.current.PrimaryGIEngine``` + User can put the attributes in the dict format below + ``` + { + "renderer.current":{ + "separateAovFiles" : True + "PrimaryGIEngine": "#RS_GIENGINE_BRUTE_FORCE" + } + } + ``` + ![Validate Attribute Setting](assets/3dsmax_validate_attributes.png) +::: ## ...to be added diff --git a/website/docs/assets/3dsmax_validate_attributes.png b/website/docs/assets/3dsmax_validate_attributes.png new file mode 100644 index 0000000000000000000000000000000000000000..5af82361887f47fda96607886ec10e15eb355df1 GIT binary patch literal 35154 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 FpM9ou89oV;b5Eca*vLo#<6{~%iTY)O)Z@( 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 sPpdcV0Zhn($&0bNe4%m4rY literal 0 HcmV?d00001 From 1795d501d995f037af9ad81d8731f14b67a7f19e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 31 Oct 2023 20:29:34 +0800 Subject: [PATCH 13/54] add docs --- website/docs/artist_hosts_3dsmax.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/artist_hosts_3dsmax.md b/website/docs/artist_hosts_3dsmax.md index bc79094746..6f23a19103 100644 --- a/website/docs/artist_hosts_3dsmax.md +++ b/website/docs/artist_hosts_3dsmax.md @@ -129,7 +129,7 @@ Some validators are mandatory while some are optional and user can choose to ena in MaxWrapper Class. :::note Users can write the properties' attributes they want to check in dict format in the setting - before validation. + before validation. The attributes are then to be converted into Maxscript and do a check. E.g. ```renderers.current.separateAovFiles``` and ```renderers.current.PrimaryGIEngine``` User can put the attributes in the dict format below ``` From cfd9f0f06c26c0d47340f9baf42239674e2cebc8 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 31 Oct 2023 20:32:28 +0800 Subject: [PATCH 14/54] edit docstring --- openpype/hosts/max/plugins/publish/validate_attributes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_attributes.py b/openpype/hosts/max/plugins/publish/validate_attributes.py index 0cd405aebd..0632ee38f0 100644 --- a/openpype/hosts/max/plugins/publish/validate_attributes.py +++ b/openpype/hosts/max/plugins/publish/validate_attributes.py @@ -40,11 +40,11 @@ class ValidateAttributes(OptionalPyblishPluginMixin, with the nodes from MaxWrapper Class in 3ds max. E.g. "renderers.current.separateAovFiles", "renderers.production.PrimaryGIEngine" - Admin(s) need to put json below and enable this validator for a check: + Admin(s) need to put the dict below and enable this validator for a check: { "renderers.current":{ "separateAovFiles" : True - } + }, "renderers.production":{ "PrimaryGIEngine": "#RS_GIENGINE_BRUTE_FORCE" } From 0916db5fa0c98e239e65ff5a95fa76184fec613f Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 1 Nov 2023 14:35:22 +0200 Subject: [PATCH 15/54] set f1 and f2 to $FSTART and $FEND respectively --- openpype/hosts/houdini/plugins/create/create_composite.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/hosts/houdini/plugins/create/create_composite.py b/openpype/hosts/houdini/plugins/create/create_composite.py index 9d4f7969bb..52ea6fa054 100644 --- a/openpype/hosts/houdini/plugins/create/create_composite.py +++ b/openpype/hosts/houdini/plugins/create/create_composite.py @@ -45,6 +45,11 @@ class CreateCompositeSequence(plugin.HoudiniCreator): instance_node.setParms(parms) + # Manually set f1 & f2 to $FSTART and $FEND respectively + # to match other Houdini nodes default. + instance_node.parm("f1").setExpression("$FSTART") + instance_node.parm("f2").setExpression("$FEND") + # Lock any parameters in this list to_lock = ["prim_to_detail_pattern"] self.lock_parameters(instance_node, to_lock) From 41d9cf65b0d8d70affefe47e6feaca71c406a4d0 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 1 Nov 2023 15:05:33 +0200 Subject: [PATCH 16/54] make tab menu name change according to the app whether OpenPype or AYON --- openpype/hosts/houdini/api/creator_node_shelves.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/houdini/api/creator_node_shelves.py b/openpype/hosts/houdini/api/creator_node_shelves.py index 1f9fef7417..085642a277 100644 --- a/openpype/hosts/houdini/api/creator_node_shelves.py +++ b/openpype/hosts/houdini/api/creator_node_shelves.py @@ -173,6 +173,7 @@ def install(): os.remove(filepath) icon = get_openpype_icon_filepath() + tab_menu_label = os.environ.get("AVALON_LABEL") or "OpenPype" # Create context only to get creator plugins, so we don't reset and only # populate what we need to retrieve the list of creator plugins @@ -197,14 +198,14 @@ def install(): if not network_categories: continue - key = "openpype_create.{}".format(identifier) + key = "ayon_create.{}".format(identifier) log.debug(f"Registering {key}") script = CREATE_SCRIPT.format(identifier=identifier) data = { "script": script, "language": hou.scriptLanguage.Python, "icon": icon, - "help": "Create OpenPype publish instance for {}".format( + "help": "Create Ayon publish instance for {}".format( creator.label ), "help_url": None, @@ -213,7 +214,7 @@ def install(): "cop_viewer_categories": [], "network_op_type": None, "viewer_op_type": None, - "locations": ["OpenPype"] + "locations": [tab_menu_label] } label = "Create {}".format(creator.label) tool = hou.shelves.tool(key) From eb242e78a5afeef40c814bc27a140dbfdc0e7f85 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 1 Nov 2023 15:13:40 +0200 Subject: [PATCH 17/54] add get_network_categories --- openpype/hosts/houdini/plugins/create/create_bgeo.py | 8 +++++++- openpype/hosts/houdini/plugins/create/create_hda.py | 7 ++++++- .../houdini/plugins/create/create_redshift_proxy.py | 12 +++++++++--- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_bgeo.py b/openpype/hosts/houdini/plugins/create/create_bgeo.py index a3f31e7e94..0f629cf9c9 100644 --- a/openpype/hosts/houdini/plugins/create/create_bgeo.py +++ b/openpype/hosts/houdini/plugins/create/create_bgeo.py @@ -3,6 +3,7 @@ from openpype.hosts.houdini.api import plugin from openpype.pipeline import CreatedInstance, CreatorError from openpype.lib import EnumDef +import hou class CreateBGEO(plugin.HoudiniCreator): @@ -13,7 +14,6 @@ class CreateBGEO(plugin.HoudiniCreator): icon = "gears" def create(self, subset_name, instance_data, pre_create_data): - import hou instance_data.pop("active", None) @@ -90,3 +90,9 @@ class CreateBGEO(plugin.HoudiniCreator): return attrs + [ EnumDef("bgeo_type", bgeo_enum, label="BGEO Options"), ] + + def get_network_categories(self): + return [ + hou.ropNodeTypeCategory(), + hou.sopNodeTypeCategory() + ] diff --git a/openpype/hosts/houdini/plugins/create/create_hda.py b/openpype/hosts/houdini/plugins/create/create_hda.py index c4093bfbc6..ac075d2072 100644 --- a/openpype/hosts/houdini/plugins/create/create_hda.py +++ b/openpype/hosts/houdini/plugins/create/create_hda.py @@ -5,6 +5,7 @@ from openpype.client import ( get_subsets, ) from openpype.hosts.houdini.api import plugin +import hou class CreateHDA(plugin.HoudiniCreator): @@ -35,7 +36,6 @@ class CreateHDA(plugin.HoudiniCreator): def create_instance_node( self, node_name, parent, node_type="geometry"): - import hou parent_node = hou.node("/obj") if self.selected_nodes: @@ -81,3 +81,8 @@ class CreateHDA(plugin.HoudiniCreator): pre_create_data) # type: plugin.CreatedInstance return instance + + def get_network_categories(self): + return [ + hou.objNodeTypeCategory() + ] diff --git a/openpype/hosts/houdini/plugins/create/create_redshift_proxy.py b/openpype/hosts/houdini/plugins/create/create_redshift_proxy.py index b814dd9d57..3a4ab7008b 100644 --- a/openpype/hosts/houdini/plugins/create/create_redshift_proxy.py +++ b/openpype/hosts/houdini/plugins/create/create_redshift_proxy.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """Creator plugin for creating Redshift proxies.""" from openpype.hosts.houdini.api import plugin -from openpype.pipeline import CreatedInstance +import hou class CreateRedshiftProxy(plugin.HoudiniCreator): @@ -12,7 +12,7 @@ class CreateRedshiftProxy(plugin.HoudiniCreator): icon = "magic" def create(self, subset_name, instance_data, pre_create_data): - import hou # noqa + # Remove the active, we are checking the bypass flag of the nodes instance_data.pop("active", None) @@ -28,7 +28,7 @@ class CreateRedshiftProxy(plugin.HoudiniCreator): instance = super(CreateRedshiftProxy, self).create( subset_name, instance_data, - pre_create_data) # type: CreatedInstance + pre_create_data) instance_node = hou.node(instance.get("instance_node")) @@ -44,3 +44,9 @@ class CreateRedshiftProxy(plugin.HoudiniCreator): # Lock some Avalon attributes to_lock = ["family", "id", "prim_to_detail_pattern"] self.lock_parameters(instance_node, to_lock) + + def get_network_categories(self): + return [ + hou.ropNodeTypeCategory(), + hou.sopNodeTypeCategory() + ] From aed9b13e0048951f9cc8605415ea7d5142ca11cb Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 1 Nov 2023 15:15:09 +0200 Subject: [PATCH 18/54] update ids to 'ayon' instead of 'openpype' --- openpype/hosts/houdini/startup/MainMenuCommon.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/houdini/startup/MainMenuCommon.xml b/openpype/hosts/houdini/startup/MainMenuCommon.xml index b2e32a70f9..c875cac0f5 100644 --- a/openpype/hosts/houdini/startup/MainMenuCommon.xml +++ b/openpype/hosts/houdini/startup/MainMenuCommon.xml @@ -16,7 +16,7 @@ return label - + - + - + Date: Wed, 1 Nov 2023 15:35:09 +0200 Subject: [PATCH 19/54] update get_network_categories --- openpype/hosts/houdini/plugins/create/create_pointcache.py | 1 + openpype/hosts/houdini/plugins/create/create_staticmesh.py | 1 + openpype/hosts/houdini/plugins/create/create_vbd_cache.py | 1 + 3 files changed, 3 insertions(+) diff --git a/openpype/hosts/houdini/plugins/create/create_pointcache.py b/openpype/hosts/houdini/plugins/create/create_pointcache.py index 7eaf2aff2b..8fe8052e0a 100644 --- a/openpype/hosts/houdini/plugins/create/create_pointcache.py +++ b/openpype/hosts/houdini/plugins/create/create_pointcache.py @@ -83,6 +83,7 @@ class CreatePointCache(plugin.HoudiniCreator): def get_network_categories(self): return [ hou.ropNodeTypeCategory(), + hou.objNodeTypeCategory(), hou.sopNodeTypeCategory() ] diff --git a/openpype/hosts/houdini/plugins/create/create_staticmesh.py b/openpype/hosts/houdini/plugins/create/create_staticmesh.py index ea0b36f03f..d0985198bd 100644 --- a/openpype/hosts/houdini/plugins/create/create_staticmesh.py +++ b/openpype/hosts/houdini/plugins/create/create_staticmesh.py @@ -54,6 +54,7 @@ class CreateStaticMesh(plugin.HoudiniCreator): def get_network_categories(self): return [ hou.ropNodeTypeCategory(), + hou.objNodeTypeCategory(), hou.sopNodeTypeCategory() ] diff --git a/openpype/hosts/houdini/plugins/create/create_vbd_cache.py b/openpype/hosts/houdini/plugins/create/create_vbd_cache.py index 9c96e48e3a..69418f9575 100644 --- a/openpype/hosts/houdini/plugins/create/create_vbd_cache.py +++ b/openpype/hosts/houdini/plugins/create/create_vbd_cache.py @@ -40,6 +40,7 @@ class CreateVDBCache(plugin.HoudiniCreator): def get_network_categories(self): return [ hou.ropNodeTypeCategory(), + hou.objNodeTypeCategory(), hou.sopNodeTypeCategory() ] From 5a873368ee9b2544ea2c19401354ec5f94712537 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 1 Nov 2023 17:56:23 +0200 Subject: [PATCH 20/54] fix loading bug --- openpype/hosts/houdini/plugins/load/load_image.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/load/load_image.py b/openpype/hosts/houdini/plugins/load/load_image.py index 663a93e48b..cff2b74e52 100644 --- a/openpype/hosts/houdini/plugins/load/load_image.py +++ b/openpype/hosts/houdini/plugins/load/load_image.py @@ -119,7 +119,8 @@ class ImageLoader(load.LoaderPlugin): if not parent.children(): parent.destroy() - def _get_file_sequence(self, root): + def _get_file_sequence(self, file_path): + root = os.path.dirname(file_path) files = sorted(os.listdir(root)) first_fname = files[0] From dca872e1fce9f1735063769d17e8256f9c003125 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 1 Nov 2023 17:57:00 +0200 Subject: [PATCH 21/54] fix collector order to fix the missing frames --- openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py | 4 ++-- openpype/hosts/houdini/plugins/publish/collect_frames.py | 4 +++- openpype/hosts/houdini/plugins/publish/collect_karma_rop.py | 4 ++-- openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py | 4 ++-- .../hosts/houdini/plugins/publish/collect_redshift_rop.py | 4 ++-- openpype/hosts/houdini/plugins/publish/collect_vray_rop.py | 4 ++-- 6 files changed, 13 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py index b489f83b29..420a8324fe 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py @@ -21,8 +21,8 @@ class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin): label = "Arnold ROP Render Products" # This specific order value is used so that - # this plugin runs after CollectRopFrameRange - order = pyblish.api.CollectorOrder + 0.4999 + # this plugin runs after CollectFrames + order = pyblish.api.CollectorOrder + 0.49999 hosts = ["houdini"] families = ["arnold_rop"] diff --git a/openpype/hosts/houdini/plugins/publish/collect_frames.py b/openpype/hosts/houdini/plugins/publish/collect_frames.py index 01df809d4c..79cfcc6139 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_frames.py +++ b/openpype/hosts/houdini/plugins/publish/collect_frames.py @@ -11,7 +11,9 @@ from openpype.hosts.houdini.api import lib class CollectFrames(pyblish.api.InstancePlugin): """Collect all frames which would be saved from the ROP nodes""" - order = pyblish.api.CollectorOrder + 0.01 + # This specific order value is used so that + # this plugin runs after CollectRopFrameRange + order = pyblish.api.CollectorOrder + 0.4999 label = "Collect Frames" families = ["vdbcache", "imagesequence", "ass", "redshiftproxy", "review", "bgeo"] diff --git a/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py b/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py index fe0b8711fc..a477529df9 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py @@ -25,8 +25,8 @@ class CollectKarmaROPRenderProducts(pyblish.api.InstancePlugin): label = "Karma ROP Render Products" # This specific order value is used so that - # this plugin runs after CollectRopFrameRange - order = pyblish.api.CollectorOrder + 0.4999 + # this plugin runs after CollectFrames + order = pyblish.api.CollectorOrder + 0.49999 hosts = ["houdini"] families = ["karma_rop"] diff --git a/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py b/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py index cc412f30a1..9f0ae8d33c 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py @@ -25,8 +25,8 @@ class CollectMantraROPRenderProducts(pyblish.api.InstancePlugin): label = "Mantra ROP Render Products" # This specific order value is used so that - # this plugin runs after CollectRopFrameRange - order = pyblish.api.CollectorOrder + 0.4999 + # this plugin runs after CollectFrames + order = pyblish.api.CollectorOrder + 0.49999 hosts = ["houdini"] families = ["mantra_rop"] diff --git a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py index deb9eac971..0bd7b41641 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py @@ -25,8 +25,8 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin): label = "Redshift ROP Render Products" # This specific order value is used so that - # this plugin runs after CollectRopFrameRange - order = pyblish.api.CollectorOrder + 0.4999 + # this plugin runs after CollectFrames + order = pyblish.api.CollectorOrder + 0.49999 hosts = ["houdini"] families = ["redshift_rop"] diff --git a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py index 53072aebc6..519c12aede 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py @@ -25,8 +25,8 @@ class CollectVrayROPRenderProducts(pyblish.api.InstancePlugin): label = "VRay ROP Render Products" # This specific order value is used so that - # this plugin runs after CollectRopFrameRange - order = pyblish.api.CollectorOrder + 0.4999 + # this plugin runs after CollectFrames + order = pyblish.api.CollectorOrder + 0.49999 hosts = ["houdini"] families = ["vray_rop"] From c15adfa327cdb30f13c21f9e0f6c11d18c73ca9e Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 1 Nov 2023 18:05:23 +0200 Subject: [PATCH 22/54] BigRoys' commit - fallback to AYON --- openpype/hosts/houdini/api/creator_node_shelves.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/api/creator_node_shelves.py b/openpype/hosts/houdini/api/creator_node_shelves.py index 085642a277..14662dc419 100644 --- a/openpype/hosts/houdini/api/creator_node_shelves.py +++ b/openpype/hosts/houdini/api/creator_node_shelves.py @@ -173,7 +173,7 @@ def install(): os.remove(filepath) icon = get_openpype_icon_filepath() - tab_menu_label = os.environ.get("AVALON_LABEL") or "OpenPype" + tab_menu_label = os.environ.get("AVALON_LABEL") or "AYON" # Create context only to get creator plugins, so we don't reset and only # populate what we need to retrieve the list of creator plugins From c64af8ddef70412de33b8c04ff048cfbdc41d77d Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 1 Nov 2023 18:06:36 +0200 Subject: [PATCH 23/54] BigRoy's comment - remove Obj from network_categories to avoid possible confusion --- openpype/hosts/houdini/plugins/create/create_pointcache.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/create/create_pointcache.py b/openpype/hosts/houdini/plugins/create/create_pointcache.py index 8fe8052e0a..7eaf2aff2b 100644 --- a/openpype/hosts/houdini/plugins/create/create_pointcache.py +++ b/openpype/hosts/houdini/plugins/create/create_pointcache.py @@ -83,7 +83,6 @@ class CreatePointCache(plugin.HoudiniCreator): def get_network_categories(self): return [ hou.ropNodeTypeCategory(), - hou.objNodeTypeCategory(), hou.sopNodeTypeCategory() ] From bd8638caa10524dca1554208d4b301413729983b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 1 Nov 2023 17:16:37 +0100 Subject: [PATCH 24/54] ftrack events are not processed if project is not available in OpenPype database --- .../event_first_version_status.py | 45 ++++++++++++++----- .../event_next_task_update.py | 6 +++ .../event_push_frame_values_to_task.py | 6 +++ .../event_task_to_parent_status.py | 6 +++ .../event_task_to_version_status.py | 6 +++ .../event_thumbnail_updates.py | 6 +++ .../event_version_to_task_statuses.py | 5 +++ 7 files changed, 70 insertions(+), 10 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_server/event_first_version_status.py b/openpype/modules/ftrack/event_handlers_server/event_first_version_status.py index 8ef333effd..2ac02f233e 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_first_version_status.py +++ b/openpype/modules/ftrack/event_handlers_server/event_first_version_status.py @@ -1,3 +1,6 @@ +import collections + +from openpype.client import get_project from openpype_modules.ftrack.lib import BaseEvent @@ -73,8 +76,21 @@ class FirstVersionStatus(BaseEvent): if not self.task_status_map: return - entities_info = self.filter_event_ents(event) - if not entities_info: + filtered_entities_info = self.filter_entities_info(event) + if not filtered_entities_info: + return + + for project_id, entities_info in filtered_entities_info.items(): + self.process_by_project(session, event, project_id, entities_info) + + def process_by_project(self, session, event, project_id, entities_info): + project_name = self.get_project_name_from_event( + session, event, project_id + ) + if get_project(project_name) is None: + self.log.debug( + f"Project '{project_name}' not found in OpenPype. Skipping" + ) return entity_ids = [] @@ -154,18 +170,18 @@ class FirstVersionStatus(BaseEvent): exc_info=True ) - def filter_event_ents(self, event): - filtered_ents = [] - for entity in event["data"].get("entities", []): + def filter_entities_info(self, event): + filtered_entities_info = collections.defaultdict(list) + for entity_info in event["data"].get("entities", []): # Care only about add actions - if entity.get("action") != "add": + if entity_info.get("action") != "add": continue # Filter AssetVersions - if entity["entityType"] != "assetversion": + if entity_info["entityType"] != "assetversion": continue - entity_changes = entity.get("changes") or {} + entity_changes = entity_info.get("changes") or {} # Check if version of Asset Version is `1` version_num = entity_changes.get("version", {}).get("new") @@ -177,9 +193,18 @@ class FirstVersionStatus(BaseEvent): if not task_id: continue - filtered_ents.append(entity) + project_id = None + for parent_item in reversed(entity_info["parents"]): + if parent_item["entityType"] == "show": + project_id = parent_item["entityId"] + break - return filtered_ents + if project_id is None: + continue + + filtered_entities_info[project_id].append(entity_info) + + return filtered_entities_info def register(session): diff --git a/openpype/modules/ftrack/event_handlers_server/event_next_task_update.py b/openpype/modules/ftrack/event_handlers_server/event_next_task_update.py index a100c34f67..07a8ff433e 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_next_task_update.py +++ b/openpype/modules/ftrack/event_handlers_server/event_next_task_update.py @@ -1,4 +1,6 @@ import collections + +from openpype.client import get_project from openpype_modules.ftrack.lib import BaseEvent @@ -99,6 +101,10 @@ class NextTaskUpdate(BaseEvent): project_name = self.get_project_name_from_event( session, event, project_id ) + if get_project(project_name) is None: + self.log.debug("Project not found in OpenPype. Skipping") + return + # Load settings project_settings = self.get_project_settings_from_event( event, project_name diff --git a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py index ed630ad59d..65c3c1a69a 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py +++ b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py @@ -3,6 +3,8 @@ import copy from typing import Any import ftrack_api + +from openpype.client import get_project from openpype_modules.ftrack.lib import ( BaseEvent, query_custom_attributes, @@ -139,6 +141,10 @@ class PushHierValuesToNonHierEvent(BaseEvent): project_name: str = self.get_project_name_from_event( session, event, project_id ) + if get_project(project_name) is None: + self.log.debug("Project not found in OpenPype. Skipping") + return set(), set() + # Load settings project_settings: dict[str, Any] = ( self.get_project_settings_from_event(event, project_name) diff --git a/openpype/modules/ftrack/event_handlers_server/event_task_to_parent_status.py b/openpype/modules/ftrack/event_handlers_server/event_task_to_parent_status.py index 25fa3b0535..d2b395a1a3 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_task_to_parent_status.py +++ b/openpype/modules/ftrack/event_handlers_server/event_task_to_parent_status.py @@ -1,4 +1,6 @@ import collections + +from openpype.client import get_project from openpype_modules.ftrack.lib import BaseEvent @@ -60,6 +62,10 @@ class TaskStatusToParent(BaseEvent): project_name = self.get_project_name_from_event( session, event, project_id ) + if get_project(project_name) is None: + self.log.debug("Project not found in OpenPype. Skipping") + return + # Load settings project_settings = self.get_project_settings_from_event( event, project_name diff --git a/openpype/modules/ftrack/event_handlers_server/event_task_to_version_status.py b/openpype/modules/ftrack/event_handlers_server/event_task_to_version_status.py index b77849c678..91ee2410d7 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_task_to_version_status.py +++ b/openpype/modules/ftrack/event_handlers_server/event_task_to_version_status.py @@ -1,4 +1,6 @@ import collections + +from openpype.client import get_project from openpype_modules.ftrack.lib import BaseEvent @@ -102,6 +104,10 @@ class TaskToVersionStatus(BaseEvent): project_name = self.get_project_name_from_event( session, event, project_id ) + if get_project(project_name) is None: + self.log.debug("Project not found in OpenPype. Skipping") + return + # Load settings project_settings = self.get_project_settings_from_event( event, project_name diff --git a/openpype/modules/ftrack/event_handlers_server/event_thumbnail_updates.py b/openpype/modules/ftrack/event_handlers_server/event_thumbnail_updates.py index 64673f792c..318e69f414 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_thumbnail_updates.py +++ b/openpype/modules/ftrack/event_handlers_server/event_thumbnail_updates.py @@ -1,4 +1,6 @@ import collections + +from openpype.client import get_project from openpype_modules.ftrack.lib import BaseEvent @@ -22,6 +24,10 @@ class ThumbnailEvents(BaseEvent): project_name = self.get_project_name_from_event( session, event, project_id ) + if get_project(project_name) is None: + self.log.debug("Project not found in OpenPype. Skipping") + return + # Load settings project_settings = self.get_project_settings_from_event( event, project_name diff --git a/openpype/modules/ftrack/event_handlers_server/event_version_to_task_statuses.py b/openpype/modules/ftrack/event_handlers_server/event_version_to_task_statuses.py index fb40fd6417..fbe44bcba7 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_version_to_task_statuses.py +++ b/openpype/modules/ftrack/event_handlers_server/event_version_to_task_statuses.py @@ -1,3 +1,4 @@ +from openpype.client import get_project from openpype_modules.ftrack.lib import BaseEvent @@ -50,6 +51,10 @@ class VersionToTaskStatus(BaseEvent): project_name = self.get_project_name_from_event( session, event, project_id ) + if get_project(project_name) is None: + self.log.debug("Project not found in OpenPype. Skipping") + return + # Load settings project_settings = self.get_project_settings_from_event( event, project_name From d4b75797c69a59725e3d8348f44aefedb5136455 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 1 Nov 2023 20:49:05 +0100 Subject: [PATCH 25/54] nuke: making sure duplicated loader is not removed --- openpype/hosts/nuke/api/plugin.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index c39e3c339d..301b9533a9 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -537,6 +537,7 @@ class NukeLoader(LoaderPlugin): node.addKnob(knob) def clear_members(self, parent_node): + parent_class = parent_node.Class() members = self.get_members(parent_node) dependent_nodes = None @@ -549,6 +550,8 @@ class NukeLoader(LoaderPlugin): break for member in members: + if member.Class() == parent_class: + continue self.log.info("removing node: `{}".format(member.name())) nuke.delete(member) From e3eea5a8e35fedd12bebfdc0da77338850b22457 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 1 Nov 2023 20:49:36 +0100 Subject: [PATCH 26/54] Nuke: updating without node renameing --- openpype/hosts/nuke/plugins/load/load_clip.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/openpype/hosts/nuke/plugins/load/load_clip.py b/openpype/hosts/nuke/plugins/load/load_clip.py index 19038b168d..737da3746d 100644 --- a/openpype/hosts/nuke/plugins/load/load_clip.py +++ b/openpype/hosts/nuke/plugins/load/load_clip.py @@ -299,9 +299,6 @@ class LoadClip(plugin.NukeLoader): "Representation id `{}` is failing to load".format(repre_id)) return - read_name = self._get_node_name(representation) - - read_node["name"].setValue(read_name) read_node["file"].setValue(filepath) # to avoid multiple undo steps for rest of process From 83798a7b5e3daa3c71bb34d0849e70817730b311 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 1 Nov 2023 22:15:41 +0200 Subject: [PATCH 27/54] BigRoy's comment - fallback to 'AYON' --- openpype/hosts/houdini/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index ac375c56d6..db7f0886c3 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -1018,7 +1018,7 @@ def self_publish(): def add_self_publish_button(node): """Adds a self publish button to the rop node.""" - label = os.environ.get("AVALON_LABEL") or "OpenPype" + label = os.environ.get("AVALON_LABEL") or "AYON" button_parm = hou.ButtonParmTemplate( "ayon_self_publish", From 3a78230ba4c8fc69285d9b660499891245479765 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 1 Nov 2023 22:16:57 +0200 Subject: [PATCH 28/54] BigRoy's comment - fallback to 'AYON' --- openpype/hosts/houdini/startup/MainMenuCommon.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/startup/MainMenuCommon.xml b/openpype/hosts/houdini/startup/MainMenuCommon.xml index c875cac0f5..0903aef7bc 100644 --- a/openpype/hosts/houdini/startup/MainMenuCommon.xml +++ b/openpype/hosts/houdini/startup/MainMenuCommon.xml @@ -4,7 +4,7 @@ Date: Wed, 1 Nov 2023 21:35:20 +0100 Subject: [PATCH 29/54] Nuke: updating ls method to have full name and node --- openpype/hosts/nuke/api/pipeline.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/nuke/api/pipeline.py b/openpype/hosts/nuke/api/pipeline.py index a1d290646c..f6ba33f00f 100644 --- a/openpype/hosts/nuke/api/pipeline.py +++ b/openpype/hosts/nuke/api/pipeline.py @@ -478,8 +478,6 @@ def parse_container(node): """ data = read_avalon_data(node) - # (TODO) Remove key validation when `ls` has re-implemented. - # # If not all required data return the empty container required = ["schema", "id", "name", "namespace", "loader", "representation"] @@ -487,7 +485,10 @@ def parse_container(node): return # Store the node's name - data["objectName"] = node["name"].value() + data.update({ + "objectName": node.fullName(), + "node": node, + }) return data From 16aad9928823a10034d6d9bfc1ba7cb25fd24a53 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 1 Nov 2023 21:36:48 +0100 Subject: [PATCH 30/54] nuke: updating loaders so they are using nodes rather then objectName --- .../hosts/nuke/plugins/load/load_backdrop.py | 16 ++++---- .../nuke/plugins/load/load_camera_abc.py | 24 ++++++----- openpype/hosts/nuke/plugins/load/load_clip.py | 6 +-- .../hosts/nuke/plugins/load/load_effects.py | 30 +++++++------- .../nuke/plugins/load/load_effects_ip.py | 40 ++++++++----------- .../hosts/nuke/plugins/load/load_gizmo.py | 28 +++++++------ .../hosts/nuke/plugins/load/load_gizmo_ip.py | 28 +++++++------ .../hosts/nuke/plugins/load/load_image.py | 6 +-- .../hosts/nuke/plugins/load/load_model.py | 29 ++++++++------ .../hosts/nuke/plugins/load/load_ociolook.py | 25 +++++------- .../nuke/plugins/load/load_script_precomp.py | 7 +--- 11 files changed, 117 insertions(+), 122 deletions(-) diff --git a/openpype/hosts/nuke/plugins/load/load_backdrop.py b/openpype/hosts/nuke/plugins/load/load_backdrop.py index 0cbd380697..54d37da203 100644 --- a/openpype/hosts/nuke/plugins/load/load_backdrop.py +++ b/openpype/hosts/nuke/plugins/load/load_backdrop.py @@ -64,8 +64,7 @@ class LoadBackdropNodes(load.LoaderPlugin): data_imprint = { "version": vname, - "colorspaceInput": colorspace, - "objectName": object_name + "colorspaceInput": colorspace } for k in add_keys: @@ -194,7 +193,7 @@ class LoadBackdropNodes(load.LoaderPlugin): version_doc = get_version_by_id(project_name, representation["parent"]) # get corresponding node - GN = nuke.toNode(container['objectName']) + GN = container["node"] file = get_representation_path(representation).replace("\\", "/") @@ -207,10 +206,11 @@ class LoadBackdropNodes(load.LoaderPlugin): add_keys = ["source", "author", "fps"] - data_imprint = {"representation": str(representation["_id"]), - "version": vname, - "colorspaceInput": colorspace, - "objectName": object_name} + data_imprint = { + "representation": str(representation["_id"]), + "version": vname, + "colorspaceInput": colorspace, + } for k in add_keys: data_imprint.update({k: version_data[k]}) @@ -252,6 +252,6 @@ class LoadBackdropNodes(load.LoaderPlugin): self.update(container, representation) def remove(self, container): - node = nuke.toNode(container['objectName']) + node = container["node"] with viewer_update_and_undo_stop(): nuke.delete(node) diff --git a/openpype/hosts/nuke/plugins/load/load_camera_abc.py b/openpype/hosts/nuke/plugins/load/load_camera_abc.py index e245b0cb5e..898c5e4e7b 100644 --- a/openpype/hosts/nuke/plugins/load/load_camera_abc.py +++ b/openpype/hosts/nuke/plugins/load/load_camera_abc.py @@ -48,10 +48,11 @@ class AlembicCameraLoader(load.LoaderPlugin): # add additional metadata from the version to imprint to Avalon knob add_keys = ["source", "author", "fps"] - data_imprint = {"frameStart": first, - "frameEnd": last, - "version": vname, - "objectName": object_name} + data_imprint = { + "frameStart": first, + "frameEnd": last, + "version": vname, + } for k in add_keys: data_imprint.update({k: version_data[k]}) @@ -111,7 +112,7 @@ class AlembicCameraLoader(load.LoaderPlugin): project_name = get_current_project_name() version_doc = get_version_by_id(project_name, representation["parent"]) - object_name = container['objectName'] + object_name = container["node"] # get main variables version_data = version_doc.get("data", {}) @@ -124,11 +125,12 @@ class AlembicCameraLoader(load.LoaderPlugin): # add additional metadata from the version to imprint to Avalon knob add_keys = ["source", "author", "fps"] - data_imprint = {"representation": str(representation["_id"]), - "frameStart": first, - "frameEnd": last, - "version": vname, - "objectName": object_name} + data_imprint = { + "representation": str(representation["_id"]), + "frameStart": first, + "frameEnd": last, + "version": vname + } for k in add_keys: data_imprint.update({k: version_data[k]}) @@ -194,6 +196,6 @@ class AlembicCameraLoader(load.LoaderPlugin): self.update(container, representation) def remove(self, container): - node = nuke.toNode(container['objectName']) + node = container["node"] with viewer_update_and_undo_stop(): nuke.delete(node) diff --git a/openpype/hosts/nuke/plugins/load/load_clip.py b/openpype/hosts/nuke/plugins/load/load_clip.py index 737da3746d..3a2ec3dbee 100644 --- a/openpype/hosts/nuke/plugins/load/load_clip.py +++ b/openpype/hosts/nuke/plugins/load/load_clip.py @@ -189,8 +189,6 @@ class LoadClip(plugin.NukeLoader): value_ = value_.replace("\\", "/") data_imprint[key] = value_ - data_imprint["objectName"] = read_name - if add_retime and version_data.get("retime", None): data_imprint["addRetime"] = True @@ -254,7 +252,7 @@ class LoadClip(plugin.NukeLoader): is_sequence = len(representation["files"]) > 1 - read_node = nuke.toNode(container['objectName']) + read_node = container["node"] if is_sequence: representation = self._representation_with_hash_in_frame( @@ -353,7 +351,7 @@ class LoadClip(plugin.NukeLoader): self.set_as_member(read_node) def remove(self, container): - read_node = nuke.toNode(container['objectName']) + read_node = container["node"] assert read_node.Class() == "Read", "Must be Read" with viewer_update_and_undo_stop(): diff --git a/openpype/hosts/nuke/plugins/load/load_effects.py b/openpype/hosts/nuke/plugins/load/load_effects.py index cacc00854e..cc048372d4 100644 --- a/openpype/hosts/nuke/plugins/load/load_effects.py +++ b/openpype/hosts/nuke/plugins/load/load_effects.py @@ -62,11 +62,12 @@ class LoadEffects(load.LoaderPlugin): add_keys = ["frameStart", "frameEnd", "handleStart", "handleEnd", "source", "author", "fps"] - data_imprint = {"frameStart": first, - "frameEnd": last, - "version": vname, - "colorspaceInput": colorspace, - "objectName": object_name} + data_imprint = { + "frameStart": first, + "frameEnd": last, + "version": vname, + "colorspaceInput": colorspace, + } for k in add_keys: data_imprint.update({k: version_data[k]}) @@ -159,7 +160,7 @@ class LoadEffects(load.LoaderPlugin): version_doc = get_version_by_id(project_name, representation["parent"]) # get corresponding node - GN = nuke.toNode(container['objectName']) + GN = container["node"] file = get_representation_path(representation).replace("\\", "/") name = container['name'] @@ -175,12 +176,13 @@ class LoadEffects(load.LoaderPlugin): add_keys = ["frameStart", "frameEnd", "handleStart", "handleEnd", "source", "author", "fps"] - data_imprint = {"representation": str(representation["_id"]), - "frameStart": first, - "frameEnd": last, - "version": vname, - "colorspaceInput": colorspace, - "objectName": object_name} + data_imprint = { + "representation": str(representation["_id"]), + "frameStart": first, + "frameEnd": last, + "version": vname, + "colorspaceInput": colorspace + } for k in add_keys: data_imprint.update({k: version_data[k]}) @@ -212,7 +214,7 @@ class LoadEffects(load.LoaderPlugin): pre_node = nuke.createNode("Input") pre_node["name"].setValue("rgb") - for ef_name, ef_val in nodes_order.items(): + for _, ef_val in nodes_order.items(): node = nuke.createNode(ef_val["class"]) for k, v in ef_val["node"].items(): if k in self.ignore_attr: @@ -346,6 +348,6 @@ class LoadEffects(load.LoaderPlugin): self.update(container, representation) def remove(self, container): - node = nuke.toNode(container['objectName']) + node = container["node"] with viewer_update_and_undo_stop(): nuke.delete(node) diff --git a/openpype/hosts/nuke/plugins/load/load_effects_ip.py b/openpype/hosts/nuke/plugins/load/load_effects_ip.py index bdf3cd6965..cdfdfef3b8 100644 --- a/openpype/hosts/nuke/plugins/load/load_effects_ip.py +++ b/openpype/hosts/nuke/plugins/load/load_effects_ip.py @@ -63,11 +63,12 @@ class LoadEffectsInputProcess(load.LoaderPlugin): add_keys = ["frameStart", "frameEnd", "handleStart", "handleEnd", "source", "author", "fps"] - data_imprint = {"frameStart": first, - "frameEnd": last, - "version": vname, - "colorspaceInput": colorspace, - "objectName": object_name} + data_imprint = { + "frameStart": first, + "frameEnd": last, + "version": vname, + "colorspaceInput": colorspace, + } for k in add_keys: data_imprint.update({k: version_data[k]}) @@ -98,7 +99,7 @@ class LoadEffectsInputProcess(load.LoaderPlugin): pre_node = nuke.createNode("Input") pre_node["name"].setValue("rgb") - for ef_name, ef_val in nodes_order.items(): + for _, ef_val in nodes_order.items(): node = nuke.createNode(ef_val["class"]) for k, v in ef_val["node"].items(): if k in self.ignore_attr: @@ -164,28 +165,26 @@ class LoadEffectsInputProcess(load.LoaderPlugin): version_doc = get_version_by_id(project_name, representation["parent"]) # get corresponding node - GN = nuke.toNode(container['objectName']) + GN = container["node"] file = get_representation_path(representation).replace("\\", "/") - name = container['name'] version_data = version_doc.get("data", {}) vname = version_doc.get("name", None) first = version_data.get("frameStart", None) last = version_data.get("frameEnd", None) workfile_first_frame = int(nuke.root()["first_frame"].getValue()) - namespace = container['namespace'] colorspace = version_data.get("colorspace", None) - object_name = "{}_{}".format(name, namespace) add_keys = ["frameStart", "frameEnd", "handleStart", "handleEnd", "source", "author", "fps"] - data_imprint = {"representation": str(representation["_id"]), - "frameStart": first, - "frameEnd": last, - "version": vname, - "colorspaceInput": colorspace, - "objectName": object_name} + data_imprint = { + "representation": str(representation["_id"]), + "frameStart": first, + "frameEnd": last, + "version": vname, + "colorspaceInput": colorspace, + } for k in add_keys: data_imprint.update({k: version_data[k]}) @@ -217,7 +216,7 @@ class LoadEffectsInputProcess(load.LoaderPlugin): pre_node = nuke.createNode("Input") pre_node["name"].setValue("rgb") - for ef_name, ef_val in nodes_order.items(): + for _, ef_val in nodes_order.items(): node = nuke.createNode(ef_val["class"]) for k, v in ef_val["node"].items(): if k in self.ignore_attr: @@ -251,11 +250,6 @@ class LoadEffectsInputProcess(load.LoaderPlugin): output = nuke.createNode("Output") output.setInput(0, pre_node) - # # try to place it under Viewer1 - # if not self.connect_active_viewer(GN): - # nuke.delete(GN) - # return - # get all versions in list last_version_doc = get_last_version_by_subset_id( project_name, version_doc["parent"], fields=["_id"] @@ -365,6 +359,6 @@ class LoadEffectsInputProcess(load.LoaderPlugin): self.update(container, representation) def remove(self, container): - node = nuke.toNode(container['objectName']) + node = container["node"] with viewer_update_and_undo_stop(): nuke.delete(node) diff --git a/openpype/hosts/nuke/plugins/load/load_gizmo.py b/openpype/hosts/nuke/plugins/load/load_gizmo.py index 23cf4d7741..19b5cca74e 100644 --- a/openpype/hosts/nuke/plugins/load/load_gizmo.py +++ b/openpype/hosts/nuke/plugins/load/load_gizmo.py @@ -64,11 +64,12 @@ class LoadGizmo(load.LoaderPlugin): add_keys = ["frameStart", "frameEnd", "handleStart", "handleEnd", "source", "author", "fps"] - data_imprint = {"frameStart": first, - "frameEnd": last, - "version": vname, - "colorspaceInput": colorspace, - "objectName": object_name} + data_imprint = { + "frameStart": first, + "frameEnd": last, + "version": vname, + "colorspaceInput": colorspace + } for k in add_keys: data_imprint.update({k: version_data[k]}) @@ -111,7 +112,7 @@ class LoadGizmo(load.LoaderPlugin): version_doc = get_version_by_id(project_name, representation["parent"]) # get corresponding node - group_node = nuke.toNode(container['objectName']) + group_node = container["node"] file = get_representation_path(representation).replace("\\", "/") name = container['name'] @@ -126,12 +127,13 @@ class LoadGizmo(load.LoaderPlugin): add_keys = ["frameStart", "frameEnd", "handleStart", "handleEnd", "source", "author", "fps"] - data_imprint = {"representation": str(representation["_id"]), - "frameStart": first, - "frameEnd": last, - "version": vname, - "colorspaceInput": colorspace, - "objectName": object_name} + data_imprint = { + "representation": str(representation["_id"]), + "frameStart": first, + "frameEnd": last, + "version": vname, + "colorspaceInput": colorspace + } for k in add_keys: data_imprint.update({k: version_data[k]}) @@ -175,6 +177,6 @@ class LoadGizmo(load.LoaderPlugin): self.update(container, representation) def remove(self, container): - node = nuke.toNode(container['objectName']) + node = container["node"] with viewer_update_and_undo_stop(): nuke.delete(node) diff --git a/openpype/hosts/nuke/plugins/load/load_gizmo_ip.py b/openpype/hosts/nuke/plugins/load/load_gizmo_ip.py index ce0a1615f1..5b4877678a 100644 --- a/openpype/hosts/nuke/plugins/load/load_gizmo_ip.py +++ b/openpype/hosts/nuke/plugins/load/load_gizmo_ip.py @@ -66,11 +66,12 @@ class LoadGizmoInputProcess(load.LoaderPlugin): add_keys = ["frameStart", "frameEnd", "handleStart", "handleEnd", "source", "author", "fps"] - data_imprint = {"frameStart": first, - "frameEnd": last, - "version": vname, - "colorspaceInput": colorspace, - "objectName": object_name} + data_imprint = { + "frameStart": first, + "frameEnd": last, + "version": vname, + "colorspaceInput": colorspace + } for k in add_keys: data_imprint.update({k: version_data[k]}) @@ -118,7 +119,7 @@ class LoadGizmoInputProcess(load.LoaderPlugin): version_doc = get_version_by_id(project_name, representation["parent"]) # get corresponding node - group_node = nuke.toNode(container['objectName']) + group_node = container["node"] file = get_representation_path(representation).replace("\\", "/") name = container['name'] @@ -133,12 +134,13 @@ class LoadGizmoInputProcess(load.LoaderPlugin): add_keys = ["frameStart", "frameEnd", "handleStart", "handleEnd", "source", "author", "fps"] - data_imprint = {"representation": str(representation["_id"]), - "frameStart": first, - "frameEnd": last, - "version": vname, - "colorspaceInput": colorspace, - "objectName": object_name} + data_imprint = { + "representation": str(representation["_id"]), + "frameStart": first, + "frameEnd": last, + "version": vname, + "colorspaceInput": colorspace + } for k in add_keys: data_imprint.update({k: version_data[k]}) @@ -256,6 +258,6 @@ class LoadGizmoInputProcess(load.LoaderPlugin): self.update(container, representation) def remove(self, container): - node = nuke.toNode(container['objectName']) + node = container["node"] with viewer_update_and_undo_stop(): nuke.delete(node) diff --git a/openpype/hosts/nuke/plugins/load/load_image.py b/openpype/hosts/nuke/plugins/load/load_image.py index 6bffb97e6f..411a61d77b 100644 --- a/openpype/hosts/nuke/plugins/load/load_image.py +++ b/openpype/hosts/nuke/plugins/load/load_image.py @@ -146,8 +146,6 @@ class LoadImage(load.LoaderPlugin): data_imprint.update( {k: context["version"]['data'].get(k, str(None))}) - data_imprint.update({"objectName": read_name}) - r["tile_color"].setValue(int("0x4ecd25ff", 16)) return containerise(r, @@ -168,7 +166,7 @@ class LoadImage(load.LoaderPlugin): inputs: """ - node = nuke.toNode(container["objectName"]) + node = container["node"] frame_number = node["first"].value() assert node.Class() == "Read", "Must be Read" @@ -237,7 +235,7 @@ class LoadImage(load.LoaderPlugin): self.log.info("updated to version: {}".format(version_doc.get("name"))) def remove(self, container): - node = nuke.toNode(container['objectName']) + node = container["node"] assert node.Class() == "Read", "Must be Read" with viewer_update_and_undo_stop(): diff --git a/openpype/hosts/nuke/plugins/load/load_model.py b/openpype/hosts/nuke/plugins/load/load_model.py index b9b8a0f4c0..3fe92b74d0 100644 --- a/openpype/hosts/nuke/plugins/load/load_model.py +++ b/openpype/hosts/nuke/plugins/load/load_model.py @@ -46,10 +46,11 @@ class AlembicModelLoader(load.LoaderPlugin): # add additional metadata from the version to imprint to Avalon knob add_keys = ["source", "author", "fps"] - data_imprint = {"frameStart": first, - "frameEnd": last, - "version": vname, - "objectName": object_name} + data_imprint = { + "frameStart": first, + "frameEnd": last, + "version": vname + } for k in add_keys: data_imprint.update({k: version_data[k]}) @@ -114,9 +115,9 @@ class AlembicModelLoader(load.LoaderPlugin): # Get version from io project_name = get_current_project_name() version_doc = get_version_by_id(project_name, representation["parent"]) - object_name = container['objectName'] + # get corresponding node - model_node = nuke.toNode(object_name) + model_node = container["node"] # get main variables version_data = version_doc.get("data", {}) @@ -129,11 +130,12 @@ class AlembicModelLoader(load.LoaderPlugin): # add additional metadata from the version to imprint to Avalon knob add_keys = ["source", "author", "fps"] - data_imprint = {"representation": str(representation["_id"]), - "frameStart": first, - "frameEnd": last, - "version": vname, - "objectName": object_name} + data_imprint = { + "representation": str(representation["_id"]), + "frameStart": first, + "frameEnd": last, + "version": vname + } for k in add_keys: data_imprint.update({k: version_data[k]}) @@ -142,7 +144,6 @@ class AlembicModelLoader(load.LoaderPlugin): file = get_representation_path(representation).replace("\\", "/") with maintained_selection(): - model_node = nuke.toNode(object_name) model_node['selected'].setValue(True) # collect input output dependencies @@ -163,8 +164,10 @@ class AlembicModelLoader(load.LoaderPlugin): ypos = model_node.ypos() nuke.nodeCopy("%clipboard%") nuke.delete(model_node) + + # paste the node back and set the position nuke.nodePaste("%clipboard%") - model_node = nuke.toNode(object_name) + model_node = nuke.selectedNode() model_node.setXYpos(xpos, ypos) # link to original input nodes diff --git a/openpype/hosts/nuke/plugins/load/load_ociolook.py b/openpype/hosts/nuke/plugins/load/load_ociolook.py index 18c8cdba35..c0f8235253 100644 --- a/openpype/hosts/nuke/plugins/load/load_ociolook.py +++ b/openpype/hosts/nuke/plugins/load/load_ociolook.py @@ -55,7 +55,7 @@ class LoadOcioLookNodes(load.LoaderPlugin): """ namespace = namespace or context['asset']['name'] suffix = secrets.token_hex(nbytes=4) - object_name = "{}_{}_{}".format( + node_name = "{}_{}_{}".format( name, namespace, suffix) # getting file path @@ -64,7 +64,9 @@ class LoadOcioLookNodes(load.LoaderPlugin): json_f = self._load_json_data(filepath) group_node = self._create_group_node( - object_name, filepath, json_f["data"]) + filepath, json_f["data"]) + # renaming group node + group_node["name"].setValue(node_name) self._node_version_color(context["version"], group_node) @@ -76,17 +78,14 @@ class LoadOcioLookNodes(load.LoaderPlugin): name=name, namespace=namespace, context=context, - loader=self.__class__.__name__, - data={ - "objectName": object_name, - } + loader=self.__class__.__name__ ) def _create_group_node( self, - object_name, filepath, - data + data, + group_node=None ): """Creates group node with all the nodes inside. @@ -94,9 +93,9 @@ class LoadOcioLookNodes(load.LoaderPlugin): in between - in case those are needed. Arguments: - object_name (str): name of the group node filepath (str): path to json file data (dict): data from json file + group_node (Optional[nuke.Node]): group node or None Returns: nuke.Node: group node with all the nodes inside @@ -117,7 +116,6 @@ class LoadOcioLookNodes(load.LoaderPlugin): input_node = None output_node = None - group_node = nuke.toNode(object_name) if group_node: # remove all nodes between Input and Output nodes for node in group_node.nodes(): @@ -130,7 +128,6 @@ class LoadOcioLookNodes(load.LoaderPlugin): else: group_node = nuke.createNode( "Group", - "name {}_1".format(object_name), inpanel=False ) @@ -227,16 +224,16 @@ class LoadOcioLookNodes(load.LoaderPlugin): project_name = get_current_project_name() version_doc = get_version_by_id(project_name, representation["parent"]) - object_name = container['objectName'] + group_node = container["node"] filepath = get_representation_path(representation) json_f = self._load_json_data(filepath) group_node = self._create_group_node( - object_name, filepath, - json_f["data"] + json_f["data"], + group_node ) self._node_version_color(version_doc, group_node) diff --git a/openpype/hosts/nuke/plugins/load/load_script_precomp.py b/openpype/hosts/nuke/plugins/load/load_script_precomp.py index d5f9d24765..cbe19d217b 100644 --- a/openpype/hosts/nuke/plugins/load/load_script_precomp.py +++ b/openpype/hosts/nuke/plugins/load/load_script_precomp.py @@ -46,8 +46,6 @@ class LinkAsGroup(load.LoaderPlugin): file = self.filepath_from_context(context).replace("\\", "/") self.log.info("file: {}\n".format(file)) - precomp_name = context["representation"]["context"]["subset"] - self.log.info("versionData: {}\n".format(context["version"]["data"])) # add additional metadata from the version to imprint to Avalon knob @@ -62,7 +60,6 @@ class LinkAsGroup(load.LoaderPlugin): } for k in add_keys: data_imprint.update({k: context["version"]['data'][k]}) - data_imprint.update({"objectName": precomp_name}) # group context is set to precomp, so back up one level. nuke.endGroup() @@ -118,7 +115,7 @@ class LinkAsGroup(load.LoaderPlugin): inputs: """ - node = nuke.toNode(container['objectName']) + node = container["node"] root = get_representation_path(representation).replace("\\", "/") @@ -159,6 +156,6 @@ class LinkAsGroup(load.LoaderPlugin): self.log.info("updated to version: {}".format(version_doc.get("name"))) def remove(self, container): - node = nuke.toNode(container['objectName']) + node = container["node"] with viewer_update_and_undo_stop(): nuke.delete(node) From 8bf570ceef7823a2bf8615a30ac15dfc59c8eaf7 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 2 Nov 2023 12:24:25 +0800 Subject: [PATCH 31/54] up version for the max bundle --- server_addon/max/server/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/max/server/version.py b/server_addon/max/server/version.py index 3dc1f76bc6..485f44ac21 100644 --- a/server_addon/max/server/version.py +++ b/server_addon/max/server/version.py @@ -1 +1 @@ -__version__ = "0.1.0" +__version__ = "0.1.1" From 6a619d023b4f4bac7d48883bd16081193950d950 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 3 Nov 2023 00:53:37 +0100 Subject: [PATCH 32/54] Remove on instance toggled callback which isn't relevant to the new publisher --- openpype/hosts/houdini/api/pipeline.py | 56 ------------------- .../publish/collect_instances_usd_layered.py | 4 -- 2 files changed, 60 deletions(-) diff --git a/openpype/hosts/houdini/api/pipeline.py b/openpype/hosts/houdini/api/pipeline.py index f8db45c56b..11135e20b2 100644 --- a/openpype/hosts/houdini/api/pipeline.py +++ b/openpype/hosts/houdini/api/pipeline.py @@ -3,7 +3,6 @@ import os import sys import logging -import contextlib import hou # noqa @@ -66,10 +65,6 @@ class HoudiniHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): register_event_callback("open", on_open) register_event_callback("new", on_new) - pyblish.api.register_callback( - "instanceToggled", on_pyblish_instance_toggled - ) - self._has_been_setup = True # add houdini vendor packages hou_pythonpath = os.path.join(HOUDINI_HOST_DIR, "vendor") @@ -406,54 +401,3 @@ def _set_context_settings(): lib.reset_framerange() lib.update_houdini_vars_context() - - -def on_pyblish_instance_toggled(instance, new_value, old_value): - """Toggle saver tool passthrough states on instance toggles.""" - @contextlib.contextmanager - def main_take(no_update=True): - """Enter root take during context""" - original_take = hou.takes.currentTake() - original_update_mode = hou.updateModeSetting() - root = hou.takes.rootTake() - has_changed = False - try: - if original_take != root: - has_changed = True - if no_update: - hou.setUpdateMode(hou.updateMode.Manual) - hou.takes.setCurrentTake(root) - yield - finally: - if has_changed: - if no_update: - hou.setUpdateMode(original_update_mode) - hou.takes.setCurrentTake(original_take) - - if not instance.data.get("_allowToggleBypass", True): - return - - nodes = instance[:] - if not nodes: - return - - # Assume instance node is first node - instance_node = nodes[0] - - if not hasattr(instance_node, "isBypassed"): - # Likely not a node that can actually be bypassed - log.debug("Can't bypass node: %s", instance_node.path()) - return - - if instance_node.isBypassed() != (not old_value): - print("%s old bypass state didn't match old instance state, " - "updating anyway.." % instance_node.path()) - - try: - # Go into the main take, because when in another take changing - # the bypass state of a note cannot be done due to it being locked - # by default. - with main_take(no_update=True): - instance_node.bypass(not new_value) - except hou.PermissionError as exc: - log.warning("%s - %s", instance_node.path(), exc) diff --git a/openpype/hosts/houdini/plugins/publish/collect_instances_usd_layered.py b/openpype/hosts/houdini/plugins/publish/collect_instances_usd_layered.py index 0600730d00..d154cdc7c0 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_instances_usd_layered.py +++ b/openpype/hosts/houdini/plugins/publish/collect_instances_usd_layered.py @@ -122,10 +122,6 @@ class CollectInstancesUsdLayered(pyblish.api.ContextPlugin): instance.data.update(save_data) instance.data["usdLayer"] = layer - # Don't allow the Pyblish `instanceToggled` we have installed - # to set this node to bypass. - instance.data["_allowToggleBypass"] = False - instances.append(instance) # Store the collected ROP node dependencies From 8fb7266ff8f0ea9c3b3f387ca120280093993294 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Fri, 3 Nov 2023 11:18:09 +0200 Subject: [PATCH 33/54] Add CollectAssetHandles and modify associated files --- openpype/hosts/houdini/api/lib.py | 20 +-- .../plugins/publish/collect_arnold_rop.py | 2 +- .../plugins/publish/collect_asset_handles.py | 125 ++++++++++++++++++ .../houdini/plugins/publish/collect_frames.py | 10 +- .../plugins/publish/collect_karma_rop.py | 2 +- .../plugins/publish/collect_mantra_rop.py | 2 +- .../plugins/publish/collect_redshift_rop.py | 2 +- .../publish/collect_rop_frame_range.py | 79 +---------- .../plugins/publish/collect_vray_rop.py | 2 +- .../plugins/publish/validate_frame_range.py | 2 +- .../defaults/project_settings/houdini.json | 2 +- .../schemas/schema_houdini_publish.json | 4 +- .../houdini/server/settings/publish.py | 2 +- 13 files changed, 151 insertions(+), 103 deletions(-) create mode 100644 openpype/hosts/houdini/plugins/publish/collect_asset_handles.py diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index ac375c56d6..c6722fb1bb 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -569,9 +569,9 @@ def get_template_from_value(key, value): return parm -def get_frame_data(node, handle_start=0, handle_end=0, log=None): - """Get the frame data: start frame, end frame, steps, - start frame with start handle and end frame with end handle. +def get_frame_data(node, log=None): + """Get the frame data: `frameStartHandle`, `frameEndHandle` + and `byFrameStep`. This function uses Houdini node's `trange`, `t1, `t2` and `t3` parameters as the source of truth for the full inclusive frame @@ -579,20 +579,17 @@ def get_frame_data(node, handle_start=0, handle_end=0, log=None): range including the handles. The non-inclusive frame start and frame end without handles - are computed by subtracting the handles from the inclusive + can be computed by subtracting the handles from the inclusive frame range. Args: node (hou.Node): ROP node to retrieve frame range from, the frame range is assumed to be the frame range *including* the start and end handles. - handle_start (int): Start handles. - handle_end (int): End handles. - log (logging.Logger): Logger to log to. Returns: - dict: frame data for start, end, steps, - start with handle and end with handle + dict: frame data for `frameStartHandle`, `frameEndHandle` + and `byFrameStep`. """ @@ -623,11 +620,6 @@ def get_frame_data(node, handle_start=0, handle_end=0, log=None): data["frameEndHandle"] = int(node.evalParm("f2")) data["byFrameStep"] = node.evalParm("f3") - data["handleStart"] = handle_start - data["handleEnd"] = handle_end - data["frameStart"] = data["frameStartHandle"] + data["handleStart"] - data["frameEnd"] = data["frameEndHandle"] - data["handleEnd"] - return data diff --git a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py index 420a8324fe..d95f763826 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py @@ -22,7 +22,7 @@ class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin): label = "Arnold ROP Render Products" # This specific order value is used so that # this plugin runs after CollectFrames - order = pyblish.api.CollectorOrder + 0.49999 + order = pyblish.api.CollectorOrder + 0.11 hosts = ["houdini"] families = ["arnold_rop"] diff --git a/openpype/hosts/houdini/plugins/publish/collect_asset_handles.py b/openpype/hosts/houdini/plugins/publish/collect_asset_handles.py new file mode 100644 index 0000000000..5c11948608 --- /dev/null +++ b/openpype/hosts/houdini/plugins/publish/collect_asset_handles.py @@ -0,0 +1,125 @@ +# -*- coding: utf-8 -*- +"""Collector plugin for frames data on ROP instances.""" +import hou # noqa +import pyblish.api +from openpype.lib import BoolDef +from openpype.pipeline import OpenPypePyblishPluginMixin + + +class CollectAssetHandles(pyblish.api.InstancePlugin, + OpenPypePyblishPluginMixin): + """Apply asset handles. + + If instance does not have: + - frameStart + - frameEnd + - handleStart + - handleEnd + But it does have: + - frameStartHandle + - frameEndHandle + + Then we will retrieve the asset's handles to compute + the exclusive frame range and actual handle ranges. + """ + + hosts = ["houdini"] + + # This specific order value is used so that + # this plugin runs after CollectAnatomyInstanceData + order = pyblish.api.CollectorOrder + 0.499 + + label = "Collect Asset Handles" + use_asset_handles = True + + + def process(self, instance): + # Only process instances without already existing handles data + # but that do have frameStartHandle and frameEndHandle defined + # like the data collected from CollectRopFrameRange + if "frameStartHandle" not in instance.data: + return + if "frameEndHandle" not in instance.data: + return + + has_existing_data = { + "handleStart", + "handleEnd", + "frameStart", + "frameEnd" + }.issubset(instance.data) + if has_existing_data: + return + + attr_values = self.get_attr_values_from_data(instance.data) + if attr_values.get("use_handles", self.use_asset_handles): + asset_data = instance.data["assetEntity"]["data"] + handle_start = asset_data.get("handleStart", 0) + handle_end = asset_data.get("handleEnd", 0) + else: + handle_start = 0 + handle_end = 0 + + frame_start = instance.data["frameStartHandle"] + handle_start + frame_end = instance.data["frameEndHandle"] - handle_end + + instance.data.update({ + "handleStart": handle_start, + "handleEnd": handle_end, + "frameStart": frame_start, + "frameEnd": frame_end + }) + + # Log debug message about the collected frame range + if attr_values.get("use_handles", self.use_asset_handles): + self.log.debug( + "Full Frame range with Handles " + "[{frame_start_handle} - {frame_end_handle}]" + .format( + frame_start_handle=instance.data["frameStartHandle"], + frame_end_handle=instance.data["frameEndHandle"] + ) + ) + else: + self.log.debug( + "Use handles is deactivated for this instance, " + "start and end handles are set to 0." + ) + + # Log collected frame range to the user + message = "Frame range [{frame_start} - {frame_end}]".format( + frame_start=frame_start, + frame_end=frame_end + ) + if handle_start or handle_end: + message += " with handles [{handle_start}]-[{handle_end}]".format( + handle_start=handle_start, + handle_end=handle_end + ) + self.log.info(message) + + if instance.data.get("byFrameStep", 1.0) != 1.0: + self.log.info( + "Frame steps {}".format(instance.data["byFrameStep"])) + + # Add frame range to label if the instance has a frame range. + label = instance.data.get("label", instance.data["name"]) + instance.data["label"] = ( + "{label} [{frame_start} - {frame_end}]" + .format( + label=label, + frame_start=frame_start, + frame_end=frame_end + ) + ) + + @classmethod + def get_attribute_defs(cls): + return [ + BoolDef("use_handles", + tooltip="Disable this if you want the publisher to" + " ignore start and end handles specified in the" + " asset data for this publish instance", + default=cls.use_asset_handles, + label="Use asset handles") + ] diff --git a/openpype/hosts/houdini/plugins/publish/collect_frames.py b/openpype/hosts/houdini/plugins/publish/collect_frames.py index 79cfcc6139..f6f538f5a5 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_frames.py +++ b/openpype/hosts/houdini/plugins/publish/collect_frames.py @@ -13,7 +13,7 @@ class CollectFrames(pyblish.api.InstancePlugin): # This specific order value is used so that # this plugin runs after CollectRopFrameRange - order = pyblish.api.CollectorOrder + 0.4999 + order = pyblish.api.CollectorOrder + 0.1 label = "Collect Frames" families = ["vdbcache", "imagesequence", "ass", "redshiftproxy", "review", "bgeo"] @@ -22,8 +22,8 @@ class CollectFrames(pyblish.api.InstancePlugin): ropnode = hou.node(instance.data["instance_node"]) - start_frame = instance.data.get("frameStart", None) - end_frame = instance.data.get("frameEnd", None) + start_frame = instance.data.get("frameStartHandle", None) + end_frame = instance.data.get("frameEndHandle", None) output_parm = lib.get_output_parameter(ropnode) if start_frame is not None: @@ -53,7 +53,7 @@ class CollectFrames(pyblish.api.InstancePlugin): # Check if frames are bigger than 1 (file collection) # override the result if end_frame - start_frame > 0: - result = self.create_file_list( + result = self.create_file_list(self, match, int(start_frame), int(end_frame) ) @@ -62,7 +62,7 @@ class CollectFrames(pyblish.api.InstancePlugin): instance.data.update({"frames": result}) @staticmethod - def create_file_list(match, start_frame, end_frame): + def create_file_list(self,match, start_frame, end_frame): """Collect files based on frame range and `regex.match` Args: diff --git a/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py b/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py index a477529df9..dac350a6ef 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py @@ -26,7 +26,7 @@ class CollectKarmaROPRenderProducts(pyblish.api.InstancePlugin): label = "Karma ROP Render Products" # This specific order value is used so that # this plugin runs after CollectFrames - order = pyblish.api.CollectorOrder + 0.49999 + order = pyblish.api.CollectorOrder + 0.11 hosts = ["houdini"] families = ["karma_rop"] diff --git a/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py b/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py index 9f0ae8d33c..a3e7927807 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py @@ -26,7 +26,7 @@ class CollectMantraROPRenderProducts(pyblish.api.InstancePlugin): label = "Mantra ROP Render Products" # This specific order value is used so that # this plugin runs after CollectFrames - order = pyblish.api.CollectorOrder + 0.49999 + order = pyblish.api.CollectorOrder + 0.11 hosts = ["houdini"] families = ["mantra_rop"] diff --git a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py index 0bd7b41641..0acddab011 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py @@ -26,7 +26,7 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin): label = "Redshift ROP Render Products" # This specific order value is used so that # this plugin runs after CollectFrames - order = pyblish.api.CollectorOrder + 0.49999 + order = pyblish.api.CollectorOrder + 0.11 hosts = ["houdini"] families = ["redshift_rop"] diff --git a/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py b/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py index 186244fedd..1e6bc3b16e 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py +++ b/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py @@ -2,22 +2,15 @@ """Collector plugin for frames data on ROP instances.""" import hou # noqa import pyblish.api -from openpype.lib import BoolDef from openpype.hosts.houdini.api import lib -from openpype.pipeline import OpenPypePyblishPluginMixin -class CollectRopFrameRange(pyblish.api.InstancePlugin, - OpenPypePyblishPluginMixin): - +class CollectRopFrameRange(pyblish.api.InstancePlugin): """Collect all frames which would be saved from the ROP nodes""" hosts = ["houdini"] - # This specific order value is used so that - # this plugin runs after CollectAnatomyInstanceData - order = pyblish.api.CollectorOrder + 0.499 + order = pyblish.api.CollectorOrder label = "Collect RopNode Frame Range" - use_asset_handles = True def process(self, instance): @@ -30,78 +23,16 @@ class CollectRopFrameRange(pyblish.api.InstancePlugin, return ropnode = hou.node(node_path) - - attr_values = self.get_attr_values_from_data(instance.data) - - if attr_values.get("use_handles", self.use_asset_handles): - asset_data = instance.data["assetEntity"]["data"] - handle_start = asset_data.get("handleStart", 0) - handle_end = asset_data.get("handleEnd", 0) - else: - handle_start = 0 - handle_end = 0 - frame_data = lib.get_frame_data( - ropnode, handle_start, handle_end, self.log + ropnode, self.log ) if not frame_data: return # Log debug message about the collected frame range - frame_start = frame_data["frameStart"] - frame_end = frame_data["frameEnd"] - - if attr_values.get("use_handles", self.use_asset_handles): - self.log.debug( - "Full Frame range with Handles " - "[{frame_start_handle} - {frame_end_handle}]" - .format( - frame_start_handle=frame_data["frameStartHandle"], - frame_end_handle=frame_data["frameEndHandle"] - ) - ) - else: - self.log.debug( - "Use handles is deactivated for this instance, " - "start and end handles are set to 0." - ) - - # Log collected frame range to the user - message = "Frame range [{frame_start} - {frame_end}]".format( - frame_start=frame_start, - frame_end=frame_end + self.log.debug( + "Collected frame_data: {}".format(frame_data) ) - if handle_start or handle_end: - message += " with handles [{handle_start}]-[{handle_end}]".format( - handle_start=handle_start, - handle_end=handle_end - ) - self.log.info(message) - - if frame_data.get("byFrameStep", 1.0) != 1.0: - self.log.info("Frame steps {}".format(frame_data["byFrameStep"])) instance.data.update(frame_data) - - # Add frame range to label if the instance has a frame range. - label = instance.data.get("label", instance.data["name"]) - instance.data["label"] = ( - "{label} [{frame_start} - {frame_end}]" - .format( - label=label, - frame_start=frame_start, - frame_end=frame_end - ) - ) - - @classmethod - def get_attribute_defs(cls): - return [ - BoolDef("use_handles", - tooltip="Disable this if you want the publisher to" - " ignore start and end handles specified in the" - " asset data for this publish instance", - default=cls.use_asset_handles, - label="Use asset handles") - ] diff --git a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py index 519c12aede..64de2079cd 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py @@ -26,7 +26,7 @@ class CollectVrayROPRenderProducts(pyblish.api.InstancePlugin): label = "VRay ROP Render Products" # This specific order value is used so that # this plugin runs after CollectFrames - order = pyblish.api.CollectorOrder + 0.49999 + order = pyblish.api.CollectorOrder + 0.11 hosts = ["houdini"] families = ["vray_rop"] diff --git a/openpype/hosts/houdini/plugins/publish/validate_frame_range.py b/openpype/hosts/houdini/plugins/publish/validate_frame_range.py index 6a66f3de9f..b49cfae901 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_frame_range.py +++ b/openpype/hosts/houdini/plugins/publish/validate_frame_range.py @@ -89,7 +89,7 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): .format(instance)) return - created_instance.publish_attributes["CollectRopFrameRange"]["use_handles"] = False # noqa + created_instance.publish_attributes["CollectAssetHandles"]["use_handles"] = False # noqa create_context.save_changes() cls.log.debug("use asset handles is turned off for '{}'" diff --git a/openpype/settings/defaults/project_settings/houdini.json b/openpype/settings/defaults/project_settings/houdini.json index 87983620ec..0dd8443e44 100644 --- a/openpype/settings/defaults/project_settings/houdini.json +++ b/openpype/settings/defaults/project_settings/houdini.json @@ -137,7 +137,7 @@ } }, "publish": { - "CollectRopFrameRange": { + "CollectAssetHandles": { "use_asset_handles": true }, "ValidateContainers": { diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_publish.json index 0de9f21c9f..324cfd8d58 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_publish.json @@ -11,8 +11,8 @@ { "type": "dict", "collapsible": true, - "key": "CollectRopFrameRange", - "label": "Collect Rop Frame Range", + "key": "CollectAssetHandles", + "label": "Collect Asset Handles", "children": [ { "type": "label", diff --git a/server_addon/houdini/server/settings/publish.py b/server_addon/houdini/server/settings/publish.py index 6615e34ca5..342bf957c1 100644 --- a/server_addon/houdini/server/settings/publish.py +++ b/server_addon/houdini/server/settings/publish.py @@ -3,7 +3,7 @@ from ayon_server.settings import BaseSettingsModel # Publish Plugins -class CollectRopFrameRangeModel(BaseSettingsModel): +class CollectAssetHandlesModel(BaseSettingsModel): """Collect Frame Range Disable this if you want the publisher to ignore start and end handles specified in the From 38d71e7c73ef5716b2964c867fe81d1b26656aed Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Fri, 3 Nov 2023 11:19:08 +0200 Subject: [PATCH 34/54] Bump Houdini addon version --- server_addon/houdini/server/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/houdini/server/version.py b/server_addon/houdini/server/version.py index 6cd38b7465..c49a95c357 100644 --- a/server_addon/houdini/server/version.py +++ b/server_addon/houdini/server/version.py @@ -1 +1 @@ -__version__ = "0.2.7" +__version__ = "0.2.8" From 3d74095521a45337d764a70eaf794fa7f98dc226 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Fri, 3 Nov 2023 11:21:10 +0200 Subject: [PATCH 35/54] Resolve Hound and Remove debug code --- .../hosts/houdini/plugins/publish/collect_asset_handles.py | 1 - openpype/hosts/houdini/plugins/publish/collect_frames.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_asset_handles.py b/openpype/hosts/houdini/plugins/publish/collect_asset_handles.py index 5c11948608..6474d64765 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_asset_handles.py +++ b/openpype/hosts/houdini/plugins/publish/collect_asset_handles.py @@ -32,7 +32,6 @@ class CollectAssetHandles(pyblish.api.InstancePlugin, label = "Collect Asset Handles" use_asset_handles = True - def process(self, instance): # Only process instances without already existing handles data # but that do have frameStartHandle and frameEndHandle defined diff --git a/openpype/hosts/houdini/plugins/publish/collect_frames.py b/openpype/hosts/houdini/plugins/publish/collect_frames.py index f6f538f5a5..cdef642174 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_frames.py +++ b/openpype/hosts/houdini/plugins/publish/collect_frames.py @@ -53,7 +53,7 @@ class CollectFrames(pyblish.api.InstancePlugin): # Check if frames are bigger than 1 (file collection) # override the result if end_frame - start_frame > 0: - result = self.create_file_list(self, + result = self.create_file_list( match, int(start_frame), int(end_frame) ) @@ -62,7 +62,7 @@ class CollectFrames(pyblish.api.InstancePlugin): instance.data.update({"frames": result}) @staticmethod - def create_file_list(self,match, start_frame, end_frame): + def create_file_list(match, start_frame, end_frame): """Collect files based on frame range and `regex.match` Args: From 69bf065851a7b82db1779a72022abf941d5c56bd Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Fri, 3 Nov 2023 15:41:37 +0200 Subject: [PATCH 36/54] BigRoy's Comment - Update label --- .../hosts/houdini/plugins/publish/collect_asset_handles.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_asset_handles.py b/openpype/hosts/houdini/plugins/publish/collect_asset_handles.py index 6474d64765..67a281639d 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_asset_handles.py +++ b/openpype/hosts/houdini/plugins/publish/collect_asset_handles.py @@ -104,11 +104,11 @@ class CollectAssetHandles(pyblish.api.InstancePlugin, # Add frame range to label if the instance has a frame range. label = instance.data.get("label", instance.data["name"]) instance.data["label"] = ( - "{label} [{frame_start} - {frame_end}]" + "{label} [{frame_start_handle} - {frame_end_handle}]" .format( label=label, - frame_start=frame_start, - frame_end=frame_end + frame_start_handle=instance.data["frameStartHandle"], + frame_end_handle=instance.data["frameEndHandle"] ) ) From dbd1fcb98912616d03c456718baf9b3f2e65a03c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 3 Nov 2023 17:03:54 +0100 Subject: [PATCH 37/54] make sure all QThread objects are always removed from python memory --- .../ayon_utils/widgets/folders_widget.py | 3 +- .../ayon_utils/widgets/projects_widget.py | 13 +++-- .../tools/ayon_utils/widgets/tasks_widget.py | 49 ++++++++++--------- 3 files changed, 35 insertions(+), 30 deletions(-) diff --git a/openpype/tools/ayon_utils/widgets/folders_widget.py b/openpype/tools/ayon_utils/widgets/folders_widget.py index 322553c51c..b72a992858 100644 --- a/openpype/tools/ayon_utils/widgets/folders_widget.py +++ b/openpype/tools/ayon_utils/widgets/folders_widget.py @@ -104,8 +104,8 @@ class FoldersModel(QtGui.QStandardItemModel): if not project_name: self._last_project_name = project_name - self._current_refresh_thread = None self._fill_items({}) + self._current_refresh_thread = None return self._is_refreshing = True @@ -152,6 +152,7 @@ class FoldersModel(QtGui.QStandardItemModel): return self._fill_items(thread.get_result()) + self._current_refresh_thread = None def _fill_item_data(self, item, folder_item): """ diff --git a/openpype/tools/ayon_utils/widgets/projects_widget.py b/openpype/tools/ayon_utils/widgets/projects_widget.py index be18cfe3ed..05347faca4 100644 --- a/openpype/tools/ayon_utils/widgets/projects_widget.py +++ b/openpype/tools/ayon_utils/widgets/projects_widget.py @@ -35,12 +35,11 @@ class ProjectsModel(QtGui.QStandardItemModel): self._selected_project = None - self._is_refreshing = False self._refresh_thread = None @property def is_refreshing(self): - return self._is_refreshing + return self._refresh_thread is not None def refresh(self): self._refresh() @@ -169,15 +168,16 @@ class ProjectsModel(QtGui.QStandardItemModel): return self._select_item def _refresh(self): - if self._is_refreshing: + if self._refresh_thread is not None: return - self._is_refreshing = True + refresh_thread = RefreshThread( "projects", self._query_project_items ) refresh_thread.refresh_finished.connect(self._refresh_finished) - refresh_thread.start() + self._refresh_thread = refresh_thread + refresh_thread.start() def _query_project_items(self): return self._controller.get_project_items() @@ -185,11 +185,10 @@ class ProjectsModel(QtGui.QStandardItemModel): def _refresh_finished(self): # TODO check if failed result = self._refresh_thread.get_result() - self._refresh_thread = None self._fill_items(result) - self._is_refreshing = False + self._refresh_thread = None self.refreshed.emit() def _fill_items(self, project_items): diff --git a/openpype/tools/ayon_utils/widgets/tasks_widget.py b/openpype/tools/ayon_utils/widgets/tasks_widget.py index d01b3a7917..a6375c6ae6 100644 --- a/openpype/tools/ayon_utils/widgets/tasks_widget.py +++ b/openpype/tools/ayon_utils/widgets/tasks_widget.py @@ -185,28 +185,7 @@ class TasksModel(QtGui.QStandardItemModel): thread.refresh_finished.connect(self._on_refresh_thread) thread.start() - def _on_refresh_thread(self, thread_id): - """Callback when refresh thread is finished. - - Technically can be running multiple refresh threads at the same time, - to avoid using values from wrong thread, we check if thread id is - current refresh thread id. - - Tasks are stored by name, so if a folder has same task name as - previously selected folder it keeps the selection. - - Args: - thread_id (str): Thread id. - """ - - # Make sure to remove thread from '_refresh_threads' dict - thread = self._refresh_threads.pop(thread_id) - if ( - self._current_refresh_thread is None - or thread_id != self._current_refresh_thread.id - ): - return - + def _fill_data_from_thread(self, thread): task_items = thread.get_result() # Task items are refreshed if task_items is None: @@ -247,7 +226,33 @@ class TasksModel(QtGui.QStandardItemModel): if new_items: root_item.appendRows(new_items) + def _on_refresh_thread(self, thread_id): + """Callback when refresh thread is finished. + + Technically can be running multiple refresh threads at the same time, + to avoid using values from wrong thread, we check if thread id is + current refresh thread id. + + Tasks are stored by name, so if a folder has same task name as + previously selected folder it keeps the selection. + + Args: + thread_id (str): Thread id. + """ + + # Make sure to remove thread from '_refresh_threads' dict + thread = self._refresh_threads.pop(thread_id) + if ( + self._current_refresh_thread is None + or thread_id != self._current_refresh_thread.id + ): + return + + self._fill_data_from_thread(thread) + + root_item = self.invisibleRootItem() self._has_content = root_item.rowCount() > 0 + self._current_refresh_thread = None self._is_refreshing = False self.refreshed.emit() From f18d0d9f8f36fcdc7193aaea5588db391fc5acec Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Fri, 3 Nov 2023 18:09:48 +0200 Subject: [PATCH 38/54] include full frame range in representation dictionary --- openpype/hosts/houdini/plugins/publish/extract_ass.py | 4 ++-- openpype/hosts/houdini/plugins/publish/extract_bgeo.py | 4 ++-- openpype/hosts/houdini/plugins/publish/extract_composite.py | 4 ++-- openpype/hosts/houdini/plugins/publish/extract_fbx.py | 6 +++--- openpype/hosts/houdini/plugins/publish/extract_opengl.py | 4 ++-- .../hosts/houdini/plugins/publish/extract_redshift_proxy.py | 6 +++--- openpype/hosts/houdini/plugins/publish/extract_vdb_cache.py | 4 ++-- 7 files changed, 16 insertions(+), 16 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/extract_ass.py b/openpype/hosts/houdini/plugins/publish/extract_ass.py index 0d246625ba..be60217055 100644 --- a/openpype/hosts/houdini/plugins/publish/extract_ass.py +++ b/openpype/hosts/houdini/plugins/publish/extract_ass.py @@ -56,7 +56,7 @@ class ExtractAss(publish.Extractor): 'ext': ext, "files": files, "stagingDir": staging_dir, - "frameStart": instance.data["frameStart"], - "frameEnd": instance.data["frameEnd"], + "frameStart": instance.data["frameStartHandle"], + "frameEnd": instance.data["frameEndHandle"], } instance.data["representations"].append(representation) diff --git a/openpype/hosts/houdini/plugins/publish/extract_bgeo.py b/openpype/hosts/houdini/plugins/publish/extract_bgeo.py index c9625ec880..d13141b426 100644 --- a/openpype/hosts/houdini/plugins/publish/extract_bgeo.py +++ b/openpype/hosts/houdini/plugins/publish/extract_bgeo.py @@ -47,7 +47,7 @@ class ExtractBGEO(publish.Extractor): "ext": ext.lstrip("."), "files": output, "stagingDir": staging_dir, - "frameStart": instance.data["frameStart"], - "frameEnd": instance.data["frameEnd"] + "frameStart": instance.data["frameStartHandle"], + "frameEnd": instance.data["frameEndHandle"] } instance.data["representations"].append(representation) diff --git a/openpype/hosts/houdini/plugins/publish/extract_composite.py b/openpype/hosts/houdini/plugins/publish/extract_composite.py index 7a1ab36b93..11cf83a46d 100644 --- a/openpype/hosts/houdini/plugins/publish/extract_composite.py +++ b/openpype/hosts/houdini/plugins/publish/extract_composite.py @@ -41,8 +41,8 @@ class ExtractComposite(publish.Extractor): "ext": ext, "files": output, "stagingDir": staging_dir, - "frameStart": instance.data["frameStart"], - "frameEnd": instance.data["frameEnd"], + "frameStart": instance.data["frameStartHandle"], + "frameEnd": instance.data["frameEndHandle"], } from pprint import pformat diff --git a/openpype/hosts/houdini/plugins/publish/extract_fbx.py b/openpype/hosts/houdini/plugins/publish/extract_fbx.py index 7993b3352f..1be61ecce1 100644 --- a/openpype/hosts/houdini/plugins/publish/extract_fbx.py +++ b/openpype/hosts/houdini/plugins/publish/extract_fbx.py @@ -40,9 +40,9 @@ class ExtractFBX(publish.Extractor): } # A single frame may also be rendered without start/end frame. - if "frameStart" in instance.data and "frameEnd" in instance.data: - representation["frameStart"] = instance.data["frameStart"] - representation["frameEnd"] = instance.data["frameEnd"] + if "frameStartHandle" in instance.data and "frameEndHandle" in instance.data: + representation["frameStart"] = instance.data["frameStartHandle"] + representation["frameEnd"] = instance.data["frameEndHandle"] # set value type for 'representations' key to list if "representations" not in instance.data: diff --git a/openpype/hosts/houdini/plugins/publish/extract_opengl.py b/openpype/hosts/houdini/plugins/publish/extract_opengl.py index 6c36dec5f5..38808089ac 100644 --- a/openpype/hosts/houdini/plugins/publish/extract_opengl.py +++ b/openpype/hosts/houdini/plugins/publish/extract_opengl.py @@ -39,8 +39,8 @@ class ExtractOpenGL(publish.Extractor): "ext": instance.data["imageFormat"], "files": output, "stagingDir": staging_dir, - "frameStart": instance.data["frameStart"], - "frameEnd": instance.data["frameEnd"], + "frameStart": instance.data["frameStartHandle"], + "frameEnd": instance.data["frameEndHandle"], "tags": tags, "preview": True, "camera_name": instance.data.get("review_camera") diff --git a/openpype/hosts/houdini/plugins/publish/extract_redshift_proxy.py b/openpype/hosts/houdini/plugins/publish/extract_redshift_proxy.py index 1d99ac665c..18cbd5712e 100644 --- a/openpype/hosts/houdini/plugins/publish/extract_redshift_proxy.py +++ b/openpype/hosts/houdini/plugins/publish/extract_redshift_proxy.py @@ -44,8 +44,8 @@ class ExtractRedshiftProxy(publish.Extractor): } # A single frame may also be rendered without start/end frame. - if "frameStart" in instance.data and "frameEnd" in instance.data: - representation["frameStart"] = instance.data["frameStart"] - representation["frameEnd"] = instance.data["frameEnd"] + if "frameStartHandle" in instance.data and "frameEndHandle" in instance.data: + representation["frameStart"] = instance.data["frameStartHandle"] + representation["frameEnd"] = instance.data["frameEndHandle"] instance.data["representations"].append(representation) diff --git a/openpype/hosts/houdini/plugins/publish/extract_vdb_cache.py b/openpype/hosts/houdini/plugins/publish/extract_vdb_cache.py index 4bca758f08..89af8e1756 100644 --- a/openpype/hosts/houdini/plugins/publish/extract_vdb_cache.py +++ b/openpype/hosts/houdini/plugins/publish/extract_vdb_cache.py @@ -40,7 +40,7 @@ class ExtractVDBCache(publish.Extractor): "ext": "vdb", "files": output, "stagingDir": staging_dir, - "frameStart": instance.data["frameStart"], - "frameEnd": instance.data["frameEnd"], + "frameStart": instance.data["frameStartHandle"], + "frameEnd": instance.data["frameEndHandle"], } instance.data["representations"].append(representation) From 2a19d5cc548e55bd526f24a70494717fbe5f23c7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 3 Nov 2023 18:57:15 +0100 Subject: [PATCH 39/54] fix default type of projects model cache --- openpype/tools/ayon_utils/models/projects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/ayon_utils/models/projects.py b/openpype/tools/ayon_utils/models/projects.py index 4ad53fbbfa..383f676c64 100644 --- a/openpype/tools/ayon_utils/models/projects.py +++ b/openpype/tools/ayon_utils/models/projects.py @@ -87,7 +87,7 @@ def _get_project_items_from_entitiy(projects): class ProjectsModel(object): def __init__(self, controller): - self._projects_cache = CacheItem(default_factory=dict) + self._projects_cache = CacheItem(default_factory=list) self._project_items_by_name = {} self._projects_by_name = {} From 3bffe3b31b3b367e4a24f0446d2b217e3623ecbe Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 3 Nov 2023 18:58:29 +0100 Subject: [PATCH 40/54] renamed 'ProjectsModel' to 'ProjectsQtModel' --- openpype/tools/ayon_launcher/ui/projects_widget.py | 4 ++-- openpype/tools/ayon_utils/widgets/__init__.py | 4 ++-- openpype/tools/ayon_utils/widgets/projects_widget.py | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/openpype/tools/ayon_launcher/ui/projects_widget.py b/openpype/tools/ayon_launcher/ui/projects_widget.py index 7dbaec5147..31c36719a6 100644 --- a/openpype/tools/ayon_launcher/ui/projects_widget.py +++ b/openpype/tools/ayon_launcher/ui/projects_widget.py @@ -3,7 +3,7 @@ from qtpy import QtWidgets, QtCore from openpype.tools.flickcharm import FlickCharm from openpype.tools.utils import PlaceholderLineEdit, RefreshButton from openpype.tools.ayon_utils.widgets import ( - ProjectsModel, + ProjectsQtModel, ProjectSortFilterProxy, ) from openpype.tools.ayon_utils.models import PROJECTS_MODEL_SENDER @@ -95,7 +95,7 @@ class ProjectsWidget(QtWidgets.QWidget): projects_view.setSelectionMode(QtWidgets.QListView.NoSelection) flick = FlickCharm(parent=self) flick.activateOn(projects_view) - projects_model = ProjectsModel(controller) + projects_model = ProjectsQtModel(controller) projects_proxy_model = ProjectSortFilterProxy() projects_proxy_model.setSourceModel(projects_model) diff --git a/openpype/tools/ayon_utils/widgets/__init__.py b/openpype/tools/ayon_utils/widgets/__init__.py index 432a249a73..1ef7dfe482 100644 --- a/openpype/tools/ayon_utils/widgets/__init__.py +++ b/openpype/tools/ayon_utils/widgets/__init__.py @@ -1,7 +1,7 @@ from .projects_widget import ( # ProjectsWidget, ProjectsCombobox, - ProjectsModel, + ProjectsQtModel, ProjectSortFilterProxy, ) @@ -25,7 +25,7 @@ from .utils import ( __all__ = ( # "ProjectsWidget", "ProjectsCombobox", - "ProjectsModel", + "ProjectsQtModel", "ProjectSortFilterProxy", "FoldersWidget", diff --git a/openpype/tools/ayon_utils/widgets/projects_widget.py b/openpype/tools/ayon_utils/widgets/projects_widget.py index 05347faca4..9f0f839281 100644 --- a/openpype/tools/ayon_utils/widgets/projects_widget.py +++ b/openpype/tools/ayon_utils/widgets/projects_widget.py @@ -10,11 +10,11 @@ PROJECT_IS_CURRENT_ROLE = QtCore.Qt.UserRole + 4 LIBRARY_PROJECT_SEPARATOR_ROLE = QtCore.Qt.UserRole + 5 -class ProjectsModel(QtGui.QStandardItemModel): +class ProjectsQtModel(QtGui.QStandardItemModel): refreshed = QtCore.Signal() def __init__(self, controller): - super(ProjectsModel, self).__init__() + super(ProjectsQtModel, self).__init__() self._controller = controller self._project_items = {} @@ -402,7 +402,7 @@ class ProjectsCombobox(QtWidgets.QWidget): projects_combobox = QtWidgets.QComboBox(self) combobox_delegate = QtWidgets.QStyledItemDelegate(projects_combobox) projects_combobox.setItemDelegate(combobox_delegate) - projects_model = ProjectsModel(controller) + projects_model = ProjectsQtModel(controller) projects_proxy_model = ProjectSortFilterProxy() projects_proxy_model.setSourceModel(projects_model) projects_combobox.setModel(projects_proxy_model) From 264e3cac79b863c5eb45841860da7789a13e714f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 3 Nov 2023 18:59:02 +0100 Subject: [PATCH 41/54] renamed other qt models to contain 'Qt' --- openpype/tools/ayon_loader/ui/folders_widget.py | 4 ++-- openpype/tools/ayon_utils/widgets/__init__.py | 8 ++++---- openpype/tools/ayon_utils/widgets/folders_widget.py | 6 +++--- openpype/tools/ayon_utils/widgets/tasks_widget.py | 8 ++++---- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/openpype/tools/ayon_loader/ui/folders_widget.py b/openpype/tools/ayon_loader/ui/folders_widget.py index 53351f76d9..eaaf7ca617 100644 --- a/openpype/tools/ayon_loader/ui/folders_widget.py +++ b/openpype/tools/ayon_loader/ui/folders_widget.py @@ -8,7 +8,7 @@ from openpype.tools.utils import ( from openpype.style import get_objected_colors from openpype.tools.ayon_utils.widgets import ( - FoldersModel, + FoldersQtModel, FOLDERS_MODEL_SENDER_NAME, ) from openpype.tools.ayon_utils.widgets.folders_widget import FOLDER_ID_ROLE @@ -182,7 +182,7 @@ class UnderlinesFolderDelegate(QtWidgets.QItemDelegate): painter.restore() -class LoaderFoldersModel(FoldersModel): +class LoaderFoldersModel(FoldersQtModel): def __init__(self, *args, **kwargs): super(LoaderFoldersModel, self).__init__(*args, **kwargs) diff --git a/openpype/tools/ayon_utils/widgets/__init__.py b/openpype/tools/ayon_utils/widgets/__init__.py index 1ef7dfe482..f58de17c4a 100644 --- a/openpype/tools/ayon_utils/widgets/__init__.py +++ b/openpype/tools/ayon_utils/widgets/__init__.py @@ -7,13 +7,13 @@ from .projects_widget import ( from .folders_widget import ( FoldersWidget, - FoldersModel, + FoldersQtModel, FOLDERS_MODEL_SENDER_NAME, ) from .tasks_widget import ( TasksWidget, - TasksModel, + TasksQtModel, TASKS_MODEL_SENDER_NAME, ) from .utils import ( @@ -29,11 +29,11 @@ __all__ = ( "ProjectSortFilterProxy", "FoldersWidget", - "FoldersModel", + "FoldersQtModel", "FOLDERS_MODEL_SENDER_NAME", "TasksWidget", - "TasksModel", + "TasksQtModel", "TASKS_MODEL_SENDER_NAME", "get_qt_icon", diff --git a/openpype/tools/ayon_utils/widgets/folders_widget.py b/openpype/tools/ayon_utils/widgets/folders_widget.py index b72a992858..44323a192c 100644 --- a/openpype/tools/ayon_utils/widgets/folders_widget.py +++ b/openpype/tools/ayon_utils/widgets/folders_widget.py @@ -16,7 +16,7 @@ FOLDER_PATH_ROLE = QtCore.Qt.UserRole + 3 FOLDER_TYPE_ROLE = QtCore.Qt.UserRole + 4 -class FoldersModel(QtGui.QStandardItemModel): +class FoldersQtModel(QtGui.QStandardItemModel): """Folders model which cares about refresh of folders. Args: @@ -26,7 +26,7 @@ class FoldersModel(QtGui.QStandardItemModel): refreshed = QtCore.Signal() def __init__(self, controller): - super(FoldersModel, self).__init__() + super(FoldersQtModel, self).__init__() self._controller = controller self._items_by_id = {} @@ -282,7 +282,7 @@ class FoldersWidget(QtWidgets.QWidget): folders_view = TreeView(self) folders_view.setHeaderHidden(True) - folders_model = FoldersModel(controller) + folders_model = FoldersQtModel(controller) folders_proxy_model = RecursiveSortFilterProxyModel() folders_proxy_model.setSourceModel(folders_model) folders_proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive) diff --git a/openpype/tools/ayon_utils/widgets/tasks_widget.py b/openpype/tools/ayon_utils/widgets/tasks_widget.py index a6375c6ae6..f27711acdd 100644 --- a/openpype/tools/ayon_utils/widgets/tasks_widget.py +++ b/openpype/tools/ayon_utils/widgets/tasks_widget.py @@ -12,7 +12,7 @@ ITEM_NAME_ROLE = QtCore.Qt.UserRole + 3 TASK_TYPE_ROLE = QtCore.Qt.UserRole + 4 -class TasksModel(QtGui.QStandardItemModel): +class TasksQtModel(QtGui.QStandardItemModel): """Tasks model which cares about refresh of tasks by folder id. Args: @@ -22,7 +22,7 @@ class TasksModel(QtGui.QStandardItemModel): refreshed = QtCore.Signal() def __init__(self, controller): - super(TasksModel, self).__init__() + super(TasksQtModel, self).__init__() self._controller = controller @@ -285,7 +285,7 @@ class TasksModel(QtGui.QStandardItemModel): if section == 0: return "Tasks" - return super(TasksModel, self).headerData( + return super(TasksQtModel, self).headerData( section, orientation, role ) @@ -310,7 +310,7 @@ class TasksWidget(QtWidgets.QWidget): tasks_view = DeselectableTreeView(self) tasks_view.setIndentation(0) - tasks_model = TasksModel(controller) + tasks_model = TasksQtModel(controller) tasks_proxy_model = QtCore.QSortFilterProxyModel() tasks_proxy_model.setSourceModel(tasks_model) tasks_proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive) From 86e4bed1514a1506200ac3ca9c51956c95414b2b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 3 Nov 2023 18:59:52 +0100 Subject: [PATCH 42/54] validate that item on which is clicked is enabled --- openpype/tools/ayon_launcher/ui/projects_widget.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/openpype/tools/ayon_launcher/ui/projects_widget.py b/openpype/tools/ayon_launcher/ui/projects_widget.py index 31c36719a6..38c7f62bd5 100644 --- a/openpype/tools/ayon_launcher/ui/projects_widget.py +++ b/openpype/tools/ayon_launcher/ui/projects_widget.py @@ -133,9 +133,14 @@ class ProjectsWidget(QtWidgets.QWidget): return self._projects_model.has_content() def _on_view_clicked(self, index): - if index.isValid(): - project_name = index.data(QtCore.Qt.DisplayRole) - self._controller.set_selected_project(project_name) + if not index.isValid(): + return + model = index.model() + flags = model.flags(index) + if not flags & QtCore.Qt.ItemIsEnabled: + return + project_name = index.data(QtCore.Qt.DisplayRole) + self._controller.set_selected_project(project_name) def _on_project_filter_change(self, text): self._projects_proxy_model.setFilterFixedString(text) From 05748bbb9291424ed82495a63bc581b2437fe772 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 3 Nov 2023 19:00:51 +0100 Subject: [PATCH 43/54] projects model pass sender value --- openpype/tools/ayon_utils/widgets/projects_widget.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/tools/ayon_utils/widgets/projects_widget.py b/openpype/tools/ayon_utils/widgets/projects_widget.py index 9f0f839281..804b7a05ac 100644 --- a/openpype/tools/ayon_utils/widgets/projects_widget.py +++ b/openpype/tools/ayon_utils/widgets/projects_widget.py @@ -180,7 +180,9 @@ class ProjectsQtModel(QtGui.QStandardItemModel): refresh_thread.start() def _query_project_items(self): - return self._controller.get_project_items() + return self._controller.get_project_items( + sender=PROJECTS_MODEL_SENDER + ) def _refresh_finished(self): # TODO check if failed From 42c32f81969399bc900fdfe2900d0da0e8c3edea Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 3 Nov 2023 19:01:26 +0100 Subject: [PATCH 44/54] projects model returns 'None' if is in middle of refreshing --- openpype/tools/ayon_utils/models/projects.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/openpype/tools/ayon_utils/models/projects.py b/openpype/tools/ayon_utils/models/projects.py index 383f676c64..36d53edc24 100644 --- a/openpype/tools/ayon_utils/models/projects.py +++ b/openpype/tools/ayon_utils/models/projects.py @@ -103,8 +103,18 @@ class ProjectsModel(object): self._refresh_projects_cache() def get_project_items(self, sender): + """ + + Args: + sender (str): Name of sender who asked for items. + + Returns: + Union[list[ProjectItem], None]: List of project items, or None + if model is refreshing. + """ + if not self._projects_cache.is_valid: - self._refresh_projects_cache(sender) + return self._refresh_projects_cache(sender) return self._projects_cache.get_data() def get_project_entity(self, project_name): @@ -136,11 +146,12 @@ class ProjectsModel(object): def _refresh_projects_cache(self, sender=None): if self._is_refreshing: - return + return None with self._project_refresh_event_manager(sender): project_items = self._query_projects() self._projects_cache.update_data(project_items) + return self._projects_cache.get_data() def _query_projects(self): projects = ayon_api.get_projects(fields=["name", "active", "library"]) From 5a0b2f69153f71469b60acf78d588c3583493764 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 3 Nov 2023 19:01:40 +0100 Subject: [PATCH 45/54] projects model handle cases when model is refreshing --- openpype/tools/ayon_utils/widgets/projects_widget.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/openpype/tools/ayon_utils/widgets/projects_widget.py b/openpype/tools/ayon_utils/widgets/projects_widget.py index 804b7a05ac..2beee29cb9 100644 --- a/openpype/tools/ayon_utils/widgets/projects_widget.py +++ b/openpype/tools/ayon_utils/widgets/projects_widget.py @@ -1,3 +1,5 @@ +import uuid + from qtpy import QtWidgets, QtCore, QtGui from openpype.tools.ayon_utils.models import PROJECTS_MODEL_SENDER @@ -187,11 +189,14 @@ class ProjectsQtModel(QtGui.QStandardItemModel): def _refresh_finished(self): # TODO check if failed result = self._refresh_thread.get_result() - - self._fill_items(result) + if result is not None: + self._fill_items(result) self._refresh_thread = None - self.refreshed.emit() + if result is None: + self._refresh() + else: + self.refreshed.emit() def _fill_items(self, project_items): new_project_names = { From 779cea668862a9354816f5fb9c62bf0d71496951 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Fri, 3 Nov 2023 20:04:16 +0200 Subject: [PATCH 46/54] update dictionary keys to be frames with handles --- .../hosts/houdini/plugins/publish/collect_review_data.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_review_data.py b/openpype/hosts/houdini/plugins/publish/collect_review_data.py index 3efb75e66c..9671945b9a 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_review_data.py +++ b/openpype/hosts/houdini/plugins/publish/collect_review_data.py @@ -6,6 +6,8 @@ class CollectHoudiniReviewData(pyblish.api.InstancePlugin): """Collect Review Data.""" label = "Collect Review Data" + # This specific order value is used so that + # this plugin runs after CollectRopFrameRange order = pyblish.api.CollectorOrder + 0.1 hosts = ["houdini"] families = ["review"] @@ -41,8 +43,8 @@ class CollectHoudiniReviewData(pyblish.api.InstancePlugin): return if focal_length_parm.isTimeDependent(): - start = instance.data["frameStart"] - end = instance.data["frameEnd"] + 1 + start = instance.data["frameStartHandle"] + end = instance.data["frameEndHandle"] + 1 focal_length = [ focal_length_parm.evalAsFloatAtFrame(t) for t in range(int(start), int(end)) From 15a52469354238165236ed890d10679e932f4439 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Fri, 3 Nov 2023 20:07:15 +0200 Subject: [PATCH 47/54] Resolve Hound --- openpype/hosts/houdini/plugins/publish/extract_fbx.py | 2 +- .../hosts/houdini/plugins/publish/extract_redshift_proxy.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/extract_fbx.py b/openpype/hosts/houdini/plugins/publish/extract_fbx.py index 1be61ecce1..7dc193c6a9 100644 --- a/openpype/hosts/houdini/plugins/publish/extract_fbx.py +++ b/openpype/hosts/houdini/plugins/publish/extract_fbx.py @@ -40,7 +40,7 @@ class ExtractFBX(publish.Extractor): } # A single frame may also be rendered without start/end frame. - if "frameStartHandle" in instance.data and "frameEndHandle" in instance.data: + if "frameStartHandle" in instance.data and "frameEndHandle" in instance.data: # noqa representation["frameStart"] = instance.data["frameStartHandle"] representation["frameEnd"] = instance.data["frameEndHandle"] diff --git a/openpype/hosts/houdini/plugins/publish/extract_redshift_proxy.py b/openpype/hosts/houdini/plugins/publish/extract_redshift_proxy.py index 18cbd5712e..ef5991924f 100644 --- a/openpype/hosts/houdini/plugins/publish/extract_redshift_proxy.py +++ b/openpype/hosts/houdini/plugins/publish/extract_redshift_proxy.py @@ -44,7 +44,7 @@ class ExtractRedshiftProxy(publish.Extractor): } # A single frame may also be rendered without start/end frame. - if "frameStartHandle" in instance.data and "frameEndHandle" in instance.data: + if "frameStartHandle" in instance.data and "frameEndHandle" in instance.data: # noqa representation["frameStart"] = instance.data["frameStartHandle"] representation["frameEnd"] = instance.data["frameEndHandle"] From d87506f8af15123cdc0d07176dba26bce262a434 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 3 Nov 2023 19:12:17 +0100 Subject: [PATCH 48/54] removed unused import --- openpype/tools/ayon_utils/widgets/projects_widget.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/tools/ayon_utils/widgets/projects_widget.py b/openpype/tools/ayon_utils/widgets/projects_widget.py index 2beee29cb9..f98bfcdf8a 100644 --- a/openpype/tools/ayon_utils/widgets/projects_widget.py +++ b/openpype/tools/ayon_utils/widgets/projects_widget.py @@ -1,5 +1,3 @@ -import uuid - from qtpy import QtWidgets, QtCore, QtGui from openpype.tools.ayon_utils.models import PROJECTS_MODEL_SENDER From 2ab07ec7ede740c13d98e8abeca9c02f73db4182 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 4 Nov 2023 03:25:51 +0000 Subject: [PATCH 49/54] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 249da3da0e..7a1fe9d83e 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.17.5-nightly.3 - 3.17.5-nightly.2 - 3.17.5-nightly.1 - 3.17.4 @@ -134,7 +135,6 @@ body: - 3.15.1-nightly.5 - 3.15.1-nightly.4 - 3.15.1-nightly.3 - - 3.15.1-nightly.2 validations: required: true - type: dropdown From 8f21d653e079662eef75a435ea97b26d101d7567 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 6 Nov 2023 21:32:43 +0800 Subject: [PATCH 50/54] use product_types instead of families --- server_addon/core/server/settings/tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/core/server/settings/tools.py b/server_addon/core/server/settings/tools.py index d7c7b367b7..0dd9d396ae 100644 --- a/server_addon/core/server/settings/tools.py +++ b/server_addon/core/server/settings/tools.py @@ -489,7 +489,7 @@ DEFAULT_TOOLS_VALUES = { "template_name": "publish_online" }, { - "families": [ + "product_types": [ "tycache" ], "hosts": [ From b7bec4b4e4fea704b0706013e76ac03de325ebee Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 6 Nov 2023 18:28:51 +0100 Subject: [PATCH 51/54] use correct label for follow workfile version --- server_addon/core/server/settings/publish_plugins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/core/server/settings/publish_plugins.py b/server_addon/core/server/settings/publish_plugins.py index 69a759465e..93d8db964d 100644 --- a/server_addon/core/server/settings/publish_plugins.py +++ b/server_addon/core/server/settings/publish_plugins.py @@ -21,7 +21,7 @@ class ValidateBaseModel(BaseSettingsModel): class CollectAnatomyInstanceDataModel(BaseSettingsModel): _isGroup = True follow_workfile_version: bool = Field( - True, title="Collect Anatomy Instance Data" + True, title="Follow workfile version" ) From efa9b8fe4c1b22af0aa561f6ea9132687b703f5e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 6 Nov 2023 18:39:58 +0100 Subject: [PATCH 52/54] Remove unused `instanceToggled` callbacks (#5862) --- openpype/hosts/aftereffects/api/pipeline.py | 10 ---------- openpype/hosts/nuke/api/pipeline.py | 22 --------------------- openpype/hosts/photoshop/api/pipeline.py | 10 ---------- openpype/hosts/tvpaint/api/pipeline.py | 4 ---- 4 files changed, 46 deletions(-) diff --git a/openpype/hosts/aftereffects/api/pipeline.py b/openpype/hosts/aftereffects/api/pipeline.py index 8fc7a70dd8..e059f7c272 100644 --- a/openpype/hosts/aftereffects/api/pipeline.py +++ b/openpype/hosts/aftereffects/api/pipeline.py @@ -74,11 +74,6 @@ class AfterEffectsHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): register_loader_plugin_path(LOAD_PATH) register_creator_plugin_path(CREATE_PATH) - log.info(PUBLISH_PATH) - - pyblish.api.register_callback( - "instanceToggled", on_pyblish_instance_toggled - ) register_event_callback("application.launched", application_launch) @@ -186,11 +181,6 @@ def application_launch(): check_inventory() -def on_pyblish_instance_toggled(instance, old_value, new_value): - """Toggle layer visibility on instance toggles.""" - instance[0].Visible = new_value - - def ls(): """Yields containers from active AfterEffects document. diff --git a/openpype/hosts/nuke/api/pipeline.py b/openpype/hosts/nuke/api/pipeline.py index f6ba33f00f..ba4d66ab63 100644 --- a/openpype/hosts/nuke/api/pipeline.py +++ b/openpype/hosts/nuke/api/pipeline.py @@ -129,9 +129,6 @@ class NukeHost( register_event_callback("workio.open_file", check_inventory_versions) register_event_callback("taskChanged", change_context_label) - pyblish.api.register_callback( - "instanceToggled", on_pyblish_instance_toggled) - _install_menu() # add script menu @@ -402,25 +399,6 @@ def add_shortcuts_from_presets(): log.error(e) -def on_pyblish_instance_toggled(instance, old_value, new_value): - """Toggle node passthrough states on instance toggles.""" - - log.info("instance toggle: {}, old_value: {}, new_value:{} ".format( - instance, old_value, new_value)) - - # Whether instances should be passthrough based on new value - - with viewer_update_and_undo_stop(): - n = instance[0] - try: - n["publish"].value() - except ValueError: - n = add_publish_knob(n) - log.info(" `Publish` knob was added to write node..") - - n["publish"].setValue(new_value) - - def containerise(node, name, namespace, diff --git a/openpype/hosts/photoshop/api/pipeline.py b/openpype/hosts/photoshop/api/pipeline.py index 56ae2a4c25..4e0dbcad06 100644 --- a/openpype/hosts/photoshop/api/pipeline.py +++ b/openpype/hosts/photoshop/api/pipeline.py @@ -48,11 +48,6 @@ class PhotoshopHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): pyblish.api.register_plugin_path(PUBLISH_PATH) register_loader_plugin_path(LOAD_PATH) register_creator_plugin_path(CREATE_PATH) - log.info(PUBLISH_PATH) - - pyblish.api.register_callback( - "instanceToggled", on_pyblish_instance_toggled - ) register_event_callback("application.launched", on_application_launch) @@ -177,11 +172,6 @@ def on_application_launch(): check_inventory() -def on_pyblish_instance_toggled(instance, old_value, new_value): - """Toggle layer visibility on instance toggles.""" - instance[0].Visible = new_value - - def ls(): """Yields containers from active Photoshop document diff --git a/openpype/hosts/tvpaint/api/pipeline.py b/openpype/hosts/tvpaint/api/pipeline.py index a84f196f09..c125da1533 100644 --- a/openpype/hosts/tvpaint/api/pipeline.py +++ b/openpype/hosts/tvpaint/api/pipeline.py @@ -84,10 +84,6 @@ class TVPaintHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): register_loader_plugin_path(load_dir) register_creator_plugin_path(create_dir) - registered_callbacks = ( - pyblish.api.registered_callbacks().get("instanceToggled") or [] - ) - register_event_callback("application.launched", self.initial_launch) register_event_callback("application.exit", self.application_exit) From ce413045130184c6da299d1f3a4a441a550a146d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 7 Nov 2023 13:29:15 +0100 Subject: [PATCH 53/54] ignore if passed icon definition is None --- openpype/tools/ayon_utils/widgets/utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/tools/ayon_utils/widgets/utils.py b/openpype/tools/ayon_utils/widgets/utils.py index 8bc3b1ea9b..2817b5efc0 100644 --- a/openpype/tools/ayon_utils/widgets/utils.py +++ b/openpype/tools/ayon_utils/widgets/utils.py @@ -54,6 +54,8 @@ class _IconsCache: @classmethod def get_icon(cls, icon_def): + if not icon_def: + return None icon_type = icon_def["type"] cache_key = cls._get_cache_key(icon_def) cache = cls._cache.get(cache_key) From 414df2370346aabbb555212f8123bbcb35817ea2 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Tue, 7 Nov 2023 13:07:26 +0000 Subject: [PATCH 54/54] Introduce app_group flag (#5869) --- openpype/cli.py | 7 +++++-- openpype/pype_commands.py | 5 ++++- tests/conftest.py | 10 ++++++++++ tests/lib/testing_classes.py | 9 ++++++--- 4 files changed, 25 insertions(+), 6 deletions(-) diff --git a/openpype/cli.py b/openpype/cli.py index 7422f32f13..f0fe550a1f 100644 --- a/openpype/cli.py +++ b/openpype/cli.py @@ -282,6 +282,9 @@ def run(script): "--app_variant", help="Provide specific app variant for test, empty for latest", default=None) +@click.option("--app_group", + help="Provide specific app group for test, empty for default", + default=None) @click.option("-t", "--timeout", help="Provide specific timeout value for test case", @@ -294,11 +297,11 @@ def run(script): help="MongoDB for testing.", default=None) def runtests(folder, mark, pyargs, test_data_folder, persist, app_variant, - timeout, setup_only, mongo_url): + timeout, setup_only, mongo_url, app_group): """Run all automatic tests after proper initialization via start.py""" PypeCommands().run_tests(folder, mark, pyargs, test_data_folder, persist, app_variant, timeout, setup_only, - mongo_url) + mongo_url, app_group) @main.command(help="DEPRECATED - run sync server") diff --git a/openpype/pype_commands.py b/openpype/pype_commands.py index 071ecfffd2..b5828d3dfe 100644 --- a/openpype/pype_commands.py +++ b/openpype/pype_commands.py @@ -214,7 +214,7 @@ class PypeCommands: def run_tests(self, folder, mark, pyargs, test_data_folder, persist, app_variant, timeout, setup_only, - mongo_url): + mongo_url, app_group): """ Runs tests from 'folder' @@ -260,6 +260,9 @@ class PypeCommands: if persist: args.extend(["--persist", persist]) + if app_group: + args.extend(["--app_group", app_group]) + if app_variant: args.extend(["--app_variant", app_variant]) diff --git a/tests/conftest.py b/tests/conftest.py index 6e82c9917d..a862030fff 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,6 +14,11 @@ def pytest_addoption(parser): help="True - keep test_db, test_openpype, outputted test files" ) + parser.addoption( + "--app_group", action="store", default=None, + help="Keep empty to use default application or explicit" + ) + parser.addoption( "--app_variant", action="store", default=None, help="Keep empty to locate latest installed variant or explicit" @@ -45,6 +50,11 @@ def persist(request): return request.config.getoption("--persist") +@pytest.fixture(scope="module") +def app_group(request): + return request.config.getoption("--app_group") + + @pytest.fixture(scope="module") def app_variant(request): return request.config.getoption("--app_variant") diff --git a/tests/lib/testing_classes.py b/tests/lib/testing_classes.py index 277b332e19..e8e338e434 100644 --- a/tests/lib/testing_classes.py +++ b/tests/lib/testing_classes.py @@ -248,19 +248,22 @@ class PublishTest(ModuleUnitTest): SETUP_ONLY = False @pytest.fixture(scope="module") - def app_name(self, app_variant): + def app_name(self, app_variant, app_group): """Returns calculated value for ApplicationManager. Eg.(nuke/12-2)""" from openpype.lib import ApplicationManager app_variant = app_variant or self.APP_VARIANT + app_group = app_group or self.APP_GROUP application_manager = ApplicationManager() if not app_variant: variant = ( application_manager.find_latest_available_variant_for_group( - self.APP_GROUP)) + app_group + ) + ) app_variant = variant.name - yield "{}/{}".format(self.APP_GROUP, app_variant) + yield "{}/{}".format(app_group, app_variant) @pytest.fixture(scope="module") def app_args(self, download_test_data):