Merge pull request #2495 from pypeclub/feature/OP-1536_Flame-Create-publishable-clips

Flame - create publishable clips
This commit is contained in:
Jakub Ježek 2022-01-12 11:44:55 +01:00 committed by GitHub
commit f5c0c64533
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 1706 additions and 302 deletions

View file

@ -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"
]

View file

@ -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"
]

View file

@ -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)
}

View file

@ -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, [
"<colour space>", "<width>", "<height>", "<depth>", "<segment>",
"<track>", "<track name>"
])
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

View file

@ -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()

View file

@ -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

View file

@ -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

View file

@ -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),

View file

@ -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():

View file

@ -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):

View file

@ -8,7 +8,7 @@ from openpype.api import Logger
# )
log = Logger().get_logger(__name__)
log = Logger.get_logger(__name__)
exported_projet_ext = ".otoc"

View file

@ -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)

View file

@ -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

View file

@ -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'
})

View file

@ -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
}
}
}

View file

@ -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": {

View file

@ -110,6 +110,10 @@
"type": "schema",
"name": "schema_project_celaction"
},
{
"type": "schema",
"name": "schema_project_flame"
},
{
"type": "schema",
"name": "schema_project_resolve"

View file

@ -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)"
}
]
}
]
}
]
}
]
}