mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-25 13:24:54 +01:00
[Automated] Merged develop into main
This commit is contained in:
commit
9b4ad20dfd
12 changed files with 469 additions and 140 deletions
|
|
@ -190,7 +190,7 @@ class CollectNukeWrites(pyblish.api.InstancePlugin,
|
|||
|
||||
# make sure rendered sequence on farm will
|
||||
# be used for extract review
|
||||
if not instance.data["review"]:
|
||||
if not instance.data.get("review"):
|
||||
instance.data["useSequenceForReview"] = False
|
||||
|
||||
self.log.debug("instance.data: {}".format(pformat(instance.data)))
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@ import os
|
|||
|
||||
import unreal
|
||||
|
||||
from openpype.settings import get_project_settings
|
||||
from openpype.pipeline import Anatomy
|
||||
from openpype.hosts.unreal.api import pipeline
|
||||
from openpype.widgets.message_window import Window
|
||||
|
||||
|
||||
queue = None
|
||||
|
|
@ -32,11 +34,20 @@ def start_rendering():
|
|||
"""
|
||||
Start the rendering process.
|
||||
"""
|
||||
print("Starting rendering...")
|
||||
unreal.log("Starting rendering...")
|
||||
|
||||
# Get selected sequences
|
||||
assets = unreal.EditorUtilityLibrary.get_selected_assets()
|
||||
|
||||
if not assets:
|
||||
Window(
|
||||
parent=None,
|
||||
title="No assets selected",
|
||||
message="No assets selected. Select a render instance.",
|
||||
level="warning")
|
||||
raise RuntimeError(
|
||||
"No assets selected. You need to select a render instance.")
|
||||
|
||||
# instances = pipeline.ls_inst()
|
||||
instances = [
|
||||
a for a in assets
|
||||
|
|
@ -66,6 +77,13 @@ def start_rendering():
|
|||
|
||||
ar = unreal.AssetRegistryHelpers.get_asset_registry()
|
||||
|
||||
data = get_project_settings(project)
|
||||
config = None
|
||||
config_path = str(data.get("unreal").get("render_config_path"))
|
||||
if config_path and unreal.EditorAssetLibrary.does_asset_exist(config_path):
|
||||
unreal.log("Found saved render configuration")
|
||||
config = ar.get_asset_by_object_path(config_path).get_asset()
|
||||
|
||||
for i in inst_data:
|
||||
sequence = ar.get_asset_by_object_path(i["sequence"]).get_asset()
|
||||
|
||||
|
|
@ -81,55 +99,80 @@ def start_rendering():
|
|||
# Get all the sequences to render. If there are subsequences,
|
||||
# add them and their frame ranges to the render list. We also
|
||||
# use the names for the output paths.
|
||||
for s in sequences:
|
||||
subscenes = pipeline.get_subsequences(s.get('sequence'))
|
||||
for seq in sequences:
|
||||
subscenes = pipeline.get_subsequences(seq.get('sequence'))
|
||||
|
||||
if subscenes:
|
||||
for ss in subscenes:
|
||||
for sub_seq in subscenes:
|
||||
sequences.append({
|
||||
"sequence": ss.get_sequence(),
|
||||
"output": (f"{s.get('output')}/"
|
||||
f"{ss.get_sequence().get_name()}"),
|
||||
"sequence": sub_seq.get_sequence(),
|
||||
"output": (f"{seq.get('output')}/"
|
||||
f"{sub_seq.get_sequence().get_name()}"),
|
||||
"frame_range": (
|
||||
ss.get_start_frame(), ss.get_end_frame())
|
||||
sub_seq.get_start_frame(), sub_seq.get_end_frame())
|
||||
})
|
||||
else:
|
||||
# Avoid rendering camera sequences
|
||||
if "_camera" not in s.get('sequence').get_name():
|
||||
render_list.append(s)
|
||||
if "_camera" not in seq.get('sequence').get_name():
|
||||
render_list.append(seq)
|
||||
|
||||
# Create the rendering jobs and add them to the queue.
|
||||
for r in render_list:
|
||||
for render_setting in render_list:
|
||||
job = queue.allocate_new_job(unreal.MoviePipelineExecutorJob)
|
||||
job.sequence = unreal.SoftObjectPath(i["master_sequence"])
|
||||
job.map = unreal.SoftObjectPath(i["master_level"])
|
||||
job.author = "OpenPype"
|
||||
|
||||
# If we have a saved configuration, copy it to the job.
|
||||
if config:
|
||||
job.get_configuration().copy_from(config)
|
||||
|
||||
# User data could be used to pass data to the job, that can be
|
||||
# read in the job's OnJobFinished callback. We could,
|
||||
# for instance, pass the AvalonPublishInstance's path to the job.
|
||||
# job.user_data = ""
|
||||
|
||||
output_dir = render_setting.get('output')
|
||||
shot_name = render_setting.get('sequence').get_name()
|
||||
|
||||
settings = job.get_configuration().find_or_add_setting_by_class(
|
||||
unreal.MoviePipelineOutputSetting)
|
||||
settings.output_resolution = unreal.IntPoint(1920, 1080)
|
||||
settings.custom_start_frame = r.get("frame_range")[0]
|
||||
settings.custom_end_frame = r.get("frame_range")[1]
|
||||
settings.custom_start_frame = render_setting.get("frame_range")[0]
|
||||
settings.custom_end_frame = render_setting.get("frame_range")[1]
|
||||
settings.use_custom_playback_range = True
|
||||
settings.file_name_format = "{sequence_name}.{frame_number}"
|
||||
settings.output_directory.path = f"{render_dir}/{r.get('output')}"
|
||||
|
||||
renderPass = job.get_configuration().find_or_add_setting_by_class(
|
||||
unreal.MoviePipelineDeferredPassBase)
|
||||
renderPass.disable_multisample_effects = True
|
||||
settings.file_name_format = f"{shot_name}" + ".{frame_number}"
|
||||
settings.output_directory.path = f"{render_dir}/{output_dir}"
|
||||
|
||||
job.get_configuration().find_or_add_setting_by_class(
|
||||
unreal.MoviePipelineImageSequenceOutput_PNG)
|
||||
unreal.MoviePipelineDeferredPassBase)
|
||||
|
||||
render_format = data.get("unreal").get("render_format", "png")
|
||||
|
||||
if render_format == "png":
|
||||
job.get_configuration().find_or_add_setting_by_class(
|
||||
unreal.MoviePipelineImageSequenceOutput_PNG)
|
||||
elif render_format == "exr":
|
||||
job.get_configuration().find_or_add_setting_by_class(
|
||||
unreal.MoviePipelineImageSequenceOutput_EXR)
|
||||
elif render_format == "jpg":
|
||||
job.get_configuration().find_or_add_setting_by_class(
|
||||
unreal.MoviePipelineImageSequenceOutput_JPG)
|
||||
elif render_format == "bmp":
|
||||
job.get_configuration().find_or_add_setting_by_class(
|
||||
unreal.MoviePipelineImageSequenceOutput_BMP)
|
||||
|
||||
# If there are jobs in the queue, start the rendering process.
|
||||
if queue.get_jobs():
|
||||
global executor
|
||||
executor = unreal.MoviePipelinePIEExecutor()
|
||||
|
||||
preroll_frames = data.get("unreal").get("preroll_frames", 0)
|
||||
|
||||
settings = unreal.MoviePipelinePIEExecutorSettings()
|
||||
settings.set_editor_property(
|
||||
"initial_delay_frame_count", preroll_frames)
|
||||
|
||||
executor.on_executor_finished_delegate.add_callable_unique(
|
||||
_queue_finish_callback)
|
||||
executor.on_individual_job_finished_delegate.add_callable_unique(
|
||||
|
|
|
|||
|
|
@ -1,14 +1,22 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from pathlib import Path
|
||||
|
||||
import unreal
|
||||
|
||||
from openpype.pipeline import CreatorError
|
||||
from openpype.hosts.unreal.api.pipeline import (
|
||||
get_subsequences
|
||||
UNREAL_VERSION,
|
||||
create_folder,
|
||||
get_subsequences,
|
||||
)
|
||||
from openpype.hosts.unreal.api.plugin import (
|
||||
UnrealAssetCreator
|
||||
)
|
||||
from openpype.lib import UILabelDef
|
||||
from openpype.lib import (
|
||||
UILabelDef,
|
||||
UISeparatorDef,
|
||||
BoolDef,
|
||||
NumberDef
|
||||
)
|
||||
|
||||
|
||||
class CreateRender(UnrealAssetCreator):
|
||||
|
|
@ -19,7 +27,92 @@ class CreateRender(UnrealAssetCreator):
|
|||
family = "render"
|
||||
icon = "eye"
|
||||
|
||||
def create(self, subset_name, instance_data, pre_create_data):
|
||||
def create_instance(
|
||||
self, instance_data, subset_name, pre_create_data,
|
||||
selected_asset_path, master_seq, master_lvl, seq_data
|
||||
):
|
||||
instance_data["members"] = [selected_asset_path]
|
||||
instance_data["sequence"] = selected_asset_path
|
||||
instance_data["master_sequence"] = master_seq
|
||||
instance_data["master_level"] = master_lvl
|
||||
instance_data["output"] = seq_data.get('output')
|
||||
instance_data["frameStart"] = seq_data.get('frame_range')[0]
|
||||
instance_data["frameEnd"] = seq_data.get('frame_range')[1]
|
||||
|
||||
super(CreateRender, self).create(
|
||||
subset_name,
|
||||
instance_data,
|
||||
pre_create_data)
|
||||
|
||||
def create_with_new_sequence(
|
||||
self, subset_name, instance_data, pre_create_data
|
||||
):
|
||||
# If the option to create a new level sequence is selected,
|
||||
# create a new level sequence and a master level.
|
||||
|
||||
root = f"/Game/OpenPype/Sequences"
|
||||
|
||||
# Create a new folder for the sequence in root
|
||||
sequence_dir_name = create_folder(root, subset_name)
|
||||
sequence_dir = f"{root}/{sequence_dir_name}"
|
||||
|
||||
unreal.log_warning(f"sequence_dir: {sequence_dir}")
|
||||
|
||||
# Create the level sequence
|
||||
asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
|
||||
seq = asset_tools.create_asset(
|
||||
asset_name=subset_name,
|
||||
package_path=sequence_dir,
|
||||
asset_class=unreal.LevelSequence,
|
||||
factory=unreal.LevelSequenceFactoryNew())
|
||||
|
||||
seq.set_playback_start(pre_create_data.get("start_frame"))
|
||||
seq.set_playback_end(pre_create_data.get("end_frame"))
|
||||
|
||||
pre_create_data["members"] = [seq.get_path_name()]
|
||||
|
||||
unreal.EditorAssetLibrary.save_asset(seq.get_path_name())
|
||||
|
||||
# Create the master level
|
||||
if UNREAL_VERSION.major >= 5:
|
||||
curr_level = unreal.LevelEditorSubsystem().get_current_level()
|
||||
else:
|
||||
world = unreal.EditorLevelLibrary.get_editor_world()
|
||||
levels = unreal.EditorLevelUtils.get_levels(world)
|
||||
curr_level = levels[0] if len(levels) else None
|
||||
if not curr_level:
|
||||
raise RuntimeError("No level loaded.")
|
||||
curr_level_path = curr_level.get_outer().get_path_name()
|
||||
|
||||
# If the level path does not start with "/Game/", the current
|
||||
# level is a temporary, unsaved level.
|
||||
if curr_level_path.startswith("/Game/"):
|
||||
if UNREAL_VERSION.major >= 5:
|
||||
unreal.LevelEditorSubsystem().save_current_level()
|
||||
else:
|
||||
unreal.EditorLevelLibrary.save_current_level()
|
||||
|
||||
ml_path = f"{sequence_dir}/{subset_name}_MasterLevel"
|
||||
|
||||
if UNREAL_VERSION.major >= 5:
|
||||
unreal.LevelEditorSubsystem().new_level(ml_path)
|
||||
else:
|
||||
unreal.EditorLevelLibrary.new_level(ml_path)
|
||||
|
||||
seq_data = {
|
||||
"sequence": seq,
|
||||
"output": f"{seq.get_name()}",
|
||||
"frame_range": (
|
||||
seq.get_playback_start(),
|
||||
seq.get_playback_end())}
|
||||
|
||||
self.create_instance(
|
||||
instance_data, subset_name, pre_create_data,
|
||||
seq.get_path_name(), seq.get_path_name(), ml_path, seq_data)
|
||||
|
||||
def create_from_existing_sequence(
|
||||
self, subset_name, instance_data, pre_create_data
|
||||
):
|
||||
ar = unreal.AssetRegistryHelpers.get_asset_registry()
|
||||
|
||||
sel_objects = unreal.EditorUtilityLibrary.get_selected_assets()
|
||||
|
|
@ -27,8 +120,8 @@ class CreateRender(UnrealAssetCreator):
|
|||
a.get_path_name() for a in sel_objects
|
||||
if a.get_class().get_name() == "LevelSequence"]
|
||||
|
||||
if not selection:
|
||||
raise CreatorError("Please select at least one Level Sequence.")
|
||||
if len(selection) == 0:
|
||||
raise RuntimeError("Please select at least one Level Sequence.")
|
||||
|
||||
seq_data = None
|
||||
|
||||
|
|
@ -42,28 +135,38 @@ class CreateRender(UnrealAssetCreator):
|
|||
f"Skipping {selected_asset.get_name()}. It isn't a Level "
|
||||
"Sequence.")
|
||||
|
||||
# The asset name is the third element of the path which
|
||||
# contains the map.
|
||||
# To take the asset name, we remove from the path the prefix
|
||||
# "/Game/OpenPype/" and then we split the path by "/".
|
||||
sel_path = selected_asset_path
|
||||
asset_name = sel_path.replace("/Game/OpenPype/", "").split("/")[0]
|
||||
if pre_create_data.get("use_hierarchy"):
|
||||
# The asset name is the the third element of the path which
|
||||
# contains the map.
|
||||
# To take the asset name, we remove from the path the prefix
|
||||
# "/Game/OpenPype/" and then we split the path by "/".
|
||||
sel_path = selected_asset_path
|
||||
asset_name = sel_path.replace(
|
||||
"/Game/OpenPype/", "").split("/")[0]
|
||||
|
||||
search_path = f"/Game/OpenPype/{asset_name}"
|
||||
else:
|
||||
search_path = Path(selected_asset_path).parent.as_posix()
|
||||
|
||||
# Get the master sequence and the master level.
|
||||
# There should be only one sequence and one level in the directory.
|
||||
ar_filter = unreal.ARFilter(
|
||||
class_names=["LevelSequence"],
|
||||
package_paths=[f"/Game/OpenPype/{asset_name}"],
|
||||
recursive_paths=False)
|
||||
sequences = ar.get_assets(ar_filter)
|
||||
master_seq = sequences[0].get_asset().get_path_name()
|
||||
master_seq_obj = sequences[0].get_asset()
|
||||
ar_filter = unreal.ARFilter(
|
||||
class_names=["World"],
|
||||
package_paths=[f"/Game/OpenPype/{asset_name}"],
|
||||
recursive_paths=False)
|
||||
levels = ar.get_assets(ar_filter)
|
||||
master_lvl = levels[0].get_asset().get_path_name()
|
||||
try:
|
||||
ar_filter = unreal.ARFilter(
|
||||
class_names=["LevelSequence"],
|
||||
package_paths=[search_path],
|
||||
recursive_paths=False)
|
||||
sequences = ar.get_assets(ar_filter)
|
||||
master_seq = sequences[0].get_asset().get_path_name()
|
||||
master_seq_obj = sequences[0].get_asset()
|
||||
ar_filter = unreal.ARFilter(
|
||||
class_names=["World"],
|
||||
package_paths=[search_path],
|
||||
recursive_paths=False)
|
||||
levels = ar.get_assets(ar_filter)
|
||||
master_lvl = levels[0].get_asset().get_path_name()
|
||||
except IndexError:
|
||||
raise RuntimeError(
|
||||
f"Could not find the hierarchy for the selected sequence.")
|
||||
|
||||
# If the selected asset is the master sequence, we get its data
|
||||
# and then we create the instance for the master sequence.
|
||||
|
|
@ -79,7 +182,8 @@ class CreateRender(UnrealAssetCreator):
|
|||
master_seq_obj.get_playback_start(),
|
||||
master_seq_obj.get_playback_end())}
|
||||
|
||||
if selected_asset_path == master_seq:
|
||||
if (selected_asset_path == master_seq or
|
||||
pre_create_data.get("use_hierarchy")):
|
||||
seq_data = master_seq_data
|
||||
else:
|
||||
seq_data_list = [master_seq_data]
|
||||
|
|
@ -119,20 +223,54 @@ class CreateRender(UnrealAssetCreator):
|
|||
"sub-sequence of the master sequence.")
|
||||
continue
|
||||
|
||||
instance_data["members"] = [selected_asset_path]
|
||||
instance_data["sequence"] = selected_asset_path
|
||||
instance_data["master_sequence"] = master_seq
|
||||
instance_data["master_level"] = master_lvl
|
||||
instance_data["output"] = seq_data.get('output')
|
||||
instance_data["frameStart"] = seq_data.get('frame_range')[0]
|
||||
instance_data["frameEnd"] = seq_data.get('frame_range')[1]
|
||||
self.create_instance(
|
||||
instance_data, subset_name, pre_create_data,
|
||||
selected_asset_path, master_seq, master_lvl, seq_data)
|
||||
|
||||
super(CreateRender, self).create(
|
||||
subset_name,
|
||||
instance_data,
|
||||
pre_create_data)
|
||||
def create(self, subset_name, instance_data, pre_create_data):
|
||||
if pre_create_data.get("create_seq"):
|
||||
self.create_with_new_sequence(
|
||||
subset_name, instance_data, pre_create_data)
|
||||
else:
|
||||
self.create_from_existing_sequence(
|
||||
subset_name, instance_data, pre_create_data)
|
||||
|
||||
def get_pre_create_attr_defs(self):
|
||||
return [
|
||||
UILabelDef("Select the sequence to render.")
|
||||
UILabelDef(
|
||||
"Select a Level Sequence to render or create a new one."
|
||||
),
|
||||
BoolDef(
|
||||
"create_seq",
|
||||
label="Create a new Level Sequence",
|
||||
default=False
|
||||
),
|
||||
UILabelDef(
|
||||
"WARNING: If you create a new Level Sequence, the current\n"
|
||||
"level will be saved and a new Master Level will be created."
|
||||
),
|
||||
NumberDef(
|
||||
"start_frame",
|
||||
label="Start Frame",
|
||||
default=0,
|
||||
minimum=-999999,
|
||||
maximum=999999
|
||||
),
|
||||
NumberDef(
|
||||
"end_frame",
|
||||
label="Start Frame",
|
||||
default=150,
|
||||
minimum=-999999,
|
||||
maximum=999999
|
||||
),
|
||||
UISeparatorDef(),
|
||||
UILabelDef(
|
||||
"The following settings are valid only if you are not\n"
|
||||
"creating a new sequence."
|
||||
),
|
||||
BoolDef(
|
||||
"use_hierarchy",
|
||||
label="Use Hierarchy",
|
||||
default=False
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
import clique
|
||||
|
||||
import pyblish.api
|
||||
|
||||
|
||||
class ValidateSequenceFrames(pyblish.api.InstancePlugin):
|
||||
"""Ensure the sequence of frames is complete
|
||||
|
||||
The files found in the folder are checked against the frameStart and
|
||||
frameEnd of the instance. If the first or last file is not
|
||||
corresponding with the first or last frame it is flagged as invalid.
|
||||
"""
|
||||
|
||||
order = pyblish.api.ValidatorOrder
|
||||
label = "Validate Sequence Frames"
|
||||
families = ["render"]
|
||||
hosts = ["unreal"]
|
||||
optional = True
|
||||
|
||||
def process(self, instance):
|
||||
representations = instance.data.get("representations")
|
||||
for repr in representations:
|
||||
data = instance.data.get("assetEntity", {}).get("data", {})
|
||||
patterns = [clique.PATTERNS["frames"]]
|
||||
collections, remainder = clique.assemble(
|
||||
repr["files"], minimum_items=1, patterns=patterns)
|
||||
|
||||
assert not remainder, "Must not have remainder"
|
||||
assert len(collections) == 1, "Must detect single collection"
|
||||
collection = collections[0]
|
||||
frames = list(collection.indexes)
|
||||
|
||||
current_range = (frames[0], frames[-1])
|
||||
required_range = (data["frameStart"],
|
||||
data["frameEnd"])
|
||||
|
||||
if current_range != required_range:
|
||||
raise ValueError(f"Invalid frame range: {current_range} - "
|
||||
f"expected: {required_range}")
|
||||
|
||||
missing = collection.holes().indexes
|
||||
assert not missing, "Missing frames: %s" % (missing,)
|
||||
|
|
@ -49,7 +49,12 @@ class ValidateSequenceFrames(pyblish.api.InstancePlugin):
|
|||
collection = collections[0]
|
||||
frames = list(collection.indexes)
|
||||
|
||||
if instance.data.get("slate"):
|
||||
# Slate is not part of the frame range
|
||||
frames = frames[1:]
|
||||
|
||||
current_range = (frames[0], frames[-1])
|
||||
|
||||
required_range = (instance.data["frameStart"],
|
||||
instance.data["frameEnd"])
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@
|
|||
},
|
||||
"level_sequences_for_layouts": false,
|
||||
"delete_unmatched_assets": false,
|
||||
"render_config_path": "",
|
||||
"preroll_frames": 0,
|
||||
"render_format": "png",
|
||||
"project_setup": {
|
||||
"dev_mode": true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,28 @@
|
|||
"key": "delete_unmatched_assets",
|
||||
"label": "Delete assets that are not matched"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"key": "render_config_path",
|
||||
"label": "Render Config Path"
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"key": "preroll_frames",
|
||||
"label": "Pre-roll frames"
|
||||
},
|
||||
{
|
||||
"key": "render_format",
|
||||
"label": "Render format",
|
||||
"type": "enum",
|
||||
"multiselection": false,
|
||||
"enum_items": [
|
||||
{"png": "PNG"},
|
||||
{"exr": "EXR"},
|
||||
{"jpg": "JPG"},
|
||||
{"bmp": "BMP"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
|
|
|
|||
|
|
@ -282,6 +282,9 @@ class CreateWidget(QtWidgets.QWidget):
|
|||
thumbnail_widget.thumbnail_created.connect(self._on_thumbnail_create)
|
||||
thumbnail_widget.thumbnail_cleared.connect(self._on_thumbnail_clear)
|
||||
|
||||
controller.event_system.add_callback(
|
||||
"main.window.closed", self._on_main_window_close
|
||||
)
|
||||
controller.event_system.add_callback(
|
||||
"plugins.refresh.finished", self._on_plugins_refresh
|
||||
)
|
||||
|
|
@ -316,6 +319,10 @@ class CreateWidget(QtWidgets.QWidget):
|
|||
self._first_show = True
|
||||
self._last_thumbnail_path = None
|
||||
|
||||
self._last_current_context_asset = None
|
||||
self._last_current_context_task = None
|
||||
self._use_current_context = True
|
||||
|
||||
@property
|
||||
def current_asset_name(self):
|
||||
return self._controller.current_asset_name
|
||||
|
|
@ -356,12 +363,39 @@ class CreateWidget(QtWidgets.QWidget):
|
|||
if check_prereq:
|
||||
self._invalidate_prereq()
|
||||
|
||||
def _on_main_window_close(self):
|
||||
"""Publisher window was closed."""
|
||||
|
||||
# Use current context on next refresh
|
||||
self._use_current_context = True
|
||||
|
||||
def refresh(self):
|
||||
current_asset_name = self._controller.current_asset_name
|
||||
current_task_name = self._controller.current_task_name
|
||||
|
||||
# Get context before refresh to keep selection of asset and
|
||||
# task widgets
|
||||
asset_name = self._get_asset_name()
|
||||
task_name = self._get_task_name()
|
||||
|
||||
# Replace by current context if last loaded context was
|
||||
# 'current context' before reset
|
||||
if (
|
||||
self._use_current_context
|
||||
or (
|
||||
self._last_current_context_asset
|
||||
and asset_name == self._last_current_context_asset
|
||||
and task_name == self._last_current_context_task
|
||||
)
|
||||
):
|
||||
asset_name = current_asset_name
|
||||
task_name = current_task_name
|
||||
|
||||
# Store values for future refresh
|
||||
self._last_current_context_asset = current_asset_name
|
||||
self._last_current_context_task = current_task_name
|
||||
self._use_current_context = False
|
||||
|
||||
self._prereq_available = False
|
||||
|
||||
# Disable context widget so refresh of asset will use context asset
|
||||
|
|
@ -398,7 +432,10 @@ class CreateWidget(QtWidgets.QWidget):
|
|||
prereq_available = False
|
||||
creator_btn_tooltips.append("Creator is not selected")
|
||||
|
||||
if self._context_change_is_enabled() and self._asset_name is None:
|
||||
if (
|
||||
self._context_change_is_enabled()
|
||||
and self._get_asset_name() is None
|
||||
):
|
||||
# QUESTION how to handle invalid asset?
|
||||
prereq_available = False
|
||||
creator_btn_tooltips.append("Context is not selected")
|
||||
|
|
|
|||
|
|
@ -406,6 +406,9 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
self._comment_input.setText("") # clear comment
|
||||
self._reset_on_show = True
|
||||
self._controller.clear_thumbnail_temp_dir_path()
|
||||
# Trigger custom event that should be captured only in UI
|
||||
# - backend (controller) must not be dependent on this event topic!!!
|
||||
self._controller.event_system.emit("main.window.closed", {}, "window")
|
||||
super(PublisherWindow, self).closeEvent(event)
|
||||
|
||||
def leaveEvent(self, event):
|
||||
|
|
|
|||
|
|
@ -199,90 +199,103 @@ class InventoryModel(TreeModel):
|
|||
"""Refresh the model"""
|
||||
|
||||
host = registered_host()
|
||||
if not items: # for debugging or testing, injecting items from outside
|
||||
# for debugging or testing, injecting items from outside
|
||||
if items is None:
|
||||
if isinstance(host, ILoadHost):
|
||||
items = host.get_containers()
|
||||
else:
|
||||
elif hasattr(host, "ls"):
|
||||
items = host.ls()
|
||||
else:
|
||||
items = []
|
||||
|
||||
self.clear()
|
||||
|
||||
if self._hierarchy_view and selected:
|
||||
if not hasattr(host.pipeline, "update_hierarchy"):
|
||||
# If host doesn't support hierarchical containers, then
|
||||
# cherry-pick only.
|
||||
self.add_items((item for item in items
|
||||
if item["objectName"] in selected))
|
||||
return
|
||||
|
||||
# Update hierarchy info for all containers
|
||||
items_by_name = {item["objectName"]: item
|
||||
for item in host.pipeline.update_hierarchy(items)}
|
||||
|
||||
selected_items = set()
|
||||
|
||||
def walk_children(names):
|
||||
"""Select containers and extend to chlid containers"""
|
||||
for name in [n for n in names if n not in selected_items]:
|
||||
selected_items.add(name)
|
||||
item = items_by_name[name]
|
||||
yield item
|
||||
|
||||
for child in walk_children(item["children"]):
|
||||
yield child
|
||||
|
||||
items = list(walk_children(selected)) # Cherry-picked and extended
|
||||
|
||||
# Cut unselected upstream containers
|
||||
for item in items:
|
||||
if not item.get("parent") in selected_items:
|
||||
# Parent not in selection, this is root item.
|
||||
item["parent"] = None
|
||||
|
||||
parents = [self._root_item]
|
||||
|
||||
# The length of `items` array is the maximum depth that a
|
||||
# hierarchy could be.
|
||||
# Take this as an easiest way to prevent looping forever.
|
||||
maximum_loop = len(items)
|
||||
count = 0
|
||||
while items:
|
||||
if count > maximum_loop:
|
||||
self.log.warning("Maximum loop count reached, possible "
|
||||
"missing parent node.")
|
||||
break
|
||||
|
||||
_parents = list()
|
||||
for parent in parents:
|
||||
_unparented = list()
|
||||
|
||||
def _children():
|
||||
"""Child item provider"""
|
||||
for item in items:
|
||||
if item.get("parent") == parent.get("objectName"):
|
||||
# (NOTE)
|
||||
# Since `self._root_node` has no "objectName"
|
||||
# entry, it will be paired with root item if
|
||||
# the value of key "parent" is None, or not
|
||||
# having the key.
|
||||
yield item
|
||||
else:
|
||||
# Not current parent's child, try next
|
||||
_unparented.append(item)
|
||||
|
||||
self.add_items(_children(), parent)
|
||||
|
||||
items[:] = _unparented
|
||||
|
||||
# Parents of next level
|
||||
for group_node in parent.children():
|
||||
_parents += group_node.children()
|
||||
|
||||
parents[:] = _parents
|
||||
count += 1
|
||||
|
||||
else:
|
||||
if not selected or not self._hierarchy_view:
|
||||
self.add_items(items)
|
||||
return
|
||||
|
||||
if (
|
||||
not hasattr(host, "pipeline")
|
||||
or not hasattr(host.pipeline, "update_hierarchy")
|
||||
):
|
||||
# If host doesn't support hierarchical containers, then
|
||||
# cherry-pick only.
|
||||
self.add_items((
|
||||
item
|
||||
for item in items
|
||||
if item["objectName"] in selected
|
||||
))
|
||||
return
|
||||
|
||||
# TODO find out what this part does. Function 'update_hierarchy' is
|
||||
# available only in 'blender' at this moment.
|
||||
|
||||
# Update hierarchy info for all containers
|
||||
items_by_name = {
|
||||
item["objectName"]: item
|
||||
for item in host.pipeline.update_hierarchy(items)
|
||||
}
|
||||
|
||||
selected_items = set()
|
||||
|
||||
def walk_children(names):
|
||||
"""Select containers and extend to chlid containers"""
|
||||
for name in [n for n in names if n not in selected_items]:
|
||||
selected_items.add(name)
|
||||
item = items_by_name[name]
|
||||
yield item
|
||||
|
||||
for child in walk_children(item["children"]):
|
||||
yield child
|
||||
|
||||
items = list(walk_children(selected)) # Cherry-picked and extended
|
||||
|
||||
# Cut unselected upstream containers
|
||||
for item in items:
|
||||
if not item.get("parent") in selected_items:
|
||||
# Parent not in selection, this is root item.
|
||||
item["parent"] = None
|
||||
|
||||
parents = [self._root_item]
|
||||
|
||||
# The length of `items` array is the maximum depth that a
|
||||
# hierarchy could be.
|
||||
# Take this as an easiest way to prevent looping forever.
|
||||
maximum_loop = len(items)
|
||||
count = 0
|
||||
while items:
|
||||
if count > maximum_loop:
|
||||
self.log.warning("Maximum loop count reached, possible "
|
||||
"missing parent node.")
|
||||
break
|
||||
|
||||
_parents = list()
|
||||
for parent in parents:
|
||||
_unparented = list()
|
||||
|
||||
def _children():
|
||||
"""Child item provider"""
|
||||
for item in items:
|
||||
if item.get("parent") == parent.get("objectName"):
|
||||
# (NOTE)
|
||||
# Since `self._root_node` has no "objectName"
|
||||
# entry, it will be paired with root item if
|
||||
# the value of key "parent" is None, or not
|
||||
# having the key.
|
||||
yield item
|
||||
else:
|
||||
# Not current parent's child, try next
|
||||
_unparented.append(item)
|
||||
|
||||
self.add_items(_children(), parent)
|
||||
|
||||
items[:] = _unparented
|
||||
|
||||
# Parents of next level
|
||||
for group_node in parent.children():
|
||||
_parents += group_node.children()
|
||||
|
||||
parents[:] = _parents
|
||||
count += 1
|
||||
|
||||
def add_items(self, items, parent=None):
|
||||
"""Add the items to the model.
|
||||
|
|
|
|||
|
|
@ -107,8 +107,8 @@ class SceneInventoryWindow(QtWidgets.QDialog):
|
|||
view.hierarchy_view_changed.connect(
|
||||
self._on_hierarchy_view_change
|
||||
)
|
||||
view.data_changed.connect(self.refresh)
|
||||
refresh_button.clicked.connect(self.refresh)
|
||||
view.data_changed.connect(self._on_refresh_request)
|
||||
refresh_button.clicked.connect(self._on_refresh_request)
|
||||
update_all_button.clicked.connect(self._on_update_all)
|
||||
|
||||
self._update_all_button = update_all_button
|
||||
|
|
@ -139,6 +139,11 @@ class SceneInventoryWindow(QtWidgets.QDialog):
|
|||
|
||||
"""
|
||||
|
||||
def _on_refresh_request(self):
|
||||
"""Signal callback to trigger 'refresh' without any arguments."""
|
||||
|
||||
self.refresh()
|
||||
|
||||
def refresh(self, items=None):
|
||||
with preserve_expanded_rows(
|
||||
tree_view=self._view,
|
||||
|
|
|
|||
|
|
@ -180,5 +180,23 @@ class TestValidateSequenceFrames(BaseTest):
|
|||
plugin.process(instance)
|
||||
assert ("Missing frames: [1002]" in str(excinfo.value))
|
||||
|
||||
def test_validate_sequence_frames_slate(self, instance, plugin):
|
||||
representations = [
|
||||
{
|
||||
"ext": "exr",
|
||||
"files": [
|
||||
"Main_beauty.1000.exr",
|
||||
"Main_beauty.1001.exr",
|
||||
"Main_beauty.1002.exr",
|
||||
"Main_beauty.1003.exr"
|
||||
]
|
||||
}
|
||||
]
|
||||
instance.data["slate"] = True
|
||||
instance.data["representations"] = representations
|
||||
instance.data["frameEnd"] = 1003
|
||||
|
||||
plugin.process(instance)
|
||||
|
||||
|
||||
test_case = TestValidateSequenceFrames()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue