From bed02e694d0372fbe5c52935e61fe4b4ab4a3f6d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 20 Feb 2020 11:44:09 +0100 Subject: [PATCH 01/55] feat(nks): adding functions for loading --- pype/nukestudio/lib.py | 184 +++++++++++++++++++++++++++++++ pype/nukestudio/precomp_clip.py | 188 -------------------------------- 2 files changed, 184 insertions(+), 188 deletions(-) delete mode 100644 pype/nukestudio/precomp_clip.py diff --git a/pype/nukestudio/lib.py b/pype/nukestudio/lib.py index c71e2cb999..04139d7416 100644 --- a/pype/nukestudio/lib.py +++ b/pype/nukestudio/lib.py @@ -1,4 +1,5 @@ import os +import re import sys import hiero import pyblish.api @@ -361,3 +362,186 @@ def CreateNukeWorkfile(nodes=None, nodes=nuke_script.getNodes(), **kwargs ) + + +def create_nk_workfile_clips(nk_workfiles, seq=None): + ''' + nk_workfile is list of dictionaries like: + [{ + 'path': 'P:/Jakub_testy_pipeline/test_v01.nk', + 'name': 'test', + 'handleStart': 15, # added asymetrically to handles + 'handleEnd': 10, # added asymetrically to handles + "clipIn": 16, + "frameStart": 991, + "frameEnd": 1023, + 'task': 'Comp-tracking', + 'work_dir': 'VFX_PR', + 'shot': '00010' + }] + ''' + + proj = hiero.core.projects()[-1] + root = proj.clipsBin() + + if not seq: + seq = hiero.core.Sequence('NewSequences') + root.addItem(hiero.core.BinItem(seq)) + # todo will ned to define this better + # track = seq[1] # lazy example to get a destination# track + clips_lst = [] + for nk in nk_workfiles: + task_path = '/'.join([nk['work_dir'], nk['shot'], nk['task']]) + bin = create_bin_in_project(task_path, proj) + + if nk['task'] not in seq.videoTracks(): + track = hiero.core.VideoTrack(nk['task']) + seq.addTrack(track) + else: + track = seq.tracks(nk['task']) + + # create slip media + print("__ path: `{}`".format(nk['path'])) + + media = hiero.core.MediaSource(nk['path']) + media_in = int(media.startTime() or 0) + media_duration = int(media.duration() or 0) + + handle_start = nk.get("handleStart") + handle_end = nk.get("handleEnd") + + if media_in: + source_in = media_in + handle_start + else: + source_in = nk["frameStart"] + handle_start + + if media_duration: + source_out = (media_in + media_duration - 1) - handle_end + else: + source_out = nk["frameEnd"] - handle_end + + print("__ media: `{}`".format(media)) + print("__ media_in: `{}`".format(media_in)) + print("__ media_duration : `{}`".format(media_duration)) + print("__ source_in: `{}`".format(source_in)) + print("__ source_out : `{}`".format(source_out)) + + source = hiero.core.Clip(media) + print("__ source : `{}`".format(source)) + print("__ source.sourceIn(): `{}`".format(source.sourceIn())) + + name = os.path.basename(os.path.splitext(nk['path'])[0]) + split_name = split_by_client_version(name)[0] or name + + print("__ split_name: `{}`".format(split_name)) + + # add to bin as clip item + items_in_bin = [b.name() for b in bin.items()] + if split_name not in items_in_bin: + binItem = hiero.core.BinItem(source) + bin.addItem(binItem) + + print("__ bin.items(): `{}`".format(bin.items())) + + new_source = [ + item for item in bin.items() if split_name in item.name() + ][0].items()[0].item() + + print("__ new_source: `{}`".format(new_source)) + print("__ new_source: `{}`".format(new_source)) + + # add to track as clip item + trackItem = hiero.core.TrackItem( + split_name, hiero.core.TrackItem.kVideo) + trackItem.setSource(new_source) + trackItem.setSourceIn(source_in) + trackItem.setSourceOut(source_out) + trackItem.setSourceIn(source_in) + trackItem.setTimelineIn(nk["clipIn"]) + trackItem.setTimelineOut(nk["clipIn"] + (source_out - source_in)) + track.addTrackItem(trackItem) + track.addTrackItem(trackItem) + clips_lst.append(trackItem) + + return clips_lst + + +def create_bin_in_project(bin_name='', project=''): + ''' + create bin in project and + if the bin_name is "bin1/bin2/bin3" it will create whole depth + ''' + + if not project: + # get the first loaded project + project = hiero.core.projects()[-1] + if not bin_name: + return None + if '/' in bin_name: + bin_name = bin_name.split('/') + else: + bin_name = [bin_name] + + clipsBin = project.clipsBin() + + done_bin_lst = [] + for i, b in enumerate(bin_name): + if i == 0 and len(bin_name) > 1: + if b in [bin.name() for bin in clipsBin.bins()]: + bin = [bin for bin in clipsBin.bins() if b in bin.name()][0] + done_bin_lst.append(bin) + else: + create_bin = hiero.core.Bin(b) + clipsBin.addItem(create_bin) + done_bin_lst.append(create_bin) + + elif i >= 1 and i < len(bin_name) - 1: + if b in [bin.name() for bin in done_bin_lst[i - 1].bins()]: + bin = [ + bin for bin in done_bin_lst[i - 1].bins() + if b in bin.name() + ][0] + done_bin_lst.append(bin) + else: + create_bin = hiero.core.Bin(b) + done_bin_lst[i - 1].addItem(create_bin) + done_bin_lst.append(create_bin) + + elif i == len(bin_name) - 1: + if b in [bin.name() for bin in done_bin_lst[i - 1].bins()]: + bin = [ + bin for bin in done_bin_lst[i - 1].bins() + if b in bin.name() + ][0] + done_bin_lst.append(bin) + else: + create_bin = hiero.core.Bin(b) + done_bin_lst[i - 1].addItem(create_bin) + done_bin_lst.append(create_bin) + # print [bin.name() for bin in clipsBin.bins()] + return done_bin_lst[-1] + + +def split_by_client_version(string): + regex = r"[/_.]v\d+" + try: + matches = re.findall(regex, string, re.IGNORECASE) + return string.split(matches[0]) + except Exception as e: + print(e) + return None + + +# nk_workfiles = [{ +# 'path': 'C:/Users/hubert/_PYPE_testing/projects/D001_projectx/episodes/ep120/ep120sq01/120sh020/publish/plates/platesMain/v023/prjx_120sh020_platesMain_v023.nk', +# 'name': '120sh020_platesMain', +# 'handles': 10, +# 'handleStart': 10, +# 'handleEnd': 10, +# "clipIn": 16, +# "frameStart": 991, +# "frameEnd": 1023, +# 'task': 'platesMain', +# 'work_dir': 'shots', +# 'shot': '120sh020' +# }] diff --git a/pype/nukestudio/precomp_clip.py b/pype/nukestudio/precomp_clip.py deleted file mode 100644 index b544b6e654..0000000000 --- a/pype/nukestudio/precomp_clip.py +++ /dev/null @@ -1,188 +0,0 @@ -import hiero.core -import hiero.ui - -import re -import os - - -def create_nk_script_clips(script_lst, seq=None): - ''' - nk_scripts is list of dictionaries like: - [{ - 'path': 'P:/Jakub_testy_pipeline/test_v01.nk', - 'name': 'test', - 'handles': 10, - 'handleStart': 15, # added asymetrically to handles - 'handleEnd': 10, # added asymetrically to handles - "clipIn": 16, - "frameStart": 991, - "frameEnd": 1023, - 'task': 'Comp-tracking', - 'work_dir': 'VFX_PR', - 'shot': '00010' - }] - ''' - - proj = hiero.core.projects()[-1] - root = proj.clipsBin() - - if not seq: - seq = hiero.core.Sequence('NewSequences') - root.addItem(hiero.core.BinItem(seq)) - # todo will ned to define this better - # track = seq[1] # lazy example to get a destination# track - clips_lst = [] - for nk in script_lst: - task_path = '/'.join([nk['work_dir'], nk['shot'], nk['task']]) - bin = create_bin_in_project(task_path, proj) - - if nk['task'] not in seq.videoTracks(): - track = hiero.core.VideoTrack(nk['task']) - seq.addTrack(track) - else: - track = seq.tracks(nk['task']) - - # create slip media - print("__ path: `{}`".format(nk['path'])) - - media = hiero.core.MediaSource(nk['path']) - media_in = int(media.startTime() or 0) - media_duration = int(media.duration() or 0) - - handle_start = nk.get("handleStart") or nk['handles'] - handle_end = nk.get("handleEnd") or nk['handles'] - - if media_in: - source_in = media_in + handle_start - else: - source_in = nk["frameStart"] + handle_start - - if media_duration: - source_out = (media_in + media_duration - 1) - handle_end - else: - source_out = nk["frameEnd"] - handle_end - - print("__ media: `{}`".format(media)) - print("__ media_in: `{}`".format(media_in)) - print("__ media_duration : `{}`".format(media_duration)) - print("__ source_in: `{}`".format(source_in)) - print("__ source_out : `{}`".format(source_out)) - - source = hiero.core.Clip(media) - print("__ source : `{}`".format(source)) - print("__ source.sourceIn(): `{}`".format(source.sourceIn())) - - name = os.path.basename(os.path.splitext(nk['path'])[0]) - split_name = split_by_client_version(name)[0] or name - - print("__ split_name: `{}`".format(split_name)) - - # add to bin as clip item - items_in_bin = [b.name() for b in bin.items()] - if split_name not in items_in_bin: - binItem = hiero.core.BinItem(source) - bin.addItem(binItem) - - print("__ bin.items(): `{}`".format(bin.items())) - - new_source = [ - item for item in bin.items() if split_name in item.name() - ][0].items()[0].item() - - print("__ new_source: `{}`".format(new_source)) - print("__ new_source: `{}`".format(new_source)) - - # add to track as clip item - trackItem = hiero.core.TrackItem(split_name, hiero.core.TrackItem.kVideo) - trackItem.setSource(new_source) - trackItem.setSourceIn(source_in) - trackItem.setSourceOut(source_out) - trackItem.setSourceIn(source_in) - trackItem.setTimelineIn(nk["clipIn"]) - trackItem.setTimelineOut(nk["clipIn"] + (source_out - source_in)) - track.addTrackItem(trackItem) - track.addTrackItem(trackItem) - clips_lst.append(trackItem) - - return clips_lst - - -def create_bin_in_project(bin_name='', project=''): - ''' - create bin in project and - if the bin_name is "bin1/bin2/bin3" it will create whole depth - ''' - - if not project: - # get the first loaded project - project = hiero.core.projects()[-1] - if not bin_name: - return None - if '/' in bin_name: - bin_name = bin_name.split('/') - else: - bin_name = [bin_name] - - clipsBin = project.clipsBin() - - done_bin_lst = [] - for i, b in enumerate(bin_name): - if i == 0 and len(bin_name) > 1: - if b in [bin.name() for bin in clipsBin.bins()]: - bin = [bin for bin in clipsBin.bins() if b in bin.name()][0] - done_bin_lst.append(bin) - else: - create_bin = hiero.core.Bin(b) - clipsBin.addItem(create_bin) - done_bin_lst.append(create_bin) - - elif i >= 1 and i < len(bin_name) - 1: - if b in [bin.name() for bin in done_bin_lst[i - 1].bins()]: - bin = [ - bin for bin in done_bin_lst[i - 1].bins() - if b in bin.name() - ][0] - done_bin_lst.append(bin) - else: - create_bin = hiero.core.Bin(b) - done_bin_lst[i - 1].addItem(create_bin) - done_bin_lst.append(create_bin) - - elif i == len(bin_name) - 1: - if b in [bin.name() for bin in done_bin_lst[i - 1].bins()]: - bin = [ - bin for bin in done_bin_lst[i - 1].bins() - if b in bin.name() - ][0] - done_bin_lst.append(bin) - else: - create_bin = hiero.core.Bin(b) - done_bin_lst[i - 1].addItem(create_bin) - done_bin_lst.append(create_bin) - # print [bin.name() for bin in clipsBin.bins()] - return done_bin_lst[-1] - - -def split_by_client_version(string): - regex = r"[/_.]v\d+" - try: - matches = re.findall(regex, string, re.IGNORECASE) - return string.split(matches[0]) - except Exception as e: - print(e) - return None - - -script_lst = [{ - 'path': 'C:/Users/hubert/_PYPE_testing/projects/D001_projectx/episodes/ep120/ep120sq01/120sh020/publish/plates/platesMain/v023/prjx_120sh020_platesMain_v023.nk', - 'name': '120sh020_platesMain', - 'handles': 10, - 'handleStart': 10, - 'handleEnd': 10, - "clipIn": 16, - "frameStart": 991, - "frameEnd": 1023, - 'task': 'platesMain', - 'work_dir': 'shots', - 'shot': '120sh020' -}] From e33727fcbfd603aeb5c639ff5f90e20b3354e14f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 20 Feb 2020 17:14:49 +0100 Subject: [PATCH 02/55] feat(nks): loading workflow wip --- pype/nukestudio/lib.py | 92 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 91 insertions(+), 1 deletion(-) diff --git a/pype/nukestudio/lib.py b/pype/nukestudio/lib.py index 04139d7416..3115428d8c 100644 --- a/pype/nukestudio/lib.py +++ b/pype/nukestudio/lib.py @@ -8,7 +8,6 @@ from avalon.vendor.Qt import (QtWidgets, QtGui) import pype.api as pype from pypeapp import Logger - log = Logger().get_logger(__name__, "nukestudio") cached_process = None @@ -364,6 +363,97 @@ def CreateNukeWorkfile(nodes=None, ) +class ClipsLoader: + + def __init__(self, representations, **kwargs): + """ Initialize object + + Arguments: + hiero_workfile_name (str): name of workfile + representations (dict): representations for processing + example: {assetName_subsetName_representationName: { + "_id": ObjectId("5as5d54fa56dfa56s6d56asddf4as"), + "path": "path/to/file/created/by/get_repr..", + "binPath": "projectBinPath", + "context": { + "subset": "subsetName", + "task": "taskName", + "family": "write", + "hierarchy": "parent/subparent", + "frame": "0996", + "project": { + "code": "j01test", + "name": "J01_jakub_test" + }, + "version": 1, + "asset": "assetName", + "representation": "representationName", + "root": "projectsRootPath" + } + } + } + """ + self.representations = representations + self.kwargs = kwargs + self.active_project = self.get_active_project() + self.project_bin = self.active_project.clipsBin() + + # inject asset data to representation dict + self.get_asset_data() + + def get_active_project(self): + """ Get hiero active project object + """ + fname = self.kwargs.get("hiero_workfile_name", "") + + return next((p for p in hiero.core.projects() + if fname in p.name()), + hiero.core.projects()[-1]) + + def get_asset_data(self): + """ Get all available asset data + + joint `data` key with asset.data dict into the representaion + + """ + for name, data in self.representations.items(): + asset_name = data["context"]["asset"] + data["data"] = pype.get_asset(asset_name)["data"] + + def make_project_bin(self, hierarchy): + """ Creare bins by given hierarchy path + + It will also make sure no duplicit bins will be created + + Arguments: + hierarchy (str): path devided by slashes "bin0/bin1/bin2" + + Returns: + bin (hiero.core.BinItem): with the bin to be used for mediaItem + """ + pass + + def make_track_item(self): + """ Create track item with """ + pass + + def set_clip_color(self, last_version=True): + """ Sets color of clip on clip/track item + + Arguments: + last_version (bool): True = green | False = red + """ + pass + + def set_container_tag(self, item, metadata): + """ Sets container tag to given clip/track item + + Arguments: + item (hiero.core.BinItem or hiero.core.TrackItem) + metadata (dict): data to be added to tag + """ + pass + def create_nk_workfile_clips(nk_workfiles, seq=None): ''' nk_workfile is list of dictionaries like: From 8eb6ad4fe68a19bfcd55d7700f54ac74848d2c89 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 20 Feb 2020 17:15:12 +0100 Subject: [PATCH 03/55] feat(nks): adding loader plugin --- pype/plugins/nukestudio/load/load_sequence.py | 343 ++++++++++++++++++ 1 file changed, 343 insertions(+) create mode 100644 pype/plugins/nukestudio/load/load_sequence.py diff --git a/pype/plugins/nukestudio/load/load_sequence.py b/pype/plugins/nukestudio/load/load_sequence.py new file mode 100644 index 0000000000..e3637d4aed --- /dev/null +++ b/pype/plugins/nukestudio/load/load_sequence.py @@ -0,0 +1,343 @@ +import re +import nuke +import contextlib + +from avalon import api, io +from pype.nuke import presets + + +@contextlib.contextmanager +def preserve_trim(node): + """Preserve the relative trim of the Loader tool. + + This tries to preserve the loader's trim (trim in and trim out) after + the context by reapplying the "amount" it trims on the clip's length at + start and end. + + """ + # working script frame range + script_start = nuke.root()["first_frame"].value() + + start_at_frame = None + offset_frame = None + if node['frame_mode'].value() == "start at": + start_at_frame = node['frame'].value() + if node['frame_mode'].value() == "offset": + offset_frame = node['frame'].value() + + try: + yield + finally: + if start_at_frame: + node['frame_mode'].setValue("start at") + node['frame'].setValue(str(script_start)) + print("start frame of Read was set to" + "{}".format(script_start)) + + if offset_frame: + node['frame_mode'].setValue("offset") + node['frame'].setValue(str((script_start + offset_frame))) + print("start frame of Read was set to" + "{}".format(script_start)) + + +def loader_shift(node, frame, relative=True): + """Shift global in time by i preserving duration + + This moves the loader by i frames preserving global duration. When relative + is False it will shift the global in to the start frame. + + Args: + loader (tool): The fusion loader tool. + frame (int): The amount of frames to move. + relative (bool): When True the shift is relative, else the shift will + change the global in to frame. + + Returns: + int: The resulting relative frame change (how much it moved) + + """ + # working script frame range + script_start = nuke.root()["first_frame"].value() + + if relative: + node['frame_mode'].setValue("start at") + node['frame'].setValue(str(frame)) + + return int(script_start) + + +class LoadSequence(api.Loader): + """Load image sequence into Nuke""" + + families = ["render2d", "source", "plate", "render"] + representations = ["exr", "dpx", "jpg", "jpeg", "png"] + + label = "Load sequence" + order = -10 + icon = "code-fork" + color = "orange" + + def load(self, context, name, namespace, data): + from avalon.nuke import ( + containerise, + viewer_update_and_undo_stop + ) + + version = context['version'] + version_data = version.get("data", {}) + import logging + self.log.setLevel(logging.DEBUG) + self.log.info("version_data: {}\n".format(version_data)) + self.log.error("version_data: {}\n".format(version_data)) + self.log.debug("__ context: {}\n".format(context)) + self.log.warning( + "__ representation: {}\n".format(context["representation"])) + + self.first_frame = int(nuke.root()["first_frame"].getValue()) + self.handle_start = version_data.get("handleStart", 0) + self.handle_end = version_data.get("handleEnd", 0) + + first = version_data.get("frameStart", None) + last = version_data.get("frameEnd", None) + + # Fallback to asset name when namespace is None + if namespace is None: + namespace = context['asset']['name'] + + first -= self.handle_start + last += self.handle_end + + file = self.fname + + if not file: + repr_id = context["representation"]["_id"] + self.log.warning( + "Representation id `{}` is failing to load".format(repr_id)) + return + + file = file.replace("\\", "/") + + repr_cont = context["representation"]["context"] + if "#" not in file: + frame = repr_cont.get("frame") + padding = len(frame) + file = file.replace(frame, "#"*padding) + + read_name = "Read_{0}_{1}_{2}".format( + repr_cont["asset"], + repr_cont["subset"], + repr_cont["representation"]) + + # Create the Loader with the filename path set + with viewer_update_and_undo_stop(): + # TODO: it might be universal read to img/geo/camera + r = nuke.createNode( + "Read", + "name {}".format(read_name)) + r["file"].setValue(file) + + # Set colorspace defined in version data + colorspace = context["version"]["data"].get("colorspace") + if colorspace: + r["colorspace"].setValue(str(colorspace)) + + # load nuke presets for Read's colorspace + read_clrs_presets = presets.get_colorspace_preset().get( + "nuke", {}).get("read", {}) + + # check if any colorspace presets for read is mathing + preset_clrsp = next((read_clrs_presets[k] + for k in read_clrs_presets + if bool(re.search(k, file))), + None) + if preset_clrsp is not None: + r["colorspace"].setValue(str(preset_clrsp)) + + loader_shift(r, first, relative=True) + r["origfirst"].setValue(int(first)) + r["first"].setValue(int(first)) + r["origlast"].setValue(int(last)) + r["last"].setValue(int(last)) + + # add additional metadata from the version to imprint Avalon knob + add_keys = ["frameStart", "frameEnd", + "source", "colorspace", "author", "fps", "version", + "handleStart", "handleEnd"] + + data_imprint = {} + for k in add_keys: + if k == 'version': + data_imprint.update({k: context["version"]['name']}) + else: + data_imprint.update( + {k: context["version"]['data'].get(k, str(None))}) + + data_imprint.update({"objectName": read_name}) + + r["tile_color"].setValue(int("0x4ecd25ff", 16)) + + if version_data.get("retime", None): + speed = version_data.get("speed", 1) + time_warp_nodes = version_data.get("timewarps", []) + self.make_retimes(r, speed, time_warp_nodes) + + return containerise(r, + name=name, + namespace=namespace, + context=context, + loader=self.__class__.__name__, + data=data_imprint) + + def make_retimes(self, node, speed, time_warp_nodes): + ''' Create all retime and timewarping nodes with coppied animation ''' + if speed != 1: + rtn = nuke.createNode( + "Retime", + "speed {}".format(speed)) + rtn["before"].setValue("continue") + rtn["after"].setValue("continue") + rtn["input.first_lock"].setValue(True) + rtn["input.first"].setValue( + self.handle_start + self.first_frame + ) + + if time_warp_nodes != []: + for timewarp in time_warp_nodes: + twn = nuke.createNode(timewarp["Class"], + "name {}".format(timewarp["name"])) + if isinstance(timewarp["lookup"], list): + # if array for animation + twn["lookup"].setAnimated() + for i, value in enumerate(timewarp["lookup"]): + twn["lookup"].setValueAt( + (self.first_frame + i) + value, + (self.first_frame + i)) + else: + # if static value `int` + twn["lookup"].setValue(timewarp["lookup"]) + + def switch(self, container, representation): + self.update(container, representation) + + 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 + ) + + node = nuke.toNode(container['objectName']) + + assert node.Class() == "Read", "Must be Read" + + repr_cont = representation["context"] + + file = self.fname + + if not file: + repr_id = representation["_id"] + self.log.warning( + "Representation id `{}` is failing to load".format(repr_id)) + return + + file = file.replace("\\", "/") + + if "#" not in file: + frame = repr_cont.get("frame") + padding = len(frame) + file = file.replace(frame, "#"*padding) + + # Get start frame from version data + version = io.find_one({ + "type": "version", + "_id": representation["parent"] + }) + + # get all versions in list + versions = io.find({ + "type": "version", + "parent": version["parent"] + }).distinct('name') + + max_version = max(versions) + + version_data = version.get("data", {}) + + self.first_frame = int(nuke.root()["first_frame"].getValue()) + self.handle_start = version_data.get("handleStart", 0) + self.handle_end = version_data.get("handleEnd", 0) + + first = version_data.get("frameStart") + last = version_data.get("frameEnd") + + if first is None: + self.log.warning("Missing start frame for updated version" + "assuming starts at frame 0 for: " + "{} ({})".format( + node['name'].value(), representation)) + first = 0 + + first -= self.handle_start + last += self.handle_end + + # Update the loader's path whilst preserving some values + with preserve_trim(node): + node["file"].setValue(file) + self.log.info("__ node['file']: {}".format(node["file"].value())) + + # Set the global in to the start frame of the sequence + loader_shift(node, first, relative=True) + node["origfirst"].setValue(int(first)) + node["first"].setValue(int(first)) + node["origlast"].setValue(int(last)) + node["last"].setValue(int(last)) + + updated_dict = {} + updated_dict.update({ + "representation": str(representation["_id"]), + "frameStart": str(first), + "frameEnd": str(last), + "version": str(version.get("name")), + "colorspace": version_data.get("colorspace"), + "source": version_data.get("source"), + "handleStart": str(self.handle_start), + "handleEnd": str(self.handle_end), + "fps": str(version_data.get("fps")), + "author": version_data.get("author"), + "outputDir": version_data.get("outputDir"), + }) + + # change color of node + if version.get("name") not in [max_version]: + node["tile_color"].setValue(int("0xd84f20ff", 16)) + else: + node["tile_color"].setValue(int("0x4ecd25ff", 16)) + + if version_data.get("retime", None): + speed = version_data.get("speed", 1) + time_warp_nodes = version_data.get("timewarps", []) + self.make_retimes(node, speed, time_warp_nodes) + + # Update the imprinted representation + update_container( + node, + updated_dict + ) + self.log.info("udated to version: {}".format(version.get("name"))) + + def remove(self, container): + + from avalon.nuke import viewer_update_and_undo_stop + + node = nuke.toNode(container['objectName']) + assert node.Class() == "Read", "Must be Read" + + with viewer_update_and_undo_stop(): + nuke.delete(node) From c4995356d3ad33dc9aee21ced89ee95f8a1d6fc2 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 20 Feb 2020 18:19:03 +0100 Subject: [PATCH 04/55] feat(nks): initial plugin for loading sequence to timeline --- pype/plugins/nukestudio/load/load_sequence.py | 343 ------------------ ...load_sequences_to_timeline_asset_origin.py | 33 ++ 2 files changed, 33 insertions(+), 343 deletions(-) delete mode 100644 pype/plugins/nukestudio/load/load_sequence.py create mode 100644 pype/plugins/nukestudio/load/load_sequences_to_timeline_asset_origin.py diff --git a/pype/plugins/nukestudio/load/load_sequence.py b/pype/plugins/nukestudio/load/load_sequence.py deleted file mode 100644 index e3637d4aed..0000000000 --- a/pype/plugins/nukestudio/load/load_sequence.py +++ /dev/null @@ -1,343 +0,0 @@ -import re -import nuke -import contextlib - -from avalon import api, io -from pype.nuke import presets - - -@contextlib.contextmanager -def preserve_trim(node): - """Preserve the relative trim of the Loader tool. - - This tries to preserve the loader's trim (trim in and trim out) after - the context by reapplying the "amount" it trims on the clip's length at - start and end. - - """ - # working script frame range - script_start = nuke.root()["first_frame"].value() - - start_at_frame = None - offset_frame = None - if node['frame_mode'].value() == "start at": - start_at_frame = node['frame'].value() - if node['frame_mode'].value() == "offset": - offset_frame = node['frame'].value() - - try: - yield - finally: - if start_at_frame: - node['frame_mode'].setValue("start at") - node['frame'].setValue(str(script_start)) - print("start frame of Read was set to" - "{}".format(script_start)) - - if offset_frame: - node['frame_mode'].setValue("offset") - node['frame'].setValue(str((script_start + offset_frame))) - print("start frame of Read was set to" - "{}".format(script_start)) - - -def loader_shift(node, frame, relative=True): - """Shift global in time by i preserving duration - - This moves the loader by i frames preserving global duration. When relative - is False it will shift the global in to the start frame. - - Args: - loader (tool): The fusion loader tool. - frame (int): The amount of frames to move. - relative (bool): When True the shift is relative, else the shift will - change the global in to frame. - - Returns: - int: The resulting relative frame change (how much it moved) - - """ - # working script frame range - script_start = nuke.root()["first_frame"].value() - - if relative: - node['frame_mode'].setValue("start at") - node['frame'].setValue(str(frame)) - - return int(script_start) - - -class LoadSequence(api.Loader): - """Load image sequence into Nuke""" - - families = ["render2d", "source", "plate", "render"] - representations = ["exr", "dpx", "jpg", "jpeg", "png"] - - label = "Load sequence" - order = -10 - icon = "code-fork" - color = "orange" - - def load(self, context, name, namespace, data): - from avalon.nuke import ( - containerise, - viewer_update_and_undo_stop - ) - - version = context['version'] - version_data = version.get("data", {}) - import logging - self.log.setLevel(logging.DEBUG) - self.log.info("version_data: {}\n".format(version_data)) - self.log.error("version_data: {}\n".format(version_data)) - self.log.debug("__ context: {}\n".format(context)) - self.log.warning( - "__ representation: {}\n".format(context["representation"])) - - self.first_frame = int(nuke.root()["first_frame"].getValue()) - self.handle_start = version_data.get("handleStart", 0) - self.handle_end = version_data.get("handleEnd", 0) - - first = version_data.get("frameStart", None) - last = version_data.get("frameEnd", None) - - # Fallback to asset name when namespace is None - if namespace is None: - namespace = context['asset']['name'] - - first -= self.handle_start - last += self.handle_end - - file = self.fname - - if not file: - repr_id = context["representation"]["_id"] - self.log.warning( - "Representation id `{}` is failing to load".format(repr_id)) - return - - file = file.replace("\\", "/") - - repr_cont = context["representation"]["context"] - if "#" not in file: - frame = repr_cont.get("frame") - padding = len(frame) - file = file.replace(frame, "#"*padding) - - read_name = "Read_{0}_{1}_{2}".format( - repr_cont["asset"], - repr_cont["subset"], - repr_cont["representation"]) - - # Create the Loader with the filename path set - with viewer_update_and_undo_stop(): - # TODO: it might be universal read to img/geo/camera - r = nuke.createNode( - "Read", - "name {}".format(read_name)) - r["file"].setValue(file) - - # Set colorspace defined in version data - colorspace = context["version"]["data"].get("colorspace") - if colorspace: - r["colorspace"].setValue(str(colorspace)) - - # load nuke presets for Read's colorspace - read_clrs_presets = presets.get_colorspace_preset().get( - "nuke", {}).get("read", {}) - - # check if any colorspace presets for read is mathing - preset_clrsp = next((read_clrs_presets[k] - for k in read_clrs_presets - if bool(re.search(k, file))), - None) - if preset_clrsp is not None: - r["colorspace"].setValue(str(preset_clrsp)) - - loader_shift(r, first, relative=True) - r["origfirst"].setValue(int(first)) - r["first"].setValue(int(first)) - r["origlast"].setValue(int(last)) - r["last"].setValue(int(last)) - - # add additional metadata from the version to imprint Avalon knob - add_keys = ["frameStart", "frameEnd", - "source", "colorspace", "author", "fps", "version", - "handleStart", "handleEnd"] - - data_imprint = {} - for k in add_keys: - if k == 'version': - data_imprint.update({k: context["version"]['name']}) - else: - data_imprint.update( - {k: context["version"]['data'].get(k, str(None))}) - - data_imprint.update({"objectName": read_name}) - - r["tile_color"].setValue(int("0x4ecd25ff", 16)) - - if version_data.get("retime", None): - speed = version_data.get("speed", 1) - time_warp_nodes = version_data.get("timewarps", []) - self.make_retimes(r, speed, time_warp_nodes) - - return containerise(r, - name=name, - namespace=namespace, - context=context, - loader=self.__class__.__name__, - data=data_imprint) - - def make_retimes(self, node, speed, time_warp_nodes): - ''' Create all retime and timewarping nodes with coppied animation ''' - if speed != 1: - rtn = nuke.createNode( - "Retime", - "speed {}".format(speed)) - rtn["before"].setValue("continue") - rtn["after"].setValue("continue") - rtn["input.first_lock"].setValue(True) - rtn["input.first"].setValue( - self.handle_start + self.first_frame - ) - - if time_warp_nodes != []: - for timewarp in time_warp_nodes: - twn = nuke.createNode(timewarp["Class"], - "name {}".format(timewarp["name"])) - if isinstance(timewarp["lookup"], list): - # if array for animation - twn["lookup"].setAnimated() - for i, value in enumerate(timewarp["lookup"]): - twn["lookup"].setValueAt( - (self.first_frame + i) + value, - (self.first_frame + i)) - else: - # if static value `int` - twn["lookup"].setValue(timewarp["lookup"]) - - def switch(self, container, representation): - self.update(container, representation) - - 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 - ) - - node = nuke.toNode(container['objectName']) - - assert node.Class() == "Read", "Must be Read" - - repr_cont = representation["context"] - - file = self.fname - - if not file: - repr_id = representation["_id"] - self.log.warning( - "Representation id `{}` is failing to load".format(repr_id)) - return - - file = file.replace("\\", "/") - - if "#" not in file: - frame = repr_cont.get("frame") - padding = len(frame) - file = file.replace(frame, "#"*padding) - - # Get start frame from version data - version = io.find_one({ - "type": "version", - "_id": representation["parent"] - }) - - # get all versions in list - versions = io.find({ - "type": "version", - "parent": version["parent"] - }).distinct('name') - - max_version = max(versions) - - version_data = version.get("data", {}) - - self.first_frame = int(nuke.root()["first_frame"].getValue()) - self.handle_start = version_data.get("handleStart", 0) - self.handle_end = version_data.get("handleEnd", 0) - - first = version_data.get("frameStart") - last = version_data.get("frameEnd") - - if first is None: - self.log.warning("Missing start frame for updated version" - "assuming starts at frame 0 for: " - "{} ({})".format( - node['name'].value(), representation)) - first = 0 - - first -= self.handle_start - last += self.handle_end - - # Update the loader's path whilst preserving some values - with preserve_trim(node): - node["file"].setValue(file) - self.log.info("__ node['file']: {}".format(node["file"].value())) - - # Set the global in to the start frame of the sequence - loader_shift(node, first, relative=True) - node["origfirst"].setValue(int(first)) - node["first"].setValue(int(first)) - node["origlast"].setValue(int(last)) - node["last"].setValue(int(last)) - - updated_dict = {} - updated_dict.update({ - "representation": str(representation["_id"]), - "frameStart": str(first), - "frameEnd": str(last), - "version": str(version.get("name")), - "colorspace": version_data.get("colorspace"), - "source": version_data.get("source"), - "handleStart": str(self.handle_start), - "handleEnd": str(self.handle_end), - "fps": str(version_data.get("fps")), - "author": version_data.get("author"), - "outputDir": version_data.get("outputDir"), - }) - - # change color of node - if version.get("name") not in [max_version]: - node["tile_color"].setValue(int("0xd84f20ff", 16)) - else: - node["tile_color"].setValue(int("0x4ecd25ff", 16)) - - if version_data.get("retime", None): - speed = version_data.get("speed", 1) - time_warp_nodes = version_data.get("timewarps", []) - self.make_retimes(node, speed, time_warp_nodes) - - # Update the imprinted representation - update_container( - node, - updated_dict - ) - self.log.info("udated to version: {}".format(version.get("name"))) - - def remove(self, container): - - from avalon.nuke import viewer_update_and_undo_stop - - node = nuke.toNode(container['objectName']) - assert node.Class() == "Read", "Must be Read" - - with viewer_update_and_undo_stop(): - nuke.delete(node) diff --git a/pype/plugins/nukestudio/load/load_sequences_to_timeline_asset_origin.py b/pype/plugins/nukestudio/load/load_sequences_to_timeline_asset_origin.py new file mode 100644 index 0000000000..4b94a941e7 --- /dev/null +++ b/pype/plugins/nukestudio/load/load_sequences_to_timeline_asset_origin.py @@ -0,0 +1,33 @@ +from avalon import api + + +class LoadSequencesToTimelineAssetOrigin(api.Loader): + """Load image sequence into Hiero timeline + + Place clip to timeline on its asset origin timings collected + during conforming to project + """ + + families = ["render2d", "source", "plate", "render"] + representations = ["exr", "dpx", "jpg", "jpeg", "png"] + + label = "Load to timeline with shot origin timing" + order = -10 + icon = "code-fork" + color = "orange" + + def load(self, context, name, namespace, data): + pass + + def switch(self, container, representation): + self.update(container, representation) + + def update(self, container, representation): + """ Updating previously loaded clips + """ + pass + + def remove(self, container): + """ Removing previously loaded clips + """ + pass From 3f4b89f6854210612ac4ce71028aa440c4915373 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 20 Feb 2020 18:19:43 +0100 Subject: [PATCH 05/55] feat(nks): loader object wip --- pype/nukestudio/lib.py | 97 +++++++++++++++++++++++++++++------------- 1 file changed, 68 insertions(+), 29 deletions(-) diff --git a/pype/nukestudio/lib.py b/pype/nukestudio/lib.py index 3115428d8c..6b255a0b40 100644 --- a/pype/nukestudio/lib.py +++ b/pype/nukestudio/lib.py @@ -363,44 +363,84 @@ def CreateNukeWorkfile(nodes=None, ) -class ClipsLoader: +class ClipLoader: + data = dict() - def __init__(self, representations, **kwargs): + def __init__(self, plugin_cls, context, **kwargs): """ Initialize object Arguments: - hiero_workfile_name (str): name of workfile - representations (dict): representations for processing - example: {assetName_subsetName_representationName: { - "_id": ObjectId("5as5d54fa56dfa56s6d56asddf4as"), - "path": "path/to/file/created/by/get_repr..", - "binPath": "projectBinPath", - "context": { - "subset": "subsetName", - "task": "taskName", - "family": "write", - "hierarchy": "parent/subparent", - "frame": "0996", - "project": { - "code": "j01test", - "name": "J01_jakub_test" - }, - "version": 1, - "asset": "assetName", - "representation": "representationName", - "root": "projectsRootPath" - } - } - } + plugin_cls (api.Loader): plugin object + context (dict): loader plugin context + kwargs (dict)[optional]: possible keys: + project_bin_path: "path/to/binItem" + hiero_workfile_name: "name_of_hiero_project_file_no_extension" + """ - self.representations = representations + self.cls = plugin_cls + self.context = context self.kwargs = kwargs self.active_project = self.get_active_project() self.project_bin = self.active_project.clipsBin() + assert self.set_data(), str("Cannot Load selected data, look into " + "database or call your supervisor") + # inject asset data to representation dict self.get_asset_data() + def set_data(self): + """ Gets context and convert it to self.data + data structure: + { + "name": "assetName_subsetName_representationName" + "path": "path/to/file/created/by/get_repr..", + "binPath": "projectBinPath", + } + """ + # create name + repr = self.context["representaion"] + repr_cntx = repr["context"] + asset = repr_cntx["asset"] + subset = repr_cntx["subset"] + representation = repr_cntx["representation"] + self.data["name"] = "_".join([asset, subset, representation]) + + # gets file path + file = self.cls.fname + + if not file: + repr_id = repr["_id"] + log.warning( + "Representation id `{}` is failing to load".format(repr_id)) + return None + + self.data["path"] = file.replace("\\", "/") + + if repr_cntx.get("frame"): + self.fix_path_hashes() + + # solve project bin structure path + hierarchy = "/".join(( + "Loader", + repr_cntx["hierarchy"].replace("\\", "/"), + asset + )) + self.data["binPath"] = self.kwargs.get( + "project_bin_path", + hierarchy + ) + + def fix_path_hashes(self): + """ Convert file path where it is needed padding with hashes + """ + file = self.data["path"] + if "#" not in file: + frame = self.context["representaion"]["context"].get("frame") + padding = len(frame) + file = file.replace(frame, "#"*padding) + self.data["path"] = file + def get_active_project(self): """ Get hiero active project object """ @@ -416,9 +456,8 @@ class ClipsLoader: joint `data` key with asset.data dict into the representaion """ - for name, data in self.representations.items(): - asset_name = data["context"]["asset"] - data["data"] = pype.get_asset(asset_name)["data"] + asset_name = self.context["representaion"]["context"]["asset"] + self.data["assetData"] = pype.get_asset(asset_name)["data"] def make_project_bin(self, hierarchy): """ Creare bins by given hierarchy path From 679acb519affbe0a5e0327afc8c322f0423b0dcc Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 25 Feb 2020 14:46:01 +0100 Subject: [PATCH 06/55] feat(nks): adding loader to menu --- pype/nukestudio/menu.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pype/nukestudio/menu.py b/pype/nukestudio/menu.py index a996389524..816ae40523 100644 --- a/pype/nukestudio/menu.py +++ b/pype/nukestudio/menu.py @@ -104,11 +104,11 @@ def install(): # 'function': creator.show, # 'icon': QIcon('icons:ColorAdd.png') # }, - # { - # 'action': QAction('Load...', None), - # 'function': cbloader.show, - # 'icon': QIcon('icons:CopyRectangle.png') - # }, + { + 'action': QAction('Load...', None), + 'function': cbloader.show, + 'icon': QIcon('icons:CopyRectangle.png') + }, { 'action': QAction('Publish...', None), 'function': publish.show, From 61ca2dee62c47a0900489c3a24c7be7aecddd864 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 25 Feb 2020 14:46:41 +0100 Subject: [PATCH 07/55] feat(nks): wip of loader plugin --- .../load_sequences_to_timeline_asset_origin.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/pype/plugins/nukestudio/load/load_sequences_to_timeline_asset_origin.py b/pype/plugins/nukestudio/load/load_sequences_to_timeline_asset_origin.py index 4b94a941e7..62ed2e1271 100644 --- a/pype/plugins/nukestudio/load/load_sequences_to_timeline_asset_origin.py +++ b/pype/plugins/nukestudio/load/load_sequences_to_timeline_asset_origin.py @@ -1,4 +1,7 @@ from avalon import api +import hiero +from pype.nukestudio import lib +reload(lib) class LoadSequencesToTimelineAssetOrigin(api.Loader): @@ -17,7 +20,18 @@ class LoadSequencesToTimelineAssetOrigin(api.Loader): color = "orange" def load(self, context, name, namespace, data): - pass + + data.update({ + # "projectBinPath": "Loaded", + "hieroWorkfileName": hiero.ui.activeProject().name() + }) + + self.log.info("data: `{}`".format(data)) + + clip_loader = lib.ClipLoader(self, context, **data) + clip_loader.load() + + self.log.info("Loader done: `{}`".format(name)) def switch(self, container, representation): self.update(container, representation) From 9ec1fecda26d5c6ef6002cc8eea7b1332564fc8a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 25 Feb 2020 14:47:00 +0100 Subject: [PATCH 08/55] feat(nks): wip of loader class --- pype/nukestudio/lib.py | 189 ++++++++++++++++++++++++++++++----------- 1 file changed, 140 insertions(+), 49 deletions(-) diff --git a/pype/nukestudio/lib.py b/pype/nukestudio/lib.py index 6b255a0b40..9cc4df1683 100644 --- a/pype/nukestudio/lib.py +++ b/pype/nukestudio/lib.py @@ -364,32 +364,40 @@ def CreateNukeWorkfile(nodes=None, class ClipLoader: - data = dict() - def __init__(self, plugin_cls, context, **kwargs): + active_bin = None + + def __init__(self, plugin_cls, context, sequence=None, track=None, **kwargs): """ Initialize object Arguments: plugin_cls (api.Loader): plugin object context (dict): loader plugin context + sequnce (hiero.core.Sequence): sequence object + track (hiero.core.Track): track object kwargs (dict)[optional]: possible keys: - project_bin_path: "path/to/binItem" - hiero_workfile_name: "name_of_hiero_project_file_no_extension" + projectBinPath: "path/to/binItem" + hieroWorkfileName: "name_of_hiero_project_file_no_extension" """ self.cls = plugin_cls self.context = context self.kwargs = kwargs - self.active_project = self.get_active_project() + self.active_project = self._get_active_project() self.project_bin = self.active_project.clipsBin() + self.active_sequence = self._get_active_sequence(sequence) + self.active_track = self._get_active_track(track) - assert self.set_data(), str("Cannot Load selected data, look into " + self.data = dict() + + assert self._set_data(), str("Cannot Load selected data, look into " "database or call your supervisor") # inject asset data to representation dict - self.get_asset_data() + self._get_asset_data() + log.debug("__init__ self.data: `{}`".format(self.data)) - def set_data(self): + def _set_data(self): """ Gets context and convert it to self.data data structure: { @@ -399,67 +407,69 @@ class ClipLoader: } """ # create name - repr = self.context["representaion"] + repr = self.context["representation"] repr_cntx = repr["context"] - asset = repr_cntx["asset"] - subset = repr_cntx["subset"] - representation = repr_cntx["representation"] + asset = str(repr_cntx["asset"]) + subset = str(repr_cntx["subset"]) + representation = str(repr_cntx["representation"]) self.data["name"] = "_".join([asset, subset, representation]) # gets file path file = self.cls.fname - if not file: repr_id = repr["_id"] log.warning( "Representation id `{}` is failing to load".format(repr_id)) return None - self.data["path"] = file.replace("\\", "/") + # convert to hashed path if repr_cntx.get("frame"): - self.fix_path_hashes() + self._fix_path_hashes() # solve project bin structure path - hierarchy = "/".join(( + hierarchy = str("/".join(( "Loader", repr_cntx["hierarchy"].replace("\\", "/"), asset - )) + ))) + self.data["binPath"] = self.kwargs.get( - "project_bin_path", + "projectBinPath", hierarchy ) - def fix_path_hashes(self): + return True + + def _fix_path_hashes(self): """ Convert file path where it is needed padding with hashes """ file = self.data["path"] if "#" not in file: - frame = self.context["representaion"]["context"].get("frame") + frame = self.context["representation"]["context"].get("frame") padding = len(frame) file = file.replace(frame, "#"*padding) self.data["path"] = file - def get_active_project(self): + def _get_active_project(self): """ Get hiero active project object """ - fname = self.kwargs.get("hiero_workfile_name", "") + fname = self.kwargs.get("hieroWorkfileName", "") return next((p for p in hiero.core.projects() if fname in p.name()), hiero.core.projects()[-1]) - def get_asset_data(self): + def _get_asset_data(self): """ Get all available asset data joint `data` key with asset.data dict into the representaion """ - asset_name = self.context["representaion"]["context"]["asset"] + asset_name = self.context["representation"]["context"]["asset"] self.data["assetData"] = pype.get_asset(asset_name)["data"] - def make_project_bin(self, hierarchy): + def _make_project_bin(self, hierarchy): """ Creare bins by given hierarchy path It will also make sure no duplicit bins will be created @@ -470,13 +480,45 @@ class ClipLoader: Returns: bin (hiero.core.BinItem): with the bin to be used for mediaItem """ - pass + if self.active_bin: + return self.active_bin - def make_track_item(self): + assert hierarchy != "", "Please add hierarchy!" + log.debug("__ hierarchy1: `{}`".format(hierarchy)) + if '/' in hierarchy: + hierarchy = hierarchy.split('/') + else: + hierarchy = [hierarchy] + + parent_bin = None + for i, name in enumerate(hierarchy): + # if first index and list is more then one long + if i == 0: + bin = next((bin for bin in self.project_bin.bins() + if name in bin.name()), None) + if not bin: + bin = hiero.core.Bin(name) + self.project_bin.addItem(bin) + log.debug("__ bin.name: `{}`".format(bin.name())) + parent_bin = bin + + # if second to prelast + elif (i >= 1) and (i <= (len(hierarchy) - 1)): + bin = next((bin for bin in parent_bin.bins() + if name in bin.name()), None) + if not bin: + bin = hiero.core.Bin(name) + parent_bin.addItem(bin) + + parent_bin = bin + + return parent_bin + + def _make_track_item(self): """ Create track item with """ pass - def set_clip_color(self, last_version=True): + def _set_clip_color(self, last_version=True): """ Sets color of clip on clip/track item Arguments: @@ -484,7 +526,7 @@ class ClipLoader: """ pass - def set_container_tag(self, item, metadata): + def _set_container_tag(self, item, metadata): """ Sets container tag to given clip/track item Arguments: @@ -493,6 +535,74 @@ class ClipLoader: """ pass + def _get_active_sequence(self, sequence): + if not sequence: + return hiero.ui.activeSequence() + else: + return sequence + + def _get_active_track(self, track): + if not track: + track_name = self.data["name"] + + if track_name not in self.active_sequence.videoTracks(): + track = hiero.core.VideoTrack(track_name) + self.active_sequence.addTrack(track) + + return track + + def load(self): + log.debug("__ active_project: `{}`".format(self.active_project)) + log.debug("__ active_sequence: `{}`".format(self.active_sequence)) + + # create project bin for the media to be imported into + self.active_bin = self._make_project_bin(self.data["binPath"]) + log.debug("__ active_bin: `{}`".format(self.active_bin)) + + # create mediaItem in active project bin + # create clip media + media = hiero.core.MediaSource(self.data["path"]) + media_in = int(media.startTime()) + media_duration = int(media.duration()) + + handle_start = self.data["assetData"]["handleStart"] + handle_end = self.data["assetData"]["handleEnd"] + + if media_in: + source_in = media_in + handle_start + else: + source_in = self.data["assetData"]["frameStart"] + handle_start + + if media_duration: + source_out = (media_in + media_duration - 1) - handle_end + else: + source_out = self.data["assetData"]["frameEnd"]- handle_end + + source = hiero.core.Clip(media) + + # add to bin as clip item + items_in_bin = [b.name() for b in bin.items()] + if self.data["name"] not in items_in_bin: + binItem = hiero.core.BinItem(source) + bin.addItem(binItem) + + new_source = [ + item for item in bin.items() if split_name in item.name() + ][0].items()[0].item() + + # add to track as clip item + trackItem = hiero.core.TrackItem( + self.data["name"], hiero.core.TrackItem.kVideo) + trackItem.setSource(new_source) + trackItem.setSourceIn(self.data["assetData"]["sourceIn"]) + trackItem.setSourceOut(self.data["assetData"]["sourceOut"]) + trackItem.setTimelineIn(self.data["assetData"]["clipIn"]) + trackItem.setTimelineOut(self.data["assetData"]["clipOut"]) + self.active_track.addTrackItem(trackItem) + + log.info("Loading clips: `{}`".format(self.data["name"])) + + def create_nk_workfile_clips(nk_workfiles, seq=None): ''' nk_workfile is list of dictionaries like: @@ -529,9 +639,7 @@ def create_nk_workfile_clips(nk_workfiles, seq=None): else: track = seq.tracks(nk['task']) - # create slip media - print("__ path: `{}`".format(nk['path'])) - + # create clip media media = hiero.core.MediaSource(nk['path']) media_in = int(media.startTime() or 0) media_duration = int(media.duration() or 0) @@ -549,47 +657,30 @@ def create_nk_workfile_clips(nk_workfiles, seq=None): else: source_out = nk["frameEnd"] - handle_end - print("__ media: `{}`".format(media)) - print("__ media_in: `{}`".format(media_in)) - print("__ media_duration : `{}`".format(media_duration)) - print("__ source_in: `{}`".format(source_in)) - print("__ source_out : `{}`".format(source_out)) - source = hiero.core.Clip(media) - print("__ source : `{}`".format(source)) - print("__ source.sourceIn(): `{}`".format(source.sourceIn())) name = os.path.basename(os.path.splitext(nk['path'])[0]) split_name = split_by_client_version(name)[0] or name - print("__ split_name: `{}`".format(split_name)) - # add to bin as clip item items_in_bin = [b.name() for b in bin.items()] if split_name not in items_in_bin: binItem = hiero.core.BinItem(source) bin.addItem(binItem) - print("__ bin.items(): `{}`".format(bin.items())) - new_source = [ item for item in bin.items() if split_name in item.name() ][0].items()[0].item() - print("__ new_source: `{}`".format(new_source)) - print("__ new_source: `{}`".format(new_source)) - # add to track as clip item trackItem = hiero.core.TrackItem( split_name, hiero.core.TrackItem.kVideo) trackItem.setSource(new_source) trackItem.setSourceIn(source_in) trackItem.setSourceOut(source_out) - trackItem.setSourceIn(source_in) trackItem.setTimelineIn(nk["clipIn"]) trackItem.setTimelineOut(nk["clipIn"] + (source_out - source_in)) track.addTrackItem(trackItem) - track.addTrackItem(trackItem) clips_lst.append(trackItem) return clips_lst From 2f1fe373b784fedbcc15eac722364d744a036294 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 27 Feb 2020 09:39:19 +0100 Subject: [PATCH 09/55] feat(nks): wip loader in lib --- pype/nukestudio/lib.py | 65 +++++++++++++++++++++++++++++------------- 1 file changed, 45 insertions(+), 20 deletions(-) diff --git a/pype/nukestudio/lib.py b/pype/nukestudio/lib.py index 9cc4df1683..6caae770e6 100644 --- a/pype/nukestudio/lib.py +++ b/pype/nukestudio/lib.py @@ -385,8 +385,6 @@ class ClipLoader: self.kwargs = kwargs self.active_project = self._get_active_project() self.project_bin = self.active_project.clipsBin() - self.active_sequence = self._get_active_sequence(sequence) - self.active_track = self._get_active_track(track) self.data = dict() @@ -397,6 +395,10 @@ class ClipLoader: self._get_asset_data() log.debug("__init__ self.data: `{}`".format(self.data)) + # add active components to class + self.active_sequence = self._get_active_sequence(sequence) + self.active_track = self._get_active_track(track) + def _set_data(self): """ Gets context and convert it to self.data data structure: @@ -564,40 +566,63 @@ class ClipLoader: media = hiero.core.MediaSource(self.data["path"]) media_in = int(media.startTime()) media_duration = int(media.duration()) + log.debug("__ media_in: `{}`".format(media_in)) + log.debug("__ media_duration: `{}`".format(media_duration)) handle_start = self.data["assetData"]["handleStart"] handle_end = self.data["assetData"]["handleEnd"] + fps = self.data["assetData"]["fps"] + if media_in: - source_in = media_in + handle_start + source_in = int(media_in + handle_start) else: - source_in = self.data["assetData"]["frameStart"] + handle_start + source_in = int(self.data["assetData"]["frameStart"] + handle_start) if media_duration: - source_out = (media_in + media_duration - 1) - handle_end + source_out = int((media_in + media_duration - 1) - handle_end) else: - source_out = self.data["assetData"]["frameEnd"]- handle_end + source_out = int(self.data["assetData"]["frameEnd"] - handle_end) - source = hiero.core.Clip(media) + log.debug("__ source_in: `{}`".format(source_in)) + log.debug("__ source_out: `{}`".format(source_out)) + log.debug("__ handle_start: `{}`".format(handle_start)) + log.debug("__ handle_end: `{}`".format(handle_end)) - # add to bin as clip item - items_in_bin = [b.name() for b in bin.items()] - if self.data["name"] not in items_in_bin: - binItem = hiero.core.BinItem(source) - bin.addItem(binItem) + # create Clip from Media + _clip = hiero.core.Clip(media) + _clip.setName(self.data["name"]) - new_source = [ - item for item in bin.items() if split_name in item.name() - ][0].items()[0].item() + # add Clip to bin if not there yet + if self.data["name"] not in [b.name() for b in self.active_bin.items()]: + binItem = hiero.core.BinItem(_clip) + self.active_bin.addItem(binItem) + + _source = next((item for item in self.active_bin.items() + if self.data["name"] in item.name()), None) + + if not _source: + log.warning("Problem with created Source clip: `{}`".format( + self.data["name"])) + + version = next((s for s in _source.items()), None) + clip = version.item() # add to track as clip item trackItem = hiero.core.TrackItem( self.data["name"], hiero.core.TrackItem.kVideo) - trackItem.setSource(new_source) - trackItem.setSourceIn(self.data["assetData"]["sourceIn"]) - trackItem.setSourceOut(self.data["assetData"]["sourceOut"]) - trackItem.setTimelineIn(self.data["assetData"]["clipIn"]) - trackItem.setTimelineOut(self.data["assetData"]["clipOut"]) + + log.info("clip: `{}`".format(clip)) + log.info("_clip: `{}`".format(_clip)) + log.info("clip.sourceIn(): `{}`".format(clip.sourceIn())) + log.info("clip.sourceOut(): `{}`".format(clip.sourceOut())) + + trackItem.setSource(clip) + # trackItem.setTimelineIn(self.data["assetData"]["clipIn"]) + trackItem.setSourceIn(5) + # trackItem.setTimelineOut(self.data["assetData"]["clipOut"]) + trackItem.setSourceOut(10) + self.active_track.addTrackItem(trackItem) log.info("Loading clips: `{}`".format(self.data["name"])) From f67fb7f79df98e6f373a5330773fdb3d19aa6d67 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 27 Feb 2020 09:39:45 +0100 Subject: [PATCH 10/55] feat(nks): project helpers from dotStudio --- .../Python/Startup/project_helpers.py | 235 ++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 setup/nukestudio/hiero_plugin_path/Python/Startup/project_helpers.py diff --git a/setup/nukestudio/hiero_plugin_path/Python/Startup/project_helpers.py b/setup/nukestudio/hiero_plugin_path/Python/Startup/project_helpers.py new file mode 100644 index 0000000000..7e274bd0a3 --- /dev/null +++ b/setup/nukestudio/hiero_plugin_path/Python/Startup/project_helpers.py @@ -0,0 +1,235 @@ +try: + from PySide.QtGui import * + from PySide.QtCore import * +except: + from PySide2.QtGui import * + from PySide2.QtWidgets import * + from PySide2.QtCore import * + +from hiero.core.util import uniquify, version_get, version_set +import hiero.core +import hiero.ui +import nuke + +# A globally variable for storing the current Project +gTrackedActiveProject = None + +# This selection handler will track changes in items selected/deselected in the Bin/Timeline/Spreadsheet Views + + +def __trackActiveProjectHandler(event): + global gTrackedActiveProject + selection = event.sender.selection() + binSelection = selection + if len(binSelection) > 0 and hasattr(binSelection[0], 'project'): + proj = binSelection[0].project() + + # We only store this if its a valid, active User Project + if proj in hiero.core.projects(hiero.core.Project.kUserProjects): + gTrackedActiveProject = proj + + +hiero.core.events.registerInterest( + 'kSelectionChanged/kBin', __trackActiveProjectHandler) +hiero.core.events.registerInterest( + 'kSelectionChanged/kTimeline', __trackActiveProjectHandler) +hiero.core.events.registerInterest( + 'kSelectionChanged/Spreadsheet', __trackActiveProjectHandler) + + +def activeProject(): + """hiero.ui.activeProject() -> returns the current Project + + Note: There is not technically a notion of a 'active' Project in Hiero/NukeStudio, as it is a multi-project App. + This method determines what is 'active' by going down the following rules... + + # 1 - If the current Viewer (hiero.ui.currentViewer) contains a Clip or Sequence, this item is assumed to give the active Project + # 2 - If nothing is currently in the Viewer, look to the active View, determine project from active selection + # 3 - If no current selection can be determined, fall back to a globally tracked last selection from trackActiveProjectHandler + # 4 - If all those rules fail, fall back to the last project in the list of hiero.core.projects() + + @return: hiero.core.Project""" + global gTrackedActiveProject + activeProject = None + + # Case 1 : Look for what the current Viewr tells us - this might not be what we want, and relies on hiero.ui.currentViewer() being robust. + cv = hiero.ui.currentViewer().player().sequence() + if hasattr(cv, 'project'): + activeProject = cv.project() + else: + # Case 2: We can't determine a project from the current Viewer, so try seeing what's selected in the activeView + # Note that currently, if you run activeProject from the Script Editor, the activeView is always None, so this will rarely get used! + activeView = hiero.ui.activeView() + if activeView: + # We can determine an active View.. see what's being worked with + selection = activeView.selection() + + # Handle the case where nothing is selected in the active view + if len(selection) == 0: + # It's possible that there is no selection in a Timeline/Spreadsheet, but these views have 'sequence' method, so try that... + if isinstance(hiero.ui.activeView(), (hiero.ui.TimelineEditor, hiero.ui.SpreadsheetView)): + activeSequence = activeView.sequence() + if hasattr(currentItem, 'project'): + activeProject = activeSequence.project() + + # The active view has a selection... assume that the first item in the selection has the active Project + else: + currentItem = selection[0] + if hasattr(currentItem, 'project'): + activeProject = currentItem.project() + + # Finally, Cases 3 and 4... + if not activeProject: + activeProjects = hiero.core.projects(hiero.core.Project.kUserProjects) + if gTrackedActiveProject in activeProjects: + activeProject = gTrackedActiveProject + else: + activeProject = activeProjects[-1] + + return activeProject + +# Method to get all recent projects + + +def recentProjects(): + """hiero.core.recentProjects() -> Returns a list of paths to recently opened projects + + Hiero stores up to 5 recent projects in uistate.ini with the [recentFile]/# key. + + @return: list of paths to .hrox Projects""" + + appSettings = hiero.core.ApplicationSettings() + recentProjects = [] + for i in range(0, 5): + proj = appSettings.value('recentFile/%i' % i) + if len(proj) > 0: + recentProjects.append(proj) + return recentProjects + +# Method to get recent project by index + + +def recentProject(k=0): + """hiero.core.recentProject(k) -> Returns the recent project path, specified by integer k (0-4) + + @param: k (optional, default = 0) - an integer from 0-4, relating to the index of recent projects. + + @return: hiero.core.Project""" + + appSettings = hiero.core.ApplicationSettings() + proj = appSettings.value('recentFile/%i' % int(k), None) + return proj + +# Method to get open project by index + + +def openRecentProject(k=0): + """hiero.core.openRecentProject(k) -> Opens the most the recent project as listed in the Open Recent list. + + @param: k (optional, default = 0) - an integer from 0-4, relating to the index of recent projects. + @return: hiero.core.Project""" + + appSettings = hiero.core.ApplicationSettings() + proj = appSettings.value('recentFile/%i' % int(k), None) + proj = hiero.core.openProject(proj) + return proj + + +# Duck punch these methods into the relevant ui/core namespaces +hiero.ui.activeProject = activeProject +hiero.core.recentProjects = recentProjects +hiero.core.recentProject = recentProject +hiero.core.openRecentProject = openRecentProject + + +# Method to Save a new Version of the activeHrox Project +class SaveAllProjects(QAction): + + def __init__(self): + QAction.__init__(self, "Save All Projects", None) + self.triggered.connect(self.projectSaveAll) + hiero.core.events.registerInterest( + "kShowContextMenu/kBin", self.eventHandler) + + def projectSaveAll(self): + allProjects = hiero.core.projects() + for proj in allProjects: + try: + proj.save() + print 'Saved Project: %s to: %s ' % (proj.name(), proj.path()) + except: + print 'Unable to save Project: %s to: %s. Check file permissions.' % (proj.name(), proj.path()) + + def eventHandler(self, event): + event.menu.addAction(self) + +# For projects with v# in the path name, saves out a new Project with v#+1 + + +class SaveNewProjectVersion(QAction): + + def __init__(self): + QAction.__init__(self, "Save New Version...", None) + self.triggered.connect(self.saveNewVersion) + hiero.core.events.registerInterest( + "kShowContextMenu/kBin", self.eventHandler) + self.selectedProjects = [] + + def saveNewVersion(self): + if len(self.selectedProjects) > 0: + projects = self.selectedProjects + else: + projects = [hiero.ui.activeProject()] + + if len(projects) < 1: + return + + for proj in projects: + oldName = proj.name() + path = proj.path() + v = None + prefix = None + try: + (prefix, v) = version_get(path, 'v') + except ValueError, msg: + print msg + + if (prefix is not None) and (v is not None): + v = int(v) + newPath = version_set(path, prefix, v, v + 1) + try: + proj.saveAs(newPath) + print 'Saved new project version: %s to: %s ' % (oldName, newPath) + except: + print 'Unable to save Project: %s. Check file permissions.' % (oldName) + else: + newPath = path.replace(".hrox", "_v01.hrox") + answer = nuke.ask( + '%s does not contain a version number.\nDo you want to save as %s?' % (proj, newPath)) + if answer: + try: + proj.saveAs(newPath) + print 'Saved new project version: %s to: %s ' % (oldName, newPath) + except: + print 'Unable to save Project: %s. Check file permissions.' % (oldName) + + def eventHandler(self, event): + self.selectedProjects = [] + if hasattr(event.sender, 'selection') and event.sender.selection() is not None and len(event.sender.selection()) != 0: + selection = event.sender.selection() + self.selectedProjects = uniquify( + [item.project() for item in selection]) + event.menu.addAction(self) + + +# Instantiate the actions +saveAllAct = SaveAllProjects() +saveNewAct = SaveNewProjectVersion() + +fileMenu = hiero.ui.findMenuAction("foundry.menu.file") +importAct = hiero.ui.findMenuAction("foundry.project.importFiles") +hiero.ui.insertMenuAction(saveNewAct, fileMenu.menu(), + before="Import File(s)...") +hiero.ui.insertMenuAction(saveAllAct, fileMenu.menu(), + before="Import File(s)...") +fileMenu.menu().insertSeparator(importAct) From 4e49c8b40b4c3a596c1c41e94b4b852c27b515ed Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 2 Mar 2020 14:32:22 +0100 Subject: [PATCH 11/55] feat(nks): creating track item on metadata time --- pype/nukestudio/lib.py | 53 ++++++++++++++++++------------------------ 1 file changed, 22 insertions(+), 31 deletions(-) diff --git a/pype/nukestudio/lib.py b/pype/nukestudio/lib.py index 6caae770e6..fad6d32ffc 100644 --- a/pype/nukestudio/lib.py +++ b/pype/nukestudio/lib.py @@ -561,34 +561,28 @@ class ClipLoader: self.active_bin = self._make_project_bin(self.data["binPath"]) log.debug("__ active_bin: `{}`".format(self.active_bin)) + # check if slate is included + slate_on = next((f for f in self.context["version"]["data"]["families"] + if "slate" in f), None) + # create mediaItem in active project bin # create clip media media = hiero.core.MediaSource(self.data["path"]) - media_in = int(media.startTime()) media_duration = int(media.duration()) - log.debug("__ media_in: `{}`".format(media_in)) - log.debug("__ media_duration: `{}`".format(media_duration)) - handle_start = self.data["assetData"]["handleStart"] - handle_end = self.data["assetData"]["handleEnd"] + handle_start = int(self.data["assetData"]["handleStart"]) + handle_end = int(self.data["assetData"]["handleEnd"]) + + clip_in = int(self.data["assetData"]["clipIn"]) + clip_out = int(self.data["assetData"]["clipOut"]) + + # calculate slate differences + if slate_on: + media_duration -= 1 + handle_start += 1 fps = self.data["assetData"]["fps"] - if media_in: - source_in = int(media_in + handle_start) - else: - source_in = int(self.data["assetData"]["frameStart"] + handle_start) - - if media_duration: - source_out = int((media_in + media_duration - 1) - handle_end) - else: - source_out = int(self.data["assetData"]["frameEnd"] - handle_end) - - log.debug("__ source_in: `{}`".format(source_in)) - log.debug("__ source_out: `{}`".format(source_out)) - log.debug("__ handle_start: `{}`".format(handle_start)) - log.debug("__ handle_end: `{}`".format(handle_end)) - # create Clip from Media _clip = hiero.core.Clip(media) _clip.setName(self.data["name"]) @@ -609,21 +603,18 @@ class ClipLoader: clip = version.item() # add to track as clip item - trackItem = hiero.core.TrackItem( + track_item = hiero.core.TrackItem( self.data["name"], hiero.core.TrackItem.kVideo) - log.info("clip: `{}`".format(clip)) - log.info("_clip: `{}`".format(_clip)) - log.info("clip.sourceIn(): `{}`".format(clip.sourceIn())) - log.info("clip.sourceOut(): `{}`".format(clip.sourceOut())) + track_item.setSource(clip) - trackItem.setSource(clip) - # trackItem.setTimelineIn(self.data["assetData"]["clipIn"]) - trackItem.setSourceIn(5) - # trackItem.setTimelineOut(self.data["assetData"]["clipOut"]) - trackItem.setSourceOut(10) + track_item.setSourceIn(handle_start) + track_item.setTimelineIn(clip_in) - self.active_track.addTrackItem(trackItem) + track_item.setSourceOut(media_duration - handle_end) + track_item.setTimelineOut(clip_out) + + self.active_track.addTrackItem(track_item) log.info("Loading clips: `{}`".format(self.data["name"])) From ec6f10f5bf524f8b60157939a3187fbdf216edbe Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 2 Mar 2020 14:33:17 +0100 Subject: [PATCH 12/55] feat(nks): printing context for better dev --- .../nukestudio/load/load_sequences_to_timeline_asset_origin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/nukestudio/load/load_sequences_to_timeline_asset_origin.py b/pype/plugins/nukestudio/load/load_sequences_to_timeline_asset_origin.py index 62ed2e1271..523bcf91eb 100644 --- a/pype/plugins/nukestudio/load/load_sequences_to_timeline_asset_origin.py +++ b/pype/plugins/nukestudio/load/load_sequences_to_timeline_asset_origin.py @@ -26,7 +26,7 @@ class LoadSequencesToTimelineAssetOrigin(api.Loader): "hieroWorkfileName": hiero.ui.activeProject().name() }) - self.log.info("data: `{}`".format(data)) + self.log.develop("_ context: `{}`".format(context)) clip_loader = lib.ClipLoader(self, context, **data) clip_loader.load() From b4c88a5c30e37f233f7abc6d4e2f7da69c016c58 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 2 Mar 2020 18:30:10 +0100 Subject: [PATCH 13/55] fix(nks): debug log wrong argument --- .../nukestudio/load/load_sequences_to_timeline_asset_origin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/nukestudio/load/load_sequences_to_timeline_asset_origin.py b/pype/plugins/nukestudio/load/load_sequences_to_timeline_asset_origin.py index 523bcf91eb..09d9b1a4bb 100644 --- a/pype/plugins/nukestudio/load/load_sequences_to_timeline_asset_origin.py +++ b/pype/plugins/nukestudio/load/load_sequences_to_timeline_asset_origin.py @@ -26,7 +26,7 @@ class LoadSequencesToTimelineAssetOrigin(api.Loader): "hieroWorkfileName": hiero.ui.activeProject().name() }) - self.log.develop("_ context: `{}`".format(context)) + self.log.debug("_ context: `{}`".format(context)) clip_loader = lib.ClipLoader(self, context, **data) clip_loader.load() From 1b219b5cfb898be8528ef3726d427ad637603d95 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 2 Mar 2020 18:30:33 +0100 Subject: [PATCH 14/55] feat(nks): dealing with track names and correct timing --- pype/nukestudio/lib.py | 55 ++++++++++++++++++++++++++++++------------ 1 file changed, 40 insertions(+), 15 deletions(-) diff --git a/pype/nukestudio/lib.py b/pype/nukestudio/lib.py index fad6d32ffc..e7b7232938 100644 --- a/pype/nukestudio/lib.py +++ b/pype/nukestudio/lib.py @@ -414,7 +414,8 @@ class ClipLoader: asset = str(repr_cntx["asset"]) subset = str(repr_cntx["subset"]) representation = str(repr_cntx["representation"]) - self.data["name"] = "_".join([asset, subset, representation]) + self.data["clip_name"] = "_".join([asset, subset, representation]) + self.data["track_name"] = "_".join([subset, representation]) # gets file path file = self.cls.fname @@ -545,13 +546,20 @@ class ClipLoader: def _get_active_track(self, track): if not track: - track_name = self.data["name"] + track_name = self.data["track_name"] + else: + track_name = track.name() - if track_name not in self.active_sequence.videoTracks(): - track = hiero.core.VideoTrack(track_name) - self.active_sequence.addTrack(track) + track_pass = next( + (t for t in self.active_sequence.videoTracks() + if t.name() in track_name), None + ) - return track + if not track_pass: + track_pass = hiero.core.VideoTrack(track_name) + self.active_sequence.addTrack(track_pass) + + return track_pass def load(self): log.debug("__ active_project: `{}`".format(self.active_project)) @@ -561,9 +569,8 @@ class ClipLoader: self.active_bin = self._make_project_bin(self.data["binPath"]) log.debug("__ active_bin: `{}`".format(self.active_bin)) - # check if slate is included - slate_on = next((f for f in self.context["version"]["data"]["families"] - if "slate" in f), None) + log.debug("__ version.data: `{}`".format( + self.context["version"]["data"])) # create mediaItem in active project bin # create clip media @@ -576,6 +583,22 @@ class ClipLoader: clip_in = int(self.data["assetData"]["clipIn"]) clip_out = int(self.data["assetData"]["clipOut"]) + log.debug("__ media_duration: `{}`".format(media_duration)) + log.debug("__ handle_start: `{}`".format(handle_start)) + log.debug("__ handle_end: `{}`".format(handle_end)) + log.debug("__ clip_in: `{}`".format(clip_in)) + log.debug("__ clip_out: `{}`".format(clip_out)) + + # check if slate is included + # either in version data families or by calculating frame diff + slate_on = next( + (f for f in self.context["version"]["data"]["families"] + if "slate" in f), + None) or bool((( + clip_in - clip_out + 1) + handle_start + handle_end + ) - media_duration) + + log.debug("__ slate_on: `{}`".format(slate_on)) # calculate slate differences if slate_on: media_duration -= 1 @@ -585,26 +608,28 @@ class ClipLoader: # create Clip from Media _clip = hiero.core.Clip(media) - _clip.setName(self.data["name"]) + _clip.setName(self.data["clip_name"]) # add Clip to bin if not there yet - if self.data["name"] not in [b.name() for b in self.active_bin.items()]: + if self.data["clip_name"] not in [ + b.name() + for b in self.active_bin.items()]: binItem = hiero.core.BinItem(_clip) self.active_bin.addItem(binItem) _source = next((item for item in self.active_bin.items() - if self.data["name"] in item.name()), None) + if self.data["clip_name"] in item.name()), None) if not _source: log.warning("Problem with created Source clip: `{}`".format( - self.data["name"])) + self.data["clip_name"])) version = next((s for s in _source.items()), None) clip = version.item() # add to track as clip item track_item = hiero.core.TrackItem( - self.data["name"], hiero.core.TrackItem.kVideo) + self.data["clip_name"], hiero.core.TrackItem.kVideo) track_item.setSource(clip) @@ -616,7 +641,7 @@ class ClipLoader: self.active_track.addTrackItem(track_item) - log.info("Loading clips: `{}`".format(self.data["name"])) + log.info("Loading clips: `{}`".format(self.data["clip_name"])) def create_nk_workfile_clips(nk_workfiles, seq=None): From 3b83a1f480e865dea2c4483aa785ca20597c3087 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 9 Mar 2020 17:41:20 +0100 Subject: [PATCH 15/55] fix(nks): loading clips with now slates --- pype/nukestudio/lib.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pype/nukestudio/lib.py b/pype/nukestudio/lib.py index e7b7232938..774a9d45bf 100644 --- a/pype/nukestudio/lib.py +++ b/pype/nukestudio/lib.py @@ -595,10 +595,11 @@ class ClipLoader: (f for f in self.context["version"]["data"]["families"] if "slate" in f), None) or bool((( - clip_in - clip_out + 1) + handle_start + handle_end + clip_out - clip_in + 1) + handle_start + handle_end ) - media_duration) log.debug("__ slate_on: `{}`".format(slate_on)) + # calculate slate differences if slate_on: media_duration -= 1 @@ -638,7 +639,7 @@ class ClipLoader: track_item.setSourceOut(media_duration - handle_end) track_item.setTimelineOut(clip_out) - + track_item.setPlaybackSpeed(1) self.active_track.addTrackItem(track_item) log.info("Loading clips: `{}`".format(self.data["clip_name"])) From ef51f1ed513bd8d740533710eb5509a684647088 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 9 Mar 2020 17:43:31 +0100 Subject: [PATCH 16/55] feat(nks): print repre _id --- .../nukestudio/load/load_sequences_to_timeline_asset_origin.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pype/plugins/nukestudio/load/load_sequences_to_timeline_asset_origin.py b/pype/plugins/nukestudio/load/load_sequences_to_timeline_asset_origin.py index 09d9b1a4bb..2ee2409b86 100644 --- a/pype/plugins/nukestudio/load/load_sequences_to_timeline_asset_origin.py +++ b/pype/plugins/nukestudio/load/load_sequences_to_timeline_asset_origin.py @@ -27,6 +27,8 @@ class LoadSequencesToTimelineAssetOrigin(api.Loader): }) self.log.debug("_ context: `{}`".format(context)) + self.log.debug("_ representation._id: `{}`".format( + context["representation"]["_id"])) clip_loader = lib.ClipLoader(self, context, **data) clip_loader.load() From 5c589dd0232accf72da96ea9b65e31657cb81881 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 13 Mar 2020 12:34:08 +0100 Subject: [PATCH 17/55] feat(global): delivery resolution to preset - also fixes of reformating --- pype/plugins/global/publish/extract_review.py | 82 ++++++++++++------- .../global/publish/extract_review_slate.py | 33 +++++--- pype/plugins/nuke/publish/collect_writes.py | 1 - 3 files changed, 75 insertions(+), 41 deletions(-) diff --git a/pype/plugins/global/publish/extract_review.py b/pype/plugins/global/publish/extract_review.py index f5dba108c5..b7b6efafb8 100644 --- a/pype/plugins/global/publish/extract_review.py +++ b/pype/plugins/global/publish/extract_review.py @@ -23,18 +23,21 @@ class ExtractReview(pyblish.api.InstancePlugin): outputs = {} ext_filter = [] + to_width = 1920 + to_height = 1080 def process(self, instance): - to_width = 1920 - to_height = 1080 output_profiles = self.outputs or {} inst_data = instance.data - fps = inst_data.get("fps") - start_frame = inst_data.get("frameStart") - resolution_width = inst_data.get("resolutionWidth", to_width) - resolution_height = inst_data.get("resolutionHeight", to_height) + fps = float(inst_data.get("fps")) + frame_start = inst_data.get("frameStart") + frame_end = inst_data.get("frameEnd") + handle_start = inst_data.get("handleStart") + handle_end = inst_data.get("handleEnd") + resolution_width = inst_data.get("resolutionWidth", self.to_width) + resolution_height = inst_data.get("resolutionHeight", self.to_height) pixel_aspect = inst_data.get("pixelAspect", 1) self.log.debug("Families In: `{}`".format(inst_data["families"])) @@ -198,30 +201,42 @@ class ExtractReview(pyblish.api.InstancePlugin): output_args.extend(profile.get('output', [])) # defining image ratios - resolution_ratio = float(resolution_width / ( - resolution_height * pixel_aspect)) - delivery_ratio = float(to_width) / float(to_height) - self.log.debug(resolution_ratio) - self.log.debug(delivery_ratio) + resolution_ratio = (float(resolution_width) * pixel_aspect) / resolution_height + delivery_ratio = float(self.to_width) / float(self.to_height) + self.log.debug( + "__ resolution_ratio: `{}`".format(resolution_ratio)) + self.log.debug( + "__ delivery_ratio: `{}`".format(delivery_ratio)) # get scale factor - scale_factor = to_height / ( + scale_factor = float(self.to_height) / ( resolution_height * pixel_aspect) - self.log.debug(scale_factor) + + # shorten two decimals long float number for testing conditions + resolution_ratio_test = float( + "{:0.2f}".format(resolution_ratio)) + delivery_ratio_test = float( + "{:0.2f}".format(delivery_ratio)) + + if resolution_ratio_test < delivery_ratio_test: + scale_factor = float(self.to_width) / ( + resolution_width * pixel_aspect) + + self.log.debug("__ scale_factor: `{}`".format(scale_factor)) # letter_box lb = profile.get('letter_box', 0) if lb != 0: - ffmpet_width = to_width - ffmpet_height = to_height + ffmpeg_width = self.to_width + ffmpeg_height = self.to_height if "reformat" not in p_tags: lb /= pixel_aspect - if resolution_ratio != delivery_ratio: - ffmpet_width = resolution_width - ffmpet_height = int( + if resolution_ratio_test != delivery_ratio_test: + ffmpeg_width = resolution_width + ffmpeg_height = int( resolution_height * pixel_aspect) else: - if resolution_ratio != delivery_ratio: + if resolution_ratio_test != delivery_ratio_test: lb /= scale_factor else: lb /= pixel_aspect @@ -233,7 +248,7 @@ class ExtractReview(pyblish.api.InstancePlugin): "c=black,drawbox=0:ih-round((ih-(iw*(" "1/{2})))/2):iw:round((ih-(iw*(1/{2})))" "/2):t=fill:c=black").format( - ffmpet_width, ffmpet_height, lb)) + ffmpeg_width, ffmpeg_height, lb)) # In case audio is longer than video. output_args.append("-shortest") @@ -252,24 +267,26 @@ class ExtractReview(pyblish.api.InstancePlugin): # scaling none square pixels and 1920 width if "reformat" in p_tags: - if resolution_ratio < delivery_ratio: + if resolution_ratio_test < delivery_ratio_test: self.log.debug("lower then delivery") - width_scale = int(to_width * scale_factor) + width_scale = int(self.to_width * scale_factor) width_half_pad = int(( - to_width - width_scale)/2) - height_scale = to_height + self.to_width - width_scale)/2) + height_scale = self.to_height height_half_pad = 0 else: self.log.debug("heigher then delivery") - width_scale = to_width + width_scale = self.to_width width_half_pad = 0 - scale_factor = float(to_width) / float( - resolution_width) - self.log.debug(scale_factor) + scale_factor = float(self.to_width) / (float( + resolution_width) * pixel_aspect) + self.log.debug( + "__ scale_factor: `{}`".format( + scale_factor)) height_scale = int( resolution_height * scale_factor) height_half_pad = int( - (to_height - height_scale)/2) + (self.to_height - height_scale)/2) self.log.debug( "__ width_scale: `{}`".format(width_scale)) @@ -287,7 +304,7 @@ class ExtractReview(pyblish.api.InstancePlugin): "scale={0}x{1}:flags=lanczos," "pad={2}:{3}:{4}:{5}:black,setsar=1" ).format(width_scale, height_scale, - to_width, to_height, + self.to_width, self.to_height, width_half_pad, height_half_pad ) @@ -372,6 +389,11 @@ class ExtractReview(pyblish.api.InstancePlugin): if "delete" in repre.get("tags", []): representations_new.remove(repre) + instance.data.update({ + "reviewToWidth": self.to_width, + "reviewToHeight": self.to_height + }) + self.log.debug( "new representations: {}".format(representations_new)) instance.data["representations"] = representations_new diff --git a/pype/plugins/global/publish/extract_review_slate.py b/pype/plugins/global/publish/extract_review_slate.py index 699ed4a5eb..8c33a0d853 100644 --- a/pype/plugins/global/publish/extract_review_slate.py +++ b/pype/plugins/global/publish/extract_review_slate.py @@ -24,24 +24,36 @@ class ExtractReviewSlate(pype.api.Extractor): slate_path = inst_data.get("slateFrame") ffmpeg_path = pype.lib.get_ffmpeg_tool_path("ffmpeg") - to_width = 1920 - to_height = 1080 + # values are set in ExtractReview + to_width = inst_data["reviewToWidth"] + to_height = inst_data["reviewToHeight"] + resolution_width = inst_data.get("resolutionWidth", to_width) resolution_height = inst_data.get("resolutionHeight", to_height) pixel_aspect = inst_data.get("pixelAspect", 1) fps = inst_data.get("fps") # defining image ratios - resolution_ratio = float(resolution_width / ( - resolution_height * pixel_aspect)) + resolution_ratio = (float(resolution_width) * pixel_aspect) / resolution_height delivery_ratio = float(to_width) / float(to_height) - self.log.debug(resolution_ratio) - self.log.debug(delivery_ratio) + self.log.debug("__ resolution_ratio: `{}`".format(resolution_ratio)) + self.log.debug("__ delivery_ratio: `{}`".format(delivery_ratio)) # get scale factor - scale_factor = to_height / ( + scale_factor = float(to_height) / ( resolution_height * pixel_aspect) - self.log.debug(scale_factor) + + # shorten two decimals long float number for testing conditions + resolution_ratio_test = float( + "{:0.2f}".format(resolution_ratio)) + delivery_ratio_test = float( + "{:0.2f}".format(delivery_ratio)) + + if resolution_ratio_test < delivery_ratio_test: + scale_factor = float(to_width) / ( + resolution_width * pixel_aspect) + + self.log.debug("__ scale_factor: `{}`".format(scale_factor)) for i, repre in enumerate(inst_data["representations"]): _remove_at_end = [] @@ -95,7 +107,7 @@ class ExtractReviewSlate(pype.api.Extractor): # scaling none square pixels and 1920 width if "reformat" in p_tags: - if resolution_ratio < delivery_ratio: + if resolution_ratio_test < delivery_ratio_test: self.log.debug("lower then delivery") width_scale = int(to_width * scale_factor) width_half_pad = int(( @@ -106,7 +118,8 @@ class ExtractReviewSlate(pype.api.Extractor): self.log.debug("heigher then delivery") width_scale = to_width width_half_pad = 0 - scale_factor = float(to_width) / float(resolution_width) + scale_factor = float(to_width) / (float( + resolution_width) * pixel_aspect) self.log.debug(scale_factor) height_scale = int( resolution_height * scale_factor) diff --git a/pype/plugins/nuke/publish/collect_writes.py b/pype/plugins/nuke/publish/collect_writes.py index 993b8574f5..a547fd70bd 100644 --- a/pype/plugins/nuke/publish/collect_writes.py +++ b/pype/plugins/nuke/publish/collect_writes.py @@ -46,7 +46,6 @@ class CollectNukeWrites(pyblish.api.InstancePlugin): ) if node["use_limit"].getValue(): - handles = 0 first_frame = int(node["first"].getValue()) last_frame = int(node["last"].getValue()) From 787e9d1295dfa1a7a115d22f14fee8e52dfe8c93 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 13 Mar 2020 14:31:01 +0100 Subject: [PATCH 18/55] feat(nks): optional version sync with project workfile --- .../nukestudio/publish/collect_clips.py | 4 +--- .../publish/collect_instance_version.py | 20 +++++++++++++++++++ .../nukestudio/publish/collect_plates.py | 9 ++++++++- 3 files changed, 29 insertions(+), 4 deletions(-) create mode 100644 pype/plugins/nukestudio/publish/collect_instance_version.py diff --git a/pype/plugins/nukestudio/publish/collect_clips.py b/pype/plugins/nukestudio/publish/collect_clips.py index b8654b0784..6a1dad9a6d 100644 --- a/pype/plugins/nukestudio/publish/collect_clips.py +++ b/pype/plugins/nukestudio/publish/collect_clips.py @@ -18,7 +18,6 @@ class CollectClips(api.ContextPlugin): context.data["assetsShared"] = dict() projectdata = context.data["projectEntity"]["data"] - version = context.data.get("version", "001") sequence = context.data.get("activeSequence") selection = context.data.get("selection") @@ -108,8 +107,7 @@ class CollectClips(api.ContextPlugin): "family": "clip", "families": [], "handleStart": projectdata.get("handleStart", 0), - "handleEnd": projectdata.get("handleEnd", 0), - "version": int(version)}) + "handleEnd": projectdata.get("handleEnd", 0)}) instance = context.create_instance(**data) diff --git a/pype/plugins/nukestudio/publish/collect_instance_version.py b/pype/plugins/nukestudio/publish/collect_instance_version.py new file mode 100644 index 0000000000..3e2eb8e8f8 --- /dev/null +++ b/pype/plugins/nukestudio/publish/collect_instance_version.py @@ -0,0 +1,20 @@ +from pyblish import api + +class CollectInstanceVersion(api.InstancePlugin): + """ Collecting versions of Hiero project into instances + + If activated then any subset version is created in + version of the actual project. + """ + + order = api.CollectorOrder + 0.015 + label = "Collect Instance Version" + + optional = True + active = True + + def process(self, instance): + version = instance.context.data.get("version", "001") + instance.data.update({ + "version": int(version) + }) diff --git a/pype/plugins/nukestudio/publish/collect_plates.py b/pype/plugins/nukestudio/publish/collect_plates.py index d08f69d4bb..4ed281f0ee 100644 --- a/pype/plugins/nukestudio/publish/collect_plates.py +++ b/pype/plugins/nukestudio/publish/collect_plates.py @@ -126,7 +126,7 @@ class CollectPlatesData(api.InstancePlugin): transfer_data = [ "handleStart", "handleEnd", "sourceIn", "sourceOut", "frameStart", "frameEnd", "sourceInH", "sourceOutH", "clipIn", "clipOut", - "clipInH", "clipOutH", "asset", "track", "version", "resolutionWidth", "resolutionHeight", "pixelAspect", "fps" + "clipInH", "clipOutH", "asset", "track", "resolutionWidth", "resolutionHeight", "pixelAspect", "fps" ] # pass data to version @@ -141,6 +141,13 @@ class CollectPlatesData(api.InstancePlugin): "fps": instance.context.data["fps"] }) + version = instance.data.get("version") + if version: + version_data.update({ + "version": version + }) + + try: basename, ext = os.path.splitext(source_file) head, padding = os.path.splitext(basename) From fa4c19083d5e0d48887b2a8bf3df506ccb792205 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 13 Mar 2020 14:35:03 +0100 Subject: [PATCH 19/55] fix(nks): no need to have class activation switcher - it will be controlled by presets --- pype/plugins/nukestudio/publish/collect_instance_version.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pype/plugins/nukestudio/publish/collect_instance_version.py b/pype/plugins/nukestudio/publish/collect_instance_version.py index 3e2eb8e8f8..82cbf201d8 100644 --- a/pype/plugins/nukestudio/publish/collect_instance_version.py +++ b/pype/plugins/nukestudio/publish/collect_instance_version.py @@ -1,5 +1,6 @@ from pyblish import api + class CollectInstanceVersion(api.InstancePlugin): """ Collecting versions of Hiero project into instances @@ -10,9 +11,6 @@ class CollectInstanceVersion(api.InstancePlugin): order = api.CollectorOrder + 0.015 label = "Collect Instance Version" - optional = True - active = True - def process(self, instance): version = instance.context.data.get("version", "001") instance.data.update({ From 569818a25db263b3dbdd59c1b7d952a939744e92 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 13 Mar 2020 14:42:36 +0100 Subject: [PATCH 20/55] fix(nks): moving order just behind Collect Clips --- pype/plugins/nukestudio/publish/collect_instance_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/nukestudio/publish/collect_instance_version.py b/pype/plugins/nukestudio/publish/collect_instance_version.py index 82cbf201d8..b79ccbdf54 100644 --- a/pype/plugins/nukestudio/publish/collect_instance_version.py +++ b/pype/plugins/nukestudio/publish/collect_instance_version.py @@ -8,7 +8,7 @@ class CollectInstanceVersion(api.InstancePlugin): version of the actual project. """ - order = api.CollectorOrder + 0.015 + order = api.CollectorOrder + 0.011 label = "Collect Instance Version" def process(self, instance): From 7244f78b66533a5e59dd4abc8df34b364fb07171 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 13 Mar 2020 18:04:59 +0100 Subject: [PATCH 21/55] feat(nuke): adding icon for pype favorite folders --- res/icons/folder-favorite.png | Bin 0 -> 7008 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 res/icons/folder-favorite.png diff --git a/res/icons/folder-favorite.png b/res/icons/folder-favorite.png new file mode 100644 index 0000000000000000000000000000000000000000..198b289e9ed39e6f8e13b6debf5aee857039a032 GIT binary patch literal 7008 zcmaJ_XH-*Lus(p1jw1C+vw}!fQKYLhk*Mrn zP>c@d{}ymU+okrNVpIC##aHeHMl#uRDV)Yn5ATQkFKF~hjia{WF1|Zo9P-reO2-FW zs+i=FrlgQeB?)J(<&BF~8m|YHI{d@>K5)3yG}ey&3iv!Ok=n^)xLkiIjC4VA=w(RNOI3UmoSJ-sf+#ZgA!>3L@%^r?No`;GK z2MSNK?RlTX{`i)VEJonR(e{pmg`d-sm9!pmZDt7i6N^7V#_$K7(r8H!=>iw)UWF1n zWBmkSx!1wNuBA2AwITi+KdB1GROGJ&>wR%c#echOrvQ6?u|w9}>CpgQsztBHH

XIfm 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 Date: Fri, 13 Mar 2020 19:24:07 +0100 Subject: [PATCH 22/55] feat(nuke): adding context_favorite --- pype/nuke/utils.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/pype/nuke/utils.py b/pype/nuke/utils.py index 7583221696..c7f98efaea 100644 --- a/pype/nuke/utils.py +++ b/pype/nuke/utils.py @@ -3,6 +3,23 @@ import nuke from avalon.nuke import lib as anlib +def set_context_favorites(favorites={}): + """ Addig favorite folders to nuke's browser + + Argumets: + favorites (dict): couples of {name:path} + """ + dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) + icon_path = os.path.join(dir, 'res', 'icons', 'folder-favorite.png') + + for name, path in favorites.items(): + nuke.addFavoriteDir( + name, + path, + nuke.IMAGE | nuke.SCRIPT | nuke.GEO, + icon=icon_path) + + def get_node_outputs(node): ''' Return a dictionary of the nodes and pipes that are connected to node From 13d1e6bf4261e1b9654b01956a37655b07311d00 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 13 Mar 2020 19:24:45 +0100 Subject: [PATCH 23/55] feat(nuke): adding bookmarks --- pype/nuke/__init__.py | 2 +- pype/nuke/lib.py | 28 ++++++++++++++++++++++++---- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/pype/nuke/__init__.py b/pype/nuke/__init__.py index f1f87e40c8..e775468996 100644 --- a/pype/nuke/__init__.py +++ b/pype/nuke/__init__.py @@ -93,11 +93,11 @@ def install(): # Set context settings. nuke.addOnCreate(workfile_settings.set_context_settings, nodeClass="Root") + nuke.addOnCreate(workfile_settings.set_favorites, nodeClass="Root") menu.install() - def launch_workfiles_app(): '''Function letting start workfiles after start of host ''' diff --git a/pype/nuke/lib.py b/pype/nuke/lib.py index dedc42fa1d..3130717a75 100644 --- a/pype/nuke/lib.py +++ b/pype/nuke/lib.py @@ -15,13 +15,12 @@ import nuke from .presets import ( get_colorspace_preset, get_node_dataflow_preset, - get_node_colorspace_preset -) - -from .presets import ( + get_node_colorspace_preset, get_anatomy ) +from .utils import set_context_favorites + from pypeapp import Logger log = Logger().get_logger(__name__, "nuke") @@ -944,6 +943,27 @@ class WorkfileSettings(object): # add colorspace menu item self.set_colorspace() + def set_favorites(self): + projects_root = os.getenv("AVALON_PROJECTS") + work_dir = os.getenv("AVALON_WORKDIR") + asset = os.getenv("AVALON_ASSET") + project = os.getenv("AVALON_PROJECT") + hierarchy = os.getenv("AVALON_HIERARCHY") + favorite_items = OrderedDict() + + # project + favorite_items.update({"Projects root": projects_root}) + favorite_items.update({"Project dir": os.path.join( + projects_root, project).replace("\\", "/")}) + # shot + favorite_items.update({"Shot dir": os.path.join( + projects_root, project, + hierarchy, asset).replace("\\", "/")}) + # workdir + favorite_items.update({"Work dir": work_dir}) + + set_context_favorites(favorite_items) + def get_hierarchical_attr(entity, attr, default=None): attr_parts = attr.split('.') From 562e59880a118410604b3b4e160d1d9d8999c30f Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Fri, 13 Mar 2020 23:49:03 +0100 Subject: [PATCH 24/55] use frameEndHandles for review publish --- pype/plugins/nuke/publish/collect_writes.py | 8 ++++---- pype/plugins/nuke/publish/extract_render_local.py | 4 ++-- pype/plugins/nuke/publish/validate_rendered_frames.py | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pype/plugins/nuke/publish/collect_writes.py b/pype/plugins/nuke/publish/collect_writes.py index 993b8574f5..0dc7c81fae 100644 --- a/pype/plugins/nuke/publish/collect_writes.py +++ b/pype/plugins/nuke/publish/collect_writes.py @@ -36,7 +36,6 @@ class CollectNukeWrites(pyblish.api.InstancePlugin): output_type = "mov" # Get frame range - handles = instance.context.data['handles'] handle_start = instance.context.data["handleStart"] handle_end = instance.context.data["handleEnd"] first_frame = int(nuke.root()["first_frame"].getValue()) @@ -46,7 +45,6 @@ class CollectNukeWrites(pyblish.api.InstancePlugin): ) if node["use_limit"].getValue(): - handles = 0 first_frame = int(node["first"].getValue()) last_frame = int(node["last"].getValue()) @@ -134,8 +132,10 @@ class CollectNukeWrites(pyblish.api.InstancePlugin): "label": label, "handleStart": handle_start, "handleEnd": handle_end, - "frameStart": first_frame, - "frameEnd": last_frame, + "frameStart": first_frame + handle_start, + "frameEnd": last_frame - handle_end, + "frameStartHandle": first_frame, + "frameEndHandle": last_frame, "outputType": output_type, "family": "write", "families": families, diff --git a/pype/plugins/nuke/publish/extract_render_local.py b/pype/plugins/nuke/publish/extract_render_local.py index 9b8baa468b..5467d239c2 100644 --- a/pype/plugins/nuke/publish/extract_render_local.py +++ b/pype/plugins/nuke/publish/extract_render_local.py @@ -27,13 +27,13 @@ class NukeRenderLocal(pype.api.Extractor): self.log.debug("instance collected: {}".format(instance.data)) - first_frame = instance.data.get("frameStart", None) + first_frame = instance.data.get("frameStartHandle", None) # exception for slate workflow if "slate" in instance.data["families"]: first_frame -= 1 - last_frame = instance.data.get("frameEnd", None) + last_frame = instance.data.get("frameEndHandle", None) node_subset_name = instance.data.get("name", None) self.log.info("Starting render") diff --git a/pype/plugins/nuke/publish/validate_rendered_frames.py b/pype/plugins/nuke/publish/validate_rendered_frames.py index 8a8bf3cc5e..6e9b91dd72 100644 --- a/pype/plugins/nuke/publish/validate_rendered_frames.py +++ b/pype/plugins/nuke/publish/validate_rendered_frames.py @@ -51,7 +51,7 @@ class ValidateRenderedFrames(pyblish.api.InstancePlugin): collection = collections[0] frame_length = int( - instance.data["frameEnd"] - instance.data["frameStart"] + 1 + instance.data["frameEndHandle"] - instance.data["frameStartHandle"] + 1 ) if frame_length != 1: From 9265f20cc7cd440746543e7d6bc8e299045a4725 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Fri, 13 Mar 2020 23:50:45 +0100 Subject: [PATCH 25/55] use frame range with handles in global review plugins --- pype/plugins/global/publish/extract_burnin.py | 19 ++++++----- pype/plugins/global/publish/extract_review.py | 33 ++++++++++--------- 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/pype/plugins/global/publish/extract_burnin.py b/pype/plugins/global/publish/extract_burnin.py index faecbb47a7..1251e5c02f 100644 --- a/pype/plugins/global/publish/extract_burnin.py +++ b/pype/plugins/global/publish/extract_burnin.py @@ -35,17 +35,20 @@ class ExtractBurnin(pype.api.Extractor): context_data.get("handleStart")) handle_end = instance.data.get("handleEnd", context_data.get("handleEnd")) - duration = frame_end - frame_start + 1 + + frame_start_handle = frame_start - handle_start + frame_end_handle = frame_end + handle_end + duration = frame_end_handle - frame_start_handle + 1 prep_data = copy.deepcopy(instance.data["anatomyData"]) if "slate.farm" in instance.data["families"]: - frame_start += 1 + frame_start_handle += 1 duration -= 1 prep_data.update({ - "frame_start": frame_start, - "frame_end": frame_end, + "frame_start": frame_start_handle, + "frame_end": frame_end_handle, "duration": duration, "version": int(version), "comment": instance.context.data.get("comment", ""), @@ -99,13 +102,13 @@ class ExtractBurnin(pype.api.Extractor): _prep_data["anatomy"] = filled_anatomy.get_solved() # copy frame range variables - frame_start_cp = frame_start - frame_end_cp = frame_end + frame_start_cp = frame_start_handle + frame_end_cp = frame_end_handle duration_cp = duration if no_handles: - frame_start_cp = frame_start + handle_start - frame_end_cp = frame_end - handle_end + frame_start_cp = frame_start + frame_end_cp = frame_end duration_cp = frame_end_cp - frame_start_cp + 1 _prep_data.update({ "frame_start": frame_start_cp, diff --git a/pype/plugins/global/publish/extract_review.py b/pype/plugins/global/publish/extract_review.py index 23e582edd2..abe3d36758 100644 --- a/pype/plugins/global/publish/extract_review.py +++ b/pype/plugins/global/publish/extract_review.py @@ -166,30 +166,33 @@ class ExtractReview(pyblish.api.InstancePlugin): # necessary input data # adds start arg only if image sequence + + frame_start_handle = frame_start - handle_start + frame_end_handle = frame_end + handle_end if isinstance(repre["files"], list): - if frame_start != repre.get("detectedStart", frame_start): - frame_start = repre.get("detectedStart") + if frame_start_handle != repre.get("detectedStart", frame_start_handle): + frame_start_handle = repre.get("detectedStart") # exclude handle if no handles defined if no_handles: - frame_start_no_handles = frame_start + handle_start - frame_end_no_handles = frame_end - handle_end + frame_start_handle = frame_start + frame_end_handle = frame_end input_args.append( "-start_number {0} -framerate {1}".format( - frame_start, fps)) + frame_start_handle, fps)) else: if no_handles: start_sec = float(handle_start) / fps input_args.append("-ss {:0.2f}".format(start_sec)) - frame_start_no_handles = frame_start + handle_start - frame_end_no_handles = frame_end - handle_end + frame_start_handle = frame_start + frame_end_handle = frame_end input_args.append("-i {}".format(full_input_path)) for audio in instance.data.get("audio", []): offset_frames = ( - instance.data.get("startFrameReview") - + instance.data.get("frameStartFtrack") - audio["offset"] ) offset_seconds = offset_frames / fps @@ -264,10 +267,8 @@ class ExtractReview(pyblish.api.InstancePlugin): output_args.append("-shortest") if no_handles: - duration_sec = float( - (frame_end - ( - frame_start + handle_start - ) + 1) - handle_end) / fps + duration_sec = float(frame_end_handle - frame_start_handle + 1) / fps + output_args.append("-t {:0.2f}".format(duration_sec)) # output filename @@ -383,7 +384,9 @@ class ExtractReview(pyblish.api.InstancePlugin): "codec": codec_args, "_profile": profile, "resolutionHeight": resolution_height, - "resolutionWidth": resolution_width + "resolutionWidth": resolution_width, + "frameStartFtrack": frame_start_handle, + "frameEndFtrack": frame_end_handle }) if is_sequence: repre_new.update({ @@ -393,8 +396,8 @@ class ExtractReview(pyblish.api.InstancePlugin): if no_handles: repre_new.update({ "outputName": name + "_noHandles", - "startFrameReview": frame_start_no_handles, - "endFrameReview": frame_end_no_handles + "frameStartFtrack": frame_start, + "frameEndFtrack": frame_end }) if repre_new.get('preview'): repre_new.pop("preview") From 0c296ae14db22f7b961b7c8784cafee2ac2d254e Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Fri, 13 Mar 2020 23:51:44 +0100 Subject: [PATCH 26/55] rename startFrameReview to frameStartFtrack --- pype/plugins/ftrack/publish/integrate_ftrack_instances.py | 4 ++-- pype/plugins/maya/publish/extract_quicktime.py | 8 ++------ .../plugins/standalonepublisher/publish/extract_review.py | 4 ++-- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/pype/plugins/ftrack/publish/integrate_ftrack_instances.py b/pype/plugins/ftrack/publish/integrate_ftrack_instances.py index 78583b0a2f..ec57f46d61 100644 --- a/pype/plugins/ftrack/publish/integrate_ftrack_instances.py +++ b/pype/plugins/ftrack/publish/integrate_ftrack_instances.py @@ -73,9 +73,9 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): ''' start_frame = 0 end_frame = 1 - if 'endFrameReview' in comp and 'startFrameReview' in comp: + if 'frameEndFtrack' in comp and 'frameStartFtrack' in comp: end_frame += ( - comp['endFrameReview'] - comp['startFrameReview'] + comp['frameEndFtrack'] - comp['frameStartFtrack'] ) else: end_frame += ( diff --git a/pype/plugins/maya/publish/extract_quicktime.py b/pype/plugins/maya/publish/extract_quicktime.py index 94b5a716a2..29d6b78051 100644 --- a/pype/plugins/maya/publish/extract_quicktime.py +++ b/pype/plugins/maya/publish/extract_quicktime.py @@ -33,17 +33,13 @@ class ExtractQuicktime(pype.api.Extractor): # if start and end frames cannot be determined, get them # from Maya timeline - start = instance.data.get("startFrameReview") - end = instance.data.get("endFrameReview") + start = instance.data.get("frameStartFtrack") + end = instance.data.get("frameEndFtrack") if start is None: start = cmds.playbackOptions(query=True, animationStartTime=True) if end is None: end = cmds.playbackOptions(query=True, animationEndTime=True) self.log.info("start: {}, end: {}".format(start, end)) - handles = instance.data.get("handles", 0) - if handles: - start -= handles - end += handles # get cameras camera = instance.data['review_camera'] diff --git a/pype/plugins/standalonepublisher/publish/extract_review.py b/pype/plugins/standalonepublisher/publish/extract_review.py index 66cdcdf4df..36793d4c62 100644 --- a/pype/plugins/standalonepublisher/publish/extract_review.py +++ b/pype/plugins/standalonepublisher/publish/extract_review.py @@ -170,8 +170,8 @@ class ExtractReviewSP(pyblish.api.InstancePlugin): "stagingDir": out_stagigng_dir, "tags": new_tags, "outputName": name, - "startFrameReview": 1, - "endFrameReview": video_len + "frameStartFtrack": 1, + "frameEndFtrack": video_len }) # cleanup thumbnail from new repre if repre_new.get("thumbnail"): From fb915f2366dd00a10443411af64122ae03bb3042 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Fri, 13 Mar 2020 23:52:17 +0100 Subject: [PATCH 27/55] use frame range with handles in maya --- pype/plugins/maya/publish/collect_instances.py | 13 +++++++++---- pype/plugins/maya/publish/collect_render.py | 14 ++++++++------ pype/plugins/maya/publish/collect_review.py | 10 ++++++---- pype/plugins/maya/publish/submit_maya_deadline.py | 6 +++--- 4 files changed, 26 insertions(+), 17 deletions(-) diff --git a/pype/plugins/maya/publish/collect_instances.py b/pype/plugins/maya/publish/collect_instances.py index 39d7bcd86d..5af717ba4d 100644 --- a/pype/plugins/maya/publish/collect_instances.py +++ b/pype/plugins/maya/publish/collect_instances.py @@ -103,16 +103,22 @@ class CollectInstances(pyblish.api.ContextPlugin): # Store the exact members of the object set instance.data["setMembers"] = members - # Define nice label name = cmds.ls(objset, long=False)[0] # use short name label = "{0} ({1})".format(name, data["asset"]) + if "handles" in data: + data["handleStart"] = data["handles"] + data["handleEnd"] = data["handles"] + # Append start frame and end frame to label if present if "frameStart" and "frameEnd" in data: - label += " [{0}-{1}]".format(int(data["frameStart"]), - int(data["frameEnd"])) + data["frameStartHandle"] = data["frameStart"] - data["handleStart"] + data["frameEndHandle"] = data["frameEnd"] + data["handleEnd"] + + label += " [{0}-{1}]".format(int(data["frameStartHandle"]), + int(data["frameEndHandle"])) instance.data["label"] = label @@ -122,7 +128,6 @@ class CollectInstances(pyblish.api.ContextPlugin): # user interface interested in visualising it. self.log.info("Found: \"%s\" " % instance.data["name"]) self.log.debug("DATA: \"%s\" " % instance.data) - def sort_by_family(instance): """Sort by family""" diff --git a/pype/plugins/maya/publish/collect_render.py b/pype/plugins/maya/publish/collect_render.py index f31198448b..f3ea1ccee5 100644 --- a/pype/plugins/maya/publish/collect_render.py +++ b/pype/plugins/maya/publish/collect_render.py @@ -211,17 +211,19 @@ class CollectMayaRender(pyblish.api.ContextPlugin): "attachTo": attachTo, "setMembers": layer_name, "publish": True, - "frameStart": int(self.get_render_attribute("startFrame", + "frameStart": int(context.data["assetEntity"]['data']['frameStart']), + "frameEnd": int(context.data["assetEntity"]['data']['frameEnd']), + "frameStartHandle": int(self.get_render_attribute("startFrame", layer=layer_name)), - "frameEnd": int(self.get_render_attribute("endFrame", + "frameEndHandle": int(self.get_render_attribute("endFrame", layer=layer_name)), "byFrameStep": int( self.get_render_attribute("byFrameStep", layer=layer_name)), "renderer": self.get_render_attribute("currentRenderer", layer=layer_name), - "handleStart": context.data["assetEntity"]['data']['handleStart'], - "handleEnd": context.data["assetEntity"]['data']['handleEnd'], + "handleStart": int(context.data["assetEntity"]['data']['handleStart']), + "handleEnd": int(context.data["assetEntity"]['data']['handleEnd']), # instance subset "family": "renderlayer", @@ -259,8 +261,8 @@ class CollectMayaRender(pyblish.api.ContextPlugin): # Define nice label label = "{0} ({1})".format(expected_layer_name, data["asset"]) - label += " [{0}-{1}]".format(int(data["frameStart"]), - int(data["frameEnd"])) + label += " [{0}-{1}]".format(int(data["frameStartHandle"]), + int(data["frameEndHandle"])) instance = context.create_instance(expected_layer_name) instance.data["label"] = label diff --git a/pype/plugins/maya/publish/collect_review.py b/pype/plugins/maya/publish/collect_review.py index 18eee78a9c..9b6027b98d 100644 --- a/pype/plugins/maya/publish/collect_review.py +++ b/pype/plugins/maya/publish/collect_review.py @@ -54,8 +54,10 @@ class CollectReview(pyblish.api.InstancePlugin): self.log.debug('adding review family to {}'.format(reviewable_subset)) data['review_camera'] = camera # data["publish"] = False - data['startFrameReview'] = instance.data["frameStart"] - data['endFrameReview'] = instance.data["frameEnd"] + data['frameStartFtrack'] = instance.data["frameStartHandle"] + data['frameEndFtrack'] = instance.data["frameEndHandle"] + data['frameStartHandle'] = instance.data["frameStartHandle"] + data['frameEndHandle'] = instance.data["frameEndHandle"] data["frameStart"] = instance.data["frameStart"] data["frameEnd"] = instance.data["frameEnd"] data['handles'] = instance.data['handles'] @@ -69,8 +71,8 @@ class CollectReview(pyblish.api.InstancePlugin): else: instance.data['subset'] = task + 'Review' instance.data['review_camera'] = camera - instance.data['startFrameReview'] = instance.data["frameStart"] - instance.data['endFrameReview'] = instance.data["frameEnd"] + instance.data['frameStartFtrack'] = instance.data["frameStartHandle"] + instance.data['frameEndFtrack'] = instance.data["frameEndHandle"] # make ftrack publishable instance.data["families"] = ['ftrack'] diff --git a/pype/plugins/maya/publish/submit_maya_deadline.py b/pype/plugins/maya/publish/submit_maya_deadline.py index bd8497152e..7547f34ba1 100644 --- a/pype/plugins/maya/publish/submit_maya_deadline.py +++ b/pype/plugins/maya/publish/submit_maya_deadline.py @@ -234,8 +234,8 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): "Plugin": instance.data.get("mayaRenderPlugin", "MayaBatch"), "Frames": "{start}-{end}x{step}".format( - start=int(instance.data["frameStart"]), - end=int(instance.data["frameEnd"]), + start=int(instance.data["frameStartHandle"]), + end=int(instance.data["frameEndHandle"]), step=int(instance.data["byFrameStep"]), ), @@ -340,7 +340,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): def preflight_check(self, instance): """Ensure the startFrame, endFrame and byFrameStep are integers""" - for key in ("frameStart", "frameEnd", "byFrameStep"): + for key in ("frameStartHandle", "frameEndHandle", "byFrameStep"): value = instance.data[key] if int(value) == value: From 2017d2315a54a477b324d25e6a0d85e895471967 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Fri, 13 Mar 2020 23:52:35 +0100 Subject: [PATCH 28/55] change workfile subset naming in nuke to match other hosts --- pype/plugins/nuke/publish/collect_workfile.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pype/plugins/nuke/publish/collect_workfile.py b/pype/plugins/nuke/publish/collect_workfile.py index 9c01a3ec97..b95edf0a93 100644 --- a/pype/plugins/nuke/publish/collect_workfile.py +++ b/pype/plugins/nuke/publish/collect_workfile.py @@ -23,11 +23,12 @@ class CollectWorkfile(pyblish.api.ContextPlugin): add_publish_knob(root) family = "workfile" + task = os.getenv("AVALON_TASK", None) # creating instances per write node file_path = context.data["currentFile"] staging_dir = os.path.dirname(file_path) base_name = os.path.basename(file_path) - subset = "{0}_{1}".format(os.getenv("AVALON_TASK", None), family) + subset = family + task.capitalize() # Get frame range first_frame = int(root["first_frame"].getValue()) From bbf03cc11939c9f1fdc013b3be5a247b5d9e1786 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Fri, 13 Mar 2020 23:52:55 +0100 Subject: [PATCH 29/55] use handles correctly in deadline job submitter --- pype/plugins/global/publish/submit_publish_job.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/pype/plugins/global/publish/submit_publish_job.py b/pype/plugins/global/publish/submit_publish_job.py index 3ad7805fe7..47c0272254 100644 --- a/pype/plugins/global/publish/submit_publish_job.py +++ b/pype/plugins/global/publish/submit_publish_job.py @@ -355,8 +355,6 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): aov) staging = os.path.dirname(list(cols[0])[0]) - start = int(instance_data.get("frameStart")) - end = int(instance_data.get("frameEnd")) self.log.info("Creating data for: {}".format(subset_name)) @@ -377,8 +375,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "name": ext, "ext": ext, "files": [os.path.basename(f) for f in list(cols[0])], - "frameStart": start, - "frameEnd": end, + "frameStart": int(instance_data.get("frameStartHandle")), + "frameEnd": int(instance_data.get("frameEndHandle")), # If expectedFile are absolute, we need only filenames "stagingDir": staging, "anatomy_template": "render", @@ -413,8 +411,6 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): """ representations = [] - start = int(instance.get("frameStart")) - end = int(instance.get("frameEnd")) cols, rem = clique.assemble(exp_files) bake_render_path = instance.get("bakeRenderPath") @@ -442,8 +438,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "name": ext, "ext": ext, "files": [os.path.basename(f) for f in list(c)], - "frameStart": start, - "frameEnd": end, + "frameStart": int(instance.get("frameStartHandle")), + "frameEnd": int(instance.get("frameEndHandle")), # If expectedFile are absolute, we need only filenames "stagingDir": os.path.dirname(list(c)[0]), "anatomy_template": "render", @@ -577,6 +573,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "frameEnd": end, "handleStart": handle_start, "handleEnd": handle_end, + "frameStartHandle": start - handle_start, + "frameEndHandle": end + handle_end, "fps": fps, "source": source, "extendFrames": data.get("extendFrames"), From 394f341fb8d904c98a902d0df1da2c298ba28579 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Sat, 14 Mar 2020 00:09:22 +0100 Subject: [PATCH 30/55] use frameStartHandle in nuke rendering --- pype/nuke/lib.py | 4 ++-- pype/plugins/nuke/publish/submit_nuke_deadline.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pype/nuke/lib.py b/pype/nuke/lib.py index dedc42fa1d..e7720c747c 100644 --- a/pype/nuke/lib.py +++ b/pype/nuke/lib.py @@ -1350,8 +1350,8 @@ class ExporterReview: else: self.fname = os.path.basename(self.path_in) self.fhead = os.path.splitext(self.fname)[0] + "." - self.first_frame = self.instance.data.get("frameStart", None) - self.last_frame = self.instance.data.get("frameEnd", None) + self.first_frame = self.instance.data.get("frameStartHandle", None) + self.last_frame = self.instance.data.get("frameEndHandle", None) if "#" in self.fhead: self.fhead = self.fhead.replace("#", "")[:-1] diff --git a/pype/plugins/nuke/publish/submit_nuke_deadline.py b/pype/plugins/nuke/publish/submit_nuke_deadline.py index ee7432e241..0a9ef33398 100644 --- a/pype/plugins/nuke/publish/submit_nuke_deadline.py +++ b/pype/plugins/nuke/publish/submit_nuke_deadline.py @@ -41,8 +41,8 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin): self._ver = re.search(r"\d+\.\d+", context.data.get("hostVersion")) self._deadline_user = context.data.get( "deadlineUser", getpass.getuser()) - self._frame_start = int(instance.data["frameStart"]) - self._frame_end = int(instance.data["frameEnd"]) + self._frame_start = int(instance.data["frameStartHandle"]) + self._frame_end = int(instance.data["frameEndHandle"]) # get output path render_path = instance.data['path'] From 90f55bcb3d9b30b7cf163d3ed345b4daeb9d0ef4 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Sat, 14 Mar 2020 20:24:56 +0100 Subject: [PATCH 31/55] use frameStartFtrack in hiero --- pype/plugins/nukestudio/publish/collect_reviews.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pype/plugins/nukestudio/publish/collect_reviews.py b/pype/plugins/nukestudio/publish/collect_reviews.py index af8fd4a0e7..7cf8d77de4 100644 --- a/pype/plugins/nukestudio/publish/collect_reviews.py +++ b/pype/plugins/nukestudio/publish/collect_reviews.py @@ -78,6 +78,8 @@ class CollectReviews(api.InstancePlugin): file_dir = os.path.dirname(file_path) file = os.path.basename(file_path) ext = os.path.splitext(file)[-1][1:] + handleStart = rev_inst.data.get("handleStart") + handleEnd = rev_inst.data.get("handleEnd") # change label instance.data["label"] = "{0} - {1} - ({2}) - review".format( @@ -86,13 +88,14 @@ class CollectReviews(api.InstancePlugin): self.log.debug("Instance review: {}".format(rev_inst.data["name"])) - # adding representation for review mov representation = { "files": file, "stagingDir": file_dir, "frameStart": rev_inst.data.get("sourceIn"), "frameEnd": rev_inst.data.get("sourceOut"), + "frameStartFtrack": rev_inst.data.get("sourceIn") - handleStart, + "frameEndFtrack": rev_inst.data.get("sourceOut") + handleEnd, "step": 1, "fps": rev_inst.data.get("fps"), "preview": True, From e86f1725bfbcdddad71965ca9a30233238c7886a Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Sat, 14 Mar 2020 21:56:12 +0100 Subject: [PATCH 32/55] add new icon and remove projects folder --- pype/nuke/lib.py | 1 - pype/nuke/utils.py | 2 +- res/icons/folder-favorite2.png | Bin 0 -> 22430 bytes res/icons/folder-favorite3.png | Bin 0 -> 7957 bytes 4 files changed, 1 insertion(+), 2 deletions(-) create mode 100644 res/icons/folder-favorite2.png create mode 100644 res/icons/folder-favorite3.png diff --git a/pype/nuke/lib.py b/pype/nuke/lib.py index 3130717a75..3bbd277ae6 100644 --- a/pype/nuke/lib.py +++ b/pype/nuke/lib.py @@ -952,7 +952,6 @@ class WorkfileSettings(object): favorite_items = OrderedDict() # project - favorite_items.update({"Projects root": projects_root}) favorite_items.update({"Project dir": os.path.join( projects_root, project).replace("\\", "/")}) # shot diff --git a/pype/nuke/utils.py b/pype/nuke/utils.py index c7f98efaea..aa5bc1077e 100644 --- a/pype/nuke/utils.py +++ b/pype/nuke/utils.py @@ -10,7 +10,7 @@ def set_context_favorites(favorites={}): favorites (dict): couples of {name:path} """ dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) - icon_path = os.path.join(dir, 'res', 'icons', 'folder-favorite.png') + icon_path = os.path.join(dir, 'res', 'icons', 'folder-favorite3.png') for name, path in favorites.items(): nuke.addFavoriteDir( diff --git a/res/icons/folder-favorite2.png b/res/icons/folder-favorite2.png new file mode 100644 index 0000000000000000000000000000000000000000..91bc3f0fbedc10687402390cf90e285f3394e2c0 GIT binary patch literal 22430 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 Date: Sat, 14 Mar 2020 22:41:29 +0100 Subject: [PATCH 33/55] call to_heigh and to_width vars from self. --- pype/plugins/global/publish/extract_review.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pype/plugins/global/publish/extract_review.py b/pype/plugins/global/publish/extract_review.py index f67aa0ae94..7b5aba818c 100644 --- a/pype/plugins/global/publish/extract_review.py +++ b/pype/plugins/global/publish/extract_review.py @@ -41,8 +41,8 @@ class ExtractReview(pyblish.api.InstancePlugin): handle_end = inst_data.get("handleEnd", context_data.get("handleEnd")) pixel_aspect = inst_data.get("pixelAspect", 1) - resolution_width = inst_data.get("resolutionWidth", to_width) - resolution_height = inst_data.get("resolutionHeight", to_height) + resolution_width = inst_data.get("resolutionWidth", self.to_width) + resolution_height = inst_data.get("resolutionHeight", self.to_height) self.log.debug("Families In: `{}`".format(inst_data["families"])) self.log.debug("__ frame_start: {}".format(frame_start)) self.log.debug("__ frame_end: {}".format(frame_end)) @@ -223,7 +223,7 @@ class ExtractReview(pyblish.api.InstancePlugin): output_args.extend(profile.get('output', [])) # defining image ratios - resolution_ratio = (float(resolution_width) * pixel_aspect) / resolution_height + resolution_ratio = (float(resolution_width) * pixel_aspect) / resolution_height delivery_ratio = float(self.to_width) / float(self.to_height) self.log.debug( "__ resolution_ratio: `{}`".format(resolution_ratio)) From a78773dc81ddad8f92c886d65969aaec78f6ff98 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Sun, 15 Mar 2020 00:22:58 +0100 Subject: [PATCH 34/55] make sure we fallback to old handles attribute --- pype/plugins/global/publish/collect_avalon_entities.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pype/plugins/global/publish/collect_avalon_entities.py b/pype/plugins/global/publish/collect_avalon_entities.py index 20899361c5..299e2f03be 100644 --- a/pype/plugins/global/publish/collect_avalon_entities.py +++ b/pype/plugins/global/publish/collect_avalon_entities.py @@ -47,6 +47,7 @@ class CollectAvalonEntities(pyblish.api.ContextPlugin): context.data["assetEntity"] = asset_entity data = asset_entity['data'] - context.data['handles'] = int(data.get("handles", 0)) - context.data["handleStart"] = int(data.get("handleStart", 0)) - context.data["handleEnd"] = int(data.get("handleEnd", 0)) + handles = int(data.get("handles", 0)) + context.data["handles"] = handles + context.data["handleStart"] = int(data.get("handleStart", handles)) + context.data["handleEnd"] = int(data.get("handleEnd", handles)) From 37778f57db87e39d7e61901db8cd915f066155e3 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Sun, 15 Mar 2020 00:30:10 +0100 Subject: [PATCH 35/55] rely on ffmpeg start second calculation --- pype/plugins/global/publish/extract_review.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pype/plugins/global/publish/extract_review.py b/pype/plugins/global/publish/extract_review.py index abe3d36758..5c40227494 100644 --- a/pype/plugins/global/publish/extract_review.py +++ b/pype/plugins/global/publish/extract_review.py @@ -183,8 +183,12 @@ class ExtractReview(pyblish.api.InstancePlugin): frame_start_handle, fps)) else: if no_handles: - start_sec = float(handle_start) / fps - input_args.append("-ss {:0.2f}".format(start_sec)) + # start_sec = float(handle_start) / fps + input_args.append( + "-start_number {0} -framerate {1}".format( + handle_start, fps + ) + ) frame_start_handle = frame_start frame_end_handle = frame_end From 317c25a7c52f5a8be517a444737344053431ae85 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Sun, 15 Mar 2020 16:27:55 +0100 Subject: [PATCH 36/55] get frameStartHandle -1 for slate --- pype/plugins/nuke/publish/extract_slate_frame.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/nuke/publish/extract_slate_frame.py b/pype/plugins/nuke/publish/extract_slate_frame.py index 488f9bd31d..0d8bfe9dc5 100644 --- a/pype/plugins/nuke/publish/extract_slate_frame.py +++ b/pype/plugins/nuke/publish/extract_slate_frame.py @@ -77,7 +77,7 @@ class ExtractSlateFrame(pype.api.Extractor): else: fname = os.path.basename(instance.data.get("path", None)) fhead = os.path.splitext(fname)[0] + "." - first_frame = instance.data.get("frameStart", None) - 1 + first_frame = instance.data.get("frameStartHandle", None) - 1 last_frame = first_frame if "#" in fhead: From 58b563ad046cdb09619eda08559462e62d27e3ad Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Sun, 15 Mar 2020 16:28:10 +0100 Subject: [PATCH 37/55] revert ffmpeg frame calculation --- pype/plugins/global/publish/extract_review.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/pype/plugins/global/publish/extract_review.py b/pype/plugins/global/publish/extract_review.py index 5c40227494..abe3d36758 100644 --- a/pype/plugins/global/publish/extract_review.py +++ b/pype/plugins/global/publish/extract_review.py @@ -183,12 +183,8 @@ class ExtractReview(pyblish.api.InstancePlugin): frame_start_handle, fps)) else: if no_handles: - # start_sec = float(handle_start) / fps - input_args.append( - "-start_number {0} -framerate {1}".format( - handle_start, fps - ) - ) + start_sec = float(handle_start) / fps + input_args.append("-ss {:0.2f}".format(start_sec)) frame_start_handle = frame_start frame_end_handle = frame_end From 86a611574b54dd6d53d937a3306fb2dcce6af134 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Mon, 16 Mar 2020 19:02:45 +0100 Subject: [PATCH 38/55] Update README.md --- README.md | 30 +++++------------------------- 1 file changed, 5 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index e254b0ad87..8110887cbd 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,11 @@ Pype ==== -The base studio _config_ for [Avalon](https://getavalon.github.io/) +Welcome to PYPE _config_ for [Avalon](https://getavalon.github.io/) -Currently this config is dependent on our customised avalon instalation so it won't work with vanilla avalon core. We're working on open sourcing all of the necessary code though. You can still get inspiration or take our individual validators and scripts which should work just fine in other pipelines. +To get all the key information about the project, go to [PYPE.club](http://pype.club) + + +Currently this config is dependent on our customised avalon instalation so it won't work with vanilla avalon core. To install it you'll need to download [pype-setup](github.com/pypeclub/pype-setup), which is able to deploy everything for you if you follow the documentation. _This configuration acts as a starting point for all pype club clients wth avalon deployment._ - -Code convention ---------------- - -Below are some of the standard practices applied to this repositories. - -- **Etiquette: PEP8** - - All code is written in PEP8. It is recommended you use a linter as you work, flake8 and pylinter are both good options. -- **Etiquette: Napoleon docstrings** - - Any docstrings are made in Google Napoleon format. See [Napoleon](https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html) for details. - -- **Etiquette: Semantic Versioning** - - This project follows [semantic versioning](http://semver.org). -- **Etiquette: Underscore means private** - - Anything prefixed with an underscore means that it is internal to wherever it is used. For example, a variable name is only ever used in the parent function or class. A module is not for use by the end-user. In contrast, anything without an underscore is public, but not necessarily part of the API. Members of the API resides in `api.py`. - -- **API: Idempotence** - - A public function must be able to be called twice and produce the exact same result. This means no changing of state without restoring previous state when finishing. For example, if a function requires changing the current selection in Autodesk Maya, it must restore the previous selection prior to completing. From 12557526c32e9d38d77d8ba73d3ff8a45975890b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 16 Mar 2020 19:47:29 +0100 Subject: [PATCH 39/55] integrate ftrack note use label from ftrack's custom attribute configuration --- .../ftrack/publish/integrate_ftrack_note.py | 71 +++++++++++++++++-- 1 file changed, 64 insertions(+), 7 deletions(-) diff --git a/pype/plugins/ftrack/publish/integrate_ftrack_note.py b/pype/plugins/ftrack/publish/integrate_ftrack_note.py index 2621ca96ab..9d040585d5 100644 --- a/pype/plugins/ftrack/publish/integrate_ftrack_note.py +++ b/pype/plugins/ftrack/publish/integrate_ftrack_note.py @@ -1,4 +1,5 @@ import sys +import json import pyblish.api import six @@ -18,6 +19,47 @@ class IntegrateFtrackNote(pyblish.api.InstancePlugin): # - note label must exist in Ftrack note_labels = [] + def get_intent_label(self, session, intent_value): + if not intent_value: + return + + intent_configurations = session.query( + "CustomAttributeConfiguration where key is intent" + ).all() + if not intent_configurations: + return + + intent_configuration = intent_configurations[0] + if len(intent_configuration) > 1: + self.log.warning(( + "Found more than one `intent` custom attribute." + " Using first found." + )) + + config = intent_configuration.get("config") + if not config: + return + + items = config.get("data") + if not items: + return + + if sys.version_info[0] < 3: + string_type = basestring + else: + string_type = str + + if isinstance(items, string_type): + items = json.loads(items) + + intent_label = None + for item in items: + if item["value"] == intent_value: + intent_label = item["menu"] + break + + return intent_label + def process(self, instance): comment = (instance.context.data.get("comment") or "").strip() if not comment: @@ -26,17 +68,33 @@ class IntegrateFtrackNote(pyblish.api.InstancePlugin): self.log.debug("Comment is set to `{}`".format(comment)) - intent = instance.context.data.get("intent") - if intent: - msg = "Intent is set to `{}` and was added to comment.".format( - intent - ) + session = instance.context.data["ftrackSession"] + + intent_val = instance.context.data.get("intent", {}).get("value") + intent_label = None + if intent_val: + intent_label = self.get_intent_label(session, intent_val) + if intent_label is None: + intent_label = intent_val + + # if intent label is set then format comment + # - it is possible that intent_label is equal to "" (empty string) + if intent_label: + msg = "Intent label is to `{}`.".format(intent_label) comment = self.note_with_intent_template.format(**{ - "intent": intent, + "intent": intent_val, "comment": comment }) + + elif intent_val: + msg = ( + "Intent is set to `{}` and was not added" + " to comment because label is set to `{}`." + ).format(intent_val, intent_label) + else: msg = "Intent is not set." + self.log.debug(msg) asset_versions_key = "ftrackIntegratedAssetVersions" @@ -45,7 +103,6 @@ class IntegrateFtrackNote(pyblish.api.InstancePlugin): self.log.info("There are any integrated AssetVersions") return - session = instance.context.data["ftrackSession"] user = session.query( "User where username is \"{}\"".format(session.api_user) ).first() From d2f9336b4541ce1ace974b6e162da525b2dad8c3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 16 Mar 2020 19:51:20 +0100 Subject: [PATCH 40/55] updated intent getting --- pype/plugins/ftrack/publish/integrate_ftrack_instances.py | 2 +- pype/plugins/global/publish/extract_burnin.py | 2 +- pype/plugins/nuke/publish/extract_slate_frame.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pype/plugins/ftrack/publish/integrate_ftrack_instances.py b/pype/plugins/ftrack/publish/integrate_ftrack_instances.py index ec57f46d61..591dcf0dc2 100644 --- a/pype/plugins/ftrack/publish/integrate_ftrack_instances.py +++ b/pype/plugins/ftrack/publish/integrate_ftrack_instances.py @@ -127,7 +127,7 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): # Add custom attributes for AssetVersion assetversion_cust_attrs = {} - intent_val = instance.context.data.get("intent") + intent_val = instance.context.data.get("intent", {}).get("value") if intent_val: assetversion_cust_attrs["intent"] = intent_val diff --git a/pype/plugins/global/publish/extract_burnin.py b/pype/plugins/global/publish/extract_burnin.py index 1251e5c02f..be287fbb14 100644 --- a/pype/plugins/global/publish/extract_burnin.py +++ b/pype/plugins/global/publish/extract_burnin.py @@ -52,7 +52,7 @@ class ExtractBurnin(pype.api.Extractor): "duration": duration, "version": int(version), "comment": instance.context.data.get("comment", ""), - "intent": instance.context.data.get("intent", "") + "intent": instance.context.data.get("intent", {}).get("label", "") }) # get anatomy project diff --git a/pype/plugins/nuke/publish/extract_slate_frame.py b/pype/plugins/nuke/publish/extract_slate_frame.py index 0d8bfe9dc5..eff51d95d4 100644 --- a/pype/plugins/nuke/publish/extract_slate_frame.py +++ b/pype/plugins/nuke/publish/extract_slate_frame.py @@ -157,7 +157,7 @@ class ExtractSlateFrame(pype.api.Extractor): return comment = instance.context.data.get("comment") - intent = instance.context.data.get("intent") + intent = instance.context.data.get("intent", {}).get("value") try: node["f_submission_note"].setValue(comment) From 354c8d7a50092e7f998552abc5c6433e2e2b54ff Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 16 Mar 2020 19:51:52 +0100 Subject: [PATCH 41/55] set intent only if set in extract burnin --- pype/plugins/global/publish/extract_burnin.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pype/plugins/global/publish/extract_burnin.py b/pype/plugins/global/publish/extract_burnin.py index be287fbb14..086a1fdfb2 100644 --- a/pype/plugins/global/publish/extract_burnin.py +++ b/pype/plugins/global/publish/extract_burnin.py @@ -51,10 +51,13 @@ class ExtractBurnin(pype.api.Extractor): "frame_end": frame_end_handle, "duration": duration, "version": int(version), - "comment": instance.context.data.get("comment", ""), - "intent": instance.context.data.get("intent", {}).get("label", "") + "comment": instance.context.data.get("comment", "") }) + intent = instance.context.data.get("intent", {}).get("label") + if intent: + prep_data["intent"] = intent + # get anatomy project anatomy = instance.context.data['anatomy'] From 349beaccfc5ca213771325c84e4ef8dec4ecdc0b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 16 Mar 2020 19:52:04 +0100 Subject: [PATCH 42/55] collect anatomy docs cleanup --- pype/plugins/global/publish/collect_anatomy.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pype/plugins/global/publish/collect_anatomy.py b/pype/plugins/global/publish/collect_anatomy.py index ae83e39513..73ae3bb024 100644 --- a/pype/plugins/global/publish/collect_anatomy.py +++ b/pype/plugins/global/publish/collect_anatomy.py @@ -6,10 +6,6 @@ Requires: username -> collect_pype_user *(pyblish.api.CollectorOrder + 0.001) datetimeData -> collect_datetime_data *(pyblish.api.CollectorOrder) -Optional: - comment -> collect_comment *(pyblish.api.CollectorOrder) - intent -> collected in pyblish-lite - Provides: context -> anatomy (pypeapp.Anatomy) context -> anatomyData From 8072f1dcc9736eeacd3387fb8ddc276725c39d7a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 16 Mar 2020 19:52:59 +0100 Subject: [PATCH 43/55] removed intent from required keys in collect rendered files --- pype/plugins/global/publish/collect_rendered_files.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/global/publish/collect_rendered_files.py b/pype/plugins/global/publish/collect_rendered_files.py index 010cf44c15..552fd49f6d 100644 --- a/pype/plugins/global/publish/collect_rendered_files.py +++ b/pype/plugins/global/publish/collect_rendered_files.py @@ -35,7 +35,7 @@ class CollectRenderedFiles(pyblish.api.ContextPlugin): def _process_path(self, data): # validate basic necessary data data_err = "invalid json file - missing data" - required = ["asset", "user", "intent", "comment", + required = ["asset", "user", "comment", "job", "instances", "session", "version"] assert all(elem in data.keys() for elem in required), data_err From d8662f56e6b5f25eba23295f51349e015b0343ca Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 16 Mar 2020 19:57:42 +0100 Subject: [PATCH 44/55] fix(nuke): mixed slashes issue on ocio config path --- pype/nuke/lib.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pype/nuke/lib.py b/pype/nuke/lib.py index e7720c747c..68a73dffac 100644 --- a/pype/nuke/lib.py +++ b/pype/nuke/lib.py @@ -620,7 +620,8 @@ class WorkfileSettings(object): # third set ocio custom path if root_dict.get("customOCIOConfigPath"): self._root_node["customOCIOConfigPath"].setValue( - str(root_dict["customOCIOConfigPath"]).format(**os.environ) + str(root_dict["customOCIOConfigPath"]).format( + **os.environ).replace("\\", "/") ) log.debug("nuke.root()['{}'] changed to: {}".format( "customOCIOConfigPath", root_dict["customOCIOConfigPath"])) From f37ea5d82ae82b9c8c04ba06d86f982a120035ad Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 16 Mar 2020 20:09:14 +0100 Subject: [PATCH 45/55] intent is not required in extract slate frame --- pype/plugins/nuke/publish/extract_slate_frame.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/nuke/publish/extract_slate_frame.py b/pype/plugins/nuke/publish/extract_slate_frame.py index eff51d95d4..369cbe0496 100644 --- a/pype/plugins/nuke/publish/extract_slate_frame.py +++ b/pype/plugins/nuke/publish/extract_slate_frame.py @@ -157,7 +157,7 @@ class ExtractSlateFrame(pype.api.Extractor): return comment = instance.context.data.get("comment") - intent = instance.context.data.get("intent", {}).get("value") + intent = instance.context.data.get("intent", {}).get("value", "") try: node["f_submission_note"].setValue(comment) From 0d20e27cb3d7912fc5f7a7716473c55df495c24c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 16 Mar 2020 20:13:51 +0100 Subject: [PATCH 46/55] it is used default intent label in ftrack if ftrack's not found --- .../ftrack/publish/integrate_ftrack_note.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/pype/plugins/ftrack/publish/integrate_ftrack_note.py b/pype/plugins/ftrack/publish/integrate_ftrack_note.py index 9d040585d5..a0e7719779 100644 --- a/pype/plugins/ftrack/publish/integrate_ftrack_note.py +++ b/pype/plugins/ftrack/publish/integrate_ftrack_note.py @@ -71,18 +71,19 @@ class IntegrateFtrackNote(pyblish.api.InstancePlugin): session = instance.context.data["ftrackSession"] intent_val = instance.context.data.get("intent", {}).get("value") - intent_label = None + intent_label = instance.context.data.get("intent", {}).get("label") + final_label = None if intent_val: - intent_label = self.get_intent_label(session, intent_val) - if intent_label is None: - intent_label = intent_val + final_label = self.get_intent_label(session, intent_val) + if final_label is None: + final_label = intent_label # if intent label is set then format comment # - it is possible that intent_label is equal to "" (empty string) - if intent_label: - msg = "Intent label is to `{}`.".format(intent_label) + if final_label: + msg = "Intent label is set to `{}`.".format(final_label) comment = self.note_with_intent_template.format(**{ - "intent": intent_val, + "intent": final_label, "comment": comment }) @@ -90,7 +91,7 @@ class IntegrateFtrackNote(pyblish.api.InstancePlugin): msg = ( "Intent is set to `{}` and was not added" " to comment because label is set to `{}`." - ).format(intent_val, intent_label) + ).format(intent_val, final_label) else: msg = "Intent is not set." From c91b49510045afc5903db2eff5813bc8a09bab39 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 16 Mar 2020 20:22:02 +0100 Subject: [PATCH 47/55] fix ftrack json parse --- pype/plugins/ftrack/publish/integrate_ftrack_note.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pype/plugins/ftrack/publish/integrate_ftrack_note.py b/pype/plugins/ftrack/publish/integrate_ftrack_note.py index a0e7719779..679010ca58 100644 --- a/pype/plugins/ftrack/publish/integrate_ftrack_note.py +++ b/pype/plugins/ftrack/publish/integrate_ftrack_note.py @@ -40,7 +40,8 @@ class IntegrateFtrackNote(pyblish.api.InstancePlugin): if not config: return - items = config.get("data") + configuration = json.loads(config) + items = configuration.get("data") if not items: return From 64e6dedde0bd0abebab175612016f8c4df99f9c2 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Tue, 17 Mar 2020 11:48:26 +0100 Subject: [PATCH 48/55] handles were returning none which can't be cast to integer --- pype/plugins/global/publish/collect_avalon_entities.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/global/publish/collect_avalon_entities.py b/pype/plugins/global/publish/collect_avalon_entities.py index 299e2f03be..103f5abd1a 100644 --- a/pype/plugins/global/publish/collect_avalon_entities.py +++ b/pype/plugins/global/publish/collect_avalon_entities.py @@ -47,7 +47,7 @@ class CollectAvalonEntities(pyblish.api.ContextPlugin): context.data["assetEntity"] = asset_entity data = asset_entity['data'] - handles = int(data.get("handles", 0)) + handles = int(data.get("handles") or 0) context.data["handles"] = handles context.data["handleStart"] = int(data.get("handleStart", handles)) context.data["handleEnd"] = int(data.get("handleEnd", handles)) From 59f97ef904057093d5b74082aadf7e35501740f3 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Tue, 17 Mar 2020 15:56:46 +0100 Subject: [PATCH 49/55] Update LICENSE --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index dfcd71eb3f..63249bb52b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2018 orbi tools s.r.o +Copyright (c) 2020 Orbi Tools s.r.o. Permission is hereby granted, free of charge, to any person obtaining a copy From a549634c0ad925a038574bc01610f8abf93089a5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 17 Mar 2020 16:12:29 +0100 Subject: [PATCH 50/55] added options to maya reference loader --- pype/maya/plugin.py | 71 ++++++++++++++++++++++++++++++--------------- 1 file changed, 47 insertions(+), 24 deletions(-) diff --git a/pype/maya/plugin.py b/pype/maya/plugin.py index 327cf47cbd..85de5adec5 100644 --- a/pype/maya/plugin.py +++ b/pype/maya/plugin.py @@ -1,4 +1,5 @@ from avalon import api +from avalon.vendor import qargparse def get_reference_node_parents(ref): @@ -33,11 +34,25 @@ class ReferenceLoader(api.Loader): `update` logic. """ - def load(self, - context, - name=None, - namespace=None, - data=None): + + options = [ + qargparse.Integer( + "count", + label="Count", + default=1, + min=1, + help="How many times to load?" + ) + ] + + def load( + self, + context, + name=None, + namespace=None, + options=None, + data=None + ): import os from avalon.maya import lib @@ -46,29 +61,37 @@ class ReferenceLoader(api.Loader): assert os.path.exists(self.fname), "%s does not exist." % self.fname asset = context['asset'] + loaded_containers = [] - namespace = namespace or lib.unique_namespace( - asset["name"] + "_", - prefix="_" if asset["name"][0].isdigit() else "", - suffix="_", - ) + count = options.get("count") or 1 + while count > 0: + count -= 1 + namespace = namespace or lib.unique_namespace( + asset["name"] + "_", + prefix="_" if asset["name"][0].isdigit() else "", + suffix="_", + ) - self.process_reference(context=context, - name=name, - namespace=namespace, - data=data) + self.process_reference( + context=context, + name=name, + namespace=namespace, + data=data + ) - # Only containerize if any nodes were loaded by the Loader - nodes = self[:] - if not nodes: - return + # Only containerize if any nodes were loaded by the Loader + nodes = self[:] + if not nodes: + return - return containerise( - name=name, - namespace=namespace, - nodes=nodes, - context=context, - loader=self.__class__.__name__) + loaded_containers.append(containerise( + name=name, + namespace=namespace, + nodes=nodes, + context=context, + loader=self.__class__.__name__ + )) + return loaded_containers def process_reference(self, context, name, namespace, data): """To be implemented by subclass""" From e10343de2be9a795a42b98bee2d5bd8c55343a44 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 18 Mar 2020 10:24:25 +0100 Subject: [PATCH 51/55] add offset when loading multiple subsets --- pype/maya/plugin.py | 23 ++++++++++++++++++----- pype/plugins/maya/load/load_reference.py | 12 ++++++++---- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/pype/maya/plugin.py b/pype/maya/plugin.py index 85de5adec5..ed244d56df 100644 --- a/pype/maya/plugin.py +++ b/pype/maya/plugin.py @@ -42,6 +42,11 @@ class ReferenceLoader(api.Loader): default=1, min=1, help="How many times to load?" + ), + qargparse.Double3( + "offset", + label="Position Offset", + help="Offset loaded models for easier selection." ) ] @@ -50,8 +55,7 @@ class ReferenceLoader(api.Loader): context, name=None, namespace=None, - options=None, - data=None + options=None ): import os @@ -64,19 +68,25 @@ class ReferenceLoader(api.Loader): loaded_containers = [] count = options.get("count") or 1 - while count > 0: - count -= 1 + for c in range(0, count): namespace = namespace or lib.unique_namespace( asset["name"] + "_", prefix="_" if asset["name"][0].isdigit() else "", suffix="_", ) + # Offset loaded subset + if "offset" in options: + offset = [i * c for i in options["offset"]] + options["translate"] = offset + + self.log.info(options) + self.process_reference( context=context, name=name, namespace=namespace, - data=data + options=options ) # Only containerize if any nodes were loaded by the Loader @@ -91,6 +101,9 @@ class ReferenceLoader(api.Loader): context=context, loader=self.__class__.__name__ )) + + c += 1 + namespace = None return loaded_containers def process_reference(self, context, name, namespace, data): diff --git a/pype/plugins/maya/load/load_reference.py b/pype/plugins/maya/load/load_reference.py index cbd1da7cbd..b1192d9c9e 100644 --- a/pype/plugins/maya/load/load_reference.py +++ b/pype/plugins/maya/load/load_reference.py @@ -1,4 +1,5 @@ import pype.maya.plugin +reload(pype.maya.plugin) from avalon import api, maya from maya import cmds import os @@ -24,7 +25,7 @@ class ReferenceLoader(pype.maya.plugin.ReferenceLoader): icon = "code-fork" color = "orange" - def process_reference(self, context, name, namespace, data): + def process_reference(self, context, name, namespace, options): import maya.cmds as cmds from avalon import maya import pymel.core as pm @@ -101,16 +102,19 @@ class ReferenceLoader(pype.maya.plugin.ReferenceLoader): cmds.setAttr(groupName + ".selectHandleY", cy) cmds.setAttr(groupName + ".selectHandleZ", cz) - if data.get("post_process", True): + if "translate" in options: + cmds.setAttr(groupName + ".t", *options["translate"]) + + if options.get("post_process", True): if family == "rig": - self._post_process_rig(name, namespace, context, data) + self._post_process_rig(name, namespace, context, options) return newNodes def switch(self, container, representation): self.update(container, representation) - def _post_process_rig(self, name, namespace, context, data): + def _post_process_rig(self, name, namespace, context, options): output = next((node for node in self if node.endswith("out_SET")), None) From 6c2b056dd479be7232e95ee7ac6adc7d09eeba24 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 18 Mar 2020 10:51:31 +0100 Subject: [PATCH 52/55] make sure not to offset rigs --- pype/plugins/maya/load/load_reference.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/pype/plugins/maya/load/load_reference.py b/pype/plugins/maya/load/load_reference.py index b1192d9c9e..797933300c 100644 --- a/pype/plugins/maya/load/load_reference.py +++ b/pype/plugins/maya/load/load_reference.py @@ -1,5 +1,4 @@ import pype.maya.plugin -reload(pype.maya.plugin) from avalon import api, maya from maya import cmds import os @@ -102,12 +101,11 @@ class ReferenceLoader(pype.maya.plugin.ReferenceLoader): cmds.setAttr(groupName + ".selectHandleY", cy) cmds.setAttr(groupName + ".selectHandleZ", cz) - if "translate" in options: - cmds.setAttr(groupName + ".t", *options["translate"]) - - if options.get("post_process", True): - if family == "rig": - self._post_process_rig(name, namespace, context, options) + if family == "rig": + self._post_process_rig(name, namespace, context, options) + else: + if "translate" in options: + cmds.setAttr(groupName + ".t", *options["translate"]) return newNodes From 47468403515e33d8b845042a474caad12ac81864 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 18 Mar 2020 12:13:48 +0100 Subject: [PATCH 53/55] add houndci configuration and change flake8 config --- .flake8 | 2 ++ .hound.yml | 0 2 files changed, 2 insertions(+) create mode 100644 .hound.yml diff --git a/.flake8 b/.flake8 index 9de8d23bb2..67ed2d77a3 100644 --- a/.flake8 +++ b/.flake8 @@ -1,5 +1,7 @@ [flake8] # ignore = D203 +ignore = BLK100 +max-line-length = 79 exclude = .git, __pycache__, diff --git a/.hound.yml b/.hound.yml new file mode 100644 index 0000000000..e69de29bb2 From 321c27234a23c169f2ae5cea94a3223f6b7b82fc Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 18 Mar 2020 12:22:38 +0100 Subject: [PATCH 54/55] wrong attribute used for pixelAspec --- pype/plugins/maya/publish/collect_render.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/maya/publish/collect_render.py b/pype/plugins/maya/publish/collect_render.py index f3ea1ccee5..be3878e6bd 100644 --- a/pype/plugins/maya/publish/collect_render.py +++ b/pype/plugins/maya/publish/collect_render.py @@ -238,7 +238,7 @@ class CollectMayaRender(pyblish.api.ContextPlugin): "expectedFiles": full_exp_files, "resolutionWidth": cmds.getAttr("defaultResolution.width"), "resolutionHeight": cmds.getAttr("defaultResolution.height"), - "pixelAspect": cmds.getAttr("defaultResolution.height") + "pixelAspect": cmds.getAttr("defaultResolution.pixelAspect") } # Apply each user defined attribute as data From 1f0383268722a5b85b40b740404c1c221b60dbe8 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 18 Mar 2020 12:50:03 +0100 Subject: [PATCH 55/55] pool was being overwritten in submit publish job --- pype/plugins/global/publish/submit_publish_job.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pype/plugins/global/publish/submit_publish_job.py b/pype/plugins/global/publish/submit_publish_job.py index 47c0272254..dcf19ae32c 100644 --- a/pype/plugins/global/publish/submit_publish_job.py +++ b/pype/plugins/global/publish/submit_publish_job.py @@ -238,8 +238,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): ) i += 1 - # Avoid copied pools and remove secondary pool - payload["JobInfo"]["Pool"] = "none" + # remove secondary pool payload["JobInfo"].pop("SecondaryPool", None) self.log.info("Submitting Deadline job ...")