From 10fa0ee5c463de5676a5e209e447c2a845f6b3f6 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 15 Dec 2021 16:34:21 +0000 Subject: [PATCH 001/398] 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 998154b5a905c9764c49abfdc6cb54dc9d5bd1f3 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 30 Jan 2022 23:53:36 +0100 Subject: [PATCH 002/398] Implement Hardware Renderer 2.0 support for Render Products --- openpype/hosts/maya/api/lib_renderproducts.py | 65 ++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/lib_renderproducts.py b/openpype/hosts/maya/api/lib_renderproducts.py index e8e4b9aaef..e879682700 100644 --- a/openpype/hosts/maya/api/lib_renderproducts.py +++ b/openpype/hosts/maya/api/lib_renderproducts.py @@ -77,6 +77,7 @@ IMAGE_PREFIXES = { "arnold": "defaultRenderGlobals.imageFilePrefix", "renderman": "rmanGlobals.imageFileFormat", "redshift": "defaultRenderGlobals.imageFilePrefix", + "mayahardware2": "defaultRenderGlobals.imageFilePrefix" } @@ -154,7 +155,8 @@ def get(layer, render_instance=None): "arnold": RenderProductsArnold, "vray": RenderProductsVray, "redshift": RenderProductsRedshift, - "renderman": RenderProductsRenderman + "renderman": RenderProductsRenderman, + "mayahardware2": RenderProductsMayaHardware }.get(renderer_name.lower(), None) if renderer is None: raise UnsupportedRendererException( @@ -1107,6 +1109,67 @@ class RenderProductsRenderman(ARenderProducts): return new_files +class RenderProductsMayaHardware(ARenderProducts): + """Expected files for MayaHardware renderer.""" + + renderer = "mayahardware2" + + extensions = [ + {"label": "JPEG", "index": 8, "extension": "jpg"}, + {"label": "PNG", "index": 32, "extension": "png"}, + {"label": "EXR(exr)", "index": 40, "extension": "exr"} + ] + + def _get_extension(self, value): + result = None + if isinstance(value, int): + extensions = { + extension["index"]: extension["extension"] + for extension in self.extensions + } + try: + result = extensions[value] + except KeyError: + raise NotImplementedError( + "Could not find extension for {}".format(value) + ) + + if isinstance(value, six.string_types): + extensions = { + extension["label"]: extension["extension"] + for extension in self.extensions + } + try: + result = extensions[value] + except KeyError: + raise NotImplementedError( + "Could not find extension for {}".format(value) + ) + + if not result: + raise NotImplementedError( + "Could not find extension for {}".format(value) + ) + + return result + + def get_render_products(self): + """Get all AOVs. + See Also: + :func:`ARenderProducts.get_render_products()` + """ + ext = self._get_extension( + self._get_attr("defaultRenderGlobals.imageFormat") + ) + + products = [] + for cam in self.get_renderable_cameras(): + product = RenderProduct(productName="beauty", ext=ext, camera=cam) + products.append(product) + + return products + + class AOVError(Exception): """Custom exception for determining AOVs.""" From 4ff7cf67ab7fe852216c7f408161ca0f9d0d5ecf Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 31 Jan 2022 11:20:13 +0000 Subject: [PATCH 003/398] 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 004/398] 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 005/398] 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 006/398] 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 007/398] 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 008/398] 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 009/398] 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 010/398] 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 011/398] 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 012/398] 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 013/398] 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 014/398] 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 015/398] 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 016/398] 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 017/398] 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 018/398] 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 019/398] 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 020/398] 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 021/398] 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 022/398] 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 023/398] 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 024/398] 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 025/398] 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 026/398] 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 027/398] 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 028/398] 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 029/398] 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 030/398] 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 031/398] 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 032/398] 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 033/398] 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 034/398] 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 035/398] 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 036/398] 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 037/398] 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 038/398] 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 039/398] 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 040/398] 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 041/398] 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 042/398] 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 043/398] 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 044/398] 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 045/398] 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 046/398] 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 047/398] 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 048/398] 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 049/398] 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 050/398] 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 051/398] 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 052/398] 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 053/398] 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 054/398] 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 055/398] 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 056/398] 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 057/398] 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 058/398] 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 059/398] 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 ddddb86e77939214d818a54efc7a0eda5587588d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Mon, 25 Apr 2022 18:56:20 +0200 Subject: [PATCH 060/398] wip on ue5 support --- openpype/hosts/unreal/lib.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/unreal/lib.py b/openpype/hosts/unreal/lib.py index d4a776e892..c0b4c7061c 100644 --- a/openpype/hosts/unreal/lib.py +++ b/openpype/hosts/unreal/lib.py @@ -74,7 +74,7 @@ def get_editor_executable_path(engine_path: Path) -> Path: """Get UE4 Editor executable path.""" ue4_path = engine_path / "Engine/Binaries" if platform.system().lower() == "windows": - ue4_path /= "Win64/UE4Editor.exe" + ue4_path /= "Win64/UnrealEditor.exe" elif platform.system().lower() == "linux": ue4_path /= "Linux/UE4Editor" @@ -420,7 +420,7 @@ class {1}_API A{0}GameModeBase : public AGameModeBase f.write(game_mode_h) u_build_tool = Path( - engine_path / "Engine/Binaries/DotNET/UnrealBuildTool.exe") + engine_path / "Engine/Binaries/DotNET/UnrealBuildTool/UnrealBuildTool.exe") u_header_tool = None arch = "Win64" From aace513c84c1f2f901be54139759dcc3bddf3a5d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 25 Apr 2022 19:25:57 +0200 Subject: [PATCH 061/398] 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 062/398] 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 063/398] 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 064/398] 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 065/398] 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 066/398] 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 e5f3d28c17989647ecfe0b406e6e4f75ac02b433 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 26 Apr 2022 15:04:08 +0200 Subject: [PATCH 067/398] OP-3113 - moved plugins into addon Plugins were only for specific customer (and not working because of missing psd-tools). --- .../publish/collect_batch_instances.py | 70 ----- .../publish/extract_bg_for_compositing.py | 243 ----------------- .../plugins/publish/extract_bg_main_groups.py | 248 ------------------ .../publish/extract_images_from_psd.py | 171 ------------ 4 files changed, 732 deletions(-) delete mode 100644 openpype/hosts/standalonepublisher/plugins/publish/collect_batch_instances.py delete mode 100644 openpype/hosts/standalonepublisher/plugins/publish/extract_bg_for_compositing.py delete mode 100644 openpype/hosts/standalonepublisher/plugins/publish/extract_bg_main_groups.py delete mode 100644 openpype/hosts/standalonepublisher/plugins/publish/extract_images_from_psd.py diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_batch_instances.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_batch_instances.py deleted file mode 100644 index 4ca1f72cc4..0000000000 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_batch_instances.py +++ /dev/null @@ -1,70 +0,0 @@ -import copy -import pyblish.api -from pprint import pformat - - -class CollectBatchInstances(pyblish.api.InstancePlugin): - """Collect all available instances for batch publish.""" - - label = "Collect Batch Instances" - order = pyblish.api.CollectorOrder + 0.489 - hosts = ["standalonepublisher"] - families = ["background_batch"] - - # presets - default_subset_task = { - "background_batch": "background" - } - subsets = { - "background_batch": { - "backgroundLayout": { - "task": "background", - "family": "backgroundLayout" - }, - "backgroundComp": { - "task": "background", - "family": "backgroundComp" - }, - "workfileBackground": { - "task": "background", - "family": "workfile" - } - } - } - unchecked_by_default = [] - - def process(self, instance): - context = instance.context - asset_name = instance.data["asset"] - family = instance.data["family"] - - default_task_name = self.default_subset_task.get(family) - for subset_name, subset_data in self.subsets[family].items(): - instance_name = f"{asset_name}_{subset_name}" - task_name = subset_data.get("task") or default_task_name - - # create new instance - new_instance = context.create_instance(instance_name) - - # add original instance data except name key - for key, value in instance.data.items(): - if key not in ["name"]: - # Make sure value is copy since value may be object which - # can be shared across all new created objects - new_instance.data[key] = copy.deepcopy(value) - - # add subset data from preset - new_instance.data.update(subset_data) - - new_instance.data["label"] = instance_name - new_instance.data["subset"] = subset_name - new_instance.data["task"] = task_name - - if subset_name in self.unchecked_by_default: - new_instance.data["publish"] = False - - self.log.info(f"Created new instance: {instance_name}") - self.log.debug(f"_ inst_data: {pformat(new_instance.data)}") - - # delete original instance - context.remove(instance) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_bg_for_compositing.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_bg_for_compositing.py deleted file mode 100644 index 9621d70739..0000000000 --- a/openpype/hosts/standalonepublisher/plugins/publish/extract_bg_for_compositing.py +++ /dev/null @@ -1,243 +0,0 @@ -import os -import json -import copy - -import openpype.api -from openpype.pipeline import legacy_io - -PSDImage = None - - -class ExtractBGForComp(openpype.api.Extractor): - label = "Extract Background for Compositing" - families = ["backgroundComp"] - hosts = ["standalonepublisher"] - - new_instance_family = "background" - - # Presetable - allowed_group_names = [ - "OL", "BG", "MG", "FG", "SB", "UL", "SKY", "Field Guide", "Field_Guide", - "ANIM" - ] - - def process(self, instance): - # Check if python module `psd_tools` is installed - try: - global PSDImage - from psd_tools import PSDImage - except Exception: - raise AssertionError( - "BUG: Python module `psd-tools` is not installed!" - ) - - self.allowed_group_names = [ - name.lower() - for name in self.allowed_group_names - ] - - self.redo_global_plugins(instance) - - repres = instance.data.get("representations") - if not repres: - self.log.info("There are no representations on instance.") - return - - if not instance.data.get("transfers"): - instance.data["transfers"] = [] - - # Prepare staging dir - staging_dir = self.staging_dir(instance) - if not os.path.exists(staging_dir): - os.makedirs(staging_dir) - - for repre in tuple(repres): - # Skip all files without .psd extension - repre_ext = repre["ext"].lower() - if repre_ext.startswith("."): - repre_ext = repre_ext[1:] - - if repre_ext != "psd": - continue - - # Prepare publish dir for transfers - publish_dir = instance.data["publishDir"] - - # Prepare json filepath where extracted metadata are stored - json_filename = "{}.json".format(instance.name) - json_full_path = os.path.join(staging_dir, json_filename) - - self.log.debug(f"`staging_dir` is \"{staging_dir}\"") - - # Prepare new repre data - new_repre = { - "name": "json", - "ext": "json", - "files": json_filename, - "stagingDir": staging_dir - } - - # TODO add check of list - psd_filename = repre["files"] - psd_folder_path = repre["stagingDir"] - psd_filepath = os.path.join(psd_folder_path, psd_filename) - self.log.debug(f"psd_filepath: \"{psd_filepath}\"") - psd_object = PSDImage.open(psd_filepath) - - json_data, transfers = self.export_compositing_images( - psd_object, staging_dir, publish_dir - ) - self.log.info("Json file path: {}".format(json_full_path)) - with open(json_full_path, "w") as json_filestream: - json.dump(json_data, json_filestream, indent=4) - - instance.data["transfers"].extend(transfers) - instance.data["representations"].remove(repre) - instance.data["representations"].append(new_repre) - - def export_compositing_images(self, psd_object, output_dir, publish_dir): - json_data = { - "__schema_version__": 1, - "children": [] - } - transfers = [] - for main_idx, main_layer in enumerate(psd_object): - if ( - not main_layer.is_visible() - or main_layer.name.lower() not in self.allowed_group_names - or not main_layer.is_group - ): - continue - - export_layers = [] - layers_idx = 0 - for layer in main_layer: - # TODO this way may be added also layers next to "ADJ" - if layer.name.lower() == "adj": - for _layer in layer: - export_layers.append((layers_idx, _layer)) - layers_idx += 1 - - else: - export_layers.append((layers_idx, layer)) - layers_idx += 1 - - if not export_layers: - continue - - main_layer_data = { - "index": main_idx, - "name": main_layer.name, - "children": [] - } - - for layer_idx, layer in export_layers: - has_size = layer.width > 0 and layer.height > 0 - if not has_size: - self.log.debug(( - "Skipping layer \"{}\" because does " - "not have any content." - ).format(layer.name)) - continue - - main_layer_name = main_layer.name.replace(" ", "_") - layer_name = layer.name.replace(" ", "_") - - filename = "{:0>2}_{}_{:0>2}_{}.png".format( - main_idx + 1, main_layer_name, layer_idx + 1, layer_name - ) - layer_data = { - "index": layer_idx, - "name": layer.name, - "filename": filename - } - output_filepath = os.path.join(output_dir, filename) - dst_filepath = os.path.join(publish_dir, filename) - transfers.append((output_filepath, dst_filepath)) - - pil_object = layer.composite(viewport=psd_object.viewbox) - pil_object.save(output_filepath, "PNG") - - main_layer_data["children"].append(layer_data) - - if main_layer_data["children"]: - json_data["children"].append(main_layer_data) - - return json_data, transfers - - def redo_global_plugins(self, instance): - # TODO do this in collection phase - # Copy `families` and check if `family` is not in current families - families = instance.data.get("families") or list() - if families: - families = list(set(families)) - - if self.new_instance_family in families: - families.remove(self.new_instance_family) - - self.log.debug( - "Setting new instance families {}".format(str(families)) - ) - instance.data["families"] = families - - # Override instance data with new information - instance.data["family"] = self.new_instance_family - - subset_name = instance.data["anatomyData"]["subset"] - asset_doc = instance.data["assetEntity"] - latest_version = self.find_last_version(subset_name, asset_doc) - version_number = 1 - if latest_version is not None: - version_number += latest_version - - instance.data["latestVersion"] = latest_version - instance.data["version"] = version_number - - # Same data apply to anatomy data - instance.data["anatomyData"].update({ - "family": self.new_instance_family, - "version": version_number - }) - - # Redo publish and resources dir - anatomy = instance.context.data["anatomy"] - template_data = copy.deepcopy(instance.data["anatomyData"]) - template_data.update({ - "frame": "FRAME_TEMP", - "representation": "TEMP" - }) - anatomy_filled = anatomy.format(template_data) - if "folder" in anatomy.templates["publish"]: - publish_folder = anatomy_filled["publish"]["folder"] - else: - publish_folder = os.path.dirname(anatomy_filled["publish"]["path"]) - - publish_folder = os.path.normpath(publish_folder) - resources_folder = os.path.join(publish_folder, "resources") - - instance.data["publishDir"] = publish_folder - instance.data["resourcesDir"] = resources_folder - - self.log.debug("publishDir: \"{}\"".format(publish_folder)) - self.log.debug("resourcesDir: \"{}\"".format(resources_folder)) - - def find_last_version(self, subset_name, asset_doc): - subset_doc = legacy_io.find_one({ - "type": "subset", - "name": subset_name, - "parent": asset_doc["_id"] - }) - - if subset_doc is None: - self.log.debug("Subset entity does not exist yet.") - else: - version_doc = legacy_io.find_one( - { - "type": "version", - "parent": subset_doc["_id"] - }, - sort=[("name", -1)] - ) - if version_doc: - return int(version_doc["name"]) - return None diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_bg_main_groups.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_bg_main_groups.py deleted file mode 100644 index b45f04e574..0000000000 --- a/openpype/hosts/standalonepublisher/plugins/publish/extract_bg_main_groups.py +++ /dev/null @@ -1,248 +0,0 @@ -import os -import copy -import json - -import pyblish.api - -import openpype.api -from openpype.pipeline import legacy_io - -PSDImage = None - - -class ExtractBGMainGroups(openpype.api.Extractor): - label = "Extract Background Layout" - order = pyblish.api.ExtractorOrder + 0.02 - families = ["backgroundLayout"] - hosts = ["standalonepublisher"] - - new_instance_family = "background" - - # Presetable - allowed_group_names = [ - "OL", "BG", "MG", "FG", "UL", "SB", "SKY", "Field Guide", "Field_Guide", - "ANIM" - ] - - def process(self, instance): - # Check if python module `psd_tools` is installed - try: - global PSDImage - from psd_tools import PSDImage - except Exception: - raise AssertionError( - "BUG: Python module `psd-tools` is not installed!" - ) - - self.allowed_group_names = [ - name.lower() - for name in self.allowed_group_names - ] - repres = instance.data.get("representations") - if not repres: - self.log.info("There are no representations on instance.") - return - - self.redo_global_plugins(instance) - - repres = instance.data.get("representations") - if not repres: - self.log.info("There are no representations on instance.") - return - - if not instance.data.get("transfers"): - instance.data["transfers"] = [] - - # Prepare staging dir - staging_dir = self.staging_dir(instance) - if not os.path.exists(staging_dir): - os.makedirs(staging_dir) - - # Prepare publish dir for transfers - publish_dir = instance.data["publishDir"] - - for repre in tuple(repres): - # Skip all files without .psd extension - repre_ext = repre["ext"].lower() - if repre_ext.startswith("."): - repre_ext = repre_ext[1:] - - if repre_ext != "psd": - continue - - # Prepare json filepath where extracted metadata are stored - json_filename = "{}.json".format(instance.name) - json_full_path = os.path.join(staging_dir, json_filename) - - self.log.debug(f"`staging_dir` is \"{staging_dir}\"") - - # Prepare new repre data - new_repre = { - "name": "json", - "ext": "json", - "files": json_filename, - "stagingDir": staging_dir - } - - # TODO add check of list - psd_filename = repre["files"] - psd_folder_path = repre["stagingDir"] - psd_filepath = os.path.join(psd_folder_path, psd_filename) - self.log.debug(f"psd_filepath: \"{psd_filepath}\"") - psd_object = PSDImage.open(psd_filepath) - - json_data, transfers = self.export_compositing_images( - psd_object, staging_dir, publish_dir - ) - self.log.info("Json file path: {}".format(json_full_path)) - with open(json_full_path, "w") as json_filestream: - json.dump(json_data, json_filestream, indent=4) - - instance.data["transfers"].extend(transfers) - instance.data["representations"].remove(repre) - instance.data["representations"].append(new_repre) - - def export_compositing_images(self, psd_object, output_dir, publish_dir): - json_data = { - "__schema_version__": 1, - "children": [] - } - output_ext = ".png" - - to_export = [] - for layer_idx, layer in enumerate(psd_object): - layer_name = layer.name.replace(" ", "_") - if ( - not layer.is_visible() - or layer_name.lower() not in self.allowed_group_names - ): - continue - - has_size = layer.width > 0 and layer.height > 0 - if not has_size: - self.log.debug(( - "Skipping layer \"{}\" because does not have any content." - ).format(layer.name)) - continue - - filebase = "{:0>2}_{}".format(layer_idx, layer_name) - if layer_name.lower() == "anim": - if not layer.is_group: - self.log.warning("ANIM layer is not a group layer.") - continue - - children = [] - for anim_idx, anim_layer in enumerate(layer): - anim_layer_name = anim_layer.name.replace(" ", "_") - filename = "{}_{:0>2}_{}{}".format( - filebase, anim_idx, anim_layer_name, output_ext - ) - children.append({ - "index": anim_idx, - "name": anim_layer.name, - "filename": filename - }) - to_export.append((anim_layer, filename)) - - json_data["children"].append({ - "index": layer_idx, - "name": layer.name, - "children": children - }) - continue - - filename = filebase + output_ext - json_data["children"].append({ - "index": layer_idx, - "name": layer.name, - "filename": filename - }) - to_export.append((layer, filename)) - - transfers = [] - for layer, filename in to_export: - output_filepath = os.path.join(output_dir, filename) - dst_filepath = os.path.join(publish_dir, filename) - transfers.append((output_filepath, dst_filepath)) - - pil_object = layer.composite(viewport=psd_object.viewbox) - pil_object.save(output_filepath, "PNG") - - return json_data, transfers - - def redo_global_plugins(self, instance): - # TODO do this in collection phase - # Copy `families` and check if `family` is not in current families - families = instance.data.get("families") or list() - if families: - families = list(set(families)) - - if self.new_instance_family in families: - families.remove(self.new_instance_family) - - self.log.debug( - "Setting new instance families {}".format(str(families)) - ) - instance.data["families"] = families - - # Override instance data with new information - instance.data["family"] = self.new_instance_family - - subset_name = instance.data["anatomyData"]["subset"] - asset_doc = instance.data["assetEntity"] - latest_version = self.find_last_version(subset_name, asset_doc) - version_number = 1 - if latest_version is not None: - version_number += latest_version - - instance.data["latestVersion"] = latest_version - instance.data["version"] = version_number - - # Same data apply to anatomy data - instance.data["anatomyData"].update({ - "family": self.new_instance_family, - "version": version_number - }) - - # Redo publish and resources dir - anatomy = instance.context.data["anatomy"] - template_data = copy.deepcopy(instance.data["anatomyData"]) - template_data.update({ - "frame": "FRAME_TEMP", - "representation": "TEMP" - }) - anatomy_filled = anatomy.format(template_data) - if "folder" in anatomy.templates["publish"]: - publish_folder = anatomy_filled["publish"]["folder"] - else: - publish_folder = os.path.dirname(anatomy_filled["publish"]["path"]) - - publish_folder = os.path.normpath(publish_folder) - resources_folder = os.path.join(publish_folder, "resources") - - instance.data["publishDir"] = publish_folder - instance.data["resourcesDir"] = resources_folder - - self.log.debug("publishDir: \"{}\"".format(publish_folder)) - self.log.debug("resourcesDir: \"{}\"".format(resources_folder)) - - def find_last_version(self, subset_name, asset_doc): - subset_doc = legacy_io.find_one({ - "type": "subset", - "name": subset_name, - "parent": asset_doc["_id"] - }) - - if subset_doc is None: - self.log.debug("Subset entity does not exist yet.") - else: - version_doc = legacy_io.find_one( - { - "type": "version", - "parent": subset_doc["_id"] - }, - sort=[("name", -1)] - ) - if version_doc: - return int(version_doc["name"]) - return None diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_images_from_psd.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_images_from_psd.py deleted file mode 100644 index 8485fa0915..0000000000 --- a/openpype/hosts/standalonepublisher/plugins/publish/extract_images_from_psd.py +++ /dev/null @@ -1,171 +0,0 @@ -import os -import copy -import pyblish.api - -import openpype.api -from openpype.pipeline import legacy_io - -PSDImage = None - - -class ExtractImagesFromPSD(openpype.api.Extractor): - # PLUGIN is not currently enabled because was decided to use different - # approach - enabled = False - active = False - label = "Extract Images from PSD" - order = pyblish.api.ExtractorOrder + 0.02 - families = ["backgroundLayout"] - hosts = ["standalonepublisher"] - - new_instance_family = "image" - ignored_instance_data_keys = ("name", "label", "stagingDir", "version") - # Presetable - allowed_group_names = [ - "OL", "BG", "MG", "FG", "UL", "SKY", "Field Guide", "Field_Guide", - "ANIM" - ] - - def process(self, instance): - # Check if python module `psd_tools` is installed - try: - global PSDImage - from psd_tools import PSDImage - except Exception: - raise AssertionError( - "BUG: Python module `psd-tools` is not installed!" - ) - - self.allowed_group_names = [ - name.lower() - for name in self.allowed_group_names - ] - repres = instance.data.get("representations") - if not repres: - self.log.info("There are no representations on instance.") - return - - for repre in tuple(repres): - # Skip all files without .psd extension - repre_ext = repre["ext"].lower() - if repre_ext.startswith("."): - repre_ext = repre_ext[1:] - - if repre_ext != "psd": - continue - - # TODO add check of list of "files" value - psd_filename = repre["files"] - psd_folder_path = repre["stagingDir"] - psd_filepath = os.path.join(psd_folder_path, psd_filename) - self.log.debug(f"psd_filepath: \"{psd_filepath}\"") - psd_object = PSDImage.open(psd_filepath) - - self.create_new_instances(instance, psd_object) - - # Remove the instance from context - instance.context.remove(instance) - - def create_new_instances(self, instance, psd_object): - asset_doc = instance.data["assetEntity"] - for layer in psd_object: - if ( - not layer.is_visible() - or layer.name.lower() not in self.allowed_group_names - ): - continue - - has_size = layer.width > 0 and layer.height > 0 - if not has_size: - self.log.debug(( - "Skipping layer \"{}\" because does " - "not have any content." - ).format(layer.name)) - continue - - layer_name = layer.name.replace(" ", "_") - instance_name = subset_name = f"image{layer_name}" - self.log.info( - f"Creating new instance with name \"{instance_name}\"" - ) - new_instance = instance.context.create_instance(instance_name) - for key, value in instance.data.items(): - if key not in self.ignored_instance_data_keys: - new_instance.data[key] = copy.deepcopy(value) - - new_instance.data["label"] = " ".join( - (new_instance.data["asset"], instance_name) - ) - - # Find latest version - latest_version = self.find_last_version(subset_name, asset_doc) - version_number = 1 - if latest_version is not None: - version_number += latest_version - - self.log.info( - "Next version of instance \"{}\" will be {}".format( - instance_name, version_number - ) - ) - - # Set family and subset - new_instance.data["family"] = self.new_instance_family - new_instance.data["subset"] = subset_name - new_instance.data["version"] = version_number - new_instance.data["latestVersion"] = latest_version - - new_instance.data["anatomyData"].update({ - "subset": subset_name, - "family": self.new_instance_family, - "version": version_number - }) - - # Copy `families` and check if `family` is not in current families - families = new_instance.data.get("families") or list() - if families: - families = list(set(families)) - - if self.new_instance_family in families: - families.remove(self.new_instance_family) - new_instance.data["families"] = families - - # Prepare staging dir for new instance - staging_dir = self.staging_dir(new_instance) - - output_filename = "{}.png".format(layer_name) - output_filepath = os.path.join(staging_dir, output_filename) - pil_object = layer.composite(viewport=psd_object.viewbox) - pil_object.save(output_filepath, "PNG") - - new_repre = { - "name": "png", - "ext": "png", - "files": output_filename, - "stagingDir": staging_dir - } - self.log.debug( - "Creating new representation: {}".format(new_repre) - ) - new_instance.data["representations"] = [new_repre] - - def find_last_version(self, subset_name, asset_doc): - subset_doc = legacy_io.find_one({ - "type": "subset", - "name": subset_name, - "parent": asset_doc["_id"] - }) - - if subset_doc is None: - self.log.debug("Subset entity does not exist yet.") - else: - version_doc = legacy_io.find_one( - { - "type": "version", - "parent": subset_doc["_id"] - }, - sort=[("name", -1)] - ) - if version_doc: - return int(version_doc["name"]) - return None From 604263b22e0cdb5ac6d98150d767732e2a068f98 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 26 Apr 2022 15:06:16 +0200 Subject: [PATCH 068/398] OP-3113 - added Background task to default Task enum --- openpype/settings/defaults/project_anatomy/tasks.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/settings/defaults/project_anatomy/tasks.json b/openpype/settings/defaults/project_anatomy/tasks.json index 74504cc4d7..178f2af639 100644 --- a/openpype/settings/defaults/project_anatomy/tasks.json +++ b/openpype/settings/defaults/project_anatomy/tasks.json @@ -40,5 +40,8 @@ }, "Compositing": { "short_name": "comp" + }, + "Background": { + "short_name": "back" } } \ No newline at end of file From 342fda6315a48a40580a885969940b32588de19e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 26 Apr 2022 15:49:22 +0200 Subject: [PATCH 069/398] 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 070/398] 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 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 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 071/398] 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 072/398] 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 073/398] 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 074/398] 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 075/398] 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 076/398] 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 077/398] 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 078/398] 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 079/398] 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 080/398] 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 081/398] 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 082/398] 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 083/398] 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 084/398] 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 085/398] 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 086/398] 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 087/398] 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 088/398] 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 089/398] 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 090/398] 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 091/398] 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 092/398] 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 093/398] 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 094/398] 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 095/398] 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 096/398] 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 097/398] 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 098/398] 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 0ca5f9304e7cb8e9a6fe5961bd41152cad73d58a Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 28 Apr 2022 18:09:43 +0100 Subject: [PATCH 099/398] Layout loading will load assets in a different directory --- .../hosts/unreal/plugins/load/load_layout.py | 2 +- .../hosts/unreal/plugins/load/load_rig.py | 71 ++++++++++--------- 2 files changed, 37 insertions(+), 36 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index e09255d88c..fc80859261 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -360,7 +360,7 @@ class LayoutLoader(plugin.Loader): continue options = { - "asset_dir": asset_dir + # "asset_dir": asset_dir } assets = load_container( diff --git a/openpype/hosts/unreal/plugins/load/load_rig.py b/openpype/hosts/unreal/plugins/load/load_rig.py index ff844a5e94..c27bd23aaf 100644 --- a/openpype/hosts/unreal/plugins/load/load_rig.py +++ b/openpype/hosts/unreal/plugins/load/load_rig.py @@ -52,54 +52,55 @@ class SkeletalMeshFBXLoader(plugin.Loader): asset_name = "{}_{}".format(asset, name) else: asset_name = "{}".format(name) + version = context.get('version').get('name') tools = unreal.AssetToolsHelpers().get_asset_tools() asset_dir, container_name = tools.create_unique_asset_name( - "{}/{}/{}".format(root, asset, name), suffix="") + f"{root}/{asset}/{name}_v{version:03d}", suffix="") container_name += suffix - unreal.EditorAssetLibrary.make_directory(asset_dir) + if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): + unreal.EditorAssetLibrary.make_directory(asset_dir) - task = unreal.AssetImportTask() + task = unreal.AssetImportTask() - task.set_editor_property('filename', self.fname) - task.set_editor_property('destination_path', asset_dir) - task.set_editor_property('destination_name', asset_name) - task.set_editor_property('replace_existing', False) - task.set_editor_property('automated', True) - task.set_editor_property('save', False) + task.set_editor_property('filename', self.fname) + task.set_editor_property('destination_path', asset_dir) + task.set_editor_property('destination_name', asset_name) + task.set_editor_property('replace_existing', False) + task.set_editor_property('automated', True) + task.set_editor_property('save', False) - # set import options here - options = unreal.FbxImportUI() - options.set_editor_property('import_as_skeletal', True) - options.set_editor_property('import_animations', False) - options.set_editor_property('import_mesh', True) - options.set_editor_property('import_materials', True) - options.set_editor_property('import_textures', True) - options.set_editor_property('skeleton', None) - options.set_editor_property('create_physics_asset', False) + # set import options here + options = unreal.FbxImportUI() + options.set_editor_property('import_as_skeletal', True) + options.set_editor_property('import_animations', False) + options.set_editor_property('import_mesh', True) + options.set_editor_property('import_materials', False) + options.set_editor_property('import_textures', False) + options.set_editor_property('skeleton', None) + options.set_editor_property('create_physics_asset', False) - options.set_editor_property('mesh_type_to_import', - unreal.FBXImportType.FBXIT_SKELETAL_MESH) + options.set_editor_property( + 'mesh_type_to_import', + unreal.FBXImportType.FBXIT_SKELETAL_MESH) - options.skeletal_mesh_import_data.set_editor_property( - 'import_content_type', - unreal.FBXImportContentType.FBXICT_ALL - ) - # set to import normals, otherwise Unreal will compute them - # and it will take a long time, depending on the size of the mesh - options.skeletal_mesh_import_data.set_editor_property( - 'normal_import_method', - unreal.FBXNormalImportMethod.FBXNIM_IMPORT_NORMALS - ) + options.skeletal_mesh_import_data.set_editor_property( + 'import_content_type', + unreal.FBXImportContentType.FBXICT_ALL) + # set to import normals, otherwise Unreal will compute them + # and it will take a long time, depending on the size of the mesh + options.skeletal_mesh_import_data.set_editor_property( + 'normal_import_method', + unreal.FBXNormalImportMethod.FBXNIM_IMPORT_NORMALS) - task.options = options - unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) # noqa: E501 + task.options = options + unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) # noqa: E501 - # Create Asset Container - unreal_pipeline.create_container( - container=container_name, path=asset_dir) + # Create Asset Container + unreal_pipeline.create_container( + container=container_name, path=asset_dir) data = { "schema": "openpype:container-2.0", From 337df2d605020e7fc7c0c45794ea9d3294d0374b Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 28 Apr 2022 18:13:46 +0100 Subject: [PATCH 100/398] Reworked update and remove operation for layouts --- .../hosts/unreal/plugins/load/load_layout.py | 526 ++++++++++-------- 1 file changed, 303 insertions(+), 223 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index fc80859261..163dd1a8af 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -208,7 +208,14 @@ class LayoutLoader(plugin.Loader): actors.append(actor) - binding = sequence.add_possessable(actor) + binding = None + for p in sequence.get_possessables(): + if p.get_name() == actor.get_name(): + binding = p + break + + if not binding: + binding = sequence.add_possessable(actor) bindings.append(binding) @@ -299,15 +306,101 @@ class LayoutLoader(plugin.Loader): # Add animation to the sequencer bindings = bindings_dict.get(instance_name) + ar = unreal.AssetRegistryHelpers.get_asset_registry() + for binding in bindings: - binding.add_track(unreal.MovieSceneSkeletalAnimationTrack) - for track in binding.get_tracks(): + tracks = binding.get_tracks() + track = None + if not tracks: + track = binding.add_track( + unreal.MovieSceneSkeletalAnimationTrack) + else: + track = tracks[0] + + sections = track.get_sections() + section = None + if not sections: section = track.add_section() - section.set_range( - sequence.get_playback_start(), - sequence.get_playback_end()) + else: + section = sections[0] + sec_params = section.get_editor_property('params') - sec_params.set_editor_property('animation', animation) + curr_anim = sec_params.get_editor_property('animation') + + if curr_anim: + # Checks if the animation path has a container. + # If it does, it means that the animation is already + # in the sequencer. + anim_path = str(Path( + curr_anim.get_path_name()).parent + ).replace('\\', '/') + + filter = unreal.ARFilter( + class_names=["AssetContainer"], + package_paths=[anim_path], + recursive_paths=False) + containers = ar.get_assets(filter) + + if len(containers) > 0: + return + + 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 _generate_sequence(self, h, h_dir): + tools = unreal.AssetToolsHelpers().get_asset_tools() + + sequence = tools.create_asset( + asset_name=h, + package_path=h_dir, + asset_class=unreal.LevelSequence, + factory=unreal.LevelSequenceFactoryNew() + ) + + asset_data = legacy_io.find_one({ + "type": "asset", + "name": h_dir.split('/')[-1] + }) + + id = asset_data.get('_id') + + start_frames = [] + end_frames = [] + + elements = list( + legacy_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(legacy_io.find({ + "type": "asset", + "data.visualParent": e.get('_id') + })) + + min_frame = min(start_frames) + max_frame = max(end_frames) + + sequence.set_display_rate( + unreal.FrameRate(asset_data.get('data').get("fps"), 1.0)) + sequence.set_playback_start(min_frame) + sequence.set_playback_end(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) + + return sequence, (min_frame, max_frame) def _process(self, lib_path, asset_dir, sequence, loaded=None): ar = unreal.AssetRegistryHelpers.get_asset_registry() @@ -326,6 +419,8 @@ class LayoutLoader(plugin.Loader): actors_dict = {} bindings_dict = {} + loaded_assets = [] + for element in data: reference = None if element.get('reference_fbx'): @@ -370,6 +465,17 @@ class LayoutLoader(plugin.Loader): options=options ) + container = None + + for asset in assets: + obj = ar.get_asset_by_object_path(asset).get_asset() + if obj.get_class().get_name() == 'AssetContainer': + container = obj + if obj.get_class().get_name() == 'Skeleton': + skeleton = obj + + loaded_assets.append(container.get_path_name()) + instances = [ item for item in data if (item.get('reference_fbx') == reference or @@ -390,15 +496,6 @@ class LayoutLoader(plugin.Loader): actors_dict[inst] = actors bindings_dict[inst] = bindings - if family == 'rig': - # Finds skeleton among the imported assets - for asset in assets: - obj = ar.get_asset_by_object_path(asset).get_asset() - if obj.get_class().get_name() == 'Skeleton': - skeleton = obj - if skeleton: - break - if skeleton: skeleton_dict[reference] = skeleton else: @@ -411,6 +508,8 @@ class LayoutLoader(plugin.Loader): asset_dir, path, instance_name, skeleton, actors_dict, animation_file, bindings_dict, sequence) + return loaded_assets + @staticmethod def _remove_family(assets, components, class_name, prop_name): ar = unreal.AssetRegistryHelpers.get_asset_registry() @@ -478,10 +577,10 @@ class LayoutLoader(plugin.Loader): hierarchy = context.get('asset').get('data').get('parents') root = self.ASSET_ROOT hierarchy_dir = root - hierarchy_list = [] + hierarchy_dir_list = [] for h in hierarchy: hierarchy_dir = f"{hierarchy_dir}/{h}" - hierarchy_list.append(hierarchy_dir) + hierarchy_dir_list.append(hierarchy_dir) asset = context.get('asset').get('name') suffix = "_CON" if asset: @@ -499,43 +598,31 @@ class LayoutLoader(plugin.Loader): # 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}) + h_dir = hierarchy_dir_list[0] + h_asset = hierarchy[0] + master_level = f"{h_dir}/{h_asset}_map.{h_asset}_map" + if not EditorAssetLibrary.does_asset_exist(master_level): + EditorLevelLibrary.new_level(f"{h_dir}/{h_asset}_map") + level = f"{asset_dir}/{asset}_map.{asset}_map" 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')) + EditorLevelLibrary.load_level(master_level) + EditorLevelUtils.add_level_to_world( + EditorLevelLibrary.get_editor_world(), + level, + unreal.LevelStreamingDynamic + ) + EditorLevelLibrary.save_all_dirty_levels() + EditorLevelLibrary.load_level(level) # Get all the sequences in the hierarchy. It will create them, if # they don't exist. sequences = [] frame_ranges = [] - i = 0 - for h in hierarchy_list: + for (h_dir, h) in zip(hierarchy_dir_list, hierarchy): root_content = EditorAssetLibrary.list_assets( - h, recursive=False, include_folder=False) + h_dir, recursive=False, include_folder=False) existing_sequences = [ EditorAssetLibrary.find_asset_data(asset) @@ -545,55 +632,10 @@ class LayoutLoader(plugin.Loader): ] if not existing_sequences: - sequence = tools.create_asset( - asset_name=hierarchy[i], - package_path=h, - asset_class=unreal.LevelSequence, - factory=unreal.LevelSequenceFactoryNew() - ) - - asset_data = legacy_io.find_one({ - "type": "asset", - "name": h.split('/')[-1] - }) - - id = asset_data.get('_id') - - start_frames = [] - end_frames = [] - - elements = list( - legacy_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(legacy_io.find({ - "type": "asset", - "data.visualParent": e.get('_id') - })) - - min_frame = min(start_frames) - max_frame = max(end_frames) - - sequence.set_display_rate( - unreal.FrameRate(asset_data.get('data').get("fps"), 1.0)) - sequence.set_playback_start(min_frame) - sequence.set_playback_end(max_frame) + sequence, frame_range = self._generate_sequence(h, h_dir) 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) + frame_ranges.append(frame_range) else: for e in existing_sequences: sequences.append(e.get_asset()) @@ -601,8 +643,6 @@ class LayoutLoader(plugin.Loader): e.get_asset().get_playback_start(), e.get_asset().get_playback_end())) - i += 1 - shot = tools.create_asset( asset_name=asset, package_path=asset_dir, @@ -612,15 +652,11 @@ class LayoutLoader(plugin.Loader): # 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')) - self._set_sequence_hierarchy( sequences[i], sequences[i + 1], frame_ranges[i][1], frame_ranges[i + 1][0], frame_ranges[i + 1][1], - maps_to_add) + [level]) data = self._get_data(asset) shot.set_display_rate( @@ -631,11 +667,11 @@ class LayoutLoader(plugin.Loader): sequences[-1], shot, frame_ranges[-1][1], data.get('clipIn'), data.get('clipOut'), - [maps[-1].get('map')]) + [level]) - EditorLevelLibrary.load_level(maps[-1].get('map')) + EditorLevelLibrary.load_level(level) - self._process(self.fname, asset_dir, shot) + loaded_assets = self._process(self.fname, asset_dir, shot) for s in sequences: EditorAssetLibrary.save_asset(s.get_full_name()) @@ -656,7 +692,8 @@ class LayoutLoader(plugin.Loader): "loader": str(self.__class__.__name__), "representation": context["representation"]["_id"], "parent": context["representation"]["parent"], - "family": context["representation"]["context"]["family"] + "family": context["representation"]["context"]["family"], + "loaded_assets": loaded_assets } unreal_pipeline.imprint( "{}/{}".format(asset_dir, container_name), data) @@ -667,148 +704,191 @@ class LayoutLoader(plugin.Loader): for a in asset_content: EditorAssetLibrary.save_asset(a) - EditorLevelLibrary.load_level(maps[0].get('map')) + EditorLevelLibrary.load_level(master_level) return asset_content def update(self, container, representation): ar = unreal.AssetRegistryHelpers.get_asset_registry() + root = "/Game/OpenPype" + + asset_dir = container.get('namespace') + + context = representation.get("context") + + hierarchy = context.get('hierarchy').split("/") + h_dir = f"{root}/{hierarchy[0]}" + h_asset = hierarchy[0] + master_level = f"{h_dir}/{h_asset}_map.{h_asset}_map" + + # # Create a temporary level to delete the layout level. + # EditorLevelLibrary.save_all_dirty_levels() + # EditorAssetLibrary.make_directory(f"{root}/tmp") + # tmp_level = f"{root}/tmp/temp_map" + # if not EditorAssetLibrary.does_asset_exist(f"{tmp_level}.temp_map"): + # EditorLevelLibrary.new_level(tmp_level) + # else: + # EditorLevelLibrary.load_level(tmp_level) + + # Get layout level + filter = unreal.ARFilter( + class_names=["World"], + package_paths=[asset_dir], + recursive_paths=False) + levels = ar.get_assets(filter) + filter = unreal.ARFilter( + class_names=["LevelSequence"], + package_paths=[asset_dir], + recursive_paths=False) + sequences = ar.get_assets(filter) + + layout_level = levels[0].get_editor_property('object_path') + + EditorLevelLibrary.save_all_dirty_levels() + EditorLevelLibrary.load_level(layout_level) + + # Delete all the actors in the level + actors = unreal.EditorLevelLibrary.get_all_level_actors() + for actor in actors: + unreal.EditorLevelLibrary.destroy_actor(actor) + + EditorLevelLibrary.save_current_level() + + EditorAssetLibrary.delete_directory(f"{asset_dir}/animations/") + source_path = get_representation_path(representation) - destination_path = container["namespace"] - lib_path = Path(get_representation_path(representation)) - self._remove_actors(destination_path) + loaded_assets = self._process( + source_path, asset_dir, sequences[0].get_asset()) - # Delete old animations - anim_path = f"{destination_path}/animations/" - EditorAssetLibrary.delete_directory(anim_path) - - with open(source_path, "r") as fp: - data = json.load(fp) - - references = [e.get('reference_fbx') for e in data] - asset_containers = self._get_asset_containers(destination_path) - loaded = [] - - # Delete all the assets imported with the previous version of the - # layout, if they're not in the new layout. - for asset_container in asset_containers: - if asset_container.get_editor_property( - 'asset_name') == container["objectName"]: - continue - ref = EditorAssetLibrary.get_metadata_tag( - asset_container.get_asset(), 'representation') - ppath = asset_container.get_editor_property('package_path') - - if ref not in references: - # If the asset is not in the new layout, delete it. - # Also check if the parent directory is empty, and delete that - # as well, if it is. - EditorAssetLibrary.delete_directory(ppath) - - parent = os.path.dirname(str(ppath)) - parent_content = EditorAssetLibrary.list_assets( - parent, recursive=False, include_folder=True - ) - - if len(parent_content) == 0: - EditorAssetLibrary.delete_directory(parent) - else: - # If the asset is in the new layout, search the instances in - # the JSON file, and create actors for them. - - actors_dict = {} - skeleton_dict = {} - - for element in data: - reference = element.get('reference_fbx') - instance_name = element.get('instance_name') - - skeleton = None - - if reference == ref and ref not in loaded: - loaded.append(ref) - - family = element.get('family') - - assets = EditorAssetLibrary.list_assets( - ppath, recursive=True, include_folder=False) - - instances = [ - item for item in data - if item.get('reference_fbx') == reference] - - for instance in instances: - transform = instance.get('transform') - inst = instance.get('instance_name') - - actors = [] - - if family == 'model': - actors = self._process_family( - assets, 'StaticMesh', transform, inst) - elif family == 'rig': - actors = self._process_family( - assets, 'SkeletalMesh', transform, inst) - actors_dict[inst] = actors - - if family == 'rig': - # Finds skeleton among the imported assets - for asset in assets: - obj = ar.get_asset_by_object_path( - asset).get_asset() - if obj.get_class().get_name() == 'Skeleton': - skeleton = obj - if skeleton: - break - - if skeleton: - skeleton_dict[reference] = skeleton - else: - skeleton = skeleton_dict.get(reference) - - animation_file = element.get('animation') - - if animation_file and skeleton: - self._import_animation( - destination_path, lib_path, - instance_name, skeleton, - actors_dict, animation_file) - - self._process(source_path, destination_path, loaded) - - container_path = "{}/{}".format(container["namespace"], - container["objectName"]) - # update metadata + data = { + "representation": str(representation["_id"]), + "parent": str(representation["parent"]), + "loaded_assets": loaded_assets + } unreal_pipeline.imprint( - container_path, - { - "representation": str(representation["_id"]), - "parent": str(representation["parent"]) - }) + "{}/{}".format(asset_dir, container.get('container_name')), data) + + EditorLevelLibrary.save_current_level() asset_content = EditorAssetLibrary.list_assets( - destination_path, recursive=True, include_folder=False) + asset_dir, recursive=True, include_folder=False) for a in asset_content: EditorAssetLibrary.save_asset(a) + EditorLevelLibrary.load_level(master_level) + def remove(self, container): """ - First, destroy all actors of the assets to be removed. Then, deletes - the asset's directory. + Delete the layout. First, check if the assets loaded with the layout + are used by other layouts. If not, delete the assets. """ - path = container["namespace"] - parent_path = os.path.dirname(path) + path = Path(container.get("namespace")) - self._remove_actors(path) + containers = unreal_pipeline.ls() + layout_containers = [ + c for c in containers + if (c.get('asset_name') != container.get('asset_name') and + c.get('family') == "layout")] - EditorAssetLibrary.delete_directory(path) + # Check if the assets have been loaded by other layouts, and deletes + # them if they haven't. + for asset in container.get('loaded_assets'): + layouts = [ + lc for lc in layout_containers + if asset in lc.get('loaded_assets')] + if len(layouts) == 0: + EditorAssetLibrary.delete_directory(str(Path(asset).parent)) + + # Remove the Level Sequence from the parent. + # We need to traverse the hierarchy from the master sequence to find + # the level sequence. + root = "/Game/OpenPype" + namespace = container.get('namespace').replace(f"{root}/", "") + ms_asset = namespace.split('/')[0] + ar = unreal.AssetRegistryHelpers.get_asset_registry() + filter = unreal.ARFilter( + class_names=["LevelSequence"], + package_paths=[f"{root}/{ms_asset}"], + recursive_paths=False) + sequences = ar.get_assets(filter) + master_sequence = sequences[0].get_asset() + filter = unreal.ARFilter( + class_names=["World"], + package_paths=[f"{root}/{ms_asset}"], + recursive_paths=False) + levels = ar.get_assets(filter) + master_level = levels[0].get_editor_property('object_path') + + sequences = [master_sequence] + + parent = None + for s in sequences: + tracks = s.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 subscene_track: + sections = subscene_track.get_sections() + for ss in sections: + if ss.get_sequence().get_name() == container.get('asset'): + parent = s + subscene_track.remove_section(ss) + break + sequences.append(ss.get_sequence()) + # Update subscenes indexes. + i = 0 + for ss in sections: + ss.set_row_index(i) + i += 1 + + if visibility_track: + sections = visibility_track.get_sections() + for ss in sections: + if unreal.Name(f"{container.get('asset')}_map") in ss.get_level_names(): + visibility_track.remove_section(ss) + # Update visibility sections indexes. + i = -1 + prev_name = [] + for ss in sections: + if prev_name != ss.get_level_names(): + i += 1 + ss.set_row_index(i) + prev_name = ss.get_level_names() + if parent: + break + + assert parent, "Could not find the parent sequence" + + # Create a temporary level to delete the layout level. + EditorLevelLibrary.save_all_dirty_levels() + EditorAssetLibrary.make_directory(f"{root}/tmp") + tmp_level = f"{root}/tmp/temp_map" + if not EditorAssetLibrary.does_asset_exist(f"{tmp_level}.temp_map"): + EditorLevelLibrary.new_level(tmp_level) + else: + EditorLevelLibrary.load_level(tmp_level) + + # Delete the layout directory. + EditorAssetLibrary.delete_directory(str(path)) + + EditorLevelLibrary.load_level(master_level) + EditorAssetLibrary.delete_directory(f"{root}/tmp") + + EditorLevelLibrary.save_current_level() + + # Delete the parent folder if there aren't any more layouts in it. asset_content = EditorAssetLibrary.list_assets( - parent_path, recursive=False, include_folder=True + str(path.parent), recursive=False, include_folder=True ) if len(asset_content) == 0: - EditorAssetLibrary.delete_directory(parent_path) + EditorAssetLibrary.delete_directory(str(path.parent)) From 9c2a7253ea53f9ecaac176852f61fe2cc04e9612 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 22 Apr 2022 11:25:23 +0100 Subject: [PATCH 101/398] Fix naming problem when loading animation for the asset container --- openpype/hosts/unreal/plugins/load/load_animation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/plugins/load/load_animation.py b/openpype/hosts/unreal/plugins/load/load_animation.py index c7581e0cdd..dc1337ed5d 100644 --- a/openpype/hosts/unreal/plugins/load/load_animation.py +++ b/openpype/hosts/unreal/plugins/load/load_animation.py @@ -165,7 +165,7 @@ class AnimationFBXLoader(plugin.Loader): instance_name = data.get("instance_name") - animation = self._process(asset_dir, container_name, instance_name) + animation = self._process(asset_dir, asset_name, instance_name) asset_content = EditorAssetLibrary.list_assets( hierarchy_dir, recursive=True, include_folder=False) From f3ed8d8403104655aa216d71f53b71d4e9ca3802 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 22 Apr 2022 11:22:00 +0100 Subject: [PATCH 102/398] Use anatomy for the render path --- openpype/hosts/unreal/api/rendering.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/api/rendering.py b/openpype/hosts/unreal/api/rendering.py index 376e1b75ce..dadfe77f45 100644 --- a/openpype/hosts/unreal/api/rendering.py +++ b/openpype/hosts/unreal/api/rendering.py @@ -1,5 +1,8 @@ +import os + import unreal +from openpype.api import Anatomy from openpype.hosts.unreal.api import pipeline @@ -46,6 +49,15 @@ def start_rendering(): if data["family"] == "render": inst_data.append(data) + try: + project = os.environ.get("AVALON_PROJECT") + anatomy = Anatomy(project) + root = anatomy.roots['renders'] + except: + raise("Could not find render root in anatomy settings.") + + render_dir = f"{root}/{project}" + # subsystem = unreal.get_editor_subsystem( # unreal.MoviePipelineQueueSubsystem) # queue = subsystem.get_queue() @@ -105,7 +117,7 @@ def start_rendering(): 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') + settings.output_directory.path = f"{render_dir}/{r.get('output')}" renderPass = job.get_configuration().find_or_add_setting_by_class( unreal.MoviePipelineDeferredPassBase) From 7724f4b943527b5d3763836daaf5e797b9b784e6 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 22 Apr 2022 16:22:01 +0100 Subject: [PATCH 103/398] Fix remove for camera assets --- .../hosts/unreal/plugins/load/load_camera.py | 93 ++++++++++++++++++- 1 file changed, 89 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index ea896f6c44..09b2dc01bf 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """Load camera from FBX.""" -import os +from pathlib import Path import unreal from unreal import EditorAssetLibrary @@ -325,11 +325,96 @@ class CameraLoader(plugin.Loader): ) def remove(self, container): - path = container["namespace"] - parent_path = os.path.dirname(path) + path = Path(container.get("namespace")) + parent_path = str(path.parent.as_posix()) - EditorAssetLibrary.delete_directory(path) + ar = unreal.AssetRegistryHelpers.get_asset_registry() + filter = unreal.ARFilter( + class_names=["LevelSequence"], + package_paths=[f"{str(path.as_posix())}"], + recursive_paths=False) + sequences = ar.get_assets(filter) + if not sequences: + raise("Could not find sequence.") + + world = ar.get_asset_by_object_path( + EditorLevelLibrary.get_editor_world().get_path_name()) + + filter = unreal.ARFilter( + class_names=["World"], + package_paths=[f"{parent_path}"], + recursive_paths=True) + maps = ar.get_assets(filter) + + # There should be only one map in the list + if not maps: + raise("Could not find map.") + + map = maps[0] + + EditorLevelLibrary.save_all_dirty_levels() + EditorLevelLibrary.load_level(map.get_full_name()) + + # Remove the camera from the level. + actors = EditorLevelLibrary.get_all_level_actors() + + for a in actors: + if a.__class__ == unreal.CineCameraActor: + EditorLevelLibrary.destroy_actor(a) + + EditorLevelLibrary.save_all_dirty_levels() + EditorLevelLibrary.load_level(world.get_full_name()) + + # There should be only one sequence in the path. + sequence_name = sequences[0].asset_name + + # Remove the Level Sequence from the parent. + # We need to traverse the hierarchy from the master sequence to find + # the level sequence. + root = "/Game/OpenPype" + namespace = container.get('namespace').replace(f"{root}/", "") + ms_asset = namespace.split('/')[0] + filter = unreal.ARFilter( + class_names=["LevelSequence"], + package_paths=[f"{root}/{ms_asset}"], + recursive_paths=False) + sequences = ar.get_assets(filter) + master_sequence = sequences[0].get_asset() + + sequences = [master_sequence] + + parent = None + for s in sequences: + tracks = s.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: + sections = subscene_track.get_sections() + for ss in sections: + if ss.get_sequence().get_name() == sequence_name: + parent = s + subscene_track.remove_section(ss) + break + sequences.append(ss.get_sequence()) + # Update subscenes indexes. + i = 0 + for ss in sections: + ss.set_row_index(i) + i += 1 + + if parent: + break + + assert parent, "Could not find the parent sequence" + + EditorAssetLibrary.delete_directory(str(path.as_posix())) + + # Check if there isn't any more assets in the parent folder, and + # delete it if not. asset_content = EditorAssetLibrary.list_assets( parent_path, recursive=False, include_folder=True ) From 8d062e8b59d6dbdafa8b92c7f853bb05dce6eee6 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 28 Apr 2022 18:18:39 +0100 Subject: [PATCH 104/398] Fixed camera update --- .../hosts/unreal/plugins/load/load_camera.py | 167 ++++++++++++++---- 1 file changed, 128 insertions(+), 39 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index 09b2dc01bf..9ae6e6ec03 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -268,62 +268,151 @@ class CameraLoader(plugin.Loader): return asset_content def update(self, container, representation): - path = container["namespace"] - ar = unreal.AssetRegistryHelpers.get_asset_registry() - tools = unreal.AssetToolsHelpers().get_asset_tools() - 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 = EditorAssetLibrary.load_asset(a) - EditorAssetLibrary.set_metadata_tag( - loaded_asset, "representation", str(representation["_id"]) - ) - EditorAssetLibrary.set_metadata_tag( - loaded_asset, "parent", str(representation["parent"]) - ) - asset_name = EditorAssetLibrary.get_metadata_tag( - loaded_asset, "asset_name" - ) - elif asset.asset_class == "LevelSequence": - EditorAssetLibrary.delete_asset(a) + root = "/Game/OpenPype" - sequence = tools.create_asset( - asset_name=asset_name, - package_path=path, - asset_class=unreal.LevelSequence, - factory=unreal.LevelSequenceFactoryNew() + asset_dir = container.get('namespace') + + context = representation.get("context") + + hierarchy = context.get('hierarchy').split("/") + h_dir = f"{root}/{hierarchy[0]}" + h_asset = hierarchy[0] + master_level = f"{h_dir}/{h_asset}_map.{h_asset}_map" + + EditorLevelLibrary.save_current_level() + + filter = unreal.ARFilter( + class_names=["LevelSequence"], + package_paths=[asset_dir], + recursive_paths=False) + sequences = ar.get_assets(filter) + filter = unreal.ARFilter( + class_names=["World"], + package_paths=[str(Path(asset_dir).parent.as_posix())], + recursive_paths=True) + maps = ar.get_assets(filter) + + # There should be only one map in the list + EditorLevelLibrary.load_level(maps[0].get_full_name()) + + level_sequence = sequences[0].get_asset() + + display_rate = level_sequence.get_display_rate() + playback_start = level_sequence.get_playback_start() + playback_end = level_sequence.get_playback_end() + + sequence_name = f"{container.get('asset')}_camera" + + # Get the actors in the level sequence. + objs = unreal.SequencerTools.get_bound_objects( + unreal.EditorLevelLibrary.get_editor_world(), + level_sequence, + level_sequence.get_bindings(), + unreal.SequencerScriptingRange( + has_start_value=True, + has_end_value=True, + inclusive_start=level_sequence.get_playback_start(), + exclusive_end=level_sequence.get_playback_end() + ) ) - io_asset = legacy_io.Session["AVALON_ASSET"] - asset_doc = legacy_io.find_one({ - "type": "asset", - "name": io_asset - }) + # Delete actors from the map + for o in objs: + if o.bound_objects[0].get_class().get_name() == "CineCameraActor": + actor_path = o.bound_objects[0].get_path_name().split(":")[-1] + actor = EditorLevelLibrary.get_actor_reference(actor_path) + EditorLevelLibrary.destroy_actor(actor) - data = asset_doc.get("data") + # Remove the Level Sequence from the parent. + # We need to traverse the hierarchy from the master sequence to find + # the level sequence. + root = "/Game/OpenPype" + namespace = container.get('namespace').replace(f"{root}/", "") + ms_asset = namespace.split('/')[0] + filter = unreal.ARFilter( + class_names=["LevelSequence"], + package_paths=[f"{root}/{ms_asset}"], + recursive_paths=False) + sequences = ar.get_assets(filter) + master_sequence = sequences[0].get_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")) + sequences = [master_sequence] + + parent = None + sub_scene = None + for s in sequences: + tracks = s.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: + sections = subscene_track.get_sections() + for ss in sections: + if ss.get_sequence().get_name() == sequence_name: + parent = s + sub_scene = ss + # subscene_track.remove_section(ss) + break + sequences.append(ss.get_sequence()) + # Update subscenes indexes. + i = 0 + for ss in sections: + ss.set_row_index(i) + i += 1 + + if parent: + break + + assert parent, "Could not find the parent sequence" + + EditorAssetLibrary.delete_asset(level_sequence.get_path_name()) settings = unreal.MovieSceneUserImportFBXSettings() settings.set_editor_property('reduce_keys', False) + tools = unreal.AssetToolsHelpers().get_asset_tools() + new_sequence = tools.create_asset( + asset_name=sequence_name, + package_path=asset_dir, + asset_class=unreal.LevelSequence, + factory=unreal.LevelSequenceFactoryNew() + ) + + new_sequence.set_display_rate(display_rate) + new_sequence.set_playback_start(playback_start) + new_sequence.set_playback_end(playback_end) + + sub_scene.set_sequence(new_sequence) + unreal.SequencerTools.import_fbx( EditorLevelLibrary.get_editor_world(), - sequence, - sequence.get_bindings(), + new_sequence, + new_sequence.get_bindings(), settings, str(representation["data"]["path"]) ) + data = { + "representation": str(representation["_id"]), + "parent": str(representation["parent"]) + } + unreal_pipeline.imprint( + "{}/{}".format(asset_dir, container.get('container_name')), data) + + EditorLevelLibrary.save_current_level() + + asset_content = EditorAssetLibrary.list_assets( + asset_dir, recursive=True, include_folder=False) + + for a in asset_content: + EditorAssetLibrary.save_asset(a) + + EditorLevelLibrary.load_level(master_level) + def remove(self, container): path = Path(container.get("namespace")) parent_path = str(path.parent.as_posix()) From 2f7ca634a29947bc60e1bf925fae9d11836f2d4a Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 27 Apr 2022 12:27:11 +0100 Subject: [PATCH 105/398] Use anatomy for render path when collecting render instances --- .../plugins/publish/collect_render_instances.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/unreal/plugins/publish/collect_render_instances.py b/openpype/hosts/unreal/plugins/publish/collect_render_instances.py index 9d60b65d08..f6a63c7ba7 100644 --- a/openpype/hosts/unreal/plugins/publish/collect_render_instances.py +++ b/openpype/hosts/unreal/plugins/publish/collect_render_instances.py @@ -1,8 +1,11 @@ +import os from pathlib import Path + import unreal -import pyblish.api +from openpype.api import Anatomy from openpype.hosts.unreal.api import pipeline +import pyblish.api class CollectRenderInstances(pyblish.api.InstancePlugin): @@ -77,9 +80,14 @@ class CollectRenderInstances(pyblish.api.InstancePlugin): 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')}") + try: + project = os.environ.get("AVALON_PROJECT") + anatomy = Anatomy(project) + root = anatomy.roots['renders'] + except: + raise("Could not find render root in anatomy settings.") + + render_dir = f"{root}/{project}/{s.get('output')}" render_path = Path(render_dir) frames = [] From 6120efc591549b466f28ce4438a7686d60074a10 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 28 Apr 2022 10:36:42 +0100 Subject: [PATCH 106/398] Fix animation loading to find the correct skeleton --- .../unreal/plugins/load/load_animation.py | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/plugins/load/load_animation.py b/openpype/hosts/unreal/plugins/load/load_animation.py index dc1337ed5d..60c1526d3d 100644 --- a/openpype/hosts/unreal/plugins/load/load_animation.py +++ b/openpype/hosts/unreal/plugins/load/load_animation.py @@ -134,7 +134,6 @@ class AnimationFBXLoader(plugin.Loader): Returns: list(str): list of container content """ - # Create directory for asset and avalon container hierarchy = context.get('asset').get('data').get('parents') root = "/Game/OpenPype" @@ -149,11 +148,30 @@ class AnimationFBXLoader(plugin.Loader): asset_dir, container_name = tools.create_unique_asset_name( f"{root}/Animations/{asset}/{name}", suffix="") + ar = unreal.AssetRegistryHelpers.get_asset_registry() + + filter = unreal.ARFilter( + class_names=["World"], + package_paths=[f"{root}/{hierarchy[0]}"], + recursive_paths=False) + levels = ar.get_assets(filter) + master_level = levels[0].get_editor_property('object_path') + hierarchy_dir = root for h in hierarchy: hierarchy_dir = f"{hierarchy_dir}/{h}" hierarchy_dir = f"{hierarchy_dir}/{asset}" + filter = unreal.ARFilter( + class_names=["World"], + package_paths=[f"{hierarchy_dir}/"], + recursive_paths=True) + levels = ar.get_assets(filter) + level = levels[0].get_editor_property('object_path') + + unreal.EditorLevelLibrary.save_all_dirty_levels() + unreal.EditorLevelLibrary.load_level(level) + container_name += suffix EditorAssetLibrary.make_directory(asset_dir) @@ -224,6 +242,9 @@ class AnimationFBXLoader(plugin.Loader): for a in imported_content: EditorAssetLibrary.save_asset(a) + unreal.EditorLevelLibrary.save_current_level() + unreal.EditorLevelLibrary.load_level(master_level) + def update(self, container, representation): name = container["asset_name"] source_path = get_representation_path(representation) From af3c8d01fd60d3bec0e2ffdecfa062cd5f25b700 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 28 Apr 2022 10:59:34 +0100 Subject: [PATCH 107/398] Fix raise calls --- openpype/hosts/unreal/api/rendering.py | 2 +- openpype/hosts/unreal/plugins/load/load_camera.py | 4 ++-- .../hosts/unreal/plugins/publish/collect_render_instances.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/unreal/api/rendering.py b/openpype/hosts/unreal/api/rendering.py index dadfe77f45..6d78ba1e33 100644 --- a/openpype/hosts/unreal/api/rendering.py +++ b/openpype/hosts/unreal/api/rendering.py @@ -54,7 +54,7 @@ def start_rendering(): anatomy = Anatomy(project) root = anatomy.roots['renders'] except: - raise("Could not find render root in anatomy settings.") + raise Exception("Could not find render root in anatomy settings.") render_dir = f"{root}/{project}" diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index 9ae6e6ec03..b33e45b6e9 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -425,7 +425,7 @@ class CameraLoader(plugin.Loader): sequences = ar.get_assets(filter) if not sequences: - raise("Could not find sequence.") + raise Exception("Could not find sequence.") world = ar.get_asset_by_object_path( EditorLevelLibrary.get_editor_world().get_path_name()) @@ -438,7 +438,7 @@ class CameraLoader(plugin.Loader): # There should be only one map in the list if not maps: - raise("Could not find map.") + raise Exception("Could not find map.") map = maps[0] diff --git a/openpype/hosts/unreal/plugins/publish/collect_render_instances.py b/openpype/hosts/unreal/plugins/publish/collect_render_instances.py index f6a63c7ba7..c37d5a5c01 100644 --- a/openpype/hosts/unreal/plugins/publish/collect_render_instances.py +++ b/openpype/hosts/unreal/plugins/publish/collect_render_instances.py @@ -85,7 +85,7 @@ class CollectRenderInstances(pyblish.api.InstancePlugin): anatomy = Anatomy(project) root = anatomy.roots['renders'] except: - raise("Could not find render root in anatomy settings.") + raise Exception("Could not find render root in anatomy settings.") render_dir = f"{root}/{project}/{s.get('output')}" render_path = Path(render_dir) From 77bac5c735ec2aee0f101f0267dc72b8635ad4b2 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 28 Apr 2022 20:20:25 +0200 Subject: [PATCH 108/398] 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 109/398] 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 110/398] 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 111/398] 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 112/398] 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 113/398] 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 114/398] 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 115/398] 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 116/398] 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 117/398] 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 118/398] 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 523a52c92f273a0ec24c256a5f2afaf911c86fd2 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 29 Apr 2022 09:43:50 +0100 Subject: [PATCH 119/398] Hound fixes --- openpype/hosts/unreal/api/rendering.py | 2 +- openpype/hosts/unreal/plugins/load/load_layout.py | 3 ++- .../hosts/unreal/plugins/publish/collect_render_instances.py | 5 +++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/unreal/api/rendering.py b/openpype/hosts/unreal/api/rendering.py index 6d78ba1e33..b2732506fc 100644 --- a/openpype/hosts/unreal/api/rendering.py +++ b/openpype/hosts/unreal/api/rendering.py @@ -53,7 +53,7 @@ def start_rendering(): project = os.environ.get("AVALON_PROJECT") anatomy = Anatomy(project) root = anatomy.roots['renders'] - except: + except Exception: raise Exception("Could not find render root in anatomy settings.") render_dir = f"{root}/{project}" diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index 163dd1a8af..412f77e3a9 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -853,7 +853,8 @@ class LayoutLoader(plugin.Loader): if visibility_track: sections = visibility_track.get_sections() for ss in sections: - if unreal.Name(f"{container.get('asset')}_map") in ss.get_level_names(): + if (unreal.Name(f"{container.get('asset')}_map") + in ss.get_level_names()): visibility_track.remove_section(ss) # Update visibility sections indexes. i = -1 diff --git a/openpype/hosts/unreal/plugins/publish/collect_render_instances.py b/openpype/hosts/unreal/plugins/publish/collect_render_instances.py index c37d5a5c01..9fb45ea7a7 100644 --- a/openpype/hosts/unreal/plugins/publish/collect_render_instances.py +++ b/openpype/hosts/unreal/plugins/publish/collect_render_instances.py @@ -84,8 +84,9 @@ class CollectRenderInstances(pyblish.api.InstancePlugin): project = os.environ.get("AVALON_PROJECT") anatomy = Anatomy(project) root = anatomy.roots['renders'] - except: - raise Exception("Could not find render root in anatomy settings.") + except Exception: + raise Exception( + "Could not find render root in anatomy settings.") render_dir = f"{root}/{project}/{s.get('output')}" render_path = Path(render_dir) From 5f7524bb1cd542d49ccd60c538d78925596cf111 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 29 Apr 2022 10:46:12 +0200 Subject: [PATCH 120/398] 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 121/398] 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 10fd26e086a2b4f6ede4e1a7fbd901d7fdb34cf3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Apr 2022 14:11:47 +0200 Subject: [PATCH 122/398] moved enable review from creator into publish collector --- openpype/hosts/traypublisher/api/plugin.py | 29 ++++++----------- .../plugins/publish/collect_review_family.py | 31 +++++++++++++++++++ .../publish/collect_simple_instances.py | 9 +++--- .../project_settings/traypublisher.json | 1 - .../schema_project_traypublisher.json | 6 ---- 5 files changed, 46 insertions(+), 30 deletions(-) create mode 100644 openpype/hosts/traypublisher/plugins/publish/collect_review_family.py diff --git a/openpype/hosts/traypublisher/api/plugin.py b/openpype/hosts/traypublisher/api/plugin.py index 813641a7d2..603f34ee29 100644 --- a/openpype/hosts/traypublisher/api/plugin.py +++ b/openpype/hosts/traypublisher/api/plugin.py @@ -2,10 +2,7 @@ from openpype.pipeline import ( Creator, CreatedInstance ) -from openpype.lib import ( - FileDef, - BoolDef, -) +from openpype.lib import FileDef from .pipeline import ( list_instances, @@ -43,7 +40,6 @@ class TrayPublishCreator(Creator): class SettingsCreator(TrayPublishCreator): create_allow_context_change = True - enable_review = False extensions = [] def collect_instances(self): @@ -67,19 +63,15 @@ class SettingsCreator(TrayPublishCreator): self._add_instance_to_context(new_instance) def get_instance_attr_defs(self): - output = [] - - file_def = FileDef( - "filepath", - folders=False, - extensions=self.extensions, - allow_sequences=self.allow_sequences, - label="Filepath", - ) - output.append(file_def) - if self.enable_review: - output.append(BoolDef("review", label="Review")) - return output + return [ + FileDef( + "filepath", + folders=False, + extensions=self.extensions, + allow_sequences=self.allow_sequences, + label="Filepath", + ) + ] @classmethod def from_settings(cls, item_data): @@ -97,7 +89,6 @@ class SettingsCreator(TrayPublishCreator): "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"], "default_variants": item_data["default_variants"] diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_review_family.py b/openpype/hosts/traypublisher/plugins/publish/collect_review_family.py new file mode 100644 index 0000000000..965e251527 --- /dev/null +++ b/openpype/hosts/traypublisher/plugins/publish/collect_review_family.py @@ -0,0 +1,31 @@ +import pyblish.api +from openpype.lib import BoolDef +from openpype.pipeline import OpenPypePyblishPluginMixin + + +class CollectReviewFamily( + pyblish.api.InstancePlugin, OpenPypePyblishPluginMixin +): + """Add review family.""" + + label = "Collect Review Family" + order = pyblish.api.CollectorOrder - 0.49 + + hosts = ["traypublisher"] + families = [ + "image", + "render", + "plate", + "review" + ] + + def process(self, instance): + values = self.get_attr_values_from_data(instance.data) + if values.get("add_review_family"): + instance.data["families"].append("review") + + @classmethod + def get_attribute_defs(cls): + return [ + BoolDef("add_review_family", label="Review", default=True) + ] diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_simple_instances.py b/openpype/hosts/traypublisher/plugins/publish/collect_simple_instances.py index 5fc66084d6..9facd90a48 100644 --- a/openpype/hosts/traypublisher/plugins/publish/collect_simple_instances.py +++ b/openpype/hosts/traypublisher/plugins/publish/collect_simple_instances.py @@ -22,10 +22,6 @@ class CollectSettingsSimpleInstances(pyblish.api.InstancePlugin): 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 = [ @@ -34,6 +30,7 @@ class CollectSettingsSimpleInstances(pyblish.api.InstancePlugin): ] instance.data["sourceFilepaths"] = filepaths + instance.data["stagingDir"] = filepath_item["directory"] filenames = filepath_item["filenames"] ext = os.path.splitext(filenames[0])[-1] @@ -46,3 +43,7 @@ class CollectSettingsSimpleInstances(pyblish.api.InstancePlugin): "stagingDir": filepath_item["directory"], "files": filenames }) + + self.log.debug("Created Simple Settings instance {}".format( + instance.data + )) diff --git a/openpype/settings/defaults/project_settings/traypublisher.json b/openpype/settings/defaults/project_settings/traypublisher.json index 1b0ad67abb..0b54cfd39e 100644 --- a/openpype/settings/defaults/project_settings/traypublisher.json +++ b/openpype/settings/defaults/project_settings/traypublisher.json @@ -8,7 +8,6 @@ "default_variants": [ "Main" ], - "enable_review": false, "description": "Publish workfile backup", "detailed_description": "", "allow_sequences": true, 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 59c675d411..55c1b7b7d7 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json @@ -45,12 +45,6 @@ "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" }, From 1ea991f4f3d277e611587200bd93c007ae0fd660 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Apr 2022 14:30:33 +0200 Subject: [PATCH 123/398] added right click callback for menu --- .../widgets/attribute_defs/files_widget.py | 69 ++++++++++++++----- 1 file changed, 50 insertions(+), 19 deletions(-) diff --git a/openpype/widgets/attribute_defs/files_widget.py b/openpype/widgets/attribute_defs/files_widget.py index a3ee370bd3..5fe0302507 100644 --- a/openpype/widgets/attribute_defs/files_widget.py +++ b/openpype/widgets/attribute_defs/files_widget.py @@ -251,7 +251,7 @@ class FilesProxyModel(QtCore.QSortFilterProxyModel): class ItemWidget(QtWidgets.QWidget): - split_requested = QtCore.Signal(str) + context_menu_requested = QtCore.Signal(QtCore.QPoint) def __init__( self, item_id, label, pixmap_icon, is_sequence, multivalue, parent=None @@ -316,19 +316,9 @@ class ItemWidget(QtWidgets.QWidget): 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) + self.context_menu_requested.emit(point) class InViewButton(IconButton): @@ -339,6 +329,7 @@ class FilesView(QtWidgets.QListView): """View showing instances and their groups.""" remove_requested = QtCore.Signal() + context_menu_requested = QtCore.Signal(QtCore.QPoint) def __init__(self, *args, **kwargs): super(FilesView, self).__init__(*args, **kwargs) @@ -347,6 +338,7 @@ class FilesView(QtWidgets.QListView): self.setSelectionMode( QtWidgets.QAbstractItemView.ExtendedSelection ) + self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) remove_btn = InViewButton(self) pix_enabled = paint_image_with_color( @@ -361,6 +353,7 @@ class FilesView(QtWidgets.QListView): remove_btn.setEnabled(False) remove_btn.clicked.connect(self._on_remove_clicked) + self.customContextMenuRequested.connect(self._on_context_menu_request) self._remove_btn = remove_btn @@ -397,6 +390,12 @@ class FilesView(QtWidgets.QListView): selected_item_ids.add(instance_id) return selected_item_ids + def has_selected_sequence(self): + for index in self.selectionModel().selectedIndexes(): + if index.data(IS_SEQUENCE_ROLE): + return True + return False + def event(self, event): if event.type() == QtCore.QEvent.KeyPress: if ( @@ -408,6 +407,12 @@ class FilesView(QtWidgets.QListView): return super(FilesView, self).event(event) + def _on_context_menu_request(self, pos): + index = self.indexAt(pos) + if index.isValid(): + point = self.mapToGlobal(pos) + self.context_menu_requested.emit(point) + def _on_selection_change(self): self._remove_btn.setEnabled(self.has_selected_item_ids()) @@ -456,6 +461,9 @@ 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) + files_view.context_menu_requested.connect( + self._on_context_menu_requested + ) self._in_set_value = False self._single_item = single_item self._multivalue = False @@ -527,7 +535,9 @@ class FilesWidget(QtWidgets.QFrame): is_sequence, self._multivalue ) - widget.split_requested.connect(self._on_split_request) + widget.context_menu_requested.connect( + self._on_context_menu_requested + ) self._files_view.setIndexWidget(index, widget) self._files_proxy_model.setData( index, widget.sizeHint(), QtCore.Qt.SizeHintRole @@ -559,17 +569,22 @@ class FilesWidget(QtWidgets.QFrame): if not self._in_set_value: self.value_changed.emit() - def _on_split_request(self, item_id): + def _on_split_request(self): if self._multivalue: return - file_item = self._files_model.get_file_item_by_id(item_id) - if not file_item: + item_ids = self._files_view.get_selected_item_ids() + if not item_ids: return - new_items = file_item.split_sequence() - self._remove_item_by_ids([item_id]) - self._add_filepaths(new_items) + for item_id in item_ids: + file_item = self._files_model.get_file_item_by_id(item_id) + if not file_item: + return + + new_items = file_item.split_sequence() + self._add_filepaths(new_items) + self._remove_item_by_ids(item_ids) def _on_remove_requested(self): if self._multivalue: @@ -579,6 +594,22 @@ class FilesWidget(QtWidgets.QFrame): if items_to_delete: self._remove_item_by_ids(items_to_delete) + def _on_context_menu_requested(self, pos): + if self._multivalue: + return + + menu = QtWidgets.QMenu(self._files_view) + + remove_action = QtWidgets.QAction("Remove", menu) + remove_action.triggered.connect(self._on_remove_requested) + menu.addAction(remove_action) + + if self._files_view.has_selected_sequence(): + remove_action = QtWidgets.QAction("Split sequence", menu) + remove_action.triggered.connect(self._on_split_request) + + menu.popup(pos) + def sizeHint(self): # Get size hints of widget and visible widgets result = super(FilesWidget, self).sizeHint() From b74b309de19a2f7de6cb06fc6b6afa1ed4d5a624 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Apr 2022 14:34:12 +0200 Subject: [PATCH 124/398] fixed split sequence action --- openpype/widgets/attribute_defs/files_widget.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/openpype/widgets/attribute_defs/files_widget.py b/openpype/widgets/attribute_defs/files_widget.py index 5fe0302507..924dbf7fe5 100644 --- a/openpype/widgets/attribute_defs/files_widget.py +++ b/openpype/widgets/attribute_defs/files_widget.py @@ -410,7 +410,7 @@ class FilesView(QtWidgets.QListView): def _on_context_menu_request(self, pos): index = self.indexAt(pos) if index.isValid(): - point = self.mapToGlobal(pos) + point = self.viewport().mapToGlobal(pos) self.context_menu_requested.emit(point) def _on_selection_change(self): @@ -600,14 +600,15 @@ class FilesWidget(QtWidgets.QFrame): menu = QtWidgets.QMenu(self._files_view) + if self._files_view.has_selected_sequence(): + split_action = QtWidgets.QAction("Split sequence", menu) + split_action.triggered.connect(self._on_split_request) + menu.addAction(split_action) + remove_action = QtWidgets.QAction("Remove", menu) remove_action.triggered.connect(self._on_remove_requested) menu.addAction(remove_action) - if self._files_view.has_selected_sequence(): - remove_action = QtWidgets.QAction("Split sequence", menu) - remove_action.triggered.connect(self._on_split_request) - menu.popup(pos) def sizeHint(self): From 0647a5dcf294e7a7c88f691c7b5114c49927b5fc Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Apr 2022 14:44:25 +0200 Subject: [PATCH 125/398] moved create button to right side --- openpype/tools/publisher/widgets/create_dialog.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/openpype/tools/publisher/widgets/create_dialog.py b/openpype/tools/publisher/widgets/create_dialog.py index 243540f243..dd1e00b79b 100644 --- a/openpype/tools/publisher/widgets/create_dialog.py +++ b/openpype/tools/publisher/widgets/create_dialog.py @@ -282,9 +282,6 @@ class CreateDialog(QtWidgets.QDialog): subset_name_input = QtWidgets.QLineEdit(self) subset_name_input.setEnabled(False) - create_btn = QtWidgets.QPushButton("Create", self) - create_btn.setEnabled(False) - form_layout = QtWidgets.QFormLayout() form_layout.addRow("Variant:", variant_widget) form_layout.addRow("Subset:", subset_name_input) @@ -295,7 +292,6 @@ class CreateDialog(QtWidgets.QDialog): mid_layout.addWidget(QtWidgets.QLabel("Choose family:", self)) mid_layout.addWidget(creators_view, 1) mid_layout.addLayout(form_layout, 0) - mid_layout.addWidget(create_btn, 0) # ------------ # --- Creator short info and attr defs --- @@ -313,11 +309,22 @@ class CreateDialog(QtWidgets.QDialog): # Precreate attributes widget pre_create_widget = PreCreateWidget(creator_attrs_widget) + # Create button + create_btn_wrapper = QtWidgets.QWidget(creator_attrs_widget) + create_btn = QtWidgets.QPushButton("Create", create_btn_wrapper) + create_btn.setEnabled(False) + + create_btn_wrap_layout = QtWidgets.QHBoxLayout(create_btn_wrapper) + create_btn_wrap_layout.setContentsMargins(0, 0, 0, 0) + create_btn_wrap_layout.addStretch(1) + create_btn_wrap_layout.addWidget(create_btn, 0) + creator_attrs_layout = QtWidgets.QVBoxLayout(creator_attrs_widget) creator_attrs_layout.setContentsMargins(0, 0, 0, 0) creator_attrs_layout.addWidget(creator_short_desc_widget, 0) creator_attrs_layout.addWidget(separator_widget, 0) creator_attrs_layout.addWidget(pre_create_widget, 1) + creator_attrs_layout.addWidget(create_btn_wrapper, 0) # ------------------------------------- # --- Detailed information about creator --- From 7924affca56d4f68cc5b88ccc6af0509c169b5c7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Apr 2022 14:45:55 +0200 Subject: [PATCH 126/398] make FileDef with vertical label as default --- openpype/lib/attribute_definitions.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/openpype/lib/attribute_definitions.py b/openpype/lib/attribute_definitions.py index bfac9da5ce..4ea691ca08 100644 --- a/openpype/lib/attribute_definitions.py +++ b/openpype/lib/attribute_definitions.py @@ -560,11 +560,7 @@ class FileDef(AbtractAttrDef): # Change horizontal label is_label_horizontal = kwargs.get("is_label_horizontal") if is_label_horizontal is None: - if single_item: - is_label_horizontal = True - else: - is_label_horizontal = False - kwargs["is_label_horizontal"] = is_label_horizontal + kwargs["is_label_horizontal"] = False self.single_item = single_item self.folders = folders From 889f17d5ee3ee46dd702da1e6a989bf61556a6ab Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Apr 2022 15:22:24 +0200 Subject: [PATCH 127/398] wrapped header widgets into single widget --- openpype/tools/utils/assets_widget.py | 55 +++++++++++++++++++++++---- 1 file changed, 48 insertions(+), 7 deletions(-) diff --git a/openpype/tools/utils/assets_widget.py b/openpype/tools/utils/assets_widget.py index 3d4efcdd4d..90e688073d 100644 --- a/openpype/tools/utils/assets_widget.py +++ b/openpype/tools/utils/assets_widget.py @@ -589,10 +589,12 @@ class AssetsWidget(QtWidgets.QWidget): view = AssetsView(self) view.setModel(proxy) + header_widget = QtWidgets.QWidget(self) + current_asset_icon = qtawesome.icon( "fa.arrow-down", color=get_default_tools_icon_color() ) - current_asset_btn = QtWidgets.QPushButton(self) + current_asset_btn = QtWidgets.QPushButton(header_widget) current_asset_btn.setIcon(current_asset_icon) current_asset_btn.setToolTip("Go to Asset from current Session") # Hide by default @@ -601,15 +603,16 @@ class AssetsWidget(QtWidgets.QWidget): refresh_icon = qtawesome.icon( "fa.refresh", color=get_default_tools_icon_color() ) - refresh_btn = QtWidgets.QPushButton(self) + refresh_btn = QtWidgets.QPushButton(header_widget) refresh_btn.setIcon(refresh_icon) refresh_btn.setToolTip("Refresh items") - filter_input = PlaceholderLineEdit(self) + filter_input = PlaceholderLineEdit(header_widget) filter_input.setPlaceholderText("Filter assets..") # Header - header_layout = QtWidgets.QHBoxLayout() + header_layout = QtWidgets.QHBoxLayout(header_widget) + header_layout.setContentsMargins(0, 0, 0, 0) header_layout.addWidget(filter_input) header_layout.addWidget(current_asset_btn) header_layout.addWidget(refresh_btn) @@ -617,9 +620,8 @@ class AssetsWidget(QtWidgets.QWidget): # Layout layout = QtWidgets.QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(4) - layout.addLayout(header_layout) - layout.addWidget(view) + layout.addWidget(header_widget, 0) + layout.addWidget(view, 1) # Signals/Slots filter_input.textChanged.connect(self._on_filter_text_change) @@ -630,6 +632,8 @@ class AssetsWidget(QtWidgets.QWidget): current_asset_btn.clicked.connect(self._on_current_asset_click) view.doubleClicked.connect(self.double_clicked) + self._header_widget = header_widget + self._filter_input = filter_input self._refresh_btn = refresh_btn self._current_asset_btn = current_asset_btn self._model = model @@ -637,8 +641,40 @@ class AssetsWidget(QtWidgets.QWidget): self._view = view self._last_project_name = None + self._last_btns_height = None + self.model_selection = {} + @property + def header_widget(self): + return self._header_widget + + def _check_btns_height(self): + """Make buttons to have same height as filter input field. + + There is not handled case when buttons are bigger then filter input. + """ + if self._filter_input.height() == self._last_btns_height: + return + + height = self._filter_input.height() + self._last_btns_height = height + + for widget in ( + self._refresh_btn, + self._current_asset_btn + ): + widget.setMinimumHeight(height) + widget.setMaximumHeight(height) + + def resizeEvent(self, event): + super(AssetsWidget, self).resizeEvent(event) + self._check_btns_height() + + def showEvent(self, event): + super(AssetsWidget, self).showEvent(event) + self._check_btns_height() + def _create_source_model(self): model = AssetModel(dbcon=self.dbcon, parent=self) model.refreshed.connect(self._on_model_refresh) @@ -669,6 +705,7 @@ class AssetsWidget(QtWidgets.QWidget): This separation gives ability to override this method and use it in differnt way. """ + self.set_current_session_asset() def set_current_session_asset(self): @@ -681,6 +718,7 @@ class AssetsWidget(QtWidgets.QWidget): Some tools may have their global refresh button or do not support refresh at all. """ + if visible is None: visible = not self._refresh_btn.isVisible() self._refresh_btn.setVisible(visible) @@ -690,6 +728,7 @@ class AssetsWidget(QtWidgets.QWidget): Not all tools support using of current context asset. """ + if visible is None: visible = not self._current_asset_btn.isVisible() self._current_asset_btn.setVisible(visible) @@ -723,6 +762,7 @@ class AssetsWidget(QtWidgets.QWidget): so if you're modifying model keep in mind that this method should be called when refresh is done. """ + self._proxy.sort(0) self._set_loading_state(loading=False, empty=not has_item) self.refreshed.emit() @@ -767,6 +807,7 @@ class SingleSelectAssetsWidget(AssetsWidget): Contain single selection specific api methods. """ + def get_selected_asset_id(self): """Currently selected asset id.""" selection_model = self._view.selectionModel() From b27ba52aaf27772a18e6c948a108925906f14c13 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 29 Apr 2022 15:27:42 +0200 Subject: [PATCH 128/398] nuke: fix regex default --- 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 fedae994bf..09a5d98f46 100644 --- a/openpype/settings/defaults/project_anatomy/imageio.json +++ b/openpype/settings/defaults/project_anatomy/imageio.json @@ -170,7 +170,7 @@ "regexInputs": { "inputs": [ { - "regex": "[^-a-zA-Z0-9]beauty[^-a-zA-Z0-9]", + "regex": "(beauty).*(?=.exr)", "colorspace": "linear" } ] From 518c60de0b63746b20d80ebbd4467bc72655afbd Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Apr 2022 15:31:05 +0200 Subject: [PATCH 129/398] 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 130/398] 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) From b17a71fe1466c84d39b74228c066a0819a9b32c2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Apr 2022 15:43:06 +0200 Subject: [PATCH 131/398] simplified btns height in assets widget --- openpype/tools/utils/assets_widget.py | 36 ++++++++------------------- 1 file changed, 10 insertions(+), 26 deletions(-) diff --git a/openpype/tools/utils/assets_widget.py b/openpype/tools/utils/assets_widget.py index 90e688073d..d1df1193d2 100644 --- a/openpype/tools/utils/assets_widget.py +++ b/openpype/tools/utils/assets_widget.py @@ -617,6 +617,16 @@ class AssetsWidget(QtWidgets.QWidget): header_layout.addWidget(current_asset_btn) header_layout.addWidget(refresh_btn) + # Make header widgets expand vertically if there is a place + for widget in ( + current_asset_btn, + refresh_btn, + filter_input, + ): + size_policy = widget.sizePolicy() + size_policy.setVerticalPolicy(size_policy.MinimumExpanding) + widget.setSizePolicy(size_policy) + # Layout layout = QtWidgets.QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) @@ -649,32 +659,6 @@ class AssetsWidget(QtWidgets.QWidget): def header_widget(self): return self._header_widget - def _check_btns_height(self): - """Make buttons to have same height as filter input field. - - There is not handled case when buttons are bigger then filter input. - """ - if self._filter_input.height() == self._last_btns_height: - return - - height = self._filter_input.height() - self._last_btns_height = height - - for widget in ( - self._refresh_btn, - self._current_asset_btn - ): - widget.setMinimumHeight(height) - widget.setMaximumHeight(height) - - def resizeEvent(self, event): - super(AssetsWidget, self).resizeEvent(event) - self._check_btns_height() - - def showEvent(self, event): - super(AssetsWidget, self).showEvent(event) - self._check_btns_height() - def _create_source_model(self): model = AssetModel(dbcon=self.dbcon, parent=self) model.refreshed.connect(self._on_model_refresh) From 7504f273806b59c8192dcf4bf7137340aa04d0ba Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 29 Apr 2022 15:53:58 +0200 Subject: [PATCH 132/398] Nuke: fixing default settings for workfile builder loaders --- openpype/settings/defaults/project_settings/nuke.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index ddf996b5f2..0b03a00187 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -220,11 +220,12 @@ "repre_names": [ "exr", "dpx", - "mov" + "mov", + "mp4", + "h264" ], "loaders": [ - "LoadSequence", - "LoadMov" + "LoadClip" ] } ], From 1bc4d6c66a554b2902372546178ddce1220f8638 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Apr 2022 16:14:50 +0200 Subject: [PATCH 133/398] keep creators view label at same height as asset filtering --- .../tools/publisher/widgets/assets_widget.py | 24 +++++++++++++++++++ .../tools/publisher/widgets/create_dialog.py | 19 ++++++++++++++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/openpype/tools/publisher/widgets/assets_widget.py b/openpype/tools/publisher/widgets/assets_widget.py index 984da59c77..c4d3cf8b1a 100644 --- a/openpype/tools/publisher/widgets/assets_widget.py +++ b/openpype/tools/publisher/widgets/assets_widget.py @@ -15,6 +15,7 @@ from openpype.tools.utils.assets_widget import ( class CreateDialogAssetsWidget(SingleSelectAssetsWidget): current_context_required = QtCore.Signal() + header_height_changed = QtCore.Signal(int) def __init__(self, controller, parent): self._controller = controller @@ -27,6 +28,27 @@ class CreateDialogAssetsWidget(SingleSelectAssetsWidget): self._last_selection = None self._enabled = None + self._last_filter_height = None + + def _check_header_height(self): + """Catch header height changes. + + Label on top of creaters should have same height so Creators view has + same offset. + """ + height = self.header_widget.height() + if height != self._last_filter_height: + self._last_filter_height = height + self.header_height_changed.emit(height) + + def resizeEvent(self, event): + super(CreateDialogAssetsWidget, self).resizeEvent(event) + self._check_header_height() + + def showEvent(self, event): + super(CreateDialogAssetsWidget, self).showEvent(event) + self._check_header_height() + def _on_current_asset_click(self): self.current_context_required.emit() @@ -71,6 +93,7 @@ class AssetsHierarchyModel(QtGui.QStandardItemModel): Uses controller to load asset hierarchy. All asset documents are stored by their parents. """ + def __init__(self, controller): super(AssetsHierarchyModel, self).__init__() self._controller = controller @@ -143,6 +166,7 @@ class AssetsHierarchyModel(QtGui.QStandardItemModel): class AssetsDialog(QtWidgets.QDialog): """Dialog to select asset for a context of instance.""" + def __init__(self, controller, parent): super(AssetsDialog, self).__init__(parent) self.setWindowTitle("Select asset") diff --git a/openpype/tools/publisher/widgets/create_dialog.py b/openpype/tools/publisher/widgets/create_dialog.py index dd1e00b79b..00a9ac785d 100644 --- a/openpype/tools/publisher/widgets/create_dialog.py +++ b/openpype/tools/publisher/widgets/create_dialog.py @@ -255,6 +255,14 @@ class CreateDialog(QtWidgets.QDialog): context_layout.addWidget(tasks_widget, 1) # --- Creators view --- + creators_header_widget = QtWidgets.QWidget(self) + header_label_widget = QtWidgets.QLabel( + "Choose family:", creators_header_widget + ) + creators_header_layout = QtWidgets.QHBoxLayout(creators_header_widget) + creators_header_layout.setContentsMargins(0, 0, 0, 0) + creators_header_layout.addWidget(header_label_widget, 1) + creators_view = QtWidgets.QListView(self) creators_model = QtGui.QStandardItemModel() creators_view.setModel(creators_model) @@ -289,7 +297,7 @@ class CreateDialog(QtWidgets.QDialog): mid_widget = QtWidgets.QWidget(self) mid_layout = QtWidgets.QVBoxLayout(mid_widget) mid_layout.setContentsMargins(0, 0, 0, 0) - mid_layout.addWidget(QtWidgets.QLabel("Choose family:", self)) + mid_layout.addWidget(creators_header_widget, 0) mid_layout.addWidget(creators_view, 1) mid_layout.addLayout(form_layout, 0) # ------------ @@ -362,6 +370,10 @@ class CreateDialog(QtWidgets.QDialog): help_btn.clicked.connect(self._on_help_btn) help_btn.resized.connect(self._on_help_btn_resize) + assets_widget.header_height_changed.connect( + self._on_asset_filter_height_change + ) + create_btn.clicked.connect(self._on_create) variant_widget.resized.connect(self._on_variant_widget_resize) variant_input.returnPressed.connect(self._on_create) @@ -394,6 +406,7 @@ class CreateDialog(QtWidgets.QDialog): self.variant_hints_menu = variant_hints_menu self.variant_hints_group = variant_hints_group + self._creators_header_widget = creators_header_widget self.creators_model = creators_model self.creators_view = creators_view self.create_btn = create_btn @@ -472,6 +485,10 @@ class CreateDialog(QtWidgets.QDialog): def _invalidate_prereq(self): self._prereq_timer.start() + def _on_asset_filter_height_change(self, height): + self._creators_header_widget.setMinimumHeight(height) + self._creators_header_widget.setMaximumHeight(height) + def _on_prereq_timer(self): prereq_available = True creator_btn_tooltips = [] From 48e887f2398a70ee9797599118c934b9dbe523f7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Apr 2022 16:39:58 +0200 Subject: [PATCH 134/398] modified description heights --- .../tools/publisher/widgets/create_dialog.py | 78 +++++++++++++++---- 1 file changed, 65 insertions(+), 13 deletions(-) diff --git a/openpype/tools/publisher/widgets/create_dialog.py b/openpype/tools/publisher/widgets/create_dialog.py index 00a9ac785d..cc47e9f175 100644 --- a/openpype/tools/publisher/widgets/create_dialog.py +++ b/openpype/tools/publisher/widgets/create_dialog.py @@ -114,6 +114,8 @@ class CreateErrorMessageBox(ErrorMessageBox): # TODO add creator identifier/label to details class CreatorShortDescWidget(QtWidgets.QWidget): + height_changed = QtCore.Signal(int) + def __init__(self, parent=None): super(CreatorShortDescWidget, self).__init__(parent=parent) @@ -152,6 +154,22 @@ class CreatorShortDescWidget(QtWidgets.QWidget): self._family_label = family_label self._description_label = description_label + self._last_height = None + + def _check_height_change(self): + height = self.height() + if height != self._last_height: + self._last_height = height + self.height_changed.emit(height) + + def showEvent(self, event): + super(CreatorShortDescWidget, self).showEvent(event) + self._check_height_change() + + def resizeEvent(self, event): + super(CreatorShortDescWidget, self).resizeEvent(event) + self._check_height_change() + def set_plugin(self, plugin=None): if not plugin: self._icon_widget.set_icon_def(None) @@ -309,10 +327,10 @@ class CreateDialog(QtWidgets.QDialog): creator_attrs_widget ) - separator_widget = QtWidgets.QWidget(self) - separator_widget.setObjectName("Separator") - separator_widget.setMinimumHeight(2) - separator_widget.setMaximumHeight(2) + attr_separator_widget = QtWidgets.QWidget(self) + attr_separator_widget.setObjectName("Separator") + attr_separator_widget.setMinimumHeight(2) + attr_separator_widget.setMaximumHeight(2) # Precreate attributes widget pre_create_widget = PreCreateWidget(creator_attrs_widget) @@ -330,21 +348,41 @@ class CreateDialog(QtWidgets.QDialog): creator_attrs_layout = QtWidgets.QVBoxLayout(creator_attrs_widget) creator_attrs_layout.setContentsMargins(0, 0, 0, 0) creator_attrs_layout.addWidget(creator_short_desc_widget, 0) - creator_attrs_layout.addWidget(separator_widget, 0) + creator_attrs_layout.addWidget(attr_separator_widget, 0) creator_attrs_layout.addWidget(pre_create_widget, 1) creator_attrs_layout.addWidget(create_btn_wrapper, 0) # ------------------------------------- # --- Detailed information about creator --- # Detailed description of creator - detail_description_widget = QtWidgets.QTextEdit(self) - detail_description_widget.setObjectName("InfoText") - detail_description_widget.setTextInteractionFlags( + detail_description_widget = QtWidgets.QWidget(self) + + detail_placoholder_widget = QtWidgets.QWidget( + detail_description_widget + ) + detail_placoholder_widget.setAttribute( + QtCore.Qt.WA_TranslucentBackground + ) + + detail_description_input = QtWidgets.QTextEdit( + detail_description_widget + ) + detail_description_input.setObjectName("InfoText") + detail_description_input.setTextInteractionFlags( QtCore.Qt.TextBrowserInteraction ) - detail_description_widget.setVisible(False) - # ------------------------------------------- + detail_description_layout = QtWidgets.QVBoxLayout( + detail_description_widget + ) + detail_description_layout.setContentsMargins(0, 0, 0, 0) + detail_description_layout.setSpacing(0) + detail_description_layout.addWidget(detail_placoholder_widget, 0) + detail_description_layout.addWidget(detail_description_input, 1) + + detail_description_widget.setVisible(False) + + # ------------------------------------------- splitter_widget = QtWidgets.QSplitter(self) splitter_widget.addWidget(context_widget) splitter_widget.addWidget(mid_widget) @@ -359,6 +397,7 @@ class CreateDialog(QtWidgets.QDialog): layout.addWidget(splitter_widget, 1) # Floating help button + # - Create this button as last to be fully visible help_btn = HelpButton(self) prereq_timer = QtCore.QTimer() @@ -388,6 +427,9 @@ class CreateDialog(QtWidgets.QDialog): self._on_current_session_context_request ) tasks_widget.task_changed.connect(self._on_task_change) + creator_short_desc_widget.height_changed.connect( + self._on_description_height_change + ) controller.add_plugins_refresh_callback(self._on_plugins_refresh) @@ -413,7 +455,11 @@ class CreateDialog(QtWidgets.QDialog): self._creator_short_desc_widget = creator_short_desc_widget self._pre_create_widget = pre_create_widget + self._attr_separator_widget = attr_separator_widget + + self._detail_placoholder_widget = detail_placoholder_widget self._detail_description_widget = detail_description_widget + self._detail_description_input = detail_description_input self._help_btn = help_btn self._prereq_timer = prereq_timer @@ -619,6 +665,12 @@ class CreateDialog(QtWidgets.QDialog): if self._task_name: self._tasks_widget.select_task_name(self._task_name) + def _on_description_height_change(self): + # Use separator's 'y' position as height + height = self._attr_separator_widget.y() + self._detail_placoholder_widget.setMinimumHeight(height) + self._detail_placoholder_widget.setMaximumHeight(height) + def _on_creator_item_change(self, new_index, _old_index): identifier = None if new_index.isValid(): @@ -666,14 +718,14 @@ class CreateDialog(QtWidgets.QDialog): def _set_creator_detailed_text(self, creator): if not creator: - self._detail_description_widget.setPlainText("") + self._detail_description_input.setPlainText("") return detailed_description = creator.get_detail_description() or "" if commonmark: html = commonmark.commonmark(detailed_description) - self._detail_description_widget.setHtml(html) + self._detail_description_input.setHtml(html) else: - self._detail_description_widget.setMarkdown(detailed_description) + self._detail_description_input.setMarkdown(detailed_description) def _set_creator_by_identifier(self, identifier): creator = self.controller.manual_creators.get(identifier) From 542efb57bc5df0a6e966c3fa6ac265bce693a599 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Apr 2022 16:51:19 +0200 Subject: [PATCH 135/398] change style of detailed description --- openpype/style/style.css | 8 ++++++++ openpype/tools/publisher/widgets/create_dialog.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/openpype/style/style.css b/openpype/style/style.css index ae04a433fb..4032732a78 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -856,6 +856,14 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { } /* New Create/Publish UI */ +#CreatorDetailedDescription { + padding-left: 5px; + padding-right: 5px; + padding-top: 5px; + background: transparent; + border: 1px solid {color:border}; +} + #CreateDialogHelpButton { background: rgba(255, 255, 255, 31); border-top-right-radius: 0; diff --git a/openpype/tools/publisher/widgets/create_dialog.py b/openpype/tools/publisher/widgets/create_dialog.py index cc47e9f175..8e1ab3502d 100644 --- a/openpype/tools/publisher/widgets/create_dialog.py +++ b/openpype/tools/publisher/widgets/create_dialog.py @@ -367,7 +367,7 @@ class CreateDialog(QtWidgets.QDialog): detail_description_input = QtWidgets.QTextEdit( detail_description_widget ) - detail_description_input.setObjectName("InfoText") + detail_description_input.setObjectName("CreatorDetailedDescription") detail_description_input.setTextInteractionFlags( QtCore.Qt.TextBrowserInteraction ) From 99096269b52db11ad2bb5f9040a221cafbee97d7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Apr 2022 19:29:36 +0200 Subject: [PATCH 136/398] change separator size --- openpype/tools/publisher/widgets/create_dialog.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/tools/publisher/widgets/create_dialog.py b/openpype/tools/publisher/widgets/create_dialog.py index 8e1ab3502d..8c4f9d52a1 100644 --- a/openpype/tools/publisher/widgets/create_dialog.py +++ b/openpype/tools/publisher/widgets/create_dialog.py @@ -329,8 +329,8 @@ class CreateDialog(QtWidgets.QDialog): attr_separator_widget = QtWidgets.QWidget(self) attr_separator_widget.setObjectName("Separator") - attr_separator_widget.setMinimumHeight(2) - attr_separator_widget.setMaximumHeight(2) + attr_separator_widget.setMinimumHeight(1) + attr_separator_widget.setMaximumHeight(1) # Precreate attributes widget pre_create_widget = PreCreateWidget(creator_attrs_widget) From 833027f163c6a313db9ff26417b39ab3276fb600 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Apr 2022 19:29:58 +0200 Subject: [PATCH 137/398] animate description showing --- .../tools/publisher/widgets/create_dialog.py | 137 ++++++++++++++++-- 1 file changed, 128 insertions(+), 9 deletions(-) diff --git a/openpype/tools/publisher/widgets/create_dialog.py b/openpype/tools/publisher/widgets/create_dialog.py index 8c4f9d52a1..10be47c517 100644 --- a/openpype/tools/publisher/widgets/create_dialog.py +++ b/openpype/tools/publisher/widgets/create_dialog.py @@ -404,8 +404,13 @@ class CreateDialog(QtWidgets.QDialog): prereq_timer.setInterval(50) prereq_timer.setSingleShot(True) + desc_width_anim_timer = QtCore.QTimer() + desc_width_anim_timer.setInterval(10) + prereq_timer.timeout.connect(self._on_prereq_timer) + desc_width_anim_timer.timeout.connect(self._on_desc_animation) + help_btn.clicked.connect(self._on_help_btn) help_btn.resized.connect(self._on_help_btn_resize) @@ -465,6 +470,15 @@ class CreateDialog(QtWidgets.QDialog): self._prereq_timer = prereq_timer self._first_show = True + # Description animation + self._description_size_policy = detail_description_widget.sizePolicy() + self._desc_width_anim_timer = desc_width_anim_timer + self._desc_widget_step = 0 + self._last_description_width = None + self._last_full_width = 0 + self._expected_description_width = 0 + self._other_widgets_widths = [] + def _emit_message(self, message): self._overlay_object.add_message(message) @@ -688,34 +702,139 @@ class CreateDialog(QtWidgets.QDialog): self._update_help_btn() def _on_help_btn(self): + if self._desc_width_anim_timer.isActive(): + return + final_size = self.size() cur_sizes = self._splitter_widget.sizes() - spacing = self._splitter_widget.handleWidth() + + if self._desc_widget_step == 0: + now_visible = self._detail_description_widget.isVisible() + else: + now_visible = self._desc_widget_step > 0 sizes = [] for idx, value in enumerate(cur_sizes): if idx < 3: sizes.append(value) - now_visible = self._detail_description_widget.isVisible() + self._last_full_width = final_size.width() + self._other_widgets_widths = list(sizes) + if now_visible: - width = final_size.width() - ( - spacing + self._detail_description_widget.width() - ) + cur_desc_width = self._detail_description_widget.width() + if cur_desc_width < 1: + cur_desc_width = 2 + step_size = int(cur_desc_width / 5) + if step_size < 1: + step_size = 1 + + step_size *= -1 + expected_width = 0 + desc_width = cur_desc_width - 1 + width = final_size.width() - 1 + min_max = desc_width + self._last_description_width = cur_desc_width else: - last_size = self._detail_description_widget.sizeHint().width() - width = final_size.width() + spacing + last_size - sizes.append(last_size) + self._detail_description_widget.setVisible(True) + handle = self._splitter_widget.handle(3) + desc_width = handle.sizeHint().width() + if self._last_description_width: + expected_width = self._last_description_width + else: + hint = self._detail_description_widget.sizeHint() + expected_width = hint.width() + + width = final_size.width() + desc_width + step_size = int(expected_width / 5) + if step_size < 1: + step_size = 1 + min_max = 0 + + self._detail_description_widget.setMinimumWidth(min_max) + self._detail_description_widget.setMaximumWidth(min_max) + self._expected_description_width = expected_width + self._desc_widget_step = step_size + + self._desc_width_anim_timer.start() + + sizes.append(desc_width) final_size.setWidth(width) - self._detail_description_widget.setVisible(not now_visible) self._splitter_widget.setSizes(sizes) self.resize(final_size) self._help_btn.set_expanded(not now_visible) + def _on_desc_animation(self): + current_width = self._detail_description_widget.width() + + desc_width = None + last_step = False + growing = self._desc_widget_step > 0 + + # Growing + if growing: + if current_width < self._expected_description_width: + desc_width = current_width + self._desc_widget_step + if desc_width >= self._expected_description_width: + desc_width = self._expected_description_width + last_step = True + + # Decreasing + elif self._desc_widget_step < 0: + if current_width > self._expected_description_width: + desc_width = current_width + self._desc_widget_step + if desc_width <= self._expected_description_width: + desc_width = self._expected_description_width + last_step = True + + if desc_width is None: + self._desc_widget_step = 0 + self._desc_width_anim_timer.stop() + return + + if last_step and not growing: + self._detail_description_widget.setVisible(False) + QtWidgets.QApplication.processEvents() + + width = self._last_full_width + handle_width = self._splitter_widget.handle(3).width() + if growing: + width += (handle_width + desc_width) + else: + width -= self._last_description_width + if last_step: + width -= handle_width + else: + width += desc_width + + if not last_step or growing: + self._detail_description_widget.setMaximumWidth(desc_width) + self._detail_description_widget.setMinimumWidth(desc_width) + + window_size = self.size() + window_size.setWidth(width) + self.resize(window_size) + if not last_step: + return + + self._desc_widget_step = 0 + self._desc_width_anim_timer.stop() + + if not growing: + return + + self._detail_description_widget.setSizePolicy( + self._description_size_policy + ) + + sizes = list(self._other_widgets_widths) + sizes.append(desc_width) + self._splitter_widget.setSizes(sizes) + def _set_creator_detailed_text(self, creator): if not creator: self._detail_description_input.setPlainText("") From b53ed826b4aa120d133f8f795007389177cff511 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 2 May 2022 12:24:00 +0200 Subject: [PATCH 138/398] nuke: creator default knob to settings --- .../defaults/project_settings/nuke.json | 6 +- .../projects_schema/schema_project_nuke.json | 12 +- .../schemas/schema_nuke_knob_inputs.json | 151 ++++++++++++++++++ 3 files changed, 165 insertions(+), 4 deletions(-) create mode 100644 openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_knob_inputs.json diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index ddf996b5f2..36daa92485 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -21,7 +21,8 @@ "defaults": [ "Main", "Mask" - ] + ], + "knobs": [] }, "CreateWritePrerender": { "fpath_template": "{work}/prerenders/nuke/{subset}/{subset}.{frame}.{ext}", @@ -33,7 +34,8 @@ "Branch01", "Part01" ], - "reviewable": false + "reviewable": false, + "knobs": [] } }, "publish": { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json b/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json index 9ab5fc65fb..dfd3306b2e 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json @@ -87,7 +87,7 @@ "children": [ { "type": "dict", - "collapsible": false, + "collapsible": true, "key": "CreateWriteRender", "label": "CreateWriteRender", "is_group": true, @@ -104,12 +104,16 @@ "object_type": { "type": "text" } + }, + { + "type": "schema", + "name": "schema_nuke_knob_inputs" } ] }, { "type": "dict", - "collapsible": false, + "collapsible": true, "key": "CreateWritePrerender", "label": "CreateWritePrerender", "is_group": true, @@ -136,6 +140,10 @@ "type": "boolean", "key": "reviewable", "label": "Add reviewable toggle" + }, + { + "type": "schema", + "name": "schema_nuke_knob_inputs" } ] } diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_knob_inputs.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_knob_inputs.json new file mode 100644 index 0000000000..0d03b89288 --- /dev/null +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_knob_inputs.json @@ -0,0 +1,151 @@ +{ + "type": "collapsible-wrap", + "label": "Knob defaults", + "collapsible": true, + "collapsed": true, + "children": [{ + "type": "list", + "key": "knobs", + "object_type": { + "type": "dict-conditional", + "enum_key": "type", + "enum_label": "Type", + "enum_children": [ + { + "key": "string", + "label": "String", + "children": [ + { + "type": "text", + "key": "name", + "label": "Name" + }, + { + "type": "text", + "key": "value", + "label": "Value" + } + ] + }, + { + "key": "bool", + "label": "Boolean", + "children": [ + { + "type": "text", + "key": "name", + "label": "Name" + }, + { + "type": "boolean", + "key": "value", + "label": "Value" + } + ] + }, + { + "key": "number", + "label": "Number", + "children": [ + { + "type": "text", + "key": "name", + "label": "Name" + }, + { + "type": "number", + "key": "value", + "default": 1, + "decimal": 0 + } + + ] + }, + { + "key": "decimal_number", + "label": "Decimal number", + "children": [ + { + "type": "text", + "key": "name", + "label": "Name" + }, + { + "type": "number", + "key": "value", + "default": 1, + "decimal": 4 + } + + ] + }, + { + "key": "2d_vector", + "label": "2D vector", + "children": [ + { + "type": "text", + "key": "name", + "label": "Name" + }, + { + "type": "list-strict", + "key": "value", + "label": "Value", + "object_types": [ + { + "type": "number", + "key": "x", + "default": 1, + "decimal": 4 + }, + { + "type": "number", + "key": "y", + "default": 1, + "decimal": 4 + } + ] + } + ] + }, + { + "key": "3d_vector", + "label": "3D vector", + "children": [ + { + "type": "text", + "key": "name", + "label": "Name" + }, + { + "type": "list-strict", + "key": "value", + "label": "Value", + "object_types": [ + { + "type": "number", + "key": "x", + "default": 1, + "decimal": 4 + }, + { + "type": "number", + "key": "y", + "default": 1, + "decimal": 4 + }, + { + "type": "number", + "key": "y", + "default": 1, + "decimal": 4 + } + ] + } + ] + } + ] + } + }] +} From 8e4dc740e8dea2b87218754816b9501998284461 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 2 May 2022 12:24:20 +0200 Subject: [PATCH 139/398] nuke: adding default knobs to created node --- openpype/hosts/nuke/api/lib.py | 25 +++++++++++++++++++ openpype/hosts/nuke/api/plugin.py | 4 ++- .../plugins/create/create_write_render.py | 8 +++--- 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 3223feaec7..065fe9beb2 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -858,6 +858,7 @@ def create_write_node(name, data, input=None, prenodes=None, Return: node (obj): group node with avalon data as Knobs ''' + knob_overrides = data.get("knobs", []) imageio_writes = get_created_node_imageio_setting(**data) for knob in imageio_writes["knobs"]: @@ -1061,6 +1062,30 @@ def create_write_node(name, data, input=None, prenodes=None, tile_color = _data.get("tile_color", "0xff0000ff") GN["tile_color"].setValue(tile_color) + # overrie knob values from settings + for knob in knob_overrides: + knob_type = knob["type"] + knob_name = knob["name"] + knob_value = knob["value"] + if knob_name not in GN.knobs(): + continue + if not knob_value: + continue + + # set correctly knob types + if knob_type == "string": + knob_value = str(knob_value) + if knob_type == "number": + knob_value = int(knob_value) + if knob_type == "decimal_number": + knob_value = float(knob_value) + if knob_type == "bool": + knob_value = bool(knob_value) + if knob_type in ["2d_vector", "3d_vector"]: + knob_value = list(knob_value) + + GN[knob_name].setValue(knob_value) + return GN diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index fdb5930cb2..37c3633d2c 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -605,6 +605,7 @@ class AbstractWriteRender(OpenPypeCreator): family = "render" icon = "sign-out" defaults = ["Main", "Mask"] + knobs = [] def __init__(self, *args, **kwargs): super(AbstractWriteRender, self).__init__(*args, **kwargs) @@ -672,7 +673,8 @@ class AbstractWriteRender(OpenPypeCreator): "nodeclass": self.n_class, "families": [self.family], "avalon": self.data, - "subset": self.data["subset"] + "subset": self.data["subset"], + "knobs": self.knobs } # add creator data diff --git a/openpype/hosts/nuke/plugins/create/create_write_render.py b/openpype/hosts/nuke/plugins/create/create_write_render.py index 18a101546f..36a7b5c33f 100644 --- a/openpype/hosts/nuke/plugins/create/create_write_render.py +++ b/openpype/hosts/nuke/plugins/create/create_write_render.py @@ -13,6 +13,7 @@ class CreateWriteRender(plugin.AbstractWriteRender): family = "render" icon = "sign-out" defaults = ["Main", "Mask"] + knobs = [] def __init__(self, *args, **kwargs): super(CreateWriteRender, self).__init__(*args, **kwargs) @@ -38,13 +39,12 @@ class CreateWriteRender(plugin.AbstractWriteRender): } ] - write_node = create_write_node( + return create_write_node( self.data["subset"], write_data, input=selected_node, - prenodes=_prenodes) - - return write_node + prenodes=_prenodes + ) def _modify_write_node(self, write_node): return write_node From d0b3cf73c4c5e6967a3f2431626b6c02771eab66 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 2 May 2022 17:11:12 +0200 Subject: [PATCH 140/398] nuke: extracting method for knob types --- openpype/hosts/nuke/api/lib.py | 36 ++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 065fe9beb2..a002e02ea3 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -1062,31 +1062,43 @@ def create_write_node(name, data, input=None, prenodes=None, tile_color = _data.get("tile_color", "0xff0000ff") GN["tile_color"].setValue(tile_color) - # overrie knob values from settings - for knob in knob_overrides: + # finally add knob overrides + set_node_knobs_from_settings(GN, knob_overrides) + + return GN + + +def set_node_knobs_from_settings(node, knob_settings): + """ Overriding knob values from settings + + Using `schema_nuke_knob_inputs` for knob type definitions. + + Args: + node (nuke.Node): nuke node + knob_settings (list): list of dict. Keys are `type`, `name`, `value` + """ + for knob in knob_settings: knob_type = knob["type"] knob_name = knob["name"] knob_value = knob["value"] - if knob_name not in GN.knobs(): + if knob_name not in node.knobs(): continue if not knob_value: continue # set correctly knob types - if knob_type == "string": - knob_value = str(knob_value) - if knob_type == "number": - knob_value = int(knob_value) - if knob_type == "decimal_number": - knob_value = float(knob_value) if knob_type == "bool": knob_value = bool(knob_value) + elif knob_type == "decimal_number": + knob_value = float(knob_value) + elif knob_type == "number": + knob_value = int(knob_value) + elif knob_type == "string": + knob_value = str(knob_value) if knob_type in ["2d_vector", "3d_vector"]: knob_value = list(knob_value) - GN[knob_name].setValue(knob_value) - - return GN + node[knob_name].setValue(knob_value) def add_rendering_knobs(node, farm=True): From 4b7f17c77eca959fe4337e148311f6dbd878c136 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 2 May 2022 18:01:31 +0200 Subject: [PATCH 141/398] nuke: adding hex and color types to nuke knob schema --- .../schemas/schema_nuke_knob_inputs.json | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_knob_inputs.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_knob_inputs.json index 0d03b89288..03eea039d6 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_knob_inputs.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_knob_inputs.json @@ -27,6 +27,22 @@ } ] }, + { + "key": "hex", + "label": "Hexadecimal (0x)", + "children": [ + { + "type": "text", + "key": "name", + "label": "Name" + }, + { + "type": "text", + "key": "value", + "label": "Value" + } + ] + }, { "key": "bool", "label": "Boolean", @@ -144,6 +160,48 @@ ] } ] + }, + { + "key": "color", + "label": "Color", + "children": [ + { + "type": "text", + "key": "name", + "label": "Name" + }, + { + "type": "list-strict", + "key": "value", + "label": "Value", + "object_types": [ + { + "type": "number", + "key": "x", + "default": 1, + "decimal": 4 + }, + { + "type": "number", + "key": "x", + "default": 1, + "decimal": 4 + }, + { + "type": "number", + "key": "y", + "default": 1, + "decimal": 4 + }, + { + "type": "number", + "key": "y", + "default": 1, + "decimal": 4 + } + ] + } + ] } ] } From a72aaecc0d1a640416e321ba5d4fe51a03b7fe93 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 2 May 2022 18:02:03 +0200 Subject: [PATCH 142/398] nuke: solving hex and color knob types --- openpype/hosts/nuke/api/lib.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index a002e02ea3..82c92f06f9 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -1,4 +1,5 @@ import os +from pprint import pformat import re import six import platform @@ -1086,6 +1087,11 @@ def set_node_knobs_from_settings(node, knob_settings): if not knob_value: continue + # first convert string types to string + # just to ditch unicode + if isinstance(knob_value, six.text_type): + knob_value = str(knob_value) + # set correctly knob types if knob_type == "bool": knob_value = bool(knob_value) @@ -1094,9 +1100,16 @@ def set_node_knobs_from_settings(node, knob_settings): elif knob_type == "number": knob_value = int(knob_value) elif knob_type == "string": - knob_value = str(knob_value) - if knob_type in ["2d_vector", "3d_vector"]: - knob_value = list(knob_value) + knob_value = knob_value + elif knob_type == "hex": + if not knob_value.startswith("0x"): + raise ValueError( + "Check your settings! Input Hexa is wrong! \n{}".format( + pformat(knob_settings) + )) + knob_value = int(knob_value, 16) + if knob_type in ["2d_vector", "3d_vector", "color"]: + knob_value = [float(v) for v in knob_value] node[knob_name].setValue(knob_value) From e6326570f8770251658b531ff3b3bd64c37a99f2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 2 May 2022 18:35:58 +0200 Subject: [PATCH 143/398] help button is resized and has more content --- openpype/style/style.css | 7 +- .../tools/publisher/widgets/create_dialog.py | 115 +++++++++++++++--- 2 files changed, 101 insertions(+), 21 deletions(-) diff --git a/openpype/style/style.css b/openpype/style/style.css index 4032732a78..8eeae1a58a 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -866,16 +866,21 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { #CreateDialogHelpButton { background: rgba(255, 255, 255, 31); + border-top-left-radius: 0.2em; + border-bottom-left-radius: 0.2em; border-top-right-radius: 0; border-bottom-right-radius: 0; font-size: 10pt; font-weight: bold; - padding: 3px 3px 3px 3px; + padding: 0px; } #CreateDialogHelpButton:hover { background: rgba(255, 255, 255, 63); } +#CreateDialogHelpButton QWidget { + background: transparent; +} #PublishLogConsole { font-family: "Noto Sans Mono"; diff --git a/openpype/tools/publisher/widgets/create_dialog.py b/openpype/tools/publisher/widgets/create_dialog.py index 10be47c517..8b2710b165 100644 --- a/openpype/tools/publisher/widgets/create_dialog.py +++ b/openpype/tools/publisher/widgets/create_dialog.py @@ -3,6 +3,7 @@ import re import traceback import copy +import qtawesome try: import commonmark except Exception: @@ -15,7 +16,8 @@ from openpype.pipeline.create import ( ) from openpype.tools.utils import ( ErrorMessageBox, - MessageOverlayObject + MessageOverlayObject, + ClickableFrame, ) from .widgets import IconValuePixmapLabel @@ -186,13 +188,43 @@ class CreatorShortDescWidget(QtWidgets.QWidget): self._description_label.setText(description) -class HelpButton(QtWidgets.QPushButton): - resized = QtCore.Signal() +class HelpButton(ClickableFrame): + resized = QtCore.Signal(int) + question_mark_icon_name = "fa.question" + help_icon_name = "fa.question-circle" + hide_icon_name = "fa.angle-left" def __init__(self, *args, **kwargs): super(HelpButton, self).__init__(*args, **kwargs) self.setObjectName("CreateDialogHelpButton") + question_mark_label = QtWidgets.QLabel(self) + help_widget = QtWidgets.QWidget(self) + + help_question = QtWidgets.QLabel(help_widget) + help_label = QtWidgets.QLabel("Help", help_widget) + hide_icon = QtWidgets.QLabel(help_widget) + + help_layout = QtWidgets.QHBoxLayout(help_widget) + help_layout.setContentsMargins(0, 0, 5, 0) + help_layout.addWidget(help_question, 0) + help_layout.addWidget(help_label, 0) + help_layout.addStretch(1) + help_layout.addWidget(hide_icon, 0) + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + layout.addWidget(question_mark_label, 0) + layout.addWidget(help_widget, 1) + + help_widget.setVisible(False) + + self._question_mark_label = question_mark_label + self._help_widget = help_widget + self._help_question = help_question + self._hide_icon = hide_icon + self._expanded = None self.set_expanded() @@ -202,27 +234,52 @@ class HelpButton(QtWidgets.QPushButton): return expanded = False self._expanded = expanded - if expanded: - text = "<" + self._help_widget.setVisible(expanded) + self._update_content() + + def _update_content(self): + width = self.get_icon_width() + if self._expanded: + question_mark_pix = QtGui.QPixmap(width, width) + question_mark_pix.fill(QtCore.Qt.transparent) + else: - text = "?" - self.setText(text) + question_mark_icon = qtawesome.icon( + self.question_mark_icon_name, color=QtCore.Qt.white + ) + question_mark_pix = question_mark_icon.pixmap(width, width) - self._update_size() + hide_icon = qtawesome.icon( + self.hide_icon_name, color=QtCore.Qt.white + ) + help_question_icon = qtawesome.icon( + self.help_icon_name, color=QtCore.Qt.white + ) + self._question_mark_label.setPixmap(question_mark_pix) + self._question_mark_label.setMaximumWidth(width) + self._hide_icon.setPixmap(hide_icon.pixmap(width, width)) + self._help_question.setPixmap(help_question_icon.pixmap(width, width)) - def _update_size(self): - new_size = self.minimumSizeHint() - if self.size() != new_size: - self.resize(new_size) - self.resized.emit() + def get_icon_width(self): + metrics = self.fontMetrics() + return metrics.height() + + def set_pos_and_size(self, pos_x, pos_y, width, height): + update_icon = self.height() != height + self.move(pos_x, pos_y) + self.resize(width, height) + + if update_icon: + self._update_content() + self.updateGeometry() def showEvent(self, event): super(HelpButton, self).showEvent(event) - self._update_size() + self.resized.emit(self.height()) def resizeEvent(self, event): super(HelpButton, self).resizeEvent(event) - self._update_size() + self.resized.emit(self.height()) class CreateDialog(QtWidgets.QDialog): @@ -692,14 +749,32 @@ class CreateDialog(QtWidgets.QDialog): self._set_creator_by_identifier(identifier) def _update_help_btn(self): - pos_x = self.width() - self._help_btn.width() - point = self._creator_short_desc_widget.rect().topRight() + short_desc_rect = self._creator_short_desc_widget.rect() + height = short_desc_rect.height() + + point = short_desc_rect.topRight() mapped_point = self._creator_short_desc_widget.mapTo(self, point) pos_y = mapped_point.y() - self._help_btn.move(max(0, pos_x), max(0, pos_y)) - def _on_help_btn_resize(self): - self._update_help_btn() + icon_width = self._help_btn.get_icon_width() + + pos_x = self.width() - icon_width + if self._detail_placoholder_widget.isVisible(): + pos_x -= ( + self._detail_placoholder_widget.width() + + self._splitter_widget.handle(3).width() + ) + + width = self.width() - pos_x + + self._help_btn.set_pos_and_size( + max(0, pos_x), max(0, pos_y), + width, height + ) + + def _on_help_btn_resize(self, height): + if self._creator_short_desc_widget.height() != height: + self._update_help_btn() def _on_help_btn(self): if self._desc_width_anim_timer.isActive(): From b7f859c0b00c41cb511f256160b4d17d7164e616 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 2 May 2022 19:27:27 +0200 Subject: [PATCH 144/398] fixed description min/max sizes --- .../tools/publisher/widgets/create_dialog.py | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/openpype/tools/publisher/widgets/create_dialog.py b/openpype/tools/publisher/widgets/create_dialog.py index 8b2710b165..bcf87dea6a 100644 --- a/openpype/tools/publisher/widgets/create_dialog.py +++ b/openpype/tools/publisher/widgets/create_dialog.py @@ -492,6 +492,7 @@ class CreateDialog(QtWidgets.QDialog): creator_short_desc_widget.height_changed.connect( self._on_description_height_change ) + splitter_widget.splitterMoved.connect(self._on_splitter_move) controller.add_plugins_refresh_callback(self._on_plugins_refresh) @@ -534,6 +535,7 @@ class CreateDialog(QtWidgets.QDialog): self._last_description_width = None self._last_full_width = 0 self._expected_description_width = 0 + self._last_desc_max_width = None self._other_widgets_widths = [] def _emit_message(self, message): @@ -750,14 +752,18 @@ class CreateDialog(QtWidgets.QDialog): def _update_help_btn(self): short_desc_rect = self._creator_short_desc_widget.rect() - height = short_desc_rect.height() - point = short_desc_rect.topRight() + # point = short_desc_rect.topRight() + point = short_desc_rect.center() mapped_point = self._creator_short_desc_widget.mapTo(self, point) - pos_y = mapped_point.y() - + # pos_y = mapped_point.y() + center_pos_y = mapped_point.y() icon_width = self._help_btn.get_icon_width() + _height = int(icon_width * 2.5) + height = min(_height, short_desc_rect.height()) + pos_y = center_pos_y - int(height / 2) + pos_x = self.width() - icon_width if self._detail_placoholder_widget.isVisible(): pos_x -= ( @@ -776,6 +782,9 @@ class CreateDialog(QtWidgets.QDialog): if self._creator_short_desc_widget.height() != height: self._update_help_btn() + def _on_splitter_move(self, *args): + self._update_help_btn() + def _on_help_btn(self): if self._desc_width_anim_timer.isActive(): return @@ -827,6 +836,10 @@ class CreateDialog(QtWidgets.QDialog): step_size = 1 min_max = 0 + if self._last_desc_max_width is None: + self._last_desc_max_width = ( + self._detail_description_widget.maximumWidth() + ) self._detail_description_widget.setMinimumWidth(min_max) self._detail_description_widget.setMaximumWidth(min_max) self._expected_description_width = expected_width @@ -902,6 +915,10 @@ class CreateDialog(QtWidgets.QDialog): if not growing: return + self._detail_description_widget.setMinimumWidth(0) + self._detail_description_widget.setMaximumWidth( + self._last_desc_max_width + ) self._detail_description_widget.setSizePolicy( self._description_size_policy ) From c608eeb2623bf97ccf65d876404d69c7e8b9988d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 2 May 2022 21:35:09 +0200 Subject: [PATCH 145/398] Remove remaining imports from avalon --- openpype/hosts/blender/api/plugin.py | 2 +- openpype/hosts/fusion/api/pipeline.py | 3 ++- openpype/hosts/harmony/api/lib.py | 2 +- openpype/hosts/hiero/api/lib.py | 11 +++++------ openpype/hosts/houdini/plugins/load/show_usdview.py | 11 +++++------ openpype/hosts/maya/plugins/load/_load_animation.py | 2 +- openpype/hosts/nuke/api/lib.py | 2 +- openpype/widgets/project_settings.py | 2 +- tests/lib/testing_classes.py | 2 +- 9 files changed, 18 insertions(+), 19 deletions(-) diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index 3207f543b7..c59be8d7ff 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -266,7 +266,7 @@ class AssetLoader(LoaderPlugin): # Only containerise if it's not already a collection from a .blend file. # representation = context["representation"]["name"] # if representation != "blend": - # from avalon.blender.pipeline import containerise + # from openpype.hosts.blender.api.pipeline import containerise # return containerise( # name=name, # namespace=namespace, diff --git a/openpype/hosts/fusion/api/pipeline.py b/openpype/hosts/fusion/api/pipeline.py index 0867b464d5..54002f9f51 100644 --- a/openpype/hosts/fusion/api/pipeline.py +++ b/openpype/hosts/fusion/api/pipeline.py @@ -45,7 +45,8 @@ def install(): This is where you install menus and register families, data and loaders into fusion. - It is called automatically when installing via `api.install(avalon.fusion)` + It is called automatically when installing via + `openpype.pipeline.install_host(openpype.hosts.fusion.api)` See the Maya equivalent for inspiration on how to implement this. diff --git a/openpype/hosts/harmony/api/lib.py b/openpype/hosts/harmony/api/lib.py index 53fd0f07dd..e5e7ad1b7e 100644 --- a/openpype/hosts/harmony/api/lib.py +++ b/openpype/hosts/harmony/api/lib.py @@ -463,7 +463,7 @@ def imprint(node_id, data, remove=False): remove (bool): Removes the data from the scene. Example: - >>> from avalon.harmony import lib + >>> from openpype.hosts.harmony.api import lib >>> node = "Top/Display" >>> data = {"str": "someting", "int": 1, "float": 0.32, "bool": True} >>> lib.imprint(layer, data) diff --git a/openpype/hosts/hiero/api/lib.py b/openpype/hosts/hiero/api/lib.py index 0e64ddcaf5..2a4cd03b76 100644 --- a/openpype/hosts/hiero/api/lib.py +++ b/openpype/hosts/hiero/api/lib.py @@ -553,10 +553,10 @@ class PublishAction(QtWidgets.QAction): # # ''' # import hiero.core -# from avalon.nuke import imprint -# from pype.hosts.nuke import ( -# lib as nklib -# ) +# from openpype.hosts.nuke.api.lib import ( +# BuildWorkfile, +# imprint +# ) # # # check if the file exists if does then Raise "File exists!" # if os.path.exists(filepath): @@ -583,8 +583,7 @@ class PublishAction(QtWidgets.QAction): # # nuke_script.addNode(root_node) # -# # here to call pype.hosts.nuke.lib.BuildWorkfile -# script_builder = nklib.BuildWorkfile( +# script_builder = BuildWorkfile( # root_node=root_node, # root_path=root_path, # nodes=nuke_script.getNodes(), diff --git a/openpype/hosts/houdini/plugins/load/show_usdview.py b/openpype/hosts/houdini/plugins/load/show_usdview.py index 8066615181..2737bc40fa 100644 --- a/openpype/hosts/houdini/plugins/load/show_usdview.py +++ b/openpype/hosts/houdini/plugins/load/show_usdview.py @@ -1,3 +1,7 @@ +import os +import subprocess + +from openpype.lib.vendor_bin_utils import find_executable from openpype.pipeline import load @@ -14,12 +18,7 @@ class ShowInUsdview(load.LoaderPlugin): def load(self, context, name=None, namespace=None, data=None): - import os - import subprocess - - import avalon.lib as lib - - usdview = lib.which("usdview") + usdview = find_executable("usdview") filepath = os.path.normpath(self.fname) filepath = filepath.replace("\\", "/") diff --git a/openpype/hosts/maya/plugins/load/_load_animation.py b/openpype/hosts/maya/plugins/load/_load_animation.py index bce1f0fc67..9c37e498ef 100644 --- a/openpype/hosts/maya/plugins/load/_load_animation.py +++ b/openpype/hosts/maya/plugins/load/_load_animation.py @@ -2,7 +2,7 @@ import openpype.hosts.maya.api.plugin class AbcLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): - """Specific loader of Alembic for the avalon.animation family""" + """Loader to reference an Alembic file""" families = ["animation", "camera", diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 3223feaec7..f0af20289c 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -193,7 +193,7 @@ def imprint(node, data, tab=None): Examples: ``` import nuke - from avalon.nuke import lib + from openpype.hosts.nuke.api import lib node = nuke.createNode("NoOp") data = { diff --git a/openpype/widgets/project_settings.py b/openpype/widgets/project_settings.py index 43ff9f2789..687e17b3bf 100644 --- a/openpype/widgets/project_settings.py +++ b/openpype/widgets/project_settings.py @@ -4,7 +4,7 @@ import platform from Qt import QtCore, QtGui, QtWidgets -from avalon import style +from openpype import style import ftrack_api diff --git a/tests/lib/testing_classes.py b/tests/lib/testing_classes.py index 7dfbf6fd0d..f991f02227 100644 --- a/tests/lib/testing_classes.py +++ b/tests/lib/testing_classes.py @@ -153,7 +153,7 @@ class ModuleUnitTest(BaseTest): Database prepared from dumps with 'db_setup' fixture. """ - from avalon.api import AvalonMongoDB + from openpype.pipeline import AvalonMongoDB dbcon = AvalonMongoDB() dbcon.Session["AVALON_PROJECT"] = self.TEST_PROJECT_NAME yield dbcon From 9e2f7f328b5746aa9c2b617520c85e672233dcef Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 2 May 2022 21:43:01 +0200 Subject: [PATCH 146/398] Cleanup some Loader docstrings --- openpype/hosts/fusion/plugins/load/actions.py | 4 ++-- openpype/hosts/houdini/plugins/load/actions.py | 4 ++-- openpype/hosts/houdini/plugins/load/load_alembic.py | 2 +- openpype/hosts/houdini/plugins/load/load_camera.py | 2 +- openpype/hosts/houdini/plugins/load/load_image.py | 4 ++-- openpype/hosts/houdini/plugins/load/load_vdb.py | 2 +- openpype/hosts/maya/plugins/load/_load_animation.py | 2 +- openpype/hosts/maya/plugins/load/actions.py | 4 ++-- openpype/hosts/maya/plugins/load/load_ass.py | 2 +- openpype/hosts/maya/plugins/load/load_gpucache.py | 2 +- openpype/hosts/maya/plugins/load/load_reference.py | 2 +- openpype/hosts/maya/plugins/load/load_vdb_to_vray.py | 1 + openpype/hosts/nuke/plugins/load/actions.py | 4 ++-- 13 files changed, 18 insertions(+), 17 deletions(-) diff --git a/openpype/hosts/fusion/plugins/load/actions.py b/openpype/hosts/fusion/plugins/load/actions.py index bc59cec77f..819c9272fd 100644 --- a/openpype/hosts/fusion/plugins/load/actions.py +++ b/openpype/hosts/fusion/plugins/load/actions.py @@ -6,7 +6,7 @@ from openpype.pipeline import load class FusionSetFrameRangeLoader(load.LoaderPlugin): - """Specific loader of Alembic for the avalon.animation family""" + """Set frame range excluding pre- and post-handles""" families = ["animation", "camera", @@ -40,7 +40,7 @@ class FusionSetFrameRangeLoader(load.LoaderPlugin): class FusionSetFrameRangeWithHandlesLoader(load.LoaderPlugin): - """Specific loader of Alembic for the avalon.animation family""" + """Set frame range including pre- and post-handles""" families = ["animation", "camera", diff --git a/openpype/hosts/houdini/plugins/load/actions.py b/openpype/hosts/houdini/plugins/load/actions.py index 63d74c39a5..637be1513d 100644 --- a/openpype/hosts/houdini/plugins/load/actions.py +++ b/openpype/hosts/houdini/plugins/load/actions.py @@ -6,7 +6,7 @@ from openpype.pipeline import load class SetFrameRangeLoader(load.LoaderPlugin): - """Set Houdini frame range""" + """Set frame range excluding pre- and post-handles""" families = [ "animation", @@ -44,7 +44,7 @@ class SetFrameRangeLoader(load.LoaderPlugin): class SetFrameRangeWithHandlesLoader(load.LoaderPlugin): - """Set Maya frame range including pre- and post-handles""" + """Set frame range including pre- and post-handles""" families = [ "animation", diff --git a/openpype/hosts/houdini/plugins/load/load_alembic.py b/openpype/hosts/houdini/plugins/load/load_alembic.py index 0214229d5a..96e666b255 100644 --- a/openpype/hosts/houdini/plugins/load/load_alembic.py +++ b/openpype/hosts/houdini/plugins/load/load_alembic.py @@ -7,7 +7,7 @@ from openpype.hosts.houdini.api import pipeline class AbcLoader(load.LoaderPlugin): - """Specific loader of Alembic for the avalon.animation family""" + """Load Alembic""" families = ["model", "animation", "pointcache", "gpuCache"] label = "Load Alembic" diff --git a/openpype/hosts/houdini/plugins/load/load_camera.py b/openpype/hosts/houdini/plugins/load/load_camera.py index ef57d115da..059ad11a76 100644 --- a/openpype/hosts/houdini/plugins/load/load_camera.py +++ b/openpype/hosts/houdini/plugins/load/load_camera.py @@ -78,7 +78,7 @@ def transfer_non_default_values(src, dest, ignore=None): class CameraLoader(load.LoaderPlugin): - """Specific loader of Alembic for the avalon.animation family""" + """Load camera from an Alembic file""" families = ["camera"] label = "Load Camera (abc)" diff --git a/openpype/hosts/houdini/plugins/load/load_image.py b/openpype/hosts/houdini/plugins/load/load_image.py index 671f08f18f..928c2ee734 100644 --- a/openpype/hosts/houdini/plugins/load/load_image.py +++ b/openpype/hosts/houdini/plugins/load/load_image.py @@ -42,9 +42,9 @@ def get_image_avalon_container(): class ImageLoader(load.LoaderPlugin): - """Specific loader of Alembic for the avalon.animation family""" + """Load images into COP2""" - families = ["colorbleed.imagesequence"] + families = ["imagesequence"] label = "Load Image (COP2)" representations = ["*"] order = -10 diff --git a/openpype/hosts/houdini/plugins/load/load_vdb.py b/openpype/hosts/houdini/plugins/load/load_vdb.py index 06bb9e45e4..bff0f8b0bf 100644 --- a/openpype/hosts/houdini/plugins/load/load_vdb.py +++ b/openpype/hosts/houdini/plugins/load/load_vdb.py @@ -9,7 +9,7 @@ from openpype.hosts.houdini.api import pipeline class VdbLoader(load.LoaderPlugin): - """Specific loader of Alembic for the avalon.animation family""" + """Load VDB""" families = ["vdbcache"] label = "Load VDB" diff --git a/openpype/hosts/maya/plugins/load/_load_animation.py b/openpype/hosts/maya/plugins/load/_load_animation.py index bce1f0fc67..9c37e498ef 100644 --- a/openpype/hosts/maya/plugins/load/_load_animation.py +++ b/openpype/hosts/maya/plugins/load/_load_animation.py @@ -2,7 +2,7 @@ import openpype.hosts.maya.api.plugin class AbcLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): - """Specific loader of Alembic for the avalon.animation family""" + """Loader to reference an Alembic file""" families = ["animation", "camera", diff --git a/openpype/hosts/maya/plugins/load/actions.py b/openpype/hosts/maya/plugins/load/actions.py index 483ad32402..4b7871a40c 100644 --- a/openpype/hosts/maya/plugins/load/actions.py +++ b/openpype/hosts/maya/plugins/load/actions.py @@ -10,7 +10,7 @@ from openpype.hosts.maya.api.lib import ( class SetFrameRangeLoader(load.LoaderPlugin): - """Specific loader of Alembic for the avalon.animation family""" + """Set frame range excluding pre- and post-handles""" families = ["animation", "camera", @@ -44,7 +44,7 @@ class SetFrameRangeLoader(load.LoaderPlugin): class SetFrameRangeWithHandlesLoader(load.LoaderPlugin): - """Specific loader of Alembic for the avalon.animation family""" + """Set frame range including pre- and post-handles""" families = ["animation", "camera", diff --git a/openpype/hosts/maya/plugins/load/load_ass.py b/openpype/hosts/maya/plugins/load/load_ass.py index 18de4df3b1..a284b7ec1f 100644 --- a/openpype/hosts/maya/plugins/load/load_ass.py +++ b/openpype/hosts/maya/plugins/load/load_ass.py @@ -16,7 +16,7 @@ from openpype.hosts.maya.api.pipeline import containerise class AssProxyLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): - """Load the Proxy""" + """Load Arnold Proxy as reference""" families = ["ass"] representations = ["ass"] diff --git a/openpype/hosts/maya/plugins/load/load_gpucache.py b/openpype/hosts/maya/plugins/load/load_gpucache.py index 591e568e4c..6d5e945508 100644 --- a/openpype/hosts/maya/plugins/load/load_gpucache.py +++ b/openpype/hosts/maya/plugins/load/load_gpucache.py @@ -8,7 +8,7 @@ from openpype.api import get_project_settings class GpuCacheLoader(load.LoaderPlugin): - """Load model Alembic as gpuCache""" + """Load Alembic as gpuCache""" families = ["model"] representations = ["abc"] diff --git a/openpype/hosts/maya/plugins/load/load_reference.py b/openpype/hosts/maya/plugins/load/load_reference.py index a8875cf216..d65b5a2c1e 100644 --- a/openpype/hosts/maya/plugins/load/load_reference.py +++ b/openpype/hosts/maya/plugins/load/load_reference.py @@ -12,7 +12,7 @@ from openpype.hosts.maya.api.lib import maintained_selection class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): - """Load the model""" + """Reference file""" families = ["model", "pointcache", diff --git a/openpype/hosts/maya/plugins/load/load_vdb_to_vray.py b/openpype/hosts/maya/plugins/load/load_vdb_to_vray.py index 4f14235bfb..3a16264ec0 100644 --- a/openpype/hosts/maya/plugins/load/load_vdb_to_vray.py +++ b/openpype/hosts/maya/plugins/load/load_vdb_to_vray.py @@ -74,6 +74,7 @@ def _fix_duplicate_vvg_callbacks(): class LoadVDBtoVRay(load.LoaderPlugin): + """Load OpenVDB in a V-Ray Volume Grid""" families = ["vdbcache"] representations = ["vdb"] diff --git a/openpype/hosts/nuke/plugins/load/actions.py b/openpype/hosts/nuke/plugins/load/actions.py index 81840b3a38..d364a4f3a1 100644 --- a/openpype/hosts/nuke/plugins/load/actions.py +++ b/openpype/hosts/nuke/plugins/load/actions.py @@ -9,7 +9,7 @@ log = Logger().get_logger(__name__) class SetFrameRangeLoader(load.LoaderPlugin): - """Specific loader of Alembic for the avalon.animation family""" + """Set frame range excluding pre- and post-handles""" families = ["animation", "camera", @@ -43,7 +43,7 @@ class SetFrameRangeLoader(load.LoaderPlugin): class SetFrameRangeWithHandlesLoader(load.LoaderPlugin): - """Specific loader of Alembic for the avalon.animation family""" + """Set frame range including pre- and post-handles""" families = ["animation", "camera", From 80494c91c145a01c7aa2cf5017af8c89ecb6a4c8 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 2 May 2022 22:29:38 +0200 Subject: [PATCH 147/398] Fix typo --- openpype/modules/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 23c908299f..58ad3a8d2f 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -310,7 +310,7 @@ def _load_modules(): init_path = os.path.join(fullpath, "__init__.py") if not os.path.exists(init_path): log.debug(( - "Module directory does not contan __init__.py file {}" + "Module directory does not contain __init__.py file {}" ).format(fullpath)) continue @@ -357,7 +357,7 @@ def _load_modules(): init_path = os.path.join(fullpath, "__init__.py") if not os.path.exists(init_path): log.debug(( - "Module directory does not contan __init__.py file {}" + "Module directory does not contain __init__.py file {}" ).format(fullpath)) continue From 4fc8617bd1037b1b77b34cb903c85ddbb651a1d4 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 2 May 2022 22:32:23 +0200 Subject: [PATCH 148/398] Fix typo in comment --- openpype/modules/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 58ad3a8d2f..e280589548 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -353,7 +353,7 @@ def _load_modules(): basename, ext = os.path.splitext(filename) if os.path.isdir(fullpath): - # Check existence of init fil + # Check existence of init file init_path = os.path.join(fullpath, "__init__.py") if not os.path.exists(init_path): log.debug(( From 445fc679f1752def211f75a298ceb3a8af80f889 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 2 May 2022 22:32:47 +0200 Subject: [PATCH 149/398] And fix the other typo in comment --- openpype/modules/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index e280589548..5ad1fc71c4 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -306,7 +306,7 @@ def _load_modules(): basename, ext = os.path.splitext(filename) if os.path.isdir(fullpath): - # Check existence of init fil + # Check existence of init file init_path = os.path.join(fullpath, "__init__.py") if not os.path.exists(init_path): log.debug(( From ef7798d5028573a4384ebf38db33eae7cf47b98e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 2 May 2022 23:04:33 +0200 Subject: [PATCH 150/398] Fix coloring of TrayModuleManager output --- openpype/lib/terminal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/lib/terminal.py b/openpype/lib/terminal.py index 5121b6ec26..f6072ed209 100644 --- a/openpype/lib/terminal.py +++ b/openpype/lib/terminal.py @@ -98,7 +98,7 @@ class Terminal: r"\*\*\* WRN": _SB + _LY + r"*** WRN" + _RST, r" \- ": _SB + _LY + r" - " + _RST, r"\[ ": _SB + _LG + r"[ " + _RST, - r"\]": _SB + _LG + r"]" + _RST, + r" \]": _SB + _LG + r" ]" + _RST, r"{": _LG + r"{", r"}": r"}" + _RST, r"\(": _LY + r"(", From 4f3cbeb9a94a237da4f12858bb4ae95f7cc581d1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 3 May 2022 11:29:57 +0200 Subject: [PATCH 151/398] fix compositing order --- openpype/hosts/tvpaint/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/tvpaint/lib.py b/openpype/hosts/tvpaint/lib.py index 715ebb4a9d..c67ab1e4fb 100644 --- a/openpype/hosts/tvpaint/lib.py +++ b/openpype/hosts/tvpaint/lib.py @@ -573,7 +573,7 @@ def composite_rendered_layers( layer_ids_by_position[layer_position] = layer["layer_id"] # Sort layer positions - sorted_positions = tuple(sorted(layer_ids_by_position.keys())) + sorted_positions = tuple(reversed(sorted(layer_ids_by_position.keys()))) # Prepare variable where filepaths without any rendered content # - transparent will be created transparent_filepaths = set() From 518ab19a0b58dbe78c7051aa1f69fc4e28f1e310 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 3 May 2022 12:11:23 +0200 Subject: [PATCH 152/398] fix missing openpype_versions variable for headless mode --- start.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/start.py b/start.py index 4d4801c1e5..750552399f 100644 --- a/start.py +++ b/start.py @@ -1029,6 +1029,9 @@ def boot(): message = str(exc) _print(message) if os.environ.get("OPENPYPE_HEADLESS_MODE") == "1": + openpype_versions = bootstrap.find_openpype( + include_zips=True, staging=use_staging + ) list_versions(openpype_versions, local_version) else: igniter.show_message_dialog("Version not found", message) @@ -1053,6 +1056,9 @@ def boot(): message = str(exc) _print(message) if os.environ.get("OPENPYPE_HEADLESS_MODE") == "1": + openpype_versions = bootstrap.find_openpype( + include_zips=True, staging=use_staging + ) list_versions(openpype_versions, local_version) else: igniter.show_message_dialog("Version not found", message) From 8e7b27cff14cba8330b7f8c9148b64130c21f2f0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 3 May 2022 12:16:19 +0200 Subject: [PATCH 153/398] nuke: change `string` type to `text` --- openpype/hosts/nuke/api/lib.py | 2 +- openpype/settings/defaults/project_settings/nuke.json | 6 +++--- .../projects_schema/schemas/schema_nuke_knob_inputs.json | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 82c92f06f9..e6e61844ba 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -1099,7 +1099,7 @@ def set_node_knobs_from_settings(node, knob_settings): knob_value = float(knob_value) elif knob_type == "number": knob_value = int(knob_value) - elif knob_type == "string": + elif knob_type == "text": knob_value = knob_value elif knob_type == "hex": if not knob_value.startswith("0x"): diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index c952d72bb1..4fc6caa57e 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -131,17 +131,17 @@ "reformat_node_add": false, "reformat_node_config": [ { - "type": "string", + "type": "text", "name": "type", "value": "to format" }, { - "type": "string", + "type": "text", "name": "format", "value": "HD_1080" }, { - "type": "string", + "type": "text", "name": "filter", "value": "Lanczos6" }, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_knob_inputs.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_knob_inputs.json index 03eea039d6..95913f5335 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_knob_inputs.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_knob_inputs.json @@ -12,8 +12,8 @@ "enum_label": "Type", "enum_children": [ { - "key": "string", - "label": "String", + "key": "text", + "label": "Text", "children": [ { "type": "text", From 712d4c72abf7f40d26e7a781f06c69b0eb6ca214 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 3 May 2022 12:23:36 +0200 Subject: [PATCH 154/398] moved print and validate versions logic to separated functions --- start.py | 86 +++++++++++++++++++++++++++++++------------------------- 1 file changed, 47 insertions(+), 39 deletions(-) diff --git a/start.py b/start.py index 750552399f..064b828744 100644 --- a/start.py +++ b/start.py @@ -897,6 +897,51 @@ def _bootstrap_from_code(use_version, use_staging): return version_path +def _boot_validate_versions(use_version, local_version): + _print(f">>> Validating version [ {use_version} ]") + openpype_versions = bootstrap.find_openpype(include_zips=True, + staging=True) + openpype_versions += bootstrap.find_openpype(include_zips=True, + staging=False) + v: OpenPypeVersion + found = [v for v in openpype_versions if str(v) == use_version] + if not found: + _print(f"!!! Version [ {use_version} ] not found.") + list_versions(openpype_versions, local_version) + sys.exit(1) + + # print result + result = bootstrap.validate_openpype_version( + bootstrap.get_version_path_from_list( + use_version, openpype_versions)) + + _print("{}{}".format( + ">>> " if result[0] else "!!! ", + bootstrap.validate_openpype_version( + bootstrap.get_version_path_from_list( + use_version, openpype_versions) + )[1]) + ) + + +def _boot_print_versions(use_staging, local_version, openpype_root): + if not use_staging: + _print("--- This will list only non-staging versions detected.") + _print(" To see staging versions, use --use-staging argument.") + else: + _print("--- This will list only staging versions detected.") + _print(" To see other version, omit --use-staging argument.") + _openpype_root = OPENPYPE_ROOT + openpype_versions = bootstrap.find_openpype(include_zips=True, + staging=use_staging) + if getattr(sys, 'frozen', False): + local_version = bootstrap.get_version(Path(_openpype_root)) + else: + local_version = OpenPypeVersion.get_installed_version_str() + + list_versions(openpype_versions, local_version) + + def boot(): """Bootstrap OpenPype.""" @@ -966,30 +1011,7 @@ def boot(): local_version = OpenPypeVersion.get_installed_version_str() if "validate" in commands: - _print(f">>> Validating version [ {use_version} ]") - openpype_versions = bootstrap.find_openpype(include_zips=True, - staging=True) - openpype_versions += bootstrap.find_openpype(include_zips=True, - staging=False) - v: OpenPypeVersion - found = [v for v in openpype_versions if str(v) == use_version] - if not found: - _print(f"!!! Version [ {use_version} ] not found.") - list_versions(openpype_versions, local_version) - sys.exit(1) - - # print result - result = bootstrap.validate_openpype_version( - bootstrap.get_version_path_from_list( - use_version, openpype_versions)) - - _print("{}{}".format( - ">>> " if result[0] else "!!! ", - bootstrap.validate_openpype_version( - bootstrap.get_version_path_from_list( - use_version, openpype_versions) - )[1]) - ) + _boot_validate_versions(use_version, local_version) sys.exit(1) if not openpype_path: @@ -999,21 +1021,7 @@ def boot(): os.environ["OPENPYPE_PATH"] = openpype_path if "print_versions" in commands: - if not use_staging: - _print("--- This will list only non-staging versions detected.") - _print(" To see staging versions, use --use-staging argument.") - else: - _print("--- This will list only staging versions detected.") - _print(" To see other version, omit --use-staging argument.") - _openpype_root = OPENPYPE_ROOT - openpype_versions = bootstrap.find_openpype(include_zips=True, - staging=use_staging) - if getattr(sys, 'frozen', False): - local_version = bootstrap.get_version(Path(_openpype_root)) - else: - local_version = OpenPypeVersion.get_installed_version_str() - - list_versions(openpype_versions, local_version) + _boot_print_versions(use_staging, local_version, OPENPYPE_ROOT) sys.exit(1) # ------------------------------------------------------------------------ From 80c7d177a6c3144fb7f8cb4464503ab53ec295cb Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 3 May 2022 12:33:26 +0200 Subject: [PATCH 155/398] simplified modules file validations and imports --- openpype/modules/base.py | 103 +++++++++++++++------------------------ 1 file changed, 38 insertions(+), 65 deletions(-) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 5ad1fc71c4..b48de59fa0 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -290,49 +290,16 @@ def _load_modules(): log = PypeLogger.get_logger("ModulesLoader") - current_dir = os.path.abspath(os.path.dirname(__file__)) - processed_paths = set() - processed_paths.add(current_dir) - # Import default modules imported from 'openpype.modules' - for filename in os.listdir(current_dir): - # Ignore filenames - if ( - filename in IGNORED_FILENAMES - or filename in IGNORED_DEFAULT_FILENAMES - ): - continue - - fullpath = os.path.join(current_dir, filename) - basename, ext = os.path.splitext(filename) - - if os.path.isdir(fullpath): - # Check existence of init file - init_path = os.path.join(fullpath, "__init__.py") - if not os.path.exists(init_path): - log.debug(( - "Module directory does not contain __init__.py file {}" - ).format(fullpath)) - continue - - elif ext not in (".py", ): - continue - - try: - import_str = "openpype.modules.{}".format(basename) - new_import_str = "{}.{}".format(modules_key, basename) - default_module = __import__(import_str, fromlist=("", )) - sys.modules[new_import_str] = default_module - setattr(openpype_modules, basename, default_module) - - except Exception: - msg = ( - "Failed to import default module '{}'." - ).format(basename) - log.error(msg, exc_info=True) - # Look for OpenPype modules in paths defined with `get_module_dirs` # - dynamically imported OpenPype modules and addons - for dirpath in get_module_dirs(): + module_dirs = get_module_dirs() + # Add current directory at first place + # - has small differences in import logic + current_dir = os.path.abspath(os.path.dirname(__file__)) + module_dirs.insert(0, current_dir) + + processed_paths = set() + for dirpath in module_dirs: # Skip already processed paths if dirpath in processed_paths: continue @@ -344,39 +311,42 @@ def _load_modules(): ).format(dirpath)) continue + is_in_current_dir = dirpath == current_dir for filename in os.listdir(dirpath): # Ignore filenames if filename in IGNORED_FILENAMES: continue + if ( + is_in_current_dir + and filename in IGNORED_DEFAULT_FILENAMES + ): + continue + fullpath = os.path.join(dirpath, filename) basename, ext = os.path.splitext(filename) - if os.path.isdir(fullpath): - # Check existence of init file - init_path = os.path.join(fullpath, "__init__.py") - if not os.path.exists(init_path): - log.debug(( - "Module directory does not contain __init__.py file {}" - ).format(fullpath)) - continue - - elif ext not in (".py", ): - continue - # TODO add more logic how to define if folder is module or not # - check manifest and content of manifest try: - if os.path.isdir(fullpath): - # Module without init file can't be used as OpenPype module - # because the module class could not be imported - init_file = os.path.join(fullpath, "__init__.py") - if not os.path.exists(init_file): - log.info(( - "Skipping module directory because of" - " missing \"__init__.py\" file. \"{}\"" + if is_in_current_dir: + # Don't import dynamically + import_str = "openpype.modules.{}".format(basename) + new_import_str = "{}.{}".format(modules_key, basename) + default_module = __import__(import_str, fromlist=("", )) + sys.modules[new_import_str] = default_module + setattr(openpype_modules, basename, default_module) + + elif os.path.isdir(fullpath): + # Check existence of init file + init_path = os.path.join(fullpath, "__init__.py") + if not os.path.exists(init_path): + log.debug(( + "Module directory does not contan __init__.py" + " file {}" ).format(fullpath)) continue + import_module_from_dirpath(dirpath, filename, modules_key) elif ext in (".py", ): @@ -384,10 +354,13 @@ def _load_modules(): setattr(openpype_modules, basename, module) except Exception: - log.error( - "Failed to import '{}'.".format(fullpath), - exc_info=True - ) + if is_in_current_dir: + msg = "Failed to import default module '{}'.".format( + basename + ) + else: + msg = "Failed to import '{}'.".format(fullpath) + log.error(msg, exc_info=True) class _OpenPypeInterfaceMeta(ABCMeta): From 16af1a2347af8ae4f87f2095a1f63379312724dd Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 3 May 2022 12:41:17 +0200 Subject: [PATCH 156/398] moved validation much earlier --- openpype/modules/base.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index b48de59fa0..a85bedac31 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -326,11 +326,25 @@ def _load_modules(): fullpath = os.path.join(dirpath, filename) basename, ext = os.path.splitext(filename) + # Validations + if os.path.isdir(fullpath): + # Check existence of init file + init_path = os.path.join(fullpath, "__init__.py") + if not os.path.exists(init_path): + log.debug(( + "Module directory does not contan __init__.py" + " file {}" + ).format(fullpath)) + continue + + elif ext not in (".py", ): + continue + # TODO add more logic how to define if folder is module or not # - check manifest and content of manifest try: + # Don't import dynamically current directory modules if is_in_current_dir: - # Don't import dynamically import_str = "openpype.modules.{}".format(basename) new_import_str = "{}.{}".format(modules_key, basename) default_module = __import__(import_str, fromlist=("", )) @@ -338,18 +352,9 @@ def _load_modules(): setattr(openpype_modules, basename, default_module) elif os.path.isdir(fullpath): - # Check existence of init file - init_path = os.path.join(fullpath, "__init__.py") - if not os.path.exists(init_path): - log.debug(( - "Module directory does not contan __init__.py" - " file {}" - ).format(fullpath)) - continue - import_module_from_dirpath(dirpath, filename, modules_key) - elif ext in (".py", ): + else: module = import_filepath(fullpath) setattr(openpype_modules, basename, module) From 313382f2fb7d6957f3fe5a11688ac5c0d8e18783 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 3 May 2022 12:46:40 +0200 Subject: [PATCH 157/398] fix OPENPYPE_ROOT usage --- start.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/start.py b/start.py index 064b828744..cd1d95dd8f 100644 --- a/start.py +++ b/start.py @@ -931,11 +931,11 @@ def _boot_print_versions(use_staging, local_version, openpype_root): else: _print("--- This will list only staging versions detected.") _print(" To see other version, omit --use-staging argument.") - _openpype_root = OPENPYPE_ROOT + openpype_versions = bootstrap.find_openpype(include_zips=True, staging=use_staging) if getattr(sys, 'frozen', False): - local_version = bootstrap.get_version(Path(_openpype_root)) + local_version = bootstrap.get_version(Path(openpype_root)) else: local_version = OpenPypeVersion.get_installed_version_str() From 7903437ba76f6ddf0a0679d25a7169a56c669bad Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 3 May 2022 12:50:52 +0200 Subject: [PATCH 158/398] nuke: converting schema knobs to template schema - refactoring imageio nuke to the template - refactoring nuke publish bake move to the template --- .../defaults/project_anatomy/imageio.json | 33 ++- .../projects_schema/schema_project_nuke.json | 20 +- .../schemas/schema_anatomy_imageio.json | 54 +---- .../schemas/schema_nuke_knob_inputs.json | 209 ----------------- .../schemas/schema_nuke_publish.json | 106 +-------- .../schemas/template_nuke_knob_inputs.json | 222 ++++++++++++++++++ 6 files changed, 281 insertions(+), 363 deletions(-) delete mode 100644 openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_knob_inputs.json create mode 100644 openpype/settings/entities/schemas/projects_schema/schemas/template_nuke_knob_inputs.json diff --git a/openpype/settings/defaults/project_anatomy/imageio.json b/openpype/settings/defaults/project_anatomy/imageio.json index fedae994bf..2023dae23c 100644 --- a/openpype/settings/defaults/project_anatomy/imageio.json +++ b/openpype/settings/defaults/project_anatomy/imageio.json @@ -55,36 +55,44 @@ "nukeNodeClass": "Write", "knobs": [ { + "type": "text", "name": "file_type", "value": "exr" }, { + "type": "text", "name": "datatype", "value": "16 bit half" }, { + "type": "text", "name": "compression", "value": "Zip (1 scanline)" }, { + "type": "bool", "name": "autocrop", - "value": "True" + "value": true }, { + "type": "hex", "name": "tile_color", "value": "0xff0000ff" }, { + "type": "text", "name": "channels", "value": "rgb" }, { + "type": "text", "name": "colorspace", "value": "linear" }, { + "type": "bool", "name": "create_directories", - "value": "True" + "value": true } ] }, @@ -95,36 +103,44 @@ "nukeNodeClass": "Write", "knobs": [ { + "type": "text", "name": "file_type", "value": "exr" }, { + "type": "text", "name": "datatype", "value": "16 bit half" }, { + "type": "text", "name": "compression", "value": "Zip (1 scanline)" }, { + "type": "bool", "name": "autocrop", - "value": "False" + "value": true }, { + "type": "hex", "name": "tile_color", "value": "0xadab1dff" }, { + "type": "text", "name": "channels", "value": "rgb" }, { + "type": "text", "name": "colorspace", "value": "linear" }, { + "type": "bool", "name": "create_directories", - "value": "True" + "value": true } ] }, @@ -135,32 +151,39 @@ "nukeNodeClass": "Write", "knobs": [ { + "type": "text", "name": "file_type", "value": "tiff" }, { + "type": "text", "name": "datatype", "value": "16 bit" }, { + "type": "text", "name": "compression", "value": "Deflate" }, { + "type": "hex", "name": "tile_color", "value": "0x23ff00ff" }, { + "type": "text", "name": "channels", "value": "rgb" }, { + "type": "text", "name": "colorspace", "value": "sRGB" }, { + "type": "bool", "name": "create_directories", - "value": "True" + "value": true } ] } diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json b/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json index dfd3306b2e..29ad8e3c6c 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json @@ -106,8 +106,14 @@ } }, { - "type": "schema", - "name": "schema_nuke_knob_inputs" + "type": "schema_template", + "name": "template_nuke_knob_inputs", + "template_data": [ + { + "label": "Node knobs", + "key": "knobs" + } + ] } ] }, @@ -142,8 +148,14 @@ "label": "Add reviewable toggle" }, { - "type": "schema", - "name": "schema_nuke_knob_inputs" + "type": "schema_template", + "name": "template_nuke_knob_inputs", + "template_data": [ + { + "label": "Node knobs", + "key": "knobs" + } + ] } ] } 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 819f7121c4..ef8c907dda 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 @@ -272,29 +272,12 @@ "label": "Nuke Node Class" }, { - "type": "collapsible-wrap", - "label": "Knobs", - "collapsible": true, - "collapsed": true, - "children": [ + "type": "schema_template", + "name": "template_nuke_knob_inputs", + "template_data": [ { - "key": "knobs", - "type": "list", - "object_type": { - "type": "dict", - "children": [ - { - "type": "text", - "key": "name", - "label": "Name" - }, - { - "type": "text", - "key": "value", - "label": "Value" - } - ] - } + "label": "Knobs", + "key": "knobs" } ] } @@ -333,29 +316,12 @@ "object_type": "text" }, { - "type": "collapsible-wrap", - "label": "Knobs overrides", - "collapsible": true, - "collapsed": true, - "children": [ + "type": "schema_template", + "name": "template_nuke_knob_inputs", + "template_data": [ { - "key": "knobs", - "type": "list", - "object_type": { - "type": "dict", - "children": [ - { - "type": "text", - "key": "name", - "label": "Name" - }, - { - "type": "text", - "key": "value", - "label": "Value" - } - ] - } + "label": "Knobs overrides", + "key": "knobs" } ] } diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_knob_inputs.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_knob_inputs.json deleted file mode 100644 index 95913f5335..0000000000 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_knob_inputs.json +++ /dev/null @@ -1,209 +0,0 @@ -{ - "type": "collapsible-wrap", - "label": "Knob defaults", - "collapsible": true, - "collapsed": true, - "children": [{ - "type": "list", - "key": "knobs", - "object_type": { - "type": "dict-conditional", - "enum_key": "type", - "enum_label": "Type", - "enum_children": [ - { - "key": "text", - "label": "Text", - "children": [ - { - "type": "text", - "key": "name", - "label": "Name" - }, - { - "type": "text", - "key": "value", - "label": "Value" - } - ] - }, - { - "key": "hex", - "label": "Hexadecimal (0x)", - "children": [ - { - "type": "text", - "key": "name", - "label": "Name" - }, - { - "type": "text", - "key": "value", - "label": "Value" - } - ] - }, - { - "key": "bool", - "label": "Boolean", - "children": [ - { - "type": "text", - "key": "name", - "label": "Name" - }, - { - "type": "boolean", - "key": "value", - "label": "Value" - } - ] - }, - { - "key": "number", - "label": "Number", - "children": [ - { - "type": "text", - "key": "name", - "label": "Name" - }, - { - "type": "number", - "key": "value", - "default": 1, - "decimal": 0 - } - - ] - }, - { - "key": "decimal_number", - "label": "Decimal number", - "children": [ - { - "type": "text", - "key": "name", - "label": "Name" - }, - { - "type": "number", - "key": "value", - "default": 1, - "decimal": 4 - } - - ] - }, - { - "key": "2d_vector", - "label": "2D vector", - "children": [ - { - "type": "text", - "key": "name", - "label": "Name" - }, - { - "type": "list-strict", - "key": "value", - "label": "Value", - "object_types": [ - { - "type": "number", - "key": "x", - "default": 1, - "decimal": 4 - }, - { - "type": "number", - "key": "y", - "default": 1, - "decimal": 4 - } - ] - } - ] - }, - { - "key": "3d_vector", - "label": "3D vector", - "children": [ - { - "type": "text", - "key": "name", - "label": "Name" - }, - { - "type": "list-strict", - "key": "value", - "label": "Value", - "object_types": [ - { - "type": "number", - "key": "x", - "default": 1, - "decimal": 4 - }, - { - "type": "number", - "key": "y", - "default": 1, - "decimal": 4 - }, - { - "type": "number", - "key": "y", - "default": 1, - "decimal": 4 - } - ] - } - ] - }, - { - "key": "color", - "label": "Color", - "children": [ - { - "type": "text", - "key": "name", - "label": "Name" - }, - { - "type": "list-strict", - "key": "value", - "label": "Value", - "object_types": [ - { - "type": "number", - "key": "x", - "default": 1, - "decimal": 4 - }, - { - "type": "number", - "key": "x", - "default": 1, - "decimal": 4 - }, - { - "type": "number", - "key": "y", - "default": 1, - "decimal": 4 - }, - { - "type": "number", - "key": "y", - "default": 1, - "decimal": 4 - } - ] - } - ] - } - ] - } - }] -} 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 d67fb309bd..94b52bba13 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 @@ -253,108 +253,12 @@ "default": false }, { - "type": "collapsible-wrap", - "label": "Reformat Node Knobs", - "collapsible": true, - "collapsed": true, - "children": [ + "type": "schema_template", + "name": "template_nuke_knob_inputs", + "template_data": [ { - "type": "list", - "key": "reformat_node_config", - "object_type": { - "type": "dict-conditional", - "enum_key": "type", - "enum_label": "Type", - "enum_children": [ - { - "key": "string", - "label": "String", - "children": [ - { - "type": "text", - "key": "name", - "label": "Name" - }, - { - "type": "text", - "key": "value", - "label": "Value" - } - ] - }, - { - "key": "bool", - "label": "Boolean", - "children": [ - { - "type": "text", - "key": "name", - "label": "Name" - }, - { - "type": "boolean", - "key": "value", - "label": "Value" - } - ] - }, - { - "key": "number", - "label": "Number", - "children": [ - { - "type": "text", - "key": "name", - "label": "Name" - }, - { - "type": "list-strict", - "key": "value", - "label": "Value", - "object_types": [ - { - "type": "number", - "key": "number", - "default": 1, - "decimal": 4 - } - ] - } - - ] - }, - { - "key": "list_numbers", - "label": "2 Numbers", - "children": [ - { - "type": "text", - "key": "name", - "label": "Name" - }, - { - "type": "list-strict", - "key": "value", - "label": "Value", - "object_types": [ - { - "type": "number", - "key": "x", - "default": 1, - "decimal": 4 - }, - { - "type": "number", - "key": "y", - "default": 1, - "decimal": 4 - } - ] - } - ] - } - ] - } + "label": "Reformat Node Knobs", + "key": "reformat_node_config" } ] }, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/template_nuke_knob_inputs.json b/openpype/settings/entities/schemas/projects_schema/schemas/template_nuke_knob_inputs.json new file mode 100644 index 0000000000..957c684013 --- /dev/null +++ b/openpype/settings/entities/schemas/projects_schema/schemas/template_nuke_knob_inputs.json @@ -0,0 +1,222 @@ +[ + { + "type": "collapsible-wrap", + "label": "{label}", + "collapsible": true, + "collapsed": true, + "children": [{ + "type": "list", + "key": "{key}", + "object_type": { + "type": "dict-conditional", + "enum_key": "type", + "enum_label": "Type", + "enum_children": [ + { + "key": "text", + "label": "Text", + "children": [ + { + "type": "text", + "key": "name", + "label": "Name" + }, + { + "type": "text", + "key": "value", + "label": "Value" + } + ] + }, + { + "key": "hex", + "label": "Hexadecimal (0x)", + "children": [ + { + "type": "text", + "key": "name", + "label": "Name" + }, + { + "type": "text", + "key": "value", + "label": "Value" + } + ] + }, + { + "key": "bool", + "label": "Boolean", + "children": [ + { + "type": "text", + "key": "name", + "label": "Name" + }, + { + "type": "boolean", + "key": "value", + "label": "Value" + } + ] + }, + { + "key": "number", + "label": "Number", + "children": [ + { + "type": "text", + "key": "name", + "label": "Name" + }, + { + "type": "number", + "key": "value", + "default": 1, + "decimal": 0, + "maximum": 99999999 + } + + ] + }, + { + "key": "decimal_number", + "label": "Decimal number", + "children": [ + { + "type": "text", + "key": "name", + "label": "Name" + }, + { + "type": "number", + "key": "value", + "default": 1, + "decimal": 4, + "maximum": 99999999 + } + + ] + }, + { + "key": "2d_vector", + "label": "2D vector", + "children": [ + { + "type": "text", + "key": "name", + "label": "Name" + }, + { + "type": "list-strict", + "key": "value", + "label": "Value", + "object_types": [ + { + "type": "number", + "key": "x", + "default": 1, + "decimal": 4, + "maximum": 99999999 + }, + { + "type": "number", + "key": "y", + "default": 1, + "decimal": 4, + "maximum": 99999999 + } + ] + } + ] + }, + { + "key": "3d_vector", + "label": "3D vector", + "children": [ + { + "type": "text", + "key": "name", + "label": "Name" + }, + { + "type": "list-strict", + "key": "value", + "label": "Value", + "object_types": [ + { + "type": "number", + "key": "x", + "default": 1, + "decimal": 4, + "maximum": 99999999 + }, + { + "type": "number", + "key": "y", + "default": 1, + "decimal": 4, + "maximum": 99999999 + }, + { + "type": "number", + "key": "y", + "default": 1, + "decimal": 4, + "maximum": 99999999 + } + ] + } + ] + }, + { + "key": "color", + "label": "Color", + "children": [ + { + "type": "text", + "key": "name", + "label": "Name" + }, + { + "type": "list-strict", + "key": "value", + "label": "Value", + "object_types": [ + { + "type": "number", + "key": "x", + "default": 1, + "decimal": 4, + "maximum": 99999999 + }, + { + "type": "number", + "key": "x", + "default": 1, + "decimal": 4, + "maximum": 99999999 + }, + { + "type": "number", + "key": "y", + "default": 1, + "decimal": 4, + "maximum": 99999999 + }, + { + "type": "number", + "key": "y", + "default": 1, + "decimal": 4, + "maximum": 99999999 + } + ] + } + ] + } + ] + } + }] + } +] From 317c27eeaf06f393122aec10508baf9f1b413f71 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 3 May 2022 14:04:40 +0200 Subject: [PATCH 159/398] modified log message Co-authored-by: Roy Nieterau --- openpype/modules/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index a85bedac31..629a2fa689 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -364,7 +364,7 @@ def _load_modules(): basename ) else: - msg = "Failed to import '{}'.".format(fullpath) + msg = "Failed to import module '{}'.".format(fullpath) log.error(msg, exc_info=True) From 0b306e2e77bed1dd939619baa9917075d9b3902d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 3 May 2022 14:04:49 +0200 Subject: [PATCH 160/398] fix typo Co-authored-by: Roy Nieterau --- openpype/modules/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 629a2fa689..0dd512ee8b 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -332,7 +332,7 @@ def _load_modules(): init_path = os.path.join(fullpath, "__init__.py") if not os.path.exists(init_path): log.debug(( - "Module directory does not contan __init__.py" + "Module directory does not contain __init__.py" " file {}" ).format(fullpath)) continue From dc94809957803a7ab57c027ff2b82b504fe78f38 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 3 May 2022 14:35:45 +0200 Subject: [PATCH 161/398] added icon to asset input dialog --- openpype/style/style.css | 19 ++++++++++++++- openpype/tools/publisher/widgets/widgets.py | 26 +++++++++++++++++---- 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/openpype/style/style.css b/openpype/style/style.css index 8eeae1a58a..93b13916c1 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -1027,7 +1027,24 @@ VariantInputsWidget QToolButton { border-left: 1px solid {color:border}; } -#TasksCombobox[state="invalid"], #AssetNameInput[state="invalid"] { +#AssetNameInputButton { + background: {color:bg-inputs}; + border: 1px solid {color:border}; + border-bottom-left-radius: 0px; + border-top-left-radius: 0px; + padding: 0px; + qproperty-iconSize: 11px 11px; +} +#AssetNameInputButton:disabled { + background: {color:bg-inputs-disabled}; +} +#AssetNameInput { + border-bottom-right-radius: 0px; + border-top-right-radius: 0px; + border-right: none; +} + +#TasksCombobox[state="invalid"], #AssetNameInput[state="invalid"], #AssetNameInputButton[state="invalid"] { border-color: {color:publisher:error}; } diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index 5ced469b59..1569e1a4c1 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -14,7 +14,8 @@ from openpype.tools.utils import ( PlaceholderLineEdit, IconButton, PixmapLabel, - BaseClickableFrame + BaseClickableFrame, + set_style_property, ) from openpype.pipeline.create import SUBSET_NAME_ALLOWED_SYMBOLS from .assets_widget import AssetsDialog @@ -350,15 +351,32 @@ class AssetsField(BaseClickableFrame): name_input = ClickableLineEdit(self) name_input.setObjectName("AssetNameInput") + icon_name = "fa.window-maximize" + icon = qtawesome.icon(icon_name, color="white") + icon_btn = QtWidgets.QPushButton(self) + icon_btn.setIcon(icon) + icon_btn.setObjectName("AssetNameInputButton") + layout = QtWidgets.QHBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) layout.addWidget(name_input, 1) + layout.addWidget(icon_btn, 0) + for widget in ( + name_input, + icon_btn + ): + size_policy = widget.sizePolicy() + size_policy.setVerticalPolicy(size_policy.MinimumExpanding) + widget.setSizePolicy(size_policy) name_input.clicked.connect(self._mouse_release_callback) + icon_btn.clicked.connect(self._mouse_release_callback) dialog.finished.connect(self._on_dialog_finish) self._dialog = dialog self._name_input = name_input + self._icon_btn = icon_btn self._origin_value = [] self._origin_selection = [] @@ -406,10 +424,8 @@ class AssetsField(BaseClickableFrame): self._set_state_property(state) def _set_state_property(self, state): - current_value = self._name_input.property("state") - if current_value != state: - self._name_input.setProperty("state", state) - self._name_input.style().polish(self._name_input) + set_style_property(self._name_input, "state", state) + set_style_property(self._icon_btn, "state", state) def is_valid(self): """Is asset valid.""" From b17d9d23497691a4cea6174c323a2bb3cf27e870 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 3 May 2022 15:22:32 +0200 Subject: [PATCH 162/398] fixed empty files def --- openpype/lib/attribute_definitions.py | 14 +++++++++++++- openpype/widgets/attribute_defs/files_widget.py | 6 ++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/openpype/lib/attribute_definitions.py b/openpype/lib/attribute_definitions.py index 4ea691ca08..a1f7c1e0f4 100644 --- a/openpype/lib/attribute_definitions.py +++ b/openpype/lib/attribute_definitions.py @@ -316,6 +316,7 @@ class FileDefItem(object): self.is_sequence = False self.template = None self.frames = [] + self.is_empty = True self.set_filenames(filenames, frames, template) @@ -323,7 +324,9 @@ class FileDefItem(object): return json.dumps(self.to_dict()) def __repr__(self): - if self.is_sequence: + if self.is_empty: + filename = "< empty >" + elif self.is_sequence: filename = self.template else: filename = self.filenames[0] @@ -335,6 +338,9 @@ class FileDefItem(object): @property def label(self): + if self.is_empty: + return None + if not self.is_sequence: return self.filenames[0] @@ -386,6 +392,8 @@ class FileDefItem(object): @property def ext(self): + if self.is_empty: + return None _, ext = os.path.splitext(self.filenames[0]) if ext: return ext @@ -393,6 +401,9 @@ class FileDefItem(object): @property def is_dir(self): + if self.is_empty: + return False + # QUESTION a better way how to define folder (in init argument?) if self.ext: return False @@ -411,6 +422,7 @@ class FileDefItem(object): if is_sequence and not template: raise ValueError("Missing template for sequence") + self.is_empty = len(filenames) == 0 self.filenames = filenames self.template = template self.frames = frames diff --git a/openpype/widgets/attribute_defs/files_widget.py b/openpype/widgets/attribute_defs/files_widget.py index 924dbf7fe5..23cf8342b1 100644 --- a/openpype/widgets/attribute_defs/files_widget.py +++ b/openpype/widgets/attribute_defs/files_widget.py @@ -151,7 +151,7 @@ class FilesModel(QtGui.QStandardItemModel): item = QtGui.QStandardItem() item_id = str(uuid.uuid4()) item.setData(item_id, ITEM_ID_ROLE) - item.setData(file_item.label, ITEM_LABEL_ROLE) + item.setData(file_item.label or "< empty >", ITEM_LABEL_ROLE) item.setData(file_item.filenames, FILENAMES_ROLE) item.setData(file_item.directory, DIRPATH_ROLE) item.setData(icon_pixmap, ITEM_ICON_ROLE) @@ -512,7 +512,9 @@ class FilesWidget(QtWidgets.QFrame): return file_items if file_items: return file_items[0] - return FileDefItem.create_empty_item() + + empty_item = FileDefItem.create_empty_item() + return empty_item.to_dict() def set_filters(self, folders_allowed, exts_filter): self._files_proxy_model.set_allow_folders(folders_allowed) From 33c3d1bc4c8278690c522f1b74b1fe5a2705ed5d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 3 May 2022 15:23:22 +0200 Subject: [PATCH 163/398] resize properly families and subset names --- openpype/tools/publisher/widgets/widgets.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index 1569e1a4c1..45c42e6558 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -908,11 +908,23 @@ class MultipleItemWidget(QtWidgets.QWidget): layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(view) + model.rowsInserted.connect(self._on_insert) + self._view = view self._model = model self._value = [] + def _on_insert(self): + self._update_size() + + def _update_size(self): + model = self._view.model() + if model.rowCount() == 0: + return + height = self._view.sizeHintForRow(0) + self.setMaximumHeight(height + (2 * self._view.spacing())) + def showEvent(self, event): super(MultipleItemWidget, self).showEvent(event) tmp_item = None @@ -920,13 +932,15 @@ class MultipleItemWidget(QtWidgets.QWidget): # Add temp item to be able calculate maximum height of widget tmp_item = QtGui.QStandardItem("tmp") self._model.appendRow(tmp_item) - - height = self._view.sizeHintForRow(0) - self.setMaximumHeight(height + (2 * self._view.spacing())) + self._update_size() if tmp_item is not None: self._model.clear() + def resizeEvent(self, event): + super(MultipleItemWidget, self).resizeEvent(event) + self._update_size() + def set_value(self, value=None): """Set value/s of currently selected instance.""" if value is None: From 1c2b6408c4d3af491a886aa3d61c5dc7ee53a7f1 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 3 May 2022 15:28:36 +0200 Subject: [PATCH 164/398] nuke: refectory imageio settings for plugin nodes --- openpype/hosts/nuke/api/lib.py | 117 +++++++++++++++------------------ 1 file changed, 52 insertions(+), 65 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index e6e61844ba..0fa5633b90 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -375,23 +375,18 @@ def add_write_node(name, **kwarg): Returns: node (obj): nuke write node """ + file_path = kwarg["path"] + knobs = kwarg["knobs"] frame_range = kwarg.get("frame_range", None) w = nuke.createNode( "Write", "name {}".format(name)) - w["file"].setValue(kwarg["file"]) + w["file"].setValue(file_path) - for k, v in kwarg.items(): - if "frame_range" in k: - continue - log.info([k, v]) - try: - w[k].setValue(v) - except KeyError as e: - log.debug(e) - continue + # finally add knob overrides + set_node_knobs_from_settings(w, knobs) if frame_range: w["use_limit"].setValue(True) @@ -501,17 +496,9 @@ def get_nuke_imageio_settings(): return get_anatomy_settings(Context.project_name)["imageio"]["nuke"] -def get_created_node_imageio_setting(**kwarg): +def get_imageio_node_setting(node_class, plugin_name, subset): ''' Get preset data for dataflow (fileType, compression, bitDepth) ''' - 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"] required_nodes = imageio_nodes["requiredNodes"] override_nodes = imageio_nodes["overrideNodes"] @@ -520,8 +507,8 @@ def get_created_node_imageio_setting(**kwarg): for node in required_nodes: log.info(node) if ( - nodeclass in node["nukeNodeClass"] - and creator in node["plugins"] + node_class in node["nukeNodeClass"] + and plugin_name in node["plugins"] ): imageio_node = node break @@ -532,10 +519,10 @@ def get_created_node_imageio_setting(**kwarg): override_imageio_node = None for onode in override_nodes: log.info(onode) - if nodeclass not in node["nukeNodeClass"]: + if node_class not in node["nukeNodeClass"]: continue - if creator not in node["plugins"]: + if plugin_name not in node["plugins"]: continue if ( @@ -726,15 +713,14 @@ def check_subsetname_exists(nodes, subset_name): def get_render_path(node): ''' Generate Render path from presets regarding avalon knob data ''' - data = {'avalon': read_avalon_data(node)} - data_preset = { - "nodeclass": data["avalon"]["family"], - "families": [data["avalon"]["families"]], - "creator": data["avalon"]["creator"], - "subset": data["avalon"]["subset"] - } + avalon_knob_data = read_avalon_data(node) + data = {'avalon': avalon_knob_data} - nuke_imageio_writes = get_created_node_imageio_setting(**data_preset) + nuke_imageio_writes = get_imageio_node_setting( + node_class=avalon_knob_data["family"], + plugin_name=avalon_knob_data["creator"], + subset=avalon_knob_data["subset"] + ) host_name = os.environ.get("AVALON_APP") data.update({ @@ -859,9 +845,20 @@ def create_write_node(name, data, input=None, prenodes=None, Return: node (obj): group node with avalon data as Knobs ''' + # group node knob overrides knob_overrides = data.get("knobs", []) - imageio_writes = get_created_node_imageio_setting(**data) + # filtering variables + plugin_name = data["creator"], + subset = data["subset"] + + # get knob settings for write node + imageio_writes = get_imageio_node_setting( + node_class=data["nodeclass"], + plugin_name=plugin_name, + subset=subset + ) + for knob in imageio_writes["knobs"]: if knob["name"] == "file_type": representation = knob["value"] @@ -893,22 +890,20 @@ def create_write_node(name, data, input=None, prenodes=None, log.warning("Path does not exist! I am creating it.") os.makedirs(os.path.dirname(fpath)) - _data = OrderedDict({ - "file": fpath - }) + _wn_props = { + "path": fpath, + "knobs": imageio_writes["knobs"] + } # adding dataflow template - log.debug("imageio_writes: `{}`".format(imageio_writes)) - for knob in imageio_writes["knobs"]: - _data[knob["name"]] = knob["value"] - - _data = fix_data_for_node_create(_data) - - log.debug("_data: `{}`".format(_data)) + log.debug("__ _wn_props: `{}`".format( + pformat(_wn_props) + )) if "frame_range" in data.keys(): - _data["frame_range"] = data.get("frame_range", None) - log.debug("_data[frame_range]: `{}`".format(_data["frame_range"])) + _wn_props["frame_range"] = data.get("frame_range", None) + log.debug("_wn_props[frame_range]: `{}`".format( + _wn_props["frame_range"])) GN = nuke.createNode("Group", "name {}".format(name)) @@ -985,7 +980,7 @@ def create_write_node(name, data, input=None, prenodes=None, # creating write node write_node = now_node = add_write_node( "inside_{}".format(name), - **_data + **_wn_props ) write_node.hideControlPanel() # connect to previous node @@ -1060,7 +1055,7 @@ def create_write_node(name, data, input=None, prenodes=None, GN[_NODE_TAB_NAME].setFlag(0) # set tile color - tile_color = _data.get("tile_color", "0xff0000ff") + tile_color = _wn_props["knobs"].get("tile_color", "0xff0000ff") GN["tile_color"].setValue(tile_color) # finally add knob overrides @@ -1414,15 +1409,11 @@ class WorkfileSettings(object): if avalon_knob_data.get("families"): families.append(avalon_knob_data.get("families")) - data_preset = { - "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( - **data_preset) + nuke_imageio_writes = get_imageio_node_setting( + node_class=avalon_knob_data["family"], + plugin_name=avalon_knob_data["creator"], + subset=avalon_knob_data["subset"] + ) log.debug("nuke_imageio_writes: `{}`".format(nuke_imageio_writes)) @@ -1737,17 +1728,13 @@ def get_write_node_template_attr(node): ''' # get avalon data from node - data = {"avalon": read_avalon_data(node)} - - data_preset = { - "nodeclass": data["avalon"]["family"], - "families": [data["avalon"]["families"]], - "creator": data["avalon"]["creator"], - "subset": data["avalon"]["subset"] - } - + avalon_knob_data = read_avalon_data(node) # get template data - nuke_imageio_writes = get_created_node_imageio_setting(**data_preset) + nuke_imageio_writes = get_imageio_node_setting( + node_class=avalon_knob_data["family"], + plugin_name=avalon_knob_data["creator"], + subset=avalon_knob_data["subset"] + ) # collecting correct data correct_data = OrderedDict({ From 9eb9c62318f0a00b4ceb5c21bd42c839bedaa9be Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 3 May 2022 15:55:38 +0200 Subject: [PATCH 165/398] different asset dialog size --- .../tools/publisher/widgets/assets_widget.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/openpype/tools/publisher/widgets/assets_widget.py b/openpype/tools/publisher/widgets/assets_widget.py index c4d3cf8b1a..46fdcc6526 100644 --- a/openpype/tools/publisher/widgets/assets_widget.py +++ b/openpype/tools/publisher/widgets/assets_widget.py @@ -220,9 +220,26 @@ class AssetsDialog(QtWidgets.QDialog): # - adds ability to call reset on multiple places without repeating self._soft_reset_enabled = True + self._first_show = True + self._default_height = 500 + + def _on_first_show(self): + center = self.rect().center() + size = self.size() + size.setHeight(self._default_height) + + self.resize(size) + new_pos = self.mapToGlobal(center) + new_pos.setX(new_pos.x() - int(self.width() / 2)) + new_pos.setY(new_pos.y() - int(self.height() / 2)) + self.move(new_pos) + def showEvent(self, event): """Refresh asset model on show.""" super(AssetsDialog, self).showEvent(event) + if self._first_show: + self._first_show = False + self._on_first_show() # Refresh on show self.reset(False) From 5ca7b53c11e9b9fe66bd05947d04edadd8efb0d4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 3 May 2022 15:56:01 +0200 Subject: [PATCH 166/398] fix styles --- openpype/style/style.css | 32 +++++++++++++++++---- openpype/tools/publisher/widgets/widgets.py | 6 +++- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/openpype/style/style.css b/openpype/style/style.css index 93b13916c1..d76d833be1 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -1027,24 +1027,44 @@ VariantInputsWidget QToolButton { border-left: 1px solid {color:border}; } -#AssetNameInputButton { +#AssetNameInputWidget { background: {color:bg-inputs}; border: 1px solid {color:border}; + border-radius: 0.3em; +} + +#AssetNameInputWidget QWidget { + background: transparent; +} + +#AssetNameInputButton { border-bottom-left-radius: 0px; border-top-left-radius: 0px; padding: 0px; qproperty-iconSize: 11px 11px; + border-left: 1px solid {color:border}; + border-right: none; + border-top: none; + border-bottom: none; } -#AssetNameInputButton:disabled { - background: {color:bg-inputs-disabled}; -} + #AssetNameInput { border-bottom-right-radius: 0px; border-top-right-radius: 0px; - border-right: none; + border: none; } -#TasksCombobox[state="invalid"], #AssetNameInput[state="invalid"], #AssetNameInputButton[state="invalid"] { +#AssetNameInputWidget:hover { + border-color: {color:border-hover}; +} +#AssetNameInputWidget:focus{ + border-color: {color:border-focus}; +} +#AssetNameInputWidget:disabled { + background: {color:bg-inputs-disabled}; +} + +#TasksCombobox[state="invalid"], #AssetNameInputWidget[state="invalid"], #AssetNameInputButton[state="invalid"] { border-color: {color:publisher:error}; } diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index 45c42e6558..6c09128857 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -345,8 +345,11 @@ class AssetsField(BaseClickableFrame): def __init__(self, controller, parent): super(AssetsField, self).__init__(parent) + self.setObjectName("AssetNameInputWidget") - dialog = AssetsDialog(controller, self) + # Don't use 'self' for parent! + # - this widget has specific styles + dialog = AssetsDialog(controller, parent) name_input = ClickableLineEdit(self) name_input.setObjectName("AssetNameInput") @@ -363,6 +366,7 @@ class AssetsField(BaseClickableFrame): layout.addWidget(name_input, 1) layout.addWidget(icon_btn, 0) + # Make sure all widgets are vertically extended to highest widget for widget in ( name_input, icon_btn From c23b55f714614671a1c002ad2c5280a07d413676 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 3 May 2022 16:09:29 +0200 Subject: [PATCH 167/398] changed default sizes of create dialog --- .../tools/publisher/widgets/create_dialog.py | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/openpype/tools/publisher/widgets/create_dialog.py b/openpype/tools/publisher/widgets/create_dialog.py index bcf87dea6a..09b2b9cb30 100644 --- a/openpype/tools/publisher/widgets/create_dialog.py +++ b/openpype/tools/publisher/widgets/create_dialog.py @@ -283,7 +283,7 @@ class HelpButton(ClickableFrame): class CreateDialog(QtWidgets.QDialog): - default_size = (900, 500) + default_size = (1000, 560) def __init__( self, controller, asset_name=None, task_name=None, parent=None @@ -354,7 +354,6 @@ class CreateDialog(QtWidgets.QDialog): variant_hints_menu = QtWidgets.QMenu(variant_widget) variant_hints_group = QtWidgets.QActionGroup(variant_hints_menu) - # variant_hints_btn.setMenu(variant_hints_menu) variant_layout = QtWidgets.QHBoxLayout(variant_widget) variant_layout.setContentsMargins(0, 0, 0, 0) @@ -1093,6 +1092,21 @@ class CreateDialog(QtWidgets.QDialog): self.variant_input.setProperty("state", state) self.variant_input.style().polish(self.variant_input) + def _on_first_show(self): + center = self.rect().center() + + width, height = self.default_size + self.resize(width, height) + part = int(width / 7) + self._splitter_widget.setSizes( + [part * 2, part * 2, width - (part * 4)] + ) + + new_pos = self.mapToGlobal(center) + new_pos.setX(new_pos.x() - int(self.width() / 2)) + new_pos.setY(new_pos.y() - int(self.height() / 2)) + self.move(new_pos) + def moveEvent(self, event): super(CreateDialog, self).moveEvent(event) self._last_pos = self.pos() @@ -1101,13 +1115,7 @@ class CreateDialog(QtWidgets.QDialog): super(CreateDialog, self).showEvent(event) if self._first_show: self._first_show = False - width, height = self.default_size - self.resize(width, height) - - third_size = int(width / 3) - self._splitter_widget.setSizes( - [third_size, third_size, width - (2 * third_size)] - ) + self._on_first_show() if self._last_pos is not None: self.move(self._last_pos) From d82480b1ee9d8558fd4281649b1a4c8a363fa516 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 3 May 2022 16:17:30 +0200 Subject: [PATCH 168/398] fix variant value changes --- openpype/tools/publisher/widgets/widgets.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index 6c09128857..169da717f7 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -862,6 +862,8 @@ class VariantInputWidget(PlaceholderLineEdit): self._ignore_value_change = True + self._has_value_changed = False + self._origin_value = list(variants) self._current_value = list(variants) From 06a1e484d61274a6646e6b3667e76d3525f2497e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 3 May 2022 16:53:59 +0200 Subject: [PATCH 169/398] fix property changes --- openpype/tools/publisher/widgets/widgets.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index 169da717f7..63dd0ad198 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -428,6 +428,7 @@ class AssetsField(BaseClickableFrame): self._set_state_property(state) def _set_state_property(self, state): + set_style_property(self, "state", state) set_style_property(self._name_input, "state", state) set_style_property(self._icon_btn, "state", state) From 6c838de7ffecf6a49df97fb76b0ae95a56fc3062 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 3 May 2022 16:55:58 +0200 Subject: [PATCH 170/398] Add Houdini loader to load Alembic through Alembic Archive node --- .../plugins/load/load_alembic_archive.py | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 openpype/hosts/houdini/plugins/load/load_alembic_archive.py diff --git a/openpype/hosts/houdini/plugins/load/load_alembic_archive.py b/openpype/hosts/houdini/plugins/load/load_alembic_archive.py new file mode 100644 index 0000000000..b960073e12 --- /dev/null +++ b/openpype/hosts/houdini/plugins/load/load_alembic_archive.py @@ -0,0 +1,75 @@ +import os +from openpype.pipeline import ( + load, + get_representation_path, +) +from openpype.hosts.houdini.api import pipeline + + +class AbcArchiveLoader(load.LoaderPlugin): + """Load Alembic as full geometry network hierarchy """ + + families = ["model", "animation", "pointcache", "gpuCache"] + label = "Load Alembic as Archive" + representations = ["abc"] + order = -5 + icon = "code-fork" + color = "orange" + + def load(self, context, name=None, namespace=None, data=None): + + import hou + + # Format file name, Houdini only wants forward slashes + file_path = os.path.normpath(self.fname) + file_path = file_path.replace("\\", "/") + + # Get the root node + obj = hou.node("/obj") + + # Define node name + namespace = namespace if namespace else context["asset"]["name"] + node_name = "{}_{}".format(namespace, name) if namespace else name + + # Create an Alembic archive node + node = obj.createNode("alembicarchive", node_name=node_name) + node.moveToGoodPosition() + + # TODO: add FPS of project / asset + node.setParms({"fileName": file_path, + "channelRef": True}) + + # Apply some magic + node.parm("buildHierarchy").pressButton() + node.moveToGoodPosition() + + nodes = [node] + + self[:] = nodes + + return pipeline.containerise(node_name, + namespace, + nodes, + context, + self.__class__.__name__, + suffix="") + + def update(self, container, representation): + + node = container["node"] + + # Update the file path + file_path = get_representation_path(representation) + file_path = file_path.replace("\\", "/") + + # Update attributes + node.setParms({"fileName": file_path, + "representation": str(representation["_id"])}) + + # Rebuild + node.parm("buildHierarchy").pressButton() + + def remove(self, container): + + node = container["node"] + node.destroy() From 50e19bbfc075dac637e1cddc775d7cb092f54c8c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 3 May 2022 17:20:42 +0200 Subject: [PATCH 171/398] create attributes use same grid layout logic --- openpype/tools/publisher/widgets/widgets.py | 30 ++++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index 63dd0ad198..7096b9fb50 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -1272,7 +1272,11 @@ class CreatorAttrsWidget(QtWidgets.QWidget): ) content_widget = QtWidgets.QWidget(self._scroll_area) - content_layout = QtWidgets.QFormLayout(content_widget) + content_layout = QtWidgets.QGridLayout(content_widget) + content_layout.setColumnStretch(0, 0) + content_layout.setColumnStretch(1, 1) + + row = 0 for attr_def, attr_instances, values in result: widget = create_widget_for_attr_def(attr_def, content_widget) if attr_def.is_value_def: @@ -1283,10 +1287,28 @@ class CreatorAttrsWidget(QtWidgets.QWidget): else: widget.set_value(values, True) - label = attr_def.label or attr_def.key - content_layout.addRow(label, widget) - widget.value_changed.connect(self._input_value_changed) + expand_cols = 2 + if attr_def.is_value_def and attr_def.is_label_horizontal: + expand_cols = 1 + col_num = 2 - expand_cols + + label = attr_def.label or attr_def.key + if label: + label_widget = QtWidgets.QLabel(label, self) + content_layout.addWidget( + label_widget, row, 0, 1, expand_cols + ) + if not attr_def.is_label_horizontal: + row += 1 + + content_layout.addWidget( + widget, row, col_num, 1, expand_cols + ) + + row += 1 + + widget.value_changed.connect(self._input_value_changed) self._attr_def_id_to_instances[attr_def.id] = attr_instances self._attr_def_id_to_attr_def[attr_def.id] = attr_def From 25fd05df9ea82f4698cc03d113b3b149d9bf2536 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 3 May 2022 16:29:45 +0100 Subject: [PATCH 172/398] Updated plugin for UE5 --- .../Source/OpenPype/OpenPype.Build.cs | 2 + .../Source/OpenPype/Private/OpenPype.cpp | 110 ++++++++---------- .../OpenPype/Private/OpenPypeCommands.cpp | 13 +++ .../Source/OpenPype/Private/OpenPypeStyle.cpp | 53 ++++----- .../Source/OpenPype/Public/OpenPype.h | 8 +- .../Source/OpenPype/Public/OpenPypeCommands.h | 24 ++++ .../Source/OpenPype/Public/OpenPypeStyle.h | 12 +- 7 files changed, 116 insertions(+), 106 deletions(-) create mode 100644 openpype/hosts/unreal/integration/Source/OpenPype/Private/OpenPypeCommands.cpp create mode 100644 openpype/hosts/unreal/integration/Source/OpenPype/Public/OpenPypeCommands.h diff --git a/openpype/hosts/unreal/integration/Source/OpenPype/OpenPype.Build.cs b/openpype/hosts/unreal/integration/Source/OpenPype/OpenPype.Build.cs index c30835b63d..fcfd268234 100644 --- a/openpype/hosts/unreal/integration/Source/OpenPype/OpenPype.Build.cs +++ b/openpype/hosts/unreal/integration/Source/OpenPype/OpenPype.Build.cs @@ -36,7 +36,9 @@ public class OpenPype : ModuleRules { "Projects", "InputCore", + "EditorFramework", "UnrealEd", + "ToolMenus", "LevelEditor", "CoreUObject", "Engine", diff --git a/openpype/hosts/unreal/integration/Source/OpenPype/Private/OpenPype.cpp b/openpype/hosts/unreal/integration/Source/OpenPype/Private/OpenPype.cpp index 15c46b3862..b3bd9a81b3 100644 --- a/openpype/hosts/unreal/integration/Source/OpenPype/Private/OpenPype.cpp +++ b/openpype/hosts/unreal/integration/Source/OpenPype/Private/OpenPype.cpp @@ -1,7 +1,10 @@ #include "OpenPype.h" -#include "LevelEditor.h" -#include "OpenPypePythonBridge.h" #include "OpenPypeStyle.h" +#include "OpenPypeCommands.h" +#include "OpenPypePythonBridge.h" +#include "LevelEditor.h" +#include "Misc/MessageDialog.h" +#include "ToolMenus.h" static const FName OpenPypeTabName("OpenPype"); @@ -11,82 +14,61 @@ static const FName OpenPypeTabName("OpenPype"); // This function is triggered when the plugin is staring up void FOpenPypeModule::StartupModule() { - FOpenPypeStyle::Initialize(); - FOpenPypeStyle::SetIcon("Logo", "openpype40"); + FOpenPypeStyle::ReloadTextures(); + FOpenPypeCommands::Register(); - // Create the Extender that will add content to the menu - FLevelEditorModule& LevelEditorModule = FModuleManager::LoadModuleChecked("LevelEditor"); - - TSharedPtr MenuExtender = MakeShareable(new FExtender()); - TSharedPtr ToolbarExtender = MakeShareable(new FExtender()); + PluginCommands = MakeShareable(new FUICommandList); - MenuExtender->AddMenuExtension( - "LevelEditor", - EExtensionHook::After, - NULL, - FMenuExtensionDelegate::CreateRaw(this, &FOpenPypeModule::AddMenuEntry) - ); - ToolbarExtender->AddToolBarExtension( - "Settings", - EExtensionHook::After, - NULL, - FToolBarExtensionDelegate::CreateRaw(this, &FOpenPypeModule::AddToobarEntry)); - - - LevelEditorModule.GetMenuExtensibilityManager()->AddExtender(MenuExtender); - LevelEditorModule.GetToolBarExtensibilityManager()->AddExtender(ToolbarExtender); + PluginCommands->MapAction( + FOpenPypeCommands::Get().OpenPypeTools, + FExecuteAction::CreateRaw(this, &FOpenPypeModule::MenuPopup), + FCanExecuteAction()); + PluginCommands->MapAction( + FOpenPypeCommands::Get().OpenPypeToolsDialog, + FExecuteAction::CreateRaw(this, &FOpenPypeModule::MenuDialog), + FCanExecuteAction()); + UToolMenus::RegisterStartupCallback(FSimpleMulticastDelegate::FDelegate::CreateRaw(this, &FOpenPypeModule::RegisterMenus)); } void FOpenPypeModule::ShutdownModule() { + UToolMenus::UnRegisterStartupCallback(this); + + UToolMenus::UnregisterOwner(this); + FOpenPypeStyle::Shutdown(); + + FOpenPypeCommands::Unregister(); } - -void FOpenPypeModule::AddMenuEntry(FMenuBuilder& MenuBuilder) +void FOpenPypeModule::RegisterMenus() { - // Create Section - MenuBuilder.BeginSection("OpenPype", TAttribute(FText::FromString("OpenPype"))); + // Owner will be used for cleanup in call to UToolMenus::UnregisterOwner + FToolMenuOwnerScoped OwnerScoped(this); + { - // Create a Submenu inside of the Section - MenuBuilder.AddMenuEntry( - FText::FromString("Tools..."), - FText::FromString("Pipeline tools"), - FSlateIcon(FOpenPypeStyle::GetStyleSetName(), "OpenPype.Logo"), - FUIAction(FExecuteAction::CreateRaw(this, &FOpenPypeModule::MenuPopup)) - ); - - MenuBuilder.AddMenuEntry( - FText::FromString("Tools dialog..."), - FText::FromString("Pipeline tools dialog"), - FSlateIcon(FOpenPypeStyle::GetStyleSetName(), "OpenPype.Logo"), - FUIAction(FExecuteAction::CreateRaw(this, &FOpenPypeModule::MenuDialog)) - ); - + UToolMenu* Menu = UToolMenus::Get()->ExtendMenu("LevelEditor.MainMenu.Tools"); + { + // FToolMenuSection& Section = Menu->FindOrAddSection("OpenPype"); + FToolMenuSection& Section = Menu->AddSection( + "OpenPype", + TAttribute(FText::FromString("OpenPype")), + FToolMenuInsert("Programming", EToolMenuInsertType::Before) + ); + Section.AddMenuEntryWithCommandList(FOpenPypeCommands::Get().OpenPypeTools, PluginCommands); + Section.AddMenuEntryWithCommandList(FOpenPypeCommands::Get().OpenPypeToolsDialog, PluginCommands); + } + UToolMenu* ToolbarMenu = UToolMenus::Get()->ExtendMenu("LevelEditor.LevelEditorToolBar.PlayToolBar"); + { + FToolMenuSection& Section = ToolbarMenu->FindOrAddSection("PluginTools"); + { + FToolMenuEntry& Entry = Section.AddEntry(FToolMenuEntry::InitToolBarButton(FOpenPypeCommands::Get().OpenPypeTools)); + Entry.SetCommandList(PluginCommands); + } + } } - MenuBuilder.EndSection(); -} - -void FOpenPypeModule::AddToobarEntry(FToolBarBuilder& ToolbarBuilder) -{ - ToolbarBuilder.BeginSection(TEXT("OpenPype")); - { - ToolbarBuilder.AddToolBarButton( - FUIAction( - FExecuteAction::CreateRaw(this, &FOpenPypeModule::MenuPopup), - NULL, - FIsActionChecked() - - ), - NAME_None, - LOCTEXT("OpenPype_label", "OpenPype"), - LOCTEXT("OpenPype_tooltip", "OpenPype Tools"), - FSlateIcon(FOpenPypeStyle::GetStyleSetName(), "OpenPype.Logo") - ); - } - ToolbarBuilder.EndSection(); } diff --git a/openpype/hosts/unreal/integration/Source/OpenPype/Private/OpenPypeCommands.cpp b/openpype/hosts/unreal/integration/Source/OpenPype/Private/OpenPypeCommands.cpp new file mode 100644 index 0000000000..6187bd7c7e --- /dev/null +++ b/openpype/hosts/unreal/integration/Source/OpenPype/Private/OpenPypeCommands.cpp @@ -0,0 +1,13 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "OpenPypeCommands.h" + +#define LOCTEXT_NAMESPACE "FOpenPypeModule" + +void FOpenPypeCommands::RegisterCommands() +{ + UI_COMMAND(OpenPypeTools, "OpenPype Tools", "Pipeline tools", EUserInterfaceActionType::Button, FInputChord()); + UI_COMMAND(OpenPypeToolsDialog, "OpenPype Tools Dialog", "Pipeline tools dialog", EUserInterfaceActionType::Button, FInputChord()); +} + +#undef LOCTEXT_NAMESPACE diff --git a/openpype/hosts/unreal/integration/Source/OpenPype/Private/OpenPypeStyle.cpp b/openpype/hosts/unreal/integration/Source/OpenPype/Private/OpenPypeStyle.cpp index a51c2d6aa5..4a53af26b5 100644 --- a/openpype/hosts/unreal/integration/Source/OpenPype/Private/OpenPypeStyle.cpp +++ b/openpype/hosts/unreal/integration/Source/OpenPype/Private/OpenPypeStyle.cpp @@ -1,10 +1,14 @@ +#include "OpenPype.h" #include "OpenPypeStyle.h" #include "Framework/Application/SlateApplication.h" -#include "Styling/SlateStyle.h" #include "Styling/SlateStyleRegistry.h" +#include "Slate/SlateGameResources.h" +#include "Interfaces/IPluginManager.h" +#include "Styling/SlateStyleMacros.h" +#define RootToContentDir Style->RootToContentDir -TUniquePtr< FSlateStyleSet > FOpenPypeStyle::OpenPypeStyleInstance = nullptr; +TSharedPtr FOpenPypeStyle::OpenPypeStyleInstance = nullptr; void FOpenPypeStyle::Initialize() { @@ -17,11 +21,9 @@ void FOpenPypeStyle::Initialize() void FOpenPypeStyle::Shutdown() { - if (OpenPypeStyleInstance.IsValid()) - { - FSlateStyleRegistry::UnRegisterSlateStyle(*OpenPypeStyleInstance); - OpenPypeStyleInstance.Reset(); - } + FSlateStyleRegistry::UnRegisterSlateStyle(*OpenPypeStyleInstance); + ensure(OpenPypeStyleInstance.IsUnique()); + OpenPypeStyleInstance.Reset(); } FName FOpenPypeStyle::GetStyleSetName() @@ -30,41 +32,30 @@ FName FOpenPypeStyle::GetStyleSetName() return StyleSetName; } -FName FOpenPypeStyle::GetContextName() -{ - static FName ContextName(TEXT("OpenPype")); - return ContextName; -} - -#define IMAGE_BRUSH(RelativePath, ...) FSlateImageBrush( Style->RootToContentDir( RelativePath, TEXT(".png") ), __VA_ARGS__ ) - +const FVector2D Icon16x16(16.0f, 16.0f); +const FVector2D Icon20x20(20.0f, 20.0f); const FVector2D Icon40x40(40.0f, 40.0f); -TUniquePtr< FSlateStyleSet > FOpenPypeStyle::Create() +TSharedRef< FSlateStyleSet > FOpenPypeStyle::Create() { - TUniquePtr< FSlateStyleSet > Style = MakeUnique(GetStyleSetName()); - Style->SetContentRoot(FPaths::ProjectPluginsDir() / TEXT("OpenPype/Resources")); + TSharedRef< FSlateStyleSet > Style = MakeShareable(new FSlateStyleSet("OpenPypeStyle")); + Style->SetContentRoot(IPluginManager::Get().FindPlugin("OpenPype")->GetBaseDir() / TEXT("Resources")); + + Style->Set("OpenPype.OpenPypeTools", new IMAGE_BRUSH(TEXT("openpype40"), Icon40x40)); + Style->Set("OpenPype.OpenPypeToolsDialog", new IMAGE_BRUSH(TEXT("openpype40"), Icon40x40)); return Style; } -void FOpenPypeStyle::SetIcon(const FString& StyleName, const FString& ResourcePath) +void FOpenPypeStyle::ReloadTextures() { - FSlateStyleSet* Style = OpenPypeStyleInstance.Get(); - - FString Name(GetContextName().ToString()); - Name = Name + "." + StyleName; - Style->Set(*Name, new FSlateImageBrush(Style->RootToContentDir(ResourcePath, TEXT(".png")), Icon40x40)); - - - FSlateApplication::Get().GetRenderer()->ReloadTextureResources(); + if (FSlateApplication::IsInitialized()) + { + FSlateApplication::Get().GetRenderer()->ReloadTextureResources(); + } } -#undef IMAGE_BRUSH - const ISlateStyle& FOpenPypeStyle::Get() { - check(OpenPypeStyleInstance); - return *OpenPypeStyleInstance; return *OpenPypeStyleInstance; } diff --git a/openpype/hosts/unreal/integration/Source/OpenPype/Public/OpenPype.h b/openpype/hosts/unreal/integration/Source/OpenPype/Public/OpenPype.h index db3f299354..3ee5eaa65f 100644 --- a/openpype/hosts/unreal/integration/Source/OpenPype/Public/OpenPype.h +++ b/openpype/hosts/unreal/integration/Source/OpenPype/Public/OpenPype.h @@ -2,7 +2,8 @@ #pragma once -#include "Engine.h" +#include "CoreMinimal.h" +#include "Modules/ModuleManager.h" class FOpenPypeModule : public IModuleInterface @@ -12,10 +13,11 @@ public: virtual void ShutdownModule() override; private: + void RegisterMenus(); - void AddMenuEntry(FMenuBuilder& MenuBuilder); - void AddToobarEntry(FToolBarBuilder& ToolbarBuilder); void MenuPopup(); void MenuDialog(); +private: + TSharedPtr PluginCommands; }; diff --git a/openpype/hosts/unreal/integration/Source/OpenPype/Public/OpenPypeCommands.h b/openpype/hosts/unreal/integration/Source/OpenPype/Public/OpenPypeCommands.h new file mode 100644 index 0000000000..62ffb8de33 --- /dev/null +++ b/openpype/hosts/unreal/integration/Source/OpenPype/Public/OpenPypeCommands.h @@ -0,0 +1,24 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Framework/Commands/Commands.h" +#include "OpenPypeStyle.h" + +class FOpenPypeCommands : public TCommands +{ +public: + + FOpenPypeCommands() + : TCommands(TEXT("OpenPype"), NSLOCTEXT("Contexts", "OpenPype", "OpenPype Tools"), NAME_None, FOpenPypeStyle::GetStyleSetName()) + { + } + + // TCommands<> interface + virtual void RegisterCommands() override; + +public: + TSharedPtr< FUICommandInfo > OpenPypeTools; + TSharedPtr< FUICommandInfo > OpenPypeToolsDialog; +}; diff --git a/openpype/hosts/unreal/integration/Source/OpenPype/Public/OpenPypeStyle.h b/openpype/hosts/unreal/integration/Source/OpenPype/Public/OpenPypeStyle.h index fbc8bcdd5b..ae704251e1 100644 --- a/openpype/hosts/unreal/integration/Source/OpenPype/Public/OpenPypeStyle.h +++ b/openpype/hosts/unreal/integration/Source/OpenPype/Public/OpenPypeStyle.h @@ -1,22 +1,18 @@ #pragma once #include "CoreMinimal.h" - -class FSlateStyleSet; -class ISlateStyle; - +#include "Styling/SlateStyle.h" class FOpenPypeStyle { public: static void Initialize(); static void Shutdown(); + static void ReloadTextures(); static const ISlateStyle& Get(); static FName GetStyleSetName(); - static FName GetContextName(); - static void SetIcon(const FString& StyleName, const FString& ResourcePath); private: - static TUniquePtr< FSlateStyleSet > Create(); - static TUniquePtr< FSlateStyleSet > OpenPypeStyleInstance; + static TSharedRef< class FSlateStyleSet > Create(); + static TSharedPtr< class FSlateStyleSet > OpenPypeStyleInstance; }; \ No newline at end of file From 8db65d56cc16bce55d6588a627ef5656c23a71fe Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 3 May 2022 17:31:51 +0200 Subject: [PATCH 173/398] hound fix --- openpype/tools/publisher/widgets/create_dialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/publisher/widgets/create_dialog.py b/openpype/tools/publisher/widgets/create_dialog.py index 09b2b9cb30..9e357f3a56 100644 --- a/openpype/tools/publisher/widgets/create_dialog.py +++ b/openpype/tools/publisher/widgets/create_dialog.py @@ -774,7 +774,7 @@ class CreateDialog(QtWidgets.QDialog): self._help_btn.set_pos_and_size( max(0, pos_x), max(0, pos_y), - width, height + width, height ) def _on_help_btn_resize(self, height): From 289e35d68d9c7d5bd6a95a07100f7e82fcc59201 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 3 May 2022 17:33:08 +0200 Subject: [PATCH 174/398] added stretches --- openpype/widgets/attribute_defs/widgets.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/widgets/attribute_defs/widgets.py b/openpype/widgets/attribute_defs/widgets.py index 875b69acb4..b6493b80a8 100644 --- a/openpype/widgets/attribute_defs/widgets.py +++ b/openpype/widgets/attribute_defs/widgets.py @@ -91,6 +91,8 @@ class AttributeDefinitionsWidget(QtWidgets.QWidget): layout.deleteLater() new_layout = QtWidgets.QGridLayout() + new_layout.setColumnStretch(0, 0) + new_layout.setColumnStretch(1, 1) self.setLayout(new_layout) def set_attr_defs(self, attr_defs): From 0dd91e8736b222651e3a158cc9d9dc266b052bd7 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 3 May 2022 22:22:49 +0200 Subject: [PATCH 175/398] Nuke: adding `formatable` to node knob keys --- openpype/hosts/nuke/api/lib.py | 33 ++++++++++++++--- .../schemas/template_nuke_knob_inputs.json | 36 +++++++++++++++++++ 2 files changed, 65 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 0fa5633b90..475eb006ff 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -1059,12 +1059,12 @@ def create_write_node(name, data, input=None, prenodes=None, GN["tile_color"].setValue(tile_color) # finally add knob overrides - set_node_knobs_from_settings(GN, knob_overrides) + set_node_knobs_from_settings(GN, knob_overrides, **kwargs) return GN -def set_node_knobs_from_settings(node, knob_settings): +def set_node_knobs_from_settings(node, knob_settings, **kwargs): """ Overriding knob values from settings Using `schema_nuke_knob_inputs` for knob type definitions. @@ -1072,13 +1072,37 @@ def set_node_knobs_from_settings(node, knob_settings): Args: node (nuke.Node): nuke node knob_settings (list): list of dict. Keys are `type`, `name`, `value` + kwargs (dict)[optional]: keys for formatable knob settings """ for knob in knob_settings: knob_type = knob["type"] knob_name = knob["name"] - knob_value = knob["value"] + if knob_name not in node.knobs(): continue + + # first deal with formatable knob settings + if knob_type == "formatable": + template = knob["template"] + to_type = knob["to_type"] + try: + _knob_value = template.format( + **kwargs + ) + except KeyError as msg: + raise KeyError(msg) + + # convert value to correct type + if to_type == "2d_vector": + knob_value = _knob_value.split(";").split(",") + else: + knob_value = _knob_value + + knob_type = to_type + + else: + knob_value = knob["value"] + if not knob_value: continue @@ -1103,9 +1127,10 @@ def set_node_knobs_from_settings(node, knob_settings): pformat(knob_settings) )) knob_value = int(knob_value, 16) - if knob_type in ["2d_vector", "3d_vector", "color"]: + elif knob_type in ["2d_vector", "3d_vector", "color"]: knob_value = [float(v) for v in knob_value] + node[knob_name].setValue(knob_value) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/template_nuke_knob_inputs.json b/openpype/settings/entities/schemas/projects_schema/schemas/template_nuke_knob_inputs.json index 957c684013..e6ca1e7fd4 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/template_nuke_knob_inputs.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/template_nuke_knob_inputs.json @@ -28,6 +28,42 @@ } ] }, + { + "key": "formatable", + "label": "Formate from template", + "children": [ + { + "type": "text", + "key": "name", + "label": "Name" + }, + { + "type": "text", + "key": "template", + "label": "Template", + "placeholder": "{{key}} or {{key}};{{key}}" + }, + { + "type": "enum", + "key": "to_type", + "label": "Knob type", + "enum_items": [ + { + "text": "Text" + }, + { + "number": "Number" + }, + { + "decimal_number": "Decimal number" + }, + { + "2d_vector": "2D vector" + } + ] + } + ] + }, { "key": "hex", "label": "Hexadecimal (0x)", From 3ddb46463c67025d4a46a70e4f3ee545ab99ac8f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 3 May 2022 22:26:52 +0200 Subject: [PATCH 176/398] nuke: updating create_write_node inputs and docs --- openpype/hosts/nuke/api/lib.py | 62 ++++++++++++++++++++++------------ 1 file changed, 40 insertions(+), 22 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 475eb006ff..4170eaf1e7 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -812,39 +812,54 @@ def add_button_clear_rendered(node, path): node.addKnob(knob) -def create_write_node(name, data, input=None, prenodes=None, - review=True, linked_knobs=None, farm=True): +def create_write_node( + name, + data, + input=None, + prenodes=None, + review=True, + farm=True, + linked_knobs=None, + **kwargs +): ''' Creating write node which is group node Arguments: name (str): name of node - data (dict): data to be imprinted - input (node): selected node to connect to - prenodes (list, optional): list of lists, definitions for nodes - to be created before write - review (bool): adding review knob + data (dict): creator write instance data + input (node)[optional]: selected node to connect to + prenodes (dict)[optional]: + nodes to be created before write with dependency + review (bool)[optional]: adding review knob + farm (bool)[optional]: rendering workflow target + kwargs (dict)[optional]: additional key arguments for formating Example: - prenodes = [ - { - "nodeName": { - "class": "" # string - "knobs": [ - ("knobName": value), - ... - ], - "dependent": [ - following_node_01, - ... - ] - } + prenodes = { + "nodeName": { + "nodeclass": "Reformat", + "dependent": [ + following_node_01, + ... + ], + "knobs": [ + { + "type": "text", + "name": "knobname", + "value": "knob value" + }, + ... + ] }, ... - ] + } + Return: node (obj): group node with avalon data as Knobs ''' + prenodes = prenodes or {} + # group node knob overrides knob_overrides = data.get("knobs", []) @@ -880,7 +895,9 @@ def create_write_node(name, data, input=None, prenodes=None, # build file path to workfiles fdir = str(anatomy_filled["work"]["folder"]).replace("\\", "/") fpath = data["fpath_template"].format( - work=fdir, version=data["version"], subset=data["subset"], + work=fdir, + version=data["version"], + subset=data["subset"], frame=data["frame"], ext=representation ) @@ -919,6 +936,7 @@ def create_write_node(name, data, input=None, prenodes=None, prev_node = nuke.createNode( "Input", "name {}".format("rgba")) prev_node.hideControlPanel() + # creating pre-write nodes `prenodes` if prenodes: for node in prenodes: From 3fd5a534c6747f3b9ceb5b333112c58a7762b7d0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 3 May 2022 22:28:05 +0200 Subject: [PATCH 177/398] nuke: adding prenodes to settings - also adding write still plugin to settings --- .../defaults/project_settings/nuke.json | 48 ++++++- .../projects_schema/schema_project_nuke.json | 125 ++++++++++++++++++ 2 files changed, 170 insertions(+), 3 deletions(-) diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index 4fc6caa57e..057d950b54 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -22,10 +22,28 @@ "Main", "Mask" ], - "knobs": [] + "knobs": [], + "prenodes": { + "Reformat01": { + "nodeclass": "Reformat", + "dependent": "", + "knobs": [ + { + "type": "text", + "name": "resize", + "value": "none" + }, + { + "type": "bool", + "name": "black_outside", + "value": true + } + ] + } + } }, "CreateWritePrerender": { - "fpath_template": "{work}/prerenders/nuke/{subset}/{subset}.{frame}.{ext}", + "fpath_template": "{work}/renders/nuke/{subset}/{subset}.{frame}.{ext}", "use_range_limit": true, "defaults": [ "Key01", @@ -35,7 +53,31 @@ "Part01" ], "reviewable": false, - "knobs": [] + "knobs": [], + "prenodes": {} + }, + "CreateWriteStill": { + "fpath_template": "{work}/renders/nuke/{subset}/{subset}.{ext}", + "defaults": [ + "ImageFrame{frame:0>4}", + "MPFrame{frame:0>4}", + "LayoutFrame{frame:0>4}" + ], + "knobs": [], + "prenodes": { + "FrameHold01": { + "nodeclass": "FrameHold", + "dependent": "", + "knobs": [ + { + "type": "formatable", + "name": "FrameHold", + "template": "{frame}", + "to_type": "number" + } + ] + } + } } }, "publish": { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json b/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json index 29ad8e3c6c..bc572cbdc8 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json @@ -114,6 +114,37 @@ "key": "knobs" } ] + }, + { + "key": "prenodes", + "label": "Pre write nodes", + "type": "dict-modifiable", + "highlight_content": true, + "object_type": { + "type": "dict", + "children": [ + { + "key": "nodeclass", + "label": "Node class", + "type": "text" + }, + { + "key": "dependent", + "label": "Outside node dependency", + "type": "text" + }, + { + "type": "schema_template", + "name": "template_nuke_knob_inputs", + "template_data": [ + { + "label": "Node knobs", + "key": "knobs" + } + ] + } + ] + } } ] }, @@ -156,6 +187,100 @@ "key": "knobs" } ] + }, + { + "key": "prenodes", + "label": "Pre write nodes", + "type": "dict-modifiable", + "highlight_content": true, + "object_type": { + "type": "dict", + "children": [ + { + "key": "nodeclass", + "label": "Node class", + "type": "text" + }, + { + "key": "dependent", + "label": "Outside node dependency", + "type": "text" + }, + { + "type": "schema_template", + "name": "template_nuke_knob_inputs", + "template_data": [ + { + "label": "Node knobs", + "key": "knobs" + } + ] + } + ] + } + } + ] + }, + { + "type": "dict", + "collapsible": true, + "key": "CreateWriteStill", + "label": "CreateWriteStill", + "is_group": true, + "children": [ + { + "type": "text", + "key": "fpath_template", + "label": "Path template" + }, + { + "type": "list", + "key": "defaults", + "label": "Subset name defaults", + "object_type": { + "type": "text" + } + }, + { + "type": "schema_template", + "name": "template_nuke_knob_inputs", + "template_data": [ + { + "label": "Node knobs", + "key": "knobs" + } + ] + }, + { + "key": "prenodes", + "label": "Pre write nodes", + "type": "dict-modifiable", + "highlight_content": true, + "object_type": { + "type": "dict", + "children": [ + { + "key": "nodeclass", + "label": "Node class", + "type": "text" + }, + { + "key": "dependent", + "label": "Outside node dependency", + "type": "text" + }, + { + "type": "schema_template", + "name": "template_nuke_knob_inputs", + "template_data": [ + { + "label": "Node knobs", + "key": "knobs" + } + ] + } + ] + } } ] } From 890e10836825968b61cd1f524d7c399c394173e0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 3 May 2022 22:28:51 +0200 Subject: [PATCH 178/398] nuke: updating plugins with new settings with prenodes removing self.preset references --- openpype/hosts/nuke/api/plugin.py | 25 +++------ .../plugins/create/create_write_prerender.py | 21 ++++--- .../plugins/create/create_write_render.py | 43 +++++++++----- .../nuke/plugins/create/create_write_still.py | 56 +++++++++++-------- 4 files changed, 82 insertions(+), 63 deletions(-) diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index 37c3633d2c..1eaccef795 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -27,9 +27,6 @@ class OpenPypeCreator(LegacyCreator): def __init__(self, *args, **kwargs): super(OpenPypeCreator, self).__init__(*args, **kwargs) - self.presets = get_current_project_settings()["nuke"]["create"].get( - self.__class__.__name__, {} - ) if check_subsetname_exists( nuke.allNodes(), self.data["subset"]): @@ -606,6 +603,7 @@ class AbstractWriteRender(OpenPypeCreator): icon = "sign-out" defaults = ["Main", "Mask"] knobs = [] + prenodes = {} def __init__(self, *args, **kwargs): super(AbstractWriteRender, self).__init__(*args, **kwargs) @@ -682,21 +680,12 @@ class AbstractWriteRender(OpenPypeCreator): self.data.update(creator_data) write_data.update(creator_data) - if self.presets.get('fpath_template'): - self.log.info("Adding template path from preset") - write_data.update( - {"fpath_template": self.presets["fpath_template"]} - ) - else: - self.log.info("Adding template path from plugin") - write_data.update({ - "fpath_template": - ("{work}/" + self.family + "s/nuke/{subset}" - "/{subset}.{frame}.{ext}")}) - - write_node = self._create_write_node(selected_node, - inputs, outputs, - write_data) + write_node = self._create_write_node( + selected_node, + inputs, + outputs, + write_data + ) # relinking to collected connections for i, input in enumerate(inputs): diff --git a/openpype/hosts/nuke/plugins/create/create_write_prerender.py b/openpype/hosts/nuke/plugins/create/create_write_prerender.py index 7297f74c13..f86ed7b89e 100644 --- a/openpype/hosts/nuke/plugins/create/create_write_prerender.py +++ b/openpype/hosts/nuke/plugins/create/create_write_prerender.py @@ -12,22 +12,27 @@ class CreateWritePrerender(plugin.AbstractWriteRender): n_class = "Write" family = "prerender" icon = "sign-out" + + # settings + fpath_template = "{work}/render/nuke/{subset}/{subset}.{frame}.{ext}" defaults = ["Key01", "Bg01", "Fg01", "Branch01", "Part01"] + reviewable = False + use_range_limit = True def __init__(self, *args, **kwargs): super(CreateWritePrerender, self).__init__(*args, **kwargs) def _create_write_node(self, selected_node, inputs, outputs, write_data): - reviewable = self.presets.get("reviewable") - write_node = create_write_node( + # add fpath_template + write_data["fpath_template"] = self.fpath_template + + return create_write_node( self.data["subset"], write_data, input=selected_node, - prenodes=[], - review=reviewable, - linked_knobs=["channels", "___", "first", "last", "use_limit"]) - - return write_node + review=self.reviewable, + linked_knobs=["channels", "___", "first", "last", "use_limit"] + ) def _modify_write_node(self, write_node): # open group node @@ -38,7 +43,7 @@ class CreateWritePrerender(plugin.AbstractWriteRender): w_node = n write_node.end() - if self.presets.get("use_range_limit"): + if self.use_range_limit: w_node["use_limit"].setValue(True) w_node["first"].setValue(nuke.root()["first_frame"].value()) w_node["last"].setValue(nuke.root()["last_frame"].value()) diff --git a/openpype/hosts/nuke/plugins/create/create_write_render.py b/openpype/hosts/nuke/plugins/create/create_write_render.py index 36a7b5c33f..43b1a9dcb4 100644 --- a/openpype/hosts/nuke/plugins/create/create_write_render.py +++ b/openpype/hosts/nuke/plugins/create/create_write_render.py @@ -12,13 +12,36 @@ class CreateWriteRender(plugin.AbstractWriteRender): n_class = "Write" family = "render" icon = "sign-out" + + # settings + fpath_template = "{work}/render/nuke/{subset}/{subset}.{frame}.{ext}" defaults = ["Main", "Mask"] - knobs = [] + prenodes = { + "Reformat01": { + "nodeclass": "Reformat", + "dependent": None, + "knobs": [ + { + "type": "text", + "name": "resize", + "value": "none" + }, + { + "type": "bool", + "name": "black_outside", + "value": True + } + ] + } + } def __init__(self, *args, **kwargs): super(CreateWriteRender, self).__init__(*args, **kwargs) def _create_write_node(self, selected_node, inputs, outputs, write_data): + # add fpath_template + write_data["fpath_template"] = self.fpath_template + # add reformat node to cut off all outside of format bounding box # get width and height try: @@ -27,23 +50,15 @@ class CreateWriteRender(plugin.AbstractWriteRender): actual_format = nuke.root().knob('format').value() width, height = (actual_format.width(), actual_format.height()) - _prenodes = [ - { - "name": "Reformat01", - "class": "Reformat", - "knobs": [ - ("resize", 0), - ("black_outside", 1), - ], - "dependent": None - } - ] - return create_write_node( self.data["subset"], write_data, input=selected_node, - prenodes=_prenodes + prenodes=self.prenodes, + **{ + "width": width, + "height": height + } ) def _modify_write_node(self, write_node): diff --git a/openpype/hosts/nuke/plugins/create/create_write_still.py b/openpype/hosts/nuke/plugins/create/create_write_still.py index d22b5eab3f..4966344b82 100644 --- a/openpype/hosts/nuke/plugins/create/create_write_still.py +++ b/openpype/hosts/nuke/plugins/create/create_write_still.py @@ -12,42 +12,52 @@ class CreateWriteStill(plugin.AbstractWriteRender): n_class = "Write" family = "still" icon = "image" + + # settings + fpath_template = "{work}/render/nuke/{subset}/{subset}.{ext}" defaults = [ - "ImageFrame{:0>4}".format(nuke.frame()), - "MPFrame{:0>4}".format(nuke.frame()), - "LayoutFrame{:0>4}".format(nuke.frame()) + "ImageFrame{frame:0>4}", + "MPFrame{frame:0>4}", + "LayoutFrame{frame:0>4}" ] + prenodes = { + "FrameHold01": { + "nodeclass": "FrameHold", + "dependent": None, + "knobs": [ + { + "type": "formatable", + "name": "first_frame", + "template": "{frame}", + "to_type": "number" + } + ] + } + } def __init__(self, *args, **kwargs): + # format defaults + new_defaults = [] + for _d in self.defaults: + new_d = _d.format(frame=nuke.frame()) + new_defaults.append(new_d) + self.defaults = new_defaults + super(CreateWriteStill, self).__init__(*args, **kwargs) def _create_write_node(self, selected_node, inputs, outputs, write_data): - # explicitly reset template to 'renders', not same as other 2 writes - write_data.update({ - "fpath_template": ( - "{work}/renders/nuke/{subset}/{subset}.{ext}")}) + # add fpath_template + write_data["fpath_template"] = self.fpath_template - _prenodes = [ - { - "name": "FrameHold01", - "class": "FrameHold", - "knobs": [ - ("first_frame", nuke.frame()) - ], - "dependent": None - } - ] - - write_node = create_write_node( + return create_write_node( self.name, write_data, input=selected_node, review=False, - prenodes=_prenodes, + prenodes=self.prenodes, farm=False, - linked_knobs=["channels", "___", "first", "last", "use_limit"]) - - return write_node + linked_knobs=["channels", "___", "first", "last", "use_limit"] + ) def _modify_write_node(self, write_node): write_node.begin() From 71e3c979768cbf24b803ef6b7995372ca4e2134a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 4 May 2022 10:45:15 +0200 Subject: [PATCH 179/398] added new session schema --- openpype/pipeline/legacy_io.py | 4 +- schema/session-3.0.json | 127 +++++++++++++++++++++++++++++++++ 2 files changed, 129 insertions(+), 2 deletions(-) create mode 100644 schema/session-3.0.json diff --git a/openpype/pipeline/legacy_io.py b/openpype/pipeline/legacy_io.py index c41406b208..b0da68f2f5 100644 --- a/openpype/pipeline/legacy_io.py +++ b/openpype/pipeline/legacy_io.py @@ -25,7 +25,7 @@ def install(): session = session_data_from_environment(context_keys=True) - session["schema"] = "openpype:session-2.0" + session["schema"] = "openpype:session-3.0" try: schema.validate(session) except schema.ValidationError as e: @@ -55,7 +55,7 @@ def uninstall(): def requires_install(func): @functools.wraps(func) def decorated(*args, **kwargs): - if not module._is_installed: + if not _is_installed: install() return func(*args, **kwargs) return decorated diff --git a/schema/session-3.0.json b/schema/session-3.0.json new file mode 100644 index 0000000000..4a89403592 --- /dev/null +++ b/schema/session-3.0.json @@ -0,0 +1,127 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + + "title": "openpype:session-3.0", + "description": "The Avalon environment", + + "type": "object", + + "additionalProperties": true, + + "required": [ + "AVALON_PROJECT", + "AVALON_ASSET" + ], + + "properties": { + "AVALON_PROJECTS": { + "description": "Absolute path to root of project directories", + "type": "string", + "example": "/nas/projects" + }, + "AVALON_PROJECT": { + "description": "Name of project", + "type": "string", + "pattern": "^\\w*$", + "example": "Hulk" + }, + "AVALON_ASSET": { + "description": "Name of asset", + "type": "string", + "pattern": "^\\w*$", + "example": "Bruce" + }, + "AVALON_SILO": { + "description": "Name of asset group or container", + "type": "string", + "pattern": "^\\w*$", + "example": "assets" + }, + "AVALON_TASK": { + "description": "Name of task", + "type": "string", + "pattern": "^\\w*$", + "example": "modeling" + }, + "AVALON_APP": { + "description": "Name of application", + "type": "string", + "pattern": "^\\w*$", + "example": "maya2016" + }, + "AVALON_DB": { + "description": "Name of database", + "type": "string", + "pattern": "^\\w*$", + "example": "avalon", + "default": "avalon" + }, + "AVALON_LABEL": { + "description": "Nice name of Avalon, used in e.g. graphical user interfaces", + "type": "string", + "example": "Mindbender", + "default": "Avalon" + }, + "AVALON_SENTRY": { + "description": "Address to Sentry", + "type": "string", + "pattern": "^http[\\w/@:.]*$", + "example": "https://5b872b280de742919b115bdc8da076a5:8d278266fe764361b8fa6024af004a9c@logs.mindbender.com/2", + "default": null + }, + "AVALON_DEADLINE": { + "description": "Address to Deadline", + "type": "string", + "pattern": "^http[\\w/@:.]*$", + "example": "http://192.168.99.101", + "default": null + }, + "AVALON_TIMEOUT": { + "description": "Wherever there is a need for a timeout, this is the default value.", + "type": "string", + "pattern": "^[0-9]*$", + "default": "1000", + "example": "1000" + }, + "AVALON_UPLOAD": { + "description": "Boolean of whether to upload published material to central asset repository", + "type": "string", + "default": null, + "example": "True" + }, + "AVALON_USERNAME": { + "description": "Generic username", + "type": "string", + "pattern": "^\\w*$", + "default": "avalon", + "example": "myself" + }, + "AVALON_PASSWORD": { + "description": "Generic password", + "type": "string", + "pattern": "^\\w*$", + "default": "secret", + "example": "abc123" + }, + "AVALON_INSTANCE_ID": { + "description": "Unique identifier for instances in a working file", + "type": "string", + "pattern": "^[\\w.]*$", + "default": "avalon.instance", + "example": "avalon.instance" + }, + "AVALON_CONTAINER_ID": { + "description": "Unique identifier for a loaded representation in a working file", + "type": "string", + "pattern": "^[\\w.]*$", + "default": "avalon.container", + "example": "avalon.container" + }, + "AVALON_DEBUG": { + "description": "Enable debugging mode. Some applications may use this for e.g. extended verbosity or mock plug-ins.", + "type": "string", + "default": null, + "example": "True" + } + } +} From 37785895972b648cdec86efddf7acf95a921f464 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 4 May 2022 10:46:05 +0200 Subject: [PATCH 180/398] reduced session keys --- schema/session-3.0.json | 48 +---------------------------------------- 1 file changed, 1 insertion(+), 47 deletions(-) diff --git a/schema/session-3.0.json b/schema/session-3.0.json index 4a89403592..9f785939e4 100644 --- a/schema/session-3.0.json +++ b/schema/session-3.0.json @@ -31,12 +31,6 @@ "pattern": "^\\w*$", "example": "Bruce" }, - "AVALON_SILO": { - "description": "Name of asset group or container", - "type": "string", - "pattern": "^\\w*$", - "example": "assets" - }, "AVALON_TASK": { "description": "Name of task", "type": "string", @@ -44,7 +38,7 @@ "example": "modeling" }, "AVALON_APP": { - "description": "Name of application", + "description": "Name of host", "type": "string", "pattern": "^\\w*$", "example": "maya2016" @@ -62,20 +56,6 @@ "example": "Mindbender", "default": "Avalon" }, - "AVALON_SENTRY": { - "description": "Address to Sentry", - "type": "string", - "pattern": "^http[\\w/@:.]*$", - "example": "https://5b872b280de742919b115bdc8da076a5:8d278266fe764361b8fa6024af004a9c@logs.mindbender.com/2", - "default": null - }, - "AVALON_DEADLINE": { - "description": "Address to Deadline", - "type": "string", - "pattern": "^http[\\w/@:.]*$", - "example": "http://192.168.99.101", - "default": null - }, "AVALON_TIMEOUT": { "description": "Wherever there is a need for a timeout, this is the default value.", "type": "string", @@ -83,26 +63,6 @@ "default": "1000", "example": "1000" }, - "AVALON_UPLOAD": { - "description": "Boolean of whether to upload published material to central asset repository", - "type": "string", - "default": null, - "example": "True" - }, - "AVALON_USERNAME": { - "description": "Generic username", - "type": "string", - "pattern": "^\\w*$", - "default": "avalon", - "example": "myself" - }, - "AVALON_PASSWORD": { - "description": "Generic password", - "type": "string", - "pattern": "^\\w*$", - "default": "secret", - "example": "abc123" - }, "AVALON_INSTANCE_ID": { "description": "Unique identifier for instances in a working file", "type": "string", @@ -116,12 +76,6 @@ "pattern": "^[\\w.]*$", "default": "avalon.container", "example": "avalon.container" - }, - "AVALON_DEBUG": { - "description": "Enable debugging mode. Some applications may use this for e.g. extended verbosity or mock plug-ins.", - "type": "string", - "default": null, - "example": "True" } } } From c18abc195b781dc756cec17ccf9543325c05bbb9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 4 May 2022 12:08:56 +0200 Subject: [PATCH 181/398] nuke: extracting prenodes creation and fixing `dependency` linking --- openpype/hosts/nuke/api/lib.py | 98 +++++++++++++++------------------- 1 file changed, 42 insertions(+), 56 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 4170eaf1e7..fc1ec7ec3d 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -812,6 +812,47 @@ def add_button_clear_rendered(node, path): node.addKnob(knob) +def create_prenodes(prev_node, nodes_setting, **kwargs): + last_node = None + for_dependency = {} + for name, node in nodes_setting.items(): + # get attributes + nodeclass = node["nodeclass"] + knobs = node["knobs"] + + # create node + now_node = nuke.createNode( + nodeclass, "name {}".format(name)) + now_node.hideControlPanel() + + # add for dependency linking + for_dependency[name] = { + "node": now_node, + "dependent": node["dependent"] + } + + # add data to knob + set_node_knobs_from_settings(now_node, knobs, **kwargs) + + # switch actual node to previous + last_node = now_node + + for _node_name, node_prop in for_dependency.items(): + if not node_prop["dependent"]: + node_prop["node"].setInput( + 0, prev_node) + elif node_prop["dependent"] in for_dependency: + _prev_node = for_dependency[node_prop["dependent"]] + node_prop["node"].setInput( + 0, _prev_node) + else: + log.warning("Dependency has wrong name of node: {}".format( + node_prop + )) + + return last_node + + def create_write_node( name, data, @@ -938,62 +979,7 @@ def create_write_node( prev_node.hideControlPanel() # creating pre-write nodes `prenodes` - if prenodes: - for node in prenodes: - # get attributes - pre_node_name = node["name"] - klass = node["class"] - knobs = node["knobs"] - dependent = node["dependent"] - - # create node - now_node = nuke.createNode( - klass, "name {}".format(pre_node_name)) - now_node.hideControlPanel() - - # add data to knob - for _knob in knobs: - knob, value = _knob - try: - now_node[knob].value() - except NameError: - log.warning( - "knob `{}` does not exist on node `{}`".format( - knob, now_node["name"].value() - )) - continue - - if not knob and not value: - continue - - log.info((knob, value)) - - if isinstance(value, str): - if "[" in value: - now_node[knob].setExpression(value) - else: - now_node[knob].setValue(value) - - # connect to previous node - if dependent: - if isinstance(dependent, (tuple or list)): - for i, node_name in enumerate(dependent): - input_node = nuke.createNode( - "Input", "name {}".format(node_name)) - input_node.hideControlPanel() - now_node.setInput(1, input_node) - - elif isinstance(dependent, str): - input_node = nuke.createNode( - "Input", "name {}".format(node_name)) - input_node.hideControlPanel() - now_node.setInput(0, input_node) - - else: - now_node.setInput(0, prev_node) - - # switch actual node to previous - prev_node = now_node + create_prenodes(prev_node, prenodes, **kwargs) # creating write node write_node = now_node = add_write_node( From bc07da3de252167ebf7c604ef7dd2fd1245414d7 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 4 May 2022 17:02:51 +0200 Subject: [PATCH 182/398] nuke: fixing create write still --- .../nuke/plugins/create/create_write_still.py | 22 +++++++++---------- .../defaults/project_settings/nuke.json | 8 +++---- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/openpype/hosts/nuke/plugins/create/create_write_still.py b/openpype/hosts/nuke/plugins/create/create_write_still.py index 4966344b82..0b1a942e0e 100644 --- a/openpype/hosts/nuke/plugins/create/create_write_still.py +++ b/openpype/hosts/nuke/plugins/create/create_write_still.py @@ -2,6 +2,10 @@ import nuke from openpype.hosts.nuke.api import plugin from openpype.hosts.nuke.api.lib import create_write_node +from openpype.api import ( + Logger +) +log = Logger.get_logger(__name__) class CreateWriteStill(plugin.AbstractWriteRender): @@ -16,9 +20,9 @@ class CreateWriteStill(plugin.AbstractWriteRender): # settings fpath_template = "{work}/render/nuke/{subset}/{subset}.{ext}" defaults = [ - "ImageFrame{frame:0>4}", - "MPFrame{frame:0>4}", - "LayoutFrame{frame:0>4}" + "ImageFrame", + "MPFrame", + "LayoutFrame" ] prenodes = { "FrameHold01": { @@ -36,13 +40,6 @@ class CreateWriteStill(plugin.AbstractWriteRender): } def __init__(self, *args, **kwargs): - # format defaults - new_defaults = [] - for _d in self.defaults: - new_d = _d.format(frame=nuke.frame()) - new_defaults.append(new_d) - self.defaults = new_defaults - super(CreateWriteStill, self).__init__(*args, **kwargs) def _create_write_node(self, selected_node, inputs, outputs, write_data): @@ -56,7 +53,10 @@ class CreateWriteStill(plugin.AbstractWriteRender): review=False, prenodes=self.prenodes, farm=False, - linked_knobs=["channels", "___", "first", "last", "use_limit"] + linked_knobs=["channels", "___", "first", "last", "use_limit"], + **{ + "frame": nuke.frame() + } ) def _modify_write_node(self, write_node): diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index 057d950b54..128d440732 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -59,9 +59,9 @@ "CreateWriteStill": { "fpath_template": "{work}/renders/nuke/{subset}/{subset}.{ext}", "defaults": [ - "ImageFrame{frame:0>4}", - "MPFrame{frame:0>4}", - "LayoutFrame{frame:0>4}" + "ImageFrame", + "MPFrame", + "LayoutFrame" ], "knobs": [], "prenodes": { @@ -71,7 +71,7 @@ "knobs": [ { "type": "formatable", - "name": "FrameHold", + "name": "first_frame", "template": "{frame}", "to_type": "number" } From a6ff0174b0a8ad0ecec5f230d860d02bd914f903 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 4 May 2022 17:03:46 +0200 Subject: [PATCH 183/398] Nuke: fixing lib functions for prenodes and others --- openpype/hosts/nuke/api/lib.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index fc1ec7ec3d..6d6a988b44 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -842,7 +842,7 @@ def create_prenodes(prev_node, nodes_setting, **kwargs): node_prop["node"].setInput( 0, prev_node) elif node_prop["dependent"] in for_dependency: - _prev_node = for_dependency[node_prop["dependent"]] + _prev_node = for_dependency[node_prop["dependent"]]["node"] node_prop["node"].setInput( 0, _prev_node) else: @@ -905,7 +905,7 @@ def create_write_node( knob_overrides = data.get("knobs", []) # filtering variables - plugin_name = data["creator"], + plugin_name = data["creator"] subset = data["subset"] # get knob settings for write node @@ -979,7 +979,9 @@ def create_write_node( prev_node.hideControlPanel() # creating pre-write nodes `prenodes` - create_prenodes(prev_node, prenodes, **kwargs) + last_prenode = create_prenodes(prev_node, prenodes, **kwargs) + if last_prenode: + prev_node = last_prenode # creating write node write_node = now_node = add_write_node( @@ -1059,8 +1061,14 @@ def create_write_node( GN[_NODE_TAB_NAME].setFlag(0) # set tile color - tile_color = _wn_props["knobs"].get("tile_color", "0xff0000ff") - GN["tile_color"].setValue(tile_color) + tile_color = next( + iter( + k["value"] for k in _wn_props["knobs"] + if "tile_color" in k["name"] + ), "0xff0000ff" + ) + GN["tile_color"].setValue( + int(tile_color, 16)) # finally add knob overrides set_node_knobs_from_settings(GN, knob_overrides, **kwargs) @@ -1079,6 +1087,7 @@ def set_node_knobs_from_settings(node, knob_settings, **kwargs): kwargs (dict)[optional]: keys for formatable knob settings """ for knob in knob_settings: + log.debug("__ knob: {}".format(pformat(knob))) knob_type = knob["type"] knob_name = knob["name"] @@ -1093,7 +1102,9 @@ def set_node_knobs_from_settings(node, knob_settings, **kwargs): _knob_value = template.format( **kwargs ) + log.debug("__ knob_value0: {}".format(_knob_value)) except KeyError as msg: + log.warning("__ msg: {}".format(msg)) raise KeyError(msg) # convert value to correct type From cb0ca1c220a231b117c3f6d35b30a8f9fb22c71b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 4 May 2022 17:38:17 +0200 Subject: [PATCH 184/398] '_is_installed' is accessed from 'module' --- openpype/pipeline/legacy_io.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/legacy_io.py b/openpype/pipeline/legacy_io.py index b0da68f2f5..c8e7e79600 100644 --- a/openpype/pipeline/legacy_io.py +++ b/openpype/pipeline/legacy_io.py @@ -55,7 +55,7 @@ def uninstall(): def requires_install(func): @functools.wraps(func) def decorated(*args, **kwargs): - if not _is_installed: + if not module._is_installed: install() return func(*args, **kwargs) return decorated From 2ec9bcfca17dcfdc8ce435c86e798475283b8f1b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 4 May 2022 17:43:17 +0200 Subject: [PATCH 185/398] reduced duplicated code when OP version is not allowed --- start.py | 31 +++++++++++++------------------ 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/start.py b/start.py index cd1d95dd8f..89c5c98d27 100644 --- a/start.py +++ b/start.py @@ -942,6 +942,17 @@ def _boot_print_versions(use_staging, local_version, openpype_root): list_versions(openpype_versions, local_version) +def _boot_handle_missing_version(local_version, use_staging, message): + _print(message) + if os.environ.get("OPENPYPE_HEADLESS_MODE") == "1": + openpype_versions = bootstrap.find_openpype( + include_zips=True, staging=use_staging + ) + list_versions(openpype_versions, local_version) + else: + igniter.show_message_dialog("Version not found", message) + + def boot(): """Bootstrap OpenPype.""" @@ -1034,15 +1045,7 @@ def boot(): try: version_path = _find_frozen_openpype(use_version, use_staging) except OpenPypeVersionNotFound as exc: - message = str(exc) - _print(message) - if os.environ.get("OPENPYPE_HEADLESS_MODE") == "1": - openpype_versions = bootstrap.find_openpype( - include_zips=True, staging=use_staging - ) - list_versions(openpype_versions, local_version) - else: - igniter.show_message_dialog("Version not found", message) + _boot_handle_missing_version(local_version, use_staging, str(exc)) sys.exit(1) except RuntimeError as e: @@ -1061,15 +1064,7 @@ def boot(): version_path = _bootstrap_from_code(use_version, use_staging) except OpenPypeVersionNotFound as exc: - message = str(exc) - _print(message) - if os.environ.get("OPENPYPE_HEADLESS_MODE") == "1": - openpype_versions = bootstrap.find_openpype( - include_zips=True, staging=use_staging - ) - list_versions(openpype_versions, local_version) - else: - igniter.show_message_dialog("Version not found", message) + _boot_handle_missing_version(local_version, use_staging, str(exc)) sys.exit(1) # set this to point either to `python` from venv in case of live code From f3cf21ebb4c4486098a2c9aea80d50656786e46b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 4 May 2022 17:45:54 +0200 Subject: [PATCH 186/398] avoid duplicated calls --- start.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/start.py b/start.py index 89c5c98d27..6e339fabab 100644 --- a/start.py +++ b/start.py @@ -911,17 +911,11 @@ def _boot_validate_versions(use_version, local_version): sys.exit(1) # print result - result = bootstrap.validate_openpype_version( - bootstrap.get_version_path_from_list( - use_version, openpype_versions)) - - _print("{}{}".format( - ">>> " if result[0] else "!!! ", - bootstrap.validate_openpype_version( - bootstrap.get_version_path_from_list( - use_version, openpype_versions) - )[1]) + version_path = bootstrap.get_version_path_from_list( + use_version, openpype_versions ) + valid, message = bootstrap.validate_openpype_version(version_path) + _print("{}{}".format(">>> " if valid else "!!! ", message)) def _boot_print_versions(use_staging, local_version, openpype_root): From 0c8b7e303a6eefcfc03986aafd6264c268567c59 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 4 May 2022 18:03:16 +0200 Subject: [PATCH 187/398] nuke: improving add write node function fixing imagio override nodes --- openpype/hosts/nuke/api/lib.py | 91 +++++++++++++++++++++------------- 1 file changed, 57 insertions(+), 34 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 6d6a988b44..2d205d0d39 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -365,7 +365,7 @@ def fix_data_for_node_create(data): return data -def add_write_node(name, **kwarg): +def add_write_node(name, file_path, knobs, **kwarg): """Adding nuke write node Arguments: @@ -375,8 +375,6 @@ def add_write_node(name, **kwarg): Returns: node (obj): nuke write node """ - file_path = kwarg["path"] - knobs = kwarg["knobs"] frame_range = kwarg.get("frame_range", None) w = nuke.createNode( @@ -386,7 +384,7 @@ def add_write_node(name, **kwarg): w["file"].setValue(file_path) # finally add knob overrides - set_node_knobs_from_settings(w, knobs) + set_node_knobs_from_settings(w, knobs, **kwarg) if frame_range: w["use_limit"].setValue(True) @@ -501,7 +499,6 @@ def get_imageio_node_setting(node_class, plugin_name, subset): ''' imageio_nodes = get_nuke_imageio_settings()["nodes"] required_nodes = imageio_nodes["requiredNodes"] - override_nodes = imageio_nodes["overrideNodes"] imageio_node = None for node in required_nodes: @@ -515,14 +512,34 @@ def get_imageio_node_setting(node_class, plugin_name, subset): log.debug("__ imageio_node: {}".format(imageio_node)) + # find overrides and update knobs with them + get_imageio_node_override_setting( + node_class, + plugin_name, + subset, + imageio_node["knobs"] + ) + + log.info("ImageIO node: {}".format(imageio_node)) + return imageio_node + + +def get_imageio_node_override_setting( + node_class, plugin_name, subset, knobs_settings +): + ''' Get imageio node overrides from settings + ''' + imageio_nodes = get_nuke_imageio_settings()["nodes"] + override_nodes = imageio_nodes["overrideNodes"] + # find matching override node override_imageio_node = None for onode in override_nodes: log.info(onode) - if node_class not in node["nukeNodeClass"]: + if node_class not in onode["nukeNodeClass"]: continue - if plugin_name not in node["plugins"]: + if plugin_name not in onode["plugins"]: continue if ( @@ -538,10 +555,10 @@ def get_imageio_node_setting(node_class, plugin_name, subset): # 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"]] + knob_names = [k["name"] for k in knobs_settings] for oknob in override_imageio_node["knobs"]: - for knob in imageio_node["knobs"]: + for knob in knobs_settings: # override matching knob name if oknob["name"] == knob["name"]: log.debug( @@ -550,7 +567,7 @@ def get_imageio_node_setting(node_class, plugin_name, subset): )) if not oknob["value"]: # remove original knob if no value found in oknob - imageio_node["knobs"].remove(knob) + knobs_settings.remove(knob) else: # override knob value with oknob's knob["value"] = oknob["value"] @@ -559,11 +576,10 @@ def get_imageio_node_setting(node_class, plugin_name, subset): if oknob["name"] not in knob_names: log.debug( "_ adding knob: `{}`".format(oknob)) - imageio_node["knobs"].append(oknob) + knobs_settings.append(oknob) knob_names.append(oknob["name"]) - log.info("ImageIO node: {}".format(imageio_node)) - return imageio_node + return knobs_settings def get_imageio_input_colorspace(filename): @@ -812,7 +828,13 @@ def add_button_clear_rendered(node, path): node.addKnob(knob) -def create_prenodes(prev_node, nodes_setting, **kwargs): +def create_prenodes( + prev_node, + nodes_setting, + plugin_name=None, + subset=None, + **kwargs +): last_node = None for_dependency = {} for name, node in nodes_setting.items(): @@ -831,6 +853,15 @@ def create_prenodes(prev_node, nodes_setting, **kwargs): "dependent": node["dependent"] } + if all([plugin_name, subset]): + # find imageio overrides + get_imageio_node_override_setting( + now_node.Class(), + plugin_name, + subset, + knobs + ) + # add data to knob set_node_knobs_from_settings(now_node, knobs, **kwargs) @@ -902,7 +933,7 @@ def create_write_node( prenodes = prenodes or {} # group node knob overrides - knob_overrides = data.get("knobs", []) + knob_overrides = data.pop("knobs", []) # filtering variables plugin_name = data["creator"] @@ -948,21 +979,6 @@ def create_write_node( log.warning("Path does not exist! I am creating it.") os.makedirs(os.path.dirname(fpath)) - _wn_props = { - "path": fpath, - "knobs": imageio_writes["knobs"] - } - - # adding dataflow template - log.debug("__ _wn_props: `{}`".format( - pformat(_wn_props) - )) - - if "frame_range" in data.keys(): - _wn_props["frame_range"] = data.get("frame_range", None) - log.debug("_wn_props[frame_range]: `{}`".format( - _wn_props["frame_range"])) - GN = nuke.createNode("Group", "name {}".format(name)) prev_node = None @@ -979,14 +995,22 @@ def create_write_node( prev_node.hideControlPanel() # creating pre-write nodes `prenodes` - last_prenode = create_prenodes(prev_node, prenodes, **kwargs) + last_prenode = create_prenodes( + prev_node, + prenodes, + plugin_name, + subset, + **kwargs + ) if last_prenode: prev_node = last_prenode # creating write node write_node = now_node = add_write_node( "inside_{}".format(name), - **_wn_props + fpath, + imageio_writes["knobs"], + **data ) write_node.hideControlPanel() # connect to previous node @@ -1063,7 +1087,7 @@ def create_write_node( # set tile color tile_color = next( iter( - k["value"] for k in _wn_props["knobs"] + k["value"] for k in imageio_writes["knobs"] if "tile_color" in k["name"] ), "0xff0000ff" ) @@ -1145,7 +1169,6 @@ def set_node_knobs_from_settings(node, knob_settings, **kwargs): elif knob_type in ["2d_vector", "3d_vector", "color"]: knob_value = [float(v) for v in knob_value] - node[knob_name].setValue(knob_value) From 3744b21266258fdd69814807a4152e268dca3d27 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 5 May 2022 09:46:51 +0200 Subject: [PATCH 188/398] fix the output dir --- .../modules/deadline/plugins/publish/submit_maya_deadline.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py index 8f776a3371..7bf68f51ee 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py @@ -266,6 +266,7 @@ def get_renderer_variables(renderlayer, root): filename_prefix = cmds.getAttr(prefix_attr) return {"ext": extension, + "renderer": renderer, "filename_prefix": filename_prefix, "padding": padding, "filename_0": filename_0} @@ -440,7 +441,8 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): output_filename_0 = filename_0 - dirname = os.path.dirname(output_filename_0) + if render_variables["renderer"] == "renderman": + dirname = os.path.dirname(output_filename_0) # Create render folder ---------------------------------------------- try: From 1997eaf0f4bfa1a47bf849afed1d6c0f313fef5a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 5 May 2022 11:26:53 +0200 Subject: [PATCH 189/398] Remove project_settings.py --- openpype/widgets/project_settings.py | 494 --------------------------- 1 file changed, 494 deletions(-) delete mode 100644 openpype/widgets/project_settings.py diff --git a/openpype/widgets/project_settings.py b/openpype/widgets/project_settings.py deleted file mode 100644 index 687e17b3bf..0000000000 --- a/openpype/widgets/project_settings.py +++ /dev/null @@ -1,494 +0,0 @@ -import os -import getpass -import platform - -from Qt import QtCore, QtGui, QtWidgets - -from openpype import style -import ftrack_api - - -class Project_name_getUI(QtWidgets.QWidget): - ''' - Project setting ui: here all the neceserry ui widgets are created - they are going to be used i later proces for dynamic linking of project - in list to project's attributes - ''' - - def __init__(self, parent=None): - super(Project_name_getUI, self).__init__(parent) - - self.platform = platform.system() - self.new_index = 0 - # get projects from ftrack - self.session = ftrack_api.Session() - self.projects_from_ft = self.session.query( - 'Project where status is active') - self.disks_from_ft = self.session.query('Disk') - self.schemas_from_ft = self.session.query('ProjectSchema') - self.projects = self._get_projects_ftrack() - - # define window geometry - self.setWindowTitle('Set project attributes') - self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint) - self.resize(550, 340) - self.setStyleSheet(style.load_stylesheet()) - - # define disk combobox widget - self.disks = self._get_all_disks() - self.disk_combobox_label = QtWidgets.QLabel('Destination storage:') - self.disk_combobox = QtWidgets.QComboBox() - - # define schema combobox widget - self.schemas = self._get_all_schemas() - self.schema_combobox_label = QtWidgets.QLabel('Project schema:') - self.schema_combobox = QtWidgets.QComboBox() - - # define fps widget - self.fps_label = QtWidgets.QLabel('Fps:') - self.fps_label.setAlignment( - QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) - self.fps = QtWidgets.QLineEdit() - - # define project dir widget - self.project_dir_label = QtWidgets.QLabel('Project dir:') - self.project_dir_label.setAlignment( - QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) - self.project_dir = QtWidgets.QLineEdit() - - self.project_path_label = QtWidgets.QLabel( - 'Project_path (if not then created):') - self.project_path_label.setAlignment( - QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter) - project_path_font = QtGui.QFont( - "Helvetica [Cronyx]", 12, QtGui.QFont.Bold) - self.project_path = QtWidgets.QLabel() - self.project_path.setObjectName('nom_plan_label') - self.project_path.setStyleSheet( - 'QtWidgets.QLabel#nom_plan_label {color: red}') - self.project_path.setAlignment( - QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter) - self.project_path.setFont(project_path_font) - - # define handles widget - self.handles_label = QtWidgets.QLabel('Handles:') - self.handles_label.setAlignment( - QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) - self.handles = QtWidgets.QLineEdit() - - # define resolution widget - self.resolution_w_label = QtWidgets.QLabel('W:') - self.resolution_w = QtWidgets.QLineEdit() - self.resolution_h_label = QtWidgets.QLabel('H:') - self.resolution_h = QtWidgets.QLineEdit() - - devider = QtWidgets.QFrame() - # devider.Shape(QFrame.HLine) - devider.setFrameShape(QtWidgets.QFrame.HLine) - devider.setFrameShadow(QtWidgets.QFrame.Sunken) - - self.generate_lines() - - # define push buttons - self.set_pushbutton = QtWidgets.QPushButton('Set project') - self.cancel_pushbutton = QtWidgets.QPushButton('Cancel') - - # definition of layouts - ############################################ - action_layout = QtWidgets.QHBoxLayout() - action_layout.addWidget(self.set_pushbutton) - action_layout.addWidget(self.cancel_pushbutton) - - # schema property - schema_layout = QtWidgets.QGridLayout() - schema_layout.addWidget(self.schema_combobox, 0, 1) - schema_layout.addWidget(self.schema_combobox_label, 0, 0) - - # storage property - storage_layout = QtWidgets.QGridLayout() - storage_layout.addWidget(self.disk_combobox, 0, 1) - storage_layout.addWidget(self.disk_combobox_label, 0, 0) - - # fps property - fps_layout = QtWidgets.QGridLayout() - fps_layout.addWidget(self.fps, 1, 1) - fps_layout.addWidget(self.fps_label, 1, 0) - - # project dir property - project_dir_layout = QtWidgets.QGridLayout() - project_dir_layout.addWidget(self.project_dir, 1, 1) - project_dir_layout.addWidget(self.project_dir_label, 1, 0) - - # project path property - project_path_layout = QtWidgets.QGridLayout() - spacer_1_item = QtWidgets.QSpacerItem(10, 10) - project_path_layout.addItem(spacer_1_item, 0, 1) - project_path_layout.addWidget(self.project_path_label, 1, 1) - project_path_layout.addWidget(self.project_path, 2, 1) - spacer_2_item = QtWidgets.QSpacerItem(20, 20) - project_path_layout.addItem(spacer_2_item, 3, 1) - - # handles property - handles_layout = QtWidgets.QGridLayout() - handles_layout.addWidget(self.handles, 1, 1) - handles_layout.addWidget(self.handles_label, 1, 0) - - # resolution property - resolution_layout = QtWidgets.QGridLayout() - resolution_layout.addWidget(self.resolution_w_label, 1, 1) - resolution_layout.addWidget(self.resolution_w, 2, 1) - resolution_layout.addWidget(self.resolution_h_label, 1, 2) - resolution_layout.addWidget(self.resolution_h, 2, 2) - - # form project property layout - p_layout = QtWidgets.QGridLayout() - p_layout.addLayout(storage_layout, 1, 0) - p_layout.addLayout(schema_layout, 2, 0) - p_layout.addLayout(project_dir_layout, 3, 0) - p_layout.addLayout(fps_layout, 4, 0) - p_layout.addLayout(handles_layout, 5, 0) - p_layout.addLayout(resolution_layout, 6, 0) - p_layout.addWidget(devider, 7, 0) - spacer_item = QtWidgets.QSpacerItem( - 150, - 40, - QtWidgets.QSizePolicy.Minimum, - QtWidgets.QSizePolicy.Expanding - ) - p_layout.addItem(spacer_item, 8, 0) - - # form with list to one layout with project property - list_layout = QtWidgets.QGridLayout() - list_layout.addLayout(p_layout, 1, 0) - list_layout.addWidget(self.listWidget, 1, 1) - - root_layout = QtWidgets.QVBoxLayout() - root_layout.addLayout(project_path_layout) - root_layout.addWidget(devider) - root_layout.addLayout(list_layout) - root_layout.addLayout(action_layout) - - self.setLayout(root_layout) - - def generate_lines(self): - ''' - Will generate lines of project list - ''' - - self.listWidget = QtWidgets.QListWidget() - for self.index, p in enumerate(self.projects): - item = QtWidgets.QListWidgetItem("{full_name}".format(**p)) - # item.setSelected(False) - self.listWidget.addItem(item) - print(self.listWidget.indexFromItem(item)) - # self.listWidget.setCurrentItem(self.listWidget.itemFromIndex(1)) - - # add options to schemas widget - self.schema_combobox.addItems(self.schemas) - - # add options to disk widget - self.disk_combobox.addItems(self.disks) - - # populate content of project info widgets - self.projects[1] = self._fill_project_attributes_widgets(p, None) - - def _fill_project_attributes_widgets(self, p=None, index=None): - ''' - will generate actual informations wich are saved on ftrack - ''' - - if index is None: - self.new_index = 1 - - if not p: - pass - # change schema selection - for i, schema in enumerate(self.schemas): - if p['project_schema']['name'] in schema: - break - self.schema_combobox.setCurrentIndex(i) - - disk_name, disk_path = self._build_disk_path() - for i, disk in enumerate(self.disks): - if disk_name in disk: - break - # change disk selection - self.disk_combobox.setCurrentIndex(i) - - # change project_dir selection - if "{root}".format(**p): - self.project_dir.setPlaceholderText("{root}".format(**p)) - else: - print("not root so it was replaced with name") - self.project_dir.setPlaceholderText("{name}".format(**p)) - p['root'] = p['name'] - - # set project path to show where it will be created - self.project_path.setText( - os.path.join(self.disks[i].split(' ')[-1], - self.project_dir.text())) - - # change fps selection - self.fps.setPlaceholderText("{custom_attributes[fps]}".format(**p)) - - # change handles selection - self.handles.setPlaceholderText( - "{custom_attributes[handles]}".format(**p)) - - # change resolution selection - self.resolution_w.setPlaceholderText( - "{custom_attributes[resolution_width]}".format(**p)) - self.resolution_h.setPlaceholderText( - "{custom_attributes[resolution_height]}".format(**p)) - - self.update_disk() - - return p - - def fix_project_path_literals(self, dir): - return dir.replace(' ', '_').lower() - - def update_disk(self): - disk = self.disk_combobox.currentText().split(' ')[-1] - - dir = self.project_dir.text() - if not dir: - dir = "{root}".format(**self.projects[self.new_index]) - self.projects[self.new_index]['project_path'] = os.path.normpath( - self.fix_project_path_literals(os.path.join(disk, dir))) - else: - self.projects[self.new_index]['project_path'] = os.path.normpath( - self.fix_project_path_literals(os.path.join(disk, dir))) - - self.projects[self.new_index]['disk'] = self.disks_from_ft[ - self.disk_combobox.currentIndex()] - self.projects[self.new_index]['disk_id'] = self.projects[ - self.new_index]['disk']['id'] - - # set project path to show where it will be created - self.project_path.setText( - self.projects[self.new_index]['project_path']) - - def update_resolution(self): - # update all values in resolution - if self.resolution_w.text(): - self.projects[self.new_index]['custom_attributes'][ - "resolutionWidth"] = int(self.resolution_w.text()) - if self.resolution_h.text(): - self.projects[self.new_index]['custom_attributes'][ - "resolutionHeight"] = int(self.resolution_h.text()) - - def _update_attributes_by_list_selection(self): - # generate actual selection index - self.new_index = self.listWidget.currentRow() - self.project_dir.setText('') - self.fps.setText('') - self.handles.setText('') - self.resolution_w.setText('') - self.resolution_h.setText('') - - # update project properities widgets and write changes - # into project dictionaries - self.projects[self.new_index] = self._fill_project_attributes_widgets( - self.projects[self.new_index], self.new_index) - - self.update_disk() - - def _build_disk_path(self): - if self.platform == "Windows": - print(self.projects[self.index].keys()) - print(self.projects[self.new_index]['disk']) - return self.projects[self.new_index]['disk'][ - 'name'], self.projects[self.new_index]['disk']['windows'] - else: - return self.projects[self.new_index]['disk'][ - 'name'], self.projects[self.new_index]['disk']['unix'] - - def _get_all_schemas(self): - schemas_list = [] - - for s in self.schemas_from_ft: - # print d.keys() - # if 'Pokus' in s['name']: - # continue - schemas_list.append('{}'.format(s['name'])) - print("\nschemas in ftrack: {}\n".format(schemas_list)) - return schemas_list - - def _get_all_disks(self): - disks_list = [] - for d in self.disks_from_ft: - # print d.keys() - if self.platform == "Windows": - if 'Local drive' in d['name']: - d['windows'] = os.path.join(d['windows'], - os.getenv('USERNAME') - or os.getenv('USER') - or os.getenv('LOGNAME')) - disks_list.append('"{}" at {}'.format(d['name'], d['windows'])) - else: - if 'Local drive' in d['name']: - d['unix'] = os.path.join(d['unix'], getpass.getuser()) - disks_list.append('"{}" at {}'.format(d['name'], d['unix'])) - return disks_list - - def _get_projects_ftrack(self): - - projects_lst = [] - for project in self.projects_from_ft: - # print project.keys() - projects_dict = {} - - for k in project.keys(): - ''' # TODO: delete this in production version ''' - - # if 'test' not in project['name']: - # continue - - # print '{}: {}\n'.format(k, project[k]) - - if '_link' == k: - # print project[k] - content = project[k] - for kc in content[0].keys(): - if content[0]['name']: - content[0][kc] = content[0][kc].encode( - 'ascii', 'ignore').decode('ascii') - print('{}: {}\n'.format(kc, content[0][kc])) - projects_dict[k] = content - print(project[k]) - print(projects_dict[k]) - elif 'root' == k: - print('{}: {}\n'.format(k, project[k])) - projects_dict[k] = project[k] - elif 'disk' == k: - print('{}: {}\n'.format(k, project[k])) - projects_dict[k] = project[k] - elif 'name' == k: - print('{}: {}\n'.format(k, project[k])) - projects_dict[k] = project[k].encode( - 'ascii', 'ignore').decode('ascii') - elif 'disk_id' == k: - print('{}: {}\n'.format(k, project[k])) - projects_dict[k] = project[k] - elif 'id' == k: - print('{}: {}\n'.format(k, project[k])) - projects_dict[k] = project[k] - elif 'full_name' == k: - print('{}: {}\n'.format(k, project[k])) - projects_dict[k] = project[k].encode( - 'ascii', 'ignore').decode('ascii') - elif 'project_schema_id' == k: - print('{}: {}\n'.format(k, project[k])) - projects_dict[k] = project[k] - elif 'project_schema' == k: - print('{}: {}\n'.format(k, project[k])) - projects_dict[k] = project[k] - elif 'custom_attributes' == k: - print('{}: {}\n'.format(k, project[k])) - projects_dict[k] = project[k] - else: - pass - - if projects_dict: - projects_lst.append(projects_dict) - - return projects_lst - - -class Project_name_get(Project_name_getUI): - def __init__(self, parent=None): - super(Project_name_get, self).__init__(parent) - # self.input_project_name.textChanged.connect(self.input_project_name.placeholderText) - - self.set_pushbutton.clicked.connect(lambda: self.execute()) - self.cancel_pushbutton.clicked.connect(self.close) - - self.listWidget.itemSelectionChanged.connect( - self._update_attributes_by_list_selection) - self.disk_combobox.currentIndexChanged.connect(self.update_disk) - self.schema_combobox.currentIndexChanged.connect(self.update_schema) - self.project_dir.textChanged.connect(self.update_disk) - self.fps.textChanged.connect(self.update_fps) - self.handles.textChanged.connect(self.update_handles) - self.resolution_w.textChanged.connect(self.update_resolution) - self.resolution_h.textChanged.connect(self.update_resolution) - - def update_handles(self): - self.projects[self.new_index]['custom_attributes']['handles'] = int( - self.handles.text()) - - def update_fps(self): - self.projects[self.new_index]['custom_attributes']['fps'] = int( - self.fps.text()) - - def update_schema(self): - self.projects[self.new_index]['project_schema'] = self.schemas_from_ft[ - self.schema_combobox.currentIndex()] - self.projects[self.new_index]['project_schema_id'] = self.projects[ - self.new_index]['project_schema']['id'] - - def execute(self): - # import ft_utils - # import hiero - # get the project which has been selected - print("well and what") - # set the project as context and create entity - # entity is task created with the name of user which is creating it - - # get the project_path and create dir if there is not any - print(self.projects[self.new_index]['project_path'].replace( - self.disk_combobox.currentText().split(' ')[-1].lower(), '')) - - # get the schema and recreate a starting project regarding the selection - # set_hiero_template(project_schema=self.projects[self.new_index][ - # 'project_schema']['name']) - - # set all project properities - # project = hiero.core.Project() - # project.setFramerate( - # int(self.projects[self.new_index]['custom_attributes']['fps'])) - # project.projectRoot() - # print 'handles: {}'.format(self.projects[self.new_index]['custom_attributes']['handles']) - # print 'resolution_width: {}'.format(self.projects[self.new_index]['custom_attributes']["resolutionWidth"]) - # print 'resolution_width: {}'.format(self.projects[self.new_index]['custom_attributes']["resolutionHeight"]) - # print "<< {}".format(self.projects[self.new_index]) - - # get path for the hrox file - # root = context.data('ftrackData')['Project']['root'] - # hrox_script_path = ft_utils.getPathsYaml(taskid, templateList=templates, root=root) - - # save the hrox into the correct path - self.session.commit() - self.close() - -# -# def set_hiero_template(project_schema=None): -# import hiero -# hiero.core.closeAllProjects() -# hiero_plugin_path = [ -# p for p in os.environ['HIERO_PLUGIN_PATH'].split(';') -# if 'hiero_plugin_path' in p -# ][0] -# path = os.path.normpath( -# os.path.join(hiero_plugin_path, 'Templates', project_schema + '.hrox')) -# print('---> path to template: {}'.format(path)) -# return hiero.core.openProject(path) - - -# def set_out_ft_session(): -# session = ftrack_api.Session() -# projects_to_ft = session.query('Project where status is active') - - -def main(): - import sys - app = QtWidgets.QApplication(sys.argv) - panel = Project_name_get() - panel.show() - - sys.exit(app.exec_()) - - -if __name__ == "__main__": - main() From fe0978a8b2932ade893be5f3ef95951bb869f955 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 5 May 2022 16:19:26 +0200 Subject: [PATCH 190/398] Tweak grammar --- openpype/lib/applications.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index b52da52dc9..a84ff990b2 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -1012,8 +1012,8 @@ class ApplicationLaunchContext: self.log.debug("Discovery of launch hooks started.") paths = self.paths_to_launch_hooks() - self.log.debug("Paths where will look for launch hooks:{}".format( - "\n- ".join(paths) + self.log.debug("Paths searched for launch hooks:\n{}".format( + "\n".join("- {}".format(path) for path in paths) )) all_classes = { @@ -1023,7 +1023,7 @@ class ApplicationLaunchContext: for path in paths: if not os.path.exists(path): self.log.info( - "Path to launch hooks does not exists: \"{}\"".format(path) + "Path to launch hooks does not exist: \"{}\"".format(path) ) continue @@ -1044,7 +1044,8 @@ class ApplicationLaunchContext: hook = klass(self) if not hook.is_valid: self.log.debug( - "Hook is not valid for current launch context." + "Hook is not valid for current " + "launch context: {}".format(str(hook)) ) continue From e5083dded8e69d2fdb572694ae98eb6983c6cab0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 6 May 2022 10:12:17 +0200 Subject: [PATCH 191/398] added dataclasses to required python modules --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 899e9375c0..d3b967a4b6 100644 --- a/setup.py +++ b/setup.py @@ -106,7 +106,8 @@ install_requires = [ "dns", # Python defaults (cx_Freeze skip them by default) "dbm", - "sqlite3" + "sqlite3", + "dataclasses" ] includes = [] From 840e830b30442830b5f8813d2e40113872c14dee Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 6 May 2022 11:00:53 +0200 Subject: [PATCH 192/398] Log name of class in a more readable manner --- openpype/lib/applications.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index a84ff990b2..01bd2768ed 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -1045,13 +1045,13 @@ class ApplicationLaunchContext: if not hook.is_valid: self.log.debug( "Hook is not valid for current " - "launch context: {}".format(str(hook)) + "launch context: {}".format(klass.__name__) ) continue if inspect.isabstract(hook): self.log.debug("Skipped abstract hook: {}".format( - str(hook) + klass.__name__ )) continue @@ -1063,7 +1063,8 @@ class ApplicationLaunchContext: except Exception: self.log.warning( - "Initialization of hook failed. {}".format(str(klass)), + "Initialization of hook failed: " + "{}".format(klass.__name__), exc_info=True ) From 1eb8300831ccec8ace6d3a030464197b9d35bcb5 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 6 May 2022 11:43:49 +0200 Subject: [PATCH 193/398] Less scary log message --- openpype/lib/applications.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index 01bd2768ed..6ade33b59c 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -1044,8 +1044,8 @@ class ApplicationLaunchContext: hook = klass(self) if not hook.is_valid: self.log.debug( - "Hook is not valid for current " - "launch context: {}".format(klass.__name__) + "Skipped hook invalid for current launch context: " + "{}".format(klass.__name__) ) continue From 6a123fcdb994aed27c3e0e64f340ccfea9ce05b2 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 6 May 2022 13:08:34 +0200 Subject: [PATCH 194/398] nuke: change hex type to color_gui in settings --- .../defaults/project_anatomy/imageio.json | 27 ++++++++++++++----- .../schemas/template_nuke_knob_inputs.json | 9 ++++--- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/openpype/settings/defaults/project_anatomy/imageio.json b/openpype/settings/defaults/project_anatomy/imageio.json index 2023dae23c..c03f5f8ee8 100644 --- a/openpype/settings/defaults/project_anatomy/imageio.json +++ b/openpype/settings/defaults/project_anatomy/imageio.json @@ -75,9 +75,14 @@ "value": true }, { - "type": "hex", + "type": "color_gui", "name": "tile_color", - "value": "0xff0000ff" + "value": [ + 186, + 35, + 35, + 255 + ] }, { "type": "text", @@ -123,9 +128,14 @@ "value": true }, { - "type": "hex", + "type": "color_gui", "name": "tile_color", - "value": "0xadab1dff" + "value": [ + 171, + 171, + 10, + 255 + ] }, { "type": "text", @@ -166,9 +176,14 @@ "value": "Deflate" }, { - "type": "hex", + "type": "color_gui", "name": "tile_color", - "value": "0x23ff00ff" + "value": [ + 56, + 162, + 7, + 255 + ] }, { "type": "text", diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/template_nuke_knob_inputs.json b/openpype/settings/entities/schemas/projects_schema/schemas/template_nuke_knob_inputs.json index e6ca1e7fd4..d2fa05e55c 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/template_nuke_knob_inputs.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/template_nuke_knob_inputs.json @@ -65,8 +65,8 @@ ] }, { - "key": "hex", - "label": "Hexadecimal (0x)", + "key": "color_gui", + "label": "Color GUI", "children": [ { "type": "text", @@ -74,9 +74,10 @@ "label": "Name" }, { - "type": "text", + "type": "color", "key": "value", - "label": "Value" + "label": "Value", + "use_alpha": false } ] }, From d640ea3e526c4bb4380505d0d08981e693b8a4b7 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 6 May 2022 13:08:58 +0200 Subject: [PATCH 195/398] nuke: remove redundant code --- openpype/hosts/nuke/plugins/create/create_write_still.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/openpype/hosts/nuke/plugins/create/create_write_still.py b/openpype/hosts/nuke/plugins/create/create_write_still.py index 0b1a942e0e..5b3141355c 100644 --- a/openpype/hosts/nuke/plugins/create/create_write_still.py +++ b/openpype/hosts/nuke/plugins/create/create_write_still.py @@ -2,10 +2,6 @@ import nuke from openpype.hosts.nuke.api import plugin from openpype.hosts.nuke.api.lib import create_write_node -from openpype.api import ( - Logger -) -log = Logger.get_logger(__name__) class CreateWriteStill(plugin.AbstractWriteRender): From e7a1c840d02c1dbb8ef8c79b2b9e115df2a9d4ee Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 6 May 2022 13:09:13 +0200 Subject: [PATCH 196/398] nuke: implementing `color_gui` type --- openpype/hosts/nuke/api/lib.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 2d205d0d39..152dfffce7 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -1089,10 +1089,10 @@ def create_write_node( iter( k["value"] for k in imageio_writes["knobs"] if "tile_color" in k["name"] - ), "0xff0000ff" + ), [255, 0, 0, 255] ) GN["tile_color"].setValue( - int(tile_color, 16)) + color_gui_to_int(tile_color)) # finally add knob overrides set_node_knobs_from_settings(GN, knob_overrides, **kwargs) @@ -1159,19 +1159,20 @@ def set_node_knobs_from_settings(node, knob_settings, **kwargs): knob_value = int(knob_value) elif knob_type == "text": knob_value = knob_value - elif knob_type == "hex": - if not knob_value.startswith("0x"): - raise ValueError( - "Check your settings! Input Hexa is wrong! \n{}".format( - pformat(knob_settings) - )) - knob_value = int(knob_value, 16) + elif knob_type == "color_gui": + knob_value = color_gui_to_int(knob_value) elif knob_type in ["2d_vector", "3d_vector", "color"]: knob_value = [float(v) for v in knob_value] node[knob_name].setValue(knob_value) +def color_gui_to_int(color_gui): + hex_value = ( + "0x{0:0>2x}{1:0>2x}{2:0>2x}{3:0>2x}").format(*color_gui) + return int(hex_value, 16) + + def add_rendering_knobs(node, farm=True): ''' Adds additional rendering knobs to given node From 166ca1b814675b81e02f66998aba4e0a8ded6023 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 May 2022 14:16:04 +0200 Subject: [PATCH 197/398] OP-3113 - added timeit module to requirements psd_tools requires it --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d3b967a4b6..8b5a545c16 100644 --- a/setup.py +++ b/setup.py @@ -107,7 +107,8 @@ install_requires = [ # Python defaults (cx_Freeze skip them by default) "dbm", "sqlite3", - "dataclasses" + "dataclasses", + "timeit" ] includes = [] From b374e8e8a1cb4f3b02c67490d536d639da3abeea Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 6 May 2022 14:17:05 +0200 Subject: [PATCH 198/398] nuke: returning legacy code for backward compatibility --- openpype/hosts/nuke/api/lib.py | 380 +++++++++++++++++- openpype/hosts/nuke/api/plugin.py | 17 +- .../plugins/create/create_write_prerender.py | 27 +- .../plugins/create/create_write_render.py | 43 +- .../nuke/plugins/create/create_write_still.py | 48 ++- 5 files changed, 481 insertions(+), 34 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 152dfffce7..25a64a5eef 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -365,6 +365,40 @@ def fix_data_for_node_create(data): return data +def add_write_node_legacy(name, **kwarg): + """Adding nuke write node + Arguments: + name (str): nuke node name + kwarg (attrs): data for nuke knobs + Returns: + node (obj): nuke write node + """ + frame_range = kwarg.get("use_range_limit", None) + + w = nuke.createNode( + "Write", + "name {}".format(name)) + + w["file"].setValue(kwarg["file"]) + + for k, v in kwarg.items(): + if "frame_range" in k: + continue + log.info([k, v]) + try: + w[k].setValue(v) + except KeyError as e: + log.debug(e) + continue + + if frame_range: + w["use_limit"].setValue(True) + w["first"].setValue(frame_range[0]) + w["last"].setValue(frame_range[1]) + + return w + + def add_write_node(name, file_path, knobs, **kwarg): """Adding nuke write node @@ -375,7 +409,7 @@ def add_write_node(name, file_path, knobs, **kwarg): Returns: node (obj): nuke write node """ - frame_range = kwarg.get("frame_range", None) + frame_range = kwarg.get("use_range_limit", None) w = nuke.createNode( "Write", @@ -494,6 +528,80 @@ def get_nuke_imageio_settings(): return get_anatomy_settings(Context.project_name)["imageio"]["nuke"] +def get_created_node_imageio_setting_legacy(nodeclass, creator, subset): + ''' Get preset data for dataflow (fileType, compression, bitDepth) + ''' + + assert any([creator, nodeclass]), nuke.message( + "`{}`: Missing mandatory kwargs `host`, `cls`".format(__file__)) + + imageio_nodes = get_nuke_imageio_settings()["nodes"] + required_nodes = imageio_nodes["requiredNodes"] + override_nodes = imageio_nodes["overrideNodes"] + + imageio_node = None + for node in required_nodes: + log.info(node) + 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 + )) + 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( + "_ adding knob: `{}`".format(oknob)) + imageio_node["knobs"].append(oknob) + knob_names.append(oknob["name"]) + + log.info("ImageIO node: {}".format(imageio_node)) + return imageio_node + + def get_imageio_node_setting(node_class, plugin_name, subset): ''' Get preset data for dataflow (fileType, compression, bitDepth) ''' @@ -1100,6 +1208,276 @@ def create_write_node( return GN +def create_write_node_legacy(name, data, input=None, prenodes=None, + review=True, linked_knobs=None, farm=True): + ''' Creating write node which is group node + + Arguments: + name (str): name of node + data (dict): data to be imprinted + input (node): selected node to connect to + prenodes (list, optional): list of lists, definitions for nodes + to be created before write + review (bool): adding review knob + + Example: + prenodes = [ + { + "nodeName": { + "class": "" # string + "knobs": [ + ("knobName": value), + ... + ], + "dependent": [ + following_node_01, + ... + ] + } + }, + ... + ] + + Return: + node (obj): group node with avalon data as Knobs + ''' + knob_overrides = data.get("knobs", []) + nodeclass = data["nodeclass"] + creator = data["creator"] + subset = data["subset"] + + imageio_writes = get_created_node_imageio_setting_legacy( + nodeclass, creator, subset + ) + for knob in imageio_writes["knobs"]: + if knob["name"] == "file_type": + representation = knob["value"] + + host_name = os.environ.get("AVALON_APP") + try: + data.update({ + "app": host_name, + "imageio_writes": imageio_writes, + "representation": representation, + }) + anatomy_filled = format_anatomy(data) + + except Exception as e: + msg = "problem with resolving anatomy template: {}".format(e) + log.error(msg) + nuke.message(msg) + + # build file path to workfiles + fdir = str(anatomy_filled["work"]["folder"]).replace("\\", "/") + fpath = data["fpath_template"].format( + work=fdir, version=data["version"], subset=data["subset"], + frame=data["frame"], + ext=representation + ) + + # create directory + if not os.path.isdir(os.path.dirname(fpath)): + log.warning("Path does not exist! I am creating it.") + os.makedirs(os.path.dirname(fpath)) + + _data = OrderedDict({ + "file": fpath + }) + + # adding dataflow template + log.debug("imageio_writes: `{}`".format(imageio_writes)) + for knob in imageio_writes["knobs"]: + _data[knob["name"]] = knob["value"] + + _data = fix_data_for_node_create(_data) + + log.debug("_data: `{}`".format(_data)) + + if "frame_range" in data.keys(): + _data["frame_range"] = data.get("frame_range", None) + log.debug("_data[frame_range]: `{}`".format(_data["frame_range"])) + + GN = nuke.createNode("Group", "name {}".format(name)) + + prev_node = None + with GN: + if input: + input_name = str(input.name()).replace(" ", "") + # if connected input node was defined + prev_node = nuke.createNode( + "Input", "name {}".format(input_name)) + else: + # generic input node connected to nothing + prev_node = nuke.createNode( + "Input", "name {}".format("rgba")) + prev_node.hideControlPanel() + # creating pre-write nodes `prenodes` + if prenodes: + for node in prenodes: + # get attributes + pre_node_name = node["name"] + klass = node["class"] + knobs = node["knobs"] + dependent = node["dependent"] + + # create node + now_node = nuke.createNode( + klass, "name {}".format(pre_node_name)) + now_node.hideControlPanel() + + # add data to knob + for _knob in knobs: + knob, value = _knob + try: + now_node[knob].value() + except NameError: + log.warning( + "knob `{}` does not exist on node `{}`".format( + knob, now_node["name"].value() + )) + continue + + if not knob and not value: + continue + + log.info((knob, value)) + + if isinstance(value, str): + if "[" in value: + now_node[knob].setExpression(value) + else: + now_node[knob].setValue(value) + + # connect to previous node + if dependent: + if isinstance(dependent, (tuple or list)): + for i, node_name in enumerate(dependent): + input_node = nuke.createNode( + "Input", "name {}".format(node_name)) + input_node.hideControlPanel() + now_node.setInput(1, input_node) + + elif isinstance(dependent, str): + input_node = nuke.createNode( + "Input", "name {}".format(node_name)) + input_node.hideControlPanel() + now_node.setInput(0, input_node) + + else: + now_node.setInput(0, prev_node) + + # switch actual node to previous + prev_node = now_node + + # creating write node + + write_node = now_node = add_write_node_legacy( + "inside_{}".format(name), + **_data + ) + write_node.hideControlPanel() + # connect to previous node + now_node.setInput(0, prev_node) + + # switch actual node to previous + prev_node = now_node + + now_node = nuke.createNode("Output", "name Output1") + now_node.hideControlPanel() + + # connect to previous node + now_node.setInput(0, prev_node) + + # imprinting group node + set_avalon_knob_data(GN, data["avalon"]) + add_publish_knob(GN) + add_rendering_knobs(GN, farm) + + if review: + add_review_knob(GN) + + # add divider + GN.addKnob(nuke.Text_Knob('', 'Rendering')) + + # Add linked knobs. + linked_knob_names = [] + + # add input linked knobs and create group only if any input + if linked_knobs: + linked_knob_names.append("_grp-start_") + linked_knob_names.extend(linked_knobs) + linked_knob_names.append("_grp-end_") + + linked_knob_names.append("Render") + + for _k_name in linked_knob_names: + if "_grp-start_" in _k_name: + knob = nuke.Tab_Knob( + "rnd_attr", "Rendering attributes", nuke.TABBEGINCLOSEDGROUP) + GN.addKnob(knob) + elif "_grp-end_" in _k_name: + knob = nuke.Tab_Knob( + "rnd_attr_end", "Rendering attributes", nuke.TABENDGROUP) + GN.addKnob(knob) + else: + if "___" in _k_name: + # add divider + GN.addKnob(nuke.Text_Knob("")) + else: + # add linked knob by _k_name + link = nuke.Link_Knob("") + link.makeLink(write_node.name(), _k_name) + link.setName(_k_name) + + # make render + if "Render" in _k_name: + link.setLabel("Render Local") + link.setFlag(0x1000) + GN.addKnob(link) + + # adding write to read button + add_button_write_to_read(GN) + + # adding write to read button + add_button_clear_rendered(GN, os.path.dirname(fpath)) + + # Deadline tab. + add_deadline_tab(GN) + + # open the our Tab as default + GN[_NODE_TAB_NAME].setFlag(0) + + # set tile color + tile_color = _data.get("tile_color", "0xff0000ff") + GN["tile_color"].setValue(tile_color) + + # overrie knob values from settings + for knob in knob_overrides: + knob_type = knob["type"] + knob_name = knob["name"] + knob_value = knob["value"] + if knob_name not in GN.knobs(): + continue + if not knob_value: + continue + + # set correctly knob types + if knob_type == "string": + knob_value = str(knob_value) + if knob_type == "number": + knob_value = int(knob_value) + if knob_type == "decimal_number": + knob_value = float(knob_value) + if knob_type == "bool": + knob_value = bool(knob_value) + if knob_type in ["2d_vector", "3d_vector"]: + knob_value = list(knob_value) + + GN[knob_name].setValue(knob_value) + + return GN + + def set_node_knobs_from_settings(node, knob_settings, **kwargs): """ Overriding knob values from settings diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index 1eaccef795..76c1e7a37c 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -17,7 +17,8 @@ from .lib import ( reset_selection, maintained_selection, set_avalon_knob_data, - add_publish_knob + add_publish_knob, + get_nuke_imageio_settings ) @@ -700,6 +701,20 @@ class AbstractWriteRender(OpenPypeCreator): return write_node + def is_legacy(self): + """Check if it needs to run legacy code + + In case where `type` key is missing in singe + knob it is legacy project anatomy. + + Returns: + bool: True if legacy + """ + imageio_nodes = get_nuke_imageio_settings()["nodes"] + node = imageio_nodes["requiredNodes"][0] + if "type" not in node["knobs"][0]: + return True + @abstractmethod def _create_write_node(self, selected_node, inputs, outputs, write_data): """Family dependent implementation of Write node creation diff --git a/openpype/hosts/nuke/plugins/create/create_write_prerender.py b/openpype/hosts/nuke/plugins/create/create_write_prerender.py index f86ed7b89e..32ee1fd86f 100644 --- a/openpype/hosts/nuke/plugins/create/create_write_prerender.py +++ b/openpype/hosts/nuke/plugins/create/create_write_prerender.py @@ -1,7 +1,8 @@ import nuke from openpype.hosts.nuke.api import plugin -from openpype.hosts.nuke.api.lib import create_write_node +from openpype.hosts.nuke.api.lib import ( + create_write_node, create_write_node_legacy) class CreateWritePrerender(plugin.AbstractWriteRender): @@ -25,14 +26,24 @@ class CreateWritePrerender(plugin.AbstractWriteRender): def _create_write_node(self, selected_node, inputs, outputs, write_data): # add fpath_template write_data["fpath_template"] = self.fpath_template + write_data["use_range_limit"] = self.use_range_limit - return create_write_node( - self.data["subset"], - write_data, - input=selected_node, - review=self.reviewable, - linked_knobs=["channels", "___", "first", "last", "use_limit"] - ) + if not self.is_legacy(): + return create_write_node( + self.data["subset"], + write_data, + input=selected_node, + review=self.reviewable, + linked_knobs=["channels", "___", "first", "last", "use_limit"] + ) + else: + return create_write_node_legacy( + self.data["subset"], + write_data, + input=selected_node, + review=self.reviewable, + linked_knobs=["channels", "___", "first", "last", "use_limit"] + ) def _modify_write_node(self, write_node): # open group node diff --git a/openpype/hosts/nuke/plugins/create/create_write_render.py b/openpype/hosts/nuke/plugins/create/create_write_render.py index 43b1a9dcb4..23846c0332 100644 --- a/openpype/hosts/nuke/plugins/create/create_write_render.py +++ b/openpype/hosts/nuke/plugins/create/create_write_render.py @@ -1,7 +1,8 @@ import nuke from openpype.hosts.nuke.api import plugin -from openpype.hosts.nuke.api.lib import create_write_node +from openpype.hosts.nuke.api.lib import ( + create_write_node, create_write_node_legacy) class CreateWriteRender(plugin.AbstractWriteRender): @@ -50,16 +51,36 @@ class CreateWriteRender(plugin.AbstractWriteRender): actual_format = nuke.root().knob('format').value() width, height = (actual_format.width(), actual_format.height()) - return create_write_node( - self.data["subset"], - write_data, - input=selected_node, - prenodes=self.prenodes, - **{ - "width": width, - "height": height - } - ) + if not self.is_legacy(): + return create_write_node( + self.data["subset"], + write_data, + input=selected_node, + prenodes=self.prenodes, + **{ + "width": width, + "height": height + } + ) + else: + _prenodes = [ + { + "name": "Reformat01", + "class": "Reformat", + "knobs": [ + ("resize", 0), + ("black_outside", 1), + ], + "dependent": None + } + ] + + return create_write_node_legacy( + self.data["subset"], + write_data, + input=selected_node, + prenodes=_prenodes + ) def _modify_write_node(self, write_node): return write_node diff --git a/openpype/hosts/nuke/plugins/create/create_write_still.py b/openpype/hosts/nuke/plugins/create/create_write_still.py index 5b3141355c..4007ccf51e 100644 --- a/openpype/hosts/nuke/plugins/create/create_write_still.py +++ b/openpype/hosts/nuke/plugins/create/create_write_still.py @@ -1,7 +1,8 @@ import nuke from openpype.hosts.nuke.api import plugin -from openpype.hosts.nuke.api.lib import create_write_node +from openpype.hosts.nuke.api.lib import ( + create_write_node, create_write_node_legacy) class CreateWriteStill(plugin.AbstractWriteRender): @@ -42,18 +43,39 @@ class CreateWriteStill(plugin.AbstractWriteRender): # add fpath_template write_data["fpath_template"] = self.fpath_template - return create_write_node( - self.name, - write_data, - input=selected_node, - review=False, - prenodes=self.prenodes, - farm=False, - linked_knobs=["channels", "___", "first", "last", "use_limit"], - **{ - "frame": nuke.frame() - } - ) + if not self.is_legacy(): + return create_write_node( + self.name, + write_data, + input=selected_node, + review=False, + prenodes=self.prenodes, + farm=False, + linked_knobs=["channels", "___", "first", "last", "use_limit"], + **{ + "frame": nuke.frame() + } + ) + else: + _prenodes = [ + { + "name": "FrameHold01", + "class": "FrameHold", + "knobs": [ + ("first_frame", nuke.frame()) + ], + "dependent": None + } + ] + return create_write_node_legacy( + self.name, + write_data, + input=selected_node, + review=False, + prenodes=_prenodes, + farm=False, + linked_knobs=["channels", "___", "first", "last", "use_limit"] + ) def _modify_write_node(self, write_node): write_node.begin() From 89499e97a38f75386bb10a4452bb8ebe225dd88f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 6 May 2022 15:04:29 +0200 Subject: [PATCH 199/398] nuke: skipping override if no node was found --- openpype/hosts/nuke/api/lib.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 25a64a5eef..0025141310 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -620,6 +620,9 @@ def get_imageio_node_setting(node_class, plugin_name, subset): log.debug("__ imageio_node: {}".format(imageio_node)) + if not imageio_node: + return + # find overrides and update knobs with them get_imageio_node_override_setting( node_class, From f14cabb667b06d5a5501f5dd4564193ae34967ad Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 6 May 2022 15:15:07 +0200 Subject: [PATCH 200/398] hound --- openpype/hosts/nuke/api/lib.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index ff3932c63d..ba8aa7a8db 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -1211,8 +1211,10 @@ def create_write_node( return GN -def create_write_node_legacy(name, data, input=None, prenodes=None, - review=True, linked_knobs=None, farm=True): +def create_write_node_legacy( + name, data, input=None, prenodes=None, + review=True, linked_knobs=None, farm=True +): ''' Creating write node which is group node Arguments: From a0284fd23947f8c4fc882947b86134227dc5d3c6 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 6 May 2022 15:41:55 +0200 Subject: [PATCH 201/398] nuke: adding legacy type --- openpype/hosts/nuke/api/plugin.py | 8 ++++++++ .../schemas/template_nuke_knob_inputs.json | 16 ++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index 76c1e7a37c..2bad6f2c78 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -713,6 +713,14 @@ class AbstractWriteRender(OpenPypeCreator): imageio_nodes = get_nuke_imageio_settings()["nodes"] node = imageio_nodes["requiredNodes"][0] if "type" not in node["knobs"][0]: + # if type is not yet in project anatomy + return True + elif next(iter( + _k for _k in node["knobs"] + if _k.get("type") == "__legacy__" + ), None): + # in case someone re-saved anatomy + # with old configuration return True @abstractmethod diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/template_nuke_knob_inputs.json b/openpype/settings/entities/schemas/projects_schema/schemas/template_nuke_knob_inputs.json index d2fa05e55c..52a14e0636 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/template_nuke_knob_inputs.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/template_nuke_knob_inputs.json @@ -251,6 +251,22 @@ ] } ] + }, + { + "key": "__legacy__", + "label": "_ Legacy type _", + "children": [ + { + "type": "text", + "key": "name", + "label": "Name" + }, + { + "type": "text", + "key": "value", + "label": "Value" + } + ] } ] } From e87024d7ff830d2229cd25753e917d586557ee5e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 6 May 2022 16:26:15 +0200 Subject: [PATCH 202/398] added backwards compatibility for imageio values --- openpype/settings/lib.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/openpype/settings/lib.py b/openpype/settings/lib.py index f921b9c318..f1a4541850 100644 --- a/openpype/settings/lib.py +++ b/openpype/settings/lib.py @@ -291,6 +291,22 @@ def _system_settings_backwards_compatible_conversion(studio_overrides): } +def _project_anatomy_backwards_compatible_conversion(project_anatomy): + # Backwards compatibility of node settings in Nuke 3.9.x - 3.10.0 + # - source PR - https://github.com/pypeclub/OpenPype/pull/3143 + value = project_anatomy + for key in ("imageio", "nuke", "nodes", "requiredNodes"): + if key not in value: + return + value = value[key] + + for item in value: + for node in item.get("requiredNodes") or []: + if "type" in node: + break + node["type"] = "__legacy__" + + @require_handler def get_studio_system_settings_overrides(return_version=False): output = _SETTINGS_HANDLER.get_studio_system_settings_overrides( @@ -326,7 +342,9 @@ def get_project_settings_overrides(project_name, return_version=False): @require_handler def get_project_anatomy_overrides(project_name): - return _SETTINGS_HANDLER.get_project_anatomy_overrides(project_name) + output = _SETTINGS_HANDLER.get_project_anatomy_overrides(project_name) + _project_anatomy_backwards_compatible_conversion(output) + return output @require_handler From b9c211c4b5e37fa31ac6058e040bf9656d3deae5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 6 May 2022 16:27:53 +0200 Subject: [PATCH 203/398] fixed order of variables in ftrack action delete old versions --- .../ftrack/event_handlers_user/action_delete_old_versions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/event_handlers_user/action_delete_old_versions.py b/openpype/modules/ftrack/event_handlers_user/action_delete_old_versions.py index f5addde8ae..a0bf6622e9 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_delete_old_versions.py +++ b/openpype/modules/ftrack/event_handlers_user/action_delete_old_versions.py @@ -569,7 +569,7 @@ class DeleteOldVersions(BaseAction): context["frame"] = self.sequence_splitter sequence_path = os.path.normpath( StringTemplate.format_strict_template( - context, template + template, context ) ) From 065964525ad367c8d536cd8768a250e1af5a0f98 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 6 May 2022 16:53:43 +0200 Subject: [PATCH 204/398] fix key access --- openpype/settings/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/lib.py b/openpype/settings/lib.py index f1a4541850..6df41112c8 100644 --- a/openpype/settings/lib.py +++ b/openpype/settings/lib.py @@ -301,7 +301,7 @@ def _project_anatomy_backwards_compatible_conversion(project_anatomy): value = value[key] for item in value: - for node in item.get("requiredNodes") or []: + for node in item.get("knobs") or []: if "type" in node: break node["type"] = "__legacy__" From ec49131a5b6647a45718574e7a1628363a03ec04 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 6 May 2022 18:18:05 +0200 Subject: [PATCH 205/398] add support for compressed bgeo to standalone and simple loader to houdini --- .../hosts/houdini/plugins/load/load_bgeo.py | 107 ++++++++++++++++++ .../widgets/widget_drop_frame.py | 2 +- 2 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 openpype/hosts/houdini/plugins/load/load_bgeo.py diff --git a/openpype/hosts/houdini/plugins/load/load_bgeo.py b/openpype/hosts/houdini/plugins/load/load_bgeo.py new file mode 100644 index 0000000000..a463d51383 --- /dev/null +++ b/openpype/hosts/houdini/plugins/load/load_bgeo.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- +import os +import re + +from openpype.pipeline import ( + load, + get_representation_path, +) +from openpype.hosts.houdini.api import pipeline + + +class BgeoLoader(load.LoaderPlugin): + """Load bgeo files to Houdini.""" + + label = "Load bgeo" + families = ["model", "pointcache", "bgeo"] + representations = [ + "bgeo", "bgeosc", "bgeogz", + "bgeo.sc", "bgeo.gz", "bgeo.lzma", "bgeo.bz2"] + order = -10 + icon = "code-fork" + color = "orange" + + def load(self, context, name=None, namespace=None, data=None): + + import hou + + # Get the root node + obj = hou.node("/obj") + + # Define node name + namespace = namespace if namespace else context["asset"]["name"] + node_name = "{}_{}".format(namespace, name) if namespace else name + + # Create a new geo node + container = obj.createNode("geo", node_name=node_name) + is_sequence = bool(context["representation"]["context"].get("frame")) + + # Remove the file node, it only loads static meshes + # Houdini 17 has removed the file node from the geo node + file_node = container.node("file1") + if file_node: + file_node.destroy() + + # Explicitly create a file node + file_node = container.createNode("file", node_name=node_name) + file_node.setParms({"file": self.format_path(self.fname, is_sequence)}) + + # Set display on last node + file_node.setDisplayFlag(True) + + nodes = [container, file_node] + self[:] = nodes + + return pipeline.containerise( + node_name, + namespace, + nodes, + context, + self.__class__.__name__, + suffix="", + ) + + @staticmethod + def format_path(path, is_sequence): + """Format file path correctly for single bgeo or bgeo sequence.""" + if not os.path.exists(path): + raise RuntimeError("Path does not exist: %s" % path) + + # The path is either a single file or sequence in a folder. + if not is_sequence: + filename = path + print("single") + else: + filename = re.sub(r"(.*)\.(\d+)\.(bgeo.*)", "\\1.$F4.\\3", path) + + filename = os.path.join(path, filename) + + filename = os.path.normpath(filename) + filename = filename.replace("\\", "/") + + return filename + + def update(self, container, representation): + + node = container["node"] + try: + file_node = next( + n for n in node.children() if n.type().name() == "file" + ) + except StopIteration: + self.log.error("Could not find node of type `alembic`") + return + + # Update the file path + file_path = get_representation_path(representation) + file_path = self.format_path(file_path) + + file_node.setParms({"fileName": file_path}) + + # Update attribute + node.setParms({"representation": str(representation["_id"])}) + + def remove(self, container): + + node = container["node"] + node.destroy() diff --git a/openpype/tools/standalonepublish/widgets/widget_drop_frame.py b/openpype/tools/standalonepublish/widgets/widget_drop_frame.py index e6c7328e88..f8a8273b26 100644 --- a/openpype/tools/standalonepublish/widgets/widget_drop_frame.py +++ b/openpype/tools/standalonepublish/widgets/widget_drop_frame.py @@ -38,7 +38,7 @@ class DropDataFrame(QtWidgets.QFrame): } sequence_types = [ - ".bgeo", ".vdb" + ".bgeo", ".vdb", ".bgeosc", ".bgeogz" ] def __init__(self, parent): From d746f9410572b2dab73e31c75037561710f31b32 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 6 May 2022 18:41:17 +0200 Subject: [PATCH 206/398] add comment and get renderer from instance --- .../modules/deadline/plugins/publish/submit_maya_deadline.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py index 7bf68f51ee..8562c85f7d 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py @@ -266,7 +266,6 @@ def get_renderer_variables(renderlayer, root): filename_prefix = cmds.getAttr(prefix_attr) return {"ext": extension, - "renderer": renderer, "filename_prefix": filename_prefix, "padding": padding, "filename_0": filename_0} @@ -441,7 +440,9 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): output_filename_0 = filename_0 - if render_variables["renderer"] == "renderman": + # this is needed because renderman handles directory and file + # prefixes separately + if self._instance.data["renderer"] == "renderman": dirname = os.path.dirname(output_filename_0) # Create render folder ---------------------------------------------- From 7226d82c8f0b6ab52ed7d0a95e7c61deb9e81015 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Mon, 9 May 2022 15:10:20 +0200 Subject: [PATCH 207/398] add support for hw prefixes --- openpype/hosts/maya/plugins/create/create_render.py | 6 ++++-- openpype/hosts/maya/plugins/publish/collect_render.py | 4 ++-- .../plugins/publish/validate_render_single_camera.py | 3 ++- .../maya/plugins/publish/validate_rendersettings.py | 10 ++++++---- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_render.py b/openpype/hosts/maya/plugins/create/create_render.py index 8e9bd0e22b..93ee6679e5 100644 --- a/openpype/hosts/maya/plugins/create/create_render.py +++ b/openpype/hosts/maya/plugins/create/create_render.py @@ -77,7 +77,8 @@ class CreateRender(plugin.Creator): 'vray': 'vraySettings.fileNamePrefix', 'arnold': 'defaultRenderGlobals.imageFilePrefix', 'renderman': 'rmanGlobals.imageFileFormat', - 'redshift': 'defaultRenderGlobals.imageFilePrefix' + 'redshift': 'defaultRenderGlobals.imageFilePrefix', + 'mayahardware2': 'defaultRenderGlobals.imageFilePrefix', } _image_prefixes = { @@ -87,7 +88,8 @@ class CreateRender(plugin.Creator): # this needs `imageOutputDir` # (/renders/maya/) set separately 'renderman': '_..', - 'redshift': 'maya///' # noqa + 'redshift': 'maya///', # noqa + 'mayahardware2': 'maya///', # noqa } _aov_chars = { diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index 912fe179dd..e66983780e 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -326,8 +326,8 @@ class CollectMayaRender(pyblish.api.ContextPlugin): "byFrameStep": int( self.get_render_attribute("byFrameStep", layer=layer_name)), - "renderer": self.get_render_attribute("currentRenderer", - layer=layer_name), + "renderer": self.get_render_attribute( + "currentRenderer", layer=layer_name).lower(), # instance subset "family": "renderlayer", "families": ["renderlayer"], diff --git a/openpype/hosts/maya/plugins/publish/validate_render_single_camera.py b/openpype/hosts/maya/plugins/publish/validate_render_single_camera.py index 0838b4fbf8..e6c6ef6c9e 100644 --- a/openpype/hosts/maya/plugins/publish/validate_render_single_camera.py +++ b/openpype/hosts/maya/plugins/publish/validate_render_single_camera.py @@ -12,7 +12,8 @@ ImagePrefixes = { 'vray': 'vraySettings.fileNamePrefix', 'arnold': 'defaultRenderGlobals.imageFilePrefix', 'renderman': 'defaultRenderGlobals.imageFilePrefix', - 'redshift': 'defaultRenderGlobals.imageFilePrefix' + 'redshift': 'defaultRenderGlobals.imageFilePrefix', + 'mayahardware2': 'defaultRenderGlobals.imageFilePrefix', } diff --git a/openpype/hosts/maya/plugins/publish/validate_rendersettings.py b/openpype/hosts/maya/plugins/publish/validate_rendersettings.py index 023e27de17..e212e8978d 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rendersettings.py +++ b/openpype/hosts/maya/plugins/publish/validate_rendersettings.py @@ -50,15 +50,17 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin): 'vray': 'vraySettings.fileNamePrefix', 'arnold': 'defaultRenderGlobals.imageFilePrefix', 'renderman': 'rmanGlobals.imageFileFormat', - 'redshift': 'defaultRenderGlobals.imageFilePrefix' + 'redshift': 'defaultRenderGlobals.imageFilePrefix', + 'mayahardware2': 'defaultRenderGlobals.imageFilePrefix', } ImagePrefixTokens = { - + 'mentalray': 'maya///{aov_separator}', 'arnold': 'maya///{aov_separator}', # noqa 'redshift': 'maya///', 'vray': 'maya///', - 'renderman': '{aov_separator}..' # noqa + 'renderman': '{aov_separator}..', # noqa + 'mayahardware2': 'maya///', # noqa } _aov_chars = { @@ -234,7 +236,7 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin): # load validation definitions from settings validation_settings = ( instance.context.data["project_settings"]["maya"]["publish"]["ValidateRenderSettings"].get( # noqa: E501 - "{}_render_attributes".format(renderer)) + "{}_render_attributes".format(renderer)) or [] ) # go through definitions and test if such node.attribute exists. From 10e4764da21de5aec20df48f619c02a06644cdc7 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 10 May 2022 11:15:42 +0200 Subject: [PATCH 208/398] OP-3113 - removed unnecessary default Task type enum is dynamically created from Ftrack, value here is targeted only for single customer, so it shouldn't probably be in defaults at all. --- openpype/settings/defaults/project_anatomy/tasks.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/openpype/settings/defaults/project_anatomy/tasks.json b/openpype/settings/defaults/project_anatomy/tasks.json index 178f2af639..74504cc4d7 100644 --- a/openpype/settings/defaults/project_anatomy/tasks.json +++ b/openpype/settings/defaults/project_anatomy/tasks.json @@ -40,8 +40,5 @@ }, "Compositing": { "short_name": "comp" - }, - "Background": { - "short_name": "back" } } \ No newline at end of file From 7ff23fd4294532d670adffe179c8ea893db9feda Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 10 May 2022 12:06:08 +0200 Subject: [PATCH 209/398] fix extension collection --- .../traypublisher/plugins/publish/collect_simple_instances.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_simple_instances.py b/openpype/hosts/traypublisher/plugins/publish/collect_simple_instances.py index 9facd90a48..b2be43c701 100644 --- a/openpype/hosts/traypublisher/plugins/publish/collect_simple_instances.py +++ b/openpype/hosts/traypublisher/plugins/publish/collect_simple_instances.py @@ -33,7 +33,8 @@ class CollectSettingsSimpleInstances(pyblish.api.InstancePlugin): instance.data["stagingDir"] = filepath_item["directory"] filenames = filepath_item["filenames"] - ext = os.path.splitext(filenames[0])[-1] + _, ext = os.path.splitext(filenames[0]) + ext = ext[1:] if len(filenames) == 1: filenames = filenames[0] From 235baf4f4c40fc92f3faf148d846655590bf6cdb Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 10 May 2022 14:48:53 +0200 Subject: [PATCH 210/398] store width and height into thumbnail representation --- .../plugins/publish/extract_thumbnail.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py index 23f0b104c8..941a76b05b 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py @@ -2,7 +2,10 @@ import os import tempfile import pyblish.api import openpype.api -import openpype.lib +from openpype.lib import ( + get_ffmpeg_tool_path, + get_ffprobe_streams, +) class ExtractThumbnailSP(pyblish.api.InstancePlugin): @@ -71,7 +74,7 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin): full_thumbnail_path = tempfile.mkstemp(suffix=".jpg")[1] self.log.info("output {}".format(full_thumbnail_path)) - ffmpeg_path = openpype.lib.get_ffmpeg_tool_path("ffmpeg") + ffmpeg_path = get_ffmpeg_tool_path("ffmpeg") ffmpeg_args = self.ffmpeg_args or {} @@ -110,6 +113,13 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin): # remove thumbnail key from origin repre thumbnail_repre.pop("thumbnail") + streams = get_ffprobe_streams(full_thumbnail_path) + width = height = None + for stream in streams: + if "width" in stream and "height" in stream: + width = stream["width"] + height = stream["height"] + break filename = os.path.basename(full_thumbnail_path) staging_dir = staging_dir or os.path.dirname(full_thumbnail_path) @@ -122,6 +132,9 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin): "stagingDir": staging_dir, "tags": ["thumbnail"], } + if width and height: + representation["width"] = width + representation["height"] = height # # add Delete tag when temp file was rendered if not is_jpeg: From 5da19f09ab487419f15a2179394a923a037e9ef4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 10 May 2022 14:49:37 +0200 Subject: [PATCH 211/398] integrate ftrack instance is collecting width and height from representation and uses ffprobe if are not available --- .../publish/integrate_ftrack_instances.py | 37 ++++++++++++++++++- 1 file changed, 36 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 5eecf34c3d..7181be6f01 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py @@ -3,6 +3,7 @@ import json import copy import pyblish.api +from openpype.lib import get_ffprobe_streams from openpype.lib.profiles_filtering import filter_profiles @@ -142,6 +143,7 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): # Create thumbnail components # TODO what if there is multiple thumbnails? first_thumbnail_component = None + first_thumbnail_component_repre = None for repre in thumbnail_representations: published_path = repre.get("published_path") if not published_path: @@ -169,12 +171,45 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): src_components_to_add.append(copy.deepcopy(thumbnail_item)) # Create copy of first thumbnail if first_thumbnail_component is None: - first_thumbnail_component = copy.deepcopy(thumbnail_item) + first_thumbnail_component_repre = repre + first_thumbnail_component = thumbnail_item # Set location thumbnail_item["component_location"] = ftrack_server_location # Add item to component list component_list.append(thumbnail_item) + if first_thumbnail_component is not None: + width = first_thumbnail_component_repre.get("width") + height = first_thumbnail_component_repre.get("height") + if not width or not height: + component_path = first_thumbnail_component["component_path"] + streams = [] + try: + streams = get_ffprobe_streams(component_path) + except Exception: + self.log.debug( + "Failed to retrieve information about intput {}".format( + component_path + ) + ) + + for stream in streams: + if "width" in stream and "height" in stream: + width = stream["width"] + height = stream["height"] + break + + if width and height: + component_data = first_thumbnail_component["component_data"] + component_data["name"] = "ftrackreview-image" + component_data["metadata"] = { + "ftr_meta": json.dumps({ + "width": width, + "height": height, + "format": "image" + }) + } + # Create review components # Change asset name of each new component for review is_first_review_repre = True From a20891e5687514b2852440948c44a9b5cf0da3b4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 10 May 2022 15:17:58 +0200 Subject: [PATCH 212/398] fix line length --- .../ftrack/plugins/publish/integrate_ftrack_instances.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py index 7181be6f01..0dd7b1c6e4 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py @@ -187,11 +187,9 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): try: streams = get_ffprobe_streams(component_path) except Exception: - self.log.debug( - "Failed to retrieve information about intput {}".format( - component_path - ) - ) + self.log.debug(( + "Failed to retrieve information about intput {}" + ).format(component_path)) for stream in streams: if "width" in stream and "height" in stream: From b80bb7f7df51d43ec0f15d80076fba3a90077f51 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 10 May 2022 15:36:27 +0200 Subject: [PATCH 213/398] OP-3137 - use validation settings to create subset name Uses Setting to replace invalid characters automatically. Creates valid subset name and renames layer. --- .../publish/collect_color_coded_instances.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/openpype/hosts/photoshop/plugins/publish/collect_color_coded_instances.py b/openpype/hosts/photoshop/plugins/publish/collect_color_coded_instances.py index 122428eea0..ca8b5d962f 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_color_coded_instances.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_color_coded_instances.py @@ -5,6 +5,7 @@ import pyblish.api from openpype.lib import prepare_template_data from openpype.hosts.photoshop import api as photoshop +from openpype.settings import get_project_settings class CollectColorCodedInstances(pyblish.api.ContextPlugin): @@ -49,6 +50,12 @@ class CollectColorCodedInstances(pyblish.api.ContextPlugin): asset_name = context.data["asset"] task_name = context.data["task"] variant = context.data["variant"] + project_name = context.data["projectEntity"]["name"] + + naming_conventions = get_project_settings(project_name).get( + "photoshop", {}).get( + "publish", {}).get( + "ValidateNaming", {}) stub = photoshop.stub() layers = stub.get_layers() @@ -83,6 +90,9 @@ class CollectColorCodedInstances(pyblish.api.ContextPlugin): subset = resolved_subset_template.format( **prepare_template_data(fill_pairs)) + subset = self._clean_subset_name(stub, naming_conventions, + subset, layer) + if subset in existing_subset_names: self.log.info( "Subset {} already created, skipping.".format(subset)) @@ -186,3 +196,21 @@ class CollectColorCodedInstances(pyblish.api.ContextPlugin): self.log.debug("resolved_subset_template {}".format( resolved_subset_template)) return family, resolved_subset_template + + def _clean_subset_name(self, stub, naming_conventions, subset, layer): + """Cleans invalid characters from subset name and layer name.""" + if re.search(naming_conventions["invalid_chars"], subset): + subset = re.sub( + naming_conventions["invalid_chars"], + naming_conventions["replace_char"], + subset + ) + layer_name = re.sub( + naming_conventions["invalid_chars"], + naming_conventions["replace_char"], + layer.clean_name + ) + layer.name = layer_name + stub.rename_layer(layer.id, layer_name) + + return subset From 734258cc195f2dee0cd7481b64b88bb3bcdbd261 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 10 May 2022 15:58:35 +0200 Subject: [PATCH 214/398] OP-3137 - use validation settings to create subset name Uses Setting to replace invalid characters automatically. Creates valid subset name and renames layer. --- .../photoshop/plugins/publish/collect_color_coded_instances.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/photoshop/plugins/publish/collect_color_coded_instances.py b/openpype/hosts/photoshop/plugins/publish/collect_color_coded_instances.py index ca8b5d962f..ae025fc61d 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_color_coded_instances.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_color_coded_instances.py @@ -151,6 +151,7 @@ class CollectColorCodedInstances(pyblish.api.ContextPlugin): instance.data["task"] = task_name instance.data["subset"] = subset instance.data["layer"] = layer + instance.data["families"] = [] return instance From edef0cca11c0cdf22097a33cdb786abd4762eca9 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 10 May 2022 14:32:18 +0200 Subject: [PATCH 215/398] OP-3137 - fix naming validator Layer name validator was looking into wrong field. Layer name should follow same naming convention as subset name to see relation between subset name and layer name better. Introduced clean_name property returning layer name without publishing highlight icons. --- openpype/hosts/photoshop/api/ws_stub.py | 10 ++++++++++ .../plugins/publish/validate_naming.py | 19 ++++++++++++------- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/photoshop/api/ws_stub.py b/openpype/hosts/photoshop/api/ws_stub.py index fa076ecc7e..b49bf1c73f 100644 --- a/openpype/hosts/photoshop/api/ws_stub.py +++ b/openpype/hosts/photoshop/api/ws_stub.py @@ -29,6 +29,16 @@ class PSItem(object): color_code = attr.ib(default=None) # color code of layer instance_id = attr.ib(default=None) + @property + def clean_name(self): + """Returns layer name without publish icon highlight + + Returns: + (str) + """ + return (self.name.replace(PhotoshopServerStub.PUBLISH_ICON, '') + .replace(PhotoshopServerStub.LOADED_ICON, '')) + class PhotoshopServerStub: """ diff --git a/openpype/hosts/photoshop/plugins/publish/validate_naming.py b/openpype/hosts/photoshop/plugins/publish/validate_naming.py index bcae24108c..b53f4e8198 100644 --- a/openpype/hosts/photoshop/plugins/publish/validate_naming.py +++ b/openpype/hosts/photoshop/plugins/publish/validate_naming.py @@ -42,7 +42,8 @@ class ValidateNamingRepair(pyblish.api.Action): layer_name = re.sub(invalid_chars, replace_char, - current_layer_state.name) + current_layer_state.clean_name) + layer_name = stub.PUBLISH_ICON + layer_name stub.rename_layer(current_layer_state.id, layer_name) @@ -73,13 +74,17 @@ class ValidateNaming(pyblish.api.InstancePlugin): def process(self, instance): help_msg = ' Use Repair action (A) in Pyblish to fix it.' - msg = "Name \"{}\" is not allowed.{}".format(instance.data["name"], - help_msg) - formatting_data = {"msg": msg} - if re.search(self.invalid_chars, instance.data["name"]): - raise PublishXmlValidationError(self, msg, - formatting_data=formatting_data) + layer = instance.data.get("layer") + if layer: + msg = "Name \"{}\" is not allowed.{}".format(layer.clean_name, + help_msg) + + formatting_data = {"msg": msg} + if re.search(self.invalid_chars, layer.clean_name): + raise PublishXmlValidationError(self, msg, + formatting_data=formatting_data + ) msg = "Subset \"{}\" is not allowed.{}".format(instance.data["subset"], help_msg) From f7fc6a3e7007d80d0f75ccedcf381267c676f089 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 10 May 2022 16:52:13 +0200 Subject: [PATCH 216/398] flame: fixing attr_name issue --- openpype/hosts/flame/api/lib.py | 2 +- openpype/hosts/flame/otio/flame_export.py | 91 +++++------------------ 2 files changed, 21 insertions(+), 72 deletions(-) diff --git a/openpype/hosts/flame/api/lib.py b/openpype/hosts/flame/api/lib.py index c7c444c1fb..2e9b535764 100644 --- a/openpype/hosts/flame/api/lib.py +++ b/openpype/hosts/flame/api/lib.py @@ -560,7 +560,7 @@ def get_segment_attributes(segment): if not hasattr(segment, attr_name): continue attr = getattr(segment, attr_name) - segment_attrs_data[attr] = str(attr).replace("+", ":") + segment_attrs_data[attr_name] = str(attr).replace("+", ":") if attr_name in ["record_in", "record_out"]: clip_data[attr_name] = attr.relative_frame diff --git a/openpype/hosts/flame/otio/flame_export.py b/openpype/hosts/flame/otio/flame_export.py index 4fe05ec1d8..d84ee98256 100644 --- a/openpype/hosts/flame/otio/flame_export.py +++ b/openpype/hosts/flame/otio/flame_export.py @@ -94,83 +94,30 @@ def create_otio_time_range(start_frame, frame_duration, fps): def _get_metadata(item): if hasattr(item, 'metadata'): - if not item.metadata: - return {} - return {key: value for key, value in dict(item.metadata)} + return dict(dict(item.metadata)) if item.metadata else {} return {} -def create_time_effects(otio_clip, item): - # todo #2426: add retiming effects to export - # get all subtrack items - # subTrackItems = flatten(track_item.parent().subTrackItems()) - # speed = track_item.playbackSpeed() +# def create_time_effects(otio_clip, clip_data): +# otio_effect = None - # otio_effect = None - # # retime on track item - # if speed != 1.: - # # make effect - # otio_effect = otio.schema.LinearTimeWarp() - # otio_effect.name = "Speed" - # otio_effect.time_scalar = speed - # otio_effect.metadata = {} +# # retime on track item +# if speed != 1.: +# # make effect +# otio_effect = otio.schema.LinearTimeWarp() +# otio_effect.name = "Speed" +# otio_effect.time_scalar = speed +# otio_effect.metadata = {} - # # freeze frame effect - # if speed == 0.: - # otio_effect = otio.schema.FreezeFrame() - # otio_effect.name = "FreezeFrame" - # otio_effect.metadata = {} +# # freeze frame effect +# if speed == 0.: +# otio_effect = otio.schema.FreezeFrame() +# otio_effect.name = "FreezeFrame" +# otio_effect.metadata = {} - # if otio_effect: - # # add otio effect to clip effects - # otio_clip.effects.append(otio_effect) - - # # loop through and get all Timewarps - # for effect in subTrackItems: - # if ((track_item not in effect.linkedItems()) - # and (len(effect.linkedItems()) > 0)): - # continue - # # avoid all effect which are not TimeWarp and disabled - # if "TimeWarp" not in effect.name(): - # continue - - # if not effect.isEnabled(): - # continue - - # node = effect.node() - # name = node["name"].value() - - # # solve effect class as effect name - # _name = effect.name() - # if "_" in _name: - # effect_name = re.sub(r"(?:_)[_0-9]+", "", _name) # more numbers - # else: - # effect_name = re.sub(r"\d+", "", _name) # one number - - # metadata = {} - # # add knob to metadata - # for knob in ["lookup", "length"]: - # value = node[knob].value() - # animated = node[knob].isAnimated() - # if animated: - # value = [ - # ((node[knob].getValueAt(i)) - i) - # for i in range( - # track_item.timelineIn(), - # track_item.timelineOut() + 1) - # ] - - # metadata[knob] = value - - # # make effect - # otio_effect = otio.schema.TimeEffect() - # otio_effect.name = name - # otio_effect.effect_name = effect_name - # otio_effect.metadata = metadata - - # # add otio effect to clip effects - # otio_clip.effects.append(otio_effect) - pass +# if otio_effect: +# # add otio effect to clip effects +# otio_clip.effects.append(otio_effect) def _get_marker_color(flame_colour): @@ -363,6 +310,8 @@ def create_otio_clip(clip_data): if MARKERS_INCLUDE: create_otio_markers(otio_clip, segment) + create_time_effects(otio_clip, clip_data) + return otio_clip From 21b83e341b754506d982c8ec05cb28d647c296cd Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 10 May 2022 16:59:20 +0200 Subject: [PATCH 217/398] flame" adding empty families to workfile instance --- openpype/hosts/flame/plugins/publish/collect_timeline_otio.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/flame/plugins/publish/collect_timeline_otio.py b/openpype/hosts/flame/plugins/publish/collect_timeline_otio.py index f2ae1f62a9..0a9b0db334 100644 --- a/openpype/hosts/flame/plugins/publish/collect_timeline_otio.py +++ b/openpype/hosts/flame/plugins/publish/collect_timeline_otio.py @@ -39,7 +39,8 @@ class CollecTimelineOTIO(pyblish.api.ContextPlugin): "name": subset_name, "asset": asset_doc["name"], "subset": subset_name, - "family": "workfile" + "family": "workfile", + "families": [] } # create instance with workfile From 9483adc91eaa0958f14090ba8af3e773b80247f9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 10 May 2022 16:59:39 +0200 Subject: [PATCH 218/398] Flame: commenting out effects - not yet implemented --- openpype/hosts/flame/otio/flame_export.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/flame/otio/flame_export.py b/openpype/hosts/flame/otio/flame_export.py index d84ee98256..fc960b670c 100644 --- a/openpype/hosts/flame/otio/flame_export.py +++ b/openpype/hosts/flame/otio/flame_export.py @@ -310,7 +310,7 @@ def create_otio_clip(clip_data): if MARKERS_INCLUDE: create_otio_markers(otio_clip, segment) - create_time_effects(otio_clip, clip_data) + # create_time_effects(otio_clip, clip_data) return otio_clip From ff37e6135b6a2e22fede236268683ae3e5e1936c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 10 May 2022 20:51:28 +0200 Subject: [PATCH 219/398] global: removing `clip` family from excluded - obsolete --- openpype/plugins/publish/integrate_new.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index bf13a4050e..353314fff2 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -113,7 +113,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "usdOverride", "simpleUnrealTexture" ] - exclude_families = ["clip", "render.farm"] + exclude_families = ["render.farm"] db_representation_context_keys = [ "project", "asset", "task", "subset", "version", "representation", "family", "hierarchy", "task", "username" From dbbc633e834beabb99d0045939b03b28d98d94c9 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 11 May 2022 10:36:41 +0200 Subject: [PATCH 220/398] Fix - added missing task Task used in validations later. --- openpype/hosts/harmony/plugins/publish/collect_farm_render.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/harmony/plugins/publish/collect_farm_render.py b/openpype/hosts/harmony/plugins/publish/collect_farm_render.py index f5bf051243..3e9e680efd 100644 --- a/openpype/hosts/harmony/plugins/publish/collect_farm_render.py +++ b/openpype/hosts/harmony/plugins/publish/collect_farm_render.py @@ -144,6 +144,7 @@ class CollectFarmRender(openpype.lib.abstract_collect_render. label=node.split("/")[1], subset=subset_name, asset=legacy_io.Session["AVALON_ASSET"], + task=task_name, attachTo=False, setMembers=[node], publish=info[4], From 97e2bd13c7ca1bce903a3cbdc6975ad276c62e19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Wed, 11 May 2022 11:42:35 +0200 Subject: [PATCH 221/398] fix setting of subset group and primary family --- .../modules/deadline/plugins/publish/submit_publish_job.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 306237c78c..ffa5718d4a 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -468,7 +468,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): new_instance = copy(instance_data) new_instance["subset"] = subset_name - new_instance["subsetGroup"] = group_name + if not instance_data.get("append"): + new_instance["subsetGroup"] = group_name if preview: new_instance["review"] = True @@ -883,8 +884,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): new_i = copy(i) new_i["version"] = at.get("version") new_i["subset"] = at.get("subset") + new_i["family"] = at.get("family") new_i["append"] = True - new_i["families"].append(at.get("family")) new_instances.append(new_i) self.log.info(" - {} / v{}".format( at.get("subset"), at.get("version"))) From 45a1cb821e6a5fdea8cba16dfdbff842c4aa25c8 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 11 May 2022 11:48:39 +0200 Subject: [PATCH 222/398] Update validate_rendersettings.py --- .../hosts/maya/plugins/publish/validate_rendersettings.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_rendersettings.py b/openpype/hosts/maya/plugins/publish/validate_rendersettings.py index e212e8978d..ba6c1397ab 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rendersettings.py +++ b/openpype/hosts/maya/plugins/publish/validate_rendersettings.py @@ -55,12 +55,12 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin): } ImagePrefixTokens = { - 'mentalray': 'maya///{aov_separator}', - 'arnold': 'maya///{aov_separator}', # noqa + 'mentalray': 'maya///{aov_separator}', # noqa: E501 + 'arnold': 'maya///{aov_separator}', # noqa: E501 'redshift': 'maya///', 'vray': 'maya///', - 'renderman': '{aov_separator}..', # noqa - 'mayahardware2': 'maya///', # noqa + 'renderman': '{aov_separator}..', + 'mayahardware2': 'maya///', } _aov_chars = { From 43baf02d98e2fa899ec84ab163cb97d8453167d8 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 11 May 2022 13:22:42 +0200 Subject: [PATCH 223/398] flame: refactory MediaInfo class - to be able match collection if sequence with holes - also make code more explicit about input arguments --- openpype/hosts/flame/api/lib.py | 119 ++++++++++++++++++++++++++++---- 1 file changed, 105 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/flame/api/lib.py b/openpype/hosts/flame/api/lib.py index 2e9b535764..15e6f8ae80 100644 --- a/openpype/hosts/flame/api/lib.py +++ b/openpype/hosts/flame/api/lib.py @@ -3,6 +3,7 @@ import os import re import json import pickle +import clique import tempfile import itertools import contextlib @@ -773,17 +774,24 @@ class MediaInfoFile(object): self._validate_media_script_path() # derivate other feed variables - self.feed_basename = os.path.basename(path) - self.feed_dir = os.path.dirname(path) - self.feed_ext = os.path.splitext(self.feed_basename)[1][1:].lower() + feed_basename = os.path.basename(path) + feed_dir = os.path.dirname(path) + feed_ext = os.path.splitext(feed_basename)[1][1:].lower() with maintained_temp_file_path(".clip") as tmp_path: self.log.info("Temp File: {}".format(tmp_path)) - self._generate_media_info_file(tmp_path) + self._generate_media_info_file(tmp_path, feed_ext, feed_dir) + + if os.path.exists(os.path.join(feed_dir, feed_basename)): + test_fname = feed_basename + else: + # get collection containing feed_basename from path + test_fname = self._get_collection(feed_basename, feed_dir, feed_ext) # get clip data and make them single if there is multiple # clips data - xml_data = self._make_single_clip_media_info(tmp_path) + xml_data = self._make_single_clip_media_info( + tmp_path, feed_basename, test_fname) self.log.debug("xml_data: {}".format(xml_data)) self.log.debug("type: {}".format(type(xml_data))) @@ -794,6 +802,73 @@ class MediaInfoFile(object): self.log.debug("drop frame: {}".format(self.drop_mode)) self.clip_data = xml_data + def _get_collection(self, feed_basename, feed_dir, feed_ext): + partialname = self._separate_file_head(feed_basename, feed_ext) + log.debug("__ partialname: {}".format(partialname)) + + # make sure partial input basename is having correct extensoon + if not partialname: + raise IOError("File doesnt exists. Basename - {}, Ext - {}".format( + feed_basename, feed_ext + )) + + # get all related files + files = [ + f for f in os.listdir(feed_dir) + if partialname == self._separate_file_head(f, feed_ext) + ] + + # ignore reminders as we dont need them + collections = clique.assemble(files)[0] + + # if no collection rise + if not collections: + raise IOError("_get_collection is failing on: {} {} {}".format( + feed_basename, feed_dir, feed_ext + )) + else: + # we expect only one collection + collection = collections[0] + + if collection.is_contiguous(): + # if no holes then return collection + return collection.format("{head}[{range}]{tail}") + + # add `[` in front to make sure it want capture + # shot name with the same number + number_from_path = "[" + self._separate_number(feed_basename, feed_ext) + # convert to multiple collections + _continues_colls = collection.separate() + for _coll in _continues_colls: + coll_to_text = _coll.format("{head}[{range}]{tail}") + log.debug("__ coll_to_text: {}".format(coll_to_text)) + if number_from_path in coll_to_text: + return coll_to_text + + def _separate_file_head(self, basename, extension): + # in case sequence file + found = re.findall( + r"(.*)[._][\d]*(?=.{})".format(extension), + basename, + ) + if found: + return found.pop() + + # in case single file + name, ext = os.path.splitext(basename) + + if extension == ext[1:]: + return name + + def _separate_number(self, basename, extension): + # in case sequence file + found = re.findall( + r"[._]([\d]*)(?=.{})".format(extension), + basename, + ) + if found: + return found.pop() + @property def clip_data(self): """Clip's xml clip data @@ -851,13 +926,13 @@ class MediaInfoFile(object): raise IOError("Media Scirpt does not exist: `{}`".format( self.MEDIA_SCRIPT_PATH)) - def _generate_media_info_file(self, fpath): + def _generate_media_info_file(self, fpath, feed_ext, feed_dir): # Create cmd arguments for gettig xml file info file cmd_args = [ self.MEDIA_SCRIPT_PATH, - "-e", self.feed_ext, + "-e", feed_ext, "-o", fpath, - self.feed_dir + feed_dir ] try: @@ -867,7 +942,7 @@ class MediaInfoFile(object): raise TypeError( "Error creating `{}` due: {}".format(fpath, error)) - def _make_single_clip_media_info(self, fpath): + def _make_single_clip_media_info(self, fpath, feed_basename, path_pattern): with open(fpath) as f: lines = f.readlines() _added_root = itertools.chain( @@ -878,14 +953,32 @@ class MediaInfoFile(object): xml_clips = new_root.findall("clip") matching_clip = None for xml_clip in xml_clips: - if xml_clip.find("name").text in self.feed_basename: - matching_clip = xml_clip + clip_name = xml_clip.find("name").text + log.debug("__ clip_name: `{}`".format(clip_name)) + if clip_name not in feed_basename: + continue + + # test path pattern + for out_track in xml_clip.iter("track"): + for out_feed in out_track.iter("feed"): + for span in out_feed.iter("span"): + # start frame + span_path = span.find("path") + if not span_path: + continue + log.debug( + "__ span_path.text: {}, path_pattern: {}".format( + span_path.text, path_pattern + ) + ) + if path_pattern in span_path.text: + matching_clip = xml_clip if matching_clip is None: # return warning there is missing clip raise ET.ParseError( "Missing clip in `{}`. Available clips {}".format( - self.feed_basename, [ + feed_basename, [ xml_clip.find("name").text for xml_clip in xml_clips ] @@ -912,8 +1005,6 @@ class MediaInfoFile(object): 'startTimecode/dropMode') self.drop_mode = out_feed_drop_mode_obj.text break - else: - continue except Exception as msg: self.log.warning(msg) From c34ecfd9127004e0c739be7bb0efdb11d162c447 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 11 May 2022 13:35:35 +0200 Subject: [PATCH 224/398] flame: fixing logger --- openpype/hosts/flame/api/lib.py | 8 ++++---- openpype/hosts/flame/otio/flame_export.py | 4 +++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/flame/api/lib.py b/openpype/hosts/flame/api/lib.py index 15e6f8ae80..6fff3edc30 100644 --- a/openpype/hosts/flame/api/lib.py +++ b/openpype/hosts/flame/api/lib.py @@ -804,7 +804,7 @@ class MediaInfoFile(object): def _get_collection(self, feed_basename, feed_dir, feed_ext): partialname = self._separate_file_head(feed_basename, feed_ext) - log.debug("__ partialname: {}".format(partialname)) + self.log.debug("__ partialname: {}".format(partialname)) # make sure partial input basename is having correct extensoon if not partialname: @@ -841,7 +841,7 @@ class MediaInfoFile(object): _continues_colls = collection.separate() for _coll in _continues_colls: coll_to_text = _coll.format("{head}[{range}]{tail}") - log.debug("__ coll_to_text: {}".format(coll_to_text)) + self.log.debug("__ coll_to_text: {}".format(coll_to_text)) if number_from_path in coll_to_text: return coll_to_text @@ -954,7 +954,7 @@ class MediaInfoFile(object): matching_clip = None for xml_clip in xml_clips: clip_name = xml_clip.find("name").text - log.debug("__ clip_name: `{}`".format(clip_name)) + self.log.debug("__ clip_name: `{}`".format(clip_name)) if clip_name not in feed_basename: continue @@ -966,7 +966,7 @@ class MediaInfoFile(object): span_path = span.find("path") if not span_path: continue - log.debug( + self.log.debug( "__ span_path.text: {}, path_pattern: {}".format( span_path.text, path_pattern ) diff --git a/openpype/hosts/flame/otio/flame_export.py b/openpype/hosts/flame/otio/flame_export.py index fc960b670c..c54ebb43d3 100644 --- a/openpype/hosts/flame/otio/flame_export.py +++ b/openpype/hosts/flame/otio/flame_export.py @@ -280,7 +280,9 @@ def create_otio_clip(clip_data): segment = clip_data["PySegment"] # calculate source in - media_info = MediaInfoFile(clip_data["fpath"]) + media_info = MediaInfoFile(clip_data["fpath"], **{ + "logger": log + }) media_timecode_start = media_info.start_frame media_fps = media_info.fps From aac7797f06934dfc123c8a97e53106b6c442c54b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 11 May 2022 14:26:36 +0200 Subject: [PATCH 225/398] flame: check collection first --- openpype/hosts/flame/api/lib.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/flame/api/lib.py b/openpype/hosts/flame/api/lib.py index 6fff3edc30..8e2ef2d624 100644 --- a/openpype/hosts/flame/api/lib.py +++ b/openpype/hosts/flame/api/lib.py @@ -782,11 +782,15 @@ class MediaInfoFile(object): self.log.info("Temp File: {}".format(tmp_path)) self._generate_media_info_file(tmp_path, feed_ext, feed_dir) - if os.path.exists(os.path.join(feed_dir, feed_basename)): + # get collection containing feed_basename from path + test_fname = self._get_collection( + feed_basename, feed_dir, feed_ext) + + if ( + not test_fname + and os.path.exists(os.path.join(feed_dir, feed_basename)) + ): test_fname = feed_basename - else: - # get collection containing feed_basename from path - test_fname = self._get_collection(feed_basename, feed_dir, feed_ext) # get clip data and make them single if there is multiple # clips data From 497094cabc84184143969ecab9e4473df252745e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 11 May 2022 14:30:44 +0200 Subject: [PATCH 226/398] Remove invalid submodules --- 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 159d2f23e4..0000000000 --- a/repos/avalon-core +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 159d2f23e4c79c04dfac57b68d2ee6ac67adec1b 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 b7f7af46fbd00e8b62822efd8b999f28fdb5bf6b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 11 May 2022 14:47:34 +0200 Subject: [PATCH 227/398] flame: redundant condition --- openpype/hosts/flame/api/lib.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/flame/api/lib.py b/openpype/hosts/flame/api/lib.py index 8e2ef2d624..660466b0aa 100644 --- a/openpype/hosts/flame/api/lib.py +++ b/openpype/hosts/flame/api/lib.py @@ -968,8 +968,6 @@ class MediaInfoFile(object): for span in out_feed.iter("span"): # start frame span_path = span.find("path") - if not span_path: - continue self.log.debug( "__ span_path.text: {}, path_pattern: {}".format( span_path.text, path_pattern From 7f1b8f7f038d63cd53ee36e6d419aff62271adb3 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 11 May 2022 16:00:30 +0200 Subject: [PATCH 228/398] flame: adding docstrings --- openpype/hosts/flame/api/lib.py | 39 ++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/flame/api/lib.py b/openpype/hosts/flame/api/lib.py index 660466b0aa..7125a540a0 100644 --- a/openpype/hosts/flame/api/lib.py +++ b/openpype/hosts/flame/api/lib.py @@ -807,12 +807,26 @@ class MediaInfoFile(object): self.clip_data = xml_data def _get_collection(self, feed_basename, feed_dir, feed_ext): + """ Get collection string + + Args: + feed_basename (str): file base name + feed_dir (str): file's directory + feed_ext (str): file extension + + Raises: + AttributeError: feed_ext is not matching feed_basename + IOError: Failing on not correct input data + + Returns: + str: collection basename with range of sequence + """ partialname = self._separate_file_head(feed_basename, feed_ext) self.log.debug("__ partialname: {}".format(partialname)) # make sure partial input basename is having correct extensoon if not partialname: - raise IOError("File doesnt exists. Basename - {}, Ext - {}".format( + raise AttributeError("Wrong input attributes. Basename - {}, Ext - {}".format( feed_basename, feed_ext )) @@ -850,6 +864,15 @@ class MediaInfoFile(object): return coll_to_text def _separate_file_head(self, basename, extension): + """ Get only head with out sequence and extension + + Args: + basename (str): file base name + extension (str): file extension + + Returns: + str: file head + """ # in case sequence file found = re.findall( r"(.*)[._][\d]*(?=.{})".format(extension), @@ -865,6 +888,15 @@ class MediaInfoFile(object): return name def _separate_number(self, basename, extension): + """ Get only sequence number as string + + Args: + basename (str): file base name + extension (str): file extension + + Returns: + str: number with padding + """ # in case sequence file found = re.findall( r"[._]([\d]*)(?=.{})".format(extension), @@ -989,6 +1021,11 @@ class MediaInfoFile(object): return matching_clip def _get_time_info_from_origin(self, xml_data): + """Set time info to class attributes + + Args: + xml_data (ET.Element): clip data + """ try: for out_track in xml_data.iter('track'): for out_feed in out_track.iter('feed'): From ac16e1f8bb6f79a3dca750848b5d7450571ff6b3 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 11 May 2022 16:05:22 +0200 Subject: [PATCH 229/398] flame: adding more docstring --- openpype/hosts/flame/api/lib.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/openpype/hosts/flame/api/lib.py b/openpype/hosts/flame/api/lib.py index 7125a540a0..03e3d117a3 100644 --- a/openpype/hosts/flame/api/lib.py +++ b/openpype/hosts/flame/api/lib.py @@ -963,6 +963,16 @@ class MediaInfoFile(object): self.MEDIA_SCRIPT_PATH)) def _generate_media_info_file(self, fpath, feed_ext, feed_dir): + """ Generate media info xml .clip file + + Args: + fpath (str): .clip file path + feed_ext (str): file extension to be filtered + feed_dir (str): look up directory + + Raises: + TypeError: Type error if it fails + """ # Create cmd arguments for gettig xml file info file cmd_args = [ self.MEDIA_SCRIPT_PATH, @@ -979,6 +989,19 @@ class MediaInfoFile(object): "Error creating `{}` due: {}".format(fpath, error)) def _make_single_clip_media_info(self, fpath, feed_basename, path_pattern): + """ Separate only relative clip object form .clip file + + Args: + fpath (str): clip file path + feed_basename (str): search basename + path_pattern (str): search file pattern (file.[1-2].exr) + + Raises: + ET.ParseError: if nothing found + + Returns: + ET.Element: xml element data of matching clip + """ with open(fpath) as f: lines = f.readlines() _added_root = itertools.chain( From 7b225feb13778872f52a56bc5cc0a54d6d652d4c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 11 May 2022 16:36:29 +0200 Subject: [PATCH 230/398] it is properly checked for not allowed characters --- openpype/lib/transcoding.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index f20bef3854..526a73de08 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -624,14 +624,13 @@ def convert_input_paths_for_ffmpeg( len(attr_value) ) - if erase_attribute: - for char in NOT_ALLOWED_FFMPEG_CHARS: - if char in attr_value: - erase_attribute = True - erase_reason = ( - "contains unsupported character \"{}\"." - ).format(char) - break + for char in NOT_ALLOWED_FFMPEG_CHARS: + if char in attr_value: + erase_attribute = True + erase_reason = ( + "contains unsupported character \"{}\"." + ).format(char) + break if erase_attribute: # Set attribute to empty string From ea54b0dc2512eeb428b1f3ea702a10a55ac6ccf3 Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Mon, 9 May 2022 10:28:09 +0200 Subject: [PATCH 231/398] refactor --- openpype/hosts/nuke/startup/menu.py | 31 +++++++++++++++++++ .../defaults/project_settings/nuke.json | 4 +++ .../projects_schema/schema_project_maya.json | 2 +- .../projects_schema/schema_project_nuke.json | 4 +++ ...riptsmenu.json => schema_scriptsmenu.json} | 0 5 files changed, 40 insertions(+), 1 deletion(-) rename openpype/settings/entities/schemas/projects_schema/schemas/{schema_maya_scriptsmenu.json => schema_scriptsmenu.json} (100%) diff --git a/openpype/hosts/nuke/startup/menu.py b/openpype/hosts/nuke/startup/menu.py index 9ed43b2110..dee1d9d868 100644 --- a/openpype/hosts/nuke/startup/menu.py +++ b/openpype/hosts/nuke/startup/menu.py @@ -1,5 +1,7 @@ import nuke +import os +import avalon.api from openpype.api import Logger from openpype.pipeline import install_host from openpype.hosts.nuke import api @@ -9,6 +11,7 @@ from openpype.hosts.nuke.api.lib import ( WorkfileSettings, dirmap_file_name_filter ) +from openpype.settings import get_project_settings log = Logger.get_logger(__name__) @@ -28,3 +31,31 @@ nuke.addOnScriptLoad(WorkfileSettings().set_context_settings) nuke.addFilenameFilter(dirmap_file_name_filter) log.info('Automatic syncing of write file knob to script version') + + +def add_scripts_menu(): + try: + from scriptsmenu import launchfornuke + except ImportError: + log.warning( + "Skipping studio.menu install, because " + "'scriptsmenu' module seems unavailable." + ) + return + + # load configuration of custom menu + project_settings = get_project_settings(os.getenv("AVALON_PROJECT")) + config = project_settings["nuke"]["scriptsmenu"]["definition"] + _menu = project_settings["nuke"]["scriptsmenu"]["name"] + + if not config: + log.warning("Skipping studio menu, no definition found.") + return + + # run the launcher for Maya menu + studio_menu = launchfornuke.main(title=_menu.title()) + + # apply configuration + studio_menu.build_from_configuration(studio_menu, config) + +add_scripts_menu() diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index 128d440732..a9d284873c 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -15,6 +15,10 @@ "destination-path": [] } }, + "scriptsmenu": { + "name": "OpenPype Tools", + "definition": [] + }, "create": { "CreateWriteRender": { "fpath_template": "{work}/renders/nuke/{subset}/{subset}.{frame}.{ext}", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json index cc70516c72..0c7943447b 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json @@ -49,7 +49,7 @@ }, { "type": "schema", - "name": "schema_maya_scriptsmenu" + "name": "schema_scriptsmenu" }, { "type": "schema", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json b/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json index bc572cbdc8..1ae4efd8ea 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json @@ -79,6 +79,10 @@ } ] }, + { + "type": "schema", + "name": "schema_scriptsmenu" + }, { "type": "dict", "collapsible": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_scriptsmenu.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_scriptsmenu.json similarity index 100% rename from openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_scriptsmenu.json rename to openpype/settings/entities/schemas/projects_schema/schemas/schema_scriptsmenu.json From 8a1f7c10061387b03674eb0e21faf05f0bf8fbd5 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 11 May 2022 17:12:54 +0200 Subject: [PATCH 232/398] flame: fix for single file use --- openpype/hosts/flame/api/lib.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/flame/api/lib.py b/openpype/hosts/flame/api/lib.py index 03e3d117a3..933cfbe267 100644 --- a/openpype/hosts/flame/api/lib.py +++ b/openpype/hosts/flame/api/lib.py @@ -839,14 +839,13 @@ class MediaInfoFile(object): # ignore reminders as we dont need them collections = clique.assemble(files)[0] - # if no collection rise + # in case no collection found return None + # it is probably just single file if not collections: - raise IOError("_get_collection is failing on: {} {} {}".format( - feed_basename, feed_dir, feed_ext - )) - else: - # we expect only one collection - collection = collections[0] + return + + # we expect only one collection + collection = collections[0] if collection.is_contiguous(): # if no holes then return collection From f7d4cbecfe36826865cb3aba3e33da10fd0aeb71 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Mon, 2 May 2022 17:17:29 +0200 Subject: [PATCH 233/398] add the scriptsmenu schema to nuke --- .../projects_schema/schema_project_nuke.json | 4 ++++ .../schemas/schema_nuke_scriptsmenu.json | 22 +++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_scriptsmenu.json diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json b/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json index 1ae4efd8ea..1fc925557b 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json @@ -290,6 +290,10 @@ } ] }, + { + "type": "schema", + "name": "schema_nuke_scriptsmenu" + }, { "type": "schema", "name": "schema_nuke_publish", diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_scriptsmenu.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_scriptsmenu.json new file mode 100644 index 0000000000..e841d6ba77 --- /dev/null +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_scriptsmenu.json @@ -0,0 +1,22 @@ +{ + "type": "dict", + "collapsible": true, + "key": "scriptsmenu", + "label": "Scripts Menu Definition", + "children": [ + { + "type": "text", + "key": "name", + "label": "Menu Name" + }, + { + "type": "splitter" + }, + { + "type": "raw-json", + "key": "definition", + "label": "Menu definition", + "is_list": true + } + ] +} \ No newline at end of file From 9640a7463bb4cd7c22bbce35bf28de1c1c6809e9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 11 May 2022 17:21:40 +0200 Subject: [PATCH 234/398] fix not allowed characters properly --- openpype/lib/transcoding.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index 526a73de08..adb9bb2c3a 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -493,8 +493,9 @@ def convert_for_ffmpeg( erase_reason = "has too long value ({} chars).".format( len(attr_value) ) + erase_attribute = True - if erase_attribute: + if not erase_attribute: for char in NOT_ALLOWED_FFMPEG_CHARS: if char in attr_value: erase_attribute = True @@ -623,14 +624,16 @@ def convert_input_paths_for_ffmpeg( erase_reason = "has too long value ({} chars).".format( len(attr_value) ) + erase_attribute = True - for char in NOT_ALLOWED_FFMPEG_CHARS: - if char in attr_value: - erase_attribute = True - erase_reason = ( - "contains unsupported character \"{}\"." - ).format(char) - break + if not erase_attribute: + for char in NOT_ALLOWED_FFMPEG_CHARS: + if char in attr_value: + erase_attribute = True + erase_reason = ( + "contains unsupported character \"{}\"." + ).format(char) + break if erase_attribute: # Set attribute to empty string From c90a949076a1c8d2237fcc6ec118549344a50343 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Tue, 3 May 2022 12:41:37 +0200 Subject: [PATCH 235/398] call the launchfornuke module from the nuke menu.py to generate custom menu --- openpype/hosts/nuke/startup/menu.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/nuke/startup/menu.py b/openpype/hosts/nuke/startup/menu.py index dee1d9d868..3a0bfdb28f 100644 --- a/openpype/hosts/nuke/startup/menu.py +++ b/openpype/hosts/nuke/startup/menu.py @@ -58,4 +58,5 @@ def add_scripts_menu(): # apply configuration studio_menu.build_from_configuration(studio_menu, config) + add_scripts_menu() From a18c4d3d16e5454f86cd1b4f539c6e3031bd7c9c Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Tue, 3 May 2022 17:27:32 +0200 Subject: [PATCH 236/398] add a function in the nuke menu.py module to also add gizmos --- openpype/hosts/nuke/startup/menu.py | 58 +++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/openpype/hosts/nuke/startup/menu.py b/openpype/hosts/nuke/startup/menu.py index 3a0bfdb28f..bb81ee7fac 100644 --- a/openpype/hosts/nuke/startup/menu.py +++ b/openpype/hosts/nuke/startup/menu.py @@ -1,5 +1,6 @@ import nuke import os +import json import avalon.api from openpype.api import Logger @@ -59,4 +60,61 @@ def add_scripts_menu(): studio_menu.build_from_configuration(studio_menu, config) +def add_gizmos(): + """ Build a custom gizmo menu from a yaml description file. + """ + quad_plugin_path = os.environ.get("QUAD_PLUGIN_PATH") + gizmos_folder = os.path.join(quad_plugin_path, 'nuke/gizmos') + icons_folder = os.path.join(quad_plugin_path, 'nuke/icons') + json_file = os.path.join(quad_plugin_path, 'nuke/toolbar.json') + + if os.path.isdir(gizmos_folder): + for p in os.listdir(gizmos_folder): + if os.path.isdir(os.path.join(gizmos_folder, p)): + nuke.pluginAddPath(os.path.join(gizmos_folder, p)) + nuke.pluginAddPath(gizmos_folder) + + with open(json_file, 'rb') as fd: + try: + data = json.loads(fd.read()) + except Exception as e: + print(f"Problem occurs when reading toolbar file: {e}") + return + + if data is None or not isinstance(data, list): + # return early if the json file is empty or not well structured + return + + bar = nuke.menu("Nodes") + menu = bar.addMenu( + "FixStudio", + icon=os.path.join(icons_folder, 'fixstudio.png') + ) + + # populate the menu + for entry in data: + # make fail if the name or command key doesn't exists + name = entry['name'] + + command = entry.get('command', "") + + if command.find('{pipe_path}') > -1: + command = command.format(pipe_path=os.environ['QUAD_PLUGIN_PATH']) + + hotkey = entry.get('hotkey', "") + icon = entry.get('icon', "") + + parent_name = os.path.dirname(name) + + if 'separator' in name: + current = menu.findItem(parent_name) + if current: + current.addSeparator() + else: + menu.addCommand( + name, command=command, shortcut=hotkey, icon=icon, + ) + + +add_gizmos() add_scripts_menu() From f479dd8635b95c1463bab44d79c85d19fc72095e Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Tue, 3 May 2022 17:54:30 +0200 Subject: [PATCH 237/398] Revert "add a function in the nuke menu.py module to also add gizmos" This reverts commit 2f3bafb2fb8cbf247c354a8904acbc78ae081731. --- openpype/hosts/nuke/startup/menu.py | 58 ----------------------------- 1 file changed, 58 deletions(-) diff --git a/openpype/hosts/nuke/startup/menu.py b/openpype/hosts/nuke/startup/menu.py index bb81ee7fac..3a0bfdb28f 100644 --- a/openpype/hosts/nuke/startup/menu.py +++ b/openpype/hosts/nuke/startup/menu.py @@ -1,6 +1,5 @@ import nuke import os -import json import avalon.api from openpype.api import Logger @@ -60,61 +59,4 @@ def add_scripts_menu(): studio_menu.build_from_configuration(studio_menu, config) -def add_gizmos(): - """ Build a custom gizmo menu from a yaml description file. - """ - quad_plugin_path = os.environ.get("QUAD_PLUGIN_PATH") - gizmos_folder = os.path.join(quad_plugin_path, 'nuke/gizmos') - icons_folder = os.path.join(quad_plugin_path, 'nuke/icons') - json_file = os.path.join(quad_plugin_path, 'nuke/toolbar.json') - - if os.path.isdir(gizmos_folder): - for p in os.listdir(gizmos_folder): - if os.path.isdir(os.path.join(gizmos_folder, p)): - nuke.pluginAddPath(os.path.join(gizmos_folder, p)) - nuke.pluginAddPath(gizmos_folder) - - with open(json_file, 'rb') as fd: - try: - data = json.loads(fd.read()) - except Exception as e: - print(f"Problem occurs when reading toolbar file: {e}") - return - - if data is None or not isinstance(data, list): - # return early if the json file is empty or not well structured - return - - bar = nuke.menu("Nodes") - menu = bar.addMenu( - "FixStudio", - icon=os.path.join(icons_folder, 'fixstudio.png') - ) - - # populate the menu - for entry in data: - # make fail if the name or command key doesn't exists - name = entry['name'] - - command = entry.get('command', "") - - if command.find('{pipe_path}') > -1: - command = command.format(pipe_path=os.environ['QUAD_PLUGIN_PATH']) - - hotkey = entry.get('hotkey', "") - icon = entry.get('icon', "") - - parent_name = os.path.dirname(name) - - if 'separator' in name: - current = menu.findItem(parent_name) - if current: - current.addSeparator() - else: - menu.addCommand( - name, command=command, shortcut=hotkey, icon=icon, - ) - - -add_gizmos() add_scripts_menu() From 4e694a3f36f740fe9b5488cf75ba421e4b520d97 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Mon, 9 May 2022 11:27:51 +0200 Subject: [PATCH 238/398] changes from comments --- .../entities/schemas/projects_schema/schema_project_nuke.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json b/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json index 1fc925557b..1ae4efd8ea 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json @@ -290,10 +290,6 @@ } ] }, - { - "type": "schema", - "name": "schema_nuke_scriptsmenu" - }, { "type": "schema", "name": "schema_nuke_publish", From 2eea2522be0ecce65c8fc5876a628b15169f3189 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Mon, 9 May 2022 11:31:05 +0200 Subject: [PATCH 239/398] delete schema_nuke_scriptsmenu.json since we use a schema_scriptsmenu.json for both maya and nuke --- .../schemas/schema_nuke_scriptsmenu.json | 22 ------------------- 1 file changed, 22 deletions(-) delete mode 100644 openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_scriptsmenu.json diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_scriptsmenu.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_scriptsmenu.json deleted file mode 100644 index e841d6ba77..0000000000 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_scriptsmenu.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "type": "dict", - "collapsible": true, - "key": "scriptsmenu", - "label": "Scripts Menu Definition", - "children": [ - { - "type": "text", - "key": "name", - "label": "Menu Name" - }, - { - "type": "splitter" - }, - { - "type": "raw-json", - "key": "definition", - "label": "Menu definition", - "is_list": true - } - ] -} \ No newline at end of file From f081fe3521158cba9425f86a866fd50fc8106f6f Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Mon, 9 May 2022 11:58:28 +0200 Subject: [PATCH 240/398] add nuke doc for custom menu --- website/docs/admin_hosts_nuke.md | 14 ++++++++++++++ website/docs/assets/nuke-admin_scriptsmenu.png | Bin 0 -> 21712 bytes website/sidebars.js | 1 + 3 files changed, 15 insertions(+) create mode 100644 website/docs/admin_hosts_nuke.md create mode 100644 website/docs/assets/nuke-admin_scriptsmenu.png diff --git a/website/docs/admin_hosts_nuke.md b/website/docs/admin_hosts_nuke.md new file mode 100644 index 0000000000..46f596a2dc --- /dev/null +++ b/website/docs/admin_hosts_nuke.md @@ -0,0 +1,14 @@ +--- +id: admin_hosts_nuke +title: Nuke +sidebar_label: Nuke +--- + +## Custom Menu +You can add your custom tools menu into Nuke by extending definitions in **Nuke -> Scripts Menu Definition**. +![Custom menu definition](assets/nuke-admin_scriptsmenu.png) + +:::note Work in progress +This is still work in progress. Menu definition will be handled more friendly with widgets and not +raw json. +::: diff --git a/website/docs/assets/nuke-admin_scriptsmenu.png b/website/docs/assets/nuke-admin_scriptsmenu.png new file mode 100644 index 0000000000000000000000000000000000000000..cad2a4411d058a35995863a57bdbcc4cd2793457 GIT binary patch literal 21712 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 literal 0 HcmV?d00001 diff --git a/website/sidebars.js b/website/sidebars.js index 105afc30eb..0e33bed949 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -88,6 +88,7 @@ module.exports = { items: [ "admin_hosts_blender", "admin_hosts_maya", + "admin_hosts_nuke", "admin_hosts_resolve", "admin_hosts_harmony", "admin_hosts_aftereffects", From d5fa437912b8f03dfa705967c60fe2212065996a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 11 May 2022 17:51:11 +0200 Subject: [PATCH 241/398] flame: thumbnail frame number if not `Sequence Publish` --- .../flame/plugins/publish/extract_subset_resources.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py index fd0ece2590..7dcaec7eee 100644 --- a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py +++ b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py @@ -236,6 +236,17 @@ class ExtractSubsetResources(openpype.api.Extractor): # define kwargs based on preset type if "thumbnail" in unique_name: + if export_type != "Sequence Publish": + # if not sequence preset + in_mark = int(source_start_handles - source_first_frame) + + self.log.debug("__ in_mark: {}".format(in_mark)) + self.log.debug("__ source_duration_handles: {}".format( + source_duration_handles)) + self.log.debug("__ thumb_frame_number: {}".format( + int(in_mark + (source_duration_handles / 2)) + )) + export_kwargs["thumb_frame_number"] = int(in_mark + ( source_duration_handles / 2)) else: From 8f24a764fd3f5061573931757e0eb6817949e77f Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 11 May 2022 18:02:22 +0200 Subject: [PATCH 242/398] remove avalon.api import --- openpype/hosts/nuke/startup/menu.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/nuke/startup/menu.py b/openpype/hosts/nuke/startup/menu.py index 3a0bfdb28f..49edb22a89 100644 --- a/openpype/hosts/nuke/startup/menu.py +++ b/openpype/hosts/nuke/startup/menu.py @@ -1,7 +1,6 @@ import nuke import os -import avalon.api from openpype.api import Logger from openpype.pipeline import install_host from openpype.hosts.nuke import api From 3615c79fdd338044f3a8fe4d2bca70996b2201f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Wed, 11 May 2022 18:29:45 +0200 Subject: [PATCH 243/398] handle subsetGroup in attached renders --- .../modules/deadline/plugins/publish/submit_publish_job.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index ffa5718d4a..782f85c9d2 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -468,8 +468,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): new_instance = copy(instance_data) new_instance["subset"] = subset_name - if not instance_data.get("append"): - new_instance["subsetGroup"] = group_name + new_instance["subsetGroup"] = group_name if preview: new_instance["review"] = True @@ -886,6 +885,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): new_i["subset"] = at.get("subset") new_i["family"] = at.get("family") new_i["append"] = True + # don't set subsetGroup if we are attaching + new_i.pop("subsetGroup") new_instances.append(new_i) self.log.info(" - {} / v{}".format( at.get("subset"), at.get("version"))) From 8892422b0edb6182a119dc848bdfa534e344744b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 11 May 2022 21:22:01 +0200 Subject: [PATCH 244/398] flame: attempt to solve issue with single frame imported clip --- openpype/hosts/flame/api/lib.py | 24 ++++++++++++++---- .../publish/extract_subset_resources.py | 25 ++++++++++++++----- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/flame/api/lib.py b/openpype/hosts/flame/api/lib.py index 933cfbe267..80818fbbfd 100644 --- a/openpype/hosts/flame/api/lib.py +++ b/openpype/hosts/flame/api/lib.py @@ -763,6 +763,7 @@ class MediaInfoFile(object): _start_frame = None _fps = None _drop_mode = None + _file_pattern = None def __init__(self, path, **kwargs): @@ -778,24 +779,25 @@ class MediaInfoFile(object): feed_dir = os.path.dirname(path) feed_ext = os.path.splitext(feed_basename)[1][1:].lower() + with maintained_temp_file_path(".clip") as tmp_path: self.log.info("Temp File: {}".format(tmp_path)) self._generate_media_info_file(tmp_path, feed_ext, feed_dir) # get collection containing feed_basename from path - test_fname = self._get_collection( + self.file_pattern = self._get_collection( feed_basename, feed_dir, feed_ext) if ( - not test_fname + not self.file_pattern and os.path.exists(os.path.join(feed_dir, feed_basename)) ): - test_fname = feed_basename + self.file_pattern = feed_basename # get clip data and make them single if there is multiple # clips data xml_data = self._make_single_clip_media_info( - tmp_path, feed_basename, test_fname) + tmp_path, feed_basename, self.file_pattern) self.log.debug("xml_data: {}".format(xml_data)) self.log.debug("type: {}".format(type(xml_data))) @@ -816,7 +818,6 @@ class MediaInfoFile(object): Raises: AttributeError: feed_ext is not matching feed_basename - IOError: Failing on not correct input data Returns: str: collection basename with range of sequence @@ -956,6 +957,19 @@ class MediaInfoFile(object): def drop_mode(self, text): self._drop_mode = str(text) + @property + def file_pattern(self): + """Clips file patter + + Returns: + str: file pattern. ex. file.[1-2].exr + """ + return self._file_pattern + + @file_pattern.setter + def file_pattern(self, fpattern): + self._file_pattern = fpattern + def _validate_media_script_path(self): if not os.path.isfile(self.MEDIA_SCRIPT_PATH): raise IOError("Media Scirpt does not exist: `{}`".format( diff --git a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py index 7dcaec7eee..d8cc14a506 100644 --- a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py +++ b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py @@ -6,6 +6,7 @@ from copy import deepcopy import pyblish.api import openpype.api from openpype.hosts.flame import api as opfapi +from openpype.hosts.flame.api import MediaInfoFile import flame @@ -67,6 +68,7 @@ class ExtractSubsetResources(openpype.api.Extractor): instance.data["representations"] = [] # flame objects + self.project = instance.context.data["flameProject"] segment = instance.data["item"] asset_name = instance.data["asset"] segment_name = segment.name.get_value() @@ -239,16 +241,18 @@ class ExtractSubsetResources(openpype.api.Extractor): if export_type != "Sequence Publish": # if not sequence preset in_mark = int(source_start_handles - source_first_frame) + thumb_frame_number = int(in_mark + ( + source_duration_handles / 2)) + else: + thumb_frame_number = int(in_mark + ( + (clip_out - clip_in) / 2)) self.log.debug("__ in_mark: {}".format(in_mark)) - self.log.debug("__ source_duration_handles: {}".format( - source_duration_handles)) self.log.debug("__ thumb_frame_number: {}".format( - int(in_mark + (source_duration_handles / 2)) + thumb_frame_number )) - export_kwargs["thumb_frame_number"] = int(in_mark + ( - source_duration_handles / 2)) + export_kwargs["thumb_frame_number"] = thumb_frame_number else: export_kwargs.update({ "in_mark": in_mark, @@ -419,7 +423,16 @@ class ExtractSubsetResources(openpype.api.Extractor): """ Import clip from path """ - clips = flame.import_clips(path) + media_info = MediaInfoFile(path, **{ + "logger": self.log + }) + file_pattern = media_info.file_pattern + self.log.debug("__ file_pattern: {}".format(file_pattern)) + + project_desktop = self.project.current_workspace.desktop + reel = project_desktop.reel_groups[0].reels[0] + + clips = flame.import_clips(file_pattern, reel) self.log.info("Clips [{}] imported from `{}`".format(clips, path)) if not clips: self.log.warning("Path `{}` is not having any clips".format(path)) From 12abc9e2bec0715836bd7d0ad3fe740b92e6eab4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 11 May 2022 21:31:12 +0200 Subject: [PATCH 245/398] flame: thumbnails and mov presets are created correctly now --- .../flame/plugins/publish/extract_subset_resources.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py index d8cc14a506..6098f2e1e9 100644 --- a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py +++ b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py @@ -423,17 +423,19 @@ class ExtractSubsetResources(openpype.api.Extractor): """ Import clip from path """ + dir_path = os.path.dirname(path) media_info = MediaInfoFile(path, **{ "logger": self.log }) file_pattern = media_info.file_pattern self.log.debug("__ file_pattern: {}".format(file_pattern)) - project_desktop = self.project.current_workspace.desktop - reel = project_desktop.reel_groups[0].reels[0] + # rejoin the pattern to dir path + new_path = os.path.join(dir_path, file_pattern) - clips = flame.import_clips(file_pattern, reel) + clips = flame.import_clips(new_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 From df1b5c6e66cdf48f5f26fca6812d208ad24dd5b3 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 12 May 2022 08:57:04 +0200 Subject: [PATCH 246/398] flame: reducing code redundancy --- .../publish/extract_subset_resources.py | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py index 6098f2e1e9..176629fbfc 100644 --- a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py +++ b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py @@ -68,7 +68,6 @@ class ExtractSubsetResources(openpype.api.Extractor): instance.data["representations"] = [] # flame objects - self.project = instance.context.data["flameProject"] segment = instance.data["item"] asset_name = instance.data["asset"] segment_name = segment.name.get_value() @@ -182,15 +181,15 @@ class ExtractSubsetResources(openpype.api.Extractor): name_patern_xml = ( "__{}.").format( unique_name) + + # change in/out marks to timeline in/out + in_mark = clip_in + out_mark = clip_out 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 - out_mark = clip_out - # add xml tags modifications modify_xml_data.update({ "exportHandles": True, @@ -238,14 +237,8 @@ class ExtractSubsetResources(openpype.api.Extractor): # define kwargs based on preset type if "thumbnail" in unique_name: - if export_type != "Sequence Publish": - # if not sequence preset - in_mark = int(source_start_handles - source_first_frame) - thumb_frame_number = int(in_mark + ( - source_duration_handles / 2)) - else: - thumb_frame_number = int(in_mark + ( - (clip_out - clip_in) / 2)) + thumb_frame_number = int(in_mark + ( + source_duration_handles / 2)) self.log.debug("__ in_mark: {}".format(in_mark)) self.log.debug("__ thumb_frame_number: {}".format( From 2a94ac1248446d8735bf34ebda62356af5658358 Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Thu, 12 May 2022 10:10:57 +0200 Subject: [PATCH 247/398] Add doc to default value --- openpype/settings/defaults/project_settings/nuke.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index a9d284873c..7f916424ed 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -17,7 +17,15 @@ }, "scriptsmenu": { "name": "OpenPype Tools", - "definition": [] + "definition": [ + { + "type": "action", + "sourcetype": "python", + "title": "OpenPype Docs", + "command": "import webbrowser;webbrowser.open(url='https://openpype.io/docs/artist_hosts_nuke_tut')", + "tooltip": "Open the OpenPype Nuke user doc page" + } + ] }, "create": { "CreateWriteRender": { From e7e6bf142f1ef4802059a03b41e58e4bc8f124fd Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Thu, 12 May 2022 10:13:02 +0200 Subject: [PATCH 248/398] Add maya 2023 to defaults --- .../system_settings/applications.json | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index 0fb99a2608..2b0de44fa9 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -12,6 +12,26 @@ "LC_ALL": "C" }, "variants": { + "2023": { + "use_python_2": false, + "executables": { + "windows": [ + "C:\\Program Files\\Autodesk\\Maya2023\\bin\\maya.exe" + ], + "darwin": [], + "linux": [ + "/usr/autodesk/maya2023/bin/maya" + ] + }, + "arguments": { + "windows": [], + "darwin": [], + "linux": [] + }, + "environment": { + "MAYA_VERSION": "2023" + } + }, "2022": { "use_python_2": false, "executables": { @@ -91,9 +111,6 @@ "environment": { "MAYA_VERSION": "2018" } - }, - "__dynamic_keys_labels__": { - "2022": "2022" } } }, From 2e5dd57fbf0600d6039a034826cbd34bc388ae59 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 12 May 2022 11:46:55 +0200 Subject: [PATCH 249/398] general: calculation of duration should not exclude one frame --- openpype/lib/editorial.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/lib/editorial.py b/openpype/lib/editorial.py index bf868953ea..1ee21deedc 100644 --- a/openpype/lib/editorial.py +++ b/openpype/lib/editorial.py @@ -17,7 +17,7 @@ def otio_range_to_frame_range(otio_range): start = _ot.to_frames( otio_range.start_time, otio_range.start_time.rate) end = start + _ot.to_frames( - otio_range.duration, otio_range.duration.rate) - 1 + otio_range.duration, otio_range.duration.rate) return start, end @@ -254,7 +254,7 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): media_in + source_in + offset_in) media_out_trimmed = ( media_in + source_in + ( - ((source_range.duration.value - 1) * abs( + (source_range.duration.value * abs( time_scalar)) + offset_out)) # calculate available handles From 1c43403ec5766ca59fd605451d12b31f362b6a4e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 12 May 2022 11:47:23 +0200 Subject: [PATCH 250/398] hiero: fitting new duration calculation --- openpype/hosts/hiero/api/otio/hiero_export.py | 2 +- openpype/hosts/hiero/plugins/publish/precollect_instances.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/hiero/api/otio/hiero_export.py b/openpype/hosts/hiero/api/otio/hiero_export.py index 1e4088d9c0..64fb81aed4 100644 --- a/openpype/hosts/hiero/api/otio/hiero_export.py +++ b/openpype/hosts/hiero/api/otio/hiero_export.py @@ -151,7 +151,7 @@ def create_otio_reference(clip): padding = media_source.filenamePadding() file_head = media_source.filenameHead() is_sequence = not media_source.singleFile() - frame_duration = media_source.duration() + frame_duration = media_source.duration() - 1 fps = utils.get_rate(clip) or self.project_fps extension = os.path.splitext(path)[-1] diff --git a/openpype/hosts/hiero/plugins/publish/precollect_instances.py b/openpype/hosts/hiero/plugins/publish/precollect_instances.py index 4eac6a008a..46f0b2440e 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_instances.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_instances.py @@ -296,6 +296,8 @@ class PrecollectInstances(pyblish.api.ContextPlugin): continue if otio_clip.name not in track_item.name(): continue + self.log.debug("__ parent_range: {}".format(parent_range)) + self.log.debug("__ timeline_range: {}".format(timeline_range)) if openpype.lib.is_overlapping_otio_ranges( parent_range, timeline_range, strict=True): From dd77f7cd9bff9bfa5405ba13aa5760675b1b2a89 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 12 May 2022 13:37:34 +0200 Subject: [PATCH 251/398] flame: fixing padding in collection ranges --- openpype/hosts/flame/api/lib.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/flame/api/lib.py b/openpype/hosts/flame/api/lib.py index 80818fbbfd..f2f5db184b 100644 --- a/openpype/hosts/flame/api/lib.py +++ b/openpype/hosts/flame/api/lib.py @@ -848,9 +848,10 @@ class MediaInfoFile(object): # we expect only one collection collection = collections[0] + self.log.debug("__ collection: {}".format(collection)) + if collection.is_contiguous(): - # if no holes then return collection - return collection.format("{head}[{range}]{tail}") + return self._format_collection(collection) # add `[` in front to make sure it want capture # shot name with the same number @@ -858,11 +859,25 @@ class MediaInfoFile(object): # convert to multiple collections _continues_colls = collection.separate() for _coll in _continues_colls: - coll_to_text = _coll.format("{head}[{range}]{tail}") + coll_to_text = self._format_collection(_coll) self.log.debug("__ coll_to_text: {}".format(coll_to_text)) if number_from_path in coll_to_text: return coll_to_text + @staticmethod + def _format_collection(collection): + # if no holes then return collection + head = collection.format("{head}") + tail = collection.format("{tail}") + range_template = "[{{:0{0}d}}-{{:0{0}d}}]".format( + len(str(max(collection.indexes)))) + ranges = range_template.format( + min(collection.indexes), + max(collection.indexes) + ) + # if no holes then return collection + return "{}{}{}".format(head, ranges, tail) + def _separate_file_head(self, basename, extension): """ Get only head with out sequence and extension From aeff57dab75cb1a37d0f971b96d36b3cebd0ef0e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 12 May 2022 15:08:38 +0200 Subject: [PATCH 252/398] flame: expanding retiming features --- openpype/hosts/flame/otio/flame_export.py | 74 ++++++++++++++--------- 1 file changed, 47 insertions(+), 27 deletions(-) diff --git a/openpype/hosts/flame/otio/flame_export.py b/openpype/hosts/flame/otio/flame_export.py index c54ebb43d3..e3801a0a4f 100644 --- a/openpype/hosts/flame/otio/flame_export.py +++ b/openpype/hosts/flame/otio/flame_export.py @@ -98,26 +98,26 @@ def _get_metadata(item): return {} -# def create_time_effects(otio_clip, clip_data): -# otio_effect = None +def create_time_effects(otio_clip, speed): + otio_effect = None -# # retime on track item -# if speed != 1.: -# # make effect -# otio_effect = otio.schema.LinearTimeWarp() -# otio_effect.name = "Speed" -# otio_effect.time_scalar = speed -# otio_effect.metadata = {} + # retime on track item + if speed != 1.: + # make effect + otio_effect = otio.schema.LinearTimeWarp() + otio_effect.name = "Speed" + otio_effect.time_scalar = speed + otio_effect.metadata = {} -# # freeze frame effect -# if speed == 0.: -# otio_effect = otio.schema.FreezeFrame() -# otio_effect.name = "FreezeFrame" -# otio_effect.metadata = {} + # freeze frame effect + if speed == 0.: + otio_effect = otio.schema.FreezeFrame() + otio_effect.name = "FreezeFrame" + otio_effect.metadata = {} -# if otio_effect: -# # add otio effect to clip effects -# otio_clip.effects.append(otio_effect) + if otio_effect: + # add otio effect to clip effects + otio_clip.effects.append(otio_effect) def _get_marker_color(flame_colour): @@ -205,7 +205,7 @@ def create_otio_markers(otio_item, item): otio_item.markers.append(otio_marker) -def create_otio_reference(clip_data, fps=None): +def create_otio_reference(clip_data, duration, fps=None): metadata = _get_metadata(clip_data) # get file info for path and start frame @@ -220,7 +220,6 @@ def create_otio_reference(clip_data, fps=None): # get padding and other file infos log.debug("_ path: {}".format(path)) - frame_duration = clip_data["source_duration"] otio_ex_ref_item = None is_sequence = frame_number = utils.get_frame_from_filename(file_name) @@ -247,7 +246,7 @@ def create_otio_reference(clip_data, fps=None): rate=fps, available_range=create_otio_time_range( frame_start, - frame_duration, + duration, fps ) ) @@ -263,7 +262,7 @@ def create_otio_reference(clip_data, fps=None): target_url=reformated_path, available_range=create_otio_time_range( frame_start, - frame_duration, + duration, fps ) ) @@ -286,19 +285,39 @@ def create_otio_clip(clip_data): media_timecode_start = media_info.start_frame media_fps = media_info.fps - # create media reference - media_reference = create_otio_reference(clip_data, media_fps) - # define first frame first_frame = media_timecode_start or utils.get_frame_from_filename( clip_data["fpath"]) or 0 - source_in = int(clip_data["source_in"]) - int(first_frame) + _clip_source_in = int(clip_data["source_in"]) + _clip_source_out = int(clip_data["source_out"]) + _clip_record_duration = int(clip_data["record_duration"]) + + # first solve if the reverse timing + speed = 1 + if clip_data["source_in"] > clip_data["source_out"]: + source_in = _clip_source_out - int(first_frame) + source_out = _clip_source_in - int(first_frame) + speed = -1 + else: + source_in = _clip_source_in - int(first_frame) + source_out = _clip_source_out - int(first_frame) + + source_duration = (source_out - source_in + 1) + + # secondly check if any change of speed + if source_duration != _clip_record_duration: + retime_speed = source_duration / _clip_record_duration + speed *= retime_speed + + # create media reference + media_reference = create_otio_reference( + clip_data, source_duration, media_fps) # creatae source range source_range = create_otio_time_range( source_in, - clip_data["record_duration"], + _clip_record_duration, CTX.get_fps() ) @@ -312,7 +331,8 @@ def create_otio_clip(clip_data): if MARKERS_INCLUDE: create_otio_markers(otio_clip, segment) - # create_time_effects(otio_clip, clip_data) + if speed != 1: + create_time_effects(otio_clip, speed) return otio_clip From ce250a6749cd0f6d6f220ada9830ed154ef4e481 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 12 May 2022 15:10:17 +0200 Subject: [PATCH 253/398] flame: debug logging --- openpype/hosts/flame/otio/flame_export.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/hosts/flame/otio/flame_export.py b/openpype/hosts/flame/otio/flame_export.py index e3801a0a4f..500b1a3eb1 100644 --- a/openpype/hosts/flame/otio/flame_export.py +++ b/openpype/hosts/flame/otio/flame_export.py @@ -305,6 +305,11 @@ def create_otio_clip(clip_data): source_duration = (source_out - source_in + 1) + log.debug("_ source_in: {}".format(source_in)) + log.debug("_ source_out: {}".format(source_out)) + log.debug("_ speed: {}".format(speed)) + log.debug("_ source_duration: {}".format(source_duration)) + # secondly check if any change of speed if source_duration != _clip_record_duration: retime_speed = source_duration / _clip_record_duration From ee274f81e33fdb8a9741f2c2d397355702de5699 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 12 May 2022 15:12:03 +0200 Subject: [PATCH 254/398] flame: solving issue with frame longer renders --- .../hosts/flame/plugins/publish/extract_subset_resources.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py index 176629fbfc..0e04336211 100644 --- a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py +++ b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py @@ -160,7 +160,7 @@ class ExtractSubsetResources(openpype.api.Extractor): # get frame range with handles for representation range frame_start_handle = frame_start - handle_start source_duration_handles = ( - source_end_handles - source_start_handles) + 1 + source_end_handles - source_start_handles) # define in/out marks in_mark = (source_start_handles - source_first_frame) + 1 From 3f917055c135be7b6ba984a6c1245384b3389c79 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 12 May 2022 15:21:15 +0200 Subject: [PATCH 255/398] flame: improving logging --- openpype/hosts/flame/otio/flame_export.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/flame/otio/flame_export.py b/openpype/hosts/flame/otio/flame_export.py index 500b1a3eb1..8562a766e9 100644 --- a/openpype/hosts/flame/otio/flame_export.py +++ b/openpype/hosts/flame/otio/flame_export.py @@ -305,15 +305,17 @@ def create_otio_clip(clip_data): source_duration = (source_out - source_in + 1) + # secondly check if any change of speed + if source_duration != _clip_record_duration: + retime_speed = source_duration / _clip_record_duration + log.debug("_ retime_speed: {}".format(retime_speed)) + speed *= retime_speed + log.debug("_ source_in: {}".format(source_in)) log.debug("_ source_out: {}".format(source_out)) log.debug("_ speed: {}".format(speed)) log.debug("_ source_duration: {}".format(source_duration)) - - # secondly check if any change of speed - if source_duration != _clip_record_duration: - retime_speed = source_duration / _clip_record_duration - speed *= retime_speed + log.debug("_ _clip_record_duration: {}".format(_clip_record_duration)) # create media reference media_reference = create_otio_reference( From 71bd7eb337bfc372b66973a10ff00d32c838a628 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 12 May 2022 15:24:38 +0200 Subject: [PATCH 256/398] flame: retime is float value --- openpype/hosts/flame/otio/flame_export.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/flame/otio/flame_export.py b/openpype/hosts/flame/otio/flame_export.py index 8562a766e9..08478d4b98 100644 --- a/openpype/hosts/flame/otio/flame_export.py +++ b/openpype/hosts/flame/otio/flame_export.py @@ -307,7 +307,7 @@ def create_otio_clip(clip_data): # secondly check if any change of speed if source_duration != _clip_record_duration: - retime_speed = source_duration / _clip_record_duration + retime_speed = float(source_duration / _clip_record_duration) log.debug("_ retime_speed: {}".format(retime_speed)) speed *= retime_speed From b105287b507d4c2dc112fe5d738206993419069d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 12 May 2022 17:29:12 +0200 Subject: [PATCH 257/398] extract jpeg exr does not create multiple representation names with "thumbnail" name --- openpype/plugins/publish/extract_jpeg_exr.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/publish/extract_jpeg_exr.py b/openpype/plugins/publish/extract_jpeg_exr.py index d6d6854092..06de858e4a 100644 --- a/openpype/plugins/publish/extract_jpeg_exr.py +++ b/openpype/plugins/publish/extract_jpeg_exr.py @@ -49,6 +49,7 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): return filtered_repres = self._get_filtered_repres(instance) + name_counter = 0 for repre in filtered_repres: repre_files = repre["files"] if not isinstance(repre_files, (list, tuple)): @@ -134,8 +135,12 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): self.log.warning("Conversion crashed", exc_info=True) raise + repre_name = "thumbnail" + if name_counter > 0: + repre_name += str(name_counter) + name_counter += 1 new_repre = { - "name": "thumbnail", + "name": repre_name, "ext": "jpg", "files": jpeg_file, "stagingDir": stagingdir, From bbdc0ce523c7c0a7c37e33c255785f8ef8d9445f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 12 May 2022 17:39:00 +0200 Subject: [PATCH 258/398] create only one thumbnail representation --- openpype/plugins/publish/extract_jpeg_exr.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/openpype/plugins/publish/extract_jpeg_exr.py b/openpype/plugins/publish/extract_jpeg_exr.py index 06de858e4a..ae29f8b95b 100644 --- a/openpype/plugins/publish/extract_jpeg_exr.py +++ b/openpype/plugins/publish/extract_jpeg_exr.py @@ -49,7 +49,7 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): return filtered_repres = self._get_filtered_repres(instance) - name_counter = 0 + for repre in filtered_repres: repre_files = repre["files"] if not isinstance(repre_files, (list, tuple)): @@ -135,12 +135,8 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): self.log.warning("Conversion crashed", exc_info=True) raise - repre_name = "thumbnail" - if name_counter > 0: - repre_name += str(name_counter) - name_counter += 1 new_repre = { - "name": repre_name, + "name": "thumbnail", "ext": "jpg", "files": jpeg_file, "stagingDir": stagingdir, @@ -156,6 +152,11 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): if convert_dir is not None and os.path.exists(convert_dir): shutil.rmtree(convert_dir) + # Create only one representation with name 'thumbnail' + # TODO maybe handle way how to decide from which representation + # will be thumbnail created + break + def _get_filtered_repres(self, instance): filtered_repres = [] src_repres = instance.data.get("representations") or [] From 7c61e36f8f04536fdf81341d1cf7897195241704 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 12 May 2022 17:55:02 +0200 Subject: [PATCH 259/398] integrate ftrack instances does not query location entities only set location names on components --- .../plugins/publish/integrate_ftrack_api.py | 66 +++++++++++++++++++ .../publish/integrate_ftrack_instances.py | 30 ++++----- 2 files changed, 81 insertions(+), 15 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py index 64af8cb208..c4f7b1f05d 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py @@ -12,6 +12,7 @@ Provides: import os import sys +import collections import six import pyblish.api import clique @@ -84,6 +85,7 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin): asset_types_by_short = self._ensure_asset_types_exists( session, component_list ) + self._fill_component_locations(session, component_list) asset_versions_data_by_id = {} used_asset_versions = [] @@ -193,6 +195,70 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin): session._configure_locations() six.reraise(tp, value, tb) + def _fill_component_locations(self, session, component_list): + components_by_location_name = collections.defaultdict(list) + components_by_location_id = collections.defaultdict(list) + for component_item in component_list: + # Location entity can be prefilled + # - this is not recommended as connection to ftrack server may + # be lost and in that case the entity is not valid when gets + # to this plugin + location = component_item.get("component_location") + if location is not None: + continue + + # Collect location id + location_id = component_item.get("component_location_id") + if location_id: + components_by_location_id[location_id].append( + component_item + ) + continue + + location_name = component_item.get("component_location_name") + if location_name: + components_by_location_name[location_name].append( + component_item + ) + continue + + # Skip if there is nothing to do + if not components_by_location_name and not components_by_location_id: + return + + # Query locations + query_filters = [] + if components_by_location_id: + joined_location_ids = ",".join([ + '"{}"'.format(location_id) + for location_id in components_by_location_id + ]) + query_filters.append("id in ({})".format(joined_location_ids)) + + if components_by_location_name: + joined_location_names = ",".join([ + '"{}"'.format(location_name) + for location_name in components_by_location_name + ]) + query_filters.append("name in ({})".format(joined_location_names)) + + locations = session.query( + "select id, name from Location where {}".format( + " or ".join(query_filters) + ) + ).all() + # Fill locations in components + for location in locations: + location_id = location["id"] + location_name = location["name"] + if location_id in components_by_location_id: + for component in components_by_location_id[location_id]: + component["component_location"] = location + + if location_name in components_by_location_name: + for component in components_by_location_name[location_name]: + component["component_location"] = location + def _ensure_asset_types_exists(self, session, component_list): """Make sure that all AssetType entities exists for integration. diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py index 0dd7b1c6e4..170be4b173 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py @@ -106,11 +106,10 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): # These must be changed for each component "component_data": None, "component_path": None, - "component_location": None + "component_location": None, + "component_location_name": None } - ft_session = instance.context.data["ftrackSession"] - # Filter types of representations review_representations = [] thumbnail_representations = [] @@ -128,12 +127,8 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): other_representations.append(repre) # Prepare ftrack locations - unmanaged_location = ft_session.query( - "Location where name is \"ftrack.unmanaged\"" - ).one() - ftrack_server_location = ft_session.query( - "Location where name is \"ftrack.server\"" - ).one() + unmanaged_location_name = "ftrack.unmanaged" + ftrack_server_location_name = "ftrack.server" # Components data component_list = [] @@ -174,7 +169,10 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): first_thumbnail_component_repre = repre first_thumbnail_component = thumbnail_item # Set location - thumbnail_item["component_location"] = ftrack_server_location + thumbnail_item["component_location_name"] = ( + ftrack_server_location_name + ) + # Add item to component list component_list.append(thumbnail_item) @@ -293,7 +291,9 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): src_components_to_add.append(copy.deepcopy(review_item)) # Set location - review_item["component_location"] = ftrack_server_location + review_item["component_location_name"] = ( + ftrack_server_location_name + ) # Add item to component list component_list.append(review_item) @@ -305,8 +305,8 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): first_thumbnail_component ) new_thumbnail_component["asset_data"]["name"] = asset_name - new_thumbnail_component["component_location"] = ( - ftrack_server_location + new_thumbnail_component["component_location_name"] = ( + ftrack_server_location_name ) component_list.append(new_thumbnail_component) @@ -315,7 +315,7 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): # Make sure thumbnail is disabled copy_src_item["thumbnail"] = False # Set location - copy_src_item["component_location"] = unmanaged_location + copy_src_item["component_location_name"] = unmanaged_location_name # Modify name of component to have suffix "_src" component_data = copy_src_item["component_data"] component_name = component_data["name"] @@ -340,7 +340,7 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): other_item["component_data"] = { "name": repre["name"] } - other_item["component_location"] = unmanaged_location + other_item["component_location_name"] = unmanaged_location_name other_item["component_path"] = published_path component_list.append(other_item) From 64380785876ed7933e20f4b7e22c9093cf932d38 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 13 May 2022 11:12:43 +0200 Subject: [PATCH 260/398] creators can be filtered by host name --- openpype/pipeline/create/context.py | 15 +++++++++++++++ openpype/pipeline/create/creator_plugins.py | 6 ++++++ 2 files changed, 21 insertions(+) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 6f862e0588..2f1922c103 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -749,6 +749,10 @@ class CreateContext: """Is host valid for creation.""" return self._host_is_valid + @property + def host_name(self): + return os.environ["AVALON_APP"] + @property def log(self): """Dynamic access to logger.""" @@ -861,6 +865,17 @@ class CreateContext: "Using first and skipping following" )) continue + + # Filter by host name + if ( + creator_class.host_name + and creator_class.host_name != self.host_name + ): + self.log.info(( + "Creator's host name is not supported for current host {}" + ).format(creator_class.host_name, self.host_name)) + continue + creator = creator_class( self, system_settings, diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index cbe19da064..c776f94d56 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -63,6 +63,12 @@ class BaseCreator: # `openpype.pipeline.attribute_definitions` instance_attr_defs = [] + # Filtering by host name - can be used to be filtered by host name + # - used on all hosts when set to 'None' for Backwards compatibility + # - was added afterwards + # QUESTIOn make this required? + host_name = None + def __init__( self, create_context, system_settings, project_settings, headless=False ): From ababf4053e8de157b9edcc9227d662e476f3e497 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 13 May 2022 11:24:36 +0200 Subject: [PATCH 261/398] flame: splitting function into smaller parts --- .../publish/extract_subset_resources.py | 80 +++++++++---------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py index 0e04336211..9ad3b21687 100644 --- a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py +++ b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py @@ -60,12 +60,7 @@ class ExtractSubsetResources(openpype.api.Extractor): export_presets_mapping = {} def process(self, instance): - if ( - self.keep_original_representation - and "representations" not in instance.data - or not self.keep_original_representation - ): - instance.data["representations"] = [] + self._make_representation_data(instance) # flame objects segment = instance.data["item"] @@ -92,7 +87,6 @@ class ExtractSubsetResources(openpype.api.Extractor): handles = max(handle_start, handle_end) # get media source range with handles - source_end_handles = instance.data["sourceEndH"] source_start_handles = instance.data["sourceStartH"] source_end_handles = instance.data["sourceEndH"] @@ -109,27 +103,7 @@ 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.get("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 any - if ( - filter_path_regex - and not re.search(filter_path_regex, clip_path) - ): + if self._should_skip(preset_config, clip_path, unique_name): continue # get all presets attributes @@ -147,18 +121,10 @@ class ExtractSubsetResources(openpype.api.Extractor): ) ) - # get attribures related loading in integrate_batch_group - load_to_batch_group = preset_config.get( - "load_to_batch_group") - batch_group_loader_name = preset_config.get( - "batch_group_loader_name") - - # convert to None if empty string - if batch_group_loader_name == "": - batch_group_loader_name = None - # get frame range with handles for representation range frame_start_handle = frame_start - handle_start + + # calculate duration with handles source_duration_handles = ( source_end_handles - source_start_handles) @@ -272,8 +238,10 @@ class ExtractSubsetResources(openpype.api.Extractor): "data": { "colorspace": color_out }, - "load_to_batch_group": load_to_batch_group, - "batch_group_loader_name": batch_group_loader_name + "load_to_batch_group": preset_config.get( + "load_to_batch_group"), + "batch_group_loader_name": preset_config.get( + "batch_group_loader_name") or None } # collect all available content of export dir @@ -328,6 +296,38 @@ class ExtractSubsetResources(openpype.api.Extractor): self.log.debug("All representations: {}".format( pformat(instance.data["representations"]))) + def _should_skip(self, preset_config, clip_path, unique_name): + # get activating attributes + activated_preset = preset_config["active"] + filter_path_regex = preset_config.get("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: + return True + + # exclude by regex filter if any + if ( + filter_path_regex + and not re.search(filter_path_regex, clip_path) + ): + return True + + def _make_representation_data(self, instance): + if ( + self.keep_original_representation + and "representations" not in instance.data + or not self.keep_original_representation + ): + instance.data["representations"] = [] + def _unfolds_nested_folders(self, stage_dir, files_list, ext): """Unfolds nested folders From 0ae531d4cec91870774169f51ce373bb73e7261f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 13 May 2022 11:25:29 +0200 Subject: [PATCH 262/398] flame: fixing small hickups removing frame range input form reference --- openpype/hosts/flame/otio/flame_export.py | 7 ++++--- openpype/lib/editorial.py | 8 ++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/flame/otio/flame_export.py b/openpype/hosts/flame/otio/flame_export.py index 08478d4b98..ffb82b97c2 100644 --- a/openpype/hosts/flame/otio/flame_export.py +++ b/openpype/hosts/flame/otio/flame_export.py @@ -205,8 +205,9 @@ def create_otio_markers(otio_item, item): otio_item.markers.append(otio_marker) -def create_otio_reference(clip_data, duration, fps=None): +def create_otio_reference(clip_data, fps=None): metadata = _get_metadata(clip_data) + duration = int(clip_data["source_duration"]) # get file info for path and start frame frame_start = 0 @@ -307,7 +308,7 @@ def create_otio_clip(clip_data): # secondly check if any change of speed if source_duration != _clip_record_duration: - retime_speed = float(source_duration / _clip_record_duration) + retime_speed = float(source_duration) / float(_clip_record_duration) log.debug("_ retime_speed: {}".format(retime_speed)) speed *= retime_speed @@ -319,7 +320,7 @@ def create_otio_clip(clip_data): # create media reference media_reference = create_otio_reference( - clip_data, source_duration, media_fps) + clip_data, media_fps) # creatae source range source_range = create_otio_time_range( diff --git a/openpype/lib/editorial.py b/openpype/lib/editorial.py index 1ee21deedc..4979bac159 100644 --- a/openpype/lib/editorial.py +++ b/openpype/lib/editorial.py @@ -269,16 +269,16 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): "retime": True, "speed": time_scalar, "timewarps": time_warp_nodes, - "handleStart": handle_start, - "handleEnd": handle_end + "handleStart": round(handle_start), + "handleEnd": round(handle_end) } } returning_dict = { "mediaIn": media_in_trimmed, "mediaOut": media_out_trimmed, - "handleStart": handle_start, - "handleEnd": handle_end + "handleStart": round(handle_start), + "handleEnd": round(handle_end) } # add version data only if retime From d2afd26ff694bd2c52e337cde8f75b2cdf721ef2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 13 May 2022 11:29:19 +0200 Subject: [PATCH 263/398] implemented collecting of creator plugins for host --- openpype/modules/base.py | 26 ++++++++++++++++++++++++++ openpype/modules/interfaces.py | 31 ++++++++++++++++++++++++++++++- 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 0dd512ee8b..5b49649359 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -702,6 +702,32 @@ class ModulesManager: ).format(expected_keys, " | ".join(msg_items))) return output + def collect_creator_plugin_paths(self, host_name): + """Helper to collect creator plugin paths from modules. + + Args: + host_name (str): For which host are creators meants. + + Returns: + list: List of creator plugin paths. + """ + # Output structure + from openpype_interfaces import IPluginPaths + + output = [] + for module in self.get_enabled_modules(): + # Skip module that do not inherit from `IPluginPaths` + if not isinstance(module, IPluginPaths): + continue + + paths = module.get_creator_plugin_paths(host_name) + if paths: + # Convert to list if value is not list + if not isinstance(paths, (list, tuple, set)): + paths = [paths] + output.extend(paths) + return output + def collect_launch_hook_paths(self): """Helper to collect hooks from modules inherited ILaunchHookPaths. diff --git a/openpype/modules/interfaces.py b/openpype/modules/interfaces.py index 13cbea690b..e553151428 100644 --- a/openpype/modules/interfaces.py +++ b/openpype/modules/interfaces.py @@ -1,5 +1,7 @@ from abc import abstractmethod +import six + from openpype import resources from openpype.modules import OpenPypeInterface @@ -14,11 +16,38 @@ class IPluginPaths(OpenPypeInterface): "publish": ["path/to/publish_plugins"] } """ - # TODO validation of an output + @abstractmethod def get_plugin_paths(self): pass + def get_creator_plugin_paths(self, host_name): + """Retreive creator plugin paths. + + Give addons ability to add creator plugin paths based on host name. + + NOTES: + - Default implementation uses 'get_plugin_paths' and always return + all creator plugins. + - Host name may help to organize plugins by host, but each creator + alsomay have host filtering. + + Args: + host_name (str): For which host are the plugins meant. + """ + + paths = self.get_plugin_paths() + if not paths or "create" not in paths: + return [] + + create_paths = paths["create"] + if not create_paths: + return [] + + if not isinstance(create_paths, (list, tuple, set)): + create_paths = [create_paths] + return create_paths + class ILaunchHookPaths(OpenPypeInterface): """Module has launch hook paths to return. From 29cbc12ca8bfb240883ba57212205b3ed8a6ebc4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 13 May 2022 11:47:03 +0200 Subject: [PATCH 264/398] install creator plugins from modules on openpype plugin install --- openpype/pipeline/context_tools.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/openpype/pipeline/context_tools.py b/openpype/pipeline/context_tools.py index 06bd639776..cda9b10f44 100644 --- a/openpype/pipeline/context_tools.py +++ b/openpype/pipeline/context_tools.py @@ -12,7 +12,7 @@ import pyblish.api from pyblish.lib import MessageHandler import openpype -from openpype.modules import load_modules +from openpype.modules import load_modules, ModulesManager from openpype.settings import get_project_settings from openpype.lib import ( Anatomy, @@ -107,7 +107,7 @@ def install_host(host): install_openpype_plugins() -def install_openpype_plugins(project_name=None): +def install_openpype_plugins(project_name=None, host_name=None): # Make sure modules are loaded load_modules() @@ -116,6 +116,14 @@ def install_openpype_plugins(project_name=None): pyblish.api.register_discovery_filter(filter_pyblish_plugins) register_loader_plugin_path(LOAD_PATH) + if host_name is None: + host_name = os.environ.get("AVALON_APP") + + modules_manager = ModulesManager() + creator_paths = modules_manager.collect_creator_plugin_paths(host_name) + for creator_path in creator_paths: + register_creator_plugin_path(creator_path) + if project_name is None: project_name = os.environ.get("AVALON_PROJECT") From 43aee1989c197b74ce692b64581b233371aa8003 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 13 May 2022 11:55:03 +0200 Subject: [PATCH 265/398] flame: fixing padding if it is higher then needed --- openpype/hosts/flame/api/lib.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/flame/api/lib.py b/openpype/hosts/flame/api/lib.py index f2f5db184b..6dc7d3d887 100644 --- a/openpype/hosts/flame/api/lib.py +++ b/openpype/hosts/flame/api/lib.py @@ -855,22 +855,24 @@ class MediaInfoFile(object): # add `[` in front to make sure it want capture # shot name with the same number - number_from_path = "[" + self._separate_number(feed_basename, feed_ext) + number_from_path = self._separate_number(feed_basename, feed_ext) + search_number_pattern = "[" + number_from_path # convert to multiple collections _continues_colls = collection.separate() for _coll in _continues_colls: - coll_to_text = self._format_collection(_coll) + coll_to_text = self._format_collection(_coll, len(number_from_path)) self.log.debug("__ coll_to_text: {}".format(coll_to_text)) - if number_from_path in coll_to_text: + if search_number_pattern in coll_to_text: return coll_to_text @staticmethod - def _format_collection(collection): + def _format_collection(collection, padding=None): + padding = padding or collection.padding # if no holes then return collection head = collection.format("{head}") tail = collection.format("{tail}") range_template = "[{{:0{0}d}}-{{:0{0}d}}]".format( - len(str(max(collection.indexes)))) + padding) ranges = range_template.format( min(collection.indexes), max(collection.indexes) From 7e23f1cc32bb22a464135df8d77283dd930c0342 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 13 May 2022 12:00:50 +0200 Subject: [PATCH 266/398] fix typo --- openpype/pipeline/create/creator_plugins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index c776f94d56..8006d4f4f8 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -66,7 +66,7 @@ class BaseCreator: # Filtering by host name - can be used to be filtered by host name # - used on all hosts when set to 'None' for Backwards compatibility # - was added afterwards - # QUESTIOn make this required? + # QUESTION make this required? host_name = None def __init__( From c4907af980f8aa03bc0b3442e7a2c578d58dca07 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 13 May 2022 12:04:40 +0200 Subject: [PATCH 267/398] traypublisher creator has host name filter --- 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 603f34ee29..202664cfc6 100644 --- a/openpype/hosts/traypublisher/api/plugin.py +++ b/openpype/hosts/traypublisher/api/plugin.py @@ -14,6 +14,7 @@ from .pipeline import ( class TrayPublishCreator(Creator): create_allow_context_change = True + host_name = "traypublisher" def collect_instances(self): for instance_data in list_instances(): From 40ea037ed4ee525f61a25e2cf271acdc100190b6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 13 May 2022 12:08:30 +0200 Subject: [PATCH 268/398] register module publish plugins during openpype plugin installation instead of using environment variable --- openpype/pipeline/context_tools.py | 6 +++++- start.py | 12 ------------ 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/openpype/pipeline/context_tools.py b/openpype/pipeline/context_tools.py index cda9b10f44..a54809bc36 100644 --- a/openpype/pipeline/context_tools.py +++ b/openpype/pipeline/context_tools.py @@ -116,10 +116,14 @@ def install_openpype_plugins(project_name=None, host_name=None): pyblish.api.register_discovery_filter(filter_pyblish_plugins) register_loader_plugin_path(LOAD_PATH) + modules_manager = ModulesManager() + publish_plugin_dirs = modules_manager.collect_plugin_paths()["publish"] + for path in publish_plugin_dirs: + pyblish.api.register_plugin_path(PUBLISH_PATH) + if host_name is None: host_name = os.environ.get("AVALON_APP") - modules_manager = ModulesManager() creator_paths = modules_manager.collect_creator_plugin_paths(host_name) for creator_path in creator_paths: register_creator_plugin_path(creator_path) diff --git a/start.py b/start.py index 6e339fabab..ace33ab92a 100644 --- a/start.py +++ b/start.py @@ -386,18 +386,6 @@ def set_modules_environments(): 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: From cf7a83eb490c36cafedc81f6988e45bbdb78a156 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 13 May 2022 12:19:54 +0200 Subject: [PATCH 269/398] remove unused import --- openpype/modules/interfaces.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/modules/interfaces.py b/openpype/modules/interfaces.py index e553151428..334485cab2 100644 --- a/openpype/modules/interfaces.py +++ b/openpype/modules/interfaces.py @@ -1,7 +1,5 @@ from abc import abstractmethod -import six - from openpype import resources from openpype.modules import OpenPypeInterface From dadf816d3de7684e9dbdefad37f998323e414dcc Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 13 May 2022 12:21:42 +0200 Subject: [PATCH 270/398] fix registering of paths --- openpype/pipeline/context_tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/context_tools.py b/openpype/pipeline/context_tools.py index a54809bc36..e849f5b0d1 100644 --- a/openpype/pipeline/context_tools.py +++ b/openpype/pipeline/context_tools.py @@ -119,7 +119,7 @@ def install_openpype_plugins(project_name=None, host_name=None): modules_manager = ModulesManager() publish_plugin_dirs = modules_manager.collect_plugin_paths()["publish"] for path in publish_plugin_dirs: - pyblish.api.register_plugin_path(PUBLISH_PATH) + pyblish.api.register_plugin_path(path) if host_name is None: host_name = os.environ.get("AVALON_APP") From 6a75497bfcaaba4ebd87c8443af77aec03d4ef24 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 13 May 2022 15:16:46 +0200 Subject: [PATCH 271/398] OP-2790 - added clean_import option to Maya's Import loader By selecting this option all occurrences of cbId of imported nodes will be removed. --- openpype/hosts/maya/plugins/load/actions.py | 35 ++++++++++++++++----- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/actions.py b/openpype/hosts/maya/plugins/load/actions.py index 4b7871a40c..14518ead5d 100644 --- a/openpype/hosts/maya/plugins/load/actions.py +++ b/openpype/hosts/maya/plugins/load/actions.py @@ -1,7 +1,7 @@ """A module containing generic loader actions that will display in the Loader. """ - +import qargparse from openpype.pipeline import load from openpype.hosts.maya.api.lib import ( maintained_selection, @@ -98,6 +98,15 @@ class ImportMayaLoader(load.LoaderPlugin): icon = "arrow-circle-down" color = "#775555" + options = [ + qargparse.Boolean( + "clean_import", + label="Clean import", + default=False, + help="Should all occurences of cbId be purged?" + ) + ] + def load(self, context, name=None, namespace=None, data=None): import maya.cmds as cmds @@ -105,6 +114,8 @@ class ImportMayaLoader(load.LoaderPlugin): if choice is False: return + clean_import = data.get("clean_import", False) + asset = context['asset'] namespace = namespace or unique_namespace( @@ -114,13 +125,21 @@ class ImportMayaLoader(load.LoaderPlugin): ) with maintained_selection(): - cmds.file(self.fname, - i=True, - preserveReferences=True, - namespace=namespace, - returnNewNodes=True, - groupReference=True, - groupName="{}:{}".format(namespace, name)) + nodes = cmds.file(self.fname, + i=True, + preserveReferences=True, + namespace=namespace, + returnNewNodes=True, + groupReference=True, + groupName="{}:{}".format(namespace, name)) + + if clean_import: + shapes = cmds.ls(nodes, shapes=True, long=True) + for shape in shapes: + meshes = cmds.ls('{}.cbId'.format(shape)) + for mesh in meshes: + print("Removing ... " + (mesh)) + cmds.deleteAttr(mesh) # We do not containerize imported content, it remains unmanaged return From f7c0f3b46ec1b38ee2a9a99c2f00ca7d492c71ec Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 13 May 2022 17:01:09 +0200 Subject: [PATCH 272/398] don't create ftrackreview-image if there are mp4 reviews --- .../ftrack/plugins/publish/integrate_ftrack_instances.py | 5 ++++- 1 file changed, 4 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 170be4b173..c8d9e4117d 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py @@ -176,7 +176,10 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): # Add item to component list component_list.append(thumbnail_item) - if first_thumbnail_component is not None: + if ( + not review_representations + and first_thumbnail_component is not None + ): width = first_thumbnail_component_repre.get("width") height = first_thumbnail_component_repre.get("height") if not width or not height: From 0ed1caa5f9d347fd820ca9e2ab471235fda80481 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Fri, 13 May 2022 18:15:03 +0200 Subject: [PATCH 273/398] add write family to default nuke version sync settings --- openpype/settings/defaults/project_settings/deadline.json | 2 +- openpype/settings/defaults/project_settings/nuke.json | 3 ++- .../schemas/projects_schema/schemas/schema_nuke_publish.json | 3 +++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/settings/defaults/project_settings/deadline.json b/openpype/settings/defaults/project_settings/deadline.json index f0b2a7e555..5c5a14bf21 100644 --- a/openpype/settings/defaults/project_settings/deadline.json +++ b/openpype/settings/defaults/project_settings/deadline.json @@ -98,4 +98,4 @@ } } } -} +} \ No newline at end of file diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index 128d440732..33ddc2f251 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -87,7 +87,8 @@ "camera", "gizmo", "source", - "render" + "render", + "write" ] }, "ValidateInstanceInContext": { 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 94b52bba13..04df957d67 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 @@ -41,6 +41,9 @@ }, { "render": "render" + }, + { + "write": "write" } ] } From 30123edb55ddbda74259318c6b85e0df19da0cd8 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 13 May 2022 20:10:51 +0200 Subject: [PATCH 274/398] flame: adding xml element if it is missing and its path possible --- openpype/hosts/flame/api/render_utils.py | 41 +++++++++++++++++++++--- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/flame/api/render_utils.py b/openpype/hosts/flame/api/render_utils.py index 473fb2f985..9957550af9 100644 --- a/openpype/hosts/flame/api/render_utils.py +++ b/openpype/hosts/flame/api/render_utils.py @@ -1,5 +1,8 @@ import os from xml.etree import ElementTree as ET +import openpype.api as openpype + +log = openpype.Logger.get_logger(__name__) def export_clip(export_path, clip, preset_path, **kwargs): @@ -143,10 +146,40 @@ def modify_preset_file(xml_path, staging_dir, data): # change xml following data keys with open(xml_path, "r") as datafile: - tree = ET.parse(datafile) + _root = ET.parse(datafile) + for key, value in data.items(): - for element in tree.findall(".//{}".format(key)): - element.text = str(value) - tree.write(temp_path) + try: + if "/" in key: + if not key.startswith("./"): + key = ".//" + key + + split_key_path = key.split("/") + element_key = split_key_path[-1] + parent_obj_path = "/".join(split_key_path[:-1]) + + parent_obj = _root.find(parent_obj_path) + element_obj = parent_obj.find(element_key) + if not element_obj: + append_element(parent_obj, element_key, value) + else: + finds = _root.findall(".//{}".format(key)) + if not finds: + raise AttributeError + for element in finds: + element.text = str(value) + except AttributeError: + log.warning( + "Cannot create attribute: {}: {}. Skipping".format( + key, value + )) + _root.write(temp_path) return temp_path + + +def append_element(root_element_obj, key, value): + new_element_obj = ET.Element(key) + log.debug("__ new_element_obj: {}".format(new_element_obj)) + new_element_obj.text = str(value) + root_element_obj.insert(0, new_element_obj) From 851df8155f43d4fd6639ee71ab3a3f023e06c7a5 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 13 May 2022 20:11:20 +0200 Subject: [PATCH 275/398] flame: treat thumbnail as poster frame --- .../publish/extract_subset_resources.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py index 9ad3b21687..eea575ea88 100644 --- a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py +++ b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py @@ -168,10 +168,6 @@ class ExtractSubsetResources(openpype.api.Extractor): # 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 - ))) - export_kwargs = {} # validate xml preset file is filled if preset_file == "": @@ -198,11 +194,13 @@ class ExtractSubsetResources(openpype.api.Extractor): preset_dir, preset_file )) - preset_path = opfapi.modify_preset_file( - preset_orig_xml_path, staging_dir, modify_xml_data) - # define kwargs based on preset type if "thumbnail" in unique_name: + modify_xml_data.update({ + "video/posterFrame": True, + "video/useFrameAsPoster": 1, + "namePattern": "__thumbnail" + }) thumb_frame_number = int(in_mark + ( source_duration_handles / 2)) @@ -218,6 +216,12 @@ class ExtractSubsetResources(openpype.api.Extractor): "out_mark": out_mark }) + self.log.debug("__ modify_xml_data: {}".format( + pformat(modify_xml_data) + )) + 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 From d7454044c92f54392b3c8108a466bb6a5758b830 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Marinov?= Date: Fri, 13 May 2022 21:02:44 +0200 Subject: [PATCH 276/398] Nuke: add pointcache and animation to loader --- openpype/hosts/nuke/plugins/load/load_model.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/nuke/plugins/load/load_model.py b/openpype/hosts/nuke/plugins/load/load_model.py index 9788bb25d2..89b58585ef 100644 --- a/openpype/hosts/nuke/plugins/load/load_model.py +++ b/openpype/hosts/nuke/plugins/load/load_model.py @@ -15,13 +15,13 @@ from openpype.hosts.nuke.api import ( class AlembicModelLoader(load.LoaderPlugin): """ - This will load alembic model into script. + This will load alembic model or anim into script. """ - families = ["model"] + families = ["model","pointcache","animation"] representations = ["abc"] - label = "Load Alembic Model" + label = "Load Alembic Model or Anim" icon = "cube" color = "orange" node_color = "0x4ecd91ff" From dfb3e53743b3bc4229b1c82beb1ff0ecbffc7323 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Marinov?= Date: Fri, 13 May 2022 21:07:45 +0200 Subject: [PATCH 277/398] fix missing whitespaces --- openpype/hosts/nuke/plugins/load/load_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/load/load_model.py b/openpype/hosts/nuke/plugins/load/load_model.py index 89b58585ef..8aaa7221eb 100644 --- a/openpype/hosts/nuke/plugins/load/load_model.py +++ b/openpype/hosts/nuke/plugins/load/load_model.py @@ -18,7 +18,7 @@ class AlembicModelLoader(load.LoaderPlugin): This will load alembic model or anim into script. """ - families = ["model","pointcache","animation"] + families = ["model", "pointcache", "animation"] representations = ["abc"] label = "Load Alembic Model or Anim" From 7d13de97f694ee735ffc4de63749c285c898b7e0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 16 May 2022 11:14:02 +0200 Subject: [PATCH 278/398] Flame: add handles including to settings --- openpype/settings/defaults/project_settings/flame.json | 3 ++- .../schemas/projects_schema/schema_project_flame.json | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/flame.json b/openpype/settings/defaults/project_settings/flame.json index dd8c05d460..a7836b9c1f 100644 --- a/openpype/settings/defaults/project_settings/flame.json +++ b/openpype/settings/defaults/project_settings/flame.json @@ -16,7 +16,8 @@ "vSyncOn": false, "workfileFrameStart": 1001, "handleStart": 5, - "handleEnd": 5 + "handleEnd": 5, + "includeHandles": false } }, "publish": { 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 ace404b47a..ca62679b3d 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json @@ -123,6 +123,11 @@ "type": "number", "key": "handleEnd", "label": "Handle end (tail)" + }, + { + "type": "boolean", + "key": "includeHandles", + "label": "Enable handles including" } ] } From 3895008eae62c2ebba2afe60b0f0320bc7604cc0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 16 May 2022 11:26:14 +0200 Subject: [PATCH 279/398] flame: implementing handles include switch --- openpype/hosts/flame/api/plugin.py | 3 +++ openpype/version.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/flame/api/plugin.py b/openpype/hosts/flame/api/plugin.py index 11108ba49f..efbabb6a55 100644 --- a/openpype/hosts/flame/api/plugin.py +++ b/openpype/hosts/flame/api/plugin.py @@ -360,6 +360,7 @@ class PublishableClip: driving_layer_default = "" index_from_segment_default = False use_shot_name_default = False + include_handles_default = False def __init__(self, segment, **kwargs): self.rename_index = kwargs["rename_index"] @@ -493,6 +494,8 @@ class PublishableClip: "reviewTrack", {}).get("value") or self.review_track_default self.audio = self.ui_inputs.get( "audio", {}).get("value") or False + self.include_handles = self.ui_inputs.get( + "includeHandles", {}).get("value") or self.include_handles_default # build subset name from layer name if self.subset_name == "[ track name ]": diff --git a/openpype/version.py b/openpype/version.py index 662adf28ca..fe2a90bdd1 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.10.0-nightly.2" +__version__ = "3.10.0-nightly.2-upp220513-1+staging" From 5c4541ca0ae6af1561ecd2e405d840579d4264ae Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 16 May 2022 11:31:06 +0200 Subject: [PATCH 280/398] flame: implementing handles including --- openpype/hosts/flame/api/plugin.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/flame/api/plugin.py b/openpype/hosts/flame/api/plugin.py index 11108ba49f..efbabb6a55 100644 --- a/openpype/hosts/flame/api/plugin.py +++ b/openpype/hosts/flame/api/plugin.py @@ -360,6 +360,7 @@ class PublishableClip: driving_layer_default = "" index_from_segment_default = False use_shot_name_default = False + include_handles_default = False def __init__(self, segment, **kwargs): self.rename_index = kwargs["rename_index"] @@ -493,6 +494,8 @@ class PublishableClip: "reviewTrack", {}).get("value") or self.review_track_default self.audio = self.ui_inputs.get( "audio", {}).get("value") or False + self.include_handles = self.ui_inputs.get( + "includeHandles", {}).get("value") or self.include_handles_default # build subset name from layer name if self.subset_name == "[ track name ]": From 45c445d438106b1ed3d989224778884f7796974b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 16 May 2022 11:33:35 +0200 Subject: [PATCH 281/398] removing version change by excident --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index fe2a90bdd1..662adf28ca 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.10.0-nightly.2-upp220513-1+staging" +__version__ = "3.10.0-nightly.2" From 5e68cfad86cb3781f3acdc66f681651a11b84945 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Marinov?= Date: Mon, 16 May 2022 11:43:44 +0200 Subject: [PATCH 282/398] More straightforward label --- openpype/hosts/nuke/plugins/load/load_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/load/load_model.py b/openpype/hosts/nuke/plugins/load/load_model.py index 8aaa7221eb..2f54595cb0 100644 --- a/openpype/hosts/nuke/plugins/load/load_model.py +++ b/openpype/hosts/nuke/plugins/load/load_model.py @@ -21,7 +21,7 @@ class AlembicModelLoader(load.LoaderPlugin): families = ["model", "pointcache", "animation"] representations = ["abc"] - label = "Load Alembic Model or Anim" + label = "Load Alembic" icon = "cube" color = "orange" node_color = "0x4ecd91ff" From 3b15167adb602394ad32dcdbeec7f8f0bbb242a3 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 16 May 2022 11:54:29 +0200 Subject: [PATCH 283/398] Flame: adding new attribute to ui includeHandles --- openpype/hosts/flame/plugins/create/create_shot_clip.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openpype/hosts/flame/plugins/create/create_shot_clip.py b/openpype/hosts/flame/plugins/create/create_shot_clip.py index 11c00dab42..fa239ea420 100644 --- a/openpype/hosts/flame/plugins/create/create_shot_clip.py +++ b/openpype/hosts/flame/plugins/create/create_shot_clip.py @@ -268,6 +268,14 @@ class CreateShotClip(opfapi.Creator): "target": "tag", "toolTip": "Handle at end of clip", # noqa "order": 2 + }, + "includeHandles": { + "value": False, + "type": "QCheckBox", + "label": "Include handles", + "target": "tag", + "toolTip": "By default handles are excluded", # noqa + "order": 3 } } } From 3484d57a7633af9eea4f380b270f361e19eb075d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Mon, 16 May 2022 12:29:34 +0200 Subject: [PATCH 284/398] add support for PxrTexture --- .../maya/plugins/publish/collect_look.py | 27 ++++++++++++------- .../maya/plugins/publish/extract_look.py | 6 +++-- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_look.py b/openpype/hosts/maya/plugins/publish/collect_look.py index b6a76f1e21..70265a160f 100644 --- a/openpype/hosts/maya/plugins/publish/collect_look.py +++ b/openpype/hosts/maya/plugins/publish/collect_look.py @@ -77,9 +77,14 @@ def node_uses_image_sequence(node): node_path = get_file_node_path(node).lower() # The following tokens imply a sequence - patterns = ["", "", "", "u_v", "", "", "", + "u_v", ""] + try: + extension = cmds.getAttr('%s.useFrameExtension' % node) + except ValueError: + extension = None - return (cmds.getAttr('%s.useFrameExtension' % node) or + return (extension or any(pattern in node_path for pattern in patterns)) @@ -165,7 +170,7 @@ def get_file_node_path(node): if any(pattern in lower for pattern in patterns): return texture_pattern - if cmds.nodeType(node) == 'aiImage': + if cmds.nodeType(node) in ['aiImage', 'PxrTexture']: return cmds.getAttr('{0}.filename'.format(node)) if cmds.nodeType(node) == 'RedshiftNormalMap': return cmds.getAttr('{}.tex0'.format(node)) @@ -326,7 +331,10 @@ class CollectLook(pyblish.api.InstancePlugin): "volumeShader", "displacementShader", "aiSurfaceShader", - "aiVolumeShader"] + "aiVolumeShader", + "rman__surface", + "rman__displacement" + ] if look_sets: materials = [] @@ -376,6 +384,7 @@ class CollectLook(pyblish.api.InstancePlugin): files = cmds.ls(history, type="file", long=True) files.extend(cmds.ls(history, type="aiImage", long=True)) + files.extend(cmds.ls(history, type="PxrTexture", long=True)) files.extend(cmds.ls(history, type="RedshiftNormalMap", long=True)) self.log.info("Collected file nodes:\n{}".format(files)) @@ -510,23 +519,21 @@ class CollectLook(pyblish.api.InstancePlugin): Returns: dict """ - self.log.debug("processing: {}".format(node)) - if cmds.nodeType(node) not in ["file", "aiImage", "RedshiftNormalMap"]: + if cmds.nodeType(node) not in [ + "file", "aiImage", "RedshiftNormalMap", "PxrTexture"]: self.log.error( "Unsupported file node: {}".format(cmds.nodeType(node))) raise AssertionError("Unsupported file node") + self.log.debug(" - got {}".format(cmds.nodeType(node))) if cmds.nodeType(node) == 'file': - self.log.debug(" - file node") attribute = "{}.fileTextureName".format(node) computed_attribute = "{}.computedFileTextureNamePattern".format(node) - elif cmds.nodeType(node) == 'aiImage': - self.log.debug("aiImage node") + elif cmds.nodeType(node) in ['aiImage', 'PxrTexture']: attribute = "{}.filename".format(node) computed_attribute = attribute elif cmds.nodeType(node) == 'RedshiftNormalMap': - self.log.debug("RedshiftNormalMap node") attribute = "{}.tex0".format(node) computed_attribute = attribute diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 881705b92c..81d7c31ae7 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -372,10 +372,12 @@ class ExtractLook(openpype.api.Extractor): if mode == COPY: transfers.append((source, destination)) - self.log.info('copying') + self.log.info('file will be copied {} -> {}'.format( + source, destination)) elif mode == HARDLINK: hardlinks.append((source, destination)) - self.log.info('hardlinking') + self.log.info('file will be hardlinked {} -> {}'.format( + source, destination)) # Store the hashes from hash to destination to include in the # database From 3ef846b1623dabfdbcb6cb464c097ec6b19bd473 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 16 May 2022 12:59:55 +0200 Subject: [PATCH 285/398] flame: fixing head and tail calculation --- .../publish/collect_timeline_instances.py | 45 ++++++++++++------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py b/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py index 5174f9db48..306d2da203 100644 --- a/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py +++ b/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py @@ -58,12 +58,16 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin): clip_name = clip_data["segment_name"] self.log.debug("clip_name: {}".format(clip_name)) + # get otio clip data + otio_data = self._get_otio_clip_instance_data(clip_data) or {} + self.log.debug("__ otio_data: {}".format(pformat(otio_data))) + # get file path file_path = clip_data["fpath"] first_frame = opfapi.get_frame_from_filename(file_path) or 0 - head, tail = self._get_head_tail(clip_data, first_frame) + head, tail = self._get_head_tail(clip_data, otio_data["otioClip"]) # solve handles length marker_data["handleStart"] = min( @@ -76,6 +80,9 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin): # add marker data to instance data inst_data = dict(marker_data.items()) + # add ocio_data to instance data + inst_data.update(otio_data) + asset = marker_data["asset"] subset = marker_data["subset"] @@ -105,13 +112,6 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin): task["name"]: {"type": task["type"]} for task in self.add_tasks} }) - - # get otio clip data - otio_data = self._get_otio_clip_instance_data(clip_data) or {} - self.log.debug("__ otio_data: {}".format(pformat(otio_data))) - - # add to instance data - inst_data.update(otio_data) self.log.debug("__ inst_data: {}".format(pformat(inst_data))) # add resolution @@ -236,20 +236,31 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin): return split_comments - def _get_head_tail(self, clip_data, first_frame): + def _get_head_tail(self, clip_data, otio_clip): # calculate head and tail with forward compatibility head = clip_data.get("segment_head") tail = clip_data.get("segment_tail") + self.log.debug("__ head: `{}`".format(head)) + self.log.debug("__ tail: `{}`".format(tail)) # HACK: it is here to serve for versions bellow 2021.1 - if not head: - head = int(clip_data["source_in"]) - int(first_frame) - if not tail: - tail = int( - clip_data["source_duration"] - ( - head + clip_data["record_duration"] - ) - ) + if not any([head, tail]): + otio_source_range = otio_clip.source_range + otio_avalable_range = otio_clip.available_range() + range_convert = openpype.lib.otio_range_to_frame_range + src_start, src_end = range_convert(otio_source_range) + av_start, av_end = range_convert(otio_avalable_range) + av_range = av_end - av_start + av_tail = av_range - src_end + + self.log.debug("__ src_start: `{}`".format(src_start)) + self.log.debug("__ src_end: `{}`".format(src_end)) + self.log.debug("__ av_range: `{}`".format(av_range)) + self.log.debug("__ av_tail: `{}`".format(av_tail)) + + head = src_start + tail = av_tail + return head, tail def _get_resolution_to_data(self, data, context): From 0699906344a8399eeb0f7c10c6b61963ce3eb3e2 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 16 May 2022 15:20:57 +0200 Subject: [PATCH 286/398] Flame: implementing handles inclusion to publishing --- .../plugins/publish/collect_timeline_instances.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py b/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py index 306d2da203..4bca0dcf93 100644 --- a/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py +++ b/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py @@ -36,6 +36,7 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin): for segment in selected_segments: # get openpype tag data marker_data = opfapi.get_segment_data_marker(segment) + self.log.debug("__ marker_data: {}".format( pformat(marker_data))) @@ -75,6 +76,8 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin): marker_data["handleEnd"] = min( marker_data["handleEnd"], abs(tail)) + workfile_start = self._set_workfile_start(marker_data) + with_audio = bool(marker_data.pop("audio")) # add marker data to instance data @@ -105,6 +108,7 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin): "families": families, "publish": marker_data["publish"], "fps": self.fps, + "workfileFrameStart": workfile_start, "sourceFirstFrame": int(first_frame), "path": file_path, "flameAddTasks": self.add_tasks, @@ -145,6 +149,17 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin): if marker_data.get("reviewTrack") is not None: instance.data["reviewAudio"] = True + @staticmethod + def _set_workfile_start(data): + include_handles = data.get("includeHandles") + workfile_start = data["workfileFrameStart"] + handle_start = data["handleStart"] + + if include_handles: + workfile_start += handle_start + + return workfile_start + def _get_comment_attributes(self, segment): comment = segment.comment.get_value() From 694c3654764034e1a3f84b4e082b58c0899c8c56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Mon, 16 May 2022 18:26:45 +0200 Subject: [PATCH 287/398] list to sets and var name change --- openpype/hosts/maya/plugins/publish/collect_look.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_look.py b/openpype/hosts/maya/plugins/publish/collect_look.py index 70265a160f..9697d0884f 100644 --- a/openpype/hosts/maya/plugins/publish/collect_look.py +++ b/openpype/hosts/maya/plugins/publish/collect_look.py @@ -60,6 +60,7 @@ def get_look_attrs(node): def node_uses_image_sequence(node): + # type: (str) -> bool """Return whether file node uses an image sequence or single image. Determine if a node uses an image sequence or just a single image, @@ -80,11 +81,11 @@ def node_uses_image_sequence(node): patterns = ["", "", "", "u_v", ""] try: - extension = cmds.getAttr('%s.useFrameExtension' % node) + use_frame_extension = cmds.getAttr('%s.useFrameExtension' % node) except ValueError: - extension = None + use_frame_extension = False - return (extension or + return (use_frame_extension or any(pattern in node_path for pattern in patterns)) @@ -170,7 +171,7 @@ def get_file_node_path(node): if any(pattern in lower for pattern in patterns): return texture_pattern - if cmds.nodeType(node) in ['aiImage', 'PxrTexture']: + if cmds.nodeType(node) in {'aiImage', 'PxrTexture'}: return cmds.getAttr('{0}.filename'.format(node)) if cmds.nodeType(node) == 'RedshiftNormalMap': return cmds.getAttr('{}.tex0'.format(node)) @@ -520,8 +521,8 @@ class CollectLook(pyblish.api.InstancePlugin): dict """ self.log.debug("processing: {}".format(node)) - if cmds.nodeType(node) not in [ - "file", "aiImage", "RedshiftNormalMap", "PxrTexture"]: + if cmds.nodeType(node) not in { + "file", "aiImage", "RedshiftNormalMap", "PxrTexture"}: self.log.error( "Unsupported file node: {}".format(cmds.nodeType(node))) raise AssertionError("Unsupported file node") From 196182f5c5ffdc5bb02d86dedf46fb27b704f64e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 16 May 2022 20:57:51 +0200 Subject: [PATCH 288/398] general: expose lib editorial function to lib init --- openpype/lib/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index 3c1d71ecd5..8d4e733b7d 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -203,6 +203,7 @@ from .editorial import ( is_overlapping_otio_ranges, otio_range_to_frame_range, otio_range_with_handles, + get_media_range_with_retimes, convert_to_padded_path, trim_media_range, range_from_frames, @@ -382,6 +383,7 @@ __all__ = [ "otio_range_with_handles", "convert_to_padded_path", "otio_range_to_frame_range", + "get_media_range_with_retimes", "trim_media_range", "range_from_frames", "frames_to_secons", From 6ee42b1d19b890ee583f4a10f8efab81ebec043e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 16 May 2022 20:58:20 +0200 Subject: [PATCH 289/398] flame: make head and tail with retimed value --- .../publish/collect_timeline_instances.py | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py b/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py index 4bca0dcf93..012cb110ec 100644 --- a/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py +++ b/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py @@ -1,8 +1,8 @@ import re import pyblish -import openpype import openpype.hosts.flame.api as opfapi from openpype.hosts.flame.otio import flame_export +import openpype.lib as oplib # # developer reload modules from pprint import pformat @@ -68,7 +68,12 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin): first_frame = opfapi.get_frame_from_filename(file_path) or 0 - head, tail = self._get_head_tail(clip_data, otio_data["otioClip"]) + head, tail = self._get_head_tail( + clip_data, + otio_data["otioClip"], + marker_data["handleStart"], + marker_data["handleEnd"] + ) # solve handles length marker_data["handleStart"] = min( @@ -251,7 +256,7 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin): return split_comments - def _get_head_tail(self, clip_data, otio_clip): + def _get_head_tail(self, clip_data, otio_clip, handle_start, handle_end): # calculate head and tail with forward compatibility head = clip_data.get("segment_head") tail = clip_data.get("segment_tail") @@ -260,21 +265,14 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin): # HACK: it is here to serve for versions bellow 2021.1 if not any([head, tail]): - otio_source_range = otio_clip.source_range - otio_avalable_range = otio_clip.available_range() - range_convert = openpype.lib.otio_range_to_frame_range - src_start, src_end = range_convert(otio_source_range) - av_start, av_end = range_convert(otio_avalable_range) - av_range = av_end - av_start - av_tail = av_range - src_end + retimed_attributes = oplib.get_media_range_with_retimes( + otio_clip, handle_start, handle_end) + self.log.debug( + ">> retimed_attributes: {}".format(retimed_attributes)) - self.log.debug("__ src_start: `{}`".format(src_start)) - self.log.debug("__ src_end: `{}`".format(src_end)) - self.log.debug("__ av_range: `{}`".format(av_range)) - self.log.debug("__ av_tail: `{}`".format(av_tail)) - - head = src_start - tail = av_tail + # retimed head and tail + head = int(retimed_attributes["handleStart"]) + tail = int(retimed_attributes["handleEnd"]) return head, tail @@ -366,7 +364,7 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin): continue if otio_clip.name not in segment.name.get_value(): continue - if openpype.lib.is_overlapping_otio_ranges( + if oplib.is_overlapping_otio_ranges( parent_range, timeline_range, strict=True): # add pypedata marker to otio_clip metadata From 257c58988181a9dcd39bf85e143a08eb686115a7 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 16 May 2022 20:58:44 +0200 Subject: [PATCH 290/398] flame: change editorial function to lib --- openpype/plugins/publish/collect_otio_subset_resources.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/openpype/plugins/publish/collect_otio_subset_resources.py b/openpype/plugins/publish/collect_otio_subset_resources.py index 7c11462ef0..53d327a51d 100644 --- a/openpype/plugins/publish/collect_otio_subset_resources.py +++ b/openpype/plugins/publish/collect_otio_subset_resources.py @@ -10,8 +10,7 @@ import os import clique import opentimelineio as otio import pyblish.api -import openpype -from openpype.lib import editorial +import openpype.lib as oplib class CollectOtioSubsetResources(pyblish.api.InstancePlugin): @@ -43,7 +42,7 @@ class CollectOtioSubsetResources(pyblish.api.InstancePlugin): available_duration = otio_avalable_range.duration.value # get available range trimmed with processed retimes - retimed_attributes = editorial.get_media_range_with_retimes( + retimed_attributes = oplib.get_media_range_with_retimes( otio_clip, handle_start, handle_end) self.log.debug( ">> retimed_attributes: {}".format(retimed_attributes)) @@ -145,7 +144,7 @@ class CollectOtioSubsetResources(pyblish.api.InstancePlugin): # in case it is file sequence but not new OTIO schema # `ImageSequenceReference` path = media_ref.target_url - collection_data = openpype.lib.make_sequence_collection( + collection_data = oplib.make_sequence_collection( path, trimmed_media_range_h, metadata) self.staging_dir, collection = collection_data From 056b92599ec28afb7de04d5c53021f716f056dfe Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 16 May 2022 21:09:00 +0200 Subject: [PATCH 291/398] global: fixing false editorial namespace --- openpype/plugins/publish/collect_otio_subset_resources.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/collect_otio_subset_resources.py b/openpype/plugins/publish/collect_otio_subset_resources.py index 53d327a51d..78e2a6428c 100644 --- a/openpype/plugins/publish/collect_otio_subset_resources.py +++ b/openpype/plugins/publish/collect_otio_subset_resources.py @@ -64,7 +64,7 @@ class CollectOtioSubsetResources(pyblish.api.InstancePlugin): a_frame_end_h = media_out + handle_end # create trimmed otio time range - trimmed_media_range_h = editorial.range_from_frames( + trimmed_media_range_h = oplib.range_from_frames( a_frame_start_h, (a_frame_end_h - a_frame_start_h + 1), media_fps ) From 34c6cab56fe5046b8ea657c4f241f7533fe10250 Mon Sep 17 00:00:00 2001 From: Petr Dvorak Date: Tue, 17 May 2022 12:09:31 +0200 Subject: [PATCH 292/398] Avalon repo removed from Jobs workflow --- .github/workflows/prerelease.yml | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 5acd20007c..8f51f27994 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -62,22 +62,6 @@ jobs: - name: "🖨️ Print changelog to console" if: steps.version_type.outputs.type != 'skip' run: cat CHANGELOG.md - - - name: 💾 Commit and Tag - id: git_commit - if: steps.version_type.outputs.type != 'skip' - run: | - git config user.email ${{ secrets.CI_EMAIL }} - git config user.name ${{ secrets.CI_USER }} - cd repos/avalon-core - git checkout main - git pull - cd ../.. - git add . - git commit -m "[Automated] Bump version" - tag_name="CI/${{ steps.version.outputs.next_tag }}" - echo $tag_name - git tag -a $tag_name -m "nightly build" - name: Push to protected main branch uses: CasperWA/push-protected@v2.10.0 From ea00dc0c6a41c21d7744b996c40a4eca84e1dcb8 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 17 May 2022 17:07:33 +0200 Subject: [PATCH 293/398] fix support for plugin location --- openpype/hosts/unreal/__init__.py | 3 ++- openpype/hosts/unreal/hooks/pre_workfile_preparation.py | 8 +++++++- openpype/hosts/unreal/lib.py | 6 +++--- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/unreal/__init__.py b/openpype/hosts/unreal/__init__.py index 533f315df3..bedf5a29f7 100644 --- a/openpype/hosts/unreal/__init__.py +++ b/openpype/hosts/unreal/__init__.py @@ -9,7 +9,8 @@ def add_implementation_envs(env, _app): os.path.dirname(os.path.abspath(openpype.hosts.__file__)), "unreal", "integration" ) - env["OPENPYPE_UNREAL_PLUGIN"] = unreal_plugin_path + if not env.get("OPENPYPE_UNREAL_PLUGIN"): + env["OPENPYPE_UNREAL_PLUGIN"] = unreal_plugin_path # Set default environments if are not set via settings defaults = { diff --git a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py index f07e96551c..fa0562a3a0 100644 --- a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py +++ b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py @@ -25,7 +25,7 @@ class UnrealPrelaunchHook(PreLaunchHook): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.signature = "( {} )".format(self.__class__.__name__) + self.signature = f"( {self.__class__.__name__} )" def _get_work_filename(self): # Use last workfile if was found @@ -99,6 +99,7 @@ class UnrealPrelaunchHook(PreLaunchHook): f"character ({unreal_project_name}). Appending 'P'" )) unreal_project_name = f"P{unreal_project_name}" + unreal_project_filename = f'{unreal_project_name}.uproject' project_path = Path(os.path.join(workdir, unreal_project_name)) @@ -138,6 +139,11 @@ class UnrealPrelaunchHook(PreLaunchHook): )) # Set "OPENPYPE_UNREAL_PLUGIN" to current process environment for # execution of `create_unreal_project` + if self.launch_context.env.get("OPENPYPE_UNREAL_PLUGIN"): + self.log.info(( + f"{self.signature} using OpenPype plugin from " + f"{self.launch_context.env.get('OPENPYPE_UNREAL_PLUGIN')}" + )) env_key = "OPENPYPE_UNREAL_PLUGIN" if self.launch_context.env.get(env_key): os.environ[env_key] = self.launch_context.env[env_key] diff --git a/openpype/hosts/unreal/lib.py b/openpype/hosts/unreal/lib.py index 906002b38f..fdf3acb37b 100644 --- a/openpype/hosts/unreal/lib.py +++ b/openpype/hosts/unreal/lib.py @@ -280,7 +280,7 @@ def create_unreal_project(project_name: str, python_path = None if platform.system().lower() == "windows": python_path = engine_path / ("Engine/Binaries/ThirdParty/" - "Python3/Win64/pythonw.exe") + "Python3/Win64/python.exe") if platform.system().lower() == "linux": python_path = engine_path / ("Engine/Binaries/ThirdParty/" @@ -294,8 +294,8 @@ def create_unreal_project(project_name: str, raise NotImplementedError("Unsupported platform") if not python_path.exists(): raise RuntimeError(f"Unreal Python not found at {python_path}") - subprocess.run( - [python_path.as_posix(), "-m", "pip", "install", "pyside2"]) + out = subprocess.check_call( + [python_path.as_posix(), "-m", "pip", "install", "--user", "pyside2"]) if dev_mode or preset["dev_mode"]: _prepare_cpp_project(project_file, engine_path) From e9a855af72ac958b23753e0f71f0fb67ce24871f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 17 May 2022 18:04:16 +0200 Subject: [PATCH 294/398] added ability to know if project is enabled without prequerying all other projects --- openpype/modules/sync_server/sync_server_module.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py index 45ff8bc4d1..5a1d8467ec 100644 --- a/openpype/modules/sync_server/sync_server_module.py +++ b/openpype/modules/sync_server/sync_server_module.py @@ -921,12 +921,18 @@ class SyncServerModule(OpenPypeModule, ITrayModule): if self.enabled: for project in self.connection.projects(projection={"name": 1}): project_name = project["name"] - project_settings = self.get_sync_project_setting(project_name) - if project_settings and project_settings.get("enabled"): + if self.is_project_enabled(project_name): enabled_projects.append(project_name) return enabled_projects + def is_project_enabled(self, project_name): + if self.enabled: + project_settings = self.get_sync_project_setting(project_name) + if project_settings and project_settings.get("enabled"): + return True + return False + def handle_alternate_site(self, collection, representation, processed_site, file_id, synced_file_id): """ From 96247b4ff636a10cf12fb71221fa3dcb6a4d465e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 17 May 2022 18:06:02 +0200 Subject: [PATCH 295/398] cache sync server data to avoid refreshing of modules manager all the time --- openpype/tools/loader/model.py | 66 +++++++++++++++++++++++++--------- 1 file changed, 50 insertions(+), 16 deletions(-) diff --git a/openpype/tools/loader/model.py b/openpype/tools/loader/model.py index 8cb8f30013..6f39c428be 100644 --- a/openpype/tools/loader/model.py +++ b/openpype/tools/loader/model.py @@ -1,6 +1,7 @@ import copy import re import math +import time from uuid import uuid4 from Qt import QtCore, QtGui @@ -38,6 +39,14 @@ def is_filtering_recursible(): class BaseRepresentationModel(object): """Methods for SyncServer useful in multiple models""" + # Cheap & hackish way how to avoid refreshing of whole sync server module + # on each selection change + _last_project = None + _modules_manager = None + _last_project_cache = 0 + _last_manager_cache = 0 + _max_project_cache_time = 30 + _max_manager_cache_time = 60 def reset_sync_server(self, project_name=None): """Sets/Resets sync server vars after every change (refresh.)""" @@ -47,28 +56,53 @@ class BaseRepresentationModel(object): remote_site = remote_provider = None if not project_name: - project_name = self.dbcon.Session["AVALON_PROJECT"] + project_name = self.dbcon.Session.get("AVALON_PROJECT") else: self.dbcon.Session["AVALON_PROJECT"] = project_name - if project_name: - manager = ModulesManager() - sync_server = manager.modules_by_name["sync_server"] + if not project_name: + self.repre_icons = repre_icons + self.sync_server = sync_server + self.active_site = active_site + self.active_provider = active_provider + self.remote_site = remote_site + self.remote_provider = remote_provider + return - if project_name in sync_server.get_enabled_projects(): - active_site = sync_server.get_active_site(project_name) - active_provider = sync_server.get_provider_for_site( - project_name, active_site) - if active_site == 'studio': # for studio use explicit icon - active_provider = 'studio' + now_time = time.time() + project_cache_diff = now_time - self._last_project_cache + if project_cache_diff > self._max_project_cache_time: + self._last_project = None - remote_site = sync_server.get_remote_site(project_name) - remote_provider = sync_server.get_provider_for_site( - project_name, remote_site) - if remote_site == 'studio': # for studio use explicit icon - remote_provider = 'studio' + if project_name == self._last_project: + return - repre_icons = lib.get_repre_icons() + self._last_project = project_name + self._last_project_cache = now_time + + manager_cache_diff = now_time - self._last_manager_cache + if manager_cache_diff > self._max_manager_cache_time: + self._modules_manager = None + + if self._modules_manager is None: + self._modules_manager = ModulesManager() + self._last_manager_cache = now_time + + sync_server = self._modules_manager.modules_by_name["sync_server"] + if sync_server.is_project_enabled(project_name): + active_site = sync_server.get_active_site(project_name) + active_provider = sync_server.get_provider_for_site( + project_name, active_site) + if active_site == 'studio': # for studio use explicit icon + active_provider = 'studio' + + remote_site = sync_server.get_remote_site(project_name) + remote_provider = sync_server.get_provider_for_site( + project_name, remote_site) + if remote_site == 'studio': # for studio use explicit icon + remote_provider = 'studio' + + repre_icons = lib.get_repre_icons() self.repre_icons = repre_icons self.sync_server = sync_server From 625b74d5215a3d1cf2e8b256c35c1277d55515fb Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 18 May 2022 09:51:50 +0200 Subject: [PATCH 296/398] Fix - skip collector in PS when automatic testing --- .../hosts/photoshop/plugins/publish/collect_batch_data.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/hosts/photoshop/plugins/publish/collect_batch_data.py b/openpype/hosts/photoshop/plugins/publish/collect_batch_data.py index 448493d370..736e43de53 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_batch_data.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_batch_data.py @@ -39,6 +39,10 @@ class CollectBatchData(pyblish.api.ContextPlugin): def process(self, context): self.log.info("CollectBatchData") batch_dir = os.environ.get("OPENPYPE_PUBLISH_DATA") + if (os.environ.get("IS_TEST") and + (not batch_dir or not os.path.exists(batch_dir))): + self.log.debug("Automatic testing, no batch data, skipping") + return assert batch_dir, ( "Missing `OPENPYPE_PUBLISH_DATA`") From b2e1c4badec2c833e8d2a75f6a5c5b114faf88cd Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 18 May 2022 09:53:59 +0200 Subject: [PATCH 297/398] Fix - skip collector in PS when automatic testing --- openpype/hosts/photoshop/plugins/publish/collect_batch_data.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/publish/collect_batch_data.py b/openpype/hosts/photoshop/plugins/publish/collect_batch_data.py index 736e43de53..2881ef0ea6 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_batch_data.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_batch_data.py @@ -39,8 +39,7 @@ class CollectBatchData(pyblish.api.ContextPlugin): def process(self, context): self.log.info("CollectBatchData") batch_dir = os.environ.get("OPENPYPE_PUBLISH_DATA") - if (os.environ.get("IS_TEST") and - (not batch_dir or not os.path.exists(batch_dir))): + if os.environ.get("IS_TEST"): self.log.debug("Automatic testing, no batch data, skipping") return From 42fd8d637deb7290defb7d98aee0089d0ac4ba07 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 18 May 2022 10:41:58 +0200 Subject: [PATCH 298/398] always create new thumbnail representation in standalone publisher no matter what extension source has --- .../plugins/publish/extract_thumbnail.py | 99 ++++++++----------- 1 file changed, 41 insertions(+), 58 deletions(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py index 941a76b05b..3ee2f70809 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py @@ -5,6 +5,7 @@ import openpype.api from openpype.lib import ( get_ffmpeg_tool_path, get_ffprobe_streams, + path_to_subprocess_arg, ) @@ -37,82 +38,69 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin): if not thumbnail_repre: return + thumbnail_repre.pop("thumbnail") files = thumbnail_repre.get("files") if not files: return if isinstance(files, list): - files_len = len(files) - file = str(files[0]) + first_filename = str(files[0]) else: - files_len = 1 - file = files + first_filename = files staging_dir = None - is_jpeg = False - if file.endswith(".jpeg") or file.endswith(".jpg"): - is_jpeg = True - if is_jpeg and files_len == 1: - # skip if already is single jpeg file - return + # Convert to jpeg if not yet + full_input_path = os.path.join( + thumbnail_repre["stagingDir"], first_filename + ) + self.log.info("input {}".format(full_input_path)) + with tempfile.NamedTemporaryFile(suffix=".jpg") as tmp: + full_thumbnail_path = tmp.name - elif is_jpeg: - # use first frame as thumbnail if is sequence of jpegs - full_thumbnail_path = os.path.join( - thumbnail_repre["stagingDir"], file - ) - self.log.info( - "For thumbnail is used file: {}".format(full_thumbnail_path) - ) + self.log.info("output {}".format(full_thumbnail_path)) - else: - # Convert to jpeg if not yet - full_input_path = os.path.join(thumbnail_repre["stagingDir"], file) - self.log.info("input {}".format(full_input_path)) + instance.context.data["cleanupFullPaths"].append(full_thumbnail_path) - full_thumbnail_path = tempfile.mkstemp(suffix=".jpg")[1] - self.log.info("output {}".format(full_thumbnail_path)) + ffmpeg_path = get_ffmpeg_tool_path("ffmpeg") - ffmpeg_path = get_ffmpeg_tool_path("ffmpeg") + ffmpeg_args = self.ffmpeg_args or {} - ffmpeg_args = self.ffmpeg_args or {} + jpeg_items = [ + path_to_subprocess_arg(ffmpeg_path), + # override file if already exists + "-y" + ] - jpeg_items = [ - "\"{}\"".format(ffmpeg_path), - # override file if already exists - "-y" - ] - - # add input filters from peresets - jpeg_items.extend(ffmpeg_args.get("input") or []) - # input file - jpeg_items.append("-i \"{}\"".format(full_input_path)) + # add input filters from peresets + jpeg_items.extend(ffmpeg_args.get("input") or []) + # input file + jpeg_items.extend([ + "-i", path_to_subprocess_arg(full_input_path), # extract only single file - jpeg_items.append("-frames:v 1") + "-frames:v", "1", # Add black background for transparent images - jpeg_items.append(( - "-filter_complex" - " \"color=black,format=rgb24[c]" + "-filter_complex", ( + "\"color=black,format=rgb24[c]" ";[c][0]scale2ref[c][i]" ";[c][i]overlay=format=auto:shortest=1,setsar=1\"" - )) + ), + ]) - jpeg_items.extend(ffmpeg_args.get("output") or []) + jpeg_items.extend(ffmpeg_args.get("output") or []) - # output file - jpeg_items.append("\"{}\"".format(full_thumbnail_path)) + # output file + jpeg_items.append(path_to_subprocess_arg(full_thumbnail_path)) - subprocess_jpeg = " ".join(jpeg_items) + subprocess_jpeg = " ".join(jpeg_items) - # run subprocess - self.log.debug("Executing: {}".format(subprocess_jpeg)) - openpype.api.run_subprocess( - subprocess_jpeg, shell=True, logger=self.log - ) + # run subprocess + self.log.debug("Executing: {}".format(subprocess_jpeg)) + openpype.api.run_subprocess( + subprocess_jpeg, shell=True, logger=self.log + ) # remove thumbnail key from origin repre - thumbnail_repre.pop("thumbnail") streams = get_ffprobe_streams(full_thumbnail_path) width = height = None for stream in streams: @@ -121,8 +109,7 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin): height = stream["height"] break - filename = os.path.basename(full_thumbnail_path) - staging_dir = staging_dir or os.path.dirname(full_thumbnail_path) + staging_dir, filename = os.path.split(full_thumbnail_path) # create new thumbnail representation representation = { @@ -130,15 +117,11 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin): 'ext': 'jpg', 'files': filename, "stagingDir": staging_dir, - "tags": ["thumbnail"], + "tags": ["thumbnail", "delete"], } if width and height: representation["width"] = width representation["height"] = height - # # add Delete tag when temp file was rendered - if not is_jpeg: - representation["tags"].append("delete") - self.log.info(f"New representation {representation}") instance.data["representations"].append(representation) From 20f2fd5904adeb27098e7bcc413494d90e9304c3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 18 May 2022 10:42:06 +0200 Subject: [PATCH 299/398] remove temp json file after publishing --- openpype/tools/standalonepublish/widgets/widget_components.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/tools/standalonepublish/widgets/widget_components.py b/openpype/tools/standalonepublish/widgets/widget_components.py index fbafc7142a..b3280089c3 100644 --- a/openpype/tools/standalonepublish/widgets/widget_components.py +++ b/openpype/tools/standalonepublish/widgets/widget_components.py @@ -202,6 +202,7 @@ def cli_publish(data, publish_paths, gui=True): if os.path.exists(json_data_path): with open(json_data_path, "r") as f: result = json.load(f) + os.remove(json_data_path) log.info(f"Publish result: {result}") From 99a273c67d950815c4fb40b15506a04f1222159d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 18 May 2022 12:35:01 +0200 Subject: [PATCH 300/398] OP-3154 - reworked logic Previous one was limiting attribute removal only on shapes, this should remove attribute in more places. --- openpype/hosts/maya/plugins/load/actions.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/actions.py b/openpype/hosts/maya/plugins/load/actions.py index 14518ead5d..253dae1e43 100644 --- a/openpype/hosts/maya/plugins/load/actions.py +++ b/openpype/hosts/maya/plugins/load/actions.py @@ -114,8 +114,6 @@ class ImportMayaLoader(load.LoaderPlugin): if choice is False: return - clean_import = data.get("clean_import", False) - asset = context['asset'] namespace = namespace or unique_namespace( @@ -133,13 +131,14 @@ class ImportMayaLoader(load.LoaderPlugin): groupReference=True, groupName="{}:{}".format(namespace, name)) - if clean_import: - shapes = cmds.ls(nodes, shapes=True, long=True) - for shape in shapes: - meshes = cmds.ls('{}.cbId'.format(shape)) - for mesh in meshes: - print("Removing ... " + (mesh)) - cmds.deleteAttr(mesh) + if data.get("clean_import", False): + remove_attributes = ["cbId"] + for node in nodes: + for attr in remove_attributes: + if cmds.attributeQuery(attr, node=node, exists=True): + full_attr = "{}.{}".format(node, attr) + print("Removing {}".format(full_attr)) + cmds.deleteAttr(full_attr) # We do not containerize imported content, it remains unmanaged return From 9fc70adfffeb0569f8c0f32f52eb80c722f923b7 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 18 May 2022 13:07:33 +0200 Subject: [PATCH 301/398] flame: abs number only if not 0 --- .../plugins/publish/collect_timeline_instances.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py b/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py index 012cb110ec..0aca7c38d5 100644 --- a/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py +++ b/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py @@ -75,11 +75,17 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin): marker_data["handleEnd"] ) + # make sure value is absolute + if head != 0: + head = abs(head) + if tail != 0: + tail = abs(tail) + # solve handles length marker_data["handleStart"] = min( - marker_data["handleStart"], abs(head)) + marker_data["handleStart"], head) marker_data["handleEnd"] = min( - marker_data["handleEnd"], abs(tail)) + marker_data["handleEnd"], tail) workfile_start = self._set_workfile_start(marker_data) From 43f7ad57ce3b3d9c289002f311855b031f1f7f05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Wed, 18 May 2022 15:08:09 +0200 Subject: [PATCH 302/398] support for other renderman nodes --- .../maya/plugins/publish/collect_look.py | 39 ++++++++++++++++--- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_look.py b/openpype/hosts/maya/plugins/publish/collect_look.py index 9697d0884f..692ecdcde1 100644 --- a/openpype/hosts/maya/plugins/publish/collect_look.py +++ b/openpype/hosts/maya/plugins/publish/collect_look.py @@ -25,6 +25,32 @@ RENDERER_NODE_TYPES = [ SHAPE_ATTRS = set(SHAPE_ATTRS) +DEFAULT_FILE_NODES = frozenset( + ["file"] +) + +ARNOLD_FILE_NODES = frozenset( + ["aiImage"] +) + +REDSHIFT_FILE_NODES = frozenset( + ["RedshiftNormalMap"] +) + +RENDERMAN_FILE_NODES = frozenset( + [ + "PxrBump", + "PxrNormalMap", + # PxrMultiTexture (need to handle multiple filename0 attrs) + "PxrPtexture", + "PxrTexture", + ] +) + +NODES_WITH_FILENAME = frozenset().union( + DEFAULT_FILE_NODES, ARNOLD_FILE_NODES, RENDERMAN_FILE_NODES +) + def get_look_attrs(node): """Returns attributes of a node that are important for the look. @@ -171,7 +197,7 @@ def get_file_node_path(node): if any(pattern in lower for pattern in patterns): return texture_pattern - if cmds.nodeType(node) in {'aiImage', 'PxrTexture'}: + if cmds.nodeType(node) in NODES_WITH_FILENAME: return cmds.getAttr('{0}.filename'.format(node)) if cmds.nodeType(node) == 'RedshiftNormalMap': return cmds.getAttr('{}.tex0'.format(node)) @@ -383,10 +409,13 @@ class CollectLook(pyblish.api.InstancePlugin): or [] ) - files = cmds.ls(history, type="file", long=True) - files.extend(cmds.ls(history, type="aiImage", long=True)) - files.extend(cmds.ls(history, type="PxrTexture", long=True)) - files.extend(cmds.ls(history, type="RedshiftNormalMap", long=True)) + all_supported_nodes = set().union( + DEFAULT_FILE_NODES, ARNOLD_FILE_NODES, REDSHIFT_FILE_NODES, + RENDERMAN_FILE_NODES + ) + files = [] + for node_type in all_supported_nodes: + files.extend(cmds.ls(history, type=node_type, long=True)) self.log.info("Collected file nodes:\n{}".format(files)) # Collect textures if any file nodes are found From 5ceba8cad4ee59c662fbf172041821d6f4d5fa62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Wed, 18 May 2022 17:52:59 +0200 Subject: [PATCH 303/398] fix supported nodes --- .../maya/plugins/publish/collect_look.py | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_look.py b/openpype/hosts/maya/plugins/publish/collect_look.py index 692ecdcde1..123e2637cb 100644 --- a/openpype/hosts/maya/plugins/publish/collect_look.py +++ b/openpype/hosts/maya/plugins/publish/collect_look.py @@ -22,21 +22,16 @@ RENDERER_NODE_TYPES = [ # redshift "RedshiftMeshParameters" ] - SHAPE_ATTRS = set(SHAPE_ATTRS) - DEFAULT_FILE_NODES = frozenset( ["file"] ) - ARNOLD_FILE_NODES = frozenset( ["aiImage"] ) - REDSHIFT_FILE_NODES = frozenset( ["RedshiftNormalMap"] ) - RENDERMAN_FILE_NODES = frozenset( [ "PxrBump", @@ -46,9 +41,14 @@ RENDERMAN_FILE_NODES = frozenset( "PxrTexture", ] ) - +NODES_WITH_FILE = frozenset().union( + DEFAULT_FILE_NODES +) NODES_WITH_FILENAME = frozenset().union( - DEFAULT_FILE_NODES, ARNOLD_FILE_NODES, RENDERMAN_FILE_NODES + ARNOLD_FILE_NODES, RENDERMAN_FILE_NODES +) +NODES_WITH_TEX = frozenset().union( + REDSHIFT_FILE_NODES ) @@ -550,20 +550,23 @@ class CollectLook(pyblish.api.InstancePlugin): dict """ self.log.debug("processing: {}".format(node)) - if cmds.nodeType(node) not in { - "file", "aiImage", "RedshiftNormalMap", "PxrTexture"}: + all_supported_nodes = set().union( + DEFAULT_FILE_NODES, ARNOLD_FILE_NODES, REDSHIFT_FILE_NODES, + RENDERMAN_FILE_NODES + ) + if cmds.nodeType(node) not in all_supported_nodes: self.log.error( "Unsupported file node: {}".format(cmds.nodeType(node))) raise AssertionError("Unsupported file node") self.log.debug(" - got {}".format(cmds.nodeType(node))) - if cmds.nodeType(node) == 'file': + if cmds.nodeType(node) in NODES_WITH_FILE: attribute = "{}.fileTextureName".format(node) computed_attribute = "{}.computedFileTextureNamePattern".format(node) - elif cmds.nodeType(node) in ['aiImage', 'PxrTexture']: + elif cmds.nodeType(node) in NODES_WITH_FILENAME: attribute = "{}.filename".format(node) computed_attribute = attribute - elif cmds.nodeType(node) == 'RedshiftNormalMap': + elif cmds.nodeType(node) in NODES_WITH_TEX: attribute = "{}.tex0".format(node) computed_attribute = attribute From b42382ea184a25aae6024dd9ee0788bba1b49bba Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Thu, 19 May 2022 11:55:59 +0200 Subject: [PATCH 304/398] commit all changes in CI --- .github/workflows/prerelease.yml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 8f51f27994..141dc0696d 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -62,7 +62,19 @@ jobs: - name: "🖨️ Print changelog to console" if: steps.version_type.outputs.type != 'skip' run: cat CHANGELOG.md - + + - name: 💾 Commit and Tag + id: git_commit + if: steps.version_type.outputs.type != 'skip' + run: | + git config user.email ${{ secrets.CI_EMAIL }} + git config user.name ${{ secrets.CI_USER }} + git add . + git commit -m "[Automated] Bump version" + tag_name="CI/${{ steps.version.outputs.next_tag }}" + echo $tag_name + git tag -a $tag_name -m "nightly build" + - name: Push to protected main branch uses: CasperWA/push-protected@v2.10.0 with: From f7b7237377d85abdc5856b2a9b0c0aa35e05cecc Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 19 May 2022 11:54:11 +0200 Subject: [PATCH 305/398] fix order of arguments in push hierarchical attributes action --- .../event_handlers_server/action_push_frame_values_to_task.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/event_handlers_server/action_push_frame_values_to_task.py b/openpype/modules/ftrack/event_handlers_server/action_push_frame_values_to_task.py index 868bbb8463..1209375f82 100644 --- a/openpype/modules/ftrack/event_handlers_server/action_push_frame_values_to_task.py +++ b/openpype/modules/ftrack/event_handlers_server/action_push_frame_values_to_task.py @@ -356,7 +356,7 @@ class PushHierValuesToNonHier(ServerAction): values_per_entity_id[entity_id][key] = None values = query_custom_attributes( - session, all_ids_with_parents, hier_attr_ids, True + session, hier_attr_ids, all_ids_with_parents, True ) for item in values: entity_id = item["entity_id"] From 3a73e7e7d742ac2a5482238da054a6d6a41c3e9f Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Thu, 19 May 2022 12:15:33 +0200 Subject: [PATCH 306/398] checkout main in CI --- .github/workflows/prerelease.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 141dc0696d..bf39f8f956 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -69,6 +69,8 @@ jobs: run: | git config user.email ${{ secrets.CI_EMAIL }} git config user.name ${{ secrets.CI_USER }} + git checkout main + git pull git add . git commit -m "[Automated] Bump version" tag_name="CI/${{ steps.version.outputs.next_tag }}" From 7d0c58c5e2355fd3fb57252e3942a8b0b016ff09 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Thu, 19 May 2022 10:24:57 +0000 Subject: [PATCH 307/398] [Automated] Bump version --- CHANGELOG.md | 256 +++++++++++++++++++++----------------------- openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 125 insertions(+), 135 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2ff9f919c..6546ab6139 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,166 +1,156 @@ # Changelog -## [3.10.0-nightly.2](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.10.0-nightly.3](https://github.com/pypeclub/OpenPype/tree/HEAD) -[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.9.4...HEAD) +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.9.8...HEAD) -### 📖 Documentation +**🆕 New features** -- Docs: add all-contributors config and initial list [\#3094](https://github.com/pypeclub/OpenPype/pull/3094) -- Nuke docs with videos [\#3052](https://github.com/pypeclub/OpenPype/pull/3052) +- General: OpenPype modules publish plugins are registered in host [\#3180](https://github.com/pypeclub/OpenPype/pull/3180) +- General: Creator plugins from addons can be registered [\#3179](https://github.com/pypeclub/OpenPype/pull/3179) +- Ftrack: Single image reviewable [\#3157](https://github.com/pypeclub/OpenPype/pull/3157) +- Nuke: Expose write attributes to settings [\#3123](https://github.com/pypeclub/OpenPype/pull/3123) +- Hiero: Initial frame publish support [\#3106](https://github.com/pypeclub/OpenPype/pull/3106) **🚀 Enhancements** -- Standalone publisher: add support for bgeo and vdb [\#3080](https://github.com/pypeclub/OpenPype/pull/3080) -- Update collect\_render.py [\#3055](https://github.com/pypeclub/OpenPype/pull/3055) -- SiteSync: Added compute\_resource\_sync\_sites to sync\_server\_module [\#2983](https://github.com/pypeclub/OpenPype/pull/2983) +- Maya: added clean\_import option to Import loader [\#3181](https://github.com/pypeclub/OpenPype/pull/3181) +- Maya: add maya 2023 to default applications [\#3167](https://github.com/pypeclub/OpenPype/pull/3167) +- Compressed bgeo publishing in SAP and Houdini loader [\#3153](https://github.com/pypeclub/OpenPype/pull/3153) +- General: Add 'dataclasses' to required python modules [\#3149](https://github.com/pypeclub/OpenPype/pull/3149) +- Hooks: Tweak logging grammar [\#3147](https://github.com/pypeclub/OpenPype/pull/3147) +- Nuke: settings for reformat node in CreateWriteRender node [\#3143](https://github.com/pypeclub/OpenPype/pull/3143) +- Houdini: Add loader for alembic through Alembic Archive node [\#3140](https://github.com/pypeclub/OpenPype/pull/3140) +- Publisher: UI Modifications and fixes [\#3139](https://github.com/pypeclub/OpenPype/pull/3139) +- General: Simplified OP modules/addons import [\#3137](https://github.com/pypeclub/OpenPype/pull/3137) +- Terminal: Tweak coloring of TrayModuleManager logging enabled states [\#3133](https://github.com/pypeclub/OpenPype/pull/3133) +- General: Cleanup some Loader docstrings [\#3131](https://github.com/pypeclub/OpenPype/pull/3131) +- Nuke: render instance with subset name filtered overrides [\#3117](https://github.com/pypeclub/OpenPype/pull/3117) +- Unreal: Layout and Camera update and remove functions reimplemented and improvements [\#3116](https://github.com/pypeclub/OpenPype/pull/3116) +- Settings: Remove environment groups from settings [\#3115](https://github.com/pypeclub/OpenPype/pull/3115) +- TVPaint: Match renderlayer key with other hosts [\#3110](https://github.com/pypeclub/OpenPype/pull/3110) +- Ftrack: AssetVersion status on publish [\#3108](https://github.com/pypeclub/OpenPype/pull/3108) +- Tray publisher: Simple families from settings [\#3105](https://github.com/pypeclub/OpenPype/pull/3105) **🐛 Bug fixes** -- RoyalRender Control Submission - AVALON\_APP\_NAME default [\#3091](https://github.com/pypeclub/OpenPype/pull/3091) -- Ftrack: Update Create Folders action [\#3089](https://github.com/pypeclub/OpenPype/pull/3089) -- Project Manager: Avoid unnecessary updates of asset documents [\#3083](https://github.com/pypeclub/OpenPype/pull/3083) -- Standalone publisher: Fix plugins install [\#3077](https://github.com/pypeclub/OpenPype/pull/3077) -- General: Extract review sequence is not converted with same names [\#3076](https://github.com/pypeclub/OpenPype/pull/3076) -- Webpublisher: Use variant value [\#3068](https://github.com/pypeclub/OpenPype/pull/3068) -- Nuke: Add aov matching even for remainder and prerender [\#3060](https://github.com/pypeclub/OpenPype/pull/3060) +- Standalone Publisher: Always create new representation for thumbnail [\#3203](https://github.com/pypeclub/OpenPype/pull/3203) +- Photoshop: skip collector when automatic testing [\#3202](https://github.com/pypeclub/OpenPype/pull/3202) +- Nuke: render/workfile version sync doesn't work on farm [\#3185](https://github.com/pypeclub/OpenPype/pull/3185) +- Ftrack: Review image only if there are no mp4 reviews [\#3183](https://github.com/pypeclub/OpenPype/pull/3183) +- Ftrack: Locations deepcopy issue [\#3177](https://github.com/pypeclub/OpenPype/pull/3177) +- General: Avoid creating multiple thumbnails [\#3176](https://github.com/pypeclub/OpenPype/pull/3176) +- General/Hiero: better clip duration calculation [\#3169](https://github.com/pypeclub/OpenPype/pull/3169) +- General: Oiio conversion for ffmpeg checks for invalid characters [\#3166](https://github.com/pypeclub/OpenPype/pull/3166) +- Fix for attaching render to subset [\#3164](https://github.com/pypeclub/OpenPype/pull/3164) +- Harmony: fixed missing task name in render instance [\#3163](https://github.com/pypeclub/OpenPype/pull/3163) +- Ftrack: Action delete old versions formatting works [\#3152](https://github.com/pypeclub/OpenPype/pull/3152) +- Deadline: fix the output directory [\#3144](https://github.com/pypeclub/OpenPype/pull/3144) +- General: New Session schema [\#3141](https://github.com/pypeclub/OpenPype/pull/3141) +- TVPaint: Composite layers in reversed order [\#3135](https://github.com/pypeclub/OpenPype/pull/3135) +- Nuke: fixing default settings for workfile builder loaders [\#3120](https://github.com/pypeclub/OpenPype/pull/3120) +- Nuke: fix anatomy imageio regex default [\#3119](https://github.com/pypeclub/OpenPype/pull/3119) +- General: Python 3 compatibility in queries [\#3112](https://github.com/pypeclub/OpenPype/pull/3112) +- General: Collect loaded versions skips not existing representations [\#3095](https://github.com/pypeclub/OpenPype/pull/3095) **🔀 Refactored code** -- General: Move host install [\#3009](https://github.com/pypeclub/OpenPype/pull/3009) +- General: Remove remaining imports from avalon [\#3130](https://github.com/pypeclub/OpenPype/pull/3130) **Merged pull requests:** -- Nuke: added suspend\_publish knob [\#3078](https://github.com/pypeclub/OpenPype/pull/3078) -- Bump async from 2.6.3 to 2.6.4 in /website [\#3065](https://github.com/pypeclub/OpenPype/pull/3065) +- Webpublisher: replace space by underscore in subset names [\#3160](https://github.com/pypeclub/OpenPype/pull/3160) +- StandalonePublisher: removed Extract Background plugins [\#3093](https://github.com/pypeclub/OpenPype/pull/3093) + +## [3.9.8](https://github.com/pypeclub/OpenPype/tree/3.9.8) (2022-05-19) + +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.9.7...3.9.8) + +**🚀 Enhancements** + +- nuke: generate publishing nodes inside render group node [\#3206](https://github.com/pypeclub/OpenPype/pull/3206) +- Backport of fix for attaching renders to subsets [\#3195](https://github.com/pypeclub/OpenPype/pull/3195) + +**🐛 Bug fixes** + +- Standalone Publisher: Always create new representation for thumbnail [\#3204](https://github.com/pypeclub/OpenPype/pull/3204) +- Nuke: render/workfile version sync doesn't work on farm [\#3184](https://github.com/pypeclub/OpenPype/pull/3184) +- Ftrack: Review image only if there are no mp4 reviews [\#3182](https://github.com/pypeclub/OpenPype/pull/3182) +- Ftrack: Locations deepcopy issue [\#3175](https://github.com/pypeclub/OpenPype/pull/3175) +- General: Avoid creating multiple thumbnails [\#3174](https://github.com/pypeclub/OpenPype/pull/3174) +- General: TemplateResult can be copied [\#3170](https://github.com/pypeclub/OpenPype/pull/3170) + +**Merged pull requests:** + +- hiero: otio p3 compatibility issue - metadata on effect use update [\#3194](https://github.com/pypeclub/OpenPype/pull/3194) + +## [3.9.7](https://github.com/pypeclub/OpenPype/tree/3.9.7) (2022-05-11) + +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.9.6...3.9.7) + +**🆕 New features** + +- Ftrack: Single image reviewable [\#3158](https://github.com/pypeclub/OpenPype/pull/3158) + +**🚀 Enhancements** + +- Deadline output dir issue to 3.9x [\#3155](https://github.com/pypeclub/OpenPype/pull/3155) +- nuke: removing redundant code from startup [\#3142](https://github.com/pypeclub/OpenPype/pull/3142) + +**🐛 Bug fixes** + +- Ftrack: Action delete old versions formatting works [\#3154](https://github.com/pypeclub/OpenPype/pull/3154) +- nuke: adding extract thumbnail settings [\#3148](https://github.com/pypeclub/OpenPype/pull/3148) + +**Merged pull requests:** + +- Webpublisher: replace space by underscore in subset names [\#3159](https://github.com/pypeclub/OpenPype/pull/3159) + +## [3.9.6](https://github.com/pypeclub/OpenPype/tree/3.9.6) (2022-05-03) + +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.9.5...3.9.6) + +**🆕 New features** + +- Nuke: render instance with subset name filtered overrides \(3.9.x\) [\#3125](https://github.com/pypeclub/OpenPype/pull/3125) + +**🚀 Enhancements** + +- TVPaint: Match renderlayer key with other hosts [\#3109](https://github.com/pypeclub/OpenPype/pull/3109) + +**🐛 Bug fixes** + +- General: Missing version on headless mode crash properly [\#3136](https://github.com/pypeclub/OpenPype/pull/3136) +- TVPaint: Composite layers in reversed order [\#3134](https://github.com/pypeclub/OpenPype/pull/3134) +- General: Python 3 compatibility in queries [\#3111](https://github.com/pypeclub/OpenPype/pull/3111) + +**Merged pull requests:** + +- Ftrack: AssetVersion status on publish [\#3114](https://github.com/pypeclub/OpenPype/pull/3114) +- renderman support for 3.9.x [\#3107](https://github.com/pypeclub/OpenPype/pull/3107) + +## [3.9.5](https://github.com/pypeclub/OpenPype/tree/3.9.5) (2022-04-25) + +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.10.0-nightly.2...3.9.5) + +**🐛 Bug fixes** + +- Ftrack: Update Create Folders action [\#3092](https://github.com/pypeclub/OpenPype/pull/3092) +- General: Extract review sequence is not converted with same names [\#3075](https://github.com/pypeclub/OpenPype/pull/3075) +- Webpublisher: Use variant value [\#3072](https://github.com/pypeclub/OpenPype/pull/3072) ## [3.9.4](https://github.com/pypeclub/OpenPype/tree/3.9.4) (2022-04-15) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.9.4-nightly.2...3.9.4) -### 📖 Documentation - -- Documentation: more info about Tasks [\#3062](https://github.com/pypeclub/OpenPype/pull/3062) -- Documentation: Python requirements to 3.7.9 [\#3035](https://github.com/pypeclub/OpenPype/pull/3035) -- Website Docs: Remove unused pages [\#2974](https://github.com/pypeclub/OpenPype/pull/2974) - -**🆕 New features** - -- General: Local overrides for environment variables [\#3045](https://github.com/pypeclub/OpenPype/pull/3045) - -**🚀 Enhancements** - -- TVPaint: Added init file for worker to triggers missing sound file dialog [\#3053](https://github.com/pypeclub/OpenPype/pull/3053) -- Ftrack: Custom attributes can be filled in slate values [\#3036](https://github.com/pypeclub/OpenPype/pull/3036) -- Resolve environment variable in google drive credential path [\#3008](https://github.com/pypeclub/OpenPype/pull/3008) - -**🐛 Bug fixes** - -- GitHub: Updated push-protected action in github workflow [\#3064](https://github.com/pypeclub/OpenPype/pull/3064) -- Nuke: Typos in imports from Nuke implementation [\#3061](https://github.com/pypeclub/OpenPype/pull/3061) -- Hotfix: fixing deadline job publishing [\#3059](https://github.com/pypeclub/OpenPype/pull/3059) -- General: Extract Review handle invalid characters for ffmpeg [\#3050](https://github.com/pypeclub/OpenPype/pull/3050) -- Slate Review: Support to keep format on slate concatenation [\#3049](https://github.com/pypeclub/OpenPype/pull/3049) -- Webpublisher: fix processing of workfile [\#3048](https://github.com/pypeclub/OpenPype/pull/3048) -- Ftrack: Integrate ftrack api fix [\#3044](https://github.com/pypeclub/OpenPype/pull/3044) -- Webpublisher - removed wrong hardcoded family [\#3043](https://github.com/pypeclub/OpenPype/pull/3043) -- LibraryLoader: Use current project for asset query in families filter [\#3042](https://github.com/pypeclub/OpenPype/pull/3042) -- SiteSync: Providers ignore that site is disabled [\#3041](https://github.com/pypeclub/OpenPype/pull/3041) -- Unreal: Creator import fixes [\#3040](https://github.com/pypeclub/OpenPype/pull/3040) -- Settings UI: Version column can be extended so version are visible [\#3032](https://github.com/pypeclub/OpenPype/pull/3032) -- SiteSync: fix transitive alternate sites, fix dropdown in Local Settings [\#3018](https://github.com/pypeclub/OpenPype/pull/3018) - -**Merged pull requests:** - -- Deadline: reworked pools assignment [\#3051](https://github.com/pypeclub/OpenPype/pull/3051) -- Houdini: Avoid ImportError on `hdefereval` when Houdini runs without UI [\#2987](https://github.com/pypeclub/OpenPype/pull/2987) - ## [3.9.3](https://github.com/pypeclub/OpenPype/tree/3.9.3) (2022-04-07) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.9.3-nightly.2...3.9.3) -### 📖 Documentation - -- Website Docs: Manager Ftrack fix broken links [\#2979](https://github.com/pypeclub/OpenPype/pull/2979) - -**🆕 New features** - -- Ftrack: Add description integrator [\#3027](https://github.com/pypeclub/OpenPype/pull/3027) -- Publishing textures for Unreal [\#2988](https://github.com/pypeclub/OpenPype/pull/2988) - -**🚀 Enhancements** - -- Ftrack: Add more options for note text of integrate ftrack note [\#3025](https://github.com/pypeclub/OpenPype/pull/3025) -- Console Interpreter: Changed how console splitter size are reused on show [\#3016](https://github.com/pypeclub/OpenPype/pull/3016) -- Deadline: Use more suitable name for sequence review logic [\#3015](https://github.com/pypeclub/OpenPype/pull/3015) -- General: default workfile subset name for workfile [\#3011](https://github.com/pypeclub/OpenPype/pull/3011) -- Deadline: priority configurable in Maya jobs [\#2995](https://github.com/pypeclub/OpenPype/pull/2995) - -**🐛 Bug fixes** - -- Deadline: Fixed default value of use sequence for review [\#3033](https://github.com/pypeclub/OpenPype/pull/3033) -- General: Fix validate asset docs plug-in filename and class name [\#3029](https://github.com/pypeclub/OpenPype/pull/3029) -- General: Fix import after movements [\#3028](https://github.com/pypeclub/OpenPype/pull/3028) -- Harmony: Added creating subset name for workfile from template [\#3024](https://github.com/pypeclub/OpenPype/pull/3024) -- AfterEffects: Added creating subset name for workfile from template [\#3023](https://github.com/pypeclub/OpenPype/pull/3023) -- General: Add example addons to ignored [\#3022](https://github.com/pypeclub/OpenPype/pull/3022) -- Maya: Remove missing import [\#3017](https://github.com/pypeclub/OpenPype/pull/3017) -- Ftrack: multiple reviewable componets [\#3012](https://github.com/pypeclub/OpenPype/pull/3012) -- Tray publisher: Fixes after code movement [\#3010](https://github.com/pypeclub/OpenPype/pull/3010) -- Nuke: fixing unicode type detection in effect loaders [\#3002](https://github.com/pypeclub/OpenPype/pull/3002) -- Nuke: removing redundant Ftrack asset when farm publishing [\#2996](https://github.com/pypeclub/OpenPype/pull/2996) - -**Merged pull requests:** - -- Maya: Allow to select invalid camera contents if no cameras found [\#3030](https://github.com/pypeclub/OpenPype/pull/3030) -- General: adding limitations for pyright [\#2994](https://github.com/pypeclub/OpenPype/pull/2994) - ## [3.9.2](https://github.com/pypeclub/OpenPype/tree/3.9.2) (2022-04-04) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.9.2-nightly.4...3.9.2) -### 📖 Documentation - -- Documentation: Added mention of adding My Drive as a root [\#2999](https://github.com/pypeclub/OpenPype/pull/2999) -- Docs: Added MongoDB requirements [\#2951](https://github.com/pypeclub/OpenPype/pull/2951) - -**🆕 New features** - -- nuke: bypass baking [\#2992](https://github.com/pypeclub/OpenPype/pull/2992) -- Maya to Unreal: Static and Skeletal Meshes [\#2978](https://github.com/pypeclub/OpenPype/pull/2978) - -**🚀 Enhancements** - -- Nuke: add concurrency attr to deadline job [\#3005](https://github.com/pypeclub/OpenPype/pull/3005) -- Photoshop: create image without instance [\#3001](https://github.com/pypeclub/OpenPype/pull/3001) -- TVPaint: Render scene family [\#3000](https://github.com/pypeclub/OpenPype/pull/3000) -- Nuke: ReviewDataMov Read RAW attribute [\#2985](https://github.com/pypeclub/OpenPype/pull/2985) -- General: `METADATA\_KEYS` constant as `frozenset` for optimal immutable lookup [\#2980](https://github.com/pypeclub/OpenPype/pull/2980) -- General: Tools with host filters [\#2975](https://github.com/pypeclub/OpenPype/pull/2975) -- Hero versions: Use custom templates [\#2967](https://github.com/pypeclub/OpenPype/pull/2967) - -**🐛 Bug fixes** - -- Hosts: Remove path existence checks in 'add\_implementation\_envs' [\#3004](https://github.com/pypeclub/OpenPype/pull/3004) -- Fix - remove doubled dot in workfile created from template [\#2998](https://github.com/pypeclub/OpenPype/pull/2998) -- PS: fix renaming subset incorrectly in PS [\#2991](https://github.com/pypeclub/OpenPype/pull/2991) -- Fix: Disable setuptools auto discovery [\#2990](https://github.com/pypeclub/OpenPype/pull/2990) -- AEL: fix opening existing workfile if no scene opened [\#2989](https://github.com/pypeclub/OpenPype/pull/2989) -- Maya: Don't do hardlinks on windows for look publishing [\#2986](https://github.com/pypeclub/OpenPype/pull/2986) -- Settings UI: Fix version completer on linux [\#2981](https://github.com/pypeclub/OpenPype/pull/2981) -- Photoshop: Fix creation of subset names in PS review and workfile [\#2969](https://github.com/pypeclub/OpenPype/pull/2969) -- Slack: Added default for review\_upload\_limit for Slack [\#2965](https://github.com/pypeclub/OpenPype/pull/2965) -- General: OIIO conversion for ffmeg can handle sequences [\#2958](https://github.com/pypeclub/OpenPype/pull/2958) -- Settings: Conditional dictionary avoid invalid logs [\#2956](https://github.com/pypeclub/OpenPype/pull/2956) -- General: Smaller fixes and typos [\#2950](https://github.com/pypeclub/OpenPype/pull/2950) - -**Merged pull requests:** - -- Bump paramiko from 2.9.2 to 2.10.1 [\#2973](https://github.com/pypeclub/OpenPype/pull/2973) -- Bump minimist from 1.2.5 to 1.2.6 in /website [\#2954](https://github.com/pypeclub/OpenPype/pull/2954) -- Bump node-forge from 1.2.1 to 1.3.0 in /website [\#2953](https://github.com/pypeclub/OpenPype/pull/2953) -- Maya - added transparency into review creator [\#2952](https://github.com/pypeclub/OpenPype/pull/2952) - ## [3.9.1](https://github.com/pypeclub/OpenPype/tree/3.9.1) (2022-03-18) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.9.1-nightly.3...3.9.1) diff --git a/openpype/version.py b/openpype/version.py index 662adf28ca..1db666efec 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.10.0-nightly.2" +__version__ = "3.10.0-nightly.3" diff --git a/pyproject.toml b/pyproject.toml index f32e385e80..4b7972c227 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.10.0-nightly.2" # OpenPype +version = "3.10.0-nightly.3" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From e38793f258fa8dd0c6195ea3d7fac24e526725d9 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 19 May 2022 14:29:29 +0200 Subject: [PATCH 308/398] fix copy --- openpype/modules/deadline/plugins/publish/submit_publish_job.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 782f85c9d2..78ab935e42 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -466,7 +466,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): if instance_data.get("multipartExr"): preview = True - new_instance = copy(instance_data) + new_instance = deepcopy(instance_data) new_instance["subset"] = subset_name new_instance["subsetGroup"] = group_name if preview: From f734b59305c465ea11a63dec041e4b38a04836a8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 19 May 2022 15:08:39 +0200 Subject: [PATCH 309/398] block model signals during set project --- openpype/tools/project_manager/project_manager/model.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 871704e13c..b7cb0ec9ed 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -264,6 +264,8 @@ class HierarchyModel(QtCore.QAbstractItemModel): if not project_doc: return + self.blockSignals(True) + # Create project item project_item = ProjectItem(project_doc) self.add_item(project_item) @@ -377,6 +379,8 @@ class HierarchyModel(QtCore.QAbstractItemModel): self.add_items(task_items, asset_item) + self.blockSignals(False) + # Emit that project was successfully changed self.project_changed.emit() From b8e978e518c12fbc18361c45217fbba2d38eff65 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 19 May 2022 15:08:56 +0200 Subject: [PATCH 310/398] refresh of projects with force refresh of current project --- openpype/tools/project_manager/project_manager/view.py | 4 ++-- openpype/tools/project_manager/project_manager/window.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index 74f5a06b71..25174232bc 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -195,13 +195,13 @@ class HierarchyView(QtWidgets.QTreeView): for idx, width in widths_by_idx.items(): self.setColumnWidth(idx, width) - def set_project(self, project_name): + def set_project(self, project_name, force=False): # Trigger helpers first self._project_doc_cache.set_project(project_name) self._tools_cache.refresh() # Trigger update of model after all data for delegates are filled - self._source_model.set_project(project_name) + self._source_model.set_project(project_name, force) def _on_project_reset(self): self.header_init() diff --git a/openpype/tools/project_manager/project_manager/window.py b/openpype/tools/project_manager/project_manager/window.py index c281479d4f..8cc3939713 100644 --- a/openpype/tools/project_manager/project_manager/window.py +++ b/openpype/tools/project_manager/project_manager/window.py @@ -191,7 +191,7 @@ class ProjectManagerWindow(QtWidgets.QWidget): self._add_task_btn.setEnabled(project_name is not None) self._save_btn.setEnabled(project_name is not None) self._project_proxy_model.set_filter_default(project_name is not None) - self.hierarchy_view.set_project(project_name) + self.hierarchy_view.set_project(project_name, True) def _current_project(self): row = self._project_combobox.currentIndex() From 6b9983fdede5e7e086e62798abddabdec3afcb85 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 19 May 2022 14:19:30 +0100 Subject: [PATCH 311/398] Added support for both UE4 and 5 Plugin won't compile in UE4 yet. UE5 needs different modules, not available in UE4. --- .../unreal/hooks/pre_workfile_preparation.py | 12 ++--- openpype/hosts/unreal/lib.py | 48 ++++++++++++------- 2 files changed, 36 insertions(+), 24 deletions(-) diff --git a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py index fa0562a3a0..5be04fc841 100644 --- a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py +++ b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py @@ -71,7 +71,7 @@ class UnrealPrelaunchHook(PreLaunchHook): if int(engine_version.split(".")[0]) < 4 and \ int(engine_version.split(".")[1]) < 26: raise ApplicationLaunchFailed(( - f"{self.signature} Old unsupported version of UE4 " + f"{self.signature} Old unsupported version of UE " f"detected - {engine_version}")) except ValueError: # there can be string in minor version and in that case @@ -104,14 +104,14 @@ class UnrealPrelaunchHook(PreLaunchHook): project_path = Path(os.path.join(workdir, unreal_project_name)) self.log.info(( - f"{self.signature} requested UE4 version: " + f"{self.signature} requested UE version: " f"[ {engine_version} ]" )) detected = unreal_lib.get_engine_versions(self.launch_context.env) detected_str = ', '.join(detected.keys()) or 'none' self.log.info(( - f"{self.signature} detected UE4 versions: " + f"{self.signature} detected UE versions: " f"[ {detected_str} ]" )) if not detected: @@ -124,10 +124,10 @@ class UnrealPrelaunchHook(PreLaunchHook): f"detected [ {engine_version} ]" )) - ue4_path = unreal_lib.get_editor_executable_path( - Path(detected[engine_version])) + ue_path = unreal_lib.get_editor_executable_path( + Path(detected[engine_version]), engine_version) - self.launch_context.launch_args = [ue4_path.as_posix()] + self.launch_context.launch_args = [ue_path.as_posix()] project_path.mkdir(parents=True, exist_ok=True) project_file = project_path / unreal_project_filename diff --git a/openpype/hosts/unreal/lib.py b/openpype/hosts/unreal/lib.py index fdf3acb37b..f220d8dedf 100644 --- a/openpype/hosts/unreal/lib.py +++ b/openpype/hosts/unreal/lib.py @@ -70,19 +70,22 @@ def get_engine_versions(env=None): return OrderedDict() -def get_editor_executable_path(engine_path: Path) -> Path: - """Get UE4 Editor executable path.""" - ue4_path = engine_path / "Engine/Binaries" +def get_editor_executable_path(engine_path: Path, engine_version: str) -> Path: + """Get UE Editor executable path.""" + ue_path = engine_path / "Engine/Binaries" if platform.system().lower() == "windows": - ue4_path /= "Win64/UnrealEditor.exe" + if engine_version.split(".")[0] == "4": + ue_path /= "Win64/UE4Editor.exe" + elif engine_version.split(".")[0] == "5": + ue_path /= "Win64/UnrealEditor.exe" elif platform.system().lower() == "linux": - ue4_path /= "Linux/UE4Editor" + ue_path /= "Linux/UE4Editor" elif platform.system().lower() == "darwin": - ue4_path /= "Mac/UE4Editor" + ue_path /= "Mac/UE4Editor" - return ue4_path + return ue_path def _win_get_engine_versions(): @@ -208,22 +211,26 @@ def create_unreal_project(project_name: str, # created in different UE4 version. When user convert such project # to his UE4 version, Engine ID is replaced in uproject file. If some # other user tries to open it, it will present him with similar error. - ue4_modules = Path() + ue_modules = Path() if platform.system().lower() == "windows": - ue4_modules = Path(os.path.join(engine_path, "Engine", "Binaries", - "Win64", "UE4Editor.modules")) + ue_modules_path = engine_path / "Engine/Binaries/Win64" + if ue_version.split(".")[0] == "4": + ue_modules_path /= "UE4Editor.modules" + elif ue_version.split(".")[0] == "5": + ue_modules_path /= "UnrealEditor.modules" + ue_modules = Path(ue_modules_path) if platform.system().lower() == "linux": - ue4_modules = Path(os.path.join(engine_path, "Engine", "Binaries", + ue_modules = Path(os.path.join(engine_path, "Engine", "Binaries", "Linux", "UE4Editor.modules")) if platform.system().lower() == "darwin": - ue4_modules = Path(os.path.join(engine_path, "Engine", "Binaries", + ue_modules = Path(os.path.join(engine_path, "Engine", "Binaries", "Mac", "UE4Editor.modules")) - if ue4_modules.exists(): + if ue_modules.exists(): print("--- Loading Engine ID from modules file ...") - with open(ue4_modules, "r") as mp: + with open(ue_modules, "r") as mp: loaded_modules = json.load(mp) if loaded_modules.get("BuildId"): @@ -298,10 +305,11 @@ def create_unreal_project(project_name: str, [python_path.as_posix(), "-m", "pip", "install", "--user", "pyside2"]) if dev_mode or preset["dev_mode"]: - _prepare_cpp_project(project_file, engine_path) + _prepare_cpp_project(project_file, engine_path, ue_version) -def _prepare_cpp_project(project_file: Path, engine_path: Path) -> None: +def _prepare_cpp_project( + project_file: Path, engine_path: Path, ue_version: str) -> None: """Prepare CPP Unreal Project. This function will add source files needed for project to be @@ -420,8 +428,12 @@ class {1}_API A{0}GameModeBase : public AGameModeBase with open(sources_dir / f"{project_name}GameModeBase.h", mode="w") as f: f.write(game_mode_h) - u_build_tool = Path( - engine_path / "Engine/Binaries/DotNET/UnrealBuildTool/UnrealBuildTool.exe") + u_build_tool_path = engine_path / "Engine/Binaries/DotNET" + if ue_version.split(".")[0] == "4": + u_build_tool_path /= "UnrealBuildTool.exe" + elif ue_version.split(".")[0] == "5": + u_build_tool_path /= "UnrealBuildTool/UnrealBuildTool.exe" + u_build_tool = Path(u_build_tool_path) u_header_tool = None arch = "Win64" From afc1fa9a1390f6864f4d39031d8248aafa4a053e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 20 May 2022 10:19:12 +0200 Subject: [PATCH 312/398] fix persistent editors on project change --- .../project_manager/project_manager/view.py | 28 +++++++++++++++++++ .../project_manager/project_manager/window.py | 8 +++--- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index 25174232bc..2c2a17d712 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -213,6 +213,34 @@ class HierarchyView(QtWidgets.QTreeView): index = self._source_model.index_for_item(project_item) self.expand(index) + self._open_persistent_editors_on_project_refresh() + + def _open_persistent_editors_on_project_refresh(self): + model = self._source_model + parent_index = QtCore.QModelIndex() + persistent_queue = collections.deque() + persistent_queue.append((parent_index, model.rowCount())) + while persistent_queue: + item = persistent_queue.popleft() + parent_index, rows = item + if not rows: + continue + + for row in range(rows): + row_index = model.index(row, 0, parent_index) + persistent_queue.append( + (row_index, model.rowCount(row_index)) + ) + for key, column in self._column_key_to_index.items(): + if key not in self.persistent_columns: + continue + col_index = model.index(row, column, parent_index) + if bool( + model.flags(col_index) + & QtCore.Qt.ItemIsEditable + ): + self.openPersistentEditor(col_index) + def _on_rows_moved(self, index): parent_index = index.parent() if not self.isExpanded(parent_index): diff --git a/openpype/tools/project_manager/project_manager/window.py b/openpype/tools/project_manager/project_manager/window.py index 8cc3939713..6a2bc29fd1 100644 --- a/openpype/tools/project_manager/project_manager/window.py +++ b/openpype/tools/project_manager/project_manager/window.py @@ -184,14 +184,14 @@ class ProjectManagerWindow(QtWidgets.QWidget): self.resize(1200, 600) self.setStyleSheet(load_stylesheet()) - def _set_project(self, project_name=None): + def _set_project(self, project_name=None, force=False): self._create_folders_btn.setEnabled(project_name is not None) self._remove_projects_btn.setEnabled(project_name is not None) self._add_asset_btn.setEnabled(project_name is not None) self._add_task_btn.setEnabled(project_name is not None) self._save_btn.setEnabled(project_name is not None) self._project_proxy_model.set_filter_default(project_name is not None) - self.hierarchy_view.set_project(project_name, True) + self.hierarchy_view.set_project(project_name, force) def _current_project(self): row = self._project_combobox.currentIndex() @@ -229,11 +229,11 @@ class ProjectManagerWindow(QtWidgets.QWidget): self._project_combobox.setCurrentIndex(row) selected_project = self._current_project() - self._set_project(selected_project) + self._set_project(selected_project, True) def _on_project_change(self): selected_project = self._current_project() - self._set_project(selected_project) + self._set_project(selected_project, False) def _on_project_refresh(self): self.refresh_projects() From d46c919281145a7d07ffb5aca1ef16f7407c6e46 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 12 May 2022 11:46:55 +0200 Subject: [PATCH 313/398] general: calculation of duration should not exclude one frame From ddfee503677c3ad6e653c9346cd742520667d0d9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 12 May 2022 11:47:23 +0200 Subject: [PATCH 314/398] hiero: fitting new duration calculation From f4ebcdb27856888794c59c142c43806b863cb61b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 19 May 2022 12:22:35 +0200 Subject: [PATCH 315/398] Hiero: small bugs - track name was not equal and was catching similar names too - publish action in timeline submenu was broken - parse_container was returning false data even it should not --- openpype/hosts/hiero/api/lib.py | 7 ++++--- openpype/hosts/hiero/api/pipeline.py | 5 +++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/hiero/api/lib.py b/openpype/hosts/hiero/api/lib.py index 2a4cd03b76..be02c7c793 100644 --- a/openpype/hosts/hiero/api/lib.py +++ b/openpype/hosts/hiero/api/lib.py @@ -118,7 +118,7 @@ def get_current_track(sequence, name, audio=False): # get track by name track = None for _track in tracks: - if _track.name() in name: + if _track.name() == name: track = _track if not track: @@ -126,6 +126,7 @@ def get_current_track(sequence, name, audio=False): track = hiero.core.VideoTrack(name) else: track = hiero.core.AudioTrack(name) + sequence.addTrack(track) return track @@ -497,7 +498,7 @@ class PyblishSubmission(hiero.exporters.FnSubmission.Submission): from . import publish # Add submission to Hiero module for retrieval in plugins. hiero.submission = self - publish() + publish(hiero.ui.mainWindow()) def add_submission(): @@ -527,7 +528,7 @@ class PublishAction(QtWidgets.QAction): # from getting picked up when not using the "Export" dialog. if hasattr(hiero, "submission"): del hiero.submission - publish() + publish(hiero.ui.mainWindow()) def eventHandler(self, event): # Add the Menu to the right-click menu diff --git a/openpype/hosts/hiero/api/pipeline.py b/openpype/hosts/hiero/api/pipeline.py index 8025ebff05..9b628ec70b 100644 --- a/openpype/hosts/hiero/api/pipeline.py +++ b/openpype/hosts/hiero/api/pipeline.py @@ -143,6 +143,11 @@ def parse_container(track_item, validate=True): """ # convert tag metadata to normal keys names data = lib.get_track_item_pype_data(track_item) + if ( + not data + or data.get("id") != "pyblish.avalon.container" + ): + return if validate and data and data.get("schema"): schema.validate(data) From c09038984190085b6182cf075455d503692e10ca Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 19 May 2022 19:46:34 +0200 Subject: [PATCH 316/398] Hiero: add new `get_timeline_selection` function --- openpype/hosts/hiero/api/__init__.py | 2 ++ openpype/hosts/hiero/api/lib.py | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/openpype/hosts/hiero/api/__init__.py b/openpype/hosts/hiero/api/__init__.py index f3c32b268c..fc2d017f04 100644 --- a/openpype/hosts/hiero/api/__init__.py +++ b/openpype/hosts/hiero/api/__init__.py @@ -27,6 +27,7 @@ from .lib import ( get_track_items, get_current_project, get_current_sequence, + get_timeline_selection, get_current_track, get_track_item_pype_tag, set_track_item_pype_tag, @@ -80,6 +81,7 @@ __all__ = [ "get_track_items", "get_current_project", "get_current_sequence", + "get_timeline_selection", "get_current_track", "get_track_item_pype_tag", "set_track_item_pype_tag", diff --git a/openpype/hosts/hiero/api/lib.py b/openpype/hosts/hiero/api/lib.py index be02c7c793..115a926d84 100644 --- a/openpype/hosts/hiero/api/lib.py +++ b/openpype/hosts/hiero/api/lib.py @@ -96,6 +96,12 @@ def get_current_sequence(name=None, new=False): return sequence +def get_timeline_selection(): + active_sequence = hiero.ui.activeSequence() + timeline_editor = hiero.ui.getTimelineEditor(active_sequence) + return list(timeline_editor.selection()) + + def get_current_track(sequence, name, audio=False): """ Get current track in context of active project. From 9dd13425c36511873aeefb46f88f721381d91cc6 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 19 May 2022 19:47:15 +0200 Subject: [PATCH 317/398] Hiero: removing event slowing down work with timeline --- openpype/hosts/hiero/api/events.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/hiero/api/events.py b/openpype/hosts/hiero/api/events.py index 7fab3edfc8..59fd278a81 100644 --- a/openpype/hosts/hiero/api/events.py +++ b/openpype/hosts/hiero/api/events.py @@ -109,8 +109,9 @@ def register_hiero_events(): # hiero.core.events.registerInterest("kShutdown", shutDown) # hiero.core.events.registerInterest("kStartup", startupCompleted) - hiero.core.events.registerInterest( - ("kSelectionChanged", "kTimeline"), selection_changed_timeline) + # INFO: was disabled because it was slowing down timeline operations + # hiero.core.events.registerInterest( + # ("kSelectionChanged", "kTimeline"), selection_changed_timeline) # workfiles try: From c70c6d99110dce5f7d880cc0d285e2a03dfbf583 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 19 May 2022 19:48:12 +0200 Subject: [PATCH 318/398] Hiero: fixing one frame difference otio clip and media --- openpype/hosts/hiero/api/otio/hiero_export.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/hiero/api/otio/hiero_export.py b/openpype/hosts/hiero/api/otio/hiero_export.py index 64fb81aed4..46e1204324 100644 --- a/openpype/hosts/hiero/api/otio/hiero_export.py +++ b/openpype/hosts/hiero/api/otio/hiero_export.py @@ -151,7 +151,7 @@ def create_otio_reference(clip): padding = media_source.filenamePadding() file_head = media_source.filenameHead() is_sequence = not media_source.singleFile() - frame_duration = media_source.duration() - 1 + frame_duration = media_source.duration() fps = utils.get_rate(clip) or self.project_fps extension = os.path.splitext(path)[-1] @@ -277,7 +277,7 @@ def create_otio_clip(track_item): # flip if speed is in minus source_in = track_item.sourceIn() if speed > 0 else track_item.sourceOut() - duration = int(track_item.duration()) + duration = int(track_item.duration()) - 1 fps = utils.get_rate(track_item) or self.project_fps name = track_item.name() From cf6ea949acca41fcd04a0155134252c21816939a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 19 May 2022 19:48:40 +0200 Subject: [PATCH 319/398] global: hierarchy fps should be taken from instance.data --- openpype/plugins/publish/collect_hierarchy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/collect_hierarchy.py b/openpype/plugins/publish/collect_hierarchy.py index a96d444be6..8398a2815a 100644 --- a/openpype/plugins/publish/collect_hierarchy.py +++ b/openpype/plugins/publish/collect_hierarchy.py @@ -62,7 +62,7 @@ class CollectHierarchy(pyblish.api.ContextPlugin): "frameEnd": instance.data["frameEnd"], "clipIn": instance.data["clipIn"], "clipOut": instance.data["clipOut"], - 'fps': instance.context.data["fps"], + "fps": instance.data["fps"], "resolutionWidth": instance.data["resolutionWidth"], "resolutionHeight": instance.data["resolutionHeight"], "pixelAspect": instance.data["pixelAspect"] From 191444167c025f0f1bf20b6d15dbd480eff1243e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 19 May 2022 19:49:52 +0200 Subject: [PATCH 320/398] Hiero: moving order bit lower under core plugins --- openpype/hosts/hiero/plugins/publish/precollect_workfile.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/hiero/plugins/publish/precollect_workfile.py b/openpype/hosts/hiero/plugins/publish/precollect_workfile.py index b9f58c15f6..c9bfb86810 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_workfile.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_workfile.py @@ -16,7 +16,7 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin): """Inject the current working file into context""" label = "Precollect Workfile" - order = pyblish.api.CollectorOrder - 0.5 + order = pyblish.api.CollectorOrder - 0.491 def process(self, context): @@ -84,6 +84,7 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin): "colorspace": self.get_colorspace(project), "fps": fps } + self.log.debug("__ context_data: {}".format(pformat(context_data))) context.data.update(context_data) self.log.info("Creating instance: {}".format(instance)) From b55bf81d352790c46ba748a7781bc75cda88afb1 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 19 May 2022 19:50:50 +0200 Subject: [PATCH 321/398] Hiero: adding timeline selected to precollector --- .../hosts/hiero/plugins/publish/precollect_instances.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/hiero/plugins/publish/precollect_instances.py b/openpype/hosts/hiero/plugins/publish/precollect_instances.py index 46f0b2440e..1ef7e5f538 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_instances.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_instances.py @@ -19,9 +19,12 @@ class PrecollectInstances(pyblish.api.ContextPlugin): def process(self, context): self.otio_timeline = context.data["otioTimeline"] - + timeline_selection = phiero.get_timeline_selection() selected_timeline_items = phiero.get_track_items( - selected=True, check_tagged=True, check_enabled=True) + selection=timeline_selection, + check_tagged=True, + check_enabled=True + ) # only return enabled track items if not selected_timeline_items: From 68439301dc5f24372ccd69202bcea3a3040ce733 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 19 May 2022 19:51:21 +0200 Subject: [PATCH 322/398] Hiero: one frame diff fix, with code improvements --- .../hosts/hiero/plugins/publish/precollect_instances.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/hiero/plugins/publish/precollect_instances.py b/openpype/hosts/hiero/plugins/publish/precollect_instances.py index 1ef7e5f538..e54d050f0d 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_instances.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_instances.py @@ -295,9 +295,9 @@ class PrecollectInstances(pyblish.api.ContextPlugin): for otio_clip in self.otio_timeline.each_clip(): track_name = otio_clip.parent().name parent_range = otio_clip.range_in_parent() - if ti_track_name not in track_name: + if ti_track_name != track_name: continue - if otio_clip.name not in track_item.name(): + if otio_clip.name != track_item.name(): continue self.log.debug("__ parent_range: {}".format(parent_range)) self.log.debug("__ timeline_range: {}".format(timeline_range)) @@ -317,7 +317,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin): speed = track_item.playbackSpeed() timeline = phiero.get_current_sequence() frame_start = int(track_item.timelineIn()) - frame_duration = int(track_item.sourceDuration() / speed) + frame_duration = int((track_item.duration() - 1) / speed) fps = timeline.framerate().toFloat() return hiero_export.create_otio_time_range( From 2798469e36c3f915692cb0cfdea5e0cb5f16900a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 19 May 2022 19:54:17 +0200 Subject: [PATCH 323/398] Hiero: refactory of get_track_items with better validation --- openpype/hosts/hiero/api/lib.py | 140 ++++++++++++++++++++------------ 1 file changed, 89 insertions(+), 51 deletions(-) diff --git a/openpype/hosts/hiero/api/lib.py b/openpype/hosts/hiero/api/lib.py index 115a926d84..15142daa09 100644 --- a/openpype/hosts/hiero/api/lib.py +++ b/openpype/hosts/hiero/api/lib.py @@ -1,7 +1,10 @@ """ Host specific functions where host api is connected """ + +import contextlib import os +from pprint import pformat import re import sys import platform @@ -139,7 +142,7 @@ def get_current_track(sequence, name, audio=False): def get_track_items( - selected=False, + selection=False, sequence_name=None, track_item_name=None, track_name=None, @@ -150,7 +153,7 @@ def get_track_items( """Get all available current timeline track items. Attribute: - selected (bool)[optional]: return only selected items on timeline + selection (list)[optional]: list of selected track items sequence_name (str)[optional]: return only clips from input sequence track_item_name (str)[optional]: return only item with input name track_name (str)[optional]: return only items from track name @@ -162,32 +165,33 @@ def get_track_items( Return: list or hiero.core.TrackItem: list of track items or single track item """ - return_list = list() - track_items = list() + track_type = track_type or "video" + selection = selection or [] + return_list = [] # get selected track items or all in active sequence - if selected: - try: - selected_items = list(hiero.selection) - for item in selected_items: - if track_name and track_name in item.parent().name(): - # filter only items fitting input track name - track_items.append(item) - elif not track_name: - # or add all if no track_name was defined - track_items.append(item) - except AttributeError: - pass + if selection: + with contextlib.suppress(AttributeError): + for track_item in selection: + log.info("___ track_item: {}".format(track_item)) + # make sure only trackitems are selected + if not isinstance(track_item, hiero.core.TrackItem): + continue + + if _validate_all_atrributes( + track_item, + track_item_name, + track_name, + track_type, + check_enabled, + check_tagged + ): + log.info("___ valid trackitem: {}".format(track_item)) + return_list.append(track_item) - # check if any collected track items are - # `core.Hiero.Python.TrackItem` instance - if track_items: - any_track_item = track_items[0] - if not isinstance(any_track_item, hiero.core.TrackItem): - selected_items = [] # collect all available active sequence track items - if not track_items: + if not return_list: sequence = get_current_sequence(name=sequence_name) # get all available tracks from sequence tracks = list(sequence.audioTracks()) + list(sequence.videoTracks()) @@ -198,42 +202,76 @@ def get_track_items( if check_enabled and not track.isEnabled(): continue # and all items in track - for item in track.items(): - if check_tagged and not item.tags(): + for track_item in track.items(): + # make sure no subtrackitem is also track items + if not isinstance(track_item, hiero.core.TrackItem): continue - # check if track item is enabled - if check_enabled: - if not item.isEnabled(): - continue - if track_item_name: - if track_item_name in item.name(): - return item - # make sure only track items with correct track names are added - if track_name and track_name in track.name(): - # filter out only defined track_name items - track_items.append(item) - elif not track_name: - # or add all if no track_name is defined - track_items.append(item) + if not _validate_all_atrributes( + track_item, + track_item_name, + track_name, + track_type, + check_enabled, + check_tagged + ): + return_list.append(track_item) - # filter out only track items with defined track_type - for track_item in track_items: - if track_type and track_type == "video" and isinstance( + return return_list + + +def _validate_all_atrributes( + track_item, + track_item_name, + track_name, + track_type, + check_enabled, + check_tagged +): + def _validate_correct_name_track_item(): + if track_item_name and track_item_name in track_item.name(): + return True + elif not track_item_name: + return True + + def _validate_tagged_track_item(): + if check_tagged and track_item.tags(): + return True + elif not check_tagged: + return True + + def _validate_enabled_track_item(): + if check_enabled and track_item.isEnabled(): + return True + elif not check_enabled: + return True + + def _validate_parent_track_item(): + if track_name and track_name in track_item.parent().name(): + # filter only items fitting input track name + return True + elif not track_name: + # or add all if no track_name was defined + return True + + def _validate_type_track_item(): + if track_type == "video" and isinstance( track_item.parent(), hiero.core.VideoTrack): # only video track items are allowed - return_list.append(track_item) - elif track_type and track_type == "audio" and isinstance( + return True + elif track_type == "audio" and isinstance( track_item.parent(), hiero.core.AudioTrack): # only audio track items are allowed - return_list.append(track_item) - elif not track_type: - # add all if no track_type is defined - return_list.append(track_item) + return True - # return output list but make sure all items are TrackItems - return [_i for _i in return_list - if type(_i) == hiero.core.TrackItem] + # check if track item is enabled + return all([ + _validate_enabled_track_item(), + _validate_type_track_item(), + _validate_tagged_track_item(), + _validate_parent_track_item(), + _validate_correct_name_track_item() + ]) def get_track_item_pype_tag(track_item): From 13599837176687e49a57762414aad97e56e9125a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 19 May 2022 19:55:16 +0200 Subject: [PATCH 324/398] Hiero: fixing events --- openpype/hosts/hiero/api/lib.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/hiero/api/lib.py b/openpype/hosts/hiero/api/lib.py index 15142daa09..d3e6441705 100644 --- a/openpype/hosts/hiero/api/lib.py +++ b/openpype/hosts/hiero/api/lib.py @@ -1027,7 +1027,7 @@ def sync_clip_name_to_data_asset(track_items_list): print("asset was changed in clip: {}".format(ti_name)) -def check_inventory_versions(): +def check_inventory_versions(track_items=None): """ Actual version color idetifier of Loaded containers @@ -1038,14 +1038,15 @@ def check_inventory_versions(): """ from . import parse_container + track_item = track_items or get_track_items() # presets clip_color_last = "green" clip_color = "red" # get all track items from current timeline - for track_item in get_track_items(): + for track_item in track_item: container = parse_container(track_item) - + log.info("___> container: {}".format(pformat(container))) if container: # get representation from io representation = legacy_io.find_one({ @@ -1083,29 +1084,31 @@ def selection_changed_timeline(event): timeline_editor = event.sender selection = timeline_editor.selection() - selection = [ti for ti in selection - if isinstance(ti, hiero.core.TrackItem)] + track_items = get_track_items( + selection=selection, + track_type="video", + check_enabled=True, + check_locked=True, + check_tagged=True + ) # run checking function - sync_clip_name_to_data_asset(selection) - - # also mark old versions of loaded containers - check_inventory_versions() + sync_clip_name_to_data_asset(track_items) def before_project_save(event): track_items = get_track_items( - selected=False, track_type="video", check_enabled=True, check_locked=True, - check_tagged=True) + check_tagged=True + ) # run checking function sync_clip_name_to_data_asset(track_items) # also mark old versions of loaded containers - check_inventory_versions() + check_inventory_versions(track_items) def get_main_window(): From 733ad125b5798d01fe9dac9d3d24e6256d614386 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 19 May 2022 19:56:15 +0200 Subject: [PATCH 325/398] Hiero: one frame diff during loading --- openpype/hosts/hiero/api/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/hiero/api/plugin.py b/openpype/hosts/hiero/api/plugin.py index 54e66bf99a..35e9d54810 100644 --- a/openpype/hosts/hiero/api/plugin.py +++ b/openpype/hosts/hiero/api/plugin.py @@ -500,7 +500,7 @@ class ClipLoader: track_item.setSource(clip) track_item.setSourceIn(self.handle_start) track_item.setTimelineIn(self.timeline_in) - track_item.setSourceOut(self.media_duration - self.handle_end) + track_item.setSourceOut((self.media_duration + 1) - self.handle_end) track_item.setTimelineOut(self.timeline_out) track_item.setPlaybackSpeed(1) self.active_track.addTrackItem(track_item) From 91af28612e367d412e70a20ece03481ed4b976e9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 20 May 2022 11:21:16 +0200 Subject: [PATCH 326/398] Hiero: poping found clip --- openpype/hosts/hiero/plugins/load/load_clip.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/hiero/plugins/load/load_clip.py b/openpype/hosts/hiero/plugins/load/load_clip.py index da4326c8c1..a3365253b3 100644 --- a/openpype/hosts/hiero/plugins/load/load_clip.py +++ b/openpype/hosts/hiero/plugins/load/load_clip.py @@ -3,10 +3,6 @@ from openpype.pipeline import ( get_representation_path, ) import openpype.hosts.hiero.api as phiero -# from openpype.hosts.hiero.api import plugin, lib -# reload(lib) -# reload(plugin) -# reload(phiero) class LoadClip(phiero.SequenceLoader): @@ -106,7 +102,7 @@ class LoadClip(phiero.SequenceLoader): name = container['name'] namespace = container['namespace'] track_item = phiero.get_track_items( - track_item_name=namespace) + track_item_name=namespace).pop() version = legacy_io.find_one({ "type": "version", "_id": representation["parent"] @@ -157,7 +153,7 @@ class LoadClip(phiero.SequenceLoader): # load clip to timeline and get main variables namespace = container['namespace'] track_item = phiero.get_track_items( - track_item_name=namespace) + track_item_name=namespace).pop() track = track_item.parent() # remove track item from track From 053287bc616f1370602383f2aa4891fe80358599 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 19 May 2022 20:00:01 +0200 Subject: [PATCH 327/398] Hiero: better logging and improving code --- openpype/hosts/hiero/api/plugin.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/hiero/api/plugin.py b/openpype/hosts/hiero/api/plugin.py index 35e9d54810..174a25102f 100644 --- a/openpype/hosts/hiero/api/plugin.py +++ b/openpype/hosts/hiero/api/plugin.py @@ -1,4 +1,5 @@ import os +from pprint import pformat import re from copy import deepcopy @@ -400,7 +401,8 @@ class ClipLoader: # inject asset data to representation dict self._get_asset_data() - log.debug("__init__ self.data: `{}`".format(self.data)) + log.info("__init__ self.data: `{}`".format(pformat(self.data))) + log.info("__init__ options: `{}`".format(pformat(options))) # add active components to class if self.new_sequence: @@ -482,7 +484,9 @@ class ClipLoader: """ asset_name = self.context["representation"]["context"]["asset"] - self.data["assetData"] = openpype.get_asset(asset_name)["data"] + asset_doc = openpype.get_asset(asset_name) + log.debug("__ asset_doc: {}".format(pformat(asset_doc))) + self.data["assetData"] = asset_doc["data"] def _make_track_item(self, source_bin_item, audio=False): """ Create track item with """ @@ -527,7 +531,8 @@ class ClipLoader: if self.sequencial_load: last_track_item = lib.get_track_items( sequence_name=self.active_sequence.name(), - track_name=self.active_track.name()) + track_name=self.active_track.name() + ) if len(last_track_item) == 0: last_timeline_out = 0 else: @@ -541,6 +546,8 @@ class ClipLoader: self.timeline_in = int(self.data["assetData"]["clipIn"]) self.timeline_out = int(self.data["assetData"]["clipOut"]) + log.debug("__ self.timeline_in: {}".format(self.timeline_in)) + log.debug("__ self.timeline_out: {}".format(self.timeline_out)) # check if slate is included # either in version data families or by calculating frame diff slate_on = next( @@ -553,6 +560,7 @@ class ClipLoader: (self.timeline_out - self.timeline_in + 1) + self.handle_start + self.handle_end) < self.media_duration) + log.debug("__ slate_on: {}".format(slate_on)) # if slate is on then remove the slate frame from beginning if slate_on: self.media_duration -= 1 @@ -599,8 +607,8 @@ class Creator(LegacyCreator): rename_index = None def __init__(self, *args, **kwargs): - import openpype.hosts.hiero.api as phiero super(Creator, self).__init__(*args, **kwargs) + import openpype.hosts.hiero.api as phiero self.presets = openpype.get_current_project_settings()[ "hiero"]["create"].get(self.__class__.__name__, {}) @@ -609,7 +617,10 @@ class Creator(LegacyCreator): self.sequence = phiero.get_current_sequence() if (self.options or {}).get("useSelection"): - self.selected = phiero.get_track_items(selected=True) + timeline_selection = phiero.get_timeline_selection() + self.selected = phiero.get_track_items( + selection=timeline_selection + ) else: self.selected = phiero.get_track_items() @@ -716,6 +727,10 @@ class PublishClip: else: self.tag_data.update({"reviewTrack": None}) + log.debug("___ self.tag_data: {}".format( + pformat(self.tag_data) + )) + # create pype tag on track_item and add data lib.imprint(self.track_item, self.tag_data) From d27448e7e36d751f16b458bde328065706378092 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 19 May 2022 20:42:29 +0200 Subject: [PATCH 328/398] Hiero: handles and slate detection - handles should be int - slate only if in families on version --- openpype/hosts/hiero/api/plugin.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/hiero/api/plugin.py b/openpype/hosts/hiero/api/plugin.py index 174a25102f..e3ec6f3cf1 100644 --- a/openpype/hosts/hiero/api/plugin.py +++ b/openpype/hosts/hiero/api/plugin.py @@ -524,9 +524,12 @@ class ClipLoader: self.handle_start = self.data["versionData"].get("handleStart") self.handle_end = self.data["versionData"].get("handleEnd") if self.handle_start is None: - self.handle_start = int(self.data["assetData"]["handleStart"]) + self.handle_start = self.data["assetData"]["handleStart"] if self.handle_end is None: - self.handle_end = int(self.data["assetData"]["handleEnd"]) + self.handle_end = self.data["assetData"]["handleEnd"] + + self.handle_start = int(self.handle_start) + self.handle_end = int(self.handle_end) if self.sequencial_load: last_track_item = lib.get_track_items( @@ -556,9 +559,7 @@ class ClipLoader: if "slate" in f), # if nothing was found then use default None # so other bool could be used - None) or bool(int( - (self.timeline_out - self.timeline_in + 1) - + self.handle_start + self.handle_end) < self.media_duration) + None) log.debug("__ slate_on: {}".format(slate_on)) # if slate is on then remove the slate frame from beginning From cac79e69031f5c9a0e082ad0b4117ce3b5339a78 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 19 May 2022 20:42:53 +0200 Subject: [PATCH 329/398] Hiero: fix timewarp lookup to be list --- openpype/lib/editorial.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/lib/editorial.py b/openpype/lib/editorial.py index 1ee21deedc..5fe498bf6a 100644 --- a/openpype/lib/editorial.py +++ b/openpype/lib/editorial.py @@ -218,6 +218,7 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): "name": name } tw_node.update(metadata) + tw_node["lookup"] = list(lookup) # get first and last frame offsets offset_in += lookup[0] From ffe1bff6599e0de5c0e07b30b4878714bd26e91a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 19 May 2022 21:06:11 +0200 Subject: [PATCH 330/398] Hiero: fix by reversing validation --- openpype/hosts/hiero/api/lib.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/hiero/api/lib.py b/openpype/hosts/hiero/api/lib.py index d3e6441705..4dc8d26c2a 100644 --- a/openpype/hosts/hiero/api/lib.py +++ b/openpype/hosts/hiero/api/lib.py @@ -207,7 +207,7 @@ def get_track_items( if not isinstance(track_item, hiero.core.TrackItem): continue - if not _validate_all_atrributes( + if _validate_all_atrributes( track_item, track_item_name, track_name, @@ -1046,7 +1046,6 @@ def check_inventory_versions(track_items=None): # get all track items from current timeline for track_item in track_item: container = parse_container(track_item) - log.info("___> container: {}".format(pformat(container))) if container: # get representation from io representation = legacy_io.find_one({ From 15726cfef92bb465a794d7ccd3acb8cdc3e828bd Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 19 May 2022 21:06:42 +0200 Subject: [PATCH 331/398] Hiero: simplify code for slate detection --- openpype/hosts/hiero/api/plugin.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/hiero/api/plugin.py b/openpype/hosts/hiero/api/plugin.py index e3ec6f3cf1..8c61baa04b 100644 --- a/openpype/hosts/hiero/api/plugin.py +++ b/openpype/hosts/hiero/api/plugin.py @@ -551,17 +551,11 @@ class ClipLoader: log.debug("__ self.timeline_in: {}".format(self.timeline_in)) log.debug("__ self.timeline_out: {}".format(self.timeline_out)) - # check if slate is included - # either in version data families or by calculating frame diff - slate_on = next( - # check iterate if slate is in families - (f for f in self.context["version"]["data"]["families"] - if "slate" in f), - # if nothing was found then use default None - # so other bool could be used - None) + # check if slate is included + slate_on = "slate" in self.context["version"]["data"]["families"] log.debug("__ slate_on: {}".format(slate_on)) + # if slate is on then remove the slate frame from beginning if slate_on: self.media_duration -= 1 @@ -581,7 +575,7 @@ class ClipLoader: # there were some cases were hiero was not creating it source_bin_item = None for item in self.active_bin.items(): - if self.data["clip_name"] in item.name(): + if self.data["clip_name"] == item.name(): source_bin_item = item if not source_bin_item: log.warning("Problem with created Source clip: `{}`".format( From 38d4c3fa67effbc25847ee8706c7a1714cfa54a8 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 19 May 2022 21:17:14 +0200 Subject: [PATCH 332/398] Hiero: lib code refactory --- openpype/hosts/hiero/api/lib.py | 37 +++++++++++++-------------------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/openpype/hosts/hiero/api/lib.py b/openpype/hosts/hiero/api/lib.py index 4dc8d26c2a..758df43968 100644 --- a/openpype/hosts/hiero/api/lib.py +++ b/openpype/hosts/hiero/api/lib.py @@ -4,7 +4,6 @@ Host specific functions where host api is connected import contextlib import os -from pprint import pformat import re import sys import platform @@ -92,7 +91,7 @@ def get_current_sequence(name=None, new=False): if not sequence: # if nothing found create new with input name sequence = get_current_sequence(name, True) - elif not name and not new: + else: # if name is none and new is False then return current open sequence sequence = hiero.ui.activeSequence() @@ -189,7 +188,6 @@ def get_track_items( log.info("___ valid trackitem: {}".format(track_item)) return_list.append(track_item) - # collect all available active sequence track items if not return_list: sequence = get_current_sequence(name=sequence_name) @@ -311,7 +309,7 @@ def set_track_item_pype_tag(track_item, data=None): "editable": "0", "note": "OpenPype data container", "icon": "openpype_icon.png", - "metadata": {k: v for k, v in data.items()} + "metadata": dict(data.items()) } # get available pype tag if any _tag = get_track_item_pype_tag(track_item) @@ -369,7 +367,7 @@ def get_track_item_pype_data(track_item): log.warning(msg) value = v - data.update({key: value}) + data[key] = value return data @@ -938,32 +936,32 @@ def apply_colorspace_clips(): def is_overlapping(ti_test, ti_original, strict=False): - covering_exp = bool( + covering_exp = ( (ti_test.timelineIn() <= ti_original.timelineIn()) and (ti_test.timelineOut() >= ti_original.timelineOut()) ) - inside_exp = bool( + inside_exp = ( (ti_test.timelineIn() >= ti_original.timelineIn()) and (ti_test.timelineOut() <= ti_original.timelineOut()) ) - overlaying_right_exp = bool( + overlaying_right_exp = ( (ti_test.timelineIn() < ti_original.timelineOut()) and (ti_test.timelineOut() >= ti_original.timelineOut()) ) - overlaying_left_exp = bool( + overlaying_left_exp = ( (ti_test.timelineOut() > ti_original.timelineIn()) and (ti_test.timelineIn() <= ti_original.timelineIn()) ) - if not strict: + if strict: + return covering_exp + else: return any(( covering_exp, inside_exp, overlaying_right_exp, overlaying_left_exp )) - else: - return covering_exp def get_sequence_pattern_and_padding(file): @@ -981,17 +979,12 @@ def get_sequence_pattern_and_padding(file): """ foundall = re.findall( r"(#+)|(%\d+d)|(?<=[^a-zA-Z0-9])(\d+)(?=\.\w+$)", file) - if foundall: - found = sorted(list(set(foundall[0])))[-1] - - if "%" in found: - padding = int(re.findall(r"\d+", found)[-1]) - else: - padding = len(found) - - return found, padding - else: + if not foundall: return None, None + found = sorted(list(set(foundall[0])))[-1] + + padding = int(re.findall(r"\d+", found)[-1]) if "%" in found else len(found) + return found, padding def sync_clip_name_to_data_asset(track_items_list): From 90948931b08dba9e4698ffc45f6a3be307996853 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 19 May 2022 19:49:08 +0200 Subject: [PATCH 333/398] Hiero: removing old code From b9278efdfd0233208e33b0e02036188ead29fd64 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 20 May 2022 12:30:00 +0200 Subject: [PATCH 334/398] Added jpg extension to filter for Maya Image plane loader --- openpype/hosts/maya/plugins/load/load_image_plane.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/load/load_image_plane.py b/openpype/hosts/maya/plugins/load/load_image_plane.py index b67c2cb209..5e44917f28 100644 --- a/openpype/hosts/maya/plugins/load/load_image_plane.py +++ b/openpype/hosts/maya/plugins/load/load_image_plane.py @@ -83,7 +83,7 @@ class ImagePlaneLoader(load.LoaderPlugin): families = ["image", "plate", "render"] label = "Load imagePlane" - representations = ["mov", "exr", "preview", "png"] + representations = ["mov", "exr", "preview", "png", "jpg"] icon = "image" color = "orange" From f02f11750ca394adb60ace7e9ab9164d778bd7e9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 20 May 2022 12:33:30 +0200 Subject: [PATCH 335/398] reversed block signals --- .../project_manager/project_manager/model.py | 4 --- .../project_manager/project_manager/view.py | 28 ------------------- 2 files changed, 32 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index b7cb0ec9ed..871704e13c 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -264,8 +264,6 @@ class HierarchyModel(QtCore.QAbstractItemModel): if not project_doc: return - self.blockSignals(True) - # Create project item project_item = ProjectItem(project_doc) self.add_item(project_item) @@ -379,8 +377,6 @@ class HierarchyModel(QtCore.QAbstractItemModel): self.add_items(task_items, asset_item) - self.blockSignals(False) - # Emit that project was successfully changed self.project_changed.emit() diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index 2c2a17d712..25174232bc 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -213,34 +213,6 @@ class HierarchyView(QtWidgets.QTreeView): index = self._source_model.index_for_item(project_item) self.expand(index) - self._open_persistent_editors_on_project_refresh() - - def _open_persistent_editors_on_project_refresh(self): - model = self._source_model - parent_index = QtCore.QModelIndex() - persistent_queue = collections.deque() - persistent_queue.append((parent_index, model.rowCount())) - while persistent_queue: - item = persistent_queue.popleft() - parent_index, rows = item - if not rows: - continue - - for row in range(rows): - row_index = model.index(row, 0, parent_index) - persistent_queue.append( - (row_index, model.rowCount(row_index)) - ) - for key, column in self._column_key_to_index.items(): - if key not in self.persistent_columns: - continue - col_index = model.index(row, column, parent_index) - if bool( - model.flags(col_index) - & QtCore.Qt.ItemIsEditable - ): - self.openPersistentEditor(col_index) - def _on_rows_moved(self, index): parent_index = index.parent() if not self.isExpanded(parent_index): From 61be6857603c8061283bcbed938f07c5bd525eee Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 20 May 2022 12:35:46 +0200 Subject: [PATCH 336/398] replaced persistent editors with added ability of any edit trigger --- .../tools/project_manager/project_manager/view.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index 25174232bc..6f5b9dc3f7 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -139,6 +139,7 @@ class HierarchyView(QtWidgets.QTreeView): self.setAlternatingRowColors(True) self.setSelectionMode(HierarchyView.ExtendedSelection) self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + self.setEditTriggers(HierarchyView.AllEditTriggers) column_delegates = {} column_key_to_index = {} @@ -301,16 +302,6 @@ class HierarchyView(QtWidgets.QTreeView): def rowsInserted(self, parent_index, start, end): super(HierarchyView, self).rowsInserted(parent_index, start, end) - for row in range(start, end + 1): - for key, column in self._column_key_to_index.items(): - if key not in self.persistent_columns: - continue - col_index = self._source_model.index(row, column, parent_index) - if bool( - self._source_model.flags(col_index) - & QtCore.Qt.ItemIsEditable - ): - self.openPersistentEditor(col_index) # Expand parent on insert if not self.isExpanded(parent_index): From 7c3fdcc633f80b73084b0fbf4ba6f51d4c8c8a71 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 20 May 2022 12:59:38 +0200 Subject: [PATCH 337/398] Hiero: tag should use deepcopy for metadata --- openpype/hosts/hiero/api/lib.py | 7 ++++--- openpype/hosts/hiero/api/tags.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/hiero/api/lib.py b/openpype/hosts/hiero/api/lib.py index 758df43968..5b2f6c814d 100644 --- a/openpype/hosts/hiero/api/lib.py +++ b/openpype/hosts/hiero/api/lib.py @@ -3,6 +3,7 @@ Host specific functions where host api is connected """ import contextlib +from copy import deepcopy import os import re import sys @@ -288,7 +289,7 @@ def get_track_item_pype_tag(track_item): return None for tag in _tags: # return only correct tag defined by global name - if tag.name() in self.pype_tag_name: + if tag.name() == self.pype_tag_name: return tag @@ -344,9 +345,9 @@ def get_track_item_pype_data(track_item): return None # get tag metadata attribute - tag_data = tag.metadata() + tag_data = deepcopy(dict(tag.metadata())) # convert tag metadata to normal keys names and values to correct types - for k, v in dict(tag_data).items(): + for k, v in tag_data.items(): key = k.replace("tag.", "") try: diff --git a/openpype/hosts/hiero/api/tags.py b/openpype/hosts/hiero/api/tags.py index 8877b92b9d..8c6ff2a77b 100644 --- a/openpype/hosts/hiero/api/tags.py +++ b/openpype/hosts/hiero/api/tags.py @@ -86,7 +86,7 @@ def update_tag(tag, data): # due to hiero bug we have to make sure keys which are not existent in # data are cleared of value by `None` - for _mk in mtd.keys(): + for _mk in mtd.dict().keys(): if _mk.replace("tag.", "") not in data_mtd.keys(): mtd.setValue(_mk, str(None)) From be9f6cf5c5ddd7e833f5dd5c31e690706767e493 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 20 May 2022 15:08:56 +0200 Subject: [PATCH 338/398] Hiero: frame difference issue --- openpype/hosts/hiero/api/otio/hiero_export.py | 2 +- openpype/hosts/hiero/api/plugin.py | 2 +- openpype/lib/editorial.py | 2 +- openpype/plugins/publish/collect_otio_frame_ranges.py | 8 ++++---- openpype/plugins/publish/collect_otio_subset_resources.py | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/hiero/api/otio/hiero_export.py b/openpype/hosts/hiero/api/otio/hiero_export.py index 46e1204324..1e4088d9c0 100644 --- a/openpype/hosts/hiero/api/otio/hiero_export.py +++ b/openpype/hosts/hiero/api/otio/hiero_export.py @@ -277,7 +277,7 @@ def create_otio_clip(track_item): # flip if speed is in minus source_in = track_item.sourceIn() if speed > 0 else track_item.sourceOut() - duration = int(track_item.duration()) - 1 + duration = int(track_item.duration()) fps = utils.get_rate(track_item) or self.project_fps name = track_item.name() diff --git a/openpype/hosts/hiero/api/plugin.py b/openpype/hosts/hiero/api/plugin.py index 8c61baa04b..add416d04e 100644 --- a/openpype/hosts/hiero/api/plugin.py +++ b/openpype/hosts/hiero/api/plugin.py @@ -504,7 +504,7 @@ class ClipLoader: track_item.setSource(clip) track_item.setSourceIn(self.handle_start) track_item.setTimelineIn(self.timeline_in) - track_item.setSourceOut((self.media_duration + 1) - self.handle_end) + track_item.setSourceOut((self.media_duration) - self.handle_end) track_item.setTimelineOut(self.timeline_out) track_item.setPlaybackSpeed(1) self.active_track.addTrackItem(track_item) diff --git a/openpype/lib/editorial.py b/openpype/lib/editorial.py index 5fe498bf6a..2c877b9d0d 100644 --- a/openpype/lib/editorial.py +++ b/openpype/lib/editorial.py @@ -255,7 +255,7 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): media_in + source_in + offset_in) media_out_trimmed = ( media_in + source_in + ( - (source_range.duration.value * abs( + ((source_range.duration.value - 1) * abs( time_scalar)) + offset_out)) # calculate available handles diff --git a/openpype/plugins/publish/collect_otio_frame_ranges.py b/openpype/plugins/publish/collect_otio_frame_ranges.py index ee7b7957ad..8eaf9d6f29 100644 --- a/openpype/plugins/publish/collect_otio_frame_ranges.py +++ b/openpype/plugins/publish/collect_otio_frame_ranges.py @@ -55,13 +55,13 @@ class CollectOtioFrameRanges(pyblish.api.InstancePlugin): "frameStart": frame_start, "frameEnd": frame_end, "clipIn": tl_start, - "clipOut": tl_end, + "clipOut": tl_end - 1, "clipInH": tl_start_h, - "clipOutH": tl_end_h, + "clipOutH": tl_end_h - 1, "sourceStart": src_starting_from + src_start, - "sourceEnd": src_starting_from + src_end, + "sourceEnd": src_starting_from + src_end - 1, "sourceStartH": src_starting_from + src_start_h, - "sourceEndH": src_starting_from + src_end_h, + "sourceEndH": src_starting_from + src_end_h - 1, } instance.data.update(data) self.log.debug( diff --git a/openpype/plugins/publish/collect_otio_subset_resources.py b/openpype/plugins/publish/collect_otio_subset_resources.py index 7c11462ef0..b89a076a44 100644 --- a/openpype/plugins/publish/collect_otio_subset_resources.py +++ b/openpype/plugins/publish/collect_otio_subset_resources.py @@ -66,7 +66,7 @@ class CollectOtioSubsetResources(pyblish.api.InstancePlugin): # create trimmed otio time range trimmed_media_range_h = editorial.range_from_frames( - a_frame_start_h, (a_frame_end_h - a_frame_start_h + 1), + a_frame_start_h, (a_frame_end_h - a_frame_start_h) + 1, media_fps ) trimmed_duration = trimmed_media_range_h.duration.value From 4c4c824d8084247c2b0ed550579849fe9b503247 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 20 May 2022 15:22:23 +0200 Subject: [PATCH 339/398] :recycle: refactored work with attributes --- .../maya/plugins/publish/collect_look.py | 153 +++++++++--------- 1 file changed, 81 insertions(+), 72 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_look.py b/openpype/hosts/maya/plugins/publish/collect_look.py index 123e2637cb..fb2ce04cad 100644 --- a/openpype/hosts/maya/plugins/publish/collect_look.py +++ b/openpype/hosts/maya/plugins/publish/collect_look.py @@ -23,33 +23,43 @@ RENDERER_NODE_TYPES = [ "RedshiftMeshParameters" ] SHAPE_ATTRS = set(SHAPE_ATTRS) -DEFAULT_FILE_NODES = frozenset( - ["file"] -) -ARNOLD_FILE_NODES = frozenset( - ["aiImage"] -) -REDSHIFT_FILE_NODES = frozenset( - ["RedshiftNormalMap"] -) -RENDERMAN_FILE_NODES = frozenset( - [ - "PxrBump", - "PxrNormalMap", - # PxrMultiTexture (need to handle multiple filename0 attrs) - "PxrPtexture", - "PxrTexture", - ] -) -NODES_WITH_FILE = frozenset().union( - DEFAULT_FILE_NODES -) -NODES_WITH_FILENAME = frozenset().union( - ARNOLD_FILE_NODES, RENDERMAN_FILE_NODES -) -NODES_WITH_TEX = frozenset().union( - REDSHIFT_FILE_NODES -) + + +def get_pxr_multitexture_file_attrs(node): + attrs = [] + for i in range(9): + if cmds.attributeQuery("filename{}".format(i), node): + file = cmds.getAttr("{}.filename{}".format(node, i)) + if file: + attrs.append("filename{}".format(i)) + return attrs + + +FILE_NODES = { + "file": "fileTextureName", + + "aiImage": "filename", + + "RedshiftNormalMap": "text0", + + "PxrBump": "filename", + "PxrNormalMap": "filename", + "PxrMultiTexture": get_pxr_multitexture_file_attrs, + "PxrPtexture": "filename", + "PxrTexture": "filename" +} + + +def get_attributes(dictionary, attr): + # type: (dict, str) -> list + if callable(dictionary[attr]): + val = dictionary[attr]() + else: + val = dictionary.get(attr, []) + + if not isinstance(val, list): + return [val] + return val def get_look_attrs(node): @@ -77,15 +87,13 @@ def get_look_attrs(node): if cmds.objectType(node, isAType="shape"): attrs = cmds.listAttr(node, changedSinceFileOpen=True) or [] for attr in attrs: - if attr in SHAPE_ATTRS: + if attr in SHAPE_ATTRS or \ + attr not in SHAPE_ATTRS and attr.startswith('ai'): result.append(attr) - elif attr.startswith('ai'): - result.append(attr) - return result -def node_uses_image_sequence(node): +def node_uses_image_sequence(node, node_path): # type: (str) -> bool """Return whether file node uses an image sequence or single image. @@ -101,8 +109,6 @@ def node_uses_image_sequence(node): """ # useFrameExtension indicates an explicit image sequence - node_path = get_file_node_path(node).lower() - # The following tokens imply a sequence patterns = ["", "", "", "u_v", ""] @@ -169,14 +175,15 @@ def seq_to_glob(path): return path -def get_file_node_path(node): +def get_file_node_paths(node): + # type: (str) -> list """Get the file path used by a Maya file node. Args: node (str): Name of the Maya file node Returns: - str: the file path in use + list: the file paths in use """ # if the path appears to be sequence, use computedFileTextureNamePattern, @@ -195,15 +202,19 @@ def get_file_node_path(node): ""] lower = texture_pattern.lower() if any(pattern in lower for pattern in patterns): - return texture_pattern + return [texture_pattern] - if cmds.nodeType(node) in NODES_WITH_FILENAME: - return cmds.getAttr('{0}.filename'.format(node)) - if cmds.nodeType(node) == 'RedshiftNormalMap': - return cmds.getAttr('{}.tex0'.format(node)) + try: + file_attributes = get_attributes(FILE_NODES, cmds.nodeType(node)) + except AttributeError: + file_attributes = "fileTextureName" - # otherwise use fileTextureName - return cmds.getAttr('{0}.fileTextureName'.format(node)) + files = [] + for file_attr in file_attributes: + if cmds.attributeQuery(file_attr, node=node, exists=True): + files.append(cmds.getAttr("{}.{}".format(node, file_attr))) + + return files def get_file_node_files(node): @@ -217,16 +228,21 @@ def get_file_node_files(node): list: List of full file paths. """ + paths = get_file_node_paths(node) + sequences = [] + replaces = [] + for index, path in enumerate(paths): + if node_uses_image_sequence(node, path): + glob_pattern = seq_to_glob(path) + sequences.extend(glob.glob(glob_pattern)) + replaces.append(index) - path = get_file_node_path(node) - path = cmds.workspace(expandName=path) - if node_uses_image_sequence(node): - glob_pattern = seq_to_glob(path) - return glob.glob(glob_pattern) - elif os.path.exists(path): - return [path] - else: - return [] + for index in replaces: + paths.pop(index) + + paths.extend(sequences) + + return [p for p in paths if os.path.exists(p)] class CollectLook(pyblish.api.InstancePlugin): @@ -270,13 +286,13 @@ class CollectLook(pyblish.api.InstancePlugin): "for %s" % instance.data['name']) # Discover related object sets - self.log.info("Gathering sets..") + self.log.info("Gathering sets ...") sets = self.collect_sets(instance) # Lookup set (optimization) instance_lookup = set(cmds.ls(instance, long=True)) - self.log.info("Gathering set relations..") + self.log.info("Gathering set relations ...") # Ensure iteration happen in a list so we can remove keys from the # dict within the loop @@ -409,10 +425,7 @@ class CollectLook(pyblish.api.InstancePlugin): or [] ) - all_supported_nodes = set().union( - DEFAULT_FILE_NODES, ARNOLD_FILE_NODES, REDSHIFT_FILE_NODES, - RENDERMAN_FILE_NODES - ) + all_supported_nodes = FILE_NODES.keys() files = [] for node_type in all_supported_nodes: files.extend(cmds.ls(history, type=node_type, long=True)) @@ -550,27 +563,23 @@ class CollectLook(pyblish.api.InstancePlugin): dict """ self.log.debug("processing: {}".format(node)) - all_supported_nodes = set().union( - DEFAULT_FILE_NODES, ARNOLD_FILE_NODES, REDSHIFT_FILE_NODES, - RENDERMAN_FILE_NODES - ) + all_supported_nodes = FILE_NODES.keys() if cmds.nodeType(node) not in all_supported_nodes: self.log.error( "Unsupported file node: {}".format(cmds.nodeType(node))) raise AssertionError("Unsupported file node") self.log.debug(" - got {}".format(cmds.nodeType(node))) - if cmds.nodeType(node) in NODES_WITH_FILE: - attribute = "{}.fileTextureName".format(node) - computed_attribute = "{}.computedFileTextureNamePattern".format(node) - elif cmds.nodeType(node) in NODES_WITH_FILENAME: - attribute = "{}.filename".format(node) - computed_attribute = attribute - elif cmds.nodeType(node) in NODES_WITH_TEX: - attribute = "{}.tex0".format(node) - computed_attribute = attribute - source = cmds.getAttr(attribute) + attribute = FILE_NODES.get(cmds.nodeType(node)) + source = cmds.getAttr("{}.{}".format( + node, + attribute + )) + computed_attribute = "{}.{}".format(node, attribute) + if attribute == "fileTextureName": + computed_attribute = node + ".computedFileTextureNamePattern" + self.log.info(" - file source: {}".format(source)) color_space_attr = "{}.colorSpace".format(node) try: From f35079aaed7eb74ccca74645c800d20d8f225801 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 20 May 2022 16:18:39 +0200 Subject: [PATCH 340/398] global: remove exclude family for `clip` --- openpype/plugins/publish/integrate_new.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index bf13a4050e..353314fff2 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -113,7 +113,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "usdOverride", "simpleUnrealTexture" ] - exclude_families = ["clip", "render.farm"] + exclude_families = ["render.farm"] db_representation_context_keys = [ "project", "asset", "task", "subset", "version", "representation", "family", "hierarchy", "task", "username" From 87182f5a3e66cbae791f49c2f8cc0692f8dff3bf Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 20 May 2022 17:00:48 +0200 Subject: [PATCH 341/398] global: otio duration is one frame longer --- openpype/plugins/publish/extract_otio_trimming_video.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/extract_otio_trimming_video.py b/openpype/plugins/publish/extract_otio_trimming_video.py index 30b57e2c69..e8e2994f36 100644 --- a/openpype/plugins/publish/extract_otio_trimming_video.py +++ b/openpype/plugins/publish/extract_otio_trimming_video.py @@ -80,7 +80,7 @@ class ExtractOTIOTrimmingVideo(openpype.api.Extractor): video_path = input_file_path frame_start = otio_range.start_time.value input_fps = otio_range.start_time.rate - frame_duration = (otio_range.duration.value + 1) + frame_duration = otio_range.duration.value - 1 sec_start = openpype.lib.frames_to_secons(frame_start, input_fps) sec_duration = openpype.lib.frames_to_secons(frame_duration, input_fps) From 939f2187291ccc6c69767ffa74472d054c3dde2f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 20 May 2022 20:44:57 +0200 Subject: [PATCH 342/398] Allow to paste Tasks into multiple assets at the same time --- .../project_manager/project_manager/model.py | 26 ++++++++++++++----- .../project_manager/project_manager/view.py | 4 +-- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index b7cb0ec9ed..2721297578 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -1476,12 +1476,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): mimedata.setData("application/copy_task", encoded_data) return mimedata - def paste_mime_data(self, index, mime_data): - if not index.isValid(): - return - - item_id = index.data(IDENTIFIER_ROLE) - item = self._items_by_id[item_id] + def paste_mime_data(self, item, mime_data): if not isinstance(item, (AssetItem, TaskItem)): return @@ -1515,6 +1510,25 @@ class HierarchyModel(QtCore.QAbstractItemModel): task_item = TaskItem(task_data, True) self.add_item(task_item, parent) + def paste(self, indices, mime_data): + + # Get the selected Assets uniquely + items = set() + for index in indices: + if not index.isValid(): + return + item_id = index.data(IDENTIFIER_ROLE) + item = self._items_by_id[item_id] + + # Do not copy into the Task Item so get parent Asset instead + if isinstance(item, TaskItem): + item = item.parent() + + items.add(item) + + for item in items: + self.paste_mime_data(item, mime_data) + class BaseItem: """Base item for HierarchyModel. diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index 25174232bc..4279cb7468 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -376,9 +376,9 @@ class HierarchyView(QtWidgets.QTreeView): self._show_message(str(exc)) def _paste_items(self): - index = self.currentIndex() mime_data = QtWidgets.QApplication.clipboard().mimeData() - self._source_model.paste_mime_data(index, mime_data) + rows = self.selectionModel().selectedRows() + self._source_model.paste(rows, mime_data) def _delete_items(self, indexes=None): if indexes is None: From 413917672a0486643f424bddf77cdcee881c1746 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Sat, 21 May 2022 03:41:16 +0000 Subject: [PATCH 343/398] [Automated] Bump version --- CHANGELOG.md | 13 ++++++++----- openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6546ab6139..b8cec29df7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [3.10.0-nightly.3](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.10.0-nightly.4](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.9.8...HEAD) @@ -14,13 +14,12 @@ **🚀 Enhancements** +- Project manager: Sped up project load [\#3216](https://github.com/pypeclub/OpenPype/pull/3216) - Maya: added clean\_import option to Import loader [\#3181](https://github.com/pypeclub/OpenPype/pull/3181) - Maya: add maya 2023 to default applications [\#3167](https://github.com/pypeclub/OpenPype/pull/3167) -- Compressed bgeo publishing in SAP and Houdini loader [\#3153](https://github.com/pypeclub/OpenPype/pull/3153) - General: Add 'dataclasses' to required python modules [\#3149](https://github.com/pypeclub/OpenPype/pull/3149) - Hooks: Tweak logging grammar [\#3147](https://github.com/pypeclub/OpenPype/pull/3147) - Nuke: settings for reformat node in CreateWriteRender node [\#3143](https://github.com/pypeclub/OpenPype/pull/3143) -- Houdini: Add loader for alembic through Alembic Archive node [\#3140](https://github.com/pypeclub/OpenPype/pull/3140) - Publisher: UI Modifications and fixes [\#3139](https://github.com/pypeclub/OpenPype/pull/3139) - General: Simplified OP modules/addons import [\#3137](https://github.com/pypeclub/OpenPype/pull/3137) - Terminal: Tweak coloring of TrayModuleManager logging enabled states [\#3133](https://github.com/pypeclub/OpenPype/pull/3133) @@ -34,6 +33,8 @@ **🐛 Bug fixes** +- Deadline: instance data overwrite fix [\#3214](https://github.com/pypeclub/OpenPype/pull/3214) +- Ftrack: Push hierarchical attributes action works [\#3210](https://github.com/pypeclub/OpenPype/pull/3210) - Standalone Publisher: Always create new representation for thumbnail [\#3203](https://github.com/pypeclub/OpenPype/pull/3203) - Photoshop: skip collector when automatic testing [\#3202](https://github.com/pypeclub/OpenPype/pull/3202) - Nuke: render/workfile version sync doesn't work on farm [\#3185](https://github.com/pypeclub/OpenPype/pull/3185) @@ -47,6 +48,7 @@ - Ftrack: Action delete old versions formatting works [\#3152](https://github.com/pypeclub/OpenPype/pull/3152) - Deadline: fix the output directory [\#3144](https://github.com/pypeclub/OpenPype/pull/3144) - General: New Session schema [\#3141](https://github.com/pypeclub/OpenPype/pull/3141) +- General: Missing version on headless mode crash properly [\#3136](https://github.com/pypeclub/OpenPype/pull/3136) - TVPaint: Composite layers in reversed order [\#3135](https://github.com/pypeclub/OpenPype/pull/3135) - Nuke: fixing default settings for workfile builder loaders [\#3120](https://github.com/pypeclub/OpenPype/pull/3120) - Nuke: fix anatomy imageio regex default [\#3119](https://github.com/pypeclub/OpenPype/pull/3119) @@ -59,6 +61,7 @@ **Merged pull requests:** +- Maya: added jpg to filter for Image Plane Loader [\#3223](https://github.com/pypeclub/OpenPype/pull/3223) - Webpublisher: replace space by underscore in subset names [\#3160](https://github.com/pypeclub/OpenPype/pull/3160) - StandalonePublisher: removed Extract Background plugins [\#3093](https://github.com/pypeclub/OpenPype/pull/3093) @@ -95,7 +98,9 @@ **🚀 Enhancements** - Deadline output dir issue to 3.9x [\#3155](https://github.com/pypeclub/OpenPype/pull/3155) +- Compressed bgeo publishing in SAP and Houdini loader [\#3153](https://github.com/pypeclub/OpenPype/pull/3153) - nuke: removing redundant code from startup [\#3142](https://github.com/pypeclub/OpenPype/pull/3142) +- Houdini: Add loader for alembic through Alembic Archive node [\#3140](https://github.com/pypeclub/OpenPype/pull/3140) **🐛 Bug fixes** @@ -120,7 +125,6 @@ **🐛 Bug fixes** -- General: Missing version on headless mode crash properly [\#3136](https://github.com/pypeclub/OpenPype/pull/3136) - TVPaint: Composite layers in reversed order [\#3134](https://github.com/pypeclub/OpenPype/pull/3134) - General: Python 3 compatibility in queries [\#3111](https://github.com/pypeclub/OpenPype/pull/3111) @@ -137,7 +141,6 @@ - Ftrack: Update Create Folders action [\#3092](https://github.com/pypeclub/OpenPype/pull/3092) - General: Extract review sequence is not converted with same names [\#3075](https://github.com/pypeclub/OpenPype/pull/3075) -- Webpublisher: Use variant value [\#3072](https://github.com/pypeclub/OpenPype/pull/3072) ## [3.9.4](https://github.com/pypeclub/OpenPype/tree/3.9.4) (2022-04-15) diff --git a/openpype/version.py b/openpype/version.py index 1db666efec..1cc854cfd1 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.10.0-nightly.3" +__version__ = "3.10.0-nightly.4" diff --git a/pyproject.toml b/pyproject.toml index 4b7972c227..a2614b24b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.10.0-nightly.3" # OpenPype +version = "3.10.0-nightly.4" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 8177a44b5d0261f7bdd683a190796ccbd52d57f0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 23 May 2022 10:57:56 +0200 Subject: [PATCH 344/398] set empty mime data on failed copy --- openpype/tools/project_manager/project_manager/view.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index 4279cb7468..7d9f1a7323 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -365,14 +365,18 @@ class HierarchyView(QtWidgets.QTreeView): event.accept() def _copy_items(self, indexes=None): + clipboard = QtWidgets.QApplication.clipboard() try: if indexes is None: indexes = self.selectedIndexes() mime_data = self._source_model.copy_mime_data(indexes) - QtWidgets.QApplication.clipboard().setMimeData(mime_data) + clipboard.setMimeData(mime_data) self._show_message("Tasks copied") except ValueError as exc: + # Change clipboard to contain empty data + empty_mime_data = QtCore.QMimeData() + clipboard.setMimeData(empty_mime_data) self._show_message(str(exc)) def _paste_items(self): From 264f3bb9278436a3a07f3d0460667d48ba72342f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 23 May 2022 11:49:09 +0200 Subject: [PATCH 345/398] loop over 100 groups instead of only 12 --- openpype/hosts/tvpaint/api/lib.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/tvpaint/api/lib.py b/openpype/hosts/tvpaint/api/lib.py index 0c63dbe5be..b81685dbd7 100644 --- a/openpype/hosts/tvpaint/api/lib.py +++ b/openpype/hosts/tvpaint/api/lib.py @@ -165,12 +165,12 @@ def parse_group_data(data): if not group_raw: continue - parts = group_raw.split(" ") + parts = group_raw.split("|") # Check for length and concatenate 2 last items until length match # - this happens if name contain spaces while len(parts) > 6: last_item = parts.pop(-1) - parts[-1] = " ".join([parts[-1], last_item]) + parts[-1] = "|".join([parts[-1], last_item]) clip_id, group_id, red, green, blue, name = parts group = { @@ -201,11 +201,16 @@ def get_groups_data(communicator=None): george_script_lines = ( # Variable containing full path to output file "output_path = \"{}\"".format(output_filepath), - "loop = 1", - "FOR idx = 1 TO 12", + "empty = 0", + # Loop over 100 groups + "FOR idx = 1 TO 100", + # Receive information about groups "tv_layercolor \"getcolor\" 0 idx", - "tv_writetextfile \"strict\" \"append\" '\"'output_path'\"' result", - "END" + "PARSE result clip_id group_index c_red c_green c_blue group_name", + # Create and add line to output file + "line = group_index'|'c_red'|'c_green'|'c_blue'|'group_name", + "tv_writetextfile \"strict\" \"append\" '\"'output_path'\"' line", + "END", ) george_script = "\n".join(george_script_lines) execute_george_through_file(george_script, communicator) From 8079654f38dfa3644357dbcc6ea44554eef48ade Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 23 May 2022 12:33:40 +0200 Subject: [PATCH 346/398] Code cosmetics from code review Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/tools/project_manager/project_manager/model.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 2721297578..a69ef4a232 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -1476,7 +1476,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): mimedata.setData("application/copy_task", encoded_data) return mimedata - def paste_mime_data(self, item, mime_data): + def _paste_mime_data(self, item, mime_data): if not isinstance(item, (AssetItem, TaskItem)): return @@ -1510,11 +1510,11 @@ class HierarchyModel(QtCore.QAbstractItemModel): task_item = TaskItem(task_data, True) self.add_item(task_item, parent) - def paste(self, indices, mime_data): + def paste(self, indexes, mime_data): # Get the selected Assets uniquely items = set() - for index in indices: + for index in indexes: if not index.isValid(): return item_id = index.data(IDENTIFIER_ROLE) @@ -1527,7 +1527,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): items.add(item) for item in items: - self.paste_mime_data(item, mime_data) + self._paste_mime_data(item, mime_data) class BaseItem: From de2ea81928d15a2536aca8ba8ab12c50248f40ee Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 23 May 2022 13:34:24 +0200 Subject: [PATCH 347/398] fix missing clip id --- openpype/hosts/tvpaint/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/tvpaint/api/lib.py b/openpype/hosts/tvpaint/api/lib.py index b81685dbd7..a341f48859 100644 --- a/openpype/hosts/tvpaint/api/lib.py +++ b/openpype/hosts/tvpaint/api/lib.py @@ -208,7 +208,7 @@ def get_groups_data(communicator=None): "tv_layercolor \"getcolor\" 0 idx", "PARSE result clip_id group_index c_red c_green c_blue group_name", # Create and add line to output file - "line = group_index'|'c_red'|'c_green'|'c_blue'|'group_name", + "line = clip_id'|'group_index'|'c_red'|'c_green'|'c_blue'|'group_name", "tv_writetextfile \"strict\" \"append\" '\"'output_path'\"' line", "END", ) From 790a7468d7a4ece5d767b2b016cf71183ebbd567 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 23 May 2022 16:48:03 +0200 Subject: [PATCH 348/398] remove usage of unused attribute _items_with_color_by_id --- openpype/tools/utils/assets_widget.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/tools/utils/assets_widget.py b/openpype/tools/utils/assets_widget.py index d1df1193d2..82bdcd63a2 100644 --- a/openpype/tools/utils/assets_widget.py +++ b/openpype/tools/utils/assets_widget.py @@ -494,8 +494,6 @@ class AssetModel(QtGui.QStandardItemModel): # Remove cache of removed items for asset_id in removed_asset_ids: self._items_by_asset_id.pop(asset_id) - if asset_id in self._items_with_color_by_id: - self._items_with_color_by_id.pop(asset_id) # Refresh data # - all items refresh all data except id From 712d226e922f31fea0ebf5559f8625be8d140d26 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 23 May 2022 17:33:10 +0200 Subject: [PATCH 349/398] Fix - use clean layer name to create subset name Clean name is without publishing highlights denoting that layer has created OP instance, eg. should be published. --- .../photoshop/plugins/publish/collect_color_coded_instances.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/photoshop/plugins/publish/collect_color_coded_instances.py b/openpype/hosts/photoshop/plugins/publish/collect_color_coded_instances.py index ae025fc61d..71bd2cd854 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_color_coded_instances.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_color_coded_instances.py @@ -84,7 +84,7 @@ class CollectColorCodedInstances(pyblish.api.ContextPlugin): "variant": variant, "family": resolved_family, "task": task_name, - "layer": layer.name + "layer": layer.clean_name } subset = resolved_subset_template.format( From 5d6ce5592dfcb02444e9d7f86e9feab6eaebf53d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 23 May 2022 17:47:54 +0200 Subject: [PATCH 350/398] Hiero: rolled back py3 compatible code make it py27 working --- openpype/hosts/hiero/api/lib.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/hiero/api/lib.py b/openpype/hosts/hiero/api/lib.py index 5b2f6c814d..ae0aef9e9b 100644 --- a/openpype/hosts/hiero/api/lib.py +++ b/openpype/hosts/hiero/api/lib.py @@ -171,7 +171,7 @@ def get_track_items( # get selected track items or all in active sequence if selection: - with contextlib.suppress(AttributeError): + try: for track_item in selection: log.info("___ track_item: {}".format(track_item)) # make sure only trackitems are selected @@ -188,6 +188,8 @@ def get_track_items( ): log.info("___ valid trackitem: {}".format(track_item)) return_list.append(track_item) + except AttributeError: + pass # collect all available active sequence track items if not return_list: From 2e5acf6221e5063517870c93046979e79b243e49 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 23 May 2022 17:50:09 +0200 Subject: [PATCH 351/398] hound --- openpype/hosts/hiero/api/lib.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/hiero/api/lib.py b/openpype/hosts/hiero/api/lib.py index ae0aef9e9b..d19cefd2da 100644 --- a/openpype/hosts/hiero/api/lib.py +++ b/openpype/hosts/hiero/api/lib.py @@ -2,7 +2,6 @@ Host specific functions where host api is connected """ -import contextlib from copy import deepcopy import os import re @@ -986,7 +985,8 @@ def get_sequence_pattern_and_padding(file): return None, None found = sorted(list(set(foundall[0])))[-1] - padding = int(re.findall(r"\d+", found)[-1]) if "%" in found else len(found) + padding = int( + re.findall(r"\d+", found)[-1]) if "%" in found else len(found) return found, padding From d4b6d6552caf6ebaef179a1f1519731746dad28e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Mon, 23 May 2022 18:34:03 +0200 Subject: [PATCH 352/398] :bug: get resolution from overrides --- .../hosts/maya/plugins/publish/collect_render.py | 12 +++++++++--- .../hosts/maya/plugins/publish/collect_vrayscene.py | 12 +++++++++--- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index e66983780e..b19572ab37 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -339,9 +339,15 @@ class CollectMayaRender(pyblish.api.ContextPlugin): "source": filepath, "expectedFiles": full_exp_files, "publishRenderMetadataFolder": common_publish_meta_path, - "resolutionWidth": cmds.getAttr("defaultResolution.width"), - "resolutionHeight": cmds.getAttr("defaultResolution.height"), - "pixelAspect": cmds.getAttr("defaultResolution.pixelAspect"), + "resolutionWidth": lib.get_attr_in_layer( + "defaultResolution.height", layer=layer + ), + "resolutionHeight": lib.get_attr_in_layer( + "defaultResolution.width", layer=layer + ), + "pixelAspect": lib.get_attr_in_layer( + "defaultResolution.pixelAspect", layer=layer + ), "tileRendering": render_instance.data.get("tileRendering") or False, # noqa: E501 "tilesX": render_instance.data.get("tilesX") or 2, "tilesY": render_instance.data.get("tilesY") or 2, diff --git a/openpype/hosts/maya/plugins/publish/collect_vrayscene.py b/openpype/hosts/maya/plugins/publish/collect_vrayscene.py index afdb570cbc..6a0c2332fe 100644 --- a/openpype/hosts/maya/plugins/publish/collect_vrayscene.py +++ b/openpype/hosts/maya/plugins/publish/collect_vrayscene.py @@ -124,9 +124,15 @@ class CollectVrayScene(pyblish.api.InstancePlugin): # Add source to allow tracing back to the scene from # which was submitted originally "source": context.data["currentFile"].replace("\\", "/"), - "resolutionWidth": cmds.getAttr("defaultResolution.width"), - "resolutionHeight": cmds.getAttr("defaultResolution.height"), - "pixelAspect": cmds.getAttr("defaultResolution.pixelAspect"), + "resolutionWidth": lib.get_attr_in_layer( + "defaultResolution.height", layer=layer + ), + "resolutionHeight": lib.get_attr_in_layer( + "defaultResolution.width", layer=layer + ), + "pixelAspect": lib.get_attr_in_layer( + "defaultResolution.pixelAspect", layer=layer + ), "priority": instance.data.get("priority"), "useMultipleSceneFiles": instance.data.get( "vraySceneMultipleFiles") From d02af55c0589a3f50f1855a01b02b531a671e2dc Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 23 May 2022 19:04:34 +0200 Subject: [PATCH 353/398] validate that the user exists on ftrack when ftrack credentials are checked --- openpype/modules/ftrack/lib/credentials.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/modules/ftrack/lib/credentials.py b/openpype/modules/ftrack/lib/credentials.py index 4e29e66382..2eb64254d1 100644 --- a/openpype/modules/ftrack/lib/credentials.py +++ b/openpype/modules/ftrack/lib/credentials.py @@ -92,14 +92,18 @@ def check_credentials(username, api_key, ftrack_server=None): if not ftrack_server or not username or not api_key: return False + user_exists = False try: session = ftrack_api.Session( server_url=ftrack_server, api_key=api_key, api_user=username ) + # Validated that the username actually exists + user = session.query("User where username is \"{}\"".format(username)) + user_exists = user is not None session.close() except Exception: - return False - return True + pass + return user_exists From c0ee519dba5f24c8040bc08f910c7024697b621d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 24 May 2022 12:59:33 +0200 Subject: [PATCH 354/398] Hound --- openpype/hosts/flame/api/lib.py | 12 +++++++----- openpype/hosts/hiero/api/lib.py | 3 ++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/flame/api/lib.py b/openpype/hosts/flame/api/lib.py index 6dc7d3d887..d59308ad6c 100644 --- a/openpype/hosts/flame/api/lib.py +++ b/openpype/hosts/flame/api/lib.py @@ -779,7 +779,6 @@ class MediaInfoFile(object): feed_dir = os.path.dirname(path) feed_ext = os.path.splitext(feed_basename)[1][1:].lower() - with maintained_temp_file_path(".clip") as tmp_path: self.log.info("Temp File: {}".format(tmp_path)) self._generate_media_info_file(tmp_path, feed_ext, feed_dir) @@ -827,9 +826,11 @@ class MediaInfoFile(object): # make sure partial input basename is having correct extensoon if not partialname: - raise AttributeError("Wrong input attributes. Basename - {}, Ext - {}".format( - feed_basename, feed_ext - )) + raise AttributeError( + "Wrong input attributes. Basename - {}, Ext - {}".format( + feed_basename, feed_ext + ) + ) # get all related files files = [ @@ -860,7 +861,8 @@ class MediaInfoFile(object): # convert to multiple collections _continues_colls = collection.separate() for _coll in _continues_colls: - coll_to_text = self._format_collection(_coll, len(number_from_path)) + coll_to_text = self._format_collection( + _coll, len(number_from_path)) self.log.debug("__ coll_to_text: {}".format(coll_to_text)) if search_number_pattern in coll_to_text: return coll_to_text diff --git a/openpype/hosts/hiero/api/lib.py b/openpype/hosts/hiero/api/lib.py index 5b2f6c814d..5a9f38bf92 100644 --- a/openpype/hosts/hiero/api/lib.py +++ b/openpype/hosts/hiero/api/lib.py @@ -984,7 +984,8 @@ def get_sequence_pattern_and_padding(file): return None, None found = sorted(list(set(foundall[0])))[-1] - padding = int(re.findall(r"\d+", found)[-1]) if "%" in found else len(found) + padding = int( + re.findall(r"\d+", found)[-1]) if "%" in found else len(found) return found, padding From 0e0bbd56992a8ee8e7c32d90e56606623bb3781c Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 24 May 2022 15:31:01 +0100 Subject: [PATCH 355/398] Fix camera in UE5 --- .../hosts/unreal/plugins/load/load_camera.py | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index b33e45b6e9..0072dd9e73 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -57,6 +57,33 @@ class CameraLoader(plugin.Loader): min_frame_j, max_frame_j + 1) + def _import_camera( + self, world, sequence, bindings, import_fbx_settings, import_filename + ): + ue_version = unreal.SystemLibrary.get_engine_version().split('.') + ue_major = int(ue_version[0]) + ue_minor = int(ue_version[1]) + + if ue_major == 4 and ue_minor <= 26: + unreal.SequencerTools.import_fbx( + world, + sequence, + bindings, + import_fbx_settings, + import_filename + ) + elif (ue_major == 4 and ue_minor >= 27) or ue_major == 5: + unreal.SequencerTools.import_level_sequence_fbx( + world, + sequence, + bindings, + import_fbx_settings, + import_filename + ) + else: + raise NotImplementedError( + f"Unreal version {ue_major} not supported") + def load(self, context, name, namespace, data): """ Load and containerise representation into Content Browser. @@ -228,7 +255,7 @@ class CameraLoader(plugin.Loader): settings.set_editor_property('reduce_keys', False) if cam_seq: - unreal.SequencerTools.import_fbx( + self._import_camera( EditorLevelLibrary.get_editor_world(), cam_seq, cam_seq.get_bindings(), @@ -388,7 +415,7 @@ class CameraLoader(plugin.Loader): sub_scene.set_sequence(new_sequence) - unreal.SequencerTools.import_fbx( + self._import_camera( EditorLevelLibrary.get_editor_world(), new_sequence, new_sequence.get_bindings(), From 9c72873a9a8ac462abf463cc86c1c04dcfecaf8b Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 24 May 2022 15:32:29 +0100 Subject: [PATCH 356/398] Fix animations in UE5 --- openpype/hosts/unreal/plugins/load/load_animation.py | 12 ++++++++---- openpype/hosts/unreal/plugins/load/load_layout.py | 6 ++++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_animation.py b/openpype/hosts/unreal/plugins/load/load_animation.py index 60c1526d3d..54b43c500c 100644 --- a/openpype/hosts/unreal/plugins/load/load_animation.py +++ b/openpype/hosts/unreal/plugins/load/load_animation.py @@ -77,13 +77,15 @@ class AnimationFBXLoader(plugin.Loader): task.options.anim_sequence_import_data.set_editor_property( 'import_meshes_in_bone_hierarchy', False) task.options.anim_sequence_import_data.set_editor_property( - 'use_default_sample_rate', True) + 'use_default_sample_rate', False) + task.options.anim_sequence_import_data.set_editor_property( + 'custom_sample_rate', 25.0) # TODO: get from database task.options.anim_sequence_import_data.set_editor_property( 'import_custom_attribute', True) task.options.anim_sequence_import_data.set_editor_property( 'import_bone_tracks', True) task.options.anim_sequence_import_data.set_editor_property( - 'remove_redundant_keys', True) + 'remove_redundant_keys', False) task.options.anim_sequence_import_data.set_editor_property( 'convert_scene', True) @@ -279,13 +281,15 @@ class AnimationFBXLoader(plugin.Loader): task.options.anim_sequence_import_data.set_editor_property( 'import_meshes_in_bone_hierarchy', False) task.options.anim_sequence_import_data.set_editor_property( - 'use_default_sample_rate', True) + 'use_default_sample_rate', False) + task.options.anim_sequence_import_data.set_editor_property( + 'custom_sample_rate', 25.0) # TODO: get from database task.options.anim_sequence_import_data.set_editor_property( 'import_custom_attribute', True) task.options.anim_sequence_import_data.set_editor_property( 'import_bone_tracks', True) task.options.anim_sequence_import_data.set_editor_property( - 'remove_redundant_keys', True) + 'remove_redundant_keys', False) task.options.anim_sequence_import_data.set_editor_property( 'convert_scene', True) diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index 412f77e3a9..49611c6c05 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -262,13 +262,15 @@ class LayoutLoader(plugin.Loader): task.options.anim_sequence_import_data.set_editor_property( 'import_meshes_in_bone_hierarchy', False) task.options.anim_sequence_import_data.set_editor_property( - 'use_default_sample_rate', True) + 'use_default_sample_rate', False) + task.options.anim_sequence_import_data.set_editor_property( + 'custom_sample_rate', 25.0) # TODO: get from database task.options.anim_sequence_import_data.set_editor_property( 'import_custom_attribute', True) task.options.anim_sequence_import_data.set_editor_property( 'import_bone_tracks', True) task.options.anim_sequence_import_data.set_editor_property( - 'remove_redundant_keys', True) + 'remove_redundant_keys', False) task.options.anim_sequence_import_data.set_editor_property( 'convert_scene', True) From 1b76f86d6691b0106ce2ae9022666979103cf57d Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 24 May 2022 15:35:47 +0100 Subject: [PATCH 357/398] Fix render create in UE5 --- openpype/hosts/unreal/plugins/create/create_render.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/unreal/plugins/create/create_render.py b/openpype/hosts/unreal/plugins/create/create_render.py index 3b6c7a9f1e..a3e125a94e 100644 --- a/openpype/hosts/unreal/plugins/create/create_render.py +++ b/openpype/hosts/unreal/plugins/create/create_render.py @@ -22,17 +22,24 @@ class CreateRender(Creator): ar = unreal.AssetRegistryHelpers.get_asset_registry() + # The asset name is the the third element of the path which contains + # the map. + # The index of the split path is 3 because the first element is an + # empty string, as the path begins with "/Content". + a = unreal.EditorUtilityLibrary.get_selected_assets()[0] + asset_name = a.get_path_name().split("/")[3] + # Get the master sequence and the master level. # There should be only one sequence and one level in the directory. filter = unreal.ARFilter( class_names=["LevelSequence"], - package_paths=[f"/Game/OpenPype/{self.data['asset']}"], + package_paths=[f"/Game/OpenPype/{asset_name}"], recursive_paths=False) sequences = ar.get_assets(filter) ms = sequences[0].get_editor_property('object_path') filter = unreal.ARFilter( class_names=["World"], - package_paths=[f"/Game/OpenPype/{self.data['asset']}"], + package_paths=[f"/Game/OpenPype/{asset_name}"], recursive_paths=False) levels = ar.get_assets(filter) ml = levels[0].get_editor_property('object_path') From f57c22e4203b9d6441a05edabeda19742e4eda91 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 24 May 2022 22:20:00 +0200 Subject: [PATCH 358/398] :bug: fix layer names --- openpype/hosts/maya/plugins/publish/collect_render.py | 6 +++--- openpype/hosts/maya/plugins/publish/collect_vrayscene.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index b19572ab37..fbd2e81279 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -340,13 +340,13 @@ class CollectMayaRender(pyblish.api.ContextPlugin): "expectedFiles": full_exp_files, "publishRenderMetadataFolder": common_publish_meta_path, "resolutionWidth": lib.get_attr_in_layer( - "defaultResolution.height", layer=layer + "defaultResolution.height", layer=layer_name ), "resolutionHeight": lib.get_attr_in_layer( - "defaultResolution.width", layer=layer + "defaultResolution.width", layer=layer_name ), "pixelAspect": lib.get_attr_in_layer( - "defaultResolution.pixelAspect", layer=layer + "defaultResolution.pixelAspect", layer=layer_name ), "tileRendering": render_instance.data.get("tileRendering") or False, # noqa: E501 "tilesX": render_instance.data.get("tilesX") or 2, diff --git a/openpype/hosts/maya/plugins/publish/collect_vrayscene.py b/openpype/hosts/maya/plugins/publish/collect_vrayscene.py index 6a0c2332fe..0bae9656f3 100644 --- a/openpype/hosts/maya/plugins/publish/collect_vrayscene.py +++ b/openpype/hosts/maya/plugins/publish/collect_vrayscene.py @@ -125,13 +125,13 @@ class CollectVrayScene(pyblish.api.InstancePlugin): # which was submitted originally "source": context.data["currentFile"].replace("\\", "/"), "resolutionWidth": lib.get_attr_in_layer( - "defaultResolution.height", layer=layer + "defaultResolution.height", layer=layer_name ), "resolutionHeight": lib.get_attr_in_layer( - "defaultResolution.width", layer=layer + "defaultResolution.width", layer=layer_name ), "pixelAspect": lib.get_attr_in_layer( - "defaultResolution.pixelAspect", layer=layer + "defaultResolution.pixelAspect", layer=layer_name ), "priority": instance.data.get("priority"), "useMultipleSceneFiles": instance.data.get( From ac79f31a279fcbd50d04a571b2b3eb8270b761d7 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 24 May 2022 22:39:50 +0200 Subject: [PATCH 359/398] :bug: filter out display types without file output don't process renderman display type that are not producing any file output (like `d_it`) --- openpype/hosts/maya/api/lib_renderproducts.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/hosts/maya/api/lib_renderproducts.py b/openpype/hosts/maya/api/lib_renderproducts.py index ff04fa7aa2..2d3bda5245 100644 --- a/openpype/hosts/maya/api/lib_renderproducts.py +++ b/openpype/hosts/maya/api/lib_renderproducts.py @@ -1093,6 +1093,11 @@ class RenderProductsRenderman(ARenderProducts): if not enabled: continue + # Skip display types not producing any file output. + # Is there a better way to do it? + if not display_types.get(display["driverNode"]["type"]): + continue + aov_name = name if aov_name == "rmanDefaultDisplay": aov_name = "beauty" From d019e5d7c22ace4d64bdbd8f73d09592b912e315 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Wed, 25 May 2022 03:48:50 +0000 Subject: [PATCH 360/398] [Automated] Bump version --- CHANGELOG.md | 23 +++++++++++------------ openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8cec29df7..3dd410391a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [3.10.0-nightly.4](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.10.0-nightly.5](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.9.8...HEAD) @@ -14,12 +14,16 @@ **🚀 Enhancements** +- Project Manager: Allow to paste Tasks into multiple assets at the same time [\#3226](https://github.com/pypeclub/OpenPype/pull/3226) - Project manager: Sped up project load [\#3216](https://github.com/pypeclub/OpenPype/pull/3216) +- Loader UI: Speed issues of loader with sync server [\#3199](https://github.com/pypeclub/OpenPype/pull/3199) - Maya: added clean\_import option to Import loader [\#3181](https://github.com/pypeclub/OpenPype/pull/3181) - Maya: add maya 2023 to default applications [\#3167](https://github.com/pypeclub/OpenPype/pull/3167) +- Compressed bgeo publishing in SAP and Houdini loader [\#3153](https://github.com/pypeclub/OpenPype/pull/3153) - General: Add 'dataclasses' to required python modules [\#3149](https://github.com/pypeclub/OpenPype/pull/3149) - Hooks: Tweak logging grammar [\#3147](https://github.com/pypeclub/OpenPype/pull/3147) - Nuke: settings for reformat node in CreateWriteRender node [\#3143](https://github.com/pypeclub/OpenPype/pull/3143) +- Houdini: Add loader for alembic through Alembic Archive node [\#3140](https://github.com/pypeclub/OpenPype/pull/3140) - Publisher: UI Modifications and fixes [\#3139](https://github.com/pypeclub/OpenPype/pull/3139) - General: Simplified OP modules/addons import [\#3137](https://github.com/pypeclub/OpenPype/pull/3137) - Terminal: Tweak coloring of TrayModuleManager logging enabled states [\#3133](https://github.com/pypeclub/OpenPype/pull/3133) @@ -28,18 +32,19 @@ - Unreal: Layout and Camera update and remove functions reimplemented and improvements [\#3116](https://github.com/pypeclub/OpenPype/pull/3116) - Settings: Remove environment groups from settings [\#3115](https://github.com/pypeclub/OpenPype/pull/3115) - TVPaint: Match renderlayer key with other hosts [\#3110](https://github.com/pypeclub/OpenPype/pull/3110) -- Ftrack: AssetVersion status on publish [\#3108](https://github.com/pypeclub/OpenPype/pull/3108) - Tray publisher: Simple families from settings [\#3105](https://github.com/pypeclub/OpenPype/pull/3105) **🐛 Bug fixes** +- Ftrack: Validate that the user exists on ftrack [\#3237](https://github.com/pypeclub/OpenPype/pull/3237) +- TVPaint: Look for more groups than 12 [\#3228](https://github.com/pypeclub/OpenPype/pull/3228) +- Project Manager: Fix persistent editors on project change [\#3218](https://github.com/pypeclub/OpenPype/pull/3218) - Deadline: instance data overwrite fix [\#3214](https://github.com/pypeclub/OpenPype/pull/3214) - Ftrack: Push hierarchical attributes action works [\#3210](https://github.com/pypeclub/OpenPype/pull/3210) - Standalone Publisher: Always create new representation for thumbnail [\#3203](https://github.com/pypeclub/OpenPype/pull/3203) - Photoshop: skip collector when automatic testing [\#3202](https://github.com/pypeclub/OpenPype/pull/3202) - Nuke: render/workfile version sync doesn't work on farm [\#3185](https://github.com/pypeclub/OpenPype/pull/3185) - Ftrack: Review image only if there are no mp4 reviews [\#3183](https://github.com/pypeclub/OpenPype/pull/3183) -- Ftrack: Locations deepcopy issue [\#3177](https://github.com/pypeclub/OpenPype/pull/3177) - General: Avoid creating multiple thumbnails [\#3176](https://github.com/pypeclub/OpenPype/pull/3176) - General/Hiero: better clip duration calculation [\#3169](https://github.com/pypeclub/OpenPype/pull/3169) - General: Oiio conversion for ffmpeg checks for invalid characters [\#3166](https://github.com/pypeclub/OpenPype/pull/3166) @@ -52,11 +57,10 @@ - TVPaint: Composite layers in reversed order [\#3135](https://github.com/pypeclub/OpenPype/pull/3135) - Nuke: fixing default settings for workfile builder loaders [\#3120](https://github.com/pypeclub/OpenPype/pull/3120) - Nuke: fix anatomy imageio regex default [\#3119](https://github.com/pypeclub/OpenPype/pull/3119) -- General: Python 3 compatibility in queries [\#3112](https://github.com/pypeclub/OpenPype/pull/3112) -- General: Collect loaded versions skips not existing representations [\#3095](https://github.com/pypeclub/OpenPype/pull/3095) **🔀 Refactored code** +- Avalon repo removed from Jobs workflow [\#3193](https://github.com/pypeclub/OpenPype/pull/3193) - General: Remove remaining imports from avalon [\#3130](https://github.com/pypeclub/OpenPype/pull/3130) **Merged pull requests:** @@ -72,6 +76,7 @@ **🚀 Enhancements** - nuke: generate publishing nodes inside render group node [\#3206](https://github.com/pypeclub/OpenPype/pull/3206) +- Loader UI: Speed issues of loader with sync server [\#3200](https://github.com/pypeclub/OpenPype/pull/3200) - Backport of fix for attaching renders to subsets [\#3195](https://github.com/pypeclub/OpenPype/pull/3195) **🐛 Bug fixes** @@ -79,6 +84,7 @@ - Standalone Publisher: Always create new representation for thumbnail [\#3204](https://github.com/pypeclub/OpenPype/pull/3204) - Nuke: render/workfile version sync doesn't work on farm [\#3184](https://github.com/pypeclub/OpenPype/pull/3184) - Ftrack: Review image only if there are no mp4 reviews [\#3182](https://github.com/pypeclub/OpenPype/pull/3182) +- Ftrack: Locations deepcopy issue [\#3177](https://github.com/pypeclub/OpenPype/pull/3177) - Ftrack: Locations deepcopy issue [\#3175](https://github.com/pypeclub/OpenPype/pull/3175) - General: Avoid creating multiple thumbnails [\#3174](https://github.com/pypeclub/OpenPype/pull/3174) - General: TemplateResult can be copied [\#3170](https://github.com/pypeclub/OpenPype/pull/3170) @@ -98,9 +104,7 @@ **🚀 Enhancements** - Deadline output dir issue to 3.9x [\#3155](https://github.com/pypeclub/OpenPype/pull/3155) -- Compressed bgeo publishing in SAP and Houdini loader [\#3153](https://github.com/pypeclub/OpenPype/pull/3153) - nuke: removing redundant code from startup [\#3142](https://github.com/pypeclub/OpenPype/pull/3142) -- Houdini: Add loader for alembic through Alembic Archive node [\#3140](https://github.com/pypeclub/OpenPype/pull/3140) **🐛 Bug fixes** @@ -137,11 +141,6 @@ [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.10.0-nightly.2...3.9.5) -**🐛 Bug fixes** - -- Ftrack: Update Create Folders action [\#3092](https://github.com/pypeclub/OpenPype/pull/3092) -- General: Extract review sequence is not converted with same names [\#3075](https://github.com/pypeclub/OpenPype/pull/3075) - ## [3.9.4](https://github.com/pypeclub/OpenPype/tree/3.9.4) (2022-04-15) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.9.4-nightly.2...3.9.4) diff --git a/openpype/version.py b/openpype/version.py index 1cc854cfd1..eee776fd2c 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.10.0-nightly.4" +__version__ = "3.10.0-nightly.5" diff --git a/pyproject.toml b/pyproject.toml index a2614b24b5..50cdefe1bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.10.0-nightly.4" # OpenPype +version = "3.10.0-nightly.5" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From c9b0bb0e54cbd3390f06ee8ea3135ffd5e9413e5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 25 May 2022 13:32:45 +0200 Subject: [PATCH 361/398] make sure chunk size is at least 1 --- openpype/modules/ftrack/lib/avalon_sync.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/modules/ftrack/lib/avalon_sync.py b/openpype/modules/ftrack/lib/avalon_sync.py index 124787e467..e4ba651bfd 100644 --- a/openpype/modules/ftrack/lib/avalon_sync.py +++ b/openpype/modules/ftrack/lib/avalon_sync.py @@ -143,14 +143,17 @@ def create_chunks(iterable, chunk_size=None): list: Chunked items. """ chunks = [] - if not iterable: - return chunks tupled_iterable = tuple(iterable) + if not tupled_iterable: + return chunks iterable_size = len(tupled_iterable) if chunk_size is None: chunk_size = 200 + if chunk_size < 1: + chunk_size = 1 + for idx in range(0, iterable_size, chunk_size): chunks.append(tupled_iterable[idx:idx + chunk_size]) return chunks From 93c4e3403aa0a26717ab47815940cb269e279c76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Wed, 25 May 2022 14:06:25 +0200 Subject: [PATCH 362/398] :bug: fix node attribute name --- .../hosts/maya/plugins/publish/collect_look.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_look.py b/openpype/hosts/maya/plugins/publish/collect_look.py index fb2ce04cad..d295492f9a 100644 --- a/openpype/hosts/maya/plugins/publish/collect_look.py +++ b/openpype/hosts/maya/plugins/publish/collect_look.py @@ -616,11 +616,15 @@ class CollectLook(pyblish.api.InstancePlugin): self.log.info(" - color space: {}".format(color_space)) # Define the resource - return {"node": node, - "attribute": attribute, - "source": source, # required for resources - "files": files, - "color_space": color_space} # required for resources + return { + "node": node, + # here we are passing not only attribute, but with node again + # this should be simplified and changed extractor. + "attribute": "{}.{}".format(node, attribute), + "source": source, # required for resources + "files": files, + "color_space": color_space + } # required for resources class CollectModelRenderSets(CollectLook): From ce882641e7baec3c7edca957e2024331943ab9af Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 25 May 2022 17:27:40 +0200 Subject: [PATCH 363/398] Vendor: updating scriptmenu to 1.5.2 --- openpype/vendor/python/common/scriptsmenu/action.py | 3 ++- openpype/vendor/python/common/scriptsmenu/launchfornuke.py | 7 ++----- openpype/vendor/python/common/scriptsmenu/scriptsmenu.py | 3 +-- openpype/vendor/python/common/scriptsmenu/version.py | 2 +- 4 files changed, 6 insertions(+), 9 deletions(-) diff --git a/openpype/vendor/python/common/scriptsmenu/action.py b/openpype/vendor/python/common/scriptsmenu/action.py index dc4d775f6a..5e68628406 100644 --- a/openpype/vendor/python/common/scriptsmenu/action.py +++ b/openpype/vendor/python/common/scriptsmenu/action.py @@ -119,7 +119,8 @@ module.{module_name}()""" """ # get the current application and its linked keyboard modifiers - modifiers = QtWidgets.QApplication.keyboardModifiers() + app = QtWidgets.QApplication.instance() + modifiers = app.keyboardModifiers() # If the menu has a callback registered for the current modifier # we run the callback instead of the action itself. diff --git a/openpype/vendor/python/common/scriptsmenu/launchfornuke.py b/openpype/vendor/python/common/scriptsmenu/launchfornuke.py index 23e4ed1b4d..72302a79a6 100644 --- a/openpype/vendor/python/common/scriptsmenu/launchfornuke.py +++ b/openpype/vendor/python/common/scriptsmenu/launchfornuke.py @@ -8,7 +8,7 @@ def _nuke_main_window(): if (obj.inherits('QMainWindow') and obj.metaObject().className() == 'Foundry::UI::DockMainWindow'): return obj - raise RuntimeError('Could not find Nuke MainWindow instance') + raise RuntimeError('Could not find Nuke MainWindow instance') def _nuke_main_menubar(): @@ -22,9 +22,6 @@ def _nuke_main_menubar(): def main(title="Scripts"): - # Register control + shift callback to add to shelf (Nuke behavior) - # modifiers = QtCore.Qt.ControlModifier | QtCore.Qt.ShiftModifier - # menu.register_callback(modifiers, to_shelf) nuke_main_bar = _nuke_main_menubar() for nuke_bar in nuke_main_bar.children(): if isinstance(nuke_bar, scriptsmenu.ScriptsMenu): @@ -33,4 +30,4 @@ def main(title="Scripts"): return menu menu = scriptsmenu.ScriptsMenu(title=title, parent=nuke_main_bar) - return menu \ No newline at end of file + return menu diff --git a/openpype/vendor/python/common/scriptsmenu/scriptsmenu.py b/openpype/vendor/python/common/scriptsmenu/scriptsmenu.py index e2b7ff96c7..9e7c094902 100644 --- a/openpype/vendor/python/common/scriptsmenu/scriptsmenu.py +++ b/openpype/vendor/python/common/scriptsmenu/scriptsmenu.py @@ -264,8 +264,7 @@ class ScriptsMenu(QtWidgets.QMenu): action.setVisible(True) else: for action in self._script_actions: - if not action.has_tag(search.lower()): - action.setVisible(False) + action.setVisible(action.has_tag(search.lower())) # Set visibility for all submenus for action in self.actions(): diff --git a/openpype/vendor/python/common/scriptsmenu/version.py b/openpype/vendor/python/common/scriptsmenu/version.py index 73f9426c2d..52ec49c845 100644 --- a/openpype/vendor/python/common/scriptsmenu/version.py +++ b/openpype/vendor/python/common/scriptsmenu/version.py @@ -1,6 +1,6 @@ VERSION_MAJOR = 1 VERSION_MINOR = 5 -VERSION_PATCH = 1 +VERSION_PATCH = 2 version = '{}.{}.{}'.format(VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH) From d17ae6d6a588be1c700be664f3e746975661034a Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Wed, 25 May 2022 19:10:32 +0200 Subject: [PATCH 364/398] Change icon path and add nukestudio icon. --- openpype/resources/app_icons/nukestudio.png | Bin 0 -> 46080 bytes .../defaults/system_settings/applications.json | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 openpype/resources/app_icons/nukestudio.png diff --git a/openpype/resources/app_icons/nukestudio.png b/openpype/resources/app_icons/nukestudio.png new file mode 100644 index 0000000000000000000000000000000000000000..99c95f59ff858d67fb92263f594f4b71bda87a66 GIT binary patch literal 46080 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 HIsgA3TR*cj literal 0 HcmV?d00001 diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index 2b0de44fa9..29c8a1cccc 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -292,7 +292,7 @@ "nukex": { "enabled": true, "label": "Nuke X", - "icon": "{}/app_icons/nuke.png", + "icon": "{}/app_icons/nukex.png", "host_name": "nuke", "environment": { "NUKE_PATH": [ @@ -431,7 +431,7 @@ "nukestudio": { "enabled": true, "label": "Nuke Studio", - "icon": "{}/app_icons/nuke.png", + "icon": "{}/app_icons/nukestudio.png", "host_name": "hiero", "environment": { "WORKFILES_STARTUP": "0", From d9a9981fefacc42a9e9466f6af6769f1605d4396 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 25 May 2022 19:37:14 +0200 Subject: [PATCH 365/398] Fix - Harmony 21.1 messed up Javascript Qt API QDataStream is missing, different way to get codec used. QApplication.activeWindow() also returned null, replaced by topLevelWidgets --- openpype/hosts/harmony/api/TB_sceneOpened.js | 31 +++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/harmony/api/TB_sceneOpened.js b/openpype/hosts/harmony/api/TB_sceneOpened.js index 6a403fa65e..29473bcc93 100644 --- a/openpype/hosts/harmony/api/TB_sceneOpened.js +++ b/openpype/hosts/harmony/api/TB_sceneOpened.js @@ -279,19 +279,13 @@ function Client() { }; self._send = function(message) { - var data = new QByteArray(); - var outstr = new QDataStream(data, QIODevice.WriteOnly); - outstr.writeInt(0); - data.append('UTF-8'); - outstr.device().seek(0); - outstr.writeInt(data.size() - 4); - var codec = QTextCodec.codecForUtfText(data); - var msg = codec.fromUnicode(message); - var l = msg.size(); - var coded = new QByteArray('AH').append(self.pack(l)); - coded = coded.append(msg); - self.socket.write(new QByteArray(coded)); - self.logDebug('Sent.'); + var codec_name = new QByteArray().append("ISO-8859-1"); + var codec = QTextCodec.codecForName(codec_name); + var msg = codec.fromUnicode(message); + var l = msg.size(); + var coded = new QByteArray().append('AH').append(self.pack(l)).append(msg); + self.socket.write(new QByteArray(coded)); + self.logDebug('Sent.'); }; self.waitForLock = function() { @@ -343,6 +337,7 @@ function start() { var host = '127.0.0.1'; /** port of the server */ var port = parseInt(System.getenv('AVALON_HARMONY_PORT')); + MessageLog.trace("port " + port.toString()); // Attach the client to the QApplication to preserve. var app = QCoreApplication.instance(); @@ -351,7 +346,15 @@ function start() { app.avalonClient = new Client(); app.avalonClient.socket.connectToHost(host, port); } - var menuBar = QApplication.activeWindow().menuBar(); + var mainWindow = null; + var widgets = QApplication.topLevelWidgets(); + for (var i = 0 ; i < widgets.length; i++) { + if (widgets[i] instanceof QMainWindow){ + MessageLog.trace('(DEBUG): START Main window '); + mainWindow = widgets[i]; + } + } + var menuBar = mainWindow.menuBar(); var actions = menuBar.actions(); app.avalonMenu = null; From f213a33f130d336ca8345ab64bbc6d1105c3a379 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 25 May 2022 19:40:40 +0200 Subject: [PATCH 366/398] Fix - Harmony 21.1 messed up Javascript Qt API Removed missed logging --- openpype/hosts/harmony/api/TB_sceneOpened.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/harmony/api/TB_sceneOpened.js b/openpype/hosts/harmony/api/TB_sceneOpened.js index 29473bcc93..610b0a73bb 100644 --- a/openpype/hosts/harmony/api/TB_sceneOpened.js +++ b/openpype/hosts/harmony/api/TB_sceneOpened.js @@ -337,7 +337,6 @@ function start() { var host = '127.0.0.1'; /** port of the server */ var port = parseInt(System.getenv('AVALON_HARMONY_PORT')); - MessageLog.trace("port " + port.toString()); // Attach the client to the QApplication to preserve. var app = QCoreApplication.instance(); @@ -350,7 +349,6 @@ function start() { var widgets = QApplication.topLevelWidgets(); for (var i = 0 ; i < widgets.length; i++) { if (widgets[i] instanceof QMainWindow){ - MessageLog.trace('(DEBUG): START Main window '); mainWindow = widgets[i]; } } From 78ddd548287c00eb99aaeb99624d380314172052 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 26 May 2022 10:42:31 +0200 Subject: [PATCH 367/398] init file for tvpaint worker also has set path to not existing file in guidelines --- openpype/hosts/tvpaint/worker/init_file.tvpp | Bin 59333 -> 59973 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/openpype/hosts/tvpaint/worker/init_file.tvpp b/openpype/hosts/tvpaint/worker/init_file.tvpp index 572d278fdb49c619d3cf040f59238377d1062d6a..22170b45bc728d3d46193a35fa5216e5c739e8cf 100644 GIT binary patch delta 627 zcmZ`!&r94u6n-1o7WW`Zp|ppVoa$-UWMbA$V4$EB5B?|x5xnSZcA6b`HWPLdQ4j>h zOHVDi6%=}`g_aV$mEuM4=0(uIz|#H|eTnu`^ufFz-+SNt=AE7oon8#RfBgJU-PvFA zf4m&KJluxSm2XF1ptOhE7+!pzoHw4j-~)jsm|%g2xl?)qUjETIFe*;gsvbWYhFK7x zApr#g*n&FP*<97TgFJ;RY#`ZTb@2`sxrbE<>gYp z2h-361`&jaR)FL)4}MD{{hCeaH6xwNKj_^`O`pA5d@~QAPx5E!yS6drWS=k3t1J~A zmF-(*}bzUpvFib`vt6KY`^C@ER2Ikj&JHK3Mhla`6R;vxE0inyHZ ziKs)T5Rt6SC0$U7$v1J4h(c5JS;!mfaagof(A(jilGA5{5SKR*R_%2jpz5vFi};`W z*Td43?b!*s;r^1=F%9nU9TO){MXT^5Br`*cmR*T2%g@OG;2 delta 199 zcmX?lh56`t<_ThQ&(|8ahFa}$5WGIGhCv`|{i@XrV8A8p>0If=z3$r*`x>8X Date: Thu, 26 May 2022 14:07:30 +0200 Subject: [PATCH 368/398] :truck: split versions of integration plugin --- openpype/hosts/unreal/__init__.py | 10 +- .../integration/{ => UE_4.7}/.gitignore | 0 .../Content/Python/init_unreal.py | 0 .../integration/{ => UE_4.7}/OpenPype.uplugin | 0 .../unreal/integration/{ => UE_4.7}/README.md | 2 +- .../{ => UE_4.7}/Resources/openpype128.png | Bin .../{ => UE_4.7}/Resources/openpype40.png | Bin .../{ => UE_4.7}/Resources/openpype512.png | Bin .../UE_4.7/Source/OpenPype/OpenPype.Build.cs | 57 +++++++++ .../OpenPype/Private/AssetContainer.cpp | 0 .../Private/AssetContainerFactory.cpp | 0 .../Source/OpenPype/Private/OpenPype.cpp | 103 ++++++++++++++++ .../Source/OpenPype/Private/OpenPypeLib.cpp | 0 .../Private/OpenPypePublishInstance.cpp | 0 .../OpenPypePublishInstanceFactory.cpp | 0 .../OpenPype/Private/OpenPypePythonBridge.cpp | 0 .../Source/OpenPype/Private/OpenPypeStyle.cpp | 70 +++++++++++ .../Source/OpenPype/Public/AssetContainer.h | 0 .../OpenPype/Public/AssetContainerFactory.h | 0 .../UE_4.7/Source/OpenPype/Public/OpenPype.h | 21 ++++ .../Source/OpenPype/Public/OpenPypeLib.h | 0 .../OpenPype/Public/OpenPypePublishInstance.h | 0 .../Public/OpenPypePublishInstanceFactory.h | 0 .../OpenPype/Public/OpenPypePythonBridge.h | 0 .../Source/OpenPype/Public/OpenPypeStyle.h | 22 ++++ .../unreal/integration/UE_5.0/.gitignore | 35 ++++++ .../UE_5.0/Content/Python/__init__.py | 0 .../UE_5.0/Content/Python/init_unreal.py | 28 +++++ .../integration/UE_5.0/Content/__init__.py | 0 .../integration/UE_5.0/OpenPype.uplugin | 24 ++++ .../hosts/unreal/integration/UE_5.0/README.md | 11 ++ .../UE_5.0/Resources/openpype128.png | Bin 0 -> 14594 bytes .../UE_5.0/Resources/openpype40.png | Bin 0 -> 4884 bytes .../UE_5.0/Resources/openpype512.png | Bin 0 -> 85856 bytes .../Source/OpenPype/OpenPype.Build.cs | 0 .../OpenPype/Private/AssetContainer.cpp | 115 ++++++++++++++++++ .../Private/AssetContainerFactory.cpp | 20 +++ .../Source/OpenPype/Private/OpenPype.cpp | 0 .../OpenPype/Private/OpenPypeCommands.cpp | 0 .../Source/OpenPype/Private/OpenPypeLib.cpp | 48 ++++++++ .../Private/OpenPypePublishInstance.cpp | 108 ++++++++++++++++ .../OpenPypePublishInstanceFactory.cpp | 20 +++ .../OpenPype/Private/OpenPypePythonBridge.cpp | 13 ++ .../Source/OpenPype/Private/OpenPypeStyle.cpp | 0 .../Source/OpenPype/Public/AssetContainer.h | 39 ++++++ .../OpenPype/Public/AssetContainerFactory.h | 21 ++++ .../Source/OpenPype/Public/OpenPype.h | 0 .../Source/OpenPype/Public/OpenPypeCommands.h | 0 .../Source/OpenPype/Public/OpenPypeLib.h | 19 +++ .../OpenPype/Public/OpenPypePublishInstance.h | 21 ++++ .../Public/OpenPypePublishInstanceFactory.h | 19 +++ .../OpenPype/Public/OpenPypePythonBridge.h | 20 +++ .../Source/OpenPype/Public/OpenPypeStyle.h | 0 .../system_settings/applications.json | 12 +- repos/avalon-core | 1 - 55 files changed, 854 insertions(+), 5 deletions(-) rename openpype/hosts/unreal/integration/{ => UE_4.7}/.gitignore (100%) rename openpype/hosts/unreal/integration/{ => UE_4.7}/Content/Python/init_unreal.py (100%) rename openpype/hosts/unreal/integration/{ => UE_4.7}/OpenPype.uplugin (100%) rename openpype/hosts/unreal/integration/{ => UE_4.7}/README.md (91%) rename openpype/hosts/unreal/integration/{ => UE_4.7}/Resources/openpype128.png (100%) rename openpype/hosts/unreal/integration/{ => UE_4.7}/Resources/openpype40.png (100%) rename openpype/hosts/unreal/integration/{ => UE_4.7}/Resources/openpype512.png (100%) create mode 100644 openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/OpenPype.Build.cs rename openpype/hosts/unreal/integration/{ => UE_4.7}/Source/OpenPype/Private/AssetContainer.cpp (100%) rename openpype/hosts/unreal/integration/{ => UE_4.7}/Source/OpenPype/Private/AssetContainerFactory.cpp (100%) create mode 100644 openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/OpenPype.cpp rename openpype/hosts/unreal/integration/{ => UE_4.7}/Source/OpenPype/Private/OpenPypeLib.cpp (100%) rename openpype/hosts/unreal/integration/{ => UE_4.7}/Source/OpenPype/Private/OpenPypePublishInstance.cpp (100%) rename openpype/hosts/unreal/integration/{ => UE_4.7}/Source/OpenPype/Private/OpenPypePublishInstanceFactory.cpp (100%) rename openpype/hosts/unreal/integration/{ => UE_4.7}/Source/OpenPype/Private/OpenPypePythonBridge.cpp (100%) create mode 100644 openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/OpenPypeStyle.cpp rename openpype/hosts/unreal/integration/{ => UE_4.7}/Source/OpenPype/Public/AssetContainer.h (100%) rename openpype/hosts/unreal/integration/{ => UE_4.7}/Source/OpenPype/Public/AssetContainerFactory.h (100%) create mode 100644 openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/OpenPype.h rename openpype/hosts/unreal/integration/{ => UE_4.7}/Source/OpenPype/Public/OpenPypeLib.h (100%) rename openpype/hosts/unreal/integration/{ => UE_4.7}/Source/OpenPype/Public/OpenPypePublishInstance.h (100%) rename openpype/hosts/unreal/integration/{ => UE_4.7}/Source/OpenPype/Public/OpenPypePublishInstanceFactory.h (100%) rename openpype/hosts/unreal/integration/{ => UE_4.7}/Source/OpenPype/Public/OpenPypePythonBridge.h (100%) create mode 100644 openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/OpenPypeStyle.h create mode 100644 openpype/hosts/unreal/integration/UE_5.0/.gitignore create mode 100644 openpype/hosts/unreal/integration/UE_5.0/Content/Python/__init__.py create mode 100644 openpype/hosts/unreal/integration/UE_5.0/Content/Python/init_unreal.py create mode 100644 openpype/hosts/unreal/integration/UE_5.0/Content/__init__.py create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype.uplugin create mode 100644 openpype/hosts/unreal/integration/UE_5.0/README.md create mode 100644 openpype/hosts/unreal/integration/UE_5.0/Resources/openpype128.png create mode 100644 openpype/hosts/unreal/integration/UE_5.0/Resources/openpype40.png create mode 100644 openpype/hosts/unreal/integration/UE_5.0/Resources/openpype512.png rename openpype/hosts/unreal/integration/{ => UE_5.0}/Source/OpenPype/OpenPype.Build.cs (100%) create mode 100644 openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/AssetContainer.cpp create mode 100644 openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/AssetContainerFactory.cpp rename openpype/hosts/unreal/integration/{ => UE_5.0}/Source/OpenPype/Private/OpenPype.cpp (100%) rename openpype/hosts/unreal/integration/{ => UE_5.0}/Source/OpenPype/Private/OpenPypeCommands.cpp (100%) create mode 100644 openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeLib.cpp create mode 100644 openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypePublishInstance.cpp create mode 100644 openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypePublishInstanceFactory.cpp create mode 100644 openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypePythonBridge.cpp rename openpype/hosts/unreal/integration/{ => UE_5.0}/Source/OpenPype/Private/OpenPypeStyle.cpp (100%) create mode 100644 openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/AssetContainer.h create mode 100644 openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/AssetContainerFactory.h rename openpype/hosts/unreal/integration/{ => UE_5.0}/Source/OpenPype/Public/OpenPype.h (100%) rename openpype/hosts/unreal/integration/{ => UE_5.0}/Source/OpenPype/Public/OpenPypeCommands.h (100%) create mode 100644 openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypeLib.h create mode 100644 openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypePublishInstance.h create mode 100644 openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypePublishInstanceFactory.h create mode 100644 openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypePythonBridge.h rename openpype/hosts/unreal/integration/{ => UE_5.0}/Source/OpenPype/Public/OpenPypeStyle.h (100%) delete mode 160000 repos/avalon-core diff --git a/openpype/hosts/unreal/__init__.py b/openpype/hosts/unreal/__init__.py index bedf5a29f7..ae9b113acd 100644 --- a/openpype/hosts/unreal/__init__.py +++ b/openpype/hosts/unreal/__init__.py @@ -1,13 +1,19 @@ import os import openpype.hosts +from openpype.lib.applications import Application -def add_implementation_envs(env, _app): +def add_implementation_envs(env: dict, _app: Application) -> None: """Modify environments to contain all required for implementation.""" # Set OPENPYPE_UNREAL_PLUGIN required for Unreal implementation + + engine_version = _app.name.split("/")[-1].replace("-", ".") + major_version = int(engine_version.split(".")[0]) + + ue_plugin = "UE_4.7" if major_version == 4 else "UE_5.0" unreal_plugin_path = os.path.join( os.path.dirname(os.path.abspath(openpype.hosts.__file__)), - "unreal", "integration" + "unreal", "integration", ue_plugin ) if not env.get("OPENPYPE_UNREAL_PLUGIN"): env["OPENPYPE_UNREAL_PLUGIN"] = unreal_plugin_path diff --git a/openpype/hosts/unreal/integration/.gitignore b/openpype/hosts/unreal/integration/UE_4.7/.gitignore similarity index 100% rename from openpype/hosts/unreal/integration/.gitignore rename to openpype/hosts/unreal/integration/UE_4.7/.gitignore diff --git a/openpype/hosts/unreal/integration/Content/Python/init_unreal.py b/openpype/hosts/unreal/integration/UE_4.7/Content/Python/init_unreal.py similarity index 100% rename from openpype/hosts/unreal/integration/Content/Python/init_unreal.py rename to openpype/hosts/unreal/integration/UE_4.7/Content/Python/init_unreal.py diff --git a/openpype/hosts/unreal/integration/OpenPype.uplugin b/openpype/hosts/unreal/integration/UE_4.7/OpenPype.uplugin similarity index 100% rename from openpype/hosts/unreal/integration/OpenPype.uplugin rename to openpype/hosts/unreal/integration/UE_4.7/OpenPype.uplugin diff --git a/openpype/hosts/unreal/integration/README.md b/openpype/hosts/unreal/integration/UE_4.7/README.md similarity index 91% rename from openpype/hosts/unreal/integration/README.md rename to openpype/hosts/unreal/integration/UE_4.7/README.md index a32d89aab8..a08c1ada39 100644 --- a/openpype/hosts/unreal/integration/README.md +++ b/openpype/hosts/unreal/integration/UE_4.7/README.md @@ -1,4 +1,4 @@ -# OpenPype Unreal Integration plugin +# OpenPype Unreal Integration plugin - UE 4.x This is plugin for Unreal Editor, creating menu for [OpenPype](https://github.com/getavalon) tools to run. diff --git a/openpype/hosts/unreal/integration/Resources/openpype128.png b/openpype/hosts/unreal/integration/UE_4.7/Resources/openpype128.png similarity index 100% rename from openpype/hosts/unreal/integration/Resources/openpype128.png rename to openpype/hosts/unreal/integration/UE_4.7/Resources/openpype128.png diff --git a/openpype/hosts/unreal/integration/Resources/openpype40.png b/openpype/hosts/unreal/integration/UE_4.7/Resources/openpype40.png similarity index 100% rename from openpype/hosts/unreal/integration/Resources/openpype40.png rename to openpype/hosts/unreal/integration/UE_4.7/Resources/openpype40.png diff --git a/openpype/hosts/unreal/integration/Resources/openpype512.png b/openpype/hosts/unreal/integration/UE_4.7/Resources/openpype512.png similarity index 100% rename from openpype/hosts/unreal/integration/Resources/openpype512.png rename to openpype/hosts/unreal/integration/UE_4.7/Resources/openpype512.png diff --git a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/OpenPype.Build.cs b/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/OpenPype.Build.cs new file mode 100644 index 0000000000..c30835b63d --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/OpenPype.Build.cs @@ -0,0 +1,57 @@ +// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. + +using UnrealBuildTool; + +public class OpenPype : ModuleRules +{ + public OpenPype(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; + + PublicIncludePaths.AddRange( + new string[] { + // ... add public include paths required here ... + } + ); + + + PrivateIncludePaths.AddRange( + new string[] { + // ... add other private include paths required here ... + } + ); + + + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core", + // ... add other public dependencies that you statically link with here ... + } + ); + + + PrivateDependencyModuleNames.AddRange( + new string[] + { + "Projects", + "InputCore", + "UnrealEd", + "LevelEditor", + "CoreUObject", + "Engine", + "Slate", + "SlateCore", + // ... add private dependencies that you statically link with here ... + } + ); + + + DynamicallyLoadedModuleNames.AddRange( + new string[] + { + // ... add any modules that your module loads dynamically here ... + } + ); + } +} diff --git a/openpype/hosts/unreal/integration/Source/OpenPype/Private/AssetContainer.cpp b/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/AssetContainer.cpp similarity index 100% rename from openpype/hosts/unreal/integration/Source/OpenPype/Private/AssetContainer.cpp rename to openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/AssetContainer.cpp diff --git a/openpype/hosts/unreal/integration/Source/OpenPype/Private/AssetContainerFactory.cpp b/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/AssetContainerFactory.cpp similarity index 100% rename from openpype/hosts/unreal/integration/Source/OpenPype/Private/AssetContainerFactory.cpp rename to openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/AssetContainerFactory.cpp diff --git a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/OpenPype.cpp b/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/OpenPype.cpp new file mode 100644 index 0000000000..15c46b3862 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/OpenPype.cpp @@ -0,0 +1,103 @@ +#include "OpenPype.h" +#include "LevelEditor.h" +#include "OpenPypePythonBridge.h" +#include "OpenPypeStyle.h" + + +static const FName OpenPypeTabName("OpenPype"); + +#define LOCTEXT_NAMESPACE "FOpenPypeModule" + +// This function is triggered when the plugin is staring up +void FOpenPypeModule::StartupModule() +{ + + FOpenPypeStyle::Initialize(); + FOpenPypeStyle::SetIcon("Logo", "openpype40"); + + // Create the Extender that will add content to the menu + FLevelEditorModule& LevelEditorModule = FModuleManager::LoadModuleChecked("LevelEditor"); + + TSharedPtr MenuExtender = MakeShareable(new FExtender()); + TSharedPtr ToolbarExtender = MakeShareable(new FExtender()); + + MenuExtender->AddMenuExtension( + "LevelEditor", + EExtensionHook::After, + NULL, + FMenuExtensionDelegate::CreateRaw(this, &FOpenPypeModule::AddMenuEntry) + ); + ToolbarExtender->AddToolBarExtension( + "Settings", + EExtensionHook::After, + NULL, + FToolBarExtensionDelegate::CreateRaw(this, &FOpenPypeModule::AddToobarEntry)); + + + LevelEditorModule.GetMenuExtensibilityManager()->AddExtender(MenuExtender); + LevelEditorModule.GetToolBarExtensibilityManager()->AddExtender(ToolbarExtender); + +} + +void FOpenPypeModule::ShutdownModule() +{ + FOpenPypeStyle::Shutdown(); +} + + +void FOpenPypeModule::AddMenuEntry(FMenuBuilder& MenuBuilder) +{ + // Create Section + MenuBuilder.BeginSection("OpenPype", TAttribute(FText::FromString("OpenPype"))); + { + // Create a Submenu inside of the Section + MenuBuilder.AddMenuEntry( + FText::FromString("Tools..."), + FText::FromString("Pipeline tools"), + FSlateIcon(FOpenPypeStyle::GetStyleSetName(), "OpenPype.Logo"), + FUIAction(FExecuteAction::CreateRaw(this, &FOpenPypeModule::MenuPopup)) + ); + + MenuBuilder.AddMenuEntry( + FText::FromString("Tools dialog..."), + FText::FromString("Pipeline tools dialog"), + FSlateIcon(FOpenPypeStyle::GetStyleSetName(), "OpenPype.Logo"), + FUIAction(FExecuteAction::CreateRaw(this, &FOpenPypeModule::MenuDialog)) + ); + + } + MenuBuilder.EndSection(); +} + +void FOpenPypeModule::AddToobarEntry(FToolBarBuilder& ToolbarBuilder) +{ + ToolbarBuilder.BeginSection(TEXT("OpenPype")); + { + ToolbarBuilder.AddToolBarButton( + FUIAction( + FExecuteAction::CreateRaw(this, &FOpenPypeModule::MenuPopup), + NULL, + FIsActionChecked() + + ), + NAME_None, + LOCTEXT("OpenPype_label", "OpenPype"), + LOCTEXT("OpenPype_tooltip", "OpenPype Tools"), + FSlateIcon(FOpenPypeStyle::GetStyleSetName(), "OpenPype.Logo") + ); + } + ToolbarBuilder.EndSection(); +} + + +void FOpenPypeModule::MenuPopup() { + UOpenPypePythonBridge* bridge = UOpenPypePythonBridge::Get(); + bridge->RunInPython_Popup(); +} + +void FOpenPypeModule::MenuDialog() { + UOpenPypePythonBridge* bridge = UOpenPypePythonBridge::Get(); + bridge->RunInPython_Dialog(); +} + +IMPLEMENT_MODULE(FOpenPypeModule, OpenPype) diff --git a/openpype/hosts/unreal/integration/Source/OpenPype/Private/OpenPypeLib.cpp b/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/OpenPypeLib.cpp similarity index 100% rename from openpype/hosts/unreal/integration/Source/OpenPype/Private/OpenPypeLib.cpp rename to openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/OpenPypeLib.cpp diff --git a/openpype/hosts/unreal/integration/Source/OpenPype/Private/OpenPypePublishInstance.cpp b/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/OpenPypePublishInstance.cpp similarity index 100% rename from openpype/hosts/unreal/integration/Source/OpenPype/Private/OpenPypePublishInstance.cpp rename to openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/OpenPypePublishInstance.cpp diff --git a/openpype/hosts/unreal/integration/Source/OpenPype/Private/OpenPypePublishInstanceFactory.cpp b/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/OpenPypePublishInstanceFactory.cpp similarity index 100% rename from openpype/hosts/unreal/integration/Source/OpenPype/Private/OpenPypePublishInstanceFactory.cpp rename to openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/OpenPypePublishInstanceFactory.cpp diff --git a/openpype/hosts/unreal/integration/Source/OpenPype/Private/OpenPypePythonBridge.cpp b/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/OpenPypePythonBridge.cpp similarity index 100% rename from openpype/hosts/unreal/integration/Source/OpenPype/Private/OpenPypePythonBridge.cpp rename to openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/OpenPypePythonBridge.cpp diff --git a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/OpenPypeStyle.cpp b/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/OpenPypeStyle.cpp new file mode 100644 index 0000000000..a51c2d6aa5 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/OpenPypeStyle.cpp @@ -0,0 +1,70 @@ +#include "OpenPypeStyle.h" +#include "Framework/Application/SlateApplication.h" +#include "Styling/SlateStyle.h" +#include "Styling/SlateStyleRegistry.h" + + +TUniquePtr< FSlateStyleSet > FOpenPypeStyle::OpenPypeStyleInstance = nullptr; + +void FOpenPypeStyle::Initialize() +{ + if (!OpenPypeStyleInstance.IsValid()) + { + OpenPypeStyleInstance = Create(); + FSlateStyleRegistry::RegisterSlateStyle(*OpenPypeStyleInstance); + } +} + +void FOpenPypeStyle::Shutdown() +{ + if (OpenPypeStyleInstance.IsValid()) + { + FSlateStyleRegistry::UnRegisterSlateStyle(*OpenPypeStyleInstance); + OpenPypeStyleInstance.Reset(); + } +} + +FName FOpenPypeStyle::GetStyleSetName() +{ + static FName StyleSetName(TEXT("OpenPypeStyle")); + return StyleSetName; +} + +FName FOpenPypeStyle::GetContextName() +{ + static FName ContextName(TEXT("OpenPype")); + return ContextName; +} + +#define IMAGE_BRUSH(RelativePath, ...) FSlateImageBrush( Style->RootToContentDir( RelativePath, TEXT(".png") ), __VA_ARGS__ ) + +const FVector2D Icon40x40(40.0f, 40.0f); + +TUniquePtr< FSlateStyleSet > FOpenPypeStyle::Create() +{ + TUniquePtr< FSlateStyleSet > Style = MakeUnique(GetStyleSetName()); + Style->SetContentRoot(FPaths::ProjectPluginsDir() / TEXT("OpenPype/Resources")); + + return Style; +} + +void FOpenPypeStyle::SetIcon(const FString& StyleName, const FString& ResourcePath) +{ + FSlateStyleSet* Style = OpenPypeStyleInstance.Get(); + + FString Name(GetContextName().ToString()); + Name = Name + "." + StyleName; + Style->Set(*Name, new FSlateImageBrush(Style->RootToContentDir(ResourcePath, TEXT(".png")), Icon40x40)); + + + FSlateApplication::Get().GetRenderer()->ReloadTextureResources(); +} + +#undef IMAGE_BRUSH + +const ISlateStyle& FOpenPypeStyle::Get() +{ + check(OpenPypeStyleInstance); + return *OpenPypeStyleInstance; + return *OpenPypeStyleInstance; +} diff --git a/openpype/hosts/unreal/integration/Source/OpenPype/Public/AssetContainer.h b/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/AssetContainer.h similarity index 100% rename from openpype/hosts/unreal/integration/Source/OpenPype/Public/AssetContainer.h rename to openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/AssetContainer.h diff --git a/openpype/hosts/unreal/integration/Source/OpenPype/Public/AssetContainerFactory.h b/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/AssetContainerFactory.h similarity index 100% rename from openpype/hosts/unreal/integration/Source/OpenPype/Public/AssetContainerFactory.h rename to openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/AssetContainerFactory.h diff --git a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/OpenPype.h b/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/OpenPype.h new file mode 100644 index 0000000000..db3f299354 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/OpenPype.h @@ -0,0 +1,21 @@ +// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "Engine.h" + + +class FOpenPypeModule : public IModuleInterface +{ +public: + virtual void StartupModule() override; + virtual void ShutdownModule() override; + +private: + + void AddMenuEntry(FMenuBuilder& MenuBuilder); + void AddToobarEntry(FToolBarBuilder& ToolbarBuilder); + void MenuPopup(); + void MenuDialog(); + +}; diff --git a/openpype/hosts/unreal/integration/Source/OpenPype/Public/OpenPypeLib.h b/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/OpenPypeLib.h similarity index 100% rename from openpype/hosts/unreal/integration/Source/OpenPype/Public/OpenPypeLib.h rename to openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/OpenPypeLib.h diff --git a/openpype/hosts/unreal/integration/Source/OpenPype/Public/OpenPypePublishInstance.h b/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/OpenPypePublishInstance.h similarity index 100% rename from openpype/hosts/unreal/integration/Source/OpenPype/Public/OpenPypePublishInstance.h rename to openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/OpenPypePublishInstance.h diff --git a/openpype/hosts/unreal/integration/Source/OpenPype/Public/OpenPypePublishInstanceFactory.h b/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/OpenPypePublishInstanceFactory.h similarity index 100% rename from openpype/hosts/unreal/integration/Source/OpenPype/Public/OpenPypePublishInstanceFactory.h rename to openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/OpenPypePublishInstanceFactory.h diff --git a/openpype/hosts/unreal/integration/Source/OpenPype/Public/OpenPypePythonBridge.h b/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/OpenPypePythonBridge.h similarity index 100% rename from openpype/hosts/unreal/integration/Source/OpenPype/Public/OpenPypePythonBridge.h rename to openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/OpenPypePythonBridge.h diff --git a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/OpenPypeStyle.h b/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/OpenPypeStyle.h new file mode 100644 index 0000000000..fbc8bcdd5b --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/OpenPypeStyle.h @@ -0,0 +1,22 @@ +#pragma once +#include "CoreMinimal.h" + +class FSlateStyleSet; +class ISlateStyle; + + +class FOpenPypeStyle +{ +public: + static void Initialize(); + static void Shutdown(); + static const ISlateStyle& Get(); + static FName GetStyleSetName(); + static FName GetContextName(); + + static void SetIcon(const FString& StyleName, const FString& ResourcePath); + +private: + static TUniquePtr< FSlateStyleSet > Create(); + static TUniquePtr< FSlateStyleSet > OpenPypeStyleInstance; +}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/.gitignore b/openpype/hosts/unreal/integration/UE_5.0/.gitignore new file mode 100644 index 0000000000..b32a6f55e5 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/.gitignore @@ -0,0 +1,35 @@ +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + +/Binaries +/Intermediate diff --git a/openpype/hosts/unreal/integration/UE_5.0/Content/Python/__init__.py b/openpype/hosts/unreal/integration/UE_5.0/Content/Python/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openpype/hosts/unreal/integration/UE_5.0/Content/Python/init_unreal.py b/openpype/hosts/unreal/integration/UE_5.0/Content/Python/init_unreal.py new file mode 100644 index 0000000000..4bb03b07ed --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/Content/Python/init_unreal.py @@ -0,0 +1,28 @@ +import unreal + +openpype_detected = True +try: + from openpype.pipeline import install_host + from openpype.hosts.unreal import api as openpype_host +except ImportError as exc: + openpype_host = None + openpype_detected = False + unreal.log_error("OpenPype: cannot load OpenPype [ {} ]".format(exc)) + +if openpype_detected: + install_host(openpype_host) + + +@unreal.uclass() +class OpenPypeIntegration(unreal.OpenPypePythonBridge): + @unreal.ufunction(override=True) + def RunInPython_Popup(self): + unreal.log_warning("OpenPype: showing tools popup") + if openpype_detected: + openpype_host.show_tools_popup() + + @unreal.ufunction(override=True) + def RunInPython_Dialog(self): + unreal.log_warning("OpenPype: showing tools dialog") + if openpype_detected: + openpype_host.show_tools_dialog() diff --git a/openpype/hosts/unreal/integration/UE_5.0/Content/__init__.py b/openpype/hosts/unreal/integration/UE_5.0/Content/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype.uplugin b/openpype/hosts/unreal/integration/UE_5.0/OpenPype.uplugin new file mode 100644 index 0000000000..4c7a74403c --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype.uplugin @@ -0,0 +1,24 @@ +{ + "FileVersion": 3, + "Version": 1, + "VersionName": "1.0", + "FriendlyName": "OpenPype", + "Description": "OpenPype Integration", + "Category": "OpenPype.Integration", + "CreatedBy": "Ondrej Samohel", + "CreatedByURL": "https://openpype.io", + "DocsURL": "https://openpype.io/docs/artist_hosts_unreal", + "MarketplaceURL": "", + "SupportURL": "https://pype.club/", + "CanContainContent": true, + "IsBetaVersion": true, + "IsExperimentalVersion": false, + "Installed": false, + "Modules": [ + { + "Name": "OpenPype", + "Type": "Editor", + "LoadingPhase": "Default" + } + ] +} \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/README.md b/openpype/hosts/unreal/integration/UE_5.0/README.md new file mode 100644 index 0000000000..cf0aa622c2 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/README.md @@ -0,0 +1,11 @@ +# OpenPype Unreal Integration plugin - UE 5.x + +This is plugin for Unreal Editor, creating menu for [OpenPype](https://github.com/getavalon) tools to run. + +## How does this work + +Plugin is creating basic menu items in **Window/OpenPype** section of Unreal Editor main menu and a button +on the main toolbar with associated menu. Clicking on those menu items is calling callbacks that are +declared in C++ but needs to be implemented during Unreal Editor +startup in `Plugins/OpenPype/Content/Python/init_unreal.py` - this should be executed by Unreal Editor +automatically. diff --git a/openpype/hosts/unreal/integration/UE_5.0/Resources/openpype128.png b/openpype/hosts/unreal/integration/UE_5.0/Resources/openpype128.png new file mode 100644 index 0000000000000000000000000000000000000000..abe8a807ef40f00b75d7446d020a2437732c7583 GIT binary patch literal 14594 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 literal 0 HcmV?d00001 diff --git a/openpype/hosts/unreal/integration/UE_5.0/Resources/openpype512.png b/openpype/hosts/unreal/integration/UE_5.0/Resources/openpype512.png new file mode 100644 index 0000000000000000000000000000000000000000..97c4d4326bc16ba6dfb45d35c4362d8bc15900ae GIT binary patch literal 85856 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 bCJFu@j}?W0K?Ok;00000NkvXXu0mjf9`4Bp literal 0 HcmV?d00001 diff --git a/openpype/hosts/unreal/integration/Source/OpenPype/OpenPype.Build.cs b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/OpenPype.Build.cs similarity index 100% rename from openpype/hosts/unreal/integration/Source/OpenPype/OpenPype.Build.cs rename to openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/OpenPype.Build.cs diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/AssetContainer.cpp b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/AssetContainer.cpp new file mode 100644 index 0000000000..c766f87a8e --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/AssetContainer.cpp @@ -0,0 +1,115 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#include "AssetContainer.h" +#include "AssetRegistryModule.h" +#include "Misc/PackageName.h" +#include "Engine.h" +#include "Containers/UnrealString.h" + +UAssetContainer::UAssetContainer(const FObjectInitializer& ObjectInitializer) +: UAssetUserData(ObjectInitializer) +{ + FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); + FString path = UAssetContainer::GetPathName(); + UE_LOG(LogTemp, Warning, TEXT("UAssetContainer %s"), *path); + FARFilter Filter; + Filter.PackagePaths.Add(FName(*path)); + + AssetRegistryModule.Get().OnAssetAdded().AddUObject(this, &UAssetContainer::OnAssetAdded); + AssetRegistryModule.Get().OnAssetRemoved().AddUObject(this, &UAssetContainer::OnAssetRemoved); + AssetRegistryModule.Get().OnAssetRenamed().AddUObject(this, &UAssetContainer::OnAssetRenamed); +} + +void UAssetContainer::OnAssetAdded(const FAssetData& AssetData) +{ + TArray split; + + // get directory of current container + FString selfFullPath = UAssetContainer::GetPathName(); + FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath); + + // get asset path and class + FString assetPath = AssetData.GetFullName(); + FString assetFName = AssetData.AssetClass.ToString(); + + // split path + assetPath.ParseIntoArray(split, TEXT(" "), true); + + FString assetDir = FPackageName::GetLongPackagePath(*split[1]); + + // take interest only in paths starting with path of current container + if (assetDir.StartsWith(*selfDir)) + { + // exclude self + if (assetFName != "AssetContainer") + { + assets.Add(assetPath); + assetsData.Add(AssetData); + UE_LOG(LogTemp, Log, TEXT("%s: asset added to %s"), *selfFullPath, *selfDir); + } + } +} + +void UAssetContainer::OnAssetRemoved(const FAssetData& AssetData) +{ + TArray split; + + // get directory of current container + FString selfFullPath = UAssetContainer::GetPathName(); + FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath); + + // get asset path and class + FString assetPath = AssetData.GetFullName(); + FString assetFName = AssetData.AssetClass.ToString(); + + // split path + assetPath.ParseIntoArray(split, TEXT(" "), true); + + FString assetDir = FPackageName::GetLongPackagePath(*split[1]); + + // take interest only in paths starting with path of current container + FString path = UAssetContainer::GetPathName(); + FString lpp = FPackageName::GetLongPackagePath(*path); + + if (assetDir.StartsWith(*selfDir)) + { + // exclude self + if (assetFName != "AssetContainer") + { + // UE_LOG(LogTemp, Warning, TEXT("%s: asset removed"), *lpp); + assets.Remove(assetPath); + assetsData.Remove(AssetData); + } + } +} + +void UAssetContainer::OnAssetRenamed(const FAssetData& AssetData, const FString& str) +{ + TArray split; + + // get directory of current container + FString selfFullPath = UAssetContainer::GetPathName(); + FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath); + + // get asset path and class + FString assetPath = AssetData.GetFullName(); + FString assetFName = AssetData.AssetClass.ToString(); + + // split path + assetPath.ParseIntoArray(split, TEXT(" "), true); + + FString assetDir = FPackageName::GetLongPackagePath(*split[1]); + if (assetDir.StartsWith(*selfDir)) + { + // exclude self + if (assetFName != "AssetContainer") + { + + assets.Remove(str); + assets.Add(assetPath); + assetsData.Remove(AssetData); + // UE_LOG(LogTemp, Warning, TEXT("%s: asset renamed %s"), *lpp, *str); + } + } +} + diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/AssetContainerFactory.cpp b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/AssetContainerFactory.cpp new file mode 100644 index 0000000000..b943150bdd --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/AssetContainerFactory.cpp @@ -0,0 +1,20 @@ +#include "AssetContainerFactory.h" +#include "AssetContainer.h" + +UAssetContainerFactory::UAssetContainerFactory(const FObjectInitializer& ObjectInitializer) + : UFactory(ObjectInitializer) +{ + SupportedClass = UAssetContainer::StaticClass(); + bCreateNew = false; + bEditorImport = true; +} + +UObject* UAssetContainerFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) +{ + UAssetContainer* AssetContainer = NewObject(InParent, Class, Name, Flags); + return AssetContainer; +} + +bool UAssetContainerFactory::ShouldShowInNewMenu() const { + return false; +} diff --git a/openpype/hosts/unreal/integration/Source/OpenPype/Private/OpenPype.cpp b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPype.cpp similarity index 100% rename from openpype/hosts/unreal/integration/Source/OpenPype/Private/OpenPype.cpp rename to openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPype.cpp diff --git a/openpype/hosts/unreal/integration/Source/OpenPype/Private/OpenPypeCommands.cpp b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeCommands.cpp similarity index 100% rename from openpype/hosts/unreal/integration/Source/OpenPype/Private/OpenPypeCommands.cpp rename to openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeCommands.cpp diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeLib.cpp b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeLib.cpp new file mode 100644 index 0000000000..5facab7b8b --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeLib.cpp @@ -0,0 +1,48 @@ +#include "OpenPypeLib.h" +#include "Misc/Paths.h" +#include "Misc/ConfigCacheIni.h" +#include "UObject/UnrealType.h" + +/** + * Sets color on folder icon on given path + * @param InPath - path to folder + * @param InFolderColor - color of the folder + * @warning This color will appear only after Editor restart. Is there a better way? + */ + +void UOpenPypeLib::CSetFolderColor(FString FolderPath, FLinearColor FolderColor, bool bForceAdd) +{ + auto SaveColorInternal = [](FString InPath, FLinearColor InFolderColor) + { + // Saves the color of the folder to the config + if (FPaths::FileExists(GEditorPerProjectIni)) + { + GConfig->SetString(TEXT("PathColor"), *InPath, *InFolderColor.ToString(), GEditorPerProjectIni); + } + + }; + + SaveColorInternal(FolderPath, FolderColor); + +} +/** + * Returns all poperties on given object + * @param cls - class + * @return TArray of properties + */ +TArray UOpenPypeLib::GetAllProperties(UClass* cls) +{ + TArray Ret; + if (cls != nullptr) + { + for (TFieldIterator It(cls); It; ++It) + { + FProperty* Property = *It; + if (Property->HasAnyPropertyFlags(EPropertyFlags::CPF_Edit)) + { + Ret.Add(Property->GetName()); + } + } + } + return Ret; +} diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypePublishInstance.cpp b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypePublishInstance.cpp new file mode 100644 index 0000000000..4f1e846c0b --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypePublishInstance.cpp @@ -0,0 +1,108 @@ +#pragma once + +#include "OpenPypePublishInstance.h" +#include "AssetRegistryModule.h" + + +UOpenPypePublishInstance::UOpenPypePublishInstance(const FObjectInitializer& ObjectInitializer) + : UObject(ObjectInitializer) +{ + FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); + FString path = UOpenPypePublishInstance::GetPathName(); + FARFilter Filter; + Filter.PackagePaths.Add(FName(*path)); + + AssetRegistryModule.Get().OnAssetAdded().AddUObject(this, &UOpenPypePublishInstance::OnAssetAdded); + AssetRegistryModule.Get().OnAssetRemoved().AddUObject(this, &UOpenPypePublishInstance::OnAssetRemoved); + AssetRegistryModule.Get().OnAssetRenamed().AddUObject(this, &UOpenPypePublishInstance::OnAssetRenamed); +} + +void UOpenPypePublishInstance::OnAssetAdded(const FAssetData& AssetData) +{ + TArray split; + + // get directory of current container + FString selfFullPath = UOpenPypePublishInstance::GetPathName(); + FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath); + + // get asset path and class + FString assetPath = AssetData.GetFullName(); + FString assetFName = AssetData.AssetClass.ToString(); + + // split path + assetPath.ParseIntoArray(split, TEXT(" "), true); + + FString assetDir = FPackageName::GetLongPackagePath(*split[1]); + + // take interest only in paths starting with path of current container + if (assetDir.StartsWith(*selfDir)) + { + // exclude self + if (assetFName != "OpenPypePublishInstance") + { + assets.Add(assetPath); + UE_LOG(LogTemp, Log, TEXT("%s: asset added to %s"), *selfFullPath, *selfDir); + } + } +} + +void UOpenPypePublishInstance::OnAssetRemoved(const FAssetData& AssetData) +{ + TArray split; + + // get directory of current container + FString selfFullPath = UOpenPypePublishInstance::GetPathName(); + FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath); + + // get asset path and class + FString assetPath = AssetData.GetFullName(); + FString assetFName = AssetData.AssetClass.ToString(); + + // split path + assetPath.ParseIntoArray(split, TEXT(" "), true); + + FString assetDir = FPackageName::GetLongPackagePath(*split[1]); + + // take interest only in paths starting with path of current container + FString path = UOpenPypePublishInstance::GetPathName(); + FString lpp = FPackageName::GetLongPackagePath(*path); + + if (assetDir.StartsWith(*selfDir)) + { + // exclude self + if (assetFName != "OpenPypePublishInstance") + { + // UE_LOG(LogTemp, Warning, TEXT("%s: asset removed"), *lpp); + assets.Remove(assetPath); + } + } +} + +void UOpenPypePublishInstance::OnAssetRenamed(const FAssetData& AssetData, const FString& str) +{ + TArray split; + + // get directory of current container + FString selfFullPath = UOpenPypePublishInstance::GetPathName(); + FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath); + + // get asset path and class + FString assetPath = AssetData.GetFullName(); + FString assetFName = AssetData.AssetClass.ToString(); + + // split path + assetPath.ParseIntoArray(split, TEXT(" "), true); + + FString assetDir = FPackageName::GetLongPackagePath(*split[1]); + if (assetDir.StartsWith(*selfDir)) + { + // exclude self + if (assetFName != "AssetContainer") + { + + assets.Remove(str); + assets.Add(assetPath); + // UE_LOG(LogTemp, Warning, TEXT("%s: asset renamed %s"), *lpp, *str); + } + } +} diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypePublishInstanceFactory.cpp b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypePublishInstanceFactory.cpp new file mode 100644 index 0000000000..e61964c689 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypePublishInstanceFactory.cpp @@ -0,0 +1,20 @@ +#include "OpenPypePublishInstanceFactory.h" +#include "OpenPypePublishInstance.h" + +UOpenPypePublishInstanceFactory::UOpenPypePublishInstanceFactory(const FObjectInitializer& ObjectInitializer) + : UFactory(ObjectInitializer) +{ + SupportedClass = UOpenPypePublishInstance::StaticClass(); + bCreateNew = false; + bEditorImport = true; +} + +UObject* UOpenPypePublishInstanceFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) +{ + UOpenPypePublishInstance* OpenPypePublishInstance = NewObject(InParent, Class, Name, Flags); + return OpenPypePublishInstance; +} + +bool UOpenPypePublishInstanceFactory::ShouldShowInNewMenu() const { + return false; +} diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypePythonBridge.cpp b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypePythonBridge.cpp new file mode 100644 index 0000000000..8113231503 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypePythonBridge.cpp @@ -0,0 +1,13 @@ +#include "OpenPypePythonBridge.h" + +UOpenPypePythonBridge* UOpenPypePythonBridge::Get() +{ + TArray OpenPypePythonBridgeClasses; + GetDerivedClasses(UOpenPypePythonBridge::StaticClass(), OpenPypePythonBridgeClasses); + int32 NumClasses = OpenPypePythonBridgeClasses.Num(); + if (NumClasses > 0) + { + return Cast(OpenPypePythonBridgeClasses[NumClasses - 1]->GetDefaultObject()); + } + return nullptr; +}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/Source/OpenPype/Private/OpenPypeStyle.cpp b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeStyle.cpp similarity index 100% rename from openpype/hosts/unreal/integration/Source/OpenPype/Private/OpenPypeStyle.cpp rename to openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeStyle.cpp diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/AssetContainer.h b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/AssetContainer.h new file mode 100644 index 0000000000..3c2a360c78 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/AssetContainer.h @@ -0,0 +1,39 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/NoExportTypes.h" +#include "Engine/AssetUserData.h" +#include "AssetData.h" +#include "AssetContainer.generated.h" + +/** + * + */ +UCLASS(Blueprintable) +class OPENPYPE_API UAssetContainer : public UAssetUserData +{ + GENERATED_BODY() + +public: + + UAssetContainer(const FObjectInitializer& ObjectInitalizer); + // ~UAssetContainer(); + + UPROPERTY(EditAnywhere, BlueprintReadOnly) + TArray assets; + + // There seems to be no reflection option to expose array of FAssetData + /* + UPROPERTY(Transient, BlueprintReadOnly, Category = "Python", meta=(DisplayName="Assets Data")) + TArray assetsData; + */ +private: + TArray assetsData; + void OnAssetAdded(const FAssetData& AssetData); + void OnAssetRemoved(const FAssetData& AssetData); + void OnAssetRenamed(const FAssetData& AssetData, const FString& str); +}; + + diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/AssetContainerFactory.h b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/AssetContainerFactory.h new file mode 100644 index 0000000000..331ce6bb50 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/AssetContainerFactory.h @@ -0,0 +1,21 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "Factories/Factory.h" +#include "AssetContainerFactory.generated.h" + +/** + * + */ +UCLASS() +class OPENPYPE_API UAssetContainerFactory : public UFactory +{ + GENERATED_BODY() + +public: + UAssetContainerFactory(const FObjectInitializer& ObjectInitializer); + virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; + virtual bool ShouldShowInNewMenu() const override; +}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/Source/OpenPype/Public/OpenPype.h b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPype.h similarity index 100% rename from openpype/hosts/unreal/integration/Source/OpenPype/Public/OpenPype.h rename to openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPype.h diff --git a/openpype/hosts/unreal/integration/Source/OpenPype/Public/OpenPypeCommands.h b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypeCommands.h similarity index 100% rename from openpype/hosts/unreal/integration/Source/OpenPype/Public/OpenPypeCommands.h rename to openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypeCommands.h diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypeLib.h b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypeLib.h new file mode 100644 index 0000000000..59e9c8bd76 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypeLib.h @@ -0,0 +1,19 @@ +#pragma once + +#include "Engine.h" +#include "OpenPypeLib.generated.h" + + +UCLASS(Blueprintable) +class OPENPYPE_API UOpenPypeLib : public UObject +{ + + GENERATED_BODY() + +public: + UFUNCTION(BlueprintCallable, Category = Python) + static void CSetFolderColor(FString FolderPath, FLinearColor FolderColor, bool bForceAdd); + + UFUNCTION(BlueprintCallable, Category = Python) + static TArray GetAllProperties(UClass* cls); +}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypePublishInstance.h b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypePublishInstance.h new file mode 100644 index 0000000000..0a27a078d7 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypePublishInstance.h @@ -0,0 +1,21 @@ +#pragma once + +#include "Engine.h" +#include "OpenPypePublishInstance.generated.h" + + +UCLASS(Blueprintable) +class OPENPYPE_API UOpenPypePublishInstance : public UObject +{ + GENERATED_BODY() + +public: + UOpenPypePublishInstance(const FObjectInitializer& ObjectInitalizer); + + UPROPERTY(EditAnywhere, BlueprintReadOnly) + TArray assets; +private: + void OnAssetAdded(const FAssetData& AssetData); + void OnAssetRemoved(const FAssetData& AssetData); + void OnAssetRenamed(const FAssetData& AssetData, const FString& str); +}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypePublishInstanceFactory.h b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypePublishInstanceFactory.h new file mode 100644 index 0000000000..a2b3abe13e --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypePublishInstanceFactory.h @@ -0,0 +1,19 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Factories/Factory.h" +#include "OpenPypePublishInstanceFactory.generated.h" + +/** + * + */ +UCLASS() +class OPENPYPE_API UOpenPypePublishInstanceFactory : public UFactory +{ + GENERATED_BODY() + +public: + UOpenPypePublishInstanceFactory(const FObjectInitializer& ObjectInitializer); + virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; + virtual bool ShouldShowInNewMenu() const override; +}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypePythonBridge.h b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypePythonBridge.h new file mode 100644 index 0000000000..692aab2e5e --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypePythonBridge.h @@ -0,0 +1,20 @@ +#pragma once +#include "Engine.h" +#include "OpenPypePythonBridge.generated.h" + +UCLASS(Blueprintable) +class UOpenPypePythonBridge : public UObject +{ + GENERATED_BODY() + +public: + UFUNCTION(BlueprintCallable, Category = Python) + static UOpenPypePythonBridge* Get(); + + UFUNCTION(BlueprintImplementableEvent, Category = Python) + void RunInPython_Popup() const; + + UFUNCTION(BlueprintImplementableEvent, Category = Python) + void RunInPython_Dialog() const; + +}; diff --git a/openpype/hosts/unreal/integration/Source/OpenPype/Public/OpenPypeStyle.h b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypeStyle.h similarity index 100% rename from openpype/hosts/unreal/integration/Source/OpenPype/Public/OpenPypeStyle.h rename to openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypeStyle.h diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index 2b0de44fa9..d17674ea2c 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -1243,9 +1243,19 @@ "host_name": "unreal", "environment": {}, "variants": { - "4-26": { + "4-27": { "use_python_2": false, "environment": {} + }, + "5-0": { + "use_python_2": false, + "environment": { + "UE_PYTHONPATH": "{PYTHONPATH}" + } + }, + "__dynamic_keys_labels__": { + "4-27": "4.27", + "5-0": "5.0" } } }, diff --git a/repos/avalon-core b/repos/avalon-core deleted file mode 160000 index 2fa14cea6f..0000000000 --- a/repos/avalon-core +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 2fa14cea6f6a9d86eec70bbb96860cbe4c75c8eb From 2b1079be32264ae78d4cb70a10e9c10960db7d6e Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 26 May 2022 14:10:46 +0200 Subject: [PATCH 369/398] :recycle: simplify version determination --- openpype/hosts/unreal/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/__init__.py b/openpype/hosts/unreal/__init__.py index ae9b113acd..9c0768b78e 100644 --- a/openpype/hosts/unreal/__init__.py +++ b/openpype/hosts/unreal/__init__.py @@ -10,7 +10,7 @@ def add_implementation_envs(env: dict, _app: Application) -> None: engine_version = _app.name.split("/")[-1].replace("-", ".") major_version = int(engine_version.split(".")[0]) - ue_plugin = "UE_4.7" if major_version == 4 else "UE_5.0" + ue_plugin = "UE_5.0" if _app.name[:1] == "5" else "UE_4.7" unreal_plugin_path = os.path.join( os.path.dirname(os.path.abspath(openpype.hosts.__file__)), "unreal", "integration", ue_plugin From 067058f5d344a9ab940cd33ff0f3ab4436e74dbb Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 26 May 2022 14:13:25 +0200 Subject: [PATCH 370/398] :recycle: hound fixes --- openpype/hosts/unreal/__init__.py | 1 - openpype/hosts/unreal/lib.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/unreal/__init__.py b/openpype/hosts/unreal/__init__.py index 9c0768b78e..e0e1f0bc3d 100644 --- a/openpype/hosts/unreal/__init__.py +++ b/openpype/hosts/unreal/__init__.py @@ -8,7 +8,6 @@ def add_implementation_envs(env: dict, _app: Application) -> None: # Set OPENPYPE_UNREAL_PLUGIN required for Unreal implementation engine_version = _app.name.split("/")[-1].replace("-", ".") - major_version = int(engine_version.split(".")[0]) ue_plugin = "UE_5.0" if _app.name[:1] == "5" else "UE_4.7" unreal_plugin_path = os.path.join( diff --git a/openpype/hosts/unreal/lib.py b/openpype/hosts/unreal/lib.py index f220d8dedf..8c453b38b9 100644 --- a/openpype/hosts/unreal/lib.py +++ b/openpype/hosts/unreal/lib.py @@ -301,8 +301,8 @@ def create_unreal_project(project_name: str, raise NotImplementedError("Unsupported platform") if not python_path.exists(): raise RuntimeError(f"Unreal Python not found at {python_path}") - out = subprocess.check_call( - [python_path.as_posix(), "-m", "pip", "install", "--user", "pyside2"]) + subprocess.check_call( + [python_path.as_posix(), "-m", "pip", "install", "pyside2"]) if dev_mode or preset["dev_mode"]: _prepare_cpp_project(project_file, engine_path, ue_version) From 9a5dce42af1d1d9befc021ca1326f2e2a3a2fdc7 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 26 May 2022 14:17:41 +0200 Subject: [PATCH 371/398] :recycle: hound fixes #2 :dog: --- openpype/hosts/unreal/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/unreal/__init__.py b/openpype/hosts/unreal/__init__.py index e0e1f0bc3d..10e9c5100e 100644 --- a/openpype/hosts/unreal/__init__.py +++ b/openpype/hosts/unreal/__init__.py @@ -7,8 +7,6 @@ def add_implementation_envs(env: dict, _app: Application) -> None: """Modify environments to contain all required for implementation.""" # Set OPENPYPE_UNREAL_PLUGIN required for Unreal implementation - engine_version = _app.name.split("/")[-1].replace("-", ".") - ue_plugin = "UE_5.0" if _app.name[:1] == "5" else "UE_4.7" unreal_plugin_path = os.path.join( os.path.dirname(os.path.abspath(openpype.hosts.__file__)), From e4878eac8aecf8993b5b690401539fba88bb182c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 26 May 2022 14:25:51 +0200 Subject: [PATCH 372/398] Flame: make sure repre name is first segment from tokenizable str --- .../flame/plugins/publish/extract_subset_resources.py | 8 ++++++-- 1 file changed, 6 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 eea575ea88..1bfe980a01 100644 --- a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py +++ b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py @@ -232,10 +232,14 @@ class ExtractSubsetResources(openpype.api.Extractor): opfapi.export_clip( export_dir_path, exporting_clip, preset_path, **export_kwargs) + # make sure only first segment is used if underscore in name + # HACK: `ftrackreview_withLUT` will result only in `ftrackreview` + repr_name = unique_name.split("_")[0] + # create representation data representation_data = { - "name": unique_name, - "outputName": unique_name, + "name": repr_name, + "outputName": repr_name, "ext": extension, "stagingDir": export_dir_path, "tags": repre_tags, From 4fbdefdb6c95b461cbe43e70a0e3de6c6ec7992e Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 26 May 2022 14:43:48 +0200 Subject: [PATCH 373/398] :recycle: fps from asset, few style changes --- .../unreal/plugins/load/load_animation.py | 25 +++++------- .../hosts/unreal/plugins/load/load_layout.py | 39 +++++++++---------- 2 files changed, 29 insertions(+), 35 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_animation.py b/openpype/hosts/unreal/plugins/load/load_animation.py index 54b43c500c..da2830bc52 100644 --- a/openpype/hosts/unreal/plugins/load/load_animation.py +++ b/openpype/hosts/unreal/plugins/load/load_animation.py @@ -14,6 +14,7 @@ from openpype.pipeline import ( ) from openpype.hosts.unreal.api import plugin from openpype.hosts.unreal.api import pipeline as unreal_pipeline +from openpype.api import get_asset class AnimationFBXLoader(plugin.Loader): @@ -79,7 +80,7 @@ class AnimationFBXLoader(plugin.Loader): task.options.anim_sequence_import_data.set_editor_property( 'use_default_sample_rate', False) task.options.anim_sequence_import_data.set_editor_property( - 'custom_sample_rate', 25.0) # TODO: get from database + 'custom_sample_rate', get_asset()["data"].get("fps")) task.options.anim_sequence_import_data.set_editor_property( 'import_custom_attribute', True) task.options.anim_sequence_import_data.set_editor_property( @@ -141,22 +142,18 @@ class AnimationFBXLoader(plugin.Loader): root = "/Game/OpenPype" asset = context.get('asset').get('name') suffix = "_CON" - if asset: - asset_name = "{}_{}".format(asset, name) - else: - asset_name = "{}".format(name) - + asset_name = f"{asset}_{name}" if asset else f"{name}" tools = unreal.AssetToolsHelpers().get_asset_tools() asset_dir, container_name = tools.create_unique_asset_name( f"{root}/Animations/{asset}/{name}", suffix="") ar = unreal.AssetRegistryHelpers.get_asset_registry() - filter = unreal.ARFilter( + _filter = unreal.ARFilter( class_names=["World"], package_paths=[f"{root}/{hierarchy[0]}"], recursive_paths=False) - levels = ar.get_assets(filter) + levels = ar.get_assets(_filter) master_level = levels[0].get_editor_property('object_path') hierarchy_dir = root @@ -164,11 +161,11 @@ class AnimationFBXLoader(plugin.Loader): hierarchy_dir = f"{hierarchy_dir}/{h}" hierarchy_dir = f"{hierarchy_dir}/{asset}" - filter = unreal.ARFilter( + _filter = unreal.ARFilter( class_names=["World"], package_paths=[f"{hierarchy_dir}/"], recursive_paths=True) - levels = ar.get_assets(filter) + levels = ar.get_assets(_filter) level = levels[0].get_editor_property('object_path') unreal.EditorLevelLibrary.save_all_dirty_levels() @@ -235,8 +232,7 @@ class AnimationFBXLoader(plugin.Loader): "parent": context["representation"]["parent"], "family": context["representation"]["context"]["family"] } - unreal_pipeline.imprint( - "{}/{}".format(asset_dir, container_name), data) + unreal_pipeline.imprint(f"{asset_dir}/{container_name}", data) imported_content = EditorAssetLibrary.list_assets( asset_dir, recursive=True, include_folder=False) @@ -283,7 +279,7 @@ class AnimationFBXLoader(plugin.Loader): task.options.anim_sequence_import_data.set_editor_property( 'use_default_sample_rate', False) task.options.anim_sequence_import_data.set_editor_property( - 'custom_sample_rate', 25.0) # TODO: get from database + 'custom_sample_rate', get_asset()["data"].get("fps")) task.options.anim_sequence_import_data.set_editor_property( 'import_custom_attribute', True) task.options.anim_sequence_import_data.set_editor_property( @@ -300,8 +296,7 @@ class AnimationFBXLoader(plugin.Loader): # do import fbx and replace existing data unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) - container_path = "{}/{}".format(container["namespace"], - container["objectName"]) + container_path = f'{container["namespace"]}/{container["objectName"]}' # update metadata unreal_pipeline.imprint( container_path, diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index 49611c6c05..0632c3c0b5 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -20,6 +20,7 @@ from openpype.pipeline import ( AVALON_CONTAINER_ID, legacy_io, ) +from openpype.api import get_asset from openpype.hosts.unreal.api import plugin from openpype.hosts.unreal.api import pipeline as unreal_pipeline @@ -87,7 +88,8 @@ class LayoutLoader(plugin.Loader): return None - def _get_data(self, asset_name): + @staticmethod + def _get_data(asset_name): asset_doc = legacy_io.find_one({ "type": "asset", "name": asset_name @@ -95,8 +97,9 @@ class LayoutLoader(plugin.Loader): return asset_doc.get("data") + @staticmethod def _set_sequence_hierarchy( - self, seq_i, seq_j, max_frame_i, min_frame_j, max_frame_j, map_paths + seq_i, seq_j, max_frame_i, min_frame_j, max_frame_j, map_paths ): # Get existing sequencer tracks or create them if they don't exist tracks = seq_i.get_master_tracks() @@ -165,8 +168,9 @@ class LayoutLoader(plugin.Loader): hid_section.set_row_index(index) hid_section.set_level_names(maps) + @staticmethod def _process_family( - self, assets, class_name, transform, sequence, inst_name=None + assets, class_name, transform, sequence, inst_name=None ): ar = unreal.AssetRegistryHelpers.get_asset_registry() @@ -264,7 +268,7 @@ class LayoutLoader(plugin.Loader): task.options.anim_sequence_import_data.set_editor_property( 'use_default_sample_rate', False) task.options.anim_sequence_import_data.set_editor_property( - 'custom_sample_rate', 25.0) # TODO: get from database + 'custom_sample_rate', get_asset()["data"].get("fps")) task.options.anim_sequence_import_data.set_editor_property( 'import_custom_attribute', True) task.options.anim_sequence_import_data.set_editor_property( @@ -313,11 +317,8 @@ class LayoutLoader(plugin.Loader): for binding in bindings: tracks = binding.get_tracks() track = None - if not tracks: - track = binding.add_track( - unreal.MovieSceneSkeletalAnimationTrack) - else: - track = tracks[0] + track = tracks[0] if tracks else binding.add_track( + unreal.MovieSceneSkeletalAnimationTrack) sections = track.get_sections() section = None @@ -337,11 +338,11 @@ class LayoutLoader(plugin.Loader): curr_anim.get_path_name()).parent ).replace('\\', '/') - filter = unreal.ARFilter( + _filter = unreal.ARFilter( class_names=["AssetContainer"], package_paths=[anim_path], recursive_paths=False) - containers = ar.get_assets(filter) + containers = ar.get_assets(_filter) if len(containers) > 0: return @@ -352,6 +353,7 @@ class LayoutLoader(plugin.Loader): sec_params = section.get_editor_property('params') sec_params.set_editor_property('animation', animation) + @staticmethod def _generate_sequence(self, h, h_dir): tools = unreal.AssetToolsHelpers().get_asset_tools() @@ -585,10 +587,7 @@ class LayoutLoader(plugin.Loader): hierarchy_dir_list.append(hierarchy_dir) asset = context.get('asset').get('name') suffix = "_CON" - if asset: - asset_name = "{}_{}".format(asset, name) - else: - asset_name = "{}".format(name) + asset_name = f"{asset}_{name}" if asset else asset_name = name tools = unreal.AssetToolsHelpers().get_asset_tools() asset_dir, container_name = tools.create_unique_asset_name( @@ -802,7 +801,7 @@ class LayoutLoader(plugin.Loader): lc for lc in layout_containers if asset in lc.get('loaded_assets')] - if len(layouts) == 0: + if not layouts: EditorAssetLibrary.delete_directory(str(Path(asset).parent)) # Remove the Level Sequence from the parent. @@ -812,17 +811,17 @@ class LayoutLoader(plugin.Loader): namespace = container.get('namespace').replace(f"{root}/", "") ms_asset = namespace.split('/')[0] ar = unreal.AssetRegistryHelpers.get_asset_registry() - filter = unreal.ARFilter( + _filter = unreal.ARFilter( class_names=["LevelSequence"], package_paths=[f"{root}/{ms_asset}"], recursive_paths=False) - sequences = ar.get_assets(filter) + sequences = ar.get_assets(_filter) master_sequence = sequences[0].get_asset() - filter = unreal.ARFilter( + _filter = unreal.ARFilter( class_names=["World"], package_paths=[f"{root}/{ms_asset}"], recursive_paths=False) - levels = ar.get_assets(filter) + levels = ar.get_assets(_filter) master_level = levels[0].get_editor_property('object_path') sequences = [master_sequence] From 0c2d0bdd75961d8a685c810c6cbbf6d36591669d Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 26 May 2022 14:45:17 +0200 Subject: [PATCH 374/398] :bug: fix wrong assignment --- openpype/hosts/unreal/plugins/load/load_layout.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index 0632c3c0b5..c65cd25ac8 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -587,7 +587,7 @@ class LayoutLoader(plugin.Loader): hierarchy_dir_list.append(hierarchy_dir) asset = context.get('asset').get('name') suffix = "_CON" - asset_name = f"{asset}_{name}" if asset else asset_name = name + asset_name = f"{asset}_{name}" if asset else name tools = unreal.AssetToolsHelpers().get_asset_tools() asset_dir, container_name = tools.create_unique_asset_name( From d3179847d266be44a96ab08e9b9b7fdffe5b3173 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 26 May 2022 14:59:08 +0200 Subject: [PATCH 375/398] General: editorial otio_range in collection was one frame longer --- openpype/lib/editorial.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/lib/editorial.py b/openpype/lib/editorial.py index 2c877b9d0d..7b2d22f738 100644 --- a/openpype/lib/editorial.py +++ b/openpype/lib/editorial.py @@ -168,7 +168,7 @@ def make_sequence_collection(path, otio_range, metadata): first, last = otio_range_to_frame_range(otio_range) collection = clique.Collection( head=head, tail=tail, padding=metadata["padding"]) - collection.indexes.update([i for i in range(first, (last + 1))]) + collection.indexes.update([i for i in range(first, last)]) return dir_path, collection From f96318cddd5e4cee2f66fa01ca85ccfb6bddb769 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Thu, 26 May 2022 15:01:56 +0200 Subject: [PATCH 376/398] Update openpype/hosts/flame/otio/flame_export.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/flame/otio/flame_export.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/flame/otio/flame_export.py b/openpype/hosts/flame/otio/flame_export.py index ffb82b97c2..9756d0442e 100644 --- a/openpype/hosts/flame/otio/flame_export.py +++ b/openpype/hosts/flame/otio/flame_export.py @@ -94,7 +94,7 @@ def create_otio_time_range(start_frame, frame_duration, fps): def _get_metadata(item): if hasattr(item, 'metadata'): - return dict(dict(item.metadata)) if item.metadata else {} + return dict(item.metadata) if item.metadata else {} return {} From 95f836f41175128e548f9369d5e50c148e180e93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Thu, 26 May 2022 15:02:17 +0200 Subject: [PATCH 377/398] Update openpype/hosts/flame/api/render_utils.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/flame/api/render_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/flame/api/render_utils.py b/openpype/hosts/flame/api/render_utils.py index 9957550af9..da22f117a7 100644 --- a/openpype/hosts/flame/api/render_utils.py +++ b/openpype/hosts/flame/api/render_utils.py @@ -1,8 +1,8 @@ import os from xml.etree import ElementTree as ET -import openpype.api as openpype +import openpype.api import Logger -log = openpype.Logger.get_logger(__name__) +log = Logger.get_logger(__name__) def export_clip(export_path, clip, preset_path, **kwargs): From 2d1f7b9873022ece16f822690a4269a15fb979c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Thu, 26 May 2022 15:02:41 +0200 Subject: [PATCH 378/398] Update openpype/hosts/flame/otio/flame_export.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/flame/otio/flame_export.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/hosts/flame/otio/flame_export.py b/openpype/hosts/flame/otio/flame_export.py index 9756d0442e..1e4ef866ed 100644 --- a/openpype/hosts/flame/otio/flame_export.py +++ b/openpype/hosts/flame/otio/flame_export.py @@ -280,9 +280,7 @@ def create_otio_clip(clip_data): segment = clip_data["PySegment"] # calculate source in - media_info = MediaInfoFile(clip_data["fpath"], **{ - "logger": log - }) + media_info = MediaInfoFile(clip_data["fpath"], logger=log) media_timecode_start = media_info.start_frame media_fps = media_info.fps From db5d85080a418ed8e78908bb72339a0a41011b5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Thu, 26 May 2022 15:03:29 +0200 Subject: [PATCH 379/398] Update openpype/hosts/flame/plugins/publish/extract_subset_resources.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../hosts/flame/plugins/publish/extract_subset_resources.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py index 1bfe980a01..6319f4b041 100644 --- a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py +++ b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py @@ -425,9 +425,7 @@ class ExtractSubsetResources(openpype.api.Extractor): Import clip from path """ dir_path = os.path.dirname(path) - media_info = MediaInfoFile(path, **{ - "logger": self.log - }) + media_info = MediaInfoFile(path, logger=self.log) file_pattern = media_info.file_pattern self.log.debug("__ file_pattern: {}".format(file_pattern)) From 024874b4f653725e56223fba8a2a7f47404c30f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Thu, 26 May 2022 15:06:16 +0200 Subject: [PATCH 380/398] Update openpype/hosts/flame/plugins/publish/extract_subset_resources.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../hosts/flame/plugins/publish/extract_subset_resources.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py index 6319f4b041..9265d3517c 100644 --- a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py +++ b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py @@ -60,7 +60,8 @@ class ExtractSubsetResources(openpype.api.Extractor): export_presets_mapping = {} def process(self, instance): - self._make_representation_data(instance) + if "representations" not in instance.data: + instance.data["representations"] = [] # flame objects segment = instance.data["item"] From a2289429a850061484300e81eaed99413a87ef38 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 26 May 2022 15:57:52 +0200 Subject: [PATCH 381/398] :sparkles: added collector for FBX camera export --- .../plugins/publish/collect_fbx_camera.py | 20 +++++++++++++++++++ .../defaults/project_settings/maya.json | 3 +++ .../schemas/schema_maya_publish.json | 14 +++++++++++++ 3 files changed, 37 insertions(+) create mode 100644 openpype/hosts/maya/plugins/publish/collect_fbx_camera.py diff --git a/openpype/hosts/maya/plugins/publish/collect_fbx_camera.py b/openpype/hosts/maya/plugins/publish/collect_fbx_camera.py new file mode 100644 index 0000000000..bfa5bccbb9 --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/collect_fbx_camera.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from maya import cmds # noqa +import pyblish.api + + +class CollectFbxCamera(pyblish.api.InstancePlugin): + """Collect Camera for FBX export.""" + + order = pyblish.api.CollectorOrder + 0.2 + label = "Collect Camera for FBX export" + families = ["camera"] + + def process(self, instance): + if not instance.data.get("families"): + instance.data["families"] = [] + + if "fbx" not in instance.data["families"]: + instance.data["families"].append("fbx") + + instance.data["cameras"] = True diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 4cdfe1ca5d..e03bdcecc3 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -165,6 +165,9 @@ "CollectMayaRender": { "sync_workfile_version": false }, + "CollectFbxCamera": { + "enabled": false + }, "ValidateInstanceInContext": { "enabled": true, "optional": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json index 2e5bc64e1c..9877b5ff0d 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json @@ -21,6 +21,20 @@ } ] }, + { + "type": "dict", + "collapsible": true, + "key": "CollectFbxCamera", + "label": "Collect Camera for FBX export", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + } + ] + }, { "type": "splitter" }, From ab8068858cf62422b6838b7e526d723f1b81230a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 26 May 2022 16:32:25 +0200 Subject: [PATCH 382/398] flame: removing unneeded code --- .../flame/plugins/publish/extract_subset_resources.py | 9 --------- 1 file changed, 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 9265d3517c..b91aec15c8 100644 --- a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py +++ b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py @@ -51,7 +51,6 @@ class ExtractSubsetResources(openpype.api.Extractor): "path_regex": ".*" } } - keep_original_representation = False # hide publisher during exporting hide_ui_on_process = True @@ -329,14 +328,6 @@ class ExtractSubsetResources(openpype.api.Extractor): ): return True - def _make_representation_data(self, instance): - if ( - self.keep_original_representation - and "representations" not in instance.data - or not self.keep_original_representation - ): - instance.data["representations"] = [] - def _unfolds_nested_folders(self, stage_dir, files_list, ext): """Unfolds nested folders From 24c289a0d5088c708d9e6754dd8ebbc033e0e491 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 26 May 2022 17:18:00 +0200 Subject: [PATCH 383/398] nuke: use framerange used as list but it is bool --- openpype/hosts/nuke/api/lib.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index ba8aa7a8db..f40425eefc 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -373,7 +373,7 @@ def add_write_node_legacy(name, **kwarg): Returns: node (obj): nuke write node """ - frame_range = kwarg.get("use_range_limit", None) + use_range_limit = kwarg.get("use_range_limit", None) w = nuke.createNode( "Write", @@ -391,10 +391,10 @@ def add_write_node_legacy(name, **kwarg): log.debug(e) continue - if frame_range: + if use_range_limit: w["use_limit"].setValue(True) - w["first"].setValue(frame_range[0]) - w["last"].setValue(frame_range[1]) + w["first"].setValue(kwarg["frame_range"][0]) + w["last"].setValue(kwarg["frame_range"][1]) return w @@ -409,7 +409,7 @@ def add_write_node(name, file_path, knobs, **kwarg): Returns: node (obj): nuke write node """ - frame_range = kwarg.get("use_range_limit", None) + use_range_limit = kwarg.get("use_range_limit", None) w = nuke.createNode( "Write", @@ -420,10 +420,10 @@ def add_write_node(name, file_path, knobs, **kwarg): # finally add knob overrides set_node_knobs_from_settings(w, knobs, **kwarg) - if frame_range: + if use_range_limit: w["use_limit"].setValue(True) - w["first"].setValue(frame_range[0]) - w["last"].setValue(frame_range[1]) + w["first"].setValue(kwarg["frame_range"][0]) + w["last"].setValue(kwarg["frame_range"][1]) return w From 693efa272fefee7c09aaa93eb78ffa07b3c198f2 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 26 May 2022 16:23:50 +0100 Subject: [PATCH 384/398] Camera creates master level if layout is missing --- .../hosts/unreal/plugins/load/load_camera.py | 44 ++++++++++++------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index b33e45b6e9..c6061bc5c1 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -5,6 +5,7 @@ from pathlib import Path import unreal from unreal import EditorAssetLibrary from unreal import EditorLevelLibrary +from unreal import EditorLevelUtils from openpype.pipeline import ( AVALON_CONTAINER_ID, @@ -84,10 +85,10 @@ class CameraLoader(plugin.Loader): hierarchy = context.get('asset').get('data').get('parents') root = "/Game/OpenPype" hierarchy_dir = root - hierarchy_list = [] + hierarchy_dir_list = [] for h in hierarchy: hierarchy_dir = f"{hierarchy_dir}/{h}" - hierarchy_list.append(hierarchy_dir) + hierarchy_dir_list.append(hierarchy_dir) asset = context.get('asset').get('name') suffix = "_CON" if asset: @@ -121,27 +122,40 @@ class CameraLoader(plugin.Loader): asset_dir, container_name = tools.create_unique_asset_name( f"{hierarchy_dir}/{asset}/{name}_{unique_number:02d}", suffix="") + asset_path = Path(asset_dir) + asset_path_parent = str(asset_path.parent.as_posix()) + container_name += suffix - current_level = EditorLevelLibrary.get_editor_world().get_full_name() + EditorAssetLibrary.make_directory(asset_dir) + + # Create map for the shot, and create hierarchy of map. If the maps + # already exist, we will use them. + h_dir = hierarchy_dir_list[0] + h_asset = hierarchy[0] + master_level = f"{h_dir}/{h_asset}_map.{h_asset}_map" + if not EditorAssetLibrary.does_asset_exist(master_level): + EditorLevelLibrary.new_level(f"{h_dir}/{h_asset}_map") + + level = f"{asset_path_parent}/{asset}_map.{asset}_map" + if not EditorAssetLibrary.does_asset_exist(level): + EditorLevelLibrary.new_level(f"{asset_path_parent}/{asset}_map") + + EditorLevelLibrary.load_level(master_level) + EditorLevelUtils.add_level_to_world( + EditorLevelLibrary.get_editor_world(), + level, + unreal.LevelStreamingDynamic + ) EditorLevelLibrary.save_all_dirty_levels() - - ar = unreal.AssetRegistryHelpers.get_asset_registry() - filter = unreal.ARFilter( - class_names=["World"], - package_paths=[f"{hierarchy_dir}/{asset}/"], - recursive_paths=True) - maps = ar.get_assets(filter) - - # There should be only one map in the list - EditorLevelLibrary.load_level(maps[0].get_full_name()) + EditorLevelLibrary.load_level(level) # Get all the sequences in the hierarchy. It will create them, if # they don't exist. sequences = [] frame_ranges = [] i = 0 - for h in hierarchy_list: + for h in hierarchy_dir_list: root_content = EditorAssetLibrary.list_assets( h, recursive=False, include_folder=False) @@ -256,7 +270,7 @@ class CameraLoader(plugin.Loader): "{}/{}".format(asset_dir, container_name), data) EditorLevelLibrary.save_all_dirty_levels() - EditorLevelLibrary.load_level(current_level) + EditorLevelLibrary.load_level(master_level) asset_content = EditorAssetLibrary.list_assets( asset_dir, recursive=True, include_folder=True From c5787b899257433f0873f61c9aa1408cd68c0bee Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 26 May 2022 17:23:51 +0200 Subject: [PATCH 385/398] Flame: small bugs --- openpype/hosts/flame/api/render_utils.py | 2 +- openpype/plugins/publish/collect_otio_subset_resources.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/flame/api/render_utils.py b/openpype/hosts/flame/api/render_utils.py index da22f117a7..a29d6be695 100644 --- a/openpype/hosts/flame/api/render_utils.py +++ b/openpype/hosts/flame/api/render_utils.py @@ -1,6 +1,6 @@ import os from xml.etree import ElementTree as ET -import openpype.api import Logger +from openpype.api import Logger log = Logger.get_logger(__name__) diff --git a/openpype/plugins/publish/collect_otio_subset_resources.py b/openpype/plugins/publish/collect_otio_subset_resources.py index fb4cc6ee5e..40d4f35bdc 100644 --- a/openpype/plugins/publish/collect_otio_subset_resources.py +++ b/openpype/plugins/publish/collect_otio_subset_resources.py @@ -64,7 +64,7 @@ class CollectOtioSubsetResources(pyblish.api.InstancePlugin): a_frame_end_h = media_out + handle_end # create trimmed otio time range - trimmed_media_range_h = editorial.range_from_frames( + trimmed_media_range_h = oplib.range_from_frames( a_frame_start_h, (a_frame_end_h - a_frame_start_h) + 1, media_fps ) From ae233ce80cc9ecf549d2fdbebbc50d1ca29b8b98 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 26 May 2022 17:26:00 +0200 Subject: [PATCH 386/398] hiero: PR suggestion https://github.com/pypeclub/OpenPype/pull/3224#discussion_r882588237 --- openpype/hosts/hiero/api/lib.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/hiero/api/lib.py b/openpype/hosts/hiero/api/lib.py index 5a9f38bf92..999dae5488 100644 --- a/openpype/hosts/hiero/api/lib.py +++ b/openpype/hosts/hiero/api/lib.py @@ -941,6 +941,10 @@ def is_overlapping(ti_test, ti_original, strict=False): (ti_test.timelineIn() <= ti_original.timelineIn()) and (ti_test.timelineOut() >= ti_original.timelineOut()) ) + + if strict: + return covering_exp + inside_exp = ( (ti_test.timelineIn() >= ti_original.timelineIn()) and (ti_test.timelineOut() <= ti_original.timelineOut()) @@ -954,15 +958,12 @@ def is_overlapping(ti_test, ti_original, strict=False): and (ti_test.timelineIn() <= ti_original.timelineIn()) ) - if strict: - return covering_exp - else: - return any(( - covering_exp, - inside_exp, - overlaying_right_exp, - overlaying_left_exp - )) + return any(( + covering_exp, + inside_exp, + overlaying_right_exp, + overlaying_left_exp + )) def get_sequence_pattern_and_padding(file): From 4cd4124e0128cac24938cf3d0098de04b9e5f58c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 26 May 2022 17:40:27 +0200 Subject: [PATCH 387/398] flame: fixing frame range from editorial --- openpype/lib/editorial.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/lib/editorial.py b/openpype/lib/editorial.py index 0de266725f..9f55d1fcb1 100644 --- a/openpype/lib/editorial.py +++ b/openpype/lib/editorial.py @@ -168,7 +168,7 @@ def make_sequence_collection(path, otio_range, metadata): first, last = otio_range_to_frame_range(otio_range) collection = clique.Collection( head=head, tail=tail, padding=metadata["padding"]) - collection.indexes.update([i for i in range(first, (last + 1))]) + collection.indexes.update(list(range(first, last))) return dir_path, collection From 6d4c057a04291da0381af2a1339ec3c68319d50f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 26 May 2022 17:40:42 +0200 Subject: [PATCH 388/398] flame: removing default preset --- .../plugins/publish/extract_subset_resources.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py index b91aec15c8..0bad3f7cfc 100644 --- a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py +++ b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py @@ -34,21 +34,6 @@ class ExtractSubsetResources(openpype.api.Extractor): "representation_add_range": False, "representation_tags": ["thumbnail"], "path_regex": ".*" - }, - "ftrackpreview": { - "active": True, - "ext": "mov", - "xml_preset_file": "Apple iPad (1920x1080).xml", - "xml_preset_dir": "", - "export_type": "Movie", - "parsed_comment_attrs": False, - "colorspace_out": "Output - Rec.709", - "representation_add_range": True, - "representation_tags": [ - "review", - "delete" - ], - "path_regex": ".*" } } From a4c32639d7bcd1aae3735f763fadd7a334968df9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 26 May 2022 17:51:25 +0200 Subject: [PATCH 389/398] nuke: adding frame range to plugin --- openpype/hosts/nuke/plugins/create/create_write_prerender.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/hosts/nuke/plugins/create/create_write_prerender.py b/openpype/hosts/nuke/plugins/create/create_write_prerender.py index 32ee1fd86f..fec97167fb 100644 --- a/openpype/hosts/nuke/plugins/create/create_write_prerender.py +++ b/openpype/hosts/nuke/plugins/create/create_write_prerender.py @@ -27,6 +27,10 @@ class CreateWritePrerender(plugin.AbstractWriteRender): # add fpath_template write_data["fpath_template"] = self.fpath_template write_data["use_range_limit"] = self.use_range_limit + write_data["frame_range"] = ( + nuke.root()["first_frame"].value(), + nuke.root()["last_frame"].value() + ) if not self.is_legacy(): return create_write_node( From b8cade1009bf4863b12c467fa0cbdef7b8d864e4 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 26 May 2022 18:43:00 +0200 Subject: [PATCH 390/398] Fix - Harmony message length Harmony 21.1 doesn't have QDataStream anymore. This means we aren't able to write bytes into QByteArray so we had modify how content lenght is sent do the server. Content lenght is sent as string of 8 char convertible into integer (instead of 0x00000001[4 bytes] > "000000001"[8 bytes]) --- openpype/hosts/harmony/api/TB_sceneOpened.js | 21 ++++++++++++++++---- openpype/hosts/harmony/api/server.py | 12 +++++++---- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/harmony/api/TB_sceneOpened.js b/openpype/hosts/harmony/api/TB_sceneOpened.js index 610b0a73bb..e7cd555332 100644 --- a/openpype/hosts/harmony/api/TB_sceneOpened.js +++ b/openpype/hosts/harmony/api/TB_sceneOpened.js @@ -35,7 +35,11 @@ function Client() { self.pack = function(num) { var ascii=''; for (var i = 3; i >= 0; i--) { - ascii += String.fromCharCode((num >> (8 * i)) & 255); + var hex = ((num >> (8 * i)) & 255).toString(16); + if (hex.length < 2){ + ascii += "0"; + } + ascii += hex; } return ascii; }; @@ -279,12 +283,21 @@ function Client() { }; self._send = function(message) { - var codec_name = new QByteArray().append("ISO-8859-1"); + /** Harmony 21.1 doesn't have QDataStream anymore. + + This means we aren't able to write bytes into QByteArray so we had + modify how content lenght is sent do the server. + Content lenght is sent as string of 8 char convertible into integer + (instead of 0x00000001[4 bytes] > "000000001"[8 bytes]) */ + var codec_name = new QByteArray().append("UTF-8"); + var codec = QTextCodec.codecForName(codec_name); var msg = codec.fromUnicode(message); var l = msg.size(); - var coded = new QByteArray().append('AH').append(self.pack(l)).append(msg); - self.socket.write(new QByteArray(coded)); + var header = new QByteArray().append('AH').append(self.pack(l)); + var coded = msg.prepend(header); + + self.socket.write(coded); self.logDebug('Sent.'); }; diff --git a/openpype/hosts/harmony/api/server.py b/openpype/hosts/harmony/api/server.py index 88cfe54521..0de359ec61 100644 --- a/openpype/hosts/harmony/api/server.py +++ b/openpype/hosts/harmony/api/server.py @@ -88,21 +88,25 @@ class Server(threading.Thread): """ current_time = time.time() while True: - + self.log.info("wait ttt") # Receive the data in small chunks and retransmit it request = None - header = self.connection.recv(6) + header = self.connection.recv(10) if len(header) == 0: # null data received, socket is closing. self.log.info(f"[{self.timestamp()}] Connection closing.") break + if header[0:2] != b"AH": self.log.error("INVALID HEADER") - length = struct.unpack(">I", header[2:])[0] + content_length_str = header[2:].decode() + + length = int(content_length_str, 16) data = self.connection.recv(length) while (len(data) < length): # we didn't received everything in first try, lets wait for # all data. + self.log.info("loop") time.sleep(0.1) if self.connection is None: self.log.error(f"[{self.timestamp()}] " @@ -113,7 +117,7 @@ class Server(threading.Thread): break data += self.connection.recv(length - len(data)) - + self.log.debug("data:: {} {}".format(data, type(data))) self.received += data.decode("utf-8") pretty = self._pretty(self.received) self.log.debug( From bc3a8bbe2ff52903e0d9ab7f029c51407cd70ec3 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Thu, 26 May 2022 18:37:41 +0000 Subject: [PATCH 391/398] [Automated] Bump version --- CHANGELOG.md | 37 +++++++++++++++++++------------------ openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 21 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3dd410391a..8437540a49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [3.10.0-nightly.5](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.10.0-nightly.6](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.9.8...HEAD) @@ -10,14 +10,17 @@ - General: Creator plugins from addons can be registered [\#3179](https://github.com/pypeclub/OpenPype/pull/3179) - Ftrack: Single image reviewable [\#3157](https://github.com/pypeclub/OpenPype/pull/3157) - Nuke: Expose write attributes to settings [\#3123](https://github.com/pypeclub/OpenPype/pull/3123) -- Hiero: Initial frame publish support [\#3106](https://github.com/pypeclub/OpenPype/pull/3106) **🚀 Enhancements** +- Maya: FBX camera export [\#3253](https://github.com/pypeclub/OpenPype/pull/3253) +- General: updating common vendor `scriptmenu` to 1.5.2 [\#3246](https://github.com/pypeclub/OpenPype/pull/3246) - Project Manager: Allow to paste Tasks into multiple assets at the same time [\#3226](https://github.com/pypeclub/OpenPype/pull/3226) - Project manager: Sped up project load [\#3216](https://github.com/pypeclub/OpenPype/pull/3216) - Loader UI: Speed issues of loader with sync server [\#3199](https://github.com/pypeclub/OpenPype/pull/3199) +- Looks: add basic support for Renderman [\#3190](https://github.com/pypeclub/OpenPype/pull/3190) - Maya: added clean\_import option to Import loader [\#3181](https://github.com/pypeclub/OpenPype/pull/3181) +- Add the scripts menu definition to nuke [\#3168](https://github.com/pypeclub/OpenPype/pull/3168) - Maya: add maya 2023 to default applications [\#3167](https://github.com/pypeclub/OpenPype/pull/3167) - Compressed bgeo publishing in SAP and Houdini loader [\#3153](https://github.com/pypeclub/OpenPype/pull/3153) - General: Add 'dataclasses' to required python modules [\#3149](https://github.com/pypeclub/OpenPype/pull/3149) @@ -28,24 +31,26 @@ - General: Simplified OP modules/addons import [\#3137](https://github.com/pypeclub/OpenPype/pull/3137) - Terminal: Tweak coloring of TrayModuleManager logging enabled states [\#3133](https://github.com/pypeclub/OpenPype/pull/3133) - General: Cleanup some Loader docstrings [\#3131](https://github.com/pypeclub/OpenPype/pull/3131) -- Nuke: render instance with subset name filtered overrides [\#3117](https://github.com/pypeclub/OpenPype/pull/3117) - Unreal: Layout and Camera update and remove functions reimplemented and improvements [\#3116](https://github.com/pypeclub/OpenPype/pull/3116) -- Settings: Remove environment groups from settings [\#3115](https://github.com/pypeclub/OpenPype/pull/3115) -- TVPaint: Match renderlayer key with other hosts [\#3110](https://github.com/pypeclub/OpenPype/pull/3110) -- Tray publisher: Simple families from settings [\#3105](https://github.com/pypeclub/OpenPype/pull/3105) **🐛 Bug fixes** +- nuke: use framerange issue [\#3254](https://github.com/pypeclub/OpenPype/pull/3254) +- Ftrack: Chunk sizes for queries has minimal condition [\#3244](https://github.com/pypeclub/OpenPype/pull/3244) +- Maya: renderman displays needs to be filtered [\#3242](https://github.com/pypeclub/OpenPype/pull/3242) - Ftrack: Validate that the user exists on ftrack [\#3237](https://github.com/pypeclub/OpenPype/pull/3237) +- Maya: Fix support for multiple resolutions [\#3236](https://github.com/pypeclub/OpenPype/pull/3236) - TVPaint: Look for more groups than 12 [\#3228](https://github.com/pypeclub/OpenPype/pull/3228) -- Project Manager: Fix persistent editors on project change [\#3218](https://github.com/pypeclub/OpenPype/pull/3218) +- Hiero: debugging frame range and other 3.10 [\#3222](https://github.com/pypeclub/OpenPype/pull/3222) - Deadline: instance data overwrite fix [\#3214](https://github.com/pypeclub/OpenPype/pull/3214) - Ftrack: Push hierarchical attributes action works [\#3210](https://github.com/pypeclub/OpenPype/pull/3210) - Standalone Publisher: Always create new representation for thumbnail [\#3203](https://github.com/pypeclub/OpenPype/pull/3203) - Photoshop: skip collector when automatic testing [\#3202](https://github.com/pypeclub/OpenPype/pull/3202) - Nuke: render/workfile version sync doesn't work on farm [\#3185](https://github.com/pypeclub/OpenPype/pull/3185) - Ftrack: Review image only if there are no mp4 reviews [\#3183](https://github.com/pypeclub/OpenPype/pull/3183) +- Ftrack: Locations deepcopy issue [\#3177](https://github.com/pypeclub/OpenPype/pull/3177) - General: Avoid creating multiple thumbnails [\#3176](https://github.com/pypeclub/OpenPype/pull/3176) +- General: Avoid creating multiple thumbnails [\#3174](https://github.com/pypeclub/OpenPype/pull/3174) - General/Hiero: better clip duration calculation [\#3169](https://github.com/pypeclub/OpenPype/pull/3169) - General: Oiio conversion for ffmpeg checks for invalid characters [\#3166](https://github.com/pypeclub/OpenPype/pull/3166) - Fix for attaching render to subset [\#3164](https://github.com/pypeclub/OpenPype/pull/3164) @@ -55,19 +60,19 @@ - General: New Session schema [\#3141](https://github.com/pypeclub/OpenPype/pull/3141) - General: Missing version on headless mode crash properly [\#3136](https://github.com/pypeclub/OpenPype/pull/3136) - TVPaint: Composite layers in reversed order [\#3135](https://github.com/pypeclub/OpenPype/pull/3135) -- Nuke: fixing default settings for workfile builder loaders [\#3120](https://github.com/pypeclub/OpenPype/pull/3120) - Nuke: fix anatomy imageio regex default [\#3119](https://github.com/pypeclub/OpenPype/pull/3119) **🔀 Refactored code** -- Avalon repo removed from Jobs workflow [\#3193](https://github.com/pypeclub/OpenPype/pull/3193) - General: Remove remaining imports from avalon [\#3130](https://github.com/pypeclub/OpenPype/pull/3130) **Merged pull requests:** +- Harmony: message length in 21.1 [\#3257](https://github.com/pypeclub/OpenPype/pull/3257) +- Harmony: 21.1 fix [\#3249](https://github.com/pypeclub/OpenPype/pull/3249) +- Webpublish: remove publish highlight when creating subset name [\#3234](https://github.com/pypeclub/OpenPype/pull/3234) - Maya: added jpg to filter for Image Plane Loader [\#3223](https://github.com/pypeclub/OpenPype/pull/3223) - Webpublisher: replace space by underscore in subset names [\#3160](https://github.com/pypeclub/OpenPype/pull/3160) -- StandalonePublisher: removed Extract Background plugins [\#3093](https://github.com/pypeclub/OpenPype/pull/3093) ## [3.9.8](https://github.com/pypeclub/OpenPype/tree/3.9.8) (2022-05-19) @@ -84,11 +89,13 @@ - Standalone Publisher: Always create new representation for thumbnail [\#3204](https://github.com/pypeclub/OpenPype/pull/3204) - Nuke: render/workfile version sync doesn't work on farm [\#3184](https://github.com/pypeclub/OpenPype/pull/3184) - Ftrack: Review image only if there are no mp4 reviews [\#3182](https://github.com/pypeclub/OpenPype/pull/3182) -- Ftrack: Locations deepcopy issue [\#3177](https://github.com/pypeclub/OpenPype/pull/3177) - Ftrack: Locations deepcopy issue [\#3175](https://github.com/pypeclub/OpenPype/pull/3175) -- General: Avoid creating multiple thumbnails [\#3174](https://github.com/pypeclub/OpenPype/pull/3174) - General: TemplateResult can be copied [\#3170](https://github.com/pypeclub/OpenPype/pull/3170) +**🔀 Refactored code** + +- Avalon repo removed from Jobs workflow [\#3193](https://github.com/pypeclub/OpenPype/pull/3193) + **Merged pull requests:** - hiero: otio p3 compatibility issue - metadata on effect use update [\#3194](https://github.com/pypeclub/OpenPype/pull/3194) @@ -123,19 +130,13 @@ - Nuke: render instance with subset name filtered overrides \(3.9.x\) [\#3125](https://github.com/pypeclub/OpenPype/pull/3125) -**🚀 Enhancements** - -- TVPaint: Match renderlayer key with other hosts [\#3109](https://github.com/pypeclub/OpenPype/pull/3109) - **🐛 Bug fixes** - TVPaint: Composite layers in reversed order [\#3134](https://github.com/pypeclub/OpenPype/pull/3134) -- General: Python 3 compatibility in queries [\#3111](https://github.com/pypeclub/OpenPype/pull/3111) **Merged pull requests:** - Ftrack: AssetVersion status on publish [\#3114](https://github.com/pypeclub/OpenPype/pull/3114) -- renderman support for 3.9.x [\#3107](https://github.com/pypeclub/OpenPype/pull/3107) ## [3.9.5](https://github.com/pypeclub/OpenPype/tree/3.9.5) (2022-04-25) diff --git a/openpype/version.py b/openpype/version.py index eee776fd2c..984e4ba426 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.10.0-nightly.5" +__version__ = "3.10.0-nightly.6" diff --git a/pyproject.toml b/pyproject.toml index 50cdefe1bb..1caa2a838a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.10.0-nightly.5" # OpenPype +version = "3.10.0-nightly.6" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 728079233484fdc277a00e4dbffe4cc8d6fe2529 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Thu, 26 May 2022 18:58:41 +0000 Subject: [PATCH 392/398] [Automated] Release --- CHANGELOG.md | 13 +++++-------- openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8437540a49..15659f8aa4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ # Changelog -## [3.10.0-nightly.6](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.10.0](https://github.com/pypeclub/OpenPype/tree/3.10.0) (2022-05-26) -[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.9.8...HEAD) +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.9.8...3.10.0) **🆕 New features** @@ -42,6 +42,7 @@ - Maya: Fix support for multiple resolutions [\#3236](https://github.com/pypeclub/OpenPype/pull/3236) - TVPaint: Look for more groups than 12 [\#3228](https://github.com/pypeclub/OpenPype/pull/3228) - Hiero: debugging frame range and other 3.10 [\#3222](https://github.com/pypeclub/OpenPype/pull/3222) +- Project Manager: Fix persistent editors on project change [\#3218](https://github.com/pypeclub/OpenPype/pull/3218) - Deadline: instance data overwrite fix [\#3214](https://github.com/pypeclub/OpenPype/pull/3214) - Ftrack: Push hierarchical attributes action works [\#3210](https://github.com/pypeclub/OpenPype/pull/3210) - Standalone Publisher: Always create new representation for thumbnail [\#3203](https://github.com/pypeclub/OpenPype/pull/3203) @@ -50,7 +51,6 @@ - Ftrack: Review image only if there are no mp4 reviews [\#3183](https://github.com/pypeclub/OpenPype/pull/3183) - Ftrack: Locations deepcopy issue [\#3177](https://github.com/pypeclub/OpenPype/pull/3177) - General: Avoid creating multiple thumbnails [\#3176](https://github.com/pypeclub/OpenPype/pull/3176) -- General: Avoid creating multiple thumbnails [\#3174](https://github.com/pypeclub/OpenPype/pull/3174) - General/Hiero: better clip duration calculation [\#3169](https://github.com/pypeclub/OpenPype/pull/3169) - General: Oiio conversion for ffmpeg checks for invalid characters [\#3166](https://github.com/pypeclub/OpenPype/pull/3166) - Fix for attaching render to subset [\#3164](https://github.com/pypeclub/OpenPype/pull/3164) @@ -64,13 +64,13 @@ **🔀 Refactored code** +- Avalon repo removed from Jobs workflow [\#3193](https://github.com/pypeclub/OpenPype/pull/3193) - General: Remove remaining imports from avalon [\#3130](https://github.com/pypeclub/OpenPype/pull/3130) **Merged pull requests:** - Harmony: message length in 21.1 [\#3257](https://github.com/pypeclub/OpenPype/pull/3257) - Harmony: 21.1 fix [\#3249](https://github.com/pypeclub/OpenPype/pull/3249) -- Webpublish: remove publish highlight when creating subset name [\#3234](https://github.com/pypeclub/OpenPype/pull/3234) - Maya: added jpg to filter for Image Plane Loader [\#3223](https://github.com/pypeclub/OpenPype/pull/3223) - Webpublisher: replace space by underscore in subset names [\#3160](https://github.com/pypeclub/OpenPype/pull/3160) @@ -90,12 +90,9 @@ - Nuke: render/workfile version sync doesn't work on farm [\#3184](https://github.com/pypeclub/OpenPype/pull/3184) - Ftrack: Review image only if there are no mp4 reviews [\#3182](https://github.com/pypeclub/OpenPype/pull/3182) - Ftrack: Locations deepcopy issue [\#3175](https://github.com/pypeclub/OpenPype/pull/3175) +- General: Avoid creating multiple thumbnails [\#3174](https://github.com/pypeclub/OpenPype/pull/3174) - General: TemplateResult can be copied [\#3170](https://github.com/pypeclub/OpenPype/pull/3170) -**🔀 Refactored code** - -- Avalon repo removed from Jobs workflow [\#3193](https://github.com/pypeclub/OpenPype/pull/3193) - **Merged pull requests:** - hiero: otio p3 compatibility issue - metadata on effect use update [\#3194](https://github.com/pypeclub/OpenPype/pull/3194) diff --git a/openpype/version.py b/openpype/version.py index 984e4ba426..31be1f2f02 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.10.0-nightly.6" +__version__ = "3.10.0" diff --git a/pyproject.toml b/pyproject.toml index 1caa2a838a..444af49273 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.10.0-nightly.6" # OpenPype +version = "3.10.0" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From c8a8831f8d7b8afb5f78257fcbeae2dc47e33c52 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 27 May 2022 10:33:46 +0200 Subject: [PATCH 393/398] :bug: fix getting attribute for multilayer pxr --- openpype/hosts/maya/plugins/publish/collect_look.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_look.py b/openpype/hosts/maya/plugins/publish/collect_look.py index d295492f9a..7d258b01fa 100644 --- a/openpype/hosts/maya/plugins/publish/collect_look.py +++ b/openpype/hosts/maya/plugins/publish/collect_look.py @@ -571,7 +571,7 @@ class CollectLook(pyblish.api.InstancePlugin): self.log.debug(" - got {}".format(cmds.nodeType(node))) - attribute = FILE_NODES.get(cmds.nodeType(node)) + attribute = get_attributes(FILE_NODES, cmds.nodeType(node)) source = cmds.getAttr("{}.{}".format( node, attribute From e1deb29da8615c050358da6254c6e9e10d9f0c13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Fri, 27 May 2022 11:06:08 +0200 Subject: [PATCH 394/398] :bug: fix multiple attributes per node --- .../maya/plugins/publish/collect_look.py | 115 +++++++++--------- 1 file changed, 59 insertions(+), 56 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_look.py b/openpype/hosts/maya/plugins/publish/collect_look.py index 7d258b01fa..93c02ce0fb 100644 --- a/openpype/hosts/maya/plugins/publish/collect_look.py +++ b/openpype/hosts/maya/plugins/publish/collect_look.py @@ -28,7 +28,7 @@ SHAPE_ATTRS = set(SHAPE_ATTRS) def get_pxr_multitexture_file_attrs(node): attrs = [] for i in range(9): - if cmds.attributeQuery("filename{}".format(i), node): + if cmds.attributeQuery("filename{}".format(i), node=node, ex=True): file = cmds.getAttr("{}.filename{}".format(node, i)) if file: attrs.append("filename{}".format(i)) @@ -50,10 +50,10 @@ FILE_NODES = { } -def get_attributes(dictionary, attr): - # type: (dict, str) -> list +def get_attributes(dictionary, attr, node=None): + # type: (dict, str, str) -> list if callable(dictionary[attr]): - val = dictionary[attr]() + val = dictionary[attr](node) else: val = dictionary.get(attr, []) @@ -205,7 +205,8 @@ def get_file_node_paths(node): return [texture_pattern] try: - file_attributes = get_attributes(FILE_NODES, cmds.nodeType(node)) + file_attributes = get_attributes( + FILE_NODES, cmds.nodeType(node), node) except AttributeError: file_attributes = "fileTextureName" @@ -434,7 +435,8 @@ class CollectLook(pyblish.api.InstancePlugin): # Collect textures if any file nodes are found instance.data["resources"] = [] for n in files: - instance.data["resources"].append(self.collect_resource(n)) + for res in self.collect_resources(n): + instance.data["resources"].append(res) self.log.info("Collected resources: {}".format(instance.data["resources"])) @@ -554,7 +556,7 @@ class CollectLook(pyblish.api.InstancePlugin): return attributes - def collect_resource(self, node): + def collect_resources(self, node): """Collect the link to the file(s) used (resource) Args: node (str): name of the node @@ -571,60 +573,61 @@ class CollectLook(pyblish.api.InstancePlugin): self.log.debug(" - got {}".format(cmds.nodeType(node))) - attribute = get_attributes(FILE_NODES, cmds.nodeType(node)) - source = cmds.getAttr("{}.{}".format( - node, - attribute - )) - computed_attribute = "{}.{}".format(node, attribute) - if attribute == "fileTextureName": - computed_attribute = node + ".computedFileTextureNamePattern" + attributes = get_attributes(FILE_NODES, cmds.nodeType(node), node) + for attribute in attributes: + source = cmds.getAttr("{}.{}".format( + node, + attribute + )) + computed_attribute = "{}.{}".format(node, attribute) + if attribute == "fileTextureName": + computed_attribute = node + ".computedFileTextureNamePattern" - self.log.info(" - file source: {}".format(source)) - color_space_attr = "{}.colorSpace".format(node) - try: - color_space = cmds.getAttr(color_space_attr) - except ValueError: - # node doesn't have colorspace attribute - color_space = "Raw" - # Compare with the computed file path, e.g. the one with the - # pattern in it, to generate some logging information about this - # difference - # computed_attribute = "{}.computedFileTextureNamePattern".format(node) - computed_source = cmds.getAttr(computed_attribute) - if source != computed_source: - self.log.debug("Detected computed file pattern difference " - "from original pattern: {0} " - "({1} -> {2})".format(node, - source, - computed_source)) + self.log.info(" - file source: {}".format(source)) + color_space_attr = "{}.colorSpace".format(node) + try: + color_space = cmds.getAttr(color_space_attr) + except ValueError: + # node doesn't have colorspace attribute + color_space = "Raw" + # Compare with the computed file path, e.g. the one with the + # pattern in it, to generate some logging information about this + # difference + # computed_attribute = "{}.computedFileTextureNamePattern".format(node) + computed_source = cmds.getAttr(computed_attribute) + if source != computed_source: + self.log.debug("Detected computed file pattern difference " + "from original pattern: {0} " + "({1} -> {2})".format(node, + source, + computed_source)) - # We replace backslashes with forward slashes because V-Ray - # can't handle the UDIM files with the backslashes in the - # paths as the computed patterns - source = source.replace("\\", "/") + # We replace backslashes with forward slashes because V-Ray + # can't handle the UDIM files with the backslashes in the + # paths as the computed patterns + source = source.replace("\\", "/") - files = get_file_node_files(node) - if len(files) == 0: - self.log.error("No valid files found from node `%s`" % node) + files = get_file_node_files(node) + if len(files) == 0: + self.log.error("No valid files found from node `%s`" % node) - self.log.info("collection of resource done:") - self.log.info(" - node: {}".format(node)) - self.log.info(" - attribute: {}".format(attribute)) - self.log.info(" - source: {}".format(source)) - self.log.info(" - file: {}".format(files)) - self.log.info(" - color space: {}".format(color_space)) + self.log.info("collection of resource done:") + self.log.info(" - node: {}".format(node)) + self.log.info(" - attribute: {}".format(attribute)) + self.log.info(" - source: {}".format(source)) + self.log.info(" - file: {}".format(files)) + self.log.info(" - color space: {}".format(color_space)) - # Define the resource - return { - "node": node, - # here we are passing not only attribute, but with node again - # this should be simplified and changed extractor. - "attribute": "{}.{}".format(node, attribute), - "source": source, # required for resources - "files": files, - "color_space": color_space - } # required for resources + # Define the resource + yield { + "node": node, + # here we are passing not only attribute, but with node again + # this should be simplified and changed extractor. + "attribute": "{}.{}".format(node, attribute), + "source": source, # required for resources + "files": files, + "color_space": color_space + } # required for resources class CollectModelRenderSets(CollectLook): From a24896962062e4db576ccb33ce2dbafec77ac789 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 27 May 2022 16:14:17 +0200 Subject: [PATCH 395/398] Nuke: bake reformat was failing on string type --- openpype/hosts/nuke/api/plugin.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index 2bad6f2c78..b8b56ef2b8 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -18,7 +18,8 @@ from .lib import ( maintained_selection, set_avalon_knob_data, add_publish_knob, - get_nuke_imageio_settings + get_nuke_imageio_settings, + set_node_knobs_from_settings ) @@ -497,16 +498,7 @@ class ExporterReviewMov(ExporterReview): add_tags.append("reformated") rf_node = nuke.createNode("Reformat") - for kn_conf in reformat_node_config: - _type = kn_conf["type"] - k_name = str(kn_conf["name"]) - k_value = kn_conf["value"] - - # to remove unicode as nuke doesn't like it - if _type == "string": - k_value = str(kn_conf["value"]) - - rf_node[k_name].setValue(k_value) + set_node_knobs_from_settings(rf_node, reformat_node_config) # connect rf_node.setInput(0, self.previous_node) From c3c9cca5c2d1c3b29d5b4beaddbe2c855a32b3b8 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 27 May 2022 17:00:04 +0200 Subject: [PATCH 396/398] :recycle: :dog: fix hound --- openpype/hosts/maya/plugins/publish/collect_look.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_look.py b/openpype/hosts/maya/plugins/publish/collect_look.py index 93c02ce0fb..323bede761 100644 --- a/openpype/hosts/maya/plugins/publish/collect_look.py +++ b/openpype/hosts/maya/plugins/publish/collect_look.py @@ -590,10 +590,9 @@ class CollectLook(pyblish.api.InstancePlugin): except ValueError: # node doesn't have colorspace attribute color_space = "Raw" - # Compare with the computed file path, e.g. the one with the - # pattern in it, to generate some logging information about this - # difference - # computed_attribute = "{}.computedFileTextureNamePattern".format(node) + # Compare with the computed file path, e.g. the one with + # the pattern in it, to generate some logging information + # about this difference computed_source = cmds.getAttr(computed_attribute) if source != computed_source: self.log.debug("Detected computed file pattern difference " From f27176bed1ac255128fdbe3d4f9c1d6100942508 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Sat, 28 May 2022 03:44:32 +0000 Subject: [PATCH 397/398] [Automated] Bump version --- CHANGELOG.md | 23 +++++++++++++++++------ openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 15659f8aa4..4a5e1f1067 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,24 @@ # Changelog +## [3.10.1-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD) + +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.10.0...HEAD) + +**🚀 Enhancements** + +- TVPaint: Init file for TVPaint worker also handle guideline images [\#3250](https://github.com/pypeclub/OpenPype/pull/3250) +- Support for Unreal 5 [\#3122](https://github.com/pypeclub/OpenPype/pull/3122) + +**🐛 Bug fixes** + +- Unreal: Fix Camera Loading if Layout is missing [\#3255](https://github.com/pypeclub/OpenPype/pull/3255) +- Unreal: Fixed Animation loading in UE5 [\#3240](https://github.com/pypeclub/OpenPype/pull/3240) +- Unreal: Fixed Render creation in UE5 [\#3239](https://github.com/pypeclub/OpenPype/pull/3239) +- Unreal: Fixed Camera loading in UE5 [\#3238](https://github.com/pypeclub/OpenPype/pull/3238) + ## [3.10.0](https://github.com/pypeclub/OpenPype/tree/3.10.0) (2022-05-26) -[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.9.8...3.10.0) +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.10.0-nightly.6...3.10.0) **🆕 New features** @@ -31,7 +47,6 @@ - General: Simplified OP modules/addons import [\#3137](https://github.com/pypeclub/OpenPype/pull/3137) - Terminal: Tweak coloring of TrayModuleManager logging enabled states [\#3133](https://github.com/pypeclub/OpenPype/pull/3133) - General: Cleanup some Loader docstrings [\#3131](https://github.com/pypeclub/OpenPype/pull/3131) -- Unreal: Layout and Camera update and remove functions reimplemented and improvements [\#3116](https://github.com/pypeclub/OpenPype/pull/3116) **🐛 Bug fixes** @@ -131,10 +146,6 @@ - TVPaint: Composite layers in reversed order [\#3134](https://github.com/pypeclub/OpenPype/pull/3134) -**Merged pull requests:** - -- Ftrack: AssetVersion status on publish [\#3114](https://github.com/pypeclub/OpenPype/pull/3114) - ## [3.9.5](https://github.com/pypeclub/OpenPype/tree/3.9.5) (2022-04-25) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.10.0-nightly.2...3.9.5) diff --git a/openpype/version.py b/openpype/version.py index 31be1f2f02..12f25cdcea 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.10.0" +__version__ = "3.10.1-nightly.1" diff --git a/pyproject.toml b/pyproject.toml index 444af49273..47d678b5e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.10.0" # OpenPype +version = "3.10.1-nightly.1" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 99b6050cbec6c35d088e106a7ef5aebb182fbb6a Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Mon, 30 May 2022 18:47:09 +0200 Subject: [PATCH 398/398] Replace by last nuke familly icon --- openpype/resources/app_icons/hiero.png | Bin 46366 -> 87054 bytes openpype/resources/app_icons/nuke.png | Bin 49012 -> 86832 bytes openpype/resources/app_icons/nukestudio.png | Bin 46080 -> 101945 bytes openpype/resources/app_icons/nukex.png | Bin 44709 -> 99787 bytes 4 files changed, 0 insertions(+), 0 deletions(-) diff --git a/openpype/resources/app_icons/hiero.png b/openpype/resources/app_icons/hiero.png index 04bbf6265bb63f0615c2b98ab48891d03483f6b2..ba666c2fe0819e7c769bc2dc6d96c853b349fd80 100644 GIT binary patch literal 87054 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 diff --git a/openpype/resources/app_icons/nuke.png b/openpype/resources/app_icons/nuke.png index 423445409618d59ff822dc09e1dac2e4d3e241aa..e734b4984e1c5ce86e957274b455fd039c5cd150 100644 GIT binary patch literal 86832 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 V+x*Ep#xxg4tS#)!Yt6hK{RcYQ(zyTt literal 49012 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 NBt&F|s|EG_{|{S*X`BE6

n)d3@oZpYFsmbJyGIDGW; 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 R3jP~ZQC3Z+PRcy&{{SdSemwvH diff --git a/openpype/resources/app_icons/nukestudio.png b/openpype/resources/app_icons/nukestudio.png index 99c95f59ff858d67fb92263f594f4b71bda87a66..601d4a591d7fd21a0c9c6a1850e569ca05e523ef 100644 GIT binary patch literal 101945 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 literal 46080 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 HIsgA3TR*cj diff --git a/openpype/resources/app_icons/nukex.png b/openpype/resources/app_icons/nukex.png index 1c5a83c8ab2e22da3e6afe8a9561441c4e0e4022..980f150124faae3715eb4d0018d3368f1091a60f 100644 GIT binary patch literal 99787 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≶y*UWJeRW_RcYBCht7M?Kk$4b?RkwBCiuA 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 a`-EZgF*J|OX($H!`pHTvNz{oM2mc>0LFUT< literal 44709 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