diff --git a/colorbleed.bat b/colorbleed.bat deleted file mode 100644 index 3bf3119e6a..0000000000 --- a/colorbleed.bat +++ /dev/null @@ -1,4 +0,0 @@ -:: Set paths to ensure plugins have access to the inhouse tools - -set PYTHONPATH=%PYTHONPATH%;P:\pipeline\dev\git\cb; -set PYTHONPATH=%PYTHONPATH%;P:\pipeline\dev\git\cbra; diff --git a/colorbleed/__init__.py b/colorbleed/__init__.py index a6d0890f25..f29597a297 100644 --- a/colorbleed/__init__.py +++ b/colorbleed/__init__.py @@ -8,10 +8,11 @@ PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish") def install(): publish_path = os.path.join(PLUGINS_DIR, "publish") - print("Registering global plug-ins..") + pyblish.register_plugin_path(publish_path) def uninstall(): - pyblish.deregister_plugin_path(PUBLISH_PATH) \ No newline at end of file + pyblish.deregister_plugin_path(PUBLISH_PATH) + diff --git a/colorbleed/action.py b/colorbleed/action.py index fef737195d..382699035e 100644 --- a/colorbleed/action.py +++ b/colorbleed/action.py @@ -1,10 +1,13 @@ # absolute_import is needed to counter the `module has no cmds error` in Maya from __future__ import absolute_import -import pyblish.api +import os +import uuid from maya import cmds +import pyblish.api + def get_errored_instances_from_context(context): @@ -34,7 +37,7 @@ def get_errored_plugins_from_data(context): plugins = list() results = context.data.get("results", []) for result in results: - if result["success"] == True: + if result["success"] is True: continue plugins.append(result["plugin"]) @@ -150,8 +153,6 @@ class GenerateUUIDsOnInvalidAction(pyblish.api.Action): icon = "wrench" # Icon from Awesome Icon def process(self, context, plugin): - import cbra.lib - import cbra.utils.maya.node_uuid as id_utils self.log.info("Finding bad nodes..") @@ -182,15 +183,73 @@ class GenerateUUIDsOnInvalidAction(pyblish.api.Action): # Parse context from current file self.log.info("Parsing current context..") - try: - current_file = context.data['currentFile'] - context = cbra.lib.parse_context(current_file) - except RuntimeError, e: - self.log.error("Can't generate UUIDs because scene isn't " - "in new-style pipeline: ".format(current_file)) - raise e + print(">>> DEBUG CONTEXT :", context) + print(">>> DEBUG CONTEXT DATA:", context.data) - # Generate and add the ids to the nodes - ids = id_utils.generate_ids(context, invalid) - id_utils.add_ids(ids) + # # Generate and add the ids to the nodes + node_ids = self.generate_ids(context, invalid) + self.apply_ids(node_ids) self.log.info("Generated ids on nodes: {0}".format(invalid)) + + def get_context(self, instance=None): + + PROJECT = os.environ["AVALON_PROJECT"] + ASSET = instance.data.get("asset") or os.environ["AVALON_ASSET"] + SILO = os.environ["AVALON_SILO"] + LOCATION = os.getenv("AVALON_LOCATION") + + return {"project": PROJECT, + "asset": ASSET, + "silo": SILO, + "location": LOCATION} + + def generate_ids(self, context, nodes): + """Generate cb UUIDs for nodes. + + The identifiers are formatted like: + assets:character/test:bluey:46D221D9-4150-8E49-6B17-43B04BFC26B6 + + This is a concatenation of: + - entity (shots or assets) + - folders (parent hierarchy) + - asset (the name of the asset) + - uuid (unique id for node in the scene) + + Raises: + RuntimeError: When context can't be parsed of the current asset + + Returns: + dict: node, uuid dictionary + + """ + + # Make a copy of the context + data = context.copy() + + # Define folders + + node_ids = dict() + for node in nodes: + # Generate a unique ID per node + data['uuid'] = uuid.uuid4() + unique_id = "{asset}:{item}:{uuid}".format(**data) + node_ids[node] = unique_id + + return node_ids + + def apply_ids(self, node_ids): + """Apply the created unique IDs to the node + Args: + node_ids (dict): each node with a unique id + + Returns: + None + """ + + attribute = "mbId" + for node, id in node_ids.items(): + # check if node has attribute + if not cmds.attributeQuery(attribute, node=node, exists=True): + cmds.addAttr(node, longName=attribute, dataType="string") + + cmds.setAttr("{}.{}".format(node, attribute), id) diff --git a/colorbleed/api.py b/colorbleed/api.py index 33924cb02b..531a63a50d 100644 --- a/colorbleed/api.py +++ b/colorbleed/api.py @@ -21,7 +21,9 @@ from .action import ( ) all = [ + # plugin classes "Extractor", + # ordering "ValidatePipelineOrder", "ValidateContentsOrder", "ValidateSceneOrder", diff --git a/colorbleed/filetypes.py b/colorbleed/filetypes.py new file mode 100644 index 0000000000..fbefeff9be --- /dev/null +++ b/colorbleed/filetypes.py @@ -0,0 +1,2 @@ +accepted_images_types = [".png", ".jpg", ".tga", ".tiff"] +ignored_images_types = [".pds"] diff --git a/colorbleed/maya/__init__.py b/colorbleed/maya/__init__.py index 7e1da1a190..3097683464 100644 --- a/colorbleed/maya/__init__.py +++ b/colorbleed/maya/__init__.py @@ -1,9 +1,12 @@ import os import site +import uuid -from avalon import api as avalon +from avalon import maya, io, api as avalon from pyblish import api as pyblish +from maya import cmds + from . import menu PARENT_DIR = os.path.dirname(__file__) @@ -28,6 +31,11 @@ def install(): menu.install() + print("Installing callbacks ... ") + avalon.on("init", on_init) + avalon.on("new", on_new) + avalon.on("save", on_save) + def uninstall(): pyblish.deregister_plugin_path(PUBLISH_PATH) @@ -35,3 +43,88 @@ def uninstall(): avalon.deregister_plugin_path(avalon.Creator, CREATE_PATH) menu.uninstall() + + +def _set_uuid(asset_id, node): + """Add cbId to `node` + Unless one already exists. + """ + + attr = "{0}.cbId".format(node) + if not cmds.attributeQuery("cbId", node=node, exists=True): + cmds.addAttr(node, longName="cbId", dataType="string") + _, uid = str(uuid.uuid4()).rsplit("-", 1) + cb_uid = "{}:{}".format(asset_id, uid) + + cmds.setAttr(attr, cb_uid, type="string") + + +def _copy_uuid(source, target): + + source_attr = "{0}.cbId".format(source) + target_attr = "{0}.cbId".format(target) + if not cmds.attributeQuery("cbId", node=target, exists=True): + cmds.addAttr(target, longName="cbId", dataType="string") + + attribute_value = cmds.getAttr(source_attr) + cmds.setAttr(target_attr, attribute_value, type="string") + + +def on_init(): + avalon.logger.info("Running callback on init..") + + maya.commands.reset_frame_range() + maya.commands.reset_resolution() + + +def on_new(): + avalon.logger.info("Running callback on new..") + + # Load dependencies + cmds.loadPlugin("AbcExport.mll", quiet=True) + cmds.loadPlugin("AbcImport.mll", quiet=True) + + maya.commands.reset_frame_range() + maya.commands.reset_resolution() + + +def on_save(): + """Automatically add IDs to new nodes + Any transform of a mesh, without an existing ID, + is given one automatically on file save. + """ + + avalon.logger.info("Running callback on save..") + + defaults = ["initialShadingGroup", "initialParticleSE"] + + # the default items which always want to have an ID + types = ["mesh", "shadingEngine", "file", "nurbsCurve"] + + # the items which need to pass the id to their parent + nodes = (set(cmds.ls(type=types, long=True)) - + set(cmds.ls(long=True, readOnly=True)) - + set(cmds.ls(long=True, lockedNodes=True))) + + transforms = set() + for n in cmds.ls(type=types, long=True): + # pass id to parent of node if in subtypes + relatives = cmds.listRelatives(n, parent=True, fullPath=True) + if not relatives: + continue + + for r in cmds.listRelatives(n, parent=True, fullPath=True): + transforms.add(r) + + # merge transforms and nodes in one set to make sure every item + # is unique + nodes |= transforms + + # Lead with asset ID from the database + asset = os.environ["AVALON_ASSET"] + asset_id = io.find_one({"type": "asset", "name": asset}) + + for node in nodes: + if node in defaults: + continue + _set_uuid(str(asset_id["_id"]), node) diff --git a/colorbleed/maya/commands.py b/colorbleed/maya/commands.py index b31e4abe64..c2943128a3 100644 --- a/colorbleed/maya/commands.py +++ b/colorbleed/maya/commands.py @@ -612,10 +612,10 @@ def auto_connect_assets(src, dst): break for input_transform in cmds.sets(in_set, query=True): - mbid = cmds.getAttr(input_transform + ".mbID") + mbid = cmds.getAttr(input_transform + ".cbId") input_shape = cmds.listRelatives(input_transform, shapes=True)[0] - for output_transform in lib.lsattr("mbID", value=mbid): + for output_transform in lib.lsattr("cbId", value=mbid): ref = cmds.referenceQuery(output_transform, referenceNode=True) if ref != src: diff --git a/colorbleed/maya/lib.py b/colorbleed/maya/lib.py index 56a54e7413..a2715863ee 100644 --- a/colorbleed/maya/lib.py +++ b/colorbleed/maya/lib.py @@ -1,15 +1,25 @@ """Standalone helper functions""" import re -import contextlib -from collections import OrderedDict -import logging import os +import bson import json +import logging +import contextlib +from collections import OrderedDict, defaultdict + +from avalon import maya, io + +from maya import cmds, mel + log = logging.getLogger(__name__) -from maya import cmds +project = io.find_one({"type": "project", + "name": os.environ["AVALON_PROJECT"]}, + projection={"config.template.publish": True, + "_id": False}) +TEMPLATE = project["config"]["template"]["publish"] def maintained_selection(arg=None): @@ -249,6 +259,78 @@ def get_current_renderlayer(): return cmds.editRenderLayerGlobals(query=True, currentRenderLayer=True) +@contextlib.contextmanager +def no_undo(flush=False): + """Disable the undo queue during the context + + Arguments: + flush (bool): When True the undo queue will be emptied when returning + from the context losing all undo history. Defaults to False. + + """ + original = cmds.undoInfo(query=True, state=True) + keyword = 'state' if flush else 'stateWithoutFlush' + + try: + cmds.undoInfo(**{keyword: False}) + yield + finally: + cmds.undoInfo(**{keyword: original}) + + +def polyConstraint(components, *args, **kwargs): + """Return the list of *components* with the constraints applied. + + A wrapper around Maya's `polySelectConstraint` to retrieve its results as + a list without altering selections. For a list of possible constraints + see `maya.cmds.polySelectConstraint` documentation. + + Arguments: + components (list): List of components of polygon meshes + + Returns: + list: The list of components filtered by the given constraints. + + """ + + kwargs.pop('mode', None) + + with no_undo(flush=False): + print("la") + with maintained_selection(): + print("po") + # Apply constraint using mode=2 (current and next) so + # it applies to the selection made before it; because just + # a `maya.cmds.select()` call will not trigger the constraint. + with reset_polySelectConstraint(): + print("do") + cmds.select(components, r=1) + cmds.polySelectConstraint(*args, mode=2, **kwargs) + result = cmds.ls(selection=True) + + return result + + +@contextlib.contextmanager +def reset_polySelectConstraint(reset=True): + """Context during which the given polyConstraint settings are disabled. + + The original settings are restored after the context. + + """ + + original = cmds.polySelectConstraint(query=True, stateString=True) + + try: + if reset: + # Reset all parameters + mel.eval("resetPolySelectConstraint;") + cmds.polySelectConstraint(disable=True) + yield + finally: + mel.eval(original) + + def is_visible(node, displayLayer=True, intermediateObject=True, @@ -344,8 +426,8 @@ def extract_alembic(file, startFrame=None, endFrame=None, selection=True, - uvWrite=True, - eulerFilter=True, + uvWrite= True, + eulerFilter= True, dataFormat="ogawa", verbose=False, **kwargs): @@ -449,14 +531,14 @@ def extract_alembic(file, valid_types = _alembic_options[key] if not isinstance(value, valid_types): raise TypeError("Alembic option unsupported type: " - "{0} (expected {1}}".format(value, valid_types)) + "{0} (expected {1})".format(value, valid_types)) # Format the job string from options job_args = list() for key, value in options.items(): if isinstance(value, (list, tuple)): for entry in value: - job_args.append("-{0} {1}".format(key, entry)) + job_args.append("-{} {}".format(key, entry)) elif isinstance(value, bool): job_args.append("-{0}".format(key)) else: @@ -476,9 +558,282 @@ def extract_alembic(file, log.debug("Extracting Alembic with job arguments: %s", job_str) # Perform extraction + print("Alembic Job Arguments : {}".format(job_str)) + cmds.AbcExport(j=job_str, verbose=verbose) if verbose: log.debug("Extracted Alembic to: %s", file) return file + + +def maya_temp_folder(): + scene_dir = os.path.dirname(cmds.file(query=True, sceneName=True)) + tmp_dir = os.path.abspath(os.path.join(scene_dir, "..", "tmp")) + if not os.path.isdir(tmp_dir): + os.makedirs(tmp_dir) + + return tmp_dir + + +def remap_resource_nodes(resources, folder=None): + + log.info("Updating resource nodes ...") + for resource in resources: + source = resource["source"] + if folder: + fname = os.path.basename(source) + fpath = os.path.join(folder, fname) + else: + fpath = source + + node_attr = resource["attribute"] + cmds.setAttr(node_attr, fpath, type="string") + + log.info("Saving file ...") + cmds.file(save=True, type="mayaAscii") + + +def _get_id(node): + """ + Get the `cbId` attribute of the given node + Args: + node (str): the name of the node to retrieve the attribute from + + Returns: + str + + """ + + if node is None: + return + + try: + attr = "{}.cbId".format(node) + attribute_value = cmds.getAttr(attr) + except Exception as e: + log.debug(e) + return + + return attribute_value + + +def filter_by_id(nodes, uuids): + """Filter all nodes which match the UUIDs + + Args: + nodes (list): collection of nodes to check + uuids (list): a list of UUIDs which are linked to the shader + + Returns: + list: matching nodes + """ + + filtered_nodes = [] + for node in nodes: + if node is None: + continue + + if not cmds.attributeQuery("cbId", node=node, exists=True): + continue + + # Deformed shaped + attr = "{}.cbId".format(node) + attribute_value = cmds.getAttr(attr) + if attribute_value not in uuids: + continue + + filtered_nodes.append(node) + + return filtered_nodes + + +def get_representation_file(representation, template=TEMPLATE): + """ + Rebuild the filepath of the representation's context + Args: + representation (dict): data of the registered in the database + template (str): the template to fill + + Returns: + str + + """ + context = representation["context"].copy() + context["root"] = os.environ["AVALON_ROOT"] + return template.format(**context) + + +def get_reference_node(path): + """ + Get the reference node when the path is found being used in a reference + Args: + path (str): the file path to check + + Returns: + node (str): name of the reference node in question + """ + node = cmds.file(path, query=True, referenceNode=True) + reference_path = cmds.referenceQuery(path, filename=True) + if os.path.normpath(path) == os.path.normpath(reference_path): + return node + + +def list_looks(asset_id): + """Return all look subsets for the given asset + + This assumes all look subsets start with "look*" in their names. + """ + + # # get all subsets with look leading in + # the name associated with the asset + subset = io.find({"parent": asset_id, + "type": "subset", + "name": {"$regex": "look*"}}) + + return list(subset) + + +def assign_look_by_version(nodes, version_id): + """Assign nodes a specific published look version by id. + + This assumes the nodes correspond with the asset. + + Args: + nodes(list): nodes to assign look to + version_id (bson.ObjectId) + + Returns: + None + """ + + # get representations of shader file and relationships + shader_file = io.find_one({"type": "representation", + "parent": version_id, + "name": "ma"}) + + shader_relations = io.find_one({"type": "representation", + "parent": version_id, + "name": "json"}) + + # Load file + shader_filepath = get_representation_file(shader_file) + shader_relation = get_representation_file(shader_relations) + + reference_node = get_reference_node(shader_filepath) + if reference_node is None: + log.info("Loading lookdev for the first time..") + + # Define namespace + assetname = shader_file['context']['asset'] + ns_assetname = "{}_".format(assetname) + namespace = maya.unique_namespace(ns_assetname, + format="%03d", + suffix="_look") + + # Reference the look file + with maya.maintained_selection(): + shader_nodes = cmds.file(shader_filepath, + namespace=namespace, + reference=True, + returnNewNodes=True) + else: + log.info("Reusing existing lookdev..") + shader_nodes = cmds.referenceQuery(reference_node, nodes=True) + + # Assign relationships + with open(shader_relation, "r") as f: + relationships = json.load(f) + + apply_shaders(relationships, shader_nodes, nodes) + + +def assign_look(nodes, subset="lookDefault"): + """Assigns a look to a node. + + Optimizes the nodes by grouping by asset id and finding + related subset by name. + + Args: + nodes (list): all nodes to assign the look to + subset (str): name of the subset to find + """ + + # Group all nodes per asset id + grouped = defaultdict(list) + for node in nodes: + colorbleed_id = cmds.getAttr("{}.cbId".format(node)) + asset_id = colorbleed_id.split(":")[0] + grouped[asset_id].append(node) + + for asset_id, asset_nodes in grouped.items(): + # create objectId for database + asset_id = bson.ObjectId(asset_id) + subset = io.find_one({"type": "subset", + "name": subset, + "parent": asset_id}) + + assert subset, "No subset found for {}".format(asset_id) + + # get last version + version = io.find_one({"parent": subset['_id'], + "type": "version", + "data.families": + {"$in":["colorbleed.lookdev"]} + }, + sort=[("name", -1)], + projection={"_id": True}) + + log.debug("Assigning look '{}' <{}> to nodes: {}".format(subset, + version, + asset_nodes)) + + assign_look_by_version(asset_nodes, version['_id']) + + +def apply_shaders(relationships, shader_nodes, nodes): + """Apply all shaders to the nodes based on the relationship data + + Args: + relationships (dict): shader to node relationships + shader_nodes (list): shader network nodes + nodes (list): nodes to assign to + + Returns: + None + """ + + shader_sets = relationships.get("sets", []) + shading_engines = cmds.ls(shader_nodes, type="shadingEngine", long=True) + assert len(shading_engines) > 0, ("Error in retrieving shading engine " + "from reference") + + # Pre-filter nodes and shader nodes + nodes_by_id = defaultdict(list) + shader_nodes_by_id = defaultdict(list) + for node in nodes: + _id = _get_id(node) + nodes_by_id[_id].append(node) + + for shader_node in shader_nodes: + _id = _get_id(shader_node) + shader_nodes_by_id[_id].append(shader_node) + + # get all nodes which we need to link per shader + for shader_set in shader_sets: + # collect shading engine + uuid = shader_set["uuid"] + shading_engine = shader_nodes_by_id.get(uuid, []) + assert len(shading_engine) == 1, ("Could not find the correct " + "shading engine with cbId " + "'{}'".format(uuid)) + + # collect members + filtered_nodes = list() + for member in shader_set["members"]: + member_uuid = member["uuid"] + members = nodes_by_id.get(member_uuid, []) + filtered_nodes.extend(members) + + cmds.sets(filtered_nodes, forceElement=shading_engine[0]) diff --git a/colorbleed/maya/menu.json b/colorbleed/maya/menu.json new file mode 100644 index 0000000000..4931063ee7 --- /dev/null +++ b/colorbleed/maya/menu.json @@ -0,0 +1,1861 @@ +{ + "order": [ + "main", + "Modeling", + "Rigging", + "Shading", + "Animation", + "Layout", + "Particles", + "Cleanup", + "Projects", + "Pyblish", + "Others" + ], + "main": [ + { + "command": "$COLORBLEED_SCRIPTS\\others\\save_scene_incremental.py", + "sourcetype": "file", + "title": "Version Up", + "tooltip": "Incremental save with a specific format" + }, + { + "title": "separator" + } + ], + "Modeling": [ + { + "command": "$COLORBLEED_SCRIPTS\\modeling\\duplicate_normalized.py", + "sourcetype": "file", + "tags": [ + "modeling", + "duplicate", + "normalized" + ], + "title": "Duplicate Normalized", + "tooltip": "" + }, + { + "command": "$COLORBLEED_SCRIPTS\\modeling\\transferUVs.py", + "sourcetype": "file", + "tags": [ + "modeling", + "transfer", + "uv" + ], + "title": "Transfer UVs", + "tooltip": "" + }, + { + "command": "$COLORBLEED_SCRIPTS\\modeling\\mirrorSymmetry.py", + "sourcetype": "file", + "tags": [ + "modeling", + "mirror", + "symmetry" + ], + "title": "Mirror Symmetry", + "tooltip": "" + }, + { + "command": "$COLORBLEED_SCRIPTS\\modeling\\selectOutlineUI", + "sourcetype": "file", + "tags": [ + "modeling", + "select", + "outline", + "ui" + ], + "title": "Select Outline UI", + "tooltip": "" + }, + { + "command": "$COLORBLEED_SCRIPTS\\modeling\\polyDeleteOtherUVSets.py", + "sourcetype": "file", + "tags": [ + "modeling", + "polygon", + "uvset", + "delete" + ], + "title": "Polygon Delete Other UV Sets", + "tooltip": "" + }, + { + "command": "$COLORBLEED_SCRIPTS\\modeling\\polyCombineQuick.py", + "sourcetype": "file", + "tags": [ + "modeling", + "combine", + "polygon", + "quick" + ], + "title": "Polygon Combine Quick", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "modeling", + "separateMeshPerShader" + ], + "title": "Separate Mesh Per Shader", + "tooltip": "" + }, + { + "command": "$COLORBLEED_SCRIPTS\\modeling\\polyDetachSeparate.py", + "sourcetype": "file", + "tags": ["modeling", "poly","detach","separate"], + "title": "Polygon Detach and Separate", + "tooltip": "" + }, + { + "command": "$COLORBLEED_SCRIPTS\\modeling\\polyRelaxVerts.py", + "sourcetype": "file", + "tags": [ + "modeling", + "relax", + "verts" + ], + "title": "Polygon Relax Vertices", + "tooltip": "" + }, + { + "command": "$COLORBLEED_SCRIPTS\\modeling\\polySelectEveryNthEdgeUI.py", + "sourcetype": "file", + "tags": ["modeling", "select","nth", "edge", "ui"], + "title": "Select Every Nth Edge" + }, + { + "command": "$COLORBLEED_SCRIPTS\\modeling\\djPFXUVs.py", + "sourcetype": "file", + "tags": [ + "modeling", + "djPFX", "UVs" + ], + "title": "dj PFX UVs", + "tooltip": "" + } + ], + "Animation": [ + { + "command": "", + "sourcetype": "", + "tags": [ + "animation", + "attributes" + ], + "title": "Attributes", + "tooltip": "", + "items": [ + { + "command": "$COLORBLEED_SCRIPTS\\animation\\attributes\\copyValues.py", + "sourcetype": "file", + "tags": [ + "animation", + "copy", + "attributes" + ], + "title": "Copy Values", + "tooltip": "Copy attribute values" + }, + { + "command": "$COLORBLEED_SCRIPTS\\animation\\attributes\\copyInConnections.py", + "sourcetype": "file", + "tags": [ + "animation", + "copy", + "attributes", + "connections", + "incoming" + ], + "title": "Copy In Connections", + "tooltip": "Copy incoming connections" + }, + { + "command": "$COLORBLEED_SCRIPTS\\animation\\attributes\\copyOutConnections.py", + "sourcetype": "file", + "tags": [ + "animation", + "copy", + "attributes", + "connections", + "out" + ], + "title": "Copy Out Connections", + "tooltip": "Copy outcoming connections" + }, + { + "command": "$COLORBLEED_SCRIPTS\\animation\\attributes\\copyTransformLocal.py", + "sourcetype": "file", + "tags": [ + "animation", + "copy", + "attributes", + "transforms", + "local" + ], + "title": "Copy Local Transfroms", + "tooltip": "Copy local transfroms" + }, + { + "command": "$COLORBLEED_SCRIPTS\\animation\\attributes\\copyTransformMatrix.py", + "sourcetype": "file", + "tags": [ + "animation", + "copy", + "attributes", + "transforms", + "matrix" + ], + "title": "Copy Matrix Transfroms", + "tooltip": "Copy Matrix transfroms" + }, + { + "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" + }, + { + "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" + } + ] + }, + { + "command": "", + "sourcetype": "file", + "tags": [], + "title": "Optimize", + "tooltip": "Optimization scripts", + "items": [ + { + "command": "$COLORBLEED_SCRIPTS\\animation\\optimize\\toggleFreezeHierarchy.py", + "sourcetype": "file", + "tags": [ + "animation", + "hierarchy", + "toggle", + "freeze" + ], + "title": "Toggle Freeze Hierarchy", + "tooltip": "Freeze and unfreeze hierarchy" + }, + { + "command": "$COLORBLEED_SCRIPTS\\animation\\optimize\\toggleParallelNucleus.py", + "sourcetype": "file", + "tags": [ + "animation", + "nucleus", + "toggle", + "parallel" + ], + "title": "Toggle Parallel Nucleus", + "tooltip": "Toggle parallel nucleus" + } + ] + }, + { + "command": "$COLORBLEED_SCRIPTS\\animation\\zvParentMaster.py", + "sourcetype": "file", + "tags": [ + "animation", + "zvParentMaster" + ], + "title": "ZV Parent Master", + "tooltip": "Open ZV Parent M UI" + }, + { + "command": "$COLORBLEED_SCRIPTS\\animation\\pathAnimation.py", + "sourcetype": "file", + "tags": [ + "animation", + "path" + ], + "title": "Path Animation", + "tooltip": "" + }, + { + "command": "$COLORBLEED_SCRIPTS\\animation\\timeStepper.py", + "sourcetype": "file", + "tags": [ + "animation", + "timeStepper" + ], + "title": "TimeStepper", + "tooltip": "" + }, + { + "command": "$COLORBLEED_SCRIPTS\\animation\\bakeSelectedToWorldSpace.py", + "sourcetype": "file", + "tags": [ + "animation", + "world space", + "bake" + ], + "title": "Bake To World Space", + "tooltip": "Select the item which need to be baked to World Space" + }, + { + "command": "$COLORBLEED_SCRIPTS\\animation\\simplePlayblastUI.py", + "sourcetype": "file", + "tags": [ + "animation", + "gui", + "simple", + "capture" + ], + "title": "Capture GUI - simple", + "tooltip": "Simplified version of the Capture GUI" + }, + { + "command": "$COLORBLEED_SCRIPTS\\animation\\capture_ui.py", + "sourcetype": "file", + "tags": [ + "animation", + "capture", + "screenshot", + "movie" + ], + "title": "Capture GUI", + "tooltip": "Render current camera to an image or movie" + }, + { + "command": "$COLORBLEED_SCRIPTS\\animation\\anim_scene_optimizer.py", + "sourcetype": "file", + "tags": [ + "animation", + "optimizer", + "scene" + ], + "title": "Animation Scene Optimizer", + "tooltip": "Optimize animation in a scene" + }, + { + "command": "$COLORBLEED_SCRIPTS\\animation\\poseLibrary.py", + "sourcetype": "file", + "tags": [ + "animation", + "poseLibrary" + ], + "title": "Pose Library", + "tooltip": "" + }, + { + "command": "$COLORBLEED_SCRIPTS\\animation\\key_amplifier_ui.py", + "sourcetype": "file", + "tags": [ + "animation", + "key_amplifier_ui" + ], + "title": "Key Amplifier UI", + "tooltip": "" + }, + { + "command": "$COLORBLEED_SCRIPTS\\animation\\selectAllAnimationCurves.py", + "sourcetype": "file", + "tags": [ + "animation", + "curves", + "scene" + ], + "title": "Select All Animation Curves", + "tooltip": "Select all animation curves in the scene" + }, + { + "command": "$COLORBLEED_SCRIPTS\\animation\\offsetSelectedObjectsUI.py", + "sourcetype": "file", + "tags": [ + "animation", + "offsetSelectedObjectsUI" + ], + "title": "Offset Selected Object UI", + "tooltip": "Offset selected objects" + }, + { + "command": "$COLORBLEED_SCRIPTS\\animation\\tweenMachineUI.py", + "sourcetype": "file", + "tags": [ + "animation", + "tweenMachineUI" + ], + "title": "Tween Machine UI", + "tooltip": "" + } + ], + "Rigging": [ + { + "command": "$COLORBLEED_SCRIPTS\\rigging\\addCurveBetween.py", + "sourcetype": "python", + "tags": [ + "rigging", + "addCurveBetween", + "python" + ], + "title": "Add Curve Between" + }, + { + "command": "$COLORBLEED_SCRIPTS\\rigging\\averageSkinWeights.py", + "sourcetype": "python", + "tags": [ + "rigging", + "averageSkinWeights", + "python" + ], + "title": "Average Skin Weights" + }, + { + "command": "$COLORBLEED_SCRIPTS\\rigging\\cbSmoothSkinWeightUI.py", + "sourcetype": "python", + "tags": [ + "rigging", + "cbSmoothSkinWeightUI", + "python" + ], + "title": "CB Smooth Skin Weight UI" + }, + { + "command": "$COLORBLEED_SCRIPTS\\rigging\\channelBoxManagerUI.py", + "sourcetype": "python", + "tags": [ + "rigging", + "channelBoxManagerUI", + "python" + ], + "title": "Channel Box Manager UI" + }, + { + "command": "$COLORBLEED_SCRIPTS\\rigging\\characterAutorigger.py", + "sourcetype": "python", + "tags": [ + "rigging", + "characterAutorigger", + "python" + ], + "title": "Character Auto Rigger" + }, + { + "command": "$COLORBLEED_SCRIPTS\\rigging\\connectUI.py", + "sourcetype": "python", + "tags": [ + "rigging", + "connectUI", + "python" + ], + "title": "Connect UI" + }, + { + "command": "$COLORBLEED_SCRIPTS\\rigging\\copySkinWeightsLocal.py", + "sourcetype": "python", + "tags": [ + "rigging", + "copySkinWeightsLocal", + "python" + ], + "title": "Copy Skin Weights Local" + }, + { + "command": "$COLORBLEED_SCRIPTS\\rigging\\createCenterLocator.py", + "sourcetype": "python", + "tags": [ + "rigging", + "createCenterLocator", + "python" + ], + "title": "Create Center Locator" + }, + { + "command": "$COLORBLEED_SCRIPTS\\rigging\\freezeTransformToGroup.py", + "sourcetype": "python", + "tags": [ + "rigging", + "freezeTransformToGroup", + "python" + ], + "title": "Freeze Transform To Group" + }, + { + "command": "$COLORBLEED_SCRIPTS\\rigging\\groupSelected.py", + "sourcetype": "python", + "tags": [ + "rigging", + "groupSelected", + "python" + ], + "title": "Group Selected" + }, + { + "command": "$COLORBLEED_SCRIPTS\\rigging\\ikHandlePoleVectorLocator.py", + "sourcetype": "python", + "tags": [ + "rigging", + "ikHandlePoleVectorLocator", + "python" + ], + "title": "IK Handle Pole Vector Locator" + }, + { + "command": "$COLORBLEED_SCRIPTS\\rigging\\jointOrientUI.py", + "sourcetype": "python", + "tags": [ + "rigging", + "jointOrientUI", + "python" + ], + "title": "Joint Orient UI" + }, + { + "command": "$COLORBLEED_SCRIPTS\\rigging\\jointsOnCurve.py", + "sourcetype": "python", + "tags": [ + "rigging", + "jointsOnCurve", + "python" + ], + "title": "Joints On Curve" + }, + { + "command": "$COLORBLEED_SCRIPTS\\rigging\\resetBindSelectedSkinJoints.py", + "sourcetype": "python", + "tags": [ + "rigging", + "resetBindSelectedSkinJoints", + "python" + ], + "title": "Reset Bind Selected Skin Joints" + }, + { + "command": "$COLORBLEED_SCRIPTS\\rigging\\selectSkinclusterJointsFromSelectedComponents.py", + "sourcetype": "python", + "tags": [ + "rigging", + "selectSkinclusterJointsFromSelectedComponents", + "python" + ], + "title": "Select Skincluster Joints From Selected Components" + }, + { + "command": "$COLORBLEED_SCRIPTS\\rigging\\selectSkinclusterJointsFromSelectedMesh.py", + "sourcetype": "python", + "tags": [ + "rigging", + "selectSkinclusterJointsFromSelectedMesh", + "python" + ], + "title": "Select Skincluster Joints From Selected Mesh" + }, + { + "command": "$COLORBLEED_SCRIPTS\\rigging\\setJointLabels.py", + "sourcetype": "python", + "tags": [ + "rigging", + "setJointLabels", + "python" + ], + "title": "Set Joint Labels" + }, + { + "command": "$COLORBLEED_SCRIPTS\\rigging\\setJointOrientationFromCurrentRotation.py", + "sourcetype": "python", + "tags": [ + "rigging", + "setJointOrientationFromCurrentRotation", + "python" + ], + "title": "Set Joint Orientation From Current Rotation" + }, + { + "command": "$COLORBLEED_SCRIPTS\\rigging\\setSelectedJointsOrientationZero.py", + "sourcetype": "python", + "tags": [ + "rigging", + "setSelectedJointsOrientationZero", + "python" + ], + "title": "Set Selected Joints Orientation Zero" + }, + { + "command": "$COLORBLEED_SCRIPTS\\rigging\\mirrorCurveShape.py", + "sourcetype": "python", + "tags": [ + "rigging", + "mirrorCurveShape", + "python" + ], + "title": "Mirror Curve Shape" + }, + { + "command": "$COLORBLEED_SCRIPTS\\rigging\\setRotationOrderUI.py", + "sourcetype": "python", + "tags": [ + "rigging", + "setRotationOrderUI", + "python" + ], + "title": "Set Rotation Order UI" + }, + { + "command": "$COLORBLEED_SCRIPTS\\rigging\\paintItNowUI.py", + "sourcetype": "python", + "tags": [ + "rigging", + "paintItNowUI", + "python" + ], + "title": "Paint It Now UI" + }, + { + "command": "$COLORBLEED_SCRIPTS\\rigging\\parentScaleConstraint.py", + "sourcetype": "python", + "tags": [ + "rigging", + "parentScaleConstraint", + "python" + ], + "title": "Parent Scale Constraint" + }, + { + "command": "$COLORBLEED_SCRIPTS\\rigging\\quickSetWeightsUI.py", + "sourcetype": "python", + "tags": [ + "rigging", + "quickSetWeightsUI", + "python" + ], + "title": "Quick Set Weights UI" + }, + { + "command": "$COLORBLEED_SCRIPTS\\rigging\\rapidRig.py", + "sourcetype": "python", + "tags": [ + "rigging", + "rapidRig", + "python" + ], + "title": "Rapid Rig" + }, + { + "command": "$COLORBLEED_SCRIPTS\\rigging\\regenerate_blendshape_targets.py", + "sourcetype": "python", + "tags": [ + "rigging", + "regenerate_blendshape_targets", + "python" + ], + "title": "Regenerate Blendshape Targets" + }, + { + "command": "$COLORBLEED_SCRIPTS\\rigging\\removeRotationAxis.py", + "sourcetype": "python", + "tags": [ + "rigging", + "removeRotationAxis", + "python" + ], + "title": "Remove Rotation Axis" + }, + { + "command": "$COLORBLEED_SCRIPTS\\rigging\\resetBindSelectedMeshes.py", + "sourcetype": "python", + "tags": [ + "rigging", + "resetBindSelectedMeshes", + "python" + ], + "title": "Reset Bind Selected Meshes" + }, + { + "command": "$COLORBLEED_SCRIPTS\\rigging\\simpleControllerOnSelection.py", + "sourcetype": "python", + "tags": [ + "rigging", + "simpleControllerOnSelection", + "python" + ], + "title": "Simple Controller On Selection" + }, + { + "command": "$COLORBLEED_SCRIPTS\\rigging\\simpleControllerOnSelectionHierarchy.py", + "sourcetype": "python", + "tags": [ + "rigging", + "simpleControllerOnSelectionHierarchy", + "python" + ], + "title": "Simple Controller On Selection Hierarchy" + }, + { + "command": "$COLORBLEED_SCRIPTS\\rigging\\superRelativeCluster.py", + "sourcetype": "python", + "tags": [ + "rigging", + "superRelativeCluster", + "python" + ], + "title": "Super Relative Cluster" + }, + { + "command": "$COLORBLEED_SCRIPTS\\rigging\\tfSmoothSkinWeight.py", + "sourcetype": "python", + "tags": [ + "rigging", + "tfSmoothSkinWeight", + "python" + ], + "title": "TF Smooth Skin Weight" + }, + { + "command": "$COLORBLEED_SCRIPTS\\rigging\\toggleIntermediates.py", + "sourcetype": "python", + "tags": [ + "rigging", + "toggleIntermediates", + "python" + ], + "title": "Toggle Intermediates" + }, + { + "command": "$COLORBLEED_SCRIPTS\\rigging\\toggleSegmentScaleCompensate.py", + "sourcetype": "python", + "tags": [ + "rigging", + "toggleSegmentScaleCompensate", + "python" + ], + "title": "Toggle Segment Scale Compensate" + }, + { + "command": "$COLORBLEED_SCRIPTS\\rigging\\toggleSkinclusterDeformNormals.py", + "sourcetype": "python", + "tags": [ + "rigging", + "toggleSkinclusterDeformNormals", + "python" + ], + "title": "Toggle Skincluster Deform Normals" + } + ], + "Shading": [ + { + "command": "", + "sourcetype": "file", + "tags": [ + "shading", + "vray" + ], + "title": "VRay", + "tooltip": "", + "items": [ + { + "title": "Import Proxies", + "command": "$COLORBLEED_SCRIPTS\\shading\\vray\\vrayImportProxies.py", + "sourcetype": "file", + "tags": [ + "shading", + "vray", + "import", + "proxies" + ], + "tooltip": "" + }, + { + "title": "separator" + }, + { + "title": "Select All GES", + "command": "$COLORBLEED_SCRIPTS\\shading\\vray\\selectAllGES.py", + "sourcetype": "file", + "tooltip": "", + "tags": [ + "shading", + "vray", + "selectAllGES.py" + ] + }, + { + "title": "Select All GES Under Selection", + "command": "$COLORBLEED_SCRIPTS\\shading\\vray\\selectAllGESUnderSelection.py", + "sourcetype": "file", + "tooltip": "", + "tags": [ + "shading", + "vray", + "selection", + "all", + "GES" + ] + }, + { + "title": "separator" + }, + { + "title": "Selection To VRay Mesh", + "command": "$COLORBLEED_SCRIPTS\\shading\\vray\\selectionToVrayMesh.py", + "sourcetype": "file", + "tooltip": "", + "tags": [ + "shading", + "vray", + "selection", + "vraymesh" + ] + }, + { + "title": "Add VRay Round Edges Attribute", + "command": "$COLORBLEED_SCRIPTS\\shading\\vray\\addVrayRoundEdgesAttribute.py", + "sourcetype": "file", + "tooltip": "", + "tags": [ + "shading", + "vray", + "round edges", + "attribute" + ] + }, + { + "title": "Add Gamma", + "command": "$COLORBLEED_SCRIPTS\\shading\\vray\\vrayAddGamma.py", + "sourcetype": "file", + "tooltip": "", + "tags": [ + "shading", + "vray", + "add gamma" + ] + }, + { + "title": "separator" + }, + { + "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": "" + }, + { + "command": "$COLORBLEED_SCRIPTS\\shading\\vray\\vrayMergeSimilarVRayMeshMaterials.py", + "sourcetype": "file", + "title": "Merge Similar VRay Mesh Materials", + "tags": [ + "shading", + "vray", + "Merge", + "VRayMesh", + "Materials" + ], + "tooltip": "" + }, + { + "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" + ] + }, + { + "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" + ] + }, + { + "title": "separator" + }, + { + "title": "Add OpenSubdiv Attribute", + "command": "$COLORBLEED_SCRIPTS\\shading\\vray\\addVrayOpenSubdivAttribute.py", + "sourcetype": "file", + "tooltip": "", + "tags": [ + "shading", + "vray", + "add", + "open subdiv", + "attribute" + ] + }, + { + "title": "Remove OpenSubdiv Attribute", + "command": "$COLORBLEED_SCRIPTS\\shading\\vray\\removeVrayOpenSubdivAttribute.py", + "sourcetype": "file", + "tooltip": "", + "tags": [ + "shading", + "vray", + "remove", + "opensubdiv", + "attributee" + ] + }, + { + "title": "separator" + }, + { + "title": "Add Subdivision Attribute", + "command": "$COLORBLEED_SCRIPTS\\shading\\vray\\addVraySubdivisionAttribute.py", + "sourcetype": "file", + "tooltip": "", + "tags": [ + "shading", + "vray", + "addVraySubdivisionAttribute" + ] + }, + { + "title": "Remove Subdivision Attribute.py", + "command": "$COLORBLEED_SCRIPTS\\shading\\vray\\removeVraySubdivisionAttribute.py", + "sourcetype": "file", + "tooltip": "", + "tags": [ + "shading", + "vray", + "remove", + "subdivision", + "attribute" + ] + }, + { + "title": "separator" + }, + { + "title": "Add Vray Object Ids", + "command": "$COLORBLEED_SCRIPTS\\shading\\vray\\addVrayObjectIds.py", + "sourcetype": "file", + "tooltip": "", + "tags": [ + "shading", + "vray", + "add", + "object id" + ] + }, + { + "title": "Add Vray Material Ids", + "command": "$COLORBLEED_SCRIPTS\\shading\\vray\\addVrayMaterialIds.py", + "sourcetype": "file", + "tooltip": "", + "tags": [ + "shading", + "vray", + "addVrayMaterialIds.py" + ] + }, + { + "title": "separator" + }, + { + "title": "Set Physical DOF Depth", + "command": "$COLORBLEED_SCRIPTS\\shading\\vray\\vrayPhysicalDOFSetDepth.py", + "sourcetype": "file", + "tooltip": "", + "tags": [ + "shading", + "vray", + "physical", + "DOF ", + "Depth" + ] + }, + { + "title": "Magic Vray Proxy UI", + "command": "$COLORBLEED_SCRIPTS\\shading\\vray\\magicVrayProxyUI.py", + "sourcetype": "file", + "tooltip": "", + "tags": [ + "shading", + "vray", + "magicVrayProxyUI" + ] + } + ] + }, + { + "command": "$COLORBLEED_SCRIPTS\\shading\\LightLinkUI.py", + "sourcetype": "file", + "tags": [ + "shading", + "LightLinkUI" + ], + "title": "Light Link UI", + "tooltip": "" + }, + { + "command": "$COLORBLEED_SCRIPTS\\shading\\setTexturePreviewToCLRImage.py", + "sourcetype": "file", + "tags": [ + "shading", + "CLRImage", + "textures", + "preview" + ], + "title": "Set Texture Preview To CLRImage", + "tooltip": "" + }, + { + "command": "$COLORBLEED_SCRIPTS\\shading\\fixDefaultShaderSetBehavior", + "sourcetype": "file", + "tags": [ + "shading", + "fix", + "DefaultShaderSet", + "Behavior" + ], + "title": "fixDefaultShaderSetBehavior", + "tooltip": "" + }, + { + "command": "$COLORBLEED_SCRIPTS\\shading\\fixSelectedShapesReferenceAssignments.py", + "sourcetype": "file", + "tags": [ + "shading", + "fixSelectedShapesReferenceAssignments" + ], + "title": "Fix Shapes Reference Assignments", + "tooltip": "Select shapes to fix the reference assignments" + }, + { + "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" + }, + { + "command": "$COLORBLEED_SCRIPTS\\shading\\selectShapesWithoutShader.py", + "sourcetype": "file", + "tags": [ + "shading", + "selectShapesWithoutShader" + ], + "title": "Select Shapes Without Shader", + "tooltip": "" + }, + { + "command": "$COLORBLEED_SCRIPTS\\shading\\fixRenderLayerOutAdjustmentErrors.py", + "sourcetype": "file", + "tags": [ + "shading", + "fixRenderLayerOutAdjustmentErrors" + ], + "title": "Fix RenderLayerOut Adjustment Errors", + "tooltip": "" + } + ], + "Layout": [ + { + "command": "$COLORBLEED_SCRIPTS\\layout\\alignDistributeUI.py", + "sourcetype": "file", + "tags": ["layout", "align", "Distribute", "UI"], + "title": "Align Distribute UI", + "tooltip": "" + }, + { + "command": "$COLORBLEED_SCRIPTS\\layout\\alignSimpleUI.py", + "sourcetype": "file", + "tags": ["layout", "align", "UI", "Simple"], + "title": "Align Simple UI", + "tooltip": "" + }, + { + "command": "$COLORBLEED_SCRIPTS\\layout\\center_locator.py", + "sourcetype": "file", + "tags": ["layout", "center", "locator"], + "title": "Center Locator", + "tooltip": "" + }, + { + "command": "$COLORBLEED_SCRIPTS\\layout\\average_locator.py", + "sourcetype": "file", + "tags": ["layout", "average", "locator"], + "title": "Average Locator", + "tooltip": "" + }, + { + "command": "$COLORBLEED_SCRIPTS\\layout\\selectWithinProximityUI.py", + "sourcetype": "file", + "tags": ["layout", "select", "proximity", "ui" + ], + "title": "Select Within Proximity UI", + "tooltip": "" + }, + { + "command": "$COLORBLEED_SCRIPTS\\layout\\dupCurveUI.py", + "sourcetype": "file", + "tags": ["layout", "Duplicate", "Curve", "UI"], + "title": "Duplicate Curve UI", + "tooltip": "" + }, + { + "command": "$COLORBLEED_SCRIPTS\\layout\\randomDeselectUI.py", + "sourcetype": "file", + "tags": ["layout", "random", "Deselect", "UI"], + "title": "Random Deselect UI", + "tooltip": "" + }, + { + "command": "$COLORBLEED_SCRIPTS\\layout\\multiReferencerUI.py", + "sourcetype": "file", + "tags": ["layout", "multi", "reference"], + "title": "Multi Referencer UI", + "tooltip": "" + }, + { + "command": "$COLORBLEED_SCRIPTS\\layout\\duplicateOffsetUI.py", + "sourcetype": "file", + "tags": ["layout", "duplicate", "offset", "UI"], + "title": "Duplicate Offset UI", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": ["layout", "spPaint3d", "paint", "tool"], + "title": "SP Paint 3d", + "tooltip": "" + }, + { + "command": "$COLORBLEED_SCRIPTS\\layout\\randomizeUI.py", + "sourcetype": "file", + "tags": ["layout", "randomize","UI"], + "title": "Randomize UI", + "tooltip": "" + }, + { + "command": "$COLORBLEED_SCRIPTS\\layout\\distributeWithinObjectUI.py", + "sourcetype": "file", + "tags": ["layout", "distribute", "ObjectUI", "within"], + "title": "Distribute Within Object UI", + "tooltip": "" + } + ], + "Particles": [ + { + "command": "", + "sourcetype": "file", + "tags": [ + "particles", + "instancerToObjects" + ], + "title": "instancerToObjects", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "particles", + "instancerToObjectsInstances" + ], + "title": "instancerToObjectsInstances", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "particles", + "objectsToParticlesAndInstancerCleanSource" + ], + "title": "objectsToParticlesAndInstancerCleanSource", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "particles", + "particleComponentsToLocators" + ], + "title": "particleComponentsToLocators", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "particles", + "objectsToParticlesAndInstancer" + ], + "title": "objectsToParticlesAndInstancer", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "particles", + "spawnParticlesOnMesh" + ], + "title": "spawnParticlesOnMesh", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "particles", + "instancerToObjectsInstancesWithAnimation" + ], + "title": "instancerToObjectsInstancesWithAnimation", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "particles", + "objectsToParticles" + ], + "title": "objectsToParticles", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "particles", + "add_particle_cacheFile_attrs" + ], + "title": "add_particle_cacheFile_attrs", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "particles", + "mergeParticleSystems" + ], + "title": "mergeParticleSystems", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "particles", + "particlesToLocators" + ], + "title": "particlesToLocators", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "particles", + "instancerToObjectsWithAnimation" + ], + "title": "instancerToObjectsWithAnimation", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "particles", + "killSelectedParticles" + ], + "title": "killSelectedParticles", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "particles", + "clearInitialState" + ], + "title": "clearInitialState", + "tooltip": "" + } + ], + "Cleanup": [ + { + "command": "", + "sourcetype": "file", + "tags": [ + "cleanup", + "selectIntermediateObjects" + ], + "title": "selectIntermediateObjects", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "cleanup", + "resetViewportCache" + ], + "title": "resetViewportCache", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "cleanup", + "selectNonUniqueNames" + ], + "title": "selectNonUniqueNames", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "cleanup", + "uniqifyNodeNames" + ], + "title": "uniqifyNodeNames", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "cleanup", + "selectByType" + ], + "title": "selectByType", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "cleanup", + "removeNamespaces" + ], + "title": "removeNamespaces", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "cleanup", + "autoRenameFileNodes" + ], + "title": "autoRenameFileNodes", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "cleanup", + "remove_user_defined_attributes" + ], + "title": "remove_user_defined_attributes", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "cleanup", + "removeUnknownNodes" + ], + "title": "removeUnknownNodes", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "cleanup", + "colorbleedRename" + ], + "title": "colorbleedRename", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "cleanup", + "removeUnloadedReferences" + ], + "title": "removeUnloadedReferences", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "cleanup", + "referenceEditsUI" + ], + "title": "referenceEditsUI", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "cleanup", + "renameShapesToTransform" + ], + "title": "renameShapesToTransform", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "cleanup", + "removeReferencesFailedEdits" + ], + "title": "removeReferencesFailedEdits", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "cleanup", + "reorderUI" + ], + "title": "reorderUI", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "cleanup", + "pastedCleaner" + ], + "title": "pastedCleaner", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "cleanup", + "deleteGhostIntermediateObjects" + ], + "title": "deleteGhostIntermediateObjects", + "tooltip": "" + } + ], + "Projects": [ + { + "command": "", + "sourcetype": "file", + "tags": [ + "projects", + "fifa" + ], + "title": "fifa", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "projects", + "beakbuds" + ], + "title": "beakbuds", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "projects", + "redeemer" + ], + "title": "redeemer", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "projects", + "bjorn" + ], + "title": "bjorn", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "projects", + "aldi" + ], + "title": "aldi", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "projects", + "eneco" + ], + "title": "eneco", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "projects", + "duurzame_verpakking" + ], + "title": "duurzame_verpakking", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "projects", + "bunch" + ], + "title": "bunch", + "tooltip": "" + } + ], + "Pyblish": [ + { + "command": "", + "sourcetype": "file", + "tags": [ + "pyblish", + "layout" + ], + "title": "layout", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "pyblish", + "look" + ], + "title": "look", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "pyblish", + "submit_to_deadline" + ], + "title": "submit_to_deadline", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "pyblish", + "instance_creator" + ], + "title": "instance_creator", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "pyblish", + "utilities" + ], + "title": "utilities", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "pyblish", + "loader" + ], + "title": "loader", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "pyblish", + "animation" + ], + "title": "animation", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "pyblish", + "lighting" + ], + "title": "lighting", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "pyblish", + "inventory_ui" + ], + "title": "inventory_ui", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "pyblish", + "explorer" + ], + "title": "explorer", + "tooltip": "" + } + ], + "Others": [ + { + "command": "", + "sourcetype": "file", + "tags": [ + "others", + "instanceSmartTransform" + ], + "title": "instanceSmartTransform", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "others", + "archiveSceneUI" + ], + "title": "archiveSceneUI", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "others", + "getSimilarMeshes" + ], + "title": "getSimilarMeshes", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "others", + "createBoundingBoxEachSelected" + ], + "title": "createBoundingBoxEachSelected", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "others", + "curveFromPositionEveryFrame" + ], + "title": "curveFromPositionEveryFrame", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "others", + "hair" + ], + "title": "hair", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "others", + "selectSoftSelection" + ], + "title": "selectSoftSelection", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "others", + "instanceLeafSmartTransform" + ], + "title": "instanceLeafSmartTransform", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "others", + "randomizeUVShellsSelectedObjects" + ], + "title": "randomizeUVShellsSelectedObjects", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "others", + "centerPivotGroup" + ], + "title": "centerPivotGroup", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "others", + "locatorsOnSelectedFaces" + ], + "title": "locatorsOnSelectedFaces", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "others", + "display" + ], + "title": "display", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "others", + "copyDeformers" + ], + "title": "copyDeformers", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "others", + "locatorsOnEdgeSelectionPrompt" + ], + "title": "locatorsOnEdgeSelectionPrompt", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "others", + "yeti" + ], + "title": "yeti", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "others", + "selectInReferenceEditor" + ], + "title": "selectInReferenceEditor", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "others", + "selectConstrainingObject" + ], + "title": "selectConstrainingObject", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "others", + "deformerSetRelationsUI" + ], + "title": "deformerSetRelationsUI", + "tooltip": "" + }, + { + "command": "", + "sourcetype": "file", + "tags": [ + "others", + "recreateBaseNodesForAllLatticeNodes" + ], + "title": "recreateBaseNodesForAllLatticeNodes", + "tooltip": "" + } + ] +} diff --git a/colorbleed/maya/menu.py b/colorbleed/maya/menu.py index 07a9ba81ce..003c303097 100644 --- a/colorbleed/maya/menu.py +++ b/colorbleed/maya/menu.py @@ -1,72 +1,69 @@ import sys -from maya import cmds +import os +import logging +import site + from avalon.vendor.Qt import QtWidgets, QtCore +import maya.cmds as cmds + self = sys.modules[__name__] self._menu = "colorbleed" -self._parent = { - widget.objectName(): widget - for widget in QtWidgets.QApplication.topLevelWidgets() -}.get("MayaWindow") + +# set colorbleed scripts path in environment keys +os.environ["COLORBLEED_SCRIPTS"] = r"P:\pipeline\dev\git\cbMayaScripts\cbMayaScripts" + +log = logging.getLogger(__name__) -def install(): - from . import interactive +def deferred(): - uninstall() + # todo: replace path with server / library path + site.addsitedir("C:\Users\User\Documents\development\scriptsmenu\python") - def deferred(): - cmds.menu(self._menu, - label="Colorbleed", - tearOff=True, - parent="MayaWindow") + from scriptsmenu import launchformaya + import scriptsmenu.scriptsmenu as menu - # Modeling sub-menu - cmds.menuItem("Modeling", - label="Modeling", - tearOff=True, - subMenu=True, - parent=self._menu) + reload(launchformaya) + reload(menu) - cmds.menuItem("Combine", command=interactive.combine) + log.info("Attempting to install ...") - # Rigging sub-menu - cmds.menuItem("Rigging", - label="Rigging", - tearOff=True, - subMenu=True, - parent=self._menu) + # load configuration of custom menu + config_path = os.path.join(os.path.dirname(__file__), "menu.json") + config = menu.load_configuration(config_path) - cmds.menuItem("Auto Connect", command=interactive.auto_connect) - cmds.menuItem("Clone (Local)", command=interactive.clone_localspace) - cmds.menuItem("Clone (World)", command=interactive.clone_worldspace) - cmds.menuItem("Clone (Special)", command=interactive.clone_special) - cmds.menuItem("Create Follicle", command=interactive.follicle) + # get Maya menubar + parent = launchformaya._maya_main_menubar() + cb_menu = menu.ScriptsMenu(objectName=self._menu, + title=self._menu.title(), + parent=parent) - # Animation sub-menu - cmds.menuItem("Animation", - label="Animation", - tearOff=True, - subMenu=True, - parent=self._menu) + # register modifiers + modifiers = QtCore.Qt.ControlModifier | QtCore.Qt.ShiftModifier + cb_menu.register_callback(modifiers, launchformaya.to_shelf) - cmds.menuItem("Set Defaults", command=interactive.set_defaults) - - cmds.setParent("..", menu=True) - - cmds.menuItem(divider=True) - - cmds.menuItem("Auto Connect", command=interactive.auto_connect_assets) - - # Allow time for uninstallation to finish. - QtCore.QTimer.singleShot(100, deferred) + # apply configuration + menu.load_from_configuration(cb_menu, config) def uninstall(): + + log.info("Attempting to uninstall ..") app = QtWidgets.QApplication.instance() widgets = dict((w.objectName(), w) for w in app.allWidgets()) menu = widgets.get(self._menu) if menu: - menu.deleteLater() - del(menu) + try: + menu.deleteLater() + del menu + except Exception as e: + log.error(e) + + +def install(): + + uninstall() + # Allow time for uninstallation to finish. + cmds.evalDeferred(deferred) diff --git a/colorbleed/plugin.py b/colorbleed/plugin.py index 5adf7dbe24..63a8b0e278 100644 --- a/colorbleed/plugin.py +++ b/colorbleed/plugin.py @@ -1,6 +1,7 @@ import tempfile import pyblish.api + ValidatePipelineOrder = pyblish.api.ValidatorOrder + 0.05 ValidateContentsOrder = pyblish.api.ValidatorOrder + 0.1 ValidateSceneOrder = pyblish.api.ValidatorOrder + 0.2 @@ -17,7 +18,7 @@ class Extractor(pyblish.api.InstancePlugin): """ - order = pyblish.api.ExtractorOrder + order = 2.0 def staging_dir(self, instance): """Provide a temporary directory in which to store extracted files diff --git a/colorbleed/plugins/maya/create/colorbleed_look.py b/colorbleed/plugins/maya/create/colorbleed_look.py index 557647edc0..e6f5751c53 100644 --- a/colorbleed/plugins/maya/create/colorbleed_look.py +++ b/colorbleed/plugins/maya/create/colorbleed_look.py @@ -8,7 +8,7 @@ class CreateLook(avalon.maya.Creator): name = "lookDefault" label = "Look Dev" - family = "colorbleed.look" + family = "colorbleed.lookdev" def __init__(self, *args, **kwargs): super(CreateLook, self).__init__(*args, **kwargs) diff --git a/colorbleed/plugins/maya/create/colorbleed_texture.py b/colorbleed/plugins/maya/create/colorbleed_texture.py new file mode 100644 index 0000000000..1080f1c6a1 --- /dev/null +++ b/colorbleed/plugins/maya/create/colorbleed_texture.py @@ -0,0 +1,9 @@ +import avalon.maya + + +class CreateTexture(avalon.maya.Creator): + """Polygonal geometry for animation""" + + name = "texturesDefault" + label = "Textures" + family = "colorbleed.texture" \ No newline at end of file diff --git a/colorbleed/plugins/maya/load/load_animation.py b/colorbleed/plugins/maya/load/load_animation.py index 68127020cd..6ef3d07110 100644 --- a/colorbleed/plugins/maya/load/load_animation.py +++ b/colorbleed/plugins/maya/load/load_animation.py @@ -1,4 +1,8 @@ -from avalon import api +import os + +from maya import cmds + +from avalon import api, maya class AbcLoader(api.Loader): @@ -13,12 +17,17 @@ class AbcLoader(api.Loader): color = "orange" def process(self, name, namespace, context, data): - from maya import cmds cmds.loadPlugin("AbcImport.mll", quiet=True) # Prevent identical alembic nodes from being shared # Create unique namespace for the cameras + # Get name from asset being loaded + assert "_" in name, "Naming convention not followed" + assetname = "{}_".format(name.split("_")[0]) + namespace = maya.unique_namespace(assetname, + format="%03d", + suffix="_abc") nodes = cmds.file(self.fname, namespace=namespace, sharedReferenceFile=False, @@ -27,6 +36,8 @@ class AbcLoader(api.Loader): reference=True, returnNewNodes=True) + # load colorbleed ID attribute + self[:] = nodes @@ -76,24 +87,19 @@ class CurvesLoader(api.Loader): ]) with maya.maintained_selection(): - cmds.select( - control_set, - replace=True, + cmds.select(control_set, + replace=True, + # Support controllers being embedded in + # additional selection sets. + noExpand=False) - # Support controllers being embedded in - # additional selection sets. - noExpand=False - ) - - nodes = cmds.file( - self.fname, - i=True, - type="atomImport", - renameAll=True, - namespace=namespace, - options=options, - returnNewNodes=True, - ) + nodes = cmds.file(self.fname, + i=True, + type="atomImport", + renameAll=True, + namespace=namespace, + options=options, + returnNewNodes=True) self[:] = nodes + cmds.sets(container, query=True) + [container] @@ -101,8 +107,7 @@ class CurvesLoader(api.Loader): self._post_process(name, namespace, context, data) def _post_process(self, name, namespace, context, data): - import os - from maya import cmds + from avalon import maya, io # Task-dependent post-process @@ -136,6 +141,7 @@ class CurvesLoader(api.Loader): cmds.select([output, controls], noExpand=True) dependencies = [context["representation"]["_id"]] + dependencies = " ".join(str(d) for d in dependencies) name = "anim{}_".format(dependency["name"].title()) # TODO(marcus): Hardcoding the family here, better separate this. @@ -143,8 +149,7 @@ class CurvesLoader(api.Loader): assert len(family) == 1, ("None or multiple animation " "families found") family = family[0] - maya.create( - name=maya.unique_name(name, suffix="_SET"), - family=family, - options={"useSelection": True}, - data={"dependencies": " ".join(str(d) for d in dependencies)}) + maya.create(name=maya.unique_name(name, suffix="_SET"), + family=family, + options={"useSelection": True}, + data={"dependencies": dependencies}) diff --git a/colorbleed/plugins/maya/load/load_look.py b/colorbleed/plugins/maya/load/load_look.py index 411f2fbed5..557d18a3c2 100644 --- a/colorbleed/plugins/maya/load/load_look.py +++ b/colorbleed/plugins/maya/load/load_look.py @@ -2,7 +2,8 @@ import os import json from maya import cmds -from avalon import api +from avalon import api, maya +import colorbleed.maya.lib as lib class LookLoader(api.Loader): @@ -17,31 +18,49 @@ class LookLoader(api.Loader): color = "orange" def process(self, name, namespace, context, data): - from avalon import maya - try: - existing_reference = cmds.file(self.fname, - query=True, - referenceNode=True) - except RuntimeError as e: - if e.message.rstrip() != "Cannot find the scene file.": - raise + """ + Load and try to ssign Lookdev to nodes based on relationship data + Args: + name: + namespace: + context: + data: - self.log.info("Loading lookdev for the first time..") + Returns: + + """ + + + + # improve readability of the namespace + assetname = context["asset"]["name"] + ns_assetname = "{}_".format(assetname) + + namespace = maya.unique_namespace(ns_assetname, + format="%03d", + suffix="_look") + + # try / except here is to ensure that the get_reference_node + # does not fail when the file doesn't exist yet + reference_node = None + try: + reference_node = lib.get_reference_node(self.fname) + except: + pass + + if reference_node is None: + self.log.info("Loading lookdev for the first time ...") with maya.maintained_selection(): - nodes = cmds.file( - self.fname, - namespace=namespace, - reference=True, - returnNewNodes=True - ) + nodes = cmds.file(self.fname, + namespace=namespace, + reference=True, + returnNewNodes=True) else: - self.log.info("Reusing existing lookdev..") - nodes = cmds.referenceQuery(existing_reference, nodes=True) - namespace = nodes[0].split(":", 1)[0] + self.log.info("Reusing existing lookdev ...") + nodes = cmds.referenceQuery(reference_node, nodes=True) # Assign shaders self.fname = self.fname.rsplit(".", 1)[0] + ".json" - if not os.path.isfile(self.fname): self.log.warning("Look development asset " "has no relationship data.") @@ -50,6 +69,65 @@ class LookLoader(api.Loader): with open(self.fname) as f: relationships = json.load(f) - maya.apply_shaders(relationships, namespace) + # Get all nodes which belong to a matching name space + # Currently this is the safest way to get all the nodes + namespace_nodes = self.get_namespace_nodes(assetname) + lib.apply_shaders(relationships, nodes, namespace_nodes) self[:] = nodes + + def get_namespace_nodes(self, assetname): + """ + Get all nodes of namespace `asset_*` and check if they have a shader + assigned, if not add to list + Args: + context (dict): current context of asset + + Returns: + list + + """ + + # types = ["transform", "mesh"] + list_nodes = [] + + namespaces = cmds.namespaceInfo(listOnlyNamespaces=True) + + # remove basic namespaces + namespaces.remove("UI") + namespaces.remove("shared") + + for ns in namespaces: + if not ns.startswith(assetname): + continue + # get reference nodes + ns_nodes = cmds.namespaceInfo(ns, listOnlyDependencyNodes=True) + # TODO: might need to extend the types + # check if any nodes are connected to something else than lambert1 + list_nodes = cmds.ls(ns_nodes, long=True) + unassigned_nodes = [self.has_default_shader(n) for n in list_nodes] + nodes = [n for n in unassigned_nodes if n is not None] + + list_nodes.extend(nodes) + + return set(list_nodes) + + def has_default_shader(self, node): + """Check if the nodes have `initialShadingGroup` shader assigned + + Args: + node (str): node to check + + Returns: + str + """ + + shaders = cmds.listConnections(node, type="shadingEngine") or [] + if "initialShadingGroup" in shaders: + # return transform node + transform = cmds.listRelatives(node, parent=True, type="transform", + fullPath=True) + if not transform: + return [] + + return transform[0] diff --git a/colorbleed/plugins/maya/load/load_model.py b/colorbleed/plugins/maya/load/load_model.py index faeb78599f..ad70ad3440 100644 --- a/colorbleed/plugins/maya/load/load_model.py +++ b/colorbleed/plugins/maya/load/load_model.py @@ -42,8 +42,6 @@ class ModelGPUCacheLoader(api.Loader): def process(self, name, namespace, context, data): - from maya import cmds - # todo: This will likely not be entirely safe with "containerize" # also this cannot work in the manager because it only works # on references at the moment! diff --git a/colorbleed/plugins/maya/load/load_rig.py b/colorbleed/plugins/maya/load/load_rig.py index 1332e63f4f..5604ec22c5 100644 --- a/colorbleed/plugins/maya/load/load_rig.py +++ b/colorbleed/plugins/maya/load/load_rig.py @@ -1,5 +1,8 @@ +import os + from maya import cmds -from avalon import api + +from avalon import api, maya class RigLoader(api.Loader): @@ -18,18 +21,20 @@ class RigLoader(api.Loader): color = "orange" def process(self, name, namespace, context, data): + + assetname = "{}_".format(context["asset"]["name"]) + unique_namespace = maya.unique_namespace(assetname, format="%03d") nodes = cmds.file(self.fname, namespace=namespace, reference=True, returnNewNodes=True, groupReference=True, - groupName=namespace + ":" + name) + groupName="{}:{}".format(namespace, name)) # Store for post-process self[:] = nodes - if data.get("post_process", True): - self._post_process(name, namespace, context, data) + self._post_process(name, unique_namespace, context, data) def _post_process(self, name, namespace, context, data): from avalon import maya @@ -38,28 +43,33 @@ class RigLoader(api.Loader): # Better register this keyword, so that it can be used # elsewhere, such as in the Integrator plug-in, # without duplication. - output = next( - (node for node in self - if node.endswith("out_SET")), None) - controls = next( - (node for node in self - if node.endswith("controls_SET")), None) + + output = next((node for node in self if + node.endswith("out_SET")), None) + controls = next((node for node in self if + node.endswith("controls_SET")), None) assert output, "No out_SET in rig, this is a bug." assert controls, "No controls_SET in rig, this is a bug." + # To ensure the asset under which is published is actually the shot + # not the asset to which the rig belongs to. + current_task = os.environ["AVALON_TASK"] + asset_name = context["asset"]["name"] + if current_task == "animate": + asset = "{}".format(os.environ["AVALON_ASSET"]) + else: + asset = "{}".format(asset_name) + with maya.maintained_selection(): cmds.select([output, controls], noExpand=True) - dependencies = [context["representation"]["_id"]] - asset = context["asset"]["name"] + "_" - # TODO(marcus): Hardcoding the family here, better separate this. - maya.create( - name=maya.unique_name(asset, suffix="_SET"), - asset=context["asset"]["name"], - family="colorbleed.animation", - options={"useSelection": True}, - data={ - "dependencies": " ".join(str(d) for d in dependencies) - }) + dependencies = [context["representation"]["_id"]] + dependencies = " ".join(str(d) for d in dependencies) + + maya.create(name=namespace, + asset=asset, + family="colorbleed.animation", + options={"useSelection": True}, + data={"dependencies": dependencies}) diff --git a/colorbleed/plugins/maya/publish/__collect_instance_per_item.py b/colorbleed/plugins/maya/publish/__collect_instance_per_item.py deleted file mode 100644 index 7dd70d444a..0000000000 --- a/colorbleed/plugins/maya/publish/__collect_instance_per_item.py +++ /dev/null @@ -1,177 +0,0 @@ -from collections import defaultdict - -from maya import cmds - -import cbra.utils.maya.node_uuid as node_uuid -import cbra.lib - -import pyblish.api - - -class CollectInstancePerItem(pyblish.api.ContextPlugin): - """Collect instances from the Maya scene and breaks them down per item id - - An instance is identified by having an _INST suffix - and a .family user-defined attribute. - - All other user-defined attributes of the object set - is accessible within each instance's data. - - This collector breaks the instances down to each Item member it contains, - by using the IDs on the nodes in the instance it will split up the instance - into separate instances for each unique "item" id it finds. - - Note: - - Only breaks down based on children members and ignores parent members. - - Discards members without IDs. - - """ - - order = pyblish.api.CollectorOrder + 0.1 - hosts = ["maya"] - label = "Instance per Item" - - _include_families = ["colorbleed.look"] - - def process(self, context): - - invalid = list() - - for objset in cmds.ls("*_SET", - objectsOnly=True, - type='objectSet', - long=True, - recursive=True): # Include namespace - - try: - family = cmds.getAttr("{}.family".format(objset)) - except ValueError: - self.log.error("Found: %s found, but no family." % objset) - continue - - if family not in self._include_families: - continue - - # ignore referenced sets - if cmds.referenceQuery(objset, isNodeReferenced=True): - continue - - instances = self.build_instances(context, objset) - if not instances: - - # Log special error messages when objectSet is completely - # empty (has no members) to clarify to artists the root of - # their problem. - if not cmds.sets(objset, query=True): - self.log.error("Instance objectSet has no members: " - "{}".format(objset)) - - self.log.error("No instances retrieved from objectSet: " - "{}".format(objset)) - invalid.append(objset) - - if invalid: - raise RuntimeError("Invalid instances: {}".format(invalid)) - - # Sort context based on family - context[:] = sorted( - context, key=lambda instance: instance.data("family")) - - def build_instances(self, context, objset): - """Build the instances for a single instance objectSet - - Returns: - list: The constructed instances from the objectSet. - - """ - - self.log.info("Collecting: %s" % objset) - - short_name = objset.rsplit("|", 1)[-1].rsplit(":", 1)[-1] - - # Default data - default_data = {"name": short_name[:-5], - "subset": "default"} - - # Get user data from user defined attributes - user_data = dict() - for attr in cmds.listAttr(objset, userDefined=True): - try: - value = cmds.getAttr("{}.{}".format(objset, attr)) - user_data[attr] = value - except RuntimeError: - continue - - # Maintain nested object sets - members = cmds.sets(objset, query=True) - members = cmds.ls(members, long=True) - - children = cmds.listRelatives(members, - allDescendents=True, - fullPath=True) or [] - - # Exclude intermediate objects - children = cmds.ls(children, noIntermediate=True, long=True) - - nodes = members + children - nodes = list(set(nodes)) - - # Group nodes using ids to an Item - nodes_id = node_uuid.build_cache(nodes, include_without_ids=True) - - # Log warning for nodes without ids - if None in nodes_id: - self.log.warning("Skipping nodes without ids: " - "{}".format(nodes_id[None])) - - # ignore nodes without ids - context.data["instancePerItemNodesWithoutId"] = nodes_id.pop(None, - None) - - item_groups = defaultdict(list) - - for id, nodes in nodes_id.iteritems(): - item_id = id.rsplit(":", 1)[0] - item_groups[item_id].extend(nodes) - - instances = list() - for item_id, item_nodes in item_groups.iteritems(): - - ctx = node_uuid.parse_id("{}:fake_node_uuid".format(item_id)) - - # Use itemPath to parse full blown context using official lib - ctx = cbra.lib.parse_context(ctx['itemPath']) - - item = ctx.get('item', None) - if item is None: - self.log.info("Unparsed item id: {}".format(item_id)) - self.log.error("Item can't be parsed and seems to be " - "non-existent. Was an asset renamed? Or your" - "project set incorrectly?") - raise RuntimeError("Item not parsed. See log for description.") - - instance = context.create_instance(objset) - - # Set the related members - instance[:] = item_nodes - instance.data['setMembers'] = item_nodes - - # Set defaults and user data - instance.data.update(default_data.copy()) - instance.data.update(user_data.copy()) - - # Override the label to be clear - name = instance.data['name'] - instance.data['label'] = "{0} ({1})".format(name, item) - - # Store that the instance is collected per item - instance.data['_instancePerItem'] = True - instance.data['_itemContext'] = ctx - - assert "family" in instance.data, "No family data in instance" - assert "name" in instance.data, ("No objectSet name data " - "in instance") - - instances.append(instance) - - return instances diff --git a/colorbleed/plugins/maya/publish/__collect_yeti_caches.py b/colorbleed/plugins/maya/publish/__collect_yeti_caches.py deleted file mode 100644 index a236836cdf..0000000000 --- a/colorbleed/plugins/maya/publish/__collect_yeti_caches.py +++ /dev/null @@ -1,156 +0,0 @@ -import os -import re -import pyseq -import glob - -import pyblish.api - -from maya import cmds - - -class SeletYetiCachesAction(pyblish.api.Action): - """Select the nodes related to the collected file textures""" - - label = "Select yeti nodes" - on = "succeeded" # This action is only available on a failed plug-in - icon = "search" # Icon from Awesome Icon - - def process(self, context, plugin): - - self.log.info("Finding textures..") - - # Get the errored instances - instances = [] - for result in context.data["results"]: - instance = result["instance"] - if instance is None: - continue - - instances.append(instance) - - # Apply pyblish.logic to get the instances for the plug-in - instances = pyblish.api.instances_by_plugin(instances, plugin) - - # Get the texture nodes from the instances - nodes = [] - for instance in instances: - texture_nodes = instance.data['yetiCaches'].keys() - nodes.extend(texture_nodes) - - # Ensure unique - nodes = list(set(nodes)) - - if nodes: - self.log.info("Selecting nodes: %s" % ", ".join(nodes)) - cmds.select(nodes, r=True, noExpand=True) - else: - self.log.info("No nodes found.") - cmds.select(deselect=True) - - -def get_sequence(filename, pattern="%04d"): - """Get pyseq sequence from filename - - Supports negative frame ranges like (-001, 0000, 0001 and -0001, 0000, 0001). - - Arguments: - filename (str): The full path to filename containing the given pattern. - pattern (str): The pattern to swap with the variable frame number. - - Returns: - pyseq.Sequence: file sequence. - - """ - - glob_pattern = filename.replace(pattern, "*") - - escaped = re.escape(filename) - re_pattern = escaped.replace(pattern, "-?[0-9]+") - - files = glob.glob(glob_pattern) - files = [str(f) for f in files if re.match(re_pattern, f)] - - return pyseq.get_sequences(files) - - -class CollectYetiCaches(pyblish.api.InstancePlugin): - """Collect used yeti caches. - - Collects the file sequences from pgYetiMaya.cacheFileName - - """ - - order = pyblish.api.CollectorOrder + 0.495 - label = 'Yeti Caches' - families = ["colorbleed.groom"] - actions = [SeletYetiCachesAction] - - TYPES = {"pgYetiMaya": "cacheFileName"} - - def process(self, instance): - - # Get textures from sets - members = instance.data("setMembers") - members = cmds.ls(members, dag=True, shapes=True, type="pgYetiMaya", - noIntermediate=True, long=True) - if not members: - raise RuntimeError("Instance appears to be empty (no members)") - - # Collect only those cache frames that are required - # If handles are required it is assumed to already be included - # in the start frame and end frames. - # (e.g. using frame handle collector) - start_frame = instance.data("startFrame") - end_frame = instance.data("endFrame") - required = set(range(int(start_frame), int(end_frame) + 1)) - - history = cmds.listHistory(members) or [] - - resources = instance.data.get("resources", []) - yeti_caches = dict() - - for node_type, attr in self.TYPES.iteritems(): - for node in cmds.ls(history, type=node_type, long=True): - - attribute = "{0}.{1}".format(node, attr) - - # Source - source = cmds.getAttr(attribute) - if not source: - self.log.error("Node does not have a file set: " - "{0}".format(node)) - - # Collect the source as expanded path because that's also - # how the attribute must be 'set' for yeti nodes. - source = os.path.realpath(cmds.workspace(expandName=source)) - - # Collect the frames we need from the sequence - sequences = get_sequence(source) - files = list() - for sequence in sequences: - for index, frame in enumerate(sequence.frames()): - if frame not in required: - continue - - item = sequence[index] - files.append(item.path) - - # Define the resource - resource = {"tags": ["maya", "yeti", "attribute"], - "node": node, - "attribute": attribute, - "source": source, # required for resources - "files": files, # required for resources - "subfolder": "caches" # optional for resources - } - - resources.append(resource) - - # For validations - yeti_caches[node] = {"attribute": attribute, - "source": source, - "sequences": sequences} - - # Store data on instance - instance.data['yetiCaches'] = yeti_caches - instance.data['resources'] = resources diff --git a/colorbleed/plugins/maya/publish/__extract_layout.py b/colorbleed/plugins/maya/publish/__extract_layout.py deleted file mode 100644 index 823337b2e1..0000000000 --- a/colorbleed/plugins/maya/publish/__extract_layout.py +++ /dev/null @@ -1,81 +0,0 @@ -import json - -from maya import cmds - -import avalon.maya -import colorbleed.api - -import cb.utils.maya.context as context -import cbra.utils.maya.layout as layout - - -def get_upstream_hierarchy_fast(nodes): - """Passed in nodes must be long names!""" - - matched = set() - parents = [] - - for node in nodes: - hierarchy = node.split("|") - num = len(hierarchy) - for x in range(1, num-1): - parent = "|".join(hierarchy[:num-x]) - if parent in parents: - break - else: - parents.append(parent) - matched.add(parent) - - return parents - - -class ExtractLayout(colorbleed.api.Extractor): - """Extract Layout as both gpuCache and Alembic""" - - label = "Layout (gpuCache & alembic)" - hosts = ["maya"] - families = ["colorbleed.layout"] - - def process(self, instance): - - # Define extract output file path - dir_path = self.staging_dir(instance) - - start = instance.data.get("startFrame", 1) - end = instance.data.get("endFrame", 1) - step = instance.data.get("step", 1.0) - placeholder = instance.data.get("placeholder", False) - write_color_sets = instance.data.get("writeColorSets", False) - renderable_only = instance.data.get("renderableOnly", False) - visible_only = instance.data.get("visibleOnly", False) - - layers = instance.data.get("animLayersActive", None) - if layers: - layers = json.loads(layers) - self.log.info("Publishing with animLayers active: " - "{0}".format(layers)) - - # Perform extraction - self.log.info("Performing extraction..") - with avalon.maya.maintained_selection(): - - # Get children hierarchy - nodes = instance.data['setMembers'] - cmds.select(nodes, r=True, hierarchy=True) - hierarchy = cmds.ls(selection=True, long=True) - - with context.evaluation("off"): - with context.no_refresh(): - with context.active_anim_layers(layers): - layout.extract_layout(hierarchy, - dir_path, - start=start, - end=end, - step=step, - placeholder=placeholder, - write_color_sets=write_color_sets, - renderable_only=renderable_only, - visible_only=visible_only) - - self.log.info("Extracted instance '{0}' to: {1}".format( - instance.name, dir_path)) diff --git a/colorbleed/plugins/maya/publish/__validate_layout_nodes.py b/colorbleed/plugins/maya/publish/__validate_layout_nodes.py deleted file mode 100644 index fda17e4433..0000000000 --- a/colorbleed/plugins/maya/publish/__validate_layout_nodes.py +++ /dev/null @@ -1,91 +0,0 @@ -import os - -import maya.cmds as cmds - -import pyblish.api -import colorbleed.api - -import cbra.lib -from cb.utils.python.decorators import memorize - - -def isclose(a, b, rel_tol=1e-9, abs_tol=0.0): - return abs(a - b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol) - - -@memorize -def is_published_path(path): - """Return whether path is from a published file""" - - # Quick check (optimization) without going through the folder - # structure - if cbra.lib.DIR_PUBLISH.lower() not in path.lower(): - return False - - try: - context = cbra.lib.parse_context(path) - except RuntimeError: - context = dict() - - return all([context.get("family", None), - context.get("subset", None), - context.get("version", None)]) - - -class ValidateLayoutNodes(pyblish.api.InstancePlugin): - """Validates that layout nodes behave to certain rules - - Gpu caches in a layout may not have sub-frame offsets, like offsets with a - value after the decimal point. (e.g. 1.45) - - Gpu caches loaded in a layout MUST come from a published source that has - family and version. - - """ - - order = colorbleed.api.ValidateContentsOrder - label = 'Layout Nodes' - families = ['colorbleed.layout'] - actions = [colorbleed.api.SelectInvalidAction] - - @classmethod - def get_invalid(cls, instance): - - caches = cmds.ls(instance, type="gpuCache", long=True) - - # Validate sub-frame offsets - invalid_offsets = list() - for cache in caches: - - offset = cmds.getAttr("{}.animOffset".format(cache)) - if not isclose(offset, round(offset)): - cls.log.warning("Invalid sub-frame offset on: %s" % cache) - invalid_offsets.append(cache) - - # Validate gpuCache paths are from published files - invalid_paths = list() - for cache in caches: - path = cmds.getAttr("{}.cacheFileName".format(cache)) - path = os.path.normpath(path) - if not is_published_path(path): - cls.log.warning("GpuCache path not from published file: " - "{0} -> {1}".format(cache, path)) - invalid_paths.append(cache) - - invalid = invalid_offsets + invalid_paths - - return invalid - - def process(self, instance): - - # Clear cache only once per publish. So we store a value on - # the context on the first instance so we clear only once. - name = self.__class__.__name__ - key = "_plugin_{0}_processed".format(name) - if not instance.context.data.get(key, False): - is_published_path.cache.clear() - instance.context.data[key] = True - - invalid = self.get_invalid(instance) - if invalid: - raise RuntimeError("Invalid nodes found: {0}".format(invalid)) diff --git a/colorbleed/plugins/maya/publish/__validate_related_node_ids.py b/colorbleed/plugins/maya/publish/__validate_related_node_ids.py deleted file mode 100644 index a79138168d..0000000000 --- a/colorbleed/plugins/maya/publish/__validate_related_node_ids.py +++ /dev/null @@ -1,83 +0,0 @@ -import pyblish.api -import colorbleed.api - -import cbra.utils.maya.node_uuid as id_utils -import cbra.lib - - -class ValidateRelatedNodeIds(pyblish.api.InstancePlugin): - """Validate nodes have related colorbleed ids. - - An ID is 'related' if its built in the current Item. - - Note that this doesn't ensure it's from the current Task. An ID created - from `lookdev` has the same relation to the Item as one coming from others, - like `rigging` or `modeling`. - - """ - - order = colorbleed.api.ValidatePipelineOrder - families = ['colorbleed.model'] - hosts = ['maya'] - label = 'Related Id Attributes' - actions = [colorbleed.api.SelectInvalidAction, - colorbleed.api.GenerateUUIDsOnInvalidAction] - - @classmethod - def get_invalid(cls, instance): - """Return the member nodes that are invalid""" - - context = instance.context - current_file = context.data.get('currentFile', None) - if not current_file: - raise RuntimeError("No current file information: " - "{0}".format(current_file)) - - try: - context = cbra.lib.parse_context(current_file) - except RuntimeError, e: - cls.log.error("Can't generate UUIDs because scene isn't " - "in new-style pipeline: ".format(current_file)) - raise e - - def to_item(id): - """Split the item id part from a node id""" - return id.rsplit(":", 1)[0] - - # Generate a fake id in the current context to retrieve the item - # id prefix that should match with ids on the nodes - fake_node = "__node__" - ids = id_utils.generate_ids(context, [fake_node]) - id = ids[fake_node] - item_prefix = to_item(id) - - # Take only the ids with more than one member - invalid = list() - invalid_items = set() - for member in instance: - member_id = id_utils.get_id(member) - - # skip nodes without ids - if not member_id: - continue - - if not member_id.startswith(item_prefix): - invalid.append(member) - invalid_items.add(to_item(member_id)) - - # Log invalid item ids - if invalid_items: - for item_id in sorted(invalid_items): - cls.log.warning("Found invalid item id: {0}".format(item_id)) - - return invalid - - def process(self, instance): - """Process all meshes""" - - # Ensure all nodes have a cbId - invalid = self.get_invalid(instance) - - if invalid: - raise RuntimeError("Nodes found with non-related " - "asset IDs: {0}".format(invalid)) diff --git a/colorbleed/plugins/maya/publish/__validate_unique_ids_in_item.py b/colorbleed/plugins/maya/publish/__validate_unique_ids_in_item.py deleted file mode 100644 index 55d3a14377..0000000000 --- a/colorbleed/plugins/maya/publish/__validate_unique_ids_in_item.py +++ /dev/null @@ -1,167 +0,0 @@ -import os -from collections import defaultdict - -import pyblish.api -import colorbleed.api - -import cbra.lib -from cbra.utils.maya.abc import get_alembic_ids -from cbra.utils.maya.node_uuid import get_id - - -def get_subset_path(context): - return os.path.join(context['itemPath'], - cbra.lib.DIR_PUBLISH, - context['family'], - context['subset']) - - -class ValidateUniqueIdsInItem(pyblish.api.InstancePlugin): - """Checks whether IDs are unique across other subsets - - This ensures a model to be published can't have ids - which are already present in another subset. For example - the "default" model can't have ids present in the "high" - subset. - - Note: - This will also invalidate the instance if it contains - nodes that are present in another instance in the scene. - So ensure the instance you're publishing actually has - the correct set members. - - """ - - order = colorbleed.api.ValidateMeshOrder - families = ['colorbleed.model'] - hosts = ['maya'] - label = 'Unique Ids in Item' - actions = [colorbleed.api.SelectInvalidAction] - optional = True - - @classmethod - def iter_invalid(cls, instance): - - verbose = instance.data.get("verbose", False) - - def _get_instance_ids(instance): - """Collect ids in an instance""" - nodes_per_id = defaultdict(list) - for node in instance: - node_id = get_id(node) - if node_id: - nodes_per_id[node_id].append(node) - return nodes_per_id - - nodes_per_id = _get_instance_ids(instance) - if not nodes_per_id: - return - - ids_lookup = set(nodes_per_id.keys()) - - instance_context = instance.data["instanceContext"] - instance_subset = instance.data['subset'] - - assert instance_context, "Instance must have 'instanceContext' data" - assert instance_subset, "Instance must have 'subset' data" - - subsets_checked = set() - subsets_checked.add(instance_subset) # we can skip this subset - - # Compare with all other *currently publishing instances* - # of family 'model' for this item - for other_instance in instance.context: - if other_instance is instance: - continue - - if other_instance.data['subset'] == instance_subset: - cls.log.error("Another instance has the same subset? " - "This should never happen.") - - if other_instance.data['family'] != "model": - continue - - if other_instance.data['instanceContext']['item'] != \ - instance_context['item']: - cls.log.error("Also publishing model for other item? " - "This should never happen.") - continue - other_ids = _get_instance_ids(other_instance).keys() - - # Perform comparison - intersection = ids_lookup.intersection(other_ids) - if intersection: - for node_id in intersection: - nodes = nodes_per_id[node_id] - for node in nodes: - yield node - - # Those that are invalid don't need to be checked again - ids_lookup.difference_update(other_ids) - - if not ids_lookup: - # Once we have no ids to check for anymore we can already - # return - return - - subsets_checked.add(other_instance.data['subset']) - - # Compare with all previously *published instances* - # of family 'model' for this item - ctx = instance_context.copy() - ctx['family'] = "model" - - published_subsets = cbra.lib.list_subsets(ctx) - published_subsets = set(x for x in published_subsets if - x != instance_subset) - - for published_subset in published_subsets: - ctx['subset'] = published_subset - ctx['subsetPath'] = get_subset_path(ctx) - - versions = cbra.lib.list_versions(ctx) - version = cbra.lib.find_highest_version(versions) - if not version: - cls.log.debug("No published version for " - "'model': {0}".format(published_subset)) - continue - - ctx['currentVersion'] = version - publish_abc = cbra.lib.get_filepath(ctx) + ".abc" - - if not os.path.exists(publish_abc): - cls.log.error("Published file to compare with does not exist: " - "{0}".format(publish_abc)) - continue - - if verbose: - cls.log.debug("Comparing with: {0}".format(publish_abc)) - - abc_ids = set(get_alembic_ids(publish_abc).values()) - - # Perform comparison - intersection = ids_lookup.intersection(abc_ids) - if intersection: - for node_id in intersection: - nodes = nodes_per_id[node_id] - for node in nodes: - yield node - - # Those that are invalid don't need to be checked again - ids_lookup.difference_update(abc_ids) - - if not ids_lookup: - # Once we have no ids to check for anymore we can already - # return - return - - return - - @classmethod - def get_invalid(cls, instance): - return list(cls.iter_invalid(instance)) - - def process(self, instance): - """Process all meshes""" - if any(self.iter_invalid(instance)): - raise RuntimeError("Invalid nodes found in {0}".format(instance)) diff --git a/colorbleed/plugins/maya/publish/collect_alembic_attrs.py b/colorbleed/plugins/maya/publish/_collect_alembic_attrs.py similarity index 100% rename from colorbleed/plugins/maya/publish/collect_alembic_attrs.py rename to colorbleed/plugins/maya/publish/_collect_alembic_attrs.py diff --git a/colorbleed/plugins/maya/publish/_debug.py b/colorbleed/plugins/maya/publish/_debug.py index 7919b1fda1..ef40126be1 100644 --- a/colorbleed/plugins/maya/publish/_debug.py +++ b/colorbleed/plugins/maya/publish/_debug.py @@ -13,7 +13,3 @@ class DebugPlugin(pyblish.api.InstancePlugin): self.log("\n\n----------------------") self.log("Instance") pprint.pprint(instance) - - self.log("\n\n----------------------") - self.log("Instance.data") - pprint.pprint(instance.data) diff --git a/colorbleed/plugins/maya/publish/_integrate_cb_asset.py b/colorbleed/plugins/maya/publish/_integrate_cb_asset.py deleted file mode 100644 index 61535e58db..0000000000 --- a/colorbleed/plugins/maya/publish/_integrate_cb_asset.py +++ /dev/null @@ -1,85 +0,0 @@ -import os -import shutil - -import pyblish_cb.lib -import colorbleed.api - - -class IntegrateColorbleedAssets(colorbleed.api.Integrator): - """Name and position instances on disk for instances. - - The files are transferred from the `extractDir` to the - computed `integrationDir` and are renamed as: - - "{item}_{family}_{subsetName}_{version}.{ext}" - - Assumptions: - - Each extracted instance is 1 file (no directories) - - """ - - label = "Asset" - families = ["colorbleed.model", "colorbleed.rig", "colorbleed.pointcache", - "colorbleed.proxy", "colorbleed.layout", "colorbleed.look", - "colorbleed.vrmeshReplace", "colorbleed.review", - "colorbleed.instancer", "colorbleed.camera", - "colorbleed.mayaAscii", - "colorbleed.furYeti"] - - def process(self, instance): - super(IntegrateColorbleedAssets, self).process(instance) - - self.log.info("Integrating {0}..".format(instance)) - - integration = pyblish_cb.lib.compute_integration(instance) - - # Store reference for upcoming plug-ins - instance.data["integrationDir"] = integration['path'] - instance.data["integrationVersion"] = integration['versionNum'] - - path = integration['path'] - data = integration.copy() - - try: - if not os.path.exists(path): - os.makedirs(path) - - self.log.info("Moving files to %s" % path) - - tmp = instance.data["extractDir"] - for src in (os.path.join(tmp, f) for f in os.listdir(tmp)): - - self.log.debug("Integrating %s" % src) - - # Source must be a file - if not os.path.isfile(src): - self.log.error("Source is not a file: {0}".format(src)) - continue - - # TODO(marcus): Consider files without extension - data["ext"] = src.split(".", 1)[-1] - dst = os.path.join(path, "{item}_" - "{family}_" - "{subsetName}_" - "{version}.{ext}".format( - **data)) - - # Copy - self.log.info("\"%s\" -> \"%s\"" % (src, dst)) - shutil.copyfile(src, dst) - - self.log.debug("Tagged %s with .Version" % path) - - try: - subset_path = os.path.dirname(path) - cquery.tag(subset_path, ".Subset") - self.log.debug("Tagged %s with .Subset" % subset_path) - except cquery.TagExists: - pass - - except OSError as e: - # If, for whatever reason, this instance did not get written. - instance.data.pop("integrationDir") - raise e - - except Exception as e: - raise Exception("An unknown error occured: %s" % e) diff --git a/colorbleed/plugins/maya/publish/validate_mesh_non_zero_edge.py b/colorbleed/plugins/maya/publish/_validate_mesh_non_zero_edge.py similarity index 83% rename from colorbleed/plugins/maya/publish/validate_mesh_non_zero_edge.py rename to colorbleed/plugins/maya/publish/_validate_mesh_non_zero_edge.py index b706913592..02019da523 100644 --- a/colorbleed/plugins/maya/publish/validate_mesh_non_zero_edge.py +++ b/colorbleed/plugins/maya/publish/_validate_mesh_non_zero_edge.py @@ -2,8 +2,7 @@ from maya import cmds import pyblish.api import colorbleed.api - -from pyblish_cb.tmp_utils import polyConstraint +import colorbleed.maya.lib as lib class ValidateMeshNonZeroEdgeLength(pyblish.api.InstancePlugin): @@ -41,17 +40,16 @@ class ValidateMeshNonZeroEdgeLength(pyblish.api.InstancePlugin): edges = ['{0}.e[*]'.format(node) for node in meshes] # Filter by constraint on edge length - invalid = polyConstraint(edges, - t=0x8000, # type=edge - length=1, - lengthbound=(0, cls.__tolerance)) + invalid = lib.polyConstraint(edges, + t=0x8000, # type=edge + length=1, + lengthbound=(0, cls.__tolerance)) return invalid def process(self, instance): """Process all meshes""" invalid = self.get_invalid(instance) - if invalid: raise RuntimeError("Meshes found with zero " "edge length: {0}".format(invalid)) diff --git a/colorbleed/plugins/maya/publish/collect_instances.py b/colorbleed/plugins/maya/publish/collect_instances.py index 498b4fb30c..bbcd8a56cb 100644 --- a/colorbleed/plugins/maya/publish/collect_instances.py +++ b/colorbleed/plugins/maya/publish/collect_instances.py @@ -3,7 +3,7 @@ from maya import cmds import pyblish.api -class CollectMindbenderInstances(pyblish.api.ContextPlugin): +class CollectInstances(pyblish.api.ContextPlugin): """Gather instances by objectSet and pre-defined attribute This collector takes into account assets that are associated with @@ -38,14 +38,24 @@ class CollectMindbenderInstances(pyblish.api.ContextPlugin): """ - label = "Collect Mindbender Instances" + label = "Collect Instances" order = pyblish.api.CollectorOrder hosts = ["maya"] + isntance_order = ["colorbleed.model", + "colorbleed.rig", + "colorbleed.animation", + "colorbleed.camera", + "colorbleed.texture", + "colorbleed.lookdev", + "colorbleed.historyLookdev", + "colorbleed.group"] def process(self, context): + objectset = cmds.ls("*.id", long=True, type="objectSet", recursive=True, objectsOnly=True) for objset in objectset: + self.log.info("Creating instance for {}".format(objset)) members = cmds.sets(objset, query=True) if members is None: @@ -61,7 +71,8 @@ class CollectMindbenderInstances(pyblish.api.ContextPlugin): # The developer is responsible for specifying # the family of each instance. - has_family = cmds.attributeQuery("family", node=objset, + has_family = cmds.attributeQuery("family", + node=objset, exists=True) assert has_family, "\"%s\" was missing a family" % objset @@ -70,7 +81,7 @@ class CollectMindbenderInstances(pyblish.api.ContextPlugin): # Apply each user defined attribute as data for attr in cmds.listAttr(objset, userDefined=True) or list(): try: - value = cmds.getAttr("{}.{}".format(objset, attr)) + value = cmds.getAttr("%s.%s" % (objset, attr)) except Exception: # Some attributes cannot be read directly, # such as mesh and color attributes. These @@ -82,9 +93,10 @@ class CollectMindbenderInstances(pyblish.api.ContextPlugin): # Collect members members = cmds.ls(members, long=True) or [] + children = cmds.listRelatives(members, allDescendents=True, - fullPath=True) + fullPath=True) or [] parents = self.get_all_parents(members) members_hierarchy = list(set(members + children + parents)) @@ -99,6 +111,10 @@ class CollectMindbenderInstances(pyblish.api.ContextPlugin): # user interface interested in visualising it. self.log.info("Found: \"%s\" " % instance.data["name"]) + context[:] = sorted(context) + + return context + def get_all_parents(self, nodes): """Get all parents by using string operations (optimization) @@ -108,6 +124,7 @@ class CollectMindbenderInstances(pyblish.api.ContextPlugin): Returns: list """ + parents = [] for node in nodes: splitted = node.split("|") diff --git a/colorbleed/plugins/maya/publish/collect_look.py b/colorbleed/plugins/maya/publish/collect_look.py index 7ee62af5fe..8511914313 100644 --- a/colorbleed/plugins/maya/publish/collect_look.py +++ b/colorbleed/plugins/maya/publish/collect_look.py @@ -1,9 +1,9 @@ from maya import cmds -from cb.utils.maya import context -import cbra.utils.maya.node_uuid as id_utils import pyblish.api +from cb.utils.maya import context, shaders +import cbra.utils.maya.node_uuid as id_utils SHAPE_ATTRS = ["castsShadows", "receiveShadows", @@ -62,7 +62,7 @@ class CollectLook(pyblish.api.InstancePlugin): """ order = pyblish.api.CollectorOrder + 0.4 - families = ["colorbleed.look"] + families = ["colorbleed.lookdev"] label = "Collect Look" hosts = ["maya"] @@ -87,7 +87,7 @@ class CollectLook(pyblish.api.InstancePlugin): # Discover related object sets self.log.info("Gathering sets..") - self.gather_sets(instance) + sets = self.gather_sets(instance) # Lookup with absolute names (from root namespace) instance_lookup = set([str(x) for x in cmds.ls(instance, @@ -95,9 +95,7 @@ class CollectLook(pyblish.api.InstancePlugin): absoluteName=True)]) self.log.info("Gathering set relations..") - sets = self.gather_sets(instance) for objset in sets: - self.log.debug("From %s.." % objset) content = cmds.sets(objset, query=True) objset_members = sets[objset]["members"] @@ -108,22 +106,23 @@ class CollectLook(pyblish.api.InstancePlugin): verbose) if not member_data: continue - sets[objset]["members"].append(member_data) # Remove sets that didn't have any members assigned in the end - sets = self.clean_sets(sets) - # Member attributes (shapes + transforms) + sets = self.remove_sets_without_members(sets) self.log.info("Gathering attribute changes to instance members..") - attributes = self.collect_attributes_changes(instance) + attributes = self.collect_attributes_changed(instance) looksets = cmds.ls(sets.keys(), absoluteName=True, long=True) # Store data on the instance instance.data["lookData"] = {"attributes": attributes, "relationships": sets.values(), "sets": looksets} + # Collect textures + resources = [self.collect_resource(n) for n in cmds.ls(type="file")] + instance.data["resources"] = resources # Log a warning when no relevant sets were retrieved for the look. if not instance.data["lookData"]["sets"]: @@ -133,41 +132,76 @@ class CollectLook(pyblish.api.InstancePlugin): self.log.info("Collected look for %s" % instance) def gather_sets(self, instance): + """Gather all objectSets which are of importance for publishing + + It checks if all nodes in the instance are related to any objectSet + which need to be + + Args: + instance (list): all nodes to be published + + Returns: + dict + """ # Get view sets (so we can ignore those sets later) sets = dict() view_sets = set() - model_panels = cmds.getPanel(type="modelPanel") - for panel in model_panels: + for panel in cmds.getPanel(type="modelPanel"): view_set = cmds.modelEditor(panel, query=True, viewObjects=True) if view_set: view_sets.add(view_set) for node in instance: - node_sets = self.filter_sets(node, view_sets) - if not node_sets: + related_sets = self.get_related_sets(node, view_sets) + if not related_sets: continue - for objset in node_sets: + for objset in related_sets: if objset in sets: continue + + unique_id = cmds.getAttr("%s.cbId" % objset) sets[objset] = {"name": objset, - "uuid": id_utils.get_id(objset), + "uuid": unique_id, "members": list()} return sets - def filter_sets(self, node, view_sets): + def get_related_sets(self, node, view_sets): + """Get the sets which do not belong to any specific group - node_sets = cmds.listSets(object=node, extendToShape=False) or [] - if not node_sets: - return + Filters out based on: + - id attribute is NOT `pyblish.avalon.container` + - shapes and deformer shapes (alembic creates meshShapeDeformed) + - set name ends with any from a predefined list + - set in not in viewport set (isolate selected for example) + + Args: + node (str): name of the current not to check + """ + + ignored = ["pyblish.avalon.instance", "pyblish.avalon.container"] + + related_sets = cmds.listSets(object=node, extendToShape=False) + if not related_sets: + return [] + + # Ignore `avalon.container` + sets = [s for s in related_sets if + not cmds.attributeQuery("id", node=s, exists=True) or + not cmds.getAttr("%s.id" % s) in ignored] # Exclude deformer sets + # Autodesk documentation on listSets command: + # type(uint) : Returns all sets in the scene of the given + # >>> type: + # >>> 1 - all rendering sets + # >>> 2 - all deformer sets deformer_sets = cmds.listSets(object=node, extendToShape=False, type=2) or [] deformer_sets = set(deformer_sets) # optimize lookup - sets = [s for s in node_sets if s not in deformer_sets] + sets = [s for s in sets if s not in deformer_sets] # Ignore specifically named sets sets = [s for s in sets if not any(s.endswith(x) for x in self.IGNORE)] @@ -176,16 +210,24 @@ class CollectLook(pyblish.api.InstancePlugin): # viewports) sets = [s for s in sets if s not in view_sets] - self.log.info("Found sets {0} for {1}".format(node_sets, node)) + self.log.info("Found sets %s for %s" % (related_sets, node)) return sets - def clean_sets(self, sets): + def remove_sets_without_members(self, sets): + """Remove any set which does not have any members + + Args: + sets (dict): collection if sets with data as value + + Returns: + dict + """ for objset, data in sets.items(): if not data['members']: - self.log.debug("Removing redundant set " - "information: %s" % objset) + self.log.debug("Removing redundant set information: " + "%s" % objset) sets.pop(objset) return sets @@ -215,10 +257,24 @@ class CollectLook(pyblish.api.InstancePlugin): if member in [m["name"] for m in objset_members]: return + # check node type, if mesh get parent! makes assigning shaders easier + if cmds.nodeType(node) == "mesh": + parent = cmds.listRelatives(node, parent=True, fullPath=True) + # a mesh NEEDS to have a parent in Maya logic, no reason for + # assertions or extra checking + parent = parent[0] + if cmds.attributeQuery("cbId", node=parent, exists=True): + node = parent + else: + self.log.error("Transform group of mesh '{}' has no attribute " + "'cbId', this is manditory") + return + if verbose: self.log.debug("Such as %s.." % member) - member_data = {"name": node, "uuid": id_utils.get_id(node)} + member_data = {"name": node, + "uuid": cmds.getAttr("{}.cbId".format(node))} # Include components information when components are assigned if components: @@ -226,7 +282,22 @@ class CollectLook(pyblish.api.InstancePlugin): return member_data - def collect_attributes_changes(self, instance): + def collect_attributes_changed(self, instance): + """Collect all userDefined attributes which have changed + + Each node gets checked for user defined attributes which have been + altered during development. Each changes gets logged in a dictionary + + [{name: node, + uuid: uuid, + attributes: {attribute: value}}] + + Args: + instance (list): all nodes which will be published + + Returns: + list + """ attributes = [] for node in instance: @@ -250,3 +321,46 @@ class CollectLook(pyblish.api.InstancePlugin): attributes.append(data) return attributes + + def collect_resource(self, node, verbose=False): + """Collect the link to the file(s) used (resource) + Args: + node (str): name of the node + verbose (bool): enable debug information + + Returns: + dict + """ + + attribute = "{}.fileTextureName".format(node) + source = cmds.getAttr(attribute) + + # Get the computed file path (e.g. the one with the pattern + # in it) So we can reassign it this computed file path whenever + # we need to. + computed_attribute = "{}.computedFileTextureNamePattern".format(node) + computed_source = cmds.getAttr(computed_attribute) + if source != computed_source: + if verbose: + self.log.debug("File node computed pattern differs from " + "original pattern: {0} " + "({1} -> {2})".format(node, + source, + computed_source)) + + # We replace backslashes with forward slashes because V-Ray + # can't handle the UDIM files with the backslashes in the + # paths as the computed patterns + source = computed_source.replace("\\", "/") + + files = shaders.get_file_node_files(node) + if not files: + self.log.error("File node does not have a texture set: " + "{0}".format(node)) + return + + # Define the resource + return {"node": node, + "attribute": attribute, + "source": source, # required for resources + "files": files} # required for resources diff --git a/colorbleed/plugins/maya/publish/collect_look_textures.py b/colorbleed/plugins/maya/publish/collect_look_textures.py deleted file mode 100644 index a6636b24a2..0000000000 --- a/colorbleed/plugins/maya/publish/collect_look_textures.py +++ /dev/null @@ -1,152 +0,0 @@ -from maya import cmds - -import pyblish.api - -import cb.utils.maya.shaders as shader - -TAGS = ["maya", "attribute", "look"] -TAGS_LOOKUP = set(TAGS) - - -class SelectTextureNodesAction(pyblish.api.Action): - """Select the nodes related to the collected file textures""" - - label = "Select texture nodes" - on = "succeeded" # This action is only available on a failed plug-in - icon = "search" # Icon from Awesome Icon - - def process(self, context, plugin): - - self.log.info("Finding textures..") - - # Get the errored instances - instances = [] - for result in context.data["results"]: - instance = result["instance"] - if instance is None: - continue - - instances.append(instance) - - # Apply pyblish.logic to get the instances for the plug-in - instances = pyblish.api.instances_by_plugin(instances, plugin) - - # Get the texture nodes from the instances - nodes = [] - for instance in instances: - for resource in instance.data.get("resources", []): - if self.is_texture_resource(resource): - node = resource['node'] - nodes.append(node) - - # Ensure unique - nodes = list(set(nodes)) - - if nodes: - self.log.info("Selecting texture nodes: %s" % ", ".join(nodes)) - cmds.select(nodes, r=True, noExpand=True) - else: - self.log.info("No texture nodes found.") - cmds.select(deselect=True) - - def is_texture_resource(self, resource): - """Return whether the resource is a texture""" - - tags = resource.get("tags", []) - if not TAGS_LOOKUP.issubset(tags): - return False - - if resource.get("subfolder", None) != "textures": - return False - - if "node" not in resource: - return False - - return True - - -class CollectLookTextures(pyblish.api.InstancePlugin): - """Collect look textures - - Includes the link from source to destination. - - """ - - order = pyblish.api.CollectorOrder + 0.498 - label = 'Textures' - families = ["colorbleed.look"] - actions = [SelectTextureNodesAction] - - def process(self, instance): - - verbose = instance.data.get("verbose", False) - - # Get textures from sets - sets = instance.data["lookData"]["sets"] - if not sets: - raise RuntimeError("No look sets found for the nodes in the " - "instance. %s" % sets) - - # Get the file nodes - history = cmds.listHistory(sets) or [] - files = cmds.ls(history, type="file") - files = list(set(files)) - - resources = instance.data.get("resources", []) - for node in files: - resource = self.collect_resources(node, verbose) - if not resource: - continue - resources.append(resource) - - # Store resources - instance.data['resources'] = resources - - def collect_resources(self, node, verbose=False): - """Collect the link to the file(s) used (resource) - Args: - node (str): name of the node - verbose (bool): enable debug information - - Returns: - dict - """ - - attribute = "{}.fileTextureName".format(node) - source = cmds.getAttr(attribute) - - # Get the computed file path (e.g. the one with the pattern - # in it) So we can reassign it this computed file path whenever - # we need to. - - computed_attribute = "{}.computedFileTextureNamePattern".format(node) - computed_source = cmds.getAttr(computed_attribute) - if source != computed_source: - if verbose: - self.log.debug("File node computed pattern differs from " - "original pattern: {0} " - "({1} -> {2})".format(node, - source, - computed_source)) - - # We replace backslashes with forward slashes because V-Ray - # can't handle the UDIM files with the backslashes in the - # paths as the computed patterns - source = computed_source.replace("\\", "/") - - files = shader.get_file_node_files(node) - if not files: - self.log.error("File node does not have a texture set: " - "{0}".format(node)) - return - - # Define the resource - resource = {"tags": TAGS[:], - "node": node, - "attribute": attribute, - "source": source, # required for resources - "files": files, # required for resources - "subfolder": "textures" # optional for resources - } - - return resource diff --git a/colorbleed/plugins/maya/publish/collect_textures.py b/colorbleed/plugins/maya/publish/collect_textures.py new file mode 100644 index 0000000000..bc1e1ede0f --- /dev/null +++ b/colorbleed/plugins/maya/publish/collect_textures.py @@ -0,0 +1,284 @@ +from maya import cmds + +import pyblish.api +import cb.utils.maya.shaders as shaders + +TAGS = ["maya", "attribute", "look"] +TAGS_LOOKUP = set(TAGS) + + +class SelectTextureNodesAction(pyblish.api.Action): + """Select the nodes related to the collected file textures""" + + label = "Select texture nodes" + on = "succeeded" # This action is only available on a failed plug-in + icon = "search" # Icon from Awesome Icon + + def process(self, context, plugin): + + self.log.info("Finding textures..") + + # Get the errored instances + instances = [] + for result in context.data["results"]: + instance = result["instance"] + if instance is None: + continue + + instances.append(instance) + + # Apply pyblish.logic to get the instances for the plug-in + instances = pyblish.api.instances_by_plugin(instances, plugin) + + # Get the texture nodes from the instances + nodes = [] + for instance in instances: + for resource in instance.data.get("resources", []): + if self.is_texture_resource(resource): + node = resource['node'] + nodes.append(node) + + # Ensure unique + nodes = list(set(nodes)) + + if nodes: + self.log.info("Selecting texture nodes: %s" % ", ".join(nodes)) + cmds.select(nodes, r=True, noExpand=True) + else: + self.log.info("No texture nodes found.") + cmds.select(deselect=True) + + def is_texture_resource(self, resource): + """Return whether the resource is a texture""" + + tags = resource.get("tags", []) + if not TAGS_LOOKUP.issubset(tags): + return False + + if resource.get("subfolder", None) != "textures": + return False + + if "node" not in resource: + return False + + return True + + +class CollectLookTextures(pyblish.api.InstancePlugin): + """Collect look textures + + Includes the link from source to destination. + + """ + + order = pyblish.api.CollectorOrder + 0.35 + label = 'Collect Look Textures' + families = ["colorbleed.texture"] + actions = [SelectTextureNodesAction] + + IGNORE = ["out_SET", "controls_SET", "_INST"] + + def process(self, instance): + + verbose = instance.data.get("verbose", False) + + # Get all texture nodes from the shader networks + sets = self.gather_sets(instance) + instance_members = {str(i) for i in cmds.ls(instance, long=True, + absoluteName=True)} + + self.log.info("Gathering set relations..") + for objset in sets: + self.log.debug("From %s.." % objset) + content = cmds.sets(objset, query=True) + objset_members = sets[objset]["members"] + for member in cmds.ls(content, long=True, absoluteName=True): + member_data = self.collect_member_data(member, + objset_members, + instance_members, + verbose) + if not member_data: + continue + + # Get the file nodes + history = cmds.listHistory(sets.keys()) or [] + files = cmds.ls(history, type="file") + files = list(set(files)) + + resources = instance.data.get("resources", []) + for node in files: + resource = self.collect_resources(node, verbose) + if not resource: + continue + resources.append(resource) + + instance.data['resources'] = resources + + def gather_sets(self, instance): + """Gather all objectSets which are of importance for publishing + + It checks if all nodes in the instance are related to any objectSet + which need to be + + Args: + instance (list): all nodes to be published + + Returns: + dict + """ + + # Get view sets (so we can ignore those sets later) + sets = dict() + view_sets = set() + for panel in cmds.getPanel(type="modelPanel"): + view_set = cmds.modelEditor(panel, query=True, + viewObjects=True) + if view_set: + view_sets.add(view_set) + + for node in instance: + related_sets = self.get_related_sets(node, view_sets) + if not related_sets: + continue + + for objset in related_sets: + if objset in sets: + continue + unique_id = cmds.getAttr("%s.cbId" % objset) + sets[objset] = {"name": objset, + "uuid": unique_id, + "members": list()} + return sets + + def collect_resources(self, node, verbose=False): + """Collect the link to the file(s) used (resource) + Args: + node (str): name of the node + verbose (bool): enable debug information + + Returns: + dict + """ + + # assure node includes full path + node = cmds.ls(node, long=True)[0] + attribute = "{}.fileTextureName".format(node) + source = cmds.getAttr(attribute) + + # Get the computed file path (e.g. the one with the pattern + # in it) So we can reassign it this computed file path whenever + # we need to. + + computed_attribute = "{}.computedFileTextureNamePattern".format(node) + computed_source = cmds.getAttr(computed_attribute) + if source != computed_source: + if verbose: + self.log.debug("File node computed pattern differs from " + "original pattern: {0} " + "({1} -> {2})".format(node, + source, + computed_source)) + + # We replace backslashes with forward slashes because V-Ray + # can't handle the UDIM files with the backslashes in the + # paths as the computed patterns + source = computed_source.replace("\\", "/") + + files = shaders.get_file_node_files(node) + if not files: + self.log.error("File node does not have a texture set: " + "{0}".format(node)) + return + + # Define the resource + # todo: find a way to generate the destination for the publisher + resource = {"tags": TAGS[:], + "node": node, + "attribute": attribute, + "source": source, # required for resources + "files": files} # required for resources + + return resource + + def collect_member_data(self, member, objset_members, instance_members, + verbose=False): + """Get all information of the node + Args: + member (str): the name of the node to check + objset_members (list): the objectSet members + instance_members (set): the collected instance members + verbose (bool): get debug information + + Returns: + dict + + """ + + node, components = (member.rsplit(".", 1) + [None])[:2] + + # Only include valid members of the instance + if node not in instance_members: + if verbose: + self.log.info("Skipping member %s" % member) + return + + if member in [m["name"] for m in objset_members]: + return + + if verbose: + self.log.debug("Such as %s.." % member) + + member_data = {"name": node, + "uuid": cmds.getAttr("{}.cbId".format(node, ))} + + # Include components information when components are assigned + if components: + member_data["components"] = components + + return member_data + + def get_related_sets(self, node, view_sets): + """Get the sets which do not belong to any specific group + + Filters out based on: + - id attribute is NOT `pyblish.avalon.container` + - shapes and deformer shapes (alembic creates meshShapeDeformed) + - set name ends with any from a predefined list + - set in not in viewport set (isolate selected for example) + + Args: + node (str): name of the current not to check + """ + + ignored = ["pyblish.avalon.instance", "pyblish.avalon.container"] + + related_sets = cmds.listSets(object=node, extendToShape=False) + if not related_sets: + return [] + + # Ignore containers + sets = [s for s in related_sets if + not cmds.attributeQuery("id", node=s, exists=True) or + not cmds.getAttr("%s.id" % s) in ignored] + + # Exclude deformer sets + # Autodesk documentation on listSets command: + # type(uint) : Returns all sets in the scene of the given + # >>> type: + # >>> 1 - all rendering sets + # >>> 2 - all deformer sets + deformer_sets = cmds.listSets(object=node, extendToShape=False, + type=2) or [] + deformer_sets = set(deformer_sets) # optimize lookup + sets = [s for s in sets if s not in deformer_sets] + + # Ignore specifically named sets + sets = [s for s in sets if not any(s.endswith(x) for x in self.IGNORE)] + + # Ignore viewport filter view sets (from isolate select and + # viewports) + sets = [s for s in sets if s not in view_sets] + + self.log.info("Found sets %s for %s" % (related_sets, node)) + + return sets \ No newline at end of file diff --git a/colorbleed/plugins/maya/publish/extract_alembic.py b/colorbleed/plugins/maya/publish/extract_alembic.py index 7db7f2ea18..1b0a50d735 100644 --- a/colorbleed/plugins/maya/publish/extract_alembic.py +++ b/colorbleed/plugins/maya/publish/extract_alembic.py @@ -1,6 +1,8 @@ import os +import copy from maya import cmds + import avalon.maya import colorbleed.api from colorbleed.maya.lib import extract_alembic @@ -14,38 +16,42 @@ class ExtractColorbleedAlembic(colorbleed.api.Extractor): """ label = "Alembic" - families = ["colorbleed.model", - "colorbleed.pointcache", - "colorbleed.proxy"] + families = ["colorbleed.model", "colorbleed.pointcache"] + optional = True def process(self, instance): parent_dir = self.staging_dir(instance) filename = "%s.abc" % instance.name path = os.path.join(parent_dir, filename) - options = dict() - # Collect the start and end including handles if any provided - # otherwise assume frame 1 as startFrame and the same as endFrame - start = instance.data.get("startFrame", 1) - end = instance.data.get("endFrame", start) - handles = instance.data.get("handles", 0) - if handles: - start -= handles - end += handles - options['frameRange'] = (start, end) + attrPrefix = instance.data.get("attrPrefix", []) + attrPrefix.append("cb") - # Default verbosity to False - options['verbose'] = instance.data.get("verbose", False) + options = copy.deepcopy(instance.data) + options['attrPrefix'] = attrPrefix - # Collect instance options if found in `instance.data` - # for specific settings (for user customization) - for key in ["renderableOnly", "writeColorSets"]: - if key in instance.data: - options[key] = instance.data[key] + # Ensure visibility keys are written + options['writeVisibility'] = True + # Write creases + options['writeCreases'] = True + + # Ensure UVs are written + options['uvWrite'] = True + + options['selection'] = True + options["attr"] = ["cbId"] + + # force elect items to ensure all items get exported by Alembic + members = instance.data("setMembers") + print "Members : {}".format(members) + + cmds.select(members) with avalon.maya.suspended_refresh(): with avalon.maya.maintained_selection(): nodes = instance[:] cmds.select(nodes, replace=True, noExpand=True) extract_alembic(file=path, **options) + + cmds.select(clear=True) diff --git a/colorbleed/plugins/maya/publish/extract_animation.py b/colorbleed/plugins/maya/publish/extract_animation.py index 48cb4711dc..f0241a79b4 100644 --- a/colorbleed/plugins/maya/publish/extract_animation.py +++ b/colorbleed/plugins/maya/publish/extract_animation.py @@ -1,4 +1,10 @@ +import os + +from maya import cmds + +import avalon.maya import colorbleed.api +from colorbleed.maya.lib import extract_alembic class ExtractColorbleedAnimation(colorbleed.api.Extractor): @@ -14,10 +20,6 @@ class ExtractColorbleedAnimation(colorbleed.api.Extractor): families = ["colorbleed.animation"] def process(self, instance): - import os - from maya import cmds - import avalon.maya - from colorbleed.maya.lib import extract_alembic # Collect the out set nodes out_sets = [node for node in instance if node.endswith("out_SET")] @@ -27,11 +29,13 @@ class ExtractColorbleedAnimation(colorbleed.api.Extractor): out_set = out_sets[0] nodes = cmds.sets(out_set, query=True) - # Include all descendents + # Include all descendants nodes += cmds.listRelatives(nodes, allDescendents=True, fullPath=True) or [] + print("Exporting {} as alembic".format(nodes)) + # Collect the start and end including handles start = instance.data["startFrame"] end = instance.data["endFrame"] @@ -52,13 +56,10 @@ class ExtractColorbleedAnimation(colorbleed.api.Extractor): with avalon.maya.suspended_refresh(): with avalon.maya.maintained_selection(): cmds.select(nodes, noExpand=True) - extract_alembic(file=path, **{ - "selection": True, - "frameRange": (start, end), - "writeVisibility": True, - "writeUV": True, - "step": instance.data.get("step", 1.0), - "attributePrefix": ("mb",) - }) + extract_alembic(file=path, + startFrame=start, + endFrame=end, + **{"step": instance.data.get("step", 1.0), + "attr": ["cbId"]}) self.log.info("Extracted {} to {}".format(instance, dirname)) diff --git a/colorbleed/plugins/maya/publish/extract_look.py b/colorbleed/plugins/maya/publish/extract_look.py index faf1e85876..04c632e2d9 100644 --- a/colorbleed/plugins/maya/publish/extract_look.py +++ b/colorbleed/plugins/maya/publish/extract_look.py @@ -3,10 +3,11 @@ import json from maya import cmds +import pyblish.api import avalon.maya import colorbleed.api -import cb.utils.maya.context as context +from cb.utils.maya import context class ExtractLook(colorbleed.api.Extractor): @@ -18,9 +19,10 @@ class ExtractLook(colorbleed.api.Extractor): """ - label = "Look (Maya ASCII + JSON)" + label = "Extract Look (Maya ASCII + JSON)" hosts = ["maya"] - families = ["colorbleed.look"] + families = ["colorbleed.lookdev"] + order = pyblish.api.ExtractorOrder + 0.2 def process(self, instance): @@ -29,6 +31,7 @@ class ExtractLook(colorbleed.api.Extractor): maya_fname = "{0}.ma".format(instance.name) json_fname = "{0}.json".format(instance.name) + # Make texture dump folder maya_path = os.path.join(dir_path, maya_fname) json_path = os.path.join(dir_path, json_fname) @@ -37,42 +40,31 @@ class ExtractLook(colorbleed.api.Extractor): # Remove all members of the sets so they are not included in the # exported file by accident self.log.info("Extract sets (Maya ASCII)..") - sets = instance.data["lookSets"] + lookdata = instance.data["lookData"] + sets = lookdata["sets"] - # Define the texture file node remapping - resource_remap = dict() - # required tags to be a look resource - required_tags = ["maya", "attribute", "look"] - resources = instance.data.get("resources", []) - for resource in resources: - resource_tags = resource.get("tags", []) - if all(tag in resource_tags for tag in required_tags): - node = resource['node'] - destination = resource['destination'] - resource_remap["{}.fileTextureName".format(node)] = destination - - # Extract in corect render layer + # Extract in correct render layer layer = instance.data.get("renderlayer", "defaultRenderLayer") with context.renderlayer(layer): # TODO: Ensure membership edits don't become renderlayer overrides with context.empty_sets(sets): - with context.attribute_values(resource_remap): - with avalon.maya.maintained_selection(): - cmds.select(sets, noExpand=True) - cmds.file(maya_path, - force=True, - typ="mayaAscii", - exportSelected=True, - preserveReferences=False, - channels=True, - constraints=True, - expressions=True, - constructionHistory=True) + with avalon.maya.maintained_selection(): + cmds.select(sets, noExpand=True) + cmds.file(maya_path, + force=True, + typ="mayaAscii", + exportSelected=True, + preserveReferences=False, + channels=True, + constraints=True, + expressions=True, + constructionHistory=True) # Write the JSON data self.log.info("Extract json..") - data = {"attributes": instance.data["lookAttributes"], - "sets": instance.data["lookSetRelations"]} + data = {"attributes": lookdata["attributes"], + "sets": lookdata["relationships"]} + with open(json_path, "w") as f: json.dump(data, f) diff --git a/colorbleed/plugins/maya/publish/extract_textures.py b/colorbleed/plugins/maya/publish/extract_textures.py new file mode 100644 index 0000000000..86380d4295 --- /dev/null +++ b/colorbleed/plugins/maya/publish/extract_textures.py @@ -0,0 +1,41 @@ +import json +import os +import shutil + +import pyblish.api +import colorbleed.api +import colorbleed.maya.lib as lib + + +class ExtractTextures(colorbleed.api.Extractor): + + label = "Extract Textures" + hosts = ["maya"] + families = ["colorbleed.texture"] + order = pyblish.api.ExtractorOrder + 0.1 + + def process(self, instance): + + self.log.info("Extracting textures ...") + + dir_path = self.staging_dir(instance) + resources = instance.data["resources"] + for resource in resources: + self.copy_files(dir_path, resource["files"]) + + self.log.info("Storing cross instance information ...") + self.store_data(resources) + + def store_data(self, data): + tmp_dir = lib.maya_temp_folder() + tmp_file = os.path.join(tmp_dir, "resources.json") + with open(tmp_file, "w") as f: + json.dump(data, fp=f, + separators=[",", ":"], + ensure_ascii=False) + + def copy_files(self, dest, files): + for f in files: + fname = os.path.basename(f) + dest_file = os.path.join(dest, fname) + shutil.copy(f, dest_file) diff --git a/colorbleed/plugins/maya/publish/validate_look_members_node_ids.py b/colorbleed/plugins/maya/publish/validate_look_members_node_ids.py index d601b22601..4e851e0b21 100644 --- a/colorbleed/plugins/maya/publish/validate_look_members_node_ids.py +++ b/colorbleed/plugins/maya/publish/validate_look_members_node_ids.py @@ -1,8 +1,8 @@ +import maya.cmds as cmds + import pyblish.api import colorbleed.api -import cbra.utils.maya.node_uuid as id_utils - class ValidateLookMembersNodeIds(pyblish.api.InstancePlugin): """Validate look members have colorbleed id attributes @@ -20,7 +20,8 @@ class ValidateLookMembersNodeIds(pyblish.api.InstancePlugin): families = ['colorbleed.look'] hosts = ['maya'] label = 'Look Members Id Attributes' - actions = [colorbleed.api.SelectInvalidAction] + actions = [colorbleed.api.SelectInvalidAction, + colorbleed.api.GenerateUUIDsOnInvalidAction] @staticmethod def get_invalid(instance): @@ -40,7 +41,7 @@ class ValidateLookMembersNodeIds(pyblish.api.InstancePlugin): # Ensure all nodes have a cbId invalid = list() for node in members: - if not id_utils.has_id(node): + if not cmds.getAttr("{}.cbId".format(node)): invalid.append(node) return invalid diff --git a/colorbleed/plugins/maya/publish/validate_look_members_unique.py b/colorbleed/plugins/maya/publish/validate_look_members_unique.py index 18de8e73c4..48dce8b8d7 100644 --- a/colorbleed/plugins/maya/publish/validate_look_members_unique.py +++ b/colorbleed/plugins/maya/publish/validate_look_members_unique.py @@ -5,7 +5,14 @@ from maya import cmds import pyblish.api import colorbleed.api -import cbra.utils.maya.node_uuid as id_utils + +def get_unique_id(node): + attr = 'cbId' + unique_id = None + has_attribute = cmds.attributeQuery(attr, node=node, exists=True) + if has_attribute: + unique_id = cmds.getAttr("{}.{}".format(node, attr)) + return unique_id class ValidateLookMembersUnique(pyblish.api.InstancePlugin): @@ -25,15 +32,16 @@ class ValidateLookMembersUnique(pyblish.api.InstancePlugin): families = ['colorbleed.look'] hosts = ['maya'] label = 'Look Members Unique' - actions = [colorbleed.api.SelectInvalidAction] + actions = [colorbleed.api.SelectInvalidAction, + colorbleed.api.GenerateUUIDsOnInvalidAction] @staticmethod def get_invalid(instance): # Get all members from the sets members = [] - relations = instance.data["lookData"]["sets"] - for sg in relations: + relationships = instance.data["lookData"]["relationships"] + for sg in relationships: sg_members = sg['members'] sg_members = [member['name'] for member in sg_members] members.extend(sg_members) @@ -45,10 +53,9 @@ class ValidateLookMembersUnique(pyblish.api.InstancePlugin): # Group members per id id_nodes = defaultdict(set) for node in members: - node_id = id_utils.get_id(node) + node_id = get_unique_id(node) if not node_id: continue - id_nodes[node_id].add(node) invalid = list() @@ -61,8 +68,9 @@ class ValidateLookMembersUnique(pyblish.api.InstancePlugin): def process(self, instance): """Process all meshes""" - invalid = self.get_invalid(instance) + print self.actions + invalid = self.get_invalid(instance) if invalid: raise RuntimeError("Members found without " "asset IDs: {0}".format(invalid)) diff --git a/colorbleed/plugins/maya/publish/validate_look_node_unique_ids.py b/colorbleed/plugins/maya/publish/validate_look_node_unique_ids.py index 738075fec5..653e8a347c 100644 --- a/colorbleed/plugins/maya/publish/validate_look_node_unique_ids.py +++ b/colorbleed/plugins/maya/publish/validate_look_node_unique_ids.py @@ -1,9 +1,10 @@ from collections import defaultdict + +import maya.cmds as cmds + import pyblish.api import colorbleed.api -import cbra.utils.maya.node_uuid as id_utils - class ValidateLookNodeUniqueIds(pyblish.api.InstancePlugin): """Validate look sets have unique colorbleed id attributes @@ -15,7 +16,7 @@ class ValidateLookNodeUniqueIds(pyblish.api.InstancePlugin): hosts = ['maya'] label = 'Look Id Unique Attributes' actions = [colorbleed.api.SelectInvalidAction, - colorbleed.api.GenerateUUIDsOnInvalidAction] + colorbleed.api.RepairAction] @staticmethod def get_invalid(instance): @@ -26,13 +27,15 @@ class ValidateLookNodeUniqueIds(pyblish.api.InstancePlugin): id_sets = defaultdict(list) invalid = list() for node in nodes: - id = id_utils.get_id(node) - if not id: + unique_id = None + if cmds.attributeQuery("mbId", node=node, exists=True): + unique_id = cmds.getAttr("{}.mbId".format(node)) + if not unique_id: continue - id_sets[id].append(node) + id_sets[unique_id].append(node) - for id, nodes in id_sets.iteritems(): + for unique_id, nodes in id_sets.iteritems(): if len(nodes) > 1: invalid.extend(nodes) @@ -42,7 +45,6 @@ class ValidateLookNodeUniqueIds(pyblish.api.InstancePlugin): """Process all meshes""" invalid = self.get_invalid(instance) - if invalid: raise RuntimeError("Nodes found without " "asset IDs: {0}".format(invalid)) diff --git a/colorbleed/plugins/maya/publish/validate_naming_convention.py b/colorbleed/plugins/maya/publish/validate_naming_convention.py index e69de29bb2..7dbf9ad3f7 100644 --- a/colorbleed/plugins/maya/publish/validate_naming_convention.py +++ b/colorbleed/plugins/maya/publish/validate_naming_convention.py @@ -0,0 +1,34 @@ +import re + +import pyblish.api +import colorbleed.api + + +class ValidateNamingConvention(pyblish.api.InstancePlugin): + + label = "" + families = ["colorbleed.model"] + host = ["maya"] + actions = [colorbleed.api.SelectInvalidAction] + + @staticmethod + def get_invalid(instance): + + invalid = [] + # todo: change pattern to company standard + pattern = re.compile("[a-zA-Z]+_[A-Z]{3}") + + nodes = list(instance) + for node in nodes: + match = pattern.match(node) + if not match: + invalid.append(node) + + return invalid + + def process(self, instance): + + invalid = self.get_invalid(instance) + if invalid: + self.log.error("Found invalid naming convention. Failed noted :\n" + "%s" % invalid) diff --git a/colorbleed/plugins/maya/publish/validate_resources.py b/colorbleed/plugins/maya/publish/validate_resources.py index 57a9239905..2dc6f8c313 100644 --- a/colorbleed/plugins/maya/publish/validate_resources.py +++ b/colorbleed/plugins/maya/publish/validate_resources.py @@ -23,9 +23,7 @@ class ValidateResources(pyblish.api.InstancePlugin): def process(self, instance): for resource in instance.data.get('resources', []): - # Required data - assert "source" in resource - assert "destination" in resource - assert "files" in resource + 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/colorbleed/plugins/maya/publish/validate_rig_contents.py b/colorbleed/plugins/maya/publish/validate_rig_contents.py index 5744806005..3ef1220e56 100644 --- a/colorbleed/plugins/maya/publish/validate_rig_contents.py +++ b/colorbleed/plugins/maya/publish/validate_rig_contents.py @@ -44,7 +44,7 @@ class ValidateRigContents(pyblish.api.InstancePlugin): output_content = cmds.sets("out_SET", query=True) or [] assert output_content, "Must have members in rig out_SET" - controls_content = cmds.set("controls_SET", query=True) or [] + controls_content = cmds.sets("controls_SET", query=True) or [] assert controls_content, "Must have members in rig controls_SET" root_node = cmds.ls(set_members, assemblies=True) @@ -56,15 +56,15 @@ class ValidateRigContents(pyblish.api.InstancePlugin): self.invalid_controls = self.validate_controls(controls_content, hierarchy) - if self.invalid_hierachy: + if self.invalid_hierarchy: self.log.error("Found nodes which reside outside of root group " "while they are set up for publishing." - "\n%s" % self.invalid_hierachy) + "\n%s" % self.invalid_hierarchy) error = True - if self.not_transforms: + if self.invalid_controls: self.log.error("Only transforms can be part of the controls_SET." - "\n%s" % self.not_transforms) + "\n%s" % self.invalid_controls) error = True if self.invalid_geometry: @@ -100,6 +100,7 @@ class ValidateRigContents(pyblish.api.InstancePlugin): """ errors = [] for node in nodes: + print node if node not in hierarchy: errors.append(node) return errors @@ -128,10 +129,12 @@ class ValidateRigContents(pyblish.api.InstancePlugin): # The user can add the shape node to the out_set, this will result # in none when querying allDescendents all_shapes = set_members + shapes + all_long_names = [cmds.ls(i, long=True)[0] for i in all_shapes] # geometry - invalid_shapes = self.validate_hierarchy(hierarchy, all_shapes) - self.invalid_hierachy.extend(invalid_shapes) + invalid_shapes = self.validate_hierarchy(hierarchy, + all_long_names) + self.invalid_hierarchy.extend(invalid_shapes) for shape in all_shapes: nodetype = cmds.nodeType(shape) if nodetype in self.ignore_nodes: @@ -157,8 +160,10 @@ class ValidateRigContents(pyblish.api.InstancePlugin): """ errors = [] - invalid_controllers = self.validate_hierarchy(hierarchy, set_members) - self.invalid_hierachy.extend(invalid_controllers) + all_long_names = [cmds.ls(i, long=True)[0] for i in set_members] + invalid_controllers = self.validate_hierarchy(hierarchy, + all_long_names) + self.invalid_hierarchy.extend(invalid_controllers) for node in set_members: nodetype = cmds.nodeType(node) if nodetype in self.ignore_nodes: diff --git a/colorbleed/plugins/maya/publish/validate_rig_controllers.py b/colorbleed/plugins/maya/publish/validate_rig_controllers.py index dd9f77006c..9b066f6351 100644 --- a/colorbleed/plugins/maya/publish/validate_rig_controllers.py +++ b/colorbleed/plugins/maya/publish/validate_rig_controllers.py @@ -1,8 +1,12 @@ +import logging + from maya import cmds import pyblish.api import colorbleed.api +log = logging.getLogger("Rig Controllers") + class ValidateRigControllers(pyblish.api.InstancePlugin): """Check if the controllers have the transformation attributes set to @@ -12,6 +16,7 @@ class ValidateRigControllers(pyblish.api.InstancePlugin): label = "Rig Controllers" hosts = ["maya"] families = ["colorbleed.rig"] + actions = [colorbleed.api.RepairAction] def process(self, instance): @@ -21,7 +26,7 @@ class ValidateRigControllers(pyblish.api.InstancePlugin): is_offset = list() controls = cmds.sets("controls_SET", query=True) - assert controls, "Must have controls in rig control_SET" + assert controls, "Must have controls in rig controls_SET" for control in controls: valid_keyed = self.validate_keyed_state(control) @@ -41,15 +46,18 @@ class ValidateRigControllers(pyblish.api.InstancePlugin): if is_keyed: self.log.error("No controls can be keyes. Failed :\n" "%s" % is_keyed) + error = True if is_offset: self.log.error("All controls default transformation values. " "Failed :\n%s" % is_offset) + error = True if not_locked: self.log.error("All controls must have visibility " "attribute locked. Failed :\n" "%s" % not_locked) + error = True if error: raise RuntimeError("Invalid rig controllers. See log for details.") @@ -78,3 +86,17 @@ class ValidateRigControllers(pyblish.api.InstancePlugin): if animation_curves: return False return True + + @classmethod + def repair(cls, instance): + + # lock all controllers in controls_SET + controls = cmds.sets("controls_SET", query=True) + for control in controls: + attr = "{}.visibility".format(control) + locked = cmds.getAttr(attr, lock=True) + if not locked: + print("Locking visibility for %s" % control) + cmds.setAttr(attr, lock=True) + + continue diff --git a/colorbleed/plugins/maya/publish/_validate_unique_node_ids.py b/colorbleed/plugins/maya/publish/validate_unique_node_ids.py similarity index 94% rename from colorbleed/plugins/maya/publish/_validate_unique_node_ids.py rename to colorbleed/plugins/maya/publish/validate_unique_node_ids.py index d7453a3a4c..8e3ccb8a43 100644 --- a/colorbleed/plugins/maya/publish/_validate_unique_node_ids.py +++ b/colorbleed/plugins/maya/publish/validate_unique_node_ids.py @@ -1,3 +1,7 @@ +from collections import defaultdict + +import maya.cmds as cmds + import pyblish.api import colorbleed.api @@ -16,12 +20,9 @@ class ValidateUniqueNodeIds(pyblish.api.InstancePlugin): def get_invalid_dict(instance): """Return a dictionary mapping of id key to list of member nodes""" - import maya.cmds as cmds - - uuid_attr = "mbId" + uuid_attr = "cbId" # Collect each id with their members - from collections import defaultdict ids = defaultdict(list) for member in instance: has_attr = cmds.attributeQuery(uuid_attr, node=member, exists=True) @@ -60,4 +61,3 @@ class ValidateUniqueNodeIds(pyblish.api.InstancePlugin): if invalid: raise RuntimeError("Nodes found with non-unique " "asset IDs: {0}".format(invalid)) - diff --git a/colorbleed/plugins/publish/integrate_asset.py b/colorbleed/plugins/publish/integrate_asset.py index 531b550749..fd4469f871 100644 --- a/colorbleed/plugins/publish/integrate_asset.py +++ b/colorbleed/plugins/publish/integrate_asset.py @@ -1,13 +1,12 @@ import os import errno import shutil -from pprint import pformat import pyblish.api -from avalon import api, io +from avalon import io -class IntegrateMindbenderAsset(pyblish.api.InstancePlugin): +class IntegrateAsset(pyblish.api.InstancePlugin): """Write to files and metadata This plug-in exposes your data to others by encapsulating it @@ -30,226 +29,33 @@ class IntegrateMindbenderAsset(pyblish.api.InstancePlugin): """ label = "Integrate Asset" - order = pyblish.api.IntegratorOrder - families = [ - "colorbleed.model", - "colorbleed.rig", - "colorbleed.animation", - "colorbleed.camera", - "colorbleed.lookdev", - "colorbleed.historyLookdev", - "colorbleed.group", - "colorbleed.pointcache" - ] + order = pyblish.api.IntegratorOrder + 0.1 + families = ["colorbleed.model", + "colorbleed.rig", + "colorbleed.animation", + "colorbleed.camera", + "colorbleed.lookdev", + "colorbleed.texture", + "colorbleed.historyLookdev", + "colorbleed.group"] def process(self, instance): - # Required environment variables - PROJECT = os.environ["AVALON_PROJECT"] - ASSET = instance.data.get("asset") or os.environ["AVALON_ASSET"] - SILO = os.environ["AVALON_SILO"] - LOCATION = os.getenv("AVALON_LOCATION") - # todo(marcus): avoid hardcoding labels in the integrator - representation_labels = {".ma": "Maya Ascii", - ".source": "Original source file", - ".abc": "Alembic"} + # get needed data + traffic = instance.data["traffic"] + representations = instance.data["representations"] - context = instance.context - # Atomicity - # - # Guarantee atomic publishes - each asset contains - # an identical set of members. - # __ - # / o - # / \ - # | o | - # \ / - # o __/ - # - assert all(result["success"] for result in context.data["results"]), ( - "Atomicity not held, aborting.") + self.log.info("Registering {} items".format(len(representations))) + io.insert_many(representations) - # Assemble - # - # | - # v - # ---> <---- - # ^ - # | - # - stagingdir = instance.data.get("stagingDir") - assert stagingdir, ("Incomplete instance \"%s\": " - "Missing reference to staging area." % instance) + # moving files + for src, dest in traffic: + self.log.info("Copying file .. {} -> {}".format(src, dest)) + self.copy_file(src, dest) - self.log.debug("Establishing staging directory @ %s" % stagingdir) - - project = io.find_one({"type": "project"}) - asset = io.find_one({"name": ASSET}) - - assert all([project, asset]), ("Could not find current project or " - "asset '%s'" % ASSET) - - subset = self.get_subset(asset, instance) - - # get next version - latest_version = io.find_one({"type": "version", - "parent": subset["_id"]}, - {"name": True}, - sort=[("name", -1)]) - - next_version = 1 - if latest_version is not None: - next_version += latest_version["name"] - - self.log.debug("Next version: %i" % next_version) - - version_data = self.create_version_data(context, instance) - version = self.create_version(subset=subset, - version_number=next_version, - locations=[LOCATION], - data=version_data) - - self.log.debug("Creating version: %s" % pformat(version)) - version_id = io.insert_one(version).inserted_id - - # Write to disk - # _ - # | | - # _| |_ - # ____\ / - # |\ \ / \ - # \ \ v \ - # \ \________. - # \|________| - # - template_data = { - "root": api.registered_root(), - "project": PROJECT, - "silo": SILO, - "asset": ASSET, - "subset": subset["name"], - "version": version["name"], - } - - template_publish = project["config"]["template"]["publish"] - - for fname in os.listdir(stagingdir): - name, ext = os.path.splitext(fname) - template_data["representation"] = ext[1:] - - src = os.path.join(stagingdir, fname) - dst = template_publish.format(**template_data) - - # Backwards compatibility - if fname == ".metadata.json": - dirname = os.path.dirname(dst) - dst = os.path.join(dirname, ".metadata.json") - - self.log.info("Copying %s -> %s" % (src, dst)) - - # copy source to destination (library) - self.copy_file(src, dst) - - representation = { - "schema": "avalon-core:representation-2.0", - "type": "representation", - "parent": version_id, - "name": ext[1:], - "data": {"label": representation_labels.get(ext)}, - "dependencies": instance.data.get("dependencies", "").split(), - - # Imprint shortcut to context - # for performance reasons. - "context": { - "project": PROJECT, - "asset": ASSET, - "silo": SILO, - "subset": subset["name"], - "version": version["name"], - "representation": ext[1:] - } - } - - io.insert_one(representation) - - self.log.info("Successfully integrated \"%s\" to \"%s\"" % ( - instance, dst)) - - def get_subset(self, asset, instance): - - subset = io.find_one({"type": "subset", - "parent": asset["_id"], - "name": instance.data["subset"]}) - - if subset is None: - subset_name = instance.data["subset"] - self.log.info("Subset '%s' not found, creating.." % subset_name) - - _id = io.insert_one({ - "schema": "avalon-core:subset-2.0", - "type": "subset", - "name": subset_name, - "data": {}, - "parent": asset["_id"] - }).inserted_id - - subset = io.find_one({"_id": _id}) - - return subset - - def create_representation(self): - pass - - def create_version(self, subset, version_number, locations, data=None): - """ Copy given source to destination - - Arguments: - subset (dict): the registered subset of the asset - version_number (int): the version number - locations (list): the currently registered locations - """ - # Imprint currently registered location - version_locations = [location for location in locations if - location is not None] - - return {"schema": "avalon-core:version-2.0", - "type": "version", - "parent": subset["_id"], - "name": version_number, - "locations": version_locations, - "data": data} - - def create_version_data(self, context, instance): - """ - Create the data collection for th version - Args: - context (object): the current context - instance(object): the current instance being published - - Returns: - dict: the required information with instance.data as key - """ - - families = [] - current_families = instance.data.get("families", list()) - instance_family = instance.data.get("family", None) - - families += current_families - if instance_family is not None: - families.append(instance_family) - - # create relative source path for DB - relative_path = os.path.relpath(context.data["currentFile"], - api.registered_root()) - source = os.path.join("{root}", relative_path).replace("\\", "/") - - version_data = {"families": families, - "time": context.data["time"], - "author": context.data["user"], - "source": source, - "comment": context.data.get("comment")} - - return dict(instance.data, **version_data) + self.log.info("Removing temporary files and folders ...") + stagingdir = instance.data["stagingDir"] + shutil.rmtree(stagingdir) def copy_file(self, src, dst): """ Copy given source to destination @@ -271,4 +77,4 @@ class IntegrateMindbenderAsset(pyblish.api.InstancePlugin): self.log.critical("An unexpected error occurred.") raise - shutil.copy(src, dst) \ No newline at end of file + shutil.copy(src, dst) diff --git a/colorbleed/plugins/publish/post_look_integrate.py b/colorbleed/plugins/publish/post_look_integrate.py new file mode 100644 index 0000000000..e66828b3c8 --- /dev/null +++ b/colorbleed/plugins/publish/post_look_integrate.py @@ -0,0 +1,40 @@ +import json +import os + + +import colorbleed.maya.lib as lib + +import pyblish.api + + +class IntegrateAsset(pyblish.api.InstancePlugin): + """Remap source paths for lookdev and textures + + """ + + label = "Remap source paths" + order = pyblish.api.IntegratorOrder + 0.15 + families = ["colorbleed.lookdev", + "colorbleed.texture"] + + def process(self, instance): + + family = instance.data['family'] + resources = instance.data['resources'] + version_folder = instance.data['versionFolder'] + + if family == "colorbleed.texture": + try: + lib.remap_resource_nodes(resources, folder=version_folder) + except Exception as e: + self.log.error(e) + + if family == "colorbleed.lookdev": + try: + tmp_dir = lib.maya_temp_folder() + resource_file = os.path.join(tmp_dir, "resources.json") + with open(resource_file, "r") as f: + resources = json.load(f) + lib.remap_resource_nodes(resources) + except Exception as e: + self.log.error(e) diff --git a/colorbleed/plugins/publish/pre_integrate.py b/colorbleed/plugins/publish/pre_integrate.py new file mode 100644 index 0000000000..a6c798914d --- /dev/null +++ b/colorbleed/plugins/publish/pre_integrate.py @@ -0,0 +1,251 @@ +import os +import logging + +import pyblish.api +from avalon import api, io +import colorbleed.filetypes as filetypes + + +log = logging.getLogger(__name__) + + +class PreIntegrateAsset(pyblish.api.InstancePlugin): + """Resolve any dependency issies + + This plug-in resolves any paths which, if not updated might break + the published file. + + The order of families is important, when working with lookdev you want to + first publish the texture, update the texture paths in the nodes and then + publish the shading network. Same goes for file dependent assets. + """ + + label = "Pre Intergrate Asset" + order = pyblish.api.IntegratorOrder + families = ["colorbleed.model", + "colorbleed.rig", + "colorbleed.animation", + "colorbleed.camera", + "colorbleed.lookdev", + "colorbleed.texture", + "colorbleed.historyLookdev", + "colorbleed.group"] + + def process(self, instance): + + # Required environment variables + PROJECT = os.environ["AVALON_PROJECT"] + ASSET = instance.data.get("asset") or os.environ["AVALON_ASSET"] + SILO = os.environ["AVALON_SILO"] + LOCATION = os.getenv("AVALON_LOCATION") + + # todo(marcus): avoid hardcoding labels in the integrator + representation_labels = {".ma": "Maya Ascii", + ".source": "Original source file", + ".abc": "Alembic"} + + context = instance.context + # Atomicity + # + # Guarantee atomic publishes - each asset contains + # an identical set of members. + # __ + # / o + # / \ + # | o | + # \ / + # o __/ + # + assert all(result["success"] for result in context.data["results"]), ( + "Atomicity not held, aborting.") + + # Assemble + # + # | + # v + # ---> <---- + # ^ + # | + # + stagingdir = instance.data.get("stagingDir") + assert stagingdir, ("Incomplete instance \"%s\": " + "Missing reference to staging area." % instance) + + # extra check if stagingDir actually exists and is available + + self.log.debug("Establishing staging directory @ %s" % stagingdir) + + project = io.find_one({"type": "project"}) + asset = io.find_one({"name": ASSET}) + + assert all([project, asset]), ("Could not find current project or " + "asset '%s'" % ASSET) + + subset = self.get_subset(asset, instance) + + # get next version + latest_version = io.find_one({"type": "version", + "parent": subset["_id"]}, + {"name": True}, + sort=[("name", -1)]) + + next_version = 1 + if latest_version is not None: + next_version += latest_version["name"] + + self.log.debug("Next version: %i" % next_version) + + version_data = self.create_version_data(context, instance) + version = self.create_version(subset=subset, + version_number=next_version, + locations=[LOCATION], + data=version_data) + + self.log.debug("Creating version ...") + version_id = io.insert_one(version).inserted_id + + # Write to disk + # _ + # | | + # _| |_ + # ____\ / + # |\ \ / \ + # \ \ v \ + # \ \________. + # \|________| + # + root = api.registered_root() + template_data = {"root": root, + "project": PROJECT, + "silo": SILO, + "asset": ASSET, + "subset": subset["name"], + "version": version["name"]} + + template_publish = project["config"]["template"]["publish"] + + representations = [] + traffic = [] + staging_content = os.listdir(stagingdir) + for v, fname in enumerate(staging_content): + + name, ext = os.path.splitext(fname) + template_data["representation"] = ext[1:] + + src = os.path.join(stagingdir, fname) + dst = template_publish.format(**template_data) + if v == 0: + instance.data["versionFolder"] = os.path.dirname(dst) + + # Files to copy as if or to specific folder + if ext in filetypes.accepted_images_types: + dirname = os.path.dirname(dst) + dst = os.path.join(dirname, fname) + + # Backwards compatibility + if fname == ".metadata.json": + dirname = os.path.dirname(dst) + dst = os.path.join(dirname, fname) + + # copy source to destination (library) + traffic.append([src, dst]) + + representation = { + "schema": "avalon-core:representation-2.0", + "type": "representation", + "parent": version_id, + "name": ext[1:], + "data": {"label": representation_labels.get(ext)}, + "dependencies": instance.data.get("dependencies", "").split(), + + # Imprint shortcut to context + # for performance reasons. + "context": { + "project": PROJECT, + "asset": ASSET, + "silo": SILO, + "subset": subset["name"], + "version": version["name"], + "representation": ext[1:] + } + } + representations.append(representation) + + # store data for database and source / destinations + instance.data["representations"] = representations + instance.data["traffic"] = traffic + + return representations + + def get_subset(self, asset, instance): + + subset = io.find_one({"type": "subset", + "parent": asset["_id"], + "name": instance.data["subset"]}) + + if subset is None: + subset_name = instance.data["subset"] + self.log.info("Subset '%s' not found, creating.." % subset_name) + + _id = io.insert_one({ + "schema": "avalon-core:subset-2.0", + "type": "subset", + "name": subset_name, + "data": {}, + "parent": asset["_id"] + }).inserted_id + + subset = io.find_one({"_id": _id}) + + return subset + + def create_version(self, subset, version_number, locations, data=None): + """ Copy given source to destination + + Arguments: + subset (dict): the registered subset of the asset + version_number (int): the version number + locations (list): the currently registered locations + """ + # Imprint currently registered location + version_locations = [location for location in locations if + location is not None] + + return {"schema": "avalon-core:version-2.0", + "type": "version", + "parent": subset["_id"], + "name": version_number, + "locations": version_locations, + "data": data} + + def create_version_data(self, context, instance): + """Create the data collection for th version + + Args: + context: the current context + instance: the current instance being published + + Returns: + dict: the required information with instance.data as key + """ + + families = [] + current_families = instance.data.get("families", list()) + instance_family = instance.data.get("family", None) + + families += current_families + if instance_family is not None: + families.append(instance_family) + + # create relative source path for DB + relative_path = os.path.relpath(context.data["currentFile"], + api.registered_root()) + source = os.path.join("{root}", relative_path).replace("\\", "/") + + version_data = {"families": families, + "time": context.data["time"], + "author": context.data["user"], + "source": source, + "comment": context.data.get("comment")} + + return dict(instance.data, **version_data)