diff --git a/pype/__init__.py b/pype/__init__.py index 14af2ee736..e5d1aee374 100644 --- a/pype/__init__.py +++ b/pype/__init__.py @@ -1,7 +1,4 @@ import os -import sys - -import imp from pyblish import api as pyblish from avalon import api as avalon diff --git a/pype/action.py b/pype/action.py index 771379c256..ea1f5e13f4 100644 --- a/pype/action.py +++ b/pype/action.py @@ -1,5 +1,6 @@ # absolute_import is needed to counter the `module has no cmds error` in Maya from __future__ import absolute_import + import pyblish.api @@ -41,7 +42,7 @@ def get_errored_plugins_from_data(context): class RepairAction(pyblish.api.Action): """Repairs the action - To process the repairing this requires a static `repair(instance)` method + To process the repairing this requires a static `repair(instance)` method is available on the plugin. """ @@ -67,7 +68,7 @@ class RepairAction(pyblish.api.Action): class RepairContextAction(pyblish.api.Action): """Repairs the action - To process the repairing this requires a static `repair(instance)` method + To process the repairing this requires a static `repair(instance)` method is available on the plugin. """ @@ -89,114 +90,3 @@ class RepairContextAction(pyblish.api.Action): plugin.repair() -class SelectInvalidAction(pyblish.api.Action): - """Select invalid nodes in Maya when plug-in failed. - - To retrieve the invalid nodes this assumes a static `get_invalid()` - method is available on the plugin. - - """ - label = "Select invalid" - on = "failed" # This action is only available on a failed plug-in - icon = "search" # Icon from Awesome Icon - - def process(self, context, plugin): - - try: - from maya import cmds - except ImportError: - raise ImportError("Current host is not Maya") - - errored_instances = get_errored_instances_from_context(context) - - # Apply pyblish.logic to get the instances for the plug-in - instances = pyblish.api.instances_by_plugin(errored_instances, plugin) - - # Get the invalid nodes for the plug-ins - self.log.info("Finding invalid nodes..") - invalid = list() - for instance in instances: - invalid_nodes = plugin.get_invalid(instance) - if invalid_nodes: - if isinstance(invalid_nodes, (list, tuple)): - invalid.extend(invalid_nodes) - else: - self.log.warning("Plug-in returned to be invalid, " - "but has no selectable nodes.") - - # Ensure unique (process each node only once) - invalid = list(set(invalid)) - - if invalid: - self.log.info("Selecting invalid nodes: %s" % ", ".join(invalid)) - cmds.select(invalid, replace=True, noExpand=True) - else: - self.log.info("No invalid nodes found.") - cmds.select(deselect=True) - - -class GenerateUUIDsOnInvalidAction(pyblish.api.Action): - """Generate UUIDs on the invalid nodes in the instance. - - Invalid nodes are those returned by the plugin's `get_invalid` method. - As such it is the plug-in's responsibility to ensure the nodes that - receive new UUIDs are actually invalid. - - Requires: - - instance.data["asset"] - - """ - - label = "Regenerate UUIDs" - on = "failed" # This action is only available on a failed plug-in - icon = "wrench" # Icon from Awesome Icon - - def process(self, context, plugin): - - self.log.info("Finding bad nodes..") - - # Get the errored instances - errored_instances = [] - for result in context.data["results"]: - if result["error"] is not None and result["instance"] is not None: - if result["error"]: - instance = result["instance"] - errored_instances.append(instance) - - # Apply pyblish logic to get the instances for the plug-in - instances = pyblish.api.instances_by_plugin(errored_instances, plugin) - - # Get the nodes from the all instances that ran through this plug-in - all_invalid = [] - for instance in instances: - invalid = plugin.get_invalid(instance) - if invalid: - - self.log.info("Fixing instance {}".format(instance.name)) - self._update_id_attribute(instance, invalid) - - all_invalid.extend(invalid) - - if not all_invalid: - self.log.info("No invalid nodes found.") - return - - all_invalid = list(set(all_invalid)) - self.log.info("Generated ids on nodes: {0}".format(all_invalid)) - - def _update_id_attribute(self, instance, nodes): - """Delete the id attribute - - Args: - instance: The instance we're fixing for - nodes (list): all nodes to regenerate ids on - """ - - import pype.maya.lib as lib - import avalon.io as io - - asset = instance.data['asset'] - asset_id = io.find_one({"name": asset, "type": "asset"}, - projection={"_id": True})['_id'] - for node, _id in lib.generate_ids(nodes, asset_id=asset_id): - lib.set_id(node, _id, overwrite=True) diff --git a/pype/api.py b/pype/api.py index 531a63a50d..e665d40535 100644 --- a/pype/api.py +++ b/pype/api.py @@ -1,5 +1,3 @@ -from collections import OrderedDict - from .plugin import ( Extractor, @@ -12,15 +10,12 @@ from .plugin import ( # temporary fix, might from .action import ( - get_errored_instances_from_context, - SelectInvalidAction, - GenerateUUIDsOnInvalidAction, RepairAction, RepairContextAction ) -all = [ +__all__ = [ # plugin classes "Extractor", # ordering @@ -30,7 +25,5 @@ all = [ "ValidateMeshOrder", # action "get_errored_instances_from_context", - "SelectInvalidAction", - "GenerateUUIDsOnInvalidAction", "RepairAction" ] diff --git a/pype/fusion/__init__.py b/pype/fusion/__init__.py index d6efaa54d3..b7b4b2d507 100644 --- a/pype/fusion/__init__.py +++ b/pype/fusion/__init__.py @@ -24,9 +24,9 @@ def install(): pyblish.register_callback("instanceToggled", on_pyblish_instance_toggled) # Disable all families except for the ones we explicitly want to see - family_states = ["imagesequence", - "camera", - "pointcache"] + family_states = ["studio.imagesequence", + "studio.camera", + "studio.pointcache"] avalon.data["familiesStateDefault"] = False avalon.data["familiesStateToggled"] = family_states diff --git a/pype/houdini/__init__.py b/pype/houdini/__init__.py new file mode 100644 index 0000000000..12bf0f2333 --- /dev/null +++ b/pype/houdini/__init__.py @@ -0,0 +1,91 @@ +import os +import logging + +import hou + +from pyblish import api as pyblish + +from avalon import api as avalon +from avalon.houdini import pipeline as houdini + +from pype.houdini import lib + +from pype.lib import ( + any_outdated, + update_task_from_path +) + + +PARENT_DIR = os.path.dirname(__file__) +PACKAGE_DIR = os.path.dirname(PARENT_DIR) +PLUGINS_DIR = os.path.join(PACKAGE_DIR, "plugins") + +PUBLISH_PATH = os.path.join(PLUGINS_DIR, "houdini", "publish") +LOAD_PATH = os.path.join(PLUGINS_DIR, "houdini", "load") +CREATE_PATH = os.path.join(PLUGINS_DIR, "houdini", "create") + +log = logging.getLogger("pype.houdini") + + +def install(): + + pyblish.register_plugin_path(PUBLISH_PATH) + avalon.register_plugin_path(avalon.Loader, LOAD_PATH) + avalon.register_plugin_path(avalon.Creator, CREATE_PATH) + + log.info("Installing callbacks ... ") + avalon.on("init", on_init) + avalon.on("save", on_save) + avalon.on("open", on_open) + + log.info("Overriding existing event 'taskChanged'") + + log.info("Setting default family states for loader..") + avalon.data["familiesStateToggled"] = ["studio.imagesequence"] + + +def on_init(*args): + houdini.on_houdini_initialize() + + +def on_save(*args): + + avalon.logger.info("Running callback on save..") + + update_task_from_path(hou.hipFile.path()) + + nodes = lib.get_id_required_nodes() + for node, new_id in lib.generate_ids(nodes): + lib.set_id(node, new_id, overwrite=False) + + +def on_open(*args): + + avalon.logger.info("Running callback on open..") + + update_task_from_path(hou.hipFile.path()) + + if any_outdated(): + from ..widgets import popup + + log.warning("Scene has outdated content.") + + # Get main window + parent = hou.ui.mainQtWindow() + + if parent is None: + log.info("Skipping outdated content pop-up " + "because Maya window can't be found.") + else: + + # Show outdated pop-up + def _on_show_inventory(): + import avalon.tools.cbsceneinventory as tool + tool.show(parent=parent) + + dialog = popup.Popup(parent=parent) + dialog.setWindowTitle("Maya scene has outdated content") + dialog.setMessage("There are outdated containers in " + "your Maya scene.") + dialog.on_show.connect(_on_show_inventory) + dialog.show() diff --git a/pype/houdini/lib.py b/pype/houdini/lib.py new file mode 100644 index 0000000000..78adbc5790 --- /dev/null +++ b/pype/houdini/lib.py @@ -0,0 +1,190 @@ +import uuid + +from contextlib import contextmanager + +import hou + +from avalon import api, io +from avalon.houdini import lib + + +def set_id(node, unique_id, overwrite=False): + + exists = node.parm("id") + if not exists: + lib.imprint(node, {"id": unique_id}) + + if not exists and overwrite: + node.setParm("id", unique_id) + + +def get_id(node): + """ + Get the `cbId` attribute of the given node + Args: + node (hou.Node): the name of the node to retrieve the attribute from + + Returns: + str + + """ + + if node is None: + return + + id = node.parm("id") + if node is None: + return + return id + + +def generate_ids(nodes, asset_id=None): + """Returns new unique ids for the given nodes. + + Note: This does not assign the new ids, it only generates the values. + + To assign new ids using this method: + >>> nodes = ["a", "b", "c"] + >>> for node, id in generate_ids(nodes): + >>> set_id(node, id) + + To also override any existing values (and assign regenerated ids): + >>> nodes = ["a", "b", "c"] + >>> for node, id in generate_ids(nodes): + >>> set_id(node, id, overwrite=True) + + Args: + nodes (list): List of nodes. + asset_id (str or bson.ObjectId): The database id for the *asset* to + generate for. When None provided the current asset in the + active session is used. + + Returns: + list: A list of (node, id) tuples. + + """ + + if asset_id is None: + # Get the asset ID from the database for the asset of current context + asset_data = io.find_one({"type": "asset", + "name": api.Session["AVALON_ASSET"]}, + projection={"_id": True}) + assert asset_data, "No current asset found in Session" + asset_id = asset_data['_id'] + + node_ids = [] + for node in nodes: + _, uid = str(uuid.uuid4()).rsplit("-", 1) + unique_id = "{}:{}".format(asset_id, uid) + node_ids.append((node, unique_id)) + + return node_ids + + +def get_id_required_nodes(): + + valid_types = ["geometry"] + nodes = {n for n in hou.node("/out").children() if + n.type().name() in valid_types} + + return list(nodes) + + +def get_additional_data(container): + """Not implemented yet!""" + return container + + +def set_parameter_callback(node, parameter, language, callback): + """Link a callback to a parameter of a node + + Args: + node(hou.Node): instance of the nodee + parameter(str): name of the parameter + language(str): name of the language, e.g.: python + callback(str): command which needs to be triggered + + Returns: + None + + """ + + template_grp = node.parmTemplateGroup() + template = template_grp.find(parameter) + if not template: + return + + script_language = (hou.scriptLanguage.Python if language == "python" else + hou.scriptLanguage.Hscript) + + template.setScriptCallbackLanguage(script_language) + template.setScriptCallback(callback) + + template.setTags({"script_callback": callback, + "script_callback_language": language.lower()}) + + # Replace the existing template with the adjusted one + template_grp.replace(parameter, template) + + node.setParmTemplateGroup(template_grp) + + +def set_parameter_callbacks(node, parameter_callbacks): + """Set callbacks for multiple parameters of a node + + Args: + node(hou.Node): instance of a hou.Node + parameter_callbacks(dict): collection of parameter and callback data + example: {"active" : + {"language": "python", + "callback": "print('hello world)'"} + } + Returns: + None + """ + for parameter, data in parameter_callbacks.items(): + language = data["language"] + callback = data["callback"] + + set_parameter_callback(node, parameter, language, callback) + + +def get_output_parameter(node): + """Return the render output parameter name of the given node + + Example: + root = hou.node("/obj") + my_alembic_node = root.createNode("alembic") + get_output_parameter(my_alembic_node) + # Result: "output" + + Args: + node(hou.Node): node instance + + Returns: + hou.Parm + + """ + + node_type = node.type().name() + if node_type == "geometry": + return node.parm("sopoutput") + + elif node_type == "alembic": + return node.parm("filename") + + else: + raise TypeError("Node type '%s' not supported" % node_type) + + +@contextmanager +def attribute_values(node, data): + + previous_attrs = {key: node.parm(key).eval() for key in data.keys()} + try: + node.setParms(data) + yield + except Exception as exc: + pass + finally: + node.setParms(previous_attrs) diff --git a/pype/lib.py b/pype/lib.py index 4ddc03bd17..3ce1441e3d 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -2,6 +2,7 @@ import os import re import logging import importlib +import itertools from .vendor import pather from .vendor.pather.error import ParseError @@ -12,6 +13,24 @@ import avalon.api log = logging.getLogger(__name__) +def pairwise(iterable): + """s -> (s0,s1), (s2,s3), (s4, s5), ...""" + a = iter(iterable) + return itertools.izip(a, a) + + +def grouper(iterable, n, fillvalue=None): + """Collect data into fixed-length chunks or blocks + + Examples: + grouper('ABCDEFG', 3, 'x') --> ABC DEF Gxx + + """ + + args = [iter(iterable)] * n + return itertools.izip_longest(fillvalue=fillvalue, *args) + + def is_latest(representation): """Return whether the representation is from latest version @@ -75,7 +94,7 @@ def update_task_from_path(path): # Find the current context from the filename project = io.find_one({"type": "project"}, - projection={"pype.template.work": True}) + projection={"config.template.work": True}) template = project['config']['template']['work'] # Force to use the registered to root to avoid using wrong paths template = pather.format(template, {"root": avalon.api.registered_root()}) @@ -252,21 +271,67 @@ def collect_container_metadata(container): return hostlib.get_additional_data(container) -def get_project_fps(): +def get_asset_fps(): """Returns project's FPS, if not found will return 25 by default Returns: int, float + + """ + + key = "fps" + + # FPS from asset data (if set) + asset_data = get_asset_data() + if key in asset_data: + return asset_data[key] + + # FPS from project data (if set) + project_data = get_project_data() + if key in project_data: + return project_data[key] + + # Fallback to 25 FPS + return 25.0 + + +def get_project_data(): + """Get the data of the current project + + The data of the project can contain things like: + resolution + fps + renderer + + Returns: + dict: + """ project_name = io.active_project() project = io.find_one({"name": project_name, "type": "project"}, - projection={"config": True}) + projection={"data": True}) - config = project.get("config", None) - assert config, "This is a bug" + data = project.get("data", {}) - fps = pype.get("fps", 25.0) + return data - return fps + +def get_asset_data(asset=None): + """Get the data from the current asset + + Args: + asset(str, Optional): name of the asset, eg: + + Returns: + dict + """ + + asset_name = asset or avalon.api.Session["AVALON_ASSET"] + document = io.find_one({"name": asset_name, + "type": "asset"}) + + data = document.get("data", {}) + + return data diff --git a/pype/maya/__init__.py b/pype/maya/__init__.py index 40dc2b072a..2e21d6acec 100644 --- a/pype/maya/__init__.py +++ b/pype/maya/__init__.py @@ -5,6 +5,7 @@ import weakref from maya import utils, cmds, mel from avalon import api as avalon, pipeline, maya +from avalon.maya.pipeline import IS_HEADLESS from pyblish import api as pyblish from ..lib import ( @@ -34,16 +35,24 @@ def install(): log.info("Installing callbacks ... ") avalon.on("init", on_init) + + # Callbacks below are not required for headless mode, the `init` however + # is important to load referenced Alembics correctly at rendertime. + if IS_HEADLESS: + log.info("Running in headless mode, skipping Colorbleed Maya " + "save/open/new callback installation..") + return + avalon.on("save", on_save) avalon.on("open", on_open) - + avalon.on("new", on_new) avalon.before("save", on_before_save) log.info("Overriding existing event 'taskChanged'") override_event("taskChanged", on_task_changed) log.info("Setting default family states for loader..") - avalon.data["familiesStateToggled"] = ["imagesequence"] + avalon.data["familiesStateToggled"] = ["studio.imagesequence"] def uninstall(): @@ -126,12 +135,13 @@ def on_open(_): from avalon.vendor.Qt import QtWidgets from ..widgets import popup - # Ensure scene's FPS is set to project config - lib.validate_fps() - # Update current task for the current scene update_task_from_path(cmds.file(query=True, sceneName=True)) + # Validate FPS after update_task_from_path to + # ensure it is using correct FPS for the asset + lib.validate_fps() + if any_outdated(): log.warning("Scene has outdated content.") @@ -158,6 +168,13 @@ def on_open(_): dialog.show() +def on_new(_): + """Set project resolution and fps when create a new file""" + avalon.logger.info("Running callback on new..") + with maya.suspended_refresh(): + lib.set_context_settings() + + def on_task_changed(*args): """Wrapped function of app initialize and maya's on task changed""" diff --git a/pype/maya/action.py b/pype/maya/action.py new file mode 100644 index 0000000000..6281a82409 --- /dev/null +++ b/pype/maya/action.py @@ -0,0 +1,128 @@ +# absolute_import is needed to counter the `module has no cmds error` in Maya +from __future__ import absolute_import + +import pyblish.api + + +from ..action import get_errored_instances_from_context + + +class GenerateUUIDsOnInvalidAction(pyblish.api.Action): + """Generate UUIDs on the invalid nodes in the instance. + + Invalid nodes are those returned by the plugin's `get_invalid` method. + As such it is the plug-in's responsibility to ensure the nodes that + receive new UUIDs are actually invalid. + + Requires: + - instance.data["asset"] + + """ + + label = "Regenerate UUIDs" + on = "failed" # This action is only available on a failed plug-in + icon = "wrench" # Icon from Awesome Icon + + def process(self, context, plugin): + + from maya import cmds + + self.log.info("Finding bad nodes..") + + errored_instances = get_errored_instances_from_context(context) + + # Apply pyblish logic to get the instances for the plug-in + instances = pyblish.api.instances_by_plugin(errored_instances, plugin) + + # Get the nodes from the all instances that ran through this plug-in + all_invalid = [] + for instance in instances: + invalid = plugin.get_invalid(instance) + + # Don't allow referenced nodes to get their ids regenerated to + # avoid loaded content getting messed up with reference edits + if invalid: + referenced = {node for node in invalid if + cmds.referenceQuery(node, isNodeReferenced=True)} + if referenced: + self.log.warning("Skipping UUID generation on referenced " + "nodes: {}".format(list(referenced))) + invalid = [node for node in invalid + if node not in referenced] + + if invalid: + + self.log.info("Fixing instance {}".format(instance.name)) + self._update_id_attribute(instance, invalid) + + all_invalid.extend(invalid) + + if not all_invalid: + self.log.info("No invalid nodes found.") + return + + all_invalid = list(set(all_invalid)) + self.log.info("Generated ids on nodes: {0}".format(all_invalid)) + + def _update_id_attribute(self, instance, nodes): + """Delete the id attribute + + Args: + instance: The instance we're fixing for + nodes (list): all nodes to regenerate ids on + """ + + import pype.maya.lib as lib + import avalon.io as io + + asset = instance.data['asset'] + asset_id = io.find_one({"name": asset, "type": "asset"}, + projection={"_id": True})['_id'] + for node, _id in lib.generate_ids(nodes, asset_id=asset_id): + lib.set_id(node, _id, overwrite=True) + + +class SelectInvalidAction(pyblish.api.Action): + """Select invalid nodes in Maya when plug-in failed. + + To retrieve the invalid nodes this assumes a static `get_invalid()` + method is available on the plugin. + + """ + label = "Select invalid" + on = "failed" # This action is only available on a failed plug-in + icon = "search" # Icon from Awesome Icon + + def process(self, context, plugin): + + try: + from maya import cmds + except ImportError: + raise ImportError("Current host is not Maya") + + errored_instances = get_errored_instances_from_context(context) + + # Apply pyblish.logic to get the instances for the plug-in + instances = pyblish.api.instances_by_plugin(errored_instances, plugin) + + # Get the invalid nodes for the plug-ins + self.log.info("Finding invalid nodes..") + invalid = list() + for instance in instances: + invalid_nodes = plugin.get_invalid(instance) + if invalid_nodes: + if isinstance(invalid_nodes, (list, tuple)): + invalid.extend(invalid_nodes) + else: + self.log.warning("Plug-in returned to be invalid, " + "but has no selectable nodes.") + + # Ensure unique (process each node only once) + invalid = list(set(invalid)) + + if invalid: + self.log.info("Selecting invalid nodes: %s" % ", ".join(invalid)) + cmds.select(invalid, replace=True, noExpand=True) + else: + self.log.info("No invalid nodes found.") + cmds.select(deselect=True) \ No newline at end of file diff --git a/pype/maya/lib.py b/pype/maya/lib.py index 8fe0e8e248..5f5c929f96 100644 --- a/pype/maya/lib.py +++ b/pype/maya/lib.py @@ -11,12 +11,13 @@ import contextlib from collections import OrderedDict, defaultdict from maya import cmds, mel +import maya.api.OpenMaya as om from avalon import api, maya, io, pipeline from avalon.vendor.six import string_types import avalon.maya.lib -from config import lib +from pype import lib log = logging.getLogger(__name__) @@ -76,6 +77,7 @@ _alembic_options = { "writeColorSets": bool, "writeFaceSets": bool, "writeCreases": bool, # Maya 2015 Ext1+ + "writeUVSets": bool, # Maya 2017+ "dataFormat": str, "root": (list, tuple), "attr": (list, tuple), @@ -89,7 +91,12 @@ _alembic_options = { } INT_FPS = {15, 24, 25, 30, 48, 50, 60, 44100, 48000} -FLOAT_FPS = {23.976, 29.97, 29.97, 47.952, 59.94} +FLOAT_FPS = {23.976, 29.97, 47.952, 59.94} + + +def _get_mel_global(name): + """Return the value of a mel global variable""" + return mel.eval("$%s = $%s;" % (name, name)) def matrix_equals(a, b, tolerance=1e-10): @@ -304,6 +311,33 @@ def attribute_values(attr_values): cmds.setAttr(attr, value) +@contextlib.contextmanager +def keytangent_default(in_tangent_type='auto', + out_tangent_type='auto'): + """Set the default keyTangent for new keys during this context""" + + original_itt = cmds.keyTangent(query=True, g=True, itt=True)[0] + original_ott = cmds.keyTangent(query=True, g=True, ott=True)[0] + cmds.keyTangent(g=True, itt=in_tangent_type) + cmds.keyTangent(g=True, ott=out_tangent_type) + try: + yield + finally: + cmds.keyTangent(g=True, itt=original_itt) + cmds.keyTangent(g=True, ott=original_ott) + + +@contextlib.contextmanager +def undo_chunk(): + """Open a undo chunk during context.""" + + try: + cmds.undoInfo(openChunk=True) + yield + finally: + cmds.undoInfo(closeChunk=True) + + @contextlib.contextmanager def renderlayer(layer): """Set the renderlayer during the context""" @@ -337,6 +371,126 @@ def evaluation(mode="off"): cmds.evaluationManager(mode=original) +@contextlib.contextmanager +def no_refresh(): + """Temporarily disables Maya's UI updates + + Note: + This only disabled the main pane and will sometimes still + trigger updates in torn off panels. + + """ + + pane = _get_mel_global('gMainPane') + state = cmds.paneLayout(pane, query=True, manage=True) + cmds.paneLayout(pane, edit=True, manage=False) + + try: + yield + finally: + cmds.paneLayout(pane, edit=True, manage=state) + + +@contextlib.contextmanager +def empty_sets(sets, force=False): + """Remove all members of the sets during the context""" + + assert isinstance(sets, (list, tuple)) + + original = dict() + original_connections = [] + + # Store original state + for obj_set in sets: + members = cmds.sets(obj_set, query=True) + original[obj_set] = members + + try: + for obj_set in sets: + cmds.sets(clear=obj_set) + if force: + # Break all connections if force is enabled, this way we + # prevent Maya from exporting any reference nodes which are + # connected with placeHolder[x] attributes + plug = "%s.dagSetMembers" % obj_set + connections = cmds.listConnections(plug, + source=True, + destination=False, + plugs=True, + connections=True) or [] + original_connections.extend(connections) + for dest, src in lib.pairwise(connections): + cmds.disconnectAttr(src, dest) + yield + finally: + + for dest, src in lib.pairwise(original_connections): + cmds.connectAttr(src, dest) + + # Restore original members + for origin_set, members in original.iteritems(): + cmds.sets(members, forceElement=origin_set) + + +@contextlib.contextmanager +def renderlayer(layer): + """Set the renderlayer during the context + + Arguments: + layer (str): Name of layer to switch to. + + """ + + original = cmds.editRenderLayerGlobals(query=True, + currentRenderLayer=True) + + try: + cmds.editRenderLayerGlobals(currentRenderLayer=layer) + yield + finally: + cmds.editRenderLayerGlobals(currentRenderLayer=original) + + +class delete_after(object): + """Context Manager that will delete collected nodes after exit. + + This allows to ensure the nodes added to the context are deleted + afterwards. This is useful if you want to ensure nodes are deleted + even if an error is raised. + + Examples: + with delete_after() as delete_bin: + cube = maya.cmds.polyCube() + delete_bin.extend(cube) + # cube exists + # cube deleted + + """ + + def __init__(self, nodes=None): + + self._nodes = list() + + if nodes: + self.extend(nodes) + + def append(self, node): + self._nodes.append(node) + + def extend(self, nodes): + self._nodes.extend(nodes) + + def __iter__(self): + return iter(self._nodes) + + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + if self._nodes: + cmds.delete(self._nodes) + + def get_renderer(layer): with renderlayer(layer): return cmds.getAttr("defaultRenderGlobals.currentRenderer") @@ -365,6 +519,161 @@ def no_undo(flush=False): cmds.undoInfo(**{keyword: original}) +def get_shader_assignments_from_shapes(shapes): + """Return the shape assignment per related shading engines. + + Returns a dictionary where the keys are shadingGroups and the values are + lists of assigned shapes or shape-components. + + For the 'shapes' this will return a dictionary like: + { + "shadingEngineX": ["nodeX", "nodeY"], + "shadingEngineY": ["nodeA", "nodeB"] + } + + Args: + shapes (list): The shapes to collect the assignments for. + + Returns: + dict: The {shadingEngine: shapes} relationships + + """ + + shapes = cmds.ls(shapes, + long=True, + selection=True, + shapes=True, + objectsOnly=True) + if not shapes: + return {} + + # Collect shading engines and their shapes + assignments = defaultdict(list) + for shape in shapes: + + # Get unique shading groups for the shape + shading_groups = cmds.listConnections(shape, + source=False, + destination=True, + plugs=False, + connections=False, + type="shadingEngine") or [] + shading_groups = list(set(shading_groups)) + for shading_group in shading_groups: + assignments[shading_group].add(shape) + + return dict(assignments) + + +@contextlib.contextmanager +def shader(nodes, shadingEngine="initialShadingGroup"): + """Assign a shader to nodes during the context""" + + shapes = cmds.ls(nodes, dag=1, o=1, shapes=1, long=1) + original = get_shader_assignments_from_shapes(shapes) + + try: + # Assign override shader + if shapes: + cmds.sets(shapes, edit=True, forceElement=shadingEngine) + yield + finally: + + # Assign original shaders + for sg, members in original.items(): + if members: + cmds.sets(shapes, edit=True, forceElement=shadingEngine) + + +@contextlib.contextmanager +def displaySmoothness(nodes, + divisionsU=0, + divisionsV=0, + pointsWire=4, + pointsShaded=1, + polygonObject=1): + """Set the displaySmoothness during the context""" + + # Ensure only non-intermediate shapes + nodes = cmds.ls(nodes, + dag=1, + shapes=1, + long=1, + noIntermediate=True) + + def parse(node): + """Parse the current state of a node""" + state = {} + for key in ["divisionsU", + "divisionsV", + "pointsWire", + "pointsShaded", + "polygonObject"]: + value = cmds.displaySmoothness(node, query=1, **{key: True}) + if value is not None: + state[key] = value[0] + return state + + originals = dict((node, parse(node)) for node in nodes) + + try: + # Apply current state + cmds.displaySmoothness(nodes, + divisionsU=divisionsU, + divisionsV=divisionsV, + pointsWire=pointsWire, + pointsShaded=pointsShaded, + polygonObject=polygonObject) + yield + finally: + # Revert state + for node, state in originals.iteritems(): + if state: + cmds.displaySmoothness(node, **state) + + +@contextlib.contextmanager +def no_display_layers(nodes): + """Ensure nodes are not in a displayLayer during context. + + Arguments: + nodes (list): The nodes to remove from any display layer. + + """ + + # Ensure long names + nodes = cmds.ls(nodes, long=True) + + # Get the original state + lookup = set(nodes) + original = {} + for layer in cmds.ls(type='displayLayer'): + + # Skip default layer + if layer == "defaultLayer": + continue + + members = cmds.editDisplayLayerMembers(layer, + query=True, + fullNames=True) + if not members: + continue + members = set(members) + + included = lookup.intersection(members) + if included: + original[layer] = list(included) + + try: + # Add all nodes to default layer + cmds.editDisplayLayerMembers("defaultLayer", nodes, noRecurse=True) + yield + finally: + # Restore original members + for layer, members in original.iteritems(): + cmds.editDisplayLayerMembers(layer, members, noRecurse=True) + + @contextlib.contextmanager def namespaced(namespace, new=True): """Work inside namespace during context @@ -607,6 +916,8 @@ def extract_alembic(file, # Discard unknown options if key not in _alembic_options: + log.warning("extract_alembic() does not support option '%s'. " + "Flag will be ignored..", key) options.pop(key) continue @@ -761,10 +1072,20 @@ def get_id(node): if node is None: return - if not cmds.attributeQuery("cbId", node=node, exists=True): + sel = om.MSelectionList() + sel.add(node) + + api_node = sel.getDependNode(0) + fn = om.MFnDependencyNode(api_node) + + if not fn.hasAttribute("cbId"): return - return cmds.getAttr("{}.cbId".format(node)) + try: + return fn.findPlug("cbId", False).asString() + except RuntimeError: + log.warning("Failed to retrieve cbId on %s", node) + return def generate_ids(nodes, asset_id=None): @@ -825,7 +1146,6 @@ def set_id(node, unique_id, overwrite=False): """ - attr = "{0}.cbId".format(node) exists = cmds.attributeQuery("cbId", node=node, exists=True) # Add the attribute if it does not exist yet @@ -834,6 +1154,7 @@ def set_id(node, unique_id, overwrite=False): # Set the value if not exists or overwrite: + attr = "{0}.cbId".format(node) cmds.setAttr(attr, unique_id, type="string") @@ -1012,11 +1333,11 @@ def assign_look(nodes, subset="lookDefault"): # Group all nodes per asset id grouped = defaultdict(list) for node in nodes: - studio_id = get_id(node) - if not studio_id: + pype_id = get_id(node) + if not pype_id: continue - parts = studio_id.split(":", 1) + parts = pype_id.split(":", 1) grouped[parts[0]].append(node) for asset_id, asset_nodes in grouped.items(): @@ -1039,7 +1360,7 @@ def assign_look(nodes, subset="lookDefault"): version = io.find_one({"parent": subset_data['_id'], "type": "version", "data.families": - {"$in": ["look"]} + {"$in": ["studio.look"]} }, sort=[("name", -1)], projection={"_id": True, "name": True}) @@ -1368,6 +1689,7 @@ def get_id_from_history(node): return _id +# Project settings def set_scene_fps(fps, update=True): """Set FPS from project configuration @@ -1381,21 +1703,104 @@ def set_scene_fps(fps, update=True): """ if fps in FLOAT_FPS: - unit = "{:f}fps".format(fps) + unit = "{}fps".format(fps) elif fps in INT_FPS: - unit = "{:d}fps".format(int(fps)) + unit = "{}fps".format(int(fps)) else: raise ValueError("Unsupported FPS value: `%s`" % fps) - log.info("Updating FPS to '{}'".format(unit)) + # Get time slider current state + start_frame = cmds.playbackOptions(query=True, minTime=True) + end_frame = cmds.playbackOptions(query=True, maxTime=True) + + # Get animation data + animation_start = cmds.playbackOptions(query=True, animationStartTime=True) + animation_end = cmds.playbackOptions(query=True, animationEndTime=True) + + current_frame = cmds.currentTime(query=True) + + log.info("Setting scene FPS to: '{}'".format(unit)) cmds.currentUnit(time=unit, updateAnimation=update) + # Set time slider data back to previous state + cmds.playbackOptions(edit=True, minTime=start_frame) + cmds.playbackOptions(edit=True, maxTime=end_frame) + + # Set animation data + cmds.playbackOptions(edit=True, animationStartTime=animation_start) + cmds.playbackOptions(edit=True, animationEndTime=animation_end) + + cmds.currentTime(current_frame, edit=True, update=True) + # Force file stated to 'modified' cmds.file(modified=True) +def set_scene_resolution(width, height): + """Set the render resolution + + Args: + width(int): value of the width + height(int): value of the height + + Returns: + None + + """ + + control_node = "defaultResolution" + current_renderer = cmds.getAttr("defaultRenderGlobals.currentRenderer") + + # Give VRay a helping hand as it is slightly different from the rest + if current_renderer == "vray": + vray_node = "vraySettings" + if cmds.objExists(vray_node): + control_node = vray_node + else: + log.error("Can't set VRay resolution because there is no node " + "named: `%s`" % vray_node) + + log.info("Setting scene resolution to: %s x %s" % (width, height)) + cmds.setAttr("%s.width" % control_node, width) + cmds.setAttr("%s.height" % control_node, height) + + +def set_context_settings(): + """Apply the project settings from the project definition + + Settings can be overwritten by an asset if the asset.data contains + any information regarding those settings. + + Examples of settings: + fps + resolution + renderer + + Returns: + None + """ + + # Todo (Wijnand): apply renderer and resolution of project + + project_data = lib.get_project_data() + asset_data = lib.get_asset_data() + + # Set project fps + fps = asset_data.get("fps", project_data.get("fps", 25)) + set_scene_fps(fps) + + # Set project resolution + width_key = "resolution_width" + height_key = "resolution_height" + + width = asset_data.get(width_key, project_data.get(width_key, 1920)) + height = asset_data.get(height_key, project_data.get(height_key, 1080)) + + set_scene_resolution(width, height) + + # Valid FPS def validate_fps(): """Validate current scene FPS and show pop-up when it is incorrect @@ -1405,7 +1810,7 @@ def validate_fps(): """ - fps = lib.get_project_fps() # can be int or float + fps = lib.get_asset_fps() current_fps = mel.eval('currentTimeUnitToFPS()') # returns float if current_fps != fps: @@ -1436,3 +1841,194 @@ def validate_fps(): return False return True + + +def bake(nodes, + frame_range=None, + step=1.0, + simulation=True, + preserve_outside_keys=False, + disable_implicit_control=True, + shape=True): + """Bake the given nodes over the time range. + + This will bake all attributes of the node, including custom attributes. + + Args: + nodes (list): Names of transform nodes, eg. camera, light. + frame_range (list): frame range with start and end frame. + or if None then takes timeSliderRange + simulation (bool): Whether to perform a full simulation of the + attributes over time. + preserve_outside_keys (bool): Keep keys that are outside of the baked + range. + disable_implicit_control (bool): When True will disable any + constraints to the object. + shape (bool): When True also bake attributes on the children shapes. + step (float): The step size to sample by. + + Returns: + None + + """ + + # Parse inputs + if not nodes: + return + + assert isinstance(nodes, (list, tuple)), "Nodes must be a list or tuple" + + # If frame range is None fall back to time slider playback time range + if frame_range is None: + frame_range = [cmds.playbackOptions(query=True, minTime=True), + cmds.playbackOptions(query=True, maxTime=True)] + + # If frame range is single frame bake one frame more, + # otherwise maya.cmds.bakeResults gets confused + if frame_range[1] == frame_range[0]: + frame_range[1] += 1 + + # Bake it + with keytangent_default(in_tangent_type='auto', + out_tangent_type='auto'): + cmds.bakeResults(nodes, + simulation=simulation, + preserveOutsideKeys=preserve_outside_keys, + disableImplicitControl=disable_implicit_control, + shape=shape, + sampleBy=step, + time=(frame_range[0], frame_range[1])) + + +def bake_to_world_space(nodes, + frame_range=None, + simulation=True, + preserve_outside_keys=False, + disable_implicit_control=True, + shape=True, + step=1.0): + """Bake the nodes to world space transformation (incl. other attributes) + + Bakes the transforms to world space (while maintaining all its animated + attributes and settings) by duplicating the node. Then parents it to world + and constrains to the original. + + Other attributes are also baked by connecting all attributes directly. + Baking is then done using Maya's bakeResults command. + + See `bake` for the argument documentation. + + Returns: + list: The newly created and baked node names. + + """ + + def _get_attrs(node): + """Workaround for buggy shape attribute listing with listAttr""" + attrs = cmds.listAttr(node, + write=True, + scalar=True, + settable=True, + connectable=True, + keyable=True, + shortNames=True) or [] + valid_attrs = [] + for attr in attrs: + node_attr = '{0}.{1}'.format(node, attr) + + # Sometimes Maya returns 'non-existent' attributes for shapes + # so we filter those out + if not cmds.attributeQuery(attr, node=node, exists=True): + continue + + # We only need those that have a connection, just to be safe + # that it's actually keyable/connectable anyway. + if cmds.connectionInfo(node_attr, + isDestination=True): + valid_attrs.append(attr) + + return valid_attrs + + transform_attrs = set(["t", "r", "s", + "tx", "ty", "tz", + "rx", "ry", "rz", + "sx", "sy", "sz"]) + + world_space_nodes = [] + with delete_after() as delete_bin: + + # Create the duplicate nodes that are in world-space connected to + # the originals + for node in nodes: + + # Duplicate the node + short_name = node.rsplit("|", 1)[-1] + new_name = "{0}_baked".format(short_name) + new_node = cmds.duplicate(node, + name=new_name, + renameChildren=True)[0] + + # Connect all attributes on the node except for transform + # attributes + attrs = _get_attrs(node) + attrs = set(attrs) - transform_attrs if attrs else [] + + for attr in attrs: + orig_node_attr = '{0}.{1}'.format(node, attr) + new_node_attr = '{0}.{1}'.format(new_node, attr) + + # unlock to avoid connection errors + cmds.setAttr(new_node_attr, lock=False) + + cmds.connectAttr(orig_node_attr, + new_node_attr, + force=True) + + # If shapes are also baked then connect those keyable attributes + if shape: + children_shapes = cmds.listRelatives(new_node, + children=True, + fullPath=True, + shapes=True) + if children_shapes: + orig_children_shapes = cmds.listRelatives(node, + children=True, + fullPath=True, + shapes=True) + for orig_shape, new_shape in zip(orig_children_shapes, + children_shapes): + attrs = _get_attrs(orig_shape) + for attr in attrs: + orig_node_attr = '{0}.{1}'.format(orig_shape, attr) + new_node_attr = '{0}.{1}'.format(new_shape, attr) + + # unlock to avoid connection errors + cmds.setAttr(new_node_attr, lock=False) + + cmds.connectAttr(orig_node_attr, + new_node_attr, + force=True) + + # Parent to world + if cmds.listRelatives(new_node, parent=True): + new_node = cmds.parent(new_node, world=True)[0] + + # Unlock transform attributes so constraint can be created + for attr in transform_attrs: + cmds.setAttr('{0}.{1}'.format(new_node, attr), lock=False) + + # Constraints + delete_bin.extend(cmds.parentConstraint(node, new_node, mo=False)) + delete_bin.extend(cmds.scaleConstraint(node, new_node, mo=False)) + + world_space_nodes.append(new_node) + + bake(world_space_nodes, + frame_range=frame_range, + step=step, + simulation=simulation, + preserve_outside_keys=preserve_outside_keys, + disable_implicit_control=disable_implicit_control, + shape=shape) + + return world_space_nodes diff --git a/pype/maya/menu.json b/pype/maya/menu.json index d394f5dba1..0c620d3e4c 100644 --- a/pype/maya/menu.json +++ b/pype/maya/menu.json @@ -1,1959 +1,1767 @@ -[ - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\others\\save_scene_incremental.py", - "sourcetype": "file", - "title": "Version Up", - "tooltip": "Incremental save with a specific format" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\others\\show_current_scene_in_explorer.py", - "sourcetype": "file", - "title": "Explore current scene..", - "tooltip": "Show current scene in Explorer" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\avalon\\launch_manager.py", - "sourcetype": "file", - "title": "Project Manager", - "tooltip": "Add assets to the project" - }, - { - "type": "separator" - }, - { - "type": "menu", - "title": "Modeling", - "items": [ - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\modeling\\duplicate_normalized.py", - "sourcetype": "file", - "tags": [ - "modeling", - "duplicate", - "normalized" - ], - "title": "Duplicate Normalized", - "tooltip": "" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\modeling\\transferUVs.py", - "sourcetype": "file", - "tags": [ - "modeling", - "transfer", - "uv" - ], - "title": "Transfer UVs", - "tooltip": "" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\modeling\\mirrorSymmetry.py", - "sourcetype": "file", - "tags": [ - "modeling", - "mirror", - "symmetry" - ], - "title": "Mirror Symmetry", - "tooltip": "" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\modeling\\selectOutlineUI.py", - "sourcetype": "file", - "tags": [ - "modeling", - "select", - "outline", - "ui" - ], - "title": "Select Outline UI", - "tooltip": "" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\modeling\\polyDeleteOtherUVSets.py", - "sourcetype": "file", - "tags": [ - "modeling", - "polygon", - "uvset", - "delete" - ], - "title": "Polygon Delete Other UV Sets", - "tooltip": "" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\modeling\\polyCombineQuick.py", - "sourcetype": "file", - "tags": [ - "modeling", - "combine", - "polygon", - "quick" - ], - "title": "Polygon Combine Quick", - "tooltip": "" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\modeling\\separateMeshPerShader.py", - "sourcetype": "file", - "tags": [ - "modeling", - "separateMeshPerShader" - ], - "title": "Separate Mesh Per Shader", - "tooltip": "" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\modeling\\polyDetachSeparate.py", - "sourcetype": "file", - "tags": [ - "modeling", - "poly", - "detach", - "separate" - ], - "title": "Polygon Detach and Separate", - "tooltip": "" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\modeling\\polyRelaxVerts.py", - "sourcetype": "file", - "tags": [ - "modeling", - "relax", - "verts" - ], - "title": "Polygon Relax Vertices", - "tooltip": "" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\modeling\\polySelectEveryNthEdgeUI.py", - "sourcetype": "file", - "tags": [ - "modeling", - "select", - "nth", - "edge", - "ui" - ], - "title": "Select Every Nth Edge" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\modeling\\djPFXUVs.py", - "sourcetype": "file", - "tags": [ - "modeling", - "djPFX", - "UVs" - ], - "title": "dj PFX UVs", - "tooltip": "" - } - ] - }, - { - "type": "menu", - "title": "Rigging", - "items": [ - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\rigging\\addCurveBetween.py", - "sourcetype": "file", - "tags": [ - "rigging", - "addCurveBetween", - "file" - ], - "title": "Add Curve Between" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\rigging\\averageSkinWeights.py", - "sourcetype": "file", - "tags": [ - "rigging", - "average", - "skin weights", - "file" - ], - "title": "Average Skin Weights" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\rigging\\cbSmoothSkinWeightUI.py", - "sourcetype": "file", - "tags": [ - "rigging", - "cbSmoothSkinWeightUI", - "file" - ], - "title": "CB Smooth Skin Weight UI" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\rigging\\channelBoxManagerUI.py", - "sourcetype": "file", - "tags": [ - "rigging", - "channelBoxManagerUI", - "file" - ], - "title": "Channel Box Manager UI" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\rigging\\characterAutorigger.py", - "sourcetype": "file", - "tags": [ - "rigging", - "characterAutorigger", - "file" - ], - "title": "Character Auto Rigger" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\rigging\\connectUI.py", - "sourcetype": "file", - "tags": [ - "rigging", - "connectUI", - "file" - ], - "title": "Connect UI" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\rigging\\copySkinWeightsLocal.py", - "sourcetype": "file", - "tags": [ - "rigging", - "copySkinWeightsLocal", - "file" - ], - "title": "Copy Skin Weights Local" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\rigging\\createCenterLocator.py", - "sourcetype": "file", - "tags": [ - "rigging", - "createCenterLocator", - "file" - ], - "title": "Create Center Locator" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\rigging\\freezeTransformToGroup.py", - "sourcetype": "file", - "tags": [ - "rigging", - "freezeTransformToGroup", - "file" - ], - "title": "Freeze Transform To Group" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\rigging\\groupSelected.py", - "sourcetype": "file", - "tags": [ - "rigging", - "groupSelected", - "file" - ], - "title": "Group Selected" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\rigging\\ikHandlePoleVectorLocator.py", - "sourcetype": "file", - "tags": [ - "rigging", - "ikHandlePoleVectorLocator", - "file" - ], - "title": "IK Handle Pole Vector Locator" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\rigging\\jointOrientUI.py", - "sourcetype": "file", - "tags": [ - "rigging", - "jointOrientUI", - "file" - ], - "title": "Joint Orient UI" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\rigging\\jointsOnCurve.py", - "sourcetype": "file", - "tags": [ - "rigging", - "jointsOnCurve", - "file" - ], - "title": "Joints On Curve" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\rigging\\resetBindSelectedSkinJoints.py", - "sourcetype": "file", - "tags": [ - "rigging", - "resetBindSelectedSkinJoints", - "file" - ], - "title": "Reset Bind Selected Skin Joints" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\rigging\\selectSkinclusterJointsFromSelectedComponents.py", - "sourcetype": "file", - "tags": [ - "rigging", - "selectSkinclusterJointsFromSelectedComponents", - "file" - ], - "title": "Select Skincluster Joints From Selected Components" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\rigging\\selectSkinclusterJointsFromSelectedMesh.py", - "sourcetype": "file", - "tags": [ - "rigging", - "selectSkinclusterJointsFromSelectedMesh", - "file" - ], - "title": "Select Skincluster Joints From Selected Mesh" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\rigging\\setJointLabels.py", - "sourcetype": "file", - "tags": [ - "rigging", - "setJointLabels", - "file" - ], - "title": "Set Joint Labels" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\rigging\\setJointOrientationFromCurrentRotation.py", - "sourcetype": "file", - "tags": [ - "rigging", - "setJointOrientationFromCurrentRotation", - "file" - ], - "title": "Set Joint Orientation From Current Rotation" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\rigging\\setSelectedJointsOrientationZero.py", - "sourcetype": "file", - "tags": [ - "rigging", - "setSelectedJointsOrientationZero", - "file" - ], - "title": "Set Selected Joints Orientation Zero" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\rigging\\mirrorCurveShape.py", - "sourcetype": "file", - "tags": [ - "rigging", - "mirrorCurveShape", - "file" - ], - "title": "Mirror Curve Shape" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\rigging\\setRotationOrderUI.py", - "sourcetype": "file", - "tags": [ - "rigging", - "setRotationOrderUI", - "file" - ], - "title": "Set Rotation Order UI" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\rigging\\paintItNowUI.py", - "sourcetype": "file", - "tags": [ - "rigging", - "paintItNowUI", - "file" - ], - "title": "Paint It Now UI" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\rigging\\parentScaleConstraint.py", - "sourcetype": "file", - "tags": [ - "rigging", - "parentScaleConstraint", - "file" - ], - "title": "Parent Scale Constraint" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\rigging\\quickSetWeightsUI.py", - "sourcetype": "file", - "tags": [ - "rigging", - "quickSetWeightsUI", - "file" - ], - "title": "Quick Set Weights UI" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\rigging\\rapidRig.py", - "sourcetype": "file", - "tags": [ - "rigging", - "rapidRig", - "file" - ], - "title": "Rapid Rig" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\rigging\\regenerate_blendshape_targets.py", - "sourcetype": "file", - "tags": [ - "rigging", - "regenerate_blendshape_targets", - "file" - ], - "title": "Regenerate Blendshape Targets" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\rigging\\removeRotationAxis.py", - "sourcetype": "file", - "tags": [ - "rigging", - "removeRotationAxis", - "file" - ], - "title": "Remove Rotation Axis" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\rigging\\resetBindSelectedMeshes.py", - "sourcetype": "file", - "tags": [ - "rigging", - "resetBindSelectedMeshes", - "file" - ], - "title": "Reset Bind Selected Meshes" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\rigging\\simpleControllerOnSelection.py", - "sourcetype": "file", - "tags": [ - "rigging", - "simpleControllerOnSelection", - "file" - ], - "title": "Simple Controller On Selection" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\rigging\\simpleControllerOnSelectionHierarchy.py", - "sourcetype": "file", - "tags": [ - "rigging", - "simpleControllerOnSelectionHierarchy", - "file" - ], - "title": "Simple Controller On Selection Hierarchy" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\rigging\\superRelativeCluster.py", - "sourcetype": "file", - "tags": [ - "rigging", - "superRelativeCluster", - "file" - ], - "title": "Super Relative Cluster" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\rigging\\tfSmoothSkinWeight.py", - "sourcetype": "file", - "tags": [ - "rigging", - "tfSmoothSkinWeight", - "file" - ], - "title": "TF Smooth Skin Weight" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\rigging\\toggleIntermediates.py", - "sourcetype": "file", - "tags": [ - "rigging", - "toggleIntermediates", - "file" - ], - "title": "Toggle Intermediates" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\rigging\\toggleSegmentScaleCompensate.py", - "sourcetype": "file", - "tags": [ - "rigging", - "toggleSegmentScaleCompensate", - "file" - ], - "title": "Toggle Segment Scale Compensate" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\rigging\\toggleSkinclusterDeformNormals.py", - "sourcetype": "file", - "tags": [ - "rigging", - "toggleSkinclusterDeformNormals", - "file" - ], - "title": "Toggle Skincluster Deform Normals" - } - ] - }, - { - "type": "menu", - "title": "Shading", - "items": [ - { - "type": "menu", - "title": "VRay", - "items": [ - { - "type": "action", - "title": "Import Proxies", - "command": "$COLORBLEED_SCRIPTS\\shading\\vray\\vrayImportProxies.py", - "sourcetype": "file", - "tags": ["shading", "vray", "import", "proxies"], - "tooltip": "" - }, - { - "type": "separator" - }, - { - "type": "action", - "title": "Select All GES", - "command": "$COLORBLEED_SCRIPTS\\shading\\vray\\selectAllGES.py", - "sourcetype": "file", - "tooltip": "", - "tags": [ - "shading", - "vray", - "select All GES" - ] - }, - { - "type": "action", - "title": "Select All GES Under Selection", - "command": "$COLORBLEED_SCRIPTS\\shading\\vray\\selectAllGESUnderSelection.py", - "sourcetype": "file", - "tooltip": "", - "tags": ["shading", "vray", "select", "all", "GES"] - }, - { - "type": "separator" - }, - { - "type": "action", - "title": "Selection To VRay Mesh", - "command": "$COLORBLEED_SCRIPTS\\shading\\vray\\selectionToVrayMesh.py", - "sourcetype": "file", - "tooltip": "", - "tags": ["shading", "vray", "selection", "vraymesh"] - }, - { - "type": "action", - "title": "Add VRay Round Edges Attribute", - "command": "$COLORBLEED_SCRIPTS\\shading\\vray\\addVrayRoundEdgesAttribute.py", - "sourcetype": "file", - "tooltip": "", - "tags": ["shading", "vray", "round edges", "attribute"] - }, - { - "type": "action", - "title": "Add Gamma", - "command": "$COLORBLEED_SCRIPTS\\shading\\vray\\vrayAddGamma.py", - "sourcetype": "file", - "tooltip": "", - "tags": ["shading", "vray", "add gamma"] - }, - { - "type": "separator" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\shading\\vray\\select_vraymesh_materials_with_unconnected_shader_slots.py", - "sourcetype": "file", - "title": "Select Unconnected Shader Materials", - "tags": [ - "shading", - "vray", - "select", - "vraymesh", - "materials", - "unconnected shader slots" - ], - "tooltip": "" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\shading\\vray\\vrayMergeSimilarVRayMeshMaterials.py", - "sourcetype": "file", - "title": "Merge Similar VRay Mesh Materials", - "tags": [ - "shading", - "vray", - "Merge", - "VRayMesh", - "Materials" - ], - "tooltip": "" - }, - { - "type": "action", - "title": "Create Two Sided Material", - "command": "$COLORBLEED_SCRIPTS\\shading\\vray\\vrayCreate2SidedMtlForSelectedMtlRenamed.py", - "sourcetype": "file", - "tooltip": "Creates two sided material for selected material and renames it", - "tags": [ - "shading", - "vray", - "two sided", - "material" - ] - }, - { - "type": "action", - "title": "Create Two Sided Material For Selected", - "command": "$COLORBLEED_SCRIPTS\\shading\\vray\\vrayCreate2SidedMtlForSelectedMtl.py", - "sourcetype": "file", - "tooltip": "Select material to create a two sided version from it", - "tags": [ - "shading", - "vray", - "Create2SidedMtlForSelectedMtl.py" - ] - }, - { - "type": "separator" - }, - { - "type": "action", - "title": "Add OpenSubdiv Attribute", - "command": "$COLORBLEED_SCRIPTS\\shading\\vray\\addVrayOpenSubdivAttribute.py", - "sourcetype": "file", - "tooltip": "", - "tags": [ - "shading", - "vray", - "add", - "open subdiv", - "attribute" - ] - }, - { - "type": "action", - "title": "Remove OpenSubdiv Attribute", - "command": "$COLORBLEED_SCRIPTS\\shading\\vray\\removeVrayOpenSubdivAttribute.py", - "sourcetype": "file", - "tooltip": "", - "tags": [ - "shading", - "vray", - "remove", - "opensubdiv", - "attributee" - ] - }, - { - "type": "separator" - }, - { - "type": "action", - "title": "Add Subdivision Attribute", - "command": "$COLORBLEED_SCRIPTS\\shading\\vray\\addVraySubdivisionAttribute.py", - "sourcetype": "file", - "tooltip": "", - "tags": [ - "shading", - "vray", - "addVraySubdivisionAttribute" - ] - }, - { - "type": "action", - "title": "Remove Subdivision Attribute.py", - "command": "$COLORBLEED_SCRIPTS\\shading\\vray\\removeVraySubdivisionAttribute.py", - "sourcetype": "file", - "tooltip": "", - "tags": [ - "shading", - "vray", - "remove", - "subdivision", - "attribute" - ] - }, - { - "type": "separator" - }, - { - "type": "action", - "title": "Add Vray Object Ids", - "command": "$COLORBLEED_SCRIPTS\\shading\\vray\\addVrayObjectIds.py", - "sourcetype": "file", - "tooltip": "", - "tags": [ - "shading", - "vray", - "add", - "object id" - ] - }, - { - "type": "action", - "title": "Add Vray Material Ids", - "command": "$COLORBLEED_SCRIPTS\\shading\\vray\\addVrayMaterialIds.py", - "sourcetype": "file", - "tooltip": "", - "tags": [ - "shading", - "vray", - "addVrayMaterialIds.py" - ] - }, - { - "type": "separator" - }, - { - "type": "action", - "title": "Set Physical DOF Depth", - "command": "$COLORBLEED_SCRIPTS\\shading\\vray\\vrayPhysicalDOFSetDepth.py", - "sourcetype": "file", - "tooltip": "", - "tags": [ - "shading", - "vray", - "physical", - "DOF ", - "Depth" - ] - }, - { - "type": "action", - "title": "Magic Vray Proxy UI", - "command": "$COLORBLEED_SCRIPTS\\shading\\vray\\magicVrayProxyUI.py", - "sourcetype": "file", - "tooltip": "", - "tags": [ - "shading", - "vray", - "magicVrayProxyUI" - ] - } - ] - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\pyblish\\lighting\\set_filename_prefix.py", - "sourcetype": "file", - "tags": [ - "shading", - "lookdev", - "assign", - "shaders", - "prefix", - "filename", - "render" - ], - "title": "Set filename prefix", - "tooltip": "Set the render file name prefix." - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\shading\\look_manager_ui.py", - "sourcetype": "file", - "tags": [ - "shading", - "look", - "assign", - "shaders", - "auto" - ], - "title": "Look Manager", - "tooltip": "Open the Look Manager UI for look assignment" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\shading\\LightLinkUi.py", - "sourcetype": "file", - "tags": [ - "shading", - "light", - "link", - "ui" - ], - "title": "Light Link UI", - "tooltip": "" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\shading\\vdviewer_ui.py", - "sourcetype": "file", - "tags": [ - "shading", - "look", - "vray", - "displacement", - "shaders", - "auto" - ], - "title": "VRay Displ Viewer", - "tooltip": "Open the VRay Displacement Viewer, select and control the content of the set" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\shading\\setTexturePreviewToCLRImage.py", - "sourcetype": "file", - "tags": [ - "shading", - "CLRImage", - "textures", - "preview" - ], - "title": "Set Texture Preview To CLRImage", - "tooltip": "" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\shading\\fixDefaultShaderSetBehavior.py", - "sourcetype": "file", - "tags": [ - "shading", - "fix", - "DefaultShaderSet", - "Behavior" - ], - "title": "Fix Default Shader Set Behavior", - "tooltip": "" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\shading\\fixSelectedShapesReferenceAssignments.py", - "sourcetype": "file", - "tags": [ - "shading", - "fix", - "Selected", - "Shapes", - "Reference", - "Assignments" - ], - "title": "Fix Shapes Reference Assignments", - "tooltip": "Select shapes to fix the reference assignments" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\shading\\selectLambert1Members.py", - "sourcetype": "file", - "tags": [ - "shading", - "selectLambert1Members" - ], - "title": "Select Lambert1 Members", - "tooltip": "Selects all objects which have the Lambert1 shader assigned" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\shading\\selectShapesWithoutShader.py", - "sourcetype": "file", - "tags": [ - "shading", - "selectShapesWithoutShader" - ], - "title": "Select Shapes Without Shader", - "tooltip": "" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\shading\\fixRenderLayerOutAdjustmentErrors.py", - "sourcetype": "file", - "tags": [ - "shading", - "fixRenderLayerOutAdjustmentErrors" - ], - "title": "Fix RenderLayer Out Adjustment Errors", - "tooltip": "" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\shading\\fix_renderlayer_missing_node_override.py", - "sourcetype": "file", - "tags": [ - "shading", - "renderlayer", - "missing", - "reference", - "switch", - "layer" - ], - "title": "Fix RenderLayer Missing Referenced Nodes Overrides", - "tooltip": "" - }, - { - "type": "action", - "title": "Image 2 Tiled EXR", - "command": "$COLORBLEED_SCRIPTS\\shading\\open_img2exr.py", - "sourcetype": "file", - "tooltip": "", - "tags": [ - "shading", - "vray", - "exr" - ] - } - ] - }, - { - "type": "menu", - "title": "Rendering", - "items": [ - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\pyblish\\open_deadline_submission_settings.py", - "sourcetype": "file", - "tags": [ - "settings", - "deadline", - "globals", - "render" - ], - "title": "DL Submission Settings UI", - "tooltip": "Open the Deadline Submission Settings UI" - } - ] - }, - { - "type": "menu", - "title": "Animation", - "items": [ - { - "type": "menu", - "title": "Attributes", - "tooltip": "", - "items": [ - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\animation\\attributes\\copyValues.py", - "sourcetype": "file", - "tags": [ - "animation", - "copy", - "attributes" - ], - "title": "Copy Values", - "tooltip": "Copy attribute values" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\animation\\attributes\\copyInConnections.py", - "sourcetype": "file", - "tags": [ - "animation", - "copy", - "attributes", - "connections", - "incoming" - ], - "title": "Copy In Connections", - "tooltip": "Copy incoming connections" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\animation\\attributes\\copyOutConnections.py", - "sourcetype": "file", - "tags": [ - "animation", - "copy", - "attributes", - "connections", - "out" - ], - "title": "Copy Out Connections", - "tooltip": "Copy outcoming connections" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\animation\\attributes\\copyTransformLocal.py", - "sourcetype": "file", - "tags": [ - "animation", - "copy", - "attributes", - "transforms", - "local" - ], - "title": "Copy Local Transfroms", - "tooltip": "Copy local transfroms" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\animation\\attributes\\copyTransformMatrix.py", - "sourcetype": "file", - "tags": [ - "animation", - "copy", - "attributes", - "transforms", - "matrix" - ], - "title": "Copy Matrix Transfroms", - "tooltip": "Copy Matrix transfroms" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\animation\\attributes\\copyTransformUI.py", - "sourcetype": "file", - "tags": [ - "animation", - "copy", - "attributes", - "transforms", - "UI" - ], - "title": "Copy Transforms UI", - "tooltip": "Open the Copy Transforms UI" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\animation\\attributes\\simpleCopyUI.py", - "sourcetype": "file", - "tags": [ - "animation", - "copy", - "attributes", - "transforms", - "UI", - "simple" - ], - "title": "Simple Copy UI", - "tooltip": "Open the simple Copy Transforms UI" - } - ] - }, - { - "type": "menu", - "title": "Optimize", - "tooltip": "Optimization scripts", - "items": [ - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\animation\\optimize\\toggleFreezeHierarchy.py", - "sourcetype": "file", - "tags": [ - "animation", - "hierarchy", - "toggle", - "freeze" - ], - "title": "Toggle Freeze Hierarchy", - "tooltip": "Freeze and unfreeze hierarchy" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\animation\\optimize\\toggleParallelNucleus.py", - "sourcetype": "file", - "tags": [ - "animation", - "nucleus", - "toggle", - "parallel" - ], - "title": "Toggle Parallel Nucleus", - "tooltip": "Toggle parallel nucleus" - } - ] - }, - { - "sourcetype": "file", - "command": "$COLORBLEED_SCRIPTS\\animation\\bakeSelectedToWorldSpace.py", - "tags": ["animation", "bake","selection", "worldspace.py"], - "title": "Bake Selected To Worldspace", - "type": "action" - }, - { - "sourcetype": "file", - "command": "$COLORBLEED_SCRIPTS\\animation\\timeStepper.py", - "tags": ["animation", "time","stepper"], - "title": "Time Stepper", - "type": "action" - }, - { - "sourcetype": "file", - "command": "$COLORBLEED_SCRIPTS\\animation\\capture_ui.py", - "tags": ["animation", "capture", "ui", "screen", "movie", "image"], - "title": "Capture UI", - "type": "action" - }, - { - "sourcetype": "file", - "command": "$COLORBLEED_SCRIPTS\\animation\\simplePlayblastUI.py", - "tags": ["animation", "simple", "playblast", "ui"], - "title": "Simple Playblast UI", - "type": "action" - }, - { - "sourcetype": "file", - "command": "$COLORBLEED_SCRIPTS\\animation\\tweenMachineUI.py", - "tags": ["animation", "tween", "machine"], - "title": "Tween Machine UI", - "type": "action" - }, - { - "sourcetype": "file", - "command": "$COLORBLEED_SCRIPTS\\animation\\selectAllAnimationCurves.py", - "tags": ["animation", "select", "curves"], - "title": "Select All Animation Curves", - "type": "action" - }, - { - "sourcetype": "file", - "command": "$COLORBLEED_SCRIPTS\\animation\\pathAnimation.py", - "tags": ["animation", "path", "along"], - "title": "Path Animation", - "type": "action" - }, - { - "sourcetype": "file", - "command": "$COLORBLEED_SCRIPTS\\animation\\offsetSelectedObjectsUI.py", - "tags": [ - "animation", - "offsetSelectedObjectsUI.py" - ], - "title": "Offset Selected Objects UI", - "type": "action" - }, - { - "sourcetype": "file", - "command": "$COLORBLEED_SCRIPTS\\animation\\key_amplifier_ui.py", - "tags": [ - "animation", - "key", "amplifier" - ], - "title": "Key Amplifier UI", - "type": "action" - }, - { - "sourcetype": "file", - "command": "$COLORBLEED_SCRIPTS\\animation\\anim_scene_optimizer.py", - "tags": [ - "animation", - "anim_scene_optimizer.py" - ], - "title": "Anim_Scene_Optimizer", - "type": "action" - }, - { - "sourcetype": "file", - "command": "$COLORBLEED_SCRIPTS\\animation\\zvParentMaster.py", - "tags": [ - "animation", - "zvParentMaster.py" - ], - "title": "ZV Parent Master", - "type": "action" - }, - { - "sourcetype": "file", - "command": "$COLORBLEED_SCRIPTS\\animation\\poseLibrary.py", - "tags": [ - "animation", - "poseLibrary.py" - ], - "title": "Pose Library", - "type": "action" - } - ] - }, - { - "type": "menu", - "title": "Layout", - "items": [ - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\layout\\alignDistributeUI.py", - "sourcetype": "file", - "tags": [ - "layout", - "align", - "Distribute", - "UI" - ], - "title": "Align Distribute UI", - "tooltip": "" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\layout\\alignSimpleUI.py", - "sourcetype": "file", - "tags": [ - "layout", - "align", - "UI", - "Simple" - ], - "title": "Align Simple UI", - "tooltip": "" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\layout\\center_locator.py", - "sourcetype": "file", - "tags": [ - "layout", - "center", - "locator" - ], - "title": "Center Locator", - "tooltip": "" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\layout\\average_locator.py", - "sourcetype": "file", - "tags": [ - "layout", - "average", - "locator" - ], - "title": "Average Locator", - "tooltip": "" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\layout\\selectWithinProximityUI.py", - "sourcetype": "file", - "tags": [ - "layout", - "select", - "proximity", - "ui" - ], - "title": "Select Within Proximity UI", - "tooltip": "" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\layout\\dupCurveUI.py", - "sourcetype": "file", - "tags": [ - "layout", - "Duplicate", - "Curve", - "UI" - ], - "title": "Duplicate Curve UI", - "tooltip": "" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\layout\\randomDeselectUI.py", - "sourcetype": "file", - "tags": [ - "layout", - "random", - "Deselect", - "UI" - ], - "title": "Random Deselect UI", - "tooltip": "" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\layout\\multiReferencerUI.py", - "sourcetype": "file", - "tags": [ - "layout", - "multi", - "reference" - ], - "title": "Multi Referencer UI", - "tooltip": "" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\layout\\duplicateOffsetUI.py", - "sourcetype": "file", - "tags": [ - "layout", - "duplicate", - "offset", - "UI" - ], - "title": "Duplicate Offset UI", - "tooltip": "" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\layout\\spPaint3d.py", - "sourcetype": "file", - "tags": [ - "layout", - "spPaint3d", - "paint", - "tool" - ], - "title": "SP Paint 3d", - "tooltip": "" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\layout\\randomizeUI.py", - "sourcetype": "file", - "tags": [ - "layout", - "randomize", - "UI" - ], - "title": "Randomize UI", - "tooltip": "" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\layout\\distributeWithinObjectUI.py", - "sourcetype": "file", - "tags": [ - "layout", - "distribute", - "ObjectUI", - "within" - ], - "title": "Distribute Within Object UI", - "tooltip": "" - } - ] - }, - { - "type": "menu", - "title": "Particles", - "items": [ - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\particles\\instancerToObjects.py", - "sourcetype": "file", - "tags": [ - "particles", - "instancerToObjects" - ], - "title": "Instancer To Objects", - "tooltip": "" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\particles\\instancerToObjectsInstances.py", - "sourcetype": "file", - "tags": [ - "particles", - "instancerToObjectsInstances" - ], - "title": "Instancer To Objects Instances", - "tooltip": "" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\particles\\objectsToParticlesAndInstancerCleanSource.py", - "sourcetype": "file", - "tags": ["particles", "objects", "Particles", "Instancer", "Clean", "Source"], - "title": "Objects To Particles & Instancer - Clean Source", - "tooltip": "" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\particles\\particleComponentsToLocators.py", - "sourcetype": "file", - "tags": ["particles", "components", "locators"], - "title": "Particle Components To Locators", - "tooltip": "" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\particles\\objectsToParticlesAndInstancer.py", - "sourcetype": "file", - "tags": [ - "particles", "objects", "particles", "instancer"], - "title": "Objects To Particles And Instancer", - "tooltip": "" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\particles\\spawnParticlesOnMesh.py", - "sourcetype": "file", - "tags": ["particles", "spawn","on","mesh"], - "title": "Spawn Particles On Mesh", - "tooltip": "" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\particles\\instancerToObjectsInstancesWithAnimation.py", - "sourcetype": "file", - "tags": [ - "particles", - "instancerToObjectsInstancesWithAnimation" - ], - "title": "Instancer To Objects Instances With Animation", - "tooltip": "" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\particles\\objectsToParticles.py", - "sourcetype": "file", - "tags": [ - "particles", - "objectsToParticles" - ], - "title": "Objects To Particles", - "tooltip": "" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\particles\\add_particle_cacheFile_attrs.py", - "sourcetype": "file", - "tags": [ - "particles", - "add_particle_cacheFile_attrs" - ], - "title": "Add Particle CacheFile Attributes", - "tooltip": "" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\particles\\mergeParticleSystems.py", - "sourcetype": "file", - "tags": [ - "particles", - "mergeParticleSystems" - ], - "title": "Merge Particle Systems", - "tooltip": "" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\particles\\particlesToLocators.py", - "sourcetype": "file", - "tags": [ - "particles", - "particlesToLocators" - ], - "title": "Particles To Locators", - "tooltip": "" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\particles\\instancerToObjectsWithAnimation.py", - "sourcetype": "file", - "tags": [ - "particles", - "instancerToObjectsWithAnimation" - ], - "title": "Instancer To Objects With Animation", - "tooltip": "" - }, - {"type": "separator"}, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\particles\\mayaReplicateHoudiniTool.py", - "sourcetype": "file", - "tags": [ - "particles", - "houdini", "houdiniTool", "houdiniEngine" - ], - "title": "Replicate Houdini Tool", - "tooltip": "" - }, - {"type": "separator"}, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\particles\\clearInitialState.py", - "sourcetype": "file", - "tags": [ - "particles", - "clearInitialState" - ], - "title": "Clear Initial State", - "tooltip": "" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\particles\\killSelectedParticles.py", - "sourcetype": "file", - "tags": [ - "particles", - "killSelectedParticles" - ], - "title": "Kill Selected Particles", - "tooltip": "" - } - ] - }, - { - "type": "menu", - "title": "Cleanup", - "items": [ - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\cleanup\\repair_faulty_containers.py", - "sourcetype": "file", - "tags": [ - "cleanup", - "repair", "containers" - ], - "title": "Find and Repair Containers", - "tooltip": "" - },{ - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\cleanup\\selectByType.py", - "sourcetype": "file", - "tags": [ - "cleanup", - "selectByType" - ], - "title": "Select By Type", - "tooltip": "" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\cleanup\\selectIntermediateObjects.py", - "sourcetype": "file", - "tags": [ - "cleanup", - "selectIntermediateObjects" - ], - "title": "Select Intermediate Objects", - "tooltip": "" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\cleanup\\selectNonUniqueNames.py", - "sourcetype": "file", - "tags": ["cleanup", "select", "non unique", "names"], - "title": "Select Non Unique Names", - "tooltip": "" - }, - {"type": "separator"}, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\cleanup\\removeNamespaces.py", - "sourcetype": "file", - "tags": ["cleanup", "remove", "namespaces"], - "title": "Remove Namespaces", - "tooltip": "Remove all namespaces" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\cleanup\\remove_user_defined_attributes.py", - "sourcetype": "file", - "tags": [ - "cleanup", - "remove_user_defined_attributes" - ], - "title": "Remove User Defined Attributes", - "tooltip": "Remove all user-defined attributs from all nodes" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\cleanup\\removeUnknownNodes.py", - "sourcetype": "file", - "tags": [ - "cleanup", - "removeUnknownNodes" - ], - "title": "Remove Unknown Nodes", - "tooltip": "Remove all unknown nodes" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\cleanup\\removeUnloadedReferences.py", - "sourcetype": "file", - "tags": [ - "cleanup", - "removeUnloadedReferences" - ], - "title": "Remove Unloaded References", - "tooltip": "Remove all unloaded references" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\cleanup\\removeReferencesFailedEdits.py", - "sourcetype": "file", - "tags": [ - "cleanup", - "removeReferencesFailedEdits" - ], - "title": "Remove References Failed Edits", - "tooltip": "Remove failed edits for all references" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\cleanup\\remove_unused_looks.py", - "sourcetype": "file", - "tags": [ - "cleanup", - "removeUnusedLooks" - ], - "title": "Remove Unused Looks", - "tooltip": "Remove all loaded yet unused Avalon look containers" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\cleanup\\deleteGhostIntermediateObjects.py", - "sourcetype": "file", - "tags": [ - "cleanup", - "deleteGhostIntermediateObjects" - ], - "title": "Delete Ghost Intermediate Objects", - "tooltip": "" - }, - {"type": "separator"}, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\cleanup\\resetViewportCache.py", - "sourcetype": "file", - "tags": ["cleanup", "reset","viewport", "cache"], - "title": "Reset Viewport Cache", - "tooltip": "" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\cleanup\\uniqifyNodeNames.py", - "sourcetype": "file", - "tags": [ - "cleanup", - "uniqifyNodeNames" - ], - "title": "Uniqify Node Names", - "tooltip": "" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\cleanup\\autoRenameFileNodes.py", - "sourcetype": "file", - "tags": [ - "cleanup", - "auto", "rename","filenodes" - ], - "title": "Auto Rename File Nodes", - "tooltip": "" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\cleanup\\update_asset_id.py", - "sourcetype": "file", - "tags":["cleanup", "update", "database", "asset", "id"], - "title": "Update Asset ID", - "tooltip": "Will replace the Colorbleed ID with a new one (asset ID : Unique number)" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\cleanup\\colorbleedRename.py", - "sourcetype": "file", - "tags": ["cleanup", "rename", "ui"], - "title": "Colorbleed Renamer", - "tooltip": "Colorbleed Rename UI" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\cleanup\\renameShapesToTransform.py", - "sourcetype": "file", - "tags": [ - "cleanup", - "renameShapesToTransform" - ], - "title": "Rename Shapes To Transform", - "tooltip": "" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\cleanup\\reorderUI.py", - "sourcetype": "file", - "tags": [ - "cleanup", - "reorderUI" - ], - "title": "Reorder UI", - "tooltip": "" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\cleanup\\pastedCleaner.py", - "sourcetype": "file", - "tags": [ - "cleanup", - "pastedCleaner" - ], - "title": "Pasted Cleaner", - "tooltip": "" - } - ] - }, - { - "type": "menu", - "title": "Others", - "items": [ - { - "type": "menu", - "sourcetype": "file", - "title": "Yeti", - "items": [ - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\others\\yeti\\cache_selected_yeti_nodes.py", - "sourcetype": "file", - "tags": ["others", "yeti", "cache", "selected"], - "title": "Cache Selected Yeti Nodes", - "tooltip": "" - } - ] - }, - { - "type": "menu", - "title": "Hair", - "tooltip": "", - "items": [ - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\others\\hair\\recolorHairCurrentCurve", - "sourcetype": "file", - "tags": ["others", "selectSoftSelection"], - "title": "Select Soft Selection", - "tooltip": "" - } - ] - }, - { - "type": "menu", - "command": "$COLORBLEED_SCRIPTS\\others\\display", - "sourcetype": "file", - "tags": [ - "others", - "display" - ], - "title": "Display", - "items": [ - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\others\\display\\wireframeSelectedObjects.py", - "sourcetype": "file", - "tags": ["others", "wireframe","selected","objects"], - "title": "Wireframe Selected Objects", - "tooltip": "" - } - ] - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\others\\archiveSceneUI.py", - "sourcetype": "file", - "tags": [ - "others", - "archiveSceneUI" - ], - "title": "Archive Scene UI", - "tooltip": "" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\others\\getSimilarMeshes.py", - "sourcetype": "file", - "tags": [ - "others", - "getSimilarMeshes" - ], - "title": "Get Similar Meshes", - "tooltip": "" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\others\\createBoundingBoxEachSelected.py", - "sourcetype": "file", - "tags": [ - "others", - "createBoundingBoxEachSelected" - ], - "title": "Create BoundingBox Each Selected", - "tooltip": "" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\others\\curveFromPositionEveryFrame.py", - "sourcetype": "file", - "tags": [ - "others", - "curveFromPositionEveryFrame" - ], - "title": "Curve From Position", - "tooltip": "" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\others\\instanceLeafSmartTransform.py", - "sourcetype": "file", - "tags": ["others", "instance","leaf", "smart", "transform"], - "title": "Instance Leaf Smart Transform", - "tooltip": "" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\others\\instanceSmartTransform.py", - "sourcetype": "file", - "tags": ["others", "instance", "smart", "transform"], - "title": "Instance Smart Transform", - "tooltip": "" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\others\\randomizeUVShellsSelectedObjects.py", - "sourcetype": "file", - "tags": [ - "others", - "randomizeUVShellsSelectedObjects" - ], - "title": "Randomize UV Shells", - "tooltip": "Select objects before running action" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\others\\centerPivotGroup.py", - "sourcetype": "file", - "tags": [ - "others", - "centerPivotGroup" - ], - "title": "Center Pivot Group", - "tooltip": "" - }, - {"type": "separator"}, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\others\\locatorsOnSelectedFaces.py", - "sourcetype": "file", - "tags": [ - "others", - "locatorsOnSelectedFaces" - ], - "title": "Locators On Selected Faces", - "tooltip": "" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\others\\locatorsOnEdgeSelectionPrompt.py", - "sourcetype": "file", - "tags": [ - "others", - "locatorsOnEdgeSelectionPrompt" - ], - "title": "Locators On Edge Selection Prompt", - "tooltip": "" - }, - {"type": "separator"}, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\others\\copyDeformers.py", - "sourcetype": "file", - "tags": [ - "others", - "copyDeformers" - ], - "title": "Copy Deformers", - "tooltip": "" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\others\\selectInReferenceEditor.py", - "sourcetype": "file", - "tags": [ - "others", - "selectInReferenceEditor" - ], - "title": "Select In Reference Editor", - "tooltip": "" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\others\\selectConstrainingObject.py", - "sourcetype": "file", - "tags": [ - "others", - "selectConstrainingObject" - ], - "title": "Select Constraining Object", - "tooltip": "" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\others\\deformerSetRelationsUI.py", - "sourcetype": "file", - "tags": [ - "others", - "deformerSetRelationsUI" - ], - "title": "Deformer Set Relations UI", - "tooltip": "" - }, - { - "type": "action", - "command": "$COLORBLEED_SCRIPTS\\others\\recreateBaseNodesForAllLatticeNodes.py", - "sourcetype": "file", - "tags": ["others", "recreate","base", "nodes", "lattice"], - "title": "Recreate Base Nodes For Lattice Nodes", - "tooltip": "" - } - ] - } -] \ No newline at end of file +[{ + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\others\\save_scene_incremental.py", + "sourcetype": "file", + "title": "Version Up", + "tooltip": "Incremental save with a specific format" +}, +{ + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\others\\show_current_scene_in_explorer.py", + "sourcetype": "file", + "title": "Explore current scene..", + "tooltip": "Show current scene in Explorer" +}, +{ + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\avalon\\launch_manager.py", + "sourcetype": "file", + "title": "Project Manager", + "tooltip": "Add assets to the project" +}, +{ + "type": "separator" +}, +{ + "type": "menu", + "title": "Modeling", + "items": [{ + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\modeling\\duplicate_normalized.py", + "sourcetype": "file", + "tags": ["modeling", + "duplicate", + "normalized"], + "title": "Duplicate Normalized", + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\modeling\\transferUVs.py", + "sourcetype": "file", + "tags": ["modeling", + "transfer", + "uv"], + "title": "Transfer UVs", + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\modeling\\mirrorSymmetry.py", + "sourcetype": "file", + "tags": ["modeling", + "mirror", + "symmetry"], + "title": "Mirror Symmetry", + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\modeling\\selectOutlineUI.py", + "sourcetype": "file", + "tags": ["modeling", + "select", + "outline", + "ui"], + "title": "Select Outline UI", + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\modeling\\polyDeleteOtherUVSets.py", + "sourcetype": "file", + "tags": ["modeling", + "polygon", + "uvset", + "delete"], + "title": "Polygon Delete Other UV Sets", + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\modeling\\polyCombineQuick.py", + "sourcetype": "file", + "tags": ["modeling", + "combine", + "polygon", + "quick"], + "title": "Polygon Combine Quick", + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\modeling\\separateMeshPerShader.py", + "sourcetype": "file", + "tags": ["modeling", + "separateMeshPerShader"], + "title": "Separate Mesh Per Shader", + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\modeling\\polyDetachSeparate.py", + "sourcetype": "file", + "tags": ["modeling", + "poly", + "detach", + "separate"], + "title": "Polygon Detach and Separate", + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\modeling\\polyRelaxVerts.py", + "sourcetype": "file", + "tags": ["modeling", + "relax", + "verts"], + "title": "Polygon Relax Vertices", + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\modeling\\polySelectEveryNthEdgeUI.py", + "sourcetype": "file", + "tags": ["modeling", + "select", + "nth", + "edge", + "ui"], + "title": "Select Every Nth Edge" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\modeling\\djPFXUVs.py", + "sourcetype": "file", + "tags": ["modeling", + "djPFX", + "UVs"], + "title": "dj PFX UVs", + "tooltip": "" + }] +}, +{ + "type": "menu", + "title": "Rigging", + "items": [{ + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\rigging\\addCurveBetween.py", + "sourcetype": "file", + "tags": ["rigging", + "addCurveBetween", + "file"], + "title": "Add Curve Between" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\rigging\\averageSkinWeights.py", + "sourcetype": "file", + "tags": ["rigging", + "average", + "skin weights", + "file"], + "title": "Average Skin Weights" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\rigging\\cbSmoothSkinWeightUI.py", + "sourcetype": "file", + "tags": ["rigging", + "cbSmoothSkinWeightUI", + "file"], + "title": "CB Smooth Skin Weight UI" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\rigging\\channelBoxManagerUI.py", + "sourcetype": "file", + "tags": ["rigging", + "channelBoxManagerUI", + "file"], + "title": "Channel Box Manager UI" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\rigging\\characterAutorigger.py", + "sourcetype": "file", + "tags": ["rigging", + "characterAutorigger", + "file"], + "title": "Character Auto Rigger" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\rigging\\connectUI.py", + "sourcetype": "file", + "tags": ["rigging", + "connectUI", + "file"], + "title": "Connect UI" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\rigging\\copySkinWeightsLocal.py", + "sourcetype": "file", + "tags": ["rigging", + "copySkinWeightsLocal", + "file"], + "title": "Copy Skin Weights Local" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\rigging\\createCenterLocator.py", + "sourcetype": "file", + "tags": ["rigging", + "createCenterLocator", + "file"], + "title": "Create Center Locator" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\rigging\\freezeTransformToGroup.py", + "sourcetype": "file", + "tags": ["rigging", + "freezeTransformToGroup", + "file"], + "title": "Freeze Transform To Group" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\rigging\\groupSelected.py", + "sourcetype": "file", + "tags": ["rigging", + "groupSelected", + "file"], + "title": "Group Selected" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\rigging\\ikHandlePoleVectorLocator.py", + "sourcetype": "file", + "tags": ["rigging", + "ikHandlePoleVectorLocator", + "file"], + "title": "IK Handle Pole Vector Locator" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\rigging\\jointOrientUI.py", + "sourcetype": "file", + "tags": ["rigging", + "jointOrientUI", + "file"], + "title": "Joint Orient UI" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\rigging\\jointsOnCurve.py", + "sourcetype": "file", + "tags": ["rigging", + "jointsOnCurve", + "file"], + "title": "Joints On Curve" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\rigging\\resetBindSelectedSkinJoints.py", + "sourcetype": "file", + "tags": ["rigging", + "resetBindSelectedSkinJoints", + "file"], + "title": "Reset Bind Selected Skin Joints" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\rigging\\selectSkinclusterJointsFromSelectedComponents.py", + "sourcetype": "file", + "tags": ["rigging", + "selectSkinclusterJointsFromSelectedComponents", + "file"], + "title": "Select Skincluster Joints From Selected Components" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\rigging\\selectSkinclusterJointsFromSelectedMesh.py", + "sourcetype": "file", + "tags": ["rigging", + "selectSkinclusterJointsFromSelectedMesh", + "file"], + "title": "Select Skincluster Joints From Selected Mesh" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\rigging\\setJointLabels.py", + "sourcetype": "file", + "tags": ["rigging", + "setJointLabels", + "file"], + "title": "Set Joint Labels" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\rigging\\setJointOrientationFromCurrentRotation.py", + "sourcetype": "file", + "tags": ["rigging", + "setJointOrientationFromCurrentRotation", + "file"], + "title": "Set Joint Orientation From Current Rotation" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\rigging\\setSelectedJointsOrientationZero.py", + "sourcetype": "file", + "tags": ["rigging", + "setSelectedJointsOrientationZero", + "file"], + "title": "Set Selected Joints Orientation Zero" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\rigging\\mirrorCurveShape.py", + "sourcetype": "file", + "tags": ["rigging", + "mirrorCurveShape", + "file"], + "title": "Mirror Curve Shape" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\rigging\\setRotationOrderUI.py", + "sourcetype": "file", + "tags": ["rigging", + "setRotationOrderUI", + "file"], + "title": "Set Rotation Order UI" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\rigging\\paintItNowUI.py", + "sourcetype": "file", + "tags": ["rigging", + "paintItNowUI", + "file"], + "title": "Paint It Now UI" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\rigging\\parentScaleConstraint.py", + "sourcetype": "file", + "tags": ["rigging", + "parentScaleConstraint", + "file"], + "title": "Parent Scale Constraint" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\rigging\\quickSetWeightsUI.py", + "sourcetype": "file", + "tags": ["rigging", + "quickSetWeightsUI", + "file"], + "title": "Quick Set Weights UI" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\rigging\\rapidRig.py", + "sourcetype": "file", + "tags": ["rigging", + "rapidRig", + "file"], + "title": "Rapid Rig" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\rigging\\regenerate_blendshape_targets.py", + "sourcetype": "file", + "tags": ["rigging", + "regenerate_blendshape_targets", + "file"], + "title": "Regenerate Blendshape Targets" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\rigging\\removeRotationAxis.py", + "sourcetype": "file", + "tags": ["rigging", + "removeRotationAxis", + "file"], + "title": "Remove Rotation Axis" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\rigging\\resetBindSelectedMeshes.py", + "sourcetype": "file", + "tags": ["rigging", + "resetBindSelectedMeshes", + "file"], + "title": "Reset Bind Selected Meshes" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\rigging\\simpleControllerOnSelection.py", + "sourcetype": "file", + "tags": ["rigging", + "simpleControllerOnSelection", + "file"], + "title": "Simple Controller On Selection" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\rigging\\simpleControllerOnSelectionHierarchy.py", + "sourcetype": "file", + "tags": ["rigging", + "simpleControllerOnSelectionHierarchy", + "file"], + "title": "Simple Controller On Selection Hierarchy" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\rigging\\superRelativeCluster.py", + "sourcetype": "file", + "tags": ["rigging", + "superRelativeCluster", + "file"], + "title": "Super Relative Cluster" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\rigging\\tfSmoothSkinWeight.py", + "sourcetype": "file", + "tags": ["rigging", + "tfSmoothSkinWeight", + "file"], + "title": "TF Smooth Skin Weight" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\rigging\\toggleIntermediates.py", + "sourcetype": "file", + "tags": ["rigging", + "toggleIntermediates", + "file"], + "title": "Toggle Intermediates" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\rigging\\toggleSegmentScaleCompensate.py", + "sourcetype": "file", + "tags": ["rigging", + "toggleSegmentScaleCompensate", + "file"], + "title": "Toggle Segment Scale Compensate" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\rigging\\toggleSkinclusterDeformNormals.py", + "sourcetype": "file", + "tags": ["rigging", + "toggleSkinclusterDeformNormals", + "file"], + "title": "Toggle Skincluster Deform Normals" + }] +}, +{ + "type": "menu", + "title": "Shading", + "items": [{ + "type": "menu", + "title": "VRay", + "items": [{ + "type": "action", + "title": "Import Proxies", + "command": "$COLORBLEED_SCRIPTS\\shading\\vray\\vrayImportProxies.py", + "sourcetype": "file", + "tags": ["shading", + "vray", + "import", + "proxies"], + "tooltip": "" + }, + { + "type": "separator" + }, + { + "type": "action", + "title": "Select All GES", + "command": "$COLORBLEED_SCRIPTS\\shading\\vray\\selectAllGES.py", + "sourcetype": "file", + "tooltip": "", + "tags": ["shading", + "vray", + "select All GES"] + }, + { + "type": "action", + "title": "Select All GES Under Selection", + "command": "$COLORBLEED_SCRIPTS\\shading\\vray\\selectAllGESUnderSelection.py", + "sourcetype": "file", + "tooltip": "", + "tags": ["shading", + "vray", + "select", + "all", + "GES"] + }, + { + "type": "separator" + }, + { + "type": "action", + "title": "Selection To VRay Mesh", + "command": "$COLORBLEED_SCRIPTS\\shading\\vray\\selectionToVrayMesh.py", + "sourcetype": "file", + "tooltip": "", + "tags": ["shading", + "vray", + "selection", + "vraymesh"] + }, + { + "type": "action", + "title": "Add VRay Round Edges Attribute", + "command": "$COLORBLEED_SCRIPTS\\shading\\vray\\addVrayRoundEdgesAttribute.py", + "sourcetype": "file", + "tooltip": "", + "tags": ["shading", + "vray", + "round edges", + "attribute"] + }, + { + "type": "action", + "title": "Add Gamma", + "command": "$COLORBLEED_SCRIPTS\\shading\\vray\\vrayAddGamma.py", + "sourcetype": "file", + "tooltip": "", + "tags": ["shading", + "vray", + "add gamma"] + }, + { + "type": "separator" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\shading\\vray\\select_vraymesh_materials_with_unconnected_shader_slots.py", + "sourcetype": "file", + "title": "Select Unconnected Shader Materials", + "tags": ["shading", + "vray", + "select", + "vraymesh", + "materials", + "unconnected shader slots"], + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\shading\\vray\\vrayMergeSimilarVRayMeshMaterials.py", + "sourcetype": "file", + "title": "Merge Similar VRay Mesh Materials", + "tags": ["shading", + "vray", + "Merge", + "VRayMesh", + "Materials"], + "tooltip": "" + }, + { + "type": "action", + "title": "Create Two Sided Material", + "command": "$COLORBLEED_SCRIPTS\\shading\\vray\\vrayCreate2SidedMtlForSelectedMtlRenamed.py", + "sourcetype": "file", + "tooltip": "Creates two sided material for selected material and renames it", + "tags": ["shading", + "vray", + "two sided", + "material"] + }, + { + "type": "action", + "title": "Create Two Sided Material For Selected", + "command": "$COLORBLEED_SCRIPTS\\shading\\vray\\vrayCreate2SidedMtlForSelectedMtl.py", + "sourcetype": "file", + "tooltip": "Select material to create a two sided version from it", + "tags": ["shading", + "vray", + "Create2SidedMtlForSelectedMtl.py"] + }, + { + "type": "separator" + }, + { + "type": "action", + "title": "Add OpenSubdiv Attribute", + "command": "$COLORBLEED_SCRIPTS\\shading\\vray\\addVrayOpenSubdivAttribute.py", + "sourcetype": "file", + "tooltip": "", + "tags": ["shading", + "vray", + "add", + "open subdiv", + "attribute"] + }, + { + "type": "action", + "title": "Remove OpenSubdiv Attribute", + "command": "$COLORBLEED_SCRIPTS\\shading\\vray\\removeVrayOpenSubdivAttribute.py", + "sourcetype": "file", + "tooltip": "", + "tags": ["shading", + "vray", + "remove", + "opensubdiv", + "attributee"] + }, + { + "type": "separator" + }, + { + "type": "action", + "title": "Add Subdivision Attribute", + "command": "$COLORBLEED_SCRIPTS\\shading\\vray\\addVraySubdivisionAttribute.py", + "sourcetype": "file", + "tooltip": "", + "tags": ["shading", + "vray", + "addVraySubdivisionAttribute"] + }, + { + "type": "action", + "title": "Remove Subdivision Attribute.py", + "command": "$COLORBLEED_SCRIPTS\\shading\\vray\\removeVraySubdivisionAttribute.py", + "sourcetype": "file", + "tooltip": "", + "tags": ["shading", + "vray", + "remove", + "subdivision", + "attribute"] + }, + { + "type": "separator" + }, + { + "type": "action", + "title": "Add Vray Object Ids", + "command": "$COLORBLEED_SCRIPTS\\shading\\vray\\addVrayObjectIds.py", + "sourcetype": "file", + "tooltip": "", + "tags": ["shading", + "vray", + "add", + "object id"] + }, + { + "type": "action", + "title": "Add Vray Material Ids", + "command": "$COLORBLEED_SCRIPTS\\shading\\vray\\addVrayMaterialIds.py", + "sourcetype": "file", + "tooltip": "", + "tags": ["shading", + "vray", + "addVrayMaterialIds.py"] + }, + { + "type": "separator" + }, + { + "type": "action", + "title": "Set Physical DOF Depth", + "command": "$COLORBLEED_SCRIPTS\\shading\\vray\\vrayPhysicalDOFSetDepth.py", + "sourcetype": "file", + "tooltip": "", + "tags": ["shading", + "vray", + "physical", + "DOF ", + "Depth"] + }, + { + "type": "action", + "title": "Magic Vray Proxy UI", + "command": "$COLORBLEED_SCRIPTS\\shading\\vray\\magicVrayProxyUI.py", + "sourcetype": "file", + "tooltip": "", + "tags": ["shading", + "vray", + "magicVrayProxyUI"] + }] + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\pyblish\\lighting\\set_filename_prefix.py", + "sourcetype": "file", + "tags": ["shading", + "lookdev", + "assign", + "shaders", + "prefix", + "filename", + "render"], + "title": "Set filename prefix", + "tooltip": "Set the render file name prefix." + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\shading\\look_manager_ui.py", + "sourcetype": "file", + "tags": ["shading", + "look", + "assign", + "shaders", + "auto"], + "title": "Look Manager", + "tooltip": "Open the Look Manager UI for look assignment" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\shading\\LightLinkUi.py", + "sourcetype": "file", + "tags": ["shading", + "light", + "link", + "ui"], + "title": "Light Link UI", + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\shading\\vdviewer_ui.py", + "sourcetype": "file", + "tags": ["shading", + "look", + "vray", + "displacement", + "shaders", + "auto"], + "title": "VRay Displ Viewer", + "tooltip": "Open the VRay Displacement Viewer, select and control the content of the set" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\shading\\setTexturePreviewToCLRImage.py", + "sourcetype": "file", + "tags": ["shading", + "CLRImage", + "textures", + "preview"], + "title": "Set Texture Preview To CLRImage", + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\shading\\fixDefaultShaderSetBehavior.py", + "sourcetype": "file", + "tags": ["shading", + "fix", + "DefaultShaderSet", + "Behavior"], + "title": "Fix Default Shader Set Behavior", + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\shading\\fixSelectedShapesReferenceAssignments.py", + "sourcetype": "file", + "tags": ["shading", + "fix", + "Selected", + "Shapes", + "Reference", + "Assignments"], + "title": "Fix Shapes Reference Assignments", + "tooltip": "Select shapes to fix the reference assignments" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\shading\\selectLambert1Members.py", + "sourcetype": "file", + "tags": ["shading", + "selectLambert1Members"], + "title": "Select Lambert1 Members", + "tooltip": "Selects all objects which have the Lambert1 shader assigned" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\shading\\selectShapesWithoutShader.py", + "sourcetype": "file", + "tags": ["shading", + "selectShapesWithoutShader"], + "title": "Select Shapes Without Shader", + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\shading\\fixRenderLayerOutAdjustmentErrors.py", + "sourcetype": "file", + "tags": ["shading", + "fixRenderLayerOutAdjustmentErrors"], + "title": "Fix RenderLayer Out Adjustment Errors", + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\shading\\fix_renderlayer_missing_node_override.py", + "sourcetype": "file", + "tags": ["shading", + "renderlayer", + "missing", + "reference", + "switch", + "layer"], + "title": "Fix RenderLayer Missing Referenced Nodes Overrides", + "tooltip": "" + }, + { + "type": "action", + "title": "Image 2 Tiled EXR", + "command": "$COLORBLEED_SCRIPTS\\shading\\open_img2exr.py", + "sourcetype": "file", + "tooltip": "", + "tags": ["shading", + "vray", + "exr"] + }] +}, +{ + "type": "menu", + "title": "Rendering", + "items": [{ + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\pyblish\\open_deadline_submission_settings.py", + "sourcetype": "file", + "tags": ["settings", + "deadline", + "globals", + "render"], + "title": "DL Submission Settings UI", + "tooltip": "Open the Deadline Submission Settings UI" + }] +}, +{ + "type": "menu", + "title": "Animation", + "items": [{ + "type": "menu", + "title": "Attributes", + "tooltip": "", + "items": [{ + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\animation\\attributes\\copyValues.py", + "sourcetype": "file", + "tags": ["animation", + "copy", + "attributes"], + "title": "Copy Values", + "tooltip": "Copy attribute values" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\animation\\attributes\\copyInConnections.py", + "sourcetype": "file", + "tags": ["animation", + "copy", + "attributes", + "connections", + "incoming"], + "title": "Copy In Connections", + "tooltip": "Copy incoming connections" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\animation\\attributes\\copyOutConnections.py", + "sourcetype": "file", + "tags": ["animation", + "copy", + "attributes", + "connections", + "out"], + "title": "Copy Out Connections", + "tooltip": "Copy outcoming connections" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\animation\\attributes\\copyTransformLocal.py", + "sourcetype": "file", + "tags": ["animation", + "copy", + "attributes", + "transforms", + "local"], + "title": "Copy Local Transfroms", + "tooltip": "Copy local transfroms" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\animation\\attributes\\copyTransformMatrix.py", + "sourcetype": "file", + "tags": ["animation", + "copy", + "attributes", + "transforms", + "matrix"], + "title": "Copy Matrix Transfroms", + "tooltip": "Copy Matrix transfroms" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\animation\\attributes\\copyTransformUI.py", + "sourcetype": "file", + "tags": ["animation", + "copy", + "attributes", + "transforms", + "UI"], + "title": "Copy Transforms UI", + "tooltip": "Open the Copy Transforms UI" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\animation\\attributes\\simpleCopyUI.py", + "sourcetype": "file", + "tags": ["animation", + "copy", + "attributes", + "transforms", + "UI", + "simple"], + "title": "Simple Copy UI", + "tooltip": "Open the simple Copy Transforms UI" + }] + }, + { + "type": "menu", + "title": "Optimize", + "tooltip": "Optimization scripts", + "items": [{ + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\animation\\optimize\\toggleFreezeHierarchy.py", + "sourcetype": "file", + "tags": ["animation", + "hierarchy", + "toggle", + "freeze"], + "title": "Toggle Freeze Hierarchy", + "tooltip": "Freeze and unfreeze hierarchy" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\animation\\optimize\\toggleParallelNucleus.py", + "sourcetype": "file", + "tags": ["animation", + "nucleus", + "toggle", + "parallel"], + "title": "Toggle Parallel Nucleus", + "tooltip": "Toggle parallel nucleus" + }] + }, + { + "sourcetype": "file", + "command": "$COLORBLEED_SCRIPTS\\animation\\bakeSelectedToWorldSpace.py", + "tags": ["animation", + "bake", + "selection", + "worldspace.py"], + "title": "Bake Selected To Worldspace", + "type": "action" + }, + { + "sourcetype": "file", + "command": "$COLORBLEED_SCRIPTS\\animation\\timeStepper.py", + "tags": ["animation", + "time", + "stepper"], + "title": "Time Stepper", + "type": "action" + }, + { + "sourcetype": "file", + "command": "$COLORBLEED_SCRIPTS\\animation\\capture_ui.py", + "tags": ["animation", + "capture", + "ui", + "screen", + "movie", + "image"], + "title": "Capture UI", + "type": "action" + }, + { + "sourcetype": "file", + "command": "$COLORBLEED_SCRIPTS\\animation\\simplePlayblastUI.py", + "tags": ["animation", + "simple", + "playblast", + "ui"], + "title": "Simple Playblast UI", + "type": "action" + }, + { + "sourcetype": "file", + "command": "$COLORBLEED_SCRIPTS\\animation\\tweenMachineUI.py", + "tags": ["animation", + "tween", + "machine"], + "title": "Tween Machine UI", + "type": "action" + }, + { + "sourcetype": "file", + "command": "$COLORBLEED_SCRIPTS\\animation\\selectAllAnimationCurves.py", + "tags": ["animation", + "select", + "curves"], + "title": "Select All Animation Curves", + "type": "action" + }, + { + "sourcetype": "file", + "command": "$COLORBLEED_SCRIPTS\\animation\\pathAnimation.py", + "tags": ["animation", + "path", + "along"], + "title": "Path Animation", + "type": "action" + }, + { + "sourcetype": "file", + "command": "$COLORBLEED_SCRIPTS\\animation\\offsetSelectedObjectsUI.py", + "tags": ["animation", + "offsetSelectedObjectsUI.py"], + "title": "Offset Selected Objects UI", + "type": "action" + }, + { + "sourcetype": "file", + "command": "$COLORBLEED_SCRIPTS\\animation\\key_amplifier_ui.py", + "tags": ["animation", + "key", + "amplifier"], + "title": "Key Amplifier UI", + "type": "action" + }, + { + "sourcetype": "file", + "command": "$COLORBLEED_SCRIPTS\\animation\\anim_scene_optimizer.py", + "tags": ["animation", + "anim_scene_optimizer.py"], + "title": "Anim_Scene_Optimizer", + "type": "action" + }, + { + "sourcetype": "file", + "command": "$COLORBLEED_SCRIPTS\\animation\\zvParentMaster.py", + "tags": ["animation", + "zvParentMaster.py"], + "title": "ZV Parent Master", + "type": "action" + }, + { + "sourcetype": "file", + "command": "$COLORBLEED_SCRIPTS\\animation\\poseLibrary.py", + "tags": ["animation", + "poseLibrary.py"], + "title": "Pose Library", + "type": "action" + }] +}, +{ + "type": "menu", + "title": "Layout", + "items": [{ + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\layout\\alignDistributeUI.py", + "sourcetype": "file", + "tags": ["layout", + "align", + "Distribute", + "UI"], + "title": "Align Distribute UI", + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\layout\\alignSimpleUI.py", + "sourcetype": "file", + "tags": ["layout", + "align", + "UI", + "Simple"], + "title": "Align Simple UI", + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\layout\\center_locator.py", + "sourcetype": "file", + "tags": ["layout", + "center", + "locator"], + "title": "Center Locator", + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\layout\\average_locator.py", + "sourcetype": "file", + "tags": ["layout", + "average", + "locator"], + "title": "Average Locator", + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\layout\\selectWithinProximityUI.py", + "sourcetype": "file", + "tags": ["layout", + "select", + "proximity", + "ui"], + "title": "Select Within Proximity UI", + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\layout\\dupCurveUI.py", + "sourcetype": "file", + "tags": ["layout", + "Duplicate", + "Curve", + "UI"], + "title": "Duplicate Curve UI", + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\layout\\randomDeselectUI.py", + "sourcetype": "file", + "tags": ["layout", + "random", + "Deselect", + "UI"], + "title": "Random Deselect UI", + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\layout\\multiReferencerUI.py", + "sourcetype": "file", + "tags": ["layout", + "multi", + "reference"], + "title": "Multi Referencer UI", + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\layout\\duplicateOffsetUI.py", + "sourcetype": "file", + "tags": ["layout", + "duplicate", + "offset", + "UI"], + "title": "Duplicate Offset UI", + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\layout\\spPaint3d.py", + "sourcetype": "file", + "tags": ["layout", + "spPaint3d", + "paint", + "tool"], + "title": "SP Paint 3d", + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\layout\\randomizeUI.py", + "sourcetype": "file", + "tags": ["layout", + "randomize", + "UI"], + "title": "Randomize UI", + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\layout\\distributeWithinObjectUI.py", + "sourcetype": "file", + "tags": ["layout", + "distribute", + "ObjectUI", + "within"], + "title": "Distribute Within Object UI", + "tooltip": "" + }] +}, +{ + "type": "menu", + "title": "Particles", + "items": [{ + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\particles\\instancerToObjects.py", + "sourcetype": "file", + "tags": ["particles", + "instancerToObjects"], + "title": "Instancer To Objects", + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\particles\\instancerToObjectsInstances.py", + "sourcetype": "file", + "tags": ["particles", + "instancerToObjectsInstances"], + "title": "Instancer To Objects Instances", + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\particles\\objectsToParticlesAndInstancerCleanSource.py", + "sourcetype": "file", + "tags": ["particles", + "objects", + "Particles", + "Instancer", + "Clean", + "Source"], + "title": "Objects To Particles & Instancer - Clean Source", + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\particles\\particleComponentsToLocators.py", + "sourcetype": "file", + "tags": ["particles", + "components", + "locators"], + "title": "Particle Components To Locators", + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\particles\\objectsToParticlesAndInstancer.py", + "sourcetype": "file", + "tags": ["particles", + "objects", + "particles", + "instancer"], + "title": "Objects To Particles And Instancer", + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\particles\\spawnParticlesOnMesh.py", + "sourcetype": "file", + "tags": ["particles", + "spawn", + "on", + "mesh"], + "title": "Spawn Particles On Mesh", + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\particles\\instancerToObjectsInstancesWithAnimation.py", + "sourcetype": "file", + "tags": ["particles", + "instancerToObjectsInstancesWithAnimation"], + "title": "Instancer To Objects Instances With Animation", + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\particles\\objectsToParticles.py", + "sourcetype": "file", + "tags": ["particles", + "objectsToParticles"], + "title": "Objects To Particles", + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\particles\\add_particle_cacheFile_attrs.py", + "sourcetype": "file", + "tags": ["particles", + "add_particle_cacheFile_attrs"], + "title": "Add Particle CacheFile Attributes", + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\particles\\mergeParticleSystems.py", + "sourcetype": "file", + "tags": ["particles", + "mergeParticleSystems"], + "title": "Merge Particle Systems", + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\particles\\particlesToLocators.py", + "sourcetype": "file", + "tags": ["particles", + "particlesToLocators"], + "title": "Particles To Locators", + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\particles\\instancerToObjectsWithAnimation.py", + "sourcetype": "file", + "tags": ["particles", + "instancerToObjectsWithAnimation"], + "title": "Instancer To Objects With Animation", + "tooltip": "" + }, + { + "type": "separator" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\particles\\mayaReplicateHoudiniTool.py", + "sourcetype": "file", + "tags": ["particles", + "houdini", + "houdiniTool", + "houdiniEngine"], + "title": "Replicate Houdini Tool", + "tooltip": "" + }, + { + "type": "separator" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\particles\\clearInitialState.py", + "sourcetype": "file", + "tags": ["particles", + "clearInitialState"], + "title": "Clear Initial State", + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\particles\\killSelectedParticles.py", + "sourcetype": "file", + "tags": ["particles", + "killSelectedParticles"], + "title": "Kill Selected Particles", + "tooltip": "" + }] +}, +{ + "type": "menu", + "title": "Yeti", + "items": [{ + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\yeti\\yeti_rig_manager.py", + "sourcetype": "file", + "tags": ["yeti", + "rig", + "fur", + "manager"], + "title": "Open Yeti Rig Manager", + "tooltip": "" + }] +}, +{ + "type": "menu", + "title": "Cleanup", + "items": [{ + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\cleanup\\repair_faulty_containers.py", + "sourcetype": "file", + "tags": ["cleanup", + "repair", + "containers"], + "title": "Find and Repair Containers", + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\cleanup\\selectByType.py", + "sourcetype": "file", + "tags": ["cleanup", + "selectByType"], + "title": "Select By Type", + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\cleanup\\selectIntermediateObjects.py", + "sourcetype": "file", + "tags": ["cleanup", + "selectIntermediateObjects"], + "title": "Select Intermediate Objects", + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\cleanup\\selectNonUniqueNames.py", + "sourcetype": "file", + "tags": ["cleanup", + "select", + "non unique", + "names"], + "title": "Select Non Unique Names", + "tooltip": "" + }, + { + "type": "separator" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\cleanup\\removeNamespaces.py", + "sourcetype": "file", + "tags": ["cleanup", + "remove", + "namespaces"], + "title": "Remove Namespaces", + "tooltip": "Remove all namespaces" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\cleanup\\remove_user_defined_attributes.py", + "sourcetype": "file", + "tags": ["cleanup", + "remove_user_defined_attributes"], + "title": "Remove User Defined Attributes", + "tooltip": "Remove all user-defined attributs from all nodes" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\cleanup\\removeUnknownNodes.py", + "sourcetype": "file", + "tags": ["cleanup", + "removeUnknownNodes"], + "title": "Remove Unknown Nodes", + "tooltip": "Remove all unknown nodes" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\cleanup\\removeUnloadedReferences.py", + "sourcetype": "file", + "tags": ["cleanup", + "removeUnloadedReferences"], + "title": "Remove Unloaded References", + "tooltip": "Remove all unloaded references" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\cleanup\\removeReferencesFailedEdits.py", + "sourcetype": "file", + "tags": ["cleanup", + "removeReferencesFailedEdits"], + "title": "Remove References Failed Edits", + "tooltip": "Remove failed edits for all references" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\cleanup\\remove_unused_looks.py", + "sourcetype": "file", + "tags": ["cleanup", + "removeUnusedLooks"], + "title": "Remove Unused Looks", + "tooltip": "Remove all loaded yet unused Avalon look containers" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\cleanup\\deleteGhostIntermediateObjects.py", + "sourcetype": "file", + "tags": ["cleanup", + "deleteGhostIntermediateObjects"], + "title": "Delete Ghost Intermediate Objects", + "tooltip": "" + }, + { + "type": "separator" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\cleanup\\resetViewportCache.py", + "sourcetype": "file", + "tags": ["cleanup", + "reset", + "viewport", + "cache"], + "title": "Reset Viewport Cache", + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\cleanup\\uniqifyNodeNames.py", + "sourcetype": "file", + "tags": ["cleanup", + "uniqifyNodeNames"], + "title": "Uniqify Node Names", + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\cleanup\\autoRenameFileNodes.py", + "sourcetype": "file", + "tags": ["cleanup", + "auto", + "rename", + "filenodes"], + "title": "Auto Rename File Nodes", + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\cleanup\\update_asset_id.py", + "sourcetype": "file", + "tags": ["cleanup", + "update", + "database", + "asset", + "id"], + "title": "Update Asset ID", + "tooltip": "Will replace the Colorbleed ID with a new one (asset ID : Unique number)" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\cleanup\\colorbleedRename.py", + "sourcetype": "file", + "tags": ["cleanup", + "rename", + "ui"], + "title": "Colorbleed Renamer", + "tooltip": "Colorbleed Rename UI" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\cleanup\\renameShapesToTransform.py", + "sourcetype": "file", + "tags": ["cleanup", + "renameShapesToTransform"], + "title": "Rename Shapes To Transform", + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\cleanup\\reorderUI.py", + "sourcetype": "file", + "tags": ["cleanup", + "reorderUI"], + "title": "Reorder UI", + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\cleanup\\pastedCleaner.py", + "sourcetype": "file", + "tags": ["cleanup", + "pastedCleaner"], + "title": "Pasted Cleaner", + "tooltip": "" + }] +}, +{ + "type": "menu", + "title": "Others", + "items": [{ + "type": "menu", + "sourcetype": "file", + "title": "Yeti", + "items": [{ + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\others\\yeti\\cache_selected_yeti_nodes.py", + "sourcetype": "file", + "tags": ["others", + "yeti", + "cache", + "selected"], + "title": "Cache Selected Yeti Nodes", + "tooltip": "" + }] + }, + { + "type": "menu", + "title": "Hair", + "tooltip": "", + "items": [{ + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\others\\hair\\recolorHairCurrentCurve", + "sourcetype": "file", + "tags": ["others", + "selectSoftSelection"], + "title": "Select Soft Selection", + "tooltip": "" + }] + }, + { + "type": "menu", + "command": "$COLORBLEED_SCRIPTS\\others\\display", + "sourcetype": "file", + "tags": ["others", + "display"], + "title": "Display", + "items": [{ + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\others\\display\\wireframeSelectedObjects.py", + "sourcetype": "file", + "tags": ["others", + "wireframe", + "selected", + "objects"], + "title": "Wireframe Selected Objects", + "tooltip": "" + }] + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\others\\archiveSceneUI.py", + "sourcetype": "file", + "tags": ["others", + "archiveSceneUI"], + "title": "Archive Scene UI", + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\others\\getSimilarMeshes.py", + "sourcetype": "file", + "tags": ["others", + "getSimilarMeshes"], + "title": "Get Similar Meshes", + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\others\\createBoundingBoxEachSelected.py", + "sourcetype": "file", + "tags": ["others", + "createBoundingBoxEachSelected"], + "title": "Create BoundingBox Each Selected", + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\others\\curveFromPositionEveryFrame.py", + "sourcetype": "file", + "tags": ["others", + "curveFromPositionEveryFrame"], + "title": "Curve From Position", + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\others\\instanceLeafSmartTransform.py", + "sourcetype": "file", + "tags": ["others", + "instance", + "leaf", + "smart", + "transform"], + "title": "Instance Leaf Smart Transform", + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\others\\instanceSmartTransform.py", + "sourcetype": "file", + "tags": ["others", + "instance", + "smart", + "transform"], + "title": "Instance Smart Transform", + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\others\\randomizeUVShellsSelectedObjects.py", + "sourcetype": "file", + "tags": ["others", + "randomizeUVShellsSelectedObjects"], + "title": "Randomize UV Shells", + "tooltip": "Select objects before running action" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\others\\centerPivotGroup.py", + "sourcetype": "file", + "tags": ["others", + "centerPivotGroup"], + "title": "Center Pivot Group", + "tooltip": "" + }, + { + "type": "separator" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\others\\locatorsOnSelectedFaces.py", + "sourcetype": "file", + "tags": ["others", + "locatorsOnSelectedFaces"], + "title": "Locators On Selected Faces", + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\others\\locatorsOnEdgeSelectionPrompt.py", + "sourcetype": "file", + "tags": ["others", + "locatorsOnEdgeSelectionPrompt"], + "title": "Locators On Edge Selection Prompt", + "tooltip": "" + }, + { + "type": "separator" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\others\\copyDeformers.py", + "sourcetype": "file", + "tags": ["others", + "copyDeformers"], + "title": "Copy Deformers", + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\others\\selectInReferenceEditor.py", + "sourcetype": "file", + "tags": ["others", + "selectInReferenceEditor"], + "title": "Select In Reference Editor", + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\others\\selectConstrainingObject.py", + "sourcetype": "file", + "tags": ["others", + "selectConstrainingObject"], + "title": "Select Constraining Object", + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\others\\deformerSetRelationsUI.py", + "sourcetype": "file", + "tags": ["others", + "deformerSetRelationsUI"], + "title": "Deformer Set Relations UI", + "tooltip": "" + }, + { + "type": "action", + "command": "$COLORBLEED_SCRIPTS\\others\\recreateBaseNodesForAllLatticeNodes.py", + "sourcetype": "file", + "tags": ["others", + "recreate", + "base", + "nodes", + "lattice"], + "title": "Recreate Base Nodes For Lattice Nodes", + "tooltip": "" + }] +}] \ No newline at end of file diff --git a/pype/maya/menu.py b/pype/maya/menu.py index 97a8799570..ed5115c274 100644 --- a/pype/maya/menu.py +++ b/pype/maya/menu.py @@ -23,10 +23,15 @@ def _get_menu(): def deferred(): - import scriptsmenu.launchformaya as launchformaya - import scriptsmenu.scriptsmenu as scriptsmenu + log.info("Attempting to install scripts menu..") - log.info("Attempting to install ...") + try: + import scriptsmenu.launchformaya as launchformaya + import scriptsmenu.scriptsmenu as scriptsmenu + except ImportError: + log.warning("Skipping pype.menu install, because " + "'scriptsmenu' module seems unavailable.") + return # load configuration of custom menu config_path = os.path.join(os.path.dirname(__file__), "menu.json") @@ -44,7 +49,7 @@ def uninstall(): menu = _get_menu() if menu: - log.info("Attempting to uninstall ..") + log.info("Attempting to uninstall..") try: menu.deleteLater() @@ -56,7 +61,7 @@ def uninstall(): def install(): if cmds.about(batch=True): - print("Skipping studio.menu initialization in batch mode..") + print("Skipping pype.menu initialization in batch mode..") return uninstall() diff --git a/pype/maya/plugin.py b/pype/maya/plugin.py index 21a074e874..327cf47cbd 100644 --- a/pype/maya/plugin.py +++ b/pype/maya/plugin.py @@ -95,6 +95,10 @@ class ReferenceLoader(api.Loader): if ref.rsplit(":", 1)[-1].startswith("sharedReferenceNode"): continue + # Ignore _UNKNOWN_REF_NODE_ (PLN-160) + if ref.rsplit(":", 1)[-1].startswith("_UNKNOWN_REF_NODE_"): + continue + references.add(ref) assert references, "No reference node found in container" diff --git a/pype/plugins/fusion/create/create_tiff_saver.py b/pype/plugins/fusion/create/create_tiff_saver.py index 4911650ed2..4baddfc36e 100644 --- a/pype/plugins/fusion/create/create_tiff_saver.py +++ b/pype/plugins/fusion/create/create_tiff_saver.py @@ -9,7 +9,7 @@ class CreateTiffSaver(avalon.api.Creator): name = "tiffDefault" label = "Create Tiff Saver" hosts = ["fusion"] - family = "saver" + family = "studio.saver" def process(self): diff --git a/pype/plugins/fusion/load/actions.py b/pype/plugins/fusion/load/actions.py index d7ee13716b..9c83f6ba79 100644 --- a/pype/plugins/fusion/load/actions.py +++ b/pype/plugins/fusion/load/actions.py @@ -8,11 +8,11 @@ from avalon import api class FusionSetFrameRangeLoader(api.Loader): """Specific loader of Alembic for the avalon.animation family""" - families = ["animation", - "camera", - "imagesequence", - "yeticache", - "pointcache"] + families = ["studio.animation", + "studio.camera", + "studio.imagesequence", + "studio.yeticache", + "studio.pointcache"] representations = ["*"] label = "Set frame range" @@ -41,11 +41,11 @@ class FusionSetFrameRangeLoader(api.Loader): class FusionSetFrameRangeWithHandlesLoader(api.Loader): """Specific loader of Alembic for the avalon.animation family""" - families = ["animation", - "camera", - "imagesequence", - "yeticache", - "pointcache"] + families = ["studio.animation", + "studio.camera", + "studio.imagesequence", + "studio.yeticache", + "studio.pointcache"] representations = ["*"] label = "Set frame range (with handles)" diff --git a/pype/plugins/fusion/load/load_sequence.py b/pype/plugins/fusion/load/load_sequence.py index 7b859e9b03..2150bd1eb0 100644 --- a/pype/plugins/fusion/load/load_sequence.py +++ b/pype/plugins/fusion/load/load_sequence.py @@ -113,7 +113,7 @@ def loader_shift(loader, frame, relative=True): class FusionLoadSequence(api.Loader): """Load image sequence into Fusion""" - families = ["imagesequence"] + families = ["studio.imagesequence"] representations = ["*"] label = "Load sequence" diff --git a/pype/plugins/fusion/publish/collect_instances.py b/pype/plugins/fusion/publish/collect_instances.py index 472e5d4741..9177f603c3 100644 --- a/pype/plugins/fusion/publish/collect_instances.py +++ b/pype/plugins/fusion/publish/collect_instances.py @@ -76,8 +76,8 @@ class CollectInstances(pyblish.api.ContextPlugin): "outputDir": os.path.dirname(path), "ext": ext, # todo: should be redundant "label": label, - "families": ["saver"], - "family": "saver", + "families": ["studio.saver"], + "family": "studio.saver", "active": active, "publish": active # backwards compatibility }) diff --git a/pype/plugins/fusion/publish/collect_render_target.py b/pype/plugins/fusion/publish/collect_render_target.py index df0a3bbe75..0abdd7113f 100644 --- a/pype/plugins/fusion/publish/collect_render_target.py +++ b/pype/plugins/fusion/publish/collect_render_target.py @@ -13,7 +13,7 @@ class CollectFusionRenderMode(pyblish.api.InstancePlugin): available tool does not visualize which render mode is set for the current comp, please run the following line in the console (Py2) - comp.GetData("rendermode") + comp.GetData("studio.rendermode") This will return the name of the current render mode as seen above under Options. @@ -23,7 +23,7 @@ class CollectFusionRenderMode(pyblish.api.InstancePlugin): order = pyblish.api.CollectorOrder + 0.4 label = "Collect Render Mode" hosts = ["fusion"] - families = ["saver"] + families = ["studio.saver"] def process(self, instance): """Collect all image sequence tools""" @@ -34,11 +34,11 @@ class CollectFusionRenderMode(pyblish.api.InstancePlugin): raise RuntimeError("No comp previously collected, unable to " "retrieve Fusion version.") - rendermode = comp.GetData("rendermode") or "renderlocal" + rendermode = comp.GetData("studio.rendermode") or "renderlocal" assert rendermode in options, "Must be supported render mode" self.log.info("Render mode: {0}".format(rendermode)) # Append family - family = "saver.{0}".format(rendermode) + family = "studio.saver.{0}".format(rendermode) instance.data["families"].append(family) diff --git a/pype/plugins/fusion/publish/increment_current_file_deadline.py b/pype/plugins/fusion/publish/increment_current_file_deadline.py index 6545d84da3..f78cd34eb1 100644 --- a/pype/plugins/fusion/publish/increment_current_file_deadline.py +++ b/pype/plugins/fusion/publish/increment_current_file_deadline.py @@ -11,7 +11,7 @@ class FusionIncrementCurrentFile(pyblish.api.ContextPlugin): label = "Increment current file" order = pyblish.api.IntegratorOrder + 9.0 hosts = ["fusion"] - families = ["saver.deadline"] + families = ["studio.saver.deadline"] optional = True def process(self, context): diff --git a/pype/plugins/fusion/publish/publish_image_sequences.py b/pype/plugins/fusion/publish/publish_image_sequences.py index 26ae74676f..3078a2af62 100644 --- a/pype/plugins/fusion/publish/publish_image_sequences.py +++ b/pype/plugins/fusion/publish/publish_image_sequences.py @@ -32,7 +32,7 @@ class PublishImageSequence(pyblish.api.InstancePlugin): order = pyblish.api.IntegratorOrder label = "Publish Rendered Image Sequence(s)" hosts = ["fusion"] - families = ["saver.renderlocal"] + families = ["studio.saver.renderlocal"] def process(self, instance): @@ -55,7 +55,7 @@ class PublishImageSequence(pyblish.api.InstancePlugin): "regex": regex, "startFrame": instance.context.data["startFrame"], "endFrame": instance.context.data["endFrame"], - "families": ["imagesequence"], + "families": ["studio.imagesequence"], } # Write metadata and store the path in the instance diff --git a/pype/plugins/fusion/publish/render_local.py b/pype/plugins/fusion/publish/render_local.py index c97fe1a13d..d34f7d56ee 100644 --- a/pype/plugins/fusion/publish/render_local.py +++ b/pype/plugins/fusion/publish/render_local.py @@ -14,7 +14,7 @@ class FusionRenderLocal(pyblish.api.InstancePlugin): order = pyblish.api.ExtractorOrder label = "Render Local" hosts = ["fusion"] - families = ["saver.renderlocal"] + families = ["studio.saver.renderlocal"] def process(self, instance): diff --git a/pype/plugins/fusion/publish/save_scene.py b/pype/plugins/fusion/publish/save_scene.py index 850ac5c372..ffc44b98d5 100644 --- a/pype/plugins/fusion/publish/save_scene.py +++ b/pype/plugins/fusion/publish/save_scene.py @@ -7,7 +7,7 @@ class FusionSaveComp(pyblish.api.ContextPlugin): label = "Save current file" order = pyblish.api.ExtractorOrder - 0.49 hosts = ["fusion"] - families = ["saver"] + families = ["studio.saver"] def process(self, context): diff --git a/pype/plugins/fusion/publish/submit_deadline.py b/pype/plugins/fusion/publish/submit_deadline.py index 6e1f405afd..cce9b1292c 100644 --- a/pype/plugins/fusion/publish/submit_deadline.py +++ b/pype/plugins/fusion/publish/submit_deadline.py @@ -19,7 +19,7 @@ class FusionSubmitDeadline(pyblish.api.InstancePlugin): label = "Submit to Deadline" order = pyblish.api.IntegratorOrder hosts = ["fusion"] - families = ["saver.deadline"] + families = ["studio.saver.deadline"] def process(self, instance): diff --git a/pype/plugins/fusion/publish/validate_background_depth.py b/pype/plugins/fusion/publish/validate_background_depth.py index c339d0137f..87d489513d 100644 --- a/pype/plugins/fusion/publish/validate_background_depth.py +++ b/pype/plugins/fusion/publish/validate_background_depth.py @@ -1,6 +1,6 @@ import pyblish.api -from config import action +from pype import action class ValidateBackgroundDepth(pyblish.api.InstancePlugin): @@ -10,7 +10,7 @@ class ValidateBackgroundDepth(pyblish.api.InstancePlugin): label = "Validate Background Depth 32 bit" actions = [action.RepairAction] hosts = ["fusion"] - families = ["saver"] + families = ["studio.saver"] optional = True @classmethod diff --git a/pype/plugins/fusion/publish/validate_comp_saved.py b/pype/plugins/fusion/publish/validate_comp_saved.py index 425168fbdf..6c94d730ce 100644 --- a/pype/plugins/fusion/publish/validate_comp_saved.py +++ b/pype/plugins/fusion/publish/validate_comp_saved.py @@ -8,7 +8,7 @@ class ValidateFusionCompSaved(pyblish.api.ContextPlugin): order = pyblish.api.ValidatorOrder label = "Validate Comp Saved" - families = ["saver"] + families = ["studio.saver"] hosts = ["fusion"] def process(self, context): diff --git a/pype/plugins/fusion/publish/validate_create_folder_checked.py b/pype/plugins/fusion/publish/validate_create_folder_checked.py index d48875143d..1c6b14b48c 100644 --- a/pype/plugins/fusion/publish/validate_create_folder_checked.py +++ b/pype/plugins/fusion/publish/validate_create_folder_checked.py @@ -1,6 +1,6 @@ import pyblish.api -from config import action +from pype import action class ValidateCreateFolderChecked(pyblish.api.InstancePlugin): @@ -13,7 +13,7 @@ class ValidateCreateFolderChecked(pyblish.api.InstancePlugin): order = pyblish.api.ValidatorOrder actions = [action.RepairAction] label = "Validate Create Folder Checked" - families = ["saver"] + families = ["studio.saver"] hosts = ["fusion"] @classmethod diff --git a/pype/plugins/fusion/publish/validate_filename_has_extension.py b/pype/plugins/fusion/publish/validate_filename_has_extension.py index d3762ad290..40f167765d 100644 --- a/pype/plugins/fusion/publish/validate_filename_has_extension.py +++ b/pype/plugins/fusion/publish/validate_filename_has_extension.py @@ -14,7 +14,7 @@ class ValidateFilenameHasExtension(pyblish.api.InstancePlugin): order = pyblish.api.ValidatorOrder label = "Validate Filename Has Extension" - families = ["saver"] + families = ["studio.saver"] hosts = ["fusion"] def process(self, instance): diff --git a/pype/plugins/fusion/publish/validate_saver_has_input.py b/pype/plugins/fusion/publish/validate_saver_has_input.py index 6887a9704c..9e94f101a0 100644 --- a/pype/plugins/fusion/publish/validate_saver_has_input.py +++ b/pype/plugins/fusion/publish/validate_saver_has_input.py @@ -10,7 +10,7 @@ class ValidateSaverHasInput(pyblish.api.InstancePlugin): order = pyblish.api.ValidatorOrder label = "Validate Saver Has Input" - families = ["saver"] + families = ["studio.saver"] hosts = ["fusion"] @classmethod diff --git a/pype/plugins/fusion/publish/validate_saver_passthrough.py b/pype/plugins/fusion/publish/validate_saver_passthrough.py index 2da5cf2494..c69873c04d 100644 --- a/pype/plugins/fusion/publish/validate_saver_passthrough.py +++ b/pype/plugins/fusion/publish/validate_saver_passthrough.py @@ -6,7 +6,7 @@ class ValidateSaverPassthrough(pyblish.api.ContextPlugin): order = pyblish.api.ValidatorOrder label = "Validate Saver Passthrough" - families = ["saver"] + families = ["studio.saver"] hosts = ["fusion"] def process(self, context): diff --git a/pype/plugins/fusion/publish/validate_unique_subsets.py b/pype/plugins/fusion/publish/validate_unique_subsets.py index 2000e1c05d..4fef26c9e9 100644 --- a/pype/plugins/fusion/publish/validate_unique_subsets.py +++ b/pype/plugins/fusion/publish/validate_unique_subsets.py @@ -6,7 +6,7 @@ class ValidateUniqueSubsets(pyblish.api.InstancePlugin): order = pyblish.api.ValidatorOrder label = "Validate Unique Subsets" - families = ["saver"] + families = ["studio.saver"] hosts = ["fusion"] @classmethod diff --git a/pype/plugins/global/load/open_imagesequence.py b/pype/plugins/global/load/open_imagesequence.py index 8cb16fc507..350ae2a6b1 100644 --- a/pype/plugins/global/load/open_imagesequence.py +++ b/pype/plugins/global/load/open_imagesequence.py @@ -18,7 +18,7 @@ def open(filepath): class PlayImageSequence(api.Loader): """Open Image Sequence with system default""" - families = ["imagesequence"] + families = ["studio.imagesequence"] representations = ["*"] label = "Play sequence" diff --git a/pype/plugins/global/publish/collect_assumed_destination.py b/pype/plugins/global/publish/collect_assumed_destination.py index 00e56cd2bf..2021a17dff 100644 --- a/pype/plugins/global/publish/collect_assumed_destination.py +++ b/pype/plugins/global/publish/collect_assumed_destination.py @@ -35,6 +35,12 @@ class CollectAssumedDestination(pyblish.api.InstancePlugin): # Add destination to the resource source_filename = os.path.basename(resource["source"]) destination = os.path.join(mock_destination, source_filename) + + # Force forward slashes to fix issue with software unable + # to work correctly with backslashes in specific scenarios + # (e.g. escape characters in PLN-151 V-Ray UDIM) + destination = destination.replace("\\", "/") + resource['destination'] = destination # Collect transfers for the individual files of the resource diff --git a/pype/plugins/global/publish/collect_deadline_user.py b/pype/plugins/global/publish/collect_deadline_user.py index d5942e6eff..f90487e6e9 100644 --- a/pype/plugins/global/publish/collect_deadline_user.py +++ b/pype/plugins/global/publish/collect_deadline_user.py @@ -35,7 +35,7 @@ class CollectDeadlineUser(pyblish.api.ContextPlugin): order = pyblish.api.CollectorOrder + 0.499 label = "Deadline User" hosts = ['maya', 'fusion'] - families = ["renderlayer", "saver.deadline"] + families = ["studio.renderlayer", "studio.saver.deadline"] def process(self, context): """Inject the current working file""" diff --git a/pype/plugins/global/publish/collect_filesequences.py b/pype/plugins/global/publish/collect_filesequences.py index 3dcf0fdea6..e2564a9918 100644 --- a/pype/plugins/global/publish/collect_filesequences.py +++ b/pype/plugins/global/publish/collect_filesequences.py @@ -148,7 +148,7 @@ class CollectFileSequences(pyblish.api.ContextPlugin): raise RuntimeError("Invalid sequence") # Get family from the data - families = data.get("families", ["imagesequence"]) + families = data.get("families", ["studio.imagesequence"]) assert isinstance(families, (list, tuple)), "Must be iterable" assert families, "Must have at least a single family" diff --git a/pype/plugins/global/publish/integrate.py b/pype/plugins/global/publish/integrate.py index 141b492f59..d99232fd10 100644 --- a/pype/plugins/global/publish/integrate.py +++ b/pype/plugins/global/publish/integrate.py @@ -23,18 +23,19 @@ class IntegrateAsset(pyblish.api.InstancePlugin): label = "Integrate Asset" order = pyblish.api.IntegratorOrder - families = ["animation", - "camera", - "imagesequence", - "look", - "pype.mayaAscii", - "model", - "pointcache", - "setdress", - "rig", - "vrayproxy", - "yetiRig", - "yeticache"] + families = ["studio.animation", + "studio.camera", + "studio.imagesequence", + "studio.look", + "studio.mayaAscii", + "studio.model", + "studio.pointcache", + "studio.vdbcache", + "studio.setdress", + "studio.rig", + "studio.vrayproxy", + "studio.yetiRig", + "studio.yeticache"] def process(self, instance): @@ -82,7 +83,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): self.log.debug("Establishing staging directory @ %s" % stagingdir) project = io.find_one({"type": "project"}, - projection={"pype.template.publish": True}) + projection={"config.template.publish": True}) asset = io.find_one({"type": "asset", "name": ASSET, diff --git a/pype/plugins/global/publish/submit_publish_job.py b/pype/plugins/global/publish/submit_publish_job.py index 63b201d8f1..e2257eacf3 100644 --- a/pype/plugins/global/publish/submit_publish_job.py +++ b/pype/plugins/global/publish/submit_publish_job.py @@ -123,7 +123,7 @@ class SubmitDependentImageSequenceJobDeadline(pyblish.api.InstancePlugin): label = "Submit image sequence jobs to Deadline" order = pyblish.api.IntegratorOrder + 0.1 hosts = ["fusion", "maya"] - families = ["saver.deadline", "renderlayer"] + families = ["studio.saver.deadline", "studio.renderlayer"] def process(self, instance): @@ -168,7 +168,7 @@ class SubmitDependentImageSequenceJobDeadline(pyblish.api.InstancePlugin): "regex": regex, "startFrame": start, "endFrame": end, - "families": ["imagesequence"], + "families": ["studio.imagesequence"], # Optional metadata (for debugging) "metadata": { @@ -185,7 +185,7 @@ class SubmitDependentImageSequenceJobDeadline(pyblish.api.InstancePlugin): if data.get("extendFrames", False): - family = "imagesequence" + family = "studio.imagesequence" override = data["overrideExistingFrame"] # override = data.get("overrideExistingFrame", False) @@ -293,6 +293,10 @@ class SubmitDependentImageSequenceJobDeadline(pyblish.api.InstancePlugin): ) for index, key in enumerate(environment) }) + # Avoid copied pools and remove secondary pool + payload["JobInfo"]["Pool"] = "none" + payload["JobInfo"].pop("SecondaryPool", None) + self.log.info("Submitting..") self.log.info(json.dumps(payload, indent=4, sort_keys=True)) diff --git a/pype/plugins/global/publish/validate_resources.py b/pype/plugins/global/publish/validate_resources.py new file mode 100644 index 0000000000..bc10d3003c --- /dev/null +++ b/pype/plugins/global/publish/validate_resources.py @@ -0,0 +1,29 @@ +import pyblish.api +import pype.api + +import os + + +class ValidateResources(pyblish.api.InstancePlugin): + """Validates mapped resources. + + These are external files to the current application, for example + these could be textures, image planes, cache files or other linked + media. + + This validates: + - The resources are existing files. + - The resources have correctly collected the data. + + """ + + order = pype.api.ValidateContentsOrder + label = "Resources" + + def process(self, instance): + + for resource in instance.data.get('resources', []): + # Required data + assert "source" in resource, "No source found" + assert "files" in resource, "No files from source" + assert all(os.path.exists(f) for f in resource['files']) diff --git a/pype/plugins/global/publish/validate_sequence_frames.py b/pype/plugins/global/publish/validate_sequence_frames.py index cd54e6becc..1258050394 100644 --- a/pype/plugins/global/publish/validate_sequence_frames.py +++ b/pype/plugins/global/publish/validate_sequence_frames.py @@ -11,7 +11,7 @@ class ValidateSequenceFrames(pyblish.api.InstancePlugin): order = pyblish.api.ValidatorOrder label = "Validate Sequence Frames" - families = ["imagesequence"] + families = ["studio.imagesequence"] hosts = ["shell"] def process(self, instance): diff --git a/pype/plugins/houdini/create/create_alembic_camera.py b/pype/plugins/houdini/create/create_alembic_camera.py new file mode 100644 index 0000000000..8c7a16506e --- /dev/null +++ b/pype/plugins/houdini/create/create_alembic_camera.py @@ -0,0 +1,34 @@ +from collections import OrderedDict + +from avalon import houdini + + +class CreateAlembicCamera(houdini.Creator): + + name = "camera" + label = "Camera (Abc)" + family = "studio.camera" + icon = "camera" + + def __init__(self, *args, **kwargs): + super(CreateAlembicCamera, self).__init__(*args, **kwargs) + + # Remove the active, we are checking the bypass flag of the nodes + self.data.pop("active", None) + + # Set node type to create for output + self.data.update({"node_type": "alembic"}) + + def process(self): + instance = super(CreateAlembicCamera, self).process() + + parms = {"use_sop_path": True, + "build_from_path": True, + "path_attrib": "path", + "filename": "$HIP/pyblish/%s.abc" % self.name} + + if self.nodes: + node = self.nodes[0] + parms.update({"sop_path": node.path()}) + + instance.setParms(parms) diff --git a/pype/plugins/houdini/create/create_pointcache.py b/pype/plugins/houdini/create/create_pointcache.py new file mode 100644 index 0000000000..36f0e799ae --- /dev/null +++ b/pype/plugins/houdini/create/create_pointcache.py @@ -0,0 +1,40 @@ +from avalon import houdini + + +class CreatePointCache(houdini.Creator): + """Alembic pointcache for animated data""" + + name = "pointcache" + label = "Point Cache" + family = "studio.pointcache" + icon = "gears" + + def __init__(self, *args, **kwargs): + super(CreatePointCache, self).__init__(*args, **kwargs) + + # Remove the active, we are checking the bypass flag of the nodes + self.data.pop("active", None) + + self.data.update({"node_type": "alembic"}) + + def process(self): + instance = super(CreatePointCache, self).process() + + parms = {"use_sop_path": True, # Export single node from SOP Path + "build_from_path": True, # Direct path of primitive in output + "path_attrib": "path", # Pass path attribute for output\ + "prim_to_detail_pattern": "cbId", + "format": 2, # Set format to Ogawa + "filename": "$HIP/pyblish/%s.abc" % self.name} + + if self.nodes: + node = self.nodes[0] + parms.update({"sop_path": node.path()}) + + instance.setParms(parms) + + # Lock any parameters in this list + to_lock = ["prim_to_detail_pattern"] + for name in to_lock: + parm = instance.parm(name) + parm.lock(True) diff --git a/pype/plugins/houdini/create/create_vbd_cache.py b/pype/plugins/houdini/create/create_vbd_cache.py new file mode 100644 index 0000000000..4c78b5bc20 --- /dev/null +++ b/pype/plugins/houdini/create/create_vbd_cache.py @@ -0,0 +1,33 @@ +from avalon import houdini + + +class CreateVDBCache(houdini.Creator): + """Alembic pointcache for animated data""" + + name = "vbdcache" + label = "VDB Cache" + family = "studio.vdbcache" + icon = "cloud" + + def __init__(self, *args, **kwargs): + super(CreateVDBCache, self).__init__(*args, **kwargs) + + # Remove the active, we are checking the bypass flag of the nodes + self.data.pop("active", None) + + self.data.update({ + "node_type": "geometry", # Set node type to create for output + "executeBackground": True # Render node in background + }) + + def process(self): + instance = super(CreateVDBCache, self).process() + + parms = {"sopoutput": "$HIP/pyblish/%s.$F4.vdb" % self.name, + "initsim": True} + + if self.nodes: + node = self.nodes[0] + parms.update({"sop_path": node.path()}) + + instance.setParms(parms) diff --git a/pype/plugins/houdini/load/load_alembic.py b/pype/plugins/houdini/load/load_alembic.py new file mode 100644 index 0000000000..de84991d53 --- /dev/null +++ b/pype/plugins/houdini/load/load_alembic.py @@ -0,0 +1,109 @@ +from avalon import api + +from avalon.houdini import pipeline, lib + + +class AbcLoader(api.Loader): + """Specific loader of Alembic for the avalon.animation family""" + + families = ["studio.model", + "studio.animation", + "studio.pointcache"] + label = "Load Alembic" + representations = ["abc"] + order = -10 + icon = "code-fork" + color = "orange" + + def load(self, context, name=None, namespace=None, data=None): + + import os + import hou + + # Format file name, Houdini only wants forward slashes + file_path = os.path.normpath(self.fname) + file_path = file_path.replace("\\", "/") + + # Get the root node + obj = hou.node("/obj") + + # Create a unique name + counter = 1 + namespace = namespace if namespace else context["asset"]["name"] + formatted = "{}_{}".format(namespace, name) if namespace else name + node_name = "{0}_{1:03d}".format(formatted, counter) + + children = lib.children_as_string(hou.node("/obj")) + while node_name in children: + counter += 1 + node_name = "{0}_{1:03d}".format(formatted, counter) + + # Create a new geo node + container = obj.createNode("geo", node_name=node_name) + + # Remove the file node, it only loads static meshes + # Houdini 17 has removed the file node from the geo node + file_node = container.node("file1") + if file_node: + file_node.destroy() + + # Create an alembic node (supports animation) + alembic = container.createNode("alembic", node_name=node_name) + alembic.setParms({"fileName": file_path}) + + # Add unpack node + unpack_name = "unpack_{}".format(name) + unpack = container.createNode("unpack", node_name=unpack_name) + unpack.setInput(0, alembic) + unpack.setParms({"transfer_attributes": "path"}) + + # Add normal to points + # Order of menu ['point', 'vertex', 'prim', 'detail'] + normal_name = "normal_{}".format(name) + normal_node = container.createNode("normal", node_name=normal_name) + normal_node.setParms({"type": 0}) + + normal_node.setInput(0, unpack) + + null = container.createNode("null", node_name="OUT".format(name)) + null.setInput(0, normal_node) + + # Set display on last node + null.setDisplayFlag(True) + + # Set new position for unpack node else it gets cluttered + nodes = [container, alembic, unpack, normal_node, null] + for nr, node in enumerate(nodes): + node.setPosition([0, (0 - nr)]) + + self[:] = nodes + + return pipeline.containerise(node_name, + namespace, + nodes, + context, + self.__class__.__name__) + + def update(self, container, representation): + + node = container["node"] + try: + alembic_node = next(n for n in node.children() if + n.type().name() == "alembic") + except StopIteration: + self.log.error("Could not find node of type `alembic`") + return + + # Update the file path + file_path = api.get_representation_path(representation) + file_path = file_path.replace("\\", "/") + + alembic_node.setParms({"fileName": file_path}) + + # Update attribute + node.setParms({"representation": str(representation["_id"])}) + + def remove(self, container): + + node = container["node"] + node.destroy() diff --git a/pype/plugins/houdini/load/load_camera.py b/pype/plugins/houdini/load/load_camera.py new file mode 100644 index 0000000000..6f3c2953d4 --- /dev/null +++ b/pype/plugins/houdini/load/load_camera.py @@ -0,0 +1,119 @@ +from avalon import api + +from avalon.houdini import pipeline, lib + + +class CameraLoader(api.Loader): + """Specific loader of Alembic for the avalon.animation family""" + + families = ["studio.camera"] + label = "Load Camera (abc)" + representations = ["abc"] + order = -10 + + icon = "code-fork" + color = "orange" + + def load(self, context, name=None, namespace=None, data=None): + + import os + import hou + + # Format file name, Houdini only wants forward slashes + file_path = os.path.normpath(self.fname) + file_path = file_path.replace("\\", "/") + + # Get the root node + obj = hou.node("/obj") + + # Create a unique name + counter = 1 + asset_name = context["asset"]["name"] + + namespace = namespace if namespace else asset_name + formatted = "{}_{}".format(namespace, name) if namespace else name + node_name = "{0}_{1:03d}".format(formatted, counter) + + children = lib.children_as_string(hou.node("/obj")) + while node_name in children: + counter += 1 + node_name = "{0}_{1:03d}".format(formatted, counter) + + # Create a archive node + container = self.create_and_connect(obj, "alembicarchive", node_name) + + # TODO: add FPS of project / asset + container.setParms({"fileName": file_path, + "channelRef": True}) + + # Apply some magic + container.parm("buildHierarchy").pressButton() + container.moveToGoodPosition() + + # Create an alembic xform node + nodes = [container] + + self[:] = nodes + + return pipeline.containerise(node_name, + namespace, + nodes, + context, + self.__class__.__name__) + + def update(self, container, representation): + + node = container["node"] + + # Update the file path + file_path = api.get_representation_path(representation) + file_path = file_path.replace("\\", "/") + + # Update attributes + node.setParms({"fileName": file_path, + "representation": str(representation["_id"])}) + + # Rebuild + node.parm("buildHierarchy").pressButton() + + def remove(self, container): + + node = container["node"] + node.destroy() + + def create_and_connect(self, node, node_type, name=None): + """Create a node within a node which and connect it to the input + + Args: + node(hou.Node): parent of the new node + node_type(str) name of the type of node, eg: 'alembic' + name(str, Optional): name of the node + + Returns: + hou.Node + + """ + + import hou + + try: + + if name: + new_node = node.createNode(node_type, node_name=name) + else: + new_node = node.createNode(node_type) + + new_node.moveToGoodPosition() + + try: + input_node = next(i for i in node.allItems() if + isinstance(i, hou.SubnetIndirectInput)) + except StopIteration: + return new_node + + new_node.setInput(0, input_node) + return new_node + + except Exception: + raise RuntimeError("Could not created node type `%s` in node `%s`" + % (node_type, node)) diff --git a/pype/plugins/houdini/publish/collect_current_file.py b/pype/plugins/houdini/publish/collect_current_file.py new file mode 100644 index 0000000000..7852943b34 --- /dev/null +++ b/pype/plugins/houdini/publish/collect_current_file.py @@ -0,0 +1,15 @@ +import hou + +import pyblish.api + + +class CollectHoudiniCurrentFile(pyblish.api.ContextPlugin): + """Inject the current working file into context""" + + order = pyblish.api.CollectorOrder - 0.5 + label = "Houdini Current File" + hosts = ['houdini'] + + def process(self, context): + """Inject the current working file""" + context.data['currentFile'] = hou.hipFile.path() diff --git a/pype/plugins/houdini/publish/collect_frames.py b/pype/plugins/houdini/publish/collect_frames.py new file mode 100644 index 0000000000..53a7cecc43 --- /dev/null +++ b/pype/plugins/houdini/publish/collect_frames.py @@ -0,0 +1,66 @@ +import os +import re + +import pyblish.api +from pype.houdini import lib + + +class CollectFrames(pyblish.api.InstancePlugin): + """Collect all frames which would be a resukl""" + + order = pyblish.api.CollectorOrder + label = "Collect Frames" + families = ["studio.vdbcache"] + + def process(self, instance): + + ropnode = instance[0] + + output_parm = lib.get_output_parameter(ropnode) + output = output_parm.eval() + + file_name = os.path.basename(output) + match = re.match("(\w+)\.(\d+)\.vdb", file_name) + result = file_name + + start_frame = instance.data.get("startFrame", None) + end_frame = instance.data.get("endFrame", None) + + if match and start_frame is not None: + + # Check if frames are bigger than 1 (file collection) + # override the result + if end_frame - start_frame > 1: + result = self.create_file_list(match, + int(start_frame), + int(end_frame)) + + instance.data.update({"frames": result}) + + def create_file_list(self, match, start_frame, end_frame): + """Collect files based on frame range and regex.match + + Args: + match(re.match): match object + start_frame(int): start of the animation + end_frame(int): end of the animation + + Returns: + list + + """ + + result = [] + + padding = len(match.group(2)) + name = match.group(1) + padding_format = "{number:0{width}d}" + + count = start_frame + while count <= end_frame: + str_count = padding_format.format(number=count, width=padding) + file_name = "{}.{}.vdb".format(name, str_count) + result.append(file_name) + count += 1 + + return result diff --git a/pype/plugins/houdini/publish/collect_instances.py b/pype/plugins/houdini/publish/collect_instances.py new file mode 100644 index 0000000000..61d4cdbe0b --- /dev/null +++ b/pype/plugins/houdini/publish/collect_instances.py @@ -0,0 +1,104 @@ +import hou + +import pyblish.api + +from avalon.houdini import lib + + +class CollectInstances(pyblish.api.ContextPlugin): + """Gather instances by all node in out graph and pre-defined attributes + + This collector takes into account assets that are associated with + an specific node and marked with a unique identifier; + + Identifier: + id (str): "pyblish.avalon.instance + + Specific node: + The specific node is important because it dictates in which way the subset + is being exported. + + alembic: will export Alembic file which supports cascading attributes + like 'cbId' and 'path' + geometry: Can export a wide range of file types, default out + + """ + + order = pyblish.api.CollectorOrder - 0.01 + label = "Collect Instances" + hosts = ["houdini"] + + def process(self, context): + + instances = [] + + nodes = hou.node("/out").children() + for node in nodes: + + if not node.parm("id"): + continue + + if node.evalParm("id") != "pyblish.avalon.instance": + continue + + has_family = node.evalParm("family") + assert has_family, "'%s' is missing 'family'" % node.name() + + data = lib.read(node) + # Check bypass state and reverse + data.update({"active": not node.isBypassed()}) + + # 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"] + + data.update(self.get_frame_data(node)) + + # Create nice name + # All nodes in the Outputs graph have the 'Valid Frame Range' + # attribute, we check here if any frames are set + label = data.get("name", node.name()) + if "startFrame" in data: + frames = "[{startFrame} - {endFrame}]".format(**data) + label = "{} {}".format(label, frames) + + instance = context.create_instance(label) + + instance[:] = [node] + instance.data.update(data) + + instances.append(instance) + + 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 + + def get_frame_data(self, node): + """Get the frame data: start frame, end frame and steps + Args: + node(hou.Node) + + Returns: + dict + + """ + + data = {} + + if node.parm("trange") is None: + return data + + if node.evalParm("trange") == 0: + return data + + data["startFrame"] = node.evalParm("f1") + data["endFrame"] = node.evalParm("f2") + data["steps"] = node.evalParm("f3") + + return data diff --git a/pype/plugins/houdini/publish/collect_output_node.py b/pype/plugins/houdini/publish/collect_output_node.py new file mode 100644 index 0000000000..dbfe8a5890 --- /dev/null +++ b/pype/plugins/houdini/publish/collect_output_node.py @@ -0,0 +1,27 @@ +import pyblish.api + + +class CollectOutputNode(pyblish.api.InstancePlugin): + """Collect the out node which of the instance""" + + order = pyblish.api.CollectorOrder + families = ["*"] + hosts = ["houdini"] + label = "Collect Output Node" + + def process(self, instance): + + import hou + + node = instance[0] + + # Get sop path + if node.type().name() == "alembic": + sop_path_parm = "sop_path" + else: + sop_path_parm = "soppath" + + sop_path = node.parm(sop_path_parm).eval() + out_node = hou.node(sop_path) + + instance.data["output_node"] = out_node diff --git a/pype/plugins/houdini/publish/extract_alembic.py b/pype/plugins/houdini/publish/extract_alembic.py new file mode 100644 index 0000000000..94cb9d8471 --- /dev/null +++ b/pype/plugins/houdini/publish/extract_alembic.py @@ -0,0 +1,32 @@ +import os + +import pyblish.api +import pype.api + + +class ExtractAlembic(pype.api.Extractor): + + order = pyblish.api.ExtractorOrder + label = "Extract Alembic" + hosts = ["houdini"] + families = ["studio.pointcache", "studio.camera"] + + def process(self, instance): + + ropnode = instance[0] + + # Get the filename from the filename parameter + output = ropnode.evalParm("filename") + staging_dir = os.path.dirname(output) + instance.data["stagingDir"] = staging_dir + + file_name = os.path.basename(output) + + # We run the render + self.log.info("Writing alembic '%s' to '%s'" % (file_name, staging_dir)) + ropnode.render() + + if "files" not in instance.data: + instance.data["files"] = [] + + instance.data["files"].append(file_name) diff --git a/pype/plugins/houdini/publish/extract_vdb_cache.py b/pype/plugins/houdini/publish/extract_vdb_cache.py new file mode 100644 index 0000000000..c601d42179 --- /dev/null +++ b/pype/plugins/houdini/publish/extract_vdb_cache.py @@ -0,0 +1,36 @@ +import os + +import pyblish.api +import pype.api + + +class ExtractVDBCache(pype.api.Extractor): + + order = pyblish.api.ExtractorOrder + 0.1 + label = "Extract VDB Cache" + families = ["studio.vdbcache"] + hosts = ["houdini"] + + def process(self, instance): + + ropnode = instance[0] + + # Get the filename from the filename parameter + # `.evalParm(parameter)` will make sure all tokens are resolved + sop_output = ropnode.evalParm("sopoutput") + staging_dir = os.path.normpath(os.path.dirname(sop_output)) + instance.data["stagingDir"] = staging_dir + + if instance.data.get("executeBackground", True): + self.log.info("Creating background task..") + ropnode.parm("executebackground").pressButton() + self.log.info("Finished") + else: + ropnode.render() + + if "files" not in instance.data: + instance.data["files"] = [] + + output = instance.data["frames"] + + instance.data["files"].append(output) diff --git a/pype/plugins/houdini/publish/valiate_vdb_input_node.py b/pype/plugins/houdini/publish/valiate_vdb_input_node.py new file mode 100644 index 0000000000..ae7eb1f6b9 --- /dev/null +++ b/pype/plugins/houdini/publish/valiate_vdb_input_node.py @@ -0,0 +1,46 @@ +import pyblish.api +import pype.api + + +class ValidateVDBInputNode(pyblish.api.InstancePlugin): + """Validate that the node connected to the output node is of type VDB + + Regardless of the amount of VDBs create the output will need to have an + equal amount of VDBs, points, primitives and vertices + + A VDB is an inherited type of Prim, holds the following data: + - Primitives: 1 + - Points: 1 + - Vertices: 1 + - VDBs: 1 + + """ + + order = pype.api.ValidateContentsOrder + 0.1 + families = ["studio.vdbcache"] + hosts = ["houdini"] + label = "Validate Input Node (VDB)" + + def process(self, instance): + invalid = self.get_invalid(instance) + if invalid: + raise RuntimeError("Node connected to the output node is not" + "of type VDB!") + + @classmethod + def get_invalid(cls, instance): + + node = instance.data["output_node"] + + prims = node.geometry().prims() + nr_of_prims = len(prims) + + nr_of_points = len(node.geometry().points()) + if nr_of_points != nr_of_prims: + cls.log.error("The number of primitives and points do not match") + return [instance] + + for prim in prims: + if prim.numVertices() != 1: + cls.log.error("Found primitive with more than 1 vertex!") + return [instance] diff --git a/pype/plugins/houdini/publish/validate_alembic_input_node.py b/pype/plugins/houdini/publish/validate_alembic_input_node.py new file mode 100644 index 0000000000..ab2a4fd6f2 --- /dev/null +++ b/pype/plugins/houdini/publish/validate_alembic_input_node.py @@ -0,0 +1,37 @@ +import pyblish.api +import pype.api + + +class ValidateAlembicInputNode(pyblish.api.InstancePlugin): + """Validate that the node connected to the output is correct + + The connected node cannot be of the following types for Alembic: + - VDB + - Volumne + + """ + + order = pype.api.ValidateContentsOrder + 0.1 + families = ["studio.pointcache"] + hosts = ["houdini"] + label = "Validate Input Node (Abc)" + + def process(self, instance): + invalid = self.get_invalid(instance) + if invalid: + raise RuntimeError("Node connected to the output node incorrect") + + @classmethod + def get_invalid(cls, instance): + + invalid_nodes = ["VDB", "Volume"] + node = instance.data["output_node"] + + prims = node.geometry().prims() + + for prim in prims: + prim_type = prim.type().name() + if prim_type in invalid_nodes: + cls.log.error("Found a primitive which is of type '%s' !" + % prim_type) + return [instance] diff --git a/pype/plugins/houdini/publish/validate_animation_settings.py b/pype/plugins/houdini/publish/validate_animation_settings.py new file mode 100644 index 0000000000..5b61b171d8 --- /dev/null +++ b/pype/plugins/houdini/publish/validate_animation_settings.py @@ -0,0 +1,50 @@ +import pyblish.api + +from pype.houdini import lib + + +class ValidateAnimationSettings(pyblish.api.InstancePlugin): + """Validate if the unexpanded string contains the frame ('$F') token + + This validator will only check the output parameter of the node if + the Valid Frame Range is not set to 'Render Current Frame' + + Rules: + If you render out a frame range it is mandatory to have the + frame token - '$F4' or similar - to ensure that each frame gets + written. If this is not the case you will override the same file + every time a frame is written out. + + Examples: + Good: 'my_vbd_cache.$F4.vdb' + Bad: 'my_vbd_cache.vdb' + + """ + + order = pyblish.api.ValidatorOrder + label = "Validate Frame Settings" + families = ["studio.vdbcache"] + + def process(self, instance): + + invalid = self.get_invalid(instance) + if invalid: + raise RuntimeError("Output settings do no match for '%s'" % + instance) + + @classmethod + def get_invalid(cls, instance): + + node = instance[0] + + # Check trange parm, 0 means Render Current Frame + frame_range = node.evalParm("trange") + if frame_range == 0: + return [] + + output_parm = lib.get_output_parameter(node) + unexpanded_str = output_parm.unexpandedString() + + if "$F" not in unexpanded_str: + cls.log.error("No frame token found in '%s'" % node.path()) + return [instance] diff --git a/pype/plugins/houdini/publish/validate_mkpaths_toggled.py b/pype/plugins/houdini/publish/validate_mkpaths_toggled.py new file mode 100644 index 0000000000..1e5cfb8d80 --- /dev/null +++ b/pype/plugins/houdini/publish/validate_mkpaths_toggled.py @@ -0,0 +1,38 @@ +import pyblish.api +import pype.api + + +class ValidateIntermediateDirectoriesChecked(pyblish.api.InstancePlugin): + """Validate if node attribute Create intermediate Directories is turned on + + Rules: + * The node must have Create intermediate Directories turned on to + ensure the output file will be created + + """ + + order = pype.api.ValidateContentsOrder + families = ["studio.pointcache'] + hosts = ['houdini'] + label = 'Create Intermediate Directories Checked' + + def process(self, instance): + + invalid = self.get_invalid(instance) + if invalid: + raise RuntimeError("Found ROP nodes with Create Intermediate " + "Directories turned off") + + @classmethod + def get_invalid(cls, instance): + + result = [] + + for node in instance[:]: + if node.parm("mkpath").eval() != 1: + cls.log.error("Invalid settings found on `%s`" % node.path()) + result.append(node.path()) + + return result + + diff --git a/pype/plugins/houdini/publish/validate_outnode_exists.py b/pype/plugins/houdini/publish/validate_outnode_exists.py new file mode 100644 index 0000000000..0ba04b87d0 --- /dev/null +++ b/pype/plugins/houdini/publish/validate_outnode_exists.py @@ -0,0 +1,50 @@ +import pyblish.api +import pype.api + + +class ValidatOutputNodeExists(pyblish.api.InstancePlugin): + """Validate if node attribute Create intermediate Directories is turned on + + Rules: + * The node must have Create intermediate Directories turned on to + ensure the output file will be created + + """ + + order = pype.api.ValidateContentsOrder + families = ["*"] + hosts = ['houdini'] + label = "Output Node Exists" + + def process(self, instance): + invalid = self.get_invalid(instance) + if invalid: + raise RuntimeError("Could not find output node(s)!") + + @classmethod + def get_invalid(cls, instance): + + import hou + + result = set() + + node = instance[0] + if node.type().name() == "alembic": + soppath_parm = "sop_path" + else: + # Fall back to geometry node + soppath_parm = "soppath" + + sop_path = node.parm(soppath_parm).eval() + output_node = hou.node(sop_path) + + if output_node is None: + cls.log.error("Node at '%s' does not exist" % sop_path) + result.add(node.path()) + + # Added cam as this is a legit output type (cameras can't + if output_node.type().name() not in ["output", "cam"]: + cls.log.error("SOP Path does not end path at output node") + result.add(node.path()) + + return result diff --git a/pype/plugins/houdini/publish/validate_output_node.py b/pype/plugins/houdini/publish/validate_output_node.py new file mode 100644 index 0000000000..d3393e1c33 --- /dev/null +++ b/pype/plugins/houdini/publish/validate_output_node.py @@ -0,0 +1,45 @@ +import pyblish.api + + +class ValidateOutputNode(pyblish.api.InstancePlugin): + """Validate if output node: + - exists + - is of type 'output' + - has an input""" + + order = pyblish.api.ValidatorOrder + families = ["*"] + hosts = ["houdini"] + label = "Validate Output Node" + + def process(self, instance): + + invalid = self.get_invalid(instance) + if invalid: + raise RuntimeError("Output node(s) `%s` are incorrect" % invalid) + + @classmethod + def get_invalid(cls, instance): + + output_node = instance.data["output_node"] + + if output_node is None: + node = instance[0] + cls.log.error("Output node at '%s' does not exist, see source" % + node.path()) + + return node.path() + + # Check if type is correct + type_name = output_node.type().name() + if type_name not in ["output", "cam"]: + cls.log.error("Output node `%s` is not an accepted type `output` " + "or `camera`" % + output_node.path()) + return [output_node.path()] + + # Check if output node has incoming connections + if type_name == "output" and not output_node.inputConnections(): + cls.log.error("Output node `%s` has no incoming connections" + % output_node.path()) + return [output_node.path()] diff --git a/pype/plugins/maya/create/colorbleed_animation.py b/pype/plugins/maya/create/colorbleed_animation.py index ea8e8ebb39..a5574dec18 100644 --- a/pype/plugins/maya/create/colorbleed_animation.py +++ b/pype/plugins/maya/create/colorbleed_animation.py @@ -9,7 +9,7 @@ class CreateAnimation(avalon.maya.Creator): name = "animationDefault" label = "Animation" - family = "animation" + family = "studio.animation" icon = "male" def __init__(self, *args, **kwargs): diff --git a/pype/plugins/maya/create/colorbleed_camera.py b/pype/plugins/maya/create/colorbleed_camera.py index 7f954c2461..a30e202722 100644 --- a/pype/plugins/maya/create/colorbleed_camera.py +++ b/pype/plugins/maya/create/colorbleed_camera.py @@ -8,7 +8,7 @@ class CreateCamera(avalon.maya.Creator): name = "cameraDefault" label = "Camera" - family = "camera" + family = "studio.camera" icon = "video-camera" def __init__(self, *args, **kwargs): diff --git a/pype/plugins/maya/create/colorbleed_look.py b/pype/plugins/maya/create/colorbleed_look.py index 17e8bcd7e3..a3f4574658 100644 --- a/pype/plugins/maya/create/colorbleed_look.py +++ b/pype/plugins/maya/create/colorbleed_look.py @@ -8,7 +8,7 @@ class CreateLook(avalon.maya.Creator): name = "look" label = "Look" - family = "look" + family = "studio.look" icon = "paint-brush" def __init__(self, *args, **kwargs): diff --git a/pype/plugins/maya/create/colorbleed_mayaascii.py b/pype/plugins/maya/create/colorbleed_mayaascii.py index c252765ff4..be163107b3 100644 --- a/pype/plugins/maya/create/colorbleed_mayaascii.py +++ b/pype/plugins/maya/create/colorbleed_mayaascii.py @@ -6,5 +6,5 @@ class CreateMayaAscii(avalon.maya.Creator): name = "mayaAscii" label = "Maya Ascii" - family = "pype.mayaAscii" + family = "studio.mayaAscii" icon = "file-archive-o" diff --git a/pype/plugins/maya/create/colorbleed_model.py b/pype/plugins/maya/create/colorbleed_model.py index 2573241107..b20404d3ca 100644 --- a/pype/plugins/maya/create/colorbleed_model.py +++ b/pype/plugins/maya/create/colorbleed_model.py @@ -1,3 +1,5 @@ +from collections import OrderedDict + import avalon.maya @@ -6,5 +8,16 @@ class CreateModel(avalon.maya.Creator): name = "modelDefault" label = "Model" - family = "model" + family = "studio.model" icon = "cube" + + def __init__(self, *args, **kwargs): + super(CreateModel, self).__init__(*args, **kwargs) + + # create an ordered dict with the existing data first + data = OrderedDict(**self.data) + + # Write vertex colors with the geometry. + data["writeColorSets"] = True + + self.data = data diff --git a/pype/plugins/maya/create/colorbleed_pointcache.py b/pype/plugins/maya/create/colorbleed_pointcache.py index a660485e94..eacec62c51 100644 --- a/pype/plugins/maya/create/colorbleed_pointcache.py +++ b/pype/plugins/maya/create/colorbleed_pointcache.py @@ -9,7 +9,7 @@ class CreatePointCache(avalon.maya.Creator): name = "pointcache" label = "Point Cache" - family = "pointcache" + family = "studio.pointcache" icon = "gears" def __init__(self, *args, **kwargs): diff --git a/pype/plugins/maya/create/colorbleed_renderglobals.py b/pype/plugins/maya/create/colorbleed_renderglobals.py index ca086f1f47..938ec9edea 100644 --- a/pype/plugins/maya/create/colorbleed_renderglobals.py +++ b/pype/plugins/maya/create/colorbleed_renderglobals.py @@ -10,7 +10,7 @@ from avalon import api class CreateRenderGlobals(avalon.maya.Creator): label = "Render Globals" - family = "renderglobals" + family = "studio.renderglobals" icon = "gears" def __init__(self, *args, **kwargs): diff --git a/pype/plugins/maya/create/colorbleed_rig.py b/pype/plugins/maya/create/colorbleed_rig.py index e8bcd2baa2..471219c6ef 100644 --- a/pype/plugins/maya/create/colorbleed_rig.py +++ b/pype/plugins/maya/create/colorbleed_rig.py @@ -8,7 +8,7 @@ class CreateRig(avalon.maya.Creator): name = "rigDefault" label = "Rig" - family = "rig" + family = "studio.rig" icon = "wheelchair" def process(self): diff --git a/pype/plugins/maya/create/colorbleed_setdress.py b/pype/plugins/maya/create/colorbleed_setdress.py index 79f08ca04c..56c1129a82 100644 --- a/pype/plugins/maya/create/colorbleed_setdress.py +++ b/pype/plugins/maya/create/colorbleed_setdress.py @@ -6,5 +6,5 @@ class CreateSetDress(avalon.maya.Creator): name = "setdress" label = "Set Dress" - family = "setdress" + family = "studio.setdress" icon = "cubes" \ No newline at end of file diff --git a/pype/plugins/maya/create/colorbleed_vrayproxy.py b/pype/plugins/maya/create/colorbleed_vrayproxy.py index ea441a3e02..805e358c22 100644 --- a/pype/plugins/maya/create/colorbleed_vrayproxy.py +++ b/pype/plugins/maya/create/colorbleed_vrayproxy.py @@ -8,7 +8,7 @@ class CreateVrayProxy(avalon.maya.Creator): name = "vrayproxy" label = "VRay Proxy" - family = "vrayproxy" + family = "studio.vrayproxy" icon = "gears" def __init__(self, *args, **kwargs): @@ -20,4 +20,7 @@ class CreateVrayProxy(avalon.maya.Creator): data["startFrame"] = 1 data["endFrame"] = 1 + # Write vertex colors + data["vertexColors"] = False + self.data.update(data) diff --git a/pype/plugins/maya/create/colorbleed_yeti_cache.py b/pype/plugins/maya/create/colorbleed_yeti_cache.py index 36f1869ec5..09a6599be0 100644 --- a/pype/plugins/maya/create/colorbleed_yeti_cache.py +++ b/pype/plugins/maya/create/colorbleed_yeti_cache.py @@ -9,17 +9,18 @@ class CreateYetiCache(avalon.maya.Creator): name = "yetiDefault" label = "Yeti Cache" - family = "yeticache" + family = "studio.yeticache" icon = "pagelines" def __init__(self, *args, **kwargs): super(CreateYetiCache, self).__init__(*args, **kwargs) - data = OrderedDict(self.data) + data = OrderedDict(**self.data) data["peroll"] = 0 anim_data = lib.collect_animation_data() data.update({"startFrame": anim_data["startFrame"], - "endFrame": anim_data["endFrame"]}) + "endFrame": anim_data["endFrame"], + "samples": 3}) self.data = data diff --git a/pype/plugins/maya/create/colorbleed_yeti_rig.py b/pype/plugins/maya/create/colorbleed_yeti_rig.py index ba44c7bf27..b0c9e1cece 100644 --- a/pype/plugins/maya/create/colorbleed_yeti_rig.py +++ b/pype/plugins/maya/create/colorbleed_yeti_rig.py @@ -7,7 +7,7 @@ class CreateYetiRig(avalon.maya.Creator): """Output for procedural plugin nodes ( Yeti / XGen / etc)""" label = "Yeti Rig" - family = "yetiRig" + family = "studio.yetiRig" icon = "usb" def process(self): diff --git a/pype/plugins/maya/load/_load_animation.py b/pype/plugins/maya/load/_load_animation.py index bfb09a2c14..0edf75f481 100644 --- a/pype/plugins/maya/load/_load_animation.py +++ b/pype/plugins/maya/load/_load_animation.py @@ -4,9 +4,9 @@ import pype.maya.plugin class AbcLoader(pype.maya.plugin.ReferenceLoader): """Specific loader of Alembic for the avalon.animation family""" - families = ["animation", - "camera", - "pointcache"] + families = ["studio.animation", + "studio.camera", + "studio.pointcache"] representations = ["abc"] label = "Reference animation" @@ -42,7 +42,7 @@ class AbcLoader(pype.maya.plugin.ReferenceLoader): reference=True, returnNewNodes=True) - # load studio ID attribute + # load colorbleed ID attribute self[:] = nodes return nodes diff --git a/pype/plugins/maya/load/actions.py b/pype/plugins/maya/load/actions.py index 6db3c6ba34..2bdca8506a 100644 --- a/pype/plugins/maya/load/actions.py +++ b/pype/plugins/maya/load/actions.py @@ -8,9 +8,9 @@ from avalon import api class SetFrameRangeLoader(api.Loader): """Specific loader of Alembic for the avalon.animation family""" - families = ["animation", - "camera", - "pointcache"] + families = ["studio.animation", + "studio.camera", + "studio.pointcache"] representations = ["abc"] label = "Set frame range" @@ -42,9 +42,9 @@ class SetFrameRangeLoader(api.Loader): class SetFrameRangeWithHandlesLoader(api.Loader): """Specific loader of Alembic for the avalon.animation family""" - families = ["animation", - "camera", - "pointcache"] + families = ["studio.animation", + "studio.camera", + "studio.pointcache"] representations = ["abc"] label = "Set frame range (with handles)" diff --git a/pype/plugins/maya/load/load_alembic.py b/pype/plugins/maya/load/load_alembic.py index c572c60355..61b52d12db 100644 --- a/pype/plugins/maya/load/load_alembic.py +++ b/pype/plugins/maya/load/load_alembic.py @@ -2,10 +2,10 @@ import pype.maya.plugin class AbcLoader(pype.maya.plugin.ReferenceLoader): - """Specific loader of Alembic for the avalon.animation family""" + """Specific loader of Alembic for the studio.animation family""" - families = ["animation", - "pointcache"] + families = ["studio.animation", + "studio.pointcache"] label = "Reference animation" representations = ["abc"] order = -10 diff --git a/pype/plugins/maya/load/load_camera.py b/pype/plugins/maya/load/load_camera.py index b91ca9ef69..1156d9e14c 100644 --- a/pype/plugins/maya/load/load_camera.py +++ b/pype/plugins/maya/load/load_camera.py @@ -2,9 +2,9 @@ import pype.maya.plugin class CameraLoader(pype.maya.plugin.ReferenceLoader): - """Specific loader of Alembic for the avalon.animation family""" + """Specific loader of Alembic for the studio.camera family""" - families = ["camera"] + families = ["studio.camera"] label = "Reference camera" representations = ["abc", "ma"] order = -10 diff --git a/pype/plugins/maya/load/load_look.py b/pype/plugins/maya/load/load_look.py index eede195a49..bcef5eabc6 100644 --- a/pype/plugins/maya/load/load_look.py +++ b/pype/plugins/maya/load/load_look.py @@ -4,7 +4,7 @@ import pype.maya.plugin class LookLoader(pype.maya.plugin.ReferenceLoader): """Specific loader for lookdev""" - families = ["look"] + families = ["studio.look"] representations = ["ma"] label = "Reference look" diff --git a/pype/plugins/maya/load/load_mayaascii.py b/pype/plugins/maya/load/load_mayaascii.py index c66add81ba..98bd8472ec 100644 --- a/pype/plugins/maya/load/load_mayaascii.py +++ b/pype/plugins/maya/load/load_mayaascii.py @@ -4,7 +4,7 @@ import pype.maya.plugin class MayaAsciiLoader(pype.maya.plugin.ReferenceLoader): """Load the model""" - families = ["pype.mayaAscii"] + families = ["studio.mayaAscii"] representations = ["ma"] label = "Reference Maya Ascii" diff --git a/pype/plugins/maya/load/load_model.py b/pype/plugins/maya/load/load_model.py index 145a1a3e00..c548e9f8e7 100644 --- a/pype/plugins/maya/load/load_model.py +++ b/pype/plugins/maya/load/load_model.py @@ -5,7 +5,7 @@ import pype.maya.plugin class ModelLoader(pype.maya.plugin.ReferenceLoader): """Load the model""" - families = ["model"] + families = ["studio.model"] representations = ["ma"] label = "Reference Model" @@ -37,7 +37,7 @@ class ModelLoader(pype.maya.plugin.ReferenceLoader): class GpuCacheLoader(api.Loader): """Load model Alembic as gpuCache""" - families = ["model"] + families = ["studio.model"] representations = ["abc"] label = "Import Gpu Cache" diff --git a/pype/plugins/maya/load/load_rig.py b/pype/plugins/maya/load/load_rig.py index aa40ca3cc2..e37e6b33dd 100644 --- a/pype/plugins/maya/load/load_rig.py +++ b/pype/plugins/maya/load/load_rig.py @@ -11,7 +11,7 @@ class RigLoader(pype.maya.plugin.ReferenceLoader): """ - families = ["rig"] + families = ["studio.rig"] representations = ["ma"] label = "Reference rig" @@ -62,7 +62,7 @@ class RigLoader(pype.maya.plugin.ReferenceLoader): cmds.select([output, controls] + roots, noExpand=True) api.create(name=namespace, asset=asset, - family="animation", + family="studio.animation", options={"useSelection": True}, data={"dependencies": dependency}) diff --git a/pype/plugins/maya/load/load_setdress.py b/pype/plugins/maya/load/load_setdress.py index e0ab27212c..ecba2466d1 100644 --- a/pype/plugins/maya/load/load_setdress.py +++ b/pype/plugins/maya/load/load_setdress.py @@ -3,7 +3,7 @@ from avalon import api class SetDressLoader(api.Loader): - families = ["setdress"] + families = ["studio.setdress"] representations = ["json"] label = "Load Set Dress" @@ -23,7 +23,7 @@ class SetDressLoader(api.Loader): suffix="_", ) - from config import setdress_api + from pype import setdress_api containers = setdress_api.load_package(filepath=self.fname, name=name, @@ -45,7 +45,7 @@ class SetDressLoader(api.Loader): def update(self, container, representation): - from config import setdress_api + from pype import setdress_api return setdress_api.update_package(container, representation) @@ -53,7 +53,7 @@ class SetDressLoader(api.Loader): """Remove all sub containers""" from avalon import api - from config import setdress_api + from pype import setdress_api import maya.cmds as cmds # Remove all members diff --git a/pype/plugins/maya/load/load_vdb_to_redshift.py b/pype/plugins/maya/load/load_vdb_to_redshift.py new file mode 100644 index 0000000000..cb0d708623 --- /dev/null +++ b/pype/plugins/maya/load/load_vdb_to_redshift.py @@ -0,0 +1,69 @@ +from avalon import api + + +class LoadVDBtoRedShift(api.Loader): + """Load OpenVDB in a Redshift Volume Shape""" + + families = ["studio.vdbcache"] + representations = ["vdb"] + + label = "Load VDB to RedShift" + icon = "cloud" + color = "orange" + + def load(self, context, name=None, namespace=None, data=None): + + from maya import cmds + import avalon.maya.lib as lib + from avalon.maya.pipeline import containerise + + # Check if the plugin for redshift is available on the pc + try: + cmds.loadPlugin("redshift4maya", quiet=True) + except Exception as exc: + self.log.error("Encountered exception:\n%s" % exc) + return + + # Check if viewport drawing engine is Open GL Core (compat) + render_engine = None + compatible = "OpenGL" + if cmds.optionVar(exists="vp2RenderingEngine"): + render_engine = cmds.optionVar(query="vp2RenderingEngine") + + if not render_engine or not render_engine.startswith(compatible): + raise RuntimeError("Current scene's settings are incompatible." + "See Preferences > Display > Viewport 2.0 to " + "set the render engine to '%s'" + % compatible) + + asset = context['asset'] + + asset_name = asset["name"] + namespace = namespace or lib.unique_namespace( + asset_name + "_", + prefix="_" if asset_name[0].isdigit() else "", + suffix="_", + ) + + # Root group + label = "{}:{}".format(namespace, name) + root = cmds.group(name=label, empty=True) + + # Create VR + volume_node = cmds.createNode("RedshiftVolumeShape", + name="{}RVSShape".format(label), + parent=root) + + cmds.setAttr("{}.fileName".format(volume_node), + self.fname, + type="string") + + nodes = [root, volume_node] + self[:] = nodes + + return containerise( + name=name, + namespace=namespace, + nodes=nodes, + context=context, + loader=self.__class__.__name__) diff --git a/pype/plugins/maya/load/load_vdb_to_vray.py b/pype/plugins/maya/load/load_vdb_to_vray.py new file mode 100644 index 0000000000..68d6cf755a --- /dev/null +++ b/pype/plugins/maya/load/load_vdb_to_vray.py @@ -0,0 +1,62 @@ +from avalon import api + + +class LoadVDBtoVRay(api.Loader): + + families = ["studio.vdbcache"] + representations = ["vdb"] + + label = "Load VDB to VRay" + icon = "cloud" + color = "orange" + + def load(self, context, name, namespace, data): + + from maya import cmds + import avalon.maya.lib as lib + from avalon.maya.pipeline import containerise + + # Check if viewport drawing engine is Open GL Core (compat) + render_engine = None + compatible = "OpenGLCoreProfileCompat" + if cmds.optionVar(exists="vp2RenderingEngine"): + render_engine = cmds.optionVar(query="vp2RenderingEngine") + + if not render_engine or render_engine != compatible: + raise RuntimeError("Current scene's settings are incompatible." + "See Preferences > Display > Viewport 2.0 to " + "set the render engine to '%s'" % compatible) + + asset = context['asset'] + version = context["version"] + + asset_name = asset["name"] + namespace = namespace or lib.unique_namespace( + asset_name + "_", + prefix="_" if asset_name[0].isdigit() else "", + suffix="_", + ) + + # Root group + label = "{}:{}".format(namespace, name) + root = cmds.group(name=label, empty=True) + + # Create VR + grid_node = cmds.createNode("VRayVolumeGrid", + name="{}VVGShape".format(label), + parent=root) + + # Set attributes + cmds.setAttr("{}.inFile".format(grid_node), self.fname, type="string") + cmds.setAttr("{}.inReadOffset".format(grid_node), + version["startFrames"]) + + nodes = [root, grid_node] + self[:] = nodes + + return containerise( + name=name, + namespace=namespace, + nodes=nodes, + context=context, + loader=self.__class__.__name__) diff --git a/pype/plugins/maya/load/load_vrayproxy.py b/pype/plugins/maya/load/load_vrayproxy.py index 1ee9151ae2..8ac5fb4954 100644 --- a/pype/plugins/maya/load/load_vrayproxy.py +++ b/pype/plugins/maya/load/load_vrayproxy.py @@ -7,7 +7,7 @@ import maya.cmds as cmds class VRayProxyLoader(api.Loader): """Load VRayMesh proxy""" - families = ["vrayproxy"] + families = ["studio.vrayproxy"] representations = ["vrmesh"] label = "Import VRay Proxy" @@ -101,10 +101,12 @@ class VRayProxyLoader(api.Loader): # Create nodes vray_mesh = cmds.createNode('VRayMesh', name="{}_VRMS".format(name)) mesh_shape = cmds.createNode("mesh", name="{}_GEOShape".format(name)) - vray_mat = cmds.createNode("VRayMeshMaterial", + vray_mat = cmds.shadingNode("VRayMeshMaterial", asShader=True, name="{}_VRMM".format(name)) - vray_mat_sg = cmds.createNode("shadingEngine", - name="{}_VRSG".format(name)) + vray_mat_sg = cmds.sets(name="{}_VRSG".format(name), + empty=True, + renderable=True, + noSurfaceShader=True) cmds.setAttr("{}.fileName".format(vray_mesh), filename, diff --git a/pype/plugins/maya/load/load_yeti_cache.py b/pype/plugins/maya/load/load_yeti_cache.py index 3942a64b49..972d83d79f 100644 --- a/pype/plugins/maya/load/load_yeti_cache.py +++ b/pype/plugins/maya/load/load_yeti_cache.py @@ -13,7 +13,7 @@ from pype.maya import lib class YetiCacheLoader(api.Loader): - families = ["yeticache", "yetiRig"] + families = ["studio.yeticache", "studio.yetiRig"] representations = ["fur"] label = "Load Yeti Cache" @@ -284,6 +284,8 @@ class YetiCacheLoader(api.Loader): # Apply attributes to pgYetiMaya node for attr, value in attributes.items(): + if value is None: + continue lib.set_attribute(attr, value, yeti_node) # Fix for : YETI-6 diff --git a/pype/plugins/maya/load/load_yeti_rig.py b/pype/plugins/maya/load/load_yeti_rig.py index 096b936b41..f942d15a8b 100644 --- a/pype/plugins/maya/load/load_yeti_rig.py +++ b/pype/plugins/maya/load/load_yeti_rig.py @@ -3,7 +3,7 @@ import pype.maya.plugin class YetiRigLoader(pype.maya.plugin.ReferenceLoader): - families = ["yetiRig"] + families = ["studio.yetiRig"] representations = ["ma"] label = "Load Yeti Rig" diff --git a/pype/plugins/maya/publish/collect_animation.py b/pype/plugins/maya/publish/collect_animation.py index 156def82bc..6eef4f4059 100644 --- a/pype/plugins/maya/publish/collect_animation.py +++ b/pype/plugins/maya/publish/collect_animation.py @@ -16,7 +16,7 @@ class CollectAnimationOutputGeometry(pyblish.api.InstancePlugin): """ order = pyblish.api.CollectorOrder + 0.4 - families = ["animation"] + families = ["studio.animation"] label = "Collect Animation Output Geometry" hosts = ["maya"] @@ -29,8 +29,11 @@ class CollectAnimationOutputGeometry(pyblish.api.InstancePlugin): out_set = next((i for i in instance.data["setMembers"] if i.endswith("out_SET")), None) - assert out_set, ("Expecting out_SET for instance of family" - " '%s'" % family) + if out_set is None: + warning = "Expecting out_SET for instance of family '%s'" % family + self.log.warning(warning) + return + members = cmds.ls(cmds.sets(out_set, query=True), long=True) # Get all the relatives of the members diff --git a/pype/plugins/maya/publish/collect_history.py b/pype/plugins/maya/publish/collect_history.py index 16c8e4342e..57af4eaf80 100644 --- a/pype/plugins/maya/publish/collect_history.py +++ b/pype/plugins/maya/publish/collect_history.py @@ -17,7 +17,7 @@ class CollectMayaHistory(pyblish.api.InstancePlugin): order = pyblish.api.CollectorOrder + 0.1 hosts = ["maya"] label = "Maya History" - families = ["rig"] + families = ["studio.rig"] verbose = False def process(self, instance): diff --git a/pype/plugins/maya/publish/collect_instances.py b/pype/plugins/maya/publish/collect_instances.py index e40893c71c..807b57c710 100644 --- a/pype/plugins/maya/publish/collect_instances.py +++ b/pype/plugins/maya/publish/collect_instances.py @@ -103,10 +103,24 @@ class CollectInstances(pyblish.api.ContextPlugin): members_hierarchy = list(set(members + children + parents)) # Create the instance - name = cmds.ls(objset, long=False)[0] # use short name - instance = context.create_instance(data.get("name", name)) + instance = context.create_instance(objset) instance[:] = members_hierarchy + + # 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"]) + + # Append start frame and end frame to label if present + if "startFrame" and "endFrame" in data: + label += " [{0}-{1}]".format(int(data["startFrame"]), + int(data["endFrame"])) + + instance.data["label"] = label + instance.data.update(data) # Produce diagnostic message for any graphical diff --git a/pype/plugins/maya/publish/collect_look.py b/pype/plugins/maya/publish/collect_look.py index f7a9c68f96..06858b39a0 100644 --- a/pype/plugins/maya/publish/collect_look.py +++ b/pype/plugins/maya/publish/collect_look.py @@ -1,7 +1,10 @@ +import re +import os +import glob + from maya import cmds import pyblish.api import pype.maya.lib as lib -from cb.utils.maya import context, shaders SHAPE_ATTRS = ["castsShadows", "receiveShadows", @@ -48,6 +51,139 @@ def get_look_attrs(node): return result +def node_uses_image_sequence(node): + """Return whether file node uses an image sequence or single image. + + Determine if a node uses an image sequence or just a single image, + not always obvious from its file path alone. + + Args: + node (str): Name of the Maya node + + Returns: + bool: True if node uses an image sequence + + """ + + # useFrameExtension indicates an explicit image sequence + node_path = get_file_node_path(node).lower() + + # The following tokens imply a sequence + patterns = ["", "", "", "u_v", ".tif will return as /path/to/texture.*.tif. + + Args: + path (str): the image sequence path + + Returns: + str: Return glob string that matches the filename pattern. + + """ + + if path is None: + return path + + # If any of the patterns, convert the pattern + patterns = { + "": "", + "": "", + "": "", + "#": "#", + "u_v": "|", + "", + "": "" + } + + lower = path.lower() + has_pattern = False + for pattern, regex_pattern in patterns.items(): + if pattern in lower: + path = re.sub(regex_pattern, "*", path, flags=re.IGNORECASE) + has_pattern = True + + if has_pattern: + return path + + base = os.path.basename(path) + matches = list(re.finditer(r'\d+', base)) + if matches: + match = matches[-1] + new_base = '{0}*{1}'.format(base[:match.start()], + base[match.end():]) + head = os.path.dirname(path) + return os.path.join(head, new_base) + else: + return path + + +def get_file_node_path(node): + """Get the file path used by a Maya file node. + + Args: + node (str): Name of the Maya file node + + Returns: + str: the file path in use + + """ + # if the path appears to be sequence, use computedFileTextureNamePattern, + # this preserves the <> tag + if cmds.attributeQuery('computedFileTextureNamePattern', + node=node, + exists=True): + plug = '{0}.computedFileTextureNamePattern'.format(node) + texture_pattern = cmds.getAttr(plug) + + patterns = ["", + "", + "u_v", + "", + ""] + lower = texture_pattern.lower() + if any(pattern in lower for pattern in patterns): + return texture_pattern + + # otherwise use fileTextureName + return cmds.getAttr('{0}.fileTextureName'.format(node)) + + +def get_file_node_files(node): + """Return the file paths related to the file node + + Note: + Will only return existing files. Returns an empty list + if not valid existing files are linked. + + Returns: + list: List of full file paths. + + """ + + path = get_file_node_path(node) + path = cmds.workspace(expandName=path) + if node_uses_image_sequence(node): + glob_pattern = seq_to_glob(path) + return glob.glob(glob_pattern) + elif os.path.exists(path): + return [path] + else: + return [] + + class CollectLook(pyblish.api.InstancePlugin): """Collect look data for instance. @@ -67,14 +203,14 @@ class CollectLook(pyblish.api.InstancePlugin): """ order = pyblish.api.CollectorOrder + 0.4 - families = ["look"] + families = ["studio.look"] label = "Collect Look" hosts = ["maya"] def process(self, instance): """Collect the Look in the instance with the correct layer settings""" - with context.renderlayer(instance.data["renderlayer"]): + with lib.renderlayer(instance.data["renderlayer"]): self.collect(instance) def collect(self, instance): @@ -268,7 +404,7 @@ class CollectLook(pyblish.api.InstancePlugin): # paths as the computed patterns source = source.replace("\\", "/") - files = shaders.get_file_node_files(node) + files = get_file_node_files(node) if len(files) == 0: self.log.error("No valid files found from node `%s`" % node) diff --git a/pype/plugins/maya/publish/collect_model.py b/pype/plugins/maya/publish/collect_model.py index 47808934b3..c43595b97c 100644 --- a/pype/plugins/maya/publish/collect_model.py +++ b/pype/plugins/maya/publish/collect_model.py @@ -17,7 +17,7 @@ class CollectModelData(pyblish.api.InstancePlugin): order = pyblish.api.CollectorOrder + 0.499 label = 'Collect Model Data' - families = ["model"] + families = ["studio.model"] def process(self, instance): # Extract only current frame (override) diff --git a/pype/plugins/maya/publish/collect_render_layer_aovs.py b/pype/plugins/maya/publish/collect_render_layer_aovs.py index 3e5cb64fa6..a2f37ab024 100644 --- a/pype/plugins/maya/publish/collect_render_layer_aovs.py +++ b/pype/plugins/maya/publish/collect_render_layer_aovs.py @@ -28,7 +28,7 @@ class CollectRenderLayerAOVS(pyblish.api.InstancePlugin): order = pyblish.api.CollectorOrder + 0.01 label = "Render Elements / AOVs" hosts = ["maya"] - families = ["renderlayer"] + families = ["studio.renderlayer"] def process(self, instance): @@ -41,9 +41,9 @@ class CollectRenderLayerAOVS(pyblish.api.InstancePlugin): self.log.info("Renderer found: {}".format(renderer)) - rp_node_types = {"vray": "VRayRenderElement", - "arnold": "aiAOV", - "redshift": "RedshiftAOV"} + rp_node_types = {"vray": ["VRayRenderElement", "VRayRenderElementSet"], + "arnold": ["aiAOV"], + "redshift": ["RedshiftAOV"]} if renderer not in rp_node_types.keys(): self.log.error("Unsupported renderer found: '{}'".format(renderer)) @@ -52,7 +52,8 @@ class CollectRenderLayerAOVS(pyblish.api.InstancePlugin): result = [] # Collect all AOVs / Render Elements - with lib.renderlayer(instance.name): + layer = instance.data["setMembers"] + with lib.renderlayer(layer): node_type = rp_node_types[renderer] render_elements = cmds.ls(type=node_type) @@ -64,32 +65,36 @@ class CollectRenderLayerAOVS(pyblish.api.InstancePlugin): continue pass_name = self.get_pass_name(renderer, element) - render_pass = "%s.%s" % (instance.name, pass_name) + render_pass = "%s.%s" % (instance.data["subset"], pass_name) result.append(render_pass) self.log.info("Found {} render elements / AOVs for " - "'{}'".format(len(result), instance.name)) + "'{}'".format(len(result), instance.data["subset"])) instance.data["renderPasses"] = result def get_pass_name(self, renderer, node): if renderer == "vray": + + # Get render element pass type vray_node_attr = next(attr for attr in cmds.listAttr(node) if attr.startswith("vray_name")) - pass_type = vray_node_attr.rsplit("_", 1)[-1] + + # Support V-Ray extratex explicit name (if set by user) if pass_type == "extratex": - vray_node_attr = "vray_explicit_name_extratex" + explicit_attr = "{}.vray_explicit_name_extratex".format(node) + explicit_name = cmds.getAttr(explicit_attr) + if explicit_name: + return explicit_name # Node type is in the attribute name but we need to check if value # of the attribute as it can be changed - pass_name = cmds.getAttr("{}.{}".format(node, vray_node_attr)) + return cmds.getAttr("{}.{}".format(node, vray_node_attr)) elif renderer in ["arnold", "redshift"]: - pass_name = cmds.getAttr("{}.name".format(node)) + return cmds.getAttr("{}.name".format(node)) else: raise RuntimeError("Unsupported renderer: '{}'".format(renderer)) - - return pass_name \ No newline at end of file diff --git a/pype/plugins/maya/publish/collect_renderlayers.py b/pype/plugins/maya/publish/collect_renderlayers.py index 984c44098b..a12c780445 100644 --- a/pype/plugins/maya/publish/collect_renderlayers.py +++ b/pype/plugins/maya/publish/collect_renderlayers.py @@ -74,7 +74,7 @@ class CollectMayaRenderlayers(pyblish.api.ContextPlugin): # instance subset "family": "Render Layers", - "families": ["renderlayer"], + "families": ["studio.renderlayer"], "asset": asset, "time": api.time(), "author": context.data["user"], @@ -103,7 +103,13 @@ class CollectMayaRenderlayers(pyblish.api.ContextPlugin): overrides = self.parse_options(render_globals) data.update(**overrides) + # Define nice label + label = "{0} ({1})".format(layername, data["asset"]) + label += " [{0}-{1}]".format(int(data["startFrame"]), + int(data["endFrame"])) + instance = context.create_instance(layername) + instance.data["label"] = label instance.data.update(data) def get_render_attribute(self, attr): diff --git a/pype/plugins/maya/publish/collect_setdress.py b/pype/plugins/maya/publish/collect_setdress.py index bb56163293..9f6421b2b4 100644 --- a/pype/plugins/maya/publish/collect_setdress.py +++ b/pype/plugins/maya/publish/collect_setdress.py @@ -25,7 +25,7 @@ class CollectSetDress(pyblish.api.InstancePlugin): order = pyblish.api.CollectorOrder + 0.49 label = "Set Dress" - families = ["setdress"] + families = ["studio.setdress"] def process(self, instance): diff --git a/pype/plugins/maya/publish/collect_yeti_cache.py b/pype/plugins/maya/publish/collect_yeti_cache.py index 2365162c05..5d3f1b4bc5 100644 --- a/pype/plugins/maya/publish/collect_yeti_cache.py +++ b/pype/plugins/maya/publish/collect_yeti_cache.py @@ -28,7 +28,7 @@ class CollectYetiCache(pyblish.api.InstancePlugin): order = pyblish.api.CollectorOrder + 0.45 label = "Collect Yeti Cache" - families = ["yetiRig", "yeticache"] + families = ["studio.yetiRig", "studio.yeticache"] hosts = ["maya"] tasks = ["animation", "fx"] diff --git a/pype/plugins/maya/publish/collect_yeti_rig.py b/pype/plugins/maya/publish/collect_yeti_rig.py index 7e919fc746..f4672feb4b 100644 --- a/pype/plugins/maya/publish/collect_yeti_rig.py +++ b/pype/plugins/maya/publish/collect_yeti_rig.py @@ -21,25 +21,27 @@ class CollectYetiRig(pyblish.api.InstancePlugin): order = pyblish.api.CollectorOrder + 0.4 label = "Collect Yeti Rig" - families = ["yetiRig"] + families = ["studio.yetiRig"] hosts = ["maya"] def process(self, instance): - assert "input_SET" in cmds.sets(instance.name, query=True), ( + assert "input_SET" in instance.data["setMembers"], ( "Yeti Rig must have an input_SET") # Get the input meshes information - input_content = cmds.sets("input_SET", query=True) - input_nodes = cmds.listRelatives(input_content, - allDescendents=True, - fullPath=True) or input_content + input_content = cmds.ls(cmds.sets("input_SET", query=True), long=True) - # Get all the shapes - input_shapes = cmds.ls(input_nodes, long=True, noIntermediate=True) + # Include children + input_content += cmds.listRelatives(input_content, + allDescendents=True, + fullPath=True) or [] + + # Ignore intermediate objects + input_content = cmds.ls(input_content, long=True, noIntermediate=True) # Store all connections - connections = cmds.listConnections(input_shapes, + connections = cmds.listConnections(input_content, source=True, destination=False, connections=True, @@ -62,10 +64,9 @@ class CollectYetiRig(pyblish.api.InstancePlugin): # Collect any textures if used yeti_resources = [] - yeti_nodes = cmds.ls(instance[:], type="pgYetiMaya") + yeti_nodes = cmds.ls(instance[:], type="pgYetiMaya", long=True) for node in yeti_nodes: # Get Yeti resources (textures) - # TODO: referenced files in Yeti Graph resources = self.get_yeti_resources(node) yeti_resources.extend(resources) @@ -78,11 +79,16 @@ class CollectYetiRig(pyblish.api.InstancePlugin): instance.data["endFrame"] = 1 def get_yeti_resources(self, node): - """Get all texture file paths + """Get all resource file paths If a texture is a sequence it gathers all sibling files to ensure the texture sequence is complete. + References can be used in the Yeti graph, this means that it is + possible to load previously caches files. The information will need + to be stored and, if the file not publish, copied to the resource + folder. + Args: node (str): node name of the pgYetiMaya node @@ -91,15 +97,25 @@ class CollectYetiRig(pyblish.api.InstancePlugin): """ resources = [] image_search_path = cmds.getAttr("{}.imageSearchPath".format(node)) + + # List all related textures texture_filenames = cmds.pgYetiCommand(node, listTextures=True) + self.log.info("Found %i texture(s)" % len(texture_filenames)) + + # Get all reference nodes + reference_nodes = cmds.pgYetiGraph(node, + listNodes=True, + type="reference") + self.log.info("Found %i reference node(s)" % len(reference_nodes)) if texture_filenames and not image_search_path: raise ValueError("pgYetiMaya node '%s' is missing the path to the " "files in the 'imageSearchPath " "atttribute'" % node) + # Collect all texture files for texture in texture_filenames: - node_resources = {"files": [], "source": texture, "node": node} + item = {"files": [], "source": texture, "node": node} texture_filepath = os.path.join(image_search_path, texture) if len(texture.split(".")) > 2: @@ -107,20 +123,46 @@ class CollectYetiRig(pyblish.api.InstancePlugin): if "" in texture: sequences = self.get_sequence(texture_filepath, pattern="") - node_resources["files"].extend(sequences) + item["files"].extend(sequences) # Based textures (animated masks f.e) elif "%04d" in texture: sequences = self.get_sequence(texture_filepath, pattern="%04d") - node_resources["files"].extend(sequences) + item["files"].extend(sequences) # Assuming it is a fixed name else: - node_resources["files"].append(texture_filepath) + item["files"].append(texture_filepath) else: - node_resources["files"].append(texture_filepath) + item["files"].append(texture_filepath) - resources.append(node_resources) + resources.append(item) + + # Collect all referenced files + for reference_node in reference_nodes: + ref_file = cmds.pgYetiGraph(node, + node=reference_node, + param="reference_file", + getParamValue=True) + + if not os.path.isfile(ref_file): + raise RuntimeError("Reference file must be a full file path!") + + # Create resource dict + item = {"files": [], + "source": ref_file, + "node": node, + "graphnode": reference_node, + "param": "reference_file"} + + ref_file_name = os.path.basename(ref_file) + if "%04d" in ref_file_name: + ref_files = self.get_sequence(ref_file) + item["files"].extend(ref_files) + else: + item["files"].append(ref_file) + + resources.append(item) return resources @@ -139,7 +181,6 @@ class CollectYetiRig(pyblish.api.InstancePlugin): list: file sequence. """ - from avalon.vendor import clique escaped = re.escape(filename) @@ -150,7 +191,6 @@ class CollectYetiRig(pyblish.api.InstancePlugin): if re.match(re_pattern, f)] pattern = [clique.PATTERNS["frames"]] - collection, remainder = clique.assemble(files, - patterns=pattern) + collection, remainder = clique.assemble(files, patterns=pattern) return collection diff --git a/pype/plugins/maya/publish/extract_animation.py b/pype/plugins/maya/publish/extract_animation.py index 576991b0a9..2a24f7dde7 100644 --- a/pype/plugins/maya/publish/extract_animation.py +++ b/pype/plugins/maya/publish/extract_animation.py @@ -17,7 +17,7 @@ class ExtractColorbleedAnimation(pype.api.Extractor): label = "Extract Animation" hosts = ["maya"] - families = ["animation"] + families = ["studio.animation"] def process(self, instance): diff --git a/pype/plugins/maya/publish/extract_camera_alembic.py b/pype/plugins/maya/publish/extract_camera_alembic.py index 57038a2d6b..6ee4769440 100644 --- a/pype/plugins/maya/publish/extract_camera_alembic.py +++ b/pype/plugins/maya/publish/extract_camera_alembic.py @@ -5,7 +5,7 @@ from maya import cmds import avalon.maya import pype.api -import cb.utils.maya.context as context +import pype.maya.lib as lib class ExtractCameraAlembic(pype.api.Extractor): @@ -18,7 +18,7 @@ class ExtractCameraAlembic(pype.api.Extractor): label = "Camera (Alembic)" hosts = ["maya"] - families = ["camera"] + families = ["studio.camera"] def process(self, instance): @@ -66,8 +66,8 @@ class ExtractCameraAlembic(pype.api.Extractor): job_str += ' -file "{0}"'.format(path) - with context.evaluation("off"): - with context.no_refresh(): + with lib.evaluation("off"): + with lib.no_refresh(): cmds.AbcExport(j=job_str, verbose=False) if "files" not in instance.data: diff --git a/pype/plugins/maya/publish/extract_camera_mayaAscii.py b/pype/plugins/maya/publish/extract_camera_mayaAscii.py index f598b11046..3263373ee6 100644 --- a/pype/plugins/maya/publish/extract_camera_mayaAscii.py +++ b/pype/plugins/maya/publish/extract_camera_mayaAscii.py @@ -4,9 +4,8 @@ from maya import cmds import avalon.maya import pype.api - -import cb.utils.maya.context as context -from cb.utils.maya.animation import bakeToWorldSpace +from pype.lib import grouper +from pype.maya import lib def massage_ma_file(path): @@ -35,6 +34,37 @@ def massage_ma_file(path): f.close() +def unlock(plug): + """Unlocks attribute and disconnects inputs for a plug. + + This will also recursively unlock the attribute + upwards to any parent attributes for compound + attributes, to ensure it's fully unlocked and free + to change the value. + + """ + node, attr = plug.rsplit(".", 1) + + # Unlock attribute + cmds.setAttr(plug, lock=False) + + # Also unlock any parent attribute (if compound) + parents = cmds.attributeQuery(attr, node=node, listParent=True) + if parents: + for parent in parents: + unlock("{0}.{1}".format(node, parent)) + + # Break incoming connections + connections = cmds.listConnections(plug, + source=True, + destination=False, + plugs=True, + connections=True) + if connections: + for destination, source in grouper(connections, 2): + cmds.disconnectAttr(source, destination) + + class ExtractCameraMayaAscii(pype.api.Extractor): """Extract a Camera as Maya Ascii. @@ -53,7 +83,7 @@ class ExtractCameraMayaAscii(pype.api.Extractor): label = "Camera (Maya Ascii)" hosts = ["maya"] - families = ["camera"] + families = ["studio.camera"] def process(self, instance): @@ -67,8 +97,8 @@ class ExtractCameraMayaAscii(pype.api.Extractor): # TODO: Implement a bake to non-world space # Currently it will always bake the resulting camera to world-space # and it does not allow to include the parent hierarchy, even though - # with `bakeToWorldSpace` set to False it should include its hierarchy - # to be correct with the family implementation. + # with `bakeToWorldSpace` set to False it should include its + # hierarchy to be correct with the family implementation. if not bake_to_worldspace: self.log.warning("Camera (Maya Ascii) export only supports world" "space baked camera extractions. The disabled " @@ -96,17 +126,30 @@ class ExtractCameraMayaAscii(pype.api.Extractor): # Perform extraction self.log.info("Performing camera bakes for: {0}".format(transform)) with avalon.maya.maintained_selection(): - with context.evaluation("off"): - with context.no_refresh(): - baked = bakeToWorldSpace(transform, - frameRange=range_with_handles, - step=step) + with lib.evaluation("off"): + with lib.no_refresh(): + baked = lib.bake_to_world_space( + transform, + frame_range=range_with_handles, + step=step + ) baked_shapes = cmds.ls(baked, type="camera", dag=True, shapes=True, long=True) + # Fix PLN-178: Don't allow background color to be non-black + for cam in baked_shapes: + attrs = {"backgroundColorR": 0.0, + "backgroundColorG": 0.0, + "backgroundColorB": 0.0, + "overscan": 1.0} + for attr, value in attrs.items(): + plug = "{0}.{1}".format(cam, attr) + unlock(plug) + cmds.setAttr(plug, value) + self.log.info("Performing extraction..") cmds.select(baked_shapes, noExpand=True) cmds.file(path, diff --git a/pype/plugins/maya/publish/extract_look.py b/pype/plugins/maya/publish/extract_look.py index cd8fdae830..866f84e641 100644 --- a/pype/plugins/maya/publish/extract_look.py +++ b/pype/plugins/maya/publish/extract_look.py @@ -6,10 +6,9 @@ from maya import cmds import pyblish.api import avalon.maya -import pype.api -import pype.maya.lib as maya -from cb.utils.maya import context +import pype.api +import pype.maya.lib as lib class ExtractLook(pype.api.Extractor): @@ -23,7 +22,7 @@ class ExtractLook(pype.api.Extractor): label = "Extract Look (Maya ASCII + JSON)" hosts = ["maya"] - families = ["look"] + families = ["studio.look"] order = pyblish.api.ExtractorOrder + 0.2 def process(self, instance): @@ -63,10 +62,10 @@ class ExtractLook(pype.api.Extractor): # Extract in correct render layer layer = instance.data.get("renderlayer", "defaultRenderLayer") - with context.renderlayer(layer): + with lib.renderlayer(layer): # TODO: Ensure membership edits don't become renderlayer overrides - with context.empty_sets(sets, force=True): - with maya.attribute_values(remap): + with lib.empty_sets(sets, force=True): + with lib.attribute_values(remap): with avalon.maya.maintained_selection(): cmds.select(sets, noExpand=True) cmds.file(maya_path, diff --git a/pype/plugins/maya/publish/extract_maya_ascii_raw.py b/pype/plugins/maya/publish/extract_maya_ascii_raw.py index c998255569..d596ad1b8b 100644 --- a/pype/plugins/maya/publish/extract_maya_ascii_raw.py +++ b/pype/plugins/maya/publish/extract_maya_ascii_raw.py @@ -15,7 +15,7 @@ class ExtractMayaAsciiRaw(pype.api.Extractor): label = "Maya ASCII (Raw)" hosts = ["maya"] - families = ["pype.mayaAscii"] + families = ["studio.mayaAscii"] def process(self, instance): @@ -44,7 +44,10 @@ class ExtractMayaAsciiRaw(pype.api.Extractor): typ="mayaAscii", exportSelected=True, preserveReferences=True, - constructionHistory=True) + constructionHistory=True, + shader=True, + constraints=True, + expressions=True) if "files" not in instance.data: instance.data["files"] = list() diff --git a/pype/plugins/maya/publish/extract_model.py b/pype/plugins/maya/publish/extract_model.py index 70e92f036e..7e79a2983d 100644 --- a/pype/plugins/maya/publish/extract_model.py +++ b/pype/plugins/maya/publish/extract_model.py @@ -4,8 +4,7 @@ from maya import cmds import avalon.maya import pype.api - -from cb.utils.maya import context +import pype.maya.lib as lib class ExtractModel(pype.api.Extractor): @@ -25,7 +24,7 @@ class ExtractModel(pype.api.Extractor): label = "Model (Maya ASCII)" hosts = ["maya"] - families = ["model"] + families = ["studio.model"] def process(self, instance): @@ -47,15 +46,15 @@ class ExtractModel(pype.api.Extractor): noIntermediate=True, long=True) - with context.no_display_layers(instance): - with context.displaySmoothness(members, - divisionsU=0, - divisionsV=0, - pointsWire=4, - pointsShaded=1, - polygonObject=1): - with context.shader(members, - shadingEngine="initialShadingGroup"): + with lib.no_display_layers(instance): + with lib.displaySmoothness(members, + divisionsU=0, + divisionsV=0, + pointsWire=4, + pointsShaded=1, + polygonObject=1): + with lib.shader(members, + shadingEngine="initialShadingGroup"): with avalon.maya.maintained_selection(): cmds.select(members, noExpand=True) cmds.file(path, diff --git a/pype/plugins/maya/publish/extract_pointcache.py b/pype/plugins/maya/publish/extract_pointcache.py index c8a1b0bff0..9f8d32d3c9 100644 --- a/pype/plugins/maya/publish/extract_pointcache.py +++ b/pype/plugins/maya/publish/extract_pointcache.py @@ -17,8 +17,8 @@ class ExtractColorbleedAlembic(pype.api.Extractor): label = "Extract Pointcache (Alembic)" hosts = ["maya"] - families = ["pointcache", - "model"] + families = ["studio.pointcache", + "studio.model"] def process(self, instance): @@ -35,11 +35,9 @@ class ExtractColorbleedAlembic(pype.api.Extractor): # Get extra export arguments writeColorSets = instance.data.get("writeColorSets", False) - self.log.info("Extracting animation..") + self.log.info("Extracting pointcache..") dirname = self.staging_dir(instance) - self.log.info("nodes: %s" % str(nodes)) - parent_dir = self.staging_dir(instance) filename = "{name}.abc".format(**instance.data) path = os.path.join(parent_dir, filename) diff --git a/pype/plugins/maya/publish/extract_rig.py b/pype/plugins/maya/publish/extract_rig.py index 6a92111bc7..9d130c355f 100644 --- a/pype/plugins/maya/publish/extract_rig.py +++ b/pype/plugins/maya/publish/extract_rig.py @@ -11,7 +11,7 @@ class ExtractColorbleedRig(pype.api.Extractor): label = "Extract Rig (Maya ASCII)" hosts = ["maya"] - families = ["rig"] + families = ["studio.rig"] def process(self, instance): diff --git a/pype/plugins/maya/publish/extract_setdress.py b/pype/plugins/maya/publish/extract_setdress.py index c4d613dc61..27d8cf518f 100644 --- a/pype/plugins/maya/publish/extract_setdress.py +++ b/pype/plugins/maya/publish/extract_setdress.py @@ -18,7 +18,7 @@ class ExtractSetDress(pype.api.Extractor): label = "Extract Set Dress" hosts = ["maya"] - families = ["setdress"] + families = ["studio.setdress"] def process(self, instance): diff --git a/pype/plugins/maya/publish/extract_vrayproxy.py b/pype/plugins/maya/publish/extract_vrayproxy.py index 2106b404a3..2c5f267803 100644 --- a/pype/plugins/maya/publish/extract_vrayproxy.py +++ b/pype/plugins/maya/publish/extract_vrayproxy.py @@ -16,7 +16,7 @@ class ExtractVRayProxy(pype.api.Extractor): label = "VRay Proxy (.vrmesh)" hosts = ["maya"] - families = ["vrayproxy"] + families = ["studio.vrayproxy"] def process(self, instance): @@ -37,6 +37,8 @@ class ExtractVRayProxy(pype.api.Extractor): start_frame = instance.data["startFrame"] end_frame = instance.data["endFrame"] + vertex_colors = instance.data.get("vertexColors", False) + # Write out vrmesh file self.log.info("Writing: '%s'" % file_path) with avalon.maya.maintained_selection(): @@ -48,6 +50,7 @@ class ExtractVRayProxy(pype.api.Extractor): animType=3, startFrame=start_frame, endFrame=end_frame, + vertexColorsOn=vertex_colors, ignoreHiddenObjects=True, createProxyNode=False) diff --git a/pype/plugins/maya/publish/extract_yeti_cache.py b/pype/plugins/maya/publish/extract_yeti_cache.py index 086ee44352..b468dfe4cd 100644 --- a/pype/plugins/maya/publish/extract_yeti_cache.py +++ b/pype/plugins/maya/publish/extract_yeti_cache.py @@ -16,7 +16,7 @@ class ExtractYetiCache(pype.api.Extractor): label = "Extract Yeti Cache" hosts = ["maya"] - families = ["yetiRig", "yeticache"] + families = ["studio.yetiRig", "studio.yeticache"] def process(self, instance): @@ -37,6 +37,13 @@ class ExtractYetiCache(pype.api.Extractor): if preroll > 0: start_frame -= preroll + kwargs = {} + samples = instance.data.get("samples", 0) + if samples == 0: + kwargs.update({"sampleTimes": "0.0 1.0"}) + else: + kwargs.update({"samples": samples}) + self.log.info("Writing out cache") # Start writing the files for snap shot # will be replace by the Yeti node name @@ -44,9 +51,9 @@ class ExtractYetiCache(pype.api.Extractor): cmds.pgYetiCommand(yeti_nodes, writeCache=path, range=(start_frame, end_frame), - sampleTimes="0.0 1.0", updateViewport=False, - generatePreview=False) + generatePreview=False, + **kwargs) cache_files = [x for x in os.listdir(dirname) if x.endswith(".fur")] diff --git a/pype/plugins/maya/publish/extract_yeti_rig.py b/pype/plugins/maya/publish/extract_yeti_rig.py index 9f21c8b44a..6e8847995d 100644 --- a/pype/plugins/maya/publish/extract_yeti_rig.py +++ b/pype/plugins/maya/publish/extract_yeti_rig.py @@ -10,7 +10,7 @@ import pype.maya.lib as maya @contextlib.contextmanager -def disconnected_attributes(settings, members): +def disconnect_plugs(settings, members): members = cmds.ls(members, long=True) original_connections = [] @@ -19,35 +19,32 @@ def disconnected_attributes(settings, members): # Get source shapes source_nodes = lib.lsattr("cbId", input["sourceID"]) - sources = [i for i in source_nodes if - not cmds.referenceQuery(i, isNodeReferenced=True) - and i in members] - try: - source = sources[0] - except IndexError: - print("source_id:", input["sourceID"]) + if not source_nodes: continue + source = next(s for s in source_nodes if s not in members) + # Get destination shapes (the shapes used as hook up) destination_nodes = lib.lsattr("cbId", input["destinationID"]) - destinations = [i for i in destination_nodes if i not in members - and i not in sources] - destination = destinations[0] + destination = next(i for i in destination_nodes if i in members) - # Break connection + # Create full connection connections = input["connections"] src_attribute = "%s.%s" % (source, connections[0]) dst_attribute = "%s.%s" % (destination, connections[1]) - # store connection pair + # Check if there is an actual connection if not cmds.isConnected(src_attribute, dst_attribute): + print("No connection between %s and %s" % ( + src_attribute, dst_attribute)) continue + # Break and store connection cmds.disconnectAttr(src_attribute, dst_attribute) original_connections.append([src_attribute, dst_attribute]) yield finally: - # restore connections + # Restore previous connections for connection in original_connections: try: cmds.connectAttr(connection[0], connection[1]) @@ -56,17 +53,47 @@ def disconnected_attributes(settings, members): continue +@contextlib.contextmanager +def yetigraph_attribute_values(assumed_destination, resources): + + try: + for resource in resources: + if "graphnode" not in resource: + continue + + fname = os.path.basename(resource["source"]) + new_fpath = os.path.join(assumed_destination, fname) + new_fpath = new_fpath.replace("\\", "/") + + try: + cmds.pgYetiGraph(resource["node"], + node=resource["graphnode"], + param=resource["param"], + setParamValueString=new_fpath) + except Exception as exc: + print(">>> Exception:", exc) + yield + + finally: + for resource in resources: + if "graphnode" not in resources: + continue + + try: + cmds.pgYetiGraph(resource["node"], + node=resource["graphnode"], + param=resource["param"], + setParamValue=resource["source"]) + except RuntimeError: + pass + + class ExtractYetiRig(pype.api.Extractor): - """Produce an alembic of just point positions and normals. - - Positions and normals are preserved, but nothing more, - for plain and predictable point caches. - - """ + """Extract the Yeti rig to a MayaAscii and write the Yeti rig data""" label = "Extract Yeti Rig" hosts = ["maya"] - families = ["yetiRig"] + families = ["studio.yetiRig"] def process(self, instance): @@ -83,44 +110,49 @@ class ExtractYetiRig(pype.api.Extractor): self.log.info("Writing metadata file") - image_search_path = "" + # Create assumed destination folder for imageSearchPath + assumed_temp_data = instance.data["assumedTemplateData"] + template = instance.data["template"] + template_formatted = template.format(**assumed_temp_data) + + destination_folder = os.path.dirname(template_formatted) + + image_search_path = os.path.join(destination_folder, "resources") + image_search_path = os.path.normpath(image_search_path) + settings = instance.data.get("rigsettings", None) - if settings is not None: - - # Create assumed destination folder for imageSearchPath - assumed_temp_data = instance.data["assumedTemplateData"] - template = instance.data["template"] - template_formatted = template.format(**assumed_temp_data) - - destination_folder = os.path.dirname(template_formatted) - image_search_path = os.path.join(destination_folder, "resources") - image_search_path = os.path.normpath(image_search_path) - + if settings: settings["imageSearchPath"] = image_search_path with open(settings_path, "w") as fp: json.dump(settings, fp, ensure_ascii=False) + # Ensure the imageSearchPath is being remapped to the publish folder attr_value = {"%s.imageSearchPath" % n: str(image_search_path) for n in yeti_nodes} # Get input_SET members - input_set = [i for i in instance if i == "input_SET"] + input_set = next(i for i in instance if i == "input_SET") + # Get all items - set_members = cmds.sets(input_set[0], query=True) - members = cmds.listRelatives(set_members, ad=True, fullPath=True) or [] - members += cmds.ls(set_members, long=True) + set_members = cmds.sets(input_set, query=True) + set_members += cmds.listRelatives(set_members, + allDescendents=True, + fullPath=True) or [] + members = cmds.ls(set_members, long=True) nodes = instance.data["setMembers"] - with disconnected_attributes(settings, members): - with maya.attribute_values(attr_value): - cmds.select(nodes, noExpand=True) - cmds.file(maya_path, - force=True, - exportSelected=True, - typ="mayaAscii", - preserveReferences=False, - constructionHistory=True, - shader=False) + resources = instance.data.get("resources", {}) + with disconnect_plugs(settings, members): + with yetigraph_attribute_values(destination_folder, resources): + with maya.attribute_values(attr_value): + cmds.select(nodes, noExpand=True) + cmds.file(maya_path, + force=True, + exportSelected=True, + typ="mayaAscii", + preserveReferences=False, + constructionHistory=True, + shader=False) # Ensure files can be stored if "files" not in instance.data: diff --git a/pype/plugins/maya/publish/increment_current_file_deadline.py b/pype/plugins/maya/publish/increment_current_file_deadline.py index 21d4b2fc7a..4f02f707fe 100644 --- a/pype/plugins/maya/publish/increment_current_file_deadline.py +++ b/pype/plugins/maya/publish/increment_current_file_deadline.py @@ -11,7 +11,7 @@ class IncrementCurrentFileDeadline(pyblish.api.ContextPlugin): label = "Increment current file" order = pyblish.api.IntegratorOrder + 9.0 hosts = ["maya"] - families = ["renderlayer"] + families = ["studio.renderlayer"] optional = True def process(self, context): diff --git a/pype/plugins/maya/publish/save_scene.py b/pype/plugins/maya/publish/save_scene.py index d3e1597e78..ac84c6ac66 100644 --- a/pype/plugins/maya/publish/save_scene.py +++ b/pype/plugins/maya/publish/save_scene.py @@ -9,7 +9,7 @@ class SaveCurrentScene(pyblish.api.ContextPlugin): label = "Save current file" order = pyblish.api.IntegratorOrder - 0.49 hosts = ["maya"] - families = ["renderlayer"] + families = ["studio.renderlayer"] def process(self, context): import maya.cmds as cmds diff --git a/pype/plugins/maya/publish/submit_deadline.py b/pype/plugins/maya/publish/submit_deadline.py index fa832b4154..2a4051283f 100644 --- a/pype/plugins/maya/publish/submit_deadline.py +++ b/pype/plugins/maya/publish/submit_deadline.py @@ -99,7 +99,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): label = "Submit to Deadline" order = pyblish.api.IntegratorOrder hosts = ["maya"] - families = ["renderlayer"] + families = ["studio.renderlayer"] def process(self, instance): @@ -115,7 +115,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): scene = os.path.splitext(filename)[0] dirname = os.path.join(workspace, "renders") renderlayer = instance.data['setMembers'] # rs_beauty - renderlayer_name = instance.name # beauty + renderlayer_name = instance.data['subset'] # beauty renderlayer_globals = instance.data["renderGlobals"] legacy_layers = renderlayer_globals["UseLegacyRenderLayers"] deadline_user = context.data.get("deadlineUser", getpass.getuser()) @@ -207,6 +207,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): # todo: This is a temporary fix for yeti variables "PEREGRINEL_LICENSE", "REDSHIFT_MAYAEXTENSIONSPATH", + "REDSHIFT_DISABLEOUTPUTLOCKFILES" "VRAY_FOR_MAYA2018_PLUGINS_X64", "VRAY_PLUGINS_X64", "VRAY_USE_THREAD_AFFINITY", @@ -230,7 +231,8 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): render_globals = instance.data.get("renderGlobals", {}) payload["JobInfo"].update(render_globals) - self.log.info("using render plugin : {}".format(payload["JobInfo"]["Plugin"])) + plugin = payload["JobInfo"]["Plugin"] + self.log.info("using render plugin : {}".format(plugin)) self.preflight_check(instance) diff --git a/pype/plugins/maya/publish/validate_animation_content.py b/pype/plugins/maya/publish/validate_animation_content.py index 194b6d2d10..ccdb476734 100644 --- a/pype/plugins/maya/publish/validate_animation_content.py +++ b/pype/plugins/maya/publish/validate_animation_content.py @@ -1,5 +1,6 @@ import pyblish.api import pype.api +import pype.maya.action class ValidateAnimationContent(pyblish.api.InstancePlugin): @@ -12,12 +13,21 @@ class ValidateAnimationContent(pyblish.api.InstancePlugin): order = pype.api.ValidateContentsOrder hosts = ["maya"] - families = ["animation"] + families = ["studio.animation"] label = "Animation Content" - actions = [pype.api.SelectInvalidAction] + actions = [pype.maya.action.SelectInvalidAction] @classmethod def get_invalid(cls, instance): + + out_set = next((i for i in instance.data["setMembers"] if + i.endswith("out_SET")), None) + + assert out_set, ("Instance '%s' has no objectSet named: `OUT_set`. " + "If this instance is an unloaded reference, " + "please deactivate by toggling the 'Active' attribute" + % instance.name) + assert 'out_hierarchy' in instance.data, "Missing `out_hierarchy` data" # All nodes in the `out_hierarchy` must be among the nodes that are diff --git a/pype/plugins/maya/publish/validate_animation_out_set_related_node_ids.py b/pype/plugins/maya/publish/validate_animation_out_set_related_node_ids.py index 8d14c87862..8af541f0cd 100644 --- a/pype/plugins/maya/publish/validate_animation_out_set_related_node_ids.py +++ b/pype/plugins/maya/publish/validate_animation_out_set_related_node_ids.py @@ -2,6 +2,7 @@ import maya.cmds as cmds import pyblish.api import pype.api +import pype.maya.action import pype.maya.lib as lib @@ -16,10 +17,10 @@ class ValidateOutRelatedNodeIds(pyblish.api.InstancePlugin): """ order = pype.api.ValidateContentsOrder - families = ['studio.animation', "pointcache"] + families = ["studio.animation', "studio.pointcache"] hosts = ['maya'] label = 'Animation Out Set Related Node Ids' - actions = [pype.api.SelectInvalidAction, pype.api.RepairAction] + actions = [pype.maya.action.SelectInvalidAction, pype.api.RepairAction] def process(self, instance): """Process all meshes""" diff --git a/pype/plugins/maya/publish/validate_camera_attributes.py b/pype/plugins/maya/publish/validate_camera_attributes.py index 1de0abc74a..f71994b8e1 100644 --- a/pype/plugins/maya/publish/validate_camera_attributes.py +++ b/pype/plugins/maya/publish/validate_camera_attributes.py @@ -2,6 +2,7 @@ from maya import cmds import pyblish.api import pype.api +import pype.maya.action class ValidateCameraAttributes(pyblish.api.InstancePlugin): @@ -14,10 +15,10 @@ class ValidateCameraAttributes(pyblish.api.InstancePlugin): """ order = pype.api.ValidateContentsOrder - families = ['studio.camera'] + families = ["studio.camera'] hosts = ['maya'] label = 'Camera Attributes' - actions = [pype.api.SelectInvalidAction] + actions = [pype.maya.action.SelectInvalidAction] DEFAULTS = [ ("filmFitOffset", 0.0), diff --git a/pype/plugins/maya/publish/validate_camera_contents.py b/pype/plugins/maya/publish/validate_camera_contents.py index bf3c4659f0..10dd5e922c 100644 --- a/pype/plugins/maya/publish/validate_camera_contents.py +++ b/pype/plugins/maya/publish/validate_camera_contents.py @@ -2,6 +2,7 @@ from maya import cmds import pyblish.api import pype.api +import pype.maya.action class ValidateCameraContents(pyblish.api.InstancePlugin): @@ -9,16 +10,16 @@ class ValidateCameraContents(pyblish.api.InstancePlugin): A Camera instance may only hold a SINGLE camera's transform, nothing else. - It may hold a "locator" as shape, but different shapes are down the + It may hold a "locator" as shape, but different shapes are down the hierarchy. """ order = pype.api.ValidateContentsOrder - families = ['studio.camera'] + families = ["studio.camera'] hosts = ['maya'] label = 'Camera Contents' - actions = [pype.api.SelectInvalidAction] + actions = [pype.maya.action.SelectInvalidAction] @classmethod def get_invalid(cls, instance): diff --git a/pype/plugins/maya/publish/validate_current_renderlayer_renderable.py b/pype/plugins/maya/publish/validate_current_renderlayer_renderable.py new file mode 100644 index 0000000000..f4dbe3136c --- /dev/null +++ b/pype/plugins/maya/publish/validate_current_renderlayer_renderable.py @@ -0,0 +1,28 @@ +import pyblish.api + +from maya import cmds + + +class ValidateCurrentRenderLayerIsRenderable(pyblish.api.ContextPlugin): + """Validate if current render layer has a renderable camera + + There is a bug in Redshift which occurs when the current render layer + at file open has no renderable camera. The error raised is as follows: + + "No renderable cameras found. Aborting render" + + This error is raised even if that render layer will not be rendered. + + """ + + label = "Current Render Layer Has Renderable Camera" + order = pyblish.api.ValidatorOrder + hosts = ["maya"] + families = ["studio.renderlayer"] + + def process(self, instance): + layer = cmds.editRenderLayerGlobals(query=True, currentRenderLayer=True) + cameras = cmds.ls(type="camera", long=True) + renderable = any(c for c in cameras if cmds.getAttr(c + ".renderable")) + assert renderable, ("Current render layer '%s' has no renderable " + "camera" % layer) diff --git a/pype/plugins/maya/publish/validate_deadline_connection.py b/pype/plugins/maya/publish/validate_deadline_connection.py index b416712d9e..bde75d6a68 100644 --- a/pype/plugins/maya/publish/validate_deadline_connection.py +++ b/pype/plugins/maya/publish/validate_deadline_connection.py @@ -10,7 +10,7 @@ class ValidateDeadlineConnection(pyblish.api.ContextPlugin): label = "Validate Deadline Web Service" order = pyblish.api.ValidatorOrder hosts = ["maya"] - families = ["renderlayer"] + families = ["studio.renderlayer"] def process(self, instance): @@ -22,5 +22,6 @@ class ValidateDeadlineConnection(pyblish.api.ContextPlugin): # Check response response = requests.get(AVALON_DEADLINE) assert response.ok, "Response must be ok" - assert response.text.startswith("Deadline Web Service "), \ - "Web service did not respond with 'Deadline Web Service'" \ No newline at end of file + assert response.text.startswith("Deadline Web Service "), ( + "Web service did not respond with 'Deadline Web Service'" + ) \ No newline at end of file diff --git a/pype/plugins/maya/publish/validate_frame_range.py b/pype/plugins/maya/publish/validate_frame_range.py index 64ec9df381..9ed8e82ec0 100644 --- a/pype/plugins/maya/publish/validate_frame_range.py +++ b/pype/plugins/maya/publish/validate_frame_range.py @@ -17,10 +17,11 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): label = "Validate Frame Range" order = pype.api.ValidateContentsOrder - families = ["animation", - "pointcache", - "camera", - "renderlayer"] + families = ["studio.animation", + "studio.pointcache", + "studio.camera", + "studio.renderlayer", + "oolorbleed.vrayproxy"] def process(self, instance): diff --git a/pype/plugins/maya/publish/validate_instance_has_members.py b/pype/plugins/maya/publish/validate_instance_has_members.py index 1d9fc93295..76c93b4c85 100644 --- a/pype/plugins/maya/publish/validate_instance_has_members.py +++ b/pype/plugins/maya/publish/validate_instance_has_members.py @@ -1,5 +1,6 @@ import pyblish.api import pype.api +import pype.maya.action class ValidateInstanceHasMembers(pyblish.api.InstancePlugin): @@ -8,7 +9,7 @@ class ValidateInstanceHasMembers(pyblish.api.InstancePlugin): order = pype.api.ValidateContentsOrder hosts = ["maya"] label = 'Instance has members' - actions = [pype.api.SelectInvalidAction] + actions = [pype.maya.action.SelectInvalidAction] @classmethod def get_invalid(cls, instance): diff --git a/pype/plugins/maya/publish/validate_instancer_content.py b/pype/plugins/maya/publish/validate_instancer_content.py index 8c24374b60..913a3aa0cb 100644 --- a/pype/plugins/maya/publish/validate_instancer_content.py +++ b/pype/plugins/maya/publish/validate_instancer_content.py @@ -12,7 +12,7 @@ class ValidateInstancerContent(pyblish.api.InstancePlugin): """ order = pyblish.api.ValidatorOrder label = 'Instancer Content' - families = ['studio.instancer'] + families = ["studio.instancer'] def process(self, instance): diff --git a/pype/plugins/maya/publish/validate_instancer_frame_ranges.py b/pype/plugins/maya/publish/validate_instancer_frame_ranges.py index ae4cd83f88..dab1c50816 100644 --- a/pype/plugins/maya/publish/validate_instancer_frame_ranges.py +++ b/pype/plugins/maya/publish/validate_instancer_frame_ranges.py @@ -44,7 +44,7 @@ class ValidateInstancerFrameRanges(pyblish.api.InstancePlugin): """ order = pyblish.api.ValidatorOrder label = 'Instancer Cache Frame Ranges' - families = ['studio.instancer'] + families = ["studio.instancer'] @classmethod def get_invalid(cls, instance): diff --git a/pype/plugins/maya/publish/validate_joints_hidden.py b/pype/plugins/maya/publish/validate_joints_hidden.py index 249ae5627b..cb408ca129 100644 --- a/pype/plugins/maya/publish/validate_joints_hidden.py +++ b/pype/plugins/maya/publish/validate_joints_hidden.py @@ -2,6 +2,7 @@ from maya import cmds import pyblish.api import pype.api +import pype.maya.action import pype.maya.lib as lib @@ -18,11 +19,11 @@ class ValidateJointsHidden(pyblish.api.InstancePlugin): order = pype.api.ValidateContentsOrder hosts = ['maya'] - families = ['studio.rig'] + families = ["studio.rig'] category = 'rig' version = (0, 1, 0) label = "Joints Hidden" - actions = [pype.api.SelectInvalidAction, + actions = [pype.maya.action.SelectInvalidAction, pype.api.RepairAction] @staticmethod diff --git a/pype/plugins/maya/publish/validate_look_contents.py b/pype/plugins/maya/publish/validate_look_contents.py index 8cddb865ad..01ceb41263 100644 --- a/pype/plugins/maya/publish/validate_look_contents.py +++ b/pype/plugins/maya/publish/validate_look_contents.py @@ -1,5 +1,6 @@ import pyblish.api import pype.api +import pype.maya.action class ValidateLookContents(pyblish.api.InstancePlugin): @@ -17,10 +18,10 @@ class ValidateLookContents(pyblish.api.InstancePlugin): """ order = pype.api.ValidateContentsOrder - families = ['studio.look'] + families = ["studio.look'] hosts = ['maya'] label = 'Look Data Contents' - actions = [pype.api.SelectInvalidAction] + actions = [pype.maya.action.SelectInvalidAction] def process(self, instance): """Process all the nodes in the instance""" diff --git a/pype/plugins/maya/publish/validate_look_default_shaders_connections.py b/pype/plugins/maya/publish/validate_look_default_shaders_connections.py index 3a25c9ea62..86b1fe28f1 100644 --- a/pype/plugins/maya/publish/validate_look_default_shaders_connections.py +++ b/pype/plugins/maya/publish/validate_look_default_shaders_connections.py @@ -17,7 +17,7 @@ class ValidateLookDefaultShadersConnections(pyblish.api.InstancePlugin): """ order = pype.api.ValidateContentsOrder - families = ['studio.look'] + families = ["studio.look'] hosts = ['maya'] label = 'Look Default Shader Connections' diff --git a/pype/plugins/maya/publish/validate_look_id_reference_edits.py b/pype/plugins/maya/publish/validate_look_id_reference_edits.py new file mode 100644 index 0000000000..a3f39d498e --- /dev/null +++ b/pype/plugins/maya/publish/validate_look_id_reference_edits.py @@ -0,0 +1,98 @@ +from collections import defaultdict +from maya import cmds + +import pyblish.api +import pype.api +import pype.maya.action + + +class ValidateLookIdReferenceEdits(pyblish.api.InstancePlugin): + """Validate nodes in look have no reference edits to cbId. + + Note: + This only validates the cbId edits on the referenced nodes that are + used in the look. For example, a transform can have its cbId changed + without being invalidated when it is not used in the look's assignment. + + """ + + order = pype.api.ValidateContentsOrder + families = ["studio.look'] + hosts = ['maya'] + label = 'Look Id Reference Edits' + actions = [pype.maya.action.SelectInvalidAction, + pype.api.RepairAction] + + def process(self, instance): + invalid = self.get_invalid(instance) + + if invalid: + raise RuntimeError("Invalid nodes %s" % (invalid,)) + + @staticmethod + def get_invalid(instance): + + # Collect all referenced members + references = defaultdict(set) + relationships = instance.data["lookData"]["relationships"] + for relationship in relationships.values(): + for member in relationship['members']: + node = member["name"] + + if cmds.referenceQuery(node, isNodeReferenced=True): + ref = cmds.referenceQuery(node, referenceNode=True) + references[ref].add(node) + + # Validate whether any has changes to 'cbId' attribute + invalid = list() + for ref, nodes in references.items(): + edits = cmds.referenceQuery(editAttrs=True, + editNodes=True, + showDagPath=True, + showNamespace=True, + onReferenceNode=ref) + for edit in edits: + + # Ensure it is an attribute ending with .cbId + # thus also ignore just node edits (like parenting) + if not edit.endswith(".cbId"): + continue + + # Ensure the attribute is 'cbId' (and not a nested attribute) + node, attr = edit.split(".", 1) + if attr != "cbId": + continue + + if node in nodes: + invalid.append(node) + + return invalid + + @classmethod + def repair(cls, instance): + + invalid = cls.get_invalid(instance) + + # Group invalid nodes by reference node + references = defaultdict(set) + for node in invalid: + ref = cmds.referenceQuery(node, referenceNode=True) + references[ref].add(node) + + # Remove the reference edits on the nodes per reference node + for ref, nodes in references.items(): + for node in nodes: + + # Somehow this only works if you run the the removal + # per edit command. + for command in ["addAttr", + "connectAttr", + "deleteAttr", + "disconnectAttr", + "setAttr"]: + cmds.referenceEdit("{}.cbId".format(node), + removeEdits=True, + successfulEdits=True, + failedEdits=True, + editCommand=command, + onReferenceNode=ref) diff --git a/pype/plugins/maya/publish/validate_look_members_unique.py b/pype/plugins/maya/publish/validate_look_members_unique.py index 3b40d1eaff..4d786e1528 100644 --- a/pype/plugins/maya/publish/validate_look_members_unique.py +++ b/pype/plugins/maya/publish/validate_look_members_unique.py @@ -2,6 +2,7 @@ from collections import defaultdict import pyblish.api import pype.api +import pype.maya.action class ValidateUniqueRelationshipMembers(pyblish.api.InstancePlugin): @@ -22,10 +23,10 @@ class ValidateUniqueRelationshipMembers(pyblish.api.InstancePlugin): order = pype.api.ValidatePipelineOrder label = 'Look members unique' hosts = ['maya'] - families = ['studio.look'] + families = ["studio.look'] - actions = [pype.api.SelectInvalidAction, - pype.api.GenerateUUIDsOnInvalidAction] + actions = [pype.maya.action.SelectInvalidAction, + pype.maya.action.GenerateUUIDsOnInvalidAction] def process(self, instance): """Process all meshes""" diff --git a/pype/plugins/maya/publish/validate_look_no_default_shaders.py b/pype/plugins/maya/publish/validate_look_no_default_shaders.py index 039cb5b5f4..c5ce222d34 100644 --- a/pype/plugins/maya/publish/validate_look_no_default_shaders.py +++ b/pype/plugins/maya/publish/validate_look_no_default_shaders.py @@ -2,6 +2,7 @@ from maya import cmds import pyblish.api import pype.api +import pype.maya.action class ValidateLookNoDefaultShaders(pyblish.api.InstancePlugin): @@ -23,10 +24,10 @@ class ValidateLookNoDefaultShaders(pyblish.api.InstancePlugin): """ order = pype.api.ValidateContentsOrder + 0.01 - families = ['studio.look'] + families = ["studio.look'] hosts = ['maya'] label = 'Look No Default Shaders' - actions = [pype.api.SelectInvalidAction] + actions = [pype.maya.action.SelectInvalidAction] DEFAULT_SHADERS = {"lambert1", "initialShadingGroup", "initialParticleSE", "particleCloud1"} diff --git a/pype/plugins/maya/publish/validate_look_sets.py b/pype/plugins/maya/publish/validate_look_sets.py index 16bc75e42d..7ad300e0ec 100644 --- a/pype/plugins/maya/publish/validate_look_sets.py +++ b/pype/plugins/maya/publish/validate_look_sets.py @@ -1,41 +1,46 @@ +import pype.maya.action from pype.maya import lib import pyblish.api import pype.api -from cb.utils.maya import context - class ValidateLookSets(pyblish.api.InstancePlugin): """Validate if any sets are missing from the instance and look data - A node might have a relationship with a shader but has no Colorbleed ID. + A shader can be assigned to a node that is missing a Colorbleed ID. Because it is missing the ID it has not been collected in the instance. + This validator ensures no relationships and thus considers it invalid + if a relationship was not collected. + When the relationship needs to be maintained the artist might need to create a different* relationship or ensure the node has the Colorbleed ID. - * The relationship might be too broad (assigned to top node if hierarchy). + *The relationship might be too broad (assigned to top node of hierarchy). This can be countered by creating the relationship on the shape or its - transform. - In essence, ensure item the shader is assigned to has the Colorbleed ID! + transform. In essence, ensure item the shader is assigned to has the + Colorbleed ID! - Displacement shaders: - Ensure all geometry is added to the displacement objectSet. - It is best practice to add the transform group of the shape to the - displacement objectSet - Example content: - [asset_GRP|geometry_GRP|body_GES, - asset_GRP|geometry_GRP|L_eye_GES, - asset_GRP|geometry_GRP|R_eye_GES, - asset_GRP|geometry_GRP|wings_GEO] + Examples: + + - Displacement objectSets (like V-Ray): + + It is best practice to add the transform group of the shape to the + displacement objectSet. + + Example content: + [asset_GRP|geometry_GRP|body_GES, + asset_GRP|geometry_GRP|L_eye_GES, + asset_GRP|geometry_GRP|R_eye_GES, + asset_GRP|geometry_GRP|wings_GEO] """ order = pype.api.ValidateContentsOrder - families = ['studio.look'] + families = ["studio.look'] hosts = ['maya'] label = 'Look Sets' - actions = [pype.api.SelectInvalidAction] + actions = [pype.maya.action.SelectInvalidAction] def process(self, instance): """Process all the nodes in the instance""" @@ -56,7 +61,7 @@ class ValidateLookSets(pyblish.api.InstancePlugin): invalid = [] renderlayer = instance.data.get("renderlayer", "defaultRenderLayer") - with context.renderlayer(renderlayer): + with lib.renderlayer(renderlayer): for node in instance: # get the connected objectSets of the node sets = lib.get_related_sets(node) @@ -91,7 +96,3 @@ class ValidateLookSets(pyblish.api.InstancePlugin): continue return invalid - - @classmethod - def repair(cls, context, instance): - pass diff --git a/pype/plugins/maya/publish/validate_look_single_shader.py b/pype/plugins/maya/publish/validate_look_single_shader.py index 990329710b..76a5479a02 100644 --- a/pype/plugins/maya/publish/validate_look_single_shader.py +++ b/pype/plugins/maya/publish/validate_look_single_shader.py @@ -2,6 +2,7 @@ from maya import cmds import pyblish.api import pype.api +import pype.maya.action class ValidateSingleShader(pyblish.api.InstancePlugin): @@ -12,10 +13,10 @@ class ValidateSingleShader(pyblish.api.InstancePlugin): """ order = pype.api.ValidateContentsOrder - families = ['studio.look'] + families = ["studio.look'] hosts = ['maya'] label = 'Look Single Shader Per Shape' - actions = [pype.api.SelectInvalidAction] + actions = [pype.maya.action.SelectInvalidAction] # The default connections to check def process(self, instance): diff --git a/pype/plugins/maya/publish/validate_maya_units.py b/pype/plugins/maya/publish/validate_maya_units.py index df2369b9e4..2659444184 100644 --- a/pype/plugins/maya/publish/validate_maya_units.py +++ b/pype/plugins/maya/publish/validate_maya_units.py @@ -2,7 +2,7 @@ import maya.cmds as cmds import pyblish.api import pype.api -from config import lib +from pype import lib import pype.maya.lib as mayalib @@ -16,11 +16,12 @@ class ValidateMayaUnits(pyblish.api.ContextPlugin): def process(self, context): + # Collected units linearunits = context.data('linearUnits') angularunits = context.data('angularUnits') - fps = context.data['fps'] - project_fps = lib.get_project_fps() + + asset_fps = lib.get_asset_fps() self.log.info('Units (linear): {0}'.format(linearunits)) self.log.info('Units (angular): {0}'.format(angularunits)) @@ -32,7 +33,7 @@ class ValidateMayaUnits(pyblish.api.ContextPlugin): assert angularunits and angularunits == 'deg', ("Scene angular units " "must be degrees") - assert fps and fps == project_fps, "Scene must be %s FPS" % project_fps + assert fps and fps == asset_fps, "Scene must be %s FPS" % asset_fps @classmethod def repair(cls): @@ -49,5 +50,5 @@ class ValidateMayaUnits(pyblish.api.ContextPlugin): cls.log.debug(current_linear) cls.log.info("Setting time unit to match project") - project_fps = lib.get_project_fps() - mayalib.set_scene_fps(project_fps) + asset_fps = lib.get_asset_fps() + mayalib.set_scene_fps(asset_fps) diff --git a/pype/plugins/maya/publish/validate_mesh_has_uv.py b/pype/plugins/maya/publish/validate_mesh_has_uv.py index 3ba52d69f3..388b9c15c3 100644 --- a/pype/plugins/maya/publish/validate_mesh_has_uv.py +++ b/pype/plugins/maya/publish/validate_mesh_has_uv.py @@ -4,6 +4,7 @@ from maya import cmds import pyblish.api import pype.api +import pype.maya.action def len_flattened(components): @@ -13,7 +14,7 @@ def len_flattened(components): when requesting with `maya.cmds.ls` without the `flatten` flag. Though enabling `flatten` on a large list (e.g. millions) will result in a slow result. This command will return the amount - of entries in a non-flattened list by parsing the result with + of entries in a non-flattened list by parsing the result with regex. Args: @@ -46,10 +47,10 @@ class ValidateMeshHasUVs(pyblish.api.InstancePlugin): order = pype.api.ValidateMeshOrder hosts = ['maya'] - families = ['studio.model'] + families = ["studio.model'] category = 'geometry' label = 'Mesh Has UVs' - actions = [pype.api.SelectInvalidAction] + actions = [pype.maya.action.SelectInvalidAction] optional = True @classmethod diff --git a/pype/plugins/maya/publish/validate_mesh_lamina_faces.py b/pype/plugins/maya/publish/validate_mesh_lamina_faces.py index 474a368116..57faec0154 100644 --- a/pype/plugins/maya/publish/validate_mesh_lamina_faces.py +++ b/pype/plugins/maya/publish/validate_mesh_lamina_faces.py @@ -2,6 +2,7 @@ from maya import cmds import pyblish.api import pype.api +import pype.maya.action class ValidateMeshLaminaFaces(pyblish.api.InstancePlugin): @@ -13,11 +14,11 @@ class ValidateMeshLaminaFaces(pyblish.api.InstancePlugin): order = pype.api.ValidateMeshOrder hosts = ['maya'] - families = ['studio.model'] + families = ["studio.model'] category = 'geometry' version = (0, 1, 0) label = 'Mesh Lamina Faces' - actions = [pype.api.SelectInvalidAction] + actions = [pype.maya.action.SelectInvalidAction] @staticmethod def get_invalid(instance): diff --git a/pype/plugins/maya/publish/validate_mesh_no_negative_scale.py b/pype/plugins/maya/publish/validate_mesh_no_negative_scale.py index 06c24501e6..e97e9a1c68 100644 --- a/pype/plugins/maya/publish/validate_mesh_no_negative_scale.py +++ b/pype/plugins/maya/publish/validate_mesh_no_negative_scale.py @@ -2,6 +2,7 @@ from maya import cmds import pyblish.api import pype.api +import pype.maya.action class ValidateMeshNoNegativeScale(pyblish.api.Validator): @@ -18,9 +19,9 @@ class ValidateMeshNoNegativeScale(pyblish.api.Validator): order = pype.api.ValidateMeshOrder hosts = ['maya'] - families = ['studio.model'] + families = ["studio.model'] label = 'Mesh No Negative Scale' - actions = [pype.api.SelectInvalidAction] + actions = [pype.maya.action.SelectInvalidAction] @staticmethod def get_invalid(instance): diff --git a/pype/plugins/maya/publish/validate_mesh_non_manifold.py b/pype/plugins/maya/publish/validate_mesh_non_manifold.py index e54297b5f2..5cc48e9bcc 100644 --- a/pype/plugins/maya/publish/validate_mesh_non_manifold.py +++ b/pype/plugins/maya/publish/validate_mesh_non_manifold.py @@ -2,6 +2,7 @@ from maya import cmds import pyblish.api import pype.api +import pype.maya.action class ValidateMeshNonManifold(pyblish.api.Validator): @@ -14,9 +15,9 @@ class ValidateMeshNonManifold(pyblish.api.Validator): order = pype.api.ValidateMeshOrder hosts = ['maya'] - families = ['studio.model'] + families = ["studio.model'] label = 'Mesh Non-Manifold Vertices/Edges' - actions = [pype.api.SelectInvalidAction] + actions = [pype.maya.action.SelectInvalidAction] @staticmethod def get_invalid(instance): diff --git a/pype/plugins/maya/publish/validate_mesh_non_zero_edge.py b/pype/plugins/maya/publish/validate_mesh_non_zero_edge.py index 3b889cb856..e0cb7c72b7 100644 --- a/pype/plugins/maya/publish/validate_mesh_non_zero_edge.py +++ b/pype/plugins/maya/publish/validate_mesh_non_zero_edge.py @@ -2,6 +2,7 @@ from maya import cmds import pyblish.api import pype.api +import pype.maya.action import pype.maya.lib as lib @@ -16,12 +17,12 @@ class ValidateMeshNonZeroEdgeLength(pyblish.api.InstancePlugin): """ order = pype.api.ValidateMeshOrder - families = ['studio.model'] + families = ["studio.model'] hosts = ['maya'] category = 'geometry' version = (0, 1, 0) label = 'Mesh Edge Length Non Zero' - actions = [pype.api.SelectInvalidAction] + actions = [pype.maya.action.SelectInvalidAction] __tolerance = 1e-5 diff --git a/pype/plugins/maya/publish/validate_mesh_normals_unlocked.py b/pype/plugins/maya/publish/validate_mesh_normals_unlocked.py index 6495d0f636..67ee3255e8 100644 --- a/pype/plugins/maya/publish/validate_mesh_normals_unlocked.py +++ b/pype/plugins/maya/publish/validate_mesh_normals_unlocked.py @@ -2,6 +2,7 @@ from maya import cmds import pyblish.api import pype.api +import pype.maya.action class ValidateMeshNormalsUnlocked(pyblish.api.Validator): @@ -14,11 +15,11 @@ class ValidateMeshNormalsUnlocked(pyblish.api.Validator): order = pype.api.ValidateMeshOrder hosts = ['maya'] - families = ['studio.model'] + families = ["studio.model'] category = 'geometry' version = (0, 1, 0) label = 'Mesh Normals Unlocked' - actions = [pype.api.SelectInvalidAction, + actions = [pype.maya.action.SelectInvalidAction, pype.api.RepairAction] optional = True diff --git a/pype/plugins/maya/publish/validate_mesh_shader_connections.py b/pype/plugins/maya/publish/validate_mesh_shader_connections.py index 5501a6b452..cb2d041736 100644 --- a/pype/plugins/maya/publish/validate_mesh_shader_connections.py +++ b/pype/plugins/maya/publish/validate_mesh_shader_connections.py @@ -2,6 +2,7 @@ from maya import cmds import pyblish.api import pype.api +import pype.maya.action def pairs(iterable): @@ -74,9 +75,9 @@ class ValidateMeshShaderConnections(pyblish.api.InstancePlugin): order = pype.api.ValidateMeshOrder hosts = ['maya'] - families = ['studio.model'] + families = ["studio.model'] label = "Mesh Shader Connections" - actions = [pype.api.SelectInvalidAction, + actions = [pype.maya.action.SelectInvalidAction, pype.api.RepairAction] def process(self, instance): diff --git a/pype/plugins/maya/publish/validate_mesh_single_uv_set.py b/pype/plugins/maya/publish/validate_mesh_single_uv_set.py index 1c6cb38f27..acd18a8991 100644 --- a/pype/plugins/maya/publish/validate_mesh_single_uv_set.py +++ b/pype/plugins/maya/publish/validate_mesh_single_uv_set.py @@ -2,6 +2,7 @@ from maya import cmds import pyblish.api import pype.api +import pype.maya.action import pype.maya.lib as lib @@ -16,12 +17,12 @@ class ValidateMeshSingleUVSet(pyblish.api.InstancePlugin): order = pype.api.ValidateMeshOrder hosts = ['maya'] - families = ['studio.model', 'studio.pointcache'] + families = ["studio.model', 'studio.pointcache'] category = 'uv' optional = True version = (0, 1, 0) label = "Mesh Single UV Set" - actions = [pype.api.SelectInvalidAction, + actions = [pype.maya.action.SelectInvalidAction, pype.api.RepairAction] @staticmethod diff --git a/pype/plugins/maya/publish/validate_mesh_uv_set_map1.py b/pype/plugins/maya/publish/validate_mesh_uv_set_map1.py new file mode 100644 index 0000000000..1cdb16568c --- /dev/null +++ b/pype/plugins/maya/publish/validate_mesh_uv_set_map1.py @@ -0,0 +1,91 @@ +from maya import cmds + +import pyblish.api +import pype.api +import pype.maya.action + + +class ValidateMeshUVSetMap1(pyblish.api.InstancePlugin): + """Validate model's default set exists and is named 'map1'. + + In Maya meshes by default have a uv set named "map1" that cannot be + deleted. It can be renamed however, introducing some issues with some + renderers. As such we ensure the first (default) UV set index is named + "map1". + + """ + + order = pype.api.ValidateMeshOrder + hosts = ['maya'] + families = ["studio.model'] + optional = True + label = "Mesh has map1 UV Set" + actions = [pype.maya.action.SelectInvalidAction, + pype.api.RepairAction] + + @staticmethod + def get_invalid(instance): + + meshes = cmds.ls(instance, type='mesh', long=True) + + invalid = [] + for mesh in meshes: + + # Get existing mapping of uv sets by index + indices = cmds.polyUVSet(mesh, query=True, allUVSetsIndices=True) + maps = cmds.polyUVSet(mesh, query=True, allUVSets=True) + mapping = dict(zip(indices, maps)) + + # Get the uv set at index zero. + name = mapping[0] + if name != "map1": + invalid.append(mesh) + + return invalid + + def process(self, instance): + """Process all the nodes in the instance 'objectSet'""" + + invalid = self.get_invalid(instance) + if invalid: + raise ValueError("Meshes found without 'map1' " + "UV set: {0}".format(invalid)) + + @classmethod + def repair(cls, instance): + """Rename uv map at index zero to map1""" + + for mesh in cls.get_invalid(instance): + + # Get existing mapping of uv sets by index + indices = cmds.polyUVSet(mesh, query=True, allUVSetsIndices=True) + maps = cmds.polyUVSet(mesh, query=True, allUVSets=True) + mapping = dict(zip(indices, maps)) + + # Ensure there is no uv set named map1 to avoid + # a clash on renaming the "default uv set" to map1 + existing = set(maps) + if "map1" in existing: + + # Find a unique name index + i = 2 + while True: + name = "map{0}".format(i) + if name not in existing: + break + i += 1 + + cls.log.warning("Renaming clashing uv set name on mesh" + " %s to '%s'", mesh, name) + + cmds.polyUVSet(mesh, + rename=True, + uvSet="map1", + newUVSet=name) + + # Rename the initial index to map1 + original = mapping[0] + cmds.polyUVSet(mesh, + rename=True, + uvSet=original, + newUVSet="map1") diff --git a/pype/plugins/maya/publish/validate_mesh_vertices_have_edges.py b/pype/plugins/maya/publish/validate_mesh_vertices_have_edges.py index e212833a58..c1d98961df 100644 --- a/pype/plugins/maya/publish/validate_mesh_vertices_have_edges.py +++ b/pype/plugins/maya/publish/validate_mesh_vertices_have_edges.py @@ -4,6 +4,7 @@ from maya import cmds import pyblish.api import pype.api +import pype.maya.action def len_flattened(components): @@ -58,10 +59,10 @@ class ValidateMeshVerticesHaveEdges(pyblish.api.InstancePlugin): order = pype.api.ValidateMeshOrder hosts = ['maya'] - families = ['studio.model'] + families = ["studio.model'] category = 'geometry' label = 'Mesh Vertices Have Edges' - actions = [pype.api.SelectInvalidAction, + actions = [pype.maya.action.SelectInvalidAction, pype.api.RepairAction] @classmethod diff --git a/pype/plugins/maya/publish/validate_model_content.py b/pype/plugins/maya/publish/validate_model_content.py index 716957ca6f..0d5180ca81 100644 --- a/pype/plugins/maya/publish/validate_model_content.py +++ b/pype/plugins/maya/publish/validate_model_content.py @@ -2,6 +2,7 @@ from maya import cmds import pyblish.api import pype.api +import pype.maya.action import pype.maya.lib as lib @@ -15,9 +16,9 @@ class ValidateModelContent(pyblish.api.InstancePlugin): order = pype.api.ValidateContentsOrder hosts = ["maya"] - families = ["model"] + families = ["studio.model"] label = "Model Content" - actions = [pype.api.SelectInvalidAction] + actions = [pype.maya.action.SelectInvalidAction] @classmethod def get_invalid(cls, instance): diff --git a/pype/plugins/maya/publish/validate_no_animation.py b/pype/plugins/maya/publish/validate_no_animation.py index 861a7ee20f..19eb79173d 100644 --- a/pype/plugins/maya/publish/validate_no_animation.py +++ b/pype/plugins/maya/publish/validate_no_animation.py @@ -2,6 +2,7 @@ from maya import cmds import pyblish.api import pype.api +import pype.maya.action class ValidateNoAnimation(pyblish.api.Validator): @@ -16,9 +17,9 @@ class ValidateNoAnimation(pyblish.api.Validator): order = pype.api.ValidateContentsOrder label = "No Animation" hosts = ["maya"] - families = ["model"] + families = ["studio.model"] optional = True - actions = [pype.api.SelectInvalidAction] + actions = [pype.maya.action.SelectInvalidAction] def process(self, instance): diff --git a/pype/plugins/maya/publish/validate_no_default_camera.py b/pype/plugins/maya/publish/validate_no_default_camera.py index 8798a19e0f..b45b8124c1 100644 --- a/pype/plugins/maya/publish/validate_no_default_camera.py +++ b/pype/plugins/maya/publish/validate_no_default_camera.py @@ -2,6 +2,7 @@ from maya import cmds import pyblish.api import pype.api +import pype.maya.action class ValidateNoDefaultCameras(pyblish.api.InstancePlugin): @@ -14,10 +15,10 @@ class ValidateNoDefaultCameras(pyblish.api.InstancePlugin): order = pype.api.ValidateContentsOrder hosts = ['maya'] - families = ['studio.camera'] + families = ["studio.camera'] version = (0, 1, 0) label = "No Default Cameras" - actions = [pype.api.SelectInvalidAction] + actions = [pype.maya.action.SelectInvalidAction] @staticmethod def get_invalid(instance): diff --git a/pype/plugins/maya/publish/validate_no_namespace.py b/pype/plugins/maya/publish/validate_no_namespace.py index ca815df2f4..eca5510044 100644 --- a/pype/plugins/maya/publish/validate_no_namespace.py +++ b/pype/plugins/maya/publish/validate_no_namespace.py @@ -3,6 +3,7 @@ import maya.cmds as cmds import pyblish.api import pype.api +import pype.maya.action def get_namespace(node_name): @@ -17,11 +18,11 @@ class ValidateNoNamespace(pyblish.api.InstancePlugin): order = pype.api.ValidateContentsOrder hosts = ['maya'] - families = ['studio.model'] + families = ["studio.model'] category = 'cleanup' version = (0, 1, 0) label = 'No Namespaces' - actions = [pype.api.SelectInvalidAction, + actions = [pype.maya.action.SelectInvalidAction, pype.api.RepairAction] @staticmethod diff --git a/pype/plugins/maya/publish/validate_no_null_transforms.py b/pype/plugins/maya/publish/validate_no_null_transforms.py index be8d5ad82b..6935643da7 100644 --- a/pype/plugins/maya/publish/validate_no_null_transforms.py +++ b/pype/plugins/maya/publish/validate_no_null_transforms.py @@ -2,6 +2,7 @@ import maya.cmds as cmds import pyblish.api import pype.api +import pype.maya.action def has_shape_children(node): @@ -38,11 +39,12 @@ class ValidateNoNullTransforms(pyblish.api.InstancePlugin): order = pype.api.ValidateContentsOrder hosts = ['maya'] - families = ['studio.model'] + families = ["studio.model'] category = 'cleanup' version = (0, 1, 0) label = 'No Empty/Null Transforms' - actions = [pype.api.RepairAction, pype.api.SelectInvalidAction] + actions = [pype.api.RepairAction, + pype.maya.action.SelectInvalidAction] @staticmethod def get_invalid(instance): diff --git a/pype/plugins/maya/publish/validate_no_unknown_nodes.py b/pype/plugins/maya/publish/validate_no_unknown_nodes.py index 41de90298c..ff82202022 100644 --- a/pype/plugins/maya/publish/validate_no_unknown_nodes.py +++ b/pype/plugins/maya/publish/validate_no_unknown_nodes.py @@ -2,6 +2,7 @@ from maya import cmds import pyblish.api import pype.api +import pype.maya.action class ValidateNoUnknownNodes(pyblish.api.InstancePlugin): @@ -17,10 +18,10 @@ class ValidateNoUnknownNodes(pyblish.api.InstancePlugin): order = pype.api.ValidateContentsOrder hosts = ['maya'] - families = ['studio.model', 'studio.rig'] + families = ["studio.model', 'studio.rig'] optional = True label = "Unknown Nodes" - actions = [pype.api.SelectInvalidAction] + actions = [pype.maya.action.SelectInvalidAction] @staticmethod def get_invalid(instance): diff --git a/pype/plugins/maya/publish/validate_no_vraymesh.py b/pype/plugins/maya/publish/validate_no_vraymesh.py index 27e5e6a006..a172191015 100644 --- a/pype/plugins/maya/publish/validate_no_vraymesh.py +++ b/pype/plugins/maya/publish/validate_no_vraymesh.py @@ -7,7 +7,7 @@ class ValidateNoVRayMesh(pyblish.api.InstancePlugin): order = pyblish.api.ValidatorOrder label = 'No V-Ray Proxies (VRayMesh)' - families = ["pointcache"] + families = ["studio.pointcache"] def process(self, instance): diff --git a/pype/plugins/maya/publish/validate_node_ids.py b/pype/plugins/maya/publish/validate_node_ids.py index cd941f04aa..4f378488c9 100644 --- a/pype/plugins/maya/publish/validate_node_ids.py +++ b/pype/plugins/maya/publish/validate_node_ids.py @@ -1,14 +1,15 @@ import pyblish.api import pype.api +import pype.maya.action from pype.maya import lib class ValidateNodeIDs(pyblish.api.InstancePlugin): """Validate nodes have a Colorbleed Id. - - When IDs are missing from nodes *save your scene* and they should be - automatically generated because IDs are created on non-referenced nodes + + When IDs are missing from nodes *save your scene* and they should be + automatically generated because IDs are created on non-referenced nodes in Maya upon scene save. """ @@ -16,15 +17,16 @@ class ValidateNodeIDs(pyblish.api.InstancePlugin): order = pype.api.ValidatePipelineOrder label = 'Instance Nodes Have ID' hosts = ['maya'] - families = ["model", - "look", - "rig", - "pointcache", - "animation", - "setdress"] + families = ["studio.model", + "studio.look", + "studio.rig", + "studio.pointcache", + "studio.animation", + "studio.setdress", + "studio.yetiRig"] - actions = [pype.api.SelectInvalidAction, - pype.api.GenerateUUIDsOnInvalidAction] + actions = [pype.maya.action.SelectInvalidAction, + pype.maya.action.GenerateUUIDsOnInvalidAction] def process(self, instance): """Process all meshes""" diff --git a/pype/plugins/maya/publish/validate_node_ids_deformed_shapes.py b/pype/plugins/maya/publish/validate_node_ids_deformed_shapes.py index 25c6997e71..dddeb9759c 100644 --- a/pype/plugins/maya/publish/validate_node_ids_deformed_shapes.py +++ b/pype/plugins/maya/publish/validate_node_ids_deformed_shapes.py @@ -2,6 +2,7 @@ from maya import cmds import pyblish.api import pype.api +import pype.maya.action import pype.maya.lib as lib @@ -16,10 +17,10 @@ class ValidateNodeIdsDeformedShape(pyblish.api.InstancePlugin): """ order = pype.api.ValidateContentsOrder - families = ['studio.look'] + families = ["studio.look'] hosts = ['maya'] label = 'Deformed shape ids' - actions = [pype.api.SelectInvalidAction, pype.api.RepairAction] + actions = [pype.maya.action.SelectInvalidAction, pype.api.RepairAction] def process(self, instance): """Process all the nodes in the instance""" @@ -63,3 +64,4 @@ class ValidateNodeIdsDeformedShape(pyblish.api.InstancePlugin): continue lib.set_id(node, history_id, overwrite=True) + diff --git a/pype/plugins/maya/publish/validate_node_ids_in_database.py b/pype/plugins/maya/publish/validate_node_ids_in_database.py index 490d7231ff..bf0e9f7d43 100644 --- a/pype/plugins/maya/publish/validate_node_ids_in_database.py +++ b/pype/plugins/maya/publish/validate_node_ids_in_database.py @@ -3,6 +3,7 @@ import pyblish.api import avalon.io as io import pype.api +import pype.maya.action from pype.maya import lib @@ -22,7 +23,7 @@ class ValidateNodeIdsInDatabase(pyblish.api.InstancePlugin): hosts = ['maya'] families = ["*"] - actions = [pype.api.SelectInvalidAction] + actions = [pype.maya.action.SelectInvalidAction] def process(self, instance): invalid = self.get_invalid(instance) diff --git a/pype/plugins/maya/publish/validate_node_ids_related.py b/pype/plugins/maya/publish/validate_node_ids_related.py index 3b6b6e3528..f9a1f0db73 100644 --- a/pype/plugins/maya/publish/validate_node_ids_related.py +++ b/pype/plugins/maya/publish/validate_node_ids_related.py @@ -2,6 +2,7 @@ import pyblish.api import pype.api import avalon.io as io +import pype.maya.action from pype.maya import lib @@ -14,13 +15,13 @@ class ValidateNodeIDsRelated(pyblish.api.InstancePlugin): order = pype.api.ValidatePipelineOrder label = 'Node Ids Related (ID)' hosts = ['maya'] - families = ["model", - "look", - "rig"] + families = ["studio.model", + "studio.look", + "studio.rig"] optional = True - actions = [pype.api.SelectInvalidAction, - pype.api.GenerateUUIDsOnInvalidAction] + actions = [pype.maya.action.SelectInvalidAction, + pype.maya.action.GenerateUUIDsOnInvalidAction] def process(self, instance): """Process all nodes in instance (including hierarchy)""" diff --git a/pype/plugins/maya/publish/validate_node_ids_unique.py b/pype/plugins/maya/publish/validate_node_ids_unique.py index 5e1ee9f91c..702f5a7961 100644 --- a/pype/plugins/maya/publish/validate_node_ids_unique.py +++ b/pype/plugins/maya/publish/validate_node_ids_unique.py @@ -2,6 +2,7 @@ from collections import defaultdict import pyblish.api import pype.api +import pype.maya.action import pype.maya.lib as lib @@ -14,12 +15,13 @@ class ValidateNodeIdsUnique(pyblish.api.InstancePlugin): order = pype.api.ValidatePipelineOrder label = 'Non Duplicate Instance Members (ID)' hosts = ['maya'] - families = ["model", - "look", - "rig"] + families = ["studio.model", + "studio.look", + "studio.rig", + "studio.yetiRig"] - actions = [pype.api.SelectInvalidAction, - pype.api.GenerateUUIDsOnInvalidAction] + actions = [pype.maya.action.SelectInvalidAction, + pype.maya.action.GenerateUUIDsOnInvalidAction] def process(self, instance): """Process all meshes""" diff --git a/pype/plugins/maya/publish/validate_node_no_ghosting.py b/pype/plugins/maya/publish/validate_node_no_ghosting.py index fa05a5e80f..13c38e024f 100644 --- a/pype/plugins/maya/publish/validate_node_no_ghosting.py +++ b/pype/plugins/maya/publish/validate_node_no_ghosting.py @@ -2,6 +2,7 @@ from maya import cmds import pyblish.api import pype.api +import pype.maya.action class ValidateNodeNoGhosting(pyblish.api.InstancePlugin): @@ -18,9 +19,9 @@ class ValidateNodeNoGhosting(pyblish.api.InstancePlugin): order = pype.api.ValidateContentsOrder hosts = ['maya'] - families = ['studio.model', 'studio.rig'] + families = ["studio.model', 'studio.rig'] label = "No Ghosting" - actions = [pype.api.SelectInvalidAction] + actions = [pype.maya.action.SelectInvalidAction] _attributes = {'ghosting': 0} diff --git a/pype/plugins/maya/publish/validate_render_image_rule.py b/pype/plugins/maya/publish/validate_render_image_rule.py index 762cf963f1..8b04c67520 100644 --- a/pype/plugins/maya/publish/validate_render_image_rule.py +++ b/pype/plugins/maya/publish/validate_render_image_rule.py @@ -17,7 +17,7 @@ class ValidateRenderImageRule(pyblish.api.ContextPlugin): order = pype.api.ValidateContentsOrder label = "Images File Rule (Workspace)" hosts = ["maya"] - families = ["renderlayer"] + families = ["studio.renderlayer"] def process(self, context): diff --git a/pype/plugins/maya/publish/validate_render_no_default_cameras.py b/pype/plugins/maya/publish/validate_render_no_default_cameras.py new file mode 100644 index 0000000000..18d18ea699 --- /dev/null +++ b/pype/plugins/maya/publish/validate_render_no_default_cameras.py @@ -0,0 +1,41 @@ +from maya import cmds + +import pyblish.api +import pype.api +import pype.maya.action +import pype.maya.lib as lib + + +class ValidateRenderNoDefaultCameras(pyblish.api.InstancePlugin): + """Ensure no default (startup) cameras are to be rendered.""" + + order = pype.api.ValidateContentsOrder + hosts = ['maya'] + families = ["studio.renderlayer'] + label = "No Default Cameras Renderable" + actions = [pype.maya.action.SelectInvalidAction] + + @staticmethod + def get_invalid(instance): + + layer = instance.data["setMembers"] + + # Collect default cameras + cameras = cmds.ls(type='camera', long=True) + defaults = [cam for cam in cameras if + cmds.camera(cam, query=True, startupCamera=True)] + + invalid = [] + with lib.renderlayer(layer): + for cam in defaults: + if cmds.getAttr(cam + ".renderable"): + invalid.append(cam) + + return invalid + + def process(self, instance): + """Process all the cameras in the instance""" + invalid = self.get_invalid(instance) + if invalid: + raise RuntimeError("Renderable default cameras " + "found: {0}".format(invalid)) diff --git a/pype/plugins/maya/publish/validate_render_single_camera.py b/pype/plugins/maya/publish/validate_render_single_camera.py new file mode 100644 index 0000000000..bbab6c4d0a --- /dev/null +++ b/pype/plugins/maya/publish/validate_render_single_camera.py @@ -0,0 +1,49 @@ +from maya import cmds + +import pyblish.api +import pype.api +import pype.maya.action +import pype.maya.lib as lib + + +class ValidateRenderSingleCamera(pyblish.api.InstancePlugin): + """Only one camera may be renderable in a layer. + + Currently the pipeline supports only a single camera per layer. + This is because when multiple cameras are rendered the output files + automatically get different names because the render token + is not in the output path. As such the output files conflict with how + our pipeline expects the output. + + """ + + order = pype.api.ValidateContentsOrder + hosts = ['maya'] + families = ["studio.renderlayer'] + label = "Render Single Camera" + actions = [pype.maya.action.SelectInvalidAction] + + @staticmethod + def get_invalid(instance): + + layer = instance.data["setMembers"] + + cameras = cmds.ls(type='camera', long=True) + + with lib.renderlayer(layer): + renderable = [cam for cam in cameras if + cmds.getAttr(cam + ".renderable")] + + if len(renderable) == 0: + raise RuntimeError("No renderable cameras found.") + elif len(renderable) > 1: + return renderable + else: + return [] + + def process(self, instance): + """Process all the cameras in the instance""" + invalid = self.get_invalid(instance) + if invalid: + raise RuntimeError("Multiple renderable cameras" + "found: {0}".format(invalid)) diff --git a/pype/plugins/maya/publish/validate_renderlayer_aovs.py b/pype/plugins/maya/publish/validate_renderlayer_aovs.py index d48bb86aed..6873e4e985 100644 --- a/pype/plugins/maya/publish/validate_renderlayer_aovs.py +++ b/pype/plugins/maya/publish/validate_renderlayer_aovs.py @@ -1,5 +1,6 @@ import pyblish.api +import pype.maya.action from avalon import io import pype.api @@ -23,8 +24,8 @@ class ValidateRenderLayerAOVs(pyblish.api.InstancePlugin): order = pyblish.api.ValidatorOrder + 0.1 label = "Render Passes / AOVs Are Registered" hosts = ["maya"] - families = ["renderlayer"] - actions = [pype.api.SelectInvalidAction] + families = ["studio.renderlayer"] + actions = [pype.maya.action.SelectInvalidAction] def process(self, instance): invalid = self.get_invalid(instance) diff --git a/pype/plugins/maya/publish/validate_rendersettings.py b/pype/plugins/maya/publish/validate_rendersettings.py index 4007404670..2fe91aa699 100644 --- a/pype/plugins/maya/publish/validate_rendersettings.py +++ b/pype/plugins/maya/publish/validate_rendersettings.py @@ -30,7 +30,7 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin): order = pype.api.ValidateContentsOrder label = "Render Settings" hosts = ["maya"] - families = ["renderlayer"] + families = ["studio.renderlayer"] actions = [pype.api.RepairAction] DEFAULT_PADDING = 4 diff --git a/pype/plugins/maya/publish/validate_rig_contents.py b/pype/plugins/maya/publish/validate_rig_contents.py index 0a9616ba1f..f58c554c9b 100644 --- a/pype/plugins/maya/publish/validate_rig_contents.py +++ b/pype/plugins/maya/publish/validate_rig_contents.py @@ -16,7 +16,7 @@ class ValidateRigContents(pyblish.api.InstancePlugin): order = pype.api.ValidateContentsOrder label = "Rig Contents" hosts = ["maya"] - families = ["rig"] + families = ["studio.rig"] accepted_output = ["mesh", "transform"] accepted_controllers = ["transform"] diff --git a/pype/plugins/maya/publish/validate_rig_controllers.py b/pype/plugins/maya/publish/validate_rig_controllers.py index b99aeae0c9..f1434d0414 100644 --- a/pype/plugins/maya/publish/validate_rig_controllers.py +++ b/pype/plugins/maya/publish/validate_rig_controllers.py @@ -1,8 +1,10 @@ from maya import cmds import pyblish.api + import pype.api -from cb.utils.maya.context import undo_chunk +import pype.maya.action +from pype.maya.lib import undo_chunk class ValidateRigControllers(pyblish.api.InstancePlugin): @@ -26,9 +28,9 @@ class ValidateRigControllers(pyblish.api.InstancePlugin): order = pype.api.ValidateContentsOrder + 0.05 label = "Rig Controllers" hosts = ["maya"] - families = ["rig"] + families = ["studio.rig"] actions = [pype.api.RepairAction, - pype.api.SelectInvalidAction] + pype.maya.action.SelectInvalidAction] # Default controller values CONTROLLER_DEFAULTS = { diff --git a/pype/plugins/maya/publish/validate_rig_controllers_arnold_attributes.py b/pype/plugins/maya/publish/validate_rig_controllers_arnold_attributes.py index 6ed17e529a..c085150c05 100644 --- a/pype/plugins/maya/publish/validate_rig_controllers_arnold_attributes.py +++ b/pype/plugins/maya/publish/validate_rig_controllers_arnold_attributes.py @@ -2,7 +2,9 @@ from maya import cmds import pyblish.api import pype.api -from cb.utils.maya.context import undo_chunk + +import pype.maya.lib as lib +import pype.maya.action class ValidateRigControllersArnoldAttributes(pyblish.api.InstancePlugin): @@ -27,9 +29,9 @@ class ValidateRigControllersArnoldAttributes(pyblish.api.InstancePlugin): order = pype.api.ValidateContentsOrder + 0.05 label = "Rig Controllers (Arnold Attributes)" hosts = ["maya"] - families = ["rig"] + families = ["studio.rig"] actions = [pype.api.RepairAction, - pype.api.SelectInvalidAction] + pype.maya.action.SelectInvalidAction] attributes = [ "rcurve", @@ -81,7 +83,7 @@ class ValidateRigControllersArnoldAttributes(pyblish.api.InstancePlugin): def repair(cls, instance): invalid = cls.get_invalid(instance) - with undo_chunk(): + with lib.undo_chunk(): for node in invalid: for attribute in cls.attributes: if cmds.attributeQuery(attribute, node=node, exists=True): diff --git a/pype/plugins/maya/publish/validate_rig_out_set_node_ids.py b/pype/plugins/maya/publish/validate_rig_out_set_node_ids.py index 72abbb9ed2..2aaf6f304a 100644 --- a/pype/plugins/maya/publish/validate_rig_out_set_node_ids.py +++ b/pype/plugins/maya/publish/validate_rig_out_set_node_ids.py @@ -2,6 +2,7 @@ import maya.cmds as cmds import pyblish.api import pype.api +import pype.maya.action import pype.maya.lib as lib @@ -16,10 +17,10 @@ class ValidateRigOutSetNodeIds(pyblish.api.InstancePlugin): """ order = pype.api.ValidateContentsOrder - families = ["rig"] + families = ["studio.rig"] hosts = ['maya'] label = 'Rig Out Set Node Ids' - actions = [pype.api.SelectInvalidAction, pype.api.RepairAction] + actions = [pype.maya.action.SelectInvalidAction, pype.api.RepairAction] def process(self, instance): """Process all meshes""" diff --git a/pype/plugins/maya/publish/validate_scene_set_workspace.py b/pype/plugins/maya/publish/validate_scene_set_workspace.py index ffa2f297c9..2b2dc6e957 100644 --- a/pype/plugins/maya/publish/validate_scene_set_workspace.py +++ b/pype/plugins/maya/publish/validate_scene_set_workspace.py @@ -30,7 +30,7 @@ class ValidateSceneSetWorkspace(pyblish.api.ContextPlugin): order = pype.api.ValidatePipelineOrder hosts = ['maya'] - families = ['studio.model'] + families = ["studio.model'] category = 'scene' version = (0, 1, 0) label = 'Maya Workspace Set' diff --git a/pype/plugins/maya/publish/validate_setdress_namespaces.py b/pype/plugins/maya/publish/validate_setdress_namespaces.py index a1056589f6..f7ea46ecbc 100644 --- a/pype/plugins/maya/publish/validate_setdress_namespaces.py +++ b/pype/plugins/maya/publish/validate_setdress_namespaces.py @@ -1,5 +1,6 @@ import pyblish.api import pype.api +import pype.maya.action class ValidateSetdressNamespaces(pyblish.api.InstancePlugin): @@ -16,8 +17,8 @@ class ValidateSetdressNamespaces(pyblish.api.InstancePlugin): label = "Validate Setdress Namespaces" order = pyblish.api.ValidatorOrder - families = ["setdress"] - actions = [pype.api.SelectInvalidAction] + families = ["studio.setdress"] + actions = [pype.maya.action.SelectInvalidAction] def process(self, instance): diff --git a/pype/plugins/maya/publish/validate_setdress_transforms.py b/pype/plugins/maya/publish/validate_setdress_transforms.py index f02ecc3a0b..5022909cc3 100644 --- a/pype/plugins/maya/publish/validate_setdress_transforms.py +++ b/pype/plugins/maya/publish/validate_setdress_transforms.py @@ -3,6 +3,8 @@ import pype.api from maya import cmds +import pype.maya.action + class ValidateSetDressModelTransforms(pyblish.api.InstancePlugin): """Verify only root nodes of the loaded asset have transformations. @@ -25,8 +27,8 @@ class ValidateSetDressModelTransforms(pyblish.api.InstancePlugin): order = pyblish.api.ValidatorOrder + 0.49 label = "Setdress Model Transforms" - families = ["setdress"] - actions = [pype.api.SelectInvalidAction, + families = ["studio.setdress"] + actions = [pype.maya.action.SelectInvalidAction, pype.api.RepairAction] prompt_message = ("You are about to reset the matrix to the default values." diff --git a/pype/plugins/maya/publish/validate_shape_default_names.py b/pype/plugins/maya/publish/validate_shape_default_names.py index 1fbe113feb..4683ed8332 100644 --- a/pype/plugins/maya/publish/validate_shape_default_names.py +++ b/pype/plugins/maya/publish/validate_shape_default_names.py @@ -4,6 +4,7 @@ from maya import cmds import pyblish.api import pype.api +import pype.maya.action def short_name(node): @@ -32,12 +33,12 @@ class ValidateShapeDefaultNames(pyblish.api.InstancePlugin): order = pype.api.ValidateContentsOrder hosts = ['maya'] - families = ['studio.model'] + families = ["studio.model'] category = 'cleanup' optional = True version = (0, 1, 0) label = "Shape Default Naming" - actions = [pype.api.SelectInvalidAction, + actions = [pype.maya.action.SelectInvalidAction, pype.api.RepairAction] @staticmethod diff --git a/pype/plugins/maya/publish/validate_shape_render_stats.py b/pype/plugins/maya/publish/validate_shape_render_stats.py index ec02b86698..d98a0463fe 100644 --- a/pype/plugins/maya/publish/validate_shape_render_stats.py +++ b/pype/plugins/maya/publish/validate_shape_render_stats.py @@ -3,15 +3,17 @@ import pype.api from maya import cmds +import pype.maya.action + class ValidateShapeRenderStats(pyblish.api.Validator): """Ensure all render stats are set to the default values.""" order = pype.api.ValidateMeshOrder hosts = ['maya'] - families = ['studio.model'] + families = ["studio.model'] label = 'Shape Default Render Stats' - actions = [pype.api.SelectInvalidAction, + actions = [pype.maya.action.SelectInvalidAction, pype.api.RepairAction] defaults = {'castsShadows': 1, diff --git a/pype/plugins/maya/publish/validate_single_assembly.py b/pype/plugins/maya/publish/validate_single_assembly.py index 0226520464..c8504d198c 100644 --- a/pype/plugins/maya/publish/validate_single_assembly.py +++ b/pype/plugins/maya/publish/validate_single_assembly.py @@ -19,7 +19,7 @@ class ValidateSingleAssembly(pyblish.api.InstancePlugin): order = pype.api.ValidateContentsOrder hosts = ['maya'] - families = ['studio.rig', 'studio.animation'] + families = ["studio.rig', 'studio.animation'] label = 'Single Assembly' def process(self, instance): diff --git a/pype/plugins/maya/publish/validate_step_size.py b/pype/plugins/maya/publish/validate_step_size.py index a1e826cb7a..16f1628b1d 100644 --- a/pype/plugins/maya/publish/validate_step_size.py +++ b/pype/plugins/maya/publish/validate_step_size.py @@ -1,5 +1,6 @@ import pyblish.api import pype.api +import pype.maya.action class ValidateStepSize(pyblish.api.InstancePlugin): @@ -11,10 +12,10 @@ class ValidateStepSize(pyblish.api.InstancePlugin): order = pype.api.ValidateContentsOrder label = 'Step size' - families = ['studio.camera', + families = ["studio.camera', 'studio.pointcache', 'studio.animation'] - actions = [pype.api.SelectInvalidAction] + actions = [pype.maya.action.SelectInvalidAction] MIN = 0.01 MAX = 1.0 diff --git a/pype/plugins/maya/publish/validate_transfers.py b/pype/plugins/maya/publish/validate_transfers.py index 3f19f2076c..3234b2240e 100644 --- a/pype/plugins/maya/publish/validate_transfers.py +++ b/pype/plugins/maya/publish/validate_transfers.py @@ -22,8 +22,6 @@ class ValidateTransfers(pyblish.api.InstancePlugin): if not transfers: return - verbose = instance.data.get('verbose', False) - # Collect all destination with its sources collected = defaultdict(set) for source, destination in transfers: @@ -39,16 +37,9 @@ class ValidateTransfers(pyblish.api.InstancePlugin): if len(sources) > 1: invalid_destinations.append(destination) - if verbose: - self.log.error("Non-unique file transfer for resources: " - "{0} (sources: {1})".format(destination, - sources)) + self.log.error("Non-unique file transfer for resources: " + "{0} (sources: {1})".format(destination, + list(sources))) if invalid_destinations: - if not verbose: - # If not verbose then still log the resource destination as - # opposed to every individual file transfer - self.log.error("Non-unique file transfers to destinations: " - "%s" % "\n".join(invalid_destinations)) - raise RuntimeError("Invalid transfers in queue.") diff --git a/pype/plugins/maya/publish/validate_transform_naming_suffix.py b/pype/plugins/maya/publish/validate_transform_naming_suffix.py index 935878d194..232f41d5ed 100644 --- a/pype/plugins/maya/publish/validate_transform_naming_suffix.py +++ b/pype/plugins/maya/publish/validate_transform_naming_suffix.py @@ -2,7 +2,7 @@ from maya import cmds import pyblish.api import pype.api - +import pype.maya.action SUFFIX_NAMING_TABLE = {'mesh': ["_GEO", "_GES", "_GEP", "_OSD"], 'nurbsCurve': ["_CRV"], @@ -33,12 +33,12 @@ class ValidateTransformNamingSuffix(pyblish.api.InstancePlugin): order = pype.api.ValidateContentsOrder hosts = ['maya'] - families = ['studio.model'] + families = ["studio.model'] category = 'cleanup' optional = True version = (0, 1, 0) label = 'Suffix Naming Conventions' - actions = [pype.api.SelectInvalidAction] + actions = [pype.maya.action.SelectInvalidAction] @staticmethod def is_valid_name(node_name, shape_type): diff --git a/pype/plugins/maya/publish/validate_transform_zero.py b/pype/plugins/maya/publish/validate_transform_zero.py index 5a2e50c93f..33182476db 100644 --- a/pype/plugins/maya/publish/validate_transform_zero.py +++ b/pype/plugins/maya/publish/validate_transform_zero.py @@ -2,6 +2,7 @@ from maya import cmds import pyblish.api import pype.api +import pype.maya.action class ValidateTransformZero(pyblish.api.Validator): @@ -15,11 +16,11 @@ class ValidateTransformZero(pyblish.api.Validator): order = pype.api.ValidateContentsOrder hosts = ["maya"] - families = ["model"] + families = ["studio.model"] category = "geometry" version = (0, 1, 0) label = "Transform Zero (Freeze)" - actions = [pype.api.SelectInvalidAction] + actions = [pype.maya.action.SelectInvalidAction] _identity = [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, diff --git a/pype/plugins/maya/publish/validate_vrayproxy_members.py b/pype/plugins/maya/publish/validate_vrayproxy_members.py new file mode 100644 index 0000000000..c0a377092b --- /dev/null +++ b/pype/plugins/maya/publish/validate_vrayproxy_members.py @@ -0,0 +1,39 @@ +import pyblish.api +import pype.api + +from maya import cmds + +import pype.maya.action + + +class ValidateVrayProxyMembers(pyblish.api.InstancePlugin): + """Validate whether the V-Ray Proxy instance has shape members""" + + order = pyblish.api.ValidatorOrder + label = 'VRay Proxy Members' + hosts = ['maya'] + families = ["studio.vrayproxy'] + actions = [pype.maya.action.SelectInvalidAction] + + def process(self, instance): + + invalid = self.get_invalid(instance) + + if invalid: + raise RuntimeError("'%s' is invalid VRay Proxy for " + "export!" % instance.name) + + @classmethod + def get_invalid(cls, instance): + + shapes = cmds.ls(instance, + shapes=True, + noIntermediate=True, + long=True) + + if not shapes: + cls.log.error("'%s' contains no shapes." % instance.name) + + # Return the instance itself + return [instance.name] + diff --git a/pype/plugins/maya/publish/validate_yeti_renderscript_callbacks.py b/pype/plugins/maya/publish/validate_yeti_renderscript_callbacks.py new file mode 100644 index 0000000000..a9a95f7664 --- /dev/null +++ b/pype/plugins/maya/publish/validate_yeti_renderscript_callbacks.py @@ -0,0 +1,94 @@ +from maya import cmds + +import pyblish.api +import pype.api + + +class ValidateYetiRenderScriptCallbacks(pyblish.api.InstancePlugin): + """Check if the render script callbacks will be used during the rendering + + In order to ensure the render tasks are executed properly we need to check + if the pre and post render callbacks are actually used. + + For example: + Yeti is not loaded but its callback scripts are still set in the + render settings. This will cause an error because Maya tries to find + and execute the callbacks. + + Developer note: + The pre and post render callbacks cannot be overridden + + """ + + order = pype.api.ValidateContentsOrder + label = "Yeti Render Script Callbacks" + hosts = ["maya"] + families = ["studio.renderlayer"] + + def process(self, instance): + + invalid = self.get_invalid(instance) + if invalid: + raise ValueError("Invalid render callbacks found for '%s'!" + % instance.name) + + @classmethod + def get_invalid(cls, instance): + + # lookup per render + render_scripts = {"vray": + {"pre": "catch(`pgYetiVRayPreRender`)", + "post": "catch(`pgYetiVRayPostRender`)"}, + "arnold": + {"pre": "pgYetiPreRender"} + } + + yeti_loaded = cmds.pluginInfo("pgYetiMaya", query=True, loaded=True) + + renderer = instance.data["renderer"] + if renderer == "redshift": + cls.log.info("Redshift ignores any pre and post render callbacks") + return False + + callback_lookup = render_scripts.get(renderer, {}) + if not callback_lookup: + cls.log.warning("Renderer '%s' is not supported in this plugin" + % renderer) + return False + + pre_render_callback = cmds.getAttr("defaultRenderGlobals.preMel") + post_render_callback = cmds.getAttr("defaultRenderGlobals.postMel") + + pre_callbacks = pre_render_callback.split(";") + post_callbacks = post_render_callback.split(";") + + pre_script = callback_lookup.get("pre", "") + post_script = callback_lookup.get("post", "") + + # If not loaded + invalid = False + if not yeti_loaded: + if pre_script and pre_script in pre_callbacks: + cls.log.error("Found pre render callback '%s' which is not " + "uses!" % pre_script) + invalid = True + + if post_script and post_script in post_callbacks: + cls.log.error("Found post render callback '%s which is " + "not used!" % post_script) + invalid = True + else: + if pre_script: + if pre_script not in pre_callbacks: + cls.log.error( + "Could not find required pre render callback " + "`%s`" % pre_script) + invalid = True + + if post_script: + if post_script not in post_callbacks: + cls.log.error("Could not find required post render callback" + " `%s`" % post_script) + invalid = True + + return invalid diff --git a/pype/plugins/maya/publish/validate_yeti_rig_input_in_instance.py b/pype/plugins/maya/publish/validate_yeti_rig_input_in_instance.py index bfc602ee47..a9b4bb9712 100644 --- a/pype/plugins/maya/publish/validate_yeti_rig_input_in_instance.py +++ b/pype/plugins/maya/publish/validate_yeti_rig_input_in_instance.py @@ -2,6 +2,7 @@ from maya import cmds import pyblish.api import pype.api +import pype.maya.action class ValidateYetiRigInputShapesInInstance(pyblish.api.Validator): @@ -9,9 +10,9 @@ class ValidateYetiRigInputShapesInInstance(pyblish.api.Validator): order = pype.api.ValidateContentsOrder hosts = ["maya"] - families = ["yetiRig"] + families = ["studio.yetiRig"] label = "Yeti Rig Input Shapes In Instance" - actions = [pype.api.SelectInvalidAction] + actions = [pype.maya.action.SelectInvalidAction] def process(self, instance): diff --git a/pype/plugins/maya/publish/validate_yeti_rig_settings.py b/pype/plugins/maya/publish/validate_yeti_rig_settings.py new file mode 100644 index 0000000000..b88d5cbeee --- /dev/null +++ b/pype/plugins/maya/publish/validate_yeti_rig_settings.py @@ -0,0 +1,38 @@ +import pyblish.api + + +class ValidateYetiRigSettings(pyblish.api.InstancePlugin): + order = pyblish.api.ValidatorOrder + label = "Validate Yeti Rig Settings" + families = ["studio.yetiRig"] + + def process(self, instance): + + invalid = self.get_invalid(instance) + if invalid: + raise RuntimeError("Detected invalid Yeti Rig data. " + "Tip: Save the scene") + + @classmethod + def get_invalid(cls, instance): + + rigsettings = instance.data.get("rigsettings", {}) + if not rigsettings: + cls.log.error("MAJOR ERROR: No rig settings found!") + return True + + # Get inputs + inputs = rigsettings.get("inputs", []) + for input in inputs: + source_id = input["sourceID"] + if source_id is None: + cls.log.error("Discovered source with 'None' as ID, please " + "check if the input shape has an cbId") + return True + + destination_id = input["destinationID"] + if destination_id is None: + cls.log.error("Discovered None as destination ID value") + return True + + return False diff --git a/pype/plugins/maya/publish/validate_yetirig_cache_state.py b/pype/plugins/maya/publish/validate_yetirig_cache_state.py index 707ddae2cd..0a65ae97e2 100644 --- a/pype/plugins/maya/publish/validate_yetirig_cache_state.py +++ b/pype/plugins/maya/publish/validate_yetirig_cache_state.py @@ -4,6 +4,8 @@ import pype.action import maya.cmds as cmds +import pype.maya.action + class ValidateYetiRigCacheState(pyblish.api.InstancePlugin): """Validate the I/O attributes of the node @@ -17,9 +19,9 @@ class ValidateYetiRigCacheState(pyblish.api.InstancePlugin): order = pyblish.api.ValidatorOrder label = "Yeti Rig Cache State" hosts = ["maya"] - families = ["yetiRig"] + families = ["studio.yetiRig"] actions = [pype.action.RepairAction, - pype.action.SelectInvalidAction] + pype.maya.action.SelectInvalidAction] def process(self, instance): invalid = self.get_invalid(instance) @@ -58,3 +60,4 @@ class ValidateYetiRigCacheState(pyblish.api.InstancePlugin): for node in invalid: cmds.setAttr("%s.fileMode" % node, 0) cmds.setAttr("%s.cacheFileName" % node, "", type="string") + diff --git a/pype/scripts/fusion_switch_shot.py b/pype/scripts/fusion_switch_shot.py index f155294db8..6e6dc04733 100644 --- a/pype/scripts/fusion_switch_shot.py +++ b/pype/scripts/fusion_switch_shot.py @@ -8,7 +8,7 @@ from avalon import api, io, pipeline import avalon.fusion # Config imports -import pype.lib as lib +import pype.lib pype import pype.fusion.lib as fusion_lib log = logging.getLogger("Update Slap Comp") @@ -87,7 +87,7 @@ def _format_filepath(session): # Create new unqiue filepath if os.path.exists(new_filepath): - new_filepath = lib.version_up(new_filepath) + new_filepath = studio.version_up(new_filepath) return new_filepath @@ -189,8 +189,8 @@ def switch(asset_name, filepath=None, new=True): representations = [] for container in containers: try: - representation = lib.switch_item(container, - asset_name=asset_name) + representation = pype.switch_item(container, + asset_name=asset_name) representations.append(representation) except Exception as e: current_comp.Print("Error in switching! %s\n" % e.message) @@ -209,7 +209,7 @@ def switch(asset_name, filepath=None, new=True): # Update savers output based on new session _update_savers(current_comp, switch_to_session) else: - comp_path = lib.version_up(filepath) + comp_path = pype.version_up(filepath) current_comp.Print(comp_path) diff --git a/pype/setdress_api.py b/pype/setdress_api.py index c6de0a4f74..f6dcf95065 100644 --- a/pype/setdress_api.py +++ b/pype/setdress_api.py @@ -137,7 +137,7 @@ def load_package(filepath, name, namespace=None): # manager # for container in containers: # cmds.setAttr("%s.id" % container, - # "setdress.container", + # "studio.setdress.container", # type="string") # TODO: Lock all loaded nodes diff --git a/setup/fusion/scripts/Comp/colorbleed/set_rendermode.py b/setup/fusion/scripts/Comp/colorbleed/set_rendermode.py index 567dfc2aa7..0fbcf1bf86 100644 --- a/setup/fusion/scripts/Comp/colorbleed/set_rendermode.py +++ b/setup/fusion/scripts/Comp/colorbleed/set_rendermode.py @@ -97,11 +97,11 @@ class SetRenderMode(QtWidgets.QWidget): return self._comp.GetAttrs("COMPS_Name") def _get_comp_rendermode(self): - return self._comp.GetData("studio.rendermode") or "renderlocal" + return self._comp.GetData("colorbleed.rendermode") or "renderlocal" def _set_comp_rendermode(self): rendermode = self.mode_options.currentText() - self._comp.SetData("studio.rendermode", rendermode) + self._comp.SetData("colorbleed.rendermode", rendermode) self._comp.Print("Updated render mode to '%s'\n" % rendermode) diff --git a/setup/fusion/scripts/Comp/colorbleed/switch_ui.py b/setup/fusion/scripts/Comp/colorbleed/switch_ui.py index 92685a1ef6..8f1466abe0 100644 --- a/setup/fusion/scripts/Comp/colorbleed/switch_ui.py +++ b/setup/fusion/scripts/Comp/colorbleed/switch_ui.py @@ -154,7 +154,7 @@ class App(QtWidgets.QWidget): asset = self._assets.currentText() - import config.scripts.fusion_switch_shot as switch_shot + import colorbleed.scripts.fusion_switch_shot as switch_shot switch_shot.switch(asset_name=asset, filepath=file_name, new=True) def _get_context_directory(self):