Merge branch 'pypeclub:develop' into develop

This commit is contained in:
Gábor Marinov 2022-05-27 20:12:57 +02:00 committed by GitHub
commit e856c55c7e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
131 changed files with 1915 additions and 601 deletions

View file

@ -35,7 +35,11 @@ function Client() {
self.pack = function(num) {
var ascii='';
for (var i = 3; i >= 0; i--) {
ascii += String.fromCharCode((num >> (8 * i)) & 255);
var hex = ((num >> (8 * i)) & 255).toString(16);
if (hex.length < 2){
ascii += "0";
}
ascii += hex;
}
return ascii;
};
@ -279,19 +283,22 @@ function Client() {
};
self._send = function(message) {
var data = new QByteArray();
var outstr = new QDataStream(data, QIODevice.WriteOnly);
outstr.writeInt(0);
data.append('UTF-8');
outstr.device().seek(0);
outstr.writeInt(data.size() - 4);
var codec = QTextCodec.codecForUtfText(data);
var msg = codec.fromUnicode(message);
var l = msg.size();
var coded = new QByteArray('AH').append(self.pack(l));
coded = coded.append(msg);
self.socket.write(new QByteArray(coded));
self.logDebug('Sent.');
/** Harmony 21.1 doesn't have QDataStream anymore.
This means we aren't able to write bytes into QByteArray so we had
modify how content lenght is sent do the server.
Content lenght is sent as string of 8 char convertible into integer
(instead of 0x00000001[4 bytes] > "000000001"[8 bytes]) */
var codec_name = new QByteArray().append("UTF-8");
var codec = QTextCodec.codecForName(codec_name);
var msg = codec.fromUnicode(message);
var l = msg.size();
var header = new QByteArray().append('AH').append(self.pack(l));
var coded = msg.prepend(header);
self.socket.write(coded);
self.logDebug('Sent.');
};
self.waitForLock = function() {
@ -351,7 +358,14 @@ function start() {
app.avalonClient = new Client();
app.avalonClient.socket.connectToHost(host, port);
}
var menuBar = QApplication.activeWindow().menuBar();
var mainWindow = null;
var widgets = QApplication.topLevelWidgets();
for (var i = 0 ; i < widgets.length; i++) {
if (widgets[i] instanceof QMainWindow){
mainWindow = widgets[i];
}
}
var menuBar = mainWindow.menuBar();
var actions = menuBar.actions();
app.avalonMenu = null;

View file

@ -88,21 +88,25 @@ class Server(threading.Thread):
"""
current_time = time.time()
while True:
self.log.info("wait ttt")
# Receive the data in small chunks and retransmit it
request = None
header = self.connection.recv(6)
header = self.connection.recv(10)
if len(header) == 0:
# null data received, socket is closing.
self.log.info(f"[{self.timestamp()}] Connection closing.")
break
if header[0:2] != b"AH":
self.log.error("INVALID HEADER")
length = struct.unpack(">I", header[2:])[0]
content_length_str = header[2:].decode()
length = int(content_length_str, 16)
data = self.connection.recv(length)
while (len(data) < length):
# we didn't received everything in first try, lets wait for
# all data.
self.log.info("loop")
time.sleep(0.1)
if self.connection is None:
self.log.error(f"[{self.timestamp()}] "
@ -113,7 +117,7 @@ class Server(threading.Thread):
break
data += self.connection.recv(length - len(data))
self.log.debug("data:: {} {}".format(data, type(data)))
self.received += data.decode("utf-8")
pretty = self._pretty(self.received)
self.log.debug(

View file

@ -27,6 +27,7 @@ from .lib import (
get_track_items,
get_current_project,
get_current_sequence,
get_timeline_selection,
get_current_track,
get_track_item_pype_tag,
set_track_item_pype_tag,
@ -80,6 +81,7 @@ __all__ = [
"get_track_items",
"get_current_project",
"get_current_sequence",
"get_timeline_selection",
"get_current_track",
"get_track_item_pype_tag",
"set_track_item_pype_tag",

View file

@ -109,8 +109,9 @@ def register_hiero_events():
# hiero.core.events.registerInterest("kShutdown", shutDown)
# hiero.core.events.registerInterest("kStartup", startupCompleted)
hiero.core.events.registerInterest(
("kSelectionChanged", "kTimeline"), selection_changed_timeline)
# INFO: was disabled because it was slowing down timeline operations
# hiero.core.events.registerInterest(
# ("kSelectionChanged", "kTimeline"), selection_changed_timeline)
# workfiles
try:

View file

@ -1,6 +1,8 @@
"""
Host specific functions where host api is connected
"""
from copy import deepcopy
import os
import re
import sys
@ -89,13 +91,19 @@ def get_current_sequence(name=None, new=False):
if not sequence:
# if nothing found create new with input name
sequence = get_current_sequence(name, True)
elif not name and not new:
else:
# if name is none and new is False then return current open sequence
sequence = hiero.ui.activeSequence()
return sequence
def get_timeline_selection():
active_sequence = hiero.ui.activeSequence()
timeline_editor = hiero.ui.getTimelineEditor(active_sequence)
return list(timeline_editor.selection())
def get_current_track(sequence, name, audio=False):
"""
Get current track in context of active project.
@ -118,7 +126,7 @@ def get_current_track(sequence, name, audio=False):
# get track by name
track = None
for _track in tracks:
if _track.name() in name:
if _track.name() == name:
track = _track
if not track:
@ -126,13 +134,14 @@ def get_current_track(sequence, name, audio=False):
track = hiero.core.VideoTrack(name)
else:
track = hiero.core.AudioTrack(name)
sequence.addTrack(track)
return track
def get_track_items(
selected=False,
selection=False,
sequence_name=None,
track_item_name=None,
track_name=None,
@ -143,7 +152,7 @@ def get_track_items(
"""Get all available current timeline track items.
Attribute:
selected (bool)[optional]: return only selected items on timeline
selection (list)[optional]: list of selected track items
sequence_name (str)[optional]: return only clips from input sequence
track_item_name (str)[optional]: return only item with input name
track_name (str)[optional]: return only items from track name
@ -155,32 +164,34 @@ def get_track_items(
Return:
list or hiero.core.TrackItem: list of track items or single track item
"""
return_list = list()
track_items = list()
track_type = track_type or "video"
selection = selection or []
return_list = []
# get selected track items or all in active sequence
if selected:
if selection:
try:
selected_items = list(hiero.selection)
for item in selected_items:
if track_name and track_name in item.parent().name():
# filter only items fitting input track name
track_items.append(item)
elif not track_name:
# or add all if no track_name was defined
track_items.append(item)
for track_item in selection:
log.info("___ track_item: {}".format(track_item))
# make sure only trackitems are selected
if not isinstance(track_item, hiero.core.TrackItem):
continue
if _validate_all_atrributes(
track_item,
track_item_name,
track_name,
track_type,
check_enabled,
check_tagged
):
log.info("___ valid trackitem: {}".format(track_item))
return_list.append(track_item)
except AttributeError:
pass
# check if any collected track items are
# `core.Hiero.Python.TrackItem` instance
if track_items:
any_track_item = track_items[0]
if not isinstance(any_track_item, hiero.core.TrackItem):
selected_items = []
# collect all available active sequence track items
if not track_items:
if not return_list:
sequence = get_current_sequence(name=sequence_name)
# get all available tracks from sequence
tracks = list(sequence.audioTracks()) + list(sequence.videoTracks())
@ -191,42 +202,76 @@ def get_track_items(
if check_enabled and not track.isEnabled():
continue
# and all items in track
for item in track.items():
if check_tagged and not item.tags():
for track_item in track.items():
# make sure no subtrackitem is also track items
if not isinstance(track_item, hiero.core.TrackItem):
continue
# check if track item is enabled
if check_enabled:
if not item.isEnabled():
continue
if track_item_name:
if track_item_name in item.name():
return item
# make sure only track items with correct track names are added
if track_name and track_name in track.name():
# filter out only defined track_name items
track_items.append(item)
elif not track_name:
# or add all if no track_name is defined
track_items.append(item)
if _validate_all_atrributes(
track_item,
track_item_name,
track_name,
track_type,
check_enabled,
check_tagged
):
return_list.append(track_item)
# filter out only track items with defined track_type
for track_item in track_items:
if track_type and track_type == "video" and isinstance(
return return_list
def _validate_all_atrributes(
track_item,
track_item_name,
track_name,
track_type,
check_enabled,
check_tagged
):
def _validate_correct_name_track_item():
if track_item_name and track_item_name in track_item.name():
return True
elif not track_item_name:
return True
def _validate_tagged_track_item():
if check_tagged and track_item.tags():
return True
elif not check_tagged:
return True
def _validate_enabled_track_item():
if check_enabled and track_item.isEnabled():
return True
elif not check_enabled:
return True
def _validate_parent_track_item():
if track_name and track_name in track_item.parent().name():
# filter only items fitting input track name
return True
elif not track_name:
# or add all if no track_name was defined
return True
def _validate_type_track_item():
if track_type == "video" and isinstance(
track_item.parent(), hiero.core.VideoTrack):
# only video track items are allowed
return_list.append(track_item)
elif track_type and track_type == "audio" and isinstance(
return True
elif track_type == "audio" and isinstance(
track_item.parent(), hiero.core.AudioTrack):
# only audio track items are allowed
return_list.append(track_item)
elif not track_type:
# add all if no track_type is defined
return_list.append(track_item)
return True
# return output list but make sure all items are TrackItems
return [_i for _i in return_list
if type(_i) == hiero.core.TrackItem]
# check if track item is enabled
return all([
_validate_enabled_track_item(),
_validate_type_track_item(),
_validate_tagged_track_item(),
_validate_parent_track_item(),
_validate_correct_name_track_item()
])
def get_track_item_pype_tag(track_item):
@ -245,7 +290,7 @@ def get_track_item_pype_tag(track_item):
return None
for tag in _tags:
# return only correct tag defined by global name
if tag.name() in self.pype_tag_name:
if tag.name() == self.pype_tag_name:
return tag
@ -266,7 +311,7 @@ def set_track_item_pype_tag(track_item, data=None):
"editable": "0",
"note": "OpenPype data container",
"icon": "openpype_icon.png",
"metadata": {k: v for k, v in data.items()}
"metadata": dict(data.items())
}
# get available pype tag if any
_tag = get_track_item_pype_tag(track_item)
@ -301,9 +346,9 @@ def get_track_item_pype_data(track_item):
return None
# get tag metadata attribute
tag_data = tag.metadata()
tag_data = deepcopy(dict(tag.metadata()))
# convert tag metadata to normal keys names and values to correct types
for k, v in dict(tag_data).items():
for k, v in tag_data.items():
key = k.replace("tag.", "")
try:
@ -324,7 +369,7 @@ def get_track_item_pype_data(track_item):
log.warning(msg)
value = v
data.update({key: value})
data[key] = value
return data
@ -497,7 +542,7 @@ class PyblishSubmission(hiero.exporters.FnSubmission.Submission):
from . import publish
# Add submission to Hiero module for retrieval in plugins.
hiero.submission = self
publish()
publish(hiero.ui.mainWindow())
def add_submission():
@ -527,7 +572,7 @@ class PublishAction(QtWidgets.QAction):
# from getting picked up when not using the "Export" dialog.
if hasattr(hiero, "submission"):
del hiero.submission
publish()
publish(hiero.ui.mainWindow())
def eventHandler(self, event):
# Add the Menu to the right-click menu
@ -893,32 +938,32 @@ def apply_colorspace_clips():
def is_overlapping(ti_test, ti_original, strict=False):
covering_exp = bool(
covering_exp = (
(ti_test.timelineIn() <= ti_original.timelineIn())
and (ti_test.timelineOut() >= ti_original.timelineOut())
)
inside_exp = bool(
inside_exp = (
(ti_test.timelineIn() >= ti_original.timelineIn())
and (ti_test.timelineOut() <= ti_original.timelineOut())
)
overlaying_right_exp = bool(
overlaying_right_exp = (
(ti_test.timelineIn() < ti_original.timelineOut())
and (ti_test.timelineOut() >= ti_original.timelineOut())
)
overlaying_left_exp = bool(
overlaying_left_exp = (
(ti_test.timelineOut() > ti_original.timelineIn())
and (ti_test.timelineIn() <= ti_original.timelineIn())
)
if not strict:
if strict:
return covering_exp
else:
return any((
covering_exp,
inside_exp,
overlaying_right_exp,
overlaying_left_exp
))
else:
return covering_exp
def get_sequence_pattern_and_padding(file):
@ -936,17 +981,13 @@ def get_sequence_pattern_and_padding(file):
"""
foundall = re.findall(
r"(#+)|(%\d+d)|(?<=[^a-zA-Z0-9])(\d+)(?=\.\w+$)", file)
if foundall:
found = sorted(list(set(foundall[0])))[-1]
if "%" in found:
padding = int(re.findall(r"\d+", found)[-1])
else:
padding = len(found)
return found, padding
else:
if not foundall:
return None, None
found = sorted(list(set(foundall[0])))[-1]
padding = int(
re.findall(r"\d+", found)[-1]) if "%" in found else len(found)
return found, padding
def sync_clip_name_to_data_asset(track_items_list):
@ -982,7 +1023,7 @@ def sync_clip_name_to_data_asset(track_items_list):
print("asset was changed in clip: {}".format(ti_name))
def check_inventory_versions():
def check_inventory_versions(track_items=None):
"""
Actual version color idetifier of Loaded containers
@ -993,14 +1034,14 @@ def check_inventory_versions():
"""
from . import parse_container
track_item = track_items or get_track_items()
# presets
clip_color_last = "green"
clip_color = "red"
# get all track items from current timeline
for track_item in get_track_items():
for track_item in track_item:
container = parse_container(track_item)
if container:
# get representation from io
representation = legacy_io.find_one({
@ -1038,29 +1079,31 @@ def selection_changed_timeline(event):
timeline_editor = event.sender
selection = timeline_editor.selection()
selection = [ti for ti in selection
if isinstance(ti, hiero.core.TrackItem)]
track_items = get_track_items(
selection=selection,
track_type="video",
check_enabled=True,
check_locked=True,
check_tagged=True
)
# run checking function
sync_clip_name_to_data_asset(selection)
# also mark old versions of loaded containers
check_inventory_versions()
sync_clip_name_to_data_asset(track_items)
def before_project_save(event):
track_items = get_track_items(
selected=False,
track_type="video",
check_enabled=True,
check_locked=True,
check_tagged=True)
check_tagged=True
)
# run checking function
sync_clip_name_to_data_asset(track_items)
# also mark old versions of loaded containers
check_inventory_versions()
check_inventory_versions(track_items)
def get_main_window():

View file

@ -143,6 +143,11 @@ def parse_container(track_item, validate=True):
"""
# convert tag metadata to normal keys names
data = lib.get_track_item_pype_data(track_item)
if (
not data
or data.get("id") != "pyblish.avalon.container"
):
return
if validate and data and data.get("schema"):
schema.validate(data)

View file

@ -1,4 +1,5 @@
import os
from pprint import pformat
import re
from copy import deepcopy
@ -400,7 +401,8 @@ class ClipLoader:
# inject asset data to representation dict
self._get_asset_data()
log.debug("__init__ self.data: `{}`".format(self.data))
log.info("__init__ self.data: `{}`".format(pformat(self.data)))
log.info("__init__ options: `{}`".format(pformat(options)))
# add active components to class
if self.new_sequence:
@ -482,7 +484,9 @@ class ClipLoader:
"""
asset_name = self.context["representation"]["context"]["asset"]
self.data["assetData"] = openpype.get_asset(asset_name)["data"]
asset_doc = openpype.get_asset(asset_name)
log.debug("__ asset_doc: {}".format(pformat(asset_doc)))
self.data["assetData"] = asset_doc["data"]
def _make_track_item(self, source_bin_item, audio=False):
""" Create track item with """
@ -500,7 +504,7 @@ class ClipLoader:
track_item.setSource(clip)
track_item.setSourceIn(self.handle_start)
track_item.setTimelineIn(self.timeline_in)
track_item.setSourceOut(self.media_duration - self.handle_end)
track_item.setSourceOut((self.media_duration) - self.handle_end)
track_item.setTimelineOut(self.timeline_out)
track_item.setPlaybackSpeed(1)
self.active_track.addTrackItem(track_item)
@ -520,14 +524,18 @@ class ClipLoader:
self.handle_start = self.data["versionData"].get("handleStart")
self.handle_end = self.data["versionData"].get("handleEnd")
if self.handle_start is None:
self.handle_start = int(self.data["assetData"]["handleStart"])
self.handle_start = self.data["assetData"]["handleStart"]
if self.handle_end is None:
self.handle_end = int(self.data["assetData"]["handleEnd"])
self.handle_end = self.data["assetData"]["handleEnd"]
self.handle_start = int(self.handle_start)
self.handle_end = int(self.handle_end)
if self.sequencial_load:
last_track_item = lib.get_track_items(
sequence_name=self.active_sequence.name(),
track_name=self.active_track.name())
track_name=self.active_track.name()
)
if len(last_track_item) == 0:
last_timeline_out = 0
else:
@ -541,17 +549,12 @@ class ClipLoader:
self.timeline_in = int(self.data["assetData"]["clipIn"])
self.timeline_out = int(self.data["assetData"]["clipOut"])
log.debug("__ self.timeline_in: {}".format(self.timeline_in))
log.debug("__ self.timeline_out: {}".format(self.timeline_out))
# check if slate is included
# either in version data families or by calculating frame diff
slate_on = next(
# check iterate if slate is in families
(f for f in self.context["version"]["data"]["families"]
if "slate" in f),
# if nothing was found then use default None
# so other bool could be used
None) or bool(int(
(self.timeline_out - self.timeline_in + 1)
+ self.handle_start + self.handle_end) < self.media_duration)
slate_on = "slate" in self.context["version"]["data"]["families"]
log.debug("__ slate_on: {}".format(slate_on))
# if slate is on then remove the slate frame from beginning
if slate_on:
@ -572,7 +575,7 @@ class ClipLoader:
# there were some cases were hiero was not creating it
source_bin_item = None
for item in self.active_bin.items():
if self.data["clip_name"] in item.name():
if self.data["clip_name"] == item.name():
source_bin_item = item
if not source_bin_item:
log.warning("Problem with created Source clip: `{}`".format(
@ -599,8 +602,8 @@ class Creator(LegacyCreator):
rename_index = None
def __init__(self, *args, **kwargs):
import openpype.hosts.hiero.api as phiero
super(Creator, self).__init__(*args, **kwargs)
import openpype.hosts.hiero.api as phiero
self.presets = openpype.get_current_project_settings()[
"hiero"]["create"].get(self.__class__.__name__, {})
@ -609,7 +612,10 @@ class Creator(LegacyCreator):
self.sequence = phiero.get_current_sequence()
if (self.options or {}).get("useSelection"):
self.selected = phiero.get_track_items(selected=True)
timeline_selection = phiero.get_timeline_selection()
self.selected = phiero.get_track_items(
selection=timeline_selection
)
else:
self.selected = phiero.get_track_items()
@ -716,6 +722,10 @@ class PublishClip:
else:
self.tag_data.update({"reviewTrack": None})
log.debug("___ self.tag_data: {}".format(
pformat(self.tag_data)
))
# create pype tag on track_item and add data
lib.imprint(self.track_item, self.tag_data)

View file

@ -86,7 +86,7 @@ def update_tag(tag, data):
# due to hiero bug we have to make sure keys which are not existent in
# data are cleared of value by `None`
for _mk in mtd.keys():
for _mk in mtd.dict().keys():
if _mk.replace("tag.", "") not in data_mtd.keys():
mtd.setValue(_mk, str(None))

View file

@ -3,10 +3,6 @@ from openpype.pipeline import (
get_representation_path,
)
import openpype.hosts.hiero.api as phiero
# from openpype.hosts.hiero.api import plugin, lib
# reload(lib)
# reload(plugin)
# reload(phiero)
class LoadClip(phiero.SequenceLoader):
@ -106,7 +102,7 @@ class LoadClip(phiero.SequenceLoader):
name = container['name']
namespace = container['namespace']
track_item = phiero.get_track_items(
track_item_name=namespace)
track_item_name=namespace).pop()
version = legacy_io.find_one({
"type": "version",
"_id": representation["parent"]
@ -157,7 +153,7 @@ class LoadClip(phiero.SequenceLoader):
# load clip to timeline and get main variables
namespace = container['namespace']
track_item = phiero.get_track_items(
track_item_name=namespace)
track_item_name=namespace).pop()
track = track_item.parent()
# remove track item from track

View file

@ -19,9 +19,12 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
def process(self, context):
self.otio_timeline = context.data["otioTimeline"]
timeline_selection = phiero.get_timeline_selection()
selected_timeline_items = phiero.get_track_items(
selected=True, check_tagged=True, check_enabled=True)
selection=timeline_selection,
check_tagged=True,
check_enabled=True
)
# only return enabled track items
if not selected_timeline_items:
@ -292,10 +295,12 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
for otio_clip in self.otio_timeline.each_clip():
track_name = otio_clip.parent().name
parent_range = otio_clip.range_in_parent()
if ti_track_name not in track_name:
if ti_track_name != track_name:
continue
if otio_clip.name not in track_item.name():
if otio_clip.name != track_item.name():
continue
self.log.debug("__ parent_range: {}".format(parent_range))
self.log.debug("__ timeline_range: {}".format(timeline_range))
if openpype.lib.is_overlapping_otio_ranges(
parent_range, timeline_range, strict=True):
@ -312,7 +317,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
speed = track_item.playbackSpeed()
timeline = phiero.get_current_sequence()
frame_start = int(track_item.timelineIn())
frame_duration = int(track_item.sourceDuration() / speed)
frame_duration = int((track_item.duration() - 1) / speed)
fps = timeline.framerate().toFloat()
return hiero_export.create_otio_time_range(

View file

@ -16,7 +16,7 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin):
"""Inject the current working file into context"""
label = "Precollect Workfile"
order = pyblish.api.CollectorOrder - 0.5
order = pyblish.api.CollectorOrder - 0.491
def process(self, context):
@ -84,6 +84,7 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin):
"colorspace": self.get_colorspace(project),
"fps": fps
}
self.log.debug("__ context_data: {}".format(pformat(context_data)))
context.data.update(context_data)
self.log.info("Creating instance: {}".format(instance))

View file

@ -1093,6 +1093,11 @@ class RenderProductsRenderman(ARenderProducts):
if not enabled:
continue
# Skip display types not producing any file output.
# Is there a better way to do it?
if not display_types.get(display["driverNode"]["type"]):
continue
aov_name = name
if aov_name == "rmanDefaultDisplay":
aov_name = "beauty"

View file

@ -1,7 +1,7 @@
"""A module containing generic loader actions that will display in the Loader.
"""
import qargparse
from openpype.pipeline import load
from openpype.hosts.maya.api.lib import (
maintained_selection,
@ -98,6 +98,15 @@ class ImportMayaLoader(load.LoaderPlugin):
icon = "arrow-circle-down"
color = "#775555"
options = [
qargparse.Boolean(
"clean_import",
label="Clean import",
default=False,
help="Should all occurences of cbId be purged?"
)
]
def load(self, context, name=None, namespace=None, data=None):
import maya.cmds as cmds
@ -114,13 +123,22 @@ class ImportMayaLoader(load.LoaderPlugin):
)
with maintained_selection():
cmds.file(self.fname,
i=True,
preserveReferences=True,
namespace=namespace,
returnNewNodes=True,
groupReference=True,
groupName="{}:{}".format(namespace, name))
nodes = cmds.file(self.fname,
i=True,
preserveReferences=True,
namespace=namespace,
returnNewNodes=True,
groupReference=True,
groupName="{}:{}".format(namespace, name))
if data.get("clean_import", False):
remove_attributes = ["cbId"]
for node in nodes:
for attr in remove_attributes:
if cmds.attributeQuery(attr, node=node, exists=True):
full_attr = "{}.{}".format(node, attr)
print("Removing {}".format(full_attr))
cmds.deleteAttr(full_attr)
# We do not containerize imported content, it remains unmanaged
return

View file

@ -83,7 +83,7 @@ class ImagePlaneLoader(load.LoaderPlugin):
families = ["image", "plate", "render"]
label = "Load imagePlane"
representations = ["mov", "exr", "preview", "png"]
representations = ["mov", "exr", "preview", "png", "jpg"]
icon = "image"
color = "orange"

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
from maya import cmds # noqa
import pyblish.api
class CollectFbxCamera(pyblish.api.InstancePlugin):
"""Collect Camera for FBX export."""
order = pyblish.api.CollectorOrder + 0.2
label = "Collect Camera for FBX export"
families = ["camera"]
def process(self, instance):
if not instance.data.get("families"):
instance.data["families"] = []
if "fbx" not in instance.data["families"]:
instance.data["families"].append("fbx")
instance.data["cameras"] = True

View file

@ -22,10 +22,46 @@ RENDERER_NODE_TYPES = [
# redshift
"RedshiftMeshParameters"
]
SHAPE_ATTRS = set(SHAPE_ATTRS)
def get_pxr_multitexture_file_attrs(node):
attrs = []
for i in range(9):
if cmds.attributeQuery("filename{}".format(i), node):
file = cmds.getAttr("{}.filename{}".format(node, i))
if file:
attrs.append("filename{}".format(i))
return attrs
FILE_NODES = {
"file": "fileTextureName",
"aiImage": "filename",
"RedshiftNormalMap": "text0",
"PxrBump": "filename",
"PxrNormalMap": "filename",
"PxrMultiTexture": get_pxr_multitexture_file_attrs,
"PxrPtexture": "filename",
"PxrTexture": "filename"
}
def get_attributes(dictionary, attr):
# type: (dict, str) -> list
if callable(dictionary[attr]):
val = dictionary[attr]()
else:
val = dictionary.get(attr, [])
if not isinstance(val, list):
return [val]
return val
def get_look_attrs(node):
"""Returns attributes of a node that are important for the look.
@ -51,15 +87,14 @@ def get_look_attrs(node):
if cmds.objectType(node, isAType="shape"):
attrs = cmds.listAttr(node, changedSinceFileOpen=True) or []
for attr in attrs:
if attr in SHAPE_ATTRS:
if attr in SHAPE_ATTRS or \
attr not in SHAPE_ATTRS and attr.startswith('ai'):
result.append(attr)
elif attr.startswith('ai'):
result.append(attr)
return result
def node_uses_image_sequence(node):
def node_uses_image_sequence(node, node_path):
# type: (str) -> bool
"""Return whether file node uses an image sequence or single image.
Determine if a node uses an image sequence or just a single image,
@ -74,12 +109,15 @@ def node_uses_image_sequence(node):
"""
# useFrameExtension indicates an explicit image sequence
node_path = get_file_node_path(node).lower()
# The following tokens imply a sequence
patterns = ["<udim>", "<tile>", "<uvtile>", "u<u>_v<v>", "<frame0"]
patterns = ["<udim>", "<tile>", "<uvtile>",
"u<u>_v<v>", "<frame0", "<f4>"]
try:
use_frame_extension = cmds.getAttr('%s.useFrameExtension' % node)
except ValueError:
use_frame_extension = False
return (cmds.getAttr('%s.useFrameExtension' % node) or
return (use_frame_extension or
any(pattern in node_path for pattern in patterns))
@ -137,14 +175,15 @@ def seq_to_glob(path):
return path
def get_file_node_path(node):
def get_file_node_paths(node):
# type: (str) -> list
"""Get the file path used by a Maya file node.
Args:
node (str): Name of the Maya file node
Returns:
str: the file path in use
list: the file paths in use
"""
# if the path appears to be sequence, use computedFileTextureNamePattern,
@ -163,15 +202,19 @@ def get_file_node_path(node):
"<uvtile>"]
lower = texture_pattern.lower()
if any(pattern in lower for pattern in patterns):
return texture_pattern
return [texture_pattern]
if cmds.nodeType(node) == 'aiImage':
return cmds.getAttr('{0}.filename'.format(node))
if cmds.nodeType(node) == 'RedshiftNormalMap':
return cmds.getAttr('{}.tex0'.format(node))
try:
file_attributes = get_attributes(FILE_NODES, cmds.nodeType(node))
except AttributeError:
file_attributes = "fileTextureName"
# otherwise use fileTextureName
return cmds.getAttr('{0}.fileTextureName'.format(node))
files = []
for file_attr in file_attributes:
if cmds.attributeQuery(file_attr, node=node, exists=True):
files.append(cmds.getAttr("{}.{}".format(node, file_attr)))
return files
def get_file_node_files(node):
@ -185,16 +228,21 @@ def get_file_node_files(node):
list: List of full file paths.
"""
paths = get_file_node_paths(node)
sequences = []
replaces = []
for index, path in enumerate(paths):
if node_uses_image_sequence(node, path):
glob_pattern = seq_to_glob(path)
sequences.extend(glob.glob(glob_pattern))
replaces.append(index)
path = get_file_node_path(node)
path = cmds.workspace(expandName=path)
if node_uses_image_sequence(node):
glob_pattern = seq_to_glob(path)
return glob.glob(glob_pattern)
elif os.path.exists(path):
return [path]
else:
return []
for index in replaces:
paths.pop(index)
paths.extend(sequences)
return [p for p in paths if os.path.exists(p)]
class CollectLook(pyblish.api.InstancePlugin):
@ -238,13 +286,13 @@ class CollectLook(pyblish.api.InstancePlugin):
"for %s" % instance.data['name'])
# Discover related object sets
self.log.info("Gathering sets..")
self.log.info("Gathering sets ...")
sets = self.collect_sets(instance)
# Lookup set (optimization)
instance_lookup = set(cmds.ls(instance, long=True))
self.log.info("Gathering set relations..")
self.log.info("Gathering set relations ...")
# Ensure iteration happen in a list so we can remove keys from the
# dict within the loop
@ -326,7 +374,10 @@ class CollectLook(pyblish.api.InstancePlugin):
"volumeShader",
"displacementShader",
"aiSurfaceShader",
"aiVolumeShader"]
"aiVolumeShader",
"rman__surface",
"rman__displacement"
]
if look_sets:
materials = []
@ -374,9 +425,10 @@ class CollectLook(pyblish.api.InstancePlugin):
or []
)
files = cmds.ls(history, type="file", long=True)
files.extend(cmds.ls(history, type="aiImage", long=True))
files.extend(cmds.ls(history, type="RedshiftNormalMap", long=True))
all_supported_nodes = FILE_NODES.keys()
files = []
for node_type in all_supported_nodes:
files.extend(cmds.ls(history, type=node_type, long=True))
self.log.info("Collected file nodes:\n{}".format(files))
# Collect textures if any file nodes are found
@ -510,27 +562,24 @@ class CollectLook(pyblish.api.InstancePlugin):
Returns:
dict
"""
self.log.debug("processing: {}".format(node))
if cmds.nodeType(node) not in ["file", "aiImage", "RedshiftNormalMap"]:
all_supported_nodes = FILE_NODES.keys()
if cmds.nodeType(node) not in all_supported_nodes:
self.log.error(
"Unsupported file node: {}".format(cmds.nodeType(node)))
raise AssertionError("Unsupported file node")
if cmds.nodeType(node) == 'file':
self.log.debug(" - file node")
attribute = "{}.fileTextureName".format(node)
computed_attribute = "{}.computedFileTextureNamePattern".format(node)
elif cmds.nodeType(node) == 'aiImage':
self.log.debug("aiImage node")
attribute = "{}.filename".format(node)
computed_attribute = attribute
elif cmds.nodeType(node) == 'RedshiftNormalMap':
self.log.debug("RedshiftNormalMap node")
attribute = "{}.tex0".format(node)
computed_attribute = attribute
self.log.debug(" - got {}".format(cmds.nodeType(node)))
attribute = FILE_NODES.get(cmds.nodeType(node))
source = cmds.getAttr("{}.{}".format(
node,
attribute
))
computed_attribute = "{}.{}".format(node, attribute)
if attribute == "fileTextureName":
computed_attribute = node + ".computedFileTextureNamePattern"
source = cmds.getAttr(attribute)
self.log.info(" - file source: {}".format(source))
color_space_attr = "{}.colorSpace".format(node)
try:
@ -567,11 +616,15 @@ class CollectLook(pyblish.api.InstancePlugin):
self.log.info(" - color space: {}".format(color_space))
# Define the resource
return {"node": node,
"attribute": attribute,
"source": source, # required for resources
"files": files,
"color_space": color_space} # required for resources
return {
"node": node,
# here we are passing not only attribute, but with node again
# this should be simplified and changed extractor.
"attribute": "{}.{}".format(node, attribute),
"source": source, # required for resources
"files": files,
"color_space": color_space
} # required for resources
class CollectModelRenderSets(CollectLook):

View file

@ -339,9 +339,15 @@ class CollectMayaRender(pyblish.api.ContextPlugin):
"source": filepath,
"expectedFiles": full_exp_files,
"publishRenderMetadataFolder": common_publish_meta_path,
"resolutionWidth": cmds.getAttr("defaultResolution.width"),
"resolutionHeight": cmds.getAttr("defaultResolution.height"),
"pixelAspect": cmds.getAttr("defaultResolution.pixelAspect"),
"resolutionWidth": lib.get_attr_in_layer(
"defaultResolution.height", layer=layer_name
),
"resolutionHeight": lib.get_attr_in_layer(
"defaultResolution.width", layer=layer_name
),
"pixelAspect": lib.get_attr_in_layer(
"defaultResolution.pixelAspect", layer=layer_name
),
"tileRendering": render_instance.data.get("tileRendering") or False, # noqa: E501
"tilesX": render_instance.data.get("tilesX") or 2,
"tilesY": render_instance.data.get("tilesY") or 2,

View file

@ -124,9 +124,15 @@ class CollectVrayScene(pyblish.api.InstancePlugin):
# Add source to allow tracing back to the scene from
# which was submitted originally
"source": context.data["currentFile"].replace("\\", "/"),
"resolutionWidth": cmds.getAttr("defaultResolution.width"),
"resolutionHeight": cmds.getAttr("defaultResolution.height"),
"pixelAspect": cmds.getAttr("defaultResolution.pixelAspect"),
"resolutionWidth": lib.get_attr_in_layer(
"defaultResolution.height", layer=layer_name
),
"resolutionHeight": lib.get_attr_in_layer(
"defaultResolution.width", layer=layer_name
),
"pixelAspect": lib.get_attr_in_layer(
"defaultResolution.pixelAspect", layer=layer_name
),
"priority": instance.data.get("priority"),
"useMultipleSceneFiles": instance.data.get(
"vraySceneMultipleFiles")

View file

@ -372,10 +372,12 @@ class ExtractLook(openpype.api.Extractor):
if mode == COPY:
transfers.append((source, destination))
self.log.info('copying')
self.log.info('file will be copied {} -> {}'.format(
source, destination))
elif mode == HARDLINK:
hardlinks.append((source, destination))
self.log.info('hardlinking')
self.log.info('file will be hardlinked {} -> {}'.format(
source, destination))
# Store the hashes from hash to destination to include in the
# database

View file

@ -373,7 +373,7 @@ def add_write_node_legacy(name, **kwarg):
Returns:
node (obj): nuke write node
"""
frame_range = kwarg.get("use_range_limit", None)
use_range_limit = kwarg.get("use_range_limit", None)
w = nuke.createNode(
"Write",
@ -391,10 +391,10 @@ def add_write_node_legacy(name, **kwarg):
log.debug(e)
continue
if frame_range:
if use_range_limit:
w["use_limit"].setValue(True)
w["first"].setValue(frame_range[0])
w["last"].setValue(frame_range[1])
w["first"].setValue(kwarg["frame_range"][0])
w["last"].setValue(kwarg["frame_range"][1])
return w
@ -409,7 +409,7 @@ def add_write_node(name, file_path, knobs, **kwarg):
Returns:
node (obj): nuke write node
"""
frame_range = kwarg.get("use_range_limit", None)
use_range_limit = kwarg.get("use_range_limit", None)
w = nuke.createNode(
"Write",
@ -420,10 +420,10 @@ def add_write_node(name, file_path, knobs, **kwarg):
# finally add knob overrides
set_node_knobs_from_settings(w, knobs, **kwarg)
if frame_range:
if use_range_limit:
w["use_limit"].setValue(True)
w["first"].setValue(frame_range[0])
w["last"].setValue(frame_range[1])
w["first"].setValue(kwarg["frame_range"][0])
w["last"].setValue(kwarg["frame_range"][1])
return w

View file

@ -27,6 +27,10 @@ class CreateWritePrerender(plugin.AbstractWriteRender):
# add fpath_template
write_data["fpath_template"] = self.fpath_template
write_data["use_range_limit"] = self.use_range_limit
write_data["frame_range"] = (
nuke.root()["first_frame"].value(),
nuke.root()["last_frame"].value()
)
if not self.is_legacy():
return create_write_node(

View file

@ -1,4 +1,5 @@
import nuke
import os
from openpype.api import Logger
from openpype.pipeline import install_host
@ -9,6 +10,7 @@ from openpype.hosts.nuke.api.lib import (
WorkfileSettings,
dirmap_file_name_filter
)
from openpype.settings import get_project_settings
log = Logger.get_logger(__name__)
@ -28,3 +30,32 @@ nuke.addOnScriptLoad(WorkfileSettings().set_context_settings)
nuke.addFilenameFilter(dirmap_file_name_filter)
log.info('Automatic syncing of write file knob to script version')
def add_scripts_menu():
try:
from scriptsmenu import launchfornuke
except ImportError:
log.warning(
"Skipping studio.menu install, because "
"'scriptsmenu' module seems unavailable."
)
return
# load configuration of custom menu
project_settings = get_project_settings(os.getenv("AVALON_PROJECT"))
config = project_settings["nuke"]["scriptsmenu"]["definition"]
_menu = project_settings["nuke"]["scriptsmenu"]["name"]
if not config:
log.warning("Skipping studio menu, no definition found.")
return
# run the launcher for Maya menu
studio_menu = launchfornuke.main(title=_menu.title())
# apply configuration
studio_menu.build_from_configuration(studio_menu, config)
add_scripts_menu()

View file

@ -39,6 +39,9 @@ class CollectBatchData(pyblish.api.ContextPlugin):
def process(self, context):
self.log.info("CollectBatchData")
batch_dir = os.environ.get("OPENPYPE_PUBLISH_DATA")
if os.environ.get("IS_TEST"):
self.log.debug("Automatic testing, no batch data, skipping")
return
assert batch_dir, (
"Missing `OPENPYPE_PUBLISH_DATA`")

View file

@ -84,7 +84,7 @@ class CollectColorCodedInstances(pyblish.api.ContextPlugin):
"variant": variant,
"family": resolved_family,
"task": task_name,
"layer": layer.name
"layer": layer.clean_name
}
subset = resolved_subset_template.format(

View file

@ -5,6 +5,7 @@ import openpype.api
from openpype.lib import (
get_ffmpeg_tool_path,
get_ffprobe_streams,
path_to_subprocess_arg,
)
@ -37,82 +38,69 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin):
if not thumbnail_repre:
return
thumbnail_repre.pop("thumbnail")
files = thumbnail_repre.get("files")
if not files:
return
if isinstance(files, list):
files_len = len(files)
file = str(files[0])
first_filename = str(files[0])
else:
files_len = 1
file = files
first_filename = files
staging_dir = None
is_jpeg = False
if file.endswith(".jpeg") or file.endswith(".jpg"):
is_jpeg = True
if is_jpeg and files_len == 1:
# skip if already is single jpeg file
return
# Convert to jpeg if not yet
full_input_path = os.path.join(
thumbnail_repre["stagingDir"], first_filename
)
self.log.info("input {}".format(full_input_path))
with tempfile.NamedTemporaryFile(suffix=".jpg") as tmp:
full_thumbnail_path = tmp.name
elif is_jpeg:
# use first frame as thumbnail if is sequence of jpegs
full_thumbnail_path = os.path.join(
thumbnail_repre["stagingDir"], file
)
self.log.info(
"For thumbnail is used file: {}".format(full_thumbnail_path)
)
self.log.info("output {}".format(full_thumbnail_path))
else:
# Convert to jpeg if not yet
full_input_path = os.path.join(thumbnail_repre["stagingDir"], file)
self.log.info("input {}".format(full_input_path))
instance.context.data["cleanupFullPaths"].append(full_thumbnail_path)
full_thumbnail_path = tempfile.mkstemp(suffix=".jpg")[1]
self.log.info("output {}".format(full_thumbnail_path))
ffmpeg_path = get_ffmpeg_tool_path("ffmpeg")
ffmpeg_path = get_ffmpeg_tool_path("ffmpeg")
ffmpeg_args = self.ffmpeg_args or {}
ffmpeg_args = self.ffmpeg_args or {}
jpeg_items = [
path_to_subprocess_arg(ffmpeg_path),
# override file if already exists
"-y"
]
jpeg_items = [
"\"{}\"".format(ffmpeg_path),
# override file if already exists
"-y"
]
# add input filters from peresets
jpeg_items.extend(ffmpeg_args.get("input") or [])
# input file
jpeg_items.append("-i \"{}\"".format(full_input_path))
# add input filters from peresets
jpeg_items.extend(ffmpeg_args.get("input") or [])
# input file
jpeg_items.extend([
"-i", path_to_subprocess_arg(full_input_path),
# extract only single file
jpeg_items.append("-frames:v 1")
"-frames:v", "1",
# Add black background for transparent images
jpeg_items.append((
"-filter_complex"
" \"color=black,format=rgb24[c]"
"-filter_complex", (
"\"color=black,format=rgb24[c]"
";[c][0]scale2ref[c][i]"
";[c][i]overlay=format=auto:shortest=1,setsar=1\""
))
),
])
jpeg_items.extend(ffmpeg_args.get("output") or [])
jpeg_items.extend(ffmpeg_args.get("output") or [])
# output file
jpeg_items.append("\"{}\"".format(full_thumbnail_path))
# output file
jpeg_items.append(path_to_subprocess_arg(full_thumbnail_path))
subprocess_jpeg = " ".join(jpeg_items)
subprocess_jpeg = " ".join(jpeg_items)
# run subprocess
self.log.debug("Executing: {}".format(subprocess_jpeg))
openpype.api.run_subprocess(
subprocess_jpeg, shell=True, logger=self.log
)
# run subprocess
self.log.debug("Executing: {}".format(subprocess_jpeg))
openpype.api.run_subprocess(
subprocess_jpeg, shell=True, logger=self.log
)
# remove thumbnail key from origin repre
thumbnail_repre.pop("thumbnail")
streams = get_ffprobe_streams(full_thumbnail_path)
width = height = None
for stream in streams:
@ -121,8 +109,7 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin):
height = stream["height"]
break
filename = os.path.basename(full_thumbnail_path)
staging_dir = staging_dir or os.path.dirname(full_thumbnail_path)
staging_dir, filename = os.path.split(full_thumbnail_path)
# create new thumbnail representation
representation = {
@ -130,15 +117,11 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin):
'ext': 'jpg',
'files': filename,
"stagingDir": staging_dir,
"tags": ["thumbnail"],
"tags": ["thumbnail", "delete"],
}
if width and height:
representation["width"] = width
representation["height"] = height
# # add Delete tag when temp file was rendered
if not is_jpeg:
representation["tags"].append("delete")
self.log.info(f"New representation {representation}")
instance.data["representations"].append(representation)

View file

@ -14,6 +14,7 @@ from .pipeline import (
class TrayPublishCreator(Creator):
create_allow_context_change = True
host_name = "traypublisher"
def collect_instances(self):
for instance_data in list_instances():

View file

@ -165,12 +165,12 @@ def parse_group_data(data):
if not group_raw:
continue
parts = group_raw.split(" ")
parts = group_raw.split("|")
# Check for length and concatenate 2 last items until length match
# - this happens if name contain spaces
while len(parts) > 6:
last_item = parts.pop(-1)
parts[-1] = " ".join([parts[-1], last_item])
parts[-1] = "|".join([parts[-1], last_item])
clip_id, group_id, red, green, blue, name = parts
group = {
@ -201,11 +201,16 @@ def get_groups_data(communicator=None):
george_script_lines = (
# Variable containing full path to output file
"output_path = \"{}\"".format(output_filepath),
"loop = 1",
"FOR idx = 1 TO 12",
"empty = 0",
# Loop over 100 groups
"FOR idx = 1 TO 100",
# Receive information about groups
"tv_layercolor \"getcolor\" 0 idx",
"tv_writetextfile \"strict\" \"append\" '\"'output_path'\"' result",
"END"
"PARSE result clip_id group_index c_red c_green c_blue group_name",
# Create and add line to output file
"line = clip_id'|'group_index'|'c_red'|'c_green'|'c_blue'|'group_name",
"tv_writetextfile \"strict\" \"append\" '\"'output_path'\"' line",
"END",
)
george_script = "\n".join(george_script_lines)
execute_george_through_file(george_script, communicator)

View file

@ -1,15 +1,19 @@
import os
import openpype.hosts
from openpype.lib.applications import Application
def add_implementation_envs(env, _app):
def add_implementation_envs(env: dict, _app: Application) -> None:
"""Modify environments to contain all required for implementation."""
# Set OPENPYPE_UNREAL_PLUGIN required for Unreal implementation
ue_plugin = "UE_5.0" if _app.name[:1] == "5" else "UE_4.7"
unreal_plugin_path = os.path.join(
os.path.dirname(os.path.abspath(openpype.hosts.__file__)),
"unreal", "integration"
"unreal", "integration", ue_plugin
)
env["OPENPYPE_UNREAL_PLUGIN"] = unreal_plugin_path
if not env.get("OPENPYPE_UNREAL_PLUGIN"):
env["OPENPYPE_UNREAL_PLUGIN"] = unreal_plugin_path
# Set default environments if are not set via settings
defaults = {

View file

@ -25,7 +25,7 @@ class UnrealPrelaunchHook(PreLaunchHook):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.signature = "( {} )".format(self.__class__.__name__)
self.signature = f"( {self.__class__.__name__} )"
def _get_work_filename(self):
# Use last workfile if was found
@ -71,7 +71,7 @@ class UnrealPrelaunchHook(PreLaunchHook):
if int(engine_version.split(".")[0]) < 4 and \
int(engine_version.split(".")[1]) < 26:
raise ApplicationLaunchFailed((
f"{self.signature} Old unsupported version of UE4 "
f"{self.signature} Old unsupported version of UE "
f"detected - {engine_version}"))
except ValueError:
# there can be string in minor version and in that case
@ -99,18 +99,19 @@ class UnrealPrelaunchHook(PreLaunchHook):
f"character ({unreal_project_name}). Appending 'P'"
))
unreal_project_name = f"P{unreal_project_name}"
unreal_project_filename = f'{unreal_project_name}.uproject'
project_path = Path(os.path.join(workdir, unreal_project_name))
self.log.info((
f"{self.signature} requested UE4 version: "
f"{self.signature} requested UE version: "
f"[ {engine_version} ]"
))
detected = unreal_lib.get_engine_versions(self.launch_context.env)
detected_str = ', '.join(detected.keys()) or 'none'
self.log.info((
f"{self.signature} detected UE4 versions: "
f"{self.signature} detected UE versions: "
f"[ {detected_str} ]"
))
if not detected:
@ -123,10 +124,10 @@ class UnrealPrelaunchHook(PreLaunchHook):
f"detected [ {engine_version} ]"
))
ue4_path = unreal_lib.get_editor_executable_path(
Path(detected[engine_version]))
ue_path = unreal_lib.get_editor_executable_path(
Path(detected[engine_version]), engine_version)
self.launch_context.launch_args = [ue4_path.as_posix()]
self.launch_context.launch_args = [ue_path.as_posix()]
project_path.mkdir(parents=True, exist_ok=True)
project_file = project_path / unreal_project_filename
@ -138,6 +139,11 @@ class UnrealPrelaunchHook(PreLaunchHook):
))
# Set "OPENPYPE_UNREAL_PLUGIN" to current process environment for
# execution of `create_unreal_project`
if self.launch_context.env.get("OPENPYPE_UNREAL_PLUGIN"):
self.log.info((
f"{self.signature} using OpenPype plugin from "
f"{self.launch_context.env.get('OPENPYPE_UNREAL_PLUGIN')}"
))
env_key = "OPENPYPE_UNREAL_PLUGIN"
if self.launch_context.env.get(env_key):
os.environ[env_key] = self.launch_context.env[env_key]

View file

@ -1,4 +1,4 @@
# OpenPype Unreal Integration plugin
# OpenPype Unreal Integration plugin - UE 4.x
This is plugin for Unreal Editor, creating menu for [OpenPype](https://github.com/getavalon) tools to run.

View file

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 84 KiB

Before After
Before After

View file

@ -0,0 +1,35 @@
# Prerequisites
*.d
# Compiled Object files
*.slo
*.lo
*.o
*.obj
# Precompiled Headers
*.gch
*.pch
# Compiled Dynamic libraries
*.so
*.dylib
*.dll
# Fortran module files
*.mod
*.smod
# Compiled Static libraries
*.lai
*.la
*.a
*.lib
# Executables
*.exe
*.out
*.app
/Binaries
/Intermediate

View file

@ -0,0 +1,28 @@
import unreal
openpype_detected = True
try:
from openpype.pipeline import install_host
from openpype.hosts.unreal import api as openpype_host
except ImportError as exc:
openpype_host = None
openpype_detected = False
unreal.log_error("OpenPype: cannot load OpenPype [ {} ]".format(exc))
if openpype_detected:
install_host(openpype_host)
@unreal.uclass()
class OpenPypeIntegration(unreal.OpenPypePythonBridge):
@unreal.ufunction(override=True)
def RunInPython_Popup(self):
unreal.log_warning("OpenPype: showing tools popup")
if openpype_detected:
openpype_host.show_tools_popup()
@unreal.ufunction(override=True)
def RunInPython_Dialog(self):
unreal.log_warning("OpenPype: showing tools dialog")
if openpype_detected:
openpype_host.show_tools_dialog()

View file

@ -0,0 +1,24 @@
{
"FileVersion": 3,
"Version": 1,
"VersionName": "1.0",
"FriendlyName": "OpenPype",
"Description": "OpenPype Integration",
"Category": "OpenPype.Integration",
"CreatedBy": "Ondrej Samohel",
"CreatedByURL": "https://openpype.io",
"DocsURL": "https://openpype.io/docs/artist_hosts_unreal",
"MarketplaceURL": "",
"SupportURL": "https://pype.club/",
"CanContainContent": true,
"IsBetaVersion": true,
"IsExperimentalVersion": false,
"Installed": false,
"Modules": [
{
"Name": "OpenPype",
"Type": "Editor",
"LoadingPhase": "Default"
}
]
}

View file

@ -0,0 +1,11 @@
# OpenPype Unreal Integration plugin - UE 5.x
This is plugin for Unreal Editor, creating menu for [OpenPype](https://github.com/getavalon) tools to run.
## How does this work
Plugin is creating basic menu items in **Window/OpenPype** section of Unreal Editor main menu and a button
on the main toolbar with associated menu. Clicking on those menu items is calling callbacks that are
declared in C++ but needs to be implemented during Unreal Editor
startup in `Plugins/OpenPype/Content/Python/init_unreal.py` - this should be executed by Unreal Editor
automatically.

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

View file

@ -0,0 +1,59 @@
// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved.
using UnrealBuildTool;
public class OpenPype : ModuleRules
{
public OpenPype(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
PublicIncludePaths.AddRange(
new string[] {
// ... add public include paths required here ...
}
);
PrivateIncludePaths.AddRange(
new string[] {
// ... add other private include paths required here ...
}
);
PublicDependencyModuleNames.AddRange(
new string[]
{
"Core",
// ... add other public dependencies that you statically link with here ...
}
);
PrivateDependencyModuleNames.AddRange(
new string[]
{
"Projects",
"InputCore",
"EditorFramework",
"UnrealEd",
"ToolMenus",
"LevelEditor",
"CoreUObject",
"Engine",
"Slate",
"SlateCore",
// ... add private dependencies that you statically link with here ...
}
);
DynamicallyLoadedModuleNames.AddRange(
new string[]
{
// ... add any modules that your module loads dynamically here ...
}
);
}
}

View file

@ -0,0 +1,115 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "AssetContainer.h"
#include "AssetRegistryModule.h"
#include "Misc/PackageName.h"
#include "Engine.h"
#include "Containers/UnrealString.h"
UAssetContainer::UAssetContainer(const FObjectInitializer& ObjectInitializer)
: UAssetUserData(ObjectInitializer)
{
FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
FString path = UAssetContainer::GetPathName();
UE_LOG(LogTemp, Warning, TEXT("UAssetContainer %s"), *path);
FARFilter Filter;
Filter.PackagePaths.Add(FName(*path));
AssetRegistryModule.Get().OnAssetAdded().AddUObject(this, &UAssetContainer::OnAssetAdded);
AssetRegistryModule.Get().OnAssetRemoved().AddUObject(this, &UAssetContainer::OnAssetRemoved);
AssetRegistryModule.Get().OnAssetRenamed().AddUObject(this, &UAssetContainer::OnAssetRenamed);
}
void UAssetContainer::OnAssetAdded(const FAssetData& AssetData)
{
TArray<FString> split;
// get directory of current container
FString selfFullPath = UAssetContainer::GetPathName();
FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath);
// get asset path and class
FString assetPath = AssetData.GetFullName();
FString assetFName = AssetData.AssetClass.ToString();
// split path
assetPath.ParseIntoArray(split, TEXT(" "), true);
FString assetDir = FPackageName::GetLongPackagePath(*split[1]);
// take interest only in paths starting with path of current container
if (assetDir.StartsWith(*selfDir))
{
// exclude self
if (assetFName != "AssetContainer")
{
assets.Add(assetPath);
assetsData.Add(AssetData);
UE_LOG(LogTemp, Log, TEXT("%s: asset added to %s"), *selfFullPath, *selfDir);
}
}
}
void UAssetContainer::OnAssetRemoved(const FAssetData& AssetData)
{
TArray<FString> split;
// get directory of current container
FString selfFullPath = UAssetContainer::GetPathName();
FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath);
// get asset path and class
FString assetPath = AssetData.GetFullName();
FString assetFName = AssetData.AssetClass.ToString();
// split path
assetPath.ParseIntoArray(split, TEXT(" "), true);
FString assetDir = FPackageName::GetLongPackagePath(*split[1]);
// take interest only in paths starting with path of current container
FString path = UAssetContainer::GetPathName();
FString lpp = FPackageName::GetLongPackagePath(*path);
if (assetDir.StartsWith(*selfDir))
{
// exclude self
if (assetFName != "AssetContainer")
{
// UE_LOG(LogTemp, Warning, TEXT("%s: asset removed"), *lpp);
assets.Remove(assetPath);
assetsData.Remove(AssetData);
}
}
}
void UAssetContainer::OnAssetRenamed(const FAssetData& AssetData, const FString& str)
{
TArray<FString> split;
// get directory of current container
FString selfFullPath = UAssetContainer::GetPathName();
FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath);
// get asset path and class
FString assetPath = AssetData.GetFullName();
FString assetFName = AssetData.AssetClass.ToString();
// split path
assetPath.ParseIntoArray(split, TEXT(" "), true);
FString assetDir = FPackageName::GetLongPackagePath(*split[1]);
if (assetDir.StartsWith(*selfDir))
{
// exclude self
if (assetFName != "AssetContainer")
{
assets.Remove(str);
assets.Add(assetPath);
assetsData.Remove(AssetData);
// UE_LOG(LogTemp, Warning, TEXT("%s: asset renamed %s"), *lpp, *str);
}
}
}

View file

@ -0,0 +1,20 @@
#include "AssetContainerFactory.h"
#include "AssetContainer.h"
UAssetContainerFactory::UAssetContainerFactory(const FObjectInitializer& ObjectInitializer)
: UFactory(ObjectInitializer)
{
SupportedClass = UAssetContainer::StaticClass();
bCreateNew = false;
bEditorImport = true;
}
UObject* UAssetContainerFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn)
{
UAssetContainer* AssetContainer = NewObject<UAssetContainer>(InParent, Class, Name, Flags);
return AssetContainer;
}
bool UAssetContainerFactory::ShouldShowInNewMenu() const {
return false;
}

View file

@ -0,0 +1,85 @@
#include "OpenPype.h"
#include "OpenPypeStyle.h"
#include "OpenPypeCommands.h"
#include "OpenPypePythonBridge.h"
#include "LevelEditor.h"
#include "Misc/MessageDialog.h"
#include "ToolMenus.h"
static const FName OpenPypeTabName("OpenPype");
#define LOCTEXT_NAMESPACE "FOpenPypeModule"
// This function is triggered when the plugin is staring up
void FOpenPypeModule::StartupModule()
{
FOpenPypeStyle::Initialize();
FOpenPypeStyle::ReloadTextures();
FOpenPypeCommands::Register();
PluginCommands = MakeShareable(new FUICommandList);
PluginCommands->MapAction(
FOpenPypeCommands::Get().OpenPypeTools,
FExecuteAction::CreateRaw(this, &FOpenPypeModule::MenuPopup),
FCanExecuteAction());
PluginCommands->MapAction(
FOpenPypeCommands::Get().OpenPypeToolsDialog,
FExecuteAction::CreateRaw(this, &FOpenPypeModule::MenuDialog),
FCanExecuteAction());
UToolMenus::RegisterStartupCallback(FSimpleMulticastDelegate::FDelegate::CreateRaw(this, &FOpenPypeModule::RegisterMenus));
}
void FOpenPypeModule::ShutdownModule()
{
UToolMenus::UnRegisterStartupCallback(this);
UToolMenus::UnregisterOwner(this);
FOpenPypeStyle::Shutdown();
FOpenPypeCommands::Unregister();
}
void FOpenPypeModule::RegisterMenus()
{
// Owner will be used for cleanup in call to UToolMenus::UnregisterOwner
FToolMenuOwnerScoped OwnerScoped(this);
{
UToolMenu* Menu = UToolMenus::Get()->ExtendMenu("LevelEditor.MainMenu.Tools");
{
// FToolMenuSection& Section = Menu->FindOrAddSection("OpenPype");
FToolMenuSection& Section = Menu->AddSection(
"OpenPype",
TAttribute<FText>(FText::FromString("OpenPype")),
FToolMenuInsert("Programming", EToolMenuInsertType::Before)
);
Section.AddMenuEntryWithCommandList(FOpenPypeCommands::Get().OpenPypeTools, PluginCommands);
Section.AddMenuEntryWithCommandList(FOpenPypeCommands::Get().OpenPypeToolsDialog, PluginCommands);
}
UToolMenu* ToolbarMenu = UToolMenus::Get()->ExtendMenu("LevelEditor.LevelEditorToolBar.PlayToolBar");
{
FToolMenuSection& Section = ToolbarMenu->FindOrAddSection("PluginTools");
{
FToolMenuEntry& Entry = Section.AddEntry(FToolMenuEntry::InitToolBarButton(FOpenPypeCommands::Get().OpenPypeTools));
Entry.SetCommandList(PluginCommands);
}
}
}
}
void FOpenPypeModule::MenuPopup() {
UOpenPypePythonBridge* bridge = UOpenPypePythonBridge::Get();
bridge->RunInPython_Popup();
}
void FOpenPypeModule::MenuDialog() {
UOpenPypePythonBridge* bridge = UOpenPypePythonBridge::Get();
bridge->RunInPython_Dialog();
}
IMPLEMENT_MODULE(FOpenPypeModule, OpenPype)

View file

@ -0,0 +1,13 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "OpenPypeCommands.h"
#define LOCTEXT_NAMESPACE "FOpenPypeModule"
void FOpenPypeCommands::RegisterCommands()
{
UI_COMMAND(OpenPypeTools, "OpenPype Tools", "Pipeline tools", EUserInterfaceActionType::Button, FInputChord());
UI_COMMAND(OpenPypeToolsDialog, "OpenPype Tools Dialog", "Pipeline tools dialog", EUserInterfaceActionType::Button, FInputChord());
}
#undef LOCTEXT_NAMESPACE

View file

@ -0,0 +1,48 @@
#include "OpenPypeLib.h"
#include "Misc/Paths.h"
#include "Misc/ConfigCacheIni.h"
#include "UObject/UnrealType.h"
/**
* Sets color on folder icon on given path
* @param InPath - path to folder
* @param InFolderColor - color of the folder
* @warning This color will appear only after Editor restart. Is there a better way?
*/
void UOpenPypeLib::CSetFolderColor(FString FolderPath, FLinearColor FolderColor, bool bForceAdd)
{
auto SaveColorInternal = [](FString InPath, FLinearColor InFolderColor)
{
// Saves the color of the folder to the config
if (FPaths::FileExists(GEditorPerProjectIni))
{
GConfig->SetString(TEXT("PathColor"), *InPath, *InFolderColor.ToString(), GEditorPerProjectIni);
}
};
SaveColorInternal(FolderPath, FolderColor);
}
/**
* Returns all poperties on given object
* @param cls - class
* @return TArray of properties
*/
TArray<FString> UOpenPypeLib::GetAllProperties(UClass* cls)
{
TArray<FString> Ret;
if (cls != nullptr)
{
for (TFieldIterator<FProperty> It(cls); It; ++It)
{
FProperty* Property = *It;
if (Property->HasAnyPropertyFlags(EPropertyFlags::CPF_Edit))
{
Ret.Add(Property->GetName());
}
}
}
return Ret;
}

View file

@ -0,0 +1,108 @@
#pragma once
#include "OpenPypePublishInstance.h"
#include "AssetRegistryModule.h"
UOpenPypePublishInstance::UOpenPypePublishInstance(const FObjectInitializer& ObjectInitializer)
: UObject(ObjectInitializer)
{
FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
FString path = UOpenPypePublishInstance::GetPathName();
FARFilter Filter;
Filter.PackagePaths.Add(FName(*path));
AssetRegistryModule.Get().OnAssetAdded().AddUObject(this, &UOpenPypePublishInstance::OnAssetAdded);
AssetRegistryModule.Get().OnAssetRemoved().AddUObject(this, &UOpenPypePublishInstance::OnAssetRemoved);
AssetRegistryModule.Get().OnAssetRenamed().AddUObject(this, &UOpenPypePublishInstance::OnAssetRenamed);
}
void UOpenPypePublishInstance::OnAssetAdded(const FAssetData& AssetData)
{
TArray<FString> split;
// get directory of current container
FString selfFullPath = UOpenPypePublishInstance::GetPathName();
FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath);
// get asset path and class
FString assetPath = AssetData.GetFullName();
FString assetFName = AssetData.AssetClass.ToString();
// split path
assetPath.ParseIntoArray(split, TEXT(" "), true);
FString assetDir = FPackageName::GetLongPackagePath(*split[1]);
// take interest only in paths starting with path of current container
if (assetDir.StartsWith(*selfDir))
{
// exclude self
if (assetFName != "OpenPypePublishInstance")
{
assets.Add(assetPath);
UE_LOG(LogTemp, Log, TEXT("%s: asset added to %s"), *selfFullPath, *selfDir);
}
}
}
void UOpenPypePublishInstance::OnAssetRemoved(const FAssetData& AssetData)
{
TArray<FString> split;
// get directory of current container
FString selfFullPath = UOpenPypePublishInstance::GetPathName();
FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath);
// get asset path and class
FString assetPath = AssetData.GetFullName();
FString assetFName = AssetData.AssetClass.ToString();
// split path
assetPath.ParseIntoArray(split, TEXT(" "), true);
FString assetDir = FPackageName::GetLongPackagePath(*split[1]);
// take interest only in paths starting with path of current container
FString path = UOpenPypePublishInstance::GetPathName();
FString lpp = FPackageName::GetLongPackagePath(*path);
if (assetDir.StartsWith(*selfDir))
{
// exclude self
if (assetFName != "OpenPypePublishInstance")
{
// UE_LOG(LogTemp, Warning, TEXT("%s: asset removed"), *lpp);
assets.Remove(assetPath);
}
}
}
void UOpenPypePublishInstance::OnAssetRenamed(const FAssetData& AssetData, const FString& str)
{
TArray<FString> split;
// get directory of current container
FString selfFullPath = UOpenPypePublishInstance::GetPathName();
FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath);
// get asset path and class
FString assetPath = AssetData.GetFullName();
FString assetFName = AssetData.AssetClass.ToString();
// split path
assetPath.ParseIntoArray(split, TEXT(" "), true);
FString assetDir = FPackageName::GetLongPackagePath(*split[1]);
if (assetDir.StartsWith(*selfDir))
{
// exclude self
if (assetFName != "AssetContainer")
{
assets.Remove(str);
assets.Add(assetPath);
// UE_LOG(LogTemp, Warning, TEXT("%s: asset renamed %s"), *lpp, *str);
}
}
}

View file

@ -0,0 +1,20 @@
#include "OpenPypePublishInstanceFactory.h"
#include "OpenPypePublishInstance.h"
UOpenPypePublishInstanceFactory::UOpenPypePublishInstanceFactory(const FObjectInitializer& ObjectInitializer)
: UFactory(ObjectInitializer)
{
SupportedClass = UOpenPypePublishInstance::StaticClass();
bCreateNew = false;
bEditorImport = true;
}
UObject* UOpenPypePublishInstanceFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn)
{
UOpenPypePublishInstance* OpenPypePublishInstance = NewObject<UOpenPypePublishInstance>(InParent, Class, Name, Flags);
return OpenPypePublishInstance;
}
bool UOpenPypePublishInstanceFactory::ShouldShowInNewMenu() const {
return false;
}

View file

@ -0,0 +1,13 @@
#include "OpenPypePythonBridge.h"
UOpenPypePythonBridge* UOpenPypePythonBridge::Get()
{
TArray<UClass*> OpenPypePythonBridgeClasses;
GetDerivedClasses(UOpenPypePythonBridge::StaticClass(), OpenPypePythonBridgeClasses);
int32 NumClasses = OpenPypePythonBridgeClasses.Num();
if (NumClasses > 0)
{
return Cast<UOpenPypePythonBridge>(OpenPypePythonBridgeClasses[NumClasses - 1]->GetDefaultObject());
}
return nullptr;
};

View file

@ -0,0 +1,61 @@
#include "OpenPype.h"
#include "OpenPypeStyle.h"
#include "Framework/Application/SlateApplication.h"
#include "Styling/SlateStyleRegistry.h"
#include "Slate/SlateGameResources.h"
#include "Interfaces/IPluginManager.h"
#include "Styling/SlateStyleMacros.h"
#define RootToContentDir Style->RootToContentDir
TSharedPtr<FSlateStyleSet> FOpenPypeStyle::OpenPypeStyleInstance = nullptr;
void FOpenPypeStyle::Initialize()
{
if (!OpenPypeStyleInstance.IsValid())
{
OpenPypeStyleInstance = Create();
FSlateStyleRegistry::RegisterSlateStyle(*OpenPypeStyleInstance);
}
}
void FOpenPypeStyle::Shutdown()
{
FSlateStyleRegistry::UnRegisterSlateStyle(*OpenPypeStyleInstance);
ensure(OpenPypeStyleInstance.IsUnique());
OpenPypeStyleInstance.Reset();
}
FName FOpenPypeStyle::GetStyleSetName()
{
static FName StyleSetName(TEXT("OpenPypeStyle"));
return StyleSetName;
}
const FVector2D Icon16x16(16.0f, 16.0f);
const FVector2D Icon20x20(20.0f, 20.0f);
const FVector2D Icon40x40(40.0f, 40.0f);
TSharedRef< FSlateStyleSet > FOpenPypeStyle::Create()
{
TSharedRef< FSlateStyleSet > Style = MakeShareable(new FSlateStyleSet("OpenPypeStyle"));
Style->SetContentRoot(IPluginManager::Get().FindPlugin("OpenPype")->GetBaseDir() / TEXT("Resources"));
Style->Set("OpenPype.OpenPypeTools", new IMAGE_BRUSH(TEXT("openpype40"), Icon40x40));
Style->Set("OpenPype.OpenPypeToolsDialog", new IMAGE_BRUSH(TEXT("openpype40"), Icon40x40));
return Style;
}
void FOpenPypeStyle::ReloadTextures()
{
if (FSlateApplication::IsInitialized())
{
FSlateApplication::Get().GetRenderer()->ReloadTextureResources();
}
}
const ISlateStyle& FOpenPypeStyle::Get()
{
return *OpenPypeStyleInstance;
}

View file

@ -0,0 +1,39 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "Engine/AssetUserData.h"
#include "AssetData.h"
#include "AssetContainer.generated.h"
/**
*
*/
UCLASS(Blueprintable)
class OPENPYPE_API UAssetContainer : public UAssetUserData
{
GENERATED_BODY()
public:
UAssetContainer(const FObjectInitializer& ObjectInitalizer);
// ~UAssetContainer();
UPROPERTY(EditAnywhere, BlueprintReadOnly)
TArray<FString> assets;
// There seems to be no reflection option to expose array of FAssetData
/*
UPROPERTY(Transient, BlueprintReadOnly, Category = "Python", meta=(DisplayName="Assets Data"))
TArray<FAssetData> assetsData;
*/
private:
TArray<FAssetData> assetsData;
void OnAssetAdded(const FAssetData& AssetData);
void OnAssetRemoved(const FAssetData& AssetData);
void OnAssetRenamed(const FAssetData& AssetData, const FString& str);
};

View file

@ -0,0 +1,21 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Factories/Factory.h"
#include "AssetContainerFactory.generated.h"
/**
*
*/
UCLASS()
class OPENPYPE_API UAssetContainerFactory : public UFactory
{
GENERATED_BODY()
public:
UAssetContainerFactory(const FObjectInitializer& ObjectInitializer);
virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override;
virtual bool ShouldShowInNewMenu() const override;
};

View file

@ -0,0 +1,23 @@
// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Modules/ModuleManager.h"
class FOpenPypeModule : public IModuleInterface
{
public:
virtual void StartupModule() override;
virtual void ShutdownModule() override;
private:
void RegisterMenus();
void MenuPopup();
void MenuDialog();
private:
TSharedPtr<class FUICommandList> PluginCommands;
};

View file

@ -0,0 +1,24 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Framework/Commands/Commands.h"
#include "OpenPypeStyle.h"
class FOpenPypeCommands : public TCommands<FOpenPypeCommands>
{
public:
FOpenPypeCommands()
: TCommands<FOpenPypeCommands>(TEXT("OpenPype"), NSLOCTEXT("Contexts", "OpenPype", "OpenPype Tools"), NAME_None, FOpenPypeStyle::GetStyleSetName())
{
}
// TCommands<> interface
virtual void RegisterCommands() override;
public:
TSharedPtr< FUICommandInfo > OpenPypeTools;
TSharedPtr< FUICommandInfo > OpenPypeToolsDialog;
};

View file

@ -0,0 +1,19 @@
#pragma once
#include "Engine.h"
#include "OpenPypeLib.generated.h"
UCLASS(Blueprintable)
class OPENPYPE_API UOpenPypeLib : public UObject
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable, Category = Python)
static void CSetFolderColor(FString FolderPath, FLinearColor FolderColor, bool bForceAdd);
UFUNCTION(BlueprintCallable, Category = Python)
static TArray<FString> GetAllProperties(UClass* cls);
};

View file

@ -0,0 +1,21 @@
#pragma once
#include "Engine.h"
#include "OpenPypePublishInstance.generated.h"
UCLASS(Blueprintable)
class OPENPYPE_API UOpenPypePublishInstance : public UObject
{
GENERATED_BODY()
public:
UOpenPypePublishInstance(const FObjectInitializer& ObjectInitalizer);
UPROPERTY(EditAnywhere, BlueprintReadOnly)
TArray<FString> assets;
private:
void OnAssetAdded(const FAssetData& AssetData);
void OnAssetRemoved(const FAssetData& AssetData);
void OnAssetRenamed(const FAssetData& AssetData, const FString& str);
};

View file

@ -0,0 +1,19 @@
#pragma once
#include "CoreMinimal.h"
#include "Factories/Factory.h"
#include "OpenPypePublishInstanceFactory.generated.h"
/**
*
*/
UCLASS()
class OPENPYPE_API UOpenPypePublishInstanceFactory : public UFactory
{
GENERATED_BODY()
public:
UOpenPypePublishInstanceFactory(const FObjectInitializer& ObjectInitializer);
virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override;
virtual bool ShouldShowInNewMenu() const override;
};

View file

@ -0,0 +1,20 @@
#pragma once
#include "Engine.h"
#include "OpenPypePythonBridge.generated.h"
UCLASS(Blueprintable)
class UOpenPypePythonBridge : public UObject
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable, Category = Python)
static UOpenPypePythonBridge* Get();
UFUNCTION(BlueprintImplementableEvent, Category = Python)
void RunInPython_Popup() const;
UFUNCTION(BlueprintImplementableEvent, Category = Python)
void RunInPython_Dialog() const;
};

View file

@ -0,0 +1,18 @@
#pragma once
#include "CoreMinimal.h"
#include "Styling/SlateStyle.h"
class FOpenPypeStyle
{
public:
static void Initialize();
static void Shutdown();
static void ReloadTextures();
static const ISlateStyle& Get();
static FName GetStyleSetName();
private:
static TSharedRef< class FSlateStyleSet > Create();
static TSharedPtr< class FSlateStyleSet > OpenPypeStyleInstance;
};

View file

@ -70,19 +70,22 @@ def get_engine_versions(env=None):
return OrderedDict()
def get_editor_executable_path(engine_path: Path) -> Path:
"""Get UE4 Editor executable path."""
ue4_path = engine_path / "Engine/Binaries"
def get_editor_executable_path(engine_path: Path, engine_version: str) -> Path:
"""Get UE Editor executable path."""
ue_path = engine_path / "Engine/Binaries"
if platform.system().lower() == "windows":
ue4_path /= "Win64/UE4Editor.exe"
if engine_version.split(".")[0] == "4":
ue_path /= "Win64/UE4Editor.exe"
elif engine_version.split(".")[0] == "5":
ue_path /= "Win64/UnrealEditor.exe"
elif platform.system().lower() == "linux":
ue4_path /= "Linux/UE4Editor"
ue_path /= "Linux/UE4Editor"
elif platform.system().lower() == "darwin":
ue4_path /= "Mac/UE4Editor"
ue_path /= "Mac/UE4Editor"
return ue4_path
return ue_path
def _win_get_engine_versions():
@ -208,22 +211,26 @@ def create_unreal_project(project_name: str,
# created in different UE4 version. When user convert such project
# to his UE4 version, Engine ID is replaced in uproject file. If some
# other user tries to open it, it will present him with similar error.
ue4_modules = Path()
ue_modules = Path()
if platform.system().lower() == "windows":
ue4_modules = Path(os.path.join(engine_path, "Engine", "Binaries",
"Win64", "UE4Editor.modules"))
ue_modules_path = engine_path / "Engine/Binaries/Win64"
if ue_version.split(".")[0] == "4":
ue_modules_path /= "UE4Editor.modules"
elif ue_version.split(".")[0] == "5":
ue_modules_path /= "UnrealEditor.modules"
ue_modules = Path(ue_modules_path)
if platform.system().lower() == "linux":
ue4_modules = Path(os.path.join(engine_path, "Engine", "Binaries",
ue_modules = Path(os.path.join(engine_path, "Engine", "Binaries",
"Linux", "UE4Editor.modules"))
if platform.system().lower() == "darwin":
ue4_modules = Path(os.path.join(engine_path, "Engine", "Binaries",
ue_modules = Path(os.path.join(engine_path, "Engine", "Binaries",
"Mac", "UE4Editor.modules"))
if ue4_modules.exists():
if ue_modules.exists():
print("--- Loading Engine ID from modules file ...")
with open(ue4_modules, "r") as mp:
with open(ue_modules, "r") as mp:
loaded_modules = json.load(mp)
if loaded_modules.get("BuildId"):
@ -280,7 +287,7 @@ def create_unreal_project(project_name: str,
python_path = None
if platform.system().lower() == "windows":
python_path = engine_path / ("Engine/Binaries/ThirdParty/"
"Python3/Win64/pythonw.exe")
"Python3/Win64/python.exe")
if platform.system().lower() == "linux":
python_path = engine_path / ("Engine/Binaries/ThirdParty/"
@ -294,14 +301,15 @@ def create_unreal_project(project_name: str,
raise NotImplementedError("Unsupported platform")
if not python_path.exists():
raise RuntimeError(f"Unreal Python not found at {python_path}")
subprocess.run(
subprocess.check_call(
[python_path.as_posix(), "-m", "pip", "install", "pyside2"])
if dev_mode or preset["dev_mode"]:
_prepare_cpp_project(project_file, engine_path)
_prepare_cpp_project(project_file, engine_path, ue_version)
def _prepare_cpp_project(project_file: Path, engine_path: Path) -> None:
def _prepare_cpp_project(
project_file: Path, engine_path: Path, ue_version: str) -> None:
"""Prepare CPP Unreal Project.
This function will add source files needed for project to be
@ -420,8 +428,12 @@ class {1}_API A{0}GameModeBase : public AGameModeBase
with open(sources_dir / f"{project_name}GameModeBase.h", mode="w") as f:
f.write(game_mode_h)
u_build_tool = Path(
engine_path / "Engine/Binaries/DotNET/UnrealBuildTool.exe")
u_build_tool_path = engine_path / "Engine/Binaries/DotNET"
if ue_version.split(".")[0] == "4":
u_build_tool_path /= "UnrealBuildTool.exe"
elif ue_version.split(".")[0] == "5":
u_build_tool_path /= "UnrealBuildTool/UnrealBuildTool.exe"
u_build_tool = Path(u_build_tool_path)
u_header_tool = None
arch = "Win64"

View file

@ -22,17 +22,24 @@ class CreateRender(Creator):
ar = unreal.AssetRegistryHelpers.get_asset_registry()
# The asset name is the the third element of the path which contains
# the map.
# The index of the split path is 3 because the first element is an
# empty string, as the path begins with "/Content".
a = unreal.EditorUtilityLibrary.get_selected_assets()[0]
asset_name = a.get_path_name().split("/")[3]
# Get the master sequence and the master level.
# There should be only one sequence and one level in the directory.
filter = unreal.ARFilter(
class_names=["LevelSequence"],
package_paths=[f"/Game/OpenPype/{self.data['asset']}"],
package_paths=[f"/Game/OpenPype/{asset_name}"],
recursive_paths=False)
sequences = ar.get_assets(filter)
ms = sequences[0].get_editor_property('object_path')
filter = unreal.ARFilter(
class_names=["World"],
package_paths=[f"/Game/OpenPype/{self.data['asset']}"],
package_paths=[f"/Game/OpenPype/{asset_name}"],
recursive_paths=False)
levels = ar.get_assets(filter)
ml = levels[0].get_editor_property('object_path')

View file

@ -14,6 +14,7 @@ from openpype.pipeline import (
)
from openpype.hosts.unreal.api import plugin
from openpype.hosts.unreal.api import pipeline as unreal_pipeline
from openpype.api import get_asset
class AnimationFBXLoader(plugin.Loader):
@ -77,13 +78,15 @@ class AnimationFBXLoader(plugin.Loader):
task.options.anim_sequence_import_data.set_editor_property(
'import_meshes_in_bone_hierarchy', False)
task.options.anim_sequence_import_data.set_editor_property(
'use_default_sample_rate', True)
'use_default_sample_rate', False)
task.options.anim_sequence_import_data.set_editor_property(
'custom_sample_rate', get_asset()["data"].get("fps"))
task.options.anim_sequence_import_data.set_editor_property(
'import_custom_attribute', True)
task.options.anim_sequence_import_data.set_editor_property(
'import_bone_tracks', True)
task.options.anim_sequence_import_data.set_editor_property(
'remove_redundant_keys', True)
'remove_redundant_keys', False)
task.options.anim_sequence_import_data.set_editor_property(
'convert_scene', True)
@ -139,22 +142,18 @@ class AnimationFBXLoader(plugin.Loader):
root = "/Game/OpenPype"
asset = context.get('asset').get('name')
suffix = "_CON"
if asset:
asset_name = "{}_{}".format(asset, name)
else:
asset_name = "{}".format(name)
asset_name = f"{asset}_{name}" if asset else f"{name}"
tools = unreal.AssetToolsHelpers().get_asset_tools()
asset_dir, container_name = tools.create_unique_asset_name(
f"{root}/Animations/{asset}/{name}", suffix="")
ar = unreal.AssetRegistryHelpers.get_asset_registry()
filter = unreal.ARFilter(
_filter = unreal.ARFilter(
class_names=["World"],
package_paths=[f"{root}/{hierarchy[0]}"],
recursive_paths=False)
levels = ar.get_assets(filter)
levels = ar.get_assets(_filter)
master_level = levels[0].get_editor_property('object_path')
hierarchy_dir = root
@ -162,11 +161,11 @@ class AnimationFBXLoader(plugin.Loader):
hierarchy_dir = f"{hierarchy_dir}/{h}"
hierarchy_dir = f"{hierarchy_dir}/{asset}"
filter = unreal.ARFilter(
_filter = unreal.ARFilter(
class_names=["World"],
package_paths=[f"{hierarchy_dir}/"],
recursive_paths=True)
levels = ar.get_assets(filter)
levels = ar.get_assets(_filter)
level = levels[0].get_editor_property('object_path')
unreal.EditorLevelLibrary.save_all_dirty_levels()
@ -233,8 +232,7 @@ class AnimationFBXLoader(plugin.Loader):
"parent": context["representation"]["parent"],
"family": context["representation"]["context"]["family"]
}
unreal_pipeline.imprint(
"{}/{}".format(asset_dir, container_name), data)
unreal_pipeline.imprint(f"{asset_dir}/{container_name}", data)
imported_content = EditorAssetLibrary.list_assets(
asset_dir, recursive=True, include_folder=False)
@ -279,13 +277,15 @@ class AnimationFBXLoader(plugin.Loader):
task.options.anim_sequence_import_data.set_editor_property(
'import_meshes_in_bone_hierarchy', False)
task.options.anim_sequence_import_data.set_editor_property(
'use_default_sample_rate', True)
'use_default_sample_rate', False)
task.options.anim_sequence_import_data.set_editor_property(
'custom_sample_rate', get_asset()["data"].get("fps"))
task.options.anim_sequence_import_data.set_editor_property(
'import_custom_attribute', True)
task.options.anim_sequence_import_data.set_editor_property(
'import_bone_tracks', True)
task.options.anim_sequence_import_data.set_editor_property(
'remove_redundant_keys', True)
'remove_redundant_keys', False)
task.options.anim_sequence_import_data.set_editor_property(
'convert_scene', True)
@ -296,8 +296,7 @@ class AnimationFBXLoader(plugin.Loader):
# do import fbx and replace existing data
unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])
container_path = "{}/{}".format(container["namespace"],
container["objectName"])
container_path = f'{container["namespace"]}/{container["objectName"]}'
# update metadata
unreal_pipeline.imprint(
container_path,

View file

@ -5,6 +5,7 @@ from pathlib import Path
import unreal
from unreal import EditorAssetLibrary
from unreal import EditorLevelLibrary
from unreal import EditorLevelUtils
from openpype.pipeline import (
AVALON_CONTAINER_ID,
@ -57,6 +58,33 @@ class CameraLoader(plugin.Loader):
min_frame_j,
max_frame_j + 1)
def _import_camera(
self, world, sequence, bindings, import_fbx_settings, import_filename
):
ue_version = unreal.SystemLibrary.get_engine_version().split('.')
ue_major = int(ue_version[0])
ue_minor = int(ue_version[1])
if ue_major == 4 and ue_minor <= 26:
unreal.SequencerTools.import_fbx(
world,
sequence,
bindings,
import_fbx_settings,
import_filename
)
elif (ue_major == 4 and ue_minor >= 27) or ue_major == 5:
unreal.SequencerTools.import_level_sequence_fbx(
world,
sequence,
bindings,
import_fbx_settings,
import_filename
)
else:
raise NotImplementedError(
f"Unreal version {ue_major} not supported")
def load(self, context, name, namespace, data):
"""
Load and containerise representation into Content Browser.
@ -84,10 +112,10 @@ class CameraLoader(plugin.Loader):
hierarchy = context.get('asset').get('data').get('parents')
root = "/Game/OpenPype"
hierarchy_dir = root
hierarchy_list = []
hierarchy_dir_list = []
for h in hierarchy:
hierarchy_dir = f"{hierarchy_dir}/{h}"
hierarchy_list.append(hierarchy_dir)
hierarchy_dir_list.append(hierarchy_dir)
asset = context.get('asset').get('name')
suffix = "_CON"
if asset:
@ -121,27 +149,40 @@ class CameraLoader(plugin.Loader):
asset_dir, container_name = tools.create_unique_asset_name(
f"{hierarchy_dir}/{asset}/{name}_{unique_number:02d}", suffix="")
asset_path = Path(asset_dir)
asset_path_parent = str(asset_path.parent.as_posix())
container_name += suffix
current_level = EditorLevelLibrary.get_editor_world().get_full_name()
EditorAssetLibrary.make_directory(asset_dir)
# Create map for the shot, and create hierarchy of map. If the maps
# already exist, we will use them.
h_dir = hierarchy_dir_list[0]
h_asset = hierarchy[0]
master_level = f"{h_dir}/{h_asset}_map.{h_asset}_map"
if not EditorAssetLibrary.does_asset_exist(master_level):
EditorLevelLibrary.new_level(f"{h_dir}/{h_asset}_map")
level = f"{asset_path_parent}/{asset}_map.{asset}_map"
if not EditorAssetLibrary.does_asset_exist(level):
EditorLevelLibrary.new_level(f"{asset_path_parent}/{asset}_map")
EditorLevelLibrary.load_level(master_level)
EditorLevelUtils.add_level_to_world(
EditorLevelLibrary.get_editor_world(),
level,
unreal.LevelStreamingDynamic
)
EditorLevelLibrary.save_all_dirty_levels()
ar = unreal.AssetRegistryHelpers.get_asset_registry()
filter = unreal.ARFilter(
class_names=["World"],
package_paths=[f"{hierarchy_dir}/{asset}/"],
recursive_paths=True)
maps = ar.get_assets(filter)
# There should be only one map in the list
EditorLevelLibrary.load_level(maps[0].get_full_name())
EditorLevelLibrary.load_level(level)
# Get all the sequences in the hierarchy. It will create them, if
# they don't exist.
sequences = []
frame_ranges = []
i = 0
for h in hierarchy_list:
for h in hierarchy_dir_list:
root_content = EditorAssetLibrary.list_assets(
h, recursive=False, include_folder=False)
@ -228,7 +269,7 @@ class CameraLoader(plugin.Loader):
settings.set_editor_property('reduce_keys', False)
if cam_seq:
unreal.SequencerTools.import_fbx(
self._import_camera(
EditorLevelLibrary.get_editor_world(),
cam_seq,
cam_seq.get_bindings(),
@ -256,7 +297,7 @@ class CameraLoader(plugin.Loader):
"{}/{}".format(asset_dir, container_name), data)
EditorLevelLibrary.save_all_dirty_levels()
EditorLevelLibrary.load_level(current_level)
EditorLevelLibrary.load_level(master_level)
asset_content = EditorAssetLibrary.list_assets(
asset_dir, recursive=True, include_folder=True
@ -388,7 +429,7 @@ class CameraLoader(plugin.Loader):
sub_scene.set_sequence(new_sequence)
unreal.SequencerTools.import_fbx(
self._import_camera(
EditorLevelLibrary.get_editor_world(),
new_sequence,
new_sequence.get_bindings(),

View file

@ -20,6 +20,7 @@ from openpype.pipeline import (
AVALON_CONTAINER_ID,
legacy_io,
)
from openpype.api import get_asset
from openpype.hosts.unreal.api import plugin
from openpype.hosts.unreal.api import pipeline as unreal_pipeline
@ -87,7 +88,8 @@ class LayoutLoader(plugin.Loader):
return None
def _get_data(self, asset_name):
@staticmethod
def _get_data(asset_name):
asset_doc = legacy_io.find_one({
"type": "asset",
"name": asset_name
@ -95,8 +97,9 @@ class LayoutLoader(plugin.Loader):
return asset_doc.get("data")
@staticmethod
def _set_sequence_hierarchy(
self, seq_i, seq_j, max_frame_i, min_frame_j, max_frame_j, map_paths
seq_i, seq_j, max_frame_i, min_frame_j, max_frame_j, map_paths
):
# Get existing sequencer tracks or create them if they don't exist
tracks = seq_i.get_master_tracks()
@ -165,8 +168,9 @@ class LayoutLoader(plugin.Loader):
hid_section.set_row_index(index)
hid_section.set_level_names(maps)
@staticmethod
def _process_family(
self, assets, class_name, transform, sequence, inst_name=None
assets, class_name, transform, sequence, inst_name=None
):
ar = unreal.AssetRegistryHelpers.get_asset_registry()
@ -262,13 +266,15 @@ class LayoutLoader(plugin.Loader):
task.options.anim_sequence_import_data.set_editor_property(
'import_meshes_in_bone_hierarchy', False)
task.options.anim_sequence_import_data.set_editor_property(
'use_default_sample_rate', True)
'use_default_sample_rate', False)
task.options.anim_sequence_import_data.set_editor_property(
'custom_sample_rate', get_asset()["data"].get("fps"))
task.options.anim_sequence_import_data.set_editor_property(
'import_custom_attribute', True)
task.options.anim_sequence_import_data.set_editor_property(
'import_bone_tracks', True)
task.options.anim_sequence_import_data.set_editor_property(
'remove_redundant_keys', True)
'remove_redundant_keys', False)
task.options.anim_sequence_import_data.set_editor_property(
'convert_scene', True)
@ -311,11 +317,8 @@ class LayoutLoader(plugin.Loader):
for binding in bindings:
tracks = binding.get_tracks()
track = None
if not tracks:
track = binding.add_track(
unreal.MovieSceneSkeletalAnimationTrack)
else:
track = tracks[0]
track = tracks[0] if tracks else binding.add_track(
unreal.MovieSceneSkeletalAnimationTrack)
sections = track.get_sections()
section = None
@ -335,11 +338,11 @@ class LayoutLoader(plugin.Loader):
curr_anim.get_path_name()).parent
).replace('\\', '/')
filter = unreal.ARFilter(
_filter = unreal.ARFilter(
class_names=["AssetContainer"],
package_paths=[anim_path],
recursive_paths=False)
containers = ar.get_assets(filter)
containers = ar.get_assets(_filter)
if len(containers) > 0:
return
@ -350,6 +353,7 @@ class LayoutLoader(plugin.Loader):
sec_params = section.get_editor_property('params')
sec_params.set_editor_property('animation', animation)
@staticmethod
def _generate_sequence(self, h, h_dir):
tools = unreal.AssetToolsHelpers().get_asset_tools()
@ -583,10 +587,7 @@ class LayoutLoader(plugin.Loader):
hierarchy_dir_list.append(hierarchy_dir)
asset = context.get('asset').get('name')
suffix = "_CON"
if asset:
asset_name = "{}_{}".format(asset, name)
else:
asset_name = "{}".format(name)
asset_name = f"{asset}_{name}" if asset else name
tools = unreal.AssetToolsHelpers().get_asset_tools()
asset_dir, container_name = tools.create_unique_asset_name(
@ -800,7 +801,7 @@ class LayoutLoader(plugin.Loader):
lc for lc in layout_containers
if asset in lc.get('loaded_assets')]
if len(layouts) == 0:
if not layouts:
EditorAssetLibrary.delete_directory(str(Path(asset).parent))
# Remove the Level Sequence from the parent.
@ -810,17 +811,17 @@ class LayoutLoader(plugin.Loader):
namespace = container.get('namespace').replace(f"{root}/", "")
ms_asset = namespace.split('/')[0]
ar = unreal.AssetRegistryHelpers.get_asset_registry()
filter = unreal.ARFilter(
_filter = unreal.ARFilter(
class_names=["LevelSequence"],
package_paths=[f"{root}/{ms_asset}"],
recursive_paths=False)
sequences = ar.get_assets(filter)
sequences = ar.get_assets(_filter)
master_sequence = sequences[0].get_asset()
filter = unreal.ARFilter(
_filter = unreal.ARFilter(
class_names=["World"],
package_paths=[f"{root}/{ms_asset}"],
recursive_paths=False)
levels = ar.get_assets(filter)
levels = ar.get_assets(_filter)
master_level = levels[0].get_editor_property('object_path')
sequences = [master_sequence]

View file

@ -17,7 +17,7 @@ def otio_range_to_frame_range(otio_range):
start = _ot.to_frames(
otio_range.start_time, otio_range.start_time.rate)
end = start + _ot.to_frames(
otio_range.duration, otio_range.duration.rate) - 1
otio_range.duration, otio_range.duration.rate)
return start, end
@ -168,7 +168,7 @@ def make_sequence_collection(path, otio_range, metadata):
first, last = otio_range_to_frame_range(otio_range)
collection = clique.Collection(
head=head, tail=tail, padding=metadata["padding"])
collection.indexes.update([i for i in range(first, (last + 1))])
collection.indexes.update([i for i in range(first, last)])
return dir_path, collection
@ -218,6 +218,7 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end):
"name": name
}
tw_node.update(metadata)
tw_node["lookup"] = list(lookup)
# get first and last frame offsets
offset_in += lookup[0]

View file

@ -702,6 +702,32 @@ class ModulesManager:
).format(expected_keys, " | ".join(msg_items)))
return output
def collect_creator_plugin_paths(self, host_name):
"""Helper to collect creator plugin paths from modules.
Args:
host_name (str): For which host are creators meants.
Returns:
list: List of creator plugin paths.
"""
# Output structure
from openpype_interfaces import IPluginPaths
output = []
for module in self.get_enabled_modules():
# Skip module that do not inherit from `IPluginPaths`
if not isinstance(module, IPluginPaths):
continue
paths = module.get_creator_plugin_paths(host_name)
if paths:
# Convert to list if value is not list
if not isinstance(paths, (list, tuple, set)):
paths = [paths]
output.extend(paths)
return output
def collect_launch_hook_paths(self):
"""Helper to collect hooks from modules inherited ILaunchHookPaths.

View file

@ -466,7 +466,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
if instance_data.get("multipartExr"):
preview = True
new_instance = copy(instance_data)
new_instance = deepcopy(instance_data)
new_instance["subset"] = subset_name
new_instance["subsetGroup"] = group_name
if preview:
@ -883,8 +883,10 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
new_i = copy(i)
new_i["version"] = at.get("version")
new_i["subset"] = at.get("subset")
new_i["family"] = at.get("family")
new_i["append"] = True
new_i["families"].append(at.get("family"))
# don't set subsetGroup if we are attaching
new_i.pop("subsetGroup")
new_instances.append(new_i)
self.log.info(" - {} / v{}".format(
at.get("subset"), at.get("version")))

View file

@ -356,7 +356,7 @@ class PushHierValuesToNonHier(ServerAction):
values_per_entity_id[entity_id][key] = None
values = query_custom_attributes(
session, all_ids_with_parents, hier_attr_ids, True
session, hier_attr_ids, all_ids_with_parents, True
)
for item in values:
entity_id = item["entity_id"]

View file

@ -143,14 +143,17 @@ def create_chunks(iterable, chunk_size=None):
list<list>: Chunked items.
"""
chunks = []
if not iterable:
return chunks
tupled_iterable = tuple(iterable)
if not tupled_iterable:
return chunks
iterable_size = len(tupled_iterable)
if chunk_size is None:
chunk_size = 200
if chunk_size < 1:
chunk_size = 1
for idx in range(0, iterable_size, chunk_size):
chunks.append(tupled_iterable[idx:idx + chunk_size])
return chunks

View file

@ -92,14 +92,18 @@ def check_credentials(username, api_key, ftrack_server=None):
if not ftrack_server or not username or not api_key:
return False
user_exists = False
try:
session = ftrack_api.Session(
server_url=ftrack_server,
api_key=api_key,
api_user=username
)
# Validated that the username actually exists
user = session.query("User where username is \"{}\"".format(username))
user_exists = user is not None
session.close()
except Exception:
return False
return True
pass
return user_exists

View file

@ -176,7 +176,10 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin):
# Add item to component list
component_list.append(thumbnail_item)
if first_thumbnail_component is not None:
if (
not review_representations
and first_thumbnail_component is not None
):
width = first_thumbnail_component_repre.get("width")
height = first_thumbnail_component_repre.get("height")
if not width or not height:

View file

@ -14,11 +14,38 @@ class IPluginPaths(OpenPypeInterface):
"publish": ["path/to/publish_plugins"]
}
"""
# TODO validation of an output
@abstractmethod
def get_plugin_paths(self):
pass
def get_creator_plugin_paths(self, host_name):
"""Retreive creator plugin paths.
Give addons ability to add creator plugin paths based on host name.
NOTES:
- Default implementation uses 'get_plugin_paths' and always return
all creator plugins.
- Host name may help to organize plugins by host, but each creator
alsomay have host filtering.
Args:
host_name (str): For which host are the plugins meant.
"""
paths = self.get_plugin_paths()
if not paths or "create" not in paths:
return []
create_paths = paths["create"]
if not create_paths:
return []
if not isinstance(create_paths, (list, tuple, set)):
create_paths = [create_paths]
return create_paths
class ILaunchHookPaths(OpenPypeInterface):
"""Module has launch hook paths to return.

View file

@ -921,12 +921,18 @@ class SyncServerModule(OpenPypeModule, ITrayModule):
if self.enabled:
for project in self.connection.projects(projection={"name": 1}):
project_name = project["name"]
project_settings = self.get_sync_project_setting(project_name)
if project_settings and project_settings.get("enabled"):
if self.is_project_enabled(project_name):
enabled_projects.append(project_name)
return enabled_projects
def is_project_enabled(self, project_name):
if self.enabled:
project_settings = self.get_sync_project_setting(project_name)
if project_settings and project_settings.get("enabled"):
return True
return False
def handle_alternate_site(self, collection, representation, processed_site,
file_id, synced_file_id):
"""

View file

@ -12,7 +12,7 @@ import pyblish.api
from pyblish.lib import MessageHandler
import openpype
from openpype.modules import load_modules
from openpype.modules import load_modules, ModulesManager
from openpype.settings import get_project_settings
from openpype.lib import (
Anatomy,
@ -107,7 +107,7 @@ def install_host(host):
install_openpype_plugins()
def install_openpype_plugins(project_name=None):
def install_openpype_plugins(project_name=None, host_name=None):
# Make sure modules are loaded
load_modules()
@ -116,6 +116,18 @@ def install_openpype_plugins(project_name=None):
pyblish.api.register_discovery_filter(filter_pyblish_plugins)
register_loader_plugin_path(LOAD_PATH)
modules_manager = ModulesManager()
publish_plugin_dirs = modules_manager.collect_plugin_paths()["publish"]
for path in publish_plugin_dirs:
pyblish.api.register_plugin_path(path)
if host_name is None:
host_name = os.environ.get("AVALON_APP")
creator_paths = modules_manager.collect_creator_plugin_paths(host_name)
for creator_path in creator_paths:
register_creator_plugin_path(creator_path)
if project_name is None:
project_name = os.environ.get("AVALON_PROJECT")

View file

@ -749,6 +749,10 @@ class CreateContext:
"""Is host valid for creation."""
return self._host_is_valid
@property
def host_name(self):
return os.environ["AVALON_APP"]
@property
def log(self):
"""Dynamic access to logger."""
@ -861,6 +865,17 @@ class CreateContext:
"Using first and skipping following"
))
continue
# Filter by host name
if (
creator_class.host_name
and creator_class.host_name != self.host_name
):
self.log.info((
"Creator's host name is not supported for current host {}"
).format(creator_class.host_name, self.host_name))
continue
creator = creator_class(
self,
system_settings,

View file

@ -63,6 +63,12 @@ class BaseCreator:
# `openpype.pipeline.attribute_definitions`
instance_attr_defs = []
# Filtering by host name - can be used to be filtered by host name
# - used on all hosts when set to 'None' for Backwards compatibility
# - was added afterwards
# QUESTION make this required?
host_name = None
def __init__(
self, create_context, system_settings, project_settings, headless=False
):

View file

@ -62,7 +62,7 @@ class CollectHierarchy(pyblish.api.ContextPlugin):
"frameEnd": instance.data["frameEnd"],
"clipIn": instance.data["clipIn"],
"clipOut": instance.data["clipOut"],
'fps': instance.context.data["fps"],
"fps": instance.data["fps"],
"resolutionWidth": instance.data["resolutionWidth"],
"resolutionHeight": instance.data["resolutionHeight"],
"pixelAspect": instance.data["pixelAspect"]

Some files were not shown because too many files have changed in this diff Show more