diff --git a/openpype/hosts/max/plugins/publish/extract_pointcache.py b/openpype/hosts/max/plugins/publish/extract_pointcache.py index 6d1e8d03b4..5a99a8b845 100644 --- a/openpype/hosts/max/plugins/publish/extract_pointcache.py +++ b/openpype/hosts/max/plugins/publish/extract_pointcache.py @@ -56,7 +56,7 @@ class ExtractAlembic(publish.Extractor): container = instance.data["instance_node"] - self.log.info("Extracting pointcache ...") + self.log.debug("Extracting pointcache ...") parent_dir = self.staging_dir(instance) file_name = "{name}.abc".format(**instance.data) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index fca4410ede..ef8ddf8bac 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -32,13 +32,11 @@ from openpype.pipeline import ( load_container, registered_host, ) -from openpype.pipeline.create import ( - legacy_create, - get_legacy_creator_by_name, -) +from openpype.lib import NumberDef +from openpype.pipeline.context_tools import get_current_project_asset +from openpype.pipeline.create import CreateContext from openpype.pipeline.context_tools import ( get_current_asset_name, - get_current_project_asset, get_current_project_name, get_current_task_name ) @@ -122,16 +120,14 @@ FLOAT_FPS = {23.98, 23.976, 29.97, 47.952, 59.94} RENDERLIKE_INSTANCE_FAMILIES = ["rendering", "vrayscene"] -DISPLAY_LIGHTS_VALUES = [ - "project_settings", "default", "all", "selected", "flat", "none" -] -DISPLAY_LIGHTS_LABELS = [ - "Use Project Settings", - "Default Lighting", - "All Lights", - "Selected Lights", - "Flat Lighting", - "No Lights" + +DISPLAY_LIGHTS_ENUM = [ + {"label": "Use Project Settings", "value": "project_settings"}, + {"label": "Default Lighting", "value": "default"}, + {"label": "All Lights", "value": "all"}, + {"label": "Selected Lights", "value": "selected"}, + {"label": "Flat Lighting", "value": "flat"}, + {"label": "No Lights", "value": "none"} ] @@ -343,8 +339,8 @@ def pairwise(iterable): return zip(a, a) -def collect_animation_data(fps=False): - """Get the basic animation data +def collect_animation_defs(fps=False): + """Get the basic animation attribute defintions for the publisher. Returns: OrderedDict @@ -363,17 +359,42 @@ def collect_animation_data(fps=False): handle_end = frame_end_handle - frame_end # build attributes - data = OrderedDict() - data["frameStart"] = frame_start - data["frameEnd"] = frame_end - data["handleStart"] = handle_start - data["handleEnd"] = handle_end - data["step"] = 1.0 + defs = [ + NumberDef("frameStart", + label="Frame Start", + default=frame_start, + decimals=0), + NumberDef("frameEnd", + label="Frame End", + default=frame_end, + decimals=0), + NumberDef("handleStart", + label="Handle Start", + default=handle_start, + decimals=0), + NumberDef("handleEnd", + label="Handle End", + default=handle_end, + decimals=0), + NumberDef("step", + label="Step size", + tooltip="A smaller step size means more samples and larger " + "output files.\n" + "A 1.0 step size is a single sample every frame.\n" + "A 0.5 step size is two samples per frame.\n" + "A 0.2 step size is five samples per frame.", + default=1.0, + decimals=3), + ] if fps: - data["fps"] = mel.eval('currentTimeUnitToFPS()') + current_fps = mel.eval('currentTimeUnitToFPS()') + fps_def = NumberDef( + "fps", label="FPS", default=current_fps, decimals=5 + ) + defs.append(fps_def) - return data + return defs def imprint(node, data): @@ -459,10 +480,10 @@ def lsattrs(attrs): attrs (dict): Name and value pairs of expected matches Example: - >> # Return nodes with an `age` of five. - >> lsattr({"age": "five"}) - >> # Return nodes with both `age` and `color` of five and blue. - >> lsattr({"age": "five", "color": "blue"}) + >>> # Return nodes with an `age` of five. + >>> lsattrs({"age": "five"}) + >>> # Return nodes with both `age` and `color` of five and blue. + >>> lsattrs({"age": "five", "color": "blue"}) Return: list: matching nodes. @@ -4086,12 +4107,10 @@ def create_rig_animation_instance( ) assert roots, "No root nodes in rig, this is a bug." - asset = legacy_io.Session["AVALON_ASSET"] - dependency = str(context["representation"]["_id"]) - custom_subset = options.get("animationSubsetName") if custom_subset: formatting_data = { + # TODO remove 'asset_type' and replace 'asset_name' with 'asset' "asset_name": context['asset']['name'], "asset_type": context['asset']['type'], "subset": context['subset']['name'], @@ -4109,14 +4128,17 @@ def create_rig_animation_instance( if log: log.info("Creating subset: {}".format(namespace)) + # Fill creator identifier + creator_identifier = "io.openpype.creators.maya.animation" + + host = registered_host() + create_context = CreateContext(host) + # Create the animation instance - creator_plugin = get_legacy_creator_by_name("CreateAnimation") with maintained_selection(): cmds.select([output, controls] + roots, noExpand=True) - legacy_create( - creator_plugin, - name=namespace, - asset=asset, - options={"useSelection": True}, - data={"dependencies": dependency} + create_context.create( + creator_identifier=creator_identifier, + variant=namespace, + pre_create_data={"use_selection": True} ) diff --git a/openpype/hosts/maya/api/lib_renderproducts.py b/openpype/hosts/maya/api/lib_renderproducts.py index 7bfb53d500..b5b71a5a36 100644 --- a/openpype/hosts/maya/api/lib_renderproducts.py +++ b/openpype/hosts/maya/api/lib_renderproducts.py @@ -177,7 +177,7 @@ def get(layer, render_instance=None): }.get(renderer_name.lower(), None) if renderer is None: raise UnsupportedRendererException( - "unsupported {}".format(renderer_name) + "Unsupported renderer: {}".format(renderer_name) ) return renderer(layer, render_instance) diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index 5284c0249d..645d6f5a1c 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -66,10 +66,12 @@ def install(): cmds.menuItem(divider=True) - # Create default items cmds.menuItem( "Create...", - command=lambda *args: host_tools.show_creator(parent=parent_widget) + command=lambda *args: host_tools.show_publisher( + parent=parent_widget, + tab="create" + ) ) cmds.menuItem( @@ -82,8 +84,9 @@ def install(): cmds.menuItem( "Publish...", - command=lambda *args: host_tools.show_publish( - parent=parent_widget + command=lambda *args: host_tools.show_publisher( + parent=parent_widget, + tab="publish" ), image=pyblish_icon ) diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py index e2d00b5bd7..2f2ab83f79 100644 --- a/openpype/hosts/maya/api/pipeline.py +++ b/openpype/hosts/maya/api/pipeline.py @@ -1,3 +1,5 @@ +import json +import base64 import os import errno import logging @@ -14,6 +16,7 @@ from openpype.host import ( HostBase, IWorkfileHost, ILoadHost, + IPublishHost, HostDirmap, ) from openpype.tools.utils import host_tools @@ -64,7 +67,7 @@ INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") AVALON_CONTAINERS = ":AVALON_CONTAINERS" -class MayaHost(HostBase, IWorkfileHost, ILoadHost): +class MayaHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): name = "maya" def __init__(self): @@ -150,6 +153,20 @@ class MayaHost(HostBase, IWorkfileHost, ILoadHost): with lib.maintained_selection(): yield + def get_context_data(self): + data = cmds.fileInfo("OpenPypeContext", query=True) + if not data: + return {} + + data = data[0] # Maya seems to return a list + decoded = base64.b64decode(data).decode("utf-8") + return json.loads(decoded) + + def update_context_data(self, data, changes): + json_str = json.dumps(data) + encoded = base64.b64encode(json_str.encode("utf-8")) + return cmds.fileInfo("OpenPypeContext", encoded) + def _register_callbacks(self): for handler, event in self._op_events.copy().items(): if event is None: diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py index 0971251469..9a67147eb4 100644 --- a/openpype/hosts/maya/api/plugin.py +++ b/openpype/hosts/maya/api/plugin.py @@ -1,29 +1,39 @@ +import json import os - -from maya import cmds +from abc import ABCMeta import qargparse +import six +from maya import cmds +from maya.app.renderSetup.model import renderSetup -from openpype.lib import Logger +from openpype.lib import BoolDef, Logger +from openpype.pipeline import AVALON_CONTAINER_ID, Anatomy, CreatedInstance +from openpype.pipeline import Creator as NewCreator from openpype.pipeline import ( - LegacyCreator, - LoaderPlugin, - get_representation_path, - AVALON_CONTAINER_ID, - Anatomy, -) + CreatorError, LegacyCreator, LoaderPlugin, get_representation_path, + legacy_io) from openpype.pipeline.load import LoadError from openpype.settings import get_project_settings -from .pipeline import containerise -from . import lib +from . import lib +from .lib import imprint, read +from .pipeline import containerise log = Logger.get_logger() +def _get_attr(node, attr, default=None): + """Helper to get attribute which allows attribute to not exist.""" + if not cmds.attributeQuery(attr, node=node, exists=True): + return default + return cmds.getAttr("{}.{}".format(node, attr)) + + # Backwards compatibility: these functions has been moved to lib. def get_reference_node(*args, **kwargs): - """ + """Get the reference node from the container members + Deprecated: This function was moved and will be removed in 3.16.x. """ @@ -60,6 +70,379 @@ class Creator(LegacyCreator): return instance +@six.add_metaclass(ABCMeta) +class MayaCreatorBase(object): + + @staticmethod + def cache_subsets(shared_data): + """Cache instances for Creators to shared data. + + Create `maya_cached_subsets` key when needed in shared data and + fill it with all collected instances from the scene under its + respective creator identifiers. + + If legacy instances are detected in the scene, create + `maya_cached_legacy_subsets` there and fill it with + all legacy subsets under family as a key. + + Args: + Dict[str, Any]: Shared data. + + Return: + Dict[str, Any]: Shared data dictionary. + + """ + if shared_data.get("maya_cached_subsets") is None: + cache = dict() + cache_legacy = dict() + + for node in cmds.ls(type="objectSet"): + + if _get_attr(node, attr="id") != "pyblish.avalon.instance": + continue + + creator_id = _get_attr(node, attr="creator_identifier") + if creator_id is not None: + # creator instance + cache.setdefault(creator_id, []).append(node) + else: + # legacy instance + family = _get_attr(node, attr="family") + if family is None: + # must be a broken instance + continue + + cache_legacy.setdefault(family, []).append(node) + + shared_data["maya_cached_subsets"] = cache + shared_data["maya_cached_legacy_subsets"] = cache_legacy + return shared_data + + def imprint_instance_node(self, node, data): + + # We never store the instance_node as value on the node since + # it's the node name itself + data.pop("instance_node", None) + + # We store creator attributes at the root level and assume they + # will not clash in names with `subset`, `task`, etc. and other + # default names. This is just so these attributes in many cases + # are still editable in the maya UI by artists. + # pop to move to end of dict to sort attributes last on the node + creator_attributes = data.pop("creator_attributes", {}) + data.update(creator_attributes) + + # We know the "publish_attributes" will be complex data of + # settings per plugins, we'll store this as a flattened json structure + # pop to move to end of dict to sort attributes last on the node + data["publish_attributes"] = json.dumps( + data.pop("publish_attributes", {}) + ) + + # Since we flattened the data structure for creator attributes we want + # to correctly detect which flattened attributes should end back in the + # creator attributes when reading the data from the node, so we store + # the relevant keys as a string + data["__creator_attributes_keys"] = ",".join(creator_attributes.keys()) + + # Kill any existing attributes just so we can imprint cleanly again + for attr in data.keys(): + if cmds.attributeQuery(attr, node=node, exists=True): + cmds.deleteAttr("{}.{}".format(node, attr)) + + return imprint(node, data) + + def read_instance_node(self, node): + node_data = read(node) + + # Never care about a cbId attribute on the object set + # being read as 'data' + node_data.pop("cbId", None) + + # Move the relevant attributes into "creator_attributes" that + # we flattened originally + node_data["creator_attributes"] = {} + creator_attribute_keys = node_data.pop("__creator_attributes_keys", + "").split(",") + for key in creator_attribute_keys: + if key in node_data: + node_data["creator_attributes"][key] = node_data.pop(key) + + publish_attributes = node_data.get("publish_attributes") + if publish_attributes: + node_data["publish_attributes"] = json.loads(publish_attributes) + + # Explicitly re-parse the node name + node_data["instance_node"] = node + + return node_data + + +@six.add_metaclass(ABCMeta) +class MayaCreator(NewCreator, MayaCreatorBase): + + def create(self, subset_name, instance_data, pre_create_data): + + members = list() + if pre_create_data.get("use_selection"): + members = cmds.ls(selection=True) + + with lib.undo_chunk(): + instance_node = cmds.sets(members, name=subset_name) + instance_data["instance_node"] = instance_node + instance = CreatedInstance( + self.family, + subset_name, + instance_data, + self) + self._add_instance_to_context(instance) + + self.imprint_instance_node(instance_node, + data=instance.data_to_store()) + return instance + + def collect_instances(self): + self.cache_subsets(self.collection_shared_data) + cached_subsets = self.collection_shared_data["maya_cached_subsets"] + for node in cached_subsets.get(self.identifier, []): + node_data = self.read_instance_node(node) + + created_instance = CreatedInstance.from_existing(node_data, self) + self._add_instance_to_context(created_instance) + + def update_instances(self, update_list): + for created_inst, _changes in update_list: + data = created_inst.data_to_store() + node = data.get("instance_node") + + self.imprint_instance_node(node, data) + + def remove_instances(self, instances): + """Remove specified instance from the scene. + + This is only removing `id` parameter so instance is no longer + instance, because it might contain valuable data for artist. + + """ + for instance in instances: + node = instance.data.get("instance_node") + if node: + cmds.delete(node) + + self._remove_instance_from_context(instance) + + def get_pre_create_attr_defs(self): + return [ + BoolDef("use_selection", + label="Use selection", + default=True) + ] + + +def ensure_namespace(namespace): + """Make sure the namespace exists. + + Args: + namespace (str): The preferred namespace name. + + Returns: + str: The generated or existing namespace + + """ + exists = cmds.namespace(exists=namespace) + if exists: + return namespace + else: + return cmds.namespace(add=namespace) + + +class RenderlayerCreator(NewCreator, MayaCreatorBase): + """Creator which creates an instance per renderlayer in the workfile. + + Create and manages renderlayer subset per renderLayer in workfile. + This generates a singleton node in the scene which, if it exists, tells the + Creator to collect Maya rendersetup renderlayers as individual instances. + As such, triggering create doesn't actually create the instance node per + layer but only the node which tells the Creator it may now collect + an instance per renderlayer. + + """ + + # These are required to be overridden in subclass + singleton_node_name = "" + + # These are optional to be overridden in subclass + layer_instance_prefix = None + + def _get_singleton_node(self, return_all=False): + nodes = lib.lsattr("pre_creator_identifier", self.identifier) + if nodes: + return nodes if return_all else nodes[0] + + def create(self, subset_name, instance_data, pre_create_data): + # A Renderlayer is never explicitly created using the create method. + # Instead, renderlayers from the scene are collected. Thus "create" + # would only ever be called to say, 'hey, please refresh collect' + self.create_singleton_node() + + # if no render layers are present, create default one with + # asterisk selector + rs = renderSetup.instance() + if not rs.getRenderLayers(): + render_layer = rs.createRenderLayer("Main") + collection = render_layer.createCollection("defaultCollection") + collection.getSelector().setPattern('*') + + # By RenderLayerCreator.create we make it so that the renderlayer + # instances directly appear even though it just collects scene + # renderlayers. This doesn't actually 'create' any scene contents. + self.collect_instances() + + def create_singleton_node(self): + if self._get_singleton_node(): + raise CreatorError("A Render instance already exists - only " + "one can be configured.") + + with lib.undo_chunk(): + node = cmds.sets(empty=True, name=self.singleton_node_name) + lib.imprint(node, data={ + "pre_creator_identifier": self.identifier + }) + + return node + + def collect_instances(self): + + # We only collect if the global render instance exists + if not self._get_singleton_node(): + return + + rs = renderSetup.instance() + layers = rs.getRenderLayers() + for layer in layers: + layer_instance_node = self.find_layer_instance_node(layer) + if layer_instance_node: + data = self.read_instance_node(layer_instance_node) + instance = CreatedInstance.from_existing(data, creator=self) + else: + # No existing scene instance node for this layer. Note that + # this instance will not have the `instance_node` data yet + # until it's been saved/persisted at least once. + # TODO: Correctly define the subset name using templates + prefix = self.layer_instance_prefix or self.family + subset_name = "{}{}".format(prefix, layer.name()) + instance_data = { + "asset": legacy_io.Session["AVALON_ASSET"], + "task": legacy_io.Session["AVALON_TASK"], + "variant": layer.name(), + } + instance = CreatedInstance( + family=self.family, + subset_name=subset_name, + data=instance_data, + creator=self + ) + + instance.transient_data["layer"] = layer + self._add_instance_to_context(instance) + + def find_layer_instance_node(self, layer): + connected_sets = cmds.listConnections( + "{}.message".format(layer.name()), + source=False, + destination=True, + type="objectSet" + ) or [] + + for node in connected_sets: + if not cmds.attributeQuery("creator_identifier", + node=node, + exists=True): + continue + + creator_identifier = cmds.getAttr(node + ".creator_identifier") + if creator_identifier == self.identifier: + self.log.info(f"Found node: {node}") + return node + + def _create_layer_instance_node(self, layer): + + # We only collect if a CreateRender instance exists + create_render_set = self._get_singleton_node() + if not create_render_set: + raise CreatorError("Creating a renderlayer instance node is not " + "allowed if no 'CreateRender' instance exists") + + namespace = "_{}".format(self.singleton_node_name) + namespace = ensure_namespace(namespace) + + name = "{}:{}".format(namespace, layer.name()) + render_set = cmds.sets(name=name, empty=True) + + # Keep an active link with the renderlayer so we can retrieve it + # later by a physical maya connection instead of relying on the layer + # name + cmds.addAttr(render_set, longName="renderlayer", at="message") + cmds.connectAttr("{}.message".format(layer.name()), + "{}.renderlayer".format(render_set), force=True) + + # Add the set to the 'CreateRender' set. + cmds.sets(render_set, forceElement=create_render_set) + + return render_set + + def update_instances(self, update_list): + # We only generate the persisting layer data into the scene once + # we save with the UI on e.g. validate or publish + for instance, _changes in update_list: + instance_node = instance.data.get("instance_node") + + # Ensure a node exists to persist the data to + if not instance_node: + layer = instance.transient_data["layer"] + instance_node = self._create_layer_instance_node(layer) + instance.data["instance_node"] = instance_node + + self.imprint_instance_node(instance_node, + data=instance.data_to_store()) + + def imprint_instance_node(self, node, data): + # Do not ever try to update the `renderlayer` since it'll try + # to remove the attribute and recreate it but fail to keep it a + # message attribute link. We only ever imprint that on the initial + # node creation. + # TODO: Improve how this is handled + data.pop("renderlayer", None) + data.get("creator_attributes", {}).pop("renderlayer", None) + + return super(RenderlayerCreator, self).imprint_instance_node(node, + data=data) + + def remove_instances(self, instances): + """Remove specified instances from the scene. + + This is only removing `id` parameter so instance is no longer + instance, because it might contain valuable data for artist. + + """ + # Instead of removing the single instance or renderlayers we instead + # remove the CreateRender node this creator relies on to decide whether + # it should collect anything at all. + nodes = self._get_singleton_node(return_all=True) + if nodes: + cmds.delete(nodes) + + # Remove ALL the instances even if only one gets deleted + for instance in list(self.create_context.instances): + if instance.get("creator_identifier") == self.identifier: + self._remove_instance_from_context(instance) + + # Remove the stored settings per renderlayer too + node = instance.data.get("instance_node") + if node and cmds.objExists(node): + cmds.delete(node) + + class Loader(LoaderPlugin): hosts = ["maya"] @@ -186,6 +569,7 @@ class ReferenceLoader(Loader): def update(self, container, representation): from maya import cmds + from openpype.hosts.maya.api.lib import get_container_members node = container["objectName"] diff --git a/openpype/hosts/maya/plugins/create/convert_legacy.py b/openpype/hosts/maya/plugins/create/convert_legacy.py new file mode 100644 index 0000000000..6133abc205 --- /dev/null +++ b/openpype/hosts/maya/plugins/create/convert_legacy.py @@ -0,0 +1,165 @@ +from openpype.pipeline.create.creator_plugins import SubsetConvertorPlugin +from openpype.hosts.maya.api import plugin +from openpype.hosts.maya.api.lib import read + +from maya import cmds +from maya.app.renderSetup.model import renderSetup + + +class MayaLegacyConvertor(SubsetConvertorPlugin, + plugin.MayaCreatorBase): + """Find and convert any legacy subsets in the scene. + + This Convertor will find all legacy subsets in the scene and will + transform them to the current system. Since the old subsets doesn't + retain any information about their original creators, the only mapping + we can do is based on their families. + + Its limitation is that you can have multiple creators creating subset + of the same family and there is no way to handle it. This code should + nevertheless cover all creators that came with OpenPype. + + """ + identifier = "io.openpype.creators.maya.legacy" + + # Cases where the identifier or new family doesn't correspond to the + # original family on the legacy instances + special_family_conversions = { + "rendering": "io.openpype.creators.maya.renderlayer", + } + + def find_instances(self): + + self.cache_subsets(self.collection_shared_data) + legacy = self.collection_shared_data.get("maya_cached_legacy_subsets") + if not legacy: + return + + self.add_convertor_item("Convert legacy instances") + + def convert(self): + self.remove_convertor_item() + + # We can't use the collected shared data cache here + # we re-query it here directly to convert all found. + cache = {} + self.cache_subsets(cache) + legacy = cache.get("maya_cached_legacy_subsets") + if not legacy: + return + + # From all current new style manual creators find the mapping + # from family to identifier + family_to_id = {} + for identifier, creator in self.create_context.manual_creators.items(): + family = getattr(creator, "family", None) + if not family: + continue + + if family in family_to_id: + # We have a clash of family -> identifier. Multiple + # new style creators use the same family + self.log.warning("Clash on family->identifier: " + "{}".format(identifier)) + family_to_id[family] = identifier + + family_to_id.update(self.special_family_conversions) + + # We also embed the current 'task' into the instance since legacy + # instances didn't store that data on the instances. The old style + # logic was thus to be live to the current task to begin with. + data = dict() + data["task"] = self.create_context.get_current_task_name() + + for family, instance_nodes in legacy.items(): + if family not in family_to_id: + self.log.warning( + "Unable to convert legacy instance with family '{}'" + " because there is no matching new creator's family" + "".format(family) + ) + continue + + creator_id = family_to_id[family] + creator = self.create_context.manual_creators[creator_id] + data["creator_identifier"] = creator_id + + if isinstance(creator, plugin.RenderlayerCreator): + self._convert_per_renderlayer(instance_nodes, data, creator) + else: + self._convert_regular(instance_nodes, data) + + def _convert_regular(self, instance_nodes, data): + # We only imprint the creator identifier for it to identify + # as the new style creator + for instance_node in instance_nodes: + self.imprint_instance_node(instance_node, + data=data.copy()) + + def _convert_per_renderlayer(self, instance_nodes, data, creator): + # Split the instance into an instance per layer + rs = renderSetup.instance() + layers = rs.getRenderLayers() + if not layers: + self.log.error( + "Can't convert legacy renderlayer instance because no existing" + " renderSetup layers exist in the scene." + ) + return + + creator_attribute_names = { + attr_def.key for attr_def in creator.get_instance_attr_defs() + } + + for instance_node in instance_nodes: + + # Ensure we have the new style singleton node generated + # TODO: Make function public + singleton_node = creator._get_singleton_node() + if singleton_node: + self.log.error( + "Can't convert legacy renderlayer instance '{}' because" + " new style instance '{}' already exists".format( + instance_node, + singleton_node + ) + ) + continue + + creator.create_singleton_node() + + # We are creating new nodes to replace the original instance + # Copy the attributes of the original instance to the new node + original_data = read(instance_node) + + # The family gets converted to the new family (this is due to + # "rendering" family being converted to "renderlayer" family) + original_data["family"] = creator.family + + # Convert to creator attributes when relevant + creator_attributes = {} + for key in list(original_data.keys()): + # Iterate in order of the original attributes to preserve order + # in the output creator attributes + if key in creator_attribute_names: + creator_attributes[key] = original_data.pop(key) + original_data["creator_attributes"] = creator_attributes + + # For layer in maya layers + for layer in layers: + layer_instance_node = creator.find_layer_instance_node(layer) + if not layer_instance_node: + # TODO: Make function public + layer_instance_node = creator._create_layer_instance_node( + layer + ) + + # Transfer the main attributes of the original instance + layer_data = original_data.copy() + layer_data.update(data) + + self.imprint_instance_node(layer_instance_node, + data=layer_data) + + # Delete the legacy instance node + cmds.delete(instance_node) diff --git a/openpype/hosts/maya/plugins/create/create_animation.py b/openpype/hosts/maya/plugins/create/create_animation.py index 095cbcdd64..cade8603ce 100644 --- a/openpype/hosts/maya/plugins/create/create_animation.py +++ b/openpype/hosts/maya/plugins/create/create_animation.py @@ -2,9 +2,13 @@ from openpype.hosts.maya.api import ( lib, plugin ) +from openpype.lib import ( + BoolDef, + TextDef +) -class CreateAnimation(plugin.Creator): +class CreateAnimation(plugin.MayaCreator): """Animation output for character rigs""" # We hide the animation creator from the UI since the creation of it @@ -13,48 +17,71 @@ class CreateAnimation(plugin.Creator): # Note: This setting is actually applied from project settings enabled = False + identifier = "io.openpype.creators.maya.animation" name = "animationDefault" label = "Animation" family = "animation" icon = "male" + write_color_sets = False write_face_sets = False include_parent_hierarchy = False include_user_defined_attributes = False - def __init__(self, *args, **kwargs): - super(CreateAnimation, self).__init__(*args, **kwargs) + # TODO: Would be great if we could visually hide this from the creator + # by default but do allow to generate it through code. - # create an ordered dict with the existing data first + def get_instance_attr_defs(self): - # get basic animation data : start / end / handles / steps - for key, value in lib.collect_animation_data().items(): - self.data[key] = value + defs = lib.collect_animation_defs() - # Write vertex colors with the geometry. - self.data["writeColorSets"] = self.write_color_sets - self.data["writeFaceSets"] = self.write_face_sets - - # Include only renderable visible shapes. - # Skips locators and empty transforms - self.data["renderableOnly"] = False - - # Include only nodes that are visible at least once during the - # frame range. - self.data["visibleOnly"] = False - - # Include the groups above the out_SET content - self.data["includeParentHierarchy"] = self.include_parent_hierarchy - - # Default to exporting world-space - self.data["worldSpace"] = True + defs.extend([ + BoolDef("writeColorSets", + label="Write vertex colors", + tooltip="Write vertex colors with the geometry", + default=self.write_color_sets), + BoolDef("writeFaceSets", + label="Write face sets", + tooltip="Write face sets with the geometry", + default=self.write_face_sets), + BoolDef("writeNormals", + label="Write normals", + tooltip="Write normals with the deforming geometry", + default=True), + BoolDef("renderableOnly", + label="Renderable Only", + tooltip="Only export renderable visible shapes", + default=False), + BoolDef("visibleOnly", + label="Visible Only", + tooltip="Only export dag objects visible during " + "frame range", + default=False), + BoolDef("includeParentHierarchy", + label="Include Parent Hierarchy", + tooltip="Whether to include parent hierarchy of nodes in " + "the publish instance", + default=self.include_parent_hierarchy), + BoolDef("worldSpace", + label="World-Space Export", + default=True), + BoolDef("includeUserDefinedAttributes", + label="Include User Defined Attributes", + default=self.include_user_defined_attributes), + TextDef("attr", + label="Custom Attributes", + default="", + placeholder="attr1, attr2"), + TextDef("attrPrefix", + label="Custom Attributes Prefix", + placeholder="prefix1, prefix2") + ]) + # TODO: Implement these on a Deadline plug-in instead? + """ # Default to not send to farm. self.data["farm"] = False self.data["priority"] = 50 + """ - # Default to write normals. - self.data["writeNormals"] = True - - value = self.include_user_defined_attributes - self.data["includeUserDefinedAttributes"] = value + return defs diff --git a/openpype/hosts/maya/plugins/create/create_arnold_scene_source.py b/openpype/hosts/maya/plugins/create/create_arnold_scene_source.py index 2afb897e94..0c8cf8d2bb 100644 --- a/openpype/hosts/maya/plugins/create/create_arnold_scene_source.py +++ b/openpype/hosts/maya/plugins/create/create_arnold_scene_source.py @@ -2,17 +2,20 @@ from openpype.hosts.maya.api import ( lib, plugin ) - -from maya import cmds +from openpype.lib import ( + NumberDef, + BoolDef +) -class CreateArnoldSceneSource(plugin.Creator): +class CreateArnoldSceneSource(plugin.MayaCreator): """Arnold Scene Source""" - name = "ass" + identifier = "io.openpype.creators.maya.ass" label = "Arnold Scene Source" family = "ass" icon = "cube" + expandProcedurals = False motionBlur = True motionBlurKeys = 2 @@ -28,39 +31,71 @@ class CreateArnoldSceneSource(plugin.Creator): maskColor_manager = False maskOperator = False - def __init__(self, *args, **kwargs): - super(CreateArnoldSceneSource, self).__init__(*args, **kwargs) + def get_instance_attr_defs(self): - # Add animation data - self.data.update(lib.collect_animation_data()) + defs = lib.collect_animation_defs() - self.data["expandProcedurals"] = self.expandProcedurals - self.data["motionBlur"] = self.motionBlur - self.data["motionBlurKeys"] = self.motionBlurKeys - self.data["motionBlurLength"] = self.motionBlurLength + defs.extend([ + BoolDef("expandProcedural", + label="Expand Procedural", + default=self.expandProcedurals), + BoolDef("motionBlur", + label="Motion Blur", + default=self.motionBlur), + NumberDef("motionBlurKeys", + label="Motion Blur Keys", + decimals=0, + default=self.motionBlurKeys), + NumberDef("motionBlurLength", + label="Motion Blur Length", + decimals=3, + default=self.motionBlurLength), - # Masks - self.data["maskOptions"] = self.maskOptions - self.data["maskCamera"] = self.maskCamera - self.data["maskLight"] = self.maskLight - self.data["maskShape"] = self.maskShape - self.data["maskShader"] = self.maskShader - self.data["maskOverride"] = self.maskOverride - self.data["maskDriver"] = self.maskDriver - self.data["maskFilter"] = self.maskFilter - self.data["maskColor_manager"] = self.maskColor_manager - self.data["maskOperator"] = self.maskOperator + # Masks + BoolDef("maskOptions", + label="Export Options", + default=self.maskOptions), + BoolDef("maskCamera", + label="Export Cameras", + default=self.maskCamera), + BoolDef("maskLight", + label="Export Lights", + default=self.maskLight), + BoolDef("maskShape", + label="Export Shapes", + default=self.maskShape), + BoolDef("maskShader", + label="Export Shaders", + default=self.maskShader), + BoolDef("maskOverride", + label="Export Override Nodes", + default=self.maskOverride), + BoolDef("maskDriver", + label="Export Drivers", + default=self.maskDriver), + BoolDef("maskFilter", + label="Export Filters", + default=self.maskFilter), + BoolDef("maskOperator", + label="Export Operators", + default=self.maskOperator), + BoolDef("maskColor_manager", + label="Export Color Managers", + default=self.maskColor_manager), + ]) - def process(self): - instance = super(CreateArnoldSceneSource, self).process() + return defs - nodes = [] + def create(self, subset_name, instance_data, pre_create_data): - if (self.options or {}).get("useSelection"): - nodes = cmds.ls(selection=True) + from maya import cmds - cmds.sets(nodes, rm=instance) + instance = super(CreateArnoldSceneSource, self).create( + subset_name, instance_data, pre_create_data + ) - assContent = cmds.sets(name=instance + "_content_SET") - assProxy = cmds.sets(name=instance + "_proxy_SET", empty=True) - cmds.sets([assContent, assProxy], forceElement=instance) + instance_node = instance.get("instance_node") + + content = cmds.sets(name=instance_node + "_content_SET", empty=True) + proxy = cmds.sets(name=instance_node + "_proxy_SET", empty=True) + cmds.sets([content, proxy], forceElement=instance_node) diff --git a/openpype/hosts/maya/plugins/create/create_assembly.py b/openpype/hosts/maya/plugins/create/create_assembly.py index ff5e1d45c4..813fe4da04 100644 --- a/openpype/hosts/maya/plugins/create/create_assembly.py +++ b/openpype/hosts/maya/plugins/create/create_assembly.py @@ -1,10 +1,10 @@ from openpype.hosts.maya.api import plugin -class CreateAssembly(plugin.Creator): +class CreateAssembly(plugin.MayaCreator): """A grouped package of loaded content""" - name = "assembly" + identifier = "io.openpype.creators.maya.assembly" label = "Assembly" family = "assembly" icon = "cubes" diff --git a/openpype/hosts/maya/plugins/create/create_camera.py b/openpype/hosts/maya/plugins/create/create_camera.py index 8b2c881036..0219f56330 100644 --- a/openpype/hosts/maya/plugins/create/create_camera.py +++ b/openpype/hosts/maya/plugins/create/create_camera.py @@ -2,33 +2,35 @@ from openpype.hosts.maya.api import ( lib, plugin ) +from openpype.lib import BoolDef -class CreateCamera(plugin.Creator): +class CreateCamera(plugin.MayaCreator): """Single baked camera""" - name = "cameraMain" + identifier = "io.openpype.creators.maya.camera" label = "Camera" family = "camera" icon = "video-camera" - def __init__(self, *args, **kwargs): - super(CreateCamera, self).__init__(*args, **kwargs) + def get_instance_attr_defs(self): - # get basic animation data : start / end / handles / steps - animation_data = lib.collect_animation_data() - for key, value in animation_data.items(): - self.data[key] = value + defs = lib.collect_animation_defs() - # Bake to world space by default, when this is False it will also - # include the parent hierarchy in the baked results - self.data['bakeToWorldSpace'] = True + defs.extend([ + BoolDef("bakeToWorldSpace", + label="Bake to World-Space", + tooltip="Bake to World-Space", + default=True), + ]) + + return defs -class CreateCameraRig(plugin.Creator): +class CreateCameraRig(plugin.MayaCreator): """Complex hierarchy with camera.""" - name = "camerarigMain" + identifier = "io.openpype.creators.maya.camerarig" label = "Camera Rig" family = "camerarig" icon = "video-camera" diff --git a/openpype/hosts/maya/plugins/create/create_layout.py b/openpype/hosts/maya/plugins/create/create_layout.py index 1768a3d49e..168743d4dc 100644 --- a/openpype/hosts/maya/plugins/create/create_layout.py +++ b/openpype/hosts/maya/plugins/create/create_layout.py @@ -1,16 +1,21 @@ from openpype.hosts.maya.api import plugin +from openpype.lib import BoolDef -class CreateLayout(plugin.Creator): +class CreateLayout(plugin.MayaCreator): """A grouped package of loaded content""" - name = "layoutMain" + identifier = "io.openpype.creators.maya.layout" label = "Layout" family = "layout" icon = "cubes" - def __init__(self, *args, **kwargs): - super(CreateLayout, self).__init__(*args, **kwargs) - # enable this when you want to - # publish group of loaded asset - self.data["groupLoadedAssets"] = False + def get_instance_attr_defs(self): + + return [ + BoolDef("groupLoadedAssets", + label="Group Loaded Assets", + tooltip="Enable this when you want to publish group of " + "loaded asset", + default=False) + ] diff --git a/openpype/hosts/maya/plugins/create/create_look.py b/openpype/hosts/maya/plugins/create/create_look.py index 51b0b8819a..385ae81e01 100644 --- a/openpype/hosts/maya/plugins/create/create_look.py +++ b/openpype/hosts/maya/plugins/create/create_look.py @@ -1,29 +1,53 @@ from openpype.hosts.maya.api import ( - lib, - plugin + plugin, + lib +) +from openpype.lib import ( + BoolDef, + TextDef ) -class CreateLook(plugin.Creator): +class CreateLook(plugin.MayaCreator): """Shader connections defining shape look""" - name = "look" + identifier = "io.openpype.creators.maya.look" label = "Look" family = "look" icon = "paint-brush" + make_tx = True rs_tex = False - def __init__(self, *args, **kwargs): - super(CreateLook, self).__init__(*args, **kwargs) + def get_instance_attr_defs(self): - self.data["renderlayer"] = lib.get_current_renderlayer() + return [ + # TODO: This value should actually get set on create! + TextDef("renderLayer", + # TODO: Bug: Hidden attribute's label is still shown in UI? + hidden=True, + default=lib.get_current_renderlayer(), + label="Renderlayer", + tooltip="Renderlayer to extract the look from"), + BoolDef("maketx", + label="MakeTX", + tooltip="Whether to generate .tx files for your textures", + default=self.make_tx), + BoolDef("rstex", + label="Convert textures to .rstex", + tooltip="Whether to generate Redshift .rstex files for " + "your textures", + default=self.rs_tex), + BoolDef("forceCopy", + label="Force Copy", + tooltip="Enable users to force a copy instead of hardlink." + "\nNote: On Windows copy is always forced due to " + "bugs in windows' implementation of hardlinks.", + default=False) + ] - # Whether to automatically convert the textures to .tx upon publish. - self.data["maketx"] = self.make_tx - # Whether to automatically convert the textures to .rstex upon publish. - self.data["rstex"] = self.rs_tex - # Enable users to force a copy. - # - on Windows is "forceCopy" always changed to `True` because of - # windows implementation of hardlinks - self.data["forceCopy"] = False + def get_pre_create_attr_defs(self): + # Show same attributes on create but include use selection + defs = super(CreateLook, self).get_pre_create_attr_defs() + defs.extend(self.get_instance_attr_defs()) + return defs diff --git a/openpype/hosts/maya/plugins/create/create_mayaascii.py b/openpype/hosts/maya/plugins/create/create_mayascene.py similarity index 65% rename from openpype/hosts/maya/plugins/create/create_mayaascii.py rename to openpype/hosts/maya/plugins/create/create_mayascene.py index f54f2df812..b61c97aebf 100644 --- a/openpype/hosts/maya/plugins/create/create_mayaascii.py +++ b/openpype/hosts/maya/plugins/create/create_mayascene.py @@ -1,9 +1,10 @@ from openpype.hosts.maya.api import plugin -class CreateMayaScene(plugin.Creator): +class CreateMayaScene(plugin.MayaCreator): """Raw Maya Scene file export""" + identifier = "io.openpype.creators.maya.mayascene" name = "mayaScene" label = "Maya Scene" family = "mayaScene" diff --git a/openpype/hosts/maya/plugins/create/create_model.py b/openpype/hosts/maya/plugins/create/create_model.py index 520e962f74..30f1a82281 100644 --- a/openpype/hosts/maya/plugins/create/create_model.py +++ b/openpype/hosts/maya/plugins/create/create_model.py @@ -1,26 +1,43 @@ from openpype.hosts.maya.api import plugin +from openpype.lib import ( + BoolDef, + TextDef +) -class CreateModel(plugin.Creator): +class CreateModel(plugin.MayaCreator): """Polygonal static geometry""" - name = "modelMain" + identifier = "io.openpype.creators.maya.model" label = "Model" family = "model" icon = "cube" defaults = ["Main", "Proxy", "_MD", "_HD", "_LD"] + write_color_sets = False write_face_sets = False - def __init__(self, *args, **kwargs): - super(CreateModel, self).__init__(*args, **kwargs) - # Vertex colors with the geometry - self.data["writeColorSets"] = self.write_color_sets - self.data["writeFaceSets"] = self.write_face_sets + def get_instance_attr_defs(self): - # Include attributes by attribute name or prefix - self.data["attr"] = "" - self.data["attrPrefix"] = "" - - # Whether to include parent hierarchy of nodes in the instance - self.data["includeParentHierarchy"] = False + return [ + BoolDef("writeColorSets", + label="Write vertex colors", + tooltip="Write vertex colors with the geometry", + default=self.write_color_sets), + BoolDef("writeFaceSets", + label="Write face sets", + tooltip="Write face sets with the geometry", + default=self.write_face_sets), + BoolDef("includeParentHierarchy", + label="Include Parent Hierarchy", + tooltip="Whether to include parent hierarchy of nodes in " + "the publish instance", + default=False), + TextDef("attr", + label="Custom Attributes", + default="", + placeholder="attr1, attr2"), + TextDef("attrPrefix", + label="Custom Attributes Prefix", + placeholder="prefix1, prefix2") + ] diff --git a/openpype/hosts/maya/plugins/create/create_multiverse_look.py b/openpype/hosts/maya/plugins/create/create_multiverse_look.py index f47c88a93b..f27eb57fc1 100644 --- a/openpype/hosts/maya/plugins/create/create_multiverse_look.py +++ b/openpype/hosts/maya/plugins/create/create_multiverse_look.py @@ -1,15 +1,27 @@ from openpype.hosts.maya.api import plugin +from openpype.lib import ( + BoolDef, + EnumDef +) -class CreateMultiverseLook(plugin.Creator): +class CreateMultiverseLook(plugin.MayaCreator): """Create Multiverse Look""" - name = "mvLook" + identifier = "io.openpype.creators.maya.mvlook" label = "Multiverse Look" family = "mvLook" icon = "cubes" - def __init__(self, *args, **kwargs): - super(CreateMultiverseLook, self).__init__(*args, **kwargs) - self.data["fileFormat"] = ["usda", "usd"] - self.data["publishMipMap"] = True + def get_instance_attr_defs(self): + + return [ + EnumDef("fileFormat", + label="File Format", + tooltip="USD export file format", + items=["usda", "usd"], + default="usda"), + BoolDef("publishMipMap", + label="Publish MipMap", + default=True), + ] diff --git a/openpype/hosts/maya/plugins/create/create_multiverse_usd.py b/openpype/hosts/maya/plugins/create/create_multiverse_usd.py index 8cd76b5f40..0b0ad3bccb 100644 --- a/openpype/hosts/maya/plugins/create/create_multiverse_usd.py +++ b/openpype/hosts/maya/plugins/create/create_multiverse_usd.py @@ -1,53 +1,135 @@ from openpype.hosts.maya.api import plugin, lib +from openpype.lib import ( + BoolDef, + NumberDef, + TextDef, + EnumDef +) -class CreateMultiverseUsd(plugin.Creator): +class CreateMultiverseUsd(plugin.MayaCreator): """Create Multiverse USD Asset""" - name = "mvUsdMain" + identifier = "io.openpype.creators.maya.mvusdasset" label = "Multiverse USD Asset" family = "usd" icon = "cubes" - def __init__(self, *args, **kwargs): - super(CreateMultiverseUsd, self).__init__(*args, **kwargs) + def get_instance_attr_defs(self): - # Add animation data first, since it maintains order. - self.data.update(lib.collect_animation_data(True)) + defs = lib.collect_animation_defs(fps=True) + defs.extend([ + EnumDef("fileFormat", + label="File format", + items=["usd", "usda", "usdz"], + default="usd"), + BoolDef("stripNamespaces", + label="Strip Namespaces", + default=True), + BoolDef("mergeTransformAndShape", + label="Merge Transform and Shape", + default=False), + BoolDef("writeAncestors", + label="Write Ancestors", + default=True), + BoolDef("flattenParentXforms", + label="Flatten Parent Xforms", + default=False), + BoolDef("writeSparseOverrides", + label="Write Sparse Overrides", + default=False), + BoolDef("useMetaPrimPath", + label="Use Meta Prim Path", + default=False), + TextDef("customRootPath", + label="Custom Root Path", + default=''), + TextDef("customAttributes", + label="Custom Attributes", + tooltip="Comma-separated list of attribute names", + default=''), + TextDef("nodeTypesToIgnore", + label="Node Types to Ignore", + tooltip="Comma-separated list of node types to be ignored", + default=''), + BoolDef("writeMeshes", + label="Write Meshes", + default=True), + BoolDef("writeCurves", + label="Write Curves", + default=True), + BoolDef("writeParticles", + label="Write Particles", + default=True), + BoolDef("writeCameras", + label="Write Cameras", + default=False), + BoolDef("writeLights", + label="Write Lights", + default=False), + BoolDef("writeJoints", + label="Write Joints", + default=False), + BoolDef("writeCollections", + label="Write Collections", + default=False), + BoolDef("writePositions", + label="Write Positions", + default=True), + BoolDef("writeNormals", + label="Write Normals", + default=True), + BoolDef("writeUVs", + label="Write UVs", + default=True), + BoolDef("writeColorSets", + label="Write Color Sets", + default=False), + BoolDef("writeTangents", + label="Write Tangents", + default=False), + BoolDef("writeRefPositions", + label="Write Ref Positions", + default=True), + BoolDef("writeBlendShapes", + label="Write BlendShapes", + default=False), + BoolDef("writeDisplayColor", + label="Write Display Color", + default=True), + BoolDef("writeSkinWeights", + label="Write Skin Weights", + default=False), + BoolDef("writeMaterialAssignment", + label="Write Material Assignment", + default=False), + BoolDef("writeHardwareShader", + label="Write Hardware Shader", + default=False), + BoolDef("writeShadingNetworks", + label="Write Shading Networks", + default=False), + BoolDef("writeTransformMatrix", + label="Write Transform Matrix", + default=True), + BoolDef("writeUsdAttributes", + label="Write USD Attributes", + default=True), + BoolDef("writeInstancesAsReferences", + label="Write Instances as References", + default=False), + BoolDef("timeVaryingTopology", + label="Time Varying Topology", + default=False), + TextDef("customMaterialNamespace", + label="Custom Material Namespace", + default=''), + NumberDef("numTimeSamples", + label="Num Time Samples", + default=1), + NumberDef("timeSamplesSpan", + label="Time Samples Span", + default=0.0), + ]) - self.data["fileFormat"] = ["usd", "usda", "usdz"] - self.data["stripNamespaces"] = True - self.data["mergeTransformAndShape"] = False - self.data["writeAncestors"] = True - self.data["flattenParentXforms"] = False - self.data["writeSparseOverrides"] = False - self.data["useMetaPrimPath"] = False - self.data["customRootPath"] = '' - self.data["customAttributes"] = '' - self.data["nodeTypesToIgnore"] = '' - self.data["writeMeshes"] = True - self.data["writeCurves"] = True - self.data["writeParticles"] = True - self.data["writeCameras"] = False - self.data["writeLights"] = False - self.data["writeJoints"] = False - self.data["writeCollections"] = False - self.data["writePositions"] = True - self.data["writeNormals"] = True - self.data["writeUVs"] = True - self.data["writeColorSets"] = False - self.data["writeTangents"] = False - self.data["writeRefPositions"] = True - self.data["writeBlendShapes"] = False - self.data["writeDisplayColor"] = True - self.data["writeSkinWeights"] = False - self.data["writeMaterialAssignment"] = False - self.data["writeHardwareShader"] = False - self.data["writeShadingNetworks"] = False - self.data["writeTransformMatrix"] = True - self.data["writeUsdAttributes"] = True - self.data["writeInstancesAsReferences"] = False - self.data["timeVaryingTopology"] = False - self.data["customMaterialNamespace"] = '' - self.data["numTimeSamples"] = 1 - self.data["timeSamplesSpan"] = 0.0 + return defs diff --git a/openpype/hosts/maya/plugins/create/create_multiverse_usd_comp.py b/openpype/hosts/maya/plugins/create/create_multiverse_usd_comp.py index ed466a8068..66ddd83eda 100644 --- a/openpype/hosts/maya/plugins/create/create_multiverse_usd_comp.py +++ b/openpype/hosts/maya/plugins/create/create_multiverse_usd_comp.py @@ -1,26 +1,48 @@ from openpype.hosts.maya.api import plugin, lib +from openpype.lib import ( + BoolDef, + NumberDef, + EnumDef +) -class CreateMultiverseUsdComp(plugin.Creator): +class CreateMultiverseUsdComp(plugin.MayaCreator): """Create Multiverse USD Composition""" - name = "mvUsdCompositionMain" + identifier = "io.openpype.creators.maya.mvusdcomposition" label = "Multiverse USD Composition" family = "mvUsdComposition" icon = "cubes" - def __init__(self, *args, **kwargs): - super(CreateMultiverseUsdComp, self).__init__(*args, **kwargs) + def get_instance_attr_defs(self): - # Add animation data first, since it maintains order. - self.data.update(lib.collect_animation_data(True)) + defs = lib.collect_animation_defs(fps=True) + defs.extend([ + EnumDef("fileFormat", + label="File format", + items=["usd", "usda"], + default="usd"), + BoolDef("stripNamespaces", + label="Strip Namespaces", + default=False), + BoolDef("mergeTransformAndShape", + label="Merge Transform and Shape", + default=False), + BoolDef("flattenContent", + label="Flatten Content", + default=False), + BoolDef("writeAsCompoundLayers", + label="Write As Compound Layers", + default=False), + BoolDef("writePendingOverrides", + label="Write Pending Overrides", + default=False), + NumberDef("numTimeSamples", + label="Num Time Samples", + default=1), + NumberDef("timeSamplesSpan", + label="Time Samples Span", + default=0.0), + ]) - # Order of `fileFormat` must match extract_multiverse_usd_comp.py - self.data["fileFormat"] = ["usda", "usd"] - self.data["stripNamespaces"] = False - self.data["mergeTransformAndShape"] = False - self.data["flattenContent"] = False - self.data["writeAsCompoundLayers"] = False - self.data["writePendingOverrides"] = False - self.data["numTimeSamples"] = 1 - self.data["timeSamplesSpan"] = 0.0 + return defs diff --git a/openpype/hosts/maya/plugins/create/create_multiverse_usd_over.py b/openpype/hosts/maya/plugins/create/create_multiverse_usd_over.py index 06e22df295..e1534dd68c 100644 --- a/openpype/hosts/maya/plugins/create/create_multiverse_usd_over.py +++ b/openpype/hosts/maya/plugins/create/create_multiverse_usd_over.py @@ -1,30 +1,59 @@ from openpype.hosts.maya.api import plugin, lib +from openpype.lib import ( + BoolDef, + NumberDef, + EnumDef +) class CreateMultiverseUsdOver(plugin.Creator): """Create Multiverse USD Override""" - name = "mvUsdOverrideMain" + identifier = "io.openpype.creators.maya.mvusdoverride" label = "Multiverse USD Override" family = "mvUsdOverride" icon = "cubes" - def __init__(self, *args, **kwargs): - super(CreateMultiverseUsdOver, self).__init__(*args, **kwargs) + def get_instance_attr_defs(self): + defs = lib.collect_animation_defs(fps=True) + defs.extend([ + EnumDef("fileFormat", + label="File format", + items=["usd", "usda"], + default="usd"), + BoolDef("writeAll", + label="Write All", + default=False), + BoolDef("writeTransforms", + label="Write Transforms", + default=True), + BoolDef("writeVisibility", + label="Write Visibility", + default=True), + BoolDef("writeAttributes", + label="Write Attributes", + default=True), + BoolDef("writeMaterials", + label="Write Materials", + default=True), + BoolDef("writeVariants", + label="Write Variants", + default=True), + BoolDef("writeVariantsDefinition", + label="Write Variants Definition", + default=True), + BoolDef("writeActiveState", + label="Write Active State", + default=True), + BoolDef("writeNamespaces", + label="Write Namespaces", + default=False), + NumberDef("numTimeSamples", + label="Num Time Samples", + default=1), + NumberDef("timeSamplesSpan", + label="Time Samples Span", + default=0.0), + ]) - # Add animation data first, since it maintains order. - self.data.update(lib.collect_animation_data(True)) - - # Order of `fileFormat` must match extract_multiverse_usd_over.py - self.data["fileFormat"] = ["usda", "usd"] - self.data["writeAll"] = False - self.data["writeTransforms"] = True - self.data["writeVisibility"] = True - self.data["writeAttributes"] = True - self.data["writeMaterials"] = True - self.data["writeVariants"] = True - self.data["writeVariantsDefinition"] = True - self.data["writeActiveState"] = True - self.data["writeNamespaces"] = False - self.data["numTimeSamples"] = 1 - self.data["timeSamplesSpan"] = 0.0 + return defs diff --git a/openpype/hosts/maya/plugins/create/create_pointcache.py b/openpype/hosts/maya/plugins/create/create_pointcache.py index 1b8d5e6850..f4e8cbfc9a 100644 --- a/openpype/hosts/maya/plugins/create/create_pointcache.py +++ b/openpype/hosts/maya/plugins/create/create_pointcache.py @@ -4,47 +4,85 @@ from openpype.hosts.maya.api import ( lib, plugin ) +from openpype.lib import ( + BoolDef, + TextDef +) -class CreatePointCache(plugin.Creator): +class CreatePointCache(plugin.MayaCreator): """Alembic pointcache for animated data""" - name = "pointcache" - label = "Point Cache" + identifier = "io.openpype.creators.maya.pointcache" + label = "Pointcache" family = "pointcache" icon = "gears" write_color_sets = False write_face_sets = False include_user_defined_attributes = False - def __init__(self, *args, **kwargs): - super(CreatePointCache, self).__init__(*args, **kwargs) + def get_instance_attr_defs(self): - # Add animation data - self.data.update(lib.collect_animation_data()) + defs = lib.collect_animation_defs() - # Vertex colors with the geometry. - self.data["writeColorSets"] = self.write_color_sets - # Vertex colors with the geometry. - self.data["writeFaceSets"] = self.write_face_sets - self.data["renderableOnly"] = False # Only renderable visible shapes - self.data["visibleOnly"] = False # only nodes that are visible - self.data["includeParentHierarchy"] = False # Include parent groups - self.data["worldSpace"] = True # Default to exporting world-space - self.data["refresh"] = False # Default to suspend refresh. - - # Add options for custom attributes - value = self.include_user_defined_attributes - self.data["includeUserDefinedAttributes"] = value - self.data["attr"] = "" - self.data["attrPrefix"] = "" + defs.extend([ + BoolDef("writeColorSets", + label="Write vertex colors", + tooltip="Write vertex colors with the geometry", + default=False), + BoolDef("writeFaceSets", + label="Write face sets", + tooltip="Write face sets with the geometry", + default=False), + BoolDef("renderableOnly", + label="Renderable Only", + tooltip="Only export renderable visible shapes", + default=False), + BoolDef("visibleOnly", + label="Visible Only", + tooltip="Only export dag objects visible during " + "frame range", + default=False), + BoolDef("includeParentHierarchy", + label="Include Parent Hierarchy", + tooltip="Whether to include parent hierarchy of nodes in " + "the publish instance", + default=False), + BoolDef("worldSpace", + label="World-Space Export", + default=True), + BoolDef("refresh", + label="Refresh viewport during export", + default=False), + BoolDef("includeUserDefinedAttributes", + label="Include User Defined Attributes", + default=self.include_user_defined_attributes), + TextDef("attr", + label="Custom Attributes", + default="", + placeholder="attr1, attr2"), + TextDef("attrPrefix", + label="Custom Attributes Prefix", + default="", + placeholder="prefix1, prefix2") + ]) + # TODO: Implement these on a Deadline plug-in instead? + """ # Default to not send to farm. self.data["farm"] = False self.data["priority"] = 50 + """ - def process(self): - instance = super(CreatePointCache, self).process() + return defs - assProxy = cmds.sets(name=instance + "_proxy_SET", empty=True) - cmds.sets(assProxy, forceElement=instance) + def create(self, subset_name, instance_data, pre_create_data): + + instance = super(CreatePointCache, self).create( + subset_name, instance_data, pre_create_data + ) + instance_node = instance.get("instance_node") + + # For Arnold standin proxy + proxy_set = cmds.sets(name=instance_node + "_proxy_SET", empty=True) + cmds.sets(proxy_set, forceElement=instance_node) diff --git a/openpype/hosts/maya/plugins/create/create_proxy_abc.py b/openpype/hosts/maya/plugins/create/create_proxy_abc.py index 2946f7b530..d89470ebee 100644 --- a/openpype/hosts/maya/plugins/create/create_proxy_abc.py +++ b/openpype/hosts/maya/plugins/create/create_proxy_abc.py @@ -2,34 +2,49 @@ from openpype.hosts.maya.api import ( lib, plugin ) +from openpype.lib import ( + BoolDef, + TextDef +) -class CreateProxyAlembic(plugin.Creator): +class CreateProxyAlembic(plugin.MayaCreator): """Proxy Alembic for animated data""" - name = "proxyAbcMain" + identifier = "io.openpype.creators.maya.proxyabc" label = "Proxy Alembic" family = "proxyAbc" icon = "gears" write_color_sets = False write_face_sets = False - def __init__(self, *args, **kwargs): - super(CreateProxyAlembic, self).__init__(*args, **kwargs) + def get_instance_attr_defs(self): - # Add animation data - self.data.update(lib.collect_animation_data()) + defs = lib.collect_animation_defs() - # Vertex colors with the geometry. - self.data["writeColorSets"] = self.write_color_sets - # Vertex colors with the geometry. - self.data["writeFaceSets"] = self.write_face_sets - # Default to exporting world-space - self.data["worldSpace"] = True + defs.extend([ + BoolDef("writeColorSets", + label="Write vertex colors", + tooltip="Write vertex colors with the geometry", + default=self.write_color_sets), + BoolDef("writeFaceSets", + label="Write face sets", + tooltip="Write face sets with the geometry", + default=self.write_face_sets), + BoolDef("worldSpace", + label="World-Space Export", + default=True), + TextDef("nameSuffix", + label="Name Suffix for Bounding Box", + default="_BBox", + placeholder="_BBox"), + TextDef("attr", + label="Custom Attributes", + default="", + placeholder="attr1, attr2"), + TextDef("attrPrefix", + label="Custom Attributes Prefix", + placeholder="prefix1, prefix2") + ]) - # name suffix for the bounding box - self.data["nameSuffix"] = "_BBox" - - # Add options for custom attributes - self.data["attr"] = "" - self.data["attrPrefix"] = "" + return defs diff --git a/openpype/hosts/maya/plugins/create/create_redshift_proxy.py b/openpype/hosts/maya/plugins/create/create_redshift_proxy.py index 419a8d99d4..2490738e8f 100644 --- a/openpype/hosts/maya/plugins/create/create_redshift_proxy.py +++ b/openpype/hosts/maya/plugins/create/create_redshift_proxy.py @@ -2,22 +2,24 @@ """Creator of Redshift proxy subset types.""" from openpype.hosts.maya.api import plugin, lib +from openpype.lib import BoolDef -class CreateRedshiftProxy(plugin.Creator): +class CreateRedshiftProxy(plugin.MayaCreator): """Create instance of Redshift Proxy subset.""" - name = "redshiftproxy" + identifier = "io.openpype.creators.maya.redshiftproxy" label = "Redshift Proxy" family = "redshiftproxy" icon = "gears" - def __init__(self, *args, **kwargs): - super(CreateRedshiftProxy, self).__init__(*args, **kwargs) + def get_instance_attr_defs(self): - animation_data = lib.collect_animation_data() + defs = [ + BoolDef("animation", + label="Export animation", + default=False) + ] - self.data["animation"] = False - self.data["proxyFrameStart"] = animation_data["frameStart"] - self.data["proxyFrameEnd"] = animation_data["frameEnd"] - self.data["proxyFrameStep"] = animation_data["step"] + defs.extend(lib.collect_animation_defs()) + return defs diff --git a/openpype/hosts/maya/plugins/create/create_render.py b/openpype/hosts/maya/plugins/create/create_render.py index 4681175808..cc5c1eb205 100644 --- a/openpype/hosts/maya/plugins/create/create_render.py +++ b/openpype/hosts/maya/plugins/create/create_render.py @@ -1,425 +1,108 @@ # -*- coding: utf-8 -*- """Create ``Render`` instance in Maya.""" -import json -import os -import appdirs -import requests - -from maya import cmds -from maya.app.renderSetup.model import renderSetup - -from openpype.settings import ( - get_system_settings, - get_project_settings, -) -from openpype.lib import requests_get -from openpype.modules import ModulesManager -from openpype.pipeline import legacy_io from openpype.hosts.maya.api import ( - lib, lib_rendersettings, plugin ) +from openpype.pipeline import CreatorError +from openpype.lib import ( + BoolDef, + NumberDef, +) -class CreateRender(plugin.Creator): - """Create *render* instance. +class CreateRenderlayer(plugin.RenderlayerCreator): + """Create and manages renderlayer subset per renderLayer in workfile. - Render instances are not actually published, they hold options for - collecting of render data. It render instance is present, it will trigger - collection of render layers, AOVs, cameras for either direct submission - to render farm or export as various standalone formats (like V-Rays - ``vrscenes`` or Arnolds ``ass`` files) and then submitting them to render - farm. - - Instance has following attributes:: - - primaryPool (list of str): Primary list of slave machine pool to use. - secondaryPool (list of str): Optional secondary list of slave pools. - suspendPublishJob (bool): Suspend the job after it is submitted. - extendFrames (bool): Use already existing frames from previous version - to extend current render. - overrideExistingFrame (bool): Overwrite already existing frames. - priority (int): Submitted job priority - framesPerTask (int): How many frames per task to render. This is - basically job division on render farm. - whitelist (list of str): White list of slave machines - machineList (list of str): Specific list of slave machines to use - useMayaBatch (bool): Use Maya batch mode to render as opposite to - Maya interactive mode. This consumes different licenses. - vrscene (bool): Submit as ``vrscene`` file for standalone V-Ray - renderer. - ass (bool): Submit as ``ass`` file for standalone Arnold renderer. - tileRendering (bool): Instance is set to tile rendering mode. We - won't submit actual render, but we'll make publish job to wait - for Tile Assembly job done and then publish. - strict_error_checking (bool): Enable/disable error checking on DL - - See Also: - https://pype.club/docs/artist_hosts_maya#creating-basic-render-setup + This generates a single node in the scene which tells the Creator to if + it exists collect Maya rendersetup renderlayers as individual instances. + As such, triggering create doesn't actually create the instance node per + layer but only the node which tells the Creator it may now collect + the renderlayers. """ + identifier = "io.openpype.creators.maya.renderlayer" + family = "renderlayer" label = "Render" - family = "rendering" icon = "eye" - _token = None - _user = None - _password = None - _project_settings = None + layer_instance_prefix = "render" + singleton_node_name = "renderingMain" - def __init__(self, *args, **kwargs): - """Constructor.""" - super(CreateRender, self).__init__(*args, **kwargs) + render_settings = {} - # Defaults - self._project_settings = get_project_settings( - legacy_io.Session["AVALON_PROJECT"]) - if self._project_settings["maya"]["RenderSettings"]["apply_render_settings"]: # noqa + @classmethod + def apply_settings(cls, project_settings, system_settings): + cls.render_settings = project_settings["maya"]["RenderSettings"] + + def create(self, subset_name, instance_data, pre_create_data): + # Only allow a single render instance to exist + if self._get_singleton_node(): + raise CreatorError("A Render instance already exists - only " + "one can be configured.") + + # Apply default project render settings on create + if self.render_settings.get("apply_render_settings"): lib_rendersettings.RenderSettings().set_default_renderer_settings() - # Deadline-only - manager = ModulesManager() - deadline_settings = get_system_settings()["modules"]["deadline"] - if not deadline_settings["enabled"]: - self.deadline_servers = {} - return - self.deadline_module = manager.modules_by_name["deadline"] - try: - default_servers = deadline_settings["deadline_urls"] - project_servers = ( - self._project_settings["deadline"]["deadline_servers"] - ) - self.deadline_servers = { - k: default_servers[k] - for k in project_servers - if k in default_servers - } + super(CreateRenderlayer, self).create(subset_name, + instance_data, + pre_create_data) - if not self.deadline_servers: - self.deadline_servers = default_servers - - except AttributeError: - # Handle situation were we had only one url for deadline. - # get default deadline webservice url from deadline module - self.deadline_servers = self.deadline_module.deadline_urls - - def process(self): - """Entry point.""" - exists = cmds.ls(self.name) - if exists: - cmds.warning("%s already exists." % exists[0]) - return - - use_selection = self.options.get("useSelection") - with lib.undo_chunk(): - self._create_render_settings() - self.instance = super(CreateRender, self).process() - # create namespace with instance - index = 1 - namespace_name = "_{}".format(str(self.instance)) - try: - cmds.namespace(rm=namespace_name) - except RuntimeError: - # namespace is not empty, so we leave it untouched - pass - - while cmds.namespace(exists=namespace_name): - namespace_name = "_{}{}".format(str(self.instance), index) - index += 1 - - namespace = cmds.namespace(add=namespace_name) - - # add Deadline server selection list - if self.deadline_servers: - cmds.scriptJob( - attributeChange=[ - "{}.deadlineServers".format(self.instance), - self._deadline_webservice_changed - ]) - - cmds.setAttr("{}.machineList".format(self.instance), lock=True) - rs = renderSetup.instance() - layers = rs.getRenderLayers() - if use_selection: - self.log.info("Processing existing layers") - sets = [] - for layer in layers: - self.log.info(" - creating set for {}:{}".format( - namespace, layer.name())) - render_set = cmds.sets( - n="{}:{}".format(namespace, layer.name())) - sets.append(render_set) - cmds.sets(sets, forceElement=self.instance) - - # if no render layers are present, create default one with - # asterisk selector - if not layers: - render_layer = rs.createRenderLayer('Main') - collection = render_layer.createCollection("defaultCollection") - collection.getSelector().setPattern('*') - - return self.instance - - def _deadline_webservice_changed(self): - """Refresh Deadline server dependent options.""" - # get selected server - webservice = self.deadline_servers[ - self.server_aliases[ - cmds.getAttr("{}.deadlineServers".format(self.instance)) - ] - ] - pools = self.deadline_module.get_deadline_pools(webservice, self.log) - cmds.deleteAttr("{}.primaryPool".format(self.instance)) - cmds.deleteAttr("{}.secondaryPool".format(self.instance)) - - pool_setting = (self._project_settings["deadline"] - ["publish"] - ["CollectDeadlinePools"]) - - primary_pool = pool_setting["primary_pool"] - sorted_pools = self._set_default_pool(list(pools), primary_pool) - cmds.addAttr( - self.instance, - longName="primaryPool", - attributeType="enum", - enumName=":".join(sorted_pools) - ) - cmds.setAttr( - "{}.primaryPool".format(self.instance), - 0, - keyable=False, - channelBox=True - ) - - pools = ["-"] + pools - secondary_pool = pool_setting["secondary_pool"] - sorted_pools = self._set_default_pool(list(pools), secondary_pool) - cmds.addAttr( - self.instance, - longName="secondaryPool", - attributeType="enum", - enumName=":".join(sorted_pools) - ) - cmds.setAttr( - "{}.secondaryPool".format(self.instance), - 0, - keyable=False, - channelBox=True - ) - - def _create_render_settings(self): + def get_instance_attr_defs(self): """Create instance settings.""" - # get pools (slave machines of the render farm) - pool_names = [] - default_priority = 50 - self.data["suspendPublishJob"] = False - self.data["review"] = True - self.data["extendFrames"] = False - self.data["overrideExistingFrame"] = True - # self.data["useLegacyRenderLayers"] = True - self.data["priority"] = default_priority - self.data["tile_priority"] = default_priority - self.data["framesPerTask"] = 1 - self.data["whitelist"] = False - self.data["machineList"] = "" - self.data["useMayaBatch"] = False - self.data["tileRendering"] = False - self.data["tilesX"] = 2 - self.data["tilesY"] = 2 - self.data["convertToScanline"] = False - self.data["useReferencedAovs"] = False - self.data["renderSetupIncludeLights"] = ( - self._project_settings.get( - "maya", {}).get( - "RenderSettings", {}).get( - "enable_all_lights", False) - ) - # Disable for now as this feature is not working yet - # self.data["assScene"] = False + return [ + BoolDef("review", + label="Review", + tooltip="Mark as reviewable", + default=True), + BoolDef("extendFrames", + label="Extend Frames", + tooltip="Extends the frames on top of the previous " + "publish.\nIf the previous was 1001-1050 and you " + "would now submit 1020-1070 only the new frames " + "1051-1070 would be rendered and published " + "together with the previously rendered frames.\n" + "If 'overrideExistingFrame' is enabled it *will* " + "render any existing frames.", + default=False), + BoolDef("overrideExistingFrame", + label="Override Existing Frame", + tooltip="Override existing rendered frames " + "(if they exist).", + default=True), - system_settings = get_system_settings()["modules"] + # TODO: Should these move to submit_maya_deadline plugin? + # Tile rendering + BoolDef("tileRendering", + label="Enable tiled rendering", + default=False), + NumberDef("tilesX", + label="Tiles X", + default=2, + minimum=1, + decimals=0), + NumberDef("tilesY", + label="Tiles Y", + default=2, + minimum=1, + decimals=0), - deadline_enabled = system_settings["deadline"]["enabled"] - muster_enabled = system_settings["muster"]["enabled"] - muster_url = system_settings["muster"]["MUSTER_REST_URL"] + # Additional settings + BoolDef("convertToScanline", + label="Convert to Scanline", + tooltip="Convert the output images to scanline images", + default=False), + BoolDef("useReferencedAovs", + label="Use Referenced AOVs", + tooltip="Consider the AOVs from referenced scenes as well", + default=False), - if deadline_enabled and muster_enabled: - self.log.error( - "Both Deadline and Muster are enabled. " "Cannot support both." - ) - raise RuntimeError("Both Deadline and Muster are enabled") - - if deadline_enabled: - self.server_aliases = list(self.deadline_servers.keys()) - self.data["deadlineServers"] = self.server_aliases - - try: - deadline_url = self.deadline_servers["default"] - except KeyError: - # if 'default' server is not between selected, - # use first one for initial list of pools. - deadline_url = next(iter(self.deadline_servers.values())) - # Uses function to get pool machines from the assigned deadline - # url in settings - pool_names = self.deadline_module.get_deadline_pools(deadline_url, - self.log) - maya_submit_dl = self._project_settings.get( - "deadline", {}).get( - "publish", {}).get( - "MayaSubmitDeadline", {}) - priority = maya_submit_dl.get("priority", default_priority) - self.data["priority"] = priority - - tile_priority = maya_submit_dl.get("tile_priority", - default_priority) - self.data["tile_priority"] = tile_priority - - strict_error_checking = maya_submit_dl.get("strict_error_checking", - True) - self.data["strict_error_checking"] = strict_error_checking - - # Pool attributes should be last since they will be recreated when - # the deadline server changes. - pool_setting = (self._project_settings["deadline"] - ["publish"] - ["CollectDeadlinePools"]) - primary_pool = pool_setting["primary_pool"] - self.data["primaryPool"] = self._set_default_pool(pool_names, - primary_pool) - # We add a string "-" to allow the user to not - # set any secondary pools - pool_names = ["-"] + pool_names - secondary_pool = pool_setting["secondary_pool"] - self.data["secondaryPool"] = self._set_default_pool(pool_names, - secondary_pool) - - if muster_enabled: - self.log.info(">>> Loading Muster credentials ...") - self._load_credentials() - self.log.info(">>> Getting pools ...") - pools = [] - try: - pools = self._get_muster_pools() - except requests.exceptions.HTTPError as e: - if e.startswith("401"): - self.log.warning("access token expired") - self._show_login() - raise RuntimeError("Access token expired") - except requests.exceptions.ConnectionError: - self.log.error("Cannot connect to Muster API endpoint.") - raise RuntimeError("Cannot connect to {}".format(muster_url)) - for pool in pools: - self.log.info(" - pool: {}".format(pool["name"])) - pool_names.append(pool["name"]) - - self.options = {"useSelection": False} # Force no content - - def _set_default_pool(self, pool_names, pool_value): - """Reorder pool names, default should come first""" - if pool_value and pool_value in pool_names: - pool_names.remove(pool_value) - pool_names = [pool_value] + pool_names - return pool_names - - def _load_credentials(self): - """Load Muster credentials. - - Load Muster credentials from file and set ``MUSTER_USER``, - ``MUSTER_PASSWORD``, ``MUSTER_REST_URL`` is loaded from settings. - - Raises: - RuntimeError: If loaded credentials are invalid. - AttributeError: If ``MUSTER_REST_URL`` is not set. - - """ - app_dir = os.path.normpath(appdirs.user_data_dir("pype-app", "pype")) - file_name = "muster_cred.json" - fpath = os.path.join(app_dir, file_name) - file = open(fpath, "r") - muster_json = json.load(file) - self._token = muster_json.get("token", None) - if not self._token: - self._show_login() - raise RuntimeError("Invalid access token for Muster") - file.close() - self.MUSTER_REST_URL = os.environ.get("MUSTER_REST_URL") - if not self.MUSTER_REST_URL: - raise AttributeError("Muster REST API url not set") - - def _get_muster_pools(self): - """Get render pools from Muster. - - Raises: - Exception: If pool list cannot be obtained from Muster. - - """ - params = {"authToken": self._token} - api_entry = "/api/pools/list" - response = requests_get(self.MUSTER_REST_URL + api_entry, - params=params) - if response.status_code != 200: - if response.status_code == 401: - self.log.warning("Authentication token expired.") - self._show_login() - else: - self.log.error( - ("Cannot get pools from " - "Muster: {}").format(response.status_code) - ) - raise Exception("Cannot get pools from Muster") - try: - pools = response.json()["ResponseData"]["pools"] - except ValueError as e: - self.log.error("Invalid response from Muster server {}".format(e)) - raise Exception("Invalid response from Muster server") - - return pools - - def _show_login(self): - # authentication token expired so we need to login to Muster - # again to get it. We use Pype API call to show login window. - api_url = "{}/muster/show_login".format( - os.environ["OPENPYPE_WEBSERVER_URL"]) - self.log.debug(api_url) - login_response = requests_get(api_url, timeout=1) - if login_response.status_code != 200: - self.log.error("Cannot show login form to Muster") - raise Exception("Cannot show login form to Muster") - - def _requests_post(self, *args, **kwargs): - """Wrap request post method. - - Disabling SSL certificate validation if ``DONT_VERIFY_SSL`` environment - variable is found. This is useful when Deadline or Muster server are - running with self-signed certificates and their certificate is not - added to trusted certificates on client machines. - - Warning: - Disabling SSL certificate validation is defeating one line - of defense SSL is providing and it is not recommended. - - """ - if "verify" not in kwargs: - kwargs["verify"] = not os.getenv("OPENPYPE_DONT_VERIFY_SSL", True) - return requests.post(*args, **kwargs) - - def _requests_get(self, *args, **kwargs): - """Wrap request get method. - - Disabling SSL certificate validation if ``DONT_VERIFY_SSL`` environment - variable is found. This is useful when Deadline or Muster server are - running with self-signed certificates and their certificate is not - added to trusted certificates on client machines. - - Warning: - Disabling SSL certificate validation is defeating one line - of defense SSL is providing and it is not recommended. - - """ - if "verify" not in kwargs: - kwargs["verify"] = not os.getenv("OPENPYPE_DONT_VERIFY_SSL", True) - return requests.get(*args, **kwargs) + BoolDef("renderSetupIncludeLights", + label="Render Setup Include Lights", + default=self.render_settings.get("enable_all_lights", + False)) + ] diff --git a/openpype/hosts/maya/plugins/create/create_rendersetup.py b/openpype/hosts/maya/plugins/create/create_rendersetup.py index 494f90d87b..dd64a0a842 100644 --- a/openpype/hosts/maya/plugins/create/create_rendersetup.py +++ b/openpype/hosts/maya/plugins/create/create_rendersetup.py @@ -1,55 +1,31 @@ -from openpype.hosts.maya.api import ( - lib, - plugin -) -from maya import cmds +from openpype.hosts.maya.api import plugin +from openpype.pipeline import CreatorError -class CreateRenderSetup(plugin.Creator): +class CreateRenderSetup(plugin.MayaCreator): """Create rendersetup template json data""" - name = "rendersetup" + identifier = "io.openpype.creators.maya.rendersetup" label = "Render Setup Preset" family = "rendersetup" icon = "tablet" - def __init__(self, *args, **kwargs): - super(CreateRenderSetup, self).__init__(*args, **kwargs) + def get_pre_create_attr_defs(self): + # Do not show the "use_selection" setting from parent class + return [] - # here we can pre-create renderSetup layers, possibly utlizing - # settings for it. + def create(self, subset_name, instance_data, pre_create_data): - # _____ - # / __\__ - # | / __\__ - # | | / \ - # | | | | - # \__| | | - # \__| | - # \_____/ + existing_instance = None + for instance in self.create_context.instances: + if instance.family == self.family: + existing_instance = instance + break - # from pype.api import get_project_settings - # import maya.app.renderSetup.model.renderSetup as renderSetup - # settings = get_project_settings(os.environ['AVALON_PROJECT']) - # layer = settings['maya']['create']['renderSetup']["layer"] + if existing_instance: + raise CreatorError("A RenderSetup instance already exists - only " + "one can be configured.") - # rs = renderSetup.instance() - # rs.createRenderLayer(layer) - - self.options = {"useSelection": False} # Force no content - - def process(self): - exists = cmds.ls(self.name) - assert len(exists) <= 1, ( - "More than one renderglobal exists, this is a bug" - ) - - if exists: - return cmds.warning("%s already exists." % exists[0]) - - with lib.undo_chunk(): - instance = super(CreateRenderSetup, self).process() - - self.data["renderSetup"] = "42" - null = cmds.sets(name="null_SET", empty=True) - cmds.sets([null], forceElement=instance) + super(CreateRenderSetup, self).create(subset_name, + instance_data, + pre_create_data) diff --git a/openpype/hosts/maya/plugins/create/create_review.py b/openpype/hosts/maya/plugins/create/create_review.py index 40ae99b57c..f60e2406bc 100644 --- a/openpype/hosts/maya/plugins/create/create_review.py +++ b/openpype/hosts/maya/plugins/create/create_review.py @@ -1,76 +1,142 @@ -import os -from collections import OrderedDict import json +from maya import cmds + from openpype.hosts.maya.api import ( lib, plugin ) -from openpype.settings import get_project_settings -from openpype.pipeline import get_current_project_name, get_current_task_name +from openpype.lib import ( + BoolDef, + NumberDef, + EnumDef +) +from openpype.pipeline import CreatedInstance from openpype.client import get_asset_by_name +TRANSPARENCIES = [ + "preset", + "simple", + "object sorting", + "weighted average", + "depth peeling", + "alpha cut" +] -class CreateReview(plugin.Creator): - """Single baked camera""" - name = "reviewDefault" +class CreateReview(plugin.MayaCreator): + """Playblast reviewable""" + + identifier = "io.openpype.creators.maya.review" label = "Review" family = "review" icon = "video-camera" - keepImages = False - isolate = False - imagePlane = True - Width = 0 - Height = 0 - transparency = [ - "preset", - "simple", - "object sorting", - "weighted average", - "depth peeling", - "alpha cut" - ] + useMayaTimeline = True panZoom = False - def __init__(self, *args, **kwargs): - super(CreateReview, self).__init__(*args, **kwargs) - data = OrderedDict(**self.data) + # Overriding "create" method to prefill values from settings. + def create(self, subset_name, instance_data, pre_create_data): - project_name = get_current_project_name() - asset_doc = get_asset_by_name(project_name, data["asset"]) - task_name = get_current_task_name() + members = list() + if pre_create_data.get("use_selection"): + members = cmds.ls(selection=True) + + project_name = self.project_name + asset_doc = get_asset_by_name(project_name, instance_data["asset"]) + task_name = instance_data["task"] preset = lib.get_capture_preset( task_name, asset_doc["data"]["tasks"][task_name]["type"], - data["subset"], - get_project_settings(project_name), + subset_name, + self.project_settings, self.log ) - if os.environ.get("OPENPYPE_DEBUG") == "1": - self.log.debug( - "Using preset: {}".format( - json.dumps(preset, indent=4, sort_keys=True) - ) + self.log.debug( + "Using preset: {}".format( + json.dumps(preset, indent=4, sort_keys=True) ) + ) + + with lib.undo_chunk(): + instance_node = cmds.sets(members, name=subset_name) + instance_data["instance_node"] = instance_node + instance = CreatedInstance( + self.family, + subset_name, + instance_data, + self) + + creator_attribute_defs_by_key = { + x.key: x for x in instance.creator_attribute_defs + } + mapping = { + "review_width": preset["Resolution"]["width"], + "review_height": preset["Resolution"]["height"], + "isolate": preset["Generic"]["isolate_view"], + "imagePlane": preset["Viewport Options"]["imagePlane"], + "panZoom": preset["Generic"]["pan_zoom"] + } + for key, value in mapping.items(): + creator_attribute_defs_by_key[key].default = value + + self._add_instance_to_context(instance) + + self.imprint_instance_node(instance_node, + data=instance.data_to_store()) + return instance + + def get_instance_attr_defs(self): + + defs = lib.collect_animation_defs() # Option for using Maya or asset frame range in settings. - frame_range = lib.get_frame_range() - if self.useMayaTimeline: - frame_range = lib.collect_animation_data(fps=True) - for key, value in frame_range.items(): - data[key] = value + if not self.useMayaTimeline: + # Update the defaults to be the asset frame range + frame_range = lib.get_frame_range() + defs_by_key = {attr_def.key: attr_def for attr_def in defs} + for key, value in frame_range.items(): + if key not in defs_by_key: + raise RuntimeError("Attribute definition not found to be " + "updated for key: {}".format(key)) + attr_def = defs_by_key[key] + attr_def.default = value - data["fps"] = lib.collect_animation_data(fps=True)["fps"] + defs.extend([ + NumberDef("review_width", + label="Review width", + tooltip="A value of zero will use the asset resolution.", + decimals=0, + minimum=0, + default=0), + NumberDef("review_height", + label="Review height", + tooltip="A value of zero will use the asset resolution.", + decimals=0, + minimum=0, + default=0), + BoolDef("keepImages", + label="Keep Images", + tooltip="Whether to also publish along the image sequence " + "next to the video reviewable.", + default=False), + BoolDef("isolate", + label="Isolate render members of instance", + tooltip="When enabled only the members of the instance " + "will be included in the playblast review.", + default=False), + BoolDef("imagePlane", + label="Show Image Plane", + default=True), + EnumDef("transparency", + label="Transparency", + items=TRANSPARENCIES), + BoolDef("panZoom", + label="Enable camera pan/zoom", + default=True), + EnumDef("displayLights", + label="Display Lights", + items=lib.DISPLAY_LIGHTS_ENUM), + ]) - data["keepImages"] = self.keepImages - data["transparency"] = self.transparency - data["review_width"] = preset["Resolution"]["width"] - data["review_height"] = preset["Resolution"]["height"] - data["isolate"] = preset["Generic"]["isolate_view"] - data["imagePlane"] = preset["Viewport Options"]["imagePlane"] - data["panZoom"] = preset["Generic"]["pan_zoom"] - data["displayLights"] = lib.DISPLAY_LIGHTS_LABELS - - self.data = data + return defs diff --git a/openpype/hosts/maya/plugins/create/create_rig.py b/openpype/hosts/maya/plugins/create/create_rig.py index 8032e5fbbd..04104cb7cb 100644 --- a/openpype/hosts/maya/plugins/create/create_rig.py +++ b/openpype/hosts/maya/plugins/create/create_rig.py @@ -1,25 +1,25 @@ from maya import cmds -from openpype.hosts.maya.api import ( - lib, - plugin -) +from openpype.hosts.maya.api import plugin -class CreateRig(plugin.Creator): +class CreateRig(plugin.MayaCreator): """Artist-friendly rig with controls to direct motion""" - name = "rigDefault" + identifier = "io.openpype.creators.maya.rig" label = "Rig" family = "rig" icon = "wheelchair" - def process(self): + def create(self, subset_name, instance_data, pre_create_data): - with lib.undo_chunk(): - instance = super(CreateRig, self).process() + instance = super(CreateRig, self).create(subset_name, + instance_data, + pre_create_data) - self.log.info("Creating Rig instance set up ...") - controls = cmds.sets(name="controls_SET", empty=True) - pointcache = cmds.sets(name="out_SET", empty=True) - cmds.sets([controls, pointcache], forceElement=instance) + instance_node = instance.get("instance_node") + + self.log.info("Creating Rig instance set up ...") + controls = cmds.sets(name="controls_SET", empty=True) + pointcache = cmds.sets(name="out_SET", empty=True) + cmds.sets([controls, pointcache], forceElement=instance_node) diff --git a/openpype/hosts/maya/plugins/create/create_setdress.py b/openpype/hosts/maya/plugins/create/create_setdress.py index 4246183fdb..594a3dc46d 100644 --- a/openpype/hosts/maya/plugins/create/create_setdress.py +++ b/openpype/hosts/maya/plugins/create/create_setdress.py @@ -1,16 +1,19 @@ from openpype.hosts.maya.api import plugin +from openpype.lib import BoolDef -class CreateSetDress(plugin.Creator): +class CreateSetDress(plugin.MayaCreator): """A grouped package of loaded content""" - name = "setdressMain" + identifier = "io.openpype.creators.maya.setdress" label = "Set Dress" family = "setdress" icon = "cubes" defaults = ["Main", "Anim"] - def __init__(self, *args, **kwargs): - super(CreateSetDress, self).__init__(*args, **kwargs) - - self.data["exactSetMembersOnly"] = True + def get_instance_attr_defs(self): + return [ + BoolDef("exactSetMembersOnly", + label="Exact Set Members Only", + default=True) + ] diff --git a/openpype/hosts/maya/plugins/create/create_unreal_skeletalmesh.py b/openpype/hosts/maya/plugins/create/create_unreal_skeletalmesh.py index 6e72bf5324..4e2a99eced 100644 --- a/openpype/hosts/maya/plugins/create/create_unreal_skeletalmesh.py +++ b/openpype/hosts/maya/plugins/create/create_unreal_skeletalmesh.py @@ -1,47 +1,63 @@ # -*- coding: utf-8 -*- """Creator for Unreal Skeletal Meshes.""" from openpype.hosts.maya.api import plugin, lib -from openpype.pipeline import legacy_io +from openpype.lib import ( + BoolDef, + TextDef +) + from maya import cmds # noqa -class CreateUnrealSkeletalMesh(plugin.Creator): +class CreateUnrealSkeletalMesh(plugin.MayaCreator): """Unreal Static Meshes with collisions.""" - name = "staticMeshMain" + + identifier = "io.openpype.creators.maya.unrealskeletalmesh" label = "Unreal - Skeletal Mesh" family = "skeletalMesh" icon = "thumbs-up" dynamic_subset_keys = ["asset"] - joint_hints = [] + # Defined in settings + joint_hints = set() - def __init__(self, *args, **kwargs): - """Constructor.""" - super(CreateUnrealSkeletalMesh, self).__init__(*args, **kwargs) - - @classmethod - def get_dynamic_data( - cls, variant, task_name, asset_id, project_name, host_name - ): - dynamic_data = super(CreateUnrealSkeletalMesh, cls).get_dynamic_data( - variant, task_name, asset_id, project_name, host_name + def apply_settings(self, project_settings, system_settings): + """Apply project settings to creator""" + settings = ( + project_settings["maya"]["create"]["CreateUnrealSkeletalMesh"] ) - dynamic_data["asset"] = legacy_io.Session.get("AVALON_ASSET") + self.joint_hints = set(settings.get("joint_hints", [])) + + def get_dynamic_data( + self, variant, task_name, asset_doc, project_name, host_name, instance + ): + """ + The default subset name templates for Unreal include {asset} and thus + we should pass that along as dynamic data. + """ + dynamic_data = super(CreateUnrealSkeletalMesh, self).get_dynamic_data( + variant, task_name, asset_doc, project_name, host_name, instance + ) + dynamic_data["asset"] = asset_doc["name"] return dynamic_data - def process(self): - self.name = "{}_{}".format(self.family, self.name) - with lib.undo_chunk(): - instance = super(CreateUnrealSkeletalMesh, self).process() - content = cmds.sets(instance, query=True) + def create(self, subset_name, instance_data, pre_create_data): + + with lib.undo_chunk(): + instance = super(CreateUnrealSkeletalMesh, self).create( + subset_name, instance_data, pre_create_data) + instance_node = instance.get("instance_node") + + # We reorganize the geometry that was originally added into the + # set into either 'joints_SET' or 'geometry_SET' based on the + # joint_hints from project settings + members = cmds.sets(instance_node, query=True) + cmds.sets(clear=instance_node) - # empty set and process its former content - cmds.sets(content, rm=instance) geometry_set = cmds.sets(name="geometry_SET", empty=True) joints_set = cmds.sets(name="joints_SET", empty=True) - cmds.sets([geometry_set, joints_set], forceElement=instance) - members = cmds.ls(content) or [] + cmds.sets([geometry_set, joints_set], forceElement=instance_node) for node in members: if node in self.joint_hints: @@ -49,20 +65,38 @@ class CreateUnrealSkeletalMesh(plugin.Creator): else: cmds.sets(node, forceElement=geometry_set) - # Add animation data - self.data.update(lib.collect_animation_data()) + def get_instance_attr_defs(self): - # Only renderable visible shapes - self.data["renderableOnly"] = False - # only nodes that are visible - self.data["visibleOnly"] = False - # Include parent groups - self.data["includeParentHierarchy"] = False - # Default to exporting world-space - self.data["worldSpace"] = True - # Default to suspend refresh. - self.data["refresh"] = False + defs = lib.collect_animation_defs() - # Add options for custom attributes - self.data["attr"] = "" - self.data["attrPrefix"] = "" + defs.extend([ + BoolDef("renderableOnly", + label="Renderable Only", + tooltip="Only export renderable visible shapes", + default=False), + BoolDef("visibleOnly", + label="Visible Only", + tooltip="Only export dag objects visible during " + "frame range", + default=False), + BoolDef("includeParentHierarchy", + label="Include Parent Hierarchy", + tooltip="Whether to include parent hierarchy of nodes in " + "the publish instance", + default=False), + BoolDef("worldSpace", + label="World-Space Export", + default=True), + BoolDef("refresh", + label="Refresh viewport during export", + default=False), + TextDef("attr", + label="Custom Attributes", + default="", + placeholder="attr1, attr2"), + TextDef("attrPrefix", + label="Custom Attributes Prefix", + placeholder="prefix1, prefix2") + ]) + + return defs diff --git a/openpype/hosts/maya/plugins/create/create_unreal_staticmesh.py b/openpype/hosts/maya/plugins/create/create_unreal_staticmesh.py index 44cbee0502..3f96d91a54 100644 --- a/openpype/hosts/maya/plugins/create/create_unreal_staticmesh.py +++ b/openpype/hosts/maya/plugins/create/create_unreal_staticmesh.py @@ -1,58 +1,90 @@ # -*- coding: utf-8 -*- """Creator for Unreal Static Meshes.""" from openpype.hosts.maya.api import plugin, lib -from openpype.settings import get_project_settings -from openpype.pipeline import legacy_io from maya import cmds # noqa -class CreateUnrealStaticMesh(plugin.Creator): +class CreateUnrealStaticMesh(plugin.MayaCreator): """Unreal Static Meshes with collisions.""" - name = "staticMeshMain" + + identifier = "io.openpype.creators.maya.unrealstaticmesh" label = "Unreal - Static Mesh" family = "staticMesh" icon = "cube" dynamic_subset_keys = ["asset"] - def __init__(self, *args, **kwargs): - """Constructor.""" - super(CreateUnrealStaticMesh, self).__init__(*args, **kwargs) - self._project_settings = get_project_settings( - legacy_io.Session["AVALON_PROJECT"]) + # Defined in settings + collision_prefixes = [] + + def apply_settings(self, project_settings, system_settings): + """Apply project settings to creator""" + settings = project_settings["maya"]["create"]["CreateUnrealStaticMesh"] + self.collision_prefixes = settings["collision_prefixes"] - @classmethod def get_dynamic_data( - cls, variant, task_name, asset_id, project_name, host_name + self, variant, task_name, asset_doc, project_name, host_name, instance ): - dynamic_data = super(CreateUnrealStaticMesh, cls).get_dynamic_data( - variant, task_name, asset_id, project_name, host_name + """ + The default subset name templates for Unreal include {asset} and thus + we should pass that along as dynamic data. + """ + dynamic_data = super(CreateUnrealStaticMesh, self).get_dynamic_data( + variant, task_name, asset_doc, project_name, host_name, instance ) - dynamic_data["asset"] = legacy_io.Session.get("AVALON_ASSET") + dynamic_data["asset"] = asset_doc["name"] return dynamic_data - def process(self): - self.name = "{}_{}".format(self.family, self.name) - with lib.undo_chunk(): - instance = super(CreateUnrealStaticMesh, self).process() - content = cmds.sets(instance, query=True) + def create(self, subset_name, instance_data, pre_create_data): + + with lib.undo_chunk(): + instance = super(CreateUnrealStaticMesh, self).create( + subset_name, instance_data, pre_create_data) + instance_node = instance.get("instance_node") + + # We reorganize the geometry that was originally added into the + # set into either 'collision_SET' or 'geometry_SET' based on the + # collision_prefixes from project settings + members = cmds.sets(instance_node, query=True) + cmds.sets(clear=instance_node) - # empty set and process its former content - cmds.sets(content, rm=instance) geometry_set = cmds.sets(name="geometry_SET", empty=True) collisions_set = cmds.sets(name="collisions_SET", empty=True) - cmds.sets([geometry_set, collisions_set], forceElement=instance) + cmds.sets([geometry_set, collisions_set], + forceElement=instance_node) - members = cmds.ls(content, long=True) or [] + members = cmds.ls(members, long=True) or [] children = cmds.listRelatives(members, allDescendents=True, fullPath=True) or [] - children = cmds.ls(children, type="transform") - for node in children: - if cmds.listRelatives(node, type="shape"): - if [ - n for n in self.collision_prefixes - if node.startswith(n) - ]: - cmds.sets(node, forceElement=collisions_set) - else: - cmds.sets(node, forceElement=geometry_set) + transforms = cmds.ls(members + children, type="transform") + for transform in transforms: + + if not cmds.listRelatives(transform, + type="shape", + noIntermediate=True): + # Exclude all transforms that have no direct shapes + continue + + if self.has_collision_prefix(transform): + cmds.sets(transform, forceElement=collisions_set) + else: + cmds.sets(transform, forceElement=geometry_set) + + def has_collision_prefix(self, node_path): + """Return whether node name of path matches collision prefix. + + If the node name matches the collision prefix we add it to the + `collisions_SET` instead of the `geometry_SET`. + + Args: + node_path (str): Maya node path. + + Returns: + bool: Whether the node should be considered a collision mesh. + + """ + node_name = node_path.rsplit("|", 1)[-1] + for prefix in self.collision_prefixes: + if node_name.startswith(prefix): + return True + return False diff --git a/openpype/hosts/maya/plugins/create/create_vrayproxy.py b/openpype/hosts/maya/plugins/create/create_vrayproxy.py index d135073e82..b0a95538e1 100644 --- a/openpype/hosts/maya/plugins/create/create_vrayproxy.py +++ b/openpype/hosts/maya/plugins/create/create_vrayproxy.py @@ -1,10 +1,14 @@ -from openpype.hosts.maya.api import plugin +from openpype.hosts.maya.api import ( + plugin, + lib +) +from openpype.lib import BoolDef -class CreateVrayProxy(plugin.Creator): +class CreateVrayProxy(plugin.MayaCreator): """Alembic pointcache for animated data""" - name = "vrayproxy" + identifier = "io.openpype.creators.maya.vrayproxy" label = "VRay Proxy" family = "vrayproxy" icon = "gears" @@ -12,15 +16,35 @@ class CreateVrayProxy(plugin.Creator): vrmesh = True alembic = True - def __init__(self, *args, **kwargs): - super(CreateVrayProxy, self).__init__(*args, **kwargs) + def get_instance_attr_defs(self): - self.data["animation"] = False - self.data["frameStart"] = 1 - self.data["frameEnd"] = 1 + defs = [ + BoolDef("animation", + label="Export Animation", + default=False) + ] - # Write vertex colors - self.data["vertexColors"] = False + # Add time range attributes but remove some attributes + # which this instance actually doesn't use + defs.extend(lib.collect_animation_defs()) + remove = {"handleStart", "handleEnd", "step"} + defs = [attr_def for attr_def in defs if attr_def.key not in remove] - self.data["vrmesh"] = self.vrmesh - self.data["alembic"] = self.alembic + defs.extend([ + BoolDef("vertexColors", + label="Write vertex colors", + tooltip="Write vertex colors with the geometry", + default=False), + BoolDef("vrmesh", + label="Export VRayMesh", + tooltip="Publish a .vrmesh (VRayMesh) file for " + "this VRayProxy", + default=self.vrmesh), + BoolDef("alembic", + label="Export Alembic", + tooltip="Publish a .abc (Alembic) file for " + "this VRayProxy", + default=self.alembic), + ]) + + return defs diff --git a/openpype/hosts/maya/plugins/create/create_vrayscene.py b/openpype/hosts/maya/plugins/create/create_vrayscene.py index 59d80e6d5b..d601dceb54 100644 --- a/openpype/hosts/maya/plugins/create/create_vrayscene.py +++ b/openpype/hosts/maya/plugins/create/create_vrayscene.py @@ -1,266 +1,52 @@ # -*- coding: utf-8 -*- """Create instance of vrayscene.""" -import os -import json -import appdirs -import requests - -from maya import cmds -import maya.app.renderSetup.model.renderSetup as renderSetup from openpype.hosts.maya.api import ( - lib, + lib_rendersettings, plugin ) -from openpype.settings import ( - get_system_settings, - get_project_settings -) - -from openpype.lib import requests_get -from openpype.pipeline import ( - CreatorError, - legacy_io, -) -from openpype.modules import ModulesManager +from openpype.pipeline import CreatorError +from openpype.lib import BoolDef -class CreateVRayScene(plugin.Creator): +class CreateVRayScene(plugin.RenderlayerCreator): """Create Vray Scene.""" - label = "VRay Scene" + identifier = "io.openpype.creators.maya.vrayscene" + family = "vrayscene" + label = "VRay Scene" icon = "cubes" - _project_settings = None + render_settings = {} + singleton_node_name = "vraysceneMain" - def __init__(self, *args, **kwargs): - """Entry.""" - super(CreateVRayScene, self).__init__(*args, **kwargs) - self._rs = renderSetup.instance() - self.data["exportOnFarm"] = False - deadline_settings = get_system_settings()["modules"]["deadline"] + @classmethod + def apply_settings(cls, project_settings, system_settings): + cls.render_settings = project_settings["maya"]["RenderSettings"] - manager = ModulesManager() - self.deadline_module = manager.modules_by_name["deadline"] + def create(self, subset_name, instance_data, pre_create_data): + # Only allow a single render instance to exist + if self._get_singleton_node(): + raise CreatorError("A Render instance already exists - only " + "one can be configured.") - if not deadline_settings["enabled"]: - self.deadline_servers = {} - return - self._project_settings = get_project_settings( - legacy_io.Session["AVALON_PROJECT"]) + super(CreateVRayScene, self).create(subset_name, + instance_data, + pre_create_data) - try: - default_servers = deadline_settings["deadline_urls"] - project_servers = ( - self._project_settings["deadline"]["deadline_servers"] - ) - self.deadline_servers = { - k: default_servers[k] - for k in project_servers - if k in default_servers - } + # Apply default project render settings on create + if self.render_settings.get("apply_render_settings"): + lib_rendersettings.RenderSettings().set_default_renderer_settings() - if not self.deadline_servers: - self.deadline_servers = default_servers + def get_instance_attr_defs(self): + """Create instance settings.""" - except AttributeError: - # Handle situation were we had only one url for deadline. - # get default deadline webservice url from deadline module - self.deadline_servers = self.deadline_module.deadline_urls - - def process(self): - """Entry point.""" - exists = cmds.ls(self.name) - if exists: - return cmds.warning("%s already exists." % exists[0]) - - use_selection = self.options.get("useSelection") - with lib.undo_chunk(): - self._create_vray_instance_settings() - self.instance = super(CreateVRayScene, self).process() - - index = 1 - namespace_name = "_{}".format(str(self.instance)) - try: - cmds.namespace(rm=namespace_name) - except RuntimeError: - # namespace is not empty, so we leave it untouched - pass - - while(cmds.namespace(exists=namespace_name)): - namespace_name = "_{}{}".format(str(self.instance), index) - index += 1 - - namespace = cmds.namespace(add=namespace_name) - - # add Deadline server selection list - if self.deadline_servers: - cmds.scriptJob( - attributeChange=[ - "{}.deadlineServers".format(self.instance), - self._deadline_webservice_changed - ]) - - # create namespace with instance - layers = self._rs.getRenderLayers() - if use_selection: - print(">>> processing existing layers") - sets = [] - for layer in layers: - print(" - creating set for {}".format(layer.name())) - render_set = cmds.sets( - n="{}:{}".format(namespace, layer.name())) - sets.append(render_set) - cmds.sets(sets, forceElement=self.instance) - - # if no render layers are present, create default one with - # asterix selector - if not layers: - render_layer = self._rs.createRenderLayer('Main') - collection = render_layer.createCollection("defaultCollection") - collection.getSelector().setPattern('*') - - def _deadline_webservice_changed(self): - """Refresh Deadline server dependent options.""" - # get selected server - from maya import cmds - webservice = self.deadline_servers[ - self.server_aliases[ - cmds.getAttr("{}.deadlineServers".format(self.instance)) - ] + return [ + BoolDef("vraySceneMultipleFiles", + label="V-Ray Scene Multiple Files", + default=False), + BoolDef("exportOnFarm", + label="Export on farm", + default=False) ] - pools = self.deadline_module.get_deadline_pools(webservice) - cmds.deleteAttr("{}.primaryPool".format(self.instance)) - cmds.deleteAttr("{}.secondaryPool".format(self.instance)) - cmds.addAttr(self.instance, longName="primaryPool", - attributeType="enum", - enumName=":".join(pools)) - cmds.addAttr(self.instance, longName="secondaryPool", - attributeType="enum", - enumName=":".join(["-"] + pools)) - - def _create_vray_instance_settings(self): - # get pools - pools = [] - - system_settings = get_system_settings()["modules"] - - deadline_enabled = system_settings["deadline"]["enabled"] - muster_enabled = system_settings["muster"]["enabled"] - muster_url = system_settings["muster"]["MUSTER_REST_URL"] - - if deadline_enabled and muster_enabled: - self.log.error( - "Both Deadline and Muster are enabled. " "Cannot support both." - ) - raise CreatorError("Both Deadline and Muster are enabled") - - self.server_aliases = self.deadline_servers.keys() - self.data["deadlineServers"] = self.server_aliases - - if deadline_enabled: - # if default server is not between selected, use first one for - # initial list of pools. - try: - deadline_url = self.deadline_servers["default"] - except KeyError: - deadline_url = [ - self.deadline_servers[k] - for k in self.deadline_servers.keys() - ][0] - - pool_names = self.deadline_module.get_deadline_pools(deadline_url) - - if muster_enabled: - self.log.info(">>> Loading Muster credentials ...") - self._load_credentials() - self.log.info(">>> Getting pools ...") - try: - pools = self._get_muster_pools() - except requests.exceptions.HTTPError as e: - if e.startswith("401"): - self.log.warning("access token expired") - self._show_login() - raise CreatorError("Access token expired") - except requests.exceptions.ConnectionError: - self.log.error("Cannot connect to Muster API endpoint.") - raise CreatorError("Cannot connect to {}".format(muster_url)) - pool_names = [] - for pool in pools: - self.log.info(" - pool: {}".format(pool["name"])) - pool_names.append(pool["name"]) - - self.data["primaryPool"] = pool_names - - self.data["suspendPublishJob"] = False - self.data["priority"] = 50 - self.data["whitelist"] = False - self.data["machineList"] = "" - self.data["vraySceneMultipleFiles"] = False - self.options = {"useSelection": False} # Force no content - - def _load_credentials(self): - """Load Muster credentials. - - Load Muster credentials from file and set ``MUSTER_USER``, - ``MUSTER_PASSWORD``, ``MUSTER_REST_URL`` is loaded from presets. - - Raises: - CreatorError: If loaded credentials are invalid. - AttributeError: If ``MUSTER_REST_URL`` is not set. - - """ - app_dir = os.path.normpath(appdirs.user_data_dir("pype-app", "pype")) - file_name = "muster_cred.json" - fpath = os.path.join(app_dir, file_name) - file = open(fpath, "r") - muster_json = json.load(file) - self._token = muster_json.get("token", None) - if not self._token: - self._show_login() - raise CreatorError("Invalid access token for Muster") - file.close() - self.MUSTER_REST_URL = os.environ.get("MUSTER_REST_URL") - if not self.MUSTER_REST_URL: - raise AttributeError("Muster REST API url not set") - - def _get_muster_pools(self): - """Get render pools from Muster. - - Raises: - CreatorError: If pool list cannot be obtained from Muster. - - """ - params = {"authToken": self._token} - api_entry = "/api/pools/list" - response = requests_get(self.MUSTER_REST_URL + api_entry, - params=params) - if response.status_code != 200: - if response.status_code == 401: - self.log.warning("Authentication token expired.") - self._show_login() - else: - self.log.error( - ("Cannot get pools from " - "Muster: {}").format(response.status_code) - ) - raise CreatorError("Cannot get pools from Muster") - try: - pools = response.json()["ResponseData"]["pools"] - except ValueError as e: - self.log.error("Invalid response from Muster server {}".format(e)) - raise CreatorError("Invalid response from Muster server") - - return pools - - def _show_login(self): - # authentication token expired so we need to login to Muster - # again to get it. We use Pype API call to show login window. - api_url = "{}/muster/show_login".format( - os.environ["OPENPYPE_WEBSERVER_URL"]) - self.log.debug(api_url) - login_response = requests_get(api_url, timeout=1) - if login_response.status_code != 200: - self.log.error("Cannot show login form to Muster") - raise CreatorError("Cannot show login form to Muster") diff --git a/openpype/hosts/maya/plugins/create/create_workfile.py b/openpype/hosts/maya/plugins/create/create_workfile.py new file mode 100644 index 0000000000..d84753cd7f --- /dev/null +++ b/openpype/hosts/maya/plugins/create/create_workfile.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- +"""Creator plugin for creating workfiles.""" +from openpype.pipeline import CreatedInstance, AutoCreator +from openpype.client import get_asset_by_name +from openpype.hosts.maya.api import plugin +from maya import cmds + + +class CreateWorkfile(plugin.MayaCreatorBase, AutoCreator): + """Workfile auto-creator.""" + identifier = "io.openpype.creators.maya.workfile" + label = "Workfile" + family = "workfile" + icon = "fa5.file" + + default_variant = "Main" + + def create(self): + + variant = self.default_variant + current_instance = next( + ( + instance for instance in self.create_context.instances + if instance.creator_identifier == self.identifier + ), None) + + project_name = self.project_name + asset_name = self.create_context.get_current_asset_name() + task_name = self.create_context.get_current_task_name() + host_name = self.create_context.host_name + + if current_instance is None: + asset_doc = get_asset_by_name(project_name, asset_name) + subset_name = self.get_subset_name( + variant, task_name, asset_doc, project_name, host_name + ) + data = { + "asset": asset_name, + "task": task_name, + "variant": variant + } + data.update( + self.get_dynamic_data( + variant, task_name, asset_doc, + project_name, host_name, current_instance) + ) + self.log.info("Auto-creating workfile instance...") + current_instance = CreatedInstance( + self.family, subset_name, data, self + ) + self._add_instance_to_context(current_instance) + elif ( + current_instance["asset"] != asset_name + or current_instance["task"] != task_name + ): + # Update instance context if is not the same + asset_doc = get_asset_by_name(project_name, asset_name) + subset_name = self.get_subset_name( + variant, task_name, asset_doc, project_name, host_name + ) + current_instance["asset"] = asset_name + current_instance["task"] = task_name + current_instance["subset"] = subset_name + + def collect_instances(self): + self.cache_subsets(self.collection_shared_data) + cached_subsets = self.collection_shared_data["maya_cached_subsets"] + for node in cached_subsets.get(self.identifier, []): + node_data = self.read_instance_node(node) + + created_instance = CreatedInstance.from_existing(node_data, self) + self._add_instance_to_context(created_instance) + + def update_instances(self, update_list): + for created_inst, _changes in update_list: + data = created_inst.data_to_store() + node = data.get("instance_node") + if not node: + node = self.create_node() + created_inst["instance_node"] = node + data = created_inst.data_to_store() + + self.imprint_instance_node(node, data) + + def create_node(self): + node = cmds.sets(empty=True, name="workfileMain") + cmds.setAttr(node + ".hiddenInOutliner", True) + return node diff --git a/openpype/hosts/maya/plugins/create/create_xgen.py b/openpype/hosts/maya/plugins/create/create_xgen.py index 70e23cf47b..eaafb0959a 100644 --- a/openpype/hosts/maya/plugins/create/create_xgen.py +++ b/openpype/hosts/maya/plugins/create/create_xgen.py @@ -1,10 +1,10 @@ from openpype.hosts.maya.api import plugin -class CreateXgen(plugin.Creator): +class CreateXgen(plugin.MayaCreator): """Xgen""" - name = "xgen" + identifier = "io.openpype.creators.maya.xgen" label = "Xgen" family = "xgen" icon = "pagelines" diff --git a/openpype/hosts/maya/plugins/create/create_yeti_cache.py b/openpype/hosts/maya/plugins/create/create_yeti_cache.py index e8c3203f21..395aa62325 100644 --- a/openpype/hosts/maya/plugins/create/create_yeti_cache.py +++ b/openpype/hosts/maya/plugins/create/create_yeti_cache.py @@ -1,15 +1,14 @@ -from collections import OrderedDict - from openpype.hosts.maya.api import ( lib, plugin ) +from openpype.lib import NumberDef -class CreateYetiCache(plugin.Creator): +class CreateYetiCache(plugin.MayaCreator): """Output for procedural plugin nodes of Yeti """ - name = "yetiDefault" + identifier = "io.openpype.creators.maya.yeticache" label = "Yeti Cache" family = "yeticache" icon = "pagelines" @@ -17,14 +16,23 @@ class CreateYetiCache(plugin.Creator): def __init__(self, *args, **kwargs): super(CreateYetiCache, self).__init__(*args, **kwargs) - self.data["preroll"] = 0 + defs = [ + NumberDef("preroll", + label="Preroll", + minimum=0, + default=0, + decimals=0) + ] # Add animation data without step and handles - anim_data = lib.collect_animation_data() - anim_data.pop("step") - anim_data.pop("handleStart") - anim_data.pop("handleEnd") - self.data.update(anim_data) + defs.extend(lib.collect_animation_defs()) + remove = {"step", "handleStart", "handleEnd"} + defs = [attr_def for attr_def in defs if attr_def.key not in remove] - # Add samples - self.data["samples"] = 3 + # Add samples after frame range + defs.append( + NumberDef("samples", + label="Samples", + default=3, + decimals=0) + ) diff --git a/openpype/hosts/maya/plugins/create/create_yeti_rig.py b/openpype/hosts/maya/plugins/create/create_yeti_rig.py index 7abe2988cd..445bcf46d8 100644 --- a/openpype/hosts/maya/plugins/create/create_yeti_rig.py +++ b/openpype/hosts/maya/plugins/create/create_yeti_rig.py @@ -6,18 +6,22 @@ from openpype.hosts.maya.api import ( ) -class CreateYetiRig(plugin.Creator): +class CreateYetiRig(plugin.MayaCreator): """Output for procedural plugin nodes ( Yeti / XGen / etc)""" + identifier = "io.openpype.creators.maya.yetirig" label = "Yeti Rig" family = "yetiRig" icon = "usb" - def process(self): + def create(self, subset_name, instance_data, pre_create_data): with lib.undo_chunk(): - instance = super(CreateYetiRig, self).process() + instance = super(CreateYetiRig, self).create(subset_name, + instance_data, + pre_create_data) + instance_node = instance.get("instance_node") self.log.info("Creating Rig instance set up ...") input_meshes = cmds.sets(name="input_SET", empty=True) - cmds.sets(input_meshes, forceElement=instance) + cmds.sets(input_meshes, forceElement=instance_node) diff --git a/openpype/hosts/maya/plugins/load/load_reference.py b/openpype/hosts/maya/plugins/load/load_reference.py index 74ca27ff3c..deadd5b9d3 100644 --- a/openpype/hosts/maya/plugins/load/load_reference.py +++ b/openpype/hosts/maya/plugins/load/load_reference.py @@ -221,6 +221,7 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): self._lock_camera_transforms(members) def _post_process_rig(self, name, namespace, context, options): + nodes = self[:] create_rig_animation_instance( nodes, context, namespace, options=options, log=self.log diff --git a/openpype/hosts/maya/plugins/publish/collect_current_file.py b/openpype/hosts/maya/plugins/publish/collect_current_file.py new file mode 100644 index 0000000000..e777a209d4 --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/collect_current_file.py @@ -0,0 +1,17 @@ + +import pyblish.api + +from maya import cmds + + +class CollectCurrentFile(pyblish.api.ContextPlugin): + """Inject the current working file.""" + + order = pyblish.api.CollectorOrder - 0.4 + label = "Maya Current File" + hosts = ['maya'] + families = ["workfile"] + + def process(self, context): + """Inject the current working file""" + context.data['currentFile'] = cmds.file(query=True, sceneName=True) diff --git a/openpype/hosts/maya/plugins/publish/collect_inputs.py b/openpype/hosts/maya/plugins/publish/collect_inputs.py index 895c92762b..30ed21da9c 100644 --- a/openpype/hosts/maya/plugins/publish/collect_inputs.py +++ b/openpype/hosts/maya/plugins/publish/collect_inputs.py @@ -172,7 +172,7 @@ class CollectUpstreamInputs(pyblish.api.InstancePlugin): """Collects inputs from nodes in renderlayer, incl. shaders + camera""" # Get the renderlayer - renderlayer = instance.data.get("setMembers") + renderlayer = instance.data.get("renderlayer") if renderlayer == "defaultRenderLayer": # Assume all loaded containers in the scene are inputs diff --git a/openpype/hosts/maya/plugins/publish/collect_instances.py b/openpype/hosts/maya/plugins/publish/collect_instances.py index 74bdc11a2c..5f914b40d7 100644 --- a/openpype/hosts/maya/plugins/publish/collect_instances.py +++ b/openpype/hosts/maya/plugins/publish/collect_instances.py @@ -1,12 +1,11 @@ from maya import cmds import pyblish.api -import json from openpype.hosts.maya.api.lib import get_all_children -class CollectInstances(pyblish.api.ContextPlugin): - """Gather instances by objectSet and pre-defined attribute +class CollectNewInstances(pyblish.api.InstancePlugin): + """Gather members for instances and pre-defined attribute This collector takes into account assets that are associated with an objectSet and marked with a unique identifier; @@ -25,134 +24,70 @@ class CollectInstances(pyblish.api.ContextPlugin): """ - label = "Collect Instances" + label = "Collect New Instance Data" order = pyblish.api.CollectorOrder hosts = ["maya"] - def process(self, context): + def process(self, instance): - objectset = cmds.ls("*.id", long=True, type="objectSet", - recursive=True, objectsOnly=True) + objset = instance.data.get("instance_node") + if not objset: + self.log.debug("Instance has no `instance_node` data") - context.data['objectsets'] = objectset - for objset in objectset: - - if not cmds.attributeQuery("id", node=objset, exists=True): - continue - - id_attr = "{}.id".format(objset) - if cmds.getAttr(id_attr) != "pyblish.avalon.instance": - continue - - # The developer is responsible for specifying - # the family of each instance. - has_family = cmds.attributeQuery("family", - node=objset, - exists=True) - assert has_family, "\"%s\" was missing a family" % objset - - members = cmds.sets(objset, query=True) - if members is None: - self.log.warning("Skipped empty instance: \"%s\" " % objset) - continue - - self.log.info("Creating instance for {}".format(objset)) - - data = dict() - - # Apply each user defined attribute as data - for attr in cmds.listAttr(objset, userDefined=True) or list(): - try: - value = cmds.getAttr("%s.%s" % (objset, attr)) - except Exception: - # Some attributes cannot be read directly, - # such as mesh and color attributes. These - # are considered non-essential to this - # particular publishing pipeline. - value = None - data[attr] = value - - # temporarily translation of `active` to `publish` till issue has - # been resolved, https://github.com/pyblish/pyblish-base/issues/307 - if "active" in data: - data["publish"] = data["active"] + # TODO: We might not want to do this in the future + # Merge creator attributes into instance.data just backwards compatible + # code still runs as expected + creator_attributes = instance.data.get("creator_attributes", {}) + if creator_attributes: + instance.data.update(creator_attributes) + members = cmds.sets(objset, query=True) or [] + if members: # Collect members members = cmds.ls(members, long=True) or [] dag_members = cmds.ls(members, type="dagNode", long=True) children = get_all_children(dag_members) children = cmds.ls(children, noIntermediate=True, long=True) - - parents = [] - if data.get("includeParentHierarchy", True): - # If `includeParentHierarchy` then include the parents - # so they will also be picked up in the instance by validators - parents = self.get_all_parents(members) + parents = ( + self.get_all_parents(members) + if creator_attributes.get("includeParentHierarchy", True) + else [] + ) members_hierarchy = list(set(members + children + parents)) - if 'families' not in data: - data['families'] = [data.get('family')] - - # Create the instance - instance = context.create_instance(objset) instance[:] = members_hierarchy - instance.data["objset"] = objset - # Store the exact members of the object set - instance.data["setMembers"] = members + elif instance.data["family"] != "workfile": + self.log.warning("Empty instance: \"%s\" " % objset) + # Store the exact members of the object set + instance.data["setMembers"] = members - # Define nice label - name = cmds.ls(objset, long=False)[0] # use short name - label = "{0} ({1})".format(name, data["asset"]) + # TODO: This might make more sense as a separate collector + # Convert frame values to integers + for attr_name in ( + "handleStart", "handleEnd", "frameStart", "frameEnd", + ): + value = instance.data.get(attr_name) + if value is not None: + instance.data[attr_name] = int(value) - # Convert frame values to integers - for attr_name in ( - "handleStart", "handleEnd", "frameStart", "frameEnd", - ): - value = data.get(attr_name) - if value is not None: - data[attr_name] = int(value) + # Append start frame and end frame to label if present + if "frameStart" in instance.data and "frameEnd" in instance.data: + # Take handles from context if not set locally on the instance + for key in ["handleStart", "handleEnd"]: + if key not in instance.data: + value = instance.context.data[key] + if value is not None: + value = int(value) + instance.data[key] = value - # Append start frame and end frame to label if present - if "frameStart" in data and "frameEnd" in data: - # Take handles from context if not set locally on the instance - for key in ["handleStart", "handleEnd"]: - if key not in data: - value = context.data[key] - if value is not None: - value = int(value) - data[key] = value - - data["frameStartHandle"] = int( - data["frameStart"] - data["handleStart"] - ) - data["frameEndHandle"] = int( - data["frameEnd"] + data["handleEnd"] - ) - - label += " [{0}-{1}]".format( - data["frameStartHandle"], data["frameEndHandle"] - ) - - instance.data["label"] = label - instance.data.update(data) - self.log.debug("{}".format(instance.data)) - - # Produce diagnostic message for any graphical - # user interface interested in visualising it. - self.log.info("Found: \"%s\" " % instance.data["name"]) - self.log.debug( - "DATA: {} ".format(json.dumps(instance.data, indent=4))) - - def sort_by_family(instance): - """Sort by family""" - return instance.data.get("families", instance.data.get("family")) - - # Sort/grouped by family (preserving local index) - context[:] = sorted(context, key=sort_by_family) - - return context + instance.data["frameStartHandle"] = int( + instance.data["frameStart"] - instance.data["handleStart"] + ) + instance.data["frameEndHandle"] = int( + instance.data["frameEnd"] + instance.data["handleEnd"] + ) def get_all_parents(self, nodes): """Get all parents by using string operations (optimization) diff --git a/openpype/hosts/maya/plugins/publish/collect_look.py b/openpype/hosts/maya/plugins/publish/collect_look.py index 287ddc228b..b3da920566 100644 --- a/openpype/hosts/maya/plugins/publish/collect_look.py +++ b/openpype/hosts/maya/plugins/publish/collect_look.py @@ -285,17 +285,17 @@ class CollectLook(pyblish.api.InstancePlugin): instance: Instance to collect. """ - self.log.info("Looking for look associations " + self.log.debug("Looking for look associations " "for %s" % instance.data['name']) # Discover related object sets - self.log.info("Gathering sets ...") + self.log.debug("Gathering sets ...") sets = self.collect_sets(instance) # Lookup set (optimization) instance_lookup = set(cmds.ls(instance, long=True)) - self.log.info("Gathering set relations ...") + self.log.debug("Gathering set relations ...") # Ensure iteration happen in a list so we can remove keys from the # dict within the loop @@ -308,7 +308,7 @@ class CollectLook(pyblish.api.InstancePlugin): # if node is specified as renderer node type, it will be # serialized with its attributes. if cmds.nodeType(obj_set) in RENDERER_NODE_TYPES: - self.log.info("- {} is {}".format( + self.log.debug("- {} is {}".format( obj_set, cmds.nodeType(obj_set))) node_attrs = [] @@ -354,13 +354,13 @@ class CollectLook(pyblish.api.InstancePlugin): # Remove sets that didn't have any members assigned in the end # Thus the data will be limited to only what we need. - self.log.info("obj_set {}".format(sets[obj_set])) + self.log.debug("obj_set {}".format(sets[obj_set])) if not sets[obj_set]["members"]: self.log.info( "Removing redundant set information: {}".format(obj_set)) sets.pop(obj_set, None) - self.log.info("Gathering attribute changes to instance members..") + self.log.debug("Gathering attribute changes to instance members..") attributes = self.collect_attributes_changed(instance) # Store data on the instance @@ -433,14 +433,14 @@ class CollectLook(pyblish.api.InstancePlugin): for node_type in all_supported_nodes: files.extend(cmds.ls(history, type=node_type, long=True)) - self.log.info("Collected file nodes:\n{}".format(files)) + self.log.debug("Collected file nodes:\n{}".format(files)) # Collect textures if any file nodes are found instance.data["resources"] = [] for n in files: for res in self.collect_resources(n): instance.data["resources"].append(res) - self.log.info("Collected resources: {}".format( + self.log.debug("Collected resources: {}".format( instance.data["resources"])) # Log warning when no relevant sets were retrieved for the look. @@ -536,7 +536,7 @@ class CollectLook(pyblish.api.InstancePlugin): # Collect changes to "custom" attributes node_attrs = get_look_attrs(node) - self.log.info( + self.log.debug( "Node \"{0}\" attributes: {1}".format(node, node_attrs) ) diff --git a/openpype/hosts/maya/plugins/publish/collect_pointcache.py b/openpype/hosts/maya/plugins/publish/collect_pointcache.py index d0430c5612..bb9065792f 100644 --- a/openpype/hosts/maya/plugins/publish/collect_pointcache.py +++ b/openpype/hosts/maya/plugins/publish/collect_pointcache.py @@ -16,14 +16,16 @@ class CollectPointcache(pyblish.api.InstancePlugin): instance.data["families"].append("publish.farm") proxy_set = None - for node in instance.data["setMembers"]: - if cmds.nodeType(node) != "objectSet": - continue - members = cmds.sets(node, query=True) - if members is None: - self.log.warning("Skipped empty objectset: \"%s\" " % node) - continue + for node in cmds.ls(instance.data["setMembers"], + exactType="objectSet"): + # Find proxy_SET objectSet in the instance for proxy meshes if node.endswith("proxy_SET"): + members = cmds.sets(node, query=True) + if members is None: + self.log.debug("Skipped empty proxy_SET: \"%s\" " % node) + continue + self.log.debug("Found proxy set: {}".format(node)) + proxy_set = node instance.data["proxy"] = [] instance.data["proxyRoots"] = [] @@ -36,8 +38,9 @@ class CollectPointcache(pyblish.api.InstancePlugin): cmds.listRelatives(member, shapes=True, fullPath=True) ) self.log.debug( - "proxy members: {}".format(instance.data["proxy"]) + "Found proxy members: {}".format(instance.data["proxy"]) ) + break if proxy_set: instance.remove(proxy_set) diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index babd494758..2b31e61d6d 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -39,27 +39,29 @@ Provides: instance -> pixelAspect """ -import re import os import platform import json from maya import cmds -import maya.app.renderSetup.model.renderSetup as renderSetup import pyblish.api +from openpype.pipeline import KnownPublishError from openpype.lib import get_formatted_current_time -from openpype.pipeline import legacy_io -from openpype.hosts.maya.api.lib_renderproducts import get as get_layer_render_products # noqa: E501 +from openpype.hosts.maya.api.lib_renderproducts import ( + get as get_layer_render_products, + UnsupportedRendererException +) from openpype.hosts.maya.api import lib -class CollectMayaRender(pyblish.api.ContextPlugin): +class CollectMayaRender(pyblish.api.InstancePlugin): """Gather all publishable render layers from renderSetup.""" order = pyblish.api.CollectorOrder + 0.01 hosts = ["maya"] + families = ["renderlayer"] label = "Collect Render Layers" sync_workfile_version = False @@ -69,388 +71,251 @@ class CollectMayaRender(pyblish.api.ContextPlugin): "underscore": "_" } - def process(self, context): - """Entry point to collector.""" - render_instance = None + def process(self, instance): - for instance in context: - if "rendering" in instance.data["families"]: - render_instance = instance - render_instance.data["remove"] = True + # 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 - # make sure workfile instance publishing is enabled - if "workfile" in instance.data["families"]: - instance.data["publish"] = True - - if not render_instance: - self.log.info( - "No render instance found, skipping render " - "layer collection." - ) - return - - render_globals = render_instance - collected_render_layers = render_instance.data["setMembers"] + layer = instance.data["transientData"]["layer"] + objset = instance.data.get("instance_node") filepath = context.data["currentFile"].replace("\\", "/") - asset = legacy_io.Session["AVALON_ASSET"] workspace = context.data["workspaceDir"] - # Retrieve render setup layers - rs = renderSetup.instance() - maya_render_layers = { - layer.name(): layer for layer in rs.getRenderLayers() + # check if layer is renderable + if not layer.isRenderable(): + msg = "Render layer [ {} ] is not " "renderable".format( + layer.name() + ) + self.log.warning(msg) + + # detect if there are sets (subsets) to attach render to + sets = cmds.sets(objset, query=True) or [] + attach_to = [] + for s in sets: + if not cmds.attributeQuery("family", node=s, exists=True): + continue + + attach_to.append( + { + "version": None, # we need integrator for that + "subset": s, + "family": cmds.getAttr("{}.family".format(s)), + } + ) + self.log.info(" -> attach render to: {}".format(s)) + + layer_name = layer.name() + + # collect all frames we are expecting to be rendered + # return all expected files for all cameras and aovs in given + # frame range + try: + layer_render_products = get_layer_render_products(layer.name()) + except UnsupportedRendererException as exc: + raise KnownPublishError(exc) + render_products = layer_render_products.layer_data.products + assert render_products, "no render products generated" + expected_files = [] + multipart = False + for product in render_products: + if product.multipart: + multipart = True + product_name = product.productName + if product.camera and layer_render_products.has_camera_token(): + product_name = "{}{}".format( + product.camera, + "_{}".format(product_name) if product_name else "") + expected_files.append( + { + product_name: layer_render_products.get_files( + product) + }) + + has_cameras = any(product.camera for product in render_products) + assert has_cameras, "No render cameras found." + + self.log.info("multipart: {}".format( + multipart)) + assert expected_files, "no file names were generated, this is a bug" + self.log.info( + "expected files: {}".format( + json.dumps(expected_files, indent=4, sort_keys=True) + ) + ) + + # if we want to attach render to subset, check if we have AOV's + # in expectedFiles. If so, raise error as we cannot attach AOV + # (considered to be subset on its own) to another subset + if attach_to: + assert isinstance(expected_files, list), ( + "attaching multiple AOVs or renderable cameras to " + "subset is not supported" + ) + + # append full path + aov_dict = {} + default_render_folder = context.data.get("project_settings")\ + .get("maya")\ + .get("RenderSettings")\ + .get("default_render_image_folder") or "" + # replace relative paths with absolute. Render products are + # returned as list of dictionaries. + publish_meta_path = None + for aov in expected_files: + full_paths = [] + aov_first_key = list(aov.keys())[0] + for file in aov[aov_first_key]: + full_path = os.path.join(workspace, default_render_folder, + file) + full_path = full_path.replace("\\", "/") + full_paths.append(full_path) + publish_meta_path = os.path.dirname(full_path) + aov_dict[aov_first_key] = full_paths + full_exp_files = [aov_dict] + self.log.info(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)) + frame_end_render = int(self.get_render_attribute( + "endFrame", layer=layer_name)) + + if (int(context.data["frameStartHandle"]) == frame_start_render + and int(context.data["frameEndHandle"]) == frame_end_render): # noqa: W503, E501 + + handle_start = context.data["handleStart"] + handle_end = context.data["handleEnd"] + frame_start = context.data["frameStart"] + frame_end = context.data["frameEnd"] + frame_start_handle = context.data["frameStartHandle"] + frame_end_handle = context.data["frameEndHandle"] + else: + handle_start = 0 + handle_end = 0 + frame_start = frame_start_render + frame_end = frame_end_render + frame_start_handle = frame_start_render + frame_end_handle = frame_end_render + + # find common path to store metadata + # so if image prefix is branching to many directories + # metadata file will be located in top-most common + # directory. + # TODO: use `os.path.commonpath()` after switch to Python 3 + publish_meta_path = os.path.normpath(publish_meta_path) + common_publish_meta_path = os.path.splitdrive( + publish_meta_path)[0] + if common_publish_meta_path: + common_publish_meta_path += os.path.sep + for part in publish_meta_path.replace( + common_publish_meta_path, "").split(os.path.sep): + common_publish_meta_path = os.path.join( + common_publish_meta_path, part) + if part == layer_name: + break + + # TODO: replace this terrible linux hotfix with real solution :) + if platform.system().lower() in ["linux", "darwin"]: + common_publish_meta_path = "/" + common_publish_meta_path + + self.log.info( + "Publish meta path: {}".format(common_publish_meta_path)) + + # Get layer specific settings, might be overrides + colorspace_data = lib.get_color_management_preferences() + data = { + "farm": True, + "attachTo": attach_to, + + "multipartExr": multipart, + "review": instance.data.get("review") or False, + + # Frame range + "handleStart": handle_start, + "handleEnd": handle_end, + "frameStart": frame_start, + "frameEnd": frame_end, + "frameStartHandle": frame_start_handle, + "frameEndHandle": frame_end_handle, + "byFrameStep": int( + self.get_render_attribute("byFrameStep", + layer=layer_name)), + + # Renderlayer + "renderer": self.get_render_attribute( + "currentRenderer", layer=layer_name).lower(), + "setMembers": layer._getLegacyNodeName(), # legacy renderlayer + "renderlayer": layer_name, + + # todo: is `time` and `author` still needed? + "time": get_formatted_current_time(), + "author": context.data["user"], + + # Add source to allow tracing back to the scene from + # which was submitted originally + "source": filepath, + "expectedFiles": full_exp_files, + "publishRenderMetadataFolder": common_publish_meta_path, + "renderProducts": layer_render_products, + "resolutionWidth": lib.get_attr_in_layer( + "defaultResolution.width", layer=layer_name + ), + "resolutionHeight": lib.get_attr_in_layer( + "defaultResolution.height", layer=layer_name + ), + "pixelAspect": lib.get_attr_in_layer( + "defaultResolution.pixelAspect", layer=layer_name + ), + + # todo: Following are likely not needed due to collecting from the + # instance itself if they are attribute definitions + "tileRendering": instance.data.get("tileRendering") or False, # noqa: E501 + "tilesX": instance.data.get("tilesX") or 2, + "tilesY": instance.data.get("tilesY") or 2, + "convertToScanline": instance.data.get( + "convertToScanline") or False, + "useReferencedAovs": instance.data.get( + "useReferencedAovs") or instance.data.get( + "vrayUseReferencedAovs") or False, + "aovSeparator": layer_render_products.layer_data.aov_separator, # noqa: E501 + "renderSetupIncludeLights": instance.data.get( + "renderSetupIncludeLights" + ), + "colorspaceConfig": colorspace_data["config"], + "colorspaceDisplay": colorspace_data["display"], + "colorspaceView": colorspace_data["view"], } - for layer in collected_render_layers: - if layer.startswith("LAYER_"): - # this is support for legacy mode where render layers - # started with `LAYER_` prefix. - layer_name_pattern = r"^LAYER_(.*)" - else: - # new way is to prefix render layer name with instance - # namespace. - layer_name_pattern = r"^.+:(.*)" + if self.sync_workfile_version: + data["version"] = context.data["version"] + for instance in context: + if instance.data['family'] == "workfile": + instance.data["version"] = context.data["version"] - # todo: We should have a more explicit way to link the renderlayer - match = re.match(layer_name_pattern, layer) - if not match: - msg = "Invalid layer name in set [ {} ]".format(layer) - self.log.warning(msg) - continue - - expected_layer_name = match.group(1) - self.log.info("Processing '{}' as layer [ {} ]" - "".format(layer, expected_layer_name)) - - # check if layer is part of renderSetup - if expected_layer_name not in maya_render_layers: - msg = "Render layer [ {} ] is not in " "Render Setup".format( - expected_layer_name - ) - self.log.warning(msg) - continue - - # check if layer is renderable - if not maya_render_layers[expected_layer_name].isRenderable(): - msg = "Render layer [ {} ] is not " "renderable".format( - expected_layer_name - ) - self.log.warning(msg) - continue - - # detect if there are sets (subsets) to attach render to - sets = cmds.sets(layer, query=True) or [] - attach_to = [] - for s in sets: - if not cmds.attributeQuery("family", node=s, exists=True): - continue - - attach_to.append( - { - "version": None, # we need integrator for that - "subset": s, - "family": cmds.getAttr("{}.family".format(s)), - } - ) - self.log.info(" -> attach render to: {}".format(s)) - - layer_name = "rs_{}".format(expected_layer_name) - - # collect all frames we are expecting to be rendered - # return all expected files for all cameras and aovs in given - # frame range - layer_render_products = get_layer_render_products(layer_name) - render_products = layer_render_products.layer_data.products - assert render_products, "no render products generated" - exp_files = [] - multipart = False - for product in render_products: - if product.multipart: - multipart = True - product_name = product.productName - if product.camera and layer_render_products.has_camera_token(): - product_name = "{}{}".format( - product.camera, - "_" + product_name if product_name else "") - exp_files.append( - { - product_name: layer_render_products.get_files( - product) - }) - - has_cameras = any(product.camera for product in render_products) - assert has_cameras, "No render cameras found." - - self.log.info("multipart: {}".format( - multipart)) - assert exp_files, "no file names were generated, this is bug" - self.log.info( - "expected files: {}".format( - json.dumps(exp_files, indent=4, sort_keys=True) - ) - ) - - # if we want to attach render to subset, check if we have AOV's - # in expectedFiles. If so, raise error as we cannot attach AOV - # (considered to be subset on its own) to another subset - if attach_to: - assert isinstance(exp_files, list), ( - "attaching multiple AOVs or renderable cameras to " - "subset is not supported" - ) - - # append full path - aov_dict = {} - default_render_file = context.data.get('project_settings')\ - .get('maya')\ - .get('RenderSettings')\ - .get('default_render_image_folder') or "" - # replace relative paths with absolute. Render products are - # returned as list of dictionaries. - publish_meta_path = None - for aov in exp_files: - full_paths = [] - aov_first_key = list(aov.keys())[0] - for file in aov[aov_first_key]: - full_path = os.path.join(workspace, default_render_file, - file) - full_path = full_path.replace("\\", "/") - full_paths.append(full_path) - publish_meta_path = os.path.dirname(full_path) - aov_dict[aov_first_key] = full_paths - full_exp_files = [aov_dict] - - frame_start_render = int(self.get_render_attribute( - "startFrame", layer=layer_name)) - frame_end_render = int(self.get_render_attribute( - "endFrame", layer=layer_name)) - - if (int(context.data['frameStartHandle']) == frame_start_render - and int(context.data['frameEndHandle']) == frame_end_render): # noqa: W503, E501 - - handle_start = context.data['handleStart'] - handle_end = context.data['handleEnd'] - frame_start = context.data['frameStart'] - frame_end = context.data['frameEnd'] - frame_start_handle = context.data['frameStartHandle'] - frame_end_handle = context.data['frameEndHandle'] - else: - handle_start = 0 - handle_end = 0 - frame_start = frame_start_render - frame_end = frame_end_render - frame_start_handle = frame_start_render - frame_end_handle = frame_end_render - - # find common path to store metadata - # so if image prefix is branching to many directories - # metadata file will be located in top-most common - # directory. - # TODO: use `os.path.commonpath()` after switch to Python 3 - publish_meta_path = os.path.normpath(publish_meta_path) - common_publish_meta_path = os.path.splitdrive( - publish_meta_path)[0] - if common_publish_meta_path: - common_publish_meta_path += os.path.sep - for part in publish_meta_path.replace( - common_publish_meta_path, "").split(os.path.sep): - common_publish_meta_path = os.path.join( - common_publish_meta_path, part) - if part == expected_layer_name: - break - - # TODO: replace this terrible linux hotfix with real solution :) - if platform.system().lower() in ["linux", "darwin"]: - common_publish_meta_path = "/" + common_publish_meta_path - - self.log.info( - "Publish meta path: {}".format(common_publish_meta_path)) - - self.log.info(full_exp_files) - self.log.info("collecting layer: {}".format(layer_name)) - # Get layer specific settings, might be overrides - colorspace_data = lib.get_color_management_preferences() - data = { - "subset": expected_layer_name, - "attachTo": attach_to, - "setMembers": layer_name, - "multipartExr": multipart, - "review": render_instance.data.get("review") or False, - "publish": True, - - "handleStart": handle_start, - "handleEnd": handle_end, - "frameStart": frame_start, - "frameEnd": frame_end, - "frameStartHandle": frame_start_handle, - "frameEndHandle": frame_end_handle, - "byFrameStep": int( - self.get_render_attribute("byFrameStep", - layer=layer_name)), - "renderer": self.get_render_attribute( - "currentRenderer", layer=layer_name).lower(), - # instance subset - "family": "renderlayer", - "families": ["renderlayer"], - "asset": asset, - "time": get_formatted_current_time(), - "author": context.data["user"], - # Add source to allow tracing back to the scene from - # which was submitted originally - "source": filepath, - "expectedFiles": full_exp_files, - "publishRenderMetadataFolder": common_publish_meta_path, - "renderProducts": layer_render_products, - "resolutionWidth": lib.get_attr_in_layer( - "defaultResolution.width", layer=layer_name - ), - "resolutionHeight": lib.get_attr_in_layer( - "defaultResolution.height", layer=layer_name - ), - "pixelAspect": lib.get_attr_in_layer( - "defaultResolution.pixelAspect", layer=layer_name - ), - "tileRendering": render_instance.data.get("tileRendering") or False, # noqa: E501 - "tilesX": render_instance.data.get("tilesX") or 2, - "tilesY": render_instance.data.get("tilesY") or 2, - "priority": render_instance.data.get("priority"), - "convertToScanline": render_instance.data.get( - "convertToScanline") or False, - "useReferencedAovs": render_instance.data.get( - "useReferencedAovs") or render_instance.data.get( - "vrayUseReferencedAovs") or False, - "aovSeparator": layer_render_products.layer_data.aov_separator, # noqa: E501 - "renderSetupIncludeLights": render_instance.data.get( - "renderSetupIncludeLights" - ), - "colorspaceConfig": colorspace_data["config"], - "colorspaceDisplay": colorspace_data["display"], - "colorspaceView": colorspace_data["view"], - "strict_error_checking": render_instance.data.get( - "strict_error_checking", True - ) - } - - # Collect Deadline url if Deadline module is enabled - deadline_settings = ( - context.data["system_settings"]["modules"]["deadline"] - ) - if deadline_settings["enabled"]: - data["deadlineUrl"] = render_instance.data["deadlineUrl"] - - if self.sync_workfile_version: - data["version"] = context.data["version"] - - for instance in context: - if instance.data['family'] == "workfile": - instance.data["version"] = context.data["version"] - - # handle standalone renderers - if render_instance.data.get("vrayScene") is True: - data["families"].append("vrayscene_render") - - if render_instance.data.get("assScene") is True: - data["families"].append("assscene_render") - - # Include (optional) global settings - # Get global overrides and translate to Deadline values - overrides = self.parse_options(str(render_globals)) - data.update(**overrides) - - # get string values for pools - primary_pool = overrides["renderGlobals"]["Pool"] - secondary_pool = overrides["renderGlobals"].get("SecondaryPool") - data["primaryPool"] = primary_pool - data["secondaryPool"] = secondary_pool - - # Define nice label - label = "{0} ({1})".format(expected_layer_name, data["asset"]) - label += " [{0}-{1}]".format( - int(data["frameStartHandle"]), int(data["frameEndHandle"]) - ) - - instance = context.create_instance(expected_layer_name) - instance.data["label"] = label - instance.data["farm"] = True - instance.data.update(data) - - def parse_options(self, render_globals): - """Get all overrides with a value, skip those without. - - Here's the kicker. These globals override defaults in the submission - integrator, but an empty value means no overriding is made. - Otherwise, Frames would override the default frames set under globals. - - Args: - render_globals (str): collection of render globals - - Returns: - dict: only overrides with values - - """ - attributes = lib.read(render_globals) - - options = {"renderGlobals": {}} - options["renderGlobals"]["Priority"] = attributes["priority"] - - # Check for specific pools - pool_a, pool_b = self._discover_pools(attributes) - options["renderGlobals"].update({"Pool": pool_a}) - if pool_b: - options["renderGlobals"].update({"SecondaryPool": pool_b}) - - # Machine list - machine_list = attributes["machineList"] - if machine_list: - key = "Whitelist" if attributes["whitelist"] else "Blacklist" - options["renderGlobals"][key] = machine_list - - # Suspend publish job - state = "Suspended" if attributes["suspendPublishJob"] else "Active" - options["publishJobState"] = state - - chunksize = attributes.get("framesPerTask", 1) - options["renderGlobals"]["ChunkSize"] = chunksize + # Define nice label + label = "{0} ({1})".format(layer_name, instance.data["asset"]) + label += " [{0}-{1}]".format( + int(data["frameStartHandle"]), int(data["frameEndHandle"]) + ) + data["label"] = label # Override frames should be False if extendFrames is False. This is # to ensure it doesn't go off doing crazy unpredictable things - override_frames = False - extend_frames = attributes.get("extendFrames", False) - if extend_frames: - override_frames = attributes.get("overrideExistingFrame", False) + extend_frames = instance.data.get("extendFrames", False) + if not extend_frames: + instance.data["overrideExistingFrame"] = False - options["extendFrames"] = extend_frames - options["overrideExistingFrame"] = override_frames - - maya_render_plugin = "MayaBatch" - - options["mayaRenderPlugin"] = maya_render_plugin - - return options - - def _discover_pools(self, attributes): - - pool_a = None - pool_b = None - - # Check for specific pools - pool_b = [] - if "primaryPool" in attributes: - pool_a = attributes["primaryPool"] - if "secondaryPool" in attributes: - pool_b = attributes["secondaryPool"] - - else: - # Backwards compatibility - pool_str = attributes.get("pools", None) - if pool_str: - pool_a, pool_b = pool_str.split(";") - - # Ensure empty entry token is caught - if pool_b == "-": - pool_b = None - - return pool_a, pool_b + # Update the instace + instance.data.update(data) @staticmethod def get_render_attribute(attr, layer): diff --git a/openpype/hosts/maya/plugins/publish/collect_render_layer_aovs.py b/openpype/hosts/maya/plugins/publish/collect_render_layer_aovs.py index 9666499c42..c3dc31ead9 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render_layer_aovs.py +++ b/openpype/hosts/maya/plugins/publish/collect_render_layer_aovs.py @@ -50,7 +50,7 @@ class CollectRenderLayerAOVS(pyblish.api.InstancePlugin): result = [] # Collect all AOVs / Render Elements - layer = instance.data["setMembers"] + layer = instance.data["renderlayer"] node_type = rp_node_types[renderer] render_elements = cmds.ls(type=node_type) diff --git a/openpype/hosts/maya/plugins/publish/collect_renderable_camera.py b/openpype/hosts/maya/plugins/publish/collect_renderable_camera.py index 93a37d8693..d1c3cf3b2c 100644 --- a/openpype/hosts/maya/plugins/publish/collect_renderable_camera.py +++ b/openpype/hosts/maya/plugins/publish/collect_renderable_camera.py @@ -19,7 +19,7 @@ class CollectRenderableCamera(pyblish.api.InstancePlugin): if "vrayscene_layer" in instance.data.get("families", []): layer = instance.data.get("layer") else: - layer = instance.data["setMembers"] + layer = instance.data["renderlayer"] self.log.info("layer: {}".format(layer)) cameras = cmds.ls(type="camera", long=True) diff --git a/openpype/hosts/maya/plugins/publish/collect_review.py b/openpype/hosts/maya/plugins/publish/collect_review.py index 5c190a4a7b..6cb10f9066 100644 --- a/openpype/hosts/maya/plugins/publish/collect_review.py +++ b/openpype/hosts/maya/plugins/publish/collect_review.py @@ -18,14 +18,10 @@ class CollectReview(pyblish.api.InstancePlugin): def process(self, instance): - self.log.debug('instance: {}'.format(instance)) - - task = legacy_io.Session["AVALON_TASK"] - # Get panel. instance.data["panel"] = cmds.playblast( activeEditor=True - ).split("|")[-1] + ).rsplit("|", 1)[-1] # get cameras members = instance.data['setMembers'] @@ -34,11 +30,12 @@ class CollectReview(pyblish.api.InstancePlugin): camera = cameras[0] if cameras else None context = instance.context - objectset = context.data['objectsets'] + objectset = { + i.data.get("instance_node") for i in context + } - # Convert enum attribute index to string for Display Lights. - index = instance.data.get("displayLights", 0) - display_lights = lib.DISPLAY_LIGHTS_VALUES[index] + # Collect display lights. + display_lights = instance.data.get("displayLights", "default") if display_lights == "project_settings": settings = instance.context.data["project_settings"] settings = settings["maya"]["publish"]["ExtractPlayblast"] @@ -60,7 +57,7 @@ class CollectReview(pyblish.api.InstancePlugin): burninDataMembers["focalLength"] = focal_length # Account for nested instances like model. - reviewable_subsets = list(set(members) & set(objectset)) + reviewable_subsets = list(set(members) & objectset) if reviewable_subsets: if len(reviewable_subsets) > 1: raise KnownPublishError( @@ -97,7 +94,11 @@ class CollectReview(pyblish.api.InstancePlugin): data["frameStart"] = instance.data["frameStart"] data["frameEnd"] = instance.data["frameEnd"] data['step'] = instance.data['step'] - data['fps'] = instance.data['fps'] + # this (with other time related data) should be set on + # representations. Once plugins like Extract Review start + # using representations, this should be removed from here + # as Extract Playblast is already adding fps to representation. + data['fps'] = context.data['fps'] data['review_width'] = instance.data['review_width'] data['review_height'] = instance.data['review_height'] data["isolate"] = instance.data["isolate"] @@ -112,6 +113,7 @@ class CollectReview(pyblish.api.InstancePlugin): instance.data['remove'] = True else: + task = legacy_io.Session["AVALON_TASK"] legacy_subset_name = task + 'Review' asset_doc = instance.context.data['assetEntity'] project_name = legacy_io.active_project() @@ -133,6 +135,11 @@ class CollectReview(pyblish.api.InstancePlugin): instance.data["frameEndHandle"] instance.data["displayLights"] = display_lights instance.data["burninDataMembers"] = burninDataMembers + # this (with other time related data) should be set on + # representations. Once plugins like Extract Review start + # using representations, this should be removed from here + # as Extract Playblast is already adding fps to representation. + instance.data["fps"] = instance.context.data["fps"] # make ftrack publishable instance.data.setdefault("families", []).append('ftrack') diff --git a/openpype/hosts/maya/plugins/publish/collect_vrayscene.py b/openpype/hosts/maya/plugins/publish/collect_vrayscene.py index 0bae9656f3..b9181337a9 100644 --- a/openpype/hosts/maya/plugins/publish/collect_vrayscene.py +++ b/openpype/hosts/maya/plugins/publish/collect_vrayscene.py @@ -24,129 +24,91 @@ class CollectVrayScene(pyblish.api.InstancePlugin): def process(self, instance): """Collector entry point.""" - collected_render_layers = instance.data["setMembers"] - instance.data["remove"] = True + context = instance.context - _rs = renderSetup.instance() - # current_layer = _rs.getVisibleRenderLayer() + layer = instance.data["transientData"]["layer"] + layer_name = layer.name() + + renderer = self.get_render_attribute("currentRenderer", + layer=layer_name) + if renderer != "vray": + self.log.warning("Layer '{}' renderer is not set to V-Ray".format( + layer_name + )) # collect all frames we are expecting to be rendered - renderer = cmds.getAttr( - "defaultRenderGlobals.currentRenderer" - ).lower() + frame_start_render = int(self.get_render_attribute( + "startFrame", layer=layer_name)) + frame_end_render = int(self.get_render_attribute( + "endFrame", layer=layer_name)) - if renderer != "vray": - raise AssertionError("Vray is not enabled.") + if (int(context.data['frameStartHandle']) == frame_start_render + and int(context.data['frameEndHandle']) == frame_end_render): # noqa: W503, E501 - maya_render_layers = { - layer.name(): layer for layer in _rs.getRenderLayers() + handle_start = context.data['handleStart'] + handle_end = context.data['handleEnd'] + frame_start = context.data['frameStart'] + frame_end = context.data['frameEnd'] + frame_start_handle = context.data['frameStartHandle'] + frame_end_handle = context.data['frameEndHandle'] + else: + handle_start = 0 + handle_end = 0 + frame_start = frame_start_render + frame_end = frame_end_render + frame_start_handle = frame_start_render + frame_end_handle = frame_end_render + + # Get layer specific settings, might be overrides + data = { + "subset": layer_name, + "layer": layer_name, + # TODO: This likely needs fixing now + # Before refactor: cmds.sets(layer, q=True) or ["*"] + "setMembers": ["*"], + "review": False, + "publish": True, + "handleStart": handle_start, + "handleEnd": handle_end, + "frameStart": frame_start, + "frameEnd": frame_end, + "frameStartHandle": frame_start_handle, + "frameEndHandle": frame_end_handle, + "byFrameStep": int( + self.get_render_attribute("byFrameStep", + layer=layer_name)), + "renderer": renderer, + # instance subset + "family": "vrayscene_layer", + "families": ["vrayscene_layer"], + "time": get_formatted_current_time(), + "author": context.data["user"], + # Add source to allow tracing back to the scene from + # which was submitted originally + "source": context.data["currentFile"].replace("\\", "/"), + "resolutionWidth": lib.get_attr_in_layer( + "defaultResolution.height", layer=layer_name + ), + "resolutionHeight": lib.get_attr_in_layer( + "defaultResolution.width", layer=layer_name + ), + "pixelAspect": lib.get_attr_in_layer( + "defaultResolution.pixelAspect", layer=layer_name + ), + "priority": instance.data.get("priority"), + "useMultipleSceneFiles": instance.data.get( + "vraySceneMultipleFiles") } - layer_list = [] - for layer in collected_render_layers: - # every layer in set should start with `LAYER_` prefix - try: - expected_layer_name = re.search(r"^.+:(.*)", layer).group(1) - except IndexError: - msg = "Invalid layer name in set [ {} ]".format(layer) - self.log.warning(msg) - continue + instance.data.update(data) - self.log.info("processing %s" % layer) - # check if layer is part of renderSetup - if expected_layer_name not in maya_render_layers: - msg = "Render layer [ {} ] is not in " "Render Setup".format( - expected_layer_name - ) - self.log.warning(msg) - continue - - # check if layer is renderable - if not maya_render_layers[expected_layer_name].isRenderable(): - msg = "Render layer [ {} ] is not " "renderable".format( - expected_layer_name - ) - self.log.warning(msg) - continue - - layer_name = "rs_{}".format(expected_layer_name) - - self.log.debug(expected_layer_name) - layer_list.append(expected_layer_name) - - frame_start_render = int(self.get_render_attribute( - "startFrame", layer=layer_name)) - frame_end_render = int(self.get_render_attribute( - "endFrame", layer=layer_name)) - - if (int(context.data['frameStartHandle']) == frame_start_render - and int(context.data['frameEndHandle']) == frame_end_render): # noqa: W503, E501 - - handle_start = context.data['handleStart'] - handle_end = context.data['handleEnd'] - frame_start = context.data['frameStart'] - frame_end = context.data['frameEnd'] - frame_start_handle = context.data['frameStartHandle'] - frame_end_handle = context.data['frameEndHandle'] - else: - handle_start = 0 - handle_end = 0 - frame_start = frame_start_render - frame_end = frame_end_render - frame_start_handle = frame_start_render - frame_end_handle = frame_end_render - - # Get layer specific settings, might be overrides - data = { - "subset": expected_layer_name, - "layer": layer_name, - "setMembers": cmds.sets(layer, q=True) or ["*"], - "review": False, - "publish": True, - "handleStart": handle_start, - "handleEnd": handle_end, - "frameStart": frame_start, - "frameEnd": frame_end, - "frameStartHandle": frame_start_handle, - "frameEndHandle": frame_end_handle, - "byFrameStep": int( - self.get_render_attribute("byFrameStep", - layer=layer_name)), - "renderer": self.get_render_attribute("currentRenderer", - layer=layer_name), - # instance subset - "family": "vrayscene_layer", - "families": ["vrayscene_layer"], - "asset": legacy_io.Session["AVALON_ASSET"], - "time": get_formatted_current_time(), - "author": context.data["user"], - # Add source to allow tracing back to the scene from - # which was submitted originally - "source": context.data["currentFile"].replace("\\", "/"), - "resolutionWidth": lib.get_attr_in_layer( - "defaultResolution.height", layer=layer_name - ), - "resolutionHeight": lib.get_attr_in_layer( - "defaultResolution.width", layer=layer_name - ), - "pixelAspect": lib.get_attr_in_layer( - "defaultResolution.pixelAspect", layer=layer_name - ), - "priority": instance.data.get("priority"), - "useMultipleSceneFiles": instance.data.get( - "vraySceneMultipleFiles") - } - - # Define nice label - label = "{0} ({1})".format(expected_layer_name, data["asset"]) - label += " [{0}-{1}]".format( - int(data["frameStartHandle"]), int(data["frameEndHandle"]) - ) - - instance = context.create_instance(expected_layer_name) - instance.data["label"] = label - instance.data.update(data) + # Define nice label + label = "{0} ({1})".format(layer_name, instance.data["asset"]) + label += " [{0}-{1}]".format( + int(data["frameStartHandle"]), int(data["frameEndHandle"]) + ) + instance.data["label"] = label def get_render_attribute(self, attr, layer): """Get attribute from render options. diff --git a/openpype/hosts/maya/plugins/publish/collect_workfile.py b/openpype/hosts/maya/plugins/publish/collect_workfile.py index 12d86869ea..e2b64f1ebd 100644 --- a/openpype/hosts/maya/plugins/publish/collect_workfile.py +++ b/openpype/hosts/maya/plugins/publish/collect_workfile.py @@ -1,46 +1,30 @@ import os import pyblish.api -from maya import cmds -from openpype.pipeline import legacy_io - -class CollectWorkfile(pyblish.api.ContextPlugin): - """Inject the current working file into context""" +class CollectWorkfileData(pyblish.api.InstancePlugin): + """Inject data into Workfile instance""" order = pyblish.api.CollectorOrder - 0.01 label = "Maya Workfile" hosts = ['maya'] + families = ["workfile"] - def process(self, context): + def process(self, instance): """Inject the current working file""" - current_file = cmds.file(query=True, sceneName=True) - context.data['currentFile'] = current_file + context = instance.context + current_file = instance.context.data['currentFile'] folder, file = os.path.split(current_file) filename, ext = os.path.splitext(file) - task = legacy_io.Session["AVALON_TASK"] - - data = {} - - # create instance - instance = context.create_instance(name=filename) - subset = 'workfile' + task.capitalize() - - data.update({ - "subset": subset, - "asset": os.getenv("AVALON_ASSET", None), - "label": subset, - "publish": True, - "family": 'workfile', - "families": ['workfile'], + data = { # noqa "setMembers": [current_file], "frameStart": context.data['frameStart'], "frameEnd": context.data['frameEnd'], "handleStart": context.data['handleStart'], "handleEnd": context.data['handleEnd'] - }) + } data['representations'] = [{ 'name': ext.lstrip("."), @@ -50,8 +34,3 @@ class CollectWorkfile(pyblish.api.ContextPlugin): }] instance.data.update(data) - - self.log.info('Collected instance: {}'.format(file)) - self.log.info('Scene path: {}'.format(current_file)) - self.log.info('staging Dir: {}'.format(folder)) - self.log.info('subset: {}'.format(subset)) diff --git a/openpype/hosts/maya/plugins/publish/extract_assembly.py b/openpype/hosts/maya/plugins/publish/extract_assembly.py index 35932003ee..9b2978d192 100644 --- a/openpype/hosts/maya/plugins/publish/extract_assembly.py +++ b/openpype/hosts/maya/plugins/publish/extract_assembly.py @@ -31,7 +31,7 @@ class ExtractAssembly(publish.Extractor): with open(json_path, "w") as filepath: json.dump(instance.data["scenedata"], filepath, ensure_ascii=False) - self.log.info("Extracting point cache ..") + self.log.debug("Extracting pointcache ..") cmds.select(instance.data["nodesHierarchy"]) # Run basic alembic exporter diff --git a/openpype/hosts/maya/plugins/publish/extract_camera_mayaScene.py b/openpype/hosts/maya/plugins/publish/extract_camera_mayaScene.py index 7467fa027d..30e6b89f2f 100644 --- a/openpype/hosts/maya/plugins/publish/extract_camera_mayaScene.py +++ b/openpype/hosts/maya/plugins/publish/extract_camera_mayaScene.py @@ -106,7 +106,7 @@ class ExtractCameraMayaScene(publish.Extractor): instance.context.data["project_settings"]["maya"]["ext_mapping"] ) if ext_mapping: - self.log.info("Looking in settings for scene type ...") + self.log.debug("Looking in settings for scene type ...") # use extension mapping for first family found for family in self.families: try: diff --git a/openpype/hosts/maya/plugins/publish/extract_import_reference.py b/openpype/hosts/maya/plugins/publish/extract_import_reference.py index 51c82dde92..8bb82be9b6 100644 --- a/openpype/hosts/maya/plugins/publish/extract_import_reference.py +++ b/openpype/hosts/maya/plugins/publish/extract_import_reference.py @@ -8,10 +8,12 @@ import tempfile from openpype.lib import run_subprocess from openpype.pipeline import publish +from openpype.pipeline.publish import OptionalPyblishPluginMixin from openpype.hosts.maya.api import lib -class ExtractImportReference(publish.Extractor): +class ExtractImportReference(publish.Extractor, + OptionalPyblishPluginMixin): """ Extract the scene with imported reference. @@ -32,11 +34,14 @@ class ExtractImportReference(publish.Extractor): cls.active = project_setting["deadline"]["publish"]["MayaSubmitDeadline"]["import_reference"] # noqa def process(self, instance): + if not self.is_active(instance.data): + return + ext_mapping = ( instance.context.data["project_settings"]["maya"]["ext_mapping"] ) if ext_mapping: - self.log.info("Looking in settings for scene type ...") + self.log.debug("Looking in settings for scene type ...") # use extension mapping for first family found for family in self.families: try: diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 3cc95a0b2e..e2c88ef44a 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -412,7 +412,7 @@ class ExtractLook(publish.Extractor): instance.context.data["project_settings"]["maya"]["ext_mapping"] ) if ext_mapping: - self.log.info("Looking in settings for scene type ...") + self.log.debug("Looking in settings for scene type ...") # use extension mapping for first family found for family in self.families: try: @@ -444,12 +444,12 @@ class ExtractLook(publish.Extractor): # Remove all members of the sets so they are not included in the # exported file by accident - self.log.info("Processing sets..") + self.log.debug("Processing sets..") lookdata = instance.data["lookData"] relationships = lookdata["relationships"] sets = list(relationships.keys()) if not sets: - self.log.info("No sets found") + self.log.info("No sets found for the look") return # Specify texture processing executables to activate diff --git a/openpype/hosts/maya/plugins/publish/extract_maya_scene_raw.py b/openpype/hosts/maya/plugins/publish/extract_maya_scene_raw.py index c2411ca651..d87d6c208a 100644 --- a/openpype/hosts/maya/plugins/publish/extract_maya_scene_raw.py +++ b/openpype/hosts/maya/plugins/publish/extract_maya_scene_raw.py @@ -29,7 +29,7 @@ class ExtractMayaSceneRaw(publish.Extractor): instance.context.data["project_settings"]["maya"]["ext_mapping"] ) if ext_mapping: - self.log.info("Looking in settings for scene type ...") + self.log.debug("Looking in settings for scene type ...") # use extension mapping for first family found for family in self.families: try: diff --git a/openpype/hosts/maya/plugins/publish/extract_model.py b/openpype/hosts/maya/plugins/publish/extract_model.py index 7c8c3a2981..5137dffd94 100644 --- a/openpype/hosts/maya/plugins/publish/extract_model.py +++ b/openpype/hosts/maya/plugins/publish/extract_model.py @@ -8,7 +8,8 @@ from openpype.pipeline import publish from openpype.hosts.maya.api import lib -class ExtractModel(publish.Extractor): +class ExtractModel(publish.Extractor, + publish.OptionalPyblishPluginMixin): """Extract as Model (Maya Scene). Only extracts contents based on the original "setMembers" data to ensure @@ -31,11 +32,14 @@ class ExtractModel(publish.Extractor): def process(self, instance): """Plugin entry point.""" + if not self.is_active(instance.data): + return + ext_mapping = ( instance.context.data["project_settings"]["maya"]["ext_mapping"] ) if ext_mapping: - self.log.info("Looking in settings for scene type ...") + self.log.debug("Looking in settings for scene type ...") # use extension mapping for first family found for family in self.families: try: diff --git a/openpype/hosts/maya/plugins/publish/extract_pointcache.py b/openpype/hosts/maya/plugins/publish/extract_pointcache.py index f44c13767c..9537a11ee4 100644 --- a/openpype/hosts/maya/plugins/publish/extract_pointcache.py +++ b/openpype/hosts/maya/plugins/publish/extract_pointcache.py @@ -45,7 +45,7 @@ class ExtractAlembic(publish.Extractor): attr_prefixes = instance.data.get("attrPrefix", "").split(";") attr_prefixes = [value for value in attr_prefixes if value.strip()] - self.log.info("Extracting pointcache..") + self.log.debug("Extracting pointcache..") dirname = self.staging_dir(instance) parent_dir = self.staging_dir(instance) @@ -86,7 +86,6 @@ class ExtractAlembic(publish.Extractor): end=end)) suspend = not instance.data.get("refresh", False) - self.log.info(nodes) with suspended_refresh(suspend=suspend): with maintained_selection(): cmds.select(nodes, noExpand=True) diff --git a/openpype/hosts/maya/plugins/publish/extract_redshift_proxy.py b/openpype/hosts/maya/plugins/publish/extract_redshift_proxy.py index 4377275635..834b335fc5 100644 --- a/openpype/hosts/maya/plugins/publish/extract_redshift_proxy.py +++ b/openpype/hosts/maya/plugins/publish/extract_redshift_proxy.py @@ -29,15 +29,21 @@ class ExtractRedshiftProxy(publish.Extractor): if not anim_on: # Remove animation information because it is not required for # non-animated subsets - instance.data.pop("proxyFrameStart", None) - instance.data.pop("proxyFrameEnd", None) + keys = ["frameStart", + "frameEnd", + "handleStart", + "handleEnd", + "frameStartHandle", + "frameEndHandle"] + for key in keys: + instance.data.pop(key, None) else: - start_frame = instance.data["proxyFrameStart"] - end_frame = instance.data["proxyFrameEnd"] + start_frame = instance.data["frameStartHandle"] + end_frame = instance.data["frameEndHandle"] rs_options = "{}startFrame={};endFrame={};frameStep={};".format( rs_options, start_frame, - end_frame, instance.data["proxyFrameStep"] + end_frame, instance.data["step"] ) root, ext = os.path.splitext(file_path) @@ -48,7 +54,7 @@ class ExtractRedshiftProxy(publish.Extractor): for frame in range( int(start_frame), int(end_frame) + 1, - int(instance.data["proxyFrameStep"]), + int(instance.data["step"]), )] # vertex_colors = instance.data.get("vertexColors", False) @@ -74,8 +80,6 @@ class ExtractRedshiftProxy(publish.Extractor): 'files': repr_files, "stagingDir": staging_dir, } - if anim_on: - representation["frameStart"] = instance.data["proxyFrameStart"] instance.data["representations"].append(representation) self.log.info("Extracted instance '%s' to: %s" diff --git a/openpype/hosts/maya/plugins/publish/extract_rig.py b/openpype/hosts/maya/plugins/publish/extract_rig.py index c71a2f710d..be57b9de07 100644 --- a/openpype/hosts/maya/plugins/publish/extract_rig.py +++ b/openpype/hosts/maya/plugins/publish/extract_rig.py @@ -22,13 +22,13 @@ class ExtractRig(publish.Extractor): instance.context.data["project_settings"]["maya"]["ext_mapping"] ) if ext_mapping: - self.log.info("Looking in settings for scene type ...") + self.log.debug("Looking in settings for scene type ...") # use extension mapping for first family found for family in self.families: try: self.scene_type = ext_mapping[family] self.log.info( - "Using {} as scene type".format(self.scene_type)) + "Using '.{}' as scene type".format(self.scene_type)) break except AttributeError: # no preset found diff --git a/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh_abc.py b/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh_abc.py index e1f847f31a..4a797eb462 100644 --- a/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh_abc.py +++ b/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh_abc.py @@ -32,7 +32,7 @@ class ExtractUnrealSkeletalMeshAbc(publish.Extractor): optional = True def process(self, instance): - self.log.info("Extracting pointcache..") + self.log.debug("Extracting pointcache..") geo = cmds.listRelatives( instance.data.get("geometry"), allDescendents=True, fullPath=True) diff --git a/openpype/hosts/maya/plugins/publish/extract_yeti_rig.py b/openpype/hosts/maya/plugins/publish/extract_yeti_rig.py index 1d0c5e88c3..9a46c31177 100644 --- a/openpype/hosts/maya/plugins/publish/extract_yeti_rig.py +++ b/openpype/hosts/maya/plugins/publish/extract_yeti_rig.py @@ -104,7 +104,7 @@ class ExtractYetiRig(publish.Extractor): instance.context.data["project_settings"]["maya"]["ext_mapping"] ) if ext_mapping: - self.log.info("Looking in settings for scene type ...") + self.log.debug("Looking in settings for scene type ...") # use extension mapping for first family found for family in self.families: try: diff --git a/openpype/hosts/maya/plugins/publish/help/validate_maya_units.xml b/openpype/hosts/maya/plugins/publish/help/validate_maya_units.xml new file mode 100644 index 0000000000..40169b28f9 --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/help/validate_maya_units.xml @@ -0,0 +1,21 @@ + + + +Maya scene units +## Invalid maya scene units + +Detected invalid maya scene units: + +{issues} + + + +### How to repair? + +You can automatically repair the scene units by clicking the Repair action on +the right. + +After that restart publishing with Reload button. + + + diff --git a/openpype/hosts/maya/plugins/publish/help/validate_node_ids.xml b/openpype/hosts/maya/plugins/publish/help/validate_node_ids.xml new file mode 100644 index 0000000000..2ef4bc95c2 --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/help/validate_node_ids.xml @@ -0,0 +1,29 @@ + + + +Missing node ids +## Nodes found with missing `cbId` + +Nodes were detected in your scene which are missing required `cbId` +attributes for identification further in the pipeline. + +### How to repair? + +The node ids are auto-generated on scene save, and thus the easiest fix is to +save your scene again. + +After that restart publishing with Reload button. + + +### Invalid nodes + +{nodes} + + +### How could this happen? + +This often happens if you've generated new nodes but haven't saved your scene +after creating the new nodes. + + + diff --git a/openpype/hosts/maya/plugins/publish/submit_maya_muster.py b/openpype/hosts/maya/plugins/publish/submit_maya_muster.py index 1a6463fb9d..298c3bd345 100644 --- a/openpype/hosts/maya/plugins/publish/submit_maya_muster.py +++ b/openpype/hosts/maya/plugins/publish/submit_maya_muster.py @@ -288,7 +288,7 @@ class MayaSubmitMuster(pyblish.api.InstancePlugin): comment = context.data.get("comment", "") scene = os.path.splitext(filename)[0] dirname = os.path.join(workspace, "renders") - renderlayer = instance.data['setMembers'] # rs_beauty + renderlayer = instance.data['renderlayer'] # rs_beauty renderlayer_name = instance.data['subset'] # beauty renderglobals = instance.data["renderGlobals"] # legacy_layers = renderlayer_globals["UseLegacyRenderLayers"] @@ -546,3 +546,9 @@ class MayaSubmitMuster(pyblish.api.InstancePlugin): "%f=%d was rounded off to nearest integer" % (value, int(value)) ) + + +# TODO: Remove hack to avoid this plug-in in new publisher +# This plug-in should actually be in dedicated module +if not os.environ.get("MUSTER_REST_URL"): + del MayaSubmitMuster diff --git a/openpype/hosts/maya/plugins/publish/validate_animation_content.py b/openpype/hosts/maya/plugins/publish/validate_animation_content.py index 9dbb09a046..99acdc7b8f 100644 --- a/openpype/hosts/maya/plugins/publish/validate_animation_content.py +++ b/openpype/hosts/maya/plugins/publish/validate_animation_content.py @@ -1,6 +1,9 @@ import pyblish.api import openpype.hosts.maya.api.action -from openpype.pipeline.publish import ValidateContentsOrder +from openpype.pipeline.publish import ( + PublishValidationError, + ValidateContentsOrder +) class ValidateAnimationContent(pyblish.api.InstancePlugin): @@ -47,4 +50,5 @@ class ValidateAnimationContent(pyblish.api.InstancePlugin): def process(self, instance): invalid = self.get_invalid(instance) if invalid: - raise RuntimeError("Animation content is invalid. See log.") + raise PublishValidationError( + "Animation content is invalid. See log.") diff --git a/openpype/hosts/maya/plugins/publish/validate_animation_out_set_related_node_ids.py b/openpype/hosts/maya/plugins/publish/validate_animation_out_set_related_node_ids.py index 5a527031be..6f5f03ab39 100644 --- a/openpype/hosts/maya/plugins/publish/validate_animation_out_set_related_node_ids.py +++ b/openpype/hosts/maya/plugins/publish/validate_animation_out_set_related_node_ids.py @@ -6,6 +6,7 @@ from openpype.hosts.maya.api import lib from openpype.pipeline.publish import ( RepairAction, ValidateContentsOrder, + PublishValidationError ) @@ -35,8 +36,10 @@ class ValidateOutRelatedNodeIds(pyblish.api.InstancePlugin): # if a deformer has been created on the shape invalid = self.get_invalid(instance) if invalid: - raise RuntimeError("Nodes found with mismatching " - "IDs: {0}".format(invalid)) + # TODO: Message formatting can be improved + raise PublishValidationError("Nodes found with mismatching " + "IDs: {0}".format(invalid), + title="Invalid node ids") @classmethod def get_invalid(cls, instance): diff --git a/openpype/hosts/maya/plugins/publish/validate_ass_relative_paths.py b/openpype/hosts/maya/plugins/publish/validate_ass_relative_paths.py index 6975d583bb..49913fa42b 100644 --- a/openpype/hosts/maya/plugins/publish/validate_ass_relative_paths.py +++ b/openpype/hosts/maya/plugins/publish/validate_ass_relative_paths.py @@ -23,11 +23,13 @@ class ValidateAssRelativePaths(pyblish.api.InstancePlugin): def process(self, instance): # we cannot ask this until user open render settings as - # `defaultArnoldRenderOptions` doesn't exists + # `defaultArnoldRenderOptions` doesn't exist + errors = [] + try: - relative_texture = cmds.getAttr( + absolute_texture = cmds.getAttr( "defaultArnoldRenderOptions.absolute_texture_paths") - relative_procedural = cmds.getAttr( + absolute_procedural = cmds.getAttr( "defaultArnoldRenderOptions.absolute_procedural_paths") texture_search_path = cmds.getAttr( "defaultArnoldRenderOptions.tspath" @@ -42,10 +44,11 @@ class ValidateAssRelativePaths(pyblish.api.InstancePlugin): scene_dir, scene_basename = os.path.split(cmds.file(q=True, loc=True)) scene_name, _ = os.path.splitext(scene_basename) - assert self.maya_is_true(relative_texture) is not True, \ - ("Texture path is set to be absolute") - assert self.maya_is_true(relative_procedural) is not True, \ - ("Procedural path is set to be absolute") + + if self.maya_is_true(absolute_texture): + errors.append("Texture path is set to be absolute") + if self.maya_is_true(absolute_procedural): + errors.append("Procedural path is set to be absolute") anatomy = instance.context.data["anatomy"] @@ -57,15 +60,20 @@ class ValidateAssRelativePaths(pyblish.api.InstancePlugin): for k in keys: paths.append("[{}]".format(k)) - self.log.info("discovered roots: {}".format(":".join(paths))) + self.log.debug("discovered roots: {}".format(":".join(paths))) - assert ":".join(paths) in texture_search_path, ( - "Project roots are not in texture_search_path" - ) + if ":".join(paths) not in texture_search_path: + errors.append(( + "Project roots {} are not in texture_search_path: {}" + ).format(paths, texture_search_path)) - assert ":".join(paths) in procedural_search_path, ( - "Project roots are not in procedural_search_path" - ) + if ":".join(paths) not in procedural_search_path: + errors.append(( + "Project roots {} are not in procedural_search_path: {}" + ).format(paths, procedural_search_path)) + + if errors: + raise PublishValidationError("\n".join(errors)) @classmethod def repair(cls, instance): diff --git a/openpype/hosts/maya/plugins/publish/validate_assembly_name.py b/openpype/hosts/maya/plugins/publish/validate_assembly_name.py index 02464b2302..bcc40760e0 100644 --- a/openpype/hosts/maya/plugins/publish/validate_assembly_name.py +++ b/openpype/hosts/maya/plugins/publish/validate_assembly_name.py @@ -1,6 +1,9 @@ import pyblish.api import maya.cmds as cmds import openpype.hosts.maya.api.action +from openpype.pipeline.publish import ( + PublishValidationError +) class ValidateAssemblyName(pyblish.api.InstancePlugin): @@ -47,5 +50,5 @@ class ValidateAssemblyName(pyblish.api.InstancePlugin): invalid = self.get_invalid(instance) if invalid: - raise RuntimeError("Found {} invalid named assembly " + raise PublishValidationError("Found {} invalid named assembly " "items".format(len(invalid))) diff --git a/openpype/hosts/maya/plugins/publish/validate_assembly_namespaces.py b/openpype/hosts/maya/plugins/publish/validate_assembly_namespaces.py index 229da63c42..41ef78aab4 100644 --- a/openpype/hosts/maya/plugins/publish/validate_assembly_namespaces.py +++ b/openpype/hosts/maya/plugins/publish/validate_assembly_namespaces.py @@ -1,6 +1,8 @@ import pyblish.api import openpype.hosts.maya.api.action - +from openpype.pipeline.publish import ( + PublishValidationError +) class ValidateAssemblyNamespaces(pyblish.api.InstancePlugin): """Ensure namespaces are not nested @@ -23,7 +25,7 @@ class ValidateAssemblyNamespaces(pyblish.api.InstancePlugin): self.log.info("Checking namespace for %s" % instance.name) if self.get_invalid(instance): - raise RuntimeError("Nested namespaces found") + raise PublishValidationError("Nested namespaces found") @classmethod def get_invalid(cls, instance): diff --git a/openpype/hosts/maya/plugins/publish/validate_assembly_transforms.py b/openpype/hosts/maya/plugins/publish/validate_assembly_transforms.py index d1bca4091b..a24455ebaa 100644 --- a/openpype/hosts/maya/plugins/publish/validate_assembly_transforms.py +++ b/openpype/hosts/maya/plugins/publish/validate_assembly_transforms.py @@ -1,9 +1,8 @@ import pyblish.api - from maya import cmds import openpype.hosts.maya.api.action -from openpype.pipeline.publish import RepairAction +from openpype.pipeline.publish import PublishValidationError, RepairAction class ValidateAssemblyModelTransforms(pyblish.api.InstancePlugin): @@ -38,8 +37,9 @@ class ValidateAssemblyModelTransforms(pyblish.api.InstancePlugin): def process(self, instance): invalid = self.get_invalid(instance) if invalid: - raise RuntimeError("Found {} invalid transforms of assembly " - "items".format(len(invalid))) + raise PublishValidationError( + ("Found {} invalid transforms of assembly " + "items").format(len(invalid))) @classmethod def get_invalid(cls, instance): @@ -90,6 +90,7 @@ class ValidateAssemblyModelTransforms(pyblish.api.InstancePlugin): """ from qtpy import QtWidgets + from openpype.hosts.maya.api import lib # Store namespace in variable, cosmetics thingy diff --git a/openpype/hosts/maya/plugins/publish/validate_attributes.py b/openpype/hosts/maya/plugins/publish/validate_attributes.py index 7ebd9d7d03..c76d979fbf 100644 --- a/openpype/hosts/maya/plugins/publish/validate_attributes.py +++ b/openpype/hosts/maya/plugins/publish/validate_attributes.py @@ -1,17 +1,16 @@ from collections import defaultdict -from maya import cmds - import pyblish.api +from maya import cmds from openpype.hosts.maya.api.lib import set_attribute from openpype.pipeline.publish import ( - RepairAction, - ValidateContentsOrder, -) + OptionalPyblishPluginMixin, PublishValidationError, RepairAction, + ValidateContentsOrder) -class ValidateAttributes(pyblish.api.InstancePlugin): +class ValidateAttributes(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Ensure attributes are consistent. Attributes to validate and their values comes from the @@ -32,13 +31,16 @@ class ValidateAttributes(pyblish.api.InstancePlugin): attributes = None def process(self, instance): + if not self.is_active(instance.data): + return + # Check for preset existence. if not self.attributes: return invalid = self.get_invalid(instance, compute=True) if invalid: - raise RuntimeError( + raise PublishValidationError( "Found attributes with invalid values: {}".format(invalid) ) diff --git a/openpype/hosts/maya/plugins/publish/validate_camera_attributes.py b/openpype/hosts/maya/plugins/publish/validate_camera_attributes.py index 13ea53a357..e5745612e9 100644 --- a/openpype/hosts/maya/plugins/publish/validate_camera_attributes.py +++ b/openpype/hosts/maya/plugins/publish/validate_camera_attributes.py @@ -1,8 +1,9 @@ +import pyblish.api from maya import cmds -import pyblish.api import openpype.hosts.maya.api.action -from openpype.pipeline.publish import ValidateContentsOrder +from openpype.pipeline.publish import ( + PublishValidationError, ValidateContentsOrder) class ValidateCameraAttributes(pyblish.api.InstancePlugin): @@ -65,4 +66,5 @@ class ValidateCameraAttributes(pyblish.api.InstancePlugin): invalid = self.get_invalid(instance) if invalid: - raise RuntimeError("Invalid camera attributes: %s" % invalid) + raise PublishValidationError( + "Invalid camera attributes: {}".format(invalid)) diff --git a/openpype/hosts/maya/plugins/publish/validate_camera_contents.py b/openpype/hosts/maya/plugins/publish/validate_camera_contents.py index 1ce8026fc2..767ac55718 100644 --- a/openpype/hosts/maya/plugins/publish/validate_camera_contents.py +++ b/openpype/hosts/maya/plugins/publish/validate_camera_contents.py @@ -1,8 +1,9 @@ +import pyblish.api from maya import cmds -import pyblish.api import openpype.hosts.maya.api.action -from openpype.pipeline.publish import ValidateContentsOrder +from openpype.pipeline.publish import ( + PublishValidationError, ValidateContentsOrder) class ValidateCameraContents(pyblish.api.InstancePlugin): @@ -34,7 +35,7 @@ class ValidateCameraContents(pyblish.api.InstancePlugin): cameras = cmds.ls(shapes, type='camera', long=True) if len(cameras) != 1: cls.log.error("Camera instance must have a single camera. " - "Found {0}: {1}".format(len(cameras), cameras)) + "Found {0}: {1}".format(len(cameras), cameras)) invalid.extend(cameras) # We need to check this edge case because returning an extended @@ -48,10 +49,12 @@ class ValidateCameraContents(pyblish.api.InstancePlugin): "members: {}".format(members)) return members - raise RuntimeError("No cameras found in empty instance.") + raise PublishValidationError( + "No cameras found in empty instance.") if not cls.validate_shapes: - cls.log.info("not validating shapes in the content") + cls.log.debug("Not validating shapes in the camera content" + " because 'validate shapes' is disabled") return invalid # non-camera shapes @@ -60,13 +63,10 @@ class ValidateCameraContents(pyblish.api.InstancePlugin): if shapes: shapes = list(shapes) cls.log.error("Camera instance should only contain camera " - "shapes. Found: {0}".format(shapes)) + "shapes. Found: {0}".format(shapes)) invalid.extend(shapes) - - invalid = list(set(invalid)) - return invalid def process(self, instance): @@ -74,5 +74,5 @@ class ValidateCameraContents(pyblish.api.InstancePlugin): invalid = self.get_invalid(instance) if invalid: - raise RuntimeError("Invalid camera contents: " + raise PublishValidationError("Invalid camera contents: " "{0}".format(invalid)) diff --git a/openpype/hosts/maya/plugins/publish/validate_color_sets.py b/openpype/hosts/maya/plugins/publish/validate_color_sets.py index 7ce3cca61a..766124cd9e 100644 --- a/openpype/hosts/maya/plugins/publish/validate_color_sets.py +++ b/openpype/hosts/maya/plugins/publish/validate_color_sets.py @@ -5,10 +5,12 @@ import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( RepairAction, ValidateMeshOrder, + OptionalPyblishPluginMixin ) -class ValidateColorSets(pyblish.api.Validator): +class ValidateColorSets(pyblish.api.Validator, + OptionalPyblishPluginMixin): """Validate all meshes in the instance have unlocked normals These can be removed manually through: @@ -40,6 +42,8 @@ class ValidateColorSets(pyblish.api.Validator): def process(self, instance): """Raise invalid when any of the meshes have ColorSets""" + if not self.is_active(instance.data): + return invalid = self.get_invalid(instance) diff --git a/openpype/hosts/maya/plugins/publish/validate_cycle_error.py b/openpype/hosts/maya/plugins/publish/validate_cycle_error.py index 210ee4127c..24da091246 100644 --- a/openpype/hosts/maya/plugins/publish/validate_cycle_error.py +++ b/openpype/hosts/maya/plugins/publish/validate_cycle_error.py @@ -1,13 +1,14 @@ -from maya import cmds - import pyblish.api +from maya import cmds import openpype.hosts.maya.api.action from openpype.hosts.maya.api.lib import maintained_selection -from openpype.pipeline.publish import ValidateContentsOrder +from openpype.pipeline.publish import ( + OptionalPyblishPluginMixin, PublishValidationError, ValidateContentsOrder) -class ValidateCycleError(pyblish.api.InstancePlugin): +class ValidateCycleError(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Validate nodes produce no cycle errors.""" order = ValidateContentsOrder + 0.05 @@ -18,9 +19,13 @@ class ValidateCycleError(pyblish.api.InstancePlugin): optional = True def process(self, instance): + if not self.is_active(instance.data): + return + invalid = self.get_invalid(instance) if invalid: - raise RuntimeError("Nodes produce a cycle error: %s" % invalid) + raise PublishValidationError( + "Nodes produce a cycle error: {}".format(invalid)) @classmethod def get_invalid(cls, instance): diff --git a/openpype/hosts/maya/plugins/publish/validate_frame_range.py b/openpype/hosts/maya/plugins/publish/validate_frame_range.py index ccb351c880..c6184ed348 100644 --- a/openpype/hosts/maya/plugins/publish/validate_frame_range.py +++ b/openpype/hosts/maya/plugins/publish/validate_frame_range.py @@ -4,7 +4,8 @@ from maya import cmds from openpype.pipeline.publish import ( RepairAction, ValidateContentsOrder, - PublishValidationError + PublishValidationError, + OptionalPyblishPluginMixin ) from openpype.hosts.maya.api.lib_rendersetup import ( get_attr_overrides, @@ -13,7 +14,8 @@ from openpype.hosts.maya.api.lib_rendersetup import ( from maya.app.renderSetup.model.override import AbsOverride -class ValidateFrameRange(pyblish.api.InstancePlugin): +class ValidateFrameRange(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Validates the frame ranges. This is an optional validator checking if the frame range on instance @@ -40,6 +42,9 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): exclude_families = [] def process(self, instance): + if not self.is_active(instance.data): + return + context = instance.context if instance.data.get("tileRendering"): self.log.info(( @@ -102,10 +107,12 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): "({}).".format(label.title(), values[1], values[0]) ) - for e in errors: - self.log.error(e) + if errors: + report = "Frame range settings are incorrect.\n\n" + for error in errors: + report += "- {}\n\n".format(error) - assert len(errors) == 0, ("Frame range settings are incorrect") + raise PublishValidationError(report, title="Frame Range incorrect") @classmethod def repair(cls, instance): @@ -150,7 +157,7 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): def repair_renderlayer(cls, instance): """Apply frame range in render settings""" - layer = instance.data["setMembers"] + layer = instance.data["renderlayer"] context = instance.context start_attr = "defaultRenderGlobals.startFrame" diff --git a/openpype/hosts/maya/plugins/publish/validate_glsl_plugin.py b/openpype/hosts/maya/plugins/publish/validate_glsl_plugin.py index 53c2cf548a..da065fcf94 100644 --- a/openpype/hosts/maya/plugins/publish/validate_glsl_plugin.py +++ b/openpype/hosts/maya/plugins/publish/validate_glsl_plugin.py @@ -4,7 +4,8 @@ from maya import cmds import pyblish.api from openpype.pipeline.publish import ( RepairAction, - ValidateContentsOrder + ValidateContentsOrder, + PublishValidationError ) @@ -21,7 +22,7 @@ class ValidateGLSLPlugin(pyblish.api.InstancePlugin): def process(self, instance): if not cmds.pluginInfo("maya2glTF", query=True, loaded=True): - raise RuntimeError("maya2glTF is not loaded") + raise PublishValidationError("maya2glTF is not loaded") @classmethod def repair(cls, instance): diff --git a/openpype/hosts/maya/plugins/publish/validate_instance_has_members.py b/openpype/hosts/maya/plugins/publish/validate_instance_has_members.py index 63849cfd12..7234f5a025 100644 --- a/openpype/hosts/maya/plugins/publish/validate_instance_has_members.py +++ b/openpype/hosts/maya/plugins/publish/validate_instance_has_members.py @@ -1,6 +1,9 @@ import pyblish.api import openpype.hosts.maya.api.action -from openpype.pipeline.publish import ValidateContentsOrder +from openpype.pipeline.publish import ( + ValidateContentsOrder, + PublishValidationError +) class ValidateInstanceHasMembers(pyblish.api.InstancePlugin): @@ -14,18 +17,23 @@ class ValidateInstanceHasMembers(pyblish.api.InstancePlugin): @classmethod def get_invalid(cls, instance): invalid = list() - if not instance.data["setMembers"]: + if not instance.data.get("setMembers"): objectset_name = instance.data['name'] invalid.append(objectset_name) return invalid def process(self, instance): - # Allow renderlayer and workfile to be empty - skip_families = ["workfile", "renderlayer", "rendersetup"] + # Allow renderlayer, rendersetup and workfile to be empty + skip_families = {"workfile", "renderlayer", "rendersetup"} if instance.data.get("family") in skip_families: return invalid = self.get_invalid(instance) if invalid: - raise RuntimeError("Empty instances found: {0}".format(invalid)) + # Invalid will always be a single entry, we log the single name + name = invalid[0] + raise PublishValidationError( + title="Empty instance", + message="Instance '{0}' is empty".format(name) + ) diff --git a/openpype/hosts/maya/plugins/publish/validate_instance_subset.py b/openpype/hosts/maya/plugins/publish/validate_instance_subset.py index bb3dde761c..69e16efe57 100644 --- a/openpype/hosts/maya/plugins/publish/validate_instance_subset.py +++ b/openpype/hosts/maya/plugins/publish/validate_instance_subset.py @@ -2,7 +2,10 @@ import pyblish.api import string import six -from openpype.pipeline.publish import ValidateContentsOrder +from openpype.pipeline.publish import ( + ValidateContentsOrder, + PublishValidationError +) # Allow only characters, numbers and underscore allowed = set(string.ascii_lowercase + @@ -28,7 +31,7 @@ class ValidateSubsetName(pyblish.api.InstancePlugin): # Ensure subset data if subset is None: - raise RuntimeError("Instance is missing subset " + raise PublishValidationError("Instance is missing subset " "name: {0}".format(subset)) if not isinstance(subset, six.string_types): diff --git a/openpype/hosts/maya/plugins/publish/validate_instancer_content.py b/openpype/hosts/maya/plugins/publish/validate_instancer_content.py index 32abe91f48..2f14693ef2 100644 --- a/openpype/hosts/maya/plugins/publish/validate_instancer_content.py +++ b/openpype/hosts/maya/plugins/publish/validate_instancer_content.py @@ -1,7 +1,8 @@ import maya.cmds as cmds - import pyblish.api + from openpype.hosts.maya.api import lib +from openpype.pipeline.publish import PublishValidationError class ValidateInstancerContent(pyblish.api.InstancePlugin): @@ -52,7 +53,8 @@ class ValidateInstancerContent(pyblish.api.InstancePlugin): error = True if error: - raise RuntimeError("Instancer Content is invalid. See log.") + raise PublishValidationError( + "Instancer Content is invalid. See log.") def check_geometry_hidden(self, export_members): diff --git a/openpype/hosts/maya/plugins/publish/validate_instancer_frame_ranges.py b/openpype/hosts/maya/plugins/publish/validate_instancer_frame_ranges.py index 3514cf0a98..fcfcdce8b6 100644 --- a/openpype/hosts/maya/plugins/publish/validate_instancer_frame_ranges.py +++ b/openpype/hosts/maya/plugins/publish/validate_instancer_frame_ranges.py @@ -1,7 +1,10 @@ import os import re + import pyblish.api +from openpype.pipeline.publish import PublishValidationError + VERBOSE = False @@ -164,5 +167,6 @@ class ValidateInstancerFrameRanges(pyblish.api.InstancePlugin): if invalid: self.log.error("Invalid nodes: {0}".format(invalid)) - raise RuntimeError("Invalid particle caches in instance. " - "See logs for details.") + raise PublishValidationError( + ("Invalid particle caches in instance. " + "See logs for details.")) diff --git a/openpype/hosts/maya/plugins/publish/validate_loaded_plugin.py b/openpype/hosts/maya/plugins/publish/validate_loaded_plugin.py index 624074aaf9..eac13053db 100644 --- a/openpype/hosts/maya/plugins/publish/validate_loaded_plugin.py +++ b/openpype/hosts/maya/plugins/publish/validate_loaded_plugin.py @@ -2,7 +2,10 @@ import os import pyblish.api import maya.cmds as cmds -from openpype.pipeline.publish import RepairContextAction +from openpype.pipeline.publish import ( + RepairContextAction, + PublishValidationError +) class ValidateLoadedPlugin(pyblish.api.ContextPlugin): @@ -35,7 +38,7 @@ class ValidateLoadedPlugin(pyblish.api.ContextPlugin): invalid = self.get_invalid(context) if invalid: - raise RuntimeError( + raise PublishValidationError( "Found forbidden plugin name: {}".format(", ".join(invalid)) ) diff --git a/openpype/hosts/maya/plugins/publish/validate_look_contents.py b/openpype/hosts/maya/plugins/publish/validate_look_contents.py index 2d38099f0f..433d997840 100644 --- a/openpype/hosts/maya/plugins/publish/validate_look_contents.py +++ b/openpype/hosts/maya/plugins/publish/validate_look_contents.py @@ -1,6 +1,11 @@ import pyblish.api import openpype.hosts.maya.api.action -from openpype.pipeline.publish import ValidateContentsOrder +from openpype.pipeline.publish import ( + PublishValidationError, + ValidateContentsOrder +) + + from maya import cmds # noqa @@ -28,19 +33,16 @@ class ValidateLookContents(pyblish.api.InstancePlugin): """Process all the nodes in the instance""" if not instance[:]: - raise RuntimeError("Instance is empty") + raise PublishValidationError("Instance is empty") invalid = self.get_invalid(instance) if invalid: - raise RuntimeError("'{}' has invalid look " + raise PublishValidationError("'{}' has invalid look " "content".format(instance.name)) @classmethod def get_invalid(cls, instance): """Get all invalid nodes""" - cls.log.info("Validating look content for " - "'{}'".format(instance.name)) - # check if data has the right attributes and content attributes = cls.validate_lookdata_attributes(instance) # check the looks for ID diff --git a/openpype/hosts/maya/plugins/publish/validate_look_default_shaders_connections.py b/openpype/hosts/maya/plugins/publish/validate_look_default_shaders_connections.py index 20f561a892..0109f6ebd5 100644 --- a/openpype/hosts/maya/plugins/publish/validate_look_default_shaders_connections.py +++ b/openpype/hosts/maya/plugins/publish/validate_look_default_shaders_connections.py @@ -1,7 +1,10 @@ from maya import cmds import pyblish.api -from openpype.pipeline.publish import ValidateContentsOrder +from openpype.pipeline.publish import ( + ValidateContentsOrder, + PublishValidationError +) class ValidateLookDefaultShadersConnections(pyblish.api.InstancePlugin): @@ -56,4 +59,4 @@ class ValidateLookDefaultShadersConnections(pyblish.api.InstancePlugin): invalid.append(plug) if invalid: - raise RuntimeError("Invalid connections.") + raise PublishValidationError("Invalid connections.") diff --git a/openpype/hosts/maya/plugins/publish/validate_look_id_reference_edits.py b/openpype/hosts/maya/plugins/publish/validate_look_id_reference_edits.py index a266a0fd74..5075d4050d 100644 --- a/openpype/hosts/maya/plugins/publish/validate_look_id_reference_edits.py +++ b/openpype/hosts/maya/plugins/publish/validate_look_id_reference_edits.py @@ -6,6 +6,7 @@ import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( RepairAction, ValidateContentsOrder, + PublishValidationError ) @@ -30,7 +31,7 @@ class ValidateLookIdReferenceEdits(pyblish.api.InstancePlugin): invalid = self.get_invalid(instance) if invalid: - raise RuntimeError("Invalid nodes %s" % (invalid,)) + raise PublishValidationError("Invalid nodes %s" % (invalid,)) @staticmethod def get_invalid(instance): diff --git a/openpype/hosts/maya/plugins/publish/validate_look_members_unique.py b/openpype/hosts/maya/plugins/publish/validate_look_members_unique.py index f81e511ff3..4e01b55249 100644 --- a/openpype/hosts/maya/plugins/publish/validate_look_members_unique.py +++ b/openpype/hosts/maya/plugins/publish/validate_look_members_unique.py @@ -1,8 +1,10 @@ from collections import defaultdict import pyblish.api + import openpype.hosts.maya.api.action -from openpype.pipeline.publish import ValidatePipelineOrder +from openpype.pipeline.publish import ( + PublishValidationError, ValidatePipelineOrder) class ValidateUniqueRelationshipMembers(pyblish.api.InstancePlugin): @@ -33,8 +35,9 @@ class ValidateUniqueRelationshipMembers(pyblish.api.InstancePlugin): invalid = self.get_invalid(instance) if invalid: - raise RuntimeError("Members found without non-unique IDs: " - "{0}".format(invalid)) + raise PublishValidationError( + ("Members found without non-unique IDs: " + "{0}").format(invalid)) @staticmethod def get_invalid(instance): diff --git a/openpype/hosts/maya/plugins/publish/validate_look_no_default_shaders.py b/openpype/hosts/maya/plugins/publish/validate_look_no_default_shaders.py index db6aadae8d..231331411b 100644 --- a/openpype/hosts/maya/plugins/publish/validate_look_no_default_shaders.py +++ b/openpype/hosts/maya/plugins/publish/validate_look_no_default_shaders.py @@ -2,7 +2,10 @@ from maya import cmds import pyblish.api import openpype.hosts.maya.api.action -from openpype.pipeline.publish import ValidateContentsOrder +from openpype.pipeline.publish import ( + ValidateContentsOrder, + PublishValidationError +) class ValidateLookNoDefaultShaders(pyblish.api.InstancePlugin): @@ -37,7 +40,7 @@ class ValidateLookNoDefaultShaders(pyblish.api.InstancePlugin): invalid = self.get_invalid(instance) if invalid: - raise RuntimeError("Invalid node relationships found: " + raise PublishValidationError("Invalid node relationships found: " "{0}".format(invalid)) @classmethod diff --git a/openpype/hosts/maya/plugins/publish/validate_look_sets.py b/openpype/hosts/maya/plugins/publish/validate_look_sets.py index 8434ddde04..657bab0479 100644 --- a/openpype/hosts/maya/plugins/publish/validate_look_sets.py +++ b/openpype/hosts/maya/plugins/publish/validate_look_sets.py @@ -1,7 +1,10 @@ import pyblish.api import openpype.hosts.maya.api.action from openpype.hosts.maya.api import lib -from openpype.pipeline.publish import ValidateContentsOrder +from openpype.pipeline.publish import ( + ValidateContentsOrder, + PublishValidationError +) class ValidateLookSets(pyblish.api.InstancePlugin): @@ -48,16 +51,13 @@ class ValidateLookSets(pyblish.api.InstancePlugin): invalid = self.get_invalid(instance) if invalid: - raise RuntimeError("'{}' has invalid look " + raise PublishValidationError("'{}' has invalid look " "content".format(instance.name)) @classmethod def get_invalid(cls, instance): """Get all invalid nodes""" - cls.log.info("Validating look content for " - "'{}'".format(instance.name)) - relationships = instance.data["lookData"]["relationships"] invalid = [] diff --git a/openpype/hosts/maya/plugins/publish/validate_look_shading_group.py b/openpype/hosts/maya/plugins/publish/validate_look_shading_group.py index 9b57b06ee7..dbe7a70e6a 100644 --- a/openpype/hosts/maya/plugins/publish/validate_look_shading_group.py +++ b/openpype/hosts/maya/plugins/publish/validate_look_shading_group.py @@ -5,6 +5,7 @@ import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( RepairAction, ValidateContentsOrder, + PublishValidationError ) @@ -27,7 +28,7 @@ class ValidateShadingEngine(pyblish.api.InstancePlugin): invalid = self.get_invalid(instance) if invalid: - raise RuntimeError( + raise PublishValidationError( "Found shading engines with incorrect naming:" "\n{}".format(invalid) ) diff --git a/openpype/hosts/maya/plugins/publish/validate_look_single_shader.py b/openpype/hosts/maya/plugins/publish/validate_look_single_shader.py index 788e440d12..acd761a944 100644 --- a/openpype/hosts/maya/plugins/publish/validate_look_single_shader.py +++ b/openpype/hosts/maya/plugins/publish/validate_look_single_shader.py @@ -1,8 +1,9 @@ +import pyblish.api from maya import cmds -import pyblish.api import openpype.hosts.maya.api.action -from openpype.pipeline.publish import ValidateContentsOrder +from openpype.pipeline.publish import ( + PublishValidationError, ValidateContentsOrder) class ValidateSingleShader(pyblish.api.InstancePlugin): @@ -23,9 +24,9 @@ class ValidateSingleShader(pyblish.api.InstancePlugin): invalid = self.get_invalid(instance) if invalid: - raise RuntimeError("Found shapes which don't have a single shader " - "assigned: " - "\n{}".format(invalid)) + raise PublishValidationError( + ("Found shapes which don't have a single shader " + "assigned:\n{}").format(invalid)) @classmethod def get_invalid(cls, instance): diff --git a/openpype/hosts/maya/plugins/publish/validate_maya_units.py b/openpype/hosts/maya/plugins/publish/validate_maya_units.py index 011df0846c..1d5619795f 100644 --- a/openpype/hosts/maya/plugins/publish/validate_maya_units.py +++ b/openpype/hosts/maya/plugins/publish/validate_maya_units.py @@ -7,6 +7,7 @@ from openpype.pipeline.context_tools import get_current_project_asset from openpype.pipeline.publish import ( RepairContextAction, ValidateSceneOrder, + PublishXmlValidationError ) @@ -26,6 +27,30 @@ class ValidateMayaUnits(pyblish.api.ContextPlugin): validate_fps = True + nice_message_format = ( + "- {setting} must be {required_value}. " + "Your scene is set to {current_value}" + ) + log_message_format = ( + "Maya scene {setting} must be '{required_value}'. " + "Current value is '{current_value}'." + ) + + @classmethod + def apply_settings(cls, project_settings, system_settings): + """Apply project settings to creator""" + settings = ( + project_settings["maya"]["publish"]["ValidateMayaUnits"] + ) + + cls.validate_linear_units = settings.get("validate_linear_units", + cls.validate_linear_units) + cls.linear_units = settings.get("linear_units", cls.linear_units) + cls.validate_angular_units = settings.get("validate_angular_units", + cls.validate_angular_units) + cls.angular_units = settings.get("angular_units", cls.angular_units) + cls.validate_fps = settings.get("validate_fps", cls.validate_fps) + def process(self, context): # Collected units @@ -34,15 +59,14 @@ class ValidateMayaUnits(pyblish.api.ContextPlugin): fps = context.data.get('fps') - # TODO replace query with using 'context.data["assetEntity"]' - asset_doc = get_current_project_asset() + asset_doc = context.data["assetEntity"] asset_fps = mayalib.convert_to_maya_fps(asset_doc["data"]["fps"]) self.log.info('Units (linear): {0}'.format(linearunits)) self.log.info('Units (angular): {0}'.format(angularunits)) self.log.info('Units (time): {0} FPS'.format(fps)) - valid = True + invalid = [] # Check if units are correct if ( @@ -50,26 +74,43 @@ class ValidateMayaUnits(pyblish.api.ContextPlugin): and linearunits and linearunits != self.linear_units ): - self.log.error("Scene linear units must be {}".format( - self.linear_units)) - valid = False + invalid.append({ + "setting": "Linear units", + "required_value": self.linear_units, + "current_value": linearunits + }) if ( self.validate_angular_units and angularunits and angularunits != self.angular_units ): - self.log.error("Scene angular units must be {}".format( - self.angular_units)) - valid = False + invalid.append({ + "setting": "Angular units", + "required_value": self.angular_units, + "current_value": angularunits + }) if self.validate_fps and fps and fps != asset_fps: - self.log.error( - "Scene must be {} FPS (now is {})".format(asset_fps, fps)) - valid = False + invalid.append({ + "setting": "FPS", + "required_value": asset_fps, + "current_value": fps + }) - if not valid: - raise RuntimeError("Invalid units set.") + if invalid: + + issues = [] + for data in invalid: + self.log.error(self.log_message_format.format(**data)) + issues.append(self.nice_message_format.format(**data)) + issues = "\n".join(issues) + + raise PublishXmlValidationError( + plugin=self, + message="Invalid maya scene units", + formatting_data={"issues": issues} + ) @classmethod def repair(cls, context): diff --git a/openpype/hosts/maya/plugins/publish/validate_mesh_arnold_attributes.py b/openpype/hosts/maya/plugins/publish/validate_mesh_arnold_attributes.py index a580a1c787..55624726ea 100644 --- a/openpype/hosts/maya/plugins/publish/validate_mesh_arnold_attributes.py +++ b/openpype/hosts/maya/plugins/publish/validate_mesh_arnold_attributes.py @@ -10,12 +10,15 @@ from openpype.hosts.maya.api.lib import ( set_attribute ) from openpype.pipeline.publish import ( + OptionalPyblishPluginMixin, RepairAction, ValidateMeshOrder, + PublishValidationError ) -class ValidateMeshArnoldAttributes(pyblish.api.InstancePlugin): +class ValidateMeshArnoldAttributes(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Validate the mesh has default Arnold attributes. It compares all Arnold attributes from a default mesh. This is to ensure @@ -30,12 +33,14 @@ class ValidateMeshArnoldAttributes(pyblish.api.InstancePlugin): openpype.hosts.maya.api.action.SelectInvalidAction, RepairAction ] + optional = True - if cmds.getAttr( - "defaultRenderGlobals.currentRenderer").lower() == "arnold": - active = True - else: - active = False + + @classmethod + def apply_settings(cls, project_settings, system_settings): + # todo: this should not be done this way + attr = "defaultRenderGlobals.currentRenderer" + cls.active = cmds.getAttr(attr).lower() == "arnold" @classmethod def get_default_attributes(cls): @@ -50,7 +55,7 @@ class ValidateMeshArnoldAttributes(pyblish.api.InstancePlugin): plug = "{}.{}".format(mesh, attr) try: defaults[attr] = get_attribute(plug) - except RuntimeError: + except PublishValidationError: cls.log.debug("Ignoring arnold attribute: {}".format(attr)) return defaults @@ -101,10 +106,12 @@ class ValidateMeshArnoldAttributes(pyblish.api.InstancePlugin): ) def process(self, instance): + if not self.is_active(instance.data): + return invalid = self.get_invalid_attributes(instance, compute=True) if invalid: - raise RuntimeError( + raise PublishValidationError( "Non-default Arnold attributes found in instance:" " {0}".format(invalid) ) diff --git a/openpype/hosts/maya/plugins/publish/validate_mesh_empty.py b/openpype/hosts/maya/plugins/publish/validate_mesh_empty.py index 848d66c4ae..c3264f3d98 100644 --- a/openpype/hosts/maya/plugins/publish/validate_mesh_empty.py +++ b/openpype/hosts/maya/plugins/publish/validate_mesh_empty.py @@ -4,7 +4,8 @@ import pyblish.api import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( RepairAction, - ValidateMeshOrder + ValidateMeshOrder, + PublishValidationError ) @@ -49,6 +50,6 @@ class ValidateMeshEmpty(pyblish.api.InstancePlugin): invalid = self.get_invalid(instance) if invalid: - raise RuntimeError( + raise PublishValidationError( "Meshes found in instance without any vertices: %s" % invalid ) diff --git a/openpype/hosts/maya/plugins/publish/validate_mesh_has_uv.py b/openpype/hosts/maya/plugins/publish/validate_mesh_has_uv.py index b7836b3e92..c382d1b983 100644 --- a/openpype/hosts/maya/plugins/publish/validate_mesh_has_uv.py +++ b/openpype/hosts/maya/plugins/publish/validate_mesh_has_uv.py @@ -2,11 +2,16 @@ from maya import cmds import pyblish.api import openpype.hosts.maya.api.action -from openpype.pipeline.publish import ValidateMeshOrder +from openpype.pipeline.publish import ( + ValidateMeshOrder, + OptionalPyblishPluginMixin, + PublishValidationError +) from openpype.hosts.maya.api.lib import len_flattened -class ValidateMeshHasUVs(pyblish.api.InstancePlugin): +class ValidateMeshHasUVs(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Validate the current mesh has UVs. It validates whether the current UV set has non-zero UVs and @@ -66,8 +71,19 @@ class ValidateMeshHasUVs(pyblish.api.InstancePlugin): return invalid def process(self, instance): + if not self.is_active(instance.data): + return invalid = self.get_invalid(instance) if invalid: - raise RuntimeError("Meshes found in instance without " - "valid UVs: {0}".format(invalid)) + + names = "
".join( + " - {}".format(node) for node in invalid + ) + + raise PublishValidationError( + title="Mesh has missing UVs", + message="Model meshes are required to have UVs.

" + "Meshes detected with invalid or missing UVs:
" + "{0}".format(names) + ) diff --git a/openpype/hosts/maya/plugins/publish/validate_mesh_no_negative_scale.py b/openpype/hosts/maya/plugins/publish/validate_mesh_no_negative_scale.py index 664e2b5772..48b4d0f557 100644 --- a/openpype/hosts/maya/plugins/publish/validate_mesh_no_negative_scale.py +++ b/openpype/hosts/maya/plugins/publish/validate_mesh_no_negative_scale.py @@ -2,7 +2,17 @@ from maya import cmds import pyblish.api import openpype.hosts.maya.api.action -from openpype.pipeline.publish import ValidateMeshOrder +from openpype.pipeline.publish import ( + ValidateMeshOrder, + PublishValidationError +) + + +def _as_report_list(values, prefix="- ", suffix="\n"): + """Return list as bullet point list for a report""" + if not values: + return "" + return prefix + (suffix + prefix).join(values) class ValidateMeshNoNegativeScale(pyblish.api.Validator): @@ -46,5 +56,9 @@ class ValidateMeshNoNegativeScale(pyblish.api.Validator): invalid = self.get_invalid(instance) if invalid: - raise ValueError("Meshes found with negative " - "scale: {0}".format(invalid)) + raise PublishValidationError( + "Meshes found with negative scale:\n\n{0}".format( + _as_report_list(sorted(invalid)) + ), + title="Negative scale" + ) diff --git a/openpype/hosts/maya/plugins/publish/validate_mesh_non_manifold.py b/openpype/hosts/maya/plugins/publish/validate_mesh_non_manifold.py index d7711da722..6fd63fb29f 100644 --- a/openpype/hosts/maya/plugins/publish/validate_mesh_non_manifold.py +++ b/openpype/hosts/maya/plugins/publish/validate_mesh_non_manifold.py @@ -2,7 +2,17 @@ from maya import cmds import pyblish.api import openpype.hosts.maya.api.action -from openpype.pipeline.publish import ValidateMeshOrder +from openpype.pipeline.publish import ( + ValidateMeshOrder, + PublishValidationError +) + + +def _as_report_list(values, prefix="- ", suffix="\n"): + """Return list as bullet point list for a report""" + if not values: + return "" + return prefix + (suffix + prefix).join(values) class ValidateMeshNonManifold(pyblish.api.Validator): @@ -16,7 +26,7 @@ class ValidateMeshNonManifold(pyblish.api.Validator): order = ValidateMeshOrder hosts = ['maya'] families = ['model'] - label = 'Mesh Non-Manifold Vertices/Edges' + label = 'Mesh Non-Manifold Edges/Vertices' actions = [openpype.hosts.maya.api.action.SelectInvalidAction] @staticmethod @@ -38,5 +48,9 @@ class ValidateMeshNonManifold(pyblish.api.Validator): invalid = self.get_invalid(instance) if invalid: - raise ValueError("Meshes found with non-manifold " - "edges/vertices: {0}".format(invalid)) + raise PublishValidationError( + "Meshes found with non-manifold edges/vertices:\n\n{0}".format( + _as_report_list(sorted(invalid)) + ), + title="Non-Manifold Edges/Vertices" + ) diff --git a/openpype/hosts/maya/plugins/publish/validate_mesh_non_zero_edge.py b/openpype/hosts/maya/plugins/publish/validate_mesh_non_zero_edge.py index b49ba85648..5ec6e5779b 100644 --- a/openpype/hosts/maya/plugins/publish/validate_mesh_non_zero_edge.py +++ b/openpype/hosts/maya/plugins/publish/validate_mesh_non_zero_edge.py @@ -3,10 +3,15 @@ from maya import cmds import pyblish.api import openpype.hosts.maya.api.action from openpype.hosts.maya.api import lib -from openpype.pipeline.publish import ValidateMeshOrder +from openpype.pipeline.publish import ( + ValidateMeshOrder, + OptionalPyblishPluginMixin, + PublishValidationError +) -class ValidateMeshNonZeroEdgeLength(pyblish.api.InstancePlugin): +class ValidateMeshNonZeroEdgeLength(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Validate meshes don't have edges with a zero length. Based on Maya's polyCleanup 'Edges with zero length'. @@ -65,7 +70,14 @@ class ValidateMeshNonZeroEdgeLength(pyblish.api.InstancePlugin): def process(self, instance): """Process all meshes""" + if not self.is_active(instance.data): + return + invalid = self.get_invalid(instance) if invalid: - raise RuntimeError("Meshes found with zero " - "edge length: {0}".format(invalid)) + label = "Meshes found with zero edge length" + raise PublishValidationError( + message="{}: {}".format(label, invalid), + title=label, + description="{}:\n- ".format(label) + "\n- ".join(invalid) + ) diff --git a/openpype/hosts/maya/plugins/publish/validate_mesh_normals_unlocked.py b/openpype/hosts/maya/plugins/publish/validate_mesh_normals_unlocked.py index 1b754a9829..7855e79119 100644 --- a/openpype/hosts/maya/plugins/publish/validate_mesh_normals_unlocked.py +++ b/openpype/hosts/maya/plugins/publish/validate_mesh_normals_unlocked.py @@ -6,10 +6,20 @@ import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( RepairAction, ValidateMeshOrder, + OptionalPyblishPluginMixin, + PublishValidationError ) -class ValidateMeshNormalsUnlocked(pyblish.api.Validator): +def _as_report_list(values, prefix="- ", suffix="\n"): + """Return list as bullet point list for a report""" + if not values: + return "" + return prefix + (suffix + prefix).join(values) + + +class ValidateMeshNormalsUnlocked(pyblish.api.Validator, + OptionalPyblishPluginMixin): """Validate all meshes in the instance have unlocked normals These can be unlocked manually through: @@ -47,12 +57,18 @@ class ValidateMeshNormalsUnlocked(pyblish.api.Validator): def process(self, instance): """Raise invalid when any of the meshes have locked normals""" + if not self.is_active(instance.data): + return invalid = self.get_invalid(instance) if invalid: - raise ValueError("Meshes found with " - "locked normals: {0}".format(invalid)) + raise PublishValidationError( + "Meshes found with locked normals:\n\n{0}".format( + _as_report_list(sorted(invalid)) + ), + title="Locked normals" + ) @classmethod def repair(cls, instance): diff --git a/openpype/hosts/maya/plugins/publish/validate_mesh_overlapping_uvs.py b/openpype/hosts/maya/plugins/publish/validate_mesh_overlapping_uvs.py index 7dd66eed6c..88e1507dd3 100644 --- a/openpype/hosts/maya/plugins/publish/validate_mesh_overlapping_uvs.py +++ b/openpype/hosts/maya/plugins/publish/validate_mesh_overlapping_uvs.py @@ -6,7 +6,18 @@ import maya.api.OpenMaya as om import pyblish.api import openpype.hosts.maya.api.action -from openpype.pipeline.publish import ValidateMeshOrder +from openpype.pipeline.publish import ( + ValidateMeshOrder, + OptionalPyblishPluginMixin, + PublishValidationError +) + + +def _as_report_list(values, prefix="- ", suffix="\n"): + """Return list as bullet point list for a report""" + if not values: + return "" + return prefix + (suffix + prefix).join(values) class GetOverlappingUVs(object): @@ -225,7 +236,8 @@ class GetOverlappingUVs(object): return faces -class ValidateMeshHasOverlappingUVs(pyblish.api.InstancePlugin): +class ValidateMeshHasOverlappingUVs(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """ Validate the current mesh overlapping UVs. It validates whether the current UVs are overlapping or not. @@ -281,9 +293,14 @@ class ValidateMeshHasOverlappingUVs(pyblish.api.InstancePlugin): return instance.data.get("overlapping_faces", []) def process(self, instance): + if not self.is_active(instance.data): + return invalid = self.get_invalid(instance, compute=True) if invalid: - raise RuntimeError( - "Meshes found with overlapping UVs: {0}".format(invalid) + raise PublishValidationError( + "Meshes found with overlapping UVs:\n\n{0}".format( + _as_report_list(sorted(invalid)) + ), + title="Overlapping UVs" ) diff --git a/openpype/hosts/maya/plugins/publish/validate_mesh_shader_connections.py b/openpype/hosts/maya/plugins/publish/validate_mesh_shader_connections.py index 2a0abe975c..1db7613999 100644 --- a/openpype/hosts/maya/plugins/publish/validate_mesh_shader_connections.py +++ b/openpype/hosts/maya/plugins/publish/validate_mesh_shader_connections.py @@ -5,6 +5,7 @@ import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( RepairAction, ValidateMeshOrder, + PublishValidationError ) @@ -102,7 +103,7 @@ class ValidateMeshShaderConnections(pyblish.api.InstancePlugin): invalid = self.get_invalid(instance) if invalid: - raise RuntimeError("Shapes found with invalid shader " + raise PublishValidationError("Shapes found with invalid shader " "connections: {0}".format(invalid)) @staticmethod diff --git a/openpype/hosts/maya/plugins/publish/validate_mesh_single_uv_set.py b/openpype/hosts/maya/plugins/publish/validate_mesh_single_uv_set.py index faa360380e..46364735b9 100644 --- a/openpype/hosts/maya/plugins/publish/validate_mesh_single_uv_set.py +++ b/openpype/hosts/maya/plugins/publish/validate_mesh_single_uv_set.py @@ -6,10 +6,12 @@ from openpype.hosts.maya.api import lib from openpype.pipeline.publish import ( RepairAction, ValidateMeshOrder, + OptionalPyblishPluginMixin ) -class ValidateMeshSingleUVSet(pyblish.api.InstancePlugin): +class ValidateMeshSingleUVSet(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Warn on multiple UV sets existing for each polygon mesh. On versions prior to Maya 2017 this will force no multiple uv sets because @@ -47,6 +49,8 @@ class ValidateMeshSingleUVSet(pyblish.api.InstancePlugin): def process(self, instance): """Process all the nodes in the instance 'objectSet'""" + if not self.is_active(instance.data): + return invalid = self.get_invalid(instance) diff --git a/openpype/hosts/maya/plugins/publish/validate_mesh_uv_set_map1.py b/openpype/hosts/maya/plugins/publish/validate_mesh_uv_set_map1.py index 40ddb916ca..116fecbcba 100644 --- a/openpype/hosts/maya/plugins/publish/validate_mesh_uv_set_map1.py +++ b/openpype/hosts/maya/plugins/publish/validate_mesh_uv_set_map1.py @@ -5,10 +5,12 @@ import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( RepairAction, ValidateMeshOrder, + OptionalPyblishPluginMixin ) -class ValidateMeshUVSetMap1(pyblish.api.InstancePlugin): +class ValidateMeshUVSetMap1(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Validate model's default set exists and is named 'map1'. In Maya meshes by default have a uv set named "map1" that cannot be @@ -48,6 +50,8 @@ class ValidateMeshUVSetMap1(pyblish.api.InstancePlugin): def process(self, instance): """Process all the nodes in the instance 'objectSet'""" + if not self.is_active(instance.data): + return invalid = self.get_invalid(instance) if invalid: diff --git a/openpype/hosts/maya/plugins/publish/validate_mesh_vertices_have_edges.py b/openpype/hosts/maya/plugins/publish/validate_mesh_vertices_have_edges.py index d885158004..7167859444 100644 --- a/openpype/hosts/maya/plugins/publish/validate_mesh_vertices_have_edges.py +++ b/openpype/hosts/maya/plugins/publish/validate_mesh_vertices_have_edges.py @@ -1,12 +1,10 @@ +import pyblish.api from maya import cmds -import pyblish.api import openpype.hosts.maya.api.action -from openpype.pipeline.publish import ( - RepairAction, - ValidateMeshOrder, -) from openpype.hosts.maya.api.lib import len_flattened +from openpype.pipeline.publish import ( + PublishValidationError, RepairAction, ValidateMeshOrder) class ValidateMeshVerticesHaveEdges(pyblish.api.InstancePlugin): @@ -40,8 +38,9 @@ class ValidateMeshVerticesHaveEdges(pyblish.api.InstancePlugin): # This fix only works in Maya 2016 EXT2 and newer if float(cmds.about(version=True)) <= 2016.0: - raise RuntimeError("Repair not supported in Maya version below " - "2016 EXT 2") + raise PublishValidationError( + ("Repair not supported in Maya version below " + "2016 EXT 2")) invalid = cls.get_invalid(instance) for node in invalid: @@ -76,5 +75,6 @@ class ValidateMeshVerticesHaveEdges(pyblish.api.InstancePlugin): invalid = self.get_invalid(instance) if invalid: - raise RuntimeError("Meshes found in instance with vertices that " - "have no edges: %s" % invalid) + raise PublishValidationError( + ("Meshes found in instance with vertices that " + "have no edges: {}").format(invalid)) diff --git a/openpype/hosts/maya/plugins/publish/validate_model_content.py b/openpype/hosts/maya/plugins/publish/validate_model_content.py index 723346a285..9ba458a416 100644 --- a/openpype/hosts/maya/plugins/publish/validate_model_content.py +++ b/openpype/hosts/maya/plugins/publish/validate_model_content.py @@ -3,7 +3,10 @@ from maya import cmds import pyblish.api import openpype.hosts.maya.api.action from openpype.hosts.maya.api import lib -from openpype.pipeline.publish import ValidateContentsOrder +from openpype.pipeline.publish import ( + ValidateContentsOrder, + PublishValidationError +) class ValidateModelContent(pyblish.api.InstancePlugin): @@ -28,7 +31,7 @@ class ValidateModelContent(pyblish.api.InstancePlugin): content_instance = instance.data.get("setMembers", None) if not content_instance: cls.log.error("Instance has no nodes!") - return True + return [instance.data["name"]] # All children will be included in the extracted export so we also # validate *all* descendents of the set members and we skip any @@ -97,4 +100,7 @@ class ValidateModelContent(pyblish.api.InstancePlugin): invalid = self.get_invalid(instance) if invalid: - raise RuntimeError("Model content is invalid. See log.") + raise PublishValidationError( + title="Model content is invalid", + message="See log for more details" + ) diff --git a/openpype/hosts/maya/plugins/publish/validate_model_name.py b/openpype/hosts/maya/plugins/publish/validate_model_name.py index 0e7adc640f..6948dcf724 100644 --- a/openpype/hosts/maya/plugins/publish/validate_model_name.py +++ b/openpype/hosts/maya/plugins/publish/validate_model_name.py @@ -1,22 +1,24 @@ # -*- coding: utf-8 -*- """Validate model nodes names.""" import os -import re import platform +import re +import gridfs +import pyblish.api from maya import cmds -import pyblish.api -from openpype.pipeline import legacy_io -from openpype.pipeline.publish import ValidateContentsOrder import openpype.hosts.maya.api.action +from openpype.client.mongo import OpenPypeMongoConnection from openpype.hosts.maya.api.shader_definition_editor import ( DEFINITION_FILENAME) -from openpype.client.mongo import OpenPypeMongoConnection -import gridfs +from openpype.pipeline import legacy_io +from openpype.pipeline.publish import ( + OptionalPyblishPluginMixin, PublishValidationError, ValidateContentsOrder) -class ValidateModelName(pyblish.api.InstancePlugin): +class ValidateModelName(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Validate name of model starts with (somename)_###_(materialID)_GEO @@ -148,7 +150,11 @@ class ValidateModelName(pyblish.api.InstancePlugin): def process(self, instance): """Plugin entry point.""" + if not self.is_active(instance.data): + return + invalid = self.get_invalid(instance) if invalid: - raise RuntimeError("Model naming is invalid. See the log.") + raise PublishValidationError( + "Model naming is invalid. See the log.") diff --git a/openpype/hosts/maya/plugins/publish/validate_mvlook_contents.py b/openpype/hosts/maya/plugins/publish/validate_mvlook_contents.py index 04db5a061b..68784a165d 100644 --- a/openpype/hosts/maya/plugins/publish/validate_mvlook_contents.py +++ b/openpype/hosts/maya/plugins/publish/validate_mvlook_contents.py @@ -1,14 +1,19 @@ import os import pyblish.api import openpype.hosts.maya.api.action -from openpype.pipeline.publish import ValidateContentsOrder +from openpype.pipeline.publish import ( + ValidateContentsOrder, + OptionalPyblishPluginMixin, + PublishValidationError +) COLOUR_SPACES = ['sRGB', 'linear', 'auto'] MIPMAP_EXTENSIONS = ['tdl'] -class ValidateMvLookContents(pyblish.api.InstancePlugin): +class ValidateMvLookContents(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): order = ValidateContentsOrder families = ['mvLook'] hosts = ['maya'] @@ -23,6 +28,9 @@ class ValidateMvLookContents(pyblish.api.InstancePlugin): enforced_intents = ['-', 'Final'] def process(self, instance): + if not self.is_active(instance.data): + return + intent = instance.context.data['intent']['value'] publishMipMap = instance.data["publishMipMap"] enforced = True @@ -35,7 +43,7 @@ class ValidateMvLookContents(pyblish.api.InstancePlugin): .format(intent)) if not instance[:]: - raise RuntimeError("Instance is empty") + raise PublishValidationError("Instance is empty") invalid = set() @@ -62,12 +70,12 @@ class ValidateMvLookContents(pyblish.api.InstancePlugin): if enforced: invalid.add(node) self.log.error(msg) - raise RuntimeError(msg) + raise PublishValidationError(msg) else: self.log.warning(msg) if invalid: - raise RuntimeError("'{}' has invalid look " + raise PublishValidationError("'{}' has invalid look " "content".format(instance.name)) def valid_file(self, fname): diff --git a/openpype/hosts/maya/plugins/publish/validate_no_animation.py b/openpype/hosts/maya/plugins/publish/validate_no_animation.py index 2e7cafe4ab..9ff189cf83 100644 --- a/openpype/hosts/maya/plugins/publish/validate_no_animation.py +++ b/openpype/hosts/maya/plugins/publish/validate_no_animation.py @@ -2,10 +2,22 @@ from maya import cmds import pyblish.api import openpype.hosts.maya.api.action -from openpype.pipeline.publish import ValidateContentsOrder +from openpype.pipeline.publish import ( + ValidateContentsOrder, + OptionalPyblishPluginMixin, + PublishValidationError +) -class ValidateNoAnimation(pyblish.api.Validator): +def _as_report_list(values, prefix="- ", suffix="\n"): + """Return list as bullet point list for a report""" + if not values: + return "" + return prefix + (suffix + prefix).join(values) + + +class ValidateNoAnimation(pyblish.api.Validator, + OptionalPyblishPluginMixin): """Ensure no keyframes on nodes in the Instance. Even though a Model would extract without animCurves correctly this avoids @@ -22,10 +34,17 @@ class ValidateNoAnimation(pyblish.api.Validator): actions = [openpype.hosts.maya.api.action.SelectInvalidAction] def process(self, instance): + if not self.is_active(instance.data): + return invalid = self.get_invalid(instance) if invalid: - raise RuntimeError("Keyframes found: {0}".format(invalid)) + raise PublishValidationError( + "Keyframes found on:\n\n{0}".format( + _as_report_list(sorted(invalid)) + ), + title="Keyframes on model" + ) @staticmethod def get_invalid(instance): diff --git a/openpype/hosts/maya/plugins/publish/validate_no_default_camera.py b/openpype/hosts/maya/plugins/publish/validate_no_default_camera.py index a4fb938d43..f0aa9261f7 100644 --- a/openpype/hosts/maya/plugins/publish/validate_no_default_camera.py +++ b/openpype/hosts/maya/plugins/publish/validate_no_default_camera.py @@ -2,7 +2,17 @@ from maya import cmds import pyblish.api import openpype.hosts.maya.api.action -from openpype.pipeline.publish import ValidateContentsOrder +from openpype.pipeline.publish import ( + ValidateContentsOrder, + PublishValidationError +) + + +def _as_report_list(values, prefix="- ", suffix="\n"): + """Return list as bullet point list for a report""" + if not values: + return "" + return prefix + (suffix + prefix).join(values) class ValidateNoDefaultCameras(pyblish.api.InstancePlugin): @@ -28,4 +38,10 @@ class ValidateNoDefaultCameras(pyblish.api.InstancePlugin): def process(self, instance): """Process all the cameras in the instance""" invalid = self.get_invalid(instance) - assert not invalid, "Default cameras found: {0}".format(invalid) + if invalid: + raise PublishValidationError( + "Default cameras found:\n\n{0}".format( + _as_report_list(sorted(invalid)) + ), + title="Default cameras" + ) diff --git a/openpype/hosts/maya/plugins/publish/validate_no_namespace.py b/openpype/hosts/maya/plugins/publish/validate_no_namespace.py index 0ff03f9165..13eeae5859 100644 --- a/openpype/hosts/maya/plugins/publish/validate_no_namespace.py +++ b/openpype/hosts/maya/plugins/publish/validate_no_namespace.py @@ -4,11 +4,19 @@ import pyblish.api from openpype.pipeline.publish import ( RepairAction, ValidateContentsOrder, + PublishValidationError ) import openpype.hosts.maya.api.action +def _as_report_list(values, prefix="- ", suffix="\n"): + """Return list as bullet point list for a report""" + if not values: + return "" + return prefix + (suffix + prefix).join(values) + + def get_namespace(node_name): # ensure only node's name (not parent path) node_name = node_name.rsplit("|", 1)[-1] @@ -36,7 +44,12 @@ class ValidateNoNamespace(pyblish.api.InstancePlugin): invalid = self.get_invalid(instance) if invalid: - raise ValueError("Namespaces found: {0}".format(invalid)) + raise PublishValidationError( + "Namespaces found:\n\n{0}".format( + _as_report_list(sorted(invalid)) + ), + title="Namespaces in model" + ) @classmethod def repair(cls, instance): diff --git a/openpype/hosts/maya/plugins/publish/validate_no_null_transforms.py b/openpype/hosts/maya/plugins/publish/validate_no_null_transforms.py index f77fc81dc1..187135fdf3 100644 --- a/openpype/hosts/maya/plugins/publish/validate_no_null_transforms.py +++ b/openpype/hosts/maya/plugins/publish/validate_no_null_transforms.py @@ -5,9 +5,17 @@ import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( RepairAction, ValidateContentsOrder, + PublishValidationError ) +def _as_report_list(values, prefix="- ", suffix="\n"): + """Return list as bullet point list for a report""" + if not values: + return "" + return prefix + (suffix + prefix).join(values) + + def has_shape_children(node): # Check if any descendants allDescendents = cmds.listRelatives(node, @@ -64,7 +72,12 @@ class ValidateNoNullTransforms(pyblish.api.InstancePlugin): """Process all the transform nodes in the instance """ invalid = self.get_invalid(instance) if invalid: - raise ValueError("Empty transforms found: {0}".format(invalid)) + raise PublishValidationError( + "Empty transforms found without shapes:\n\n{0}".format( + _as_report_list(sorted(invalid)) + ), + title="Empty transforms" + ) @classmethod def repair(cls, instance): diff --git a/openpype/hosts/maya/plugins/publish/validate_no_unknown_nodes.py b/openpype/hosts/maya/plugins/publish/validate_no_unknown_nodes.py index 2cfdc28128..6ae634be24 100644 --- a/openpype/hosts/maya/plugins/publish/validate_no_unknown_nodes.py +++ b/openpype/hosts/maya/plugins/publish/validate_no_unknown_nodes.py @@ -2,10 +2,22 @@ from maya import cmds import pyblish.api import openpype.hosts.maya.api.action -from openpype.pipeline.publish import ValidateContentsOrder +from openpype.pipeline.publish import ( + ValidateContentsOrder, + OptionalPyblishPluginMixin, + PublishValidationError +) -class ValidateNoUnknownNodes(pyblish.api.InstancePlugin): +def _as_report_list(values, prefix="- ", suffix="\n"): + """Return list as bullet point list for a report""" + if not values: + return "" + return prefix + (suffix + prefix).join(values) + + +class ValidateNoUnknownNodes(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Checks to see if there are any unknown nodes in the instance. This often happens if nodes from plug-ins are used but are not available @@ -29,7 +41,14 @@ class ValidateNoUnknownNodes(pyblish.api.InstancePlugin): def process(self, instance): """Process all the nodes in the instance""" + if not self.is_active(instance.data): + return invalid = self.get_invalid(instance) if invalid: - raise ValueError("Unknown nodes found: {0}".format(invalid)) + raise PublishValidationError( + "Unknown nodes found:\n\n{0}".format( + _as_report_list(sorted(invalid)) + ), + title="Unknown nodes" + ) diff --git a/openpype/hosts/maya/plugins/publish/validate_no_vraymesh.py b/openpype/hosts/maya/plugins/publish/validate_no_vraymesh.py index 27e5e6a006..22fd1edc29 100644 --- a/openpype/hosts/maya/plugins/publish/validate_no_vraymesh.py +++ b/openpype/hosts/maya/plugins/publish/validate_no_vraymesh.py @@ -1,5 +1,13 @@ import pyblish.api from maya import cmds +from openpype.pipeline.publish import PublishValidationError + + +def _as_report_list(values, prefix="- ", suffix="\n"): + """Return list as bullet point list for a report""" + if not values: + return "" + return prefix + (suffix + prefix).join(values) class ValidateNoVRayMesh(pyblish.api.InstancePlugin): @@ -11,6 +19,9 @@ class ValidateNoVRayMesh(pyblish.api.InstancePlugin): def process(self, instance): + if not cmds.pluginInfo("vrayformaya", query=True, loaded=True): + return + shapes = cmds.ls(instance, shapes=True, type="mesh") @@ -20,5 +31,11 @@ class ValidateNoVRayMesh(pyblish.api.InstancePlugin): source=True) or [] vray_meshes = cmds.ls(inputs, type='VRayMesh') if vray_meshes: - raise RuntimeError("Meshes that are VRayMeshes shouldn't " - "be pointcached: {0}".format(vray_meshes)) + raise PublishValidationError( + "Meshes that are V-Ray Proxies should not be in an Alembic " + "pointcache.\n" + "Found V-Ray proxies:\n\n{}".format( + _as_report_list(sorted(vray_meshes)) + ), + title="V-Ray Proxies in pointcache" + ) diff --git a/openpype/hosts/maya/plugins/publish/validate_node_ids.py b/openpype/hosts/maya/plugins/publish/validate_node_ids.py index 796f4c8d76..0c7d647014 100644 --- a/openpype/hosts/maya/plugins/publish/validate_node_ids.py +++ b/openpype/hosts/maya/plugins/publish/validate_node_ids.py @@ -1,6 +1,9 @@ import pyblish.api -from openpype.pipeline.publish import ValidatePipelineOrder +from openpype.pipeline.publish import ( + ValidatePipelineOrder, + PublishXmlValidationError +) import openpype.hosts.maya.api.action from openpype.hosts.maya.api import lib @@ -34,8 +37,14 @@ class ValidateNodeIDs(pyblish.api.InstancePlugin): # Ensure all nodes have a cbId invalid = self.get_invalid(instance) if invalid: - raise RuntimeError("Nodes found without " - "IDs: {0}".format(invalid)) + names = "\n".join( + "- {}".format(node) for node in invalid + ) + raise PublishXmlValidationError( + plugin=self, + message="Nodes found without IDs: {}".format(invalid), + formatting_data={"nodes": names} + ) @classmethod def get_invalid(cls, instance): diff --git a/openpype/hosts/maya/plugins/publish/validate_node_ids_deformed_shapes.py b/openpype/hosts/maya/plugins/publish/validate_node_ids_deformed_shapes.py index 68c47f3a96..643c970463 100644 --- a/openpype/hosts/maya/plugins/publish/validate_node_ids_deformed_shapes.py +++ b/openpype/hosts/maya/plugins/publish/validate_node_ids_deformed_shapes.py @@ -1,12 +1,10 @@ +import pyblish.api from maya import cmds -import pyblish.api import openpype.hosts.maya.api.action from openpype.hosts.maya.api import lib from openpype.pipeline.publish import ( - RepairAction, - ValidateContentsOrder, -) + PublishValidationError, RepairAction, ValidateContentsOrder) class ValidateNodeIdsDeformedShape(pyblish.api.InstancePlugin): @@ -35,8 +33,9 @@ class ValidateNodeIdsDeformedShape(pyblish.api.InstancePlugin): # if a deformer has been created on the shape invalid = self.get_invalid(instance) if invalid: - raise RuntimeError("Shapes found that are considered 'Deformed'" - "without object ids: {0}".format(invalid)) + raise PublishValidationError( + ("Shapes found that are considered 'Deformed'" + "without object ids: {0}").format(invalid)) @classmethod def get_invalid(cls, instance): diff --git a/openpype/hosts/maya/plugins/publish/validate_node_ids_in_database.py b/openpype/hosts/maya/plugins/publish/validate_node_ids_in_database.py index b2f28fd4e5..f15aa2efa8 100644 --- a/openpype/hosts/maya/plugins/publish/validate_node_ids_in_database.py +++ b/openpype/hosts/maya/plugins/publish/validate_node_ids_in_database.py @@ -1,10 +1,11 @@ import pyblish.api -from openpype.client import get_assets -from openpype.pipeline import legacy_io -from openpype.pipeline.publish import ValidatePipelineOrder import openpype.hosts.maya.api.action +from openpype.client import get_assets from openpype.hosts.maya.api import lib +from openpype.pipeline import legacy_io +from openpype.pipeline.publish import ( + PublishValidationError, ValidatePipelineOrder) class ValidateNodeIdsInDatabase(pyblish.api.InstancePlugin): @@ -29,9 +30,9 @@ class ValidateNodeIdsInDatabase(pyblish.api.InstancePlugin): def process(self, instance): invalid = self.get_invalid(instance) if invalid: - raise RuntimeError("Found asset IDs which are not related to " - "current project in instance: " - "`%s`" % instance.name) + raise PublishValidationError( + ("Found asset IDs which are not related to " + "current project in instance: `{}`").format(instance.name)) @classmethod def get_invalid(cls, instance): diff --git a/openpype/hosts/maya/plugins/publish/validate_node_ids_related.py b/openpype/hosts/maya/plugins/publish/validate_node_ids_related.py index f901dc58c4..52e706fec9 100644 --- a/openpype/hosts/maya/plugins/publish/validate_node_ids_related.py +++ b/openpype/hosts/maya/plugins/publish/validate_node_ids_related.py @@ -1,11 +1,13 @@ import pyblish.api -from openpype.pipeline.publish import ValidatePipelineOrder import openpype.hosts.maya.api.action from openpype.hosts.maya.api import lib +from openpype.pipeline.publish import ( + OptionalPyblishPluginMixin, PublishValidationError, ValidatePipelineOrder) -class ValidateNodeIDsRelated(pyblish.api.InstancePlugin): +class ValidateNodeIDsRelated(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Validate nodes have a related Colorbleed Id to the instance.data[asset] """ @@ -23,12 +25,15 @@ class ValidateNodeIDsRelated(pyblish.api.InstancePlugin): def process(self, instance): """Process all nodes in instance (including hierarchy)""" + if not self.is_active(instance.data): + return + # Ensure all nodes have a cbId invalid = self.get_invalid(instance) if invalid: - raise RuntimeError("Nodes IDs found that are not related to asset " - "'{}' : {}".format(instance.data['asset'], - invalid)) + raise PublishValidationError( + ("Nodes IDs found that are not related to asset " + "'{}' : {}").format(instance.data['asset'], invalid)) @classmethod def get_invalid(cls, instance): diff --git a/openpype/hosts/maya/plugins/publish/validate_node_ids_unique.py b/openpype/hosts/maya/plugins/publish/validate_node_ids_unique.py index f7a5e6e292..61386fc939 100644 --- a/openpype/hosts/maya/plugins/publish/validate_node_ids_unique.py +++ b/openpype/hosts/maya/plugins/publish/validate_node_ids_unique.py @@ -1,7 +1,10 @@ from collections import defaultdict import pyblish.api -from openpype.pipeline.publish import ValidatePipelineOrder +from openpype.pipeline.publish import ( + ValidatePipelineOrder, + PublishValidationError +) import openpype.hosts.maya.api.action from openpype.hosts.maya.api import lib @@ -29,8 +32,13 @@ class ValidateNodeIdsUnique(pyblish.api.InstancePlugin): # Ensure all nodes have a cbId invalid = self.get_invalid(instance) if invalid: - raise RuntimeError("Nodes found with non-unique " - "asset IDs: {0}".format(invalid)) + label = "Nodes found with non-unique asset IDs" + raise PublishValidationError( + message="{}: {}".format(label, invalid), + title="Non-unique asset ids on nodes", + description="{}\n- {}".format(label, + "\n- ".join(sorted(invalid))) + ) @classmethod def get_invalid(cls, instance): diff --git a/openpype/hosts/maya/plugins/publish/validate_plugin_path_attributes.py b/openpype/hosts/maya/plugins/publish/validate_plugin_path_attributes.py index 6135c9c695..78334cd01f 100644 --- a/openpype/hosts/maya/plugins/publish/validate_plugin_path_attributes.py +++ b/openpype/hosts/maya/plugins/publish/validate_plugin_path_attributes.py @@ -4,7 +4,10 @@ from maya import cmds import pyblish.api -from openpype.pipeline.publish import ValidateContentsOrder +from openpype.pipeline.publish import ( + ValidateContentsOrder, + PublishValidationError +) class ValidatePluginPathAttributes(pyblish.api.InstancePlugin): @@ -48,5 +51,5 @@ class ValidatePluginPathAttributes(pyblish.api.InstancePlugin): """Process all directories Set as Filenames in Non-Maya Nodes""" invalid = self.get_invalid(instance) if invalid: - raise RuntimeError("Non-existent Path " + raise PublishValidationError("Non-existent Path " "found: {0}".format(invalid)) diff --git a/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py b/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py index 78bb022785..f9aa7f82d0 100644 --- a/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py +++ b/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py @@ -1,10 +1,8 @@ +import pyblish.api from maya import cmds -import pyblish.api from openpype.pipeline.publish import ( - RepairAction, - ValidateContentsOrder, -) + PublishValidationError, RepairAction, ValidateContentsOrder) class ValidateRenderImageRule(pyblish.api.InstancePlugin): @@ -27,12 +25,12 @@ class ValidateRenderImageRule(pyblish.api.InstancePlugin): required_images_rule = self.get_default_render_image_folder(instance) current_images_rule = cmds.workspace(fileRuleEntry="images") - assert current_images_rule == required_images_rule, ( - "Invalid workspace `images` file rule value: '{}'. " - "Must be set to: '{}'".format( - current_images_rule, required_images_rule - ) - ) + if current_images_rule != required_images_rule: + raise PublishValidationError( + ( + "Invalid workspace `images` file rule value: '{}'. " + "Must be set to: '{}'" + ).format(current_images_rule, required_images_rule)) @classmethod def repair(cls, instance): diff --git a/openpype/hosts/maya/plugins/publish/validate_render_no_default_cameras.py b/openpype/hosts/maya/plugins/publish/validate_render_no_default_cameras.py index 67ece75af8..9d4410186b 100644 --- a/openpype/hosts/maya/plugins/publish/validate_render_no_default_cameras.py +++ b/openpype/hosts/maya/plugins/publish/validate_render_no_default_cameras.py @@ -3,7 +3,10 @@ from maya import cmds import pyblish.api import openpype.hosts.maya.api.action -from openpype.pipeline.publish import ValidateContentsOrder +from openpype.pipeline.publish import ( + ValidateContentsOrder, + PublishValidationError, +) class ValidateRenderNoDefaultCameras(pyblish.api.InstancePlugin): @@ -31,5 +34,7 @@ class ValidateRenderNoDefaultCameras(pyblish.api.InstancePlugin): """Process all the cameras in the instance""" invalid = self.get_invalid(instance) if invalid: - raise RuntimeError("Renderable default cameras " - "found: {0}".format(invalid)) + raise PublishValidationError( + title="Rendering default cameras", + message="Renderable default cameras " + "found: {0}".format(invalid)) diff --git a/openpype/hosts/maya/plugins/publish/validate_render_single_camera.py b/openpype/hosts/maya/plugins/publish/validate_render_single_camera.py index 77322fefd5..2c0d604175 100644 --- a/openpype/hosts/maya/plugins/publish/validate_render_single_camera.py +++ b/openpype/hosts/maya/plugins/publish/validate_render_single_camera.py @@ -5,7 +5,10 @@ from maya import cmds import openpype.hosts.maya.api.action from openpype.hosts.maya.api.lib_rendersettings import RenderSettings -from openpype.pipeline.publish import ValidateContentsOrder +from openpype.pipeline.publish import ( + ValidateContentsOrder, + PublishValidationError +) class ValidateRenderSingleCamera(pyblish.api.InstancePlugin): @@ -28,7 +31,7 @@ class ValidateRenderSingleCamera(pyblish.api.InstancePlugin): """Process all the cameras in the instance""" invalid = self.get_invalid(instance) if invalid: - raise RuntimeError("Invalid cameras for render.") + raise PublishValidationError("Invalid cameras for render.") @classmethod def get_invalid(cls, instance): diff --git a/openpype/hosts/maya/plugins/publish/validate_renderlayer_aovs.py b/openpype/hosts/maya/plugins/publish/validate_renderlayer_aovs.py index 7919a6eaa1..f8de983e06 100644 --- a/openpype/hosts/maya/plugins/publish/validate_renderlayer_aovs.py +++ b/openpype/hosts/maya/plugins/publish/validate_renderlayer_aovs.py @@ -1,8 +1,9 @@ import pyblish.api -from openpype.client import get_subset_by_name import openpype.hosts.maya.api.action +from openpype.client import get_subset_by_name from openpype.pipeline import legacy_io +from openpype.pipeline.publish import PublishValidationError class ValidateRenderLayerAOVs(pyblish.api.InstancePlugin): @@ -30,7 +31,8 @@ class ValidateRenderLayerAOVs(pyblish.api.InstancePlugin): def process(self, instance): invalid = self.get_invalid(instance) if invalid: - raise RuntimeError("Found unregistered subsets: {}".format(invalid)) + raise PublishValidationError( + "Found unregistered subsets: {}".format(invalid)) def get_invalid(self, instance): invalid = [] diff --git a/openpype/hosts/maya/plugins/publish/validate_rendersettings.py b/openpype/hosts/maya/plugins/publish/validate_rendersettings.py index 71b91b8e54..dccb4ade78 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rendersettings.py +++ b/openpype/hosts/maya/plugins/publish/validate_rendersettings.py @@ -9,6 +9,7 @@ import pyblish.api from openpype.pipeline.publish import ( RepairAction, ValidateContentsOrder, + PublishValidationError, ) from openpype.hosts.maya.api import lib @@ -112,17 +113,20 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin): def process(self, instance): invalid = self.get_invalid(instance) - assert invalid is False, ("Invalid render settings " - "found for '{}'!".format(instance.name)) + if invalid: + raise PublishValidationError( + title="Invalid Render Settings", + message=("Invalid render settings found " + "for '{}'!".format(instance.name)) + ) @classmethod def get_invalid(cls, instance): invalid = False - multipart = False renderer = instance.data['renderer'] - layer = instance.data['setMembers'] + layer = instance.data['renderlayer'] cameras = instance.data.get("cameras", []) # Get the node attributes for current renderer @@ -280,7 +284,7 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin): render_value = cmds.getAttr( "{}.{}".format(node, data["attribute"]) ) - except RuntimeError: + except PublishValidationError: invalid = True cls.log.error( "Cannot get value of {}.{}".format( diff --git a/openpype/hosts/maya/plugins/publish/validate_resources.py b/openpype/hosts/maya/plugins/publish/validate_resources.py index b7bd47ad0a..7d894a2bef 100644 --- a/openpype/hosts/maya/plugins/publish/validate_resources.py +++ b/openpype/hosts/maya/plugins/publish/validate_resources.py @@ -2,7 +2,10 @@ import os from collections import defaultdict import pyblish.api -from openpype.pipeline.publish import ValidateContentsOrder +from openpype.pipeline.publish import ( + ValidateContentsOrder, + PublishValidationError +) class ValidateResources(pyblish.api.InstancePlugin): @@ -54,4 +57,4 @@ class ValidateResources(pyblish.api.InstancePlugin): ) if invalid_resources: - raise RuntimeError("Invalid resources in instance.") + raise PublishValidationError("Invalid resources in instance.") diff --git a/openpype/hosts/maya/plugins/publish/validate_rig_contents.py b/openpype/hosts/maya/plugins/publish/validate_rig_contents.py index 1096c95486..7b5392f8f9 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rig_contents.py +++ b/openpype/hosts/maya/plugins/publish/validate_rig_contents.py @@ -1,7 +1,8 @@ +import pyblish.api from maya import cmds -import pyblish.api -from openpype.pipeline.publish import ValidateContentsOrder +from openpype.pipeline.publish import ( + PublishValidationError, ValidateContentsOrder) class ValidateRigContents(pyblish.api.InstancePlugin): @@ -31,8 +32,9 @@ class ValidateRigContents(pyblish.api.InstancePlugin): # in the rig instance set_members = instance.data['setMembers'] if not cmds.ls(set_members, type="dagNode", long=True): - raise RuntimeError("No dag nodes in the pointcache instance. " - "(Empty instance?)") + raise PublishValidationError( + ("No dag nodes in the pointcache instance. " + "(Empty instance?)")) # Ensure contents in sets and retrieve long path for all objects output_content = cmds.sets("out_SET", query=True) or [] @@ -79,7 +81,8 @@ class ValidateRigContents(pyblish.api.InstancePlugin): error = True if error: - raise RuntimeError("Invalid rig content. See log for details.") + raise PublishValidationError( + "Invalid rig content. See log for details.") def validate_geometry(self, set_members): """Check if the out set passes the validations diff --git a/openpype/hosts/maya/plugins/publish/validate_rig_controllers.py b/openpype/hosts/maya/plugins/publish/validate_rig_controllers.py index 1e42abdcd9..7bbf4257ab 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rig_controllers.py +++ b/openpype/hosts/maya/plugins/publish/validate_rig_controllers.py @@ -5,6 +5,7 @@ import pyblish.api from openpype.pipeline.publish import ( ValidateContentsOrder, RepairAction, + PublishValidationError ) import openpype.hosts.maya.api.action from openpype.hosts.maya.api.lib import undo_chunk @@ -51,7 +52,7 @@ class ValidateRigControllers(pyblish.api.InstancePlugin): def process(self, instance): invalid = self.get_invalid(instance) if invalid: - raise RuntimeError('{} failed, see log ' + raise PublishValidationError('{} failed, see log ' 'information'.format(self.label)) @classmethod diff --git a/openpype/hosts/maya/plugins/publish/validate_rig_controllers_arnold_attributes.py b/openpype/hosts/maya/plugins/publish/validate_rig_controllers_arnold_attributes.py index 55b2ebd6d8..842c1de01b 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rig_controllers_arnold_attributes.py +++ b/openpype/hosts/maya/plugins/publish/validate_rig_controllers_arnold_attributes.py @@ -5,7 +5,9 @@ import pyblish.api from openpype.pipeline.publish import ( ValidateContentsOrder, RepairAction, + PublishValidationError ) + from openpype.hosts.maya.api import lib import openpype.hosts.maya.api.action @@ -48,7 +50,7 @@ class ValidateRigControllersArnoldAttributes(pyblish.api.InstancePlugin): def process(self, instance): invalid = self.get_invalid(instance) if invalid: - raise RuntimeError('{} failed, see log ' + raise PublishValidationError('{} failed, see log ' 'information'.format(self.label)) @classmethod diff --git a/openpype/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py b/openpype/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py index 03ba381f8d..39f0941faa 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py +++ b/openpype/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py @@ -7,6 +7,7 @@ from openpype.hosts.maya.api import lib from openpype.pipeline.publish import ( RepairAction, ValidateContentsOrder, + PublishValidationError ) @@ -37,7 +38,7 @@ class ValidateRigOutSetNodeIds(pyblish.api.InstancePlugin): # if a deformer has been created on the shape invalid = self.get_invalid(instance) if invalid: - raise RuntimeError("Nodes found with mismatching " + raise PublishValidationError("Nodes found with mismatching " "IDs: {0}".format(invalid)) @classmethod diff --git a/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py b/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py index cba70a21b7..75447fdfea 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py +++ b/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py @@ -9,6 +9,7 @@ from openpype.hosts.maya.api.lib import get_id, set_id from openpype.pipeline.publish import ( RepairAction, ValidateContentsOrder, + PublishValidationError ) @@ -34,7 +35,7 @@ class ValidateRigOutputIds(pyblish.api.InstancePlugin): def process(self, instance): invalid = self.get_invalid(instance, compute=True) if invalid: - raise RuntimeError("Found nodes with mismatched IDs.") + raise PublishValidationError("Found nodes with mismatched IDs.") @classmethod def get_invalid(cls, instance, compute=False): @@ -107,7 +108,7 @@ class ValidateRigOutputIds(pyblish.api.InstancePlugin): set_id(instance_node, id_to_set, overwrite=True) if multiple_ids_match: - raise RuntimeError( + raise PublishValidationError( "Multiple matched ids found. Please repair manually: " "{}".format(multiple_ids_match) ) diff --git a/openpype/hosts/maya/plugins/publish/validate_scene_set_workspace.py b/openpype/hosts/maya/plugins/publish/validate_scene_set_workspace.py index f1fa4d3c4c..b48d67e416 100644 --- a/openpype/hosts/maya/plugins/publish/validate_scene_set_workspace.py +++ b/openpype/hosts/maya/plugins/publish/validate_scene_set_workspace.py @@ -1,10 +1,10 @@ import os import maya.cmds as cmds - import pyblish.api -from openpype.pipeline.publish import ValidatePipelineOrder +from openpype.pipeline.publish import ( + PublishValidationError, ValidatePipelineOrder) def is_subdir(path, root_dir): @@ -37,10 +37,11 @@ class ValidateSceneSetWorkspace(pyblish.api.ContextPlugin): scene_name = cmds.file(query=True, sceneName=True) if not scene_name: - raise RuntimeError("Scene hasn't been saved. Workspace can't be " - "validated.") + raise PublishValidationError( + "Scene hasn't been saved. Workspace can't be validated.") root_dir = cmds.workspace(query=True, rootDirectory=True) if not is_subdir(scene_name, root_dir): - raise RuntimeError("Maya workspace is not set correctly.") + raise PublishValidationError( + "Maya workspace is not set correctly.") diff --git a/openpype/hosts/maya/plugins/publish/validate_shader_name.py b/openpype/hosts/maya/plugins/publish/validate_shader_name.py index 034db471da..36bb2c1fee 100644 --- a/openpype/hosts/maya/plugins/publish/validate_shader_name.py +++ b/openpype/hosts/maya/plugins/publish/validate_shader_name.py @@ -1,13 +1,15 @@ import re -from maya import cmds import pyblish.api +from maya import cmds import openpype.hosts.maya.api.action -from openpype.pipeline.publish import ValidateContentsOrder +from openpype.pipeline.publish import ( + OptionalPyblishPluginMixin, PublishValidationError, ValidateContentsOrder) -class ValidateShaderName(pyblish.api.InstancePlugin): +class ValidateShaderName(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Validate shader name assigned. It should be _<*>_SHD @@ -23,12 +25,14 @@ class ValidateShaderName(pyblish.api.InstancePlugin): # The default connections to check def process(self, instance): + if not self.is_active(instance.data): + return invalid = self.get_invalid(instance) if invalid: - raise RuntimeError("Found shapes with invalid shader names " - "assigned: " - "\n{}".format(invalid)) + raise PublishValidationError( + ("Found shapes with invalid shader names " + "assigned:\n{}").format(invalid)) @classmethod def get_invalid(cls, instance): diff --git a/openpype/hosts/maya/plugins/publish/validate_shape_default_names.py b/openpype/hosts/maya/plugins/publish/validate_shape_default_names.py index 4ab669f46b..d8ad366ed8 100644 --- a/openpype/hosts/maya/plugins/publish/validate_shape_default_names.py +++ b/openpype/hosts/maya/plugins/publish/validate_shape_default_names.py @@ -8,6 +8,7 @@ import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( ValidateContentsOrder, RepairAction, + OptionalPyblishPluginMixin ) @@ -15,7 +16,8 @@ def short_name(node): return node.rsplit("|", 1)[-1].rsplit(":", 1)[-1] -class ValidateShapeDefaultNames(pyblish.api.InstancePlugin): +class ValidateShapeDefaultNames(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Validates that Shape names are using Maya's default format. When you create a new polygon cube Maya will name the transform @@ -77,6 +79,8 @@ class ValidateShapeDefaultNames(pyblish.api.InstancePlugin): def process(self, instance): """Process all the shape nodes in the instance""" + if not self.is_active(instance.data): + return invalid = self.get_invalid(instance) if invalid: diff --git a/openpype/hosts/maya/plugins/publish/validate_skeletalmesh_triangulated.py b/openpype/hosts/maya/plugins/publish/validate_skeletalmesh_triangulated.py index c0a9ddcf69..701c80a8af 100644 --- a/openpype/hosts/maya/plugins/publish/validate_skeletalmesh_triangulated.py +++ b/openpype/hosts/maya/plugins/publish/validate_skeletalmesh_triangulated.py @@ -7,8 +7,10 @@ from openpype.hosts.maya.api.action import ( from openpype.pipeline.publish import ( RepairAction, ValidateContentsOrder, + PublishValidationError ) + from maya import cmds @@ -28,7 +30,7 @@ class ValidateSkeletalMeshTriangulated(pyblish.api.InstancePlugin): def process(self, instance): invalid = self.get_invalid(instance) if invalid: - raise RuntimeError( + raise PublishValidationError( "The following objects needs to be triangulated: " "{}".format(invalid)) diff --git a/openpype/hosts/maya/plugins/publish/validate_step_size.py b/openpype/hosts/maya/plugins/publish/validate_step_size.py index 294458f63c..493a6ee65c 100644 --- a/openpype/hosts/maya/plugins/publish/validate_step_size.py +++ b/openpype/hosts/maya/plugins/publish/validate_step_size.py @@ -1,7 +1,10 @@ import pyblish.api import openpype.hosts.maya.api.action -from openpype.pipeline.publish import ValidateContentsOrder +from openpype.pipeline.publish import ( + PublishValidationError, + ValidateContentsOrder +) class ValidateStepSize(pyblish.api.InstancePlugin): @@ -40,4 +43,5 @@ class ValidateStepSize(pyblish.api.InstancePlugin): invalid = self.get_invalid(instance) if invalid: - raise RuntimeError("Invalid instances found: {0}".format(invalid)) + raise PublishValidationError( + "Invalid instances found: {0}".format(invalid)) diff --git a/openpype/hosts/maya/plugins/publish/validate_transform_naming_suffix.py b/openpype/hosts/maya/plugins/publish/validate_transform_naming_suffix.py index b2a83a80fb..cbc7ee9d5c 100644 --- a/openpype/hosts/maya/plugins/publish/validate_transform_naming_suffix.py +++ b/openpype/hosts/maya/plugins/publish/validate_transform_naming_suffix.py @@ -5,10 +5,15 @@ from maya import cmds import pyblish.api import openpype.hosts.maya.api.action -from openpype.pipeline.publish import ValidateContentsOrder +from openpype.pipeline.publish import ( + ValidateContentsOrder, + OptionalPyblishPluginMixin, + PublishValidationError +) -class ValidateTransformNamingSuffix(pyblish.api.InstancePlugin): +class ValidateTransformNamingSuffix(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Validates transform suffix based on the type of its children shapes. Suffices must be: @@ -47,8 +52,8 @@ class ValidateTransformNamingSuffix(pyblish.api.InstancePlugin): def get_table_for_invalid(cls): ss = [] for k, v in cls.SUFFIX_NAMING_TABLE.items(): - ss.append(" - {}: {}".format(k, ", ".join(v))) - return "\n".join(ss) + ss.append(" - {}: {}".format(k, ", ".join(v))) + return "
".join(ss) @staticmethod def is_valid_name(node_name, shape_type, @@ -110,9 +115,20 @@ class ValidateTransformNamingSuffix(pyblish.api.InstancePlugin): instance (:class:`pyblish.api.Instance`): published instance. """ + if not self.is_active(instance.data): + return + invalid = self.get_invalid(instance) if invalid: valid = self.get_table_for_invalid() - raise ValueError("Incorrectly named geometry " - "transforms: {0}, accepted suffixes are: " - "\n{1}".format(invalid, valid)) + + names = "
".join( + " - {}".format(node) for node in invalid + ) + valid = valid.replace("\n", "
") + + raise PublishValidationError( + title="Invalid naming suffix", + message="Valid suffixes are:
{0}

" + "Incorrectly named geometry transforms:
{1}" + "".format(valid, names)) diff --git a/openpype/hosts/maya/plugins/publish/validate_transform_zero.py b/openpype/hosts/maya/plugins/publish/validate_transform_zero.py index abd9e00af1..906ff17ec9 100644 --- a/openpype/hosts/maya/plugins/publish/validate_transform_zero.py +++ b/openpype/hosts/maya/plugins/publish/validate_transform_zero.py @@ -3,7 +3,10 @@ from maya import cmds import pyblish.api import openpype.hosts.maya.api.action -from openpype.pipeline.publish import ValidateContentsOrder +from openpype.pipeline.publish import ( + ValidateContentsOrder, + PublishValidationError +) class ValidateTransformZero(pyblish.api.Validator): @@ -62,5 +65,14 @@ class ValidateTransformZero(pyblish.api.Validator): invalid = self.get_invalid(instance) if invalid: - raise ValueError("Nodes found with transform " - "values: {0}".format(invalid)) + + names = "
".join( + " - {}".format(node) for node in invalid + ) + + raise PublishValidationError( + title="Transform Zero", + message="The model publish allows no transformations. You must" + " freeze transformations to continue.

" + "Nodes found with transform values: " + "{0}".format(names)) diff --git a/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py b/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py index 1425190b82..b2cb2ebda2 100644 --- a/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py +++ b/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py @@ -7,10 +7,15 @@ import pyblish.api import openpype.hosts.maya.api.action from openpype.pipeline import legacy_io from openpype.settings import get_project_settings -from openpype.pipeline.publish import ValidateContentsOrder +from openpype.pipeline.publish import ( + ValidateContentsOrder, + OptionalPyblishPluginMixin, + PublishValidationError +) -class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin): +class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Validate name of Unreal Static Mesh Unreals naming convention states that staticMesh should start with `SM` @@ -131,6 +136,9 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin): return invalid def process(self, instance): + if not self.is_active(instance.data): + return + if not self.validate_mesh and not self.validate_collision: self.log.info("Validation of both mesh and collision names" "is disabled.") @@ -143,4 +151,4 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin): invalid = self.get_invalid(instance) if invalid: - raise RuntimeError("Model naming is invalid. See log.") + raise PublishValidationError("Model naming is invalid. See log.") diff --git a/openpype/hosts/maya/plugins/publish/validate_unreal_up_axis.py b/openpype/hosts/maya/plugins/publish/validate_unreal_up_axis.py index dd699735d9..a420dcb900 100644 --- a/openpype/hosts/maya/plugins/publish/validate_unreal_up_axis.py +++ b/openpype/hosts/maya/plugins/publish/validate_unreal_up_axis.py @@ -6,10 +6,12 @@ import pyblish.api from openpype.pipeline.publish import ( ValidateContentsOrder, RepairAction, + OptionalPyblishPluginMixin ) -class ValidateUnrealUpAxis(pyblish.api.ContextPlugin): +class ValidateUnrealUpAxis(pyblish.api.ContextPlugin, + OptionalPyblishPluginMixin): """Validate if Z is set as up axis in Maya""" optional = True @@ -21,6 +23,9 @@ class ValidateUnrealUpAxis(pyblish.api.ContextPlugin): actions = [RepairAction] def process(self, context): + if not self.is_active(context.data): + return + assert cmds.upAxis(q=True, axis=True) == "z", ( "Invalid axis set as up axis" ) diff --git a/openpype/hosts/maya/plugins/publish/validate_visible_only.py b/openpype/hosts/maya/plugins/publish/validate_visible_only.py index faf634f258..e72782e552 100644 --- a/openpype/hosts/maya/plugins/publish/validate_visible_only.py +++ b/openpype/hosts/maya/plugins/publish/validate_visible_only.py @@ -2,7 +2,10 @@ import pyblish.api from openpype.hosts.maya.api.lib import iter_visible_nodes_in_range import openpype.hosts.maya.api.action -from openpype.pipeline.publish import ValidateContentsOrder +from openpype.pipeline.publish import ( + ValidateContentsOrder, + PublishValidationError +) class ValidateAlembicVisibleOnly(pyblish.api.InstancePlugin): @@ -27,7 +30,7 @@ class ValidateAlembicVisibleOnly(pyblish.api.InstancePlugin): invalid = self.get_invalid(instance) if invalid: start, end = self.get_frame_range(instance) - raise RuntimeError("No visible nodes found in " + raise PublishValidationError("No visible nodes found in " "frame range {}-{}.".format(start, end)) @classmethod diff --git a/openpype/hosts/maya/plugins/publish/validate_vray.py b/openpype/hosts/maya/plugins/publish/validate_vray.py index 045ac258a1..bef5967cc9 100644 --- a/openpype/hosts/maya/plugins/publish/validate_vray.py +++ b/openpype/hosts/maya/plugins/publish/validate_vray.py @@ -1,7 +1,7 @@ from maya import cmds import pyblish.api -from openpype.pipeline import PublishValidationError +from openpype.pipeline.publish import PublishValidationError class ValidateVray(pyblish.api.InstancePlugin): diff --git a/openpype/hosts/maya/plugins/publish/validate_vray_distributed_rendering.py b/openpype/hosts/maya/plugins/publish/validate_vray_distributed_rendering.py index 366f3bd10e..a71849da00 100644 --- a/openpype/hosts/maya/plugins/publish/validate_vray_distributed_rendering.py +++ b/openpype/hosts/maya/plugins/publish/validate_vray_distributed_rendering.py @@ -1,11 +1,9 @@ import pyblish.api +from maya import cmds + from openpype.hosts.maya.api import lib from openpype.pipeline.publish import ( - ValidateContentsOrder, - RepairAction, -) - -from maya import cmds + PublishValidationError, RepairAction, ValidateContentsOrder) class ValidateVRayDistributedRendering(pyblish.api.InstancePlugin): @@ -36,7 +34,7 @@ class ValidateVRayDistributedRendering(pyblish.api.InstancePlugin): vray_settings = cmds.ls("vraySettings", type="VRaySettingsNode") assert vray_settings, "Please ensure a VRay Settings Node is present" - renderlayer = instance.data['setMembers'] + renderlayer = instance.data['renderlayer'] if not lib.get_attr_in_layer(self.enabled_attr, layer=renderlayer): # If not distributed rendering enabled, ignore.. @@ -45,13 +43,14 @@ class ValidateVRayDistributedRendering(pyblish.api.InstancePlugin): # If distributed rendering is enabled but it is *not* set to ignore # during batch mode we invalidate the instance if not lib.get_attr_in_layer(self.ignored_attr, layer=renderlayer): - raise RuntimeError("Renderlayer has distributed rendering enabled " - "but is not set to ignore in batch mode.") + raise PublishValidationError( + ("Renderlayer has distributed rendering enabled " + "but is not set to ignore in batch mode.")) @classmethod def repair(cls, instance): - renderlayer = instance.data.get("setMembers") + renderlayer = instance.data.get("renderlayer") with lib.renderlayer(renderlayer): cls.log.info("Enabling Distributed Rendering " "ignore in batch mode..") diff --git a/openpype/hosts/maya/plugins/publish/validate_vray_translator_settings.py b/openpype/hosts/maya/plugins/publish/validate_vray_translator_settings.py index f49811c2c0..4474f08ba4 100644 --- a/openpype/hosts/maya/plugins/publish/validate_vray_translator_settings.py +++ b/openpype/hosts/maya/plugins/publish/validate_vray_translator_settings.py @@ -5,6 +5,7 @@ from openpype.pipeline.publish import ( context_plugin_should_run, RepairContextAction, ValidateContentsOrder, + PublishValidationError ) from maya import cmds @@ -26,7 +27,10 @@ class ValidateVRayTranslatorEnabled(pyblish.api.ContextPlugin): invalid = self.get_invalid(context) if invalid: - raise RuntimeError("Found invalid VRay Translator settings!") + raise PublishValidationError( + message="Found invalid VRay Translator settings", + title=self.label + ) @classmethod def get_invalid(cls, context): @@ -35,7 +39,11 @@ class ValidateVRayTranslatorEnabled(pyblish.api.ContextPlugin): # Get vraySettings node vray_settings = cmds.ls(type="VRaySettingsNode") - assert vray_settings, "Please ensure a VRay Settings Node is present" + if not vray_settings: + raise PublishValidationError( + "Please ensure a VRay Settings Node is present", + title=cls.label + ) node = vray_settings[0] diff --git a/openpype/hosts/maya/plugins/publish/validate_vrayproxy_members.py b/openpype/hosts/maya/plugins/publish/validate_vrayproxy_members.py index 855a96e6b9..7b726de3a8 100644 --- a/openpype/hosts/maya/plugins/publish/validate_vrayproxy_members.py +++ b/openpype/hosts/maya/plugins/publish/validate_vrayproxy_members.py @@ -3,6 +3,10 @@ import pyblish.api from maya import cmds import openpype.hosts.maya.api.action +from openpype.pipeline.publish import ( + PublishValidationError +) + class ValidateVrayProxyMembers(pyblish.api.InstancePlugin): @@ -19,7 +23,7 @@ class ValidateVrayProxyMembers(pyblish.api.InstancePlugin): invalid = self.get_invalid(instance) if invalid: - raise RuntimeError("'%s' is invalid VRay Proxy for " + raise PublishValidationError("'%s' is invalid VRay Proxy for " "export!" % instance.name) @classmethod diff --git a/openpype/hosts/maya/plugins/publish/validate_yeti_rig_cache_state.py b/openpype/hosts/maya/plugins/publish/validate_yeti_rig_cache_state.py index 4842134b12..2b7249ad94 100644 --- a/openpype/hosts/maya/plugins/publish/validate_yeti_rig_cache_state.py +++ b/openpype/hosts/maya/plugins/publish/validate_yeti_rig_cache_state.py @@ -1,7 +1,11 @@ import pyblish.api import maya.cmds as cmds import openpype.hosts.maya.api.action -from openpype.pipeline.publish import RepairAction +from openpype.pipeline.publish import ( + RepairAction, + PublishValidationError +) + class ValidateYetiRigCacheState(pyblish.api.InstancePlugin): @@ -23,7 +27,7 @@ class ValidateYetiRigCacheState(pyblish.api.InstancePlugin): def process(self, instance): invalid = self.get_invalid(instance) if invalid: - raise RuntimeError("Nodes have incorrect I/O settings") + raise PublishValidationError("Nodes have incorrect I/O settings") @classmethod def get_invalid(cls, instance): diff --git a/openpype/hosts/maya/plugins/publish/validate_yeti_rig_input_in_instance.py b/openpype/hosts/maya/plugins/publish/validate_yeti_rig_input_in_instance.py index ebef44774d..96fb475a0a 100644 --- a/openpype/hosts/maya/plugins/publish/validate_yeti_rig_input_in_instance.py +++ b/openpype/hosts/maya/plugins/publish/validate_yeti_rig_input_in_instance.py @@ -3,7 +3,10 @@ from maya import cmds import pyblish.api import openpype.hosts.maya.api.action -from openpype.pipeline.publish import ValidateContentsOrder +from openpype.pipeline.publish import ( + ValidateContentsOrder, + PublishValidationError +) class ValidateYetiRigInputShapesInInstance(pyblish.api.Validator): @@ -19,7 +22,7 @@ class ValidateYetiRigInputShapesInInstance(pyblish.api.Validator): invalid = self.get_invalid(instance) if invalid: - raise RuntimeError("Yeti Rig has invalid input meshes") + raise PublishValidationError("Yeti Rig has invalid input meshes") @classmethod def get_invalid(cls, instance): diff --git a/openpype/hosts/maya/plugins/publish/validate_yeti_rig_settings.py b/openpype/hosts/maya/plugins/publish/validate_yeti_rig_settings.py index 9914277721..455bf5291a 100644 --- a/openpype/hosts/maya/plugins/publish/validate_yeti_rig_settings.py +++ b/openpype/hosts/maya/plugins/publish/validate_yeti_rig_settings.py @@ -1,5 +1,7 @@ import pyblish.api +from openpype.pipeline.publish import PublishValidationError + class ValidateYetiRigSettings(pyblish.api.InstancePlugin): """Validate Yeti Rig Settings have collected input connections. @@ -18,8 +20,9 @@ class ValidateYetiRigSettings(pyblish.api.InstancePlugin): invalid = self.get_invalid(instance) if invalid: - raise RuntimeError("Detected invalid Yeti Rig data. (See log) " - "Tip: Save the scene") + raise PublishValidationError( + ("Detected invalid Yeti Rig data. (See log) " + "Tip: Save the scene")) @classmethod def get_invalid(cls, instance): diff --git a/openpype/modules/deadline/abstract_submit_deadline.py b/openpype/modules/deadline/abstract_submit_deadline.py index e3e94d50cd..551a2f7373 100644 --- a/openpype/modules/deadline/abstract_submit_deadline.py +++ b/openpype/modules/deadline/abstract_submit_deadline.py @@ -19,7 +19,8 @@ import requests import pyblish.api from openpype.pipeline.publish import ( AbstractMetaInstancePlugin, - KnownPublishError + KnownPublishError, + OpenPypePyblishPluginMixin ) JSONDecodeError = getattr(json.decoder, "JSONDecodeError", ValueError) @@ -35,7 +36,7 @@ def requests_post(*args, **kwargs): Warning: Disabling SSL certificate validation is defeating one line - of defense SSL is providing and it is not recommended. + of defense SSL is providing, and it is not recommended. """ if 'verify' not in kwargs: @@ -56,7 +57,7 @@ def requests_get(*args, **kwargs): Warning: Disabling SSL certificate validation is defeating one line - of defense SSL is providing and it is not recommended. + of defense SSL is providing, and it is not recommended. """ if 'verify' not in kwargs: @@ -395,14 +396,17 @@ class DeadlineJobInfo(object): @six.add_metaclass(AbstractMetaInstancePlugin) -class AbstractSubmitDeadline(pyblish.api.InstancePlugin): +class AbstractSubmitDeadline(pyblish.api.InstancePlugin, + OpenPypePyblishPluginMixin): """Class abstracting access to Deadline.""" label = "Submit to Deadline" order = pyblish.api.IntegratorOrder + 0.1 + import_reference = False use_published = True asset_dependencies = False + default_priority = 50 def __init__(self, *args, **kwargs): super(AbstractSubmitDeadline, self).__init__(*args, **kwargs) @@ -651,18 +655,18 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin): @staticmethod def _get_workfile_instance(context): """Find workfile instance in context""" - for i in context: + for instance in context: is_workfile = ( - "workfile" in i.data.get("families", []) or - i.data["family"] == "workfile" + "workfile" in instance.data.get("families", []) or + instance.data["family"] == "workfile" ) if not is_workfile: continue # test if there is instance of workfile waiting # to be published. - assert i.data.get("publish", True) is True, ( + assert instance.data.get("publish", True) is True, ( "Workfile (scene) must be published along") - return i + return instance diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py index a6cdcb7e71..159ac43289 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py @@ -30,8 +30,16 @@ import attr from maya import cmds -from openpype.pipeline import legacy_io - +from openpype.pipeline import ( + legacy_io, + OpenPypePyblishPluginMixin +) +from openpype.lib import ( + BoolDef, + NumberDef, + TextDef, + EnumDef +) from openpype.hosts.maya.api.lib_rendersettings import RenderSettings from openpype.hosts.maya.api.lib import get_attr_in_layer @@ -92,7 +100,8 @@ class ArnoldPluginInfo(object): ArnoldFile = attr.ib(default=None) -class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): +class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, + OpenPypePyblishPluginMixin): label = "Submit Render to Deadline" hosts = ["maya"] @@ -106,6 +115,24 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): jobInfo = {} pluginInfo = {} group = "none" + strict_error_checking = True + + @classmethod + def apply_settings(cls, project_settings, system_settings): + settings = project_settings["deadline"]["publish"]["MayaSubmitDeadline"] # noqa + + # Take some defaults from settings + cls.asset_dependencies = settings.get("asset_dependencies", + cls.asset_dependencies) + cls.import_reference = settings.get("import_reference", + cls.import_reference) + cls.use_published = settings.get("use_published", cls.use_published) + cls.priority = settings.get("priority", cls.priority) + cls.tile_priority = settings.get("tile_priority", cls.tile_priority) + cls.limit = settings.get("limit", cls.limit) + cls.group = settings.get("group", cls.group) + cls.strict_error_checking = settings.get("strict_error_checking", + cls.strict_error_checking) def get_job_info(self): job_info = DeadlineJobInfo(Plugin="MayaBatch") @@ -151,6 +178,19 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): if self.limit: job_info.LimitGroups = ",".join(self.limit) + attr_values = self.get_attr_values_from_data(instance.data) + render_globals = instance.data.setdefault("renderGlobals", dict()) + machine_list = attr_values.get("machineList", "") + if machine_list: + if attr_values.get("whitelist", True): + machine_list_key = "Whitelist" + else: + machine_list_key = "Blacklist" + render_globals[machine_list_key] = machine_list + + job_info.Priority = attr_values.get("priority") + job_info.ChunkSize = attr_values.get("chunkSize") + # Add options from RenderGlobals render_globals = instance.data.get("renderGlobals", {}) job_info.update(render_globals) @@ -223,8 +263,10 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): "renderSetupIncludeLights", default_rs_include_lights) if rs_include_lights not in {"1", "0", True, False}: rs_include_lights = default_rs_include_lights - strict_error_checking = instance.data.get("strict_error_checking", - True) + + attr_values = self.get_attr_values_from_data(instance.data) + strict_error_checking = attr_values.get("strict_error_checking", + self.strict_error_checking) plugin_info = MayaPluginInfo( SceneFile=self.scene_path, Version=cmds.about(version=True), @@ -422,11 +464,13 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): assembly_job_info.Name += " - Tile Assembly Job" assembly_job_info.Frames = 1 assembly_job_info.MachineLimit = 1 - assembly_job_info.Priority = instance.data.get( - "tile_priority", self.tile_priority - ) + + attr_values = self.get_attr_values_from_data(instance.data) + assembly_job_info.Priority = attr_values.get("tile_priority", + self.tile_priority) assembly_job_info.TileJob = False + # TODO: This should be a new publisher attribute definition pool = instance.context.data["project_settings"]["deadline"] pool = pool["publish"]["ProcessSubmittedJobOnFarm"]["deadline_pool"] assembly_job_info.Pool = pool or instance.data.get("primaryPool", "") @@ -519,7 +563,6 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): "submitting assembly job {} of {}".format(i + 1, num_assemblies) ) - self.log.info(payload) assembly_job_id = self.submit(payload) assembly_job_ids.append(assembly_job_id) @@ -782,6 +825,44 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): for file in exp: yield file + @classmethod + def get_attribute_defs(cls): + defs = super(MayaSubmitDeadline, cls).get_attribute_defs() + + defs.extend([ + NumberDef("priority", + label="Priority", + default=cls.default_priority, + decimals=0), + NumberDef("chunkSize", + label="Frames Per Task", + default=1, + decimals=0, + minimum=1, + maximum=1000), + TextDef("machineList", + label="Machine List", + default="", + placeholder="machine1,machine2"), + EnumDef("whitelist", + label="Machine List (Allow/Deny)", + items={ + True: "Allow List", + False: "Deny List", + }, + default=False), + NumberDef("tile_priority", + label="Tile Assembler Priority", + decimals=0, + default=cls.tile_priority), + BoolDef("strict_error_checking", + label="Strict Error Checking", + default=cls.strict_error_checking), + + ]) + + return defs + def _format_tiles( filename, diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py index 25f859554f..3b04f6d3bc 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py @@ -1,18 +1,33 @@ import os -import requests +import attr from datetime import datetime from maya import cmds from openpype.pipeline import legacy_io, PublishXmlValidationError -from openpype.settings import get_project_settings from openpype.tests.lib import is_in_tests from openpype.lib import is_running_from_build +from openpype_modules.deadline import abstract_submit_deadline +from openpype_modules.deadline.abstract_submit_deadline import DeadlineJobInfo import pyblish.api -class MayaSubmitRemotePublishDeadline(pyblish.api.InstancePlugin): +@attr.s +class MayaPluginInfo(object): + Build = attr.ib(default=None) # Don't force build + StrictErrorChecking = attr.ib(default=True) + + SceneFile = attr.ib(default=None) # Input scene + Version = attr.ib(default=None) # Mandatory for Deadline + ProjectPath = attr.ib(default=None) + + ScriptJob = attr.ib(default=True) + ScriptFilename = attr.ib(default=None) + + +class MayaSubmitRemotePublishDeadline( + abstract_submit_deadline.AbstractSubmitDeadline): """Submit Maya scene to perform a local publish in Deadline. Publishing in Deadline can be helpful for scenes that publish very slow. @@ -36,13 +51,6 @@ class MayaSubmitRemotePublishDeadline(pyblish.api.InstancePlugin): targets = ["local"] def process(self, instance): - project_name = instance.context.data["projectName"] - # TODO settings can be received from 'context.data["project_settings"]' - settings = get_project_settings(project_name) - # use setting for publish job on farm, no reason to have it separately - deadline_publish_job_sett = (settings["deadline"] - ["publish"] - ["ProcessSubmittedJobOnFarm"]) # Ensure no errors so far if not (all(result["success"] @@ -54,54 +62,39 @@ class MayaSubmitRemotePublishDeadline(pyblish.api.InstancePlugin): "Skipping submission..") return + super(MayaSubmitRemotePublishDeadline, self).process(instance) + + def get_job_info(self): + instance = self._instance + context = instance.context + + project_name = instance.context.data["projectName"] scene = instance.context.data["currentFile"] scenename = os.path.basename(scene) job_name = "{scene} [PUBLISH]".format(scene=scenename) batch_name = "{code} - {scene}".format(code=project_name, scene=scenename) + if is_in_tests(): batch_name += datetime.now().strftime("%d%m%Y%H%M%S") - # Generate the payload for Deadline submission - payload = { - "JobInfo": { - "Plugin": "MayaBatch", - "BatchName": batch_name, - "Name": job_name, - "UserName": instance.context.data["user"], - "Comment": instance.context.data.get("comment", ""), - # "InitialStatus": state - "Department": deadline_publish_job_sett["deadline_department"], - "ChunkSize": deadline_publish_job_sett["deadline_chunk_size"], - "Priority": deadline_publish_job_sett["deadline_priority"], - "Group": deadline_publish_job_sett["deadline_group"], - "Pool": deadline_publish_job_sett["deadline_pool"], - }, - "PluginInfo": { + job_info = DeadlineJobInfo(Plugin="MayaBatch") + job_info.BatchName = batch_name + job_info.Name = job_name + job_info.UserName = context.data.get("user") + job_info.Comment = context.data.get("comment", "") - "Build": None, # Don't force build - "StrictErrorChecking": True, - "ScriptJob": True, + # use setting for publish job on farm, no reason to have it separately + project_settings = context.data["project_settings"] + deadline_publish_job_sett = project_settings["deadline"]["publish"]["ProcessSubmittedJobOnFarm"] # noqa + job_info.Department = deadline_publish_job_sett["deadline_department"] + job_info.ChunkSize = deadline_publish_job_sett["deadline_chunk_size"] + job_info.Priority = deadline_publish_job_sett["deadline_priority"] + job_info.Group = deadline_publish_job_sett["deadline_group"] + job_info.Pool = deadline_publish_job_sett["deadline_pool"] - # Inputs - "SceneFile": scene, - "ScriptFilename": "{OPENPYPE_REPOS_ROOT}/openpype/scripts/remote_publish.py", # noqa - - # Mandatory for Deadline - "Version": cmds.about(version=True), - - # Resolve relative references - "ProjectPath": cmds.workspace(query=True, - rootDirectory=True), - - }, - - # Mandatory for Deadline, may be empty - "AuxFiles": [] - } - - # Include critical environment variables with submission + api.Session + # Include critical environment variables with submission + Session keys = [ "FTRACK_API_USER", "FTRACK_API_KEY", @@ -126,20 +119,18 @@ class MayaSubmitRemotePublishDeadline(pyblish.api.InstancePlugin): environment["OPENPYPE_PUBLISH_SUBSET"] = instance.data["subset"] environment["OPENPYPE_REMOTE_PUBLISH"] = "1" - payload["JobInfo"].update({ - "EnvironmentKeyValue%d" % index: "{key}={value}".format( - key=key, - value=environment[key] - ) for index, key in enumerate(environment) - }) + for key, value in environment.items(): + job_info.EnvironmentKeyValue[key] = value - self.log.info("Submitting Deadline job ...") - deadline_url = instance.context.data["defaultDeadline"] - # if custom one is set in instance, use that - if instance.data.get("deadlineUrl"): - deadline_url = instance.data.get("deadlineUrl") - assert deadline_url, "Requires Deadline Webservice URL" - url = "{}/api/jobs".format(deadline_url) - response = requests.post(url, json=payload, timeout=10) - if not response.ok: - raise Exception(response.text) + def get_plugin_info(self): + + scene = self._instance.context.data["currentFile"] + + plugin_info = MayaPluginInfo() + plugin_info.SceneFile = scene + plugin_info.ScriptFilename = "{OPENPYPE_REPOS_ROOT}/openpype/scripts/remote_publish.py" # noqa + plugin_info.Version = cmds.about(version=True) + plugin_info.ProjectPath = cmds.workspace(query=True, + rootDirectory=True) + + return attr.asdict(plugin_info) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 292fe58cca..c893f43c4c 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -18,6 +18,8 @@ from openpype.pipeline import ( get_representation_path, legacy_io, ) +from openpype.pipeline.publish import OpenPypePyblishPluginMixin +from openpype.lib import EnumDef from openpype.tests.lib import is_in_tests from openpype.pipeline.farm.patterning import match_aov_pattern from openpype.lib import is_running_from_build @@ -81,7 +83,7 @@ def get_resource_files(resources, frame_range=None): class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, - publish.ColormanagedPyblishPluginMixin): + OpenPypePyblishPluginMixin): """Process Job submitted on farm. These jobs are dependent on a deadline or muster job @@ -278,6 +280,12 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, priority = self.deadline_priority or instance.data.get("priority", 50) + instance_settings = self.get_attr_values_from_data(instance.data) + initial_status = instance_settings.get("publishJobState", "Active") + # TODO: Remove this backwards compatibility of `suspend_publish` + if instance.data.get("suspend_publish"): + initial_status = "Suspended" + args = [ "--headless", 'publish', @@ -304,6 +312,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, "Department": self.deadline_department, "ChunkSize": self.deadline_chunk_size, "Priority": priority, + "InitialStatus": initial_status, "Group": self.deadline_group, "Pool": self.deadline_pool or instance.data.get("primaryPool"), @@ -336,9 +345,6 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, else: payload["JobInfo"]["JobDependency0"] = job["_id"] - if instance.data.get("suspend_publish"): - payload["JobInfo"]["InitialStatus"] = "Suspended" - for index, (key_, value_) in enumerate(environment.items()): payload["JobInfo"].update( { @@ -1251,3 +1257,12 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, publish_folder = os.path.dirname(file_path) return publish_folder + + @classmethod + def get_attribute_defs(cls): + return [ + EnumDef("publishJobState", + label="Publish Job State", + items=["Active", "Suspended"], + default="Active") + ] diff --git a/openpype/hosts/maya/plugins/publish/validate_muster_connection.py b/openpype/modules/muster/plugins/publish/validate_muster_connection.py similarity index 100% rename from openpype/hosts/maya/plugins/publish/validate_muster_connection.py rename to openpype/modules/muster/plugins/publish/validate_muster_connection.py diff --git a/openpype/pipeline/create/__init__.py b/openpype/pipeline/create/__init__.py index 5eee18df0f..6755224c19 100644 --- a/openpype/pipeline/create/__init__.py +++ b/openpype/pipeline/create/__init__.py @@ -74,6 +74,8 @@ __all__ = ( "register_creator_plugin_path", "deregister_creator_plugin_path", + "cache_and_get_instances", + "CreatedInstance", "CreateContext", diff --git a/openpype/plugins/publish/collect_scene_version.py b/openpype/plugins/publish/collect_scene_version.py index cd3231a07d..70a0aca296 100644 --- a/openpype/plugins/publish/collect_scene_version.py +++ b/openpype/plugins/publish/collect_scene_version.py @@ -3,6 +3,7 @@ import pyblish.api from openpype.lib import get_version_from_path from openpype.tests.lib import is_in_tests +from openpype.pipeline import KnownPublishError class CollectSceneVersion(pyblish.api.ContextPlugin): @@ -38,11 +39,15 @@ class CollectSceneVersion(pyblish.api.ContextPlugin): if ( os.environ.get("HEADLESS_PUBLISH") and not is_in_tests() - and context.data["hostName"] in self.skip_hosts_headless_publish): + and context.data["hostName"] in self.skip_hosts_headless_publish + ): self.log.debug("Skipping for headless publishing") return - assert context.data.get('currentFile'), "Cannot get current file" + if not context.data.get('currentFile'): + raise KnownPublishError("Cannot get current workfile path. " + "Make sure your scene is saved.") + filename = os.path.basename(context.data.get('currentFile')) if '' in filename: @@ -53,8 +58,9 @@ class CollectSceneVersion(pyblish.api.ContextPlugin): ) version = get_version_from_path(filename) - assert version, "Cannot determine version" + if version is None: + raise KnownPublishError("Unable to retrieve version number from " + "filename: {}".format(filename)) - rootVersion = int(version) - context.data['version'] = rootVersion + context.data['version'] = int(version) self.log.info('Scene Version: %s' % context.data.get('version')) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index e3fc5f0723..a25775e592 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -615,8 +615,8 @@ "maskOverride": false, "maskDriver": false, "maskFilter": false, - "maskColor_manager": false, - "maskOperator": false + "maskOperator": false, + "maskColor_manager": false }, "CreateVrayProxy": { "enabled": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json index a8b76a0331..1c37638c90 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json @@ -318,52 +318,52 @@ { "type": "boolean", "key": "maskOptions", - "label": "Mask Options" + "label": "Export Options" }, { "type": "boolean", "key": "maskCamera", - "label": "Mask Camera" + "label": "Export Cameras" }, { "type": "boolean", "key": "maskLight", - "label": "Mask Light" + "label": "Export Lights" }, { "type": "boolean", "key": "maskShape", - "label": "Mask Shape" + "label": "Export Shapes" }, { "type": "boolean", "key": "maskShader", - "label": "Mask Shader" + "label": "Export Shaders" }, { "type": "boolean", "key": "maskOverride", - "label": "Mask Override" + "label": "Export Override Nodes" }, { "type": "boolean", "key": "maskDriver", - "label": "Mask Driver" + "label": "Export Drivers" }, { "type": "boolean", "key": "maskFilter", - "label": "Mask Filter" - }, - { - "type": "boolean", - "key": "maskColor_manager", - "label": "Mask Color Manager" + "label": "Export Filters" }, { "type": "boolean", "key": "maskOperator", - "label": "Mask Operator" + "label": "Export Operators" + }, + { + "type": "boolean", + "key": "maskColor_manager", + "label": "Export Color Managers" } ] },