diff --git a/client/ayon_core/hosts/blender/plugins/publish/validate_mesh_no_negative_scale.py b/client/ayon_core/hosts/blender/plugins/publish/validate_mesh_no_negative_scale.py index 63b7dc7530..fb16bb7f8d 100644 --- a/client/ayon_core/hosts/blender/plugins/publish/validate_mesh_no_negative_scale.py +++ b/client/ayon_core/hosts/blender/plugins/publish/validate_mesh_no_negative_scale.py @@ -12,7 +12,7 @@ from ayon_core.pipeline.publish import ( import ayon_core.hosts.blender.api.action -class ValidateMeshNoNegativeScale(pyblish.api.Validator, +class ValidateMeshNoNegativeScale(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Ensure that meshes don't have a negative scale.""" diff --git a/client/ayon_core/hosts/celaction/plugins/publish/collect_celaction_cli_kwargs.py b/client/ayon_core/hosts/celaction/plugins/publish/collect_celaction_cli_kwargs.py index 54dea15dff..1820569918 100644 --- a/client/ayon_core/hosts/celaction/plugins/publish/collect_celaction_cli_kwargs.py +++ b/client/ayon_core/hosts/celaction/plugins/publish/collect_celaction_cli_kwargs.py @@ -3,11 +3,11 @@ import sys from pprint import pformat -class CollectCelactionCliKwargs(pyblish.api.Collector): +class CollectCelactionCliKwargs(pyblish.api.ContextPlugin): """ Collects all keyword arguments passed from the terminal """ label = "Collect Celaction Cli Kwargs" - order = pyblish.api.Collector.order - 0.1 + order = pyblish.api.CollectorOrder - 0.1 def process(self, context): args = list(sys.argv[1:]) diff --git a/client/ayon_core/hosts/fusion/api/lib.py b/client/ayon_core/hosts/fusion/api/lib.py index e5bf4b5a44..ba650cc73f 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,98 @@ 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 workfile into a different folder or task." + "\n\n" + "Would you like to update some settings to 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.") + 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 dfac0640b0..2d1073ec7d 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,7 +29,8 @@ from ayon_core.tools.utils import host_tools from .lib import ( get_current_comp, - validate_comp_prefs + validate_comp_prefs, + prompt_reset_context ) log = Logger.get_logger(__name__) @@ -40,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) @@ -103,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): @@ -168,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"] @@ -201,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 @@ -337,7 +379,6 @@ class FusionEventHandler(QtCore.QObject): >>> handler = FusionEventHandler(parent=window) >>> handler.start() - """ ACTION_IDS = [ "Comp_Save", diff --git a/client/ayon_core/hosts/houdini/api/creator_node_shelves.py b/client/ayon_core/hosts/houdini/api/creator_node_shelves.py index 6e48cb375b..72c157f187 100644 --- a/client/ayon_core/hosts/houdini/api/creator_node_shelves.py +++ b/client/ayon_core/hosts/houdini/api/creator_node_shelves.py @@ -91,7 +91,7 @@ def create_interactive(creator_identifier, **kwargs): pane = stateutils.activePane(kwargs) if isinstance(pane, hou.NetworkEditor): pwd = pane.pwd() - project_name = context.get_current_project_name(), + project_name = context.get_current_project_name() folder_path = context.get_current_folder_path() task_name = context.get_current_task_name() folder_entity = ayon_api.get_folder_by_path( diff --git a/client/ayon_core/hosts/houdini/api/pipeline.py b/client/ayon_core/hosts/houdini/api/pipeline.py index b9446933ac..4797cf36a0 100644 --- a/client/ayon_core/hosts/houdini/api/pipeline.py +++ b/client/ayon_core/hosts/houdini/api/pipeline.py @@ -39,7 +39,7 @@ 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 +_about_to_save = False class HoudiniHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): @@ -292,8 +292,8 @@ def ls(): def before_workfile_save(event): - global ABOUT_TO_SAVE - ABOUT_TO_SAVE = True + global _about_to_save + _about_to_save = True def before_save(): @@ -307,18 +307,14 @@ def on_save(): # update houdini vars lib.update_houdini_vars_context_dialog() - nodes = lib.get_id_required_nodes() - 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 + global _about_to_save + _about_to_save = False def on_task_changed(): - global ABOUT_TO_SAVE - if not IS_HEADLESS and ABOUT_TO_SAVE: + global _about_to_save + if not IS_HEADLESS and _about_to_save: # Let's prompt the user to update the context settings or not lib.prompt_reset_context() diff --git a/client/ayon_core/hosts/maya/api/action.py b/client/ayon_core/hosts/maya/api/action.py index baf558036e..d845ac6066 100644 --- a/client/ayon_core/hosts/maya/api/action.py +++ b/client/ayon_core/hosts/maya/api/action.py @@ -4,7 +4,10 @@ from __future__ import absolute_import import pyblish.api import ayon_api -from ayon_core.pipeline.publish import get_errored_instances_from_context +from ayon_core.pipeline.publish import ( + get_errored_instances_from_context, + get_errored_plugins_from_context +) class GenerateUUIDsOnInvalidAction(pyblish.api.Action): @@ -112,20 +115,25 @@ class SelectInvalidAction(pyblish.api.Action): except ImportError: raise ImportError("Current host is not Maya") - errored_instances = get_errored_instances_from_context(context, - plugin=plugin) - # Get the invalid nodes for the plug-ins self.log.info("Finding invalid nodes..") invalid = list() - for instance in errored_instances: - invalid_nodes = plugin.get_invalid(instance) - if invalid_nodes: - if isinstance(invalid_nodes, (list, tuple)): - invalid.extend(invalid_nodes) - else: - self.log.warning("Plug-in returned to be invalid, " - "but has no selectable nodes.") + if issubclass(plugin, pyblish.api.ContextPlugin): + errored_plugins = get_errored_plugins_from_context(context) + if plugin in errored_plugins: + invalid = plugin.get_invalid(context) + else: + errored_instances = get_errored_instances_from_context( + context, plugin=plugin + ) + for instance in errored_instances: + invalid_nodes = plugin.get_invalid(instance) + if invalid_nodes: + if isinstance(invalid_nodes, (list, tuple)): + invalid.extend(invalid_nodes) + else: + self.log.warning("Plug-in returned to be invalid, " + "but has no selectable nodes.") # Ensure unique (process each node only once) invalid = list(set(invalid)) diff --git a/client/ayon_core/hosts/maya/api/customize.py b/client/ayon_core/hosts/maya/api/customize.py index 4db8819ff5..16255f69ba 100644 --- a/client/ayon_core/hosts/maya/api/customize.py +++ b/client/ayon_core/hosts/maya/api/customize.py @@ -113,7 +113,9 @@ def override_toolbox_ui(): annotation="Look Manager", label="Look Manager", image=os.path.join(icons, "lookmanager.png"), - command=show_look_assigner, + command=lambda: show_look_assigner( + parent=parent_widget + ), width=icon_size, height=icon_size, parent=parent diff --git a/client/ayon_core/hosts/maya/api/lib.py b/client/ayon_core/hosts/maya/api/lib.py index acfac760e6..7569e88e4c 100644 --- a/client/ayon_core/hosts/maya/api/lib.py +++ b/client/ayon_core/hosts/maya/api/lib.py @@ -1876,18 +1876,9 @@ def list_looks(project_name, folder_id): list[dict[str, Any]]: List of look products. """ - # # get all products with look leading in - # the name associated with the asset - # TODO this should probably look for product type 'look' instead of - # checking product name that can not start with product type - product_entities = ayon_api.get_products( - project_name, folder_ids=[folder_id] - ) - return [ - product_entity - for product_entity in product_entities - if product_entity["name"].startswith("look") - ] + return list(ayon_api.get_products( + project_name, folder_ids=[folder_id], product_types={"look"} + )) def assign_look_by_version(nodes, version_id): @@ -1906,12 +1897,15 @@ def assign_look_by_version(nodes, version_id): project_name = get_current_project_name() # Get representations of shader file and relationships - look_representation = ayon_api.get_representation_by_name( - project_name, "ma", version_id - ) - json_representation = ayon_api.get_representation_by_name( - project_name, "json", version_id - ) + representations = list(ayon_api.get_representations( + project_name=project_name, + representation_names={"ma", "json"}, + version_ids=[version_id] + )) + look_representation = next( + repre for repre in representations if repre["name"] == "ma") + json_representation = next( + repre for repre in representations if repre["name"] == "json") # See if representation is already loaded, if so reuse it. host = registered_host() @@ -1948,7 +1942,7 @@ def assign_look_by_version(nodes, version_id): apply_shaders(relationships, shader_nodes, nodes) -def assign_look(nodes, product_name="lookDefault"): +def assign_look(nodes, product_name="lookMain"): """Assigns a look to a node. Optimizes the nodes by grouping by folder id and finding @@ -1981,14 +1975,10 @@ def assign_look(nodes, product_name="lookDefault"): product_entity["id"] for product_entity in product_entities_by_folder_id.values() } - last_version_entities = ayon_api.get_last_versions( + last_version_entities_by_product_id = ayon_api.get_last_versions( project_name, product_ids ) - last_version_entities_by_product_id = { - last_version_entity["productId"]: last_version_entity - for last_version_entity in last_version_entities - } for folder_id, asset_nodes in grouped.items(): product_entity = product_entities_by_folder_id.get(folder_id) @@ -2651,31 +2641,114 @@ 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 workfile into a different folder or task." + "\n\n" + "Would you like to update some settings to the new context?\n" + ) + ), + BoolDef( + "fps", + label="FPS", + tooltip="Reset workfile FPS", + default=True + ), + BoolDef( + "frame_range", + label="Frame Range", + tooltip="Reset workfile start and end frame ranges", + default=True + ), + BoolDef( + "resolution", + label="Resolution", + tooltip="Reset workfile resolution", + default=True + ), + BoolDef( + "colorspace", + label="Colorspace", + tooltip="Reset workfile 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.") + 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..8e6e2ccd8a 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. 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..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,9 +142,21 @@ 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 ) + + # 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 diff --git a/client/ayon_core/hosts/maya/plugins/publish/collect_render.py b/client/ayon_core/hosts/maya/plugins/publish/collect_render.py index ff959afabc..21095935a2 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/collect_render.py +++ b/client/ayon_core/hosts/maya/plugins/publish/collect_render.py @@ -1,24 +1,19 @@ # -*- coding: utf-8 -*- """Collect render data. -This collector will go through render layers in maya and prepare all data -needed to create instances and their representations for submission and -publishing on farm. +This collector will go through renderlayer instances and prepare all data +needed to detect the expected rendered files for a layer, with resolution, +frame ranges and collects the data needed for publishing on the farm. Requires: instance -> families - instance -> setMembers - instance -> folderPath context -> currentFile - context -> workspaceDir context -> user -Optional: - Provides: instance -> label - instance -> productName + instance -> subset instance -> attachTo instance -> setMembers instance -> publish @@ -26,6 +21,8 @@ Provides: instance -> frameEnd instance -> byFrameStep instance -> renderer + instance -> family + instance -> asset instance -> time instance -> author instance -> source @@ -71,8 +68,6 @@ class CollectMayaRender(pyblish.api.InstancePlugin): # TODO: Re-add force enable of workfile instance? # TODO: Re-add legacy layer support with LAYER_ prefix but in Creator - # TODO: Set and collect active state of RenderLayer in Creator using - # renderlayer.isRenderable() context = instance.context layer = instance.data["transientData"]["layer"] @@ -112,7 +107,13 @@ class CollectMayaRender(pyblish.api.InstancePlugin): except UnsupportedRendererException as exc: raise KnownPublishError(exc) render_products = layer_render_products.layer_data.products - assert render_products, "no render products generated" + if not render_products: + self.log.error( + "No render products generated for '%s'. You might not have " + "any render camera in the renderlayer or render end frame is " + "lower than start frame.", + instance.name + ) expected_files = [] multipart = False for product in render_products: @@ -130,16 +131,21 @@ class CollectMayaRender(pyblish.api.InstancePlugin): }) has_cameras = any(product.camera for product in render_products) - assert has_cameras, "No render cameras found." - - self.log.debug("multipart: {}".format( - multipart)) - assert expected_files, "no file names were generated, this is a bug" - self.log.debug( - "expected files: {}".format( - json.dumps(expected_files, indent=4, sort_keys=True) + if render_products and not has_cameras: + self.log.error( + "No render cameras found for: %s", + instance ) - ) + if not expected_files: + self.log.warning( + "No file names were generated, this is a bug.") + + for render_product in render_products: + self.log.debug(render_product) + self.log.debug("multipart: {}".format(multipart)) + self.log.debug("expected files: {}".format( + json.dumps(expected_files, indent=4, sort_keys=True) + )) # if we want to attach render to product, check if we have AOV's # in expectedFiles. If so, raise error as we cannot attach AOV @@ -151,14 +157,14 @@ class CollectMayaRender(pyblish.api.InstancePlugin): ) # append full path - aov_dict = {} image_directory = os.path.join( cmds.workspace(query=True, rootDirectory=True), cmds.workspace(fileRuleEntry="images") ) # replace relative paths with absolute. Render products are # returned as list of dictionaries. - publish_meta_path = None + publish_meta_path = "NOT-SET" + aov_dict = {} for aov in expected_files: full_paths = [] aov_first_key = list(aov.keys())[0] @@ -169,14 +175,6 @@ class CollectMayaRender(pyblish.api.InstancePlugin): publish_meta_path = os.path.dirname(full_path) aov_dict[aov_first_key] = full_paths full_exp_files = [aov_dict] - self.log.debug(full_exp_files) - - if publish_meta_path is None: - raise KnownPublishError("Unable to detect any expected output " - "images for: {}. Make sure you have a " - "renderable camera and a valid frame " - "range set for your renderlayer." - "".format(instance.name)) frame_start_render = int(self.get_render_attribute( "startFrame", layer=layer_name)) @@ -222,7 +220,8 @@ class CollectMayaRender(pyblish.api.InstancePlugin): common_publish_meta_path = "/" + common_publish_meta_path self.log.debug( - "Publish meta path: {}".format(common_publish_meta_path)) + "Publish meta path: {}".format(common_publish_meta_path) + ) # Get layer specific settings, might be overrides colorspace_data = lib.get_color_management_preferences() diff --git a/client/ayon_core/hosts/maya/plugins/publish/extract_gpu_cache.py b/client/ayon_core/hosts/maya/plugins/publish/extract_gpu_cache.py index 19825b769c..4b293b5785 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/extract_gpu_cache.py +++ b/client/ayon_core/hosts/maya/plugins/publish/extract_gpu_cache.py @@ -5,7 +5,8 @@ from maya import cmds from ayon_core.pipeline import publish -class ExtractGPUCache(publish.Extractor): +class ExtractGPUCache(publish.Extractor, + publish.OptionalPyblishPluginMixin): """Extract the content of the instance to a GPU cache file.""" label = "GPU Cache" @@ -20,6 +21,9 @@ class ExtractGPUCache(publish.Extractor): useBaseTessellation = True def process(self, instance): + if not self.is_active(instance.data): + return + cmds.loadPlugin("gpuCache", quiet=True) staging_dir = self.staging_dir(instance) diff --git a/client/ayon_core/hosts/maya/plugins/publish/extract_pointcache.py b/client/ayon_core/hosts/maya/plugins/publish/extract_pointcache.py index f2187063fc..5de72f7674 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/extract_pointcache.py +++ b/client/ayon_core/hosts/maya/plugins/publish/extract_pointcache.py @@ -26,6 +26,10 @@ class ExtractAlembic(publish.Extractor): families = ["pointcache", "model", "vrayproxy.alembic"] targets = ["local", "remote"] + # From settings + bake_attributes = [] + bake_attribute_prefixes = [] + def process(self, instance): if instance.data.get("farm"): self.log.debug("Should be processed on farm, skipping.") @@ -40,10 +44,12 @@ class ExtractAlembic(publish.Extractor): attrs = instance.data.get("attr", "").split(";") attrs = [value for value in attrs if value.strip()] attrs += instance.data.get("userDefinedAttributes", []) + attrs += self.bake_attributes attrs += ["cbId"] attr_prefixes = instance.data.get("attrPrefix", "").split(";") attr_prefixes = [value for value in attr_prefixes if value.strip()] + attr_prefixes += self.bake_attribute_prefixes self.log.debug("Extracting pointcache..") dirname = self.staging_dir(instance) 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/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 + + + + 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: diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_color_sets.py b/client/ayon_core/hosts/maya/plugins/publish/validate_color_sets.py index e69717fad0..f70b46f89e 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_color_sets.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_color_sets.py @@ -10,7 +10,7 @@ from ayon_core.pipeline.publish import ( ) -class ValidateColorSets(pyblish.api.Validator, +class ValidateColorSets(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validate all meshes in the instance have unlocked normals diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_look_shading_group.py b/client/ayon_core/hosts/maya/plugins/publish/validate_look_shading_group.py index e70a805de4..070974aef5 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_look_shading_group.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_look_shading_group.py @@ -47,10 +47,18 @@ class ValidateShadingEngine(pyblish.api.InstancePlugin, shape, destination=True, type="shadingEngine" ) or [] for shading_engine in shading_engines: - name = ( - cmds.listConnections(shading_engine + ".surfaceShader")[0] - + "SG" + materials = cmds.listConnections( + shading_engine + ".surfaceShader", + source=True, destination=False ) + if not materials: + cls.log.warning( + "Shading engine '{}' has no material connected to its " + ".surfaceShader attribute.".format(shading_engine)) + continue + + material = materials[0] # there should only ever be one input + name = material + "SG" if shading_engine != name: invalid.append(shading_engine) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_ngons.py b/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_ngons.py index b6d3dc73fd..d1d7e49fa4 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_ngons.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_ngons.py @@ -10,7 +10,7 @@ from ayon_core.pipeline.publish import ( ) -class ValidateMeshNgons(pyblish.api.Validator, +class ValidateMeshNgons(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Ensure that meshes don't have ngons diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_no_negative_scale.py b/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_no_negative_scale.py index ff1dca87cf..bf1489f92e 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_no_negative_scale.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_no_negative_scale.py @@ -16,7 +16,7 @@ def _as_report_list(values, prefix="- ", suffix="\n"): return prefix + (suffix + prefix).join(values) -class ValidateMeshNoNegativeScale(pyblish.api.Validator, +class ValidateMeshNoNegativeScale(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Ensure that meshes don't have a negative scale. 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..958707e4f4 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: @@ -16,7 +101,7 @@ def _as_report_list(values, prefix="- ", suffix="\n"): return prefix + (suffix + prefix).join(values) -class ValidateMeshNonManifold(pyblish.api.Validator, +class ValidateMeshNonManifold(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Ensure that meshes don't have non-manifold edges or vertices @@ -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) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_normals_unlocked.py b/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_normals_unlocked.py index 1790a94580..76b716d01f 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_normals_unlocked.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_normals_unlocked.py @@ -18,7 +18,7 @@ def _as_report_list(values, prefix="- ", suffix="\n"): return prefix + (suffix + prefix).join(values) -class ValidateMeshNormalsUnlocked(pyblish.api.Validator, +class ValidateMeshNormalsUnlocked(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validate all meshes in the instance have unlocked normals diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_no_animation.py b/client/ayon_core/hosts/maya/plugins/publish/validate_no_animation.py index 6e0719628f..bf45c0e974 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_no_animation.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_no_animation.py @@ -16,7 +16,7 @@ def _as_report_list(values, prefix="- ", suffix="\n"): return prefix + (suffix + prefix).join(values) -class ValidateNoAnimation(pyblish.api.Validator, +class ValidateNoAnimation(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Ensure no keyframes on nodes in the Instance. 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..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,22 +19,17 @@ 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) + shapes = cmds.ls(all_descendents, 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 diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_render_single_camera.py b/client/ayon_core/hosts/maya/plugins/publish/validate_render_single_camera.py index 0171318813..e186d74b89 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_render_single_camera.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_render_single_camera.py @@ -1,4 +1,5 @@ import re +import inspect import pyblish.api from maya import cmds @@ -36,7 +37,10 @@ class ValidateRenderSingleCamera(pyblish.api.InstancePlugin, return invalid = self.get_invalid(instance) if invalid: - raise PublishValidationError("Invalid cameras for render.") + raise PublishValidationError( + "Invalid render cameras.", + description=self.get_description() + ) @classmethod def get_invalid(cls, instance): @@ -51,17 +55,30 @@ class ValidateRenderSingleCamera(pyblish.api.InstancePlugin, RenderSettings.get_image_prefix_attr(renderer) ) - + renderlayer = instance.data["renderlayer"] if len(cameras) > 1: if re.search(cls.R_CAMERA_TOKEN, file_prefix): # if there is token in prefix and we have more then # 1 camera, all is ok. return - cls.log.error("Multiple renderable cameras found for %s: %s " % - (instance.data["setMembers"], cameras)) - return [instance.data["setMembers"]] + cameras + cls.log.error( + "Multiple renderable cameras found for %s: %s ", + renderlayer, ", ".join(cameras)) + return [renderlayer] + cameras elif len(cameras) < 1: - cls.log.error("No renderable cameras found for %s " % - instance.data["setMembers"]) - return [instance.data["setMembers"]] + cls.log.error("No renderable cameras found for %s ", renderlayer) + return [renderlayer] + + def get_description(self): + return inspect.cleandoc( + """### Render Cameras Invalid + + Your render cameras are misconfigured. You may have no render + camera set or have multiple cameras with a render filename + prefix that does not include the `` token. + + See the logs for more details about the cameras. + + """ + ) 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..52ce3c5436 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,11 +6,12 @@ import ayon_core.hosts.maya.api.action from ayon_core.pipeline.publish import ( RepairAction, ValidateMeshOrder, + PublishValidationError, OptionalPyblishPluginMixin ) -class ValidateShapeRenderStats(pyblish.api.Validator, +class ValidateShapeRenderStats(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Ensure all render stats are set to the default values.""" @@ -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) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_shape_zero.py b/client/ayon_core/hosts/maya/plugins/publish/validate_shape_zero.py index 4f4826776c..6c89258085 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_shape_zero.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_shape_zero.py @@ -12,7 +12,7 @@ from ayon_core.pipeline.publish import ( ) -class ValidateShapeZero(pyblish.api.Validator, +class ValidateShapeZero(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Shape components may not have any "tweak" values 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..cd96ebb10d 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 @@ -10,7 +11,7 @@ from ayon_core.pipeline.publish import ( ) -class ValidateTransformZero(pyblish.api.Validator, +class ValidateTransformZero(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Transforms can't have any values @@ -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,14 +70,24 @@ class ValidateTransformZero(pyblish.api.Validator, return invalid = self.get_invalid(instance) if invalid: - names = "
".join( " - {}".format(node) for node in invalid ) raise PublishValidationError( title="Transform Zero", + description=self.get_description(), message="The model publish allows no transformations. You must" " freeze transformations to continue.

" - "Nodes found with transform values: " + "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. + + """) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_unique_names.py b/client/ayon_core/hosts/maya/plugins/publish/validate_unique_names.py index 72c3c7dc72..0066d70531 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_unique_names.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_unique_names.py @@ -9,7 +9,7 @@ from ayon_core.pipeline.publish import ( ) -class ValidateUniqueNames(pyblish.api.Validator, +class ValidateUniqueNames(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """transform names should be unique diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_yeti_rig_input_in_instance.py b/client/ayon_core/hosts/maya/plugins/publish/validate_yeti_rig_input_in_instance.py index aa229875fe..77e189e37b 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_yeti_rig_input_in_instance.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_yeti_rig_input_in_instance.py @@ -10,7 +10,7 @@ from ayon_core.pipeline.publish import ( ) -class ValidateYetiRigInputShapesInInstance(pyblish.api.Validator, +class ValidateYetiRigInputShapesInInstance(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validate if all input nodes are part of the instance's hierarchy""" diff --git a/client/ayon_core/hosts/maya/tools/mayalookassigner/vray_proxies.py b/client/ayon_core/hosts/maya/tools/mayalookassigner/vray_proxies.py index 74cdbeb7d4..88ef4b201a 100644 --- a/client/ayon_core/hosts/maya/tools/mayalookassigner/vray_proxies.py +++ b/client/ayon_core/hosts/maya/tools/mayalookassigner/vray_proxies.py @@ -51,7 +51,7 @@ def assign_vrayproxy_shaders(vrayproxy, assignments): index += 1 -def vrayproxy_assign_look(vrayproxy, product_name="lookDefault"): +def vrayproxy_assign_look(vrayproxy, product_name="lookMain"): # type: (str, str) -> None """Assign look to vray proxy. diff --git a/client/ayon_core/hosts/nuke/api/lib.py b/client/ayon_core/hosts/nuke/api/lib.py index a9c5aac659..4fcba8d2d4 100644 --- a/client/ayon_core/hosts/nuke/api/lib.py +++ b/client/ayon_core/hosts/nuke/api/lib.py @@ -389,7 +389,13 @@ def imprint(node, data, tab=None): """ for knob in create_knobs(data, tab): - node.addKnob(knob) + # If knob name exists we set the value. Technically there could be + # multiple knobs with the same name, but the intent is not to have + # duplicated knobs so we do not account for that. + if knob.name() in node.knobs().keys(): + node[knob.name()].setValue(knob.value()) + else: + node.addKnob(knob) @deprecated diff --git a/client/ayon_core/hosts/nuke/plugins/publish/extract_script_save.py b/client/ayon_core/hosts/nuke/plugins/publish/extract_script_save.py index e44e5686b6..d325684a7c 100644 --- a/client/ayon_core/hosts/nuke/plugins/publish/extract_script_save.py +++ b/client/ayon_core/hosts/nuke/plugins/publish/extract_script_save.py @@ -2,10 +2,10 @@ import nuke import pyblish.api -class ExtractScriptSave(pyblish.api.Extractor): +class ExtractScriptSave(pyblish.api.InstancePlugin): """Save current Nuke workfile script""" label = 'Script Save' - order = pyblish.api.Extractor.order - 0.1 + order = pyblish.api.ExtractorOrder - 0.1 hosts = ['nuke'] def process(self, instance): diff --git a/client/ayon_core/hosts/substancepainter/plugins/load/load_mesh.py b/client/ayon_core/hosts/substancepainter/plugins/load/load_mesh.py index f2254c0907..d940d7b05c 100644 --- a/client/ayon_core/hosts/substancepainter/plugins/load/load_mesh.py +++ b/client/ayon_core/hosts/substancepainter/plugins/load/load_mesh.py @@ -18,7 +18,7 @@ class SubstanceLoadProjectMesh(load.LoaderPlugin): """Load mesh for project""" product_types = {"*"} - representations = ["abc", "fbx", "obj", "gltf"] + representations = ["abc", "fbx", "obj", "gltf", "usd", "usda", "usdc"] label = "Load mesh" order = -10 diff --git a/client/ayon_core/hosts/tvpaint/plugins/publish/extract_sequence.py b/client/ayon_core/hosts/tvpaint/plugins/publish/extract_sequence.py index ab30e3dc10..fe5e148b7b 100644 --- a/client/ayon_core/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/client/ayon_core/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -25,8 +25,9 @@ from ayon_core.hosts.tvpaint.lib import ( ) -class ExtractSequence(pyblish.api.Extractor): +class ExtractSequence(pyblish.api.InstancePlugin): label = "Extract Sequence" + order = pyblish.api.ExtractorOrder hosts = ["tvpaint"] families = ["review", "render"] diff --git a/client/ayon_core/lib/terminal.py b/client/ayon_core/lib/terminal.py index a22f2358aa..10fcc79a27 100644 --- a/client/ayon_core/lib/terminal.py +++ b/client/ayon_core/lib/terminal.py @@ -1,15 +1,5 @@ # -*- coding: utf-8 -*- """Package helping with colorizing and formatting terminal output.""" -# :: -# //. ... .. ///. //. -# ///\\\ \\\ \\ ///\\\ /// -# /// \\ \\\ \\ /// \\ /// // -# \\\ // \\\ // \\\ // \\\// ./ -# \\\// \\\// \\\// \\\' // -# \\\ \\\ \\\ \\\// -# ''' ''' ''' ''' -# ..---===[[ PyP3 Setup ]]===---... -# import re import time import threading diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index 08e0bc9237..4d778c2091 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -45,7 +45,7 @@ ARRAY_TYPE_REGEX = re.compile(r"^(int|float|string)\[\d+\]$") IMAGE_EXTENSIONS = { ".ani", ".anim", ".apng", ".art", ".bmp", ".bpg", ".bsave", - ".cal", ".cin", ".cpc", ".cpt", ".dds", ".dpx", ".ecw", ".exr", + ".cal", ".cin", ".cpc", ".cpt", ".dds", ".dng", ".dpx", ".ecw", ".exr", ".fits", ".flic", ".flif", ".fpx", ".gif", ".hdri", ".hevc", ".icer", ".icns", ".ico", ".cur", ".ics", ".ilbm", ".jbig", ".jbig2", ".jng", ".jpeg", ".jpeg-ls", ".jpeg-hdr", ".2000", ".jpg", diff --git a/client/ayon_core/modules/clockify/launcher_actions/ClockifyStart.py b/client/ayon_core/modules/clockify/launcher_actions/ClockifyStart.py index 61c5eac2f5..8381c7d73e 100644 --- a/client/ayon_core/modules/clockify/launcher_actions/ClockifyStart.py +++ b/client/ayon_core/modules/clockify/launcher_actions/ClockifyStart.py @@ -11,19 +11,17 @@ class ClockifyStart(LauncherAction): order = 500 clockify_api = ClockifyAPI() - def is_compatible(self, session): + def is_compatible(self, selection): """Return whether the action is compatible with the session""" - if "AYON_TASK_NAME" in session: - return True - return False + return selection.is_task_selected - def process(self, session, **kwargs): + def process(self, selection, **kwargs): self.clockify_api.set_api() user_id = self.clockify_api.user_id workspace_id = self.clockify_api.workspace_id - project_name = session["AYON_PROJECT_NAME"] - folder_path = session["AYON_FOLDER_PATH"] - task_name = session["AYON_TASK_NAME"] + project_name = selection.project_name + folder_path = selection.folder_path + task_name = selection.task_name description = "/".join([folder_path.lstrip("/"), task_name]) # fetch folder entity diff --git a/client/ayon_core/modules/clockify/launcher_actions/ClockifySync.py b/client/ayon_core/modules/clockify/launcher_actions/ClockifySync.py index 72187c6d28..5388f47c98 100644 --- a/client/ayon_core/modules/clockify/launcher_actions/ClockifySync.py +++ b/client/ayon_core/modules/clockify/launcher_actions/ClockifySync.py @@ -19,15 +19,18 @@ class ClockifySync(LauncherAction): order = 500 clockify_api = ClockifyAPI() - def is_compatible(self, session): + def is_compatible(self, selection): """Check if there's some projects to sync""" + if selection.is_project_selected: + return True + try: next(ayon_api.get_projects()) return True except StopIteration: return False - def process(self, session, **kwargs): + def process(self, selection, **kwargs): self.clockify_api.set_api() workspace_id = self.clockify_api.workspace_id user_id = self.clockify_api.user_id @@ -37,10 +40,9 @@ class ClockifySync(LauncherAction): raise ClockifyPermissionsCheckFailed( "Current CLockify user is missing permissions for this action!" ) - project_name = session.get("AYON_PROJECT_NAME") or "" - if project_name.strip(): - projects_to_sync = [ayon_api.get_project(project_name)] + if selection.is_project_selected: + projects_to_sync = [selection.project_entity] else: projects_to_sync = ayon_api.get_projects() diff --git a/client/ayon_core/modules/deadline/plugins/publish/submit_aftereffects_deadline.py b/client/ayon_core/modules/deadline/plugins/publish/submit_aftereffects_deadline.py index a284464009..675346105c 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/submit_aftereffects_deadline.py +++ b/client/ayon_core/modules/deadline/plugins/publish/submit_aftereffects_deadline.py @@ -80,6 +80,8 @@ class AfterEffectsSubmitDeadline( "FTRACK_API_KEY", "FTRACK_API_USER", "FTRACK_SERVER", + "AYON_BUNDLE_NAME", + "AYON_DEFAULT_SETTINGS_VARIANT", "AYON_PROJECT_NAME", "AYON_FOLDER_PATH", "AYON_TASK_NAME", diff --git a/client/ayon_core/modules/deadline/plugins/publish/submit_blender_deadline.py b/client/ayon_core/modules/deadline/plugins/publish/submit_blender_deadline.py index ae19e63a37..ab342c1a9d 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/submit_blender_deadline.py +++ b/client/ayon_core/modules/deadline/plugins/publish/submit_blender_deadline.py @@ -102,6 +102,8 @@ class BlenderSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, "FTRACK_API_USER", "FTRACK_SERVER", "OPENPYPE_SG_USER", + "AYON_BUNDLE_NAME", + "AYON_DEFAULT_SETTINGS_VARIANT", "AYON_PROJECT_NAME", "AYON_FOLDER_PATH", "AYON_TASK_NAME", diff --git a/client/ayon_core/modules/deadline/plugins/publish/submit_fusion_deadline.py b/client/ayon_core/modules/deadline/plugins/publish/submit_fusion_deadline.py index cf124c0bcc..bfb65708e6 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/submit_fusion_deadline.py +++ b/client/ayon_core/modules/deadline/plugins/publish/submit_fusion_deadline.py @@ -225,6 +225,8 @@ class FusionSubmitDeadline( "FTRACK_API_KEY", "FTRACK_API_USER", "FTRACK_SERVER", + "AYON_BUNDLE_NAME", + "AYON_DEFAULT_SETTINGS_VARIANT", "AYON_PROJECT_NAME", "AYON_FOLDER_PATH", "AYON_TASK_NAME", diff --git a/client/ayon_core/modules/deadline/plugins/publish/submit_harmony_deadline.py b/client/ayon_core/modules/deadline/plugins/publish/submit_harmony_deadline.py index beb8afc3a3..d52b16b27d 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/submit_harmony_deadline.py +++ b/client/ayon_core/modules/deadline/plugins/publish/submit_harmony_deadline.py @@ -273,6 +273,8 @@ class HarmonySubmitDeadline( "FTRACK_API_KEY", "FTRACK_API_USER", "FTRACK_SERVER", + "AYON_BUNDLE_NAME", + "AYON_DEFAULT_SETTINGS_VARIANT", "AYON_PROJECT_NAME", "AYON_FOLDER_PATH", "AYON_TASK_NAME", diff --git a/client/ayon_core/modules/deadline/plugins/publish/submit_max_deadline.py b/client/ayon_core/modules/deadline/plugins/publish/submit_max_deadline.py index 1abefa515a..cba05f6948 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/client/ayon_core/modules/deadline/plugins/publish/submit_max_deadline.py @@ -106,12 +106,14 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, "FTRACK_API_USER", "FTRACK_SERVER", "OPENPYPE_SG_USER", + "AYON_BUNDLE_NAME", + "AYON_DEFAULT_SETTINGS_VARIANT", "AYON_PROJECT_NAME", "AYON_FOLDER_PATH", "AYON_TASK_NAME", "AYON_WORKDIR", "AYON_APP_NAME", - "IS_TEST" + "IS_TEST", ] environment = { diff --git a/client/ayon_core/modules/deadline/plugins/publish/submit_maya_deadline.py b/client/ayon_core/modules/deadline/plugins/publish/submit_maya_deadline.py index 5602b02707..0300b12104 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/client/ayon_core/modules/deadline/plugins/publish/submit_maya_deadline.py @@ -207,6 +207,8 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, "FTRACK_API_USER", "FTRACK_SERVER", "OPENPYPE_SG_USER", + "AYON_BUNDLE_NAME", + "AYON_DEFAULT_SETTINGS_VARIANT", "AYON_PROJECT_NAME", "AYON_FOLDER_PATH", "AYON_TASK_NAME", diff --git a/client/ayon_core/modules/deadline/plugins/publish/submit_nuke_deadline.py b/client/ayon_core/modules/deadline/plugins/publish/submit_nuke_deadline.py index ac01af901c..d70cb75bf3 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/submit_nuke_deadline.py +++ b/client/ayon_core/modules/deadline/plugins/publish/submit_nuke_deadline.py @@ -376,6 +376,8 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin, keys = [ "PYTHONPATH", "PATH", + "AYON_BUNDLE_NAME", + "AYON_DEFAULT_SETTINGS_VARIANT", "AYON_PROJECT_NAME", "AYON_FOLDER_PATH", "AYON_TASK_NAME", @@ -388,7 +390,6 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin, "TOOL_ENV", "FOUNDRY_LICENSE", "OPENPYPE_SG_USER", - "AYON_BUNDLE_NAME", ] # add allowed keys from preset if any diff --git a/client/ayon_core/modules/deadline/plugins/publish/submit_publish_cache_job.py b/client/ayon_core/modules/deadline/plugins/publish/submit_publish_cache_job.py index 50bd414587..910b2e46db 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/submit_publish_cache_job.py +++ b/client/ayon_core/modules/deadline/plugins/publish/submit_publish_cache_job.py @@ -133,6 +133,9 @@ class ProcessSubmittedCacheJobOnFarm(pyblish.api.InstancePlugin, "AYON_RENDER_JOB": "0", "AYON_REMOTE_PUBLISH": "0", "AYON_BUNDLE_NAME": os.environ["AYON_BUNDLE_NAME"], + "AYON_DEFAULT_SETTINGS_VARIANT": ( + os.environ["AYON_DEFAULT_SETTINGS_VARIANT"] + ), } # add environments from self.environ_keys diff --git a/client/ayon_core/modules/deadline/plugins/publish/submit_publish_job.py b/client/ayon_core/modules/deadline/plugins/publish/submit_publish_job.py index 84bac6d017..af5839d0cf 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/submit_publish_job.py +++ b/client/ayon_core/modules/deadline/plugins/publish/submit_publish_job.py @@ -210,6 +210,9 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, "AYON_RENDER_JOB": "0", "AYON_REMOTE_PUBLISH": "0", "AYON_BUNDLE_NAME": os.environ["AYON_BUNDLE_NAME"], + "AYON_DEFAULT_SETTINGS_VARIANT": ( + os.environ["AYON_DEFAULT_SETTINGS_VARIANT"] + ), } # add environments from self.environ_keys diff --git a/client/ayon_core/pipeline/actions.py b/client/ayon_core/pipeline/actions.py index 8e0ce7e583..eae2fc94b5 100644 --- a/client/ayon_core/pipeline/actions.py +++ b/client/ayon_core/pipeline/actions.py @@ -1,4 +1,8 @@ import logging +import warnings + +import ayon_api + from ayon_core.pipeline.plugin_discover import ( discover, register_plugin, @@ -10,6 +14,288 @@ from ayon_core.pipeline.plugin_discover import ( from .load.utils import get_representation_path_from_context +class LauncherActionSelection: + """Object helper to pass selection to actions. + + Object support backwards compatibility for 'session' from OpenPype where + environment variable keys were used to define selection. + + Args: + project_name (str): Selected project name. + folder_id (str): Selected folder id. + task_id (str): Selected task id. + folder_path (Optional[str]): Selected folder path. + task_name (Optional[str]): Selected task name. + project_entity (Optional[dict[str, Any]]): Project entity. + folder_entity (Optional[dict[str, Any]]): Folder entity. + task_entity (Optional[dict[str, Any]]): Task entity. + + """ + def __init__( + self, + project_name, + folder_id, + task_id, + folder_path=None, + task_name=None, + project_entity=None, + folder_entity=None, + task_entity=None + ): + self._project_name = project_name + self._folder_id = folder_id + self._task_id = task_id + + self._folder_path = folder_path + self._task_name = task_name + + self._project_entity = project_entity + self._folder_entity = folder_entity + self._task_entity = task_entity + + def __getitem__(self, key): + warnings.warn( + ( + "Using deprecated access to selection data. Please use" + " attributes and methods" + " defined by 'LauncherActionSelection'." + ), + category=DeprecationWarning + ) + if key in {"AYON_PROJECT_NAME", "AVALON_PROJECT"}: + return self.project_name + if key in {"AYON_FOLDER_PATH", "AVALON_ASSET"}: + return self.folder_path + if key in {"AYON_TASK_NAME", "AVALON_TASK"}: + return self.task_name + raise KeyError(f"Key: {key} not found") + + def __iter__(self): + for key in self.keys(): + yield key + + def __contains__(self, key): + warnings.warn( + ( + "Using deprecated access to selection data. Please use" + " attributes and methods" + " defined by 'LauncherActionSelection'." + ), + category=DeprecationWarning + ) + # Fake missing keys check for backwards compatibility + if key in { + "AYON_PROJECT_NAME", + "AVALON_PROJECT", + }: + return self._project_name is not None + if key in { + "AYON_FOLDER_PATH", + "AVALON_ASSET", + }: + return self._folder_id is not None + if key in { + "AYON_TASK_NAME", + "AVALON_TASK", + }: + return self._task_id is not None + return False + + def get(self, key, default=None): + """ + + Deprecated: + Added for backwards compatibility with older actions. + + """ + warnings.warn( + ( + "Using deprecated access to selection data. Please use" + " attributes and methods" + " defined by 'LauncherActionSelection'." + ), + category=DeprecationWarning + ) + try: + return self[key] + except KeyError: + return default + + def items(self): + """ + + Deprecated: + Added for backwards compatibility with older actions. + + """ + for key, value in ( + ("AYON_PROJECT_NAME", self.project_name), + ("AYON_FOLDER_PATH", self.folder_path), + ("AYON_TASK_NAME", self.task_name), + ): + if value is not None: + yield (key, value) + + def keys(self): + """ + + Deprecated: + Added for backwards compatibility with older actions. + + """ + for key, _ in self.items(): + yield key + + def values(self): + """ + + Deprecated: + Added for backwards compatibility with older actions. + + """ + for _, value in self.items(): + yield value + + def get_project_name(self): + """Selected project name. + + Returns: + Union[str, None]: Selected project name. + + """ + return self._project_name + + def get_folder_id(self): + """Selected folder id. + + Returns: + Union[str, None]: Selected folder id. + + """ + return self._folder_id + + def get_folder_path(self): + """Selected folder path. + + Returns: + Union[str, None]: Selected folder path. + + """ + if self._folder_id is None: + return None + if self._folder_path is None: + self._folder_path = self.folder_entity["path"] + return self._folder_path + + def get_task_id(self): + """Selected task id. + + Returns: + Union[str, None]: Selected task id. + + """ + return self._task_id + + def get_task_name(self): + """Selected task name. + + Returns: + Union[str, None]: Selected task name. + + """ + if self._task_id is None: + return None + if self._task_name is None: + self._task_name = self.task_entity["name"] + return self._task_name + + def get_project_entity(self): + """Project entity for the selection. + + Returns: + Union[dict[str, Any], None]: Project entity. + + """ + if self._project_name is None: + return None + if self._project_entity is None: + self._project_entity = ayon_api.get_project(self._project_name) + return self._project_entity + + def get_folder_entity(self): + """Folder entity for the selection. + + Returns: + Union[dict[str, Any], None]: Folder entity. + + """ + if self._project_name is None or self._folder_id is None: + return None + if self._folder_entity is None: + self._folder_entity = ayon_api.get_folder_by_id( + self._project_name, self._folder_id + ) + return self._folder_entity + + def get_task_entity(self): + """Task entity for the selection. + + Returns: + Union[dict[str, Any], None]: Task entity. + + """ + if ( + self._project_name is None + or self._task_id is None + ): + return None + if self._task_entity is None: + self._task_entity = ayon_api.get_task_by_id( + self._project_name, self._task_id + ) + return self._task_entity + + @property + def is_project_selected(self): + """Return whether a project is selected. + + Returns: + bool: Whether a project is selected. + + """ + return self._project_name is not None + + @property + def is_folder_selected(self): + """Return whether a folder is selected. + + Returns: + bool: Whether a folder is selected. + + """ + return self._folder_id is not None + + @property + def is_task_selected(self): + """Return whether a task is selected. + + Returns: + bool: Whether a task is selected. + + """ + return self._task_id is not None + + project_name = property(get_project_name) + folder_id = property(get_folder_id) + task_id = property(get_task_id) + folder_path = property(get_folder_path) + task_name = property(get_task_name) + + project_entity = property(get_project_entity) + folder_entity = property(get_folder_entity) + task_entity = property(get_task_entity) + + class LauncherAction(object): """A custom action available""" name = None @@ -21,17 +307,23 @@ class LauncherAction(object): log = logging.getLogger("LauncherAction") log.propagate = True - def is_compatible(self, session): + def is_compatible(self, selection): """Return whether the class is compatible with the Session. Args: - session (dict[str, Union[str, None]]): Session data with - AYON_PROJECT_NAME, AYON_FOLDER_PATH and AYON_TASK_NAME. - """ + selection (LauncherActionSelection): Data with selection. + """ return True - def process(self, session, **kwargs): + def process(self, selection, **kwargs): + """Process the action. + + Args: + selection (LauncherActionSelection): Data with selection. + **kwargs: Additional arguments. + + """ pass diff --git a/client/ayon_core/pipeline/context_tools.py b/client/ayon_core/pipeline/context_tools.py index 84a17be8f2..ca409fadf2 100644 --- a/client/ayon_core/pipeline/context_tools.py +++ b/client/ayon_core/pipeline/context_tools.py @@ -97,8 +97,8 @@ def install_host(host): """Install `host` into the running Python session. Args: - host (module): A Python module containing the Avalon - avalon host-interface. + host (HostBase): A host interface object. + """ global _is_installed @@ -154,6 +154,13 @@ def install_host(host): def install_ayon_plugins(project_name=None, host_name=None): + """Install AYON core plugins and make sure the core is initialized. + + Args: + project_name (Optional[str]): Name of project to install plugins for. + host_name (Optional[str]): Name of host to install plugins for. + + """ # Make sure global AYON connection has set site id and version # - this is necessary if 'install_host' is not called initialize_ayon_connection() @@ -223,6 +230,12 @@ def install_ayon_plugins(project_name=None, host_name=None): def install_openpype_plugins(project_name=None, host_name=None): + """Install AYON core plugins and make sure the core is initialized. + + Deprecated: + Use `install_ayon_plugins` instead. + + """ install_ayon_plugins(project_name, host_name) @@ -281,47 +294,6 @@ def deregister_host(): _registered_host["_"] = None -def debug_host(): - """A debug host, useful to debugging features that depend on a host""" - - host = types.ModuleType("debugHost") - - def ls(): - containers = [ - { - "representation": "ee-ft-a-uuid1", - "schema": "openpype:container-1.0", - "name": "Bruce01", - "objectName": "Bruce01_node", - "namespace": "_bruce01_", - "version": 3, - }, - { - "representation": "aa-bc-s-uuid2", - "schema": "openpype:container-1.0", - "name": "Bruce02", - "objectName": "Bruce01_node", - "namespace": "_bruce02_", - "version": 2, - } - ] - - for container in containers: - yield container - - host.__dict__.update({ - "ls": ls, - "open_file": lambda fname: None, - "save_file": lambda fname: None, - "current_file": lambda: os.path.expanduser("~/temp.txt"), - "has_unsaved_changes": lambda: False, - "work_root": lambda: os.path.expanduser("~/temp"), - "file_extensions": lambda: ["txt"], - }) - - return host - - def get_current_host_name(): """Current host name. @@ -347,7 +319,8 @@ def get_global_context(): Use 'get_current_context' to make sure you'll get current host integration context info. - Example: + Example:: + { "project_name": "Commercial", "folder_path": "Bunny", @@ -515,88 +488,13 @@ def get_current_context_template_data(settings=None): ) -def get_workdir_from_session(session=None, template_key=None): - """Template data for template fill from session keys. - - Args: - session (Union[Dict[str, str], None]): The Session to use. If not - provided use the currently active global Session. - template_key (str): Prepared template key from which workdir is - calculated. - - Returns: - str: Workdir path. - """ - - if session is not None: - project_name = session["AYON_PROJECT_NAME"] - host_name = session["AYON_HOST_NAME"] - else: - project_name = get_current_project_name() - host_name = get_current_host_name() - template_data = get_template_data_from_session(session) - - if not template_key: - task_type = template_data["task"]["type"] - template_key = get_workfile_template_key( - project_name, - task_type, - host_name, - ) - - anatomy = Anatomy(project_name) - template_obj = anatomy.get_template_item("work", template_key, "directory") - path = template_obj.format_strict(template_data) - if path: - path = os.path.normpath(path) - return path - - -def get_custom_workfile_template_from_session( - session=None, project_settings=None -): - """Filter and fill workfile template profiles by current context. - - This function cab be used only inside host where context is set. - - Args: - session (Optional[Dict[str, str]]): Session from which are taken - data. - project_settings(Optional[Dict[str, Any]]): Project settings. - - Returns: - str: Path to template or None if none of profiles match current - context. (Existence of formatted path is not validated.) - """ - - if session is not None: - project_name = session["AYON_PROJECT_NAME"] - folder_path = session["AYON_FOLDER_PATH"] - task_name = session["AYON_TASK_NAME"] - host_name = session["AYON_HOST_NAME"] - else: - context = get_current_context() - project_name = context["project_name"] - folder_path = context["folder_path"] - task_name = context["task_name"] - host_name = get_current_host_name() - - return get_custom_workfile_template_by_string_context( - project_name, - folder_path, - task_name, - host_name, - project_settings=project_settings - ) - - def get_current_context_custom_workfile_template(project_settings=None): """Filter and fill workfile template profiles by current context. - This function can be used only inside host where context is set. + This function can be used only inside host where current context is set. Args: - project_settings(Optional[Dict[str, Any]]): Project settings. + project_settings (Optional[dict[str, Any]]): Project settings Returns: str: Path to template or None if none of profiles match current diff --git a/client/ayon_core/pipeline/create/README.md b/client/ayon_core/pipeline/create/README.md index bbfd1bfa0f..09d3a22222 100644 --- a/client/ayon_core/pipeline/create/README.md +++ b/client/ayon_core/pipeline/create/README.md @@ -8,7 +8,7 @@ Discovers Creator plugins to be able create new instances and convert existing i Publish plugins are loaded because they can also define attributes definitions. These are less product type specific To be able define attributes Publish plugin must inherit from `AYONPyblishPluginMixin` and must override `get_attribute_defs` class method which must return list of attribute definitions. Values of publish plugin definitions are stored per plugin name under `publish_attributes`. Also can override `convert_attribute_values` class method which gives ability to modify values on instance before are used in CreatedInstance. Method `convert_attribute_values` can be also used without `get_attribute_defs` to modify values when changing compatibility (remove metadata from instance because are irrelevant). -Possible attribute definitions can be found in `openpype/pipeline/lib/attribute_definitions.py`. +Possible attribute definitions can be found in `ayon_core/lib/attribute_definitions.py`. Except creating and removing instances are all changes not automatically propagated to host context (scene/workfile/...) to propagate changes call `save_changes` which trigger update of all instances in context using Creators implementation. diff --git a/client/ayon_core/pipeline/legacy_io.py b/client/ayon_core/pipeline/legacy_io.py deleted file mode 100644 index d5b555845b..0000000000 --- a/client/ayon_core/pipeline/legacy_io.py +++ /dev/null @@ -1,36 +0,0 @@ -import logging -from ayon_core.pipeline import get_current_project_name - -Session = {} - -log = logging.getLogger(__name__) -log.warning( - "DEPRECATION WARNING: 'legacy_io' is deprecated and will be removed in" - " future versions of ayon-core addon." - "\nReading from Session won't give you updated information and changing" - " values won't affect global state of a process." -) - - -def session_data_from_environment(context_keys=False): - return {} - - -def is_installed(): - return False - - -def install(): - pass - - -def uninstall(): - pass - - -def active_project(*args, **kwargs): - return get_current_project_name() - - -def current_project(*args, **kwargs): - return get_current_project_name() diff --git a/client/ayon_core/plugins/actions/open_file_explorer.py b/client/ayon_core/plugins/actions/open_file_explorer.py index 69375a7859..6a456c75c1 100644 --- a/client/ayon_core/plugins/actions/open_file_explorer.py +++ b/client/ayon_core/plugins/actions/open_file_explorer.py @@ -18,18 +18,14 @@ class OpenTaskPath(LauncherAction): icon = "folder-open" order = 500 - def is_compatible(self, session): + def is_compatible(self, selection): """Return whether the action is compatible with the session""" - return bool(session.get("AYON_FOLDER_PATH")) + return selection.is_folder_selected - def process(self, session, **kwargs): + def process(self, selection, **kwargs): from qtpy import QtCore, QtWidgets - project_name = session["AYON_PROJECT_NAME"] - folder_path = session["AYON_FOLDER_PATH"] - task_name = session.get("AYON_TASK_NAME", None) - - path = self._get_workdir(project_name, folder_path, task_name) + path = self._get_workdir(selection) if not path: return @@ -60,16 +56,17 @@ class OpenTaskPath(LauncherAction): path = path.split(field, 1)[0] return path - def _get_workdir(self, project_name, folder_path, task_name): - project_entity = ayon_api.get_project(project_name) - folder_entity = ayon_api.get_folder_by_path(project_name, folder_path) - task_entity = ayon_api.get_task_by_name( - project_name, folder_entity["id"], task_name + def _get_workdir(self, selection): + data = get_template_data( + selection.project_entity, + selection.folder_entity, + selection.task_entity ) - data = get_template_data(project_entity, folder_entity, task_entity) - - anatomy = Anatomy(project_name) + anatomy = Anatomy( + selection.project_name, + project_entity=selection.project_entity + ) workdir = anatomy.get_template_item( "work", "default", "folder" ).format(data) diff --git a/client/ayon_core/plugins/publish/extract_burnin.py b/client/ayon_core/plugins/publish/extract_burnin.py index ab6353a29f..727d7f1bc2 100644 --- a/client/ayon_core/plugins/publish/extract_burnin.py +++ b/client/ayon_core/plugins/publish/extract_burnin.py @@ -194,6 +194,16 @@ class ExtractBurnin(publish.Extractor): ).format(host_name, product_type, task_name, profile)) return + burnins_per_repres = self._get_burnins_per_representations( + instance, burnin_defs + ) + if not burnins_per_repres: + self.log.debug( + "Skipped instance. No representations found matching a burnin" + "definition in: %s", burnin_defs + ) + return + burnin_options = self._get_burnin_options() # Prepare basic data for processing @@ -204,9 +214,6 @@ class ExtractBurnin(publish.Extractor): # Args that will execute the script executable_args = ["run", scriptpath] - burnins_per_repres = self._get_burnins_per_representations( - instance, burnin_defs - ) for repre, repre_burnin_defs in burnins_per_repres: # Create copy of `_burnin_data` and `_temp_data` for repre. burnin_data = copy.deepcopy(_burnin_data) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index 905158c851..790f7a32ed 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -619,7 +619,7 @@ class ExtractReview(pyblish.api.InstancePlugin): # Prepare input and output filepaths self.input_output_paths(new_repre, output_def, temp_data) - # Set output frames len to 1 when ouput is single image + # Set output frames len to 1 when output is single image if ( temp_data["output_ext_is_image"] and not temp_data["output_is_sequence"] @@ -955,7 +955,7 @@ class ExtractReview(pyblish.api.InstancePlugin): self.log.debug("New representation ext: `{}`".format(output_ext)) - # Output is image file sequence witht frames + # Output is image file sequence with frames output_ext_is_image = bool(output_ext in self.image_exts) output_is_sequence = bool( output_ext_is_image @@ -967,7 +967,7 @@ class ExtractReview(pyblish.api.InstancePlugin): frame_end = temp_data["output_frame_end"] filename_base = "{}_{}".format(filename, filename_suffix) - # Temporary tempalte for frame filling. Example output: + # Temporary template for frame filling. Example output: # "basename.%04d.exr" when `frame_end` == 1001 repr_file = "{}.%{:0>2}d.{}".format( filename_base, len(str(frame_end)), output_ext @@ -997,7 +997,7 @@ class ExtractReview(pyblish.api.InstancePlugin): self.log.debug("Creating dir: {}".format(dst_staging_dir)) os.makedirs(dst_staging_dir) - # Store stagingDir to representaion + # Store stagingDir to representation new_repre["stagingDir"] = dst_staging_dir # Store paths to temp data @@ -1228,16 +1228,6 @@ class ExtractReview(pyblish.api.InstancePlugin): reformat_in_baking = bool("reformated" in new_repre["tags"]) self.log.debug("reformat_in_baking: `{}`".format(reformat_in_baking)) - # Get instance data - pixel_aspect = temp_data["pixel_aspect"] - - if reformat_in_baking: - self.log.debug(( - "Using resolution from input. It is already " - "reformated from upstream process" - )) - pixel_aspect = 1 - # NOTE Skipped using instance's resolution full_input_path_single_file = temp_data["full_input_path_single_file"] try: @@ -1268,7 +1258,7 @@ class ExtractReview(pyblish.api.InstancePlugin): if reformat_in_baking: self.log.debug(( "Using resolution from input. It is already " - "reformated from upstream process" + "reformatted from upstream process" )) pixel_aspect = 1 output_width = input_width @@ -1374,7 +1364,7 @@ class ExtractReview(pyblish.api.InstancePlugin): # Make sure output width and height is not an odd number # When this can happen: # - if output definition has set width and height with odd number - # - `instance.data` contain width and height with odd numbeer + # - `instance.data` contain width and height with odd number if output_width % 2 != 0: self.log.warning(( "Converting output width from odd to even number. {} -> {}" @@ -1555,7 +1545,7 @@ class ExtractReview(pyblish.api.InstancePlugin): custom_tags (list): Custom Tags of processed representation. Returns: - list: Containg all output definitions matching entered tags. + list: Containing all output definitions matching entered tags. """ filtered_outputs = [] @@ -1820,8 +1810,8 @@ class OverscanCrop: """ # crop=width:height:x:y - explicit start x, y position # crop=width:height - x, y are related to center by width/height - # pad=width:heigth:x:y - explicit start x, y position - # pad=width:heigth - x, y are set to 0 by default + # pad=width:height:x:y - explicit start x, y position + # pad=width:height - x, y are set to 0 by default width = self.width() height = self.height() @@ -1869,7 +1859,7 @@ class OverscanCrop: # Replace "px" (and spaces before) with single space string_value = re.sub(r"([ ]+)?px", " ", string_value) string_value = re.sub(r"([ ]+)%", "%", string_value) - # Make sure +/- sign at the beggining of string is next to number + # Make sure +/- sign at the beginning of string is next to number string_value = re.sub(r"^([\+\-])[ ]+", "\g<1>", string_value) # Make sure +/- sign in the middle has zero spaces before number under # which belongs diff --git a/client/ayon_core/plugins/publish/help/validate_containers.xml b/client/ayon_core/plugins/publish/help/validate_containers.xml index 5d18bb4c19..321e73a303 100644 --- a/client/ayon_core/plugins/publish/help/validate_containers.xml +++ b/client/ayon_core/plugins/publish/help/validate_containers.xml @@ -10,7 +10,7 @@ Scene contains one or more outdated loaded containers, eg. versions loaded into ### How to repair? Use 'Scene Inventory' and update all highlighted old container to latest OR - refresh Publish and switch 'Validate Containers' toggle on 'Options' tab. +refresh Publish and switch 'Validate Containers' toggle on 'Context' tab. WARNING: Skipping this validator will result in publishing (and probably rendering) old version of loaded assets. diff --git a/client/ayon_core/tools/launcher/models/actions.py b/client/ayon_core/tools/launcher/models/actions.py index 97943e6ad7..6da34151b6 100644 --- a/client/ayon_core/tools/launcher/models/actions.py +++ b/client/ayon_core/tools/launcher/models/actions.py @@ -5,6 +5,7 @@ from ayon_core.lib import Logger, AYONSettingsRegistry from ayon_core.pipeline.actions import ( discover_launcher_actions, LauncherAction, + LauncherActionSelection, ) from ayon_core.pipeline.workfile import should_use_last_workfile_on_launch @@ -69,11 +70,6 @@ class ApplicationAction(LauncherAction): project_entities = {} _log = None - required_session_keys = ( - "AYON_PROJECT_NAME", - "AYON_FOLDER_PATH", - "AYON_TASK_NAME" - ) @property def log(self): @@ -81,18 +77,16 @@ class ApplicationAction(LauncherAction): self._log = Logger.get_logger(self.__class__.__name__) return self._log - def is_compatible(self, session): - for key in self.required_session_keys: - if not session.get(key): - return False + def is_compatible(self, selection): + if not selection.is_task_selected: + return False - project_name = session["AYON_PROJECT_NAME"] - project_entity = self.project_entities[project_name] + project_entity = self.project_entities[selection.project_name] apps = project_entity["attrib"].get("applications") if not apps or self.application.full_name not in apps: return False - project_settings = self.project_settings[project_name] + project_settings = self.project_settings[selection.project_name] only_available = project_settings["applications"]["only_available"] if only_available and not self.application.find_executable(): return False @@ -112,7 +106,7 @@ class ApplicationAction(LauncherAction): dialog.setDetailedText(details) dialog.exec_() - def process(self, session, **kwargs): + def process(self, selection, **kwargs): """Process the full Application action""" from ayon_core.lib import ( @@ -120,14 +114,11 @@ class ApplicationAction(LauncherAction): ApplicationLaunchFailed, ) - project_name = session["AYON_PROJECT_NAME"] - folder_path = session["AYON_FOLDER_PATH"] - task_name = session["AYON_TASK_NAME"] try: self.application.launch( - project_name=project_name, - folder_path=folder_path, - task_name=task_name, + project_name=selection.project_name, + folder_path=selection.folder_path, + task_name=selection.task_name, **self.data ) @@ -335,11 +326,11 @@ class ActionsModel: """ not_open_workfile_actions = self._get_no_last_workfile_for_context( project_name, folder_id, task_id) - session = self._prepare_session(project_name, folder_id, task_id) + selection = self._prepare_selection(project_name, folder_id, task_id) output = [] action_items = self._get_action_items(project_name) for identifier, action in self._get_action_objects().items(): - if not action.is_compatible(session): + if not action.is_compatible(selection): continue action_item = action_items[identifier] @@ -374,7 +365,7 @@ class ActionsModel: ) def trigger_action(self, project_name, folder_id, task_id, identifier): - session = self._prepare_session(project_name, folder_id, task_id) + selection = self._prepare_selection(project_name, folder_id, task_id) failed = False error_message = None action_label = identifier @@ -403,7 +394,7 @@ class ActionsModel: ) action.data["start_last_workfile"] = start_last_workfile - action.process(session) + action.process(selection) except Exception as exc: self.log.warning("Action trigger failed.", exc_info=True) failed = True @@ -440,29 +431,8 @@ class ActionsModel: .get(task_id, {}) ) - def _prepare_session(self, project_name, folder_id, task_id): - folder_path = None - if folder_id: - folder = self._controller.get_folder_entity( - project_name, folder_id) - if folder: - folder_path = folder["path"] - - task_name = None - if task_id: - task = self._controller.get_task_entity(project_name, task_id) - if task: - task_name = task["name"] - - return { - "AYON_PROJECT_NAME": project_name, - "AYON_FOLDER_PATH": folder_path, - "AYON_TASK_NAME": task_name, - # Deprecated - kept for backwards compatibility - "AVALON_PROJECT": project_name, - "AVALON_ASSET": folder_path, - "AVALON_TASK": task_name, - } + def _prepare_selection(self, project_name, folder_id, task_id): + return LauncherActionSelection(project_name, folder_id, task_id) def _get_discovered_action_classes(self): if self._discovered_actions is None: diff --git a/client/ayon_core/widgets/__init__.py b/client/ayon_core/widgets/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/client/ayon_core/widgets/password_dialog.py b/client/ayon_core/widgets/password_dialog.py deleted file mode 100644 index a4c50128ff..0000000000 --- a/client/ayon_core/widgets/password_dialog.py +++ /dev/null @@ -1,33 +0,0 @@ -# TODO remove - kept for kitsu addon which imported it -from qtpy import QtWidgets, QtCore, QtGui - - -class PressHoverButton(QtWidgets.QPushButton): - """ - Deprecated: - Use `openpype.tools.utils.PressHoverButton` instead. - """ - _mouse_pressed = False - _mouse_hovered = False - change_state = QtCore.Signal(bool) - - def mousePressEvent(self, event): - self._mouse_pressed = True - self._mouse_hovered = True - self.change_state.emit(self._mouse_hovered) - super(PressHoverButton, self).mousePressEvent(event) - - def mouseReleaseEvent(self, event): - self._mouse_pressed = False - self._mouse_hovered = False - self.change_state.emit(self._mouse_hovered) - super(PressHoverButton, self).mouseReleaseEvent(event) - - def mouseMoveEvent(self, event): - mouse_pos = self.mapFromGlobal(QtGui.QCursor.pos()) - under_mouse = self.rect().contains(mouse_pos) - if under_mouse != self._mouse_hovered: - self._mouse_hovered = under_mouse - self.change_state.emit(self._mouse_hovered) - - super(PressHoverButton, self).mouseMoveEvent(event) diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index f9ac1059ac..e61bf6986b 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -9,7 +9,7 @@ from ayon_server.settings import ( task_types_enum, ) -from ayon_server.types import ColorRGB_uint8, ColorRGBA_uint8 +from ayon_server.types import ColorRGBA_uint8 class ValidateBaseModel(BaseSettingsModel): @@ -221,7 +221,12 @@ class OIIOToolArgumentsModel(BaseSettingsModel): class ExtractOIIOTranscodeOutputModel(BaseSettingsModel): _layout = "expanded" - name: str = SettingsField("", title="Name") + name: str = SettingsField( + "", + title="Name", + description="Output name (no space)", + regex=r"[a-zA-Z0-9_]([a-zA-Z0-9_\.\-]*[a-zA-Z0-9_])?$", + ) extension: str = SettingsField("", title="Extension") transcoding_type: str = SettingsField( "colorspace", diff --git a/server_addon/maya/server/settings/publishers.py b/server_addon/maya/server/settings/publishers.py index 510a3d3af9..27288053a2 100644 --- a/server_addon/maya/server/settings/publishers.py +++ b/server_addon/maya/server/settings/publishers.py @@ -299,6 +299,16 @@ class ExtractAlembicModel(BaseSettingsModel): families: list[str] = SettingsField( default_factory=list, title="Families") + bake_attributes: list[str] = SettingsField( + default_factory=list, title="Bake Attributes", + description="List of attributes that will be included in the alembic " + "export.", + ) + bake_attribute_prefixes: list[str] = SettingsField( + default_factory=list, title="Bake Attribute Prefixes", + description="List of attribute prefixes for attributes that will be " + "included in the alembic export.", + ) class ExtractObjModel(BaseSettingsModel): @@ -306,6 +316,12 @@ class ExtractObjModel(BaseSettingsModel): optional: bool = SettingsField(title="Optional") +class ExtractModelModel(BaseSettingsModel): + enabled: bool = SettingsField(title="Enabled") + optional: bool = SettingsField(title="Optional") + active: bool = SettingsField(title="Active") + + class ExtractMayaSceneRawModel(BaseSettingsModel): """Add loaded instances to those published families:""" enabled: bool = SettingsField(title="ExtractMayaSceneRaw") @@ -362,7 +378,9 @@ class ExtractLookModel(BaseSettingsModel): class ExtractGPUCacheModel(BaseSettingsModel): - enabled: bool = True + enabled: bool = SettingsField(title="Enabled") + optional: bool = SettingsField(title="Optional") + active: bool = SettingsField(title="Active") families: list[str] = SettingsField(default_factory=list, title="Families") step: float = SettingsField(1.0, ge=1.0, title="Step") stepSave: int = SettingsField(1, ge=1, title="Step Save") @@ -789,6 +807,10 @@ class PublishersModel(BaseSettingsModel): default_factory=ExtractGPUCacheModel, title="Extract GPU Cache", ) + ExtractModel: ExtractModelModel = SettingsField( + default_factory=ExtractModelModel, + title="Extract Model (Maya Scene)" + ) DEFAULT_SUFFIX_NAMING = { @@ -1184,7 +1206,9 @@ DEFAULT_PUBLISH_SETTINGS = { "pointcache", "model", "vrayproxy.alembic" - ] + ], + "bake_attributes": [], + "bake_attribute_prefixes": [] }, "ExtractObj": { "enabled": False, @@ -1329,6 +1353,8 @@ DEFAULT_PUBLISH_SETTINGS = { }, "ExtractGPUCache": { "enabled": False, + "optional": False, + "active": True, "families": [ "model", "animation", @@ -1341,5 +1367,10 @@ DEFAULT_PUBLISH_SETTINGS = { "optimizeAnimationsForMotionBlur": True, "writeMaterials": True, "useBaseTessellation": True + }, + "ExtractModel": { + "enabled": True, + "optional": True, + "active": True, } } diff --git a/server_addon/maya/server/version.py b/server_addon/maya/server/version.py index 0b44700d27..1a4f79a972 100644 --- a/server_addon/maya/server/version.py +++ b/server_addon/maya/server/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring addon version.""" -__version__ = "0.1.11" +__version__ = "0.1.13" diff --git a/server_addon/traypublisher/server/settings/simple_creators.py b/server_addon/traypublisher/server/settings/simple_creators.py index 924eeedd23..6b979bbe52 100644 --- a/server_addon/traypublisher/server/settings/simple_creators.py +++ b/server_addon/traypublisher/server/settings/simple_creators.py @@ -142,6 +142,7 @@ DEFAULT_SIMPLE_CREATORS = [ "extensions": [ ".exr", ".png", + ".dng", ".dpx", ".jpg", ".tiff", @@ -165,6 +166,7 @@ DEFAULT_SIMPLE_CREATORS = [ "extensions": [ ".exr", ".png", + ".dng", ".dpx", ".jpg", ".jpeg", @@ -215,6 +217,7 @@ DEFAULT_SIMPLE_CREATORS = [ ".exr", ".jpg", ".jpeg", + ".dng", ".dpx", ".bmp", ".tif", diff --git a/server_addon/traypublisher/server/version.py b/server_addon/traypublisher/server/version.py index e57ad00718..de699158fd 100644 --- a/server_addon/traypublisher/server/version.py +++ b/server_addon/traypublisher/server/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring addon version.""" -__version__ = "0.1.3" +__version__ = "0.1.4"