From 6448380312eea365b376774f09c458e739a5c0b5 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 27 Nov 2020 18:29:29 +0100 Subject: [PATCH] feat(hiero): wip new crator --- pype/hosts/resolve/__init__.py | 16 +- pype/hosts/resolve/lib.py | 123 +++++++- pype/hosts/resolve/pipeline.py | 100 +++++- pype/hosts/resolve/plugin.py | 293 ++++++++++++++++++ .../resolve/create/create_shot_clip_new.py | 31 +- 5 files changed, 538 insertions(+), 25 deletions(-) diff --git a/pype/hosts/resolve/__init__.py b/pype/hosts/resolve/__init__.py index 8b0ca774b5..b8457438c6 100644 --- a/pype/hosts/resolve/__init__.py +++ b/pype/hosts/resolve/__init__.py @@ -19,6 +19,11 @@ from .lib import ( get_current_sequence, get_video_track_names, get_current_track_items, + get_track_item_pype_tag, + set_track_item_pype_tag, + imprint, + set_publish_attribute, + get_publish_attribute, create_current_sequence_media_bin, create_compound_clip, swap_clips, @@ -28,7 +33,10 @@ from .lib import ( from .menu import launch_pype_menu -from .plugin import Creator +from .plugin import ( + Creator, + PublishClip +) from .workio import ( open_file, @@ -63,6 +71,11 @@ __all__ = [ "get_current_sequence", "get_video_track_names", "get_current_track_items", + "get_track_item_pype_tag", + "set_track_item_pype_tag", + "imprint", + "set_publish_attribute", + "get_publish_attribute", "create_current_sequence_media_bin", "create_compound_clip", "swap_clips", @@ -74,6 +87,7 @@ __all__ = [ # plugin "Creator", + "PublishClip", # workio "open_file", diff --git a/pype/hosts/resolve/lib.py b/pype/hosts/resolve/lib.py index e36dc1bb15..4052fa74fd 100644 --- a/pype/hosts/resolve/lib.py +++ b/pype/hosts/resolve/lib.py @@ -1,5 +1,6 @@ import sys import json +import ast from opentimelineio import opentime from pprint import pformat @@ -11,7 +12,7 @@ self = sys.modules[__name__] self.pm = None self.rename_index = 0 self.rename_add = 0 -self.pype_metadata_key = "VFX Notes" +self.pype_tag_name = "VFX Notes" def get_project_manager(): @@ -93,13 +94,121 @@ def get_current_track_items( if filter is True: if selecting_color in ti_color: selected_clips.append(data) - # ti.ClearClipColor() else: selected_clips.append(data) return selected_clips +def get_track_item_pype_tag(track_item): + """ + Get pype track item tag created by creator or loader plugin. + + Attributes: + trackItem (hiero.core.TrackItem): hiero object + + Returns: + hiero.core.Tag: hierarchy, orig clip attributes + """ + return_tag = None + # get all tags from track item + _tags = track_item.GetMetadata() + if not _tags: + return None + for key, data in _tags.items(): + # return only correct tag defined by global name + if key in self.pype_tag_name: + return_tag = json.loads(data) + + return return_tag + + +def set_track_item_pype_tag(track_item, data=None): + """ + Set pype track item tag to input track_item. + + Attributes: + trackItem (hiero.core.TrackItem): hiero object + + Returns: + hiero.core.Tag + """ + data = data or dict() + + # basic Tag's attribute + tag_data = { + "editable": "0", + "note": "Pype data holder", + "icon": "pype_icon.png", + "metadata": {k: v for k, v in data.items()} + } + # get available pype tag if any + _tag = get_track_item_pype_tag(track_item) + + if _tag: + # it not tag then create one + _tag.update(tag_data) + track_item.SetMetadata(self.pype_tag_name, json.dumps(_tag)) + return _tag + else: + # if pype tag available then update with input data + # add it to the input track item + track_item.SetMetadata(self.pype_tag_name, json.dumps(tag_data)) + return tag_data + + +def imprint(track_item, data=None): + """ + Adding `Avalon data` into a hiero track item tag. + + Also including publish attribute into tag. + + Arguments: + track_item (hiero.core.TrackItem): hiero track item object + data (dict): Any data which needst to be imprinted + + Examples: + data = { + 'asset': 'sq020sh0280', + 'family': 'render', + 'subset': 'subsetMain' + } + """ + data = data or {} + + set_track_item_pype_tag(track_item, data) + + # add publish attribute + set_publish_attribute(track_item, True) + + +def set_publish_attribute(track_item, value): + """ Set Publish attribute in input Tag object + + Attribute: + tag (hiero.core.Tag): a tag object + value (bool): True or False + """ + tag_data = get_track_item_pype_tag(track_item) + tag_data["publish"] = str(value) + # set data to the publish attribute + set_track_item_pype_tag(track_item, tag_data) + + +def get_publish_attribute(track_item): + """ Get Publish attribute from input Tag object + + Attribute: + tag (hiero.core.Tag): a tag object + value (bool): True or False + """ + tag_data = get_track_item_pype_tag(track_item) + value = tag_data["publish"] + + # return value converted to bool value. Atring is stored in tag. + return ast.literal_eval(value) + + def create_current_sequence_media_bin(sequence): seq_name = sequence.GetName() media_pool = get_current_project().GetMediaPool() @@ -299,9 +408,9 @@ def create_compound_clip(clip_data, folder, rename=False, **kwargs): project.SetCurrentTimeline(sq_origin) # Add collected metadata and attributes to the comound clip: - if mp_item.GetMetadata(self.pype_metadata_key): - clip_attributes[self.pype_metadata_key] = mp_item.GetMetadata( - self.pype_metadata_key)[self.pype_metadata_key] + if mp_item.GetMetadata(self.pype_tag_name): + clip_attributes[self.pype_tag_name] = mp_item.GetMetadata( + self.pype_tag_name)[self.pype_tag_name] # stringify clip_attributes = json.dumps(clip_attributes) @@ -311,7 +420,7 @@ def create_compound_clip(clip_data, folder, rename=False, **kwargs): cct.SetMetadata(k, v) # add metadata to cct - cct.SetMetadata(self.pype_metadata_key, clip_attributes) + cct.SetMetadata(self.pype_tag_name, clip_attributes) # reset start timecode of the compound clip cct.SetClipProperty("Start TC", mp_props["Start TC"]) @@ -389,7 +498,7 @@ def get_pype_clip_metadata(clip): mp_item = clip.GetMediaPoolItem() metadata = mp_item.GetMetadata() - return metadata.get(self.pype_metadata_key) + return metadata.get(self.pype_tag_name) def get_clip_attributes(clip): diff --git a/pype/hosts/resolve/pipeline.py b/pype/hosts/resolve/pipeline.py index 92bef2e13b..22437980e7 100644 --- a/pype/hosts/resolve/pipeline.py +++ b/pype/hosts/resolve/pipeline.py @@ -3,11 +3,15 @@ Basic avalon integration """ import os import contextlib +from collections import OrderedDict from avalon.tools import workfiles from avalon import api as avalon +from avalon import schema +from avalon.pipeline import AVALON_CONTAINER_ID from pyblish import api as pyblish import pype from pype.api import Logger +from . import lib log = Logger().get_logger(__name__, "resolve") @@ -80,29 +84,46 @@ def uninstall(): avalon.deregister_plugin_path(avalon.InventoryAction, INVENTORY_PATH) -def containerise(obj, +def containerise(track_item, name, namespace, context, loader=None, data=None): - """Bundle Resolve's object into an assembly and imprint it with metadata + """Bundle Hiero's object into an assembly and imprint it with metadata Containerisation enables a tracking of version, author and origin for loaded assets. Arguments: - obj (obj): Resolve's object to imprint as container + track_item (hiero.core.TrackItem): object to imprint as container name (str): Name of resulting assembly namespace (str): Namespace under which to host container context (dict): Asset information loader (str, optional): Name of node used to produce this container. Returns: - obj (obj): containerised object + track_item (hiero.core.TrackItem): containerised object """ - pass + + data_imprint = OrderedDict({ + "schema": "avalon-core:container-2.0", + "id": AVALON_CONTAINER_ID, + "name": str(name), + "namespace": str(namespace), + "loader": str(loader), + "representation": str(context["representation"]["_id"]), + }) + + if data: + for k, v in data.items(): + data_imprint.update({k: v}) + + print("_ data_imprint: {}".format(data_imprint)) + lib.set_track_item_pype_tag(track_item, data_imprint) + + return track_item def ls(): @@ -115,20 +136,77 @@ def ls(): See the `container.json` schema for details on how it should look, and the Maya equivalent, which is in `avalon.maya.pipeline` """ - pass + + # get all track items from current timeline + all_track_items = lib.get_current_track_items(filter=False) + + for track_item_data in all_track_items: + track_item = track_item_data["clip"]["item"] + container = parse_container(track_item) + if container: + yield container -def parse_container(container): - """Return the container node's full container data. +def parse_container(track_item, validate=True): + """Return container data from track_item's pype tag. Args: - container (str): A container node name. + track_item (hiero.core.TrackItem): A containerised track item. + validate (bool)[optional]: validating with avalon scheme Returns: - dict: The container schema data for this container node. + dict: The container schema data for input containerized track item. """ - pass + # convert tag metadata to normal keys names + data = lib.get_track_item_pype_tag(track_item) + + if validate and data and data.get("schema"): + schema.validate(data) + + if not isinstance(data, dict): + return + + # If not all required data return the empty container + required = ['schema', 'id', 'name', + 'namespace', 'loader', 'representation'] + + if not all(key in data for key in required): + return + + container = {key: data[key] for key in required} + + container["objectName"] = track_item.name() + + # Store reference to the node object + container["_track_item"] = track_item + + return container + + +def update_container(track_item, data=None): + """Update container data to input track_item's pype tag. + + Args: + track_item (hiero.core.TrackItem): A containerised track item. + data (dict)[optional]: dictionery with data to be updated + + Returns: + bool: True if container was updated correctly + + """ + data = data or dict() + + container = lib.get_track_item_pype_tag(track_item) + + for _key, _value in container.items(): + try: + container[_key] = data[_key] + except KeyError: + pass + + log.info("Updating container: `{}`".format(track_item)) + return bool(lib.set_track_item_pype_tag(track_item, container)) def launch_workfiles_app(*args): diff --git a/pype/hosts/resolve/plugin.py b/pype/hosts/resolve/plugin.py index a652fbfe64..b465d77950 100644 --- a/pype/hosts/resolve/plugin.py +++ b/pype/hosts/resolve/plugin.py @@ -3,6 +3,7 @@ from avalon import api from pype.hosts import resolve from avalon.vendor import qargparse from pype.api import config +from . import lib from Qt import QtWidgets, QtCore @@ -351,3 +352,295 @@ class Creator(api.Creator): self.selected = resolve.get_current_track_items(filter=False) self.widget = CreatorWidget + + +class PublishClip: + """ + Convert a track item to publishable instance + + Args: + track_item (hiero.core.TrackItem): hiero track item object + kwargs (optional): additional data needed for rename=True (presets) + + Returns: + hiero.core.TrackItem: hiero track item object with pype tag + """ + vertical_clip_match = dict() + tag_data = dict() + types = { + "shot": "shot", + "folder": "folder", + "episode": "episode", + "sequence": "sequence", + "track": "sequence", + } + + # parents search patern + parents_search_patern = r"\{([a-z]*?)\}" + + # default templates for non-ui use + rename_default = False + hierarchy_default = "{_folder_}/{_sequence_}/{_track_}" + clip_name_default = "shot_{_trackIndex_:0>3}_{_clipIndex_:0>4}" + subset_name_default = "" + review_track_default = "< none >" + subset_family_default = "plate" + count_from_default = 10 + count_steps_default = 10 + vertical_sync_default = False + driving_layer_default = "" + + def __init__(self, cls, track_item, **kwargs): + # populate input cls attribute onto self.[attr] + self.__dict__.update(cls.__dict__) + + # get main parent objects + self.track_item = track_item["clip"]["item"] + sequence_name = track_item["sequence"].GetName() + self.sequence_name = str(sequence_name).replace(" ", "_") + + # track item (clip) main attributes + self.ti_name = self.track_item.GetName() + self.ti_index = int(track_item["clip"]["index"]) + + # get track name and index + track_name = track_item["track"]["name"] + self.track_name = str(track_name).replace(" ", "_") + self.track_index = int(track_item["track"]["index"]) + + # adding tag.family into tag + if kwargs.get("avalon"): + self.tag_data.update(kwargs["avalon"]) + + # adding ui inputs if any + self.ui_inputs = kwargs.get("ui_inputs", {}) + + # populate default data before we get other attributes + self._populate_track_item_default_data() + + # use all populated default data to create all important attributes + self._populate_attributes() + + # create parents with correct types + self._create_parents() + + def convert(self): + # solve track item data and add them to tag data + self._convert_to_tag_data() + + # if track name is in review track name and also if driving track name + # is not in review track name: skip tag creation + if (self.track_name in self.review_layer) and ( + self.driving_layer not in self.review_layer): + return + + # deal with clip name + new_name = self.tag_data.pop("newClipName") + + if self.rename: + # rename track item + self.track_item.setName(new_name) + self.tag_data["asset"] = new_name + else: + self.tag_data["asset"] = self.ti_name + + # create pype tag on track_item and add data + lib.imprint(self.track_item, self.tag_data) + + return self.track_item + + def _populate_track_item_default_data(self): + """ Populate default formating data from track item. """ + + self.track_item_default_data = { + "_folder_": "shots", + "_sequence_": self.sequence_name, + "_track_": self.track_name, + "_clip_": self.ti_name, + "_trackIndex_": self.track_index, + "_clipIndex_": self.ti_index + } + + def _populate_attributes(self): + """ Populate main object attributes. """ + # track item frame range and parent track name for vertical sync check + self.clip_in = int(self.track_item.GetStart()) + self.clip_out = int(self.track_item.GetEnd()) + + # define ui inputs if non gui mode was used + self.shot_num = self.ti_index + print( + "____ self.shot_num: {}".format(self.shot_num)) + + # ui_inputs data or default values if gui was not used + self.rename = self.ui_inputs.get( + "clipRename", {}).get("value") or self.rename_default + self.clip_name = self.ui_inputs.get( + "clipName", {}).get("value") or self.clip_name_default + self.hierarchy = self.ui_inputs.get( + "hierarchy", {}).get("value") or self.hierarchy_default + self.hierarchy_data = self.ui_inputs.get( + "hierarchyData", {}).get("value") or \ + self.track_item_default_data.copy() + self.count_from = self.ui_inputs.get( + "countFrom", {}).get("value") or self.count_from_default + self.count_steps = self.ui_inputs.get( + "countSteps", {}).get("value") or self.count_steps_default + self.subset_name = self.ui_inputs.get( + "subsetName", {}).get("value") or self.subset_name_default + self.subset_family = self.ui_inputs.get( + "subsetFamily", {}).get("value") or self.subset_family_default + self.vertical_sync = self.ui_inputs.get( + "vSyncOn", {}).get("value") or self.vertical_sync_default + self.driving_layer = self.ui_inputs.get( + "vSyncTrack", {}).get("value") or self.driving_layer_default + self.review_track = self.ui_inputs.get( + "reviewTrack", {}).get("value") or self.review_track_default + + # build subset name from layer name + if self.subset_name == "": + self.subset_name = self.track_name + + # create subset for publishing + self.subset = self.subset_family + self.subset_name.capitalize() + + def _replace_hash_to_expression(self, name, text): + """ Replace hash with number in correct padding. """ + _spl = text.split("#") + _len = (len(_spl) - 1) + _repl = "{{{0}:0>{1}}}".format(name, _len) + new_text = text.replace(("#" * _len), _repl) + return new_text + + def _convert_to_tag_data(self): + """ Convert internal data to tag data. + + Populating the tag data into internal variable self.tag_data + """ + # define vertical sync attributes + master_layer = True + self.review_layer = "" + if self.vertical_sync: + # check if track name is not in driving layer + if self.track_name not in self.driving_layer: + # if it is not then define vertical sync as None + master_layer = False + + # increasing steps by index of rename iteration + self.count_steps *= self.rename_index + + hierarchy_formating_data = dict() + _data = self.track_item_default_data.copy() + if self.ui_inputs: + # adding tag metadata from ui + for _k, _v in self.ui_inputs.items(): + if _v["target"] == "tag": + self.tag_data[_k] = _v["value"] + + # driving layer is set as positive match + if master_layer or self.vertical_sync: + # mark review layer + if self.review_track and ( + self.review_track not in self.review_track_default): + # if review layer is defined and not the same as defalut + self.review_layer = self.review_track + # shot num calculate + if self.rename_index == 0: + self.shot_num = self.count_from + else: + self.shot_num = self.count_from + self.count_steps + + # clip name sequence number + _data.update({"shot": self.shot_num}) + + # solve # in test to pythonic expression + for _k, _v in self.hierarchy_data.items(): + if "#" not in _v["value"]: + continue + self.hierarchy_data[ + _k]["value"] = self._replace_hash_to_expression( + _k, _v["value"]) + + # fill up pythonic expresisons in hierarchy data + for k, _v in self.hierarchy_data.items(): + hierarchy_formating_data[k] = _v["value"].format(**_data) + else: + # if no gui mode then just pass default data + hierarchy_formating_data = self.hierarchy_data + + tag_hierarchy_data = self._solve_tag_hierarchy_data( + hierarchy_formating_data + ) + + if master_layer and self.vertical_sync: + tag_hierarchy_data.update({"masterLayer": True}) + self.vertical_clip_match.update({ + (self.clip_in, self.clip_out): tag_hierarchy_data + }) + + if not master_layer and self.vertical_sync: + # driving layer is set as negative match + for (_in, _out), master_data in self.vertical_clip_match.items(): + master_data.update({"masterLayer": False}) + if _in == self.clip_in and _out == self.clip_out: + data_subset = master_data["subset"] + # add track index in case duplicity of names in master data + if self.subset in data_subset: + master_data["subset"] = self.subset + str( + self.track_index) + # in case track name and subset name is the same then add + if self.subset_name == self.track_name: + master_data["subset"] = self.subset + # assing data to return hierarchy data to tag + tag_hierarchy_data = master_data + + # add data to return data dict + self.tag_data.update(tag_hierarchy_data) + + if master_layer and self.review_layer: + self.tag_data.update({"review": self.review_layer}) + else: + self.tag_data.update({"review": False}) + + def _solve_tag_hierarchy_data(self, hierarchy_formating_data): + """ Solve tag data from hierarchy data and templates. """ + # fill up clip name and hierarchy keys + hierarchy_filled = self.hierarchy.format(**hierarchy_formating_data) + clip_name_filled = self.clip_name.format(**hierarchy_formating_data) + + return { + "newClipName": clip_name_filled, + "hierarchy": hierarchy_filled, + "parents": self.parents, + "hierarchyData": hierarchy_formating_data, + "subset": self.subset, + "families": [self.subset_family] + } + + def _convert_to_entity(self, key): + """ Converting input key to key with type. """ + # convert to entity type + entity_type = self.types.get(key, None) + + assert entity_type, "Missing entity type for `{}`".format( + key + ) + + return { + "entity_type": entity_type, + "entity_name": self.hierarchy_data[key]["value"].format( + **self.track_item_default_data + ) + } + + def _create_parents(self): + """ Create parents and return it in list. """ + self.parents = list() + + patern = re.compile(self.parents_search_patern) + par_split = [patern.findall(t).pop() + for t in self.hierarchy.split("/")] + + for key in par_split: + parent = self._convert_to_entity(key) + self.parents.append(parent) diff --git a/pype/plugins/resolve/create/create_shot_clip_new.py b/pype/plugins/resolve/create/create_shot_clip_new.py index 03a3041089..5d6c0a2e79 100644 --- a/pype/plugins/resolve/create/create_shot_clip_new.py +++ b/pype/plugins/resolve/create/create_shot_clip_new.py @@ -232,11 +232,11 @@ class CreateShotClipNew(resolve.Creator): # sort selected trackItems by sorted_selected_track_items = list() unsorted_selected_track_items = list() - for _ti in self.selected: - if _ti.parent().name() in v_sync_track: - sorted_selected_track_items.append(_ti) + for track_item_data in self.selected: + if track_item_data["track"]["name"] in v_sync_track: + sorted_selected_track_items.append(track_item_data) else: - unsorted_selected_track_items.append(_ti) + unsorted_selected_track_items.append(track_item_data) sorted_selected_track_items.extend(unsorted_selected_track_items) @@ -245,8 +245,27 @@ class CreateShotClipNew(resolve.Creator): "avalon": self.data } - for i, track_item in enumerate(sorted_selected_track_items): + # sequence attrs + sq_frame_start = self.sequence.GetStartFrame() + sq_markers = self.sequence.GetMarkers() + + # create media bin for compound clips (trackItems) + mp_folder = resolve.create_current_sequence_media_bin(self.sequence) + + for i, track_item_data in enumerate(sorted_selected_track_items): self.rename_index = i # convert track item to timeline media pool item - phiero.PublishClip(self, track_item, **kwargs).convert() + resolve.PublishClip(self, track_item_data, **kwargs).convert() + + # clear color after it is done + track_item_data["clip"]["item"].ClearClipColor() + + # convert track item to timeline media pool item + resolve.create_compound_clip( + track_item_data, + mp_folder, + rename=True, + **dict( + {"presets": widget.result}) + )