From 10fa0ee5c463de5676a5e209e447c2a845f6b3f6 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 15 Dec 2021 16:34:21 +0000 Subject: [PATCH 001/109] Implemented creator for render --- .../unreal/plugins/create/create_render.py | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 openpype/hosts/unreal/plugins/create/create_render.py diff --git a/openpype/hosts/unreal/plugins/create/create_render.py b/openpype/hosts/unreal/plugins/create/create_render.py new file mode 100644 index 0000000000..a0bf320225 --- /dev/null +++ b/openpype/hosts/unreal/plugins/create/create_render.py @@ -0,0 +1,52 @@ +import unreal +from openpype.hosts.unreal.api.plugin import Creator +from avalon.unreal import pipeline + + +class CreateRender(Creator): + """Create instance for sequence for rendering""" + + name = "unrealRender" + label = "Unreal - Render" + family = "render" + icon = "cube" + asset_types = ["LevelSequence"] + + root = "/Game/AvalonInstances" + suffix = "_INS" + + def __init__(self, *args, **kwargs): + super(CreateRender, self).__init__(*args, **kwargs) + + def process(self): + name = self.data["subset"] + + print(self.data) + + selection = [] + if (self.options or {}).get("useSelection"): + sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() + selection = [ + a.get_path_name() for a in sel_objects + if a.get_class().get_name() in self.asset_types] + + unreal.log("selection: {}".format(selection)) + # instantiate(self.root, name, self.data, selection, self.suffix) + # container_name = "{}{}".format(name, self.suffix) + + # if we specify assets, create new folder and move them there. If not, + # just create empty folder + # new_name = pipeline.create_folder(self.root, name) + path = "{}/{}".format(self.root, name) + unreal.EditorAssetLibrary.make_directory(path) + + ar = unreal.AssetRegistryHelpers.get_asset_registry() + + for a in selection: + d = self.data.copy() + d["sequence"] = a + asset = ar.get_asset_by_object_path(a).get_asset() + container_name = asset.get_name() + pipeline.create_publish_instance(instance=container_name, path=path) + pipeline.imprint("{}/{}".format(path, container_name), d) + From 4ff7cf67ab7fe852216c7f408161ca0f9d0d5ecf Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 31 Jan 2022 11:20:13 +0000 Subject: [PATCH 002/109] Loading layouts and cameras now create level sequences for hierarchy --- .../hosts/unreal/plugins/load/load_camera.py | 143 ++++++++++++++--- .../hosts/unreal/plugins/load/load_layout.py | 149 ++++++++++++++++-- 2 files changed, 256 insertions(+), 36 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index b2b25eec73..00d17407f9 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -15,6 +15,20 @@ class CameraLoader(api.Loader): icon = "cube" color = "orange" + def _add_sub_sequence(self, master, sub): + track = master.add_master_track(unreal.MovieSceneCinematicShotTrack) + section = track.add_section() + section.set_editor_property('sub_sequence', sub) + return section + + def _get_data(self, asset_name): + asset_doc = io.find_one({ + "type": "asset", + "name": asset_name + }) + + return asset_doc.get("data") + def load(self, context, name, namespace, data): """ Load and containerise representation into Content Browser. @@ -39,7 +53,13 @@ class CameraLoader(api.Loader): """ # Create directory for asset and avalon container - root = "/Game/Avalon/Assets" + hierarchy = context.get('asset').get('data').get('parents') + root = "/Game/Avalon" + hierarchy_dir = root + hierarchy_list = [] + for h in hierarchy: + hierarchy_dir = f"{hierarchy_dir}/{h}" + hierarchy_list.append(hierarchy_dir) asset = context.get('asset').get('name') suffix = "_CON" if asset: @@ -49,9 +69,9 @@ class CameraLoader(api.Loader): tools = unreal.AssetToolsHelpers().get_asset_tools() + # Create a unique name for the camera directory unique_number = 1 - - if unreal.EditorAssetLibrary.does_directory_exist(f"{root}/{asset}"): + if unreal.EditorAssetLibrary.does_directory_exist(f"{hierarchy_dir}/{asset}"): asset_content = unreal.EditorAssetLibrary.list_assets( f"{root}/{asset}", recursive=False, include_folder=True ) @@ -71,42 +91,121 @@ class CameraLoader(api.Loader): unique_number = f_numbers[-1] + 1 asset_dir, container_name = tools.create_unique_asset_name( - f"{root}/{asset}/{name}_{unique_number:02d}", suffix="") + f"{hierarchy_dir}/{asset}/{name}_{unique_number:02d}", suffix="") container_name += suffix + # sequence = None + + # ar = unreal.AssetRegistryHelpers.get_asset_registry() + + # if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): + # unreal.EditorAssetLibrary.make_directory(asset_dir) + + # sequence = tools.create_asset( + # asset_name=asset_name, + # package_path=asset_dir, + # asset_class=unreal.LevelSequence, + # factory=unreal.LevelSequenceFactoryNew() + # ) + # else: + # asset_content = unreal.EditorAssetLibrary.list_assets( + # asset_dir, recursive=False) + # for a in asset_content: + # obj = ar.get_asset_by_object_path(a) + # if obj.get_asset().get_class().get_name() == 'LevelSequence': + # sequence = obj.get_asset() + # break + + # assert sequence, "Sequence not found" + + # Get all the sequences in the hierarchy. It will create them, if + # they don't exist. + sequences = [] + i = 0 + for h in hierarchy_list: + root_content = unreal.EditorAssetLibrary.list_assets( + h, recursive=False, include_folder=False) + + existing_sequences = [ + unreal.EditorAssetLibrary.find_asset_data(asset) + for asset in root_content + if unreal.EditorAssetLibrary.find_asset_data( + asset).get_class().get_name() == 'LevelSequence' + ] + + # for asset in root_content: + # asset_data = EditorAssetLibrary.find_asset_data(asset) + # # imported_asset = unreal.AssetRegistryHelpers.get_asset( + # # imported_asset_data) + # if asset_data.get_class().get_name() == 'LevelSequence': + # break + + if not existing_sequences: + scene = tools.create_asset( + asset_name=hierarchy[i], + package_path=h, + asset_class=unreal.LevelSequence, + factory=unreal.LevelSequenceFactoryNew() + ) + sequences.append(scene) + else: + for e in existing_sequences: + sequences.append(e.get_asset()) + + i += 1 + unreal.EditorAssetLibrary.make_directory(asset_dir) - sequence = tools.create_asset( - asset_name=asset_name, + cam_seq = tools.create_asset( + asset_name=asset, package_path=asset_dir, asset_class=unreal.LevelSequence, factory=unreal.LevelSequenceFactoryNew() ) - io_asset = io.Session["AVALON_ASSET"] - asset_doc = io.find_one({ - "type": "asset", - "name": io_asset - }) + sequences.append(cam_seq) - data = asset_doc.get("data") + # Add sequences data to hierarchy + data_i = self._get_data(sequences[0].get_name()) + + for i in range(0, len(sequences) - 1): + section = self._add_sub_sequence(sequences[i], sequences[i + 1]) + + print(sequences[i]) + print(sequences[i + 1]) + + data_j = self._get_data(sequences[i + 1].get_name()) + + if data_i: + sequences[i].set_display_rate(unreal.FrameRate(data_i.get("fps"), 1.0)) + sequences[i].set_playback_start(data_i.get("frameStart")) + sequences[i].set_playback_end(data_i.get("frameEnd")) + if data_j: + section.set_range( + data_j.get("frameStart"), + data_j.get("frameEnd")) + + data_i = data_j + + data = self._get_data(asset) if data: - sequence.set_display_rate(unreal.FrameRate(data.get("fps"), 1.0)) - sequence.set_playback_start(data.get("frameStart")) - sequence.set_playback_end(data.get("frameEnd")) + cam_seq.set_display_rate(unreal.FrameRate(data.get("fps"), 1.0)) + cam_seq.set_playback_start(data.get("frameStart")) + cam_seq.set_playback_end(data.get("frameEnd")) settings = unreal.MovieSceneUserImportFBXSettings() settings.set_editor_property('reduce_keys', False) - unreal.SequencerTools.import_fbx( - unreal.EditorLevelLibrary.get_editor_world(), - sequence, - sequence.get_bindings(), - settings, - self.fname - ) + if cam_seq: + unreal.SequencerTools.import_fbx( + unreal.EditorLevelLibrary.get_editor_world(), + cam_seq, + cam_seq.get_bindings(), + settings, + self.fname + ) # Create Asset Container lib.create_avalon_container(container=container_name, path=asset_dir) diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index 19d0b74e3e..7554a4658b 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -9,7 +9,7 @@ from unreal import AssetToolsHelpers from unreal import FBXImportType from unreal import MathLibrary as umath -from avalon import api, pipeline +from avalon import api, io, pipeline from avalon.unreal import lib from avalon.unreal import pipeline as unreal_pipeline @@ -74,10 +74,26 @@ class LayoutLoader(api.Loader): return None - def _process_family(self, assets, classname, transform, inst_name=None): + def _add_sub_sequence(self, master, sub): + track = master.add_master_track(unreal.MovieSceneCinematicShotTrack) + section = track.add_section() + section.set_editor_property('sub_sequence', sub) + return section + + def _get_data(self, asset_name): + asset_doc = io.find_one({ + "type": "asset", + "name": asset_name + }) + + return asset_doc.get("data") + + def _process_family( + self, assets, classname, transform, sequence, inst_name=None): ar = unreal.AssetRegistryHelpers.get_asset_registry() actors = [] + bindings = [] for asset in assets: obj = ar.get_asset_by_object_path(asset).get_asset() @@ -109,11 +125,17 @@ class LayoutLoader(api.Loader): actors.append(actor) - return actors + binding = sequence.add_possessable(actor) + # root_component_binding = sequence.add_possessable(actor.root_component) + # root_component_binding.set_parent(binding) + + bindings.append(binding) + + return actors, bindings def _import_animation( self, asset_dir, path, instance_name, skeleton, actors_dict, - animation_file): + animation_file, bindings_dict, sequence): anim_file = Path(animation_file) anim_file_name = anim_file.with_suffix('') @@ -192,7 +214,20 @@ class LayoutLoader(api.Loader): actor.skeletal_mesh_component.animation_data.set_editor_property( 'anim_to_play', animation) - def _process(self, libpath, asset_dir, loaded=None): + # Add animation to the sequencer + bindings = bindings_dict.get(instance_name) + + for binding in bindings: + binding.add_track(unreal.MovieSceneSkeletalAnimationTrack) + for track in binding.get_tracks(): + section = track.add_section() + section.set_range( + sequence.get_playback_start(), + sequence.get_playback_end()) + sec_params = section.get_editor_property('params') + sec_params.set_editor_property('animation', animation) + + def _process(self, libpath, asset_dir, sequence, loaded=None): ar = unreal.AssetRegistryHelpers.get_asset_registry() with open(libpath, "r") as fp: @@ -207,6 +242,7 @@ class LayoutLoader(api.Loader): skeleton_dict = {} actors_dict = {} + bindings_dict = {} for element in data: reference = None @@ -264,12 +300,13 @@ class LayoutLoader(api.Loader): actors = [] if family == 'model': - actors = self._process_family( - assets, 'StaticMesh', transform, inst) + actors, _ = self._process_family( + assets, 'StaticMesh', transform, sequence, inst) elif family == 'rig': - actors = self._process_family( - assets, 'SkeletalMesh', transform, inst) + actors, bindings = self._process_family( + assets, 'SkeletalMesh', transform, sequence, inst) actors_dict[inst] = actors + bindings_dict[inst] = bindings if family == 'rig': # Finds skeleton among the imported assets @@ -289,8 +326,13 @@ class LayoutLoader(api.Loader): if animation_file and skeleton: self._import_animation( - asset_dir, path, instance_name, skeleton, - actors_dict, animation_file) + asset_dir, path, instance_name, skeleton, actors_dict, + animation_file, bindings_dict, sequence) + + # track = sequence.add_master_track( + # unreal.MovieSceneActorReferenceTrack) + # section = track.add_section() + # section.set_editor_property('sub_sequence', sequence) def _remove_family(self, assets, components, classname, propname): ar = unreal.AssetRegistryHelpers.get_asset_registry() @@ -356,7 +398,13 @@ class LayoutLoader(api.Loader): list(str): list of container content """ # Create directory for asset and avalon container - root = "/Game/Avalon/Assets" + hierarchy = context.get('asset').get('data').get('parents') + root = "/Game/Avalon" + hierarchy_dir = root + hierarchy_list = [] + for h in hierarchy: + hierarchy_dir = f"{hierarchy_dir}/{h}" + hierarchy_list.append(hierarchy_dir) asset = context.get('asset').get('name') suffix = "_CON" if asset: @@ -366,13 +414,86 @@ class LayoutLoader(api.Loader): tools = unreal.AssetToolsHelpers().get_asset_tools() asset_dir, container_name = tools.create_unique_asset_name( - "{}/{}/{}".format(root, asset, name), suffix="") + "{}/{}/{}".format(hierarchy_dir, asset, name), suffix="") container_name += suffix EditorAssetLibrary.make_directory(asset_dir) - self._process(self.fname, asset_dir) + # Get all the sequences in the hierarchy. It will create them, if + # they don't exist. + sequences = [] + i = 0 + for h in hierarchy_list: + root_content = EditorAssetLibrary.list_assets( + h, recursive=False, include_folder=False) + + existing_sequences = [ + EditorAssetLibrary.find_asset_data(asset) + for asset in root_content + if EditorAssetLibrary.find_asset_data( + asset).get_class().get_name() == 'LevelSequence' + ] + + # for asset in root_content: + # asset_data = EditorAssetLibrary.find_asset_data(asset) + # # imported_asset = unreal.AssetRegistryHelpers.get_asset( + # # imported_asset_data) + # if asset_data.get_class().get_name() == 'LevelSequence': + # break + + if not existing_sequences: + scene = tools.create_asset( + asset_name=hierarchy[i], + package_path=h, + asset_class=unreal.LevelSequence, + factory=unreal.LevelSequenceFactoryNew() + ) + sequences.append(scene) + else: + for e in existing_sequences: + sequences.append(e.get_asset()) + + i += 1 + + # TODO: check if shot already exists + + shot = tools.create_asset( + asset_name=asset, + package_path=asset_dir, + asset_class=unreal.LevelSequence, + factory=unreal.LevelSequenceFactoryNew() + ) + + sequences.append(shot) + + # Add sequences data to hierarchy + data_i = self._get_data(sequences[0].get_name()) + + for i in range(0, len(sequences) - 1): + section = self._add_sub_sequence(sequences[i], sequences[i + 1]) + + data_j = self._get_data(sequences[i + 1].get_name()) + + if data_i: + sequences[i].set_display_rate(unreal.FrameRate(data_i.get("fps"), 1.0)) + sequences[i].set_playback_start(data_i.get("frameStart")) + sequences[i].set_playback_end(data_i.get("frameEnd")) + if data_j: + section.set_range( + data_j.get("frameStart"), + data_j.get("frameEnd")) + + data_i = data_j + + data = self._get_data(asset) + + if data: + shot.set_display_rate(unreal.FrameRate(data.get("fps"), 1.0)) + shot.set_playback_start(data.get("frameStart")) + shot.set_playback_end(data.get("frameEnd")) + + self._process(self.fname, asset_dir, shot) # Create Asset Container lib.create_avalon_container( From 5efc23c7433c08da7b42c182e7bcd1a4ad7b16ac Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 31 Jan 2022 11:22:47 +0000 Subject: [PATCH 003/109] Added button for starting the rendering of the selected instance --- openpype/hosts/unreal/api/rendering.py | 85 +++++++++++++++++++ openpype/hosts/unreal/api/tools_ui.py | 7 ++ .../unreal/plugins/create/create_render.py | 5 +- 3 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 openpype/hosts/unreal/api/rendering.py diff --git a/openpype/hosts/unreal/api/rendering.py b/openpype/hosts/unreal/api/rendering.py new file mode 100644 index 0000000000..7c58987c0d --- /dev/null +++ b/openpype/hosts/unreal/api/rendering.py @@ -0,0 +1,85 @@ +import avalon.unreal.pipeline as pipeline +import avalon.unreal.lib as lib +import unreal + + +queue = None +executor = None + +def _queue_finish_callback(exec, success): + unreal.log("Render completed. Success: " + str(success)) + + # Delete our reference so we don't keep it alive. + global executor + global queue + del executor + del queue + + +def _job_finish_callback(job, success): + # You can make any edits you want to the editor world here, and the world + # will be duplicated when the next render happens. Make sure you undo your + # edits in OnQueueFinishedCallback if you don't want to leak state changes + # into the editor world. + unreal.log("Individual job completed.") + + +def start_rendering(): + """ + Start the rendering process. + """ + print("Starting rendering...") + + # Get selected sequences + assets = unreal.EditorUtilityLibrary.get_selected_assets() + + # instances = pipeline.ls_inst() + instances = [ + a for a in assets + if a.get_class().get_name() == "AvalonPublishInstance"] + + inst_data = [] + + for i in instances: + data = pipeline.parse_container(i.get_path_name()) + if data["family"] == "render": + inst_data.append(data) + + # subsystem = unreal.get_editor_subsystem(unreal.MoviePipelineQueueSubsystem) + # queue = subsystem.get_queue() + global queue + queue = unreal.MoviePipelineQueue() + + for i in inst_data: + job = queue.allocate_new_job(unreal.MoviePipelineExecutorJob) + job.sequence = unreal.SoftObjectPath(i["sequence"]) + job.map = unreal.SoftObjectPath(i["map"]) + job.author = "OpenPype" + + # 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_setting = job.get_configuration().find_or_add_setting_by_class( + unreal.MoviePipelineOutputSetting) + output_setting.output_resolution = unreal.IntPoint(1280, 720) + output_setting.file_name_format = "{sequence_name}.{frame_number}" + output_setting.output_directory.path += f"{i['subset']}/" + + renderPass = job.get_configuration().find_or_add_setting_by_class( + unreal.MoviePipelineDeferredPassBase) + renderPass.disable_multisample_effects = True + + job.get_configuration().find_or_add_setting_by_class( + unreal.MoviePipelineImageSequenceOutput_PNG) + + # TODO: check if queue is empty + + global executor + executor = unreal.MoviePipelinePIEExecutor() + executor.on_executor_finished_delegate.add_callable_unique( + _queue_finish_callback) + executor.on_individual_job_finished_delegate.add_callable_unique( + _job_finish_callback) # Only available on PIE Executor + executor.execute(queue) diff --git a/openpype/hosts/unreal/api/tools_ui.py b/openpype/hosts/unreal/api/tools_ui.py index 93361c3574..2500f8495f 100644 --- a/openpype/hosts/unreal/api/tools_ui.py +++ b/openpype/hosts/unreal/api/tools_ui.py @@ -7,6 +7,7 @@ from openpype import ( ) from openpype.tools.utils import host_tools from openpype.tools.utils.lib import qt_app_context +from openpype.hosts.unreal.api import rendering class ToolsBtnsWidget(QtWidgets.QWidget): @@ -20,6 +21,7 @@ class ToolsBtnsWidget(QtWidgets.QWidget): load_btn = QtWidgets.QPushButton("Load...", self) publish_btn = QtWidgets.QPushButton("Publish...", self) manage_btn = QtWidgets.QPushButton("Manage...", self) + render_btn = QtWidgets.QPushButton("Render...", self) experimental_tools_btn = QtWidgets.QPushButton( "Experimental tools...", self ) @@ -30,6 +32,7 @@ class ToolsBtnsWidget(QtWidgets.QWidget): layout.addWidget(load_btn, 0) layout.addWidget(publish_btn, 0) layout.addWidget(manage_btn, 0) + layout.addWidget(render_btn, 0) layout.addWidget(experimental_tools_btn, 0) layout.addStretch(1) @@ -37,6 +40,7 @@ class ToolsBtnsWidget(QtWidgets.QWidget): load_btn.clicked.connect(self._on_load) publish_btn.clicked.connect(self._on_publish) manage_btn.clicked.connect(self._on_manage) + render_btn.clicked.connect(self._on_render) experimental_tools_btn.clicked.connect(self._on_experimental) def _on_create(self): @@ -51,6 +55,9 @@ class ToolsBtnsWidget(QtWidgets.QWidget): def _on_manage(self): self.tool_required.emit("sceneinventory") + def _on_render(self): + rendering.start_rendering() + def _on_experimental(self): self.tool_required.emit("experimental_tools") diff --git a/openpype/hosts/unreal/plugins/create/create_render.py b/openpype/hosts/unreal/plugins/create/create_render.py index a0bf320225..0128808a70 100644 --- a/openpype/hosts/unreal/plugins/create/create_render.py +++ b/openpype/hosts/unreal/plugins/create/create_render.py @@ -44,9 +44,10 @@ class CreateRender(Creator): for a in selection: d = self.data.copy() + d["members"] = [a] d["sequence"] = a + d["map"] = unreal.EditorLevelLibrary.get_editor_world().get_path_name() asset = ar.get_asset_by_object_path(a).get_asset() - container_name = asset.get_name() + container_name = f"{asset.get_name()}{self.suffix}" pipeline.create_publish_instance(instance=container_name, path=path) pipeline.imprint("{}/{}".format(path, container_name), d) - From 67339b488be8727d864f4f7804dacc9d9f267e6a Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 31 Jan 2022 11:23:26 +0000 Subject: [PATCH 004/109] Implemented extraction of renders --- .../unreal/plugins/publish/extract_render.py | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 openpype/hosts/unreal/plugins/publish/extract_render.py diff --git a/openpype/hosts/unreal/plugins/publish/extract_render.py b/openpype/hosts/unreal/plugins/publish/extract_render.py new file mode 100644 index 0000000000..7ba53c9155 --- /dev/null +++ b/openpype/hosts/unreal/plugins/publish/extract_render.py @@ -0,0 +1,46 @@ +from pathlib import Path +import openpype.api +from avalon import io +import unreal + + +class ExtractRender(openpype.api.Extractor): + """Extract render.""" + + label = "Extract Render" + hosts = ["unreal"] + families = ["render"] + optional = True + + def process(self, instance): + # Define extract output file path + stagingdir = self.staging_dir(instance) + + # Perform extraction + self.log.info("Performing extraction..") + + # Get the render output directory + project_dir = unreal.Paths.project_dir() + render_dir = f"{project_dir}/Saved/MovieRenders/{instance.data['subset']}" + + assert unreal.Paths.directory_exists(render_dir), \ + "Render directory does not exist" + + render_path = Path(render_dir) + + frames = [] + + for x in render_path.iterdir(): + if x.is_file() and x.suffix == '.png': + frames.append(str(x)) + + if "representations" not in instance.data: + instance.data["representations"] = [] + + render_representation = { + 'name': 'png', + 'ext': 'png', + 'files': frames, + "stagingDir": stagingdir, + } + instance.data["representations"].append(render_representation) From 2966068aa58d7e42f5a86cd289355242a51e779b Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 15 Feb 2022 12:27:54 +0000 Subject: [PATCH 005/109] Layout and Cameras create the level and sequence hierarchy structure --- .../hosts/unreal/plugins/load/load_camera.py | 93 +++++------ .../hosts/unreal/plugins/load/load_layout.py | 147 +++++++++++++++--- 2 files changed, 159 insertions(+), 81 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index 00d17407f9..feab531aaa 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -15,12 +15,6 @@ class CameraLoader(api.Loader): icon = "cube" color = "orange" - def _add_sub_sequence(self, master, sub): - track = master.add_master_track(unreal.MovieSceneCinematicShotTrack) - section = track.add_section() - section.set_editor_property('sub_sequence', sub) - return section - def _get_data(self, asset_name): asset_doc = io.find_one({ "type": "asset", @@ -29,6 +23,35 @@ class CameraLoader(api.Loader): return asset_doc.get("data") + def _set_sequence_hierarchy(self, seq_i, seq_j, data_i, data_j): + if data_i: + seq_i.set_display_rate(unreal.FrameRate(data_i.get("fps"), 1.0)) + seq_i.set_playback_start(data_i.get("frameStart")) + seq_i.set_playback_end(data_i.get("frameEnd") + 1) + + tracks = seq_i.get_master_tracks() + track = None + for t in tracks: + if t.get_class() == unreal.MovieSceneSubTrack.static_class(): + track = t + break + if not track: + track = seq_i.add_master_track(unreal.MovieSceneSubTrack) + + subscenes = track.get_sections() + subscene = None + for s in subscenes: + if s.get_editor_property('sub_sequence') == seq_j: + subscene = s + break + if not subscene: + subscene = track.add_section() + subscene.set_row_index(len(track.get_sections())) + subscene.set_editor_property('sub_sequence', seq_j) + subscene.set_range( + data_j.get("frameStart"), + data_j.get("frameEnd") + 1) + def load(self, context, name, namespace, data): """ Load and containerise representation into Content Browser. @@ -95,30 +118,6 @@ class CameraLoader(api.Loader): container_name += suffix - # sequence = None - - # ar = unreal.AssetRegistryHelpers.get_asset_registry() - - # if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): - # unreal.EditorAssetLibrary.make_directory(asset_dir) - - # sequence = tools.create_asset( - # asset_name=asset_name, - # package_path=asset_dir, - # asset_class=unreal.LevelSequence, - # factory=unreal.LevelSequenceFactoryNew() - # ) - # else: - # asset_content = unreal.EditorAssetLibrary.list_assets( - # asset_dir, recursive=False) - # for a in asset_content: - # obj = ar.get_asset_by_object_path(a) - # if obj.get_asset().get_class().get_name() == 'LevelSequence': - # sequence = obj.get_asset() - # break - - # assert sequence, "Sequence not found" - # Get all the sequences in the hierarchy. It will create them, if # they don't exist. sequences = [] @@ -134,13 +133,6 @@ class CameraLoader(api.Loader): asset).get_class().get_name() == 'LevelSequence' ] - # for asset in root_content: - # asset_data = EditorAssetLibrary.find_asset_data(asset) - # # imported_asset = unreal.AssetRegistryHelpers.get_asset( - # # imported_asset_data) - # if asset_data.get_class().get_name() == 'LevelSequence': - # break - if not existing_sequences: scene = tools.create_asset( asset_name=hierarchy[i], @@ -158,42 +150,27 @@ class CameraLoader(api.Loader): unreal.EditorAssetLibrary.make_directory(asset_dir) cam_seq = tools.create_asset( - asset_name=asset, + asset_name=f"{asset}_camera", package_path=asset_dir, asset_class=unreal.LevelSequence, factory=unreal.LevelSequenceFactoryNew() ) - sequences.append(cam_seq) - # Add sequences data to hierarchy data_i = self._get_data(sequences[0].get_name()) for i in range(0, len(sequences) - 1): - section = self._add_sub_sequence(sequences[i], sequences[i + 1]) - - print(sequences[i]) - print(sequences[i + 1]) - data_j = self._get_data(sequences[i + 1].get_name()) - if data_i: - sequences[i].set_display_rate(unreal.FrameRate(data_i.get("fps"), 1.0)) - sequences[i].set_playback_start(data_i.get("frameStart")) - sequences[i].set_playback_end(data_i.get("frameEnd")) - if data_j: - section.set_range( - data_j.get("frameStart"), - data_j.get("frameEnd")) + self._set_sequence_hierarchy( + sequences[i], sequences[i + 1], data_i, data_j) data_i = data_j + parent_data = self._get_data(sequences[-1].get_name()) data = self._get_data(asset) - - if data: - cam_seq.set_display_rate(unreal.FrameRate(data.get("fps"), 1.0)) - cam_seq.set_playback_start(data.get("frameStart")) - cam_seq.set_playback_end(data.get("frameEnd")) + self._set_sequence_hierarchy( + sequences[-1], cam_seq, parent_data, data) settings = unreal.MovieSceneUserImportFBXSettings() settings.set_editor_property('reduce_keys', False) diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index 7554a4658b..a7d5a5841f 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -5,6 +5,7 @@ from pathlib import Path import unreal from unreal import EditorAssetLibrary from unreal import EditorLevelLibrary +from unreal import EditorLevelUtils from unreal import AssetToolsHelpers from unreal import FBXImportType from unreal import MathLibrary as umath @@ -74,12 +75,6 @@ class LayoutLoader(api.Loader): return None - def _add_sub_sequence(self, master, sub): - track = master.add_master_track(unreal.MovieSceneCinematicShotTrack) - section = track.add_section() - section.set_editor_property('sub_sequence', sub) - return section - def _get_data(self, asset_name): asset_doc = io.find_one({ "type": "asset", @@ -88,6 +83,78 @@ class LayoutLoader(api.Loader): return asset_doc.get("data") + def _set_sequence_hierarchy(self, seq_i, seq_j, data_i, data_j, map_paths): + # Set data for the parent sequence + if data_i: + seq_i.set_display_rate(unreal.FrameRate(data_i.get("fps"), 1.0)) + seq_i.set_playback_start(data_i.get("frameStart")) + seq_i.set_playback_end(data_i.get("frameEnd") + 1) + + # Get existing sequencer tracks or create them if they don't exist + tracks = seq_i.get_master_tracks() + subscene_track = None + visibility_track = None + for t in tracks: + if t.get_class() == unreal.MovieSceneSubTrack.static_class(): + subscene_track = t + if t.get_class() == unreal.MovieSceneLevelVisibilityTrack.static_class(): + visibility_track = t + if not subscene_track: + subscene_track = seq_i.add_master_track(unreal.MovieSceneSubTrack) + if not visibility_track: + visibility_track = seq_i.add_master_track(unreal.MovieSceneLevelVisibilityTrack) + + # Create the sub-scene section + subscenes = subscene_track.get_sections() + subscene = None + for s in subscenes: + if s.get_editor_property('sub_sequence') == seq_j: + subscene = s + break + if not subscene: + subscene = subscene_track.add_section() + subscene.set_row_index(len(subscene_track.get_sections())) + subscene.set_editor_property('sub_sequence', seq_j) + subscene.set_range( + data_j.get("frameStart"), + data_j.get("frameEnd") + 1) + + # Create the visibility section + ar = unreal.AssetRegistryHelpers.get_asset_registry() + maps = [] + for m in map_paths: + # Unreal requires to load the level to get the map name + EditorLevelLibrary.save_all_dirty_levels() + EditorLevelLibrary.load_level(m) + maps.append(str(ar.get_asset_by_object_path(m).asset_name)) + + vis_section = visibility_track.add_section() + index = len(visibility_track.get_sections()) + + vis_section.set_range( + data_j.get("frameStart"), + data_j.get("frameEnd") + 1) + vis_section.set_visibility(unreal.LevelVisibility.VISIBLE) + vis_section.set_row_index(index) + vis_section.set_level_names(maps) + + if data_j.get("frameStart") > 1: + hid_section = visibility_track.add_section() + hid_section.set_range( + 1, + data_j.get("frameStart")) + hid_section.set_visibility(unreal.LevelVisibility.HIDDEN) + hid_section.set_row_index(index) + hid_section.set_level_names(maps) + if data_j.get("frameEnd") < data_i.get("frameEnd"): + hid_section = visibility_track.add_section() + hid_section.set_range( + data_j.get("frameEnd") + 1, + data_i.get("frameEnd") + 1) + hid_section.set_visibility(unreal.LevelVisibility.HIDDEN) + hid_section.set_row_index(index) + hid_section.set_level_names(maps) + def _process_family( self, assets, classname, transform, sequence, inst_name=None): ar = unreal.AssetRegistryHelpers.get_asset_registry() @@ -420,6 +487,37 @@ class LayoutLoader(api.Loader): EditorAssetLibrary.make_directory(asset_dir) + # Create map for the shot, and create hierarchy of map. If the maps + # already exist, we will use them. + maps = [] + for h in hierarchy_list: + a = h.split('/')[-1] + map = f"{h}/{a}_map.{a}_map" + new = False + + if not EditorAssetLibrary.does_asset_exist(map): + EditorLevelLibrary.new_level(f"{h}/{a}_map") + new = True + + maps.append({"map": map, "new": new}) + + EditorLevelLibrary.new_level(f"{asset_dir}/{asset}_map") + maps.append( + {"map":f"{asset_dir}/{asset}_map.{asset}_map", "new": True}) + + for i in range(0, len(maps) - 1): + for j in range(i + 1, len(maps)): + if maps[j].get('new'): + EditorLevelLibrary.load_level(maps[i].get('map')) + EditorLevelUtils.add_level_to_world( + EditorLevelLibrary.get_editor_world(), + maps[j].get('map'), + unreal.LevelStreamingDynamic + ) + EditorLevelLibrary.save_all_dirty_levels() + + EditorLevelLibrary.load_level(maps[-1].get('map')) + # Get all the sequences in the hierarchy. It will create them, if # they don't exist. sequences = [] @@ -456,8 +554,6 @@ class LayoutLoader(api.Loader): i += 1 - # TODO: check if shot already exists - shot = tools.create_asset( asset_name=asset, package_path=asset_dir, @@ -465,36 +561,39 @@ class LayoutLoader(api.Loader): factory=unreal.LevelSequenceFactoryNew() ) - sequences.append(shot) - # Add sequences data to hierarchy data_i = self._get_data(sequences[0].get_name()) for i in range(0, len(sequences) - 1): - section = self._add_sub_sequence(sequences[i], sequences[i + 1]) + maps_to_add = [] + for j in range(i + 1, len(maps)): + maps_to_add.append(maps[j].get('map')) data_j = self._get_data(sequences[i + 1].get_name()) - if data_i: - sequences[i].set_display_rate(unreal.FrameRate(data_i.get("fps"), 1.0)) - sequences[i].set_playback_start(data_i.get("frameStart")) - sequences[i].set_playback_end(data_i.get("frameEnd")) - if data_j: - section.set_range( - data_j.get("frameStart"), - data_j.get("frameEnd")) + self._set_sequence_hierarchy( + sequences[i], sequences[i + 1], + data_i, data_j, + maps_to_add) data_i = data_j + parent_data = self._get_data(sequences[-1].get_name()) data = self._get_data(asset) + self._set_sequence_hierarchy( + sequences[-1], shot, + parent_data, data, + [maps[-1].get('map')]) - if data: - shot.set_display_rate(unreal.FrameRate(data.get("fps"), 1.0)) - shot.set_playback_start(data.get("frameStart")) - shot.set_playback_end(data.get("frameEnd")) + EditorLevelLibrary.load_level(maps[-1].get('map')) self._process(self.fname, asset_dir, shot) + for s in sequences: + EditorAssetLibrary.save_asset(s.get_full_name()) + + EditorLevelLibrary.save_current_level() + # Create Asset Container lib.create_avalon_container( container=container_name, path=asset_dir) @@ -520,6 +619,8 @@ class LayoutLoader(api.Loader): for a in asset_content: EditorAssetLibrary.save_asset(a) + EditorLevelLibrary.load_level(maps[0].get('map')) + return asset_content def update(self, container, representation): From 9e7208187599b8095c9e03a873b750682862e717 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 16 Feb 2022 09:48:28 +0000 Subject: [PATCH 006/109] Animation are added to sequence when loaded --- .../unreal/plugins/load/load_animation.py | 188 +++++++++++------- 1 file changed, 119 insertions(+), 69 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_animation.py b/openpype/hosts/unreal/plugins/load/load_animation.py index 20baa30847..7d054c4899 100644 --- a/openpype/hosts/unreal/plugins/load/load_animation.py +++ b/openpype/hosts/unreal/plugins/load/load_animation.py @@ -1,10 +1,14 @@ import os import json +import unreal +from unreal import EditorAssetLibrary +from unreal import MovieSceneSkeletalAnimationTrack +from unreal import MovieSceneSkeletalAnimationSection + from avalon import api, pipeline from avalon.unreal import lib from avalon.unreal import pipeline as unreal_pipeline -import unreal class AnimationFBXLoader(api.Loader): @@ -16,59 +20,13 @@ class AnimationFBXLoader(api.Loader): icon = "cube" color = "orange" - def load(self, context, name, namespace, options=None): - """ - Load and containerise representation into Content Browser. - - This is two step process. First, import FBX to temporary path and - then call `containerise()` on it - this moves all content to new - directory and then it will create AssetContainer there and imprint it - with metadata. This will mark this path as container. - - Args: - context (dict): application context - name (str): subset name - namespace (str): in Unreal this is basically path to container. - This is not passed here, so namespace is set - by `containerise()` because only then we know - real path. - data (dict): Those would be data to be imprinted. This is not used - now, data are imprinted by `containerise()`. - - Returns: - list(str): list of container content - """ - - # Create directory for asset and avalon container - root = "/Game/Avalon/Assets" - asset = context.get('asset').get('name') - suffix = "_CON" - if asset: - asset_name = "{}_{}".format(asset, name) - else: - asset_name = "{}".format(name) - - tools = unreal.AssetToolsHelpers().get_asset_tools() - asset_dir, container_name = tools.create_unique_asset_name( - "{}/{}/{}".format(root, asset, name), suffix="") - - container_name += suffix - - unreal.EditorAssetLibrary.make_directory(asset_dir) - + def _process(self, asset_dir, asset_name, instance_name): automated = False actor = None task = unreal.AssetImportTask() task.options = unreal.FbxImportUI() - libpath = self.fname.replace("fbx", "json") - - with open(libpath, "r") as fp: - data = json.load(fp) - - instance_name = data.get("instance_name") - if instance_name: automated = True # Old method to get the actor @@ -126,6 +84,116 @@ class AnimationFBXLoader(api.Loader): unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) + asset_content = unreal.EditorAssetLibrary.list_assets( + asset_dir, recursive=True, include_folder=True + ) + + animation = None + + for a in asset_content: + imported_asset_data = unreal.EditorAssetLibrary.find_asset_data(a) + imported_asset = unreal.AssetRegistryHelpers.get_asset( + imported_asset_data) + if imported_asset.__class__ == unreal.AnimSequence: + animation = imported_asset + break + + if animation: + animation.set_editor_property('enable_root_motion', True) + actor.skeletal_mesh_component.set_editor_property( + 'animation_mode', unreal.AnimationMode.ANIMATION_SINGLE_NODE) + actor.skeletal_mesh_component.animation_data.set_editor_property( + 'anim_to_play', animation) + + return animation + + def load(self, context, name, namespace, options=None): + """ + Load and containerise representation into Content Browser. + + This is two step process. First, import FBX to temporary path and + then call `containerise()` on it - this moves all content to new + directory and then it will create AssetContainer there and imprint it + with metadata. This will mark this path as container. + + Args: + context (dict): application context + name (str): subset name + namespace (str): in Unreal this is basically path to container. + This is not passed here, so namespace is set + by `containerise()` because only then we know + real path. + data (dict): Those would be data to be imprinted. This is not used + now, data are imprinted by `containerise()`. + + Returns: + list(str): list of container content + """ + + # Create directory for asset and avalon container + hierarchy = context.get('asset').get('data').get('parents') + root = "/Game/Avalon" + asset = context.get('asset').get('name') + suffix = "_CON" + if asset: + asset_name = "{}_{}".format(asset, name) + else: + asset_name = "{}".format(name) + + tools = unreal.AssetToolsHelpers().get_asset_tools() + asset_dir, container_name = tools.create_unique_asset_name( + f"{root}/Assets/{asset}/{name}", suffix="") + + hierarchy_dir = root + for h in hierarchy: + hierarchy_dir = f"{hierarchy_dir}/{h}" + hierarchy_dir = f"{hierarchy_dir}/{asset}" + + container_name += suffix + + unreal.EditorAssetLibrary.make_directory(asset_dir) + + libpath = self.fname.replace("fbx", "json") + + with open(libpath, "r") as fp: + data = json.load(fp) + + instance_name = data.get("instance_name") + + animation = self._process(asset_dir, container_name, instance_name) + + asset_content = unreal.EditorAssetLibrary.list_assets( + hierarchy_dir, recursive=True, include_folder=False) + + # Get the sequence for the layout, excluding the camera one. + sequences = [a for a in asset_content + if (EditorAssetLibrary.find_asset_data(a).get_class() == + unreal.LevelSequence.static_class() and + "_camera" not in a.split("/")[-1])] + + ar = unreal.AssetRegistryHelpers.get_asset_registry() + + for s in sequences: + sequence = ar.get_asset_by_object_path(s).get_asset() + possessables = [ + p for p in sequence.get_possessables() + if p.get_display_name() == instance_name] + + for p in possessables: + tracks = [ + t for t in p.get_tracks() + if (t.get_class() == + MovieSceneSkeletalAnimationTrack.static_class())] + + for t in tracks: + sections = [ + s for s in t.get_sections() + if (s.get_class() == + MovieSceneSkeletalAnimationSection.static_class())] + + for s in sections: + s.params.set_editor_property('animation', animation) + # Create Asset Container lib.create_avalon_container( container=container_name, path=asset_dir) @@ -145,29 +213,11 @@ class AnimationFBXLoader(api.Loader): unreal_pipeline.imprint( "{}/{}".format(asset_dir, container_name), data) - asset_content = unreal.EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=True - ) + imported_content = unreal.EditorAssetLibrary.list_assets( + asset_dir, recursive=True, include_folder=False) - animation = None - - for a in asset_content: + for a in imported_content: unreal.EditorAssetLibrary.save_asset(a) - imported_asset_data = unreal.EditorAssetLibrary.find_asset_data(a) - imported_asset = unreal.AssetRegistryHelpers.get_asset( - imported_asset_data) - if imported_asset.__class__ == unreal.AnimSequence: - animation = imported_asset - break - - if animation: - animation.set_editor_property('enable_root_motion', True) - actor.skeletal_mesh_component.set_editor_property( - 'animation_mode', unreal.AnimationMode.ANIMATION_SINGLE_NODE) - actor.skeletal_mesh_component.animation_data.set_editor_property( - 'anim_to_play', animation) - - return asset_content def update(self, container, representation): name = container["asset_name"] From d11a871bb181d0934efda4579ccafb5c73cb8c17 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 17 Feb 2022 16:34:18 +0000 Subject: [PATCH 007/109] Changed logic to obtain min and max frame of sequences --- .../hosts/unreal/plugins/load/load_camera.py | 32 ++++++++++++++--- .../hosts/unreal/plugins/load/load_layout.py | 35 +++++++++++++++---- 2 files changed, 55 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index feab531aaa..2d29319fc7 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -24,11 +24,6 @@ class CameraLoader(api.Loader): return asset_doc.get("data") def _set_sequence_hierarchy(self, seq_i, seq_j, data_i, data_j): - if data_i: - seq_i.set_display_rate(unreal.FrameRate(data_i.get("fps"), 1.0)) - seq_i.set_playback_start(data_i.get("frameStart")) - seq_i.set_playback_end(data_i.get("frameEnd") + 1) - tracks = seq_i.get_master_tracks() track = None for t in tracks: @@ -140,6 +135,33 @@ class CameraLoader(api.Loader): asset_class=unreal.LevelSequence, factory=unreal.LevelSequenceFactoryNew() ) + + asset_data = io.find_one({ + "type": "asset", + "name": h.split('/')[-1] + }) + + id = asset_data.get('_id') + + start_frames = [] + end_frames = [] + + elements = list( + io.find({"type": "asset", "data.visualParent": id})) + for e in elements: + start_frames.append(e.get('data').get('clipIn')) + end_frames.append(e.get('data').get('clipOut')) + + elements.extend(io.find({ + "type": "asset", + "data.visualParent": e.get('_id') + })) + + scene.set_display_rate( + unreal.FrameRate(asset_data.get('data').get("fps"), 1.0)) + scene.set_playback_start(min(start_frames)) + scene.set_playback_end(max(end_frames)) + sequences.append(scene) else: for e in existing_sequences: diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index a7d5a5841f..a36bd6663a 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -1,4 +1,4 @@ -import os +import os, sys import json from pathlib import Path @@ -84,12 +84,6 @@ class LayoutLoader(api.Loader): return asset_doc.get("data") def _set_sequence_hierarchy(self, seq_i, seq_j, data_i, data_j, map_paths): - # Set data for the parent sequence - if data_i: - seq_i.set_display_rate(unreal.FrameRate(data_i.get("fps"), 1.0)) - seq_i.set_playback_start(data_i.get("frameStart")) - seq_i.set_playback_end(data_i.get("frameEnd") + 1) - # Get existing sequencer tracks or create them if they don't exist tracks = seq_i.get_master_tracks() subscene_track = None @@ -547,6 +541,33 @@ class LayoutLoader(api.Loader): asset_class=unreal.LevelSequence, factory=unreal.LevelSequenceFactoryNew() ) + + asset_data = io.find_one({ + "type": "asset", + "name": h.split('/')[-1] + }) + + id = asset_data.get('_id') + + start_frames = [] + end_frames = [] + + elements = list( + io.find({"type": "asset", "data.visualParent": id})) + for e in elements: + start_frames.append(e.get('data').get('clipIn')) + end_frames.append(e.get('data').get('clipOut')) + + elements.extend(io.find({ + "type": "asset", + "data.visualParent": e.get('_id') + })) + + scene.set_display_rate( + unreal.FrameRate(asset_data.get('data').get("fps"), 1.0)) + scene.set_playback_start(min(start_frames)) + scene.set_playback_end(max(end_frames)) + sequences.append(scene) else: for e in existing_sequences: From ce4984d7e60488b20e08850a87468f488805822c Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 17 Feb 2022 17:35:14 +0000 Subject: [PATCH 008/109] Camera is now saved in the right level --- .../hosts/unreal/plugins/load/load_camera.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index 2d29319fc7..61d9c04d2f 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -78,7 +78,10 @@ class CameraLoader(api.Loader): for h in hierarchy: hierarchy_dir = f"{hierarchy_dir}/{h}" hierarchy_list.append(hierarchy_dir) + print(h) + print(hierarchy_dir) asset = context.get('asset').get('name') + print(asset) suffix = "_CON" if asset: asset_name = "{}_{}".format(asset, name) @@ -113,6 +116,23 @@ class CameraLoader(api.Loader): container_name += suffix + current_level = unreal.EditorLevelLibrary.get_editor_world().get_full_name() + unreal.EditorLevelLibrary.save_all_dirty_levels() + + # asset_content = unreal.EditorAssetLibrary.list_assets( + # f"{hierarchy_dir}/{asset}/", recursive=True, include_folder=False + # ) + + 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 + unreal.EditorLevelLibrary.load_level(maps[0].get_full_name()) + # Get all the sequences in the hierarchy. It will create them, if # they don't exist. sequences = [] @@ -224,6 +244,9 @@ class CameraLoader(api.Loader): unreal_pipeline.imprint( "{}/{}".format(asset_dir, container_name), data) + unreal.EditorLevelLibrary.save_all_dirty_levels() + unreal.EditorLevelLibrary.load_level(current_level) + asset_content = unreal.EditorAssetLibrary.list_assets( asset_dir, recursive=True, include_folder=True ) From fa64a26a17271522e80af955a48f6238c5c4af2b Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 17 Feb 2022 17:46:45 +0000 Subject: [PATCH 009/109] Removed debug prints --- openpype/hosts/unreal/plugins/load/load_camera.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index 61d9c04d2f..6ee88f8acc 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -78,10 +78,7 @@ class CameraLoader(api.Loader): for h in hierarchy: hierarchy_dir = f"{hierarchy_dir}/{h}" hierarchy_list.append(hierarchy_dir) - print(h) - print(hierarchy_dir) asset = context.get('asset').get('name') - print(asset) suffix = "_CON" if asset: asset_name = "{}_{}".format(asset, name) @@ -119,10 +116,6 @@ class CameraLoader(api.Loader): current_level = unreal.EditorLevelLibrary.get_editor_world().get_full_name() unreal.EditorLevelLibrary.save_all_dirty_levels() - # asset_content = unreal.EditorAssetLibrary.list_assets( - # f"{hierarchy_dir}/{asset}/", recursive=True, include_folder=False - # ) - ar = unreal.AssetRegistryHelpers.get_asset_registry() filter = unreal.ARFilter( class_names = ["World"], From 26d63e6beacf7334eaa4d5c4a5cad789adc75db5 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 18 Feb 2022 12:15:06 +0000 Subject: [PATCH 010/109] Fix start and end frames of sequences --- .../hosts/unreal/plugins/load/load_layout.py | 48 ++++++++++--------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index a36bd6663a..5d7977b237 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -1,4 +1,4 @@ -import os, sys +import os import json from pathlib import Path @@ -83,7 +83,9 @@ class LayoutLoader(api.Loader): return asset_doc.get("data") - def _set_sequence_hierarchy(self, seq_i, seq_j, data_i, data_j, map_paths): + def _set_sequence_hierarchy(self, + 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() subscene_track = None @@ -110,8 +112,8 @@ class LayoutLoader(api.Loader): subscene.set_row_index(len(subscene_track.get_sections())) subscene.set_editor_property('sub_sequence', seq_j) subscene.set_range( - data_j.get("frameStart"), - data_j.get("frameEnd") + 1) + min_frame_j, + max_frame_j + 1) # Create the visibility section ar = unreal.AssetRegistryHelpers.get_asset_registry() @@ -126,25 +128,25 @@ class LayoutLoader(api.Loader): index = len(visibility_track.get_sections()) vis_section.set_range( - data_j.get("frameStart"), - data_j.get("frameEnd") + 1) + min_frame_j, + max_frame_j + 1) vis_section.set_visibility(unreal.LevelVisibility.VISIBLE) vis_section.set_row_index(index) vis_section.set_level_names(maps) - if data_j.get("frameStart") > 1: + if min_frame_j > 1: hid_section = visibility_track.add_section() hid_section.set_range( 1, - data_j.get("frameStart")) + min_frame_j) hid_section.set_visibility(unreal.LevelVisibility.HIDDEN) hid_section.set_row_index(index) hid_section.set_level_names(maps) - if data_j.get("frameEnd") < data_i.get("frameEnd"): + if max_frame_j < max_frame_i: hid_section = visibility_track.add_section() hid_section.set_range( - data_j.get("frameEnd") + 1, - data_i.get("frameEnd") + 1) + max_frame_j + 1, + max_frame_i + 1) hid_section.set_visibility(unreal.LevelVisibility.HIDDEN) hid_section.set_row_index(index) hid_section.set_level_names(maps) @@ -515,6 +517,7 @@ class LayoutLoader(api.Loader): # 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: root_content = EditorAssetLibrary.list_assets( @@ -563,12 +566,16 @@ class LayoutLoader(api.Loader): "data.visualParent": e.get('_id') })) + min_frame = min(start_frames) + max_frame = max(end_frames) + scene.set_display_rate( unreal.FrameRate(asset_data.get('data').get("fps"), 1.0)) - scene.set_playback_start(min(start_frames)) - scene.set_playback_end(max(end_frames)) + scene.set_playback_start(min_frame) + scene.set_playback_end(max_frame) sequences.append(scene) + frame_ranges.append((min_frame, max_frame)) else: for e in existing_sequences: sequences.append(e.get_asset()) @@ -582,28 +589,23 @@ class LayoutLoader(api.Loader): factory=unreal.LevelSequenceFactoryNew() ) - # Add sequences data to hierarchy - data_i = self._get_data(sequences[0].get_name()) - + # sequences and frame_ranges have the same length for i in range(0, len(sequences) - 1): maps_to_add = [] for j in range(i + 1, len(maps)): maps_to_add.append(maps[j].get('map')) - data_j = self._get_data(sequences[i + 1].get_name()) - self._set_sequence_hierarchy( sequences[i], sequences[i + 1], - data_i, data_j, + frame_ranges[i][1], + frame_ranges[i + 1][0], frame_ranges[i + 1][1], maps_to_add) - data_i = data_j - - parent_data = self._get_data(sequences[-1].get_name()) data = self._get_data(asset) self._set_sequence_hierarchy( sequences[-1], shot, - parent_data, data, + frame_ranges[-1][1], + data.get('clipIn'), data.get('clipOut'), [maps[-1].get('map')]) EditorLevelLibrary.load_level(maps[-1].get('map')) From 55bcd6bba71f0ef191f2597a9aefc61c01639950 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 23 Feb 2022 10:19:29 +0000 Subject: [PATCH 011/109] Set correct start and end frames for existing sequences --- openpype/hosts/unreal/plugins/load/load_layout.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index 5d7977b237..58b6f661b9 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -579,6 +579,9 @@ class LayoutLoader(api.Loader): else: for e in existing_sequences: sequences.append(e.get_asset()) + frame_ranges.append(( + e.get_asset().get_playback_start(), + e.get_asset().get_playback_end())) i += 1 From 10c6d7be08aaeccc219dec51361c793ac495fcff Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 23 Feb 2022 10:20:30 +0000 Subject: [PATCH 012/109] Add an empty Camera Cut Track to the sequences in the hierarchy --- .../hosts/unreal/plugins/load/load_layout.py | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index 58b6f661b9..05615ff083 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -530,15 +530,8 @@ class LayoutLoader(api.Loader): asset).get_class().get_name() == 'LevelSequence' ] - # for asset in root_content: - # asset_data = EditorAssetLibrary.find_asset_data(asset) - # # imported_asset = unreal.AssetRegistryHelpers.get_asset( - # # imported_asset_data) - # if asset_data.get_class().get_name() == 'LevelSequence': - # break - if not existing_sequences: - scene = tools.create_asset( + sequence = tools.create_asset( asset_name=hierarchy[i], package_path=h, asset_class=unreal.LevelSequence, @@ -569,13 +562,23 @@ class LayoutLoader(api.Loader): min_frame = min(start_frames) max_frame = max(end_frames) - scene.set_display_rate( + sequence.set_display_rate( unreal.FrameRate(asset_data.get('data').get("fps"), 1.0)) - scene.set_playback_start(min_frame) - scene.set_playback_end(max_frame) + sequence.set_playback_start(min_frame) + sequence.set_playback_end(max_frame) - sequences.append(scene) + sequences.append(sequence) frame_ranges.append((min_frame, max_frame)) + + tracks = sequence.get_master_tracks() + track = None + for t in tracks: + if t.get_class() == unreal.MovieSceneCameraCutTrack.static_class(): + track = t + break + if not track: + track = sequence.add_master_track( + unreal.MovieSceneCameraCutTrack) else: for e in existing_sequences: sequences.append(e.get_asset()) From 6bb615e2a04d47068b091290fcbac6481c1fa01e Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 23 Feb 2022 11:31:19 +0000 Subject: [PATCH 013/109] Set correct start and end frame for camera sequences --- .../hosts/unreal/plugins/load/load_camera.py | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index 6ee88f8acc..12aaceb385 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -23,7 +23,9 @@ class CameraLoader(api.Loader): return asset_doc.get("data") - def _set_sequence_hierarchy(self, seq_i, seq_j, data_i, data_j): + def _set_sequence_hierarchy(self, + seq_i, seq_j, min_frame_j, max_frame_j + ): tracks = seq_i.get_master_tracks() track = None for t in tracks: @@ -44,8 +46,8 @@ class CameraLoader(api.Loader): subscene.set_row_index(len(track.get_sections())) subscene.set_editor_property('sub_sequence', seq_j) subscene.set_range( - data_j.get("frameStart"), - data_j.get("frameEnd") + 1) + min_frame_j, + max_frame_j + 1) def load(self, context, name, namespace, data): """ @@ -129,6 +131,7 @@ class CameraLoader(api.Loader): # 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: root_content = unreal.EditorAssetLibrary.list_assets( @@ -170,15 +173,22 @@ class CameraLoader(api.Loader): "data.visualParent": e.get('_id') })) + min_frame = min(start_frames) + max_frame = max(end_frames) + scene.set_display_rate( unreal.FrameRate(asset_data.get('data').get("fps"), 1.0)) - scene.set_playback_start(min(start_frames)) - scene.set_playback_end(max(end_frames)) + scene.set_playback_start(min_frame) + scene.set_playback_end(max_frame) sequences.append(scene) + frame_ranges.append((min_frame, max_frame)) else: for e in existing_sequences: sequences.append(e.get_asset()) + frame_ranges.append(( + e.get_asset().get_playback_start(), + e.get_asset().get_playback_end())) i += 1 @@ -192,20 +202,15 @@ class CameraLoader(api.Loader): ) # Add sequences data to hierarchy - data_i = self._get_data(sequences[0].get_name()) - for i in range(0, len(sequences) - 1): - data_j = self._get_data(sequences[i + 1].get_name()) - self._set_sequence_hierarchy( - sequences[i], sequences[i + 1], data_i, data_j) + sequences[i], sequences[i + 1], + frame_ranges[i + 1][0], frame_ranges[i + 1][1]) - data_i = data_j - - parent_data = self._get_data(sequences[-1].get_name()) data = self._get_data(asset) self._set_sequence_hierarchy( - sequences[-1], cam_seq, parent_data, data) + sequences[-1], cam_seq, + data.get('clipIn'), data.get('clipOut')) settings = unreal.MovieSceneUserImportFBXSettings() settings.set_editor_property('reduce_keys', False) From 35f5e4a8408fb52e1c0d1d318416e922a55f32f5 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 2 Mar 2022 11:17:58 +0000 Subject: [PATCH 014/109] Render from the master sequence and output keeps the hierarchy --- openpype/hosts/unreal/api/rendering.py | 99 +++++++++++++------ .../unreal/plugins/create/create_render.py | 62 +++++++++++- 2 files changed, 129 insertions(+), 32 deletions(-) diff --git a/openpype/hosts/unreal/api/rendering.py b/openpype/hosts/unreal/api/rendering.py index 7c58987c0d..8eb4e1e75a 100644 --- a/openpype/hosts/unreal/api/rendering.py +++ b/openpype/hosts/unreal/api/rendering.py @@ -1,11 +1,11 @@ import avalon.unreal.pipeline as pipeline -import avalon.unreal.lib as lib import unreal queue = None executor = None + def _queue_finish_callback(exec, success): unreal.log("Render completed. Success: " + str(success)) @@ -45,41 +45,84 @@ def start_rendering(): if data["family"] == "render": inst_data.append(data) - # subsystem = unreal.get_editor_subsystem(unreal.MoviePipelineQueueSubsystem) + # subsystem = unreal.get_editor_subsystem( + # unreal.MoviePipelineQueueSubsystem) # queue = subsystem.get_queue() global queue queue = unreal.MoviePipelineQueue() + ar = unreal.AssetRegistryHelpers.get_asset_registry() + for i in inst_data: - job = queue.allocate_new_job(unreal.MoviePipelineExecutorJob) - job.sequence = unreal.SoftObjectPath(i["sequence"]) - job.map = unreal.SoftObjectPath(i["map"]) - job.author = "OpenPype" + sequence = ar.get_asset_by_object_path(i["sequence"]).get_asset() - # 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 = "" + sequences = [{ + "sequence": sequence, + "output": f"{i['subset']}/{sequence.get_name()}", + "frame_range": ( + int(float(i["startFrame"])), + int(float(i["endFrame"])) + 1) + }] + render_list = [] - output_setting = job.get_configuration().find_or_add_setting_by_class( - unreal.MoviePipelineOutputSetting) - output_setting.output_resolution = unreal.IntPoint(1280, 720) - output_setting.file_name_format = "{sequence_name}.{frame_number}" - output_setting.output_directory.path += f"{i['subset']}/" + # 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: + tracks = s.get('sequence').get_master_tracks() + subscene_track = None + for t in tracks: + if t.get_class() == unreal.MovieSceneSubTrack.static_class(): + subscene_track = t + if subscene_track is not None and subscene_track.get_sections(): + subscenes = subscene_track.get_sections() - renderPass = job.get_configuration().find_or_add_setting_by_class( - unreal.MoviePipelineDeferredPassBase) - renderPass.disable_multisample_effects = True + for ss in subscenes: + sequences.append({ + "sequence": ss.get_sequence(), + "output": f"{s.get('output')}/{ss.get_sequence().get_name()}", + "frame_range": ( + ss.get_start_frame(), ss.get_end_frame()) + }) + else: + # Avoid rendering camera sequences + if "_camera" not in s.get('sequence').get_name(): + render_list.append(s) - job.get_configuration().find_or_add_setting_by_class( - unreal.MoviePipelineImageSequenceOutput_PNG) + # Create the rendering jobs and add them to the queue. + for r 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" - # TODO: check if queue is empty + # 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 = "" - global executor - executor = unreal.MoviePipelinePIEExecutor() - executor.on_executor_finished_delegate.add_callable_unique( - _queue_finish_callback) - executor.on_individual_job_finished_delegate.add_callable_unique( - _job_finish_callback) # Only available on PIE Executor - executor.execute(queue) + 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.use_custom_playback_range = True + settings.file_name_format = "{sequence_name}.{frame_number}" + settings.output_directory.path += r.get('output') + + renderPass = job.get_configuration().find_or_add_setting_by_class( + unreal.MoviePipelineDeferredPassBase) + renderPass.disable_multisample_effects = True + + job.get_configuration().find_or_add_setting_by_class( + unreal.MoviePipelineImageSequenceOutput_PNG) + + # If there are jobs in the queue, start the rendering process. + if queue.get_jobs(): + global executor + executor = unreal.MoviePipelinePIEExecutor() + executor.on_executor_finished_delegate.add_callable_unique( + _queue_finish_callback) + executor.on_individual_job_finished_delegate.add_callable_unique( + _job_finish_callback) # Only available on PIE Executor + executor.execute(queue) diff --git a/openpype/hosts/unreal/plugins/create/create_render.py b/openpype/hosts/unreal/plugins/create/create_render.py index 0128808a70..de092c4dd7 100644 --- a/openpype/hosts/unreal/plugins/create/create_render.py +++ b/openpype/hosts/unreal/plugins/create/create_render.py @@ -1,5 +1,8 @@ import unreal + from openpype.hosts.unreal.api.plugin import Creator + +from avalon import io from avalon.unreal import pipeline @@ -21,7 +24,22 @@ class CreateRender(Creator): def process(self): name = self.data["subset"] - print(self.data) + ar = unreal.AssetRegistryHelpers.get_asset_registry() + + # 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/Avalon/{self.data['asset']}"], + recursive_paths = False) + sequences = ar.get_assets(filter) + ms = sequences[0].object_path + filter = unreal.ARFilter( + class_names = ["World"], + package_paths = [f"/Game/Avalon/{self.data['asset']}"], + recursive_paths = False) + levels = ar.get_assets(filter) + ml = levels[0].object_path selection = [] if (self.options or {}).get("useSelection"): @@ -46,8 +64,44 @@ class CreateRender(Creator): d = self.data.copy() d["members"] = [a] d["sequence"] = a - d["map"] = unreal.EditorLevelLibrary.get_editor_world().get_path_name() + d["master_sequence"] = ms + d["master_level"] = ml asset = ar.get_asset_by_object_path(a).get_asset() - container_name = f"{asset.get_name()}{self.suffix}" - pipeline.create_publish_instance(instance=container_name, path=path) + asset_name = asset.get_name() + + # Get frame range. We need to go through the hierarchy and check + # the frame range for the children. + asset_data = io.find_one({ + "type": "asset", + "name": asset_name + }) + id = asset_data.get('_id') + + elements = list( + io.find({"type": "asset", "data.visualParent": id})) + + if elements: + start_frames = [] + end_frames = [] + for e in elements: + start_frames.append(e.get('data').get('clipIn')) + end_frames.append(e.get('data').get('clipOut')) + + elements.extend(io.find({ + "type": "asset", + "data.visualParent": e.get('_id') + })) + + min_frame = min(start_frames) + max_frame = max(end_frames) + else: + min_frame = asset_data.get('data').get('clipIn') + max_frame = asset_data.get('data').get('clipOut') + + d["startFrame"] = min_frame + d["endFrame"] = max_frame + + container_name = f"{asset_name}{self.suffix}" + pipeline.create_publish_instance( + instance=container_name, path=path) pipeline.imprint("{}/{}".format(path, container_name), d) From 5d8bac337f1631f4653d19b5e59f40ee406247c0 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 3 Mar 2022 10:37:02 +0000 Subject: [PATCH 015/109] Fixed frame range and frame rate for shot sequences --- openpype/hosts/unreal/plugins/load/load_camera.py | 4 ++++ openpype/hosts/unreal/plugins/load/load_layout.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index 12aaceb385..cea59ae93f 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -208,6 +208,10 @@ class CameraLoader(api.Loader): frame_ranges[i + 1][0], frame_ranges[i + 1][1]) data = self._get_data(asset) + cam_seq.set_display_rate( + unreal.FrameRate(data.get("fps"), 1.0)) + cam_seq.set_playback_start(0) + cam_seq.set_playback_end(data.get('clipOut') - data.get('clipIn') + 1) self._set_sequence_hierarchy( sequences[-1], cam_seq, data.get('clipIn'), data.get('clipOut')) diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index 05615ff083..5a976a1fb5 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -608,6 +608,10 @@ class LayoutLoader(api.Loader): maps_to_add) data = self._get_data(asset) + shot.set_display_rate( + unreal.FrameRate(data.get("fps"), 1.0)) + shot.set_playback_start(0) + shot.set_playback_end(data.get('clipOut') - data.get('clipIn') + 1) self._set_sequence_hierarchy( sequences[-1], shot, frame_ranges[-1][1], From 468a5e145ce138c9470db35a59612e68bcebcb4e Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 3 Mar 2022 11:05:11 +0000 Subject: [PATCH 016/109] Hound fixes --- openpype/hosts/unreal/api/rendering.py | 3 +- .../hosts/unreal/plugins/load/load_camera.py | 69 ++++++++++--------- .../hosts/unreal/plugins/load/load_layout.py | 42 ++++++----- .../unreal/plugins/publish/extract_render.py | 8 ++- 4 files changed, 63 insertions(+), 59 deletions(-) diff --git a/openpype/hosts/unreal/api/rendering.py b/openpype/hosts/unreal/api/rendering.py index 8eb4e1e75a..3ed77cc651 100644 --- a/openpype/hosts/unreal/api/rendering.py +++ b/openpype/hosts/unreal/api/rendering.py @@ -80,7 +80,8 @@ def start_rendering(): for ss in subscenes: sequences.append({ "sequence": ss.get_sequence(), - "output": f"{s.get('output')}/{ss.get_sequence().get_name()}", + "output": (f"{s.get('output')}/" + f"{ss.get_sequence().get_name()}"), "frame_range": ( ss.get_start_frame(), ss.get_end_frame()) }) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index cea59ae93f..f93de0a79a 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -1,9 +1,12 @@ import os +import unreal +from unreal import EditorAssetLibrary +from unreal import EditorLevelLibrary + from avalon import api, io, pipeline from avalon.unreal import lib from avalon.unreal import pipeline as unreal_pipeline -import unreal class CameraLoader(api.Loader): @@ -23,8 +26,8 @@ class CameraLoader(api.Loader): return asset_doc.get("data") - def _set_sequence_hierarchy(self, - seq_i, seq_j, min_frame_j, max_frame_j + def _set_sequence_hierarchy( + self, seq_i, seq_j, min_frame_j, max_frame_j ): tracks = seq_i.get_master_tracks() track = None @@ -91,8 +94,8 @@ class CameraLoader(api.Loader): # Create a unique name for the camera directory unique_number = 1 - if unreal.EditorAssetLibrary.does_directory_exist(f"{hierarchy_dir}/{asset}"): - asset_content = unreal.EditorAssetLibrary.list_assets( + if EditorAssetLibrary.does_directory_exist(f"{hierarchy_dir}/{asset}"): + asset_content = EditorAssetLibrary.list_assets( f"{root}/{asset}", recursive=False, include_folder=True ) @@ -115,32 +118,32 @@ class CameraLoader(api.Loader): container_name += suffix - current_level = unreal.EditorLevelLibrary.get_editor_world().get_full_name() - unreal.EditorLevelLibrary.save_all_dirty_levels() + current_level = EditorLevelLibrary.get_editor_world().get_full_name() + 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) + 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 - unreal.EditorLevelLibrary.load_level(maps[0].get_full_name()) + EditorLevelLibrary.load_level(maps[0].get_full_name()) - # Get all the sequences in the hierarchy. It will create them, if + # 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: - root_content = unreal.EditorAssetLibrary.list_assets( + root_content = EditorAssetLibrary.list_assets( h, recursive=False, include_folder=False) existing_sequences = [ - unreal.EditorAssetLibrary.find_asset_data(asset) + EditorAssetLibrary.find_asset_data(asset) for asset in root_content - if unreal.EditorAssetLibrary.find_asset_data( + if EditorAssetLibrary.find_asset_data( asset).get_class().get_name() == 'LevelSequence' ] @@ -192,7 +195,7 @@ class CameraLoader(api.Loader): i += 1 - unreal.EditorAssetLibrary.make_directory(asset_dir) + EditorAssetLibrary.make_directory(asset_dir) cam_seq = tools.create_asset( asset_name=f"{asset}_camera", @@ -213,15 +216,15 @@ class CameraLoader(api.Loader): cam_seq.set_playback_start(0) cam_seq.set_playback_end(data.get('clipOut') - data.get('clipIn') + 1) self._set_sequence_hierarchy( - sequences[-1], cam_seq, - data.get('clipIn'), data.get('clipOut')) + sequences[-1], cam_seq, + data.get('clipIn'), data.get('clipOut')) settings = unreal.MovieSceneUserImportFBXSettings() settings.set_editor_property('reduce_keys', False) if cam_seq: unreal.SequencerTools.import_fbx( - unreal.EditorLevelLibrary.get_editor_world(), + EditorLevelLibrary.get_editor_world(), cam_seq, cam_seq.get_bindings(), settings, @@ -246,15 +249,15 @@ class CameraLoader(api.Loader): unreal_pipeline.imprint( "{}/{}".format(asset_dir, container_name), data) - unreal.EditorLevelLibrary.save_all_dirty_levels() - unreal.EditorLevelLibrary.load_level(current_level) + EditorLevelLibrary.save_all_dirty_levels() + EditorLevelLibrary.load_level(current_level) - asset_content = unreal.EditorAssetLibrary.list_assets( + asset_content = EditorAssetLibrary.list_assets( asset_dir, recursive=True, include_folder=True ) for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) + EditorAssetLibrary.save_asset(a) return asset_content @@ -264,25 +267,25 @@ class CameraLoader(api.Loader): ar = unreal.AssetRegistryHelpers.get_asset_registry() tools = unreal.AssetToolsHelpers().get_asset_tools() - asset_content = unreal.EditorAssetLibrary.list_assets( + asset_content = EditorAssetLibrary.list_assets( path, recursive=False, include_folder=False ) asset_name = "" for a in asset_content: asset = ar.get_asset_by_object_path(a) if a.endswith("_CON"): - loaded_asset = unreal.EditorAssetLibrary.load_asset(a) - unreal.EditorAssetLibrary.set_metadata_tag( + loaded_asset = EditorAssetLibrary.load_asset(a) + EditorAssetLibrary.set_metadata_tag( loaded_asset, "representation", str(representation["_id"]) ) - unreal.EditorAssetLibrary.set_metadata_tag( + EditorAssetLibrary.set_metadata_tag( loaded_asset, "parent", str(representation["parent"]) ) - asset_name = unreal.EditorAssetLibrary.get_metadata_tag( + asset_name = EditorAssetLibrary.get_metadata_tag( loaded_asset, "asset_name" ) elif asset.asset_class == "LevelSequence": - unreal.EditorAssetLibrary.delete_asset(a) + EditorAssetLibrary.delete_asset(a) sequence = tools.create_asset( asset_name=asset_name, @@ -308,7 +311,7 @@ class CameraLoader(api.Loader): settings.set_editor_property('reduce_keys', False) unreal.SequencerTools.import_fbx( - unreal.EditorLevelLibrary.get_editor_world(), + EditorLevelLibrary.get_editor_world(), sequence, sequence.get_bindings(), settings, @@ -319,11 +322,11 @@ class CameraLoader(api.Loader): path = container["namespace"] parent_path = os.path.dirname(path) - unreal.EditorAssetLibrary.delete_directory(path) + EditorAssetLibrary.delete_directory(path) - asset_content = unreal.EditorAssetLibrary.list_assets( + asset_content = EditorAssetLibrary.list_assets( parent_path, recursive=False, include_folder=True ) if len(asset_content) == 0: - unreal.EditorAssetLibrary.delete_directory(parent_path) + EditorAssetLibrary.delete_directory(parent_path) diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index 5a976a1fb5..e25f06ad42 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -83,8 +83,8 @@ class LayoutLoader(api.Loader): return asset_doc.get("data") - def _set_sequence_hierarchy(self, - seq_i, seq_j, max_frame_i, min_frame_j, max_frame_j, map_paths + def _set_sequence_hierarchy( + self, 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() @@ -93,12 +93,14 @@ class LayoutLoader(api.Loader): for t in tracks: if t.get_class() == unreal.MovieSceneSubTrack.static_class(): subscene_track = t - if t.get_class() == unreal.MovieSceneLevelVisibilityTrack.static_class(): + if (t.get_class() == + unreal.MovieSceneLevelVisibilityTrack.static_class()): visibility_track = t if not subscene_track: subscene_track = seq_i.add_master_track(unreal.MovieSceneSubTrack) if not visibility_track: - visibility_track = seq_i.add_master_track(unreal.MovieSceneLevelVisibilityTrack) + visibility_track = seq_i.add_master_track( + unreal.MovieSceneLevelVisibilityTrack) # Create the sub-scene section subscenes = subscene_track.get_sections() @@ -152,7 +154,8 @@ class LayoutLoader(api.Loader): hid_section.set_level_names(maps) def _process_family( - self, assets, classname, transform, sequence, inst_name=None): + self, assets, classname, transform, sequence, inst_name=None + ): ar = unreal.AssetRegistryHelpers.get_asset_registry() actors = [] @@ -189,16 +192,15 @@ class LayoutLoader(api.Loader): actors.append(actor) binding = sequence.add_possessable(actor) - # root_component_binding = sequence.add_possessable(actor.root_component) - # root_component_binding.set_parent(binding) bindings.append(binding) return actors, bindings def _import_animation( - self, asset_dir, path, instance_name, skeleton, actors_dict, - animation_file, bindings_dict, sequence): + self, asset_dir, path, instance_name, skeleton, actors_dict, + animation_file, bindings_dict, sequence + ): anim_file = Path(animation_file) anim_file_name = anim_file.with_suffix('') @@ -389,14 +391,9 @@ class LayoutLoader(api.Loader): if animation_file and skeleton: self._import_animation( - asset_dir, path, instance_name, skeleton, actors_dict, + asset_dir, path, instance_name, skeleton, actors_dict, animation_file, bindings_dict, sequence) - # track = sequence.add_master_track( - # unreal.MovieSceneActorReferenceTrack) - # section = track.add_section() - # section.set_editor_property('sub_sequence', sequence) - def _remove_family(self, assets, components, classname, propname): ar = unreal.AssetRegistryHelpers.get_asset_registry() @@ -499,7 +496,7 @@ class LayoutLoader(api.Loader): EditorLevelLibrary.new_level(f"{asset_dir}/{asset}_map") maps.append( - {"map":f"{asset_dir}/{asset}_map.{asset}_map", "new": True}) + {"map": f"{asset_dir}/{asset}_map.{asset}_map", "new": True}) for i in range(0, len(maps) - 1): for j in range(i + 1, len(maps)): @@ -514,7 +511,7 @@ class LayoutLoader(api.Loader): EditorLevelLibrary.load_level(maps[-1].get('map')) - # Get all the sequences in the hierarchy. It will create them, if + # Get all the sequences in the hierarchy. It will create them, if # they don't exist. sequences = [] frame_ranges = [] @@ -573,7 +570,8 @@ class LayoutLoader(api.Loader): tracks = sequence.get_master_tracks() track = None for t in tracks: - if t.get_class() == unreal.MovieSceneCameraCutTrack.static_class(): + if (t.get_class() == + unreal.MovieSceneCameraCutTrack.static_class()): track = t break if not track: @@ -613,10 +611,10 @@ class LayoutLoader(api.Loader): shot.set_playback_start(0) shot.set_playback_end(data.get('clipOut') - data.get('clipIn') + 1) self._set_sequence_hierarchy( - sequences[-1], shot, - frame_ranges[-1][1], - data.get('clipIn'), data.get('clipOut'), - [maps[-1].get('map')]) + sequences[-1], shot, + frame_ranges[-1][1], + data.get('clipIn'), data.get('clipOut'), + [maps[-1].get('map')]) EditorLevelLibrary.load_level(maps[-1].get('map')) diff --git a/openpype/hosts/unreal/plugins/publish/extract_render.py b/openpype/hosts/unreal/plugins/publish/extract_render.py index 7ba53c9155..37fe7e916f 100644 --- a/openpype/hosts/unreal/plugins/publish/extract_render.py +++ b/openpype/hosts/unreal/plugins/publish/extract_render.py @@ -1,8 +1,9 @@ from pathlib import Path -import openpype.api -from avalon import io + import unreal +import openpype.api + class ExtractRender(openpype.api.Extractor): """Extract render.""" @@ -21,7 +22,8 @@ class ExtractRender(openpype.api.Extractor): # Get the render output directory project_dir = unreal.Paths.project_dir() - render_dir = f"{project_dir}/Saved/MovieRenders/{instance.data['subset']}" + render_dir = (f"{project_dir}/Saved/MovieRenders/" + f"{instance.data['subset']}") assert unreal.Paths.directory_exists(render_dir), \ "Render directory does not exist" From df02c64f18ec3b8e3bca1b07b99de93a5629a634 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 3 Mar 2022 11:07:23 +0000 Subject: [PATCH 017/109] More hound fixes --- .../hosts/unreal/plugins/create/create_render.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/unreal/plugins/create/create_render.py b/openpype/hosts/unreal/plugins/create/create_render.py index de092c4dd7..e6c233a2c5 100644 --- a/openpype/hosts/unreal/plugins/create/create_render.py +++ b/openpype/hosts/unreal/plugins/create/create_render.py @@ -29,15 +29,15 @@ class CreateRender(Creator): # 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/Avalon/{self.data['asset']}"], - recursive_paths = False) + class_names=["LevelSequence"], + package_paths=[f"/Game/Avalon/{self.data['asset']}"], + recursive_paths=False) sequences = ar.get_assets(filter) ms = sequences[0].object_path filter = unreal.ARFilter( - class_names = ["World"], - package_paths = [f"/Game/Avalon/{self.data['asset']}"], - recursive_paths = False) + class_names=["World"], + package_paths=[f"/Game/Avalon/{self.data['asset']}"], + recursive_paths=False) levels = ar.get_assets(filter) ml = levels[0].object_path From e127e08be4bc7ca1c05c3e5b560cf3e00dd53590 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 8 Mar 2022 12:08:56 +0000 Subject: [PATCH 018/109] Fix paths for loading animations --- openpype/hosts/unreal/plugins/load/load_animation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_animation.py b/openpype/hosts/unreal/plugins/load/load_animation.py index bc4a42c84b..c1f7942ef0 100644 --- a/openpype/hosts/unreal/plugins/load/load_animation.py +++ b/openpype/hosts/unreal/plugins/load/load_animation.py @@ -134,7 +134,7 @@ class AnimationFBXLoader(plugin.Loader): # Create directory for asset and avalon container hierarchy = context.get('asset').get('data').get('parents') - root = "/Game/Avalon" + root = "/Game/OpenPype" asset = context.get('asset').get('name') suffix = "_CON" if asset: @@ -144,7 +144,7 @@ class AnimationFBXLoader(plugin.Loader): tools = unreal.AssetToolsHelpers().get_asset_tools() asset_dir, container_name = tools.create_unique_asset_name( - f"{root}/Assets/{asset}/{name}", suffix="") + f"{root}/Animations/{asset}/{name}", suffix="") hierarchy_dir = root for h in hierarchy: From ef726be366bbaaa877262b5843ff118205f24183 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 8 Mar 2022 12:30:38 +0000 Subject: [PATCH 019/109] Activates MovieRenderPipeline plugin when creating Unreal project --- openpype/hosts/unreal/lib.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/unreal/lib.py b/openpype/hosts/unreal/lib.py index d4a776e892..805e883c64 100644 --- a/openpype/hosts/unreal/lib.py +++ b/openpype/hosts/unreal/lib.py @@ -254,6 +254,7 @@ def create_unreal_project(project_name: str, {"Name": "PythonScriptPlugin", "Enabled": True}, {"Name": "EditorScriptingUtilities", "Enabled": True}, {"Name": "SequencerScripting", "Enabled": True}, + {"Name": "MovieRenderPipeline", "Enabled": True}, {"Name": "OpenPype", "Enabled": True} ] } From fedc09f83dd304923715803253702a547cfbe9ff Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 8 Mar 2022 13:05:14 +0000 Subject: [PATCH 020/109] Set bound scale for rig actors loaded with layout This is needed for actors that gets close to the camera, that wouldn't be rendered without this parameter set to the maximum value. --- openpype/hosts/unreal/plugins/load/load_layout.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index 9f30affa3d..d99224042a 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -194,6 +194,11 @@ class LayoutLoader(plugin.Loader): ), False) actor.set_actor_scale3d(transform.get('scale')) + if class_name == 'SkeletalMesh': + skm_comp = actor.get_editor_property( + 'skeletal_mesh_component') + skm_comp.set_bounds_scale(10.0) + actors.append(actor) binding = sequence.add_possessable(actor) From 88a59bc0ee4efa61c6765a26094342f7d4d6b106 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 10 Mar 2022 16:18:49 +0000 Subject: [PATCH 021/109] Fixed class name for Render Publish Instance --- openpype/hosts/unreal/api/rendering.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/api/rendering.py b/openpype/hosts/unreal/api/rendering.py index d70d621b8a..38bcf21b1c 100644 --- a/openpype/hosts/unreal/api/rendering.py +++ b/openpype/hosts/unreal/api/rendering.py @@ -37,7 +37,7 @@ def start_rendering(): # instances = pipeline.ls_inst() instances = [ a for a in assets - if a.get_class().get_name() == "AvalonPublishInstance"] + if a.get_class().get_name() == "OpenPypePublishInstance"] inst_data = [] From 4ad37ad687324c934612aa12235a4292a70955ac Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 12 Mar 2022 01:10:30 +0000 Subject: [PATCH 022/109] Bump pillow from 9.0.0 to 9.0.1 Bumps [pillow](https://github.com/python-pillow/Pillow) from 9.0.0 to 9.0.1. - [Release notes](https://github.com/python-pillow/Pillow/releases) - [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) - [Commits](https://github.com/python-pillow/Pillow/compare/9.0.0...9.0.1) --- updated-dependencies: - dependency-name: pillow dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- poetry.lock | 146 ++++++++++++++++++++++++++-------------------------- 1 file changed, 73 insertions(+), 73 deletions(-) diff --git a/poetry.lock b/poetry.lock index ee7b839b8d..b8c7090cc0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -680,15 +680,8 @@ category = "main" optional = false python-versions = "*" -[package.dependencies] -attrs = ">=17.4.0" -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} -pyrsistent = ">=0.14.0" -six = ">=1.11.0" - [package.extras] -format = ["idna", "jsonpointer (>1.13)", "rfc3987", "strict-rfc3339", "webcolors"] -format_nongpl = ["idna", "jsonpointer (>1.13)", "webcolors", "rfc3986-validator (>0.1.0)", "rfc3339-validator"] +format = ["rfc3987", "strict-rfc3339", "webcolors"] [[package]] name = "keyring" @@ -826,7 +819,7 @@ six = "*" [[package]] name = "pillow" -version = "9.0.0" +version = "9.0.1" description = "Python Imaging Library (Fork)" category = "main" optional = false @@ -1087,14 +1080,6 @@ category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -[[package]] -name = "pyrsistent" -version = "0.18.1" -description = "Persistent/Functional/Immutable data structures" -category = "main" -optional = false -python-versions = ">=3.7" - [[package]] name = "pysftp" version = "0.2.9" @@ -1633,7 +1618,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest- [metadata] lock-version = "1.1" python-versions = "3.7.*" -content-hash = "2f78d48a6aad2d8a88b7dd7f31a76d907bec9fb65f0086fba6b6d2e1605f0f88" +content-hash = "b02313c8255a1897b0f0617ad4884a5943696c363512921aab1cb2dd8f4fdbe0" [metadata.files] acre = [] @@ -2171,12 +2156,28 @@ log4mongo = [ {file = "log4mongo-1.7.0.tar.gz", hash = "sha256:dc374617206162a0b14167fbb5feac01dbef587539a235dadba6200362984a68"}, ] markupsafe = [ + {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, @@ -2185,14 +2186,27 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"}, {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, @@ -2202,6 +2216,12 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, @@ -2289,38 +2309,41 @@ pathlib2 = [ {file = "pathlib2-2.3.6.tar.gz", hash = "sha256:7d8bcb5555003cdf4a8d2872c538faa3a0f5d20630cb360e518ca3b981795e5f"}, ] pillow = [ - {file = "Pillow-9.0.0-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:113723312215b25c22df1fdf0e2da7a3b9c357a7d24a93ebbe80bfda4f37a8d4"}, - {file = "Pillow-9.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bb47a548cea95b86494a26c89d153fd31122ed65255db5dcbc421a2d28eb3379"}, - {file = "Pillow-9.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31b265496e603985fad54d52d11970383e317d11e18e856971bdbb86af7242a4"}, - {file = "Pillow-9.0.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d154ed971a4cc04b93a6d5b47f37948d1f621f25de3e8fa0c26b2d44f24e3e8f"}, - {file = "Pillow-9.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80fe92813d208ce8aa7d76da878bdc84b90809f79ccbad2a288e9bcbeac1d9bd"}, - {file = "Pillow-9.0.0-cp310-cp310-win32.whl", hash = "sha256:d5dcea1387331c905405b09cdbfb34611050cc52c865d71f2362f354faee1e9f"}, - {file = "Pillow-9.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:52abae4c96b5da630a8b4247de5428f593465291e5b239f3f843a911a3cf0105"}, - {file = "Pillow-9.0.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:72c3110228944019e5f27232296c5923398496b28be42535e3b2dc7297b6e8b6"}, - {file = "Pillow-9.0.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97b6d21771da41497b81652d44191489296555b761684f82b7b544c49989110f"}, - {file = "Pillow-9.0.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72f649d93d4cc4d8cf79c91ebc25137c358718ad75f99e99e043325ea7d56100"}, - {file = "Pillow-9.0.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7aaf07085c756f6cb1c692ee0d5a86c531703b6e8c9cae581b31b562c16b98ce"}, - {file = "Pillow-9.0.0-cp37-cp37m-win32.whl", hash = "sha256:03b27b197deb4ee400ed57d8d4e572d2d8d80f825b6634daf6e2c18c3c6ccfa6"}, - {file = "Pillow-9.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:a09a9d4ec2b7887f7a088bbaacfd5c07160e746e3d47ec5e8050ae3b2a229e9f"}, - {file = "Pillow-9.0.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:490e52e99224858f154975db61c060686df8a6b3f0212a678e5d2e2ce24675c9"}, - {file = "Pillow-9.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:500d397ddf4bbf2ca42e198399ac13e7841956c72645513e8ddf243b31ad2128"}, - {file = "Pillow-9.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ebd8b9137630a7bbbff8c4b31e774ff05bbb90f7911d93ea2c9371e41039b52"}, - {file = "Pillow-9.0.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd0e5062f11cb3e730450a7d9f323f4051b532781026395c4323b8ad055523c4"}, - {file = "Pillow-9.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f3b4522148586d35e78313db4db0df4b759ddd7649ef70002b6c3767d0fdeb7"}, - {file = "Pillow-9.0.0-cp38-cp38-win32.whl", hash = "sha256:0b281fcadbb688607ea6ece7649c5d59d4bbd574e90db6cd030e9e85bde9fecc"}, - {file = "Pillow-9.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:b5050d681bcf5c9f2570b93bee5d3ec8ae4cf23158812f91ed57f7126df91762"}, - {file = "Pillow-9.0.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:c2067b3bb0781f14059b112c9da5a91c80a600a97915b4f48b37f197895dd925"}, - {file = "Pillow-9.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2d16b6196fb7a54aff6b5e3ecd00f7c0bab1b56eee39214b2b223a9d938c50af"}, - {file = "Pillow-9.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98cb63ca63cb61f594511c06218ab4394bf80388b3d66cd61d0b1f63ee0ea69f"}, - {file = "Pillow-9.0.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bc462d24500ba707e9cbdef436c16e5c8cbf29908278af053008d9f689f56dee"}, - {file = "Pillow-9.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3586e12d874ce2f1bc875a3ffba98732ebb12e18fb6d97be482bd62b56803281"}, - {file = "Pillow-9.0.0-cp39-cp39-win32.whl", hash = "sha256:68e06f8b2248f6dc8b899c3e7ecf02c9f413aab622f4d6190df53a78b93d97a5"}, - {file = "Pillow-9.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:6579f9ba84a3d4f1807c4aab4be06f373017fc65fff43498885ac50a9b47a553"}, - {file = "Pillow-9.0.0-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:47f5cf60bcb9fbc46011f75c9b45a8b5ad077ca352a78185bd3e7f1d294b98bb"}, - {file = "Pillow-9.0.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fd8053e1f8ff1844419842fd474fc359676b2e2a2b66b11cc59f4fa0a301315"}, - {file = "Pillow-9.0.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c5439bfb35a89cac50e81c751317faea647b9a3ec11c039900cd6915831064d"}, - {file = "Pillow-9.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:95545137fc56ce8c10de646074d242001a112a92de169986abd8c88c27566a05"}, - {file = "Pillow-9.0.0.tar.gz", hash = "sha256:ee6e2963e92762923956fe5d3479b1fdc3b76c83f290aad131a2f98c3df0593e"}, + {file = "Pillow-9.0.1-1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a5d24e1d674dd9d72c66ad3ea9131322819ff86250b30dc5821cbafcfa0b96b4"}, + {file = "Pillow-9.0.1-1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2632d0f846b7c7600edf53c48f8f9f1e13e62f66a6dbc15191029d950bfed976"}, + {file = "Pillow-9.0.1-1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b9618823bd237c0d2575283f2939655f54d51b4527ec3972907a927acbcc5bfc"}, + {file = "Pillow-9.0.1-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:9bfdb82cdfeccec50aad441afc332faf8606dfa5e8efd18a6692b5d6e79f00fd"}, + {file = "Pillow-9.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5100b45a4638e3c00e4d2320d3193bdabb2d75e79793af7c3eb139e4f569f16f"}, + {file = "Pillow-9.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:528a2a692c65dd5cafc130de286030af251d2ee0483a5bf50c9348aefe834e8a"}, + {file = "Pillow-9.0.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f29d831e2151e0b7b39981756d201f7108d3d215896212ffe2e992d06bfe049"}, + {file = "Pillow-9.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:855c583f268edde09474b081e3ddcd5cf3b20c12f26e0d434e1386cc5d318e7a"}, + {file = "Pillow-9.0.1-cp310-cp310-win32.whl", hash = "sha256:d9d7942b624b04b895cb95af03a23407f17646815495ce4547f0e60e0b06f58e"}, + {file = "Pillow-9.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:81c4b81611e3a3cb30e59b0cf05b888c675f97e3adb2c8672c3154047980726b"}, + {file = "Pillow-9.0.1-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:413ce0bbf9fc6278b2d63309dfeefe452835e1c78398efb431bab0672fe9274e"}, + {file = "Pillow-9.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80fe64a6deb6fcfdf7b8386f2cf216d329be6f2781f7d90304351811fb591360"}, + {file = "Pillow-9.0.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cef9c85ccbe9bee00909758936ea841ef12035296c748aaceee535969e27d31b"}, + {file = "Pillow-9.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d19397351f73a88904ad1aee421e800fe4bbcd1aeee6435fb62d0a05ccd1030"}, + {file = "Pillow-9.0.1-cp37-cp37m-win32.whl", hash = "sha256:d21237d0cd37acded35154e29aec853e945950321dd2ffd1a7d86fe686814669"}, + {file = "Pillow-9.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:ede5af4a2702444a832a800b8eb7f0a7a1c0eed55b644642e049c98d589e5092"}, + {file = "Pillow-9.0.1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:b5b3f092fe345c03bca1e0b687dfbb39364b21ebb8ba90e3fa707374b7915204"}, + {file = "Pillow-9.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:335ace1a22325395c4ea88e00ba3dc89ca029bd66bd5a3c382d53e44f0ccd77e"}, + {file = "Pillow-9.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db6d9fac65bd08cea7f3540b899977c6dee9edad959fa4eaf305940d9cbd861c"}, + {file = "Pillow-9.0.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f154d173286a5d1863637a7dcd8c3437bb557520b01bddb0be0258dcb72696b5"}, + {file = "Pillow-9.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14d4b1341ac07ae07eb2cc682f459bec932a380c3b122f5540432d8977e64eae"}, + {file = "Pillow-9.0.1-cp38-cp38-win32.whl", hash = "sha256:effb7749713d5317478bb3acb3f81d9d7c7f86726d41c1facca068a04cf5bb4c"}, + {file = "Pillow-9.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:7f7609a718b177bf171ac93cea9fd2ddc0e03e84d8fa4e887bdfc39671d46b00"}, + {file = "Pillow-9.0.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:80ca33961ced9c63358056bd08403ff866512038883e74f3a4bf88ad3eb66838"}, + {file = "Pillow-9.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1c3c33ac69cf059bbb9d1a71eeaba76781b450bc307e2291f8a4764d779a6b28"}, + {file = "Pillow-9.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12875d118f21cf35604176872447cdb57b07126750a33748bac15e77f90f1f9c"}, + {file = "Pillow-9.0.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:514ceac913076feefbeaf89771fd6febde78b0c4c1b23aaeab082c41c694e81b"}, + {file = "Pillow-9.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3c5c79ab7dfce6d88f1ba639b77e77a17ea33a01b07b99840d6ed08031cb2a7"}, + {file = "Pillow-9.0.1-cp39-cp39-win32.whl", hash = "sha256:718856856ba31f14f13ba885ff13874be7fefc53984d2832458f12c38205f7f7"}, + {file = "Pillow-9.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:f25ed6e28ddf50de7e7ea99d7a976d6a9c415f03adcaac9c41ff6ff41b6d86ac"}, + {file = "Pillow-9.0.1-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:011233e0c42a4a7836498e98c1acf5e744c96a67dd5032a6f666cc1fb97eab97"}, + {file = "Pillow-9.0.1-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:253e8a302a96df6927310a9d44e6103055e8fb96a6822f8b7f514bb7ef77de56"}, + {file = "Pillow-9.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6295f6763749b89c994fcb6d8a7f7ce03c3992e695f89f00b741b4580b199b7e"}, + {file = "Pillow-9.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:a9f44cd7e162ac6191491d7249cceb02b8116b0f7e847ee33f739d7cb1ea1f70"}, + {file = "Pillow-9.0.1.tar.gz", hash = "sha256:6c8bc8238a7dfdaf7a75f5ec5a663f4173f8c367e5a39f87e720495e1eed75fa"}, ] platformdirs = [ {file = "platformdirs-2.4.1-py3-none-any.whl", hash = "sha256:1d7385c7db91728b83efd0ca99a5afb296cab9d0ed8313a45ed8ba17967ecfca"}, @@ -2598,29 +2621,6 @@ pyparsing = [ {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, ] -pyrsistent = [ - {file = "pyrsistent-0.18.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:df46c854f490f81210870e509818b729db4488e1f30f2a1ce1698b2295a878d1"}, - {file = "pyrsistent-0.18.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d45866ececf4a5fff8742c25722da6d4c9e180daa7b405dc0a2a2790d668c26"}, - {file = "pyrsistent-0.18.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4ed6784ceac462a7d6fcb7e9b663e93b9a6fb373b7f43594f9ff68875788e01e"}, - {file = "pyrsistent-0.18.1-cp310-cp310-win32.whl", hash = "sha256:e4f3149fd5eb9b285d6bfb54d2e5173f6a116fe19172686797c056672689daf6"}, - {file = "pyrsistent-0.18.1-cp310-cp310-win_amd64.whl", hash = "sha256:636ce2dc235046ccd3d8c56a7ad54e99d5c1cd0ef07d9ae847306c91d11b5fec"}, - {file = "pyrsistent-0.18.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e92a52c166426efbe0d1ec1332ee9119b6d32fc1f0bbfd55d5c1088070e7fc1b"}, - {file = "pyrsistent-0.18.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7a096646eab884bf8bed965bad63ea327e0d0c38989fc83c5ea7b8a87037bfc"}, - {file = "pyrsistent-0.18.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cdfd2c361b8a8e5d9499b9082b501c452ade8bbf42aef97ea04854f4a3f43b22"}, - {file = "pyrsistent-0.18.1-cp37-cp37m-win32.whl", hash = "sha256:7ec335fc998faa4febe75cc5268a9eac0478b3f681602c1f27befaf2a1abe1d8"}, - {file = "pyrsistent-0.18.1-cp37-cp37m-win_amd64.whl", hash = "sha256:6455fc599df93d1f60e1c5c4fe471499f08d190d57eca040c0ea182301321286"}, - {file = "pyrsistent-0.18.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fd8da6d0124efa2f67d86fa70c851022f87c98e205f0594e1fae044e7119a5a6"}, - {file = "pyrsistent-0.18.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bfe2388663fd18bd8ce7db2c91c7400bf3e1a9e8bd7d63bf7e77d39051b85ec"}, - {file = "pyrsistent-0.18.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e3e1fcc45199df76053026a51cc59ab2ea3fc7c094c6627e93b7b44cdae2c8c"}, - {file = "pyrsistent-0.18.1-cp38-cp38-win32.whl", hash = "sha256:b568f35ad53a7b07ed9b1b2bae09eb15cdd671a5ba5d2c66caee40dbf91c68ca"}, - {file = "pyrsistent-0.18.1-cp38-cp38-win_amd64.whl", hash = "sha256:d1b96547410f76078eaf66d282ddca2e4baae8964364abb4f4dcdde855cd123a"}, - {file = "pyrsistent-0.18.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f87cc2863ef33c709e237d4b5f4502a62a00fab450c9e020892e8e2ede5847f5"}, - {file = "pyrsistent-0.18.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bc66318fb7ee012071b2792024564973ecc80e9522842eb4e17743604b5e045"}, - {file = "pyrsistent-0.18.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:914474c9f1d93080338ace89cb2acee74f4f666fb0424896fcfb8d86058bf17c"}, - {file = "pyrsistent-0.18.1-cp39-cp39-win32.whl", hash = "sha256:1b34eedd6812bf4d33814fca1b66005805d3640ce53140ab8bbb1e2651b0d9bc"}, - {file = "pyrsistent-0.18.1-cp39-cp39-win_amd64.whl", hash = "sha256:e24a828f57e0c337c8d8bb9f6b12f09dfdf0273da25fda9e314f0b684b415a07"}, - {file = "pyrsistent-0.18.1.tar.gz", hash = "sha256:d4d61f8b993a7255ba714df3aca52700f8125289f84f704cf80916517c46eb96"}, -] pysftp = [ {file = "pysftp-0.2.9.tar.gz", hash = "sha256:fbf55a802e74d663673400acd92d5373c1c7ee94d765b428d9f977567ac4854a"}, ] From 6d787cadd1e21d966332d02b3c6f0915b15633ee Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 18 Mar 2022 15:07:30 +0000 Subject: [PATCH 023/109] Implemented render publishing --- openpype/hosts/unreal/api/pipeline.py | 22 ++++ .../unreal/plugins/create/create_render.py | 113 ++++++++++++++++++ .../plugins/publish/collect_instances.py | 2 +- .../plugins/publish/collect_remove_marked.py | 24 ++++ .../publish/collect_render_instances.py | 106 ++++++++++++++++ .../publish/validate_sequence_frames.py | 45 +++++++ openpype/plugins/publish/extract_review.py | 3 +- 7 files changed, 313 insertions(+), 2 deletions(-) create mode 100644 openpype/hosts/unreal/plugins/create/create_render.py create mode 100644 openpype/hosts/unreal/plugins/publish/collect_remove_marked.py create mode 100644 openpype/hosts/unreal/plugins/publish/collect_render_instances.py create mode 100644 openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py diff --git a/openpype/hosts/unreal/api/pipeline.py b/openpype/hosts/unreal/api/pipeline.py index 9ec11b942d..cf5ac6e4e0 100644 --- a/openpype/hosts/unreal/api/pipeline.py +++ b/openpype/hosts/unreal/api/pipeline.py @@ -47,6 +47,7 @@ def install(): print("installing OpenPype for Unreal ...") print("-=" * 40) logger.info("installing OpenPype for Unreal") + pyblish.api.register_host("unreal") pyblish.api.register_plugin_path(str(PUBLISH_PATH)) register_loader_plugin_path(str(LOAD_PATH)) api.register_plugin_path(LegacyCreator, str(CREATE_PATH)) @@ -416,3 +417,24 @@ def cast_map_to_str_dict(umap) -> dict: """ return {str(key): str(value) for (key, value) in umap.items()} + + +def get_subsequences(sequence: unreal.LevelSequence): + """Get list of subsequences from sequence. + + Args: + sequence (unreal.LevelSequence): Sequence + + Returns: + list(unreal.LevelSequence): List of subsequences + + """ + tracks = sequence.get_master_tracks() + subscene_track = None + for t in tracks: + if t.get_class() == unreal.MovieSceneSubTrack.static_class(): + subscene_track = t + break + if subscene_track is not None and subscene_track.get_sections(): + return subscene_track.get_sections() + return [] diff --git a/openpype/hosts/unreal/plugins/create/create_render.py b/openpype/hosts/unreal/plugins/create/create_render.py new file mode 100644 index 0000000000..49268c91f5 --- /dev/null +++ b/openpype/hosts/unreal/plugins/create/create_render.py @@ -0,0 +1,113 @@ +import unreal + +from openpype.hosts.unreal.api import pipeline +from openpype.hosts.unreal.api.plugin import Creator + + +class CreateRender(Creator): + """Create instance for sequence for rendering""" + + name = "unrealRender" + label = "Unreal - Render" + family = "render" + icon = "cube" + asset_types = ["LevelSequence"] + + root = "/Game/OpenPype/PublishInstances" + suffix = "_INS" + + def __init__(self, *args, **kwargs): + super(CreateRender, self).__init__(*args, **kwargs) + + def process(self): + subset = self.data["subset"] + + ar = unreal.AssetRegistryHelpers.get_asset_registry() + + # 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']}"], + 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']}"], + recursive_paths=False) + levels = ar.get_assets(filter) + ml = levels[0].get_editor_property('object_path') + + selection = [] + if (self.options or {}).get("useSelection"): + sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() + selection = [ + a.get_path_name() for a in sel_objects + if a.get_class().get_name() in self.asset_types] + else: + selection.append(self.data['sequence']) + + unreal.log(f"selection: {selection}") + + path = f"{self.root}" + unreal.EditorAssetLibrary.make_directory(path) + + ar = unreal.AssetRegistryHelpers.get_asset_registry() + + for a in selection: + ms_obj = ar.get_asset_by_object_path(ms).get_asset() + + seq_data = None + + if a == ms: + seq_data = { + "sequence": ms_obj, + "output": f"{ms_obj.get_name()}", + "frame_range": ( + ms_obj.get_playback_start(), ms_obj.get_playback_end()) + } + else: + seq_data_list = [{ + "sequence": ms_obj, + "output": f"{ms_obj.get_name()}", + "frame_range": ( + ms_obj.get_playback_start(), ms_obj.get_playback_end()) + }] + + for s in seq_data_list: + subscenes = pipeline.get_subsequences(s.get('sequence')) + + for ss in subscenes: + curr_data = { + "sequence": ss.get_sequence(), + "output": (f"{s.get('output')}/" + f"{ss.get_sequence().get_name()}"), + "frame_range": ( + ss.get_start_frame(), ss.get_end_frame() - 1) + } + + if ss.get_sequence().get_path_name() == a: + seq_data = curr_data + break + seq_data_list.append(curr_data) + + if seq_data is not None: + break + + if not seq_data: + continue + + d = self.data.copy() + d["members"] = [a] + d["sequence"] = a + d["master_sequence"] = ms + d["master_level"] = ml + d["output"] = seq_data.get('output') + d["frameStart"] = seq_data.get('frame_range')[0] + d["frameEnd"] = seq_data.get('frame_range')[1] + + container_name = f"{subset}{self.suffix}" + pipeline.create_publish_instance( + instance=container_name, path=path) + pipeline.imprint(f"{path}/{container_name}", d) diff --git a/openpype/hosts/unreal/plugins/publish/collect_instances.py b/openpype/hosts/unreal/plugins/publish/collect_instances.py index 94e732d728..2f604cb322 100644 --- a/openpype/hosts/unreal/plugins/publish/collect_instances.py +++ b/openpype/hosts/unreal/plugins/publish/collect_instances.py @@ -17,7 +17,7 @@ class CollectInstances(pyblish.api.ContextPlugin): """ label = "Collect Instances" - order = pyblish.api.CollectorOrder + order = pyblish.api.CollectorOrder - 0.1 hosts = ["unreal"] def process(self, context): diff --git a/openpype/hosts/unreal/plugins/publish/collect_remove_marked.py b/openpype/hosts/unreal/plugins/publish/collect_remove_marked.py new file mode 100644 index 0000000000..69e69f6630 --- /dev/null +++ b/openpype/hosts/unreal/plugins/publish/collect_remove_marked.py @@ -0,0 +1,24 @@ +import pyblish.api + + +class CollectRemoveMarked(pyblish.api.ContextPlugin): + """Remove marked data + + Remove instances that have 'remove' in their instance.data + + """ + + order = pyblish.api.CollectorOrder + 0.499 + label = 'Remove Marked Instances' + + def process(self, context): + + self.log.debug(context) + # make ftrack publishable + instances_to_remove = [] + for instance in context: + if instance.data.get('remove'): + instances_to_remove.append(instance) + + for instance in instances_to_remove: + context.remove(instance) diff --git a/openpype/hosts/unreal/plugins/publish/collect_render_instances.py b/openpype/hosts/unreal/plugins/publish/collect_render_instances.py new file mode 100644 index 0000000000..6eb51517c6 --- /dev/null +++ b/openpype/hosts/unreal/plugins/publish/collect_render_instances.py @@ -0,0 +1,106 @@ +from pathlib import Path +from tkinter.font import families +import unreal + +import pyblish.api +from openpype import lib +from openpype.pipeline import legacy_create +from openpype.hosts.unreal.api import pipeline + + +class CollectRenderInstances(pyblish.api.InstancePlugin): + """ This collector will try to find all the rendered frames. + + """ + order = pyblish.api.CollectorOrder + hosts = ["unreal"] + families = ["render"] + label = "Collect Render Instances" + + def process(self, instance): + self.log.debug("Preparing Rendering Instances") + + context = instance.context + + data = instance.data + data['remove'] = True + + ar = unreal.AssetRegistryHelpers.get_asset_registry() + + sequence = ar.get_asset_by_object_path( + data.get('sequence')).get_asset() + + sequences = [{ + "sequence": sequence, + "output": data.get('output'), + "frame_range": ( + data.get('frameStart'), data.get('frameEnd')) + }] + + for s in sequences: + self.log.debug(f"Processing: {s.get('sequence').get_name()}") + subscenes = pipeline.get_subsequences(s.get('sequence')) + + if subscenes: + for ss in subscenes: + sequences.append({ + "sequence": ss.get_sequence(), + "output": (f"{s.get('output')}/" + f"{ss.get_sequence().get_name()}"), + "frame_range": ( + ss.get_start_frame(), ss.get_end_frame() - 1) + }) + else: + # Avoid creating instances for camera sequences + if "_camera" not in s.get('sequence').get_name(): + seq = s.get('sequence') + seq_name = seq.get_name() + + new_instance = context.create_instance( + f"{data.get('subset')}_" + f"{seq_name}") + new_instance[:] = seq_name + + new_data = new_instance.data + + new_data["asset"] = seq_name + new_data["setMembers"] = seq_name + new_data["family"] = "render" + new_data["families"] = ["render", "review"] + new_data["parent"] = data.get("parent") + new_data["subset"] = f"{data.get('subset')}_{seq_name}" + new_data["level"] = data.get("level") + new_data["output"] = s.get('output') + new_data["fps"] = seq.get_display_rate().numerator + new_data["frameStart"] = s.get('frame_range')[0] + new_data["frameEnd"] = s.get('frame_range')[1] + new_data["sequence"] = seq.get_path_name() + new_data["master_sequence"] = data["master_sequence"] + new_data["master_level"] = data["master_level"] + + self.log.debug(f"new instance data: {new_data}") + + project_dir = unreal.Paths.project_dir() + render_dir = (f"{project_dir}/Saved/MovieRenders/" + f"{s.get('output')}") + render_path = Path(render_dir) + + frames = [] + + for x in render_path.iterdir(): + if x.is_file() and x.suffix == '.png': + frames.append(str(x.name)) + + if "representations" not in new_instance.data: + new_instance.data["representations"] = [] + + repr = { + 'frameStart': s.get('frame_range')[0], + 'frameEnd': s.get('frame_range')[1], + 'name': 'png', + 'ext': 'png', + 'files': frames, + 'stagingDir': render_dir, + 'tags': ['review'] + } + new_instance.data["representations"].append(repr) diff --git a/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py b/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py new file mode 100644 index 0000000000..0a77281d16 --- /dev/null +++ b/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py @@ -0,0 +1,45 @@ +from pathlib import Path + +import unreal + +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): + self.log.debug(instance.data) + + representations = instance.data.get("representations") + for repr in representations: + frames = [] + for x in repr.get("files"): + # Get frame number. The last one contains the file extension, + # while the one before that is the frame number. + # `lstrip` removes any leading zeros. `or "0"` is to tackle + # the case where the frame number is "00". + frame = int(str(x).split('.')[-2]) + frames.append(frame) + frames.sort() + current_range = (frames[0], frames[-1]) + required_range = (instance.data["frameStart"], + instance.data["frameEnd"]) + + if current_range != required_range: + raise ValueError(f"Invalid frame range: {current_range} - " + f"expected: {required_range}") + + assert len(frames) == int(frames[-1]) - int(frames[0]) + 1, \ + "Missing frames" diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index 3ecea1f8bd..35ad6270cf 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -51,7 +51,8 @@ class ExtractReview(pyblish.api.InstancePlugin): "resolve", "webpublisher", "aftereffects", - "flame" + "flame", + "unreal" ] # Supported extensions From b9f387dc505664f0c52c6a45330cbc2c6786a611 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 18 Mar 2022 16:09:53 +0000 Subject: [PATCH 024/109] Hound fix --- .../hosts/unreal/plugins/publish/collect_render_instances.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/openpype/hosts/unreal/plugins/publish/collect_render_instances.py b/openpype/hosts/unreal/plugins/publish/collect_render_instances.py index 6eb51517c6..9d60b65d08 100644 --- a/openpype/hosts/unreal/plugins/publish/collect_render_instances.py +++ b/openpype/hosts/unreal/plugins/publish/collect_render_instances.py @@ -1,10 +1,7 @@ from pathlib import Path -from tkinter.font import families import unreal import pyblish.api -from openpype import lib -from openpype.pipeline import legacy_create from openpype.hosts.unreal.api import pipeline From 9fab478edf926bbe45dcfa294d2b53c767ebf086 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 18 Mar 2022 16:54:20 +0000 Subject: [PATCH 025/109] Improvements and more consistency in validator for rendered frames --- .../publish/validate_sequence_frames.py | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py b/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py index 0a77281d16..2684581e9d 100644 --- a/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py +++ b/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py @@ -1,4 +1,5 @@ from pathlib import Path +import clique import unreal @@ -20,19 +21,17 @@ class ValidateSequenceFrames(pyblish.api.InstancePlugin): optional = True def process(self, instance): - self.log.debug(instance.data) - representations = instance.data.get("representations") for repr in representations: - frames = [] - for x in repr.get("files"): - # Get frame number. The last one contains the file extension, - # while the one before that is the frame number. - # `lstrip` removes any leading zeros. `or "0"` is to tackle - # the case where the frame number is "00". - frame = int(str(x).split('.')[-2]) - frames.append(frame) - frames.sort() + 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 = (instance.data["frameStart"], instance.data["frameEnd"]) @@ -41,5 +40,5 @@ class ValidateSequenceFrames(pyblish.api.InstancePlugin): raise ValueError(f"Invalid frame range: {current_range} - " f"expected: {required_range}") - assert len(frames) == int(frames[-1]) - int(frames[0]) + 1, \ - "Missing frames" + missing = collection.holes().indexes + assert not missing, "Missing frames: %s" % (missing,) From 541f44988dc654bd2609d865875a1ed47908014e Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 18 Mar 2022 16:55:54 +0000 Subject: [PATCH 026/109] More hound fixes --- .../hosts/unreal/plugins/publish/validate_sequence_frames.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py b/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py index 2684581e9d..87f1338ee8 100644 --- a/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py +++ b/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py @@ -1,8 +1,5 @@ -from pathlib import Path import clique -import unreal - import pyblish.api From 0274be6bed9c7bf92c862ae33c0eab0db3b88eb4 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 18 Mar 2022 17:22:52 +0000 Subject: [PATCH 027/109] Added rendering --- openpype/hosts/unreal/api/rendering.py | 125 +++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 openpype/hosts/unreal/api/rendering.py diff --git a/openpype/hosts/unreal/api/rendering.py b/openpype/hosts/unreal/api/rendering.py new file mode 100644 index 0000000000..376e1b75ce --- /dev/null +++ b/openpype/hosts/unreal/api/rendering.py @@ -0,0 +1,125 @@ +import unreal + +from openpype.hosts.unreal.api import pipeline + + +queue = None +executor = None + + +def _queue_finish_callback(exec, success): + unreal.log("Render completed. Success: " + str(success)) + + # Delete our reference so we don't keep it alive. + global executor + global queue + del executor + del queue + + +def _job_finish_callback(job, success): + # You can make any edits you want to the editor world here, and the world + # will be duplicated when the next render happens. Make sure you undo your + # edits in OnQueueFinishedCallback if you don't want to leak state changes + # into the editor world. + unreal.log("Individual job completed.") + + +def start_rendering(): + """ + Start the rendering process. + """ + print("Starting rendering...") + + # Get selected sequences + assets = unreal.EditorUtilityLibrary.get_selected_assets() + + # instances = pipeline.ls_inst() + instances = [ + a for a in assets + if a.get_class().get_name() == "OpenPypePublishInstance"] + + inst_data = [] + + for i in instances: + data = pipeline.parse_container(i.get_path_name()) + if data["family"] == "render": + inst_data.append(data) + + # subsystem = unreal.get_editor_subsystem( + # unreal.MoviePipelineQueueSubsystem) + # queue = subsystem.get_queue() + global queue + queue = unreal.MoviePipelineQueue() + + ar = unreal.AssetRegistryHelpers.get_asset_registry() + + for i in inst_data: + sequence = ar.get_asset_by_object_path(i["sequence"]).get_asset() + + sequences = [{ + "sequence": sequence, + "output": f"{i['output']}", + "frame_range": ( + int(float(i["frameStart"])), + int(float(i["frameEnd"])) + 1) + }] + render_list = [] + + # 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')) + + if subscenes: + for ss in subscenes: + sequences.append({ + "sequence": ss.get_sequence(), + "output": (f"{s.get('output')}/" + f"{ss.get_sequence().get_name()}"), + "frame_range": ( + ss.get_start_frame(), ss.get_end_frame()) + }) + else: + # Avoid rendering camera sequences + if "_camera" not in s.get('sequence').get_name(): + render_list.append(s) + + # Create the rendering jobs and add them to the queue. + for r 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" + + # 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 = "" + + 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.use_custom_playback_range = True + settings.file_name_format = "{sequence_name}.{frame_number}" + settings.output_directory.path += r.get('output') + + renderPass = job.get_configuration().find_or_add_setting_by_class( + unreal.MoviePipelineDeferredPassBase) + renderPass.disable_multisample_effects = True + + job.get_configuration().find_or_add_setting_by_class( + unreal.MoviePipelineImageSequenceOutput_PNG) + + # If there are jobs in the queue, start the rendering process. + if queue.get_jobs(): + global executor + executor = unreal.MoviePipelinePIEExecutor() + executor.on_executor_finished_delegate.add_callable_unique( + _queue_finish_callback) + executor.on_individual_job_finished_delegate.add_callable_unique( + _job_finish_callback) # Only available on PIE Executor + executor.execute(queue) From a8680e9f23e885bd3a1957876198b9c249f23fdb Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 18 Mar 2022 17:25:03 +0000 Subject: [PATCH 028/109] Code cleanup --- openpype/hosts/unreal/plugins/create/create_render.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/openpype/hosts/unreal/plugins/create/create_render.py b/openpype/hosts/unreal/plugins/create/create_render.py index 49268c91f5..77fc98bcec 100644 --- a/openpype/hosts/unreal/plugins/create/create_render.py +++ b/openpype/hosts/unreal/plugins/create/create_render.py @@ -16,9 +16,6 @@ class CreateRender(Creator): root = "/Game/OpenPype/PublishInstances" suffix = "_INS" - def __init__(self, *args, **kwargs): - super(CreateRender, self).__init__(*args, **kwargs) - def process(self): subset = self.data["subset"] From 8f8a4efab9ccf7c30089e78d6dcffb4b76142ce3 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 18 Mar 2022 17:39:43 +0000 Subject: [PATCH 029/109] Fixed import problems --- .../unreal/plugins/load/load_animation.py | 29 +++++++++++-------- .../hosts/unreal/plugins/load/load_layout.py | 1 + 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_animation.py b/openpype/hosts/unreal/plugins/load/load_animation.py index ebfce75ca9..65a9de9353 100644 --- a/openpype/hosts/unreal/plugins/load/load_animation.py +++ b/openpype/hosts/unreal/plugins/load/load_animation.py @@ -3,6 +3,11 @@ import os import json +import unreal +from unreal import EditorAssetLibrary +from unreal import MovieSceneSkeletalAnimationTrack +from unreal import MovieSceneSkeletalAnimationSection + from avalon import pipeline from openpype.pipeline import get_representation_path from openpype.hosts.unreal.api import plugin @@ -82,14 +87,14 @@ class AnimationFBXLoader(plugin.Loader): unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) - asset_content = unreal.EditorAssetLibrary.list_assets( + asset_content = EditorAssetLibrary.list_assets( asset_dir, recursive=True, include_folder=True ) animation = None for a in asset_content: - imported_asset_data = unreal.EditorAssetLibrary.find_asset_data(a) + imported_asset_data = EditorAssetLibrary.find_asset_data(a) imported_asset = unreal.AssetRegistryHelpers.get_asset( imported_asset_data) if imported_asset.__class__ == unreal.AnimSequence: @@ -149,7 +154,7 @@ class AnimationFBXLoader(plugin.Loader): container_name += suffix - unreal.EditorAssetLibrary.make_directory(asset_dir) + EditorAssetLibrary.make_directory(asset_dir) libpath = self.fname.replace("fbx", "json") @@ -160,7 +165,7 @@ class AnimationFBXLoader(plugin.Loader): animation = self._process(asset_dir, container_name, instance_name) - asset_content = unreal.EditorAssetLibrary.list_assets( + asset_content = EditorAssetLibrary.list_assets( hierarchy_dir, recursive=True, include_folder=False) # Get the sequence for the layout, excluding the camera one. @@ -211,11 +216,11 @@ class AnimationFBXLoader(plugin.Loader): unreal_pipeline.imprint( "{}/{}".format(asset_dir, container_name), data) - imported_content = unreal.EditorAssetLibrary.list_assets( + imported_content = EditorAssetLibrary.list_assets( asset_dir, recursive=True, include_folder=False) for a in imported_content: - unreal.EditorAssetLibrary.save_asset(a) + EditorAssetLibrary.save_asset(a) def update(self, container, representation): name = container["asset_name"] @@ -261,7 +266,7 @@ class AnimationFBXLoader(plugin.Loader): task.options.anim_sequence_import_data.set_editor_property( 'convert_scene', True) - skeletal_mesh = unreal.EditorAssetLibrary.load_asset( + skeletal_mesh = EditorAssetLibrary.load_asset( container.get('namespace') + "/" + container.get('asset_name')) skeleton = skeletal_mesh.get_editor_property('skeleton') task.options.set_editor_property('skeleton', skeleton) @@ -278,22 +283,22 @@ class AnimationFBXLoader(plugin.Loader): "parent": str(representation["parent"]) }) - asset_content = unreal.EditorAssetLibrary.list_assets( + asset_content = EditorAssetLibrary.list_assets( destination_path, recursive=True, include_folder=True ) for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) + EditorAssetLibrary.save_asset(a) def remove(self, container): path = container["namespace"] parent_path = os.path.dirname(path) - unreal.EditorAssetLibrary.delete_directory(path) + EditorAssetLibrary.delete_directory(path) - asset_content = unreal.EditorAssetLibrary.list_assets( + asset_content = EditorAssetLibrary.list_assets( parent_path, recursive=False, include_folder=True ) if len(asset_content) == 0: - unreal.EditorAssetLibrary.delete_directory(parent_path) + EditorAssetLibrary.delete_directory(parent_path) diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index 5a82ad6df6..86923ea3b4 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -12,6 +12,7 @@ from unreal import AssetToolsHelpers from unreal import FBXImportType from unreal import MathLibrary as umath +from avalon import io from avalon.pipeline import AVALON_CONTAINER_ID from openpype.pipeline import ( discover_loader_plugins, From 1c153ebb6089664e9c841f0cafb70cba1192149b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 13 Apr 2022 21:33:02 +0200 Subject: [PATCH 030/109] flame: remove clip in reels dependency --- .../publish/collect_timeline_instances.py | 13 ---------- .../plugins/publish/validate_source_clip.py | 26 ------------------- 2 files changed, 39 deletions(-) delete mode 100644 openpype/hosts/flame/plugins/publish/validate_source_clip.py diff --git a/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py b/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py index 95c2002bd9..bc849a4742 100644 --- a/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py +++ b/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py @@ -31,7 +31,6 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin): self.log.debug("__ selected_segments: {}".format(selected_segments)) self.otio_timeline = context.data["otioTimeline"] - self.clips_in_reels = opfapi.get_clips_in_reels(project) self.fps = context.data["fps"] # process all sellected @@ -63,9 +62,6 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin): # get file path file_path = clip_data["fpath"] - # get source clip - source_clip = self._get_reel_clip(file_path) - first_frame = opfapi.get_frame_from_filename(file_path) or 0 head, tail = self._get_head_tail(clip_data, first_frame) @@ -103,7 +99,6 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin): "families": families, "publish": marker_data["publish"], "fps": self.fps, - "flameSourceClip": source_clip, "sourceFirstFrame": int(first_frame), "path": file_path, "flameAddTasks": self.add_tasks, @@ -258,14 +253,6 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin): ) return head, tail - def _get_reel_clip(self, path): - match_reel_clip = [ - clip for clip in self.clips_in_reels - if clip["fpath"] == path - ] - if match_reel_clip: - return match_reel_clip.pop() - def _get_resolution_to_data(self, data, context): assert data.get("otioClip"), "Missing `otioClip` data" diff --git a/openpype/hosts/flame/plugins/publish/validate_source_clip.py b/openpype/hosts/flame/plugins/publish/validate_source_clip.py deleted file mode 100644 index 345c00e05a..0000000000 --- a/openpype/hosts/flame/plugins/publish/validate_source_clip.py +++ /dev/null @@ -1,26 +0,0 @@ -import pyblish - - -@pyblish.api.log -class ValidateSourceClip(pyblish.api.InstancePlugin): - """Validate instance is not having empty `flameSourceClip`""" - - order = pyblish.api.ValidatorOrder - label = "Validate Source Clip" - hosts = ["flame"] - families = ["clip"] - optional = True - active = False - - def process(self, instance): - flame_source_clip = instance.data["flameSourceClip"] - - self.log.debug("_ flame_source_clip: {}".format(flame_source_clip)) - - if flame_source_clip is None: - raise AttributeError(( - "Timeline segment `{}` is not having " - "relative clip in reels. Please make sure " - "you push `Save Sources` button in Conform Tab").format( - instance.data["asset"] - )) From f245ca5073a68fcdae21045b45db6ad390c751ca Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 13 Apr 2022 21:34:23 +0200 Subject: [PATCH 031/109] flame: refectory of extractor settings --- .../defaults/project_settings/flame.json | 16 +- .../projects_schema/schema_project_flame.json | 147 ++++++++++++------ 2 files changed, 106 insertions(+), 57 deletions(-) diff --git a/openpype/settings/defaults/project_settings/flame.json b/openpype/settings/defaults/project_settings/flame.json index ef7a2a4467..028fda2e66 100644 --- a/openpype/settings/defaults/project_settings/flame.json +++ b/openpype/settings/defaults/project_settings/flame.json @@ -55,16 +55,18 @@ "keep_original_representation": false, "export_presets_mapping": { "exr16fpdwaa": { + "active": true, + "export_type": "File Sequence", "ext": "exr", "xml_preset_file": "OpenEXR (16-bit fp DWAA).xml", - "xml_preset_dir": "", - "export_type": "File Sequence", - "ignore_comment_attrs": false, "colorspace_out": "ACES - ACEScg", + "xml_preset_dir": "", + "parsed_comment_attrs": true, "representation_add_range": true, "representation_tags": [], "load_to_batch_group": true, - "batch_group_loader_name": "LoadClip" + "batch_group_loader_name": "LoadClipBatch", + "filter_path_regex": ".*" } } } @@ -87,7 +89,8 @@ "png", "h264", "mov", - "mp4" + "mp4", + "exr16fpdwaa" ], "reel_group_name": "OpenPype_Reels", "reel_name": "Loaded", @@ -110,7 +113,8 @@ "png", "h264", "mov", - "mp4" + "mp4", + "exr16fpdwaa" ], "reel_name": "OP_LoadedReel", "clip_name_template": "{asset}_{subset}_{output}" diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json b/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json index fe11d63ac2..fcbbddbe29 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json @@ -238,25 +238,19 @@ "type": "dict", "children": [ { - "key": "ext", - "label": "Output extension", - "type": "text" + "type": "boolean", + "key": "active", + "label": "Is active", + "default": true }, { - "key": "xml_preset_file", - "label": "XML preset file (with ext)", - "type": "text" - }, - { - "key": "xml_preset_dir", - "label": "XML preset folder (optional)", - "type": "text" + "type": "separator" }, { "key": "export_type", "label": "Eport clip type", "type": "enum", - "default": "File Sequence", + "default": "Sequence Publish", "enum_items": [ { "Movie": "Movie" @@ -268,54 +262,105 @@ "Sequence Publish": "Sequence Publish" } ] - }, { - "type": "separator" + "key": "ext", + "label": "Output extension", + "type": "text", + "default": "exr" }, { - "type": "boolean", - "key": "ignore_comment_attrs", - "label": "Ignore attributes parsed from a segment comments" - }, - { - "type": "separator" + "key": "xml_preset_file", + "label": "XML preset file (with ext)", + "type": "text" }, { "key": "colorspace_out", "label": "Output color (imageio)", - "type": "text" - }, - { - "type": "separator" - }, - { - "type": "boolean", - "key": "representation_add_range", - "label": "Add frame range to representation" - }, - { - "type": "list", - "key": "representation_tags", - "label": "Add representation tags", - "object_type": { - "type": "text", - "multiline": false - } - }, - { - "type": "separator" - }, - { - "type": "boolean", - "key": "load_to_batch_group", - "label": "Load to batch group reel", - "default": false - }, - { "type": "text", - "key": "batch_group_loader_name", - "label": "Use loader name" + "default": "linear" + }, + { + "type": "collapsible-wrap", + "label": "Other parameters", + "collapsible": true, + "collapsed": true, + "children": [ + { + "key": "xml_preset_dir", + "label": "XML preset folder (optional)", + "type": "text" + }, + { + "type": "separator" + }, + { + "type": "boolean", + "key": "parsed_comment_attrs", + "label": "Include parsed attributes from comments", + "default": false + + }, + { + "type": "separator" + }, + { + "type": "collapsible-wrap", + "label": "Representation", + "collapsible": true, + "collapsed": true, + "children": [ + { + "type": "boolean", + "key": "representation_add_range", + "label": "Add frame range to representation" + }, + { + "type": "list", + "key": "representation_tags", + "label": "Add representation tags", + "object_type": { + "type": "text", + "multiline": false + } + } + ] + }, + { + "type": "collapsible-wrap", + "label": "Loading during publish", + "collapsible": true, + "collapsed": true, + "children": [ + { + "type": "boolean", + "key": "load_to_batch_group", + "label": "Load to batch group reel", + "default": false + }, + { + "type": "text", + "key": "batch_group_loader_name", + "label": "Use loader name" + } + ] + } + + ] + }, + { + "type": "collapsible-wrap", + "label": "Filtering", + "collapsible": true, + "collapsed": true, + "children": [ + { + "key": "filter_path_regex", + "label": "Regex in clip path", + "type": "text", + "default": ".*" + } + ] } ] } From 9174e437a6d4f5cff2df2d0de9cd19ece954ec89 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 13 Apr 2022 21:38:47 +0200 Subject: [PATCH 032/109] flame: add new settings with filter - removing reel clip dependency --- .../publish/extract_subset_resources.py | 278 ++++++++++-------- 1 file changed, 154 insertions(+), 124 deletions(-) diff --git a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py index a780f8c9e5..f1eca9a67d 100644 --- a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py +++ b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py @@ -1,4 +1,5 @@ import os +import re from pprint import pformat from copy import deepcopy @@ -6,6 +7,8 @@ import pyblish.api import openpype.api from openpype.hosts.flame import api as opfapi +import flame + class ExtractSubsetResources(openpype.api.Extractor): """ @@ -20,27 +23,31 @@ class ExtractSubsetResources(openpype.api.Extractor): # plugin defaults default_presets = { "thumbnail": { + "active": True, "ext": "jpg", "xml_preset_file": "Jpeg (8-bit).xml", "xml_preset_dir": "", "export_type": "File Sequence", - "ignore_comment_attrs": True, + "parsed_comment_attrs": False, "colorspace_out": "Output - sRGB", "representation_add_range": False, - "representation_tags": ["thumbnail"] + "representation_tags": ["thumbnail"], + "path_regex": ".*" }, "ftrackpreview": { + "active": True, "ext": "mov", "xml_preset_file": "Apple iPad (1920x1080).xml", "xml_preset_dir": "", "export_type": "Movie", - "ignore_comment_attrs": True, + "parsed_comment_attrs": False, "colorspace_out": "Output - Rec.709", "representation_add_range": True, "representation_tags": [ "review", "delete" - ] + ], + "path_regex": ".*" } } keep_original_representation = False @@ -62,12 +69,8 @@ class ExtractSubsetResources(openpype.api.Extractor): # flame objects segment = instance.data["item"] segment_name = segment.name.get_value() + clip_path = instance.data["path"] sequence_clip = instance.context.data["flameSequence"] - clip_data = instance.data["flameSourceClip"] - - reel_clip = None - if clip_data: - reel_clip = clip_data["PyClip"] # segment's parent track name s_track_name = segment.parent.name.get_value() @@ -104,14 +107,41 @@ class ExtractSubsetResources(openpype.api.Extractor): for unique_name, preset_config in export_presets.items(): modify_xml_data = {} + # get activating attributes + activated_preset = preset_config["active"] + filter_path_regex = preset_config["filter_path_regex"] + + self.log.info( + "Preset `{}` is active `{}` with filter `{}`".format( + unique_name, activated_preset, filter_path_regex + ) + ) + self.log.debug( + "__ clip_path: `{}`".format(clip_path)) + + # skip if not activated presete + if not activated_preset: + continue + + # exclude by regex filter + if not re.search(filter_path_regex, clip_path): + continue + # get all presets attributes + extension = preset_config["ext"] preset_file = preset_config["xml_preset_file"] preset_dir = preset_config["xml_preset_dir"] export_type = preset_config["export_type"] repre_tags = preset_config["representation_tags"] - ignore_comment_attrs = preset_config["ignore_comment_attrs"] + parsed_comment_attrs = preset_config["parsed_comment_attrs"] color_out = preset_config["colorspace_out"] + self.log.info( + "Processing `{}` as `{}` to `{}` type...".format( + preset_file, export_type, extension + ) + ) + # get attribures related loading in integrate_batch_group load_to_batch_group = preset_config.get( "load_to_batch_group") @@ -131,24 +161,14 @@ class ExtractSubsetResources(openpype.api.Extractor): in_mark = (source_start_handles - source_first_frame) + 1 out_mark = in_mark + source_duration_handles - # make test for type of preset and available reel_clip - if ( - not reel_clip - and export_type != "Sequence Publish" - ): - self.log.warning(( - "Skipping preset {}. Not available " - "reel clip for {}").format( - preset_file, segment_name - )) - continue - - # by default export source clips - exporting_clip = reel_clip - + exporting_clip = None if export_type == "Sequence Publish": # change export clip to sequence - exporting_clip = sequence_clip + exporting_clip = flame.duplicate(sequence_clip) + + # only keep visible layer where instance segment is child + self.hide_others( + exporting_clip, segment_name, s_track_name) # change in/out marks to timeline in/out in_mark = clip_in @@ -161,131 +181,126 @@ class ExtractSubsetResources(openpype.api.Extractor): "startFrame": frame_start }) - if not ignore_comment_attrs: + if parsed_comment_attrs: # add any xml overrides collected form segment.comment modify_xml_data.update(instance.data["xml_overrides"]) self.log.debug("__ modify_xml_data: {}".format(pformat( modify_xml_data ))) + else: + exporting_clip = self.import_clip(clip_path) - # with maintained duplication loop all presets - with opfapi.maintained_object_duplication( - exporting_clip) as duplclip: - kwargs = {} + export_kwargs = {} + # validate xml preset file is filled + if preset_file == "": + raise ValueError( + ("Check Settings for {} preset: " + "`XML preset file` is not filled").format( + unique_name) + ) - if export_type == "Sequence Publish": - # only keep visible layer where instance segment is child - self.hide_others(duplclip, segment_name, s_track_name) + # resolve xml preset dir if not filled + if preset_dir == "": + preset_dir = opfapi.get_preset_path_by_xml_name( + preset_file) - # validate xml preset file is filled - if preset_file == "": + if not preset_dir: raise ValueError( ("Check Settings for {} preset: " - "`XML preset file` is not filled").format( - unique_name) + "`XML preset file` {} is not found").format( + unique_name, preset_file) ) - # resolve xml preset dir if not filled - if preset_dir == "": - preset_dir = opfapi.get_preset_path_by_xml_name( - preset_file) + # create preset path + preset_orig_xml_path = str(os.path.join( + preset_dir, preset_file + )) - if not preset_dir: - raise ValueError( - ("Check Settings for {} preset: " - "`XML preset file` {} is not found").format( - unique_name, preset_file) - ) + preset_path = opfapi.modify_preset_file( + preset_orig_xml_path, staging_dir, modify_xml_data) - # create preset path - preset_orig_xml_path = str(os.path.join( - preset_dir, preset_file - )) + # define kwargs based on preset type + if "thumbnail" in unique_name: + export_kwargs["thumb_frame_number"] = in_mark + ( + source_duration_handles / 2) + else: + export_kwargs.update({ + "in_mark": in_mark, + "out_mark": out_mark + }) - preset_path = opfapi.modify_preset_file( - preset_orig_xml_path, staging_dir, modify_xml_data) + # get and make export dir paths + export_dir_path = str(os.path.join( + staging_dir, unique_name + )) + os.makedirs(export_dir_path) - # define kwargs based on preset type - if "thumbnail" in unique_name: - kwargs["thumb_frame_number"] = in_mark + ( - source_duration_handles / 2) - else: - kwargs.update({ - "in_mark": in_mark, - "out_mark": out_mark - }) + # export + opfapi.export_clip( + export_dir_path, exporting_clip, preset_path, **export_kwargs) - # get and make export dir paths - export_dir_path = str(os.path.join( - staging_dir, unique_name - )) - os.makedirs(export_dir_path) + # create representation data + representation_data = { + "name": unique_name, + "outputName": unique_name, + "ext": extension, + "stagingDir": export_dir_path, + "tags": repre_tags, + "data": { + "colorspace": color_out + }, + "load_to_batch_group": load_to_batch_group, + "batch_group_loader_name": batch_group_loader_name + } - # export - opfapi.export_clip( - export_dir_path, duplclip, preset_path, **kwargs) + # collect all available content of export dir + files = os.listdir(export_dir_path) - extension = preset_config["ext"] + # make sure no nested folders inside + n_stage_dir, n_files = self._unfolds_nested_folders( + export_dir_path, files, extension) - # create representation data - representation_data = { - "name": unique_name, - "outputName": unique_name, - "ext": extension, - "stagingDir": export_dir_path, - "tags": repre_tags, - "data": { - "colorspace": color_out - }, - "load_to_batch_group": load_to_batch_group, - "batch_group_loader_name": batch_group_loader_name - } + # fix representation in case of nested folders + if n_stage_dir: + representation_data["stagingDir"] = n_stage_dir + files = n_files - # collect all available content of export dir - files = os.listdir(export_dir_path) + # add files to represetation but add + # imagesequence as list + if ( + # first check if path in files is not mov extension + [ + f for f in files + if os.path.splitext(f)[-1] == ".mov" + ] + # then try if thumbnail is not in unique name + or unique_name == "thumbnail" + ): + representation_data["files"] = files.pop() + else: + representation_data["files"] = files - # make sure no nested folders inside - n_stage_dir, n_files = self._unfolds_nested_folders( - export_dir_path, files, extension) + # add frame range + if preset_config["representation_add_range"]: + representation_data.update({ + "frameStart": frame_start_handle, + "frameEnd": ( + frame_start_handle + source_duration_handles), + "fps": instance.data["fps"] + }) - # fix representation in case of nested folders - if n_stage_dir: - representation_data["stagingDir"] = n_stage_dir - files = n_files + instance.data["representations"].append(representation_data) - # add files to represetation but add - # imagesequence as list - if ( - # first check if path in files is not mov extension - [ - f for f in files - if os.path.splitext(f)[-1] == ".mov" - ] - # then try if thumbnail is not in unique name - or unique_name == "thumbnail" - ): - representation_data["files"] = files.pop() - else: - representation_data["files"] = files + # add review family if found in tags + if "review" in repre_tags: + instance.data["families"].append("review") - # add frame range - if preset_config["representation_add_range"]: - representation_data.update({ - "frameStart": frame_start_handle, - "frameEnd": ( - frame_start_handle + source_duration_handles), - "fps": instance.data["fps"] - }) + self.log.info("Added representation: {}".format( + representation_data)) - instance.data["representations"].append(representation_data) - - # add review family if found in tags - if "review" in repre_tags: - instance.data["families"].append("review") - - self.log.info("Added representation: {}".format( - representation_data)) + # at the end remove the duplicated clip + flame.delete(exporting_clip) self.log.debug("All representations: {}".format( pformat(instance.data["representations"]))) @@ -373,3 +388,18 @@ class ExtractSubsetResources(openpype.api.Extractor): for segment in track.segments: if segment.name.get_value() != segment_name: segment.hidden = True + + def import_clip(self, path): + """ + Import clip from path + """ + clips = flame.import_clips(path) + self.log.info("Clips [{}] imported from `{}`".format(clips, path)) + if not clips: + self.log.warning("Path `{}` is not having any clips".format(path)) + return None + elif len(clips) > 1: + self.log.warning( + "Path `{}` is containing more that one clip".format(path) + ) + return clips.pop() From b086680289c2a897164d9db5f868c1c5d78690e6 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 14 Apr 2022 08:55:30 +0200 Subject: [PATCH 033/109] flame: improving work with presets --- .../publish/extract_subset_resources.py | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py index f1eca9a67d..ba4a8c41ad 100644 --- a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py +++ b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py @@ -169,27 +169,30 @@ class ExtractSubsetResources(openpype.api.Extractor): # only keep visible layer where instance segment is child self.hide_others( exporting_clip, segment_name, s_track_name) + else: + exporting_clip = self.import_clip(clip_path) - # change in/out marks to timeline in/out - in_mark = clip_in - out_mark = clip_out + # change in/out marks to timeline in/out + in_mark = clip_in + out_mark = clip_out - # add xml tags modifications - modify_xml_data.update({ - "exportHandles": True, - "nbHandles": handles, - "startFrame": frame_start - }) + # add xml tags modifications + modify_xml_data.update({ + "exportHandles": True, + "nbHandles": handles, + "startFrame": frame_start, + "namePattern": ( + "<segment name>_<shot name>_{}.").format( + unique_name) + }) - if parsed_comment_attrs: - # add any xml overrides collected form segment.comment - modify_xml_data.update(instance.data["xml_overrides"]) + if parsed_comment_attrs: + # add any xml overrides collected form segment.comment + modify_xml_data.update(instance.data["xml_overrides"]) self.log.debug("__ modify_xml_data: {}".format(pformat( modify_xml_data ))) - else: - exporting_clip = self.import_clip(clip_path) export_kwargs = {} # validate xml preset file is filled From b3b1938a43d2832be5f72c2439f1701ec229c977 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 14 Apr 2022 11:44:13 +0200 Subject: [PATCH 034/109] flame: IntegrateBatchGroup disable from settings --- .../settings/defaults/project_settings/flame.json | 3 +++ .../projects_schema/schema_project_flame.json | 15 +++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/openpype/settings/defaults/project_settings/flame.json b/openpype/settings/defaults/project_settings/flame.json index 028fda2e66..dd8c05d460 100644 --- a/openpype/settings/defaults/project_settings/flame.json +++ b/openpype/settings/defaults/project_settings/flame.json @@ -69,6 +69,9 @@ "filter_path_regex": ".*" } } + }, + "IntegrateBatchGroup": { + "enabled": false } }, "load": { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json b/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json index fcbbddbe29..ace404b47a 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json @@ -366,6 +366,21 @@ } } ] + }, + { + "type": "dict", + "collapsible": true, + "key": "IntegrateBatchGroup", + "label": "IntegrateBatchGroup", + "is_group": true, + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + } + ] } ] }, From 891ba74d6c6e2d7370a9c8eec1b13e1a311b8094 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 14 Apr 2022 11:46:16 +0200 Subject: [PATCH 035/109] flame: no need to assign project object anymore --- .../hosts/flame/plugins/publish/collect_timeline_instances.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py b/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py index bc849a4742..5174f9db48 100644 --- a/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py +++ b/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py @@ -26,7 +26,6 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin): add_tasks = [] def process(self, context): - project = context.data["flameProject"] selected_segments = context.data["flameSelectedSegments"] self.log.debug("__ selected_segments: {}".format(selected_segments)) From 30a959f429072449dbadf7d7425a181eb9a6cb7b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 14 Apr 2022 11:50:35 +0200 Subject: [PATCH 036/109] flame: improving extractor's preset filtering --- .../publish/extract_subset_resources.py | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py index ba4a8c41ad..4598405923 100644 --- a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py +++ b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py @@ -68,6 +68,7 @@ class ExtractSubsetResources(openpype.api.Extractor): # flame objects segment = instance.data["item"] + asset_name = instance.data["asset"] segment_name = segment.name.get_value() clip_path = instance.data["path"] sequence_clip = instance.context.data["flameSequence"] @@ -109,7 +110,7 @@ class ExtractSubsetResources(openpype.api.Extractor): # get activating attributes activated_preset = preset_config["active"] - filter_path_regex = preset_config["filter_path_regex"] + filter_path_regex = preset_config.get("filter_path_regex") self.log.info( "Preset `{}` is active `{}` with filter `{}`".format( @@ -123,8 +124,11 @@ class ExtractSubsetResources(openpype.api.Extractor): if not activated_preset: continue - # exclude by regex filter - if not re.search(filter_path_regex, clip_path): + # exclude by regex filter if any + if ( + filter_path_regex + and not re.search(filter_path_regex, clip_path) + ): continue # get all presets attributes @@ -162,6 +166,8 @@ class ExtractSubsetResources(openpype.api.Extractor): out_mark = in_mark + source_duration_handles exporting_clip = None + name_patern_xml = "_{}.".format( + unique_name) if export_type == "Sequence Publish": # change export clip to sequence exporting_clip = flame.duplicate(sequence_clip) @@ -169,8 +175,15 @@ class ExtractSubsetResources(openpype.api.Extractor): # only keep visible layer where instance segment is child self.hide_others( exporting_clip, segment_name, s_track_name) + + # change name patern + name_patern_xml = ( + "__{}.").format( + unique_name) else: exporting_clip = self.import_clip(clip_path) + exporting_clip.name.set_value("{}_{}".format( + asset_name, segment_name)) # change in/out marks to timeline in/out in_mark = clip_in @@ -181,9 +194,7 @@ class ExtractSubsetResources(openpype.api.Extractor): "exportHandles": True, "nbHandles": handles, "startFrame": frame_start, - "namePattern": ( - "<segment name>_<shot name>_{}.").format( - unique_name) + "namePattern": name_patern_xml }) if parsed_comment_attrs: @@ -302,8 +313,9 @@ class ExtractSubsetResources(openpype.api.Extractor): self.log.info("Added representation: {}".format( representation_data)) - # at the end remove the duplicated clip - flame.delete(exporting_clip) + if export_type == "Sequence Publish": + # at the end remove the duplicated clip + flame.delete(exporting_clip) self.log.debug("All representations: {}".format( pformat(instance.data["representations"]))) @@ -405,4 +417,4 @@ class ExtractSubsetResources(openpype.api.Extractor): self.log.warning( "Path `{}` is containing more that one clip".format(path) ) - return clips.pop() + return clips[0] From 31020f6a9c9764649040c874110eeeaec2b50269 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 15 Apr 2022 11:53:53 +0200 Subject: [PATCH 037/109] flame: fixing flair to flare --- .../schemas/projects_schema/schemas/schema_anatomy_imageio.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_imageio.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_imageio.json index 9f142bad09..1d6c428fe0 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_imageio.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_imageio.json @@ -446,7 +446,7 @@ { "key": "flame", "type": "dict", - "label": "Flame/Flair", + "label": "Flame & Flare", "children": [ { "key": "project", From eae3934aa82751988f282381203343db21b60a9f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 15 Apr 2022 15:21:26 +0200 Subject: [PATCH 038/109] flame: fixing loading --- openpype/hosts/flame/api/plugin.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/flame/api/plugin.py b/openpype/hosts/flame/api/plugin.py index c87445fdd3..11108ba49f 100644 --- a/openpype/hosts/flame/api/plugin.py +++ b/openpype/hosts/flame/api/plugin.py @@ -873,6 +873,5 @@ class OpenClipSolver(flib.MediaInfoFile): if feed_clr_obj is not None: feed_clr_obj = ET.Element( "colourSpace", {"type": "string"}) + feed_clr_obj.text = profile_name feed_storage_obj.append(feed_clr_obj) - - feed_clr_obj.text = profile_name From 884e1a409ed6d0b5b47af640a2f82b38aab7b5bd Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 20 Apr 2022 14:18:00 +0200 Subject: [PATCH 039/109] removed env_group_key from schemas --- openpype/settings/entities/schemas/README.md | 3 +-- .../schemas/system_schema/example_schema.json | 13 ------------- .../schemas/system_schema/example_template.json | 3 +-- .../schemas/system_schema/schema_general.json | 1 - openpype/tools/settings/settings/README.md | 3 +-- 5 files changed, 3 insertions(+), 20 deletions(-) diff --git a/openpype/settings/entities/schemas/README.md b/openpype/settings/entities/schemas/README.md index b4bfef2972..b4c878fe0f 100644 --- a/openpype/settings/entities/schemas/README.md +++ b/openpype/settings/entities/schemas/README.md @@ -46,8 +46,7 @@ }, { "type": "raw-json", "label": "{host_label} Environments", - "key": "{host_name}_environments", - "env_group_key": "{host_name}" + "key": "{host_name}_environments" }, { "type": "path", "key": "{host_name}_executables", diff --git a/openpype/settings/entities/schemas/system_schema/example_schema.json b/openpype/settings/entities/schemas/system_schema/example_schema.json index 6a86dae259..b9747b5f4f 100644 --- a/openpype/settings/entities/schemas/system_schema/example_schema.json +++ b/openpype/settings/entities/schemas/system_schema/example_schema.json @@ -117,19 +117,6 @@ } ] }, - { - "key": "env_group_test", - "label": "EnvGroup Test", - "type": "dict", - "children": [ - { - "key": "key_to_store_in_system_settings", - "label": "Testing environment group", - "type": "raw-json", - "env_group_key": "test_group" - } - ] - }, { "key": "dict_wrapper", "type": "dict", diff --git a/openpype/settings/entities/schemas/system_schema/example_template.json b/openpype/settings/entities/schemas/system_schema/example_template.json index ff78c78e8f..9955cf5651 100644 --- a/openpype/settings/entities/schemas/system_schema/example_template.json +++ b/openpype/settings/entities/schemas/system_schema/example_template.json @@ -7,8 +7,7 @@ { "type": "raw-json", "label": "{host_label} Environments", - "key": "{host_name}_environments", - "env_group_key": "{host_name}" + "key": "{host_name}_environments" }, { "type": "path", diff --git a/openpype/settings/entities/schemas/system_schema/schema_general.json b/openpype/settings/entities/schemas/system_schema/schema_general.json index fcab4cd5d8..695ab8bceb 100644 --- a/openpype/settings/entities/schemas/system_schema/schema_general.json +++ b/openpype/settings/entities/schemas/system_schema/schema_general.json @@ -34,7 +34,6 @@ "key": "environment", "label": "Environment", "type": "raw-json", - "env_group_key": "global", "require_restart": true }, { diff --git a/openpype/tools/settings/settings/README.md b/openpype/tools/settings/settings/README.md index 1c916ddff2..c29664a907 100644 --- a/openpype/tools/settings/settings/README.md +++ b/openpype/tools/settings/settings/README.md @@ -44,8 +44,7 @@ }, { "type": "raw-json", "label": "{host_label} Environments", - "key": "{host_name}_environments", - "env_group_key": "{host_name}" + "key": "{host_name}_environments" }, { "type": "path-widget", "key": "{host_name}_executables", From ecbf5d859b13332b9afbada6524dff8b25d9b72a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 20 Apr 2022 14:19:03 +0200 Subject: [PATCH 040/109] removed env group ogic from entities --- openpype/settings/entities/base_entity.py | 21 ----------- .../entities/dict_mutable_keys_entity.py | 33 ++++------------- openpype/settings/entities/input_entities.py | 20 +---------- openpype/settings/entities/root_entities.py | 36 ------------------- .../settings/settings/dict_mutable_widget.py | 4 --- 5 files changed, 7 insertions(+), 107 deletions(-) diff --git a/openpype/settings/entities/base_entity.py b/openpype/settings/entities/base_entity.py index 21ee44ae77..741f13c49b 100644 --- a/openpype/settings/entities/base_entity.py +++ b/openpype/settings/entities/base_entity.py @@ -127,12 +127,6 @@ class BaseItemEntity(BaseEntity): # Entity is in hierarchy of dynamically created entity self.is_in_dynamic_item = False - # Entity will save metadata about environments - # - this is current possible only for RawJsonEnity - self.is_env_group = False - # Key of environment group key must be unique across system settings - self.env_group_key = None - # Roles of an entity self.roles = None @@ -286,16 +280,6 @@ class BaseItemEntity(BaseEntity): ).format(self.group_item.path) raise EntitySchemaError(self, reason) - # Validate that env group entities will be stored into file. - # - env group entities must store metadata which is not possible if - # metadata would be outside of file - if self.file_item is None and self.is_env_group: - reason = ( - "Environment item is not inside file" - " item so can't store metadata for defaults." - ) - raise EntitySchemaError(self, reason) - # Dynamic items must not have defined labels. (UI specific) if self.label and self.is_dynamic_item: raise EntitySchemaError( @@ -862,11 +846,6 @@ class ItemEntity(BaseItemEntity): if self.is_dynamic_item: self.require_key = False - # If value should be stored to environments and uder which group key - # - the key may be dynamically changed by it's parent on save - self.env_group_key = self.schema_data.get("env_group_key") - self.is_env_group = bool(self.env_group_key is not None) - # Root item reference self.root_item = self.parent.root_item diff --git a/openpype/settings/entities/dict_mutable_keys_entity.py b/openpype/settings/entities/dict_mutable_keys_entity.py index a0c93b97a7..3dc07524af 100644 --- a/openpype/settings/entities/dict_mutable_keys_entity.py +++ b/openpype/settings/entities/dict_mutable_keys_entity.py @@ -148,11 +148,7 @@ class DictMutableKeysEntity(EndpointEntity): ): raise InvalidKeySymbols(self.path, key) - if self.value_is_env_group: - item_schema = copy.deepcopy(self.item_schema) - item_schema["env_group_key"] = key - else: - item_schema = self.item_schema + item_schema = self.item_schema new_child = self.create_schema_object(item_schema, self, True) self.children_by_key[key] = new_child @@ -216,9 +212,7 @@ class DictMutableKeysEntity(EndpointEntity): self.children_label_by_id = {} self.store_as_list = self.schema_data.get("store_as_list") or False - self.value_is_env_group = ( - self.schema_data.get("value_is_env_group") or False - ) + self.required_keys = self.schema_data.get("required_keys") or [] self.collapsible_key = self.schema_data.get("collapsible_key") or False # GUI attributes @@ -241,9 +235,6 @@ class DictMutableKeysEntity(EndpointEntity): object_type.update(input_modifiers) self.item_schema = object_type - if self.value_is_env_group: - self.item_schema["env_group_key"] = "" - if self.group_item is None: self.is_group = True @@ -259,10 +250,6 @@ class DictMutableKeysEntity(EndpointEntity): if used_temp_label: self.label = None - if self.value_is_env_group and self.store_as_list: - reason = "Item can't store environments metadata to list output." - raise EntitySchemaError(self, reason) - if not self.schema_data.get("object_type"): reason = ( "Modifiable dictionary must have specified `object_type`." @@ -579,18 +566,10 @@ class DictMutableKeysEntity(EndpointEntity): output.append([key, child_value]) return output - output = {} - for key, child_entity in self.children_by_key.items(): - child_value = child_entity.settings_value() - # TODO child should have setter of env group key se child can - # know what env group represents. - if self.value_is_env_group: - if key not in child_value[M_ENVIRONMENT_KEY]: - _metadata = child_value[M_ENVIRONMENT_KEY] - _m_keykey = tuple(_metadata.keys())[0] - env_keys = child_value[M_ENVIRONMENT_KEY].pop(_m_keykey) - child_value[M_ENVIRONMENT_KEY][key] = env_keys - output[key] = child_value + output = { + key: child_entity.settings_value() + for key, child_entity in self.children_by_key.items() + } output.update(self.metadata) return output diff --git a/openpype/settings/entities/input_entities.py b/openpype/settings/entities/input_entities.py index 3dcd238672..32eedf3b3e 100644 --- a/openpype/settings/entities/input_entities.py +++ b/openpype/settings/entities/input_entities.py @@ -534,13 +534,7 @@ class RawJsonEntity(InputEntity): @property def metadata(self): - output = {} - if isinstance(self._current_value, dict) and self.is_env_group: - output[M_ENVIRONMENT_KEY] = { - self.env_group_key: list(self._current_value.keys()) - } - - return output + return {} @property def has_unsaved_changes(self): @@ -549,15 +543,6 @@ class RawJsonEntity(InputEntity): result = self.metadata != self._metadata_for_current_state() return result - def schema_validations(self): - if self.store_as_string and self.is_env_group: - reason = ( - "RawJson entity can't store environment group metadata" - " as string." - ) - raise EntitySchemaError(self, reason) - super(RawJsonEntity, self).schema_validations() - def _convert_to_valid_type(self, value): if isinstance(value, STRING_TYPE): try: @@ -583,9 +568,6 @@ class RawJsonEntity(InputEntity): def _settings_value(self): value = super(RawJsonEntity, self)._settings_value() - if self.is_env_group and isinstance(value, dict): - value.update(self.metadata) - if self.store_as_string: return json.dumps(value) return value diff --git a/openpype/settings/entities/root_entities.py b/openpype/settings/entities/root_entities.py index edb4407679..ff76fa5180 100644 --- a/openpype/settings/entities/root_entities.py +++ b/openpype/settings/entities/root_entities.py @@ -52,7 +52,6 @@ from openpype.settings.lib import ( get_available_studio_project_settings_overrides_versions, get_available_studio_project_anatomy_overrides_versions, - find_environments, apply_overrides ) @@ -422,11 +421,6 @@ class RootEntity(BaseItemEntity): """ pass - @abstractmethod - def _validate_defaults_to_save(self, value): - """Validate default values before save.""" - pass - def _save_default_values(self): """Save default values. @@ -435,7 +429,6 @@ class RootEntity(BaseItemEntity): DEFAULTS. """ settings_value = self.settings_value() - self._validate_defaults_to_save(settings_value) defaults_dir = self.defaults_dir() for file_path, value in settings_value.items(): @@ -604,8 +597,6 @@ class SystemSettings(RootEntity): def _save_studio_values(self): settings_value = self.settings_value() - self._validate_duplicated_env_group(settings_value) - self.log.debug("Saving system settings: {}".format( json.dumps(settings_value, indent=4) )) @@ -613,29 +604,6 @@ class SystemSettings(RootEntity): # Reset source version after restart self._source_version = None - def _validate_defaults_to_save(self, value): - """Valiations of default values before save.""" - self._validate_duplicated_env_group(value) - - def _validate_duplicated_env_group(self, value, override_state=None): - """ Validate duplicated environment groups. - - Raises: - DuplicatedEnvGroups: When value contain duplicated env groups. - """ - value = copy.deepcopy(value) - if override_state is None: - override_state = self._override_state - - if override_state is OverrideState.STUDIO: - default_values = get_default_settings()[SYSTEM_SETTINGS_KEY] - final_value = apply_overrides(default_values, value) - else: - final_value = value - - # Check if final_value contain duplicated environment groups - find_environments(final_value) - def _save_project_values(self): """System settings can't have project overrides. @@ -911,10 +879,6 @@ class ProjectSettings(RootEntity): if warnings: raise SaveWarningExc(warnings) - def _validate_defaults_to_save(self, value): - """Valiations of default values before save.""" - pass - def _validate_values_to_save(self, value): pass diff --git a/openpype/tools/settings/settings/dict_mutable_widget.py b/openpype/tools/settings/settings/dict_mutable_widget.py index 6489266131..1c704b3cd5 100644 --- a/openpype/tools/settings/settings/dict_mutable_widget.py +++ b/openpype/tools/settings/settings/dict_mutable_widget.py @@ -465,10 +465,6 @@ class ModifiableDictItem(QtWidgets.QWidget): self.entity_widget.change_key(key, self) self.update_style() - @property - def value_is_env_group(self): - return self.entity_widget.value_is_env_group - def update_key_label(self): if not self.collapsible_key: return From 95a8ccb47488dbf0d3c4be9333c3222887bfe017 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 20 Apr 2022 14:20:33 +0200 Subject: [PATCH 041/109] removed logic related to env groups --- openpype/api.py | 8 +- openpype/lib/env_tools.py | 54 ----------- openpype/settings/__init__.py | 2 - openpype/settings/constants.py | 4 - .../entities/dict_mutable_keys_entity.py | 1 - openpype/settings/entities/input_entities.py | 5 +- openpype/settings/lib.py | 95 ------------------- 7 files changed, 4 insertions(+), 165 deletions(-) diff --git a/openpype/api.py b/openpype/api.py index b692b36065..9ce745b653 100644 --- a/openpype/api.py +++ b/openpype/api.py @@ -3,7 +3,6 @@ from .settings import ( get_project_settings, get_current_project_settings, get_anatomy_settings, - get_environments, SystemSettings, ProjectSettings @@ -23,7 +22,6 @@ from .lib import ( get_app_environments_for_context, source_hash, get_latest_version, - get_global_environments, get_local_site_id, change_openpype_mongo_url, create_project_folders, @@ -69,10 +67,10 @@ __all__ = [ "get_project_settings", "get_current_project_settings", "get_anatomy_settings", - "get_environments", "get_project_basic_paths", "SystemSettings", + "ProjectSettings", "PypeLogger", "Logger", @@ -102,8 +100,9 @@ __all__ = [ # get contextual data "version_up", - "get_hierarchy", "get_asset", + "get_hierarchy", + "get_workdir_data", "get_version_from_path", "get_last_version_from_path", "get_app_environments_for_context", @@ -111,7 +110,6 @@ __all__ = [ "run_subprocess", "get_latest_version", - "get_global_environments", "get_local_site_id", "change_openpype_mongo_url", diff --git a/openpype/lib/env_tools.py b/openpype/lib/env_tools.py index 6521d20f1e..25bcbf7c1b 100644 --- a/openpype/lib/env_tools.py +++ b/openpype/lib/env_tools.py @@ -69,57 +69,3 @@ def get_paths_from_environ(env_key=None, env_value=None, return_first=False): return None # Return all existing paths from environment variable return existing_paths - - -def get_global_environments(env=None): - """Load global environments from Pype. - - Return prepared and parsed global environments by pype's settings. Use - combination of "global" environments set in pype's settings and enabled - modules. - - Args: - env (dict, optional): Initial environments. Empty dictionary is used - when not entered. - - Returns; - dict of str: Loaded and processed environments. - - """ - import acre - from openpype.modules import ModulesManager - from openpype.settings import get_environments - - if env is None: - env = {} - - # Get global environments from settings - all_settings_env = get_environments() - parsed_global_env = acre.parse(all_settings_env["global"]) - - # Merge with entered environments - merged_env = acre.append(env, parsed_global_env) - - # Get environments from Pype modules - modules_manager = ModulesManager() - - module_envs = modules_manager.collect_global_environments() - publish_plugin_dirs = modules_manager.collect_plugin_paths()["publish"] - - # Set pyblish plugins paths if any module want to register them - if publish_plugin_dirs: - publish_paths_str = os.environ.get("PYBLISHPLUGINPATH") or "" - publish_paths = publish_paths_str.split(os.pathsep) - _publish_paths = { - os.path.normpath(path) for path in publish_paths if path - } - for path in publish_plugin_dirs: - _publish_paths.add(os.path.normpath(path)) - module_envs["PYBLISHPLUGINPATH"] = os.pathsep.join(_publish_paths) - - # Merge environments with current environments and update values - if module_envs: - parsed_envs = acre.parse(module_envs) - merged_env = acre.merge(parsed_envs, merged_env) - - return acre.compute(merged_env, cleanup=True) diff --git a/openpype/settings/__init__.py b/openpype/settings/__init__.py index 14e4678050..ca7157812d 100644 --- a/openpype/settings/__init__.py +++ b/openpype/settings/__init__.py @@ -22,7 +22,6 @@ from .lib import ( get_project_settings, get_current_project_settings, get_anatomy_settings, - get_environments, get_local_settings ) from .entities import ( @@ -54,7 +53,6 @@ __all__ = ( "get_project_settings", "get_current_project_settings", "get_anatomy_settings", - "get_environments", "get_local_settings", "SystemSettings", diff --git a/openpype/settings/constants.py b/openpype/settings/constants.py index 19ff953eb4..cd84d4db1c 100644 --- a/openpype/settings/constants.py +++ b/openpype/settings/constants.py @@ -3,14 +3,11 @@ import re # Metadata keys for work with studio and project overrides M_OVERRIDDEN_KEY = "__overriden_keys__" -# Metadata key for storing information about environments -M_ENVIRONMENT_KEY = "__environment_keys__" # Metadata key for storing dynamic created labels M_DYNAMIC_KEY_LABEL = "__dynamic_keys_labels__" METADATA_KEYS = frozenset([ M_OVERRIDDEN_KEY, - M_ENVIRONMENT_KEY, M_DYNAMIC_KEY_LABEL ]) @@ -35,7 +32,6 @@ KEY_REGEX = re.compile(r"^[{}]+$".format(KEY_ALLOWED_SYMBOLS)) __all__ = ( "M_OVERRIDDEN_KEY", - "M_ENVIRONMENT_KEY", "M_DYNAMIC_KEY_LABEL", "METADATA_KEYS", diff --git a/openpype/settings/entities/dict_mutable_keys_entity.py b/openpype/settings/entities/dict_mutable_keys_entity.py index 3dc07524af..e6d332b9ad 100644 --- a/openpype/settings/entities/dict_mutable_keys_entity.py +++ b/openpype/settings/entities/dict_mutable_keys_entity.py @@ -15,7 +15,6 @@ from .exceptions import ( from openpype.settings.constants import ( METADATA_KEYS, M_DYNAMIC_KEY_LABEL, - M_ENVIRONMENT_KEY, KEY_REGEX, KEY_ALLOWED_SYMBOLS ) diff --git a/openpype/settings/entities/input_entities.py b/openpype/settings/entities/input_entities.py index 32eedf3b3e..89f12afd9b 100644 --- a/openpype/settings/entities/input_entities.py +++ b/openpype/settings/entities/input_entities.py @@ -15,10 +15,7 @@ from .exceptions import ( EntitySchemaError ) -from openpype.settings.constants import ( - METADATA_KEYS, - M_ENVIRONMENT_KEY -) +from openpype.settings.constants import METADATA_KEYS class EndpointEntity(ItemEntity): diff --git a/openpype/settings/lib.py b/openpype/settings/lib.py index 937329b417..f921b9c318 100644 --- a/openpype/settings/lib.py +++ b/openpype/settings/lib.py @@ -9,7 +9,6 @@ from .exceptions import ( ) from .constants import ( M_OVERRIDDEN_KEY, - M_ENVIRONMENT_KEY, METADATA_KEYS, @@ -457,24 +456,6 @@ def get_local_settings(): return _LOCAL_SETTINGS_HANDLER.get_local_settings() -class DuplicatedEnvGroups(Exception): - def __init__(self, duplicated): - self.origin_duplicated = duplicated - self.duplicated = {} - for key, items in duplicated.items(): - self.duplicated[key] = [] - for item in items: - self.duplicated[key].append("/".join(item["parents"])) - - msg = "Duplicated environment group keys. {}".format( - ", ".join([ - "\"{}\"".format(env_key) for env_key in self.duplicated.keys() - ]) - ) - - super(DuplicatedEnvGroups, self).__init__(msg) - - def load_openpype_default_settings(): """Load openpype default settings.""" return load_jsons_from_dir(DEFAULTS_DIR) @@ -624,69 +605,6 @@ def load_jsons_from_dir(path, *args, **kwargs): return output -def find_environments(data, with_items=False, parents=None): - """ Find environemnt values from system settings by it's metadata. - - Args: - data(dict): System settings data or dictionary which may contain - environments metadata. - - Returns: - dict: Key as Environment key and value for `acre` module. - """ - if not data or not isinstance(data, dict): - return {} - - output = {} - if parents is None: - parents = [] - - if M_ENVIRONMENT_KEY in data: - metadata = data.get(M_ENVIRONMENT_KEY) - for env_group_key, env_keys in metadata.items(): - if env_group_key not in output: - output[env_group_key] = [] - - _env_values = {} - for key in env_keys: - _env_values[key] = data[key] - - item = { - "env": _env_values, - "parents": parents[:-1] - } - output[env_group_key].append(item) - - for key, value in data.items(): - _parents = copy.deepcopy(parents) - _parents.append(key) - result = find_environments(value, True, _parents) - if not result: - continue - - for env_group_key, env_values in result.items(): - if env_group_key not in output: - output[env_group_key] = [] - - for env_values_item in env_values: - output[env_group_key].append(env_values_item) - - if with_items: - return output - - duplicated_env_groups = {} - final_output = {} - for key, value_in_list in output.items(): - if len(value_in_list) > 1: - duplicated_env_groups[key] = value_in_list - else: - final_output[key] = value_in_list[0]["env"] - - if duplicated_env_groups: - raise DuplicatedEnvGroups(duplicated_env_groups) - return final_output - - def subkey_merge(_dict, value, keys): key = keys.pop(0) if not keys: @@ -1082,19 +1000,6 @@ def get_current_project_settings(): return get_project_settings(project_name) -def get_environments(): - """Calculated environment based on defaults and system settings. - - Any default environment also found in the system settings will be fully - overridden by the one from the system settings. - - Returns: - dict: Output should be ready for `acre` module. - """ - - return find_environments(get_system_settings(False)) - - def get_general_environments(): """Get general environments. From 475654f51f5d98a4230dd46e66910a60959d276e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 20 Apr 2022 18:33:10 +0200 Subject: [PATCH 042/109] fix report messages --- openpype/pipeline/plugin_discover.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/pipeline/plugin_discover.py b/openpype/pipeline/plugin_discover.py index fb860fe5f2..004e530b1c 100644 --- a/openpype/pipeline/plugin_discover.py +++ b/openpype/pipeline/plugin_discover.py @@ -59,7 +59,7 @@ class DiscoverResult: self.ignored_plugins ))) for cls in self.ignored_plugins: - lines.append("- {}".format(cls.__class__.__name__)) + lines.append("- {}".format(cls.__name__)) # Abstract classes if self.abstract_plugins or full_report: @@ -67,7 +67,7 @@ class DiscoverResult: self.abstract_plugins ))) for cls in self.abstract_plugins: - lines.append("- {}".format(cls.__class__.__name__)) + lines.append("- {}".format(cls.__name__)) # Abstract classes if self.duplicated_plugins or full_report: @@ -75,7 +75,7 @@ class DiscoverResult: self.duplicated_plugins ))) for cls in self.duplicated_plugins: - lines.append("- {}".format(cls.__class__.__name__)) + lines.append("- {}".format(cls.__name__)) if self.crashed_file_paths or full_report: lines.append("*** Failed to load {} files".format(len( From e50d8ee1ed596064cd0fb1b3d83d3823b4184af3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 21 Apr 2022 11:58:20 +0200 Subject: [PATCH 043/109] initial settings for tray publisher --- .../project_settings/traypublisher.json | 38 ++++++ .../schemas/projects_schema/schema_main.json | 4 + .../schema_project_traypublisher.json | 117 ++++++++++++++++++ 3 files changed, 159 insertions(+) create mode 100644 openpype/settings/defaults/project_settings/traypublisher.json create mode 100644 openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json diff --git a/openpype/settings/defaults/project_settings/traypublisher.json b/openpype/settings/defaults/project_settings/traypublisher.json new file mode 100644 index 0000000000..e6c6747ca2 --- /dev/null +++ b/openpype/settings/defaults/project_settings/traypublisher.json @@ -0,0 +1,38 @@ +{ + "simple_creators": [ + { + "family": "workfile", + "identifier": "", + "label": "Workfile", + "icon": "fa.file", + "default_variants": [ + "Main" + ], + "enable_review": false, + "description": "Publish workfile backup", + "detailed_description": "", + "extensions": [ + ".ma", + ".mb", + ".nk", + ".hrox", + ".hip", + ".hiplc", + ".hipnc", + ".blend", + ".scn", + ".tvpp", + ".comp", + ".zip", + ".prproj", + ".drp", + ".psd", + ".psb", + ".aep" + ], + "allow_sequences": { + "allow": "no" + } + } + ] +} \ No newline at end of file diff --git a/openpype/settings/entities/schemas/projects_schema/schema_main.json b/openpype/settings/entities/schemas/projects_schema/schema_main.json index 8e4eba86ef..dbddd18c80 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_main.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_main.json @@ -126,6 +126,10 @@ "type": "schema", "name": "schema_project_standalonepublisher" }, + { + "type": "schema", + "name": "schema_project_traypublisher" + }, { "type": "schema", "name": "schema_project_webpublisher" diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json new file mode 100644 index 0000000000..00deb84172 --- /dev/null +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json @@ -0,0 +1,117 @@ +{ + "type": "dict", + "collapsible": true, + "key": "traypublisher", + "label": "Tray Publisher", + "is_file": true, + "children": [ + { + "type": "list", + "collapsible": true, + "key": "simple_creators", + "label": "Creator plugins", + "use_label_wrap": true, + "collapsible_key": true, + "object_type": { + "type": "dict", + "children": [ + { + "type": "text", + "key": "family", + "label": "Family" + }, + { + "type": "text", + "key": "identifier", + "label": "Identifier", + "placeholder": "< Use 'Family' >", + "tooltip": "All creators must have unique identifier.\nBy default is used 'family' but if you need to have more creators with same families\nyou have to set identifier too." + }, + { + "type": "text", + "key": "label", + "label": "Label" + }, + { + "type": "text", + "key": "icon", + "label": "Icon" + }, + { + "type": "list", + "key": "default_variants", + "label": "Default variants", + "object_type": { + "type": "text" + } + }, + { + "type": "boolean", + "key": "enable_review", + "label": "Enable review", + "tooltip": "Allow to create review from source file/s.\nFiles must be supported to be able create review." + }, + { + "type": "separator" + }, + { + "type": "text", + "key": "description", + "label": "Description" + }, + { + "type": "text", + "key": "detailed_description", + "label": "Detailed Description", + "multiline": true + }, + { + "type": "separator" + }, + { + "type": "list", + "key": "extensions", + "label": "Extensions", + "use_label_wrap": true, + "collapsible_key": true, + "collapsed": false, + "object_type": "text" + }, + { + "key": "allow_sequences", + "label": "Allow sequences", + "type": "dict-conditional", + "use_label_wrap": true, + "collapsible_key": true, + "enum_key": "allow", + "enum_children": [ + { + "key": "all", + "label": "Yes (all extensions)" + }, + { + "key": "selection", + "label": "Yes (limited extensions)", + "children": [ + { + "type": "list", + "key": "extensions", + "label": "Extensions", + "use_label_wrap": true, + "collapsible_key": true, + "collapsed": false, + "object_type": "text" + } + ] + }, + { + "key": "no", + "label": "No" + } + ] + } + ] + } + } + ] +} From 9780de94c53244426e48f192d59f476da9cbb606 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 21 Apr 2022 11:58:55 +0200 Subject: [PATCH 044/109] added file adding creators from settings --- .../plugins/create/create_from_settings.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 openpype/hosts/traypublisher/plugins/create/create_from_settings.py diff --git a/openpype/hosts/traypublisher/plugins/create/create_from_settings.py b/openpype/hosts/traypublisher/plugins/create/create_from_settings.py new file mode 100644 index 0000000000..19ade437ab --- /dev/null +++ b/openpype/hosts/traypublisher/plugins/create/create_from_settings.py @@ -0,0 +1,34 @@ +import os +import copy + +from openpype.api import get_project_settings + + +def initialize(): + from openpype.hosts.traypublisher.api.plugin import SettingsCreator + + project_name = os.environ["AVALON_PROJECT"] + project_settings = get_project_settings(project_name) + + simple_creators = project_settings["traypublisher"]["simple_creators"] + + global_variables = globals() + for item in simple_creators: + allow_sequences_value = item["allow_sequences"] + allow_sequences = allow_sequences_value["allow"] + if allow_sequences == "all": + sequence_extensions = copy.deepcopy(item["extensions"]) + + elif allow_sequences == "no": + sequence_extensions = [] + + elif allow_sequences == "selection": + sequence_extensions = allow_sequences_value["extensions"] + + item["sequence_extensions"] = sequence_extensions + item["enable_review"] = False + dynamic_plugin = SettingsCreator.from_settings(item) + global_variables[dynamic_plugin.__name__] = dynamic_plugin + + +initialize() From 20ef8b0c58358992f242c3c286cfca44d102999e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 21 Apr 2022 11:59:04 +0200 Subject: [PATCH 045/109] removed current workfile creator --- .../plugins/create/create_workfile.py | 97 ------------------- 1 file changed, 97 deletions(-) delete mode 100644 openpype/hosts/traypublisher/plugins/create/create_workfile.py diff --git a/openpype/hosts/traypublisher/plugins/create/create_workfile.py b/openpype/hosts/traypublisher/plugins/create/create_workfile.py deleted file mode 100644 index 5e0af350f0..0000000000 --- a/openpype/hosts/traypublisher/plugins/create/create_workfile.py +++ /dev/null @@ -1,97 +0,0 @@ -from openpype.hosts.traypublisher.api import pipeline -from openpype.lib import FileDef -from openpype.pipeline import ( - Creator, - CreatedInstance -) - - -class WorkfileCreator(Creator): - identifier = "workfile" - label = "Workfile" - family = "workfile" - description = "Publish backup of workfile" - - create_allow_context_change = True - - extensions = [ - # Maya - ".ma", ".mb", - # Nuke - ".nk", - # Hiero - ".hrox", - # Houdini - ".hip", ".hiplc", ".hipnc", - # Blender - ".blend", - # Celaction - ".scn", - # TVPaint - ".tvpp", - # Fusion - ".comp", - # Harmony - ".zip", - # Premiere - ".prproj", - # Resolve - ".drp", - # Photoshop - ".psd", ".psb", - # Aftereffects - ".aep" - ] - - def get_icon(self): - return "fa.file" - - def collect_instances(self): - for instance_data in pipeline.list_instances(): - creator_id = instance_data.get("creator_identifier") - if creator_id == self.identifier: - instance = CreatedInstance.from_existing( - instance_data, self - ) - self._add_instance_to_context(instance) - - def update_instances(self, update_list): - pipeline.update_instances(update_list) - - def remove_instances(self, instances): - pipeline.remove_instances(instances) - for instance in instances: - self._remove_instance_from_context(instance) - - def create(self, subset_name, data, pre_create_data): - # Pass precreate data to creator attributes - data["creator_attributes"] = pre_create_data - # Create new instance - new_instance = CreatedInstance(self.family, subset_name, data, self) - # Host implementation of storing metadata about instance - pipeline.HostContext.add_instance(new_instance.data_to_store()) - # Add instance to current context - self._add_instance_to_context(new_instance) - - def get_default_variants(self): - return [ - "Main" - ] - - def get_instance_attr_defs(self): - output = [ - FileDef( - "filepath", - folders=False, - extensions=self.extensions, - label="Filepath" - ) - ] - return output - - def get_pre_create_attr_defs(self): - # Use same attributes as for instance attrobites - return self.get_instance_attr_defs() - - def get_detail_description(self): - return """# Publish workfile backup""" From 0666af82e6ec8f2ec2b8694877c193df598c1dc5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 21 Apr 2022 18:45:36 +0200 Subject: [PATCH 046/109] variant input has aligned options button --- openpype/style/style.css | 11 +++++- .../tools/publisher/widgets/create_dialog.py | 37 +++++++++++++++---- 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/openpype/style/style.css b/openpype/style/style.css index b5f6962eee..9df615d953 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -852,7 +852,16 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { #PublishLogConsole { font-family: "Noto Sans Mono"; } - +VariantInputsWidget QLineEdit { + border-bottom-right-radius: 0px; + border-top-right-radius: 0px; +} +VariantInputsWidget QToolButton { + border-bottom-left-radius: 0px; + border-top-left-radius: 0px; + padding-top: 0.5em; + padding-bottom: 0.5em; +} #VariantInput[state="new"], #VariantInput[state="new"]:focus, #VariantInput[state="new"]:hover { border-color: {color:publisher:success}; } diff --git a/openpype/tools/publisher/widgets/create_dialog.py b/openpype/tools/publisher/widgets/create_dialog.py index 7d98609c2c..21e1bd5cfc 100644 --- a/openpype/tools/publisher/widgets/create_dialog.py +++ b/openpype/tools/publisher/widgets/create_dialog.py @@ -29,6 +29,14 @@ from ..constants import ( SEPARATORS = ("---separator---", "---") +class VariantInputsWidget(QtWidgets.QWidget): + resized = QtCore.Signal() + + def resizeEvent(self, event): + super(VariantInputsWidget, self).resizeEvent(event) + self.resized.emit() + + class CreateErrorMessageBox(ErrorMessageBox): def __init__( self, @@ -247,22 +255,25 @@ class CreateDialog(QtWidgets.QDialog): creators_model = QtGui.QStandardItemModel() creators_view.setModel(creators_model) - variant_input = QtWidgets.QLineEdit(self) + variant_widget = VariantInputsWidget(self) + + variant_input = QtWidgets.QLineEdit(variant_widget) variant_input.setObjectName("VariantInput") variant_input.setToolTip(VARIANT_TOOLTIP) - variant_hints_btn = QtWidgets.QPushButton(self) - variant_hints_btn.setFixedWidth(18) + variant_hints_btn = QtWidgets.QToolButton(variant_widget) + variant_hints_btn.setArrowType(QtCore.Qt.DownArrow) + variant_hints_btn.setIconSize(QtCore.QSize(12, 12)) - variant_hints_menu = QtWidgets.QMenu(variant_hints_btn) + variant_hints_menu = QtWidgets.QMenu(variant_widget) variant_hints_group = QtWidgets.QActionGroup(variant_hints_menu) - variant_hints_btn.setMenu(variant_hints_menu) + # variant_hints_btn.setMenu(variant_hints_menu) - variant_layout = QtWidgets.QHBoxLayout() + variant_layout = QtWidgets.QHBoxLayout(variant_widget) variant_layout.setContentsMargins(0, 0, 0, 0) variant_layout.setSpacing(0) variant_layout.addWidget(variant_input, 1) - variant_layout.addWidget(variant_hints_btn, 0) + variant_layout.addWidget(variant_hints_btn, 0, QtCore.Qt.AlignVCenter) subset_name_input = QtWidgets.QLineEdit(self) subset_name_input.setEnabled(False) @@ -271,7 +282,7 @@ class CreateDialog(QtWidgets.QDialog): create_btn.setEnabled(False) form_layout = QtWidgets.QFormLayout() - form_layout.addRow("Variant:", variant_layout) + form_layout.addRow("Variant:", variant_widget) form_layout.addRow("Subset:", subset_name_input) mid_widget = QtWidgets.QWidget(self) @@ -341,11 +352,13 @@ class CreateDialog(QtWidgets.QDialog): help_btn.resized.connect(self._on_help_btn_resize) create_btn.clicked.connect(self._on_create) + variant_widget.resized.connect(self._on_variant_widget_resize) variant_input.returnPressed.connect(self._on_create) variant_input.textChanged.connect(self._on_variant_change) creators_view.selectionModel().currentChanged.connect( self._on_creator_item_change ) + variant_hints_btn.clicked.connect(self._on_variant_btn_click) variant_hints_menu.triggered.connect(self._on_variant_action) assets_widget.selection_changed.connect(self._on_asset_change) assets_widget.current_context_required.connect( @@ -660,6 +673,14 @@ class CreateDialog(QtWidgets.QDialog): self.variant_input.setText(default_variant or "Main") + def _on_variant_widget_resize(self): + self.variant_hints_btn.setFixedHeight(self.variant_input.height()) + + def _on_variant_btn_click(self): + pos = self.variant_hints_btn.rect().bottomLeft() + point = self.variant_hints_btn.mapToGlobal(pos) + self.variant_hints_menu.popup(point) + def _on_variant_action(self, action): value = action.text() if self.variant_input.text() != value: From 7415c857905a8eddb71eff26e2e1c1456330b113 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 22 Apr 2022 10:27:58 +0200 Subject: [PATCH 047/109] use operational patter to recognize op atom mxf format --- openpype/lib/transcoding.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index fcec5d4216..f20bef3854 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -727,9 +727,9 @@ def get_ffmpeg_format_args(ffprobe_data, source_ffmpeg_cmd=None): def _ffmpeg_mxf_format_args(ffprobe_data, source_ffmpeg_cmd): input_format = ffprobe_data["format"] format_tags = input_format.get("tags") or {} - product_name = format_tags.get("product_name") or "" + operational_pattern_ul = format_tags.get("operational_pattern_ul") or "" output = [] - if "opatom" in product_name.lower(): + if operational_pattern_ul == "060e2b34.04010102.0d010201.10030000": output.extend(["-f", "mxf_opatom"]) return output From e311a48ef47f0ba8d80c59e30c1fb3dcf3f1c93a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 22 Apr 2022 18:23:03 +0200 Subject: [PATCH 048/109] skip containers with not found versions --- .../plugins/publish/collect_scene_loaded_versions.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/publish/collect_scene_loaded_versions.py b/openpype/plugins/publish/collect_scene_loaded_versions.py index e54592abb8..4c54a7d46c 100644 --- a/openpype/plugins/publish/collect_scene_loaded_versions.py +++ b/openpype/plugins/publish/collect_scene_loaded_versions.py @@ -44,12 +44,20 @@ class CollectSceneLoadedVersions(pyblish.api.ContextPlugin): } for con in _containers: + repre_id = con["representation"] + version_id = version_by_repr.get(repre_id) + if version_id is None: + self.log.warning(( + "Skipping container, did not find version document. {}" + ).format(str(con))) + continue + # NOTE: # may have more then one representation that are same version version = { "subsetName": con["name"], - "representation": ObjectId(con["representation"]), - "version": version_by_repr[con["representation"]], # _id + "representation": ObjectId(repre_id), + "version": version_id, } loaded_versions.append(version) From 7c460886442aa0ec5097f7f93c097b2987386882 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 22 Apr 2022 18:27:30 +0200 Subject: [PATCH 049/109] better log message --- openpype/plugins/publish/collect_scene_loaded_versions.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/publish/collect_scene_loaded_versions.py b/openpype/plugins/publish/collect_scene_loaded_versions.py index 4c54a7d46c..7b44aa7963 100644 --- a/openpype/plugins/publish/collect_scene_loaded_versions.py +++ b/openpype/plugins/publish/collect_scene_loaded_versions.py @@ -43,12 +43,15 @@ class CollectSceneLoadedVersions(pyblish.api.ContextPlugin): io.find({"_id": {"$in": _repr_ids}}, projection={"parent": 1}) } + # QUESTION should we add same representation id when loaded multiple + # times? for con in _containers: repre_id = con["representation"] version_id = version_by_repr.get(repre_id) if version_id is None: self.log.warning(( - "Skipping container, did not find version document. {}" + "Skipping container," + " did not find representation document. {}" ).format(str(con))) continue From a5826ae33667c67d424376a45edb59ca80c31c6f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 22 Apr 2022 18:27:39 +0200 Subject: [PATCH 050/109] reorganized code a little bit --- openpype/plugins/publish/collect_scene_loaded_versions.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/publish/collect_scene_loaded_versions.py b/openpype/plugins/publish/collect_scene_loaded_versions.py index 7b44aa7963..ffdd532df2 100644 --- a/openpype/plugins/publish/collect_scene_loaded_versions.py +++ b/openpype/plugins/publish/collect_scene_loaded_versions.py @@ -38,9 +38,13 @@ class CollectSceneLoadedVersions(pyblish.api.ContextPlugin): loaded_versions = [] _containers = list(host.ls()) _repr_ids = [ObjectId(c["representation"]) for c in _containers] + repre_docs = io.find( + {"_id": {"$in": _repr_ids}}, + projection={"_id": 1, "parent": 1} + ) version_by_repr = { - str(doc["_id"]): doc["parent"] for doc in - io.find({"_id": {"$in": _repr_ids}}, projection={"parent": 1}) + str(doc["_id"]): doc["parent"] + for doc in repre_docs } # QUESTION should we add same representation id when loaded multiple From 750ec30c55d63daedf2a5741010a415fe479390f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 22 Apr 2022 18:50:50 +0200 Subject: [PATCH 051/109] files widget has only one widget --- .../widgets/attribute_defs/files_widget.py | 122 +++++------------- openpype/widgets/attribute_defs/widgets.py | 11 +- 2 files changed, 34 insertions(+), 99 deletions(-) diff --git a/openpype/widgets/attribute_defs/files_widget.py b/openpype/widgets/attribute_defs/files_widget.py index 34f7d159ad..af00ffe5ad 100644 --- a/openpype/widgets/attribute_defs/files_widget.py +++ b/openpype/widgets/attribute_defs/files_widget.py @@ -87,15 +87,29 @@ class FilesModel(QtGui.QStandardItemModel): ".xpm", ".xwd" ] - def __init__(self): + def __init__(self, multivalue): super(FilesModel, self).__init__() self._filenames_by_dirpath = collections.defaultdict(set) self._items_by_dirpath = collections.defaultdict(list) + self._multivalue = multivalue + def add_filepaths(self, filepaths): if not filepaths: return + if not self._multivalue: + filepaths = [filepaths[0]] + item_ids = [] + for items in self._items_by_dirpath.values(): + for item in items: + item_id = item.data(ITEM_ID_ROLE) + if item_id: + item_ids.append(item_id) + + if item_ids: + self.remove_item_by_ids(item_ids) + new_dirpaths = set() for filepath in filepaths: filename = os.path.basename(filepath) @@ -368,16 +382,16 @@ class FilesView(QtWidgets.QListView): return super(FilesView, self).event(event) -class MultiFilesWidget(QtWidgets.QFrame): +class FilesWidget(QtWidgets.QFrame): value_changed = QtCore.Signal() - def __init__(self, parent): - super(MultiFilesWidget, self).__init__(parent) + def __init__(self, multiselect, parent): + super(FilesWidget, self).__init__(parent) self.setAcceptDrops(True) empty_widget = DropEmpty(self) - files_model = FilesModel() + files_model = FilesModel(multiselect) files_proxy_model = FilesProxyModel() files_proxy_model.setSourceModel(files_model) files_view = FilesView(self) @@ -392,6 +406,11 @@ class MultiFilesWidget(QtWidgets.QFrame): files_proxy_model.rowsInserted.connect(self._on_rows_inserted) files_proxy_model.rowsRemoved.connect(self._on_rows_removed) + drag_label = DragLabel() + drag_label.setVisible(False) + + self._drag_label = drag_label + self._in_set_value = False self._empty_widget = empty_widget @@ -501,7 +520,7 @@ class MultiFilesWidget(QtWidgets.QFrame): def sizeHint(self): # Get size hints of widget and visible widgets - result = super(MultiFilesWidget, self).sizeHint() + result = super(FilesWidget, self).sizeHint() if not self._files_view.isVisible(): not_visible_hint = self._files_view.sizeHint() else: @@ -557,90 +576,11 @@ class MultiFilesWidget(QtWidgets.QFrame): self._empty_widget.setVisible(not files_exists) -class SingleFileWidget(QtWidgets.QWidget): - value_changed = QtCore.Signal() - - def __init__(self, parent): - super(SingleFileWidget, self).__init__(parent) - - self.setAcceptDrops(True) - - filepath_input = QtWidgets.QLineEdit(self) - - browse_btn = QtWidgets.QPushButton("Browse", self) - browse_btn.setVisible(False) +class DragLabel(QtWidgets.QWidget): + def __init__(self, parent=None): + super(DragLabel, self).__init__(parent) + t_label = QtWidgets.QLabel("TESTING", self) layout = QtWidgets.QHBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(filepath_input, 1) - layout.addWidget(browse_btn, 0) - - browse_btn.clicked.connect(self._on_browse_clicked) - filepath_input.textChanged.connect(self._on_text_change) - - self._in_set_value = False - - self._filepath_input = filepath_input - self._folders_allowed = False - self._exts_filter = [] - - def set_value(self, value, multivalue): - self._in_set_value = True - - if multivalue: - set_value = set(value) - if len(set_value) == 1: - value = tuple(set_value)[0] - else: - value = "< Multiselection >" - self._filepath_input.setText(value) - - self._in_set_value = False - - def current_value(self): - return self._filepath_input.text() - - def set_filters(self, folders_allowed, exts_filter): - self._folders_allowed = folders_allowed - self._exts_filter = exts_filter - - def _on_text_change(self, text): - if not self._in_set_value: - self.value_changed.emit() - - def _on_browse_clicked(self): - # TODO implement file dialog logic in '_on_browse_clicked' - print("_on_browse_clicked") - - def dragEnterEvent(self, event): - mime_data = event.mimeData() - if not mime_data.hasUrls(): - return - - filepaths = [] - for url in mime_data.urls(): - filepath = url.toLocalFile() - if os.path.exists(filepath): - filepaths.append(filepath) - - # TODO add folder, extensions check - if len(filepaths) == 1: - event.setDropAction(QtCore.Qt.CopyAction) - event.accept() - - def dragLeaveEvent(self, event): - event.accept() - - def dropEvent(self, event): - mime_data = event.mimeData() - if mime_data.hasUrls(): - filepaths = [] - for url in mime_data.urls(): - filepath = url.toLocalFile() - if os.path.exists(filepath): - filepaths.append(filepath) - # TODO filter check - if len(filepaths) == 1: - self._filepath_input.setText(filepaths[0]) - - event.accept() + layout.addWidget(t_label) + self._t_label = t_label diff --git a/openpype/widgets/attribute_defs/widgets.py b/openpype/widgets/attribute_defs/widgets.py index 23f025967d..83eeaea61f 100644 --- a/openpype/widgets/attribute_defs/widgets.py +++ b/openpype/widgets/attribute_defs/widgets.py @@ -15,6 +15,8 @@ from openpype.lib.attribute_definitions import ( ) from openpype.widgets.nice_checkbox import NiceCheckbox +from .files_widget import FilesWidget + def create_widget_for_attr_def(attr_def, parent=None): if not isinstance(attr_def, AbtractAttrDef): @@ -337,15 +339,8 @@ class UnknownAttrWidget(_BaseAttrDefWidget): class FileAttrWidget(_BaseAttrDefWidget): def _ui_init(self): self.multipath = self.attr_def.multipath - if self.multipath: - from .files_widget import MultiFilesWidget - input_widget = MultiFilesWidget(self) - - else: - from .files_widget import SingleFileWidget - - input_widget = SingleFileWidget(self) + input_widget = FilesWidget(self.multipath, self) if self.attr_def.tooltip: input_widget.setToolTip(self.attr_def.tooltip) From 7965001b162b4aa09028584fc447acac66e21a3f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 22 Apr 2022 18:51:08 +0200 Subject: [PATCH 052/109] added first idea of FileDefItem for FileDef --- openpype/lib/attribute_definitions.py | 173 +++++++++++++++++++++++--- 1 file changed, 157 insertions(+), 16 deletions(-) diff --git a/openpype/lib/attribute_definitions.py b/openpype/lib/attribute_definitions.py index 189a5e7acd..3d17818ecb 100644 --- a/openpype/lib/attribute_definitions.py +++ b/openpype/lib/attribute_definitions.py @@ -1,8 +1,12 @@ +import os import re import collections import uuid +import json from abc import ABCMeta, abstractmethod + import six +import clique class AbstractAttrDefMeta(ABCMeta): @@ -302,6 +306,126 @@ class BoolDef(AbtractAttrDef): return self.default +class FileDefItem(object): + def __init__( + self, directory, filenames, frames=None, template=None + ): + self.directory = directory + + self.filenames = [] + self.is_sequence = False + self.template = None + self.frames = [] + + self.set_filenames(filenames, frames, template) + + def __str__(self): + return json.dumps(self.to_dict()) + + def __repr__(self): + if self.is_sequence: + filename = self.template + else: + filename = self.filenames[0] + + return "<{}: \"{}\">".format( + self.__class__.__name__, + os.path.join(self.directory, filename) + ) + + def set_directory(self, directory): + self.directory = directory + + def set_filenames(self, filenames, frames=None, template=None): + if frames is None: + frames = [] + is_sequence = False + if frames: + is_sequence = True + + if is_sequence and not template: + raise ValueError("Missing template for sequence") + + self.filenames = filenames + self.template = template + self.frames = frames + self.is_sequence = is_sequence + + @classmethod + def create_empty_item(cls): + return cls("", "") + + @classmethod + def from_value(cls, value): + multi = isinstance(value, (list, tuple, set)) + if not multi: + value = [value] + + output = [] + for item in value: + if isinstance(item, dict): + output.append(cls.from_dict(item)) + elif isinstance(item, six.string_types): + output.extend(cls.from_paths([item])) + else: + raise TypeError( + "Unknown type \"{}\". Can't convert to {}".format( + str(type(item)), cls.__name__ + ) + ) + if multi: + return output + return output[0] + + @classmethod + def from_dict(cls, data): + return cls( + data["directory"], + data["filenames"], + data.get("frames"), + data.get("template") + ) + + @classmethod + def from_paths(cls, paths): + filenames_by_dir = collections.defaultdict(list) + for path in paths: + normalized = os.path.normpath(path) + directory, filename = os.path.split(normalized) + filenames_by_dir[directory].append(filename) + + output = [] + for directory, filenames in filenames_by_dir.items(): + cols, remainders = clique.assemble(filenames) + for remainder in remainders: + output.append(cls(directory, [remainder])) + + for col in cols: + frames = list(col.indexes) + paths = [filename for filename in col] + template = col.format("{head}{padding}{tail}") + + output.append(cls( + directory, paths, frames, template + )) + + return output + + def to_dict(self): + output = { + "is_sequence": self.is_sequence, + "directory": self.directory, + "filenames": list(self.filenames), + } + if self.is_sequence: + output.update({ + "template": self.template, + "frames": list(sorted(self.frames)), + }) + + return output + + class FileDef(AbtractAttrDef): """File definition. It is possible to define filters of allowed file extensions and if supports @@ -326,7 +450,7 @@ class FileDef(AbtractAttrDef): if multipath: default = [] else: - default = "" + default = FileDefItem.create_empty_item().to_dict() else: if multipath: if not isinstance(default, (tuple, list, set)): @@ -336,11 +460,16 @@ class FileDef(AbtractAttrDef): ).format(type(default))) else: - if not isinstance(default, six.string_types): + if isinstance(default, dict): + FileDefItem.from_dict(default) + + elif isinstance(default, six.string_types): + default = FileDefItem.from_paths([default.strip()])[0] + + else: raise TypeError(( - "'default' argument must be 'str' not '{}'" + "'default' argument must be 'str' or 'dict' not '{}'" ).format(type(default))) - default = default.strip() # Change horizontal label is_label_horizontal = kwargs.get("is_label_horizontal") @@ -366,24 +495,36 @@ class FileDef(AbtractAttrDef): ) def convert_value(self, value): - if isinstance(value, six.string_types): - if self.multipath: - value = [value.strip()] - else: - value = value.strip() - return value + if isinstance(value, six.string_types) or isinstance(value, dict): + value = [value] if isinstance(value, (tuple, list, set)): - _value = [] + string_paths = [] + dict_items = [] for item in value: if isinstance(item, six.string_types): - _value.append(item.strip()) + string_paths.append(item.strip()) + elif isinstance(item, dict): + try: + FileDefItem.from_dict(item) + dict_items.append(item) + except (ValueError, KeyError): + pass + + if string_paths: + file_items = FileDefItem.from_paths(string_paths) + dict_items.extend([ + file_item.to_dict() + for file_item in file_items + ]) if self.multipath: - return _value + return dict_items - if not _value: + if not dict_items: return self.default - return _value[0].strip() + return dict_items[0] - return str(value).strip() + if self.multipath: + return [] + return FileDefItem.create_empty_item().to_dict() From a7b3a85712b1a0ee29d547dc5ddacc9dab80e160 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Apr 2022 11:55:34 +0200 Subject: [PATCH 053/109] added sequence handling to files widget --- openpype/lib/attribute_definitions.py | 24 ++++++- .../widgets/attribute_defs/files_widget.py | 63 ++++++++----------- openpype/widgets/attribute_defs/widgets.py | 4 +- 3 files changed, 52 insertions(+), 39 deletions(-) diff --git a/openpype/lib/attribute_definitions.py b/openpype/lib/attribute_definitions.py index 3d17818ecb..2cf1706b78 100644 --- a/openpype/lib/attribute_definitions.py +++ b/openpype/lib/attribute_definitions.py @@ -3,6 +3,7 @@ import re import collections import uuid import json +import copy from abc import ABCMeta, abstractmethod import six @@ -438,9 +439,23 @@ class FileDef(AbtractAttrDef): default(str, list): Defautl value. """ + default_sequence_extensions = [ + ".ani", ".anim", ".apng", ".art", ".bmp", ".bpg", ".bsave", + ".cal", ".cin", ".cpc", ".cpt", ".dds", ".dpx", ".ecw", ".exr", + ".fits", ".flic", ".flif", ".fpx", ".gif", ".hdri", ".hevc", + ".icer", ".icns", ".ico", ".cur", ".ics", ".ilbm", ".jbig", + ".jbig2", ".jng", ".jpeg", ".jpeg-ls", ".2000", ".jpg", ".xr", + ".jpeg-hdr", ".kra", ".mng", ".miff", ".nrrd", + ".ora", ".pam", ".pbm", ".pgm", ".ppm", ".pnm", ".pcx", ".pgf", + ".pictor", ".png", ".psb", ".psp", ".qtvr", ".ras", + ".rgbe", ".logluv", ".tiff", ".sgi", ".tga", ".tiff", + ".tiff/ep", ".tiff/it", ".ufo", ".ufp", ".wbmp", ".webp", + ".xbm", ".xcf", ".xpm", ".xwd" + ] + def __init__( self, key, multipath=False, folders=None, extensions=None, - default=None, **kwargs + sequence_extensions=None, default=None, **kwargs ): if folders is None and extensions is None: folders = True @@ -479,9 +494,13 @@ class FileDef(AbtractAttrDef): is_label_horizontal = False kwargs["is_label_horizontal"] = is_label_horizontal + if sequence_extensions is None: + sequence_extensions = self.default_sequence_extensions + self.multipath = multipath self.folders = folders - self.extensions = extensions + self.extensions = set(extensions) + self.sequence_extensions = set(sequence_extensions) super(FileDef, self).__init__(key, default=default, **kwargs) def __eq__(self, other): @@ -492,6 +511,7 @@ class FileDef(AbtractAttrDef): self.multipath == other.multipath and self.folders == other.folders and self.extensions == other.extensions + and self.sequence_extensions == self.sequence_extensions ) def convert_value(self, value): diff --git a/openpype/widgets/attribute_defs/files_widget.py b/openpype/widgets/attribute_defs/files_widget.py index af00ffe5ad..ffdc730455 100644 --- a/openpype/widgets/attribute_defs/files_widget.py +++ b/openpype/widgets/attribute_defs/files_widget.py @@ -73,32 +73,19 @@ class DropEmpty(QtWidgets.QWidget): class FilesModel(QtGui.QStandardItemModel): - sequence_exts = [ - ".ani", ".anim", ".apng", ".art", ".bmp", ".bpg", ".bsave", ".cal", - ".cin", ".cpc", ".cpt", ".dds", ".dpx", ".ecw", ".exr", ".fits", - ".flic", ".flif", ".fpx", ".gif", ".hdri", ".hevc", ".icer", - ".icns", ".ico", ".cur", ".ics", ".ilbm", ".jbig", ".jbig2", - ".jng", ".jpeg", ".jpeg-ls", ".2000", ".jpg", ".xr", - ".jpeg-hdr", ".kra", ".mng", ".miff", ".nrrd", - ".ora", ".pam", ".pbm", ".pgm", ".ppm", ".pnm", ".pcx", ".pgf", - ".pictor", ".png", ".psb", ".psp", ".qtvr", ".ras", - ".rgbe", ".logluv", ".tiff", ".sgi", ".tga", ".tiff", ".tiff/ep", - ".tiff/it", ".ufo", ".ufp", ".wbmp", ".webp", ".xbm", ".xcf", - ".xpm", ".xwd" - ] - - def __init__(self, multivalue): + def __init__(self, allow_multiple_items, sequence_exts): super(FilesModel, self).__init__() self._filenames_by_dirpath = collections.defaultdict(set) self._items_by_dirpath = collections.defaultdict(list) - self._multivalue = multivalue + self._allow_multiple_items = allow_multiple_items + self.sequence_exts = sequence_exts def add_filepaths(self, filepaths): if not filepaths: return - if not self._multivalue: + if not self._allow_multiple_items: filepaths = [filepaths[0]] item_ids = [] for items in self._items_by_dirpath.values(): @@ -281,6 +268,17 @@ class FilesProxyModel(QtCore.QSortFilterProxyModel): self._allowed_extensions = extensions self.invalidateFilter() + def are_valid_files(self, filepaths): + for filepath in filepaths: + if os.path.isfile(filepath): + _, ext = os.path.splitext(filepath) + if ext in self._allowed_extensions: + return True + + elif self._allow_folders: + return True + return False + def filterAcceptsRow(self, row, parent_index): model = self.sourceModel() index = model.index(row, self.filterKeyColumn(), parent_index) @@ -385,13 +383,13 @@ class FilesView(QtWidgets.QListView): class FilesWidget(QtWidgets.QFrame): value_changed = QtCore.Signal() - def __init__(self, multiselect, parent): + def __init__(self, allow_multiple_items, sequence_exts, parent): super(FilesWidget, self).__init__(parent) self.setAcceptDrops(True) empty_widget = DropEmpty(self) - files_model = FilesModel(multiselect) + files_model = FilesModel(allow_multiple_items, sequence_exts) files_proxy_model = FilesProxyModel() files_proxy_model.setSourceModel(files_model) files_view = FilesView(self) @@ -406,13 +404,9 @@ class FilesWidget(QtWidgets.QFrame): files_proxy_model.rowsInserted.connect(self._on_rows_inserted) files_proxy_model.rowsRemoved.connect(self._on_rows_removed) - drag_label = DragLabel() - drag_label.setVisible(False) - - self._drag_label = drag_label - self._in_set_value = False + self._allow_multiple_items = allow_multiple_items self._empty_widget = empty_widget self._files_model = files_model self._files_proxy_model = files_proxy_model @@ -544,8 +538,15 @@ class FilesWidget(QtWidgets.QFrame): def dragEnterEvent(self, event): mime_data = event.mimeData() if mime_data.hasUrls(): - event.setDropAction(QtCore.Qt.CopyAction) - event.accept() + filepaths = [] + for url in mime_data.urls(): + filepath = url.toLocalFile() + if os.path.exists(filepath): + filepaths.append(filepath) + + if self._files_proxy_model.are_valid_files(filepaths): + event.setDropAction(QtCore.Qt.CopyAction) + event.accept() def dragLeaveEvent(self, event): event.accept() @@ -574,13 +575,3 @@ class FilesWidget(QtWidgets.QFrame): files_exists = self._files_proxy_model.rowCount() > 0 self._files_view.setVisible(files_exists) self._empty_widget.setVisible(not files_exists) - - -class DragLabel(QtWidgets.QWidget): - def __init__(self, parent=None): - super(DragLabel, self).__init__(parent) - - t_label = QtWidgets.QLabel("TESTING", self) - layout = QtWidgets.QHBoxLayout(self) - layout.addWidget(t_label) - self._t_label = t_label diff --git a/openpype/widgets/attribute_defs/widgets.py b/openpype/widgets/attribute_defs/widgets.py index 83eeaea61f..d3f53de032 100644 --- a/openpype/widgets/attribute_defs/widgets.py +++ b/openpype/widgets/attribute_defs/widgets.py @@ -340,7 +340,9 @@ class FileAttrWidget(_BaseAttrDefWidget): def _ui_init(self): self.multipath = self.attr_def.multipath - input_widget = FilesWidget(self.multipath, self) + input_widget = FilesWidget( + self.multipath, self.attr_def.sequence_extensions, self + ) if self.attr_def.tooltip: input_widget.setToolTip(self.attr_def.tooltip) From 0b7cdeee840650816fd379066915b53c1a9f58fa Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Apr 2022 12:12:43 +0200 Subject: [PATCH 054/109] disable always on top flags --- openpype/tools/traypublisher/window.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openpype/tools/traypublisher/window.py b/openpype/tools/traypublisher/window.py index a550c88ead..306b567acd 100644 --- a/openpype/tools/traypublisher/window.py +++ b/openpype/tools/traypublisher/window.py @@ -108,6 +108,13 @@ class TrayPublishWindow(PublisherWindow): def __init__(self, *args, **kwargs): super(TrayPublishWindow, self).__init__(reset_on_show=False) + flags = self.windowFlags() + # Disable always on top hint + if flags & QtCore.Qt.WindowStaysOnTopHint: + flags ^= QtCore.Qt.WindowStaysOnTopHint + + self.setWindowFlags(flags) + overlay_widget = StandaloneOverlayWidget(self) btns_widget = QtWidgets.QWidget(self) From ed98bbcd322dd98aade17e229e745ed890e3c85a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Apr 2022 14:54:19 +0200 Subject: [PATCH 055/109] changed how files widget works with extensions and handle file items --- openpype/lib/__init__.py | 2 + openpype/lib/attribute_definitions.py | 83 ++++++- .../widgets/attribute_defs/files_widget.py | 214 +++++------------- 3 files changed, 141 insertions(+), 158 deletions(-) diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index b57e469f5b..d053ec8636 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -42,6 +42,7 @@ from .attribute_definitions import ( EnumDef, BoolDef, FileDef, + FileDefItem, ) from .env_tools import ( @@ -266,6 +267,7 @@ __all__ = [ "EnumDef", "BoolDef", "FileDef", + "FileDefItem", "import_filepath", "modules_from_path", diff --git a/openpype/lib/attribute_definitions.py b/openpype/lib/attribute_definitions.py index 2cf1706b78..6e754e6668 100644 --- a/openpype/lib/attribute_definitions.py +++ b/openpype/lib/attribute_definitions.py @@ -334,6 +334,61 @@ class FileDefItem(object): os.path.join(self.directory, filename) ) + @property + def label(self): + if not self.is_sequence: + return self.filenames[0] + + frame_start = self.frames[0] + filename_template = os.path.basename(self.template) + if len(self.frames) == 1: + return "{} [{}]".format(filename_template, frame_start) + + frame_end = self.frames[-1] + expected_len = (frame_end - frame_start) + 1 + if expected_len == len(self.frames): + return "{} [{}-{}]".format( + filename_template, frame_start, frame_end + ) + + ranges = [] + _frame_start = None + _frame_end = None + for frame in range(frame_start, frame_end + 1): + if frame not in self.frames: + add_to_ranges = _frame_start is not None + elif _frame_start is None: + _frame_start = _frame_end = frame + add_to_ranges = frame == frame_end + else: + _frame_end = frame + add_to_ranges = frame == frame_end + + if add_to_ranges: + if _frame_start != _frame_end: + _range = "{}-{}".format(_frame_start, _frame_end) + else: + _range = str(_frame_start) + ranges.append(_range) + _frame_start = _frame_end = None + return "{} [{}]".format( + filename_template, ",".join(ranges) + ) + + @property + def ext(self): + _, ext = os.path.splitext(self.filenames[0]) + if ext: + return ext + return None + + @property + def is_dir(self): + # QUESTION a better way how to define folder (in init argument?) + if self.ext: + return False + return True + def set_directory(self, directory): self.directory = directory @@ -357,23 +412,30 @@ class FileDefItem(object): return cls("", "") @classmethod - def from_value(cls, value): + def from_value(cls, value, sequence_extensions): multi = isinstance(value, (list, tuple, set)) if not multi: value = [value] output = [] + str_filepaths = [] for item in value: - if isinstance(item, dict): + if isinstance(item, FileDefItem): + output.append(item) + elif isinstance(item, dict): output.append(cls.from_dict(item)) elif isinstance(item, six.string_types): - output.extend(cls.from_paths([item])) + str_filepaths.append(item) else: raise TypeError( "Unknown type \"{}\". Can't convert to {}".format( str(type(item)), cls.__name__ ) ) + + if str_filepaths: + output.extend(cls.from_paths(str_filepaths, sequence_extensions)) + if multi: return output return output[0] @@ -388,7 +450,7 @@ class FileDefItem(object): ) @classmethod - def from_paths(cls, paths): + def from_paths(cls, paths, sequence_extensions): filenames_by_dir = collections.defaultdict(list) for path in paths: normalized = os.path.normpath(path) @@ -397,7 +459,18 @@ class FileDefItem(object): output = [] for directory, filenames in filenames_by_dir.items(): - cols, remainders = clique.assemble(filenames) + filtered_filenames = [] + for filename in filenames: + _, ext = os.path.splitext(filename) + if ext in sequence_extensions: + filtered_filenames.append(filename) + else: + output.append(cls(directory, [filename])) + + if not filtered_filenames: + continue + + cols, remainders = clique.assemble(filtered_filenames) for remainder in remainders: output.append(cls(directory, [remainder])) diff --git a/openpype/widgets/attribute_defs/files_widget.py b/openpype/widgets/attribute_defs/files_widget.py index ffdc730455..6e2ab6e4f2 100644 --- a/openpype/widgets/attribute_defs/files_widget.py +++ b/openpype/widgets/attribute_defs/files_widget.py @@ -2,8 +2,10 @@ import os import collections import uuid import clique +import six from Qt import QtWidgets, QtCore, QtGui +from openpype.lib import FileDefItem from openpype.tools.utils import paint_image_with_color # TODO change imports from openpype.tools.resources import ( @@ -75,174 +77,76 @@ class DropEmpty(QtWidgets.QWidget): class FilesModel(QtGui.QStandardItemModel): def __init__(self, allow_multiple_items, sequence_exts): super(FilesModel, self).__init__() + + self._allow_multiple_items = allow_multiple_items + self._sequence_exts = sequence_exts + + self._items_by_id = {} + self._file_items_by_id = {} self._filenames_by_dirpath = collections.defaultdict(set) self._items_by_dirpath = collections.defaultdict(list) - self._allow_multiple_items = allow_multiple_items - self.sequence_exts = sequence_exts + def add_filepaths(self, items): + if not items: + return - def add_filepaths(self, filepaths): - if not filepaths: + obj_items = FileDefItem.from_value(items, self._sequence_exts) + if not obj_items: return if not self._allow_multiple_items: - filepaths = [filepaths[0]] - item_ids = [] - for items in self._items_by_dirpath.values(): - for item in items: - item_id = item.data(ITEM_ID_ROLE) - if item_id: - item_ids.append(item_id) + obj_items = [obj_items[0]] + current_ids = list(self._file_items_by_id.keys()) + if current_ids: + self.remove_item_by_ids(current_ids) - if item_ids: - self.remove_item_by_ids(item_ids) + new_model_items = [] + for obj_item in obj_items: + _, ext = os.path.splitext(obj_item.filenames[0]) + if ext: + icon_pixmap = get_pixmap(filename="file.png") + else: + icon_pixmap = get_pixmap(filename="folder.png") - new_dirpaths = set() - for filepath in filepaths: - filename = os.path.basename(filepath) - dirpath = os.path.dirname(filepath) - filenames = self._filenames_by_dirpath[dirpath] - if filename not in filenames: - new_dirpaths.add(dirpath) - filenames.add(filename) - self._refresh_items(new_dirpaths) + item_id, model_item = self._create_item(obj_item, icon_pixmap) + new_model_items.append(model_item) + self._file_items_by_id[item_id] = obj_item + self._items_by_id[item_id] = model_item + + if new_model_items: + self.invisibleRootItem().appendRows(new_model_items) def remove_item_by_ids(self, item_ids): if not item_ids: return - remaining_ids = set(item_ids) - result = collections.defaultdict(list) - for dirpath, items in self._items_by_dirpath.items(): - if not remaining_ids: - break + items = [] + for item_id in set(item_ids): + if item_id not in self._items_by_id: + continue + item = self._items_by_id.pop(item_id) + self._file_items_by_id.pop(item_id) + items.append(item) + + if items: for item in items: - if not remaining_ids: - break - item_id = item.data(ITEM_ID_ROLE) - if item_id in remaining_ids: - remaining_ids.remove(item_id) - result[dirpath].append(item) - - if not result: - return - - dirpaths = set(result.keys()) - for dirpath, items in result.items(): - filenames_cache = self._filenames_by_dirpath[dirpath] - for item in items: - filenames = item.data(FILENAMES_ROLE) - - self._items_by_dirpath[dirpath].remove(item) - self.removeRows(item.row(), 1) - for filename in filenames: - if filename in filenames_cache: - filenames_cache.remove(filename) - - self._refresh_items(dirpaths) - - def _refresh_items(self, dirpaths=None): - if dirpaths is None: - dirpaths = set(self._items_by_dirpath.keys()) - - new_items = [] - for dirpath in dirpaths: - items_to_remove = list(self._items_by_dirpath[dirpath]) - cols, remainders = clique.assemble( - self._filenames_by_dirpath[dirpath] - ) - filtered_cols = [] - for collection in cols: - filenames = set(collection) - valid_col = True - for filename in filenames: - ext = os.path.splitext(filename)[-1] - valid_col = ext in self.sequence_exts - break - - if valid_col: - filtered_cols.append(collection) - else: - for filename in filenames: - remainders.append(filename) - - for filename in remainders: - found = False - for item in items_to_remove: - item_filenames = item.data(FILENAMES_ROLE) - if filename in item_filenames and len(item_filenames) == 1: - found = True - items_to_remove.remove(item) - break - - if found: - continue - - fullpath = os.path.join(dirpath, filename) - if os.path.isdir(fullpath): - icon_pixmap = get_pixmap(filename="folder.png") - else: - icon_pixmap = get_pixmap(filename="file.png") - label = filename - filenames = [filename] - item = self._create_item( - label, filenames, dirpath, icon_pixmap - ) - new_items.append(item) - self._items_by_dirpath[dirpath].append(item) - - for collection in filtered_cols: - filenames = set(collection) - found = False - for item in items_to_remove: - item_filenames = item.data(FILENAMES_ROLE) - if item_filenames == filenames: - found = True - items_to_remove.remove(item) - break - - if found: - continue - - col_range = collection.format("{ranges}") - label = "{}<{}>{}".format( - collection.head, col_range, collection.tail - ) - icon_pixmap = get_pixmap(filename="files.png") - item = self._create_item( - label, filenames, dirpath, icon_pixmap - ) - new_items.append(item) - self._items_by_dirpath[dirpath].append(item) - - for item in items_to_remove: - self._items_by_dirpath[dirpath].remove(item) self.removeRows(item.row(), 1) - if new_items: - self.invisibleRootItem().appendRows(new_items) - - def _create_item(self, label, filenames, dirpath, icon_pixmap=None): - first_filename = None - for filename in filenames: - first_filename = filename - break - ext = os.path.splitext(first_filename)[-1] - is_dir = False - if len(filenames) == 1: - filepath = os.path.join(dirpath, first_filename) - is_dir = os.path.isdir(filepath) + def get_file_item_by_id(self, item_id): + return self._file_items_by_id.get(item_id) + def _create_item(self, file_item, icon_pixmap=None): item = QtGui.QStandardItem() - item.setData(str(uuid.uuid4()), ITEM_ID_ROLE) - item.setData(label, ITEM_LABEL_ROLE) - item.setData(filenames, FILENAMES_ROLE) - item.setData(dirpath, DIRPATH_ROLE) + item_id = str(uuid.uuid4()) + item.setData(item_id, ITEM_ID_ROLE) + item.setData(file_item.label, ITEM_LABEL_ROLE) + item.setData(file_item.filenames, FILENAMES_ROLE) + item.setData(file_item.directory, DIRPATH_ROLE) item.setData(icon_pixmap, ITEM_ICON_ROLE) - item.setData(ext, EXT_ROLE) - item.setData(is_dir, IS_DIR_ROLE) + item.setData(file_item.ext, EXT_ROLE) + item.setData(file_item.is_dir, IS_DIR_ROLE) - return item + return item_id, item class FilesProxyModel(QtCore.QSortFilterProxyModel): @@ -344,6 +248,7 @@ class ItemWidget(QtWidgets.QWidget): class FilesView(QtWidgets.QListView): """View showing instances and their groups.""" + def __init__(self, *args, **kwargs): super(FilesView, self).__init__(*args, **kwargs) @@ -439,14 +344,17 @@ class FilesWidget(QtWidgets.QFrame): def current_value(self): model = self._files_proxy_model - filepaths = set() + item_ids = set() for row in range(model.rowCount()): index = model.index(row, 0) - dirpath = index.data(DIRPATH_ROLE) - filenames = index.data(FILENAMES_ROLE) - for filename in filenames: - filepaths.add(os.path.join(dirpath, filename)) - return list(filepaths) + item_ids.add(index.data(ITEM_ID_ROLE)) + + file_items = [] + for item_id in item_ids: + file_item = self._files_model.get_file_item_by_id(item_id) + if file_item is not None: + file_items.append(file_item.to_dict()) + return file_items def set_filters(self, folders_allowed, exts_filter): self._files_proxy_model.set_allow_folders(folders_allowed) From 7fe279fda5ff295e9673d6d54366f8a0dd662b1b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Apr 2022 15:10:18 +0200 Subject: [PATCH 056/109] added missing plugins file --- openpype/hosts/traypublisher/api/plugin.py | 104 +++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 openpype/hosts/traypublisher/api/plugin.py diff --git a/openpype/hosts/traypublisher/api/plugin.py b/openpype/hosts/traypublisher/api/plugin.py new file mode 100644 index 0000000000..6907450b15 --- /dev/null +++ b/openpype/hosts/traypublisher/api/plugin.py @@ -0,0 +1,104 @@ +from openpype.pipeline import ( + Creator, + CreatedInstance +) +from openpype.lib import ( + FileDef, + BoolDef, +) + +from .pipeline import ( + list_instances, + update_instances, + remove_instances, + HostContext, +) + + +class TrayPublishCreator(Creator): + create_allow_context_change = True + + def collect_instances(self): + for instance_data in list_instances(): + creator_id = instance_data.get("creator_identifier") + if creator_id == self.identifier: + instance = CreatedInstance.from_existing( + instance_data, self + ) + self._add_instance_to_context(instance) + + def update_instances(self, update_list): + update_instances(update_list) + + def remove_instances(self, instances): + remove_instances(instances) + for instance in instances: + self._remove_instance_from_context(instance) + + def get_pre_create_attr_defs(self): + # Use same attributes as for instance attrobites + return self.get_instance_attr_defs() + + +class SettingsCreator(TrayPublishCreator): + create_allow_context_change = True + + enable_review = False + extensions = [] + sequence_extensions = [] + + def collect_instances(self): + for instance_data in list_instances(): + creator_id = instance_data.get("creator_identifier") + if creator_id == self.identifier: + instance = CreatedInstance.from_existing( + instance_data, self + ) + self._add_instance_to_context(instance) + + def create(self, subset_name, data, pre_create_data): + # Pass precreate data to creator attributes + data["creator_attributes"] = pre_create_data + # Create new instance + new_instance = CreatedInstance(self.family, subset_name, data, self) + # Host implementation of storing metadata about instance + HostContext.add_instance(new_instance.data_to_store()) + # Add instance to current context + self._add_instance_to_context(new_instance) + + def get_instance_attr_defs(self): + output = [] + + file_def = FileDef( + "filepath", + folders=False, + extensions=self.extensions, + sequence_extensions=self.sequence_extensions, + label="Filepath" + ) + output.append(file_def) + if self.enable_review: + output.append(BoolDef("review", label="Review")) + return output + + @classmethod + def from_settings(cls, item_data): + identifier = item_data["identifier"] + family = item_data["family"] + if not identifier: + identifier = "settings_{}".format(family) + return type( + "{}{}".format(cls.__name__, identifier), + (cls, ), + { + "family": family, + "identifier": identifier, + "label": item_data["label"].strip(), + "icon": item_data["icon"], + "description": item_data["description"], + "enable_review": item_data["enable_review"], + "extensions": item_data["extensions"], + "sequence_extensions": item_data["sequence_extensions"], + "default_variants": item_data["default_variants"] + } + ) From 2efd8b774cf4e24823fd98225fd277a2353f273c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Apr 2022 15:24:37 +0200 Subject: [PATCH 057/109] changed multi item to single item --- openpype/hosts/traypublisher/api/plugin.py | 2 +- openpype/lib/attribute_definitions.py | 43 ++++++++++--------- .../widgets/attribute_defs/files_widget.py | 11 +++-- openpype/widgets/attribute_defs/widgets.py | 4 +- 4 files changed, 29 insertions(+), 31 deletions(-) diff --git a/openpype/hosts/traypublisher/api/plugin.py b/openpype/hosts/traypublisher/api/plugin.py index 6907450b15..d31e0a1ef7 100644 --- a/openpype/hosts/traypublisher/api/plugin.py +++ b/openpype/hosts/traypublisher/api/plugin.py @@ -74,7 +74,7 @@ class SettingsCreator(TrayPublishCreator): folders=False, extensions=self.extensions, sequence_extensions=self.sequence_extensions, - label="Filepath" + label="Filepath", ) output.append(file_def) if self.enable_review: diff --git a/openpype/lib/attribute_definitions.py b/openpype/lib/attribute_definitions.py index 6e754e6668..7a00fcdeb4 100644 --- a/openpype/lib/attribute_definitions.py +++ b/openpype/lib/attribute_definitions.py @@ -505,7 +505,7 @@ class FileDef(AbtractAttrDef): It is possible to define filters of allowed file extensions and if supports folders. Args: - multipath(bool): Allow multiple path. + single_item(bool): Allow only single path item. folders(bool): Allow folder paths. extensions(list): Allow files with extensions. Empty list will allow all extensions and None will disable files completely. @@ -527,7 +527,7 @@ class FileDef(AbtractAttrDef): ] def __init__( - self, key, multipath=False, folders=None, extensions=None, + self, key, single_item=True, folders=None, extensions=None, sequence_extensions=None, default=None, **kwargs ): if folders is None and extensions is None: @@ -535,19 +535,12 @@ class FileDef(AbtractAttrDef): extensions = [] if default is None: - if multipath: - default = [] - else: + if single_item: default = FileDefItem.create_empty_item().to_dict() - else: - if multipath: - if not isinstance(default, (tuple, list, set)): - raise TypeError(( - "'default' argument must be 'list', 'tuple' or 'set'" - ", not '{}'" - ).format(type(default))) - else: + default = [] + else: + if single_item: if isinstance(default, dict): FileDefItem.from_dict(default) @@ -559,18 +552,26 @@ class FileDef(AbtractAttrDef): "'default' argument must be 'str' or 'dict' not '{}'" ).format(type(default))) + else: + if not isinstance(default, (tuple, list, set)): + raise TypeError(( + "'default' argument must be 'list', 'tuple' or 'set'" + ", not '{}'" + ).format(type(default))) + # Change horizontal label is_label_horizontal = kwargs.get("is_label_horizontal") if is_label_horizontal is None: - is_label_horizontal = True - if multipath: + if single_item: + is_label_horizontal = True + else: is_label_horizontal = False kwargs["is_label_horizontal"] = is_label_horizontal if sequence_extensions is None: sequence_extensions = self.default_sequence_extensions - self.multipath = multipath + self.single_item = single_item self.folders = folders self.extensions = set(extensions) self.sequence_extensions = set(sequence_extensions) @@ -581,7 +582,7 @@ class FileDef(AbtractAttrDef): return False return ( - self.multipath == other.multipath + self.single_item == other.single_item and self.folders == other.folders and self.extensions == other.extensions and self.sequence_extensions == self.sequence_extensions @@ -611,13 +612,13 @@ class FileDef(AbtractAttrDef): for file_item in file_items ]) - if self.multipath: + if not self.single_item: return dict_items if not dict_items: return self.default return dict_items[0] - if self.multipath: - return [] - return FileDefItem.create_empty_item().to_dict() + if self.single_item: + return FileDefItem.create_empty_item().to_dict() + return [] diff --git a/openpype/widgets/attribute_defs/files_widget.py b/openpype/widgets/attribute_defs/files_widget.py index 6e2ab6e4f2..e41387e0e5 100644 --- a/openpype/widgets/attribute_defs/files_widget.py +++ b/openpype/widgets/attribute_defs/files_widget.py @@ -75,10 +75,10 @@ class DropEmpty(QtWidgets.QWidget): class FilesModel(QtGui.QStandardItemModel): - def __init__(self, allow_multiple_items, sequence_exts): + def __init__(self, single_item, sequence_exts): super(FilesModel, self).__init__() - self._allow_multiple_items = allow_multiple_items + self._single_item = single_item self._sequence_exts = sequence_exts self._items_by_id = {} @@ -94,7 +94,7 @@ class FilesModel(QtGui.QStandardItemModel): if not obj_items: return - if not self._allow_multiple_items: + if self._single_item: obj_items = [obj_items[0]] current_ids = list(self._file_items_by_id.keys()) if current_ids: @@ -288,13 +288,13 @@ class FilesView(QtWidgets.QListView): class FilesWidget(QtWidgets.QFrame): value_changed = QtCore.Signal() - def __init__(self, allow_multiple_items, sequence_exts, parent): + def __init__(self, single_item, sequence_exts, parent): super(FilesWidget, self).__init__(parent) self.setAcceptDrops(True) empty_widget = DropEmpty(self) - files_model = FilesModel(allow_multiple_items, sequence_exts) + files_model = FilesModel(single_item, sequence_exts) files_proxy_model = FilesProxyModel() files_proxy_model.setSourceModel(files_model) files_view = FilesView(self) @@ -311,7 +311,6 @@ class FilesWidget(QtWidgets.QFrame): self._in_set_value = False - self._allow_multiple_items = allow_multiple_items self._empty_widget = empty_widget self._files_model = files_model self._files_proxy_model = files_proxy_model diff --git a/openpype/widgets/attribute_defs/widgets.py b/openpype/widgets/attribute_defs/widgets.py index d3f53de032..62877be4cf 100644 --- a/openpype/widgets/attribute_defs/widgets.py +++ b/openpype/widgets/attribute_defs/widgets.py @@ -338,10 +338,8 @@ class UnknownAttrWidget(_BaseAttrDefWidget): class FileAttrWidget(_BaseAttrDefWidget): def _ui_init(self): - self.multipath = self.attr_def.multipath - input_widget = FilesWidget( - self.multipath, self.attr_def.sequence_extensions, self + self.attr_def.single_item, self.attr_def.sequence_extensions, self ) if self.attr_def.tooltip: From c4e826e77f97191b267421ccf23f9b2401ddc35b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Apr 2022 17:29:32 +0200 Subject: [PATCH 058/109] added ability to create copy of TemplateResult --- openpype/lib/path_templates.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/openpype/lib/path_templates.py b/openpype/lib/path_templates.py index 14e5fe59f8..5c40aa4549 100644 --- a/openpype/lib/path_templates.py +++ b/openpype/lib/path_templates.py @@ -365,6 +365,7 @@ class TemplateResult(str): when value of key in data is dictionary but template expect string of number. """ + used_values = None solved = None template = None @@ -383,6 +384,12 @@ class TemplateResult(str): new_obj.invalid_types = invalid_types return new_obj + def __copy__(self, *args, **kwargs): + return self.copy() + + def __deepcopy__(self, *args, **kwargs): + return self.copy() + def validate(self): if not self.solved: raise TemplateUnsolved( @@ -391,6 +398,17 @@ class TemplateResult(str): self.invalid_types ) + def copy(self): + cls = self.__class__ + return cls( + str(self), + self.template, + self.solved, + self.used_values, + self.missing_keys, + self.invalid_types + ) + class TemplatesResultDict(dict): """Holds and wrap TemplateResults for easy bug report.""" From aace513c84c1f2f901be54139759dcc3bddf3a5d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Apr 2022 19:25:57 +0200 Subject: [PATCH 059/109] moved remove button to view's bottom --- .../widgets/attribute_defs/files_widget.py | 70 ++++++++++--------- 1 file changed, 36 insertions(+), 34 deletions(-) diff --git a/openpype/widgets/attribute_defs/files_widget.py b/openpype/widgets/attribute_defs/files_widget.py index e41387e0e5..f483fe7ef5 100644 --- a/openpype/widgets/attribute_defs/files_widget.py +++ b/openpype/widgets/attribute_defs/files_widget.py @@ -1,8 +1,7 @@ import os import collections import uuid -import clique -import six + from Qt import QtWidgets, QtCore, QtGui from openpype.lib import FileDefItem @@ -213,8 +212,6 @@ class FilesProxyModel(QtCore.QSortFilterProxyModel): class ItemWidget(QtWidgets.QWidget): - remove_requested = QtCore.Signal(str) - def __init__(self, item_id, label, pixmap_icon, parent=None): self._item_id = item_id @@ -224,31 +221,21 @@ class ItemWidget(QtWidgets.QWidget): icon_widget = PixmapLabel(pixmap_icon, self) label_widget = QtWidgets.QLabel(label, self) - pixmap = paint_image_with_color( - get_image(filename="delete.png"), QtCore.Qt.white - ) - remove_btn = IconButton(self) - remove_btn.setIcon(QtGui.QIcon(pixmap)) layout = QtWidgets.QHBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(icon_widget, 0) layout.addWidget(label_widget, 1) - layout.addWidget(remove_btn, 0) - - remove_btn.clicked.connect(self._on_remove_clicked) self._icon_widget = icon_widget self._label_widget = label_widget - self._remove_btn = remove_btn - - def _on_remove_clicked(self): - self.remove_requested.emit(self._item_id) class FilesView(QtWidgets.QListView): """View showing instances and their groups.""" + remove_requested = QtCore.Signal() + def __init__(self, *args, **kwargs): super(FilesView, self).__init__(*args, **kwargs) @@ -257,6 +244,17 @@ class FilesView(QtWidgets.QListView): QtWidgets.QAbstractItemView.ExtendedSelection ) + remove_btn = IconButton(self) + pix = paint_image_with_color( + get_image(filename="delete.png"), QtCore.Qt.white + ) + icon = QtGui.QIcon(pix) + remove_btn.setIcon(icon) + + remove_btn.clicked.connect(self._on_remove_clicked) + + self._remove_btn = remove_btn + def get_selected_item_ids(self): """Ids of selected instances.""" selected_item_ids = set() @@ -284,6 +282,24 @@ class FilesView(QtWidgets.QListView): return super(FilesView, self).event(event) + def _on_remove_clicked(self): + self.remove_requested.emit() + + def _update_remove_btn(self): + viewport = self.viewport() + height = viewport.height() + pos_x = viewport.width() - self._remove_btn.width() - 5 + pos_y = height - self._remove_btn.height() - 5 + self._remove_btn.move(max(0, pos_x), max(0, pos_y)) + + def resizeEvent(self, event): + super(FilesView, self).resizeEvent(event) + self._update_remove_btn() + + def showEvent(self, event): + super(FilesView, self).showEvent(event) + self._update_remove_btn() + class FilesWidget(QtWidgets.QFrame): value_changed = QtCore.Signal() @@ -308,7 +324,7 @@ class FilesWidget(QtWidgets.QFrame): files_proxy_model.rowsInserted.connect(self._on_rows_inserted) files_proxy_model.rowsRemoved.connect(self._on_rows_removed) - + files_view.remove_requested.connect(self._on_remove_requested) self._in_set_value = False self._empty_widget = empty_widget @@ -373,7 +389,6 @@ class FilesWidget(QtWidgets.QFrame): self._files_proxy_model.setData( index, widget.sizeHint(), QtCore.Qt.SizeHintRole ) - widget.remove_requested.connect(self._on_remove_request) self._widgets_by_id[item_id] = widget self._files_proxy_model.sort(0) @@ -401,23 +416,10 @@ class FilesWidget(QtWidgets.QFrame): if not self._in_set_value: self.value_changed.emit() - def _on_remove_request(self, item_id): - found_index = None - for row in range(self._files_model.rowCount()): - index = self._files_model.index(row, 0) - _item_id = index.data(ITEM_ID_ROLE) - if item_id == _item_id: - found_index = index - break - - if found_index is None: - return - + def _on_remove_requested(self): items_to_delete = self._files_view.get_selected_item_ids() - if item_id not in items_to_delete: - items_to_delete = [item_id] - - self._remove_item_by_ids(items_to_delete) + if items_to_delete: + self._remove_item_by_ids(items_to_delete) def sizeHint(self): # Get size hints of widget and visible widgets From 05ede8031e26f3f86408d1aa88aef8b10b6ad2de Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Apr 2022 19:31:48 +0200 Subject: [PATCH 060/109] a little bit nicer look of items in files widget --- .../widgets/attribute_defs/files_widget.py | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/openpype/widgets/attribute_defs/files_widget.py b/openpype/widgets/attribute_defs/files_widget.py index f483fe7ef5..72bfd6cfa2 100644 --- a/openpype/widgets/attribute_defs/files_widget.py +++ b/openpype/widgets/attribute_defs/files_widget.py @@ -89,31 +89,26 @@ class FilesModel(QtGui.QStandardItemModel): if not items: return - obj_items = FileDefItem.from_value(items, self._sequence_exts) - if not obj_items: + file_items = FileDefItem.from_value(items, self._sequence_exts) + if not file_items: return if self._single_item: - obj_items = [obj_items[0]] + file_items = [file_items[0]] current_ids = list(self._file_items_by_id.keys()) if current_ids: self.remove_item_by_ids(current_ids) new_model_items = [] - for obj_item in obj_items: - _, ext = os.path.splitext(obj_item.filenames[0]) - if ext: - icon_pixmap = get_pixmap(filename="file.png") - else: - icon_pixmap = get_pixmap(filename="folder.png") - - item_id, model_item = self._create_item(obj_item, icon_pixmap) + for file_item in file_items: + item_id, model_item = self._create_item(file_item) new_model_items.append(model_item) - self._file_items_by_id[item_id] = obj_item + self._file_items_by_id[item_id] = file_item self._items_by_id[item_id] = model_item if new_model_items: - self.invisibleRootItem().appendRows(new_model_items) + roow_item = self.invisibleRootItem() + roow_item.appendRows(new_model_items) def remove_item_by_ids(self, item_ids): if not item_ids: @@ -134,7 +129,16 @@ class FilesModel(QtGui.QStandardItemModel): def get_file_item_by_id(self, item_id): return self._file_items_by_id.get(item_id) - def _create_item(self, file_item, icon_pixmap=None): + def _create_item(self, file_item): + if file_item.is_dir: + icon_pixmap = paint_image_with_color( + get_image(filename="folder.png"), QtCore.Qt.white + ) + else: + icon_pixmap = paint_image_with_color( + get_image(filename="file.png"), QtCore.Qt.white + ) + item = QtGui.QStandardItem() item_id = str(uuid.uuid4()) item.setData(item_id, ITEM_ID_ROLE) @@ -223,7 +227,7 @@ class ItemWidget(QtWidgets.QWidget): label_widget = QtWidgets.QLabel(label, self) layout = QtWidgets.QHBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) + layout.setContentsMargins(5, 5, 0, 5) layout.addWidget(icon_widget, 0) layout.addWidget(label_widget, 1) From cc1f800740c77b5a5fafdb665a03ad9b630b239f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 26 Apr 2022 10:12:35 +0200 Subject: [PATCH 061/109] added testing widget for attribute definitions --- openpype/widgets/attribute_defs/__init__.py | 6 +- openpype/widgets/attribute_defs/widgets.py | 102 ++++++++++++++++++++ 2 files changed, 107 insertions(+), 1 deletion(-) diff --git a/openpype/widgets/attribute_defs/__init__.py b/openpype/widgets/attribute_defs/__init__.py index 147efeb3d6..ce6b80109e 100644 --- a/openpype/widgets/attribute_defs/__init__.py +++ b/openpype/widgets/attribute_defs/__init__.py @@ -1,6 +1,10 @@ -from .widgets import create_widget_for_attr_def +from .widgets import ( + create_widget_for_attr_def, + AttributeDefinitionsWidget, +) __all__ = ( "create_widget_for_attr_def", + "AttributeDefinitionsWidget", ) diff --git a/openpype/widgets/attribute_defs/widgets.py b/openpype/widgets/attribute_defs/widgets.py index 62877be4cf..3f36c078cb 100644 --- a/openpype/widgets/attribute_defs/widgets.py +++ b/openpype/widgets/attribute_defs/widgets.py @@ -1,4 +1,5 @@ import uuid +import copy from Qt import QtWidgets, QtCore @@ -10,6 +11,7 @@ from openpype.lib.attribute_definitions import ( EnumDef, BoolDef, FileDef, + UIDef, UISeparatorDef, UILabelDef ) @@ -53,6 +55,106 @@ def create_widget_for_attr_def(attr_def, parent=None): )) +class AttributeDefinitionsWidget(QtWidgets.QWidget): + """Create widgets for attribute definitions in grid layout. + + Widget creates input widgets for passed attribute definitions. + + Widget can't handle multiselection values. + """ + + def __init__(self, attr_defs=None, parent=None): + super(AttributeDefinitionsWidget, self).__init__(parent) + + self._widgets = [] + self._current_keys = set() + + self.set_attr_defs(attr_defs) + + def clear_attr_defs(self): + """Remove all existing widgets and reset layout if needed.""" + self._widgets = [] + self._current_keys = set() + + layout = self.layout() + if layout is not None: + if layout.count() == 0: + return + + while layout.count(): + item = layout.takeAt(0) + widget = item.widget() + if widget: + widget.setVisible(False) + widget.deleteLater() + + layout.deleteLater() + + new_layout = QtWidgets.QGridLayout() + self.setLayout(new_layout) + + def set_attr_defs(self, attr_defs): + """Replace current attribute definitions with passed.""" + self.clear_attr_defs() + if attr_defs: + self.add_attr_defs(attr_defs) + + def add_attr_defs(self, attr_defs): + """Add attribute definitions to current.""" + layout = self.layout() + + row = 0 + for attr_def in attr_defs: + if attr_def.key in self._current_keys: + raise KeyError("Duplicated key \"{}\"".format(attr_def.key)) + + self._current_keys.add(attr_def.key) + widget = create_widget_for_attr_def(attr_def, self) + + expand_cols = 2 + if attr_def.is_value_def and attr_def.is_label_horizontal: + expand_cols = 1 + + col_num = 2 - expand_cols + + if attr_def.label: + label_widget = QtWidgets.QLabel(attr_def.label, self) + layout.addWidget( + label_widget, row, 0, 1, expand_cols + ) + if not attr_def.is_label_horizontal: + row += 1 + + layout.addWidget( + widget, row, col_num, 1, expand_cols + ) + self._widgets.append(widget) + row += 1 + + def set_value(self, value): + new_value = copy.deepcopy(value) + unused_keys = set(new_value.keys()) + for widget in self._widgets: + attr_def = widget.attr_def + if attr_def.key not in new_value: + continue + unused_keys.remove(attr_def.key) + + widget_value = new_value[attr_def.key] + if widget_value is None: + widget_value = copy.deepcopy(attr_def.default) + widget.set_value(widget_value) + + def current_value(self): + output = {} + for widget in self._widgets: + attr_def = widget.attr_def + if not isinstance(attr_def, UIDef): + output[attr_def.key] = widget.current_value() + + return output + + class _BaseAttrDefWidget(QtWidgets.QWidget): # Type 'object' may not work with older PySide versions value_changed = QtCore.Signal(object, uuid.UUID) From c9f35c480507471128efbca377ad71464f028788 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 26 Apr 2022 12:08:07 +0200 Subject: [PATCH 062/109] remove button is enabled/disabled on selection change --- openpype/style/style.css | 8 +++++ .../widgets/attribute_defs/files_widget.py | 30 +++++++++++++++++-- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/openpype/style/style.css b/openpype/style/style.css index 9df615d953..59253a474c 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -1340,3 +1340,11 @@ VariantInputsWidget QToolButton { #LikeDisabledInput:focus { border-color: {color:border}; } + +/* Attribute Definition widgets */ +InViewButton, InViewButton:disabled { + background: transparent; +} +InViewButton:hover { + background: rgba(255, 255, 255, 37); +} diff --git a/openpype/widgets/attribute_defs/files_widget.py b/openpype/widgets/attribute_defs/files_widget.py index 72bfd6cfa2..3a9455584c 100644 --- a/openpype/widgets/attribute_defs/files_widget.py +++ b/openpype/widgets/attribute_defs/files_widget.py @@ -235,6 +235,10 @@ class ItemWidget(QtWidgets.QWidget): self._label_widget = label_widget +class InViewButton(IconButton): + pass + + class FilesView(QtWidgets.QListView): """View showing instances and their groups.""" @@ -248,17 +252,34 @@ class FilesView(QtWidgets.QListView): QtWidgets.QAbstractItemView.ExtendedSelection ) - remove_btn = IconButton(self) - pix = paint_image_with_color( + remove_btn = InViewButton(self) + pix_enabled = paint_image_with_color( get_image(filename="delete.png"), QtCore.Qt.white ) - icon = QtGui.QIcon(pix) + pix_disabled = paint_image_with_color( + get_image(filename="delete.png"), QtCore.Qt.gray + ) + icon = QtGui.QIcon(pix_enabled) + icon.addPixmap(pix_disabled, icon.Disabled, icon.Off) remove_btn.setIcon(icon) + remove_btn.setEnabled(False) remove_btn.clicked.connect(self._on_remove_clicked) self._remove_btn = remove_btn + def setSelectionModel(self, *args, **kwargs): + super(FilesView, self).setSelectionModel(*args, **kwargs) + selection_model = self.selectionModel() + selection_model.selectionChanged.connect(self._on_selection_change) + + def has_selected_item_ids(self): + for index in self.selectionModel().selectedIndexes(): + instance_id = index.data(ITEM_ID_ROLE) + if instance_id is not None: + return True + return False + def get_selected_item_ids(self): """Ids of selected instances.""" selected_item_ids = set() @@ -286,6 +307,9 @@ class FilesView(QtWidgets.QListView): return super(FilesView, self).event(event) + def _on_selection_change(self): + self._remove_btn.setEnabled(self.has_selected_item_ids()) + def _on_remove_clicked(self): self.remove_requested.emit() From 171e73bf218da9130b58a59647840320a42eca40 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 26 Apr 2022 12:10:33 +0200 Subject: [PATCH 063/109] fixed event handling on files view --- .../widgets/attribute_defs/files_widget.py | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/openpype/widgets/attribute_defs/files_widget.py b/openpype/widgets/attribute_defs/files_widget.py index 3a9455584c..cb339f3d52 100644 --- a/openpype/widgets/attribute_defs/files_widget.py +++ b/openpype/widgets/attribute_defs/files_widget.py @@ -290,20 +290,13 @@ class FilesView(QtWidgets.QListView): return selected_item_ids def event(self, event): - if not event.type() == QtCore.QEvent.KeyPress: - pass - - elif event.key() == QtCore.Qt.Key_Space: - self.toggle_requested.emit(-1) - return True - - elif event.key() == QtCore.Qt.Key_Backspace: - self.toggle_requested.emit(0) - return True - - elif event.key() == QtCore.Qt.Key_Return: - self.toggle_requested.emit(1) - return True + if event.type() == QtCore.QEvent.KeyPress: + if ( + event.key() == QtCore.Qt.Key_Delete + and self.has_selected_item_ids() + ): + self.remove_requested.emit() + return True return super(FilesView, self).event(event) From 911163756e994edd9e332f6ef26f973c50cd24d9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 26 Apr 2022 13:35:53 +0200 Subject: [PATCH 064/109] simplified allow sequence --- openpype/hosts/traypublisher/api/plugin.py | 5 +- .../plugins/create/create_from_settings.py | 13 ---- openpype/lib/attribute_definitions.py | 66 ++++++++----------- .../project_settings/traypublisher.json | 6 +- .../schema_project_traypublisher.json | 38 ++--------- .../widgets/attribute_defs/files_widget.py | 10 +-- openpype/widgets/attribute_defs/widgets.py | 2 +- 7 files changed, 44 insertions(+), 96 deletions(-) diff --git a/openpype/hosts/traypublisher/api/plugin.py b/openpype/hosts/traypublisher/api/plugin.py index d31e0a1ef7..d4bbe4c9d6 100644 --- a/openpype/hosts/traypublisher/api/plugin.py +++ b/openpype/hosts/traypublisher/api/plugin.py @@ -45,7 +45,6 @@ class SettingsCreator(TrayPublishCreator): enable_review = False extensions = [] - sequence_extensions = [] def collect_instances(self): for instance_data in list_instances(): @@ -73,7 +72,7 @@ class SettingsCreator(TrayPublishCreator): "filepath", folders=False, extensions=self.extensions, - sequence_extensions=self.sequence_extensions, + allow_sequences=self.allow_sequences, label="Filepath", ) output.append(file_def) @@ -98,7 +97,7 @@ class SettingsCreator(TrayPublishCreator): "description": item_data["description"], "enable_review": item_data["enable_review"], "extensions": item_data["extensions"], - "sequence_extensions": item_data["sequence_extensions"], + "allow_sequences": item_data["allow_sequences"], "default_variants": item_data["default_variants"] } ) diff --git a/openpype/hosts/traypublisher/plugins/create/create_from_settings.py b/openpype/hosts/traypublisher/plugins/create/create_from_settings.py index 19ade437ab..836939fe94 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_from_settings.py +++ b/openpype/hosts/traypublisher/plugins/create/create_from_settings.py @@ -14,19 +14,6 @@ def initialize(): global_variables = globals() for item in simple_creators: - allow_sequences_value = item["allow_sequences"] - allow_sequences = allow_sequences_value["allow"] - if allow_sequences == "all": - sequence_extensions = copy.deepcopy(item["extensions"]) - - elif allow_sequences == "no": - sequence_extensions = [] - - elif allow_sequences == "selection": - sequence_extensions = allow_sequences_value["extensions"] - - item["sequence_extensions"] = sequence_extensions - item["enable_review"] = False dynamic_plugin = SettingsCreator.from_settings(item) global_variables[dynamic_plugin.__name__] = dynamic_plugin diff --git a/openpype/lib/attribute_definitions.py b/openpype/lib/attribute_definitions.py index 7a00fcdeb4..ea3da53a9e 100644 --- a/openpype/lib/attribute_definitions.py +++ b/openpype/lib/attribute_definitions.py @@ -375,6 +375,16 @@ class FileDefItem(object): filename_template, ",".join(ranges) ) + def split_sequence(self): + if not self.is_sequence: + raise ValueError("Cannot split single file item") + + output = [] + for filename in self.filenames: + path = os.path.join(self.directory, filename) + output.append(self.from_paths([path])) + return output + @property def ext(self): _, ext = os.path.splitext(self.filenames[0]) @@ -412,7 +422,7 @@ class FileDefItem(object): return cls("", "") @classmethod - def from_value(cls, value, sequence_extensions): + def from_value(cls, value, allow_sequences): multi = isinstance(value, (list, tuple, set)) if not multi: value = [value] @@ -420,10 +430,15 @@ class FileDefItem(object): output = [] str_filepaths = [] for item in value: + if isinstance(item, dict): + item = cls.from_dict(item) + if isinstance(item, FileDefItem): - output.append(item) - elif isinstance(item, dict): - output.append(cls.from_dict(item)) + if not allow_sequences and item.is_sequence: + output.extend(item.split_sequence()) + else: + output.append(item) + elif isinstance(item, six.string_types): str_filepaths.append(item) else: @@ -434,7 +449,7 @@ class FileDefItem(object): ) if str_filepaths: - output.extend(cls.from_paths(str_filepaths, sequence_extensions)) + output.extend(cls.from_paths(str_filepaths, allow_sequences)) if multi: return output @@ -450,7 +465,7 @@ class FileDefItem(object): ) @classmethod - def from_paths(cls, paths, sequence_extensions): + def from_paths(cls, paths, allow_sequences): filenames_by_dir = collections.defaultdict(list) for path in paths: normalized = os.path.normpath(path) @@ -459,18 +474,12 @@ class FileDefItem(object): output = [] for directory, filenames in filenames_by_dir.items(): - filtered_filenames = [] - for filename in filenames: - _, ext = os.path.splitext(filename) - if ext in sequence_extensions: - filtered_filenames.append(filename) - else: - output.append(cls(directory, [filename])) + if allow_sequences: + cols, remainders = clique.assemble(filenames) + else: + cols = [] + remainders = filenames - if not filtered_filenames: - continue - - cols, remainders = clique.assemble(filtered_filenames) for remainder in remainders: output.append(cls(directory, [remainder])) @@ -512,23 +521,9 @@ class FileDef(AbtractAttrDef): default(str, list): Defautl value. """ - default_sequence_extensions = [ - ".ani", ".anim", ".apng", ".art", ".bmp", ".bpg", ".bsave", - ".cal", ".cin", ".cpc", ".cpt", ".dds", ".dpx", ".ecw", ".exr", - ".fits", ".flic", ".flif", ".fpx", ".gif", ".hdri", ".hevc", - ".icer", ".icns", ".ico", ".cur", ".ics", ".ilbm", ".jbig", - ".jbig2", ".jng", ".jpeg", ".jpeg-ls", ".2000", ".jpg", ".xr", - ".jpeg-hdr", ".kra", ".mng", ".miff", ".nrrd", - ".ora", ".pam", ".pbm", ".pgm", ".ppm", ".pnm", ".pcx", ".pgf", - ".pictor", ".png", ".psb", ".psp", ".qtvr", ".ras", - ".rgbe", ".logluv", ".tiff", ".sgi", ".tga", ".tiff", - ".tiff/ep", ".tiff/it", ".ufo", ".ufp", ".wbmp", ".webp", - ".xbm", ".xcf", ".xpm", ".xwd" - ] - def __init__( self, key, single_item=True, folders=None, extensions=None, - sequence_extensions=None, default=None, **kwargs + allow_sequences=True, default=None, **kwargs ): if folders is None and extensions is None: folders = True @@ -568,13 +563,10 @@ class FileDef(AbtractAttrDef): is_label_horizontal = False kwargs["is_label_horizontal"] = is_label_horizontal - if sequence_extensions is None: - sequence_extensions = self.default_sequence_extensions - self.single_item = single_item self.folders = folders self.extensions = set(extensions) - self.sequence_extensions = set(sequence_extensions) + self.allow_sequences = allow_sequences super(FileDef, self).__init__(key, default=default, **kwargs) def __eq__(self, other): @@ -585,7 +577,7 @@ class FileDef(AbtractAttrDef): self.single_item == other.single_item and self.folders == other.folders and self.extensions == other.extensions - and self.sequence_extensions == self.sequence_extensions + and self.allow_sequences == other.allow_sequences ) def convert_value(self, value): diff --git a/openpype/settings/defaults/project_settings/traypublisher.json b/openpype/settings/defaults/project_settings/traypublisher.json index e6c6747ca2..1b0ad67abb 100644 --- a/openpype/settings/defaults/project_settings/traypublisher.json +++ b/openpype/settings/defaults/project_settings/traypublisher.json @@ -11,6 +11,7 @@ "enable_review": false, "description": "Publish workfile backup", "detailed_description": "", + "allow_sequences": true, "extensions": [ ".ma", ".mb", @@ -29,10 +30,7 @@ ".psd", ".psb", ".aep" - ], - "allow_sequences": { - "allow": "no" - } + ] } ] } \ No newline at end of file diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json index 00deb84172..59c675d411 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json @@ -68,6 +68,11 @@ { "type": "separator" }, + { + "key": "allow_sequences", + "label": "Allow sequences", + "type": "boolean" + }, { "type": "list", "key": "extensions", @@ -76,39 +81,6 @@ "collapsible_key": true, "collapsed": false, "object_type": "text" - }, - { - "key": "allow_sequences", - "label": "Allow sequences", - "type": "dict-conditional", - "use_label_wrap": true, - "collapsible_key": true, - "enum_key": "allow", - "enum_children": [ - { - "key": "all", - "label": "Yes (all extensions)" - }, - { - "key": "selection", - "label": "Yes (limited extensions)", - "children": [ - { - "type": "list", - "key": "extensions", - "label": "Extensions", - "use_label_wrap": true, - "collapsible_key": true, - "collapsed": false, - "object_type": "text" - } - ] - }, - { - "key": "no", - "label": "No" - } - ] } ] } diff --git a/openpype/widgets/attribute_defs/files_widget.py b/openpype/widgets/attribute_defs/files_widget.py index cb339f3d52..f694f2473f 100644 --- a/openpype/widgets/attribute_defs/files_widget.py +++ b/openpype/widgets/attribute_defs/files_widget.py @@ -74,11 +74,11 @@ class DropEmpty(QtWidgets.QWidget): class FilesModel(QtGui.QStandardItemModel): - def __init__(self, single_item, sequence_exts): + def __init__(self, single_item, allow_sequences): super(FilesModel, self).__init__() self._single_item = single_item - self._sequence_exts = sequence_exts + self._allow_sequences = allow_sequences self._items_by_id = {} self._file_items_by_id = {} @@ -89,7 +89,7 @@ class FilesModel(QtGui.QStandardItemModel): if not items: return - file_items = FileDefItem.from_value(items, self._sequence_exts) + file_items = FileDefItem.from_value(items, self._allow_sequences) if not file_items: return @@ -325,13 +325,13 @@ class FilesView(QtWidgets.QListView): class FilesWidget(QtWidgets.QFrame): value_changed = QtCore.Signal() - def __init__(self, single_item, sequence_exts, parent): + def __init__(self, single_item, allow_sequences, parent): super(FilesWidget, self).__init__(parent) self.setAcceptDrops(True) empty_widget = DropEmpty(self) - files_model = FilesModel(single_item, sequence_exts) + files_model = FilesModel(single_item, allow_sequences) files_proxy_model = FilesProxyModel() files_proxy_model.setSourceModel(files_model) files_view = FilesView(self) diff --git a/openpype/widgets/attribute_defs/widgets.py b/openpype/widgets/attribute_defs/widgets.py index 3f36c078cb..97e7d698b5 100644 --- a/openpype/widgets/attribute_defs/widgets.py +++ b/openpype/widgets/attribute_defs/widgets.py @@ -441,7 +441,7 @@ class UnknownAttrWidget(_BaseAttrDefWidget): class FileAttrWidget(_BaseAttrDefWidget): def _ui_init(self): input_widget = FilesWidget( - self.attr_def.single_item, self.attr_def.sequence_extensions, self + self.attr_def.single_item, self.attr_def.allow_sequences, self ) if self.attr_def.tooltip: From 342fda6315a48a40580a885969940b32588de19e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 26 Apr 2022 15:49:22 +0200 Subject: [PATCH 065/109] fixed splitting of sequence --- openpype/lib/attribute_definitions.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/lib/attribute_definitions.py b/openpype/lib/attribute_definitions.py index ea3da53a9e..0c40d0f195 100644 --- a/openpype/lib/attribute_definitions.py +++ b/openpype/lib/attribute_definitions.py @@ -379,11 +379,11 @@ class FileDefItem(object): if not self.is_sequence: raise ValueError("Cannot split single file item") - output = [] - for filename in self.filenames: - path = os.path.join(self.directory, filename) - output.append(self.from_paths([path])) - return output + paths = [ + os.path.join(self.directory, filename) + for filename in self.filenames + ] + return self.from_paths(paths, False) @property def ext(self): From 1c7c0fef32180917772d24932dc11a1952740f92 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 26 Apr 2022 15:49:46 +0200 Subject: [PATCH 066/109] added splitting menu option to files item widget --- openpype/tools/resources/images/menu.png | Bin 0 -> 8472 bytes .../widgets/attribute_defs/files_widget.py | 87 ++++++++++++++++-- 2 files changed, 78 insertions(+), 9 deletions(-) create mode 100644 openpype/tools/resources/images/menu.png diff --git a/openpype/tools/resources/images/menu.png b/openpype/tools/resources/images/menu.png new file mode 100644 index 0000000000000000000000000000000000000000..14a991f092fb9cf54363eccb16d1836704846ad8 GIT binary patch literal 8472 zcmZX41z3~s+x9a?_vn(Cq(~!OA}9!q9F2@rLb?Un6lDT}h@_)_l#CWo1c4DsC?POH ziP4B4-Sv*&`@R47JHBtnvE!-hx%0ZO`@GKcd0_&(Lr2X;4FCY0zMhU50Dy?6Ab^sb zc(4z6=T1D325ReDP!fNUl&)yvHI<*9O&|b}uU-B@yunI0h&MTdbghHTecgjXodVo| z(9lpx4CS#Tqy|Dgt`%1jD05fy9u7% zoJL>Y&J$G1m@EL@hPV5}6{G+7b5-BgX}JcKII%1dvI3w1(tfOdTAmUx0c$c$88+Hb zLeSUNlZ+pFRcB!tch+!}Ex`j!Nw)!@o91BHsE;KgV)YsdVmue=Y1Po7E=x*=<^T z3Z%&g1B}VPV?<8-KHPUiE&t9$@AMoltCaaxHoX4%;#EPltm7;P;!c)ti7UmdQQO*x zcKL%|fFHU+_85f8ur(kL0vLdJR2P+S^j-Lj9u~_E-R4CwXiJZCXOQw}2a?Ww!lhg6 z&6GZKH5w)ZAe_cZjN8d0wyUnp)vq%|7_2Qvy_ICdhe$sxX!6_%XE0L-WB$R4o6+>V zE~W;c+jIm9T$7nsDTs{MDSno{Y}Ly{Xo9#HAeQ6~AScb<;9r{xekkm{Oj|umd0k>( z4nSj|E|D}d8V1Jx>X~9}RUumn351ghW7u8cr{}C(zsXJThM!q^3cNHsCUzv2MA1Ca z!rlwTjpByy{qD4-euiG7$8c6T3}Z{zechKLjx^GmUX|@XX?q@ z(&w)&!#++@`rC^zk+k*T_B~kX^h5;ozbEyoR2ItNTE$K;7%%;9)oew64j6@Ui)v3~YAc@4$6z^90OzG?@lo^=|Iw>9{ z@h0sz!8?|)4e{wlW>F*27=!WQ+@GiGI7JkA>hz09;ty1ejF<8C?Mgsr z0ZJGeQ9;_FeBZ6o5R0q73N1j1qXtSwN$kt9=|yZm-nNq=IV7UftiPJ$g5YOS&R1aM z=ePxrZb->qW?pC`aM3%bO4KU8^NI|E{T2xUU z*hUT03s_?3_tr6R3bXzLip1;f8mI+d&>Vg1O!C)LUuGB!@5Ykvl{srZV~&%L%z7O3 zr;`0=g@O*C&gQdk`nAoW60>Td-&mjeePE35oHWehAsql>i_}zKd6O|3?@j4)?aY99 z2uq=JJW5Duk?p0cIENoJ`g%;jobtxeLNvCD`LGe2Gnl}6FZReg&U&^6g1uk zFWoU-fc^al$jpDM>e0;XA}8!%BkuQ~sR(K72s0}n7o(~lNN#@mEJssh4Z_?xUvgxK z9O~Pky^Xn=EBHNAO*xtH(!M8{jFCA^Qh4Si#~Zx7 zApYXN_l#O59_8|9%nYol-e3--yAJI4*8EyVp6%RK&RLeg1uuA=FXCo>9+%vkn^eX~ zr*Avoe_|CNkPa}4?=RTY&40wn^qDlY=^J2fbulhk+~eU>cS$^w&??7&OgQZgo}$DQ zVBZF;4tRicTqyNix1-9|$uG@t=nZqw04LN> z);dSgY|?DYT?Dk78Ys59Xk+Y3Ud_m%k{H!opW9r9!G(jC3o68WtWS5TW{K{LKyO*6 z{>8QzKMaI3CP~oU8Zlf4;R{}lca%(KjZd;C(N&b`DFYfPdIScmkrv#MSq02l&Xjd` zo&MQ6IE9hSVN??%Lul#2m2uO(Eb(^8S`-!e`o25ZX<|e56eHOANikEJy&_X<)3Vq# zf`(D9q8xPFee}Uh5L3?n$@F6pIy#aiq=oTRz0|w=-hY{*wLO z`DMwZNp82@#gQ2{i;vg7$xcBAd{klijLG3Ri+LXwCHXQ}g-lXp!%b&0f!a7BFRIK) zwai#^rMa&k;o~Ljc4P?y&I1OP9|6aDjgT$Qb!U#J@*JBPWhus z(1&l@g8-%kYsGOet3-DdDu?9b4Bg?HMImlc>DF3<>`~1}8>ElDCu8Sbza&uwj=o-} zoc2OLw*+dk2KcyGWhnnpQZcFdwwiOziErV@H&{+xufm^mIe_e^2^$QzMe^;_iM?X= z?|%$Bx!yq+Uj3IC#B#~MDh!3QMhIUWVoer{#8i1+TR&14M?{WE20 zcJRG}uTe@c$<9xaW&Vo6;qRC1h>`Hl_}+b8)#?t~KWejyH>K%Ue2fqFj-?qKsx zYNVE=`h35&vu{J5+aKoWJZaQ7UG71w>k%{W1M4e?;Veof@EyrPC4T6(Q~Un6heX(b zoeM^!V$UV&U)|Ml72Ra4t}*mJI8dvB&;-#mt_aFTo*t zW!{O+{&w=lf?dNNKn?&O#ddM<& zxJrKP7EZ)$ph~H$Qsxwsjq`zfmEPRNKmTWj^foI3Aqgn3A>JRm1wJH|6ve4=vo3$e zFMn1U27u3eb}pxdz|(*TknlUlBl{9|M9W#{=FUUXX~&m|o--tvIESeVlYH9~*tcGulLFfuBF+)D!kw$r{dQi2sC+eB-y|hBjq?gk8-- zoon9^2fq1UD2w&+DU14Fz*Jr*Mqy3@X`47kn!w~p>lX_N-xqdWVQo+3$jgM~$EgW=h~-O*O11*5F+>=Z@_fOCPhEQK-BC6b@89$`66E6cg;~c3 zFM*`FM3{_N@3>urlNQ4PHxbBj6AHgClCr&9^(^?{wfJL%1mnu$yjH-@#&ILsY&Rg6 zwvHqtZtBMuua`e7VzUyBRg83#zfSiucW~y}{?G#9JdZA}Nzc4nj}RRa^xH1dbJ5Ht z(9s<41>_Q8ng%J4i{O$1Jn9?Kz&dU!@p?%^5aZVtMjhix_mNYXAHy4Yt*kiLH3gB$ z2O~ZX?^bJZq2_=50+yTn*WzPR6`wPzozWlD0Cza4Yz&lW9ONG(A7;swSeNrBg#WPC z-b9l{MW}25eg*U&4wOJ#*m4+Hp>T9AqZX7=%xN*&m$gDy2GF1;b3dsu)dG4 zO_WO>Fr1RlkrNo7h9r@=Tb<{1`6D16;PadN_vcv;^RO+i{hJbpgV5c21h@CbJdM1E zNNfn7#%dHzHJ`!@8D9S#Ramf2m74S5F6EQ1rr%e9^1l}wH$2DuvslFkb~nTBfe`to z#3IVA!;53PnsPy%&K!Sw&VfN;L=3hi?XvVbbax1;rhD_%kJ*GS*vcmoj@Z86t*Jhb zyGmcKaK?As?I_iYG2;a2-EEzzo2@*FPO}eqLsm6|^dw06}R2F%vdM9H*F?W~s#BiR#S}KL)_=P(xavp0W!= z6;mV0T|^BOH4?Xk0_5+~V5Lto>WSf*(vQdohr|I$ID;SEM7mQzTs$z=StCW$LLxm! zi5MG!VkRe2rT5+PEqp7QOn*xMbaEXmW@XzO!vvTV*C`&H_I$7E@tZ$k9U>%Q zqINO4YpCNK2JqvqDGY^ntYSq`ESE5 zHL(XC9@xS>NXmV{5`?Hj>rKdkyI$1F-%Cs(g+LH>H;0Mc{s0P*z4W}~Y|5P8tbGN7 z(9`E_l?xg3bzGMBB1-9+-rst}b?L>=Q`1N5Km#PcobrFgfvz5M8mVS7wO_^9x2O`j z|9E2wJUGxwn=$?uF?;;iV{O59cO7j=kyi}TLMHsF@9YjHk{i9H06_cyI{XXu>QWmg zUI6C{<{@SjYmlBpgiI)!wH z6w=Un*#8&!G{M!phgyap05{lsVFg!=?}idO6I9sQorQVaUQ9bbY^R1KqxdrG zEY#F-4JC*bEFD|rBU54y;r676*PAt z0Q7+Cg_0bbvM38wHfFQ0HP}-kQP~bWM)Yr%AKaFpJY(bKD?kXDFjxH)B|Bl}w4yVt zJ%jz_3(_QO6LX*;rTW>}6YVMT&XTkA!qM4Av@3y@C zaI9l?oaa+eT%UCNIQx^87b!2ZT16s`uk3y#0UFx$zVG8$3O@XCviAhRWaH%b%_*m= z^BkoZSb)E{Vn0!w^6mA}TggPcAf?OoNZh!N`SYa6894xtVs4nyhWKI7YrZ2P6>D#8 zktqq<2~oT4{fbz40RDkA1^TDvcM$nM{K zYW8SjZ{v$OWpSpeulyezRlPCeX*HLFRY82% zxOvss8@I~mLFt!o`U?&b!TFkb`n}$K`(`Q09-V!}$MgNvdv(iee*}o%G#VS1`Ll0Q=V(*lo-oM}@Vy=PMI!r2+;$ zK3XCnHg=Wz*)N11_Zu^AlN9zl&s%-?f3YmylXF)>O9&dk>}QK_HQ}Zi--^M$A% z0z!^uf*eT;h^>4el6defmFm0ji!@t3p{@8ufwd3(;?+r5%j}`5Vj@ut-9eV#pe}pn zi(O59zOD*m(plifCDAnz8BXJOi|ngZz&vFSYsCW*Z$wyIHA7bqD}>W%+$24Cz3=YV zGEbu>Zm;Pl9I6dXqcdfwS!5M}ai72I-4`8Nu@tLf2>?)8s)nj1$MTmh|DBX1Dnqx$ zyj$f#i6;0j_2*^|AV5ne4e{3Ow^%!vE?R2i4I6~>c9>sFtdpVvyuQ-4=_N+Hu?{s; z@myUm5{C1aC1`v1G0=t8{AC4@Xf@J=iY8RsZ&D&5F%5K;rXKc3Lz+GP+`o5VBM{B4 zlPk)uf7DNJp&Vc6jpF03biGl!AN{Q2DE}sd0t;XfEFfKytbj#`XbYvc!&w3_E$G0# zi1x{PSJ*CMNV|bvMC6E@;N5 zF8f-B5R_thFlm0o2vblSu(JBghR4Gvj0~9ia>I&o|C`-=HYF|i#luOWMFRRWyY(}N zs8&f!NXXd@`xdDb*BI08Pca>oX=~K>Uc_8;P|4_67gLEAv&4gjT4_EWV2i;p+Z!-$=yo33lIRYs7zBM@JaEQysl{ zKt0>$=zb(F*LR2aS(NxUg%c-HzcMsW)-A`xVJUz5>A2=2P)lmjk-$7F5Y!5GATjK& zn{BaS;Hkt$_vTA={k~bgTFcVyQt(q&`)j!&-P>@0h;yPSOEb#f=UBd{S2Bd>4iN1? zjl&UAU5{%SVqA;oE%s{4xfR6pS)TP*KWJhIAz>qu-k0Qt|D3&@mc|lAhLh7{X{loRe~}vK8F;z2~F~@R>b~;}C5lqJAVXyRId? zp?9U^b;ImK-pvV2V@_jK^iT}MBf`3_Q68_fd;DS0b63 z7^Px#JCYD8ax}Gb>z%!tP*UPHEwBN$1gkC&IT5G`O3}J2Bex>)AAxc4E}vZR{=lF3 zZ>MZ|@rNjwkoSCFd^2*iH~$&Xyi>SPF0*?*=RQF~){9myvYaxB>YGJL`J`~*3?qo65h4GIdq{$73d=|+0qi7GNs zt@agoihHzDD0kMkv3@&pK z6DfJ|!sT;T(9#U0p~RD;e0~tnSVo%-@`edIi6D~Eks_8s!C90LPGRVbK)L+R~{t-dSay8Rwkt5n#{dGyh7J!(~8*Uc8w#u59AOtBes+b zr~g9aCycQZIqAV> zV?rp7!rbeHH~*WPO_PCig+e(KntX?{M;o&nN$IEomPPoC)*C1Qpu4*$83N6`x&J27 zw?BWqQMT%69`7NfvrvG0hLhhe)ijkDU<^4tQ?7sIDQW~D|JH&-lb3)WTHYi}K<($d z$r3WAU4{htyazpwd?1bNh)2E>Z>|Aa2V(92VevoxHGl^Pd&>01px2DU)FYw4XK44n zMB(08lpL=X1zm>o4t#%`k#;j9gG4G`RG2O!(}3t1gfUU&I`Jk`a+;03$TJX2o42k7I3cwSq>Bu>ednm9dSL~Uh7Tc-td3WQmCO5DhcDd{>Op+SAY9^2aL zD-bhjoFL%=LA3oB2I~E@H0>i5>m#DFCMceD?Q+P&2yAinj(KbfAn^$%#h=`xFKUNhVwd4w zyWZYY<8D`}@-jb$IY-V;Of|@Wj5s?o=rSbO%*&s;?OJ<4>tb9d%a+MAW&gY`up>BY zJG;r8wFm*- zv9~qi+)LD(xgg0?loJmI{yFOBc#}%6?epw%9ht?8G4leeIy%z~n7fpu6hdjkST#NA z^FHkH0P#Jdj7`g!EEA*UN4}_MWa*xB^-X-!P54+Ln|X*D;Hsw<6eK9Xfzgra&Ab6# z$F0Zsp6C)KI}s^0fCExHYbZqe)Mb+*w*Ea;LoDK0wKt2kbR`Vv20@M(r88f#{SigP z*2PhOi;uB*a6l4@U7cb6;}M|-2ofnSXM>#2S5XjsvLYAtYIMeh2|QO%yku_^c*W(R zcZK-Dz>CXo0XQHYS6#c*Rqio+Oy8#<3gp`Fho9lDVjGn!3P*_}(#DvaAG7u;_J`cY z^rgs75Q7jPuM5+u)J8n{FT|FP AivR!s literal 0 HcmV?d00001 diff --git a/openpype/widgets/attribute_defs/files_widget.py b/openpype/widgets/attribute_defs/files_widget.py index f694f2473f..5ea7fcc5eb 100644 --- a/openpype/widgets/attribute_defs/files_widget.py +++ b/openpype/widgets/attribute_defs/files_widget.py @@ -5,12 +5,12 @@ import uuid from Qt import QtWidgets, QtCore, QtGui from openpype.lib import FileDefItem -from openpype.tools.utils import paint_image_with_color -# TODO change imports -from openpype.tools.resources import ( - get_pixmap, - get_image, +from openpype.tools.utils import ( + paint_image_with_color, + ClickableLabel, ) +# TODO change imports +from openpype.tools.resources import get_image from openpype.tools.utils import ( IconButton, PixmapLabel @@ -22,7 +22,8 @@ ITEM_ICON_ROLE = QtCore.Qt.UserRole + 3 FILENAMES_ROLE = QtCore.Qt.UserRole + 4 DIRPATH_ROLE = QtCore.Qt.UserRole + 5 IS_DIR_ROLE = QtCore.Qt.UserRole + 6 -EXT_ROLE = QtCore.Qt.UserRole + 7 +IS_SEQUENCE_ROLE = QtCore.Qt.UserRole + 7 +EXT_ROLE = QtCore.Qt.UserRole + 8 class DropEmpty(QtWidgets.QWidget): @@ -148,6 +149,7 @@ class FilesModel(QtGui.QStandardItemModel): item.setData(icon_pixmap, ITEM_ICON_ROLE) item.setData(file_item.ext, EXT_ROLE) item.setData(file_item.is_dir, IS_DIR_ROLE) + item.setData(file_item.is_sequence, IS_SEQUENCE_ROLE) return item_id, item @@ -216,7 +218,9 @@ class FilesProxyModel(QtCore.QSortFilterProxyModel): class ItemWidget(QtWidgets.QWidget): - def __init__(self, item_id, label, pixmap_icon, parent=None): + split_requested = QtCore.Signal(str) + + def __init__(self, item_id, label, pixmap_icon, is_sequence, parent=None): self._item_id = item_id super(ItemWidget, self).__init__(parent) @@ -226,13 +230,67 @@ class ItemWidget(QtWidgets.QWidget): icon_widget = PixmapLabel(pixmap_icon, self) label_widget = QtWidgets.QLabel(label, self) + label_size_hint = label_widget.sizeHint() + height = label_size_hint.height() + actions_menu_pix = paint_image_with_color( + get_image(filename="menu.png"), QtCore.Qt.white + ) + + split_btn = ClickableLabel(self) + split_btn.setFixedSize(height, height) + split_btn.setPixmap(actions_menu_pix) + split_btn.setVisible(is_sequence) + layout = QtWidgets.QHBoxLayout(self) - layout.setContentsMargins(5, 5, 0, 5) + layout.setContentsMargins(5, 5, 5, 5) layout.addWidget(icon_widget, 0) layout.addWidget(label_widget, 1) + layout.addWidget(split_btn, 0) + + split_btn.clicked.connect(self._on_actions_clicked) self._icon_widget = icon_widget self._label_widget = label_widget + self._split_btn = split_btn + self._actions_menu_pix = actions_menu_pix + self._last_scaled_pix_height = None + + def _update_btn_size(self): + label_size_hint = self._label_widget.sizeHint() + height = label_size_hint.height() + if height == self._last_scaled_pix_height: + return + self._last_scaled_pix_height = height + self._split_btn.setFixedSize(height, height) + pix = self._actions_menu_pix.scaled( + height, height, + QtCore.Qt.KeepAspectRatio, + QtCore.Qt.SmoothTransformation + ) + self._split_btn.setPixmap(pix) + + def showEvent(self, event): + super(ItemWidget, self).showEvent(event) + self._update_btn_size() + + def resizeEvent(self, event): + super(ItemWidget, self).resizeEvent(event) + self._update_btn_size() + + def _on_actions_clicked(self): + menu = QtWidgets.QMenu(self._split_btn) + + action = QtWidgets.QAction("Split sequence", menu) + action.triggered.connect(self._on_split_sequence) + + menu.addAction(action) + + pos = self._split_btn.rect().bottomLeft() + point = self._split_btn.mapToGlobal(pos) + menu.popup(point) + + def _on_split_sequence(self): + self.split_requested.emit(self._item_id) class InViewButton(IconButton): @@ -404,8 +462,10 @@ class FilesWidget(QtWidgets.QFrame): continue label = index.data(ITEM_LABEL_ROLE) pixmap_icon = index.data(ITEM_ICON_ROLE) + is_sequence = index.data(IS_SEQUENCE_ROLE) - widget = ItemWidget(item_id, label, pixmap_icon) + widget = ItemWidget(item_id, label, pixmap_icon, is_sequence) + widget.split_requested.connect(self._on_split_request) self._files_view.setIndexWidget(index, widget) self._files_proxy_model.setData( index, widget.sizeHint(), QtCore.Qt.SizeHintRole @@ -437,6 +497,15 @@ class FilesWidget(QtWidgets.QFrame): if not self._in_set_value: self.value_changed.emit() + def _on_split_request(self, item_id): + file_item = self._files_model.get_file_item_by_id(item_id) + if not file_item: + return + + new_items = file_item.split_sequence() + self._remove_item_by_ids([item_id]) + self._add_filepaths(new_items) + def _on_remove_requested(self): items_to_delete = self._files_view.get_selected_item_ids() if items_to_delete: From f1434fa175b42f314bb92d13e32ef6ae8e466a55 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 26 Apr 2022 16:15:08 +0200 Subject: [PATCH 067/109] Modified publishing plugins to work with general families --- openpype/hosts/traypublisher/api/plugin.py | 1 + .../publish/collect_simple_instances.py | 48 +++++++++++++++++++ .../plugins/publish/collect_workfile.py | 31 ------------ .../plugins/publish/validate_filepaths.py | 45 +++++++++++++++++ .../plugins/publish/validate_workfile.py | 35 -------------- .../widgets/attribute_defs/files_widget.py | 11 ++++- 6 files changed, 104 insertions(+), 67 deletions(-) create mode 100644 openpype/hosts/traypublisher/plugins/publish/collect_simple_instances.py delete mode 100644 openpype/hosts/traypublisher/plugins/publish/collect_workfile.py create mode 100644 openpype/hosts/traypublisher/plugins/publish/validate_filepaths.py delete mode 100644 openpype/hosts/traypublisher/plugins/publish/validate_workfile.py diff --git a/openpype/hosts/traypublisher/api/plugin.py b/openpype/hosts/traypublisher/api/plugin.py index d4bbe4c9d6..731bf7918a 100644 --- a/openpype/hosts/traypublisher/api/plugin.py +++ b/openpype/hosts/traypublisher/api/plugin.py @@ -58,6 +58,7 @@ class SettingsCreator(TrayPublishCreator): def create(self, subset_name, data, pre_create_data): # Pass precreate data to creator attributes data["creator_attributes"] = pre_create_data + data["settings_creator"] = True # Create new instance new_instance = CreatedInstance(self.family, subset_name, data, self) # Host implementation of storing metadata about instance diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_simple_instances.py b/openpype/hosts/traypublisher/plugins/publish/collect_simple_instances.py new file mode 100644 index 0000000000..5fc66084d6 --- /dev/null +++ b/openpype/hosts/traypublisher/plugins/publish/collect_simple_instances.py @@ -0,0 +1,48 @@ +import os +import pyblish.api + + +class CollectSettingsSimpleInstances(pyblish.api.InstancePlugin): + """Collect data for instances created by settings creators.""" + + label = "Collect Settings Simple Instances" + order = pyblish.api.CollectorOrder - 0.49 + + hosts = ["traypublisher"] + + def process(self, instance): + if not instance.data.get("settings_creator"): + return + + if "families" not in instance.data: + instance.data["families"] = [] + + if "representations" not in instance.data: + instance.data["representations"] = [] + repres = instance.data["representations"] + + creator_attributes = instance.data["creator_attributes"] + + if creator_attributes.get("review"): + instance.data["families"].append("review") + + filepath_item = creator_attributes["filepath"] + self.log.info(filepath_item) + filepaths = [ + os.path.join(filepath_item["directory"], filename) + for filename in filepath_item["filenames"] + ] + + instance.data["sourceFilepaths"] = filepaths + + filenames = filepath_item["filenames"] + ext = os.path.splitext(filenames[0])[-1] + if len(filenames) == 1: + filenames = filenames[0] + + repres.append({ + "ext": ext, + "name": ext, + "stagingDir": filepath_item["directory"], + "files": filenames + }) diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_workfile.py b/openpype/hosts/traypublisher/plugins/publish/collect_workfile.py deleted file mode 100644 index d48bace047..0000000000 --- a/openpype/hosts/traypublisher/plugins/publish/collect_workfile.py +++ /dev/null @@ -1,31 +0,0 @@ -import os -import pyblish.api - - -class CollectWorkfile(pyblish.api.InstancePlugin): - """Collect representation of workfile instances.""" - - label = "Collect Workfile" - order = pyblish.api.CollectorOrder - 0.49 - families = ["workfile"] - hosts = ["traypublisher"] - - def process(self, instance): - if "representations" not in instance.data: - instance.data["representations"] = [] - repres = instance.data["representations"] - - creator_attributes = instance.data["creator_attributes"] - filepath = creator_attributes["filepath"] - instance.data["sourceFilepath"] = filepath - - staging_dir = os.path.dirname(filepath) - filename = os.path.basename(filepath) - ext = os.path.splitext(filename)[-1] - - repres.append({ - "ext": ext, - "name": ext, - "stagingDir": staging_dir, - "files": filename - }) diff --git a/openpype/hosts/traypublisher/plugins/publish/validate_filepaths.py b/openpype/hosts/traypublisher/plugins/publish/validate_filepaths.py new file mode 100644 index 0000000000..41df638ac6 --- /dev/null +++ b/openpype/hosts/traypublisher/plugins/publish/validate_filepaths.py @@ -0,0 +1,45 @@ +import os +import pyblish.api +from openpype.pipeline import PublishValidationError + + +class ValidateWorkfilePath(pyblish.api.InstancePlugin): + """Validate existence of workfile instance existence.""" + + label = "Validate Workfile" + order = pyblish.api.ValidatorOrder - 0.49 + + hosts = ["traypublisher"] + + def process(self, instance): + if "sourceFilepaths" not in instance.data: + self.log.info(( + "Can't validate source filepaths existence." + " Instance does not have collected 'sourceFilepaths'" + )) + return + + filepaths = instance.data.get("sourceFilepaths") + + not_found_files = [ + filepath + for filepath in filepaths + if not os.path.exists(filepath) + ] + if not_found_files: + joined_paths = "\n".join([ + "- {}".format(filepath) + for filepath in not_found_files + ]) + raise PublishValidationError( + ( + "Filepath of '{}' instance \"{}\" does not exist:\n{}" + ).format( + instance.data["family"], instance.data["name"], joined_paths + ), + "File not found", + ( + "## Files were not found\nFiles\n{}" + "\n\nCheck if the path is still available." + ).format(joined_paths) + ) diff --git a/openpype/hosts/traypublisher/plugins/publish/validate_workfile.py b/openpype/hosts/traypublisher/plugins/publish/validate_workfile.py deleted file mode 100644 index 7501051669..0000000000 --- a/openpype/hosts/traypublisher/plugins/publish/validate_workfile.py +++ /dev/null @@ -1,35 +0,0 @@ -import os -import pyblish.api -from openpype.pipeline import PublishValidationError - - -class ValidateWorkfilePath(pyblish.api.InstancePlugin): - """Validate existence of workfile instance existence.""" - - label = "Validate Workfile" - order = pyblish.api.ValidatorOrder - 0.49 - families = ["workfile"] - hosts = ["traypublisher"] - - def process(self, instance): - filepath = instance.data["sourceFilepath"] - if not filepath: - raise PublishValidationError( - ( - "Filepath of 'workfile' instance \"{}\" is not set" - ).format(instance.data["name"]), - "File not filled", - "## Missing file\nYou are supposed to fill the path." - ) - - if not os.path.exists(filepath): - raise PublishValidationError( - ( - "Filepath of 'workfile' instance \"{}\" does not exist: {}" - ).format(instance.data["name"], filepath), - "File not found", - ( - "## File was not found\nFile \"{}\" was not found." - " Check if the path is still available." - ).format(filepath) - ) diff --git a/openpype/widgets/attribute_defs/files_widget.py b/openpype/widgets/attribute_defs/files_widget.py index 5ea7fcc5eb..59e9029340 100644 --- a/openpype/widgets/attribute_defs/files_widget.py +++ b/openpype/widgets/attribute_defs/files_widget.py @@ -405,6 +405,7 @@ class FilesWidget(QtWidgets.QFrame): files_proxy_model.rowsRemoved.connect(self._on_rows_removed) files_view.remove_requested.connect(self._on_remove_requested) self._in_set_value = False + self._single_item = single_item self._empty_widget = empty_widget self._files_model = files_model @@ -432,6 +433,9 @@ class FilesWidget(QtWidgets.QFrame): all_same = False value = new_value + if not isinstance(value, (list, tuple, set)): + value = [value] + if value: self._add_filepaths(value) self._in_set_value = False @@ -448,7 +452,12 @@ class FilesWidget(QtWidgets.QFrame): file_item = self._files_model.get_file_item_by_id(item_id) if file_item is not None: file_items.append(file_item.to_dict()) - return file_items + + if not self._single_item: + return file_items + if file_items: + return file_items[0] + return FileDefItem.create_empty_item() def set_filters(self, folders_allowed, exts_filter): self._files_proxy_model.set_allow_folders(folders_allowed) From 52fd938b5aa9cb08f78ae2391798ea8e207d9883 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 26 Apr 2022 17:20:16 +0200 Subject: [PATCH 068/109] hound fixes --- .../traypublisher/plugins/create/create_from_settings.py | 1 - .../hosts/traypublisher/plugins/publish/validate_filepaths.py | 4 +++- openpype/lib/attribute_definitions.py | 1 - openpype/widgets/attribute_defs/widgets.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/create/create_from_settings.py b/openpype/hosts/traypublisher/plugins/create/create_from_settings.py index 836939fe94..baca274ea6 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_from_settings.py +++ b/openpype/hosts/traypublisher/plugins/create/create_from_settings.py @@ -1,5 +1,4 @@ import os -import copy from openpype.api import get_project_settings diff --git a/openpype/hosts/traypublisher/plugins/publish/validate_filepaths.py b/openpype/hosts/traypublisher/plugins/publish/validate_filepaths.py index 41df638ac6..c7302b1005 100644 --- a/openpype/hosts/traypublisher/plugins/publish/validate_filepaths.py +++ b/openpype/hosts/traypublisher/plugins/publish/validate_filepaths.py @@ -35,7 +35,9 @@ class ValidateWorkfilePath(pyblish.api.InstancePlugin): ( "Filepath of '{}' instance \"{}\" does not exist:\n{}" ).format( - instance.data["family"], instance.data["name"], joined_paths + instance.data["family"], + instance.data["name"], + joined_paths ), "File not found", ( diff --git a/openpype/lib/attribute_definitions.py b/openpype/lib/attribute_definitions.py index 0c40d0f195..ef87002a63 100644 --- a/openpype/lib/attribute_definitions.py +++ b/openpype/lib/attribute_definitions.py @@ -3,7 +3,6 @@ import re import collections import uuid import json -import copy from abc import ABCMeta, abstractmethod import six diff --git a/openpype/widgets/attribute_defs/widgets.py b/openpype/widgets/attribute_defs/widgets.py index 97e7d698b5..875b69acb4 100644 --- a/openpype/widgets/attribute_defs/widgets.py +++ b/openpype/widgets/attribute_defs/widgets.py @@ -441,7 +441,7 @@ class UnknownAttrWidget(_BaseAttrDefWidget): class FileAttrWidget(_BaseAttrDefWidget): def _ui_init(self): input_widget = FilesWidget( - self.attr_def.single_item, self.attr_def.allow_sequences, self + self.attr_def.single_item, self.attr_def.allow_sequences, self ) if self.attr_def.tooltip: From 0c643190463f82def4c3aaeaed99ea3fdf22f9e7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Apr 2022 10:49:17 +0200 Subject: [PATCH 069/109] disable files widget abilities on multivalue --- openpype/lib/attribute_definitions.py | 14 ++- .../widgets/attribute_defs/files_widget.py | 94 ++++++++++++++----- 2 files changed, 81 insertions(+), 27 deletions(-) diff --git a/openpype/lib/attribute_definitions.py b/openpype/lib/attribute_definitions.py index ef87002a63..bfac9da5ce 100644 --- a/openpype/lib/attribute_definitions.py +++ b/openpype/lib/attribute_definitions.py @@ -422,8 +422,14 @@ class FileDefItem(object): @classmethod def from_value(cls, value, allow_sequences): - multi = isinstance(value, (list, tuple, set)) - if not multi: + """Convert passed value to FileDefItem objects. + + Returns: + list: Created FileDefItem objects. + """ + + # Convert single item to iterable + if not isinstance(value, (list, tuple, set)): value = [value] output = [] @@ -450,9 +456,7 @@ class FileDefItem(object): if str_filepaths: output.extend(cls.from_paths(str_filepaths, allow_sequences)) - if multi: - return output - return output[0] + return output @classmethod def from_dict(cls, data): diff --git a/openpype/widgets/attribute_defs/files_widget.py b/openpype/widgets/attribute_defs/files_widget.py index 59e9029340..c76474d957 100644 --- a/openpype/widgets/attribute_defs/files_widget.py +++ b/openpype/widgets/attribute_defs/files_widget.py @@ -79,6 +79,7 @@ class FilesModel(QtGui.QStandardItemModel): super(FilesModel, self).__init__() self._single_item = single_item + self._multivalue = False self._allow_sequences = allow_sequences self._items_by_id = {} @@ -86,6 +87,13 @@ class FilesModel(QtGui.QStandardItemModel): self._filenames_by_dirpath = collections.defaultdict(set) self._items_by_dirpath = collections.defaultdict(list) + def set_multivalue(self, multivalue): + """Disable filtering.""" + + if self._multivalue == multivalue: + return + self._multivalue = multivalue + def add_filepaths(self, items): if not items: return @@ -94,7 +102,7 @@ class FilesModel(QtGui.QStandardItemModel): if not file_items: return - if self._single_item: + if not self._multivalue and self._single_item: file_items = [file_items[0]] current_ids = list(self._file_items_by_id.keys()) if current_ids: @@ -159,6 +167,15 @@ class FilesProxyModel(QtCore.QSortFilterProxyModel): super(FilesProxyModel, self).__init__(*args, **kwargs) self._allow_folders = False self._allowed_extensions = None + self._multivalue = False + + def set_multivalue(self, multivalue): + """Disable filtering.""" + + if self._multivalue == multivalue: + return + self._multivalue = multivalue + self.invalidateFilter() def set_allow_folders(self, allow=None): if allow is None: @@ -189,6 +206,10 @@ class FilesProxyModel(QtCore.QSortFilterProxyModel): return False def filterAcceptsRow(self, row, parent_index): + # Skip filtering if multivalue is set + if self._multivalue: + return True + model = self.sourceModel() index = model.index(row, self.filterKeyColumn(), parent_index) # First check if item is folder and if folders are enabled @@ -220,7 +241,9 @@ class FilesProxyModel(QtCore.QSortFilterProxyModel): class ItemWidget(QtWidgets.QWidget): split_requested = QtCore.Signal(str) - def __init__(self, item_id, label, pixmap_icon, is_sequence, parent=None): + def __init__( + self, item_id, label, pixmap_icon, is_sequence, multivalue, parent=None + ): self._item_id = item_id super(ItemWidget, self).__init__(parent) @@ -239,7 +262,10 @@ class ItemWidget(QtWidgets.QWidget): split_btn = ClickableLabel(self) split_btn.setFixedSize(height, height) split_btn.setPixmap(actions_menu_pix) - split_btn.setVisible(is_sequence) + if multivalue: + split_btn.setVisible(False) + else: + split_btn.setVisible(is_sequence) layout = QtWidgets.QHBoxLayout(self) layout.setContentsMargins(5, 5, 5, 5) @@ -327,11 +353,22 @@ class FilesView(QtWidgets.QListView): self._remove_btn = remove_btn def setSelectionModel(self, *args, **kwargs): + """Catch selection model set to register signal callback. + + Selection model is not available during initialization. + """ + super(FilesView, self).setSelectionModel(*args, **kwargs) selection_model = self.selectionModel() selection_model.selectionChanged.connect(self._on_selection_change) + def set_multivalue(self, multivalue): + """Disable remove button on multivalue.""" + + self._remove_btn.setVisible(not multivalue) + def has_selected_item_ids(self): + """Is any index selected.""" for index in self.selectionModel().selectedIndexes(): instance_id = index.data(ITEM_ID_ROLE) if instance_id is not None: @@ -340,6 +377,7 @@ class FilesView(QtWidgets.QListView): def get_selected_item_ids(self): """Ids of selected instances.""" + selected_item_ids = set() for index in self.selectionModel().selectedIndexes(): instance_id = index.data(ITEM_ID_ROLE) @@ -365,6 +403,8 @@ class FilesView(QtWidgets.QListView): self.remove_requested.emit() def _update_remove_btn(self): + """Position remove button to bottom right.""" + viewport = self.viewport() height = viewport.height() pos_x = viewport.width() - self._remove_btn.width() - 5 @@ -406,6 +446,7 @@ class FilesWidget(QtWidgets.QFrame): files_view.remove_requested.connect(self._on_remove_requested) self._in_set_value = False self._single_item = single_item + self._multivalue = False self._empty_widget = empty_widget self._files_model = files_model @@ -414,30 +455,24 @@ class FilesWidget(QtWidgets.QFrame): self._widgets_by_id = {} + def _set_multivalue(self, multivalue): + if self._multivalue == multivalue: + return + self._multivalue = multivalue + self._files_view.set_multivalue(multivalue) + self._files_model.set_multivalue(multivalue) + self._files_proxy_model.set_multivalue(multivalue) + def set_value(self, value, multivalue): self._in_set_value = True + widget_ids = set(self._widgets_by_id.keys()) self._remove_item_by_ids(widget_ids) - # TODO how to display multivalue? - all_same = True - if multivalue: - new_value = set() - item_row = None - for _value in value: - _value_set = set(_value) - new_value |= _value_set - if item_row is None: - item_row = _value_set - elif item_row != _value_set: - all_same = False - value = new_value + self._set_multivalue(multivalue) - if not isinstance(value, (list, tuple, set)): - value = [value] + self._add_filepaths(value) - if value: - self._add_filepaths(value) self._in_set_value = False def current_value(self): @@ -473,7 +508,13 @@ class FilesWidget(QtWidgets.QFrame): pixmap_icon = index.data(ITEM_ICON_ROLE) is_sequence = index.data(IS_SEQUENCE_ROLE) - widget = ItemWidget(item_id, label, pixmap_icon, is_sequence) + widget = ItemWidget( + item_id, + label, + pixmap_icon, + is_sequence, + self._multivalue + ) widget.split_requested.connect(self._on_split_request) self._files_view.setIndexWidget(index, widget) self._files_proxy_model.setData( @@ -507,6 +548,9 @@ class FilesWidget(QtWidgets.QFrame): self.value_changed.emit() def _on_split_request(self, item_id): + if self._multivalue: + return + file_item = self._files_model.get_file_item_by_id(item_id) if not file_item: return @@ -516,6 +560,9 @@ class FilesWidget(QtWidgets.QFrame): self._add_filepaths(new_items) def _on_remove_requested(self): + if self._multivalue: + return + items_to_delete = self._files_view.get_selected_item_ids() if items_to_delete: self._remove_item_by_ids(items_to_delete) @@ -544,6 +591,9 @@ class FilesWidget(QtWidgets.QFrame): return result def dragEnterEvent(self, event): + if self._multivalue: + return + mime_data = event.mimeData() if mime_data.hasUrls(): filepaths = [] @@ -561,7 +611,7 @@ class FilesWidget(QtWidgets.QFrame): def dropEvent(self, event): mime_data = event.mimeData() - if mime_data.hasUrls(): + if not self._multivalue and mime_data.hasUrls(): filepaths = [] for url in mime_data.urls(): filepath = url.toLocalFile() From 6f98abbee285d93c4d565fc231cba3d844691b84 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Apr 2022 10:54:33 +0200 Subject: [PATCH 070/109] reset creator on create --- openpype/tools/publisher/widgets/create_dialog.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/openpype/tools/publisher/widgets/create_dialog.py b/openpype/tools/publisher/widgets/create_dialog.py index 21e1bd5cfc..22f358f1aa 100644 --- a/openpype/tools/publisher/widgets/create_dialog.py +++ b/openpype/tools/publisher/widgets/create_dialog.py @@ -553,7 +553,7 @@ class CreateDialog(QtWidgets.QDialog): identifier = index.data(CREATOR_IDENTIFIER_ROLE) - self._set_creator(identifier) + self._set_creator_by_identifier(identifier) def _on_plugins_refresh(self): # Trigger refresh only if is visible @@ -581,7 +581,7 @@ class CreateDialog(QtWidgets.QDialog): identifier = None if new_index.isValid(): identifier = new_index.data(CREATOR_IDENTIFIER_ROLE) - self._set_creator(identifier) + self._set_creator_by_identifier(identifier) def _update_help_btn(self): pos_x = self.width() - self._help_btn.width() @@ -633,9 +633,11 @@ class CreateDialog(QtWidgets.QDialog): else: self._detail_description_widget.setMarkdown(detailed_description) - def _set_creator(self, identifier): + def _set_creator_by_identifier(self, identifier): creator = self.controller.manual_creators.get(identifier) + self._set_creator(creator) + def _set_creator(self, creator): self._creator_short_desc_widget.set_plugin(creator) self._set_creator_detailed_text(creator) self._pre_create_widget.set_plugin(creator) @@ -861,7 +863,9 @@ class CreateDialog(QtWidgets.QDialog): )) error_msg = str(exc_value) - if error_msg is not None: + if error_msg is None: + self._set_creator(self._selected_creator) + else: box = CreateErrorMessageBox( creator_label, subset_name, From 9246383621aee77701d778a2252e9ef4495f33de Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Apr 2022 10:58:14 +0200 Subject: [PATCH 071/109] Added overlay widget showing message thant creation finished --- openpype/tools/publisher/widgets/create_dialog.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/openpype/tools/publisher/widgets/create_dialog.py b/openpype/tools/publisher/widgets/create_dialog.py index 22f358f1aa..971799a35a 100644 --- a/openpype/tools/publisher/widgets/create_dialog.py +++ b/openpype/tools/publisher/widgets/create_dialog.py @@ -13,8 +13,10 @@ from openpype.pipeline.create import ( CreatorError, SUBSET_NAME_ALLOWED_SYMBOLS ) - -from openpype.tools.utils import ErrorMessageBox +from openpype.tools.utils import ( + ErrorMessageBox, + MessageOverlayObject +) from .widgets import IconValuePixmapLabel from .assets_widget import CreateDialogAssetsWidget @@ -239,6 +241,8 @@ class CreateDialog(QtWidgets.QDialog): self._name_pattern = name_pattern self._compiled_name_pattern = re.compile(name_pattern) + overlay_object = MessageOverlayObject(self) + context_widget = QtWidgets.QWidget(self) assets_widget = CreateDialogAssetsWidget(controller, context_widget) @@ -368,6 +372,8 @@ class CreateDialog(QtWidgets.QDialog): controller.add_plugins_refresh_callback(self._on_plugins_refresh) + self._overlay_object = overlay_object + self._splitter_widget = splitter_widget self._context_widget = context_widget @@ -393,6 +399,9 @@ class CreateDialog(QtWidgets.QDialog): self._prereq_timer = prereq_timer self._first_show = True + def _emit_message(self, message): + self._overlay_object.add_message(message) + def _context_change_is_enabled(self): return self._context_widget.isEnabled() @@ -865,6 +874,7 @@ class CreateDialog(QtWidgets.QDialog): if error_msg is None: self._set_creator(self._selected_creator) + self._emit_message("Creation finished...") else: box = CreateErrorMessageBox( creator_label, From 0653f77e06cb9a54f1f60b05500265ec7d8336de Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Apr 2022 11:18:55 +0200 Subject: [PATCH 072/109] fixed Py2 compatibility --- openpype/tools/publisher/widgets/validations_widget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/publisher/widgets/validations_widget.py b/openpype/tools/publisher/widgets/validations_widget.py index 798c1f9d92..e7ab4ecf5a 100644 --- a/openpype/tools/publisher/widgets/validations_widget.py +++ b/openpype/tools/publisher/widgets/validations_widget.py @@ -142,7 +142,7 @@ class ValidationErrorTitleWidget(QtWidgets.QWidget): self._help_text_by_instance_id = help_text_by_instance_id def sizeHint(self): - result = super().sizeHint() + result = super(ValidationErrorTitleWidget, self).sizeHint() expected_width = 0 for idx in range(self._view_layout.count()): expected_width += self._view_layout.itemAt(idx).sizeHint().width() From 716a120cc1228a3bc9c6798e044b1a9a962debc5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Apr 2022 11:19:11 +0200 Subject: [PATCH 073/109] hide creator dialog on showing publish frame --- openpype/tools/publisher/window.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index b74e95b227..ba0c4c54c3 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -340,9 +340,23 @@ class PublisherWindow(QtWidgets.QDialog): def _set_publish_visibility(self, visible): if visible: widget = self.publish_frame + publish_frame_visible = True else: widget = self.subset_frame + publish_frame_visible = False self.content_stacked_layout.setCurrentWidget(widget) + self._set_publish_frame_visible(publish_frame_visible) + + def _set_publish_frame_visible(self, publish_frame_visible): + """Publish frame visibility has changed. + + Also used in TrayPublisher to be able handle start/end of publish + widget overlay. + """ + + # Hide creator dialog if visible + if publish_frame_visible and self.creator_window.isVisible(): + self.creator_window.close() def _on_reset_clicked(self): self.controller.reset() From 6a303361b2163d778b0479ecd14f1d2f36bf588a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Apr 2022 11:19:42 +0200 Subject: [PATCH 074/109] hide Change Project button of publish frame show --- openpype/tools/traypublisher/window.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/tools/traypublisher/window.py b/openpype/tools/traypublisher/window.py index bbb6398c35..1c201230f0 100644 --- a/openpype/tools/traypublisher/window.py +++ b/openpype/tools/traypublisher/window.py @@ -143,6 +143,12 @@ class TrayPublishWindow(PublisherWindow): self._back_to_overlay_btn = back_to_overlay_btn self._overlay_widget = overlay_widget + def _set_publish_frame_visible(self, publish_frame_visible): + super(TrayPublishWindow, self)._set_publish_frame_visible( + publish_frame_visible + ) + self._back_to_overlay_btn.setVisible(not publish_frame_visible) + def _on_back_to_overlay(self): self._overlay_widget.setVisible(True) self._resize_overlay() From 67fee85cd9ec7a159ef825ccc7acce77155ea0f4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Apr 2022 11:31:22 +0200 Subject: [PATCH 075/109] put stack of frames in published into widget --- openpype/tools/publisher/window.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index ba0c4c54c3..90a36b4f01 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -83,8 +83,10 @@ class PublisherWindow(QtWidgets.QDialog): line_widget.setMinimumHeight(2) # Content + content_stacked_widget = QtWidgets.QWidget(self) + # Subset widget - subset_frame = QtWidgets.QFrame(self) + subset_frame = QtWidgets.QFrame(content_stacked_widget) subset_views_widget = BorderedLabelWidget( "Subsets to publish", subset_frame @@ -171,9 +173,12 @@ class PublisherWindow(QtWidgets.QDialog): subset_layout.addLayout(footer_layout, 0) # Create publish frame - publish_frame = PublishFrame(controller, self) + publish_frame = PublishFrame(controller, content_stacked_widget) - content_stacked_layout = QtWidgets.QStackedLayout() + content_stacked_layout = QtWidgets.QStackedLayout( + content_stacked_widget + ) + content_stacked_layout.setContentsMargins(0, 0, 0, 0) content_stacked_layout.setStackingMode( QtWidgets.QStackedLayout.StackAll ) @@ -186,7 +191,7 @@ class PublisherWindow(QtWidgets.QDialog): main_layout.setSpacing(0) main_layout.addWidget(header_widget, 0) main_layout.addWidget(line_widget, 0) - main_layout.addLayout(content_stacked_layout, 1) + main_layout.addWidget(content_stacked_widget, 1) creator_window = CreateDialog(controller, parent=self) @@ -228,6 +233,7 @@ class PublisherWindow(QtWidgets.QDialog): # Store header for TrayPublisher self._header_layout = header_layout + self._content_stacked_widget = content_stacked_widget self.content_stacked_layout = content_stacked_layout self.publish_frame = publish_frame self.subset_frame = subset_frame From 8913b74b19aa9967738309f46bded6eedd70770c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Apr 2022 11:33:58 +0200 Subject: [PATCH 076/109] added cancel button to change project widget --- openpype/tools/traypublisher/window.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/openpype/tools/traypublisher/window.py b/openpype/tools/traypublisher/window.py index 1c201230f0..5934c4aa8a 100644 --- a/openpype/tools/traypublisher/window.py +++ b/openpype/tools/traypublisher/window.py @@ -54,8 +54,11 @@ class StandaloneOverlayWidget(QtWidgets.QFrame): ) confirm_btn = QtWidgets.QPushButton("Confirm", content_widget) + cancel_btn = QtWidgets.QPushButton("Cancel", content_widget) + cancel_btn.setVisible(False) btns_layout = QtWidgets.QHBoxLayout() btns_layout.addStretch(1) + btns_layout.addWidget(cancel_btn, 0) btns_layout.addWidget(confirm_btn, 0) content_layout = QtWidgets.QVBoxLayout(content_widget) @@ -77,15 +80,19 @@ class StandaloneOverlayWidget(QtWidgets.QFrame): projects_view.doubleClicked.connect(self._on_double_click) confirm_btn.clicked.connect(self._on_confirm_click) + cancel_btn.clicked.connect(self._on_cancel_click) self._projects_view = projects_view self._projects_model = projects_model + self._cancel_btn = cancel_btn self._confirm_btn = confirm_btn self._publisher_window = publisher_window + self._project_name = None def showEvent(self, event): self._projects_model.refresh() + self._cancel_btn.setVisible(self._project_name is not None) super(StandaloneOverlayWidget, self).showEvent(event) def _on_double_click(self): @@ -94,13 +101,18 @@ class StandaloneOverlayWidget(QtWidgets.QFrame): def _on_confirm_click(self): self.set_selected_project() + def _on_cancel_click(self): + self._set_project(self._project_name) + def set_selected_project(self): index = self._projects_view.currentIndex() project_name = index.data(PROJECT_NAME_ROLE) - if not project_name: - return + if project_name: + self._set_project(project_name) + def _set_project(self, project_name): + self._project_name = project_name traypublisher.set_project_name(project_name) self.setVisible(False) self.project_selected.emit(project_name) From 5afeccd4e5d396c3a68ae167254095156bba3ac4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 27 Apr 2022 17:17:50 +0200 Subject: [PATCH 077/109] hiero: removing old plugins --- .../collect_clip_resolution.py | 38 --- .../collect_host_version.py | 15 -- .../collect_tag_retime.py | 32 --- .../precollect_instances.py | 223 ------------------ .../precollect_workfile.py | 74 ------ 5 files changed, 382 deletions(-) delete mode 100644 openpype/hosts/hiero/plugins/publish_old_workflow/collect_clip_resolution.py delete mode 100644 openpype/hosts/hiero/plugins/publish_old_workflow/collect_host_version.py delete mode 100644 openpype/hosts/hiero/plugins/publish_old_workflow/collect_tag_retime.py delete mode 100644 openpype/hosts/hiero/plugins/publish_old_workflow/precollect_instances.py delete mode 100644 openpype/hosts/hiero/plugins/publish_old_workflow/precollect_workfile.py diff --git a/openpype/hosts/hiero/plugins/publish_old_workflow/collect_clip_resolution.py b/openpype/hosts/hiero/plugins/publish_old_workflow/collect_clip_resolution.py deleted file mode 100644 index 1d0727d0af..0000000000 --- a/openpype/hosts/hiero/plugins/publish_old_workflow/collect_clip_resolution.py +++ /dev/null @@ -1,38 +0,0 @@ -import pyblish.api - - -class CollectClipResolution(pyblish.api.InstancePlugin): - """Collect clip geometry resolution""" - - order = pyblish.api.CollectorOrder - 0.1 - label = "Collect Clip Resolution" - hosts = ["hiero"] - families = ["clip"] - - def process(self, instance): - sequence = instance.context.data['activeSequence'] - item = instance.data["item"] - source_resolution = instance.data.get("sourceResolution", None) - - resolution_width = int(sequence.format().width()) - resolution_height = int(sequence.format().height()) - pixel_aspect = sequence.format().pixelAspect() - - # source exception - if source_resolution: - resolution_width = int(item.source().mediaSource().width()) - resolution_height = int(item.source().mediaSource().height()) - pixel_aspect = item.source().mediaSource().pixelAspect() - - resolution_data = { - "resolutionWidth": resolution_width, - "resolutionHeight": resolution_height, - "pixelAspect": pixel_aspect - } - # add to instacne data - instance.data.update(resolution_data) - - self.log.info("Resolution of instance '{}' is: {}".format( - instance, - resolution_data - )) diff --git a/openpype/hosts/hiero/plugins/publish_old_workflow/collect_host_version.py b/openpype/hosts/hiero/plugins/publish_old_workflow/collect_host_version.py deleted file mode 100644 index 76e5bd11d5..0000000000 --- a/openpype/hosts/hiero/plugins/publish_old_workflow/collect_host_version.py +++ /dev/null @@ -1,15 +0,0 @@ -import pyblish.api - - -class CollectHostVersion(pyblish.api.ContextPlugin): - """Inject the hosts version into context""" - - label = "Collect Host and HostVersion" - order = pyblish.api.CollectorOrder - 0.5 - - def process(self, context): - import nuke - import pyblish.api - - context.set_data("host", pyblish.api.current_host()) - context.set_data('hostVersion', value=nuke.NUKE_VERSION_STRING) diff --git a/openpype/hosts/hiero/plugins/publish_old_workflow/collect_tag_retime.py b/openpype/hosts/hiero/plugins/publish_old_workflow/collect_tag_retime.py deleted file mode 100644 index 0634130976..0000000000 --- a/openpype/hosts/hiero/plugins/publish_old_workflow/collect_tag_retime.py +++ /dev/null @@ -1,32 +0,0 @@ -from pyblish import api - - -class CollectTagRetime(api.InstancePlugin): - """Collect Retiming from Tags of selected track items.""" - - order = api.CollectorOrder + 0.014 - label = "Collect Retiming Tag" - hosts = ["hiero"] - families = ['clip'] - - def process(self, instance): - # gets tags - tags = instance.data["tags"] - - for t in tags: - t_metadata = dict(t["metadata"]) - t_family = t_metadata.get("tag.family", "") - - # gets only task family tags and collect labels - if "retiming" in t_family: - margin_in = t_metadata.get("tag.marginIn", "") - margin_out = t_metadata.get("tag.marginOut", "") - - instance.data["retimeMarginIn"] = int(margin_in) - instance.data["retimeMarginOut"] = int(margin_out) - instance.data["retime"] = True - - self.log.info("retimeMarginIn: `{}`".format(margin_in)) - self.log.info("retimeMarginOut: `{}`".format(margin_out)) - - instance.data["families"] += ["retime"] diff --git a/openpype/hosts/hiero/plugins/publish_old_workflow/precollect_instances.py b/openpype/hosts/hiero/plugins/publish_old_workflow/precollect_instances.py deleted file mode 100644 index f9cc158e79..0000000000 --- a/openpype/hosts/hiero/plugins/publish_old_workflow/precollect_instances.py +++ /dev/null @@ -1,223 +0,0 @@ -from compiler.ast import flatten -from pyblish import api -from openpype.hosts.hiero import api as phiero -import hiero -# from openpype.hosts.hiero.api import lib -# reload(lib) -# reload(phiero) - - -class PreCollectInstances(api.ContextPlugin): - """Collect all Track items selection.""" - - order = api.CollectorOrder - 0.509 - label = "Pre-collect Instances" - hosts = ["hiero"] - - def process(self, context): - track_items = phiero.get_track_items( - selected=True, check_tagged=True, check_enabled=True) - # only return enabled track items - if not track_items: - track_items = phiero.get_track_items( - check_enabled=True, check_tagged=True) - # get sequence and video tracks - sequence = context.data["activeSequence"] - tracks = sequence.videoTracks() - - # add collection to context - tracks_effect_items = self.collect_sub_track_items(tracks) - - context.data["tracksEffectItems"] = tracks_effect_items - - self.log.info( - "Processing enabled track items: {}".format(len(track_items))) - - for _ti in track_items: - data = {} - clip = _ti.source() - - # get clips subtracks and anotations - annotations = self.clip_annotations(clip) - subtracks = self.clip_subtrack(_ti) - self.log.debug("Annotations: {}".format(annotations)) - self.log.debug(">> Subtracks: {}".format(subtracks)) - - # get pype tag data - tag_parsed_data = phiero.get_track_item_pype_data(_ti) - # self.log.debug(pformat(tag_parsed_data)) - - if not tag_parsed_data: - continue - - if tag_parsed_data.get("id") != "pyblish.avalon.instance": - continue - # add tag data to instance data - data.update({ - k: v for k, v in tag_parsed_data.items() - if k not in ("id", "applieswhole", "label") - }) - - asset = tag_parsed_data["asset"] - subset = tag_parsed_data["subset"] - review_track = tag_parsed_data.get("reviewTrack") - hiero_track = tag_parsed_data.get("heroTrack") - audio = tag_parsed_data.get("audio") - - # remove audio attribute from data - data.pop("audio") - - # insert family into families - family = tag_parsed_data["family"] - families = [str(f) for f in tag_parsed_data["families"]] - families.insert(0, str(family)) - - track = _ti.parent() - media_source = _ti.source().mediaSource() - source_path = media_source.firstpath() - file_head = media_source.filenameHead() - file_info = media_source.fileinfos().pop() - source_first_frame = int(file_info.startFrame()) - - # apply only for review and master track instance - if review_track and hiero_track: - families += ["review", "ftrack"] - - data.update({ - "name": "{} {} {}".format(asset, subset, families), - "asset": asset, - "item": _ti, - "families": families, - - # tags - "tags": _ti.tags(), - - # track item attributes - "track": track.name(), - "trackItem": track, - "reviewTrack": review_track, - - # version data - "versionData": { - "colorspace": _ti.sourceMediaColourTransform() - }, - - # source attribute - "source": source_path, - "sourceMedia": media_source, - "sourcePath": source_path, - "sourceFileHead": file_head, - "sourceFirst": source_first_frame, - - # clip's effect - "clipEffectItems": subtracks - }) - - instance = context.create_instance(**data) - - self.log.info("Creating instance.data: {}".format(instance.data)) - - if audio: - a_data = dict() - - # add tag data to instance data - a_data.update({ - k: v for k, v in tag_parsed_data.items() - if k not in ("id", "applieswhole", "label") - }) - - # create main attributes - subset = "audioMain" - family = "audio" - families = ["clip", "ftrack"] - families.insert(0, str(family)) - - name = "{} {} {}".format(asset, subset, families) - - a_data.update({ - "name": name, - "subset": subset, - "asset": asset, - "family": family, - "families": families, - "item": _ti, - - # tags - "tags": _ti.tags(), - }) - - a_instance = context.create_instance(**a_data) - self.log.info("Creating audio instance: {}".format(a_instance)) - - @staticmethod - def clip_annotations(clip): - """ - Returns list of Clip's hiero.core.Annotation - """ - annotations = [] - subTrackItems = flatten(clip.subTrackItems()) - annotations += [item for item in subTrackItems if isinstance( - item, hiero.core.Annotation)] - return annotations - - @staticmethod - def clip_subtrack(clip): - """ - Returns list of Clip's hiero.core.SubTrackItem - """ - subtracks = [] - subTrackItems = flatten(clip.parent().subTrackItems()) - for item in subTrackItems: - # avoid all anotation - if isinstance(item, hiero.core.Annotation): - continue - # # avoid all not anaibled - if not item.isEnabled(): - continue - subtracks.append(item) - return subtracks - - @staticmethod - def collect_sub_track_items(tracks): - """ - Returns dictionary with track index as key and list of subtracks - """ - # collect all subtrack items - sub_track_items = dict() - for track in tracks: - items = track.items() - - # skip if no clips on track > need track with effect only - if items: - continue - - # skip all disabled tracks - if not track.isEnabled(): - continue - - track_index = track.trackIndex() - _sub_track_items = flatten(track.subTrackItems()) - - # continue only if any subtrack items are collected - if len(_sub_track_items) < 1: - continue - - enabled_sti = list() - # loop all found subtrack items and check if they are enabled - for _sti in _sub_track_items: - # checking if not enabled - if not _sti.isEnabled(): - continue - if isinstance(_sti, hiero.core.Annotation): - continue - # collect the subtrack item - enabled_sti.append(_sti) - - # continue only if any subtrack items are collected - if len(enabled_sti) < 1: - continue - - # add collection of subtrackitems to dict - sub_track_items[track_index] = enabled_sti - - return sub_track_items diff --git a/openpype/hosts/hiero/plugins/publish_old_workflow/precollect_workfile.py b/openpype/hosts/hiero/plugins/publish_old_workflow/precollect_workfile.py deleted file mode 100644 index 693e151f6f..0000000000 --- a/openpype/hosts/hiero/plugins/publish_old_workflow/precollect_workfile.py +++ /dev/null @@ -1,74 +0,0 @@ -import os -import pyblish.api -from openpype.hosts.hiero import api as phiero -from openpype.pipeline import legacy_io - - -class PreCollectWorkfile(pyblish.api.ContextPlugin): - """Inject the current working file into context""" - - label = "Pre-collect Workfile" - order = pyblish.api.CollectorOrder - 0.51 - - def process(self, context): - asset = legacy_io.Session["AVALON_ASSET"] - subset = "workfile" - - project = phiero.get_current_project() - active_sequence = phiero.get_current_sequence() - video_tracks = active_sequence.videoTracks() - audio_tracks = active_sequence.audioTracks() - current_file = project.path() - staging_dir = os.path.dirname(current_file) - base_name = os.path.basename(current_file) - - # get workfile's colorspace properties - _clrs = {} - _clrs["useOCIOEnvironmentOverride"] = project.useOCIOEnvironmentOverride() # noqa - _clrs["lutSetting16Bit"] = project.lutSetting16Bit() - _clrs["lutSetting8Bit"] = project.lutSetting8Bit() - _clrs["lutSettingFloat"] = project.lutSettingFloat() - _clrs["lutSettingLog"] = project.lutSettingLog() - _clrs["lutSettingViewer"] = project.lutSettingViewer() - _clrs["lutSettingWorkingSpace"] = project.lutSettingWorkingSpace() - _clrs["lutUseOCIOForExport"] = project.lutUseOCIOForExport() - _clrs["ocioConfigName"] = project.ocioConfigName() - _clrs["ocioConfigPath"] = project.ocioConfigPath() - - # set main project attributes to context - context.data["activeProject"] = project - context.data["activeSequence"] = active_sequence - context.data["videoTracks"] = video_tracks - context.data["audioTracks"] = audio_tracks - context.data["currentFile"] = current_file - context.data["colorspace"] = _clrs - - self.log.info("currentFile: {}".format(current_file)) - - # creating workfile representation - representation = { - 'name': 'hrox', - 'ext': 'hrox', - 'files': base_name, - "stagingDir": staging_dir, - } - - instance_data = { - "name": "{}_{}".format(asset, subset), - "asset": asset, - "subset": "{}{}".format(asset, subset.capitalize()), - "item": project, - "family": "workfile", - - # version data - "versionData": { - "colorspace": _clrs - }, - - # source attribute - "sourcePath": current_file, - "representations": [representation] - } - - instance = context.create_instance(**instance_data) - self.log.info("Creating instance: {}".format(instance)) From ec8e277e1d794838de3021a977090cd0c4865b47 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 27 Apr 2022 17:18:42 +0200 Subject: [PATCH 078/109] hiero: adding collector for frame tags --- openpype/hosts/hiero/api/tags.py | 20 +-- .../publish/collect_frame_tag_instances.py | 146 ++++++++++++++++++ .../plugins/publish/precollect_workfile.py | 2 + 3 files changed, 158 insertions(+), 10 deletions(-) create mode 100644 openpype/hosts/hiero/plugins/publish/collect_frame_tag_instances.py diff --git a/openpype/hosts/hiero/api/tags.py b/openpype/hosts/hiero/api/tags.py index e15e3119a6..8877b92b9d 100644 --- a/openpype/hosts/hiero/api/tags.py +++ b/openpype/hosts/hiero/api/tags.py @@ -10,16 +10,6 @@ log = Logger.get_logger(__name__) def tag_data(): return { - # "Retiming": { - # "editable": "1", - # "note": "Clip has retime or TimeWarp effects (or multiple effects stacked on the clip)", # noqa - # "icon": "retiming.png", - # "metadata": { - # "family": "retiming", - # "marginIn": 1, - # "marginOut": 1 - # } - # }, "[Lenses]": { "Set lense here": { "editable": "1", @@ -48,6 +38,16 @@ def tag_data(): "family": "comment", "subset": "main" } + }, + "FrameMain": { + "editable": "1", + "note": "Publishing a frame subset.", + "icon": "z_layer_main.png", + "metadata": { + "family": "frame", + "subset": "main", + "format": "png" + } } } diff --git a/openpype/hosts/hiero/plugins/publish/collect_frame_tag_instances.py b/openpype/hosts/hiero/plugins/publish/collect_frame_tag_instances.py new file mode 100644 index 0000000000..84b6f9149b --- /dev/null +++ b/openpype/hosts/hiero/plugins/publish/collect_frame_tag_instances.py @@ -0,0 +1,146 @@ +from pprint import pformat +import re +import ast +import json + +import pyblish.api + + +class CollectFrameTagInstances(pyblish.api.ContextPlugin): + """Collect frames from tags. + + Tag is expected to have metadata: + { + "family": "frame" + "subset": "main" + } + """ + + order = pyblish.api.CollectorOrder + label = "Collect Frames" + hosts = ["hiero"] + + def process(self, context): + self._context = context + + # collect all sequence tags + subset_data = self._create_frame_subset_data_sequence(context) + self.log.debug("__ subset_data: {}".format( + pformat(subset_data) + )) + + # if sequence tags and frame type then create instances + self._create_instances(subset_data) + + # collect all instance tags + ## if instance tags and frame type then create instances + + pass + + def _get_tag_data(self, tag): + data = {} + + # get tag metadata attribute + tag_data = tag.metadata() + + # convert tag metadata to normal keys names and values to correct types + for k, v in dict(tag_data).items(): + key = k.replace("tag.", "") + + try: + # capture exceptions which are related to strings only + if re.match(r"^[\d]+$", v): + value = int(v) + elif re.match(r"^True$", v): + value = True + elif re.match(r"^False$", v): + value = False + elif re.match(r"^None$", v): + value = None + elif re.match(r"^[\w\d_]+$", v): + value = v + else: + value = ast.literal_eval(v) + except (ValueError, SyntaxError) as msg: + value = v + + data[key] = value + + return data + + def _create_frame_subset_data_sequence(self, context): + + sequence_tags = [] + sequence = context.data["activeTimeline"] + + # get all publishable sequence frames + publish_frames = range(int(sequence.duration() + 1)) + + self.log.debug("__ publish_frames: {}".format( + pformat(publish_frames) + )) + + # get all sequence tags + for tag in sequence.tags(): + tag_data = self._get_tag_data(tag) + self.log.debug("__ tag_data: {}".format( + pformat(tag_data) + )) + if not tag_data: + continue + + if "family" not in tag_data: + continue + + if tag_data["family"] != "frame": + continue + + sequence_tags.append(tag_data) + + self.log.debug("__ sequence_tags: {}".format( + pformat(sequence_tags) + )) + + # first collect all available subset tag frames + subset_data = {} + for tag_data in sequence_tags: + frame = int(tag_data["start"]) + + if frame not in publish_frames: + continue + + subset = tag_data["subset"] + + if subset in subset_data: + # update existing subset key + subset_data[subset]["frames"].append(frame) + else: + # create new subset key + subset_data[subset] = { + "frames": [frame], + "format": tag_data["format"], + "asset": context.data["assetEntity"]["name"] + } + return subset_data + + def _create_instances(self, subset_data): + # create instance per subset + for subset_name, subset_data in subset_data.items(): + name = "frame" + subset_name.title() + data = { + "name": name, + "label": "{} {}".format(name, subset_data["frames"]), + "family": "image", + "families": ["frame"], + "asset": subset_data["asset"], + "subset": subset_name, + "format": subset_data["format"], + "frames": subset_data["frames"] + } + self._context.create_instance(**data) + + self.log.info( + "Created instance: {}".format( + json.dumps(data, sort_keys=True, indent=4) + ) + ) diff --git a/openpype/hosts/hiero/plugins/publish/precollect_workfile.py b/openpype/hosts/hiero/plugins/publish/precollect_workfile.py index 29c0397f79..b9f58c15f6 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_workfile.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_workfile.py @@ -68,6 +68,7 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin): "subset": "{}{}".format(asset, subset.capitalize()), "item": project, "family": "workfile", + "families": [], "representations": [workfile_representation, thumb_representation] } @@ -77,6 +78,7 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin): # update context with main project attributes context_data = { "activeProject": project, + "activeTimeline": active_timeline, "otioTimeline": otio_timeline, "currentFile": curent_file, "colorspace": self.get_colorspace(project), From 4687d00a1700d1940f0c06d44835aa68db756a6b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 27 Apr 2022 17:19:10 +0200 Subject: [PATCH 079/109] hiero: fixing families exception for hierarchy --- openpype/plugins/publish/collect_hierarchy.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/publish/collect_hierarchy.py b/openpype/plugins/publish/collect_hierarchy.py index 4e94acce4a..a96d444be6 100644 --- a/openpype/plugins/publish/collect_hierarchy.py +++ b/openpype/plugins/publish/collect_hierarchy.py @@ -30,14 +30,15 @@ class CollectHierarchy(pyblish.api.ContextPlugin): # shot data dict shot_data = {} - family = instance.data.get("family") + family = instance.data["family"] + families = instance.data["families"] # filter out all unepropriate instances if not instance.data["publish"]: continue # exclude other families then self.families with intersection - if not set(self.families).intersection([family]): + if not set(self.families).intersection(set(families + [family])): continue # exclude if not masterLayer True From ca0e6592db03dfeb8d0186a443683f7bbb530f92 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 27 Apr 2022 17:19:59 +0200 Subject: [PATCH 080/109] hiero: adding frame family extractor --- .../hiero/plugins/publish/extract_frames.py | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 openpype/hosts/hiero/plugins/publish/extract_frames.py diff --git a/openpype/hosts/hiero/plugins/publish/extract_frames.py b/openpype/hosts/hiero/plugins/publish/extract_frames.py new file mode 100644 index 0000000000..5396298aac --- /dev/null +++ b/openpype/hosts/hiero/plugins/publish/extract_frames.py @@ -0,0 +1,82 @@ +import os +import pyblish.api +import openpype + + +class ExtractFrames(openpype.api.Extractor): + """Extracts frames""" + + order = pyblish.api.ExtractorOrder + label = "Extract Frames" + hosts = ["hiero"] + families = ["frame"] + movie_extensions = ["mov", "mp4"] + + def process(self, instance): + oiio_tool_path = openpype.lib.get_oiio_tools_path() + staging_dir = self.staging_dir(instance) + output_template = os.path.join(staging_dir, instance.data["name"]) + sequence = instance.context.data["activeTimeline"] + + files = [] + for frame in instance.data["frames"]: + track_item = sequence.trackItemAt(frame) + media_source = track_item.source().mediaSource() + input_path = media_source.fileinfos()[0].filename() + input_frame = ( + track_item.mapTimelineToSource(frame) + + track_item.source().mediaSource().startTime() + ) + output_ext = instance.data["format"] + output_path = output_template + output_path += ".{:04d}.{}".format(int(frame), output_ext) + + args = [oiio_tool_path] + + ext = os.path.splitext(input_path)[1][1:] + if ext in self.movie_extensions: + args.extend(["--subimage", str(int(input_frame))]) + else: + args.extend(["--frames", str(int(input_frame))]) + + if ext == "exr": + args.extend(["--powc", "0.45,0.45,0.45,1.0"]) + + args.extend([input_path, "-o", output_path]) + output = openpype.api.run_subprocess(args) + + failed_output = "oiiotool produced no output." + if failed_output in output: + raise ValueError( + "oiiotool processing failed. Args: {}".format(args) + ) + + files.append(output_path) + + # Feedback to user because "oiiotool" can make the publishing + # appear unresponsive. + self.log.info( + "Processed {} of {} frames".format( + instance.data["frames"].index(frame) + 1, + len(instance.data["frames"]) + ) + ) + + if len(files) == 1: + instance.data["representations"] = [ + { + "name": output_ext, + "ext": output_ext, + "files": os.path.basename(files[0]), + "stagingDir": staging_dir + } + ] + else: + instance.data["representations"] = [ + { + "name": output_ext, + "ext": output_ext, + "files": [os.path.basename(x) for x in files], + "stagingDir": staging_dir + } + ] \ No newline at end of file From df81486d30c4ddbbcec92cf307e30464d084e21f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 27 Apr 2022 17:27:29 +0200 Subject: [PATCH 081/109] hound --- .../plugins/publish/collect_frame_tag_instances.py | 12 ++++-------- .../hosts/hiero/plugins/publish/extract_frames.py | 2 +- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/hiero/plugins/publish/collect_frame_tag_instances.py b/openpype/hosts/hiero/plugins/publish/collect_frame_tag_instances.py index 84b6f9149b..80a54ba2c5 100644 --- a/openpype/hosts/hiero/plugins/publish/collect_frame_tag_instances.py +++ b/openpype/hosts/hiero/plugins/publish/collect_frame_tag_instances.py @@ -25,18 +25,14 @@ class CollectFrameTagInstances(pyblish.api.ContextPlugin): # collect all sequence tags subset_data = self._create_frame_subset_data_sequence(context) + self.log.debug("__ subset_data: {}".format( pformat(subset_data) )) - # if sequence tags and frame type then create instances + # create instances self._create_instances(subset_data) - # collect all instance tags - ## if instance tags and frame type then create instances - - pass - def _get_tag_data(self, tag): data = {} @@ -61,7 +57,7 @@ class CollectFrameTagInstances(pyblish.api.ContextPlugin): value = v else: value = ast.literal_eval(v) - except (ValueError, SyntaxError) as msg: + except (ValueError, SyntaxError): value = v data[key] = value @@ -85,7 +81,7 @@ class CollectFrameTagInstances(pyblish.api.ContextPlugin): tag_data = self._get_tag_data(tag) self.log.debug("__ tag_data: {}".format( pformat(tag_data) - )) + )) if not tag_data: continue diff --git a/openpype/hosts/hiero/plugins/publish/extract_frames.py b/openpype/hosts/hiero/plugins/publish/extract_frames.py index 5396298aac..aa3eda2e9f 100644 --- a/openpype/hosts/hiero/plugins/publish/extract_frames.py +++ b/openpype/hosts/hiero/plugins/publish/extract_frames.py @@ -79,4 +79,4 @@ class ExtractFrames(openpype.api.Extractor): "files": [os.path.basename(x) for x in files], "stagingDir": staging_dir } - ] \ No newline at end of file + ] From 546b1d42015598a9b36dcfb07d942c12b391029b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Apr 2022 19:07:23 +0200 Subject: [PATCH 082/109] removed unused method --- .../plugins/publish/integrate_ftrack_api.py | 42 ------------------- 1 file changed, 42 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py index 650c59fae8..e60d00c7c3 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py @@ -24,48 +24,6 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin): label = "Integrate Ftrack Api" families = ["ftrack"] - def query(self, entitytype, data): - """ Generate a query expression from data supplied. - - If a value is not a string, we'll add the id of the entity to the - query. - - Args: - entitytype (str): The type of entity to query. - data (dict): The data to identify the entity. - exclusions (list): All keys to exclude from the query. - - Returns: - str: String query to use with "session.query" - """ - queries = [] - if sys.version_info[0] < 3: - for key, value in data.iteritems(): - if not isinstance(value, (basestring, int)): - self.log.info("value: {}".format(value)) - if "id" in value.keys(): - queries.append( - "{0}.id is \"{1}\"".format(key, value["id"]) - ) - else: - queries.append("{0} is \"{1}\"".format(key, value)) - else: - for key, value in data.items(): - if not isinstance(value, (str, int)): - self.log.info("value: {}".format(value)) - if "id" in value.keys(): - queries.append( - "{0}.id is \"{1}\"".format(key, value["id"]) - ) - else: - queries.append("{0} is \"{1}\"".format(key, value)) - - query = ( - "select id from " + entitytype + " where " + " and ".join(queries) - ) - self.log.debug(query) - return query - def process(self, instance): session = instance.context.data["ftrackSession"] context = instance.context From 9c1fb9de477394b8b22c1910ab2a862a3005dd96 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Apr 2022 19:08:22 +0200 Subject: [PATCH 083/109] added asset status name filtering for asset version --- .../publish/integrate_ftrack_instances.py | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py index 5ea0469bce..5eecf34c3d 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py @@ -3,6 +3,8 @@ import json import copy import pyblish.api +from openpype.lib.profiles_filtering import filter_profiles + class IntegrateFtrackInstance(pyblish.api.InstancePlugin): """Collect ftrack component data (not integrate yet). @@ -36,6 +38,7 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): "reference": "reference" } keep_first_subset_name_for_review = True + asset_versions_status_profiles = {} def process(self, instance): self.log.debug("instance {}".format(instance)) @@ -80,6 +83,8 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): if instance_fps is None: instance_fps = instance.context.data["fps"] + status_name = self._get_asset_version_status_name(instance) + # Base of component item data # - create a copy of this object when want to use it base_component_item = { @@ -91,7 +96,8 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): }, "assetversion_data": { "version": version_number, - "comment": instance.context.data.get("comment") or "" + "comment": instance.context.data.get("comment") or "", + "status_name": status_name }, "component_overwrite": False, # This can be change optionally @@ -317,3 +323,24 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): ) )) instance.data["ftrackComponentsList"] = component_list + + def _get_asset_version_status_name(self, instance): + if not self.asset_versions_status_profiles: + return None + + # Prepare filtering data for new asset version status + anatomy_data = instance.data["anatomyData"] + task_type = anatomy_data.get("task", {}).get("type") + filtering_criteria = { + "families": instance.data["family"], + "hosts": instance.context.data["hostName"], + "task_types": task_type + } + matching_profile = filter_profiles( + self.asset_versions_status_profiles, + filtering_criteria + ) + if not matching_profile: + return None + + return matching_profile["status"] or None From b0dd4d51530a5a345bad9fae2c3d9fac2ee6ef19 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Apr 2022 19:10:53 +0200 Subject: [PATCH 084/109] implemented logic which change status of asset version --- .../plugins/publish/integrate_ftrack_api.py | 44 ++++++++++++++++--- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py index e60d00c7c3..64af8cb208 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py @@ -66,7 +66,19 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin): default_asset_name = parent_entity["name"] # Change status on task - self._set_task_status(instance, task_entity, session) + asset_version_status_ids_by_name = {} + project_entity = instance.context.data.get("ftrackProject") + if project_entity: + project_schema = project_entity["project_schema"] + asset_version_statuses = ( + project_schema.get_statuses("AssetVersion") + ) + asset_version_status_ids_by_name = { + status["name"].lower(): status["id"] + for status in asset_version_statuses + } + + self._set_task_status(instance, project_entity, task_entity, session) # Prepare AssetTypes asset_types_by_short = self._ensure_asset_types_exists( @@ -97,7 +109,11 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin): # Asset Version asset_version_data = data.get("assetversion_data") or {} asset_version_entity = self._ensure_asset_version_exists( - session, asset_version_data, asset_entity["id"], task_entity + session, + asset_version_data, + asset_entity["id"], + task_entity, + asset_version_status_ids_by_name ) # Component @@ -132,8 +148,7 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin): if asset_version not in instance.data[asset_versions_key]: instance.data[asset_versions_key].append(asset_version) - def _set_task_status(self, instance, task_entity, session): - project_entity = instance.context.data.get("ftrackProject") + def _set_task_status(self, instance, project_entity, task_entity, session): if not project_entity: self.log.info("Task status won't be set, project is not known.") return @@ -277,12 +292,19 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin): ).first() def _ensure_asset_version_exists( - self, session, asset_version_data, asset_id, task_entity + self, + session, + asset_version_data, + asset_id, + task_entity, + status_ids_by_name ): task_id = None if task_entity: task_id = task_entity["id"] + status_name = asset_version_data.pop("status_name", None) + # Try query asset version by criteria (asset id and version) version = asset_version_data.get("version") or 0 asset_version_entity = self._query_asset_version( @@ -324,6 +346,18 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin): session, version, asset_id ) + if status_name: + status_id = status_ids_by_name.get(status_name.lower()) + if not status_id: + self.log.info(( + "Ftrack status with name \"{}\"" + " for AssetVersion was not found." + ).format(status_name)) + + elif asset_version_entity["status_id"] != status_id: + asset_version_entity["status_id"] = status_id + session.commit() + # Set custom attributes if there were any set custom_attrs = asset_version_data.get("custom_attributes") or {} for attr_key, attr_value in custom_attrs.items(): From eea8f906a2a2b4031b672d021db1052ba0afdd0d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Apr 2022 19:16:10 +0200 Subject: [PATCH 085/109] added settings schemas and defaults for new attribute --- .../defaults/project_settings/ftrack.json | 3 +- .../schema_project_ftrack.json | 37 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index a846a596c2..f9d16d6476 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -418,7 +418,8 @@ "redshiftproxy": "cache", "usd": "usd" }, - "keep_first_subset_name_for_review": true + "keep_first_subset_name_for_review": true, + "asset_versions_status_profiles": [] } } } \ No newline at end of file diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json index 47effb3dbd..7db490b114 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json @@ -858,6 +858,43 @@ "key": "keep_first_subset_name_for_review", "label": "Make subset name as first asset name", "default": true + }, + { + "type": "list", + "collapsible": true, + "key": "asset_versions_status_profiles", + "label": "AssetVersion status on publish", + "use_label_wrap": true, + "object_type": { + "type": "dict", + "children": [ + { + "key": "hosts", + "label": "Host names", + "type": "hosts-enum", + "multiselection": true + }, + { + "key": "task_types", + "label": "Task types", + "type": "task-types-enum" + }, + { + "key": "family", + "label": "Family", + "type": "list", + "object_type": "text" + }, + { + "type": "separator" + }, + { + "key": "status", + "label": "Status name", + "type": "text" + } + ] + } } ] } From c2e0c84034b94a876eca830e6c4ae5188b4c000a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 28 Apr 2022 10:36:52 +0200 Subject: [PATCH 086/109] replaced renderlayer with render_layer and renderpass with render_pass --- .../plugins/create/create_render_layer.py | 13 ++++++++++--- .../plugins/create/create_render_pass.py | 13 ++++++++++--- .../plugins/publish/collect_instances.py | 18 +++++++++++++++++- .../plugins/publish/collect_scene_render.py | 10 ++++++++-- 4 files changed, 45 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render_layer.py b/openpype/hosts/tvpaint/plugins/create/create_render_layer.py index c1af9632b1..3b5bd47189 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render_layer.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render_layer.py @@ -24,7 +24,9 @@ class CreateRenderlayer(plugin.Creator): " {clip_id} {group_id} {r} {g} {b} \"{name}\"" ) - dynamic_subset_keys = ["render_pass", "render_layer", "group"] + dynamic_subset_keys = [ + "renderpass", "renderlayer", "render_pass", "render_layer", "group" + ] @classmethod def get_dynamic_data( @@ -34,12 +36,17 @@ class CreateRenderlayer(plugin.Creator): variant, task_name, asset_id, project_name, host_name ) # Use render pass name from creator's plugin - dynamic_data["render_pass"] = cls.render_pass + dynamic_data["renderpass"] = cls.render_pass # Add variant to render layer - dynamic_data["render_layer"] = variant + dynamic_data["renderlayer"] = variant # Change family for subset name fill dynamic_data["family"] = "render" + # TODO remove - Backwards compatibility for old subset name templates + # - added 2022/04/28 + dynamic_data["render_pass"] = dynamic_data["renderpass"] + dynamic_data["render_layer"] = dynamic_data["renderlayer"] + return dynamic_data @classmethod diff --git a/openpype/hosts/tvpaint/plugins/create/create_render_pass.py b/openpype/hosts/tvpaint/plugins/create/create_render_pass.py index a7f717ccec..1c9f31e656 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render_pass.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render_pass.py @@ -20,7 +20,9 @@ class CreateRenderPass(plugin.Creator): icon = "cube" defaults = ["Main"] - dynamic_subset_keys = ["render_pass", "render_layer"] + dynamic_subset_keys = [ + "renderpass", "renderlayer", "render_pass", "render_layer" + ] @classmethod def get_dynamic_data( @@ -29,9 +31,13 @@ class CreateRenderPass(plugin.Creator): dynamic_data = super(CreateRenderPass, cls).get_dynamic_data( variant, task_name, asset_id, project_name, host_name ) - dynamic_data["render_pass"] = variant + dynamic_data["renderpass"] = variant dynamic_data["family"] = "render" + # TODO remove - Backwards compatibility for old subset name templates + # - added 2022/04/28 + dynamic_data["renderpass"] = dynamic_data["render_pass"] + return dynamic_data @classmethod @@ -115,6 +121,7 @@ class CreateRenderPass(plugin.Creator): else: render_layer = beauty_instance["variant"] + subset_name_fill_data["renderlayer"] = render_layer subset_name_fill_data["render_layer"] = render_layer # Format dynamic keys in subset name @@ -129,7 +136,7 @@ class CreateRenderPass(plugin.Creator): self.data["group_id"] = group_id self.data["pass"] = variant - self.data["render_layer"] = render_layer + self.data["renderlayer"] = render_layer # Collect selected layer ids to be stored into instance layer_names = [layer["name"] for layer in selected_layers] diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_instances.py b/openpype/hosts/tvpaint/plugins/publish/collect_instances.py index 188aa8c41a..9b51f88cae 100644 --- a/openpype/hosts/tvpaint/plugins/publish/collect_instances.py +++ b/openpype/hosts/tvpaint/plugins/publish/collect_instances.py @@ -45,6 +45,22 @@ class CollectInstances(pyblish.api.ContextPlugin): for instance_data in filtered_instance_data: instance_data["fps"] = context.data["sceneFps"] + # Conversion from older instances + # - change 'render_layer' to 'renderlayer' + # and 'render_pass' to 'renderpass' + + if ( + "renderlayer" not in instance_data + and "render_layer" in instance_data + ): + instance_data["renderlayer"] = instance_data["render_layer"] + + if ( + "renderpass" not in instance_data + and "render_pass" in instance_data + ): + instance_data["renderpass"] = instance_data["render_pass"] + # Store workfile instance data to instance data instance_data["originData"] = copy.deepcopy(instance_data) # Global instance data modifications @@ -191,7 +207,7 @@ class CollectInstances(pyblish.api.ContextPlugin): "Creating render pass instance. \"{}\"".format(pass_name) ) # Change label - render_layer = instance_data["render_layer"] + render_layer = instance_data["renderlayer"] # Backwards compatibility # - subset names were not stored as final subset names during creation diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_scene_render.py b/openpype/hosts/tvpaint/plugins/publish/collect_scene_render.py index 1c042a62fb..02e0575a2c 100644 --- a/openpype/hosts/tvpaint/plugins/publish/collect_scene_render.py +++ b/openpype/hosts/tvpaint/plugins/publish/collect_scene_render.py @@ -69,9 +69,13 @@ class CollectRenderScene(pyblish.api.ContextPlugin): # Variant is using render pass name variant = self.render_layer dynamic_data = { - "render_layer": self.render_layer, - "render_pass": self.render_pass + "renderlayer": self.render_layer, + "renderpass": self.render_pass, } + # TODO remove - Backwards compatibility for old subset name templates + # - added 2022/04/28 + dynamic_data["render_layer"] = dynamic_data["renderlayer"] + dynamic_data["render_pass"] = dynamic_data["renderpass"] task_name = workfile_context["task"] subset_name = get_subset_name_with_asset_doc( @@ -102,6 +106,8 @@ class CollectRenderScene(pyblish.api.ContextPlugin): "asset": asset_name, "task": task_name } + # Add 'renderlayer' and 'renderpass' to data + instance_data.update(dynamic_data) instance = context.create_instance(**instance_data) From e7d244cd585dcd4171cc50f8cb0ac537fda360ff Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 28 Apr 2022 10:48:43 +0200 Subject: [PATCH 087/109] changed default settings of TVPaint --- openpype/settings/defaults/project_settings/global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 7317a3da1c..7b223798f1 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -307,7 +307,7 @@ ], "task_types": [], "tasks": [], - "template": "{family}{Task}_{Render_layer}_{Render_pass}" + "template": "{family}{Task}_{Renderlayer}_{Renderpass}" }, { "families": [ From ed9771392db1475a6cb74e0a4417079e4505b248 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 28 Apr 2022 11:22:11 +0200 Subject: [PATCH 088/109] Nuke: anatomy node overrides plus UX improvements --- .../schemas/schema_anatomy_imageio.json | 96 +++++++++++-------- 1 file changed, 57 insertions(+), 39 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_imageio.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_imageio.json index 9f142bad09..434f474f6e 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_imageio.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_imageio.json @@ -253,7 +253,7 @@ { "key": "requiredNodes", "type": "list", - "label": "Required Nodes", + "label": "Plugin required", "object_type": { "type": "dict", "children": [ @@ -272,35 +272,43 @@ "label": "Nuke Node Class" }, { - "type": "splitter" - }, - { - "key": "knobs", + "type": "collapsible-wrap", "label": "Knobs", - "type": "list", - "object_type": { - "type": "dict", - "children": [ - { - "type": "text", - "key": "name", - "label": "Name" - }, - { - "type": "text", - "key": "value", - "label": "Value" + "collapsible": true, + "collapsed": true, + "children": [ + { + "key": "knobs", + "type": "list", + "object_type": { + "type": "dict", + "children": [ + { + "type": "text", + "key": "name", + "label": "Name" + }, + { + "type": "text", + "key": "value", + "label": "Value" + } + ] } - ] - } + } + ] } + ] } }, + { + "type": "splitter" + }, { "type": "list", - "key": "customNodes", - "label": "Custom Nodes", + "key": "overrideNodes", + "label": "Plugin's node overrides", "object_type": { "type": "dict", "children": [ @@ -319,27 +327,37 @@ "label": "Nuke Node Class" }, { - "type": "splitter" + "key": "sebsets", + "label": "Subsets", + "type": "list", + "object_type": "text" }, { - "key": "knobs", + "type": "collapsible-wrap", "label": "Knobs", - "type": "list", - "object_type": { - "type": "dict", - "children": [ - { - "type": "text", - "key": "name", - "label": "Name" - }, - { - "type": "text", - "key": "value", - "label": "Value" + "collapsible": true, + "collapsed": true, + "children": [ + { + "key": "knobs", + "type": "list", + "object_type": { + "type": "dict", + "children": [ + { + "type": "text", + "key": "name", + "label": "Name" + }, + { + "type": "text", + "key": "value", + "label": "Value" + } + ] } - ] - } + } + ] } ] } From e0fba3583a75f832135f77c36bb6bb8690b8898b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 28 Apr 2022 12:13:37 +0200 Subject: [PATCH 089/109] swapr keys --- openpype/hosts/tvpaint/plugins/create/create_render_pass.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render_pass.py b/openpype/hosts/tvpaint/plugins/create/create_render_pass.py index 1c9f31e656..26fa8ac51a 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render_pass.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render_pass.py @@ -36,7 +36,7 @@ class CreateRenderPass(plugin.Creator): # TODO remove - Backwards compatibility for old subset name templates # - added 2022/04/28 - dynamic_data["renderpass"] = dynamic_data["render_pass"] + dynamic_data["render_pass"] = dynamic_data["renderpass"] return dynamic_data From e4b5ee107b77c88833428161188d65474f3d7e84 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 28 Apr 2022 12:17:59 +0200 Subject: [PATCH 090/109] nuke: ux improvement --- .../schemas/projects_schema/schemas/schema_anatomy_imageio.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_imageio.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_imageio.json index 434f474f6e..141b51da0a 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_imageio.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_imageio.json @@ -334,7 +334,7 @@ }, { "type": "collapsible-wrap", - "label": "Knobs", + "label": "Knobs overrides", "collapsible": true, "collapsed": true, "children": [ From ca803331e595bb6bb8d2ff740d33fdfe25793aeb Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 28 Apr 2022 14:00:02 +0200 Subject: [PATCH 091/109] make sure keys are as list instead of 'dict_keys' in py2 --- openpype/lib/avalon_context.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index 3fcddef745..9d8a92cfe9 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -1532,13 +1532,13 @@ class BuildWorkfile: subsets = list(legacy_io.find({ "type": "subset", - "parent": {"$in": asset_entity_by_ids.keys()} + "parent": {"$in": list(asset_entity_by_ids.keys())} })) subset_entity_by_ids = {subset["_id"]: subset for subset in subsets} sorted_versions = list(legacy_io.find({ "type": "version", - "parent": {"$in": subset_entity_by_ids.keys()} + "parent": {"$in": list(subset_entity_by_ids.keys())} }).sort("name", -1)) subset_id_with_latest_version = [] @@ -1552,7 +1552,7 @@ class BuildWorkfile: repres = legacy_io.find({ "type": "representation", - "parent": {"$in": last_versions_by_id.keys()} + "parent": {"$in": list(last_versions_by_id.keys())} }) output = {} From ba537e263d756034d70b3d5f08ead851684c9f8e Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 28 Apr 2022 15:09:12 +0100 Subject: [PATCH 092/109] Fix old avalon-core import --- openpype/hosts/unreal/plugins/create/create_render.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/unreal/plugins/create/create_render.py b/openpype/hosts/unreal/plugins/create/create_render.py index d81f7c7aab..1e6f5fb4d1 100644 --- a/openpype/hosts/unreal/plugins/create/create_render.py +++ b/openpype/hosts/unreal/plugins/create/create_render.py @@ -1,6 +1,6 @@ import unreal -from avalon import io +from openpype.pipeline import legacy_io from openpype.hosts.unreal.api import pipeline from openpype.hosts.unreal.api.plugin import Creator @@ -70,14 +70,14 @@ class CreateRender(Creator): # Get frame range. We need to go through the hierarchy and check # the frame range for the children. - asset_data = io.find_one({ + asset_data = legacy_io.find_one({ "type": "asset", "name": asset_name }) id = asset_data.get('_id') elements = list( - io.find({"type": "asset", "data.visualParent": id})) + legacy_io.find({"type": "asset", "data.visualParent": id})) if elements: start_frames = [] @@ -86,7 +86,7 @@ class CreateRender(Creator): start_frames.append(e.get('data').get('clipIn')) end_frames.append(e.get('data').get('clipOut')) - elements.extend(io.find({ + elements.extend(legacy_io.find({ "type": "asset", "data.visualParent": e.get('_id') })) From ab0980895fdac8ec1a83b777b5f41045202647c9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 28 Apr 2022 16:43:37 +0200 Subject: [PATCH 093/109] nuke: adding key to default settings --- openpype/settings/defaults/project_anatomy/imageio.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_anatomy/imageio.json b/openpype/settings/defaults/project_anatomy/imageio.json index 7a3f49452e..fedae994bf 100644 --- a/openpype/settings/defaults/project_anatomy/imageio.json +++ b/openpype/settings/defaults/project_anatomy/imageio.json @@ -165,7 +165,7 @@ ] } ], - "customNodes": [] + "overrideNodes": [] }, "regexInputs": { "inputs": [ From 45a5538a5c3eec03316e9fc24c0f4feb21be3742 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 28 Apr 2022 16:50:05 +0200 Subject: [PATCH 094/109] removed backwards compatibility --- start.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/start.py b/start.py index 38eb9e9bf4..4d4801c1e5 100644 --- a/start.py +++ b/start.py @@ -266,18 +266,9 @@ def set_openpype_global_environments() -> None: """Set global OpenPype's environments.""" import acre - try: - from openpype.settings import get_general_environments + from openpype.settings import get_general_environments - general_env = get_general_environments() - - except Exception: - # Backwards compatibility for OpenPype versions where - # `get_general_environments` does not exists yet - from openpype.settings import get_environments - - all_env = get_environments() - general_env = all_env["global"] + general_env = get_general_environments() merged_env = acre.merge( acre.parse(general_env), From 77bac5c735ec2aee0f101f0267dc72b8635ad4b2 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 28 Apr 2022 20:20:25 +0200 Subject: [PATCH 095/109] nuke: fix `read` and rename to `read_avalon_data` --- openpype/hosts/nuke/api/lib.py | 22 ++++++++++------------ openpype/hosts/nuke/api/pipeline.py | 4 ++-- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 4e38f811c9..bd39a1f0a8 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -400,7 +400,7 @@ def add_write_node(name, **kwarg): return w -def read(node): +def read_avalon_data(node): """Return user-defined knobs from given `node` Args: @@ -415,8 +415,6 @@ def read(node): return knob_name[len("avalon:"):] elif knob_name.startswith("ak:"): return knob_name[len("ak:"):] - else: - return knob_name data = dict() @@ -445,7 +443,8 @@ def read(node): (knob_type == 26 and value) ): key = compat_prefixed(knob_name) - data[key] = value + if key is not None: + data[key] = value if knob_name == first_user_knob: break @@ -567,7 +566,7 @@ def check_inventory_versions(): if container: node = nuke.toNode(container["objectName"]) - avalon_knob_data = read(node) + avalon_knob_data = read_avalon_data(node) # get representation from io representation = legacy_io.find_one({ @@ -623,7 +622,7 @@ def writes_version_sync(): if _NODE_TAB_NAME not in each.knobs(): continue - avalon_knob_data = read(each) + avalon_knob_data = read_avalon_data(each) try: if avalon_knob_data['families'] not in ["render"]: @@ -665,14 +664,14 @@ def check_subsetname_exists(nodes, subset_name): bool: True of False """ return next((True for n in nodes - if subset_name in read(n).get("subset", "")), + if subset_name in read_avalon_data(n).get("subset", "")), False) def get_render_path(node): ''' Generate Render path from presets regarding avalon knob data ''' - data = {'avalon': read(node)} + data = {'avalon': read_avalon_data(node)} data_preset = { "nodeclass": data['avalon']['family'], "families": [data['avalon']['families']], @@ -1293,7 +1292,7 @@ class WorkfileSettings(object): for node in nuke.allNodes(filter="Group"): # get data from avalon knob - avalon_knob_data = read(node) + avalon_knob_data = read_avalon_data(node) if not avalon_knob_data: continue @@ -1342,7 +1341,6 @@ class WorkfileSettings(object): write_node[knob["name"]].setValue(value) - def set_reads_colorspace(self, read_clrs_inputs): """ Setting colorspace to Read nodes @@ -1630,8 +1628,8 @@ def get_write_node_template_attr(node): ''' # get avalon data from node - data = dict() - data['avalon'] = read(node) + data = {"avalon": read_avalon_data(node)} + data_preset = { "nodeclass": data['avalon']['family'], "families": [data['avalon']['families']], diff --git a/openpype/hosts/nuke/api/pipeline.py b/openpype/hosts/nuke/api/pipeline.py index 0194acd196..2785eb65cd 100644 --- a/openpype/hosts/nuke/api/pipeline.py +++ b/openpype/hosts/nuke/api/pipeline.py @@ -32,7 +32,7 @@ from .lib import ( launch_workfiles_app, check_inventory_versions, set_avalon_knob_data, - read, + read_avalon_data, Context ) @@ -359,7 +359,7 @@ def parse_container(node): dict: The container schema data for this container node. """ - data = read(node) + data = read_avalon_data(node) # (TODO) Remove key validation when `ls` has re-implemented. # From 2d3a4541c99c083565d56bcf6533d3c24099dce8 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Thu, 28 Apr 2022 20:29:28 +0200 Subject: [PATCH 096/109] remove submodules after accidentally merging them back with PR --- openpype/modules/default_modules/ftrack/python2_vendor/arrow | 1 - .../default_modules/ftrack/python2_vendor/ftrack-python-api | 1 - repos/avalon-core | 1 - repos/avalon-unreal-integration | 1 - 4 files changed, 4 deletions(-) delete mode 160000 openpype/modules/default_modules/ftrack/python2_vendor/arrow delete mode 160000 openpype/modules/default_modules/ftrack/python2_vendor/ftrack-python-api delete mode 160000 repos/avalon-core delete mode 160000 repos/avalon-unreal-integration diff --git a/openpype/modules/default_modules/ftrack/python2_vendor/arrow b/openpype/modules/default_modules/ftrack/python2_vendor/arrow deleted file mode 160000 index b746fedf72..0000000000 --- a/openpype/modules/default_modules/ftrack/python2_vendor/arrow +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b746fedf7286c3755a46f07ab72f4c414cd41fc0 diff --git a/openpype/modules/default_modules/ftrack/python2_vendor/ftrack-python-api b/openpype/modules/default_modules/ftrack/python2_vendor/ftrack-python-api deleted file mode 160000 index d277f474ab..0000000000 --- a/openpype/modules/default_modules/ftrack/python2_vendor/ftrack-python-api +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d277f474ab016e7b53479c36af87cb861d0cc53e diff --git a/repos/avalon-core b/repos/avalon-core deleted file mode 160000 index 64491fbbcf..0000000000 --- a/repos/avalon-core +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 64491fbbcf89ba2a0b3a20d67d7486c6142232b3 diff --git a/repos/avalon-unreal-integration b/repos/avalon-unreal-integration deleted file mode 160000 index 43f6ea9439..0000000000 --- a/repos/avalon-unreal-integration +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 43f6ea943980b29c02a170942b566ae11f2b7080 From fbdb06a9ac542831ed2da5e1cb5361acf62bc938 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 28 Apr 2022 20:34:57 +0200 Subject: [PATCH 097/109] nuke: code consistency - replacing single quotations with double - improving code --- openpype/hosts/nuke/api/lib.py | 86 ++++++++++++++++--------------- openpype/hosts/nuke/api/plugin.py | 2 - 2 files changed, 45 insertions(+), 43 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index bd39a1f0a8..77945c6fec 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -541,7 +541,7 @@ def get_imageio_input_colorspace(filename): def on_script_load(): ''' Callback for ffmpeg support ''' - if nuke.env['LINUX']: + if nuke.env["LINUX"]: nuke.tcl('load ffmpegReader') nuke.tcl('load ffmpegWriter') else: @@ -592,7 +592,7 @@ def check_inventory_versions(): versions = legacy_io.find({ "type": "version", "parent": version["parent"] - }).distinct('name') + }).distinct("name") max_version = max(versions) @@ -625,17 +625,17 @@ def writes_version_sync(): avalon_knob_data = read_avalon_data(each) try: - if avalon_knob_data['families'] not in ["render"]: - log.debug(avalon_knob_data['families']) + if avalon_knob_data["families"] not in ["render"]: + log.debug(avalon_knob_data["families"]) continue - node_file = each['file'].value() + node_file = each["file"].value() node_version = "v" + get_version_from_path(node_file) log.debug("node_version: {}".format(node_version)) node_new_file = node_file.replace(node_version, new_version) - each['file'].setValue(node_new_file) + each["file"].setValue(node_new_file) if not os.path.isdir(os.path.dirname(node_new_file)): log.warning("Path does not exist! I am creating it.") os.makedirs(os.path.dirname(node_new_file)) @@ -673,9 +673,9 @@ def get_render_path(node): ''' data = {'avalon': read_avalon_data(node)} data_preset = { - "nodeclass": data['avalon']['family'], - "families": [data['avalon']['families']], - "creator": data['avalon']['creator'] + "nodeclass": data["avalon"]["family"], + "families": [data["avalon"]["families"]], + "creator": data["avalon"]["creator"], } nuke_imageio_writes = get_created_node_imageio_setting(**data_preset) @@ -748,7 +748,7 @@ def format_anatomy(data): def script_name(): ''' Returns nuke script path ''' - return nuke.root().knob('name').value() + return nuke.root().knob("name").value() def add_button_write_to_read(node): @@ -843,7 +843,7 @@ def create_write_node(name, data, input=None, prenodes=None, # adding dataflow template log.debug("imageio_writes: `{}`".format(imageio_writes)) for knob in imageio_writes["knobs"]: - _data.update({knob["name"]: knob["value"]}) + _data[knob["name"]] = knob["value"] _data = fix_data_for_node_create(_data) @@ -1192,15 +1192,19 @@ class WorkfileSettings(object): erased_viewers = [] for v in nuke.allNodes(filter="Viewer"): - v['viewerProcess'].setValue(str(viewer_dict["viewerProcess"])) + # set viewProcess to preset from settings + v["viewerProcess"].setValue( + str(viewer_dict["viewerProcess"]) + ) + if str(viewer_dict["viewerProcess"]) \ - not in v['viewerProcess'].value(): + not in v["viewerProcess"].value(): copy_inputs = v.dependencies() copy_knobs = {k: v[k].value() for k in v.knobs() if k not in filter_knobs} # delete viewer with wrong settings - erased_viewers.append(v['name'].value()) + erased_viewers.append(v["name"].value()) nuke.delete(v) # create new viewer @@ -1216,7 +1220,7 @@ class WorkfileSettings(object): nv[k].setValue(v) # set viewerProcess - nv['viewerProcess'].setValue(str(viewer_dict["viewerProcess"])) + nv["viewerProcess"].setValue(str(viewer_dict["viewerProcess"])) if erased_viewers: log.warning( @@ -1308,7 +1312,7 @@ class WorkfileSettings(object): data_preset = { "nodeclass": avalon_knob_data["family"], "families": families, - "creator": avalon_knob_data['creator'] + "creator": avalon_knob_data["creator"], } nuke_imageio_writes = get_created_node_imageio_setting( @@ -1366,17 +1370,16 @@ class WorkfileSettings(object): current = n["colorspace"].value() future = str(preset_clrsp) if current != future: - changes.update({ - n.name(): { - "from": current, - "to": future - } - }) + changes[n.name()] = { + "from": current, + "to": future + } + log.debug(changes) if changes: msg = "Read nodes are not set to correct colospace:\n\n" for nname, knobs in changes.items(): - msg += str( + msg += ( " - node: '{0}' is now '{1}' but should be '{2}'\n" ).format(nname, knobs["from"], knobs["to"]) @@ -1608,17 +1611,17 @@ def get_hierarchical_attr(entity, attr, default=None): if not value: break - if value or entity['type'].lower() == 'project': + if value or entity["type"].lower() == "project": return value - parent_id = entity['parent'] + parent_id = entity["parent"] if ( - entity['type'].lower() == 'asset' - and entity.get('data', {}).get('visualParent') + entity["type"].lower() == "asset" + and entity.get("data", {}).get("visualParent") ): - parent_id = entity['data']['visualParent'] + parent_id = entity["data"]["visualParent"] - parent = legacy_io.find_one({'_id': parent_id}) + parent = legacy_io.find_one({"_id": parent_id}) return get_hierarchical_attr(parent, attr) @@ -1631,9 +1634,9 @@ def get_write_node_template_attr(node): data = {"avalon": read_avalon_data(node)} data_preset = { - "nodeclass": data['avalon']['family'], - "families": [data['avalon']['families']], - "creator": data['avalon']['creator'] + "nodeclass": data["avalon"]["family"], + "families": [data["avalon"]["families"]], + "creator": data["avalon"]["creator"], } # get template data @@ -1644,10 +1647,11 @@ def get_write_node_template_attr(node): "file": get_render_path(node) }) - # adding imageio template - {correct_data.update({k: v}) - for k, v in nuke_imageio_writes.items() - if k not in ["_id", "_previous"]} + # adding imageio knob presets + for k, v in nuke_imageio_writes.items(): + if k in ["_id", "_previous"]: + continue + correct_data[k] = v # fix badly encoded data return fix_data_for_node_create(correct_data) @@ -1763,8 +1767,8 @@ def maintained_selection(): Example: >>> with maintained_selection(): - ... node['selected'].setValue(True) - >>> print(node['selected'].value()) + ... node["selected"].setValue(True) + >>> print(node["selected"].value()) False """ previous_selection = nuke.selectedNodes() @@ -1772,11 +1776,11 @@ def maintained_selection(): yield finally: # unselect all selection in case there is some - current_seletion = nuke.selectedNodes() - [n['selected'].setValue(False) for n in current_seletion] + reset_selection() + # and select all previously selected nodes if previous_selection: - [n['selected'].setValue(True) for n in previous_selection] + select_nodes(previous_selection) def reset_selection(): diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index eaf0ab6911..9c22edf63d 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -260,8 +260,6 @@ class ExporterReview(object): return nuke_imageio["viewer"]["viewerProcess"] - - class ExporterReviewLut(ExporterReview): """ Generator object for review lut from Nuke From 162b21b61e88b26a7a82830bc5db7c8c696f0249 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 28 Apr 2022 20:36:18 +0200 Subject: [PATCH 098/109] nuke: typo in subset (sebset) --- openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py | 2 +- openpype/settings/defaults/project_settings/nuke.json | 2 +- .../schemas/projects_schema/schemas/schema_anatomy_imageio.json | 2 +- .../schemas/projects_schema/schemas/schema_nuke_publish.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py b/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py index 2e8843d2e0..2a79d600ba 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py +++ b/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py @@ -52,7 +52,7 @@ class ExtractReviewDataMov(openpype.api.Extractor): for o_name, o_data in self.outputs.items(): f_families = o_data["filter"]["families"] f_task_types = o_data["filter"]["task_types"] - f_subsets = o_data["filter"]["sebsets"] + f_subsets = o_data["filter"]["subsets"] self.log.debug( "f_families `{}` > families: {}".format( diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index ab015271ff..ddf996b5f2 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -120,7 +120,7 @@ "filter": { "task_types": [], "families": [], - "sebsets": [] + "subsets": [] }, "read_raw": false, "viewer_process_override": "", diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_imageio.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_imageio.json index 141b51da0a..c90eeef787 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_imageio.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_imageio.json @@ -327,7 +327,7 @@ "label": "Nuke Node Class" }, { - "key": "sebsets", + "key": "subsets", "label": "Subsets", "type": "list", "object_type": "text" diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json index 4a796f1933..d67fb309bd 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json @@ -212,7 +212,7 @@ "object_type": "text" }, { - "key": "sebsets", + "key": "subsets", "label": "Subsets", "type": "list", "object_type": "text" From 5546ea2fa7bce5db07b7cb27a430d503bf62762c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 28 Apr 2022 20:37:34 +0200 Subject: [PATCH 099/109] nuke: including `overrideNodes` keys also adding subset to filtering kwargs --- openpype/hosts/nuke/api/lib.py | 63 ++++++++++++++++++++++++++++--- openpype/hosts/nuke/api/plugin.py | 3 +- 2 files changed, 59 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 77945c6fec..3745bd7be7 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -506,20 +506,68 @@ def get_created_node_imageio_setting(**kwarg): log.debug(kwarg) nodeclass = kwarg.get("nodeclass", None) creator = kwarg.get("creator", None) + subset = kwarg.get("subset", None) assert any([creator, nodeclass]), nuke.message( "`{}`: Missing mandatory kwargs `host`, `cls`".format(__file__)) - imageio_nodes = get_nuke_imageio_settings()["nodes"]["requiredNodes"] + imageio_nodes = get_nuke_imageio_settings()["nodes"] + required_nodes = imageio_nodes["requiredNodes"] + override_nodes = imageio_nodes["overrideNodes"] imageio_node = None - for node in imageio_nodes: + for node in required_nodes: log.info(node) - if (nodeclass in node["nukeNodeClass"]) and ( - creator in node["plugins"]): + if ( + nodeclass in node["nukeNodeClass"] + and creator in node["plugins"] + ): imageio_node = node break + log.debug("__ imageio_node: {}".format(imageio_node)) + + # find matching override node + override_imageio_node = None + for onode in override_nodes: + log.info(onode) + if nodeclass not in node["nukeNodeClass"]: + continue + + if creator not in node["plugins"]: + continue + + if ( + onode["subsets"] + and not any(re.search(s, subset) for s in onode["subsets"]) + ): + continue + + override_imageio_node = onode + break + + log.debug("__ override_imageio_node: {}".format(override_imageio_node)) + # add overrides to imageio_node + if override_imageio_node: + # get all knob names in imageio_node + knob_names = [k["name"] for k in imageio_node["knobs"]] + + for oknob in override_imageio_node["knobs"]: + for knob in imageio_node["knobs"]: + # override matching knob name + if oknob["name"] == knob["name"]: + log.debug( + "_ overriding knob: `{}` > `{}`".format( + knob, oknob + )) + knob["value"] = oknob["value"] + # add missing knobs into imageio_node + if oknob["name"] not in knob_names: + log.debug( + "_ adding knob: `{}`".format(oknob)) + imageio_node["knobs"].append(oknob) + knob_names.append(oknob["name"]) + log.info("ImageIO node: {}".format(imageio_node)) return imageio_node @@ -676,6 +724,7 @@ def get_render_path(node): "nodeclass": data["avalon"]["family"], "families": [data["avalon"]["families"]], "creator": data["avalon"]["creator"], + "subset": data["avalon"]["subset"] } nuke_imageio_writes = get_created_node_imageio_setting(**data_preset) @@ -1298,10 +1347,10 @@ class WorkfileSettings(object): # get data from avalon knob avalon_knob_data = read_avalon_data(node) - if not avalon_knob_data: + if avalon_knob_data.get("id") != "pyblish.avalon.instance": continue - if avalon_knob_data["id"] != "pyblish.avalon.instance": + if "creator" not in avalon_knob_data: continue # establish families @@ -1313,6 +1362,7 @@ class WorkfileSettings(object): "nodeclass": avalon_knob_data["family"], "families": families, "creator": avalon_knob_data["creator"], + "subset": avalon_knob_data["subset"] } nuke_imageio_writes = get_created_node_imageio_setting( @@ -1637,6 +1687,7 @@ def get_write_node_template_attr(node): "nodeclass": data["avalon"]["family"], "families": [data["avalon"]["families"]], "creator": data["avalon"]["creator"], + "subset": data["avalon"]["subset"] } # get template data diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index 9c22edf63d..fdb5930cb2 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -671,7 +671,8 @@ class AbstractWriteRender(OpenPypeCreator): write_data = { "nodeclass": self.n_class, "families": [self.family], - "avalon": self.data + "avalon": self.data, + "subset": self.data["subset"] } # add creator data From 1765f25ecd3410fffd6ae9a35da96374ecb67abb Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 28 Apr 2022 20:48:46 +0200 Subject: [PATCH 100/109] nuke: removing knob preset if no value in override --- openpype/hosts/nuke/api/lib.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 3745bd7be7..3223feaec7 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -560,7 +560,13 @@ def get_created_node_imageio_setting(**kwarg): "_ overriding knob: `{}` > `{}`".format( knob, oknob )) - knob["value"] = oknob["value"] + if not oknob["value"]: + # remove original knob if no value found in oknob + imageio_node["knobs"].remove(knob) + else: + # override knob value with oknob's + knob["value"] = oknob["value"] + # add missing knobs into imageio_node if oknob["name"] not in knob_names: log.debug( From 6d411d2617273de9c27d69efab0426c386d91316 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Apr 2022 10:09:09 +0200 Subject: [PATCH 101/109] added missing detailed description --- openpype/hosts/traypublisher/api/plugin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/traypublisher/api/plugin.py b/openpype/hosts/traypublisher/api/plugin.py index 731bf7918a..813641a7d2 100644 --- a/openpype/hosts/traypublisher/api/plugin.py +++ b/openpype/hosts/traypublisher/api/plugin.py @@ -96,6 +96,7 @@ class SettingsCreator(TrayPublishCreator): "label": item_data["label"].strip(), "icon": item_data["icon"], "description": item_data["description"], + "detailed_description": item_data["detailed_description"], "enable_review": item_data["enable_review"], "extensions": item_data["extensions"], "allow_sequences": item_data["allow_sequences"], From 7a6602a4d5c84aa0b996fe1bcdd9d516b42bbebd Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Apr 2022 10:11:09 +0200 Subject: [PATCH 102/109] added tray publisher to extract review/burnin plugins --- openpype/plugins/publish/extract_burnin.py | 1 + openpype/plugins/publish/extract_review.py | 1 + 2 files changed, 2 insertions(+) diff --git a/openpype/plugins/publish/extract_burnin.py b/openpype/plugins/publish/extract_burnin.py index 544c763b52..88093fb92f 100644 --- a/openpype/plugins/publish/extract_burnin.py +++ b/openpype/plugins/publish/extract_burnin.py @@ -41,6 +41,7 @@ class ExtractBurnin(openpype.api.Extractor): "shell", "hiero", "premiere", + "traypublisher", "standalonepublisher", "harmony", "fusion", diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index 9ee57c5a67..879125dac3 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -45,6 +45,7 @@ class ExtractReview(pyblish.api.InstancePlugin): "hiero", "premiere", "harmony", + "traypublisher", "standalonepublisher", "fusion", "tvpaint", From b2b4b3cb9cbe1beefbec4defd2b526650a71abdc Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Apr 2022 10:16:13 +0200 Subject: [PATCH 103/109] fixed drop of multiple files for single file item input --- openpype/widgets/attribute_defs/files_widget.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/openpype/widgets/attribute_defs/files_widget.py b/openpype/widgets/attribute_defs/files_widget.py index c76474d957..a3ee370bd3 100644 --- a/openpype/widgets/attribute_defs/files_widget.py +++ b/openpype/widgets/attribute_defs/files_widget.py @@ -205,6 +205,18 @@ class FilesProxyModel(QtCore.QSortFilterProxyModel): return True return False + def filter_valid_files(self, filepaths): + filtered_paths = [] + for filepath in filepaths: + if os.path.isfile(filepath): + _, ext = os.path.splitext(filepath) + if ext in self._allowed_extensions: + filtered_paths.append(filepath) + + elif self._allow_folders: + filtered_paths.append(filepath) + return filtered_paths + def filterAcceptsRow(self, row, parent_index): # Skip filtering if multivalue is set if self._multivalue: @@ -617,6 +629,9 @@ class FilesWidget(QtWidgets.QFrame): filepath = url.toLocalFile() if os.path.exists(filepath): filepaths.append(filepath) + + # Filter filepaths before passing it to model + filepaths = self._files_proxy_model.filter_valid_files(filepaths) if filepaths: self._add_filepaths(filepaths) event.accept() From fe128ff4728f7a675d6e15700fd2faeecf5b0647 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Apr 2022 10:27:16 +0200 Subject: [PATCH 104/109] added tooltip to disabled create button --- openpype/tools/publisher/widgets/create_dialog.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/openpype/tools/publisher/widgets/create_dialog.py b/openpype/tools/publisher/widgets/create_dialog.py index 971799a35a..243540f243 100644 --- a/openpype/tools/publisher/widgets/create_dialog.py +++ b/openpype/tools/publisher/widgets/create_dialog.py @@ -467,12 +467,15 @@ class CreateDialog(QtWidgets.QDialog): def _on_prereq_timer(self): prereq_available = True + creator_btn_tooltips = [] if self.creators_model.rowCount() < 1: prereq_available = False + creator_btn_tooltips.append("Creator is not selected") if self._asset_doc is None: # QUESTION how to handle invalid asset? prereq_available = False + creator_btn_tooltips.append("Context is not selected") if prereq_available != self._prereq_available: self._prereq_available = prereq_available @@ -481,6 +484,12 @@ class CreateDialog(QtWidgets.QDialog): self.creators_view.setEnabled(prereq_available) self.variant_input.setEnabled(prereq_available) self.variant_hints_btn.setEnabled(prereq_available) + + tooltip = "" + if creator_btn_tooltips: + tooltip = "\n".join(creator_btn_tooltips) + self.create_btn.setToolTip(tooltip) + self._on_variant_change() def _refresh_asset(self): From 42399403810f4ddabc28ce4c1c82a7e1a402aa80 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Apr 2022 10:38:51 +0200 Subject: [PATCH 105/109] fixed import --- openpype/lib/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index 29719b63bd..d19dacaff8 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -47,7 +47,6 @@ from .attribute_definitions import ( from .env_tools import ( env_value_to_bool, get_paths_from_environ, - get_global_environments ) from .terminal import Terminal @@ -248,7 +247,6 @@ __all__ = [ "env_value_to_bool", "get_paths_from_environ", - "get_global_environments", "get_vendor_bin_path", "get_oiio_tools_path", From 5f7524bb1cd542d49ccd60c538d78925596cf111 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 29 Apr 2022 10:46:12 +0200 Subject: [PATCH 106/109] hiero: better subset name --- .../hosts/hiero/plugins/publish/collect_frame_tag_instances.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/hiero/plugins/publish/collect_frame_tag_instances.py b/openpype/hosts/hiero/plugins/publish/collect_frame_tag_instances.py index 80a54ba2c5..982a34efd6 100644 --- a/openpype/hosts/hiero/plugins/publish/collect_frame_tag_instances.py +++ b/openpype/hosts/hiero/plugins/publish/collect_frame_tag_instances.py @@ -129,7 +129,7 @@ class CollectFrameTagInstances(pyblish.api.ContextPlugin): "family": "image", "families": ["frame"], "asset": subset_data["asset"], - "subset": subset_name, + "subset": name, "format": subset_data["format"], "frames": subset_data["frames"] } From 05cc463ed9064ab81a993f2dd0a078a0fff67b2f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 29 Apr 2022 11:25:23 +0200 Subject: [PATCH 107/109] flame: addressing pr comment --- .../hosts/flame/plugins/publish/extract_subset_resources.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py index 4598405923..fd0ece2590 100644 --- a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py +++ b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py @@ -236,8 +236,8 @@ class ExtractSubsetResources(openpype.api.Extractor): # define kwargs based on preset type if "thumbnail" in unique_name: - export_kwargs["thumb_frame_number"] = in_mark + ( - source_duration_handles / 2) + export_kwargs["thumb_frame_number"] = int(in_mark + ( + source_duration_handles / 2)) else: export_kwargs.update({ "in_mark": in_mark, From 518c60de0b63746b20d80ebbd4467bc72655afbd Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Apr 2022 15:31:05 +0200 Subject: [PATCH 108/109] fixed renderlayer key access --- .../plugins/publish/collect_instances.py | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_instances.py b/openpype/hosts/tvpaint/plugins/publish/collect_instances.py index 9b51f88cae..782907b65d 100644 --- a/openpype/hosts/tvpaint/plugins/publish/collect_instances.py +++ b/openpype/hosts/tvpaint/plugins/publish/collect_instances.py @@ -47,19 +47,18 @@ class CollectInstances(pyblish.api.ContextPlugin): # Conversion from older instances # - change 'render_layer' to 'renderlayer' - # and 'render_pass' to 'renderpass' + render_layer = instance_data.get("instance_data") + if not render_layer: + # Render Layer has only variant + if instance_data["family"] == "renderLayer": + render_layer = instance_data.get("variant") - if ( - "renderlayer" not in instance_data - and "render_layer" in instance_data - ): - instance_data["renderlayer"] = instance_data["render_layer"] + # Backwards compatibility for renderPasses + elif "render_layer" in instance_data: + render_layer = instance_data["render_layer"] - if ( - "renderpass" not in instance_data - and "render_pass" in instance_data - ): - instance_data["renderpass"] = instance_data["render_pass"] + if render_layer: + instance_data["renderlayer"] = render_layer # Store workfile instance data to instance data instance_data["originData"] = copy.deepcopy(instance_data) From f3726d0c95c35ee1e5fd3f480c751dd12fae0046 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Apr 2022 15:35:25 +0200 Subject: [PATCH 109/109] modified collect scene instance to add only 'renderlayer' --- .../hosts/tvpaint/plugins/publish/collect_scene_render.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_scene_render.py b/openpype/hosts/tvpaint/plugins/publish/collect_scene_render.py index 02e0575a2c..2b8dbdc5b4 100644 --- a/openpype/hosts/tvpaint/plugins/publish/collect_scene_render.py +++ b/openpype/hosts/tvpaint/plugins/publish/collect_scene_render.py @@ -104,10 +104,10 @@ class CollectRenderScene(pyblish.api.ContextPlugin): "representations": [], "layers": copy.deepcopy(context.data["layersData"]), "asset": asset_name, - "task": task_name + "task": task_name, + # Add render layer to instance data + "renderlayer": self.render_layer } - # Add 'renderlayer' and 'renderpass' to data - instance_data.update(dynamic_data) instance = context.create_instance(**instance_data)