From 05a442aec20978d8467fe0ef5657ac0492bc9c09 Mon Sep 17 00:00:00 2001 From: wijnand Date: Fri, 2 Mar 2018 12:47:11 +0100 Subject: [PATCH 01/18] Implemented switch function for loader to support switching subset --- colorbleed/plugins/fusion/load/load_sequence.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/colorbleed/plugins/fusion/load/load_sequence.py b/colorbleed/plugins/fusion/load/load_sequence.py index 06304c685c..6d3d743c8a 100644 --- a/colorbleed/plugins/fusion/load/load_sequence.py +++ b/colorbleed/plugins/fusion/load/load_sequence.py @@ -143,6 +143,9 @@ class FusionLoadSequence(api.Loader): context=context, loader=self.__class__.__name__) + def switch(self, container, representation): + self.update(container, representation) + def update(self, container, representation): """Update the Loader's path From 9520f563b8a6b4bc17753a4fab421ef2a0dbee05 Mon Sep 17 00:00:00 2001 From: wijnand Date: Fri, 2 Mar 2018 12:52:23 +0100 Subject: [PATCH 02/18] Added switch function --- colorbleed/plugins/maya/load/load_model.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/colorbleed/plugins/maya/load/load_model.py b/colorbleed/plugins/maya/load/load_model.py index ff512610a3..9428ad40bd 100644 --- a/colorbleed/plugins/maya/load/load_model.py +++ b/colorbleed/plugins/maya/load/load_model.py @@ -30,6 +30,9 @@ class ModelLoader(colorbleed.maya.plugin.ReferenceLoader): return nodes + def switch(self, container, representation): + self.update(container, representation) + class ImportModelLoader(api.Loader): """An ImportModelLoader for Maya From 5bd79a1b2ea75ef5a5f8112b693a846fa8ac343b Mon Sep 17 00:00:00 2001 From: wijnand Date: Fri, 2 Mar 2018 16:37:33 +0100 Subject: [PATCH 03/18] slapcomp switch batch script --- colorbleed/scripts/slapcomp_switch.py | 80 +++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 colorbleed/scripts/slapcomp_switch.py diff --git a/colorbleed/scripts/slapcomp_switch.py b/colorbleed/scripts/slapcomp_switch.py new file mode 100644 index 0000000000..70553dd903 --- /dev/null +++ b/colorbleed/scripts/slapcomp_switch.py @@ -0,0 +1,80 @@ +import logging +import argparse + +import avalon.io as io +import avalon.api as api +import avalon.fusion + + +log = logging.getLogger("UpdateSlapComp") + + +def switch(data, targets=None): + """Update comp loaders through the containers + + Args: + data (dict): collection if {asset: [subset, subset]} + targets (list): to be implemented + """ + + representations = [] + + for asset_name, subsets in data.iteritems(): + + asset = io.find_one({"type": "asset", "name": asset_name}) + assert asset, ("Could not find asset in the database with the name " + "'%s'" % asset_name) + + subsets = io.find({"type": "subset", + "name": {"$in": subsets}, + "parent": asset["_id"]}) + + for subset in subsets: + version = io.find_one({"type": "version", + "parent": subset["_id"]}, + sort=[('name', -1)]) + + if not version: + log.error("Could not find a verison for {}.{}".format( + asset["name"], subset["name"] + )) + continue + + representation = io.find_one({"type": "representation", + "parent": version["_id"]}) + + representations.append(representation) + + host = api.registered_host() + containers = list(host.ls()) + count = 0 + for i, container in enumerate(containers): + representation = representations[i] + api.switch(container, representation) + count += 1 + + log.info("Updated %i items" % count) + + +if __name__ == '__main__': + + parser = argparse.ArgumentParser() + + parser.add_argument("--data", + help="Dictionary with asset name as key and related " + "subsets (list) as value") + + parser.add_argument("--targets", + optional=True, + help="A list of target to update with given data") + + args = parser.parse_known_args() + if not args.data: + raise RuntimeError("No data given, cannot update slap comp") + + if not isinstance(args.data, dict): + raise ValueError("Expecting data in the form of a dict, got " + "%s" % type(args.data)) + + api.install(avalon.fusion) + switch(data=args.data) From a5be74475e5f0b5c6aa4c0acaf96269d504485e2 Mon Sep 17 00:00:00 2001 From: wijnand Date: Fri, 2 Mar 2018 18:10:21 +0100 Subject: [PATCH 04/18] Improved logic, arg only needs shot name --- colorbleed/scripts/slapcomp_switch.py | 83 +++++++++++++++------------ 1 file changed, 45 insertions(+), 38 deletions(-) diff --git a/colorbleed/scripts/slapcomp_switch.py b/colorbleed/scripts/slapcomp_switch.py index 70553dd903..95270259aa 100644 --- a/colorbleed/scripts/slapcomp_switch.py +++ b/colorbleed/scripts/slapcomp_switch.py @@ -9,47 +9,55 @@ import avalon.fusion log = logging.getLogger("UpdateSlapComp") -def switch(data, targets=None): +def switch(shot): """Update comp loaders through the containers Args: data (dict): collection if {asset: [subset, subset]} - targets (list): to be implemented """ - representations = [] - - for asset_name, subsets in data.iteritems(): - - asset = io.find_one({"type": "asset", "name": asset_name}) - assert asset, ("Could not find asset in the database with the name " - "'%s'" % asset_name) - - subsets = io.find({"type": "subset", - "name": {"$in": subsets}, - "parent": asset["_id"]}) - - for subset in subsets: - version = io.find_one({"type": "version", - "parent": subset["_id"]}, - sort=[('name', -1)]) - - if not version: - log.error("Could not find a verison for {}.{}".format( - asset["name"], subset["name"] - )) - continue - - representation = io.find_one({"type": "representation", - "parent": version["_id"]}) - - representations.append(representation) - host = api.registered_host() containers = list(host.ls()) + + targets = [c["name"] for c in containers] + + representations = {} + + asset = io.find_one({"type": "asset", "name": shot}) + assert asset, ("Could not find asset in the database with the name " + "'%s'" % shot) + + subsets = io.find({"type": "subset", + "name": {"$in": targets}, + "parent": asset["_id"]}) + + if len(subsets) != len(targets): + log.warning("Could not find the same amount of subsets in '%s'" % shot) + + for subset in subsets: + version = io.find_one({"type": "version", + "parent": subset["_id"]}, + sort=[('name', -1)]) + + if not version: + log.error("Could not find a verison for {}.{}".format( + asset["name"], subset["name"] + )) + continue + + representation = io.find_one({"type": "representation", + "parent": version["_id"]}) + + representations[subset["name"]] = representation + count = 0 for i, container in enumerate(containers): - representation = representations[i] + subsetname = container["name"] + if subsetname not in representations: + log.error("Subset '%s' not found in comp, skipping" % subsetname) + continue + + representation = representations[container["name"]] api.switch(container, representation) count += 1 @@ -60,21 +68,20 @@ if __name__ == '__main__': parser = argparse.ArgumentParser() - parser.add_argument("--data", - help="Dictionary with asset name as key and related " - "subsets (list) as value") + parser.add_argument("--shot", + help="Shotname to update the current comp to") parser.add_argument("--targets", optional=True, help="A list of target to update with given data") args = parser.parse_known_args() - if not args.data: + if not args.shot: raise RuntimeError("No data given, cannot update slap comp") - if not isinstance(args.data, dict): + if not isinstance(args.shot, dict): raise ValueError("Expecting data in the form of a dict, got " - "%s" % type(args.data)) + "%s" % type(args.shot)) api.install(avalon.fusion) - switch(data=args.data) + switch(shot=args.shot) From 0b7161dfac1a863940c84dc35c0439571771188e Mon Sep 17 00:00:00 2001 From: wijnand Date: Mon, 5 Mar 2018 13:49:25 +0100 Subject: [PATCH 05/18] Added switch method to support switching --- colorbleed/plugins/maya/load/load_alembic.py | 3 +++ colorbleed/plugins/maya/load/load_camera.py | 4 +++- colorbleed/plugins/maya/load/load_look.py | 3 +++ colorbleed/plugins/maya/load/load_mayaascii.py | 3 +++ colorbleed/plugins/maya/load/load_model.py | 3 +++ colorbleed/plugins/maya/load/load_rig.py | 3 +++ colorbleed/plugins/maya/load/load_yeti_cache.py | 3 +++ 7 files changed, 21 insertions(+), 1 deletion(-) diff --git a/colorbleed/plugins/maya/load/load_alembic.py b/colorbleed/plugins/maya/load/load_alembic.py index e9f4ce284b..afea761ab8 100644 --- a/colorbleed/plugins/maya/load/load_alembic.py +++ b/colorbleed/plugins/maya/load/load_alembic.py @@ -28,3 +28,6 @@ class AbcLoader(colorbleed.maya.plugin.ReferenceLoader): self[:] = nodes return nodes + + def switch(self, container, representation): + self.update(container, representation) diff --git a/colorbleed/plugins/maya/load/load_camera.py b/colorbleed/plugins/maya/load/load_camera.py index 7bb095df67..c1e1b4bf25 100644 --- a/colorbleed/plugins/maya/load/load_camera.py +++ b/colorbleed/plugins/maya/load/load_camera.py @@ -14,7 +14,6 @@ class CameraLoader(colorbleed.maya.plugin.ReferenceLoader): def process_reference(self, context, name, namespace, data): import maya.cmds as cmds - # import pprint # Get family type from the context cmds.loadPlugin("AbcImport.mll", quiet=True) @@ -41,3 +40,6 @@ class CameraLoader(colorbleed.maya.plugin.ReferenceLoader): self[:] = nodes return nodes + + def switch(self, container, representation): + self.update(container, representation) diff --git a/colorbleed/plugins/maya/load/load_look.py b/colorbleed/plugins/maya/load/load_look.py index 16428ef034..cc13918ba1 100644 --- a/colorbleed/plugins/maya/load/load_look.py +++ b/colorbleed/plugins/maya/load/load_look.py @@ -35,3 +35,6 @@ class LookLoader(colorbleed.maya.plugin.ReferenceLoader): returnNewNodes=True) self[:] = nodes + + def switch(self, container, representation): + self.update(container, representation) diff --git a/colorbleed/plugins/maya/load/load_mayaascii.py b/colorbleed/plugins/maya/load/load_mayaascii.py index 07bf1ea836..dd86cc97b5 100644 --- a/colorbleed/plugins/maya/load/load_mayaascii.py +++ b/colorbleed/plugins/maya/load/load_mayaascii.py @@ -28,3 +28,6 @@ class MayaAsciiLoader(colorbleed.maya.plugin.ReferenceLoader): self[:] = nodes return nodes + + def switch(self, container, representation): + self.update(container, representation) diff --git a/colorbleed/plugins/maya/load/load_model.py b/colorbleed/plugins/maya/load/load_model.py index 9428ad40bd..eb76bd9ada 100644 --- a/colorbleed/plugins/maya/load/load_model.py +++ b/colorbleed/plugins/maya/load/load_model.py @@ -203,6 +203,9 @@ class GpuCacheLoader(api.Loader): str(representation["_id"]), type="string") + def switch(self, container, representation): + self.update(container, representation) + def remove(self, container): import maya.cmds as cmds members = cmds.sets(container['objectName'], query=True) diff --git a/colorbleed/plugins/maya/load/load_rig.py b/colorbleed/plugins/maya/load/load_rig.py index cb4b5f2f7c..1f3e83f60d 100644 --- a/colorbleed/plugins/maya/load/load_rig.py +++ b/colorbleed/plugins/maya/load/load_rig.py @@ -65,3 +65,6 @@ class RigLoader(colorbleed.maya.plugin.ReferenceLoader): family="colorbleed.animation", options={"useSelection": True}, data={"dependencies": dependency}) + + def switch(self, container, representation): + self.update(container, representation) diff --git a/colorbleed/plugins/maya/load/load_yeti_cache.py b/colorbleed/plugins/maya/load/load_yeti_cache.py index 0544868340..ec7ab11474 100644 --- a/colorbleed/plugins/maya/load/load_yeti_cache.py +++ b/colorbleed/plugins/maya/load/load_yeti_cache.py @@ -108,6 +108,9 @@ class YetiCacheLoader(api.Loader): str(representation["_id"]), type="string") + def switch(self, container, representation): + self.update(container, representation) + # helper functions def create_namespace(self, asset): From 2bfdaff5c4e7463779325e5380d051b15141037b Mon Sep 17 00:00:00 2001 From: wijnand Date: Mon, 5 Mar 2018 13:50:18 +0100 Subject: [PATCH 06/18] Added switch_item function to support switching --- colorbleed/lib.py | 67 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/colorbleed/lib.py b/colorbleed/lib.py index b57c9c4a83..686ff992a9 100644 --- a/colorbleed/lib.py +++ b/colorbleed/lib.py @@ -144,3 +144,70 @@ def version_up(filepath): log.info("New version %s" % new_label) return new_filename + + +def switch_item(container, + asset_name=None, + subset_name=None, + representation_name=None): + """Switch container asset, subset or representation of a container by name. + + It'll always switch to the latest version - of course a different + approach could be implemented. + + Args: + container (dict): data of the item to switch with + asset_name (str): name of the asset + subset_name (str): name of the subset + representation_name (str): name of the representation + + Returns: + None + + """ + + if all(not x for x in [asset_name, subset_name, representation_name]): + raise ValueError("Must have at least one change provided to switch.") + + # Collect any of current asset, subset and representation if not provided + # so we can use the original name from those. + if any(not x for x in [asset_name, subset_name, representation_name]): + _id = io.ObjectId(container["representation"]) + representation = io.find_one({"type": "representation", "_id": _id}) + version, subset, asset, project = io.parenthood(representation) + + if asset_name is None: + asset_name = asset["name"] + + if subset_name is None: + subset_name = subset["name"] + + if representation_name is None: + representation_name = representation["name"] + + # Find the new one + asset = io.find_one({"name": asset_name, "type": "asset"}) + assert asset, ("Could not find asset in the database with the name " + "'%s'" % asset_name) + + subset = io.find_one({"name": subset_name, + "type": "subset", + "parent": asset["_id"]}) + assert subset, ("Could not find subset in the database with the name " + "'%s'" % subset_name) + + version = io.find_one({"type": "version", + "parent": subset["_id"]}, + sort=[('name', -1)]) + + assert version, "Could not find a version for {}.{}".format( + asset_name, subset_name + ) + + representation = io.find_one({"name": representation_name, + "type": "representation", + "parent": version["_id"]}) + assert representation, ("Could not find subset in the database with the " + "name '%s'" % representation_name) + + avalon.api.switch(container, representation) From 85214e67daab338f9bc34c31695a8e0f1d1237a5 Mon Sep 17 00:00:00 2001 From: wijnand Date: Wed, 7 Mar 2018 09:39:23 +0100 Subject: [PATCH 07/18] Fixed assertion error message, return representation document --- colorbleed/lib.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/colorbleed/lib.py b/colorbleed/lib.py index 686ff992a9..9084410d18 100644 --- a/colorbleed/lib.py +++ b/colorbleed/lib.py @@ -207,7 +207,10 @@ def switch_item(container, representation = io.find_one({"name": representation_name, "type": "representation", "parent": version["_id"]}) - assert representation, ("Could not find subset in the database with the " - "name '%s'" % representation_name) + + assert representation, ("Could not find representation in the database with " + "the name '%s'" % representation_name) avalon.api.switch(container, representation) + + return representation From 7977b0f05d197c3eddbbebfc57510f18dff7076b Mon Sep 17 00:00:00 2001 From: wijnand Date: Thu, 8 Mar 2018 15:01:15 +0100 Subject: [PATCH 08/18] Create lib module, added update_frame_range function --- colorbleed/fusion/lib.py | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 colorbleed/fusion/lib.py diff --git a/colorbleed/fusion/lib.py b/colorbleed/fusion/lib.py new file mode 100644 index 0000000000..7186cd2cac --- /dev/null +++ b/colorbleed/fusion/lib.py @@ -0,0 +1,40 @@ +import sys + +import avalon.fusion + + +self = sys.modules[__name__] +self._project = None + + +def update_frame_range(start, end, comp=None, set_render_range=True): + """Set Fusion comp's start and end frame range + + Args: + start (float, int): start frame + end (float, int): end frame + comp (object, Optional): comp object from fusion + set_render_range (bool, Optional): When True this will also set the + composition's render start and end frame. + + Returns: + None + + """ + + if not comp: + comp = avalon.fusion.get_current_comp() + + attrs = { + "COMPN_GlobalStart": start, + "COMPN_GlobalEnd": end + } + + if set_render_range: + attrs.update({ + "COMPN_RenderStart": start, + "COMPN_RenderEnd": end + }) + + with avalon.fusion.comp_lock_and_undo_chunk(comp): + comp.SetAttrs(attrs) From 62696a2678144931ebf78f155527eb1c19b66f9f Mon Sep 17 00:00:00 2001 From: wijnand Date: Thu, 8 Mar 2018 15:02:53 +0100 Subject: [PATCH 09/18] Removed _set_frame_range, switched with lib.update_frame_range --- colorbleed/plugins/fusion/load/actions.py | 39 ++++------------------- 1 file changed, 6 insertions(+), 33 deletions(-) diff --git a/colorbleed/plugins/fusion/load/actions.py b/colorbleed/plugins/fusion/load/actions.py index 68f66775a8..c986229748 100644 --- a/colorbleed/plugins/fusion/load/actions.py +++ b/colorbleed/plugins/fusion/load/actions.py @@ -5,37 +5,6 @@ from avalon import api -def _set_frame_range(start, end, set_render_range=True): - """Set Fusion comp's start and end frame range - - Attrs: - set_render_range (bool, Optional): When True this will also set the - composition's render start and end frame. - - Returns: - None - - """ - - from avalon.fusion import get_current_comp, comp_lock_and_undo_chunk - - comp = get_current_comp() - - attrs = { - "COMPN_GlobalStart": start, - "COMPN_GlobalEnd": end - } - - if set_render_range: - attrs.update({ - "COMPN_RenderStart": start, - "COMPN_RenderEnd": end - }) - - with comp_lock_and_undo_chunk(comp): - comp.SetAttrs(attrs) - - class FusionSetFrameRangeLoader(api.Loader): """Specific loader of Alembic for the avalon.animation family""" @@ -53,6 +22,8 @@ class FusionSetFrameRangeLoader(api.Loader): def load(self, context, name, namespace, data): + from colorbleed.fusion import lib + version = context['version'] version_data = version.get("data", {}) @@ -64,7 +35,7 @@ class FusionSetFrameRangeLoader(api.Loader): "end frame data is missing..") return - _set_frame_range(start, end) + lib.update_frame_range(start, end) class FusionSetFrameRangeWithHandlesLoader(api.Loader): @@ -84,6 +55,8 @@ class FusionSetFrameRangeWithHandlesLoader(api.Loader): def load(self, context, name, namespace, data): + from colorbleed.fusion import lib + version = context['version'] version_data = version.get("data", {}) @@ -100,4 +73,4 @@ class FusionSetFrameRangeWithHandlesLoader(api.Loader): start -= handles end += handles - _set_frame_range(start, end) + lib.update_frame_range(start, end) From 81b2768bd29452a2fd436a988248f1aae89bc17e Mon Sep 17 00:00:00 2001 From: wijnand Date: Thu, 8 Mar 2018 15:03:41 +0100 Subject: [PATCH 10/18] Corrected return type in docstrings --- colorbleed/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/colorbleed/lib.py b/colorbleed/lib.py index 9084410d18..14ad6884b5 100644 --- a/colorbleed/lib.py +++ b/colorbleed/lib.py @@ -162,7 +162,7 @@ def switch_item(container, representation_name (str): name of the representation Returns: - None + dict """ From 02b24d3ce8a3e8741b945f0073e2427985d65792 Mon Sep 17 00:00:00 2001 From: wijnand Date: Thu, 8 Mar 2018 18:09:08 +0100 Subject: [PATCH 11/18] Scrip to automate shot switching --- colorbleed/scripts/fusion_switch_shot.py | 215 +++++++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 colorbleed/scripts/fusion_switch_shot.py diff --git a/colorbleed/scripts/fusion_switch_shot.py b/colorbleed/scripts/fusion_switch_shot.py new file mode 100644 index 0000000000..6119862b42 --- /dev/null +++ b/colorbleed/scripts/fusion_switch_shot.py @@ -0,0 +1,215 @@ +import os +import sys +import logging + +# Pipeline imports +from avalon import api, io, pipeline +import avalon.fusion + +# Config imports +import colorbleed.lib as colorbleed +import colorbleed.fusion.lib as fusion_lib + +log = logging.getLogger("Update Slap Comp") + +self = sys.modules[__name__] +self._project = None +fusion = None + + +def _get_work_folder(session): + """Convenience function to get the work folder path of the current asset""" + + # Get new filename, create path based on asset and work template + template_work = self._project["config"]["template"]["work"] + work_path = pipeline._format_work_template(template_work, session) + + return os.path.normpath(work_path) + + +def _get_fusion_instance(): + fusion = getattr(sys.modules["__main__"], "fusion", None) + if fusion is None: + try: + # Support for FuScript.exe, BlackmagicFusion module for py2 only + import BlackmagicFusion as bmf + fusion = bmf.scriptapp("Fusion") + except ImportError: + raise RuntimeError("Could not find a Fusion instance") + return fusion + + +def _format_filepath(session): + + project = session["AVALON_PROJECT"] + asset = session["AVALON_ASSET"] + + # Save updated slap comp + work_path = _get_work_folder(session) + walk_to_dir = os.path.join(work_path, "scenes", "slapcomp") + slapcomp_dir = os.path.abspath(walk_to_dir) + + # Ensure destination exists + if not os.path.isdir(slapcomp_dir): + log.warning("Folder did not exist, creating folder structure") + os.makedirs(slapcomp_dir) + + # Compute output path + new_filename = "{}_{}_slapcomp_v001.comp".format(project, asset) + new_filepath = os.path.join(slapcomp_dir, new_filename) + + # Create new unqiue filepath + if os.path.exists(new_filepath): + new_filepath = colorbleed.version_up(new_filepath) + + return new_filepath + + +def _update_savers(comp, session): + """Update all savers of the current comp to ensure the output is correct + + Args: + comp (object): current comp instance + asset (dict): asset document of the asset to update TO + + Returns: + None + """ + + new_work = _get_work_folder(session) + # TODO + renders = os.path.join(new_work, "renders", "?") + + comp.Print("New renders to: %s\n" % renders) + + with avalon.fusion.comp_lock_and_undo_chunk(comp): + savers = comp.GetToolList(False, "Saver").values() + for saver in savers: + filepath = saver.GetAttrs("TOOLST_Clip_Name")[1.0] + filename = os.path.basename(filepath) + new_path = os.path.join(renders, filename) + saver["Clip"] = new_path + + +def update_frame_range(comp, representations): + """Update the frame range of the comp and render length + + The start and end frame are based on the lowest start frame and the highest + end frame + + Args: + comp (object): current focused comp + representations (list) collection of dicts + + Returns: + None + + """ + + version_ids = [r["parent"] for r in representations] + versions = io.find({"type": "version", "_id": {"$in": version_ids}}) + versions = list(versions) + + start = min(v["data"]["startFrame"] for v in versions) + end = max(v["data"]["endFrame"] for v in versions) + + fusion_lib.update_frame_range(start, end, comp=comp) + + +def switch(filepath, asset_name, new=True, fusion=None): + """Switch the current containers of the file to the other asset (shot) + + Args: + filepath (str): file path of the comp file + asset_name (str): name of the asset (shot) + new (bool): Save updated comp under a different name + fusion (object, Optional): pass on the fusion instance + + Returns: + comp path (str): new filepath of the updated comp + + """ + + # Ensure filename is absolute + if not os.path.abspath(filepath): + filepath = os.path.abspath(filepath) + + # Get current project + self._project = io.find_one({"type": "project", + "name": api.Session["AVALON_PROJECT"]}) + + # Assert asset name exists + # It is better to do this here then to wait till switch_shot does it + asset = io.find_one({"type": "asset", "name": asset_name}) + assert asset, "Could not find '%s' in the database" % asset_name + + # Go to comp + if fusion is None: + fusion = _get_fusion_instance() + + current_comp = fusion.LoadComp(filepath) + assert current_comp is not None, "Fusion could not load '%s'" % filepath + + host = api.registered_host() + assert host is not None, "No host found! This is a bug" + containers = list(host.ls()) + assert containers, "Nothing to update" + + representations = [] + for container in containers: + try: + representation = colorbleed.switch_item(container, + asset_name=asset_name) + representations.append(representation) + current_comp.Print(str(representation["_id"]) + "\n") + except Exception as e: + current_comp.Print("Error in switching! %s\n" % e.message) + + message = "Switched %i Loaders of the %i\n" % (len(representations), + len(containers)) + current_comp.Print(message) + + # Build the session to switch to + switch_to_session = api.Session.copy() + switch_to_session["AVALON_ASSET"] = asset['name'] + + if new: + comp_path = _format_filepath(switch_to_session) + + # Update savers output based on new session + _update_savers(current_comp, switch_to_session) + else: + comp_path = colorbleed.version_up(filepath) + + current_comp.Print(comp_path) + + current_comp.Print("\nUpdating frame range") + update_frame_range(current_comp, representations) + + current_comp.Save(comp_path) + + return comp_path + + +if __name__ == '__main__': + + import argparse + + parser = argparse.ArgumentParser(description="Switch to a shot within an" + "existing comp file") + + parser.add_argument("--file_path", + type=str, + default=True, + help="File path of the comp to use") + parser.add_argument("--asset_name", + type=str, + default=True, + help="Name of the asset (shot) to switch") + + args, unknown = parser.parse_args() + + api.install(avalon.fusion) + switch(args.file_path, args.asset_name) + + sys.exit(0) From 73f2e404d0f556877400061443315e2675abaec6 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 9 Mar 2018 16:02:58 +0100 Subject: [PATCH 12/18] Fix MOD-6: Invalid shader connections brings along shader with model publish --- .../validate_mesh_shader_connections.py | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 colorbleed/plugins/maya/publish/validate_mesh_shader_connections.py diff --git a/colorbleed/plugins/maya/publish/validate_mesh_shader_connections.py b/colorbleed/plugins/maya/publish/validate_mesh_shader_connections.py new file mode 100644 index 0000000000..5f44bf1f9f --- /dev/null +++ b/colorbleed/plugins/maya/publish/validate_mesh_shader_connections.py @@ -0,0 +1,111 @@ +from maya import cmds + +import pyblish.api +import colorbleed.api + + +def pairs(iterable): + """Iterate over iterable per group of two""" + a = iter(iterable) + for i, y in zip(a, a): + yield i, y + + +def get_invalid_sets(shape): + """Get sets that are considered related but do not contain the shape. + + In some scenarios Maya keeps connections to multiple shaders + even if just a single one is assigned on the full object. + + These are related sets returned by `maya.cmds.listSets` that don't + actually have the shape as member. + + """ + + invalid = [] + sets = cmds.listSets(object=shape, t=1, extendToShape=False) + for s in sets: + members = cmds.sets(s, query=True, nodesOnly=True) + if not members: + invalid.append(s) + continue + + members = set(cmds.ls(members, long=True)) + if shape not in members: + invalid.append(s) + + return invalid + + +def disconnect(node_a, node_b): + """Remove all connections between node a and b.""" + + # Disconnect outputs + outputs = cmds.listConnections(node_a, + plugs=True, + connections=True, + source=False, + destination=True) + for output, destination in pairs(outputs): + if destination.split(".", 1)[0] == node_b: + cmds.disconnectAttr(output, destination) + + # Disconnect inputs + inputs = cmds.listConnections(node_a, + plugs=True, + connections=True, + source=True, + destination=False) + for input, source in pairs(inputs): + if source.split(".", 1)[0] == node_b: + cmds.disconnectAttr(source, input) + + +class ValidateMeshShaderConnections(pyblish.api.InstancePlugin): + """Ensure mesh shading engine connections are valid. + + In some scenarios Maya keeps connections to multiple shaders even if just + a single one is assigned on the shape. + + These are related sets returned by `maya.cmds.listSets` that don't + actually have the shape as member. + + """ + + order = colorbleed.api.ValidateMeshOrder + hosts = ['maya'] + families = ['colorbleed.model'] + label = "Mesh Shader Connections" + actions = [colorbleed.api.SelectInvalidAction, + colorbleed.api.RepairAction] + + def process(self, instance): + """Process all the nodes in the instance 'objectSet'""" + + invalid = self.get_invalid(instance) + + if invalid: + raise RuntimeError("Shapes found with invalid shader " + "connections: {0}".format(invalid)) + + @staticmethod + def get_invalid(instance): + + shapes = cmds.ls(instance[:], dag=1, leaf=1, shapes=1, long=True) + shapes = cmds.ls(shapes, shapes=True, noIntermediate=True, long=True) + + invalid = [] + for shape in shapes: + if get_invalid_sets(shape): + invalid.append(shape) + + return invalid + + @classmethod + def repair(cls, instance): + + shapes = cls.get_invalid(instance) + for shape in shapes: + invalid_sets = get_invalid_sets(shape) + for set_node in invalid_sets: + disconnect(shape, set_node) From 4d975aab49c2ecc73d447186129de7f9cc553e6e Mon Sep 17 00:00:00 2001 From: wijnand Date: Fri, 9 Mar 2018 16:55:18 +0100 Subject: [PATCH 13/18] Adds simple switch UI --- .../scripts/Comp/colorbleed/switch_ui.py | 213 ++++++++++++++++++ 1 file changed, 213 insertions(+) create mode 100644 setup/fusion/scripts/Comp/colorbleed/switch_ui.py diff --git a/setup/fusion/scripts/Comp/colorbleed/switch_ui.py b/setup/fusion/scripts/Comp/colorbleed/switch_ui.py new file mode 100644 index 0000000000..1b0d4a1b82 --- /dev/null +++ b/setup/fusion/scripts/Comp/colorbleed/switch_ui.py @@ -0,0 +1,213 @@ +import os +import glob +import logging + +import avalon.io as io +import avalon.api as api +import avalon.pipeline as pipeline +import avalon.fusion +import avalon.style as style +from avalon.vendor.Qt import QtWidgets, QtCore +from avalon.vendor import qtawesome as qta + + +log = logging.getLogger("Fusion Switch Shot") + + +class App(QtWidgets.QWidget): + + def __init__(self, parent=None): + + ################################################ + # |---------------------| |------------------| # + # |Comp | |Asset | # + # |[..][ v]| |[ v]| # + # |---------------------| |------------------| # + # | Update existing comp [ ] | # + # |------------------------------------------| # + # | Switch | # + # |------------------------------------------| # + ################################################ + + QtWidgets.QWidget.__init__(self, parent) + + layout = QtWidgets.QVBoxLayout() + + # Comp related input + comp_hlayout = QtWidgets.QHBoxLayout() + comp_label = QtWidgets.QLabel("Comp file") + comp_label.setFixedWidth(50) + comp_box = QtWidgets.QComboBox() + + button_icon = qta.icon("fa.folder", color="white") + open_from_dir = QtWidgets.QPushButton() + open_from_dir.setIcon(button_icon) + + comp_box.setFixedHeight(25) + open_from_dir.setFixedWidth(25) + open_from_dir.setFixedHeight(25) + + comp_hlayout.addWidget(comp_label) + comp_hlayout.addWidget(comp_box) + comp_hlayout.addWidget(open_from_dir) + + # Asset related input + asset_hlayout = QtWidgets.QHBoxLayout() + asset_label = QtWidgets.QLabel("Shot") + asset_label.setFixedWidth(50) + + asset_box = QtWidgets.QComboBox() + asset_box.setLineEdit(QtWidgets.QLineEdit()) + asset_box.setFixedHeight(25) + + refresh_icon = qta.icon("fa.refresh", color="white") + refresh_btn = QtWidgets.QPushButton() + refresh_btn.setIcon(refresh_icon) + + asset_box.setFixedHeight(25) + refresh_btn.setFixedWidth(25) + refresh_btn.setFixedHeight(25) + + asset_hlayout.addWidget(asset_label) + asset_hlayout.addWidget(asset_box) + asset_hlayout.addWidget(refresh_btn) + + # Options + options = QtWidgets.QHBoxLayout() + options.setAlignment(QtCore.Qt.AlignLeft) + + update_label = QtWidgets.QLabel("Update version") + update_check = QtWidgets.QCheckBox() + update_check.setChecked(False) + update_check.setToolTip("If checked it versions up the selected comp " + "file with else it will create a new slapcomp " + "file based on the selected comp file") + + current_comp_check = QtWidgets.QCheckBox() + current_comp_check.setChecked(True) + current_comp_label = QtWidgets.QLabel("Use current comp") + + options.addWidget(update_label) + options.addWidget(update_check) + + options.addWidget(current_comp_label) + options.addWidget(current_comp_check) + + accept_btn = QtWidgets.QPushButton("Switch") + + layout.addLayout(options) + layout.addLayout(comp_hlayout) + layout.addLayout(asset_hlayout) + layout.addWidget(accept_btn) + + self._open_from_dir = open_from_dir + self._comps = comp_box + self._assets = asset_box + self._update = update_check + self._use_current = current_comp_check + self._accept_btn = accept_btn + self._refresh_btn = refresh_btn + + self.setWindowTitle("Fusion Switch Shot") + self.setLayout(layout) + + self.resize(260, 140) + self.setMinimumWidth(260) + self.setFixedHeight(140) + + self.connections() + + # Update ui to correct state + self._on_use_current_comp() + self._refresh() + + def connections(self): + self._use_current.clicked.connect(self._on_use_current_comp) + self._open_from_dir.clicked.connect(self._on_open_from_dir) + self._refresh_btn.clicked.connect(self._refresh) + self._accept_btn.clicked.connect(self._on_switch) + + def _on_use_current_comp(self): + state = self._use_current.isChecked() + self._open_from_dir.setEnabled(not state) + self._comps.setEnabled(not state) + + def _on_open_from_dir(self): + + start_dir = self._get_context_directory() + comp_file, _ = QtWidgets.QFileDialog.getOpenFileName( + self, "Choose comp", start_dir) + + if not comp_file: + return + + # Create completer + self.populate_comp_box([comp_file]) + self._refresh() + + def _refresh(self): + # Clear any existing items + self._assets.clear() + + asset_names = [a["name"] for a in self.collect_assets()] + completer = QtWidgets.QCompleter(asset_names) + + self._assets.setCompleter(completer) + self._assets.addItems(asset_names) + + def _on_switch(self): + + if not self._use_current.isChecked(): + file_name = self._comps.itemData(self._comps.currentIndex()) + else: + comp = avalon.fusion.get_current_comp() + file_name = comp.GetAttrs("COMPS_FileName") + + asset = self._assets.currentText() + new = not self._update.isChecked() + + import colorbleed.scripts.fusion_switch_shot as switch_shot + switch_shot.switch(file_name, asset, new) + + def _get_context_directory(self): + + project = io.find_one({"type": "project", + "name": api.Session["AVALON_PROJECT"]}, + projection={"config": True}) + + template = project["config"]["template"]["work"] + dir = pipeline._format_work_template(template, api.Session) + + return dir + + def collect_slap_comps(self, directory): + items = glob.glob("{}/*.comp".format(directory)) + return items + + def collect_assets(self): + return list(io.find({"type": "asset", "silo": "film"})) + + def populate_comp_box(self, files): + """Ensure we display the filename only but the path is stored as well + + Args: + files (list): list of full file path [path/to/item/item.ext,] + + Returns: + None + """ + + for f in files: + filename = os.path.basename(f) + self._comps.addItem(filename, userData=f) + + +if __name__ == '__main__': + import sys + api.install(avalon.fusion) + + app = QtWidgets.QApplication(sys.argv) + window = App() + window.setStyleSheet(style.load_stylesheet()) + window.show() + sys.exit(app.exec_()) From 16bb134a7f4fc88827190125a1250727de65481d Mon Sep 17 00:00:00 2001 From: wijnand Date: Fri, 9 Mar 2018 16:56:55 +0100 Subject: [PATCH 14/18] renamed module, removed unused kwarg --- colorbleed/scripts/fusion_switch_shot.py | 6 +- colorbleed/scripts/slapcomp_switch.py | 87 ------------------------ 2 files changed, 2 insertions(+), 91 deletions(-) delete mode 100644 colorbleed/scripts/slapcomp_switch.py diff --git a/colorbleed/scripts/fusion_switch_shot.py b/colorbleed/scripts/fusion_switch_shot.py index 6119862b42..43ad149ca5 100644 --- a/colorbleed/scripts/fusion_switch_shot.py +++ b/colorbleed/scripts/fusion_switch_shot.py @@ -116,7 +116,7 @@ def update_frame_range(comp, representations): fusion_lib.update_frame_range(start, end, comp=comp) -def switch(filepath, asset_name, new=True, fusion=None): +def switch(filepath, asset_name, new=True): """Switch the current containers of the file to the other asset (shot) Args: @@ -144,14 +144,12 @@ def switch(filepath, asset_name, new=True, fusion=None): assert asset, "Could not find '%s' in the database" % asset_name # Go to comp - if fusion is None: - fusion = _get_fusion_instance() + fusion = _get_fusion_instance() current_comp = fusion.LoadComp(filepath) assert current_comp is not None, "Fusion could not load '%s'" % filepath host = api.registered_host() - assert host is not None, "No host found! This is a bug" containers = list(host.ls()) assert containers, "Nothing to update" diff --git a/colorbleed/scripts/slapcomp_switch.py b/colorbleed/scripts/slapcomp_switch.py deleted file mode 100644 index 95270259aa..0000000000 --- a/colorbleed/scripts/slapcomp_switch.py +++ /dev/null @@ -1,87 +0,0 @@ -import logging -import argparse - -import avalon.io as io -import avalon.api as api -import avalon.fusion - - -log = logging.getLogger("UpdateSlapComp") - - -def switch(shot): - """Update comp loaders through the containers - - Args: - data (dict): collection if {asset: [subset, subset]} - """ - - host = api.registered_host() - containers = list(host.ls()) - - targets = [c["name"] for c in containers] - - representations = {} - - asset = io.find_one({"type": "asset", "name": shot}) - assert asset, ("Could not find asset in the database with the name " - "'%s'" % shot) - - subsets = io.find({"type": "subset", - "name": {"$in": targets}, - "parent": asset["_id"]}) - - if len(subsets) != len(targets): - log.warning("Could not find the same amount of subsets in '%s'" % shot) - - for subset in subsets: - version = io.find_one({"type": "version", - "parent": subset["_id"]}, - sort=[('name', -1)]) - - if not version: - log.error("Could not find a verison for {}.{}".format( - asset["name"], subset["name"] - )) - continue - - representation = io.find_one({"type": "representation", - "parent": version["_id"]}) - - representations[subset["name"]] = representation - - count = 0 - for i, container in enumerate(containers): - subsetname = container["name"] - if subsetname not in representations: - log.error("Subset '%s' not found in comp, skipping" % subsetname) - continue - - representation = representations[container["name"]] - api.switch(container, representation) - count += 1 - - log.info("Updated %i items" % count) - - -if __name__ == '__main__': - - parser = argparse.ArgumentParser() - - parser.add_argument("--shot", - help="Shotname to update the current comp to") - - parser.add_argument("--targets", - optional=True, - help="A list of target to update with given data") - - args = parser.parse_known_args() - if not args.shot: - raise RuntimeError("No data given, cannot update slap comp") - - if not isinstance(args.shot, dict): - raise ValueError("Expecting data in the form of a dict, got " - "%s" % type(args.shot)) - - api.install(avalon.fusion) - switch(shot=args.shot) From ade5b04091b20d27a5f0f35a8a9ccee7f5d1096f Mon Sep 17 00:00:00 2001 From: wijnand Date: Mon, 12 Mar 2018 11:57:10 +0100 Subject: [PATCH 15/18] Logic to format version folder --- colorbleed/scripts/fusion_switch_shot.py | 39 ++++++++++++++++++++---- 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/colorbleed/scripts/fusion_switch_shot.py b/colorbleed/scripts/fusion_switch_shot.py index 43ad149ca5..4c215c65c0 100644 --- a/colorbleed/scripts/fusion_switch_shot.py +++ b/colorbleed/scripts/fusion_switch_shot.py @@ -1,4 +1,5 @@ import os +import re import sys import logging @@ -14,7 +15,33 @@ log = logging.getLogger("Update Slap Comp") self = sys.modules[__name__] self._project = None -fusion = None + + +def _format_version_folder(folder): + """Format a version folder based on the filepath + + Assumption here is made that, if the path does not exists the folder + will be "v001" + + Args: + folder: file path to a folder + + Returns: + str: new version folder name + """ + + if not os.path.isdir(folder): + return "v001" + + re_version = re.compile("v\d+") + + content = sorted(os.listdir(folder)) + versions = [i for i in content if re_version.match(i)] + + new_version = int(max(versions)[1:]) + 1 # ensure the "v" is not included + version_folder = "v{:03d}".format(new_version) + + return version_folder def _get_work_folder(session): @@ -70,15 +97,16 @@ def _update_savers(comp, session): Args: comp (object): current comp instance - asset (dict): asset document of the asset to update TO + session (dict): the current Avalon session Returns: None """ new_work = _get_work_folder(session) - # TODO - renders = os.path.join(new_work, "renders", "?") + renders = os.path.join(new_work, "renders") + version_folder = _format_version_folder(renders) + renders_version = os.path.join(renders, version_folder) comp.Print("New renders to: %s\n" % renders) @@ -87,7 +115,7 @@ def _update_savers(comp, session): for saver in savers: filepath = saver.GetAttrs("TOOLST_Clip_Name")[1.0] filename = os.path.basename(filepath) - new_path = os.path.join(renders, filename) + new_path = os.path.join(renders_version, filename) saver["Clip"] = new_path @@ -123,7 +151,6 @@ def switch(filepath, asset_name, new=True): filepath (str): file path of the comp file asset_name (str): name of the asset (shot) new (bool): Save updated comp under a different name - fusion (object, Optional): pass on the fusion instance Returns: comp path (str): new filepath of the updated comp From 8c359655c1d5ed878826ddf20e432123a038bfcf Mon Sep 17 00:00:00 2001 From: wijnand Date: Mon, 12 Mar 2018 12:06:11 +0100 Subject: [PATCH 16/18] Ensure only dirs are filtered --- colorbleed/scripts/fusion_switch_shot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/colorbleed/scripts/fusion_switch_shot.py b/colorbleed/scripts/fusion_switch_shot.py index 4c215c65c0..928be6309a 100644 --- a/colorbleed/scripts/fusion_switch_shot.py +++ b/colorbleed/scripts/fusion_switch_shot.py @@ -36,7 +36,7 @@ def _format_version_folder(folder): re_version = re.compile("v\d+") content = sorted(os.listdir(folder)) - versions = [i for i in content if re_version.match(i)] + versions = [i for i in content if os.path.isdir(i) and re_version.match(i)] new_version = int(max(versions)[1:]) + 1 # ensure the "v" is not included version_folder = "v{:03d}".format(new_version) From bf2df9a4500e62ea5cab2ac9215c1af00b9a613c Mon Sep 17 00:00:00 2001 From: wijnand Date: Mon, 12 Mar 2018 15:18:27 +0100 Subject: [PATCH 17/18] Explicitly get "v###", exclude any mutations "v###_a" --- colorbleed/scripts/fusion_switch_shot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/colorbleed/scripts/fusion_switch_shot.py b/colorbleed/scripts/fusion_switch_shot.py index 928be6309a..ca5b859f73 100644 --- a/colorbleed/scripts/fusion_switch_shot.py +++ b/colorbleed/scripts/fusion_switch_shot.py @@ -33,7 +33,7 @@ def _format_version_folder(folder): if not os.path.isdir(folder): return "v001" - re_version = re.compile("v\d+") + re_version = re.compile("v\d+$") content = sorted(os.listdir(folder)) versions = [i for i in content if os.path.isdir(i) and re_version.match(i)] From 4c90aeda3a27f609485b5a362c1d11266bef3640 Mon Sep 17 00:00:00 2001 From: wijnand Date: Mon, 12 Mar 2018 15:29:15 +0100 Subject: [PATCH 18/18] improved _format_version_folder --- colorbleed/scripts/fusion_switch_shot.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/colorbleed/scripts/fusion_switch_shot.py b/colorbleed/scripts/fusion_switch_shot.py index ca5b859f73..1a7e6bfed6 100644 --- a/colorbleed/scripts/fusion_switch_shot.py +++ b/colorbleed/scripts/fusion_switch_shot.py @@ -34,9 +34,8 @@ def _format_version_folder(folder): return "v001" re_version = re.compile("v\d+$") - - content = sorted(os.listdir(folder)) - versions = [i for i in content if os.path.isdir(i) and re_version.match(i)] + versions = [i for i in os.listdir(folder) if os.path.isdir(i) + and re_version.match(i)] new_version = int(max(versions)[1:]) + 1 # ensure the "v" is not included version_folder = "v{:03d}".format(new_version)