From 9073feb469c729cd1986f0aab3abc8b9fada3852 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Fri, 2 Aug 2019 10:49:25 +0100 Subject: [PATCH 01/23] Repair legacy write nodes. Grabs the existing data from the legacy write nodes, and creates a new write from it. --- .../nuke/publish/collect_legacy_write.py | 25 ++++++++ .../nuke/publish/validate_write_legacy.py | 62 +++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 pype/plugins/nuke/publish/collect_legacy_write.py create mode 100644 pype/plugins/nuke/publish/validate_write_legacy.py diff --git a/pype/plugins/nuke/publish/collect_legacy_write.py b/pype/plugins/nuke/publish/collect_legacy_write.py new file mode 100644 index 0000000000..05dbe4216c --- /dev/null +++ b/pype/plugins/nuke/publish/collect_legacy_write.py @@ -0,0 +1,25 @@ +import nuke + +import pyblish.api + + +class CollectWriteLegacy(pyblish.api.ContextPlugin): + """Collect legacy write nodes.""" + + order = pyblish.api.CollectorOrder + label = "Collect Write Legacy" + hosts = ["nuke", "nukeassist"] + + def process(self, context): + + for node in nuke.allNodes(): + if node.Class() != "Write": + continue + + if "avalon" not in node.knobs().keys(): + continue + + instance = context.create_instance( + node.name(), family="write.legacy" + ) + instance.append(node) diff --git a/pype/plugins/nuke/publish/validate_write_legacy.py b/pype/plugins/nuke/publish/validate_write_legacy.py new file mode 100644 index 0000000000..b452e60ba4 --- /dev/null +++ b/pype/plugins/nuke/publish/validate_write_legacy.py @@ -0,0 +1,62 @@ +import toml +import os + +import nuke + +from avalon import api +import pyblish.api + + +class RepairWriteLegacyAction(pyblish.api.Action): + + label = "Repair" + icon = "wrench" + on = "failed" + + def process(self, context, plugin): + + # Get the errored instances + failed = [] + for result in context.data["results"]: + if (result["error"] is not None and result["instance"] is not None + and result["instance"] not in failed): + failed.append(result["instance"]) + + # Apply pyblish.logic to get the instances for the plug-in + instances = pyblish.api.instances_by_plugin(failed, plugin) + + for instance in instances: + data = toml.loads(instance[0]["avalon"].value()) + data["xpos"] = instance[0].xpos() + data["ypos"] = instance[0].ypos() + data["input"] = instance[0].input(0) + data["publish"] = instance[0]["publish"].value() + data["render"] = instance[0]["render"].value() + data["render_farm"] = instance[0]["render_farm"].value() + + nuke.delete(instance[0]) + + family = "render{}".format(os.environ["AVALON_TASK"].capitalize()) + api.create(data["subset"], data["asset"], family) + node = nuke.toNode(data["subset"]) + node.setXYpos(data["xpos"], data["ypos"]) + node.setInput(0, data["input"]) + node["publish"].setValue(data["publish"]) + node["render"].setValue(data["render"]) + node["render_farm"].setValue(data["render_farm"]) + + +class ValidateWriteLegacy(pyblish.api.InstancePlugin): + """Validate legacy write nodes.""" + + order = pyblish.api.ValidatorOrder + optional = True + families = ["write.legacy"] + label = "Write Legacy" + hosts = ["nuke"] + actions = [RepairWriteLegacyAction] + + def process(self, instance): + + msg = "Clean up legacy write node \"{}\"".format(instance) + assert False, msg From e108281c98c5bdc47d86c8ae9344e201e8ac13b8 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 10 Sep 2019 09:53:14 +0200 Subject: [PATCH 02/23] fix(nks): little problems on collectors --- pype/plugins/nukestudio/publish/collect_effects.py | 6 +++++- pype/plugins/nukestudio/publish/collect_plates.py | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/pype/plugins/nukestudio/publish/collect_effects.py b/pype/plugins/nukestudio/publish/collect_effects.py index 11693ab1fe..ba8920236f 100644 --- a/pype/plugins/nukestudio/publish/collect_effects.py +++ b/pype/plugins/nukestudio/publish/collect_effects.py @@ -14,7 +14,11 @@ class CollectVideoTracksLuts(pyblish.api.InstancePlugin): self.log.debug("Finding soft effect for subset: `{}`".format(instance.data.get("subset"))) # taking active sequence - subset = instance.data["subset"] + subset = instance.data.get("subset") + + if not subset: + return + track_effects = instance.context.data.get("trackEffects", {}) track_index = instance.data["trackIndex"] effects = instance.data["effects"] diff --git a/pype/plugins/nukestudio/publish/collect_plates.py b/pype/plugins/nukestudio/publish/collect_plates.py index 7f6f4138cb..2ebbfde551 100644 --- a/pype/plugins/nukestudio/publish/collect_plates.py +++ b/pype/plugins/nukestudio/publish/collect_plates.py @@ -175,6 +175,8 @@ class CollectPlatesData(api.InstancePlugin): if os.path.exists(mov_path): # adding mov into the representations self.log.debug("__ mov_path: {}".format(mov_path)) + instance.data["label"] += " - review" + plates_mov_representation = { 'files': mov_file, 'stagingDir': staging_dir, From 3f1a05536813824e132355947a347b5d6a070354 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 11 Sep 2019 17:52:54 +0200 Subject: [PATCH 03/23] fixed icon name from "boxes" to "cubes" in 3 creator plugins --- pype/plugins/maya/create/create_assembly.py | 2 +- pype/plugins/maya/create/create_layout.py | 2 +- pype/plugins/maya/create/create_setdress.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pype/plugins/maya/create/create_assembly.py b/pype/plugins/maya/create/create_assembly.py index 2a00d4a29a..6d0321b718 100644 --- a/pype/plugins/maya/create/create_assembly.py +++ b/pype/plugins/maya/create/create_assembly.py @@ -7,5 +7,5 @@ class CreateAssembly(avalon.maya.Creator): name = "assembly" label = "Assembly" family = "assembly" - icon = "boxes" + icon = "cubes" defaults = ['Main'] diff --git a/pype/plugins/maya/create/create_layout.py b/pype/plugins/maya/create/create_layout.py index 3f6dd5d769..7f0c82d80e 100644 --- a/pype/plugins/maya/create/create_layout.py +++ b/pype/plugins/maya/create/create_layout.py @@ -7,5 +7,5 @@ class CreateLayout(avalon.maya.Creator): name = "layoutMain" label = "Layout" family = "layout" - icon = "boxes" + icon = "cubes" defaults = ["Main"] diff --git a/pype/plugins/maya/create/create_setdress.py b/pype/plugins/maya/create/create_setdress.py index 079ccbd029..d5fc001299 100644 --- a/pype/plugins/maya/create/create_setdress.py +++ b/pype/plugins/maya/create/create_setdress.py @@ -7,5 +7,5 @@ class CreateSetDress(avalon.maya.Creator): name = "setdressMain" label = "Set Dress" family = "setdress" - icon = "boxes" + icon = "cubes" defaults = ["Main", "Anim"] From 8e3969c26cf220a28e5cdce1fb4a7ed175e3e44a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 11 Sep 2019 19:02:16 +0200 Subject: [PATCH 04/23] fix(nks): adding ability to transfer animated transform soft effect --- pype/plugins/nukestudio/publish/collect_effects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/nukestudio/publish/collect_effects.py b/pype/plugins/nukestudio/publish/collect_effects.py index ba8920236f..0aee0adf2e 100644 --- a/pype/plugins/nukestudio/publish/collect_effects.py +++ b/pype/plugins/nukestudio/publish/collect_effects.py @@ -78,7 +78,7 @@ class CollectVideoTracksLuts(pyblish.api.InstancePlugin): 'channels', 'maskChannelMask', 'maskChannelInput', 'note_font', 'note_font_size', 'unpremult', 'postage_stamp_frame', 'maskChannel', 'export_cc', - 'select_cccid', 'mix', 'version'] + 'select_cccid', 'mix', 'version', 'matrix'] # loop trough all knobs and collect not ignored # and any with any value From 581ba78aff2061b81664f963fdfed003cd714512 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 12 Sep 2019 11:13:04 +0200 Subject: [PATCH 05/23] return original plugins if registered host is not set --- pype/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pype/__init__.py b/pype/__init__.py index bcbedc9a90..c2311dd528 100644 --- a/pype/__init__.py +++ b/pype/__init__.py @@ -35,6 +35,8 @@ def patched_discover(superclass): plugins = _original_discover(superclass) # determine host application to use for finding presets + if avalon.registered_host() is None: + return plugins host = avalon.registered_host().__name__.split(".")[-1] # map plugin superclass to preset json. Currenly suppoted is load and From 497ae5265c6b655f3743b7eaaef8e91fd1ec4f01 Mon Sep 17 00:00:00 2001 From: Jana Mizikova Date: Fri, 13 Sep 2019 09:46:25 +0200 Subject: [PATCH 06/23] fix(ftrack): custome attribute doctor was not respecting attributes after change of names --- pype/ftrack/actions/action_cust_attr_doctor.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pype/ftrack/actions/action_cust_attr_doctor.py b/pype/ftrack/actions/action_cust_attr_doctor.py index 1b8f250e5b..b875f52ab8 100644 --- a/pype/ftrack/actions/action_cust_attr_doctor.py +++ b/pype/ftrack/actions/action_cust_attr_doctor.py @@ -23,10 +23,10 @@ class CustomAttributeDoctor(BaseAction): icon = '{}/ftrack/action_icons/PypeDoctor.svg'.format( os.environ.get('PYPE_STATICS_SERVER', '') ) - hierarchical_ca = ['handle_start', 'handle_end', 'fstart', 'fend'] + hierarchical_ca = ['handleStart', 'handleEnd', 'frameStart', 'frameEnd'] hierarchical_alternatives = { - 'handle_start': 'handles', - 'handle_end': 'handles' + 'handleStart': 'handles', + 'handleEnd': 'handles' } # Roles for new custom attributes @@ -34,22 +34,22 @@ class CustomAttributeDoctor(BaseAction): write_roles = ['ALL',] data_ca = { - 'handle_start': { + 'handleStart': { 'label': 'Frame handles start', 'type': 'number', 'config': json.dumps({'isdecimal': False}) }, - 'handle_end': { + 'handleEnd': { 'label': 'Frame handles end', 'type': 'number', 'config': json.dumps({'isdecimal': False}) }, - 'fstart': { + 'frameStart': { 'label': 'Frame start', 'type': 'number', 'config': json.dumps({'isdecimal': False}) }, - 'fend': { + 'frameEnd': { 'label': 'Frame end', 'type': 'number', 'config': json.dumps({'isdecimal': False}) From 1cb83eadabfaeb905c75f7a43adb012860af5a1f Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 20 Sep 2019 10:13:43 +0200 Subject: [PATCH 07/23] fixed PEP and other cosmetic issues in maya.lib --- pype/maya/lib.py | 98 ++++++++++++++++++++++++++---------------------- 1 file changed, 53 insertions(+), 45 deletions(-) diff --git a/pype/maya/lib.py b/pype/maya/lib.py index e54dac78f2..bd48862721 100644 --- a/pype/maya/lib.py +++ b/pype/maya/lib.py @@ -39,19 +39,17 @@ SHAPE_ATTRS = {"castsShadows", "doubleSided", "opposite"} -RENDER_ATTRS = {"vray": - { +RENDER_ATTRS = {"vray": { "node": "vraySettings", "prefix": "fileNamePrefix", "padding": "fileNamePadding", "ext": "imageFormatStr" - }, - "default": - { + }, + "default": { "node": "defaultRenderGlobals", "prefix": "imageFilePrefix", "padding": "extensionPadding" - } + } } @@ -341,19 +339,6 @@ def undo_chunk(): cmds.undoInfo(closeChunk=True) -@contextlib.contextmanager -def renderlayer(layer): - """Set the renderlayer during the context""" - - original = cmds.editRenderLayerGlobals(query=True, currentRenderLayer=True) - - try: - cmds.editRenderLayerGlobals(currentRenderLayer=layer) - yield - finally: - cmds.editRenderLayerGlobals(currentRenderLayer=original) - - @contextlib.contextmanager def evaluation(mode="off"): """Set the evaluation manager during context. @@ -832,7 +817,8 @@ def is_visible(node, # Display layers set overrideEnabled and overrideVisibility on members if cmds.attributeQuery('overrideEnabled', node=node, exists=True): override_enabled = cmds.getAttr('{}.overrideEnabled'.format(node)) - override_visibility = cmds.getAttr('{}.overrideVisibility'.format(node)) + override_visibility = cmds.getAttr('{}.overrideVisibility'.format( + node)) if override_enabled and override_visibility: return False @@ -854,8 +840,8 @@ def extract_alembic(file, startFrame=None, endFrame=None, selection=True, - uvWrite= True, - eulerFilter= True, + uvWrite=True, + eulerFilter=True, dataFormat="ogawa", verbose=False, **kwargs): @@ -1470,8 +1456,8 @@ def apply_shaders(relationships, shadernodes, nodes): member_uuids = [member["uuid"] for member in data["members"]] filtered_nodes = list() - for uuid in member_uuids: - filtered_nodes.extend(nodes_by_id[uuid]) + for m_uuid in member_uuids: + filtered_nodes.extend(nodes_by_id[m_uuid]) id_shading_engines = shading_engines_by_id[shader_uuid] if not id_shading_engines: @@ -2110,6 +2096,7 @@ def bake_to_world_space(nodes, return world_space_nodes + def load_capture_preset(path=None, data=None): import capture_gui import capture @@ -2119,14 +2106,14 @@ def load_capture_preset(path=None, data=None): else: path = path preset = capture_gui.lib.load_json(path) - print preset + print(preset) options = dict() # CODEC id = 'Codec' for key in preset[id]: - options[str(key)]= preset[id][key] + options[str(key)] = preset[id][key] # GENERIC id = 'Generic' @@ -2142,7 +2129,6 @@ def load_capture_preset(path=None, data=None): options['height'] = preset[id]['height'] options['width'] = preset[id]['width'] - # DISPLAY OPTIONS id = 'Display Options' disp_options = {} @@ -2154,7 +2140,6 @@ def load_capture_preset(path=None, data=None): options['display_options'] = disp_options - # VIEWPORT OPTIONS temp_options = {} id = 'Renderer' @@ -2163,11 +2148,12 @@ def load_capture_preset(path=None, data=None): temp_options2 = {} id = 'Viewport Options' - light_options = { 0: "default", - 1: 'all', - 2: 'selected', - 3: 'flat', - 4: 'nolights'} + light_options = { + 0: "default", + 1: 'all', + 2: 'selected', + 3: 'flat', + 4: 'nolights'} for key in preset[id]: if key == 'high_quality': temp_options2['multiSampleEnable'] = True @@ -2190,7 +2176,10 @@ def load_capture_preset(path=None, data=None): else: temp_options[str(key)] = preset[id][key] - for key in ['override_viewport_options', 'high_quality', 'alphaCut', "gpuCacheDisplayFilter"]: + for key in ['override_viewport_options', + 'high_quality', + 'alphaCut', + 'gpuCacheDisplayFilter']: temp_options.pop(key, None) for key in ['ssaoEnable']: @@ -2199,7 +2188,6 @@ def load_capture_preset(path=None, data=None): options['viewport_options'] = temp_options options['viewport2_options'] = temp_options2 - # use active sound track scene = capture.parse_active_scene() options['sound'] = scene['sound'] @@ -2363,31 +2351,51 @@ class shelf(): if item['type'] == 'button': self.addButon(item['name'], command=item['command']) if item['type'] == 'menuItem': - self.addMenuItem(item['parent'], item['name'], command=item['command']) + self.addMenuItem(item['parent'], + item['name'], + command=item['command']) if item['type'] == 'subMenu': - self.addMenuItem(item['parent'], item['name'], command=item['command']) + self.addMenuItem(item['parent'], + item['name'], + command=item['command']) - def addButon(self, label, icon="commandButton.png", command=_null, doubleCommand=_null): - '''Adds a shelf button with the specified label, command, double click command and image.''' + def addButon(self, label, icon="commandButton.png", + command=_null, doubleCommand=_null): + ''' + Adds a shelf button with the specified label, command, + double click command and image. + ''' cmds.setParent(self.name) if icon: icon = self.iconPath + icon - cmds.shelfButton(width=37, height=37, image=icon, l=label, command=command, dcc=doubleCommand, imageOverlayLabel=label, olb=self.labelBackground, olc=self.labelColour) + cmds.shelfButton(width=37, height=37, image=icon, label=label, + command=command, dcc=doubleCommand, + imageOverlayLabel=label, olb=self.labelBackground, + olc=self.labelColour) def addMenuItem(self, parent, label, command=_null, icon=""): - '''Adds a shelf button with the specified label, command, double click command and image.''' + ''' + Adds a shelf button with the specified label, command, + double click command and image. + ''' if icon: icon = self.iconPath + icon - return cmds.menuItem(p=parent, l=label, c=command, i="") + return cmds.menuItem(p=parent, label=label, c=command, i="") def addSubMenu(self, parent, label, icon=None): - '''Adds a sub menu item with the specified label and icon to the specified parent popup menu.''' + ''' + Adds a sub menu item with the specified label and icon to + the specified parent popup menu. + ''' if icon: icon = self.iconPath + icon - return cmds.menuItem(p=parent, l=label, i=icon, subMenu=1) + return cmds.menuItem(p=parent, label=label, i=icon, subMenu=1) def _cleanOldShelf(self): - '''Checks if the shelf exists and empties it if it does or creates it if it does not.''' + ''' + Checks if the shelf exists and empties it if it does + or creates it if it does not. + ''' if cmds.shelfLayout(self.name, ex=1): if cmds.shelfLayout(self.name, q=1, ca=1): for each in cmds.shelfLayout(self.name, q=1, ca=1): From 621cfddc0d2b3c7a6e9b0b05c70ad379fa282f92 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 20 Sep 2019 13:40:51 +0200 Subject: [PATCH 08/23] show selection handle on load and offset it to loaded bbox center --- pype/plugins/maya/load/load_mayaascii.py | 18 +++++++++++++++++- pype/plugins/maya/load/load_reference.py | 24 +++++++++++++++++++++++- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/pype/plugins/maya/load/load_mayaascii.py b/pype/plugins/maya/load/load_mayaascii.py index 03a15b0524..b9a5de2782 100644 --- a/pype/plugins/maya/load/load_mayaascii.py +++ b/pype/plugins/maya/load/load_mayaascii.py @@ -45,7 +45,23 @@ class MayaAsciiLoader(pype.maya.plugin.ReferenceLoader): cmds.setAttr(groupName + ".useOutlinerColor", 1) cmds.setAttr(groupName + ".outlinerColor", c[0], c[1], c[2]) - + cmds.setAttr(groupName + ".displayHandle", 1) + # get bounding box + bbox = cmds.exactWorldBoundingBox(groupName) + # get pivot position on world space + pivot = cmds.xform(groupName, q=True, sp=True, ws=True) + # center of bounding box + cx = (bbox[0] + bbox[3]) / 2 + cy = (bbox[1] + bbox[4]) / 2 + cz = (bbox[2] + bbox[5]) / 2 + # add pivot position to calculate offset + cx = cx + pivot[0] + cy = cy + pivot[1] + cz = cz + pivot[2] + # set selection handle offset to center of bounding box + cmds.setAttr(groupName + ".selectHandleX", cx) + cmds.setAttr(groupName + ".selectHandleY", cy) + cmds.setAttr(groupName + ".selectHandleZ", cz) return nodes def switch(self, container, representation): diff --git a/pype/plugins/maya/load/load_reference.py b/pype/plugins/maya/load/load_reference.py index fb4b90a1cd..0a9796e5d7 100644 --- a/pype/plugins/maya/load/load_reference.py +++ b/pype/plugins/maya/load/load_reference.py @@ -1,8 +1,9 @@ -from avalon import api + import pype.maya.plugin import os from pypeapp import config import pymel.core as pm +from pprint import pprint reload(config) @@ -58,6 +59,9 @@ class ReferenceLoader(pype.maya.plugin.ReferenceLoader): for root in roots: root.setParent(groupNode) + cmds.setAttr(groupName + ".displayHandle", 1) + groupNode + presets = config.get_presets(project=os.environ['AVALON_PROJECT']) colors = presets['plugins']['maya']['load']['colors'] c = colors.get(family) @@ -67,6 +71,24 @@ class ReferenceLoader(pype.maya.plugin.ReferenceLoader): self[:] = nodes + cmds.setAttr(groupName + ".displayHandle", 1) + # get bounding box + bbox = cmds.exactWorldBoundingBox(groupName) + # get pivot position on world space + pivot = cmds.xform(groupName, q=True, sp=True, ws=True) + # center of bounding box + cx = (bbox[0] + bbox[3]) / 2 + cy = (bbox[1] + bbox[4]) / 2 + cz = (bbox[2] + bbox[5]) / 2 + # add pivot position to calculate offset + cx = cx + pivot[0] + cy = cy + pivot[1] + cz = cz + pivot[2] + # set selection handle offset to center of bounding box + cmds.setAttr(groupName + ".selectHandleX", cx) + cmds.setAttr(groupName + ".selectHandleY", cy) + cmds.setAttr(groupName + ".selectHandleZ", cz) + return nodes def switch(self, container, representation): From b145cfe6d29c4f2e0baada93217ca9a043dbe371 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 20 Sep 2019 13:45:34 +0200 Subject: [PATCH 09/23] fixed unused pprint import --- pype/plugins/maya/load/load_reference.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pype/plugins/maya/load/load_reference.py b/pype/plugins/maya/load/load_reference.py index 0a9796e5d7..f855cb55f9 100644 --- a/pype/plugins/maya/load/load_reference.py +++ b/pype/plugins/maya/load/load_reference.py @@ -3,7 +3,6 @@ import pype.maya.plugin import os from pypeapp import config import pymel.core as pm -from pprint import pprint reload(config) @@ -94,6 +93,7 @@ class ReferenceLoader(pype.maya.plugin.ReferenceLoader): def switch(self, container, representation): self.update(container, representation) + # for backwards compatibility class AbcLoader(ReferenceLoader): label = "Deprecated loader (don't use)" @@ -101,6 +101,7 @@ class AbcLoader(ReferenceLoader): representations = ["abc"] tool_names = [] + # for backwards compatibility class ModelLoader(ReferenceLoader): label = "Deprecated loader (don't use)" From 16189b56918acebb51295b93f157f0d892e5497e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 2 Oct 2019 13:45:01 +0200 Subject: [PATCH 10/23] fix: some changes improving publishing and loading luts --- pype/plugins/global/publish/integrate_new.py | 2 + pype/plugins/nuke/load/load_luts.py | 322 +++++++++++++++++ pype/plugins/nuke/load/load_luts_ip.py | 335 ++++++++++++++++++ .../nuke/publish/validate_active_viewer.py | 24 ++ 4 files changed, 683 insertions(+) create mode 100644 pype/plugins/nuke/load/load_luts.py create mode 100644 pype/plugins/nuke/load/load_luts_ip.py create mode 100644 pype/plugins/nuke/publish/validate_active_viewer.py diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index d9e4f3f533..e87ee97087 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -429,6 +429,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): Returns: None """ + src = os.path.normpath(src) + dst = os.path.normpath(dst) self.log.debug("Copying file .. {} -> {}".format(src, dst)) dirname = os.path.dirname(dst) diff --git a/pype/plugins/nuke/load/load_luts.py b/pype/plugins/nuke/load/load_luts.py new file mode 100644 index 0000000000..4f7c19a588 --- /dev/null +++ b/pype/plugins/nuke/load/load_luts.py @@ -0,0 +1,322 @@ +from avalon import api, style, io +import nuke +import json +from collections import OrderedDict + + +class LoadLuts(api.Loader): + """Loading colorspace soft effect exported from nukestudio""" + + representations = ["lutJson"] + families = ["lut"] + + label = "Load Luts - nodes" + order = 0 + icon = "cc" + color = style.colors.light + ignore_attr = ["useLifetime"] + + def load(self, context, name, namespace, data): + """ + Loading function to get the soft effects to particular read node + + Arguments: + context (dict): context of version + name (str): name of the version + namespace (str): asset name + data (dict): compulsory attribute > not used + + Returns: + nuke node: containerised nuke node object + """ + # import dependencies + from avalon.nuke import containerise + + # get main variables + version = context['version'] + version_data = version.get("data", {}) + vname = version.get("name", None) + first = version_data.get("frameStart", None) + last = version_data.get("frameEnd", None) + workfile_first_frame = int(nuke.root()["first_frame"].getValue()) + namespace = namespace or context['asset']['name'] + colorspace = version_data.get("colorspace", None) + object_name = "{}_{}".format(name, namespace) + + # prepare data for imprinting + # add additional metadata from the version to imprint to Avalon knob + add_keys = ["frameStart", "frameEnd", "handleStart", "handleEnd", + "source", "author", "fps"] + + data_imprint = {"frameStart": first, + "frameEnd": last, + "version": vname, + "colorspaceInput": colorspace, + "objectName": object_name} + + for k in add_keys: + data_imprint.update({k: version_data[k]}) + + # getting file path + file = self.fname.replace("\\", "/") + + # getting data from json file with unicode conversion + with open(file, "r") as f: + json_f = {self.byteify(key): self.byteify(value) + for key, value in json.load(f).iteritems()} + + # get correct order of nodes by positions on track and subtrack + nodes_order = self.reorder_nodes(json_f["effects"]) + + # adding nodes to node graph + # just in case we are in group lets jump out of it + nuke.endGroup() + + GN = nuke.createNode("Group") + + GN["name"].setValue(object_name) + + # adding content to the group node + with GN: + pre_node = nuke.createNode("Input") + pre_node["name"].setValue("rgb") + + for ef_name, ef_val in nodes_order.items(): + node = nuke.createNode(ef_val["class"]) + for k, v in ef_val["node"].items(): + if k in self.ignore_attr: + continue + if isinstance(v, list) and len(v) > 4: + node[k].setAnimated() + for i, value in enumerate(v): + if isinstance(value, list): + for ci, cv in enumerate(value): + node[k].setValueAt( + cv, + (workfile_first_frame + i), + ci) + else: + node[k].setValueAt( + value, + (workfile_first_frame + i)) + else: + node[k].setValue(v) + node.setInput(0, pre_node) + pre_node = node + + output = nuke.createNode("Output") + output.setInput(0, pre_node) + + # try to find parent read node + self.connect_read_node(GN, namespace, json_f["assignTo"]) + + GN["tile_color"].setValue(int("0x3469ffff", 16)) + + self.log.info("Loaded lut setup: `{}`".format(GN["name"].value())) + + return containerise( + node=GN, + name=name, + namespace=namespace, + context=context, + loader=self.__class__.__name__, + data=data_imprint) + + def update(self, container, representation): + """Update the Loader's path + + Nuke automatically tries to reset some variables when changing + the loader's path to a new file. These automatic changes are to its + inputs: + + """ + + from avalon.nuke import ( + update_container + ) + # get main variables + # Get version from io + version = io.find_one({ + "type": "version", + "_id": representation["parent"] + }) + # get corresponding node + GN = nuke.toNode(container['objectName']) + + file = api.get_representation_path(representation).replace("\\", "/") + name = container['name'] + version_data = version.get("data", {}) + vname = version.get("name", None) + first = version_data.get("frameStart", None) + last = version_data.get("frameEnd", None) + workfile_first_frame = int(nuke.root()["first_frame"].getValue()) + namespace = container['namespace'] + colorspace = version_data.get("colorspace", None) + object_name = "{}_{}".format(name, namespace) + + add_keys = ["frameStart", "frameEnd", "handleStart", "handleEnd", + "source", "author", "fps"] + + data_imprint = {"representation": str(representation["_id"]), + "frameStart": first, + "frameEnd": last, + "version": vname, + "colorspaceInput": colorspace, + "objectName": object_name} + + for k in add_keys: + data_imprint.update({k: version_data[k]}) + + # Update the imprinted representation + update_container( + GN, + data_imprint + ) + + # getting data from json file with unicode conversion + with open(file, "r") as f: + json_f = {self.byteify(key): self.byteify(value) + for key, value in json.load(f).iteritems()} + + # get correct order of nodes by positions on track and subtrack + nodes_order = self.reorder_nodes(json_f["effects"]) + + # adding nodes to node graph + # just in case we are in group lets jump out of it + nuke.endGroup() + + # adding content to the group node + with GN: + # first remove all nodes + [nuke.delete(n) for n in nuke.allNodes()] + + # create input node + pre_node = nuke.createNode("Input") + pre_node["name"].setValue("rgb") + + for ef_name, ef_val in nodes_order.items(): + node = nuke.createNode(ef_val["class"]) + for k, v in ef_val["node"].items(): + if k in self.ignore_attr: + continue + if isinstance(v, list) and len(v) > 3: + node[k].setAnimated() + for i, value in enumerate(v): + if isinstance(value, list): + for ci, cv in enumerate(value): + node[k].setValueAt( + cv, + (workfile_first_frame + i), + ci) + else: + node[k].setValueAt( + value, + (workfile_first_frame + i)) + else: + node[k].setValue(v) + node.setInput(0, pre_node) + pre_node = node + + # create output node + output = nuke.createNode("Output") + output.setInput(0, pre_node) + + # try to find parent read node + self.connect_read_node(GN, namespace, json_f["assignTo"]) + + # get all versions in list + versions = io.find({ + "type": "version", + "parent": version["parent"] + }).distinct('name') + + max_version = max(versions) + + # change color of node + if version.get("name") not in [max_version]: + GN["tile_color"].setValue(int("0xd84f20ff", 16)) + else: + GN["tile_color"].setValue(int("0x3469ffff", 16)) + + self.log.info("udated to version: {}".format(version.get("name"))) + + def connect_read_node(self, group_node, asset, subset): + """ + Finds read node and selects it + + Arguments: + asset (str): asset name + + Returns: + nuke node: node is selected + None: if nothing found + """ + search_name = "{0}_{1}".format(asset, subset) + node = [n for n in nuke.allNodes() if search_name in n["name"].value()] + if len(node) > 0: + rn = node[0] + else: + rn = None + + # Parent read node has been found + # solving connections + if rn: + dep_nodes = rn.dependent() + + if len(dep_nodes) > 0: + for dn in dep_nodes: + dn.setInput(0, group_node) + + group_node.setInput(0, rn) + group_node.autoplace() + + def reorder_nodes(self, data): + new_order = OrderedDict() + trackNums = [v["trackIndex"] for k, v in data.items()] + subTrackNums = [v["subTrackIndex"] for k, v in data.items()] + + for trackIndex in range( + min(trackNums), max(trackNums) + 1): + for subTrackIndex in range( + min(subTrackNums), max(subTrackNums) + 1): + item = self.get_item(data, trackIndex, subTrackIndex) + if item is not {}: + new_order.update(item) + return new_order + + def get_item(self, data, trackIndex, subTrackIndex): + return {key: val for key, val in data.items() + if subTrackIndex == val["subTrackIndex"] + if trackIndex == val["trackIndex"]} + + def byteify(self, input): + """ + Converts unicode strings to strings + It goes trought all dictionary + + Arguments: + input (dict/str): input + + Returns: + dict: with fixed values and keys + + """ + + if isinstance(input, dict): + return {self.byteify(key): self.byteify(value) + for key, value in input.iteritems()} + elif isinstance(input, list): + return [self.byteify(element) for element in input] + elif isinstance(input, unicode): + return input.encode('utf-8') + else: + return input + + def switch(self, container, representation): + self.update(container, representation) + + def remove(self, container): + from avalon.nuke import viewer_update_and_undo_stop + node = nuke.toNode(container['objectName']) + with viewer_update_and_undo_stop(): + nuke.delete(node) diff --git a/pype/plugins/nuke/load/load_luts_ip.py b/pype/plugins/nuke/load/load_luts_ip.py new file mode 100644 index 0000000000..b30f84cc42 --- /dev/null +++ b/pype/plugins/nuke/load/load_luts_ip.py @@ -0,0 +1,335 @@ +from avalon import api, style, io +import nuke +import json +from collections import OrderedDict +from pype.nuke import lib + +class LoadLutsInputProcess(api.Loader): + """Loading colorspace soft effect exported from nukestudio""" + + representations = ["lutJson"] + families = ["lut"] + + label = "Load Luts - Input Process" + order = 0 + icon = "eye" + color = style.colors.alert + ignore_attr = ["useLifetime"] + + def load(self, context, name, namespace, data): + """ + Loading function to get the soft effects to particular read node + + Arguments: + context (dict): context of version + name (str): name of the version + namespace (str): asset name + data (dict): compulsory attribute > not used + + Returns: + nuke node: containerised nuke node object + """ + # import dependencies + from avalon.nuke import containerise + + # get main variables + version = context['version'] + version_data = version.get("data", {}) + vname = version.get("name", None) + first = version_data.get("frameStart", None) + last = version_data.get("frameEnd", None) + workfile_first_frame = int(nuke.root()["first_frame"].getValue()) + namespace = namespace or context['asset']['name'] + colorspace = version_data.get("colorspace", None) + object_name = "{}_{}".format(name, namespace) + + # prepare data for imprinting + # add additional metadata from the version to imprint to Avalon knob + add_keys = ["frameStart", "frameEnd", "handleStart", "handleEnd", + "source", "author", "fps"] + + data_imprint = {"frameStart": first, + "frameEnd": last, + "version": vname, + "colorspaceInput": colorspace, + "objectName": object_name} + + for k in add_keys: + data_imprint.update({k: version_data[k]}) + + # getting file path + file = self.fname.replace("\\", "/") + + # getting data from json file with unicode conversion + with open(file, "r") as f: + json_f = {self.byteify(key): self.byteify(value) + for key, value in json.load(f).iteritems()} + + # get correct order of nodes by positions on track and subtrack + nodes_order = self.reorder_nodes(json_f["effects"]) + + # adding nodes to node graph + # just in case we are in group lets jump out of it + nuke.endGroup() + + GN = nuke.createNode("Group") + + GN["name"].setValue(object_name) + + # adding content to the group node + with GN: + pre_node = nuke.createNode("Input") + pre_node["name"].setValue("rgb") + + for ef_name, ef_val in nodes_order.items(): + node = nuke.createNode(ef_val["class"]) + for k, v in ef_val["node"].items(): + if k in self.ignore_attr: + continue + if isinstance(v, list) and len(v) > 4: + node[k].setAnimated() + for i, value in enumerate(v): + if isinstance(value, list): + for ci, cv in enumerate(value): + node[k].setValueAt( + cv, + (workfile_first_frame + i), + ci) + else: + node[k].setValueAt( + value, + (workfile_first_frame + i)) + else: + node[k].setValue(v) + node.setInput(0, pre_node) + pre_node = node + + output = nuke.createNode("Output") + output.setInput(0, pre_node) + + # try to place it under Viewer1 + if not self.connect_active_viewer(GN): + nuke.delete(GN) + return + + GN["tile_color"].setValue(int("0x3469ffff", 16)) + + self.log.info("Loaded lut setup: `{}`".format(GN["name"].value())) + + return containerise( + node=GN, + name=name, + namespace=namespace, + context=context, + loader=self.__class__.__name__, + data=data_imprint) + + def update(self, container, representation): + """Update the Loader's path + + Nuke automatically tries to reset some variables when changing + the loader's path to a new file. These automatic changes are to its + inputs: + + """ + + from avalon.nuke import ( + update_container + ) + # get main variables + # Get version from io + version = io.find_one({ + "type": "version", + "_id": representation["parent"] + }) + # get corresponding node + GN = nuke.toNode(container['objectName']) + + file = api.get_representation_path(representation).replace("\\", "/") + name = container['name'] + version_data = version.get("data", {}) + vname = version.get("name", None) + first = version_data.get("frameStart", None) + last = version_data.get("frameEnd", None) + workfile_first_frame = int(nuke.root()["first_frame"].getValue()) + namespace = container['namespace'] + colorspace = version_data.get("colorspace", None) + object_name = "{}_{}".format(name, namespace) + + add_keys = ["frameStart", "frameEnd", "handleStart", "handleEnd", + "source", "author", "fps"] + + data_imprint = {"representation": str(representation["_id"]), + "frameStart": first, + "frameEnd": last, + "version": vname, + "colorspaceInput": colorspace, + "objectName": object_name} + + for k in add_keys: + data_imprint.update({k: version_data[k]}) + + # Update the imprinted representation + update_container( + GN, + data_imprint + ) + + # getting data from json file with unicode conversion + with open(file, "r") as f: + json_f = {self.byteify(key): self.byteify(value) + for key, value in json.load(f).iteritems()} + + # get correct order of nodes by positions on track and subtrack + nodes_order = self.reorder_nodes(json_f["effects"]) + + # adding nodes to node graph + # just in case we are in group lets jump out of it + nuke.endGroup() + + # adding content to the group node + with GN: + # first remove all nodes + [nuke.delete(n) for n in nuke.allNodes()] + + # create input node + pre_node = nuke.createNode("Input") + pre_node["name"].setValue("rgb") + + for ef_name, ef_val in nodes_order.items(): + node = nuke.createNode(ef_val["class"]) + for k, v in ef_val["node"].items(): + if k in self.ignore_attr: + continue + if isinstance(v, list) and len(v) > 3: + node[k].setAnimated() + for i, value in enumerate(v): + if isinstance(value, list): + for ci, cv in enumerate(value): + node[k].setValueAt( + cv, + (workfile_first_frame + i), + ci) + else: + node[k].setValueAt( + value, + (workfile_first_frame + i)) + else: + node[k].setValue(v) + node.setInput(0, pre_node) + pre_node = node + + # create output node + output = nuke.createNode("Output") + output.setInput(0, pre_node) + + # try to place it under Viewer1 + if not self.connect_active_viewer(GN): + nuke.delete(GN) + return + + # get all versions in list + versions = io.find({ + "type": "version", + "parent": version["parent"] + }).distinct('name') + + max_version = max(versions) + + # change color of node + if version.get("name") not in [max_version]: + GN["tile_color"].setValue(int("0xd84f20ff", 16)) + else: + GN["tile_color"].setValue(int("0x3469ffff", 16)) + + self.log.info("udated to version: {}".format(version.get("name"))) + + def connect_active_viewer(self, group_node): + """ + Finds Active viewer and + place the node under it, also adds + name of group into Input Process of the viewer + + Arguments: + group_node (nuke node): nuke group node object + + """ + group_node_name = group_node["name"].value() + + viewer = [n for n in nuke.allNodes() if "Viewer1" in n["name"].value()] + if len(viewer) > 0: + viewer = viewer[0] + else: + self.log.error("Please create Viewer node before you run this action again") + return None + + # get coordinates of Viewer1 + xpos = viewer["xpos"].value() + ypos = viewer["ypos"].value() + + ypos += 150 + + viewer["ypos"].setValue(ypos) + + # set coordinates to group node + group_node["xpos"].setValue(xpos) + group_node["ypos"].setValue(ypos + 50) + + # add group node name to Viewer Input Process + viewer["input_process_node"].setValue(group_node_name) + + # put backdrop under + lib.create_backdrop(label="Input Process", layer=2, nodes=[viewer, group_node], color="0x7c7faaff") + + return True + + def reorder_nodes(self, data): + new_order = OrderedDict() + trackNums = [v["trackIndex"] for k, v in data.items()] + subTrackNums = [v["subTrackIndex"] for k, v in data.items()] + + for trackIndex in range( + min(trackNums), max(trackNums) + 1): + for subTrackIndex in range( + min(subTrackNums), max(subTrackNums) + 1): + item = self.get_item(data, trackIndex, subTrackIndex) + if item is not {}: + new_order.update(item) + return new_order + + def get_item(self, data, trackIndex, subTrackIndex): + return {key: val for key, val in data.items() + if subTrackIndex == val["subTrackIndex"] + if trackIndex == val["trackIndex"]} + + def byteify(self, input): + """ + Converts unicode strings to strings + It goes trought all dictionary + + Arguments: + input (dict/str): input + + Returns: + dict: with fixed values and keys + + """ + + if isinstance(input, dict): + return {self.byteify(key): self.byteify(value) + for key, value in input.iteritems()} + elif isinstance(input, list): + return [self.byteify(element) for element in input] + elif isinstance(input, unicode): + return input.encode('utf-8') + else: + return input + + def switch(self, container, representation): + self.update(container, representation) + + def remove(self, container): + from avalon.nuke import viewer_update_and_undo_stop + node = nuke.toNode(container['objectName']) + with viewer_update_and_undo_stop(): + nuke.delete(node) diff --git a/pype/plugins/nuke/publish/validate_active_viewer.py b/pype/plugins/nuke/publish/validate_active_viewer.py new file mode 100644 index 0000000000..618a7f1502 --- /dev/null +++ b/pype/plugins/nuke/publish/validate_active_viewer.py @@ -0,0 +1,24 @@ +import pyblish.api +import nuke + + +class ValidateActiveViewer(pyblish.api.ContextPlugin): + """Validate presentse of the active viewer from nodes + """ + + order = pyblish.api.ValidatorOrder + label = "Validate Active Viewer" + hosts = ["nuke"] + + def process(self, context): + viewer_process_node = context.data.get("ViewerProcess") + + assert viewer_process_node, ( + "Missing active viewer process! Please click on output write node and push key number 1-9" + ) + active_viewer = context.data["ActiveViewer"] + active_input = active_viewer.activeInput() + + assert active_input is not None, ( + "Missing active viewer input! Please click on output write node and push key number 1-9" + ) From b7c827f35d4b8d223ed62838bb15d60abfdb8b48 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 4 Oct 2019 13:13:51 +0200 Subject: [PATCH 11/23] family widget also stores and collect key of family in presets so can be trackable on pyblish --- pype/standalonepublish/widgets/__init__.py | 1 + pype/standalonepublish/widgets/widget_family.py | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pype/standalonepublish/widgets/__init__.py b/pype/standalonepublish/widgets/__init__.py index 4c6a0e85a5..c6e0dd9a47 100644 --- a/pype/standalonepublish/widgets/__init__.py +++ b/pype/standalonepublish/widgets/__init__.py @@ -6,6 +6,7 @@ HelpRole = QtCore.Qt.UserRole + 2 FamilyRole = QtCore.Qt.UserRole + 3 ExistsRole = QtCore.Qt.UserRole + 4 PluginRole = QtCore.Qt.UserRole + 5 +PluginKeyRole = QtCore.Qt.UserRole + 6 from ..resources import get_resource from .button_from_svgs import SvgResizable, SvgButton diff --git a/pype/standalonepublish/widgets/widget_family.py b/pype/standalonepublish/widgets/widget_family.py index 63776b1df3..26eb8077d9 100644 --- a/pype/standalonepublish/widgets/widget_family.py +++ b/pype/standalonepublish/widgets/widget_family.py @@ -5,7 +5,7 @@ import json from collections import namedtuple from . import QtWidgets, QtCore -from . import HelpRole, FamilyRole, ExistsRole, PluginRole +from . import HelpRole, FamilyRole, ExistsRole, PluginRole, PluginKeyRole from . import FamilyDescriptionWidget from pypeapp import config @@ -116,8 +116,10 @@ class FamilyWidget(QtWidgets.QWidget): def collect_data(self): plugin = self.list_families.currentItem().data(PluginRole) + key = self.list_families.currentItem().data(PluginKeyRole) family = plugin.family.rsplit(".", 1)[-1] data = { + 'family_preset_key': key, 'family': family, 'subset': self.input_result.text(), 'version': self.version_spinbox.value() @@ -318,7 +320,7 @@ class FamilyWidget(QtWidgets.QWidget): has_families = False presets = config.get_presets().get('standalone_publish', {}) - for creator in presets.get('families', {}).values(): + for key, creator in presets.get('families', {}).items(): creator = namedtuple("Creator", creator.keys())(*creator.values()) label = creator.label or creator.family @@ -327,6 +329,7 @@ class FamilyWidget(QtWidgets.QWidget): item.setData(HelpRole, creator.help or "") item.setData(FamilyRole, creator.family) item.setData(PluginRole, creator) + item.setData(PluginKeyRole, key) item.setData(ExistsRole, False) self.list_families.addItem(item) From 71ad16e650e8df39fe50bfb63a6792084e34e7bb Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 4 Oct 2019 13:14:50 +0200 Subject: [PATCH 12/23] drop frame uses full path to ffprobe set in FFMPEG_PATH env --- pype/standalonepublish/widgets/widget_drop_frame.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pype/standalonepublish/widgets/widget_drop_frame.py b/pype/standalonepublish/widgets/widget_drop_frame.py index e60db892db..a5a686bae1 100644 --- a/pype/standalonepublish/widgets/widget_drop_frame.py +++ b/pype/standalonepublish/widgets/widget_drop_frame.py @@ -220,15 +220,21 @@ class DropDataFrame(QtWidgets.QFrame): self._process_data(data) def load_data_with_probe(self, filepath): + ffprobe_path = os.getenv("FFMPEG_PATH", "") + if ffprobe_path: + ffprobe_path += '/ffprobe' + else: + ffprobe_path = 'ffprobe' + args = [ - 'ffprobe', + ffprobe_path, '-v', 'quiet', '-print_format', 'json', '-show_format', '-show_streams', filepath ] ffprobe_p = subprocess.Popen( - args, + ' '.join(args), stdout=subprocess.PIPE, shell=True ) From c9b8bcc60247dbd4471c6e0280543158703f14de Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 4 Oct 2019 13:15:14 +0200 Subject: [PATCH 13/23] host of application is set to standalonepublisher instead of shell --- pype/standalonepublish/publish.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/standalonepublish/publish.py b/pype/standalonepublish/publish.py index 13b505666c..f199aaf84e 100644 --- a/pype/standalonepublish/publish.py +++ b/pype/standalonepublish/publish.py @@ -103,7 +103,7 @@ def avalon_api_publish(data, gui=True): "-pp", os.pathsep.join(pyblish.api.registered_paths()) ] - os.environ["PYBLISH_HOSTS"] = "shell" + os.environ["PYBLISH_HOSTS"] = "standalonepublisher" os.environ["SAPUBLISH_INPATH"] = json_data_path if gui: @@ -139,7 +139,7 @@ def cli_publish(data, gui=True): if gui: args += ["gui"] - os.environ["PYBLISH_HOSTS"] = "shell" + os.environ["PYBLISH_HOSTS"] = "standalonepublisher" os.environ["SAPUBLISH_INPATH"] = json_data_path os.environ["SAPUBLISH_OUTPATH"] = return_data_path From 6f889eec9e332ba42ed7ea01df5e385e1e4164af Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 4 Oct 2019 13:15:54 +0200 Subject: [PATCH 14/23] added conversion to int in case frameStart contain string --- pype/plugins/global/publish/integrate_new.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index 6c89e22a83..a3efd10b2e 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -307,7 +307,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): if repre.get("frameStart"): frame_start_padding = len(str( repre.get("frameEnd"))) - index_frame_start = repre.get("frameStart") + index_frame_start = int(repre.get("frameStart")) dst_padding_exp = src_padding_exp for i in src_collection.indexes: From e3ac16427256cb59a87eb86eb939246d7f245e70 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 4 Oct 2019 13:17:30 +0200 Subject: [PATCH 15/23] collect scene version is skipped in standalone publisher --- pype/plugins/global/publish/collect_scene_version.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pype/plugins/global/publish/collect_scene_version.py b/pype/plugins/global/publish/collect_scene_version.py index 12075e2417..3fac823b5c 100644 --- a/pype/plugins/global/publish/collect_scene_version.py +++ b/pype/plugins/global/publish/collect_scene_version.py @@ -13,6 +13,8 @@ class CollectSceneVersion(pyblish.api.ContextPlugin): label = 'Collect Version' def process(self, context): + if "standalonepublisher" in context.data.get("host"): + return filename = os.path.basename(context.data.get('currentFile')) From 4bd116b096bd529dd3c09945ce9870fd42e38863 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 4 Oct 2019 13:18:16 +0200 Subject: [PATCH 16/23] collect presets order is lowered so they are collected much earlier (before collect_context plugin) --- pype/plugins/global/publish/collect_presets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/global/publish/collect_presets.py b/pype/plugins/global/publish/collect_presets.py index 7e0d3e2f4b..4d483ec79b 100644 --- a/pype/plugins/global/publish/collect_presets.py +++ b/pype/plugins/global/publish/collect_presets.py @@ -5,7 +5,7 @@ from pypeapp import config class CollectPresets(api.ContextPlugin): """Collect Presets.""" - order = api.CollectorOrder + order = api.CollectorOrder - 0.491 label = "Collect Presets" def process(self, context): From 3e00bb57e848f0e3074c4d4ff07444f9dd843fc9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 4 Oct 2019 13:18:51 +0200 Subject: [PATCH 17/23] added standalonepublisher to hosts of collect output repre config --- pype/plugins/global/publish/collect_output_repre_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/global/publish/collect_output_repre_config.py b/pype/plugins/global/publish/collect_output_repre_config.py index 5595e29cab..248599f749 100644 --- a/pype/plugins/global/publish/collect_output_repre_config.py +++ b/pype/plugins/global/publish/collect_output_repre_config.py @@ -9,7 +9,7 @@ class CollectOutputRepreConfig(pyblish.api.ContextPlugin): order = pyblish.api.CollectorOrder label = "Collect Config for representation" - hosts = ["shell"] + hosts = ["shell", "standalonepublisher"] def process(self, context): config_data = config.get_presets()["ftrack"]["output_representation"] From ff0009f8147cbbe4c90bb0b346d8586fcb38be0c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 4 Oct 2019 13:21:17 +0200 Subject: [PATCH 18/23] anatomy template is not set if family don't have set anatom_template key --- .../plugins/global/publish/collect_context.py | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/pype/plugins/global/publish/collect_context.py b/pype/plugins/global/publish/collect_context.py index 31ab95259c..61c3bcf4d8 100644 --- a/pype/plugins/global/publish/collect_context.py +++ b/pype/plugins/global/publish/collect_context.py @@ -31,9 +31,25 @@ class CollectContextDataSAPublish(pyblish.api.ContextPlugin): in_data = json.load(f) asset_name = in_data['asset'] + family_preset_key = in_data.get('family_preset_key', '') family = in_data['family'] subset = in_data['subset'] + # Load presets + presets = context.data.get("presets") + if not presets: + from pypeapp import config + presets = config.get_presets() + + # Get from presets anatomy key that will be used for getting template + # - default integrate new is used if not set + anatomy_key = presets.get( + "standalone_publish", {}).get( + "families", {}).get( + family_preset_key, {}).get( + "anatomy_template" + ) + project = io.find_one({'type': 'project'}) asset = io.find_one({ 'type': 'asset', @@ -63,7 +79,9 @@ class CollectContextDataSAPublish(pyblish.api.ContextPlugin): component['destination'] = component['files'] component['stagingDir'] = component['stagingDir'] - component['anatomy_template'] = 'render' + # Do not set anatomy_template if not specified + if anatomy_key: + component['anatomy_template'] = anatomy_key if isinstance(component['files'], list): collections, remainder = clique.assemble(component['files']) self.log.debug("collecting sequence: {}".format(collections)) From 195f9640bd87c44929db23c542497527333d2f23 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 4 Oct 2019 13:22:37 +0200 Subject: [PATCH 19/23] added extractor for thumbnails in standalone publisher should create thumbnail from any image or video input --- .../global/publish/extract_thumbnail_sa.py | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 pype/plugins/global/publish/extract_thumbnail_sa.py diff --git a/pype/plugins/global/publish/extract_thumbnail_sa.py b/pype/plugins/global/publish/extract_thumbnail_sa.py new file mode 100644 index 0000000000..7e31e3c701 --- /dev/null +++ b/pype/plugins/global/publish/extract_thumbnail_sa.py @@ -0,0 +1,126 @@ +import os +import tempfile +import subprocess +import pyblish.api +import pype.api + + +class ExtractThumbnail(pyblish.api.InstancePlugin): + """Extract jpeg thumbnail from component input from standalone publisher + + Uses jpeg file from component if possible (when single or multiple jpegs + are loaded to component selected as thumbnail) otherwise extracts from + input file/s single jpeg to temp. + """ + + label = "Extract Thumbnail" + hosts = ["standalonepublisher"] + order = pyblish.api.ExtractorOrder + + def process(self, instance): + repres = instance.data.get('representations') + if not repres: + return + + thumbnail_repre = None + for repre in repres: + if repre.get("thumbnail"): + thumbnail_repre = repre + break + + if not thumbnail_repre: + return + + files = thumbnail_repre.get("files") + if not files: + return + + if isinstance(files, list): + files_len = len(files) + file = str(files[0]) + else: + files_len = 1 + file = files + + is_jpeg = False + if file.endswith(".jpeg") or file.endswith(".jpg"): + is_jpeg = True + + if is_jpeg and files_len == 1: + # skip if already is single jpeg file + return + + elif is_jpeg: + # use first frame as thumbnail if is sequence of jpegs + full_thumbnail_path = file + self.log.info( + "For thumbnail is used file: {}".format(full_thumbnail_path) + ) + + else: + # Convert to jpeg if not yet + full_input_path = os.path.join(thumbnail_repre["stagingDir"], file) + self.log.info("input {}".format(full_input_path)) + + full_thumbnail_path = tempfile.mkstemp(suffix=".jpg")[1] + self.log.info("output {}".format(full_thumbnail_path)) + + config_data = instance.context.data.get("output_repre_config", {}) + + proj_name = os.environ.get("AVALON_PROJECT", "__default__") + profile = config_data.get( + proj_name, + config_data.get("__default__", {}) + ) + + ffmpeg_path = os.getenv("FFMPEG_PATH", "") + if ffmpeg_path: + ffmpeg_path += "/ffmpeg" + else: + ffmpeg_path = "ffmpeg" + + jpeg_items = [] + jpeg_items.append(ffmpeg_path) + # override file if already exists + jpeg_items.append("-y") + # add input filters from peresets + if profile: + jpeg_items.extend(profile.get('input', [])) + # input file + jpeg_items.append("-i {}".format(full_input_path)) + # extract only single file + jpeg_items.append("-vframes 1") + # output file + jpeg_items.append(full_thumbnail_path) + + subprocess_jpeg = " ".join(jpeg_items) + + # run subprocess + self.log.debug("Executing: {}".format(subprocess_jpeg)) + subprocess.Popen( + subprocess_jpeg, + stdout=subprocess.PIPE, + shell=True + ) + + # remove thumbnail key from origin repre + thumbnail_repre.pop("thumbnail") + + filename = os.path.basename(full_thumbnail_path) + staging_dir = os.path.dirname(full_thumbnail_path) + + # create new thumbnail representation + representation = { + 'name': 'jpg', + 'ext': 'jpg', + 'files': filename, + "stagingDir": staging_dir, + "thumbnail": True, + "tags": [] + } + + # add Delete tag when temp file was rendered + if not is_jpeg: + representation["tags"].append("delete") + + instance.data["representations"].append(representation) From 2f06a65198a3f5d71aacbae934d3283cdb2d36ca Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 4 Oct 2019 16:41:36 +0200 Subject: [PATCH 20/23] impoving publishing of baked mov - adding feature to add input process node to baking - removing collec/validate active viewer process (not necessary) - output node added to render write node --- .../collect_active_viewer.py | 1 - .../validate_active_viewer.py | 0 .../nuke/publish/extract_ouput_node.py | 20 ++--- .../nuke/publish/extract_review_data.py | 73 ++++++++++++------- .../nuke/publish/validate_rendered_frames.py | 2 + 5 files changed, 57 insertions(+), 39 deletions(-) rename pype/plugins/nuke/{publish => _publish_unused}/collect_active_viewer.py (83%) rename pype/plugins/nuke/{publish => _publish_unused}/validate_active_viewer.py (100%) diff --git a/pype/plugins/nuke/publish/collect_active_viewer.py b/pype/plugins/nuke/_publish_unused/collect_active_viewer.py similarity index 83% rename from pype/plugins/nuke/publish/collect_active_viewer.py rename to pype/plugins/nuke/_publish_unused/collect_active_viewer.py index 5dc17d8768..5a6cc02b88 100644 --- a/pype/plugins/nuke/publish/collect_active_viewer.py +++ b/pype/plugins/nuke/_publish_unused/collect_active_viewer.py @@ -11,5 +11,4 @@ class CollectActiveViewer(pyblish.api.ContextPlugin): hosts = ["nuke"] def process(self, context): - context.data["ViewerProcess"] = nuke.ViewerProcess.node() context.data["ActiveViewer"] = nuke.activeViewer() diff --git a/pype/plugins/nuke/publish/validate_active_viewer.py b/pype/plugins/nuke/_publish_unused/validate_active_viewer.py similarity index 100% rename from pype/plugins/nuke/publish/validate_active_viewer.py rename to pype/plugins/nuke/_publish_unused/validate_active_viewer.py diff --git a/pype/plugins/nuke/publish/extract_ouput_node.py b/pype/plugins/nuke/publish/extract_ouput_node.py index 4d7533f010..a144761e5f 100644 --- a/pype/plugins/nuke/publish/extract_ouput_node.py +++ b/pype/plugins/nuke/publish/extract_ouput_node.py @@ -15,21 +15,17 @@ class CreateOutputNode(pyblish.api.ContextPlugin): def process(self, context): # capture selection state with maintained_selection(): - # deselect all allNodes - self.log.info(context.data["ActiveViewer"]) + active_node = [node for inst in context[:] + for node in inst[:] + if "ak:family" in node.knobs()] - active_viewer = context.data["ActiveViewer"] - active_input = active_viewer.activeInput() - active_node = active_viewer.node() - - - last_viewer_node = active_node.input(active_input) - - name = last_viewer_node.name() - self.log.info("Node name: {}".format(name)) + if active_node: + self.log.info(active_node) + active_node = active_node[0] + self.log.info(active_node) + active_node['selected'].setValue(True) # select only instance render node - last_viewer_node['selected'].setValue(True) output_node = nuke.createNode("Output") # deselect all and select the original selection diff --git a/pype/plugins/nuke/publish/extract_review_data.py b/pype/plugins/nuke/publish/extract_review_data.py index 40c3e37434..885fe99b3d 100644 --- a/pype/plugins/nuke/publish/extract_review_data.py +++ b/pype/plugins/nuke/publish/extract_review_data.py @@ -2,7 +2,7 @@ import os import nuke import pyblish.api import pype - +import copy class ExtractReviewData(pype.api.Extractor): """Extracts movie and thumbnail with baked in luts @@ -48,9 +48,9 @@ class ExtractReviewData(pype.api.Extractor): assert instance.data['representations'][0]['files'], "Instance data files should't be empty!" - import nuke temporary_nodes = [] - stagingDir = instance.data['representations'][0]["stagingDir"].replace("\\", "/") + stagingDir = instance.data[ + 'representations'][0]["stagingDir"].replace("\\", "/") self.log.debug("StagingDir `{0}`...".format(stagingDir)) collection = instance.data.get("collection", None) @@ -70,16 +70,24 @@ class ExtractReviewData(pype.api.Extractor): first_frame = instance.data.get("frameStart", None) last_frame = instance.data.get("frameEnd", None) - node = previous_node = nuke.createNode("Read") + rnode = nuke.createNode("Read") - node["file"].setValue( + rnode["file"].setValue( os.path.join(stagingDir, fname).replace("\\", "/")) - node["first"].setValue(first_frame) - node["origfirst"].setValue(first_frame) - node["last"].setValue(last_frame) - node["origlast"].setValue(last_frame) - temporary_nodes.append(node) + rnode["first"].setValue(first_frame) + rnode["origfirst"].setValue(first_frame) + rnode["last"].setValue(last_frame) + rnode["origlast"].setValue(last_frame) + temporary_nodes.append(rnode) + previous_node = rnode + + # get input process and connect it to baking + ipn = self.get_view_process_node() + if ipn is not None: + ipn.setInput(0, previous_node) + previous_node = ipn + temporary_nodes.append(ipn) reformat_node = nuke.createNode("Reformat") @@ -95,22 +103,10 @@ class ExtractReviewData(pype.api.Extractor): previous_node = reformat_node temporary_nodes.append(reformat_node) - viewer_process_node = instance.context.data.get("ViewerProcess") - dag_node = None - if viewer_process_node: - dag_node = nuke.createNode(viewer_process_node.Class()) - dag_node.setInput(0, previous_node) - previous_node = dag_node - temporary_nodes.append(dag_node) - # Copy viewer process values - excludedKnobs = ["name", "xpos", "ypos"] - for item in viewer_process_node.knobs().keys(): - if item not in excludedKnobs and item in dag_node.knobs(): - x1 = viewer_process_node[item] - x2 = dag_node[item] - x2.fromScript(x1.toScript(False)) - else: - self.log.warning("No viewer node found.") + dag_node = nuke.createNode("OCIODisplay") + dag_node.setInput(0, previous_node) + previous_node = dag_node + temporary_nodes.append(dag_node) # create write node write_node = nuke.createNode("Write") @@ -164,3 +160,28 @@ class ExtractReviewData(pype.api.Extractor): # Clean up for node in temporary_nodes: nuke.delete(node) + + def get_view_process_node(self): + + # Select only the target node + if nuke.selectedNodes(): + [n.setSelected(False) for n in nuke.selectedNodes()] + + for v in [n for n in nuke.allNodes() + if "Viewer" in n.Class()]: + ip = v['input_process'].getValue() + ipn = v['input_process_node'].getValue() + if "VIEWER_INPUT" not in ipn and ip: + ipn_orig = nuke.toNode(ipn) + ipn_orig.setSelected(True) + + if ipn_orig: + nuke.nodeCopy('%clipboard%') + + [n.setSelected(False) for n in nuke.selectedNodes()] # Deselect all + + nuke.nodePaste('%clipboard%') + + ipn = nuke.selectedNode() + + return ipn diff --git a/pype/plugins/nuke/publish/validate_rendered_frames.py b/pype/plugins/nuke/publish/validate_rendered_frames.py index 85cbe7b2c0..3887b5d5b7 100644 --- a/pype/plugins/nuke/publish/validate_rendered_frames.py +++ b/pype/plugins/nuke/publish/validate_rendered_frames.py @@ -81,3 +81,5 @@ class ValidateRenderedFrames(pyblish.api.InstancePlugin): ).format(__name__) instance.data['collection'] = collection + + return From 185e3c29a7157e3264db06c344fa12c0329c30b1 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 4 Oct 2019 16:45:45 +0200 Subject: [PATCH 21/23] fix: standalone publishing and image sequence had troubles --- pype/plugins/ftrack/publish/integrate_remove_components.py | 3 +++ pype/plugins/global/publish/integrate_new.py | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pype/plugins/ftrack/publish/integrate_remove_components.py b/pype/plugins/ftrack/publish/integrate_remove_components.py index a215ee1b97..bad50f7200 100644 --- a/pype/plugins/ftrack/publish/integrate_remove_components.py +++ b/pype/plugins/ftrack/publish/integrate_remove_components.py @@ -17,6 +17,9 @@ class IntegrateCleanComponentData(pyblish.api.InstancePlugin): for comp in instance.data['representations']: self.log.debug('component {}'.format(comp)) + + if "%" in comp['published_path'] or "#" in comp['published_path']: + continue if comp.get('thumbnail') or ("thumbnail" in comp.get('tags', [])): os.remove(comp['published_path']) diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index 6c89e22a83..61881b2a34 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -307,7 +307,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): if repre.get("frameStart"): frame_start_padding = len(str( repre.get("frameEnd"))) - index_frame_start = repre.get("frameStart") + index_frame_start = int(repre.get("frameStart")) dst_padding_exp = src_padding_exp for i in src_collection.indexes: @@ -322,7 +322,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): dst_padding = dst_padding_exp % index_frame_start index_frame_start += 1 - dst = "{0}{1}{2}".format(dst_head, dst_padding, dst_tail) + dst = "{0}{1}{2}".format(dst_head, dst_padding, dst_tail).replace("..", ".") self.log.debug("destination: `{}`".format(dst)) src = os.path.join(stagingdir, src_file_name) self.log.debug("source: {}".format(src)) @@ -357,7 +357,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): src = os.path.join(stagingdir, fname) anatomy_filled = anatomy.format(template_data) dst = os.path.normpath( - anatomy_filled[template_name]["path"]) + anatomy_filled[template_name]["path"]).replace("..", ".") instance.data["transfers"].append([src, dst]) From 776e8922bcff08fe37453173dfaea82126a571aa Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 4 Oct 2019 16:50:18 +0200 Subject: [PATCH 22/23] fix: unnecessary import of module --- pype/plugins/nuke/publish/extract_review_data.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pype/plugins/nuke/publish/extract_review_data.py b/pype/plugins/nuke/publish/extract_review_data.py index 885fe99b3d..08eba5bb1e 100644 --- a/pype/plugins/nuke/publish/extract_review_data.py +++ b/pype/plugins/nuke/publish/extract_review_data.py @@ -2,7 +2,6 @@ import os import nuke import pyblish.api import pype -import copy class ExtractReviewData(pype.api.Extractor): """Extracts movie and thumbnail with baked in luts From 97673d503ccdd70e5b991ebbbf144a5c836aa80e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 7 Oct 2019 12:32:35 +0200 Subject: [PATCH 23/23] fix(standalonepublish): getting `frameStart` and `frameEnd` from representaions --- pype/plugins/global/publish/collect_context.py | 2 ++ pype/plugins/global/publish/extract_thumbnail_sa.py | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/pype/plugins/global/publish/collect_context.py b/pype/plugins/global/publish/collect_context.py index 61c3bcf4d8..f538509a9b 100644 --- a/pype/plugins/global/publish/collect_context.py +++ b/pype/plugins/global/publish/collect_context.py @@ -66,6 +66,8 @@ class CollectContextDataSAPublish(pyblish.api.ContextPlugin): "label": subset, "name": subset, "family": family, + "frameStart": in_data.get("representations", [None])[0].get("frameStart", None), + "frameEnd": in_data.get("representations", [None])[0].get("frameEnd", None), "families": [family, 'ftrack'], }) self.log.info("collected instance: {}".format(instance.data)) diff --git a/pype/plugins/global/publish/extract_thumbnail_sa.py b/pype/plugins/global/publish/extract_thumbnail_sa.py index 7e31e3c701..f42985b560 100644 --- a/pype/plugins/global/publish/extract_thumbnail_sa.py +++ b/pype/plugins/global/publish/extract_thumbnail_sa.py @@ -119,8 +119,8 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): "tags": [] } - # add Delete tag when temp file was rendered - if not is_jpeg: - representation["tags"].append("delete") + # # add Delete tag when temp file was rendered + # if not is_jpeg: + # representation["tags"].append("delete") instance.data["representations"].append(representation)