From 0536998b660d9c3d00ba6c2a500613329687f15b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 25 Mar 2024 23:04:03 +0100 Subject: [PATCH 01/16] Maya: load image plane set colorspace --- .../maya/plugins/load/load_image_plane.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/client/ayon_core/hosts/maya/plugins/load/load_image_plane.py b/client/ayon_core/hosts/maya/plugins/load/load_image_plane.py index 7d6f7e26cf..c5b85d2cd4 100644 --- a/client/ayon_core/hosts/maya/plugins/load/load_image_plane.py +++ b/client/ayon_core/hosts/maya/plugins/load/load_image_plane.py @@ -145,6 +145,18 @@ class ImagePlaneLoader(load.LoaderPlugin): fileName=context["representation"]["data"]["path"], camera=camera ) + + # Set colorspace + colorspace = self.get_colorspace(context["representation"]) + if colorspace: + cmds.setAttr( + "{}.ignoreColorSpaceFileRules".format(image_plane_shape), + True + ) + cmds.setAttr("{}.colorSpace".format(image_plane_shape), + colorspace, type="string") + + # Set offset frame range start_frame = cmds.playbackOptions(query=True, min=True) end_frame = cmds.playbackOptions(query=True, max=True) @@ -216,6 +228,15 @@ class ImagePlaneLoader(load.LoaderPlugin): repre_entity["id"], type="string") + colorspace = self.get_colorspace(repre_entity) + if colorspace: + cmds.setAttr( + "{}.ignoreColorSpaceFileRules".format(image_plane_shape), + True + ) + cmds.setAttr("{}.colorSpace".format(image_plane_shape), + colorspace, type="string") + # Set frame range. start_frame = folder_entity["attrib"]["frameStart"] end_frame = folder_entity["attrib"]["frameEnd"] @@ -243,3 +264,12 @@ class ImagePlaneLoader(load.LoaderPlugin): deleteNamespaceContent=True) except RuntimeError: pass + + def get_colorspace(self, representation): + + data = representation.get("data", {}).get("colorspaceData", {}) + if not data: + return + + colorspace = data.get("colorspace") + return colorspace From 248265d5eb2b01e0dc1f724383b0ae656d25dd24 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 26 Mar 2024 10:11:47 +0100 Subject: [PATCH 02/16] Optimize logic --- .../maya/plugins/publish/validate_no_null_transforms.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_no_null_transforms.py b/client/ayon_core/hosts/maya/plugins/publish/validate_no_null_transforms.py index a9dc1d5bef..48af387f95 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_no_null_transforms.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_no_null_transforms.py @@ -26,15 +26,10 @@ def has_shape_children(node): return False # Check if there are any shapes at all - shapes = cmds.ls(allDescendents, shapes=True) + shapes = cmds.ls(allDescendents, shapes=True, noIntermediate=True) if not shapes: return False - # Check if all descendent shapes are intermediateObjects; - # if so we consider this node a null node and return False. - if all(cmds.getAttr('{0}.intermediateObject'.format(x)) for x in shapes): - return False - return True From 6b3808fcfa58798046be2f3c4a9a1254c403e1c8 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 26 Mar 2024 10:12:10 +0100 Subject: [PATCH 03/16] Cosmetics/hound --- .../plugins/publish/validate_no_null_transforms.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_no_null_transforms.py b/client/ayon_core/hosts/maya/plugins/publish/validate_no_null_transforms.py index 48af387f95..38955fd777 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_no_null_transforms.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_no_null_transforms.py @@ -19,14 +19,14 @@ def _as_report_list(values, prefix="- ", suffix="\n"): def has_shape_children(node): # Check if any descendants - allDescendents = cmds.listRelatives(node, - allDescendents=True, - fullPath=True) - if not allDescendents: + all_descendents = cmds.listRelatives(node, + allDescendents=True, + fullPath=True) + if not all_descendents: return False # Check if there are any shapes at all - shapes = cmds.ls(allDescendents, shapes=True, noIntermediate=True) + shapes = cmds.ls(all_descendents, shapes=True, noIntermediate=True) if not shapes: return False From 40f2001b886c3d4eb11d871bd0eaf32dd8d7ade3 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 26 Mar 2024 16:15:47 +0100 Subject: [PATCH 04/16] Fusion: Prompt reset scene context on saving to another task --- client/ayon_core/hosts/fusion/api/lib.py | 127 +++++++++++++++++- client/ayon_core/hosts/fusion/api/pipeline.py | 46 ++++++- 2 files changed, 166 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/hosts/fusion/api/lib.py b/client/ayon_core/hosts/fusion/api/lib.py index e5bf4b5a44..cfb77a3652 100644 --- a/client/ayon_core/hosts/fusion/api/lib.py +++ b/client/ayon_core/hosts/fusion/api/lib.py @@ -5,6 +5,8 @@ import contextlib from ayon_core.lib import Logger +from ayon_core.pipeline import registered_host +from ayon_core.pipeline.create import CreateContext from ayon_core.pipeline.context_tools import get_current_project_folder self = sys.modules[__name__] @@ -52,9 +54,15 @@ def update_frame_range(start, end, comp=None, set_render_range=True, comp.SetAttrs(attrs) -def set_current_context_framerange(): +def set_current_context_framerange(folder_entity=None): """Set Comp's frame range based on current folder.""" - folder_entity = get_current_project_folder() + if folder_entity is None: + folder_entity = get_current_project_folder( + fields={"attrib.frameStart", + "attrib.frameEnd", + "attrib.handleStart", + "attrib.handleEnd"}) + folder_attributes = folder_entity["attrib"] start = folder_attributes["frameStart"] end = folder_attributes["frameEnd"] @@ -65,9 +73,24 @@ def set_current_context_framerange(): handle_end=handle_end) -def set_current_context_resolution(): +def set_current_context_fps(folder_entity=None): + """Set Comp's frame rate (FPS) to based on current asset""" + if folder_entity is None: + folder_entity = get_current_project_folder(fields={"attrib.fps"}) + + fps = float(folder_entity["attrib"].get("fps", 24.0)) + comp = get_current_comp() + comp.SetPrefs({ + "Comp.FrameFormat.Rate": fps, + }) + + +def set_current_context_resolution(folder_entity=None): """Set Comp's resolution width x height default based on current folder""" - folder_entity = get_current_project_folder() + if folder_entity is None: + folder_entity = get_current_project_folder( + fields={"attrib.resolutionWidth", "attrib.resolutionHeight"}) + folder_attributes = folder_entity["attrib"] width = folder_attributes["resolutionWidth"] height = folder_attributes["resolutionHeight"] @@ -285,3 +308,99 @@ def comp_lock_and_undo_chunk( finally: comp.Unlock() comp.EndUndo(keep_undo) + + +def update_content_on_context_change(): + """Update all Creator instances to current asset""" + host = registered_host() + context = host.get_current_context() + + folder_path = context["folder_path"] + task = context["task_name"] + + create_context = CreateContext(host, reset=True) + + for instance in create_context.instances: + instance_folder_path = instance.get("folderPath") + if instance_folder_path and instance_folder_path != folder_path: + instance["folderPath"] = folder_path + instance_task = instance.get("task") + if instance_task and instance_task != task: + instance["task"] = task + + create_context.save_changes() + + +def prompt_reset_context(): + """Prompt the user what context settings to reset. + This prompt is used on saving to a different task to allow the scene to + get matched to the new context. + """ + # TODO: Cleanup this prototyped mess of imports and odd dialog + from ayon_core.tools.attribute_defs.dialog import ( + AttributeDefinitionsDialog + ) + from ayon_core.style import load_stylesheet + from ayon_core.lib import BoolDef, UILabelDef + from qtpy import QtWidgets, QtCore + + definitions = [ + UILabelDef( + label=( + "You are saving your scene into a different task." + "\n\n" + "Would you like to reset some settings for the " + "for the new context?\n" + ) + ), + BoolDef( + "fps", + label="FPS", + tooltip="Reset Comp FPS", + default=True + ), + BoolDef( + "frame_range", + label="Frame Range", + tooltip="Reset Comp start and end frame ranges", + default=True + ), + BoolDef( + "resolution", + label="Comp Resolution", + tooltip="Reset Comp resolution", + default=True + ), + BoolDef( + "instances", + label="Publish instances", + tooltip="Update all publish instance's folder and task to match " + "the new folder and task", + default=True + ), + ] + + dialog = AttributeDefinitionsDialog(definitions) + dialog.setWindowFlags( + dialog.windowFlags() | QtCore.Qt.WindowStaysOnTopHint + ) + dialog.setWindowTitle("Saving to different context. Reset options") + dialog.setStyleSheet(load_stylesheet()) + if not dialog.exec_(): + return None + + options = dialog.get_values() + folder_entity = get_current_project_folder() + if options["frame_range"]: + set_current_context_framerange(folder_entity) + + if options["fps"]: + set_current_context_fps(folder_entity) + + if options["resolution"]: + set_current_context_resolution(folder_entity) + + if options["instances"]: + update_content_on_context_change() + + dialog.deleteLater() diff --git a/client/ayon_core/hosts/fusion/api/pipeline.py b/client/ayon_core/hosts/fusion/api/pipeline.py index 50157cfae6..03773790e4 100644 --- a/client/ayon_core/hosts/fusion/api/pipeline.py +++ b/client/ayon_core/hosts/fusion/api/pipeline.py @@ -5,6 +5,7 @@ import os import sys import logging import contextlib +from pathlib import Path import pyblish.api from qtpy import QtCore @@ -28,8 +29,8 @@ from ayon_core.tools.utils import host_tools from .lib import ( get_current_comp, - comp_lock_and_undo_chunk, - validate_comp_prefs + validate_comp_prefs, + prompt_reset_context ) log = Logger.get_logger(__name__) @@ -41,6 +42,9 @@ LOAD_PATH = os.path.join(PLUGINS_DIR, "load") CREATE_PATH = os.path.join(PLUGINS_DIR, "create") INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") +# Track whether the workfile tool is about to save +ABOUT_TO_SAVE = False + class FusionLogHandler(logging.Handler): # Keep a reference to fusion's Print function (Remote Object) @@ -104,8 +108,10 @@ class FusionHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): # Register events register_event_callback("open", on_after_open) + register_event_callback("workfile.save.before", before_workfile_save) register_event_callback("save", on_save) register_event_callback("new", on_new) + register_event_callback("taskChanged", on_task_changed) # region workfile io api def has_unsaved_changes(self): @@ -169,6 +175,19 @@ def on_save(event): comp = event["sender"] validate_comp_prefs(comp) + # We are now starting the actual save directly + global ABOUT_TO_SAVE + ABOUT_TO_SAVE = False + + +def on_task_changed(): + global ABOUT_TO_SAVE + print(f"Task changed: {ABOUT_TO_SAVE}") + # TODO: Only do this if not headless + if ABOUT_TO_SAVE: + # Let's prompt the user to update the context settings or not + prompt_reset_context() + def on_after_open(event): comp = event["sender"] @@ -202,6 +221,28 @@ def on_after_open(event): dialog.setStyleSheet(load_stylesheet()) +def before_workfile_save(event): + # Due to Fusion's external python process design we can't really + # detect whether the current Fusion environment matches the one the artists + # expects it to be. For example, our pipeline python process might + # have been shut down, and restarted - which will restart it to the + # environment Fusion started with; not necessarily where the artist + # is currently working. + # The `ABOUT_TO_SAVE` var is used to detect context changes when + # saving into another asset. If we keep it False it will be ignored + # as context change. As such, before we change tasks we will only + # consider it the current filepath is within the currently known + # AVALON_WORKDIR. This way we avoid false positives of thinking it's + # saving to another context and instead sometimes just have false negatives + # where we fail to show the "Update on task change" prompt. + comp = get_current_comp() + filepath = comp.GetAttrs()["COMPS_FileName"] + workdir = os.environ.get("AYON_WORKDIR") + if Path(workdir) in Path(filepath).parents: + global ABOUT_TO_SAVE + ABOUT_TO_SAVE = True + + def ls(): """List containers from active Fusion scene @@ -338,7 +379,6 @@ class FusionEventHandler(QtCore.QObject): >>> handler = FusionEventHandler(parent=window) >>> handler.start() - """ ACTION_IDS = [ "Comp_Save", From 59756f812b51c324b28e776d7bf586fb7d7d8539 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 26 Mar 2024 17:34:54 +0100 Subject: [PATCH 05/16] Maya: Prompt reset scene context on saving to another task --- client/ayon_core/hosts/maya/api/lib.py | 106 ++++++++++++++++++-- client/ayon_core/hosts/maya/api/pipeline.py | 15 +++ 2 files changed, 110 insertions(+), 11 deletions(-) diff --git a/client/ayon_core/hosts/maya/api/lib.py b/client/ayon_core/hosts/maya/api/lib.py index 8ca898f621..7fa4700d6a 100644 --- a/client/ayon_core/hosts/maya/api/lib.py +++ b/client/ayon_core/hosts/maya/api/lib.py @@ -2647,31 +2647,115 @@ def reset_scene_resolution(): set_scene_resolution(width, height, pixelAspect) -def set_context_settings(): +def set_context_settings( + fps=True, + resolution=True, + frame_range=True, + colorspace=True +): """Apply the project settings from the project definition - Settings can be overwritten by an folder if the folder.attrib contains + Settings can be overwritten by an asset if the asset.data contains any information regarding those settings. - Examples of settings: - fps - resolution - renderer + Args: + fps (bool): Whether to set the scene FPS. + resolution (bool): Whether to set the render resolution. + frame_range (bool): Whether to reset the time slide frame ranges. + colorspace (bool): Whether to reset the colorspace. Returns: None """ - # Set project fps - set_scene_fps(get_fps_for_current_context()) + if fps: + # Set project fps + set_scene_fps(get_fps_for_current_context()) - reset_scene_resolution() + if resolution: + reset_scene_resolution() # Set frame range. - reset_frame_range() + if frame_range: + reset_frame_range(fps=False) # Set colorspace - set_colorspace() + if colorspace: + set_colorspace() + + +def prompt_reset_context(): + """Prompt the user what context settings to reset. + This prompt is used on saving to a different task to allow the scene to + get matched to the new context. + """ + # TODO: Cleanup this prototyped mess of imports and odd dialog + from ayon_core.tools.attribute_defs.dialog import ( + AttributeDefinitionsDialog + ) + from ayon_core.style import load_stylesheet + from ayon_core.lib import BoolDef, UILabelDef + + definitions = [ + UILabelDef( + label=( + "You are saving your scene into a different task." + "\n\n" + "Would you like to reset some settings for the " + "for the new context?\n" + ) + ), + BoolDef( + "fps", + label="FPS", + tooltip="Reset Comp FPS", + default=True + ), + BoolDef( + "frame_range", + label="Frame Range", + tooltip="Reset Comp start and end frame ranges", + default=True + ), + BoolDef( + "resolution", + label="Resolution", + tooltip="Reset Comp resolution", + default=True + ), + BoolDef( + "colorspace", + label="Colorspace", + tooltip="Reset Comp resolution", + default=True + ), + BoolDef( + "instances", + label="Publish instances", + tooltip="Update all publish instance's folder and task to match " + "the new folder and task", + default=True + ), + ] + + dialog = AttributeDefinitionsDialog(definitions) + dialog.setWindowTitle("Saving to different context. Reset options") + dialog.setStyleSheet(load_stylesheet()) + if not dialog.exec_(): + return None + + options = dialog.get_values() + with suspended_refresh(): + set_context_settings( + fps=options["fps"], + resolution=options["resolution"], + frame_range=options["frame_range"], + colorspace=options["colorspace"] + ) + if options["instances"]: + update_content_on_context_change() + + dialog.deleteLater() # Valid FPS diff --git a/client/ayon_core/hosts/maya/api/pipeline.py b/client/ayon_core/hosts/maya/api/pipeline.py index b3e401b91e..2be452a22a 100644 --- a/client/ayon_core/hosts/maya/api/pipeline.py +++ b/client/ayon_core/hosts/maya/api/pipeline.py @@ -67,6 +67,9 @@ INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") AVALON_CONTAINERS = ":AVALON_CONTAINERS" +# Track whether the workfile tool is about to save +ABOUT_TO_SAVE = False + class MayaHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): name = "maya" @@ -581,6 +584,10 @@ def on_save(): for node, new_id in lib.generate_ids(nodes): lib.set_id(node, new_id, overwrite=False) + # We are now starting the actual save directly + global ABOUT_TO_SAVE + ABOUT_TO_SAVE = False + def on_open(): """On scene open let's assume the containers have changed.""" @@ -650,6 +657,11 @@ def on_task_changed(): lib.set_context_settings() lib.update_content_on_context_change() + global ABOUT_TO_SAVE + if not lib.IS_HEADLESS and ABOUT_TO_SAVE: + # Let's prompt the user to update the context settings or not + lib.prompt_reset_context() + def before_workfile_open(): if handle_workfile_locks(): @@ -664,6 +676,9 @@ def before_workfile_save(event): if workdir_path: create_workspace_mel(workdir_path, project_name) + global ABOUT_TO_SAVE + ABOUT_TO_SAVE = True + def workfile_save_before_xgen(event): """Manage Xgen external files when switching context. From 62fc3e2cdd210009c381d56941335d583713e6b6 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 26 Mar 2024 17:38:52 +0100 Subject: [PATCH 06/16] Fix tooltips --- client/ayon_core/hosts/maya/api/lib.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/hosts/maya/api/lib.py b/client/ayon_core/hosts/maya/api/lib.py index 7fa4700d6a..4e9410af41 100644 --- a/client/ayon_core/hosts/maya/api/lib.py +++ b/client/ayon_core/hosts/maya/api/lib.py @@ -2708,25 +2708,25 @@ def prompt_reset_context(): BoolDef( "fps", label="FPS", - tooltip="Reset Comp FPS", + tooltip="Reset workfile FPS", default=True ), BoolDef( "frame_range", label="Frame Range", - tooltip="Reset Comp start and end frame ranges", + tooltip="Reset workfile start and end frame ranges", default=True ), BoolDef( "resolution", label="Resolution", - tooltip="Reset Comp resolution", + tooltip="Reset workfile resolution", default=True ), BoolDef( "colorspace", label="Colorspace", - tooltip="Reset Comp resolution", + tooltip="Reset workfile resolution", default=True ), BoolDef( From a94081e820f562dd7574607a4ce4e24bada666a5 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 26 Mar 2024 22:39:00 +0100 Subject: [PATCH 07/16] Improve label and window title --- client/ayon_core/hosts/fusion/api/lib.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/hosts/fusion/api/lib.py b/client/ayon_core/hosts/fusion/api/lib.py index cfb77a3652..4fdc8c6856 100644 --- a/client/ayon_core/hosts/fusion/api/lib.py +++ b/client/ayon_core/hosts/fusion/api/lib.py @@ -349,8 +349,7 @@ def prompt_reset_context(): label=( "You are saving your scene into a different task." "\n\n" - "Would you like to reset some settings for the " - "for the new context?\n" + "Would you like to update some settings to the new context?\n" ) ), BoolDef( @@ -384,7 +383,7 @@ def prompt_reset_context(): dialog.setWindowFlags( dialog.windowFlags() | QtCore.Qt.WindowStaysOnTopHint ) - dialog.setWindowTitle("Saving to different context. Reset options") + dialog.setWindowTitle("Saving to different context.") dialog.setStyleSheet(load_stylesheet()) if not dialog.exec_(): return None From cfbcc32953c1dbf924f3dca6a48d0f830422f65e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 26 Mar 2024 22:44:18 +0100 Subject: [PATCH 08/16] Improve artist report + add repair --- .../publish/validate_mesh_non_manifold.py | 132 ++++++++++++++++-- 1 file changed, 121 insertions(+), 11 deletions(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_non_manifold.py b/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_non_manifold.py index 6dbad538ef..8eee2b754b 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_non_manifold.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_non_manifold.py @@ -1,14 +1,99 @@ -from maya import cmds +from maya import cmds, mel import pyblish.api import ayon_core.hosts.maya.api.action from ayon_core.pipeline.publish import ( ValidateMeshOrder, - PublishValidationError, + PublishXmlValidationError, + RepairAction, OptionalPyblishPluginMixin ) +def poly_cleanup(version=4, + meshes=None, + # Version 1 + all_meshes=False, + select_only=False, + history_on=True, + quads=False, + nsided=False, + concave=False, + holed=False, + nonplanar=False, + zeroGeom=False, + zeroGeomTolerance=1e-05, + zeroEdge=False, + zeroEdgeTolerance=1e-05, + zeroMap=False, + zeroMapTolerance=1e-05, + # Version 2 + shared_uvs=False, + non_manifold=False, + # Version 3 + lamina=False, + # Version 4 + invalid_components=False): + """Wrapper around `polyCleanupArgList` mel command""" + + # Get all inputs named as `dict` to easily do conversions and formatting + values = locals() + + # Convert booleans to 1 or 0 + for key in [ + "all_meshes", + "select_only", + "history_on", + "quads", + "nsided", + "concave", + "holed", + "nonplanar", + "zeroGeom", + "zeroEdge", + "zeroMap", + "shared_uvs", + "non_manifold", + "lamina", + "invalid_components", + ]: + values[key] = 1 if values[key] else 0 + + cmd = ( + 'polyCleanupArgList {version} {{ ' + '"{all_meshes}",' # 0: All selectable meshes + '"{select_only}",' # 1: Only perform a selection + '"{history_on}",' # 2: Keep construction history + '"{quads}",' # 3: Check for quads polys + '"{nsided}",' # 4: Check for n-sides polys + '"{concave}",' # 5: Check for concave polys + '"{holed}",' # 6: Check for holed polys + '"{nonplanar}",' # 7: Check for non-planar polys + '"{zeroGeom}",' # 8: Check for 0 area faces + '"{zeroGeomTolerance}",' # 9: Tolerance for face areas + '"{zeroEdge}",' # 10: Check for 0 length edges + '"{zeroEdgeTolerance}",' # 11: Tolerance for edge length + '"{zeroMap}",' # 12: Check for 0 uv face area + '"{zeroMapTolerance}",' # 13: Tolerance for uv face areas + '"{shared_uvs}",' # 14: Unshare uvs that are shared + # across vertices + '"{non_manifold}",' # 15: Check for nonmanifold polys + '"{lamina}",' # 16: Check for lamina polys + '"{invalid_components}"' # 17: Remove invalid components + ' }};'.format(**values) + ) + + mel.eval("source polyCleanupArgList") + if not all_meshes and meshes: + # Allow to specify meshes to run over by selecting them + cmds.select(meshes, replace=True) + mel.eval(cmd) + + +class CleanupMatchingPolygons(RepairAction): + label = "Cleanup matching polygons" + + def _as_report_list(values, prefix="- ", suffix="\n"): """Return list as bullet point list for a report""" if not values: @@ -29,7 +114,8 @@ class ValidateMeshNonManifold(pyblish.api.Validator, hosts = ['maya'] families = ['model'] label = 'Mesh Non-Manifold Edges/Vertices' - actions = [ayon_core.hosts.maya.api.action.SelectInvalidAction] + actions = [ayon_core.hosts.maya.api.action.SelectInvalidAction, + CleanupMatchingPolygons] optional = True @staticmethod @@ -39,9 +125,11 @@ class ValidateMeshNonManifold(pyblish.api.Validator, invalid = [] for mesh in meshes: - if (cmds.polyInfo(mesh, nonManifoldVertices=True) or - cmds.polyInfo(mesh, nonManifoldEdges=True)): - invalid.append(mesh) + components = cmds.polyInfo(mesh, + nonManifoldVertices=True, + nonManifoldEdges=True) + if components: + invalid.extend(components) return invalid @@ -49,12 +137,34 @@ class ValidateMeshNonManifold(pyblish.api.Validator, """Process all the nodes in the instance 'objectSet'""" if not self.is_active(instance.data): return + invalid = self.get_invalid(instance) if invalid: - raise PublishValidationError( - "Meshes found with non-manifold edges/vertices:\n\n{0}".format( - _as_report_list(sorted(invalid)) - ), - title="Non-Manifold Edges/Vertices" + # Report only the meshes instead of all component indices + invalid_meshes = { + component.split(".", 1)[0] for component in invalid + } + invalid_meshes = _as_report_list(sorted(invalid_meshes)) + + raise PublishXmlValidationError( + plugin=self, + message=( + "Meshes found with non-manifold " + "edges/vertices:\n\n{0}".format(invalid_meshes) + ) ) + + @classmethod + def repair(cls, instance): + invalid_components = cls.get_invalid(instance) + if not invalid_components: + cls.log.info("No invalid components found to cleanup.") + return + + invalid_meshes = { + component.split(".", 1)[0] for component in invalid_components + } + poly_cleanup(meshes=list(invalid_meshes), + select_only=True, + non_manifold=True) From b41d5fc0136e0fe0d74f9daef889b7fd35fcb4ae Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 26 Mar 2024 22:46:40 +0100 Subject: [PATCH 09/16] Add XML help --- .../help/validate_mesh_non_manifold.xml | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 client/ayon_core/hosts/maya/plugins/publish/help/validate_mesh_non_manifold.xml diff --git a/client/ayon_core/hosts/maya/plugins/publish/help/validate_mesh_non_manifold.xml b/client/ayon_core/hosts/maya/plugins/publish/help/validate_mesh_non_manifold.xml new file mode 100644 index 0000000000..5aec3009a7 --- /dev/null +++ b/client/ayon_core/hosts/maya/plugins/publish/help/validate_mesh_non_manifold.xml @@ -0,0 +1,33 @@ + + + +Non-Manifold Edges/Vertices +## Non-Manifold Edges/Vertices + +Meshes found with non-manifold edges or vertices. + +### How to repair? + +Run select invalid to select the invalid components. + +You can also try the _cleanup matching polygons_ action which will perform a +cleanup like Maya's `Mesh > Cleanup...` modeling tool. + +It is recommended to always select the invalid to see where the issue is +because if you run any repair on it you will need to double check the topology +is still like you wanted. + + + +### What is non-manifold topology? + +_Non-manifold topology_ polygons have a configuration that cannot be unfolded +into a continuous flat piece, for example: + +- Three or more faces share an edge +- Two or more faces share a single vertex but no edge. +- Adjacent faces have opposite normals + + + + From b2075054914ba4c0f4e51b730a74871be2fdb9f8 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 26 Mar 2024 22:57:09 +0100 Subject: [PATCH 10/16] Maya: Validate Animation Out Set Related Node Ids improve validation report message - Ignore nodes without ids - that is validated elsewhere - Only check specific types instead of excluding only locators (so that e.g. also constraints or deformers are excluded in the check) - Improve report message --- ...ate_animation_out_set_related_node_ids.xml | 29 +++++++++++++++ ...date_animation_out_set_related_node_ids.py | 37 +++++++++---------- 2 files changed, 47 insertions(+), 19 deletions(-) create mode 100644 client/ayon_core/hosts/maya/plugins/publish/help/validate_animation_out_set_related_node_ids.xml diff --git a/client/ayon_core/hosts/maya/plugins/publish/help/validate_animation_out_set_related_node_ids.xml b/client/ayon_core/hosts/maya/plugins/publish/help/validate_animation_out_set_related_node_ids.xml new file mode 100644 index 0000000000..a855dd90a5 --- /dev/null +++ b/client/ayon_core/hosts/maya/plugins/publish/help/validate_animation_out_set_related_node_ids.xml @@ -0,0 +1,29 @@ + + + +Shape IDs mismatch original shape +## Shapes mismatch IDs with original shape + +Meshes are detected where the (deformed) mesh has a different `cbId` than +the same mesh in its deformation history. +Theses should normally be the same. + +### How to repair? + +By using the repair action the IDs from the shape in history will be +copied to the deformed shape. For **animation** instances using the +repair action usually is usually the correct fix. + + + +### How does this happen? + +When a deformer is applied in the scene on a referenced mesh that had no +deformers then Maya will create a new shape node for the mesh that +does not have the original id. Then on scene save new ids get created for the +meshes lacking a `cbId` and thus the mesh then has a different `cbId` than +the mesh in the deformation history. + + + + diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_animation_out_set_related_node_ids.py b/client/ayon_core/hosts/maya/plugins/publish/validate_animation_out_set_related_node_ids.py index 2502fd74b2..7ecd602662 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_animation_out_set_related_node_ids.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_animation_out_set_related_node_ids.py @@ -6,7 +6,7 @@ from ayon_core.hosts.maya.api import lib from ayon_core.pipeline.publish import ( RepairAction, ValidateContentsOrder, - PublishValidationError, + PublishXmlValidationError, OptionalPyblishPluginMixin, get_plugin_settings, apply_plugin_settings_automatically @@ -56,40 +56,39 @@ class ValidateOutRelatedNodeIds(pyblish.api.InstancePlugin, # if a deformer has been created on the shape invalid = self.get_invalid(instance) if invalid: - # TODO: Message formatting can be improved - raise PublishValidationError("Nodes found with mismatching " - "IDs: {0}".format(invalid), - title="Invalid node ids") + + # Use the short names + invalid = cmds.ls(invalid) + invalid.sort() + + # Construct a human-readable list + invalid = "\n".join("- {}".format(node) for node in invalid) + + raise PublishXmlValidationError( + plugin=self, + message=( + "Nodes have different IDs than their input " + "history: \n{0}".format(invalid) + ) + ) @classmethod def get_invalid(cls, instance): """Get all nodes which do not match the criteria""" invalid = [] - types_to_skip = ["locator"] + types = ["mesh", "nurbsCurve", "nurbsSurface"] # get asset id nodes = instance.data.get("out_hierarchy", instance[:]) - for node in nodes: + for node in cmds.ls(nodes, type=types, long=True): # We only check when the node is *not* referenced if cmds.referenceQuery(node, isNodeReferenced=True): continue - # Check if node is a shape as deformers only work on shapes - obj_type = cmds.objectType(node, isAType="shape") - if not obj_type: - continue - - # Skip specific types - if cmds.objectType(node) in types_to_skip: - continue - # Get the current id of the node node_id = lib.get_id(node) - if not node_id: - invalid.append(node) - continue history_id = lib.get_id_from_sibling(node) if history_id is not None and node_id != history_id: From 74d64f9dcbfdf7ce82993bf822acc0972f5636dc Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 26 Mar 2024 23:55:20 +0100 Subject: [PATCH 11/16] Raise PublishValidationError and improve error message --- .../publish/validate_shape_render_stats.py | 40 ++++++++++++++----- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_shape_render_stats.py b/client/ayon_core/hosts/maya/plugins/publish/validate_shape_render_stats.py index 2783a6dbe8..f9fadb4a3e 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_shape_render_stats.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_shape_render_stats.py @@ -6,6 +6,7 @@ import ayon_core.hosts.maya.api.action from ayon_core.pipeline.publish import ( RepairAction, ValidateMeshOrder, + PublishValidationError, OptionalPyblishPluginMixin ) @@ -20,7 +21,6 @@ class ValidateShapeRenderStats(pyblish.api.Validator, label = 'Shape Default Render Stats' actions = [ayon_core.hosts.maya.api.action.SelectInvalidAction, RepairAction] - optional = True defaults = {'castsShadows': 1, 'receiveShadows': 1, @@ -37,14 +37,13 @@ class ValidateShapeRenderStats(pyblish.api.Validator, # It seems the "surfaceShape" and those derived from it have # `renderStat` attributes. shapes = cmds.ls(instance, long=True, type='surfaceShape') - invalid = [] + invalid = set() for shape in shapes: - _iteritems = getattr(cls.defaults, "iteritems", cls.defaults.items) - for attr, default_value in _iteritems(): + for attr, default_value in cls.defaults.items(): if cmds.attributeQuery(attr, node=shape, exists=True): value = cmds.getAttr('{}.{}'.format(shape, attr)) if value != default_value: - invalid.append(shape) + invalid.add(shape) return invalid @@ -52,17 +51,36 @@ class ValidateShapeRenderStats(pyblish.api.Validator, if not self.is_active(instance.data): return invalid = self.get_invalid(instance) + if not invalid: + return - if invalid: - raise ValueError("Shapes with non-default renderStats " - "found: {0}".format(invalid)) + defaults_str = "\n".join( + "- {}: {}\n".format(key, value) + for key, value in self.defaults.items() + ) + description = ( + "## Shape Default Render Stats\n" + "Shapes are detected with non-default render stats.\n\n" + "To ensure a model's shapes behave like a shape would by default " + "we require the render stats to have not been altered in " + "the published models.\n\n" + "### How to repair?\n" + "You can reset the default values on the shapes by using the " + "repair action." + ) + + raise PublishValidationError( + "Shapes with non-default renderStats " + "found: {0}".format(", ".join(sorted(invalid))), + description=description, + detail="The expected default values " + "are:\n\n{}".format(defaults_str) + ) @classmethod def repair(cls, instance): for shape in cls.get_invalid(instance): - _iteritems = getattr(cls.defaults, "iteritems", cls.defaults.items) - for attr, default_value in _iteritems(): - + for attr, default_value in cls.defaults.items(): if cmds.attributeQuery(attr, node=shape, exists=True): plug = '{0}.{1}'.format(shape, attr) value = cmds.getAttr(plug) From db81bb1ad46d3fe8cc5f1fade48aee246f24db05 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 27 Mar 2024 00:10:59 +0100 Subject: [PATCH 12/16] Change wording for clarity --- client/ayon_core/hosts/fusion/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/fusion/api/lib.py b/client/ayon_core/hosts/fusion/api/lib.py index 4fdc8c6856..ba650cc73f 100644 --- a/client/ayon_core/hosts/fusion/api/lib.py +++ b/client/ayon_core/hosts/fusion/api/lib.py @@ -347,7 +347,7 @@ def prompt_reset_context(): definitions = [ UILabelDef( label=( - "You are saving your scene into a different task." + "You are saving your workfile into a different folder or task." "\n\n" "Would you like to update some settings to the new context?\n" ) From 5a413faf277ed2ca72457491c5fb76394d85ca35 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 27 Mar 2024 00:14:02 +0100 Subject: [PATCH 13/16] Tweak dialog wording to match #259 --- client/ayon_core/hosts/maya/api/lib.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/hosts/maya/api/lib.py b/client/ayon_core/hosts/maya/api/lib.py index 4e9410af41..9f36193413 100644 --- a/client/ayon_core/hosts/maya/api/lib.py +++ b/client/ayon_core/hosts/maya/api/lib.py @@ -2699,10 +2699,9 @@ def prompt_reset_context(): definitions = [ UILabelDef( label=( - "You are saving your scene into a different task." + "You are saving your workfile into a different folder or task." "\n\n" - "Would you like to reset some settings for the " - "for the new context?\n" + "Would you like to update some settings to the new context?\n" ) ), BoolDef( @@ -2739,7 +2738,7 @@ def prompt_reset_context(): ] dialog = AttributeDefinitionsDialog(definitions) - dialog.setWindowTitle("Saving to different context. Reset options") + dialog.setWindowTitle("Saving to different context.") dialog.setStyleSheet(load_stylesheet()) if not dialog.exec_(): return None From 3d38ddef9fb5acafb699d41a768ce1f39fc8d65a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 27 Mar 2024 00:32:31 +0100 Subject: [PATCH 14/16] Fix bullet point list formatting - In HTML mode the `-` doesn't get converted into nice bullet point list --- .../maya/plugins/publish/validate_transform_zero.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_transform_zero.py b/client/ayon_core/hosts/maya/plugins/publish/validate_transform_zero.py index 1cbdd05b0b..e003e9018f 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_transform_zero.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_transform_zero.py @@ -69,14 +69,13 @@ class ValidateTransformZero(pyblish.api.Validator, return invalid = self.get_invalid(instance) if invalid: - - names = "
".join( - " - {}".format(node) for node in invalid + names = "\n".join( + "- {}".format(node) for node in invalid ) raise PublishValidationError( title="Transform Zero", - message="The model publish allows no transformations. You must" - " freeze transformations to continue.

" - "Nodes found with transform values: " + message="The model publish allows no transformations. " + "You must **freeze transformations** to continue.\n\n" + "Nodes found with transform values:\n" "{0}".format(names)) From 6d741751945227e51a3f92c3336b96786ef271e2 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 27 Mar 2024 00:41:08 +0100 Subject: [PATCH 15/16] Improve validation report --- .../publish/validate_transform_zero.py | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_transform_zero.py b/client/ayon_core/hosts/maya/plugins/publish/validate_transform_zero.py index e003e9018f..0814b4525b 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_transform_zero.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_transform_zero.py @@ -1,5 +1,6 @@ -from maya import cmds +import inspect +from maya import cmds import pyblish.api import ayon_core.hosts.maya.api.action @@ -57,7 +58,7 @@ class ValidateTransformZero(pyblish.api.Validator, if ('_LOC' in transform) or ('_loc' in transform): continue mat = cmds.xform(transform, q=1, matrix=True, objectSpace=True) - if not all(abs(x-y) < cls._tolerance + if not all(abs(x - y) < cls._tolerance for x, y in zip(cls._identity, mat)): invalid.append(transform) @@ -69,13 +70,24 @@ class ValidateTransformZero(pyblish.api.Validator, return invalid = self.get_invalid(instance) if invalid: - names = "\n".join( - "- {}".format(node) for node in invalid + names = "
".join( + " - {}".format(node) for node in invalid ) raise PublishValidationError( title="Transform Zero", - message="The model publish allows no transformations. " - "You must **freeze transformations** to continue.\n\n" - "Nodes found with transform values:\n" + description=self.get_description(), + message="The model publish allows no transformations. You must" + " freeze transformations to continue.

" + "Nodes found with transform values:
" "{0}".format(names)) + + @staticmethod + def get_description(): + return inspect.cleandoc("""### Transform can't have any values + + The model publish allows no transformations. + + You must **freeze transformations** to continue. + + """) From e78b8043ab9bf2e4e44d1e545e0ed64b9745c3c4 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 27 Mar 2024 13:15:07 +0100 Subject: [PATCH 16/16] Update client/ayon_core/hosts/maya/plugins/load/load_image_plane.py --- client/ayon_core/hosts/maya/plugins/load/load_image_plane.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/maya/plugins/load/load_image_plane.py b/client/ayon_core/hosts/maya/plugins/load/load_image_plane.py index c5b85d2cd4..b298d5b892 100644 --- a/client/ayon_core/hosts/maya/plugins/load/load_image_plane.py +++ b/client/ayon_core/hosts/maya/plugins/load/load_image_plane.py @@ -142,7 +142,7 @@ class ImagePlaneLoader(load.LoaderPlugin): with namespaced(namespace): # Create inside the namespace image_plane_transform, image_plane_shape = cmds.imagePlane( - fileName=context["representation"]["data"]["path"], + fileName=self.filepath_from_context(context), camera=camera )