diff --git a/client/ayon_core/cli_commands.py b/client/ayon_core/cli_commands.py index 4335a3f2d9..bc0a22382c 100644 --- a/client/ayon_core/cli_commands.py +++ b/client/ayon_core/cli_commands.py @@ -67,8 +67,6 @@ class Commands: install_ayon_plugins, get_global_context, ) - from ayon_core.tools.utils.host_tools import show_publish - from ayon_core.tools.utils.lib import qt_app_context # Register target and host import pyblish.api @@ -134,6 +132,8 @@ class Commands: print(plugin) if gui: + from ayon_core.tools.utils.host_tools import show_publish + from ayon_core.tools.utils.lib import qt_app_context with qt_app_context(): show_publish() else: diff --git a/client/ayon_core/hosts/blender/plugins/publish/validate_file_saved.py b/client/ayon_core/hosts/blender/plugins/publish/validate_file_saved.py index 6a053eb47b..aa73525555 100644 --- a/client/ayon_core/hosts/blender/plugins/publish/validate_file_saved.py +++ b/client/ayon_core/hosts/blender/plugins/publish/validate_file_saved.py @@ -37,7 +37,8 @@ class ValidateFileSaved(pyblish.api.ContextPlugin, if not context.data["currentFile"]: # File has not been saved at all and has no filename raise PublishValidationError( - "Current file is empty. Save the file before continuing." + "Current workfile has not been saved yet.\n" + "Save the workfile before continuing." ) # Do not validate workfile has unsaved changes if only instances diff --git a/client/ayon_core/hosts/blender/plugins/publish/validate_model_uv_map1.py b/client/ayon_core/hosts/blender/plugins/publish/validate_model_uv_map1.py new file mode 100644 index 0000000000..752bc5fa58 --- /dev/null +++ b/client/ayon_core/hosts/blender/plugins/publish/validate_model_uv_map1.py @@ -0,0 +1,94 @@ +import inspect +from typing import List + +import bpy + +import pyblish.api + +from ayon_core.pipeline.publish import ( + ValidateContentsOrder, + OptionalPyblishPluginMixin, + PublishValidationError, + RepairAction +) +import ayon_core.hosts.blender.api.action + + +class ValidateModelMeshUvMap1( + pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin, +): + """Validate model mesh uvs are named `map1`. + + This is solely to get them to work nicely for the Maya pipeline. + """ + + order = ValidateContentsOrder + hosts = ["blender"] + families = ["model"] + label = "Mesh UVs named map1" + actions = [ayon_core.hosts.blender.api.action.SelectInvalidAction, + RepairAction] + optional = True + enabled = False + + @classmethod + def get_invalid(cls, instance) -> List: + + invalid = [] + for obj in instance: + if obj.mode != "OBJECT": + cls.log.warning( + f"Mesh object {obj.name} should be in 'OBJECT' mode" + " to be properly checked." + ) + + obj_data = obj.data + if isinstance(obj_data, bpy.types.Mesh): + mesh = obj_data + + # Ignore mesh without UVs + if not mesh.uv_layers: + continue + + # If mesh has map1 all is ok + if mesh.uv_layers.get("map1"): + continue + + cls.log.warning( + f"Mesh object {obj.name} should be in 'OBJECT' mode" + " to be properly checked." + ) + invalid.append(obj) + + return invalid + + @classmethod + def repair(cls, instance): + for obj in cls.get_invalid(instance): + mesh = obj.data + + # Rename the first UV set to map1 + mesh.uv_layers[0].name = "map1" + + def process(self, instance): + if not self.is_active(instance.data): + return + + invalid = self.get_invalid(instance) + if invalid: + raise PublishValidationError( + f"Meshes found in instance without valid UV's: {invalid}", + description=self.get_description() + ) + + def get_description(self): + return inspect.cleandoc( + """## Meshes must have map1 uv set + + To accompany a better Maya-focused pipeline with Alembics it is + expected that a Mesh has a `map1` UV set. Blender defaults to + a UV set named `UVMap` and thus needs to be renamed. + + """ + ) diff --git a/client/ayon_core/hosts/blender/plugins/publish/validate_transform_zero.py b/client/ayon_core/hosts/blender/plugins/publish/validate_transform_zero.py index 267eff47e4..465ec15d7b 100644 --- a/client/ayon_core/hosts/blender/plugins/publish/validate_transform_zero.py +++ b/client/ayon_core/hosts/blender/plugins/publish/validate_transform_zero.py @@ -1,3 +1,4 @@ +import inspect from typing import List import mathutils @@ -5,29 +6,26 @@ import bpy import pyblish.api +from ayon_core.hosts.blender.api import plugin, lib import ayon_core.hosts.blender.api.action from ayon_core.pipeline.publish import ( ValidateContentsOrder, OptionalPyblishPluginMixin, - PublishValidationError + PublishValidationError, + RepairAction ) class ValidateTransformZero(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): - """Transforms can't have any values - - To solve this issue, try freezing the transforms. So long - as the transforms, rotation and scale values are zero, - you're all good. - - """ + """Transforms can't have any values""" order = ValidateContentsOrder hosts = ["blender"] families = ["model"] label = "Transform Zero" - actions = [ayon_core.hosts.blender.api.action.SelectInvalidAction] + actions = [ayon_core.hosts.blender.api.action.SelectInvalidAction, + RepairAction] _identity = mathutils.Matrix() @@ -51,5 +49,46 @@ class ValidateTransformZero(pyblish.api.InstancePlugin, names = ", ".join(obj.name for obj in invalid) raise PublishValidationError( "Objects found in instance which do not" - f" have transform set to zero: {names}" + f" have transform set to zero: {names}", + description=self.get_description() ) + + @classmethod + def repair(cls, instance): + + invalid = cls.get_invalid(instance) + if not invalid: + return + + context = plugin.create_blender_context( + active=invalid[0], selected=invalid + ) + with lib.maintained_selection(): + with bpy.context.temp_override(**context): + plugin.deselect_all() + for obj in invalid: + obj.select_set(True) + + # TODO: Preferably this does allow custom pivot point locations + # and if so, this should likely apply to the delta instead + # using `bpy.ops.object.transforms_to_deltas(mode="ALL")` + bpy.ops.object.transform_apply(location=True, + rotation=True, + scale=True) + + def get_description(self): + return inspect.cleandoc( + """## Transforms can't have any values. + + The location, rotation and scale on the transform must be at + the default values. This also goes for the delta transforms. + + To solve this issue, try freezing the transforms: + - `Object` > `Apply` > `All Transforms` + + Using the Repair action directly will do the same. + + So long as the transforms, rotation and scale values are zero, + you're all good. + """ + ) diff --git a/client/ayon_core/hosts/fusion/plugins/create/create_image_saver.py b/client/ayon_core/hosts/fusion/plugins/create/create_image_saver.py index 8110898ae9..729843d078 100644 --- a/client/ayon_core/hosts/fusion/plugins/create/create_image_saver.py +++ b/client/ayon_core/hosts/fusion/plugins/create/create_image_saver.py @@ -1,7 +1,6 @@ from ayon_core.lib import NumberDef from ayon_core.hosts.fusion.api.plugin import GenericCreateSaver -from ayon_core.hosts.fusion.api import get_current_comp class CreateImageSaver(GenericCreateSaver): diff --git a/client/ayon_core/hosts/harmony/plugins/load/load_template_workfile.py b/client/ayon_core/hosts/harmony/plugins/load/load_template_workfile.py index 7bf634f00c..c7132ce373 100644 --- a/client/ayon_core/hosts/harmony/plugins/load/load_template_workfile.py +++ b/client/ayon_core/hosts/harmony/plugins/load/load_template_workfile.py @@ -50,11 +50,11 @@ class ImportTemplateLoader(load.LoaderPlugin): self.__class__.__name__ ) - def update(self, container, context): - pass + def update(self, container, context): + pass - def remove(self, container): - pass + def remove(self, container): + pass class ImportWorkfileLoader(ImportTemplateLoader): diff --git a/client/ayon_core/hosts/houdini/api/plugin.py b/client/ayon_core/hosts/houdini/api/plugin.py index 0809f4e566..b33d0fe297 100644 --- a/client/ayon_core/hosts/houdini/api/plugin.py +++ b/client/ayon_core/hosts/houdini/api/plugin.py @@ -147,7 +147,6 @@ class HoudiniCreatorBase(object): def create_instance_node( folder_path, node_name, parent, node_type="geometry" ): - # type: (str, str, str) -> hou.Node """Create node representing instance. Arguments: diff --git a/client/ayon_core/hosts/houdini/plugins/create/create_hda.py b/client/ayon_core/hosts/houdini/plugins/create/create_hda.py index c16c95a270..6a2a41d1d0 100644 --- a/client/ayon_core/hosts/houdini/plugins/create/create_hda.py +++ b/client/ayon_core/hosts/houdini/plugins/create/create_hda.py @@ -16,7 +16,7 @@ class CreateHDA(plugin.HoudiniCreator): maintain_selection = False def _check_existing(self, folder_path, product_name): - # type: (str) -> bool + # type: (str, str) -> bool """Check if existing product name versions already exists.""" # Get all products of the current folder project_name = self.project_name diff --git a/client/ayon_core/hosts/maya/plugins/publish/collect_multiverse_look.py b/client/ayon_core/hosts/maya/plugins/publish/collect_multiverse_look.py index 31c0d0eaa1..83e743c92e 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/collect_multiverse_look.py +++ b/client/ayon_core/hosts/maya/plugins/publish/collect_multiverse_look.py @@ -40,9 +40,11 @@ class _NodeTypeAttrib(object): return "{}.{}".format(node, self.colour_space) def __str__(self): - return "_NodeTypeAttrib(name={}, fname={}, " - "computed_fname={}, colour_space={})".format( - self.name, self.fname, self.computed_fname, self.colour_space) + return ( + "_NodeTypeAttrib(name={}, fname={}, " + "computed_fname={}, colour_space={})".format( + self.name, self.fname, self.computed_fname, self.colour_space) + ) NODETYPES = { diff --git a/client/ayon_core/hosts/photoshop/plugins/publish/collect_auto_image.py b/client/ayon_core/hosts/photoshop/plugins/publish/collect_auto_image.py index b488ab364d..adbe02eb74 100644 --- a/client/ayon_core/hosts/photoshop/plugins/publish/collect_auto_image.py +++ b/client/ayon_core/hosts/photoshop/plugins/publish/collect_auto_image.py @@ -9,7 +9,6 @@ class CollectAutoImage(pyblish.api.ContextPlugin): """ label = "Collect Auto Image" - order = pyblish.api.CollectorOrder hosts = ["photoshop"] order = pyblish.api.CollectorOrder + 0.2 diff --git a/client/ayon_core/hosts/photoshop/plugins/publish/collect_auto_image_refresh.py b/client/ayon_core/hosts/photoshop/plugins/publish/collect_auto_image_refresh.py index 0585f4f226..7a5f297c89 100644 --- a/client/ayon_core/hosts/photoshop/plugins/publish/collect_auto_image_refresh.py +++ b/client/ayon_core/hosts/photoshop/plugins/publish/collect_auto_image_refresh.py @@ -8,7 +8,6 @@ class CollectAutoImageRefresh(pyblish.api.ContextPlugin): """ label = "Collect Auto Image Refresh" - order = pyblish.api.CollectorOrder hosts = ["photoshop"] order = pyblish.api.CollectorOrder + 0.2 diff --git a/client/ayon_core/lib/ayon_connection.py b/client/ayon_core/lib/ayon_connection.py index 5ee654943f..1132d77aaa 100644 --- a/client/ayon_core/lib/ayon_connection.py +++ b/client/ayon_core/lib/ayon_connection.py @@ -10,6 +10,87 @@ class _Cache: initialized = False +def _new_get_last_versions( + self, + project_name, + product_ids, + active=True, + fields=None, + own_attributes=False +): + """Query last version entities by product ids. + + Args: + project_name (str): Project where to look for representation. + product_ids (Iterable[str]): Product ids. + active (Optional[bool]): Receive active/inactive entities. + Both are returned when 'None' is passed. + fields (Optional[Iterable[str]]): fields to be queried + for representations. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. + + Returns: + dict[str, dict[str, Any]]: Last versions by product id. + + """ + if fields: + fields = set(fields) + fields.add("productId") + + versions = self.get_versions( + project_name, + product_ids=product_ids, + latest=True, + hero=False, + active=active, + fields=fields, + own_attributes=own_attributes + ) + return { + version["productId"]: version + for version in versions + } + + +def _new_get_last_version_by_product_id( + self, + project_name, + product_id, + active=True, + fields=None, + own_attributes=False +): + """Query last version entity by product id. + + Args: + project_name (str): Project where to look for representation. + product_id (str): Product id. + active (Optional[bool]): Receive active/inactive entities. + Both are returned when 'None' is passed. + fields (Optional[Iterable[str]]): fields to be queried + for representations. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. + + Returns: + Union[dict[str, Any], None]: Queried version entity or None. + + """ + versions = self.get_versions( + project_name, + product_ids=[product_id], + latest=True, + hero=False, + active=active, + fields=fields, + own_attributes=own_attributes + ) + for version in versions: + return version + return None + + def _new_get_last_version_by_product_name( self, project_name, @@ -73,9 +154,15 @@ def initialize_ayon_connection(force=False): semver.VersionInfo.parse(ayon_api.__version__).to_tuple() ) # TODO remove mokey patching after when AYON api is safely updated - fix_last_version_by_product_name = ayon_api_version < (1, 0, 2) + fix_before_1_0_2 = ayon_api_version < (1, 0, 2) # Monkey patching to fix 'get_last_version_by_product_name' - if fix_last_version_by_product_name: + if fix_before_1_0_2: + ayon_api.ServerAPI.get_last_versions = ( + _new_get_last_versions + ) + ayon_api.ServerAPI.get_last_version_by_product_id = ( + _new_get_last_version_by_product_id + ) ayon_api.ServerAPI.get_last_version_by_product_name = ( _new_get_last_version_by_product_name ) @@ -85,12 +172,22 @@ def initialize_ayon_connection(force=False): if ayon_api.is_connection_created(): con = ayon_api.get_server_api_connection() # Monkey patching to fix 'get_last_version_by_product_name' - if fix_last_version_by_product_name: - def _con_wrapper(*args, **kwargs): + if fix_before_1_0_2: + def _lvs_wrapper(*args, **kwargs): + return _new_get_last_versions( + con, *args, **kwargs + ) + def _lv_by_pi_wrapper(*args, **kwargs): + return _new_get_last_version_by_product_id( + con, *args, **kwargs + ) + def _lv_by_pn_wrapper(*args, **kwargs): return _new_get_last_version_by_product_name( con, *args, **kwargs ) - con.get_last_version_by_product_name = _con_wrapper + con.get_last_versions = _lvs_wrapper + con.get_last_version_by_product_id = _lv_by_pi_wrapper + con.get_last_version_by_product_name = _lv_by_pn_wrapper con.set_site_id(site_id) con.set_client_version(version) else: diff --git a/client/ayon_core/modules/deadline/deadline_module.py b/client/ayon_core/modules/deadline/deadline_module.py index d2f0e263d4..c0ba83477e 100644 --- a/client/ayon_core/modules/deadline/deadline_module.py +++ b/client/ayon_core/modules/deadline/deadline_module.py @@ -46,7 +46,6 @@ class DeadlineModule(AYONAddon, IPluginPaths): @staticmethod def get_deadline_pools(webservice, log=None): - # type: (str) -> list """Get pools from Deadline. Args: webservice (str): Server url. diff --git a/client/ayon_core/modules/deadline/plugins/publish/collect_default_deadline_server.py b/client/ayon_core/modules/deadline/plugins/publish/collect_default_deadline_server.py index 8123409052..b7ca227b01 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/collect_default_deadline_server.py +++ b/client/ayon_core/modules/deadline/plugins/publish/collect_default_deadline_server.py @@ -9,11 +9,11 @@ class CollectDefaultDeadlineServer(pyblish.api.ContextPlugin): DL webservice addresses must be configured first in System Settings for project settings enum to work. - Default webservice could be overriden by + Default webservice could be overridden by `project_settings/deadline/deadline_servers`. Currently only single url is expected. - This url could be overriden by some hosts directly on instances with + This url could be overridden by some hosts directly on instances with `CollectDeadlineServerFromInstance`. """ diff --git a/client/ayon_core/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py b/client/ayon_core/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py index a666c5c2dc..6263526d5c 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py +++ b/client/ayon_core/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py @@ -149,7 +149,7 @@ class ValidateExpectedFiles(pyblish.api.InstancePlugin): """ # no frames in file name at all, eg 'renderCompositingMain.withLut.mov' if not frame_placeholder: - return set([file_name_template]) + return {file_name_template} real_expected_rendered = set() src_padding_exp = "%0{}d".format(len(frame_placeholder)) diff --git a/client/ayon_core/modules/deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.py b/client/ayon_core/modules/deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.py index 9641c16d20..f146aef7b4 100644 --- a/client/ayon_core/modules/deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.py +++ b/client/ayon_core/modules/deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.py @@ -404,7 +404,7 @@ class OpenPypeTileAssembler(DeadlinePlugin): Args: output_width (int): Width of output image. output_height (int): Height of output image. - tiles_info (list): List of tile items, each item must be + tile_info (list): List of tile items, each item must be dictionary with `filepath`, `pos_x` and `pos_y` keys representing path to file and x, y coordinates on output image where top-left point of tile item should start. diff --git a/server_addon/blender/server/settings/publish_plugins.py b/server_addon/blender/server/settings/publish_plugins.py index 79c489d080..c742fdc5bd 100644 --- a/server_addon/blender/server/settings/publish_plugins.py +++ b/server_addon/blender/server/settings/publish_plugins.py @@ -89,6 +89,10 @@ class PublishPuginsModel(BaseSettingsModel): default_factory=ValidatePluginModel, title="Validate Mesh No Negative Scale" ) + ValidateModelMeshUvMap1: ValidatePluginModel = SettingsField( + default_factory=ValidatePluginModel, + title="Validate Model Mesh Has UV map named map1" + ) ValidateTransformZero: ValidatePluginModel = SettingsField( default_factory=ValidatePluginModel, title="Validate Transform Zero" @@ -181,6 +185,11 @@ DEFAULT_BLENDER_PUBLISH_SETTINGS = { "optional": False, "active": True }, + "ValidateModelMeshUvMap1": { + "enabled": False, + "optional": True, + "active": True + }, "ValidateTransformZero": { "enabled": False, "optional": True, diff --git a/server_addon/blender/server/version.py b/server_addon/blender/server/version.py index f1380eede2..9cb17e7976 100644 --- a/server_addon/blender/server/version.py +++ b/server_addon/blender/server/version.py @@ -1 +1 @@ -__version__ = "0.1.7" +__version__ = "0.1.8"