diff --git a/openpype/hosts/flame/__init__.py b/openpype/hosts/flame/__init__.py index da28170679..02befa76e2 100644 --- a/openpype/hosts/flame/__init__.py +++ b/openpype/hosts/flame/__init__.py @@ -1,107 +1,5 @@ -from .api.utils import ( - setup -) - -from .api.pipeline import ( - install, - uninstall, - ls, - containerise, - update_container, - maintained_selection, - remove_instance, - list_instances, - imprint -) - -from .api.lib import ( - FlameAppFramework, - maintain_current_timeline, - get_project_manager, - get_current_project, - get_current_sequence, - create_bin, -) - -from .api.menu import ( - FlameMenuProjectConnect, - FlameMenuTimeline -) - -from .api.workio import ( - open_file, - save_file, - current_file, - has_unsaved_changes, - file_extensions, - work_root -) - import os HOST_DIR = os.path.dirname( os.path.abspath(__file__) ) -API_DIR = os.path.join(HOST_DIR, "api") -PLUGINS_DIR = os.path.join(HOST_DIR, "plugins") -PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish") -LOAD_PATH = os.path.join(PLUGINS_DIR, "load") -CREATE_PATH = os.path.join(PLUGINS_DIR, "create") -INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") - -app_framework = None -apps = [] -selection = None - - -__all__ = [ - "HOST_DIR", - "API_DIR", - "PLUGINS_DIR", - "PUBLISH_PATH", - "LOAD_PATH", - "CREATE_PATH", - "INVENTORY_PATH", - "INVENTORY_PATH", - - "app_framework", - "apps", - "selection", - - # pipeline - "install", - "uninstall", - "ls", - "containerise", - "update_container", - "reload_pipeline", - "maintained_selection", - "remove_instance", - "list_instances", - "imprint", - - # utils - "setup", - - # lib - "FlameAppFramework", - "maintain_current_timeline", - "get_project_manager", - "get_current_project", - "get_current_sequence", - "create_bin", - - # menu - "FlameMenuProjectConnect", - "FlameMenuTimeline", - - # plugin - - # workio - "open_file", - "save_file", - "current_file", - "has_unsaved_changes", - "file_extensions", - "work_root" -] diff --git a/openpype/hosts/flame/api/__init__.py b/openpype/hosts/flame/api/__init__.py index 50a6b3f098..dc47488dc1 100644 --- a/openpype/hosts/flame/api/__init__.py +++ b/openpype/hosts/flame/api/__init__.py @@ -1,3 +1,115 @@ """ OpenPype Autodesk Flame api """ +from .constants import ( + COLOR_MAP, + MARKER_NAME, + MARKER_COLOR, + MARKER_DURATION, + MARKER_PUBLISH_DEFAULT +) +from .lib import ( + CTX, + FlameAppFramework, + get_project_manager, + get_current_project, + get_current_sequence, + create_bin, + create_segment_data_marker, + get_segment_data_marker, + set_segment_data_marker, + set_publish_attribute, + get_publish_attribute, + get_sequence_segments, + maintained_segment_selection, + reset_segment_selection, + get_segment_attributes +) +from .utils import ( + setup +) +from .pipeline import ( + install, + uninstall, + ls, + containerise, + update_container, + remove_instance, + list_instances, + imprint, + maintained_selection +) +from .menu import ( + FlameMenuProjectConnect, + FlameMenuTimeline +) +from .plugin import ( + Creator, + PublishableClip +) +from .workio import ( + open_file, + save_file, + current_file, + has_unsaved_changes, + file_extensions, + work_root +) + +__all__ = [ + # constants + "COLOR_MAP", + "MARKER_NAME", + "MARKER_COLOR", + "MARKER_DURATION", + "MARKER_PUBLISH_DEFAULT", + + # lib + "CTX", + "FlameAppFramework", + "get_project_manager", + "get_current_project", + "get_current_sequence", + "create_bin", + "create_segment_data_marker", + "get_segment_data_marker", + "set_segment_data_marker", + "set_publish_attribute", + "get_publish_attribute", + "get_sequence_segments", + "maintained_segment_selection", + "reset_segment_selection", + "get_segment_attributes", + + # pipeline + "install", + "uninstall", + "ls", + "containerise", + "update_container", + "reload_pipeline", + "maintained_selection", + "remove_instance", + "list_instances", + "imprint", + "maintained_selection", + + # utils + "setup", + + # menu + "FlameMenuProjectConnect", + "FlameMenuTimeline", + + # plugin + "Creator", + "PublishableClip", + + # workio + "open_file", + "save_file", + "current_file", + "has_unsaved_changes", + "file_extensions", + "work_root" +] diff --git a/openpype/hosts/flame/api/constants.py b/openpype/hosts/flame/api/constants.py new file mode 100644 index 0000000000..1833031e13 --- /dev/null +++ b/openpype/hosts/flame/api/constants.py @@ -0,0 +1,24 @@ + +""" +OpenPype Flame api constances +""" +# OpenPype marker workflow variables +MARKER_NAME = "OpenPypeData" +MARKER_DURATION = 0 +MARKER_COLOR = "cyan" +MARKER_PUBLISH_DEFAULT = False + +# OpenPype color definitions +COLOR_MAP = { + "red": (1.0, 0.0, 0.0), + "orange": (1.0, 0.5, 0.0), + "yellow": (1.0, 1.0, 0.0), + "pink": (1.0, 0.5, 1.0), + "white": (1.0, 1.0, 1.0), + "green": (0.0, 1.0, 0.0), + "cyan": (0.0, 1.0, 1.0), + "blue": (0.0, 0.0, 1.0), + "purple": (0.5, 0.0, 0.5), + "magenta": (0.5, 0.0, 1.0), + "black": (0.0, 0.0, 0.0) +} diff --git a/openpype/hosts/flame/api/lib.py b/openpype/hosts/flame/api/lib.py index 96bffab774..7788a6b3f4 100644 --- a/openpype/hosts/flame/api/lib.py +++ b/openpype/hosts/flame/api/lib.py @@ -1,12 +1,27 @@ import sys import os +import re +import json import pickle import contextlib from pprint import pformat - +from .constants import ( + MARKER_COLOR, + MARKER_DURATION, + MARKER_NAME, + COLOR_MAP, + MARKER_PUBLISH_DEFAULT +) from openpype.api import Logger -log = Logger().get_logger(__name__) +log = Logger.get_logger(__name__) + + +class CTX: + # singleton used for passing data between api modules + app_framework = None + flame_apps = [] + selection = None @contextlib.contextmanager @@ -115,10 +130,13 @@ class FlameAppFramework(object): ) self.log.info("[{}] waking up".format(self.__class__.__name__)) - self.load_prefs() + + try: + self.load_prefs() + except RuntimeError: + self.save_prefs() # menu auto-refresh defaults - if not self.prefs_global.get("menu_auto_refresh"): self.prefs_global["menu_auto_refresh"] = { "media_panel": True, @@ -207,40 +225,6 @@ class FlameAppFramework(object): return True -@contextlib.contextmanager -def maintain_current_timeline(to_timeline, from_timeline=None): - """Maintain current timeline selection during context - - Attributes: - from_timeline (resolve.Timeline)[optional]: - Example: - >>> print(from_timeline.GetName()) - timeline1 - >>> print(to_timeline.GetName()) - timeline2 - - >>> with maintain_current_timeline(to_timeline): - ... print(get_current_sequence().GetName()) - timeline2 - - >>> print(get_current_sequence().GetName()) - timeline1 - """ - # todo: this is still Resolve's implementation - project = get_current_project() - working_timeline = from_timeline or project.GetCurrentTimeline() - - # swith to the input timeline - project.SetCurrentTimeline(to_timeline) - - try: - # do a work - yield - finally: - # put the original working timeline to context - project.SetCurrentTimeline(working_timeline) - - def get_project_manager(): # TODO: get_project_manager return @@ -252,8 +236,8 @@ def get_media_storage(): def get_current_project(): - # TODO: get_current_project - return + import flame + return flame.project.current_project def get_current_sequence(selection): @@ -334,3 +318,244 @@ def get_metadata(project_name, _log=None): policy_wiretap = GetProjectColorPolicy(_log=_log) return policy_wiretap.process(project_name) + + +def get_segment_data_marker(segment, with_marker=None): + """ + Get openpype track item tag created by creator or loader plugin. + + Attributes: + segment (flame.PySegment): flame api object + with_marker (bool)[optional]: if true it will return also marker object + + Returns: + dict: openpype tag data + + Returns(with_marker=True): + flame.PyMarker, dict + """ + for marker in segment.markers: + comment = marker.comment.get_value() + color = marker.colour.get_value() + name = marker.name.get_value() + + if (name == MARKER_NAME) and ( + color == COLOR_MAP[MARKER_COLOR]): + if not with_marker: + return json.loads(comment) + else: + return marker, json.loads(comment) + + +def set_segment_data_marker(segment, data=None): + """ + Set openpype track item tag to input segment. + + Attributes: + segment (flame.PySegment): flame api object + + Returns: + dict: json loaded data + """ + data = data or dict() + + marker_data = get_segment_data_marker(segment, True) + + if marker_data: + # get available openpype tag if any + marker, tag_data = marker_data + # update tag data with new data + tag_data.update(data) + # update marker with tag data + marker.comment = json.dumps(tag_data) + else: + # update tag data with new data + marker = create_segment_data_marker(segment) + # add tag data to marker's comment + marker.comment = json.dumps(data) + + +def set_publish_attribute(segment, value): + """ Set Publish attribute in input Tag object + + Attribute: + segment (flame.PySegment)): flame api object + value (bool): True or False + """ + tag_data = get_segment_data_marker(segment) + tag_data["publish"] = value + + # set data to the publish attribute + set_segment_data_marker(segment, tag_data) + + +def get_publish_attribute(segment): + """ Get Publish attribute from input Tag object + + Attribute: + segment (flame.PySegment)): flame api object + + Returns: + bool: True or False + """ + tag_data = get_segment_data_marker(segment) + + if not tag_data: + set_publish_attribute(segment, MARKER_PUBLISH_DEFAULT) + return MARKER_PUBLISH_DEFAULT + + return tag_data["publish"] + + +def create_segment_data_marker(segment): + """ Create openpype marker on a segment. + + Attributes: + segment (flame.PySegment): flame api object + + Returns: + flame.PyMarker: flame api object + """ + # get duration of segment + duration = segment.record_duration.relative_frame + # calculate start frame of the new marker + start_frame = int(segment.record_in.relative_frame) + int(duration / 2) + # create marker + marker = segment.create_marker(start_frame) + # set marker name + marker.name = MARKER_NAME + # set duration + marker.duration = MARKER_DURATION + # set colour + marker.colour = COLOR_MAP[MARKER_COLOR] # Red + + return marker + + +def get_sequence_segments(sequence, selected=False): + segments = [] + # loop versions in sequence + for ver in sequence.versions: + # loop track in versions + for track in ver.tracks: + # ignore all empty tracks and hidden too + if len(track.segments) == 0 and track.hidden: + continue + # loop all segment in remaining tracks + for segment in track.segments: + if segment.name.get_value() == "": + continue + if ( + selected is True + and segment.selected.get_value() is not True + ): + continue + # add it to original selection + segments.append(segment) + return segments + + +@contextlib.contextmanager +def maintained_segment_selection(sequence): + """Maintain selection during context + + Attributes: + sequence (flame.PySequence): python api object + + Yield: + list of flame.PySegment + + Example: + >>> with maintained_segment_selection(sequence) as selected_segments: + ... for segment in selected_segments: + ... segment.selected = False + >>> print(segment.selected) + True + """ + selected_segments = get_sequence_segments(sequence, True) + try: + # do the operation on selected segments + yield selected_segments + finally: + # reset all selected clips + reset_segment_selection(sequence) + # select only original selection of segments + for segment in selected_segments: + segment.selected = True + + +def reset_segment_selection(sequence): + """Deselect all selected nodes + """ + for ver in sequence.versions: + for track in ver.tracks: + if len(track.segments) == 0 and track.hidden: + continue + for segment in track.segments: + segment.selected = False + + +def _get_shot_tokens_values(clip, tokens): + old_value = None + output = {} + + if not clip.shot_name: + return output + + old_value = clip.shot_name.get_value() + + for token in tokens: + clip.shot_name.set_value(token) + _key = str(re.sub("[<>]", "", token)).replace(" ", "_") + + try: + output[_key] = int(clip.shot_name.get_value()) + except ValueError: + output[_key] = clip.shot_name.get_value() + + clip.shot_name.set_value(old_value) + + return output + + +def get_segment_attributes(segment): + if str(segment.name)[1:-1] == "": + return None + + # Add timeline segment to tree + clip_data = { + "segment_name": segment.name.get_value(), + "segment_comment": segment.comment.get_value(), + "tape_name": segment.tape_name, + "source_name": segment.source_name, + "fpath": segment.file_path, + "PySegment": segment + } + + # add all available shot tokens + shot_tokens = _get_shot_tokens_values(segment, [ + "", "", "", "", "", + "", "" + ]) + clip_data.update(shot_tokens) + + # populate shot source metadata + segment_attrs = [ + "record_duration", "record_in", "record_out", + "source_duration", "source_in", "source_out" + ] + segment_attrs_data = {} + for attr_name in segment_attrs: + if not hasattr(segment, attr_name): + continue + attr = getattr(segment, attr_name) + segment_attrs_data[attr] = str(attr).replace("+", ":") + + if attr in ["record_in", "record_out"]: + clip_data[attr_name] = attr.relative_frame + else: + clip_data[attr_name] = attr.frame + + clip_data["segment_timecodes"] = segment_attrs_data + + return clip_data diff --git a/openpype/hosts/flame/api/menu.py b/openpype/hosts/flame/api/menu.py index fef6dbfa35..b7a94e7866 100644 --- a/openpype/hosts/flame/api/menu.py +++ b/openpype/hosts/flame/api/menu.py @@ -1,7 +1,7 @@ import os from Qt import QtWidgets from copy import deepcopy - +from pprint import pformat from openpype.tools.utils.host_tools import HostToolsHelper menu_group_name = 'OpenPype' @@ -26,9 +26,13 @@ default_flame_export_presets = { def callback_selection(selection, function): - import openpype.hosts.flame as opflame - opflame.selection = selection - print(opflame.selection) + import openpype.hosts.flame.api as opfapi + opfapi.CTX.selection = selection + print("Hook Selection: \n\t{}".format( + pformat({ + index: (type(item), item.name) + for index, item in enumerate(opfapi.CTX.selection)}) + )) function() @@ -109,16 +113,6 @@ class FlameMenuProjectConnect(_FlameMenuApp): "name": "Workfiles ...", "execute": lambda x: self.tools_helper.show_workfiles() }) - menu['actions'].append({ - "name": "Create ...", - "execute": lambda x: callback_selection( - x, self.tools_helper.show_creator) - }) - menu['actions'].append({ - "name": "Publish ...", - "execute": lambda x: callback_selection( - x, self.tools_helper.show_publish) - }) menu['actions'].append({ "name": "Load ...", "execute": lambda x: self.tools_helper.show_loader() diff --git a/openpype/hosts/flame/api/pipeline.py b/openpype/hosts/flame/api/pipeline.py index 26dfe7c032..30c70b491b 100644 --- a/openpype/hosts/flame/api/pipeline.py +++ b/openpype/hosts/flame/api/pipeline.py @@ -1,25 +1,33 @@ """ Basic avalon integration """ +import os import contextlib from avalon import api as avalon from pyblish import api as pyblish from openpype.api import Logger +from .lib import ( + set_segment_data_marker, + set_publish_attribute, + maintained_segment_selection, + get_current_sequence, + reset_segment_selection +) +from .. import HOST_DIR + +API_DIR = os.path.join(HOST_DIR, "api") +PLUGINS_DIR = os.path.join(HOST_DIR, "plugins") +PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish") +LOAD_PATH = os.path.join(PLUGINS_DIR, "load") +CREATE_PATH = os.path.join(PLUGINS_DIR, "create") +INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") AVALON_CONTAINERS = "AVALON_CONTAINERS" -log = Logger().get_logger(__name__) +log = Logger.get_logger(__name__) def install(): - from .. import ( - PUBLISH_PATH, - LOAD_PATH, - CREATE_PATH, - INVENTORY_PATH - ) - # TODO: install - # Disable all families except for the ones we explicitly want to see family_states = [ "imagesequence", @@ -32,33 +40,24 @@ def install(): avalon.data["familiesStateDefault"] = False avalon.data["familiesStateToggled"] = family_states - log.info("openpype.hosts.flame installed") pyblish.register_host("flame") pyblish.register_plugin_path(PUBLISH_PATH) - log.info("Registering Flame plug-ins..") - avalon.register_plugin_path(avalon.Loader, LOAD_PATH) avalon.register_plugin_path(avalon.Creator, CREATE_PATH) avalon.register_plugin_path(avalon.InventoryAction, INVENTORY_PATH) + log.info("OpenPype Flame plug-ins registred ...") # register callback for switching publishable pyblish.register_callback("instanceToggled", on_pyblish_instance_toggled) + log.info("OpenPype Flame host installed ...") def uninstall(): - from .. import ( - PUBLISH_PATH, - LOAD_PATH, - CREATE_PATH, - INVENTORY_PATH - ) - - # TODO: uninstall pyblish.deregister_host("flame") - pyblish.deregister_plugin_path(PUBLISH_PATH) - log.info("Deregistering DaVinci Resovle plug-ins..") + log.info("Deregistering Flame plug-ins..") + pyblish.deregister_plugin_path(PUBLISH_PATH) avalon.deregister_plugin_path(avalon.Loader, LOAD_PATH) avalon.deregister_plugin_path(avalon.Creator, CREATE_PATH) avalon.deregister_plugin_path(avalon.InventoryAction, INVENTORY_PATH) @@ -66,6 +65,8 @@ def uninstall(): # register callback for switching publishable pyblish.deregister_callback("instanceToggled", on_pyblish_instance_toggled) + log.info("OpenPype Flame host uninstalled ...") + def containerise(tl_segment, name, @@ -97,32 +98,6 @@ def update_container(tl_segment, data=None): # TODO: update_container pass - -@contextlib.contextmanager -def maintained_selection(): - """Maintain selection during context - - Example: - >>> with maintained_selection(): - ... node['selected'].setValue(True) - >>> print(node['selected'].value()) - False - """ - # TODO: maintained_selection + remove undo steps - - try: - # do the operation - yield - finally: - pass - - -def reset_selection(): - """Deselect all selected nodes - """ - pass - - def on_pyblish_instance_toggled(instance, old_value, new_value): """Toggle node passthrough states on instance toggles.""" @@ -150,6 +125,46 @@ def list_instances(): pass -def imprint(item, data=None): - # TODO: imprint - pass +def imprint(segment, data=None): + """ + Adding openpype data to Flame timeline segment. + + Also including publish attribute into tag. + + Arguments: + segment (flame.PySegment)): flame api object + data (dict): Any data which needst to be imprinted + + Examples: + data = { + 'asset': 'sq020sh0280', + 'family': 'render', + 'subset': 'subsetMain' + } + """ + data = data or {} + + set_segment_data_marker(segment, data) + + # add publish attribute + set_publish_attribute(segment, True) + + +@contextlib.contextmanager +def maintained_selection(): + import flame + from .lib import CTX + + # check if segment is selected + if isinstance(CTX.selection[0], flame.PySegment): + sequence = get_current_sequence(CTX.selection) + + try: + with maintained_segment_selection(sequence) as selected: + yield + finally: + # reset all selected clips + reset_segment_selection(sequence) + # select only original selection of segments + for segment in selected: + segment.selected = True diff --git a/openpype/hosts/flame/api/plugin.py b/openpype/hosts/flame/api/plugin.py index 2a28a20a75..f34999bcf3 100644 --- a/openpype/hosts/flame/api/plugin.py +++ b/openpype/hosts/flame/api/plugin.py @@ -1,3 +1,646 @@ -# Creator plugin functions +import re +from Qt import QtWidgets, QtCore +import openpype.api as openpype +from openpype import style +from . import ( + lib as flib, + pipeline as fpipeline, + constants +) + +from copy import deepcopy + +log = openpype.Logger.get_logger(__name__) + + +class CreatorWidget(QtWidgets.QDialog): + + # output items + items = dict() + _results_back = None + + def __init__(self, name, info, ui_inputs, parent=None): + super(CreatorWidget, self).__init__(parent) + + self.setObjectName(name) + + self.setWindowFlags( + QtCore.Qt.Window + | QtCore.Qt.CustomizeWindowHint + | QtCore.Qt.WindowTitleHint + | QtCore.Qt.WindowCloseButtonHint + | QtCore.Qt.WindowStaysOnTopHint + ) + self.setWindowTitle(name or "Pype Creator Input") + self.resize(500, 700) + + # Where inputs and labels are set + self.content_widget = [QtWidgets.QWidget(self)] + top_layout = QtWidgets.QFormLayout(self.content_widget[0]) + top_layout.setObjectName("ContentLayout") + top_layout.addWidget(Spacer(5, self)) + + # first add widget tag line + top_layout.addWidget(QtWidgets.QLabel(info)) + + # main dynamic layout + self.scroll_area = QtWidgets.QScrollArea(self, widgetResizable=True) + self.scroll_area.setVerticalScrollBarPolicy( + QtCore.Qt.ScrollBarAsNeeded) + self.scroll_area.setVerticalScrollBarPolicy( + QtCore.Qt.ScrollBarAlwaysOn) + self.scroll_area.setHorizontalScrollBarPolicy( + QtCore.Qt.ScrollBarAlwaysOff) + self.scroll_area.setWidgetResizable(True) + + self.content_widget.append(self.scroll_area) + + scroll_widget = QtWidgets.QWidget(self) + in_scroll_area = QtWidgets.QVBoxLayout(scroll_widget) + self.content_layout = [in_scroll_area] + + # add preset data into input widget layout + self.items = self.populate_widgets(ui_inputs) + self.scroll_area.setWidget(scroll_widget) + + # Confirmation buttons + btns_widget = QtWidgets.QWidget(self) + btns_layout = QtWidgets.QHBoxLayout(btns_widget) + + cancel_btn = QtWidgets.QPushButton("Cancel") + btns_layout.addWidget(cancel_btn) + + ok_btn = QtWidgets.QPushButton("Ok") + btns_layout.addWidget(ok_btn) + + # Main layout of the dialog + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.setContentsMargins(10, 10, 10, 10) + main_layout.setSpacing(0) + + # adding content widget + for w in self.content_widget: + main_layout.addWidget(w) + + main_layout.addWidget(btns_widget) + + ok_btn.clicked.connect(self._on_ok_clicked) + cancel_btn.clicked.connect(self._on_cancel_clicked) + + self.setStyleSheet(style.load_stylesheet()) + + @classmethod + def set_results_back(cls, value): + cls._results_back = value + + @classmethod + def get_results_back(cls): + return cls._results_back + + def _on_ok_clicked(self): + log.debug("ok is clicked: {}".format(self.items)) + results_back = self._values(self.items) + self.set_results_back(results_back) + self.close() + + def _on_cancel_clicked(self): + self.set_results_back(None) + self.close() + + def showEvent(self, event): + self.set_results_back(None) + super(CreatorWidget, self).showEvent(event) + + def _values(self, data, new_data=None): + new_data = new_data or dict() + for k, v in data.items(): + new_data[k] = { + "target": None, + "value": None + } + if v["type"] == "dict": + new_data[k]["target"] = v["target"] + new_data[k]["value"] = self._values(v["value"]) + if v["type"] == "section": + new_data.pop(k) + new_data = self._values(v["value"], new_data) + elif getattr(v["value"], "currentText", None): + new_data[k]["target"] = v["target"] + new_data[k]["value"] = v["value"].currentText() + elif getattr(v["value"], "isChecked", None): + new_data[k]["target"] = v["target"] + new_data[k]["value"] = v["value"].isChecked() + elif getattr(v["value"], "value", None): + new_data[k]["target"] = v["target"] + new_data[k]["value"] = v["value"].value() + elif getattr(v["value"], "text", None): + new_data[k]["target"] = v["target"] + new_data[k]["value"] = v["value"].text() + + return new_data + + def camel_case_split(self, text): + matches = re.finditer( + '.+?(?:(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|$)', text) + return " ".join([str(m.group(0)).capitalize() for m in matches]) + + def create_row(self, layout, type_name, text, **kwargs): + # get type attribute from qwidgets + attr = getattr(QtWidgets, type_name) + + # convert label text to normal capitalized text with spaces + label_text = self.camel_case_split(text) + + # assign the new text to lable widget + label = QtWidgets.QLabel(label_text) + label.setObjectName("LineLabel") + + # create attribute name text strip of spaces + attr_name = text.replace(" ", "") + + # create attribute and assign default values + setattr( + self, + attr_name, + attr(parent=self)) + + # assign the created attribute to variable + item = getattr(self, attr_name) + for func, val in kwargs.items(): + if getattr(item, func): + func_attr = getattr(item, func) + func_attr(val) + + # add to layout + layout.addRow(label, item) + + return item + + def populate_widgets(self, data, content_layout=None): + """ + Populate widget from input dict. + + Each plugin has its own set of widget rows defined in dictionary + each row values should have following keys: `type`, `target`, + `label`, `order`, `value` and optionally also `toolTip`. + + Args: + data (dict): widget rows or organized groups defined + by types `dict` or `section` + content_layout (QtWidgets.QFormLayout)[optional]: used when nesting + + Returns: + dict: redefined data dict updated with created widgets + + """ + + content_layout = content_layout or self.content_layout[-1] + # fix order of process by defined order value + ordered_keys = list(data.keys()) + for k, v in data.items(): + try: + # try removing a key from index which should + # be filled with new + ordered_keys.pop(v["order"]) + except IndexError: + pass + # add key into correct order + ordered_keys.insert(v["order"], k) + + # process ordered + for k in ordered_keys: + v = data[k] + tool_tip = v.get("toolTip", "") + if v["type"] == "dict": + self.content_layout.append(QtWidgets.QWidget(self)) + content_layout.addWidget(self.content_layout[-1]) + self.content_layout[-1].setObjectName("sectionHeadline") + + headline = QtWidgets.QVBoxLayout(self.content_layout[-1]) + headline.addWidget(Spacer(20, self)) + headline.addWidget(QtWidgets.QLabel(v["label"])) + + # adding nested layout with label + self.content_layout.append(QtWidgets.QWidget(self)) + self.content_layout[-1].setObjectName("sectionContent") + + nested_content_layout = QtWidgets.QFormLayout( + self.content_layout[-1]) + nested_content_layout.setObjectName("NestedContentLayout") + content_layout.addWidget(self.content_layout[-1]) + + # add nested key as label + data[k]["value"] = self.populate_widgets( + v["value"], nested_content_layout) + + if v["type"] == "section": + self.content_layout.append(QtWidgets.QWidget(self)) + content_layout.addWidget(self.content_layout[-1]) + self.content_layout[-1].setObjectName("sectionHeadline") + + headline = QtWidgets.QVBoxLayout(self.content_layout[-1]) + headline.addWidget(Spacer(20, self)) + headline.addWidget(QtWidgets.QLabel(v["label"])) + + # adding nested layout with label + self.content_layout.append(QtWidgets.QWidget(self)) + self.content_layout[-1].setObjectName("sectionContent") + + nested_content_layout = QtWidgets.QFormLayout( + self.content_layout[-1]) + nested_content_layout.setObjectName("NestedContentLayout") + content_layout.addWidget(self.content_layout[-1]) + + # add nested key as label + data[k]["value"] = self.populate_widgets( + v["value"], nested_content_layout) + + elif v["type"] == "QLineEdit": + data[k]["value"] = self.create_row( + content_layout, "QLineEdit", v["label"], + setText=v["value"], setToolTip=tool_tip) + elif v["type"] == "QComboBox": + data[k]["value"] = self.create_row( + content_layout, "QComboBox", v["label"], + addItems=v["value"], setToolTip=tool_tip) + elif v["type"] == "QCheckBox": + data[k]["value"] = self.create_row( + content_layout, "QCheckBox", v["label"], + setChecked=v["value"], setToolTip=tool_tip) + elif v["type"] == "QSpinBox": + data[k]["value"] = self.create_row( + content_layout, "QSpinBox", v["label"], + setValue=v["value"], setMinimum=0, + setMaximum=100000, setToolTip=tool_tip) + return data + + +class Spacer(QtWidgets.QWidget): + def __init__(self, height, *args, **kwargs): + super(self.__class__, self).__init__(*args, **kwargs) + + self.setFixedHeight(height) + + real_spacer = QtWidgets.QWidget(self) + real_spacer.setObjectName("Spacer") + real_spacer.setFixedHeight(height) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(real_spacer) + + self.setLayout(layout) + + +class Creator(openpype.Creator): + """Creator class wrapper + """ + clip_color = constants.COLOR_MAP["purple"] + rename_index = None + + def __init__(self, *args, **kwargs): + super(Creator, self).__init__(*args, **kwargs) + self.presets = openpype.get_current_project_settings()[ + "flame"]["create"].get(self.__class__.__name__, {}) + + # adding basic current context flame objects + self.project = flib.get_current_project() + self.sequence = flib.get_current_sequence(flib.CTX.selection) + + if (self.options or {}).get("useSelection"): + self.selected = flib.get_sequence_segments(self.sequence, True) + else: + self.selected = flib.get_sequence_segments(self.sequence) + + def create_widget(self, *args, **kwargs): + widget = CreatorWidget(*args, **kwargs) + widget.exec_() + return widget.get_results_back() + + +class PublishableClip: + """ + Convert a segment to publishable instance + + Args: + segment (flame.PySegment): flame api object + kwargs (optional): additional data needed for rename=True (presets) + + Returns: + flame.PySegment: flame api object + """ + vertical_clip_match = {} + marker_data = {} + 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 = "[ track name ]" + review_track_default = "[ none ]" + subset_family_default = "plate" + count_from_default = 10 + count_steps_default = 10 + vertical_sync_default = False + driving_layer_default = "" + index_from_segment_default = False + + def __init__(self, segment, **kwargs): + self.rename_index = kwargs["rename_index"] + self.family = kwargs["family"] + self.log = kwargs["log"] + + # get main parent objects + self.current_segment = segment + sequence_name = flib.get_current_sequence([segment]).name.get_value() + self.sequence_name = str(sequence_name).replace(" ", "_") + + self.clip_data = flib.get_segment_attributes(segment) + # segment (clip) main attributes + self.cs_name = self.clip_data["segment_name"] + self.cs_index = int(self.clip_data["segment"]) + + # get track name and index + self.track_index = int(self.clip_data["track"]) + track_name = self.clip_data["track_name"] + self.track_name = str(track_name).replace(" ", "_").replace( + "*", "noname{}".format(self.track_index)) + + # adding tag.family into tag + if kwargs.get("avalon"): + self.marker_data.update(kwargs["avalon"]) + + # add publish attribute to marker data + self.marker_data.update({"publish": True}) + + # adding ui inputs if any + self.ui_inputs = kwargs.get("ui_inputs", {}) + + self.log.info("Inside of plugin: {}".format( + self.marker_data + )) + # populate default data before we get other attributes + self._populate_segment_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 segment data and add them to marker data + self._convert_to_marker_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.marker_data.pop("newClipName") + + if self.rename: + # rename segment + self.current_segment.name = str(new_name) + self.marker_data["asset"] = str(new_name) + else: + self.marker_data["asset"] = self.cs_name + self.marker_data["hierarchyData"]["shot"] = self.cs_name + + if self.marker_data["heroTrack"] and self.review_layer: + self.marker_data.update({"reviewTrack": self.review_layer}) + else: + self.marker_data.update({"reviewTrack": None}) + + # create pype tag on track_item and add data + fpipeline.imprint(self.current_segment, self.marker_data) + + return self.current_segment + + def _populate_segment_default_data(self): + """ Populate default formating data from segment. """ + + self.current_segment_default_data = { + "_folder_": "shots", + "_sequence_": self.sequence_name, + "_track_": self.track_name, + "_clip_": self.cs_name, + "_trackIndex_": self.track_index, + "_clipIndex_": self.cs_index + } + + def _populate_attributes(self): + """ Populate main object attributes. """ + # segment frame range and parent track name for vertical sync check + self.clip_in = int(self.clip_data["record_in"]) + self.clip_out = int(self.clip_data["record_out"]) + + # define ui inputs if non gui mode was used + self.shot_num = self.cs_index + self.log.debug( + "____ 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.current_segment_default_data.copy() + self.index_from_segment = self.ui_inputs.get( + "segmentIndex", {}).get("value") or self.index_from_segment_default + 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 + self.audio = self.ui_inputs.get( + "audio", {}).get("value") or False + + # build subset name from layer name + if self.subset_name == "[ track 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) + return text.replace(("#" * _len), _repl) + + def _convert_to_marker_data(self): + """ Convert internal data to marker data. + + Populating the marker data into internal variable self.marker_data + """ + # define vertical sync attributes + hero_track = True + self.review_layer = "" + if self.vertical_sync and self.track_name not in self.driving_layer: + # if it is not then define vertical sync as None + hero_track = False + + # increasing steps by index of rename iteration + if not self.index_from_segment: + self.count_steps *= self.rename_index + + hierarchy_formating_data = {} + hierarchy_data = deepcopy(self.hierarchy_data) + _data = self.current_segment_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.marker_data[_k] = _v["value"] + + # driving layer is set as positive match + if hero_track 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.index_from_segment: + # use clip index from timeline + self.shot_num = self.count_steps * self.cs_index + else: + 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 hierarchy_data.items(): + if "#" not in _v["value"]: + continue + hierarchy_data[ + _k]["value"] = self._replace_hash_to_expression( + _k, _v["value"]) + + # fill up pythonic expresisons in hierarchy data + for k, _v in hierarchy_data.items(): + hierarchy_formating_data[k] = _v["value"].format(**_data) + else: + # if no gui mode then just pass default data + hierarchy_formating_data = hierarchy_data + + tag_hierarchy_data = self._solve_tag_hierarchy_data( + hierarchy_formating_data + ) + + tag_hierarchy_data.update({"heroTrack": True}) + if hero_track and self.vertical_sync: + self.vertical_clip_match.update({ + (self.clip_in, self.clip_out): tag_hierarchy_data + }) + + if not hero_track and self.vertical_sync: + # driving layer is set as negative match + for (_in, _out), hero_data in self.vertical_clip_match.items(): + hero_data.update({"heroTrack": False}) + if _in == self.clip_in and _out == self.clip_out: + data_subset = hero_data["subset"] + # add track index in case duplicity of names in hero data + if self.subset in data_subset: + hero_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: + hero_data["subset"] = self.subset + # assing data to return hierarchy data to tag + tag_hierarchy_data = hero_data + + # add data to return data dict + self.marker_data.update(tag_hierarchy_data) + + def _solve_tag_hierarchy_data(self, hierarchy_formating_data): + """ Solve marker 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) + + # remove shot from hierarchy data: is not needed anymore + hierarchy_formating_data.pop("shot") + + return { + "newClipName": clip_name_filled, + "hierarchy": hierarchy_filled, + "parents": self.parents, + "hierarchyData": hierarchy_formating_data, + "subset": self.subset, + "family": self.subset_family, + "families": [self.family] + } + + def _convert_to_entity(self, type, template): + """ Converting input key to key with type. """ + # convert to entity type + entity_type = self.types.get(type, None) + + assert entity_type, "Missing entity type for `{}`".format( + type + ) + + # first collect formating data to use for formating template + formating_data = {} + for _k, _v in self.hierarchy_data.items(): + value = _v["value"].format( + **self.current_segment_default_data) + formating_data[_k] = value + + return { + "entity_type": entity_type, + "entity_name": template.format( + **formating_data + ) + } + + def _create_parents(self): + """ Create parents and return it in list. """ + self.parents = [] + + patern = re.compile(self.parents_search_patern) + + par_split = [(patern.findall(t).pop(), t) + for t in self.hierarchy.split("/")] + + for type, template in par_split: + parent = self._convert_to_entity(type, template) + self.parents.append(parent) + + # Publishing plugin functions # Loader plugin functions diff --git a/openpype/hosts/flame/api/scripts/wiretap_com.py b/openpype/hosts/flame/api/scripts/wiretap_com.py index d8dc1884cf..2cd9a46184 100644 --- a/openpype/hosts/flame/api/scripts/wiretap_com.py +++ b/openpype/hosts/flame/api/scripts/wiretap_com.py @@ -9,26 +9,14 @@ import json import xml.dom.minidom as minidom from copy import deepcopy import datetime - -try: - from libwiretapPythonClientAPI import ( - WireTapClientInit) -except ImportError: - flame_python_path = "/opt/Autodesk/flame_2021/python" - flame_exe_path = ( - "/opt/Autodesk/flame_2021/bin/flame.app" - "/Contents/MacOS/startApp") - - sys.path.append(flame_python_path) - - from libwiretapPythonClientAPI import ( - WireTapClientInit, - WireTapClientUninit, - WireTapNodeHandle, - WireTapServerHandle, - WireTapInt, - WireTapStr - ) +from libwiretapPythonClientAPI import ( # noqa + WireTapClientInit, + WireTapClientUninit, + WireTapNodeHandle, + WireTapServerHandle, + WireTapInt, + WireTapStr +) class WireTapCom(object): @@ -55,6 +43,9 @@ class WireTapCom(object): self.volume_name = volume_name or "stonefs" self.group_name = group_name or "staff" + # wiretap tools dir path + self.wiretap_tools_dir = os.getenv("OPENPYPE_WIRETAP_TOOLS") + # initialize WireTap client WireTapClientInit() @@ -84,9 +75,11 @@ class WireTapCom(object): workspace_name = kwargs.get("workspace_name") color_policy = kwargs.get("color_policy") - self._project_prep(project_name) - self._set_project_settings(project_name, project_data) - self._set_project_colorspace(project_name, color_policy) + project_exists = self._project_prep(project_name) + if not project_exists: + self._set_project_settings(project_name, project_data) + self._set_project_colorspace(project_name, color_policy) + user_name = self._user_prep(user_name) if workspace_name is None: @@ -169,18 +162,15 @@ class WireTapCom(object): # check if volumes exists if self.volume_name not in volumes: raise AttributeError( - ("Volume '{}' does not exist '{}'").format( + ("Volume '{}' does not exist in '{}'").format( self.volume_name, volumes) ) # form cmd arguments project_create_cmd = [ os.path.join( - "/opt/Autodesk/", - "wiretap", - "tools", - "2021", - "wiretap_create_node", + self.wiretap_tools_dir, + "wiretap_create_node" ), '-n', os.path.join("/volumes", self.volume_name), @@ -202,6 +192,7 @@ class WireTapCom(object): print( "A new project '{}' is created.".format(project_name)) + return project_exists def _get_all_volumes(self): """Request all available volumens from WireTap @@ -431,11 +422,8 @@ class WireTapCom(object): color_policy = color_policy or "Legacy" project_colorspace_cmd = [ os.path.join( - "/opt/Autodesk/", - "wiretap", - "tools", - "2021", - "wiretap_duplicate_node", + self.wiretap_tools_dir, + "wiretap_duplicate_node" ), "-s", "/syncolor/policies/Autodesk/{}".format(color_policy), diff --git a/openpype/hosts/flame/api/utility_scripts/openpype_in_flame.py b/openpype/hosts/flame/api/utility_scripts/openpype_in_flame.py index c5fa881f3c..72614f2b5d 100644 --- a/openpype/hosts/flame/api/utility_scripts/openpype_in_flame.py +++ b/openpype/hosts/flame/api/utility_scripts/openpype_in_flame.py @@ -5,17 +5,14 @@ from pprint import pformat import atexit import openpype import avalon -import openpype.hosts.flame as opflame - -flh = sys.modules[__name__] -flh._project = None +import openpype.hosts.flame.api as opfapi def openpype_install(): """Registering OpenPype in context """ openpype.install() - avalon.api.install(opflame) + avalon.api.install(opfapi) print("Avalon registred hosts: {}".format( avalon.api.registered_host())) @@ -48,30 +45,34 @@ sys.excepthook = exeption_handler def cleanup(): """Cleaning up Flame framework context """ - if opflame.apps: - print('`{}` cleaning up apps:\n {}\n'.format( - __file__, pformat(opflame.apps))) - while len(opflame.apps): - app = opflame.apps.pop() + if opfapi.CTX.flame_apps: + print('`{}` cleaning up flame_apps:\n {}\n'.format( + __file__, pformat(opfapi.CTX.flame_apps))) + while len(opfapi.CTX.flame_apps): + app = opfapi.CTX.flame_apps.pop() print('`{}` removing : {}'.format(__file__, app.name)) del app - opflame.apps = [] + opfapi.CTX.flame_apps = [] - if opflame.app_framework: - print('PYTHON\t: %s cleaning up' % opflame.app_framework.bundle_name) - opflame.app_framework.save_prefs() - opflame.app_framework = None + if opfapi.CTX.app_framework: + print('openpype\t: {} cleaning up'.format( + opfapi.CTX.app_framework.bundle_name) + ) + opfapi.CTX.app_framework.save_prefs() + opfapi.CTX.app_framework = None atexit.register(cleanup) def load_apps(): - """Load available apps into Flame framework + """Load available flame_apps into Flame framework """ - opflame.apps.append(opflame.FlameMenuProjectConnect(opflame.app_framework)) - opflame.apps.append(opflame.FlameMenuTimeline(opflame.app_framework)) - opflame.app_framework.log.info("Apps are loaded") + opfapi.CTX.flame_apps.append( + opfapi.FlameMenuProjectConnect(opfapi.CTX.app_framework)) + opfapi.CTX.flame_apps.append( + opfapi.FlameMenuTimeline(opfapi.CTX.app_framework)) + opfapi.CTX.app_framework.log.info("Apps are loaded") def project_changed_dict(info): @@ -89,10 +90,10 @@ def app_initialized(parent=None): Args: parent (obj, optional): Parent object. Defaults to None. """ - opflame.app_framework = opflame.FlameAppFramework() + opfapi.CTX.app_framework = opfapi.FlameAppFramework() print("{} initializing".format( - opflame.app_framework.bundle_name)) + opfapi.CTX.app_framework.bundle_name)) load_apps() @@ -103,7 +104,7 @@ Initialisation of the hook is starting from here First it needs to test if it can import the flame modul. This will happen only in case a project has been loaded. Then `app_initialized` will load main Framework which will load -all menu objects as apps. +all menu objects as flame_apps. """ try: @@ -131,15 +132,15 @@ def _build_app_menu(app_name): # first find the relative appname app = None - for _app in opflame.apps: + for _app in opfapi.CTX.flame_apps: if _app.__class__.__name__ == app_name: app = _app if app: menu.append(app.build_menu()) - if opflame.app_framework: - menu_auto_refresh = opflame.app_framework.prefs_global.get( + if opfapi.CTX.app_framework: + menu_auto_refresh = opfapi.CTX.app_framework.prefs_global.get( 'menu_auto_refresh', {}) if menu_auto_refresh.get('timeline_menu', True): try: @@ -163,8 +164,8 @@ def project_saved(project_name, save_time, is_auto_save): save_time (str): time when it was saved is_auto_save (bool): autosave is on or off """ - if opflame.app_framework: - opflame.app_framework.save_prefs() + if opfapi.CTX.app_framework: + opfapi.CTX.app_framework.save_prefs() def get_main_menu_custom_ui_actions(): diff --git a/openpype/hosts/flame/api/utils.py b/openpype/hosts/flame/api/utils.py index 201c7d2fac..b9899900f5 100644 --- a/openpype/hosts/flame/api/utils.py +++ b/openpype/hosts/flame/api/utils.py @@ -5,7 +5,7 @@ Flame utils for syncing scripts import os import shutil from openpype.api import Logger -log = Logger().get_logger(__name__) +log = Logger.get_logger(__name__) def _sync_utility_scripts(env=None): @@ -75,10 +75,19 @@ def _sync_utility_scripts(env=None): path = os.path.join(flame_shared_dir, _itm) log.info("Removing `{path}`...".format(**locals())) - if os.path.isdir(path): - shutil.rmtree(path, onerror=None) - else: - os.remove(path) + + try: + if os.path.isdir(path): + shutil.rmtree(path, onerror=None) + else: + os.remove(path) + except PermissionError as msg: + log.warning( + "Not able to remove: `{}`, Problem with: `{}`".format( + path, + msg + ) + ) # copy scripts into Resolve's utility scripts dir for dirpath, scriptlist in scripts.items(): @@ -88,13 +97,22 @@ def _sync_utility_scripts(env=None): src = os.path.join(dirpath, _script) dst = os.path.join(flame_shared_dir, _script) log.info("Copying `{src}` to `{dst}`...".format(**locals())) - if os.path.isdir(src): - shutil.copytree( - src, dst, symlinks=False, - ignore=None, ignore_dangling_symlinks=False + + try: + if os.path.isdir(src): + shutil.copytree( + src, dst, symlinks=False, + ignore=None, ignore_dangling_symlinks=False + ) + else: + shutil.copy2(src, dst) + except (PermissionError, FileExistsError) as msg: + log.warning( + "Not able to coppy to: `{}`, Problem with: `{}`".format( + dst, + msg + ) ) - else: - shutil.copy2(src, dst) def setup(env=None): diff --git a/openpype/hosts/flame/api/workio.py b/openpype/hosts/flame/api/workio.py index d2e2408798..0c96c0752a 100644 --- a/openpype/hosts/flame/api/workio.py +++ b/openpype/hosts/flame/api/workio.py @@ -8,7 +8,7 @@ from openpype.api import Logger # ) -log = Logger().get_logger(__name__) +log = Logger.get_logger(__name__) exported_projet_ext = ".otoc" diff --git a/openpype/hosts/flame/hooks/pre_flame_setup.py b/openpype/hosts/flame/hooks/pre_flame_setup.py index 159fb37410..fe8acda257 100644 --- a/openpype/hosts/flame/hooks/pre_flame_setup.py +++ b/openpype/hosts/flame/hooks/pre_flame_setup.py @@ -6,6 +6,7 @@ import socket from openpype.lib import ( PreLaunchHook, get_openpype_username) from openpype.hosts import flame as opflame +import openpype.hosts.flame.api as opfapi import openpype from pprint import pformat @@ -18,18 +19,18 @@ class FlamePrelaunch(PreLaunchHook): """ app_groups = ["flame"] - # todo: replace version number with avalon launch app version - flame_python_exe = "/opt/Autodesk/python/2021/bin/python2.7" - wtc_script_path = os.path.join( opflame.HOST_DIR, "api", "scripts", "wiretap_com.py") def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.signature = "( {} )".format(self.__class__.__name__) def execute(self): + _env = self.launch_context.env + self.flame_python_exe = _env["OPENPYPE_FLAME_PYTHON_EXEC"] + self.flame_pythonpath = _env["OPENPYPE_FLAME_PYTHONPATH"] + """Hook entry method.""" project_doc = self.data["project_doc"] user_name = get_openpype_username() @@ -55,12 +56,11 @@ class FlamePrelaunch(PreLaunchHook): "FieldDominance": "PROGRESSIVE" } - data_to_script = { # from settings - "host_name": os.getenv("FLAME_WIRETAP_HOSTNAME") or hostname, - "volume_name": os.getenv("FLAME_WIRETAP_VOLUME"), - "group_name": os.getenv("FLAME_WIRETAP_GROUP"), + "host_name": _env.get("FLAME_WIRETAP_HOSTNAME") or hostname, + "volume_name": _env.get("FLAME_WIRETAP_VOLUME"), + "group_name": _env.get("FLAME_WIRETAP_GROUP"), "color_policy": "ACES 1.1", # from project @@ -68,14 +68,28 @@ class FlamePrelaunch(PreLaunchHook): "user_name": user_name, "project_data": project_data } + + self.log.info(pformat(dict(_env))) + self.log.info(pformat(data_to_script)) + + # add to python path from settings + self._add_pythonpath() + app_arguments = self._get_launch_arguments(data_to_script) - self.log.info(pformat(dict(self.launch_context.env))) - - opflame.setup(self.launch_context.env) + opfapi.setup(self.launch_context.env) self.launch_context.launch_args.extend(app_arguments) + def _add_pythonpath(self): + pythonpath = self.launch_context.env.get("PYTHONPATH") + + # separate it explicity by `;` that is what we use in settings + new_pythonpath = self.flame_pythonpath.split(os.pathsep) + new_pythonpath += pythonpath.split(os.pathsep) + + self.launch_context.env["PYTHONPATH"] = os.pathsep.join(new_pythonpath) + def _get_launch_arguments(self, script_data): # Dump data to string dumped_script_data = json.dumps(script_data) @@ -83,7 +97,9 @@ class FlamePrelaunch(PreLaunchHook): with make_temp_file(dumped_script_data) as tmp_json_path: # Prepare subprocess arguments args = [ - self.flame_python_exe, + self.flame_python_exe.format( + **self.launch_context.env + ), self.wtc_script_path, tmp_json_path ] @@ -91,7 +107,7 @@ class FlamePrelaunch(PreLaunchHook): process_kwargs = { "logger": self.log, - "env": {} + "env": self.launch_context.env } openpype.api.run_subprocess(args, **process_kwargs) diff --git a/openpype/hosts/flame/plugins/create/create_shot_clip.py b/openpype/hosts/flame/plugins/create/create_shot_clip.py new file mode 100644 index 0000000000..f055c77a89 --- /dev/null +++ b/openpype/hosts/flame/plugins/create/create_shot_clip.py @@ -0,0 +1,275 @@ +from copy import deepcopy +import openpype.hosts.flame.api as opfapi + + +class CreateShotClip(opfapi.Creator): + """Publishable clip""" + + label = "Create Publishable Clip" + family = "clip" + icon = "film" + defaults = ["Main"] + + presets = None + + def process(self): + # Creator copy of object attributes that are modified during `process` + presets = deepcopy(self.presets) + gui_inputs = self.get_gui_inputs() + + # get key pares from presets and match it on ui inputs + for k, v in gui_inputs.items(): + if v["type"] in ("dict", "section"): + # nested dictionary (only one level allowed + # for sections and dict) + for _k, _v in v["value"].items(): + if presets.get(_k): + gui_inputs[k][ + "value"][_k]["value"] = presets[_k] + if presets.get(k): + gui_inputs[k]["value"] = presets[k] + + # open widget for plugins inputs + results_back = self.create_widget( + "Pype publish attributes creator", + "Define sequential rename and fill hierarchy data.", + gui_inputs + ) + + if len(self.selected) < 1: + return + + if not results_back: + print("Operation aborted") + return + + # get ui output for track name for vertical sync + v_sync_track = results_back["vSyncTrack"]["value"] + + # sort selected trackItems by + sorted_selected_segments = [] + unsorted_selected_segments = [] + for _segment in self.selected: + if _segment.parent.name.get_value() in v_sync_track: + sorted_selected_segments.append(_segment) + else: + unsorted_selected_segments.append(_segment) + + sorted_selected_segments.extend(unsorted_selected_segments) + + kwargs = { + "log": self.log, + "ui_inputs": results_back, + "avalon": self.data, + "family": self.data["family"] + } + + for i, segment in enumerate(sorted_selected_segments): + kwargs["rename_index"] = i + # convert track item to timeline media pool item + opfapi.PublishableClip(segment, **kwargs).convert() + + def get_gui_inputs(self): + gui_tracks = self._get_video_track_names( + opfapi.get_current_sequence(opfapi.CTX.selection) + ) + return deepcopy({ + "renameHierarchy": { + "type": "section", + "label": "Shot Hierarchy And Rename Settings", + "target": "ui", + "order": 0, + "value": { + "hierarchy": { + "value": "{folder}/{sequence}", + "type": "QLineEdit", + "label": "Shot Parent Hierarchy", + "target": "tag", + "toolTip": "Parents folder for shot root folder, Template filled with `Hierarchy Data` section", # noqa + "order": 0}, + "clipRename": { + "value": False, + "type": "QCheckBox", + "label": "Rename clips", + "target": "ui", + "toolTip": "Renaming selected clips on fly", # noqa + "order": 1}, + "clipName": { + "value": "{sequence}{shot}", + "type": "QLineEdit", + "label": "Clip Name Template", + "target": "ui", + "toolTip": "template for creating shot namespaused for renaming (use rename: on)", # noqa + "order": 2}, + "segmentIndex": { + "value": True, + "type": "QCheckBox", + "label": "Segment index", + "target": "ui", + "toolTip": "Take number from segment index", # noqa + "order": 3}, + "countFrom": { + "value": 10, + "type": "QSpinBox", + "label": "Count sequence from", + "target": "ui", + "toolTip": "Set when the sequence number stafrom", # noqa + "order": 4}, + "countSteps": { + "value": 10, + "type": "QSpinBox", + "label": "Stepping number", + "target": "ui", + "toolTip": "What number is adding every new step", # noqa + "order": 5}, + } + }, + "hierarchyData": { + "type": "dict", + "label": "Shot Template Keywords", + "target": "tag", + "order": 1, + "value": { + "folder": { + "value": "shots", + "type": "QLineEdit", + "label": "{folder}", + "target": "tag", + "toolTip": "Name of folder used for root of generated shots.\nUsable tokens:\n\t{_clip_}: name of used clip\n\t{_track_}: name of parent track layer\n\t{_sequence_}: name of parent sequence (timeline)", # noqa + "order": 0}, + "episode": { + "value": "ep01", + "type": "QLineEdit", + "label": "{episode}", + "target": "tag", + "toolTip": "Name of episode.\nUsable tokens:\n\t{_clip_}: name of used clip\n\t{_track_}: name of parent track layer\n\t{_sequence_}: name of parent sequence (timeline)", # noqa + "order": 1}, + "sequence": { + "value": "sq01", + "type": "QLineEdit", + "label": "{sequence}", + "target": "tag", + "toolTip": "Name of sequence of shots.\nUsable tokens:\n\t{_clip_}: name of used clip\n\t{_track_}: name of parent track layer\n\t{_sequence_}: name of parent sequence (timeline)", # noqa + "order": 2}, + "track": { + "value": "{_track_}", + "type": "QLineEdit", + "label": "{track}", + "target": "tag", + "toolTip": "Name of sequence of shots.\nUsable tokens:\n\t{_clip_}: name of used clip\n\t{_track_}: name of parent track layer\n\t{_sequence_}: name of parent sequence (timeline)", # noqa + "order": 3}, + "shot": { + "value": "sh###", + "type": "QLineEdit", + "label": "{shot}", + "target": "tag", + "toolTip": "Name of shot. `#` is converted to paded number. \nAlso could be used with usable tokens:\n\t{_clip_}: name of used clip\n\t{_track_}: name of parent track layer\n\t{_sequence_}: name of parent sequence (timeline)", # noqa + "order": 4} + } + }, + "verticalSync": { + "type": "section", + "label": "Vertical Synchronization Of Attributes", + "target": "ui", + "order": 2, + "value": { + "vSyncOn": { + "value": True, + "type": "QCheckBox", + "label": "Enable Vertical Sync", + "target": "ui", + "toolTip": "Switch on if you want clips above each other to share its attributes", # noqa + "order": 0}, + "vSyncTrack": { + "value": gui_tracks, # noqa + "type": "QComboBox", + "label": "Hero track", + "target": "ui", + "toolTip": "Select driving track name which should be hero for all others", # noqa + "order": 1} + } + }, + "publishSettings": { + "type": "section", + "label": "Publish Settings", + "target": "ui", + "order": 3, + "value": { + "subsetName": { + "value": ["[ track name ]", "main", "bg", "fg", "bg", + "animatic"], + "type": "QComboBox", + "label": "Subset Name", + "target": "ui", + "toolTip": "chose subset name patern, if [ track name ] is selected, name of track layer will be used", # noqa + "order": 0}, + "subsetFamily": { + "value": ["plate", "take"], + "type": "QComboBox", + "label": "Subset Family", + "target": "ui", "toolTip": "What use of this subset is for", # noqa + "order": 1}, + "reviewTrack": { + "value": ["< none >"] + gui_tracks, + "type": "QComboBox", + "label": "Use Review Track", + "target": "ui", + "toolTip": "Generate preview videos on fly, if `< none >` is defined nothing will be generated.", # noqa + "order": 2}, + "audio": { + "value": False, + "type": "QCheckBox", + "label": "Include audio", + "target": "tag", + "toolTip": "Process subsets with corresponding audio", # noqa + "order": 3}, + "sourceResolution": { + "value": False, + "type": "QCheckBox", + "label": "Source resolution", + "target": "tag", + "toolTip": "Is resloution taken from timeline or source?", # noqa + "order": 4}, + } + }, + "frameRangeAttr": { + "type": "section", + "label": "Shot Attributes", + "target": "ui", + "order": 4, + "value": { + "workfileFrameStart": { + "value": 1001, + "type": "QSpinBox", + "label": "Workfiles Start Frame", + "target": "tag", + "toolTip": "Set workfile starting frame number", # noqa + "order": 0 + }, + "handleStart": { + "value": 0, + "type": "QSpinBox", + "label": "Handle Start", + "target": "tag", + "toolTip": "Handle at start of clip", # noqa + "order": 1 + }, + "handleEnd": { + "value": 0, + "type": "QSpinBox", + "label": "Handle End", + "target": "tag", + "toolTip": "Handle at end of clip", # noqa + "order": 2 + } + } + } + }) + + def _get_video_track_names(self, sequence): + track_names = [] + for ver in sequence.versions: + for track in ver.tracks: + track_names.append(track.name.get_value()) + + return track_names diff --git a/openpype/hosts/flame/plugins/publish/collect_test_selection.py b/openpype/hosts/flame/plugins/publish/collect_test_selection.py index 9a80a92414..73401368b1 100644 --- a/openpype/hosts/flame/plugins/publish/collect_test_selection.py +++ b/openpype/hosts/flame/plugins/publish/collect_test_selection.py @@ -1,9 +1,10 @@ +import os import pyblish.api -import openpype.hosts.flame as opflame +import tempfile +import openpype.hosts.flame.api as opfapi from openpype.hosts.flame.otio import flame_export as otio_export -from openpype.hosts.flame.api import lib +import opentimelineio as otio from pprint import pformat -reload(lib) # noqa reload(otio_export) # noqa @@ -17,10 +18,46 @@ class CollectTestSelection(pyblish.api.ContextPlugin): hosts = ["flame"] def process(self, context): - self.log.info(opflame.selection) + self.log.info( + "Active Selection: {}".format(opfapi.CTX.selection)) - sequence = lib.get_current_sequence(opflame.selection) + sequence = opfapi.get_current_sequence(opfapi.CTX.selection) + self.test_imprint_data(sequence) + self.test_otio_export(sequence) + + def test_otio_export(self, sequence): + test_dir = os.path.normpath( + tempfile.mkdtemp(prefix="test_pyblish_tmp_") + ) + export_path = os.path.normpath( + os.path.join( + test_dir, "otio_timeline_export.otio" + ) + ) otio_timeline = otio_export.create_otio_timeline(sequence) + otio_export.write_to_file( + otio_timeline, export_path + ) + read_timeline_otio = otio.adapters.read_from_file(export_path) + + if otio_timeline != read_timeline_otio: + raise Exception("Exported timeline is different from original") self.log.info(pformat(otio_timeline)) + self.log.info("Otio exported to: {}".format(export_path)) + + def test_imprint_data(self, sequence): + with opfapi.maintained_segment_selection(sequence) as sel_segments: + for segment in sel_segments: + if str(segment.name)[1:-1] == "": + continue + + self.log.debug("Segment with OpenPypeData: {}".format( + segment.name)) + + opfapi.imprint(segment, { + 'asset': segment.name.get_value(), + 'family': 'render', + 'subset': 'subsetMain' + }) diff --git a/openpype/settings/defaults/project_settings/flame.json b/openpype/settings/defaults/project_settings/flame.json new file mode 100644 index 0000000000..b6fbdecc95 --- /dev/null +++ b/openpype/settings/defaults/project_settings/flame.json @@ -0,0 +1,20 @@ +{ + "create": { + "CreateShotClip": { + "hierarchy": "{folder}/{sequence}", + "clipRename": true, + "clipName": "{track}{sequence}{shot}", + "countFrom": 10, + "countSteps": 10, + "folder": "shots", + "episode": "ep01", + "sequence": "sq01", + "track": "{_track_}", + "shot": "sh###", + "vSyncOn": false, + "workfileFrameStart": 1001, + "handleStart": 10, + "handleEnd": 10 + } + } +} \ No newline at end of file diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index 1cbe09f576..4a8b6d82a2 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -129,7 +129,11 @@ "darwin": [], "linux": [] }, - "environment": {} + "environment": { + "OPENPYPE_FLAME_PYTHON_EXEC": "/opt/Autodesk/python/2021/bin/python2.7", + "OPENPYPE_FLAME_PYTHONPATH": "/opt/Autodesk/flame_2021/python", + "OPENPYPE_WIRETAP_TOOLS": "/opt/Autodesk/wiretap/tools/2021" + } }, "__dynamic_keys_labels__": { "2021": "2021 (Testing Only)" @@ -142,7 +146,10 @@ "icon": "{}/app_icons/nuke.png", "host_name": "nuke", "environment": { - "NUKE_PATH": ["{NUKE_PATH}", "{OPENPYPE_STUDIO_PLUGINS}/nuke"] + "NUKE_PATH": [ + "{NUKE_PATH}", + "{OPENPYPE_STUDIO_PLUGINS}/nuke" + ] }, "variants": { "13-0": { @@ -248,7 +255,10 @@ "icon": "{}/app_icons/nuke.png", "host_name": "nuke", "environment": { - "NUKE_PATH": ["{NUKE_PATH}", "{OPENPYPE_STUDIO_PLUGINS}/nuke"] + "NUKE_PATH": [ + "{NUKE_PATH}", + "{OPENPYPE_STUDIO_PLUGINS}/nuke" + ] }, "variants": { "13-0": { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_main.json b/openpype/settings/entities/schemas/projects_schema/schema_main.json index c9eca5dedd..8a2ad451ee 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_main.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_main.json @@ -110,6 +110,10 @@ "type": "schema", "name": "schema_project_celaction" }, + { + "type": "schema", + "name": "schema_project_flame" + }, { "type": "schema", "name": "schema_project_resolve" diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json b/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json new file mode 100644 index 0000000000..d713c37620 --- /dev/null +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json @@ -0,0 +1,124 @@ +{ + "type": "dict", + "collapsible": true, + "key": "flame", + "label": "Flame", + "is_file": true, + "children": [ + { + "type": "dict", + "collapsible": true, + "key": "create", + "label": "Create plugins", + "children": [ + { + "type": "dict", + "collapsible": true, + "key": "CreateShotClip", + "label": "Create Shot Clip", + "is_group": true, + "children": [ + { + "type": "collapsible-wrap", + "label": "Shot Hierarchy And Rename Settings", + "collapsible": false, + "children": [ + { + "type": "text", + "key": "hierarchy", + "label": "Shot parent hierarchy" + }, + { + "type": "boolean", + "key": "clipRename", + "label": "Rename clips" + }, + { + "type": "text", + "key": "clipName", + "label": "Clip name template" + }, + { + "type": "number", + "key": "countFrom", + "label": "Count sequence from" + }, + { + "type": "number", + "key": "countSteps", + "label": "Stepping number" + } + ] + }, + { + "type": "collapsible-wrap", + "label": "Shot Template Keywords", + "collapsible": false, + "children": [ + { + "type": "text", + "key": "folder", + "label": "{folder}" + }, + { + "type": "text", + "key": "episode", + "label": "{episode}" + }, + { + "type": "text", + "key": "sequence", + "label": "{sequence}" + }, + { + "type": "text", + "key": "track", + "label": "{track}" + }, + { + "type": "text", + "key": "shot", + "label": "{shot}" + } + ] + }, + { + "type": "collapsible-wrap", + "label": "Vertical Synchronization Of Attributes", + "collapsible": false, + "children": [ + { + "type": "boolean", + "key": "vSyncOn", + "label": "Enable Vertical Sync" + } + ] + }, + { + "type": "collapsible-wrap", + "label": "Shot Attributes", + "collapsible": false, + "children": [ + { + "type": "number", + "key": "workfileFrameStart", + "label": "Workfiles Start Frame" + }, + { + "type": "number", + "key": "handleStart", + "label": "Handle start (head)" + }, + { + "type": "number", + "key": "handleEnd", + "label": "Handle end (tail)" + } + ] + } + ] + } + ] + } + ] +}