From 9a6dc109254ab4d1aca99f4abd3464d65b4e46c2 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 6 Jan 2023 03:02:31 +0100 Subject: [PATCH 001/360] Initial draft for Substance Painter integration --- openpype/hosts/substancepainter/__init__.py | 10 + openpype/hosts/substancepainter/addon.py | 34 +++ .../hosts/substancepainter/api/__init__.py | 8 + .../hosts/substancepainter/api/pipeline.py | 234 ++++++++++++++++++ .../deploy/plugins/openpype_plugin.py | 15 ++ .../resources/app_icons/substancepainter.png | Bin 0 -> 107059 bytes .../system_settings/applications.json | 27 ++ openpype/settings/entities/enum_entity.py | 1 + .../schema_substancepainter.json | 40 +++ .../system_schema/schema_applications.json | 4 + 10 files changed, 373 insertions(+) create mode 100644 openpype/hosts/substancepainter/__init__.py create mode 100644 openpype/hosts/substancepainter/addon.py create mode 100644 openpype/hosts/substancepainter/api/__init__.py create mode 100644 openpype/hosts/substancepainter/api/pipeline.py create mode 100644 openpype/hosts/substancepainter/deploy/plugins/openpype_plugin.py create mode 100644 openpype/resources/app_icons/substancepainter.png create mode 100644 openpype/settings/entities/schemas/system_schema/host_settings/schema_substancepainter.json diff --git a/openpype/hosts/substancepainter/__init__.py b/openpype/hosts/substancepainter/__init__.py new file mode 100644 index 0000000000..4c33b9f507 --- /dev/null +++ b/openpype/hosts/substancepainter/__init__.py @@ -0,0 +1,10 @@ +from .addon import ( + SubstanceAddon, + SUBSTANCE_HOST_DIR, +) + + +__all__ = ( + "SubstanceAddon", + "SUBSTANCE_HOST_DIR" +) diff --git a/openpype/hosts/substancepainter/addon.py b/openpype/hosts/substancepainter/addon.py new file mode 100644 index 0000000000..bb55f20189 --- /dev/null +++ b/openpype/hosts/substancepainter/addon.py @@ -0,0 +1,34 @@ +import os +from openpype.modules import OpenPypeModule, IHostAddon + +SUBSTANCE_HOST_DIR = os.path.dirname(os.path.abspath(__file__)) + + +class SubstanceAddon(OpenPypeModule, IHostAddon): + name = "substancepainter" + host_name = "substancepainter" + + def initialize(self, module_settings): + self.enabled = True + + def add_implementation_envs(self, env, _app): + # Add requirements to SUBSTANCE_PAINTER_PLUGINS_PATH + plugin_path = os.path.join(SUBSTANCE_HOST_DIR, "deploy") + plugin_path = plugin_path.replace("\\", "/") + if env.get("SUBSTANCE_PAINTER_PLUGINS_PATH"): + plugin_path += os.pathsep + env["SUBSTANCE_PAINTER_PLUGINS_PATH"] + + env["SUBSTANCE_PAINTER_PLUGINS_PATH"] = plugin_path + + # Fix UI scale issue + env.pop("QT_AUTO_SCREEN_SCALE_FACTOR", None) + + def get_launch_hook_paths(self, app): + if app.host_name != self.host_name: + return [] + return [ + os.path.join(SUBSTANCE_HOST_DIR, "hooks") + ] + + def get_workfile_extensions(self): + return [".spp", ".toc"] diff --git a/openpype/hosts/substancepainter/api/__init__.py b/openpype/hosts/substancepainter/api/__init__.py new file mode 100644 index 0000000000..937d0c429e --- /dev/null +++ b/openpype/hosts/substancepainter/api/__init__.py @@ -0,0 +1,8 @@ +from .pipeline import ( + SubstanceHost, + +) + +__all__ = [ + "SubstanceHost", +] diff --git a/openpype/hosts/substancepainter/api/pipeline.py b/openpype/hosts/substancepainter/api/pipeline.py new file mode 100644 index 0000000000..3fd081ca1c --- /dev/null +++ b/openpype/hosts/substancepainter/api/pipeline.py @@ -0,0 +1,234 @@ +# -*- coding: utf-8 -*- +"""Pipeline tools for OpenPype Gaffer integration.""" +import os +import sys +import logging +from functools import partial + +# Substance 3D Painter modules +import substance_painter.ui +import substance_painter.event +import substance_painter.export +import substance_painter.project +import substance_painter.textureset + +from openpype.host import HostBase, IWorkfileHost, ILoadHost, IPublishHost + +import pyblish.api + +from openpype.pipeline import ( + register_creator_plugin_path, + register_loader_plugin_path, + AVALON_CONTAINER_ID +) +from openpype.lib import ( + register_event_callback, + emit_event, +) +from openpype.pipeline.load import any_outdated_containers +from openpype.hosts.substancepainter import SUBSTANCE_HOST_DIR + +log = logging.getLogger("openpype.hosts.substance") + +PLUGINS_DIR = os.path.join(SUBSTANCE_HOST_DIR, "plugins") +PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish") +LOAD_PATH = os.path.join(PLUGINS_DIR, "load") +CREATE_PATH = os.path.join(PLUGINS_DIR, "create") +INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") + +self = sys.modules[__name__] +self.menu = None +self.callbacks = [] + + +class SubstanceHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): + name = "substancepainter" + + def __init__(self): + super(SubstanceHost, self).__init__() + self._has_been_setup = False + + def install(self): + pyblish.api.register_host("substancepainter") + + pyblish.api.register_plugin_path(PUBLISH_PATH) + register_loader_plugin_path(LOAD_PATH) + register_creator_plugin_path(CREATE_PATH) + + log.info("Installing callbacks ... ") + # register_event_callback("init", on_init) + _register_callbacks() + # register_event_callback("before.save", before_save) + # register_event_callback("save", on_save) + register_event_callback("open", on_open) + # register_event_callback("new", on_new) + + log.info("Installing menu ... ") + _install_menu() + + self._has_been_setup = True + + def uninstall(self): + _uninstall_menu() + _deregister_callbacks() + + def has_unsaved_changes(self): + + if not substance_painter.project.is_open(): + return False + + return substance_painter.project.needs_saving() + + def get_workfile_extensions(self): + return [".spp", ".toc"] + + def save_workfile(self, dst_path=None): + + if not substance_painter.project.is_open(): + return False + + if not dst_path: + dst_path = self.get_current_workfile() + + full_save_mode = substance_painter.project.ProjectSaveMode.Full + substance_painter.project.save_as(dst_path, full_save_mode) + + return dst_path + + def open_workfile(self, filepath): + + if not os.path.exists(filepath): + raise RuntimeError("File does not exist: {}".format(filepath)) + + # We must first explicitly close current project before opening another + if substance_painter.project.is_open(): + substance_painter.project.close() + + substance_painter.project.open(filepath) + return filepath + + def get_current_workfile(self): + if not substance_painter.project.is_open(): + return None + + filepath = substance_painter.project.file_path() + if filepath.endswith(".spt"): + # When currently in a Substance Painter template assume our + # scene isn't saved. This can be the case directly after doing + # "New project", the path will then be the template used. This + # avoids Workfiles tool trying to save as .spt extension if the + # file hasn't been saved before. + return + + return filepath + + def get_containers(self): + return [] + + @staticmethod + def create_context_node(): + pass + + def update_context_data(self, data, changes): + pass + + def get_context_data(self): + pass + + +def _install_menu(): + from PySide2 import QtWidgets + from openpype.tools.utils import host_tools + + parent = substance_painter.ui.get_main_window() + + menu = QtWidgets.QMenu("OpenPype") + + action = menu.addAction("Load...") + action.triggered.connect( + lambda: host_tools.show_loader(parent=parent, use_context=True) + ) + + action = menu.addAction("Publish...") + action.triggered.connect( + lambda: host_tools.show_publisher(parent=parent) + ) + + action = menu.addAction("Manage...") + action.triggered.connect( + lambda: host_tools.show_scene_inventory(parent=parent) + ) + + action = menu.addAction("Library...") + action.triggered.connect( + lambda: host_tools.show_library_loader(parent=parent) + ) + + menu.addSeparator() + action = menu.addAction("Work Files...") + action.triggered.connect( + lambda: host_tools.show_workfiles(parent=parent) + ) + + substance_painter.ui.add_menu(menu) + + def on_menu_destroyed(): + self.menu = None + + menu.destroyed.connect(on_menu_destroyed) + + self.menu = menu + + +def _uninstall_menu(): + if self.menu: + self.menu.destroy() + self.menu = None + + +def _register_callbacks(): + # Prepare emit event callbacks + open_callback = partial(emit_event, "open") + + # Connect to the Substance Painter events + dispatcher = substance_painter.event.DISPATCHER + for event, callback in [ + (substance_painter.event.ProjectOpened, open_callback) + ]: + dispatcher.connect(event, callback) + # Keep a reference so we can deregister if needed + self.callbacks.append((event, callback)) + + +def _deregister_callbacks(): + for event, callback in self.callbacks: + substance_painter.event.DISPATCHER.disconnect(event, callback) + + +def on_open(): + log.info("Running callback on open..") + print("Run") + + if any_outdated_containers(): + from openpype.widgets import popup + + log.warning("Scene has outdated content.") + + # Get main window + parent = substance_painter.ui.get_main_window() + if parent is None: + log.info("Skipping outdated content pop-up " + "because Substance window can't be found.") + else: + + # Show outdated pop-up + def _on_show_inventory(): + from openpype.tools.utils import host_tools + host_tools.show_scene_inventory(parent=parent) + + dialog = popup.Popup(parent=parent) + dialog.setWindowTitle("Substance scene has outdated content") + dialog.setMessage("There are outdated containers in " + "your Substance scene.") + dialog.on_clicked.connect(_on_show_inventory) + dialog.show() \ No newline at end of file diff --git a/openpype/hosts/substancepainter/deploy/plugins/openpype_plugin.py b/openpype/hosts/substancepainter/deploy/plugins/openpype_plugin.py new file mode 100644 index 0000000000..01779156f1 --- /dev/null +++ b/openpype/hosts/substancepainter/deploy/plugins/openpype_plugin.py @@ -0,0 +1,15 @@ + +def start_plugin(): + from openpype.pipeline import install_host + from openpype.hosts.substancepainter.api import SubstanceHost + + install_host(SubstanceHost()) + + +def close_plugin(): + from openpype.pipeline import uninstall_host + uninstall_host() + + +if __name__ == "__main__": + start_plugin() diff --git a/openpype/resources/app_icons/substancepainter.png b/openpype/resources/app_icons/substancepainter.png new file mode 100644 index 0000000000000000000000000000000000000000..dc46f25d747f6626fd085818bbc593636898eecf GIT binary patch literal 107059 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 Wwf31l8gz< Date: Fri, 6 Jan 2023 04:04:32 +0100 Subject: [PATCH 002/360] Add substance mesh loader --- .../plugins/load/load_mesh.py | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 openpype/hosts/substancepainter/plugins/load/load_mesh.py diff --git a/openpype/hosts/substancepainter/plugins/load/load_mesh.py b/openpype/hosts/substancepainter/plugins/load/load_mesh.py new file mode 100644 index 0000000000..7cc5e35912 --- /dev/null +++ b/openpype/hosts/substancepainter/plugins/load/load_mesh.py @@ -0,0 +1,98 @@ +from openpype.pipeline import ( + load, + get_representation_path, +) +from openpype.pipeline import legacy_io + +import substance_painter.project +import qargparse + + +class SubstanceLoadProjectMesh(load.LoaderPlugin): + """Load mesh for project""" + + families = ["*"] + representations = ["abc", "fbx", "obj", "gltf"] + + label = "Load mesh" + order = -10 + icon = "code-fork" + color = "orange" + + options = [ + qargparse.Boolean( + "preserve_strokes", + default=True, + help="Preserve strokes positions on mesh.\n" + "(only relevant when loading into existing project)" + ), + qargparse.Boolean( + "import_cameras", + default=True, + help="Import cameras from the mesh file." + ) + ] + + def load(self, context, name, namespace, data): + + if not substance_painter.project.is_open(): + # Allow to 'initialize' a new project + # TODO: preferably these settings would come from the actual + # new project prompt of Substance (or something that is + # visually similar to still allow artist decisions) + settings = substance_painter.project.Settings( + default_texture_resolution=4096, + import_cameras=data.get("import_cameras", True), + ) + + substance_painter.project.create( + mesh_file_path=self.fname, + settings=settings + ) + return + + # Reload the mesh + settings = substance_painter.project.MeshReloadingSettings( + import_cameras=data.get("import_cameras", True), + preserve_strokes=data.get("preserve_strokes", True) + ) + + def on_mesh_reload(status: substance_painter.project.ReloadMeshStatus): + if status == substance_painter.project.ReloadMeshStatus.SUCCESS: + print("Reload succeeded") + else: + raise RuntimeError("Reload of mesh failed") + + path = self.fname + substance_painter.project.reload_mesh(path, settings, on_mesh_reload) + + # TODO: Register with the project so host.get_containers() can return + # the loaded content in manager + + def switch(self, container, representation): + self.update(container, representation) + + def update(self, container, representation): + + path = get_representation_path(representation) + + # Reload the mesh + # TODO: Re-use settings from first load? + settings = substance_painter.project.MeshReloadingSettings( + import_cameras=True, + preserve_strokes=True + ) + + def on_mesh_reload(status: substance_painter.project.ReloadMeshStatus): + if status == substance_painter.project.ReloadMeshStatus.SUCCESS: + print("Reload succeeded") + else: + raise RuntimeError("Reload of mesh failed") + + substance_painter.project.reload_mesh(path, settings, on_mesh_reload) + + def remove(self, container): + + # Remove OpenPype related settings about what model was loaded + # or close the project? + pass From 3cb797b10a04726183ca740a5f10b593be45aea1 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 6 Jan 2023 04:05:13 +0100 Subject: [PATCH 003/360] Add some fixes to stylesheet to avoid very odd looking OpenPype UIs in Substance Painter --- openpype/style/style.css | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/openpype/style/style.css b/openpype/style/style.css index a7a48cdb9d..ae1b9d2991 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -127,6 +127,7 @@ QPushButton { border-radius: 0.2em; padding: 3px 5px 3px 5px; background: {color:bg-buttons}; + min-width: 0px; /* Substance Painter fix */ } QPushButton:hover { @@ -328,7 +329,15 @@ QTabWidget::tab-bar { alignment: left; } +/* avoid QTabBar overrides in Substance Painter */ +QTabBar { + text-transform: none; + font-weight: normal; +} + QTabBar::tab { + text-transform: none; + font-weight: normal; border-top: 1px solid {color:border}; border-left: 1px solid {color:border}; border-right: 1px solid {color:border}; @@ -368,6 +377,7 @@ QHeaderView { QHeaderView::section { background: {color:bg-view-header}; padding: 4px; + border-top: 0px; /* Substance Painter fix */ border-right: 1px solid {color:bg-view}; border-radius: 0px; text-align: center; From e710a8dc70496e042e000da50c5ad2181376c84a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 6 Jan 2023 05:03:19 +0100 Subject: [PATCH 004/360] Fix bug if file wasn't saved yet, file_path() would return None --- openpype/hosts/substancepainter/api/pipeline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/substancepainter/api/pipeline.py b/openpype/hosts/substancepainter/api/pipeline.py index 3fd081ca1c..31c87f079d 100644 --- a/openpype/hosts/substancepainter/api/pipeline.py +++ b/openpype/hosts/substancepainter/api/pipeline.py @@ -112,7 +112,7 @@ class SubstanceHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): return None filepath = substance_painter.project.file_path() - if filepath.endswith(".spt"): + if filepath and filepath.endswith(".spt"): # When currently in a Substance Painter template assume our # scene isn't saved. This can be the case directly after doing # "New project", the path will then be the template used. This From 8468dbce679cc5dfee58e99e4015bb812f47080d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 6 Jan 2023 05:04:53 +0100 Subject: [PATCH 005/360] Implement managing for Load Mesh (draft implementation) --- .../hosts/substancepainter/api/pipeline.py | 47 +++++++++++- .../plugins/load/load_mesh.py | 71 ++++++++++++++----- 2 files changed, 97 insertions(+), 21 deletions(-) diff --git a/openpype/hosts/substancepainter/api/pipeline.py b/openpype/hosts/substancepainter/api/pipeline.py index 31c87f079d..4d49fa83d7 100644 --- a/openpype/hosts/substancepainter/api/pipeline.py +++ b/openpype/hosts/substancepainter/api/pipeline.py @@ -123,7 +123,16 @@ class SubstanceHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): return filepath def get_containers(self): - return [] + + if not substance_painter.project.is_open(): + return + + metadata = substance_painter.project.Metadata("OpenPype") + containers = metadata.get("containers") + if containers: + for key, container in containers.items(): + container["objectName"] = key + yield container @staticmethod def create_context_node(): @@ -231,4 +240,38 @@ def on_open(): dialog.setMessage("There are outdated containers in " "your Substance scene.") dialog.on_clicked.connect(_on_show_inventory) - dialog.show() \ No newline at end of file + dialog.show() + + +def imprint_container(container, + name, + namespace, + context, + loader): + """Imprint a loaded container with metadata. + + Containerisation enables a tracking of version, author and origin + for loaded assets. + + Arguments: + container (dict): The (substance metadata) dictionary to imprint into. + name (str): Name of resulting assembly + namespace (str): Namespace under which to host container + context (dict): Asset information + loader (load.LoaderPlugin): loader instance used to produce container. + + Returns: + None + + """ + + data = [ + ("schema", "openpype:container-2.0"), + ("id", AVALON_CONTAINER_ID), + ("name", str(name)), + ("namespace", str(namespace) if namespace else None), + ("loader", str(loader.__class__.__name__)), + ("representation", str(context["representation"]["_id"])), + ] + for key, value in data: + container[key] = value diff --git a/openpype/hosts/substancepainter/plugins/load/load_mesh.py b/openpype/hosts/substancepainter/plugins/load/load_mesh.py index 7cc5e35912..519ed3ad4e 100644 --- a/openpype/hosts/substancepainter/plugins/load/load_mesh.py +++ b/openpype/hosts/substancepainter/plugins/load/load_mesh.py @@ -2,12 +2,27 @@ from openpype.pipeline import ( load, get_representation_path, ) -from openpype.pipeline import legacy_io +from openpype.hosts.substancepainter.api.pipeline import imprint_container import substance_painter.project import qargparse +def set_container(key, container): + metadata = substance_painter.project.Metadata("OpenPype") + containers = metadata.get("containers") or {} + containers[key] = container + metadata.set("containers", containers) + + +def remove_container(key): + metadata = substance_painter.project.Metadata("OpenPype") + containers = metadata.get("containers") + if containers: + containers.pop(key, None) + metadata.set("containers", containers) + + class SubstanceLoadProjectMesh(load.LoaderPlugin): """Load mesh for project""" @@ -33,6 +48,8 @@ class SubstanceLoadProjectMesh(load.LoaderPlugin): ) ] + container_key = "ProjectMesh" + def load(self, context, name, namespace, data): if not substance_painter.project.is_open(): @@ -49,25 +66,34 @@ class SubstanceLoadProjectMesh(load.LoaderPlugin): mesh_file_path=self.fname, settings=settings ) - return - # Reload the mesh - settings = substance_painter.project.MeshReloadingSettings( - import_cameras=data.get("import_cameras", True), - preserve_strokes=data.get("preserve_strokes", True) - ) + else: + # Reload the mesh + settings = substance_painter.project.MeshReloadingSettings( + import_cameras=data.get("import_cameras", True), + preserve_strokes=data.get("preserve_strokes", True) + ) - def on_mesh_reload(status: substance_painter.project.ReloadMeshStatus): - if status == substance_painter.project.ReloadMeshStatus.SUCCESS: - print("Reload succeeded") - else: - raise RuntimeError("Reload of mesh failed") + def on_mesh_reload(status: substance_painter.project.ReloadMeshStatus): # noqa + if status == substance_painter.project.ReloadMeshStatus.SUCCESS: # noqa + print("Reload succeeded") + else: + raise RuntimeError("Reload of mesh failed") - path = self.fname - substance_painter.project.reload_mesh(path, settings, on_mesh_reload) + path = self.fname + substance_painter.project.reload_mesh(path, + settings, + on_mesh_reload) - # TODO: Register with the project so host.get_containers() can return - # the loaded content in manager + # Store container + container = {} + imprint_container(container, + name=self.container_key, + namespace=self.container_key, + context=context, + loader=self) + container["options"] = data + set_container(self.container_key, container) def switch(self, container, representation): self.update(container, representation) @@ -78,9 +104,10 @@ class SubstanceLoadProjectMesh(load.LoaderPlugin): # Reload the mesh # TODO: Re-use settings from first load? + container_options = container.get("options", {}) settings = substance_painter.project.MeshReloadingSettings( - import_cameras=True, - preserve_strokes=True + import_cameras=container_options.get("import_cameras", True), + preserve_strokes=container_options.get("preserve_strokes", True) ) def on_mesh_reload(status: substance_painter.project.ReloadMeshStatus): @@ -91,8 +118,14 @@ class SubstanceLoadProjectMesh(load.LoaderPlugin): substance_painter.project.reload_mesh(path, settings, on_mesh_reload) + # Update container representation + container["representation"] = str(representation["_id"]) + set_container(self.container_key, container) + def remove(self, container): # Remove OpenPype related settings about what model was loaded # or close the project? - pass + # TODO: This is likely best 'hidden' away to the user because + # this will leave the project's mesh unmanaged. + remove_container(self.container_key) From 30764456afa4f92053b61d6a3e39576874c235a0 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 6 Jan 2023 05:22:59 +0100 Subject: [PATCH 006/360] Add launch with last workfile support for Substance Painter --- openpype/hooks/pre_add_last_workfile_arg.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hooks/pre_add_last_workfile_arg.py b/openpype/hooks/pre_add_last_workfile_arg.py index 3609620917..d5a9a41e5a 100644 --- a/openpype/hooks/pre_add_last_workfile_arg.py +++ b/openpype/hooks/pre_add_last_workfile_arg.py @@ -23,6 +23,7 @@ class AddLastWorkfileToLaunchArgs(PreLaunchHook): "blender", "photoshop", "tvpaint", + "substance", "aftereffects" ] From bcac4d1fafde2a3a2b7ce6f426d603d586b4df05 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 6 Jan 2023 12:17:49 +0100 Subject: [PATCH 007/360] Add draft for workfile Creator --- .../plugins/create/create_workfile.py | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 openpype/hosts/substancepainter/plugins/create/create_workfile.py diff --git a/openpype/hosts/substancepainter/plugins/create/create_workfile.py b/openpype/hosts/substancepainter/plugins/create/create_workfile.py new file mode 100644 index 0000000000..cec760040b --- /dev/null +++ b/openpype/hosts/substancepainter/plugins/create/create_workfile.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +"""Creator plugin for creating workfiles.""" + +from openpype.pipeline import CreatedInstance, AutoCreator +from openpype.pipeline import legacy_io +from openpype.client import get_asset_by_name + +import substance_painter.project + + +def set_workfile_data(data, update=False): + if update: + data = get_workfile_data().update(data) + metadata = substance_painter.project.Metadata("OpenPype") + metadata.set("workfile", data) + + +def get_workfile_data(): + metadata = substance_painter.project.Metadata("OpenPype") + return metadata.get("workfile") or {} + + +class CreateWorkfile(AutoCreator): + """Workfile auto-creator.""" + identifier = "io.openpype.creators.substancepainter.workfile" + label = "Workfile" + family = "workfile" + icon = "document" + + default_variant = "Main" + + def create(self): + + variant = self.default_variant + project_name = self.project_name + asset_name = legacy_io.Session["AVALON_ASSET"] + task_name = legacy_io.Session["AVALON_TASK"] + host_name = legacy_io.Session["AVALON_APP"] + + # Workfile instance should always exist and must only exist once. + # As such we'll first check if it already exists and is collected. + current_instance = next( + ( + instance for instance in self.create_context.instances + if instance.creator_identifier == self.identifier + ), None) + + if current_instance is None: + self.log.info("Auto-creating workfile instance...") + asset_doc = get_asset_by_name(project_name, asset_name) + subset_name = self.get_subset_name( + variant, task_name, asset_doc, project_name, host_name + ) + data = { + "asset": asset_name, + "task": task_name, + "variant": variant + } + current_instance = self.create_instance_in_context(subset_name, + data) + elif ( + current_instance["asset"] != asset_name + or current_instance["task"] != task_name + ): + # Update instance context if is not the same + asset_doc = get_asset_by_name(project_name, asset_name) + subset_name = self.get_subset_name( + variant, task_name, asset_doc, project_name, host_name + ) + current_instance["asset"] = asset_name + current_instance["task"] = task_name + current_instance["subset"] = subset_name + + set_workfile_data(current_instance.data_to_store()) + + def collect_instances(self): + workfile = get_workfile_data() + if not workfile: + return + self.create_instance_in_context_from_existing(workfile) + + def update_instances(self, update_list): + for instance, _changes in update_list: + set_workfile_data(instance.data_to_store(), update=True) + + # Helper methods (this might get moved into Creator class) + def create_instance_in_context(self, subset_name, data): + instance = CreatedInstance( + self.family, subset_name, data, self + ) + self.create_context.creator_adds_instance(instance) + return instance + + def create_instance_in_context_from_existing(self, data): + instance = CreatedInstance.from_existing(data, self) + self.create_context.creator_adds_instance(instance) + return instance From 1c4ff746adaee6e2ac34f765d57f64bda967765e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 6 Jan 2023 16:10:26 +0100 Subject: [PATCH 008/360] Remove 'fix' which didn't originally fix the UI issue - it was a styleSheet issue --- openpype/hosts/substancepainter/addon.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/openpype/hosts/substancepainter/addon.py b/openpype/hosts/substancepainter/addon.py index bb55f20189..6288ef1559 100644 --- a/openpype/hosts/substancepainter/addon.py +++ b/openpype/hosts/substancepainter/addon.py @@ -20,9 +20,6 @@ class SubstanceAddon(OpenPypeModule, IHostAddon): env["SUBSTANCE_PAINTER_PLUGINS_PATH"] = plugin_path - # Fix UI scale issue - env.pop("QT_AUTO_SCREEN_SCALE_FACTOR", None) - def get_launch_hook_paths(self, app): if app.host_name != self.host_name: return [] From 82639e8634587b7f63c703903c947c13f5e6f327 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 7 Jan 2023 16:18:07 +0100 Subject: [PATCH 009/360] Avoid trying to import blessed terminal coloring in Substance Painter --- openpype/hosts/substancepainter/addon.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/substancepainter/addon.py b/openpype/hosts/substancepainter/addon.py index 6288ef1559..2fbea139c5 100644 --- a/openpype/hosts/substancepainter/addon.py +++ b/openpype/hosts/substancepainter/addon.py @@ -20,6 +20,9 @@ class SubstanceAddon(OpenPypeModule, IHostAddon): env["SUBSTANCE_PAINTER_PLUGINS_PATH"] = plugin_path + # Log in Substance Painter doesn't support custom terminal colors + env["OPENPYPE_LOG_NO_COLORS"] = "Yes" + def get_launch_hook_paths(self, app): if app.host_name != self.host_name: return [] From c101f6a2cbce65bdf97d8ccc7812f85895f38bdc Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 7 Jan 2023 16:19:47 +0100 Subject: [PATCH 010/360] Cleanup OpenPype Qt widgets on Substance Painter shutdown --- .../deploy/plugins/openpype_plugin.py | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/substancepainter/deploy/plugins/openpype_plugin.py b/openpype/hosts/substancepainter/deploy/plugins/openpype_plugin.py index 01779156f1..e7e1849546 100644 --- a/openpype/hosts/substancepainter/deploy/plugins/openpype_plugin.py +++ b/openpype/hosts/substancepainter/deploy/plugins/openpype_plugin.py @@ -1,13 +1,34 @@ + +def cleanup_openpype_qt_widgets(): + """ + Workaround for Substance failing to shut down correctly + when a Qt window was still open at the time of shutting down. + + This seems to work sometimes, but not all the time. + + """ + # TODO: Create a more reliable method to close down all OpenPype Qt widgets + from PySide2 import QtWidgets + import substance_painter.ui + + # Kill OpenPype Qt widgets + print("Killing OpenPype Qt widgets..") + for widget in QtWidgets.QApplication.topLevelWidgets(): + if widget.__module__.startswith("openpype."): + print(f"Deleting widget: {widget.__class__.__name__}") + substance_painter.ui.delete_ui_element(widget) + + def start_plugin(): from openpype.pipeline import install_host from openpype.hosts.substancepainter.api import SubstanceHost - install_host(SubstanceHost()) def close_plugin(): from openpype.pipeline import uninstall_host + cleanup_openpype_qt_widgets() uninstall_host() From ccb4371641b79275702bc5557fefdf3c8d39c0a6 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 7 Jan 2023 17:42:43 +0100 Subject: [PATCH 011/360] Refactor metadata code to allow more structure for future Substance Painter plugins --- .../hosts/substancepainter/api/pipeline.py | 54 ++++++++++++++++- .../plugins/create/create_workfile.py | 27 ++++----- .../plugins/load/load_mesh.py | 58 +++++++++---------- 3 files changed, 91 insertions(+), 48 deletions(-) diff --git a/openpype/hosts/substancepainter/api/pipeline.py b/openpype/hosts/substancepainter/api/pipeline.py index 4d49fa83d7..e7dbe5e5eb 100644 --- a/openpype/hosts/substancepainter/api/pipeline.py +++ b/openpype/hosts/substancepainter/api/pipeline.py @@ -36,6 +36,10 @@ LOAD_PATH = os.path.join(PLUGINS_DIR, "load") CREATE_PATH = os.path.join(PLUGINS_DIR, "create") INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") + +OPENPYPE_METADATA_KEY = "OpenPype" +OPENPYPE_METADATA_CONTAINERS_KEY = "containers" # child key + self = sys.modules[__name__] self.menu = None self.callbacks = [] @@ -127,8 +131,8 @@ class SubstanceHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): if not substance_painter.project.is_open(): return - metadata = substance_painter.project.Metadata("OpenPype") - containers = metadata.get("containers") + metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY) + containers = metadata.get(OPENPYPE_METADATA_CONTAINERS_KEY) if containers: for key, container in containers.items(): container["objectName"] = key @@ -275,3 +279,49 @@ def imprint_container(container, ] for key, value in data: container[key] = value + + +def set_project_metadata(key, data): + """Set a key in project's OpenPype metadata.""" + metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY) + metadata.set(key, data) + + +def get_project_metadata(key): + """Get a key from project's OpenPype metadata.""" + metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY) + return metadata.get(key) + + +def set_container_metadata(object_name, container_data, update=False): + """Helper method to directly set the data for a specific container + + Args: + object_name (str): The unique object name identifier for the container + container_data (dict): The data for the container. + Note 'objectName' data is derived from `object_name` and key in + `container_data` will be ignored. + update (bool): Whether to only update the dict data. + + """ + # The objectName is derived from the key in the metadata so won't be stored + # in the metadata in the container's data. + container_data.pop("objectName", None) + + metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY) + containers = metadata.get(OPENPYPE_METADATA_CONTAINERS_KEY) or {} + if update: + existing_data = containers.setdefault(object_name, {}) + existing_data.update(container_data) # mutable dict, in-place update + else: + containers[object_name] = container_data + metadata.set("containers", containers) + + +def remove_container_metadata(object_name): + """Helper method to remove the data for a specific container""" + metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY) + containers = metadata.get(OPENPYPE_METADATA_CONTAINERS_KEY) + if containers: + containers.pop(object_name, None) + metadata.set("containers", containers) diff --git a/openpype/hosts/substancepainter/plugins/create/create_workfile.py b/openpype/hosts/substancepainter/plugins/create/create_workfile.py index cec760040b..8b010ebe2c 100644 --- a/openpype/hosts/substancepainter/plugins/create/create_workfile.py +++ b/openpype/hosts/substancepainter/plugins/create/create_workfile.py @@ -5,20 +5,10 @@ from openpype.pipeline import CreatedInstance, AutoCreator from openpype.pipeline import legacy_io from openpype.client import get_asset_by_name -import substance_painter.project - - -def set_workfile_data(data, update=False): - if update: - data = get_workfile_data().update(data) - metadata = substance_painter.project.Metadata("OpenPype") - metadata.set("workfile", data) - - -def get_workfile_data(): - metadata = substance_painter.project.Metadata("OpenPype") - return metadata.get("workfile") or {} - +from openpype.hosts.substancepainter.api.pipeline import ( + set_project_metadata, + get_project_metadata +) class CreateWorkfile(AutoCreator): """Workfile auto-creator.""" @@ -71,17 +61,20 @@ class CreateWorkfile(AutoCreator): current_instance["task"] = task_name current_instance["subset"] = subset_name - set_workfile_data(current_instance.data_to_store()) + set_project_metadata("workfile", current_instance.data_to_store()) def collect_instances(self): - workfile = get_workfile_data() + workfile = get_project_metadata("workfile") if not workfile: return self.create_instance_in_context_from_existing(workfile) def update_instances(self, update_list): for instance, _changes in update_list: - set_workfile_data(instance.data_to_store(), update=True) + # Update project's workfile metadata + data = get_project_metadata("workfile") or {} + data.update(instance.data_to_store()) + set_project_metadata("workfile", data) # Helper methods (this might get moved into Creator class) def create_instance_in_context(self, subset_name, data): diff --git a/openpype/hosts/substancepainter/plugins/load/load_mesh.py b/openpype/hosts/substancepainter/plugins/load/load_mesh.py index 519ed3ad4e..3e62b90988 100644 --- a/openpype/hosts/substancepainter/plugins/load/load_mesh.py +++ b/openpype/hosts/substancepainter/plugins/load/load_mesh.py @@ -2,27 +2,16 @@ from openpype.pipeline import ( load, get_representation_path, ) -from openpype.hosts.substancepainter.api.pipeline import imprint_container +from openpype.hosts.substancepainter.api.pipeline import ( + imprint_container, + set_container_metadata, + remove_container_metadata +) import substance_painter.project import qargparse -def set_container(key, container): - metadata = substance_painter.project.Metadata("OpenPype") - containers = metadata.get("containers") or {} - containers[key] = container - metadata.set("containers", containers) - - -def remove_container(key): - metadata = substance_painter.project.Metadata("OpenPype") - containers = metadata.get("containers") - if containers: - containers.pop(key, None) - metadata.set("containers", containers) - - class SubstanceLoadProjectMesh(load.LoaderPlugin): """Load mesh for project""" @@ -48,10 +37,12 @@ class SubstanceLoadProjectMesh(load.LoaderPlugin): ) ] - container_key = "ProjectMesh" - def load(self, context, name, namespace, data): + # Get user inputs + import_cameras = data.get("import_cameras", True) + preserve_strokes = data.get("preserve_strokes", True) + if not substance_painter.project.is_open(): # Allow to 'initialize' a new project # TODO: preferably these settings would come from the actual @@ -59,7 +50,7 @@ class SubstanceLoadProjectMesh(load.LoaderPlugin): # visually similar to still allow artist decisions) settings = substance_painter.project.Settings( default_texture_resolution=4096, - import_cameras=data.get("import_cameras", True), + import_cameras=import_cameras, ) substance_painter.project.create( @@ -70,8 +61,8 @@ class SubstanceLoadProjectMesh(load.LoaderPlugin): else: # Reload the mesh settings = substance_painter.project.MeshReloadingSettings( - import_cameras=data.get("import_cameras", True), - preserve_strokes=data.get("preserve_strokes", True) + import_cameras=import_cameras, + preserve_strokes=preserve_strokes ) def on_mesh_reload(status: substance_painter.project.ReloadMeshStatus): # noqa @@ -87,13 +78,21 @@ class SubstanceLoadProjectMesh(load.LoaderPlugin): # Store container container = {} + project_mesh_object_name = "_ProjectMesh_" imprint_container(container, - name=self.container_key, - namespace=self.container_key, + name=project_mesh_object_name, + namespace=project_mesh_object_name, context=context, loader=self) - container["options"] = data - set_container(self.container_key, container) + + # We want store some options for updating to keep consistent behavior + # from the user's original choice. We don't store 'preserve_strokes' + # as we always preserve strokes on updates. + container["options"] = { + "import_cameras": import_cameras, + } + + set_container_metadata(project_mesh_object_name, container) def switch(self, container, representation): self.update(container, representation) @@ -107,7 +106,7 @@ class SubstanceLoadProjectMesh(load.LoaderPlugin): container_options = container.get("options", {}) settings = substance_painter.project.MeshReloadingSettings( import_cameras=container_options.get("import_cameras", True), - preserve_strokes=container_options.get("preserve_strokes", True) + preserve_strokes=True ) def on_mesh_reload(status: substance_painter.project.ReloadMeshStatus): @@ -119,8 +118,9 @@ class SubstanceLoadProjectMesh(load.LoaderPlugin): substance_painter.project.reload_mesh(path, settings, on_mesh_reload) # Update container representation - container["representation"] = str(representation["_id"]) - set_container(self.container_key, container) + object_name = container["objectName"] + update_data = {"representation": str(representation["_id"])} + set_container_metadata(object_name, update_data, update=True) def remove(self, container): @@ -128,4 +128,4 @@ class SubstanceLoadProjectMesh(load.LoaderPlugin): # or close the project? # TODO: This is likely best 'hidden' away to the user because # this will leave the project's mesh unmanaged. - remove_container(self.container_key) + remove_container_metadata(container["objectName"]) From cf92213dd1fde6efb5ab117a1d4e4b7a96b188d5 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 7 Jan 2023 17:42:55 +0100 Subject: [PATCH 012/360] Cosmetics --- .../hosts/substancepainter/plugins/create/create_workfile.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/substancepainter/plugins/create/create_workfile.py b/openpype/hosts/substancepainter/plugins/create/create_workfile.py index 8b010ebe2c..4b34f4cc8c 100644 --- a/openpype/hosts/substancepainter/plugins/create/create_workfile.py +++ b/openpype/hosts/substancepainter/plugins/create/create_workfile.py @@ -10,6 +10,7 @@ from openpype.hosts.substancepainter.api.pipeline import ( get_project_metadata ) + class CreateWorkfile(AutoCreator): """Workfile auto-creator.""" identifier = "io.openpype.creators.substancepainter.workfile" From c34f8fed24a7c84ce22a615b5f438798b2f461c4 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 9 Jan 2023 10:29:44 +0100 Subject: [PATCH 013/360] Bypass silently if a project was not open when querying metadata --- openpype/hosts/substancepainter/api/pipeline.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/substancepainter/api/pipeline.py b/openpype/hosts/substancepainter/api/pipeline.py index e7dbe5e5eb..70353039f5 100644 --- a/openpype/hosts/substancepainter/api/pipeline.py +++ b/openpype/hosts/substancepainter/api/pipeline.py @@ -289,6 +289,9 @@ def set_project_metadata(key, data): def get_project_metadata(key): """Get a key from project's OpenPype metadata.""" + if not substance_painter.project.is_open(): + return + metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY) return metadata.get(key) From 2c544246fd855de080387e1f86a053e5fd31e12f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 9 Jan 2023 10:30:18 +0100 Subject: [PATCH 014/360] Do not auto create workfile instance if project isn't open. --- .../hosts/substancepainter/plugins/create/create_workfile.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/hosts/substancepainter/plugins/create/create_workfile.py b/openpype/hosts/substancepainter/plugins/create/create_workfile.py index 4b34f4cc8c..22e12b4079 100644 --- a/openpype/hosts/substancepainter/plugins/create/create_workfile.py +++ b/openpype/hosts/substancepainter/plugins/create/create_workfile.py @@ -10,6 +10,8 @@ from openpype.hosts.substancepainter.api.pipeline import ( get_project_metadata ) +import substance_painter.project + class CreateWorkfile(AutoCreator): """Workfile auto-creator.""" @@ -22,6 +24,9 @@ class CreateWorkfile(AutoCreator): def create(self): + if not substance_painter.project.is_open(): + return + variant = self.default_variant project_name = self.project_name asset_name = legacy_io.Session["AVALON_ASSET"] From ec2f10caf383a769fd90a3777ee47568054b6d41 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 9 Jan 2023 10:30:32 +0100 Subject: [PATCH 015/360] Simplify logic --- .../hosts/substancepainter/plugins/create/create_workfile.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/substancepainter/plugins/create/create_workfile.py b/openpype/hosts/substancepainter/plugins/create/create_workfile.py index 22e12b4079..729cc8f718 100644 --- a/openpype/hosts/substancepainter/plugins/create/create_workfile.py +++ b/openpype/hosts/substancepainter/plugins/create/create_workfile.py @@ -71,9 +71,8 @@ class CreateWorkfile(AutoCreator): def collect_instances(self): workfile = get_project_metadata("workfile") - if not workfile: - return - self.create_instance_in_context_from_existing(workfile) + if workfile: + self.create_instance_in_context_from_existing(workfile) def update_instances(self, update_list): for instance, _changes in update_list: From c3fca896d48f82026aea0f81055a996c366ea920 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 9 Jan 2023 11:16:23 +0100 Subject: [PATCH 016/360] Implement plug-ins to support workfile publishing --- .../plugins/publish/collect_current_file.py | 17 ++++++++++++ .../collect_workfile_representation.py | 26 +++++++++++++++++++ .../plugins/publish/increment_workfile.py | 23 ++++++++++++++++ .../plugins/publish/save_workfile.py | 23 ++++++++++++++++ 4 files changed, 89 insertions(+) create mode 100644 openpype/hosts/substancepainter/plugins/publish/collect_current_file.py create mode 100644 openpype/hosts/substancepainter/plugins/publish/collect_workfile_representation.py create mode 100644 openpype/hosts/substancepainter/plugins/publish/increment_workfile.py create mode 100644 openpype/hosts/substancepainter/plugins/publish/save_workfile.py diff --git a/openpype/hosts/substancepainter/plugins/publish/collect_current_file.py b/openpype/hosts/substancepainter/plugins/publish/collect_current_file.py new file mode 100644 index 0000000000..dac493bbf1 --- /dev/null +++ b/openpype/hosts/substancepainter/plugins/publish/collect_current_file.py @@ -0,0 +1,17 @@ +import pyblish.api + +from openpype.pipeline import registered_host + + +class CollectCurrentFile(pyblish.api.ContextPlugin): + """Inject the current working file into context""" + + order = pyblish.api.CollectorOrder - 0.49 + label = "Current Workfile" + hosts = ["substancepainter"] + + def process(self, context): + host = registered_host() + path = host.get_current_workfile() + context.data["currentFile"] = path + self.log.debug(f"Current workfile: {path}") \ No newline at end of file diff --git a/openpype/hosts/substancepainter/plugins/publish/collect_workfile_representation.py b/openpype/hosts/substancepainter/plugins/publish/collect_workfile_representation.py new file mode 100644 index 0000000000..563c2d4c07 --- /dev/null +++ b/openpype/hosts/substancepainter/plugins/publish/collect_workfile_representation.py @@ -0,0 +1,26 @@ +import os +import pyblish.api + + +class CollectWorkfileRepresentation(pyblish.api.InstancePlugin): + """Create a publish representation for the current workfile instance.""" + + order = pyblish.api.CollectorOrder + label = "Workfile representation" + hosts = ['substancepainter'] + families = ["workfile"] + + def process(self, instance): + + context = instance.context + current_file = context.data["currentFile"] + + folder, file = os.path.split(current_file) + filename, ext = os.path.splitext(file) + + instance.data['representations'] = [{ + 'name': ext.lstrip("."), + 'ext': ext.lstrip("."), + 'files': file, + "stagingDir": folder, + }] diff --git a/openpype/hosts/substancepainter/plugins/publish/increment_workfile.py b/openpype/hosts/substancepainter/plugins/publish/increment_workfile.py new file mode 100644 index 0000000000..b45d66fbb1 --- /dev/null +++ b/openpype/hosts/substancepainter/plugins/publish/increment_workfile.py @@ -0,0 +1,23 @@ +import pyblish.api + +from openpype.lib import version_up +from openpype.pipeline import registered_host + + +class IncrementWorkfileVersion(pyblish.api.ContextPlugin): + """Increment current workfile version.""" + + order = pyblish.api.IntegratorOrder + 1 + label = "Increment Workfile Version" + optional = True + hosts = ["substancepainter"] + + def process(self, context): + + assert all(result["success"] for result in context.data["results"]), ( + "Publishing not successful so version is not increased.") + + host = registered_host() + path = context.data["currentFile"] + self.log.info(f"Incrementing current workfile to: {path}") + host.save_workfile(version_up(path)) diff --git a/openpype/hosts/substancepainter/plugins/publish/save_workfile.py b/openpype/hosts/substancepainter/plugins/publish/save_workfile.py new file mode 100644 index 0000000000..5e86785e0d --- /dev/null +++ b/openpype/hosts/substancepainter/plugins/publish/save_workfile.py @@ -0,0 +1,23 @@ +import pyblish.api + +from openpype.pipeline import registered_host + + +class SaveCurrentWorkfile(pyblish.api.ContextPlugin): + """Save current workfile""" + + label = "Save current workfile" + order = pyblish.api.ExtractorOrder - 0.49 + hosts = ["substancepainter"] + + def process(self, context): + + host = registered_host() + assert context.data['currentFile'] == host.get_current_workfile() + + if host.has_unsaved_changes(): + self.log.info("Saving current file..") + host.save_workfile() + else: + self.log.debug("Skipping workfile save because there are no " + "unsaved changes.") From 564e8f4d40febfb08b65fc31e10b710d38cbddc7 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 9 Jan 2023 11:17:25 +0100 Subject: [PATCH 017/360] Cosmetics --- .../substancepainter/plugins/publish/collect_current_file.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/substancepainter/plugins/publish/collect_current_file.py b/openpype/hosts/substancepainter/plugins/publish/collect_current_file.py index dac493bbf1..9a37eb0d1c 100644 --- a/openpype/hosts/substancepainter/plugins/publish/collect_current_file.py +++ b/openpype/hosts/substancepainter/plugins/publish/collect_current_file.py @@ -14,4 +14,4 @@ class CollectCurrentFile(pyblish.api.ContextPlugin): host = registered_host() path = host.get_current_workfile() context.data["currentFile"] = path - self.log.debug(f"Current workfile: {path}") \ No newline at end of file + self.log.debug(f"Current workfile: {path}") From f9d3c9f77227fef2ddcf43649e69d0fb88d4e2bd Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 9 Jan 2023 18:13:49 +0100 Subject: [PATCH 018/360] Early prototype for Texture publishing in Substance Painter (WIP - not functional; doesn't integrate yet) --- .../plugins/create/create_textures.py | 149 ++++++++++++++++++ .../plugins/publish/extract_textures.py | 71 +++++++++ 2 files changed, 220 insertions(+) create mode 100644 openpype/hosts/substancepainter/plugins/create/create_textures.py create mode 100644 openpype/hosts/substancepainter/plugins/publish/extract_textures.py diff --git a/openpype/hosts/substancepainter/plugins/create/create_textures.py b/openpype/hosts/substancepainter/plugins/create/create_textures.py new file mode 100644 index 0000000000..af2e23b3bf --- /dev/null +++ b/openpype/hosts/substancepainter/plugins/create/create_textures.py @@ -0,0 +1,149 @@ +# -*- coding: utf-8 -*- +"""Creator plugin for creating textures.""" +import os + +from openpype.pipeline import CreatedInstance, Creator + +from openpype.hosts.substancepainter.api.pipeline import ( + set_project_metadata, + get_project_metadata +) + +from openpype.lib import ( + EnumDef, + UILabelDef, + NumberDef +) + +import substance_painter.project +import substance_painter.resource + + +def get_export_presets(): + import substance_painter.resource + + preset_resources = {} + + # TODO: Find more optimal way to find all export templates + for shelf in substance_painter.resource.Shelves.all(): + shelf_path = os.path.normpath(shelf.path()) + + presets_path = os.path.join(shelf_path, "export-presets") + if not os.path.exists(presets_path): + continue + + for fname in os.listdir(presets_path): + if fname.endswith(".spexp"): + template_name = os.path.splitext(fname)[0] + + resource = substance_painter.resource.ResourceID( + context=shelf.name(), + name=template_name + ) + resource_url = resource.url() + + preset_resources[resource_url] = template_name + + # Sort by template name + export_templates = dict(sorted(preset_resources.items(), + key=lambda x: x[1])) + + return export_templates + + +class CreateTextures(Creator): + """Create a texture set.""" + identifier = "io.openpype.creators.substancepainter.textures" + label = "Textures" + family = "textures" + icon = "picture-o" + + default_variant = "Main" + + def create(self, subset_name, instance_data, pre_create_data): + + if not substance_painter.project.is_open(): + return + + instance = self.create_instance_in_context(subset_name, instance_data) + set_project_metadata("textures", instance.data_to_store()) + + def collect_instances(self): + workfile = get_project_metadata("textures") + if workfile: + self.create_instance_in_context_from_existing(workfile) + + def update_instances(self, update_list): + for instance, _changes in update_list: + # Update project's metadata + data = get_project_metadata("textures") or {} + data.update(instance.data_to_store()) + set_project_metadata("textures", data) + + def remove_instances(self, instances): + for instance in instances: + # TODO: Implement removal + # api.remove_instance(instance) + self._remove_instance_from_context(instance) + + # Helper methods (this might get moved into Creator class) + def create_instance_in_context(self, subset_name, data): + instance = CreatedInstance( + self.family, subset_name, data, self + ) + self.create_context.creator_adds_instance(instance) + return instance + + def create_instance_in_context_from_existing(self, data): + instance = CreatedInstance.from_existing(data, self) + self.create_context.creator_adds_instance(instance) + return instance + + def get_instance_attr_defs(self): + + return [ + EnumDef("exportPresetUrl", + items=get_export_presets(), + label="Output Template"), + EnumDef("exportFileFormat", + items={ + None: "Based on output template", + # TODO: implement extensions + }, + label="File type"), + EnumDef("exportSize", + items={ + None: "Based on each Texture Set's size", + # The key is size of the texture file in log2. + # (i.e. 10 means 2^10 = 1024) + 7: "128", + 8: "256", + 9: "512", + 10: "1024", + 11: "2048", + 12: "4096" + }, + label="Size"), + + EnumDef("exportPadding", + items={ + "passthrough": "No padding (passthrough)", + "infinite": "Dilation infinite", + "transparent": "Dilation + transparent", + "color": "Dilation + default background color", + "diffusion": "Dilation + diffusion" + }, + label="Padding"), + NumberDef("exportDilationDistance", + minimum=0, + maximum=256, + decimals=0, + default=16, + label="Dilation Distance"), + UILabelDef("Note: Dilation Distance is only used with " + "'Dilation + ' padding options"), + ] + + def get_pre_create_attr_defs(self): + # Use same attributes as for instance attributes + return self.get_instance_attr_defs() diff --git a/openpype/hosts/substancepainter/plugins/publish/extract_textures.py b/openpype/hosts/substancepainter/plugins/publish/extract_textures.py new file mode 100644 index 0000000000..93e0c8cb31 --- /dev/null +++ b/openpype/hosts/substancepainter/plugins/publish/extract_textures.py @@ -0,0 +1,71 @@ +from openpype.pipeline import KnownPublishError, publish + +import substance_painter.export + + +class ExtractTextures(publish.Extractor): + """Extract Textures using an output template config""" + + label = "Extract Texture Sets" + hosts = ['substancepainter'] + families = ["textures"] + + def process(self, instance): + + staging_dir = self.staging_dir(instance) + + # See: https://substance3d.adobe.com/documentation/ptpy/api/substance_painter/export # noqa + creator_attrs = instance.data["creator_attributes"] + config = { + "exportShaderParams": True, + "exportPath": staging_dir, + "defaultExportPreset": creator_attrs["exportPresetUrl"], + + # Custom overrides to the exporter + "exportParameters": [ + { + "parameters": { + "fileFormat": creator_attrs["exportFileFormat"], + "sizeLog2": creator_attrs["exportSize"], + "paddingAlgorithm": creator_attrs["exportPadding"], + "dilationDistance": creator_attrs["exportDilationDistance"] # noqa + } + } + ] + } + + # Create the list of Texture Sets to export. + config["exportList"] = [] + for texture_set in substance_painter.textureset.all_texture_sets(): + # stack = texture_set.get_stack() + config["exportList"].append({"rootPath": texture_set.name()}) + + # Consider None values optionals + for override in config["exportParameters"]: + parameters = override.get("parameters") + for key, value in dict(parameters).items(): + if value is None: + parameters.pop(key) + + result = substance_painter.export.export_project_textures(config) + + if result.status != substance_painter.export.ExportStatus.Success: + raise KnownPublishError( + "Failed to export texture set: {}".format(result.message) + ) + + files = [] + for stack, maps in result.textures.items(): + for texture_map in maps: + self.log.info(f"Exported texture: {texture_map}") + files.append(texture_map) + + # TODO: add the representations so they integrate the way we'd want + """ + instance.data['representations'] = [{ + 'name': ext.lstrip("."), + 'ext': ext.lstrip("."), + 'files': file, + "stagingDir": folder, + }] + """ From 0741c9850861779974e95cf764c3a7d2f0b097cc Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 9 Jan 2023 18:15:06 +0100 Subject: [PATCH 019/360] Cosmetics --- .../hosts/substancepainter/plugins/publish/extract_textures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/substancepainter/plugins/publish/extract_textures.py b/openpype/hosts/substancepainter/plugins/publish/extract_textures.py index 93e0c8cb31..d72d9920fd 100644 --- a/openpype/hosts/substancepainter/plugins/publish/extract_textures.py +++ b/openpype/hosts/substancepainter/plugins/publish/extract_textures.py @@ -55,7 +55,7 @@ class ExtractTextures(publish.Extractor): ) files = [] - for stack, maps in result.textures.items(): + for _stack, maps in result.textures.items(): for texture_map in maps: self.log.info(f"Exported texture: {texture_map}") files.append(texture_map) From 87f23c978d44d587e74adfb2d517da798dfecafe Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 10 Jan 2023 00:52:07 +0100 Subject: [PATCH 020/360] Add the built-in `export-preset-generator` template entries --- .../plugins/create/create_textures.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/substancepainter/plugins/create/create_textures.py b/openpype/hosts/substancepainter/plugins/create/create_textures.py index af2e23b3bf..41de2ad946 100644 --- a/openpype/hosts/substancepainter/plugins/create/create_textures.py +++ b/openpype/hosts/substancepainter/plugins/create/create_textures.py @@ -48,7 +48,20 @@ def get_export_presets(): export_templates = dict(sorted(preset_resources.items(), key=lambda x: x[1])) - return export_templates + # Add default built-ins at the start + # TODO: find the built-ins automatically; scraped with https://gist.github.com/BigRoy/97150c7c6f0a0c916418207b9a2bc8f1 # noqa + result = { + "export-preset-generator://viewport2d": "2D View", # noqa + "export-preset-generator://doc-channel-normal-no-alpha": "Document channels + Normal + AO (No Alpha)", # noqa + "export-preset-generator://doc-channel-normal-with-alpha": "Document channels + Normal + AO (With Alpha)", # noqa + "export-preset-generator://sketchfab": "Sketchfab", # noqa + "export-preset-generator://adobe-standard-material": "Substance 3D Stager", # noqa + "export-preset-generator://usd": "USD PBR Metal Roughness", # noqa + "export-preset-generator://gltf": "glTF PBR Metal Roughness", # noqa + "export-preset-generator://gltf-displacement": "glTF PBR Metal Roughness + Displacement texture (experimental)" # noqa + } + result.update(export_templates) + return result class CreateTextures(Creator): From 9a4f5650199000658e93e189810cca7b1482e9ed Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 10 Jan 2023 01:21:08 +0100 Subject: [PATCH 021/360] Shorten label --- .../hosts/substancepainter/plugins/create/create_textures.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/substancepainter/plugins/create/create_textures.py b/openpype/hosts/substancepainter/plugins/create/create_textures.py index 41de2ad946..c1d907a974 100644 --- a/openpype/hosts/substancepainter/plugins/create/create_textures.py +++ b/openpype/hosts/substancepainter/plugins/create/create_textures.py @@ -153,8 +153,8 @@ class CreateTextures(Creator): decimals=0, default=16, label="Dilation Distance"), - UILabelDef("Note: Dilation Distance is only used with " - "'Dilation + ' padding options"), + UILabelDef("*only used with " + "'Dilation + ' padding"), ] def get_pre_create_attr_defs(self): From 139eafb5c7e951dcc08fa1c1a8e7e5bf2a4928d1 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 10 Jan 2023 01:21:31 +0100 Subject: [PATCH 022/360] Debug log used Substance Painter export preset --- .../substancepainter/plugins/publish/extract_textures.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/substancepainter/plugins/publish/extract_textures.py b/openpype/hosts/substancepainter/plugins/publish/extract_textures.py index d72d9920fd..8ebad3193f 100644 --- a/openpype/hosts/substancepainter/plugins/publish/extract_textures.py +++ b/openpype/hosts/substancepainter/plugins/publish/extract_textures.py @@ -14,12 +14,15 @@ class ExtractTextures(publish.Extractor): staging_dir = self.staging_dir(instance) - # See: https://substance3d.adobe.com/documentation/ptpy/api/substance_painter/export # noqa creator_attrs = instance.data["creator_attributes"] + preset_url = creator_attrs["exportPresetUrl"] + self.log.debug(f"Exporting using preset: {preset_url}") + + # See: https://substance3d.adobe.com/documentation/ptpy/api/substance_painter/export # noqa config = { "exportShaderParams": True, "exportPath": staging_dir, - "defaultExportPreset": creator_attrs["exportPresetUrl"], + "defaultExportPreset": preset_url, # Custom overrides to the exporter "exportParameters": [ From 391ba1ada24ffb275443a47f008b6afce2feba52 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 10 Jan 2023 11:21:55 +0100 Subject: [PATCH 023/360] Remove unusued imports --- openpype/hosts/substancepainter/api/pipeline.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/substancepainter/api/pipeline.py b/openpype/hosts/substancepainter/api/pipeline.py index 70353039f5..aae1f39a3e 100644 --- a/openpype/hosts/substancepainter/api/pipeline.py +++ b/openpype/hosts/substancepainter/api/pipeline.py @@ -8,9 +8,7 @@ from functools import partial # Substance 3D Painter modules import substance_painter.ui import substance_painter.event -import substance_painter.export import substance_painter.project -import substance_painter.textureset from openpype.host import HostBase, IWorkfileHost, ILoadHost, IPublishHost From c1abd00bba43cb98501efd649462c990414f720c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 10 Jan 2023 16:33:17 +0100 Subject: [PATCH 024/360] Store menu and callbacks on the SubstanceHost instance --- .../hosts/substancepainter/api/pipeline.py | 120 +++++++++--------- 1 file changed, 57 insertions(+), 63 deletions(-) diff --git a/openpype/hosts/substancepainter/api/pipeline.py b/openpype/hosts/substancepainter/api/pipeline.py index aae1f39a3e..db4bb47401 100644 --- a/openpype/hosts/substancepainter/api/pipeline.py +++ b/openpype/hosts/substancepainter/api/pipeline.py @@ -34,14 +34,9 @@ LOAD_PATH = os.path.join(PLUGINS_DIR, "load") CREATE_PATH = os.path.join(PLUGINS_DIR, "create") INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") - OPENPYPE_METADATA_KEY = "OpenPype" OPENPYPE_METADATA_CONTAINERS_KEY = "containers" # child key -self = sys.modules[__name__] -self.menu = None -self.callbacks = [] - class SubstanceHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): name = "substancepainter" @@ -49,6 +44,8 @@ class SubstanceHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): def __init__(self): super(SubstanceHost, self).__init__() self._has_been_setup = False + self.menu = None + self.callbacks = [] def install(self): pyblish.api.register_host("substancepainter") @@ -59,20 +56,20 @@ class SubstanceHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): log.info("Installing callbacks ... ") # register_event_callback("init", on_init) - _register_callbacks() + self._register_callbacks() # register_event_callback("before.save", before_save) # register_event_callback("save", on_save) register_event_callback("open", on_open) # register_event_callback("new", on_new) log.info("Installing menu ... ") - _install_menu() + self._install_menu() self._has_been_setup = True def uninstall(self): - _uninstall_menu() - _deregister_callbacks() + self._uninstall_menu() + self._deregister_callbacks() def has_unsaved_changes(self): @@ -146,74 +143,71 @@ class SubstanceHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): def get_context_data(self): pass + def _install_menu(self): + from PySide2 import QtWidgets + from openpype.tools.utils import host_tools -def _install_menu(): - from PySide2 import QtWidgets - from openpype.tools.utils import host_tools + parent = substance_painter.ui.get_main_window() - parent = substance_painter.ui.get_main_window() + menu = QtWidgets.QMenu("OpenPype") - menu = QtWidgets.QMenu("OpenPype") + action = menu.addAction("Load...") + action.triggered.connect( + lambda: host_tools.show_loader(parent=parent, use_context=True) + ) - action = menu.addAction("Load...") - action.triggered.connect( - lambda: host_tools.show_loader(parent=parent, use_context=True) - ) + action = menu.addAction("Publish...") + action.triggered.connect( + lambda: host_tools.show_publisher(parent=parent) + ) - action = menu.addAction("Publish...") - action.triggered.connect( - lambda: host_tools.show_publisher(parent=parent) - ) + action = menu.addAction("Manage...") + action.triggered.connect( + lambda: host_tools.show_scene_inventory(parent=parent) + ) - action = menu.addAction("Manage...") - action.triggered.connect( - lambda: host_tools.show_scene_inventory(parent=parent) - ) + action = menu.addAction("Library...") + action.triggered.connect( + lambda: host_tools.show_library_loader(parent=parent) + ) - action = menu.addAction("Library...") - action.triggered.connect( - lambda: host_tools.show_library_loader(parent=parent) - ) + menu.addSeparator() + action = menu.addAction("Work Files...") + action.triggered.connect( + lambda: host_tools.show_workfiles(parent=parent) + ) - menu.addSeparator() - action = menu.addAction("Work Files...") - action.triggered.connect( - lambda: host_tools.show_workfiles(parent=parent) - ) + substance_painter.ui.add_menu(menu) - substance_painter.ui.add_menu(menu) + def on_menu_destroyed(): + self.menu = None - def on_menu_destroyed(): - self.menu = None + menu.destroyed.connect(on_menu_destroyed) - menu.destroyed.connect(on_menu_destroyed) + self.menu = menu - self.menu = menu + def _uninstall_menu(self): + if self.menu: + self.menu.destroy() + self.menu = None + + def _register_callbacks(self): + # Prepare emit event callbacks + open_callback = partial(emit_event, "open") + + # Connect to the Substance Painter events + dispatcher = substance_painter.event.DISPATCHER + for event, callback in [ + (substance_painter.event.ProjectOpened, open_callback) + ]: + dispatcher.connect(event, callback) + # Keep a reference so we can deregister if needed + self.callbacks.append((event, callback)) -def _uninstall_menu(): - if self.menu: - self.menu.destroy() - self.menu = None - - -def _register_callbacks(): - # Prepare emit event callbacks - open_callback = partial(emit_event, "open") - - # Connect to the Substance Painter events - dispatcher = substance_painter.event.DISPATCHER - for event, callback in [ - (substance_painter.event.ProjectOpened, open_callback) - ]: - dispatcher.connect(event, callback) - # Keep a reference so we can deregister if needed - self.callbacks.append((event, callback)) - - -def _deregister_callbacks(): - for event, callback in self.callbacks: - substance_painter.event.DISPATCHER.disconnect(event, callback) + def _deregister_callbacks(self): + for event, callback in self.callbacks: + substance_painter.event.DISPATCHER.disconnect(event, callback) def on_open(): From df5300ed32a0a4cff5af52a930c535773238deda Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 10 Jan 2023 16:33:33 +0100 Subject: [PATCH 025/360] Cosmetics --- openpype/hosts/substancepainter/api/pipeline.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/substancepainter/api/pipeline.py b/openpype/hosts/substancepainter/api/pipeline.py index db4bb47401..48adc107e2 100644 --- a/openpype/hosts/substancepainter/api/pipeline.py +++ b/openpype/hosts/substancepainter/api/pipeline.py @@ -204,7 +204,6 @@ class SubstanceHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): # Keep a reference so we can deregister if needed self.callbacks.append((event, callback)) - def _deregister_callbacks(self): for event, callback in self.callbacks: substance_painter.event.DISPATCHER.disconnect(event, callback) From 3b4f9feaadfaaee4ae763a78744a274cd467e744 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 10 Jan 2023 16:34:20 +0100 Subject: [PATCH 026/360] Remove unused import --- openpype/hosts/substancepainter/api/pipeline.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/substancepainter/api/pipeline.py b/openpype/hosts/substancepainter/api/pipeline.py index 48adc107e2..df705bb010 100644 --- a/openpype/hosts/substancepainter/api/pipeline.py +++ b/openpype/hosts/substancepainter/api/pipeline.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- """Pipeline tools for OpenPype Gaffer integration.""" import os -import sys import logging from functools import partial From 5a7c5762847ed22f89a26d09f062a0948c34397b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 10 Jan 2023 16:44:09 +0100 Subject: [PATCH 027/360] Remove debug print message --- openpype/hosts/substancepainter/api/pipeline.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/substancepainter/api/pipeline.py b/openpype/hosts/substancepainter/api/pipeline.py index df705bb010..3a68a7fa86 100644 --- a/openpype/hosts/substancepainter/api/pipeline.py +++ b/openpype/hosts/substancepainter/api/pipeline.py @@ -210,7 +210,6 @@ class SubstanceHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): def on_open(): log.info("Running callback on open..") - print("Run") if any_outdated_containers(): from openpype.widgets import popup From 24b6583c63ea14920bc6a56649c7db6ed1e3176c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 10 Jan 2023 17:58:47 +0100 Subject: [PATCH 028/360] Set explicit defaults for creator --- .../hosts/substancepainter/plugins/create/create_textures.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/substancepainter/plugins/create/create_textures.py b/openpype/hosts/substancepainter/plugins/create/create_textures.py index c1d907a974..6d4f816961 100644 --- a/openpype/hosts/substancepainter/plugins/create/create_textures.py +++ b/openpype/hosts/substancepainter/plugins/create/create_textures.py @@ -123,6 +123,7 @@ class CreateTextures(Creator): None: "Based on output template", # TODO: implement extensions }, + default=None, label="File type"), EnumDef("exportSize", items={ @@ -136,6 +137,7 @@ class CreateTextures(Creator): 11: "2048", 12: "4096" }, + default=None, label="Size"), EnumDef("exportPadding", @@ -146,6 +148,7 @@ class CreateTextures(Creator): "color": "Dilation + default background color", "diffusion": "Dilation + diffusion" }, + default="infinite", label="Padding"), NumberDef("exportDilationDistance", minimum=0, From 61710d614d5753b2287c9c5be5110147bd4612b0 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 12 Jan 2023 13:23:51 +0100 Subject: [PATCH 029/360] TODO was already resolved --- openpype/hosts/substancepainter/plugins/load/load_mesh.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/substancepainter/plugins/load/load_mesh.py b/openpype/hosts/substancepainter/plugins/load/load_mesh.py index 3e62b90988..00f808199f 100644 --- a/openpype/hosts/substancepainter/plugins/load/load_mesh.py +++ b/openpype/hosts/substancepainter/plugins/load/load_mesh.py @@ -102,7 +102,6 @@ class SubstanceLoadProjectMesh(load.LoaderPlugin): path = get_representation_path(representation) # Reload the mesh - # TODO: Re-use settings from first load? container_options = container.get("options", {}) settings = substance_painter.project.MeshReloadingSettings( import_cameras=container_options.get("import_cameras", True), From 2177877713f538f70217a944014212fc183c7412 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 12 Jan 2023 14:47:38 +0100 Subject: [PATCH 030/360] Load OpenPype plug-in on first run of Substance Painter through OpenPype --- .../startup/openpype_load_on_first_run.py | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 openpype/hosts/substancepainter/deploy/startup/openpype_load_on_first_run.py diff --git a/openpype/hosts/substancepainter/deploy/startup/openpype_load_on_first_run.py b/openpype/hosts/substancepainter/deploy/startup/openpype_load_on_first_run.py new file mode 100644 index 0000000000..90b1ec6bbd --- /dev/null +++ b/openpype/hosts/substancepainter/deploy/startup/openpype_load_on_first_run.py @@ -0,0 +1,43 @@ +"""Ease the OpenPype on-boarding process by loading the plug-in on first run""" + +OPENPYPE_PLUGIN_NAME = "openpype_plugin" + + +def start_plugin(): + try: + # This isn't exposed in the official API so we keep it in a try-except + from painter_plugins_ui import ( + get_settings, + LAUNCH_AT_START_KEY, + ON_STATE, + PLUGINS_MENU, + plugin_manager + ) + + # The `painter_plugins_ui` plug-in itself is also a startup plug-in + # we need to take into account that it could run either earlier or + # later than this startup script, we check whether its menu initialized + is_before_plugins_menu = PLUGINS_MENU is None + + settings = get_settings(OPENPYPE_PLUGIN_NAME) + if settings.value(LAUNCH_AT_START_KEY, None) is not None: + print("Initializing OpenPype plug-in on first run...") + if is_before_plugins_menu: + print("- running before 'painter_plugins_ui'") + # Delay the launch to the painter_plugins_ui initialization + settings.setValue(LAUNCH_AT_START_KEY, ON_STATE) + else: + # Launch now + print("- running after 'painter_plugins_ui'") + plugin_manager(OPENPYPE_PLUGIN_NAME)(True) + + # Set the checked state in the menu to avoid confusion + action = next(action for action in PLUGINS_MENU._menu.actions() + if action.text() == OPENPYPE_PLUGIN_NAME) + if action is not None: + action.blockSignals(True) + action.setChecked(True) + action.blockSignals(False) + + except Exception as exc: + print(exc) From d1d15683983db8d3d9ca9e1a121b794b9b0acf3e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 12 Jan 2023 14:54:07 +0100 Subject: [PATCH 031/360] Fix logic --- .../deploy/startup/openpype_load_on_first_run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/substancepainter/deploy/startup/openpype_load_on_first_run.py b/openpype/hosts/substancepainter/deploy/startup/openpype_load_on_first_run.py index 90b1ec6bbd..04b610b4df 100644 --- a/openpype/hosts/substancepainter/deploy/startup/openpype_load_on_first_run.py +++ b/openpype/hosts/substancepainter/deploy/startup/openpype_load_on_first_run.py @@ -20,7 +20,7 @@ def start_plugin(): is_before_plugins_menu = PLUGINS_MENU is None settings = get_settings(OPENPYPE_PLUGIN_NAME) - if settings.value(LAUNCH_AT_START_KEY, None) is not None: + if settings.value(LAUNCH_AT_START_KEY, None) is None: print("Initializing OpenPype plug-in on first run...") if is_before_plugins_menu: print("- running before 'painter_plugins_ui'") From d2baa5ec4d9f92c143172f95719bb7b319ae79a2 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 12 Jan 2023 15:38:22 +0100 Subject: [PATCH 032/360] Allow to configure custom shelves for Substance Painter in project settings --- openpype/hosts/substancepainter/api/lib.py | 57 +++++++++++++++++++ .../hosts/substancepainter/api/pipeline.py | 28 +++++++++ .../project_settings/substancepainter.json | 3 + .../schemas/projects_schema/schema_main.json | 4 ++ .../schema_project_substancepainter.json | 18 ++++++ 5 files changed, 110 insertions(+) create mode 100644 openpype/hosts/substancepainter/api/lib.py create mode 100644 openpype/settings/defaults/project_settings/substancepainter.json create mode 100644 openpype/settings/entities/schemas/projects_schema/schema_project_substancepainter.json diff --git a/openpype/hosts/substancepainter/api/lib.py b/openpype/hosts/substancepainter/api/lib.py new file mode 100644 index 0000000000..d468f6cc45 --- /dev/null +++ b/openpype/hosts/substancepainter/api/lib.py @@ -0,0 +1,57 @@ +import os +import re +import substance_painter.resource + + +def load_shelf(path, name=None): + """Add shelf to substance painter (for current application session) + + This will dynamically add a Shelf for the current session. It's good + to note however that these will *not* persist on restart of the host. + + Note: + Consider the loaded shelf a static library of resources. + + The shelf will *not* be visible in application preferences in + Edit > Settings > Libraries. + + The shelf will *not* show in the Assets browser if it has no existing + assets + + The shelf will *not* be a selectable option for selecting it as a + destination to import resources too. + + """ + + # Ensure expanded path with forward slashes + path = os.path.expandvars(path) + path = os.path.abspath(path) + path = path.replace("\\", "/") + + # Path must exist + if not os.path.isdir(path): + raise ValueError(f"Path is not an existing folder: {path}") + + # This name must be unique and must only contain lowercase letters, + # numbers, underscores or hyphens. + if name is None: + name = os.path.basename(path) + + name = name.lower() + name = re.sub(r"[^a-z0-9_\-]", "_", name) # sanitize to underscores + + if substance_painter.resource.Shelves.exists(name): + shelf = next( + shelf for shelf in substance_painter.resource.Shelves.all() + if shelf.name() == name + ) + if os.path.normpath(shelf.path()) != os.path.normpath(path): + raise ValueError(f"Shelf with name '{name}' already exists " + f"for a different path: '{shelf.path()}") + + return + + print(f"Adding Shelf '{name}' to path: {path}") + substance_painter.resource.Shelves.add(name, path) + + return name diff --git a/openpype/hosts/substancepainter/api/pipeline.py b/openpype/hosts/substancepainter/api/pipeline.py index 3a68a7fa86..f4d4c5b00c 100644 --- a/openpype/hosts/substancepainter/api/pipeline.py +++ b/openpype/hosts/substancepainter/api/pipeline.py @@ -10,6 +10,7 @@ import substance_painter.event import substance_painter.project from openpype.host import HostBase, IWorkfileHost, ILoadHost, IPublishHost +from openpype.settings import get_current_project_settings import pyblish.api @@ -25,6 +26,8 @@ from openpype.lib import ( from openpype.pipeline.load import any_outdated_containers from openpype.hosts.substancepainter import SUBSTANCE_HOST_DIR +from . import lib + log = logging.getLogger("openpype.hosts.substance") PLUGINS_DIR = os.path.join(SUBSTANCE_HOST_DIR, "plugins") @@ -45,6 +48,7 @@ class SubstanceHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): self._has_been_setup = False self.menu = None self.callbacks = [] + self.shelves = [] def install(self): pyblish.api.register_host("substancepainter") @@ -64,9 +68,13 @@ class SubstanceHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): log.info("Installing menu ... ") self._install_menu() + project_settings = get_current_project_settings() + self._install_shelves(project_settings) + self._has_been_setup = True def uninstall(self): + self._uninstall_shelves() self._uninstall_menu() self._deregister_callbacks() @@ -206,6 +214,26 @@ class SubstanceHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): def _deregister_callbacks(self): for event, callback in self.callbacks: substance_painter.event.DISPATCHER.disconnect(event, callback) + self.callbacks.clear() + + def _install_shelves(self, project_settings): + + shelves = project_settings["substancepainter"].get("shelves", {}) + for name, path in shelves.items(): + # TODO: Allow formatting with anatomy for the paths + shelf_name = None + try: + shelf_name = lib.load_shelf(path, name=name) + except ValueError as exc: + print(f"Failed to load shelf -> {exc}") + + if shelf_name: + self.shelves.append(shelf_name) + + def _uninstall_shelves(self): + for shelf_name in self.shelves: + substance_painter.resource.Shelves.remove(shelf_name) + self.shelves.clear() def on_open(): diff --git a/openpype/settings/defaults/project_settings/substancepainter.json b/openpype/settings/defaults/project_settings/substancepainter.json new file mode 100644 index 0000000000..a424a923da --- /dev/null +++ b/openpype/settings/defaults/project_settings/substancepainter.json @@ -0,0 +1,3 @@ +{ + "shelves": {} +} \ 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 0b9fbf7470..b3c5c62a89 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_main.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_main.json @@ -114,6 +114,10 @@ "type": "schema", "name": "schema_project_photoshop" }, + { + "type": "schema", + "name": "schema_project_substancepainter" + }, { "type": "schema", "name": "schema_project_harmony" diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_substancepainter.json b/openpype/settings/entities/schemas/projects_schema/schema_project_substancepainter.json new file mode 100644 index 0000000000..4a02a9d8ca --- /dev/null +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_substancepainter.json @@ -0,0 +1,18 @@ +{ + "type": "dict", + "collapsible": true, + "key": "substancepainter", + "label": "Substance Painter", + "is_file": true, + "children": [ + { + "type": "dict-modifiable", + "key": "shelves", + "label": "Shelves", + "use_label_wrap": true, + "object_type": { + "type": "text" + } + } + ] +} From 42b207445ed49dab7d5ce23556d7cbd0e7316ba3 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 13 Jan 2023 12:32:38 +0100 Subject: [PATCH 033/360] Implement working WIP draft for Texture Publishing --- .../hosts/substancepainter/api/colorspace.py | 157 +++++++++++++ openpype/hosts/substancepainter/api/lib.py | 139 ++++++++++++ .../plugins/create/create_textures.py | 71 +----- .../publish/collect_textureset_images.py | 207 ++++++++++++++++++ .../plugins/publish/extract_textures.py | 87 +++----- 5 files changed, 548 insertions(+), 113 deletions(-) create mode 100644 openpype/hosts/substancepainter/api/colorspace.py create mode 100644 openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py diff --git a/openpype/hosts/substancepainter/api/colorspace.py b/openpype/hosts/substancepainter/api/colorspace.py new file mode 100644 index 0000000000..f7b9f7694a --- /dev/null +++ b/openpype/hosts/substancepainter/api/colorspace.py @@ -0,0 +1,157 @@ +"""Substance Painter OCIO management + +Adobe Substance 3D Painter supports OCIO color management using a per project +configuration. Output color spaces are defined at the project level + +More information see: + - https://substance3d.adobe.com/documentation/spdoc/color-management-223053233.html # noqa + - https://substance3d.adobe.com/documentation/spdoc/color-management-with-opencolorio-225969419.html # noqa + +""" +import substance_painter.export +import substance_painter.js +import json + +from .lib import ( + get_document_structure, + get_channel_format +) + + +def _iter_document_stack_channels(): + """Yield all stack paths and channels project""" + + for material in get_document_structure()["materials"]: + material_name = material["name"] + for stack in material["stacks"]: + stack_name = stack["name"] + for channel in stack["channels"]: + if stack_name: + stack_path = [material_name, stack_name] + else: + stack_path = material_name + yield stack_path, channel + + +def _get_first_color_and_data_stack_and_channel(): + """Return first found color channel and data channel.""" + color_channel = None + data_channel = None + for stack_path, channel in _iter_document_stack_channels(): + channel_format = get_channel_format(stack_path, channel) + if channel_format["color"]: + color_channel = (stack_path, channel) + else: + data_channel = (stack_path, channel) + + if color_channel and data_channel: + return color_channel, data_channel + + return color_channel, data_channel + + +def get_project_channel_data(): + """Return colorSpace settings for the current substance painter project. + + In Substance Painter only color channels have Color Management enabled + whereas data channels have no color management applied. This can't be + changed. The artist can only customize the export color space for color + channels per bit-depth for 8 bpc, 16 bpc and 32 bpc. + + As such this returns the color space for 'data' and for per bit-depth + for color channels. + + Example output: + { + "data": {'colorSpace': 'Utility - Raw'}, + "8": {"colorSpace": "ACES - AcesCG"}, + "16": {"colorSpace": "ACES - AcesCG"}, + "16f": {"colorSpace": "ACES - AcesCG"}, + "32f": {"colorSpace": "ACES - AcesCG"} + } + + """ + + keys = ["colorSpace"] + query = {key: f"${key}" for key in keys} + + config = { + "exportPath": "/", + "exportShaderParams": False, + "defaultExportPreset": "query_preset", + + "exportPresets": [{ + "name": "query_preset", + + # List of maps making up this export preset. + "maps": [{ + "fileName": json.dumps(query), + # List of source/destination defining which channels will + # make up the texture file. + "channels": [], + "parameters": { + "fileFormat": "exr", + "bitDepth": "32f", + "dithering": False, + "sizeLog2": 4, + "paddingAlgorithm": "passthrough", + "dilationDistance": 16 + } + }] + }], + } + + def _get_query_output(config): + # Return the basename of the single output path we defined + result = substance_painter.export.list_project_textures(config) + path = next(iter(result.values()))[0] + # strip extension and slash since we know relevant json data starts + # and ends with { and } characters + path = path.strip("/\\.exr") + return json.loads(path) + + # Query for each type of channel (color and data) + color_channel, data_channel = _get_first_color_and_data_stack_and_channel() + colorspaces = {} + for key, channel_data in { + "data": data_channel, + "color": color_channel + }.items(): + if channel_data is None: + # No channel of that datatype anywhere in the Stack. We're + # unable to identify the output color space of the project + colorspaces[key] = None + continue + + stack, channel = channel_data + + # Stack must be a string + if not isinstance(stack, str): + # Assume iterable + stack = "/".join(stack) + + # Define the temp output config + config["exportList"] = [{"rootPath": stack}] + config_map = config["exportPresets"][0]["maps"][0] + config_map["channels"] = [ + { + "destChannel": x, + "srcChannel": x, + "srcMapType": "documentMap", + "srcMapName": channel + } for x in "RGB" + ] + + if key == "color": + # Query for each bit depth + # Color space definition can have a different OCIO config set + # for 8-bit, 16-bit and 32-bit outputs so we need to check each + # bit depth + for depth in ["8", "16", "16f", "32f"]: + config_map["parameters"]["bitDepth"] = depth # noqa + colorspaces[key + depth] = _get_query_output(config) + else: + # Data channel (not color managed) + colorspaces[key] = _get_query_output(config) + + return colorspaces diff --git a/openpype/hosts/substancepainter/api/lib.py b/openpype/hosts/substancepainter/api/lib.py index d468f6cc45..b929f881a8 100644 --- a/openpype/hosts/substancepainter/api/lib.py +++ b/openpype/hosts/substancepainter/api/lib.py @@ -1,6 +1,145 @@ import os import re +import json + import substance_painter.resource +import substance_painter.js + + +def get_export_presets(): + """Return Export Preset resource URLs for all available Export Presets. + + Returns: + dict: {Resource url: GUI Label} + + """ + # TODO: Find more optimal way to find all export templates + + preset_resources = {} + for shelf in substance_painter.resource.Shelves.all(): + shelf_path = os.path.normpath(shelf.path()) + + presets_path = os.path.join(shelf_path, "export-presets") + if not os.path.exists(presets_path): + continue + + for filename in os.listdir(presets_path): + if filename.endswith(".spexp"): + template_name = os.path.splitext(filename)[0] + + resource = substance_painter.resource.ResourceID( + context=shelf.name(), + name=template_name + ) + resource_url = resource.url() + + preset_resources[resource_url] = template_name + + # Sort by template name + export_templates = dict(sorted(preset_resources.items(), + key=lambda x: x[1])) + + # Add default built-ins at the start + # TODO: find the built-ins automatically; scraped with https://gist.github.com/BigRoy/97150c7c6f0a0c916418207b9a2bc8f1 # noqa + result = { + "export-preset-generator://viewport2d": "2D View", # noqa + "export-preset-generator://doc-channel-normal-no-alpha": "Document channels + Normal + AO (No Alpha)", # noqa + "export-preset-generator://doc-channel-normal-with-alpha": "Document channels + Normal + AO (With Alpha)", # noqa + "export-preset-generator://sketchfab": "Sketchfab", # noqa + "export-preset-generator://adobe-standard-material": "Substance 3D Stager", # noqa + "export-preset-generator://usd": "USD PBR Metal Roughness", # noqa + "export-preset-generator://gltf": "glTF PBR Metal Roughness", # noqa + "export-preset-generator://gltf-displacement": "glTF PBR Metal Roughness + Displacement texture (experimental)" # noqa + } + result.update(export_templates) + return result + + +def _convert_stack_path_to_cmd_str(stack_path): + """Convert stack path `str` or `[str, str]` for javascript query + + Example usage: + >>> stack_path = _convert_stack_path_to_cmd_str(stack_path) + >>> cmd = f"alg.mapexport.channelIdentifiers({stack_path})" + >>> substance_painter.js.evaluate(cmd) + + Args: + stack_path (list or str): Path to the stack, could be + "Texture set name" or ["Texture set name", "Stack name"] + + Returns: + str: Stack path usable as argument in javascript query. + + """ + return json.dumps(stack_path) + + +def get_channel_identifiers(stack_path=None): + """Return the list of channel identifiers. + + If a context is passed (texture set/stack), + return only used channels with resolved user channels. + + Channel identifiers are: + basecolor, height, specular, opacity, emissive, displacement, + glossiness, roughness, anisotropylevel, anisotropyangle, transmissive, + scattering, reflection, ior, metallic, normal, ambientOcclusion, + diffuse, specularlevel, blendingmask, [custom user names]. + + Args: + stack_path (list or str, Optional): Path to the stack, could be + "Texture set name" or ["Texture set name", "Stack name"] + + Returns: + list: List of channel identifiers. + + """ + if stack_path is None: + stack_path = "" + else: + stack_path = _convert_stack_path_to_cmd_str(stack_path) + cmd = f"alg.mapexport.channelIdentifiers({stack_path})" + return substance_painter.js.evaluate(cmd) + + +def get_channel_format(stack_path, channel): + """Retrieve the channel format of a specific stack channel. + + See `alg.mapexport.channelFormat` (javascript API) for more details. + + The channel format data is: + "label" (str): The channel format label: could be one of + [sRGB8, L8, RGB8, L16, RGB16, L16F, RGB16F, L32F, RGB32F] + "color" (bool): True if the format is in color, False is grayscale + "floating" (bool): True if the format uses floating point + representation, false otherwise + "bitDepth" (int): Bit per color channel (could be 8, 16 or 32 bpc) + + Args: + stack_path (list or str): Path to the stack, could be + "Texture set name" or ["Texture set name", "Stack name"] + channel (str): Identifier of the channel to export + (see `get_channel_identifiers`) + + Returns: + dict: The channel format data. + + """ + stack_path = _convert_stack_path_to_cmd_str(stack_path) + cmd = f"alg.mapexport.channelFormat({stack_path}, '{channel}')" + return substance_painter.js.evaluate(cmd) + + +def get_document_structure(): + """Dump the document structure. + + See `alg.mapexport.documentStructure` (javascript API) for more details. + + Returns: + dict: Document structure or None when no project is open + + """ + return substance_painter.js.evaluate("alg.mapexport.documentStructure()") def load_shelf(path, name=None): diff --git a/openpype/hosts/substancepainter/plugins/create/create_textures.py b/openpype/hosts/substancepainter/plugins/create/create_textures.py index 6d4f816961..9d641215dc 100644 --- a/openpype/hosts/substancepainter/plugins/create/create_textures.py +++ b/openpype/hosts/substancepainter/plugins/create/create_textures.py @@ -1,74 +1,27 @@ # -*- coding: utf-8 -*- """Creator plugin for creating textures.""" -import os from openpype.pipeline import CreatedInstance, Creator - -from openpype.hosts.substancepainter.api.pipeline import ( - set_project_metadata, - get_project_metadata -) - from openpype.lib import ( EnumDef, UILabelDef, NumberDef ) +from openpype.hosts.substancepainter.api.pipeline import ( + set_project_metadata, + get_project_metadata +) +from openpype.hosts.substancepainter.api.lib import get_export_presets + import substance_painter.project -import substance_painter.resource - - -def get_export_presets(): - import substance_painter.resource - - preset_resources = {} - - # TODO: Find more optimal way to find all export templates - for shelf in substance_painter.resource.Shelves.all(): - shelf_path = os.path.normpath(shelf.path()) - - presets_path = os.path.join(shelf_path, "export-presets") - if not os.path.exists(presets_path): - continue - - for fname in os.listdir(presets_path): - if fname.endswith(".spexp"): - template_name = os.path.splitext(fname)[0] - - resource = substance_painter.resource.ResourceID( - context=shelf.name(), - name=template_name - ) - resource_url = resource.url() - - preset_resources[resource_url] = template_name - - # Sort by template name - export_templates = dict(sorted(preset_resources.items(), - key=lambda x: x[1])) - - # Add default built-ins at the start - # TODO: find the built-ins automatically; scraped with https://gist.github.com/BigRoy/97150c7c6f0a0c916418207b9a2bc8f1 # noqa - result = { - "export-preset-generator://viewport2d": "2D View", # noqa - "export-preset-generator://doc-channel-normal-no-alpha": "Document channels + Normal + AO (No Alpha)", # noqa - "export-preset-generator://doc-channel-normal-with-alpha": "Document channels + Normal + AO (With Alpha)", # noqa - "export-preset-generator://sketchfab": "Sketchfab", # noqa - "export-preset-generator://adobe-standard-material": "Substance 3D Stager", # noqa - "export-preset-generator://usd": "USD PBR Metal Roughness", # noqa - "export-preset-generator://gltf": "glTF PBR Metal Roughness", # noqa - "export-preset-generator://gltf-displacement": "glTF PBR Metal Roughness + Displacement texture (experimental)" # noqa - } - result.update(export_templates) - return result class CreateTextures(Creator): """Create a texture set.""" - identifier = "io.openpype.creators.substancepainter.textures" + identifier = "io.openpype.creators.substancepainter.textureset" label = "Textures" - family = "textures" + family = "textureSet" icon = "picture-o" default_variant = "Main" @@ -79,19 +32,19 @@ class CreateTextures(Creator): return instance = self.create_instance_in_context(subset_name, instance_data) - set_project_metadata("textures", instance.data_to_store()) + set_project_metadata("textureSet", instance.data_to_store()) def collect_instances(self): - workfile = get_project_metadata("textures") + workfile = get_project_metadata("textureSet") if workfile: self.create_instance_in_context_from_existing(workfile) def update_instances(self, update_list): for instance, _changes in update_list: # Update project's metadata - data = get_project_metadata("textures") or {} + data = get_project_metadata("textureSet") or {} data.update(instance.data_to_store()) - set_project_metadata("textures", data) + set_project_metadata("textureSet", data) def remove_instances(self, instances): for instance in instances: diff --git a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py new file mode 100644 index 0000000000..96f2daa525 --- /dev/null +++ b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py @@ -0,0 +1,207 @@ +import os +import copy +import clique +import pyblish.api + +from openpype.pipeline import publish + +import substance_painter.export +from openpype.hosts.substancepainter.api.colorspace import ( + get_project_channel_data, +) + + +def get_project_color_spaces(): + """Return unique color space names used for exports. + + This is based on the Color Management preferences of the project. + + See also: + func:`get_project_channel_data` + + """ + return set( + data["colorSpace"] for data in get_project_channel_data().values() + ) + + +def _get_channel_name(path, + texture_set_name, + project_colorspaces): + """Return expected 'name' for the output image. + + This will be used as a suffix to the separate image publish subsets. + + """ + # TODO: This will require improvement before being production ready. + # TODO(Question): Should we preserve the texture set name in the suffix + # TODO so that exports with multiple texture sets can work within a single + # TODO parent textureSet, like `texture{Variant}.{TextureSet}{Channel}` + name = os.path.basename(path) # filename + name = os.path.splitext(name)[0] # no extension + # Usually the channel identifier comes after $textureSet in + # the export preset. Unfortunately getting the export maps + # and channels explicitly is not trivial so for now we just + # assume this will generate a nice identifier for the end user + name = name.split(f"{texture_set_name}_", 1)[-1] + + # TODO: We need more explicit ways to detect the color space part + for colorspace in project_colorspaces: + if name.endswith(f"_{colorspace}"): + name = name[:-len(f"_{colorspace}")] + break + + return name + + +class CollectTextureSet(pyblish.api.InstancePlugin): + """Extract Textures using an output template config""" + # TODO: More explicitly detect UDIM tiles + # TODO: Get color spaces + # TODO: Detect what source data channels end up in each file + + label = "Collect Texture Set images" + hosts = ['substancepainter'] + families = ["textureSet"] + order = pyblish.api.CollectorOrder + + def process(self, instance): + + config = self.get_export_config(instance) + textures = substance_painter.export.list_project_textures(config) + + instance.data["exportConfig"] = config + + colorspaces = get_project_color_spaces() + + outputs = {} + for (texture_set_name, stack_name), maps in textures.items(): + + # Log our texture outputs + self.log.debug(f"Processing stack: {stack_name}") + for texture_map in maps: + self.log.debug(f"Expecting texture: {texture_map}") + + # For now assume the UDIM textures end with .. and + # when no trailing number is present before the extension then it's + # considered to *not* be a UDIM export. + collections, remainder = clique.assemble( + maps, + patterns=[clique.PATTERNS["frames"]], + minimum_items=True + ) + + outputs = {} + if collections: + # UDIM tile sequence + for collection in collections: + name = _get_channel_name(collection.head, + texture_set_name=texture_set_name, + project_colorspaces=colorspaces) + outputs[name] = collection + self.log.info(f"UDIM Collection: {collection}") + else: + # Single file per channel without UDIM number + for path in remainder: + name = _get_channel_name(path, + texture_set_name=texture_set_name, + project_colorspaces=colorspaces) + outputs[name] = path + self.log.info(f"Single file: {path}") + + # Let's break the instance into multiple instances to integrate + # a subset per generated texture or texture UDIM sequence + context = instance.context + for map_name, map_output in outputs.items(): + + is_udim = isinstance(map_output, clique.Collection) + if is_udim: + first_file = list(map_output)[0] + map_fnames = [os.path.basename(path) for path in map_output] + else: + first_file = map_output + map_fnames = map_output + + ext = os.path.splitext(first_file)[1] + assert ext.lstrip('.'), f"No extension: {ext}" + + # Define the suffix we want to give this particular texture + # set and set up a remapped subset naming for it. + suffix = f".{map_name}" + image_subset = instance.data["subset"][len("textureSet"):] + image_subset = "texture" + image_subset + suffix + + # TODO: Retrieve and store color space with the representation + + # Clone the instance + image_instance = context.create_instance(instance.name) + image_instance[:] = instance[:] + image_instance.data.update(copy.deepcopy(instance.data)) + image_instance.data["name"] = image_subset + image_instance.data["label"] = image_subset + image_instance.data["subset"] = image_subset + image_instance.data["family"] = "image" + image_instance.data["families"] = ["image", "textures"] + image_instance.data['representations'] = [{ + 'name': ext.lstrip("."), + 'ext': ext.lstrip("."), + 'files': map_fnames, + }] + + instance.append(image_instance) + + def get_export_config(self, instance): + """Return an export configuration dict for texture exports. + + This config can be supplied to: + - `substance_painter.export.export_project_textures` + - `substance_painter.export.list_project_textures` + + See documentation on substance_painter.export module about the + formatting of the configuration dictionary. + + Args: + instance (pyblish.api.Instance): Texture Set instance to be + published. + + Returns: + dict: Export config + + """ + + creator_attrs = instance.data["creator_attributes"] + preset_url = creator_attrs["exportPresetUrl"] + self.log.debug(f"Exporting using preset: {preset_url}") + + # See: https://substance3d.adobe.com/documentation/ptpy/api/substance_painter/export # noqa + config = { # noqa + "exportShaderParams": True, + "exportPath": publish.get_instance_staging_dir(instance), + "defaultExportPreset": preset_url, + + # Custom overrides to the exporter + "exportParameters": [ + { + "parameters": { + "fileFormat": creator_attrs["exportFileFormat"], + "sizeLog2": creator_attrs["exportSize"], + "paddingAlgorithm": creator_attrs["exportPadding"], + "dilationDistance": creator_attrs["exportDilationDistance"] # noqa + } + } + ] + } + + # Create the list of Texture Sets to export. + config["exportList"] = [] + for texture_set in substance_painter.textureset.all_texture_sets(): + config["exportList"].append({"rootPath": texture_set.name()}) + + # Consider None values from the creator attributes optionals + for override in config["exportParameters"]: + parameters = override.get("parameters") + for key, value in dict(parameters).items(): + if value is None: + parameters.pop(key) + + return config diff --git a/openpype/hosts/substancepainter/plugins/publish/extract_textures.py b/openpype/hosts/substancepainter/plugins/publish/extract_textures.py index 8ebad3193f..e99b93cac9 100644 --- a/openpype/hosts/substancepainter/plugins/publish/extract_textures.py +++ b/openpype/hosts/substancepainter/plugins/publish/extract_textures.py @@ -1,55 +1,28 @@ from openpype.pipeline import KnownPublishError, publish - import substance_painter.export class ExtractTextures(publish.Extractor): - """Extract Textures using an output template config""" + """Extract Textures using an output template config. - label = "Extract Texture Sets" + Note: + This Extractor assumes that `collect_textureset_images` has prepared + the relevant export config and has also collected the individual image + instances for publishing including its representation. That is why this + particular Extractor doesn't specify representations to integrate. + + """ + # TODO: More explicitly detect UDIM tiles + # TODO: Get color spaces + # TODO: Detect what source data channels end up in each file + + label = "Extract Texture Set" hosts = ['substancepainter'] - families = ["textures"] + families = ["textureSet"] def process(self, instance): - staging_dir = self.staging_dir(instance) - - creator_attrs = instance.data["creator_attributes"] - preset_url = creator_attrs["exportPresetUrl"] - self.log.debug(f"Exporting using preset: {preset_url}") - - # See: https://substance3d.adobe.com/documentation/ptpy/api/substance_painter/export # noqa - config = { - "exportShaderParams": True, - "exportPath": staging_dir, - "defaultExportPreset": preset_url, - - # Custom overrides to the exporter - "exportParameters": [ - { - "parameters": { - "fileFormat": creator_attrs["exportFileFormat"], - "sizeLog2": creator_attrs["exportSize"], - "paddingAlgorithm": creator_attrs["exportPadding"], - "dilationDistance": creator_attrs["exportDilationDistance"] # noqa - } - } - ] - } - - # Create the list of Texture Sets to export. - config["exportList"] = [] - for texture_set in substance_painter.textureset.all_texture_sets(): - # stack = texture_set.get_stack() - config["exportList"].append({"rootPath": texture_set.name()}) - - # Consider None values optionals - for override in config["exportParameters"]: - parameters = override.get("parameters") - for key, value in dict(parameters).items(): - if value is None: - parameters.pop(key) - + config = instance.data["exportConfig"] result = substance_painter.export.export_project_textures(config) if result.status != substance_painter.export.ExportStatus.Success: @@ -57,18 +30,24 @@ class ExtractTextures(publish.Extractor): "Failed to export texture set: {}".format(result.message) ) - files = [] - for _stack, maps in result.textures.items(): + for (texture_set_name, stack_name), maps in result.textures.items(): + # Log our texture outputs + self.log.info(f"Processing stack: {stack_name}") for texture_map in maps: self.log.info(f"Exported texture: {texture_map}") - files.append(texture_map) - # TODO: add the representations so they integrate the way we'd want - """ - instance.data['representations'] = [{ - 'name': ext.lstrip("."), - 'ext': ext.lstrip("."), - 'files': file, - "stagingDir": folder, - }] - """ + # TODO: Confirm outputs match what we collected + # TODO: Confirm the files indeed exist + # TODO: make sure representations are registered + + # Add a fake representation which won't be integrated so the + # Integrator leaves us alone - otherwise it would error + # TODO: Add `instance.data["integrate"] = False` support in Integrator? + instance.data["representations"] = [ + { + "name": "_fake", + "ext": "_fake", + "delete": True, + "files": [] + } + ] From bd73709463440b520deafb6e9ac82995b6e6e430 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 13 Jan 2023 12:33:43 +0100 Subject: [PATCH 034/360] Fix indentation --- openpype/hosts/substancepainter/api/colorspace.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/substancepainter/api/colorspace.py b/openpype/hosts/substancepainter/api/colorspace.py index f7b9f7694a..a9df3eb066 100644 --- a/openpype/hosts/substancepainter/api/colorspace.py +++ b/openpype/hosts/substancepainter/api/colorspace.py @@ -135,10 +135,10 @@ def get_project_channel_data(): config_map = config["exportPresets"][0]["maps"][0] config_map["channels"] = [ { - "destChannel": x, - "srcChannel": x, - "srcMapType": "documentMap", - "srcMapName": channel + "destChannel": x, + "srcChannel": x, + "srcMapType": "documentMap", + "srcMapName": channel } for x in "RGB" ] From fbcb88b457faa1e468b71104a158da03558a4c23 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 13 Jan 2023 12:35:00 +0100 Subject: [PATCH 035/360] Include texture set name in the logging --- .../hosts/substancepainter/plugins/publish/extract_textures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/substancepainter/plugins/publish/extract_textures.py b/openpype/hosts/substancepainter/plugins/publish/extract_textures.py index e99b93cac9..a32a81db48 100644 --- a/openpype/hosts/substancepainter/plugins/publish/extract_textures.py +++ b/openpype/hosts/substancepainter/plugins/publish/extract_textures.py @@ -32,7 +32,7 @@ class ExtractTextures(publish.Extractor): for (texture_set_name, stack_name), maps in result.textures.items(): # Log our texture outputs - self.log.info(f"Processing stack: {stack_name}") + self.log.info(f"Processing stack: {texture_set_name} {stack_name}") for texture_map in maps: self.log.info(f"Exported texture: {texture_map}") From 78c4875dcb26488cae3e8ccb27b6bc7f6f8c4350 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 13 Jan 2023 18:03:34 +0100 Subject: [PATCH 036/360] Add support for thumbnail generation of extracted textures from Substance Painter --- .../plugins/publish/collect_textureset_images.py | 6 ++++++ .../substancepainter/plugins/publish/extract_textures.py | 3 +++ openpype/plugins/publish/extract_thumbnail.py | 4 ++-- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py index 96f2daa525..5a179f7526 100644 --- a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py +++ b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py @@ -148,6 +148,12 @@ class CollectTextureSet(pyblish.api.InstancePlugin): 'files': map_fnames, }] + # Set up the representation for thumbnail generation + # TODO: Simplify this once thumbnail extraction is refactored + staging_dir = os.path.dirname(first_file) + image_instance.data["representations"][0]["tags"] = ["review"] + image_instance.data["representations"][0]["stagingDir"] = staging_dir # noqa + instance.append(image_instance) def get_export_config(self, instance): diff --git a/openpype/hosts/substancepainter/plugins/publish/extract_textures.py b/openpype/hosts/substancepainter/plugins/publish/extract_textures.py index a32a81db48..22acf07284 100644 --- a/openpype/hosts/substancepainter/plugins/publish/extract_textures.py +++ b/openpype/hosts/substancepainter/plugins/publish/extract_textures.py @@ -20,6 +20,9 @@ class ExtractTextures(publish.Extractor): hosts = ['substancepainter'] families = ["textureSet"] + # Run before thumbnail extractors + order = publish.Extractor.order - 0.1 + def process(self, instance): config = instance.data["exportConfig"] diff --git a/openpype/plugins/publish/extract_thumbnail.py b/openpype/plugins/publish/extract_thumbnail.py index 14b43beae8..dcdb8341ba 100644 --- a/openpype/plugins/publish/extract_thumbnail.py +++ b/openpype/plugins/publish/extract_thumbnail.py @@ -19,9 +19,9 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): order = pyblish.api.ExtractorOrder families = [ "imagesequence", "render", "render2d", "prerender", - "source", "clip", "take" + "source", "clip", "take", "image" ] - hosts = ["shell", "fusion", "resolve", "traypublisher"] + hosts = ["shell", "fusion", "resolve", "traypublisher", "substancepainter"] enabled = False # presetable attribute From 5c0a7e30ed59b63bd177ff64c07c5f55417556f3 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 13 Jan 2023 18:14:18 +0100 Subject: [PATCH 037/360] Group textures together to look like a package/textureSet --- .../plugins/publish/collect_textureset_images.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py index 5a179f7526..3832f724d4 100644 --- a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py +++ b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py @@ -148,6 +148,9 @@ class CollectTextureSet(pyblish.api.InstancePlugin): 'files': map_fnames, }] + # Group the textures together in the loader + image_instance.data["subsetGroup"] = instance.data["subset"] + # Set up the representation for thumbnail generation # TODO: Simplify this once thumbnail extraction is refactored staging_dir = os.path.dirname(first_file) From cba71b9e0d22da265429fe2fcbcba1d77dd63a3e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 13 Jan 2023 18:29:59 +0100 Subject: [PATCH 038/360] Fix full path in representation for single images (non-UDIM) --- .../plugins/publish/collect_textureset_images.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py index 3832f724d4..851a22c1ee 100644 --- a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py +++ b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py @@ -120,7 +120,7 @@ class CollectTextureSet(pyblish.api.InstancePlugin): map_fnames = [os.path.basename(path) for path in map_output] else: first_file = map_output - map_fnames = map_output + map_fnames = os.path.basename(map_output) ext = os.path.splitext(first_file)[1] assert ext.lstrip('.'), f"No extension: {ext}" From b17ca1efeac834d9038555f522c8602bc4701035 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 14 Jan 2023 15:38:22 +0100 Subject: [PATCH 039/360] More explicit parsing of extracted textures, prepare for color space data --- openpype/hosts/substancepainter/api/lib.py | 328 +++++++++++++++++- .../publish/collect_textureset_images.py | 177 +++------- .../plugins/publish/extract_textures.py | 3 - 3 files changed, 379 insertions(+), 129 deletions(-) diff --git a/openpype/hosts/substancepainter/api/lib.py b/openpype/hosts/substancepainter/api/lib.py index b929f881a8..2406680a68 100644 --- a/openpype/hosts/substancepainter/api/lib.py +++ b/openpype/hosts/substancepainter/api/lib.py @@ -1,7 +1,9 @@ import os import re import json +from collections import defaultdict +import substance_painter.project import substance_painter.resource import substance_painter.js @@ -115,7 +117,7 @@ def get_channel_format(stack_path, channel): representation, false otherwise "bitDepth" (int): Bit per color channel (could be 8, 16 or 32 bpc) - Args: + Arguments: stack_path (list or str): Path to the stack, could be "Texture set name" or ["Texture set name", "Stack name"] channel (str): Identifier of the channel to export @@ -142,6 +144,330 @@ def get_document_structure(): return substance_painter.js.evaluate("alg.mapexport.documentStructure()") +def get_export_templates(config, format="png", strip_folder=True): + """Return export config outputs. + + This use the Javascript API `alg.mapexport.getPathsExportDocumentMaps` + which returns a different output than using the Python equivalent + `substance_painter.export.list_project_textures(config)`. + + The nice thing about the Javascript API version is that it returns the + output textures grouped by filename template. + + A downside is that it doesn't return all the UDIM tiles but per template + always returns a single file. + + Note: + The file format needs to be explicitly passed to the Javascript API + but upon exporting through the Python API the file format can be based + on the output preset. So it's likely the file extension will mismatch + + Warning: + Even though the function appears to solely get the expected outputs + the Javascript API will actually create the config's texture output + folder if it does not exist yet. As such, a valid path must be set. + + Example output: + { + "DefaultMaterial": { + "$textureSet_BaseColor(_$colorSpace)(.$udim)": "DefaultMaterial_BaseColor_ACES - ACEScg.1002.png", # noqa + "$textureSet_Emissive(_$colorSpace)(.$udim)": "DefaultMaterial_Emissive_ACES - ACEScg.1002.png", # noqa + "$textureSet_Height(_$colorSpace)(.$udim)": "DefaultMaterial_Height_Utility - Raw.1002.png", # noqa + "$textureSet_Metallic(_$colorSpace)(.$udim)": "DefaultMaterial_Metallic_Utility - Raw.1002.png", # noqa + "$textureSet_Normal(_$colorSpace)(.$udim)": "DefaultMaterial_Normal_Utility - Raw.1002.png", # noqa + "$textureSet_Roughness(_$colorSpace)(.$udim)": "DefaultMaterial_Roughness_Utility - Raw.1002.png" # noqa + } + } + + Arguments: + config (dict) Export config + format (str, Optional): Output format to write to, defaults to 'png' + strip_folder (bool, Optional): Whether to strip the output folder + from the output filenames. + + Returns: + dict: The expected output maps. + + """ + folder = config["exportPath"] + preset = config["defaultExportPreset"] + cmd = f'alg.mapexport.getPathsExportDocumentMaps("{preset}", "{folder}", "{format}")' # noqa + result = substance_painter.js.evaluate(cmd) + + if strip_folder: + for stack, maps in result.items(): + for map_template, map_filepath in maps.items(): + map_filename = map_filepath[len(folder):].lstrip("/") + maps[map_template] = map_filename + + return result + + +def _templates_to_regex(templates, + texture_set, + colorspaces, + project, + mesh): + """Return regex based on a Substance Painter expot filename template. + + This converts Substance Painter export filename templates like + `$mesh_$textureSet_BaseColor(_$colorSpace)(.$udim)` into a regex + which can be used to query an output filename to help retrieve: + + - Which template filename the file belongs to. + - Which color space the file is written with. + - Which udim tile it is exactly. + + This is used by `get_parsed_export_maps` which tries to as explicitly + as possible match the filename pattern against the known possible outputs. + That's why Texture Set name, Color spaces, Project path and mesh path must + be provided. By doing so we get the best shot at correctly matching the + right template because otherwise $texture_set could basically be any string + and thus match even that of a color space or mesh. + + Arguments: + templates (list): List of templates to convert to regex. + texture_set (str): The texture set to match against. + colorspaces (list): The colorspaces defined in the current project. + project (str): Filepath of current substance project. + mesh (str): Path to mesh file used in current project. + + Returns: + dict: Template: Template regex pattern + + """ + def _filename_no_ext(path): + return os.path.splitext(os.path.basename(path))[0] + + if colorspaces and any(colorspaces): + colorspace_match = ( + "(" + "|".join(re.escape(c) for c in colorspaces) + ")" + ) + else: + # No colorspace support enabled + colorspace_match = "" + + # Key to regex valid search values + key_matches = { + "$project": re.escape(_filename_no_ext(project)), + "$mesh": re.escape(_filename_no_ext(mesh)), + "$textureSet": re.escape(texture_set), + "$colorSpace": colorspace_match, + "$udim": "([0-9]{4})" + } + + # Turn the templates into regexes + regexes = {} + for template in templates: + + # We need to tweak a temp + search_regex = re.escape(template) + + # Let's assume that any ( and ) character in the file template was + # intended as an optional template key and do a simple `str.replace` + # Note: we are matching against re.escape(template) so will need to + # search for the escaped brackets. + search_regex = search_regex.replace(re.escape("("), "(") + search_regex = search_regex.replace(re.escape(")"), ")?") + + # Substitute each key into a named group + for key, key_expected_regex in key_matches.items(): + + # We want to use the template as a regex basis in the end so will + # escape the whole thing first. Note that thus we'll need to + # search for the escaped versions of the keys too. + escaped_key = re.escape(key) + key_label = key[1:] # key without $ prefix + + key_expected_grp_regex = f"(?P<{key_label}>{key_expected_regex})" + search_regex = search_regex.replace(escaped_key, + key_expected_grp_regex) + + # The filename templates don't include the extension so we add it + # to be able to match the out filename beginning to end + ext_regex = "(?P\.[A-Za-z][A-Za-z0-9-]*)" + search_regex = rf"^{search_regex}{ext_regex}$" + + regexes[template] = search_regex + + return regexes + + +def strip_template(template, strip="._ "): + """Return static characters in a substance painter filename template. + + >>> strip_template("$textureSet_HELLO(.$udim)") + # HELLO + >>> strip_template("$mesh_$textureSet_HELLO_WORLD_$colorSpace(.$udim)") + # HELLO_WORLD + >>> strip_template("$textureSet_HELLO(.$udim)", strip=None) + # _HELLO + >>> strip_template("$mesh_$textureSet_$colorSpace(.$udim)", strip=None) + # _HELLO_ + >>> strip_template("$textureSet_HELLO(.$udim)") + # _HELLO + + Arguments: + template (str): Filename template to strip. + strip (str, optional): Characters to strip from beginning and end + of the static string in template. Defaults to: `._ `. + + Returns: + str: The static string in filename template. + + """ + # Return only characters that were part of the template that were static. + # Remove all keys + keys = ["$project", "$mesh", "$textureSet", "$udim", "$colorSpace"] + stripped_template = template + for key in keys: + stripped_template = stripped_template.replace(key, "") + + # Everything inside an optional bracket space is excluded since it's not + # static. We keep a counter to track whether we are currently iterating + # over parts of the template that are inside an 'optional' group or not. + counter = 0 + result = "" + for char in stripped_template: + if char == "(": + counter += 1 + elif char == ")": + counter -= 1 + if counter < 0: + counter = 0 + else: + if counter == 0: + result += char + + if strip: + # Strip of any trailing start/end characters. Technically these are + # static but usually start and end separators like space or underscore + # aren't wanted. + result = result.strip(strip) + + return result + + +def get_parsed_export_maps(config): + """ + + This tries to parse the texture outputs using a Python API export config. + + Parses template keys: $project, $mesh, $textureSet, $colorSpace, $udim + + Example: + {("DefaultMaterial", ""): { + "$mesh_$textureSet_BaseColor(_$colorSpace)(.$udim)": [ + { + // OUTPUT DATA FOR FILE #1 OF THE TEMPLATE + }, + { + // OUTPUT DATA FOR FILE #2 OF THE TEMPLATE + }, + ] + }, + }} + + File output data (all outputs are `str`). + 1) Parsed tokens: These are parsed tokens from the template, they will + only exist if found in the filename template and output filename. + + project: Workfile filename without extension + mesh: Filename of the loaded mesh without extension + textureSet: The texture set, e.g. "DefaultMaterial", + colorSpace: The color space, e.g. "ACES - ACEScg", + udim: The udim tile, e.g. "1001" + + 2) Template and file outputs + + filepath: Full path to the resulting texture map, e.g. + "/path/to/mesh_DefaultMaterial_BaseColor_ACES - ACEScg.1002.png", + output: "mesh_DefaultMaterial_BaseColor_ACES - ACEScg.1002.png" + Note: if template had slashes (folders) then `output` will too. + So `output` might include a folder. + + channel: The stripped static characters of the filename template which + usually look like an identifier for that map, e.g. "BaseColor". + See `_stripped_template` + + Returns: + dict: [texture_set, stack]: {template: [file1_data, file2_data]} + + """ + import substance_painter.export + from .colorspace import get_project_channel_data + + outputs = substance_painter.export.list_project_textures(config) + templates = get_export_templates(config) + + # Get all color spaces set for the current project + project_colorspaces = set( + data["colorSpace"] for data in get_project_channel_data().values() + ) + + # Get current project mesh path and project path to explicitly match + # the $mesh and $project tokens + project_mesh_path = substance_painter.project.last_imported_mesh_path() + project_path = substance_painter.project.file_path() + + # Get the current export path to strip this of the beginning of filepath + # results, since filename templates don't have these we'll match without + # that part of the filename. + export_path = config["exportPath"] + export_path = export_path.replace("\\", "/") + if not export_path.endswith("/"): + export_path += "/" + + # Parse the outputs + result = {} + for key, filepaths in outputs.items(): + texture_set, stack = key + + if stack: + stack_path = f"{texture_set}/{stack}" + else: + stack_path = texture_set + + stack_templates = list(templates[stack_path].keys()) + + template_regex = _templates_to_regex(stack_templates, + texture_set=texture_set, + colorspaces=project_colorspaces, + mesh=project_mesh_path, + project=project_path) + + # Let's precompile the regexes + for template, regex in template_regex.items(): + template_regex[template] = re.compile(regex) + + stack_results = defaultdict(list) + for filepath in sorted(filepaths): + # We strip explicitly using the full parent export path instead of + # using `os.path.basename` because export template is allowed to + # have subfolders in its template which we want to match against + assert filepath.startswith(export_path) + filename = filepath[len(export_path):] + + for template, regex in template_regex.items(): + match = regex.match(filename) + if match: + parsed = match.groupdict(default={}) + + # Include some special outputs for convenience + parsed["filepath"] = filepath + parsed["output"] = filename + + stack_results[template].append(parsed) + break + else: + raise ValueError(f"Unable to match {filename} against any " + f"template in: {list(template_regex.keys())}") + + result[key] = dict(stack_results) + + return result + + def load_shelf(path, name=None): """Add shelf to substance painter (for current application session) diff --git a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py index 851a22c1ee..6928bdb36c 100644 --- a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py +++ b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py @@ -1,63 +1,19 @@ import os import copy -import clique import pyblish.api from openpype.pipeline import publish -import substance_painter.export -from openpype.hosts.substancepainter.api.colorspace import ( - get_project_channel_data, +import substance_painter.textureset +from openpype.hosts.substancepainter.api.lib import ( + get_parsed_export_maps, + strip_template ) -def get_project_color_spaces(): - """Return unique color space names used for exports. - - This is based on the Color Management preferences of the project. - - See also: - func:`get_project_channel_data` - - """ - return set( - data["colorSpace"] for data in get_project_channel_data().values() - ) - - -def _get_channel_name(path, - texture_set_name, - project_colorspaces): - """Return expected 'name' for the output image. - - This will be used as a suffix to the separate image publish subsets. - - """ - # TODO: This will require improvement before being production ready. - # TODO(Question): Should we preserve the texture set name in the suffix - # TODO so that exports with multiple texture sets can work within a single - # TODO parent textureSet, like `texture{Variant}.{TextureSet}{Channel}` - name = os.path.basename(path) # filename - name = os.path.splitext(name)[0] # no extension - # Usually the channel identifier comes after $textureSet in - # the export preset. Unfortunately getting the export maps - # and channels explicitly is not trivial so for now we just - # assume this will generate a nice identifier for the end user - name = name.split(f"{texture_set_name}_", 1)[-1] - - # TODO: We need more explicit ways to detect the color space part - for colorspace in project_colorspaces: - if name.endswith(f"_{colorspace}"): - name = name[:-len(f"_{colorspace}")] - break - - return name - - class CollectTextureSet(pyblish.api.InstancePlugin): """Extract Textures using an output template config""" - # TODO: More explicitly detect UDIM tiles - # TODO: Get color spaces + # TODO: Production-test usage of color spaces # TODO: Detect what source data channels end up in each file label = "Collect Texture Set images" @@ -68,96 +24,67 @@ class CollectTextureSet(pyblish.api.InstancePlugin): def process(self, instance): config = self.get_export_config(instance) - textures = substance_painter.export.list_project_textures(config) instance.data["exportConfig"] = config - - colorspaces = get_project_color_spaces() - - outputs = {} - for (texture_set_name, stack_name), maps in textures.items(): - - # Log our texture outputs - self.log.debug(f"Processing stack: {stack_name}") - for texture_map in maps: - self.log.debug(f"Expecting texture: {texture_map}") - - # For now assume the UDIM textures end with .. and - # when no trailing number is present before the extension then it's - # considered to *not* be a UDIM export. - collections, remainder = clique.assemble( - maps, - patterns=[clique.PATTERNS["frames"]], - minimum_items=True - ) - - outputs = {} - if collections: - # UDIM tile sequence - for collection in collections: - name = _get_channel_name(collection.head, - texture_set_name=texture_set_name, - project_colorspaces=colorspaces) - outputs[name] = collection - self.log.info(f"UDIM Collection: {collection}") - else: - # Single file per channel without UDIM number - for path in remainder: - name = _get_channel_name(path, - texture_set_name=texture_set_name, - project_colorspaces=colorspaces) - outputs[name] = path - self.log.info(f"Single file: {path}") + maps = get_parsed_export_maps(config) # Let's break the instance into multiple instances to integrate # a subset per generated texture or texture UDIM sequence + for (texture_set_name, stack_name), template_maps in maps.items(): + self.log.info(f"Processing {texture_set_name}/{stack_name}") + for template, outputs in template_maps.items(): + self.log.info(f"Processing {template}") + self.create_image_instance(instance, template, outputs) + + def create_image_instance(self, instance, template, outputs): + context = instance.context - for map_name, map_output in outputs.items(): + first_filepath = outputs[0]["filepath"] + fnames = [os.path.basename(output["filepath"]) for output in outputs] + ext = os.path.splitext(first_filepath)[1] + assert ext.lstrip('.'), f"No extension: {ext}" - is_udim = isinstance(map_output, clique.Collection) - if is_udim: - first_file = list(map_output)[0] - map_fnames = [os.path.basename(path) for path in map_output] - else: - first_file = map_output - map_fnames = os.path.basename(map_output) + map_identifier = strip_template(template) - ext = os.path.splitext(first_file)[1] - assert ext.lstrip('.'), f"No extension: {ext}" + # Define the suffix we want to give this particular texture + # set and set up a remapped subset naming for it. + suffix = f".{map_identifier}" + image_subset = instance.data["subset"][len("textureSet"):] + image_subset = "texture" + image_subset + suffix + # Prepare representation + representation = { + 'name': ext.lstrip("."), + 'ext': ext.lstrip("."), + 'files': fnames, + } - # Define the suffix we want to give this particular texture - # set and set up a remapped subset naming for it. - suffix = f".{map_name}" - image_subset = instance.data["subset"][len("textureSet"):] - image_subset = "texture" + image_subset + suffix + # Mark as UDIM explicitly if it has UDIM tiles. + if bool(outputs[0].get("udim")): + representation["udim"] = True - # TODO: Retrieve and store color space with the representation + # TODO: Store color space with the representation - # Clone the instance - image_instance = context.create_instance(instance.name) - image_instance[:] = instance[:] - image_instance.data.update(copy.deepcopy(instance.data)) - image_instance.data["name"] = image_subset - image_instance.data["label"] = image_subset - image_instance.data["subset"] = image_subset - image_instance.data["family"] = "image" - image_instance.data["families"] = ["image", "textures"] - image_instance.data['representations'] = [{ - 'name': ext.lstrip("."), - 'ext': ext.lstrip("."), - 'files': map_fnames, - }] + # Clone the instance + image_instance = context.create_instance(instance.name) + image_instance[:] = instance[:] + image_instance.data.update(copy.deepcopy(instance.data)) + image_instance.data["name"] = image_subset + image_instance.data["label"] = image_subset + image_instance.data["subset"] = image_subset + image_instance.data["family"] = "image" + image_instance.data["families"] = ["image", "textures"] + image_instance.data['representations'] = [representation] - # Group the textures together in the loader - image_instance.data["subsetGroup"] = instance.data["subset"] + # Group the textures together in the loader + image_instance.data["subsetGroup"] = instance.data["subset"] - # Set up the representation for thumbnail generation - # TODO: Simplify this once thumbnail extraction is refactored - staging_dir = os.path.dirname(first_file) - image_instance.data["representations"][0]["tags"] = ["review"] - image_instance.data["representations"][0]["stagingDir"] = staging_dir # noqa + # Set up the representation for thumbnail generation + # TODO: Simplify this once thumbnail extraction is refactored + staging_dir = os.path.dirname(first_filepath) + image_instance.data["representations"][0]["tags"] = ["review"] + image_instance.data["representations"][0]["stagingDir"] = staging_dir - instance.append(image_instance) + instance.append(image_instance) def get_export_config(self, instance): """Return an export configuration dict for texture exports. diff --git a/openpype/hosts/substancepainter/plugins/publish/extract_textures.py b/openpype/hosts/substancepainter/plugins/publish/extract_textures.py index 22acf07284..a5bb274b78 100644 --- a/openpype/hosts/substancepainter/plugins/publish/extract_textures.py +++ b/openpype/hosts/substancepainter/plugins/publish/extract_textures.py @@ -12,9 +12,6 @@ class ExtractTextures(publish.Extractor): particular Extractor doesn't specify representations to integrate. """ - # TODO: More explicitly detect UDIM tiles - # TODO: Get color spaces - # TODO: Detect what source data channels end up in each file label = "Extract Texture Set" hosts = ['substancepainter'] From 04b32350202e17877ddce8832767668e34e95715 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 14 Jan 2023 20:32:05 +0100 Subject: [PATCH 040/360] Cosmetics --- .../plugins/publish/collect_textureset_images.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py index 6928bdb36c..f85861d0eb 100644 --- a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py +++ b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py @@ -37,13 +37,17 @@ class CollectTextureSet(pyblish.api.InstancePlugin): self.create_image_instance(instance, template, outputs) def create_image_instance(self, instance, template, outputs): + f"""Create a new instance per image or UDIM sequence. + + The new instances will be of family `image`. + + """ context = instance.context first_filepath = outputs[0]["filepath"] fnames = [os.path.basename(output["filepath"]) for output in outputs] ext = os.path.splitext(first_filepath)[1] assert ext.lstrip('.'), f"No extension: {ext}" - map_identifier = strip_template(template) # Define the suffix we want to give this particular texture @@ -51,6 +55,7 @@ class CollectTextureSet(pyblish.api.InstancePlugin): suffix = f".{map_identifier}" image_subset = instance.data["subset"][len("textureSet"):] image_subset = "texture" + image_subset + suffix + # Prepare representation representation = { 'name': ext.lstrip("."), @@ -84,6 +89,7 @@ class CollectTextureSet(pyblish.api.InstancePlugin): image_instance.data["representations"][0]["tags"] = ["review"] image_instance.data["representations"][0]["stagingDir"] = staging_dir + # Store the instance in the original instance as a member instance.append(image_instance) def get_export_config(self, instance): From d80e20482b96b388ab91edece375f067f2b9e6b4 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 14 Jan 2023 20:33:19 +0100 Subject: [PATCH 041/360] Cosmetics + add assertion --- openpype/hosts/substancepainter/api/lib.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/substancepainter/api/lib.py b/openpype/hosts/substancepainter/api/lib.py index 2406680a68..bf4415af8a 100644 --- a/openpype/hosts/substancepainter/api/lib.py +++ b/openpype/hosts/substancepainter/api/lib.py @@ -195,8 +195,9 @@ def get_export_templates(config, format="png", strip_folder=True): result = substance_painter.js.evaluate(cmd) if strip_folder: - for stack, maps in result.items(): + for _stack, maps in result.items(): for map_template, map_filepath in maps.items(): + assert map_filepath.startswith(folder) map_filename = map_filepath[len(folder):].lstrip("/") maps[map_template] = map_filename From 196b91896bf9f55414ef766eb2e72631ef066e51 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 14 Jan 2023 20:35:43 +0100 Subject: [PATCH 042/360] Shush hound --- openpype/hosts/substancepainter/api/lib.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/substancepainter/api/lib.py b/openpype/hosts/substancepainter/api/lib.py index bf4415af8a..5b32e3a9aa 100644 --- a/openpype/hosts/substancepainter/api/lib.py +++ b/openpype/hosts/substancepainter/api/lib.py @@ -241,9 +241,8 @@ def _templates_to_regex(templates, return os.path.splitext(os.path.basename(path))[0] if colorspaces and any(colorspaces): - colorspace_match = ( - "(" + "|".join(re.escape(c) for c in colorspaces) + ")" - ) + colorspace_match = "|".join(re.escape(c) for c in set(colorspaces)) + colorspace_match = f"({colorspace_match})" else: # No colorspace support enabled colorspace_match = "" From 5bfb010fbfc0211c7266993fb1b9ddbc2d21162d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 14 Jan 2023 20:36:23 +0100 Subject: [PATCH 043/360] Shush hound - fix invalid escape sequence --- openpype/hosts/substancepainter/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/substancepainter/api/lib.py b/openpype/hosts/substancepainter/api/lib.py index 5b32e3a9aa..278a23ce01 100644 --- a/openpype/hosts/substancepainter/api/lib.py +++ b/openpype/hosts/substancepainter/api/lib.py @@ -285,7 +285,7 @@ def _templates_to_regex(templates, # The filename templates don't include the extension so we add it # to be able to match the out filename beginning to end - ext_regex = "(?P\.[A-Za-z][A-Za-z0-9-]*)" + ext_regex = r"(?P\.[A-Za-z][A-Za-z0-9-]*)" search_regex = rf"^{search_regex}{ext_regex}$" regexes[template] = search_regex From 2335facfff9d800b32bd3b09f71cbb4daf57035e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 14 Jan 2023 20:37:35 +0100 Subject: [PATCH 044/360] Fix docstring --- openpype/hosts/substancepainter/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/substancepainter/api/lib.py b/openpype/hosts/substancepainter/api/lib.py index 278a23ce01..7a10ae1eb6 100644 --- a/openpype/hosts/substancepainter/api/lib.py +++ b/openpype/hosts/substancepainter/api/lib.py @@ -349,7 +349,7 @@ def strip_template(template, strip="._ "): def get_parsed_export_maps(config): - """ + """Return Export Config's expected output textures with parsed data. This tries to parse the texture outputs using a Python API export config. From aa0c62b4d7e73d10e63f7384a9d534a12c8fd16e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 14 Jan 2023 20:38:56 +0100 Subject: [PATCH 045/360] Cleanup --- .../plugins/publish/collect_textureset_images.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py index f85861d0eb..53319ba96d 100644 --- a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py +++ b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py @@ -37,10 +37,10 @@ class CollectTextureSet(pyblish.api.InstancePlugin): self.create_image_instance(instance, template, outputs) def create_image_instance(self, instance, template, outputs): - f"""Create a new instance per image or UDIM sequence. - + """Create a new instance per image or UDIM sequence. + The new instances will be of family `image`. - + """ context = instance.context From cb04f6bb8b07b776544ed0666fe8440ff52a2ce1 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 14 Jan 2023 20:56:29 +0100 Subject: [PATCH 046/360] Fix/Cleanup docstring --- openpype/hosts/substancepainter/api/lib.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/openpype/hosts/substancepainter/api/lib.py b/openpype/hosts/substancepainter/api/lib.py index 7a10ae1eb6..22dc3059fc 100644 --- a/openpype/hosts/substancepainter/api/lib.py +++ b/openpype/hosts/substancepainter/api/lib.py @@ -378,7 +378,7 @@ def get_parsed_export_maps(config): colorSpace: The color space, e.g. "ACES - ACEScg", udim: The udim tile, e.g. "1001" - 2) Template and file outputs + 2) Template output and filepath filepath: Full path to the resulting texture map, e.g. "/path/to/mesh_DefaultMaterial_BaseColor_ACES - ACEScg.1002.png", @@ -386,10 +386,6 @@ def get_parsed_export_maps(config): Note: if template had slashes (folders) then `output` will too. So `output` might include a folder. - channel: The stripped static characters of the filename template which - usually look like an identifier for that map, e.g. "BaseColor". - See `_stripped_template` - Returns: dict: [texture_set, stack]: {template: [file1_data, file2_data]} From 33aafc3ff6f7e1b4f213345e7baa80f50d4e1f51 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 15 Jan 2023 01:30:43 +0100 Subject: [PATCH 047/360] Implement OCIO support for Substance Painter + publish color space with textures --- openpype/hooks/pre_host_set_ocio.py | 37 +++++++++++++++++++ .../publish/collect_textureset_images.py | 9 ++++- .../plugins/publish/extract_textures.py | 19 +++++++++- .../project_settings/substancepainter.json | 10 +++++ .../schema_project_substancepainter.json | 17 +++++++++ 5 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 openpype/hooks/pre_host_set_ocio.py diff --git a/openpype/hooks/pre_host_set_ocio.py b/openpype/hooks/pre_host_set_ocio.py new file mode 100644 index 0000000000..b9e2b79bf4 --- /dev/null +++ b/openpype/hooks/pre_host_set_ocio.py @@ -0,0 +1,37 @@ +from openpype.lib import PreLaunchHook + +from openpype.pipeline.colorspace import get_imageio_config +from openpype.pipeline.template_data import get_template_data_with_names + + +class PreLaunchHostSetOCIO(PreLaunchHook): + """Set OCIO environment for the host""" + + order = 0 + app_groups = ["substancepainter"] + + def execute(self): + """Hook entry method.""" + + anatomy_data = get_template_data_with_names( + project_name=self.data["project_doc"]["name"], + asset_name=self.data["asset_doc"]["name"], + task_name=self.data["task_name"], + host_name=self.host_name, + system_settings=self.data["system_settings"] + ) + + ocio_config = get_imageio_config( + project_name=self.data["project_doc"]["name"], + host_name=self.host_name, + project_settings=self.data["project_settings"], + anatomy_data=anatomy_data, + anatomy=self.data["anatomy"] + ) + + if ocio_config: + ocio_path = ocio_config["path"] + self.log.info(f"Setting OCIO config path: {ocio_path}") + self.launch_context.env["OCIO"] = ocio_path + else: + self.log.debug("OCIO not set or enabled") diff --git a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py index 53319ba96d..0e445c9c1c 100644 --- a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py +++ b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py @@ -67,8 +67,6 @@ class CollectTextureSet(pyblish.api.InstancePlugin): if bool(outputs[0].get("udim")): representation["udim"] = True - # TODO: Store color space with the representation - # Clone the instance image_instance = context.create_instance(instance.name) image_instance[:] = instance[:] @@ -83,6 +81,13 @@ class CollectTextureSet(pyblish.api.InstancePlugin): # Group the textures together in the loader image_instance.data["subsetGroup"] = instance.data["subset"] + # Store color space with the instance + # Note: The extractor will assign it to the representation + colorspace = outputs[0].get("colorSpace") + if colorspace: + self.log.debug(f"{image_subset} colorspace: {colorspace}") + image_instance.data["colorspace"] = colorspace + # Set up the representation for thumbnail generation # TODO: Simplify this once thumbnail extraction is refactored staging_dir = os.path.dirname(first_filepath) diff --git a/openpype/hosts/substancepainter/plugins/publish/extract_textures.py b/openpype/hosts/substancepainter/plugins/publish/extract_textures.py index a5bb274b78..e66ce6dbf6 100644 --- a/openpype/hosts/substancepainter/plugins/publish/extract_textures.py +++ b/openpype/hosts/substancepainter/plugins/publish/extract_textures.py @@ -2,7 +2,7 @@ from openpype.pipeline import KnownPublishError, publish import substance_painter.export -class ExtractTextures(publish.Extractor): +class ExtractTextures(publish.ExtractorColormanaged): """Extract Textures using an output template config. Note: @@ -40,6 +40,23 @@ class ExtractTextures(publish.Extractor): # TODO: Confirm the files indeed exist # TODO: make sure representations are registered + # We'll insert the color space data for each image instance that we + # added into this texture set. The collector couldn't do so because + # some anatomy and other instance data needs to be collected prior + context = instance.context + for image_instance in instance: + + colorspace = image_instance.data.get("colorspace") + if not colorspace: + self.log.debug("No color space data present for instance: " + f"{image_instance}") + continue + + for representation in image_instance.data["representations"]: + self.set_representation_colorspace(representation, + context=context, + colorspace=colorspace) + # Add a fake representation which won't be integrated so the # Integrator leaves us alone - otherwise it would error # TODO: Add `instance.data["integrate"] = False` support in Integrator? diff --git a/openpype/settings/defaults/project_settings/substancepainter.json b/openpype/settings/defaults/project_settings/substancepainter.json index a424a923da..0f9f1af71e 100644 --- a/openpype/settings/defaults/project_settings/substancepainter.json +++ b/openpype/settings/defaults/project_settings/substancepainter.json @@ -1,3 +1,13 @@ { + "imageio": { + "ocio_config": { + "enabled": true, + "filepath": [] + }, + "file_rules": { + "enabled": true, + "rules": {} + } + }, "shelves": {} } \ No newline at end of file diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_substancepainter.json b/openpype/settings/entities/schemas/projects_schema/schema_project_substancepainter.json index 4a02a9d8ca..79a39b8e6e 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_substancepainter.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_substancepainter.json @@ -5,6 +5,23 @@ "label": "Substance Painter", "is_file": true, "children": [ + { + "key": "imageio", + "type": "dict", + "label": "Color Management (ImageIO)", + "is_group": true, + "children": [ + { + "type": "schema", + "name": "schema_imageio_config" + }, + { + "type": "schema", + "name": "schema_imageio_file_rules" + } + + ] + }, { "type": "dict-modifiable", "key": "shelves", From eecf109cab26ab34940ece267e7b26ecd6dc6177 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 15 Jan 2023 01:32:42 +0100 Subject: [PATCH 048/360] Support single image (otherwise integrator will fail) --- .../plugins/publish/collect_textureset_images.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py index 53319ba96d..18d1e59c4c 100644 --- a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py +++ b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py @@ -60,7 +60,7 @@ class CollectTextureSet(pyblish.api.InstancePlugin): representation = { 'name': ext.lstrip("."), 'ext': ext.lstrip("."), - 'files': fnames, + 'files': fnames if len(fnames) > 1 else fnames[0], } # Mark as UDIM explicitly if it has UDIM tiles. From 30ae52770d551bca7d35c0b1cdd9893140cf6db7 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 15 Jan 2023 01:33:21 +0100 Subject: [PATCH 049/360] Rename application group to substancepainter for consistency and clarity --- openpype/hooks/pre_add_last_workfile_arg.py | 2 +- openpype/settings/defaults/system_settings/applications.json | 2 +- .../system_schema/host_settings/schema_substancepainter.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hooks/pre_add_last_workfile_arg.py b/openpype/hooks/pre_add_last_workfile_arg.py index d5a9a41e5a..49fb54d263 100644 --- a/openpype/hooks/pre_add_last_workfile_arg.py +++ b/openpype/hooks/pre_add_last_workfile_arg.py @@ -23,7 +23,7 @@ class AddLastWorkfileToLaunchArgs(PreLaunchHook): "blender", "photoshop", "tvpaint", - "substance", + "substancepainter", "aftereffects" ] diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index 30c692d0e6..d78b54fa05 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -1315,7 +1315,7 @@ } } }, - "substance": { + "substancepainter": { "enabled": true, "label": "Substance Painter", "icon": "app_icons/substancepainter.png", diff --git a/openpype/settings/entities/schemas/system_schema/host_settings/schema_substancepainter.json b/openpype/settings/entities/schemas/system_schema/host_settings/schema_substancepainter.json index 513f98c610..fb3b21e63f 100644 --- a/openpype/settings/entities/schemas/system_schema/host_settings/schema_substancepainter.json +++ b/openpype/settings/entities/schemas/system_schema/host_settings/schema_substancepainter.json @@ -1,6 +1,6 @@ { "type": "dict", - "key": "substance", + "key": "substancepainter", "label": "Substance Painter", "collapsible": true, "checkbox_key": "enabled", From 313cb0d550174bacb0a9377829a62283f3520523 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 15 Jan 2023 01:34:00 +0100 Subject: [PATCH 050/360] Ensure safeguarding against forward/backslashes differences --- openpype/hosts/substancepainter/api/lib.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/substancepainter/api/lib.py b/openpype/hosts/substancepainter/api/lib.py index 22dc3059fc..9bd408f0f2 100644 --- a/openpype/hosts/substancepainter/api/lib.py +++ b/openpype/hosts/substancepainter/api/lib.py @@ -189,7 +189,7 @@ def get_export_templates(config, format="png", strip_folder=True): dict: The expected output maps. """ - folder = config["exportPath"] + folder = config["exportPath"].replace("\\", "/") preset = config["defaultExportPreset"] cmd = f'alg.mapexport.getPathsExportDocumentMaps("{preset}", "{folder}", "{format}")' # noqa result = substance_painter.js.evaluate(cmd) @@ -197,6 +197,7 @@ def get_export_templates(config, format="png", strip_folder=True): if strip_folder: for _stack, maps in result.items(): for map_template, map_filepath in maps.items(): + map_filepath = map_filepath.replace("\\", "/") assert map_filepath.startswith(folder) map_filename = map_filepath[len(folder):].lstrip("/") maps[map_template] = map_filename @@ -441,7 +442,10 @@ def get_parsed_export_maps(config): # We strip explicitly using the full parent export path instead of # using `os.path.basename` because export template is allowed to # have subfolders in its template which we want to match against - assert filepath.startswith(export_path) + filepath = filepath.replace("\\", "/") + assert filepath.startswith(export_path), ( + f"Filepath {filepath} must start with folder {export_path}" + ) filename = filepath[len(export_path):] for template, regex in template_regex.items(): From ece0e7ded2d721dfe92849a8d246bfb4ef0464cd Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 15 Jan 2023 01:36:04 +0100 Subject: [PATCH 051/360] No need to strip folder for the templates, we're not using the filename values of the result. --- openpype/hosts/substancepainter/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/substancepainter/api/lib.py b/openpype/hosts/substancepainter/api/lib.py index 9bd408f0f2..754f8a2bd6 100644 --- a/openpype/hosts/substancepainter/api/lib.py +++ b/openpype/hosts/substancepainter/api/lib.py @@ -395,7 +395,7 @@ def get_parsed_export_maps(config): from .colorspace import get_project_channel_data outputs = substance_painter.export.list_project_textures(config) - templates = get_export_templates(config) + templates = get_export_templates(config, strip_folder=False) # Get all color spaces set for the current project project_colorspaces = set( From 31e37e5a33298718c541bb1969e464ff7ae930e9 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 15 Jan 2023 02:07:00 +0100 Subject: [PATCH 052/360] Use project doc and asset doc directly for `get_template_data` --- openpype/hooks/pre_host_set_ocio.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hooks/pre_host_set_ocio.py b/openpype/hooks/pre_host_set_ocio.py index b9e2b79bf4..3620d88db6 100644 --- a/openpype/hooks/pre_host_set_ocio.py +++ b/openpype/hooks/pre_host_set_ocio.py @@ -1,7 +1,7 @@ from openpype.lib import PreLaunchHook from openpype.pipeline.colorspace import get_imageio_config -from openpype.pipeline.template_data import get_template_data_with_names +from openpype.pipeline.template_data import get_template_data class PreLaunchHostSetOCIO(PreLaunchHook): @@ -13,9 +13,9 @@ class PreLaunchHostSetOCIO(PreLaunchHook): def execute(self): """Hook entry method.""" - anatomy_data = get_template_data_with_names( - project_name=self.data["project_doc"]["name"], - asset_name=self.data["asset_doc"]["name"], + anatomy_data = get_template_data( + project_doc=self.data["project_doc"], + asset_doc=self.data["asset_doc"], task_name=self.data["task_name"], host_name=self.host_name, system_settings=self.data["system_settings"] From 9329ff28d57f75d54dec1ba5aa25f390e02f7f3d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 17 Jan 2023 15:39:59 +0100 Subject: [PATCH 053/360] Show new project prompt with mesh preloaded --- openpype/hosts/substancepainter/api/lib.py | 126 ++++++++++++++++++ .../plugins/load/load_mesh.py | 17 +-- 2 files changed, 131 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/substancepainter/api/lib.py b/openpype/hosts/substancepainter/api/lib.py index 754f8a2bd6..e552caee6d 100644 --- a/openpype/hosts/substancepainter/api/lib.py +++ b/openpype/hosts/substancepainter/api/lib.py @@ -520,3 +520,129 @@ def load_shelf(path, name=None): substance_painter.resource.Shelves.add(name, path) return name + + +def _get_new_project_action(): + """Return QAction which triggers Substance Painter's new project dialog""" + from PySide2 import QtGui + + main_window = substance_painter.ui.get_main_window() + + # Find the file menu's New file action + menubar = main_window.menuBar() + new_action = None + for action in menubar.actions(): + menu = action.menu() + if not menu: + continue + + if menu.objectName() != "file": + continue + + # Find the action with the CTRL+N key sequence + new_action = next(action for action in menu.actions() + if action.shortcut() == QtGui.QKeySequence.New) + break + + return new_action + + +def prompt_new_file_with_mesh(mesh_filepath): + """Prompts the user for a new file using Substance Painter's own dialog. + + This will set the mesh path to load to the given mesh and disables the + dialog box to disallow the user to change the path. This way we can allow + user configuration of a project but set the mesh path ourselves. + + Warning: + This is very hacky and experimental. + + Note: + If a project is currently open using the same mesh filepath it can't + accurately detect whether the user had actually accepted the new project + dialog or whether the project afterwards is still the original project, + for example when the user might have cancelled the operation. + + """ + from PySide2 import QtWidgets, QtCore + + app = QtWidgets.QApplication.instance() + assert os.path.isfile(mesh_filepath), \ + f"Mesh filepath does not exist: {mesh_filepath}" + + def _setup_file_dialog(): + """Set filepath in QFileDialog and trigger accept result""" + file_dialog = app.activeModalWidget() + assert isinstance(file_dialog, QtWidgets.QFileDialog) + + # Quickly hide the dialog + file_dialog.hide() + app.processEvents(QtCore.QEventLoop.ExcludeUserInputEvents, 1000) + + file_dialog.setDirectory(os.path.dirname(mesh_filepath)) + url = QtCore.QUrl.fromLocalFile(os.path.basename(mesh_filepath)) + file_dialog.selectUrl(url) + + # Give the explorer window time to refresh to the folder and select + # the file + while not file_dialog.selectedFiles(): + app.processEvents(QtCore.QEventLoop.ExcludeUserInputEvents, 1000) + print(f"Selected: {file_dialog.selectedFiles()}") + + # Set it again now we know the path is refreshed - without this + # accepting the dialog will often not trigger the correct filepath + file_dialog.setDirectory(os.path.dirname(mesh_filepath)) + url = QtCore.QUrl.fromLocalFile(os.path.basename(mesh_filepath)) + file_dialog.selectUrl(url) + + file_dialog.done(file_dialog.Accepted) + app.processEvents(QtCore.QEventLoop.AllEvents) + + def _setup_prompt(): + app.processEvents(QtCore.QEventLoop.ExcludeUserInputEvents) + dialog = app.activeModalWidget() + assert dialog.objectName() == "NewProjectDialog" + + # Set the window title + mesh = os.path.basename(mesh_filepath) + dialog.setWindowTitle(f"New Project with mesh: {mesh}") + + # Get the select mesh file button + mesh_select = dialog.findChild(QtWidgets.QPushButton, "meshSelect") + + # Hide the select mesh button to the user to block changing of mesh + mesh_select.setVisible(False) + + # Ensure UI is visually up-to-date + app.processEvents(QtCore.QEventLoop.ExcludeUserInputEvents) + + # Trigger the 'select file' dialog to set the path and have the + # new file dialog to use the path. + QtCore.QTimer.singleShot(10, _setup_file_dialog) + mesh_select.click() + + app.processEvents(QtCore.QEventLoop.AllEvents, 5000) + + mesh_filename = dialog.findChild(QtWidgets.QFrame, "meshFileName") + mesh_filename_label = mesh_filename.findChild(QtWidgets.QLabel) + if not mesh_filename_label.text(): + dialog.close() + raise RuntimeError(f"Failed to set mesh path: {mesh_filepath}") + + new_action = _get_new_project_action() + if not new_action: + raise RuntimeError("Unable to detect new file action..") + + QtCore.QTimer.singleShot(0, _setup_prompt) + new_action.trigger() + app.processEvents(QtCore.QEventLoop.AllEvents, 5000) + + if not substance_painter.project.is_open(): + return + + # Confirm mesh was set as expected + project_mesh = substance_painter.project.last_imported_mesh_path() + if os.path.normpath(project_mesh) != os.path.normpath(mesh_filepath): + return + + return project_mesh diff --git a/openpype/hosts/substancepainter/plugins/load/load_mesh.py b/openpype/hosts/substancepainter/plugins/load/load_mesh.py index 00f808199f..4e800bd623 100644 --- a/openpype/hosts/substancepainter/plugins/load/load_mesh.py +++ b/openpype/hosts/substancepainter/plugins/load/load_mesh.py @@ -7,6 +7,7 @@ from openpype.hosts.substancepainter.api.pipeline import ( set_container_metadata, remove_container_metadata ) +from openpype.hosts.substancepainter.api.lib import prompt_new_file_with_mesh import substance_painter.project import qargparse @@ -45,18 +46,10 @@ class SubstanceLoadProjectMesh(load.LoaderPlugin): if not substance_painter.project.is_open(): # Allow to 'initialize' a new project - # TODO: preferably these settings would come from the actual - # new project prompt of Substance (or something that is - # visually similar to still allow artist decisions) - settings = substance_painter.project.Settings( - default_texture_resolution=4096, - import_cameras=import_cameras, - ) - - substance_painter.project.create( - mesh_file_path=self.fname, - settings=settings - ) + result = prompt_new_file_with_mesh(mesh_filepath=self.fname) + if not result: + self.log.info("User cancelled new project prompt.") + return else: # Reload the mesh From 033d37ca283e6fed6d9a9337e4001e5978b12271 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 17 Jan 2023 17:01:59 +0100 Subject: [PATCH 054/360] Early draft for Substance Painter documentation --- website/docs/artist_hosts_substancepainter.md | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 website/docs/artist_hosts_substancepainter.md diff --git a/website/docs/artist_hosts_substancepainter.md b/website/docs/artist_hosts_substancepainter.md new file mode 100644 index 0000000000..9ed83421af --- /dev/null +++ b/website/docs/artist_hosts_substancepainter.md @@ -0,0 +1,80 @@ +--- +id: artist_hosts_substancepainter +title: Substance Painter +sidebar_label: Substance Painter +--- + +## OpenPype global tools + +- [Work Files](artist_tools.md#workfiles) +- [Load](artist_tools.md#loader) +- [Manage (Inventory)](artist_tools.md#inventory) +- [Publish](artist_tools.md#publisher) +- [Library Loader](artist_tools.md#library-loader) + +## Working with OpenPype in Substance Painter + +The Substance Painter OpenPype integration allows you to: + +- Set the project mesh and easily keep it in sync with updates of the model +- Easily export your textures as versioned publishes for others to load and update. + +## Setting the project mesh + +Substance Painter requires a project file to have a mesh path configured. +As such, you can't start a workfile without choosing a mesh path. + +To start a new project using a published model you can _without an open project_ +use OpenPype > Load.. > Load Mesh on a supported publish. This will prompt you +with a New Project prompt preset to that particular mesh file. + +If you already have a project open, you can also replace (reload) your mesh +using the same Load Mesh functionality. + +After having the project mesh loaded or reloaded through the loader +tool the mesh will be _managed_ by OpenPype. For example, you'll be notified +on workfile open whether the mesh in your workfile is outdated. You can also +set it to specific version using OpenPype > Manage.. where you can right click +on the project mesh to perform _Set Version_ + +:::info +A Substance Painter project will always have only one mesh set. Whenever you +trigger _Load Mesh_ from the loader this will **replace** your currently loaded +mesh for your open project. +::: + +## Publishing textures + +To publish your textures we must first create a `textureSet` +publish instance. + +To create a **TextureSet instance** we will use OpenPype's publisher tool. Go +to **OpenPype → Publish... → TextureSet** + +The texture set instance will define what Substance Painter export template `.spexp` to +use and thus defines what texture maps will be exported from your workfile. + +:::info +The TextureSet instance gets saved with your Substance Painter project. As such, +you will only need to configure this once for your workfile. Next time you can +just click **OpenPype → Publish...** and start publishing directly with the +same settings. +::: + + +### Known issues + +#### Can't see the OpenPype menu? + +If you're unable to see the OpenPype top level menu in Substance Painter make +sure you have launched Substance Painter through OpenPype and that the OpenPype +Integration plug-in is loaded inside Substance Painter: **Python > openpype_plugin** + +#### Substance Painter + Steam + +Running the steam version of Substance Painter within OpenPype will require you +to close the Steam executable before launching Substance Painter through OpenPype. +Otherwise the Substance Painter process is launched using Steam's existing +environment and thus will not be able to pick up the pipeline integration. + +This appears to be a limitation of how Steam works. \ No newline at end of file From 1c77d2b002527a450c8be21d93040bccd588413e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 18 Jan 2023 10:18:01 +0100 Subject: [PATCH 055/360] Fix UDIM integration --- .../plugins/publish/collect_textureset_images.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py index 18d1e59c4c..5f06880663 100644 --- a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py +++ b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py @@ -65,7 +65,10 @@ class CollectTextureSet(pyblish.api.InstancePlugin): # Mark as UDIM explicitly if it has UDIM tiles. if bool(outputs[0].get("udim")): - representation["udim"] = True + # The representation for a UDIM sequence should have a `udim` key + # that is a list of all udim tiles (str) like: ["1001", "1002"] + # strings. See CollectTextures plug-in and Integrators. + representation["udim"] = [output["udim"] for output in outputs] # TODO: Store color space with the representation From a31b6035fe81ff0fe71b335fbd96e6c6f8e5ab9e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 2 Mar 2023 17:18:05 +0800 Subject: [PATCH 056/360] add model creator, extractors and loaders --- .../hosts/max/plugins/create/create_model.py | 26 +++++ .../hosts/max/plugins/load/load_max_scene.py | 3 +- openpype/hosts/max/plugins/load/load_model.py | 98 +++++++++++++++++++ .../hosts/max/plugins/load/load_pointcache.py | 3 +- .../plugins/publish/extract_max_scene_raw.py | 3 +- .../max/plugins/publish/extract_model.py | 74 ++++++++++++++ 6 files changed, 203 insertions(+), 4 deletions(-) create mode 100644 openpype/hosts/max/plugins/create/create_model.py create mode 100644 openpype/hosts/max/plugins/load/load_model.py create mode 100644 openpype/hosts/max/plugins/publish/extract_model.py diff --git a/openpype/hosts/max/plugins/create/create_model.py b/openpype/hosts/max/plugins/create/create_model.py new file mode 100644 index 0000000000..a78a30e0c7 --- /dev/null +++ b/openpype/hosts/max/plugins/create/create_model.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +"""Creator plugin for model.""" +from openpype.hosts.max.api import plugin +from openpype.pipeline import CreatedInstance + + +class CreateModel(plugin.MaxCreator): + identifier = "io.openpype.creators.max.model" + label = "Model" + family = "model" + icon = "gear" + + def create(self, subset_name, instance_data, pre_create_data): + from pymxs import runtime as rt + sel_obj = list(rt.selection) + instance = super(CreateModel, self).create( + subset_name, + instance_data, + pre_create_data) # type: CreatedInstance + container = rt.getNodeByName(instance.data.get("instance_node")) + # TODO: Disable "Add to Containers?" Panel + # parent the selected cameras into the container + for obj in sel_obj: + obj.parent = container + # for additional work on the node: + # instance_node = rt.getNodeByName(instance.get("instance_node")) diff --git a/openpype/hosts/max/plugins/load/load_max_scene.py b/openpype/hosts/max/plugins/load/load_max_scene.py index b863b9363f..d37d3439fb 100644 --- a/openpype/hosts/max/plugins/load/load_max_scene.py +++ b/openpype/hosts/max/plugins/load/load_max_scene.py @@ -9,7 +9,8 @@ from openpype.hosts.max.api import lib class MaxSceneLoader(load.LoaderPlugin): """Max Scene Loader""" - families = ["camera"] + families = ["camera", + "model"] representations = ["max"] order = -8 icon = "code-fork" diff --git a/openpype/hosts/max/plugins/load/load_model.py b/openpype/hosts/max/plugins/load/load_model.py new file mode 100644 index 0000000000..e6262b4f86 --- /dev/null +++ b/openpype/hosts/max/plugins/load/load_model.py @@ -0,0 +1,98 @@ + +import os +from openpype.pipeline import ( + load, get_representation_path +) +from openpype.hosts.max.api.pipeline import containerise +from openpype.hosts.max.api import lib + + +class ModelAbcLoader(load.LoaderPlugin): + """Loading model with the Alembic loader.""" + + families = ["model"] + label = "Load Model(Alembic)" + representations = ["abc"] + order = -10 + icon = "code-fork" + color = "orange" + + def load(self, context, name=None, namespace=None, data=None): + from pymxs import runtime as rt + + file_path = os.path.normpath(self.fname) + + abc_before = { + c for c in rt.rootNode.Children + if rt.classOf(c) == rt.AlembicContainer + } + + abc_import_cmd = (f""" +AlembicImport.ImportToRoot = false +AlembicImport.CustomAttributes = true +AlembicImport.UVs = true +AlembicImport.VertexColors = true + +importFile @"{file_path}" #noPrompt + """) + + self.log.debug(f"Executing command: {abc_import_cmd}") + rt.execute(abc_import_cmd) + + abc_after = { + c for c in rt.rootNode.Children + if rt.classOf(c) == rt.AlembicContainer + } + + # This should yield new AlembicContainer node + abc_containers = abc_after.difference(abc_before) + + if len(abc_containers) != 1: + self.log.error("Something failed when loading.") + + abc_container = abc_containers.pop() + + return containerise( + name, [abc_container], context, loader=self.__class__.__name__) + + def update(self, container, representation): + from pymxs import runtime as rt + + path = get_representation_path(representation) + node = rt.getNodeByName(container["instance_node"]) + + alembic_objects = self.get_container_children(node, "AlembicObject") + for alembic_object in alembic_objects: + alembic_object.source = path + + lib.imprint(container["instance_node"], { + "representation": str(representation["_id"]) + }) + + def switch(self, container, representation): + self.update(container, representation) + + def remove(self, container): + from pymxs import runtime as rt + + node = rt.getNodeByName(container["instance_node"]) + rt.delete(node) + + @staticmethod + def get_container_children(parent, type_name): + from pymxs import runtime as rt + + def list_children(node): + children = [] + for c in node.Children: + children.append(c) + children += list_children(c) + return children + + filtered = [] + for child in list_children(parent): + class_type = str(rt.classOf(child.baseObject)) + if class_type == type_name: + filtered.append(child) + + return filtered diff --git a/openpype/hosts/max/plugins/load/load_pointcache.py b/openpype/hosts/max/plugins/load/load_pointcache.py index f7a72ece25..b3e12adc7b 100644 --- a/openpype/hosts/max/plugins/load/load_pointcache.py +++ b/openpype/hosts/max/plugins/load/load_pointcache.py @@ -15,8 +15,7 @@ from openpype.hosts.max.api import lib class AbcLoader(load.LoaderPlugin): """Alembic loader.""" - families = ["model", - "camera", + families = ["camera", "animation", "pointcache"] label = "Load Alembic" diff --git a/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py b/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py index cacc84c591..aa01ad1a3a 100644 --- a/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py +++ b/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py @@ -20,7 +20,8 @@ class ExtractMaxSceneRaw(publish.Extractor, order = pyblish.api.ExtractorOrder - 0.2 label = "Extract Max Scene (Raw)" hosts = ["max"] - families = ["camera"] + families = ["camera", + "model"] optional = True def process(self, instance): diff --git a/openpype/hosts/max/plugins/publish/extract_model.py b/openpype/hosts/max/plugins/publish/extract_model.py new file mode 100644 index 0000000000..710ad5f97d --- /dev/null +++ b/openpype/hosts/max/plugins/publish/extract_model.py @@ -0,0 +1,74 @@ +import os +import pyblish.api +from openpype.pipeline import ( + publish, + OptionalPyblishPluginMixin +) +from pymxs import runtime as rt +from openpype.hosts.max.api import ( + maintained_selection, + get_all_children +) + + +class ExtractModel(publish.Extractor, + OptionalPyblishPluginMixin): + """ + Extract Geometry in Alembic Format + """ + + order = pyblish.api.ExtractorOrder - 0.1 + label = "Extract Geometry (Alembic)" + hosts = ["max"] + families = ["model"] + optional = True + + def process(self, instance): + if not self.is_active(instance.data): + return + + container = instance.data["instance_node"] + + self.log.info("Extracting Geometry ...") + + stagingdir = self.staging_dir(instance) + filename = "{name}.abc".format(**instance.data) + filepath = os.path.join(stagingdir, filename) + + # We run the render + self.log.info("Writing alembic '%s' to '%s'" % (filename, + stagingdir)) + + export_cmd = ( + f""" +AlembicExport.ArchiveType = #ogawa +AlembicExport.CoordinateSystem = #maya +AlembicExport.CustomAttributes = true +AlembicExport.UVs = true +AlembicExport.VertexColors = true +AlembicExport.PreserveInstances = true + +exportFile @"{filepath}" #noPrompt selectedOnly:on using:AlembicExport + + """) + + self.log.debug(f"Executing command: {export_cmd}") + + with maintained_selection(): + # select and export + rt.select(get_all_children(rt.getNodeByName(container))) + rt.execute(export_cmd) + + self.log.info("Performing Extraction ...") + if "representations" not in instance.data: + instance.data["representations"] = [] + + representation = { + 'name': 'abc', + 'ext': 'abc', + 'files': filename, + "stagingDir": stagingdir, + } + instance.data["representations"].append(representation) + self.log.info("Extracted instance '%s' to: %s" % (instance.name, + filepath)) From f18455717c95b67846558e59a785143961d5fc58 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 2 Mar 2023 17:25:45 +0800 Subject: [PATCH 057/360] OP-4245 - Data Exchange: geometry --- .../max/plugins/publish/extract_model_usd.py | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 openpype/hosts/max/plugins/publish/extract_model_usd.py diff --git a/openpype/hosts/max/plugins/publish/extract_model_usd.py b/openpype/hosts/max/plugins/publish/extract_model_usd.py new file mode 100644 index 0000000000..1c8bf073da --- /dev/null +++ b/openpype/hosts/max/plugins/publish/extract_model_usd.py @@ -0,0 +1,112 @@ +import os +import pyblish.api +from openpype.pipeline import ( + publish, + OptionalPyblishPluginMixin +) +from pymxs import runtime as rt +from openpype.hosts.max.api import ( + maintained_selection, + get_all_children +) + + +class ExtractModelUSD(publish.Extractor, + OptionalPyblishPluginMixin): + """ + Extract Geometry in USDA Format + """ + + order = pyblish.api.ExtractorOrder - 0.05 + label = "Extract Geometry (USD)" + hosts = ["max"] + families = ["model"] + optional = True + + def process(self, instance): + if not self.is_active(instance.data): + return + + container = instance.data["instance_node"] + + self.log.info("Extracting Geometry ...") + + stagingdir = self.staging_dir(instance) + asset_filename = "{name}.usda".format(**instance.data) + asset_filepath = os.path.join(stagingdir, + asset_filename) + self.log.info("Writing USD '%s' to '%s'" % (asset_filepath, + stagingdir)) + + log_filename ="{name}.txt".format(**instance.data) + log_filepath = os.path.join(stagingdir, + log_filename) + self.log.info("Writing log '%s' to '%s'" % (log_filepath, + stagingdir)) + + # get the nodes which need to be exported + export_options = self.get_export_options(log_filepath) + with maintained_selection(): + # select and export + node_list = self.get_node_list(container) + rt.USDExporter.ExportFile(asset_filepath, + exportOptions=export_options, + nodeList=node_list) + + self.log.info("Performing Extraction ...") + if "representations" not in instance.data: + instance.data["representations"] = [] + + representation = { + 'name': 'usda', + 'ext': 'usda', + 'files': asset_filename, + "stagingDir": stagingdir, + } + instance.data["representations"].append(representation) + + log_representation = { + 'name': 'txt', + 'ext': 'txt', + 'files': log_filename, + "stagingDir": stagingdir, + } + instance.data["representations"].append(log_representation) + + self.log.info("Extracted instance '%s' to: %s" % (instance.name, + asset_filepath)) + + def get_node_list(self, container): + """ + Get the target nodes which are + the children of the container + """ + node_list = [] + + container_node = rt.getNodeByName(container) + target_node = container_node.Children + rt.select(target_node) + for sel in rt.selection: + node_list.append(sel) + + return node_list + + def get_export_options(self, log_path): + """Set Export Options for USD Exporter""" + + export_options = rt.USDExporter.createOptions() + + export_options.Meshes = True + export_options.Lights = False + export_options.Cameras = False + export_options.Materials = False + export_options.FileFormat = rt.name('ascii') + export_options.UpAxis = rt.name('y') + export_options.LogLevel = rt.name('info') + export_options.LogPath = log_path + export_options.PreserveEdgeOrientation = True + export_options.TimeMode = rt.name('current') + + rt.USDexporter.UIOptions = export_options + + return export_options From a7c11f0aece3b0484d94b64e92955103fc5b93e2 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 2 Mar 2023 17:30:25 +0800 Subject: [PATCH 058/360] hound fix --- openpype/hosts/max/plugins/publish/extract_model_usd.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_model_usd.py b/openpype/hosts/max/plugins/publish/extract_model_usd.py index 1c8bf073da..0f8d283907 100644 --- a/openpype/hosts/max/plugins/publish/extract_model_usd.py +++ b/openpype/hosts/max/plugins/publish/extract_model_usd.py @@ -6,8 +6,7 @@ from openpype.pipeline import ( ) from pymxs import runtime as rt from openpype.hosts.max.api import ( - maintained_selection, - get_all_children + maintained_selection ) @@ -38,7 +37,7 @@ class ExtractModelUSD(publish.Extractor, self.log.info("Writing USD '%s' to '%s'" % (asset_filepath, stagingdir)) - log_filename ="{name}.txt".format(**instance.data) + log_filename = "{name}.txt".format(**instance.data) log_filepath = os.path.join(stagingdir, log_filename) self.log.info("Writing log '%s' to '%s'" % (log_filepath, From b5d748f466858557d09680923a30f1851cc8e6a2 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 3 Mar 2023 13:02:24 +0800 Subject: [PATCH 059/360] add export options to the usd extractors and add usd loader --- .../hosts/max/plugins/load/load_model_usd.py | 59 +++++++++++++++++++ .../max/plugins/publish/extract_model_usd.py | 2 + 2 files changed, 61 insertions(+) create mode 100644 openpype/hosts/max/plugins/load/load_model_usd.py diff --git a/openpype/hosts/max/plugins/load/load_model_usd.py b/openpype/hosts/max/plugins/load/load_model_usd.py new file mode 100644 index 0000000000..c6c414b91c --- /dev/null +++ b/openpype/hosts/max/plugins/load/load_model_usd.py @@ -0,0 +1,59 @@ +import os +from openpype.pipeline import ( + load, get_representation_path +) +from openpype.hosts.max.api.pipeline import containerise +from openpype.hosts.max.api import lib + + +class ModelUSDLoader(load.LoaderPlugin): + """Loading model with the USD loader.""" + + families = ["model"] + label = "Load Model(USD)" + representations = ["usda"] + order = -10 + icon = "code-fork" + color = "orange" + + def load(self, context, name=None, namespace=None, data=None): + from pymxs import runtime as rt + # asset_filepath + filepath = os.path.normpath(self.fname) + import_options = rt.USDImporter.CreateOptions() + base_filename = os.path.basename(filepath) + filename, ext = os.path.splitext(base_filename) + log_filepath = filepath.replace(ext, "txt") + + rt.LogPath = log_filepath + rt.LogLevel = rt.name('info') + rt.USDImporter.importFile(filepath, + importOptions=import_options) + + asset = rt.getNodeByName(f"{name}") + + return containerise( + name, [asset], context, loader=self.__class__.__name__) + + def update(self, container, representation): + from pymxs import runtime as rt + + path = get_representation_path(representation) + node = rt.getNodeByName(container["instance_node"]) + + usd_objects = self.get_container_children(node) + for usd_object in usd_objects: + usd_object.source = path + + lib.imprint(container["instance_node"], { + "representation": str(representation["_id"]) + }) + + def switch(self, container, representation): + self.update(container, representation) + + def remove(self, container): + from pymxs import runtime as rt + + node = rt.getNodeByName(container["instance_node"]) + rt.delete(node) diff --git a/openpype/hosts/max/plugins/publish/extract_model_usd.py b/openpype/hosts/max/plugins/publish/extract_model_usd.py index 0f8d283907..2f89e4de16 100644 --- a/openpype/hosts/max/plugins/publish/extract_model_usd.py +++ b/openpype/hosts/max/plugins/publish/extract_model_usd.py @@ -96,9 +96,11 @@ class ExtractModelUSD(publish.Extractor, export_options = rt.USDExporter.createOptions() export_options.Meshes = True + export_options.Shapes = True export_options.Lights = False export_options.Cameras = False export_options.Materials = False + export_options.MeshFormat = rt.name('fromScene') export_options.FileFormat = rt.name('ascii') export_options.UpAxis = rt.name('y') export_options.LogLevel = rt.name('info') From 519cef018529e17fb94c7c8bb197885c762ede93 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 3 Mar 2023 13:34:35 +0800 Subject: [PATCH 060/360] add validator for model family --- .../max/plugins/publish/extract_model_usd.py | 2 +- .../publish/validate_model_contents.py | 44 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 openpype/hosts/max/plugins/publish/validate_model_contents.py diff --git a/openpype/hosts/max/plugins/publish/extract_model_usd.py b/openpype/hosts/max/plugins/publish/extract_model_usd.py index 2f89e4de16..b20fd45eae 100644 --- a/openpype/hosts/max/plugins/publish/extract_model_usd.py +++ b/openpype/hosts/max/plugins/publish/extract_model_usd.py @@ -96,7 +96,7 @@ class ExtractModelUSD(publish.Extractor, export_options = rt.USDExporter.createOptions() export_options.Meshes = True - export_options.Shapes = True + export_options.Shapes = False export_options.Lights = False export_options.Cameras = False export_options.Materials = False diff --git a/openpype/hosts/max/plugins/publish/validate_model_contents.py b/openpype/hosts/max/plugins/publish/validate_model_contents.py new file mode 100644 index 0000000000..01ae869c30 --- /dev/null +++ b/openpype/hosts/max/plugins/publish/validate_model_contents.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +import pyblish.api +from openpype.pipeline import PublishValidationError +from pymxs import runtime as rt + + +class ValidateModelContent(pyblish.api.InstancePlugin): + """Validates Model instance contents. + + A model instance may only hold either geometry + or editable meshes. + """ + + order = pyblish.api.ValidatorOrder + families = ["model"] + hosts = ["max"] + label = "Model Contents" + + def process(self, instance): + invalid = self.get_invalid(instance) + if invalid: + raise PublishValidationError("Model instance must only include" + "Geometry and Editable Mesh") + + def get_invalid(self, instance): + """ + Get invalid nodes if the instance is not camera + """ + invalid = list() + container = instance.data["instance_node"] + self.log.info("Validating look content for " + "{}".format(container)) + + con = rt.getNodeByName(container) + selection_list = list(con.Children) + for sel in selection_list: + if rt.classOf(sel) in rt.Camera.classes: + invalid.append(sel) + if rt.classOf(sel) in rt.Light.classes: + invalid.append(sel) + if rt.classOf(sel) in rt.Shape.classes: + invalid.append(sel) + + return invalid From 12211d70371354fafad96f980d05743542be6c5e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 3 Mar 2023 13:35:52 +0800 Subject: [PATCH 061/360] add info in docstring for the validator --- openpype/hosts/max/plugins/publish/validate_model_contents.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_model_contents.py b/openpype/hosts/max/plugins/publish/validate_model_contents.py index 01ae869c30..dd9c8de2cf 100644 --- a/openpype/hosts/max/plugins/publish/validate_model_contents.py +++ b/openpype/hosts/max/plugins/publish/validate_model_contents.py @@ -7,8 +7,8 @@ from pymxs import runtime as rt class ValidateModelContent(pyblish.api.InstancePlugin): """Validates Model instance contents. - A model instance may only hold either geometry - or editable meshes. + A model instance may only hold either geometry-related + object(excluding Shapes) or editable meshes. """ order = pyblish.api.ValidatorOrder From c98160691b9e1273de8294ad1080792e8080c8a5 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 3 Mar 2023 16:01:22 +0800 Subject: [PATCH 062/360] add usdmodel as families --- .../max/plugins/create/create_model_usd.py | 22 +++++++++++++++++++ .../hosts/max/plugins/load/load_model_usd.py | 2 +- .../max/plugins/publish/extract_model_usd.py | 2 +- .../publish/validate_model_contents.py | 2 +- openpype/plugins/publish/integrate.py | 1 + openpype/plugins/publish/integrate_legacy.py | 1 + 6 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 openpype/hosts/max/plugins/create/create_model_usd.py diff --git a/openpype/hosts/max/plugins/create/create_model_usd.py b/openpype/hosts/max/plugins/create/create_model_usd.py new file mode 100644 index 0000000000..237ae8f4ae --- /dev/null +++ b/openpype/hosts/max/plugins/create/create_model_usd.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +"""Creator plugin for model exported in USD format.""" +from openpype.hosts.max.api import plugin +from openpype.pipeline import CreatedInstance + + +class CreateUSDModel(plugin.MaxCreator): + identifier = "io.openpype.creators.max.usdmodel" + label = "USD Model" + family = "usdmodel" + icon = "gear" + + def create(self, subset_name, instance_data, pre_create_data): + from pymxs import runtime as rt + _ = super(CreateUSDModel, self).create( + subset_name, + instance_data, + pre_create_data) # type: CreatedInstance + # TODO: Disable "Add to Containers?" Panel + # parent the selected cameras into the container + # for additional work on the node: + # instance_node = rt.getNodeByName(instance.get("instance_node")) diff --git a/openpype/hosts/max/plugins/load/load_model_usd.py b/openpype/hosts/max/plugins/load/load_model_usd.py index c6c414b91c..ac318fbb57 100644 --- a/openpype/hosts/max/plugins/load/load_model_usd.py +++ b/openpype/hosts/max/plugins/load/load_model_usd.py @@ -9,7 +9,7 @@ from openpype.hosts.max.api import lib class ModelUSDLoader(load.LoaderPlugin): """Loading model with the USD loader.""" - families = ["model"] + families = ["usdmodel"] label = "Load Model(USD)" representations = ["usda"] order = -10 diff --git a/openpype/hosts/max/plugins/publish/extract_model_usd.py b/openpype/hosts/max/plugins/publish/extract_model_usd.py index b20fd45eae..e0ad3bb23e 100644 --- a/openpype/hosts/max/plugins/publish/extract_model_usd.py +++ b/openpype/hosts/max/plugins/publish/extract_model_usd.py @@ -19,7 +19,7 @@ class ExtractModelUSD(publish.Extractor, order = pyblish.api.ExtractorOrder - 0.05 label = "Extract Geometry (USD)" hosts = ["max"] - families = ["model"] + families = ["usdmodel"] optional = True def process(self, instance): diff --git a/openpype/hosts/max/plugins/publish/validate_model_contents.py b/openpype/hosts/max/plugins/publish/validate_model_contents.py index dd9c8de2cf..34578e6920 100644 --- a/openpype/hosts/max/plugins/publish/validate_model_contents.py +++ b/openpype/hosts/max/plugins/publish/validate_model_contents.py @@ -12,7 +12,7 @@ class ValidateModelContent(pyblish.api.InstancePlugin): """ order = pyblish.api.ValidatorOrder - families = ["model"] + families = ["model", "usdmodel"] hosts = ["max"] label = "Model Contents" diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index b117006871..fc098b416a 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -124,6 +124,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): "xgen", "hda", "usd", + "usdmodel", "staticMesh", "skeletalMesh", "mvLook", diff --git a/openpype/plugins/publish/integrate_legacy.py b/openpype/plugins/publish/integrate_legacy.py index b93abab1d8..ba32c376d8 100644 --- a/openpype/plugins/publish/integrate_legacy.py +++ b/openpype/plugins/publish/integrate_legacy.py @@ -120,6 +120,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "xgen", "hda", "usd", + "usdmodel", "staticMesh", "skeletalMesh", "mvLook", From fd6aa8302eee6cfcb44cb4d80f30466cd994485d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 3 Mar 2023 16:02:24 +0800 Subject: [PATCH 063/360] add usdmodel as families --- openpype/hosts/max/plugins/create/create_model_usd.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/max/plugins/create/create_model_usd.py b/openpype/hosts/max/plugins/create/create_model_usd.py index 237ae8f4ae..21407ae1f3 100644 --- a/openpype/hosts/max/plugins/create/create_model_usd.py +++ b/openpype/hosts/max/plugins/create/create_model_usd.py @@ -11,7 +11,6 @@ class CreateUSDModel(plugin.MaxCreator): icon = "gear" def create(self, subset_name, instance_data, pre_create_data): - from pymxs import runtime as rt _ = super(CreateUSDModel, self).create( subset_name, instance_data, From 5b4eff51acd3fdb3b6700fa154986fc34cb022a6 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 3 Mar 2023 20:30:05 +0800 Subject: [PATCH 064/360] include only model family --- openpype/hosts/max/plugins/publish/validate_model_contents.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/validate_model_contents.py b/openpype/hosts/max/plugins/publish/validate_model_contents.py index 34578e6920..dd9c8de2cf 100644 --- a/openpype/hosts/max/plugins/publish/validate_model_contents.py +++ b/openpype/hosts/max/plugins/publish/validate_model_contents.py @@ -12,7 +12,7 @@ class ValidateModelContent(pyblish.api.InstancePlugin): """ order = pyblish.api.ValidatorOrder - families = ["model", "usdmodel"] + families = ["model"] hosts = ["max"] label = "Model Contents" From 1511ddbccf7f89f5ce90d934536d8a5d1b0eeb71 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 6 Mar 2023 16:22:17 +0800 Subject: [PATCH 065/360] usdmodel extractor with selected node and remove usdmodel family --- .../max/plugins/create/create_model_usd.py | 21 ------------------- .../hosts/max/plugins/load/load_model_usd.py | 2 +- .../max/plugins/publish/extract_model_usd.py | 3 ++- openpype/plugins/publish/integrate.py | 1 - openpype/plugins/publish/integrate_legacy.py | 1 - 5 files changed, 3 insertions(+), 25 deletions(-) delete mode 100644 openpype/hosts/max/plugins/create/create_model_usd.py diff --git a/openpype/hosts/max/plugins/create/create_model_usd.py b/openpype/hosts/max/plugins/create/create_model_usd.py deleted file mode 100644 index 21407ae1f3..0000000000 --- a/openpype/hosts/max/plugins/create/create_model_usd.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- -"""Creator plugin for model exported in USD format.""" -from openpype.hosts.max.api import plugin -from openpype.pipeline import CreatedInstance - - -class CreateUSDModel(plugin.MaxCreator): - identifier = "io.openpype.creators.max.usdmodel" - label = "USD Model" - family = "usdmodel" - icon = "gear" - - def create(self, subset_name, instance_data, pre_create_data): - _ = super(CreateUSDModel, self).create( - subset_name, - instance_data, - pre_create_data) # type: CreatedInstance - # TODO: Disable "Add to Containers?" Panel - # parent the selected cameras into the container - # for additional work on the node: - # instance_node = rt.getNodeByName(instance.get("instance_node")) diff --git a/openpype/hosts/max/plugins/load/load_model_usd.py b/openpype/hosts/max/plugins/load/load_model_usd.py index ac318fbb57..c6c414b91c 100644 --- a/openpype/hosts/max/plugins/load/load_model_usd.py +++ b/openpype/hosts/max/plugins/load/load_model_usd.py @@ -9,7 +9,7 @@ from openpype.hosts.max.api import lib class ModelUSDLoader(load.LoaderPlugin): """Loading model with the USD loader.""" - families = ["usdmodel"] + families = ["model"] label = "Load Model(USD)" representations = ["usda"] order = -10 diff --git a/openpype/hosts/max/plugins/publish/extract_model_usd.py b/openpype/hosts/max/plugins/publish/extract_model_usd.py index e0ad3bb23e..0bed2d855e 100644 --- a/openpype/hosts/max/plugins/publish/extract_model_usd.py +++ b/openpype/hosts/max/plugins/publish/extract_model_usd.py @@ -19,7 +19,7 @@ class ExtractModelUSD(publish.Extractor, order = pyblish.api.ExtractorOrder - 0.05 label = "Extract Geometry (USD)" hosts = ["max"] - families = ["usdmodel"] + families = ["model"] optional = True def process(self, instance): @@ -50,6 +50,7 @@ class ExtractModelUSD(publish.Extractor, node_list = self.get_node_list(container) rt.USDExporter.ExportFile(asset_filepath, exportOptions=export_options, + contentSource=rt.name("selected"), nodeList=node_list) self.log.info("Performing Extraction ...") diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index fc098b416a..b117006871 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -124,7 +124,6 @@ class IntegrateAsset(pyblish.api.InstancePlugin): "xgen", "hda", "usd", - "usdmodel", "staticMesh", "skeletalMesh", "mvLook", diff --git a/openpype/plugins/publish/integrate_legacy.py b/openpype/plugins/publish/integrate_legacy.py index ba32c376d8..b93abab1d8 100644 --- a/openpype/plugins/publish/integrate_legacy.py +++ b/openpype/plugins/publish/integrate_legacy.py @@ -120,7 +120,6 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "xgen", "hda", "usd", - "usdmodel", "staticMesh", "skeletalMesh", "mvLook", From d8efd09797467cf1464d06c36b654f8ec3e02b17 Mon Sep 17 00:00:00 2001 From: moonyuet Date: Thu, 9 Mar 2023 07:03:53 +0100 Subject: [PATCH 066/360] update the mesh format to poly mesh --- openpype/hosts/max/plugins/publish/extract_model_usd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/extract_model_usd.py b/openpype/hosts/max/plugins/publish/extract_model_usd.py index 0bed2d855e..f70a14ba0b 100644 --- a/openpype/hosts/max/plugins/publish/extract_model_usd.py +++ b/openpype/hosts/max/plugins/publish/extract_model_usd.py @@ -101,7 +101,7 @@ class ExtractModelUSD(publish.Extractor, export_options.Lights = False export_options.Cameras = False export_options.Materials = False - export_options.MeshFormat = rt.name('fromScene') + export_options.MeshFormat = rt.name('polyMesh') export_options.FileFormat = rt.name('ascii') export_options.UpAxis = rt.name('y') export_options.LogLevel = rt.name('info') From 861d60ca0cd3144e75e8ddb135ce071d0b1b65ae Mon Sep 17 00:00:00 2001 From: moonyuet Date: Mon, 13 Mar 2023 11:31:42 +0100 Subject: [PATCH 067/360] fbx obj extractors and oaders --- .../hosts/max/plugins/load/load_camera_fbx.py | 2 - .../hosts/max/plugins/load/load_model_fbx.py | 62 ++++++++++++++++ .../hosts/max/plugins/load/load_model_obj.py | 56 ++++++++++++++ .../max/plugins/publish/extract_model_fbx.py | 74 +++++++++++++++++++ .../max/plugins/publish/extract_model_obj.py | 59 +++++++++++++++ 5 files changed, 251 insertions(+), 2 deletions(-) create mode 100644 openpype/hosts/max/plugins/load/load_model_fbx.py create mode 100644 openpype/hosts/max/plugins/load/load_model_obj.py create mode 100644 openpype/hosts/max/plugins/publish/extract_model_fbx.py create mode 100644 openpype/hosts/max/plugins/publish/extract_model_obj.py diff --git a/openpype/hosts/max/plugins/load/load_camera_fbx.py b/openpype/hosts/max/plugins/load/load_camera_fbx.py index 3a6947798e..205e815dc8 100644 --- a/openpype/hosts/max/plugins/load/load_camera_fbx.py +++ b/openpype/hosts/max/plugins/load/load_camera_fbx.py @@ -36,8 +36,6 @@ importFile @"{filepath}" #noPrompt using:FBXIMP self.log.debug(f"Executing command: {fbx_import_cmd}") rt.execute(fbx_import_cmd) - container_name = f"{name}_CON" - asset = rt.getNodeByName(f"{name}") return containerise( diff --git a/openpype/hosts/max/plugins/load/load_model_fbx.py b/openpype/hosts/max/plugins/load/load_model_fbx.py new file mode 100644 index 0000000000..38b8555d28 --- /dev/null +++ b/openpype/hosts/max/plugins/load/load_model_fbx.py @@ -0,0 +1,62 @@ +import os +from openpype.pipeline import ( + load, + get_representation_path +) +from openpype.hosts.max.api.pipeline import containerise +from openpype.hosts.max.api import lib + + +class FbxModelLoader(load.LoaderPlugin): + """Fbx Model Loader""" + + families = ["model"] + representations = ["fbx"] + order = -9 + icon = "code-fork" + color = "white" + + def load(self, context, name=None, namespace=None, data=None): + from pymxs import runtime as rt + + filepath = os.path.normpath(self.fname) + + fbx_import_cmd = ( + f""" + +FBXImporterSetParam "Animation" false +FBXImporterSetParam "Cameras" false +FBXImporterSetParam "AxisConversionMethod" true +FbxExporterSetParam "UpAxis" "Y" +FbxExporterSetParam "Preserveinstances" true + +importFile @"{filepath}" #noPrompt using:FBXIMP + """) + + self.log.debug(f"Executing command: {fbx_import_cmd}") + rt.execute(fbx_import_cmd) + + asset = rt.getNodeByName(f"{name}") + + return containerise( + name, [asset], context, loader=self.__class__.__name__) + + def update(self, container, representation): + from pymxs import runtime as rt + + path = get_representation_path(representation) + node = rt.getNodeByName(container["instance_node"]) + + fbx_objects = self.get_container_children(node) + for fbx_object in fbx_objects: + fbx_object.source = path + + lib.imprint(container["instance_node"], { + "representation": str(representation["_id"]) + }) + + def remove(self, container): + from pymxs import runtime as rt + + node = rt.getNodeByName(container["instance_node"]) + rt.delete(node) diff --git a/openpype/hosts/max/plugins/load/load_model_obj.py b/openpype/hosts/max/plugins/load/load_model_obj.py new file mode 100644 index 0000000000..06b411cb5c --- /dev/null +++ b/openpype/hosts/max/plugins/load/load_model_obj.py @@ -0,0 +1,56 @@ +import os +from openpype.pipeline import ( + load, + get_representation_path +) +from openpype.hosts.max.api.pipeline import containerise +from openpype.hosts.max.api import lib + + +class ObjLoader(load.LoaderPlugin): + """Obj Loader""" + + families = ["model"] + representations = ["obj"] + order = -9 + icon = "code-fork" + color = "white" + + def load(self, context, name=None, namespace=None, data=None): + from pymxs import runtime as rt + + filepath = os.path.normpath(self.fname) + self.log.debug(f"Executing command to import..") + + rt.execute(f'importFile @"{filepath}" #noPrompt using:ObjImp') + # get current selection + for selection in rt.getCurrentSelection(): + # create "missing" container for obj import + container = rt.container() + container.name = f"{name}" + selection.Parent = container + + asset = rt.getNodeByName(f"{name}") + + return containerise( + name, [asset], context, loader=self.__class__.__name__) + + def update(self, container, representation): + from pymxs import runtime as rt + + path = get_representation_path(representation) + node = rt.getNodeByName(container["instance_node"]) + + objects = self.get_container_children(node) + for obj in objects: + obj.source = path + + lib.imprint(container["instance_node"], { + "representation": str(representation["_id"]) + }) + + def remove(self, container): + from pymxs import runtime as rt + + node = rt.getNodeByName(container["instance_node"]) + rt.delete(node) diff --git a/openpype/hosts/max/plugins/publish/extract_model_fbx.py b/openpype/hosts/max/plugins/publish/extract_model_fbx.py new file mode 100644 index 0000000000..ce58e8cc17 --- /dev/null +++ b/openpype/hosts/max/plugins/publish/extract_model_fbx.py @@ -0,0 +1,74 @@ +import os +import pyblish.api +from openpype.pipeline import ( + publish, + OptionalPyblishPluginMixin +) +from pymxs import runtime as rt +from openpype.hosts.max.api import ( + maintained_selection, + get_all_children +) + + +class ExtractModelFbx(publish.Extractor, + OptionalPyblishPluginMixin): + """ + Extract Geometry in FBX Format + """ + + order = pyblish.api.ExtractorOrder - 0.05 + label = "Extract FBX" + hosts = ["max"] + families = ["model"] + optional = True + + def process(self, instance): + if not self.is_active(instance.data): + return + + container = instance.data["instance_node"] + + self.log.info("Extracting Geometry ...") + + stagingdir = self.staging_dir(instance) + filename = "{name}.fbx".format(**instance.data) + filepath = os.path.join(stagingdir, + filename) + self.log.info("Writing FBX '%s' to '%s'" % (filepath, + stagingdir)) + + export_fbx_cmd = ( + f""" +FBXExporterSetParam "Animation" false +FBXExporterSetParam "Cameras" false +FBXExporterSetParam "Lights" false +FBXExporterSetParam "PointCache" false +FBXExporterSetParam "AxisConversionMethod" "Animation" +FbxExporterSetParam "UpAxis" "Y" +FbxExporterSetParam "Preserveinstances" true + +exportFile @"{filepath}" #noPrompt selectedOnly:true using:FBXEXP + + """) + + self.log.debug(f"Executing command: {export_fbx_cmd}") + + with maintained_selection(): + # select and export + rt.select(get_all_children(rt.getNodeByName(container))) + rt.execute(export_fbx_cmd) + + self.log.info("Performing Extraction ...") + if "representations" not in instance.data: + instance.data["representations"] = [] + + representation = { + 'name': 'fbx', + 'ext': 'fbx', + 'files': filename, + "stagingDir": stagingdir, + } + instance.data["representations"].append(representation) + self.log.info("Extracted instance '%s' to: %s" % (instance.name, + filepath)) diff --git a/openpype/hosts/max/plugins/publish/extract_model_obj.py b/openpype/hosts/max/plugins/publish/extract_model_obj.py new file mode 100644 index 0000000000..298e19151d --- /dev/null +++ b/openpype/hosts/max/plugins/publish/extract_model_obj.py @@ -0,0 +1,59 @@ +import os +import pyblish.api +from openpype.pipeline import ( + publish, + OptionalPyblishPluginMixin +) +from pymxs import runtime as rt +from openpype.hosts.max.api import ( + maintained_selection, + get_all_children +) + + +class ExtractModelObj(publish.Extractor, + OptionalPyblishPluginMixin): + """ + Extract Geometry in OBJ Format + """ + + order = pyblish.api.ExtractorOrder - 0.05 + label = "Extract OBJ" + hosts = ["max"] + families = ["model"] + optional = True + + def process(self, instance): + if not self.is_active(instance.data): + return + + container = instance.data["instance_node"] + + self.log.info("Extracting Geometry ...") + + stagingdir = self.staging_dir(instance) + filename = "{name}.obj".format(**instance.data) + filepath = os.path.join(stagingdir, + filename) + self.log.info("Writing OBJ '%s' to '%s'" % (filepath, + stagingdir)) + + with maintained_selection(): + # select and export + rt.select(get_all_children(rt.getNodeByName(container))) + rt.execute(f'exportFile @"{filepath}" #noPrompt selectedOnly:true using:ObjExp') + + self.log.info("Performing Extraction ...") + if "representations" not in instance.data: + instance.data["representations"] = [] + + representation = { + 'name': 'obj', + 'ext': 'obj', + 'files': filename, + "stagingDir": stagingdir, + } + + instance.data["representations"].append(representation) + self.log.info("Extracted instance '%s' to: %s" % (instance.name, + filepath)) From 64e8ff68b54d420c142c9276674e6cac74646ce0 Mon Sep 17 00:00:00 2001 From: moonyuet Date: Mon, 13 Mar 2023 11:32:47 +0100 Subject: [PATCH 068/360] cosmetic issue fixed --- openpype/hosts/max/plugins/publish/extract_model_obj.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/extract_model_obj.py b/openpype/hosts/max/plugins/publish/extract_model_obj.py index 298e19151d..7bda237880 100644 --- a/openpype/hosts/max/plugins/publish/extract_model_obj.py +++ b/openpype/hosts/max/plugins/publish/extract_model_obj.py @@ -41,7 +41,7 @@ class ExtractModelObj(publish.Extractor, with maintained_selection(): # select and export rt.select(get_all_children(rt.getNodeByName(container))) - rt.execute(f'exportFile @"{filepath}" #noPrompt selectedOnly:true using:ObjExp') + rt.execute(f'exportFile @"{filepath}" #noPrompt selectedOnly:true using:ObjExp') # noqa self.log.info("Performing Extraction ...") if "representations" not in instance.data: From 55a10a87932130828eeca112f7098e4a4cf5a24f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 13 Mar 2023 22:55:00 +0100 Subject: [PATCH 069/360] Use new style `ColormanagedPyblishPluginMixin` --- .../hosts/substancepainter/plugins/publish/extract_textures.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/substancepainter/plugins/publish/extract_textures.py b/openpype/hosts/substancepainter/plugins/publish/extract_textures.py index e66ce6dbf6..469f8501f7 100644 --- a/openpype/hosts/substancepainter/plugins/publish/extract_textures.py +++ b/openpype/hosts/substancepainter/plugins/publish/extract_textures.py @@ -2,7 +2,8 @@ from openpype.pipeline import KnownPublishError, publish import substance_painter.export -class ExtractTextures(publish.ExtractorColormanaged): +class ExtractTextures(publish.Extractor, + publish.ColormanagedPyblishPluginMixin): """Extract Textures using an output template config. Note: From 7714ebc990730053afa3424bb297c890548a457a Mon Sep 17 00:00:00 2001 From: Joseff Date: Thu, 16 Mar 2023 20:25:00 +0100 Subject: [PATCH 070/360] Renaming the plugin to Ayon. --- .../OpenPype/Config/DefaultAyonSettings.ini | 2 + .../UE_4.7/OpenPype/OpenPype.uplugin | 5 + .../UE_4.7/OpenPype/Resources/ayon128.png | Bin 0 -> 2358 bytes .../UE_4.7/OpenPype/Resources/ayon40.png | Bin 0 -> 721 bytes .../UE_4.7/OpenPype/Resources/ayon512.png | Bin 0 -> 16705 bytes .../UE_4.7/OpenPype/Source/Ayon/Ayon.Build.cs | 61 ++++++ .../OpenPype/Source/Ayon/Private/Ayon.cpp | 156 ++++++++++++++ .../Ayon/Private/AyonAssetContainer.cpp | 115 ++++++++++ .../Private/AyonAssetContainerFactory.cpp | 20 ++ .../OpenPype/Source/Ayon/Private/AyonLib.cpp | 53 +++++ .../Ayon/Private/AyonPublishInstance.cpp | 201 ++++++++++++++++++ .../Private/AyonPublishInstanceFactory.cpp | 21 ++ .../Source/Ayon/Private/AyonPythonBridge.cpp | 14 ++ .../Source/Ayon/Private/AyonSettings.cpp | 20 ++ .../Source/Ayon/Private/AyonStyle.cpp | 70 ++++++ .../Private/Commandlets/AyonActionResult.cpp | 41 ++++ .../AyonGenerateProjectCommandlet.cpp | 141 ++++++++++++ .../UE_4.7/OpenPype/Source/Ayon/Public/Ayon.h | 22 ++ .../Source/Ayon/Public/AyonAssetContainer.h | 39 ++++ .../Ayon/Public/AyonAssetContainerFactory.h | 21 ++ .../Source/Ayon/Public/AyonConstants.h | 15 ++ .../OpenPype/Source/Ayon/Public/AyonLib.h | 20 ++ .../Source/Ayon/Public/AyonPublishInstance.h | 102 +++++++++ .../Ayon/Public/AyonPublishInstanceFactory.h | 20 ++ .../Source/Ayon/Public/AyonPythonBridge.h | 21 ++ .../Source/Ayon/Public/AyonSettings.h | 31 +++ .../OpenPype/Source/Ayon/Public/AyonStyle.h | 23 ++ .../Public/Commandlets/AyonActionResult.h | 83 ++++++++ .../AyonGenerateProjectCommandlet.h | 60 ++++++ .../Source/Ayon/Public/Logging/Ayon_Log.h | 4 + .../OpenPype/Private/AssetContainer.cpp | 115 ++++++++++ .../Private/AssetContainerFactory.cpp | 20 ++ .../Source/OpenPype/Public/AssetContainer.h | 39 ++++ .../OpenPype/Public/AssetContainerFactory.h | 21 ++ .../OpenPype/Config/DefaultAyonSettings.ini | 2 + .../UE_5.0/OpenPype/OpenPype.uplugin | 5 + .../UE_5.0/OpenPype/Resources/ayon128.png | Bin 0 -> 2358 bytes .../UE_5.0/OpenPype/Resources/ayon40.png | Bin 0 -> 721 bytes .../UE_5.0/OpenPype/Resources/ayon512.png | Bin 0 -> 16705 bytes .../UE_5.0/OpenPype/Source/Ayon/Ayon.Build.cs | 65 ++++++ .../OpenPype/Source/Ayon/Private/Ayon.cpp | 139 ++++++++++++ .../Ayon/Private/AyonAssetContainer.cpp | 113 ++++++++++ .../Private/AyonAssetContainerFactory.cpp | 20 ++ .../Source/Ayon/Private/AyonCommands.cpp | 13 ++ .../OpenPype/Source/Ayon/Private/AyonLib.cpp | 51 +++++ .../Ayon/Private/AyonPublishInstance.cpp | 201 ++++++++++++++++++ .../Private/AyonPublishInstanceFactory.cpp | 21 ++ .../Source/Ayon/Private/AyonPythonBridge.cpp | 14 ++ .../Source/Ayon/Private/AyonSettings.cpp | 21 ++ .../Source/Ayon/Private/AyonStyle.cpp | 62 ++++++ .../Private/Commandlets/AyonActionResult.cpp | 40 ++++ .../AyonGenerateProjectCommandlet.cpp | 140 ++++++++++++ .../UE_5.0/OpenPype/Source/Ayon/Public/Ayon.h | 24 +++ .../Source/Ayon/Public/AyonAssetContainer.h | 34 +++ .../Ayon/Public/AyonAssetContainerFactory.h | 18 ++ .../Source/Ayon/Public/AyonCommands.h | 24 +++ .../Source/Ayon/Public/AyonConstants.h | 13 ++ .../OpenPype/Source/Ayon/Public/AyonLib.h | 19 ++ .../Source/Ayon/Public/AyonPublishInstance.h | 102 +++++++++ .../Ayon/Public/AyonPublishInstanceFactory.h | 20 ++ .../Source/Ayon/Public/AyonPythonBridge.h | 20 ++ .../Source/Ayon/Public/AyonSettings.h | 32 +++ .../OpenPype/Source/Ayon/Public/AyonStyle.h | 19 ++ .../Public/Commandlets/AyonActionResult.h | 83 ++++++++ .../AyonGenerateProjectCommandlet.h | 61 ++++++ .../Source/Ayon/Public/Logging/Ayon_Log.h | 4 + 66 files changed, 2956 insertions(+) create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Config/DefaultAyonSettings.ini create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Resources/ayon128.png create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Resources/ayon40.png create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Resources/ayon512.png create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Ayon.Build.cs create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/Ayon.cpp create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonAssetContainer.cpp create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonAssetContainerFactory.cpp create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonLib.cpp create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonPublishInstance.cpp create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonPublishInstanceFactory.cpp create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonPythonBridge.cpp create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonSettings.cpp create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonStyle.cpp create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/Commandlets/AyonActionResult.cpp create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/Ayon.h create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonAssetContainer.h create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonAssetContainerFactory.h create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonConstants.h create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonLib.h create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonPublishInstance.h create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonPublishInstanceFactory.h create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonPythonBridge.h create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonSettings.h create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonStyle.h create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/Commandlets/AyonActionResult.h create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/Logging/Ayon_Log.h create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/AssetContainer.cpp create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/AssetContainerFactory.cpp create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/AssetContainer.h create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/AssetContainerFactory.h create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Config/DefaultAyonSettings.ini create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Resources/ayon128.png create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Resources/ayon40.png create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Resources/ayon512.png create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Ayon.Build.cs create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/Ayon.cpp create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonAssetContainer.cpp create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonAssetContainerFactory.cpp create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonCommands.cpp create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonLib.cpp create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonPublishInstance.cpp create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonPublishInstanceFactory.cpp create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonPythonBridge.cpp create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonSettings.cpp create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonStyle.cpp create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/Commandlets/AyonActionResult.cpp create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/Ayon.h create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonAssetContainer.h create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonAssetContainerFactory.h create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonCommands.h create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonConstants.h create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonLib.h create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonPublishInstance.h create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonPublishInstanceFactory.h create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonPythonBridge.h create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonSettings.h create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonStyle.h create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/Commandlets/AyonActionResult.h create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/Logging/Ayon_Log.h diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Config/DefaultAyonSettings.ini b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Config/DefaultAyonSettings.ini new file mode 100644 index 0000000000..9ad7f55201 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Config/DefaultAyonSettings.ini @@ -0,0 +1,2 @@ +[/Script/Ayon.AyonSettings] +FolderColor=(R=91,G=197,B=220,A=255) \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/OpenPype.uplugin b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/OpenPype.uplugin index b2cbe3cff3..37bb170eb4 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/OpenPype.uplugin +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/OpenPype.uplugin @@ -18,6 +18,11 @@ "Name": "OpenPype", "Type": "Editor", "LoadingPhase": "Default" + }, + { + "Name": "Ayon", + "Type": "Editor", + "LoadingPhase": "Default" } ] } \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Resources/ayon128.png b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Resources/ayon128.png new file mode 100644 index 0000000000000000000000000000000000000000..799d849aa3163ecb16be39c641a6ac30324906b9 GIT binary patch literal 2358 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 D;gw7~ literal 0 HcmV?d00001 diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Resources/ayon512.png b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Resources/ayon512.png new file mode 100644 index 0000000000000000000000000000000000000000..990d5917e232a0644820428fb2790943de5ffaa4 GIT binary patch literal 16705 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"LevelEditor"); + + TSharedPtr MenuExtender = MakeShareable(new FExtender()); + TSharedPtr ToolbarExtender = MakeShareable(new FExtender()); + + MenuExtender->AddMenuExtension( + "LevelEditor", + EExtensionHook::After, + NULL, + FMenuExtensionDelegate::CreateRaw(this, &FAyonModule::AddMenuEntry) + ); + ToolbarExtender->AddToolBarExtension( + "Settings", + EExtensionHook::After, + NULL, + FToolBarExtensionDelegate::CreateRaw(this, &FAyonModule::AddToobarEntry)); + + + LevelEditorModule.GetMenuExtensibilityManager()->AddExtender(MenuExtender); + LevelEditorModule.GetToolBarExtensibilityManager()->AddExtender(ToolbarExtender); + + RegisterSettings(); + } +} + +void FAyonModule::ShutdownModule() +{ + FAyonStyle::Shutdown(); +} + + +void FAyonModule::AddMenuEntry(FMenuBuilder& MenuBuilder) +{ + // Create Section + MenuBuilder.BeginSection("Ayon", TAttribute(FText::FromString("Ayon"))); + { + // Create a Submenu inside of the Section + MenuBuilder.AddMenuEntry( + FText::FromString("Tools..."), + FText::FromString("Pipeline tools"), + FSlateIcon(FAyonStyle::GetStyleSetName(), "Ayon.Logo"), + FUIAction(FExecuteAction::CreateRaw(this, &FAyonModule::MenuPopup)) + ); + + MenuBuilder.AddMenuEntry( + FText::FromString("Tools dialog..."), + FText::FromString("Pipeline tools dialog"), + FSlateIcon(FAyonStyle::GetStyleSetName(), "Ayon.Logo"), + FUIAction(FExecuteAction::CreateRaw(this, &FAyonModule::MenuDialog)) + ); + } + MenuBuilder.EndSection(); +} + +void FAyonModule::AddToobarEntry(FToolBarBuilder& ToolbarBuilder) +{ + ToolbarBuilder.BeginSection(TEXT("Ayon")); + { + ToolbarBuilder.AddToolBarButton( + FUIAction( + FExecuteAction::CreateRaw(this, &FAyonModule::MenuPopup), + NULL, + FIsActionChecked() + + ), + NAME_None, + LOCTEXT("Ayon_label", "Ayon"), + LOCTEXT("Ayon_tooltip", "Ayon Tools"), + FSlateIcon(FAyonStyle::GetStyleSetName(), "Ayon.Logo") + ); + } + ToolbarBuilder.EndSection(); +} + +void FAyonModule::RegisterSettings() +{ + ISettingsModule& SettingsModule = FModuleManager::LoadModuleChecked("Settings"); + + // Create the new category + // TODO: After the movement of the plugin from the game to editor, it might be necessary to move this! + ISettingsContainerPtr SettingsContainer = SettingsModule.GetContainer("Project"); + + UAyonSettings* Settings = GetMutableDefault(); + + // Register the settings + ISettingsSectionPtr SettingsSection = SettingsModule.RegisterSettings("Project", "Ayon", "General", + LOCTEXT("RuntimeGeneralSettingsName", + "General"), + LOCTEXT("RuntimeGeneralSettingsDescription", + "Base configuration for Open Pype Module"), + Settings + ); + + // Register the save handler to your settings, you might want to use it to + // validate those or just act to settings changes. + if (SettingsSection.IsValid()) + { + SettingsSection->OnModified().BindRaw(this, &FAyonModule::HandleSettingsSaved); + } +} + +bool FAyonModule::HandleSettingsSaved() +{ + UAyonSettings* Settings = GetMutableDefault(); + bool ResaveSettings = false; + + // You can put any validation code in here and resave the settings in case an invalid + // value has been entered + + if (ResaveSettings) + { + Settings->SaveConfig(); + } + + return true; +} + + +void FAyonModule::MenuPopup() +{ + UAyonPythonBridge* bridge = UAyonPythonBridge::Get(); + bridge->RunInPython_Popup(); +} + +void FAyonModule::MenuDialog() +{ + UAyonPythonBridge* bridge = UAyonPythonBridge::Get(); + bridge->RunInPython_Dialog(); +} + +IMPLEMENT_MODULE(FAyonModule, Ayon) diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonAssetContainer.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonAssetContainer.cpp new file mode 100644 index 0000000000..316c4015af --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonAssetContainer.cpp @@ -0,0 +1,115 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#include "AyonAssetContainer.h" +#include "AssetRegistryModule.h" +#include "Misc/PackageName.h" +#include "Engine.h" +#include "Containers/UnrealString.h" + +UAyonAssetContainer::UAyonAssetContainer(const FObjectInitializer& ObjectInitializer) +: UAssetUserData(ObjectInitializer) +{ + FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); + FString path = UAyonAssetContainer::GetPathName(); + UE_LOG(LogTemp, Warning, TEXT("UAyonAssetContainer %s"), *path); + FARFilter Filter; + Filter.PackagePaths.Add(FName(*path)); + + AssetRegistryModule.Get().OnAssetAdded().AddUObject(this, &UAyonAssetContainer::OnAssetAdded); + AssetRegistryModule.Get().OnAssetRemoved().AddUObject(this, &UAyonAssetContainer::OnAssetRemoved); + AssetRegistryModule.Get().OnAssetRenamed().AddUObject(this, &UAyonAssetContainer::OnAssetRenamed); +} + +void UAyonAssetContainer::OnAssetAdded(const FAssetData& AssetData) +{ + TArray split; + + // get directory of current container + FString selfFullPath = UAyonAssetContainer::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 != "AyonAssetContainer") + { + assets.Add(assetPath); + assetsData.Add(AssetData); + UE_LOG(LogTemp, Log, TEXT("%s: asset added to %s"), *selfFullPath, *selfDir); + } + } +} + +void UAyonAssetContainer::OnAssetRemoved(const FAssetData& AssetData) +{ + TArray split; + + // get directory of current container + FString selfFullPath = UAyonAssetContainer::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 = UAyonAssetContainer::GetPathName(); + FString lpp = FPackageName::GetLongPackagePath(*path); + + if (assetDir.StartsWith(*selfDir)) + { + // exclude self + if (assetFName != "AyonAssetContainer") + { + // UE_LOG(LogTemp, Warning, TEXT("%s: asset removed"), *lpp); + assets.Remove(assetPath); + assetsData.Remove(AssetData); + } + } +} + +void UAyonAssetContainer::OnAssetRenamed(const FAssetData& AssetData, const FString& str) +{ + TArray split; + + // get directory of current container + FString selfFullPath = UAyonAssetContainer::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 != "AyonAssetContainer") + { + + 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_4.7/OpenPype/Source/Ayon/Private/AyonAssetContainerFactory.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonAssetContainerFactory.cpp new file mode 100644 index 0000000000..086fc1036e --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonAssetContainerFactory.cpp @@ -0,0 +1,20 @@ +#include "AyonAssetContainerFactory.h" +#include "AyonAssetContainer.h" + +UAyonAssetContainerFactory::UAyonAssetContainerFactory(const FObjectInitializer& ObjectInitializer) + : UFactory(ObjectInitializer) +{ + SupportedClass = UAyonAssetContainer::StaticClass(); + bCreateNew = false; + bEditorImport = true; +} + +UObject* UAyonAssetContainerFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) +{ + UAyonAssetContainer* AssetContainer = NewObject(InParent, Class, Name, Flags); + return AssetContainer; +} + +bool UAyonAssetContainerFactory::ShouldShowInNewMenu() const { + return false; +} diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonLib.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonLib.cpp new file mode 100644 index 0000000000..bff99caee3 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonLib.cpp @@ -0,0 +1,53 @@ +// Copyright 2023, Ayon, All rights reserved. +#include "AyonLib.h" + +#include "AssetViewUtils.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? + */ + +bool UAyonLib::SetFolderColor(const FString& FolderPath, const FLinearColor& FolderColor, const bool& bForceAdd) +{ + if (AssetViewUtils::DoesFolderExist(FolderPath)) + { + const TSharedPtr LinearColor = MakeShared(FolderColor); + + AssetViewUtils::SaveColor(FolderPath, LinearColor, true); + UE_LOG(LogAssetData, Display, TEXT("A color {%s} has been set to folder \"%s\""), *LinearColor->ToString(), + *FolderPath) + return true; + } + + UE_LOG(LogAssetData, Display, TEXT("Setting a color {%s} to folder \"%s\" has failed! Directory doesn't exist!"), + *FolderColor.ToString(), *FolderPath) + return false; +} + +/** + * Returns all poperties on given object + * @param cls - class + * @return TArray of properties + */ +TArray UAyonLib::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_4.7/OpenPype/Source/Ayon/Private/AyonPublishInstance.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonPublishInstance.cpp new file mode 100644 index 0000000000..424addd7bf --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonPublishInstance.cpp @@ -0,0 +1,201 @@ +// Copyright 2023, Ayon, All rights reserved. +#pragma once + +#include "AyonPublishInstance.h" +#include "AssetRegistryModule.h" +#include "AyonLib.h" +#include "AyonSettings.h" +#include "Framework/Notifications/NotificationManager.h" +#include "Widgets/Notifications/SNotificationList.h" + +//Moves all the invalid pointers to the end to prepare them for the shrinking +#define REMOVE_INVALID_ENTRIES(VAR) VAR.CompactStable(); \ + VAR.Shrink(); + +UAyonPublishInstance::UAyonPublishInstance(const FObjectInitializer& ObjectInitializer) + : UPrimaryDataAsset(ObjectInitializer) +{ + const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked< + FAssetRegistryModule>("AssetRegistry"); + + const FPropertyEditorModule& PropertyEditorModule = FModuleManager::LoadModuleChecked( + "PropertyEditor"); + + FString Left, Right; + GetPathName().Split("/" + GetName(), &Left, &Right); + + FARFilter Filter; + Filter.PackagePaths.Emplace(FName(Left)); + + TArray FoundAssets; + AssetRegistryModule.GetRegistry().GetAssets(Filter, FoundAssets); + + for (const FAssetData& AssetData : FoundAssets) + OnAssetCreated(AssetData); + + REMOVE_INVALID_ENTRIES(AssetDataInternal) + REMOVE_INVALID_ENTRIES(AssetDataExternal) + + AssetRegistryModule.Get().OnAssetAdded().AddUObject(this, &UAyonPublishInstance::OnAssetCreated); + AssetRegistryModule.Get().OnAssetRemoved().AddUObject(this, &UAyonPublishInstance::OnAssetRemoved); + AssetRegistryModule.Get().OnAssetUpdated().AddUObject(this, &UAyonPublishInstance::OnAssetUpdated); + +#ifdef WITH_EDITOR + ColorAyonDirs(); +#endif + +} + +void UAyonPublishInstance::OnAssetCreated(const FAssetData& InAssetData) +{ + TArray split; + + UObject* Asset = InAssetData.GetAsset(); + + if (!IsValid(Asset)) + { + UE_LOG(LogAssetData, Warning, TEXT("Asset \"%s\" is not valid! Skipping the addition."), + *InAssetData.ObjectPath.ToString()); + return; + } + + const bool result = IsUnderSameDir(Asset) && Cast(Asset) == nullptr; + + if (result) + { + if (AssetDataInternal.Emplace(Asset).IsValidId()) + { + UE_LOG(LogTemp, Log, TEXT("Added an Asset to PublishInstance - Publish Instance: %s, Asset %s"), + *this->GetName(), *Asset->GetName()); + } + } +} + +void UAyonPublishInstance::OnAssetRemoved(const FAssetData& InAssetData) +{ + if (Cast(InAssetData.GetAsset()) == nullptr) + { + if (AssetDataInternal.Contains(nullptr)) + { + AssetDataInternal.Remove(nullptr); + REMOVE_INVALID_ENTRIES(AssetDataInternal) + } + else + { + AssetDataExternal.Remove(nullptr); + REMOVE_INVALID_ENTRIES(AssetDataExternal) + } + } +} + +void UAyonPublishInstance::OnAssetUpdated(const FAssetData& InAssetData) +{ + REMOVE_INVALID_ENTRIES(AssetDataInternal); + REMOVE_INVALID_ENTRIES(AssetDataExternal); +} + +bool UAyonPublishInstance::IsUnderSameDir(const UObject* InAsset) const +{ + FString ThisLeft, ThisRight; + this->GetPathName().Split(this->GetName(), &ThisLeft, &ThisRight); + + return InAsset->GetPathName().StartsWith(ThisLeft); +} + +#ifdef WITH_EDITOR + +void UAyonPublishInstance::ColorAyonDirs() +{ + FString PathName = this->GetPathName(); + + //Check whether the path contains the defined Ayon folder + if (!PathName.Contains(TEXT("Ayon"))) return; + + //Get the base path for open pype + FString PathLeft, PathRight; + PathName.Split(FString("Ayon"), &PathLeft, &PathRight); + + if (PathLeft.IsEmpty() || PathRight.IsEmpty()) + { + UE_LOG(LogAssetData, Error, TEXT("Failed to retrieve the base Ayon directory!")) + return; + } + + PathName.RemoveFromEnd(PathRight, ESearchCase::CaseSensitive); + + //Get the current settings + const UAyonSettings* Settings = GetMutableDefault(); + + //Color the base folder + UAyonLib::SetFolderColor(PathName, Settings->GetFolderFColor(), false); + + //Get Sub paths, iterate through them and color them according to the folder color in UAyonSettings + const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked( + "AssetRegistry"); + + TArray PathList; + + AssetRegistryModule.Get().GetSubPaths(PathName, PathList, true); + + if (PathList.Num() > 0) + { + for (const FString& Path : PathList) + { + UAyonLib::SetFolderColor(Path, Settings->GetFolderFColor(), false); + } + } +} + +void UAyonPublishInstance::SendNotification(const FString& Text) const +{ + FNotificationInfo Info{FText::FromString(Text)}; + + Info.bFireAndForget = true; + Info.bUseLargeFont = false; + Info.bUseThrobber = false; + Info.bUseSuccessFailIcons = false; + Info.ExpireDuration = 4.f; + Info.FadeOutDuration = 2.f; + + FSlateNotificationManager::Get().AddNotification(Info); + + UE_LOG(LogAssetData, Warning, + TEXT( + "Removed duplicated asset from the AssetsDataExternal in Container \"%s\", Asset is already included in the AssetDataInternal!" + ), *GetName() + ) +} + + +void UAyonPublishInstance::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) +{ + Super::PostEditChangeProperty(PropertyChangedEvent); + + if (PropertyChangedEvent.ChangeType == EPropertyChangeType::ValueSet && + PropertyChangedEvent.Property->GetFName() == GET_MEMBER_NAME_CHECKED( + UAyonPublishInstance, AssetDataExternal)) + { + // Check for duplicated assets + for (const auto& Asset : AssetDataInternal) + { + if (AssetDataExternal.Contains(Asset)) + { + AssetDataExternal.Remove(Asset); + return SendNotification( + "You are not allowed to add assets into AssetDataExternal which are already included in AssetDataInternal!"); + } + } + + // Check if no UAyonPublishInstance type assets are included + for (const auto& Asset : AssetDataExternal) + { + if (Cast(Asset.Get()) != nullptr) + { + AssetDataExternal.Remove(Asset); + return SendNotification("You are not allowed to add publish instances!"); + } + } + } +} + +#endif diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonPublishInstanceFactory.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonPublishInstanceFactory.cpp new file mode 100644 index 0000000000..c54e789dca --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonPublishInstanceFactory.cpp @@ -0,0 +1,21 @@ +// Copyright 2023, Ayon, All rights reserved. +#include "AyonPublishInstanceFactory.h" +#include "AyonPublishInstance.h" + +UAyonPublishInstanceFactory::UAyonPublishInstanceFactory(const FObjectInitializer& ObjectInitializer) + : UFactory(ObjectInitializer) +{ + SupportedClass = UAyonPublishInstance::StaticClass(); + bCreateNew = false; + bEditorImport = true; +} + +UObject* UAyonPublishInstanceFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) +{ + check(InClass->IsChildOf(UAyonPublishInstance::StaticClass())); + return NewObject(InParent, InClass, InName, Flags); +} + +bool UAyonPublishInstanceFactory::ShouldShowInNewMenu() const { + return false; +} diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonPythonBridge.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonPythonBridge.cpp new file mode 100644 index 0000000000..0ed4b2f704 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonPythonBridge.cpp @@ -0,0 +1,14 @@ +// Copyright 2023, Ayon, All rights reserved. +#include "AyonPythonBridge.h" + +UAyonPythonBridge* UAyonPythonBridge::Get() +{ + TArray AyonPythonBridgeClasses; + GetDerivedClasses(UAyonPythonBridge::StaticClass(), AyonPythonBridgeClasses); + int32 NumClasses = AyonPythonBridgeClasses.Num(); + if (NumClasses > 0) + { + return Cast(AyonPythonBridgeClasses[NumClasses - 1]->GetDefaultObject()); + } + return nullptr; +}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonSettings.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonSettings.cpp new file mode 100644 index 0000000000..d91dc94db1 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonSettings.cpp @@ -0,0 +1,20 @@ +// Copyright 2023, Ayon, All rights reserved. + +#include "AyonSettings.h" + +#include "Interfaces/IPluginManager.h" + +/** + * Mainly is used for initializing default values if the DefaultAyonSettings.ini file does not exist in the saved config + */ +UAyonSettings::UAyonSettings(const FObjectInitializer& ObjectInitializer) +{ + + const FString ConfigFilePath = OPENPYPE_SETTINGS_FILEPATH; + + // This has to be probably in the future set using the UE Reflection system + FColor Color; + GConfig->GetColor(TEXT("/Script/Ayon.AyonSettings"), TEXT("FolderColor"), Color, ConfigFilePath); + + FolderColor = Color; +} \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonStyle.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonStyle.cpp new file mode 100644 index 0000000000..dc8f0f1f40 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonStyle.cpp @@ -0,0 +1,70 @@ +// Copyright 2023, Ayon, All rights reserved. +#include "AyonStyle.h" +#include "Framework/Application/SlateApplication.h" +#include "Styling/SlateStyle.h" +#include "Styling/SlateStyleRegistry.h" + + +TUniquePtr< FSlateStyleSet > FAyonStyle::AyonStyleInstance = nullptr; + +void FAyonStyle::Initialize() +{ + if (!AyonStyleInstance.IsValid()) + { + AyonStyleInstance = Create(); + FSlateStyleRegistry::RegisterSlateStyle(*AyonStyleInstance); + } +} + +void FAyonStyle::Shutdown() +{ + if (AyonStyleInstance.IsValid()) + { + FSlateStyleRegistry::UnRegisterSlateStyle(*AyonStyleInstance); + AyonStyleInstance.Reset(); + } +} + +FName FAyonStyle::GetStyleSetName() +{ + static FName StyleSetName(TEXT("AyonStyle")); + return StyleSetName; +} + +FName FAyonStyle::GetContextName() +{ + static FName ContextName(TEXT("Ayon")); + return ContextName; +} + +#define IMAGE_BRUSH(RelativePath, ...) FSlateImageBrush( Style->RootToContentDir( RelativePath, TEXT(".png") ), __VA_ARGS__ ) + +const FVector2D Icon40x40(40.0f, 40.0f); + +TUniquePtr< FSlateStyleSet > FAyonStyle::Create() +{ + TUniquePtr< FSlateStyleSet > Style = MakeUnique(GetStyleSetName()); + Style->SetContentRoot(FPaths::EnginePluginsDir() / TEXT("Marketplace/OpenPype/Resources")); + + return Style; +} + +void FAyonStyle::SetIcon(const FString& StyleName, const FString& ResourcePath) +{ + FSlateStyleSet* Style = AyonStyleInstance.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& FAyonStyle::Get() +{ + check(AyonStyleInstance); + return *AyonStyleInstance; +} diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/Commandlets/AyonActionResult.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/Commandlets/AyonActionResult.cpp new file mode 100644 index 0000000000..49376e8648 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/Commandlets/AyonActionResult.cpp @@ -0,0 +1,41 @@ +// Copyright 2023, Ayon, All rights reserved. + + +#include "Commandlets/AyonActionResult.h" +#include "Logging/Ayon_Log.h" + +EAyon_ActionResult::Type& FAyon_ActionResult::GetStatus() +{ + return Status; +} + +FText& FAyon_ActionResult::GetReason() +{ + return Reason; +} + +FAyon_ActionResult::FAyon_ActionResult():Status(EAyon_ActionResult::Type::Ok) +{ + +} + +FAyon_ActionResult::FAyon_ActionResult(const EAyon_ActionResult::Type& InEnum):Status(InEnum) +{ + TryLog(); +} + +FAyon_ActionResult::FAyon_ActionResult(const EAyon_ActionResult::Type& InEnum, const FText& InReason):Status(InEnum), Reason(InReason) +{ + TryLog(); +}; + +bool FAyon_ActionResult::IsProblem() const +{ + return Status != EAyon_ActionResult::Ok; +} + +void FAyon_ActionResult::TryLog() const +{ + if(IsProblem()) + UE_LOG(LogCommandletOPGenerateProject, Error, TEXT("%s"), *Reason.ToString()); +} diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp new file mode 100644 index 0000000000..0328d3b7e6 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp @@ -0,0 +1,141 @@ +// Copyright 2023, Ayon, All rights reserved. +#include "Commandlets/Implementations/AyonGenerateProjectCommandlet.h" + +#include "Editor.h" +#include "GameProjectUtils.h" +#include "AyonConstants.h" +#include "Commandlets/AyonActionResult.h" +#include "ProjectDescriptor.h" + +int32 UAyonGenerateProjectCommandlet::Main(const FString& CommandLineParams) +{ + //Parses command line parameters & creates structure FProjectInformation + const FAyonGenerateProjectParams ParsedParams = FAyonGenerateProjectParams(CommandLineParams); + ProjectInformation = ParsedParams.GenerateUEProjectInformation(); + + //Creates .uproject & other UE files + EVALUATE_AYON_ACTION_RESULT(TryCreateProject()); + + //Loads created .uproject + EVALUATE_AYON_ACTION_RESULT(TryLoadProjectDescriptor()); + + //Adds needed plugin to .uproject + AttachPluginsToProjectDescriptor(); + + //Saves .uproject + EVALUATE_AYON_ACTION_RESULT(TrySave()); + + //When we are here, there should not be problems in generating Unreal Project for Ayon + return 0; +} + + +FAyonGenerateProjectParams::FAyonGenerateProjectParams(): FAyonGenerateProjectParams("") +{ +} + +FAyonGenerateProjectParams::FAyonGenerateProjectParams(const FString& CommandLineParams): CommandLineParams( + CommandLineParams) +{ + UCommandlet::ParseCommandLine(*CommandLineParams, Tokens, Switches); +} + +FProjectInformation FAyonGenerateProjectParams::GenerateUEProjectInformation() const +{ + FProjectInformation ProjectInformation = FProjectInformation(); + ProjectInformation.ProjectFilename = GetProjectFileName(); + + ProjectInformation.bShouldGenerateCode = IsSwitchPresent("GenerateCode"); + + return ProjectInformation; +} + +FString FAyonGenerateProjectParams::TryGetToken(const int32 Index) const +{ + return Tokens.IsValidIndex(Index) ? Tokens[Index] : ""; +} + +FString FAyonGenerateProjectParams::GetProjectFileName() const +{ + return TryGetToken(0); +} + +bool FAyonGenerateProjectParams::IsSwitchPresent(const FString& Switch) const +{ + return INDEX_NONE != Switches.IndexOfByPredicate([&Switch](const FString& Item) -> bool + { + return Item.Equals(Switch); + } + ); +} + + +UAyonGenerateProjectCommandlet::UAyonGenerateProjectCommandlet() +{ + LogToConsole = true; +} + +FAyon_ActionResult UAyonGenerateProjectCommandlet::TryCreateProject() const +{ + FText FailReason; + FText FailLog; + TArray OutCreatedFiles; + + if (!GameProjectUtils::CreateProject(ProjectInformation, FailReason, FailLog, &OutCreatedFiles)) + return FAyon_ActionResult(EAyon_ActionResult::ProjectNotCreated, FailReason); + return FAyon_ActionResult(); +} + +FAyon_ActionResult UAyonGenerateProjectCommandlet::TryLoadProjectDescriptor() +{ + FText FailReason; + const bool bLoaded = ProjectDescriptor.Load(ProjectInformation.ProjectFilename, FailReason); + + return FAyon_ActionResult(bLoaded ? EAyon_ActionResult::Ok : EAyon_ActionResult::ProjectNotLoaded, FailReason); +} + +void UAyonGenerateProjectCommandlet::AttachPluginsToProjectDescriptor() +{ + FPluginReferenceDescriptor AyonPluginDescriptor; + AyonPluginDescriptor.bEnabled = true; + AyonPluginDescriptor.Name = AyonConstants::Ayon_PluginName; + ProjectDescriptor.Plugins.Add(AyonPluginDescriptor); + + FPluginReferenceDescriptor PythonPluginDescriptor; + PythonPluginDescriptor.bEnabled = true; + PythonPluginDescriptor.Name = AyonConstants::PythonScript_PluginName; + ProjectDescriptor.Plugins.Add(PythonPluginDescriptor); + + FPluginReferenceDescriptor SequencerScriptingPluginDescriptor; + SequencerScriptingPluginDescriptor.bEnabled = true; + SequencerScriptingPluginDescriptor.Name = AyonConstants::SequencerScripting_PluginName; + ProjectDescriptor.Plugins.Add(SequencerScriptingPluginDescriptor); + + FPluginReferenceDescriptor MovieRenderPipelinePluginDescriptor; + MovieRenderPipelinePluginDescriptor.bEnabled = true; + MovieRenderPipelinePluginDescriptor.Name = AyonConstants::MovieRenderPipeline_PluginName; + ProjectDescriptor.Plugins.Add(MovieRenderPipelinePluginDescriptor); + + FPluginReferenceDescriptor EditorScriptingPluginDescriptor; + EditorScriptingPluginDescriptor.bEnabled = true; + EditorScriptingPluginDescriptor.Name = AyonConstants::EditorScriptingUtils_PluginName; + ProjectDescriptor.Plugins.Add(EditorScriptingPluginDescriptor); +} + +FAyon_ActionResult UAyonGenerateProjectCommandlet::TrySave() +{ + FText FailReason; + const bool bSaved = ProjectDescriptor.Save(ProjectInformation.ProjectFilename, FailReason); + + return FAyon_ActionResult(bSaved ? EAyon_ActionResult::Ok : EAyon_ActionResult::ProjectNotSaved, FailReason); +} + +FAyonGenerateProjectParams UAyonGenerateProjectCommandlet::ParseParameters(const FString& Params) const +{ + FAyonGenerateProjectParams ParamsResult; + + TArray Tokens, Switches; + ParseCommandLine(*Params, Tokens, Switches); + + return ParamsResult; +} diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/Ayon.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/Ayon.h new file mode 100644 index 0000000000..9535ff4b13 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/Ayon.h @@ -0,0 +1,22 @@ +// Copyright 2023, Ayon, All rights reserved. + +#pragma once + +#include "Engine.h" + + +class FAyonModule : public IModuleInterface +{ +public: + virtual void StartupModule() override; + virtual void ShutdownModule() override; + +private: + void RegisterSettings(); + bool HandleSettingsSaved(); + + void AddMenuEntry(FMenuBuilder& MenuBuilder); + void AddToobarEntry(FToolBarBuilder& ToolbarBuilder); + void MenuPopup(); + void MenuDialog(); +}; diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonAssetContainer.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonAssetContainer.h new file mode 100644 index 0000000000..cc17b3960a --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonAssetContainer.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 "AyonAssetContainer.generated.h" + +/** + * + */ +UCLASS(Blueprintable) +class AYON_API UAyonAssetContainer : public UAssetUserData +{ + GENERATED_BODY() + +public: + + UAyonAssetContainer(const FObjectInitializer& ObjectInitalizer); + // ~UAyonAssetContainer(); + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Assets") + 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_4.7/OpenPype/Source/Ayon/Public/AyonAssetContainerFactory.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonAssetContainerFactory.h new file mode 100644 index 0000000000..7c35897911 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonAssetContainerFactory.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 "AyonAssetContainerFactory.generated.h" + +/** + * + */ +UCLASS() +class AYON_API UAyonAssetContainerFactory : public UFactory +{ + GENERATED_BODY() + +public: + UAyonAssetContainerFactory(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_4.7/OpenPype/Source/Ayon/Public/AyonConstants.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonConstants.h new file mode 100644 index 0000000000..6a02b5682f --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonConstants.h @@ -0,0 +1,15 @@ +// Copyright 2023, Ayon, All rights reserved. +#pragma once + +#include "CoreMinimal.h" + +namespace AyonConstants +{ + const FString Ayon_PluginName = "Ayon"; + const FString PythonScript_PluginName = "PythonScriptPlugin"; + const FString SequencerScripting_PluginName = "SequencerScripting"; + const FString MovieRenderPipeline_PluginName = "MovieRenderPipeline"; + const FString EditorScriptingUtils_PluginName = "EditorScriptingUtilities"; +} + + diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonLib.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonLib.h new file mode 100644 index 0000000000..ed657a735c --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonLib.h @@ -0,0 +1,20 @@ +// Copyright 2023, Ayon, All rights reserved. +#pragma once + +#include "Engine.h" +#include "AyonLib.generated.h" + + +UCLASS(Blueprintable) +class AYON_API UAyonLib : public UBlueprintFunctionLibrary +{ + + GENERATED_BODY() + +public: + UFUNCTION(BlueprintCallable, Category = Python) + static bool SetFolderColor(const FString& FolderPath, const FLinearColor& FolderColor,const 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_4.7/OpenPype/Source/Ayon/Public/AyonPublishInstance.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonPublishInstance.h new file mode 100644 index 0000000000..4eace68827 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonPublishInstance.h @@ -0,0 +1,102 @@ +// Copyright 2023, Ayon, All rights reserved. +#pragma once + +#include "Engine.h" +#include "AyonPublishInstance.generated.h" + + +UCLASS(Blueprintable) +class AYON_API UAyonPublishInstance : public UPrimaryDataAsset +{ + GENERATED_UCLASS_BODY() + +public: + /** + * Retrieves all the assets which are monitored by the Publish Instance (Monitors assets in the directory which is + * placed in) + * + * @return - Set of UObjects. Careful! They are returning raw pointers. Seems like an issue in UE5 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") + TSet GetInternalAssets() const + { + //For some reason it can only return Raw Pointers? Seems like an issue which they haven't fixed. + TSet ResultSet; + + for (const auto& Asset : AssetDataInternal) + ResultSet.Add(Asset.LoadSynchronous()); + + return ResultSet; + } + + /** + * Retrieves all the assets which have been added manually by the Publish Instance + * + * @return - TSet of assets (UObjects). Careful! They are returning raw pointers. Seems like an issue in UE5 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") + TSet GetExternalAssets() const + { + //For some reason it can only return Raw Pointers? Seems like an issue which they haven't fixed. + TSet ResultSet; + + for (const auto& Asset : AssetDataExternal) + ResultSet.Add(Asset.LoadSynchronous()); + + return ResultSet; + } + + /** + * Function for returning all the assets in the container combined. + * + * @return Returns all the internal and externally added assets into one set (TSet of UObjects). Careful! They are + * returning raw pointers. Seems like an issue in UE5 + * + * @attention If the bAddExternalAssets variable is false, external assets won't be included! + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") + TSet GetAllAssets() const + { + const TSet>& IteratedSet = bAddExternalAssets + ? AssetDataInternal.Union(AssetDataExternal) + : AssetDataInternal; + + //Create a new TSet only with raw pointers. + TSet ResultSet; + + for (auto& Asset : IteratedSet) + ResultSet.Add(Asset.LoadSynchronous()); + + return ResultSet; + } + +private: + UPROPERTY(VisibleAnywhere, Category="Assets") + TSet> AssetDataInternal; + + /** + * This property allows exposing the array to include other assets from any other directory than what it's currently + * monitoring. NOTE: that these assets have to be added manually! They are not automatically registered or added! + */ + UPROPERTY(EditAnywhere, Category = "Assets") + bool bAddExternalAssets = false; + + UPROPERTY(EditAnywhere, meta=(EditCondition="bAddExternalAssets"), Category="Assets") + TSet> AssetDataExternal; + + + void OnAssetCreated(const FAssetData& InAssetData); + void OnAssetRemoved(const FAssetData& InAssetData); + void OnAssetUpdated(const FAssetData& InAssetData); + + bool IsUnderSameDir(const UObject* InAsset) const; + +#ifdef WITH_EDITOR + + void ColorAyonDirs(); + + void SendNotification(const FString& Text) const; + virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; + +#endif +}; diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonPublishInstanceFactory.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonPublishInstanceFactory.h new file mode 100644 index 0000000000..443d618c9a --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonPublishInstanceFactory.h @@ -0,0 +1,20 @@ +// Copyright 2023, Ayon, All rights reserved. +#pragma once + +#include "CoreMinimal.h" +#include "Factories/Factory.h" +#include "AyonPublishInstanceFactory.generated.h" + +/** + * + */ +UCLASS() +class AYON_API UAyonPublishInstanceFactory : public UFactory +{ + GENERATED_BODY() + +public: + UAyonPublishInstanceFactory(const FObjectInitializer& ObjectInitializer); + virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; + virtual bool ShouldShowInNewMenu() const override; +}; diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonPythonBridge.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonPythonBridge.h new file mode 100644 index 0000000000..831ac022a5 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonPythonBridge.h @@ -0,0 +1,21 @@ +// Copyright 2023, Ayon, All rights reserved. +#pragma once +#include "Engine.h" +#include "AyonPythonBridge.generated.h" + +UCLASS(Blueprintable) +class UAyonPythonBridge : public UObject +{ + GENERATED_BODY() + +public: + UFUNCTION(BlueprintCallable, Category = Python) + static UAyonPythonBridge* Get(); + + UFUNCTION(BlueprintImplementableEvent, Category = Python) + void RunInPython_Popup() const; + + UFUNCTION(BlueprintImplementableEvent, Category = Python) + void RunInPython_Dialog() const; + +}; diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonSettings.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonSettings.h new file mode 100644 index 0000000000..f600cfbf9a --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonSettings.h @@ -0,0 +1,31 @@ +// Copyright 2023, Ayon, All rights reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "AyonSettings.generated.h" + +#define OPENPYPE_SETTINGS_FILEPATH IPluginManager::Get().FindPlugin("OpenPype")->GetBaseDir() / TEXT("Config") / TEXT("DefaultAyonSettings.ini") + +UCLASS(Config=AyonSettings, DefaultConfig) +class AYON_API UAyonSettings : public UObject +{ + GENERATED_UCLASS_BODY() + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = Settings) + FColor GetFolderFColor() const + { + return FolderColor; + } + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = Settings) + FLinearColor GetFolderFLinearColor() const + { + return FLinearColor(FolderColor); + } + +protected: + + UPROPERTY(config, EditAnywhere, Category = Folders) + FColor FolderColor = FColor(25,45,223); +}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonStyle.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonStyle.h new file mode 100644 index 0000000000..188e4a510c --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonStyle.h @@ -0,0 +1,23 @@ +// Copyright 2023, Ayon, All rights reserved. +#pragma once +#include "CoreMinimal.h" + +class FSlateStyleSet; +class ISlateStyle; + + +class FAyonStyle +{ +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 > AyonStyleInstance; +}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/Commandlets/AyonActionResult.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/Commandlets/AyonActionResult.h new file mode 100644 index 0000000000..4694055164 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/Commandlets/AyonActionResult.h @@ -0,0 +1,83 @@ +// Copyright 2023, Ayon, All rights reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "AyonActionResult.generated.h" + +/** + * @brief This macro returns error code when is problem or does nothing when there is no problem. + * @param ActionResult FAyon_ActionResult structure + */ +#define EVALUATE_AYON_ACTION_RESULT(ActionResult) \ + if(ActionResult.IsProblem()) \ + return ActionResult.GetStatus(); + +/** +* @brief This enum values are humanly readable mapping of error codes. +* Here should be all error codes to be possible find what went wrong. +* TODO: In the future should exists an web document where is mapped error code & what problem occured & how to repair it... +*/ +UENUM() +namespace EAyon_ActionResult +{ + enum Type + { + Ok, + ProjectNotCreated, + ProjectNotLoaded, + ProjectNotSaved, + //....Here insert another values + + //Do not remove! + //Usable for looping through enum values + __Last UMETA(Hidden) + }; +} + + +/** + * @brief This struct holds action result enum and optionally reason of fail + */ +USTRUCT() +struct FAyon_ActionResult +{ + GENERATED_BODY() + +public: + /** @brief Default constructor usable when there is no problem */ + FAyon_ActionResult(); + + /** + * @brief This constructor initializes variables & attempts to log when is error + * @param InEnum Status + */ + FAyon_ActionResult(const EAyon_ActionResult::Type& InEnum); + + /** + * @brief This constructor initializes variables & attempts to log when is error + * @param InEnum Status + * @param InReason Reason of potential fail + */ + FAyon_ActionResult(const EAyon_ActionResult::Type& InEnum, const FText& InReason); + +private: + /** @brief Action status */ + EAyon_ActionResult::Type Status; + + /** @brief Optional reason of fail */ + FText Reason; + +public: + /** + * @brief Checks if there is problematic state + * @return true when status is not equal to EAyon_ActionResult::Ok + */ + bool IsProblem() const; + EAyon_ActionResult::Type& GetStatus(); + FText& GetReason(); + +private: + void TryLog() const; +}; + diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h new file mode 100644 index 0000000000..cabd524b8c --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h @@ -0,0 +1,60 @@ +// Copyright 2023, Ayon, All rights reserved. +#pragma once + +#include "GameProjectUtils.h" +#include "Commandlets/AyonActionResult.h" +#include "ProjectDescriptor.h" +#include "Commandlets/Commandlet.h" +#include "AyonGenerateProjectCommandlet.generated.h" + +struct FProjectDescriptor; +struct FProjectInformation; + +/** +* @brief Structure which parses command line parameters and generates FProjectInformation +*/ +USTRUCT() +struct FAyonGenerateProjectParams +{ + GENERATED_BODY() + +private: + FString CommandLineParams; + TArray Tokens; + TArray Switches; + +public: + FAyonGenerateProjectParams(); + FAyonGenerateProjectParams(const FString& CommandLineParams); + + FProjectInformation GenerateUEProjectInformation() const; + +private: + FString TryGetToken(const int32 Index) const; + FString GetProjectFileName() const; + + bool IsSwitchPresent(const FString& Switch) const; +}; + +UCLASS() +class AYON_API UAyonGenerateProjectCommandlet : public UCommandlet +{ + GENERATED_BODY() + +private: + FProjectInformation ProjectInformation; + FProjectDescriptor ProjectDescriptor; + +public: + UAyonGenerateProjectCommandlet(); + + virtual int32 Main(const FString& CommandLineParams) override; + +private: + FAyonGenerateProjectParams ParseParameters(const FString& Params) const; + FAyon_ActionResult TryCreateProject() const; + FAyon_ActionResult TryLoadProjectDescriptor(); + void AttachPluginsToProjectDescriptor(); + FAyon_ActionResult TrySave(); +}; + diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/Logging/Ayon_Log.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/Logging/Ayon_Log.h new file mode 100644 index 0000000000..21571afd02 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/Logging/Ayon_Log.h @@ -0,0 +1,4 @@ +// Copyright 2023, Ayon, All rights reserved. +#pragma once + +DEFINE_LOG_CATEGORY_STATIC(LogCommandletOPGenerateProject, Log, All); diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/AssetContainer.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/AssetContainer.cpp new file mode 100644 index 0000000000..c766f87a8e --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/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_4.7/OpenPype/Source/OpenPype/Private/AssetContainerFactory.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/AssetContainerFactory.cpp new file mode 100644 index 0000000000..b943150bdd --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/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/UE_4.7/OpenPype/Source/OpenPype/Public/AssetContainer.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/AssetContainer.h new file mode 100644 index 0000000000..3b0230391c --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/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, Category="Assets") + 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_4.7/OpenPype/Source/OpenPype/Public/AssetContainerFactory.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/AssetContainerFactory.h new file mode 100644 index 0000000000..331ce6bb50 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/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/UE_5.0/OpenPype/Config/DefaultAyonSettings.ini b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Config/DefaultAyonSettings.ini new file mode 100644 index 0000000000..9ad7f55201 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Config/DefaultAyonSettings.ini @@ -0,0 +1,2 @@ +[/Script/Ayon.AyonSettings] +FolderColor=(R=91,G=197,B=220,A=255) \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/OpenPype.uplugin b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/OpenPype.uplugin index ff08edc13e..0fe7b249a8 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/OpenPype.uplugin +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/OpenPype.uplugin @@ -19,6 +19,11 @@ "Name": "OpenPype", "Type": "Editor", "LoadingPhase": "Default" + }, + { + "Name": "Ayon", + "Type": "Editor", + "LoadingPhase": "Default" } ] } \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Resources/ayon128.png b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Resources/ayon128.png new file mode 100644 index 0000000000000000000000000000000000000000..799d849aa3163ecb16be39c641a6ac30324906b9 GIT binary patch literal 2358 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 D;gw7~ literal 0 HcmV?d00001 diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Resources/ayon512.png b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Resources/ayon512.png new file mode 100644 index 0000000000000000000000000000000000000000..990d5917e232a0644820428fb2790943de5ffaa4 GIT binary patch literal 16705 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 + FAyonCommands::Get().AyonTools, + FExecuteAction::CreateRaw(this, &FAyonModule::MenuPopup), + FCanExecuteAction()); + PluginCommands->MapAction( + FAyonCommands::Get().AyonToolsDialog, + FExecuteAction::CreateRaw(this, &FAyonModule::MenuDialog), + FCanExecuteAction()); + + UToolMenus::RegisterStartupCallback( + FSimpleMulticastDelegate::FDelegate::CreateRaw(this, &FAyonModule::RegisterMenus)); + + RegisterSettings(); +} + +void FAyonModule::ShutdownModule() +{ + UToolMenus::UnRegisterStartupCallback(this); + + UToolMenus::UnregisterOwner(this); + + FAyonStyle::Shutdown(); + + FAyonCommands::Unregister(); +} + + +void FAyonModule::RegisterSettings() +{ + ISettingsModule& SettingsModule = FModuleManager::LoadModuleChecked("Settings"); + + // Create the new category + // TODO: After the movement of the plugin from the game to editor, it might be necessary to move this! + ISettingsContainerPtr SettingsContainer = SettingsModule.GetContainer("Project"); + + UAyonSettings* Settings = GetMutableDefault(); + + // Register the settings + ISettingsSectionPtr SettingsSection = SettingsModule.RegisterSettings("Project", "Ayon", "General", + LOCTEXT("RuntimeGeneralSettingsName", + "General"), + LOCTEXT("RuntimeGeneralSettingsDescription", + "Base configuration for Open Pype Module"), + Settings + ); + + // Register the save handler to your settings, you might want to use it to + // validate those or just act to settings changes. + if (SettingsSection.IsValid()) + { + SettingsSection->OnModified().BindRaw(this, &FAyonModule::HandleSettingsSaved); + } +} + +bool FAyonModule::HandleSettingsSaved() +{ + UAyonSettings* Settings = GetMutableDefault(); + bool ResaveSettings = false; + + // You can put any validation code in here and resave the settings in case an invalid + // value has been entered + + if (ResaveSettings) + { + Settings->SaveConfig(); + } + + return true; +} + +void FAyonModule::RegisterMenus() +{ + // Owner will be used for cleanup in call to UToolMenus::UnregisterOwner + FToolMenuOwnerScoped OwnerScoped(this); + + { + UToolMenu* Menu = UToolMenus::Get()->ExtendMenu("LevelEditor.MainMenu.Tools"); + { + // FToolMenuSection& Section = Menu->FindOrAddSection("Ayon"); + FToolMenuSection& Section = Menu->AddSection( + "Ayon", + TAttribute(FText::FromString("Ayon")), + FToolMenuInsert("Programming", EToolMenuInsertType::Before) + ); + Section.AddMenuEntryWithCommandList(FAyonCommands::Get().AyonTools, PluginCommands); + Section.AddMenuEntryWithCommandList(FAyonCommands::Get().AyonToolsDialog, PluginCommands); + } + UToolMenu* ToolbarMenu = UToolMenus::Get()->ExtendMenu("LevelEditor.LevelEditorToolBar.PlayToolBar"); + { + FToolMenuSection& Section = ToolbarMenu->FindOrAddSection("PluginTools"); + { + FToolMenuEntry& Entry = Section.AddEntry( + FToolMenuEntry::InitToolBarButton(FAyonCommands::Get().AyonTools)); + Entry.SetCommandList(PluginCommands); + } + } + } +} + + +void FAyonModule::MenuPopup() +{ + UAyonPythonBridge* bridge = UAyonPythonBridge::Get(); + bridge->RunInPython_Popup(); +} + +void FAyonModule::MenuDialog() +{ + UAyonPythonBridge* bridge = UAyonPythonBridge::Get(); + bridge->RunInPython_Dialog(); +} + +IMPLEMENT_MODULE(FAyonModule, Ayon) diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonAssetContainer.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonAssetContainer.cpp new file mode 100644 index 0000000000..869aa45256 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonAssetContainer.cpp @@ -0,0 +1,113 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#include "AyonAssetContainer.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "Misc/PackageName.h" +#include "Containers/UnrealString.h" + +UAyonAssetContainer::UAyonAssetContainer(const FObjectInitializer& ObjectInitializer) +: UAssetUserData(ObjectInitializer) +{ + FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); + FString path = UAyonAssetContainer::GetPathName(); + UE_LOG(LogTemp, Warning, TEXT("UAyonAssetContainer %s"), *path); + FARFilter Filter; + Filter.PackagePaths.Add(FName(*path)); + + AssetRegistryModule.Get().OnAssetAdded().AddUObject(this, &UAyonAssetContainer::OnAssetAdded); + AssetRegistryModule.Get().OnAssetRemoved().AddUObject(this, &UAyonAssetContainer::OnAssetRemoved); + AssetRegistryModule.Get().OnAssetRenamed().AddUObject(this, &UAyonAssetContainer::OnAssetRenamed); +} + +void UAyonAssetContainer::OnAssetAdded(const FAssetData& AssetData) +{ + TArray split; + + // get directory of current container + FString selfFullPath = UAyonAssetContainer::GetPathName(); + FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath); + + // get asset path and class + FString assetPath = AssetData.GetFullName(); + FString assetFName = AssetData.ObjectPath.ToString(); + UE_LOG(LogTemp, Log, TEXT("asset name %s"), *assetFName); + // 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 UAyonAssetContainer::OnAssetRemoved(const FAssetData& AssetData) +{ + TArray split; + + // get directory of current container + FString selfFullPath = UAyonAssetContainer::GetPathName(); + FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath); + + // get asset path and class + FString assetPath = AssetData.GetFullName(); + FString assetFName = AssetData.ObjectPath.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 = UAyonAssetContainer::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 UAyonAssetContainer::OnAssetRenamed(const FAssetData& AssetData, const FString& str) +{ + TArray split; + + // get directory of current container + FString selfFullPath = UAyonAssetContainer::GetPathName(); + FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath); + + // get asset path and class + FString assetPath = AssetData.GetFullName(); + FString assetFName = AssetData.ObjectPath.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/OpenPype/Source/Ayon/Private/AyonAssetContainerFactory.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonAssetContainerFactory.cpp new file mode 100644 index 0000000000..086fc1036e --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonAssetContainerFactory.cpp @@ -0,0 +1,20 @@ +#include "AyonAssetContainerFactory.h" +#include "AyonAssetContainer.h" + +UAyonAssetContainerFactory::UAyonAssetContainerFactory(const FObjectInitializer& ObjectInitializer) + : UFactory(ObjectInitializer) +{ + SupportedClass = UAyonAssetContainer::StaticClass(); + bCreateNew = false; + bEditorImport = true; +} + +UObject* UAyonAssetContainerFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) +{ + UAyonAssetContainer* AssetContainer = NewObject(InParent, Class, Name, Flags); + return AssetContainer; +} + +bool UAyonAssetContainerFactory::ShouldShowInNewMenu() const { + return false; +} diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonCommands.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonCommands.cpp new file mode 100644 index 0000000000..566ee1dcd1 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonCommands.cpp @@ -0,0 +1,13 @@ +// Copyright 2023, Ayon, All rights reserved. + +#include "AyonCommands.h" + +#define LOCTEXT_NAMESPACE "FAyonModule" + +void FAyonCommands::RegisterCommands() +{ + UI_COMMAND(AyonTools, "Ayon Tools", "Pipeline tools", EUserInterfaceActionType::Button, FInputChord()); + UI_COMMAND(AyonToolsDialog, "Ayon Tools Dialog", "Pipeline tools dialog", EUserInterfaceActionType::Button, FInputChord()); +} + +#undef LOCTEXT_NAMESPACE diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonLib.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonLib.cpp new file mode 100644 index 0000000000..7cfa0c9c30 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonLib.cpp @@ -0,0 +1,51 @@ +// Copyright 2023, Ayon, All rights reserved. +#include "AyonLib.h" + +#include "AssetViewUtils.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? + */ + +bool UAyonLib::SetFolderColor(const FString& FolderPath, const FLinearColor& FolderColor, const bool& bForceAdd) +{ + if (AssetViewUtils::DoesFolderExist(FolderPath)) + { + const TSharedPtr LinearColor = MakeShared(FolderColor); + + AssetViewUtils::SaveColor(FolderPath, LinearColor, true); + UE_LOG(LogAssetData, Display, TEXT("A color {%s} has been set to folder \"%s\""), *LinearColor->ToString(), + *FolderPath) + return true; + } + + UE_LOG(LogAssetData, Display, TEXT("Setting a color {%s} to folder \"%s\" has failed! Directory doesn't exist!"), + *FolderColor.ToString(), *FolderPath) + return false; +} + +/** + * Returns all poperties on given object + * @param cls - class + * @return TArray of properties + */ +TArray UAyonLib::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/OpenPype/Source/Ayon/Private/AyonPublishInstance.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonPublishInstance.cpp new file mode 100644 index 0000000000..f8d95ac048 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonPublishInstance.cpp @@ -0,0 +1,201 @@ +// Copyright 2023, Ayon, All rights reserved. +#pragma once + +#include "AyonPublishInstance.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "Framework/Notifications/NotificationManager.h" +#include "AyonLib.h" +#include "AyonSettings.h" +#include "Widgets/Notifications/SNotificationList.h" + + +//Moves all the invalid pointers to the end to prepare them for the shrinking +#define REMOVE_INVALID_ENTRIES(VAR) VAR.CompactStable(); \ + VAR.Shrink(); + +UAyonPublishInstance::UAyonPublishInstance(const FObjectInitializer& ObjectInitializer) + : UPrimaryDataAsset(ObjectInitializer) +{ + const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked< + FAssetRegistryModule>("AssetRegistry"); + + const FPropertyEditorModule& PropertyEditorModule = FModuleManager::LoadModuleChecked( + "PropertyEditor"); + + FString Left, Right; + GetPathName().Split("/" + GetName(), &Left, &Right); + + FARFilter Filter; + Filter.PackagePaths.Emplace(FName(Left)); + + TArray FoundAssets; + AssetRegistryModule.GetRegistry().GetAssets(Filter, FoundAssets); + + for (const FAssetData& AssetData : FoundAssets) + OnAssetCreated(AssetData); + + REMOVE_INVALID_ENTRIES(AssetDataInternal) + REMOVE_INVALID_ENTRIES(AssetDataExternal) + + AssetRegistryModule.Get().OnAssetAdded().AddUObject(this, &UAyonPublishInstance::OnAssetCreated); + AssetRegistryModule.Get().OnAssetRemoved().AddUObject(this, &UAyonPublishInstance::OnAssetRemoved); + AssetRegistryModule.Get().OnAssetUpdated().AddUObject(this, &UAyonPublishInstance::OnAssetUpdated); + +#ifdef WITH_EDITOR + ColorAyonDirs(); +#endif +} + +void UAyonPublishInstance::OnAssetCreated(const FAssetData& InAssetData) +{ + TArray split; + + UObject* Asset = InAssetData.GetAsset(); + + if (!IsValid(Asset)) + { + UE_LOG(LogAssetData, Warning, TEXT("Asset \"%s\" is not valid! Skipping the addition."), + *InAssetData.ObjectPath.ToString()); + return; + } + + const bool result = IsUnderSameDir(Asset) && Cast(Asset) == nullptr; + + if (result) + { + if (AssetDataInternal.Emplace(Asset).IsValidId()) + { + UE_LOG(LogTemp, Log, TEXT("Added an Asset to PublishInstance - Publish Instance: %s, Asset %s"), + *this->GetName(), *Asset->GetName()); + } + } +} + +void UAyonPublishInstance::OnAssetRemoved(const FAssetData& InAssetData) +{ + if (Cast(InAssetData.GetAsset()) == nullptr) + { + if (AssetDataInternal.Contains(nullptr)) + { + AssetDataInternal.Remove(nullptr); + REMOVE_INVALID_ENTRIES(AssetDataInternal) + } + else + { + AssetDataExternal.Remove(nullptr); + REMOVE_INVALID_ENTRIES(AssetDataExternal) + } + } +} + +void UAyonPublishInstance::OnAssetUpdated(const FAssetData& InAssetData) +{ + REMOVE_INVALID_ENTRIES(AssetDataInternal); + REMOVE_INVALID_ENTRIES(AssetDataExternal); +} + +bool UAyonPublishInstance::IsUnderSameDir(const UObject* InAsset) const +{ + FString ThisLeft, ThisRight; + this->GetPathName().Split(this->GetName(), &ThisLeft, &ThisRight); + + return InAsset->GetPathName().StartsWith(ThisLeft); +} + +#ifdef WITH_EDITOR + +void UAyonPublishInstance::ColorAyonDirs() +{ + FString PathName = this->GetPathName(); + + //Check whether the path contains the defined Ayon folder + if (!PathName.Contains(TEXT("Ayon"))) return; + + //Get the base path for open pype + FString PathLeft, PathRight; + PathName.Split(FString("Ayon"), &PathLeft, &PathRight); + + if (PathLeft.IsEmpty() || PathRight.IsEmpty()) + { + UE_LOG(LogAssetData, Error, TEXT("Failed to retrieve the base Ayon directory!")) + return; + } + + PathName.RemoveFromEnd(PathRight, ESearchCase::CaseSensitive); + + //Get the current settings + const UAyonSettings* Settings = GetMutableDefault(); + + //Color the base folder + UAyonLib::SetFolderColor(PathName, Settings->GetFolderFColor(), false); + + //Get Sub paths, iterate through them and color them according to the folder color in UAyonSettings + const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked( + "AssetRegistry"); + + TArray PathList; + + AssetRegistryModule.Get().GetSubPaths(PathName, PathList, true); + + if (PathList.Num() > 0) + { + for (const FString& Path : PathList) + { + UAyonLib::SetFolderColor(Path, Settings->GetFolderFColor(), false); + } + } +} + +void UAyonPublishInstance::SendNotification(const FString& Text) const +{ + FNotificationInfo Info{FText::FromString(Text)}; + + Info.bFireAndForget = true; + Info.bUseLargeFont = false; + Info.bUseThrobber = false; + Info.bUseSuccessFailIcons = false; + Info.ExpireDuration = 4.f; + Info.FadeOutDuration = 2.f; + + FSlateNotificationManager::Get().AddNotification(Info); + + UE_LOG(LogAssetData, Warning, + TEXT( + "Removed duplicated asset from the AssetsDataExternal in Container \"%s\", Asset is already included in the AssetDataInternal!" + ), *GetName() + ) +} + + +void UAyonPublishInstance::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) +{ + Super::PostEditChangeProperty(PropertyChangedEvent); + + if (PropertyChangedEvent.ChangeType == EPropertyChangeType::ValueSet && + PropertyChangedEvent.Property->GetFName() == GET_MEMBER_NAME_CHECKED( + UAyonPublishInstance, AssetDataExternal)) + { + // Check for duplicated assets + for (const auto& Asset : AssetDataInternal) + { + if (AssetDataExternal.Contains(Asset)) + { + AssetDataExternal.Remove(Asset); + return SendNotification( + "You are not allowed to add assets into AssetDataExternal which are already included in AssetDataInternal!"); + } + } + + // Check if no UAyonPublishInstance type assets are included + for (const auto& Asset : AssetDataExternal) + { + if (Cast(Asset.Get()) != nullptr) + { + AssetDataExternal.Remove(Asset); + return SendNotification("You are not allowed to add publish instances!"); + } + } + } +} + +#endif diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonPublishInstanceFactory.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonPublishInstanceFactory.cpp new file mode 100644 index 0000000000..c54e789dca --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonPublishInstanceFactory.cpp @@ -0,0 +1,21 @@ +// Copyright 2023, Ayon, All rights reserved. +#include "AyonPublishInstanceFactory.h" +#include "AyonPublishInstance.h" + +UAyonPublishInstanceFactory::UAyonPublishInstanceFactory(const FObjectInitializer& ObjectInitializer) + : UFactory(ObjectInitializer) +{ + SupportedClass = UAyonPublishInstance::StaticClass(); + bCreateNew = false; + bEditorImport = true; +} + +UObject* UAyonPublishInstanceFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) +{ + check(InClass->IsChildOf(UAyonPublishInstance::StaticClass())); + return NewObject(InParent, InClass, InName, Flags); +} + +bool UAyonPublishInstanceFactory::ShouldShowInNewMenu() const { + return false; +} diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonPythonBridge.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonPythonBridge.cpp new file mode 100644 index 0000000000..0ed4b2f704 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonPythonBridge.cpp @@ -0,0 +1,14 @@ +// Copyright 2023, Ayon, All rights reserved. +#include "AyonPythonBridge.h" + +UAyonPythonBridge* UAyonPythonBridge::Get() +{ + TArray AyonPythonBridgeClasses; + GetDerivedClasses(UAyonPythonBridge::StaticClass(), AyonPythonBridgeClasses); + int32 NumClasses = AyonPythonBridgeClasses.Num(); + if (NumClasses > 0) + { + return Cast(AyonPythonBridgeClasses[NumClasses - 1]->GetDefaultObject()); + } + return nullptr; +}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonSettings.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonSettings.cpp new file mode 100644 index 0000000000..da388fbc8f --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonSettings.cpp @@ -0,0 +1,21 @@ +// Copyright 2023, Ayon, All rights reserved. + +#include "AyonSettings.h" + +#include "Interfaces/IPluginManager.h" +#include "UObject/UObjectGlobals.h" + +/** + * Mainly is used for initializing default values if the DefaultAyonSettings.ini file does not exist in the saved config + */ +UAyonSettings::UAyonSettings(const FObjectInitializer& ObjectInitializer) +{ + + const FString ConfigFilePath = AYON_SETTINGS_FILEPATH; + + // This has to be probably in the future set using the UE Reflection system + FColor Color; + GConfig->GetColor(TEXT("/Script/Ayon.AyonSettings"), TEXT("FolderColor"), Color, ConfigFilePath); + + FolderColor = Color; +} \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonStyle.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonStyle.cpp new file mode 100644 index 0000000000..91a0c6996b --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonStyle.cpp @@ -0,0 +1,62 @@ +// Copyright 2023, Ayon, All rights reserved. + +#include "AyonStyle.h" +#include "Framework/Application/SlateApplication.h" +#include "Styling/SlateStyleRegistry.h" +#include "Slate/SlateGameResources.h" +#include "Interfaces/IPluginManager.h" +#include "Styling/SlateStyleMacros.h" + +#define RootToContentDir Style->RootToContentDir + +TSharedPtr FAyonStyle::AyonStyleInstance = nullptr; + +void FAyonStyle::Initialize() +{ + if (!AyonStyleInstance.IsValid()) + { + AyonStyleInstance = Create(); + FSlateStyleRegistry::RegisterSlateStyle(*AyonStyleInstance); + } +} + +void FAyonStyle::Shutdown() +{ + FSlateStyleRegistry::UnRegisterSlateStyle(*AyonStyleInstance); + ensure(AyonStyleInstance.IsUnique()); + AyonStyleInstance.Reset(); +} + +FName FAyonStyle::GetStyleSetName() +{ + static FName StyleSetName(TEXT("AyonStyle")); + return StyleSetName; +} + +const FVector2D Icon16x16(16.0f, 16.0f); +const FVector2D Icon20x20(20.0f, 20.0f); +const FVector2D Icon40x40(40.0f, 40.0f); + +TSharedRef< FSlateStyleSet > FAyonStyle::Create() +{ + TSharedRef< FSlateStyleSet > Style = MakeShareable(new FSlateStyleSet("AyonStyle")); + Style->SetContentRoot(IPluginManager::Get().FindPlugin("OpenPype")->GetBaseDir() / TEXT("Resources")); + + Style->Set("Ayon.AyonTools", new IMAGE_BRUSH(TEXT("ayon40"), Icon40x40)); + Style->Set("Ayon.AyonToolsDialog", new IMAGE_BRUSH(TEXT("ayon40"), Icon40x40)); + + return Style; +} + +void FAyonStyle::ReloadTextures() +{ + if (FSlateApplication::IsInitialized()) + { + FSlateApplication::Get().GetRenderer()->ReloadTextureResources(); + } +} + +const ISlateStyle& FAyonStyle::Get() +{ + return *AyonStyleInstance; +} diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/Commandlets/AyonActionResult.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/Commandlets/AyonActionResult.cpp new file mode 100644 index 0000000000..2a137e3ed7 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/Commandlets/AyonActionResult.cpp @@ -0,0 +1,40 @@ +// Copyright 2023, Ayon, All rights reserved. + +#include "Commandlets/AyonActionResult.h" +#include "Logging/Ayon_Log.h" + +EAyon_ActionResult::Type& FAyon_ActionResult::GetStatus() +{ + return Status; +} + +FText& FAyon_ActionResult::GetReason() +{ + return Reason; +} + +FAyon_ActionResult::FAyon_ActionResult():Status(EAyon_ActionResult::Type::Ok) +{ + +} + +FAyon_ActionResult::FAyon_ActionResult(const EAyon_ActionResult::Type& InEnum):Status(InEnum) +{ + TryLog(); +} + +FAyon_ActionResult::FAyon_ActionResult(const EAyon_ActionResult::Type& InEnum, const FText& InReason):Status(InEnum), Reason(InReason) +{ + TryLog(); +}; + +bool FAyon_ActionResult::IsProblem() const +{ + return Status != EAyon_ActionResult::Ok; +} + +void FAyon_ActionResult::TryLog() const +{ + if(IsProblem()) + UE_LOG(LogCommandletAyonGenerateProject, Error, TEXT("%s"), *Reason.ToString()); +} diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp new file mode 100644 index 0000000000..ed876c8128 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp @@ -0,0 +1,140 @@ +// Copyright 2023, Ayon, All rights reserved. +#include "Commandlets/Implementations/AyonGenerateProjectCommandlet.h" + +#include "GameProjectUtils.h" +#include "AyonConstants.h" +#include "Commandlets/AyonActionResult.h" +#include "ProjectDescriptor.h" + +int32 UAyonGenerateProjectCommandlet::Main(const FString& CommandLineParams) +{ + //Parses command line parameters & creates structure FProjectInformation + const FAyonGenerateProjectParams ParsedParams = FAyonGenerateProjectParams(CommandLineParams); + ProjectInformation = ParsedParams.GenerateUEProjectInformation(); + + //Creates .uproject & other UE files + EVALUATE_Ayon_ACTION_RESULT(TryCreateProject()); + + //Loads created .uproject + EVALUATE_Ayon_ACTION_RESULT(TryLoadProjectDescriptor()); + + //Adds needed plugin to .uproject + AttachPluginsToProjectDescriptor(); + + //Saves .uproject + EVALUATE_Ayon_ACTION_RESULT(TrySave()); + + //When we are here, there should not be problems in generating Unreal Project for Ayon + return 0; +} + + +FAyonGenerateProjectParams::FAyonGenerateProjectParams(): FAyonGenerateProjectParams("") +{ +} + +FAyonGenerateProjectParams::FAyonGenerateProjectParams(const FString& CommandLineParams): CommandLineParams( + CommandLineParams) +{ + UCommandlet::ParseCommandLine(*CommandLineParams, Tokens, Switches); +} + +FProjectInformation FAyonGenerateProjectParams::GenerateUEProjectInformation() const +{ + FProjectInformation ProjectInformation = FProjectInformation(); + ProjectInformation.ProjectFilename = GetProjectFileName(); + + ProjectInformation.bShouldGenerateCode = IsSwitchPresent("GenerateCode"); + + return ProjectInformation; +} + +FString FAyonGenerateProjectParams::TryGetToken(const int32 Index) const +{ + return Tokens.IsValidIndex(Index) ? Tokens[Index] : ""; +} + +FString FAyonGenerateProjectParams::GetProjectFileName() const +{ + return TryGetToken(0); +} + +bool FAyonGenerateProjectParams::IsSwitchPresent(const FString& Switch) const +{ + return INDEX_NONE != Switches.IndexOfByPredicate([&Switch](const FString& Item) -> bool + { + return Item.Equals(Switch); + } + ); +} + + +UAyonGenerateProjectCommandlet::UAyonGenerateProjectCommandlet() +{ + LogToConsole = true; +} + +FAyon_ActionResult UAyonGenerateProjectCommandlet::TryCreateProject() const +{ + FText FailReason; + FText FailLog; + TArray OutCreatedFiles; + + if (!GameProjectUtils::CreateProject(ProjectInformation, FailReason, FailLog, &OutCreatedFiles)) + return FAyon_ActionResult(EAyon_ActionResult::ProjectNotCreated, FailReason); + return FAyon_ActionResult(); +} + +FAyon_ActionResult UAyonGenerateProjectCommandlet::TryLoadProjectDescriptor() +{ + FText FailReason; + const bool bLoaded = ProjectDescriptor.Load(ProjectInformation.ProjectFilename, FailReason); + + return FAyon_ActionResult(bLoaded ? EAyon_ActionResult::Ok : EAyon_ActionResult::ProjectNotLoaded, FailReason); +} + +void UAyonGenerateProjectCommandlet::AttachPluginsToProjectDescriptor() +{ + FPluginReferenceDescriptor AyonPluginDescriptor; + AyonPluginDescriptor.bEnabled = true; + AyonPluginDescriptor.Name = AyonConstants::Ayon_PluginName; + ProjectDescriptor.Plugins.Add(AyonPluginDescriptor); + + FPluginReferenceDescriptor PythonPluginDescriptor; + PythonPluginDescriptor.bEnabled = true; + PythonPluginDescriptor.Name = AyonConstants::PythonScript_PluginName; + ProjectDescriptor.Plugins.Add(PythonPluginDescriptor); + + FPluginReferenceDescriptor SequencerScriptingPluginDescriptor; + SequencerScriptingPluginDescriptor.bEnabled = true; + SequencerScriptingPluginDescriptor.Name = AyonConstants::SequencerScripting_PluginName; + ProjectDescriptor.Plugins.Add(SequencerScriptingPluginDescriptor); + + FPluginReferenceDescriptor MovieRenderPipelinePluginDescriptor; + MovieRenderPipelinePluginDescriptor.bEnabled = true; + MovieRenderPipelinePluginDescriptor.Name = AyonConstants::MovieRenderPipeline_PluginName; + ProjectDescriptor.Plugins.Add(MovieRenderPipelinePluginDescriptor); + + FPluginReferenceDescriptor EditorScriptingPluginDescriptor; + EditorScriptingPluginDescriptor.bEnabled = true; + EditorScriptingPluginDescriptor.Name = AyonConstants::EditorScriptingUtils_PluginName; + ProjectDescriptor.Plugins.Add(EditorScriptingPluginDescriptor); +} + +FAyon_ActionResult UAyonGenerateProjectCommandlet::TrySave() +{ + FText FailReason; + const bool bSaved = ProjectDescriptor.Save(ProjectInformation.ProjectFilename, FailReason); + + return FAyon_ActionResult(bSaved ? EAyon_ActionResult::Ok : EAyon_ActionResult::ProjectNotSaved, FailReason); +} + +FAyonGenerateProjectParams UAyonGenerateProjectCommandlet::ParseParameters(const FString& Params) const +{ + FAyonGenerateProjectParams ParamsResult; + + TArray Tokens, Switches; + ParseCommandLine(*Params, Tokens, Switches); + + return ParamsResult; +} diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/Ayon.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/Ayon.h new file mode 100644 index 0000000000..bb25430411 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/Ayon.h @@ -0,0 +1,24 @@ +// Copyright 2023, Ayon, All rights reserved. + +#pragma once + +#include "CoreMinimal.h" + + +class FAyonModule : public IModuleInterface +{ +public: + virtual void StartupModule() override; + virtual void ShutdownModule() override; + +private: + void RegisterMenus(); + void RegisterSettings(); + bool HandleSettingsSaved(); + + void MenuPopup(); + void MenuDialog(); + +private: + TSharedPtr PluginCommands; +}; diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonAssetContainer.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonAssetContainer.h new file mode 100644 index 0000000000..d40642b149 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonAssetContainer.h @@ -0,0 +1,34 @@ +// 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 "AssetRegistry/AssetData.h" +#include "AyonAssetContainer.generated.h" + +UCLASS(Blueprintable) +class AYON_API UAyonAssetContainer : public UAssetUserData +{ + GENERATED_BODY() + +public: + + UAyonAssetContainer(const FObjectInitializer& ObjectInitalizer); + // ~UAyonAssetContainer(); + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Assets") + 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/OpenPype/Source/Ayon/Public/AyonAssetContainerFactory.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonAssetContainerFactory.h new file mode 100644 index 0000000000..da424cde2e --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonAssetContainerFactory.h @@ -0,0 +1,18 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "Factories/Factory.h" +#include "AyonAssetContainerFactory.generated.h" + +UCLASS() +class AYON_API UAyonAssetContainerFactory : public UFactory +{ + GENERATED_BODY() + +public: + UAyonAssetContainerFactory(const FObjectInitializer& ObjectInitializer); + virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; + virtual bool ShouldShowInNewMenu() const override; +}; diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonCommands.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonCommands.h new file mode 100644 index 0000000000..9c40dc8241 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonCommands.h @@ -0,0 +1,24 @@ +// Copyright 2023, Ayon, All rights reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Framework/Commands/Commands.h" +#include "AyonStyle.h" + +class FAyonCommands : public TCommands +{ +public: + + FAyonCommands() + : TCommands(TEXT("Ayon"), NSLOCTEXT("Contexts", "Ayon", "Ayon Tools"), NAME_None, FAyonStyle::GetStyleSetName()) + { + } + + // TCommands<> interface + virtual void RegisterCommands() override; + +public: + TSharedPtr< FUICommandInfo > AyonTools; + TSharedPtr< FUICommandInfo > AyonToolsDialog; +}; diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonConstants.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonConstants.h new file mode 100644 index 0000000000..5fe7c14360 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonConstants.h @@ -0,0 +1,13 @@ +// Copyright 2023, Ayon, All rights reserved. +#pragma once + +namespace AyonConstants +{ + const FString Ayon_PluginName = "Ayon"; + const FString PythonScript_PluginName = "PythonScriptPlugin"; + const FString SequencerScripting_PluginName = "SequencerScripting"; + const FString MovieRenderPipeline_PluginName = "MovieRenderPipeline"; + const FString EditorScriptingUtils_PluginName = "EditorScriptingUtilities"; +} + + diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonLib.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonLib.h new file mode 100644 index 0000000000..da83b448fb --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonLib.h @@ -0,0 +1,19 @@ +// Copyright 2023, Ayon, All rights reserved. +#pragma once + +#include "AyonLib.generated.h" + + +UCLASS(Blueprintable) +class AYON_API UAyonLib : public UBlueprintFunctionLibrary +{ + + GENERATED_BODY() + +public: + UFUNCTION(BlueprintCallable, Category = Python) + static bool SetFolderColor(const FString& FolderPath, const FLinearColor& FolderColor,const 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/OpenPype/Source/Ayon/Public/AyonPublishInstance.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonPublishInstance.h new file mode 100644 index 0000000000..1c51f98b4a --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonPublishInstance.h @@ -0,0 +1,102 @@ +// Copyright 2023, Ayon, All rights reserved. +#pragma once + +#include "AyonPublishInstance.generated.h" + + +UCLASS(Blueprintable) +class AYON_API UAyonPublishInstance : public UPrimaryDataAsset +{ + GENERATED_UCLASS_BODY() + +public: + /** + /** + * Retrieves all the assets which are monitored by the Publish Instance (Monitors assets in the directory which is + * placed in) + * + * @return - Set of UObjects. Careful! They are returning raw pointers. Seems like an issue in UE5 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") + TSet GetInternalAssets() const + { + //For some reason it can only return Raw Pointers? Seems like an issue which they haven't fixed. + TSet ResultSet; + + for (const auto& Asset : AssetDataInternal) + ResultSet.Add(Asset.LoadSynchronous()); + + return ResultSet; + } + + /** + * Retrieves all the assets which have been added manually by the Publish Instance + * + * @return - TSet of assets (UObjects). Careful! They are returning raw pointers. Seems like an issue in UE5 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") + TSet GetExternalAssets() const + { + //For some reason it can only return Raw Pointers? Seems like an issue which they haven't fixed. + TSet ResultSet; + + for (const auto& Asset : AssetDataExternal) + ResultSet.Add(Asset.LoadSynchronous()); + + return ResultSet; + } + + /** + * Function for returning all the assets in the container combined. + * + * @return Returns all the internal and externally added assets into one set (TSet of UObjects). Careful! They are + * returning raw pointers. Seems like an issue in UE5 + * + * @attention If the bAddExternalAssets variable is false, external assets won't be included! + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") + TSet GetAllAssets() const + { + const TSet>& IteratedSet = bAddExternalAssets + ? AssetDataInternal.Union(AssetDataExternal) + : AssetDataInternal; + + //Create a new TSet only with raw pointers. + TSet ResultSet; + + for (auto& Asset : IteratedSet) + ResultSet.Add(Asset.LoadSynchronous()); + + return ResultSet; + } + +private: + UPROPERTY(VisibleAnywhere, Category="Assets") + TSet> AssetDataInternal; + + /** + * This property allows exposing the array to include other assets from any other directory than what it's currently + * monitoring. NOTE: that these assets have to be added manually! They are not automatically registered or added! + */ + UPROPERTY(EditAnywhere, Category = "Assets") + bool bAddExternalAssets = false; + + UPROPERTY(EditAnywhere, meta=(EditCondition="bAddExternalAssets"), Category="Assets") + TSet> AssetDataExternal; + + + void OnAssetCreated(const FAssetData& InAssetData); + void OnAssetRemoved(const FAssetData& InAssetData); + void OnAssetUpdated(const FAssetData& InAssetData); + + bool IsUnderSameDir(const UObject* InAsset) const; + +#ifdef WITH_EDITOR + + void ColorAyonDirs(); + + void SendNotification(const FString& Text) const; + virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; + +#endif +}; diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonPublishInstanceFactory.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonPublishInstanceFactory.h new file mode 100644 index 0000000000..443d618c9a --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonPublishInstanceFactory.h @@ -0,0 +1,20 @@ +// Copyright 2023, Ayon, All rights reserved. +#pragma once + +#include "CoreMinimal.h" +#include "Factories/Factory.h" +#include "AyonPublishInstanceFactory.generated.h" + +/** + * + */ +UCLASS() +class AYON_API UAyonPublishInstanceFactory : public UFactory +{ + GENERATED_BODY() + +public: + UAyonPublishInstanceFactory(const FObjectInitializer& ObjectInitializer); + virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; + virtual bool ShouldShowInNewMenu() const override; +}; diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonPythonBridge.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonPythonBridge.h new file mode 100644 index 0000000000..3c429fd7d3 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonPythonBridge.h @@ -0,0 +1,20 @@ +// Copyright 2023, Ayon, All rights reserved. +#pragma once +#include "AyonPythonBridge.generated.h" + +UCLASS(Blueprintable) +class UAyonPythonBridge : public UObject +{ + GENERATED_BODY() + +public: + UFUNCTION(BlueprintCallable, Category = Python) + static UAyonPythonBridge* Get(); + + UFUNCTION(BlueprintImplementableEvent, Category = Python) + void RunInPython_Popup() const; + + UFUNCTION(BlueprintImplementableEvent, Category = Python) + void RunInPython_Dialog() const; + +}; diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonSettings.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonSettings.h new file mode 100644 index 0000000000..42a724b95a --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonSettings.h @@ -0,0 +1,32 @@ +// Copyright 2023, Ayon, All rights reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Object.h" +#include "AyonSettings.generated.h" + +#define AYON_SETTINGS_FILEPATH IPluginManager::Get().FindPlugin("OpenPype")->GetBaseDir() / TEXT("Config") / TEXT("DefaultAyonSettings.ini") + +UCLASS(Config=AyonSettings, DefaultConfig) +class AYON_API UAyonSettings : public UObject +{ + GENERATED_UCLASS_BODY() + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = Settings) + FColor GetFolderFColor() const + { + return FolderColor; + } + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = Settings) + FLinearColor GetFolderFLinearColor() const + { + return FLinearColor(FolderColor); + } + +protected: + + UPROPERTY(config, EditAnywhere, Category = Folders) + FColor FolderColor = FColor(25,45,223); +}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonStyle.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonStyle.h new file mode 100644 index 0000000000..58f6af656e --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonStyle.h @@ -0,0 +1,19 @@ +// Copyright 2023, Ayon, All rights reserved. +#pragma once +#include "CoreMinimal.h" +#include "Styling/SlateStyle.h" + +class FAyonStyle +{ +public: + static void Initialize(); + static void Shutdown(); + static void ReloadTextures(); + static const ISlateStyle& Get(); + static FName GetStyleSetName(); + + +private: + static TSharedRef< class FSlateStyleSet > Create(); + static TSharedPtr< class FSlateStyleSet > AyonStyleInstance; +}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/Commandlets/AyonActionResult.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/Commandlets/AyonActionResult.h new file mode 100644 index 0000000000..bb995ec452 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/Commandlets/AyonActionResult.h @@ -0,0 +1,83 @@ +// Copyright 2023, Ayon, All rights reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "AyonActionResult.generated.h" + +/** + * @brief This macro returns error code when is problem or does nothing when there is no problem. + * @param ActionResult FAyon_ActionResult structure + */ +#define EVALUATE_Ayon_ACTION_RESULT(ActionResult) \ + if(ActionResult.IsProblem()) \ + return ActionResult.GetStatus(); + +/** +* @brief This enum values are humanly readable mapping of error codes. +* Here should be all error codes to be possible find what went wrong. +* TODO: In the future should exists an web document where is mapped error code & what problem occured & how to repair it... +*/ +UENUM() +namespace EAyon_ActionResult +{ + enum Type + { + Ok, + ProjectNotCreated, + ProjectNotLoaded, + ProjectNotSaved, + //....Here insert another values + + //Do not remove! + //Usable for looping through enum values + __Last UMETA(Hidden) + }; +} + + +/** + * @brief This struct holds action result enum and optionally reason of fail + */ +USTRUCT() +struct FAyon_ActionResult +{ + GENERATED_BODY() + +public: + /** @brief Default constructor usable when there is no problem */ + FAyon_ActionResult(); + + /** + * @brief This constructor initializes variables & attempts to log when is error + * @param InEnum Status + */ + FAyon_ActionResult(const EAyon_ActionResult::Type& InEnum); + + /** + * @brief This constructor initializes variables & attempts to log when is error + * @param InEnum Status + * @param InReason Reason of potential fail + */ + FAyon_ActionResult(const EAyon_ActionResult::Type& InEnum, const FText& InReason); + +private: + /** @brief Action status */ + EAyon_ActionResult::Type Status; + + /** @brief Optional reason of fail */ + FText Reason; + +public: + /** + * @brief Checks if there is problematic state + * @return true when status is not equal to EAyon_ActionResult::Ok + */ + bool IsProblem() const; + EAyon_ActionResult::Type& GetStatus(); + FText& GetReason(); + +private: + void TryLog() const; +}; + diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h new file mode 100644 index 0000000000..da8e9af661 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h @@ -0,0 +1,61 @@ +// Copyright 2023, Ayon, All rights reserved. +#pragma once + + +#include "GameProjectUtils.h" +#include "Commandlets/AyonActionResult.h" +#include "ProjectDescriptor.h" +#include "Commandlets/Commandlet.h" +#include "AyonGenerateProjectCommandlet.generated.h" + +struct FProjectDescriptor; +struct FProjectInformation; + +/** +* @brief Structure which parses command line parameters and generates FProjectInformation +*/ +USTRUCT() +struct FAyonGenerateProjectParams +{ + GENERATED_BODY() + +private: + FString CommandLineParams; + TArray Tokens; + TArray Switches; + +public: + FAyonGenerateProjectParams(); + FAyonGenerateProjectParams(const FString& CommandLineParams); + + FProjectInformation GenerateUEProjectInformation() const; + +private: + FString TryGetToken(const int32 Index) const; + FString GetProjectFileName() const; + + bool IsSwitchPresent(const FString& Switch) const; +}; + +UCLASS() +class AYON_API UAyonGenerateProjectCommandlet : public UCommandlet +{ + GENERATED_BODY() + +private: + FProjectInformation ProjectInformation; + FProjectDescriptor ProjectDescriptor; + +public: + UAyonGenerateProjectCommandlet(); + + virtual int32 Main(const FString& CommandLineParams) override; + +private: + FAyonGenerateProjectParams ParseParameters(const FString& Params) const; + FAyon_ActionResult TryCreateProject() const; + FAyon_ActionResult TryLoadProjectDescriptor(); + void AttachPluginsToProjectDescriptor(); + FAyon_ActionResult TrySave(); +}; + diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/Logging/Ayon_Log.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/Logging/Ayon_Log.h new file mode 100644 index 0000000000..25b33a63e8 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/Logging/Ayon_Log.h @@ -0,0 +1,4 @@ +// Copyright 2023, Ayon, All rights reserved. +#pragma once + +DEFINE_LOG_CATEGORY_STATIC(LogCommandletAyonGenerateProject, Log, All); \ No newline at end of file From d780974b1b1e0f2e49fdeffddc8ed8d44e673f0e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 17 Mar 2023 17:23:44 +0800 Subject: [PATCH 071/360] usd mesh format to trimesh and adjustment on update function in loaders. --- openpype/hosts/max/plugins/load/load_model_fbx.py | 2 +- openpype/hosts/max/plugins/load/load_model_obj.py | 2 +- openpype/hosts/max/plugins/load/load_model_usd.py | 2 +- openpype/hosts/max/plugins/publish/extract_model_usd.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_model_fbx.py b/openpype/hosts/max/plugins/load/load_model_fbx.py index 38b8555d28..1729874a6b 100644 --- a/openpype/hosts/max/plugins/load/load_model_fbx.py +++ b/openpype/hosts/max/plugins/load/load_model_fbx.py @@ -47,7 +47,7 @@ importFile @"{filepath}" #noPrompt using:FBXIMP path = get_representation_path(representation) node = rt.getNodeByName(container["instance_node"]) - fbx_objects = self.get_container_children(node) + fbx_objects = node.Children for fbx_object in fbx_objects: fbx_object.source = path diff --git a/openpype/hosts/max/plugins/load/load_model_obj.py b/openpype/hosts/max/plugins/load/load_model_obj.py index 06b411cb5c..281a986934 100644 --- a/openpype/hosts/max/plugins/load/load_model_obj.py +++ b/openpype/hosts/max/plugins/load/load_model_obj.py @@ -41,7 +41,7 @@ class ObjLoader(load.LoaderPlugin): path = get_representation_path(representation) node = rt.getNodeByName(container["instance_node"]) - objects = self.get_container_children(node) + objects = node.Children for obj in objects: obj.source = path diff --git a/openpype/hosts/max/plugins/load/load_model_usd.py b/openpype/hosts/max/plugins/load/load_model_usd.py index c6c414b91c..b6a41f4e68 100644 --- a/openpype/hosts/max/plugins/load/load_model_usd.py +++ b/openpype/hosts/max/plugins/load/load_model_usd.py @@ -41,7 +41,7 @@ class ModelUSDLoader(load.LoaderPlugin): path = get_representation_path(representation) node = rt.getNodeByName(container["instance_node"]) - usd_objects = self.get_container_children(node) + usd_objects = node.Children for usd_object in usd_objects: usd_object.source = path diff --git a/openpype/hosts/max/plugins/publish/extract_model_usd.py b/openpype/hosts/max/plugins/publish/extract_model_usd.py index f70a14ba0b..60dddc8670 100644 --- a/openpype/hosts/max/plugins/publish/extract_model_usd.py +++ b/openpype/hosts/max/plugins/publish/extract_model_usd.py @@ -101,7 +101,7 @@ class ExtractModelUSD(publish.Extractor, export_options.Lights = False export_options.Cameras = False export_options.Materials = False - export_options.MeshFormat = rt.name('polyMesh') + export_options.MeshFormat = rt.name('triMesh') export_options.FileFormat = rt.name('ascii') export_options.UpAxis = rt.name('y') export_options.LogLevel = rt.name('info') From fd2d210522fbeddd27f707b0683e2f7411affd8e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 20 Mar 2023 11:26:48 +0100 Subject: [PATCH 072/360] Use create context environment Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../substancepainter/plugins/create/create_workfile.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/substancepainter/plugins/create/create_workfile.py b/openpype/hosts/substancepainter/plugins/create/create_workfile.py index 729cc8f718..29191a1714 100644 --- a/openpype/hosts/substancepainter/plugins/create/create_workfile.py +++ b/openpype/hosts/substancepainter/plugins/create/create_workfile.py @@ -29,9 +29,9 @@ class CreateWorkfile(AutoCreator): variant = self.default_variant project_name = self.project_name - asset_name = legacy_io.Session["AVALON_ASSET"] - task_name = legacy_io.Session["AVALON_TASK"] - host_name = legacy_io.Session["AVALON_APP"] + asset_name = self.create_context.get_current_asset_name() + task_name = self.create_context.get_current_task_name() + host_name = self.create_context.host_name # Workfile instance should always exist and must only exist once. # As such we'll first check if it already exists and is collected. From eeaa807588317b10e641e83566a07e278f3be6a7 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 20 Mar 2023 11:41:54 +0100 Subject: [PATCH 073/360] Remove unused import --- .../hosts/substancepainter/plugins/create/create_workfile.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/substancepainter/plugins/create/create_workfile.py b/openpype/hosts/substancepainter/plugins/create/create_workfile.py index 29191a1714..4e316f3b64 100644 --- a/openpype/hosts/substancepainter/plugins/create/create_workfile.py +++ b/openpype/hosts/substancepainter/plugins/create/create_workfile.py @@ -2,7 +2,6 @@ """Creator plugin for creating workfiles.""" from openpype.pipeline import CreatedInstance, AutoCreator -from openpype.pipeline import legacy_io from openpype.client import get_asset_by_name from openpype.hosts.substancepainter.api.pipeline import ( From 9020bf23d325b706485ed7374d22f6073aa71e79 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 20 Mar 2023 11:44:56 +0100 Subject: [PATCH 074/360] Implement `get_context_data` and `update_context_data` --- .../hosts/substancepainter/api/pipeline.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/substancepainter/api/pipeline.py b/openpype/hosts/substancepainter/api/pipeline.py index f4d4c5b00c..b377db1641 100644 --- a/openpype/hosts/substancepainter/api/pipeline.py +++ b/openpype/hosts/substancepainter/api/pipeline.py @@ -38,6 +38,7 @@ INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") OPENPYPE_METADATA_KEY = "OpenPype" OPENPYPE_METADATA_CONTAINERS_KEY = "containers" # child key +OPENPYPE_METADATA_CONTEXT_KEY = "context" # child key class SubstanceHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): @@ -140,15 +141,21 @@ class SubstanceHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): container["objectName"] = key yield container - @staticmethod - def create_context_node(): - pass - def update_context_data(self, data, changes): - pass + + if not substance_painter.project.is_open(): + return + + metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY) + metadata.set(OPENPYPE_METADATA_CONTEXT_KEY, data) def get_context_data(self): - pass + + if not substance_painter.project.is_open(): + return + + metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY) + return metadata.get(OPENPYPE_METADATA_CONTEXT_KEY) or {} def _install_menu(self): from PySide2 import QtWidgets From eeb2388475d664aa95dff4b09fdef9fc6ed17549 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 20 Mar 2023 14:13:21 +0100 Subject: [PATCH 075/360] Use `openpype.pipeline.create.get_subset_name` to define the subset name --- .../publish/collect_textureset_images.py | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py index 04187d4079..b368c86749 100644 --- a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py +++ b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py @@ -9,6 +9,8 @@ from openpype.hosts.substancepainter.api.lib import ( get_parsed_export_maps, strip_template ) +from openpype.pipeline.create import get_subset_name +from openpype.client import get_asset_by_name class CollectTextureSet(pyblish.api.InstancePlugin): @@ -24,6 +26,10 @@ class CollectTextureSet(pyblish.api.InstancePlugin): def process(self, instance): config = self.get_export_config(instance) + asset_doc = get_asset_by_name( + project_name=instance.context.data["projectName"], + asset_name=instance.data["asset"] + ) instance.data["exportConfig"] = config maps = get_parsed_export_maps(config) @@ -34,9 +40,11 @@ class CollectTextureSet(pyblish.api.InstancePlugin): self.log.info(f"Processing {texture_set_name}/{stack_name}") for template, outputs in template_maps.items(): self.log.info(f"Processing {template}") - self.create_image_instance(instance, template, outputs) + self.create_image_instance(instance, template, outputs, + asset_doc=asset_doc) - def create_image_instance(self, instance, template, outputs): + def create_image_instance(self, instance, template, outputs, + asset_doc): """Create a new instance per image or UDIM sequence. The new instances will be of family `image`. @@ -53,8 +61,17 @@ class CollectTextureSet(pyblish.api.InstancePlugin): # Define the suffix we want to give this particular texture # set and set up a remapped subset naming for it. suffix = f".{map_identifier}" - image_subset = instance.data["subset"][len("textureSet"):] - image_subset = "texture" + image_subset + suffix + image_subset = get_subset_name( + # TODO: The family actually isn't 'texture' currently but for now + # this is only done so the subset name starts with 'texture' + family="texture", + variant=instance.data["variant"] + suffix, + task_name=instance.data.get("task"), + asset_doc=asset_doc, + project_name=context.data["projectName"], + host_name=context.data["hostName"], + project_settings=context.data["project_settings"] + ) # Prepare representation representation = { From f8a3e24c606048883fa0942c01df1f7aff893436 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 20 Mar 2023 20:04:36 +0100 Subject: [PATCH 076/360] Explain how Texture Sets are split into separate publishes per output map in documentation --- website/docs/artist_hosts_substancepainter.md | 33 ++++++++++++++++-- ...ter_pbrmetallicroughness_export_preset.png | Bin 0 -> 45842 bytes ...painter_pbrmetallicroughness_published.png | Bin 0 -> 7497 bytes 3 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 website/docs/assets/substancepainter_pbrmetallicroughness_export_preset.png create mode 100644 website/docs/assets/substancepainter_pbrmetallicroughness_published.png diff --git a/website/docs/artist_hosts_substancepainter.md b/website/docs/artist_hosts_substancepainter.md index 9ed83421af..86bcbba82e 100644 --- a/website/docs/artist_hosts_substancepainter.md +++ b/website/docs/artist_hosts_substancepainter.md @@ -51,8 +51,9 @@ publish instance. To create a **TextureSet instance** we will use OpenPype's publisher tool. Go to **OpenPype → Publish... → TextureSet** -The texture set instance will define what Substance Painter export template `.spexp` to -use and thus defines what texture maps will be exported from your workfile. +The texture set instance will define what Substance Painter export template (`.spexp`) to +use and thus defines what texture maps will be exported from your workfile. This +can be set with the **Output Template** attribute on the instance. :::info The TextureSet instance gets saved with your Substance Painter project. As such, @@ -61,8 +62,34 @@ just click **OpenPype → Publish...** and start publishing directly with the same settings. ::: +#### Publish per output map of the Substance Painter preset -### Known issues +The Texture Set instance generates a publish per output map that is defined in +the Substance Painter's export preset. For example a publish from a default +PBR Metallic Roughness texture set results in six separate published subsets +(if all the channels exist in your file). + +![Substance Painter PBR Metallic Roughness Export Preset](assets/substancepainter_pbrmetallicroughness_export_preset.png) + +When publishing for example a texture set with variant **Main** six instances will +be published with the variants: +- Main.**BaseColor** +- Main.**Emissive** +- Main.**Height** +- Main.**Metallic** +- Main.**Normal** +- Main.**Roughness** + +The bold output map name for the publish is based on the string that is pulled +from the what is considered to be the static part of the filename templates in +the export preset. The tokens like `$mesh` and `(_$colorSpace)` are ignored. +So `$mesh_$textureSet_BaseColor(_$colorSpace)(.$udim)` becomes `BaseColor`. + +An example output for PBR Metallic Roughness would be: + +![Substance Painter PBR Metallic Roughness Publish Example in Loader](assets/substancepainter_pbrmetallicroughness_published.png) + +## Known issues #### Can't see the OpenPype menu? diff --git a/website/docs/assets/substancepainter_pbrmetallicroughness_export_preset.png b/website/docs/assets/substancepainter_pbrmetallicroughness_export_preset.png new file mode 100644 index 0000000000000000000000000000000000000000..35a4545f83563332e983ada1698433955498a969 GIT binary patch literal 45842 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 literal 0 HcmV?d00001 diff --git a/website/docs/assets/substancepainter_pbrmetallicroughness_published.png b/website/docs/assets/substancepainter_pbrmetallicroughness_published.png new file mode 100644 index 0000000000000000000000000000000000000000..15b0e5b87687a8675c481a8265e5f0c5639de8fe GIT binary patch literal 7497 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 Date: Mon, 20 Mar 2023 20:07:23 +0100 Subject: [PATCH 077/360] Simplify setting review tag and stagingDir for thumbnail on representation --- .../plugins/publish/collect_textureset_images.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py index b368c86749..f7187b638f 100644 --- a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py +++ b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py @@ -87,6 +87,12 @@ class CollectTextureSet(pyblish.api.InstancePlugin): # strings. See CollectTextures plug-in and Integrators. representation["udim"] = [output["udim"] for output in outputs] + # Set up the representation for thumbnail generation + # TODO: Simplify this once thumbnail extraction is refactored + staging_dir = os.path.dirname(first_filepath) + representation["tags"] = ["review"] + representation["stagingDir"] = staging_dir + # Clone the instance image_instance = context.create_instance(instance.name) image_instance[:] = instance[:] @@ -108,12 +114,6 @@ class CollectTextureSet(pyblish.api.InstancePlugin): self.log.debug(f"{image_subset} colorspace: {colorspace}") image_instance.data["colorspace"] = colorspace - # Set up the representation for thumbnail generation - # TODO: Simplify this once thumbnail extraction is refactored - staging_dir = os.path.dirname(first_filepath) - image_instance.data["representations"][0]["tags"] = ["review"] - image_instance.data["representations"][0]["stagingDir"] = staging_dir - # Store the instance in the original instance as a member instance.append(image_instance) From 0b3cb6942dc03e231743fd1713f3e919fdc785f7 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 20 Mar 2023 20:27:34 +0100 Subject: [PATCH 078/360] Add todo about a potentially critical issue to still be solved. --- .../plugins/publish/collect_textureset_images.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py index f7187b638f..14168138b6 100644 --- a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py +++ b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py @@ -60,6 +60,9 @@ class CollectTextureSet(pyblish.api.InstancePlugin): # Define the suffix we want to give this particular texture # set and set up a remapped subset naming for it. + # TODO (Critical) Support needs to be added to have multiple materials + # with each their own maps. So we might need to include the + # material or alike in the variant suffix too? suffix = f".{map_identifier}" image_subset = get_subset_name( # TODO: The family actually isn't 'texture' currently but for now From 217b9dd70822ecccfaf6e2d45b4caac0d479835b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 23 Mar 2023 10:54:18 +0100 Subject: [PATCH 079/360] Move and refactor PySide2 imports to `qtpy` and top of file --- openpype/hosts/substancepainter/api/lib.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/substancepainter/api/lib.py b/openpype/hosts/substancepainter/api/lib.py index e552caee6d..e299ab03de 100644 --- a/openpype/hosts/substancepainter/api/lib.py +++ b/openpype/hosts/substancepainter/api/lib.py @@ -7,6 +7,8 @@ import substance_painter.project import substance_painter.resource import substance_painter.js +from qtpy import QtGui, QtWidgets, QtCore + def get_export_presets(): """Return Export Preset resource URLs for all available Export Presets. @@ -391,8 +393,6 @@ def get_parsed_export_maps(config): dict: [texture_set, stack]: {template: [file1_data, file2_data]} """ - import substance_painter.export - from .colorspace import get_project_channel_data outputs = substance_painter.export.list_project_textures(config) templates = get_export_templates(config, strip_folder=False) @@ -524,7 +524,6 @@ def load_shelf(path, name=None): def _get_new_project_action(): """Return QAction which triggers Substance Painter's new project dialog""" - from PySide2 import QtGui main_window = substance_painter.ui.get_main_window() @@ -564,7 +563,6 @@ def prompt_new_file_with_mesh(mesh_filepath): for example when the user might have cancelled the operation. """ - from PySide2 import QtWidgets, QtCore app = QtWidgets.QApplication.instance() assert os.path.isfile(mesh_filepath), \ From 1cc2db14bbd0be5a380fadc7108f0ed646f95abc Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 23 Mar 2023 10:56:14 +0100 Subject: [PATCH 080/360] Add back in imports that accidentally got removed --- openpype/hosts/substancepainter/api/lib.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/substancepainter/api/lib.py b/openpype/hosts/substancepainter/api/lib.py index e299ab03de..2cd08f862e 100644 --- a/openpype/hosts/substancepainter/api/lib.py +++ b/openpype/hosts/substancepainter/api/lib.py @@ -6,6 +6,7 @@ from collections import defaultdict import substance_painter.project import substance_painter.resource import substance_painter.js +import substance_painter.export from qtpy import QtGui, QtWidgets, QtCore @@ -393,6 +394,8 @@ def get_parsed_export_maps(config): dict: [texture_set, stack]: {template: [file1_data, file2_data]} """ + # Import is here to avoid recursive lib <-> colorspace imports + from .colorspace import get_project_channel_data outputs = substance_painter.export.list_project_textures(config) templates = get_export_templates(config, strip_folder=False) From 8b3ce3044a9368663d91ba45279c7a63fcb3876e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 23 Mar 2023 10:56:58 +0100 Subject: [PATCH 081/360] Raise KnownPublishError instead of assert Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../hosts/substancepainter/plugins/publish/save_workfile.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/substancepainter/plugins/publish/save_workfile.py b/openpype/hosts/substancepainter/plugins/publish/save_workfile.py index 5e86785e0d..2bd342cda1 100644 --- a/openpype/hosts/substancepainter/plugins/publish/save_workfile.py +++ b/openpype/hosts/substancepainter/plugins/publish/save_workfile.py @@ -13,7 +13,8 @@ class SaveCurrentWorkfile(pyblish.api.ContextPlugin): def process(self, context): host = registered_host() - assert context.data['currentFile'] == host.get_current_workfile() + if context.data['currentFile'] != host.get_current_workfile(): + raise KnownPublishError("Workfile has changed during publishing!") if host.has_unsaved_changes(): self.log.info("Saving current file..") From 17fc4ed9251551c37f5405101f12af8e1bc8e890 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 23 Mar 2023 10:58:04 +0100 Subject: [PATCH 082/360] Fix import --- .../hosts/substancepainter/plugins/publish/save_workfile.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/substancepainter/plugins/publish/save_workfile.py b/openpype/hosts/substancepainter/plugins/publish/save_workfile.py index 2bd342cda1..f19deccb0e 100644 --- a/openpype/hosts/substancepainter/plugins/publish/save_workfile.py +++ b/openpype/hosts/substancepainter/plugins/publish/save_workfile.py @@ -1,6 +1,9 @@ import pyblish.api -from openpype.pipeline import registered_host +from openpype.pipeline import ( + registered_host, + KnownPublishError +) class SaveCurrentWorkfile(pyblish.api.ContextPlugin): From 4fdb31611dc9810346a45a10c50ea9a209d7a99f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 23 Mar 2023 11:03:54 +0100 Subject: [PATCH 083/360] Allow to mark an instance to skip integration explicitly Use `instance.data["integrate"] = False` --- .../plugins/publish/extract_textures.py | 15 ++++----------- openpype/plugins/publish/integrate.py | 5 +++++ 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/substancepainter/plugins/publish/extract_textures.py b/openpype/hosts/substancepainter/plugins/publish/extract_textures.py index 469f8501f7..bd933610f4 100644 --- a/openpype/hosts/substancepainter/plugins/publish/extract_textures.py +++ b/openpype/hosts/substancepainter/plugins/publish/extract_textures.py @@ -58,14 +58,7 @@ class ExtractTextures(publish.Extractor, context=context, colorspace=colorspace) - # Add a fake representation which won't be integrated so the - # Integrator leaves us alone - otherwise it would error - # TODO: Add `instance.data["integrate"] = False` support in Integrator? - instance.data["representations"] = [ - { - "name": "_fake", - "ext": "_fake", - "delete": True, - "files": [] - } - ] + # The TextureSet instance should not be integrated. It generates no + # output data. Instead the separated texture instances are generated + # from it which themselves integrate into the database. + instance.data["integrate"] = False diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 6a0327ec84..c24758ba0f 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -160,6 +160,11 @@ class IntegrateAsset(pyblish.api.InstancePlugin): "Instance is marked to be processed on farm. Skipping") return + # Instance is marked to not get integrated + if instance.data.get("integrate", True): + self.log.info("Instance is marked to skip integrating. Skipping") + return + filtered_repres = self.filter_representations(instance) # Skip instance if there are not representations to integrate # all representations should not be integrated From 5b3af11f0f6bbd53dcc590de49f51660dbdeb556 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 23 Mar 2023 11:04:25 +0100 Subject: [PATCH 084/360] Fix the if statement --- openpype/plugins/publish/integrate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index c24758ba0f..fa29d2a58b 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -161,7 +161,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): return # Instance is marked to not get integrated - if instance.data.get("integrate", True): + if not instance.data.get("integrate", True): self.log.info("Instance is marked to skip integrating. Skipping") return From ddc0117aeda6fd1542d96ee54fb374a1339d8aae Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 23 Mar 2023 11:14:39 +0100 Subject: [PATCH 085/360] Update openpype/settings/defaults/project_settings/substancepainter.json Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../settings/defaults/project_settings/substancepainter.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/substancepainter.json b/openpype/settings/defaults/project_settings/substancepainter.json index 0f9f1af71e..60929e85fd 100644 --- a/openpype/settings/defaults/project_settings/substancepainter.json +++ b/openpype/settings/defaults/project_settings/substancepainter.json @@ -10,4 +10,4 @@ } }, "shelves": {} -} \ No newline at end of file +} From 57b84f18bc343b4892382d642927847496f3e43e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 23 Mar 2023 11:18:37 +0100 Subject: [PATCH 086/360] Fix docstring --- openpype/hosts/substancepainter/api/pipeline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/substancepainter/api/pipeline.py b/openpype/hosts/substancepainter/api/pipeline.py index b377db1641..652ec9ec7d 100644 --- a/openpype/hosts/substancepainter/api/pipeline.py +++ b/openpype/hosts/substancepainter/api/pipeline.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""Pipeline tools for OpenPype Gaffer integration.""" +"""Pipeline tools for OpenPype Substance Painter integration.""" import os import logging from functools import partial From f4d423dc4f7b1a42310540c74230ba3a1dcd20ab Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 23 Mar 2023 14:39:48 +0100 Subject: [PATCH 087/360] Add Create... menu entry to match other hosts --- openpype/hosts/substancepainter/api/pipeline.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/substancepainter/api/pipeline.py b/openpype/hosts/substancepainter/api/pipeline.py index 652ec9ec7d..df41d9bb70 100644 --- a/openpype/hosts/substancepainter/api/pipeline.py +++ b/openpype/hosts/substancepainter/api/pipeline.py @@ -165,6 +165,12 @@ class SubstanceHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): menu = QtWidgets.QMenu("OpenPype") + action = menu.addAction("Create...") + action.triggered.connect( + lambda: host_tools.show_publisher(parent=parent, + tab="create") + ) + action = menu.addAction("Load...") action.triggered.connect( lambda: host_tools.show_loader(parent=parent, use_context=True) @@ -172,7 +178,8 @@ class SubstanceHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): action = menu.addAction("Publish...") action.triggered.connect( - lambda: host_tools.show_publisher(parent=parent) + lambda: host_tools.show_publisher(parent=parent, + tab="publish") ) action = menu.addAction("Manage...") From d4a0c6634cd0d9c31ea8f1cf12b92fee5e7ba797 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 23 Mar 2023 15:45:13 +0100 Subject: [PATCH 088/360] Optimize logic --- openpype/hosts/substancepainter/api/colorspace.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/substancepainter/api/colorspace.py b/openpype/hosts/substancepainter/api/colorspace.py index a9df3eb066..375b61b39b 100644 --- a/openpype/hosts/substancepainter/api/colorspace.py +++ b/openpype/hosts/substancepainter/api/colorspace.py @@ -25,11 +25,11 @@ def _iter_document_stack_channels(): material_name = material["name"] for stack in material["stacks"]: stack_name = stack["name"] + if stack_name: + stack_path = [material_name, stack_name] + else: + stack_path = material_name for channel in stack["channels"]: - if stack_name: - stack_path = [material_name, stack_name] - else: - stack_path = material_name yield stack_path, channel From 22d628d054809a9e8f1d816994a7426197d864f8 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 23 Mar 2023 18:09:13 +0100 Subject: [PATCH 089/360] Store instances in single project metadata key by id + fix adding/removing instances --- .../hosts/substancepainter/api/pipeline.py | 67 ++++++++++++++----- .../plugins/create/create_textures.py | 39 ++++++----- .../plugins/create/create_workfile.py | 27 +++++--- 3 files changed, 93 insertions(+), 40 deletions(-) diff --git a/openpype/hosts/substancepainter/api/pipeline.py b/openpype/hosts/substancepainter/api/pipeline.py index df41d9bb70..b995c9030d 100644 --- a/openpype/hosts/substancepainter/api/pipeline.py +++ b/openpype/hosts/substancepainter/api/pipeline.py @@ -39,6 +39,7 @@ INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") OPENPYPE_METADATA_KEY = "OpenPype" OPENPYPE_METADATA_CONTAINERS_KEY = "containers" # child key OPENPYPE_METADATA_CONTEXT_KEY = "context" # child key +OPENPYPE_METADATA_INSTANCES_KEY = "instances" # child key class SubstanceHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): @@ -312,21 +313,6 @@ def imprint_container(container, container[key] = value -def set_project_metadata(key, data): - """Set a key in project's OpenPype metadata.""" - metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY) - metadata.set(key, data) - - -def get_project_metadata(key): - """Get a key from project's OpenPype metadata.""" - if not substance_painter.project.is_open(): - return - - metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY) - return metadata.get(key) - - def set_container_metadata(object_name, container_data, update=False): """Helper method to directly set the data for a specific container @@ -359,3 +345,54 @@ def remove_container_metadata(object_name): if containers: containers.pop(object_name, None) metadata.set("containers", containers) + + +def set_instance(instance_id, instance_data, update=False): + """Helper method to directly set the data for a specific container + + Args: + instance_id (str): Unique identifier for the instance + instance_data (dict): The instance data to store in the metaadata. + """ + set_instances({instance_id: instance_data}, update=update) + + +def set_instances(instance_data_by_id, update=False): + """Store data for multiple instances at the same time. + + This is more optimal than querying and setting them in the metadata one + by one. + """ + metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY) + instances = metadata.get(OPENPYPE_METADATA_INSTANCES_KEY) or {} + + for instance_id, instance_data in instance_data_by_id.items(): + if update: + existing_data = instances.get(instance_id, {}) + existing_data.update(instance_data) + else: + instances[instance_id] = instance_data + + metadata.set("instances", instances) + + +def remove_instance(instance_id): + """Helper method to remove the data for a specific container""" + metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY) + instances = metadata.get(OPENPYPE_METADATA_INSTANCES_KEY) or {} + instances.pop(instance_id, None) + metadata.set("instances", instances) + + +def get_instances_by_id(): + """Return all instances stored in the project instances metadata""" + if not substance_painter.project.is_open(): + return {} + + metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY) + return metadata.get(OPENPYPE_METADATA_INSTANCES_KEY) or {} + + +def get_instances(): + """Return all instances stored in the project instances as a list""" + return list(get_instances_by_id().values()) diff --git a/openpype/hosts/substancepainter/plugins/create/create_textures.py b/openpype/hosts/substancepainter/plugins/create/create_textures.py index 9d641215dc..19133768a5 100644 --- a/openpype/hosts/substancepainter/plugins/create/create_textures.py +++ b/openpype/hosts/substancepainter/plugins/create/create_textures.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """Creator plugin for creating textures.""" -from openpype.pipeline import CreatedInstance, Creator +from openpype.pipeline import CreatedInstance, Creator, CreatorError from openpype.lib import ( EnumDef, UILabelDef, @@ -9,8 +9,10 @@ from openpype.lib import ( ) from openpype.hosts.substancepainter.api.pipeline import ( - set_project_metadata, - get_project_metadata + get_instances, + set_instance, + set_instances, + remove_instance ) from openpype.hosts.substancepainter.api.lib import get_export_presets @@ -29,27 +31,34 @@ class CreateTextures(Creator): def create(self, subset_name, instance_data, pre_create_data): if not substance_painter.project.is_open(): - return + raise CreatorError("Can't create a Texture Set instance without " + "an open project.") - instance = self.create_instance_in_context(subset_name, instance_data) - set_project_metadata("textureSet", instance.data_to_store()) + instance = self.create_instance_in_context(subset_name, + instance_data) + set_instance( + instance_id=instance["instance_id"], + instance_data=instance.data_to_store() + ) def collect_instances(self): - workfile = get_project_metadata("textureSet") - if workfile: - self.create_instance_in_context_from_existing(workfile) + for instance in get_instances(): + if (instance.get("creator_identifier") == self.identifier or + instance.get("family") == self.family): + self.create_instance_in_context_from_existing(instance) def update_instances(self, update_list): + instance_data_by_id = {} for instance, _changes in update_list: - # Update project's metadata - data = get_project_metadata("textureSet") or {} - data.update(instance.data_to_store()) - set_project_metadata("textureSet", data) + # Persist the data + instance_id = instance.get("instance_id") + instance_data = instance.data_to_store() + instance_data_by_id[instance_id] = instance_data + set_instances(instance_data_by_id, update=True) def remove_instances(self, instances): for instance in instances: - # TODO: Implement removal - # api.remove_instance(instance) + remove_instance(instance["instance_id"]) self._remove_instance_from_context(instance) # Helper methods (this might get moved into Creator class) diff --git a/openpype/hosts/substancepainter/plugins/create/create_workfile.py b/openpype/hosts/substancepainter/plugins/create/create_workfile.py index 4e316f3b64..d7f31f9dcf 100644 --- a/openpype/hosts/substancepainter/plugins/create/create_workfile.py +++ b/openpype/hosts/substancepainter/plugins/create/create_workfile.py @@ -5,8 +5,9 @@ from openpype.pipeline import CreatedInstance, AutoCreator from openpype.client import get_asset_by_name from openpype.hosts.substancepainter.api.pipeline import ( - set_project_metadata, - get_project_metadata + set_instances, + set_instance, + get_instances ) import substance_painter.project @@ -66,19 +67,25 @@ class CreateWorkfile(AutoCreator): current_instance["task"] = task_name current_instance["subset"] = subset_name - set_project_metadata("workfile", current_instance.data_to_store()) + set_instance( + instance_id=current_instance.get("instance_id"), + instance_data=current_instance.data_to_store() + ) def collect_instances(self): - workfile = get_project_metadata("workfile") - if workfile: - self.create_instance_in_context_from_existing(workfile) + for instance in get_instances(): + if (instance.get("creator_identifier") == self.identifier or + instance.get("family") == self.family): + self.create_instance_in_context_from_existing(instance) def update_instances(self, update_list): + instance_data_by_id = {} for instance, _changes in update_list: - # Update project's workfile metadata - data = get_project_metadata("workfile") or {} - data.update(instance.data_to_store()) - set_project_metadata("workfile", data) + # Persist the data + instance_id = instance.get("instance_id") + instance_data = instance.data_to_store() + instance_data_by_id[instance_id] = instance_data + set_instances(instance_data_by_id, update=True) # Helper methods (this might get moved into Creator class) def create_instance_in_context(self, subset_name, data): From c20f45e88136371dd2a8a35eca66cf28f7ac3ee8 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 27 Mar 2023 23:48:27 +0800 Subject: [PATCH 090/360] skip unrelated script --- openpype/hosts/max/plugins/load/load_camera_fbx.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/max/plugins/load/load_camera_fbx.py b/openpype/hosts/max/plugins/load/load_camera_fbx.py index 205e815dc8..3a6947798e 100644 --- a/openpype/hosts/max/plugins/load/load_camera_fbx.py +++ b/openpype/hosts/max/plugins/load/load_camera_fbx.py @@ -36,6 +36,8 @@ importFile @"{filepath}" #noPrompt using:FBXIMP self.log.debug(f"Executing command: {fbx_import_cmd}") rt.execute(fbx_import_cmd) + container_name = f"{name}_CON" + asset = rt.getNodeByName(f"{name}") return containerise( From 32bb42e37922dd2de79f01c6e133b17ee8e7c6fa Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 28 Mar 2023 17:26:04 +0800 Subject: [PATCH 091/360] update the obj loader and add maintained_selection for loaders --- openpype/hosts/max/plugins/load/load_model.py | 12 ++++++++---- openpype/hosts/max/plugins/load/load_model_fbx.py | 4 ++++ openpype/hosts/max/plugins/load/load_model_obj.py | 12 ++++++++---- openpype/hosts/max/plugins/load/load_model_usd.py | 4 ++++ 4 files changed, 24 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_model.py b/openpype/hosts/max/plugins/load/load_model.py index c248d75718..95ee014e07 100644 --- a/openpype/hosts/max/plugins/load/load_model.py +++ b/openpype/hosts/max/plugins/load/load_model.py @@ -5,6 +5,7 @@ from openpype.pipeline import ( ) from openpype.hosts.max.api.pipeline import containerise from openpype.hosts.max.api import lib +from openpype.hosts.max.api.lib import maintained_selection class ModelAbcLoader(load.LoaderPlugin): @@ -57,12 +58,8 @@ importFile @"{file_path}" #noPrompt def update(self, container, representation): from pymxs import runtime as rt - path = get_representation_path(representation) node = rt.getNodeByName(container["instance_node"]) - lib.imprint(container["instance_node"], { - "representation": str(representation["_id"]) - }) rt.select(node.Children) for alembic in rt.selection: @@ -76,6 +73,13 @@ importFile @"{file_path}" #noPrompt alembic_obj = rt.getNodeByName(abc_obj.name) alembic_obj.source = path + with maintained_selection(): + rt.select(node) + + lib.imprint(container["instance_node"], { + "representation": str(representation["_id"]) + }) + def switch(self, container, representation): self.update(container, representation) diff --git a/openpype/hosts/max/plugins/load/load_model_fbx.py b/openpype/hosts/max/plugins/load/load_model_fbx.py index d8f4011277..88b8f1ed89 100644 --- a/openpype/hosts/max/plugins/load/load_model_fbx.py +++ b/openpype/hosts/max/plugins/load/load_model_fbx.py @@ -5,6 +5,7 @@ from openpype.pipeline import ( ) from openpype.hosts.max.api.pipeline import containerise from openpype.hosts.max.api import lib +from openpype.hosts.max.api.lib import maintained_selection class FbxModelLoader(load.LoaderPlugin): @@ -59,6 +60,9 @@ importFile @"{path}" #noPrompt using:FBXIMP """) rt.execute(fbx_reimport_cmd) + with maintained_selection(): + rt.select(node) + lib.imprint(container["instance_node"], { "representation": str(representation["_id"]) }) diff --git a/openpype/hosts/max/plugins/load/load_model_obj.py b/openpype/hosts/max/plugins/load/load_model_obj.py index 63ae058ae0..c55e462111 100644 --- a/openpype/hosts/max/plugins/load/load_model_obj.py +++ b/openpype/hosts/max/plugins/load/load_model_obj.py @@ -5,6 +5,7 @@ from openpype.pipeline import ( ) from openpype.hosts.max.api.pipeline import containerise from openpype.hosts.max.api import lib +from openpype.hosts.max.api.lib import maintained_selection class ObjLoader(load.LoaderPlugin): @@ -42,16 +43,19 @@ class ObjLoader(load.LoaderPlugin): path = get_representation_path(representation) node_name = container["instance_node"] node = rt.getNodeByName(node_name) + instance_name, _ = node_name.split("_") + container = rt.getNodeByName(instance_name) + for n in container.Children: + rt.delete(n) rt.execute(f'importFile @"{path}" #noPrompt using:ObjImp') - # create "missing" container for obj import - container = rt.container() - container.name = f"{instance_name}" # get current selection for selection in rt.getCurrentSelection(): selection.Parent = container - container.Parent = node + + with maintained_selection(): + rt.select(node) lib.imprint(node_name, { "representation": str(representation["_id"]) diff --git a/openpype/hosts/max/plugins/load/load_model_usd.py b/openpype/hosts/max/plugins/load/load_model_usd.py index 2237426187..143f91f40b 100644 --- a/openpype/hosts/max/plugins/load/load_model_usd.py +++ b/openpype/hosts/max/plugins/load/load_model_usd.py @@ -4,6 +4,7 @@ from openpype.pipeline import ( ) from openpype.hosts.max.api.pipeline import containerise from openpype.hosts.max.api import lib +from openpype.hosts.max.api.lib import maintained_selection class ModelUSDLoader(load.LoaderPlugin): @@ -60,6 +61,9 @@ class ModelUSDLoader(load.LoaderPlugin): asset = rt.getNodeByName(f"{instance_name}") asset.Parent = node + with maintained_selection(): + rt.select(node) + lib.imprint(node_name, { "representation": str(representation["_id"]) }) From a644949a29b017f2d38c6697fd73c67e0d11f1c5 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 29 Mar 2023 15:21:03 +0800 Subject: [PATCH 092/360] make sure the render dialog is close for the update of resolution and other render settings --- openpype/hosts/max/api/lib.py | 8 +++++++- openpype/hosts/max/plugins/create/create_render.py | 7 +++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index ac7d75db08..519eeffd7f 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -173,10 +173,16 @@ def set_scene_resolution(width: int, height: int): None """ + # make sure the render dialog is closed + # for the update of resolution + # Changing the Render Setup dialog settingsshould be done + # with the actual Render Setup dialog in a closed state. + if rt.renderSceneDialog.isOpen(): + rt.renderSceneDialog.close() + rt.renderWidth = width rt.renderHeight = height - def reset_scene_resolution(): """Apply the scene resolution from the project definition diff --git a/openpype/hosts/max/plugins/create/create_render.py b/openpype/hosts/max/plugins/create/create_render.py index 269fff2e32..a8720f464d 100644 --- a/openpype/hosts/max/plugins/create/create_render.py +++ b/openpype/hosts/max/plugins/create/create_render.py @@ -27,6 +27,13 @@ class CreateRender(plugin.MaxCreator): # for additional work on the node: # instance_node = rt.getNodeByName(instance.get("instance_node")) + # make sure the render dialog is closed + # for the update of resolution + # Changing the Render Setup dialog settings should be done + # with the actual Render Setup dialog in a closed state. + if rt.renderSceneDialog.isOpen(): + rt.renderSceneDialog.close() + # set viewport camera for rendering(mandatory for deadline) RenderSettings().set_render_camera(sel_obj) # set output paths for rendering(mandatory for deadline) From 0079322a6de30b70108dd5103bb04403a0730e56 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 31 Mar 2023 16:25:51 +0800 Subject: [PATCH 093/360] close the render setup dialog before the render settings --- openpype/hosts/max/api/lib_rendersettings.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/max/api/lib_rendersettings.py b/openpype/hosts/max/api/lib_rendersettings.py index 4940265a23..e7f4ee1e6b 100644 --- a/openpype/hosts/max/api/lib_rendersettings.py +++ b/openpype/hosts/max/api/lib_rendersettings.py @@ -105,6 +105,9 @@ class RenderSettings(object): rt.rendSaveFile = True + if rt.renderSceneDialog.isOpen(): + rt.renderSceneDialog.close() + def arnold_setup(self): # get Arnold RenderView run in the background # for setting up renderable camera From ee8b5e770013b42553b3c3e32aec44d37479728f Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 31 Mar 2023 16:28:41 +0800 Subject: [PATCH 094/360] cleanup --- openpype/hosts/max/plugins/create/create_render.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/max/plugins/create/create_render.py b/openpype/hosts/max/plugins/create/create_render.py index a8720f464d..68ae5eac72 100644 --- a/openpype/hosts/max/plugins/create/create_render.py +++ b/openpype/hosts/max/plugins/create/create_render.py @@ -31,8 +31,6 @@ class CreateRender(plugin.MaxCreator): # for the update of resolution # Changing the Render Setup dialog settings should be done # with the actual Render Setup dialog in a closed state. - if rt.renderSceneDialog.isOpen(): - rt.renderSceneDialog.close() # set viewport camera for rendering(mandatory for deadline) RenderSettings().set_render_camera(sel_obj) From 76c0a0266f9ea976d992718dc0c3a4a3ca0c62c3 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 3 Apr 2023 11:59:23 +0200 Subject: [PATCH 095/360] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondřej Samohel <33513211+antirotor@users.noreply.github.com> --- .../hosts/substancepainter/plugins/load/load_mesh.py | 4 ++-- .../plugins/publish/collect_textureset_images.py | 12 ++++++------ .../publish/collect_workfile_representation.py | 10 +++++----- .../plugins/publish/extract_textures.py | 2 +- .../plugins/publish/save_workfile.py | 2 +- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/openpype/hosts/substancepainter/plugins/load/load_mesh.py b/openpype/hosts/substancepainter/plugins/load/load_mesh.py index 4e800bd623..a93b830de0 100644 --- a/openpype/hosts/substancepainter/plugins/load/load_mesh.py +++ b/openpype/hosts/substancepainter/plugins/load/load_mesh.py @@ -62,7 +62,7 @@ class SubstanceLoadProjectMesh(load.LoaderPlugin): if status == substance_painter.project.ReloadMeshStatus.SUCCESS: # noqa print("Reload succeeded") else: - raise RuntimeError("Reload of mesh failed") + raise LoadError("Reload of mesh failed") path = self.fname substance_painter.project.reload_mesh(path, @@ -105,7 +105,7 @@ class SubstanceLoadProjectMesh(load.LoaderPlugin): if status == substance_painter.project.ReloadMeshStatus.SUCCESS: print("Reload succeeded") else: - raise RuntimeError("Reload of mesh failed") + raise LoaderError("Reload of mesh failed") substance_painter.project.reload_mesh(path, settings, on_mesh_reload) diff --git a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py index 14168138b6..56694614eb 100644 --- a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py +++ b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py @@ -19,7 +19,7 @@ class CollectTextureSet(pyblish.api.InstancePlugin): # TODO: Detect what source data channels end up in each file label = "Collect Texture Set images" - hosts = ['substancepainter'] + hosts = ["substancepainter"] families = ["textureSet"] order = pyblish.api.CollectorOrder @@ -55,7 +55,7 @@ class CollectTextureSet(pyblish.api.InstancePlugin): first_filepath = outputs[0]["filepath"] fnames = [os.path.basename(output["filepath"]) for output in outputs] ext = os.path.splitext(first_filepath)[1] - assert ext.lstrip('.'), f"No extension: {ext}" + assert ext.lstrip("."), f"No extension: {ext}" map_identifier = strip_template(template) # Define the suffix we want to give this particular texture @@ -78,9 +78,9 @@ class CollectTextureSet(pyblish.api.InstancePlugin): # Prepare representation representation = { - 'name': ext.lstrip("."), - 'ext': ext.lstrip("."), - 'files': fnames if len(fnames) > 1 else fnames[0], + "name": ext.lstrip("."), + "ext": ext.lstrip("."), + "files": fnames if len(fnames) > 1 else fnames[0], } # Mark as UDIM explicitly if it has UDIM tiles. @@ -105,7 +105,7 @@ class CollectTextureSet(pyblish.api.InstancePlugin): image_instance.data["subset"] = image_subset image_instance.data["family"] = "image" image_instance.data["families"] = ["image", "textures"] - image_instance.data['representations'] = [representation] + image_instance.data["representations"] = [representation] # Group the textures together in the loader image_instance.data["subsetGroup"] = instance.data["subset"] diff --git a/openpype/hosts/substancepainter/plugins/publish/collect_workfile_representation.py b/openpype/hosts/substancepainter/plugins/publish/collect_workfile_representation.py index 563c2d4c07..8d98d0b014 100644 --- a/openpype/hosts/substancepainter/plugins/publish/collect_workfile_representation.py +++ b/openpype/hosts/substancepainter/plugins/publish/collect_workfile_representation.py @@ -7,7 +7,7 @@ class CollectWorkfileRepresentation(pyblish.api.InstancePlugin): order = pyblish.api.CollectorOrder label = "Workfile representation" - hosts = ['substancepainter'] + hosts = ["substancepainter"] families = ["workfile"] def process(self, instance): @@ -18,9 +18,9 @@ class CollectWorkfileRepresentation(pyblish.api.InstancePlugin): folder, file = os.path.split(current_file) filename, ext = os.path.splitext(file) - instance.data['representations'] = [{ - 'name': ext.lstrip("."), - 'ext': ext.lstrip("."), - 'files': file, + instance.data["representations"] = [{ + "name": ext.lstrip("."), + "ext": ext.lstrip("."), + "files": file, "stagingDir": folder, }] diff --git a/openpype/hosts/substancepainter/plugins/publish/extract_textures.py b/openpype/hosts/substancepainter/plugins/publish/extract_textures.py index bd933610f4..b9654947db 100644 --- a/openpype/hosts/substancepainter/plugins/publish/extract_textures.py +++ b/openpype/hosts/substancepainter/plugins/publish/extract_textures.py @@ -15,7 +15,7 @@ class ExtractTextures(publish.Extractor, """ label = "Extract Texture Set" - hosts = ['substancepainter'] + hosts = ["substancepainter"] families = ["textureSet"] # Run before thumbnail extractors diff --git a/openpype/hosts/substancepainter/plugins/publish/save_workfile.py b/openpype/hosts/substancepainter/plugins/publish/save_workfile.py index f19deccb0e..4874b5e5c7 100644 --- a/openpype/hosts/substancepainter/plugins/publish/save_workfile.py +++ b/openpype/hosts/substancepainter/plugins/publish/save_workfile.py @@ -16,7 +16,7 @@ class SaveCurrentWorkfile(pyblish.api.ContextPlugin): def process(self, context): host = registered_host() - if context.data['currentFile'] != host.get_current_workfile(): + if context.data["currentFile"] != host.get_current_workfile(): raise KnownPublishError("Workfile has changed during publishing!") if host.has_unsaved_changes(): From 35428df6b0942e779a0bbaa50578e0c0fbfa2921 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 3 Apr 2023 12:00:51 +0200 Subject: [PATCH 096/360] Fix LoadError --- openpype/hosts/substancepainter/plugins/load/load_mesh.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/substancepainter/plugins/load/load_mesh.py b/openpype/hosts/substancepainter/plugins/load/load_mesh.py index a93b830de0..2450a9316e 100644 --- a/openpype/hosts/substancepainter/plugins/load/load_mesh.py +++ b/openpype/hosts/substancepainter/plugins/load/load_mesh.py @@ -2,6 +2,7 @@ from openpype.pipeline import ( load, get_representation_path, ) +from openpype.pipeline.load import LoadError from openpype.hosts.substancepainter.api.pipeline import ( imprint_container, set_container_metadata, @@ -105,7 +106,7 @@ class SubstanceLoadProjectMesh(load.LoaderPlugin): if status == substance_painter.project.ReloadMeshStatus.SUCCESS: print("Reload succeeded") else: - raise LoaderError("Reload of mesh failed") + raise LoadError("Reload of mesh failed") substance_painter.project.reload_mesh(path, settings, on_mesh_reload) From 5c0dee53188e12b7ddb8eec364495596b36de29c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 3 Apr 2023 12:01:24 +0200 Subject: [PATCH 097/360] Log instead of print --- openpype/hosts/substancepainter/plugins/load/load_mesh.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/substancepainter/plugins/load/load_mesh.py b/openpype/hosts/substancepainter/plugins/load/load_mesh.py index 2450a9316e..822095641d 100644 --- a/openpype/hosts/substancepainter/plugins/load/load_mesh.py +++ b/openpype/hosts/substancepainter/plugins/load/load_mesh.py @@ -61,7 +61,7 @@ class SubstanceLoadProjectMesh(load.LoaderPlugin): def on_mesh_reload(status: substance_painter.project.ReloadMeshStatus): # noqa if status == substance_painter.project.ReloadMeshStatus.SUCCESS: # noqa - print("Reload succeeded") + self.log.info("Reload succeeded") else: raise LoadError("Reload of mesh failed") @@ -104,7 +104,7 @@ class SubstanceLoadProjectMesh(load.LoaderPlugin): def on_mesh_reload(status: substance_painter.project.ReloadMeshStatus): if status == substance_painter.project.ReloadMeshStatus.SUCCESS: - print("Reload succeeded") + self.log.info("Reload succeeded") else: raise LoadError("Reload of mesh failed") From 4300939199f9cfcd4626c0bcbdafdf5a05926649 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 3 Apr 2023 12:17:48 +0200 Subject: [PATCH 098/360] Allow formatting shelf path using anatomy data --- .../hosts/substancepainter/api/pipeline.py | 39 ++++++++++++++++--- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/substancepainter/api/pipeline.py b/openpype/hosts/substancepainter/api/pipeline.py index b995c9030d..9406fb8edb 100644 --- a/openpype/hosts/substancepainter/api/pipeline.py +++ b/openpype/hosts/substancepainter/api/pipeline.py @@ -9,17 +9,23 @@ import substance_painter.ui import substance_painter.event import substance_painter.project -from openpype.host import HostBase, IWorkfileHost, ILoadHost, IPublishHost -from openpype.settings import get_current_project_settings - import pyblish.api +from openpype.host import HostBase, IWorkfileHost, ILoadHost, IPublishHost +from openpype.settings import ( + get_current_project_settings, + get_system_settings +) + +from openpype.pipeline.template_data import get_template_data_with_names from openpype.pipeline import ( register_creator_plugin_path, register_loader_plugin_path, - AVALON_CONTAINER_ID + AVALON_CONTAINER_ID, + Anatomy ) from openpype.lib import ( + StringTemplate, register_event_callback, emit_event, ) @@ -234,9 +240,32 @@ class SubstanceHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): def _install_shelves(self, project_settings): shelves = project_settings["substancepainter"].get("shelves", {}) + if not shelves: + return + + # Prepare formatting data if we detect any path which might have + # template tokens like {asset} in there. + formatting_data = {} + has_formatting_entries = any("{" in path for path in shelves.values()) + if has_formatting_entries: + project_name = self.get_current_project_name() + asset_name = self.get_current_asset_name() + task_name = self.get_current_asset_name() + system_settings = get_system_settings() + formatting_data = get_template_data_with_names(project_name, + asset_name, + task_name, + system_settings) + anatomy = Anatomy(project_name) + formatting_data["root"] = anatomy.roots + for name, path in shelves.items(): - # TODO: Allow formatting with anatomy for the paths shelf_name = None + + # Allow formatting with anatomy for the paths + if "{" in path: + path = StringTemplate.format_template(path, formatting_data) + try: shelf_name = lib.load_shelf(path, name=name) except ValueError as exc: From 9d68db0e16bc91a87f0b4fd4f7935426c70a8ffb Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 3 Apr 2023 16:03:57 +0200 Subject: [PATCH 099/360] Validate the generated output maps for missing channels --- .../plugins/create/create_textures.py | 10 +- .../publish/collect_textureset_images.py | 2 +- .../plugins/publish/extract_textures.py | 18 ++- .../plugins/publish/validate_ouput_maps.py | 108 ++++++++++++++++++ 4 files changed, 126 insertions(+), 12 deletions(-) create mode 100644 openpype/hosts/substancepainter/plugins/publish/validate_ouput_maps.py diff --git a/openpype/hosts/substancepainter/plugins/create/create_textures.py b/openpype/hosts/substancepainter/plugins/create/create_textures.py index 19133768a5..6070a06367 100644 --- a/openpype/hosts/substancepainter/plugins/create/create_textures.py +++ b/openpype/hosts/substancepainter/plugins/create/create_textures.py @@ -5,7 +5,8 @@ from openpype.pipeline import CreatedInstance, Creator, CreatorError from openpype.lib import ( EnumDef, UILabelDef, - NumberDef + NumberDef, + BoolDef ) from openpype.hosts.substancepainter.api.pipeline import ( @@ -80,6 +81,13 @@ class CreateTextures(Creator): EnumDef("exportPresetUrl", items=get_export_presets(), label="Output Template"), + BoolDef("allowSkippedMaps", + label="Allow Skipped Output Maps", + tooltip="When enabled this allows the publish to ignore " + "output maps in the used output template if one " + "or more maps are skipped due to the required " + "channels not being present in the current file.", + default=True), EnumDef("exportFileFormat", items={ None: "Based on output template", diff --git a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py index 56694614eb..50a96b94ae 100644 --- a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py +++ b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py @@ -97,7 +97,7 @@ class CollectTextureSet(pyblish.api.InstancePlugin): representation["stagingDir"] = staging_dir # Clone the instance - image_instance = context.create_instance(instance.name) + image_instance = context.create_instance(image_subset) image_instance[:] = instance[:] image_instance.data.update(copy.deepcopy(instance.data)) image_instance.data["name"] = image_subset diff --git a/openpype/hosts/substancepainter/plugins/publish/extract_textures.py b/openpype/hosts/substancepainter/plugins/publish/extract_textures.py index b9654947db..bb6f15ead9 100644 --- a/openpype/hosts/substancepainter/plugins/publish/extract_textures.py +++ b/openpype/hosts/substancepainter/plugins/publish/extract_textures.py @@ -1,6 +1,7 @@ -from openpype.pipeline import KnownPublishError, publish import substance_painter.export +from openpype.pipeline import KnownPublishError, publish + class ExtractTextures(publish.Extractor, publish.ColormanagedPyblishPluginMixin): @@ -31,21 +32,19 @@ class ExtractTextures(publish.Extractor, "Failed to export texture set: {}".format(result.message) ) + # Log what files we generated for (texture_set_name, stack_name), maps in result.textures.items(): # Log our texture outputs - self.log.info(f"Processing stack: {texture_set_name} {stack_name}") + self.log.info(f"Exported stack: {texture_set_name} {stack_name}") for texture_map in maps: self.log.info(f"Exported texture: {texture_map}") - # TODO: Confirm outputs match what we collected - # TODO: Confirm the files indeed exist - # TODO: make sure representations are registered - # We'll insert the color space data for each image instance that we # added into this texture set. The collector couldn't do so because # some anatomy and other instance data needs to be collected prior context = instance.context for image_instance in instance: + representation = next(iter(image_instance.data["representations"])) colorspace = image_instance.data.get("colorspace") if not colorspace: @@ -53,10 +52,9 @@ class ExtractTextures(publish.Extractor, f"{image_instance}") continue - for representation in image_instance.data["representations"]: - self.set_representation_colorspace(representation, - context=context, - colorspace=colorspace) + self.set_representation_colorspace(representation, + context=context, + colorspace=colorspace) # The TextureSet instance should not be integrated. It generates no # output data. Instead the separated texture instances are generated diff --git a/openpype/hosts/substancepainter/plugins/publish/validate_ouput_maps.py b/openpype/hosts/substancepainter/plugins/publish/validate_ouput_maps.py new file mode 100644 index 0000000000..203cf7c5fe --- /dev/null +++ b/openpype/hosts/substancepainter/plugins/publish/validate_ouput_maps.py @@ -0,0 +1,108 @@ +import copy +import os + +import pyblish.api + +import substance_painter.export + +from openpype.pipeline import PublishValidationError + + +class ValidateOutputMaps(pyblish.api.InstancePlugin): + """Validate all output maps for Output Template are generated. + + Output maps will be skipped by Substance Painter if it is an output + map in the Substance Output Template which uses channels that the current + substance painter project has not painted or generated. + + """ + + order = pyblish.api.ValidatorOrder + label = "Validate output maps" + hosts = ["substancepainter"] + families = ["textureSet"] + + def process(self, instance): + + config = instance.data["exportConfig"] + + # Substance Painter API does not allow to query the actual output maps + # it will generate without actually exporting the files. So we try to + # generate the smallest size / fastest export as possible + config = copy.deepcopy(config) + parameters = config["exportParameters"][0]["parameters"] + parameters["sizeLog2"] = [1, 1] # output 2x2 images (smallest) + parameters["paddingAlgorithm"] = "passthrough" # no dilation (faster) + parameters["dithering"] = False # no dithering (faster) + config["exportParameters"][0]["parameters"]["sizeLog2"] = [1, 1] + + result = substance_painter.export.export_project_textures(config) + if result.status != substance_painter.export.ExportStatus.Success: + raise PublishValidationError( + "Failed to export texture set: {}".format(result.message) + ) + + generated_files = set() + for texture_maps in result.textures.values(): + for texture_map in texture_maps: + generated_files.add(os.path.normpath(texture_map)) + # Directly clean up our temporary export + os.remove(texture_map) + + creator_attributes = instance.data.get("creator_attributes", {}) + allow_skipped_maps = creator_attributes.get("allowSkippedMaps", True) + error_report_missing = [] + for image_instance in instance: + + # Confirm whether the instance has its expected files generated. + # We assume there's just one representation and that it is + # the actual texture representation from the collector. + representation = next(iter(image_instance.data["representations"])) + staging_dir = representation["stagingDir"] + filenames = representation["files"] + if not isinstance(filenames, (list, tuple)): + # Convert single file to list + filenames = [filenames] + + missing = [] + for filename in filenames: + filepath = os.path.join(staging_dir, filename) + filepath = os.path.normpath(filepath) + if filepath not in generated_files: + self.log.warning(f"Missing texture: {filepath}") + missing.append(filepath) + + if allow_skipped_maps: + # TODO: This is changing state on the instance's which + # usually should not be done during validation. + self.log.warning(f"Disabling texture instance: " + f"{image_instance}") + image_instance.data["active"] = False + image_instance.data["integrate"] = False + representation.setdefault("tags", []).append("delete") + continue + + if missing: + error_report_missing.append((image_instance, missing)) + + if error_report_missing: + + message = ( + "The Texture Set skipped exporting some output maps which are " + "defined in the Output Template. This happens if the Output " + "Templates exports maps from channels which you do not " + "have in your current Substance Painter project.\n\n" + "To allow this enable the *Allow Skipped Output Maps* setting " + "on the instance.\n\n" + f"Instance {instance} skipped exporting output maps:\n" + "" + ) + + for image_instance, missing in error_report_missing: + missing_str = ", ".join(missing) + message += f"- **{image_instance}** skipped: {missing_str}\n" + + raise PublishValidationError( + message=message, + title="Missing output maps" + ) From 23568e5b060caff2a56d65ba3229cc74f588b62c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 4 Apr 2023 00:11:49 +0200 Subject: [PATCH 100/360] Fix allow skipped maps logic --- .../plugins/publish/validate_ouput_maps.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/substancepainter/plugins/publish/validate_ouput_maps.py b/openpype/hosts/substancepainter/plugins/publish/validate_ouput_maps.py index 203cf7c5fe..e3d4c733e1 100644 --- a/openpype/hosts/substancepainter/plugins/publish/validate_ouput_maps.py +++ b/openpype/hosts/substancepainter/plugins/publish/validate_ouput_maps.py @@ -72,17 +72,19 @@ class ValidateOutputMaps(pyblish.api.InstancePlugin): self.log.warning(f"Missing texture: {filepath}") missing.append(filepath) + if not missing: + continue + if allow_skipped_maps: # TODO: This is changing state on the instance's which - # usually should not be done during validation. + # should not be done during validation. self.log.warning(f"Disabling texture instance: " f"{image_instance}") image_instance.data["active"] = False image_instance.data["integrate"] = False representation.setdefault("tags", []).append("delete") continue - - if missing: + else: error_report_missing.append((image_instance, missing)) if error_report_missing: From 5059cf74b5bddfa85b4b9157fd2ffe7f346cc203 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 4 Apr 2023 00:13:50 +0200 Subject: [PATCH 101/360] Support multiple texture sets + stacks --- .../publish/collect_textureset_images.py | 33 +++++++++++++++---- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py index 50a96b94ae..d11abd1019 100644 --- a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py +++ b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py @@ -41,10 +41,12 @@ class CollectTextureSet(pyblish.api.InstancePlugin): for template, outputs in template_maps.items(): self.log.info(f"Processing {template}") self.create_image_instance(instance, template, outputs, - asset_doc=asset_doc) + asset_doc=asset_doc, + texture_set_name=texture_set_name, + stack_name=stack_name) def create_image_instance(self, instance, template, outputs, - asset_doc): + asset_doc, texture_set_name, stack_name): """Create a new instance per image or UDIM sequence. The new instances will be of family `image`. @@ -56,14 +58,27 @@ class CollectTextureSet(pyblish.api.InstancePlugin): fnames = [os.path.basename(output["filepath"]) for output in outputs] ext = os.path.splitext(first_filepath)[1] assert ext.lstrip("."), f"No extension: {ext}" - map_identifier = strip_template(template) + + always_include_texture_set_name = False # todo: make this configurable + all_texture_sets = substance_painter.textureset.all_texture_sets() + texture_set = substance_painter.textureset.TextureSet.from_name( + texture_set_name + ) # Define the suffix we want to give this particular texture # set and set up a remapped subset naming for it. - # TODO (Critical) Support needs to be added to have multiple materials - # with each their own maps. So we might need to include the - # material or alike in the variant suffix too? - suffix = f".{map_identifier}" + suffix = "" + if always_include_texture_set_name or len(all_texture_sets) > 1: + # More than one texture set, include texture set name + suffix += f".{texture_set_name}" + if texture_set.is_layered_material() and stack_name: + # More than one stack, include stack name + suffix += f".{stack_name}" + + # Always include the map identifier + map_identifier = strip_template(template) + suffix += f".{map_identifier}" + image_subset = get_subset_name( # TODO: The family actually isn't 'texture' currently but for now # this is only done so the subset name starts with 'texture' @@ -110,6 +125,10 @@ class CollectTextureSet(pyblish.api.InstancePlugin): # Group the textures together in the loader image_instance.data["subsetGroup"] = instance.data["subset"] + # Store the texture set name and stack name on the instance + image_instance.data["textureSetName"] = texture_set_name + image_instance.data["textureStackName"] = stack_name + # Store color space with the instance # Note: The extractor will assign it to the representation colorspace = outputs[0].get("colorSpace") From 359b039388dc68cd88893efd80b0e5db5be0eaab Mon Sep 17 00:00:00 2001 From: Joseff Date: Wed, 5 Apr 2023 13:37:57 +0200 Subject: [PATCH 102/360] Removed the previous Module OpenPype in plugin (OPPublishInstance still remains), Created new UE 5.1 ver. --- openpype/hosts/unreal/addon.py | 5 +- .../unreal/hooks/pre_workfile_preparation.py | 2 +- .../OpenPype => UE_4.27/Ayon}/.gitignore | 0 .../Ayon/Ayon.uplugin} | 5 - .../Ayon}/Config/DefaultAyonSettings.ini | 0 .../Ayon}/Config/FilterPlugin.ini | 0 .../Ayon}/Content/Python/init_unreal.py | 2 +- .../OpenPype => UE_4.27/Ayon}/README.md | 0 .../Ayon}/Resources/ayon128.png | Bin .../Ayon}/Resources/ayon40.png | Bin .../Ayon}/Resources/ayon512.png | Bin .../Ayon}/Source/Ayon/Ayon.Build.cs | 0 .../Ayon}/Source/Ayon/Private/Ayon.cpp | 0 .../Ayon/Private/AyonAssetContainer.cpp | 0 .../Private/AyonAssetContainerFactory.cpp | 0 .../Ayon}/Source/Ayon/Private/AyonLib.cpp | 0 .../Source/Ayon/Private/AyonPythonBridge.cpp | 0 .../Source/Ayon/Private/AyonSettings.cpp | 0 .../Ayon}/Source/Ayon/Private/AyonStyle.cpp | 2 +- .../Private/Commandlets/AyonActionResult.cpp | 0 .../AyonGenerateProjectCommandlet.cpp | 0 .../Ayon}/Private/OpenPypePublishInstance.cpp | 12 +- .../OpenPypePublishInstanceFactory.cpp | 0 .../Ayon}/Source/Ayon/Public/Ayon.h | 0 .../Source/Ayon/Public/AyonAssetContainer.h | 0 .../Ayon/Public/AyonAssetContainerFactory.h | 0 .../Ayon}/Source/Ayon/Public/AyonConstants.h | 0 .../Ayon}/Source/Ayon/Public/AyonLib.h | 0 .../Source/Ayon/Public/AyonPythonBridge.h | 0 .../Ayon}/Source/Ayon/Public/AyonSettings.h | 2 +- .../Ayon}/Source/Ayon/Public/AyonStyle.h | 0 .../Public/Commandlets/AyonActionResult.h | 0 .../AyonGenerateProjectCommandlet.h | 0 .../Source/Ayon/Public/Logging/Ayon_Log.h | 0 .../Ayon}/Public/OpenPypePublishInstance.h | 2 +- .../Public/OpenPypePublishInstanceFactory.h | 2 +- .../CommandletProject/.gitignore | 0 .../CommandletProject.uproject | 2 +- .../Config/DefaultOpenPypeSettings.ini | 2 - .../UE_4.7/OpenPype/Resources/openpype128.png | Bin 14594 -> 0 bytes .../UE_4.7/OpenPype/Resources/openpype40.png | Bin 4884 -> 0 bytes .../UE_4.7/OpenPype/Resources/openpype512.png | Bin 85856 -> 0 bytes .../Ayon/Private/AyonPublishInstance.cpp | 201 ------------------ .../Private/AyonPublishInstanceFactory.cpp | 21 -- .../Source/OpenPype/OpenPype.Build.cs | 59 ----- .../OpenPype/Private/AssetContainer.cpp | 115 ---------- .../Private/AssetContainerFactory.cpp | 20 -- .../OPGenerateProjectCommandlet.cpp | 141 ------------ .../Private/Commandlets/OPActionResult.cpp | 41 ---- .../OpenPype/Private/Logging/OP_Log.cpp | 1 - .../Source/OpenPype/Private/OpenPype.cpp | 155 -------------- .../OpenPype/Private/OpenPypePythonBridge.cpp | 14 -- .../OpenPype/Private/OpenPypeSettings.cpp | 20 -- .../Source/OpenPype/Private/OpenPypeStyle.cpp | 70 ------ .../Source/OpenPype/Public/AssetContainer.h | 39 ---- .../OpenPype/Public/AssetContainerFactory.h | 21 -- .../OPGenerateProjectCommandlet.h | 60 ------ .../Source/OpenPype/Public/Logging/OP_Log.h | 4 - .../Source/OpenPype/Public/OpenPype.h | 22 -- .../Source/OpenPype/Public/OpenPypeLib.h | 20 -- .../Source/OpenPype/Public/OpenPypeSettings.h | 31 --- .../Source/OpenPype/Public/OpenPypeStyle.h | 23 -- .../UE_5.0/{OpenPype => Ayon}/.gitignore | 0 .../OpenPype.uplugin => Ayon/Ayon.uplugin} | 5 - .../Config/DefaultAyonSettings.ini | 0 .../Config/FilterPlugin.ini | 0 .../Ayon}/Content/Python/init_unreal.py | 2 +- .../UE_5.0/{OpenPype => Ayon}/README.md | 0 .../{OpenPype => Ayon}/Resources/ayon128.png | Bin .../{OpenPype => Ayon}/Resources/ayon40.png | Bin .../{OpenPype => Ayon}/Resources/ayon512.png | Bin .../Source/Ayon/Ayon.Build.cs | 0 .../Source/Ayon/Private/Ayon.cpp | 0 .../Ayon/Private/AyonAssetContainer.cpp | 0 .../Private/AyonAssetContainerFactory.cpp | 0 .../Source/Ayon/Private/AyonCommands.cpp | 0 .../Source/Ayon/Private/AyonLib.cpp | 0 .../Source/Ayon/Private/AyonPythonBridge.cpp | 0 .../Source/Ayon/Private/AyonSettings.cpp | 0 .../Source/Ayon/Private/AyonStyle.cpp | 2 +- .../Private/Commandlets/AyonActionResult.cpp | 0 .../AyonGenerateProjectCommandlet.cpp | 0 .../Ayon}/Private/OpenPypePublishInstance.cpp | 10 +- .../OpenPypePublishInstanceFactory.cpp | 0 .../Source/Ayon/Public/Ayon.h | 0 .../Source/Ayon/Public/AyonAssetContainer.h | 0 .../Ayon/Public/AyonAssetContainerFactory.h | 0 .../Source/Ayon/Public/AyonCommands.h | 0 .../Source/Ayon/Public/AyonConstants.h | 0 .../Source/Ayon/Public/AyonLib.h | 0 .../Source/Ayon/Public/AyonPythonBridge.h | 0 .../Source/Ayon/Public/AyonSettings.h | 2 +- .../Source/Ayon/Public/AyonStyle.h | 0 .../Public/Commandlets/AyonActionResult.h | 0 .../AyonGenerateProjectCommandlet.h | 0 .../Source/Ayon/Public/Logging/Ayon_Log.h | 0 .../Ayon}/Public/OpenPypePublishInstance.h | 2 +- .../Public/OpenPypePublishInstanceFactory.h | 2 +- .../CommandletProject.uproject | 2 +- .../Config/DefaultOpenPypeSettings.ini | 2 - .../UE_5.0/OpenPype/Resources/openpype128.png | Bin 14594 -> 0 bytes .../UE_5.0/OpenPype/Resources/openpype40.png | Bin 4884 -> 0 bytes .../UE_5.0/OpenPype/Resources/openpype512.png | Bin 85856 -> 0 bytes .../Private/AyonPublishInstanceFactory.cpp | 21 -- .../Source/Ayon/Public/AyonPublishInstance.h | 102 --------- .../Ayon/Public/AyonPublishInstanceFactory.h | 20 -- .../Private/AssetContainerFactory.cpp | 20 -- .../OPGenerateProjectCommandlet.cpp | 141 ------------ .../Private/Commandlets/OPActionResult.cpp | 40 ---- .../OpenPype/Private/Logging/OP_Log.cpp | 3 - .../OpenPype/Private/OpenPypeCommands.cpp | 13 -- .../Source/OpenPype/Private/OpenPypeLib.cpp | 53 ----- .../OpenPype/Private/OpenPypePythonBridge.cpp | 14 -- .../OpenPype/Private/OpenPypeSettings.cpp | 21 -- .../Source/OpenPype/Private/OpenPypeStyle.cpp | 63 ------ .../Public/Commandlets/OPActionResult.h | 83 -------- .../Source/OpenPype/Public/Logging/OP_Log.h | 4 - .../Source/OpenPype/Public/OPConstants.h | 13 -- .../Source/OpenPype/Public/OpenPypeCommands.h | 24 --- .../OpenPype/Public/OpenPypePythonBridge.h | 21 -- .../unreal/integration/UE_5.1/Ayon/.gitignore | 35 +++ .../integration/UE_5.1/Ayon/Ayon.uplugin | 24 +++ .../Ayon/Config/DefaultAyonSettings.ini | 2 + .../UE_5.1/Ayon/Config/FilterPlugin.ini | 8 + .../UE_5.1/Ayon/Content/Python/init_unreal.py | 30 +++ .../unreal/integration/UE_5.1/Ayon/README.md | 11 + .../UE_5.1/Ayon/Resources/ayon128.png | Bin 0 -> 2358 bytes .../UE_5.1/Ayon/Resources/ayon40.png | Bin 0 -> 721 bytes .../UE_5.1/Ayon/Resources/ayon512.png | Bin 0 -> 16705 bytes .../Ayon/Source/Ayon/Ayon.Build.cs} | 4 +- .../Ayon/Source/Ayon/Private/Ayon.cpp} | 75 ++++--- .../Ayon/Private/AyonAssetContainer.cpp} | 35 ++- .../Private/AyonAssetContainerFactory.cpp | 20 ++ .../Ayon/Source/Ayon/Private/AyonCommands.cpp | 13 ++ .../Ayon/Source/Ayon/Private/AyonLib.cpp} | 10 +- .../Source/Ayon/Private/AyonPythonBridge.cpp | 14 ++ .../Ayon/Source/Ayon/Private/AyonSettings.cpp | 21 ++ .../Ayon/Source/Ayon/Private/AyonStyle.cpp | 62 ++++++ .../Private/Commandlets/AyonActionResult.cpp | 40 ++++ .../AyonGenerateProjectCommandlet.cpp | 140 ++++++++++++ .../Ayon/Private/OpenPypePublishInstance.cpp} | 47 ++-- .../OpenPypePublishInstanceFactory.cpp | 21 ++ .../Ayon/Source/Ayon/Public/Ayon.h} | 3 +- .../Source/Ayon/Public/AyonAssetContainer.h} | 11 +- .../Ayon/Public/AyonAssetContainerFactory.h} | 9 +- .../Ayon/Source/Ayon/Public/AyonCommands.h | 24 +++ .../Ayon/Source/Ayon/Public/AyonConstants.h} | 4 +- .../Ayon/Source/Ayon/Public/AyonLib.h} | 5 +- .../Source/Ayon/Public/AyonPythonBridge.h} | 7 +- .../Ayon/Source/Ayon/Public/AyonSettings.h} | 10 +- .../Ayon/Source/Ayon/Public/AyonStyle.h} | 4 +- .../Public/Commandlets/AyonActionResult.h} | 32 +-- .../AyonGenerateProjectCommandlet.h} | 22 +- .../Source/Ayon/Public/Logging/Ayon_Log.h | 4 + .../Ayon/Public/OpenPypePublishInstance.h} | 9 +- .../Public/OpenPypePublishInstanceFactory.h} | 6 +- .../UE_5.1/CommandletProject/.gitignore | 41 ++++ .../CommandletProject.uproject | 20 ++ openpype/hosts/unreal/lib.py | 15 +- openpype/hosts/unreal/ue_workers.py | 8 +- 160 files changed, 708 insertions(+), 1939 deletions(-) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_4.27/Ayon}/.gitignore (100%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype/OpenPype.uplugin => UE_4.27/Ayon/Ayon.uplugin} (86%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_4.27/Ayon}/Config/DefaultAyonSettings.ini (100%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_4.27/Ayon}/Config/FilterPlugin.ini (100%) rename openpype/hosts/unreal/integration/{UE_5.0/OpenPype => UE_4.27/Ayon}/Content/Python/init_unreal.py (93%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_4.27/Ayon}/README.md (100%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_4.27/Ayon}/Resources/ayon128.png (100%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_4.27/Ayon}/Resources/ayon40.png (100%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_4.27/Ayon}/Resources/ayon512.png (100%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_4.27/Ayon}/Source/Ayon/Ayon.Build.cs (100%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_4.27/Ayon}/Source/Ayon/Private/Ayon.cpp (100%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_4.27/Ayon}/Source/Ayon/Private/AyonAssetContainer.cpp (100%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_4.27/Ayon}/Source/Ayon/Private/AyonAssetContainerFactory.cpp (100%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_4.27/Ayon}/Source/Ayon/Private/AyonLib.cpp (100%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_4.27/Ayon}/Source/Ayon/Private/AyonPythonBridge.cpp (100%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_4.27/Ayon}/Source/Ayon/Private/AyonSettings.cpp (100%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_4.27/Ayon}/Source/Ayon/Private/AyonStyle.cpp (98%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_4.27/Ayon}/Source/Ayon/Private/Commandlets/AyonActionResult.cpp (100%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_4.27/Ayon}/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp (100%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype/Source/OpenPype => UE_4.27/Ayon/Source/Ayon}/Private/OpenPypePublishInstance.cpp (94%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype/Source/OpenPype => UE_4.27/Ayon/Source/Ayon}/Private/OpenPypePublishInstanceFactory.cpp (100%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_4.27/Ayon}/Source/Ayon/Public/Ayon.h (100%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_4.27/Ayon}/Source/Ayon/Public/AyonAssetContainer.h (100%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_4.27/Ayon}/Source/Ayon/Public/AyonAssetContainerFactory.h (100%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_4.27/Ayon}/Source/Ayon/Public/AyonConstants.h (100%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_4.27/Ayon}/Source/Ayon/Public/AyonLib.h (100%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_4.27/Ayon}/Source/Ayon/Public/AyonPythonBridge.h (100%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_4.27/Ayon}/Source/Ayon/Public/AyonSettings.h (89%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_4.27/Ayon}/Source/Ayon/Public/AyonStyle.h (100%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_4.27/Ayon}/Source/Ayon/Public/Commandlets/AyonActionResult.h (100%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_4.27/Ayon}/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h (100%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_4.27/Ayon}/Source/Ayon/Public/Logging/Ayon_Log.h (100%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype/Source/OpenPype => UE_4.27/Ayon/Source/Ayon}/Public/OpenPypePublishInstance.h (97%) rename openpype/hosts/unreal/integration/{UE_5.0/OpenPype/Source/OpenPype => UE_4.27/Ayon/Source/Ayon}/Public/OpenPypePublishInstanceFactory.h (88%) rename openpype/hosts/unreal/integration/{UE_4.7 => UE_4.27}/CommandletProject/.gitignore (100%) rename openpype/hosts/unreal/integration/{UE_4.7 => UE_4.27}/CommandletProject/CommandletProject.uproject (85%) delete mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Config/DefaultOpenPypeSettings.ini delete mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Resources/openpype128.png delete mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Resources/openpype40.png delete mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Resources/openpype512.png delete mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonPublishInstance.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonPublishInstanceFactory.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/OpenPype.Build.cs delete mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/AssetContainer.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/AssetContainerFactory.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/Commandlets/Implementations/OPGenerateProjectCommandlet.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/Commandlets/OPActionResult.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/Logging/OP_Log.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPype.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypePythonBridge.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypeSettings.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypeStyle.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/AssetContainer.h delete mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/AssetContainerFactory.h delete mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/Commandlets/Implementations/OPGenerateProjectCommandlet.h delete mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/Logging/OP_Log.h delete mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPype.h delete mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypeLib.h delete mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypeSettings.h delete mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypeStyle.h rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/.gitignore (100%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype/OpenPype.uplugin => Ayon/Ayon.uplugin} (87%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/Config/DefaultAyonSettings.ini (100%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/Config/FilterPlugin.ini (100%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_5.0/Ayon}/Content/Python/init_unreal.py (93%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/README.md (100%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/Resources/ayon128.png (100%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/Resources/ayon40.png (100%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/Resources/ayon512.png (100%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/Source/Ayon/Ayon.Build.cs (100%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/Source/Ayon/Private/Ayon.cpp (100%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/Source/Ayon/Private/AyonAssetContainer.cpp (100%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/Source/Ayon/Private/AyonAssetContainerFactory.cpp (100%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/Source/Ayon/Private/AyonCommands.cpp (100%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/Source/Ayon/Private/AyonLib.cpp (100%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/Source/Ayon/Private/AyonPythonBridge.cpp (100%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/Source/Ayon/Private/AyonSettings.cpp (100%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/Source/Ayon/Private/AyonStyle.cpp (93%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/Source/Ayon/Private/Commandlets/AyonActionResult.cpp (100%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp (100%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype/Source/OpenPype => Ayon/Source/Ayon}/Private/OpenPypePublishInstance.cpp (95%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype/Source/OpenPype => Ayon/Source/Ayon}/Private/OpenPypePublishInstanceFactory.cpp (100%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/Source/Ayon/Public/Ayon.h (100%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/Source/Ayon/Public/AyonAssetContainer.h (100%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/Source/Ayon/Public/AyonAssetContainerFactory.h (100%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/Source/Ayon/Public/AyonCommands.h (100%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/Source/Ayon/Public/AyonConstants.h (100%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/Source/Ayon/Public/AyonLib.h (100%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/Source/Ayon/Public/AyonPythonBridge.h (100%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/Source/Ayon/Public/AyonSettings.h (90%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/Source/Ayon/Public/AyonStyle.h (100%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/Source/Ayon/Public/Commandlets/AyonActionResult.h (100%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h (100%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/Source/Ayon/Public/Logging/Ayon_Log.h (100%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype/Source/OpenPype => Ayon/Source/Ayon}/Public/OpenPypePublishInstance.h (97%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype/Source/OpenPype => UE_5.0/Ayon/Source/Ayon}/Public/OpenPypePublishInstanceFactory.h (88%) delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Config/DefaultOpenPypeSettings.ini delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Resources/openpype128.png delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Resources/openpype40.png delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Resources/openpype512.png delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonPublishInstanceFactory.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonPublishInstance.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonPublishInstanceFactory.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/AssetContainerFactory.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/Commandlets/Implementations/OPGenerateProjectCommandlet.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/Commandlets/OPActionResult.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/Logging/OP_Log.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeCommands.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeLib.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypePythonBridge.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeSettings.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeStyle.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/Commandlets/OPActionResult.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/Logging/OP_Log.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OPConstants.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeCommands.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypePythonBridge.h create mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/.gitignore create mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Ayon.uplugin create mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Config/DefaultAyonSettings.ini create mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Config/FilterPlugin.ini create mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Content/Python/init_unreal.py create mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/README.md create mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Resources/ayon128.png create mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Resources/ayon40.png create mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Resources/ayon512.png rename openpype/hosts/unreal/integration/{UE_5.0/OpenPype/Source/OpenPype/OpenPype.Build.cs => UE_5.1/Ayon/Source/Ayon/Ayon.Build.cs} (93%) rename openpype/hosts/unreal/integration/{UE_5.0/OpenPype/Source/OpenPype/Private/OpenPype.cpp => UE_5.1/Ayon/Source/Ayon/Private/Ayon.cpp} (57%) rename openpype/hosts/unreal/integration/{UE_5.0/OpenPype/Source/OpenPype/Private/AssetContainer.cpp => UE_5.1/Ayon/Source/Ayon/Private/AyonAssetContainer.cpp} (71%) create mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonAssetContainerFactory.cpp create mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonCommands.cpp rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypeLib.cpp => UE_5.1/Ayon/Source/Ayon/Private/AyonLib.cpp} (79%) create mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonPythonBridge.cpp create mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonSettings.cpp create mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonStyle.cpp create mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/Commandlets/AyonActionResult.cpp create mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp rename openpype/hosts/unreal/integration/{UE_5.0/OpenPype/Source/Ayon/Private/AyonPublishInstance.cpp => UE_5.1/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp} (72%) create mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp rename openpype/hosts/unreal/integration/{UE_5.0/OpenPype/Source/OpenPype/Public/OpenPype.h => UE_5.1/Ayon/Source/Ayon/Public/Ayon.h} (81%) rename openpype/hosts/unreal/integration/{UE_5.0/OpenPype/Source/OpenPype/Public/AssetContainer.h => UE_5.1/Ayon/Source/Ayon/Public/AyonAssetContainer.h} (80%) rename openpype/hosts/unreal/integration/{UE_5.0/OpenPype/Source/OpenPype/Public/AssetContainerFactory.h => UE_5.1/Ayon/Source/Ayon/Public/AyonAssetContainerFactory.h} (68%) create mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonCommands.h rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype/Source/OpenPype/Public/OPConstants.h => UE_5.1/Ayon/Source/Ayon/Public/AyonConstants.h} (83%) rename openpype/hosts/unreal/integration/{UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeLib.h => UE_5.1/Ayon/Source/Ayon/Public/AyonLib.h} (75%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypePythonBridge.h => UE_5.1/Ayon/Source/Ayon/Public/AyonPythonBridge.h} (70%) rename openpype/hosts/unreal/integration/{UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeSettings.h => UE_5.1/Ayon/Source/Ayon/Public/AyonSettings.h} (59%) rename openpype/hosts/unreal/integration/{UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeStyle.h => UE_5.1/Ayon/Source/Ayon/Public/AyonStyle.h} (79%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype/Source/OpenPype/Public/Commandlets/OPActionResult.h => UE_5.1/Ayon/Source/Ayon/Public/Commandlets/AyonActionResult.h} (63%) rename openpype/hosts/unreal/integration/{UE_5.0/OpenPype/Source/OpenPype/Public/Commandlets/Implementations/OPGenerateProjectCommandlet.h => UE_5.1/Ayon/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h} (63%) create mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Logging/Ayon_Log.h rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype/Source/Ayon/Public/AyonPublishInstance.h => UE_5.1/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h} (95%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype/Source/Ayon/Public/AyonPublishInstanceFactory.h => UE_5.1/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h} (65%) create mode 100644 openpype/hosts/unreal/integration/UE_5.1/CommandletProject/.gitignore create mode 100644 openpype/hosts/unreal/integration/UE_5.1/CommandletProject/CommandletProject.uproject diff --git a/openpype/hosts/unreal/addon.py b/openpype/hosts/unreal/addon.py index 24e2db975d..2fb55a9b11 100644 --- a/openpype/hosts/unreal/addon.py +++ b/openpype/hosts/unreal/addon.py @@ -15,10 +15,11 @@ class UnrealAddon(OpenPypeModule, IHostAddon): """Modify environments to contain all required for implementation.""" # Set OPENPYPE_UNREAL_PLUGIN required for Unreal implementation - ue_plugin = "UE_5.0" if app.name[:1] == "5" else "UE_4.7" + ue_version = app.name.replace("-",".") unreal_plugin_path = os.path.join( - UNREAL_ROOT_DIR, "integration", ue_plugin, "OpenPype" + UNREAL_ROOT_DIR, "integration", f"UE_{ue_version}", "Ayon" ) + if not env.get("OPENPYPE_UNREAL_PLUGIN") or \ env.get("OPENPYPE_UNREAL_PLUGIN") != unreal_plugin_path: env["OPENPYPE_UNREAL_PLUGIN"] = unreal_plugin_path diff --git a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py index da12bc75de..80ed946ec1 100644 --- a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py +++ b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py @@ -210,7 +210,7 @@ class UnrealPrelaunchHook(PreLaunchHook): if self.launch_context.env.get("OPENPYPE_UNREAL_PLUGIN"): self.log.info(( - f"{self.signature} using OpenPype plugin from " + f"{self.signature} using Ayon plugin from " f"{self.launch_context.env.get('OPENPYPE_UNREAL_PLUGIN')}" )) env_key = "OPENPYPE_UNREAL_PLUGIN" diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/.gitignore b/openpype/hosts/unreal/integration/UE_4.27/Ayon/.gitignore similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/.gitignore rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/.gitignore diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/OpenPype.uplugin b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Ayon.uplugin similarity index 86% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/OpenPype.uplugin rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Ayon.uplugin index 37bb170eb4..299a5edc6a 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/OpenPype.uplugin +++ b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Ayon.uplugin @@ -14,11 +14,6 @@ "CanContainContent": true, "Installed": true, "Modules": [ - { - "Name": "OpenPype", - "Type": "Editor", - "LoadingPhase": "Default" - }, { "Name": "Ayon", "Type": "Editor", diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Config/DefaultAyonSettings.ini b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Config/DefaultAyonSettings.ini similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Config/DefaultAyonSettings.ini rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Config/DefaultAyonSettings.ini diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Config/FilterPlugin.ini b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Config/FilterPlugin.ini similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Config/FilterPlugin.ini rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Config/FilterPlugin.ini diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/init_unreal.py b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Content/Python/init_unreal.py similarity index 93% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/init_unreal.py rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Content/Python/init_unreal.py index b85f970699..9ed5a2cb19 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/init_unreal.py +++ b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Content/Python/init_unreal.py @@ -16,7 +16,7 @@ if openpype_detected: @unreal.uclass() -class OpenPypeIntegration(unreal.OpenPypePythonBridge): +class AyonIntegration(unreal.AyonPythonBridge): @unreal.ufunction(override=True) def RunInPython_Popup(self): unreal.log_warning("OpenPype: showing tools popup") diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/README.md b/openpype/hosts/unreal/integration/UE_4.27/Ayon/README.md similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/README.md rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/README.md diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Resources/ayon128.png b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Resources/ayon128.png similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Resources/ayon128.png rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Resources/ayon128.png diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Resources/ayon40.png b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Resources/ayon40.png similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Resources/ayon40.png rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Resources/ayon40.png diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Resources/ayon512.png b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Resources/ayon512.png similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Resources/ayon512.png rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Resources/ayon512.png diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Ayon.Build.cs b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Ayon.Build.cs similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Ayon.Build.cs rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Ayon.Build.cs diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/Ayon.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/Ayon.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/Ayon.cpp rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/Ayon.cpp diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonAssetContainer.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonAssetContainer.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonAssetContainer.cpp rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonAssetContainer.cpp diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonAssetContainerFactory.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonAssetContainerFactory.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonAssetContainerFactory.cpp rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonAssetContainerFactory.cpp diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonLib.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonLib.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonLib.cpp rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonLib.cpp diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonPythonBridge.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonPythonBridge.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonPythonBridge.cpp rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonPythonBridge.cpp diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonSettings.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonSettings.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonSettings.cpp rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonSettings.cpp diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonStyle.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonStyle.cpp similarity index 98% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonStyle.cpp rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonStyle.cpp index dc8f0f1f40..b133225fd5 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonStyle.cpp +++ b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonStyle.cpp @@ -44,7 +44,7 @@ const FVector2D Icon40x40(40.0f, 40.0f); TUniquePtr< FSlateStyleSet > FAyonStyle::Create() { TUniquePtr< FSlateStyleSet > Style = MakeUnique(GetStyleSetName()); - Style->SetContentRoot(FPaths::EnginePluginsDir() / TEXT("Marketplace/OpenPype/Resources")); + Style->SetContentRoot(FPaths::EnginePluginsDir() / TEXT("Marketplace/Ayon/Resources")); return Style; } diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/Commandlets/AyonActionResult.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/Commandlets/AyonActionResult.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/Commandlets/AyonActionResult.cpp rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/Commandlets/AyonActionResult.cpp diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypePublishInstance.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp similarity index 94% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypePublishInstance.cpp rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp index 05638fbd0b..548bc4c399 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypePublishInstance.cpp +++ b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp @@ -3,8 +3,8 @@ #include "OpenPypePublishInstance.h" #include "AssetRegistryModule.h" -#include "OpenPypeLib.h" -#include "OpenPypeSettings.h" +#include "AyonLib.h" +#include "AyonSettings.h" #include "Framework/Notifications/NotificationManager.h" #include "Widgets/Notifications/SNotificationList.h" @@ -124,12 +124,12 @@ void UOpenPypePublishInstance::ColorOpenPypeDirs() PathName.RemoveFromEnd(PathRight, ESearchCase::CaseSensitive); //Get the current settings - const UOpenPypeSettings* Settings = GetMutableDefault(); + const UAyonSettings* Settings = GetMutableDefault(); //Color the base folder - UOpenPypeLib::SetFolderColor(PathName, Settings->GetFolderFColor(), false); + UAyonLib::SetFolderColor(PathName, Settings->GetFolderFColor(), false); - //Get Sub paths, iterate through them and color them according to the folder color in UOpenPypeSettings + //Get Sub paths, iterate through them and color them according to the folder color in UAyonSettings const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked( "AssetRegistry"); @@ -141,7 +141,7 @@ void UOpenPypePublishInstance::ColorOpenPypeDirs() { for (const FString& Path : PathList) { - UOpenPypeLib::SetFolderColor(Path, Settings->GetFolderFColor(), false); + UAyonLib::SetFolderColor(Path, Settings->GetFolderFColor(), false); } } } diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypePublishInstanceFactory.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypePublishInstanceFactory.cpp rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/Ayon.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/Ayon.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/Ayon.h rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/Ayon.h diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonAssetContainer.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonAssetContainer.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonAssetContainer.h rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonAssetContainer.h diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonAssetContainerFactory.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonAssetContainerFactory.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonAssetContainerFactory.h rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonAssetContainerFactory.h diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonConstants.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonConstants.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonConstants.h rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonConstants.h diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonLib.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonLib.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonLib.h rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonLib.h diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonPythonBridge.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonPythonBridge.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonPythonBridge.h rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonPythonBridge.h diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonSettings.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonSettings.h similarity index 89% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonSettings.h rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonSettings.h index f600cfbf9a..0902019c72 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonSettings.h +++ b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonSettings.h @@ -5,7 +5,7 @@ #include "CoreMinimal.h" #include "AyonSettings.generated.h" -#define OPENPYPE_SETTINGS_FILEPATH IPluginManager::Get().FindPlugin("OpenPype")->GetBaseDir() / TEXT("Config") / TEXT("DefaultAyonSettings.ini") +#define OPENPYPE_SETTINGS_FILEPATH IPluginManager::Get().FindPlugin("Ayon")->GetBaseDir() / TEXT("Config") / TEXT("DefaultAyonSettings.ini") UCLASS(Config=AyonSettings, DefaultConfig) class AYON_API UAyonSettings : public UObject diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonStyle.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonStyle.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonStyle.h rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonStyle.h diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/Commandlets/AyonActionResult.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/Commandlets/AyonActionResult.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/Commandlets/AyonActionResult.h rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/Commandlets/AyonActionResult.h diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/Logging/Ayon_Log.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/Logging/Ayon_Log.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/Logging/Ayon_Log.h rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/Logging/Ayon_Log.h diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypePublishInstance.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h similarity index 97% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypePublishInstance.h rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h index 8cfcd067c0..8f2dca5d69 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypePublishInstance.h +++ b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h @@ -6,7 +6,7 @@ UCLASS(Blueprintable) -class OPENPYPE_API UOpenPypePublishInstance : public UPrimaryDataAsset +class AYON_API UOpenPypePublishInstance : public UPrimaryDataAsset { GENERATED_UCLASS_BODY() diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypePublishInstanceFactory.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h similarity index 88% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypePublishInstanceFactory.h rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h index 3fdb984411..54dc3e8c1d 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypePublishInstanceFactory.h +++ b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h @@ -9,7 +9,7 @@ * */ UCLASS() -class OPENPYPE_API UOpenPypePublishInstanceFactory : public UFactory +class AYON_API UOpenPypePublishInstanceFactory : public UFactory { GENERATED_BODY() diff --git a/openpype/hosts/unreal/integration/UE_4.7/CommandletProject/.gitignore b/openpype/hosts/unreal/integration/UE_4.27/CommandletProject/.gitignore similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/CommandletProject/.gitignore rename to openpype/hosts/unreal/integration/UE_4.27/CommandletProject/.gitignore diff --git a/openpype/hosts/unreal/integration/UE_4.7/CommandletProject/CommandletProject.uproject b/openpype/hosts/unreal/integration/UE_4.27/CommandletProject/CommandletProject.uproject similarity index 85% rename from openpype/hosts/unreal/integration/UE_4.7/CommandletProject/CommandletProject.uproject rename to openpype/hosts/unreal/integration/UE_4.27/CommandletProject/CommandletProject.uproject index 4d75e03bf3..ea7bf21dc4 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/CommandletProject/CommandletProject.uproject +++ b/openpype/hosts/unreal/integration/UE_4.27/CommandletProject/CommandletProject.uproject @@ -5,7 +5,7 @@ "Description": "", "Plugins": [ { - "Name": "OpenPype", + "Name": "Ayon", "Enabled": true } ] diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Config/DefaultOpenPypeSettings.ini b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Config/DefaultOpenPypeSettings.ini deleted file mode 100644 index 8a883cf1db..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Config/DefaultOpenPypeSettings.ini +++ /dev/null @@ -1,2 +0,0 @@ -[/Script/OpenPype.OpenPypeSettings] -FolderColor=(R=91,G=197,B=220,A=255) \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Resources/openpype128.png b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Resources/openpype128.png deleted file mode 100644 index abe8a807ef40f00b75d7446d020a2437732c7583..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14594 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 diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Resources/openpype512.png b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Resources/openpype512.png deleted file mode 100644 index 97c4d4326bc16ba6dfb45d35c4362d8bc15900ae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 85856 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 bCJFu@j}?W0K?Ok;00000NkvXXu0mjf9`4Bp diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonPublishInstance.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonPublishInstance.cpp deleted file mode 100644 index 424addd7bf..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonPublishInstance.cpp +++ /dev/null @@ -1,201 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#pragma once - -#include "AyonPublishInstance.h" -#include "AssetRegistryModule.h" -#include "AyonLib.h" -#include "AyonSettings.h" -#include "Framework/Notifications/NotificationManager.h" -#include "Widgets/Notifications/SNotificationList.h" - -//Moves all the invalid pointers to the end to prepare them for the shrinking -#define REMOVE_INVALID_ENTRIES(VAR) VAR.CompactStable(); \ - VAR.Shrink(); - -UAyonPublishInstance::UAyonPublishInstance(const FObjectInitializer& ObjectInitializer) - : UPrimaryDataAsset(ObjectInitializer) -{ - const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked< - FAssetRegistryModule>("AssetRegistry"); - - const FPropertyEditorModule& PropertyEditorModule = FModuleManager::LoadModuleChecked( - "PropertyEditor"); - - FString Left, Right; - GetPathName().Split("/" + GetName(), &Left, &Right); - - FARFilter Filter; - Filter.PackagePaths.Emplace(FName(Left)); - - TArray FoundAssets; - AssetRegistryModule.GetRegistry().GetAssets(Filter, FoundAssets); - - for (const FAssetData& AssetData : FoundAssets) - OnAssetCreated(AssetData); - - REMOVE_INVALID_ENTRIES(AssetDataInternal) - REMOVE_INVALID_ENTRIES(AssetDataExternal) - - AssetRegistryModule.Get().OnAssetAdded().AddUObject(this, &UAyonPublishInstance::OnAssetCreated); - AssetRegistryModule.Get().OnAssetRemoved().AddUObject(this, &UAyonPublishInstance::OnAssetRemoved); - AssetRegistryModule.Get().OnAssetUpdated().AddUObject(this, &UAyonPublishInstance::OnAssetUpdated); - -#ifdef WITH_EDITOR - ColorAyonDirs(); -#endif - -} - -void UAyonPublishInstance::OnAssetCreated(const FAssetData& InAssetData) -{ - TArray split; - - UObject* Asset = InAssetData.GetAsset(); - - if (!IsValid(Asset)) - { - UE_LOG(LogAssetData, Warning, TEXT("Asset \"%s\" is not valid! Skipping the addition."), - *InAssetData.ObjectPath.ToString()); - return; - } - - const bool result = IsUnderSameDir(Asset) && Cast(Asset) == nullptr; - - if (result) - { - if (AssetDataInternal.Emplace(Asset).IsValidId()) - { - UE_LOG(LogTemp, Log, TEXT("Added an Asset to PublishInstance - Publish Instance: %s, Asset %s"), - *this->GetName(), *Asset->GetName()); - } - } -} - -void UAyonPublishInstance::OnAssetRemoved(const FAssetData& InAssetData) -{ - if (Cast(InAssetData.GetAsset()) == nullptr) - { - if (AssetDataInternal.Contains(nullptr)) - { - AssetDataInternal.Remove(nullptr); - REMOVE_INVALID_ENTRIES(AssetDataInternal) - } - else - { - AssetDataExternal.Remove(nullptr); - REMOVE_INVALID_ENTRIES(AssetDataExternal) - } - } -} - -void UAyonPublishInstance::OnAssetUpdated(const FAssetData& InAssetData) -{ - REMOVE_INVALID_ENTRIES(AssetDataInternal); - REMOVE_INVALID_ENTRIES(AssetDataExternal); -} - -bool UAyonPublishInstance::IsUnderSameDir(const UObject* InAsset) const -{ - FString ThisLeft, ThisRight; - this->GetPathName().Split(this->GetName(), &ThisLeft, &ThisRight); - - return InAsset->GetPathName().StartsWith(ThisLeft); -} - -#ifdef WITH_EDITOR - -void UAyonPublishInstance::ColorAyonDirs() -{ - FString PathName = this->GetPathName(); - - //Check whether the path contains the defined Ayon folder - if (!PathName.Contains(TEXT("Ayon"))) return; - - //Get the base path for open pype - FString PathLeft, PathRight; - PathName.Split(FString("Ayon"), &PathLeft, &PathRight); - - if (PathLeft.IsEmpty() || PathRight.IsEmpty()) - { - UE_LOG(LogAssetData, Error, TEXT("Failed to retrieve the base Ayon directory!")) - return; - } - - PathName.RemoveFromEnd(PathRight, ESearchCase::CaseSensitive); - - //Get the current settings - const UAyonSettings* Settings = GetMutableDefault(); - - //Color the base folder - UAyonLib::SetFolderColor(PathName, Settings->GetFolderFColor(), false); - - //Get Sub paths, iterate through them and color them according to the folder color in UAyonSettings - const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked( - "AssetRegistry"); - - TArray PathList; - - AssetRegistryModule.Get().GetSubPaths(PathName, PathList, true); - - if (PathList.Num() > 0) - { - for (const FString& Path : PathList) - { - UAyonLib::SetFolderColor(Path, Settings->GetFolderFColor(), false); - } - } -} - -void UAyonPublishInstance::SendNotification(const FString& Text) const -{ - FNotificationInfo Info{FText::FromString(Text)}; - - Info.bFireAndForget = true; - Info.bUseLargeFont = false; - Info.bUseThrobber = false; - Info.bUseSuccessFailIcons = false; - Info.ExpireDuration = 4.f; - Info.FadeOutDuration = 2.f; - - FSlateNotificationManager::Get().AddNotification(Info); - - UE_LOG(LogAssetData, Warning, - TEXT( - "Removed duplicated asset from the AssetsDataExternal in Container \"%s\", Asset is already included in the AssetDataInternal!" - ), *GetName() - ) -} - - -void UAyonPublishInstance::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) -{ - Super::PostEditChangeProperty(PropertyChangedEvent); - - if (PropertyChangedEvent.ChangeType == EPropertyChangeType::ValueSet && - PropertyChangedEvent.Property->GetFName() == GET_MEMBER_NAME_CHECKED( - UAyonPublishInstance, AssetDataExternal)) - { - // Check for duplicated assets - for (const auto& Asset : AssetDataInternal) - { - if (AssetDataExternal.Contains(Asset)) - { - AssetDataExternal.Remove(Asset); - return SendNotification( - "You are not allowed to add assets into AssetDataExternal which are already included in AssetDataInternal!"); - } - } - - // Check if no UAyonPublishInstance type assets are included - for (const auto& Asset : AssetDataExternal) - { - if (Cast(Asset.Get()) != nullptr) - { - AssetDataExternal.Remove(Asset); - return SendNotification("You are not allowed to add publish instances!"); - } - } - } -} - -#endif diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonPublishInstanceFactory.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonPublishInstanceFactory.cpp deleted file mode 100644 index c54e789dca..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonPublishInstanceFactory.cpp +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#include "AyonPublishInstanceFactory.h" -#include "AyonPublishInstance.h" - -UAyonPublishInstanceFactory::UAyonPublishInstanceFactory(const FObjectInitializer& ObjectInitializer) - : UFactory(ObjectInitializer) -{ - SupportedClass = UAyonPublishInstance::StaticClass(); - bCreateNew = false; - bEditorImport = true; -} - -UObject* UAyonPublishInstanceFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) -{ - check(InClass->IsChildOf(UAyonPublishInstance::StaticClass())); - return NewObject(InParent, InClass, InName, Flags); -} - -bool UAyonPublishInstanceFactory::ShouldShowInNewMenu() const { - return false; -} diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/OpenPype.Build.cs b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/OpenPype.Build.cs deleted file mode 100644 index f77c1383eb..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/OpenPype.Build.cs +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -using UnrealBuildTool; - -public class OpenPype : ModuleRules -{ - public OpenPype(ReadOnlyTargetRules Target) : base(Target) - { - PCHUsage = 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[] - { - "GameProjectGeneration", - "Projects", - "InputCore", - "UnrealEd", - "LevelEditor", - "CoreUObject", - "Engine", - "Slate", - "SlateCore", - "AssetTools" - // ... 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/UE_4.7/OpenPype/Source/OpenPype/Private/AssetContainer.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/AssetContainer.cpp deleted file mode 100644 index c766f87a8e..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/AssetContainer.cpp +++ /dev/null @@ -1,115 +0,0 @@ -// 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_4.7/OpenPype/Source/OpenPype/Private/AssetContainerFactory.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/AssetContainerFactory.cpp deleted file mode 100644 index b943150bdd..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/AssetContainerFactory.cpp +++ /dev/null @@ -1,20 +0,0 @@ -#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/UE_4.7/OpenPype/Source/OpenPype/Private/Commandlets/Implementations/OPGenerateProjectCommandlet.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/Commandlets/Implementations/OPGenerateProjectCommandlet.cpp deleted file mode 100644 index abb1975027..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/Commandlets/Implementations/OPGenerateProjectCommandlet.cpp +++ /dev/null @@ -1,141 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#include "Commandlets/Implementations/OPGenerateProjectCommandlet.h" - -#include "Editor.h" -#include "GameProjectUtils.h" -#include "OPConstants.h" -#include "Commandlets/OPActionResult.h" -#include "ProjectDescriptor.h" - -int32 UOPGenerateProjectCommandlet::Main(const FString& CommandLineParams) -{ - //Parses command line parameters & creates structure FProjectInformation - const FOPGenerateProjectParams ParsedParams = FOPGenerateProjectParams(CommandLineParams); - ProjectInformation = ParsedParams.GenerateUEProjectInformation(); - - //Creates .uproject & other UE files - EVALUATE_OP_ACTION_RESULT(TryCreateProject()); - - //Loads created .uproject - EVALUATE_OP_ACTION_RESULT(TryLoadProjectDescriptor()); - - //Adds needed plugin to .uproject - AttachPluginsToProjectDescriptor(); - - //Saves .uproject - EVALUATE_OP_ACTION_RESULT(TrySave()); - - //When we are here, there should not be problems in generating Unreal Project for OpenPype - return 0; -} - - -FOPGenerateProjectParams::FOPGenerateProjectParams(): FOPGenerateProjectParams("") -{ -} - -FOPGenerateProjectParams::FOPGenerateProjectParams(const FString& CommandLineParams): CommandLineParams( - CommandLineParams) -{ - UCommandlet::ParseCommandLine(*CommandLineParams, Tokens, Switches); -} - -FProjectInformation FOPGenerateProjectParams::GenerateUEProjectInformation() const -{ - FProjectInformation ProjectInformation = FProjectInformation(); - ProjectInformation.ProjectFilename = GetProjectFileName(); - - ProjectInformation.bShouldGenerateCode = IsSwitchPresent("GenerateCode"); - - return ProjectInformation; -} - -FString FOPGenerateProjectParams::TryGetToken(const int32 Index) const -{ - return Tokens.IsValidIndex(Index) ? Tokens[Index] : ""; -} - -FString FOPGenerateProjectParams::GetProjectFileName() const -{ - return TryGetToken(0); -} - -bool FOPGenerateProjectParams::IsSwitchPresent(const FString& Switch) const -{ - return INDEX_NONE != Switches.IndexOfByPredicate([&Switch](const FString& Item) -> bool - { - return Item.Equals(Switch); - } - ); -} - - -UOPGenerateProjectCommandlet::UOPGenerateProjectCommandlet() -{ - LogToConsole = true; -} - -FOP_ActionResult UOPGenerateProjectCommandlet::TryCreateProject() const -{ - FText FailReason; - FText FailLog; - TArray OutCreatedFiles; - - if (!GameProjectUtils::CreateProject(ProjectInformation, FailReason, FailLog, &OutCreatedFiles)) - return FOP_ActionResult(EOP_ActionResult::ProjectNotCreated, FailReason); - return FOP_ActionResult(); -} - -FOP_ActionResult UOPGenerateProjectCommandlet::TryLoadProjectDescriptor() -{ - FText FailReason; - const bool bLoaded = ProjectDescriptor.Load(ProjectInformation.ProjectFilename, FailReason); - - return FOP_ActionResult(bLoaded ? EOP_ActionResult::Ok : EOP_ActionResult::ProjectNotLoaded, FailReason); -} - -void UOPGenerateProjectCommandlet::AttachPluginsToProjectDescriptor() -{ - FPluginReferenceDescriptor OPPluginDescriptor; - OPPluginDescriptor.bEnabled = true; - OPPluginDescriptor.Name = OPConstants::OP_PluginName; - ProjectDescriptor.Plugins.Add(OPPluginDescriptor); - - FPluginReferenceDescriptor PythonPluginDescriptor; - PythonPluginDescriptor.bEnabled = true; - PythonPluginDescriptor.Name = OPConstants::PythonScript_PluginName; - ProjectDescriptor.Plugins.Add(PythonPluginDescriptor); - - FPluginReferenceDescriptor SequencerScriptingPluginDescriptor; - SequencerScriptingPluginDescriptor.bEnabled = true; - SequencerScriptingPluginDescriptor.Name = OPConstants::SequencerScripting_PluginName; - ProjectDescriptor.Plugins.Add(SequencerScriptingPluginDescriptor); - - FPluginReferenceDescriptor MovieRenderPipelinePluginDescriptor; - MovieRenderPipelinePluginDescriptor.bEnabled = true; - MovieRenderPipelinePluginDescriptor.Name = OPConstants::MovieRenderPipeline_PluginName; - ProjectDescriptor.Plugins.Add(MovieRenderPipelinePluginDescriptor); - - FPluginReferenceDescriptor EditorScriptingPluginDescriptor; - EditorScriptingPluginDescriptor.bEnabled = true; - EditorScriptingPluginDescriptor.Name = OPConstants::EditorScriptingUtils_PluginName; - ProjectDescriptor.Plugins.Add(EditorScriptingPluginDescriptor); -} - -FOP_ActionResult UOPGenerateProjectCommandlet::TrySave() -{ - FText FailReason; - const bool bSaved = ProjectDescriptor.Save(ProjectInformation.ProjectFilename, FailReason); - - return FOP_ActionResult(bSaved ? EOP_ActionResult::Ok : EOP_ActionResult::ProjectNotSaved, FailReason); -} - -FOPGenerateProjectParams UOPGenerateProjectCommandlet::ParseParameters(const FString& Params) const -{ - FOPGenerateProjectParams ParamsResult; - - TArray Tokens, Switches; - ParseCommandLine(*Params, Tokens, Switches); - - return ParamsResult; -} diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/Commandlets/OPActionResult.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/Commandlets/OPActionResult.cpp deleted file mode 100644 index 6e50ef2221..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/Commandlets/OPActionResult.cpp +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - - -#include "Commandlets/OPActionResult.h" -#include "Logging/OP_Log.h" - -EOP_ActionResult::Type& FOP_ActionResult::GetStatus() -{ - return Status; -} - -FText& FOP_ActionResult::GetReason() -{ - return Reason; -} - -FOP_ActionResult::FOP_ActionResult():Status(EOP_ActionResult::Type::Ok) -{ - -} - -FOP_ActionResult::FOP_ActionResult(const EOP_ActionResult::Type& InEnum):Status(InEnum) -{ - TryLog(); -} - -FOP_ActionResult::FOP_ActionResult(const EOP_ActionResult::Type& InEnum, const FText& InReason):Status(InEnum), Reason(InReason) -{ - TryLog(); -}; - -bool FOP_ActionResult::IsProblem() const -{ - return Status != EOP_ActionResult::Ok; -} - -void FOP_ActionResult::TryLog() const -{ - if(IsProblem()) - UE_LOG(LogCommandletOPGenerateProject, Error, TEXT("%s"), *Reason.ToString()); -} diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/Logging/OP_Log.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/Logging/OP_Log.cpp deleted file mode 100644 index 29b1068c21..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/Logging/OP_Log.cpp +++ /dev/null @@ -1 +0,0 @@ -#include "Logging/OP_Log.h" diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPype.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPype.cpp deleted file mode 100644 index 9bf7b341c5..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPype.cpp +++ /dev/null @@ -1,155 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#include "OpenPype.h" - -#include "ISettingsContainer.h" -#include "ISettingsModule.h" -#include "ISettingsSection.h" -#include "LevelEditor.h" -#include "OpenPypePythonBridge.h" -#include "OpenPypeSettings.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() -{ - if (!IsRunningCommandlet()) { - 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); - - RegisterSettings(); - } -} - -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::RegisterSettings() -{ - ISettingsModule& SettingsModule = FModuleManager::LoadModuleChecked("Settings"); - - // Create the new category - // TODO: After the movement of the plugin from the game to editor, it might be necessary to move this! - ISettingsContainerPtr SettingsContainer = SettingsModule.GetContainer("Project"); - - UOpenPypeSettings* Settings = GetMutableDefault(); - - // Register the settings - ISettingsSectionPtr SettingsSection = SettingsModule.RegisterSettings("Project", "OpenPype", "General", - LOCTEXT("RuntimeGeneralSettingsName", - "General"), - LOCTEXT("RuntimeGeneralSettingsDescription", - "Base configuration for Open Pype Module"), - Settings - ); - - // Register the save handler to your settings, you might want to use it to - // validate those or just act to settings changes. - if (SettingsSection.IsValid()) - { - SettingsSection->OnModified().BindRaw(this, &FOpenPypeModule::HandleSettingsSaved); - } -} - -bool FOpenPypeModule::HandleSettingsSaved() -{ - UOpenPypeSettings* Settings = GetMutableDefault(); - bool ResaveSettings = false; - - // You can put any validation code in here and resave the settings in case an invalid - // value has been entered - - if (ResaveSettings) - { - Settings->SaveConfig(); - } - - return true; -} - - -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/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypePythonBridge.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypePythonBridge.cpp deleted file mode 100644 index 6ebfc528f0..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypePythonBridge.cpp +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#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/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypeSettings.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypeSettings.cpp deleted file mode 100644 index dd4228dfd0..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypeSettings.cpp +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#include "OpenPypeSettings.h" - -#include "Interfaces/IPluginManager.h" - -/** - * Mainly is used for initializing default values if the DefaultOpenPypeSettings.ini file does not exist in the saved config - */ -UOpenPypeSettings::UOpenPypeSettings(const FObjectInitializer& ObjectInitializer) -{ - - const FString ConfigFilePath = OPENPYPE_SETTINGS_FILEPATH; - - // This has to be probably in the future set using the UE Reflection system - FColor Color; - GConfig->GetColor(TEXT("/Script/OpenPype.OpenPypeSettings"), TEXT("FolderColor"), Color, ConfigFilePath); - - FolderColor = Color; -} \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypeStyle.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypeStyle.cpp deleted file mode 100644 index 0cc854c5ef..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypeStyle.cpp +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#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::EnginePluginsDir() / TEXT("Marketplace/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; -} diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/AssetContainer.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/AssetContainer.h deleted file mode 100644 index 3b0230391c..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/AssetContainer.h +++ /dev/null @@ -1,39 +0,0 @@ -// 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, Category="Assets") - 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_4.7/OpenPype/Source/OpenPype/Public/AssetContainerFactory.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/AssetContainerFactory.h deleted file mode 100644 index 331ce6bb50..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/AssetContainerFactory.h +++ /dev/null @@ -1,21 +0,0 @@ -// 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/UE_4.7/OpenPype/Source/OpenPype/Public/Commandlets/Implementations/OPGenerateProjectCommandlet.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/Commandlets/Implementations/OPGenerateProjectCommandlet.h deleted file mode 100644 index d1129aa070..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/Commandlets/Implementations/OPGenerateProjectCommandlet.h +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#pragma once - -#include "GameProjectUtils.h" -#include "Commandlets/OPActionResult.h" -#include "ProjectDescriptor.h" -#include "Commandlets/Commandlet.h" -#include "OPGenerateProjectCommandlet.generated.h" - -struct FProjectDescriptor; -struct FProjectInformation; - -/** -* @brief Structure which parses command line parameters and generates FProjectInformation -*/ -USTRUCT() -struct FOPGenerateProjectParams -{ - GENERATED_BODY() - -private: - FString CommandLineParams; - TArray Tokens; - TArray Switches; - -public: - FOPGenerateProjectParams(); - FOPGenerateProjectParams(const FString& CommandLineParams); - - FProjectInformation GenerateUEProjectInformation() const; - -private: - FString TryGetToken(const int32 Index) const; - FString GetProjectFileName() const; - - bool IsSwitchPresent(const FString& Switch) const; -}; - -UCLASS() -class OPENPYPE_API UOPGenerateProjectCommandlet : public UCommandlet -{ - GENERATED_BODY() - -private: - FProjectInformation ProjectInformation; - FProjectDescriptor ProjectDescriptor; - -public: - UOPGenerateProjectCommandlet(); - - virtual int32 Main(const FString& CommandLineParams) override; - -private: - FOPGenerateProjectParams ParseParameters(const FString& Params) const; - FOP_ActionResult TryCreateProject() const; - FOP_ActionResult TryLoadProjectDescriptor(); - void AttachPluginsToProjectDescriptor(); - FOP_ActionResult TrySave(); -}; - diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/Logging/OP_Log.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/Logging/OP_Log.h deleted file mode 100644 index 3740c5285a..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/Logging/OP_Log.h +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#pragma once - -DEFINE_LOG_CATEGORY_STATIC(LogCommandletOPGenerateProject, Log, All); \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPype.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPype.h deleted file mode 100644 index 2454344128..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPype.h +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#pragma once - -#include "Engine.h" - - -class FOpenPypeModule : public IModuleInterface -{ -public: - virtual void StartupModule() override; - virtual void ShutdownModule() override; - -private: - void RegisterSettings(); - bool HandleSettingsSaved(); - - void AddMenuEntry(FMenuBuilder& MenuBuilder); - void AddToobarEntry(FToolBarBuilder& ToolbarBuilder); - void MenuPopup(); - void MenuDialog(); -}; diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypeLib.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypeLib.h deleted file mode 100644 index ef4d1027ea..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypeLib.h +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#pragma once - -#include "Engine.h" -#include "OpenPypeLib.generated.h" - - -UCLASS(Blueprintable) -class OPENPYPE_API UOpenPypeLib : public UBlueprintFunctionLibrary -{ - - GENERATED_BODY() - -public: - UFUNCTION(BlueprintCallable, Category = Python) - static bool SetFolderColor(const FString& FolderPath, const FLinearColor& FolderColor,const 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_4.7/OpenPype/Source/OpenPype/Public/OpenPypeSettings.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypeSettings.h deleted file mode 100644 index 88defaa773..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypeSettings.h +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#pragma once - -#include "CoreMinimal.h" -#include "OpenPypeSettings.generated.h" - -#define OPENPYPE_SETTINGS_FILEPATH IPluginManager::Get().FindPlugin("OpenPype")->GetBaseDir() / TEXT("Config") / TEXT("DefaultOpenPypeSettings.ini") - -UCLASS(Config=OpenPypeSettings, DefaultConfig) -class OPENPYPE_API UOpenPypeSettings : public UObject -{ - GENERATED_UCLASS_BODY() - - UFUNCTION(BlueprintCallable, BlueprintPure, Category = Settings) - FColor GetFolderFColor() const - { - return FolderColor; - } - - UFUNCTION(BlueprintCallable, BlueprintPure, Category = Settings) - FLinearColor GetFolderFLinearColor() const - { - return FLinearColor(FolderColor); - } - -protected: - - UPROPERTY(config, EditAnywhere, Category = Folders) - FColor FolderColor = FColor(25,45,223); -}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypeStyle.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypeStyle.h deleted file mode 100644 index 0e4af129d0..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypeStyle.h +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#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/OpenPype/.gitignore b/openpype/hosts/unreal/integration/UE_5.0/Ayon/.gitignore similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/.gitignore rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/.gitignore diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/OpenPype.uplugin b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Ayon.uplugin similarity index 87% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/OpenPype.uplugin rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Ayon.uplugin index 0fe7b249a8..c93a9b4b68 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/OpenPype.uplugin +++ b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Ayon.uplugin @@ -15,11 +15,6 @@ "IsExperimentalVersion": false, "Installed": true, "Modules": [ - { - "Name": "OpenPype", - "Type": "Editor", - "LoadingPhase": "Default" - }, { "Name": "Ayon", "Type": "Editor", diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Config/DefaultAyonSettings.ini b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Config/DefaultAyonSettings.ini similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Config/DefaultAyonSettings.ini rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Config/DefaultAyonSettings.ini diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Config/FilterPlugin.ini b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Config/FilterPlugin.ini similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Config/FilterPlugin.ini rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Config/FilterPlugin.ini diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Content/Python/init_unreal.py b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Content/Python/init_unreal.py similarity index 93% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Content/Python/init_unreal.py rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Content/Python/init_unreal.py index b85f970699..9ed5a2cb19 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Content/Python/init_unreal.py +++ b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Content/Python/init_unreal.py @@ -16,7 +16,7 @@ if openpype_detected: @unreal.uclass() -class OpenPypeIntegration(unreal.OpenPypePythonBridge): +class AyonIntegration(unreal.AyonPythonBridge): @unreal.ufunction(override=True) def RunInPython_Popup(self): unreal.log_warning("OpenPype: showing tools popup") diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/README.md b/openpype/hosts/unreal/integration/UE_5.0/Ayon/README.md similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/README.md rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/README.md diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Resources/ayon128.png b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Resources/ayon128.png similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Resources/ayon128.png rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Resources/ayon128.png diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Resources/ayon40.png b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Resources/ayon40.png similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Resources/ayon40.png rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Resources/ayon40.png diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Resources/ayon512.png b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Resources/ayon512.png similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Resources/ayon512.png rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Resources/ayon512.png diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Ayon.Build.cs b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Ayon.Build.cs similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Ayon.Build.cs rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Ayon.Build.cs diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/Ayon.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/Ayon.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/Ayon.cpp rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/Ayon.cpp diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonAssetContainer.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonAssetContainer.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonAssetContainer.cpp rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonAssetContainer.cpp diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonAssetContainerFactory.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonAssetContainerFactory.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonAssetContainerFactory.cpp rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonAssetContainerFactory.cpp diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonCommands.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonCommands.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonCommands.cpp rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonCommands.cpp diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonLib.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonLib.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonLib.cpp rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonLib.cpp diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonPythonBridge.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonPythonBridge.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonPythonBridge.cpp rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonPythonBridge.cpp diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonSettings.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonSettings.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonSettings.cpp rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonSettings.cpp diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonStyle.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonStyle.cpp similarity index 93% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonStyle.cpp rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonStyle.cpp index 91a0c6996b..d88df78735 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonStyle.cpp +++ b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonStyle.cpp @@ -40,7 +40,7 @@ const FVector2D Icon40x40(40.0f, 40.0f); TSharedRef< FSlateStyleSet > FAyonStyle::Create() { TSharedRef< FSlateStyleSet > Style = MakeShareable(new FSlateStyleSet("AyonStyle")); - Style->SetContentRoot(IPluginManager::Get().FindPlugin("OpenPype")->GetBaseDir() / TEXT("Resources")); + Style->SetContentRoot(IPluginManager::Get().FindPlugin("Ayon")->GetBaseDir() / TEXT("Resources")); Style->Set("Ayon.AyonTools", new IMAGE_BRUSH(TEXT("ayon40"), Icon40x40)); Style->Set("Ayon.AyonToolsDialog", new IMAGE_BRUSH(TEXT("ayon40"), Icon40x40)); diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/Commandlets/AyonActionResult.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/Commandlets/AyonActionResult.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/Commandlets/AyonActionResult.cpp rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/Commandlets/AyonActionResult.cpp diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypePublishInstance.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp similarity index 95% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypePublishInstance.cpp rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp index 05d5c8a87d..0d9cddfd1c 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypePublishInstance.cpp +++ b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp @@ -5,8 +5,8 @@ #include "AssetRegistry/AssetRegistryModule.h" #include "AssetToolsModule.h" #include "Framework/Notifications/NotificationManager.h" -#include "OpenPypeLib.h" -#include "OpenPypeSettings.h" +#include "AyonLib.h" +#include "AyonSettings.h" #include "Widgets/Notifications/SNotificationList.h" @@ -125,10 +125,10 @@ void UOpenPypePublishInstance::ColorOpenPypeDirs() PathName.RemoveFromEnd(PathRight, ESearchCase::CaseSensitive); //Get the current settings - const UOpenPypeSettings* Settings = GetMutableDefault(); + const UAyonSettings* Settings = GetMutableDefault(); //Color the base folder - UOpenPypeLib::SetFolderColor(PathName, Settings->GetFolderFColor(), false); + UAyonLib::SetFolderColor(PathName, Settings->GetFolderFColor(), false); //Get Sub paths, iterate through them and color them according to the folder color in UOpenPypeSettings const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked( @@ -142,7 +142,7 @@ void UOpenPypePublishInstance::ColorOpenPypeDirs() { for (const FString& Path : PathList) { - UOpenPypeLib::SetFolderColor(Path, Settings->GetFolderFColor(), false); + UAyonLib::SetFolderColor(Path, Settings->GetFolderFColor(), false); } } } diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypePublishInstanceFactory.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypePublishInstanceFactory.cpp rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/Ayon.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/Ayon.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/Ayon.h rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/Ayon.h diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonAssetContainer.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonAssetContainer.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonAssetContainer.h rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonAssetContainer.h diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonAssetContainerFactory.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonAssetContainerFactory.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonAssetContainerFactory.h rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonAssetContainerFactory.h diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonCommands.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonCommands.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonCommands.h rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonCommands.h diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonConstants.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonConstants.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonConstants.h rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonConstants.h diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonLib.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonLib.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonLib.h rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonLib.h diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonPythonBridge.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonPythonBridge.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonPythonBridge.h rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonPythonBridge.h diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonSettings.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonSettings.h similarity index 90% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonSettings.h rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonSettings.h index 42a724b95a..4f12d1a5f2 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonSettings.h +++ b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonSettings.h @@ -6,7 +6,7 @@ #include "UObject/Object.h" #include "AyonSettings.generated.h" -#define AYON_SETTINGS_FILEPATH IPluginManager::Get().FindPlugin("OpenPype")->GetBaseDir() / TEXT("Config") / TEXT("DefaultAyonSettings.ini") +#define AYON_SETTINGS_FILEPATH IPluginManager::Get().FindPlugin("Ayon")->GetBaseDir() / TEXT("Config") / TEXT("DefaultAyonSettings.ini") UCLASS(Config=AyonSettings, DefaultConfig) class AYON_API UAyonSettings : public UObject diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonStyle.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonStyle.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonStyle.h rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonStyle.h diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/Commandlets/AyonActionResult.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/Commandlets/AyonActionResult.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/Commandlets/AyonActionResult.h rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/Commandlets/AyonActionResult.h diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/Logging/Ayon_Log.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/Logging/Ayon_Log.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/Logging/Ayon_Log.h rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/Logging/Ayon_Log.h diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypePublishInstance.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h similarity index 97% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypePublishInstance.h rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h index bce41ef1b1..03a22c6cde 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypePublishInstance.h +++ b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h @@ -6,7 +6,7 @@ UCLASS(Blueprintable) -class OPENPYPE_API UOpenPypePublishInstance : public UPrimaryDataAsset +class AYON_API UOpenPypePublishInstance : public UPrimaryDataAsset { GENERATED_UCLASS_BODY() diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypePublishInstanceFactory.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h similarity index 88% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypePublishInstanceFactory.h rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h index 3fdb984411..54dc3e8c1d 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypePublishInstanceFactory.h +++ b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h @@ -9,7 +9,7 @@ * */ UCLASS() -class OPENPYPE_API UOpenPypePublishInstanceFactory : public UFactory +class AYON_API UOpenPypePublishInstanceFactory : public UFactory { GENERATED_BODY() diff --git a/openpype/hosts/unreal/integration/UE_5.0/CommandletProject/CommandletProject.uproject b/openpype/hosts/unreal/integration/UE_5.0/CommandletProject/CommandletProject.uproject index c8dc1c673e..9cf75ebaf2 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/CommandletProject/CommandletProject.uproject +++ b/openpype/hosts/unreal/integration/UE_5.0/CommandletProject/CommandletProject.uproject @@ -12,7 +12,7 @@ ] }, { - "Name": "OpenPype", + "Name": "Ayon", "Enabled": true, "Type": "Editor" } diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Config/DefaultOpenPypeSettings.ini b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Config/DefaultOpenPypeSettings.ini deleted file mode 100644 index 8a883cf1db..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Config/DefaultOpenPypeSettings.ini +++ /dev/null @@ -1,2 +0,0 @@ -[/Script/OpenPype.OpenPypeSettings] -FolderColor=(R=91,G=197,B=220,A=255) \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Resources/openpype128.png b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Resources/openpype128.png deleted file mode 100644 index abe8a807ef40f00b75d7446d020a2437732c7583..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14594 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 diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Resources/openpype512.png b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Resources/openpype512.png deleted file mode 100644 index 97c4d4326bc16ba6dfb45d35c4362d8bc15900ae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 85856 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 bCJFu@j}?W0K?Ok;00000NkvXXu0mjf9`4Bp diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonPublishInstanceFactory.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonPublishInstanceFactory.cpp deleted file mode 100644 index c54e789dca..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonPublishInstanceFactory.cpp +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#include "AyonPublishInstanceFactory.h" -#include "AyonPublishInstance.h" - -UAyonPublishInstanceFactory::UAyonPublishInstanceFactory(const FObjectInitializer& ObjectInitializer) - : UFactory(ObjectInitializer) -{ - SupportedClass = UAyonPublishInstance::StaticClass(); - bCreateNew = false; - bEditorImport = true; -} - -UObject* UAyonPublishInstanceFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) -{ - check(InClass->IsChildOf(UAyonPublishInstance::StaticClass())); - return NewObject(InParent, InClass, InName, Flags); -} - -bool UAyonPublishInstanceFactory::ShouldShowInNewMenu() const { - return false; -} diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonPublishInstance.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonPublishInstance.h deleted file mode 100644 index 1c51f98b4a..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonPublishInstance.h +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#pragma once - -#include "AyonPublishInstance.generated.h" - - -UCLASS(Blueprintable) -class AYON_API UAyonPublishInstance : public UPrimaryDataAsset -{ - GENERATED_UCLASS_BODY() - -public: - /** - /** - * Retrieves all the assets which are monitored by the Publish Instance (Monitors assets in the directory which is - * placed in) - * - * @return - Set of UObjects. Careful! They are returning raw pointers. Seems like an issue in UE5 - */ - UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") - TSet GetInternalAssets() const - { - //For some reason it can only return Raw Pointers? Seems like an issue which they haven't fixed. - TSet ResultSet; - - for (const auto& Asset : AssetDataInternal) - ResultSet.Add(Asset.LoadSynchronous()); - - return ResultSet; - } - - /** - * Retrieves all the assets which have been added manually by the Publish Instance - * - * @return - TSet of assets (UObjects). Careful! They are returning raw pointers. Seems like an issue in UE5 - */ - UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") - TSet GetExternalAssets() const - { - //For some reason it can only return Raw Pointers? Seems like an issue which they haven't fixed. - TSet ResultSet; - - for (const auto& Asset : AssetDataExternal) - ResultSet.Add(Asset.LoadSynchronous()); - - return ResultSet; - } - - /** - * Function for returning all the assets in the container combined. - * - * @return Returns all the internal and externally added assets into one set (TSet of UObjects). Careful! They are - * returning raw pointers. Seems like an issue in UE5 - * - * @attention If the bAddExternalAssets variable is false, external assets won't be included! - */ - UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") - TSet GetAllAssets() const - { - const TSet>& IteratedSet = bAddExternalAssets - ? AssetDataInternal.Union(AssetDataExternal) - : AssetDataInternal; - - //Create a new TSet only with raw pointers. - TSet ResultSet; - - for (auto& Asset : IteratedSet) - ResultSet.Add(Asset.LoadSynchronous()); - - return ResultSet; - } - -private: - UPROPERTY(VisibleAnywhere, Category="Assets") - TSet> AssetDataInternal; - - /** - * This property allows exposing the array to include other assets from any other directory than what it's currently - * monitoring. NOTE: that these assets have to be added manually! They are not automatically registered or added! - */ - UPROPERTY(EditAnywhere, Category = "Assets") - bool bAddExternalAssets = false; - - UPROPERTY(EditAnywhere, meta=(EditCondition="bAddExternalAssets"), Category="Assets") - TSet> AssetDataExternal; - - - void OnAssetCreated(const FAssetData& InAssetData); - void OnAssetRemoved(const FAssetData& InAssetData); - void OnAssetUpdated(const FAssetData& InAssetData); - - bool IsUnderSameDir(const UObject* InAsset) const; - -#ifdef WITH_EDITOR - - void ColorAyonDirs(); - - void SendNotification(const FString& Text) const; - virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; - -#endif -}; diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonPublishInstanceFactory.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonPublishInstanceFactory.h deleted file mode 100644 index 443d618c9a..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonPublishInstanceFactory.h +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#pragma once - -#include "CoreMinimal.h" -#include "Factories/Factory.h" -#include "AyonPublishInstanceFactory.generated.h" - -/** - * - */ -UCLASS() -class AYON_API UAyonPublishInstanceFactory : public UFactory -{ - GENERATED_BODY() - -public: - UAyonPublishInstanceFactory(const FObjectInitializer& ObjectInitializer); - virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; - virtual bool ShouldShowInNewMenu() const override; -}; diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/AssetContainerFactory.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/AssetContainerFactory.cpp deleted file mode 100644 index b943150bdd..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/AssetContainerFactory.cpp +++ /dev/null @@ -1,20 +0,0 @@ -#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/UE_5.0/OpenPype/Source/OpenPype/Private/Commandlets/Implementations/OPGenerateProjectCommandlet.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/Commandlets/Implementations/OPGenerateProjectCommandlet.cpp deleted file mode 100644 index abb1975027..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/Commandlets/Implementations/OPGenerateProjectCommandlet.cpp +++ /dev/null @@ -1,141 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#include "Commandlets/Implementations/OPGenerateProjectCommandlet.h" - -#include "Editor.h" -#include "GameProjectUtils.h" -#include "OPConstants.h" -#include "Commandlets/OPActionResult.h" -#include "ProjectDescriptor.h" - -int32 UOPGenerateProjectCommandlet::Main(const FString& CommandLineParams) -{ - //Parses command line parameters & creates structure FProjectInformation - const FOPGenerateProjectParams ParsedParams = FOPGenerateProjectParams(CommandLineParams); - ProjectInformation = ParsedParams.GenerateUEProjectInformation(); - - //Creates .uproject & other UE files - EVALUATE_OP_ACTION_RESULT(TryCreateProject()); - - //Loads created .uproject - EVALUATE_OP_ACTION_RESULT(TryLoadProjectDescriptor()); - - //Adds needed plugin to .uproject - AttachPluginsToProjectDescriptor(); - - //Saves .uproject - EVALUATE_OP_ACTION_RESULT(TrySave()); - - //When we are here, there should not be problems in generating Unreal Project for OpenPype - return 0; -} - - -FOPGenerateProjectParams::FOPGenerateProjectParams(): FOPGenerateProjectParams("") -{ -} - -FOPGenerateProjectParams::FOPGenerateProjectParams(const FString& CommandLineParams): CommandLineParams( - CommandLineParams) -{ - UCommandlet::ParseCommandLine(*CommandLineParams, Tokens, Switches); -} - -FProjectInformation FOPGenerateProjectParams::GenerateUEProjectInformation() const -{ - FProjectInformation ProjectInformation = FProjectInformation(); - ProjectInformation.ProjectFilename = GetProjectFileName(); - - ProjectInformation.bShouldGenerateCode = IsSwitchPresent("GenerateCode"); - - return ProjectInformation; -} - -FString FOPGenerateProjectParams::TryGetToken(const int32 Index) const -{ - return Tokens.IsValidIndex(Index) ? Tokens[Index] : ""; -} - -FString FOPGenerateProjectParams::GetProjectFileName() const -{ - return TryGetToken(0); -} - -bool FOPGenerateProjectParams::IsSwitchPresent(const FString& Switch) const -{ - return INDEX_NONE != Switches.IndexOfByPredicate([&Switch](const FString& Item) -> bool - { - return Item.Equals(Switch); - } - ); -} - - -UOPGenerateProjectCommandlet::UOPGenerateProjectCommandlet() -{ - LogToConsole = true; -} - -FOP_ActionResult UOPGenerateProjectCommandlet::TryCreateProject() const -{ - FText FailReason; - FText FailLog; - TArray OutCreatedFiles; - - if (!GameProjectUtils::CreateProject(ProjectInformation, FailReason, FailLog, &OutCreatedFiles)) - return FOP_ActionResult(EOP_ActionResult::ProjectNotCreated, FailReason); - return FOP_ActionResult(); -} - -FOP_ActionResult UOPGenerateProjectCommandlet::TryLoadProjectDescriptor() -{ - FText FailReason; - const bool bLoaded = ProjectDescriptor.Load(ProjectInformation.ProjectFilename, FailReason); - - return FOP_ActionResult(bLoaded ? EOP_ActionResult::Ok : EOP_ActionResult::ProjectNotLoaded, FailReason); -} - -void UOPGenerateProjectCommandlet::AttachPluginsToProjectDescriptor() -{ - FPluginReferenceDescriptor OPPluginDescriptor; - OPPluginDescriptor.bEnabled = true; - OPPluginDescriptor.Name = OPConstants::OP_PluginName; - ProjectDescriptor.Plugins.Add(OPPluginDescriptor); - - FPluginReferenceDescriptor PythonPluginDescriptor; - PythonPluginDescriptor.bEnabled = true; - PythonPluginDescriptor.Name = OPConstants::PythonScript_PluginName; - ProjectDescriptor.Plugins.Add(PythonPluginDescriptor); - - FPluginReferenceDescriptor SequencerScriptingPluginDescriptor; - SequencerScriptingPluginDescriptor.bEnabled = true; - SequencerScriptingPluginDescriptor.Name = OPConstants::SequencerScripting_PluginName; - ProjectDescriptor.Plugins.Add(SequencerScriptingPluginDescriptor); - - FPluginReferenceDescriptor MovieRenderPipelinePluginDescriptor; - MovieRenderPipelinePluginDescriptor.bEnabled = true; - MovieRenderPipelinePluginDescriptor.Name = OPConstants::MovieRenderPipeline_PluginName; - ProjectDescriptor.Plugins.Add(MovieRenderPipelinePluginDescriptor); - - FPluginReferenceDescriptor EditorScriptingPluginDescriptor; - EditorScriptingPluginDescriptor.bEnabled = true; - EditorScriptingPluginDescriptor.Name = OPConstants::EditorScriptingUtils_PluginName; - ProjectDescriptor.Plugins.Add(EditorScriptingPluginDescriptor); -} - -FOP_ActionResult UOPGenerateProjectCommandlet::TrySave() -{ - FText FailReason; - const bool bSaved = ProjectDescriptor.Save(ProjectInformation.ProjectFilename, FailReason); - - return FOP_ActionResult(bSaved ? EOP_ActionResult::Ok : EOP_ActionResult::ProjectNotSaved, FailReason); -} - -FOPGenerateProjectParams UOPGenerateProjectCommandlet::ParseParameters(const FString& Params) const -{ - FOPGenerateProjectParams ParamsResult; - - TArray Tokens, Switches; - ParseCommandLine(*Params, Tokens, Switches); - - return ParamsResult; -} diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/Commandlets/OPActionResult.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/Commandlets/OPActionResult.cpp deleted file mode 100644 index 23ae2dd329..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/Commandlets/OPActionResult.cpp +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#include "Commandlets/OPActionResult.h" -#include "Logging/OP_Log.h" - -EOP_ActionResult::Type& FOP_ActionResult::GetStatus() -{ - return Status; -} - -FText& FOP_ActionResult::GetReason() -{ - return Reason; -} - -FOP_ActionResult::FOP_ActionResult():Status(EOP_ActionResult::Type::Ok) -{ - -} - -FOP_ActionResult::FOP_ActionResult(const EOP_ActionResult::Type& InEnum):Status(InEnum) -{ - TryLog(); -} - -FOP_ActionResult::FOP_ActionResult(const EOP_ActionResult::Type& InEnum, const FText& InReason):Status(InEnum), Reason(InReason) -{ - TryLog(); -}; - -bool FOP_ActionResult::IsProblem() const -{ - return Status != EOP_ActionResult::Ok; -} - -void FOP_ActionResult::TryLog() const -{ - if(IsProblem()) - UE_LOG(LogCommandletOPGenerateProject, Error, TEXT("%s"), *Reason.ToString()); -} diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/Logging/OP_Log.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/Logging/OP_Log.cpp deleted file mode 100644 index 198fb9df0c..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/Logging/OP_Log.cpp +++ /dev/null @@ -1,3 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#include "Logging/OP_Log.h" diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeCommands.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeCommands.cpp deleted file mode 100644 index 881814e278..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeCommands.cpp +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2023, Ayon, 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/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeLib.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeLib.cpp deleted file mode 100644 index 34faba1f49..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeLib.cpp +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#include "OpenPypeLib.h" - -#include "AssetViewUtils.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? - */ - -bool UOpenPypeLib::SetFolderColor(const FString& FolderPath, const FLinearColor& FolderColor, const bool& bForceAdd) -{ - if (AssetViewUtils::DoesFolderExist(FolderPath)) - { - const TSharedPtr LinearColor = MakeShared(FolderColor); - - AssetViewUtils::SaveColor(FolderPath, LinearColor, true); - UE_LOG(LogAssetData, Display, TEXT("A color {%s} has been set to folder \"%s\""), *LinearColor->ToString(), - *FolderPath) - return true; - } - - UE_LOG(LogAssetData, Display, TEXT("Setting a color {%s} to folder \"%s\" has failed! Directory doesn't exist!"), - *FolderColor.ToString(), *FolderPath) - return false; -} - -/** - * Returns all properties 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/OpenPype/Source/OpenPype/Private/OpenPypePythonBridge.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypePythonBridge.cpp deleted file mode 100644 index 6ebfc528f0..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypePythonBridge.cpp +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#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/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeSettings.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeSettings.cpp deleted file mode 100644 index 6562a81138..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeSettings.cpp +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#include "OpenPypeSettings.h" - -#include "Interfaces/IPluginManager.h" -#include "UObject/UObjectGlobals.h" - -/** - * Mainly is used for initializing default values if the DefaultOpenPypeSettings.ini file does not exist in the saved config - */ -UOpenPypeSettings::UOpenPypeSettings(const FObjectInitializer& ObjectInitializer) -{ - - const FString ConfigFilePath = OPENPYPE_SETTINGS_FILEPATH; - - // This has to be probably in the future set using the UE Reflection system - FColor Color; - GConfig->GetColor(TEXT("/Script/OpenPype.OpenPypeSettings"), TEXT("FolderColor"), Color, ConfigFilePath); - - FolderColor = Color; -} \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeStyle.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeStyle.cpp deleted file mode 100644 index a4d75e048e..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeStyle.cpp +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#include "OpenPypeStyle.h" -#include "OpenPype.h" -#include "Framework/Application/SlateApplication.h" -#include "Styling/SlateStyleRegistry.h" -#include "Slate/SlateGameResources.h" -#include "Interfaces/IPluginManager.h" -#include "Styling/SlateStyleMacros.h" - -#define RootToContentDir Style->RootToContentDir - -TSharedPtr FOpenPypeStyle::OpenPypeStyleInstance = nullptr; - -void FOpenPypeStyle::Initialize() -{ - if (!OpenPypeStyleInstance.IsValid()) - { - OpenPypeStyleInstance = Create(); - FSlateStyleRegistry::RegisterSlateStyle(*OpenPypeStyleInstance); - } -} - -void FOpenPypeStyle::Shutdown() -{ - FSlateStyleRegistry::UnRegisterSlateStyle(*OpenPypeStyleInstance); - ensure(OpenPypeStyleInstance.IsUnique()); - OpenPypeStyleInstance.Reset(); -} - -FName FOpenPypeStyle::GetStyleSetName() -{ - static FName StyleSetName(TEXT("OpenPypeStyle")); - return StyleSetName; -} - -const FVector2D Icon16x16(16.0f, 16.0f); -const FVector2D Icon20x20(20.0f, 20.0f); -const FVector2D Icon40x40(40.0f, 40.0f); - -TSharedRef< FSlateStyleSet > FOpenPypeStyle::Create() -{ - TSharedRef< FSlateStyleSet > Style = MakeShareable(new FSlateStyleSet("OpenPypeStyle")); - Style->SetContentRoot(IPluginManager::Get().FindPlugin("OpenPype")->GetBaseDir() / TEXT("Resources")); - - Style->Set("OpenPype.OpenPypeTools", new IMAGE_BRUSH(TEXT("openpype40"), Icon40x40)); - Style->Set("OpenPype.OpenPypeToolsDialog", new IMAGE_BRUSH(TEXT("openpype40"), Icon40x40)); - - return Style; -} - -void FOpenPypeStyle::ReloadTextures() -{ - if (FSlateApplication::IsInitialized()) - { - FSlateApplication::Get().GetRenderer()->ReloadTextureResources(); - } -} - -const ISlateStyle& FOpenPypeStyle::Get() -{ - return *OpenPypeStyleInstance; -} diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/Commandlets/OPActionResult.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/Commandlets/OPActionResult.h deleted file mode 100644 index 322a23a3e8..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/Commandlets/OPActionResult.h +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#pragma once - -#include "CoreMinimal.h" -#include "OPActionResult.generated.h" - -/** - * @brief This macro returns error code when is problem or does nothing when there is no problem. - * @param ActionResult FOP_ActionResult structure - */ -#define EVALUATE_OP_ACTION_RESULT(ActionResult) \ - if(ActionResult.IsProblem()) \ - return ActionResult.GetStatus(); - -/** -* @brief This enum values are humanly readable mapping of error codes. -* Here should be all error codes to be possible find what went wrong. -* TODO: In the future a web document should exists with the mapped error code & what problem occurred & how to repair it... -*/ -UENUM() -namespace EOP_ActionResult -{ - enum Type - { - Ok, - ProjectNotCreated, - ProjectNotLoaded, - ProjectNotSaved, - //....Here insert another values - - //Do not remove! - //Usable for looping through enum values - __Last UMETA(Hidden) - }; -} - - -/** - * @brief This struct holds action result enum and optionally reason of fail - */ -USTRUCT() -struct FOP_ActionResult -{ - GENERATED_BODY() - -public: - /** @brief Default constructor usable when there is no problem */ - FOP_ActionResult(); - - /** - * @brief This constructor initializes variables & attempts to log when is error - * @param InEnum Status - */ - FOP_ActionResult(const EOP_ActionResult::Type& InEnum); - - /** - * @brief This constructor initializes variables & attempts to log when is error - * @param InEnum Status - * @param InReason Reason of potential fail - */ - FOP_ActionResult(const EOP_ActionResult::Type& InEnum, const FText& InReason); - -private: - /** @brief Action status */ - EOP_ActionResult::Type Status; - - /** @brief Optional reason of fail */ - FText Reason; - -public: - /** - * @brief Checks if there is problematic state - * @return true when status is not equal to EOP_ActionResult::Ok - */ - bool IsProblem() const; - EOP_ActionResult::Type& GetStatus(); - FText& GetReason(); - -private: - void TryLog() const; -}; - diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/Logging/OP_Log.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/Logging/OP_Log.h deleted file mode 100644 index 3740c5285a..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/Logging/OP_Log.h +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#pragma once - -DEFINE_LOG_CATEGORY_STATIC(LogCommandletOPGenerateProject, Log, All); \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OPConstants.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OPConstants.h deleted file mode 100644 index f4587f7a50..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OPConstants.h +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#pragma once - -namespace OPConstants -{ - const FString OP_PluginName = "OpenPype"; - const FString PythonScript_PluginName = "PythonScriptPlugin"; - const FString SequencerScripting_PluginName = "SequencerScripting"; - const FString MovieRenderPipeline_PluginName = "MovieRenderPipeline"; - const FString EditorScriptingUtils_PluginName = "EditorScriptingUtilities"; -} - - diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeCommands.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeCommands.h deleted file mode 100644 index 99b0be26f0..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeCommands.h +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2023, Ayon, 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/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypePythonBridge.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypePythonBridge.h deleted file mode 100644 index 827f76f56b..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypePythonBridge.h +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#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/UE_5.1/Ayon/.gitignore b/openpype/hosts/unreal/integration/UE_5.1/Ayon/.gitignore new file mode 100644 index 0000000000..b32a6f55e5 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/.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.1/Ayon/Ayon.uplugin b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Ayon.uplugin new file mode 100644 index 0000000000..c93a9b4b68 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Ayon.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, + "EngineVersion": "5.0", + "IsExperimentalVersion": false, + "Installed": true, + "Modules": [ + { + "Name": "Ayon", + "Type": "Editor", + "LoadingPhase": "Default" + } + ] +} \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Config/DefaultAyonSettings.ini b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Config/DefaultAyonSettings.ini new file mode 100644 index 0000000000..9ad7f55201 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Config/DefaultAyonSettings.ini @@ -0,0 +1,2 @@ +[/Script/Ayon.AyonSettings] +FolderColor=(R=91,G=197,B=220,A=255) \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Config/FilterPlugin.ini b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Config/FilterPlugin.ini new file mode 100644 index 0000000000..ccebca2f32 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Config/FilterPlugin.ini @@ -0,0 +1,8 @@ +[FilterPlugin] +; This section lists additional files which will be packaged along with your plugin. Paths should be listed relative to the root plugin directory, and +; may include "...", "*", and "?" wildcards to match directories, files, and individual characters respectively. +; +; Examples: +; /README.txt +; /Extras/... +; /Binaries/ThirdParty/*.dll diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Content/Python/init_unreal.py b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Content/Python/init_unreal.py new file mode 100644 index 0000000000..9ed5a2cb19 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Content/Python/init_unreal.py @@ -0,0 +1,30 @@ +import unreal + +openpype_detected = True +try: + from openpype.pipeline import install_host + from openpype.hosts.unreal.api import UnrealHost + + openpype_host = UnrealHost() +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 AyonIntegration(unreal.AyonPythonBridge): + @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.1/Ayon/README.md b/openpype/hosts/unreal/integration/UE_5.1/Ayon/README.md new file mode 100644 index 0000000000..cf0aa622c2 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/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.1/Ayon/Resources/ayon128.png b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Resources/ayon128.png new file mode 100644 index 0000000000000000000000000000000000000000..799d849aa3163ecb16be39c641a6ac30324906b9 GIT binary patch literal 2358 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 D;gw7~ literal 0 HcmV?d00001 diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Resources/ayon512.png b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Resources/ayon512.png new file mode 100644 index 0000000000000000000000000000000000000000..990d5917e232a0644820428fb2790943de5ffaa4 GIT binary patch literal 16705 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 - FOpenPypeCommands::Get().OpenPypeTools, - FExecuteAction::CreateRaw(this, &FOpenPypeModule::MenuPopup), + FAyonCommands::Get().AyonTools, + FExecuteAction::CreateRaw(this, &FAyonModule::MenuPopup), FCanExecuteAction()); PluginCommands->MapAction( - FOpenPypeCommands::Get().OpenPypeToolsDialog, - FExecuteAction::CreateRaw(this, &FOpenPypeModule::MenuDialog), + FAyonCommands::Get().AyonToolsDialog, + FExecuteAction::CreateRaw(this, &FAyonModule::MenuDialog), FCanExecuteAction()); UToolMenus::RegisterStartupCallback( - FSimpleMulticastDelegate::FDelegate::CreateRaw(this, &FOpenPypeModule::RegisterMenus)); + FSimpleMulticastDelegate::FDelegate::CreateRaw(this, &FAyonModule::RegisterMenus)); RegisterSettings(); } -void FOpenPypeModule::ShutdownModule() +void FAyonModule::ShutdownModule() { UToolMenus::UnRegisterStartupCallback(this); UToolMenus::UnregisterOwner(this); - FOpenPypeStyle::Shutdown(); + FAyonStyle::Shutdown(); - FOpenPypeCommands::Unregister(); + FAyonCommands::Unregister(); } -void FOpenPypeModule::RegisterSettings() +void FAyonModule::RegisterSettings() { ISettingsModule& SettingsModule = FModuleManager::LoadModuleChecked("Settings"); @@ -60,10 +59,10 @@ void FOpenPypeModule::RegisterSettings() // TODO: After the movement of the plugin from the game to editor, it might be necessary to move this! ISettingsContainerPtr SettingsContainer = SettingsModule.GetContainer("Project"); - UOpenPypeSettings* Settings = GetMutableDefault(); + UAyonSettings* Settings = GetMutableDefault(); // Register the settings - ISettingsSectionPtr SettingsSection = SettingsModule.RegisterSettings("Project", "OpenPype", "General", + ISettingsSectionPtr SettingsSection = SettingsModule.RegisterSettings("Project", "Ayon", "General", LOCTEXT("RuntimeGeneralSettingsName", "General"), LOCTEXT("RuntimeGeneralSettingsDescription", @@ -75,13 +74,13 @@ void FOpenPypeModule::RegisterSettings() // validate those or just act to settings changes. if (SettingsSection.IsValid()) { - SettingsSection->OnModified().BindRaw(this, &FOpenPypeModule::HandleSettingsSaved); + SettingsSection->OnModified().BindRaw(this, &FAyonModule::HandleSettingsSaved); } } -bool FOpenPypeModule::HandleSettingsSaved() +bool FAyonModule::HandleSettingsSaved() { - UOpenPypeSettings* Settings = GetMutableDefault(); + UAyonSettings* Settings = GetMutableDefault(); bool ResaveSettings = false; // You can put any validation code in here and resave the settings in case an invalid @@ -95,7 +94,7 @@ bool FOpenPypeModule::HandleSettingsSaved() return true; } -void FOpenPypeModule::RegisterMenus() +void FAyonModule::RegisterMenus() { // Owner will be used for cleanup in call to UToolMenus::UnregisterOwner FToolMenuOwnerScoped OwnerScoped(this); @@ -103,21 +102,21 @@ void FOpenPypeModule::RegisterMenus() { UToolMenu* Menu = UToolMenus::Get()->ExtendMenu("LevelEditor.MainMenu.Tools"); { - // FToolMenuSection& Section = Menu->FindOrAddSection("OpenPype"); + // FToolMenuSection& Section = Menu->FindOrAddSection("Ayon"); FToolMenuSection& Section = Menu->AddSection( - "OpenPype", - TAttribute(FText::FromString("OpenPype")), + "Ayon", + TAttribute(FText::FromString("Ayon")), FToolMenuInsert("Programming", EToolMenuInsertType::Before) ); - Section.AddMenuEntryWithCommandList(FOpenPypeCommands::Get().OpenPypeTools, PluginCommands); - Section.AddMenuEntryWithCommandList(FOpenPypeCommands::Get().OpenPypeToolsDialog, PluginCommands); + Section.AddMenuEntryWithCommandList(FAyonCommands::Get().AyonTools, PluginCommands); + Section.AddMenuEntryWithCommandList(FAyonCommands::Get().AyonToolsDialog, PluginCommands); } UToolMenu* ToolbarMenu = UToolMenus::Get()->ExtendMenu("LevelEditor.LevelEditorToolBar.PlayToolBar"); { FToolMenuSection& Section = ToolbarMenu->FindOrAddSection("PluginTools"); { FToolMenuEntry& Entry = Section.AddEntry( - FToolMenuEntry::InitToolBarButton(FOpenPypeCommands::Get().OpenPypeTools)); + FToolMenuEntry::InitToolBarButton(FAyonCommands::Get().AyonTools)); Entry.SetCommandList(PluginCommands); } } @@ -125,16 +124,16 @@ void FOpenPypeModule::RegisterMenus() } -void FOpenPypeModule::MenuPopup() +void FAyonModule::MenuPopup() { - UOpenPypePythonBridge* bridge = UOpenPypePythonBridge::Get(); + UAyonPythonBridge* bridge = UAyonPythonBridge::Get(); bridge->RunInPython_Popup(); } -void FOpenPypeModule::MenuDialog() +void FAyonModule::MenuDialog() { - UOpenPypePythonBridge* bridge = UOpenPypePythonBridge::Get(); + UAyonPythonBridge* bridge = UAyonPythonBridge::Get(); bridge->RunInPython_Dialog(); } -IMPLEMENT_MODULE(FOpenPypeModule, OpenPype) +IMPLEMENT_MODULE(FAyonModule, Ayon) diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/AssetContainer.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonAssetContainer.cpp similarity index 71% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/AssetContainer.cpp rename to openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonAssetContainer.cpp index 06dcd67808..3022757dc8 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/AssetContainer.cpp +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonAssetContainer.cpp @@ -1,36 +1,35 @@ // Fill out your copyright notice in the Description page of Project Settings. -#include "AssetContainer.h" +#include "AyonAssetContainer.h" #include "AssetRegistry/AssetRegistryModule.h" #include "Misc/PackageName.h" -#include "Engine.h" #include "Containers/UnrealString.h" -UAssetContainer::UAssetContainer(const FObjectInitializer& ObjectInitializer) +UAyonAssetContainer::UAyonAssetContainer(const FObjectInitializer& ObjectInitializer) : UAssetUserData(ObjectInitializer) { FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); - FString path = UAssetContainer::GetPathName(); - UE_LOG(LogTemp, Warning, TEXT("UAssetContainer %s"), *path); + FString path = UAyonAssetContainer::GetPathName(); + UE_LOG(LogTemp, Warning, TEXT("UAyonAssetContainer %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); + AssetRegistryModule.Get().OnAssetAdded().AddUObject(this, &UAyonAssetContainer::OnAssetAdded); + AssetRegistryModule.Get().OnAssetRemoved().AddUObject(this, &UAyonAssetContainer::OnAssetRemoved); + AssetRegistryModule.Get().OnAssetRenamed().AddUObject(this, &UAyonAssetContainer::OnAssetRenamed); } -void UAssetContainer::OnAssetAdded(const FAssetData& AssetData) +void UAyonAssetContainer::OnAssetAdded(const FAssetData& AssetData) { TArray split; // get directory of current container - FString selfFullPath = UAssetContainer::GetPathName(); + FString selfFullPath = UAyonAssetContainer::GetPathName(); FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath); // get asset path and class FString assetPath = AssetData.GetFullName(); - FString assetFName = AssetData.ObjectPath.ToString(); + FString assetFName = AssetData.GetObjectPathString(); UE_LOG(LogTemp, Log, TEXT("asset name %s"), *assetFName); // split path assetPath.ParseIntoArray(split, TEXT(" "), true); @@ -50,17 +49,17 @@ void UAssetContainer::OnAssetAdded(const FAssetData& AssetData) } } -void UAssetContainer::OnAssetRemoved(const FAssetData& AssetData) +void UAyonAssetContainer::OnAssetRemoved(const FAssetData& AssetData) { TArray split; // get directory of current container - FString selfFullPath = UAssetContainer::GetPathName(); + FString selfFullPath = UAyonAssetContainer::GetPathName(); FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath); // get asset path and class FString assetPath = AssetData.GetFullName(); - FString assetFName = AssetData.ObjectPath.ToString(); + FString assetFName = AssetData.GetObjectPathString(); // split path assetPath.ParseIntoArray(split, TEXT(" "), true); @@ -68,7 +67,7 @@ void UAssetContainer::OnAssetRemoved(const FAssetData& AssetData) FString assetDir = FPackageName::GetLongPackagePath(*split[1]); // take interest only in paths starting with path of current container - FString path = UAssetContainer::GetPathName(); + FString path = UAyonAssetContainer::GetPathName(); FString lpp = FPackageName::GetLongPackagePath(*path); if (assetDir.StartsWith(*selfDir)) @@ -83,17 +82,17 @@ void UAssetContainer::OnAssetRemoved(const FAssetData& AssetData) } } -void UAssetContainer::OnAssetRenamed(const FAssetData& AssetData, const FString& str) +void UAyonAssetContainer::OnAssetRenamed(const FAssetData& AssetData, const FString& str) { TArray split; // get directory of current container - FString selfFullPath = UAssetContainer::GetPathName(); + FString selfFullPath = UAyonAssetContainer::GetPathName(); FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath); // get asset path and class FString assetPath = AssetData.GetFullName(); - FString assetFName = AssetData.ObjectPath.ToString(); + FString assetFName = AssetData.GetObjectPathString(); // split path assetPath.ParseIntoArray(split, TEXT(" "), true); diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonAssetContainerFactory.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonAssetContainerFactory.cpp new file mode 100644 index 0000000000..086fc1036e --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonAssetContainerFactory.cpp @@ -0,0 +1,20 @@ +#include "AyonAssetContainerFactory.h" +#include "AyonAssetContainer.h" + +UAyonAssetContainerFactory::UAyonAssetContainerFactory(const FObjectInitializer& ObjectInitializer) + : UFactory(ObjectInitializer) +{ + SupportedClass = UAyonAssetContainer::StaticClass(); + bCreateNew = false; + bEditorImport = true; +} + +UObject* UAyonAssetContainerFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) +{ + UAyonAssetContainer* AssetContainer = NewObject(InParent, Class, Name, Flags); + return AssetContainer; +} + +bool UAyonAssetContainerFactory::ShouldShowInNewMenu() const { + return false; +} diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonCommands.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonCommands.cpp new file mode 100644 index 0000000000..566ee1dcd1 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonCommands.cpp @@ -0,0 +1,13 @@ +// Copyright 2023, Ayon, All rights reserved. + +#include "AyonCommands.h" + +#define LOCTEXT_NAMESPACE "FAyonModule" + +void FAyonCommands::RegisterCommands() +{ + UI_COMMAND(AyonTools, "Ayon Tools", "Pipeline tools", EUserInterfaceActionType::Button, FInputChord()); + UI_COMMAND(AyonToolsDialog, "Ayon Tools Dialog", "Pipeline tools dialog", EUserInterfaceActionType::Button, FInputChord()); +} + +#undef LOCTEXT_NAMESPACE diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypeLib.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonLib.cpp similarity index 79% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypeLib.cpp rename to openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonLib.cpp index 34faba1f49..7cfa0c9c30 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypeLib.cpp +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonLib.cpp @@ -1,9 +1,7 @@ // Copyright 2023, Ayon, All rights reserved. -#include "OpenPypeLib.h" +#include "AyonLib.h" #include "AssetViewUtils.h" -#include "Misc/Paths.h" -#include "Misc/ConfigCacheIni.h" #include "UObject/UnrealType.h" /** @@ -13,7 +11,7 @@ * @warning This color will appear only after Editor restart. Is there a better way? */ -bool UOpenPypeLib::SetFolderColor(const FString& FolderPath, const FLinearColor& FolderColor, const bool& bForceAdd) +bool UAyonLib::SetFolderColor(const FString& FolderPath, const FLinearColor& FolderColor, const bool& bForceAdd) { if (AssetViewUtils::DoesFolderExist(FolderPath)) { @@ -31,11 +29,11 @@ bool UOpenPypeLib::SetFolderColor(const FString& FolderPath, const FLinearColor& } /** - * Returns all properties on given object + * Returns all poperties on given object * @param cls - class * @return TArray of properties */ -TArray UOpenPypeLib::GetAllProperties(UClass* cls) +TArray UAyonLib::GetAllProperties(UClass* cls) { TArray Ret; if (cls != nullptr) diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonPythonBridge.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonPythonBridge.cpp new file mode 100644 index 0000000000..0ed4b2f704 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonPythonBridge.cpp @@ -0,0 +1,14 @@ +// Copyright 2023, Ayon, All rights reserved. +#include "AyonPythonBridge.h" + +UAyonPythonBridge* UAyonPythonBridge::Get() +{ + TArray AyonPythonBridgeClasses; + GetDerivedClasses(UAyonPythonBridge::StaticClass(), AyonPythonBridgeClasses); + int32 NumClasses = AyonPythonBridgeClasses.Num(); + if (NumClasses > 0) + { + return Cast(AyonPythonBridgeClasses[NumClasses - 1]->GetDefaultObject()); + } + return nullptr; +}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonSettings.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonSettings.cpp new file mode 100644 index 0000000000..da388fbc8f --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonSettings.cpp @@ -0,0 +1,21 @@ +// Copyright 2023, Ayon, All rights reserved. + +#include "AyonSettings.h" + +#include "Interfaces/IPluginManager.h" +#include "UObject/UObjectGlobals.h" + +/** + * Mainly is used for initializing default values if the DefaultAyonSettings.ini file does not exist in the saved config + */ +UAyonSettings::UAyonSettings(const FObjectInitializer& ObjectInitializer) +{ + + const FString ConfigFilePath = AYON_SETTINGS_FILEPATH; + + // This has to be probably in the future set using the UE Reflection system + FColor Color; + GConfig->GetColor(TEXT("/Script/Ayon.AyonSettings"), TEXT("FolderColor"), Color, ConfigFilePath); + + FolderColor = Color; +} \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonStyle.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonStyle.cpp new file mode 100644 index 0000000000..d88df78735 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonStyle.cpp @@ -0,0 +1,62 @@ +// Copyright 2023, Ayon, All rights reserved. + +#include "AyonStyle.h" +#include "Framework/Application/SlateApplication.h" +#include "Styling/SlateStyleRegistry.h" +#include "Slate/SlateGameResources.h" +#include "Interfaces/IPluginManager.h" +#include "Styling/SlateStyleMacros.h" + +#define RootToContentDir Style->RootToContentDir + +TSharedPtr FAyonStyle::AyonStyleInstance = nullptr; + +void FAyonStyle::Initialize() +{ + if (!AyonStyleInstance.IsValid()) + { + AyonStyleInstance = Create(); + FSlateStyleRegistry::RegisterSlateStyle(*AyonStyleInstance); + } +} + +void FAyonStyle::Shutdown() +{ + FSlateStyleRegistry::UnRegisterSlateStyle(*AyonStyleInstance); + ensure(AyonStyleInstance.IsUnique()); + AyonStyleInstance.Reset(); +} + +FName FAyonStyle::GetStyleSetName() +{ + static FName StyleSetName(TEXT("AyonStyle")); + return StyleSetName; +} + +const FVector2D Icon16x16(16.0f, 16.0f); +const FVector2D Icon20x20(20.0f, 20.0f); +const FVector2D Icon40x40(40.0f, 40.0f); + +TSharedRef< FSlateStyleSet > FAyonStyle::Create() +{ + TSharedRef< FSlateStyleSet > Style = MakeShareable(new FSlateStyleSet("AyonStyle")); + Style->SetContentRoot(IPluginManager::Get().FindPlugin("Ayon")->GetBaseDir() / TEXT("Resources")); + + Style->Set("Ayon.AyonTools", new IMAGE_BRUSH(TEXT("ayon40"), Icon40x40)); + Style->Set("Ayon.AyonToolsDialog", new IMAGE_BRUSH(TEXT("ayon40"), Icon40x40)); + + return Style; +} + +void FAyonStyle::ReloadTextures() +{ + if (FSlateApplication::IsInitialized()) + { + FSlateApplication::Get().GetRenderer()->ReloadTextureResources(); + } +} + +const ISlateStyle& FAyonStyle::Get() +{ + return *AyonStyleInstance; +} diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/Commandlets/AyonActionResult.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/Commandlets/AyonActionResult.cpp new file mode 100644 index 0000000000..2a137e3ed7 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/Commandlets/AyonActionResult.cpp @@ -0,0 +1,40 @@ +// Copyright 2023, Ayon, All rights reserved. + +#include "Commandlets/AyonActionResult.h" +#include "Logging/Ayon_Log.h" + +EAyon_ActionResult::Type& FAyon_ActionResult::GetStatus() +{ + return Status; +} + +FText& FAyon_ActionResult::GetReason() +{ + return Reason; +} + +FAyon_ActionResult::FAyon_ActionResult():Status(EAyon_ActionResult::Type::Ok) +{ + +} + +FAyon_ActionResult::FAyon_ActionResult(const EAyon_ActionResult::Type& InEnum):Status(InEnum) +{ + TryLog(); +} + +FAyon_ActionResult::FAyon_ActionResult(const EAyon_ActionResult::Type& InEnum, const FText& InReason):Status(InEnum), Reason(InReason) +{ + TryLog(); +}; + +bool FAyon_ActionResult::IsProblem() const +{ + return Status != EAyon_ActionResult::Ok; +} + +void FAyon_ActionResult::TryLog() const +{ + if(IsProblem()) + UE_LOG(LogCommandletAyonGenerateProject, Error, TEXT("%s"), *Reason.ToString()); +} diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp new file mode 100644 index 0000000000..ed876c8128 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp @@ -0,0 +1,140 @@ +// Copyright 2023, Ayon, All rights reserved. +#include "Commandlets/Implementations/AyonGenerateProjectCommandlet.h" + +#include "GameProjectUtils.h" +#include "AyonConstants.h" +#include "Commandlets/AyonActionResult.h" +#include "ProjectDescriptor.h" + +int32 UAyonGenerateProjectCommandlet::Main(const FString& CommandLineParams) +{ + //Parses command line parameters & creates structure FProjectInformation + const FAyonGenerateProjectParams ParsedParams = FAyonGenerateProjectParams(CommandLineParams); + ProjectInformation = ParsedParams.GenerateUEProjectInformation(); + + //Creates .uproject & other UE files + EVALUATE_Ayon_ACTION_RESULT(TryCreateProject()); + + //Loads created .uproject + EVALUATE_Ayon_ACTION_RESULT(TryLoadProjectDescriptor()); + + //Adds needed plugin to .uproject + AttachPluginsToProjectDescriptor(); + + //Saves .uproject + EVALUATE_Ayon_ACTION_RESULT(TrySave()); + + //When we are here, there should not be problems in generating Unreal Project for Ayon + return 0; +} + + +FAyonGenerateProjectParams::FAyonGenerateProjectParams(): FAyonGenerateProjectParams("") +{ +} + +FAyonGenerateProjectParams::FAyonGenerateProjectParams(const FString& CommandLineParams): CommandLineParams( + CommandLineParams) +{ + UCommandlet::ParseCommandLine(*CommandLineParams, Tokens, Switches); +} + +FProjectInformation FAyonGenerateProjectParams::GenerateUEProjectInformation() const +{ + FProjectInformation ProjectInformation = FProjectInformation(); + ProjectInformation.ProjectFilename = GetProjectFileName(); + + ProjectInformation.bShouldGenerateCode = IsSwitchPresent("GenerateCode"); + + return ProjectInformation; +} + +FString FAyonGenerateProjectParams::TryGetToken(const int32 Index) const +{ + return Tokens.IsValidIndex(Index) ? Tokens[Index] : ""; +} + +FString FAyonGenerateProjectParams::GetProjectFileName() const +{ + return TryGetToken(0); +} + +bool FAyonGenerateProjectParams::IsSwitchPresent(const FString& Switch) const +{ + return INDEX_NONE != Switches.IndexOfByPredicate([&Switch](const FString& Item) -> bool + { + return Item.Equals(Switch); + } + ); +} + + +UAyonGenerateProjectCommandlet::UAyonGenerateProjectCommandlet() +{ + LogToConsole = true; +} + +FAyon_ActionResult UAyonGenerateProjectCommandlet::TryCreateProject() const +{ + FText FailReason; + FText FailLog; + TArray OutCreatedFiles; + + if (!GameProjectUtils::CreateProject(ProjectInformation, FailReason, FailLog, &OutCreatedFiles)) + return FAyon_ActionResult(EAyon_ActionResult::ProjectNotCreated, FailReason); + return FAyon_ActionResult(); +} + +FAyon_ActionResult UAyonGenerateProjectCommandlet::TryLoadProjectDescriptor() +{ + FText FailReason; + const bool bLoaded = ProjectDescriptor.Load(ProjectInformation.ProjectFilename, FailReason); + + return FAyon_ActionResult(bLoaded ? EAyon_ActionResult::Ok : EAyon_ActionResult::ProjectNotLoaded, FailReason); +} + +void UAyonGenerateProjectCommandlet::AttachPluginsToProjectDescriptor() +{ + FPluginReferenceDescriptor AyonPluginDescriptor; + AyonPluginDescriptor.bEnabled = true; + AyonPluginDescriptor.Name = AyonConstants::Ayon_PluginName; + ProjectDescriptor.Plugins.Add(AyonPluginDescriptor); + + FPluginReferenceDescriptor PythonPluginDescriptor; + PythonPluginDescriptor.bEnabled = true; + PythonPluginDescriptor.Name = AyonConstants::PythonScript_PluginName; + ProjectDescriptor.Plugins.Add(PythonPluginDescriptor); + + FPluginReferenceDescriptor SequencerScriptingPluginDescriptor; + SequencerScriptingPluginDescriptor.bEnabled = true; + SequencerScriptingPluginDescriptor.Name = AyonConstants::SequencerScripting_PluginName; + ProjectDescriptor.Plugins.Add(SequencerScriptingPluginDescriptor); + + FPluginReferenceDescriptor MovieRenderPipelinePluginDescriptor; + MovieRenderPipelinePluginDescriptor.bEnabled = true; + MovieRenderPipelinePluginDescriptor.Name = AyonConstants::MovieRenderPipeline_PluginName; + ProjectDescriptor.Plugins.Add(MovieRenderPipelinePluginDescriptor); + + FPluginReferenceDescriptor EditorScriptingPluginDescriptor; + EditorScriptingPluginDescriptor.bEnabled = true; + EditorScriptingPluginDescriptor.Name = AyonConstants::EditorScriptingUtils_PluginName; + ProjectDescriptor.Plugins.Add(EditorScriptingPluginDescriptor); +} + +FAyon_ActionResult UAyonGenerateProjectCommandlet::TrySave() +{ + FText FailReason; + const bool bSaved = ProjectDescriptor.Save(ProjectInformation.ProjectFilename, FailReason); + + return FAyon_ActionResult(bSaved ? EAyon_ActionResult::Ok : EAyon_ActionResult::ProjectNotSaved, FailReason); +} + +FAyonGenerateProjectParams UAyonGenerateProjectCommandlet::ParseParameters(const FString& Params) const +{ + FAyonGenerateProjectParams ParamsResult; + + TArray Tokens, Switches; + ParseCommandLine(*Params, Tokens, Switches); + + return ParamsResult; +} diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonPublishInstance.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp similarity index 72% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonPublishInstance.cpp rename to openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp index f8d95ac048..0d9cddfd1c 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonPublishInstance.cpp +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp @@ -1,8 +1,9 @@ // Copyright 2023, Ayon, All rights reserved. #pragma once -#include "AyonPublishInstance.h" +#include "OpenPypePublishInstance.h" #include "AssetRegistry/AssetRegistryModule.h" +#include "AssetToolsModule.h" #include "Framework/Notifications/NotificationManager.h" #include "AyonLib.h" #include "AyonSettings.h" @@ -13,7 +14,7 @@ #define REMOVE_INVALID_ENTRIES(VAR) VAR.CompactStable(); \ VAR.Shrink(); -UAyonPublishInstance::UAyonPublishInstance(const FObjectInitializer& ObjectInitializer) +UOpenPypePublishInstance::UOpenPypePublishInstance(const FObjectInitializer& ObjectInitializer) : UPrimaryDataAsset(ObjectInitializer) { const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked< @@ -37,16 +38,16 @@ UAyonPublishInstance::UAyonPublishInstance(const FObjectInitializer& ObjectIniti REMOVE_INVALID_ENTRIES(AssetDataInternal) REMOVE_INVALID_ENTRIES(AssetDataExternal) - AssetRegistryModule.Get().OnAssetAdded().AddUObject(this, &UAyonPublishInstance::OnAssetCreated); - AssetRegistryModule.Get().OnAssetRemoved().AddUObject(this, &UAyonPublishInstance::OnAssetRemoved); - AssetRegistryModule.Get().OnAssetUpdated().AddUObject(this, &UAyonPublishInstance::OnAssetUpdated); + AssetRegistryModule.Get().OnAssetAdded().AddUObject(this, &UOpenPypePublishInstance::OnAssetCreated); + AssetRegistryModule.Get().OnAssetRemoved().AddUObject(this, &UOpenPypePublishInstance::OnAssetRemoved); + AssetRegistryModule.Get().OnAssetUpdated().AddUObject(this, &UOpenPypePublishInstance::OnAssetUpdated); #ifdef WITH_EDITOR - ColorAyonDirs(); + ColorOpenPypeDirs(); #endif } -void UAyonPublishInstance::OnAssetCreated(const FAssetData& InAssetData) +void UOpenPypePublishInstance::OnAssetCreated(const FAssetData& InAssetData) { TArray split; @@ -59,7 +60,7 @@ void UAyonPublishInstance::OnAssetCreated(const FAssetData& InAssetData) return; } - const bool result = IsUnderSameDir(Asset) && Cast(Asset) == nullptr; + const bool result = IsUnderSameDir(Asset) && Cast(Asset) == nullptr; if (result) { @@ -71,9 +72,9 @@ void UAyonPublishInstance::OnAssetCreated(const FAssetData& InAssetData) } } -void UAyonPublishInstance::OnAssetRemoved(const FAssetData& InAssetData) +void UOpenPypePublishInstance::OnAssetRemoved(const FAssetData& InAssetData) { - if (Cast(InAssetData.GetAsset()) == nullptr) + if (Cast(InAssetData.GetAsset()) == nullptr) { if (AssetDataInternal.Contains(nullptr)) { @@ -88,13 +89,13 @@ void UAyonPublishInstance::OnAssetRemoved(const FAssetData& InAssetData) } } -void UAyonPublishInstance::OnAssetUpdated(const FAssetData& InAssetData) +void UOpenPypePublishInstance::OnAssetUpdated(const FAssetData& InAssetData) { REMOVE_INVALID_ENTRIES(AssetDataInternal); REMOVE_INVALID_ENTRIES(AssetDataExternal); } -bool UAyonPublishInstance::IsUnderSameDir(const UObject* InAsset) const +bool UOpenPypePublishInstance::IsUnderSameDir(const UObject* InAsset) const { FString ThisLeft, ThisRight; this->GetPathName().Split(this->GetName(), &ThisLeft, &ThisRight); @@ -104,20 +105,20 @@ bool UAyonPublishInstance::IsUnderSameDir(const UObject* InAsset) const #ifdef WITH_EDITOR -void UAyonPublishInstance::ColorAyonDirs() +void UOpenPypePublishInstance::ColorOpenPypeDirs() { FString PathName = this->GetPathName(); - //Check whether the path contains the defined Ayon folder - if (!PathName.Contains(TEXT("Ayon"))) return; + //Check whether the path contains the defined OpenPype folder + if (!PathName.Contains(TEXT("OpenPype"))) return; //Get the base path for open pype FString PathLeft, PathRight; - PathName.Split(FString("Ayon"), &PathLeft, &PathRight); + PathName.Split(FString("OpenPype"), &PathLeft, &PathRight); if (PathLeft.IsEmpty() || PathRight.IsEmpty()) { - UE_LOG(LogAssetData, Error, TEXT("Failed to retrieve the base Ayon directory!")) + UE_LOG(LogAssetData, Error, TEXT("Failed to retrieve the base OpenPype directory!")) return; } @@ -129,7 +130,7 @@ void UAyonPublishInstance::ColorAyonDirs() //Color the base folder UAyonLib::SetFolderColor(PathName, Settings->GetFolderFColor(), false); - //Get Sub paths, iterate through them and color them according to the folder color in UAyonSettings + //Get Sub paths, iterate through them and color them according to the folder color in UOpenPypeSettings const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked( "AssetRegistry"); @@ -146,7 +147,7 @@ void UAyonPublishInstance::ColorAyonDirs() } } -void UAyonPublishInstance::SendNotification(const FString& Text) const +void UOpenPypePublishInstance::SendNotification(const FString& Text) const { FNotificationInfo Info{FText::FromString(Text)}; @@ -167,13 +168,13 @@ void UAyonPublishInstance::SendNotification(const FString& Text) const } -void UAyonPublishInstance::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) +void UOpenPypePublishInstance::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) { Super::PostEditChangeProperty(PropertyChangedEvent); if (PropertyChangedEvent.ChangeType == EPropertyChangeType::ValueSet && PropertyChangedEvent.Property->GetFName() == GET_MEMBER_NAME_CHECKED( - UAyonPublishInstance, AssetDataExternal)) + UOpenPypePublishInstance, AssetDataExternal)) { // Check for duplicated assets for (const auto& Asset : AssetDataInternal) @@ -186,10 +187,10 @@ void UAyonPublishInstance::PostEditChangeProperty(FPropertyChangedEvent& Propert } } - // Check if no UAyonPublishInstance type assets are included + // Check if no UOpenPypePublishInstance type assets are included for (const auto& Asset : AssetDataExternal) { - if (Cast(Asset.Get()) != nullptr) + if (Cast(Asset.Get()) != nullptr) { AssetDataExternal.Remove(Asset); return SendNotification("You are not allowed to add publish instances!"); diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp new file mode 100644 index 0000000000..a32ebe32cb --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp @@ -0,0 +1,21 @@ +// Copyright 2023, Ayon, All rights reserved. +#include "OpenPypePublishInstanceFactory.h" +#include "OpenPypePublishInstance.h" + +UOpenPypePublishInstanceFactory::UOpenPypePublishInstanceFactory(const FObjectInitializer& ObjectInitializer) + : UFactory(ObjectInitializer) +{ + SupportedClass = UOpenPypePublishInstance::StaticClass(); + bCreateNew = false; + bEditorImport = true; +} + +UObject* UOpenPypePublishInstanceFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) +{ + check(InClass->IsChildOf(UOpenPypePublishInstance::StaticClass())); + return NewObject(InParent, InClass, InName, Flags); +} + +bool UOpenPypePublishInstanceFactory::ShouldShowInNewMenu() const { + return false; +} diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPype.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Ayon.h similarity index 81% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPype.h rename to openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Ayon.h index b89760099b..bb25430411 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPype.h +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Ayon.h @@ -3,10 +3,9 @@ #pragma once #include "CoreMinimal.h" -#include "Modules/ModuleManager.h" -class FOpenPypeModule : public IModuleInterface +class FAyonModule : public IModuleInterface { public: virtual void StartupModule() override; diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/AssetContainer.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonAssetContainer.h similarity index 80% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/AssetContainer.h rename to openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonAssetContainer.h index 9157569c08..d40642b149 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/AssetContainer.h +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonAssetContainer.h @@ -6,20 +6,17 @@ #include "UObject/NoExportTypes.h" #include "Engine/AssetUserData.h" #include "AssetRegistry/AssetData.h" -#include "AssetContainer.generated.h" +#include "AyonAssetContainer.generated.h" -/** - * - */ UCLASS(Blueprintable) -class OPENPYPE_API UAssetContainer : public UAssetUserData +class AYON_API UAyonAssetContainer : public UAssetUserData { GENERATED_BODY() public: - UAssetContainer(const FObjectInitializer& ObjectInitalizer); - // ~UAssetContainer(); + UAyonAssetContainer(const FObjectInitializer& ObjectInitalizer); + // ~UAyonAssetContainer(); UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Assets") TArray assets; diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/AssetContainerFactory.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonAssetContainerFactory.h similarity index 68% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/AssetContainerFactory.h rename to openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonAssetContainerFactory.h index 9095f8a3d7..da424cde2e 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/AssetContainerFactory.h +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonAssetContainerFactory.h @@ -4,18 +4,15 @@ #include "CoreMinimal.h" #include "Factories/Factory.h" -#include "AssetContainerFactory.generated.h" +#include "AyonAssetContainerFactory.generated.h" -/** - * - */ UCLASS() -class OPENPYPE_API UAssetContainerFactory : public UFactory +class AYON_API UAyonAssetContainerFactory : public UFactory { GENERATED_BODY() public: - UAssetContainerFactory(const FObjectInitializer& ObjectInitializer); + UAyonAssetContainerFactory(const FObjectInitializer& ObjectInitializer); virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; virtual bool ShouldShowInNewMenu() const override; }; diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonCommands.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonCommands.h new file mode 100644 index 0000000000..9c40dc8241 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonCommands.h @@ -0,0 +1,24 @@ +// Copyright 2023, Ayon, All rights reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Framework/Commands/Commands.h" +#include "AyonStyle.h" + +class FAyonCommands : public TCommands +{ +public: + + FAyonCommands() + : TCommands(TEXT("Ayon"), NSLOCTEXT("Contexts", "Ayon", "Ayon Tools"), NAME_None, FAyonStyle::GetStyleSetName()) + { + } + + // TCommands<> interface + virtual void RegisterCommands() override; + +public: + TSharedPtr< FUICommandInfo > AyonTools; + TSharedPtr< FUICommandInfo > AyonToolsDialog; +}; diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OPConstants.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonConstants.h similarity index 83% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OPConstants.h rename to openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonConstants.h index f4587f7a50..5fe7c14360 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OPConstants.h +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonConstants.h @@ -1,9 +1,9 @@ // Copyright 2023, Ayon, All rights reserved. #pragma once -namespace OPConstants +namespace AyonConstants { - const FString OP_PluginName = "OpenPype"; + const FString Ayon_PluginName = "Ayon"; const FString PythonScript_PluginName = "PythonScriptPlugin"; const FString SequencerScripting_PluginName = "SequencerScripting"; const FString MovieRenderPipeline_PluginName = "MovieRenderPipeline"; diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeLib.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonLib.h similarity index 75% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeLib.h rename to openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonLib.h index ef4d1027ea..da83b448fb 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeLib.h +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonLib.h @@ -1,12 +1,11 @@ // Copyright 2023, Ayon, All rights reserved. #pragma once -#include "Engine.h" -#include "OpenPypeLib.generated.h" +#include "AyonLib.generated.h" UCLASS(Blueprintable) -class OPENPYPE_API UOpenPypeLib : public UBlueprintFunctionLibrary +class AYON_API UAyonLib : public UBlueprintFunctionLibrary { GENERATED_BODY() diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypePythonBridge.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonPythonBridge.h similarity index 70% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypePythonBridge.h rename to openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonPythonBridge.h index 827f76f56b..3c429fd7d3 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypePythonBridge.h +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonPythonBridge.h @@ -1,16 +1,15 @@ // Copyright 2023, Ayon, All rights reserved. #pragma once -#include "Engine.h" -#include "OpenPypePythonBridge.generated.h" +#include "AyonPythonBridge.generated.h" UCLASS(Blueprintable) -class UOpenPypePythonBridge : public UObject +class UAyonPythonBridge : public UObject { GENERATED_BODY() public: UFUNCTION(BlueprintCallable, Category = Python) - static UOpenPypePythonBridge* Get(); + static UAyonPythonBridge* Get(); UFUNCTION(BlueprintImplementableEvent, Category = Python) void RunInPython_Popup() const; diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeSettings.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonSettings.h similarity index 59% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeSettings.h rename to openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonSettings.h index b818fe0e95..4f12d1a5f2 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeSettings.h +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonSettings.h @@ -1,15 +1,15 @@ -// Copyright 2023, Ayon, All rights reserved. +// Copyright 2023, Ayon, All rights reserved. #pragma once #include "CoreMinimal.h" #include "UObject/Object.h" -#include "OpenPypeSettings.generated.h" +#include "AyonSettings.generated.h" -#define OPENPYPE_SETTINGS_FILEPATH IPluginManager::Get().FindPlugin("OpenPype")->GetBaseDir() / TEXT("Config") / TEXT("DefaultOpenPypeSettings.ini") +#define AYON_SETTINGS_FILEPATH IPluginManager::Get().FindPlugin("Ayon")->GetBaseDir() / TEXT("Config") / TEXT("DefaultAyonSettings.ini") -UCLASS(Config=OpenPypeSettings, DefaultConfig) -class OPENPYPE_API UOpenPypeSettings : public UObject +UCLASS(Config=AyonSettings, DefaultConfig) +class AYON_API UAyonSettings : public UObject { GENERATED_UCLASS_BODY() diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeStyle.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonStyle.h similarity index 79% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeStyle.h rename to openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonStyle.h index 039abe96ef..58f6af656e 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeStyle.h +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonStyle.h @@ -3,7 +3,7 @@ #include "CoreMinimal.h" #include "Styling/SlateStyle.h" -class FOpenPypeStyle +class FAyonStyle { public: static void Initialize(); @@ -15,5 +15,5 @@ public: private: static TSharedRef< class FSlateStyleSet > Create(); - static TSharedPtr< class FSlateStyleSet > OpenPypeStyleInstance; + static TSharedPtr< class FSlateStyleSet > AyonStyleInstance; }; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/Commandlets/OPActionResult.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Commandlets/AyonActionResult.h similarity index 63% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/Commandlets/OPActionResult.h rename to openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Commandlets/AyonActionResult.h index 322a23a3e8..bb995ec452 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/Commandlets/OPActionResult.h +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Commandlets/AyonActionResult.h @@ -3,23 +3,23 @@ #pragma once #include "CoreMinimal.h" -#include "OPActionResult.generated.h" +#include "AyonActionResult.generated.h" /** * @brief This macro returns error code when is problem or does nothing when there is no problem. - * @param ActionResult FOP_ActionResult structure + * @param ActionResult FAyon_ActionResult structure */ -#define EVALUATE_OP_ACTION_RESULT(ActionResult) \ +#define EVALUATE_Ayon_ACTION_RESULT(ActionResult) \ if(ActionResult.IsProblem()) \ return ActionResult.GetStatus(); /** * @brief This enum values are humanly readable mapping of error codes. * Here should be all error codes to be possible find what went wrong. -* TODO: In the future a web document should exists with the mapped error code & what problem occurred & how to repair it... +* TODO: In the future should exists an web document where is mapped error code & what problem occured & how to repair it... */ UENUM() -namespace EOP_ActionResult +namespace EAyon_ActionResult { enum Type { @@ -27,11 +27,11 @@ namespace EOP_ActionResult ProjectNotCreated, ProjectNotLoaded, ProjectNotSaved, - //....Here insert another values + //....Here insert another values //Do not remove! //Usable for looping through enum values - __Last UMETA(Hidden) + __Last UMETA(Hidden) }; } @@ -40,44 +40,44 @@ namespace EOP_ActionResult * @brief This struct holds action result enum and optionally reason of fail */ USTRUCT() -struct FOP_ActionResult +struct FAyon_ActionResult { GENERATED_BODY() public: /** @brief Default constructor usable when there is no problem */ - FOP_ActionResult(); + FAyon_ActionResult(); /** * @brief This constructor initializes variables & attempts to log when is error * @param InEnum Status */ - FOP_ActionResult(const EOP_ActionResult::Type& InEnum); + FAyon_ActionResult(const EAyon_ActionResult::Type& InEnum); /** * @brief This constructor initializes variables & attempts to log when is error * @param InEnum Status * @param InReason Reason of potential fail */ - FOP_ActionResult(const EOP_ActionResult::Type& InEnum, const FText& InReason); + FAyon_ActionResult(const EAyon_ActionResult::Type& InEnum, const FText& InReason); private: /** @brief Action status */ - EOP_ActionResult::Type Status; + EAyon_ActionResult::Type Status; /** @brief Optional reason of fail */ - FText Reason; + FText Reason; public: /** * @brief Checks if there is problematic state - * @return true when status is not equal to EOP_ActionResult::Ok + * @return true when status is not equal to EAyon_ActionResult::Ok */ bool IsProblem() const; - EOP_ActionResult::Type& GetStatus(); + EAyon_ActionResult::Type& GetStatus(); FText& GetReason(); -private: +private: void TryLog() const; }; diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/Commandlets/Implementations/OPGenerateProjectCommandlet.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h similarity index 63% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/Commandlets/Implementations/OPGenerateProjectCommandlet.h rename to openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h index 6a6c6406e7..da8e9af661 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/Commandlets/Implementations/OPGenerateProjectCommandlet.h +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h @@ -3,10 +3,10 @@ #include "GameProjectUtils.h" -#include "Commandlets/OPActionResult.h" +#include "Commandlets/AyonActionResult.h" #include "ProjectDescriptor.h" #include "Commandlets/Commandlet.h" -#include "OPGenerateProjectCommandlet.generated.h" +#include "AyonGenerateProjectCommandlet.generated.h" struct FProjectDescriptor; struct FProjectInformation; @@ -15,7 +15,7 @@ struct FProjectInformation; * @brief Structure which parses command line parameters and generates FProjectInformation */ USTRUCT() -struct FOPGenerateProjectParams +struct FAyonGenerateProjectParams { GENERATED_BODY() @@ -25,8 +25,8 @@ private: TArray Switches; public: - FOPGenerateProjectParams(); - FOPGenerateProjectParams(const FString& CommandLineParams); + FAyonGenerateProjectParams(); + FAyonGenerateProjectParams(const FString& CommandLineParams); FProjectInformation GenerateUEProjectInformation() const; @@ -38,7 +38,7 @@ private: }; UCLASS() -class OPENPYPE_API UOPGenerateProjectCommandlet : public UCommandlet +class AYON_API UAyonGenerateProjectCommandlet : public UCommandlet { GENERATED_BODY() @@ -47,15 +47,15 @@ private: FProjectDescriptor ProjectDescriptor; public: - UOPGenerateProjectCommandlet(); + UAyonGenerateProjectCommandlet(); virtual int32 Main(const FString& CommandLineParams) override; private: - FOPGenerateProjectParams ParseParameters(const FString& Params) const; - FOP_ActionResult TryCreateProject() const; - FOP_ActionResult TryLoadProjectDescriptor(); + FAyonGenerateProjectParams ParseParameters(const FString& Params) const; + FAyon_ActionResult TryCreateProject() const; + FAyon_ActionResult TryLoadProjectDescriptor(); void AttachPluginsToProjectDescriptor(); - FOP_ActionResult TrySave(); + FAyon_ActionResult TrySave(); }; diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Logging/Ayon_Log.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Logging/Ayon_Log.h new file mode 100644 index 0000000000..25b33a63e8 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Logging/Ayon_Log.h @@ -0,0 +1,4 @@ +// Copyright 2023, Ayon, All rights reserved. +#pragma once + +DEFINE_LOG_CATEGORY_STATIC(LogCommandletAyonGenerateProject, Log, All); \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonPublishInstance.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h similarity index 95% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonPublishInstance.h rename to openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h index 4eace68827..03a22c6cde 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonPublishInstance.h +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h @@ -2,15 +2,16 @@ #pragma once #include "Engine.h" -#include "AyonPublishInstance.generated.h" +#include "OpenPypePublishInstance.generated.h" UCLASS(Blueprintable) -class AYON_API UAyonPublishInstance : public UPrimaryDataAsset +class AYON_API UOpenPypePublishInstance : public UPrimaryDataAsset { GENERATED_UCLASS_BODY() public: + /** /** * Retrieves all the assets which are monitored by the Publish Instance (Monitors assets in the directory which is * placed in) @@ -93,8 +94,8 @@ private: #ifdef WITH_EDITOR - void ColorAyonDirs(); - + void ColorOpenPypeDirs(); + void SendNotification(const FString& Text) const; virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonPublishInstanceFactory.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h similarity index 65% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonPublishInstanceFactory.h rename to openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h index 443d618c9a..54dc3e8c1d 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonPublishInstanceFactory.h +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h @@ -3,18 +3,18 @@ #include "CoreMinimal.h" #include "Factories/Factory.h" -#include "AyonPublishInstanceFactory.generated.h" +#include "OpenPypePublishInstanceFactory.generated.h" /** * */ UCLASS() -class AYON_API UAyonPublishInstanceFactory : public UFactory +class AYON_API UOpenPypePublishInstanceFactory : public UFactory { GENERATED_BODY() public: - UAyonPublishInstanceFactory(const FObjectInitializer& ObjectInitializer); + UOpenPypePublishInstanceFactory(const FObjectInitializer& ObjectInitializer); virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; virtual bool ShouldShowInNewMenu() const override; }; diff --git a/openpype/hosts/unreal/integration/UE_5.1/CommandletProject/.gitignore b/openpype/hosts/unreal/integration/UE_5.1/CommandletProject/.gitignore new file mode 100644 index 0000000000..80814ef0a6 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.1/CommandletProject/.gitignore @@ -0,0 +1,41 @@ +# 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 + +/Saved +/DerivedDataCache +/Intermediate +/Binaries +/Content +/Config +/.idea +/.vs \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.1/CommandletProject/CommandletProject.uproject b/openpype/hosts/unreal/integration/UE_5.1/CommandletProject/CommandletProject.uproject new file mode 100644 index 0000000000..fe83346624 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.1/CommandletProject/CommandletProject.uproject @@ -0,0 +1,20 @@ +{ + "FileVersion": 3, + "EngineAssociation": "5.1", + "Category": "", + "Description": "", + "Plugins": [ + { + "Name": "ModelingToolsEditorMode", + "Enabled": true, + "TargetAllowList": [ + "Editor" + ] + }, + { + "Name": "Ayon", + "Enabled": true, + "Type": "Editor" + } + ] +} \ No newline at end of file diff --git a/openpype/hosts/unreal/lib.py b/openpype/hosts/unreal/lib.py index 86ce0bb033..8b8e02f271 100644 --- a/openpype/hosts/unreal/lib.py +++ b/openpype/hosts/unreal/lib.py @@ -229,7 +229,7 @@ def create_unreal_project(project_name: str, print("--- Generating a new project ...") commandlet_cmd = [f'{ue_editor_exe.as_posix()}', f'{cmdlet_project.as_posix()}', - f'-run=OPGenerateProject', + f'-run=AyonGenerateProject', f'{project_file.resolve().as_posix()}'] if dev_mode or preset["dev_mode"]: @@ -319,10 +319,7 @@ def get_path_to_cmdlet_project(ue_version: str) -> Path: cmd_project = Path(os.path.dirname(os.path.abspath(openpype.__file__))) # For now, only tested on Windows (For Linux and Mac it has to be implemented) - if ue_version.split(".")[0] == "4": - cmd_project /= "hosts/unreal/integration/UE_4.7" - elif ue_version.split(".")[0] == "5": - cmd_project /= "hosts/unreal/integration/UE_5.0" + cmd_project /= f"hosts/unreal/integration/UE_{ue_version}" return cmd_project / "CommandletProject/CommandletProject.uproject" @@ -373,7 +370,7 @@ def check_plugin_existence(engine_path: Path, env: dict = None) -> bool: raise RuntimeError("Path to the integration plugin is null!") # Create a path to the plugin in the engine - op_plugin_path: Path = engine_path / "Engine/Plugins/Marketplace/OpenPype" + op_plugin_path: Path = engine_path / "Engine/Plugins/Marketplace/Ayon" if not op_plugin_path.is_dir(): return False @@ -394,7 +391,7 @@ def try_installing_plugin(engine_path: Path, env: dict = None) -> None: raise RuntimeError("Path to the integration plugin is null!") # Create a path to the plugin in the engine - op_plugin_path: Path = engine_path / "Engine/Plugins/Marketplace/OpenPype" + op_plugin_path: Path = engine_path / "Engine/Plugins/Marketplace/Ayon" if not op_plugin_path.is_dir(): op_plugin_path.mkdir(parents=True, exist_ok=True) @@ -420,7 +417,7 @@ def _build_and_move_plugin(engine_path: Path, if uat_path.is_file(): temp_dir: Path = integration_plugin_path.parent / "Temp" temp_dir.mkdir(exist_ok=True) - uplugin_path: Path = integration_plugin_path / "OpenPype.uplugin" + uplugin_path: Path = integration_plugin_path / "Ayon.uplugin" # in order to successfully build the plugin, # It must be built outside the Engine directory and then moved @@ -431,7 +428,7 @@ def _build_and_move_plugin(engine_path: Path, subprocess.run(build_plugin_cmd) # Copy the contents of the 'Temp' dir into the - # 'OpenPype' directory in the engine + # 'Ayon' directory in the engine dir_util.copy_tree(temp_dir.as_posix(), plugin_build_path.as_posix()) # We need to also copy the config folder. diff --git a/openpype/hosts/unreal/ue_workers.py b/openpype/hosts/unreal/ue_workers.py index d1740124a8..1d8023c4d7 100644 --- a/openpype/hosts/unreal/ue_workers.py +++ b/openpype/hosts/unreal/ue_workers.py @@ -93,7 +93,7 @@ class UEProjectGenerationWorker(QtCore.QObject): commandlet_cmd = [ f"{ue_editor_exe.as_posix()}", f"{cmdlet_project.as_posix()}", - "-run=OPGenerateProject", + "-run=AyonGenerateProject", f"{project_file.resolve().as_posix()}", ] @@ -300,7 +300,7 @@ class UEPluginInstallWorker(QtCore.QObject): temp_dir: Path = src_plugin_dir.parent / "Temp" temp_dir.mkdir(exist_ok=True) - uplugin_path: Path = src_plugin_dir / "OpenPype.uplugin" + uplugin_path: Path = src_plugin_dir / "Ayon.uplugin" # in order to successfully build the plugin, # It must be built outside the Engine directory and then moved @@ -332,7 +332,7 @@ class UEPluginInstallWorker(QtCore.QObject): raise RuntimeError(msg) # Copy the contents of the 'Temp' dir into the - # 'OpenPype' directory in the engine + # 'Ayon' directory in the engine dir_util.copy_tree(temp_dir.as_posix(), plugin_build_path.as_posix()) @@ -356,7 +356,7 @@ class UEPluginInstallWorker(QtCore.QObject): # Create a path to the plugin in the engine op_plugin_path = self.engine_path / "Engine/Plugins/Marketplace" \ - "/OpenPype" + "/Ayon" if not op_plugin_path.is_dir(): self.installing.emit("Installing and building the plugin ...") From 10ad412218ce1168abe25e07dee83b2058ba1cb4 Mon Sep 17 00:00:00 2001 From: Joseff Date: Wed, 5 Apr 2023 13:54:00 +0200 Subject: [PATCH 103/360] Added whitespace --- openpype/hosts/unreal/addon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/addon.py b/openpype/hosts/unreal/addon.py index 2fb55a9b11..6a7c6ba941 100644 --- a/openpype/hosts/unreal/addon.py +++ b/openpype/hosts/unreal/addon.py @@ -15,7 +15,7 @@ class UnrealAddon(OpenPypeModule, IHostAddon): """Modify environments to contain all required for implementation.""" # Set OPENPYPE_UNREAL_PLUGIN required for Unreal implementation - ue_version = app.name.replace("-",".") + ue_version = app.name.replace("-", ".") unreal_plugin_path = os.path.join( UNREAL_ROOT_DIR, "integration", f"UE_{ue_version}", "Ayon" ) From d9c67a0bd50fb5c8625632d942c6bf4bf85eb908 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 7 Apr 2023 16:43:53 +0200 Subject: [PATCH 104/360] Improve speed of logging for when its validating a node with many prims. --- .../publish/validate_vdb_output_node.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py b/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py index f9f88b3bf9..e7908ab119 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py +++ b/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py @@ -2,6 +2,7 @@ import pyblish.api import hou from openpype.pipeline import PublishValidationError +import clique class ValidateVDBOutputNode(pyblish.api.InstancePlugin): @@ -56,12 +57,21 @@ class ValidateVDBOutputNode(pyblish.api.InstancePlugin): nr_of_prims = len(prims) # All primitives must be hou.VDB - invalid_prim = False + invalid_prims = [] for prim in prims: if not isinstance(prim, hou.VDB): - cls.log.error("Found non-VDB primitive: %s" % prim) - invalid_prim = True - if invalid_prim: + invalid_prims.append(prim) + if invalid_prims: + # Log all invalid primitives in a short readable way, like 0-5 + collections, remainder = clique.assemble( + str(prim.number()) for prim in invalid_prims + ) + collection = collections[0] + cls.log.error("Found non-VDB primitives for '{}', " + "primitive indices: {}".format( + node.path(), + collection.format("{ranges}") + )) return [instance] nr_of_points = len(geometry.points()) From e2e03346fa5592c39fdd4cf3904a479f8f029f75 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 7 Apr 2023 17:35:29 +0200 Subject: [PATCH 105/360] Fix VDB validation --- ..._node.xml => validate_vdb_output_node.xml} | 0 .../publish/validate_vdb_input_node.py | 52 ------------------- .../publish/validate_vdb_output_node.py | 27 +++++----- 3 files changed, 13 insertions(+), 66 deletions(-) rename openpype/hosts/houdini/plugins/publish/help/{validate_vdb_input_node.xml => validate_vdb_output_node.xml} (100%) delete mode 100644 openpype/hosts/houdini/plugins/publish/validate_vdb_input_node.py diff --git a/openpype/hosts/houdini/plugins/publish/help/validate_vdb_input_node.xml b/openpype/hosts/houdini/plugins/publish/help/validate_vdb_output_node.xml similarity index 100% rename from openpype/hosts/houdini/plugins/publish/help/validate_vdb_input_node.xml rename to openpype/hosts/houdini/plugins/publish/help/validate_vdb_output_node.xml diff --git a/openpype/hosts/houdini/plugins/publish/validate_vdb_input_node.py b/openpype/hosts/houdini/plugins/publish/validate_vdb_input_node.py deleted file mode 100644 index 1f9ccc9c42..0000000000 --- a/openpype/hosts/houdini/plugins/publish/validate_vdb_input_node.py +++ /dev/null @@ -1,52 +0,0 @@ -# -*- coding: utf-8 -*- -import pyblish.api -from openpype.pipeline import ( - PublishValidationError -) - - -class ValidateVDBInputNode(pyblish.api.InstancePlugin): - """Validate that the node connected to the output node is of type VDB. - - Regardless of the amount of VDBs create the output will need to have an - equal amount of VDBs, points, primitives and vertices - - A VDB is an inherited type of Prim, holds the following data: - - Primitives: 1 - - Points: 1 - - Vertices: 1 - - VDBs: 1 - - """ - - order = pyblish.api.ValidatorOrder + 0.1 - families = ["vdbcache"] - hosts = ["houdini"] - label = "Validate Input Node (VDB)" - - def process(self, instance): - invalid = self.get_invalid(instance) - if invalid: - raise PublishValidationError( - self, - "Node connected to the output node is not of type VDB", - title=self.label - ) - - @classmethod - def get_invalid(cls, instance): - - node = instance.data["output_node"] - - prims = node.geometry().prims() - nr_of_prims = len(prims) - - nr_of_points = len(node.geometry().points()) - if nr_of_points != nr_of_prims: - cls.log.error("The number of primitives and points do not match") - return [instance] - - for prim in prims: - if prim.numVertices() != 1: - cls.log.error("Found primitive with more than 1 vertex!") - return [instance] diff --git a/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py b/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py index e7908ab119..ee3b9a0a6a 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py +++ b/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py @@ -1,8 +1,7 @@ # -*- coding: utf-8 -*- import pyblish.api import hou -from openpype.pipeline import PublishValidationError -import clique +from openpype.pipeline import PublishXmlValidationError class ValidateVDBOutputNode(pyblish.api.InstancePlugin): @@ -27,9 +26,9 @@ class ValidateVDBOutputNode(pyblish.api.InstancePlugin): def process(self, instance): invalid = self.get_invalid(instance) if invalid: - raise PublishValidationError( - "Node connected to the output node is not" " of type VDB!", - title=self.label + raise PublishXmlValidationError( + self, + "Node connected to the output node is not" " of type VDB!" ) @classmethod @@ -62,16 +61,16 @@ class ValidateVDBOutputNode(pyblish.api.InstancePlugin): if not isinstance(prim, hou.VDB): invalid_prims.append(prim) if invalid_prims: - # Log all invalid primitives in a short readable way, like 0-5 - collections, remainder = clique.assemble( - str(prim.number()) for prim in invalid_prims + # TODO Log all invalid primitives in a short readable way, like 0-5 + # This logging can be really slow for many primitives, say 20000+ + # which might be fixed by logging only consecutive ranges + cls.log.error( + "Found non-VDB primitives for '{}', " + "primitive indices: {}".format( + node.path(), + ", ".join(prim.number() for prim in invalid_prims) + ) ) - collection = collections[0] - cls.log.error("Found non-VDB primitives for '{}', " - "primitive indices: {}".format( - node.path(), - collection.format("{ranges}") - )) return [instance] nr_of_points = len(geometry.points()) From 3e71ace6b762806d3b4ee097d4bd523d13dbe627 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 7 Apr 2023 17:37:35 +0200 Subject: [PATCH 106/360] Fix logic --- .../hosts/houdini/plugins/publish/validate_vdb_output_node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py b/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py index ee3b9a0a6a..a8fb5007cf 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py +++ b/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py @@ -68,7 +68,7 @@ class ValidateVDBOutputNode(pyblish.api.InstancePlugin): "Found non-VDB primitives for '{}', " "primitive indices: {}".format( node.path(), - ", ".join(prim.number() for prim in invalid_prims) + ", ".join(str(prim.number()) for prim in invalid_prims) ) ) return [instance] From 3f404002e5abc8eee6778fda6a7363a29273329f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 7 Apr 2023 17:41:41 +0200 Subject: [PATCH 107/360] Cosmetics + less aggresive message (no exclamation point) --- .../hosts/houdini/plugins/publish/validate_vdb_output_node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py b/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py index a8fb5007cf..dd9ffc2a12 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py +++ b/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py @@ -28,7 +28,7 @@ class ValidateVDBOutputNode(pyblish.api.InstancePlugin): if invalid: raise PublishXmlValidationError( self, - "Node connected to the output node is not" " of type VDB!" + "Node connected to the output node is not of type VDB." ) @classmethod From 13b72fa57ccdb1353d515eac1da797e024175774 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 7 Apr 2023 17:59:36 +0200 Subject: [PATCH 108/360] Improve logging speed + readability for large number of primitives --- .../publish/validate_vdb_output_node.py | 42 +++++++++++++++++-- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py b/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py index dd9ffc2a12..98a0796fec 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py +++ b/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py @@ -4,6 +4,39 @@ import hou from openpype.pipeline import PublishXmlValidationError +def group_consecutive_numbers(nums): + """ + Args: + nums (list): List of sorted integer numbers. + + Yields: + str: Group ranges as {start}-{end} if more than one number in the range + else it yields {end} + + """ + start = None + end = None + + def _result(a, b): + if a == b: + return "{}".format(a) + else: + return "{}-{}".format(a, b) + + for num in nums: + if start is None: + start = num + end = num + elif num == end + 1: + end = num + else: + yield _result(start, end) + start = num + end = num + if start is not None: + yield _result(start, end) + + class ValidateVDBOutputNode(pyblish.api.InstancePlugin): """Validate that the node connected to the output node is of type VDB. @@ -61,14 +94,15 @@ class ValidateVDBOutputNode(pyblish.api.InstancePlugin): if not isinstance(prim, hou.VDB): invalid_prims.append(prim) if invalid_prims: - # TODO Log all invalid primitives in a short readable way, like 0-5 - # This logging can be really slow for many primitives, say 20000+ - # which might be fixed by logging only consecutive ranges + # Log prim numbers as consecutive ranges so logging isn't very + # slow for large number of primitives cls.log.error( "Found non-VDB primitives for '{}', " "primitive indices: {}".format( node.path(), - ", ".join(str(prim.number()) for prim in invalid_prims) + ", ".join(group_consecutive_numbers( + prim.number() for prim in invalid_prims + )) ) ) return [instance] From 97f13a169b421ec8341f6f3c1b02a1cd5d1b4206 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 7 Apr 2023 18:11:35 +0200 Subject: [PATCH 109/360] Allow output node to be not collected, then correctly show error --- .../hosts/houdini/plugins/publish/validate_vdb_output_node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py b/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py index 98a0796fec..b2b5c63799 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py +++ b/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py @@ -67,7 +67,7 @@ class ValidateVDBOutputNode(pyblish.api.InstancePlugin): @classmethod def get_invalid(cls, instance): - node = instance.data["output_node"] + node = instance.data.get("output_node") if node is None: cls.log.error( "SOP path is not correctly set on " From 6872e32c1eca3b739aed578b6aa059efbf2348bc Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 11 Apr 2023 21:45:16 +0200 Subject: [PATCH 110/360] Generate shelves only in UI mode + defer generation to avoid slow Houdini launch on Windows --- openpype/hosts/houdini/api/pipeline.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/api/pipeline.py b/openpype/hosts/houdini/api/pipeline.py index 45e2f8f87f..62a8fba55e 100644 --- a/openpype/hosts/houdini/api/pipeline.py +++ b/openpype/hosts/houdini/api/pipeline.py @@ -81,7 +81,13 @@ class HoudiniHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): # TODO: make sure this doesn't trigger when # opening with last workfile. _set_context_settings() - shelves.generate_shelves() + + if not IS_HEADLESS: + import hdefereval # noqa, hdefereval is only available in ui mode + # Defer generation of shelves due to issue on Windows where shelf + # initialization during start up delays Houdini UI by minutes + # making it extremely slow to launch. + hdefereval.executeDeferred(shelves.generate_shelves) def has_unsaved_changes(self): return hou.hipFile.hasUnsavedChanges() From a15d8fde0145dc9e7d5fb41a248f7b25af5d3592 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 12 Apr 2023 15:01:49 +0200 Subject: [PATCH 111/360] Specify per Creator where it is listed in Tab search + Add a null node in COP2 or SOP network when generated there --- .../hosts/houdini/api/creator_node_shelves.py | 57 +++++++++++++++---- .../plugins/create/create_alembic_camera.py | 8 +++ .../plugins/create/create_composite.py | 16 +++++- .../plugins/create/create_pointcache.py | 9 +++ .../plugins/create/create_vbd_cache.py | 8 +++ 5 files changed, 87 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/houdini/api/creator_node_shelves.py b/openpype/hosts/houdini/api/creator_node_shelves.py index 3638e14296..bc02b258b7 100644 --- a/openpype/hosts/houdini/api/creator_node_shelves.py +++ b/openpype/hosts/houdini/api/creator_node_shelves.py @@ -12,26 +12,35 @@ import tempfile import logging import os +from openpype.client import get_asset_by_name from openpype.pipeline import registered_host from openpype.pipeline.create import CreateContext from openpype.resources import get_openpype_icon_filepath import hou +import stateutils +import soptoolutils +import cop2toolutils + log = logging.getLogger(__name__) CREATE_SCRIPT = """ from openpype.hosts.houdini.api.creator_node_shelves import create_interactive -create_interactive("{identifier}") +create_interactive("{identifier}", **kwargs) """ -def create_interactive(creator_identifier): +def create_interactive(creator_identifier, **kwargs): """Create a Creator using its identifier interactively. This is used by the generated shelf tools as callback when a user selects the creator from the node tab search menu. + The `kwargs` should be what Houdini passes to the tool create scripts + context. For more information see: + https://www.sidefx.com/docs/houdini/hom/tool_script.html#arguments + Args: creator_identifier (str): The creator identifier of the Creator plugin to create. @@ -58,6 +67,33 @@ def create_interactive(creator_identifier): host = registered_host() context = CreateContext(host) + creator = context.manual_creators.get(creator_identifier) + if not creator: + raise RuntimeError("Invalid creator identifier: " + "{}".format(creator_identifier)) + + pane = stateutils.activePane(kwargs) + if isinstance(pane, hou.NetworkEditor): + pwd = pane.pwd() + subset_name = creator.get_subset_name( + variant=variant, + task_name=context.get_current_task_name(), + asset_doc=get_asset_by_name( + project_name=context.get_current_project_name(), + asset_name=context.get_current_asset_name() + ), + project_name=context.get_current_project_name(), + host_name=context.host_name + ) + + tool_fn = { + hou.sopNodeTypeCategory(): soptoolutils.genericTool, + hou.cop2NodeTypeCategory(): cop2toolutils.genericTool + }.get(pwd.childTypeCategory()) + + if tool_fn != None: + out_null = tool_fn(kwargs, "null") + out_null.setName("OUT_{}".format(subset_name), unique_name=True) before = context.instances_by_id.copy() @@ -135,12 +171,17 @@ def install(): log.debug("Writing OpenPype Creator nodes to shelf: {}".format(filepath)) tools = [] + + default_network_categories = [hou.ropNodeTypeCategory()] with shelves_change_block(): for identifier, creator in create_context.manual_creators.items(): - # TODO: Allow the creator plug-in itself to override the categories - # for where they are shown, by e.g. defining - # `Creator.get_network_categories()` + # Allow the creator plug-in itself to override the categories + # for where they are shown with `Creator.get_network_categories()` + if hasattr(creator, "get_network_categories"): + network_categories = creator.get_network_categories() + else: + network_categories = default_network_categories key = "openpype_create.{}".format(identifier) log.debug(f"Registering {key}") @@ -153,17 +194,13 @@ def install(): creator.label ), "help_url": None, - "network_categories": [ - hou.ropNodeTypeCategory(), - hou.sopNodeTypeCategory() - ], + "network_categories": network_categories, "viewer_categories": [], "cop_viewer_categories": [], "network_op_type": None, "viewer_op_type": None, "locations": ["OpenPype"] } - label = "Create {}".format(creator.label) tool = hou.shelves.tool(key) if tool: diff --git a/openpype/hosts/houdini/plugins/create/create_alembic_camera.py b/openpype/hosts/houdini/plugins/create/create_alembic_camera.py index fec64eb4a1..8c8a5e9eed 100644 --- a/openpype/hosts/houdini/plugins/create/create_alembic_camera.py +++ b/openpype/hosts/houdini/plugins/create/create_alembic_camera.py @@ -3,6 +3,8 @@ from openpype.hosts.houdini.api import plugin from openpype.pipeline import CreatedInstance, CreatorError +import hou + class CreateAlembicCamera(plugin.HoudiniCreator): """Single baked camera from Alembic ROP.""" @@ -47,3 +49,9 @@ class CreateAlembicCamera(plugin.HoudiniCreator): self.lock_parameters(instance_node, to_lock) instance_node.parm("trange").set(1) + + def get_network_categories(self): + return [ + hou.ropNodeTypeCategory(), + hou.objNodeTypeCategory() + ] diff --git a/openpype/hosts/houdini/plugins/create/create_composite.py b/openpype/hosts/houdini/plugins/create/create_composite.py index 45af2b0630..9d4f7969bb 100644 --- a/openpype/hosts/houdini/plugins/create/create_composite.py +++ b/openpype/hosts/houdini/plugins/create/create_composite.py @@ -1,7 +1,9 @@ # -*- coding: utf-8 -*- """Creator plugin for creating composite sequences.""" from openpype.hosts.houdini.api import plugin -from openpype.pipeline import CreatedInstance +from openpype.pipeline import CreatedInstance, CreatorError + +import hou class CreateCompositeSequence(plugin.HoudiniCreator): @@ -35,8 +37,20 @@ class CreateCompositeSequence(plugin.HoudiniCreator): "copoutput": filepath } + if self.selected_nodes: + if len(self.selected_nodes) > 1: + raise CreatorError("More than one item selected.") + path = self.selected_nodes[0].path() + parms["coppath"] = path + instance_node.setParms(parms) # Lock any parameters in this list to_lock = ["prim_to_detail_pattern"] self.lock_parameters(instance_node, to_lock) + + def get_network_categories(self): + return [ + hou.ropNodeTypeCategory(), + hou.cop2NodeTypeCategory() + ] diff --git a/openpype/hosts/houdini/plugins/create/create_pointcache.py b/openpype/hosts/houdini/plugins/create/create_pointcache.py index 6b6b277422..6efa96a42b 100644 --- a/openpype/hosts/houdini/plugins/create/create_pointcache.py +++ b/openpype/hosts/houdini/plugins/create/create_pointcache.py @@ -3,6 +3,8 @@ from openpype.hosts.houdini.api import plugin from openpype.pipeline import CreatedInstance +import hou + class CreatePointCache(plugin.HoudiniCreator): """Alembic ROP to pointcache""" @@ -49,3 +51,10 @@ class CreatePointCache(plugin.HoudiniCreator): # Lock any parameters in this list to_lock = ["prim_to_detail_pattern"] self.lock_parameters(instance_node, to_lock) + + def get_network_categories(self): + return [ + hou.ropNodeTypeCategory(), + hou.sopNodeTypeCategory() + ] + diff --git a/openpype/hosts/houdini/plugins/create/create_vbd_cache.py b/openpype/hosts/houdini/plugins/create/create_vbd_cache.py index 1a5011745f..c015cebd49 100644 --- a/openpype/hosts/houdini/plugins/create/create_vbd_cache.py +++ b/openpype/hosts/houdini/plugins/create/create_vbd_cache.py @@ -3,6 +3,8 @@ from openpype.hosts.houdini.api import plugin from openpype.pipeline import CreatedInstance +import hou + class CreateVDBCache(plugin.HoudiniCreator): """OpenVDB from Geometry ROP""" @@ -34,3 +36,9 @@ class CreateVDBCache(plugin.HoudiniCreator): parms["soppath"] = self.selected_nodes[0].path() instance_node.setParms(parms) + + def get_network_categories(self): + return [ + hou.ropNodeTypeCategory(), + hou.sopNodeTypeCategory() + ] From c6a0b7ff4546bddd687a617cdb05edd4e88f5447 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 12 Apr 2023 15:23:37 +0200 Subject: [PATCH 112/360] Shush hound --- openpype/hosts/houdini/api/creator_node_shelves.py | 2 +- openpype/hosts/houdini/plugins/create/create_pointcache.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/api/creator_node_shelves.py b/openpype/hosts/houdini/api/creator_node_shelves.py index bc02b258b7..cd14090104 100644 --- a/openpype/hosts/houdini/api/creator_node_shelves.py +++ b/openpype/hosts/houdini/api/creator_node_shelves.py @@ -91,7 +91,7 @@ def create_interactive(creator_identifier, **kwargs): hou.cop2NodeTypeCategory(): cop2toolutils.genericTool }.get(pwd.childTypeCategory()) - if tool_fn != None: + if tool_fn is not None: out_null = tool_fn(kwargs, "null") out_null.setName("OUT_{}".format(subset_name), unique_name=True) diff --git a/openpype/hosts/houdini/plugins/create/create_pointcache.py b/openpype/hosts/houdini/plugins/create/create_pointcache.py index 6efa96a42b..df74070fee 100644 --- a/openpype/hosts/houdini/plugins/create/create_pointcache.py +++ b/openpype/hosts/houdini/plugins/create/create_pointcache.py @@ -57,4 +57,3 @@ class CreatePointCache(plugin.HoudiniCreator): hou.ropNodeTypeCategory(), hou.sopNodeTypeCategory() ] - From 43b86f47c304a7dac4325a6b59d0247b53f619a0 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 14 Apr 2023 17:42:42 +0200 Subject: [PATCH 113/360] :recycle: replace OpenPype/Avalon for Ayon --- openpype/hosts/unreal/README.md | 4 +- openpype/hosts/unreal/addon.py | 8 +- openpype/hosts/unreal/api/__init__.py | 2 +- openpype/hosts/unreal/api/helpers.py | 12 +- openpype/hosts/unreal/api/pipeline.py | 138 +++++++++--------- openpype/hosts/unreal/api/plugin.py | 4 +- openpype/hosts/unreal/api/rendering.py | 14 +- openpype/hosts/unreal/api/tools_ui.py | 2 +- .../unreal/hooks/pre_workfile_preparation.py | 10 +- .../integration/UE_4.27/Ayon/Ayon.uplugin | 14 +- .../Ayon/Content/Python/init_unreal.py | 26 ++-- .../unreal/integration/UE_4.27/Ayon/README.md | 12 +- .../Ayon/Source/Ayon/Private/AyonSettings.cpp | 6 +- .../Ayon/Private/OpenPypePublishInstance.cpp | 4 +- .../OpenPypePublishInstanceFactory.cpp | 2 + .../Ayon/Source/Ayon/Public/AyonSettings.h | 8 +- .../Ayon/Public/OpenPypePublishInstance.h | 6 +- .../Public/OpenPypePublishInstanceFactory.h | 2 + .../integration/UE_5.0/Ayon/Ayon.uplugin | 14 +- .../UE_5.0/Ayon/Content/Python/init_unreal.py | 26 ++-- .../unreal/integration/UE_5.0/Ayon/README.md | 12 +- .../Ayon/Private/OpenPypePublishInstance.cpp | 2 + .../OpenPypePublishInstanceFactory.cpp | 2 + .../Ayon/Public/OpenPypePublishInstance.h | 4 +- .../Public/OpenPypePublishInstanceFactory.h | 2 + .../integration/UE_5.1/Ayon/Ayon.uplugin | 14 +- .../UE_5.1/Ayon/Content/Python/init_unreal.py | 26 ++-- .../unreal/integration/UE_5.1/Ayon/README.md | 12 +- .../Ayon/Private/OpenPypePublishInstance.cpp | 2 + .../OpenPypePublishInstanceFactory.cpp | 2 + .../Ayon/Public/OpenPypePublishInstance.h | 4 +- .../Public/OpenPypePublishInstanceFactory.h | 2 + openpype/hosts/unreal/lib.py | 17 ++- .../unreal/plugins/create/create_camera.py | 2 +- .../unreal/plugins/create/create_layout.py | 2 +- .../unreal/plugins/create/create_look.py | 4 +- .../unreal/plugins/create/create_render.py | 10 +- .../plugins/create/create_staticmeshfbx.py | 2 +- .../unreal/plugins/create/create_uasset.py | 2 +- .../plugins/load/load_alembic_animation.py | 12 +- .../unreal/plugins/load/load_animation.py | 10 +- .../hosts/unreal/plugins/load/load_camera.py | 16 +- .../plugins/load/load_geometrycache_abc.py | 10 +- .../hosts/unreal/plugins/load/load_layout.py | 14 +- .../plugins/load/load_layout_existing.py | 12 +- .../plugins/load/load_skeletalmesh_abc.py | 12 +- .../plugins/load/load_skeletalmesh_fbx.py | 12 +- .../plugins/load/load_staticmesh_abc.py | 13 +- .../plugins/load/load_staticmesh_fbx.py | 16 +- .../hosts/unreal/plugins/load/load_uasset.py | 16 +- .../publish/collect_render_instances.py | 10 +- openpype/hosts/unreal/ue_workers.py | 4 +- openpype/pipeline/__init__.py | 2 + openpype/pipeline/constants.py | 2 +- 54 files changed, 306 insertions(+), 292 deletions(-) diff --git a/openpype/hosts/unreal/README.md b/openpype/hosts/unreal/README.md index 0a69b9e0cf..d131105659 100644 --- a/openpype/hosts/unreal/README.md +++ b/openpype/hosts/unreal/README.md @@ -4,6 +4,6 @@ Supported Unreal Engine version is 4.26+ (mainly because of major Python changes ### Project naming Unreal doesn't support project names starting with non-alphabetic character. So names like `123_myProject` are -invalid. If OpenPype detects such name it automatically prepends letter **P** to make it valid name, so `123_myProject` +invalid. If Ayon detects such name it automatically prepends letter **P** to make it valid name, so `123_myProject` will become `P123_myProject`. There is also soft-limit on project name length to be shorter than 20 characters. -Longer names will issue warning in Unreal Editor that there might be possible side effects. \ No newline at end of file +Longer names will issue warning in Unreal Editor that there might be possible side effects. diff --git a/openpype/hosts/unreal/addon.py b/openpype/hosts/unreal/addon.py index 6a7c6ba941..0c42755d37 100644 --- a/openpype/hosts/unreal/addon.py +++ b/openpype/hosts/unreal/addon.py @@ -13,16 +13,16 @@ class UnrealAddon(OpenPypeModule, IHostAddon): def add_implementation_envs(self, env, app): """Modify environments to contain all required for implementation.""" - # Set OPENPYPE_UNREAL_PLUGIN required for Unreal implementation + # Set AYON_UNREAL_PLUGIN required for Unreal implementation ue_version = app.name.replace("-", ".") unreal_plugin_path = os.path.join( UNREAL_ROOT_DIR, "integration", f"UE_{ue_version}", "Ayon" ) - if not env.get("OPENPYPE_UNREAL_PLUGIN") or \ - env.get("OPENPYPE_UNREAL_PLUGIN") != unreal_plugin_path: - env["OPENPYPE_UNREAL_PLUGIN"] = unreal_plugin_path + if not env.get("AYON_UNREAL_PLUGIN") or \ + env.get("AYON_UNREAL_PLUGIN") != unreal_plugin_path: + env["AYON_UNREAL_PLUGIN"] = unreal_plugin_path # Set default environments if are not set via settings defaults = { diff --git a/openpype/hosts/unreal/api/__init__.py b/openpype/hosts/unreal/api/__init__.py index 2618a7677c..de0fce13d5 100644 --- a/openpype/hosts/unreal/api/__init__.py +++ b/openpype/hosts/unreal/api/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""Unreal Editor OpenPype host API.""" +"""Unreal Editor Ayon host API.""" from .plugin import ( UnrealActorCreator, diff --git a/openpype/hosts/unreal/api/helpers.py b/openpype/hosts/unreal/api/helpers.py index 0b6f07f52f..e9ab3fb4c5 100644 --- a/openpype/hosts/unreal/api/helpers.py +++ b/openpype/hosts/unreal/api/helpers.py @@ -2,15 +2,15 @@ import unreal # noqa -class OpenPypeUnrealException(Exception): +class AyonUnrealException(Exception): pass @unreal.uclass() -class OpenPypeHelpers(unreal.OpenPypeLib): - """Class wrapping some useful functions for OpenPype. +class AyonHelpers(unreal.AyonLib): + """Class wrapping some useful functions for Ayon. - This class is extending native BP class in OpenPype Integration Plugin. + This class is extending native BP class in Ayon Integration Plugin. """ @@ -29,13 +29,13 @@ class OpenPypeHelpers(unreal.OpenPypeLib): Example: - OpenPypeHelpers().set_folder_color( + AyonHelpers().set_folder_color( "/Game/Path", unreal.LinearColor(a=1.0, r=1.0, g=0.5, b=0) ) Note: This will take effect only after Editor is restarted. I couldn't - find a way to refresh it. Also this saves the color definition + find a way to refresh it. Also, this saves the color definition into the project config, binding this path with color. So if you delete this path and later re-create, it will set this color again. diff --git a/openpype/hosts/unreal/api/pipeline.py b/openpype/hosts/unreal/api/pipeline.py index 1a7c626984..0d8922d2e6 100644 --- a/openpype/hosts/unreal/api/pipeline.py +++ b/openpype/hosts/unreal/api/pipeline.py @@ -14,7 +14,7 @@ from openpype.pipeline import ( register_creator_plugin_path, deregister_loader_plugin_path, deregister_creator_plugin_path, - AVALON_CONTAINER_ID, + AYON_CONTAINER_ID, ) from openpype.tools.utils import host_tools import openpype.hosts.unreal @@ -22,12 +22,13 @@ from openpype.host import HostBase, ILoadHost, IPublishHost import unreal # noqa +# Rename to Ayon once parent module renames logger = logging.getLogger("openpype.hosts.unreal") -OPENPYPE_CONTAINERS = "OpenPypeContainers" -CONTEXT_CONTAINER = "OpenPype/context.json" +AYON_CONTAINERS = "AyonContainers" +CONTEXT_CONTAINER = "Ayon/context.json" UNREAL_VERSION = semver.VersionInfo( - *os.getenv("OPENPYPE_UNREAL_VERSION").split(".") + *os.getenv("AYON_UNREAL_VERSION").split(".") ) HOST_DIR = os.path.dirname(os.path.abspath(openpype.hosts.unreal.__file__)) @@ -53,14 +54,14 @@ class UnrealHost(HostBase, ILoadHost, IPublishHost): def get_containers(self): return ls() - def show_tools_popup(self): + @staticmethod + def show_tools_popup(): """Show tools popup with actions leading to show other tools.""" - show_tools_popup() - def show_tools_dialog(self): + @staticmethod + def show_tools_dialog(): """Show tools dialog with actions leading to show other tools.""" - show_tools_dialog() def update_context_data(self, data, changes): @@ -72,9 +73,10 @@ class UnrealHost(HostBase, ILoadHost, IPublishHost): with open(op_ctx, "w+") as f: json.dump(data, f) break - except IOError: + except IOError as e: if i == attempts - 1: - raise Exception("Failed to write context data. Aborting.") + raise Exception( + "Failed to write context data. Aborting.") from e unreal.log_warning("Failed to write context data. Retrying...") i += 1 time.sleep(3) @@ -95,19 +97,30 @@ def install(): print("-=" * 40) logo = '''. . - ____________ - / \\ __ \\ - \\ \\ \\/_\\ \\ - \\ \\ _____/ ______ - \\ \\ \\___// \\ \\ - \\ \\____\\ \\ \\_____\\ - \\/_____/ \\/______/ PYPE Club . + · + │ + ·∙/ + ·-∙•∙-· + / \\ /∙· / \\ + ∙ \\ │ / ∙ + \\ \\ · / / + \\\\ ∙ ∙ // + \\\\/ \\// + ___ + │ │ + │ │ + │ │ + │___│ + -· + + ·-─═─-∙ A Y O N ∙-─═─-· + by YNPUT . ''' print(logo) - print("installing OpenPype for Unreal ...") + print("installing Ayon for Unreal ...") print("-=" * 40) - logger.info("installing OpenPype for Unreal") + logger.info("installing Ayon for Unreal") pyblish.api.register_host("unreal") pyblish.api.register_plugin_path(str(PUBLISH_PATH)) register_loader_plugin_path(str(LOAD_PATH)) @@ -117,7 +130,7 @@ def install(): def uninstall(): - """Uninstall Unreal configuration for Avalon.""" + """Uninstall Unreal configuration for Ayon.""" pyblish.api.deregister_plugin_path(str(PUBLISH_PATH)) deregister_loader_plugin_path(str(LOAD_PATH)) deregister_creator_plugin_path(str(CREATE_PATH)) @@ -125,14 +138,14 @@ def uninstall(): def _register_callbacks(): """ - TODO: Implement callbacks if supported by UE4 + TODO: Implement callbacks if supported by UE """ pass def _register_events(): """ - TODO: Implement callbacks if supported by UE4 + TODO: Implement callbacks if supported by UE """ pass @@ -146,32 +159,30 @@ def ls(): """ ar = unreal.AssetRegistryHelpers.get_asset_registry() # UE 5.1 changed how class name is specified - class_name = ["/Script/OpenPype", "AssetContainer"] if UNREAL_VERSION.major == 5 and UNREAL_VERSION.minor > 0 else "AssetContainer" # noqa - openpype_containers = ar.get_assets_by_class(class_name, True) + class_name = ["/Script/Ayon", "AyonAssetContainer"] if UNREAL_VERSION.major == 5 and UNREAL_VERSION.minor > 0 else "AyonAssetContainer" # noqa + ayon_containers = ar.get_assets_by_class(class_name, True) # get_asset_by_class returns AssetData. To get all metadata we need to # load asset. get_tag_values() work only on metadata registered in # Asset Registry Project settings (and there is no way to set it with # python short of editing ini configuration file). - for asset_data in openpype_containers: + for asset_data in ayon_containers: asset = asset_data.get_asset() data = unreal.EditorAssetLibrary.get_metadata_tag_values(asset) data["objectName"] = asset_data.asset_name - data = cast_map_to_str_dict(data) - - yield data + yield cast_map_to_str_dict(data) def ls_inst(): ar = unreal.AssetRegistryHelpers.get_asset_registry() # UE 5.1 changed how class name is specified class_name = [ - "/Script/OpenPype", - "OpenPypePublishInstance" + "/Script/Ayon", + "AyonPublishInstance" ] if ( UNREAL_VERSION.major == 5 and UNREAL_VERSION.minor > 0 - ) else "OpenPypePublishInstance" # noqa + ) else "AyonPublishInstance" # noqa instances = ar.get_assets_by_class(class_name, True) # get_asset_by_class returns AssetData. To get all metadata we need to @@ -182,13 +193,11 @@ def ls_inst(): asset = asset_data.get_asset() data = unreal.EditorAssetLibrary.get_metadata_tag_values(asset) data["objectName"] = asset_data.asset_name - data = cast_map_to_str_dict(data) - - yield data + yield cast_map_to_str_dict(data) def parse_container(container): - """To get data from container, AssetContainer must be loaded. + """To get data from container, AyonAssetContainer must be loaded. Args: container(str): path to container @@ -217,7 +226,7 @@ def containerise(name, namespace, nodes, context, loader=None, suffix="_CON"): Unreal doesn't support *groups* of assets that you can add metadata to. But it does support folders that helps to organize asset. Unfortunately those folders are just that - you cannot add any additional information - to them. OpenPype Integration Plugin is providing way out - Implementing + to them. Ayon Integration Plugin is providing way out - Implementing `AssetContainer` Blueprint class. This class when added to folder can handle metadata on it using standard :func:`unreal.EditorAssetLibrary.set_metadata_tag()` and @@ -226,30 +235,30 @@ def containerise(name, namespace, nodes, context, loader=None, suffix="_CON"): those assets is available as `assets` property. This is list of strings starting with asset type and ending with its path: - `Material /Game/OpenPype/Test/TestMaterial.TestMaterial` + `Material /Game/Ayon/Test/TestMaterial.TestMaterial` """ # 1 - create directory for container root = "/Game" - container_name = "{}{}".format(name, suffix) + container_name = f"{name}{suffix}" new_name = move_assets_to_path(root, container_name, nodes) # 2 - create Asset Container there - path = "{}/{}".format(root, new_name) + path = f"{root}/{new_name}" create_container(container=container_name, path=path) namespace = path data = { - "schema": "openpype:container-2.0", - "id": AVALON_CONTAINER_ID, + "schema": "ayon:container-2.0", + "id": AYON_CONTAINER_ID, "name": new_name, "namespace": namespace, "loader": str(loader), "representation": context["representation"]["_id"], } # 3 - imprint data - imprint("{}/{}".format(path, container_name), data) + imprint(f"{path}/{container_name}", data) return path @@ -257,7 +266,7 @@ def instantiate(root, name, data, assets=None, suffix="_INS"): """Bundles *nodes* into *container*. Marking it with metadata as publishable instance. If assets are provided, - they are moved to new path where `OpenPypePublishInstance` class asset is + they are moved to new path where `AyonPublishInstance` class asset is created and imprinted with metadata. This can then be collected for publishing by Pyblish for example. @@ -271,7 +280,7 @@ def instantiate(root, name, data, assets=None, suffix="_INS"): suffix (str): suffix string to append to instance name """ - container_name = "{}{}".format(name, suffix) + container_name = f"{name}{suffix}" # if we specify assets, create new folder and move them there. If not, # just create empty folder @@ -280,10 +289,10 @@ def instantiate(root, name, data, assets=None, suffix="_INS"): else: new_name = create_folder(root, name) - path = "{}/{}".format(root, new_name) + path = f"{root}/{new_name}" create_publish_instance(instance=container_name, path=path) - imprint("{}/{}".format(path, container_name), data) + imprint(f"{path}/{container_name}", data) def imprint(node, data): @@ -299,7 +308,7 @@ def imprint(node, data): loaded_asset, key, str(value) ) - with unreal.ScopedEditorTransaction("OpenPype containerising"): + with unreal.ScopedEditorTransaction("Ayon containerising"): unreal.EditorAssetLibrary.save_asset(node) @@ -366,11 +375,11 @@ def create_folder(root: str, name: str) -> str: eal = unreal.EditorAssetLibrary index = 1 while True: - if eal.does_directory_exist("{}/{}".format(root, name)): - name = "{}{}".format(name, index) + if eal.does_directory_exist(f"{root}/{name}"): + name = f"{name}{index}" index += 1 else: - eal.make_directory("{}/{}".format(root, name)) + eal.make_directory(f"{root}/{name}") break return name @@ -403,9 +412,7 @@ def move_assets_to_path(root: str, name: str, assets: List[str]) -> str: unreal.log(assets) for asset in assets: loaded = eal.load_asset(asset) - eal.rename_asset( - asset, "{}/{}/{}".format(root, name, loaded.get_name()) - ) + eal.rename_asset(asset, f"{root}/{name}/{loaded.get_name()}") return name @@ -435,14 +442,13 @@ def create_container(container: str, path: str) -> unreal.Object: factory = unreal.AssetContainerFactory() tools = unreal.AssetToolsHelpers().get_asset_tools() - asset = tools.create_asset(container, path, None, factory) - return asset + return tools.create_asset(container, path, None, factory) def create_publish_instance(instance: str, path: str) -> unreal.Object: - """Helper function to create OpenPype Publish Instance on given path. + """Helper function to create Ayon Publish Instance on given path. - This behaves similarly as :func:`create_openpype_container`. + This behaves similarly as :func:`create_ayon_container`. Args: path (str): Path where to create Publish Instance. @@ -460,10 +466,9 @@ def create_publish_instance(instance: str, path: str) -> unreal.Object: ) """ - factory = unreal.OpenPypePublishInstanceFactory() + factory = unreal.AyonPublishInstanceFactory() tools = unreal.AssetToolsHelpers().get_asset_tools() - asset = tools.create_asset(instance, path, None, factory) - return asset + return tools.create_asset(instance, path, None, factory) def cast_map_to_str_dict(umap) -> dict: @@ -494,11 +499,14 @@ def get_subsequences(sequence: unreal.LevelSequence): """ tracks = sequence.get_master_tracks() - subscene_track = None - for t in tracks: - if t.get_class() == unreal.MovieSceneSubTrack.static_class(): - subscene_track = t - break + subscene_track = next( + ( + t + for t in tracks + if t.get_class() == unreal.MovieSceneSubTrack.static_class() + ), + None, + ) if subscene_track is not None and subscene_track.get_sections(): return subscene_track.get_sections() return [] diff --git a/openpype/hosts/unreal/api/plugin.py b/openpype/hosts/unreal/api/plugin.py index d60050a696..26ef69af86 100644 --- a/openpype/hosts/unreal/api/plugin.py +++ b/openpype/hosts/unreal/api/plugin.py @@ -31,7 +31,7 @@ from openpype.pipeline import ( @six.add_metaclass(ABCMeta) class UnrealBaseCreator(Creator): """Base class for Unreal creator plugins.""" - root = "/Game/OpenPype/PublishInstances" + root = "/Game/Ayon/AyonPublishInstances" suffix = "_INS" @staticmethod @@ -243,5 +243,5 @@ class UnrealActorCreator(UnrealBaseCreator): class Loader(LoaderPlugin, ABC): - """This serves as skeleton for future OpenPype specific functionality""" + """This serves as skeleton for future Ayon specific functionality""" pass diff --git a/openpype/hosts/unreal/api/rendering.py b/openpype/hosts/unreal/api/rendering.py index 29e4747f6e..fac1459416 100644 --- a/openpype/hosts/unreal/api/rendering.py +++ b/openpype/hosts/unreal/api/rendering.py @@ -2,6 +2,7 @@ import os import unreal +import openpype.pipeline from openpype.pipeline import Anatomy from openpype.hosts.unreal.api import pipeline @@ -40,7 +41,7 @@ def start_rendering(): # instances = pipeline.ls_inst() instances = [ a for a in assets - if a.get_class().get_name() == "OpenPypePublishInstance"] + if a.get_class().get_name() == "AyonPublishInstance"] inst_data = [] @@ -50,11 +51,12 @@ def start_rendering(): inst_data.append(data) try: - project = os.environ.get("AVALON_PROJECT") + project = openpype.pipeline.get_current_project_name() anatomy = Anatomy(project) root = anatomy.roots['renders'] - except Exception: - raise Exception("Could not find render root in anatomy settings.") + except Exception as e: + raise Exception( + "Could not find render root in anatomy settings.") from e render_dir = f"{root}/{project}" @@ -103,11 +105,11 @@ def start_rendering(): job = queue.allocate_new_job(unreal.MoviePipelineExecutorJob) job.sequence = unreal.SoftObjectPath(i["master_sequence"]) job.map = unreal.SoftObjectPath(i["master_level"]) - job.author = "OpenPype" + job.author = "Ayon" # 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. + # for instance, pass the AyonPublishInstance's path to the job. # job.user_data = "" settings = job.get_configuration().find_or_add_setting_by_class( diff --git a/openpype/hosts/unreal/api/tools_ui.py b/openpype/hosts/unreal/api/tools_ui.py index 8531472142..5a4c689918 100644 --- a/openpype/hosts/unreal/api/tools_ui.py +++ b/openpype/hosts/unreal/api/tools_ui.py @@ -64,7 +64,7 @@ class ToolsDialog(QtWidgets.QDialog): def __init__(self, *args, **kwargs): super(ToolsDialog, self).__init__(*args, **kwargs) - self.setWindowTitle("OpenPype tools") + self.setWindowTitle("Ayon tools") icon = QtGui.QIcon(resources.get_openpype_icon_filepath()) self.setWindowIcon(icon) diff --git a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py index b628d89b2c..cc43bd2ca6 100644 --- a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py +++ b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py @@ -186,15 +186,15 @@ class UnrealPrelaunchHook(PreLaunchHook): project_path.mkdir(parents=True, exist_ok=True) - # Set "OPENPYPE_UNREAL_PLUGIN" to current process environment for + # Set "AYON_UNREAL_PLUGIN" to current process environment for # execution of `create_unreal_project` - if self.launch_context.env.get("OPENPYPE_UNREAL_PLUGIN"): + if self.launch_context.env.get("AYON_UNREAL_PLUGIN"): self.log.info(( f"{self.signature} using Ayon plugin from " - f"{self.launch_context.env.get('OPENPYPE_UNREAL_PLUGIN')}" + f"{self.launch_context.env.get('AYON_UNREAL_PLUGIN')}" )) - env_key = "OPENPYPE_UNREAL_PLUGIN" + env_key = "AYON_UNREAL_PLUGIN" if self.launch_context.env.get(env_key): os.environ[env_key] = self.launch_context.env[env_key] @@ -213,7 +213,7 @@ class UnrealPrelaunchHook(PreLaunchHook): engine_path, project_path) - self.launch_context.env["OPENPYPE_UNREAL_VERSION"] = engine_version + self.launch_context.env["AYON_UNREAL_VERSION"] = engine_version # Append project file to launch arguments self.launch_context.launch_args.append( f"\"{project_file.as_posix()}\"") diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Ayon.uplugin b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Ayon.uplugin index 299a5edc6a..0838da5577 100644 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Ayon.uplugin +++ b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Ayon.uplugin @@ -2,14 +2,14 @@ "FileVersion": 3, "Version": 1, "VersionName": "1.0", - "FriendlyName": "OpenPype", - "Description": "OpenPype Integration", - "Category": "OpenPype.Integration", + "FriendlyName": "Ayon", + "Description": "Ayon Integration", + "Category": "Ayon.Integration", "CreatedBy": "Ondrej Samohel", - "CreatedByURL": "https://openpype.io", - "DocsURL": "https://openpype.io/docs/artist_hosts_unreal", + "CreatedByURL": "https://ayon.ynput.io", + "DocsURL": "https://ayon.ynput.io/docs/artist_hosts_unreal", "MarketplaceURL": "", - "SupportURL": "https://pype.club/", + "SupportURL": "https://ynput.io/", "EngineVersion": "4.27", "CanContainContent": true, "Installed": true, @@ -20,4 +20,4 @@ "LoadingPhase": "Default" } ] -} \ No newline at end of file +} diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Content/Python/init_unreal.py b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Content/Python/init_unreal.py index 9ed5a2cb19..43d6b8b7cf 100644 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Content/Python/init_unreal.py +++ b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Content/Python/init_unreal.py @@ -1,30 +1,30 @@ import unreal -openpype_detected = True +ayon_detected = True try: from openpype.pipeline import install_host from openpype.hosts.unreal.api import UnrealHost - openpype_host = UnrealHost() + ayon_host = UnrealHost() except ImportError as exc: - openpype_host = None - openpype_detected = False - unreal.log_error("OpenPype: cannot load OpenPype [ {} ]".format(exc)) + ayon_host = None + ayon_detected = False + unreal.log_error(f"OpenPype: cannot load Ayon [ {exc} ]") -if openpype_detected: - install_host(openpype_host) +if ayon_detected: + install_host(ayon_host) @unreal.uclass() class AyonIntegration(unreal.AyonPythonBridge): @unreal.ufunction(override=True) def RunInPython_Popup(self): - unreal.log_warning("OpenPype: showing tools popup") - if openpype_detected: - openpype_host.show_tools_popup() + unreal.log_warning("Ayon: showing tools popup") + if ayon_detected: + ayon_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() + unreal.log_warning("Ayon: showing tools dialog") + if ayon_detected: + ayon_host.show_tools_dialog() diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/README.md b/openpype/hosts/unreal/integration/UE_4.27/Ayon/README.md index a08c1ada39..77ae8c7e98 100644 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/README.md +++ b/openpype/hosts/unreal/integration/UE_4.27/Ayon/README.md @@ -1,11 +1,3 @@ -# OpenPype Unreal Integration plugin - UE 4.x +# Ayon Unreal Integration plugin - UE 4.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. +This is plugin for Unreal Editor, creating menu for [Ayon](https://github.com/ynput/OpenPype) tools to run. diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonSettings.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonSettings.cpp index d91dc94db1..509b7268ba 100644 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonSettings.cpp +++ b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonSettings.cpp @@ -9,12 +9,12 @@ */ UAyonSettings::UAyonSettings(const FObjectInitializer& ObjectInitializer) { - - const FString ConfigFilePath = OPENPYPE_SETTINGS_FILEPATH; + + const FString ConfigFilePath = AYON_SETTINGS_FILEPATH; // This has to be probably in the future set using the UE Reflection system FColor Color; GConfig->GetColor(TEXT("/Script/Ayon.AyonSettings"), TEXT("FolderColor"), Color, ConfigFilePath); FolderColor = Color; -} \ No newline at end of file +} diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp index 548bc4c399..320285591e 100644 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp +++ b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp @@ -1,4 +1,6 @@ // Copyright 2023, Ayon, All rights reserved. +// Deprecation warning: this is left here just for backwards compatibility +// and will be removed in next versions of Ayon. #pragma once #include "OpenPypePublishInstance.h" @@ -43,7 +45,7 @@ UOpenPypePublishInstance::UOpenPypePublishInstance(const FObjectInitializer& Obj #ifdef WITH_EDITOR ColorOpenPypeDirs(); #endif - + } void UOpenPypePublishInstance::OnAssetCreated(const FAssetData& InAssetData) diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp index a32ebe32cb..4b4492bd20 100644 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp +++ b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp @@ -1,4 +1,6 @@ // Copyright 2023, Ayon, All rights reserved. +// Deprecation warning: this is left here just for backwards compatibility +// and will be removed in next versions of Ayon. #include "OpenPypePublishInstanceFactory.h" #include "OpenPypePublishInstance.h" diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonSettings.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonSettings.h index 0902019c72..7a93f107c5 100644 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonSettings.h +++ b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonSettings.h @@ -5,19 +5,19 @@ #include "CoreMinimal.h" #include "AyonSettings.generated.h" -#define OPENPYPE_SETTINGS_FILEPATH IPluginManager::Get().FindPlugin("Ayon")->GetBaseDir() / TEXT("Config") / TEXT("DefaultAyonSettings.ini") +#define AYON_SETTINGS_FILEPATH IPluginManager::Get().FindPlugin("Ayon")->GetBaseDir() / TEXT("Config") / TEXT("DefaultAyonSettings.ini") UCLASS(Config=AyonSettings, DefaultConfig) class AYON_API UAyonSettings : public UObject { GENERATED_UCLASS_BODY() - + UFUNCTION(BlueprintCallable, BlueprintPure, Category = Settings) FColor GetFolderFColor() const { return FolderColor; } - + UFUNCTION(BlueprintCallable, BlueprintPure, Category = Settings) FLinearColor GetFolderFLinearColor() const { @@ -28,4 +28,4 @@ protected: UPROPERTY(config, EditAnywhere, Category = Folders) FColor FolderColor = FColor(25,45,223); -}; \ No newline at end of file +}; diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h index 8f2dca5d69..2f3b6aa596 100644 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h +++ b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h @@ -1,4 +1,6 @@ // Copyright 2023, Ayon, All rights reserved. +// Deprecation warning: this is left here just for backwards compatibility +// and will be removed in next versions of Ayon. #pragma once #include "Engine.h" @@ -48,7 +50,7 @@ public: /** * Function for returning all the assets in the container combined. - * + * * @return Returns all the internal and externally added assets into one set (TSet of UObjects). Careful! They are * returning raw pointers. Seems like an issue in UE5 * @@ -94,7 +96,7 @@ private: #ifdef WITH_EDITOR void ColorOpenPypeDirs(); - + void SendNotification(const FString& Text) const; virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h index 54dc3e8c1d..5a02a51d1c 100644 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h +++ b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h @@ -1,4 +1,6 @@ // Copyright 2023, Ayon, All rights reserved. +// Deprecation warning: this is left here just for backwards compatibility +// and will be removed in next versions of Ayon. #pragma once #include "CoreMinimal.h" diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Ayon.uplugin b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Ayon.uplugin index c93a9b4b68..70ed8f6b9a 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Ayon.uplugin +++ b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Ayon.uplugin @@ -2,14 +2,14 @@ "FileVersion": 3, "Version": 1, "VersionName": "1.0", - "FriendlyName": "OpenPype", - "Description": "OpenPype Integration", - "Category": "OpenPype.Integration", + "FriendlyName": "Ayon", + "Description": "Ayon Integration", + "Category": "Ayon.Integration", "CreatedBy": "Ondrej Samohel", - "CreatedByURL": "https://openpype.io", - "DocsURL": "https://openpype.io/docs/artist_hosts_unreal", + "CreatedByURL": "https://ayon.ynput.io", + "DocsURL": "https://ayon.ynput.io/docs/artist_hosts_unreal", "MarketplaceURL": "", - "SupportURL": "https://pype.club/", + "SupportURL": "https://ynput.io/", "CanContainContent": true, "EngineVersion": "5.0", "IsExperimentalVersion": false, @@ -21,4 +21,4 @@ "LoadingPhase": "Default" } ] -} \ No newline at end of file +} diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Content/Python/init_unreal.py b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Content/Python/init_unreal.py index 9ed5a2cb19..c0b1d0ce5d 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Content/Python/init_unreal.py +++ b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Content/Python/init_unreal.py @@ -1,30 +1,30 @@ import unreal -openpype_detected = True +ayon_detected = True try: from openpype.pipeline import install_host from openpype.hosts.unreal.api import UnrealHost - openpype_host = UnrealHost() + ayon_host = UnrealHost() except ImportError as exc: - openpype_host = None - openpype_detected = False - unreal.log_error("OpenPype: cannot load OpenPype [ {} ]".format(exc)) + ayon_host = None + ayon_detected = False + unreal.log_error(f"Ayon: cannot load Ayon integration [ {exc} ]") -if openpype_detected: - install_host(openpype_host) +if ayon_detected: + install_host(ayon_host) @unreal.uclass() class AyonIntegration(unreal.AyonPythonBridge): @unreal.ufunction(override=True) def RunInPython_Popup(self): - unreal.log_warning("OpenPype: showing tools popup") - if openpype_detected: - openpype_host.show_tools_popup() + unreal.log_warning("Ayon: showing tools popup") + if ayon_detected: + ayon_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() + unreal.log_warning("Ayon: showing tools dialog") + if ayon_detected: + ayon_host.show_tools_dialog() diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/README.md b/openpype/hosts/unreal/integration/UE_5.0/Ayon/README.md index cf0aa622c2..865c8cafea 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/README.md +++ b/openpype/hosts/unreal/integration/UE_5.0/Ayon/README.md @@ -1,11 +1,3 @@ -# OpenPype Unreal Integration plugin - UE 5.x +# Ayon Unreal Integration plugin - UE 5.0 -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. +This is plugin for Unreal Editor, creating menu for [Ayon](https://github.com/ynput/OpenPype) tools to run. diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp index 0d9cddfd1c..7a65fd0c98 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp +++ b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp @@ -1,4 +1,6 @@ // Copyright 2023, Ayon, All rights reserved. +// Deprecation warning: this is left here just for backwards compatibility +// and will be removed in next versions of Ayon. #pragma once #include "OpenPypePublishInstance.h" diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp index a32ebe32cb..4b4492bd20 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp +++ b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp @@ -1,4 +1,6 @@ // Copyright 2023, Ayon, All rights reserved. +// Deprecation warning: this is left here just for backwards compatibility +// and will be removed in next versions of Ayon. #include "OpenPypePublishInstanceFactory.h" #include "OpenPypePublishInstance.h" diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h index 03a22c6cde..544cb6d915 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h +++ b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h @@ -1,4 +1,6 @@ // Copyright 2023, Ayon, All rights reserved. +// Deprecation warning: this is left here just for backwards compatibility +// and will be removed in next versions of Ayon. #pragma once #include "Engine.h" @@ -49,7 +51,7 @@ public: /** * Function for returning all the assets in the container combined. - * + * * @return Returns all the internal and externally added assets into one set (TSet of UObjects). Careful! They are * returning raw pointers. Seems like an issue in UE5 * diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h index 54dc3e8c1d..5a02a51d1c 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h +++ b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h @@ -1,4 +1,6 @@ // Copyright 2023, Ayon, All rights reserved. +// Deprecation warning: this is left here just for backwards compatibility +// and will be removed in next versions of Ayon. #pragma once #include "CoreMinimal.h" diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Ayon.uplugin b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Ayon.uplugin index c93a9b4b68..70ed8f6b9a 100644 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Ayon.uplugin +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Ayon.uplugin @@ -2,14 +2,14 @@ "FileVersion": 3, "Version": 1, "VersionName": "1.0", - "FriendlyName": "OpenPype", - "Description": "OpenPype Integration", - "Category": "OpenPype.Integration", + "FriendlyName": "Ayon", + "Description": "Ayon Integration", + "Category": "Ayon.Integration", "CreatedBy": "Ondrej Samohel", - "CreatedByURL": "https://openpype.io", - "DocsURL": "https://openpype.io/docs/artist_hosts_unreal", + "CreatedByURL": "https://ayon.ynput.io", + "DocsURL": "https://ayon.ynput.io/docs/artist_hosts_unreal", "MarketplaceURL": "", - "SupportURL": "https://pype.club/", + "SupportURL": "https://ynput.io/", "CanContainContent": true, "EngineVersion": "5.0", "IsExperimentalVersion": false, @@ -21,4 +21,4 @@ "LoadingPhase": "Default" } ] -} \ No newline at end of file +} diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Content/Python/init_unreal.py b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Content/Python/init_unreal.py index 9ed5a2cb19..c0b1d0ce5d 100644 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Content/Python/init_unreal.py +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Content/Python/init_unreal.py @@ -1,30 +1,30 @@ import unreal -openpype_detected = True +ayon_detected = True try: from openpype.pipeline import install_host from openpype.hosts.unreal.api import UnrealHost - openpype_host = UnrealHost() + ayon_host = UnrealHost() except ImportError as exc: - openpype_host = None - openpype_detected = False - unreal.log_error("OpenPype: cannot load OpenPype [ {} ]".format(exc)) + ayon_host = None + ayon_detected = False + unreal.log_error(f"Ayon: cannot load Ayon integration [ {exc} ]") -if openpype_detected: - install_host(openpype_host) +if ayon_detected: + install_host(ayon_host) @unreal.uclass() class AyonIntegration(unreal.AyonPythonBridge): @unreal.ufunction(override=True) def RunInPython_Popup(self): - unreal.log_warning("OpenPype: showing tools popup") - if openpype_detected: - openpype_host.show_tools_popup() + unreal.log_warning("Ayon: showing tools popup") + if ayon_detected: + ayon_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() + unreal.log_warning("Ayon: showing tools dialog") + if ayon_detected: + ayon_host.show_tools_dialog() diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/README.md b/openpype/hosts/unreal/integration/UE_5.1/Ayon/README.md index cf0aa622c2..417d490548 100644 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/README.md +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/README.md @@ -1,11 +1,3 @@ -# OpenPype Unreal Integration plugin - UE 5.x +# Ayon Unreal Integration plugin - UE 5.1 -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. +This is plugin for Unreal Editor, creating menu for [Ayon](https://github.com/ynput/OpenPype) tools to run. diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp index 0d9cddfd1c..7a65fd0c98 100644 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp @@ -1,4 +1,6 @@ // Copyright 2023, Ayon, All rights reserved. +// Deprecation warning: this is left here just for backwards compatibility +// and will be removed in next versions of Ayon. #pragma once #include "OpenPypePublishInstance.h" diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp index a32ebe32cb..4b4492bd20 100644 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp @@ -1,4 +1,6 @@ // Copyright 2023, Ayon, All rights reserved. +// Deprecation warning: this is left here just for backwards compatibility +// and will be removed in next versions of Ayon. #include "OpenPypePublishInstanceFactory.h" #include "OpenPypePublishInstance.h" diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h index 03a22c6cde..544cb6d915 100644 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h @@ -1,4 +1,6 @@ // Copyright 2023, Ayon, All rights reserved. +// Deprecation warning: this is left here just for backwards compatibility +// and will be removed in next versions of Ayon. #pragma once #include "Engine.h" @@ -49,7 +51,7 @@ public: /** * Function for returning all the assets in the container combined. - * + * * @return Returns all the internal and externally added assets into one set (TSet of UObjects). Careful! They are * returning raw pointers. Seems like an issue in UE5 * diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h index 54dc3e8c1d..5a02a51d1c 100644 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h @@ -1,4 +1,6 @@ // Copyright 2023, Ayon, All rights reserved. +// Deprecation warning: this is left here just for backwards compatibility +// and will be removed in next versions of Ayon. #pragma once #include "CoreMinimal.h" diff --git a/openpype/hosts/unreal/lib.py b/openpype/hosts/unreal/lib.py index 681d55e3b1..aa5b09fda8 100644 --- a/openpype/hosts/unreal/lib.py +++ b/openpype/hosts/unreal/lib.py @@ -189,7 +189,7 @@ def create_unreal_project(project_name: str, As there is no way I know to create a project via command line, this is easiest option. Unreal project file is basically a JSON file. If we find - the `OPENPYPE_UNREAL_PLUGIN` environment variable we assume this is the + the `AYON_UNREAL_PLUGIN` environment variable we assume this is the location of the Integration Plugin and we copy its content to the project folder and enable this plugin. @@ -203,8 +203,7 @@ def create_unreal_project(project_name: str, sources. This will trigger automatically if `Binaries` directory is not found in plugin folders as this indicates this is only source distribution of the plugin. Dev mode - is also set by preset file `unreal/project_setup.json` in - **OPENPYPE_CONFIG**. + is also set in Settings. env (dict, optional): Environment to use. If not set, `os.environ`. Throws: @@ -324,9 +323,11 @@ def get_path_to_uat(engine_path: Path) -> Path: def get_path_to_cmdlet_project(ue_version: str) -> Path: - cmd_project = Path(os.path.dirname(os.path.abspath(openpype.__file__))) + cmd_project = Path(os.path.dirname( + os.path.abspath(os.getenv("OPENPYPE_ROOT")))) - # For now, only tested on Windows (For Linux and Mac it has to be implemented) + # For now, only tested on Windows (For Linux and Mac + # it has to be implemented) cmd_project /= f"hosts/unreal/integration/UE_{ue_version}" return cmd_project / "CommandletProject/CommandletProject.uproject" @@ -372,7 +373,7 @@ def get_build_id(engine_path: Path, ue_version: str) -> str: def check_plugin_existence(engine_path: Path, env: dict = None) -> bool: env = env or os.environ - integration_plugin_path: Path = Path(env.get("OPENPYPE_UNREAL_PLUGIN", "")) + integration_plugin_path: Path = Path(env.get("AYON_UNREAL_PLUGIN", "")) if not os.path.isdir(integration_plugin_path): raise RuntimeError("Path to the integration plugin is null!") @@ -393,7 +394,7 @@ def check_plugin_existence(engine_path: Path, env: dict = None) -> bool: def try_installing_plugin(engine_path: Path, env: dict = None) -> None: env = env or os.environ - integration_plugin_path: Path = Path(env.get("OPENPYPE_UNREAL_PLUGIN", "")) + integration_plugin_path: Path = Path(env.get("AYON_UNREAL_PLUGIN", "")) if not os.path.isdir(integration_plugin_path): raise RuntimeError("Path to the integration plugin is null!") @@ -420,7 +421,7 @@ def _build_and_move_plugin(engine_path: Path, uat_path: Path = get_path_to_uat(engine_path) env = env or os.environ - integration_plugin_path: Path = Path(env.get("OPENPYPE_UNREAL_PLUGIN", "")) + integration_plugin_path: Path = Path(env.get("AYON_UNREAL_PLUGIN", "")) if uat_path.is_file(): temp_dir: Path = integration_plugin_path.parent / "Temp" diff --git a/openpype/hosts/unreal/plugins/create/create_camera.py b/openpype/hosts/unreal/plugins/create/create_camera.py index 642924e2d6..73afb6cefd 100644 --- a/openpype/hosts/unreal/plugins/create/create_camera.py +++ b/openpype/hosts/unreal/plugins/create/create_camera.py @@ -11,7 +11,7 @@ from openpype.hosts.unreal.api.plugin import ( class CreateCamera(UnrealAssetCreator): """Create Camera.""" - identifier = "io.openpype.creators.unreal.camera" + identifier = "io.ayon.creators.unreal.camera" label = "Camera" family = "camera" icon = "fa.camera" diff --git a/openpype/hosts/unreal/plugins/create/create_layout.py b/openpype/hosts/unreal/plugins/create/create_layout.py index 1d2e800a13..e5c7b8ee19 100644 --- a/openpype/hosts/unreal/plugins/create/create_layout.py +++ b/openpype/hosts/unreal/plugins/create/create_layout.py @@ -7,7 +7,7 @@ from openpype.hosts.unreal.api.plugin import ( class CreateLayout(UnrealActorCreator): """Layout output for character rigs.""" - identifier = "io.openpype.creators.unreal.layout" + identifier = "io.ayon.creators.unreal.layout" label = "Layout" family = "layout" icon = "cubes" diff --git a/openpype/hosts/unreal/plugins/create/create_look.py b/openpype/hosts/unreal/plugins/create/create_look.py index f6c73e47e6..e15b57b2ee 100644 --- a/openpype/hosts/unreal/plugins/create/create_look.py +++ b/openpype/hosts/unreal/plugins/create/create_look.py @@ -14,7 +14,7 @@ from openpype.lib import UILabelDef class CreateLook(UnrealAssetCreator): """Shader connections defining shape look.""" - identifier = "io.openpype.creators.unreal.look" + identifier = "io.ayon.creators.unreal.look" label = "Look" family = "look" icon = "paint-brush" @@ -30,7 +30,7 @@ class CreateLook(UnrealAssetCreator): selected_asset = selection[0] - look_directory = "/Game/OpenPype/Looks" + look_directory = "/Game/Ayon/Looks" # Create the folder folder_name = create_folder(look_directory, subset_name) diff --git a/openpype/hosts/unreal/plugins/create/create_render.py b/openpype/hosts/unreal/plugins/create/create_render.py index 5834d2e7a7..2f434d0a60 100644 --- a/openpype/hosts/unreal/plugins/create/create_render.py +++ b/openpype/hosts/unreal/plugins/create/create_render.py @@ -14,7 +14,7 @@ from openpype.lib import UILabelDef class CreateRender(UnrealAssetCreator): """Create instance for sequence for rendering""" - identifier = "io.openpype.creators.unreal.render" + identifier = "io.ayon.creators.unreal.render" label = "Render" family = "render" icon = "eye" @@ -45,22 +45,22 @@ class CreateRender(UnrealAssetCreator): # The asset name is the third element of the path which # contains the map. # To take the asset name, we remove from the path the prefix - # "/Game/OpenPype/" and then we split the path by "/". + # "/Game/Ayon/" and then we split the path by "/". sel_path = selected_asset_path - asset_name = sel_path.replace("/Game/OpenPype/", "").split("/")[0] + asset_name = sel_path.replace("/Game/Ayon/", "").split("/")[0] # Get the master sequence and the master level. # There should be only one sequence and one level in the directory. ar_filter = unreal.ARFilter( class_names=["LevelSequence"], - package_paths=[f"/Game/OpenPype/{asset_name}"], + package_paths=[f"/Game/Ayon/{asset_name}"], recursive_paths=False) sequences = ar.get_assets(ar_filter) master_seq = sequences[0].get_asset().get_path_name() master_seq_obj = sequences[0].get_asset() ar_filter = unreal.ARFilter( class_names=["World"], - package_paths=[f"/Game/OpenPype/{asset_name}"], + package_paths=[f"/Game/Ayon/{asset_name}"], recursive_paths=False) levels = ar.get_assets(ar_filter) master_lvl = levels[0].get_asset().get_path_name() diff --git a/openpype/hosts/unreal/plugins/create/create_staticmeshfbx.py b/openpype/hosts/unreal/plugins/create/create_staticmeshfbx.py index 1acf7084d1..80816d8386 100644 --- a/openpype/hosts/unreal/plugins/create/create_staticmeshfbx.py +++ b/openpype/hosts/unreal/plugins/create/create_staticmeshfbx.py @@ -7,7 +7,7 @@ from openpype.hosts.unreal.api.plugin import ( class CreateStaticMeshFBX(UnrealAssetCreator): """Create Static Meshes as FBX geometry.""" - identifier = "io.openpype.creators.unreal.staticmeshfbx" + identifier = "io.ayon.creators.unreal.staticmeshfbx" label = "Static Mesh (FBX)" family = "unrealStaticMesh" icon = "cube" diff --git a/openpype/hosts/unreal/plugins/create/create_uasset.py b/openpype/hosts/unreal/plugins/create/create_uasset.py index 70f17d478b..c78518e86b 100644 --- a/openpype/hosts/unreal/plugins/create/create_uasset.py +++ b/openpype/hosts/unreal/plugins/create/create_uasset.py @@ -12,7 +12,7 @@ from openpype.hosts.unreal.api.plugin import ( class CreateUAsset(UnrealAssetCreator): """Create UAsset.""" - identifier = "io.openpype.creators.unreal.uasset" + identifier = "io.ayon.creators.unreal.uasset" label = "UAsset" family = "uasset" icon = "cube" diff --git a/openpype/hosts/unreal/plugins/load/load_alembic_animation.py b/openpype/hosts/unreal/plugins/load/load_alembic_animation.py index 496b6056ea..52eea4122a 100644 --- a/openpype/hosts/unreal/plugins/load/load_alembic_animation.py +++ b/openpype/hosts/unreal/plugins/load/load_alembic_animation.py @@ -4,7 +4,7 @@ import os from openpype.pipeline import ( get_representation_path, - AVALON_CONTAINER_ID + AYON_CONTAINER_ID ) from openpype.hosts.unreal.api import plugin from openpype.hosts.unreal.api import pipeline as unreal_pipeline @@ -68,8 +68,8 @@ class AnimationAlembicLoader(plugin.Loader): list(str): list of container content """ - # Create directory for asset and openpype container - root = "/Game/OpenPype/Assets" + # Create directory for asset and ayon container + root = "/Game/Ayon/Assets" asset = context.get('asset').get('name') suffix = "_CON" if asset: @@ -97,8 +97,8 @@ class AnimationAlembicLoader(plugin.Loader): container=container_name, path=asset_dir) data = { - "schema": "openpype:container-2.0", - "id": AVALON_CONTAINER_ID, + "schema": "ayon:container-2.0", + "id": AYON_CONTAINER_ID, "asset": asset, "namespace": asset_dir, "container_name": container_name, @@ -109,7 +109,7 @@ class AnimationAlembicLoader(plugin.Loader): "family": context["representation"]["context"]["family"] } unreal_pipeline.imprint( - "{}/{}".format(asset_dir, container_name), data) + f"{asset_dir}/{container_name}", data) asset_content = unreal.EditorAssetLibrary.list_assets( asset_dir, recursive=True, include_folder=True diff --git a/openpype/hosts/unreal/plugins/load/load_animation.py b/openpype/hosts/unreal/plugins/load/load_animation.py index 1fe0bef462..c1fc7e1e32 100644 --- a/openpype/hosts/unreal/plugins/load/load_animation.py +++ b/openpype/hosts/unreal/plugins/load/load_animation.py @@ -11,7 +11,7 @@ from unreal import MovieSceneSkeletalAnimationSection from openpype.pipeline.context_tools import get_current_project_asset from openpype.pipeline import ( get_representation_path, - AVALON_CONTAINER_ID + AYON_CONTAINER_ID ) from openpype.hosts.unreal.api import plugin from openpype.hosts.unreal.api import pipeline as unreal_pipeline @@ -139,9 +139,9 @@ class AnimationFBXLoader(plugin.Loader): Returns: list(str): list of container content """ - # Create directory for asset and avalon container + # Create directory for asset and Ayon container hierarchy = context.get('asset').get('data').get('parents') - root = "/Game/OpenPype" + root = "/Game/Ayon" asset = context.get('asset').get('name') suffix = "_CON" asset_name = f"{asset}_{name}" if asset else f"{name}" @@ -223,8 +223,8 @@ class AnimationFBXLoader(plugin.Loader): container=container_name, path=asset_dir) data = { - "schema": "openpype:container-2.0", - "id": AVALON_CONTAINER_ID, + "schema": "ayon:container-2.0", + "id": AYON_CONTAINER_ID, "asset": asset, "namespace": asset_dir, "container_name": container_name, diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index 2496440e5f..c082562775 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -8,7 +8,7 @@ from unreal import EditorLevelLibrary from unreal import EditorLevelUtils from openpype.client import get_assets, get_asset_by_name from openpype.pipeline import ( - AVALON_CONTAINER_ID, + AYON_CONTAINER_ID, legacy_io, ) from openpype.hosts.unreal.api import plugin @@ -100,9 +100,9 @@ class CameraLoader(plugin.Loader): list(str): list of container content """ - # Create directory for asset and avalon container + # Create directory for asset and Ayon container hierarchy = context.get('asset').get('data').get('parents') - root = "/Game/OpenPype" + root = "/Game/Ayon" hierarchy_dir = root hierarchy_dir_list = [] for h in hierarchy: @@ -291,8 +291,8 @@ class CameraLoader(plugin.Loader): container=container_name, path=asset_dir) data = { - "schema": "openpype:container-2.0", - "id": AVALON_CONTAINER_ID, + "schema": "ayon:container-2.0", + "id": AYON_CONTAINER_ID, "asset": asset, "namespace": asset_dir, "container_name": container_name, @@ -320,7 +320,7 @@ class CameraLoader(plugin.Loader): def update(self, container, representation): ar = unreal.AssetRegistryHelpers.get_asset_registry() - root = "/Game/OpenPype" + root = "/Game/ayon" asset_dir = container.get('namespace') @@ -378,7 +378,7 @@ class CameraLoader(plugin.Loader): # 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" + root = "/Game/Ayon" namespace = container.get('namespace').replace(f"{root}/", "") ms_asset = namespace.split('/')[0] filter = unreal.ARFilter( @@ -511,7 +511,7 @@ class CameraLoader(plugin.Loader): # 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" + root = "/Game/Ayon" namespace = container.get('namespace').replace(f"{root}/", "") ms_asset = namespace.split('/')[0] filter = unreal.ARFilter( diff --git a/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py b/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py index 6ac3531b40..74101d6a53 100644 --- a/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py +++ b/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py @@ -4,7 +4,7 @@ import os from openpype.pipeline import ( get_representation_path, - AVALON_CONTAINER_ID + AYON_CONTAINER_ID ) from openpype.hosts.unreal.api import plugin from openpype.hosts.unreal.api import pipeline as unreal_pipeline @@ -83,8 +83,8 @@ class PointCacheAlembicLoader(plugin.Loader): list(str): list of container content """ - # Create directory for asset and OpenPype container - root = "/Game/OpenPype/Assets" + # Create directory for asset and Ayon container + root = "/Game/Ayon/Assets" asset = context.get('asset').get('name') suffix = "_CON" if asset: @@ -118,8 +118,8 @@ class PointCacheAlembicLoader(plugin.Loader): container=container_name, path=asset_dir) data = { - "schema": "openpype:container-2.0", - "id": AVALON_CONTAINER_ID, + "schema": "ayon:container-2.0", + "id": AYON_CONTAINER_ID, "asset": asset, "namespace": asset_dir, "container_name": container_name, diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index 63d415a52b..0f25677484 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -19,7 +19,7 @@ from openpype.pipeline import ( loaders_from_representation, load_container, get_representation_path, - AVALON_CONTAINER_ID, + AYON_CONTAINER_ID, legacy_io, ) from openpype.pipeline.context_tools import get_current_project_asset @@ -37,7 +37,7 @@ class LayoutLoader(plugin.Loader): label = "Load Layout" icon = "code-fork" color = "orange" - ASSET_ROOT = "/Game/OpenPype" + ASSET_ROOT = "/Game/Ayon" def _get_asset_containers(self, path): ar = unreal.AssetRegistryHelpers.get_asset_registry() @@ -634,7 +634,7 @@ class LayoutLoader(plugin.Loader): data = get_current_project_settings() create_sequences = data["unreal"]["level_sequences_for_layouts"] - # Create directory for asset and avalon container + # Create directory for asset and Ayon container hierarchy = context.get('asset').get('data').get('parents') root = self.ASSET_ROOT hierarchy_dir = root @@ -749,8 +749,8 @@ class LayoutLoader(plugin.Loader): container=container_name, path=asset_dir) data = { - "schema": "openpype:container-2.0", - "id": AVALON_CONTAINER_ID, + "schema": "ayon:container-2.0", + "id": AYON_CONTAINER_ID, "asset": asset, "namespace": asset_dir, "container_name": container_name, @@ -781,7 +781,7 @@ class LayoutLoader(plugin.Loader): ar = unreal.AssetRegistryHelpers.get_asset_registry() - root = "/Game/OpenPype" + root = "/Game/Ayon" asset_dir = container.get('namespace') context = representation.get("context") @@ -867,7 +867,7 @@ class LayoutLoader(plugin.Loader): data = get_current_project_settings() create_sequences = data["unreal"]["level_sequences_for_layouts"] - root = "/Game/OpenPype" + root = "/Game/Ayon" path = Path(container.get("namespace")) containers = unreal_pipeline.ls() diff --git a/openpype/hosts/unreal/plugins/load/load_layout_existing.py b/openpype/hosts/unreal/plugins/load/load_layout_existing.py index 092b273ded..96ee8cfc25 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout_existing.py +++ b/openpype/hosts/unreal/plugins/load/load_layout_existing.py @@ -10,7 +10,7 @@ from openpype.pipeline import ( loaders_from_representation, load_container, get_representation_path, - AVALON_CONTAINER_ID, + AYON_CONTAINER_ID, legacy_io, ) from openpype.hosts.unreal.api import plugin @@ -28,7 +28,7 @@ class ExistingLayoutLoader(plugin.Loader): label = "Load Layout on Existing Scene" icon = "code-fork" color = "orange" - ASSET_ROOT = "/Game/OpenPype" + ASSET_ROOT = "/Game/Ayon" delete_unmatched_assets = True @@ -59,8 +59,8 @@ class ExistingLayoutLoader(plugin.Loader): container = obj.get_asset() data = { - "schema": "openpype:container-2.0", - "id": AVALON_CONTAINER_ID, + "schema": "ayon:container-2.0", + "id": AYON_CONTAINER_ID, "asset": asset, "namespace": asset_dir, "container_name": container_name, @@ -416,8 +416,8 @@ class ExistingLayoutLoader(plugin.Loader): container=container_name, path=curr_level_path) data = { - "schema": "openpype:container-2.0", - "id": AVALON_CONTAINER_ID, + "schema": "ayon:container-2.0", + "id": AYON_CONTAINER_ID, "asset": asset, "namespace": curr_level_path, "container_name": container_name, diff --git a/openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py b/openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py index e316d255e9..7591d5582f 100644 --- a/openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py +++ b/openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py @@ -4,7 +4,7 @@ import os from openpype.pipeline import ( get_representation_path, - AVALON_CONTAINER_ID + AYON_CONTAINER_ID ) from openpype.hosts.unreal.api import plugin from openpype.hosts.unreal.api import pipeline as unreal_pipeline @@ -70,8 +70,8 @@ class SkeletalMeshAlembicLoader(plugin.Loader): list(str): list of container content """ - # Create directory for asset and openpype container - root = "/Game/OpenPype/Assets" + # Create directory for asset and ayon container + root = "/Game/Ayon/Assets" asset = context.get('asset').get('name') suffix = "_CON" if asset: @@ -98,8 +98,8 @@ class SkeletalMeshAlembicLoader(plugin.Loader): container=container_name, path=asset_dir) data = { - "schema": "openpype:container-2.0", - "id": AVALON_CONTAINER_ID, + "schema": "ayon:container-2.0", + "id": AYON_CONTAINER_ID, "asset": asset, "namespace": asset_dir, "container_name": container_name, @@ -110,7 +110,7 @@ class SkeletalMeshAlembicLoader(plugin.Loader): "family": context["representation"]["context"]["family"] } unreal_pipeline.imprint( - "{}/{}".format(asset_dir, container_name), data) + f"{asset_dir}/{container_name}", data) asset_content = unreal.EditorAssetLibrary.list_assets( asset_dir, recursive=True, include_folder=True diff --git a/openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py b/openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py index 227c5c9292..e9676cde3a 100644 --- a/openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py +++ b/openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py @@ -4,7 +4,7 @@ import os from openpype.pipeline import ( get_representation_path, - AVALON_CONTAINER_ID + AYON_CONTAINER_ID ) from openpype.hosts.unreal.api import plugin from openpype.hosts.unreal.api import pipeline as unreal_pipeline @@ -42,8 +42,8 @@ class SkeletalMeshFBXLoader(plugin.Loader): list(str): list of container content """ - # Create directory for asset and OpenPype container - root = "/Game/OpenPype/Assets" + # Create directory for asset and Ayon container + root = "/Game/Ayon/Assets" if options and options.get("asset_dir"): root = options["asset_dir"] asset = context.get('asset').get('name') @@ -103,8 +103,8 @@ class SkeletalMeshFBXLoader(plugin.Loader): container=container_name, path=asset_dir) data = { - "schema": "openpype:container-2.0", - "id": AVALON_CONTAINER_ID, + "schema": "ayon:container-2.0", + "id": AYON_CONTAINER_ID, "asset": asset, "namespace": asset_dir, "container_name": container_name, @@ -115,7 +115,7 @@ class SkeletalMeshFBXLoader(plugin.Loader): "family": context["representation"]["context"]["family"] } unreal_pipeline.imprint( - "{}/{}".format(asset_dir, container_name), data) + f"{asset_dir}/{container_name}", data) asset_content = unreal.EditorAssetLibrary.list_assets( asset_dir, recursive=True, include_folder=True diff --git a/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py b/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py index c7841cef53..c435b8843d 100644 --- a/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py +++ b/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py @@ -4,7 +4,7 @@ import os from openpype.pipeline import ( get_representation_path, - AVALON_CONTAINER_ID + AYON_CONTAINER_ID ) from openpype.hosts.unreal.api import plugin from openpype.hosts.unreal.api import pipeline as unreal_pipeline @@ -75,8 +75,8 @@ class StaticMeshAlembicLoader(plugin.Loader): list(str): list of container content """ - # Create directory for asset and OpenPype container - root = "/Game/OpenPype/Assets" + # Create directory for asset and Ayon container + root = "/Game/Ayon/Assets" asset = context.get('asset').get('name') suffix = "_CON" if asset: @@ -108,8 +108,8 @@ class StaticMeshAlembicLoader(plugin.Loader): container=container_name, path=asset_dir) data = { - "schema": "openpype:container-2.0", - "id": AVALON_CONTAINER_ID, + "schema": "ayon:container-2.0", + "id": AYON_CONTAINER_ID, "asset": asset, "namespace": asset_dir, "container_name": container_name, @@ -119,8 +119,7 @@ class StaticMeshAlembicLoader(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) asset_content = unreal.EditorAssetLibrary.list_assets( asset_dir, recursive=True, include_folder=True diff --git a/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py b/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py index 351c686095..e416256486 100644 --- a/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py +++ b/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py @@ -4,7 +4,7 @@ import os from openpype.pipeline import ( get_representation_path, - AVALON_CONTAINER_ID + AYON_CONTAINER_ID ) from openpype.hosts.unreal.api import plugin from openpype.hosts.unreal.api import pipeline as unreal_pipeline @@ -68,8 +68,8 @@ class StaticMeshFBXLoader(plugin.Loader): list(str): list of container content """ - # Create directory for asset and OpenPype container - root = "/Game/OpenPype/Assets" + # Create directory for asset and Ayon container + root = "/Game/Ayon/Assets" if options and options.get("asset_dir"): root = options["asset_dir"] asset = context.get('asset').get('name') @@ -81,7 +81,8 @@ class StaticMeshFBXLoader(plugin.Loader): tools = unreal.AssetToolsHelpers().get_asset_tools() asset_dir, container_name = tools.create_unique_asset_name( - "{}/{}/{}".format(root, asset, name), suffix="") + f"{root}/{asset}/{name}", suffix="" + ) container_name += suffix @@ -96,8 +97,8 @@ class StaticMeshFBXLoader(plugin.Loader): container=container_name, path=asset_dir) data = { - "schema": "openpype:container-2.0", - "id": AVALON_CONTAINER_ID, + "schema": "ayon:container-2.0", + "id": AYON_CONTAINER_ID, "asset": asset, "namespace": asset_dir, "container_name": container_name, @@ -107,8 +108,7 @@ class StaticMeshFBXLoader(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) asset_content = unreal.EditorAssetLibrary.list_assets( asset_dir, recursive=True, include_folder=True diff --git a/openpype/hosts/unreal/plugins/load/load_uasset.py b/openpype/hosts/unreal/plugins/load/load_uasset.py index eccfc7b445..b1a4fc6971 100644 --- a/openpype/hosts/unreal/plugins/load/load_uasset.py +++ b/openpype/hosts/unreal/plugins/load/load_uasset.py @@ -5,7 +5,7 @@ import shutil from openpype.pipeline import ( get_representation_path, - AVALON_CONTAINER_ID + AYON_CONTAINER_ID ) from openpype.hosts.unreal.api import plugin from openpype.hosts.unreal.api import pipeline as unreal_pipeline @@ -38,8 +38,8 @@ class UAssetLoader(plugin.Loader): list(str): list of container content """ - # Create directory for asset and OpenPype container - root = "/Game/OpenPype/Assets" + # Create directory for asset and Ayon container + root = "/Game/Ayon/Assets" asset = context.get('asset').get('name') suffix = "_CON" if asset: @@ -49,7 +49,8 @@ class UAssetLoader(plugin.Loader): tools = unreal.AssetToolsHelpers().get_asset_tools() asset_dir, container_name = tools.create_unique_asset_name( - "{}/{}/{}".format(root, asset, name), suffix="") + f"{root}/{asset}/{name}", suffix="" + ) container_name += suffix @@ -67,8 +68,8 @@ class UAssetLoader(plugin.Loader): container=container_name, path=asset_dir) data = { - "schema": "openpype:container-2.0", - "id": AVALON_CONTAINER_ID, + "schema": "ayon:container-2.0", + "id": AYON_CONTAINER_ID, "asset": asset, "namespace": asset_dir, "container_name": container_name, @@ -78,8 +79,7 @@ class UAssetLoader(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) asset_content = unreal.EditorAssetLibrary.list_assets( asset_dir, recursive=True, include_folder=True diff --git a/openpype/hosts/unreal/plugins/publish/collect_render_instances.py b/openpype/hosts/unreal/plugins/publish/collect_render_instances.py index cb28f4bf60..6697a6b90d 100644 --- a/openpype/hosts/unreal/plugins/publish/collect_render_instances.py +++ b/openpype/hosts/unreal/plugins/publish/collect_render_instances.py @@ -3,6 +3,7 @@ from pathlib import Path import unreal +from openpype.pipeline import get_current_project_name from openpype.pipeline import Anatomy from openpype.hosts.unreal.api import pipeline import pyblish.api @@ -81,12 +82,13 @@ class CollectRenderInstances(pyblish.api.InstancePlugin): self.log.debug(f"new instance data: {new_data}") try: - project = os.environ.get("AVALON_PROJECT") + project = get_current_project_name() anatomy = Anatomy(project) root = anatomy.roots['renders'] - except Exception: - raise Exception( - "Could not find render root in anatomy settings.") + except Exception as e: + raise Exception(( + "Could not find render root " + "in anatomy settings.")) from e render_dir = f"{root}/{project}/{s.get('output')}" render_path = Path(render_dir) diff --git a/openpype/hosts/unreal/ue_workers.py b/openpype/hosts/unreal/ue_workers.py index 1d8023c4d7..e7a690ac9c 100644 --- a/openpype/hosts/unreal/ue_workers.py +++ b/openpype/hosts/unreal/ue_workers.py @@ -286,7 +286,7 @@ class UEPluginInstallWorker(QtCore.QObject): def _build_and_move_plugin(self, plugin_build_path: Path): uat_path: Path = ue_lib.get_path_to_uat(self.engine_path) - src_plugin_dir = Path(self.env.get("OPENPYPE_UNREAL_PLUGIN", "")) + src_plugin_dir = Path(self.env.get("AYON_UNREAL_PLUGIN", "")) if not os.path.isdir(src_plugin_dir): msg = "Path to the integration plugin is null!" @@ -347,7 +347,7 @@ class UEPluginInstallWorker(QtCore.QObject): dir_util.remove_tree(temp_dir.as_posix()) def run(self): - src_plugin_dir = Path(self.env.get("OPENPYPE_UNREAL_PLUGIN", "")) + src_plugin_dir = Path(self.env.get("AYON_UNREAL_PLUGIN", "")) if not os.path.isdir(src_plugin_dir): msg = "Path to the integration plugin is null!" diff --git a/openpype/pipeline/__init__.py b/openpype/pipeline/__init__.py index 7a2ef59a5a..d656d58adc 100644 --- a/openpype/pipeline/__init__.py +++ b/openpype/pipeline/__init__.py @@ -1,5 +1,6 @@ from .constants import ( AVALON_CONTAINER_ID, + AYON_CONTAINER_ID, HOST_WORKFILE_EXTENSIONS, ) @@ -99,6 +100,7 @@ uninstall = uninstall_host __all__ = ( "AVALON_CONTAINER_ID", + "AYON_CONTAINER_ID", "HOST_WORKFILE_EXTENSIONS", # --- MongoDB --- diff --git a/openpype/pipeline/constants.py b/openpype/pipeline/constants.py index e6496cbf95..755a5fb380 100644 --- a/openpype/pipeline/constants.py +++ b/openpype/pipeline/constants.py @@ -1,5 +1,5 @@ # Metadata ID of loaded container into scene -AVALON_CONTAINER_ID = "pyblish.avalon.container" +AVALON_CONTAINER_ID = AYON_CONTAINER_ID = "pyblish.avalon.container" # TODO get extensions from host implementations HOST_WORKFILE_EXTENSIONS = { From db72ed2e634389e4f57ee555585efb1b934c3e32 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 18 Apr 2023 15:46:47 +0800 Subject: [PATCH 114/360] the resolution and the frame range set correctly before saving the scene --- openpype/hosts/max/api/pipeline.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/max/api/pipeline.py b/openpype/hosts/max/api/pipeline.py index dacc402318..957c674518 100644 --- a/openpype/hosts/max/api/pipeline.py +++ b/openpype/hosts/max/api/pipeline.py @@ -52,8 +52,11 @@ class MaxHost(HostBase, IWorkfileHost, ILoadHost, INewPublisher): def context_setting(): return lib.set_context_setting() + rt.callbacks.addScript(rt.Name('systemPostNew'), context_setting) + rt.callbacks.addScript(rt.Name('filePreSave'), + context_setting) def has_unsaved_changes(self): # TODO: how to get it from 3dsmax? From 973caf2d6de18b1f63384aef8089a0908878b4ba Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 18 Apr 2023 15:56:13 +0800 Subject: [PATCH 115/360] not adding the before save callback --- openpype/hosts/max/api/pipeline.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/max/api/pipeline.py b/openpype/hosts/max/api/pipeline.py index 957c674518..50fe30b299 100644 --- a/openpype/hosts/max/api/pipeline.py +++ b/openpype/hosts/max/api/pipeline.py @@ -55,8 +55,6 @@ class MaxHost(HostBase, IWorkfileHost, ILoadHost, INewPublisher): rt.callbacks.addScript(rt.Name('systemPostNew'), context_setting) - rt.callbacks.addScript(rt.Name('filePreSave'), - context_setting) def has_unsaved_changes(self): # TODO: how to get it from 3dsmax? From 5f78ba9bb8a4c5747618b6f669d9c0ddd287a363 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 18 Apr 2023 16:06:20 +0800 Subject: [PATCH 116/360] adding callback to reset the resolution when opening the file --- openpype/hosts/max/api/pipeline.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/max/api/pipeline.py b/openpype/hosts/max/api/pipeline.py index 50fe30b299..ac841d395f 100644 --- a/openpype/hosts/max/api/pipeline.py +++ b/openpype/hosts/max/api/pipeline.py @@ -56,6 +56,9 @@ class MaxHost(HostBase, IWorkfileHost, ILoadHost, INewPublisher): rt.callbacks.addScript(rt.Name('systemPostNew'), context_setting) + rt.callbacks.addScript(rt.Name('filePostOpen'), + context_setting) + def has_unsaved_changes(self): # TODO: how to get it from 3dsmax? return True From b3044398fc9181db2d2230f9f0f5cc1de7e9d297 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 19 Apr 2023 23:43:47 +0200 Subject: [PATCH 117/360] Improve validation report + allow to select the invalid node --- openpype/hosts/houdini/api/action.py | 46 +++++++ .../publish/help/validate_vdb_output_node.xml | 25 ++-- .../publish/validate_vdb_output_node.py | 112 ++++++++++++------ 3 files changed, 135 insertions(+), 48 deletions(-) create mode 100644 openpype/hosts/houdini/api/action.py diff --git a/openpype/hosts/houdini/api/action.py b/openpype/hosts/houdini/api/action.py new file mode 100644 index 0000000000..27e8ce55bb --- /dev/null +++ b/openpype/hosts/houdini/api/action.py @@ -0,0 +1,46 @@ +import pyblish.api +import hou + +from openpype.pipeline.publish import get_errored_instances_from_context + + +class SelectInvalidAction(pyblish.api.Action): + """Select invalid nodes in Maya when plug-in failed. + + To retrieve the invalid nodes this assumes a static `get_invalid()` + method is available on the plugin. + + """ + label = "Select invalid" + on = "failed" # This action is only available on a failed plug-in + icon = "search" # Icon from Awesome Icon + + def process(self, context, plugin): + + errored_instances = get_errored_instances_from_context(context) + + # Apply pyblish.logic to get the instances for the plug-in + instances = pyblish.api.instances_by_plugin(errored_instances, plugin) + + # Get the invalid nodes for the plug-ins + self.log.info("Finding invalid nodes..") + invalid = list() + for instance in instances: + invalid_nodes = plugin.get_invalid(instance) + if invalid_nodes: + if isinstance(invalid_nodes, (list, tuple)): + invalid.extend(invalid_nodes) + else: + self.log.warning("Plug-in returned to be invalid, " + "but has no selectable nodes.") + + hou.clearAllSelected() + if invalid: + self.log.info("Selecting invalid nodes: {}".format( + ", ".join(node.path() for node in invalid) + )) + for node in invalid: + node.setSelected(True) + node.setCurrent(True) + else: + self.log.info("No invalid nodes found.") diff --git a/openpype/hosts/houdini/plugins/publish/help/validate_vdb_output_node.xml b/openpype/hosts/houdini/plugins/publish/help/validate_vdb_output_node.xml index 0f92560bf7..eb83bfffe3 100644 --- a/openpype/hosts/houdini/plugins/publish/help/validate_vdb_output_node.xml +++ b/openpype/hosts/houdini/plugins/publish/help/validate_vdb_output_node.xml @@ -1,21 +1,28 @@ -Scene setting +Invalid VDB -## Invalid input node +## Invalid VDB output + +All primitives of the output geometry must be VDBs, no other primitive +types are allowed. That means that regardless of the amount of VDBs in the +geometry it will have an equal amount of VDBs, points, primitives and +vertices since each VDB primitive is one point, one vertex and one VDB. + +This validation only checks the geometry on the first frame of the export +frame range. + -VDB input must have the same number of VDBs, points, primitives and vertices as output. -### __Detailed Info__ (optional) +### Detailed Info + +ROP node `{rop_path}` is set to export SOP path `{sop_path}`. + +{message} -A VDB is an inherited type of Prim, holds the following data: - - Primitives: 1 - - Points: 1 - - Vertices: 1 - - VDBs: 1 \ No newline at end of file diff --git a/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py b/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py index b2b5c63799..3fa75e5822 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py +++ b/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py @@ -1,7 +1,9 @@ # -*- coding: utf-8 -*- import pyblish.api import hou + from openpype.pipeline import PublishXmlValidationError +from openpype.hosts.houdini.api.action import SelectInvalidAction def group_consecutive_numbers(nums): @@ -40,8 +42,13 @@ def group_consecutive_numbers(nums): class ValidateVDBOutputNode(pyblish.api.InstancePlugin): """Validate that the node connected to the output node is of type VDB. - Regardless of the amount of VDBs create the output will need to have an - equal amount of VDBs, points, primitives and vertices + All primitives of the output geometry must be VDBs, no other primitive + types are allowed. That means that regardless of the amount of VDBs in the + geometry it will have an equal amount of VDBs, points, primitives and + vertices since each VDB primitive is one point, one vertex and one VDB. + + This validation only checks the geometry on the first frame of the export + frame range for optimization purposes. A VDB is an inherited type of Prim, holds the following data: - Primitives: 1 @@ -55,64 +62,91 @@ class ValidateVDBOutputNode(pyblish.api.InstancePlugin): families = ["vdbcache"] hosts = ["houdini"] label = "Validate Output Node (VDB)" + actions = [SelectInvalidAction] def process(self, instance): - invalid = self.get_invalid(instance) - if invalid: + invalid_nodes, message = self.get_invalid_with_message(instance) + if invalid_nodes: raise PublishXmlValidationError( self, - "Node connected to the output node is not of type VDB." + "Node connected to the output node is not of type VDB.", + formatting_data={ + "message": message, + "rop_path": instance.data.get("instance_node"), + "sop_path": instance.data.get("output_node") + } ) @classmethod - def get_invalid(cls, instance): + def get_invalid_with_message(cls, instance): node = instance.data.get("output_node") if node is None: - cls.log.error( + instance_node = instance.data.get("instance_node") + error = ( "SOP path is not correctly set on " - "ROP node '%s'." % instance.data.get("instance_node") + "ROP node `%s`." % instance_node ) - return [instance] + return [instance_node, error] frame = instance.data.get("frameStart", 0) + node.cook(force=True, frame_range=(frame, frame)) geometry = node.geometryAtFrame(frame) if geometry is None: # No geometry data on this node, maybe the node hasn't cooked? - cls.log.error( + error = ( "SOP node has no geometry data. " "Is it cooked? %s" % node.path() ) - return [node] + return [node, error] - prims = geometry.prims() - nr_of_prims = len(prims) - - # All primitives must be hou.VDB - invalid_prims = [] - for prim in prims: - if not isinstance(prim, hou.VDB): - invalid_prims.append(prim) - if invalid_prims: - # Log prim numbers as consecutive ranges so logging isn't very - # slow for large number of primitives - cls.log.error( - "Found non-VDB primitives for '{}', " - "primitive indices: {}".format( - node.path(), - ", ".join(group_consecutive_numbers( - prim.number() for prim in invalid_prims - )) - ) + num_prims = geometry.intrinsicValue("primitivecount") + num_points = geometry.intrinsicValue("pointcount") + if num_prims == 0 and num_points == 0: + # Since we are only checking the first frame it doesn't mean there + # won't be VDB prims in a few frames. As such we'll assume for now + # the user knows what he or she is doing + cls.log.warning( + "SOP node `{}` has no primitives on start frame {}. " + "Validation is skipped and it is assumed elsewhere in the " + "frame range VDB prims and only VDB prims will exist." + "".format(node.path(), int(frame)) ) - return [instance] + return [None, None] - nr_of_points = len(geometry.points()) - if nr_of_points != nr_of_prims: - cls.log.error("The number of primitives and points do not match") - return [instance] + num_vdb_prims = geometry.countPrimType(hou.primType.VDB) + cls.log.debug("Detected {} VDB primitives".format(num_vdb_prims)) + if num_prims != num_vdb_prims: + # There's at least one primitive that is not a VDB. + # Search them and report them to the artist. + prims = geometry.prims() + invalid_prims = [prim for prim in prims + if not isinstance(prim, hou.VDB)] + if invalid_prims: + # Log prim numbers as consecutive ranges so logging isn't very + # slow for large number of primitives + error = ( + "Found non-VDB primitives for `{}`. " + "Primitive indices {} are not VDB primitives.".format( + node.path(), + ", ".join(group_consecutive_numbers( + prim.number() for prim in invalid_prims + )) + ) + ) + return [node, error] - for prim in prims: - if prim.numVertices() != 1: - cls.log.error("Found primitive with more than 1 vertex!") - return [instance] + if num_points != num_vdb_prims: + # We have points unrelated to the VDB primitives. + error = ( + "The number of primitives and points do not match in '{}'. " + "This likely means you have unconnected points, which we do " + "not allow in the VDB output.".format(node.path())) + return [node, error] + + return [None, None] + + @classmethod + def get_invalid(cls, instance): + nodes, _ = cls.get_invalid_with_message(instance) + return nodes From bb24b823649c3cf124fafb9c465a9fd5709d193a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 20 Apr 2023 00:05:07 +0200 Subject: [PATCH 118/360] Fix type bug --- .../houdini/plugins/publish/validate_vdb_output_node.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py b/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py index 3fa75e5822..def9595e9a 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py +++ b/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py @@ -67,13 +67,18 @@ class ValidateVDBOutputNode(pyblish.api.InstancePlugin): def process(self, instance): invalid_nodes, message = self.get_invalid_with_message(instance) if invalid_nodes: + + # instance_node is str, but output_node is hou.Node so we convert + output = instance.data.get("output_node") + output_path = output.path() if output else None + raise PublishXmlValidationError( self, "Node connected to the output node is not of type VDB.", formatting_data={ "message": message, "rop_path": instance.data.get("instance_node"), - "sop_path": instance.data.get("output_node") + "sop_path": output_path } ) From 9484bd4a51c465957b49c83c915b0995f1a4de98 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 20 Apr 2023 00:05:47 +0200 Subject: [PATCH 119/360] Force geometry update, otherwise manual update mode will fail to get the geometry correctly --- .../publish/validate_vdb_output_node.py | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py b/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py index def9595e9a..43da4b0528 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py +++ b/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +import contextlib + import pyblish.api import hou @@ -39,6 +41,23 @@ def group_consecutive_numbers(nums): yield _result(start, end) +@contextlib.contextmanager +def update_mode_context(mode): + original = hou.updateModeSetting() + try: + hou.setUpdateMode(mode) + yield + finally: + hou.setUpdateMode(original) + + +def get_geometry_at_frame(sop_node, frame, force=True): + """Return geometry at frame but force a cooked value.""" + with update_mode_context(hou.updateMode.AutoUpdate): + sop_node.cook(force=force, frame_range=(frame, frame)) + return sop_node.geometryAtFrame(frame) + + class ValidateVDBOutputNode(pyblish.api.InstancePlugin): """Validate that the node connected to the output node is of type VDB. @@ -95,8 +114,7 @@ class ValidateVDBOutputNode(pyblish.api.InstancePlugin): return [instance_node, error] frame = instance.data.get("frameStart", 0) - node.cook(force=True, frame_range=(frame, frame)) - geometry = node.geometryAtFrame(frame) + geometry = get_geometry_at_frame(node, frame) if geometry is None: # No geometry data on this node, maybe the node hasn't cooked? error = ( From cbd88a616c0420448c3cb3b9028d6e15482a314c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 20 Apr 2023 00:06:10 +0200 Subject: [PATCH 120/360] Tweak formatting, fix type bug for instance node --- .../houdini/plugins/publish/validate_vdb_output_node.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py b/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py index 43da4b0528..bd1fb0b887 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py +++ b/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py @@ -109,17 +109,17 @@ class ValidateVDBOutputNode(pyblish.api.InstancePlugin): instance_node = instance.data.get("instance_node") error = ( "SOP path is not correctly set on " - "ROP node `%s`." % instance_node + "ROP node `{}`.".format(instance_node) ) - return [instance_node, error] + return [hou.node(instance_node), error] frame = instance.data.get("frameStart", 0) geometry = get_geometry_at_frame(node, frame) if geometry is None: # No geometry data on this node, maybe the node hasn't cooked? error = ( - "SOP node has no geometry data. " - "Is it cooked? %s" % node.path() + "SOP node `{}` has no geometry data. " + "Was it unable to cook?".format(node.path()) ) return [node, error] From 175db5407403dcb8e0b3a3f7a49b39463b2ceb56 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 20 Apr 2023 00:09:50 +0200 Subject: [PATCH 121/360] Tweak logged message for non-UI report --- .../hosts/houdini/plugins/publish/validate_vdb_output_node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py b/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py index bd1fb0b887..674782179c 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py +++ b/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py @@ -93,7 +93,7 @@ class ValidateVDBOutputNode(pyblish.api.InstancePlugin): raise PublishXmlValidationError( self, - "Node connected to the output node is not of type VDB.", + "Invalid VDB content: {}".format(message), formatting_data={ "message": message, "rop_path": instance.data.get("instance_node"), From 16b169205ef8816099d1d94ff263069298d406cc Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 20 Apr 2023 01:29:51 +0200 Subject: [PATCH 122/360] Allow camera path to not be set correctly in review instance until validation --- .../plugins/publish/collect_review_data.py | 10 +++--- .../plugins/publish/validate_scene_review.py | 33 ++++++++++++++----- 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_review_data.py b/openpype/hosts/houdini/plugins/publish/collect_review_data.py index e321dcb2fa..3ab93dc491 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_review_data.py +++ b/openpype/hosts/houdini/plugins/publish/collect_review_data.py @@ -18,6 +18,9 @@ class CollectHoudiniReviewData(pyblish.api.InstancePlugin): instance.data["handleStart"] = 0 instance.data["handleEnd"] = 0 + # Enable ftrack functionality + instance.data.setdefault("families", []).append('ftrack') + # Get the camera from the rop node to collect the focal length ropnode_path = instance.data["instance_node"] ropnode = hou.node(ropnode_path) @@ -25,8 +28,9 @@ class CollectHoudiniReviewData(pyblish.api.InstancePlugin): camera_path = ropnode.parm("camera").eval() camera_node = hou.node(camera_path) if not camera_node: - raise RuntimeError("No valid camera node found on review node: " - "{}".format(camera_path)) + self.log.warning("No valid camera node found on review node: " + "{}".format(camera_path)) + return # Collect focal length. focal_length_parm = camera_node.parm("focal") @@ -48,5 +52,3 @@ class CollectHoudiniReviewData(pyblish.api.InstancePlugin): # Store focal length in `burninDataMembers` burnin_members = instance.data.setdefault("burninDataMembers", {}) burnin_members["focalLength"] = focal_length - - instance.data.setdefault("families", []).append('ftrack') diff --git a/openpype/hosts/houdini/plugins/publish/validate_scene_review.py b/openpype/hosts/houdini/plugins/publish/validate_scene_review.py index ade01d4b90..58d8a37240 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_scene_review.py +++ b/openpype/hosts/houdini/plugins/publish/validate_scene_review.py @@ -16,13 +16,17 @@ class ValidateSceneReview(pyblish.api.InstancePlugin): label = "Scene Setting for review" def process(self, instance): - invalid = self.get_invalid_scene_path(instance) report = [] + instance_node = hou.node(instance.data.get("instance_node")) + + invalid = self.get_invalid_scene_path(instance_node) if invalid: - report.append( - "Scene path does not exist: '%s'" % invalid[0], - ) + report.append(invalid) + + invalid = self.get_invalid_camera_path(instance_node) + if invalid: + report.append(invalid) invalid = self.get_invalid_resolution(instance) if invalid: @@ -33,13 +37,24 @@ class ValidateSceneReview(pyblish.api.InstancePlugin): "\n\n".join(report), title=self.label) - def get_invalid_scene_path(self, instance): - - node = hou.node(instance.data.get("instance_node")) - scene_path_parm = node.parm("scenepath") + def get_invalid_scene_path(self, rop_node): + scene_path_parm = rop_node.parm("scenepath") scene_path_node = scene_path_parm.evalAsNode() if not scene_path_node: - return [scene_path_parm.evalAsString()] + path = scene_path_parm.evalAsString() + return "Scene path does not exist: '{}'".format(path) + + def get_invalid_camera_path(self, rop_node): + camera_path_parm = rop_node.parm("camera") + camera_node = camera_path_parm.evalAsNode() + path = camera_path_parm.evalAsString() + if not camera_node: + return "Camera path does not exist: '{}'".format(path) + type_name = camera_node.type().name() + if type_name != "cam": + return "Camera path is not a camera: '{}' (type: {})".format( + path, type_name + ) def get_invalid_resolution(self, instance): node = hou.node(instance.data.get("instance_node")) From 0424f66164717b5127f89612f0d83b7865bece63 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 20 Apr 2023 01:34:01 +0200 Subject: [PATCH 123/360] Re-use instance node --- .../houdini/plugins/publish/validate_scene_review.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_scene_review.py b/openpype/hosts/houdini/plugins/publish/validate_scene_review.py index 58d8a37240..a44b7e1597 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_scene_review.py +++ b/openpype/hosts/houdini/plugins/publish/validate_scene_review.py @@ -28,7 +28,7 @@ class ValidateSceneReview(pyblish.api.InstancePlugin): if invalid: report.append(invalid) - invalid = self.get_invalid_resolution(instance) + invalid = self.get_invalid_resolution(instance_node) if invalid: report.extend(invalid) @@ -56,18 +56,17 @@ class ValidateSceneReview(pyblish.api.InstancePlugin): path, type_name ) - def get_invalid_resolution(self, instance): - node = hou.node(instance.data.get("instance_node")) + def get_invalid_resolution(self, rop_node): # The resolution setting is only used when Override Camera Resolution # is enabled. So we skip validation if it is disabled. - override = node.parm("tres").eval() + override = rop_node.parm("tres").eval() if not override: return invalid = [] - res_width = node.parm("res1").eval() - res_height = node.parm("res2").eval() + res_width = rop_node.parm("res1").eval() + res_height = rop_node.parm("res2").eval() if res_width == 0: invalid.append("Override Resolution width is set to zero.") if res_height == 0: From ef192d3edd1da53736ed54f176e662923c718e7b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 20 Apr 2023 12:16:40 +0200 Subject: [PATCH 124/360] Add `get_network_categories` to `CreateUSD` --- openpype/hosts/houdini/plugins/create/create_usd.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/create/create_usd.py b/openpype/hosts/houdini/plugins/create/create_usd.py index 51ed8237c5..e05d254863 100644 --- a/openpype/hosts/houdini/plugins/create/create_usd.py +++ b/openpype/hosts/houdini/plugins/create/create_usd.py @@ -3,6 +3,8 @@ from openpype.hosts.houdini.api import plugin from openpype.pipeline import CreatedInstance +import hou + class CreateUSD(plugin.HoudiniCreator): """Universal Scene Description""" @@ -13,7 +15,6 @@ class CreateUSD(plugin.HoudiniCreator): enabled = False def create(self, subset_name, instance_data, pre_create_data): - import hou # noqa instance_data.pop("active", None) instance_data.update({"node_type": "usd"}) @@ -43,3 +44,9 @@ class CreateUSD(plugin.HoudiniCreator): "id", ] self.lock_parameters(instance_node, to_lock) + + def get_network_categories(self): + return [ + hou.ropNodeTypeCategory(), + hou.lopNodeTypeCategory() + ] From 96b1b3e19d6a3e7dd7387b4477224c208eeaba90 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 20 Apr 2023 12:28:44 +0200 Subject: [PATCH 125/360] Implement `get_network_categories` on Houdini base creator plugin --- .../hosts/houdini/api/creator_node_shelves.py | 13 ++++++++----- openpype/hosts/houdini/api/plugin.py | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/houdini/api/creator_node_shelves.py b/openpype/hosts/houdini/api/creator_node_shelves.py index cd14090104..8a15d902b5 100644 --- a/openpype/hosts/houdini/api/creator_node_shelves.py +++ b/openpype/hosts/houdini/api/creator_node_shelves.py @@ -172,16 +172,19 @@ def install(): log.debug("Writing OpenPype Creator nodes to shelf: {}".format(filepath)) tools = [] - default_network_categories = [hou.ropNodeTypeCategory()] with shelves_change_block(): for identifier, creator in create_context.manual_creators.items(): # Allow the creator plug-in itself to override the categories # for where they are shown with `Creator.get_network_categories()` - if hasattr(creator, "get_network_categories"): - network_categories = creator.get_network_categories() - else: - network_categories = default_network_categories + if not hasattr(creator, "get_network_categories"): + log.debug("Creator {} has no `get_network_categories` method " + "and will not be added to TAB search.") + continue + + network_categories = creator.get_network_categories() + if not network_categories: + continue key = "openpype_create.{}".format(identifier) log.debug(f"Registering {key}") diff --git a/openpype/hosts/houdini/api/plugin.py b/openpype/hosts/houdini/api/plugin.py index 340a7f0770..1e7eaa7e22 100644 --- a/openpype/hosts/houdini/api/plugin.py +++ b/openpype/hosts/houdini/api/plugin.py @@ -276,3 +276,19 @@ class HoudiniCreator(NewCreator, HoudiniCreatorBase): color = hou.Color((0.616, 0.871, 0.769)) node.setUserData('nodeshape', shape) node.setColor(color) + + def get_network_categories(self): + """Return in which network view type this creator should show. + + The node type categories returned here will be used to define where + the creator will show up in the TAB search for nodes in Houdini's + Network View. + + This can be overridden in inherited classes to define where that + particular Creator should be visible in the TAB search. + + Returns: + list: List of houdini node type categories + + """ + return [hou.ropNodeTypeCategory()] From 3cbeda17a8cfefb31fdf2b35314b53779334867c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 20 Apr 2023 12:29:08 +0200 Subject: [PATCH 126/360] Support auto `null` node in LOPs --- openpype/hosts/houdini/api/creator_node_shelves.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/api/creator_node_shelves.py b/openpype/hosts/houdini/api/creator_node_shelves.py index 8a15d902b5..96e843b3a9 100644 --- a/openpype/hosts/houdini/api/creator_node_shelves.py +++ b/openpype/hosts/houdini/api/creator_node_shelves.py @@ -20,6 +20,7 @@ from openpype.resources import get_openpype_icon_filepath import hou import stateutils import soptoolutils +import loptoolutils import cop2toolutils @@ -88,7 +89,8 @@ def create_interactive(creator_identifier, **kwargs): tool_fn = { hou.sopNodeTypeCategory(): soptoolutils.genericTool, - hou.cop2NodeTypeCategory(): cop2toolutils.genericTool + hou.cop2NodeTypeCategory(): cop2toolutils.genericTool, + hou.lopNodeTypeCategory(): loptoolutils.genericTool }.get(pwd.childTypeCategory()) if tool_fn is not None: From 0941469c248c5d0503c8c40fadb0b1a280b55d94 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 20 Apr 2023 12:31:37 +0200 Subject: [PATCH 127/360] Move variable to module level --- openpype/hosts/houdini/api/creator_node_shelves.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/houdini/api/creator_node_shelves.py b/openpype/hosts/houdini/api/creator_node_shelves.py index 96e843b3a9..1cc28add86 100644 --- a/openpype/hosts/houdini/api/creator_node_shelves.py +++ b/openpype/hosts/houdini/api/creator_node_shelves.py @@ -26,6 +26,13 @@ import cop2toolutils log = logging.getLogger(__name__) +CATEGORY_GENERIC_TOOL = { + hou.sopNodeTypeCategory(): soptoolutils.genericTool, + hou.cop2NodeTypeCategory(): cop2toolutils.genericTool, + hou.lopNodeTypeCategory(): loptoolutils.genericTool +} + + CREATE_SCRIPT = """ from openpype.hosts.houdini.api.creator_node_shelves import create_interactive create_interactive("{identifier}", **kwargs) @@ -87,12 +94,7 @@ def create_interactive(creator_identifier, **kwargs): host_name=context.host_name ) - tool_fn = { - hou.sopNodeTypeCategory(): soptoolutils.genericTool, - hou.cop2NodeTypeCategory(): cop2toolutils.genericTool, - hou.lopNodeTypeCategory(): loptoolutils.genericTool - }.get(pwd.childTypeCategory()) - + tool_fn = CATEGORY_GENERIC_TOOL.get(pwd.childTypeCategory()) if tool_fn is not None: out_null = tool_fn(kwargs, "null") out_null.setName("OUT_{}".format(subset_name), unique_name=True) From 9012b9f18f45562c03ecbf7c9d1ac807a0019f93 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 20 Apr 2023 12:34:14 +0200 Subject: [PATCH 128/360] Add todo for later --- openpype/hosts/houdini/api/creator_node_shelves.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/hosts/houdini/api/creator_node_shelves.py b/openpype/hosts/houdini/api/creator_node_shelves.py index 1cc28add86..7c6122cffe 100644 --- a/openpype/hosts/houdini/api/creator_node_shelves.py +++ b/openpype/hosts/houdini/api/creator_node_shelves.py @@ -80,6 +80,10 @@ def create_interactive(creator_identifier, **kwargs): raise RuntimeError("Invalid creator identifier: " "{}".format(creator_identifier)) + # TODO: Once more elaborate unique create behavior should exist per Creator + # instead of per network editor area then we should move this from here + # to a method on the Creators for which this could be the default + # implementation. pane = stateutils.activePane(kwargs) if isinstance(pane, hou.NetworkEditor): pwd = pane.pwd() From 95c802047cff3dc211c7f0ad037497befbff0c14 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 20 Apr 2023 16:40:49 +0200 Subject: [PATCH 129/360] Don't make ExtractOpenGL optional --- .../hosts/houdini/plugins/publish/extract_opengl.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/extract_opengl.py b/openpype/hosts/houdini/plugins/publish/extract_opengl.py index c26d0813a6..6c36dec5f5 100644 --- a/openpype/hosts/houdini/plugins/publish/extract_opengl.py +++ b/openpype/hosts/houdini/plugins/publish/extract_opengl.py @@ -2,27 +2,20 @@ import os import pyblish.api -from openpype.pipeline import ( - publish, - OptionalPyblishPluginMixin -) +from openpype.pipeline import publish from openpype.hosts.houdini.api.lib import render_rop import hou -class ExtractOpenGL(publish.Extractor, - OptionalPyblishPluginMixin): +class ExtractOpenGL(publish.Extractor): order = pyblish.api.ExtractorOrder - 0.01 label = "Extract OpenGL" families = ["review"] hosts = ["houdini"] - optional = True def process(self, instance): - if not self.is_active(instance.data): - return ropnode = hou.node(instance.data.get("instance_node")) output = ropnode.evalParm("picture") From 5e012d6be254dfa68721d5f08a981cafecbc8842 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 21 Apr 2023 17:29:47 +0800 Subject: [PATCH 130/360] add validator for resolution setting --- openpype/hosts/max/api/pipeline.py | 3 - .../publish/validate_resolution_setting.py | 59 +++++++++++++++++++ 2 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 openpype/hosts/max/plugins/publish/validate_resolution_setting.py diff --git a/openpype/hosts/max/api/pipeline.py b/openpype/hosts/max/api/pipeline.py index ac841d395f..50fe30b299 100644 --- a/openpype/hosts/max/api/pipeline.py +++ b/openpype/hosts/max/api/pipeline.py @@ -56,9 +56,6 @@ class MaxHost(HostBase, IWorkfileHost, ILoadHost, INewPublisher): rt.callbacks.addScript(rt.Name('systemPostNew'), context_setting) - rt.callbacks.addScript(rt.Name('filePostOpen'), - context_setting) - def has_unsaved_changes(self): # TODO: how to get it from 3dsmax? return True diff --git a/openpype/hosts/max/plugins/publish/validate_resolution_setting.py b/openpype/hosts/max/plugins/publish/validate_resolution_setting.py new file mode 100644 index 0000000000..43a3a3a278 --- /dev/null +++ b/openpype/hosts/max/plugins/publish/validate_resolution_setting.py @@ -0,0 +1,59 @@ +import pyblish.api +from openpype.pipeline import ( + PublishValidationError, + OptionalPyblishPluginMixin +) +from pymxs import runtime as rt +from openpype.hosts.max.api.lib import reset_scene_resolution + +from openpype.pipeline.context_tools import ( + get_current_project_asset, + get_current_project +) + + +class ValidateResolutionSetting(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): + """Validate the resolution setting aligned with DB""" + + order = pyblish.api.ValidatorOrder- 0.01 + families = ["maxrender"] + hosts = ["max"] + label = "Validate Resolution Setting" + optional = True + + def process(self, instance): + if not self.is_active(instance.data): + return + width, height = self.get_db_resolution(instance) + current_width = rt.renderwidth + current_height = rt.renderHeight + if current_width != width and current_height != height: + raise PublishValidationError("Resolution Setting" + " not aligned with DB") + if current_width != width: + raise PublishValidationError("Width in Resolution Setting " + "not aligned with DB") + + if current_height != height: + raise PublishValidationError("Height in Resolution Setting " + "not aligned with DB") + + + def get_db_resolution(self, instance): + data = ["data.resolutionWidth", "data.resolutionHeight"] + project_resolution = get_current_project(fields=data) + project_resolution_data = project_resolution["data"] + asset_resolution = get_current_project_asset(fields=data) + asset_resolution_data = asset_resolution["data"] + # Set project resolution + project_width = int(project_resolution_data.get("resolutionWidth", 1920)) + project_height = int(project_resolution_data.get("resolutionHeight", 1080)) + width = int(asset_resolution_data.get("resolutionWidth", project_width)) + height = int(asset_resolution_data.get("resolutionHeight", project_height)) + + return width, height + + @classmethod + def repair(cls, instance): + reset_scene_resolution() From dac51f41ec5d97384698faf0f19e4ba398d96be6 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 21 Apr 2023 17:32:11 +0800 Subject: [PATCH 131/360] hound fix --- .../publish/validate_resolution_setting.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_resolution_setting.py b/openpype/hosts/max/plugins/publish/validate_resolution_setting.py index 43a3a3a278..ce1f8a975d 100644 --- a/openpype/hosts/max/plugins/publish/validate_resolution_setting.py +++ b/openpype/hosts/max/plugins/publish/validate_resolution_setting.py @@ -16,7 +16,7 @@ class ValidateResolutionSetting(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validate the resolution setting aligned with DB""" - order = pyblish.api.ValidatorOrder- 0.01 + order = pyblish.api.ValidatorOrder - 0.01 families = ["maxrender"] hosts = ["max"] label = "Validate Resolution Setting" @@ -39,7 +39,6 @@ class ValidateResolutionSetting(pyblish.api.InstancePlugin, raise PublishValidationError("Height in Resolution Setting " "not aligned with DB") - def get_db_resolution(self, instance): data = ["data.resolutionWidth", "data.resolutionHeight"] project_resolution = get_current_project(fields=data) @@ -47,10 +46,17 @@ class ValidateResolutionSetting(pyblish.api.InstancePlugin, asset_resolution = get_current_project_asset(fields=data) asset_resolution_data = asset_resolution["data"] # Set project resolution - project_width = int(project_resolution_data.get("resolutionWidth", 1920)) - project_height = int(project_resolution_data.get("resolutionHeight", 1080)) - width = int(asset_resolution_data.get("resolutionWidth", project_width)) - height = int(asset_resolution_data.get("resolutionHeight", project_height)) + project_width = int( + project_resolution_data.get("resolutionWidth", 1920) + ) + project_height = int( + project_resolution_data.get("resolutionHeight", 1080)) + width = int( + asset_resolution_data.get("resolutionWidth", project_width) + ) + height = int( + asset_resolution_data.get("resolutionHeight", project_height) + ) return width, height From 90f12d15e050e0136eaec50a53d2049385e3fcf6 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 21 Apr 2023 17:33:08 +0800 Subject: [PATCH 132/360] hound fix --- .../max/plugins/publish/validate_resolution_setting.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_resolution_setting.py b/openpype/hosts/max/plugins/publish/validate_resolution_setting.py index ce1f8a975d..9424b24380 100644 --- a/openpype/hosts/max/plugins/publish/validate_resolution_setting.py +++ b/openpype/hosts/max/plugins/publish/validate_resolution_setting.py @@ -47,16 +47,13 @@ class ValidateResolutionSetting(pyblish.api.InstancePlugin, asset_resolution_data = asset_resolution["data"] # Set project resolution project_width = int( - project_resolution_data.get("resolutionWidth", 1920) - ) + project_resolution_data.get("resolutionWidth", 1920)) project_height = int( project_resolution_data.get("resolutionHeight", 1080)) width = int( - asset_resolution_data.get("resolutionWidth", project_width) - ) + asset_resolution_data.get("resolutionWidth", project_width)) height = int( - asset_resolution_data.get("resolutionHeight", project_height) - ) + asset_resolution_data.get("resolutionHeight", project_height)) return width, height From 36a5beaa7b6f2ac084f3cef546a2f9077e481d12 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 24 Apr 2023 12:05:10 +0200 Subject: [PATCH 133/360] :bug: few fixes --- openpype/hosts/unreal/api/pipeline.py | 2 +- openpype/hosts/unreal/lib.py | 38 +++++++++++++++++++++++---- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/unreal/api/pipeline.py b/openpype/hosts/unreal/api/pipeline.py index 0d8922d2e6..bb45fa8c01 100644 --- a/openpype/hosts/unreal/api/pipeline.py +++ b/openpype/hosts/unreal/api/pipeline.py @@ -439,7 +439,7 @@ def create_container(container: str, path: str) -> unreal.Object: ) """ - factory = unreal.AssetContainerFactory() + factory = unreal.AyonAssetContainerFactory() tools = unreal.AssetToolsHelpers().get_asset_tools() return tools.create_asset(container, path, None, factory) diff --git a/openpype/hosts/unreal/lib.py b/openpype/hosts/unreal/lib.py index aa5b09fda8..38976c3ef1 100644 --- a/openpype/hosts/unreal/lib.py +++ b/openpype/hosts/unreal/lib.py @@ -317,18 +317,46 @@ def get_path_to_uat(engine_path: Path) -> Path: if platform.system().lower() == "windows": return engine_path / "Engine/Build/BatchFiles/RunUAT.bat" - if platform.system().lower() == "linux" \ - or platform.system().lower() == "darwin": + if platform.system().lower() in ["linux", "darwin"]: return engine_path / "Engine/Build/BatchFiles/RunUAT.sh" def get_path_to_cmdlet_project(ue_version: str) -> Path: - cmd_project = Path(os.path.dirname( - os.path.abspath(os.getenv("OPENPYPE_ROOT")))) + cmd_project = Path( + os.path.abspath(os.getenv("OPENPYPE_ROOT"))) # For now, only tested on Windows (For Linux and Mac # it has to be implemented) - cmd_project /= f"hosts/unreal/integration/UE_{ue_version}" + cmd_project /= f"openpype/hosts/unreal/integration/UE_{ue_version}" + + # if the integration doesn't exist for current engine version + # try to find the closest to it. + if cmd_project.exists(): + return cmd_project / "CommandletProject/CommandletProject.uproject" + + major, minor = ue_version.split(".") + integration_paths = [p for p in cmd_project.parent.iterdir() + if p.is_dir()] + + compatible_versions = [cmd_project] + for i in integration_paths: + + # parse version from path + i_major, i_minor = re.search( + r"(?P\d+).(?P\d+)$", i.name).groups() + + # consider versions with different major so different that they + # are incompatible + if int(major) != int(i_major): + continue + + compatible_versions.append(i) + + sorted(set(compatible_versions)) + + + + return cmd_project / "CommandletProject/CommandletProject.uproject" From 17cb32beda9947770fd7a0bc17ec13973df5d0b1 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 24 Apr 2023 12:10:15 +0200 Subject: [PATCH 134/360] :bug: workaround for alembic --- .../unreal/plugins/load/load_geometrycache_abc.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py b/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py index 74101d6a53..8b1b9d8f9e 100644 --- a/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py +++ b/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py @@ -22,7 +22,7 @@ class PointCacheAlembicLoader(plugin.Loader): color = "orange" def get_task( - self, filename, asset_dir, asset_name, replace, frame_start, frame_end + self, filename, asset_dir, asset_name, replace, frame_start=None, frame_end=None ): task = unreal.AssetImportTask() options = unreal.AbcImportSettings() @@ -51,8 +51,10 @@ class PointCacheAlembicLoader(plugin.Loader): conversion_settings.set_editor_property( 'rotation', unreal.Vector(x=-90.0, y=0.0, z=180.0)) - sampling_settings.set_editor_property('frame_start', frame_start) - sampling_settings.set_editor_property('frame_end', frame_end) + if frame_start is not None: + sampling_settings.set_editor_property('frame_start', frame_start) + if frame_end is not None: + sampling_settings.set_editor_property('frame_end', frame_end) options.geometry_cache_settings = gc_settings options.conversion_settings = conversion_settings @@ -145,9 +147,9 @@ class PointCacheAlembicLoader(plugin.Loader): name = container["asset_name"] source_path = get_representation_path(representation) destination_path = container["namespace"] + representation["context"] - task = self.get_task(source_path, destination_path, name, True) - + task = self.get_task(source_path, destination_path, name, False) # do import fbx and replace existing data unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) From ea83a40f8b5e25e0528775f2a5c6689391ac278e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 24 Apr 2023 15:24:04 +0200 Subject: [PATCH 135/360] Attribute is already set in `parameters` above --- .../substancepainter/plugins/publish/validate_ouput_maps.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/substancepainter/plugins/publish/validate_ouput_maps.py b/openpype/hosts/substancepainter/plugins/publish/validate_ouput_maps.py index e3d4c733e1..1f4dbaba13 100644 --- a/openpype/hosts/substancepainter/plugins/publish/validate_ouput_maps.py +++ b/openpype/hosts/substancepainter/plugins/publish/validate_ouput_maps.py @@ -34,7 +34,6 @@ class ValidateOutputMaps(pyblish.api.InstancePlugin): parameters["sizeLog2"] = [1, 1] # output 2x2 images (smallest) parameters["paddingAlgorithm"] = "passthrough" # no dilation (faster) parameters["dithering"] = False # no dithering (faster) - config["exportParameters"][0]["parameters"]["sizeLog2"] = [1, 1] result = substance_painter.export.export_project_textures(config) if result.status != substance_painter.export.ExportStatus.Success: From 2ff7d7ee1d8e24412bb50be1c2da12886d104e0a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 24 Apr 2023 15:24:30 +0200 Subject: [PATCH 136/360] Cosmetics --- .../substancepainter/plugins/publish/validate_ouput_maps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/substancepainter/plugins/publish/validate_ouput_maps.py b/openpype/hosts/substancepainter/plugins/publish/validate_ouput_maps.py index 1f4dbaba13..b57cf4c5a2 100644 --- a/openpype/hosts/substancepainter/plugins/publish/validate_ouput_maps.py +++ b/openpype/hosts/substancepainter/plugins/publish/validate_ouput_maps.py @@ -31,7 +31,7 @@ class ValidateOutputMaps(pyblish.api.InstancePlugin): # generate the smallest size / fastest export as possible config = copy.deepcopy(config) parameters = config["exportParameters"][0]["parameters"] - parameters["sizeLog2"] = [1, 1] # output 2x2 images (smallest) + parameters["sizeLog2"] = [1, 1] # output 2x2 images (smallest) parameters["paddingAlgorithm"] = "passthrough" # no dilation (faster) parameters["dithering"] = False # no dithering (faster) From 042efaae33c495999ad5b0fdfedbff0feab77af3 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 24 Apr 2023 15:34:15 +0200 Subject: [PATCH 137/360] Implement output template extensions override --- .../plugins/create/create_textures.py | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/substancepainter/plugins/create/create_textures.py b/openpype/hosts/substancepainter/plugins/create/create_textures.py index 6070a06367..dece4b2cc1 100644 --- a/openpype/hosts/substancepainter/plugins/create/create_textures.py +++ b/openpype/hosts/substancepainter/plugins/create/create_textures.py @@ -91,7 +91,34 @@ class CreateTextures(Creator): EnumDef("exportFileFormat", items={ None: "Based on output template", - # TODO: implement extensions + # TODO: Get available extensions from substance API + "bmp": "bmp", + "ico": "ico", + "jpeg": "jpeg", + "jng": "jng", + "pbm": "pbm", + "pgm": "pgm", + "png": "png", + "ppm": "ppm", + "tga": "targa", + "tif": "tiff", + "wap": "wap", + "wbmp": "wbmp", + "xpm": "xpm", + "gif": "gif", + "hdr": "hdr", + "exr": "exr", + "j2k": "j2k", + "jp2": "jp2", + "pfm": "pfm", + "webp": "webp", + # TODO: Unsure why jxr format fails to export + # "jxr": "jpeg-xr", + # TODO: File formats that combine the exported textures + # like psd are not correctly supported due to + # publishing only a single file + # "psd": "psd", + # "sbsar": "sbsar", }, default=None, label="File type"), From a1b264de9b2b910f1c7a5b7aadd0b931103fcb5d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 24 Apr 2023 16:19:44 +0200 Subject: [PATCH 138/360] Fix houdini workfile icon --- openpype/hosts/houdini/plugins/create/create_workfile.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_workfile.py b/openpype/hosts/houdini/plugins/create/create_workfile.py index 0c6d840810..5f5aa306ee 100644 --- a/openpype/hosts/houdini/plugins/create/create_workfile.py +++ b/openpype/hosts/houdini/plugins/create/create_workfile.py @@ -14,7 +14,7 @@ class CreateWorkfile(plugin.HoudiniCreatorBase, AutoCreator): identifier = "io.openpype.creators.houdini.workfile" label = "Workfile" family = "workfile" - icon = "document" + icon = "file-o" default_variant = "Main" @@ -90,4 +90,4 @@ class CreateWorkfile(plugin.HoudiniCreatorBase, AutoCreator): for created_inst, _changes in update_list: if created_inst["creator_identifier"] == self.identifier: workfile_data = {"workfile": created_inst.data_to_store()} - imprint(op_ctx, workfile_data, update=True) + imprint(op_ctx, workfile_data, update=True) \ No newline at end of file From e2fc8564e6e2fe64b47d3d8561f0f288dec35b98 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 24 Apr 2023 16:23:41 +0200 Subject: [PATCH 139/360] Fix accidental newline at end of file removal --- openpype/hosts/houdini/plugins/create/create_workfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/create/create_workfile.py b/openpype/hosts/houdini/plugins/create/create_workfile.py index 5f5aa306ee..9884fca325 100644 --- a/openpype/hosts/houdini/plugins/create/create_workfile.py +++ b/openpype/hosts/houdini/plugins/create/create_workfile.py @@ -90,4 +90,4 @@ class CreateWorkfile(plugin.HoudiniCreatorBase, AutoCreator): for created_inst, _changes in update_list: if created_inst["creator_identifier"] == self.identifier: workfile_data = {"workfile": created_inst.data_to_store()} - imprint(op_ctx, workfile_data, update=True) \ No newline at end of file + imprint(op_ctx, workfile_data, update=True) From 3a096bcf8bf4ff60ead25495a63ec2bcf6054d18 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 24 Apr 2023 17:51:40 +0200 Subject: [PATCH 140/360] Use explicit font awesome 5 name --- openpype/hosts/houdini/plugins/create/create_workfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/create/create_workfile.py b/openpype/hosts/houdini/plugins/create/create_workfile.py index 9884fca325..1a8537adcd 100644 --- a/openpype/hosts/houdini/plugins/create/create_workfile.py +++ b/openpype/hosts/houdini/plugins/create/create_workfile.py @@ -14,7 +14,7 @@ class CreateWorkfile(plugin.HoudiniCreatorBase, AutoCreator): identifier = "io.openpype.creators.houdini.workfile" label = "Workfile" family = "workfile" - icon = "file-o" + icon = "fa5.file" default_variant = "Main" From 5d14869180d0c04c744edcf5f88abcca22cbb579 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 26 Apr 2023 18:35:14 +0800 Subject: [PATCH 141/360] validator and selected nodes use for containers --- .../hosts/max/plugins/create/create_model.py | 8 ++-- .../plugins/publish/validate_usd_plugin.py | 38 +++++++++++++++++++ 2 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 openpype/hosts/max/plugins/publish/validate_usd_plugin.py diff --git a/openpype/hosts/max/plugins/create/create_model.py b/openpype/hosts/max/plugins/create/create_model.py index a78a30e0c7..e7ae3af9db 100644 --- a/openpype/hosts/max/plugins/create/create_model.py +++ b/openpype/hosts/max/plugins/create/create_model.py @@ -12,7 +12,6 @@ class CreateModel(plugin.MaxCreator): def create(self, subset_name, instance_data, pre_create_data): from pymxs import runtime as rt - sel_obj = list(rt.selection) instance = super(CreateModel, self).create( subset_name, instance_data, @@ -20,7 +19,10 @@ class CreateModel(plugin.MaxCreator): container = rt.getNodeByName(instance.data.get("instance_node")) # TODO: Disable "Add to Containers?" Panel # parent the selected cameras into the container - for obj in sel_obj: - obj.parent = container + sel_obj = None + if self.selected_nodes: + sel_obj = list(self.selected_nodes) + for obj in sel_obj: + obj.parent = container # for additional work on the node: # instance_node = rt.getNodeByName(instance.get("instance_node")) diff --git a/openpype/hosts/max/plugins/publish/validate_usd_plugin.py b/openpype/hosts/max/plugins/publish/validate_usd_plugin.py new file mode 100644 index 0000000000..8a92263884 --- /dev/null +++ b/openpype/hosts/max/plugins/publish/validate_usd_plugin.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +import pyblish.api +from openpype.pipeline import PublishValidationError +from pymxs import runtime as rt + + +class ValidateUSDPlugin(pyblish.api.InstancePlugin): + """Validates if USD plugin is installed or loaded in Max + """ + + order = pyblish.api.ValidatorOrder - 0.01 + families = ["model"] + hosts = ["max"] + label = "USD Plugin" + + def process(self, instance): + #usdimport.dli + #usdexport.dle + plugin_mgr = rt.pluginManager + plugin_count = plugin_mgr.pluginDllCount + plugin_info = self.get_plugins(plugin_mgr, + plugin_count) + usd_import = "usdimport.dli" + if usd_import not in plugin_info: + raise PublishValidationError("USD Plugin {}" + " not found".format(usd_import)) + usd_export = "usdexport.dle" + if usd_export not in plugin_info: + raise PublishValidationError("USD Plugin {}" + " not found".format(usd_export)) + + def get_plugins(self, manager, count): + plugin_info_list = list() + for p in range(1, count + 1): + plugin_info = manager.pluginDllName(p) + plugin_info_list.append(plugin_info) + + return plugin_info_list From 12c9d10ba1faebae7c71bcb1f15fdd293d946e29 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 26 Apr 2023 18:36:27 +0800 Subject: [PATCH 142/360] hound fix --- openpype/hosts/max/plugins/publish/validate_usd_plugin.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_usd_plugin.py b/openpype/hosts/max/plugins/publish/validate_usd_plugin.py index 8a92263884..747147020a 100644 --- a/openpype/hosts/max/plugins/publish/validate_usd_plugin.py +++ b/openpype/hosts/max/plugins/publish/validate_usd_plugin.py @@ -14,8 +14,6 @@ class ValidateUSDPlugin(pyblish.api.InstancePlugin): label = "USD Plugin" def process(self, instance): - #usdimport.dli - #usdexport.dle plugin_mgr = rt.pluginManager plugin_count = plugin_mgr.pluginDllCount plugin_info = self.get_plugins(plugin_mgr, From fdbe5ac3a1b033bcf4ec7e28b916106914fda951 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 26 Apr 2023 18:45:39 +0800 Subject: [PATCH 143/360] adjustment --- openpype/hosts/max/plugins/publish/validate_model_contents.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/validate_model_contents.py b/openpype/hosts/max/plugins/publish/validate_model_contents.py index dd9c8de2cf..dd782674ff 100644 --- a/openpype/hosts/max/plugins/publish/validate_model_contents.py +++ b/openpype/hosts/max/plugins/publish/validate_model_contents.py @@ -32,7 +32,7 @@ class ValidateModelContent(pyblish.api.InstancePlugin): "{}".format(container)) con = rt.getNodeByName(container) - selection_list = list(con.Children) + selection_list = list(con.Children) or rt.getCurrentSelection() for sel in selection_list: if rt.classOf(sel) in rt.Camera.classes: invalid.append(sel) From 668fc9f10f65c4805c6577d51c7bab22478f5d7b Mon Sep 17 00:00:00 2001 From: Joseff Date: Wed, 26 Apr 2023 18:28:24 +0200 Subject: [PATCH 144/360] Preparation for the submission of the plugin to marketplace, fixed a bug with the cmdlet path not being valid. --- .../hosts/unreal/hooks/pre_workfile_preparation.py | 3 +++ openpype/hosts/unreal/integration/README.md | 10 ++++++++++ .../Ayon/Source/Ayon/Private/AyonAssetContainer.cpp | 1 - .../integration/UE_4.27/Ayon/Source/Ayon/Public/Ayon.h | 2 -- .../UE_4.27/Ayon/Source/Ayon/Public/AyonLib.h | 1 - .../UE_4.27/Ayon/Source/Ayon/Public/AyonPythonBridge.h | 1 - .../Ayon/Source/Ayon/Public/OpenPypePublishInstance.h | 1 - .../unreal/integration/UE_4.27/BuildPlugin_4-27.bat | 1 + .../integration/UE_4.27/BuildPlugin_4-27_Window.bat | 1 + .../Ayon/Source/Ayon/Public/OpenPypePublishInstance.h | 1 - .../unreal/integration/UE_5.0/BuildPlugin_5-0.bat | 1 + .../integration/UE_5.0/BuildPlugin_5-0_Window.bat | 1 + .../Source/Ayon/Private/OpenPypePublishInstance.cpp | 2 +- .../Ayon/Source/Ayon/Public/OpenPypePublishInstance.h | 1 - .../unreal/integration/UE_5.1/BuildPlugin_5-1.bat | 1 + .../integration/UE_5.1/BuildPlugin_5-1_Window.bat | 1 + openpype/hosts/unreal/lib.py | 5 ++--- 17 files changed, 22 insertions(+), 12 deletions(-) create mode 100644 openpype/hosts/unreal/integration/README.md create mode 100644 openpype/hosts/unreal/integration/UE_4.27/BuildPlugin_4-27.bat create mode 100644 openpype/hosts/unreal/integration/UE_4.27/BuildPlugin_4-27_Window.bat create mode 100644 openpype/hosts/unreal/integration/UE_5.0/BuildPlugin_5-0.bat create mode 100644 openpype/hosts/unreal/integration/UE_5.0/BuildPlugin_5-0_Window.bat create mode 100644 openpype/hosts/unreal/integration/UE_5.1/BuildPlugin_5-1.bat create mode 100644 openpype/hosts/unreal/integration/UE_5.1/BuildPlugin_5-1_Window.bat diff --git a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py index f01609d314..085f80209d 100644 --- a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py +++ b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py @@ -107,6 +107,9 @@ class UnrealPrelaunchHook(PreLaunchHook): f"project [ {unreal_project_name} ]" )) + import openpype.hosts.unreal.lib as ue_lib + path = ue_lib.get_path_to_cmdlet_project(engine_version) + q_thread = QtCore.QThread() ue_project_worker = UEProjectGenerationWorker() ue_project_worker.setup( diff --git a/openpype/hosts/unreal/integration/README.md b/openpype/hosts/unreal/integration/README.md new file mode 100644 index 0000000000..961eea83e6 --- /dev/null +++ b/openpype/hosts/unreal/integration/README.md @@ -0,0 +1,10 @@ +# Building the plugin + +In order to successfully build the plugin, make sure that the path to the UnrealBuildTool.exe is specified correctly. +After the UBT path specify for which platform it will be compiled. in the -Project parameter, specify the path to the +CommandletProject.uproject file. Next the build type has to be specified (DebugGame, Development, Package, etc.) and then the -TargetType (Editor, Runtime, etc.) + +`BuildPlugin_[Ver].bat` runs the building process in the background. If you want to show the progress inside the +command prompt, use the `BuildPlugin_[Ver]_Window.bat` file. + + diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonAssetContainer.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonAssetContainer.cpp index 316c4015af..e3989eb03c 100644 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonAssetContainer.cpp +++ b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonAssetContainer.cpp @@ -3,7 +3,6 @@ #include "AyonAssetContainer.h" #include "AssetRegistryModule.h" #include "Misc/PackageName.h" -#include "Engine.h" #include "Containers/UnrealString.h" UAyonAssetContainer::UAyonAssetContainer(const FObjectInitializer& ObjectInitializer) diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/Ayon.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/Ayon.h index 9535ff4b13..d11af70058 100644 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/Ayon.h +++ b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/Ayon.h @@ -2,8 +2,6 @@ #pragma once -#include "Engine.h" - class FAyonModule : public IModuleInterface { diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonLib.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonLib.h index ed657a735c..da83b448fb 100644 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonLib.h +++ b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonLib.h @@ -1,7 +1,6 @@ // Copyright 2023, Ayon, All rights reserved. #pragma once -#include "Engine.h" #include "AyonLib.generated.h" diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonPythonBridge.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonPythonBridge.h index 831ac022a5..3c429fd7d3 100644 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonPythonBridge.h +++ b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonPythonBridge.h @@ -1,6 +1,5 @@ // Copyright 2023, Ayon, All rights reserved. #pragma once -#include "Engine.h" #include "AyonPythonBridge.generated.h" UCLASS(Blueprintable) diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h index 2f3b6aa596..4a7a6a3a9f 100644 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h +++ b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h @@ -3,7 +3,6 @@ // and will be removed in next versions of Ayon. #pragma once -#include "Engine.h" #include "OpenPypePublishInstance.generated.h" diff --git a/openpype/hosts/unreal/integration/UE_4.27/BuildPlugin_4-27.bat b/openpype/hosts/unreal/integration/UE_4.27/BuildPlugin_4-27.bat new file mode 100644 index 0000000000..96cdb96f8a --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.27/BuildPlugin_4-27.bat @@ -0,0 +1 @@ +D:\UE4\UE_4.27\Engine\Build\BatchFiles\RunUAT.bat BuildPlugin -plugin="D:\OpenPype\openpype\hosts\unreal\integration\UE_4.27\Ayon\Ayon.uplugin" -Package="D:\BuiltPlugins\4.27" \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_4.27/BuildPlugin_4-27_Window.bat b/openpype/hosts/unreal/integration/UE_4.27/BuildPlugin_4-27_Window.bat new file mode 100644 index 0000000000..1343843a82 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.27/BuildPlugin_4-27_Window.bat @@ -0,0 +1 @@ +cmd /k "BuildPlugin_4-27.bat" \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h index 544cb6d915..9c0c4a69e5 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h +++ b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h @@ -3,7 +3,6 @@ // and will be removed in next versions of Ayon. #pragma once -#include "Engine.h" #include "OpenPypePublishInstance.generated.h" diff --git a/openpype/hosts/unreal/integration/UE_5.0/BuildPlugin_5-0.bat b/openpype/hosts/unreal/integration/UE_5.0/BuildPlugin_5-0.bat new file mode 100644 index 0000000000..473c248cbe --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/BuildPlugin_5-0.bat @@ -0,0 +1 @@ +"C:\Program Files\Epic Games\UE_5.0\Engine\Build\BatchFiles\RunUAT.bat" BuildPlugin -plugin="D:\OpenPype\openpype\hosts\unreal\integration\UE_5.0\Ayon\Ayon.uplugin" -Package="D:\BuiltPlugins\5.0" \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/BuildPlugin_5-0_Window.bat b/openpype/hosts/unreal/integration/UE_5.0/BuildPlugin_5-0_Window.bat new file mode 100644 index 0000000000..b96de6d6c9 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/BuildPlugin_5-0_Window.bat @@ -0,0 +1 @@ +cmd /k "BuildPlugin_5-0.bat" \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp index 7a65fd0c98..02a8ac800a 100644 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp @@ -58,7 +58,7 @@ void UOpenPypePublishInstance::OnAssetCreated(const FAssetData& InAssetData) if (!IsValid(Asset)) { UE_LOG(LogAssetData, Warning, TEXT("Asset \"%s\" is not valid! Skipping the addition."), - *InAssetData.ObjectPath.ToString()); + *InAssetData.GetSoftObjectPath().ToString()); return; } diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h index 544cb6d915..9c0c4a69e5 100644 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h @@ -3,7 +3,6 @@ // and will be removed in next versions of Ayon. #pragma once -#include "Engine.h" #include "OpenPypePublishInstance.generated.h" diff --git a/openpype/hosts/unreal/integration/UE_5.1/BuildPlugin_5-1.bat b/openpype/hosts/unreal/integration/UE_5.1/BuildPlugin_5-1.bat new file mode 100644 index 0000000000..3cc82d54af --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.1/BuildPlugin_5-1.bat @@ -0,0 +1 @@ +"D:\UE_5.1\Engine\Build\BatchFiles\RunUAT.bat" BuildPlugin -plugin="D:\OpenPype\openpype\hosts\unreal\integration\UE_5.1\Ayon\Ayon.uplugin" -Package="D:\BuiltPlugins\5.1" \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.1/BuildPlugin_5-1_Window.bat b/openpype/hosts/unreal/integration/UE_5.1/BuildPlugin_5-1_Window.bat new file mode 100644 index 0000000000..e10f2c7add --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.1/BuildPlugin_5-1_Window.bat @@ -0,0 +1 @@ +cmd /k "BuildPlugin_5-1.bat" \ No newline at end of file diff --git a/openpype/hosts/unreal/lib.py b/openpype/hosts/unreal/lib.py index aa5b09fda8..840f79f3c8 100644 --- a/openpype/hosts/unreal/lib.py +++ b/openpype/hosts/unreal/lib.py @@ -323,12 +323,11 @@ def get_path_to_uat(engine_path: Path) -> Path: def get_path_to_cmdlet_project(ue_version: str) -> Path: - cmd_project = Path(os.path.dirname( - os.path.abspath(os.getenv("OPENPYPE_ROOT")))) + cmd_project = Path(os.getenv("OPENPYPE_ROOT")) # For now, only tested on Windows (For Linux and Mac # it has to be implemented) - cmd_project /= f"hosts/unreal/integration/UE_{ue_version}" + cmd_project /= f"openpype/hosts/unreal/integration/UE_{ue_version}" return cmd_project / "CommandletProject/CommandletProject.uproject" From 557cbb72cefa955f75fa6c3f1df7fe38665e1c99 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 27 Apr 2023 11:26:57 +0200 Subject: [PATCH 145/360] :recycle: escape rootless path --- 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 f80bd40133..6ee100ddb4 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -275,7 +275,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): args = [ "--headless", 'publish', - rootless_metadata_path, + '"{}"'.format(rootless_metadata_path), "--targets", "deadline", "--targets", "farm" ] From 61c37ebb2263af58666b314186652636186f3896 Mon Sep 17 00:00:00 2001 From: Seyedmohammadreza Hashemizadeh Date: Tue, 25 Apr 2023 15:54:25 +0200 Subject: [PATCH 146/360] add display handle setting for maya load references --- openpype/hosts/maya/plugins/load/load_reference.py | 9 ++++++--- openpype/settings/defaults/project_settings/maya.json | 3 ++- .../projects_schema/schemas/schema_maya_load.json | 8 ++++++++ 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_reference.py b/openpype/hosts/maya/plugins/load/load_reference.py index 0dbdb03bb7..3309d7c207 100644 --- a/openpype/hosts/maya/plugins/load/load_reference.py +++ b/openpype/hosts/maya/plugins/load/load_reference.py @@ -162,9 +162,12 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): with parent_nodes(roots, parent=None): cmds.xform(group_name, zeroTransformPivots=True) - cmds.setAttr("{}.displayHandle".format(group_name), 1) - settings = get_project_settings(os.environ['AVALON_PROJECT']) + + display_handle = settings['maya']['load'].get('reference_loader', {}).get( + 'display_handle', True) + cmds.setAttr("{}.displayHandle".format(group_name), display_handle) + colors = settings['maya']['load']['colors'] c = colors.get(family) if c is not None: @@ -174,7 +177,7 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): (float(c[1]) / 255), (float(c[2]) / 255)) - cmds.setAttr("{}.displayHandle".format(group_name), 1) + cmds.setAttr("{}.displayHandle".format(group_name), display_handle) # get bounding box bbox = cmds.exactWorldBoundingBox(group_name) # get pivot position on world space diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 12223216cd..72b330ce7a 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -1460,7 +1460,8 @@ }, "reference_loader": { "namespace": "{asset_name}_{subset}_##_", - "group_name": "_GRP" + "group_name": "_GRP", + "display_handle": true } }, "workfile_build": { diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_load.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_load.json index c1895c4824..4b6b97ab4e 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_load.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_load.json @@ -111,6 +111,14 @@ { "type": "label", "label": "Here's a link to the doc where you can find explanations about customing the naming of referenced assets: https://openpype.io/docs/admin_hosts_maya#load-plugins" + }, + { + "type": "separator" + }, + { + "type": "boolean", + "key": "display_handle", + "label": "Display Handle On Load References" } ] } From 0d4fb1d8162f5647f53abc6d66419bd5f7cce5ba Mon Sep 17 00:00:00 2001 From: Seyedmohammadreza Hashemizadeh Date: Wed, 26 Apr 2023 10:06:00 +0200 Subject: [PATCH 147/360] linting clean up --- openpype/hosts/maya/plugins/load/load_reference.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_reference.py b/openpype/hosts/maya/plugins/load/load_reference.py index 3309d7c207..86c2a92a07 100644 --- a/openpype/hosts/maya/plugins/load/load_reference.py +++ b/openpype/hosts/maya/plugins/load/load_reference.py @@ -164,9 +164,10 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): settings = get_project_settings(os.environ['AVALON_PROJECT']) - display_handle = settings['maya']['load'].get('reference_loader', {}).get( - 'display_handle', True) - cmds.setAttr("{}.displayHandle".format(group_name), display_handle) + display_handle = settings['maya']['load'].get( + 'reference_loader', {}).get('display_handle', True) + cmds.setAttr( + "{}.displayHandle".format(group_name), display_handle) colors = settings['maya']['load']['colors'] c = colors.get(family) @@ -177,7 +178,8 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): (float(c[1]) / 255), (float(c[2]) / 255)) - cmds.setAttr("{}.displayHandle".format(group_name), display_handle) + cmds.setAttr( + "{}.displayHandle".format(group_name), display_handle) # get bounding box bbox = cmds.exactWorldBoundingBox(group_name) # get pivot position on world space From 37ea36b811d427f1c31563967789837c26b96cd6 Mon Sep 17 00:00:00 2001 From: Seyedmohammadreza Hashemizadeh Date: Wed, 26 Apr 2023 10:38:00 +0200 Subject: [PATCH 148/360] cosmetiques --- openpype/hosts/maya/plugins/load/load_reference.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_reference.py b/openpype/hosts/maya/plugins/load/load_reference.py index 86c2a92a07..7d717dcd44 100644 --- a/openpype/hosts/maya/plugins/load/load_reference.py +++ b/openpype/hosts/maya/plugins/load/load_reference.py @@ -165,9 +165,11 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): settings = get_project_settings(os.environ['AVALON_PROJECT']) display_handle = settings['maya']['load'].get( - 'reference_loader', {}).get('display_handle', True) + 'reference_loader', {} + ).get('display_handle', True) cmds.setAttr( - "{}.displayHandle".format(group_name), display_handle) + "{}.displayHandle".format(group_name), display_handle + ) colors = settings['maya']['load']['colors'] c = colors.get(family) @@ -179,7 +181,8 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): (float(c[2]) / 255)) cmds.setAttr( - "{}.displayHandle".format(group_name), display_handle) + "{}.displayHandle".format(group_name), display_handle + ) # get bounding box bbox = cmds.exactWorldBoundingBox(group_name) # get pivot position on world space From 289c0ffa060f4df41895c76055bfbac2930da1c4 Mon Sep 17 00:00:00 2001 From: Seyedmohammadreza Hashemizadeh Date: Thu, 6 Apr 2023 18:35:09 +0200 Subject: [PATCH 149/360] remove defautl cameras from renderable cameras --- openpype/hosts/maya/api/workfile_template_builder.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openpype/hosts/maya/api/workfile_template_builder.py b/openpype/hosts/maya/api/workfile_template_builder.py index d65e4c74d2..e8d5fc4bfd 100644 --- a/openpype/hosts/maya/api/workfile_template_builder.py +++ b/openpype/hosts/maya/api/workfile_template_builder.py @@ -45,6 +45,13 @@ class MayaTemplateBuilder(AbstractTemplateBuilder): cmds.sets(name=PLACEHOLDER_SET, empty=True) new_nodes = cmds.file(path, i=True, returnNewNodes=True) + # make default cameras non-renderable + default_cameras = [u'perspShape'] + for cam in default_cameras: + if not cmds.objExists("{}.renderable".format(cam)): + continue + cmds.setAttr("{}.renderable".format(cam), 0) + cmds.setAttr(PLACEHOLDER_SET + ".hiddenInOutliner", True) imported_sets = cmds.ls(new_nodes, set=True) From 6f57d567e45cbd3f3ca12c7b09c036f5b66d6123 Mon Sep 17 00:00:00 2001 From: Seyedmohammadreza Hashemizadeh Date: Fri, 7 Apr 2023 12:36:56 +0200 Subject: [PATCH 150/360] get default cameras from maya --- openpype/hosts/maya/api/workfile_template_builder.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/workfile_template_builder.py b/openpype/hosts/maya/api/workfile_template_builder.py index e8d5fc4bfd..d91fb1e83a 100644 --- a/openpype/hosts/maya/api/workfile_template_builder.py +++ b/openpype/hosts/maya/api/workfile_template_builder.py @@ -46,7 +46,8 @@ class MayaTemplateBuilder(AbstractTemplateBuilder): new_nodes = cmds.file(path, i=True, returnNewNodes=True) # make default cameras non-renderable - default_cameras = [u'perspShape'] + default_cameras = [cam for cam in cmds.ls(cameras=True) + if cmds.camera(cam, query=True, startupCamera=True)] for cam in default_cameras: if not cmds.objExists("{}.renderable".format(cam)): continue From 9cbcef4fd9d3883a13f62c0e24cb28463af499e4 Mon Sep 17 00:00:00 2001 From: Seyedmohammadreza Hashemizadeh Date: Fri, 7 Apr 2023 14:58:03 +0200 Subject: [PATCH 151/360] apply suggetion. use attribute query --- openpype/hosts/maya/api/workfile_template_builder.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/workfile_template_builder.py b/openpype/hosts/maya/api/workfile_template_builder.py index d91fb1e83a..81fc54fe6f 100644 --- a/openpype/hosts/maya/api/workfile_template_builder.py +++ b/openpype/hosts/maya/api/workfile_template_builder.py @@ -49,7 +49,10 @@ class MayaTemplateBuilder(AbstractTemplateBuilder): default_cameras = [cam for cam in cmds.ls(cameras=True) if cmds.camera(cam, query=True, startupCamera=True)] for cam in default_cameras: - if not cmds.objExists("{}.renderable".format(cam)): + if not cmds.attributeQuery("renderable", node=cam, exists=True): + self.log.debug( + "Camera {} has no attribute 'renderable'".format(cam) + ) continue cmds.setAttr("{}.renderable".format(cam), 0) From a1eff27bbfcb5e54e05061b644245c4eae9deaeb Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Fri, 28 Apr 2023 09:17:09 +0200 Subject: [PATCH 152/360] Added OptionalPyblishPluginMixin and is_active check --- .../plugins/publish/increment_current_file.py | 19 ++++++++++++++----- .../publish/validate_background_depth.py | 15 ++++++++++----- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/increment_current_file.py b/openpype/hosts/fusion/plugins/publish/increment_current_file.py index 42891446f7..4facd61893 100644 --- a/openpype/hosts/fusion/plugins/publish/increment_current_file.py +++ b/openpype/hosts/fusion/plugins/publish/increment_current_file.py @@ -1,7 +1,10 @@ import pyblish.api +from openpype.pipeline import OptionalPyblishPluginMixin -class FusionIncrementCurrentFile(pyblish.api.ContextPlugin): +class FusionIncrementCurrentFile( + pyblish.api.ContextPlugin, OptionalPyblishPluginMixin +): """Increment the current file. Saves the current file with an increased version number. @@ -15,15 +18,21 @@ class FusionIncrementCurrentFile(pyblish.api.ContextPlugin): optional = True def process(self, context): + if not self.is_active(context.data): + return from openpype.lib import version_up from openpype.pipeline.publish import get_errored_plugins_from_context errored_plugins = get_errored_plugins_from_context(context) - if any(plugin.__name__ == "FusionSubmitDeadline" - for plugin in errored_plugins): - raise RuntimeError("Skipping incrementing current file because " - "submission to render farm failed.") + if any( + plugin.__name__ == "FusionSubmitDeadline" + for plugin in errored_plugins + ): + raise RuntimeError( + "Skipping incrementing current file because " + "submission to render farm failed." + ) comp = context.data.get("currentComp") assert comp, "Must have comp" diff --git a/openpype/hosts/fusion/plugins/publish/validate_background_depth.py b/openpype/hosts/fusion/plugins/publish/validate_background_depth.py index db2c4f0dd9..384f6c2979 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_background_depth.py +++ b/openpype/hosts/fusion/plugins/publish/validate_background_depth.py @@ -1,12 +1,14 @@ import pyblish.api -from openpype.pipeline.publish import RepairAction +from openpype.pipeline import publish, OptionalPyblishPluginMixin from openpype.pipeline import PublishValidationError from openpype.hosts.fusion.api.action import SelectInvalidAction -class ValidateBackgroundDepth(pyblish.api.InstancePlugin): +class ValidateBackgroundDepth( + pyblish.api.InstancePlugin, OptionalPyblishPluginMixin +): """Validate if all Background tool are set to float32 bit""" order = pyblish.api.ValidatorOrder @@ -15,11 +17,10 @@ class ValidateBackgroundDepth(pyblish.api.InstancePlugin): families = ["render"] optional = True - actions = [SelectInvalidAction, RepairAction] + actions = [SelectInvalidAction, publish.RepairAction] @classmethod def get_invalid(cls, instance): - context = instance.context comp = context.data.get("currentComp") assert comp, "Must have Comp object" @@ -31,12 +32,16 @@ class ValidateBackgroundDepth(pyblish.api.InstancePlugin): return [i for i in backgrounds if i.GetInput("Depth") != 4.0] def process(self, instance): + if not self.is_active(instance.data): + return + invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( "Found {} Backgrounds tools which" " are not set to float32".format(len(invalid)), - title=self.label) + title=self.label, + ) @classmethod def repair(cls, instance): From 1cf5a9e6a726d68f995da56933edba7ebea00dea Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Fri, 28 Apr 2023 09:24:17 +0200 Subject: [PATCH 153/360] Cleaned up imports --- .../hosts/fusion/plugins/publish/increment_current_file.py | 1 + .../fusion/plugins/publish/validate_background_depth.py | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/increment_current_file.py b/openpype/hosts/fusion/plugins/publish/increment_current_file.py index 4facd61893..938a0ed698 100644 --- a/openpype/hosts/fusion/plugins/publish/increment_current_file.py +++ b/openpype/hosts/fusion/plugins/publish/increment_current_file.py @@ -1,4 +1,5 @@ import pyblish.api + from openpype.pipeline import OptionalPyblishPluginMixin diff --git a/openpype/hosts/fusion/plugins/publish/validate_background_depth.py b/openpype/hosts/fusion/plugins/publish/validate_background_depth.py index 384f6c2979..6908889eb4 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_background_depth.py +++ b/openpype/hosts/fusion/plugins/publish/validate_background_depth.py @@ -1,7 +1,10 @@ import pyblish.api -from openpype.pipeline import publish, OptionalPyblishPluginMixin -from openpype.pipeline import PublishValidationError +from openpype.pipeline import ( + publish, + OptionalPyblishPluginMixin, + PublishValidationError, +) from openpype.hosts.fusion.api.action import SelectInvalidAction From d5b719b8c87ca2cee643b460436852e64bc5c912 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Fri, 28 Apr 2023 11:00:51 +0200 Subject: [PATCH 154/360] Made name more clear when showing up under `Context` in publisher --- openpype/hosts/fusion/plugins/publish/increment_current_file.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/publish/increment_current_file.py b/openpype/hosts/fusion/plugins/publish/increment_current_file.py index 938a0ed698..ed0fd0fbc8 100644 --- a/openpype/hosts/fusion/plugins/publish/increment_current_file.py +++ b/openpype/hosts/fusion/plugins/publish/increment_current_file.py @@ -12,7 +12,7 @@ class FusionIncrementCurrentFile( """ - label = "Increment current file" + label = "Increment workfile version" order = pyblish.api.IntegratorOrder + 9.0 hosts = ["fusion"] families = ["workfile"] From 4f5a85aee0d48738cfae3fda8acdf453d3e2556f Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Fri, 28 Apr 2023 11:01:26 +0200 Subject: [PATCH 155/360] Removed familiy so it can version up even if workfile isn't published --- openpype/hosts/fusion/plugins/publish/increment_current_file.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/publish/increment_current_file.py b/openpype/hosts/fusion/plugins/publish/increment_current_file.py index ed0fd0fbc8..de6f697073 100644 --- a/openpype/hosts/fusion/plugins/publish/increment_current_file.py +++ b/openpype/hosts/fusion/plugins/publish/increment_current_file.py @@ -15,7 +15,6 @@ class FusionIncrementCurrentFile( label = "Increment workfile version" order = pyblish.api.IntegratorOrder + 9.0 hosts = ["fusion"] - families = ["workfile"] optional = True def process(self, context): From eb78b8359b31ded4a4f3339b7d983599df667aff Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 28 Apr 2023 17:47:09 +0800 Subject: [PATCH 156/360] add validators and change to take the frame range from the render setting --- openpype/hosts/max/api/lib.py | 8 ++- openpype/hosts/max/api/lib_renderproducts.py | 5 +- .../max/plugins/publish/collect_render.py | 4 +- .../plugins/publish/validate_frame_range.py | 61 +++++++++++++++++++ .../publish/validate_frame_range_type.py | 32 ++++++++++ 5 files changed, 103 insertions(+), 7 deletions(-) create mode 100644 openpype/hosts/max/plugins/publish/validate_frame_range.py create mode 100644 openpype/hosts/max/plugins/publish/validate_frame_range_type.py diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index ad9a450cad..7d629922fc 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -150,10 +150,10 @@ def set_framerange(start_frame, end_frame): Todo: Current type is hard-coded, there should be a custom setting for this. """ - rt.rendTimeType = 4 + rt.rendTimeType = 3 if start_frame is not None and end_frame is not None: - frame_range = "{0}-{1}".format(start_frame, end_frame) - rt.rendPickupFrames = frame_range + rt.rendStart = int(start_frame) + rt.rendEnd = int(end_frame) def get_multipass_setting(project_setting=None): @@ -243,6 +243,7 @@ def reset_frame_range(fps: bool = True): frame_end = frame_range["frameEnd"] + int(frame_range["handleEnd"]) frange_cmd = f"animationRange = interval {frame_start} {frame_end}" rt.execute(frange_cmd) + set_framerange(frame_start, frame_end) def set_context_setting(): @@ -259,6 +260,7 @@ def set_context_setting(): None """ reset_scene_resolution() + reset_frame_range() def get_max_version(): diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 350eb97661..8224d589ad 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -36,8 +36,9 @@ class RenderProducts(object): container) context = get_current_project_asset() - startFrame = context["data"].get("frameStart") - endFrame = context["data"].get("frameEnd") + 1 + # TODO: change the frame range follows the current render setting + startFrame = int(rt.rendStart) + endFrame = int(rt.rendEnd) + 1 img_fmt = self._project_settings["max"]["RenderSettings"]["image_format"] # noqa full_render_list = self.beauty_render_product(output_file, diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index b040467522..9d93a40021 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -59,8 +59,8 @@ class CollectRender(pyblish.api.InstancePlugin): "source": filepath, "expectedFiles": render_layer_files, "plugin": "3dsmax", - "frameStart": context.data['frameStart'], - "frameEnd": context.data['frameEnd'], + "frameStart": int(rt.rendStart), + "frameEnd": int(rt.rendEnd), "version": version_int, "farm": True } diff --git a/openpype/hosts/max/plugins/publish/validate_frame_range.py b/openpype/hosts/max/plugins/publish/validate_frame_range.py new file mode 100644 index 0000000000..2e1d7c9177 --- /dev/null +++ b/openpype/hosts/max/plugins/publish/validate_frame_range.py @@ -0,0 +1,61 @@ +import pyblish.api + +from pymxs import runtime as rt +from openpype.pipeline import ( + OptionalPyblishPluginMixin +) +from openpype.pipeline.publish import ( + RepairAction, + ValidateContentsOrder, + PublishValidationError +) + + +class ValidateFrameRange(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): + """Validates the frame ranges. + + This is an optional validator checking if the frame range on instance + matches the frame range specified for the asset. + + It also validates render frame ranges of render layers. + + Repair action will change everything to match the asset frame range. + + This can be turned off by the artist to allow custom ranges. + """ + + label = "Validate Frame Range" + order = ValidateContentsOrder + families = ["maxrender"] + hosts = ["max"] + optional = True + actions = [RepairAction] + + def process(self, instance): + if not self.is_active(instance.data): + self.log.info("Skipping validation...") + return + context = instance.context + + frame_start = int(context.data.get("frameStart")) + frame_end = int(context.data.get("frameEnd")) + + inst_frame_start = int(instance.data.get("frameStart")) + inst_frame_end = int(instance.data.get("frameEnd")) + + + if frame_start != inst_frame_start: + raise PublishValidationError( + "startFrame on instance does not match" + " with startFrame from the context data") + + if frame_end != inst_frame_end: + raise PublishValidationError( + "endFrame on instance does not match" + " with endFrame from the context data") + + @classmethod + def repair(cls, instance): + rt.rendStart = instance.context.data.get("frameStart") + rt.rendEnd = instance.context.data.get("frameEnd") diff --git a/openpype/hosts/max/plugins/publish/validate_frame_range_type.py b/openpype/hosts/max/plugins/publish/validate_frame_range_type.py new file mode 100644 index 0000000000..2403595b3b --- /dev/null +++ b/openpype/hosts/max/plugins/publish/validate_frame_range_type.py @@ -0,0 +1,32 @@ +import pyblish.api + +from pymxs import runtime as rt +from openpype.pipeline.publish import ( + RepairAction, + ValidateContentsOrder, + PublishValidationError +) + + +class ValidateFrameRangeType(pyblish.api.InstancePlugin): + """ + Validates whether the User + specified Frame Range(Type 3) is used in render setting + + """ + + label = "Validate Render Frame Range Type" + order = ValidateContentsOrder + families = ["maxrender"] + hosts = ["max"] + actions = [RepairAction] + + def process(self, instance): + if rt.rendTimeType != 3: + raise PublishValidationError("Incorrect type of frame range" + " used in render setting..") + + @classmethod + def repair(cls, instance): + rt.renderTimeType = 3 + return instance From c54447372308ac7fda9d91aa16a0ced50480ad17 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 28 Apr 2023 17:59:36 +0800 Subject: [PATCH 157/360] set frame range validator to switch off by default --- openpype/hosts/max/plugins/publish/validate_frame_range.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/validate_frame_range.py b/openpype/hosts/max/plugins/publish/validate_frame_range.py index 2e1d7c9177..8d5d99197e 100644 --- a/openpype/hosts/max/plugins/publish/validate_frame_range.py +++ b/openpype/hosts/max/plugins/publish/validate_frame_range.py @@ -29,7 +29,7 @@ class ValidateFrameRange(pyblish.api.InstancePlugin, order = ValidateContentsOrder families = ["maxrender"] hosts = ["max"] - optional = True + optional = False actions = [RepairAction] def process(self, instance): From d4422d7ec57fc894bac76b4bf974945d1db3413d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 28 Apr 2023 18:00:12 +0800 Subject: [PATCH 158/360] cosmetic fix --- openpype/hosts/max/plugins/publish/validate_frame_range.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/validate_frame_range.py b/openpype/hosts/max/plugins/publish/validate_frame_range.py index 8d5d99197e..1fafacb8b0 100644 --- a/openpype/hosts/max/plugins/publish/validate_frame_range.py +++ b/openpype/hosts/max/plugins/publish/validate_frame_range.py @@ -44,7 +44,6 @@ class ValidateFrameRange(pyblish.api.InstancePlugin, inst_frame_start = int(instance.data.get("frameStart")) inst_frame_end = int(instance.data.get("frameEnd")) - if frame_start != inst_frame_start: raise PublishValidationError( "startFrame on instance does not match" From 13264ea11c7985f8c5c117f4dcc84ecc925c009b Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 28 Apr 2023 18:41:05 +0800 Subject: [PATCH 159/360] roy's comment --- openpype/hosts/max/api/lib_rendersettings.py | 4 ++-- openpype/hosts/max/plugins/publish/validate_frame_range.py | 6 ++++-- .../hosts/max/plugins/publish/validate_frame_range_type.py | 3 ++- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/max/api/lib_rendersettings.py b/openpype/hosts/max/api/lib_rendersettings.py index 4940265a23..82a25dfa29 100644 --- a/openpype/hosts/max/api/lib_rendersettings.py +++ b/openpype/hosts/max/api/lib_rendersettings.py @@ -6,7 +6,7 @@ from openpype.pipeline import legacy_io from openpype.pipeline.context_tools import get_current_project_asset from openpype.hosts.max.api.lib import ( - set_framerange, + set_render_frame_range, get_current_renderer, get_default_render_folder ) @@ -68,7 +68,7 @@ class RenderSettings(object): # Set Frame Range frame_start = context["data"].get("frame_start") frame_end = context["data"].get("frame_end") - set_framerange(frame_start, frame_end) + set_render_frame_range(frame_start, frame_end) # get the production render renderer_class = get_current_renderer() renderer = str(renderer_class).split(":")[0] diff --git a/openpype/hosts/max/plugins/publish/validate_frame_range.py b/openpype/hosts/max/plugins/publish/validate_frame_range.py index 1fafacb8b0..fc2782ded6 100644 --- a/openpype/hosts/max/plugins/publish/validate_frame_range.py +++ b/openpype/hosts/max/plugins/publish/validate_frame_range.py @@ -47,12 +47,14 @@ class ValidateFrameRange(pyblish.api.InstancePlugin, if frame_start != inst_frame_start: raise PublishValidationError( "startFrame on instance does not match" - " with startFrame from the context data") + " with startFrame from the context data" + " You can use repair action to fix it") if frame_end != inst_frame_end: raise PublishValidationError( "endFrame on instance does not match" - " with endFrame from the context data") + " with endFrame from the context data" + " You can use repair action to fix it") @classmethod def repair(cls, instance): diff --git a/openpype/hosts/max/plugins/publish/validate_frame_range_type.py b/openpype/hosts/max/plugins/publish/validate_frame_range_type.py index 2403595b3b..7bc23e5a70 100644 --- a/openpype/hosts/max/plugins/publish/validate_frame_range_type.py +++ b/openpype/hosts/max/plugins/publish/validate_frame_range_type.py @@ -24,7 +24,8 @@ class ValidateFrameRangeType(pyblish.api.InstancePlugin): def process(self, instance): if rt.rendTimeType != 3: raise PublishValidationError("Incorrect type of frame range" - " used in render setting..") + " used in render setting.." + "Repair action can help to fix it.") @classmethod def repair(cls, instance): From f3b1002f2641bcc122e24debee6b042cb05dee1e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 28 Apr 2023 19:02:17 +0800 Subject: [PATCH 160/360] style fix --- openpype/hosts/max/api/lib.py | 4 ++-- openpype/hosts/max/plugins/publish/validate_frame_range.py | 4 ++-- .../hosts/max/plugins/publish/validate_frame_range_type.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 7d629922fc..1673fc5ab8 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -138,7 +138,7 @@ def get_default_render_folder(project_setting=None): ["default_render_image_folder"]) -def set_framerange(start_frame, end_frame): +def set_render_frame_range(start_frame, end_frame): """ Note: Frame range can be specified in different types. Possible values are: @@ -243,7 +243,7 @@ def reset_frame_range(fps: bool = True): frame_end = frame_range["frameEnd"] + int(frame_range["handleEnd"]) frange_cmd = f"animationRange = interval {frame_start} {frame_end}" rt.execute(frange_cmd) - set_framerange(frame_start, frame_end) + set_render_frame_range(frame_start, frame_end) def set_context_setting(): diff --git a/openpype/hosts/max/plugins/publish/validate_frame_range.py b/openpype/hosts/max/plugins/publish/validate_frame_range.py index fc2782ded6..dc12eece39 100644 --- a/openpype/hosts/max/plugins/publish/validate_frame_range.py +++ b/openpype/hosts/max/plugins/publish/validate_frame_range.py @@ -47,13 +47,13 @@ class ValidateFrameRange(pyblish.api.InstancePlugin, if frame_start != inst_frame_start: raise PublishValidationError( "startFrame on instance does not match" - " with startFrame from the context data" + " with startFrame from the context data." " You can use repair action to fix it") if frame_end != inst_frame_end: raise PublishValidationError( "endFrame on instance does not match" - " with endFrame from the context data" + " with endFrame from the context data." " You can use repair action to fix it") @classmethod diff --git a/openpype/hosts/max/plugins/publish/validate_frame_range_type.py b/openpype/hosts/max/plugins/publish/validate_frame_range_type.py index 7bc23e5a70..944780f6fa 100644 --- a/openpype/hosts/max/plugins/publish/validate_frame_range_type.py +++ b/openpype/hosts/max/plugins/publish/validate_frame_range_type.py @@ -24,7 +24,7 @@ class ValidateFrameRangeType(pyblish.api.InstancePlugin): def process(self, instance): if rt.rendTimeType != 3: raise PublishValidationError("Incorrect type of frame range" - " used in render setting.." + " used in render setting." "Repair action can help to fix it.") @classmethod From fdc94f3b88a1eaaa1509ec5e802d82e261623034 Mon Sep 17 00:00:00 2001 From: Alexey Bogomolov Date: Fri, 28 Apr 2023 17:50:55 +0300 Subject: [PATCH 161/360] add vscode workspace to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 18e7cd7bf2..a565c57b54 100644 --- a/.gitignore +++ b/.gitignore @@ -85,6 +85,7 @@ openpype/premiere/ppro/js/debug.log .env dump.sql test_localsystem.txt +*.code-workspace # website ########## From 015f13bb908e48db077c528e0388ef4c31a39d7a Mon Sep 17 00:00:00 2001 From: Alexey Bogomolov Date: Fri, 28 Apr 2023 17:57:00 +0300 Subject: [PATCH 162/360] better variable naming for utils --- openpype/hosts/resolve/utils.py | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/openpype/hosts/resolve/utils.py b/openpype/hosts/resolve/utils.py index 5881f153ae..8e5dd9a188 100644 --- a/openpype/hosts/resolve/utils.py +++ b/openpype/hosts/resolve/utils.py @@ -8,30 +8,30 @@ RESOLVE_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) def setup(env): log = Logger.get_logger("ResolveSetup") scripts = {} - us_env = env.get("RESOLVE_UTILITY_SCRIPTS_SOURCE_DIR") - us_dir = env["RESOLVE_UTILITY_SCRIPTS_DIR"] + util_scripts_env = env.get("RESOLVE_UTILITY_SCRIPTS_SOURCE_DIR") + util_scripts_dir = env["RESOLVE_UTILITY_SCRIPTS_DIR"] - us_paths = [os.path.join( + util_scripts_paths = [os.path.join( RESOLVE_ROOT_DIR, "utility_scripts" )] # collect script dirs - if us_env: - log.info("Utility Scripts Env: `{}`".format(us_env)) - us_paths = us_env.split( - os.pathsep) + us_paths + if util_scripts_env: + log.info("Utility Scripts Env: `{}`".format(util_scripts_env)) + util_scripts_paths = util_scripts_env.split( + os.pathsep) + util_scripts_paths # collect scripts from dirs - for path in us_paths: + for path in util_scripts_paths: scripts.update({path: os.listdir(path)}) - log.info("Utility Scripts Dir: `{}`".format(us_paths)) + log.info("Utility Scripts Dir: `{}`".format(util_scripts_paths)) log.info("Utility Scripts: `{}`".format(scripts)) # make sure no script file is in folder - for s in os.listdir(us_dir): - path = os.path.join(us_dir, s) + for script in os.listdir(util_scripts_dir): + path = os.path.join(util_scripts_dir, script) log.info("Removing `{}`...".format(path)) if os.path.isdir(path): shutil.rmtree(path, onerror=None) @@ -39,12 +39,10 @@ def setup(env): os.remove(path) # copy scripts into Resolve's utility scripts dir - for d, sl in scripts.items(): - # directory and scripts list - for s in sl: - # script in script list - src = os.path.join(d, s) - dst = os.path.join(us_dir, s) + for directory, scripts in scripts.items(): + for script in scripts: + src = os.path.join(directory, script) + dst = os.path.join(util_scripts_dir, script) log.info("Copying `{}` to `{}`...".format(src, dst)) if os.path.isdir(src): shutil.copytree( From 7cc08d026a1d812ee62c5f12ba67dbf6c2670c60 Mon Sep 17 00:00:00 2001 From: Alexey Bogomolov Date: Fri, 28 Apr 2023 17:57:29 +0300 Subject: [PATCH 163/360] upse pathlib instead of os.path, some cleanup --- .../hosts/resolve/hooks/pre_resolve_setup.py | 55 ++++++++----------- 1 file changed, 23 insertions(+), 32 deletions(-) diff --git a/openpype/hosts/resolve/hooks/pre_resolve_setup.py b/openpype/hosts/resolve/hooks/pre_resolve_setup.py index 8574b3ad01..3144a60312 100644 --- a/openpype/hosts/resolve/hooks/pre_resolve_setup.py +++ b/openpype/hosts/resolve/hooks/pre_resolve_setup.py @@ -1,4 +1,5 @@ import os +from pathlib import Path import platform from openpype.lib import PreLaunchHook from openpype.hosts.resolve.utils import setup @@ -16,10 +17,10 @@ class ResolvePrelaunch(PreLaunchHook): def execute(self): current_platform = platform.system().lower() - PROGRAMDATA = self.launch_context.env.get("PROGRAMDATA", "") - RESOLVE_SCRIPT_API_ = { + programdata = self.launch_context.env.get("PROGRAMDATA", "") + resolve_script_api_locations = { "windows": ( - f"{PROGRAMDATA}/Blackmagic Design/" + f"{programdata}/Blackmagic Design/" "DaVinci Resolve/Support/Developer/Scripting" ), "darwin": ( @@ -28,11 +29,10 @@ class ResolvePrelaunch(PreLaunchHook): ), "linux": "/opt/resolve/Developer/Scripting" } - RESOLVE_SCRIPT_API = os.path.normpath( - RESOLVE_SCRIPT_API_[current_platform]) - self.launch_context.env["RESOLVE_SCRIPT_API"] = RESOLVE_SCRIPT_API + resolve_script_api = Path(resolve_script_api_locations[current_platform]) + self.launch_context.env["RESOLVE_SCRIPT_API"] = resolve_script_api.as_posix() - RESOLVE_SCRIPT_LIB_ = { + resolve_script_lib_dirs = { "windows": ( "C:/Program Files/Blackmagic Design" "/DaVinci Resolve/fusionscript.dll" @@ -43,45 +43,39 @@ class ResolvePrelaunch(PreLaunchHook): ), "linux": "/opt/resolve/libs/Fusion/fusionscript.so" } - RESOLVE_SCRIPT_LIB = os.path.normpath( - RESOLVE_SCRIPT_LIB_[current_platform]) - self.launch_context.env["RESOLVE_SCRIPT_LIB"] = RESOLVE_SCRIPT_LIB + resolve_script_lib = Path(resolve_script_lib_dirs[current_platform]) + self.launch_context.env["RESOLVE_SCRIPT_LIB"] = resolve_script_lib.as_posix() - # TODO: add OTIO installation from `openpype/requirements.py` + # TODO: add OTIO installation from `openpype/requirements.py` # making sure python <3.9.* is installed at provided path - python3_home = os.path.normpath( - self.launch_context.env.get("RESOLVE_PYTHON3_HOME", "")) + python3_home = Path(self.launch_context.env.get("RESOLVE_PYTHON3_HOME", "")) - assert os.path.isdir(python3_home), ( + assert python3_home.is_dir(), ( "Python 3 is not installed at the provided folder path. Either " "make sure the `environments\resolve.json` is having correctly " "set `RESOLVE_PYTHON3_HOME` or make sure Python 3 is installed " f"in given path. \nRESOLVE_PYTHON3_HOME: `{python3_home}`" ) - self.launch_context.env["PYTHONHOME"] = python3_home - self.log.info(f"Path to Resolve Python folder: `{python3_home}`...") + python3_home_str = python3_home.as_posix() + self.launch_context.env["PYTHONHOME"] = python3_home_str + self.log.info(f"Path to Resolve Python folder: `{python3_home_str}`") - # add to the python path to path + # add to the python path to PATH env_path = self.launch_context.env["PATH"] - self.launch_context.env["PATH"] = os.pathsep.join([ - python3_home, - os.path.join(python3_home, "Scripts") - ] + env_path.split(os.pathsep)) + self.launch_context.env["PATH"] = f"{python3_home_str}{os.pathsep}{env_path}" self.log.debug(f"PATH: {self.launch_context.env['PATH']}") # add to the PYTHONPATH env_pythonpath = self.launch_context.env["PYTHONPATH"] - self.launch_context.env["PYTHONPATH"] = os.pathsep.join([ - os.path.join(python3_home, "Lib", "site-packages"), - os.path.join(RESOLVE_SCRIPT_API, "Modules"), - ] + env_pythonpath.split(os.pathsep)) + modules_path = Path(resolve_script_api, "Modules").as_posix() + self.launch_context.env["PYTHONPATH"] = f"{modules_path}{os.pathsep}{env_pythonpath}" self.log.debug(f"PYTHONPATH: {self.launch_context.env['PYTHONPATH']}") - RESOLVE_UTILITY_SCRIPTS_DIR_ = { + resolve_utility_scripts_dirs = { "windows": ( - f"{PROGRAMDATA}/Blackmagic Design" + f"{programdata}/Blackmagic Design" "/DaVinci Resolve/Fusion/Scripts/Comp" ), "darwin": ( @@ -90,12 +84,9 @@ class ResolvePrelaunch(PreLaunchHook): ), "linux": "/opt/resolve/Fusion/Scripts/Comp" } - RESOLVE_UTILITY_SCRIPTS_DIR = os.path.normpath( - RESOLVE_UTILITY_SCRIPTS_DIR_[current_platform] - ) + resolve_utility_scripts_dir = Path(resolve_utility_scripts_dirs[current_platform]) # setting utility scripts dir for scripts syncing - self.launch_context.env["RESOLVE_UTILITY_SCRIPTS_DIR"] = ( - RESOLVE_UTILITY_SCRIPTS_DIR) + self.launch_context.env["RESOLVE_UTILITY_SCRIPTS_DIR"] = resolve_utility_scripts_dir.as_posix() # remove terminal coloring tags self.launch_context.env["OPENPYPE_LOG_NO_COLORS"] = "True" From affc00c77c1bcf473a5aa13e40ef229eb2f30c0f Mon Sep 17 00:00:00 2001 From: Alexey Bogomolov Date: Fri, 28 Apr 2023 17:59:26 +0300 Subject: [PATCH 164/360] remove bin folder from default values --- openpype/settings/defaults/system_settings/applications.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index df5b5e07c6..2c38676126 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -1069,8 +1069,8 @@ "RESOLVE_UTILITY_SCRIPTS_SOURCE_DIR": [], "RESOLVE_PYTHON3_HOME": { "windows": "{LOCALAPPDATA}/Programs/Python/Python36", - "darwin": "~/Library/Python/3.6/bin", - "linux": "/opt/Python/3.6/bin" + "darwin": "~/Library/Python/3.9", + "linux": "/opt/Python/3.9" } }, "variants": { From 74c1e6f3bb860c5aea18a669cb285d5fb2f0ca43 Mon Sep 17 00:00:00 2001 From: Alexey Bogomolov Date: Fri, 28 Apr 2023 18:12:26 +0300 Subject: [PATCH 165/360] defaults to py3.6, set actual macos python path --- openpype/settings/defaults/system_settings/applications.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index 2c38676126..9aa30093cc 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -1069,8 +1069,8 @@ "RESOLVE_UTILITY_SCRIPTS_SOURCE_DIR": [], "RESOLVE_PYTHON3_HOME": { "windows": "{LOCALAPPDATA}/Programs/Python/Python36", - "darwin": "~/Library/Python/3.9", - "linux": "/opt/Python/3.9" + "darwin": "/Library/Frameworks/Python.framework/Versions/3.6", + "linux": "/opt/Python/3.6" } }, "variants": { From 4388da15df4b6a5f5a07e3514c54652e12b58691 Mon Sep 17 00:00:00 2001 From: Alexey Bogomolov Date: Fri, 28 Apr 2023 18:32:33 +0300 Subject: [PATCH 166/360] black formatting --- .../hosts/resolve/hooks/pre_resolve_setup.py | 39 +++++++++++++------ 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/resolve/hooks/pre_resolve_setup.py b/openpype/hosts/resolve/hooks/pre_resolve_setup.py index 3144a60312..8c88478104 100644 --- a/openpype/hosts/resolve/hooks/pre_resolve_setup.py +++ b/openpype/hosts/resolve/hooks/pre_resolve_setup.py @@ -12,6 +12,7 @@ class ResolvePrelaunch(PreLaunchHook): path to the project by environment variable to Premiere launcher shell script. """ + app_groups = ["resolve"] def execute(self): @@ -27,10 +28,14 @@ class ResolvePrelaunch(PreLaunchHook): "/Library/Application Support/Blackmagic Design" "/DaVinci Resolve/Developer/Scripting" ), - "linux": "/opt/resolve/Developer/Scripting" + "linux": "/opt/resolve/Developer/Scripting", } - resolve_script_api = Path(resolve_script_api_locations[current_platform]) - self.launch_context.env["RESOLVE_SCRIPT_API"] = resolve_script_api.as_posix() + resolve_script_api = Path( + resolve_script_api_locations[current_platform] + ) + self.launch_context.env[ + "RESOLVE_SCRIPT_API" + ] = resolve_script_api.as_posix() resolve_script_lib_dirs = { "windows": ( @@ -41,14 +46,18 @@ class ResolvePrelaunch(PreLaunchHook): "/Applications/DaVinci Resolve/DaVinci Resolve.app" "/Contents/Libraries/Fusion/fusionscript.so" ), - "linux": "/opt/resolve/libs/Fusion/fusionscript.so" + "linux": "/opt/resolve/libs/Fusion/fusionscript.so", } resolve_script_lib = Path(resolve_script_lib_dirs[current_platform]) - self.launch_context.env["RESOLVE_SCRIPT_LIB"] = resolve_script_lib.as_posix() + self.launch_context.env[ + "RESOLVE_SCRIPT_LIB" + ] = resolve_script_lib.as_posix() # TODO: add OTIO installation from `openpype/requirements.py` # making sure python <3.9.* is installed at provided path - python3_home = Path(self.launch_context.env.get("RESOLVE_PYTHON3_HOME", "")) + python3_home = Path( + self.launch_context.env.get("RESOLVE_PYTHON3_HOME", "") + ) assert python3_home.is_dir(), ( "Python 3 is not installed at the provided folder path. Either " @@ -62,14 +71,18 @@ class ResolvePrelaunch(PreLaunchHook): # add to the python path to PATH env_path = self.launch_context.env["PATH"] - self.launch_context.env["PATH"] = f"{python3_home_str}{os.pathsep}{env_path}" + self.launch_context.env[ + "PATH" + ] = f"{python3_home_str}{os.pathsep}{env_path}" self.log.debug(f"PATH: {self.launch_context.env['PATH']}") # add to the PYTHONPATH env_pythonpath = self.launch_context.env["PYTHONPATH"] modules_path = Path(resolve_script_api, "Modules").as_posix() - self.launch_context.env["PYTHONPATH"] = f"{modules_path}{os.pathsep}{env_pythonpath}" + self.launch_context.env[ + "PYTHONPATH" + ] = f"{modules_path}{os.pathsep}{env_pythonpath}" self.log.debug(f"PYTHONPATH: {self.launch_context.env['PYTHONPATH']}") @@ -82,11 +95,15 @@ class ResolvePrelaunch(PreLaunchHook): "/Library/Application Support/Blackmagic Design" "/DaVinci Resolve/Fusion/Scripts/Comp" ), - "linux": "/opt/resolve/Fusion/Scripts/Comp" + "linux": "/opt/resolve/Fusion/Scripts/Comp", } - resolve_utility_scripts_dir = Path(resolve_utility_scripts_dirs[current_platform]) + resolve_utility_scripts_dir = Path( + resolve_utility_scripts_dirs[current_platform] + ) # setting utility scripts dir for scripts syncing - self.launch_context.env["RESOLVE_UTILITY_SCRIPTS_DIR"] = resolve_utility_scripts_dir.as_posix() + self.launch_context.env[ + "RESOLVE_UTILITY_SCRIPTS_DIR" + ] = resolve_utility_scripts_dir.as_posix() # remove terminal coloring tags self.launch_context.env["OPENPYPE_LOG_NO_COLORS"] = "True" From 1d2123dce8cf3d5fd70467cac32e1f9a484ee199 Mon Sep 17 00:00:00 2001 From: Alexey Bogomolov <11698866+movalex@users.noreply.github.com> Date: Sat, 29 Apr 2023 10:42:15 +0300 Subject: [PATCH 167/360] Update .gitignore Co-authored-by: Roy Nieterau --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index a565c57b54..18e7cd7bf2 100644 --- a/.gitignore +++ b/.gitignore @@ -85,7 +85,6 @@ openpype/premiere/ppro/js/debug.log .env dump.sql test_localsystem.txt -*.code-workspace # website ########## From 3e2559c0c2797c8c3dba717ac9594cd22499b80b Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Sat, 29 Apr 2023 16:32:52 +0100 Subject: [PATCH 168/360] Fix repair and validation --- openpype/hosts/maya/plugins/publish/validate_attributes.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_attributes.py b/openpype/hosts/maya/plugins/publish/validate_attributes.py index 6ca9afb9a4..7ebd9d7d03 100644 --- a/openpype/hosts/maya/plugins/publish/validate_attributes.py +++ b/openpype/hosts/maya/plugins/publish/validate_attributes.py @@ -6,7 +6,7 @@ import pyblish.api from openpype.hosts.maya.api.lib import set_attribute from openpype.pipeline.publish import ( - RepairContextAction, + RepairAction, ValidateContentsOrder, ) @@ -26,7 +26,7 @@ class ValidateAttributes(pyblish.api.InstancePlugin): order = ValidateContentsOrder label = "Attributes" hosts = ["maya"] - actions = [RepairContextAction] + actions = [RepairAction] optional = True attributes = None @@ -81,7 +81,7 @@ class ValidateAttributes(pyblish.api.InstancePlugin): if node_name not in attributes: continue - for attr_name, expected in attributes.items(): + for attr_name, expected in attributes[node_name].items(): # Skip if attribute does not exist if not cmds.attributeQuery(attr_name, node=node, exists=True): From 88e7b3386b70756ff0b3a81bfd6a33bcd062830a Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 2 May 2023 11:22:10 +0800 Subject: [PATCH 169/360] cosmetic fix --- openpype/hosts/max/plugins/publish/validate_frame_range_type.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/validate_frame_range_type.py b/openpype/hosts/max/plugins/publish/validate_frame_range_type.py index 944780f6fa..d77b1503e0 100644 --- a/openpype/hosts/max/plugins/publish/validate_frame_range_type.py +++ b/openpype/hosts/max/plugins/publish/validate_frame_range_type.py @@ -25,7 +25,7 @@ class ValidateFrameRangeType(pyblish.api.InstancePlugin): if rt.rendTimeType != 3: raise PublishValidationError("Incorrect type of frame range" " used in render setting." - "Repair action can help to fix it.") + " Repair action can help to fix it.") @classmethod def repair(cls, instance): From b8ce6e9e9c10383c7e7e0c36fba7bb603a5d9ee7 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 2 May 2023 11:19:50 +0200 Subject: [PATCH 170/360] Photoshop: add autocreators for review and flat image (#4871) * OP-5656 - added auto creator for review in PS Review instance should be togglable. Review instance needs to be created for non publisher based workflows. * OP-5656 - refactored names * OP-5656 - refactored names * OP-5656 - new auto creator for flat image In old version flat image was created if no instances were created. Explicit auto creator added for clarity. Standardization of state of plugins * OP-5656 - updated according to auto image creator Subset template should be used from autocreator and not be separate. * OP-5656 - fix proper creator name * OP-5656 - fix log message * OP-5656 - fix use enable state * OP-5656 - fix formatting * OP-5656 - add review toggle to image instance For special cases where each image should have separate review. * OP-5656 - fix description * OP-5656 - fix not present asset and task in instance context * OP-5656 - refactor - both auto creators should use same class Provided separate description. * OP-5656 - fix - propagate review to families Image and auto image could have now review flag. Bottom logic is only for Webpublisher. * OP-5656 - fix - rename review files to avaid collision Image family produces jpg and png, jpg review would clash with name. It should be replaced by 'jpg_jpg'. * OP-5656 - fix - limit additional auto created only on WP In artist based publishing auto image would be created by auto creator (if enabled). Artist might want to disable image creation. * OP-5656 - added mark_for_review flag to Publish tab * OP-5656 - fixes for auto creator * OP-5656 - fixe - outputDef not needed outputDef should contain dict of output definition. In PS it doesn't make sense as it has separate extract_review without output definitions. * OP-5656 - added persistency of changes to auto creators Changes as enabling/disabling, changing review flag should persist. * OP-5656 - added documentation for admins * OP-5656 - added link to new documentation for admins * OP-5656 - Hound * OP-5656 - Hound * OP-5656 - fix shared families list * OP-5656 - added default variant for review and workfile creator For workfile Main was default variant, "" was for review. * OP-5656 - fix - use values from Settings * OP-5656 - fix - use original name of review for main review family outputName cannot be in repre or file would have ..._jpg.jpg * OP-5656 - refactor - standardized settings Active by default denotes if created instance is active (eg. publishable) when created. * OP-5656 - fixes for skipping collecting auto_image data["ids"] are necessary for extracting. Members are physical layers in image, ids are "virtual" items, won't get grouped into real image instance. * OP-5656 - reworked auto collectors This allows to use automatic test for proper testing. * OP-5656 - added automatic tests * OP-5656 - fixes for auto collectors * OP-5656 - removed unnecessary collector Logic moved to auto collectors. * OP-5656 - Hound --- .../create/workfile_creator.py => lib.py} | 23 +-- .../plugins/create/create_flatten_image.py | 120 ++++++++++++++ .../photoshop/plugins/create/create_image.py | 47 +++++- .../photoshop/plugins/create/create_review.py | 28 ++++ .../plugins/create/create_workfile.py | 28 ++++ .../plugins/publish/collect_auto_image.py | 101 ++++++++++++ .../plugins/publish/collect_auto_review.py | 92 +++++++++++ .../plugins/publish/collect_auto_workfile.py | 99 ++++++++++++ .../plugins/publish/collect_instances.py | 116 -------------- .../plugins/publish/collect_review.py | 32 +--- .../plugins/publish/collect_workfile.py | 57 ++----- .../plugins/publish/extract_review.py | 34 ++-- .../defaults/project_settings/photoshop.json | 29 +++- .../schema_project_photoshop.json | 151 +++++++++++++++--- .../test_publish_in_photoshop_auto_image.py | 93 +++++++++++ .../test_publish_in_photoshop_review.py | 111 +++++++++++++ website/docs/admin_hosts_photoshop.md | 127 +++++++++++++++ .../assets/admin_hosts_photoshop_settings.png | Bin 0 -> 14364 bytes website/sidebars.js | 1 + 19 files changed, 1044 insertions(+), 245 deletions(-) rename openpype/hosts/photoshop/{plugins/create/workfile_creator.py => lib.py} (83%) create mode 100644 openpype/hosts/photoshop/plugins/create/create_flatten_image.py create mode 100644 openpype/hosts/photoshop/plugins/create/create_review.py create mode 100644 openpype/hosts/photoshop/plugins/create/create_workfile.py create mode 100644 openpype/hosts/photoshop/plugins/publish/collect_auto_image.py create mode 100644 openpype/hosts/photoshop/plugins/publish/collect_auto_review.py create mode 100644 openpype/hosts/photoshop/plugins/publish/collect_auto_workfile.py delete mode 100644 openpype/hosts/photoshop/plugins/publish/collect_instances.py create mode 100644 tests/integration/hosts/photoshop/test_publish_in_photoshop_auto_image.py create mode 100644 tests/integration/hosts/photoshop/test_publish_in_photoshop_review.py create mode 100644 website/docs/admin_hosts_photoshop.md create mode 100644 website/docs/assets/admin_hosts_photoshop_settings.png diff --git a/openpype/hosts/photoshop/plugins/create/workfile_creator.py b/openpype/hosts/photoshop/lib.py similarity index 83% rename from openpype/hosts/photoshop/plugins/create/workfile_creator.py rename to openpype/hosts/photoshop/lib.py index f5d56adcbc..ae7a33b7b6 100644 --- a/openpype/hosts/photoshop/plugins/create/workfile_creator.py +++ b/openpype/hosts/photoshop/lib.py @@ -7,28 +7,26 @@ from openpype.pipeline import ( from openpype.hosts.photoshop.api.pipeline import cache_and_get_instances -class PSWorkfileCreator(AutoCreator): - identifier = "workfile" - family = "workfile" - - default_variant = "Main" - +class PSAutoCreator(AutoCreator): + """Generic autocreator to extend.""" def get_instance_attr_defs(self): return [] def collect_instances(self): for instance_data in cache_and_get_instances(self): creator_id = instance_data.get("creator_identifier") + if creator_id == self.identifier: - subset_name = instance_data["subset"] - instance = CreatedInstance( - self.family, subset_name, instance_data, self + instance = CreatedInstance.from_existing( + instance_data, self ) self._add_instance_to_context(instance) def update_instances(self, update_list): - # nothing to change on workfiles - pass + self.log.debug("update_list:: {}".format(update_list)) + for created_inst, _changes in update_list: + api.stub().imprint(created_inst.get("instance_id"), + created_inst.data_to_store()) def create(self, options=None): existing_instance = None @@ -58,6 +56,9 @@ class PSWorkfileCreator(AutoCreator): project_name, host_name, None )) + if not self.active_on_create: + data["active"] = False + new_instance = CreatedInstance( self.family, subset_name, data, self ) diff --git a/openpype/hosts/photoshop/plugins/create/create_flatten_image.py b/openpype/hosts/photoshop/plugins/create/create_flatten_image.py new file mode 100644 index 0000000000..3bc61c8184 --- /dev/null +++ b/openpype/hosts/photoshop/plugins/create/create_flatten_image.py @@ -0,0 +1,120 @@ +from openpype.pipeline import CreatedInstance + +from openpype.lib import BoolDef +import openpype.hosts.photoshop.api as api +from openpype.hosts.photoshop.lib import PSAutoCreator +from openpype.pipeline.create import get_subset_name +from openpype.client import get_asset_by_name + + +class AutoImageCreator(PSAutoCreator): + """Creates flatten image from all visible layers. + + Used in simplified publishing as auto created instance. + Must be enabled in Setting and template for subset name provided + """ + identifier = "auto_image" + family = "image" + + # Settings + default_variant = "" + # - Mark by default instance for review + mark_for_review = True + active_on_create = True + + def create(self, options=None): + existing_instance = None + for instance in self.create_context.instances: + if instance.creator_identifier == self.identifier: + existing_instance = instance + break + + context = self.create_context + project_name = context.get_current_project_name() + asset_name = context.get_current_asset_name() + task_name = context.get_current_task_name() + host_name = context.host_name + asset_doc = get_asset_by_name(project_name, asset_name) + + if existing_instance is None: + subset_name = get_subset_name( + self.family, self.default_variant, task_name, asset_doc, + project_name, host_name + ) + + publishable_ids = [layer.id for layer in api.stub().get_layers() + if layer.visible] + data = { + "asset": asset_name, + "task": task_name, + # ids are "virtual" layers, won't get grouped as 'members' do + # same difference in color coded layers in WP + "ids": publishable_ids + } + + if not self.active_on_create: + data["active"] = False + + creator_attributes = {"mark_for_review": self.mark_for_review} + data.update({"creator_attributes": creator_attributes}) + + new_instance = CreatedInstance( + self.family, subset_name, data, self + ) + self._add_instance_to_context(new_instance) + api.stub().imprint(new_instance.get("instance_id"), + new_instance.data_to_store()) + + elif ( # existing instance from different context + existing_instance["asset"] != asset_name + or existing_instance["task"] != task_name + ): + subset_name = get_subset_name( + self.family, self.default_variant, task_name, asset_doc, + project_name, host_name + ) + + existing_instance["asset"] = asset_name + existing_instance["task"] = task_name + existing_instance["subset"] = subset_name + + api.stub().imprint(existing_instance.get("instance_id"), + existing_instance.data_to_store()) + + def get_pre_create_attr_defs(self): + return [ + BoolDef( + "mark_for_review", + label="Review", + default=self.mark_for_review + ) + ] + + def get_instance_attr_defs(self): + return [ + BoolDef( + "mark_for_review", + label="Review" + ) + ] + + def apply_settings(self, project_settings, system_settings): + plugin_settings = ( + project_settings["photoshop"]["create"]["AutoImageCreator"] + ) + + self.active_on_create = plugin_settings["active_on_create"] + self.default_variant = plugin_settings["default_variant"] + self.mark_for_review = plugin_settings["mark_for_review"] + self.enabled = plugin_settings["enabled"] + + def get_detail_description(self): + return """Creator for flatten image. + + Studio might configure simple publishing workflow. In that case + `image` instance is automatically created which will publish flat + image from all visible layers. + + Artist might disable this instance from publishing or from creating + review for it though. + """ diff --git a/openpype/hosts/photoshop/plugins/create/create_image.py b/openpype/hosts/photoshop/plugins/create/create_image.py index 3d82d6b6f0..f3165fca57 100644 --- a/openpype/hosts/photoshop/plugins/create/create_image.py +++ b/openpype/hosts/photoshop/plugins/create/create_image.py @@ -23,6 +23,11 @@ class ImageCreator(Creator): family = "image" description = "Image creator" + # Settings + default_variants = "" + mark_for_review = False + active_on_create = True + def create(self, subset_name_from_ui, data, pre_create_data): groups_to_create = [] top_layers_to_wrap = [] @@ -94,6 +99,12 @@ class ImageCreator(Creator): data.update({"layer_name": layer_name}) data.update({"long_name": "_".join(layer_names_in_hierarchy)}) + creator_attributes = {"mark_for_review": self.mark_for_review} + data.update({"creator_attributes": creator_attributes}) + + if not self.active_on_create: + data["active"] = False + new_instance = CreatedInstance(self.family, subset_name, data, self) @@ -134,11 +145,6 @@ class ImageCreator(Creator): self.host.remove_instance(instance) self._remove_instance_from_context(instance) - def get_default_variants(self): - return [ - "Main" - ] - def get_pre_create_attr_defs(self): output = [ BoolDef("use_selection", default=True, @@ -148,10 +154,34 @@ class ImageCreator(Creator): label="Create separate instance for each selected"), BoolDef("use_layer_name", default=False, - label="Use layer name in subset") + label="Use layer name in subset"), + BoolDef( + "mark_for_review", + label="Create separate review", + default=False + ) ] return output + def get_instance_attr_defs(self): + return [ + BoolDef( + "mark_for_review", + label="Review" + ) + ] + + def apply_settings(self, project_settings, system_settings): + plugin_settings = ( + project_settings["photoshop"]["create"]["ImageCreator"] + ) + + self.active_on_create = plugin_settings["active_on_create"] + self.default_variants = plugin_settings["default_variants"] + self.mark_for_review = plugin_settings["mark_for_review"] + self.enabled = plugin_settings["enabled"] + + def get_detail_description(self): return """Creator for Image instances @@ -180,6 +210,11 @@ class ImageCreator(Creator): but layer name should be used (set explicitly in UI or implicitly if multiple images should be created), it is added in capitalized form as a suffix to subset name. + + Each image could have its separate review created if necessary via + `Create separate review` toggle. + But more use case is to use separate `review` instance to create review + from all published items. """ def _handle_legacy(self, instance_data): diff --git a/openpype/hosts/photoshop/plugins/create/create_review.py b/openpype/hosts/photoshop/plugins/create/create_review.py new file mode 100644 index 0000000000..064485d465 --- /dev/null +++ b/openpype/hosts/photoshop/plugins/create/create_review.py @@ -0,0 +1,28 @@ +from openpype.hosts.photoshop.lib import PSAutoCreator + + +class ReviewCreator(PSAutoCreator): + """Creates review instance which might be disabled from publishing.""" + identifier = "review" + family = "review" + + default_variant = "Main" + + def get_detail_description(self): + return """Auto creator for review. + + Photoshop review is created from all published images or from all + visible layers if no `image` instances got created. + + Review might be disabled by an artist (instance shouldn't be deleted as + it will get recreated in next publish either way). + """ + + def apply_settings(self, project_settings, system_settings): + plugin_settings = ( + project_settings["photoshop"]["create"]["ReviewCreator"] + ) + + self.default_variant = plugin_settings["default_variant"] + self.active_on_create = plugin_settings["active_on_create"] + self.enabled = plugin_settings["enabled"] diff --git a/openpype/hosts/photoshop/plugins/create/create_workfile.py b/openpype/hosts/photoshop/plugins/create/create_workfile.py new file mode 100644 index 0000000000..d498f0549c --- /dev/null +++ b/openpype/hosts/photoshop/plugins/create/create_workfile.py @@ -0,0 +1,28 @@ +from openpype.hosts.photoshop.lib import PSAutoCreator + + +class WorkfileCreator(PSAutoCreator): + identifier = "workfile" + family = "workfile" + + default_variant = "Main" + + def get_detail_description(self): + return """Auto creator for workfile. + + It is expected that each publish will also publish its source workfile + for safekeeping. This creator triggers automatically without need for + an artist to remember and trigger it explicitly. + + Workfile instance could be disabled if it is not required to publish + workfile. (Instance shouldn't be deleted though as it will be recreated + in next publish automatically). + """ + + def apply_settings(self, project_settings, system_settings): + plugin_settings = ( + project_settings["photoshop"]["create"]["WorkfileCreator"] + ) + + self.active_on_create = plugin_settings["active_on_create"] + self.enabled = plugin_settings["enabled"] diff --git a/openpype/hosts/photoshop/plugins/publish/collect_auto_image.py b/openpype/hosts/photoshop/plugins/publish/collect_auto_image.py new file mode 100644 index 0000000000..ce408f8d01 --- /dev/null +++ b/openpype/hosts/photoshop/plugins/publish/collect_auto_image.py @@ -0,0 +1,101 @@ +import pyblish.api + +from openpype.hosts.photoshop import api as photoshop +from openpype.pipeline.create import get_subset_name + + +class CollectAutoImage(pyblish.api.ContextPlugin): + """Creates auto image in non artist based publishes (Webpublisher). + + 'remotepublish' should be renamed to 'autopublish' or similar in the future + """ + + label = "Collect Auto Image" + order = pyblish.api.CollectorOrder + hosts = ["photoshop"] + order = pyblish.api.CollectorOrder + 0.2 + + targets = ["remotepublish"] + + def process(self, context): + family = "image" + for instance in context: + creator_identifier = instance.data.get("creator_identifier") + if creator_identifier and creator_identifier == "auto_image": + self.log.debug("Auto image instance found, won't create new") + return + + project_name = context.data["anatomyData"]["project"]["name"] + proj_settings = context.data["project_settings"] + task_name = context.data["anatomyData"]["task"]["name"] + host_name = context.data["hostName"] + asset_doc = context.data["assetEntity"] + asset_name = asset_doc["name"] + + auto_creator = proj_settings.get( + "photoshop", {}).get( + "create", {}).get( + "AutoImageCreator", {}) + + if not auto_creator or not auto_creator["enabled"]: + self.log.debug("Auto image creator disabled, won't create new") + return + + stub = photoshop.stub() + stored_items = stub.get_layers_metadata() + for item in stored_items: + if item.get("creator_identifier") == "auto_image": + if not item.get("active"): + self.log.debug("Auto_image instance disabled") + return + + layer_items = stub.get_layers() + + publishable_ids = [layer.id for layer in layer_items + if layer.visible] + + # collect stored image instances + instance_names = [] + for layer_item in layer_items: + layer_meta_data = stub.read(layer_item, stored_items) + + # Skip layers without metadata. + if layer_meta_data is None: + continue + + # Skip containers. + if "container" in layer_meta_data["id"]: + continue + + # active might not be in legacy meta + if layer_meta_data.get("active", True) and layer_item.visible: + instance_names.append(layer_meta_data["subset"]) + + if len(instance_names) == 0: + variants = proj_settings.get( + "photoshop", {}).get( + "create", {}).get( + "CreateImage", {}).get( + "default_variants", ['']) + family = "image" + + variant = context.data.get("variant") or variants[0] + + subset_name = get_subset_name( + family, variant, task_name, asset_doc, + project_name, host_name + ) + + instance = context.create_instance(subset_name) + instance.data["family"] = family + instance.data["asset"] = asset_name + instance.data["subset"] = subset_name + instance.data["ids"] = publishable_ids + instance.data["publish"] = True + instance.data["creator_identifier"] = "auto_image" + + if auto_creator["mark_for_review"]: + instance.data["creator_attributes"] = {"mark_for_review": True} + instance.data["families"] = ["review"] + + self.log.info("auto image instance: {} ".format(instance.data)) diff --git a/openpype/hosts/photoshop/plugins/publish/collect_auto_review.py b/openpype/hosts/photoshop/plugins/publish/collect_auto_review.py new file mode 100644 index 0000000000..7de4adcaf4 --- /dev/null +++ b/openpype/hosts/photoshop/plugins/publish/collect_auto_review.py @@ -0,0 +1,92 @@ +""" +Requires: + None + +Provides: + instance -> family ("review") +""" +import pyblish.api + +from openpype.hosts.photoshop import api as photoshop +from openpype.pipeline.create import get_subset_name + + +class CollectAutoReview(pyblish.api.ContextPlugin): + """Create review instance in non artist based workflow. + + Called only if PS is triggered in Webpublisher or in tests. + """ + + label = "Collect Auto Review" + hosts = ["photoshop"] + order = pyblish.api.CollectorOrder + 0.2 + targets = ["remotepublish"] + + publish = True + + def process(self, context): + family = "review" + has_review = False + for instance in context: + if instance.data["family"] == family: + self.log.debug("Review instance found, won't create new") + has_review = True + + creator_attributes = instance.data.get("creator_attributes", {}) + if (creator_attributes.get("mark_for_review") and + "review" not in instance.data["families"]): + instance.data["families"].append("review") + + if has_review: + return + + stub = photoshop.stub() + stored_items = stub.get_layers_metadata() + for item in stored_items: + if item.get("creator_identifier") == family: + if not item.get("active"): + self.log.debug("Review instance disabled") + return + + auto_creator = context.data["project_settings"].get( + "photoshop", {}).get( + "create", {}).get( + "ReviewCreator", {}) + + if not auto_creator or not auto_creator["enabled"]: + self.log.debug("Review creator disabled, won't create new") + return + + variant = (context.data.get("variant") or + auto_creator["default_variant"]) + + project_name = context.data["anatomyData"]["project"]["name"] + proj_settings = context.data["project_settings"] + task_name = context.data["anatomyData"]["task"]["name"] + host_name = context.data["hostName"] + asset_doc = context.data["assetEntity"] + asset_name = asset_doc["name"] + + subset_name = get_subset_name( + family, + variant, + task_name, + asset_doc, + project_name, + host_name=host_name, + project_settings=proj_settings + ) + + instance = context.create_instance(subset_name) + instance.data.update({ + "subset": subset_name, + "label": subset_name, + "name": subset_name, + "family": family, + "families": [], + "representations": [], + "asset": asset_name, + "publish": self.publish + }) + + self.log.debug("auto review created::{}".format(instance.data)) diff --git a/openpype/hosts/photoshop/plugins/publish/collect_auto_workfile.py b/openpype/hosts/photoshop/plugins/publish/collect_auto_workfile.py new file mode 100644 index 0000000000..d10cf62c67 --- /dev/null +++ b/openpype/hosts/photoshop/plugins/publish/collect_auto_workfile.py @@ -0,0 +1,99 @@ +import os +import pyblish.api + +from openpype.hosts.photoshop import api as photoshop +from openpype.pipeline.create import get_subset_name + + +class CollectAutoWorkfile(pyblish.api.ContextPlugin): + """Collect current script for publish.""" + + order = pyblish.api.CollectorOrder + 0.2 + label = "Collect Workfile" + hosts = ["photoshop"] + + targets = ["remotepublish"] + + def process(self, context): + family = "workfile" + file_path = context.data["currentFile"] + _, ext = os.path.splitext(file_path) + staging_dir = os.path.dirname(file_path) + base_name = os.path.basename(file_path) + workfile_representation = { + "name": ext[1:], + "ext": ext[1:], + "files": base_name, + "stagingDir": staging_dir, + } + + for instance in context: + if instance.data["family"] == family: + self.log.debug("Workfile instance found, won't create new") + instance.data.update({ + "label": base_name, + "name": base_name, + "representations": [], + }) + + # creating representation + _, ext = os.path.splitext(file_path) + instance.data["representations"].append( + workfile_representation) + + return + + stub = photoshop.stub() + stored_items = stub.get_layers_metadata() + for item in stored_items: + if item.get("creator_identifier") == family: + if not item.get("active"): + self.log.debug("Workfile instance disabled") + return + + project_name = context.data["anatomyData"]["project"]["name"] + proj_settings = context.data["project_settings"] + auto_creator = proj_settings.get( + "photoshop", {}).get( + "create", {}).get( + "WorkfileCreator", {}) + + if not auto_creator or not auto_creator["enabled"]: + self.log.debug("Workfile creator disabled, won't create new") + return + + # context.data["variant"] might come only from collect_batch_data + variant = (context.data.get("variant") or + auto_creator["default_variant"]) + + task_name = context.data["anatomyData"]["task"]["name"] + host_name = context.data["hostName"] + asset_doc = context.data["assetEntity"] + asset_name = asset_doc["name"] + + subset_name = get_subset_name( + family, + variant, + task_name, + asset_doc, + project_name, + host_name=host_name, + project_settings=proj_settings + ) + + # Create instance + instance = context.create_instance(subset_name) + instance.data.update({ + "subset": subset_name, + "label": base_name, + "name": base_name, + "family": family, + "families": [], + "representations": [], + "asset": asset_name + }) + + # creating representation + instance.data["representations"].append(workfile_representation) + + self.log.debug("auto workfile review created:{}".format(instance.data)) diff --git a/openpype/hosts/photoshop/plugins/publish/collect_instances.py b/openpype/hosts/photoshop/plugins/publish/collect_instances.py deleted file mode 100644 index 5bf12379b1..0000000000 --- a/openpype/hosts/photoshop/plugins/publish/collect_instances.py +++ /dev/null @@ -1,116 +0,0 @@ -import pprint - -import pyblish.api - -from openpype.settings import get_project_settings -from openpype.hosts.photoshop import api as photoshop -from openpype.lib import prepare_template_data -from openpype.pipeline import legacy_io - - -class CollectInstances(pyblish.api.ContextPlugin): - """Gather instances by LayerSet and file metadata - - Collects publishable instances from file metadata or enhance - already collected by creator (family == "image"). - - If no image instances are explicitly created, it looks if there is value - in `flatten_subset_template` (configurable in Settings), in that case it - produces flatten image with all visible layers. - - Identifier: - id (str): "pyblish.avalon.instance" - """ - - label = "Collect Instances" - order = pyblish.api.CollectorOrder - hosts = ["photoshop"] - families_mapping = { - "image": [] - } - # configurable in Settings - flatten_subset_template = "" - - def process(self, context): - instance_by_layer_id = {} - for instance in context: - if ( - instance.data["family"] == "image" and - instance.data.get("members")): - layer_id = str(instance.data["members"][0]) - instance_by_layer_id[layer_id] = instance - - stub = photoshop.stub() - layer_items = stub.get_layers() - layers_meta = stub.get_layers_metadata() - instance_names = [] - - all_layer_ids = [] - for layer_item in layer_items: - layer_meta_data = stub.read(layer_item, layers_meta) - all_layer_ids.append(layer_item.id) - - # Skip layers without metadata. - if layer_meta_data is None: - continue - - # Skip containers. - if "container" in layer_meta_data["id"]: - continue - - # active might not be in legacy meta - if not layer_meta_data.get("active", True): - continue - - instance = instance_by_layer_id.get(str(layer_item.id)) - if instance is None: - instance = context.create_instance(layer_meta_data["subset"]) - - instance.data["layer"] = layer_item - instance.data.update(layer_meta_data) - instance.data["families"] = self.families_mapping[ - layer_meta_data["family"] - ] - instance.data["publish"] = layer_item.visible - instance_names.append(layer_meta_data["subset"]) - - # Produce diagnostic message for any graphical - # user interface interested in visualising it. - self.log.info("Found: \"%s\" " % instance.data["name"]) - self.log.info("instance: {} ".format( - pprint.pformat(instance.data, indent=4))) - - if len(instance_names) != len(set(instance_names)): - self.log.warning("Duplicate instances found. " + - "Remove unwanted via Publisher") - - if len(instance_names) == 0 and self.flatten_subset_template: - project_name = context.data["projectEntity"]["name"] - variants = get_project_settings(project_name).get( - "photoshop", {}).get( - "create", {}).get( - "CreateImage", {}).get( - "defaults", ['']) - family = "image" - task_name = legacy_io.Session["AVALON_TASK"] - asset_name = context.data["assetEntity"]["name"] - - variant = context.data.get("variant") or variants[0] - fill_pairs = { - "variant": variant, - "family": family, - "task": task_name - } - - subset = self.flatten_subset_template.format( - **prepare_template_data(fill_pairs)) - - instance = context.create_instance(subset) - instance.data["family"] = family - instance.data["asset"] = asset_name - instance.data["subset"] = subset - instance.data["ids"] = all_layer_ids - instance.data["families"] = self.families_mapping[family] - instance.data["publish"] = True - - self.log.info("flatten instance: {} ".format(instance.data)) diff --git a/openpype/hosts/photoshop/plugins/publish/collect_review.py b/openpype/hosts/photoshop/plugins/publish/collect_review.py index 7e598a8250..87ec4ee3f1 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_review.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_review.py @@ -14,10 +14,7 @@ from openpype.pipeline.create import get_subset_name class CollectReview(pyblish.api.ContextPlugin): - """Gather the active document as review instance. - - Triggers once even if no 'image' is published as by defaults it creates - flatten image from a workfile. + """Adds review to families for instances marked to be reviewable. """ label = "Collect Review" @@ -28,25 +25,8 @@ class CollectReview(pyblish.api.ContextPlugin): publish = True def process(self, context): - family = "review" - subset = get_subset_name( - family, - context.data.get("variant", ''), - context.data["anatomyData"]["task"]["name"], - context.data["assetEntity"], - context.data["anatomyData"]["project"]["name"], - host_name=context.data["hostName"], - project_settings=context.data["project_settings"] - ) - - instance = context.create_instance(subset) - instance.data.update({ - "subset": subset, - "label": subset, - "name": subset, - "family": family, - "families": [], - "representations": [], - "asset": os.environ["AVALON_ASSET"], - "publish": self.publish - }) + for instance in context: + creator_attributes = instance.data["creator_attributes"] + if (creator_attributes.get("mark_for_review") and + "review" not in instance.data["families"]): + instance.data["families"].append("review") diff --git a/openpype/hosts/photoshop/plugins/publish/collect_workfile.py b/openpype/hosts/photoshop/plugins/publish/collect_workfile.py index 9a5aad5569..9625464499 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_workfile.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_workfile.py @@ -14,50 +14,19 @@ class CollectWorkfile(pyblish.api.ContextPlugin): default_variant = "Main" def process(self, context): - existing_instance = None for instance in context: if instance.data["family"] == "workfile": - self.log.debug("Workfile instance found, won't create new") - existing_instance = instance - break + file_path = context.data["currentFile"] + _, ext = os.path.splitext(file_path) + staging_dir = os.path.dirname(file_path) + base_name = os.path.basename(file_path) - family = "workfile" - # context.data["variant"] might come only from collect_batch_data - variant = context.data.get("variant") or self.default_variant - subset = get_subset_name( - family, - variant, - context.data["anatomyData"]["task"]["name"], - context.data["assetEntity"], - context.data["anatomyData"]["project"]["name"], - host_name=context.data["hostName"], - project_settings=context.data["project_settings"] - ) - - file_path = context.data["currentFile"] - staging_dir = os.path.dirname(file_path) - base_name = os.path.basename(file_path) - - # Create instance - if existing_instance is None: - instance = context.create_instance(subset) - instance.data.update({ - "subset": subset, - "label": base_name, - "name": base_name, - "family": family, - "families": [], - "representations": [], - "asset": os.environ["AVALON_ASSET"] - }) - else: - instance = existing_instance - - # creating representation - _, ext = os.path.splitext(file_path) - instance.data["representations"].append({ - "name": ext[1:], - "ext": ext[1:], - "files": base_name, - "stagingDir": staging_dir, - }) + # creating representation + _, ext = os.path.splitext(file_path) + instance.data["representations"].append({ + "name": ext[1:], + "ext": ext[1:], + "files": base_name, + "stagingDir": staging_dir, + }) + return diff --git a/openpype/hosts/photoshop/plugins/publish/extract_review.py b/openpype/hosts/photoshop/plugins/publish/extract_review.py index 9d7eff0211..d5416a389d 100644 --- a/openpype/hosts/photoshop/plugins/publish/extract_review.py +++ b/openpype/hosts/photoshop/plugins/publish/extract_review.py @@ -47,32 +47,42 @@ class ExtractReview(publish.Extractor): layers = self._get_layers_from_image_instances(instance) self.log.info("Layers image instance found: {}".format(layers)) + repre_name = "jpg" + repre_skeleton = { + "name": repre_name, + "ext": "jpg", + "stagingDir": staging_dir, + "tags": self.jpg_options['tags'], + } + + if instance.data["family"] != "review": + # enable creation of review, without this jpg review would clash + # with jpg of the image family + output_name = repre_name + repre_name = "{}_{}".format(repre_name, output_name) + repre_skeleton.update({"name": repre_name, + "outputName": output_name}) + if self.make_image_sequence and len(layers) > 1: self.log.info("Extract layers to image sequence.") img_list = self._save_sequence_images(staging_dir, layers) - instance.data["representations"].append({ - "name": "jpg", - "ext": "jpg", - "files": img_list, + repre_skeleton.update({ "frameStart": 0, "frameEnd": len(img_list), "fps": fps, - "stagingDir": staging_dir, - "tags": self.jpg_options['tags'], + "files": img_list, }) + instance.data["representations"].append(repre_skeleton) processed_img_names = img_list else: self.log.info("Extract layers to flatten image.") img_list = self._save_flatten_image(staging_dir, layers) - instance.data["representations"].append({ - "name": "jpg", - "ext": "jpg", - "files": img_list, # cannot be [] for single frame - "stagingDir": staging_dir, - "tags": self.jpg_options['tags'] + repre_skeleton.update({ + "files": img_list, }) + instance.data["representations"].append(repre_skeleton) processed_img_names = [img_list] ffmpeg_path = get_ffmpeg_tool_path("ffmpeg") diff --git a/openpype/settings/defaults/project_settings/photoshop.json b/openpype/settings/defaults/project_settings/photoshop.json index bcf21f55dd..2454691958 100644 --- a/openpype/settings/defaults/project_settings/photoshop.json +++ b/openpype/settings/defaults/project_settings/photoshop.json @@ -10,23 +10,40 @@ } }, "create": { - "CreateImage": { - "defaults": [ + "ImageCreator": { + "enabled": true, + "active_on_create": true, + "mark_for_review": false, + "default_variants": [ "Main" ] + }, + "AutoImageCreator": { + "enabled": false, + "active_on_create": true, + "mark_for_review": false, + "default_variant": "" + }, + "ReviewCreator": { + "enabled": true, + "active_on_create": true, + "default_variant": "" + }, + "WorkfileCreator": { + "enabled": true, + "active_on_create": true, + "default_variant": "Main" } }, "publish": { "CollectColorCodedInstances": { + "enabled": true, "create_flatten_image": "no", "flatten_subset_template": "", "color_code_mapping": [] }, - "CollectInstances": { - "flatten_subset_template": "" - }, "CollectReview": { - "publish": true + "enabled": true }, "CollectVersion": { "enabled": false diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json b/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json index 0071e632af..f6c46aba8b 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json @@ -31,16 +31,126 @@ { "type": "dict", "collapsible": true, - "key": "CreateImage", + "key": "ImageCreator", "label": "Create Image", + "checkbox_key": "enabled", "children": [ + { + "type": "label", + "label": "Manually create instance from layer or group of layers. \n Separate review could be created for this image to be sent to Asset Management System." + }, + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "boolean", + "key": "active_on_create", + "label": "Active by default" + }, + { + "type": "boolean", + "key": "mark_for_review", + "label": "Review by default" + }, { "type": "list", - "key": "defaults", - "label": "Default Subsets", + "key": "default_variants", + "label": "Default Variants", "object_type": "text" } ] + }, + { + "type": "dict", + "collapsible": true, + "key": "AutoImageCreator", + "label": "Create Flatten Image", + "checkbox_key": "enabled", + "children": [ + { + "type": "label", + "label": "Auto create image for all visible layers, used for simplified processing. \n Separate review could be created for this image to be sent to Asset Management System." + }, + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "boolean", + "key": "active_on_create", + "label": "Active by default" + }, + { + "type": "boolean", + "key": "mark_for_review", + "label": "Review by default" + }, + { + "type": "text", + "key": "default_variant", + "label": "Default variant" + } + ] + }, + { + "type": "dict", + "collapsible": true, + "key": "ReviewCreator", + "label": "Create Review", + "checkbox_key": "enabled", + "children": [ + { + "type": "label", + "label": "Auto create review instance containing all published image instances or visible layers if no image instance." + }, + { + "type": "boolean", + "key": "enabled", + "label": "Enabled", + "default": true + }, + { + "type": "boolean", + "key": "active_on_create", + "label": "Active by default" + }, + { + "type": "text", + "key": "default_variant", + "label": "Default variant" + } + ] + }, + { + "type": "dict", + "collapsible": true, + "key": "WorkfileCreator", + "label": "Create Workfile", + "checkbox_key": "enabled", + "children": [ + { + "type": "label", + "label": "Auto create workfile instance" + }, + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "boolean", + "key": "active_on_create", + "label": "Active by default" + }, + { + "type": "text", + "key": "default_variant", + "label": "Default variant" + } + ] } ] }, @@ -56,11 +166,18 @@ "is_group": true, "key": "CollectColorCodedInstances", "label": "Collect Color Coded Instances", + "checkbox_key": "enabled", "children": [ { "type": "label", "label": "Set color for publishable layers, set its resulting family and template for subset name. \nCan create flatten image from published instances.(Applicable only for remote publishing!)" }, + { + "type": "boolean", + "key": "enabled", + "label": "Enabled", + "default": true + }, { "key": "create_flatten_image", "label": "Create flatten image", @@ -131,40 +248,26 @@ } ] }, - { - "type": "dict", - "collapsible": true, - "key": "CollectInstances", - "label": "Collect Instances", - "children": [ - { - "type": "label", - "label": "Name for flatten image created if no image instance present" - }, - { - "type": "text", - "key": "flatten_subset_template", - "label": "Subset template for flatten image" - } - ] - }, { "type": "dict", "collapsible": true, "key": "CollectReview", "label": "Collect Review", + "checkbox_key": "enabled", "children": [ { "type": "boolean", - "key": "publish", - "label": "Active" - } - ] + "key": "enabled", + "label": "Enabled", + "default": true + } + ] }, { "type": "dict", "key": "CollectVersion", "label": "Collect Version", + "checkbox_key": "enabled", "children": [ { "type": "label", diff --git a/tests/integration/hosts/photoshop/test_publish_in_photoshop_auto_image.py b/tests/integration/hosts/photoshop/test_publish_in_photoshop_auto_image.py new file mode 100644 index 0000000000..1594b36dec --- /dev/null +++ b/tests/integration/hosts/photoshop/test_publish_in_photoshop_auto_image.py @@ -0,0 +1,93 @@ +import logging + +from tests.lib.assert_classes import DBAssert +from tests.integration.hosts.photoshop.lib import PhotoshopTestClass + +log = logging.getLogger("test_publish_in_photoshop") + + +class TestPublishInPhotoshopAutoImage(PhotoshopTestClass): + """Test for publish in Phohoshop with different review configuration. + + Workfile contains 3 layers, auto image and review instances created. + + Test contains updates to Settings!!! + + """ + PERSIST = True + + TEST_FILES = [ + ("1iLF6aNI31qlUCD1rGg9X9eMieZzxL-rc", + "test_photoshop_publish_auto_image.zip", "") + ] + + APP_GROUP = "photoshop" + # keep empty to locate latest installed variant or explicit + APP_VARIANT = "" + + APP_NAME = "{}/{}".format(APP_GROUP, APP_VARIANT) + + TIMEOUT = 120 # publish timeout + + def test_db_asserts(self, dbcon, publish_finished): + """Host and input data dependent expected results in DB.""" + print("test_db_asserts") + failures = [] + + failures.append(DBAssert.count_of_types(dbcon, "version", 3)) + + failures.append( + DBAssert.count_of_types(dbcon, "version", 0, name={"$ne": 1})) + + failures.append( + DBAssert.count_of_types(dbcon, "subset", 0, + name="imageMainForeground")) + + failures.append( + DBAssert.count_of_types(dbcon, "subset", 0, + name="imageMainBackground")) + + failures.append( + DBAssert.count_of_types(dbcon, "subset", 1, + name="workfileTest_task")) + + failures.append( + DBAssert.count_of_types(dbcon, "representation", 5)) + + additional_args = {"context.subset": "imageMainForeground", + "context.ext": "png"} + failures.append( + DBAssert.count_of_types(dbcon, "representation", 0, + additional_args=additional_args)) + + additional_args = {"context.subset": "imageMainBackground", + "context.ext": "png"} + failures.append( + DBAssert.count_of_types(dbcon, "representation", 0, + additional_args=additional_args)) + + # review from image + additional_args = {"context.subset": "imageBeautyMain", + "context.ext": "jpg", + "name": "jpg_jpg"} + failures.append( + DBAssert.count_of_types(dbcon, "representation", 1, + additional_args=additional_args)) + + additional_args = {"context.subset": "imageBeautyMain", + "context.ext": "jpg", + "name": "jpg"} + failures.append( + DBAssert.count_of_types(dbcon, "representation", 1, + additional_args=additional_args)) + + additional_args = {"context.subset": "review"} + failures.append( + DBAssert.count_of_types(dbcon, "representation", 1, + additional_args=additional_args)) + + assert not any(failures) + + +if __name__ == "__main__": + test_case = TestPublishInPhotoshopAutoImage() diff --git a/tests/integration/hosts/photoshop/test_publish_in_photoshop_review.py b/tests/integration/hosts/photoshop/test_publish_in_photoshop_review.py new file mode 100644 index 0000000000..64b6868d7c --- /dev/null +++ b/tests/integration/hosts/photoshop/test_publish_in_photoshop_review.py @@ -0,0 +1,111 @@ +import logging + +from tests.lib.assert_classes import DBAssert +from tests.integration.hosts.photoshop.lib import PhotoshopTestClass + +log = logging.getLogger("test_publish_in_photoshop") + + +class TestPublishInPhotoshopImageReviews(PhotoshopTestClass): + """Test for publish in Phohoshop with different review configuration. + + Workfile contains 2 image instance, one has review flag, second doesn't. + + Regular `review` family is disabled. + + Expected result is to `imageMainForeground` to have additional file with + review, `imageMainBackground` without. No separate `review` family. + + `test_project_test_asset_imageMainForeground_v001_jpg.jpg` is expected name + of imageForeground review, `_jpg` suffix is needed to differentiate between + image and review file. + + """ + PERSIST = True + + TEST_FILES = [ + ("12WGbNy9RJ3m9jlnk0Ib9-IZmONoxIz_p", + "test_photoshop_publish_review.zip", "") + ] + + APP_GROUP = "photoshop" + # keep empty to locate latest installed variant or explicit + APP_VARIANT = "" + + APP_NAME = "{}/{}".format(APP_GROUP, APP_VARIANT) + + TIMEOUT = 120 # publish timeout + + def test_db_asserts(self, dbcon, publish_finished): + """Host and input data dependent expected results in DB.""" + print("test_db_asserts") + failures = [] + + failures.append(DBAssert.count_of_types(dbcon, "version", 3)) + + failures.append( + DBAssert.count_of_types(dbcon, "version", 0, name={"$ne": 1})) + + failures.append( + DBAssert.count_of_types(dbcon, "subset", 1, + name="imageMainForeground")) + + failures.append( + DBAssert.count_of_types(dbcon, "subset", 1, + name="imageMainBackground")) + + failures.append( + DBAssert.count_of_types(dbcon, "subset", 1, + name="workfileTest_task")) + + failures.append( + DBAssert.count_of_types(dbcon, "representation", 6)) + + additional_args = {"context.subset": "imageMainForeground", + "context.ext": "png"} + failures.append( + DBAssert.count_of_types(dbcon, "representation", 1, + additional_args=additional_args)) + + additional_args = {"context.subset": "imageMainForeground", + "context.ext": "jpg"} + failures.append( + DBAssert.count_of_types(dbcon, "representation", 2, + additional_args=additional_args)) + + additional_args = {"context.subset": "imageMainForeground", + "context.ext": "jpg", + "context.representation": "jpg_jpg"} + failures.append( + DBAssert.count_of_types(dbcon, "representation", 1, + additional_args=additional_args)) + + additional_args = {"context.subset": "imageMainBackground", + "context.ext": "png"} + failures.append( + DBAssert.count_of_types(dbcon, "representation", 1, + additional_args=additional_args)) + + additional_args = {"context.subset": "imageMainBackground", + "context.ext": "jpg"} + failures.append( + DBAssert.count_of_types(dbcon, "representation", 1, + additional_args=additional_args)) + + additional_args = {"context.subset": "imageMainBackground", + "context.ext": "jpg", + "context.representation": "jpg_jpg"} + failures.append( + DBAssert.count_of_types(dbcon, "representation", 0, + additional_args=additional_args)) + + additional_args = {"context.subset": "review"} + failures.append( + DBAssert.count_of_types(dbcon, "representation", 0, + additional_args=additional_args)) + + assert not any(failures) + + +if __name__ == "__main__": + test_case = TestPublishInPhotoshopImageReviews() diff --git a/website/docs/admin_hosts_photoshop.md b/website/docs/admin_hosts_photoshop.md new file mode 100644 index 0000000000..de684f01d2 --- /dev/null +++ b/website/docs/admin_hosts_photoshop.md @@ -0,0 +1,127 @@ +--- +id: admin_hosts_photoshop +title: Photoshop Settings +sidebar_label: Photoshop +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +## Photoshop settings + +There is a couple of settings that could configure publishing process for **Photoshop**. +All of them are Project based, eg. each project could have different configuration. + +Location: Settings > Project > Photoshop + +![AfterEffects Project Settings](assets/admin_hosts_photoshop_settings.png) + +## Color Management (ImageIO) + +Placeholder for Color Management. Currently not implemented yet. + +## Creator plugins + +Contains configurable items for creators used during publishing from Photoshop. + +### Create Image + +Provides list of [variants](artist_concepts.md#variant) that will be shown to an artist in Publisher. Default value `Main`. + +### Create Flatten Image + +Provides simplified publishing process. It will create single `image` instance for artist automatically. This instance will +produce flatten image from all visible layers in a workfile. + +- Subset template for flatten image - provide template for subset name for this instance (example `imageBeauty`) +- Review - should be separate review created for this instance + +### Create Review + +Creates single `review` instance automatically. This allows artists to disable it if needed. + +### Create Workfile + +Creates single `workfile` instance automatically. This allows artists to disable it if needed. + +## Publish plugins + +Contains configurable items for publish plugins used during publishing from Photoshop. + +### Collect Color Coded Instances + +Used only in remote publishing! + +Allows to create automatically `image` instances for configurable highlight color set on layer or group in the workfile. + +#### Create flatten image + - Flatten with images - produce additional `image` with all published `image` instances merged + - Flatten only - produce only merged `image` instance + - No - produce only separate `image` instances + +#### Subset template for flatten image + +Template used to create subset name automatically (example `image{layer}Main` - uses layer name in subset name) + +### Collect Review + +Disable if no review should be created + +### Collect Version + +If enabled it will push version from workfile name to all published items. Eg. if artist is publishing `test_asset_workfile_v005.psd` +produced `image` and `review` files will contain `v005` (even if some previous version were skipped for particular family). + +### Validate Containers + +Checks if all imported assets to the workfile through `Loader` are in latest version. Limits cases that older version of asset would be used. + +If enabled, artist might still decide to disable validation for each publish (for special use cases). +Limit this optionality by toggling `Optional`. +`Active` toggle denotes that by default artists sees that optional validation as enabled. + +### Validate naming of subsets and layers + +Subset cannot contain invalid characters or extract to file would fail + +#### Regex pattern of invalid characters + +Contains weird characters like `/`, `/`, these might cause an issue when file (which contains subset name) is created on OS disk. + +#### Replacement character + +Replace all offending characters with this one. `_` is default. + +### Extract Image + +Controls extension formats of published instances of `image` family. `png` and `jpg` are by default. + +### Extract Review + +Controls output definitions of extracted reviews to upload on Asset Management (AM). + +#### Makes an image sequence instead of flatten image + +If multiple `image` instances are produced, glue created images into image sequence (`mov`) to review all of them separetely. +Without it only flatten image would be produced. + +#### Maximum size of sources for review + +Set Byte limit for review file. Applicable if gigantic `image` instances are produced, full image size is unnecessary to upload to AM. + +#### Extract jpg Options + +Handles tags for produced `.jpg` representation. `Create review` and `Add review to Ftrack` are defaults. + +#### Extract mov Options + +Handles tags for produced `.mov` representation. `Create review` and `Add review to Ftrack` are defaults. + + +### Workfile Builder + +Allows to open prepared workfile for an artist when no workfile exists. Useful to share standards, additional helpful content in the workfile. + +Could be configured per `Task type`, eg. `composition` task type could use different `.psd` template file than `art` task. +Workfile template must be accessible for all artists. +(Currently not handled by [SiteSync](module_site_sync.md)) \ No newline at end of file diff --git a/website/docs/assets/admin_hosts_photoshop_settings.png b/website/docs/assets/admin_hosts_photoshop_settings.png new file mode 100644 index 0000000000000000000000000000000000000000..aaa6ecbed7b353733f8424abe3c2df0cb903935a GIT binary patch literal 14364 zcmc(Gd03KZ+xOix*{-cZxwTBI=2DYeDDAfDIF(B(gr-)mq_~1qR+b{vG^XWNIa5t8 zxa5jRxj<&Zm?M*q5VGtz;|>2nDud<0r-i_$KjRWpII?KyX^(an5M(vKi`M$ zIlcz~Do6{bQFFlm=SLs%jR63|7X8mGM%2@@0N_w@c>kW02?!}S;6}(~exLfxvNX&u zndrT1zTbD%E2`*rT!?ae*U`Ykp3-w$N_(F=>@@sU6zKOD`n2c1f#*p0hkgUuuP#4c zK=w)B+owK%ue@S|U3wX}mOn4|xV5pB0eNntxoy2&{r1*dSN-#*@uE+OOedBsu{i0O z<_{6pz%`Z~!<;PCIraRE*YHJ&Q}6`&CmSySu-$1f^=i}Ht9Afz=Q$dRmvCs#ihDNL z#Q-q+TL~uB)&My3b7k-1W+-s!aQkV8c|icMaYGn)4Ph3L^XtELKs<~f^M-ivOABB) zpZwoAg=x6FsMU0-dVQQR)xNdx64@NnT#>C}?bMAv~_J#UN zuJ2k-_s%vd&gfu7tchPr$(`roOhR9?1755+TWS)wj4q? zUQAcabJ>OW3Fv{_m}fp3uNz?_6Nyr8!V{|CbZ1{NDWg9B&0d#S%YKK#?r=iKTx!g? z_v=|eT^Z<@d$Z$+YkJFLlqt%$+0HAA(%0C?C))8t>%@as#h&xujhaG4>f$@yk6hVe zf$NDY4Kq`oH`(2DKQ#supR@?dc0GBiTK1-bI)5af^ulc5n0IdjDimcJXVQim-+W+x z=f*XZ-6P*sLI_Gl^QY=%C2n#Iq`Ydg_B7^VKOxaGEnKbK?94V}N00;tmi3gJ6T1%9 z;_aqq?@_D<08Ynghmju%Eu)Vd&qg&R_bu$R-m4*PSyZ%r;cI!AFElF7UxA6RCkMPa z44n-O-US!LY)Pm!w)h=-+TvktP2ixEjqk1F#Y19Fn-+1b!Dk-1F^n5Mfr6!&27O56 zCC1X9MZuO2!d6#I)%m>4WV43ueT-;Hkmfbmy^dpnMn_J93;GV_|1PRrGPI*6OREu> zF>*x%5pCh8;@Z#xkE*(c5oT?M6GoFgR&b!o<97h)yY}sopA1GX23}Z$PwuK5u?!5t z+OG#s{R{ZC>$dqy3{Z=J7r*Jh)}P*Asw0ajsb>>G>-Py#ERl#9v{OwDVh-{CfsCCAf)5#l>z$#Z>~V z=A}~;G61%fjZLxP8v@I-l^2%Kk7m^+Ohqa`4!4MZ;4-;?rdZg>HgOEu?Y|=_K>4xW zVpLS~1XT?`grxmZ)IGPSt?0&8uM*P&p~jGV5XvuRhQzEQaX6~adoVxt9Ta~JEcxrK z_deTF-g2JIrx&;^7M@bToN+z@i&Kun%dFkqid&S_MD{Z~;D<`Y;^dX|T_nM>B>}Kj zRxC}XQxC-9`4|= zQ|CpJx#SIi&n@MJK8h_bz?8K<_WV3HE6`r^?#Q{kH+#3t7aUj78j(YvP|NEV7T*pi zfgyZ4CF{tD`6F$|TzU;eu$!WD-MHRN&(;NJDbi>AeU_i4zPDoIdS@O6$|rcHQ@4%P z4_jwOQnG>!W>JQ7j+PZ8c1jUZ+scSc#xRZ7_6e(#1hhzb?3D_uFYuFD0t$Np=>)Lb zTGCUVjcUB(I2wo=PVa|pFSFKIMg<&-csh#ie;W$r%;YYg!LD$C?1lswJ}kS2XO&aKS{nTtEHOa-yXonaX*>brKObH2gPyj z#FUxDv}4SAJo_U_cJ3J|zg{0@zxb>v-|FZ(mgV5`$>=v-t|p{2z=LhX*Z=R@kIH^Kbfw_sH#xGH&Ee zn$#HwotJ1Kgur0b?_{$e;~l{|-=7ZbK5%J+hP>z%CTImU`6NDRui06xU0TpPO(z}M zII2W74(AlNIF9m1j-%1jYZjSw`U?eb0vk?{`w*hlk?JO)BAmz(#t^(q56ymYGMKvB z(3Q~db+g*JVQOh|TJ`0e1BxnTxJ)u(^gZyxzqfb~)PtuLubVkC)2Nk9N6x9DZBJ&n z6oy6=if|F$wWY?hcW>2W-6k8a?QZyde4?Z0pp$b-uO*$OZgSN~=3Jtz|My_w*P`P; z!!7KN9SqakG^s(DH|wcbBF@@<&BTRHazo4y6q=}pT0I) zW03_VGsN}IGzXLT#v~Vu_%>`v1J3;9rflxCdA1~sft)9hvky@32OuP5KrO_cqPtSU z=@HE-?K#1bn?4xbDJM zUrWSTN+m4`aqzpDtAc#C7yzno>CEW;7f)K96Xz{Bwz+0QDk-yzL%q zH-%-`;+V^hp6Q^EAo)X$2I|dTYz_Vp03;B?* z(lxZ|L)BDgoHV7XoIt`!T!QjiFN`c_y|*VVA7?}ex5Fa)u~QAW*H9CeAQ(KgP%k`T znj|nag6~RHcrX-~w-V?;0=)OwfA`9GyPhu0=mm~~U#=fApmwI<+=#8u)>vZte z0?f4uV=f_ie05-b81qk3puXgN+3_D!MlV54^E4K}k@@eshEv5uU6h_XPfGQ*VmFqb z@09XWfD6t)-)s}dKKf#RZd6@k;pkkz_LOhT_0;)-e1?3u#n&H^wWcXyyl&?2P%?#@ zz9&fT@r5w5jh+=rh@i6%KxgN!q*F2k%d(|M(B{qR;tXe)Ec+H%6IR2?wPt)9hW*k2 zOHbU}jP4{|*Vfot?bFr-7cW-NBsG&R4I65R507fvl&!EU3!Gt4Psx3j@}tYDd06t1 zs}6g38B^PYi-56`vz@C9tqJkV7)rsSr5C~3!;>NRcaS8nMrbJ*#JcAh!Y3Q%QG(WQ zT)(?RRb~A%M`7Btwx1Bclu>AmITUFd$$D#R_-PQ5t$0wXwnVZ7b@82+Lg$! zU;qwA7Eb15#H@m{FerEK*5qVjglrD*V#hB#Lmi*mGHU`l=qOnFD{F)0#~1NTV6Sa! zQP52_3k8#jy|SvLvJ3`E*z_q6jOeTqre;f8r`q&5DX=IzkP42O}Og&+Skd7|60w1Q>j@142J zVWGsG8kn4E%R5^})tta@J>Vjnxu6cwo4Ttvm1DMLsec+;l{N>sq`kc}bP3(R8>|}4 z)s9gXcRqVvFP%3VNVX0G!>InT&suYrjp!}uvo2|~^&`KgTxpURGl>GlP zK;;V71n+(S=?j`|Q1cAO0^w`9Ry;JUM+u|iJF&Zf7b&mq?>Od65Xf%^TebyN@CO)M zPj?2boOIgd_QdE}hTGC#{w17TBs)u3^%zfpPq}q{%m)^emmeh@|6uj1?8p)%EXiNjIR6bNCe~cke;kSoxSLpeUtVJ* z2v*cj!W^Rg%YM%gbjN-3pfT}yUvVoIwGO*FSSR|h29TYGzF|0=zI|6w} zp@bEa!goL7!=n2nZ40=hrpriZCphaA)`pmDIUW3FMoDg9o|fG&`wCQ)G!s5}>w$ph zCEl5^&$zrl<=({j&5?=VK;4g5m-kOzPzHUs6s|`ujg{@L4PV5z1t4^p%h_#SH=mC7 z%`3lAQ>iUkNKwJuFxZ`ffL8gF17=7Q!mc}|P&hO*rfSk3U!c`^5i{#C6$9FrfoIP{ z)3(c?MGFU#Xfrul89!ZO`iA1#wD7$(fgJz=%ER<~V?lptLGD)t7j&e{zoOb4KhL3J z!)J((7*uu{Ec-<`{oY(Jg}?M%b$l^egn~I3Hg4EcJB+ou3=z7V#_&~|1;C(BZ{CMB&5rx)LdfJV{sneCM}pgjo;fEU&3~GlN}DP4TmC4F zA>{}?)5Dmd{gNvETL=Ilw);!Um@KsF>B!;M~aZ>x11TMzff*)V@ zkDS2Y->-d2zmJ|jtgT-6XQgDD?+H8`-gWpM5diO^=WZgm6}=x&kFot9+{+@btwQ0#Cd`N zZwX`TjH;Vo%D)9EbX^+tJR>xn?dmG;f=EKsa~Yc+PuTD(@<)vz)Mx{s;Exip9J%g? zp*7ra+souIerU)-SvCV>gJ9;kYB%`o??C%L@!wL(`~$MRH<&4cPa;oR=*A&$BVMF@ zvtM_4o}f_MjrTHbF$42spT^eU5>90?(+)q;hSv?VW*mCb;Hrhgt}pZ)@_T~@TcWPj zZZHA_-^d3Rba|oUb;?ob>=%m(EsitG8PN{&gMhlrRs;bYy(pdp8Z+@R{x<4>epJZ< zflj2;>P@WGn>cd{%E0SJ`C8-sVMb|Z+rb8kfnsQ;<4L_cu+<|`$nx1oKz95R`J0$L8cSl%KRlVEh z_8a;q6LVV!6&R_E(>vvv$lbh>{=&axh1&`S9xsMZlDl!2wA(XniKiIxyk|{nrmAZR zsz!#wLWBtctkHof&q=HAPC#28Y9^kzBFU?D_jF(O^{{Y5JHV%ya&JEr|0z?(E+)za zww1r!da4sQ`SuihTXJZ0a;KNIppGYzVQZ(3#7|%O(9}68sJ=)XwY%IeZ^3E0P9pbJ zMszy~XBlf^*%lAIQf3CGyRL0g;`&nD6%~{SXg!NQ&vMXyWN3Qshy1BVqyhnMd@sa zEGnfGh*lq4L*M{6eV%#~8Ebn;q_{$Rf~)W8 zas+QHrr3Pb9fc<}7~M>3Vz(r`wv%bsG>RQLUWQ4C5TLWa=T>OpYVQduE2b2P!6$~*GYaZa@}_a&uTr+oH5^@<8g`V$r(OTp zRQ8TW5}<#Pi4vw?Vv`u}$WeG&HWMY#^uS#?y@irrBtD5{UFa%pRXy;68#4y>DtdHb zyn*-VRL?_%<}+)ZyH{3j*i#w1b6Y=LWyY%d%nWc7pW)nhGE_w@QvIP6Rp6jD`6uGI z=y3G_U11X7mju_a%Rg%VteSR>w0GA^{-AEs7TTCEM>>ddkly%0t4PMufzjkrnMjP> zp_NJBJ9`y`VKehbPS1Kj>NI9cQhnCO0=+1kYLRrF%vuL1ER{Lb@&$;`5CCh^gW+i_CAL<WacQL*})n#TQ$aFB8GPL3gp%|nj;p4PtS@1!#odLI7pF7F zk1WvzDk~sxKiq_%u*`M*w#Rob^2fU%h%(oiKUiLcx-Z2&Ec_@)j>|At?QF-|vzvTy zP5b4HqbovnbHUB;aziBiF*OJwz2$41o1xHz$t-fOWm~`Q58y?Bu_Z&U@mFb(OXx3l zml!8wzq_<=JjOy3t9^RruTL92*J|L)0mtwEHt;B}YKpl_=EYCm#)*6S70Wd}@x;oA zU9tZJ=Jo~xsyw_56^4%DKWoa5BwK3^|1;UqNKLvSCLd(2ccvq-xJF$jA{uThiFCwMe{p{qlK#u@!71^GfFlPUpkGE$lKv2Iert+{xdD_E8q z(VxJkUFQz!X0#&`Rv1Ud`69_7aU;SQ0d;8|#DE|44scUt1le5h}Jxl(__MEqe%9df}>^U?GYC}ob8yeB|bPV zt|iEgu`*RNcpxB&Y|@T=*L&3pT zX86ts<72tkc*W`I$(9SLOgYQ>YvTGW3lgg_zjWyb)Ol8@64%wo|QdGE{4wzQ9r^$~|-`1@A zZtl|Cb6O{=p+z@GeUJ5sKJUeraSGae_|2*;L4=8qqzv3pafN{>e{OG_B3ylbF;VI^ zcnojeu0DU(G26s{pK9Q)8lU*5N>nXXN3>zis%?l;1ZPO|b}2Z|b`-4p`@_rU9FFc; z+SPW*_;$y-Qin+Rk!8LF<+gC76R+lS_2~U1xW!;bBxhP86*~Rs7-vgk{^GP)bgZH3h9 zLX5u?)~9MPmf=@Ft2VhVe=G5c*Lih*dnDjdNcz2W|6W#L>v#>-j`-?};w6;2+l0hj zn7#2z5@(8`WI3f*e&S?j)&`&H(PRf@r@W;HHexhXEnU*7g%UWC$X%?5s8`oN)Emo! zUEM>^Ly=%3BSDv@rbM9;92{RLLe>$xro~7`?o84#ut+uRk8R1h{liB`SkX#h1Xx2F z-m`tYfnbEBGz=nfJg|lQ{?`1U)6El0dRQye7Fl&KFQ7q81NVGU_%Z10=vPu7DudI6evY#*H4O z`B2B+TK2fP&M1a;V73kb$v^B+ccCSXOg&9~9$4)-FX+r4tm$}Uv8(!%x5Qy9IA#TF z1#NngT;jiFR9~Hc>r#V&@r~j5-vDy?I~$w?|7=FZ*K)<1@MC{55;OAlZVNA@j$Zo~ z9~U#)CT_9NO^SnTQtZtjfa*5ruC_V*8GlMzEPjF?XY?e?@a1a|ctt7y57+4mj}4x% z@t6&_kO{*JCli4)B+%oRQCbz}{1TE@h@31wzle0eG*mR}5^f#g12cg-#p|wp7WIS^ zx5>QFr{-svv8EJX^5{dw0zj1y-fYIC4thn1RHLtmvHVV#Tw7vjdeuCHFiM+m@!MLM zF$=Ud`x0&~;e+vhC{0Uu5Rs`WubTo6X&~T+LtpT$#$zLe*Tx=!Ji)9NO-ty}3yquW z5K<|5HG{2K`AJ4KCVxe9fdI@JU?uDA@fGqi={~S{YEpm&-yCuq(KK2^YJ1{w_N%e9 zu}33Vpxg$$U|m7yF$MNb=5xln;)`o3Js<5)hc4xea`s2T3E3piLP;l^BghTnrG%Ql z%jj}F+XlV#$1>0b@u>mch0!%}nzjBW3>j7#Ul(OV@XoX|lrC$xD-3zlN2ZAzU+tg+ z$Fjgp_!F6BR%Q%QD(FSg)(u+8(@1Zu25xax{@PBc>AX_6_+#4x#K#j`fSj$UMYZ5H zs%7PebwjaZb*-CR85;R=FSK^;F09of9dtiFxb7U-R5}=5bEWYtA*mUHLQJ&a)gR!*$ zrbTHDC?IJvVOOvPp_^#UQ0{0mF9hCOgQ)k9Qj7odAqMF^no^3)pL_#pG5}>(;Eglk zFMi$D45USD@LJYFFen~_>^aC3A&Joz%pO3B5K?dp-~s__2Wm&vkIaBVi; zUpJAg`{>pqp-q}79s4Ih-i`$FBOhBPvdFa4+Akdb;q`Sgo9Xv9-gpHAPn#On=o*q_ zS|j6D30`0^Q$E#fGxKwjImi$0>&-*GMVVSQ@(0$W@7s?yZVe(GHT839D%14z7yYgO z7zbR+1$zgk`=M^PKq_ZB$>Z!&m@&!2%5RH#hIXhTeFTg9O8Pl}=SMR43Vkx!v@2%% zK||71o%1gDnuw1GlQJAAMd{G=csA&20;jk0istTGZ3LY01R0_;&5ma|UN^zuTw)G# z78*H)$=Aq9Zxx++u6fr!oti(Cu`E@t$nY~16e%28k3` z;PUreL%*uAsT$3U%TGuR(3whFXZj~sE9&0tn@GWQT^Dvuwrqq^KEsG^i|>Eq_YOq< zo!`S6SCkejl1(yqu{)^{wsLE%X}8~}0B1xzR_1y0rCeHVRAW=fX%B9+FFdNi*in$u zzDH)j=XP*c{Gn;#g~yCv>ghbWlh;jcm7=Hemg(oA@^09(Kk-`ebPDF}ff)F@^(jXk z3$mo4*%8NTL{86USFMt*Bf__d9}J!uyw_$M>GiN9tZKsF1yQRTF`6S4CoUfr*z&?l zW-7cf`)e6S^hnlR;1~kjviHr7oTfK1zIMEI1RM9{J#4TK;ivV#@uq26bIU7#e^hD2 z&QWZ2@&j6PV`O93*2o=C(DRI1;)*K7h1p@dqVQIV!(<4M)A()QY9g{kUkGxr8DTVV z8gldI#xt_4CE<_I=UQytL84@8l|Us*)!IX&j9UU4Sp_>}Q3S91c&W$l;6P8-O zQTiN}?f83pXhexAU_JRcR$d#vA!bEd{#j-0gIenRmu+bK06RqF^yUK>x8)MpqcNUC z4Krxt-XfJ zZa~3&N^w8I*JHhC{gEqf%L(Z4+){H=OI`n_N8=?aCxbfcW(#bsLqt-q5QkM{3jG$9 zN3D73!<`H&RQV3gXw6DOW&O}>Dk!q+{~Jxxgsz)tO-hO$;L1j}dIc7ctL|JioN zKQ}~BvKBDW7l3650Q{>=KZ1j5>g)j^Zi~L}@>Mz1ef|pzf}twp^l4=e z(o!@b!T&MU^Mr=TpMLLfcWM+qkuO&3`r&0W@2To%g)<%SGW;|Z|D{N*+Vd0RHCO5+S;66a`TU?Al z4IYT%w!^l>mAt5kC2eaTIW8QkU~C|kn#!90vCEOXSp!SQt@ajyPW2$;8R6RvK;bKR zXj111=Dh`v`H=jk)fEQ#`)dmX3G=$0PnLZif4N3n088@8&lhS&-^f$N;$eY!#Ie7o zmul~v#whf!QP3LU{90EJ|L#=r)8vRSNs;RFw0wjY;D1+33?hP1*`Ej|mh#PD!_gLH zS{YH?x}(i^9;t0dn|2~VB-2i~4Nr!+TiEaM_CN40D<`86Id~E3ur`rvTi`Z^EeNo3 z%TukZqco*hMMdD=Su;by+7!qDS#YoyyB36w28RL6(Be34Ov!Rhs3`1B&~~E+d*P9j z=K1h&F9hWUtO-fJ(c87^$jQR-b+tVF^99@p_|}Tkq@EZ|N>mX#l>U-ax`-`&NnKJB zJS`VbV5fuGyNClHaJi+%tkx7k5FH80MOXsS<@!k>;G~=G9EjfNiVMUNPX+2mu+`P1 zRk3#LME`)rd+n~M^bmO#KD938=;!>e5IJ29LS$F$TyL5rW)%XZ+uR< zWX3q(pR$Ssi}h@%quMp``eZz-8cI$f(qosbJ2}{08#EQPDaJRs6nOCvq?kdN4&`Jg zT3gcnuF4PGr1neN1@pPj&NqB9Eq<n zS$}m)LU-64!0_jPft!AIi@7niOYar$L(d4SiaX{icQl78bEOBnAS@`O=2~c?XzeOZ zDAw*Fe;K&+H) z3veQ`*QYyZvt`Dv8-_d~_d534{aS9~Vr{2;!~0N^FxyWN9vPLljNF1>Qk487s;4eP zc$v!&HP2sYS!oHMAJ_r{HU7al2HzDl$zA|_?E-#maco@>1@l_UG9@SPL%_V`8r`@> zPgHq^N8t?%!M&jbY%?ukE>Nxm4;WbRfhzO=nGWn%A^U$!0V3mU;6;@l6#rS_t1mmh zO2#h!|B#H4V`&Y4Q}doJlB$cV+=^AQ5|em8in&dA(l!8yKl+b~-lpV;J|);p4anmi zRRx4_Y>6?hWo70rPl?7VDP(m7TE?o{IkgYAeAfS|>5T2+n?3gN6 zxCuSCVIi1AqI15fIYFI=H9@o%8_R2z_av6A-ei{u)`98lkmBE@uKLoDT3$eQCNZFR z9NGDv858;psu|;E}41kU?R}wpEHRDVU!} z1!tI5@N-raUL@FnR)8Ajz7AX7aZ{NsL6EcTQbsZJ=um~G;c6%qze@HIRLIGnn#Mbu z)}}PB_j*oYxE@I`BnQ>u9Y`CGIpn$dU?P;04%C{E!dT`}uo^mp*GopX6RM;f6PF_u z_S5OVThbM$dhaBi!ua}=nnw59F`ayzq$-K<=d+d#0&OzK%WY;t5(rCisP(-;(ygfo ze-mGvVI7y9SszUWvkWL+ERP5EB9wweD64h-(tsP0%Sc>Thk0*b7R{+rY!bOMcg8Cc z3FL?bY?a4yt)#gQqG)gYBAEm9S$y005v9Zr`)lCln+#qNaQySO(W;(w`7v2PC+V6! zhm4Cj1?8VL7lct%>2k5{D19{|3U2~#O%}K{xwbqi1~usCSysrA(&k5+(l_kpgeFU* zVSd{+(X!e^L*SCgMlaBkUFFOZY!88jpv^5xZ?1E$2UVz1?( z*emE-U9wfthpxH6nFZhh`xfT^UX}beWoXL|km>>Y;{U6gW5Pz%h+qI50nw|?zP>*2 zKO_A7XKkt6eUNNj(B$WmI92X^xHhelRn+$cjhT`3_-ZlnZ#D)E)@b~g!w8B_Ut)mG zigLU@5|``WSlKf*g$aHccX1VYWwu(RF^4}b8>)5#6$Yc#rKO~SA_b$f-omBC+PWi! zu{7&z(Gj(b9`L|0AS)(6d}RrTseipA9#ov7$ju4a=)nVHZRgdeZOk=Gu~X%Vr4}82 zeiqdkid7s?hLQD|I95>V&c84QUX$OCwkPcz34qG1h>UNA!rIy;C5#OQl&6bQPqFP+ zbmX{+jB@2-Ib)WSQ4ch}<}_@%CX;k{-SamayrM%H+X7}Wzr);-Gm1Z%n4TC7i?;y5 z=ehb>JTEIIlHpf}=Yn@q&ffu`c4_ZHYF|Dp4&6my`rOXU|x3qo&{;#12;L7 z^#kk<$Izn9;M)OaRaSfN)W=$~GDtyhS`t2^F%=!Qh@9?ozb8raC~~^m2zcNBZL2~# zWnt#Q5x1&e+Gl*U{}^IPk!n~;SnVG}6dS3jvQ z^Mr1?jvCAbQ__yRV9XcWG7+@f_R~XTt;AyPtm!)3$w!bo?dhRSfiOeLsIrnjnW_>| z(GdEu#T~bE_Sv6)u|*_oyO$hK^z*kfKOzKxF#;&SRBa?gxj_P{2^nlk*4t}`Bzws> zhd;5Pq?X9TyO~!Mt5L9=j#Tl-@f3_PYlNQLKVT=KD+=I2d6{h7*})O>@YVDxGDH1@ zQY#U{%=OFcNZM(d0A-~ Date: Tue, 2 May 2023 16:25:03 +0200 Subject: [PATCH 171/360] :art: soft-fail when pan/zoom locked on camera --- openpype/hosts/maya/plugins/publish/extract_playblast.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 825a8d38c7..3ceef6f3d3 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -217,7 +217,11 @@ class ExtractPlayblast(publish.Extractor): instance.data["panel"], edit=True, **viewport_defaults ) - cmds.setAttr("{}.panZoomEnabled".format(preset["camera"]), pan_zoom) + try: + cmds.setAttr( + "{}.panZoomEnabled".format(preset["camera"]), pan_zoom) + except RuntimeError: + self.log.warning("Cannot restore Pan/Zoom settings.") collected_files = os.listdir(stagingdir) patterns = [clique.PATTERNS["frames"]] From 0d2a0f87230a78fedbf3e68e54868c3a5269ff06 Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Tue, 2 May 2023 14:36:18 +0200 Subject: [PATCH 172/360] Feature: Remove and load inv action --- openpype/plugins/inventory/remove_and_load.py | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 openpype/plugins/inventory/remove_and_load.py diff --git a/openpype/plugins/inventory/remove_and_load.py b/openpype/plugins/inventory/remove_and_load.py new file mode 100644 index 0000000000..27ae1d4139 --- /dev/null +++ b/openpype/plugins/inventory/remove_and_load.py @@ -0,0 +1,44 @@ +from openpype.pipeline import InventoryAction +from openpype.pipeline.legacy_io import Session +from openpype.pipeline.load.plugins import discover_loader_plugins +from openpype.pipeline.load.utils import ( + get_loader_identifier, + remove_container, + load_container, +) +from openpype.client import get_representation_by_id + + +class RemoveAndLoad(InventoryAction): + """Delete inventory item and reload it.""" + + label = "Remove and load" + icon = "refresh" + + def process(self, containers): + for container in containers: + project_name = Session.get("AVALON_PROJECT") + + # Get loader + loader_name = container["loader"] + for plugin in discover_loader_plugins(project_name=project_name): + if get_loader_identifier(plugin) == loader_name: + loader = plugin + break + + assert ( + loader, + "Failed to get loader, can't remove and load container", + ) + + # Get representation + representation = get_representation_by_id( + project_name, container["representation"] + ) + assert representation, "Represenatation not found" + + # Remove container + remove_container(container) + + # Load container + load_container(loader, representation) From cffe72f0010f0128efd257bf2d6ec749f56958a8 Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Tue, 2 May 2023 16:46:01 +0200 Subject: [PATCH 173/360] register inventory actions --- openpype/pipeline/context_tools.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/pipeline/context_tools.py b/openpype/pipeline/context_tools.py index dede2b8fce..ada78b989d 100644 --- a/openpype/pipeline/context_tools.py +++ b/openpype/pipeline/context_tools.py @@ -35,6 +35,7 @@ from . import ( register_inventory_action_path, register_creator_plugin_path, deregister_loader_plugin_path, + deregister_inventory_action_path, ) @@ -54,6 +55,7 @@ PLUGINS_DIR = os.path.join(PACKAGE_DIR, "plugins") # Global plugin paths PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish") LOAD_PATH = os.path.join(PLUGINS_DIR, "load") +INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") def _get_modules_manager(): @@ -158,6 +160,7 @@ def install_openpype_plugins(project_name=None, host_name=None): pyblish.api.register_plugin_path(PUBLISH_PATH) pyblish.api.register_discovery_filter(filter_pyblish_plugins) register_loader_plugin_path(LOAD_PATH) + register_inventory_action_path(INVENTORY_PATH) if host_name is None: host_name = os.environ.get("AVALON_APP") @@ -223,6 +226,7 @@ def uninstall_host(): pyblish.api.deregister_plugin_path(PUBLISH_PATH) pyblish.api.deregister_discovery_filter(filter_pyblish_plugins) deregister_loader_plugin_path(LOAD_PATH) + deregister_inventory_action_path(INVENTORY_PATH) log.info("Global plug-ins unregistred") deregister_host() From dc34bcc776c14aba094d7d2bde0f58cbf53002d5 Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Tue, 2 May 2023 17:08:39 +0200 Subject: [PATCH 174/360] pre rebase From 865ba2c97539396ac4a871733d1088c2e25661bb Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Tue, 2 May 2023 17:12:36 +0200 Subject: [PATCH 175/360] edited assertion --- openpype/plugins/inventory/remove_and_load.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/plugins/inventory/remove_and_load.py b/openpype/plugins/inventory/remove_and_load.py index 27ae1d4139..981722c065 100644 --- a/openpype/plugins/inventory/remove_and_load.py +++ b/openpype/plugins/inventory/remove_and_load.py @@ -27,9 +27,8 @@ class RemoveAndLoad(InventoryAction): break assert ( - loader, - "Failed to get loader, can't remove and load container", - ) + loader + ), "Failed to get loader, can't remove and load container" # Get representation representation = get_representation_by_id( From fec104de8e085d0ce0d70e9679c98924338ab3ce Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 2 May 2023 18:49:02 +0200 Subject: [PATCH 176/360] Fix: Locally copied version of last published workfile is not incremented (#4722) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix: Locally copied version of last published workfile is not incremented * fix subset first match * correct anatomy name * Fix typo and linting * keep source filepath for further path conformation * fetch also input dependencies of workfile * required changes * lint * fix case only one subset * Enhancement: copy last workfile as reusable methods (#6) * Enhancement: copy last published workfile as reusable methods (WiP) * Added get_host_extensions method, added subset_id and las_version_doc access, added optional arguments to get_last_published_workfile * Plugged in the new methods + minor changes * Added docstrings, last workfile optional argument, and removed unused code * Using new implementation to get local workfile path. Warning: It adds an extra dot to the extension which I need to fix * Refactoring and fixed double dots * Added match subset_id and get representation method, plus clan up * Removed unused vars * Fixed some rebasing errors * delinted unchanged code and renamed get_representation into get_representation_with_task * This time it's really delinted, I hope... * Update openpype/modules/sync_server/sync_server.py reprenation isn't the right spelling (: Co-authored-by: Félix David * Changes based on reviews * Fixed non imperative docstring and missing space * Fixed another non imperative docstring * Update openpype/modules/sync_server/sync_server.py Fixed typo Co-authored-by: Félix David Co-authored-by: Hayley GUILLOT Co-authored-by: Félix David * Fix: syntax error * fix single subset case * Restore sync server enabled test in hook * Python2 syntax * renaming and missing key case handling * Fix local workfile overwritten on update in some cases (#7) * Fix: Local workfile overwrite when local version number is higher than published workfile version number (WiP) * Changed regex search, clean up * Readded mistakenly removed newline * lint * remove anticipated functions for cleaner PR * remove funcs from entities.py * change to get_last_workfile_with_version * clean * Update openpype/modules/sync_server/sync_server.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> * removed get_last_published_workfile_path * moved hook to sync server module * fix lint * Refactor - download only if not present * Refactor - change to list instead of set * Refactor - removing unnecessary code last_published_workfile_path must exists or we wouldn't get there. Use version only from that. * Refactor - removing unnecessary imports * Added check for max fail tries * Refactor - cleaned up how to get last workfile * Updated docstrings * Remove unused imports Co-authored-by: Félix David * OP-5466 - run this on more DCC * Updated documentation * Fix - handle hero versions Skip hero versions, look only for versioned published to get max version id. * Hound * Refactor - simplified download_last_published_workfile Logic should be in pre hook * Skip if no profile found * Removed unwanted import * Use collected project_doc Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> * Use cached project_settings Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --------- Co-authored-by: Félix David Co-authored-by: Sharkitty <81646000+Sharkitty@users.noreply.github.com> Co-authored-by: Hayley GUILLOT Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Co-authored-by: Jakub Ježek --- .../pre_copy_last_published_workfile.py | 151 +++++------ openpype/modules/sync_server/sync_server.py | 104 +++++++- .../modules/sync_server/sync_server_module.py | 35 ++- website/docs/module_site_sync.md | 237 ++++++++++++------ 4 files changed, 379 insertions(+), 148 deletions(-) rename openpype/{hooks => modules/sync_server/launch_hooks}/pre_copy_last_published_workfile.py (56%) diff --git a/openpype/hooks/pre_copy_last_published_workfile.py b/openpype/modules/sync_server/launch_hooks/pre_copy_last_published_workfile.py similarity index 56% rename from openpype/hooks/pre_copy_last_published_workfile.py rename to openpype/modules/sync_server/launch_hooks/pre_copy_last_published_workfile.py index 26b43c39cb..bbc220945c 100644 --- a/openpype/hooks/pre_copy_last_published_workfile.py +++ b/openpype/modules/sync_server/launch_hooks/pre_copy_last_published_workfile.py @@ -1,15 +1,20 @@ import os import shutil -from time import sleep + from openpype.client.entities import ( - get_last_version_by_subset_id, get_representations, - get_subsets, + get_project ) + from openpype.lib import PreLaunchHook -from openpype.lib.local_settings import get_local_site_id from openpype.lib.profiles_filtering import filter_profiles -from openpype.pipeline.load.utils import get_representation_path +from openpype.modules.sync_server.sync_server import ( + download_last_published_workfile, +) +from openpype.pipeline.template_data import get_template_data +from openpype.pipeline.workfile.path_resolving import ( + get_workfile_template_key, +) from openpype.settings.lib import get_project_settings @@ -22,7 +27,11 @@ class CopyLastPublishedWorkfile(PreLaunchHook): # Before `AddLastWorkfileToLaunchArgs` order = -1 - app_groups = ["blender", "photoshop", "tvpaint", "aftereffects"] + # any DCC could be used but TrayPublisher and other specials + app_groups = ["blender", "photoshop", "tvpaint", "aftereffects", + "nuke", "nukeassist", "nukex", "hiero", "nukestudio", + "maya", "harmony", "celaction", "flame", "fusion", + "houdini", "tvpaint"] def execute(self): """Check if local workfile doesn't exist, else copy it. @@ -31,11 +40,11 @@ class CopyLastPublishedWorkfile(PreLaunchHook): 2- Check if workfile in work area doesn't exist 3- Check if published workfile exists and is copied locally in publish 4- Substitute copied published workfile as first workfile + with incremented version by +1 Returns: None: This is a void method. """ - sync_server = self.modules_manager.get("sync_server") if not sync_server or not sync_server.enabled: self.log.debug("Sync server module is not enabled or available") @@ -53,6 +62,7 @@ class CopyLastPublishedWorkfile(PreLaunchHook): # Get data project_name = self.data["project_name"] + asset_name = self.data["asset_name"] task_name = self.data["task_name"] task_type = self.data["task_type"] host_name = self.application.host_name @@ -68,6 +78,8 @@ class CopyLastPublishedWorkfile(PreLaunchHook): "hosts": host_name, } last_workfile_settings = filter_profiles(profiles, filter_data) + if not last_workfile_settings: + return use_last_published_workfile = last_workfile_settings.get( "use_last_published_workfile" ) @@ -92,57 +104,27 @@ class CopyLastPublishedWorkfile(PreLaunchHook): ) return + max_retries = int((sync_server.sync_project_settings[project_name] + ["config"] + ["retry_cnt"])) + self.log.info("Trying to fetch last published workfile...") - project_doc = self.data.get("project_doc") asset_doc = self.data.get("asset_doc") anatomy = self.data.get("anatomy") - # Check it can proceed - if not project_doc and not asset_doc: - return + context_filters = { + "asset": asset_name, + "family": "workfile", + "task": {"name": task_name, "type": task_type} + } - # Get subset id - subset_id = next( - ( - subset["_id"] - for subset in get_subsets( - project_name, - asset_ids=[asset_doc["_id"]], - fields=["_id", "data.family", "data.families"], - ) - if subset["data"].get("family") == "workfile" - # Legacy compatibility - or "workfile" in subset["data"].get("families", {}) - ), - None, - ) - if not subset_id: - self.log.debug( - 'No any workfile for asset "{}".'.format(asset_doc["name"]) - ) - return + workfile_representations = list(get_representations( + project_name, + context_filters=context_filters + )) - # Get workfile representation - last_version_doc = get_last_version_by_subset_id( - project_name, subset_id, fields=["_id"] - ) - if not last_version_doc: - self.log.debug("Subset does not have any versions") - return - - workfile_representation = next( - ( - representation - for representation in get_representations( - project_name, version_ids=[last_version_doc["_id"]] - ) - if representation["context"]["task"]["name"] == task_name - ), - None, - ) - - if not workfile_representation: + if not workfile_representations: self.log.debug( 'No published workfile for task "{}" and host "{}".'.format( task_name, host_name @@ -150,28 +132,55 @@ class CopyLastPublishedWorkfile(PreLaunchHook): ) return - local_site_id = get_local_site_id() - sync_server.add_site( - project_name, - workfile_representation["_id"], - local_site_id, - force=True, - priority=99, - reset_timer=True, + filtered_repres = filter( + lambda r: r["context"].get("version") is not None, + workfile_representations ) - - while not sync_server.is_representation_on_site( - project_name, workfile_representation["_id"], local_site_id - ): - sleep(5) - - # Get paths - published_workfile_path = get_representation_path( - workfile_representation, root=anatomy.roots + workfile_representation = max( + filtered_repres, key=lambda r: r["context"]["version"] ) - local_workfile_dir = os.path.dirname(last_workfile) # Copy file and substitute path - self.data["last_workfile_path"] = shutil.copy( - published_workfile_path, local_workfile_dir + last_published_workfile_path = download_last_published_workfile( + host_name, + project_name, + task_name, + workfile_representation, + max_retries, + anatomy=anatomy ) + if not last_published_workfile_path: + self.log.debug( + "Couldn't download {}".format(last_published_workfile_path) + ) + return + + project_doc = self.data["project_doc"] + + project_settings = self.data["project_settings"] + template_key = get_workfile_template_key( + task_name, host_name, project_name, project_settings + ) + + # Get workfile data + workfile_data = get_template_data( + project_doc, asset_doc, task_name, host_name + ) + + extension = last_published_workfile_path.split(".")[-1] + workfile_data["version"] = ( + workfile_representation["context"]["version"] + 1) + workfile_data["ext"] = extension + + anatomy_result = anatomy.format(workfile_data) + local_workfile_path = anatomy_result[template_key]["path"] + + # Copy last published workfile to local workfile directory + shutil.copy( + last_published_workfile_path, + local_workfile_path, + ) + + self.data["last_workfile_path"] = local_workfile_path + # Keep source filepath for further path conformation + self.data["source_filepath"] = last_published_workfile_path diff --git a/openpype/modules/sync_server/sync_server.py b/openpype/modules/sync_server/sync_server.py index 5b873a37cf..d1d5c2863d 100644 --- a/openpype/modules/sync_server/sync_server.py +++ b/openpype/modules/sync_server/sync_server.py @@ -3,10 +3,15 @@ import os import asyncio import threading import concurrent.futures -from concurrent.futures._base import CancelledError +from time import sleep from .providers import lib +from openpype.client.entity_links import get_linked_representation_id from openpype.lib import Logger +from openpype.lib.local_settings import get_local_site_id +from openpype.modules.base import ModulesManager +from openpype.pipeline import Anatomy +from openpype.pipeline.load.utils import get_representation_path_with_anatomy from .utils import SyncStatus, ResumableError @@ -189,6 +194,98 @@ def _site_is_working(module, project_name, site_name, site_config): return handler.is_active() +def download_last_published_workfile( + host_name: str, + project_name: str, + task_name: str, + workfile_representation: dict, + max_retries: int, + anatomy: Anatomy = None, +) -> str: + """Download the last published workfile + + Args: + host_name (str): Host name. + project_name (str): Project name. + task_name (str): Task name. + workfile_representation (dict): Workfile representation. + max_retries (int): complete file failure only after so many attempts + anatomy (Anatomy, optional): Anatomy (Used for optimization). + Defaults to None. + + Returns: + str: last published workfile path localized + """ + + if not anatomy: + anatomy = Anatomy(project_name) + + # Get sync server module + sync_server = ModulesManager().modules_by_name.get("sync_server") + if not sync_server or not sync_server.enabled: + print("Sync server module is disabled or unavailable.") + return + + if not workfile_representation: + print( + "Not published workfile for task '{}' and host '{}'.".format( + task_name, host_name + ) + ) + return + + last_published_workfile_path = get_representation_path_with_anatomy( + workfile_representation, anatomy + ) + if (not last_published_workfile_path or + not os.path.exists(last_published_workfile_path)): + return + + # If representation isn't available on remote site, then return. + if not sync_server.is_representation_on_site( + project_name, + workfile_representation["_id"], + sync_server.get_remote_site(project_name), + ): + print( + "Representation for task '{}' and host '{}'".format( + task_name, host_name + ) + ) + return + + # Get local site + local_site_id = get_local_site_id() + + # Add workfile representation to local site + representation_ids = {workfile_representation["_id"]} + representation_ids.update( + get_linked_representation_id( + project_name, repre_id=workfile_representation["_id"] + ) + ) + for repre_id in representation_ids: + if not sync_server.is_representation_on_site(project_name, repre_id, + local_site_id): + sync_server.add_site( + project_name, + repre_id, + local_site_id, + force=True, + priority=99 + ) + sync_server.reset_timer() + print("Starting to download:{}".format(last_published_workfile_path)) + # While representation unavailable locally, wait. + while not sync_server.is_representation_on_site( + project_name, workfile_representation["_id"], local_site_id, + max_retries=max_retries + ): + sleep(5) + + return last_published_workfile_path + + class SyncServerThread(threading.Thread): """ Separate thread running synchronization server with asyncio loop. @@ -358,7 +455,6 @@ class SyncServerThread(threading.Thread): duration = time.time() - start_time self.log.debug("One loop took {:.2f}s".format(duration)) - delay = self.module.get_loop_delay(project_name) self.log.debug( "Waiting for {} seconds to new loop".format(delay) @@ -370,8 +466,8 @@ class SyncServerThread(threading.Thread): self.log.warning( "ConnectionResetError in sync loop, trying next loop", exc_info=True) - except CancelledError: - # just stopping server + except asyncio.exceptions.CancelledError: + # cancelling timer pass except ResumableError: self.log.warning( diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py index 5a4fa07e98..b85b045bd9 100644 --- a/openpype/modules/sync_server/sync_server_module.py +++ b/openpype/modules/sync_server/sync_server_module.py @@ -838,6 +838,18 @@ class SyncServerModule(OpenPypeModule, ITrayModule): return ret_dict + def get_launch_hook_paths(self): + """Implementation for applications launch hooks. + + Returns: + (str): full absolut path to directory with hooks for the module + """ + + return os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "launch_hooks" + ) + # Needs to be refactored after Settings are updated # # Methods for Settings to get appriate values to fill forms # def get_configurable_items(self, scope=None): @@ -1045,9 +1057,23 @@ class SyncServerModule(OpenPypeModule, ITrayModule): self.sync_server_thread.reset_timer() def is_representation_on_site( - self, project_name, representation_id, site_name + self, project_name, representation_id, site_name, max_retries=None ): - """Checks if 'representation_id' has all files avail. on 'site_name'""" + """Checks if 'representation_id' has all files avail. on 'site_name' + + Args: + project_name (str) + representation_id (str) + site_name (str) + max_retries (int) (optional) - provide only if method used in while + loop to bail out + Returns: + (bool): True if 'representation_id' has all files correctly on the + 'site_name' + Raises: + (ValueError) Only If 'max_retries' provided if upload/download + failed too many times to limit infinite loop check. + """ representation = get_representation_by_id(project_name, representation_id, fields=["_id", "files"]) @@ -1060,6 +1086,11 @@ class SyncServerModule(OpenPypeModule, ITrayModule): if site["name"] != site_name: continue + if max_retries: + tries = self._get_tries_count_from_rec(site) + if tries >= max_retries: + raise ValueError("Failed too many times") + if (site.get("progress") or site.get("error") or not site.get("created_dt")): return False diff --git a/website/docs/module_site_sync.md b/website/docs/module_site_sync.md index 3e5794579c..68f56cb548 100644 --- a/website/docs/module_site_sync.md +++ b/website/docs/module_site_sync.md @@ -7,80 +7,112 @@ sidebar_label: Site Sync import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; +Site Sync allows users and studios to synchronize published assets between +multiple 'sites'. Site denotes a storage location, +which could be a physical disk, server, cloud storage. To be able to use site +sync, it first needs to be configured. -:::warning -**This feature is** currently **in a beta stage** and it is not recommended to rely on it fully for production. -::: - -Site Sync allows users and studios to synchronize published assets between multiple 'sites'. Site denotes a storage location, -which could be a physical disk, server, cloud storage. To be able to use site sync, it first needs to be configured. - -The general idea is that each user acts as an individual site and can download and upload any published project files when they are needed. that way, artist can have access to the whole project, but only every store files that are relevant to them on their home workstation. +The general idea is that each user acts as an individual site and can download +and upload any published project files when they are needed. that way, artist +can have access to the whole project, but only every store files that are +relevant to them on their home workstation. :::note -At the moment site sync is only able to deal with publishes files. No workfiles will be synchronized unless they are published. We are working on making workfile synchronization possible as well. +At the moment site sync is only able to deal with publishes files. No workfiles +will be synchronized unless they are published. We are working on making +workfile synchronization possible as well. ::: ## System Settings -To use synchronization, *Site Sync* needs to be enabled globally in **OpenPype Settings/System/Modules/Site Sync**. +To use synchronization, *Site Sync* needs to be enabled globally in **OpenPype +Settings/System/Modules/Site Sync**. ![Configure module](assets/site_sync_system.png) -### Sites +### Sites By default there are two sites created for each OpenPype installation: -- **studio** - default site - usually a centralized mounted disk accessible to all artists. Studio site is used if Site Sync is disabled. -- **local** - each workstation or server running OpenPype Tray receives its own with unique site name. Workstation refers to itself as "local"however all other sites will see it under it's unique ID. -Artists can explore their site ID by opening OpenPype Info tool by clicking on a version number in the tray app. +- **studio** - default site - usually a centralized mounted disk accessible to + all artists. Studio site is used if Site Sync is disabled. +- **local** - each workstation or server running OpenPype Tray receives its own + with unique site name. Workstation refers to itself as "local"however all + other sites will see it under it's unique ID. -Many different sites can be created and configured on the system level, and some or all can be assigned to each project. +Artists can explore their site ID by opening OpenPype Info tool by clicking on +a version number in the tray app. -Each OpenPype Tray app works with two sites at one time. (Sites can be the same, and no syncing is done in this setup). +Many different sites can be created and configured on the system level, and +some or all can be assigned to each project. -Sites could be configured differently per project basis. +Each OpenPype Tray app works with two sites at one time. (Sites can be the +same, and no syncing is done in this setup). -Each new site needs to be created first in `System Settings`. Most important feature of site is its Provider, select one from already prepared Providers. +Sites could be configured differently per project basis. -#### Alternative sites +Each new site needs to be created first in `System Settings`. Most important +feature of site is its Provider, select one from already prepared Providers. + +#### Alternative sites This attribute is meant for special use cases only. -One of the use cases is sftp site vendoring (exposing) same data as regular site (studio). Each site is accessible for different audience. 'studio' for artists in a studio via shared disk, 'sftp' for externals via sftp server with mounted 'studio' drive. +One of the use cases is sftp site vendoring (exposing) same data as regular +site (studio). Each site is accessible for different audience. 'studio' for +artists in a studio via shared disk, 'sftp' for externals via sftp server with +mounted 'studio' drive. -Change of file status on one site actually means same change on 'alternate' site occurred too. (eg. artists publish to 'studio', 'sftp' is using -same location >> file is accessible on 'sftp' site right away, no need to sync it anyhow.) +Change of file status on one site actually means same change on 'alternate' +site occurred too. (eg. artists publish to 'studio', 'sftp' is using +same location >> file is accessible on 'sftp' site right away, no need to sync +it anyhow.) ##### Example + ![Configure module](assets/site_sync_system_sites.png) -Admin created new `sftp` site which is handled by `SFTP` provider. Somewhere in the studio SFTP server is deployed on a machine that has access to `studio` drive. +Admin created new `sftp` site which is handled by `SFTP` provider. Somewhere in +the studio SFTP server is deployed on a machine that has access to `studio` +drive. Alternative sites work both way: + - everything published to `studio` is accessible on a `sftp` site too -- everything published to `sftp` (most probably via artist's local disk - artists publishes locally, representation is marked to be synced to `sftp`. Immediately after it is synced, it is marked to be available on `studio` too for artists in the studio to use.) +- everything published to `sftp` (most probably via artist's local disk - + artists publishes locally, representation is marked to be synced to `sftp`. + Immediately after it is synced, it is marked to be available on `studio` too + for artists in the studio to use.) ## Project Settings -Sites need to be made available for each project. Of course this is possible to do on the default project as well, in which case all other projects will inherit these settings until overridden explicitly. +Sites need to be made available for each project. Of course this is possible to +do on the default project as well, in which case all other projects will +inherit these settings until overridden explicitly. You'll find the setting in **Settings/Project/Global/Site Sync** -The attributes that can be configured will vary between sites and their providers. +The attributes that can be configured will vary between sites and their +providers. ## Local settings -Each user should configure root folder for their 'local' site via **Local Settings** in OpenPype Tray. This folder will be used for all files that the user publishes or downloads while working on a project. Artist has the option to set the folder as "default"in which case it is used for all the projects, or it can be set on a project level individually. +Each user should configure root folder for their 'local' site via **Local +Settings** in OpenPype Tray. This folder will be used for all files that the +user publishes or downloads while working on a project. Artist has the option +to set the folder as "default"in which case it is used for all the projects, or +it can be set on a project level individually. -Artists can also override which site they use as active and remote if need be. +Artists can also override which site they use as active and remote if need be. ![Local overrides](assets/site_sync_local_setting.png) - ## Providers -Each site implements a so called `provider` which handles most common operations (list files, copy files etc.) and provides interface with a particular type of storage. (disk, gdrive, aws, etc.) -Multiple configured sites could share the same provider with different settings (multiple mounted disks - each disk can be a separate site, while +Each site implements a so called `provider` which handles most common +operations (list files, copy files etc.) and provides interface with a +particular type of storage. (disk, gdrive, aws, etc.) +Multiple configured sites could share the same provider with different +settings (multiple mounted disks - each disk can be a separate site, while all share the same provider). **Currently implemented providers:** @@ -89,21 +121,30 @@ all share the same provider). Handles files stored on disk storage. -Local drive provider is the most basic one that is used for accessing all standard hard disk storage scenarios. It will work with any storage that can be mounted on your system in a standard way. This could correspond to a physical external hard drive, network mounted storage, internal drive or even VPN connected network drive. It doesn't care about how the drive is mounted, but you must be able to point to it with a simple directory path. +Local drive provider is the most basic one that is used for accessing all +standard hard disk storage scenarios. It will work with any storage that can be +mounted on your system in a standard way. This could correspond to a physical +external hard drive, network mounted storage, internal drive or even VPN +connected network drive. It doesn't care about how the drive is mounted, but +you must be able to point to it with a simple directory path. Default sites `local` and `studio` both use local drive provider. - ### Google Drive -Handles files on Google Drive (this). GDrive is provided as a production example for implementing other cloud providers +Handles files on Google Drive (this). GDrive is provided as a production +example for implementing other cloud providers -Let's imagine a small globally distributed studio which wants all published work for all their freelancers uploaded to Google Drive folder. +Let's imagine a small globally distributed studio which wants all published +work for all their freelancers uploaded to Google Drive folder. For this use case admin needs to configure: -- how many times it tries to synchronize file in case of some issue (network, permissions) + +- how many times it tries to synchronize file in case of some issue (network, + permissions) - how often should synchronization check for new assets -- sites for synchronization - 'local' and 'gdrive' (this can be overridden in local settings) +- sites for synchronization - 'local' and 'gdrive' (this can be overridden in + local settings) - user credentials - root folder location on Google Drive side @@ -111,30 +152,43 @@ Configuration would look like this: ![Configure project](assets/site_sync_project_settings.png) -*Site Sync* for Google Drive works using its API: https://developers.google.com/drive/api/v3/about-sdk +*Site Sync* for Google Drive works using its +API: https://developers.google.com/drive/api/v3/about-sdk -To configure Google Drive side you would need to have access to Google Cloud Platform project: https://console.cloud.google.com/ +To configure Google Drive side you would need to have access to Google Cloud +Platform project: https://console.cloud.google.com/ To get working connection to Google Drive there are some necessary steps: -- first you need to enable GDrive API: https://developers.google.com/drive/api/v3/enable-drive-api -- next you need to create user, choose **Service Account** (for basic configuration no roles for account are necessary) + +- first you need to enable GDrive + API: https://developers.google.com/drive/api/v3/enable-drive-api +- next you need to create user, choose **Service Account** (for basic + configuration no roles for account are necessary) - add new key for created account and download .json file with credentials -- share destination folder on the Google Drive with created account (directly in GDrive web application) -- add new site back in OpenPype Settings, name as you want, provider needs to be 'gdrive' +- share destination folder on the Google Drive with created account (directly + in GDrive web application) +- add new site back in OpenPype Settings, name as you want, provider needs to + be 'gdrive' - distribute credentials file via shared mounted disk location :::note -If you are using regular personal GDrive for testing don't forget adding `/My Drive` as the prefix in root configuration. Business accounts and share drives don't need this. +If you are using regular personal GDrive for testing don't forget +adding `/My Drive` as the prefix in root configuration. Business accounts and +share drives don't need this. ::: ### SFTP -SFTP provider is used to connect to SFTP server. Currently authentication with `user:password` or `user:ssh key` is implemented. -Please provide only one combination, don't forget to provide password for ssh key if ssh key was created with a passphrase. +SFTP provider is used to connect to SFTP server. Currently authentication +with `user:password` or `user:ssh key` is implemented. +Please provide only one combination, don't forget to provide password for ssh +key if ssh key was created with a passphrase. -(SFTP connection could be a bit finicky, use FileZilla or WinSCP for testing connection, it will be mush faster.) +(SFTP connection could be a bit finicky, use FileZilla or WinSCP for testing +connection, it will be mush faster.) -Beware that ssh key expects OpenSSH format (`.pem`) not a Putty format (`.ppk`)! +Beware that ssh key expects OpenSSH format (`.pem`) not a Putty +format (`.ppk`)! #### How to set SFTP site @@ -143,60 +197,101 @@ Beware that ssh key expects OpenSSH format (`.pem`) not a Putty format (`.ppk`)! ![Enable syncing and create site](assets/site_sync_sftp_system.png) -- In Projects setting enable Site Sync (on default project - all project will be synched, or on specific project) -- Configure SFTP connection and destination folder on a SFTP server (in screenshot `/upload`) +- In Projects setting enable Site Sync (on default project - all project will + be synched, or on specific project) +- Configure SFTP connection and destination folder on a SFTP server (in + screenshot `/upload`) ![SFTP connection](assets/site_sync_project_sftp_settings.png) - -- if you want to force syncing between local and sftp site for all users, use combination `active site: local`, `remote site: NAME_OF_SFTP_SITE` -- if you want to allow only specific users to use SFTP syncing (external users, not located in the office), use `active site: studio`, `remote site: studio`. + +- if you want to force syncing between local and sftp site for all users, use + combination `active site: local`, `remote site: NAME_OF_SFTP_SITE` +- if you want to allow only specific users to use SFTP syncing (external users, + not located in the office), use `active site: studio`, `remote site: studio`. ![Select active and remote site on a project](assets/site_sync_sftp_project_setting_not_forced.png) -- Each artist can decide and configure syncing from his/her local to SFTP via `Local Settings` +- Each artist can decide and configure syncing from his/her local to SFTP + via `Local Settings` ![Select active and remote site on a project](assets/site_sync_sftp_settings_local.png) - + ### Custom providers -If a studio needs to use other services for cloud storage, or want to implement totally different storage providers, they can do so by writing their own provider plugin. We're working on a developer documentation, however, for now we recommend looking at `abstract_provider.py`and `gdrive.py` inside `openpype/modules/sync_server/providers` and using it as a template. +If a studio needs to use other services for cloud storage, or want to implement +totally different storage providers, they can do so by writing their own +provider plugin. We're working on a developer documentation, however, for now +we recommend looking at `abstract_provider.py`and `gdrive.py` +inside `openpype/modules/sync_server/providers` and using it as a template. ### Running Site Sync in background -Site Sync server synchronizes new published files from artist machine into configured remote location by default. +Site Sync server synchronizes new published files from artist machine into +configured remote location by default. -There might be a use case where you need to synchronize between "non-artist" sites, for example between studio site and cloud. In this case -you need to run Site Sync as a background process from a command line (via service etc) 24/7. +There might be a use case where you need to synchronize between "non-artist" +sites, for example between studio site and cloud. In this case +you need to run Site Sync as a background process from a command line (via +service etc) 24/7. -To configure all sites where all published files should be synced eventually you need to configure `project_settings/global/sync_server/config/always_accessible_on` property in Settings (per project) first. +To configure all sites where all published files should be synced eventually +you need to +configure `project_settings/global/sync_server/config/always_accessible_on` +property in Settings (per project) first. ![Set another non artist remote site](assets/site_sync_always_on.png) This is an example of: + - Site Sync is enabled for a project -- default active and remote sites are set to `studio` - eg. standard process: everyone is working in a studio, publishing to shared location etc. -- (but this also allows any of the artists to work remotely, they would change their active site in their own Local Settings to `local` and configure local root. - This would result in everything artist publishes is saved first onto his local folder AND synchronized to `studio` site eventually.) +- default active and remote sites are set to `studio` - eg. standard process: + everyone is working in a studio, publishing to shared location etc. +- (but this also allows any of the artists to work remotely, they would change + their active site in their own Local Settings to `local` and configure local + root. + This would result in everything artist publishes is saved first onto his + local folder AND synchronized to `studio` site eventually.) - everything exported must also be eventually uploaded to `sftp` site -This eventual synchronization between `studio` and `sftp` sites must be physically handled by background process. +This eventual synchronization between `studio` and `sftp` sites must be +physically handled by background process. -As current implementation relies heavily on Settings and Local Settings, background process for a specific site ('studio' for example) must be configured via Tray first to `syncserver` command to work. +As current implementation relies heavily on Settings and Local Settings, +background process for a specific site ('studio' for example) must be +configured via Tray first to `syncserver` command to work. To do this: -- run OP `Tray` with environment variable OPENPYPE_LOCAL_ID set to name of active (source) site. In most use cases it would be studio (for cases of backups of everything published to studio site to different cloud site etc.) +- run OP `Tray` with environment variable OPENPYPE_LOCAL_ID set to name of + active (source) site. In most use cases it would be studio (for cases of + backups of everything published to studio site to different cloud site etc.) - start `Tray` -- check `Local ID` in information dialog after clicking on version number in the Tray +- check `Local ID` in information dialog after clicking on version number in + the Tray - open `Local Settings` in the `Tray` - configure for each project necessary active site and remote site - close `Tray` - run OP from a command line with `syncserver` and `--active_site` arguments - -This is an example how to trigger background syncing process where active (source) site is `studio`. -(It is expected that OP is installed on a machine, `openpype_console` is on PATH. If not, add full path to executable. +This is an example how to trigger background syncing process where active ( +source) site is `studio`. +(It is expected that OP is installed on a machine, `openpype_console` is on +PATH. If not, add full path to executable. ) + ```shell openpype_console syncserver --active_site studio -``` \ No newline at end of file +``` + +### Syncing of last published workfile + +Some DCC might have enabled +in `project_setting/global/tools/Workfiles/last_workfile_on_startup`, eg. open +DCC with last opened workfile. + +Flag `use_last_published_workfile` tells that last published workfile should be +used if no workfile is present locally. +This use case could happen if artists starts working on new task locally, +doesn't have any workfile present. In that case last published will be +synchronized locally and its version bumped by 1 (as workfile's version is +always +1 from published version). \ No newline at end of file From c542934da45f6dc50bb8ceabfb23f4ff822f016b Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 3 May 2023 03:25:25 +0000 Subject: [PATCH 177/360] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index 72297a4430..9832ff4747 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.6-nightly.2" +__version__ = "3.15.6-nightly.3" From 7c2a1542145ba75808d7927507bfda45f5166810 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 3 May 2023 15:32:22 +0800 Subject: [PATCH 178/360] add settings to switch on/off the frame range validator --- .../plugins/publish/validate_frame_range.py | 2 +- .../publish/validate_frame_range_type.py | 33 ------------------- .../defaults/project_settings/max.json | 7 ++++ .../projects_schema/schema_project_max.json | 4 +++ .../schemas/schema_max_publish.json | 33 +++++++++++++++++++ 5 files changed, 45 insertions(+), 34 deletions(-) delete mode 100644 openpype/hosts/max/plugins/publish/validate_frame_range_type.py create mode 100644 openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json diff --git a/openpype/hosts/max/plugins/publish/validate_frame_range.py b/openpype/hosts/max/plugins/publish/validate_frame_range.py index dc12eece39..e07c6390c1 100644 --- a/openpype/hosts/max/plugins/publish/validate_frame_range.py +++ b/openpype/hosts/max/plugins/publish/validate_frame_range.py @@ -29,7 +29,7 @@ class ValidateFrameRange(pyblish.api.InstancePlugin, order = ValidateContentsOrder families = ["maxrender"] hosts = ["max"] - optional = False + optional = True actions = [RepairAction] def process(self, instance): diff --git a/openpype/hosts/max/plugins/publish/validate_frame_range_type.py b/openpype/hosts/max/plugins/publish/validate_frame_range_type.py deleted file mode 100644 index d77b1503e0..0000000000 --- a/openpype/hosts/max/plugins/publish/validate_frame_range_type.py +++ /dev/null @@ -1,33 +0,0 @@ -import pyblish.api - -from pymxs import runtime as rt -from openpype.pipeline.publish import ( - RepairAction, - ValidateContentsOrder, - PublishValidationError -) - - -class ValidateFrameRangeType(pyblish.api.InstancePlugin): - """ - Validates whether the User - specified Frame Range(Type 3) is used in render setting - - """ - - label = "Validate Render Frame Range Type" - order = ValidateContentsOrder - families = ["maxrender"] - hosts = ["max"] - actions = [RepairAction] - - def process(self, instance): - if rt.rendTimeType != 3: - raise PublishValidationError("Incorrect type of frame range" - " used in render setting." - " Repair action can help to fix it.") - - @classmethod - def repair(cls, instance): - rt.renderTimeType = 3 - return instance diff --git a/openpype/settings/defaults/project_settings/max.json b/openpype/settings/defaults/project_settings/max.json index d59cdf8c4a..a757e08ef5 100644 --- a/openpype/settings/defaults/project_settings/max.json +++ b/openpype/settings/defaults/project_settings/max.json @@ -19,5 +19,12 @@ "custFloats": "custFloats", "custVecs": "custVecs" } + }, + "publish": { + "ValidateFrameRange": { + "enabled": true, + "optional": true, + "active": true + } } } diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_max.json b/openpype/settings/entities/schemas/projects_schema/schema_project_max.json index 4fba9aff0a..42506559d0 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_max.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_max.json @@ -73,6 +73,10 @@ } } ] + }, + { + "type": "schema", + "name": "schema_max_publish" } ] } diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json new file mode 100644 index 0000000000..ea08c735a6 --- /dev/null +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json @@ -0,0 +1,33 @@ +{ + "type": "dict", + "collapsible": true, + "key": "publish", + "label": "Publish plugins", + "children": [ + { + "type": "dict", + "collapsible": true, + "checkbox_key": "enabled", + "key": "ValidateFrameRange", + "label": "Validate Frame Range", + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "boolean", + "key": "optional", + "label": "Optional" + }, + { + "type": "boolean", + "key": "active", + "label": "Active" + } + ] + } + ] + } From 273eb747915722c29e2c88d123cac43977b3b575 Mon Sep 17 00:00:00 2001 From: Sharkitty <81646000+Sharkitty@users.noreply.github.com> Date: Wed, 3 May 2023 08:13:37 +0000 Subject: [PATCH 179/360] Update openpype/plugins/inventory/remove_and_load.py Fix typo Co-authored-by: Roy Nieterau --- openpype/plugins/inventory/remove_and_load.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/inventory/remove_and_load.py b/openpype/plugins/inventory/remove_and_load.py index 981722c065..9befaf8729 100644 --- a/openpype/plugins/inventory/remove_and_load.py +++ b/openpype/plugins/inventory/remove_and_load.py @@ -34,7 +34,7 @@ class RemoveAndLoad(InventoryAction): representation = get_representation_by_id( project_name, container["representation"] ) - assert representation, "Represenatation not found" + assert representation, "Representation not found" # Remove container remove_container(container) From e2da9a0552356688f2fe2e091ca5de0ae6a5b84c Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Wed, 3 May 2023 10:20:50 +0200 Subject: [PATCH 180/360] Using for else to raise loader not found error --- openpype/plugins/inventory/remove_and_load.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/plugins/inventory/remove_and_load.py b/openpype/plugins/inventory/remove_and_load.py index 9befaf8729..998be119d5 100644 --- a/openpype/plugins/inventory/remove_and_load.py +++ b/openpype/plugins/inventory/remove_and_load.py @@ -25,10 +25,10 @@ class RemoveAndLoad(InventoryAction): if get_loader_identifier(plugin) == loader_name: loader = plugin break - - assert ( - loader - ), "Failed to get loader, can't remove and load container" + else: + raise RuntimeError( + "Failed to get loader, can't remove and load container" + ) # Get representation representation = get_representation_by_id( From bc92395a7eb4fd98deb33299adca314b6c5ebfa0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 3 May 2023 15:44:37 +0200 Subject: [PATCH 181/360] update bug report workflow --- .github/workflows/update_bug_report.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/update_bug_report.yml b/.github/workflows/update_bug_report.yml index 7a1bfb7bfd..1e5da414bb 100644 --- a/.github/workflows/update_bug_report.yml +++ b/.github/workflows/update_bug_report.yml @@ -18,10 +18,16 @@ jobs: uses: ynput/gha-populate-form-version@main with: github_token: ${{ secrets.YNPUT_BOT_TOKEN }} - github_user: ${{ secrets.CI_USER }} - github_email: ${{ secrets.CI_EMAIL }} registry: github dropdown: _version limit_to: 100 form: .github/ISSUE_TEMPLATE/bug_report.yml commit_message: 'chore(): update bug report / version' + dry_run: no-push + + - name: Push to protected develop branch + uses: CasperWA/push-protected@v2.10.0 + with: + token: ${{ secrets.YNPUT_BOT_TOKEN }} + branch: develop + unprotect_reviews: true \ No newline at end of file From 3d870ef794c8fbdf7bf6ac17351a7aaaeaa1811a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 3 May 2023 13:45:37 +0000 Subject: [PATCH 182/360] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index fe86a8400b..8328a35cad 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,9 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.15.6-nightly.3 + - 3.15.6-nightly.2 + - 3.15.6-nightly.1 - 3.15.5 - 3.15.5-nightly.2 - 3.15.5-nightly.1 @@ -132,9 +135,6 @@ body: - 3.14.0 - 3.14.0-nightly.1 - 3.13.1-nightly.3 - - 3.13.1-nightly.2 - - 3.13.1-nightly.1 - - 3.13.0 validations: required: true - type: dropdown From 9d3e15378b0109f332c99a21c2468012ff7d60c4 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 3 May 2023 22:31:43 +0800 Subject: [PATCH 183/360] repharse the actual msg for the artists --- .../max/plugins/publish/validate_resolution_setting.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_resolution_setting.py b/openpype/hosts/max/plugins/publish/validate_resolution_setting.py index 9424b24380..94cd093569 100644 --- a/openpype/hosts/max/plugins/publish/validate_resolution_setting.py +++ b/openpype/hosts/max/plugins/publish/validate_resolution_setting.py @@ -29,15 +29,15 @@ class ValidateResolutionSetting(pyblish.api.InstancePlugin, current_width = rt.renderwidth current_height = rt.renderHeight if current_width != width and current_height != height: - raise PublishValidationError("Resolution Setting" - " not aligned with DB") + raise PublishValidationError("Resolution Setting " + "not matching resolution set on asset or shot.") if current_width != width: raise PublishValidationError("Width in Resolution Setting " - "not aligned with DB") + "not matching resolution set on asset or shot.") if current_height != height: raise PublishValidationError("Height in Resolution Setting " - "not aligned with DB") + "not matching resolution set on asset or shot.") def get_db_resolution(self, instance): data = ["data.resolutionWidth", "data.resolutionHeight"] From d8e62093acaf92ca21df28240c420a8d7895f808 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 3 May 2023 22:33:13 +0800 Subject: [PATCH 184/360] hound fix --- .../max/plugins/publish/validate_resolution_setting.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_resolution_setting.py b/openpype/hosts/max/plugins/publish/validate_resolution_setting.py index 94cd093569..5fcb843b20 100644 --- a/openpype/hosts/max/plugins/publish/validate_resolution_setting.py +++ b/openpype/hosts/max/plugins/publish/validate_resolution_setting.py @@ -30,14 +30,17 @@ class ValidateResolutionSetting(pyblish.api.InstancePlugin, current_height = rt.renderHeight if current_width != width and current_height != height: raise PublishValidationError("Resolution Setting " - "not matching resolution set on asset or shot.") + "not matching resolution " + "set on asset or shot.") if current_width != width: raise PublishValidationError("Width in Resolution Setting " - "not matching resolution set on asset or shot.") + "not matching resolution set " + "on asset or shot.") if current_height != height: raise PublishValidationError("Height in Resolution Setting " - "not matching resolution set on asset or shot.") + "not matching resolution set " + "on asset or shot.") def get_db_resolution(self, instance): data = ["data.resolutionWidth", "data.resolutionHeight"] From 17d39cc3561bc418191bf454bcc5567355a3fbcf Mon Sep 17 00:00:00 2001 From: Seyedmohammadreza Hashemizadeh Date: Wed, 5 Apr 2023 18:43:19 +0200 Subject: [PATCH 185/360] preserve all references when importing a maya template --- openpype/hosts/maya/api/workfile_template_builder.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/workfile_template_builder.py b/openpype/hosts/maya/api/workfile_template_builder.py index d65e4c74d2..c91544be0a 100644 --- a/openpype/hosts/maya/api/workfile_template_builder.py +++ b/openpype/hosts/maya/api/workfile_template_builder.py @@ -43,7 +43,13 @@ class MayaTemplateBuilder(AbstractTemplateBuilder): )) cmds.sets(name=PLACEHOLDER_SET, empty=True) - new_nodes = cmds.file(path, i=True, returnNewNodes=True) + new_nodes = cmds.file( + path, + i=True, + returnNewNodes=True, + preserveReferences=True, + loadReferenceDepth="all", + ) cmds.setAttr(PLACEHOLDER_SET + ".hiddenInOutliner", True) From 64eea1dc4a58444afaa8a58e32e2b50d834144c7 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Wed, 3 May 2023 21:33:01 +0200 Subject: [PATCH 186/360] Updated raise error --- .../hosts/fusion/plugins/publish/increment_current_file.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/publish/increment_current_file.py b/openpype/hosts/fusion/plugins/publish/increment_current_file.py index de6f697073..08a65bf52d 100644 --- a/openpype/hosts/fusion/plugins/publish/increment_current_file.py +++ b/openpype/hosts/fusion/plugins/publish/increment_current_file.py @@ -1,6 +1,7 @@ import pyblish.api from openpype.pipeline import OptionalPyblishPluginMixin +from openpype.pipeline import KnownPublishError class FusionIncrementCurrentFile( @@ -29,7 +30,7 @@ class FusionIncrementCurrentFile( plugin.__name__ == "FusionSubmitDeadline" for plugin in errored_plugins ): - raise RuntimeError( + raise KnownPublishError( "Skipping incrementing current file because " "submission to render farm failed." ) From e7aa413038f186b4f523318762d438f33c2004a8 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 4 May 2023 12:16:58 +0200 Subject: [PATCH 187/360] AfterEffects: add review flag to each instance (#4884) * OP-5657 - add artist control for review in AfterEffects Artist can disable review to be created for particular publish. * OP-5657 - add artist control for review in AfterEffects Removed configuration for Deadline, should be controlled by what is on instance. * OP-5657 - handle legacy instances Legacy instances wont't have mark_for_review in creator_attributes. Set to true as by default we always want review. * OP-5657 - remove explicit review for all AE Now handled directly on instance * OP-5657 - fix - cannot remove now Without this 'review' wont be added to tags on representation. Eventually this should be refactored. Control on whole instance, eg. disabling review, should be enough. * OP-5657 - fix - correct host name used * OP-5657 - fix - correct handling of review On local renders review should be added only from families, not from older approach through Settings. Farm instance cannot have review in families or extract_review would get triggered even locally. * OP-5657 - refactor - changed label * OP-5657 - Hound * OP-5657 - added explicitly skipping review Instance might have set 'review' to False, which should explicitly skip review (might come from Publisher where artist can disable/enable review on an instance). * OP-5657 - updated setting of review variable instance.data.review == False >> explicitly set to do not create review. Keep None to let logic decide. * OP-5657 - fix adding review flag * OP-5657 - updated test Removed review for second instance. * OP-5657 - refactor to context plugin * OP-5657 - tie thumbnail to review for local render Produce thumbnail only when review should be created to synchronize state with farm rendering. Move creation of thumnbail out of this plugin to general plugin to limit duplication of logic. --- .../plugins/create/create_render.py | 45 +++++++++++++------ .../plugins/publish/collect_render.py | 18 +++----- .../plugins/publish/collect_review.py | 25 +++++++++++ .../plugins/publish/extract_local_render.py | 28 +----------- .../plugins/publish/submit_publish_job.py | 23 +++++++--- .../publish/abstract_collect_render.py | 2 +- .../project_settings/aftereffects.json | 6 ++- .../schema_project_aftereffects.json | 23 +++++++++- ...ublish_in_aftereffects_multicomposition.py | 9 ++-- 9 files changed, 116 insertions(+), 63 deletions(-) create mode 100644 openpype/hosts/aftereffects/plugins/publish/collect_review.py diff --git a/openpype/hosts/aftereffects/plugins/create/create_render.py b/openpype/hosts/aftereffects/plugins/create/create_render.py index c20b0ec51b..171d7053ce 100644 --- a/openpype/hosts/aftereffects/plugins/create/create_render.py +++ b/openpype/hosts/aftereffects/plugins/create/create_render.py @@ -26,12 +26,9 @@ class RenderCreator(Creator): create_allow_context_change = True - def __init__(self, project_settings, *args, **kwargs): - super(RenderCreator, self).__init__(project_settings, *args, **kwargs) - self._default_variants = (project_settings["aftereffects"] - ["create"] - ["RenderCreator"] - ["defaults"]) + # Settings + default_variants = [] + mark_for_review = True def create(self, subset_name_from_ui, data, pre_create_data): stub = api.get_stub() # only after After Effects is up @@ -82,28 +79,40 @@ class RenderCreator(Creator): use_farm = pre_create_data["farm"] new_instance.creator_attributes["farm"] = use_farm + review = pre_create_data["mark_for_review"] + new_instance.creator_attributes["mark_for_review"] = review + api.get_stub().imprint(new_instance.id, new_instance.data_to_store()) self._add_instance_to_context(new_instance) stub.rename_item(comp.id, subset_name) - def get_default_variants(self): - return self._default_variants - - def get_instance_attr_defs(self): - return [BoolDef("farm", label="Render on farm")] - def get_pre_create_attr_defs(self): output = [ BoolDef("use_selection", default=True, label="Use selection"), BoolDef("use_composition_name", label="Use composition name in subset"), UISeparatorDef(), - BoolDef("farm", label="Render on farm") + BoolDef("farm", label="Render on farm"), + BoolDef( + "mark_for_review", + label="Review", + default=self.mark_for_review + ) ] return output + def get_instance_attr_defs(self): + return [ + BoolDef("farm", label="Render on farm"), + BoolDef( + "mark_for_review", + label="Review", + default=False + ) + ] + def get_icon(self): return resources.get_openpype_splash_filepath() @@ -143,6 +152,13 @@ class RenderCreator(Creator): api.get_stub().rename_item(comp_id, new_comp_name) + def apply_settings(self, project_settings, system_settings): + plugin_settings = ( + project_settings["aftereffects"]["create"]["RenderCreator"] + ) + + self.mark_for_review = plugin_settings["mark_for_review"] + def get_detail_description(self): return """Creator for Render instances @@ -201,4 +217,7 @@ class RenderCreator(Creator): instance_data["creator_attributes"] = {"farm": is_old_farm} instance_data["family"] = self.family + if instance_data["creator_attributes"].get("mark_for_review") is None: + instance_data["creator_attributes"]["mark_for_review"] = True + return instance_data diff --git a/openpype/hosts/aftereffects/plugins/publish/collect_render.py b/openpype/hosts/aftereffects/plugins/publish/collect_render.py index 6153a426cf..b01b707246 100644 --- a/openpype/hosts/aftereffects/plugins/publish/collect_render.py +++ b/openpype/hosts/aftereffects/plugins/publish/collect_render.py @@ -88,10 +88,11 @@ class CollectAERender(publish.AbstractCollectRender): raise ValueError("No file extension set in Render Queue") render_item = render_q[0] + instance_families = inst.data.get("families", []) subset_name = inst.data["subset"] instance = AERenderInstance( family="render", - families=inst.data.get("families", []), + families=instance_families, version=version, time="", source=current_file, @@ -109,6 +110,7 @@ class CollectAERender(publish.AbstractCollectRender): tileRendering=False, tilesX=0, tilesY=0, + review="review" in instance_families, frameStart=frame_start, frameEnd=frame_end, frameStep=1, @@ -139,6 +141,9 @@ class CollectAERender(publish.AbstractCollectRender): instance.toBeRenderedOn = "deadline" instance.renderer = "aerender" instance.farm = True # to skip integrate + if "review" in instance.families: + # to skip ExtractReview locally + instance.families.remove("review") instances.append(instance) instances_to_remove.append(inst) @@ -218,15 +223,4 @@ class CollectAERender(publish.AbstractCollectRender): if fam not in instance.families: instance.families.append(fam) - settings = get_project_settings(os.getenv("AVALON_PROJECT")) - reviewable_subset_filter = (settings["deadline"] - ["publish"] - ["ProcessSubmittedJobOnFarm"] - ["aov_filter"].get(self.hosts[0])) - for aov_pattern in reviewable_subset_filter: - if re.match(aov_pattern, instance.subset): - instance.families.append("review") - instance.review = True - break - return instance diff --git a/openpype/hosts/aftereffects/plugins/publish/collect_review.py b/openpype/hosts/aftereffects/plugins/publish/collect_review.py new file mode 100644 index 0000000000..a933b9fed2 --- /dev/null +++ b/openpype/hosts/aftereffects/plugins/publish/collect_review.py @@ -0,0 +1,25 @@ +""" +Requires: + None + +Provides: + instance -> family ("review") +""" +import pyblish.api + + +class CollectReview(pyblish.api.ContextPlugin): + """Add review to families if instance created with 'mark_for_review' flag + """ + label = "Collect Review" + hosts = ["aftereffects"] + order = pyblish.api.CollectorOrder + 0.1 + + def process(self, context): + for instance in context: + creator_attributes = instance.data.get("creator_attributes") or {} + if ( + creator_attributes.get("mark_for_review") + and "review" not in instance.data["families"] + ): + instance.data["families"].append("review") diff --git a/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py b/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py index d535329eb4..c70aa41dbe 100644 --- a/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py +++ b/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py @@ -66,33 +66,9 @@ class ExtractLocalRender(publish.Extractor): first_repre = not representations if instance.data["review"] and first_repre: repre_data["tags"] = ["review"] + thumbnail_path = os.path.join(staging_dir, files[0]) + instance.data["thumbnailSource"] = thumbnail_path representations.append(repre_data) instance.data["representations"] = representations - - ffmpeg_path = get_ffmpeg_tool_path("ffmpeg") - # Generate thumbnail. - thumbnail_path = os.path.join(staging_dir, "thumbnail.jpg") - - args = [ - ffmpeg_path, "-y", - "-i", first_file_path, - "-vf", "scale=300:-1", - "-vframes", "1", - thumbnail_path - ] - self.log.debug("Thumbnail args:: {}".format(args)) - try: - output = run_subprocess(args) - except TypeError: - self.log.warning("Error in creating thumbnail") - six.reraise(*sys.exc_info()) - - instance.data["representations"].append({ - "name": "thumbnail", - "ext": "jpg", - "files": os.path.basename(thumbnail_path), - "stagingDir": staging_dir, - "tags": ["thumbnail"] - }) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index f80bd40133..eeb813cb62 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -438,7 +438,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "Finished copying %i files" % len(resource_files)) def _create_instances_for_aov( - self, instance_data, exp_files, additional_data + self, instance_data, exp_files, additional_data, do_not_add_review ): """Create instance for each AOV found. @@ -449,6 +449,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): instance_data (pyblish.plugin.Instance): skeleton data for instance (those needed) later by collector exp_files (list): list of expected files divided by aovs + additional_data (dict): + do_not_add_review (bool): explicitly skip review Returns: list of instances @@ -514,8 +516,6 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): app = os.environ.get("AVALON_APP", "") - preview = False - if isinstance(col, list): render_file_name = os.path.basename(col[0]) else: @@ -532,6 +532,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): new_instance = deepcopy(instance_data) new_instance["subset"] = subset_name new_instance["subsetGroup"] = group_name + + preview = preview and not do_not_add_review if preview: new_instance["review"] = True @@ -591,7 +593,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): self.log.debug("instances:{}".format(instances)) return instances - def _get_representations(self, instance, exp_files): + def _get_representations(self, instance, exp_files, do_not_add_review): """Create representations for file sequences. This will return representations of expected files if they are not @@ -602,6 +604,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): instance (dict): instance data for which we are setting representations exp_files (list): list of expected files + do_not_add_review (bool): explicitly skip review Returns: list of representations @@ -651,6 +654,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): if instance.get("slate"): frame_start -= 1 + preview = preview and not do_not_add_review rep = { "name": ext, "ext": ext, @@ -705,6 +709,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): preview = match_aov_pattern( host_name, self.aov_filter, remainder ) + preview = preview and not do_not_add_review if preview: rep.update({ "fps": instance.get("fps"), @@ -820,8 +825,12 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): families = [family] # pass review to families if marked as review + do_not_add_review = False if data.get("review"): families.append("review") + elif data.get("review") == False: + self.log.debug("Instance has review explicitly disabled.") + do_not_add_review = True instance_skeleton_data = { "family": family, @@ -977,7 +986,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): instances = self._create_instances_for_aov( instance_skeleton_data, data.get("expectedFiles"), - additional_data + additional_data, + do_not_add_review ) self.log.info("got {} instance{}".format( len(instances), @@ -986,7 +996,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): else: representations = self._get_representations( instance_skeleton_data, - data.get("expectedFiles") + data.get("expectedFiles"), + do_not_add_review ) if "representations" not in instance_skeleton_data.keys(): diff --git a/openpype/pipeline/publish/abstract_collect_render.py b/openpype/pipeline/publish/abstract_collect_render.py index ccb2415346..fd35ddb719 100644 --- a/openpype/pipeline/publish/abstract_collect_render.py +++ b/openpype/pipeline/publish/abstract_collect_render.py @@ -58,7 +58,7 @@ class RenderInstance(object): # With default values # metadata renderer = attr.ib(default="") # renderer - can be used in Deadline - review = attr.ib(default=False) # generate review from instance (bool) + review = attr.ib(default=None) # False - explicitly skip review priority = attr.ib(default=50) # job priority on farm family = attr.ib(default="renderlayer") diff --git a/openpype/settings/defaults/project_settings/aftereffects.json b/openpype/settings/defaults/project_settings/aftereffects.json index 669e1db0b8..6128534344 100644 --- a/openpype/settings/defaults/project_settings/aftereffects.json +++ b/openpype/settings/defaults/project_settings/aftereffects.json @@ -13,10 +13,14 @@ "RenderCreator": { "defaults": [ "Main" - ] + ], + "mark_for_review": true } }, "publish": { + "CollectReview": { + "enabled": true + }, "ValidateSceneSettings": { "enabled": true, "optional": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json b/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json index 8dc83f5506..313e0ce8ea 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json @@ -40,7 +40,13 @@ "label": "Default Variants", "object_type": "text", "docstring": "Fill default variant(s) (like 'Main' or 'Default') used in subset name creation." - } + }, + { + "type": "boolean", + "key": "mark_for_review", + "label": "Review", + "default": true + } ] } ] @@ -51,6 +57,21 @@ "key": "publish", "label": "Publish plugins", "children": [ + { + "type": "dict", + "collapsible": true, + "key": "CollectReview", + "label": "Collect Review", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled", + "default": true + } + ] + }, { "type": "dict", "collapsible": true, diff --git a/tests/integration/hosts/aftereffects/test_deadline_publish_in_aftereffects_multicomposition.py b/tests/integration/hosts/aftereffects/test_deadline_publish_in_aftereffects_multicomposition.py index d372efcb9a..0e9cd3b00d 100644 --- a/tests/integration/hosts/aftereffects/test_deadline_publish_in_aftereffects_multicomposition.py +++ b/tests/integration/hosts/aftereffects/test_deadline_publish_in_aftereffects_multicomposition.py @@ -9,6 +9,9 @@ log = logging.getLogger("test_publish_in_aftereffects") class TestDeadlinePublishInAfterEffectsMultiComposition(AEDeadlinePublishTestClass): # noqa """est case for DL publishing in AfterEffects with multiple compositions. + Workfile contains 2 prepared `render` instances. First has review set, + second doesn't. + Uses generic TestCase to prepare fixtures for test data, testing DBs, env vars. @@ -68,7 +71,7 @@ class TestDeadlinePublishInAfterEffectsMultiComposition(AEDeadlinePublishTestCla name="renderTest_taskMain2")) failures.append( - DBAssert.count_of_types(dbcon, "representation", 7)) + DBAssert.count_of_types(dbcon, "representation", 5)) additional_args = {"context.subset": "workfileTest_task", "context.ext": "aep"} @@ -105,13 +108,13 @@ class TestDeadlinePublishInAfterEffectsMultiComposition(AEDeadlinePublishTestCla additional_args = {"context.subset": "renderTest_taskMain2", "name": "thumbnail"} failures.append( - DBAssert.count_of_types(dbcon, "representation", 1, + DBAssert.count_of_types(dbcon, "representation", 0, additional_args=additional_args)) additional_args = {"context.subset": "renderTest_taskMain2", "name": "png_exr"} failures.append( - DBAssert.count_of_types(dbcon, "representation", 1, + DBAssert.count_of_types(dbcon, "representation", 0, additional_args=additional_args)) assert not any(failures) From e1a2792a7a8d774abf1777fa079938d71559e57e Mon Sep 17 00:00:00 2001 From: Ynbot Date: Thu, 4 May 2023 13:39:59 +0000 Subject: [PATCH 188/360] [Automated] Release --- CHANGELOG.md | 248 ++++++++++++++++++++++++++++++++++++++++++++ openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 250 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16deaaa4fd..07c1e7d5fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,254 @@ # Changelog +## [3.15.6](https://github.com/ynput/OpenPype/tree/3.15.6) + + +[Full Changelog](https://github.com/ynput/OpenPype/compare/3.15.5...3.15.6) + +### **🆕 New features** + + +
+Substance Painter Integration #4283 + +This implements a part of #4205 by implementing a Substance Painter integration + +Status: +- [x] Implement Host +- [x] start substance with last workfile using `AddLastWorkfileToLaunchArgs` prelaunch hook +- [x] Implement Qt tools +- [x] Implement loaders +- [x] Implemented a Set project mesh loader (this is relatively special case because a Project will always have exactly one mesh - a Substance Painter project cannot exist without a mesh). +- [x] Implement project open callback +- [x] On project open it notifies the user if the loaded model is outdated +- [x] Implement publishing logic +- [x] Workfile publishing +- [x] Export Texture Sets +- [x] Support OCIO using #4195 (draft brach is set up - see comment) +- [ ] Likely needs more testing on the OCIO front +- [x] Validate all outputs of the Export template are exported/generated +- [x] Allow validation to be optional **(issue: there's no API method to detect what maps will be exported without doing an actual export to disk)** +- [x] Support extracting/integration if not all outputs are generated +- [x] Support multiple materials/texture sets per instance +- [ ] Add validator that can enforce only a single texture set output if studio prefers that. +- [ ] Implement Export File Format (extensions) override in Creator +- [ ] Add settings so Admin can choose which extensions are available. + + +___ + +
+ + +
+Data Exchange: Geometry in 3dsMax #4555 + +Introduces and updates a creator, extractors and loaders for model family + +Introduces new creator, extractors and loaders for model family while adding model families into the existing max scene loader and extractor +- [x] creators +- [x] adding model family into max scene loader and extractor +- [x] fbx loader +- [x] fbx extractor +- [x] usd loader +- [x] usd extractor +- [x] validator for model family +- [x] obj loader(update function) +- [x] fix the update function of the loader as #4675 +- [x] Add documentation + + +___ + +
+ + +
+AfterEffects: add review flag to each instance #4884 + +Adds `mark_for_review` flag to the Creator to allow artists to disable review if necessary.Exposed this flag in Settings, by default set to True (eg. same behavior as previously). + + +___ + +
+ +### **🚀 Enhancements** + + +
+Houdini: Fix Validate Output Node (VDB) #4819 + +- Removes plug-in that was a duplicate of this plug-in. +- Optimize logging of many prims slightly +- Fix error reporting like https://github.com/ynput/OpenPype/pull/4818 did + + +___ + +
+ + +
+Houdini: Add null node as output indicator when using TAB search #4834 + + +___ + +
+ + +
+Houdini: Don't error in collect review if camera is not set correctly #4874 + +Do not raise an error in collector when invalid path is set as camera path. Allow camera path to not be set correctly in review instance until validation so it's nicely shown in a validation report. + + +___ + +
+ + +
+Project packager: Backup and restore can store only database #4879 + +Pack project functionality have option to zip only project database without project files. Unpack project can skip project copy if the folder is not found.Added helper functions to `openpype.client.mongo` that can be also used for tests as replacement of mongo dump. + + +___ + +
+ + +
+Houdini: ExtractOpenGL for Review instance not optional #4881 + +Don't make ExtractOpenGL optional for review instance optional. + + +___ + +
+ + +
+Publisher: Small style changes #4894 + +Small changes in styles and form of publisher UI. + + +___ + +
+ + +
+Houdini: Workfile icon in new publisher #4898 + +Fix icon for the workfile instance in new publisher + + +___ + +
+ + +
+Fusion: Simplify creator icons code #4899 + +Simplify code for setting the icons for the Fusion creators + + +___ + +
+ + +
+Enhancement: Fix PySide 6.5 support for loader #4900 + +Fixes PySide 6.5 support in Loader. + + +___ + +
+ +### **🐛 Bug fixes** + + +
+Maya: Validate Attributes #4917 + +This plugin was broken due to bad fetching of data and wrong repair action. + + +___ + +
+ + +
+Fix: Locally copied version of last published workfile is not incremented #4722 + +### Fix 1 +When copied, the local workfile version keeps the published version number, when it must be +1 to follow OP's naming convention. + +### Fix 2 +Local workfile version's name is built from anatomy. This avoids to get workfiles with their publish template naming. + +### Fix 3 +In the case a subset has at least two tasks with published workfiles, for example `Modeling` and `Rigging`, launching `Rigging` was getting the first one with the `next` and trying to find representations, therefore `workfileModeling` and trying to match the current `task_name` (`Rigging`) with the `representation["context"]["task"]["name"]` of a Modeling representation, which was ending up to a `workfile_representation` to `None`, and exiting the process. + +Trying to find the `task_name` in the `subset['name']` fixes it. + +### Fix 4 +Fetch input dependencies of workfile. + +Replacing https://github.com/ynput/OpenPype/pull/4102 for changes to bring this home. +___ + +
+ + +
+Maya: soft-fail when pan/zoom locked on camera when playblasting #4929 + +When pan/zoom enabled attribute on camera is locked, playblasting with pan/zoom fails because it is trying to restore it. This is fixing it by skipping over with warning. + + +___ + +
+ +### **Merged pull requests** + + +
+Maya Load References - Add Display Handle Setting #4904 + +When we load a reference in Maya using OpenPype loader, display handle is checked by default and prevent us to select easily the object in the viewport. I understand that some productions like to keep this option, so I propose to add display handle to the reference loader settings. + + +___ + +
+ + +
+Photoshop: add autocreators for review and flat image #4871 + +Review and flatten image (produced when no instance of `image` family was created) were created somehow magically. This PRintroduces two new auto creators which allow artists to disable review or flatten image.For all `image` instances `Review` flag was added to provide functionality to create separate review per `image` instance. Previously was possible only to have separate instance of `review` family.Review is not enabled on `image` family by default. (Eg. follows original behavior)Review auto creator is enabled by default as it was before.Flatten image creator must be set in Settings in `project_settings/photoshop/create/AutoImageCreator`. + + +___ + +
+ + + + ## [3.15.5](https://github.com/ynput/OpenPype/tree/3.15.5) diff --git a/openpype/version.py b/openpype/version.py index 9832ff4747..dc0a3a8c9f 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.6-nightly.3" +__version__ = "3.15.6" diff --git a/pyproject.toml b/pyproject.toml index 2f40d58f56..003f6cf2d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.15.5" # OpenPype +version = "3.15.6" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 11343088b55eb9962a91919295ecbe865b03c11c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 4 May 2023 13:41:12 +0000 Subject: [PATCH 189/360] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 8328a35cad..5050d37c7a 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.15.6 - 3.15.6-nightly.3 - 3.15.6-nightly.2 - 3.15.6-nightly.1 @@ -134,7 +135,6 @@ body: - 3.14.1-nightly.1 - 3.14.0 - 3.14.0-nightly.1 - - 3.13.1-nightly.3 validations: required: true - type: dropdown From b9055d61af83760430cce647f7f2226b23b91bcf Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 4 May 2023 17:42:16 +0200 Subject: [PATCH 190/360] POC wip --- .../fusion/plugins/create/create_saver.py | 2 +- .../fusion/plugins/publish/collect_renders.py | 8 + .../plugins/publish/submit_fusion_deadline.py | 140 ++++++++++++++++-- 3 files changed, 133 insertions(+), 17 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index cedc4029fa..a66d9b7e86 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -28,7 +28,7 @@ class CreateSaver(Creator): description = "Fusion Saver to generate image sequence" icon = "fa5.eye" - instance_attributes = ["reviewable"] + instance_attributes = ["reviewable", "farm_rendering"] def create(self, subset_name, instance_data, pre_create_data): # TODO: Add pre_create attributes to choose file format? diff --git a/openpype/hosts/fusion/plugins/publish/collect_renders.py b/openpype/hosts/fusion/plugins/publish/collect_renders.py index 7f38e68447..b1c12c7393 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_renders.py +++ b/openpype/hosts/fusion/plugins/publish/collect_renders.py @@ -23,3 +23,11 @@ class CollectFusionRenders(pyblish.api.InstancePlugin): instance.data["families"].append( "{}.{}".format(family, render_target) ) + if render_target == "farm": + if "review" in instance.data["families"]: + instance.data["families"].remove("review") + + # Farm rendering + instance.data["transfer"] = False + instance.data["farm"] = True + self.log.info("Farm rendering ON ...") diff --git a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py index 8570c759bc..2885d91d07 100644 --- a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py @@ -1,15 +1,26 @@ import os import json import getpass +from pprint import pformat import requests import pyblish.api from openpype.pipeline import legacy_io +from openpype.pipeline.publish import ( + OpenPypePyblishPluginMixin +) +from openpype.lib import ( + BoolDef, + NumberDef +) -class FusionSubmitDeadline(pyblish.api.InstancePlugin): +class FusionSubmitDeadline( + pyblish.api.InstancePlugin, + OpenPypePyblishPluginMixin +): """Submit current Comp to Deadline Renders are submitted to a Deadline Web Service as @@ -17,12 +28,76 @@ class FusionSubmitDeadline(pyblish.api.InstancePlugin): """ - label = "Submit to Deadline" + label = "Submit Fusion to Deadline" order = pyblish.api.IntegratorOrder hosts = ["fusion"] - families = ["render.farm"] + families = ["render"] + targets = ["local"] + + # presets + priority = 50 + chunk_size = 1 + concurrent_tasks = 1 + group = "" + department = "" + limit_groups = {} + use_gpu = False + env_allowed_keys = [] + env_search_replace_values = {} + + @classmethod + def get_attribute_defs(cls): + return [ + NumberDef( + "priority", + label="Priority", + default=cls.priority, + decimals=0 + ), + NumberDef( + "chunk", + label="Frames Per Task", + default=cls.chunk_size, + decimals=0, + minimum=1, + maximum=1000 + ), + NumberDef( + "concurrency", + label="Concurrency", + default=cls.concurrent_tasks, + decimals=0, + minimum=1, + maximum=10 + ), + BoolDef( + "use_gpu", + default=cls.use_gpu, + label="Use GPU" + ), + BoolDef( + "suspend_publish", + default=False, + label="Suspend publish" + ) + ] def process(self, instance): + if not instance.data.get("farm"): + self.log.info("Skipping local instance.") + return + + attribute_values = self.get_attr_values_from_data( + instance.data) + + self.log.debug(pformat(attribute_values)) + + # add suspend_publish attributeValue to instance data + instance.data["suspend_publish"] = attribute_values[ + "suspend_publish"] + + instance.data["toBeRenderedOn"] = "deadline" + context = instance.context key = "__hasRun{}".format(self.__class__.__name__) @@ -33,24 +108,24 @@ class FusionSubmitDeadline(pyblish.api.InstancePlugin): from openpype.hosts.fusion.api.lib import get_frame_path - deadline_url = ( - context.data["system_settings"] - ["modules"] - ["deadline"] - ["DEADLINE_REST_URL"] - ) - assert deadline_url, "Requires DEADLINE_REST_URL" + # get default deadline webservice url from deadline module + deadline_url = instance.context.data["defaultDeadline"] + # if custom one is set in instance, use that + if instance.data.get("deadlineUrl"): + deadline_url = instance.data.get("deadlineUrl") + assert deadline_url, "Requires Deadline Webservice URL" # Collect all saver instances in context that are to be rendered saver_instances = [] - for instance in context[:]: - if not self.families[0] in instance.data.get("families"): + for instance in context: + if instance.data["family"] != "render": # Allow only saver family instances continue if not instance.data.get("publish", True): # Skip inactive instances continue + self.log.debug(instance.data["name"]) saver_instances.append(instance) @@ -58,11 +133,31 @@ class FusionSubmitDeadline(pyblish.api.InstancePlugin): raise RuntimeError("No instances found for Deadline submittion") fusion_version = int(context.data["fusionVersion"]) - filepath = context.data["currentFile"] - filename = os.path.basename(filepath) comment = context.data.get("comment", "") deadline_user = context.data.get("deadlineUser", getpass.getuser()) + script_path = context.data["currentFile"] + + for item in context: + if "workfile" in item.data["families"]: + msg = "Workfile (scene) must be published along" + assert item.data["publish"] is True, msg + + template_data = item.data.get("anatomyData") + rep = item.data.get("representations")[0].get("name") + template_data["representation"] = rep + template_data["ext"] = rep + template_data["comment"] = None + anatomy_filled = context.data["anatomy"].format(template_data) + template_filled = anatomy_filled["publish"]["path"] + script_path = os.path.normpath(template_filled) + + self.log.info( + "Using published scene for render {}".format(script_path) + ) + + filename = os.path.basename(script_path) + # Documentation for keys available at: # https://docs.thinkboxsoftware.com # /products/deadline/8.0/1_User%20Manual/manual @@ -73,11 +168,20 @@ class FusionSubmitDeadline(pyblish.api.InstancePlugin): "BatchName": filename, # Asset dependency to wait for at least the scene file to sync. - "AssetDependency0": filepath, + "AssetDependency0": script_path, # Job name, as seen in Monitor "Name": filename, + "Priority": attribute_values.get( + "priority", self.priority), + "ChunkSize": attribute_values.get( + "chunk", self.chunk_size), + "ConcurrentTasks": attribute_values.get( + "concurrency", + self.concurrent_tasks + ), + # User, as seen in Monitor "UserName": deadline_user, @@ -94,7 +198,7 @@ class FusionSubmitDeadline(pyblish.api.InstancePlugin): }, "PluginInfo": { # Input - "FlowFile": filepath, + "FlowFile": script_path, # Mandatory for Deadline "Version": str(fusion_version), @@ -109,6 +213,10 @@ class FusionSubmitDeadline(pyblish.api.InstancePlugin): # Proxy: higher numbers smaller images for faster test renders # 1 = no proxy quality "Proxy": 1, + + # using GPU by default + "UseGpu": attribute_values.get( + "use_gpu", self.use_gpu) }, # Mandatory for Deadline, may be empty From 44a88c3f32a049c0eac33c11d6127c0e62fc95c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Fri, 5 May 2023 10:11:03 +0200 Subject: [PATCH 191/360] :bug: add missing pyblish.util import --- openpype/pipeline/publish/lib.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index 265a9c7822..8b6212b3ef 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -7,6 +7,7 @@ import tempfile import xml.etree.ElementTree import six +import pyblish.util import pyblish.plugin import pyblish.api From b103d6d8373e0785a5449bad8ecc14b65609cd77 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 5 May 2023 10:45:42 +0100 Subject: [PATCH 192/360] Fix missing 'object_oath' property --- openpype/hosts/unreal/plugins/load/load_animation.py | 4 ++-- openpype/hosts/unreal/plugins/load/load_layout.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_animation.py b/openpype/hosts/unreal/plugins/load/load_animation.py index 1fe0bef462..f0c08680d3 100644 --- a/openpype/hosts/unreal/plugins/load/load_animation.py +++ b/openpype/hosts/unreal/plugins/load/load_animation.py @@ -156,7 +156,7 @@ class AnimationFBXLoader(plugin.Loader): package_paths=[f"{root}/{hierarchy[0]}"], recursive_paths=False) levels = ar.get_assets(_filter) - master_level = levels[0].get_editor_property('object_path') + master_level = levels[0].get_full_name() hierarchy_dir = root for h in hierarchy: @@ -168,7 +168,7 @@ class AnimationFBXLoader(plugin.Loader): package_paths=[f"{hierarchy_dir}/"], recursive_paths=True) levels = ar.get_assets(_filter) - level = levels[0].get_editor_property('object_path') + level = levels[0].get_full_name() unreal.EditorLevelLibrary.save_all_dirty_levels() unreal.EditorLevelLibrary.load_level(level) diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index 63d415a52b..f0663a8778 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -819,7 +819,7 @@ class LayoutLoader(plugin.Loader): recursive_paths=False) levels = ar.get_assets(filter) - layout_level = levels[0].get_editor_property('object_path') + layout_level = levels[0].get_full_name() EditorLevelLibrary.save_all_dirty_levels() EditorLevelLibrary.load_level(layout_level) @@ -919,7 +919,7 @@ class LayoutLoader(plugin.Loader): package_paths=[f"{root}/{ms_asset}"], recursive_paths=False) levels = ar.get_assets(_filter) - master_level = levels[0].get_editor_property('object_path') + master_level = levels[0].get_full_name() sequences = [master_sequence] From feaa01eff56d9fbae19adf97edb5ef3f620ac1f5 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 5 May 2023 11:49:47 +0200 Subject: [PATCH 193/360] :fire: remove obsolete validator --- .../publish/validate_sequence_frames.py | 66 ------------------- 1 file changed, 66 deletions(-) delete mode 100644 openpype/plugins/publish/validate_sequence_frames.py diff --git a/openpype/plugins/publish/validate_sequence_frames.py b/openpype/plugins/publish/validate_sequence_frames.py deleted file mode 100644 index 239008ee21..0000000000 --- a/openpype/plugins/publish/validate_sequence_frames.py +++ /dev/null @@ -1,66 +0,0 @@ -import os -import re - -import clique -import pyblish.api - - -class ValidateSequenceFrames(pyblish.api.InstancePlugin): - """Ensure the sequence of frames is complete - - The files found in the folder are checked against the startFrame and - endFrame of the instance. If the first or last file is not - corresponding with the first or last frame it is flagged as invalid. - - Used regular expression pattern handles numbers in the file names - (eg "Main_beauty.v001.1001.exr", "Main_beauty_v001.1001.exr", - "Main_beauty.1001.1001.exr") but not numbers behind frames (eg. - "Main_beauty.1001.v001.exr") - """ - - order = pyblish.api.ValidatorOrder - label = "Validate Sequence Frames" - families = ["imagesequence", "render"] - hosts = ["shell", "unreal"] - - def process(self, instance): - representations = instance.data.get("representations") - if not representations: - return - for repr in representations: - repr_files = repr["files"] - if isinstance(repr_files, str): - continue - - ext = repr.get("ext") - if not ext: - _, ext = os.path.splitext(repr_files[0]) - elif not ext.startswith("."): - ext = ".{}".format(ext) - pattern = r"\D?(?P(?P0*)\d+){}$".format( - re.escape(ext)) - patterns = [pattern] - - 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) - - if instance.data.get("slate"): - # Slate is not part of the frame range - frames = frames[1:] - - current_range = (frames[0], frames[-1]) - - required_range = (instance.data["frameStart"], - instance.data["frameEnd"]) - - if current_range != required_range: - raise ValueError(f"Invalid frame range: {current_range} - " - f"expected: {required_range}") - - missing = collection.holes().indexes - assert not missing, "Missing frames: %s" % (missing,) From c0499c46b852c3ffe9983d17f739cabeee596963 Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Fri, 5 May 2023 14:02:40 +0200 Subject: [PATCH 194/360] Changes based on suggestions --- openpype/plugins/inventory/remove_and_load.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/plugins/inventory/remove_and_load.py b/openpype/plugins/inventory/remove_and_load.py index 998be119d5..be24220c56 100644 --- a/openpype/plugins/inventory/remove_and_load.py +++ b/openpype/plugins/inventory/remove_and_load.py @@ -1,5 +1,5 @@ from openpype.pipeline import InventoryAction -from openpype.pipeline.legacy_io import Session +from openpype.pipeline import get_current_project_name from openpype.pipeline.load.plugins import discover_loader_plugins from openpype.pipeline.load.utils import ( get_loader_identifier, @@ -16,12 +16,13 @@ class RemoveAndLoad(InventoryAction): icon = "refresh" def process(self, containers): + project_name = get_current_project_name() for container in containers: - project_name = Session.get("AVALON_PROJECT") # Get loader loader_name = container["loader"] - for plugin in discover_loader_plugins(project_name=project_name): + loaders = discover_loader_plugins(project_name=project_name) + for plugin in loader: if get_loader_identifier(plugin) == loader_name: loader = plugin break From 43b71e4f1e2f8387cb9fba4b20e401a15b140e51 Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Fri, 5 May 2023 14:24:46 +0200 Subject: [PATCH 195/360] Fixed wrong variable --- openpype/plugins/inventory/remove_and_load.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/inventory/remove_and_load.py b/openpype/plugins/inventory/remove_and_load.py index be24220c56..d465154187 100644 --- a/openpype/plugins/inventory/remove_and_load.py +++ b/openpype/plugins/inventory/remove_and_load.py @@ -22,7 +22,7 @@ class RemoveAndLoad(InventoryAction): # Get loader loader_name = container["loader"] loaders = discover_loader_plugins(project_name=project_name) - for plugin in loader: + for plugin in loaders: if get_loader_identifier(plugin) == loader_name: loader = plugin break From 76352bdfea0002fcfc2bb73865f9db3a326c5931 Mon Sep 17 00:00:00 2001 From: kaamaurice Date: Fri, 5 May 2023 15:07:30 +0200 Subject: [PATCH 196/360] fix error dialog missing parent arg --- openpype/tools/sceneinventory/view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/sceneinventory/view.py b/openpype/tools/sceneinventory/view.py index 3279be6094..73d33392b9 100644 --- a/openpype/tools/sceneinventory/view.py +++ b/openpype/tools/sceneinventory/view.py @@ -791,7 +791,7 @@ class SceneInventoryView(QtWidgets.QTreeView): else: version_str = version - dialog = QtWidgets.QMessageBox() + dialog = QtWidgets.QMessageBox(self) dialog.setIcon(QtWidgets.QMessageBox.Warning) dialog.setStyleSheet(style.load_stylesheet()) dialog.setWindowTitle("Update failed") From e08ff46bfa4edb8b526501b88ff2dcdc36538885 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 5 May 2023 17:22:14 +0200 Subject: [PATCH 197/360] adding settings for creators --- .../defaults/project_settings/fusion.json | 13 ++++++ .../schema_project_fusion.json | 44 +++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/openpype/settings/defaults/project_settings/fusion.json b/openpype/settings/defaults/project_settings/fusion.json index f974eebaca..d76ed82942 100644 --- a/openpype/settings/defaults/project_settings/fusion.json +++ b/openpype/settings/defaults/project_settings/fusion.json @@ -21,5 +21,18 @@ "copy_path": "~/.openpype/hosts/fusion/profiles", "copy_status": false, "force_sync": false + }, + "create": { + "CreateSaver": { + "temp_rendering_path_template": "{workdir}/renders/fusion/{subset}/{subset}..{ext}", + "default_variants": [ + "Main", + "Mask" + ], + "instance_attributes": [ + "reviewable", + "farm_rendering" + ] + } } } diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json b/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json index 464cf2c06d..7971c62300 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json @@ -68,6 +68,50 @@ "label": "Resync profile on each launch" } ] + }, + { + "type": "dict", + "collapsible": true, + "key": "create", + "label": "Creator plugins", + "children": [ + { + "type": "dict", + "collapsible": true, + "key": "CreateSaver", + "label": "Create Saver", + "is_group": true, + "children": [ + { + "type": "text", + "key": "temp_rendering_path_template", + "label": "Temporary rendering path template" + }, + { + "type": "list", + "key": "default_variants", + "label": "Default variants", + "object_type": { + "type": "text" + } + }, + { + "key": "instance_attributes", + "label": "Instance attributes", + "type": "enum", + "multiselection": true, + "enum_items": [ + { + "reviewable": "Reviewable" + }, + { + "farm_rendering": "Farm rendering" + } + ] + } + ] + } + ] } ] } From d637fed33de88d4048c40b56ecf3f5cb86aab654 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 5 May 2023 17:23:27 +0200 Subject: [PATCH 198/360] implementing settings also adding temp rendering path attribute with with support for tempates --- .../fusion/plugins/create/create_saver.py | 78 +++++++++++++++---- 1 file changed, 65 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index cedc4029fa..fb6767d2cd 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -1,4 +1,6 @@ +from copy import deepcopy import os +from pprint import pformat from openpype.hosts.fusion.api import ( get_current_comp, @@ -11,7 +13,7 @@ from openpype.lib import ( ) from openpype.pipeline import ( legacy_io, - Creator, + Creator as NewCreator, CreatedInstance, ) from openpype.client import ( @@ -19,7 +21,7 @@ from openpype.client import ( ) -class CreateSaver(Creator): +class CreateSaver(NewCreator): identifier = "io.openpype.creators.fusion.saver" label = "Render (saver)" name = "render" @@ -28,7 +30,15 @@ class CreateSaver(Creator): description = "Fusion Saver to generate image sequence" icon = "fa5.eye" - instance_attributes = ["reviewable"] + instance_attributes = [ + "reviewable" + ] + default_variants = [ + "Main", + "Mask" + ] + temp_rendering_path_template = ( + "{workdir}/renders/fusion/{subset}/{subset}..{ext}") def create(self, subset_name, instance_data, pre_create_data): # TODO: Add pre_create attributes to choose file format? @@ -125,17 +135,34 @@ class CreateSaver(Creator): original_subset = tool.GetData("openpype.subset") subset = data["subset"] if original_subset != subset: - # Subset change detected - # Update output filepath - workdir = os.path.normpath(legacy_io.Session["AVALON_WORKDIR"]) - filename = f"{subset}..exr" - filepath = os.path.join(workdir, "render", subset, filename) - tool["Clip"] = filepath + self._configure_saver_tool(data, tool, subset) - # Rename tool - if tool.Name != subset: - print(f"Renaming {tool.Name} -> {subset}") - tool.SetAttrs({"TOOLS_Name": subset}) + def _configure_saver_tool(self, data, tool, subset): + formatting_data = deepcopy(data) + self.log.warning(pformat(formatting_data)) + + # Subset change detected + workdir = os.path.normpath(legacy_io.Session["AVALON_WORKDIR"]) + formatting_data.update({ + "workdir": workdir, + "ext": "exr" + }) + + # build file path to render + filepath = self.temp_rendering_path_template.format( + **formatting_data) + + # create directory + if not os.path.isdir(os.path.dirname(filepath)): + self.log.warning("Path does not exist! I am creating it.") + os.makedirs(os.path.dirname(filepath)) + + tool["Clip"] = filepath + + # Rename tool + if tool.Name != subset: + print(f"Renaming {tool.Name} -> {subset}") + tool.SetAttrs({"TOOLS_Name": subset}) def _collect_unmanaged_saver(self, tool): # TODO: this should not be done this way - this should actually @@ -238,3 +265,28 @@ class CreateSaver(Creator): default=("reviewable" in self.instance_attributes), label="Review", ) + + def apply_settings( + self, + project_settings, + system_settings + ): + """Method called on initialization of plugin to apply settings.""" + + # plugin settings + plugin_settings = self._get_creator_settings(project_settings) + + # individual attributes + self.instance_attributes = plugin_settings.get( + "instance_attributes") or self.instance_attributes + self.default_variants = plugin_settings.get( + "default_variants") or self.default_variants + self.temp_rendering_path_template = ( + plugin_settings.get("temp_rendering_path_template") + or self.temp_rendering_path_template + ) + + def _get_creator_settings(self, project_settings, settings_key=None): + if not settings_key: + settings_key = self.__class__.__name__ + return project_settings["fusion"]["create"][settings_key] From c73772919a32f349ecb16229140338b58e466445 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 5 May 2023 17:42:38 +0200 Subject: [PATCH 199/360] fixing path slashes --- openpype/hosts/fusion/plugins/create/create_saver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index fb6767d2cd..8bf364cf20 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -144,7 +144,7 @@ class CreateSaver(NewCreator): # Subset change detected workdir = os.path.normpath(legacy_io.Session["AVALON_WORKDIR"]) formatting_data.update({ - "workdir": workdir, + "workdir": workdir.replace("\\", "/"), "ext": "exr" }) From 7c6bbe8306fc0c17289d17c399105b1dc2c2019d Mon Sep 17 00:00:00 2001 From: Ynbot Date: Sat, 6 May 2023 03:25:00 +0000 Subject: [PATCH 200/360] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index dc0a3a8c9f..e02053ba76 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.6" +__version__ = "3.15.7-nightly.1" From d87f217fb9cf766dfce449426a6ffa9550a2e99d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 6 May 2023 03:25:44 +0000 Subject: [PATCH 201/360] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 5050d37c7a..cae6a6486b 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.15.7-nightly.1 - 3.15.6 - 3.15.6-nightly.3 - 3.15.6-nightly.2 @@ -134,7 +135,6 @@ body: - 3.14.1-nightly.2 - 3.14.1-nightly.1 - 3.14.0 - - 3.14.0-nightly.1 validations: required: true - type: dropdown From 291432d49bcd92120904ad868a27e9981d010865 Mon Sep 17 00:00:00 2001 From: Sharkitty <81646000+Sharkitty@users.noreply.github.com> Date: Mon, 8 May 2023 16:01:57 +0000 Subject: [PATCH 202/360] Update openpype/plugins/inventory/remove_and_load.py Changed representation assertion into a warning with more info Co-authored-by: Roy Nieterau --- openpype/plugins/inventory/remove_and_load.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/inventory/remove_and_load.py b/openpype/plugins/inventory/remove_and_load.py index d465154187..015f80cd4e 100644 --- a/openpype/plugins/inventory/remove_and_load.py +++ b/openpype/plugins/inventory/remove_and_load.py @@ -35,7 +35,10 @@ class RemoveAndLoad(InventoryAction): representation = get_representation_by_id( project_name, container["representation"] ) - assert representation, "Representation not found" + if not representation: + self.log.warning( + "Skipping remove and load because representation id is not" + " found in database: '{}'".format(container["representation"]) # Remove container remove_container(container) From 9a2a25a957cf25ed7fc5f1bcf10c3d15dc6b505c Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Mon, 8 May 2023 18:05:18 +0200 Subject: [PATCH 203/360] Added continue statement + linting --- openpype/plugins/inventory/remove_and_load.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/inventory/remove_and_load.py b/openpype/plugins/inventory/remove_and_load.py index 015f80cd4e..062a44354b 100644 --- a/openpype/plugins/inventory/remove_and_load.py +++ b/openpype/plugins/inventory/remove_and_load.py @@ -18,7 +18,6 @@ class RemoveAndLoad(InventoryAction): def process(self, containers): project_name = get_current_project_name() for container in containers: - # Get loader loader_name = container["loader"] loaders = discover_loader_plugins(project_name=project_name) @@ -38,7 +37,11 @@ class RemoveAndLoad(InventoryAction): if not representation: self.log.warning( "Skipping remove and load because representation id is not" - " found in database: '{}'".format(container["representation"]) + " found in database: '{}'".format( + container["representation"] + ) + ) + continue # Remove container remove_container(container) From 41bd47d9b53335ed5b806a548b0589d219b86415 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 9 May 2023 11:01:18 +0100 Subject: [PATCH 204/360] Refactor code to use AyonAssetContainer instead of AssetContainer --- openpype/hosts/unreal/plugins/load/load_layout.py | 6 +++--- openpype/hosts/unreal/plugins/load/load_uasset.py | 2 +- openpype/hosts/unreal/plugins/publish/extract_layout.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index d4910f91b6..e5f32c3412 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -50,7 +50,7 @@ class LayoutLoader(plugin.Loader): # Get all the asset containers for a in asset_content: obj = ar.get_asset_by_object_path(a) - if obj.get_asset().get_class().get_name() == 'AssetContainer': + if obj.get_asset().get_class().get_name() == 'AyonAssetContainer': asset_containers.append(obj) return asset_containers @@ -338,7 +338,7 @@ class LayoutLoader(plugin.Loader): ).replace('\\', '/') _filter = unreal.ARFilter( - class_names=["AssetContainer"], + class_names=["AyonAssetContainer"], package_paths=[anim_path], recursive_paths=False) containers = ar.get_assets(_filter) @@ -519,7 +519,7 @@ class LayoutLoader(plugin.Loader): for asset in assets: obj = ar.get_asset_by_object_path(asset).get_asset() - if obj.get_class().get_name() == 'AssetContainer': + if obj.get_class().get_name() == 'AyonAssetContainer': container = obj if obj.get_class().get_name() == 'Skeleton': skeleton = obj diff --git a/openpype/hosts/unreal/plugins/load/load_uasset.py b/openpype/hosts/unreal/plugins/load/load_uasset.py index b1a4fc6971..7606bc14e4 100644 --- a/openpype/hosts/unreal/plugins/load/load_uasset.py +++ b/openpype/hosts/unreal/plugins/load/load_uasset.py @@ -107,7 +107,7 @@ class UAssetLoader(plugin.Loader): for asset in asset_content: obj = ar.get_asset_by_object_path(asset).get_asset() - if not obj.get_class().get_name() == 'AssetContainer': + if not obj.get_class().get_name() == 'AyonAssetContainer': unreal.EditorAssetLibrary.delete_asset(asset) update_filepath = get_representation_path(representation) diff --git a/openpype/hosts/unreal/plugins/publish/extract_layout.py b/openpype/hosts/unreal/plugins/publish/extract_layout.py index cac7991f00..57e7957575 100644 --- a/openpype/hosts/unreal/plugins/publish/extract_layout.py +++ b/openpype/hosts/unreal/plugins/publish/extract_layout.py @@ -48,7 +48,7 @@ class ExtractLayout(publish.Extractor): # Search the reference to the Asset Container for the object path = unreal.Paths.get_path(mesh.get_path_name()) filter = unreal.ARFilter( - class_names=["AssetContainer"], package_paths=[path]) + class_names=["AyonAssetContainer"], package_paths=[path]) ar = unreal.AssetRegistryHelpers.get_asset_registry() try: asset_container = ar.get_assets(filter)[0].get_asset() From 5d7bf26c8c0d3476eda5a4f6762cbfdb32858b58 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 9 May 2023 16:17:47 +0100 Subject: [PATCH 205/360] Added AyonPublishInstance class and its Factory --- .../Ayon/Private/AyonPublishInstance.cpp | 203 +++++++++++++++++ .../Private/AyonPublishInstanceFactory.cpp | 23 ++ .../OpenPypePublishInstanceFactory.cpp | 23 -- .../Source/Ayon/Public/AyonPublishInstance.h | 103 +++++++++ .../Ayon/Public/AyonPublishInstanceFactory.h} | 6 +- .../Ayon/Private/AyonPublishInstance.cpp | 204 ++++++++++++++++++ .../Private/AyonPublishInstanceFactory.cpp | 23 ++ .../OpenPypePublishInstanceFactory.cpp | 23 -- .../Source/Ayon/Public/AyonPublishInstance.h | 104 +++++++++ .../Ayon/Public/AyonPublishInstanceFactory.h} | 6 +- .../Ayon/Private/AyonPublishInstance.cpp | 204 ++++++++++++++++++ .../Private/AyonPublishInstanceFactory.cpp | 23 ++ .../OpenPypePublishInstanceFactory.cpp | 23 -- .../Source/Ayon/Public/AyonPublishInstance.h | 104 +++++++++ .../Ayon/Public/AyonPublishInstanceFactory.h} | 6 +- 15 files changed, 1000 insertions(+), 78 deletions(-) create mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonPublishInstance.cpp create mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonPublishInstanceFactory.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp create mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonPublishInstance.h rename openpype/hosts/unreal/integration/{UE_5.1/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h => UE_4.27/Ayon/Source/Ayon/Public/AyonPublishInstanceFactory.h} (71%) create mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonPublishInstance.cpp create mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonPublishInstanceFactory.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp create mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonPublishInstance.h rename openpype/hosts/unreal/integration/{UE_4.27/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h => UE_5.0/Ayon/Source/Ayon/Public/AyonPublishInstanceFactory.h} (71%) create mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonPublishInstance.cpp create mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonPublishInstanceFactory.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp create mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonPublishInstance.h rename openpype/hosts/unreal/integration/{UE_5.0/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h => UE_5.1/Ayon/Source/Ayon/Public/AyonPublishInstanceFactory.h} (71%) diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonPublishInstance.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonPublishInstance.cpp new file mode 100644 index 0000000000..d7550e2ed1 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonPublishInstance.cpp @@ -0,0 +1,203 @@ +// Copyright 2023, Ayon, All rights reserved. +// Deprecation warning: this is left here just for backwards compatibility +// and will be removed in next versions of Ayon. +#pragma once + +#include "AyonPublishInstance.h" +#include "AssetRegistryModule.h" +#include "AyonLib.h" +#include "AyonSettings.h" +#include "Framework/Notifications/NotificationManager.h" +#include "Widgets/Notifications/SNotificationList.h" + +//Moves all the invalid pointers to the end to prepare them for the shrinking +#define REMOVE_INVALID_ENTRIES(VAR) VAR.CompactStable(); \ + VAR.Shrink(); + +UAyonPublishInstance::UAyonPublishInstance(const FObjectInitializer& ObjectInitializer) + : UPrimaryDataAsset(ObjectInitializer) +{ + const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked< + FAssetRegistryModule>("AssetRegistry"); + + const FPropertyEditorModule& PropertyEditorModule = FModuleManager::LoadModuleChecked( + "PropertyEditor"); + + FString Left, Right; + GetPathName().Split("/" + GetName(), &Left, &Right); + + FARFilter Filter; + Filter.PackagePaths.Emplace(FName(Left)); + + TArray FoundAssets; + AssetRegistryModule.GetRegistry().GetAssets(Filter, FoundAssets); + + for (const FAssetData& AssetData : FoundAssets) + OnAssetCreated(AssetData); + + REMOVE_INVALID_ENTRIES(AssetDataInternal) + REMOVE_INVALID_ENTRIES(AssetDataExternal) + + AssetRegistryModule.Get().OnAssetAdded().AddUObject(this, &UAyonPublishInstance::OnAssetCreated); + AssetRegistryModule.Get().OnAssetRemoved().AddUObject(this, &UAyonPublishInstance::OnAssetRemoved); + AssetRegistryModule.Get().OnAssetUpdated().AddUObject(this, &UAyonPublishInstance::OnAssetUpdated); + +#ifdef WITH_EDITOR + ColorAyonDirs(); +#endif + +} + +void UAyonPublishInstance::OnAssetCreated(const FAssetData& InAssetData) +{ + TArray split; + + UObject* Asset = InAssetData.GetAsset(); + + if (!IsValid(Asset)) + { + UE_LOG(LogAssetData, Warning, TEXT("Asset \"%s\" is not valid! Skipping the addition."), + *InAssetData.ObjectPath.ToString()); + return; + } + + const bool result = IsUnderSameDir(Asset) && Cast(Asset) == nullptr; + + if (result) + { + if (AssetDataInternal.Emplace(Asset).IsValidId()) + { + UE_LOG(LogTemp, Log, TEXT("Added an Asset to PublishInstance - Publish Instance: %s, Asset %s"), + *this->GetName(), *Asset->GetName()); + } + } +} + +void UAyonPublishInstance::OnAssetRemoved(const FAssetData& InAssetData) +{ + if (Cast(InAssetData.GetAsset()) == nullptr) + { + if (AssetDataInternal.Contains(nullptr)) + { + AssetDataInternal.Remove(nullptr); + REMOVE_INVALID_ENTRIES(AssetDataInternal) + } + else + { + AssetDataExternal.Remove(nullptr); + REMOVE_INVALID_ENTRIES(AssetDataExternal) + } + } +} + +void UAyonPublishInstance::OnAssetUpdated(const FAssetData& InAssetData) +{ + REMOVE_INVALID_ENTRIES(AssetDataInternal); + REMOVE_INVALID_ENTRIES(AssetDataExternal); +} + +bool UAyonPublishInstance::IsUnderSameDir(const UObject* InAsset) const +{ + FString ThisLeft, ThisRight; + this->GetPathName().Split(this->GetName(), &ThisLeft, &ThisRight); + + return InAsset->GetPathName().StartsWith(ThisLeft); +} + +#ifdef WITH_EDITOR + +void UAyonPublishInstance::ColorAyonDirs() +{ + FString PathName = this->GetPathName(); + + //Check whether the path contains the defined Ayon folder + if (!PathName.Contains(TEXT("Ayon"))) return; + + //Get the base path for open pype + FString PathLeft, PathRight; + PathName.Split(FString("Ayon"), &PathLeft, &PathRight); + + if (PathLeft.IsEmpty() || PathRight.IsEmpty()) + { + UE_LOG(LogAssetData, Error, TEXT("Failed to retrieve the base Ayon directory!")) + return; + } + + PathName.RemoveFromEnd(PathRight, ESearchCase::CaseSensitive); + + //Get the current settings + const UAyonSettings* Settings = GetMutableDefault(); + + //Color the base folder + UAyonLib::SetFolderColor(PathName, Settings->GetFolderFColor(), false); + + //Get Sub paths, iterate through them and color them according to the folder color in UAyonSettings + const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked( + "AssetRegistry"); + + TArray PathList; + + AssetRegistryModule.Get().GetSubPaths(PathName, PathList, true); + + if (PathList.Num() > 0) + { + for (const FString& Path : PathList) + { + UAyonLib::SetFolderColor(Path, Settings->GetFolderFColor(), false); + } + } +} + +void UAyonPublishInstance::SendNotification(const FString& Text) const +{ + FNotificationInfo Info{FText::FromString(Text)}; + + Info.bFireAndForget = true; + Info.bUseLargeFont = false; + Info.bUseThrobber = false; + Info.bUseSuccessFailIcons = false; + Info.ExpireDuration = 4.f; + Info.FadeOutDuration = 2.f; + + FSlateNotificationManager::Get().AddNotification(Info); + + UE_LOG(LogAssetData, Warning, + TEXT( + "Removed duplicated asset from the AssetsDataExternal in Container \"%s\", Asset is already included in the AssetDataInternal!" + ), *GetName() + ) +} + + +void UAyonPublishInstance::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) +{ + Super::PostEditChangeProperty(PropertyChangedEvent); + + if (PropertyChangedEvent.ChangeType == EPropertyChangeType::ValueSet && + PropertyChangedEvent.Property->GetFName() == GET_MEMBER_NAME_CHECKED( + UAyonPublishInstance, AssetDataExternal)) + { + // Check for duplicated assets + for (const auto& Asset : AssetDataInternal) + { + if (AssetDataExternal.Contains(Asset)) + { + AssetDataExternal.Remove(Asset); + return SendNotification( + "You are not allowed to add assets into AssetDataExternal which are already included in AssetDataInternal!"); + } + } + + // Check if no UAyonPublishInstance type assets are included + for (const auto& Asset : AssetDataExternal) + { + if (Cast(Asset.Get()) != nullptr) + { + AssetDataExternal.Remove(Asset); + return SendNotification("You are not allowed to add publish instances!"); + } + } + } +} + +#endif diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonPublishInstanceFactory.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonPublishInstanceFactory.cpp new file mode 100644 index 0000000000..f79c428a6d --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonPublishInstanceFactory.cpp @@ -0,0 +1,23 @@ +// Copyright 2023, Ayon, All rights reserved. +// Deprecation warning: this is left here just for backwards compatibility +// and will be removed in next versions of Ayon. +#include "AyonPublishInstanceFactory.h" +#include "AyonPublishInstance.h" + +UAyonPublishInstanceFactory::UAyonPublishInstanceFactory(const FObjectInitializer& ObjectInitializer) + : UFactory(ObjectInitializer) +{ + SupportedClass = UAyonPublishInstance::StaticClass(); + bCreateNew = false; + bEditorImport = true; +} + +UObject* UAyonPublishInstanceFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) +{ + check(InClass->IsChildOf(UAyonPublishInstance::StaticClass())); + return NewObject(InParent, InClass, InName, Flags); +} + +bool UAyonPublishInstanceFactory::ShouldShowInNewMenu() const { + return false; +} diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp deleted file mode 100644 index 4b4492bd20..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -// Deprecation warning: this is left here just for backwards compatibility -// and will be removed in next versions of Ayon. -#include "OpenPypePublishInstanceFactory.h" -#include "OpenPypePublishInstance.h" - -UOpenPypePublishInstanceFactory::UOpenPypePublishInstanceFactory(const FObjectInitializer& ObjectInitializer) - : UFactory(ObjectInitializer) -{ - SupportedClass = UOpenPypePublishInstance::StaticClass(); - bCreateNew = false; - bEditorImport = true; -} - -UObject* UOpenPypePublishInstanceFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) -{ - check(InClass->IsChildOf(UOpenPypePublishInstance::StaticClass())); - return NewObject(InParent, InClass, InName, Flags); -} - -bool UOpenPypePublishInstanceFactory::ShouldShowInNewMenu() const { - return false; -} diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonPublishInstance.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonPublishInstance.h new file mode 100644 index 0000000000..0a0628c3ec --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonPublishInstance.h @@ -0,0 +1,103 @@ +// Copyright 2023, Ayon, All rights reserved. +// Deprecation warning: this is left here just for backwards compatibility +// and will be removed in next versions of Ayon. +#pragma once + +#include "AyonPublishInstance.generated.h" + + +UCLASS(Blueprintable) +class AYON_API UAyonPublishInstance : public UPrimaryDataAsset +{ + GENERATED_UCLASS_BODY() + +public: + /** + * Retrieves all the assets which are monitored by the Publish Instance (Monitors assets in the directory which is + * placed in) + * + * @return - Set of UObjects. Careful! They are returning raw pointers. Seems like an issue in UE5 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") + TSet GetInternalAssets() const + { + //For some reason it can only return Raw Pointers? Seems like an issue which they haven't fixed. + TSet ResultSet; + + for (const auto& Asset : AssetDataInternal) + ResultSet.Add(Asset.LoadSynchronous()); + + return ResultSet; + } + + /** + * Retrieves all the assets which have been added manually by the Publish Instance + * + * @return - TSet of assets (UObjects). Careful! They are returning raw pointers. Seems like an issue in UE5 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") + TSet GetExternalAssets() const + { + //For some reason it can only return Raw Pointers? Seems like an issue which they haven't fixed. + TSet ResultSet; + + for (const auto& Asset : AssetDataExternal) + ResultSet.Add(Asset.LoadSynchronous()); + + return ResultSet; + } + + /** + * Function for returning all the assets in the container combined. + * + * @return Returns all the internal and externally added assets into one set (TSet of UObjects). Careful! They are + * returning raw pointers. Seems like an issue in UE5 + * + * @attention If the bAddExternalAssets variable is false, external assets won't be included! + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") + TSet GetAllAssets() const + { + const TSet>& IteratedSet = bAddExternalAssets + ? AssetDataInternal.Union(AssetDataExternal) + : AssetDataInternal; + + //Create a new TSet only with raw pointers. + TSet ResultSet; + + for (auto& Asset : IteratedSet) + ResultSet.Add(Asset.LoadSynchronous()); + + return ResultSet; + } + +private: + UPROPERTY(VisibleAnywhere, Category="Assets") + TSet> AssetDataInternal; + + /** + * This property allows exposing the array to include other assets from any other directory than what it's currently + * monitoring. NOTE: that these assets have to be added manually! They are not automatically registered or added! + */ + UPROPERTY(EditAnywhere, Category = "Assets") + bool bAddExternalAssets = false; + + UPROPERTY(EditAnywhere, meta=(EditCondition="bAddExternalAssets"), Category="Assets") + TSet> AssetDataExternal; + + + void OnAssetCreated(const FAssetData& InAssetData); + void OnAssetRemoved(const FAssetData& InAssetData); + void OnAssetUpdated(const FAssetData& InAssetData); + + bool IsUnderSameDir(const UObject* InAsset) const; + +#ifdef WITH_EDITOR + + void ColorAyonDirs(); + + void SendNotification(const FString& Text) const; + virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; + +#endif +}; diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonPublishInstanceFactory.h similarity index 71% rename from openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonPublishInstanceFactory.h index 5a02a51d1c..3cef8e76b2 100644 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h +++ b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonPublishInstanceFactory.h @@ -5,18 +5,18 @@ #include "CoreMinimal.h" #include "Factories/Factory.h" -#include "OpenPypePublishInstanceFactory.generated.h" +#include "AyonPublishInstanceFactory.generated.h" /** * */ UCLASS() -class AYON_API UOpenPypePublishInstanceFactory : public UFactory +class AYON_API UAyonPublishInstanceFactory : public UFactory { GENERATED_BODY() public: - UOpenPypePublishInstanceFactory(const FObjectInitializer& ObjectInitializer); + UAyonPublishInstanceFactory(const FObjectInitializer& ObjectInitializer); virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; virtual bool ShouldShowInNewMenu() const override; }; diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonPublishInstance.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonPublishInstance.cpp new file mode 100644 index 0000000000..8d34090a15 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonPublishInstance.cpp @@ -0,0 +1,204 @@ +// Copyright 2023, Ayon, All rights reserved. +// Deprecation warning: this is left here just for backwards compatibility +// and will be removed in next versions of Ayon. +#pragma once + +#include "AyonPublishInstance.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "AssetToolsModule.h" +#include "Framework/Notifications/NotificationManager.h" +#include "AyonLib.h" +#include "AyonSettings.h" +#include "Widgets/Notifications/SNotificationList.h" + + +//Moves all the invalid pointers to the end to prepare them for the shrinking +#define REMOVE_INVALID_ENTRIES(VAR) VAR.CompactStable(); \ + VAR.Shrink(); + +UAyonPublishInstance::UAyonPublishInstance(const FObjectInitializer& ObjectInitializer) + : UPrimaryDataAsset(ObjectInitializer) +{ + const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked< + FAssetRegistryModule>("AssetRegistry"); + + const FPropertyEditorModule& PropertyEditorModule = FModuleManager::LoadModuleChecked( + "PropertyEditor"); + + FString Left, Right; + GetPathName().Split("/" + GetName(), &Left, &Right); + + FARFilter Filter; + Filter.PackagePaths.Emplace(FName(Left)); + + TArray FoundAssets; + AssetRegistryModule.GetRegistry().GetAssets(Filter, FoundAssets); + + for (const FAssetData& AssetData : FoundAssets) + OnAssetCreated(AssetData); + + REMOVE_INVALID_ENTRIES(AssetDataInternal) + REMOVE_INVALID_ENTRIES(AssetDataExternal) + + AssetRegistryModule.Get().OnAssetAdded().AddUObject(this, &UAyonPublishInstance::OnAssetCreated); + AssetRegistryModule.Get().OnAssetRemoved().AddUObject(this, &UAyonPublishInstance::OnAssetRemoved); + AssetRegistryModule.Get().OnAssetUpdated().AddUObject(this, &UAyonPublishInstance::OnAssetUpdated); + +#ifdef WITH_EDITOR + ColorAyonDirs(); +#endif +} + +void UAyonPublishInstance::OnAssetCreated(const FAssetData& InAssetData) +{ + TArray split; + + UObject* Asset = InAssetData.GetAsset(); + + if (!IsValid(Asset)) + { + UE_LOG(LogAssetData, Warning, TEXT("Asset \"%s\" is not valid! Skipping the addition."), + *InAssetData.ObjectPath.ToString()); + return; + } + + const bool result = IsUnderSameDir(Asset) && Cast(Asset) == nullptr; + + if (result) + { + if (AssetDataInternal.Emplace(Asset).IsValidId()) + { + UE_LOG(LogTemp, Log, TEXT("Added an Asset to PublishInstance - Publish Instance: %s, Asset %s"), + *this->GetName(), *Asset->GetName()); + } + } +} + +void UAyonPublishInstance::OnAssetRemoved(const FAssetData& InAssetData) +{ + if (Cast(InAssetData.GetAsset()) == nullptr) + { + if (AssetDataInternal.Contains(nullptr)) + { + AssetDataInternal.Remove(nullptr); + REMOVE_INVALID_ENTRIES(AssetDataInternal) + } + else + { + AssetDataExternal.Remove(nullptr); + REMOVE_INVALID_ENTRIES(AssetDataExternal) + } + } +} + +void UAyonPublishInstance::OnAssetUpdated(const FAssetData& InAssetData) +{ + REMOVE_INVALID_ENTRIES(AssetDataInternal); + REMOVE_INVALID_ENTRIES(AssetDataExternal); +} + +bool UAyonPublishInstance::IsUnderSameDir(const UObject* InAsset) const +{ + FString ThisLeft, ThisRight; + this->GetPathName().Split(this->GetName(), &ThisLeft, &ThisRight); + + return InAsset->GetPathName().StartsWith(ThisLeft); +} + +#ifdef WITH_EDITOR + +void UAyonPublishInstance::ColorAyonDirs() +{ + FString PathName = this->GetPathName(); + + //Check whether the path contains the defined Ayon folder + if (!PathName.Contains(TEXT("Ayon"))) return; + + //Get the base path for open pype + FString PathLeft, PathRight; + PathName.Split(FString("Ayon"), &PathLeft, &PathRight); + + if (PathLeft.IsEmpty() || PathRight.IsEmpty()) + { + UE_LOG(LogAssetData, Error, TEXT("Failed to retrieve the base Ayon directory!")) + return; + } + + PathName.RemoveFromEnd(PathRight, ESearchCase::CaseSensitive); + + //Get the current settings + const UAyonSettings* Settings = GetMutableDefault(); + + //Color the base folder + UAyonLib::SetFolderColor(PathName, Settings->GetFolderFColor(), false); + + //Get Sub paths, iterate through them and color them according to the folder color in UAyonSettings + const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked( + "AssetRegistry"); + + TArray PathList; + + AssetRegistryModule.Get().GetSubPaths(PathName, PathList, true); + + if (PathList.Num() > 0) + { + for (const FString& Path : PathList) + { + UAyonLib::SetFolderColor(Path, Settings->GetFolderFColor(), false); + } + } +} + +void UAyonPublishInstance::SendNotification(const FString& Text) const +{ + FNotificationInfo Info{FText::FromString(Text)}; + + Info.bFireAndForget = true; + Info.bUseLargeFont = false; + Info.bUseThrobber = false; + Info.bUseSuccessFailIcons = false; + Info.ExpireDuration = 4.f; + Info.FadeOutDuration = 2.f; + + FSlateNotificationManager::Get().AddNotification(Info); + + UE_LOG(LogAssetData, Warning, + TEXT( + "Removed duplicated asset from the AssetsDataExternal in Container \"%s\", Asset is already included in the AssetDataInternal!" + ), *GetName() + ) +} + + +void UAyonPublishInstance::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) +{ + Super::PostEditChangeProperty(PropertyChangedEvent); + + if (PropertyChangedEvent.ChangeType == EPropertyChangeType::ValueSet && + PropertyChangedEvent.Property->GetFName() == GET_MEMBER_NAME_CHECKED( + UAyonPublishInstance, AssetDataExternal)) + { + // Check for duplicated assets + for (const auto& Asset : AssetDataInternal) + { + if (AssetDataExternal.Contains(Asset)) + { + AssetDataExternal.Remove(Asset); + return SendNotification( + "You are not allowed to add assets into AssetDataExternal which are already included in AssetDataInternal!"); + } + } + + // Check if no UAyonPublishInstance type assets are included + for (const auto& Asset : AssetDataExternal) + { + if (Cast(Asset.Get()) != nullptr) + { + AssetDataExternal.Remove(Asset); + return SendNotification("You are not allowed to add publish instances!"); + } + } + } +} + +#endif diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonPublishInstanceFactory.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonPublishInstanceFactory.cpp new file mode 100644 index 0000000000..f79c428a6d --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonPublishInstanceFactory.cpp @@ -0,0 +1,23 @@ +// Copyright 2023, Ayon, All rights reserved. +// Deprecation warning: this is left here just for backwards compatibility +// and will be removed in next versions of Ayon. +#include "AyonPublishInstanceFactory.h" +#include "AyonPublishInstance.h" + +UAyonPublishInstanceFactory::UAyonPublishInstanceFactory(const FObjectInitializer& ObjectInitializer) + : UFactory(ObjectInitializer) +{ + SupportedClass = UAyonPublishInstance::StaticClass(); + bCreateNew = false; + bEditorImport = true; +} + +UObject* UAyonPublishInstanceFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) +{ + check(InClass->IsChildOf(UAyonPublishInstance::StaticClass())); + return NewObject(InParent, InClass, InName, Flags); +} + +bool UAyonPublishInstanceFactory::ShouldShowInNewMenu() const { + return false; +} diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp deleted file mode 100644 index 4b4492bd20..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -// Deprecation warning: this is left here just for backwards compatibility -// and will be removed in next versions of Ayon. -#include "OpenPypePublishInstanceFactory.h" -#include "OpenPypePublishInstance.h" - -UOpenPypePublishInstanceFactory::UOpenPypePublishInstanceFactory(const FObjectInitializer& ObjectInitializer) - : UFactory(ObjectInitializer) -{ - SupportedClass = UOpenPypePublishInstance::StaticClass(); - bCreateNew = false; - bEditorImport = true; -} - -UObject* UOpenPypePublishInstanceFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) -{ - check(InClass->IsChildOf(UOpenPypePublishInstance::StaticClass())); - return NewObject(InParent, InClass, InName, Flags); -} - -bool UOpenPypePublishInstanceFactory::ShouldShowInNewMenu() const { - return false; -} diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonPublishInstance.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonPublishInstance.h new file mode 100644 index 0000000000..c89388036f --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonPublishInstance.h @@ -0,0 +1,104 @@ +// Copyright 2023, Ayon, All rights reserved. +// Deprecation warning: this is left here just for backwards compatibility +// and will be removed in next versions of Ayon. +#pragma once + +#include "AyonPublishInstance.generated.h" + + +UCLASS(Blueprintable) +class AYON_API UAyonPublishInstance : public UPrimaryDataAsset +{ + GENERATED_UCLASS_BODY() + +public: + /** + /** + * Retrieves all the assets which are monitored by the Publish Instance (Monitors assets in the directory which is + * placed in) + * + * @return - Set of UObjects. Careful! They are returning raw pointers. Seems like an issue in UE5 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") + TSet GetInternalAssets() const + { + //For some reason it can only return Raw Pointers? Seems like an issue which they haven't fixed. + TSet ResultSet; + + for (const auto& Asset : AssetDataInternal) + ResultSet.Add(Asset.LoadSynchronous()); + + return ResultSet; + } + + /** + * Retrieves all the assets which have been added manually by the Publish Instance + * + * @return - TSet of assets (UObjects). Careful! They are returning raw pointers. Seems like an issue in UE5 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") + TSet GetExternalAssets() const + { + //For some reason it can only return Raw Pointers? Seems like an issue which they haven't fixed. + TSet ResultSet; + + for (const auto& Asset : AssetDataExternal) + ResultSet.Add(Asset.LoadSynchronous()); + + return ResultSet; + } + + /** + * Function for returning all the assets in the container combined. + * + * @return Returns all the internal and externally added assets into one set (TSet of UObjects). Careful! They are + * returning raw pointers. Seems like an issue in UE5 + * + * @attention If the bAddExternalAssets variable is false, external assets won't be included! + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") + TSet GetAllAssets() const + { + const TSet>& IteratedSet = bAddExternalAssets + ? AssetDataInternal.Union(AssetDataExternal) + : AssetDataInternal; + + //Create a new TSet only with raw pointers. + TSet ResultSet; + + for (auto& Asset : IteratedSet) + ResultSet.Add(Asset.LoadSynchronous()); + + return ResultSet; + } + +private: + UPROPERTY(VisibleAnywhere, Category="Assets") + TSet> AssetDataInternal; + + /** + * This property allows exposing the array to include other assets from any other directory than what it's currently + * monitoring. NOTE: that these assets have to be added manually! They are not automatically registered or added! + */ + UPROPERTY(EditAnywhere, Category = "Assets") + bool bAddExternalAssets = false; + + UPROPERTY(EditAnywhere, meta=(EditCondition="bAddExternalAssets"), Category="Assets") + TSet> AssetDataExternal; + + + void OnAssetCreated(const FAssetData& InAssetData); + void OnAssetRemoved(const FAssetData& InAssetData); + void OnAssetUpdated(const FAssetData& InAssetData); + + bool IsUnderSameDir(const UObject* InAsset) const; + +#ifdef WITH_EDITOR + + void ColorAyonDirs(); + + void SendNotification(const FString& Text) const; + virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; + +#endif +}; diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonPublishInstanceFactory.h similarity index 71% rename from openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonPublishInstanceFactory.h index 5a02a51d1c..3cef8e76b2 100644 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h +++ b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonPublishInstanceFactory.h @@ -5,18 +5,18 @@ #include "CoreMinimal.h" #include "Factories/Factory.h" -#include "OpenPypePublishInstanceFactory.generated.h" +#include "AyonPublishInstanceFactory.generated.h" /** * */ UCLASS() -class AYON_API UOpenPypePublishInstanceFactory : public UFactory +class AYON_API UAyonPublishInstanceFactory : public UFactory { GENERATED_BODY() public: - UOpenPypePublishInstanceFactory(const FObjectInitializer& ObjectInitializer); + UAyonPublishInstanceFactory(const FObjectInitializer& ObjectInitializer); virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; virtual bool ShouldShowInNewMenu() const override; }; diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonPublishInstance.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonPublishInstance.cpp new file mode 100644 index 0000000000..d1b47a19d4 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonPublishInstance.cpp @@ -0,0 +1,204 @@ +// Copyright 2023, Ayon, All rights reserved. +// Deprecation warning: this is left here just for backwards compatibility +// and will be removed in next versions of Ayon. +#pragma once + +#include "AyonPublishInstance.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "AssetToolsModule.h" +#include "Framework/Notifications/NotificationManager.h" +#include "AyonLib.h" +#include "AyonSettings.h" +#include "Widgets/Notifications/SNotificationList.h" + + +//Moves all the invalid pointers to the end to prepare them for the shrinking +#define REMOVE_INVALID_ENTRIES(VAR) VAR.CompactStable(); \ + VAR.Shrink(); + +UAyonPublishInstance::UAyonPublishInstance(const FObjectInitializer& ObjectInitializer) + : UPrimaryDataAsset(ObjectInitializer) +{ + const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked< + FAssetRegistryModule>("AssetRegistry"); + + const FPropertyEditorModule& PropertyEditorModule = FModuleManager::LoadModuleChecked( + "PropertyEditor"); + + FString Left, Right; + GetPathName().Split("/" + GetName(), &Left, &Right); + + FARFilter Filter; + Filter.PackagePaths.Emplace(FName(Left)); + + TArray FoundAssets; + AssetRegistryModule.GetRegistry().GetAssets(Filter, FoundAssets); + + for (const FAssetData& AssetData : FoundAssets) + OnAssetCreated(AssetData); + + REMOVE_INVALID_ENTRIES(AssetDataInternal) + REMOVE_INVALID_ENTRIES(AssetDataExternal) + + AssetRegistryModule.Get().OnAssetAdded().AddUObject(this, &UAyonPublishInstance::OnAssetCreated); + AssetRegistryModule.Get().OnAssetRemoved().AddUObject(this, &UAyonPublishInstance::OnAssetRemoved); + AssetRegistryModule.Get().OnAssetUpdated().AddUObject(this, &UAyonPublishInstance::OnAssetUpdated); + +#ifdef WITH_EDITOR + ColorAyonDirs(); +#endif +} + +void UAyonPublishInstance::OnAssetCreated(const FAssetData& InAssetData) +{ + TArray split; + + UObject* Asset = InAssetData.GetAsset(); + + if (!IsValid(Asset)) + { + UE_LOG(LogAssetData, Warning, TEXT("Asset \"%s\" is not valid! Skipping the addition."), + *InAssetData.GetSoftObjectPath().ToString()); + return; + } + + const bool result = IsUnderSameDir(Asset) && Cast(Asset) == nullptr; + + if (result) + { + if (AssetDataInternal.Emplace(Asset).IsValidId()) + { + UE_LOG(LogTemp, Log, TEXT("Added an Asset to PublishInstance - Publish Instance: %s, Asset %s"), + *this->GetName(), *Asset->GetName()); + } + } +} + +void UAyonPublishInstance::OnAssetRemoved(const FAssetData& InAssetData) +{ + if (Cast(InAssetData.GetAsset()) == nullptr) + { + if (AssetDataInternal.Contains(nullptr)) + { + AssetDataInternal.Remove(nullptr); + REMOVE_INVALID_ENTRIES(AssetDataInternal) + } + else + { + AssetDataExternal.Remove(nullptr); + REMOVE_INVALID_ENTRIES(AssetDataExternal) + } + } +} + +void UAyonPublishInstance::OnAssetUpdated(const FAssetData& InAssetData) +{ + REMOVE_INVALID_ENTRIES(AssetDataInternal); + REMOVE_INVALID_ENTRIES(AssetDataExternal); +} + +bool UAyonPublishInstance::IsUnderSameDir(const UObject* InAsset) const +{ + FString ThisLeft, ThisRight; + this->GetPathName().Split(this->GetName(), &ThisLeft, &ThisRight); + + return InAsset->GetPathName().StartsWith(ThisLeft); +} + +#ifdef WITH_EDITOR + +void UAyonPublishInstance::ColorAyonDirs() +{ + FString PathName = this->GetPathName(); + + //Check whether the path contains the defined Ayon folder + if (!PathName.Contains(TEXT("Ayon"))) return; + + //Get the base path for open pype + FString PathLeft, PathRight; + PathName.Split(FString("Ayon"), &PathLeft, &PathRight); + + if (PathLeft.IsEmpty() || PathRight.IsEmpty()) + { + UE_LOG(LogAssetData, Error, TEXT("Failed to retrieve the base Ayon directory!")) + return; + } + + PathName.RemoveFromEnd(PathRight, ESearchCase::CaseSensitive); + + //Get the current settings + const UAyonSettings* Settings = GetMutableDefault(); + + //Color the base folder + UAyonLib::SetFolderColor(PathName, Settings->GetFolderFColor(), false); + + //Get Sub paths, iterate through them and color them according to the folder color in UAyonSettings + const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked( + "AssetRegistry"); + + TArray PathList; + + AssetRegistryModule.Get().GetSubPaths(PathName, PathList, true); + + if (PathList.Num() > 0) + { + for (const FString& Path : PathList) + { + UAyonLib::SetFolderColor(Path, Settings->GetFolderFColor(), false); + } + } +} + +void UAyonPublishInstance::SendNotification(const FString& Text) const +{ + FNotificationInfo Info{FText::FromString(Text)}; + + Info.bFireAndForget = true; + Info.bUseLargeFont = false; + Info.bUseThrobber = false; + Info.bUseSuccessFailIcons = false; + Info.ExpireDuration = 4.f; + Info.FadeOutDuration = 2.f; + + FSlateNotificationManager::Get().AddNotification(Info); + + UE_LOG(LogAssetData, Warning, + TEXT( + "Removed duplicated asset from the AssetsDataExternal in Container \"%s\", Asset is already included in the AssetDataInternal!" + ), *GetName() + ) +} + + +void UAyonPublishInstance::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) +{ + Super::PostEditChangeProperty(PropertyChangedEvent); + + if (PropertyChangedEvent.ChangeType == EPropertyChangeType::ValueSet && + PropertyChangedEvent.Property->GetFName() == GET_MEMBER_NAME_CHECKED( + UAyonPublishInstance, AssetDataExternal)) + { + // Check for duplicated assets + for (const auto& Asset : AssetDataInternal) + { + if (AssetDataExternal.Contains(Asset)) + { + AssetDataExternal.Remove(Asset); + return SendNotification( + "You are not allowed to add assets into AssetDataExternal which are already included in AssetDataInternal!"); + } + } + + // Check if no UAyonPublishInstance type assets are included + for (const auto& Asset : AssetDataExternal) + { + if (Cast(Asset.Get()) != nullptr) + { + AssetDataExternal.Remove(Asset); + return SendNotification("You are not allowed to add publish instances!"); + } + } + } +} + +#endif diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonPublishInstanceFactory.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonPublishInstanceFactory.cpp new file mode 100644 index 0000000000..f79c428a6d --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonPublishInstanceFactory.cpp @@ -0,0 +1,23 @@ +// Copyright 2023, Ayon, All rights reserved. +// Deprecation warning: this is left here just for backwards compatibility +// and will be removed in next versions of Ayon. +#include "AyonPublishInstanceFactory.h" +#include "AyonPublishInstance.h" + +UAyonPublishInstanceFactory::UAyonPublishInstanceFactory(const FObjectInitializer& ObjectInitializer) + : UFactory(ObjectInitializer) +{ + SupportedClass = UAyonPublishInstance::StaticClass(); + bCreateNew = false; + bEditorImport = true; +} + +UObject* UAyonPublishInstanceFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) +{ + check(InClass->IsChildOf(UAyonPublishInstance::StaticClass())); + return NewObject(InParent, InClass, InName, Flags); +} + +bool UAyonPublishInstanceFactory::ShouldShowInNewMenu() const { + return false; +} diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp deleted file mode 100644 index 4b4492bd20..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -// Deprecation warning: this is left here just for backwards compatibility -// and will be removed in next versions of Ayon. -#include "OpenPypePublishInstanceFactory.h" -#include "OpenPypePublishInstance.h" - -UOpenPypePublishInstanceFactory::UOpenPypePublishInstanceFactory(const FObjectInitializer& ObjectInitializer) - : UFactory(ObjectInitializer) -{ - SupportedClass = UOpenPypePublishInstance::StaticClass(); - bCreateNew = false; - bEditorImport = true; -} - -UObject* UOpenPypePublishInstanceFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) -{ - check(InClass->IsChildOf(UOpenPypePublishInstance::StaticClass())); - return NewObject(InParent, InClass, InName, Flags); -} - -bool UOpenPypePublishInstanceFactory::ShouldShowInNewMenu() const { - return false; -} diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonPublishInstance.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonPublishInstance.h new file mode 100644 index 0000000000..c89388036f --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonPublishInstance.h @@ -0,0 +1,104 @@ +// Copyright 2023, Ayon, All rights reserved. +// Deprecation warning: this is left here just for backwards compatibility +// and will be removed in next versions of Ayon. +#pragma once + +#include "AyonPublishInstance.generated.h" + + +UCLASS(Blueprintable) +class AYON_API UAyonPublishInstance : public UPrimaryDataAsset +{ + GENERATED_UCLASS_BODY() + +public: + /** + /** + * Retrieves all the assets which are monitored by the Publish Instance (Monitors assets in the directory which is + * placed in) + * + * @return - Set of UObjects. Careful! They are returning raw pointers. Seems like an issue in UE5 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") + TSet GetInternalAssets() const + { + //For some reason it can only return Raw Pointers? Seems like an issue which they haven't fixed. + TSet ResultSet; + + for (const auto& Asset : AssetDataInternal) + ResultSet.Add(Asset.LoadSynchronous()); + + return ResultSet; + } + + /** + * Retrieves all the assets which have been added manually by the Publish Instance + * + * @return - TSet of assets (UObjects). Careful! They are returning raw pointers. Seems like an issue in UE5 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") + TSet GetExternalAssets() const + { + //For some reason it can only return Raw Pointers? Seems like an issue which they haven't fixed. + TSet ResultSet; + + for (const auto& Asset : AssetDataExternal) + ResultSet.Add(Asset.LoadSynchronous()); + + return ResultSet; + } + + /** + * Function for returning all the assets in the container combined. + * + * @return Returns all the internal and externally added assets into one set (TSet of UObjects). Careful! They are + * returning raw pointers. Seems like an issue in UE5 + * + * @attention If the bAddExternalAssets variable is false, external assets won't be included! + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") + TSet GetAllAssets() const + { + const TSet>& IteratedSet = bAddExternalAssets + ? AssetDataInternal.Union(AssetDataExternal) + : AssetDataInternal; + + //Create a new TSet only with raw pointers. + TSet ResultSet; + + for (auto& Asset : IteratedSet) + ResultSet.Add(Asset.LoadSynchronous()); + + return ResultSet; + } + +private: + UPROPERTY(VisibleAnywhere, Category="Assets") + TSet> AssetDataInternal; + + /** + * This property allows exposing the array to include other assets from any other directory than what it's currently + * monitoring. NOTE: that these assets have to be added manually! They are not automatically registered or added! + */ + UPROPERTY(EditAnywhere, Category = "Assets") + bool bAddExternalAssets = false; + + UPROPERTY(EditAnywhere, meta=(EditCondition="bAddExternalAssets"), Category="Assets") + TSet> AssetDataExternal; + + + void OnAssetCreated(const FAssetData& InAssetData); + void OnAssetRemoved(const FAssetData& InAssetData); + void OnAssetUpdated(const FAssetData& InAssetData); + + bool IsUnderSameDir(const UObject* InAsset) const; + +#ifdef WITH_EDITOR + + void ColorAyonDirs(); + + void SendNotification(const FString& Text) const; + virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; + +#endif +}; diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonPublishInstanceFactory.h similarity index 71% rename from openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h rename to openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonPublishInstanceFactory.h index 5a02a51d1c..3cef8e76b2 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonPublishInstanceFactory.h @@ -5,18 +5,18 @@ #include "CoreMinimal.h" #include "Factories/Factory.h" -#include "OpenPypePublishInstanceFactory.generated.h" +#include "AyonPublishInstanceFactory.generated.h" /** * */ UCLASS() -class AYON_API UOpenPypePublishInstanceFactory : public UFactory +class AYON_API UAyonPublishInstanceFactory : public UFactory { GENERATED_BODY() public: - UOpenPypePublishInstanceFactory(const FObjectInitializer& ObjectInitializer); + UAyonPublishInstanceFactory(const FObjectInitializer& ObjectInitializer); virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; virtual bool ShouldShowInNewMenu() const override; }; From 1e26b177261a0dfe9ce5baf43ba43ecec22eb735 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 9 May 2023 17:20:36 +0200 Subject: [PATCH 206/360] Fusion: rewrite collecting renders --- .../publish/collect_expected_frames.py | 50 ----- .../plugins/publish/collect_fusion_version.py | 22 -- .../plugins/publish/collect_instances.py | 28 --- .../fusion/plugins/publish/collect_render.py | 206 ++++++++++++++++++ .../fusion/plugins/publish/collect_renders.py | 33 --- 5 files changed, 206 insertions(+), 133 deletions(-) delete mode 100644 openpype/hosts/fusion/plugins/publish/collect_expected_frames.py delete mode 100644 openpype/hosts/fusion/plugins/publish/collect_fusion_version.py create mode 100644 openpype/hosts/fusion/plugins/publish/collect_render.py delete mode 100644 openpype/hosts/fusion/plugins/publish/collect_renders.py diff --git a/openpype/hosts/fusion/plugins/publish/collect_expected_frames.py b/openpype/hosts/fusion/plugins/publish/collect_expected_frames.py deleted file mode 100644 index 0ba777629f..0000000000 --- a/openpype/hosts/fusion/plugins/publish/collect_expected_frames.py +++ /dev/null @@ -1,50 +0,0 @@ -import pyblish.api -from openpype.pipeline import publish -import os - - -class CollectFusionExpectedFrames( - pyblish.api.InstancePlugin, publish.ColormanagedPyblishPluginMixin -): - """Collect all frames needed to publish expected frames""" - - order = pyblish.api.CollectorOrder + 0.5 - label = "Collect Expected Frames" - hosts = ["fusion"] - families = ["render"] - - def process(self, instance): - context = instance.context - - frame_start = context.data["frameStartHandle"] - frame_end = context.data["frameEndHandle"] - path = instance.data["path"] - output_dir = instance.data["outputDir"] - - basename = os.path.basename(path) - head, ext = os.path.splitext(basename) - files = [ - f"{head}{str(frame).zfill(4)}{ext}" - for frame in range(frame_start, frame_end + 1) - ] - repre = { - "name": ext[1:], - "ext": ext[1:], - "frameStart": f"%0{len(str(frame_end))}d" % frame_start, - "files": files, - "stagingDir": output_dir, - } - - self.set_representation_colorspace( - representation=repre, - context=context, - ) - - # review representation - if instance.data.get("review", False): - repre["tags"] = ["review"] - - # add the repre to the instance - if "representations" not in instance.data: - instance.data["representations"] = [] - instance.data["representations"].append(repre) diff --git a/openpype/hosts/fusion/plugins/publish/collect_fusion_version.py b/openpype/hosts/fusion/plugins/publish/collect_fusion_version.py deleted file mode 100644 index 65d8386f33..0000000000 --- a/openpype/hosts/fusion/plugins/publish/collect_fusion_version.py +++ /dev/null @@ -1,22 +0,0 @@ -import pyblish.api - - -class CollectFusionVersion(pyblish.api.ContextPlugin): - """Collect current comp""" - - order = pyblish.api.CollectorOrder - label = "Collect Fusion Version" - hosts = ["fusion"] - - def process(self, context): - """Collect all image sequence tools""" - - comp = context.data.get("currentComp") - if not comp: - raise RuntimeError("No comp previously collected, unable to " - "retrieve Fusion version.") - - version = comp.GetApp().Version - context.data["fusionVersion"] = version - - self.log.info("Fusion version: %s" % version) diff --git a/openpype/hosts/fusion/plugins/publish/collect_instances.py b/openpype/hosts/fusion/plugins/publish/collect_instances.py index af227f03db..4608f79420 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_instances.py +++ b/openpype/hosts/fusion/plugins/publish/collect_instances.py @@ -49,31 +49,3 @@ class CollectInstanceData(pyblish.api.InstancePlugin): if instance.data.get("review", False): self.log.info("Adding review family..") instance.data["families"].append("review") - - if instance.data["family"] == "render": - # TODO: This should probably move into a collector of - # its own for the "render" family - from openpype.hosts.fusion.api.lib import get_frame_path - comp = context.data["currentComp"] - - # This is only the case for savers currently but not - # for workfile instances. So we assume saver here. - tool = instance.data["transientData"]["tool"] - path = tool["Clip"][comp.TIME_UNDEFINED] - - filename = os.path.basename(path) - head, padding, tail = get_frame_path(filename) - ext = os.path.splitext(path)[1] - assert tail == ext, ("Tail does not match %s" % ext) - - instance.data.update({ - "path": path, - "outputDir": os.path.dirname(path), - "ext": ext, # todo: should be redundant? - - # Backwards compatibility: embed tool in instance.data - "tool": tool - }) - - # Add tool itself as member - instance.append(tool) diff --git a/openpype/hosts/fusion/plugins/publish/collect_render.py b/openpype/hosts/fusion/plugins/publish/collect_render.py new file mode 100644 index 0000000000..87c1d952e8 --- /dev/null +++ b/openpype/hosts/fusion/plugins/publish/collect_render.py @@ -0,0 +1,206 @@ +import os +from pprint import pformat +import attr +import pyblish.api + +from openpype.pipeline import publish +from openpype.pipeline.publish import RenderInstance +from openpype.hosts.fusion.api.lib import get_frame_path + + +@attr.s +class FusionRenderInstance(RenderInstance): + # extend generic, composition name is needed + fps = attr.ib(default=None) + projectEntity = attr.ib(default=None) + stagingDir = attr.ib(default=None) + app_version = attr.ib(default=None) + toolSaver = attr.ib(default=None) + workfileComp = attr.ib(default=None) + publish_attributes = attr.ib(default={}) + + +class CollectFusionRender( + publish.AbstractCollectRender, + publish.ColormanagedPyblishPluginMixin +): + + order = pyblish.api.CollectorOrder + 0.09 + label = "Collect Fusion Render" + hosts = ["fusion"] + + def get_instances(self, context): + + comp = context.data.get("currentComp") + comp_frame_format_prefs = comp.GetPrefs("Comp.FrameFormat") + aspect_x = comp_frame_format_prefs.get("AspectX") + aspect_y = comp_frame_format_prefs.get("AspectY") + + instances = [] + instances_to_remove = [] + + current_file = context.data["currentFile"] + version = context.data["version"] + + project_entity = context.data["projectEntity"] + + for inst in context: + if not inst.data.get("active", True): + continue + + family = inst.data["family"] + if family not in ["render"]: + continue + + task_name = inst.data.get("task") # legacy + tool = inst.data["transientData"]["tool"] + + instance_families = inst.data.get("families", []) + subset_name = inst.data["subset"] + instance = FusionRenderInstance( + family="render", + toolSaver=tool, + workfileComp=comp, + families=instance_families, + version=version, + time="", + source=current_file, + label="{} - {}".format(subset_name, family), + subset=subset_name, + asset=inst.data["asset"], + task=task_name, + attachTo=False, + setMembers='', + publish=True, + name=subset_name, + resolutionWidth=comp_frame_format_prefs.get("Width"), + resolutionHeight=comp_frame_format_prefs.get("Height"), + pixelAspect=aspect_x / aspect_y, + tileRendering=False, + tilesX=0, + tilesY=0, + review="review" in instance_families, + frameStart=context.data["frameStart"], + frameEnd=context.data["frameEnd"], + handleStart=context.data["handleStart"], + handleEnd=context.data["handleEnd"], + frameStep=1, + fps=comp_frame_format_prefs.get("Rate"), + app_version=comp.GetApp().Version, + publish_attributes=inst.data.get("publish_attributes", {}) + ) + + render_target = inst.data["creator_attributes"]["render_target"] + self.log.debug("render_target: '{}'".format(render_target)) + + if render_target == "local": + # for local renders + self._instance_data_local_update( + project_entity, instance, f"render.{render_target}") + + if render_target == "frames": + self._instance_data_local_update( + project_entity, instance, f"render.{render_target}") + + if render_target == "farm": + fam = "render.farm" + if fam not in instance.families: + instance.families.append(fam) + instance.toBeRenderedOn = "deadline" + instance.farm = True # to skip integrate + if "review" in instance.families: + # to skip ExtractReview locally + instance.families.remove("review") + + instances.append(instance) + instances_to_remove.append(inst) + + for instance in instances_to_remove: + context.remove(instance) + + return instances + + def post_collecting_action(self): + for instance in self._context: + if "render.frames" in instance.data.get("families", []): + self._update_for_frames(instance) + self.log.debug(pformat(instance.data)) + + def get_expected_files(self, render_instance): + """ + Returns list of rendered files that should be created by + Deadline. These are not published directly, they are source + for later 'submit_publish_job'. + + Args: + render_instance (RenderInstance): to pull anatomy and parts used + in url + + Returns: + (list) of absolute urls to rendered file + """ + start = render_instance.frameStart - render_instance.handleStart + end = render_instance.frameEnd + render_instance.handleEnd + + path = ( + render_instance.toolSaver["Clip"] + [render_instance.workfileComp.TIME_UNDEFINED] + ) + output_dir = os.path.dirname(path) + render_instance.outputDir = output_dir + + basename = os.path.basename(path) + + head, padding, ext = get_frame_path(basename) + + expected_files = [] + for frame in range(start, end + 1): + expected_files.append( + os.path.join( + output_dir, + f"{head}{str(frame).zfill(padding)}{ext}" + ) + ) + + return expected_files + + def _update_for_frames(self, instance): + """Update old saved instances to current publishing format""" + + expected_files = instance.data["expectedFiles"] + + start = instance.data["frameStart"] - instance.data["handleStart"] + + path = expected_files[0] + basename = os.path.basename(path) + staging_dir = os.path.dirname(path) + _, padding, ext = get_frame_path(basename) + + repre = { + "name": ext[1:], + "ext": ext[1:], + "frameStart": f"%0{padding}d" % start, + "files": [os.path.basename(f) for f in expected_files], + "stagingDir": staging_dir, + } + + self.set_representation_colorspace( + representation=repre, + context=instance.context, + ) + + # review representation + if instance.data.get("review", False): + repre["tags"] = ["review"] + + # add the repre to the instance + if "representations" not in instance.data: + instance.data["representations"] = [] + instance.data["representations"].append(repre) + + return instance + + def _instance_data_local_update(self, project_entity, instance, family): + instance.projectEntity = project_entity + if family not in instance.families: + instance.families.append(family) diff --git a/openpype/hosts/fusion/plugins/publish/collect_renders.py b/openpype/hosts/fusion/plugins/publish/collect_renders.py deleted file mode 100644 index b1c12c7393..0000000000 --- a/openpype/hosts/fusion/plugins/publish/collect_renders.py +++ /dev/null @@ -1,33 +0,0 @@ -import pyblish.api - - -class CollectFusionRenders(pyblish.api.InstancePlugin): - """Collect current saver node's render Mode - - Options: - local (Render locally) - frames (Use existing frames) - - """ - - order = pyblish.api.CollectorOrder + 0.4 - label = "Collect Renders" - hosts = ["fusion"] - families = ["render"] - - def process(self, instance): - render_target = instance.data["render_target"] - family = instance.data["family"] - - # add targeted family to families - instance.data["families"].append( - "{}.{}".format(family, render_target) - ) - if render_target == "farm": - if "review" in instance.data["families"]: - instance.data["families"].remove("review") - - # Farm rendering - instance.data["transfer"] = False - instance.data["farm"] = True - self.log.info("Farm rendering ON ...") From 7013be47de335bcb9d118d60e67d234360839d8b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 9 May 2023 17:21:16 +0200 Subject: [PATCH 207/360] Fusion: refactor validators to work with new collected data --- .../publish/validate_create_folder_checked.py | 2 +- .../validate_expected_frames_existence.py | 27 ++++++------------- .../validate_filename_has_extension.py | 4 +-- .../publish/validate_saver_has_input.py | 2 +- .../publish/validate_saver_passthrough.py | 2 +- .../publish/validate_unique_subsets.py | 2 +- 6 files changed, 14 insertions(+), 25 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/validate_create_folder_checked.py b/openpype/hosts/fusion/plugins/publish/validate_create_folder_checked.py index 8a91f23578..82d34b0b5d 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_create_folder_checked.py +++ b/openpype/hosts/fusion/plugins/publish/validate_create_folder_checked.py @@ -21,7 +21,7 @@ class ValidateCreateFolderChecked(pyblish.api.InstancePlugin): @classmethod def get_invalid(cls, instance): - tool = instance[0] + tool = instance.data["toolSaver"] create_dir = tool.GetInput("CreateDir") if create_dir == 0.0: cls.log.error( diff --git a/openpype/hosts/fusion/plugins/publish/validate_expected_frames_existence.py b/openpype/hosts/fusion/plugins/publish/validate_expected_frames_existence.py index c208b8ef15..befaae13be 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_expected_frames_existence.py +++ b/openpype/hosts/fusion/plugins/publish/validate_expected_frames_existence.py @@ -14,7 +14,7 @@ class ValidateLocalFramesExistence(pyblish.api.InstancePlugin): order = pyblish.api.ValidatorOrder label = "Validate Expected Frames Exists" - families = ["render"] + families = ["render.frames"] hosts = ["fusion"] actions = [RepairAction, SelectInvalidAction] @@ -23,25 +23,15 @@ class ValidateLocalFramesExistence(pyblish.api.InstancePlugin): if non_existing_frames is None: non_existing_frames = [] - if instance.data.get("render_target") == "frames": - tool = instance[0] + if "render.frames" in instance.data.get("families", []): + tool = instance.data["toolSaver"] - frame_start = instance.data["frameStart"] - frame_end = instance.data["frameEnd"] - path = instance.data["path"] - output_dir = instance.data["outputDir"] + expected_files = instance.data["expectedFiles"] - basename = os.path.basename(path) - head, ext = os.path.splitext(basename) - files = [ - f"{head}{str(frame).zfill(4)}{ext}" - for frame in range(frame_start, frame_end + 1) - ] - - for file in files: - if not os.path.exists(os.path.join(output_dir, file)): + for file in expected_files: + if not os.path.exists(file): cls.log.error( - f"Missing file: {os.path.join(output_dir, file)}" + f"Missing file: {file}" ) non_existing_frames.append(file) @@ -67,8 +57,7 @@ class ValidateLocalFramesExistence(pyblish.api.InstancePlugin): def repair(cls, instance): invalid = cls.get_invalid(instance) if invalid: - tool = invalid[0] - + tool = instance.data["toolSaver"] # Change render target to local to render locally tool.SetData("openpype.creator_attributes.render_target", "local") diff --git a/openpype/hosts/fusion/plugins/publish/validate_filename_has_extension.py b/openpype/hosts/fusion/plugins/publish/validate_filename_has_extension.py index bbba2dde6e..1bf603e5b1 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_filename_has_extension.py +++ b/openpype/hosts/fusion/plugins/publish/validate_filename_has_extension.py @@ -30,11 +30,11 @@ class ValidateFilenameHasExtension(pyblish.api.InstancePlugin): @classmethod def get_invalid(cls, instance): - path = instance.data["path"] + path = instance.data["expectedFiles"][0] fname, ext = os.path.splitext(path) if not ext: - tool = instance[0] + tool = instance.data["toolSaver"] cls.log.error("%s has no extension specified" % tool.Name) return [tool] diff --git a/openpype/hosts/fusion/plugins/publish/validate_saver_has_input.py b/openpype/hosts/fusion/plugins/publish/validate_saver_has_input.py index e02125f531..b409608ec3 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_saver_has_input.py +++ b/openpype/hosts/fusion/plugins/publish/validate_saver_has_input.py @@ -20,7 +20,7 @@ class ValidateSaverHasInput(pyblish.api.InstancePlugin): @classmethod def get_invalid(cls, instance): - saver = instance[0] + saver = instance.data["toolSaver"] if not saver.Input.GetConnectedOutput(): return [saver] diff --git a/openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py b/openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py index 56f2e7e6b8..677861a654 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py +++ b/openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py @@ -37,7 +37,7 @@ class ValidateSaverPassthrough(pyblish.api.ContextPlugin): def is_invalid(self, instance): - saver = instance[0] + saver = instance.data["toolSaver"] attr = saver.GetAttrs() active = not attr["TOOLB_PassThrough"] diff --git a/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py b/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py index 5b6ceb2fdb..6a65182fae 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py +++ b/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py @@ -43,7 +43,7 @@ class ValidateUniqueSubsets(pyblish.api.ContextPlugin): invalid.extend(instances) # Return tools for the invalid instances so they can be selected - invalid = [instance.data["tool"] for instance in invalid] + invalid = [instance.data["toolSaver"] for instance in invalid] return invalid From b8e8ce66606d643e82bccf3d8864063581754867 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 9 May 2023 17:22:01 +0200 Subject: [PATCH 208/360] fusion: rewriting render local to work with new instance data also adding colorspace data to representation --- .../plugins/publish/extract_render_local.py | 52 +++++++++++++++++-- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/extract_render_local.py b/openpype/hosts/fusion/plugins/publish/extract_render_local.py index 5a0140c525..c2e38884c7 100644 --- a/openpype/hosts/fusion/plugins/publish/extract_render_local.py +++ b/openpype/hosts/fusion/plugins/publish/extract_render_local.py @@ -1,8 +1,11 @@ +import os import logging import contextlib import pyblish.api -from openpype.hosts.fusion.api import comp_lock_and_undo_chunk +from openpype.pipeline import publish +from openpype.hosts.fusion.api import comp_lock_and_undo_chunk +from openpype.hosts.fusion.api.lib import get_frame_path log = logging.getLogger(__name__) @@ -38,7 +41,10 @@ def enabled_savers(comp, savers): saver.SetAttrs({"TOOLB_PassThrough": original_state}) -class FusionRenderLocal(pyblish.api.InstancePlugin): +class FusionRenderLocal( + pyblish.api.InstancePlugin, + publish.ColormanagedPyblishPluginMixin +): """Render the current Fusion composition locally.""" order = pyblish.api.ExtractorOrder - 0.2 @@ -52,6 +58,8 @@ class FusionRenderLocal(pyblish.api.InstancePlugin): # Start render self.render_once(context) + self._add_representation(instance) + # Log render status self.log.info( "Rendered '{nm}' for asset '{ast}' under the task '{tsk}'".format( @@ -71,11 +79,11 @@ class FusionRenderLocal(pyblish.api.InstancePlugin): savers_to_render = [ # Get the saver tool from the instance - instance[0] for instance in context if + instance.data["toolSaver"] for instance in context if # Only active instances instance.data.get("publish", True) and # Only render.local instances - "render.local" in instance.data["families"] + "render.local" in instance.data.get("families") ] if key not in context.data: @@ -107,3 +115,39 @@ class FusionRenderLocal(pyblish.api.InstancePlugin): if context.data[key] is False: raise RuntimeError("Comp render failed") + + def _add_representation(self, instance): + """Add representation to instance""" + + expected_files = instance.data["expectedFiles"] + + start = instance.data["frameStart"] - instance.data["handleStart"] + + path = expected_files[0] + _, padding, ext = get_frame_path(path) + + staging_dir = os.path.dirname(path) + + repre = { + "name": ext[1:], + "ext": ext[1:], + "frameStart": f"%0{padding}d" % start, + "files": [os.path.basename(f) for f in expected_files], + "stagingDir": staging_dir, + } + + self.set_representation_colorspace( + representation=repre, + context=instance.context, + ) + + # review representation + if instance.data.get("review", False): + repre["tags"] = ["review"] + + # add the repre to the instance + if "representations" not in instance.data: + instance.data["representations"] = [] + instance.data["representations"].append(repre) + + return instance From aace680fa19dee09dcb19d9692fd75ec4e6859ab Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 9 May 2023 17:22:31 +0200 Subject: [PATCH 209/360] fusion deadline, rewriting to new instance data --- .../plugins/publish/submit_fusion_deadline.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py index 2885d91d07..092c317ce3 100644 --- a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py @@ -96,8 +96,6 @@ class FusionSubmitDeadline( instance.data["suspend_publish"] = attribute_values[ "suspend_publish"] - instance.data["toBeRenderedOn"] = "deadline" - context = instance.context key = "__hasRun{}".format(self.__class__.__name__) @@ -132,8 +130,7 @@ class FusionSubmitDeadline( if not saver_instances: raise RuntimeError("No instances found for Deadline submittion") - fusion_version = int(context.data["fusionVersion"]) - comment = context.data.get("comment", "") + comment = instance.data.get("comment", "") deadline_user = context.data.get("deadlineUser", getpass.getuser()) script_path = context.data["currentFile"] @@ -201,7 +198,7 @@ class FusionSubmitDeadline( "FlowFile": script_path, # Mandatory for Deadline - "Version": str(fusion_version), + "Version": str(instance.data["app_version"]), # Render in high quality "HighQuality": True, @@ -225,7 +222,9 @@ class FusionSubmitDeadline( # Enable going to rendered frames from Deadline Monitor for index, instance in enumerate(saver_instances): - head, padding, tail = get_frame_path(instance.data["path"]) + head, padding, tail = get_frame_path( + instance.data["expectedFiles"][0] + ) path = "{}{}{}".format(head, "#" * padding, tail) folder, filename = os.path.split(path) payload["JobInfo"]["OutputDirectory%d" % index] = folder From 11691f091c3beccf9d7035b87596efe4e3813e8b Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 9 May 2023 17:52:26 +0100 Subject: [PATCH 210/360] Fix hound issues --- openpype/hosts/unreal/hooks/pre_workfile_preparation.py | 3 --- openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py | 3 ++- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py index 085f80209d..f01609d314 100644 --- a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py +++ b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py @@ -107,9 +107,6 @@ class UnrealPrelaunchHook(PreLaunchHook): f"project [ {unreal_project_name} ]" )) - import openpype.hosts.unreal.lib as ue_lib - path = ue_lib.get_path_to_cmdlet_project(engine_version) - q_thread = QtCore.QThread() ue_project_worker = UEProjectGenerationWorker() ue_project_worker.setup( diff --git a/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py b/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py index 8b1b9d8f9e..3a292fdbd1 100644 --- a/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py +++ b/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py @@ -22,7 +22,8 @@ class PointCacheAlembicLoader(plugin.Loader): color = "orange" def get_task( - self, filename, asset_dir, asset_name, replace, frame_start=None, frame_end=None + self, filename, asset_dir, asset_name, replace, + frame_start=None, frame_end=None ): task = unreal.AssetImportTask() options = unreal.AbcImportSettings() From bbe5dbb14b8c8cb50a88ac945516fc3be0df0d9f Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 10 May 2023 03:25:27 +0000 Subject: [PATCH 211/360] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index e02053ba76..7df154fe1e 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.7-nightly.1" +__version__ = "3.15.7-nightly.2" From 28bf443a29b95e78d18d08e844b9ca2e0750829a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 10 May 2023 03:26:09 +0000 Subject: [PATCH 212/360] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index cae6a6486b..0d75b669d2 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.15.7-nightly.2 - 3.15.7-nightly.1 - 3.15.6 - 3.15.6-nightly.3 @@ -134,7 +135,6 @@ body: - 3.14.1-nightly.3 - 3.14.1-nightly.2 - 3.14.1-nightly.1 - - 3.14.0 validations: required: true - type: dropdown From b09efa96756ecb02cd0c1ae8b1183c692ff38147 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 10 May 2023 11:18:30 +0200 Subject: [PATCH 213/360] fusion: storing asset frame attribute at comp openpype_instance data --- openpype/hosts/fusion/api/lib.py | 7 ++++ .../publish/collect_comp_frame_range.py | 38 ++++++++++++++----- 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/fusion/api/lib.py b/openpype/hosts/fusion/api/lib.py index 40cc4d2963..8f7b29f0c4 100644 --- a/openpype/hosts/fusion/api/lib.py +++ b/openpype/hosts/fusion/api/lib.py @@ -51,6 +51,12 @@ def update_frame_range(start, end, comp=None, set_render_range=True, "COMPN_GlobalStart": start - handle_start, "COMPN_GlobalEnd": end + handle_end } + frame_data = { + "frameStart": start, + "frameEnd": end, + "handleStart": handle_start, + "handleEnd": handle_end + } # set frame range if set_render_range: @@ -61,6 +67,7 @@ def update_frame_range(start, end, comp=None, set_render_range=True, with comp_lock_and_undo_chunk(comp): comp.SetAttrs(attrs) + comp.SetData("openpype_instance", frame_data) def set_asset_framerange(): diff --git a/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py b/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py index fbd7606cd7..2db0002ee6 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py +++ b/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py @@ -9,14 +9,20 @@ def get_comp_render_range(comp): global_start = comp_attrs["COMPN_GlobalStart"] global_end = comp_attrs["COMPN_GlobalEnd"] + frame_data = comp.GetData("openpype_instance") + handle_start = frame_data.get("handleStart", 0) + handle_end = frame_data.get("handleEnd", 0) + frame_start = frame_data.get("frameStart", 0) + frame_end = frame_data.get("frameEnd", 0) + # Whenever render ranges are undefined fall back # to the comp's global start and end if start == -1000000000: - start = global_start + start = frame_start if end == -1000000000: - end = global_end + end = frame_end - return start, end, global_start, global_end + return start, end, global_start, global_end, handle_start, handle_end class CollectFusionCompFrameRanges(pyblish.api.ContextPlugin): @@ -34,10 +40,22 @@ class CollectFusionCompFrameRanges(pyblish.api.ContextPlugin): comp = context.data["currentComp"] # Store comp render ranges - start, end, global_start, global_end = get_comp_render_range(comp) - context.data["frameStart"] = int(start) - context.data["frameEnd"] = int(end) - context.data["frameStartHandle"] = int(global_start) - context.data["frameEndHandle"] = int(global_end) - context.data["handleStart"] = int(start) - int(global_start) - context.data["handleEnd"] = int(global_end) - int(end) + ( + start, end, + global_start, + global_end, + handle_start, + handle_end + ) = get_comp_render_range(comp) + + data = {} + data["frameStart"] = int(start) + data["frameEnd"] = int(end) + data["frameStartHandle"] = int(global_start) + data["frameEndHandle"] = int(global_end) + data["handleStart"] = int(handle_start) + data["handleEnd"] = int(handle_end) + + self.log.debug("_ data: {}".format(data)) + + context.data.update(data) From 903d873dbe253e92b3b805f798b21c374af0a661 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 10 May 2023 11:19:08 +0200 Subject: [PATCH 214/360] fusion: frame ranges taken from instance --- .../hosts/fusion/plugins/publish/collect_render.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/collect_render.py b/openpype/hosts/fusion/plugins/publish/collect_render.py index 87c1d952e8..5adb8a13f0 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_render.py +++ b/openpype/hosts/fusion/plugins/publish/collect_render.py @@ -49,7 +49,7 @@ class CollectFusionRender( continue family = inst.data["family"] - if family not in ["render"]: + if family != "render": continue task_name = inst.data.get("task") # legacy @@ -80,10 +80,11 @@ class CollectFusionRender( tilesX=0, tilesY=0, review="review" in instance_families, - frameStart=context.data["frameStart"], - frameEnd=context.data["frameEnd"], - handleStart=context.data["handleStart"], - handleEnd=context.data["handleEnd"], + frameStart=inst.data["frameStart"], + frameEnd=inst.data["frameEnd"], + handleStart=inst.data["handleStart"], + handleEnd=inst.data["handleEnd"], + ignoreFrameHandleCheck=True, frameStep=1, fps=comp_frame_format_prefs.get("Rate"), app_version=comp.GetApp().Version, From 9cbbbe818ca7df153ea00f04a452705d9279efca Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 10 May 2023 11:19:40 +0200 Subject: [PATCH 215/360] deadline fusion: frame range taken from handles version --- .../deadline/plugins/publish/submit_fusion_deadline.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py index 092c317ce3..891bfde6c5 100644 --- a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py @@ -128,7 +128,7 @@ class FusionSubmitDeadline( saver_instances.append(instance) if not saver_instances: - raise RuntimeError("No instances found for Deadline submittion") + raise RuntimeError("No instances found for Deadline submission") comment = instance.data.get("comment", "") deadline_user = context.data.get("deadlineUser", getpass.getuser()) @@ -187,8 +187,8 @@ class FusionSubmitDeadline( "Plugin": "Fusion", "Frames": "{start}-{end}".format( - start=int(context.data["frameStart"]), - end=int(context.data["frameEnd"]) + start=int(instance.data["frameStartHandle"]), + end=int(instance.data["frameEndHandle"]) ), "Comment": comment, From 6ea1ab1c9f3da8d06bea10bb3b2b8a3305139120 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 10 May 2023 11:20:09 +0200 Subject: [PATCH 216/360] deadline submitter settings for aov filter --- openpype/settings/defaults/project_settings/deadline.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/settings/defaults/project_settings/deadline.json b/openpype/settings/defaults/project_settings/deadline.json index fdd70f1a44..3f114025f3 100644 --- a/openpype/settings/defaults/project_settings/deadline.json +++ b/openpype/settings/defaults/project_settings/deadline.json @@ -114,6 +114,9 @@ ], "max": [ ".*" + ], + "fusion": [ + ".*" ] } } From 55bebf86426cf467f6f4268882ec00c7b44234e2 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 10 May 2023 12:33:38 +0200 Subject: [PATCH 217/360] pr comments --- openpype/hosts/fusion/plugins/create/create_saver.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index 8bf364cf20..7378dec2b4 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -1,6 +1,5 @@ from copy import deepcopy import os -from pprint import pformat from openpype.hosts.fusion.api import ( get_current_comp, @@ -139,7 +138,6 @@ class CreateSaver(NewCreator): def _configure_saver_tool(self, data, tool, subset): formatting_data = deepcopy(data) - self.log.warning(pformat(formatting_data)) # Subset change detected workdir = os.path.normpath(legacy_io.Session["AVALON_WORKDIR"]) @@ -274,7 +272,9 @@ class CreateSaver(NewCreator): """Method called on initialization of plugin to apply settings.""" # plugin settings - plugin_settings = self._get_creator_settings(project_settings) + plugin_settings = ( + project_settings["fusion"]["create"][self.__class__.__name__] + ) # individual attributes self.instance_attributes = plugin_settings.get( @@ -285,8 +285,3 @@ class CreateSaver(NewCreator): plugin_settings.get("temp_rendering_path_template") or self.temp_rendering_path_template ) - - def _get_creator_settings(self, project_settings, settings_key=None): - if not settings_key: - settings_key = self.__class__.__name__ - return project_settings["fusion"]["create"][settings_key] From 98d27fa5a403143e5910d343ccc62af8b5e8cbb1 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 10 May 2023 12:54:30 +0200 Subject: [PATCH 218/360] fusion: frame padding from anatomy templates --- .../hosts/fusion/plugins/create/create_saver.py | 16 +++++++++++++--- .../defaults/project_settings/fusion.json | 2 +- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index 7378dec2b4..fb05d13597 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -5,6 +5,7 @@ from openpype.hosts.fusion.api import ( get_current_comp, comp_lock_and_undo_chunk, ) +from openpype.hosts.fusion.api.lib import get_frame_path from openpype.lib import ( BoolDef, @@ -14,6 +15,7 @@ from openpype.pipeline import ( legacy_io, Creator as NewCreator, CreatedInstance, + Anatomy ) from openpype.client import ( get_asset_by_name, @@ -37,7 +39,7 @@ class CreateSaver(NewCreator): "Mask" ] temp_rendering_path_template = ( - "{workdir}/renders/fusion/{subset}/{subset}..{ext}") + "{workdir}/renders/fusion/{subset}/{subset}.{frame}.{ext}") def create(self, subset_name, instance_data, pre_create_data): # TODO: Add pre_create attributes to choose file format? @@ -139,10 +141,17 @@ class CreateSaver(NewCreator): def _configure_saver_tool(self, data, tool, subset): formatting_data = deepcopy(data) + # get frame padding from anatomy templates + anatomy = Anatomy() + frame_padding = int( + anatomy.templates["render"].get("frame_padding", 4) + ) + # Subset change detected workdir = os.path.normpath(legacy_io.Session["AVALON_WORKDIR"]) formatting_data.update({ "workdir": workdir.replace("\\", "/"), + "frame": "0" * frame_padding, "ext": "exr" }) @@ -180,8 +189,9 @@ class CreateSaver(NewCreator): path = tool["Clip"][comp.TIME_UNDEFINED] fname = os.path.basename(path) - fname, _ext = os.path.splitext(fname) - variant = fname.rstrip(".") + head, _, _ = get_frame_path(fname) + + variant = head.rstrip(".") subset = self.get_subset_name( variant=variant, task_name=task, diff --git a/openpype/settings/defaults/project_settings/fusion.json b/openpype/settings/defaults/project_settings/fusion.json index d76ed82942..066fc3816a 100644 --- a/openpype/settings/defaults/project_settings/fusion.json +++ b/openpype/settings/defaults/project_settings/fusion.json @@ -24,7 +24,7 @@ }, "create": { "CreateSaver": { - "temp_rendering_path_template": "{workdir}/renders/fusion/{subset}/{subset}..{ext}", + "temp_rendering_path_template": "{workdir}/renders/fusion/{subset}/{subset}.{frame}.{ext}", "default_variants": [ "Main", "Mask" From a08f9176b0bc26d9a5f32968f8643966f59ad502 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 10 May 2023 12:55:30 +0200 Subject: [PATCH 219/360] fusion: removing path making during creation --- openpype/hosts/fusion/plugins/create/create_saver.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index fb05d13597..64a99d07c1 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -159,11 +159,6 @@ class CreateSaver(NewCreator): filepath = self.temp_rendering_path_template.format( **formatting_data) - # create directory - if not os.path.isdir(os.path.dirname(filepath)): - self.log.warning("Path does not exist! I am creating it.") - os.makedirs(os.path.dirname(filepath)) - tool["Clip"] = filepath # Rename tool From 95237c43c775f3d15b8475ffacac53fcc4d94002 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 10 May 2023 13:00:53 +0200 Subject: [PATCH 220/360] adding todo for later renaming --- openpype/hosts/fusion/plugins/create/create_saver.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index 64a99d07c1..bb4615db17 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -38,6 +38,8 @@ class CreateSaver(NewCreator): "Main", "Mask" ] + + # TODO: This should be renamed together with Nuke so it is aligned temp_rendering_path_template = ( "{workdir}/renders/fusion/{subset}/{subset}.{frame}.{ext}") From da6b2b31335cefbce32289ac11fa5432666e96d8 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 10 May 2023 13:22:38 +0200 Subject: [PATCH 221/360] :bug: fix use and detection of compatible integrations --- openpype/hosts/unreal/addon.py | 13 +++++- openpype/hosts/unreal/lib.py | 75 ++++++++++++++++++++++------------ 2 files changed, 61 insertions(+), 27 deletions(-) diff --git a/openpype/hosts/unreal/addon.py b/openpype/hosts/unreal/addon.py index 0c42755d37..4468ce036c 100644 --- a/openpype/hosts/unreal/addon.py +++ b/openpype/hosts/unreal/addon.py @@ -1,5 +1,8 @@ import os -from openpype.modules import OpenPypeModule, IHostAddon +from pathlib import Path + +from openpype.modules import IHostAddon, OpenPypeModule +from .lib import get_compatible_integration UNREAL_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -19,9 +22,15 @@ class UnrealAddon(OpenPypeModule, IHostAddon): unreal_plugin_path = os.path.join( UNREAL_ROOT_DIR, "integration", f"UE_{ue_version}", "Ayon" ) + if not Path(unreal_plugin_path).exists(): + if compatible_versions := get_compatible_integration( + ue_version, Path(UNREAL_ROOT_DIR) / "integration" + ): + unreal_plugin_path = compatible_versions[-1] / "Ayon" + unreal_plugin_path = unreal_plugin_path.as_posix() if not env.get("AYON_UNREAL_PLUGIN") or \ - env.get("AYON_UNREAL_PLUGIN") != unreal_plugin_path: + env.get("AYON_UNREAL_PLUGIN") != unreal_plugin_path: env["AYON_UNREAL_PLUGIN"] = unreal_plugin_path # Set default environments if are not set via settings diff --git a/openpype/hosts/unreal/lib.py b/openpype/hosts/unreal/lib.py index 38976c3ef1..821b4daecc 100644 --- a/openpype/hosts/unreal/lib.py +++ b/openpype/hosts/unreal/lib.py @@ -321,6 +321,47 @@ def get_path_to_uat(engine_path: Path) -> Path: return engine_path / "Engine/Build/BatchFiles/RunUAT.sh" +def get_compatible_integration( + ue_version: str, integration_root: Path) -> List[Path]: + """Get path to compatible version of integration plugin. + + This will try to get the closest compatible versions to the one + specified in sorted list. + + Args: + ue_version (str): version of the current Unreal Engine. + integration_root (Path): path to built-in integration plugins. + + Returns: + list of Path: Sorted list of paths closest to the specified + version. + + """ + major, minor = ue_version.split(".") + integration_paths = [p for p in integration_root.iterdir() + if p.is_dir()] + + compatible_versions = [] + for i in integration_paths: + # parse version from path + try: + i_major, i_minor = re.search( + r"(?P\d+).(?P\d+)$", i.name).groups() + except AttributeError: + # in case there is no match, just skip to next + continue + + # consider versions with different major so different that they + # are incompatible + if int(major) != int(i_major): + continue + + compatible_versions.append(i) + + sorted(set(compatible_versions)) + return compatible_versions + + def get_path_to_cmdlet_project(ue_version: str) -> Path: cmd_project = Path( os.path.abspath(os.getenv("OPENPYPE_ROOT"))) @@ -334,31 +375,15 @@ def get_path_to_cmdlet_project(ue_version: str) -> Path: if cmd_project.exists(): return cmd_project / "CommandletProject/CommandletProject.uproject" - major, minor = ue_version.split(".") - integration_paths = [p for p in cmd_project.parent.iterdir() - if p.is_dir()] - - compatible_versions = [cmd_project] - for i in integration_paths: - - # parse version from path - i_major, i_minor = re.search( - r"(?P\d+).(?P\d+)$", i.name).groups() - - # consider versions with different major so different that they - # are incompatible - if int(major) != int(i_major): - continue - - compatible_versions.append(i) - - sorted(set(compatible_versions)) - - - - - - return cmd_project / "CommandletProject/CommandletProject.uproject" + if compatible_versions := get_compatible_integration( + ue_version, cmd_project.parent + ): + return compatible_versions[-1] / "CommandletProject/CommandletProject.uproject" # noqa: E501 + else: + raise RuntimeError( + ("There are no compatible versions of Unreal " + "integration plugin compatible with running version " + f"of Unreal Engine {ue_version}")) def get_path_to_ubt(engine_path: Path, ue_version: str) -> Path: From ce6b02862ad9179a199588b4c56ec90c456cffa5 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 10 May 2023 13:25:48 +0200 Subject: [PATCH 222/360] :rotating_light: fix fixable hound issue --- openpype/hosts/unreal/addon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/addon.py b/openpype/hosts/unreal/addon.py index 4468ce036c..db40d629bc 100644 --- a/openpype/hosts/unreal/addon.py +++ b/openpype/hosts/unreal/addon.py @@ -30,7 +30,7 @@ class UnrealAddon(OpenPypeModule, IHostAddon): unreal_plugin_path = unreal_plugin_path.as_posix() if not env.get("AYON_UNREAL_PLUGIN") or \ - env.get("AYON_UNREAL_PLUGIN") != unreal_plugin_path: + env.get("AYON_UNREAL_PLUGIN") != unreal_plugin_path: env["AYON_UNREAL_PLUGIN"] = unreal_plugin_path # Set default environments if are not set via settings From 1a48ddefe4a665997dc9e4bac066200721949d3c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 10 May 2023 15:37:19 +0200 Subject: [PATCH 223/360] normalizing path --- openpype/hosts/fusion/plugins/create/create_saver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index bb4615db17..f924c30b0a 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -152,7 +152,7 @@ class CreateSaver(NewCreator): # Subset change detected workdir = os.path.normpath(legacy_io.Session["AVALON_WORKDIR"]) formatting_data.update({ - "workdir": workdir.replace("\\", "/"), + "workdir": workdir, "frame": "0" * frame_padding, "ext": "exr" }) @@ -161,7 +161,7 @@ class CreateSaver(NewCreator): filepath = self.temp_rendering_path_template.format( **formatting_data) - tool["Clip"] = filepath + tool["Clip"] = os.path.normpath(filepath) # Rename tool if tool.Name != subset: From cbde1425fae8466d3b3db3f201708c1f08a80e88 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 10 May 2023 14:37:33 +0100 Subject: [PATCH 224/360] Fix camera framerange when loading it in Unreal --- openpype/hosts/unreal/plugins/load/load_camera.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index c082562775..2303ed1ffc 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -268,8 +268,8 @@ class CameraLoader(plugin.Loader): data = get_asset_by_name(project_name, asset)["data"] 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) + cam_seq.set_playback_start(data.get('clipIn')) + cam_seq.set_playback_end(data.get('clipOut') + 1) self._set_sequence_hierarchy( sequences[-1], cam_seq, data.get('clipIn'), data.get('clipOut')) From a42fbf5a47f8fcbeca35f6f50ee0fd091d45fe36 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 10 May 2023 14:51:45 +0100 Subject: [PATCH 225/360] Fix missing parameter when updating alembic staticmesh --- openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py b/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py index c435b8843d..befc7b0ac9 100644 --- a/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py +++ b/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py @@ -135,7 +135,7 @@ class StaticMeshAlembicLoader(plugin.Loader): source_path = get_representation_path(representation) destination_path = container["namespace"] - task = self.get_task(source_path, destination_path, name, True) + task = self.get_task(source_path, destination_path, name, True, False) # do import fbx and replace existing data unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) From 27ac1b4590c2211548d94dfc5a26b902c3938c72 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 10 May 2023 16:16:50 +0200 Subject: [PATCH 226/360] Removing unmanaged compatibility This fixes issue https://github.com/ynput/OpenPype/pull/4943#pullrequestreview-1420557288 --- .../fusion/plugins/create/create_saver.py | 48 ++++++------------- 1 file changed, 14 insertions(+), 34 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index f924c30b0a..27394ae15b 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -91,7 +91,7 @@ class CreateSaver(NewCreator): for tool in tools: data = self.get_managed_tool_data(tool) if not data: - data = self._collect_unmanaged_saver(tool) + data = self._collect_saver(tool) # Add instance created_instance = CreatedInstance.from_existing(data, self) @@ -168,43 +168,23 @@ class CreateSaver(NewCreator): print(f"Renaming {tool.Name} -> {subset}") tool.SetAttrs({"TOOLS_Name": subset}) - def _collect_unmanaged_saver(self, tool): - # TODO: this should not be done this way - this should actually - # get the data as stored on the tool explicitly (however) - # that would disallow any 'regular saver' to be collected - # unless the instance data is stored on it to begin with - - print("Collecting unmanaged saver..") - comp = tool.Comp() - - # Allow regular non-managed savers to also be picked up - project = legacy_io.Session["AVALON_PROJECT"] - asset = legacy_io.Session["AVALON_ASSET"] - task = legacy_io.Session["AVALON_TASK"] - - asset_doc = get_asset_by_name(project_name=project, asset_name=asset) - - path = tool["Clip"][comp.TIME_UNDEFINED] - fname = os.path.basename(path) - head, _, _ = get_frame_path(fname) - - variant = head.rstrip(".") - subset = self.get_subset_name( - variant=variant, - task_name=task, - asset_doc=asset_doc, - project_name=project, - ) - + def _collect_saver(self, tool): + print("Collecting saver..") attrs = tool.GetAttrs() + + ctx_data = {} + keys = ["asset", "subset", "task", "variant"] + for key in keys: + ctx_data[key] = tool.GetData(f"openpype.{key}") + passthrough = attrs["TOOLB_PassThrough"] return { # Required data - "project": project, - "asset": asset, - "subset": subset, - "task": task, - "variant": variant, + "project": self.project_name, + "asset": ctx_data["asset"], + "subset": ctx_data["subset"], + "task": ctx_data["task"], + "variant": ctx_data["variant"], "active": not passthrough, "family": self.family, # Unique identifier for instance and this creator From dfea365474995dfbdffd85d296f3b8e61b3e4ffd Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 10 May 2023 16:38:17 +0200 Subject: [PATCH 227/360] moving instnance id so it is imprinted once created addressing issue form here https://github.com/ynput/OpenPype/pull/4943#issuecomment-1542241467 --- openpype/hosts/fusion/plugins/create/create_saver.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index 27394ae15b..224ec0d48e 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -44,6 +44,11 @@ class CreateSaver(NewCreator): "{workdir}/renders/fusion/{subset}/{subset}.{frame}.{ext}") def create(self, subset_name, instance_data, pre_create_data): + instance_data.update({ + "id": "pyblish.avalon.instance", + "subset": subset_name + }) + # TODO: Add pre_create attributes to choose file format? file_format = "OpenEXRFormat" @@ -52,7 +57,6 @@ class CreateSaver(NewCreator): args = (-32768, -32768) # Magical position numbers saver = comp.AddTool("Saver", *args) - instance_data["subset"] = subset_name self._update_tool_with_data(saver, data=instance_data) saver["OutputFormat"] = file_format @@ -173,7 +177,7 @@ class CreateSaver(NewCreator): attrs = tool.GetAttrs() ctx_data = {} - keys = ["asset", "subset", "task", "variant"] + keys = ["id", "asset", "subset", "task", "variant"] for key in keys: ctx_data[key] = tool.GetData(f"openpype.{key}") @@ -188,7 +192,7 @@ class CreateSaver(NewCreator): "active": not passthrough, "family": self.family, # Unique identifier for instance and this creator - "id": "pyblish.avalon.instance", + "id": ctx_data["id"], "creator_identifier": self.identifier, } From f5215f323b3c3b8b76ef278de6771bfdf6b0dcca Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 10 May 2023 16:39:29 +0200 Subject: [PATCH 228/360] hound --- openpype/hosts/fusion/plugins/create/create_saver.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index 224ec0d48e..ecdad30b4c 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -5,7 +5,6 @@ from openpype.hosts.fusion.api import ( get_current_comp, comp_lock_and_undo_chunk, ) -from openpype.hosts.fusion.api.lib import get_frame_path from openpype.lib import ( BoolDef, @@ -17,9 +16,6 @@ from openpype.pipeline import ( CreatedInstance, Anatomy ) -from openpype.client import ( - get_asset_by_name, -) class CreateSaver(NewCreator): @@ -173,14 +169,11 @@ class CreateSaver(NewCreator): tool.SetAttrs({"TOOLS_Name": subset}) def _collect_saver(self, tool): - print("Collecting saver..") + self.log.info("Collecting saver..") attrs = tool.GetAttrs() - ctx_data = {} keys = ["id", "asset", "subset", "task", "variant"] - for key in keys: - ctx_data[key] = tool.GetData(f"openpype.{key}") - + ctx_data = {key: tool.GetData(f"openpype.{key}") for key in keys} passthrough = attrs["TOOLB_PassThrough"] return { # Required data From 4095c7bd008923f288c8b16f5ce94e0c0aacca99 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 10 May 2023 22:51:52 +0800 Subject: [PATCH 229/360] use rt.rendpickupframe and ondrej's comment --- openpype/hosts/max/api/lib.py | 6 ++--- .../max/plugins/publish/collect_render.py | 8 +++--- .../plugins/publish/validate_frame_range.py | 26 +++++++++++-------- 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 1673fc5ab8..b21ce0f789 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -150,10 +150,10 @@ def set_render_frame_range(start_frame, end_frame): Todo: Current type is hard-coded, there should be a custom setting for this. """ - rt.rendTimeType = 3 + rt.rendTimeType = 4 if start_frame is not None and end_frame is not None: - rt.rendStart = int(start_frame) - rt.rendEnd = int(end_frame) + frame_range = "{0}-{1}".format(start_frame, end_frame) + rt.rendPickupFrames = frame_range def get_multipass_setting(project_setting=None): diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index 9d93a40021..2742c36fc8 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- """Collect Render""" import os +import re import pyblish.api from pymxs import runtime as rt @@ -46,7 +47,8 @@ class CollectRender(pyblish.api.InstancePlugin): self.log.debug(f"Setting {version_int} to context.") context.data["version"] = version_int - + pattern = r"^(?P-?[0-9]+)(?:(?:-)(?P-?[0-9]+))?$" + match = re.match(pattern, rt.rendPickupFrames) # setup the plugin as 3dsmax for the internal renderer data = { "subset": instance.name, @@ -59,8 +61,8 @@ class CollectRender(pyblish.api.InstancePlugin): "source": filepath, "expectedFiles": render_layer_files, "plugin": "3dsmax", - "frameStart": int(rt.rendStart), - "frameEnd": int(rt.rendEnd), + "frameStart": int(match.group("start")), + "frameEnd": int(match.group("end")), "version": version_int, "farm": True } diff --git a/openpype/hosts/max/plugins/publish/validate_frame_range.py b/openpype/hosts/max/plugins/publish/validate_frame_range.py index e07c6390c1..4cc9cb530c 100644 --- a/openpype/hosts/max/plugins/publish/validate_frame_range.py +++ b/openpype/hosts/max/plugins/publish/validate_frame_range.py @@ -44,19 +44,23 @@ class ValidateFrameRange(pyblish.api.InstancePlugin, inst_frame_start = int(instance.data.get("frameStart")) inst_frame_end = int(instance.data.get("frameEnd")) + errors = [] if frame_start != inst_frame_start: - raise PublishValidationError( - "startFrame on instance does not match" - " with startFrame from the context data." - " You can use repair action to fix it") - + errors.append( + f"Start frame ({inst_frame_start}) on instance does not match " + f"with the start frame ({frame_start}) set on the asset data. ") if frame_end != inst_frame_end: - raise PublishValidationError( - "endFrame on instance does not match" - " with endFrame from the context data." - " You can use repair action to fix it") + errors.append( + f"End frame ({inst_frame_end}) on instance does not match " + f"with the end frame ({frame_start}) from the asset data. ") + + if errors: + errors.append("You can use repair action to fix it.") + raise PublishValidationError("\n".join(errors)) @classmethod def repair(cls, instance): - rt.rendStart = instance.context.data.get("frameStart") - rt.rendEnd = instance.context.data.get("frameEnd") + start = instance.context.data.get("frameStart") + end = instance.context.data.get("frameEnd") + frame_range = "{0}-{1}".format(start, end) + rt.rendPickupFrames = frame_range From da7d8ac091d45d6d73a2d34177f2f7ab3992f9b0 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 10 May 2023 22:53:02 +0800 Subject: [PATCH 230/360] hound fix --- openpype/hosts/max/plugins/publish/validate_frame_range.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_frame_range.py b/openpype/hosts/max/plugins/publish/validate_frame_range.py index 4cc9cb530c..761e7bf085 100644 --- a/openpype/hosts/max/plugins/publish/validate_frame_range.py +++ b/openpype/hosts/max/plugins/publish/validate_frame_range.py @@ -47,8 +47,8 @@ class ValidateFrameRange(pyblish.api.InstancePlugin, errors = [] if frame_start != inst_frame_start: errors.append( - f"Start frame ({inst_frame_start}) on instance does not match " - f"with the start frame ({frame_start}) set on the asset data. ") + f"Start frame ({inst_frame_start}) on instance does not match " # noqa + f"with the start frame ({frame_start}) set on the asset data. ") # noqa if frame_end != inst_frame_end: errors.append( f"End frame ({inst_frame_end}) on instance does not match " From e69e1f76c8f86ab2cfc9ca7a9272b99d2ea98546 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 10 May 2023 23:16:39 +0800 Subject: [PATCH 231/360] use ranges --- openpype/hosts/max/api/lib.py | 6 +++--- openpype/hosts/max/plugins/publish/collect_render.py | 6 ++---- openpype/hosts/max/plugins/publish/validate_frame_range.py | 6 ++---- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index b21ce0f789..1673fc5ab8 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -150,10 +150,10 @@ def set_render_frame_range(start_frame, end_frame): Todo: Current type is hard-coded, there should be a custom setting for this. """ - rt.rendTimeType = 4 + rt.rendTimeType = 3 if start_frame is not None and end_frame is not None: - frame_range = "{0}-{1}".format(start_frame, end_frame) - rt.rendPickupFrames = frame_range + rt.rendStart = int(start_frame) + rt.rendEnd = int(end_frame) def get_multipass_setting(project_setting=None): diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index 2742c36fc8..31f1eba409 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -47,8 +47,6 @@ class CollectRender(pyblish.api.InstancePlugin): self.log.debug(f"Setting {version_int} to context.") context.data["version"] = version_int - pattern = r"^(?P-?[0-9]+)(?:(?:-)(?P-?[0-9]+))?$" - match = re.match(pattern, rt.rendPickupFrames) # setup the plugin as 3dsmax for the internal renderer data = { "subset": instance.name, @@ -61,8 +59,8 @@ class CollectRender(pyblish.api.InstancePlugin): "source": filepath, "expectedFiles": render_layer_files, "plugin": "3dsmax", - "frameStart": int(match.group("start")), - "frameEnd": int(match.group("end")), + "frameStart": int(rt.rendStart), + "frameEnd": int(rt.rendEnd), "version": version_int, "farm": True } diff --git a/openpype/hosts/max/plugins/publish/validate_frame_range.py b/openpype/hosts/max/plugins/publish/validate_frame_range.py index 761e7bf085..21e847405e 100644 --- a/openpype/hosts/max/plugins/publish/validate_frame_range.py +++ b/openpype/hosts/max/plugins/publish/validate_frame_range.py @@ -60,7 +60,5 @@ class ValidateFrameRange(pyblish.api.InstancePlugin, @classmethod def repair(cls, instance): - start = instance.context.data.get("frameStart") - end = instance.context.data.get("frameEnd") - frame_range = "{0}-{1}".format(start, end) - rt.rendPickupFrames = frame_range + rt.rendStart = instance.context.data.get("frameStart") + rt.rendEnd = instance.context.data.get("frameEnd") From 210ed4d41fdc8c28cb18e9a2d61b10b8848cbf26 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 10 May 2023 23:17:58 +0800 Subject: [PATCH 232/360] hound fix --- openpype/hosts/max/plugins/publish/collect_render.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index 31f1eba409..00e00a8eb5 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- """Collect Render""" import os -import re import pyblish.api from pymxs import runtime as rt From 9f1bdda8b35509907351cc12ac271299bb186c0b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 11 May 2023 12:18:36 +0200 Subject: [PATCH 233/360] fusion: removing obsolete code --- .../fusion/plugins/create/create_saver.py | 23 ------------------- 1 file changed, 23 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index ecdad30b4c..2af811ef5b 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -90,8 +90,6 @@ class CreateSaver(NewCreator): tools = comp.GetToolList(False, "Saver").values() for tool in tools: data = self.get_managed_tool_data(tool) - if not data: - data = self._collect_saver(tool) # Add instance created_instance = CreatedInstance.from_existing(data, self) @@ -168,27 +166,6 @@ class CreateSaver(NewCreator): print(f"Renaming {tool.Name} -> {subset}") tool.SetAttrs({"TOOLS_Name": subset}) - def _collect_saver(self, tool): - self.log.info("Collecting saver..") - attrs = tool.GetAttrs() - - keys = ["id", "asset", "subset", "task", "variant"] - ctx_data = {key: tool.GetData(f"openpype.{key}") for key in keys} - passthrough = attrs["TOOLB_PassThrough"] - return { - # Required data - "project": self.project_name, - "asset": ctx_data["asset"], - "subset": ctx_data["subset"], - "task": ctx_data["task"], - "variant": ctx_data["variant"], - "active": not passthrough, - "family": self.family, - # Unique identifier for instance and this creator - "id": ctx_data["id"], - "creator_identifier": self.identifier, - } - def get_managed_tool_data(self, tool): """Return data of the tool if it matches creator identifier""" data = tool.GetData("openpype") From bf2e02699a0b60af16b1a6057b4f82d9faa33373 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 11 May 2023 12:20:56 +0200 Subject: [PATCH 234/360] returning important condition --- openpype/hosts/fusion/plugins/create/create_saver.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index 2af811ef5b..13836aa1a0 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -90,6 +90,8 @@ class CreateSaver(NewCreator): tools = comp.GetToolList(False, "Saver").values() for tool in tools: data = self.get_managed_tool_data(tool) + if not data: + continue # Add instance created_instance = CreatedInstance.from_existing(data, self) From 03d777503cc8da73cd46ad213d9b249c3b3f7aa3 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 11 May 2023 16:14:16 +0100 Subject: [PATCH 235/360] Remove render extractor --- .../unreal/plugins/publish/extract_render.py | 48 ------------------- 1 file changed, 48 deletions(-) delete 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 deleted file mode 100644 index 8ff38fbee0..0000000000 --- a/openpype/hosts/unreal/plugins/publish/extract_render.py +++ /dev/null @@ -1,48 +0,0 @@ -from pathlib import Path - -import unreal - -from openpype.pipeline import publish - - -class ExtractRender(publish.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/" - f"{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 9086511970927f662807e67db02e43628f1adce9 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 11 May 2023 16:14:41 +0100 Subject: [PATCH 236/360] Fix start and end frames to be int --- .../hosts/unreal/plugins/publish/collect_render_instances.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/unreal/plugins/publish/collect_render_instances.py b/openpype/hosts/unreal/plugins/publish/collect_render_instances.py index 6697a6b90d..a352b2c3f3 100644 --- a/openpype/hosts/unreal/plugins/publish/collect_render_instances.py +++ b/openpype/hosts/unreal/plugins/publish/collect_render_instances.py @@ -73,8 +73,8 @@ class CollectRenderInstances(pyblish.api.InstancePlugin): 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["frameStart"] = int(s.get('frame_range')[0]) + new_data["frameEnd"] = int(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"] From fbee0a8b3c295dc55cd64a3037f517f88de150fa Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 12 May 2023 11:07:50 +0200 Subject: [PATCH 237/360] pr comments --- .../fusion/plugins/publish/collect_render.py | 3 --- .../validate_expected_frames_existence.py | 23 +++++++++---------- .../plugins/publish/submit_fusion_deadline.py | 3 --- 3 files changed, 11 insertions(+), 18 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/collect_render.py b/openpype/hosts/fusion/plugins/publish/collect_render.py index 5adb8a13f0..4898226f03 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_render.py +++ b/openpype/hosts/fusion/plugins/publish/collect_render.py @@ -1,5 +1,4 @@ import os -from pprint import pformat import attr import pyblish.api @@ -92,7 +91,6 @@ class CollectFusionRender( ) render_target = inst.data["creator_attributes"]["render_target"] - self.log.debug("render_target: '{}'".format(render_target)) if render_target == "local": # for local renders @@ -125,7 +123,6 @@ class CollectFusionRender( for instance in self._context: if "render.frames" in instance.data.get("families", []): self._update_for_frames(instance) - self.log.debug(pformat(instance.data)) def get_expected_files(self, render_instance): """ diff --git a/openpype/hosts/fusion/plugins/publish/validate_expected_frames_existence.py b/openpype/hosts/fusion/plugins/publish/validate_expected_frames_existence.py index befaae13be..aa89799867 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_expected_frames_existence.py +++ b/openpype/hosts/fusion/plugins/publish/validate_expected_frames_existence.py @@ -23,21 +23,20 @@ class ValidateLocalFramesExistence(pyblish.api.InstancePlugin): if non_existing_frames is None: non_existing_frames = [] - if "render.frames" in instance.data.get("families", []): - tool = instance.data["toolSaver"] + tool = instance.data["toolSaver"] - expected_files = instance.data["expectedFiles"] + expected_files = instance.data["expectedFiles"] - for file in expected_files: - if not os.path.exists(file): - cls.log.error( - f"Missing file: {file}" - ) - non_existing_frames.append(file) + for file in expected_files: + if not os.path.exists(file): + cls.log.error( + f"Missing file: {file}" + ) + non_existing_frames.append(file) - if len(non_existing_frames) > 0: - cls.log.error(f"Some of {tool.Name}'s files does not exist") - return [tool] + if len(non_existing_frames) > 0: + cls.log.error(f"Some of {tool.Name}'s files does not exist") + return [tool] def process(self, instance): non_existing_frames = [] diff --git a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py index 891bfde6c5..af4bd37302 100644 --- a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py @@ -1,7 +1,6 @@ import os import json import getpass -from pprint import pformat import requests @@ -90,8 +89,6 @@ class FusionSubmitDeadline( attribute_values = self.get_attr_values_from_data( instance.data) - self.log.debug(pformat(attribute_values)) - # add suspend_publish attributeValue to instance data instance.data["suspend_publish"] = attribute_values[ "suspend_publish"] From 801b904aea357ac0523fd4b75f0f2e7bd8ac5df9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 12 May 2023 11:29:29 +0200 Subject: [PATCH 238/360] rewriting logic for frame ranges --- openpype/hosts/fusion/api/lib.py | 7 ---- .../fusion/plugins/create/create_saver.py | 3 ++ .../publish/collect_comp_frame_range.py | 26 ++++----------- .../plugins/publish/collect_instances.py | 32 +++++++++++++++---- 4 files changed, 36 insertions(+), 32 deletions(-) diff --git a/openpype/hosts/fusion/api/lib.py b/openpype/hosts/fusion/api/lib.py index 8f7b29f0c4..40cc4d2963 100644 --- a/openpype/hosts/fusion/api/lib.py +++ b/openpype/hosts/fusion/api/lib.py @@ -51,12 +51,6 @@ def update_frame_range(start, end, comp=None, set_render_range=True, "COMPN_GlobalStart": start - handle_start, "COMPN_GlobalEnd": end + handle_end } - frame_data = { - "frameStart": start, - "frameEnd": end, - "handleStart": handle_start, - "handleEnd": handle_end - } # set frame range if set_render_range: @@ -67,7 +61,6 @@ def update_frame_range(start, end, comp=None, set_render_range=True, with comp_lock_and_undo_chunk(comp): comp.SetAttrs(attrs) - comp.SetData("openpype_instance", frame_data) def set_asset_framerange(): diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index ecdad30b4c..58ffbe928a 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -223,6 +223,9 @@ class CreateSaver(NewCreator): attr_defs = [ self._get_render_target_enum(), self._get_reviewable_bool(), + BoolDef( + "custom_range", label="Custom range", default=False, + ) ] return attr_defs diff --git a/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py b/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py index 2db0002ee6..38d6577667 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py +++ b/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py @@ -9,20 +9,14 @@ def get_comp_render_range(comp): global_start = comp_attrs["COMPN_GlobalStart"] global_end = comp_attrs["COMPN_GlobalEnd"] - frame_data = comp.GetData("openpype_instance") - handle_start = frame_data.get("handleStart", 0) - handle_end = frame_data.get("handleEnd", 0) - frame_start = frame_data.get("frameStart", 0) - frame_end = frame_data.get("frameEnd", 0) - # Whenever render ranges are undefined fall back # to the comp's global start and end if start == -1000000000: - start = frame_start + start = global_start if end == -1000000000: - end = frame_end + end = global_end - return start, end, global_start, global_end, handle_start, handle_end + return start, end, global_start, global_end class CollectFusionCompFrameRanges(pyblish.api.ContextPlugin): @@ -44,18 +38,12 @@ class CollectFusionCompFrameRanges(pyblish.api.ContextPlugin): start, end, global_start, global_end, - handle_start, - handle_end ) = get_comp_render_range(comp) data = {} - data["frameStart"] = int(start) - data["frameEnd"] = int(end) - data["frameStartHandle"] = int(global_start) - data["frameEndHandle"] = int(global_end) - data["handleStart"] = int(handle_start) - data["handleEnd"] = int(handle_end) - - self.log.debug("_ data: {}".format(data)) + data["compFrameStart"] = int(start) + data["compFrameEnd"] = int(end) + data["compFrameStartHandle"] = int(global_start) + data["compFrameEndHandle"] = int(global_end) context.data.update(data) diff --git a/openpype/hosts/fusion/plugins/publish/collect_instances.py b/openpype/hosts/fusion/plugins/publish/collect_instances.py index 4608f79420..9c27e3f027 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_instances.py +++ b/openpype/hosts/fusion/plugins/publish/collect_instances.py @@ -1,4 +1,6 @@ +from math import e import os +from turtle import st import pyblish.api @@ -24,6 +26,23 @@ class CollectInstanceData(pyblish.api.InstancePlugin): creator_attributes = instance.data["creator_attributes"] instance.data.update(creator_attributes) + # get asset frame ranges + start = context.data["frameStart"] + end = context.data["frameEnd"] + handle_start = context.data["handleStart"] + handle_end = context.data["handleEnd"] + start_handle = start - handle_start + end_handle = end + handle_end + + if creator_attributes["custom_range"]: + # get comp frame ranges + start = context.data["compFrameStart"] + end = context.data["compFrameEnd"] + handle_start = 0 + handle_end = 0 + start_handle = context.data["compFrameStartHandle"] + end_handle = context.data["compFrameEndHandle"] + # Include start and end render frame in label subset = instance.data["subset"] start = context.data["frameStart"] @@ -31,16 +50,17 @@ class CollectInstanceData(pyblish.api.InstancePlugin): label = "{subset} ({start}-{end})".format(subset=subset, start=int(start), end=int(end)) + instance.data.update({ "label": label, # todo: Allow custom frame range per instance - "frameStart": context.data["frameStart"], - "frameEnd": context.data["frameEnd"], - "frameStartHandle": context.data["frameStartHandle"], - "frameEndHandle": context.data["frameStartHandle"], - "handleStart": context.data["handleStart"], - "handleEnd": context.data["handleEnd"], + "frameStart": start, + "frameEnd": end, + "frameStartHandle": start_handle, + "frameEndHandle": end_handle, + "handleStart": handle_start, + "handleEnd": handle_end, "fps": context.data["fps"], }) From c80842f5a95aa88a6eaec0dd19b8326c89db760a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 12 May 2023 13:19:33 +0200 Subject: [PATCH 239/360] fusion: improving custom frame range --- .../fusion/plugins/create/create_saver.py | 21 +++++++++++++++++++ .../plugins/publish/collect_instances.py | 8 +++---- .../fusion/plugins/publish/collect_render.py | 2 +- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index 58ffbe928a..860a873442 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -40,6 +40,11 @@ class CreateSaver(NewCreator): "{workdir}/renders/fusion/{subset}/{subset}.{frame}.{ext}") def create(self, subset_name, instance_data, pre_create_data): + self.pass_pre_attributes_to_instance( + instance_data, + pre_create_data + ) + instance_data.update({ "id": "pyblish.avalon.instance", "subset": subset_name @@ -215,6 +220,9 @@ class CreateSaver(NewCreator): attr_defs = [ self._get_render_target_enum(), self._get_reviewable_bool(), + BoolDef( + "custom_range", label="Custom range", default=False, + ) ] return attr_defs @@ -229,6 +237,19 @@ class CreateSaver(NewCreator): ] return attr_defs + def pass_pre_attributes_to_instance( + self, + instance_data, + pre_create_data, + keys=None + ): + if not keys: + keys = pre_create_data.keys() + + creator_attrs = instance_data["creator_attributes"] = {} + for pass_key in keys: + creator_attrs[pass_key] = pre_create_data[pass_key] + # These functions below should be moved to another file # so it can be used by other plugins. plugin.py ? diff --git a/openpype/hosts/fusion/plugins/publish/collect_instances.py b/openpype/hosts/fusion/plugins/publish/collect_instances.py index 9c27e3f027..997bd66e4a 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_instances.py +++ b/openpype/hosts/fusion/plugins/publish/collect_instances.py @@ -34,19 +34,17 @@ class CollectInstanceData(pyblish.api.InstancePlugin): start_handle = start - handle_start end_handle = end + handle_end - if creator_attributes["custom_range"]: + if creator_attributes.get("custom_range"): # get comp frame ranges start = context.data["compFrameStart"] end = context.data["compFrameEnd"] handle_start = 0 handle_end = 0 - start_handle = context.data["compFrameStartHandle"] - end_handle = context.data["compFrameEndHandle"] + start_handle = start + end_handle = end # Include start and end render frame in label subset = instance.data["subset"] - start = context.data["frameStart"] - end = context.data["frameEnd"] label = "{subset} ({start}-{end})".format(subset=subset, start=int(start), end=int(end)) diff --git a/openpype/hosts/fusion/plugins/publish/collect_render.py b/openpype/hosts/fusion/plugins/publish/collect_render.py index 4898226f03..26355b24d3 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_render.py +++ b/openpype/hosts/fusion/plugins/publish/collect_render.py @@ -83,7 +83,7 @@ class CollectFusionRender( frameEnd=inst.data["frameEnd"], handleStart=inst.data["handleStart"], handleEnd=inst.data["handleEnd"], - ignoreFrameHandleCheck=True, + ignoreFrameHandleCheck=(not inst.data.get("custom_range")), frameStep=1, fps=comp_frame_format_prefs.get("Rate"), app_version=comp.GetApp().Version, From 78b0e3daa1922402b4e8d2068208959cf22fa793 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 12 May 2023 13:35:15 +0200 Subject: [PATCH 240/360] removing unusable attribute GPU --- .../plugins/publish/submit_fusion_deadline.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py index af4bd37302..d51299506c 100644 --- a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py @@ -40,7 +40,6 @@ class FusionSubmitDeadline( group = "" department = "" limit_groups = {} - use_gpu = False env_allowed_keys = [] env_search_replace_values = {} @@ -69,11 +68,6 @@ class FusionSubmitDeadline( minimum=1, maximum=10 ), - BoolDef( - "use_gpu", - default=cls.use_gpu, - label="Use GPU" - ), BoolDef( "suspend_publish", default=False, @@ -206,11 +200,7 @@ class FusionSubmitDeadline( # Proxy: higher numbers smaller images for faster test renders # 1 = no proxy quality - "Proxy": 1, - - # using GPU by default - "UseGpu": attribute_values.get( - "use_gpu", self.use_gpu) + "Proxy": 1 }, # Mandatory for Deadline, may be empty From 5896c080132f4d62b3bc337ee586b40d7b4d991d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 12 May 2023 13:38:23 +0200 Subject: [PATCH 241/360] hound --- openpype/hosts/fusion/plugins/publish/collect_instances.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/collect_instances.py b/openpype/hosts/fusion/plugins/publish/collect_instances.py index 997bd66e4a..5a6a918730 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_instances.py +++ b/openpype/hosts/fusion/plugins/publish/collect_instances.py @@ -1,7 +1,3 @@ -from math import e -import os -from turtle import st - import pyblish.api From e56f4286c123149f3595920fcb1d550e0c1a8c38 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 12 May 2023 13:39:51 +0200 Subject: [PATCH 242/360] remove python syntax available since 3.8 from unreal addon --- openpype/hosts/unreal/addon.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/unreal/addon.py b/openpype/hosts/unreal/addon.py index db40d629bc..9ded333d7d 100644 --- a/openpype/hosts/unreal/addon.py +++ b/openpype/hosts/unreal/addon.py @@ -23,9 +23,10 @@ class UnrealAddon(OpenPypeModule, IHostAddon): UNREAL_ROOT_DIR, "integration", f"UE_{ue_version}", "Ayon" ) if not Path(unreal_plugin_path).exists(): - if compatible_versions := get_compatible_integration( + compatible_versions = get_compatible_integration( ue_version, Path(UNREAL_ROOT_DIR) / "integration" - ): + ) + if compatible_versions: unreal_plugin_path = compatible_versions[-1] / "Ayon" unreal_plugin_path = unreal_plugin_path.as_posix() From 233c7b34545f9c6cfec2e87eb41983099bc96849 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Fri, 12 May 2023 14:12:40 +0200 Subject: [PATCH 243/360] Update openpype/hosts/fusion/plugins/create/create_saver.py Co-authored-by: Roy Nieterau --- openpype/hosts/fusion/plugins/create/create_saver.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index 860a873442..4993c882de 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -174,7 +174,6 @@ class CreateSaver(NewCreator): tool.SetAttrs({"TOOLS_Name": subset}) def _collect_saver(self, tool): - self.log.info("Collecting saver..") attrs = tool.GetAttrs() keys = ["id", "asset", "subset", "task", "variant"] From 02279a51c8f97d0abf8e2d53d4d89acf1b13f1ce Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 12 May 2023 14:19:13 +0200 Subject: [PATCH 244/360] pr comments https://github.com/ynput/OpenPype/pull/4955#discussion_r1192264433 https://github.com/ynput/OpenPype/pull/4955#discussion_r1192267231 --- .../hosts/fusion/plugins/create/create_saver.py | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index 4993c882de..caffb8e4a1 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -227,26 +227,15 @@ class CreateSaver(NewCreator): def get_instance_attr_defs(self): """Settings for publish page""" - attr_defs = [ - self._get_render_target_enum(), - self._get_reviewable_bool(), - BoolDef( - "custom_range", label="Custom range", default=False, - ) - ] - return attr_defs + return self.get_pre_create_attr_defs() def pass_pre_attributes_to_instance( self, instance_data, - pre_create_data, - keys=None + pre_create_data ): - if not keys: - keys = pre_create_data.keys() - creator_attrs = instance_data["creator_attributes"] = {} - for pass_key in keys: + for pass_key in pre_create_data.keys(): creator_attrs[pass_key] = pre_create_data[pass_key] # These functions below should be moved to another file From 425ddc7b2bb9399dd2d187761a4cc8755b000b79 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 12 May 2023 14:26:15 +0200 Subject: [PATCH 245/360] fusion: reversing toolSaver back to tool --- .../hosts/fusion/plugins/publish/collect_render.py | 14 ++++++++++---- .../fusion/plugins/publish/extract_render_local.py | 2 +- .../publish/validate_create_folder_checked.py | 2 +- .../publish/validate_expected_frames_existence.py | 4 ++-- .../publish/validate_filename_has_extension.py | 2 +- .../plugins/publish/validate_saver_has_input.py | 2 +- .../plugins/publish/validate_saver_passthrough.py | 2 +- .../plugins/publish/validate_unique_subsets.py | 2 +- 8 files changed, 18 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/collect_render.py b/openpype/hosts/fusion/plugins/publish/collect_render.py index 26355b24d3..64d9aedc3b 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_render.py +++ b/openpype/hosts/fusion/plugins/publish/collect_render.py @@ -14,7 +14,7 @@ class FusionRenderInstance(RenderInstance): projectEntity = attr.ib(default=None) stagingDir = attr.ib(default=None) app_version = attr.ib(default=None) - toolSaver = attr.ib(default=None) + tool = attr.ib(default=None) workfileComp = attr.ib(default=None) publish_attributes = attr.ib(default={}) @@ -58,7 +58,7 @@ class CollectFusionRender( subset_name = inst.data["subset"] instance = FusionRenderInstance( family="render", - toolSaver=tool, + tool=tool, workfileComp=comp, families=instance_families, version=version, @@ -111,6 +111,8 @@ class CollectFusionRender( # to skip ExtractReview locally instance.families.remove("review") + # add new instance to the list and remove the original + # instance since it is not needed anymore instances.append(instance) instances_to_remove.append(inst) @@ -141,7 +143,7 @@ class CollectFusionRender( end = render_instance.frameEnd + render_instance.handleEnd path = ( - render_instance.toolSaver["Clip"] + render_instance.tool["Clip"] [render_instance.workfileComp.TIME_UNDEFINED] ) output_dir = os.path.dirname(path) @@ -163,7 +165,11 @@ class CollectFusionRender( return expected_files def _update_for_frames(self, instance): - """Update old saved instances to current publishing format""" + """Updating instance for render.frames family + + Adding representation data to the instance. Also setting + colorspaceData to the representation based on file rules. + """ expected_files = instance.data["expectedFiles"] diff --git a/openpype/hosts/fusion/plugins/publish/extract_render_local.py b/openpype/hosts/fusion/plugins/publish/extract_render_local.py index c2e38884c7..f093f7793f 100644 --- a/openpype/hosts/fusion/plugins/publish/extract_render_local.py +++ b/openpype/hosts/fusion/plugins/publish/extract_render_local.py @@ -79,7 +79,7 @@ class FusionRenderLocal( savers_to_render = [ # Get the saver tool from the instance - instance.data["toolSaver"] for instance in context if + instance.data["tool"] for instance in context if # Only active instances instance.data.get("publish", True) and # Only render.local instances diff --git a/openpype/hosts/fusion/plugins/publish/validate_create_folder_checked.py b/openpype/hosts/fusion/plugins/publish/validate_create_folder_checked.py index 82d34b0b5d..35c92163eb 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_create_folder_checked.py +++ b/openpype/hosts/fusion/plugins/publish/validate_create_folder_checked.py @@ -21,7 +21,7 @@ class ValidateCreateFolderChecked(pyblish.api.InstancePlugin): @classmethod def get_invalid(cls, instance): - tool = instance.data["toolSaver"] + tool = instance.data["tool"] create_dir = tool.GetInput("CreateDir") if create_dir == 0.0: cls.log.error( diff --git a/openpype/hosts/fusion/plugins/publish/validate_expected_frames_existence.py b/openpype/hosts/fusion/plugins/publish/validate_expected_frames_existence.py index aa89799867..3f84f59678 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_expected_frames_existence.py +++ b/openpype/hosts/fusion/plugins/publish/validate_expected_frames_existence.py @@ -23,7 +23,7 @@ class ValidateLocalFramesExistence(pyblish.api.InstancePlugin): if non_existing_frames is None: non_existing_frames = [] - tool = instance.data["toolSaver"] + tool = instance.data["tool"] expected_files = instance.data["expectedFiles"] @@ -56,7 +56,7 @@ class ValidateLocalFramesExistence(pyblish.api.InstancePlugin): def repair(cls, instance): invalid = cls.get_invalid(instance) if invalid: - tool = instance.data["toolSaver"] + tool = instance.data["tool"] # Change render target to local to render locally tool.SetData("openpype.creator_attributes.render_target", "local") diff --git a/openpype/hosts/fusion/plugins/publish/validate_filename_has_extension.py b/openpype/hosts/fusion/plugins/publish/validate_filename_has_extension.py index 1bf603e5b1..537e43c875 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_filename_has_extension.py +++ b/openpype/hosts/fusion/plugins/publish/validate_filename_has_extension.py @@ -34,7 +34,7 @@ class ValidateFilenameHasExtension(pyblish.api.InstancePlugin): fname, ext = os.path.splitext(path) if not ext: - tool = instance.data["toolSaver"] + tool = instance.data["tool"] cls.log.error("%s has no extension specified" % tool.Name) return [tool] diff --git a/openpype/hosts/fusion/plugins/publish/validate_saver_has_input.py b/openpype/hosts/fusion/plugins/publish/validate_saver_has_input.py index b409608ec3..faf2102a8b 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_saver_has_input.py +++ b/openpype/hosts/fusion/plugins/publish/validate_saver_has_input.py @@ -20,7 +20,7 @@ class ValidateSaverHasInput(pyblish.api.InstancePlugin): @classmethod def get_invalid(cls, instance): - saver = instance.data["toolSaver"] + saver = instance.data["tool"] if not saver.Input.GetConnectedOutput(): return [saver] diff --git a/openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py b/openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py index 677861a654..9004976dc5 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py +++ b/openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py @@ -37,7 +37,7 @@ class ValidateSaverPassthrough(pyblish.api.ContextPlugin): def is_invalid(self, instance): - saver = instance.data["toolSaver"] + saver = instance.data["tool"] attr = saver.GetAttrs() active = not attr["TOOLB_PassThrough"] diff --git a/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py b/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py index 6a65182fae..5b6ceb2fdb 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py +++ b/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py @@ -43,7 +43,7 @@ class ValidateUniqueSubsets(pyblish.api.ContextPlugin): invalid.extend(instances) # Return tools for the invalid instances so they can be selected - invalid = [instance.data["toolSaver"] for instance in invalid] + invalid = [instance.data["tool"] for instance in invalid] return invalid From 6968c7c8ba0bed66e131c81ec4b0f53eaa99c3da Mon Sep 17 00:00:00 2001 From: Thomas Fricard <51854004+friquette@users.noreply.github.com> Date: Fri, 12 May 2023 14:57:17 +0200 Subject: [PATCH 246/360] add shortcut to action if in configuration (#4927) Co-authored-by: Thomas Fricard --- .../python/common/scriptsmenu/scriptsmenu.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/openpype/vendor/python/common/scriptsmenu/scriptsmenu.py b/openpype/vendor/python/common/scriptsmenu/scriptsmenu.py index 6f6d0b5715..8ab621f757 100644 --- a/openpype/vendor/python/common/scriptsmenu/scriptsmenu.py +++ b/openpype/vendor/python/common/scriptsmenu/scriptsmenu.py @@ -19,9 +19,9 @@ class ScriptsMenu(QtWidgets.QMenu): Args: title (str): the name of the root menu which will be created - + parent (QtWidgets.QObject) : the QObject to parent the menu to - + Returns: None @@ -94,7 +94,7 @@ class ScriptsMenu(QtWidgets.QMenu): parent(QtWidgets.QWidget): the object to parent the menu to title(str): the title of the menu - + Returns: QtWidget.QMenu instance """ @@ -111,7 +111,7 @@ class ScriptsMenu(QtWidgets.QMenu): return menu def add_script(self, parent, title, command, sourcetype, icon=None, - tags=None, label=None, tooltip=None): + tags=None, label=None, tooltip=None, shortcut=None): """Create an action item which runs a script when clicked Args: @@ -134,6 +134,8 @@ class ScriptsMenu(QtWidgets.QMenu): tooltip (str): A tip for the user about the usage fo the tool + shortcut (str): A shortcut to run the command + Returns: QtWidget.QAction instance @@ -166,6 +168,9 @@ class ScriptsMenu(QtWidgets.QMenu): raise RuntimeError("Script action can't be " "processed: {}".format(e)) + if shortcut: + script_action.setShortcut(shortcut) + if icon: iconfile = os.path.expandvars(icon) script_action.iconfile = iconfile @@ -253,7 +258,7 @@ class ScriptsMenu(QtWidgets.QMenu): def _update_search(self, search): """Hide all the samples which do not match the user's import - + Returns: None From d3428da3937bb0dbad2e1f86705e95d2bbe6b695 Mon Sep 17 00:00:00 2001 From: kaa Date: Fri, 12 May 2023 15:19:23 +0200 Subject: [PATCH 247/360] fix get_linked_assets project name arg (#4940) --- openpype/pipeline/workfile/build_workfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/workfile/build_workfile.py b/openpype/pipeline/workfile/build_workfile.py index 26b17fa151..8329487839 100644 --- a/openpype/pipeline/workfile/build_workfile.py +++ b/openpype/pipeline/workfile/build_workfile.py @@ -186,7 +186,7 @@ class BuildWorkfile: if link_context_profiles: # Find and append linked assets if preset has set linked mapping - link_assets = get_linked_assets(current_asset_entity) + link_assets = get_linked_assets(project_name, current_asset_entity) if link_assets: assets.extend(link_assets) From d8b569b160167d491ab29367d00d0f504f40c0ab Mon Sep 17 00:00:00 2001 From: Sharkitty <81646000+Sharkitty@users.noreply.github.com> Date: Fri, 12 May 2023 14:14:44 +0000 Subject: [PATCH 248/360] Update openpype/plugins/inventory/remove_and_load.py Discover plugins only once Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/plugins/inventory/remove_and_load.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/inventory/remove_and_load.py b/openpype/plugins/inventory/remove_and_load.py index 062a44354b..d90cc9462c 100644 --- a/openpype/plugins/inventory/remove_and_load.py +++ b/openpype/plugins/inventory/remove_and_load.py @@ -17,10 +17,10 @@ class RemoveAndLoad(InventoryAction): def process(self, containers): project_name = get_current_project_name() + loaders = discover_loader_plugins(project_name=project_name) for container in containers: # Get loader loader_name = container["loader"] - loaders = discover_loader_plugins(project_name=project_name) for plugin in loaders: if get_loader_identifier(plugin) == loader_name: loader = plugin From 9d6cd8d2c83fe3d98aca92fea206c34b208f49a0 Mon Sep 17 00:00:00 2001 From: Seyedmohammadreza Hashemizadeh Date: Fri, 12 May 2023 16:21:58 +0200 Subject: [PATCH 249/360] add options --- openpype/hosts/maya/api/lib.py | 4 ++-- openpype/hosts/maya/plugins/load/load_reference.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index f814187cc1..d44f1754f9 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -3937,7 +3937,7 @@ def get_capture_preset(task_name, task_type, subset, project_settings, log): return capture_preset or {} -def create_rig_animation_instance(nodes, context, namespace, log=None): +def create_rig_animation_instance(nodes, context, namespace, options, log=None): """Create an animation publish instance for loaded rigs. See the RecreateRigAnimationInstance inventory action on how to use this @@ -3982,6 +3982,6 @@ def create_rig_animation_instance(nodes, context, namespace, log=None): creator_plugin, name=namespace, asset=asset, - options={"useSelection": True}, + options=options.update({"useSelection": True}), data={"dependencies": dependency} ) diff --git a/openpype/hosts/maya/plugins/load/load_reference.py b/openpype/hosts/maya/plugins/load/load_reference.py index 7d717dcd44..31b6e9d624 100644 --- a/openpype/hosts/maya/plugins/load/load_reference.py +++ b/openpype/hosts/maya/plugins/load/load_reference.py @@ -223,7 +223,7 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): def _post_process_rig(self, name, namespace, context, options): nodes = self[:] create_rig_animation_instance( - nodes, context, namespace, log=self.log + nodes, context, namespace, options, log=self.log ) def _lock_camera_transforms(self, nodes): From ce1e45f7082a108258b2fa1bbd3e5a37e25f52e3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 12 May 2023 16:35:10 +0200 Subject: [PATCH 250/360] fix key assignment on instance data (#4966) --- .../ftrack/plugins/publish/integrate_hierarchy_ftrack.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py b/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py index 9f35424d42..6daaea5f18 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py @@ -378,7 +378,7 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): existing_tasks.append(task_name_low) for instance in instances_by_task_name[task_name_low]: - instance["ftrackTask"] = child + instance.data["ftrackTask"] = child for task_name in tasks: task_type = tasks[task_name]["type"] From ab357eb03e316691057633b5762cdb094b35ef8d Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Fri, 12 May 2023 16:44:44 +0200 Subject: [PATCH 251/360] Addons directory (#4893) * Add addons directory * add addons dir to modules dirs automatically --------- Co-authored-by: iLLiCiTiT --- .gitignore | 6 ++++++ openpype/addons/README.md | 3 +++ openpype/modules/base.py | 6 +++++- 3 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 openpype/addons/README.md diff --git a/.gitignore b/.gitignore index 18e7cd7bf2..50f52f65a3 100644 --- a/.gitignore +++ b/.gitignore @@ -112,3 +112,9 @@ tools/run_eventserver.* tools/dev_* .github_changelog_generator + + +# Addons +######## +/openpype/addons/* +!/openpype/addons/README.md diff --git a/openpype/addons/README.md b/openpype/addons/README.md new file mode 100644 index 0000000000..92b8b8c07c --- /dev/null +++ b/openpype/addons/README.md @@ -0,0 +1,3 @@ +This directory is for storing external addons that needs to be included in the pipeline when distributed. + +The directory is ignored by Git, but included in the zip and installation files. diff --git a/openpype/modules/base.py b/openpype/modules/base.py index ed1eeb04cd..732525b6eb 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -311,6 +311,7 @@ def _load_modules(): # Look for OpenPype modules in paths defined with `get_module_dirs` # - dynamically imported OpenPype modules and addons 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__)) @@ -318,8 +319,11 @@ def _load_modules(): module_dirs.insert(0, hosts_dir) module_dirs.insert(0, current_dir) + addons_dir = os.path.join(os.path.dirname(current_dir), "addons") + module_dirs.append(addons_dir) + processed_paths = set() - for dirpath in module_dirs: + for dirpath in frozenset(module_dirs): # Skip already processed paths if dirpath in processed_paths: continue From 5ff9ab368b392e48dbc1c522a461d5b52c300b79 Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Fri, 12 May 2023 17:07:52 +0200 Subject: [PATCH 252/360] pre cache loader by name, loader name in error --- openpype/plugins/inventory/remove_and_load.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/openpype/plugins/inventory/remove_and_load.py b/openpype/plugins/inventory/remove_and_load.py index d90cc9462c..ae66b95f6e 100644 --- a/openpype/plugins/inventory/remove_and_load.py +++ b/openpype/plugins/inventory/remove_and_load.py @@ -17,17 +17,18 @@ class RemoveAndLoad(InventoryAction): def process(self, containers): project_name = get_current_project_name() - loaders = discover_loader_plugins(project_name=project_name) + loaders_by_name = { + get_loader_identifier(plugin): plugin + for plugin in discover_loader_plugins(project_name=project_name) + } for container in containers: # Get loader loader_name = container["loader"] - for plugin in loaders: - if get_loader_identifier(plugin) == loader_name: - loader = plugin - break - else: + loader = loaders_by_name.get(loader_name, None) + if not loader: raise RuntimeError( - "Failed to get loader, can't remove and load container" + "Failed to get loader '{}', can't remove " + "and load container".format(loader_name) ) # Get representation From 78f8bbfd8066604d490aa89b56e0061a0d8d191b Mon Sep 17 00:00:00 2001 From: Ember Light <49758407+EmberLightVFX@users.noreply.github.com> Date: Fri, 12 May 2023 17:29:38 +0200 Subject: [PATCH 253/360] Kitsu - Add "image", "online" and "plate" to review families (#4923) * Add kitsu review to the default png review's tags * Add "image", "online" and "plate" as possible review families --- .../modules/kitsu/plugins/publish/integrate_kitsu_note.py | 7 ++++--- .../kitsu/plugins/publish/integrate_kitsu_review.py | 3 +-- openpype/settings/defaults/project_settings/global.json | 3 ++- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py index f8e56377bb..6e5dd056f3 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py @@ -9,7 +9,7 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): order = pyblish.api.IntegratorOrder label = "Kitsu Note and Status" - families = ["render", "kitsu"] + families = ["render", "image", "online", "plate", "kitsu"] # status settings set_status_note = False @@ -52,8 +52,9 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): for instance in context: # Check if instance is a review by checking its family # Allow a match to primary family or any of families - families = set([instance.data["family"]] + - instance.data.get("families", [])) + families = set( + [instance.data["family"]] + instance.data.get("families", []) + ) if "review" not in families: continue diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py index e05ff05f50..bbed4a3024 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py @@ -8,11 +8,10 @@ class IntegrateKitsuReview(pyblish.api.InstancePlugin): order = pyblish.api.IntegratorOrder + 0.01 label = "Kitsu Review" - families = ["render", "kitsu"] + families = ["render", "image", "online", "plate", "kitsu"] optional = True def process(self, instance): - # Check comment has been created comment_id = instance.data.get("kitsu_comment", {}).get("id") if not comment_id: diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 50b62737d8..75f335f1de 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -82,7 +82,8 @@ "png": { "ext": "png", "tags": [ - "ftrackreview" + "ftrackreview", + "kitsureview" ], "burnins": [], "ffmpeg_args": { From 454abe459ed834e6d10f014708c83a7142d1840d Mon Sep 17 00:00:00 2001 From: Seyedmohammadreza Hashemizadeh Date: Fri, 12 May 2023 17:53:05 +0200 Subject: [PATCH 254/360] add custom animation subset --- openpype/hosts/maya/api/lib.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index d44f1754f9..124e0e5b8a 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -3937,7 +3937,9 @@ def get_capture_preset(task_name, task_type, subset, project_settings, log): return capture_preset or {} -def create_rig_animation_instance(nodes, context, namespace, options, log=None): +def create_rig_animation_instance( + nodes, context, namespace, options, log=None +): """Create an animation publish instance for loaded rigs. See the RecreateRigAnimationInstance inventory action on how to use this @@ -3971,6 +3973,12 @@ def create_rig_animation_instance(nodes, context, namespace, options, log=None): asset = legacy_io.Session["AVALON_ASSET"] dependency = str(context["representation"]["_id"]) + custom_subset = options.get("animationSubsetName") + + if custom_subset: + rig_subset = context['subset']['name'] + namespace = namespace.replace(rig_subset, custom_subset) + if log: log.info("Creating subset: {}".format(namespace)) @@ -3982,6 +3990,6 @@ def create_rig_animation_instance(nodes, context, namespace, options, log=None): creator_plugin, name=namespace, asset=asset, - options=options.update({"useSelection": True}), + options={"useSelection": True}, data={"dependencies": dependency} ) From eaefd594b6f2f89a0e8a1accbb5912b1a744035a Mon Sep 17 00:00:00 2001 From: Ynbot Date: Sat, 13 May 2023 03:25:18 +0000 Subject: [PATCH 255/360] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index 7df154fe1e..319a58d384 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.7-nightly.2" +__version__ = "3.15.7-nightly.3" From 927f92df87d3bae5d035c3ae4f857ac171e84dfa Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 13 May 2023 03:26:01 +0000 Subject: [PATCH 256/360] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 0d75b669d2..7d224aa73f 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.15.7-nightly.3 - 3.15.7-nightly.2 - 3.15.7-nightly.1 - 3.15.6 @@ -134,7 +135,6 @@ body: - 3.14.1-nightly.4 - 3.14.1-nightly.3 - 3.14.1-nightly.2 - - 3.14.1-nightly.1 validations: required: true - type: dropdown From 11b895624afb2d1dde099eefe09c46b7d3f65b24 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 15 May 2023 10:44:58 +0100 Subject: [PATCH 257/360] Fix transform when loading layout to match existing assets --- .../plugins/load/load_layout_existing.py | 81 +++++++------------ 1 file changed, 28 insertions(+), 53 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_layout_existing.py b/openpype/hosts/unreal/plugins/load/load_layout_existing.py index 96ee8cfc25..f4a2d1e7cc 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout_existing.py +++ b/openpype/hosts/unreal/plugins/load/load_layout_existing.py @@ -89,50 +89,26 @@ class ExistingLayoutLoader(plugin.Loader): raise NotImplementedError( f"Unreal version {ue_major} not supported") - def _get_transform(self, ext, import_data, lasset): - conversion = unreal.Matrix.IDENTITY.transform() - fbx_tuning = unreal.Matrix.IDENTITY.transform() + def _transform_from_basis(self, transform, basis): + """Transform a transform from a basis to a new basis.""" + # Get the basis matrix + basis_matrix = unreal.Matrix( + basis[0], + basis[1], + basis[2], + basis[3] + ) + transform_matrix = unreal.Matrix( + transform[0], + transform[1], + transform[2], + transform[3] + ) - basis = unreal.Matrix( - lasset.get('basis')[0], - lasset.get('basis')[1], - lasset.get('basis')[2], - lasset.get('basis')[3] - ).transform() - transform = unreal.Matrix( - lasset.get('transform_matrix')[0], - lasset.get('transform_matrix')[1], - lasset.get('transform_matrix')[2], - lasset.get('transform_matrix')[3] - ).transform() + new_transform = ( + basis_matrix.get_inverse() * transform_matrix * basis_matrix) - # Check for the conversion settings. We cannot access - # the alembic conversion settings, so we assume that - # the maya ones have been applied. - if ext == '.fbx': - loc = import_data.import_translation - rot = import_data.import_rotation.to_vector() - scale = import_data.import_uniform_scale - conversion = unreal.Transform( - location=[loc.x, loc.y, loc.z], - rotation=[rot.x, rot.y, rot.z], - scale=[-scale, scale, scale] - ) - fbx_tuning = unreal.Transform( - rotation=[180.0, 0.0, 90.0], - scale=[1.0, 1.0, 1.0] - ) - elif ext == '.abc': - # This is the standard conversion settings for - # alembic files from Maya. - conversion = unreal.Transform( - location=[0.0, 0.0, 0.0], - rotation=[0.0, 0.0, 0.0], - scale=[1.0, -1.0, 1.0] - ) - - new_transform = (basis.inverse() * transform * basis) - return fbx_tuning * conversion.inverse() * new_transform + return new_transform.transform() def _spawn_actor(self, obj, lasset): actor = EditorLevelLibrary.spawn_actor_from_object( @@ -140,16 +116,13 @@ class ExistingLayoutLoader(plugin.Loader): ) actor.set_actor_label(lasset.get('instance_name')) - smc = actor.get_editor_property('static_mesh_component') - mesh = smc.get_editor_property('static_mesh') - import_data = mesh.get_editor_property('asset_import_data') - filename = import_data.get_first_filename() - path = Path(filename) - transform = self._get_transform( - path.suffix, import_data, lasset) + transform = lasset.get('transform_matrix') + basis = lasset.get('basis') - actor.set_actor_transform(transform, False, True) + t = self._transform_from_basis(transform, basis) + + actor.set_actor_transform(t, False, True) @staticmethod def _get_fbx_loader(loaders, family): @@ -320,9 +293,11 @@ class ExistingLayoutLoader(plugin.Loader): containers.append(container) # Set the transform for the actor. - transform = self._get_transform( - path.suffix, import_data, lasset) - actor.set_actor_transform(transform, False, True) + transform = lasset.get('transform_matrix') + basis = lasset.get('basis') + + t = self._transform_from_basis(transform, basis) + actor.set_actor_transform(t, False, True) actors_matched.append(actor) found = True From 2369d50224cd4768c182cee7004df4da7d381f27 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 15 May 2023 11:25:56 +0100 Subject: [PATCH 258/360] Skipping rendersetup for members. --- .../maya/plugins/publish/validate_instance_has_members.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/validate_instance_has_members.py b/openpype/hosts/maya/plugins/publish/validate_instance_has_members.py index 4870f27bff..fcafc2be79 100644 --- a/openpype/hosts/maya/plugins/publish/validate_instance_has_members.py +++ b/openpype/hosts/maya/plugins/publish/validate_instance_has_members.py @@ -14,6 +14,11 @@ class ValidateInstanceHasMembers(pyblish.api.InstancePlugin): @classmethod def get_invalid(cls, instance): + # Allow renderlayer and workfile to be empty + skip_families = ["workfile", "renderlayer", "rendersetup"] + if instance.data.get("family") in skip_families: + return + invalid = list() if not instance.data["setMembers"]: objectset_name = instance.data['name'] From 5625c1f45b542286877bc7a8894c7562053f7a30 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 15 May 2023 12:09:25 +0100 Subject: [PATCH 259/360] Use proper variable names --- .../hosts/unreal/plugins/load/load_layout_existing.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_layout_existing.py b/openpype/hosts/unreal/plugins/load/load_layout_existing.py index f4a2d1e7cc..929a9a1399 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout_existing.py +++ b/openpype/hosts/unreal/plugins/load/load_layout_existing.py @@ -120,9 +120,9 @@ class ExistingLayoutLoader(plugin.Loader): transform = lasset.get('transform_matrix') basis = lasset.get('basis') - t = self._transform_from_basis(transform, basis) + computed_transform = self._transform_from_basis(transform, basis) - actor.set_actor_transform(t, False, True) + actor.set_actor_transform(computed_transform, False, True) @staticmethod def _get_fbx_loader(loaders, family): @@ -296,8 +296,9 @@ class ExistingLayoutLoader(plugin.Loader): transform = lasset.get('transform_matrix') basis = lasset.get('basis') - t = self._transform_from_basis(transform, basis) - actor.set_actor_transform(t, False, True) + computed_transform = self._transform_from_basis( + transform, basis) + actor.set_actor_transform(computed_transform, False, True) actors_matched.append(actor) found = True From b47d172944aa1e0039b3fb554093dbc7508b3033 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 15 May 2023 12:30:32 +0100 Subject: [PATCH 260/360] Move family check to process --- .../plugins/publish/validate_instance_has_members.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_instance_has_members.py b/openpype/hosts/maya/plugins/publish/validate_instance_has_members.py index fcafc2be79..63849cfd12 100644 --- a/openpype/hosts/maya/plugins/publish/validate_instance_has_members.py +++ b/openpype/hosts/maya/plugins/publish/validate_instance_has_members.py @@ -13,12 +13,6 @@ class ValidateInstanceHasMembers(pyblish.api.InstancePlugin): @classmethod def get_invalid(cls, instance): - - # Allow renderlayer and workfile to be empty - skip_families = ["workfile", "renderlayer", "rendersetup"] - if instance.data.get("family") in skip_families: - return - invalid = list() if not instance.data["setMembers"]: objectset_name = instance.data['name'] @@ -27,6 +21,10 @@ class ValidateInstanceHasMembers(pyblish.api.InstancePlugin): return invalid def process(self, instance): + # Allow renderlayer and workfile to be empty + skip_families = ["workfile", "renderlayer", "rendersetup"] + if instance.data.get("family") in skip_families: + return invalid = self.get_invalid(instance) if invalid: From 0845ba29dd4edd1872118a65fe72653b6e429531 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 15 May 2023 14:08:35 +0200 Subject: [PATCH 261/360] General: Project Anatomy on creators (#4962) * added project anatomy to create context * added project anatomy to create plugin --- openpype/pipeline/create/context.py | 19 ++++++++++++++++++- openpype/pipeline/create/creator_plugins.py | 16 +++++++++++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 382bbea05e..2fc0669732 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -23,7 +23,7 @@ from openpype.lib.attribute_definitions import ( get_default_values, ) from openpype.host import IPublishHost, IWorkfileHost -from openpype.pipeline import legacy_io +from openpype.pipeline import legacy_io, Anatomy from openpype.pipeline.plugin_discover import DiscoverResult from .creator_plugins import ( @@ -1383,6 +1383,8 @@ class CreateContext: self._current_task_name = None self._current_workfile_path = None + self._current_project_anatomy = None + self._host_is_valid = host_is_valid # Currently unused variable self.headless = headless @@ -1546,6 +1548,18 @@ class CreateContext: return self._current_workfile_path + def get_current_project_anatomy(self): + """Project anatomy for current project. + + Returns: + Anatomy: Anatomy object ready to be used. + """ + + if self._current_project_anatomy is None: + self._current_project_anatomy = Anatomy( + self._current_project_name) + return self._current_project_anatomy + @property def context_has_changed(self): """Host context has changed. @@ -1568,6 +1582,7 @@ class CreateContext: ) project_name = property(get_current_project_name) + project_anatomy = property(get_current_project_anatomy) @property def log(self): @@ -1680,6 +1695,8 @@ class CreateContext: self._current_task_name = task_name self._current_workfile_path = workfile_path + self._current_project_anatomy = None + def reset_plugins(self, discover_publish_plugins=True): """Reload plugins. diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index bd3fbaf78f..9e47e9cc12 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -231,10 +231,24 @@ class BaseCreator: @property def project_name(self): - """Family that plugin represents.""" + """Current project name. + + Returns: + str: Name of a project. + """ return self.create_context.project_name + @property + def project_anatomy(self): + """Current project anatomy. + + Returns: + Anatomy: Project anatomy object. + """ + + return self.create_context.project_anatomy + @property def host(self): return self.create_context.host From 40d4fea857202fcb4194376511f208d8db0f3655 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Mon, 15 May 2023 14:10:49 +0200 Subject: [PATCH 262/360] Maya: Validate shader name - OP-5903 (#4971) * Fix regex matching. * Add active setting * Update openpype/hosts/maya/plugins/publish/validate_shader_name.py --- .../plugins/publish/validate_shader_name.py | 20 +++++++++---------- .../defaults/project_settings/maya.json | 1 + .../schemas/schema_maya_publish.json | 5 +++++ 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_shader_name.py b/openpype/hosts/maya/plugins/publish/validate_shader_name.py index b3e51f011d..034db471da 100644 --- a/openpype/hosts/maya/plugins/publish/validate_shader_name.py +++ b/openpype/hosts/maya/plugins/publish/validate_shader_name.py @@ -50,7 +50,8 @@ class ValidateShaderName(pyblish.api.InstancePlugin): asset_name = instance.data.get("asset", None) # Check the number of connected shadingEngines per shape - r = re.compile(cls.regex) + regex_compile = re.compile(cls.regex) + error_message = "object {0} has invalid shader name {1}" for shape in shapes: shading_engines = cmds.listConnections(shape, destination=True, @@ -60,19 +61,18 @@ class ValidateShaderName(pyblish.api.InstancePlugin): ) for shader in shaders: - m = r.match(cls.regex, shader) + m = regex_compile.match(shader) if m is None: invalid.append(shape) - cls.log.error( - "object {0} has invalid shader name {1}".format(shape, - shader) - ) + cls.log.error(error_message.format(shape, shader)) else: - if 'asset' in r.groupindex: + if 'asset' in regex_compile.groupindex: if m.group('asset') != asset_name: invalid.append(shape) - cls.log.error(("object {0} has invalid " - "shader name {1}").format(shape, - shader)) + message = error_message + message += " with missing asset name \"{2}\"" + cls.log.error( + message.format(shape, shader, asset_name) + ) return invalid diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 72b330ce7a..a2a43eefb5 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -734,6 +734,7 @@ "ValidateShaderName": { "enabled": false, "optional": true, + "active": true, "regex": "(?P.*)_(.*)_SHD" }, "ValidateShadingEngine": { 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 346948c658..07c8d8715b 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 @@ -126,6 +126,11 @@ "key": "optional", "label": "Optional" }, + { + "type": "boolean", + "key": "active", + "label": "Active" + }, { "type": "label", "label": "Shader name regex can use named capture group asset to validate against current asset name.

Example:
^.*(?P=<asset>.+)_SHD

" From 251470997783c56c0bf075fd1e206e37e1ccb9d3 Mon Sep 17 00:00:00 2001 From: Seyedmohammadreza Hashemizadeh Date: Mon, 15 May 2023 15:46:20 +0200 Subject: [PATCH 263/360] use custom namespace for animation instance --- openpype/hosts/maya/api/lib.py | 55 +++++++++++++++++++++++++++++-- openpype/hosts/maya/api/plugin.py | 38 --------------------- 2 files changed, 52 insertions(+), 41 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 124e0e5b8a..b84056f5ce 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -190,6 +190,44 @@ def maintained_selection(): cmds.select(clear=True) +def get_custom_namespace(custom_namespace): + """Return unique namespace. + + The input namespace can contain a single group + of '#' number tokens to indicate where the namespace's + unique index should go. The amount of tokens defines + the zero padding of the number, e.g ### turns into 001. + + Warning: Note that a namespace will always be + prefixed with a _ if it starts with a digit + + Example: + >>> get_custom_namespace("myspace_##_") + # myspace_01_ + >>> get_custom_namespace("##_myspace") + # _01_myspace + >>> get_custom_namespace("myspace##") + # myspace01 + + """ + split = re.split("([#]+)", custom_namespace, 1) + + if len(split) == 3: + base, padding, suffix = split + padding = "%0{}d".format(len(padding)) + else: + base = split[0] + padding = "%02d" # default padding + suffix = "" + + return unique_namespace( + base, + format=padding, + prefix="_" if not base or base[0].isdigit() else "", + suffix=suffix + ) + + def unique_namespace(namespace, format="%02d", prefix="", suffix=""): """Return unique namespace @@ -3974,10 +4012,21 @@ def create_rig_animation_instance( dependency = str(context["representation"]["_id"]) custom_subset = options.get("animationSubsetName") - if custom_subset: - rig_subset = context['subset']['name'] - namespace = namespace.replace(rig_subset, custom_subset) + formatting_data = { + "asset_name": context['asset']['name'], + "asset_type": context['asset']['type'], + "subset": context['subset']['name'], + "family": ( + context['subset']['data'].get('family') or + context['subset']['data']['families'][0] + ) + } + namespace = get_custom_namespace( + custom_subset.format( + **formatting_data + ) + ) if log: log.info("Creating subset: {}".format(namespace)) diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py index 714278ba6c..3fce92db28 100644 --- a/openpype/hosts/maya/api/plugin.py +++ b/openpype/hosts/maya/api/plugin.py @@ -84,44 +84,6 @@ def get_reference_node_parents(ref): return parents -def get_custom_namespace(custom_namespace): - """Return unique namespace. - - The input namespace can contain a single group - of '#' number tokens to indicate where the namespace's - unique index should go. The amount of tokens defines - the zero padding of the number, e.g ### turns into 001. - - Warning: Note that a namespace will always be - prefixed with a _ if it starts with a digit - - Example: - >>> get_custom_namespace("myspace_##_") - # myspace_01_ - >>> get_custom_namespace("##_myspace") - # _01_myspace - >>> get_custom_namespace("myspace##") - # myspace01 - - """ - split = re.split("([#]+)", custom_namespace, 1) - - if len(split) == 3: - base, padding, suffix = split - padding = "%0{}d".format(len(padding)) - else: - base = split[0] - padding = "%02d" # default padding - suffix = "" - - return lib.unique_namespace( - base, - format=padding, - prefix="_" if not base or base[0].isdigit() else "", - suffix=suffix - ) - - class Creator(LegacyCreator): defaults = ['Main'] From 8ffe7f55522b364c07e6fd65cc8ad9cf5a125ed9 Mon Sep 17 00:00:00 2001 From: Seyedmohammadreza Hashemizadeh Date: Mon, 15 May 2023 16:27:03 +0200 Subject: [PATCH 264/360] fix function call --- openpype/hosts/maya/api/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py index 3fce92db28..604ff101db 100644 --- a/openpype/hosts/maya/api/plugin.py +++ b/openpype/hosts/maya/api/plugin.py @@ -178,7 +178,7 @@ class ReferenceLoader(Loader): count = options.get("count") or 1 for c in range(0, count): - namespace = get_custom_namespace(custom_namespace) + namespace = lib.get_custom_namespace(custom_namespace) group_name = "{}:{}".format( namespace, custom_group_name From b192f8395d7cdd9c417e96e06a112e5761983b9d Mon Sep 17 00:00:00 2001 From: Seyedmohammadreza Hashemizadeh Date: Mon, 15 May 2023 16:34:37 +0200 Subject: [PATCH 265/360] update docstring --- openpype/hosts/maya/api/lib.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index b84056f5ce..56ba59186d 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -3987,6 +3987,7 @@ def create_rig_animation_instance( nodes (list): Member nodes of the rig instance. context (dict): Representation context of the rig container namespace (str): Namespace of the rig container + options (dict): Additional loader data log (logging.Logger, optional): Logger to log to if provided Returns: From f8ad6966fb53248e4c33ea2d3166c076fb078a42 Mon Sep 17 00:00:00 2001 From: Seyedmohammadreza Hashemizadeh Date: Mon, 15 May 2023 17:12:17 +0200 Subject: [PATCH 266/360] make options optional --- openpype/hosts/maya/api/lib.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 56ba59186d..7f160afd3e 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -3976,7 +3976,7 @@ def get_capture_preset(task_name, task_type, subset, project_settings, log): def create_rig_animation_instance( - nodes, context, namespace, options, log=None + nodes, context, namespace, options=None, log=None ): """Create an animation publish instance for loaded rigs. @@ -3987,13 +3987,16 @@ def create_rig_animation_instance( nodes (list): Member nodes of the rig instance. context (dict): Representation context of the rig container namespace (str): Namespace of the rig container - options (dict): Additional loader data + options (dict, optional): Additional loader data log (logging.Logger, optional): Logger to log to if provided Returns: None """ + if options is None: + options = {} + output = next((node for node in nodes if node.endswith("out_SET")), None) controls = next((node for node in nodes if From a051bb281a880c155c59083c153d9ed4e44e615f Mon Sep 17 00:00:00 2001 From: Mreza Hashemizadeh <68907585+mre7a@users.noreply.github.com> Date: Mon, 15 May 2023 17:22:40 +0200 Subject: [PATCH 267/360] Update openpype/hosts/maya/plugins/load/load_reference.py Co-authored-by: Toke Jepsen --- openpype/hosts/maya/plugins/load/load_reference.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/load/load_reference.py b/openpype/hosts/maya/plugins/load/load_reference.py index 31b6e9d624..f4a4a44344 100644 --- a/openpype/hosts/maya/plugins/load/load_reference.py +++ b/openpype/hosts/maya/plugins/load/load_reference.py @@ -223,7 +223,7 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): def _post_process_rig(self, name, namespace, context, options): nodes = self[:] create_rig_animation_instance( - nodes, context, namespace, options, log=self.log + nodes, context, namespace, options=options, log=self.log ) def _lock_camera_transforms(self, nodes): From 628ecbe5e2e6fb2c11df0185726e60a168eba013 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 15 May 2023 17:58:12 +0200 Subject: [PATCH 268/360] :truck: move test file --- .../unreal}/plugins/publish/test_validate_sequence_frames.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/unit/openpype/{ => hosts/unreal}/plugins/publish/test_validate_sequence_frames.py (100%) diff --git a/tests/unit/openpype/plugins/publish/test_validate_sequence_frames.py b/tests/unit/openpype/hosts/unreal/plugins/publish/test_validate_sequence_frames.py similarity index 100% rename from tests/unit/openpype/plugins/publish/test_validate_sequence_frames.py rename to tests/unit/openpype/hosts/unreal/plugins/publish/test_validate_sequence_frames.py From 3df689787699f6f51557e058357273689a64cbf0 Mon Sep 17 00:00:00 2001 From: Seyedmohammadreza Hashemizadeh Date: Mon, 15 May 2023 18:01:52 +0200 Subject: [PATCH 269/360] Add some documentations --- website/docs/admin_hosts_maya.md | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/website/docs/admin_hosts_maya.md b/website/docs/admin_hosts_maya.md index f0b8710246..700822843f 100644 --- a/website/docs/admin_hosts_maya.md +++ b/website/docs/admin_hosts_maya.md @@ -247,15 +247,24 @@ Fill in the necessary fields (the optional fields are regex filters) ![new place holder](assets/maya-placeholder_new.png) - - Builder type: Whether the the placeholder should load current asset representations or linked assets representations + - ***Builder type***: Whether the the placeholder should load current asset representations or linked assets representations - - Representation: Representation that will be loaded (ex: ma, abc, png, etc...) + - ***Representation***: Representation that will be loaded (ex: ma, abc, png, etc...) - - Family: Family of the representation to load (main, look, image, etc ...) + - ***Family***: Family of the representation to load (main, look, image, etc ...) - - Loader: Placeholder loader name that will be used to load corresponding representations + - ***Loader***: Placeholder loader name that will be used to load corresponding representations + + - ***Order***: Priority for current placeholder loader (priority is lowest first, highest last) + + - ***Loader arguments***: Loader arguments dictionary can be used to pass optional data to loaders. + One use case is to define a custom Subset name for the animation instances created while loading Rig references.This follows the custom namespace system used by loaders. + + **Example** + ``` + {"animationSubsetName": "{asset_name}_animation_{subset}_##_"} + ``` - - Order: Priority for current placeholder loader (priority is lowest first, highet last) - **Save your template** From f51af7de278e77724c058196eed7dd487da07dfe Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 15 May 2023 21:54:01 +0200 Subject: [PATCH 270/360] fusion: renaming comp frame range related attributes --- .../fusion/plugins/publish/collect_comp_frame_range.py | 8 ++++---- .../hosts/fusion/plugins/publish/collect_instances.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py b/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py index 38d6577667..08bdad3120 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py +++ b/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py @@ -41,9 +41,9 @@ class CollectFusionCompFrameRanges(pyblish.api.ContextPlugin): ) = get_comp_render_range(comp) data = {} - data["compFrameStart"] = int(start) - data["compFrameEnd"] = int(end) - data["compFrameStartHandle"] = int(global_start) - data["compFrameEndHandle"] = int(global_end) + data["renderFrameStart"] = int(start) + data["renderFrameEnd"] = int(end) + data["compFrameStart"] = int(global_start) + data["compFrameEnd"] = int(global_end) context.data.update(data) diff --git a/openpype/hosts/fusion/plugins/publish/collect_instances.py b/openpype/hosts/fusion/plugins/publish/collect_instances.py index 5a6a918730..c1c23ec570 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_instances.py +++ b/openpype/hosts/fusion/plugins/publish/collect_instances.py @@ -32,8 +32,8 @@ class CollectInstanceData(pyblish.api.InstancePlugin): if creator_attributes.get("custom_range"): # get comp frame ranges - start = context.data["compFrameStart"] - end = context.data["compFrameEnd"] + start = context.data["renderFrameStart"] + end = context.data["renderFrameEnd"] handle_start = 0 handle_end = 0 start_handle = start From 478c85d7cc96188db55af4cbc8c1a2479ff8717e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 15 May 2023 22:09:18 +0200 Subject: [PATCH 271/360] Fusion: renaming confusing attribute name and label --- openpype/hosts/fusion/plugins/create/create_saver.py | 4 +++- openpype/hosts/fusion/plugins/publish/collect_instances.py | 2 +- openpype/hosts/fusion/plugins/publish/collect_render.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index 1a60526e42..67b1465ec7 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -200,7 +200,9 @@ class CreateSaver(NewCreator): self._get_render_target_enum(), self._get_reviewable_bool(), BoolDef( - "custom_range", label="Custom range", default=False, + "viewer_render_range", + label="Viewer render in/out", + default=False, ) ] return attr_defs diff --git a/openpype/hosts/fusion/plugins/publish/collect_instances.py b/openpype/hosts/fusion/plugins/publish/collect_instances.py index c1c23ec570..6887f4f4e9 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_instances.py +++ b/openpype/hosts/fusion/plugins/publish/collect_instances.py @@ -30,7 +30,7 @@ class CollectInstanceData(pyblish.api.InstancePlugin): start_handle = start - handle_start end_handle = end + handle_end - if creator_attributes.get("custom_range"): + if creator_attributes.get("viewer_render_range"): # get comp frame ranges start = context.data["renderFrameStart"] end = context.data["renderFrameEnd"] diff --git a/openpype/hosts/fusion/plugins/publish/collect_render.py b/openpype/hosts/fusion/plugins/publish/collect_render.py index 64d9aedc3b..c3ae9f381d 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_render.py +++ b/openpype/hosts/fusion/plugins/publish/collect_render.py @@ -83,7 +83,7 @@ class CollectFusionRender( frameEnd=inst.data["frameEnd"], handleStart=inst.data["handleStart"], handleEnd=inst.data["handleEnd"], - ignoreFrameHandleCheck=(not inst.data.get("custom_range")), + ignoreFrameHandleCheck=(not inst.data.get("viewer_render_range")), frameStep=1, fps=comp_frame_format_prefs.get("Rate"), app_version=comp.GetApp().Version, From e14e0f5a40c6a48ffc9302544c99e0c3f757b2b6 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 15 May 2023 22:11:35 +0200 Subject: [PATCH 272/360] hound --- openpype/hosts/fusion/plugins/publish/collect_render.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/publish/collect_render.py b/openpype/hosts/fusion/plugins/publish/collect_render.py index c3ae9f381d..6956b566ad 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_render.py +++ b/openpype/hosts/fusion/plugins/publish/collect_render.py @@ -83,7 +83,8 @@ class CollectFusionRender( frameEnd=inst.data["frameEnd"], handleStart=inst.data["handleStart"], handleEnd=inst.data["handleEnd"], - ignoreFrameHandleCheck=(not inst.data.get("viewer_render_range")), + ignoreFrameHandleCheck=( + not inst.data.get("viewer_render_range")), frameStep=1, fps=comp_frame_format_prefs.get("Rate"), app_version=comp.GetApp().Version, From 11d47eb9407be381a6a10731b683886f9f26f0da Mon Sep 17 00:00:00 2001 From: Petr Dvorak Date: Tue, 16 May 2023 10:28:24 +0200 Subject: [PATCH 273/360] Company name and URL changed --- inno_setup.iss | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/inno_setup.iss b/inno_setup.iss index 3adde52a8b..418bedbd4d 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -14,10 +14,10 @@ AppId={{B9E9DF6A-5BDA-42DD-9F35-C09D564C4D93} AppName={#MyAppName} AppVersion={#AppVer} AppVerName={#MyAppName} version {#AppVer} -AppPublisher=Orbi Tools s.r.o -AppPublisherURL=http://pype.club -AppSupportURL=http://pype.club -AppUpdatesURL=http://pype.club +AppPublisher=Ynput s.r.o +AppPublisherURL=https://ynput.io +AppSupportURL=https://ynput.io +AppUpdatesURL=https://ynput.io DefaultDirName={autopf}\{#MyAppName}\{#AppVer} UsePreviousAppDir=no DisableProgramGroupPage=yes From 380a9cba51448a90e3e7b1c67fd50a818837f20c Mon Sep 17 00:00:00 2001 From: Ynbot Date: Tue, 16 May 2023 08:44:24 +0000 Subject: [PATCH 274/360] [Automated] Release --- CHANGELOG.md | 297 ++++++++++++++++++++++++++++++++++++++++++++ openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 299 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07c1e7d5fd..bba6b64bfe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,303 @@ # Changelog +## [3.15.7](https://github.com/ynput/OpenPype/tree/3.15.7) + + +[Full Changelog](https://github.com/ynput/OpenPype/compare/3.15.6...3.15.7) + +### **🆕 New features** + + +
+Addons directory #4893 + +This adds a directory for Addons, for easier distribution of studio specific code. + + +___ + +
+ + +
+Kitsu - Add "image", "online" and "plate" to review families #4923 + +This PR adds "image", "online" and "plate" to the review families so they also can be uploaded to Kitsu.It also adds the `Add review to Kitsu` tag to the default png review. Without it the user would manually need to add it for single image uploads to Kitsu and might confuse users (it confused me first for a while as movies did work). + + +___ + +
+ + +
+Feature/remove and load inv action #4930 + +Added the ability to remove and load a container, as a way to reset it.This can be useful in cases where a container breaks in a way that can be fixed by removing it, then reloading it.Also added the ability to add `InventoryAction` plugins by placing them in `openpype/plugins/inventory`. + + +___ + +
+ +### **🚀 Enhancements** + + +
+Load Rig References - Change Rig to Animation in Animation instance #4877 + +We are using the template builder to build an animation scene. All the rig placeholders are imported correctly, but the automatically created animation instances retain the rig family in their names and subsets. In our example, we need animationMain instead of rigMain, because this name will be used in the following steps like lighting.Here is the result we need. I checked, and it's not a template builder problem, because even if I load a rig as a reference, the result is the same. For me, since we are in the animation instance, it makes more sense to have animation instead of rig in the name. The naming is just fine if we use create from the Openpype menu. + + +___ + +
+ + +
+Maya template builder - preserve all references when importing a template #4797 + +When building a template with Maya template builder, we import the template and also the references inside the template file. This causes some problems: +- We cannot use the references to version assets imported by the template. +- When we import the file, the internal reference files are also imported. As a side effect, Maya complains about a reference that no longer exists.`// Error: file: /xxx/maya/2023.3/linux/scripts/AETemplates/AEtransformRelated.mel line 58: Reference node 'turntable_mayaSceneMain_01_RN' is not associated with a reference file.` + + +___ + +
+ + +
+Unreal: Renaming the integration plugin to Ayon. #4646 + +Renamed the .h, and .cpp files to Ayon. Also renamed the classes to with the Ayon keyword. + + +___ + +
+ + +
+3dsMax: render dialogue needs to be closed #4729 + +Make sure the render setup dialog is in a closed state for the update of resolution and other render settings + + +___ + +
+ + +
+Maya Template Builder - Remove default cameras from renderable cameras #4815 + +When we build an asset workfile with build workfile from template inside Maya, we load our turntable camera. But then we end up with 2 renderables camera : **persp** the one imported from the template.We need to remove the **persp** camera (or any other default camera) from renderable cameras when building the work file. + + +___ + +
+ + +
+Validators for Frame Range in Max #4914 + +Switch Render Frame Range Type to 3 for specific ranges (initial setup for the range type is 4)Reset Frame Range will also set the frame range for render settingsRender Collector won't take the frame range from context data but take the range directly from render settingAdd validators for render frame range type and frame range respectively with repair action + + +___ + +
+ + +
+Fusion: Saver creator settings #4943 + +Adding Saver creator settings and enhanced rendering path with template. + + +___ + +
+ + +
+General: Project Anatomy on creators #4962 + +Anatomy object of current project is available on `CreateContext` and create plugins. + + +___ + +
+ +### **🐛 Bug fixes** + + +
+Maya: Validate shader name - OP-5903 #4971 + +Running the plugin would error with: +``` +// TypeError: 'str' object cannot be interpreted as an integer +```Fixed and added setting `active`. + + +___ + +
+ + +
+Houdini: Fix slow Houdini launch due to shelves generation #4829 + +Shelf generation during Houdini startup would add an insane amount of delay for the Houdini UI to launch correctly. By deferring the shelf generation this takes away the 5+ minutes of delay for the Houdini UI to launch. + + +___ + +
+ + +
+Fusion - Fixed "optional validation" #4912 + +Added OptionalPyblishPluginMixin and is_active checks for all publish tools that should be optional + + +___ + +
+ + +
+Bug: add missing `pyblish.util` import #4937 + +remote publishing was missing import of `remote_publish`. This is adding it back. + + +___ + +
+ + +
+Unreal: Fix missing 'object_path' property #4938 + +Epic removed the `object_path` property from `AssetData`. This PR fixes usages of that property.Fixes #4936 + + +___ + +
+ + +
+Remove obsolete global validator #4939 + +Removing `Validate Sequence Frames` validator from global plugins as it wasn't handling correctly many things and was by mistake enabled, breaking functionality on Deadline. + + +___ + +
+ + +
+General: fix build_workfile get_linked_assets missing project_name arg #4940 + +Linked assets collection don't work within `build_workfile` because `get_linked_assets` function call has a missing `project_name`argument. +- Added the `project_name` arg to the `get_linked_assets` function call. + + +___ + +
+ + +
+General: fix Scene Inventory switch version error dialog missing parent arg on init #4941 + +QuickFix for the switch version error dialog to set inventory widget as parent. + + +___ + +
+ + +
+Unreal: Fix camera frame range #4956 + +Fix the frame range of the level sequence for the Camera in Unreal. + + +___ + +
+ + +
+Unreal: Fix missing parameter when updating Alembic StaticMesh #4957 + +Fix an error when updating an Alembic StaticMesh in Unreal, due to a missing parameter in a function call. + + +___ + +
+ + +
+Unreal: Fix render extraction #4963 + +Fix a problem with the extraction of renders in Unreal. + + +___ + +
+ + +
+Unreal: Remove Python 3.8 syntax from addon #4965 + +Removed Python 3.8 syntax from addon. + + +___ + +
+ + +
+Ftrack: Fix editorial task creation #4966 + +Fix key assignment on instance data during editorial publishing in ftrack hierarchy integration. + + +___ + +
+ +### **Merged pull requests** + + +
+Add "shortcut" to Scripts Menu Definition #4927 + +Add the possibility to associate a shorcut for an entry in the script menu definition with the key "shortcut" + + +___ + +
+ + + + ## [3.15.6](https://github.com/ynput/OpenPype/tree/3.15.6) diff --git a/openpype/version.py b/openpype/version.py index 319a58d384..3a0d05be0e 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.7-nightly.3" +__version__ = "3.15.7" diff --git a/pyproject.toml b/pyproject.toml index 003f6cf2d3..190ecb9329 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.15.6" # OpenPype +version = "3.15.7" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 28e0838d00e9b3edc8880c3bc59a753cb6727929 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 16 May 2023 08:45:43 +0000 Subject: [PATCH 275/360] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 7d224aa73f..08d20c2058 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.15.7 - 3.15.7-nightly.3 - 3.15.7-nightly.2 - 3.15.7-nightly.1 @@ -134,7 +135,6 @@ body: - 3.14.1 - 3.14.1-nightly.4 - 3.14.1-nightly.3 - - 3.14.1-nightly.2 validations: required: true - type: dropdown From 10b953f8bf0a16c2f49f5d89d3f0df6030ea85c9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 16 May 2023 11:34:54 +0200 Subject: [PATCH 276/360] fusion: converting frame range source to enum --- .../fusion/plugins/create/create_saver.py | 19 +++++++++++++------ .../plugins/publish/collect_instances.py | 5 +++-- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index 67b1465ec7..80f60a0c6e 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -199,11 +199,7 @@ class CreateSaver(NewCreator): attr_defs = [ self._get_render_target_enum(), self._get_reviewable_bool(), - BoolDef( - "viewer_render_range", - label="Viewer render in/out", - default=False, - ) + self._get_frame_range_enum() ] return attr_defs @@ -222,7 +218,6 @@ class CreateSaver(NewCreator): # These functions below should be moved to another file # so it can be used by other plugins. plugin.py ? - def _get_render_target_enum(self): rendering_targets = { "local": "Local machine rendering", @@ -235,6 +230,18 @@ class CreateSaver(NewCreator): "render_target", items=rendering_targets, label="Render target" ) + def _get_frame_range_enum(self): + frame_range_options = { + "asset_db": "From asset database", + "viewer_render_range": "From viewer render in/out" + } + + return EnumDef( + "frame_range_source", + items=frame_range_options, + label="Frame range source" + ) + def _get_reviewable_bool(self): return BoolDef( "review", diff --git a/openpype/hosts/fusion/plugins/publish/collect_instances.py b/openpype/hosts/fusion/plugins/publish/collect_instances.py index 6887f4f4e9..61ce10d32f 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_instances.py +++ b/openpype/hosts/fusion/plugins/publish/collect_instances.py @@ -20,6 +20,7 @@ class CollectInstanceData(pyblish.api.InstancePlugin): # Include creator attributes directly as instance data creator_attributes = instance.data["creator_attributes"] + frame_range_source = creator_attributes.get("frame_range_source") instance.data.update(creator_attributes) # get asset frame ranges @@ -30,8 +31,8 @@ class CollectInstanceData(pyblish.api.InstancePlugin): start_handle = start - handle_start end_handle = end + handle_end - if creator_attributes.get("viewer_render_range"): - # get comp frame ranges + if frame_range_source == "viewer_render_range": + # set comp render frame ranges start = context.data["renderFrameStart"] end = context.data["renderFrameEnd"] handle_start = 0 From 2d6919297cdc370bc43a4cc76390021e2ed8564b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 16 May 2023 11:40:00 +0200 Subject: [PATCH 277/360] fusion: adding comp range option --- .../hosts/fusion/plugins/create/create_saver.py | 3 ++- .../fusion/plugins/publish/collect_instances.py | 13 +++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index 80f60a0c6e..d5e77730c8 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -233,7 +233,8 @@ class CreateSaver(NewCreator): def _get_frame_range_enum(self): frame_range_options = { "asset_db": "From asset database", - "viewer_render_range": "From viewer render in/out" + "viewer_render_range": "From viewer render in/out", + "comp_range": "From composition timeline" } return EnumDef( diff --git a/openpype/hosts/fusion/plugins/publish/collect_instances.py b/openpype/hosts/fusion/plugins/publish/collect_instances.py index 61ce10d32f..59ff52f5b2 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_instances.py +++ b/openpype/hosts/fusion/plugins/publish/collect_instances.py @@ -40,6 +40,19 @@ class CollectInstanceData(pyblish.api.InstancePlugin): start_handle = start end_handle = end + if frame_range_source == "comp_range": + comp_start = context.data["compFrameStart"] + comp_end = context.data["compFrameEnd"] + render_start = context.data["renderFrameStart"] + render_end = context.data["renderFrameEnd"] + # set comp frame ranges + start = render_start + end = render_end + handle_start = render_start - comp_start + handle_end = comp_end - render_end + start_handle = comp_start + end_handle = comp_end + # Include start and end render frame in label subset = instance.data["subset"] label = "{subset} ({start}-{end})".format(subset=subset, From 16f24c253803026ab0f768ef5c3dca1320561ca4 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 16 May 2023 22:48:02 +0800 Subject: [PATCH 278/360] fix the bug of fbx loaders --- openpype/hosts/max/plugins/load/load_camera_fbx.py | 10 +++++++--- openpype/hosts/max/plugins/load/load_model_fbx.py | 9 +++++++-- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_camera_fbx.py b/openpype/hosts/max/plugins/load/load_camera_fbx.py index 3a6947798e..ce4dec32a0 100644 --- a/openpype/hosts/max/plugins/load/load_camera_fbx.py +++ b/openpype/hosts/max/plugins/load/load_camera_fbx.py @@ -36,12 +36,16 @@ importFile @"{filepath}" #noPrompt using:FBXIMP self.log.debug(f"Executing command: {fbx_import_cmd}") rt.execute(fbx_import_cmd) - container_name = f"{name}_CON" + container = rt.getNodeByName(f"{name}") + if not container: + container = rt.container() + container.name = f"{name}" - asset = rt.getNodeByName(f"{name}") + for selection in rt.getCurrentSelection(): + selection.Parent = container return containerise( - name, [asset], context, loader=self.__class__.__name__) + name, [container], context, loader=self.__class__.__name__) def update(self, container, representation): from pymxs import runtime as rt diff --git a/openpype/hosts/max/plugins/load/load_model_fbx.py b/openpype/hosts/max/plugins/load/load_model_fbx.py index 88b8f1ed89..7532e3a8a0 100644 --- a/openpype/hosts/max/plugins/load/load_model_fbx.py +++ b/openpype/hosts/max/plugins/load/load_model_fbx.py @@ -36,11 +36,16 @@ importFile @"{filepath}" #noPrompt using:FBXIMP self.log.debug(f"Executing command: {fbx_import_cmd}") rt.execute(fbx_import_cmd) + container = rt.getNodeByName(f"{name}") + if not container: + container = rt.container() + container.name = f"{name}" - asset = rt.getNodeByName(f"{name}") + for selection in rt.getCurrentSelection(): + selection.Parent = container return containerise( - name, [asset], context, loader=self.__class__.__name__) + name, [container], context, loader=self.__class__.__name__) def update(self, container, representation): from pymxs import runtime as rt From 255a72bfed1e50dcc30ad46cee1bc44b82d1f2bd Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 17 May 2023 03:25:43 +0000 Subject: [PATCH 279/360] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index 3a0d05be0e..954cfa945b 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.7" +__version__ = "3.15.8-nightly.1" From c85ebbfac2236aa3bf761817024290a2a5c1a015 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 17 May 2023 03:26:23 +0000 Subject: [PATCH 280/360] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 08d20c2058..0f58d61881 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.15.8-nightly.1 - 3.15.7 - 3.15.7-nightly.3 - 3.15.7-nightly.2 @@ -134,7 +135,6 @@ body: - 3.14.2-nightly.1 - 3.14.1 - 3.14.1-nightly.4 - - 3.14.1-nightly.3 validations: required: true - type: dropdown From c78a74248702d47cf33ff55e38bf8def1f86ec17 Mon Sep 17 00:00:00 2001 From: Alexey Bogomolov Date: Wed, 17 May 2023 10:02:59 +0300 Subject: [PATCH 281/360] add logs, remove adding PYTHONHOME to PATH --- .../Support/logs/davinci_resolve.log | 443 ++++++++++++++++++ .../hosts/resolve/hooks/pre_resolve_setup.py | 14 +- 2 files changed, 449 insertions(+), 8 deletions(-) create mode 100644 igniter/#ProgramData#Blackmagic Design/DaVinci Resolve/Support/logs/davinci_resolve.log diff --git a/igniter/#ProgramData#Blackmagic Design/DaVinci Resolve/Support/logs/davinci_resolve.log b/igniter/#ProgramData#Blackmagic Design/DaVinci Resolve/Support/logs/davinci_resolve.log new file mode 100644 index 0000000000..8f1a3b712e --- /dev/null +++ b/igniter/#ProgramData#Blackmagic Design/DaVinci Resolve/Support/logs/davinci_resolve.log @@ -0,0 +1,443 @@ +[0x00004fe8] | Undefined | INFO | 2023-05-17 09:46:08,249 | -------------------------------------------------------------------------------- +[0x00004fe8] | Undefined | INFO | 2023-05-17 09:46:08,250 | Loaded log config from C:\Users\videopro\AppData\Roaming\Blackmagic Design\DaVinci Resolve\Preferences\log-conf.xml +[0x00004fe8] | Undefined | INFO | 2023-05-17 09:46:08,250 | -------------------------------------------------------------------------------- +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:08,251 | Running DaVinci Resolve Studio v18.1.2.0006 (Windows/MSVC x86_64) +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:08,251 | BMD_BUILD_UUID 3ff36663-26c9-45d9-8506-101676c881e0 +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:08,251 | BMD_GIT_COMMIT a3be29d0542aeafcb1b0933bb5ace426aa7d047d +[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,259 | Starting GPUDetect 1.2_3-a1 +[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,421 | Done in 161 ms. +[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,421 | Detected System: +[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,421 | - OS: Windows 10 Pro (Build 19045) +[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,421 | - Model: ASUSTeK ROG STRIX B550-F GAMING (WI-FI) +[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,421 | - System ID: 838d5045-58b0-44ed-854c-19be5b814d6f +[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,421 | - CPU: AMD Ryzen 7 3700X, 16 threads, x86-64 +[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,421 | - RAM: 16.6 GiB used of 63.9 GiB +[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,421 | - NVIDIA GPU Driver: 527.56, supports CUDA 12.0 +[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,421 | Detected 1 GPUs: +[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,423 | - "NVIDIA GeForce RTX 2060 SUPER" (gpu:a24a3d50.83f1fdb7) <- Main Display GPU +[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,423 | Discrete, 2.0 GiB used of 7.6 GiB VRAM, PCI:8:0 (x16) +[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,423 | Matches: CUDA, DirectX, NVAPI, NVML, OpenCL, Win32 +[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,423 | Detected 1 monitors: +[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,423 | - "Generic PnP Monitor" <- Main Monitor +[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,423 | 3840x2160, connected to "NVIDIA GeForce RTX 2060 SUPER" (gpu:a24a3d50.83f1fdb7) +[0x00004fe8] | Main.GPUConfig | INFO | 2023-05-17 09:46:08,424 | Selected compute API: CUDA +[0x00004fe8] | Main.GPUConfig | INFO | 2023-05-17 09:46:08,424 | Automatic GPU Selection: +[0x00004fe8] | Main.GPUConfig | INFO | 2023-05-17 09:46:08,424 | - "NVIDIA GeForce RTX 2060 SUPER" (gpu:a24a3d50.83f1fdb7) +[0x00004fe8] | IO | INFO | 2023-05-17 09:46:08,438 | RED InitializeSdk with library path at C:\ProgramData\Blackmagic Design\DaVinci Resolve\Support\Libraries +[0x00004fe8] | IO | INFO | 2023-05-17 09:46:08,642 | R3DAPI 8.3.1-52407 (20220725 Wx64S) R3DSDK 8.3.1-52407 (20220725 Wx64D C3B3) RED CUDA 8.3.1-52408 (20220725) [C:\ProgramData\Blackmagic Design\DaVinci Resolve\Support\Libraries\] init is successful +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:08,642 | 0 RED rocket cards available +[0x00004fe8] | SyManager.DeckLink | ERROR | 2023-05-17 09:46:08,645 | Failed to create instance. +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:08,645 | Decklink model name: '', version: '' +[0x00004fe8] | DVIP | INFO | 2023-05-17 09:46:08,645 | DVIP release/18.1.2 build 2 (86acfdf407856b6cd8daf2517ab0b44f1efc332f). Release, version 18.1.2. +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:08,645 | Resolve Module Handle 00007FF6A4B50000 +[0x00001238] | IO | INFO | 2023-05-17 09:46:08,646 | Using DNxHR library v2.7.3.27r +[0x00002044] | Fusion | INFO | 2023-05-17 09:46:09,093 | Fusion Build: 008b13c7_0004 (Dec 7 2022 15:23:40) +[0x00005f70] | Fusion | INFO | 2023-05-17 09:46:09,114 | fusionsystem: = "C:\Program Files\Blackmagic Design\DaVinci Resolve\fusionsystem.dll" +[0x00001238] | IO | INFO | 2023-05-17 09:46:09,117 | NVDEC is using upto (1023) MB +[0x00001238] | IO | INFO | 2023-05-17 09:46:09,119 | NVDEC decodes H264, chroma 4:2:0, bitdepth 8, upto 4096 x 4096 +[0x00001238] | IO | INFO | 2023-05-17 09:46:09,121 | NVDEC decodes HEVC, chroma 4:2:0, bitdepth 8, upto 8192 x 8192 +[0x00001238] | IO | INFO | 2023-05-17 09:46:09,123 | NVDEC decodes HEVC, chroma 4:2:0, bitdepth 10, upto 8192 x 8192 +[0x00001238] | IO | INFO | 2023-05-17 09:46:09,127 | NVDEC decodes HEVC, chroma 4:2:0, bitdepth 12, upto 8192 x 8192 +[0x00001238] | IO | INFO | 2023-05-17 09:46:09,128 | NVDEC decodes HEVC, chroma 4:4:4, bitdepth 8, upto 8192 x 8192 +[0x00001238] | IO | INFO | 2023-05-17 09:46:09,130 | NVDEC decodes HEVC, chroma 4:4:4, bitdepth 10, upto 8192 x 8192 +[0x00001238] | IO | INFO | 2023-05-17 09:46:09,131 | NVDEC decodes HEVC, chroma 4:4:4, bitdepth 12, upto 8192 x 8192 +[0x00001238] | IO | INFO | 2023-05-17 09:46:09,133 | NVDEC decodes VP9, chroma 4:2:0, bitdepth 8, upto 8192 x 8192 +[0x00001238] | IO | INFO | 2023-05-17 09:46:09,134 | NVDEC decodes VP9, chroma 4:2:0, bitdepth 10, upto 8192 x 8192 +[0x00005f70] | Fusion | INFO | 2023-05-17 09:46:09,148 | FusionLibs: = "C:\Program Files\Blackmagic Design\DaVinci Resolve\" +[0x00005f70] | Fusion | INFO | 2023-05-17 09:46:09,148 | UserData: = "C:\Users\videopro\AppData\Roaming\Blackmagic Design\DaVinci Resolve\Support\Fusion" +[0x00005f70] | Fusion | INFO | 2023-05-17 09:46:09,148 | Profiles: = "UserData:Profiles\" +[0x00001238] | IO | INFO | 2023-05-17 09:46:09,148 | Nvidia GPU (0) is initialised as decoding and encoding device. +[0x00001238] | IO | INFO | 2023-05-17 09:46:09,182 | IO codec library load completed in 536 ms. +[0x00005cbc] | SyManager | ERROR | 2023-05-17 09:46:09,202 | BlackmagicIDHelper::GetProjectLibraries() - Access token is empty +[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:09,268 | Loading dblist file: C:\Users\videopro\AppData\Roaming\Blackmagic Design\DaVinci Resolve\Preferences\dblist.conf +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:09,314 | Finished loading Application style sheet +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:09,396 | Show splash screen +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:09,396 | Show splash screen message: Starting Up +[0x00004fe8] | Fusion | INFO | 2023-05-17 09:46:09,398 | Module Handle 0000023A5FC80000 fusionsystem +[0x0000204c] | Fusion | INFO | 2023-05-17 09:46:09,410 | Module Handle 0000023A6C160000 C:\Program Files\Blackmagic Design\DaVinci Resolve\fusiongraphics.dll +[0x00004f28] | Fusion | INFO | 2023-05-17 09:46:09,437 | Module Handle 0000023A6FB90000 fusionoperators.dll +[0x00005f70] | Fusion | INFO | 2023-05-17 09:46:09,462 | Module Handle 0000023A70490000 fusioncontrols.dll +[0x00007b34] | Fusion | INFO | 2023-05-17 09:46:09,488 | Module Handle 0000023A70870000 3d.plugin +[0x0000772c] | Fusion | INFO | 2023-05-17 09:46:09,510 | Module Handle 0000023A70B50000 dimension.plugin +[0x00003934] | Fusion | INFO | 2023-05-17 09:46:09,528 | Module Handle 0000023A70F70000 alembic.plugin +[0x0000204c] | Fusion | INFO | 2023-05-17 09:46:09,551 | Module Handle 0000023A71320000 fbx.plugin +[0x000078cc] | Fusion | INFO | 2023-05-17 09:46:09,566 | Module Handle 0000023A464C0000 fuses.plugin +[0x00004f28] | Fusion | INFO | 2023-05-17 09:46:09,582 | Module Handle 0000023A71A70000 opencolorio.plugin +[0x00005f70] | Fusion | INFO | 2023-05-17 09:46:09,599 | Module Handle 0000023A6ADF0000 openfx.plugin +[0x00007b34] | Fusion | INFO | 2023-05-17 09:46:09,615 | Module Handle 0000023A68DA0000 openvr.plugin +[0x0000772c] | Fusion | INFO | 2023-05-17 09:46:09,632 | Module Handle 0000023A6AE60000 paint.plugin +[0x00003934] | Fusion | INFO | 2023-05-17 09:46:09,653 | Module Handle 0000023A6AED0000 particles.plugin +[0x0000204c] | Fusion | INFO | 2023-05-17 09:46:09,673 | Module Handle 0000023A720D0000 text.plugin +[0x000078cc] | Fusion | INFO | 2023-05-17 09:46:09,690 | Module Handle 0000023A6CB80000 utilities.plugin +[0x00004f28] | Fusion | INFO | 2023-05-17 09:46:09,715 | Module Handle 00007FF89F590000 KrokodoveFu16.plugin +[0x00005f70] | Fusion | INFO | 2023-05-17 09:46:09,717 | Module Handle 00007FF89F330000 KrokodoveFu17.plugin +[0x00004f28] | OpenFX | INFO | 2023-05-17 09:46:09,817 | No context is available for com.absoft.NeatVideo5 +[0x00007fc8] | Main | INFO | 2023-05-17 09:46:09,828 | Started listener socket at port 15000 +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:10,308 | Show splash screen message: Checking Licenses +[0x00004fe8] | BtCommon | INFO | 2023-05-17 09:46:10,532 | Memory config: reserved=12270M pinned=8000M log=0 +[0x00004fe8] | BtCommon | INFO | 2023-05-17 09:46:10,532 | Using default pooled memory manager +[0x00003798] | LeManager | INFO | 2023-05-17 09:46:10,533 | 521, 29 +[0x00008180] | BtCommon | INFO | 2023-05-17 09:46:10,533 | BtResourceManager Process Thread Started +[0x00004fe8] | IO | INFO | 2023-05-17 09:46:10,609 | WMF encoder cnt for SW (hvc1) is (0) +[0x00004fe8] | IO | INFO | 2023-05-17 09:46:10,624 | WMF encoder cnt for HW (hvc1) is (1) +[0x00004fe8] | IO | INFO | 2023-05-17 09:46:10,624 | Setting codec capacity (0) +[0x00004fe8] | SyManager | INFO | 2023-05-17 09:46:10,625 | Total: 20, NumDtThreads: 8, NumComms: 0, NumSites: 1 + +[0x00004fe8] | SyManager | INFO | 2023-05-17 09:46:10,625 | Lookaheads -> playback = 20, record = 20, stop = 2 + +[0x00004fe8] | DtManager | INFO | 2023-05-17 09:46:10,626 | Using 8 generic IO threads +[0x00004fe8] | DtManager | INFO | 2023-05-17 09:46:10,626 | Total of 16 IO threads (including 8 generic and 8 Red decode threads) +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:10,627 | Show splash screen message: Loading Project Libraries +[0x00001a20] | DtManager | INFO | 2023-05-17 09:46:10,627 | Dt Worker Thread Started +[0x00007da0] | GsManager | INFO | 2023-05-17 09:46:10,628 | Gs Processor Thread ----- (32160) + +[0x00004474] | DtManager | INFO | 2023-05-17 09:46:10,628 | Dt Worker Thread Started +[0x00002b7c] | DtManager | INFO | 2023-05-17 09:46:10,628 | Dt Worker Thread Started +[0x00002d64] | DtManager | INFO | 2023-05-17 09:46:10,628 | Dt Worker Thread Started +[0x00007bbc] | DtManager | INFO | 2023-05-17 09:46:10,628 | Dt Data Handler Thread Started +[0x00002ad0] | DtManager | INFO | 2023-05-17 09:46:10,628 | Dt Worker Thread Started +[0x00004a48] | DtManager | INFO | 2023-05-17 09:46:10,629 | Dt Worker Thread Started +[0x00005084] | DtManager | INFO | 2023-05-17 09:46:10,629 | Dt Worker Thread Started +[0x00003050] | DtManager | INFO | 2023-05-17 09:46:10,629 | Dt Worker Thread Started +[0x00003e88] | DtManager | INFO | 2023-05-17 09:46:10,629 | Dt Worker Thread Started +[0x00006550] | DtManager | INFO | 2023-05-17 09:46:10,629 | Dt Worker Thread Started +[0x00007950] | DtManager | INFO | 2023-05-17 09:46:10,629 | Dt Worker Thread Started +[0x000059a8] | DtManager | INFO | 2023-05-17 09:46:10,629 | Dt Worker Thread Started +[0x00007de4] | DtManager | INFO | 2023-05-17 09:46:10,629 | Dt Worker Thread Started +[0x00007a24] | DtManager | INFO | 2023-05-17 09:46:10,629 | Dt Worker Thread Started +[0x00001eec] | DtManager | INFO | 2023-05-17 09:46:10,630 | Dt Worker Thread Started +[0x00001f5c] | DtManager | INFO | 2023-05-17 09:46:10,630 | Dt Worker Thread Started +[0x00007a50] | DtManager | INFO | 2023-05-17 09:46:10,630 | Dt Worker Thread Started +[0x000009c4] | DtManager | INFO | 2023-05-17 09:46:10,630 | Dt Worker Thread Started +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:10,633 | Show splash screen message: Initializing system components +[0x00001190] | GPU.MultiBoardMgr | INFO | 2023-05-17 09:46:10,635 | Let There Be CUDA Light! +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:10,639 | Show splash screen message: Loading video codecs +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:10,644 | Show splash screen message: Loading video plugins +[0x00004fe8] | UI.GLContext | INFO | 2023-05-17 09:46:10,680 | Creating shared OpenGL context for this thread (1 total). +[0x00004fe8] | UI.GLContext | INFO | 2023-05-17 09:46:10,708 | Initialized OpenGL 4.6 (requested 2.0) on device 'NVIDIA Corporation NVIDIA GeForce RTX 2060 SUPER/PCIe/SSE2' +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:10,708 | Show splash screen message: Loading Fairlight Engine +[0x00001190] | GPU.MultiBoardMgr | INFO | 2023-05-17 09:46:10,708 | Initializing CUDA board manager for Main Display GPU gpu:a24a3d50.83f1fdb7. +[0x00001968] | IO | INFO | 2023-05-17 09:46:10,788 | IO codec initialization completed in 143 ms. +[0x000065b4] | GPU.SingleBoardMgr | INFO | 2023-05-17 09:46:10,811 | Board manager thread for "NVIDIA GeForce RTX 2060 SUPER" (gpu:a24a3d50.83f1fdb7) is ready. +[0x00001190] | UI.GLInterop | INFO | 2023-05-17 09:46:10,811 | OpenGL interop was initialized. +[0x00001190] | UI.GLInterop | INFO | 2023-05-17 09:46:10,811 | OpenGL interop was initialized. +[0x00001190] | GPU.MultiBoardMgr | INFO | 2023-05-17 09:46:10,811 | Enabled CUDA pinned memory. +[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,011 | 00.00.00.122(003): WASAPI: Scanning Speakers (Steam Streaming Microphone) for formats +[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,011 | 00.00.00.128(000): WASAPI: Device: Speakers (Steam Streaming Microphone). Scan time 6. Formats = 1 15 formats scanned +[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,011 | 00.00.00.128(000): WASAPI: Scanning Speakers (High Definition Audio Device) for formats +[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,011 | 00.00.00.190(000): WASAPI: Device: Speakers (High Definition Audio Device). Scan time 61. Formats = 1 15 formats scanned +[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,011 | 00.00.00.190(000): WASAPI: Scanning Mi 27 NU (NVIDIA High Definition Audio) for formats +[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,011 | 00.00.00.236(000): WASAPI: Device: Mi 27 NU (NVIDIA High Definition Audio). Scan time 45. Formats = 1 15 formats scanned +[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,011 | 00.00.00.236(000): WASAPI: Scanning Speakers (Steam Streaming Speakers) for formats +[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,011 | 00.00.00.242(000): WASAPI: Device: Speakers (Steam Streaming Speakers). Scan time 6. Formats = 1 15 formats scanned +[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,011 | 00.00.00.243(001): WASAPI: Scanning Digital Audio (S/PDIF) (High Definition Audio Device) for formats +[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,212 | 00.00.00.286(000): WASAPI: Device: Digital Audio (S/PDIF) (High Definition Audio Device). Scan time 42. Formats = 1 15 formats scanned +[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,212 | 00.00.00.287(001): WASAPI: Scanning Microphone (Steam Streaming Microphone) for formats +[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,212 | 00.00.00.294(000): WASAPI: Device: Microphone (Steam Streaming Microphone). Scan time 7. Formats = 0 16 formats scanned +[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,212 | 00.00.00.390(002): WASAPI: switching to AUTO because exclusive mode not allowed +[0x00004f94] | DbCommon2 | ERROR | 2023-05-17 09:46:11,307 | Cannot connect to soundfx192.168.100.31 database: could not connect to server: Connection refused (0x0000274D/10061) + Is the server running on host "192.168.100.31" and accepting + TCP/IP connections on port 5432? +QPSQL: Unable to connect +[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:11,339 | postgres project library homepc at 127.0.0.1 version 9.5.22 +[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:11,347 | Project library [homepc127.0.0.1] current version <18.1.0.004> updated on <2022-11-22T16:16:24.518>, remark: +[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:11,347 | Connect to postgres project library homepc127.0.0.1 +[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:11,396 | postgres project library barney at 192.168.100.30 version 9.5.25 +[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:11,422 | Project library [barney192.168.100.30] current version <18.1.0.004> updated on <2022-11-17T08:47:18.322>, remark: +[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:11,422 | Connect to postgres project library barney192.168.100.30 +[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:11,475 | postgres project library shtv_barney at 192.168.100.30 version 9.5.25 +[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:11,501 | Project library [shtv_barney192.168.100.30] current version <18.1.0.004> updated on <2022-11-15T13:16:50.006>, remark: +[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:11,501 | Connect to postgres project library shtv_barney192.168.100.30 +[0x00005850] | Fusion | INFO | 2023-05-17 09:46:11,704 | 260 templates scanned in 0.12 secs +[0x00004fe8] | Fairlight | INFO | 2023-05-17 09:46:12,310 | 00.00.01.565(000): Running Fairlight (939d5c56aa8d66c1f392ff963f9b9c349ef4d9fb) +[0x00004fe8] | FairlightLoader | INFO | 2023-05-17 09:46:12,310 | Fairlight lib initialized in 1598 ms. +[0x00006a7c] | UI.GLContext | INFO | 2023-05-17 09:46:12,405 | Creating shared OpenGL context for this thread (2 total). +[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:12,417 | 00.00.01.501(002): WASAPI: switching to AUTO because exclusive mode not allowed +[0x00006a7c] | UI.GLIO | INFO | 2023-05-17 09:46:12,454 | Initialized MainPlayer OpenGL I/O on CUDA device 'NVIDIA GeForce RTX 2060 SUPER' +[0x00004fe8] | UI.GLIO | INFO | 2023-05-17 09:46:12,454 | MainPlayer: OpenGL I/O setup done. +[0x000076bc] | UI.GLContext | INFO | 2023-05-17 09:46:12,455 | Creating shared OpenGL context for this thread (3 total). +[0x000076bc] | UI.GLIO | INFO | 2023-05-17 09:46:12,505 | Initialized AuxPlayer OpenGL I/O on CUDA device 'NVIDIA GeForce RTX 2060 SUPER' +[0x00004fe8] | UI.GLIO | INFO | 2023-05-17 09:46:12,505 | AuxPlayer: OpenGL I/O setup done. +[0x00008190] | UI.GLContext | INFO | 2023-05-17 09:46:12,506 | Creating shared OpenGL context for this thread (4 total). +[0x00008190] | UI.Scopes | INFO | 2023-05-17 09:46:12,565 | Initialized GPU Scopes Manager on CUDA device 'NVIDIA GeForce RTX 2060 SUPER' +[0x00004fe8] | UI.MenuBar | WARN | 2023-05-17 09:46:12,649 | Main menu action [workspaceLayoutFusion_sub001Default]'s slot is not defined: workspaceLayoutFusion_sub001Default_triggered() +[0x00004fe8] | UI.MenuBar | WARN | 2023-05-17 09:46:12,649 | Main menu action [workspaceWIPlugins_placeholder]'s slot is not defined: workspaceWIPlugins_placeholder_triggered() +[0x00006e14] | SyManager | WARN | 2023-05-17 09:46:12,656 | socket failed to connect to server, error: 10061 + +[0x00006e14] | SyManager | ERROR | 2023-05-17 09:46:12,656 | DRIVER: panel connection failed +[0x00004fe8] | FusionUtils | WARN | 2023-05-17 09:46:12,657 | Fusion DoAction ACTION_SET_FUSION_HOTKEY ignored, init not completed +[0x000052e4] | BtCommon | INFO | 2023-05-17 09:46:12,661 | Starting Daemon: C:/Program Files/Blackmagic Design/DaVinci Resolve/DaVinciPanelDaemon.exe +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:12,686 | Show splash screen message: Loading Project Settings +[0x00006e14] | SyManager | INFO | 2023-05-17 09:46:12,757 | Connection to the panel server has been re-established +[0x00004fe8] | UI | WARN | 2023-05-17 09:46:12,899 | Failed to find value '0' in combo-box +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:19,055 | Show splash screen message: Loading Media Page +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:46:19,145 | Action [fairlightBusStructure] is not a global action +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:46:19,145 | Action [fairlightBusStructure] is not a valid global action +[0x00004fe8] | UI.FairlightInterface | WARN | 2023-05-17 09:46:19,269 | SetActionEnabled: Failed to find action [viewTimelineScrollingFixed]'s action connector for handler Id [1] +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:19,424 | Show splash screen message: Loading Cut Page +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:19,698 | Show splash screen message: Loading Edit Page +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:19,951 | Show splash screen message: Loading Fusion Page +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:20,038 | Show splash screen message: Loading Fairlight Page +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:20,227 | Show splash screen message: Loading Color Page +[0x00004fe8] | UI | INFO | 2023-05-17 09:46:20,296 | Not creating special GL widget for screen 0 +[0x00004fe8] | UI | WARN | 2023-05-17 09:46:21,003 | Failed to find value '8192' in combo-box +[0x00004fe8] | UI | WARN | 2023-05-17 09:46:21,284 | Failed to find value '100' in combo-box +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:22,006 | Show splash screen message: Loading Waveform Monitor +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:22,433 | Show splash screen message: Loading Audio Plugins +[0x00004fe8] | SyManager | INFO | 2023-05-17 09:46:22,528 | Collaboration IP 127.0.0.1 Port 0 +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:22,674 | Show splash screen message: Loading Projects +[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:46:22,739 | Current user pointer is changed +[0x00004fe8] | UI | WARN | 2023-05-17 09:46:22,755 | UI Persistence - SecondaryScreenIdx not read +[0x00004fe8] | UI | WARN | 2023-05-17 09:46:22,755 | UI Persistence - UseDisplayNameForClips not read +[0x00004fe8] | UI | WARN | 2023-05-17 09:46:22,755 | UI Persistence - DefaultTransitionKey not read +[0x00004fe8] | UI | WARN | 2023-05-17 09:46:22,755 | UI Persistence - DefaultAudioTransitionKey not read +[0x00004fe8] | IO | INFO | 2023-05-17 09:46:22,767 | RED rocket decode has been disabled in the config file +[0x00004fe8] | LeManager | ERROR | 2023-05-17 09:46:22,820 | 444, 139, 0 +[0x00004fe8] | SyManager.DeckLink | ERROR | 2023-05-17 09:46:22,922 | Failed to create instance. +[0x00004fe8] | SyManager | INFO | 2023-05-17 09:46:22,973 | Search Parameter (-2) or Search Operation (0) not supported, and hence disabling the condition. +[0x00004fe8] | SyManager | INFO | 2023-05-17 09:46:22,973 | Search Parameter (-1) or Search Operation (-1) not supported, and hence disabling the condition. +[0x00004fe8] | UI | WARN | 2023-05-17 09:46:23,077 | Failed to find value '0' in combo-box +[0x00004fe8] | UI | WARN | 2023-05-17 09:46:23,080 | Failed to find value '0' in combo-box +[0x00004fe8] | UI | WARN | 2023-05-17 09:46:23,098 | Failed to find value '0' in combo-box +[0x00004fe8] | UI | WARN | 2023-05-17 09:46:23,099 | Failed to find value '4' in combo-box +[0x00004fe8] | SyManager.Gallery | INFO | 2023-05-17 09:46:23,119 | Start purging still caches +[0x00004fe8] | SyManager.Gallery | INFO | 2023-05-17 09:46:23,119 | Finish purging still caches +[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:46:23,149 | Media pool relink status changed to 0 +[0x00004fe8] | FusionUtils | WARN | 2023-05-17 09:46:23,167 | Fusion DoAction ACTION_SET_FUSION_HOTKEY ignored, init not completed +[0x00004fe8] | UI | INFO | 2023-05-17 09:46:23,179 | Launching project manager +[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:46:23,182 | Main view page is changed to 12 +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:23,209 | Show splash screen message: Ready +[0x00004fe8] | UI | INFO | 2023-05-17 09:46:23,214 | Gallery pointer is changed, refreshing color gallery +[0x00004fe8] | UI | INFO | 2023-05-17 09:46:23,223 | Gallery pointer is changed, refreshing gallery browser +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:23,312 | Close splash screen +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:23,314 | Launching main loop +[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:46:24,215 | Fusion templates changed +[0x00004fe8] | Fusion | INFO | 2023-05-17 09:46:24,758 | Started script server: 32648 + +[0x00004fe8] | SyManager.ActionExecutor | INFO | 2023-05-17 09:48:41,800 | Database transaction is ongoing, user initiated action Sync Asset Map is postponed +[0x00004fe8] | SyManager.ActionExecutor | INFO | 2023-05-17 09:48:41,800 | Database transaction completed, enqueueing 1 postponed actions +[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:48:41,842 | Loading project (gazprom_screens_06_R_Home_Painter_Lookdev_v002) from project library (homepc127.0.0.1) took 291 ms +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:42,159 | Action [fairlightBusStructure] is not a global action +[0x000038ec] | Fairlight | INFO | 2023-05-17 09:48:42,390 | 00.02.31.644(002): WASAPI: switching to AUTO because exclusive mode not allowed +[0x00004fe8] | UI | WARN | 2023-05-17 09:48:43,528 | Failed to find value '0' in combo-box +[0x00004fe8] | UI | WARN | 2023-05-17 09:48:43,531 | Failed to find value '0' in combo-box +[0x00004fe8] | SyManager.Gallery | INFO | 2023-05-17 09:48:43,539 | Start purging still caches +[0x00004fe8] | SyManager.Gallery | INFO | 2023-05-17 09:48:43,539 | Finish purging still caches +[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:48:43,557 | Media pool relink status changed to 0 +[0x000038ec] | Fairlight | INFO | 2023-05-17 09:48:43,596 | 00.02.32.735(002): WASAPI: switching to AUTO because exclusive mode not allowed +[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:48:43,862 | Current project pointer (gazprom_screens_06_R_Home_Painter_Lookdev_v002) is changed +[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:48:43,862 | Current timeline pointer () is changed +[0x00004fe8] | UI | INFO | 2023-05-17 09:48:43,868 | Lock project 513e336f-59c3-4fa8-b9e9-814cf2b797a3 +[0x00004fe8] | Undefined | INFO | 2023-05-17 09:48:43,868 | The buttonId [7] is not found for [0] in [0] +[0x00004fe8] | UI | INFO | 2023-05-17 09:48:43,876 | UiGPU::UploadWipeData called before UiGPU has been initialized. Returning false +[0x00004fe8] | UI | INFO | 2023-05-17 09:48:43,877 | Gallery pointer is changed, refreshing color gallery +[0x00004fe8] | UI | INFO | 2023-05-17 09:48:43,891 | Gallery pointer is changed, refreshing gallery browser +[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:48:43,921 | Main view page is changed to 1 +[0x00004fe8] | UI | WARN | 2023-05-17 09:48:43,921 | Failed to get auto update information. +[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:48:43,930 | Main view page is changed to 1 +[0x00004fe8] | UI | WARN | 2023-05-17 09:48:44,566 | UI Persistence - MediaPoolFloatingWindowGeometry not read +[0x00004fe8] | Undefined | INFO | 2023-05-17 09:48:44,583 | The buttonId [7] is not found for [0] in [0] +[0x000038ec] | Fairlight | INFO | 2023-05-17 09:48:44,602 | 00.02.33.795(001): WASAPI: switching to AUTO because exclusive mode not allowed +[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:48:44,912 | Main view page is changed to 1 +[0x00004fe8] | UI | INFO | 2023-05-17 09:48:45,076 | Launching main window +[0x00004fe8] | SyManager | WARN | 2023-05-17 09:48:45,094 | No reply received from file system, assume successfully deleted folder D:\SYNC\BACKUP\Resolve Project Backups\64d8dbe8-256e-46b1-81da-671a6a8fc423. +[0x00004fe8] | SyManager | WARN | 2023-05-17 09:48:45,105 | No reply received from file system, assume successfully deleted folder C:\Users\videopro\Videos\CacheClip\64d8dbe8-256e-46b1-81da-671a6a8fc423. +[0x00004fe8] | SyManager | WARN | 2023-05-17 09:48:45,116 | No reply received from file system, assume successfully deleted folder C:\Users\videopro\Videos\.gallery\64d8dbe8-256e-46b1-81da-671a6a8fc423. +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,122 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_newProject] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,122 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_newFolder] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,122 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_openProject] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,122 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_openProjectInReadOnlyMode] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,122 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_Close] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,122 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_Rename] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,122 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_saveProject] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,122 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_saveProjectAs] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_importProject] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_importProjectPlus] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_exportProject] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_exportProjectWithStillsAndLuts] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_restoreProject] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_restoreProjectPlus] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_archiveProject] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_unlockProject] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [editDelete] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [editCut] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [editCopy] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [editPaste] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_loadProjectSettingsToCurrentProject] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_projectBackups] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_projectSettings] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_dynamicProjectSwitching] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_closeProjectsInMemory] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_updateThumbnails] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_otherProjectBackups] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_refresh] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_resetUiLayout] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:51,277 | Action owner [MediaPool] is not active when refreshing shortcut [editDuplicateTimelineOrClips] due to [No focused widget is set yet] +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:01,060 | Traceback (most recent call last): +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:01,060 | File "", line 1, in +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:01,061 | AttributeError +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:01,061 | : +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:01,061 | module 'sys' has no attribute '__path__' +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:01,061 | +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:08,715 | Traceback (most recent call last): +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:08,715 | File "", line 1, in +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:08,715 | AttributeError +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:08,716 | : +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:08,716 | module 're' has no attribute '__path__' +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:08,716 | +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:21,531 | Traceback (most recent call last): +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:21,531 | File "", line 1, in +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:21,532 | TypeError +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:21,532 | : +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:21,532 | split() missing 2 required positional arguments: 'pattern' and 'string' +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:21,532 | +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:29,443 | Traceback (most recent call last): +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:29,443 | File "", line 1, in +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:29,444 | NameError +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:29,444 | : +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:29,444 | name 're__path__' is not defined +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:29,444 | +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:33,563 | Traceback (most recent call last): +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:33,563 | File "", line 1, in +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:33,564 | AttributeError +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:33,564 | : +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:33,564 | module 're' has no attribute '__path__' +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:33,564 | +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:39,740 | Traceback (most recent call last): +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:39,740 | File "", line 1, in +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:39,740 | AttributeError +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:39,740 | : +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:39,740 | module 'sys' has no attribute 'info' +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:39,740 | +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:41,372 | Traceback (most recent call last): +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:41,372 | File "", line 1, in +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:41,372 | AttributeError +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:41,372 | : +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:41,372 | module 'sys' has no attribute 'info' +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:41,372 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:50:52,078 | >>> [ openpype.hosts.resolve installed ] +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:50:52,083 | >>> [ Registering DaVinci Resovle plug-ins.. ] +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:50:52,100 | >>> [ Assigning resolve module to `pype.hosts.resolve.api.bmdvr`: Resolve (0x00007FF6BB5340D0) [App: 'Resolve' on 127.0.0.1, UUID: f0d29a26-23ed-495a-9eb5-f264f7187252] ] +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:50:52,105 | >>> [ Assigning resolve module to `pype.hosts.resolve.api.bmdvf`: FusionUI (0x0000023A629E7040) [App: 'Resolve' on 127.0.0.1, UUID: f0d29a26-23ed-495a-9eb5-f264f7187252] ] +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:50:52,110 | - { timers_manager }: [ Installing task changed callback ] +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:51:12,816 | - { openpype.pipeline.anatomy }: [ Looking for matching root in path "D:/SYNC/OPENPYPE/gazprom_screens/06_R_Home_Painter/work/Lookdev". ] +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:51:12,821 | >>> [ Found match in root "work". ] +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,952 | Traceback (most recent call last): +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,953 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\subsetmanager\window.py", line 190, in showEvent +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,953 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,953 | self.refresh() +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,953 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,953 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\subsetmanager\window.py", line 176, in refresh +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,953 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,954 | self._model.refresh() +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,954 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,954 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\subsetmanager\model.py", line 27, in refresh +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,954 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,954 | instances = list_instances() +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,954 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,954 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\hosts\resolve\api\pipeline.py", line 285, in list_instances +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,955 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,955 | selected_timeline_items = lib.get_current_timeline_items( +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,955 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,955 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\hosts\resolve\api\lib.py", line 319, in get_current_timeline_items +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,955 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,955 | selected_track_count = timeline.GetTrackCount(track_type) +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,955 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,956 | AttributeError +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,956 | : +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,956 | 'NoneType' object has no attribute 'GetTrackCount' +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,956 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,000 | Traceback (most recent call last): +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,000 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\libraryloader\app.py", line 376, in _assetschanged +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,000 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,000 | subsets_model.set_assets(asset_ids) +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,001 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,001 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\loader\model.py", line 259, in set_assets +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,001 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,001 | self.refresh() +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,001 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,001 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\loader\model.py", line 577, in refresh +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,001 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,002 | repre_ids = {con.get("representation") for con in containers} +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,002 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,002 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\loader\model.py", line 577, in +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,002 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,002 | repre_ids = {con.get("representation") for con in containers} +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,002 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,002 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\hosts\resolve\api\pipeline.py", line 138, in ls +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,003 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,003 | all_timeline_items = lib.get_current_timeline_items(filter=False) +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,003 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,003 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\hosts\resolve\api\lib.py", line 319, in get_current_timeline_items +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,003 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,003 | selected_track_count = timeline.GetTrackCount(track_type) +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,003 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,003 | AttributeError +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,004 | : +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,004 | 'NoneType' object has no attribute 'GetTrackCount' +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,004 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,731 | Traceback (most recent call last): +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,732 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\libraryloader\app.py", line 376, in _assetschanged +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,732 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,732 | subsets_model.set_assets(asset_ids) +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,732 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,732 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\loader\model.py", line 259, in set_assets +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,732 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,733 | self.refresh() +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,733 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,733 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\loader\model.py", line 577, in refresh +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,733 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,733 | repre_ids = {con.get("representation") for con in containers} +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,733 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,734 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\loader\model.py", line 577, in +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,734 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,734 | repre_ids = {con.get("representation") for con in containers} +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,734 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,734 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\hosts\resolve\api\pipeline.py", line 138, in ls +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,734 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,734 | all_timeline_items = lib.get_current_timeline_items(filter=False) +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,735 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,735 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\hosts\resolve\api\lib.py", line 319, in get_current_timeline_items +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,735 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,735 | selected_track_count = timeline.GetTrackCount(track_type) +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,735 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,735 | AttributeError +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,735 | : +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,735 | 'NoneType' object has no attribute 'GetTrackCount' +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,736 | +[0x00004fe8] | UI | WARN | 2023-05-17 09:53:56,329 | UI Persistence - MediaPoolFloatingWindowGeometry not read +[0x00004fe8] | UI | WARN | 2023-05-17 09:53:56,329 | UI Persistence - ConformEdlEffectsLibrary not read +[0x000065b4] | GPU.SingleBoardMgr | INFO | 2023-05-17 09:53:56,766 | Flushing GPU memory... +[0x000065b4] | UI.GLContext | INFO | 2023-05-17 09:53:56,766 | Creating shared OpenGL context for this thread (5 total). +[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:53:56,782 | Main view page is changed to 2 +[0x000065b4] | UI.GLTexPool | INFO | 2023-05-17 09:53:56,884 | Released 0 MiB in 0 unused textures. +[0x00004fe8] | UI | INFO | 2023-05-17 09:53:56,944 | PBO is initialized with size [1920x1080], bitDepth=[8], hasAlpha=[1]. +[0x00004fe8] | UI | WARN | 2023-05-17 09:53:59,491 | Failed to find value '1' in combo-box +[0x00004fe8] | UI | WARN | 2023-05-17 09:53:59,491 | Failed to find value '2' in combo-box +[0x00004fe8] | UI | WARN | 2023-05-17 09:53:59,491 | Failed to find value '2' in combo-box +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:53:59,548 | Action owner [MediaPool] is not active when refreshing shortcut [editDuplicateTimelineOrClips] due to [No focused widget is set yet] +[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:54:01,297 | Current timeline pointer (Timeline 1) is changed +[0x00004fe8] | UI | ERROR | 2023-05-17 09:54:01,405 | No marker found at frame -90000 to delete. +[0x00004fe8] | UI | ERROR | 2023-05-17 09:54:01,405 | No marker found at frame -90000 to delete. +[0x00004fe8] | UI | ERROR | 2023-05-17 09:54:01,405 | No marker found at frame -90000 to delete. +[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:01,495 | Start saving project gazprom_screens_06_R_Home_Painter_Lookdev_v002 +[0x000083dc] | SyManager.ActionExecutor | INFO | 2023-05-17 09:54:01,526 | Database transaction is ongoing, user initiated action SmActIOCPSigInvoker is postponed +[0x000083dc] | SyManager.ActionExecutor | INFO | 2023-05-17 09:54:01,550 | Database transaction is ongoing, user initiated action SmActIOCPSigInvoker is postponed +[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:01,592 | Saving project took 97 ms +[0x00004fe8] | SyManager.ActionExecutor | INFO | 2023-05-17 09:54:01,619 | Database transaction completed, enqueueing 2 postponed actions +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:54:23,357 | >>> [ openpype.hosts.resolve installed ] +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:54:23,363 | >>> [ Registering DaVinci Resovle plug-ins.. ] +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:54:23,375 | >>> [ Assigning resolve module to `pype.hosts.resolve.api.bmdvr`: Resolve (0x00007FF6BB5340D0) [App: 'Resolve' on 127.0.0.1, UUID: f0d29a26-23ed-495a-9eb5-f264f7187252] ] +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:54:23,380 | >>> [ Assigning resolve module to `pype.hosts.resolve.api.bmdvf`: FusionUI (0x0000023A629E7040) [App: 'Resolve' on 127.0.0.1, UUID: f0d29a26-23ed-495a-9eb5-f264f7187252] ] +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:54:23,386 | - { timers_manager }: [ Installing task changed callback ] +[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:39,823 | Start saving project gazprom_screens_06_R_Home_Painter_Lookdev_v002 +[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:39,826 | Saving project took 2 ms +[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:39,920 | Start saving project gazprom_screens_06_R_Home_Painter_Lookdev_v002 +[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:39,923 | Saving project took 3 ms +[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:40,137 | Start saving project gazprom_screens_06_R_Home_Painter_Lookdev_v002 +[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:40,321 | Saving project took 183 ms +[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:54:40,326 | Main view page is changed to 2 +[0x00004fe8] | BtCommon | WARN | 2023-05-17 09:54:40,570 | Negative duration +[0x00004fe8] | UI | WARN | 2023-05-17 09:54:40,638 | Unable to submit frame to GPU scopes, legacy OpenGL uploads not supported (Player Model). +[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:42,497 | Start saving project gazprom_screens_06_R_Home_Painter_Lookdev_v002 +[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:42,555 | Saving project took 59 ms diff --git a/openpype/hosts/resolve/hooks/pre_resolve_setup.py b/openpype/hosts/resolve/hooks/pre_resolve_setup.py index 8c88478104..486d8121cf 100644 --- a/openpype/hosts/resolve/hooks/pre_resolve_setup.py +++ b/openpype/hosts/resolve/hooks/pre_resolve_setup.py @@ -33,6 +33,9 @@ class ResolvePrelaunch(PreLaunchHook): resolve_script_api = Path( resolve_script_api_locations[current_platform] ) + self.log.info( + f"setting RESOLVE_SCRIPT_API variable to {resolve_script_api}" + ) self.launch_context.env[ "RESOLVE_SCRIPT_API" ] = resolve_script_api.as_posix() @@ -52,6 +55,9 @@ class ResolvePrelaunch(PreLaunchHook): self.launch_context.env[ "RESOLVE_SCRIPT_LIB" ] = resolve_script_lib.as_posix() + self.log.info( + f"setting RESOLVE_SCRIPT_LIB variable to {resolve_script_lib}" + ) # TODO: add OTIO installation from `openpype/requirements.py` # making sure python <3.9.* is installed at provided path @@ -69,14 +75,6 @@ class ResolvePrelaunch(PreLaunchHook): self.launch_context.env["PYTHONHOME"] = python3_home_str self.log.info(f"Path to Resolve Python folder: `{python3_home_str}`") - # add to the python path to PATH - env_path = self.launch_context.env["PATH"] - self.launch_context.env[ - "PATH" - ] = f"{python3_home_str}{os.pathsep}{env_path}" - - self.log.debug(f"PATH: {self.launch_context.env['PATH']}") - # add to the PYTHONPATH env_pythonpath = self.launch_context.env["PYTHONPATH"] modules_path = Path(resolve_script_api, "Modules").as_posix() From cd7b1d92f8169ac655f69cd1ba69272e449fa19b Mon Sep 17 00:00:00 2001 From: Alexey Bogomolov Date: Wed, 17 May 2023 10:05:28 +0300 Subject: [PATCH 282/360] remove resolve logs --- .../Support/logs/davinci_resolve.log | 443 ------------------ 1 file changed, 443 deletions(-) delete mode 100644 igniter/#ProgramData#Blackmagic Design/DaVinci Resolve/Support/logs/davinci_resolve.log diff --git a/igniter/#ProgramData#Blackmagic Design/DaVinci Resolve/Support/logs/davinci_resolve.log b/igniter/#ProgramData#Blackmagic Design/DaVinci Resolve/Support/logs/davinci_resolve.log deleted file mode 100644 index 8f1a3b712e..0000000000 --- a/igniter/#ProgramData#Blackmagic Design/DaVinci Resolve/Support/logs/davinci_resolve.log +++ /dev/null @@ -1,443 +0,0 @@ -[0x00004fe8] | Undefined | INFO | 2023-05-17 09:46:08,249 | -------------------------------------------------------------------------------- -[0x00004fe8] | Undefined | INFO | 2023-05-17 09:46:08,250 | Loaded log config from C:\Users\videopro\AppData\Roaming\Blackmagic Design\DaVinci Resolve\Preferences\log-conf.xml -[0x00004fe8] | Undefined | INFO | 2023-05-17 09:46:08,250 | -------------------------------------------------------------------------------- -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:08,251 | Running DaVinci Resolve Studio v18.1.2.0006 (Windows/MSVC x86_64) -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:08,251 | BMD_BUILD_UUID 3ff36663-26c9-45d9-8506-101676c881e0 -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:08,251 | BMD_GIT_COMMIT a3be29d0542aeafcb1b0933bb5ace426aa7d047d -[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,259 | Starting GPUDetect 1.2_3-a1 -[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,421 | Done in 161 ms. -[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,421 | Detected System: -[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,421 | - OS: Windows 10 Pro (Build 19045) -[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,421 | - Model: ASUSTeK ROG STRIX B550-F GAMING (WI-FI) -[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,421 | - System ID: 838d5045-58b0-44ed-854c-19be5b814d6f -[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,421 | - CPU: AMD Ryzen 7 3700X, 16 threads, x86-64 -[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,421 | - RAM: 16.6 GiB used of 63.9 GiB -[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,421 | - NVIDIA GPU Driver: 527.56, supports CUDA 12.0 -[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,421 | Detected 1 GPUs: -[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,423 | - "NVIDIA GeForce RTX 2060 SUPER" (gpu:a24a3d50.83f1fdb7) <- Main Display GPU -[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,423 | Discrete, 2.0 GiB used of 7.6 GiB VRAM, PCI:8:0 (x16) -[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,423 | Matches: CUDA, DirectX, NVAPI, NVML, OpenCL, Win32 -[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,423 | Detected 1 monitors: -[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,423 | - "Generic PnP Monitor" <- Main Monitor -[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,423 | 3840x2160, connected to "NVIDIA GeForce RTX 2060 SUPER" (gpu:a24a3d50.83f1fdb7) -[0x00004fe8] | Main.GPUConfig | INFO | 2023-05-17 09:46:08,424 | Selected compute API: CUDA -[0x00004fe8] | Main.GPUConfig | INFO | 2023-05-17 09:46:08,424 | Automatic GPU Selection: -[0x00004fe8] | Main.GPUConfig | INFO | 2023-05-17 09:46:08,424 | - "NVIDIA GeForce RTX 2060 SUPER" (gpu:a24a3d50.83f1fdb7) -[0x00004fe8] | IO | INFO | 2023-05-17 09:46:08,438 | RED InitializeSdk with library path at C:\ProgramData\Blackmagic Design\DaVinci Resolve\Support\Libraries -[0x00004fe8] | IO | INFO | 2023-05-17 09:46:08,642 | R3DAPI 8.3.1-52407 (20220725 Wx64S) R3DSDK 8.3.1-52407 (20220725 Wx64D C3B3) RED CUDA 8.3.1-52408 (20220725) [C:\ProgramData\Blackmagic Design\DaVinci Resolve\Support\Libraries\] init is successful -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:08,642 | 0 RED rocket cards available -[0x00004fe8] | SyManager.DeckLink | ERROR | 2023-05-17 09:46:08,645 | Failed to create instance. -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:08,645 | Decklink model name: '', version: '' -[0x00004fe8] | DVIP | INFO | 2023-05-17 09:46:08,645 | DVIP release/18.1.2 build 2 (86acfdf407856b6cd8daf2517ab0b44f1efc332f). Release, version 18.1.2. -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:08,645 | Resolve Module Handle 00007FF6A4B50000 -[0x00001238] | IO | INFO | 2023-05-17 09:46:08,646 | Using DNxHR library v2.7.3.27r -[0x00002044] | Fusion | INFO | 2023-05-17 09:46:09,093 | Fusion Build: 008b13c7_0004 (Dec 7 2022 15:23:40) -[0x00005f70] | Fusion | INFO | 2023-05-17 09:46:09,114 | fusionsystem: = "C:\Program Files\Blackmagic Design\DaVinci Resolve\fusionsystem.dll" -[0x00001238] | IO | INFO | 2023-05-17 09:46:09,117 | NVDEC is using upto (1023) MB -[0x00001238] | IO | INFO | 2023-05-17 09:46:09,119 | NVDEC decodes H264, chroma 4:2:0, bitdepth 8, upto 4096 x 4096 -[0x00001238] | IO | INFO | 2023-05-17 09:46:09,121 | NVDEC decodes HEVC, chroma 4:2:0, bitdepth 8, upto 8192 x 8192 -[0x00001238] | IO | INFO | 2023-05-17 09:46:09,123 | NVDEC decodes HEVC, chroma 4:2:0, bitdepth 10, upto 8192 x 8192 -[0x00001238] | IO | INFO | 2023-05-17 09:46:09,127 | NVDEC decodes HEVC, chroma 4:2:0, bitdepth 12, upto 8192 x 8192 -[0x00001238] | IO | INFO | 2023-05-17 09:46:09,128 | NVDEC decodes HEVC, chroma 4:4:4, bitdepth 8, upto 8192 x 8192 -[0x00001238] | IO | INFO | 2023-05-17 09:46:09,130 | NVDEC decodes HEVC, chroma 4:4:4, bitdepth 10, upto 8192 x 8192 -[0x00001238] | IO | INFO | 2023-05-17 09:46:09,131 | NVDEC decodes HEVC, chroma 4:4:4, bitdepth 12, upto 8192 x 8192 -[0x00001238] | IO | INFO | 2023-05-17 09:46:09,133 | NVDEC decodes VP9, chroma 4:2:0, bitdepth 8, upto 8192 x 8192 -[0x00001238] | IO | INFO | 2023-05-17 09:46:09,134 | NVDEC decodes VP9, chroma 4:2:0, bitdepth 10, upto 8192 x 8192 -[0x00005f70] | Fusion | INFO | 2023-05-17 09:46:09,148 | FusionLibs: = "C:\Program Files\Blackmagic Design\DaVinci Resolve\" -[0x00005f70] | Fusion | INFO | 2023-05-17 09:46:09,148 | UserData: = "C:\Users\videopro\AppData\Roaming\Blackmagic Design\DaVinci Resolve\Support\Fusion" -[0x00005f70] | Fusion | INFO | 2023-05-17 09:46:09,148 | Profiles: = "UserData:Profiles\" -[0x00001238] | IO | INFO | 2023-05-17 09:46:09,148 | Nvidia GPU (0) is initialised as decoding and encoding device. -[0x00001238] | IO | INFO | 2023-05-17 09:46:09,182 | IO codec library load completed in 536 ms. -[0x00005cbc] | SyManager | ERROR | 2023-05-17 09:46:09,202 | BlackmagicIDHelper::GetProjectLibraries() - Access token is empty -[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:09,268 | Loading dblist file: C:\Users\videopro\AppData\Roaming\Blackmagic Design\DaVinci Resolve\Preferences\dblist.conf -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:09,314 | Finished loading Application style sheet -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:09,396 | Show splash screen -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:09,396 | Show splash screen message: Starting Up -[0x00004fe8] | Fusion | INFO | 2023-05-17 09:46:09,398 | Module Handle 0000023A5FC80000 fusionsystem -[0x0000204c] | Fusion | INFO | 2023-05-17 09:46:09,410 | Module Handle 0000023A6C160000 C:\Program Files\Blackmagic Design\DaVinci Resolve\fusiongraphics.dll -[0x00004f28] | Fusion | INFO | 2023-05-17 09:46:09,437 | Module Handle 0000023A6FB90000 fusionoperators.dll -[0x00005f70] | Fusion | INFO | 2023-05-17 09:46:09,462 | Module Handle 0000023A70490000 fusioncontrols.dll -[0x00007b34] | Fusion | INFO | 2023-05-17 09:46:09,488 | Module Handle 0000023A70870000 3d.plugin -[0x0000772c] | Fusion | INFO | 2023-05-17 09:46:09,510 | Module Handle 0000023A70B50000 dimension.plugin -[0x00003934] | Fusion | INFO | 2023-05-17 09:46:09,528 | Module Handle 0000023A70F70000 alembic.plugin -[0x0000204c] | Fusion | INFO | 2023-05-17 09:46:09,551 | Module Handle 0000023A71320000 fbx.plugin -[0x000078cc] | Fusion | INFO | 2023-05-17 09:46:09,566 | Module Handle 0000023A464C0000 fuses.plugin -[0x00004f28] | Fusion | INFO | 2023-05-17 09:46:09,582 | Module Handle 0000023A71A70000 opencolorio.plugin -[0x00005f70] | Fusion | INFO | 2023-05-17 09:46:09,599 | Module Handle 0000023A6ADF0000 openfx.plugin -[0x00007b34] | Fusion | INFO | 2023-05-17 09:46:09,615 | Module Handle 0000023A68DA0000 openvr.plugin -[0x0000772c] | Fusion | INFO | 2023-05-17 09:46:09,632 | Module Handle 0000023A6AE60000 paint.plugin -[0x00003934] | Fusion | INFO | 2023-05-17 09:46:09,653 | Module Handle 0000023A6AED0000 particles.plugin -[0x0000204c] | Fusion | INFO | 2023-05-17 09:46:09,673 | Module Handle 0000023A720D0000 text.plugin -[0x000078cc] | Fusion | INFO | 2023-05-17 09:46:09,690 | Module Handle 0000023A6CB80000 utilities.plugin -[0x00004f28] | Fusion | INFO | 2023-05-17 09:46:09,715 | Module Handle 00007FF89F590000 KrokodoveFu16.plugin -[0x00005f70] | Fusion | INFO | 2023-05-17 09:46:09,717 | Module Handle 00007FF89F330000 KrokodoveFu17.plugin -[0x00004f28] | OpenFX | INFO | 2023-05-17 09:46:09,817 | No context is available for com.absoft.NeatVideo5 -[0x00007fc8] | Main | INFO | 2023-05-17 09:46:09,828 | Started listener socket at port 15000 -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:10,308 | Show splash screen message: Checking Licenses -[0x00004fe8] | BtCommon | INFO | 2023-05-17 09:46:10,532 | Memory config: reserved=12270M pinned=8000M log=0 -[0x00004fe8] | BtCommon | INFO | 2023-05-17 09:46:10,532 | Using default pooled memory manager -[0x00003798] | LeManager | INFO | 2023-05-17 09:46:10,533 | 521, 29 -[0x00008180] | BtCommon | INFO | 2023-05-17 09:46:10,533 | BtResourceManager Process Thread Started -[0x00004fe8] | IO | INFO | 2023-05-17 09:46:10,609 | WMF encoder cnt for SW (hvc1) is (0) -[0x00004fe8] | IO | INFO | 2023-05-17 09:46:10,624 | WMF encoder cnt for HW (hvc1) is (1) -[0x00004fe8] | IO | INFO | 2023-05-17 09:46:10,624 | Setting codec capacity (0) -[0x00004fe8] | SyManager | INFO | 2023-05-17 09:46:10,625 | Total: 20, NumDtThreads: 8, NumComms: 0, NumSites: 1 - -[0x00004fe8] | SyManager | INFO | 2023-05-17 09:46:10,625 | Lookaheads -> playback = 20, record = 20, stop = 2 - -[0x00004fe8] | DtManager | INFO | 2023-05-17 09:46:10,626 | Using 8 generic IO threads -[0x00004fe8] | DtManager | INFO | 2023-05-17 09:46:10,626 | Total of 16 IO threads (including 8 generic and 8 Red decode threads) -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:10,627 | Show splash screen message: Loading Project Libraries -[0x00001a20] | DtManager | INFO | 2023-05-17 09:46:10,627 | Dt Worker Thread Started -[0x00007da0] | GsManager | INFO | 2023-05-17 09:46:10,628 | Gs Processor Thread ----- (32160) - -[0x00004474] | DtManager | INFO | 2023-05-17 09:46:10,628 | Dt Worker Thread Started -[0x00002b7c] | DtManager | INFO | 2023-05-17 09:46:10,628 | Dt Worker Thread Started -[0x00002d64] | DtManager | INFO | 2023-05-17 09:46:10,628 | Dt Worker Thread Started -[0x00007bbc] | DtManager | INFO | 2023-05-17 09:46:10,628 | Dt Data Handler Thread Started -[0x00002ad0] | DtManager | INFO | 2023-05-17 09:46:10,628 | Dt Worker Thread Started -[0x00004a48] | DtManager | INFO | 2023-05-17 09:46:10,629 | Dt Worker Thread Started -[0x00005084] | DtManager | INFO | 2023-05-17 09:46:10,629 | Dt Worker Thread Started -[0x00003050] | DtManager | INFO | 2023-05-17 09:46:10,629 | Dt Worker Thread Started -[0x00003e88] | DtManager | INFO | 2023-05-17 09:46:10,629 | Dt Worker Thread Started -[0x00006550] | DtManager | INFO | 2023-05-17 09:46:10,629 | Dt Worker Thread Started -[0x00007950] | DtManager | INFO | 2023-05-17 09:46:10,629 | Dt Worker Thread Started -[0x000059a8] | DtManager | INFO | 2023-05-17 09:46:10,629 | Dt Worker Thread Started -[0x00007de4] | DtManager | INFO | 2023-05-17 09:46:10,629 | Dt Worker Thread Started -[0x00007a24] | DtManager | INFO | 2023-05-17 09:46:10,629 | Dt Worker Thread Started -[0x00001eec] | DtManager | INFO | 2023-05-17 09:46:10,630 | Dt Worker Thread Started -[0x00001f5c] | DtManager | INFO | 2023-05-17 09:46:10,630 | Dt Worker Thread Started -[0x00007a50] | DtManager | INFO | 2023-05-17 09:46:10,630 | Dt Worker Thread Started -[0x000009c4] | DtManager | INFO | 2023-05-17 09:46:10,630 | Dt Worker Thread Started -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:10,633 | Show splash screen message: Initializing system components -[0x00001190] | GPU.MultiBoardMgr | INFO | 2023-05-17 09:46:10,635 | Let There Be CUDA Light! -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:10,639 | Show splash screen message: Loading video codecs -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:10,644 | Show splash screen message: Loading video plugins -[0x00004fe8] | UI.GLContext | INFO | 2023-05-17 09:46:10,680 | Creating shared OpenGL context for this thread (1 total). -[0x00004fe8] | UI.GLContext | INFO | 2023-05-17 09:46:10,708 | Initialized OpenGL 4.6 (requested 2.0) on device 'NVIDIA Corporation NVIDIA GeForce RTX 2060 SUPER/PCIe/SSE2' -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:10,708 | Show splash screen message: Loading Fairlight Engine -[0x00001190] | GPU.MultiBoardMgr | INFO | 2023-05-17 09:46:10,708 | Initializing CUDA board manager for Main Display GPU gpu:a24a3d50.83f1fdb7. -[0x00001968] | IO | INFO | 2023-05-17 09:46:10,788 | IO codec initialization completed in 143 ms. -[0x000065b4] | GPU.SingleBoardMgr | INFO | 2023-05-17 09:46:10,811 | Board manager thread for "NVIDIA GeForce RTX 2060 SUPER" (gpu:a24a3d50.83f1fdb7) is ready. -[0x00001190] | UI.GLInterop | INFO | 2023-05-17 09:46:10,811 | OpenGL interop was initialized. -[0x00001190] | UI.GLInterop | INFO | 2023-05-17 09:46:10,811 | OpenGL interop was initialized. -[0x00001190] | GPU.MultiBoardMgr | INFO | 2023-05-17 09:46:10,811 | Enabled CUDA pinned memory. -[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,011 | 00.00.00.122(003): WASAPI: Scanning Speakers (Steam Streaming Microphone) for formats -[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,011 | 00.00.00.128(000): WASAPI: Device: Speakers (Steam Streaming Microphone). Scan time 6. Formats = 1 15 formats scanned -[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,011 | 00.00.00.128(000): WASAPI: Scanning Speakers (High Definition Audio Device) for formats -[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,011 | 00.00.00.190(000): WASAPI: Device: Speakers (High Definition Audio Device). Scan time 61. Formats = 1 15 formats scanned -[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,011 | 00.00.00.190(000): WASAPI: Scanning Mi 27 NU (NVIDIA High Definition Audio) for formats -[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,011 | 00.00.00.236(000): WASAPI: Device: Mi 27 NU (NVIDIA High Definition Audio). Scan time 45. Formats = 1 15 formats scanned -[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,011 | 00.00.00.236(000): WASAPI: Scanning Speakers (Steam Streaming Speakers) for formats -[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,011 | 00.00.00.242(000): WASAPI: Device: Speakers (Steam Streaming Speakers). Scan time 6. Formats = 1 15 formats scanned -[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,011 | 00.00.00.243(001): WASAPI: Scanning Digital Audio (S/PDIF) (High Definition Audio Device) for formats -[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,212 | 00.00.00.286(000): WASAPI: Device: Digital Audio (S/PDIF) (High Definition Audio Device). Scan time 42. Formats = 1 15 formats scanned -[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,212 | 00.00.00.287(001): WASAPI: Scanning Microphone (Steam Streaming Microphone) for formats -[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,212 | 00.00.00.294(000): WASAPI: Device: Microphone (Steam Streaming Microphone). Scan time 7. Formats = 0 16 formats scanned -[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,212 | 00.00.00.390(002): WASAPI: switching to AUTO because exclusive mode not allowed -[0x00004f94] | DbCommon2 | ERROR | 2023-05-17 09:46:11,307 | Cannot connect to soundfx192.168.100.31 database: could not connect to server: Connection refused (0x0000274D/10061) - Is the server running on host "192.168.100.31" and accepting - TCP/IP connections on port 5432? -QPSQL: Unable to connect -[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:11,339 | postgres project library homepc at 127.0.0.1 version 9.5.22 -[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:11,347 | Project library [homepc127.0.0.1] current version <18.1.0.004> updated on <2022-11-22T16:16:24.518>, remark: -[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:11,347 | Connect to postgres project library homepc127.0.0.1 -[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:11,396 | postgres project library barney at 192.168.100.30 version 9.5.25 -[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:11,422 | Project library [barney192.168.100.30] current version <18.1.0.004> updated on <2022-11-17T08:47:18.322>, remark: -[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:11,422 | Connect to postgres project library barney192.168.100.30 -[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:11,475 | postgres project library shtv_barney at 192.168.100.30 version 9.5.25 -[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:11,501 | Project library [shtv_barney192.168.100.30] current version <18.1.0.004> updated on <2022-11-15T13:16:50.006>, remark: -[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:11,501 | Connect to postgres project library shtv_barney192.168.100.30 -[0x00005850] | Fusion | INFO | 2023-05-17 09:46:11,704 | 260 templates scanned in 0.12 secs -[0x00004fe8] | Fairlight | INFO | 2023-05-17 09:46:12,310 | 00.00.01.565(000): Running Fairlight (939d5c56aa8d66c1f392ff963f9b9c349ef4d9fb) -[0x00004fe8] | FairlightLoader | INFO | 2023-05-17 09:46:12,310 | Fairlight lib initialized in 1598 ms. -[0x00006a7c] | UI.GLContext | INFO | 2023-05-17 09:46:12,405 | Creating shared OpenGL context for this thread (2 total). -[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:12,417 | 00.00.01.501(002): WASAPI: switching to AUTO because exclusive mode not allowed -[0x00006a7c] | UI.GLIO | INFO | 2023-05-17 09:46:12,454 | Initialized MainPlayer OpenGL I/O on CUDA device 'NVIDIA GeForce RTX 2060 SUPER' -[0x00004fe8] | UI.GLIO | INFO | 2023-05-17 09:46:12,454 | MainPlayer: OpenGL I/O setup done. -[0x000076bc] | UI.GLContext | INFO | 2023-05-17 09:46:12,455 | Creating shared OpenGL context for this thread (3 total). -[0x000076bc] | UI.GLIO | INFO | 2023-05-17 09:46:12,505 | Initialized AuxPlayer OpenGL I/O on CUDA device 'NVIDIA GeForce RTX 2060 SUPER' -[0x00004fe8] | UI.GLIO | INFO | 2023-05-17 09:46:12,505 | AuxPlayer: OpenGL I/O setup done. -[0x00008190] | UI.GLContext | INFO | 2023-05-17 09:46:12,506 | Creating shared OpenGL context for this thread (4 total). -[0x00008190] | UI.Scopes | INFO | 2023-05-17 09:46:12,565 | Initialized GPU Scopes Manager on CUDA device 'NVIDIA GeForce RTX 2060 SUPER' -[0x00004fe8] | UI.MenuBar | WARN | 2023-05-17 09:46:12,649 | Main menu action [workspaceLayoutFusion_sub001Default]'s slot is not defined: workspaceLayoutFusion_sub001Default_triggered() -[0x00004fe8] | UI.MenuBar | WARN | 2023-05-17 09:46:12,649 | Main menu action [workspaceWIPlugins_placeholder]'s slot is not defined: workspaceWIPlugins_placeholder_triggered() -[0x00006e14] | SyManager | WARN | 2023-05-17 09:46:12,656 | socket failed to connect to server, error: 10061 - -[0x00006e14] | SyManager | ERROR | 2023-05-17 09:46:12,656 | DRIVER: panel connection failed -[0x00004fe8] | FusionUtils | WARN | 2023-05-17 09:46:12,657 | Fusion DoAction ACTION_SET_FUSION_HOTKEY ignored, init not completed -[0x000052e4] | BtCommon | INFO | 2023-05-17 09:46:12,661 | Starting Daemon: C:/Program Files/Blackmagic Design/DaVinci Resolve/DaVinciPanelDaemon.exe -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:12,686 | Show splash screen message: Loading Project Settings -[0x00006e14] | SyManager | INFO | 2023-05-17 09:46:12,757 | Connection to the panel server has been re-established -[0x00004fe8] | UI | WARN | 2023-05-17 09:46:12,899 | Failed to find value '0' in combo-box -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:19,055 | Show splash screen message: Loading Media Page -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:46:19,145 | Action [fairlightBusStructure] is not a global action -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:46:19,145 | Action [fairlightBusStructure] is not a valid global action -[0x00004fe8] | UI.FairlightInterface | WARN | 2023-05-17 09:46:19,269 | SetActionEnabled: Failed to find action [viewTimelineScrollingFixed]'s action connector for handler Id [1] -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:19,424 | Show splash screen message: Loading Cut Page -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:19,698 | Show splash screen message: Loading Edit Page -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:19,951 | Show splash screen message: Loading Fusion Page -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:20,038 | Show splash screen message: Loading Fairlight Page -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:20,227 | Show splash screen message: Loading Color Page -[0x00004fe8] | UI | INFO | 2023-05-17 09:46:20,296 | Not creating special GL widget for screen 0 -[0x00004fe8] | UI | WARN | 2023-05-17 09:46:21,003 | Failed to find value '8192' in combo-box -[0x00004fe8] | UI | WARN | 2023-05-17 09:46:21,284 | Failed to find value '100' in combo-box -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:22,006 | Show splash screen message: Loading Waveform Monitor -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:22,433 | Show splash screen message: Loading Audio Plugins -[0x00004fe8] | SyManager | INFO | 2023-05-17 09:46:22,528 | Collaboration IP 127.0.0.1 Port 0 -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:22,674 | Show splash screen message: Loading Projects -[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:46:22,739 | Current user pointer is changed -[0x00004fe8] | UI | WARN | 2023-05-17 09:46:22,755 | UI Persistence - SecondaryScreenIdx not read -[0x00004fe8] | UI | WARN | 2023-05-17 09:46:22,755 | UI Persistence - UseDisplayNameForClips not read -[0x00004fe8] | UI | WARN | 2023-05-17 09:46:22,755 | UI Persistence - DefaultTransitionKey not read -[0x00004fe8] | UI | WARN | 2023-05-17 09:46:22,755 | UI Persistence - DefaultAudioTransitionKey not read -[0x00004fe8] | IO | INFO | 2023-05-17 09:46:22,767 | RED rocket decode has been disabled in the config file -[0x00004fe8] | LeManager | ERROR | 2023-05-17 09:46:22,820 | 444, 139, 0 -[0x00004fe8] | SyManager.DeckLink | ERROR | 2023-05-17 09:46:22,922 | Failed to create instance. -[0x00004fe8] | SyManager | INFO | 2023-05-17 09:46:22,973 | Search Parameter (-2) or Search Operation (0) not supported, and hence disabling the condition. -[0x00004fe8] | SyManager | INFO | 2023-05-17 09:46:22,973 | Search Parameter (-1) or Search Operation (-1) not supported, and hence disabling the condition. -[0x00004fe8] | UI | WARN | 2023-05-17 09:46:23,077 | Failed to find value '0' in combo-box -[0x00004fe8] | UI | WARN | 2023-05-17 09:46:23,080 | Failed to find value '0' in combo-box -[0x00004fe8] | UI | WARN | 2023-05-17 09:46:23,098 | Failed to find value '0' in combo-box -[0x00004fe8] | UI | WARN | 2023-05-17 09:46:23,099 | Failed to find value '4' in combo-box -[0x00004fe8] | SyManager.Gallery | INFO | 2023-05-17 09:46:23,119 | Start purging still caches -[0x00004fe8] | SyManager.Gallery | INFO | 2023-05-17 09:46:23,119 | Finish purging still caches -[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:46:23,149 | Media pool relink status changed to 0 -[0x00004fe8] | FusionUtils | WARN | 2023-05-17 09:46:23,167 | Fusion DoAction ACTION_SET_FUSION_HOTKEY ignored, init not completed -[0x00004fe8] | UI | INFO | 2023-05-17 09:46:23,179 | Launching project manager -[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:46:23,182 | Main view page is changed to 12 -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:23,209 | Show splash screen message: Ready -[0x00004fe8] | UI | INFO | 2023-05-17 09:46:23,214 | Gallery pointer is changed, refreshing color gallery -[0x00004fe8] | UI | INFO | 2023-05-17 09:46:23,223 | Gallery pointer is changed, refreshing gallery browser -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:23,312 | Close splash screen -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:23,314 | Launching main loop -[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:46:24,215 | Fusion templates changed -[0x00004fe8] | Fusion | INFO | 2023-05-17 09:46:24,758 | Started script server: 32648 - -[0x00004fe8] | SyManager.ActionExecutor | INFO | 2023-05-17 09:48:41,800 | Database transaction is ongoing, user initiated action Sync Asset Map is postponed -[0x00004fe8] | SyManager.ActionExecutor | INFO | 2023-05-17 09:48:41,800 | Database transaction completed, enqueueing 1 postponed actions -[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:48:41,842 | Loading project (gazprom_screens_06_R_Home_Painter_Lookdev_v002) from project library (homepc127.0.0.1) took 291 ms -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:42,159 | Action [fairlightBusStructure] is not a global action -[0x000038ec] | Fairlight | INFO | 2023-05-17 09:48:42,390 | 00.02.31.644(002): WASAPI: switching to AUTO because exclusive mode not allowed -[0x00004fe8] | UI | WARN | 2023-05-17 09:48:43,528 | Failed to find value '0' in combo-box -[0x00004fe8] | UI | WARN | 2023-05-17 09:48:43,531 | Failed to find value '0' in combo-box -[0x00004fe8] | SyManager.Gallery | INFO | 2023-05-17 09:48:43,539 | Start purging still caches -[0x00004fe8] | SyManager.Gallery | INFO | 2023-05-17 09:48:43,539 | Finish purging still caches -[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:48:43,557 | Media pool relink status changed to 0 -[0x000038ec] | Fairlight | INFO | 2023-05-17 09:48:43,596 | 00.02.32.735(002): WASAPI: switching to AUTO because exclusive mode not allowed -[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:48:43,862 | Current project pointer (gazprom_screens_06_R_Home_Painter_Lookdev_v002) is changed -[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:48:43,862 | Current timeline pointer () is changed -[0x00004fe8] | UI | INFO | 2023-05-17 09:48:43,868 | Lock project 513e336f-59c3-4fa8-b9e9-814cf2b797a3 -[0x00004fe8] | Undefined | INFO | 2023-05-17 09:48:43,868 | The buttonId [7] is not found for [0] in [0] -[0x00004fe8] | UI | INFO | 2023-05-17 09:48:43,876 | UiGPU::UploadWipeData called before UiGPU has been initialized. Returning false -[0x00004fe8] | UI | INFO | 2023-05-17 09:48:43,877 | Gallery pointer is changed, refreshing color gallery -[0x00004fe8] | UI | INFO | 2023-05-17 09:48:43,891 | Gallery pointer is changed, refreshing gallery browser -[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:48:43,921 | Main view page is changed to 1 -[0x00004fe8] | UI | WARN | 2023-05-17 09:48:43,921 | Failed to get auto update information. -[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:48:43,930 | Main view page is changed to 1 -[0x00004fe8] | UI | WARN | 2023-05-17 09:48:44,566 | UI Persistence - MediaPoolFloatingWindowGeometry not read -[0x00004fe8] | Undefined | INFO | 2023-05-17 09:48:44,583 | The buttonId [7] is not found for [0] in [0] -[0x000038ec] | Fairlight | INFO | 2023-05-17 09:48:44,602 | 00.02.33.795(001): WASAPI: switching to AUTO because exclusive mode not allowed -[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:48:44,912 | Main view page is changed to 1 -[0x00004fe8] | UI | INFO | 2023-05-17 09:48:45,076 | Launching main window -[0x00004fe8] | SyManager | WARN | 2023-05-17 09:48:45,094 | No reply received from file system, assume successfully deleted folder D:\SYNC\BACKUP\Resolve Project Backups\64d8dbe8-256e-46b1-81da-671a6a8fc423. -[0x00004fe8] | SyManager | WARN | 2023-05-17 09:48:45,105 | No reply received from file system, assume successfully deleted folder C:\Users\videopro\Videos\CacheClip\64d8dbe8-256e-46b1-81da-671a6a8fc423. -[0x00004fe8] | SyManager | WARN | 2023-05-17 09:48:45,116 | No reply received from file system, assume successfully deleted folder C:\Users\videopro\Videos\.gallery\64d8dbe8-256e-46b1-81da-671a6a8fc423. -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,122 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_newProject] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,122 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_newFolder] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,122 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_openProject] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,122 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_openProjectInReadOnlyMode] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,122 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_Close] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,122 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_Rename] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,122 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_saveProject] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,122 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_saveProjectAs] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_importProject] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_importProjectPlus] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_exportProject] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_exportProjectWithStillsAndLuts] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_restoreProject] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_restoreProjectPlus] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_archiveProject] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_unlockProject] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [editDelete] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [editCut] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [editCopy] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [editPaste] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_loadProjectSettingsToCurrentProject] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_projectBackups] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_projectSettings] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_dynamicProjectSwitching] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_closeProjectsInMemory] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_updateThumbnails] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_otherProjectBackups] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_refresh] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_resetUiLayout] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:51,277 | Action owner [MediaPool] is not active when refreshing shortcut [editDuplicateTimelineOrClips] due to [No focused widget is set yet] -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:01,060 | Traceback (most recent call last): -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:01,060 | File "", line 1, in -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:01,061 | AttributeError -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:01,061 | : -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:01,061 | module 'sys' has no attribute '__path__' -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:01,061 | -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:08,715 | Traceback (most recent call last): -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:08,715 | File "", line 1, in -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:08,715 | AttributeError -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:08,716 | : -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:08,716 | module 're' has no attribute '__path__' -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:08,716 | -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:21,531 | Traceback (most recent call last): -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:21,531 | File "", line 1, in -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:21,532 | TypeError -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:21,532 | : -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:21,532 | split() missing 2 required positional arguments: 'pattern' and 'string' -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:21,532 | -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:29,443 | Traceback (most recent call last): -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:29,443 | File "", line 1, in -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:29,444 | NameError -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:29,444 | : -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:29,444 | name 're__path__' is not defined -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:29,444 | -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:33,563 | Traceback (most recent call last): -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:33,563 | File "", line 1, in -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:33,564 | AttributeError -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:33,564 | : -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:33,564 | module 're' has no attribute '__path__' -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:33,564 | -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:39,740 | Traceback (most recent call last): -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:39,740 | File "", line 1, in -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:39,740 | AttributeError -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:39,740 | : -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:39,740 | module 'sys' has no attribute 'info' -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:39,740 | -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:41,372 | Traceback (most recent call last): -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:41,372 | File "", line 1, in -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:41,372 | AttributeError -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:41,372 | : -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:41,372 | module 'sys' has no attribute 'info' -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:41,372 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:50:52,078 | >>> [ openpype.hosts.resolve installed ] -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:50:52,083 | >>> [ Registering DaVinci Resovle plug-ins.. ] -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:50:52,100 | >>> [ Assigning resolve module to `pype.hosts.resolve.api.bmdvr`: Resolve (0x00007FF6BB5340D0) [App: 'Resolve' on 127.0.0.1, UUID: f0d29a26-23ed-495a-9eb5-f264f7187252] ] -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:50:52,105 | >>> [ Assigning resolve module to `pype.hosts.resolve.api.bmdvf`: FusionUI (0x0000023A629E7040) [App: 'Resolve' on 127.0.0.1, UUID: f0d29a26-23ed-495a-9eb5-f264f7187252] ] -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:50:52,110 | - { timers_manager }: [ Installing task changed callback ] -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:51:12,816 | - { openpype.pipeline.anatomy }: [ Looking for matching root in path "D:/SYNC/OPENPYPE/gazprom_screens/06_R_Home_Painter/work/Lookdev". ] -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:51:12,821 | >>> [ Found match in root "work". ] -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,952 | Traceback (most recent call last): -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,953 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\subsetmanager\window.py", line 190, in showEvent -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,953 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,953 | self.refresh() -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,953 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,953 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\subsetmanager\window.py", line 176, in refresh -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,953 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,954 | self._model.refresh() -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,954 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,954 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\subsetmanager\model.py", line 27, in refresh -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,954 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,954 | instances = list_instances() -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,954 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,954 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\hosts\resolve\api\pipeline.py", line 285, in list_instances -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,955 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,955 | selected_timeline_items = lib.get_current_timeline_items( -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,955 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,955 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\hosts\resolve\api\lib.py", line 319, in get_current_timeline_items -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,955 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,955 | selected_track_count = timeline.GetTrackCount(track_type) -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,955 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,956 | AttributeError -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,956 | : -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,956 | 'NoneType' object has no attribute 'GetTrackCount' -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,956 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,000 | Traceback (most recent call last): -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,000 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\libraryloader\app.py", line 376, in _assetschanged -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,000 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,000 | subsets_model.set_assets(asset_ids) -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,001 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,001 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\loader\model.py", line 259, in set_assets -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,001 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,001 | self.refresh() -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,001 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,001 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\loader\model.py", line 577, in refresh -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,001 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,002 | repre_ids = {con.get("representation") for con in containers} -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,002 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,002 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\loader\model.py", line 577, in -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,002 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,002 | repre_ids = {con.get("representation") for con in containers} -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,002 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,002 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\hosts\resolve\api\pipeline.py", line 138, in ls -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,003 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,003 | all_timeline_items = lib.get_current_timeline_items(filter=False) -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,003 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,003 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\hosts\resolve\api\lib.py", line 319, in get_current_timeline_items -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,003 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,003 | selected_track_count = timeline.GetTrackCount(track_type) -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,003 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,003 | AttributeError -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,004 | : -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,004 | 'NoneType' object has no attribute 'GetTrackCount' -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,004 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,731 | Traceback (most recent call last): -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,732 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\libraryloader\app.py", line 376, in _assetschanged -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,732 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,732 | subsets_model.set_assets(asset_ids) -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,732 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,732 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\loader\model.py", line 259, in set_assets -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,732 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,733 | self.refresh() -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,733 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,733 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\loader\model.py", line 577, in refresh -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,733 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,733 | repre_ids = {con.get("representation") for con in containers} -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,733 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,734 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\loader\model.py", line 577, in -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,734 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,734 | repre_ids = {con.get("representation") for con in containers} -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,734 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,734 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\hosts\resolve\api\pipeline.py", line 138, in ls -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,734 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,734 | all_timeline_items = lib.get_current_timeline_items(filter=False) -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,735 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,735 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\hosts\resolve\api\lib.py", line 319, in get_current_timeline_items -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,735 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,735 | selected_track_count = timeline.GetTrackCount(track_type) -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,735 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,735 | AttributeError -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,735 | : -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,735 | 'NoneType' object has no attribute 'GetTrackCount' -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,736 | -[0x00004fe8] | UI | WARN | 2023-05-17 09:53:56,329 | UI Persistence - MediaPoolFloatingWindowGeometry not read -[0x00004fe8] | UI | WARN | 2023-05-17 09:53:56,329 | UI Persistence - ConformEdlEffectsLibrary not read -[0x000065b4] | GPU.SingleBoardMgr | INFO | 2023-05-17 09:53:56,766 | Flushing GPU memory... -[0x000065b4] | UI.GLContext | INFO | 2023-05-17 09:53:56,766 | Creating shared OpenGL context for this thread (5 total). -[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:53:56,782 | Main view page is changed to 2 -[0x000065b4] | UI.GLTexPool | INFO | 2023-05-17 09:53:56,884 | Released 0 MiB in 0 unused textures. -[0x00004fe8] | UI | INFO | 2023-05-17 09:53:56,944 | PBO is initialized with size [1920x1080], bitDepth=[8], hasAlpha=[1]. -[0x00004fe8] | UI | WARN | 2023-05-17 09:53:59,491 | Failed to find value '1' in combo-box -[0x00004fe8] | UI | WARN | 2023-05-17 09:53:59,491 | Failed to find value '2' in combo-box -[0x00004fe8] | UI | WARN | 2023-05-17 09:53:59,491 | Failed to find value '2' in combo-box -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:53:59,548 | Action owner [MediaPool] is not active when refreshing shortcut [editDuplicateTimelineOrClips] due to [No focused widget is set yet] -[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:54:01,297 | Current timeline pointer (Timeline 1) is changed -[0x00004fe8] | UI | ERROR | 2023-05-17 09:54:01,405 | No marker found at frame -90000 to delete. -[0x00004fe8] | UI | ERROR | 2023-05-17 09:54:01,405 | No marker found at frame -90000 to delete. -[0x00004fe8] | UI | ERROR | 2023-05-17 09:54:01,405 | No marker found at frame -90000 to delete. -[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:01,495 | Start saving project gazprom_screens_06_R_Home_Painter_Lookdev_v002 -[0x000083dc] | SyManager.ActionExecutor | INFO | 2023-05-17 09:54:01,526 | Database transaction is ongoing, user initiated action SmActIOCPSigInvoker is postponed -[0x000083dc] | SyManager.ActionExecutor | INFO | 2023-05-17 09:54:01,550 | Database transaction is ongoing, user initiated action SmActIOCPSigInvoker is postponed -[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:01,592 | Saving project took 97 ms -[0x00004fe8] | SyManager.ActionExecutor | INFO | 2023-05-17 09:54:01,619 | Database transaction completed, enqueueing 2 postponed actions -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:54:23,357 | >>> [ openpype.hosts.resolve installed ] -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:54:23,363 | >>> [ Registering DaVinci Resovle plug-ins.. ] -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:54:23,375 | >>> [ Assigning resolve module to `pype.hosts.resolve.api.bmdvr`: Resolve (0x00007FF6BB5340D0) [App: 'Resolve' on 127.0.0.1, UUID: f0d29a26-23ed-495a-9eb5-f264f7187252] ] -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:54:23,380 | >>> [ Assigning resolve module to `pype.hosts.resolve.api.bmdvf`: FusionUI (0x0000023A629E7040) [App: 'Resolve' on 127.0.0.1, UUID: f0d29a26-23ed-495a-9eb5-f264f7187252] ] -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:54:23,386 | - { timers_manager }: [ Installing task changed callback ] -[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:39,823 | Start saving project gazprom_screens_06_R_Home_Painter_Lookdev_v002 -[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:39,826 | Saving project took 2 ms -[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:39,920 | Start saving project gazprom_screens_06_R_Home_Painter_Lookdev_v002 -[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:39,923 | Saving project took 3 ms -[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:40,137 | Start saving project gazprom_screens_06_R_Home_Painter_Lookdev_v002 -[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:40,321 | Saving project took 183 ms -[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:54:40,326 | Main view page is changed to 2 -[0x00004fe8] | BtCommon | WARN | 2023-05-17 09:54:40,570 | Negative duration -[0x00004fe8] | UI | WARN | 2023-05-17 09:54:40,638 | Unable to submit frame to GPU scopes, legacy OpenGL uploads not supported (Player Model). -[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:42,497 | Start saving project gazprom_screens_06_R_Home_Painter_Lookdev_v002 -[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:42,555 | Saving project took 59 ms From 8b6feb38ab7a6d71b82b27dce0c9a807cff291ed Mon Sep 17 00:00:00 2001 From: Alexey Bogomolov Date: Wed, 17 May 2023 10:55:41 +0300 Subject: [PATCH 283/360] Bring the PATH setting back --- openpype/hosts/resolve/hooks/pre_resolve_setup.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/openpype/hosts/resolve/hooks/pre_resolve_setup.py b/openpype/hosts/resolve/hooks/pre_resolve_setup.py index 486d8121cf..549777c34e 100644 --- a/openpype/hosts/resolve/hooks/pre_resolve_setup.py +++ b/openpype/hosts/resolve/hooks/pre_resolve_setup.py @@ -84,6 +84,15 @@ class ResolvePrelaunch(PreLaunchHook): self.log.debug(f"PYTHONPATH: {self.launch_context.env['PYTHONPATH']}") + # add to the python path to PATH + env_path = self.launch_context.env["PATH"] + self.log.info(f"Adding `{python3_home_str}` to the PATH variable") + self.launch_context.env[ + "PATH" + ] = f"{python3_home_str}{os.pathsep}{env_path}" + + self.log.debug(f"PATH: {self.launch_context.env['PATH']}") + resolve_utility_scripts_dirs = { "windows": ( f"{programdata}/Blackmagic Design" From 05a25bb30b8770aad253fe9ecbe3609205c09923 Mon Sep 17 00:00:00 2001 From: Alexey Bogomolov Date: Wed, 17 May 2023 11:46:06 +0300 Subject: [PATCH 284/360] add docstrings --- .../hosts/resolve/hooks/pre_resolve_setup.py | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/resolve/hooks/pre_resolve_setup.py b/openpype/hosts/resolve/hooks/pre_resolve_setup.py index 549777c34e..2cdeb8c4ff 100644 --- a/openpype/hosts/resolve/hooks/pre_resolve_setup.py +++ b/openpype/hosts/resolve/hooks/pre_resolve_setup.py @@ -7,10 +7,27 @@ from openpype.hosts.resolve.utils import setup class ResolvePrelaunch(PreLaunchHook): """ - This hook will check if current workfile path has Resolve - project inside. IF not, it initialize it and finally it pass - path to the project by environment variable to Premiere launcher - shell script. + This hook will set up the Resolve scripting environment as described in + Resolve's documentation found with the installed application at + {resolve}/Support/Developer/Scripting/README.txt + + Prepares the following environment variables: + - `RESOLVE_SCRIPT_API` + - `RESOLVE_SCRIPT_LIB` + + It adds $RESOLVE_SCRIPT_API/Modules to PYTHONPATH. + + Additionally it sets up the Python home for Python 3 based on the + RESOLVE_PYTHON3_HOME in the environment (usually defined in OpenPype's + Application environment for Resolve by the admin). For this it sets + PYTHONHOME and PATH variables. + + It also defines: + - `RESOLVE_UTILITY_SCRIPTS_DIR`: Destination directory for OpenPype + Fusion scripts to be copied to for Resolve to pick them up. + - `OPENPYPE_LOG_NO_COLORS` to True to ensure OP doesn't try to + use logging with terminal colors as it fails in Resolve. + """ app_groups = ["resolve"] From a5fde9663805326f1fea5de5b278e4f429d85e1a Mon Sep 17 00:00:00 2001 From: Alexey Bogomolov Date: Wed, 17 May 2023 11:47:08 +0300 Subject: [PATCH 285/360] formatting --- openpype/hosts/resolve/hooks/pre_resolve_setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/resolve/hooks/pre_resolve_setup.py b/openpype/hosts/resolve/hooks/pre_resolve_setup.py index 2cdeb8c4ff..6747e773a3 100644 --- a/openpype/hosts/resolve/hooks/pre_resolve_setup.py +++ b/openpype/hosts/resolve/hooks/pre_resolve_setup.py @@ -26,7 +26,7 @@ class ResolvePrelaunch(PreLaunchHook): - `RESOLVE_UTILITY_SCRIPTS_DIR`: Destination directory for OpenPype Fusion scripts to be copied to for Resolve to pick them up. - `OPENPYPE_LOG_NO_COLORS` to True to ensure OP doesn't try to - use logging with terminal colors as it fails in Resolve. + use logging with terminal colors as it fails in Resolve. """ From 9112b87ba6ffc095fd43d5a769ce3501ef85e765 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 17 May 2023 12:20:43 +0200 Subject: [PATCH 286/360] :truck: move unreal plugin to separate repository --- openpype/hosts/unreal/integration/README.md | 10 - .../integration/UE_4.27/Ayon/.gitignore | 35 --- .../integration/UE_4.27/Ayon/Ayon.uplugin | 23 -- .../Ayon/Config/DefaultAyonSettings.ini | 2 - .../UE_4.27/Ayon/Config/FilterPlugin.ini | 8 - .../Ayon/Content/Python/init_unreal.py | 30 --- .../unreal/integration/UE_4.27/Ayon/README.md | 3 - .../UE_4.27/Ayon/Resources/ayon128.png | Bin 2358 -> 0 bytes .../UE_4.27/Ayon/Resources/ayon40.png | Bin 721 -> 0 bytes .../UE_4.27/Ayon/Resources/ayon512.png | Bin 16705 -> 0 bytes .../UE_4.27/Ayon/Source/Ayon/Ayon.Build.cs | 61 ------ .../UE_4.27/Ayon/Source/Ayon/Private/Ayon.cpp | 156 -------------- .../Ayon/Private/AyonAssetContainer.cpp | 114 ---------- .../Private/AyonAssetContainerFactory.cpp | 20 -- .../Ayon/Source/Ayon/Private/AyonLib.cpp | 53 ----- .../Ayon/Private/AyonPublishInstance.cpp | 203 ----------------- .../Private/AyonPublishInstanceFactory.cpp | 23 -- .../Source/Ayon/Private/AyonPythonBridge.cpp | 14 -- .../Ayon/Source/Ayon/Private/AyonSettings.cpp | 20 -- .../Ayon/Source/Ayon/Private/AyonStyle.cpp | 70 ------ .../Private/Commandlets/AyonActionResult.cpp | 41 ---- .../AyonGenerateProjectCommandlet.cpp | 141 ------------ .../Ayon/Private/OpenPypePublishInstance.cpp | 203 ----------------- .../UE_4.27/Ayon/Source/Ayon/Public/Ayon.h | 20 -- .../Source/Ayon/Public/AyonAssetContainer.h | 39 ---- .../Ayon/Public/AyonAssetContainerFactory.h | 21 -- .../Ayon/Source/Ayon/Public/AyonConstants.h | 15 -- .../UE_4.27/Ayon/Source/Ayon/Public/AyonLib.h | 19 -- .../Source/Ayon/Public/AyonPublishInstance.h | 103 --------- .../Ayon/Public/AyonPublishInstanceFactory.h | 22 -- .../Source/Ayon/Public/AyonPythonBridge.h | 20 -- .../Ayon/Source/Ayon/Public/AyonSettings.h | 31 --- .../Ayon/Source/Ayon/Public/AyonStyle.h | 23 -- .../Public/Commandlets/AyonActionResult.h | 83 ------- .../AyonGenerateProjectCommandlet.h | 60 ------ .../Source/Ayon/Public/Logging/Ayon_Log.h | 4 - .../Ayon/Public/OpenPypePublishInstance.h | 103 --------- .../integration/UE_4.27/BuildPlugin_4-27.bat | 1 - .../UE_4.27/BuildPlugin_4-27_Window.bat | 1 - .../UE_4.27/CommandletProject/.gitignore | 8 - .../CommandletProject.uproject | 12 -- .../unreal/integration/UE_5.0/Ayon/.gitignore | 35 --- .../integration/UE_5.0/Ayon/Ayon.uplugin | 24 --- .../Ayon/Config/DefaultAyonSettings.ini | 2 - .../UE_5.0/Ayon/Config/FilterPlugin.ini | 8 - .../UE_5.0/Ayon/Content/Python/init_unreal.py | 30 --- .../unreal/integration/UE_5.0/Ayon/README.md | 3 - .../UE_5.0/Ayon/Resources/ayon128.png | Bin 2358 -> 0 bytes .../UE_5.0/Ayon/Resources/ayon40.png | Bin 721 -> 0 bytes .../UE_5.0/Ayon/Resources/ayon512.png | Bin 16705 -> 0 bytes .../UE_5.0/Ayon/Source/Ayon/Ayon.Build.cs | 65 ------ .../UE_5.0/Ayon/Source/Ayon/Private/Ayon.cpp | 139 ------------ .../Ayon/Private/AyonAssetContainer.cpp | 113 ---------- .../Private/AyonAssetContainerFactory.cpp | 20 -- .../Ayon/Source/Ayon/Private/AyonCommands.cpp | 13 -- .../Ayon/Source/Ayon/Private/AyonLib.cpp | 51 ----- .../Ayon/Private/AyonPublishInstance.cpp | 204 ------------------ .../Private/AyonPublishInstanceFactory.cpp | 23 -- .../Source/Ayon/Private/AyonPythonBridge.cpp | 14 -- .../Ayon/Source/Ayon/Private/AyonSettings.cpp | 21 -- .../Ayon/Source/Ayon/Private/AyonStyle.cpp | 62 ------ .../Private/Commandlets/AyonActionResult.cpp | 40 ---- .../AyonGenerateProjectCommandlet.cpp | 140 ------------ .../Ayon/Private/OpenPypePublishInstance.cpp | 204 ------------------ .../UE_5.0/Ayon/Source/Ayon/Public/Ayon.h | 24 --- .../Source/Ayon/Public/AyonAssetContainer.h | 34 --- .../Ayon/Public/AyonAssetContainerFactory.h | 18 -- .../Ayon/Source/Ayon/Public/AyonCommands.h | 24 --- .../Ayon/Source/Ayon/Public/AyonConstants.h | 13 -- .../UE_5.0/Ayon/Source/Ayon/Public/AyonLib.h | 19 -- .../Source/Ayon/Public/AyonPublishInstance.h | 104 --------- .../Ayon/Public/AyonPublishInstanceFactory.h | 22 -- .../Source/Ayon/Public/AyonPythonBridge.h | 20 -- .../Ayon/Source/Ayon/Public/AyonSettings.h | 32 --- .../Ayon/Source/Ayon/Public/AyonStyle.h | 19 -- .../Public/Commandlets/AyonActionResult.h | 83 ------- .../AyonGenerateProjectCommandlet.h | 61 ------ .../Source/Ayon/Public/Logging/Ayon_Log.h | 4 - .../Ayon/Public/OpenPypePublishInstance.h | 104 --------- .../integration/UE_5.0/BuildPlugin_5-0.bat | 1 - .../UE_5.0/BuildPlugin_5-0_Window.bat | 1 - .../UE_5.0/CommandletProject/.gitignore | 41 ---- .../CommandletProject.uproject | 20 -- .../unreal/integration/UE_5.1/Ayon/.gitignore | 35 --- .../integration/UE_5.1/Ayon/Ayon.uplugin | 24 --- .../Ayon/Config/DefaultAyonSettings.ini | 2 - .../UE_5.1/Ayon/Config/FilterPlugin.ini | 8 - .../UE_5.1/Ayon/Content/Python/init_unreal.py | 30 --- .../unreal/integration/UE_5.1/Ayon/README.md | 3 - .../UE_5.1/Ayon/Resources/ayon128.png | Bin 2358 -> 0 bytes .../UE_5.1/Ayon/Resources/ayon40.png | Bin 721 -> 0 bytes .../UE_5.1/Ayon/Resources/ayon512.png | Bin 16705 -> 0 bytes .../UE_5.1/Ayon/Source/Ayon/Ayon.Build.cs | 65 ------ .../UE_5.1/Ayon/Source/Ayon/Private/Ayon.cpp | 139 ------------ .../Ayon/Private/AyonAssetContainer.cpp | 113 ---------- .../Private/AyonAssetContainerFactory.cpp | 20 -- .../Ayon/Source/Ayon/Private/AyonCommands.cpp | 13 -- .../Ayon/Source/Ayon/Private/AyonLib.cpp | 51 ----- .../Ayon/Private/AyonPublishInstance.cpp | 204 ------------------ .../Private/AyonPublishInstanceFactory.cpp | 23 -- .../Source/Ayon/Private/AyonPythonBridge.cpp | 14 -- .../Ayon/Source/Ayon/Private/AyonSettings.cpp | 21 -- .../Ayon/Source/Ayon/Private/AyonStyle.cpp | 62 ------ .../Private/Commandlets/AyonActionResult.cpp | 40 ---- .../AyonGenerateProjectCommandlet.cpp | 140 ------------ .../Ayon/Private/OpenPypePublishInstance.cpp | 204 ------------------ .../UE_5.1/Ayon/Source/Ayon/Public/Ayon.h | 24 --- .../Source/Ayon/Public/AyonAssetContainer.h | 34 --- .../Ayon/Public/AyonAssetContainerFactory.h | 18 -- .../Ayon/Source/Ayon/Public/AyonCommands.h | 24 --- .../Ayon/Source/Ayon/Public/AyonConstants.h | 13 -- .../UE_5.1/Ayon/Source/Ayon/Public/AyonLib.h | 19 -- .../Source/Ayon/Public/AyonPublishInstance.h | 104 --------- .../Ayon/Public/AyonPublishInstanceFactory.h | 22 -- .../Source/Ayon/Public/AyonPythonBridge.h | 20 -- .../Ayon/Source/Ayon/Public/AyonSettings.h | 32 --- .../Ayon/Source/Ayon/Public/AyonStyle.h | 19 -- .../Public/Commandlets/AyonActionResult.h | 83 ------- .../AyonGenerateProjectCommandlet.h | 61 ------ .../Source/Ayon/Public/Logging/Ayon_Log.h | 4 - .../Ayon/Public/OpenPypePublishInstance.h | 104 --------- .../integration/UE_5.1/BuildPlugin_5-1.bat | 1 - .../UE_5.1/BuildPlugin_5-1_Window.bat | 1 - .../UE_5.1/CommandletProject/.gitignore | 41 ---- .../CommandletProject.uproject | 20 -- 125 files changed, 5525 deletions(-) delete mode 100644 openpype/hosts/unreal/integration/README.md delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/.gitignore delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Ayon.uplugin delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Config/DefaultAyonSettings.ini delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Config/FilterPlugin.ini delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Content/Python/init_unreal.py delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/README.md delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Resources/ayon128.png delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Resources/ayon40.png delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Resources/ayon512.png delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Ayon.Build.cs delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/Ayon.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonAssetContainer.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonAssetContainerFactory.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonLib.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonPublishInstance.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonPublishInstanceFactory.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonPythonBridge.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonSettings.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonStyle.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/Commandlets/AyonActionResult.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/Ayon.h delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonAssetContainer.h delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonAssetContainerFactory.h delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonConstants.h delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonLib.h delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonPublishInstance.h delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonPublishInstanceFactory.h delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonPythonBridge.h delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonSettings.h delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonStyle.h delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/Commandlets/AyonActionResult.h delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/Logging/Ayon_Log.h delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/BuildPlugin_4-27.bat delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/BuildPlugin_4-27_Window.bat delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/CommandletProject/.gitignore delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/CommandletProject/CommandletProject.uproject delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/.gitignore delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Ayon.uplugin delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Config/DefaultAyonSettings.ini delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Config/FilterPlugin.ini delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Content/Python/init_unreal.py delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/README.md delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Resources/ayon128.png delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Resources/ayon40.png delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Resources/ayon512.png delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Ayon.Build.cs delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/Ayon.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonAssetContainer.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonAssetContainerFactory.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonCommands.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonLib.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonPublishInstance.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonPublishInstanceFactory.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonPythonBridge.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonSettings.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonStyle.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/Commandlets/AyonActionResult.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/Ayon.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonAssetContainer.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonAssetContainerFactory.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonCommands.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonConstants.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonLib.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonPublishInstance.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonPublishInstanceFactory.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonPythonBridge.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonSettings.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonStyle.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/Commandlets/AyonActionResult.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/Logging/Ayon_Log.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/BuildPlugin_5-0.bat delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/BuildPlugin_5-0_Window.bat delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/CommandletProject/.gitignore delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/CommandletProject/CommandletProject.uproject delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/.gitignore delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Ayon.uplugin delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Config/DefaultAyonSettings.ini delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Config/FilterPlugin.ini delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Content/Python/init_unreal.py delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/README.md delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Resources/ayon128.png delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Resources/ayon40.png delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Resources/ayon512.png delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Ayon.Build.cs delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/Ayon.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonAssetContainer.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonAssetContainerFactory.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonCommands.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonLib.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonPublishInstance.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonPublishInstanceFactory.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonPythonBridge.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonSettings.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonStyle.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/Commandlets/AyonActionResult.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Ayon.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonAssetContainer.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonAssetContainerFactory.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonCommands.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonConstants.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonLib.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonPublishInstance.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonPublishInstanceFactory.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonPythonBridge.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonSettings.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonStyle.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Commandlets/AyonActionResult.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Logging/Ayon_Log.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/BuildPlugin_5-1.bat delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/BuildPlugin_5-1_Window.bat delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/CommandletProject/.gitignore delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/CommandletProject/CommandletProject.uproject diff --git a/openpype/hosts/unreal/integration/README.md b/openpype/hosts/unreal/integration/README.md deleted file mode 100644 index 961eea83e6..0000000000 --- a/openpype/hosts/unreal/integration/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# Building the plugin - -In order to successfully build the plugin, make sure that the path to the UnrealBuildTool.exe is specified correctly. -After the UBT path specify for which platform it will be compiled. in the -Project parameter, specify the path to the -CommandletProject.uproject file. Next the build type has to be specified (DebugGame, Development, Package, etc.) and then the -TargetType (Editor, Runtime, etc.) - -`BuildPlugin_[Ver].bat` runs the building process in the background. If you want to show the progress inside the -command prompt, use the `BuildPlugin_[Ver]_Window.bat` file. - - diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/.gitignore b/openpype/hosts/unreal/integration/UE_4.27/Ayon/.gitignore deleted file mode 100644 index b32a6f55e5..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/.gitignore +++ /dev/null @@ -1,35 +0,0 @@ -# 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_4.27/Ayon/Ayon.uplugin b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Ayon.uplugin deleted file mode 100644 index 0838da5577..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Ayon.uplugin +++ /dev/null @@ -1,23 +0,0 @@ -{ - "FileVersion": 3, - "Version": 1, - "VersionName": "1.0", - "FriendlyName": "Ayon", - "Description": "Ayon Integration", - "Category": "Ayon.Integration", - "CreatedBy": "Ondrej Samohel", - "CreatedByURL": "https://ayon.ynput.io", - "DocsURL": "https://ayon.ynput.io/docs/artist_hosts_unreal", - "MarketplaceURL": "", - "SupportURL": "https://ynput.io/", - "EngineVersion": "4.27", - "CanContainContent": true, - "Installed": true, - "Modules": [ - { - "Name": "Ayon", - "Type": "Editor", - "LoadingPhase": "Default" - } - ] -} diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Config/DefaultAyonSettings.ini b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Config/DefaultAyonSettings.ini deleted file mode 100644 index 9ad7f55201..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Config/DefaultAyonSettings.ini +++ /dev/null @@ -1,2 +0,0 @@ -[/Script/Ayon.AyonSettings] -FolderColor=(R=91,G=197,B=220,A=255) \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Config/FilterPlugin.ini b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Config/FilterPlugin.ini deleted file mode 100644 index ccebca2f32..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Config/FilterPlugin.ini +++ /dev/null @@ -1,8 +0,0 @@ -[FilterPlugin] -; This section lists additional files which will be packaged along with your plugin. Paths should be listed relative to the root plugin directory, and -; may include "...", "*", and "?" wildcards to match directories, files, and individual characters respectively. -; -; Examples: -; /README.txt -; /Extras/... -; /Binaries/ThirdParty/*.dll diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Content/Python/init_unreal.py b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Content/Python/init_unreal.py deleted file mode 100644 index 43d6b8b7cf..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Content/Python/init_unreal.py +++ /dev/null @@ -1,30 +0,0 @@ -import unreal - -ayon_detected = True -try: - from openpype.pipeline import install_host - from openpype.hosts.unreal.api import UnrealHost - - ayon_host = UnrealHost() -except ImportError as exc: - ayon_host = None - ayon_detected = False - unreal.log_error(f"OpenPype: cannot load Ayon [ {exc} ]") - -if ayon_detected: - install_host(ayon_host) - - -@unreal.uclass() -class AyonIntegration(unreal.AyonPythonBridge): - @unreal.ufunction(override=True) - def RunInPython_Popup(self): - unreal.log_warning("Ayon: showing tools popup") - if ayon_detected: - ayon_host.show_tools_popup() - - @unreal.ufunction(override=True) - def RunInPython_Dialog(self): - unreal.log_warning("Ayon: showing tools dialog") - if ayon_detected: - ayon_host.show_tools_dialog() diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/README.md b/openpype/hosts/unreal/integration/UE_4.27/Ayon/README.md deleted file mode 100644 index 77ae8c7e98..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Ayon Unreal Integration plugin - UE 4.x - -This is plugin for Unreal Editor, creating menu for [Ayon](https://github.com/ynput/OpenPype) tools to run. diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Resources/ayon128.png b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Resources/ayon128.png deleted file mode 100644 index 799d849aa3163ecb16be39c641a6ac30324906b9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2358 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 D;gw7~ diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Resources/ayon512.png b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Resources/ayon512.png deleted file mode 100644 index 990d5917e232a0644820428fb2790943de5ffaa4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16705 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"LevelEditor"); - - TSharedPtr MenuExtender = MakeShareable(new FExtender()); - TSharedPtr ToolbarExtender = MakeShareable(new FExtender()); - - MenuExtender->AddMenuExtension( - "LevelEditor", - EExtensionHook::After, - NULL, - FMenuExtensionDelegate::CreateRaw(this, &FAyonModule::AddMenuEntry) - ); - ToolbarExtender->AddToolBarExtension( - "Settings", - EExtensionHook::After, - NULL, - FToolBarExtensionDelegate::CreateRaw(this, &FAyonModule::AddToobarEntry)); - - - LevelEditorModule.GetMenuExtensibilityManager()->AddExtender(MenuExtender); - LevelEditorModule.GetToolBarExtensibilityManager()->AddExtender(ToolbarExtender); - - RegisterSettings(); - } -} - -void FAyonModule::ShutdownModule() -{ - FAyonStyle::Shutdown(); -} - - -void FAyonModule::AddMenuEntry(FMenuBuilder& MenuBuilder) -{ - // Create Section - MenuBuilder.BeginSection("Ayon", TAttribute(FText::FromString("Ayon"))); - { - // Create a Submenu inside of the Section - MenuBuilder.AddMenuEntry( - FText::FromString("Tools..."), - FText::FromString("Pipeline tools"), - FSlateIcon(FAyonStyle::GetStyleSetName(), "Ayon.Logo"), - FUIAction(FExecuteAction::CreateRaw(this, &FAyonModule::MenuPopup)) - ); - - MenuBuilder.AddMenuEntry( - FText::FromString("Tools dialog..."), - FText::FromString("Pipeline tools dialog"), - FSlateIcon(FAyonStyle::GetStyleSetName(), "Ayon.Logo"), - FUIAction(FExecuteAction::CreateRaw(this, &FAyonModule::MenuDialog)) - ); - } - MenuBuilder.EndSection(); -} - -void FAyonModule::AddToobarEntry(FToolBarBuilder& ToolbarBuilder) -{ - ToolbarBuilder.BeginSection(TEXT("Ayon")); - { - ToolbarBuilder.AddToolBarButton( - FUIAction( - FExecuteAction::CreateRaw(this, &FAyonModule::MenuPopup), - NULL, - FIsActionChecked() - - ), - NAME_None, - LOCTEXT("Ayon_label", "Ayon"), - LOCTEXT("Ayon_tooltip", "Ayon Tools"), - FSlateIcon(FAyonStyle::GetStyleSetName(), "Ayon.Logo") - ); - } - ToolbarBuilder.EndSection(); -} - -void FAyonModule::RegisterSettings() -{ - ISettingsModule& SettingsModule = FModuleManager::LoadModuleChecked("Settings"); - - // Create the new category - // TODO: After the movement of the plugin from the game to editor, it might be necessary to move this! - ISettingsContainerPtr SettingsContainer = SettingsModule.GetContainer("Project"); - - UAyonSettings* Settings = GetMutableDefault(); - - // Register the settings - ISettingsSectionPtr SettingsSection = SettingsModule.RegisterSettings("Project", "Ayon", "General", - LOCTEXT("RuntimeGeneralSettingsName", - "General"), - LOCTEXT("RuntimeGeneralSettingsDescription", - "Base configuration for Open Pype Module"), - Settings - ); - - // Register the save handler to your settings, you might want to use it to - // validate those or just act to settings changes. - if (SettingsSection.IsValid()) - { - SettingsSection->OnModified().BindRaw(this, &FAyonModule::HandleSettingsSaved); - } -} - -bool FAyonModule::HandleSettingsSaved() -{ - UAyonSettings* Settings = GetMutableDefault(); - bool ResaveSettings = false; - - // You can put any validation code in here and resave the settings in case an invalid - // value has been entered - - if (ResaveSettings) - { - Settings->SaveConfig(); - } - - return true; -} - - -void FAyonModule::MenuPopup() -{ - UAyonPythonBridge* bridge = UAyonPythonBridge::Get(); - bridge->RunInPython_Popup(); -} - -void FAyonModule::MenuDialog() -{ - UAyonPythonBridge* bridge = UAyonPythonBridge::Get(); - bridge->RunInPython_Dialog(); -} - -IMPLEMENT_MODULE(FAyonModule, Ayon) diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonAssetContainer.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonAssetContainer.cpp deleted file mode 100644 index e3989eb03c..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonAssetContainer.cpp +++ /dev/null @@ -1,114 +0,0 @@ -// Fill out your copyright notice in the Description page of Project Settings. - -#include "AyonAssetContainer.h" -#include "AssetRegistryModule.h" -#include "Misc/PackageName.h" -#include "Containers/UnrealString.h" - -UAyonAssetContainer::UAyonAssetContainer(const FObjectInitializer& ObjectInitializer) -: UAssetUserData(ObjectInitializer) -{ - FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); - FString path = UAyonAssetContainer::GetPathName(); - UE_LOG(LogTemp, Warning, TEXT("UAyonAssetContainer %s"), *path); - FARFilter Filter; - Filter.PackagePaths.Add(FName(*path)); - - AssetRegistryModule.Get().OnAssetAdded().AddUObject(this, &UAyonAssetContainer::OnAssetAdded); - AssetRegistryModule.Get().OnAssetRemoved().AddUObject(this, &UAyonAssetContainer::OnAssetRemoved); - AssetRegistryModule.Get().OnAssetRenamed().AddUObject(this, &UAyonAssetContainer::OnAssetRenamed); -} - -void UAyonAssetContainer::OnAssetAdded(const FAssetData& AssetData) -{ - TArray split; - - // get directory of current container - FString selfFullPath = UAyonAssetContainer::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 != "AyonAssetContainer") - { - assets.Add(assetPath); - assetsData.Add(AssetData); - UE_LOG(LogTemp, Log, TEXT("%s: asset added to %s"), *selfFullPath, *selfDir); - } - } -} - -void UAyonAssetContainer::OnAssetRemoved(const FAssetData& AssetData) -{ - TArray split; - - // get directory of current container - FString selfFullPath = UAyonAssetContainer::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 = UAyonAssetContainer::GetPathName(); - FString lpp = FPackageName::GetLongPackagePath(*path); - - if (assetDir.StartsWith(*selfDir)) - { - // exclude self - if (assetFName != "AyonAssetContainer") - { - // UE_LOG(LogTemp, Warning, TEXT("%s: asset removed"), *lpp); - assets.Remove(assetPath); - assetsData.Remove(AssetData); - } - } -} - -void UAyonAssetContainer::OnAssetRenamed(const FAssetData& AssetData, const FString& str) -{ - TArray split; - - // get directory of current container - FString selfFullPath = UAyonAssetContainer::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 != "AyonAssetContainer") - { - - 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_4.27/Ayon/Source/Ayon/Private/AyonAssetContainerFactory.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonAssetContainerFactory.cpp deleted file mode 100644 index 086fc1036e..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonAssetContainerFactory.cpp +++ /dev/null @@ -1,20 +0,0 @@ -#include "AyonAssetContainerFactory.h" -#include "AyonAssetContainer.h" - -UAyonAssetContainerFactory::UAyonAssetContainerFactory(const FObjectInitializer& ObjectInitializer) - : UFactory(ObjectInitializer) -{ - SupportedClass = UAyonAssetContainer::StaticClass(); - bCreateNew = false; - bEditorImport = true; -} - -UObject* UAyonAssetContainerFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) -{ - UAyonAssetContainer* AssetContainer = NewObject(InParent, Class, Name, Flags); - return AssetContainer; -} - -bool UAyonAssetContainerFactory::ShouldShowInNewMenu() const { - return false; -} diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonLib.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonLib.cpp deleted file mode 100644 index bff99caee3..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonLib.cpp +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#include "AyonLib.h" - -#include "AssetViewUtils.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? - */ - -bool UAyonLib::SetFolderColor(const FString& FolderPath, const FLinearColor& FolderColor, const bool& bForceAdd) -{ - if (AssetViewUtils::DoesFolderExist(FolderPath)) - { - const TSharedPtr LinearColor = MakeShared(FolderColor); - - AssetViewUtils::SaveColor(FolderPath, LinearColor, true); - UE_LOG(LogAssetData, Display, TEXT("A color {%s} has been set to folder \"%s\""), *LinearColor->ToString(), - *FolderPath) - return true; - } - - UE_LOG(LogAssetData, Display, TEXT("Setting a color {%s} to folder \"%s\" has failed! Directory doesn't exist!"), - *FolderColor.ToString(), *FolderPath) - return false; -} - -/** - * Returns all poperties on given object - * @param cls - class - * @return TArray of properties - */ -TArray UAyonLib::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_4.27/Ayon/Source/Ayon/Private/AyonPublishInstance.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonPublishInstance.cpp deleted file mode 100644 index d7550e2ed1..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonPublishInstance.cpp +++ /dev/null @@ -1,203 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -// Deprecation warning: this is left here just for backwards compatibility -// and will be removed in next versions of Ayon. -#pragma once - -#include "AyonPublishInstance.h" -#include "AssetRegistryModule.h" -#include "AyonLib.h" -#include "AyonSettings.h" -#include "Framework/Notifications/NotificationManager.h" -#include "Widgets/Notifications/SNotificationList.h" - -//Moves all the invalid pointers to the end to prepare them for the shrinking -#define REMOVE_INVALID_ENTRIES(VAR) VAR.CompactStable(); \ - VAR.Shrink(); - -UAyonPublishInstance::UAyonPublishInstance(const FObjectInitializer& ObjectInitializer) - : UPrimaryDataAsset(ObjectInitializer) -{ - const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked< - FAssetRegistryModule>("AssetRegistry"); - - const FPropertyEditorModule& PropertyEditorModule = FModuleManager::LoadModuleChecked( - "PropertyEditor"); - - FString Left, Right; - GetPathName().Split("/" + GetName(), &Left, &Right); - - FARFilter Filter; - Filter.PackagePaths.Emplace(FName(Left)); - - TArray FoundAssets; - AssetRegistryModule.GetRegistry().GetAssets(Filter, FoundAssets); - - for (const FAssetData& AssetData : FoundAssets) - OnAssetCreated(AssetData); - - REMOVE_INVALID_ENTRIES(AssetDataInternal) - REMOVE_INVALID_ENTRIES(AssetDataExternal) - - AssetRegistryModule.Get().OnAssetAdded().AddUObject(this, &UAyonPublishInstance::OnAssetCreated); - AssetRegistryModule.Get().OnAssetRemoved().AddUObject(this, &UAyonPublishInstance::OnAssetRemoved); - AssetRegistryModule.Get().OnAssetUpdated().AddUObject(this, &UAyonPublishInstance::OnAssetUpdated); - -#ifdef WITH_EDITOR - ColorAyonDirs(); -#endif - -} - -void UAyonPublishInstance::OnAssetCreated(const FAssetData& InAssetData) -{ - TArray split; - - UObject* Asset = InAssetData.GetAsset(); - - if (!IsValid(Asset)) - { - UE_LOG(LogAssetData, Warning, TEXT("Asset \"%s\" is not valid! Skipping the addition."), - *InAssetData.ObjectPath.ToString()); - return; - } - - const bool result = IsUnderSameDir(Asset) && Cast(Asset) == nullptr; - - if (result) - { - if (AssetDataInternal.Emplace(Asset).IsValidId()) - { - UE_LOG(LogTemp, Log, TEXT("Added an Asset to PublishInstance - Publish Instance: %s, Asset %s"), - *this->GetName(), *Asset->GetName()); - } - } -} - -void UAyonPublishInstance::OnAssetRemoved(const FAssetData& InAssetData) -{ - if (Cast(InAssetData.GetAsset()) == nullptr) - { - if (AssetDataInternal.Contains(nullptr)) - { - AssetDataInternal.Remove(nullptr); - REMOVE_INVALID_ENTRIES(AssetDataInternal) - } - else - { - AssetDataExternal.Remove(nullptr); - REMOVE_INVALID_ENTRIES(AssetDataExternal) - } - } -} - -void UAyonPublishInstance::OnAssetUpdated(const FAssetData& InAssetData) -{ - REMOVE_INVALID_ENTRIES(AssetDataInternal); - REMOVE_INVALID_ENTRIES(AssetDataExternal); -} - -bool UAyonPublishInstance::IsUnderSameDir(const UObject* InAsset) const -{ - FString ThisLeft, ThisRight; - this->GetPathName().Split(this->GetName(), &ThisLeft, &ThisRight); - - return InAsset->GetPathName().StartsWith(ThisLeft); -} - -#ifdef WITH_EDITOR - -void UAyonPublishInstance::ColorAyonDirs() -{ - FString PathName = this->GetPathName(); - - //Check whether the path contains the defined Ayon folder - if (!PathName.Contains(TEXT("Ayon"))) return; - - //Get the base path for open pype - FString PathLeft, PathRight; - PathName.Split(FString("Ayon"), &PathLeft, &PathRight); - - if (PathLeft.IsEmpty() || PathRight.IsEmpty()) - { - UE_LOG(LogAssetData, Error, TEXT("Failed to retrieve the base Ayon directory!")) - return; - } - - PathName.RemoveFromEnd(PathRight, ESearchCase::CaseSensitive); - - //Get the current settings - const UAyonSettings* Settings = GetMutableDefault(); - - //Color the base folder - UAyonLib::SetFolderColor(PathName, Settings->GetFolderFColor(), false); - - //Get Sub paths, iterate through them and color them according to the folder color in UAyonSettings - const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked( - "AssetRegistry"); - - TArray PathList; - - AssetRegistryModule.Get().GetSubPaths(PathName, PathList, true); - - if (PathList.Num() > 0) - { - for (const FString& Path : PathList) - { - UAyonLib::SetFolderColor(Path, Settings->GetFolderFColor(), false); - } - } -} - -void UAyonPublishInstance::SendNotification(const FString& Text) const -{ - FNotificationInfo Info{FText::FromString(Text)}; - - Info.bFireAndForget = true; - Info.bUseLargeFont = false; - Info.bUseThrobber = false; - Info.bUseSuccessFailIcons = false; - Info.ExpireDuration = 4.f; - Info.FadeOutDuration = 2.f; - - FSlateNotificationManager::Get().AddNotification(Info); - - UE_LOG(LogAssetData, Warning, - TEXT( - "Removed duplicated asset from the AssetsDataExternal in Container \"%s\", Asset is already included in the AssetDataInternal!" - ), *GetName() - ) -} - - -void UAyonPublishInstance::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) -{ - Super::PostEditChangeProperty(PropertyChangedEvent); - - if (PropertyChangedEvent.ChangeType == EPropertyChangeType::ValueSet && - PropertyChangedEvent.Property->GetFName() == GET_MEMBER_NAME_CHECKED( - UAyonPublishInstance, AssetDataExternal)) - { - // Check for duplicated assets - for (const auto& Asset : AssetDataInternal) - { - if (AssetDataExternal.Contains(Asset)) - { - AssetDataExternal.Remove(Asset); - return SendNotification( - "You are not allowed to add assets into AssetDataExternal which are already included in AssetDataInternal!"); - } - } - - // Check if no UAyonPublishInstance type assets are included - for (const auto& Asset : AssetDataExternal) - { - if (Cast(Asset.Get()) != nullptr) - { - AssetDataExternal.Remove(Asset); - return SendNotification("You are not allowed to add publish instances!"); - } - } - } -} - -#endif diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonPublishInstanceFactory.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonPublishInstanceFactory.cpp deleted file mode 100644 index f79c428a6d..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonPublishInstanceFactory.cpp +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -// Deprecation warning: this is left here just for backwards compatibility -// and will be removed in next versions of Ayon. -#include "AyonPublishInstanceFactory.h" -#include "AyonPublishInstance.h" - -UAyonPublishInstanceFactory::UAyonPublishInstanceFactory(const FObjectInitializer& ObjectInitializer) - : UFactory(ObjectInitializer) -{ - SupportedClass = UAyonPublishInstance::StaticClass(); - bCreateNew = false; - bEditorImport = true; -} - -UObject* UAyonPublishInstanceFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) -{ - check(InClass->IsChildOf(UAyonPublishInstance::StaticClass())); - return NewObject(InParent, InClass, InName, Flags); -} - -bool UAyonPublishInstanceFactory::ShouldShowInNewMenu() const { - return false; -} diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonPythonBridge.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonPythonBridge.cpp deleted file mode 100644 index 0ed4b2f704..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonPythonBridge.cpp +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#include "AyonPythonBridge.h" - -UAyonPythonBridge* UAyonPythonBridge::Get() -{ - TArray AyonPythonBridgeClasses; - GetDerivedClasses(UAyonPythonBridge::StaticClass(), AyonPythonBridgeClasses); - int32 NumClasses = AyonPythonBridgeClasses.Num(); - if (NumClasses > 0) - { - return Cast(AyonPythonBridgeClasses[NumClasses - 1]->GetDefaultObject()); - } - return nullptr; -}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonSettings.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonSettings.cpp deleted file mode 100644 index 509b7268ba..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonSettings.cpp +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#include "AyonSettings.h" - -#include "Interfaces/IPluginManager.h" - -/** - * Mainly is used for initializing default values if the DefaultAyonSettings.ini file does not exist in the saved config - */ -UAyonSettings::UAyonSettings(const FObjectInitializer& ObjectInitializer) -{ - - const FString ConfigFilePath = AYON_SETTINGS_FILEPATH; - - // This has to be probably in the future set using the UE Reflection system - FColor Color; - GConfig->GetColor(TEXT("/Script/Ayon.AyonSettings"), TEXT("FolderColor"), Color, ConfigFilePath); - - FolderColor = Color; -} diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonStyle.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonStyle.cpp deleted file mode 100644 index b133225fd5..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonStyle.cpp +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#include "AyonStyle.h" -#include "Framework/Application/SlateApplication.h" -#include "Styling/SlateStyle.h" -#include "Styling/SlateStyleRegistry.h" - - -TUniquePtr< FSlateStyleSet > FAyonStyle::AyonStyleInstance = nullptr; - -void FAyonStyle::Initialize() -{ - if (!AyonStyleInstance.IsValid()) - { - AyonStyleInstance = Create(); - FSlateStyleRegistry::RegisterSlateStyle(*AyonStyleInstance); - } -} - -void FAyonStyle::Shutdown() -{ - if (AyonStyleInstance.IsValid()) - { - FSlateStyleRegistry::UnRegisterSlateStyle(*AyonStyleInstance); - AyonStyleInstance.Reset(); - } -} - -FName FAyonStyle::GetStyleSetName() -{ - static FName StyleSetName(TEXT("AyonStyle")); - return StyleSetName; -} - -FName FAyonStyle::GetContextName() -{ - static FName ContextName(TEXT("Ayon")); - return ContextName; -} - -#define IMAGE_BRUSH(RelativePath, ...) FSlateImageBrush( Style->RootToContentDir( RelativePath, TEXT(".png") ), __VA_ARGS__ ) - -const FVector2D Icon40x40(40.0f, 40.0f); - -TUniquePtr< FSlateStyleSet > FAyonStyle::Create() -{ - TUniquePtr< FSlateStyleSet > Style = MakeUnique(GetStyleSetName()); - Style->SetContentRoot(FPaths::EnginePluginsDir() / TEXT("Marketplace/Ayon/Resources")); - - return Style; -} - -void FAyonStyle::SetIcon(const FString& StyleName, const FString& ResourcePath) -{ - FSlateStyleSet* Style = AyonStyleInstance.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& FAyonStyle::Get() -{ - check(AyonStyleInstance); - return *AyonStyleInstance; -} diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/Commandlets/AyonActionResult.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/Commandlets/AyonActionResult.cpp deleted file mode 100644 index 49376e8648..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/Commandlets/AyonActionResult.cpp +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - - -#include "Commandlets/AyonActionResult.h" -#include "Logging/Ayon_Log.h" - -EAyon_ActionResult::Type& FAyon_ActionResult::GetStatus() -{ - return Status; -} - -FText& FAyon_ActionResult::GetReason() -{ - return Reason; -} - -FAyon_ActionResult::FAyon_ActionResult():Status(EAyon_ActionResult::Type::Ok) -{ - -} - -FAyon_ActionResult::FAyon_ActionResult(const EAyon_ActionResult::Type& InEnum):Status(InEnum) -{ - TryLog(); -} - -FAyon_ActionResult::FAyon_ActionResult(const EAyon_ActionResult::Type& InEnum, const FText& InReason):Status(InEnum), Reason(InReason) -{ - TryLog(); -}; - -bool FAyon_ActionResult::IsProblem() const -{ - return Status != EAyon_ActionResult::Ok; -} - -void FAyon_ActionResult::TryLog() const -{ - if(IsProblem()) - UE_LOG(LogCommandletOPGenerateProject, Error, TEXT("%s"), *Reason.ToString()); -} diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp deleted file mode 100644 index 0328d3b7e6..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp +++ /dev/null @@ -1,141 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#include "Commandlets/Implementations/AyonGenerateProjectCommandlet.h" - -#include "Editor.h" -#include "GameProjectUtils.h" -#include "AyonConstants.h" -#include "Commandlets/AyonActionResult.h" -#include "ProjectDescriptor.h" - -int32 UAyonGenerateProjectCommandlet::Main(const FString& CommandLineParams) -{ - //Parses command line parameters & creates structure FProjectInformation - const FAyonGenerateProjectParams ParsedParams = FAyonGenerateProjectParams(CommandLineParams); - ProjectInformation = ParsedParams.GenerateUEProjectInformation(); - - //Creates .uproject & other UE files - EVALUATE_AYON_ACTION_RESULT(TryCreateProject()); - - //Loads created .uproject - EVALUATE_AYON_ACTION_RESULT(TryLoadProjectDescriptor()); - - //Adds needed plugin to .uproject - AttachPluginsToProjectDescriptor(); - - //Saves .uproject - EVALUATE_AYON_ACTION_RESULT(TrySave()); - - //When we are here, there should not be problems in generating Unreal Project for Ayon - return 0; -} - - -FAyonGenerateProjectParams::FAyonGenerateProjectParams(): FAyonGenerateProjectParams("") -{ -} - -FAyonGenerateProjectParams::FAyonGenerateProjectParams(const FString& CommandLineParams): CommandLineParams( - CommandLineParams) -{ - UCommandlet::ParseCommandLine(*CommandLineParams, Tokens, Switches); -} - -FProjectInformation FAyonGenerateProjectParams::GenerateUEProjectInformation() const -{ - FProjectInformation ProjectInformation = FProjectInformation(); - ProjectInformation.ProjectFilename = GetProjectFileName(); - - ProjectInformation.bShouldGenerateCode = IsSwitchPresent("GenerateCode"); - - return ProjectInformation; -} - -FString FAyonGenerateProjectParams::TryGetToken(const int32 Index) const -{ - return Tokens.IsValidIndex(Index) ? Tokens[Index] : ""; -} - -FString FAyonGenerateProjectParams::GetProjectFileName() const -{ - return TryGetToken(0); -} - -bool FAyonGenerateProjectParams::IsSwitchPresent(const FString& Switch) const -{ - return INDEX_NONE != Switches.IndexOfByPredicate([&Switch](const FString& Item) -> bool - { - return Item.Equals(Switch); - } - ); -} - - -UAyonGenerateProjectCommandlet::UAyonGenerateProjectCommandlet() -{ - LogToConsole = true; -} - -FAyon_ActionResult UAyonGenerateProjectCommandlet::TryCreateProject() const -{ - FText FailReason; - FText FailLog; - TArray OutCreatedFiles; - - if (!GameProjectUtils::CreateProject(ProjectInformation, FailReason, FailLog, &OutCreatedFiles)) - return FAyon_ActionResult(EAyon_ActionResult::ProjectNotCreated, FailReason); - return FAyon_ActionResult(); -} - -FAyon_ActionResult UAyonGenerateProjectCommandlet::TryLoadProjectDescriptor() -{ - FText FailReason; - const bool bLoaded = ProjectDescriptor.Load(ProjectInformation.ProjectFilename, FailReason); - - return FAyon_ActionResult(bLoaded ? EAyon_ActionResult::Ok : EAyon_ActionResult::ProjectNotLoaded, FailReason); -} - -void UAyonGenerateProjectCommandlet::AttachPluginsToProjectDescriptor() -{ - FPluginReferenceDescriptor AyonPluginDescriptor; - AyonPluginDescriptor.bEnabled = true; - AyonPluginDescriptor.Name = AyonConstants::Ayon_PluginName; - ProjectDescriptor.Plugins.Add(AyonPluginDescriptor); - - FPluginReferenceDescriptor PythonPluginDescriptor; - PythonPluginDescriptor.bEnabled = true; - PythonPluginDescriptor.Name = AyonConstants::PythonScript_PluginName; - ProjectDescriptor.Plugins.Add(PythonPluginDescriptor); - - FPluginReferenceDescriptor SequencerScriptingPluginDescriptor; - SequencerScriptingPluginDescriptor.bEnabled = true; - SequencerScriptingPluginDescriptor.Name = AyonConstants::SequencerScripting_PluginName; - ProjectDescriptor.Plugins.Add(SequencerScriptingPluginDescriptor); - - FPluginReferenceDescriptor MovieRenderPipelinePluginDescriptor; - MovieRenderPipelinePluginDescriptor.bEnabled = true; - MovieRenderPipelinePluginDescriptor.Name = AyonConstants::MovieRenderPipeline_PluginName; - ProjectDescriptor.Plugins.Add(MovieRenderPipelinePluginDescriptor); - - FPluginReferenceDescriptor EditorScriptingPluginDescriptor; - EditorScriptingPluginDescriptor.bEnabled = true; - EditorScriptingPluginDescriptor.Name = AyonConstants::EditorScriptingUtils_PluginName; - ProjectDescriptor.Plugins.Add(EditorScriptingPluginDescriptor); -} - -FAyon_ActionResult UAyonGenerateProjectCommandlet::TrySave() -{ - FText FailReason; - const bool bSaved = ProjectDescriptor.Save(ProjectInformation.ProjectFilename, FailReason); - - return FAyon_ActionResult(bSaved ? EAyon_ActionResult::Ok : EAyon_ActionResult::ProjectNotSaved, FailReason); -} - -FAyonGenerateProjectParams UAyonGenerateProjectCommandlet::ParseParameters(const FString& Params) const -{ - FAyonGenerateProjectParams ParamsResult; - - TArray Tokens, Switches; - ParseCommandLine(*Params, Tokens, Switches); - - return ParamsResult; -} diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp deleted file mode 100644 index 320285591e..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp +++ /dev/null @@ -1,203 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -// Deprecation warning: this is left here just for backwards compatibility -// and will be removed in next versions of Ayon. -#pragma once - -#include "OpenPypePublishInstance.h" -#include "AssetRegistryModule.h" -#include "AyonLib.h" -#include "AyonSettings.h" -#include "Framework/Notifications/NotificationManager.h" -#include "Widgets/Notifications/SNotificationList.h" - -//Moves all the invalid pointers to the end to prepare them for the shrinking -#define REMOVE_INVALID_ENTRIES(VAR) VAR.CompactStable(); \ - VAR.Shrink(); - -UOpenPypePublishInstance::UOpenPypePublishInstance(const FObjectInitializer& ObjectInitializer) - : UPrimaryDataAsset(ObjectInitializer) -{ - const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked< - FAssetRegistryModule>("AssetRegistry"); - - const FPropertyEditorModule& PropertyEditorModule = FModuleManager::LoadModuleChecked( - "PropertyEditor"); - - FString Left, Right; - GetPathName().Split("/" + GetName(), &Left, &Right); - - FARFilter Filter; - Filter.PackagePaths.Emplace(FName(Left)); - - TArray FoundAssets; - AssetRegistryModule.GetRegistry().GetAssets(Filter, FoundAssets); - - for (const FAssetData& AssetData : FoundAssets) - OnAssetCreated(AssetData); - - REMOVE_INVALID_ENTRIES(AssetDataInternal) - REMOVE_INVALID_ENTRIES(AssetDataExternal) - - AssetRegistryModule.Get().OnAssetAdded().AddUObject(this, &UOpenPypePublishInstance::OnAssetCreated); - AssetRegistryModule.Get().OnAssetRemoved().AddUObject(this, &UOpenPypePublishInstance::OnAssetRemoved); - AssetRegistryModule.Get().OnAssetUpdated().AddUObject(this, &UOpenPypePublishInstance::OnAssetUpdated); - -#ifdef WITH_EDITOR - ColorOpenPypeDirs(); -#endif - -} - -void UOpenPypePublishInstance::OnAssetCreated(const FAssetData& InAssetData) -{ - TArray split; - - UObject* Asset = InAssetData.GetAsset(); - - if (!IsValid(Asset)) - { - UE_LOG(LogAssetData, Warning, TEXT("Asset \"%s\" is not valid! Skipping the addition."), - *InAssetData.ObjectPath.ToString()); - return; - } - - const bool result = IsUnderSameDir(Asset) && Cast(Asset) == nullptr; - - if (result) - { - if (AssetDataInternal.Emplace(Asset).IsValidId()) - { - UE_LOG(LogTemp, Log, TEXT("Added an Asset to PublishInstance - Publish Instance: %s, Asset %s"), - *this->GetName(), *Asset->GetName()); - } - } -} - -void UOpenPypePublishInstance::OnAssetRemoved(const FAssetData& InAssetData) -{ - if (Cast(InAssetData.GetAsset()) == nullptr) - { - if (AssetDataInternal.Contains(nullptr)) - { - AssetDataInternal.Remove(nullptr); - REMOVE_INVALID_ENTRIES(AssetDataInternal) - } - else - { - AssetDataExternal.Remove(nullptr); - REMOVE_INVALID_ENTRIES(AssetDataExternal) - } - } -} - -void UOpenPypePublishInstance::OnAssetUpdated(const FAssetData& InAssetData) -{ - REMOVE_INVALID_ENTRIES(AssetDataInternal); - REMOVE_INVALID_ENTRIES(AssetDataExternal); -} - -bool UOpenPypePublishInstance::IsUnderSameDir(const UObject* InAsset) const -{ - FString ThisLeft, ThisRight; - this->GetPathName().Split(this->GetName(), &ThisLeft, &ThisRight); - - return InAsset->GetPathName().StartsWith(ThisLeft); -} - -#ifdef WITH_EDITOR - -void UOpenPypePublishInstance::ColorOpenPypeDirs() -{ - FString PathName = this->GetPathName(); - - //Check whether the path contains the defined OpenPype folder - if (!PathName.Contains(TEXT("OpenPype"))) return; - - //Get the base path for open pype - FString PathLeft, PathRight; - PathName.Split(FString("OpenPype"), &PathLeft, &PathRight); - - if (PathLeft.IsEmpty() || PathRight.IsEmpty()) - { - UE_LOG(LogAssetData, Error, TEXT("Failed to retrieve the base OpenPype directory!")) - return; - } - - PathName.RemoveFromEnd(PathRight, ESearchCase::CaseSensitive); - - //Get the current settings - const UAyonSettings* Settings = GetMutableDefault(); - - //Color the base folder - UAyonLib::SetFolderColor(PathName, Settings->GetFolderFColor(), false); - - //Get Sub paths, iterate through them and color them according to the folder color in UAyonSettings - const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked( - "AssetRegistry"); - - TArray PathList; - - AssetRegistryModule.Get().GetSubPaths(PathName, PathList, true); - - if (PathList.Num() > 0) - { - for (const FString& Path : PathList) - { - UAyonLib::SetFolderColor(Path, Settings->GetFolderFColor(), false); - } - } -} - -void UOpenPypePublishInstance::SendNotification(const FString& Text) const -{ - FNotificationInfo Info{FText::FromString(Text)}; - - Info.bFireAndForget = true; - Info.bUseLargeFont = false; - Info.bUseThrobber = false; - Info.bUseSuccessFailIcons = false; - Info.ExpireDuration = 4.f; - Info.FadeOutDuration = 2.f; - - FSlateNotificationManager::Get().AddNotification(Info); - - UE_LOG(LogAssetData, Warning, - TEXT( - "Removed duplicated asset from the AssetsDataExternal in Container \"%s\", Asset is already included in the AssetDataInternal!" - ), *GetName() - ) -} - - -void UOpenPypePublishInstance::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) -{ - Super::PostEditChangeProperty(PropertyChangedEvent); - - if (PropertyChangedEvent.ChangeType == EPropertyChangeType::ValueSet && - PropertyChangedEvent.Property->GetFName() == GET_MEMBER_NAME_CHECKED( - UOpenPypePublishInstance, AssetDataExternal)) - { - // Check for duplicated assets - for (const auto& Asset : AssetDataInternal) - { - if (AssetDataExternal.Contains(Asset)) - { - AssetDataExternal.Remove(Asset); - return SendNotification( - "You are not allowed to add assets into AssetDataExternal which are already included in AssetDataInternal!"); - } - } - - // Check if no UOpenPypePublishInstance type assets are included - for (const auto& Asset : AssetDataExternal) - { - if (Cast(Asset.Get()) != nullptr) - { - AssetDataExternal.Remove(Asset); - return SendNotification("You are not allowed to add publish instances!"); - } - } - } -} - -#endif diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/Ayon.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/Ayon.h deleted file mode 100644 index d11af70058..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/Ayon.h +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#pragma once - - -class FAyonModule : public IModuleInterface -{ -public: - virtual void StartupModule() override; - virtual void ShutdownModule() override; - -private: - void RegisterSettings(); - bool HandleSettingsSaved(); - - void AddMenuEntry(FMenuBuilder& MenuBuilder); - void AddToobarEntry(FToolBarBuilder& ToolbarBuilder); - void MenuPopup(); - void MenuDialog(); -}; diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonAssetContainer.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonAssetContainer.h deleted file mode 100644 index cc17b3960a..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonAssetContainer.h +++ /dev/null @@ -1,39 +0,0 @@ -// 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 "AyonAssetContainer.generated.h" - -/** - * - */ -UCLASS(Blueprintable) -class AYON_API UAyonAssetContainer : public UAssetUserData -{ - GENERATED_BODY() - -public: - - UAyonAssetContainer(const FObjectInitializer& ObjectInitalizer); - // ~UAyonAssetContainer(); - - UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Assets") - 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_4.27/Ayon/Source/Ayon/Public/AyonAssetContainerFactory.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonAssetContainerFactory.h deleted file mode 100644 index 7c35897911..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonAssetContainerFactory.h +++ /dev/null @@ -1,21 +0,0 @@ -// Fill out your copyright notice in the Description page of Project Settings. - -#pragma once - -#include "CoreMinimal.h" -#include "Factories/Factory.h" -#include "AyonAssetContainerFactory.generated.h" - -/** - * - */ -UCLASS() -class AYON_API UAyonAssetContainerFactory : public UFactory -{ - GENERATED_BODY() - -public: - UAyonAssetContainerFactory(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_4.27/Ayon/Source/Ayon/Public/AyonConstants.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonConstants.h deleted file mode 100644 index 6a02b5682f..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonConstants.h +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#pragma once - -#include "CoreMinimal.h" - -namespace AyonConstants -{ - const FString Ayon_PluginName = "Ayon"; - const FString PythonScript_PluginName = "PythonScriptPlugin"; - const FString SequencerScripting_PluginName = "SequencerScripting"; - const FString MovieRenderPipeline_PluginName = "MovieRenderPipeline"; - const FString EditorScriptingUtils_PluginName = "EditorScriptingUtilities"; -} - - diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonLib.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonLib.h deleted file mode 100644 index da83b448fb..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonLib.h +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#pragma once - -#include "AyonLib.generated.h" - - -UCLASS(Blueprintable) -class AYON_API UAyonLib : public UBlueprintFunctionLibrary -{ - - GENERATED_BODY() - -public: - UFUNCTION(BlueprintCallable, Category = Python) - static bool SetFolderColor(const FString& FolderPath, const FLinearColor& FolderColor,const 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_4.27/Ayon/Source/Ayon/Public/AyonPublishInstance.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonPublishInstance.h deleted file mode 100644 index 0a0628c3ec..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonPublishInstance.h +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -// Deprecation warning: this is left here just for backwards compatibility -// and will be removed in next versions of Ayon. -#pragma once - -#include "AyonPublishInstance.generated.h" - - -UCLASS(Blueprintable) -class AYON_API UAyonPublishInstance : public UPrimaryDataAsset -{ - GENERATED_UCLASS_BODY() - -public: - /** - * Retrieves all the assets which are monitored by the Publish Instance (Monitors assets in the directory which is - * placed in) - * - * @return - Set of UObjects. Careful! They are returning raw pointers. Seems like an issue in UE5 - */ - UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") - TSet GetInternalAssets() const - { - //For some reason it can only return Raw Pointers? Seems like an issue which they haven't fixed. - TSet ResultSet; - - for (const auto& Asset : AssetDataInternal) - ResultSet.Add(Asset.LoadSynchronous()); - - return ResultSet; - } - - /** - * Retrieves all the assets which have been added manually by the Publish Instance - * - * @return - TSet of assets (UObjects). Careful! They are returning raw pointers. Seems like an issue in UE5 - */ - UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") - TSet GetExternalAssets() const - { - //For some reason it can only return Raw Pointers? Seems like an issue which they haven't fixed. - TSet ResultSet; - - for (const auto& Asset : AssetDataExternal) - ResultSet.Add(Asset.LoadSynchronous()); - - return ResultSet; - } - - /** - * Function for returning all the assets in the container combined. - * - * @return Returns all the internal and externally added assets into one set (TSet of UObjects). Careful! They are - * returning raw pointers. Seems like an issue in UE5 - * - * @attention If the bAddExternalAssets variable is false, external assets won't be included! - */ - UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") - TSet GetAllAssets() const - { - const TSet>& IteratedSet = bAddExternalAssets - ? AssetDataInternal.Union(AssetDataExternal) - : AssetDataInternal; - - //Create a new TSet only with raw pointers. - TSet ResultSet; - - for (auto& Asset : IteratedSet) - ResultSet.Add(Asset.LoadSynchronous()); - - return ResultSet; - } - -private: - UPROPERTY(VisibleAnywhere, Category="Assets") - TSet> AssetDataInternal; - - /** - * This property allows exposing the array to include other assets from any other directory than what it's currently - * monitoring. NOTE: that these assets have to be added manually! They are not automatically registered or added! - */ - UPROPERTY(EditAnywhere, Category = "Assets") - bool bAddExternalAssets = false; - - UPROPERTY(EditAnywhere, meta=(EditCondition="bAddExternalAssets"), Category="Assets") - TSet> AssetDataExternal; - - - void OnAssetCreated(const FAssetData& InAssetData); - void OnAssetRemoved(const FAssetData& InAssetData); - void OnAssetUpdated(const FAssetData& InAssetData); - - bool IsUnderSameDir(const UObject* InAsset) const; - -#ifdef WITH_EDITOR - - void ColorAyonDirs(); - - void SendNotification(const FString& Text) const; - virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; - -#endif -}; diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonPublishInstanceFactory.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonPublishInstanceFactory.h deleted file mode 100644 index 3cef8e76b2..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonPublishInstanceFactory.h +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -// Deprecation warning: this is left here just for backwards compatibility -// and will be removed in next versions of Ayon. -#pragma once - -#include "CoreMinimal.h" -#include "Factories/Factory.h" -#include "AyonPublishInstanceFactory.generated.h" - -/** - * - */ -UCLASS() -class AYON_API UAyonPublishInstanceFactory : public UFactory -{ - GENERATED_BODY() - -public: - UAyonPublishInstanceFactory(const FObjectInitializer& ObjectInitializer); - virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; - virtual bool ShouldShowInNewMenu() const override; -}; diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonPythonBridge.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonPythonBridge.h deleted file mode 100644 index 3c429fd7d3..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonPythonBridge.h +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#pragma once -#include "AyonPythonBridge.generated.h" - -UCLASS(Blueprintable) -class UAyonPythonBridge : public UObject -{ - GENERATED_BODY() - -public: - UFUNCTION(BlueprintCallable, Category = Python) - static UAyonPythonBridge* Get(); - - UFUNCTION(BlueprintImplementableEvent, Category = Python) - void RunInPython_Popup() const; - - UFUNCTION(BlueprintImplementableEvent, Category = Python) - void RunInPython_Dialog() const; - -}; diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonSettings.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonSettings.h deleted file mode 100644 index 7a93f107c5..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonSettings.h +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#pragma once - -#include "CoreMinimal.h" -#include "AyonSettings.generated.h" - -#define AYON_SETTINGS_FILEPATH IPluginManager::Get().FindPlugin("Ayon")->GetBaseDir() / TEXT("Config") / TEXT("DefaultAyonSettings.ini") - -UCLASS(Config=AyonSettings, DefaultConfig) -class AYON_API UAyonSettings : public UObject -{ - GENERATED_UCLASS_BODY() - - UFUNCTION(BlueprintCallable, BlueprintPure, Category = Settings) - FColor GetFolderFColor() const - { - return FolderColor; - } - - UFUNCTION(BlueprintCallable, BlueprintPure, Category = Settings) - FLinearColor GetFolderFLinearColor() const - { - return FLinearColor(FolderColor); - } - -protected: - - UPROPERTY(config, EditAnywhere, Category = Folders) - FColor FolderColor = FColor(25,45,223); -}; diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonStyle.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonStyle.h deleted file mode 100644 index 188e4a510c..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonStyle.h +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#pragma once -#include "CoreMinimal.h" - -class FSlateStyleSet; -class ISlateStyle; - - -class FAyonStyle -{ -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 > AyonStyleInstance; -}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/Commandlets/AyonActionResult.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/Commandlets/AyonActionResult.h deleted file mode 100644 index 4694055164..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/Commandlets/AyonActionResult.h +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#pragma once - -#include "CoreMinimal.h" -#include "AyonActionResult.generated.h" - -/** - * @brief This macro returns error code when is problem or does nothing when there is no problem. - * @param ActionResult FAyon_ActionResult structure - */ -#define EVALUATE_AYON_ACTION_RESULT(ActionResult) \ - if(ActionResult.IsProblem()) \ - return ActionResult.GetStatus(); - -/** -* @brief This enum values are humanly readable mapping of error codes. -* Here should be all error codes to be possible find what went wrong. -* TODO: In the future should exists an web document where is mapped error code & what problem occured & how to repair it... -*/ -UENUM() -namespace EAyon_ActionResult -{ - enum Type - { - Ok, - ProjectNotCreated, - ProjectNotLoaded, - ProjectNotSaved, - //....Here insert another values - - //Do not remove! - //Usable for looping through enum values - __Last UMETA(Hidden) - }; -} - - -/** - * @brief This struct holds action result enum and optionally reason of fail - */ -USTRUCT() -struct FAyon_ActionResult -{ - GENERATED_BODY() - -public: - /** @brief Default constructor usable when there is no problem */ - FAyon_ActionResult(); - - /** - * @brief This constructor initializes variables & attempts to log when is error - * @param InEnum Status - */ - FAyon_ActionResult(const EAyon_ActionResult::Type& InEnum); - - /** - * @brief This constructor initializes variables & attempts to log when is error - * @param InEnum Status - * @param InReason Reason of potential fail - */ - FAyon_ActionResult(const EAyon_ActionResult::Type& InEnum, const FText& InReason); - -private: - /** @brief Action status */ - EAyon_ActionResult::Type Status; - - /** @brief Optional reason of fail */ - FText Reason; - -public: - /** - * @brief Checks if there is problematic state - * @return true when status is not equal to EAyon_ActionResult::Ok - */ - bool IsProblem() const; - EAyon_ActionResult::Type& GetStatus(); - FText& GetReason(); - -private: - void TryLog() const; -}; - diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h deleted file mode 100644 index cabd524b8c..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#pragma once - -#include "GameProjectUtils.h" -#include "Commandlets/AyonActionResult.h" -#include "ProjectDescriptor.h" -#include "Commandlets/Commandlet.h" -#include "AyonGenerateProjectCommandlet.generated.h" - -struct FProjectDescriptor; -struct FProjectInformation; - -/** -* @brief Structure which parses command line parameters and generates FProjectInformation -*/ -USTRUCT() -struct FAyonGenerateProjectParams -{ - GENERATED_BODY() - -private: - FString CommandLineParams; - TArray Tokens; - TArray Switches; - -public: - FAyonGenerateProjectParams(); - FAyonGenerateProjectParams(const FString& CommandLineParams); - - FProjectInformation GenerateUEProjectInformation() const; - -private: - FString TryGetToken(const int32 Index) const; - FString GetProjectFileName() const; - - bool IsSwitchPresent(const FString& Switch) const; -}; - -UCLASS() -class AYON_API UAyonGenerateProjectCommandlet : public UCommandlet -{ - GENERATED_BODY() - -private: - FProjectInformation ProjectInformation; - FProjectDescriptor ProjectDescriptor; - -public: - UAyonGenerateProjectCommandlet(); - - virtual int32 Main(const FString& CommandLineParams) override; - -private: - FAyonGenerateProjectParams ParseParameters(const FString& Params) const; - FAyon_ActionResult TryCreateProject() const; - FAyon_ActionResult TryLoadProjectDescriptor(); - void AttachPluginsToProjectDescriptor(); - FAyon_ActionResult TrySave(); -}; - diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/Logging/Ayon_Log.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/Logging/Ayon_Log.h deleted file mode 100644 index 21571afd02..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/Logging/Ayon_Log.h +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#pragma once - -DEFINE_LOG_CATEGORY_STATIC(LogCommandletOPGenerateProject, Log, All); diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h deleted file mode 100644 index 4a7a6a3a9f..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -// Deprecation warning: this is left here just for backwards compatibility -// and will be removed in next versions of Ayon. -#pragma once - -#include "OpenPypePublishInstance.generated.h" - - -UCLASS(Blueprintable) -class AYON_API UOpenPypePublishInstance : public UPrimaryDataAsset -{ - GENERATED_UCLASS_BODY() - -public: - /** - * Retrieves all the assets which are monitored by the Publish Instance (Monitors assets in the directory which is - * placed in) - * - * @return - Set of UObjects. Careful! They are returning raw pointers. Seems like an issue in UE5 - */ - UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") - TSet GetInternalAssets() const - { - //For some reason it can only return Raw Pointers? Seems like an issue which they haven't fixed. - TSet ResultSet; - - for (const auto& Asset : AssetDataInternal) - ResultSet.Add(Asset.LoadSynchronous()); - - return ResultSet; - } - - /** - * Retrieves all the assets which have been added manually by the Publish Instance - * - * @return - TSet of assets (UObjects). Careful! They are returning raw pointers. Seems like an issue in UE5 - */ - UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") - TSet GetExternalAssets() const - { - //For some reason it can only return Raw Pointers? Seems like an issue which they haven't fixed. - TSet ResultSet; - - for (const auto& Asset : AssetDataExternal) - ResultSet.Add(Asset.LoadSynchronous()); - - return ResultSet; - } - - /** - * Function for returning all the assets in the container combined. - * - * @return Returns all the internal and externally added assets into one set (TSet of UObjects). Careful! They are - * returning raw pointers. Seems like an issue in UE5 - * - * @attention If the bAddExternalAssets variable is false, external assets won't be included! - */ - UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") - TSet GetAllAssets() const - { - const TSet>& IteratedSet = bAddExternalAssets - ? AssetDataInternal.Union(AssetDataExternal) - : AssetDataInternal; - - //Create a new TSet only with raw pointers. - TSet ResultSet; - - for (auto& Asset : IteratedSet) - ResultSet.Add(Asset.LoadSynchronous()); - - return ResultSet; - } - -private: - UPROPERTY(VisibleAnywhere, Category="Assets") - TSet> AssetDataInternal; - - /** - * This property allows exposing the array to include other assets from any other directory than what it's currently - * monitoring. NOTE: that these assets have to be added manually! They are not automatically registered or added! - */ - UPROPERTY(EditAnywhere, Category = "Assets") - bool bAddExternalAssets = false; - - UPROPERTY(EditAnywhere, meta=(EditCondition="bAddExternalAssets"), Category="Assets") - TSet> AssetDataExternal; - - - void OnAssetCreated(const FAssetData& InAssetData); - void OnAssetRemoved(const FAssetData& InAssetData); - void OnAssetUpdated(const FAssetData& InAssetData); - - bool IsUnderSameDir(const UObject* InAsset) const; - -#ifdef WITH_EDITOR - - void ColorOpenPypeDirs(); - - void SendNotification(const FString& Text) const; - virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; - -#endif -}; diff --git a/openpype/hosts/unreal/integration/UE_4.27/BuildPlugin_4-27.bat b/openpype/hosts/unreal/integration/UE_4.27/BuildPlugin_4-27.bat deleted file mode 100644 index 96cdb96f8a..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/BuildPlugin_4-27.bat +++ /dev/null @@ -1 +0,0 @@ -D:\UE4\UE_4.27\Engine\Build\BatchFiles\RunUAT.bat BuildPlugin -plugin="D:\OpenPype\openpype\hosts\unreal\integration\UE_4.27\Ayon\Ayon.uplugin" -Package="D:\BuiltPlugins\4.27" \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_4.27/BuildPlugin_4-27_Window.bat b/openpype/hosts/unreal/integration/UE_4.27/BuildPlugin_4-27_Window.bat deleted file mode 100644 index 1343843a82..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/BuildPlugin_4-27_Window.bat +++ /dev/null @@ -1 +0,0 @@ -cmd /k "BuildPlugin_4-27.bat" \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_4.27/CommandletProject/.gitignore b/openpype/hosts/unreal/integration/UE_4.27/CommandletProject/.gitignore deleted file mode 100644 index e74e6886b7..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/CommandletProject/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -/Saved -/DerivedDataCache -/Intermediate -/Content -/Config -/Binaries -/.idea -/.vs \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_4.27/CommandletProject/CommandletProject.uproject b/openpype/hosts/unreal/integration/UE_4.27/CommandletProject/CommandletProject.uproject deleted file mode 100644 index ea7bf21dc4..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/CommandletProject/CommandletProject.uproject +++ /dev/null @@ -1,12 +0,0 @@ -{ - "FileVersion": 3, - "EngineAssociation": "4.27", - "Category": "", - "Description": "", - "Plugins": [ - { - "Name": "Ayon", - "Enabled": true - } - ] -} \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/.gitignore b/openpype/hosts/unreal/integration/UE_5.0/Ayon/.gitignore deleted file mode 100644 index b32a6f55e5..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/.gitignore +++ /dev/null @@ -1,35 +0,0 @@ -# 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/Ayon/Ayon.uplugin b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Ayon.uplugin deleted file mode 100644 index 70ed8f6b9a..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Ayon.uplugin +++ /dev/null @@ -1,24 +0,0 @@ -{ - "FileVersion": 3, - "Version": 1, - "VersionName": "1.0", - "FriendlyName": "Ayon", - "Description": "Ayon Integration", - "Category": "Ayon.Integration", - "CreatedBy": "Ondrej Samohel", - "CreatedByURL": "https://ayon.ynput.io", - "DocsURL": "https://ayon.ynput.io/docs/artist_hosts_unreal", - "MarketplaceURL": "", - "SupportURL": "https://ynput.io/", - "CanContainContent": true, - "EngineVersion": "5.0", - "IsExperimentalVersion": false, - "Installed": true, - "Modules": [ - { - "Name": "Ayon", - "Type": "Editor", - "LoadingPhase": "Default" - } - ] -} diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Config/DefaultAyonSettings.ini b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Config/DefaultAyonSettings.ini deleted file mode 100644 index 9ad7f55201..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Config/DefaultAyonSettings.ini +++ /dev/null @@ -1,2 +0,0 @@ -[/Script/Ayon.AyonSettings] -FolderColor=(R=91,G=197,B=220,A=255) \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Config/FilterPlugin.ini b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Config/FilterPlugin.ini deleted file mode 100644 index ccebca2f32..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Config/FilterPlugin.ini +++ /dev/null @@ -1,8 +0,0 @@ -[FilterPlugin] -; This section lists additional files which will be packaged along with your plugin. Paths should be listed relative to the root plugin directory, and -; may include "...", "*", and "?" wildcards to match directories, files, and individual characters respectively. -; -; Examples: -; /README.txt -; /Extras/... -; /Binaries/ThirdParty/*.dll diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Content/Python/init_unreal.py b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Content/Python/init_unreal.py deleted file mode 100644 index c0b1d0ce5d..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Content/Python/init_unreal.py +++ /dev/null @@ -1,30 +0,0 @@ -import unreal - -ayon_detected = True -try: - from openpype.pipeline import install_host - from openpype.hosts.unreal.api import UnrealHost - - ayon_host = UnrealHost() -except ImportError as exc: - ayon_host = None - ayon_detected = False - unreal.log_error(f"Ayon: cannot load Ayon integration [ {exc} ]") - -if ayon_detected: - install_host(ayon_host) - - -@unreal.uclass() -class AyonIntegration(unreal.AyonPythonBridge): - @unreal.ufunction(override=True) - def RunInPython_Popup(self): - unreal.log_warning("Ayon: showing tools popup") - if ayon_detected: - ayon_host.show_tools_popup() - - @unreal.ufunction(override=True) - def RunInPython_Dialog(self): - unreal.log_warning("Ayon: showing tools dialog") - if ayon_detected: - ayon_host.show_tools_dialog() diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/README.md b/openpype/hosts/unreal/integration/UE_5.0/Ayon/README.md deleted file mode 100644 index 865c8cafea..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Ayon Unreal Integration plugin - UE 5.0 - -This is plugin for Unreal Editor, creating menu for [Ayon](https://github.com/ynput/OpenPype) tools to run. diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Resources/ayon128.png b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Resources/ayon128.png deleted file mode 100644 index 799d849aa3163ecb16be39c641a6ac30324906b9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2358 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 D;gw7~ diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Resources/ayon512.png b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Resources/ayon512.png deleted file mode 100644 index 990d5917e232a0644820428fb2790943de5ffaa4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16705 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 - FAyonCommands::Get().AyonTools, - FExecuteAction::CreateRaw(this, &FAyonModule::MenuPopup), - FCanExecuteAction()); - PluginCommands->MapAction( - FAyonCommands::Get().AyonToolsDialog, - FExecuteAction::CreateRaw(this, &FAyonModule::MenuDialog), - FCanExecuteAction()); - - UToolMenus::RegisterStartupCallback( - FSimpleMulticastDelegate::FDelegate::CreateRaw(this, &FAyonModule::RegisterMenus)); - - RegisterSettings(); -} - -void FAyonModule::ShutdownModule() -{ - UToolMenus::UnRegisterStartupCallback(this); - - UToolMenus::UnregisterOwner(this); - - FAyonStyle::Shutdown(); - - FAyonCommands::Unregister(); -} - - -void FAyonModule::RegisterSettings() -{ - ISettingsModule& SettingsModule = FModuleManager::LoadModuleChecked("Settings"); - - // Create the new category - // TODO: After the movement of the plugin from the game to editor, it might be necessary to move this! - ISettingsContainerPtr SettingsContainer = SettingsModule.GetContainer("Project"); - - UAyonSettings* Settings = GetMutableDefault(); - - // Register the settings - ISettingsSectionPtr SettingsSection = SettingsModule.RegisterSettings("Project", "Ayon", "General", - LOCTEXT("RuntimeGeneralSettingsName", - "General"), - LOCTEXT("RuntimeGeneralSettingsDescription", - "Base configuration for Open Pype Module"), - Settings - ); - - // Register the save handler to your settings, you might want to use it to - // validate those or just act to settings changes. - if (SettingsSection.IsValid()) - { - SettingsSection->OnModified().BindRaw(this, &FAyonModule::HandleSettingsSaved); - } -} - -bool FAyonModule::HandleSettingsSaved() -{ - UAyonSettings* Settings = GetMutableDefault(); - bool ResaveSettings = false; - - // You can put any validation code in here and resave the settings in case an invalid - // value has been entered - - if (ResaveSettings) - { - Settings->SaveConfig(); - } - - return true; -} - -void FAyonModule::RegisterMenus() -{ - // Owner will be used for cleanup in call to UToolMenus::UnregisterOwner - FToolMenuOwnerScoped OwnerScoped(this); - - { - UToolMenu* Menu = UToolMenus::Get()->ExtendMenu("LevelEditor.MainMenu.Tools"); - { - // FToolMenuSection& Section = Menu->FindOrAddSection("Ayon"); - FToolMenuSection& Section = Menu->AddSection( - "Ayon", - TAttribute(FText::FromString("Ayon")), - FToolMenuInsert("Programming", EToolMenuInsertType::Before) - ); - Section.AddMenuEntryWithCommandList(FAyonCommands::Get().AyonTools, PluginCommands); - Section.AddMenuEntryWithCommandList(FAyonCommands::Get().AyonToolsDialog, PluginCommands); - } - UToolMenu* ToolbarMenu = UToolMenus::Get()->ExtendMenu("LevelEditor.LevelEditorToolBar.PlayToolBar"); - { - FToolMenuSection& Section = ToolbarMenu->FindOrAddSection("PluginTools"); - { - FToolMenuEntry& Entry = Section.AddEntry( - FToolMenuEntry::InitToolBarButton(FAyonCommands::Get().AyonTools)); - Entry.SetCommandList(PluginCommands); - } - } - } -} - - -void FAyonModule::MenuPopup() -{ - UAyonPythonBridge* bridge = UAyonPythonBridge::Get(); - bridge->RunInPython_Popup(); -} - -void FAyonModule::MenuDialog() -{ - UAyonPythonBridge* bridge = UAyonPythonBridge::Get(); - bridge->RunInPython_Dialog(); -} - -IMPLEMENT_MODULE(FAyonModule, Ayon) diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonAssetContainer.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonAssetContainer.cpp deleted file mode 100644 index 869aa45256..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonAssetContainer.cpp +++ /dev/null @@ -1,113 +0,0 @@ -// Fill out your copyright notice in the Description page of Project Settings. - -#include "AyonAssetContainer.h" -#include "AssetRegistry/AssetRegistryModule.h" -#include "Misc/PackageName.h" -#include "Containers/UnrealString.h" - -UAyonAssetContainer::UAyonAssetContainer(const FObjectInitializer& ObjectInitializer) -: UAssetUserData(ObjectInitializer) -{ - FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); - FString path = UAyonAssetContainer::GetPathName(); - UE_LOG(LogTemp, Warning, TEXT("UAyonAssetContainer %s"), *path); - FARFilter Filter; - Filter.PackagePaths.Add(FName(*path)); - - AssetRegistryModule.Get().OnAssetAdded().AddUObject(this, &UAyonAssetContainer::OnAssetAdded); - AssetRegistryModule.Get().OnAssetRemoved().AddUObject(this, &UAyonAssetContainer::OnAssetRemoved); - AssetRegistryModule.Get().OnAssetRenamed().AddUObject(this, &UAyonAssetContainer::OnAssetRenamed); -} - -void UAyonAssetContainer::OnAssetAdded(const FAssetData& AssetData) -{ - TArray split; - - // get directory of current container - FString selfFullPath = UAyonAssetContainer::GetPathName(); - FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath); - - // get asset path and class - FString assetPath = AssetData.GetFullName(); - FString assetFName = AssetData.ObjectPath.ToString(); - UE_LOG(LogTemp, Log, TEXT("asset name %s"), *assetFName); - // 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 UAyonAssetContainer::OnAssetRemoved(const FAssetData& AssetData) -{ - TArray split; - - // get directory of current container - FString selfFullPath = UAyonAssetContainer::GetPathName(); - FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath); - - // get asset path and class - FString assetPath = AssetData.GetFullName(); - FString assetFName = AssetData.ObjectPath.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 = UAyonAssetContainer::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 UAyonAssetContainer::OnAssetRenamed(const FAssetData& AssetData, const FString& str) -{ - TArray split; - - // get directory of current container - FString selfFullPath = UAyonAssetContainer::GetPathName(); - FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath); - - // get asset path and class - FString assetPath = AssetData.GetFullName(); - FString assetFName = AssetData.ObjectPath.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/Ayon/Source/Ayon/Private/AyonAssetContainerFactory.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonAssetContainerFactory.cpp deleted file mode 100644 index 086fc1036e..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonAssetContainerFactory.cpp +++ /dev/null @@ -1,20 +0,0 @@ -#include "AyonAssetContainerFactory.h" -#include "AyonAssetContainer.h" - -UAyonAssetContainerFactory::UAyonAssetContainerFactory(const FObjectInitializer& ObjectInitializer) - : UFactory(ObjectInitializer) -{ - SupportedClass = UAyonAssetContainer::StaticClass(); - bCreateNew = false; - bEditorImport = true; -} - -UObject* UAyonAssetContainerFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) -{ - UAyonAssetContainer* AssetContainer = NewObject(InParent, Class, Name, Flags); - return AssetContainer; -} - -bool UAyonAssetContainerFactory::ShouldShowInNewMenu() const { - return false; -} diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonCommands.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonCommands.cpp deleted file mode 100644 index 566ee1dcd1..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonCommands.cpp +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#include "AyonCommands.h" - -#define LOCTEXT_NAMESPACE "FAyonModule" - -void FAyonCommands::RegisterCommands() -{ - UI_COMMAND(AyonTools, "Ayon Tools", "Pipeline tools", EUserInterfaceActionType::Button, FInputChord()); - UI_COMMAND(AyonToolsDialog, "Ayon Tools Dialog", "Pipeline tools dialog", EUserInterfaceActionType::Button, FInputChord()); -} - -#undef LOCTEXT_NAMESPACE diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonLib.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonLib.cpp deleted file mode 100644 index 7cfa0c9c30..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonLib.cpp +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#include "AyonLib.h" - -#include "AssetViewUtils.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? - */ - -bool UAyonLib::SetFolderColor(const FString& FolderPath, const FLinearColor& FolderColor, const bool& bForceAdd) -{ - if (AssetViewUtils::DoesFolderExist(FolderPath)) - { - const TSharedPtr LinearColor = MakeShared(FolderColor); - - AssetViewUtils::SaveColor(FolderPath, LinearColor, true); - UE_LOG(LogAssetData, Display, TEXT("A color {%s} has been set to folder \"%s\""), *LinearColor->ToString(), - *FolderPath) - return true; - } - - UE_LOG(LogAssetData, Display, TEXT("Setting a color {%s} to folder \"%s\" has failed! Directory doesn't exist!"), - *FolderColor.ToString(), *FolderPath) - return false; -} - -/** - * Returns all poperties on given object - * @param cls - class - * @return TArray of properties - */ -TArray UAyonLib::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/Ayon/Source/Ayon/Private/AyonPublishInstance.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonPublishInstance.cpp deleted file mode 100644 index 8d34090a15..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonPublishInstance.cpp +++ /dev/null @@ -1,204 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -// Deprecation warning: this is left here just for backwards compatibility -// and will be removed in next versions of Ayon. -#pragma once - -#include "AyonPublishInstance.h" -#include "AssetRegistry/AssetRegistryModule.h" -#include "AssetToolsModule.h" -#include "Framework/Notifications/NotificationManager.h" -#include "AyonLib.h" -#include "AyonSettings.h" -#include "Widgets/Notifications/SNotificationList.h" - - -//Moves all the invalid pointers to the end to prepare them for the shrinking -#define REMOVE_INVALID_ENTRIES(VAR) VAR.CompactStable(); \ - VAR.Shrink(); - -UAyonPublishInstance::UAyonPublishInstance(const FObjectInitializer& ObjectInitializer) - : UPrimaryDataAsset(ObjectInitializer) -{ - const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked< - FAssetRegistryModule>("AssetRegistry"); - - const FPropertyEditorModule& PropertyEditorModule = FModuleManager::LoadModuleChecked( - "PropertyEditor"); - - FString Left, Right; - GetPathName().Split("/" + GetName(), &Left, &Right); - - FARFilter Filter; - Filter.PackagePaths.Emplace(FName(Left)); - - TArray FoundAssets; - AssetRegistryModule.GetRegistry().GetAssets(Filter, FoundAssets); - - for (const FAssetData& AssetData : FoundAssets) - OnAssetCreated(AssetData); - - REMOVE_INVALID_ENTRIES(AssetDataInternal) - REMOVE_INVALID_ENTRIES(AssetDataExternal) - - AssetRegistryModule.Get().OnAssetAdded().AddUObject(this, &UAyonPublishInstance::OnAssetCreated); - AssetRegistryModule.Get().OnAssetRemoved().AddUObject(this, &UAyonPublishInstance::OnAssetRemoved); - AssetRegistryModule.Get().OnAssetUpdated().AddUObject(this, &UAyonPublishInstance::OnAssetUpdated); - -#ifdef WITH_EDITOR - ColorAyonDirs(); -#endif -} - -void UAyonPublishInstance::OnAssetCreated(const FAssetData& InAssetData) -{ - TArray split; - - UObject* Asset = InAssetData.GetAsset(); - - if (!IsValid(Asset)) - { - UE_LOG(LogAssetData, Warning, TEXT("Asset \"%s\" is not valid! Skipping the addition."), - *InAssetData.ObjectPath.ToString()); - return; - } - - const bool result = IsUnderSameDir(Asset) && Cast(Asset) == nullptr; - - if (result) - { - if (AssetDataInternal.Emplace(Asset).IsValidId()) - { - UE_LOG(LogTemp, Log, TEXT("Added an Asset to PublishInstance - Publish Instance: %s, Asset %s"), - *this->GetName(), *Asset->GetName()); - } - } -} - -void UAyonPublishInstance::OnAssetRemoved(const FAssetData& InAssetData) -{ - if (Cast(InAssetData.GetAsset()) == nullptr) - { - if (AssetDataInternal.Contains(nullptr)) - { - AssetDataInternal.Remove(nullptr); - REMOVE_INVALID_ENTRIES(AssetDataInternal) - } - else - { - AssetDataExternal.Remove(nullptr); - REMOVE_INVALID_ENTRIES(AssetDataExternal) - } - } -} - -void UAyonPublishInstance::OnAssetUpdated(const FAssetData& InAssetData) -{ - REMOVE_INVALID_ENTRIES(AssetDataInternal); - REMOVE_INVALID_ENTRIES(AssetDataExternal); -} - -bool UAyonPublishInstance::IsUnderSameDir(const UObject* InAsset) const -{ - FString ThisLeft, ThisRight; - this->GetPathName().Split(this->GetName(), &ThisLeft, &ThisRight); - - return InAsset->GetPathName().StartsWith(ThisLeft); -} - -#ifdef WITH_EDITOR - -void UAyonPublishInstance::ColorAyonDirs() -{ - FString PathName = this->GetPathName(); - - //Check whether the path contains the defined Ayon folder - if (!PathName.Contains(TEXT("Ayon"))) return; - - //Get the base path for open pype - FString PathLeft, PathRight; - PathName.Split(FString("Ayon"), &PathLeft, &PathRight); - - if (PathLeft.IsEmpty() || PathRight.IsEmpty()) - { - UE_LOG(LogAssetData, Error, TEXT("Failed to retrieve the base Ayon directory!")) - return; - } - - PathName.RemoveFromEnd(PathRight, ESearchCase::CaseSensitive); - - //Get the current settings - const UAyonSettings* Settings = GetMutableDefault(); - - //Color the base folder - UAyonLib::SetFolderColor(PathName, Settings->GetFolderFColor(), false); - - //Get Sub paths, iterate through them and color them according to the folder color in UAyonSettings - const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked( - "AssetRegistry"); - - TArray PathList; - - AssetRegistryModule.Get().GetSubPaths(PathName, PathList, true); - - if (PathList.Num() > 0) - { - for (const FString& Path : PathList) - { - UAyonLib::SetFolderColor(Path, Settings->GetFolderFColor(), false); - } - } -} - -void UAyonPublishInstance::SendNotification(const FString& Text) const -{ - FNotificationInfo Info{FText::FromString(Text)}; - - Info.bFireAndForget = true; - Info.bUseLargeFont = false; - Info.bUseThrobber = false; - Info.bUseSuccessFailIcons = false; - Info.ExpireDuration = 4.f; - Info.FadeOutDuration = 2.f; - - FSlateNotificationManager::Get().AddNotification(Info); - - UE_LOG(LogAssetData, Warning, - TEXT( - "Removed duplicated asset from the AssetsDataExternal in Container \"%s\", Asset is already included in the AssetDataInternal!" - ), *GetName() - ) -} - - -void UAyonPublishInstance::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) -{ - Super::PostEditChangeProperty(PropertyChangedEvent); - - if (PropertyChangedEvent.ChangeType == EPropertyChangeType::ValueSet && - PropertyChangedEvent.Property->GetFName() == GET_MEMBER_NAME_CHECKED( - UAyonPublishInstance, AssetDataExternal)) - { - // Check for duplicated assets - for (const auto& Asset : AssetDataInternal) - { - if (AssetDataExternal.Contains(Asset)) - { - AssetDataExternal.Remove(Asset); - return SendNotification( - "You are not allowed to add assets into AssetDataExternal which are already included in AssetDataInternal!"); - } - } - - // Check if no UAyonPublishInstance type assets are included - for (const auto& Asset : AssetDataExternal) - { - if (Cast(Asset.Get()) != nullptr) - { - AssetDataExternal.Remove(Asset); - return SendNotification("You are not allowed to add publish instances!"); - } - } - } -} - -#endif diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonPublishInstanceFactory.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonPublishInstanceFactory.cpp deleted file mode 100644 index f79c428a6d..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonPublishInstanceFactory.cpp +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -// Deprecation warning: this is left here just for backwards compatibility -// and will be removed in next versions of Ayon. -#include "AyonPublishInstanceFactory.h" -#include "AyonPublishInstance.h" - -UAyonPublishInstanceFactory::UAyonPublishInstanceFactory(const FObjectInitializer& ObjectInitializer) - : UFactory(ObjectInitializer) -{ - SupportedClass = UAyonPublishInstance::StaticClass(); - bCreateNew = false; - bEditorImport = true; -} - -UObject* UAyonPublishInstanceFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) -{ - check(InClass->IsChildOf(UAyonPublishInstance::StaticClass())); - return NewObject(InParent, InClass, InName, Flags); -} - -bool UAyonPublishInstanceFactory::ShouldShowInNewMenu() const { - return false; -} diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonPythonBridge.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonPythonBridge.cpp deleted file mode 100644 index 0ed4b2f704..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonPythonBridge.cpp +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#include "AyonPythonBridge.h" - -UAyonPythonBridge* UAyonPythonBridge::Get() -{ - TArray AyonPythonBridgeClasses; - GetDerivedClasses(UAyonPythonBridge::StaticClass(), AyonPythonBridgeClasses); - int32 NumClasses = AyonPythonBridgeClasses.Num(); - if (NumClasses > 0) - { - return Cast(AyonPythonBridgeClasses[NumClasses - 1]->GetDefaultObject()); - } - return nullptr; -}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonSettings.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonSettings.cpp deleted file mode 100644 index da388fbc8f..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonSettings.cpp +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#include "AyonSettings.h" - -#include "Interfaces/IPluginManager.h" -#include "UObject/UObjectGlobals.h" - -/** - * Mainly is used for initializing default values if the DefaultAyonSettings.ini file does not exist in the saved config - */ -UAyonSettings::UAyonSettings(const FObjectInitializer& ObjectInitializer) -{ - - const FString ConfigFilePath = AYON_SETTINGS_FILEPATH; - - // This has to be probably in the future set using the UE Reflection system - FColor Color; - GConfig->GetColor(TEXT("/Script/Ayon.AyonSettings"), TEXT("FolderColor"), Color, ConfigFilePath); - - FolderColor = Color; -} \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonStyle.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonStyle.cpp deleted file mode 100644 index d88df78735..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonStyle.cpp +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#include "AyonStyle.h" -#include "Framework/Application/SlateApplication.h" -#include "Styling/SlateStyleRegistry.h" -#include "Slate/SlateGameResources.h" -#include "Interfaces/IPluginManager.h" -#include "Styling/SlateStyleMacros.h" - -#define RootToContentDir Style->RootToContentDir - -TSharedPtr FAyonStyle::AyonStyleInstance = nullptr; - -void FAyonStyle::Initialize() -{ - if (!AyonStyleInstance.IsValid()) - { - AyonStyleInstance = Create(); - FSlateStyleRegistry::RegisterSlateStyle(*AyonStyleInstance); - } -} - -void FAyonStyle::Shutdown() -{ - FSlateStyleRegistry::UnRegisterSlateStyle(*AyonStyleInstance); - ensure(AyonStyleInstance.IsUnique()); - AyonStyleInstance.Reset(); -} - -FName FAyonStyle::GetStyleSetName() -{ - static FName StyleSetName(TEXT("AyonStyle")); - return StyleSetName; -} - -const FVector2D Icon16x16(16.0f, 16.0f); -const FVector2D Icon20x20(20.0f, 20.0f); -const FVector2D Icon40x40(40.0f, 40.0f); - -TSharedRef< FSlateStyleSet > FAyonStyle::Create() -{ - TSharedRef< FSlateStyleSet > Style = MakeShareable(new FSlateStyleSet("AyonStyle")); - Style->SetContentRoot(IPluginManager::Get().FindPlugin("Ayon")->GetBaseDir() / TEXT("Resources")); - - Style->Set("Ayon.AyonTools", new IMAGE_BRUSH(TEXT("ayon40"), Icon40x40)); - Style->Set("Ayon.AyonToolsDialog", new IMAGE_BRUSH(TEXT("ayon40"), Icon40x40)); - - return Style; -} - -void FAyonStyle::ReloadTextures() -{ - if (FSlateApplication::IsInitialized()) - { - FSlateApplication::Get().GetRenderer()->ReloadTextureResources(); - } -} - -const ISlateStyle& FAyonStyle::Get() -{ - return *AyonStyleInstance; -} diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/Commandlets/AyonActionResult.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/Commandlets/AyonActionResult.cpp deleted file mode 100644 index 2a137e3ed7..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/Commandlets/AyonActionResult.cpp +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#include "Commandlets/AyonActionResult.h" -#include "Logging/Ayon_Log.h" - -EAyon_ActionResult::Type& FAyon_ActionResult::GetStatus() -{ - return Status; -} - -FText& FAyon_ActionResult::GetReason() -{ - return Reason; -} - -FAyon_ActionResult::FAyon_ActionResult():Status(EAyon_ActionResult::Type::Ok) -{ - -} - -FAyon_ActionResult::FAyon_ActionResult(const EAyon_ActionResult::Type& InEnum):Status(InEnum) -{ - TryLog(); -} - -FAyon_ActionResult::FAyon_ActionResult(const EAyon_ActionResult::Type& InEnum, const FText& InReason):Status(InEnum), Reason(InReason) -{ - TryLog(); -}; - -bool FAyon_ActionResult::IsProblem() const -{ - return Status != EAyon_ActionResult::Ok; -} - -void FAyon_ActionResult::TryLog() const -{ - if(IsProblem()) - UE_LOG(LogCommandletAyonGenerateProject, Error, TEXT("%s"), *Reason.ToString()); -} diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp deleted file mode 100644 index ed876c8128..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp +++ /dev/null @@ -1,140 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#include "Commandlets/Implementations/AyonGenerateProjectCommandlet.h" - -#include "GameProjectUtils.h" -#include "AyonConstants.h" -#include "Commandlets/AyonActionResult.h" -#include "ProjectDescriptor.h" - -int32 UAyonGenerateProjectCommandlet::Main(const FString& CommandLineParams) -{ - //Parses command line parameters & creates structure FProjectInformation - const FAyonGenerateProjectParams ParsedParams = FAyonGenerateProjectParams(CommandLineParams); - ProjectInformation = ParsedParams.GenerateUEProjectInformation(); - - //Creates .uproject & other UE files - EVALUATE_Ayon_ACTION_RESULT(TryCreateProject()); - - //Loads created .uproject - EVALUATE_Ayon_ACTION_RESULT(TryLoadProjectDescriptor()); - - //Adds needed plugin to .uproject - AttachPluginsToProjectDescriptor(); - - //Saves .uproject - EVALUATE_Ayon_ACTION_RESULT(TrySave()); - - //When we are here, there should not be problems in generating Unreal Project for Ayon - return 0; -} - - -FAyonGenerateProjectParams::FAyonGenerateProjectParams(): FAyonGenerateProjectParams("") -{ -} - -FAyonGenerateProjectParams::FAyonGenerateProjectParams(const FString& CommandLineParams): CommandLineParams( - CommandLineParams) -{ - UCommandlet::ParseCommandLine(*CommandLineParams, Tokens, Switches); -} - -FProjectInformation FAyonGenerateProjectParams::GenerateUEProjectInformation() const -{ - FProjectInformation ProjectInformation = FProjectInformation(); - ProjectInformation.ProjectFilename = GetProjectFileName(); - - ProjectInformation.bShouldGenerateCode = IsSwitchPresent("GenerateCode"); - - return ProjectInformation; -} - -FString FAyonGenerateProjectParams::TryGetToken(const int32 Index) const -{ - return Tokens.IsValidIndex(Index) ? Tokens[Index] : ""; -} - -FString FAyonGenerateProjectParams::GetProjectFileName() const -{ - return TryGetToken(0); -} - -bool FAyonGenerateProjectParams::IsSwitchPresent(const FString& Switch) const -{ - return INDEX_NONE != Switches.IndexOfByPredicate([&Switch](const FString& Item) -> bool - { - return Item.Equals(Switch); - } - ); -} - - -UAyonGenerateProjectCommandlet::UAyonGenerateProjectCommandlet() -{ - LogToConsole = true; -} - -FAyon_ActionResult UAyonGenerateProjectCommandlet::TryCreateProject() const -{ - FText FailReason; - FText FailLog; - TArray OutCreatedFiles; - - if (!GameProjectUtils::CreateProject(ProjectInformation, FailReason, FailLog, &OutCreatedFiles)) - return FAyon_ActionResult(EAyon_ActionResult::ProjectNotCreated, FailReason); - return FAyon_ActionResult(); -} - -FAyon_ActionResult UAyonGenerateProjectCommandlet::TryLoadProjectDescriptor() -{ - FText FailReason; - const bool bLoaded = ProjectDescriptor.Load(ProjectInformation.ProjectFilename, FailReason); - - return FAyon_ActionResult(bLoaded ? EAyon_ActionResult::Ok : EAyon_ActionResult::ProjectNotLoaded, FailReason); -} - -void UAyonGenerateProjectCommandlet::AttachPluginsToProjectDescriptor() -{ - FPluginReferenceDescriptor AyonPluginDescriptor; - AyonPluginDescriptor.bEnabled = true; - AyonPluginDescriptor.Name = AyonConstants::Ayon_PluginName; - ProjectDescriptor.Plugins.Add(AyonPluginDescriptor); - - FPluginReferenceDescriptor PythonPluginDescriptor; - PythonPluginDescriptor.bEnabled = true; - PythonPluginDescriptor.Name = AyonConstants::PythonScript_PluginName; - ProjectDescriptor.Plugins.Add(PythonPluginDescriptor); - - FPluginReferenceDescriptor SequencerScriptingPluginDescriptor; - SequencerScriptingPluginDescriptor.bEnabled = true; - SequencerScriptingPluginDescriptor.Name = AyonConstants::SequencerScripting_PluginName; - ProjectDescriptor.Plugins.Add(SequencerScriptingPluginDescriptor); - - FPluginReferenceDescriptor MovieRenderPipelinePluginDescriptor; - MovieRenderPipelinePluginDescriptor.bEnabled = true; - MovieRenderPipelinePluginDescriptor.Name = AyonConstants::MovieRenderPipeline_PluginName; - ProjectDescriptor.Plugins.Add(MovieRenderPipelinePluginDescriptor); - - FPluginReferenceDescriptor EditorScriptingPluginDescriptor; - EditorScriptingPluginDescriptor.bEnabled = true; - EditorScriptingPluginDescriptor.Name = AyonConstants::EditorScriptingUtils_PluginName; - ProjectDescriptor.Plugins.Add(EditorScriptingPluginDescriptor); -} - -FAyon_ActionResult UAyonGenerateProjectCommandlet::TrySave() -{ - FText FailReason; - const bool bSaved = ProjectDescriptor.Save(ProjectInformation.ProjectFilename, FailReason); - - return FAyon_ActionResult(bSaved ? EAyon_ActionResult::Ok : EAyon_ActionResult::ProjectNotSaved, FailReason); -} - -FAyonGenerateProjectParams UAyonGenerateProjectCommandlet::ParseParameters(const FString& Params) const -{ - FAyonGenerateProjectParams ParamsResult; - - TArray Tokens, Switches; - ParseCommandLine(*Params, Tokens, Switches); - - return ParamsResult; -} diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp deleted file mode 100644 index 7a65fd0c98..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp +++ /dev/null @@ -1,204 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -// Deprecation warning: this is left here just for backwards compatibility -// and will be removed in next versions of Ayon. -#pragma once - -#include "OpenPypePublishInstance.h" -#include "AssetRegistry/AssetRegistryModule.h" -#include "AssetToolsModule.h" -#include "Framework/Notifications/NotificationManager.h" -#include "AyonLib.h" -#include "AyonSettings.h" -#include "Widgets/Notifications/SNotificationList.h" - - -//Moves all the invalid pointers to the end to prepare them for the shrinking -#define REMOVE_INVALID_ENTRIES(VAR) VAR.CompactStable(); \ - VAR.Shrink(); - -UOpenPypePublishInstance::UOpenPypePublishInstance(const FObjectInitializer& ObjectInitializer) - : UPrimaryDataAsset(ObjectInitializer) -{ - const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked< - FAssetRegistryModule>("AssetRegistry"); - - const FPropertyEditorModule& PropertyEditorModule = FModuleManager::LoadModuleChecked( - "PropertyEditor"); - - FString Left, Right; - GetPathName().Split("/" + GetName(), &Left, &Right); - - FARFilter Filter; - Filter.PackagePaths.Emplace(FName(Left)); - - TArray FoundAssets; - AssetRegistryModule.GetRegistry().GetAssets(Filter, FoundAssets); - - for (const FAssetData& AssetData : FoundAssets) - OnAssetCreated(AssetData); - - REMOVE_INVALID_ENTRIES(AssetDataInternal) - REMOVE_INVALID_ENTRIES(AssetDataExternal) - - AssetRegistryModule.Get().OnAssetAdded().AddUObject(this, &UOpenPypePublishInstance::OnAssetCreated); - AssetRegistryModule.Get().OnAssetRemoved().AddUObject(this, &UOpenPypePublishInstance::OnAssetRemoved); - AssetRegistryModule.Get().OnAssetUpdated().AddUObject(this, &UOpenPypePublishInstance::OnAssetUpdated); - -#ifdef WITH_EDITOR - ColorOpenPypeDirs(); -#endif -} - -void UOpenPypePublishInstance::OnAssetCreated(const FAssetData& InAssetData) -{ - TArray split; - - UObject* Asset = InAssetData.GetAsset(); - - if (!IsValid(Asset)) - { - UE_LOG(LogAssetData, Warning, TEXT("Asset \"%s\" is not valid! Skipping the addition."), - *InAssetData.ObjectPath.ToString()); - return; - } - - const bool result = IsUnderSameDir(Asset) && Cast(Asset) == nullptr; - - if (result) - { - if (AssetDataInternal.Emplace(Asset).IsValidId()) - { - UE_LOG(LogTemp, Log, TEXT("Added an Asset to PublishInstance - Publish Instance: %s, Asset %s"), - *this->GetName(), *Asset->GetName()); - } - } -} - -void UOpenPypePublishInstance::OnAssetRemoved(const FAssetData& InAssetData) -{ - if (Cast(InAssetData.GetAsset()) == nullptr) - { - if (AssetDataInternal.Contains(nullptr)) - { - AssetDataInternal.Remove(nullptr); - REMOVE_INVALID_ENTRIES(AssetDataInternal) - } - else - { - AssetDataExternal.Remove(nullptr); - REMOVE_INVALID_ENTRIES(AssetDataExternal) - } - } -} - -void UOpenPypePublishInstance::OnAssetUpdated(const FAssetData& InAssetData) -{ - REMOVE_INVALID_ENTRIES(AssetDataInternal); - REMOVE_INVALID_ENTRIES(AssetDataExternal); -} - -bool UOpenPypePublishInstance::IsUnderSameDir(const UObject* InAsset) const -{ - FString ThisLeft, ThisRight; - this->GetPathName().Split(this->GetName(), &ThisLeft, &ThisRight); - - return InAsset->GetPathName().StartsWith(ThisLeft); -} - -#ifdef WITH_EDITOR - -void UOpenPypePublishInstance::ColorOpenPypeDirs() -{ - FString PathName = this->GetPathName(); - - //Check whether the path contains the defined OpenPype folder - if (!PathName.Contains(TEXT("OpenPype"))) return; - - //Get the base path for open pype - FString PathLeft, PathRight; - PathName.Split(FString("OpenPype"), &PathLeft, &PathRight); - - if (PathLeft.IsEmpty() || PathRight.IsEmpty()) - { - UE_LOG(LogAssetData, Error, TEXT("Failed to retrieve the base OpenPype directory!")) - return; - } - - PathName.RemoveFromEnd(PathRight, ESearchCase::CaseSensitive); - - //Get the current settings - const UAyonSettings* Settings = GetMutableDefault(); - - //Color the base folder - UAyonLib::SetFolderColor(PathName, Settings->GetFolderFColor(), false); - - //Get Sub paths, iterate through them and color them according to the folder color in UOpenPypeSettings - const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked( - "AssetRegistry"); - - TArray PathList; - - AssetRegistryModule.Get().GetSubPaths(PathName, PathList, true); - - if (PathList.Num() > 0) - { - for (const FString& Path : PathList) - { - UAyonLib::SetFolderColor(Path, Settings->GetFolderFColor(), false); - } - } -} - -void UOpenPypePublishInstance::SendNotification(const FString& Text) const -{ - FNotificationInfo Info{FText::FromString(Text)}; - - Info.bFireAndForget = true; - Info.bUseLargeFont = false; - Info.bUseThrobber = false; - Info.bUseSuccessFailIcons = false; - Info.ExpireDuration = 4.f; - Info.FadeOutDuration = 2.f; - - FSlateNotificationManager::Get().AddNotification(Info); - - UE_LOG(LogAssetData, Warning, - TEXT( - "Removed duplicated asset from the AssetsDataExternal in Container \"%s\", Asset is already included in the AssetDataInternal!" - ), *GetName() - ) -} - - -void UOpenPypePublishInstance::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) -{ - Super::PostEditChangeProperty(PropertyChangedEvent); - - if (PropertyChangedEvent.ChangeType == EPropertyChangeType::ValueSet && - PropertyChangedEvent.Property->GetFName() == GET_MEMBER_NAME_CHECKED( - UOpenPypePublishInstance, AssetDataExternal)) - { - // Check for duplicated assets - for (const auto& Asset : AssetDataInternal) - { - if (AssetDataExternal.Contains(Asset)) - { - AssetDataExternal.Remove(Asset); - return SendNotification( - "You are not allowed to add assets into AssetDataExternal which are already included in AssetDataInternal!"); - } - } - - // Check if no UOpenPypePublishInstance type assets are included - for (const auto& Asset : AssetDataExternal) - { - if (Cast(Asset.Get()) != nullptr) - { - AssetDataExternal.Remove(Asset); - return SendNotification("You are not allowed to add publish instances!"); - } - } - } -} - -#endif diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/Ayon.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/Ayon.h deleted file mode 100644 index bb25430411..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/Ayon.h +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#pragma once - -#include "CoreMinimal.h" - - -class FAyonModule : public IModuleInterface -{ -public: - virtual void StartupModule() override; - virtual void ShutdownModule() override; - -private: - void RegisterMenus(); - void RegisterSettings(); - bool HandleSettingsSaved(); - - void MenuPopup(); - void MenuDialog(); - -private: - TSharedPtr PluginCommands; -}; diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonAssetContainer.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonAssetContainer.h deleted file mode 100644 index d40642b149..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonAssetContainer.h +++ /dev/null @@ -1,34 +0,0 @@ -// 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 "AssetRegistry/AssetData.h" -#include "AyonAssetContainer.generated.h" - -UCLASS(Blueprintable) -class AYON_API UAyonAssetContainer : public UAssetUserData -{ - GENERATED_BODY() - -public: - - UAyonAssetContainer(const FObjectInitializer& ObjectInitalizer); - // ~UAyonAssetContainer(); - - UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Assets") - 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/Ayon/Source/Ayon/Public/AyonAssetContainerFactory.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonAssetContainerFactory.h deleted file mode 100644 index da424cde2e..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonAssetContainerFactory.h +++ /dev/null @@ -1,18 +0,0 @@ -// Fill out your copyright notice in the Description page of Project Settings. - -#pragma once - -#include "CoreMinimal.h" -#include "Factories/Factory.h" -#include "AyonAssetContainerFactory.generated.h" - -UCLASS() -class AYON_API UAyonAssetContainerFactory : public UFactory -{ - GENERATED_BODY() - -public: - UAyonAssetContainerFactory(const FObjectInitializer& ObjectInitializer); - virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; - virtual bool ShouldShowInNewMenu() const override; -}; diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonCommands.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonCommands.h deleted file mode 100644 index 9c40dc8241..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonCommands.h +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#pragma once - -#include "CoreMinimal.h" -#include "Framework/Commands/Commands.h" -#include "AyonStyle.h" - -class FAyonCommands : public TCommands -{ -public: - - FAyonCommands() - : TCommands(TEXT("Ayon"), NSLOCTEXT("Contexts", "Ayon", "Ayon Tools"), NAME_None, FAyonStyle::GetStyleSetName()) - { - } - - // TCommands<> interface - virtual void RegisterCommands() override; - -public: - TSharedPtr< FUICommandInfo > AyonTools; - TSharedPtr< FUICommandInfo > AyonToolsDialog; -}; diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonConstants.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonConstants.h deleted file mode 100644 index 5fe7c14360..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonConstants.h +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#pragma once - -namespace AyonConstants -{ - const FString Ayon_PluginName = "Ayon"; - const FString PythonScript_PluginName = "PythonScriptPlugin"; - const FString SequencerScripting_PluginName = "SequencerScripting"; - const FString MovieRenderPipeline_PluginName = "MovieRenderPipeline"; - const FString EditorScriptingUtils_PluginName = "EditorScriptingUtilities"; -} - - diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonLib.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonLib.h deleted file mode 100644 index da83b448fb..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonLib.h +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#pragma once - -#include "AyonLib.generated.h" - - -UCLASS(Blueprintable) -class AYON_API UAyonLib : public UBlueprintFunctionLibrary -{ - - GENERATED_BODY() - -public: - UFUNCTION(BlueprintCallable, Category = Python) - static bool SetFolderColor(const FString& FolderPath, const FLinearColor& FolderColor,const 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/Ayon/Source/Ayon/Public/AyonPublishInstance.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonPublishInstance.h deleted file mode 100644 index c89388036f..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonPublishInstance.h +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -// Deprecation warning: this is left here just for backwards compatibility -// and will be removed in next versions of Ayon. -#pragma once - -#include "AyonPublishInstance.generated.h" - - -UCLASS(Blueprintable) -class AYON_API UAyonPublishInstance : public UPrimaryDataAsset -{ - GENERATED_UCLASS_BODY() - -public: - /** - /** - * Retrieves all the assets which are monitored by the Publish Instance (Monitors assets in the directory which is - * placed in) - * - * @return - Set of UObjects. Careful! They are returning raw pointers. Seems like an issue in UE5 - */ - UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") - TSet GetInternalAssets() const - { - //For some reason it can only return Raw Pointers? Seems like an issue which they haven't fixed. - TSet ResultSet; - - for (const auto& Asset : AssetDataInternal) - ResultSet.Add(Asset.LoadSynchronous()); - - return ResultSet; - } - - /** - * Retrieves all the assets which have been added manually by the Publish Instance - * - * @return - TSet of assets (UObjects). Careful! They are returning raw pointers. Seems like an issue in UE5 - */ - UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") - TSet GetExternalAssets() const - { - //For some reason it can only return Raw Pointers? Seems like an issue which they haven't fixed. - TSet ResultSet; - - for (const auto& Asset : AssetDataExternal) - ResultSet.Add(Asset.LoadSynchronous()); - - return ResultSet; - } - - /** - * Function for returning all the assets in the container combined. - * - * @return Returns all the internal and externally added assets into one set (TSet of UObjects). Careful! They are - * returning raw pointers. Seems like an issue in UE5 - * - * @attention If the bAddExternalAssets variable is false, external assets won't be included! - */ - UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") - TSet GetAllAssets() const - { - const TSet>& IteratedSet = bAddExternalAssets - ? AssetDataInternal.Union(AssetDataExternal) - : AssetDataInternal; - - //Create a new TSet only with raw pointers. - TSet ResultSet; - - for (auto& Asset : IteratedSet) - ResultSet.Add(Asset.LoadSynchronous()); - - return ResultSet; - } - -private: - UPROPERTY(VisibleAnywhere, Category="Assets") - TSet> AssetDataInternal; - - /** - * This property allows exposing the array to include other assets from any other directory than what it's currently - * monitoring. NOTE: that these assets have to be added manually! They are not automatically registered or added! - */ - UPROPERTY(EditAnywhere, Category = "Assets") - bool bAddExternalAssets = false; - - UPROPERTY(EditAnywhere, meta=(EditCondition="bAddExternalAssets"), Category="Assets") - TSet> AssetDataExternal; - - - void OnAssetCreated(const FAssetData& InAssetData); - void OnAssetRemoved(const FAssetData& InAssetData); - void OnAssetUpdated(const FAssetData& InAssetData); - - bool IsUnderSameDir(const UObject* InAsset) const; - -#ifdef WITH_EDITOR - - void ColorAyonDirs(); - - void SendNotification(const FString& Text) const; - virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; - -#endif -}; diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonPublishInstanceFactory.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonPublishInstanceFactory.h deleted file mode 100644 index 3cef8e76b2..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonPublishInstanceFactory.h +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -// Deprecation warning: this is left here just for backwards compatibility -// and will be removed in next versions of Ayon. -#pragma once - -#include "CoreMinimal.h" -#include "Factories/Factory.h" -#include "AyonPublishInstanceFactory.generated.h" - -/** - * - */ -UCLASS() -class AYON_API UAyonPublishInstanceFactory : public UFactory -{ - GENERATED_BODY() - -public: - UAyonPublishInstanceFactory(const FObjectInitializer& ObjectInitializer); - virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; - virtual bool ShouldShowInNewMenu() const override; -}; diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonPythonBridge.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonPythonBridge.h deleted file mode 100644 index 3c429fd7d3..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonPythonBridge.h +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#pragma once -#include "AyonPythonBridge.generated.h" - -UCLASS(Blueprintable) -class UAyonPythonBridge : public UObject -{ - GENERATED_BODY() - -public: - UFUNCTION(BlueprintCallable, Category = Python) - static UAyonPythonBridge* Get(); - - UFUNCTION(BlueprintImplementableEvent, Category = Python) - void RunInPython_Popup() const; - - UFUNCTION(BlueprintImplementableEvent, Category = Python) - void RunInPython_Dialog() const; - -}; diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonSettings.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonSettings.h deleted file mode 100644 index 4f12d1a5f2..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonSettings.h +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#pragma once - -#include "CoreMinimal.h" -#include "UObject/Object.h" -#include "AyonSettings.generated.h" - -#define AYON_SETTINGS_FILEPATH IPluginManager::Get().FindPlugin("Ayon")->GetBaseDir() / TEXT("Config") / TEXT("DefaultAyonSettings.ini") - -UCLASS(Config=AyonSettings, DefaultConfig) -class AYON_API UAyonSettings : public UObject -{ - GENERATED_UCLASS_BODY() - - UFUNCTION(BlueprintCallable, BlueprintPure, Category = Settings) - FColor GetFolderFColor() const - { - return FolderColor; - } - - UFUNCTION(BlueprintCallable, BlueprintPure, Category = Settings) - FLinearColor GetFolderFLinearColor() const - { - return FLinearColor(FolderColor); - } - -protected: - - UPROPERTY(config, EditAnywhere, Category = Folders) - FColor FolderColor = FColor(25,45,223); -}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonStyle.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonStyle.h deleted file mode 100644 index 58f6af656e..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonStyle.h +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#pragma once -#include "CoreMinimal.h" -#include "Styling/SlateStyle.h" - -class FAyonStyle -{ -public: - static void Initialize(); - static void Shutdown(); - static void ReloadTextures(); - static const ISlateStyle& Get(); - static FName GetStyleSetName(); - - -private: - static TSharedRef< class FSlateStyleSet > Create(); - static TSharedPtr< class FSlateStyleSet > AyonStyleInstance; -}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/Commandlets/AyonActionResult.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/Commandlets/AyonActionResult.h deleted file mode 100644 index bb995ec452..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/Commandlets/AyonActionResult.h +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#pragma once - -#include "CoreMinimal.h" -#include "AyonActionResult.generated.h" - -/** - * @brief This macro returns error code when is problem or does nothing when there is no problem. - * @param ActionResult FAyon_ActionResult structure - */ -#define EVALUATE_Ayon_ACTION_RESULT(ActionResult) \ - if(ActionResult.IsProblem()) \ - return ActionResult.GetStatus(); - -/** -* @brief This enum values are humanly readable mapping of error codes. -* Here should be all error codes to be possible find what went wrong. -* TODO: In the future should exists an web document where is mapped error code & what problem occured & how to repair it... -*/ -UENUM() -namespace EAyon_ActionResult -{ - enum Type - { - Ok, - ProjectNotCreated, - ProjectNotLoaded, - ProjectNotSaved, - //....Here insert another values - - //Do not remove! - //Usable for looping through enum values - __Last UMETA(Hidden) - }; -} - - -/** - * @brief This struct holds action result enum and optionally reason of fail - */ -USTRUCT() -struct FAyon_ActionResult -{ - GENERATED_BODY() - -public: - /** @brief Default constructor usable when there is no problem */ - FAyon_ActionResult(); - - /** - * @brief This constructor initializes variables & attempts to log when is error - * @param InEnum Status - */ - FAyon_ActionResult(const EAyon_ActionResult::Type& InEnum); - - /** - * @brief This constructor initializes variables & attempts to log when is error - * @param InEnum Status - * @param InReason Reason of potential fail - */ - FAyon_ActionResult(const EAyon_ActionResult::Type& InEnum, const FText& InReason); - -private: - /** @brief Action status */ - EAyon_ActionResult::Type Status; - - /** @brief Optional reason of fail */ - FText Reason; - -public: - /** - * @brief Checks if there is problematic state - * @return true when status is not equal to EAyon_ActionResult::Ok - */ - bool IsProblem() const; - EAyon_ActionResult::Type& GetStatus(); - FText& GetReason(); - -private: - void TryLog() const; -}; - diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h deleted file mode 100644 index da8e9af661..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#pragma once - - -#include "GameProjectUtils.h" -#include "Commandlets/AyonActionResult.h" -#include "ProjectDescriptor.h" -#include "Commandlets/Commandlet.h" -#include "AyonGenerateProjectCommandlet.generated.h" - -struct FProjectDescriptor; -struct FProjectInformation; - -/** -* @brief Structure which parses command line parameters and generates FProjectInformation -*/ -USTRUCT() -struct FAyonGenerateProjectParams -{ - GENERATED_BODY() - -private: - FString CommandLineParams; - TArray Tokens; - TArray Switches; - -public: - FAyonGenerateProjectParams(); - FAyonGenerateProjectParams(const FString& CommandLineParams); - - FProjectInformation GenerateUEProjectInformation() const; - -private: - FString TryGetToken(const int32 Index) const; - FString GetProjectFileName() const; - - bool IsSwitchPresent(const FString& Switch) const; -}; - -UCLASS() -class AYON_API UAyonGenerateProjectCommandlet : public UCommandlet -{ - GENERATED_BODY() - -private: - FProjectInformation ProjectInformation; - FProjectDescriptor ProjectDescriptor; - -public: - UAyonGenerateProjectCommandlet(); - - virtual int32 Main(const FString& CommandLineParams) override; - -private: - FAyonGenerateProjectParams ParseParameters(const FString& Params) const; - FAyon_ActionResult TryCreateProject() const; - FAyon_ActionResult TryLoadProjectDescriptor(); - void AttachPluginsToProjectDescriptor(); - FAyon_ActionResult TrySave(); -}; - diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/Logging/Ayon_Log.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/Logging/Ayon_Log.h deleted file mode 100644 index 25b33a63e8..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/Logging/Ayon_Log.h +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#pragma once - -DEFINE_LOG_CATEGORY_STATIC(LogCommandletAyonGenerateProject, Log, All); \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h deleted file mode 100644 index 9c0c4a69e5..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -// Deprecation warning: this is left here just for backwards compatibility -// and will be removed in next versions of Ayon. -#pragma once - -#include "OpenPypePublishInstance.generated.h" - - -UCLASS(Blueprintable) -class AYON_API UOpenPypePublishInstance : public UPrimaryDataAsset -{ - GENERATED_UCLASS_BODY() - -public: - /** - /** - * Retrieves all the assets which are monitored by the Publish Instance (Monitors assets in the directory which is - * placed in) - * - * @return - Set of UObjects. Careful! They are returning raw pointers. Seems like an issue in UE5 - */ - UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") - TSet GetInternalAssets() const - { - //For some reason it can only return Raw Pointers? Seems like an issue which they haven't fixed. - TSet ResultSet; - - for (const auto& Asset : AssetDataInternal) - ResultSet.Add(Asset.LoadSynchronous()); - - return ResultSet; - } - - /** - * Retrieves all the assets which have been added manually by the Publish Instance - * - * @return - TSet of assets (UObjects). Careful! They are returning raw pointers. Seems like an issue in UE5 - */ - UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") - TSet GetExternalAssets() const - { - //For some reason it can only return Raw Pointers? Seems like an issue which they haven't fixed. - TSet ResultSet; - - for (const auto& Asset : AssetDataExternal) - ResultSet.Add(Asset.LoadSynchronous()); - - return ResultSet; - } - - /** - * Function for returning all the assets in the container combined. - * - * @return Returns all the internal and externally added assets into one set (TSet of UObjects). Careful! They are - * returning raw pointers. Seems like an issue in UE5 - * - * @attention If the bAddExternalAssets variable is false, external assets won't be included! - */ - UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") - TSet GetAllAssets() const - { - const TSet>& IteratedSet = bAddExternalAssets - ? AssetDataInternal.Union(AssetDataExternal) - : AssetDataInternal; - - //Create a new TSet only with raw pointers. - TSet ResultSet; - - for (auto& Asset : IteratedSet) - ResultSet.Add(Asset.LoadSynchronous()); - - return ResultSet; - } - -private: - UPROPERTY(VisibleAnywhere, Category="Assets") - TSet> AssetDataInternal; - - /** - * This property allows exposing the array to include other assets from any other directory than what it's currently - * monitoring. NOTE: that these assets have to be added manually! They are not automatically registered or added! - */ - UPROPERTY(EditAnywhere, Category = "Assets") - bool bAddExternalAssets = false; - - UPROPERTY(EditAnywhere, meta=(EditCondition="bAddExternalAssets"), Category="Assets") - TSet> AssetDataExternal; - - - void OnAssetCreated(const FAssetData& InAssetData); - void OnAssetRemoved(const FAssetData& InAssetData); - void OnAssetUpdated(const FAssetData& InAssetData); - - bool IsUnderSameDir(const UObject* InAsset) const; - -#ifdef WITH_EDITOR - - void ColorOpenPypeDirs(); - - void SendNotification(const FString& Text) const; - virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; - -#endif -}; diff --git a/openpype/hosts/unreal/integration/UE_5.0/BuildPlugin_5-0.bat b/openpype/hosts/unreal/integration/UE_5.0/BuildPlugin_5-0.bat deleted file mode 100644 index 473c248cbe..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/BuildPlugin_5-0.bat +++ /dev/null @@ -1 +0,0 @@ -"C:\Program Files\Epic Games\UE_5.0\Engine\Build\BatchFiles\RunUAT.bat" BuildPlugin -plugin="D:\OpenPype\openpype\hosts\unreal\integration\UE_5.0\Ayon\Ayon.uplugin" -Package="D:\BuiltPlugins\5.0" \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/BuildPlugin_5-0_Window.bat b/openpype/hosts/unreal/integration/UE_5.0/BuildPlugin_5-0_Window.bat deleted file mode 100644 index b96de6d6c9..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/BuildPlugin_5-0_Window.bat +++ /dev/null @@ -1 +0,0 @@ -cmd /k "BuildPlugin_5-0.bat" \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/CommandletProject/.gitignore b/openpype/hosts/unreal/integration/UE_5.0/CommandletProject/.gitignore deleted file mode 100644 index 80814ef0a6..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/CommandletProject/.gitignore +++ /dev/null @@ -1,41 +0,0 @@ -# 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 - -/Saved -/DerivedDataCache -/Intermediate -/Binaries -/Content -/Config -/.idea -/.vs \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/CommandletProject/CommandletProject.uproject b/openpype/hosts/unreal/integration/UE_5.0/CommandletProject/CommandletProject.uproject deleted file mode 100644 index 9cf75ebaf2..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/CommandletProject/CommandletProject.uproject +++ /dev/null @@ -1,20 +0,0 @@ -{ - "FileVersion": 3, - "EngineAssociation": "5.0", - "Category": "", - "Description": "", - "Plugins": [ - { - "Name": "ModelingToolsEditorMode", - "Enabled": true, - "TargetAllowList": [ - "Editor" - ] - }, - { - "Name": "Ayon", - "Enabled": true, - "Type": "Editor" - } - ] -} \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/.gitignore b/openpype/hosts/unreal/integration/UE_5.1/Ayon/.gitignore deleted file mode 100644 index b32a6f55e5..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/.gitignore +++ /dev/null @@ -1,35 +0,0 @@ -# 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.1/Ayon/Ayon.uplugin b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Ayon.uplugin deleted file mode 100644 index 70ed8f6b9a..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Ayon.uplugin +++ /dev/null @@ -1,24 +0,0 @@ -{ - "FileVersion": 3, - "Version": 1, - "VersionName": "1.0", - "FriendlyName": "Ayon", - "Description": "Ayon Integration", - "Category": "Ayon.Integration", - "CreatedBy": "Ondrej Samohel", - "CreatedByURL": "https://ayon.ynput.io", - "DocsURL": "https://ayon.ynput.io/docs/artist_hosts_unreal", - "MarketplaceURL": "", - "SupportURL": "https://ynput.io/", - "CanContainContent": true, - "EngineVersion": "5.0", - "IsExperimentalVersion": false, - "Installed": true, - "Modules": [ - { - "Name": "Ayon", - "Type": "Editor", - "LoadingPhase": "Default" - } - ] -} diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Config/DefaultAyonSettings.ini b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Config/DefaultAyonSettings.ini deleted file mode 100644 index 9ad7f55201..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Config/DefaultAyonSettings.ini +++ /dev/null @@ -1,2 +0,0 @@ -[/Script/Ayon.AyonSettings] -FolderColor=(R=91,G=197,B=220,A=255) \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Config/FilterPlugin.ini b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Config/FilterPlugin.ini deleted file mode 100644 index ccebca2f32..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Config/FilterPlugin.ini +++ /dev/null @@ -1,8 +0,0 @@ -[FilterPlugin] -; This section lists additional files which will be packaged along with your plugin. Paths should be listed relative to the root plugin directory, and -; may include "...", "*", and "?" wildcards to match directories, files, and individual characters respectively. -; -; Examples: -; /README.txt -; /Extras/... -; /Binaries/ThirdParty/*.dll diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Content/Python/init_unreal.py b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Content/Python/init_unreal.py deleted file mode 100644 index c0b1d0ce5d..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Content/Python/init_unreal.py +++ /dev/null @@ -1,30 +0,0 @@ -import unreal - -ayon_detected = True -try: - from openpype.pipeline import install_host - from openpype.hosts.unreal.api import UnrealHost - - ayon_host = UnrealHost() -except ImportError as exc: - ayon_host = None - ayon_detected = False - unreal.log_error(f"Ayon: cannot load Ayon integration [ {exc} ]") - -if ayon_detected: - install_host(ayon_host) - - -@unreal.uclass() -class AyonIntegration(unreal.AyonPythonBridge): - @unreal.ufunction(override=True) - def RunInPython_Popup(self): - unreal.log_warning("Ayon: showing tools popup") - if ayon_detected: - ayon_host.show_tools_popup() - - @unreal.ufunction(override=True) - def RunInPython_Dialog(self): - unreal.log_warning("Ayon: showing tools dialog") - if ayon_detected: - ayon_host.show_tools_dialog() diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/README.md b/openpype/hosts/unreal/integration/UE_5.1/Ayon/README.md deleted file mode 100644 index 417d490548..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Ayon Unreal Integration plugin - UE 5.1 - -This is plugin for Unreal Editor, creating menu for [Ayon](https://github.com/ynput/OpenPype) tools to run. diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Resources/ayon128.png b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Resources/ayon128.png deleted file mode 100644 index 799d849aa3163ecb16be39c641a6ac30324906b9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2358 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 D;gw7~ diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Resources/ayon512.png b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Resources/ayon512.png deleted file mode 100644 index 990d5917e232a0644820428fb2790943de5ffaa4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16705 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 - FAyonCommands::Get().AyonTools, - FExecuteAction::CreateRaw(this, &FAyonModule::MenuPopup), - FCanExecuteAction()); - PluginCommands->MapAction( - FAyonCommands::Get().AyonToolsDialog, - FExecuteAction::CreateRaw(this, &FAyonModule::MenuDialog), - FCanExecuteAction()); - - UToolMenus::RegisterStartupCallback( - FSimpleMulticastDelegate::FDelegate::CreateRaw(this, &FAyonModule::RegisterMenus)); - - RegisterSettings(); -} - -void FAyonModule::ShutdownModule() -{ - UToolMenus::UnRegisterStartupCallback(this); - - UToolMenus::UnregisterOwner(this); - - FAyonStyle::Shutdown(); - - FAyonCommands::Unregister(); -} - - -void FAyonModule::RegisterSettings() -{ - ISettingsModule& SettingsModule = FModuleManager::LoadModuleChecked("Settings"); - - // Create the new category - // TODO: After the movement of the plugin from the game to editor, it might be necessary to move this! - ISettingsContainerPtr SettingsContainer = SettingsModule.GetContainer("Project"); - - UAyonSettings* Settings = GetMutableDefault(); - - // Register the settings - ISettingsSectionPtr SettingsSection = SettingsModule.RegisterSettings("Project", "Ayon", "General", - LOCTEXT("RuntimeGeneralSettingsName", - "General"), - LOCTEXT("RuntimeGeneralSettingsDescription", - "Base configuration for Open Pype Module"), - Settings - ); - - // Register the save handler to your settings, you might want to use it to - // validate those or just act to settings changes. - if (SettingsSection.IsValid()) - { - SettingsSection->OnModified().BindRaw(this, &FAyonModule::HandleSettingsSaved); - } -} - -bool FAyonModule::HandleSettingsSaved() -{ - UAyonSettings* Settings = GetMutableDefault(); - bool ResaveSettings = false; - - // You can put any validation code in here and resave the settings in case an invalid - // value has been entered - - if (ResaveSettings) - { - Settings->SaveConfig(); - } - - return true; -} - -void FAyonModule::RegisterMenus() -{ - // Owner will be used for cleanup in call to UToolMenus::UnregisterOwner - FToolMenuOwnerScoped OwnerScoped(this); - - { - UToolMenu* Menu = UToolMenus::Get()->ExtendMenu("LevelEditor.MainMenu.Tools"); - { - // FToolMenuSection& Section = Menu->FindOrAddSection("Ayon"); - FToolMenuSection& Section = Menu->AddSection( - "Ayon", - TAttribute(FText::FromString("Ayon")), - FToolMenuInsert("Programming", EToolMenuInsertType::Before) - ); - Section.AddMenuEntryWithCommandList(FAyonCommands::Get().AyonTools, PluginCommands); - Section.AddMenuEntryWithCommandList(FAyonCommands::Get().AyonToolsDialog, PluginCommands); - } - UToolMenu* ToolbarMenu = UToolMenus::Get()->ExtendMenu("LevelEditor.LevelEditorToolBar.PlayToolBar"); - { - FToolMenuSection& Section = ToolbarMenu->FindOrAddSection("PluginTools"); - { - FToolMenuEntry& Entry = Section.AddEntry( - FToolMenuEntry::InitToolBarButton(FAyonCommands::Get().AyonTools)); - Entry.SetCommandList(PluginCommands); - } - } - } -} - - -void FAyonModule::MenuPopup() -{ - UAyonPythonBridge* bridge = UAyonPythonBridge::Get(); - bridge->RunInPython_Popup(); -} - -void FAyonModule::MenuDialog() -{ - UAyonPythonBridge* bridge = UAyonPythonBridge::Get(); - bridge->RunInPython_Dialog(); -} - -IMPLEMENT_MODULE(FAyonModule, Ayon) diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonAssetContainer.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonAssetContainer.cpp deleted file mode 100644 index 3022757dc8..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonAssetContainer.cpp +++ /dev/null @@ -1,113 +0,0 @@ -// Fill out your copyright notice in the Description page of Project Settings. - -#include "AyonAssetContainer.h" -#include "AssetRegistry/AssetRegistryModule.h" -#include "Misc/PackageName.h" -#include "Containers/UnrealString.h" - -UAyonAssetContainer::UAyonAssetContainer(const FObjectInitializer& ObjectInitializer) -: UAssetUserData(ObjectInitializer) -{ - FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); - FString path = UAyonAssetContainer::GetPathName(); - UE_LOG(LogTemp, Warning, TEXT("UAyonAssetContainer %s"), *path); - FARFilter Filter; - Filter.PackagePaths.Add(FName(*path)); - - AssetRegistryModule.Get().OnAssetAdded().AddUObject(this, &UAyonAssetContainer::OnAssetAdded); - AssetRegistryModule.Get().OnAssetRemoved().AddUObject(this, &UAyonAssetContainer::OnAssetRemoved); - AssetRegistryModule.Get().OnAssetRenamed().AddUObject(this, &UAyonAssetContainer::OnAssetRenamed); -} - -void UAyonAssetContainer::OnAssetAdded(const FAssetData& AssetData) -{ - TArray split; - - // get directory of current container - FString selfFullPath = UAyonAssetContainer::GetPathName(); - FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath); - - // get asset path and class - FString assetPath = AssetData.GetFullName(); - FString assetFName = AssetData.GetObjectPathString(); - UE_LOG(LogTemp, Log, TEXT("asset name %s"), *assetFName); - // 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 UAyonAssetContainer::OnAssetRemoved(const FAssetData& AssetData) -{ - TArray split; - - // get directory of current container - FString selfFullPath = UAyonAssetContainer::GetPathName(); - FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath); - - // get asset path and class - FString assetPath = AssetData.GetFullName(); - FString assetFName = AssetData.GetObjectPathString(); - - // 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 = UAyonAssetContainer::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 UAyonAssetContainer::OnAssetRenamed(const FAssetData& AssetData, const FString& str) -{ - TArray split; - - // get directory of current container - FString selfFullPath = UAyonAssetContainer::GetPathName(); - FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath); - - // get asset path and class - FString assetPath = AssetData.GetFullName(); - FString assetFName = AssetData.GetObjectPathString(); - - // 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.1/Ayon/Source/Ayon/Private/AyonAssetContainerFactory.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonAssetContainerFactory.cpp deleted file mode 100644 index 086fc1036e..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonAssetContainerFactory.cpp +++ /dev/null @@ -1,20 +0,0 @@ -#include "AyonAssetContainerFactory.h" -#include "AyonAssetContainer.h" - -UAyonAssetContainerFactory::UAyonAssetContainerFactory(const FObjectInitializer& ObjectInitializer) - : UFactory(ObjectInitializer) -{ - SupportedClass = UAyonAssetContainer::StaticClass(); - bCreateNew = false; - bEditorImport = true; -} - -UObject* UAyonAssetContainerFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) -{ - UAyonAssetContainer* AssetContainer = NewObject(InParent, Class, Name, Flags); - return AssetContainer; -} - -bool UAyonAssetContainerFactory::ShouldShowInNewMenu() const { - return false; -} diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonCommands.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonCommands.cpp deleted file mode 100644 index 566ee1dcd1..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonCommands.cpp +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#include "AyonCommands.h" - -#define LOCTEXT_NAMESPACE "FAyonModule" - -void FAyonCommands::RegisterCommands() -{ - UI_COMMAND(AyonTools, "Ayon Tools", "Pipeline tools", EUserInterfaceActionType::Button, FInputChord()); - UI_COMMAND(AyonToolsDialog, "Ayon Tools Dialog", "Pipeline tools dialog", EUserInterfaceActionType::Button, FInputChord()); -} - -#undef LOCTEXT_NAMESPACE diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonLib.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonLib.cpp deleted file mode 100644 index 7cfa0c9c30..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonLib.cpp +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#include "AyonLib.h" - -#include "AssetViewUtils.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? - */ - -bool UAyonLib::SetFolderColor(const FString& FolderPath, const FLinearColor& FolderColor, const bool& bForceAdd) -{ - if (AssetViewUtils::DoesFolderExist(FolderPath)) - { - const TSharedPtr LinearColor = MakeShared(FolderColor); - - AssetViewUtils::SaveColor(FolderPath, LinearColor, true); - UE_LOG(LogAssetData, Display, TEXT("A color {%s} has been set to folder \"%s\""), *LinearColor->ToString(), - *FolderPath) - return true; - } - - UE_LOG(LogAssetData, Display, TEXT("Setting a color {%s} to folder \"%s\" has failed! Directory doesn't exist!"), - *FolderColor.ToString(), *FolderPath) - return false; -} - -/** - * Returns all poperties on given object - * @param cls - class - * @return TArray of properties - */ -TArray UAyonLib::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.1/Ayon/Source/Ayon/Private/AyonPublishInstance.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonPublishInstance.cpp deleted file mode 100644 index d1b47a19d4..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonPublishInstance.cpp +++ /dev/null @@ -1,204 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -// Deprecation warning: this is left here just for backwards compatibility -// and will be removed in next versions of Ayon. -#pragma once - -#include "AyonPublishInstance.h" -#include "AssetRegistry/AssetRegistryModule.h" -#include "AssetToolsModule.h" -#include "Framework/Notifications/NotificationManager.h" -#include "AyonLib.h" -#include "AyonSettings.h" -#include "Widgets/Notifications/SNotificationList.h" - - -//Moves all the invalid pointers to the end to prepare them for the shrinking -#define REMOVE_INVALID_ENTRIES(VAR) VAR.CompactStable(); \ - VAR.Shrink(); - -UAyonPublishInstance::UAyonPublishInstance(const FObjectInitializer& ObjectInitializer) - : UPrimaryDataAsset(ObjectInitializer) -{ - const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked< - FAssetRegistryModule>("AssetRegistry"); - - const FPropertyEditorModule& PropertyEditorModule = FModuleManager::LoadModuleChecked( - "PropertyEditor"); - - FString Left, Right; - GetPathName().Split("/" + GetName(), &Left, &Right); - - FARFilter Filter; - Filter.PackagePaths.Emplace(FName(Left)); - - TArray FoundAssets; - AssetRegistryModule.GetRegistry().GetAssets(Filter, FoundAssets); - - for (const FAssetData& AssetData : FoundAssets) - OnAssetCreated(AssetData); - - REMOVE_INVALID_ENTRIES(AssetDataInternal) - REMOVE_INVALID_ENTRIES(AssetDataExternal) - - AssetRegistryModule.Get().OnAssetAdded().AddUObject(this, &UAyonPublishInstance::OnAssetCreated); - AssetRegistryModule.Get().OnAssetRemoved().AddUObject(this, &UAyonPublishInstance::OnAssetRemoved); - AssetRegistryModule.Get().OnAssetUpdated().AddUObject(this, &UAyonPublishInstance::OnAssetUpdated); - -#ifdef WITH_EDITOR - ColorAyonDirs(); -#endif -} - -void UAyonPublishInstance::OnAssetCreated(const FAssetData& InAssetData) -{ - TArray split; - - UObject* Asset = InAssetData.GetAsset(); - - if (!IsValid(Asset)) - { - UE_LOG(LogAssetData, Warning, TEXT("Asset \"%s\" is not valid! Skipping the addition."), - *InAssetData.GetSoftObjectPath().ToString()); - return; - } - - const bool result = IsUnderSameDir(Asset) && Cast(Asset) == nullptr; - - if (result) - { - if (AssetDataInternal.Emplace(Asset).IsValidId()) - { - UE_LOG(LogTemp, Log, TEXT("Added an Asset to PublishInstance - Publish Instance: %s, Asset %s"), - *this->GetName(), *Asset->GetName()); - } - } -} - -void UAyonPublishInstance::OnAssetRemoved(const FAssetData& InAssetData) -{ - if (Cast(InAssetData.GetAsset()) == nullptr) - { - if (AssetDataInternal.Contains(nullptr)) - { - AssetDataInternal.Remove(nullptr); - REMOVE_INVALID_ENTRIES(AssetDataInternal) - } - else - { - AssetDataExternal.Remove(nullptr); - REMOVE_INVALID_ENTRIES(AssetDataExternal) - } - } -} - -void UAyonPublishInstance::OnAssetUpdated(const FAssetData& InAssetData) -{ - REMOVE_INVALID_ENTRIES(AssetDataInternal); - REMOVE_INVALID_ENTRIES(AssetDataExternal); -} - -bool UAyonPublishInstance::IsUnderSameDir(const UObject* InAsset) const -{ - FString ThisLeft, ThisRight; - this->GetPathName().Split(this->GetName(), &ThisLeft, &ThisRight); - - return InAsset->GetPathName().StartsWith(ThisLeft); -} - -#ifdef WITH_EDITOR - -void UAyonPublishInstance::ColorAyonDirs() -{ - FString PathName = this->GetPathName(); - - //Check whether the path contains the defined Ayon folder - if (!PathName.Contains(TEXT("Ayon"))) return; - - //Get the base path for open pype - FString PathLeft, PathRight; - PathName.Split(FString("Ayon"), &PathLeft, &PathRight); - - if (PathLeft.IsEmpty() || PathRight.IsEmpty()) - { - UE_LOG(LogAssetData, Error, TEXT("Failed to retrieve the base Ayon directory!")) - return; - } - - PathName.RemoveFromEnd(PathRight, ESearchCase::CaseSensitive); - - //Get the current settings - const UAyonSettings* Settings = GetMutableDefault(); - - //Color the base folder - UAyonLib::SetFolderColor(PathName, Settings->GetFolderFColor(), false); - - //Get Sub paths, iterate through them and color them according to the folder color in UAyonSettings - const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked( - "AssetRegistry"); - - TArray PathList; - - AssetRegistryModule.Get().GetSubPaths(PathName, PathList, true); - - if (PathList.Num() > 0) - { - for (const FString& Path : PathList) - { - UAyonLib::SetFolderColor(Path, Settings->GetFolderFColor(), false); - } - } -} - -void UAyonPublishInstance::SendNotification(const FString& Text) const -{ - FNotificationInfo Info{FText::FromString(Text)}; - - Info.bFireAndForget = true; - Info.bUseLargeFont = false; - Info.bUseThrobber = false; - Info.bUseSuccessFailIcons = false; - Info.ExpireDuration = 4.f; - Info.FadeOutDuration = 2.f; - - FSlateNotificationManager::Get().AddNotification(Info); - - UE_LOG(LogAssetData, Warning, - TEXT( - "Removed duplicated asset from the AssetsDataExternal in Container \"%s\", Asset is already included in the AssetDataInternal!" - ), *GetName() - ) -} - - -void UAyonPublishInstance::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) -{ - Super::PostEditChangeProperty(PropertyChangedEvent); - - if (PropertyChangedEvent.ChangeType == EPropertyChangeType::ValueSet && - PropertyChangedEvent.Property->GetFName() == GET_MEMBER_NAME_CHECKED( - UAyonPublishInstance, AssetDataExternal)) - { - // Check for duplicated assets - for (const auto& Asset : AssetDataInternal) - { - if (AssetDataExternal.Contains(Asset)) - { - AssetDataExternal.Remove(Asset); - return SendNotification( - "You are not allowed to add assets into AssetDataExternal which are already included in AssetDataInternal!"); - } - } - - // Check if no UAyonPublishInstance type assets are included - for (const auto& Asset : AssetDataExternal) - { - if (Cast(Asset.Get()) != nullptr) - { - AssetDataExternal.Remove(Asset); - return SendNotification("You are not allowed to add publish instances!"); - } - } - } -} - -#endif diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonPublishInstanceFactory.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonPublishInstanceFactory.cpp deleted file mode 100644 index f79c428a6d..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonPublishInstanceFactory.cpp +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -// Deprecation warning: this is left here just for backwards compatibility -// and will be removed in next versions of Ayon. -#include "AyonPublishInstanceFactory.h" -#include "AyonPublishInstance.h" - -UAyonPublishInstanceFactory::UAyonPublishInstanceFactory(const FObjectInitializer& ObjectInitializer) - : UFactory(ObjectInitializer) -{ - SupportedClass = UAyonPublishInstance::StaticClass(); - bCreateNew = false; - bEditorImport = true; -} - -UObject* UAyonPublishInstanceFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) -{ - check(InClass->IsChildOf(UAyonPublishInstance::StaticClass())); - return NewObject(InParent, InClass, InName, Flags); -} - -bool UAyonPublishInstanceFactory::ShouldShowInNewMenu() const { - return false; -} diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonPythonBridge.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonPythonBridge.cpp deleted file mode 100644 index 0ed4b2f704..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonPythonBridge.cpp +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#include "AyonPythonBridge.h" - -UAyonPythonBridge* UAyonPythonBridge::Get() -{ - TArray AyonPythonBridgeClasses; - GetDerivedClasses(UAyonPythonBridge::StaticClass(), AyonPythonBridgeClasses); - int32 NumClasses = AyonPythonBridgeClasses.Num(); - if (NumClasses > 0) - { - return Cast(AyonPythonBridgeClasses[NumClasses - 1]->GetDefaultObject()); - } - return nullptr; -}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonSettings.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonSettings.cpp deleted file mode 100644 index da388fbc8f..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonSettings.cpp +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#include "AyonSettings.h" - -#include "Interfaces/IPluginManager.h" -#include "UObject/UObjectGlobals.h" - -/** - * Mainly is used for initializing default values if the DefaultAyonSettings.ini file does not exist in the saved config - */ -UAyonSettings::UAyonSettings(const FObjectInitializer& ObjectInitializer) -{ - - const FString ConfigFilePath = AYON_SETTINGS_FILEPATH; - - // This has to be probably in the future set using the UE Reflection system - FColor Color; - GConfig->GetColor(TEXT("/Script/Ayon.AyonSettings"), TEXT("FolderColor"), Color, ConfigFilePath); - - FolderColor = Color; -} \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonStyle.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonStyle.cpp deleted file mode 100644 index d88df78735..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonStyle.cpp +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#include "AyonStyle.h" -#include "Framework/Application/SlateApplication.h" -#include "Styling/SlateStyleRegistry.h" -#include "Slate/SlateGameResources.h" -#include "Interfaces/IPluginManager.h" -#include "Styling/SlateStyleMacros.h" - -#define RootToContentDir Style->RootToContentDir - -TSharedPtr FAyonStyle::AyonStyleInstance = nullptr; - -void FAyonStyle::Initialize() -{ - if (!AyonStyleInstance.IsValid()) - { - AyonStyleInstance = Create(); - FSlateStyleRegistry::RegisterSlateStyle(*AyonStyleInstance); - } -} - -void FAyonStyle::Shutdown() -{ - FSlateStyleRegistry::UnRegisterSlateStyle(*AyonStyleInstance); - ensure(AyonStyleInstance.IsUnique()); - AyonStyleInstance.Reset(); -} - -FName FAyonStyle::GetStyleSetName() -{ - static FName StyleSetName(TEXT("AyonStyle")); - return StyleSetName; -} - -const FVector2D Icon16x16(16.0f, 16.0f); -const FVector2D Icon20x20(20.0f, 20.0f); -const FVector2D Icon40x40(40.0f, 40.0f); - -TSharedRef< FSlateStyleSet > FAyonStyle::Create() -{ - TSharedRef< FSlateStyleSet > Style = MakeShareable(new FSlateStyleSet("AyonStyle")); - Style->SetContentRoot(IPluginManager::Get().FindPlugin("Ayon")->GetBaseDir() / TEXT("Resources")); - - Style->Set("Ayon.AyonTools", new IMAGE_BRUSH(TEXT("ayon40"), Icon40x40)); - Style->Set("Ayon.AyonToolsDialog", new IMAGE_BRUSH(TEXT("ayon40"), Icon40x40)); - - return Style; -} - -void FAyonStyle::ReloadTextures() -{ - if (FSlateApplication::IsInitialized()) - { - FSlateApplication::Get().GetRenderer()->ReloadTextureResources(); - } -} - -const ISlateStyle& FAyonStyle::Get() -{ - return *AyonStyleInstance; -} diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/Commandlets/AyonActionResult.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/Commandlets/AyonActionResult.cpp deleted file mode 100644 index 2a137e3ed7..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/Commandlets/AyonActionResult.cpp +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#include "Commandlets/AyonActionResult.h" -#include "Logging/Ayon_Log.h" - -EAyon_ActionResult::Type& FAyon_ActionResult::GetStatus() -{ - return Status; -} - -FText& FAyon_ActionResult::GetReason() -{ - return Reason; -} - -FAyon_ActionResult::FAyon_ActionResult():Status(EAyon_ActionResult::Type::Ok) -{ - -} - -FAyon_ActionResult::FAyon_ActionResult(const EAyon_ActionResult::Type& InEnum):Status(InEnum) -{ - TryLog(); -} - -FAyon_ActionResult::FAyon_ActionResult(const EAyon_ActionResult::Type& InEnum, const FText& InReason):Status(InEnum), Reason(InReason) -{ - TryLog(); -}; - -bool FAyon_ActionResult::IsProblem() const -{ - return Status != EAyon_ActionResult::Ok; -} - -void FAyon_ActionResult::TryLog() const -{ - if(IsProblem()) - UE_LOG(LogCommandletAyonGenerateProject, Error, TEXT("%s"), *Reason.ToString()); -} diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp deleted file mode 100644 index ed876c8128..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp +++ /dev/null @@ -1,140 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#include "Commandlets/Implementations/AyonGenerateProjectCommandlet.h" - -#include "GameProjectUtils.h" -#include "AyonConstants.h" -#include "Commandlets/AyonActionResult.h" -#include "ProjectDescriptor.h" - -int32 UAyonGenerateProjectCommandlet::Main(const FString& CommandLineParams) -{ - //Parses command line parameters & creates structure FProjectInformation - const FAyonGenerateProjectParams ParsedParams = FAyonGenerateProjectParams(CommandLineParams); - ProjectInformation = ParsedParams.GenerateUEProjectInformation(); - - //Creates .uproject & other UE files - EVALUATE_Ayon_ACTION_RESULT(TryCreateProject()); - - //Loads created .uproject - EVALUATE_Ayon_ACTION_RESULT(TryLoadProjectDescriptor()); - - //Adds needed plugin to .uproject - AttachPluginsToProjectDescriptor(); - - //Saves .uproject - EVALUATE_Ayon_ACTION_RESULT(TrySave()); - - //When we are here, there should not be problems in generating Unreal Project for Ayon - return 0; -} - - -FAyonGenerateProjectParams::FAyonGenerateProjectParams(): FAyonGenerateProjectParams("") -{ -} - -FAyonGenerateProjectParams::FAyonGenerateProjectParams(const FString& CommandLineParams): CommandLineParams( - CommandLineParams) -{ - UCommandlet::ParseCommandLine(*CommandLineParams, Tokens, Switches); -} - -FProjectInformation FAyonGenerateProjectParams::GenerateUEProjectInformation() const -{ - FProjectInformation ProjectInformation = FProjectInformation(); - ProjectInformation.ProjectFilename = GetProjectFileName(); - - ProjectInformation.bShouldGenerateCode = IsSwitchPresent("GenerateCode"); - - return ProjectInformation; -} - -FString FAyonGenerateProjectParams::TryGetToken(const int32 Index) const -{ - return Tokens.IsValidIndex(Index) ? Tokens[Index] : ""; -} - -FString FAyonGenerateProjectParams::GetProjectFileName() const -{ - return TryGetToken(0); -} - -bool FAyonGenerateProjectParams::IsSwitchPresent(const FString& Switch) const -{ - return INDEX_NONE != Switches.IndexOfByPredicate([&Switch](const FString& Item) -> bool - { - return Item.Equals(Switch); - } - ); -} - - -UAyonGenerateProjectCommandlet::UAyonGenerateProjectCommandlet() -{ - LogToConsole = true; -} - -FAyon_ActionResult UAyonGenerateProjectCommandlet::TryCreateProject() const -{ - FText FailReason; - FText FailLog; - TArray OutCreatedFiles; - - if (!GameProjectUtils::CreateProject(ProjectInformation, FailReason, FailLog, &OutCreatedFiles)) - return FAyon_ActionResult(EAyon_ActionResult::ProjectNotCreated, FailReason); - return FAyon_ActionResult(); -} - -FAyon_ActionResult UAyonGenerateProjectCommandlet::TryLoadProjectDescriptor() -{ - FText FailReason; - const bool bLoaded = ProjectDescriptor.Load(ProjectInformation.ProjectFilename, FailReason); - - return FAyon_ActionResult(bLoaded ? EAyon_ActionResult::Ok : EAyon_ActionResult::ProjectNotLoaded, FailReason); -} - -void UAyonGenerateProjectCommandlet::AttachPluginsToProjectDescriptor() -{ - FPluginReferenceDescriptor AyonPluginDescriptor; - AyonPluginDescriptor.bEnabled = true; - AyonPluginDescriptor.Name = AyonConstants::Ayon_PluginName; - ProjectDescriptor.Plugins.Add(AyonPluginDescriptor); - - FPluginReferenceDescriptor PythonPluginDescriptor; - PythonPluginDescriptor.bEnabled = true; - PythonPluginDescriptor.Name = AyonConstants::PythonScript_PluginName; - ProjectDescriptor.Plugins.Add(PythonPluginDescriptor); - - FPluginReferenceDescriptor SequencerScriptingPluginDescriptor; - SequencerScriptingPluginDescriptor.bEnabled = true; - SequencerScriptingPluginDescriptor.Name = AyonConstants::SequencerScripting_PluginName; - ProjectDescriptor.Plugins.Add(SequencerScriptingPluginDescriptor); - - FPluginReferenceDescriptor MovieRenderPipelinePluginDescriptor; - MovieRenderPipelinePluginDescriptor.bEnabled = true; - MovieRenderPipelinePluginDescriptor.Name = AyonConstants::MovieRenderPipeline_PluginName; - ProjectDescriptor.Plugins.Add(MovieRenderPipelinePluginDescriptor); - - FPluginReferenceDescriptor EditorScriptingPluginDescriptor; - EditorScriptingPluginDescriptor.bEnabled = true; - EditorScriptingPluginDescriptor.Name = AyonConstants::EditorScriptingUtils_PluginName; - ProjectDescriptor.Plugins.Add(EditorScriptingPluginDescriptor); -} - -FAyon_ActionResult UAyonGenerateProjectCommandlet::TrySave() -{ - FText FailReason; - const bool bSaved = ProjectDescriptor.Save(ProjectInformation.ProjectFilename, FailReason); - - return FAyon_ActionResult(bSaved ? EAyon_ActionResult::Ok : EAyon_ActionResult::ProjectNotSaved, FailReason); -} - -FAyonGenerateProjectParams UAyonGenerateProjectCommandlet::ParseParameters(const FString& Params) const -{ - FAyonGenerateProjectParams ParamsResult; - - TArray Tokens, Switches; - ParseCommandLine(*Params, Tokens, Switches); - - return ParamsResult; -} diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp deleted file mode 100644 index 02a8ac800a..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp +++ /dev/null @@ -1,204 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -// Deprecation warning: this is left here just for backwards compatibility -// and will be removed in next versions of Ayon. -#pragma once - -#include "OpenPypePublishInstance.h" -#include "AssetRegistry/AssetRegistryModule.h" -#include "AssetToolsModule.h" -#include "Framework/Notifications/NotificationManager.h" -#include "AyonLib.h" -#include "AyonSettings.h" -#include "Widgets/Notifications/SNotificationList.h" - - -//Moves all the invalid pointers to the end to prepare them for the shrinking -#define REMOVE_INVALID_ENTRIES(VAR) VAR.CompactStable(); \ - VAR.Shrink(); - -UOpenPypePublishInstance::UOpenPypePublishInstance(const FObjectInitializer& ObjectInitializer) - : UPrimaryDataAsset(ObjectInitializer) -{ - const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked< - FAssetRegistryModule>("AssetRegistry"); - - const FPropertyEditorModule& PropertyEditorModule = FModuleManager::LoadModuleChecked( - "PropertyEditor"); - - FString Left, Right; - GetPathName().Split("/" + GetName(), &Left, &Right); - - FARFilter Filter; - Filter.PackagePaths.Emplace(FName(Left)); - - TArray FoundAssets; - AssetRegistryModule.GetRegistry().GetAssets(Filter, FoundAssets); - - for (const FAssetData& AssetData : FoundAssets) - OnAssetCreated(AssetData); - - REMOVE_INVALID_ENTRIES(AssetDataInternal) - REMOVE_INVALID_ENTRIES(AssetDataExternal) - - AssetRegistryModule.Get().OnAssetAdded().AddUObject(this, &UOpenPypePublishInstance::OnAssetCreated); - AssetRegistryModule.Get().OnAssetRemoved().AddUObject(this, &UOpenPypePublishInstance::OnAssetRemoved); - AssetRegistryModule.Get().OnAssetUpdated().AddUObject(this, &UOpenPypePublishInstance::OnAssetUpdated); - -#ifdef WITH_EDITOR - ColorOpenPypeDirs(); -#endif -} - -void UOpenPypePublishInstance::OnAssetCreated(const FAssetData& InAssetData) -{ - TArray split; - - UObject* Asset = InAssetData.GetAsset(); - - if (!IsValid(Asset)) - { - UE_LOG(LogAssetData, Warning, TEXT("Asset \"%s\" is not valid! Skipping the addition."), - *InAssetData.GetSoftObjectPath().ToString()); - return; - } - - const bool result = IsUnderSameDir(Asset) && Cast(Asset) == nullptr; - - if (result) - { - if (AssetDataInternal.Emplace(Asset).IsValidId()) - { - UE_LOG(LogTemp, Log, TEXT("Added an Asset to PublishInstance - Publish Instance: %s, Asset %s"), - *this->GetName(), *Asset->GetName()); - } - } -} - -void UOpenPypePublishInstance::OnAssetRemoved(const FAssetData& InAssetData) -{ - if (Cast(InAssetData.GetAsset()) == nullptr) - { - if (AssetDataInternal.Contains(nullptr)) - { - AssetDataInternal.Remove(nullptr); - REMOVE_INVALID_ENTRIES(AssetDataInternal) - } - else - { - AssetDataExternal.Remove(nullptr); - REMOVE_INVALID_ENTRIES(AssetDataExternal) - } - } -} - -void UOpenPypePublishInstance::OnAssetUpdated(const FAssetData& InAssetData) -{ - REMOVE_INVALID_ENTRIES(AssetDataInternal); - REMOVE_INVALID_ENTRIES(AssetDataExternal); -} - -bool UOpenPypePublishInstance::IsUnderSameDir(const UObject* InAsset) const -{ - FString ThisLeft, ThisRight; - this->GetPathName().Split(this->GetName(), &ThisLeft, &ThisRight); - - return InAsset->GetPathName().StartsWith(ThisLeft); -} - -#ifdef WITH_EDITOR - -void UOpenPypePublishInstance::ColorOpenPypeDirs() -{ - FString PathName = this->GetPathName(); - - //Check whether the path contains the defined OpenPype folder - if (!PathName.Contains(TEXT("OpenPype"))) return; - - //Get the base path for open pype - FString PathLeft, PathRight; - PathName.Split(FString("OpenPype"), &PathLeft, &PathRight); - - if (PathLeft.IsEmpty() || PathRight.IsEmpty()) - { - UE_LOG(LogAssetData, Error, TEXT("Failed to retrieve the base OpenPype directory!")) - return; - } - - PathName.RemoveFromEnd(PathRight, ESearchCase::CaseSensitive); - - //Get the current settings - const UAyonSettings* Settings = GetMutableDefault(); - - //Color the base folder - UAyonLib::SetFolderColor(PathName, Settings->GetFolderFColor(), false); - - //Get Sub paths, iterate through them and color them according to the folder color in UOpenPypeSettings - const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked( - "AssetRegistry"); - - TArray PathList; - - AssetRegistryModule.Get().GetSubPaths(PathName, PathList, true); - - if (PathList.Num() > 0) - { - for (const FString& Path : PathList) - { - UAyonLib::SetFolderColor(Path, Settings->GetFolderFColor(), false); - } - } -} - -void UOpenPypePublishInstance::SendNotification(const FString& Text) const -{ - FNotificationInfo Info{FText::FromString(Text)}; - - Info.bFireAndForget = true; - Info.bUseLargeFont = false; - Info.bUseThrobber = false; - Info.bUseSuccessFailIcons = false; - Info.ExpireDuration = 4.f; - Info.FadeOutDuration = 2.f; - - FSlateNotificationManager::Get().AddNotification(Info); - - UE_LOG(LogAssetData, Warning, - TEXT( - "Removed duplicated asset from the AssetsDataExternal in Container \"%s\", Asset is already included in the AssetDataInternal!" - ), *GetName() - ) -} - - -void UOpenPypePublishInstance::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) -{ - Super::PostEditChangeProperty(PropertyChangedEvent); - - if (PropertyChangedEvent.ChangeType == EPropertyChangeType::ValueSet && - PropertyChangedEvent.Property->GetFName() == GET_MEMBER_NAME_CHECKED( - UOpenPypePublishInstance, AssetDataExternal)) - { - // Check for duplicated assets - for (const auto& Asset : AssetDataInternal) - { - if (AssetDataExternal.Contains(Asset)) - { - AssetDataExternal.Remove(Asset); - return SendNotification( - "You are not allowed to add assets into AssetDataExternal which are already included in AssetDataInternal!"); - } - } - - // Check if no UOpenPypePublishInstance type assets are included - for (const auto& Asset : AssetDataExternal) - { - if (Cast(Asset.Get()) != nullptr) - { - AssetDataExternal.Remove(Asset); - return SendNotification("You are not allowed to add publish instances!"); - } - } - } -} - -#endif diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Ayon.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Ayon.h deleted file mode 100644 index bb25430411..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Ayon.h +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#pragma once - -#include "CoreMinimal.h" - - -class FAyonModule : public IModuleInterface -{ -public: - virtual void StartupModule() override; - virtual void ShutdownModule() override; - -private: - void RegisterMenus(); - void RegisterSettings(); - bool HandleSettingsSaved(); - - void MenuPopup(); - void MenuDialog(); - -private: - TSharedPtr PluginCommands; -}; diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonAssetContainer.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonAssetContainer.h deleted file mode 100644 index d40642b149..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonAssetContainer.h +++ /dev/null @@ -1,34 +0,0 @@ -// 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 "AssetRegistry/AssetData.h" -#include "AyonAssetContainer.generated.h" - -UCLASS(Blueprintable) -class AYON_API UAyonAssetContainer : public UAssetUserData -{ - GENERATED_BODY() - -public: - - UAyonAssetContainer(const FObjectInitializer& ObjectInitalizer); - // ~UAyonAssetContainer(); - - UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Assets") - 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.1/Ayon/Source/Ayon/Public/AyonAssetContainerFactory.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonAssetContainerFactory.h deleted file mode 100644 index da424cde2e..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonAssetContainerFactory.h +++ /dev/null @@ -1,18 +0,0 @@ -// Fill out your copyright notice in the Description page of Project Settings. - -#pragma once - -#include "CoreMinimal.h" -#include "Factories/Factory.h" -#include "AyonAssetContainerFactory.generated.h" - -UCLASS() -class AYON_API UAyonAssetContainerFactory : public UFactory -{ - GENERATED_BODY() - -public: - UAyonAssetContainerFactory(const FObjectInitializer& ObjectInitializer); - virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; - virtual bool ShouldShowInNewMenu() const override; -}; diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonCommands.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonCommands.h deleted file mode 100644 index 9c40dc8241..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonCommands.h +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#pragma once - -#include "CoreMinimal.h" -#include "Framework/Commands/Commands.h" -#include "AyonStyle.h" - -class FAyonCommands : public TCommands -{ -public: - - FAyonCommands() - : TCommands(TEXT("Ayon"), NSLOCTEXT("Contexts", "Ayon", "Ayon Tools"), NAME_None, FAyonStyle::GetStyleSetName()) - { - } - - // TCommands<> interface - virtual void RegisterCommands() override; - -public: - TSharedPtr< FUICommandInfo > AyonTools; - TSharedPtr< FUICommandInfo > AyonToolsDialog; -}; diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonConstants.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonConstants.h deleted file mode 100644 index 5fe7c14360..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonConstants.h +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#pragma once - -namespace AyonConstants -{ - const FString Ayon_PluginName = "Ayon"; - const FString PythonScript_PluginName = "PythonScriptPlugin"; - const FString SequencerScripting_PluginName = "SequencerScripting"; - const FString MovieRenderPipeline_PluginName = "MovieRenderPipeline"; - const FString EditorScriptingUtils_PluginName = "EditorScriptingUtilities"; -} - - diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonLib.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonLib.h deleted file mode 100644 index da83b448fb..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonLib.h +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#pragma once - -#include "AyonLib.generated.h" - - -UCLASS(Blueprintable) -class AYON_API UAyonLib : public UBlueprintFunctionLibrary -{ - - GENERATED_BODY() - -public: - UFUNCTION(BlueprintCallable, Category = Python) - static bool SetFolderColor(const FString& FolderPath, const FLinearColor& FolderColor,const 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.1/Ayon/Source/Ayon/Public/AyonPublishInstance.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonPublishInstance.h deleted file mode 100644 index c89388036f..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonPublishInstance.h +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -// Deprecation warning: this is left here just for backwards compatibility -// and will be removed in next versions of Ayon. -#pragma once - -#include "AyonPublishInstance.generated.h" - - -UCLASS(Blueprintable) -class AYON_API UAyonPublishInstance : public UPrimaryDataAsset -{ - GENERATED_UCLASS_BODY() - -public: - /** - /** - * Retrieves all the assets which are monitored by the Publish Instance (Monitors assets in the directory which is - * placed in) - * - * @return - Set of UObjects. Careful! They are returning raw pointers. Seems like an issue in UE5 - */ - UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") - TSet GetInternalAssets() const - { - //For some reason it can only return Raw Pointers? Seems like an issue which they haven't fixed. - TSet ResultSet; - - for (const auto& Asset : AssetDataInternal) - ResultSet.Add(Asset.LoadSynchronous()); - - return ResultSet; - } - - /** - * Retrieves all the assets which have been added manually by the Publish Instance - * - * @return - TSet of assets (UObjects). Careful! They are returning raw pointers. Seems like an issue in UE5 - */ - UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") - TSet GetExternalAssets() const - { - //For some reason it can only return Raw Pointers? Seems like an issue which they haven't fixed. - TSet ResultSet; - - for (const auto& Asset : AssetDataExternal) - ResultSet.Add(Asset.LoadSynchronous()); - - return ResultSet; - } - - /** - * Function for returning all the assets in the container combined. - * - * @return Returns all the internal and externally added assets into one set (TSet of UObjects). Careful! They are - * returning raw pointers. Seems like an issue in UE5 - * - * @attention If the bAddExternalAssets variable is false, external assets won't be included! - */ - UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") - TSet GetAllAssets() const - { - const TSet>& IteratedSet = bAddExternalAssets - ? AssetDataInternal.Union(AssetDataExternal) - : AssetDataInternal; - - //Create a new TSet only with raw pointers. - TSet ResultSet; - - for (auto& Asset : IteratedSet) - ResultSet.Add(Asset.LoadSynchronous()); - - return ResultSet; - } - -private: - UPROPERTY(VisibleAnywhere, Category="Assets") - TSet> AssetDataInternal; - - /** - * This property allows exposing the array to include other assets from any other directory than what it's currently - * monitoring. NOTE: that these assets have to be added manually! They are not automatically registered or added! - */ - UPROPERTY(EditAnywhere, Category = "Assets") - bool bAddExternalAssets = false; - - UPROPERTY(EditAnywhere, meta=(EditCondition="bAddExternalAssets"), Category="Assets") - TSet> AssetDataExternal; - - - void OnAssetCreated(const FAssetData& InAssetData); - void OnAssetRemoved(const FAssetData& InAssetData); - void OnAssetUpdated(const FAssetData& InAssetData); - - bool IsUnderSameDir(const UObject* InAsset) const; - -#ifdef WITH_EDITOR - - void ColorAyonDirs(); - - void SendNotification(const FString& Text) const; - virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; - -#endif -}; diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonPublishInstanceFactory.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonPublishInstanceFactory.h deleted file mode 100644 index 3cef8e76b2..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonPublishInstanceFactory.h +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -// Deprecation warning: this is left here just for backwards compatibility -// and will be removed in next versions of Ayon. -#pragma once - -#include "CoreMinimal.h" -#include "Factories/Factory.h" -#include "AyonPublishInstanceFactory.generated.h" - -/** - * - */ -UCLASS() -class AYON_API UAyonPublishInstanceFactory : public UFactory -{ - GENERATED_BODY() - -public: - UAyonPublishInstanceFactory(const FObjectInitializer& ObjectInitializer); - virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; - virtual bool ShouldShowInNewMenu() const override; -}; diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonPythonBridge.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonPythonBridge.h deleted file mode 100644 index 3c429fd7d3..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonPythonBridge.h +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#pragma once -#include "AyonPythonBridge.generated.h" - -UCLASS(Blueprintable) -class UAyonPythonBridge : public UObject -{ - GENERATED_BODY() - -public: - UFUNCTION(BlueprintCallable, Category = Python) - static UAyonPythonBridge* Get(); - - UFUNCTION(BlueprintImplementableEvent, Category = Python) - void RunInPython_Popup() const; - - UFUNCTION(BlueprintImplementableEvent, Category = Python) - void RunInPython_Dialog() const; - -}; diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonSettings.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonSettings.h deleted file mode 100644 index 4f12d1a5f2..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonSettings.h +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#pragma once - -#include "CoreMinimal.h" -#include "UObject/Object.h" -#include "AyonSettings.generated.h" - -#define AYON_SETTINGS_FILEPATH IPluginManager::Get().FindPlugin("Ayon")->GetBaseDir() / TEXT("Config") / TEXT("DefaultAyonSettings.ini") - -UCLASS(Config=AyonSettings, DefaultConfig) -class AYON_API UAyonSettings : public UObject -{ - GENERATED_UCLASS_BODY() - - UFUNCTION(BlueprintCallable, BlueprintPure, Category = Settings) - FColor GetFolderFColor() const - { - return FolderColor; - } - - UFUNCTION(BlueprintCallable, BlueprintPure, Category = Settings) - FLinearColor GetFolderFLinearColor() const - { - return FLinearColor(FolderColor); - } - -protected: - - UPROPERTY(config, EditAnywhere, Category = Folders) - FColor FolderColor = FColor(25,45,223); -}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonStyle.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonStyle.h deleted file mode 100644 index 58f6af656e..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonStyle.h +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#pragma once -#include "CoreMinimal.h" -#include "Styling/SlateStyle.h" - -class FAyonStyle -{ -public: - static void Initialize(); - static void Shutdown(); - static void ReloadTextures(); - static const ISlateStyle& Get(); - static FName GetStyleSetName(); - - -private: - static TSharedRef< class FSlateStyleSet > Create(); - static TSharedPtr< class FSlateStyleSet > AyonStyleInstance; -}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Commandlets/AyonActionResult.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Commandlets/AyonActionResult.h deleted file mode 100644 index bb995ec452..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Commandlets/AyonActionResult.h +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#pragma once - -#include "CoreMinimal.h" -#include "AyonActionResult.generated.h" - -/** - * @brief This macro returns error code when is problem or does nothing when there is no problem. - * @param ActionResult FAyon_ActionResult structure - */ -#define EVALUATE_Ayon_ACTION_RESULT(ActionResult) \ - if(ActionResult.IsProblem()) \ - return ActionResult.GetStatus(); - -/** -* @brief This enum values are humanly readable mapping of error codes. -* Here should be all error codes to be possible find what went wrong. -* TODO: In the future should exists an web document where is mapped error code & what problem occured & how to repair it... -*/ -UENUM() -namespace EAyon_ActionResult -{ - enum Type - { - Ok, - ProjectNotCreated, - ProjectNotLoaded, - ProjectNotSaved, - //....Here insert another values - - //Do not remove! - //Usable for looping through enum values - __Last UMETA(Hidden) - }; -} - - -/** - * @brief This struct holds action result enum and optionally reason of fail - */ -USTRUCT() -struct FAyon_ActionResult -{ - GENERATED_BODY() - -public: - /** @brief Default constructor usable when there is no problem */ - FAyon_ActionResult(); - - /** - * @brief This constructor initializes variables & attempts to log when is error - * @param InEnum Status - */ - FAyon_ActionResult(const EAyon_ActionResult::Type& InEnum); - - /** - * @brief This constructor initializes variables & attempts to log when is error - * @param InEnum Status - * @param InReason Reason of potential fail - */ - FAyon_ActionResult(const EAyon_ActionResult::Type& InEnum, const FText& InReason); - -private: - /** @brief Action status */ - EAyon_ActionResult::Type Status; - - /** @brief Optional reason of fail */ - FText Reason; - -public: - /** - * @brief Checks if there is problematic state - * @return true when status is not equal to EAyon_ActionResult::Ok - */ - bool IsProblem() const; - EAyon_ActionResult::Type& GetStatus(); - FText& GetReason(); - -private: - void TryLog() const; -}; - diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h deleted file mode 100644 index da8e9af661..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#pragma once - - -#include "GameProjectUtils.h" -#include "Commandlets/AyonActionResult.h" -#include "ProjectDescriptor.h" -#include "Commandlets/Commandlet.h" -#include "AyonGenerateProjectCommandlet.generated.h" - -struct FProjectDescriptor; -struct FProjectInformation; - -/** -* @brief Structure which parses command line parameters and generates FProjectInformation -*/ -USTRUCT() -struct FAyonGenerateProjectParams -{ - GENERATED_BODY() - -private: - FString CommandLineParams; - TArray Tokens; - TArray Switches; - -public: - FAyonGenerateProjectParams(); - FAyonGenerateProjectParams(const FString& CommandLineParams); - - FProjectInformation GenerateUEProjectInformation() const; - -private: - FString TryGetToken(const int32 Index) const; - FString GetProjectFileName() const; - - bool IsSwitchPresent(const FString& Switch) const; -}; - -UCLASS() -class AYON_API UAyonGenerateProjectCommandlet : public UCommandlet -{ - GENERATED_BODY() - -private: - FProjectInformation ProjectInformation; - FProjectDescriptor ProjectDescriptor; - -public: - UAyonGenerateProjectCommandlet(); - - virtual int32 Main(const FString& CommandLineParams) override; - -private: - FAyonGenerateProjectParams ParseParameters(const FString& Params) const; - FAyon_ActionResult TryCreateProject() const; - FAyon_ActionResult TryLoadProjectDescriptor(); - void AttachPluginsToProjectDescriptor(); - FAyon_ActionResult TrySave(); -}; - diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Logging/Ayon_Log.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Logging/Ayon_Log.h deleted file mode 100644 index 25b33a63e8..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Logging/Ayon_Log.h +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#pragma once - -DEFINE_LOG_CATEGORY_STATIC(LogCommandletAyonGenerateProject, Log, All); \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h deleted file mode 100644 index 9c0c4a69e5..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -// Deprecation warning: this is left here just for backwards compatibility -// and will be removed in next versions of Ayon. -#pragma once - -#include "OpenPypePublishInstance.generated.h" - - -UCLASS(Blueprintable) -class AYON_API UOpenPypePublishInstance : public UPrimaryDataAsset -{ - GENERATED_UCLASS_BODY() - -public: - /** - /** - * Retrieves all the assets which are monitored by the Publish Instance (Monitors assets in the directory which is - * placed in) - * - * @return - Set of UObjects. Careful! They are returning raw pointers. Seems like an issue in UE5 - */ - UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") - TSet GetInternalAssets() const - { - //For some reason it can only return Raw Pointers? Seems like an issue which they haven't fixed. - TSet ResultSet; - - for (const auto& Asset : AssetDataInternal) - ResultSet.Add(Asset.LoadSynchronous()); - - return ResultSet; - } - - /** - * Retrieves all the assets which have been added manually by the Publish Instance - * - * @return - TSet of assets (UObjects). Careful! They are returning raw pointers. Seems like an issue in UE5 - */ - UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") - TSet GetExternalAssets() const - { - //For some reason it can only return Raw Pointers? Seems like an issue which they haven't fixed. - TSet ResultSet; - - for (const auto& Asset : AssetDataExternal) - ResultSet.Add(Asset.LoadSynchronous()); - - return ResultSet; - } - - /** - * Function for returning all the assets in the container combined. - * - * @return Returns all the internal and externally added assets into one set (TSet of UObjects). Careful! They are - * returning raw pointers. Seems like an issue in UE5 - * - * @attention If the bAddExternalAssets variable is false, external assets won't be included! - */ - UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") - TSet GetAllAssets() const - { - const TSet>& IteratedSet = bAddExternalAssets - ? AssetDataInternal.Union(AssetDataExternal) - : AssetDataInternal; - - //Create a new TSet only with raw pointers. - TSet ResultSet; - - for (auto& Asset : IteratedSet) - ResultSet.Add(Asset.LoadSynchronous()); - - return ResultSet; - } - -private: - UPROPERTY(VisibleAnywhere, Category="Assets") - TSet> AssetDataInternal; - - /** - * This property allows exposing the array to include other assets from any other directory than what it's currently - * monitoring. NOTE: that these assets have to be added manually! They are not automatically registered or added! - */ - UPROPERTY(EditAnywhere, Category = "Assets") - bool bAddExternalAssets = false; - - UPROPERTY(EditAnywhere, meta=(EditCondition="bAddExternalAssets"), Category="Assets") - TSet> AssetDataExternal; - - - void OnAssetCreated(const FAssetData& InAssetData); - void OnAssetRemoved(const FAssetData& InAssetData); - void OnAssetUpdated(const FAssetData& InAssetData); - - bool IsUnderSameDir(const UObject* InAsset) const; - -#ifdef WITH_EDITOR - - void ColorOpenPypeDirs(); - - void SendNotification(const FString& Text) const; - virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; - -#endif -}; diff --git a/openpype/hosts/unreal/integration/UE_5.1/BuildPlugin_5-1.bat b/openpype/hosts/unreal/integration/UE_5.1/BuildPlugin_5-1.bat deleted file mode 100644 index 3cc82d54af..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/BuildPlugin_5-1.bat +++ /dev/null @@ -1 +0,0 @@ -"D:\UE_5.1\Engine\Build\BatchFiles\RunUAT.bat" BuildPlugin -plugin="D:\OpenPype\openpype\hosts\unreal\integration\UE_5.1\Ayon\Ayon.uplugin" -Package="D:\BuiltPlugins\5.1" \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.1/BuildPlugin_5-1_Window.bat b/openpype/hosts/unreal/integration/UE_5.1/BuildPlugin_5-1_Window.bat deleted file mode 100644 index e10f2c7add..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/BuildPlugin_5-1_Window.bat +++ /dev/null @@ -1 +0,0 @@ -cmd /k "BuildPlugin_5-1.bat" \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.1/CommandletProject/.gitignore b/openpype/hosts/unreal/integration/UE_5.1/CommandletProject/.gitignore deleted file mode 100644 index 80814ef0a6..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/CommandletProject/.gitignore +++ /dev/null @@ -1,41 +0,0 @@ -# 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 - -/Saved -/DerivedDataCache -/Intermediate -/Binaries -/Content -/Config -/.idea -/.vs \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.1/CommandletProject/CommandletProject.uproject b/openpype/hosts/unreal/integration/UE_5.1/CommandletProject/CommandletProject.uproject deleted file mode 100644 index fe83346624..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/CommandletProject/CommandletProject.uproject +++ /dev/null @@ -1,20 +0,0 @@ -{ - "FileVersion": 3, - "EngineAssociation": "5.1", - "Category": "", - "Description": "", - "Plugins": [ - { - "Name": "ModelingToolsEditorMode", - "Enabled": true, - "TargetAllowList": [ - "Editor" - ] - }, - { - "Name": "Ayon", - "Enabled": true, - "Type": "Editor" - } - ] -} \ No newline at end of file From bc1ce951035917705669b1a66188dfe0109469e9 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 17 May 2023 12:22:21 +0200 Subject: [PATCH 287/360] =?UTF-8?q?=F0=9F=A7=B1=20add=20unreal=20plugin=20?= =?UTF-8?q?repo=20as=20submodule?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitmodules | 5 ++++- openpype/hosts/unreal/integration | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) create mode 160000 openpype/hosts/unreal/integration diff --git a/.gitmodules b/.gitmodules index fe93791c4e..4de92471f7 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,4 +4,7 @@ [submodule "tools/modules/powershell/PSWriteColor"] path = tools/modules/powershell/PSWriteColor - url = https://github.com/EvotecIT/PSWriteColor.git \ No newline at end of file + url = https://github.com/EvotecIT/PSWriteColor.git +[submodule "openpype/hosts/unreal/integration"] + path = openpype/hosts/unreal/integration + url = https://github.com/ynput/ayon-unreal-plugin.git diff --git a/openpype/hosts/unreal/integration b/openpype/hosts/unreal/integration new file mode 160000 index 0000000000..ff15c70077 --- /dev/null +++ b/openpype/hosts/unreal/integration @@ -0,0 +1 @@ +Subproject commit ff15c700771e719cc5f3d561ac5d6f7590623986 From da29890d9be2634d92115c6e960ba066e4e48954 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Wed, 17 May 2023 13:48:34 +0200 Subject: [PATCH 288/360] Update openpype/hosts/fusion/plugins/publish/extract_render_local.py Co-authored-by: Roy Nieterau --- openpype/hosts/fusion/plugins/publish/extract_render_local.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/publish/extract_render_local.py b/openpype/hosts/fusion/plugins/publish/extract_render_local.py index f093f7793f..f801f30577 100644 --- a/openpype/hosts/fusion/plugins/publish/extract_render_local.py +++ b/openpype/hosts/fusion/plugins/publish/extract_render_local.py @@ -83,7 +83,7 @@ class FusionRenderLocal( # Only active instances instance.data.get("publish", True) and # Only render.local instances - "render.local" in instance.data.get("families") + "render.local" in instance.data.get("families", []) ] if key not in context.data: From 6b09504eadb131dfcb78834c6b67ae90102fd8ca Mon Sep 17 00:00:00 2001 From: Zipodod <49460980+Zipodod@users.noreply.github.com> Date: Wed, 17 May 2023 08:14:26 -0400 Subject: [PATCH 289/360] Bugfix/frame variable fix (#4978) * Fix variable name on Max reset frame range * Fix variable name on Maya collect animation * Fix variable name on Nuke reset frame range * Fix lines over max width * Fix error on variable rename * Fix line over max width --------- Co-authored-by: jbeaulieu --- openpype/hosts/max/api/lib.py | 12 ++++++++---- openpype/hosts/maya/api/lib.py | 10 ++++++---- openpype/hosts/nuke/api/lib.py | 8 ++++---- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 1310097f29..d9213863b1 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -245,11 +245,15 @@ def reset_frame_range(fps: bool = True): fps_number = float(data_fps["data"]["fps"]) rt.frameRate = fps_number frame_range = get_frame_range() - frame_start = frame_range["frameStart"] - int(frame_range["handleStart"]) - frame_end = frame_range["frameEnd"] + int(frame_range["handleEnd"]) - frange_cmd = f"animationRange = interval {frame_start} {frame_end}" + frame_start_handle = frame_range["frameStart"] - int( + frame_range["handleStart"] + ) + frame_end_handle = frame_range["frameEnd"] + int(frame_range["handleEnd"]) + frange_cmd = ( + f"animationRange = interval {frame_start_handle} {frame_end_handle}" + ) rt.execute(frange_cmd) - set_render_frame_range(frame_start, frame_end) + set_render_frame_range(frame_start_handle, frame_end_handle) def set_context_setting(): diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index f814187cc1..0bcadbe76e 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -316,11 +316,13 @@ def collect_animation_data(fps=False): # get scene values as defaults frame_start = cmds.playbackOptions(query=True, minTime=True) frame_end = cmds.playbackOptions(query=True, maxTime=True) - handle_start = cmds.playbackOptions(query=True, animationStartTime=True) - handle_end = cmds.playbackOptions(query=True, animationEndTime=True) + frame_start_handle = cmds.playbackOptions( + query=True, animationStartTime=True + ) + frame_end_handle = cmds.playbackOptions(query=True, animationEndTime=True) - handle_start = frame_start - handle_start - handle_end = handle_end - frame_end + handle_start = frame_start - frame_start_handle + handle_end = frame_end_handle - frame_end # build attributes data = OrderedDict() diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 64fa32a383..a439142051 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -2239,13 +2239,13 @@ class WorkfileSettings(object): handle_end = data["handleEnd"] fps = float(data["fps"]) - frame_start = int(data["frameStart"]) - handle_start - frame_end = int(data["frameEnd"]) + handle_end + frame_start_handle = int(data["frameStart"]) - handle_start + frame_end_handle = int(data["frameEnd"]) + handle_end self._root_node["lock_range"].setValue(False) self._root_node["fps"].setValue(fps) - self._root_node["first_frame"].setValue(frame_start) - self._root_node["last_frame"].setValue(frame_end) + self._root_node["first_frame"].setValue(frame_start_handle) + self._root_node["last_frame"].setValue(frame_end_handle) self._root_node["lock_range"].setValue(True) # setting active viewers From 0f80ad01ec243e08e6d7824772861a2b655c64f6 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 17 May 2023 14:16:49 +0200 Subject: [PATCH 290/360] adding deadline settings including Pools --- .../plugins/publish/submit_fusion_deadline.py | 9 ++-- .../defaults/project_settings/deadline.json | 9 ++++ .../schema_project_deadline.json | 44 +++++++++++++++++++ 3 files changed, 56 insertions(+), 6 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py index d51299506c..717391100d 100644 --- a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py @@ -38,10 +38,6 @@ class FusionSubmitDeadline( chunk_size = 1 concurrent_tasks = 1 group = "" - department = "" - limit_groups = {} - env_allowed_keys = [] - env_search_replace_values = {} @classmethod def get_attribute_defs(cls): @@ -173,8 +169,9 @@ class FusionSubmitDeadline( # User, as seen in Monitor "UserName": deadline_user, - # Use a default submission pool for Fusion - "Pool": "fusion", + "Pool": instance.data.get("primaryPool"), + "SecondaryPool": instance.data.get("secondaryPool"), + "Group": self.group, "Plugin": "Fusion", "Frames": "{start}-{end}".format( diff --git a/openpype/settings/defaults/project_settings/deadline.json b/openpype/settings/defaults/project_settings/deadline.json index 3f114025f3..1b8c8397d7 100644 --- a/openpype/settings/defaults/project_settings/deadline.json +++ b/openpype/settings/defaults/project_settings/deadline.json @@ -45,6 +45,15 @@ "chunk_size": 10, "group": "none" }, + "FusionSubmitDeadline": { + "enabled": true, + "optional": false, + "active": true, + "priority": 50, + "chunk_size": 10, + "concurrent_tasks": 1, + "group": "" + }, "NukeSubmitDeadline": { "enabled": true, "optional": false, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json index d8b5e4dc1f..6d59b5a92b 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json @@ -248,6 +248,50 @@ } ] }, + { + "type": "dict", + "collapsible": true, + "key": "FusionSubmitDeadline", + "label": "Fusion submit to Deadline", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "boolean", + "key": "optional", + "label": "Optional" + }, + { + "type": "boolean", + "key": "active", + "label": "Active" + }, + { + "type": "number", + "key": "priority", + "label": "Priority" + }, + { + "type": "number", + "key": "chunk_size", + "label": "Frame per Task" + }, + { + "type": "number", + "key": "concurrent_tasks", + "label": "Number of concurrent tasks" + }, + { + "type": "text", + "key": "group", + "label": "Group Name" + } + ] + }, { "type": "dict", "collapsible": true, From a6059afe869aed7996d44699f87ef30e338da4ae Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 17 May 2023 14:37:37 +0200 Subject: [PATCH 291/360] pr comments also renamed start_handle as it is easily confusable with handles --- .../fusion/plugins/create/create_saver.py | 2 +- .../plugins/publish/collect_instances.py | 41 +++++++++++-------- .../fusion/plugins/publish/collect_render.py | 2 +- 3 files changed, 25 insertions(+), 20 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index d5e77730c8..28917cb27d 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -233,7 +233,7 @@ class CreateSaver(NewCreator): def _get_frame_range_enum(self): frame_range_options = { "asset_db": "From asset database", - "viewer_render_range": "From viewer render in/out", + "render_range": "From viewer render in/out", "comp_range": "From composition timeline" } diff --git a/openpype/hosts/fusion/plugins/publish/collect_instances.py b/openpype/hosts/fusion/plugins/publish/collect_instances.py index 59ff52f5b2..98ea0a34e4 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_instances.py +++ b/openpype/hosts/fusion/plugins/publish/collect_instances.py @@ -20,25 +20,28 @@ class CollectInstanceData(pyblish.api.InstancePlugin): # Include creator attributes directly as instance data creator_attributes = instance.data["creator_attributes"] - frame_range_source = creator_attributes.get("frame_range_source") instance.data.update(creator_attributes) - # get asset frame ranges - start = context.data["frameStart"] - end = context.data["frameEnd"] - handle_start = context.data["handleStart"] - handle_end = context.data["handleEnd"] - start_handle = start - handle_start - end_handle = end + handle_end + frame_range_source = creator_attributes.get("frame_range_source") + instance.data["frame_range_source"] = frame_range_source - if frame_range_source == "viewer_render_range": + if frame_range_source == "asset_db": + # get asset frame ranges + start = context.data["frameStart"] + end = context.data["frameEnd"] + handle_start = context.data["handleStart"] + handle_end = context.data["handleEnd"] + start_with_handle = start - handle_start + end_with_handle = end + handle_end + + if frame_range_source == "render_range": # set comp render frame ranges start = context.data["renderFrameStart"] end = context.data["renderFrameEnd"] handle_start = 0 handle_end = 0 - start_handle = start - end_handle = end + start_with_handle = start + end_with_handle = end if frame_range_source == "comp_range": comp_start = context.data["compFrameStart"] @@ -50,14 +53,16 @@ class CollectInstanceData(pyblish.api.InstancePlugin): end = render_end handle_start = render_start - comp_start handle_end = comp_end - render_end - start_handle = comp_start - end_handle = comp_end + start_with_handle = comp_start + end_with_handle = comp_end # Include start and end render frame in label subset = instance.data["subset"] - label = "{subset} ({start}-{end})".format(subset=subset, - start=int(start), - end=int(end)) + label = "{subset} ({start}-{end})".format( + subset=subset, + start=int(start), + end=int(end) + ) instance.data.update({ "label": label, @@ -65,8 +70,8 @@ class CollectInstanceData(pyblish.api.InstancePlugin): # todo: Allow custom frame range per instance "frameStart": start, "frameEnd": end, - "frameStartHandle": start_handle, - "frameEndHandle": end_handle, + "frameStartHandle": start_with_handle, + "frameEndHandle": end_with_handle, "handleStart": handle_start, "handleEnd": handle_end, "fps": context.data["fps"], diff --git a/openpype/hosts/fusion/plugins/publish/collect_render.py b/openpype/hosts/fusion/plugins/publish/collect_render.py index 6956b566ad..dd6d9c2567 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_render.py +++ b/openpype/hosts/fusion/plugins/publish/collect_render.py @@ -84,7 +84,7 @@ class CollectFusionRender( handleStart=inst.data["handleStart"], handleEnd=inst.data["handleEnd"], ignoreFrameHandleCheck=( - not inst.data.get("viewer_render_range")), + inst.data["frame_range_source"] == "render_range"), frameStep=1, fps=comp_frame_format_prefs.get("Rate"), app_version=comp.GetApp().Version, From 497a97b70d1eef294e0b7d2ea14365f5add380b4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 17 May 2023 14:40:47 +0200 Subject: [PATCH 292/360] pr comment --- openpype/hosts/fusion/plugins/create/create_saver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index 28917cb27d..f1e7791972 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -232,7 +232,7 @@ class CreateSaver(NewCreator): def _get_frame_range_enum(self): frame_range_options = { - "asset_db": "From asset database", + "asset_db": "Current asset context", "render_range": "From viewer render in/out", "comp_range": "From composition timeline" } From fb3c4b613ffd08ac5677a11c2a42f9d217770ca7 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 17 May 2023 14:47:32 +0200 Subject: [PATCH 293/360] improving label --- .../hosts/fusion/plugins/publish/collect_instances.py | 8 ++++++-- openpype/hosts/fusion/plugins/publish/collect_render.py | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/collect_instances.py b/openpype/hosts/fusion/plugins/publish/collect_instances.py index 98ea0a34e4..458f00c7ed 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_instances.py +++ b/openpype/hosts/fusion/plugins/publish/collect_instances.py @@ -58,10 +58,14 @@ class CollectInstanceData(pyblish.api.InstancePlugin): # Include start and end render frame in label subset = instance.data["subset"] - label = "{subset} ({start}-{end})".format( + label = ( + "{subset} ({start}-{end}) [{handle_start}-{handle_end}]" + ).format( subset=subset, start=int(start), - end=int(end) + end=int(end), + handle_start=int(handle_start), + handle_end=int(handle_end) ) instance.data.update({ diff --git a/openpype/hosts/fusion/plugins/publish/collect_render.py b/openpype/hosts/fusion/plugins/publish/collect_render.py index dd6d9c2567..c13d2a0c99 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_render.py +++ b/openpype/hosts/fusion/plugins/publish/collect_render.py @@ -64,7 +64,7 @@ class CollectFusionRender( version=version, time="", source=current_file, - label="{} - {}".format(subset_name, family), + label=inst.data["label"], subset=subset_name, asset=inst.data["asset"], task=task_name, From 089fe88ee104c7f0c3d7d85f66d9d03f3aafbbbf Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 17 May 2023 14:53:09 +0200 Subject: [PATCH 294/360] task is on context --- openpype/hosts/fusion/plugins/publish/collect_render.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/publish/collect_render.py b/openpype/hosts/fusion/plugins/publish/collect_render.py index c13d2a0c99..0a850a4982 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_render.py +++ b/openpype/hosts/fusion/plugins/publish/collect_render.py @@ -51,7 +51,7 @@ class CollectFusionRender( if family != "render": continue - task_name = inst.data.get("task") # legacy + task_name = context.data["task"] tool = inst.data["transientData"]["tool"] instance_families = inst.data.get("families", []) From 163756d74c93ad74b5edf0095ee2279de66fabef Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 17 May 2023 14:58:24 +0200 Subject: [PATCH 295/360] adding comment for ambiguous function call --- openpype/hosts/fusion/plugins/publish/collect_render.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/fusion/plugins/publish/collect_render.py b/openpype/hosts/fusion/plugins/publish/collect_render.py index 0a850a4982..cbb9fea76d 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_render.py +++ b/openpype/hosts/fusion/plugins/publish/collect_render.py @@ -125,6 +125,7 @@ class CollectFusionRender( def post_collecting_action(self): for instance in self._context: if "render.frames" in instance.data.get("families", []): + # adding representation data to the instance self._update_for_frames(instance) def get_expected_files(self, render_instance): From 40426cd69c4c70723254d0b301d7060fc858a1f7 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 17 May 2023 15:03:21 +0200 Subject: [PATCH 296/360] simplifying code --- .../fusion/plugins/publish/collect_render.py | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/collect_render.py b/openpype/hosts/fusion/plugins/publish/collect_render.py index cbb9fea76d..d0b7f1c4ff 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_render.py +++ b/openpype/hosts/fusion/plugins/publish/collect_render.py @@ -32,8 +32,8 @@ class CollectFusionRender( comp = context.data.get("currentComp") comp_frame_format_prefs = comp.GetPrefs("Comp.FrameFormat") - aspect_x = comp_frame_format_prefs.get("AspectX") - aspect_y = comp_frame_format_prefs.get("AspectY") + aspect_x = comp_frame_format_prefs["AspectX"] + aspect_y = comp_frame_format_prefs["AspectY"] instances = [] instances_to_remove = [] @@ -93,14 +93,14 @@ class CollectFusionRender( render_target = inst.data["creator_attributes"]["render_target"] - if render_target == "local": - # for local renders - self._instance_data_local_update( - project_entity, instance, f"render.{render_target}") + # Add render target family + render_target_family = f"render.{render_target}" + if render_target_family not in instance.families: + instance.families.append(render_target_family) - if render_target == "frames": - self._instance_data_local_update( - project_entity, instance, f"render.{render_target}") + # Add render target specific data + if render_target in {"local", "frames"}: + instance.projectEntity = project_entity if render_target == "farm": fam = "render.farm" @@ -205,8 +205,3 @@ class CollectFusionRender( instance.data["representations"].append(repre) return instance - - def _instance_data_local_update(self, project_entity, instance, family): - instance.projectEntity = project_entity - if family not in instance.families: - instance.families.append(family) From 25832ed496f79063c4c2f0c3f9c87eb0e4b227c9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 17 May 2023 15:07:10 +0200 Subject: [PATCH 297/360] collect frame range simplification --- .../publish/collect_comp_frame_range.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py b/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py index 08bdad3120..24a9a92337 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py +++ b/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py @@ -34,16 +34,11 @@ class CollectFusionCompFrameRanges(pyblish.api.ContextPlugin): comp = context.data["currentComp"] # Store comp render ranges - ( - start, end, - global_start, - global_end, - ) = get_comp_render_range(comp) + start, end, global_start, global_end = get_comp_render_range(comp) - data = {} - data["renderFrameStart"] = int(start) - data["renderFrameEnd"] = int(end) - data["compFrameStart"] = int(global_start) - data["compFrameEnd"] = int(global_end) - - context.data.update(data) + context.data.update({ + "renderFrameStart": int(start), + "renderFrameEnd": int(end), + "compFrameStart": int(global_start), + "compFrameEnd": int(global_end) + }) From d39fe870923383c62ea8995b578c9b3b82d8995a Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 17 May 2023 14:58:10 +0100 Subject: [PATCH 298/360] Fix abc loading in Blender --- .../hosts/blender/plugins/load/load_abc.py | 35 +++++-------------- 1 file changed, 9 insertions(+), 26 deletions(-) diff --git a/openpype/hosts/blender/plugins/load/load_abc.py b/openpype/hosts/blender/plugins/load/load_abc.py index 1b2e800769..c1d73eff02 100644 --- a/openpype/hosts/blender/plugins/load/load_abc.py +++ b/openpype/hosts/blender/plugins/load/load_abc.py @@ -65,37 +65,19 @@ class CacheModelLoader(plugin.AssetLoader): imported = lib.get_selection() - empties = [obj for obj in imported if obj.type == 'EMPTY'] - - container = None - - for empty in empties: - if not empty.parent: - container = empty - break - - assert container, "No asset group found" - # Children must be linked before parents, # otherwise the hierarchy will break objects = [] - nodes = list(container.children) - for obj in nodes: + for obj in imported: obj.parent = asset_group - bpy.data.objects.remove(container) - - for obj in nodes: + for obj in imported: objects.append(obj) - nodes.extend(list(obj.children)) + imported.extend(list(obj.children)) objects.reverse() - for obj in objects: - parent.objects.link(obj) - collection.objects.unlink(obj) - for obj in objects: name = obj.name obj.name = f"{group_name}:{name}" @@ -138,13 +120,14 @@ class CacheModelLoader(plugin.AssetLoader): group_name = plugin.asset_name(asset, subset, unique_number) namespace = namespace or f"{asset}_{unique_number}" - avalon_container = bpy.data.collections.get(AVALON_CONTAINERS) - if not avalon_container: - avalon_container = bpy.data.collections.new(name=AVALON_CONTAINERS) - bpy.context.scene.collection.children.link(avalon_container) + avalon_containers = bpy.data.collections.get(AVALON_CONTAINERS) + if not avalon_containers: + avalon_containers = bpy.data.collections.new( + name=AVALON_CONTAINERS) + bpy.context.scene.collection.children.link(avalon_containers) asset_group = bpy.data.objects.new(group_name, object_data=None) - avalon_container.objects.link(asset_group) + avalon_containers.objects.link(asset_group) objects = self._process(libpath, asset_group, group_name) From 9ea2bb0c4d039636fd27142c12a7c0e87119d6cd Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 18 May 2023 15:24:19 +0800 Subject: [PATCH 299/360] use pymxs instead of maxscript for fbx loader --- .../hosts/max/plugins/load/load_camera_fbx.py | 24 +++++++------------ .../hosts/max/plugins/load/load_model_fbx.py | 22 +++++++---------- 2 files changed, 17 insertions(+), 29 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_camera_fbx.py b/openpype/hosts/max/plugins/load/load_camera_fbx.py index ce4dec32a0..9767321e3d 100644 --- a/openpype/hosts/max/plugins/load/load_camera_fbx.py +++ b/openpype/hosts/max/plugins/load/load_camera_fbx.py @@ -20,21 +20,15 @@ class FbxLoader(load.LoaderPlugin): from pymxs import runtime as rt filepath = os.path.normpath(self.fname) - - fbx_import_cmd = ( - f""" - -FBXImporterSetParam "Animation" true -FBXImporterSetParam "Cameras" true -FBXImporterSetParam "AxisConversionMethod" true -FbxExporterSetParam "UpAxis" "Y" -FbxExporterSetParam "Preserveinstances" true - -importFile @"{filepath}" #noPrompt using:FBXIMP - """) - - self.log.debug(f"Executing command: {fbx_import_cmd}") - rt.execute(fbx_import_cmd) + rt.FBXImporterSetParam("Animation", True) + rt.FBXImporterSetParam("Camera", True) + rt.FBXImporterSetParam("AxisConversionMethod", True) + rt.FBXImporterSetParam("UpAxis", "Y") + rt.FBXImporterSetParam("Preserveinstances", True) + rt.importFile( + filepath, + rt.name("noPrompt"), + using=rt.FBXIMP) container = rt.getNodeByName(f"{name}") if not container: diff --git a/openpype/hosts/max/plugins/load/load_model_fbx.py b/openpype/hosts/max/plugins/load/load_model_fbx.py index 7532e3a8a0..bf51e3b191 100644 --- a/openpype/hosts/max/plugins/load/load_model_fbx.py +++ b/openpype/hosts/max/plugins/load/load_model_fbx.py @@ -21,21 +21,15 @@ class FbxModelLoader(load.LoaderPlugin): from pymxs import runtime as rt filepath = os.path.normpath(self.fname) + rt.FBXImporterSetParam("Animation", False) + rt.FBXImporterSetParam("Cameras", False) + rt.FBXImporterSetParam("UpAxis", "Y") + rt.FBXImporterSetParam("Preserveinstances", True) + rt.importFile( + filepath, + rt.name("noPrompt"), + using=rt.FBXIMP) - fbx_import_cmd = ( - f""" - -FBXImporterSetParam "Animation" false -FBXImporterSetParam "Cameras" false -FBXImporterSetParam "AxisConversionMethod" true -FbxExporterSetParam "UpAxis" "Y" -FbxExporterSetParam "Preserveinstances" true - -importFile @"{filepath}" #noPrompt using:FBXIMP - """) - - self.log.debug(f"Executing command: {fbx_import_cmd}") - rt.execute(fbx_import_cmd) container = rt.getNodeByName(f"{name}") if not container: container = rt.container() From 6fd4cbf2998f6d486858d01472d6385c8f2bc809 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 18 May 2023 16:47:58 +0800 Subject: [PATCH 300/360] fix the axis conversion issue --- openpype/hosts/max/plugins/load/load_camera_fbx.py | 1 - openpype/hosts/max/plugins/load/load_model_fbx.py | 1 - 2 files changed, 2 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_camera_fbx.py b/openpype/hosts/max/plugins/load/load_camera_fbx.py index 9767321e3d..0c5dd762cf 100644 --- a/openpype/hosts/max/plugins/load/load_camera_fbx.py +++ b/openpype/hosts/max/plugins/load/load_camera_fbx.py @@ -23,7 +23,6 @@ class FbxLoader(load.LoaderPlugin): rt.FBXImporterSetParam("Animation", True) rt.FBXImporterSetParam("Camera", True) rt.FBXImporterSetParam("AxisConversionMethod", True) - rt.FBXImporterSetParam("UpAxis", "Y") rt.FBXImporterSetParam("Preserveinstances", True) rt.importFile( filepath, diff --git a/openpype/hosts/max/plugins/load/load_model_fbx.py b/openpype/hosts/max/plugins/load/load_model_fbx.py index bf51e3b191..01e6acae12 100644 --- a/openpype/hosts/max/plugins/load/load_model_fbx.py +++ b/openpype/hosts/max/plugins/load/load_model_fbx.py @@ -23,7 +23,6 @@ class FbxModelLoader(load.LoaderPlugin): filepath = os.path.normpath(self.fname) rt.FBXImporterSetParam("Animation", False) rt.FBXImporterSetParam("Cameras", False) - rt.FBXImporterSetParam("UpAxis", "Y") rt.FBXImporterSetParam("Preserveinstances", True) rt.importFile( filepath, From 60efa939a86196749a1dab1f5afe2fbdc01991dc Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 18 May 2023 10:59:13 +0200 Subject: [PATCH 301/360] OP-5714 - allow returning stub with not saved workfile (#4984) Without it is not possible to create first workile. --- openpype/hosts/aftereffects/api/pipeline.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/openpype/hosts/aftereffects/api/pipeline.py b/openpype/hosts/aftereffects/api/pipeline.py index 95f6f3235b..020022e263 100644 --- a/openpype/hosts/aftereffects/api/pipeline.py +++ b/openpype/hosts/aftereffects/api/pipeline.py @@ -60,9 +60,6 @@ class AfterEffectsHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): print("Not connected yet, ignoring") return - if not stub.get_active_document_name(): - return - self._stub = stub return self._stub From 87802d06203ad1cdad22d2aec5112fba1a04f0e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Thu, 18 May 2023 14:39:27 +0200 Subject: [PATCH 302/360] Fix: Download last workfile doesn't work if wf not already downloaded (#4942) --- openpype/modules/sync_server/sync_server.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/modules/sync_server/sync_server.py b/openpype/modules/sync_server/sync_server.py index d1d5c2863d..98065b68a0 100644 --- a/openpype/modules/sync_server/sync_server.py +++ b/openpype/modules/sync_server/sync_server.py @@ -237,8 +237,7 @@ def download_last_published_workfile( last_published_workfile_path = get_representation_path_with_anatomy( workfile_representation, anatomy ) - if (not last_published_workfile_path or - not os.path.exists(last_published_workfile_path)): + if not last_published_workfile_path: return # If representation isn't available on remote site, then return. From 51bba73ebe1801ae1c9443307277d6e0156783a6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 19 May 2023 17:13:53 +0200 Subject: [PATCH 303/360] Unreal: Addon Py2 compatibility (#4994) * fix python 2 compatibility of unreal addon * added a comment --- openpype/hosts/unreal/addon.py | 9 +++++---- openpype/hosts/unreal/lib.py | 1 - 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/unreal/addon.py b/openpype/hosts/unreal/addon.py index 9ded333d7d..1119b5c16c 100644 --- a/openpype/hosts/unreal/addon.py +++ b/openpype/hosts/unreal/addon.py @@ -1,8 +1,5 @@ import os -from pathlib import Path - from openpype.modules import IHostAddon, OpenPypeModule -from .lib import get_compatible_integration UNREAL_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -17,10 +14,14 @@ class UnrealAddon(OpenPypeModule, IHostAddon): def add_implementation_envs(self, env, app): """Modify environments to contain all required for implementation.""" # Set AYON_UNREAL_PLUGIN required for Unreal implementation + # Imports are in this method for Python 2 compatiblity of an addon + from pathlib import Path + + from .lib import get_compatible_integration ue_version = app.name.replace("-", ".") unreal_plugin_path = os.path.join( - UNREAL_ROOT_DIR, "integration", f"UE_{ue_version}", "Ayon" + UNREAL_ROOT_DIR, "integration", "UE_{}".format(ue_version), "Ayon" ) if not Path(unreal_plugin_path).exists(): compatible_versions = get_compatible_integration( diff --git a/openpype/hosts/unreal/lib.py b/openpype/hosts/unreal/lib.py index 821b4daecc..97771472cf 100644 --- a/openpype/hosts/unreal/lib.py +++ b/openpype/hosts/unreal/lib.py @@ -7,7 +7,6 @@ import json from typing import List -import openpype from distutils import dir_util import subprocess import re From 8de85e94a7e42cf12b26b6d813dc375bfb0485b5 Mon Sep 17 00:00:00 2001 From: Sponge96 Date: Fri, 19 May 2023 16:15:09 +0100 Subject: [PATCH 304/360] 3dsmax: Refactored publish plugins to use proper implementation of pymxs (#4988) * refactor: rt.execute(saveNodes) replaces with pymxs function - changed the function to no longer use the selection and instead feed it the nodes directly from get_all_children function. - removed maintained_seclection() as we're no longer overriding the selection of the Max scene. - black also used to format. * refactor: replaced rt.export string with proper pymxs implementation - black used for formatting - moved the general flow around as each function call is now seperate instead of large string * refactor: replaced rt.execute with pymxs implementation * refactor: replaced rt.execute with proper pymxs * refactor: replaced rt.execute where possible * fix: pymxs terrible argument handling - noPrompt doesn't seem to work unless you call rt.name and is also positional - using doesn't work as a string you need to feed it the actual rt object * fix: exportFile to use correct arguments * fix: rt.exportfile args * fix: exportfile args * refactor: replaced rt.execute with proper function * refactor: removed use of rt.execute and replaced with pymxs * refactor: removed configs from maintained_selection() block * refactor: updated black to be 79 charlines --- .../max/plugins/publish/extract_camera_abc.py | 50 ++++++---------- .../max/plugins/publish/extract_camera_fbx.py | 57 +++++++----------- .../plugins/publish/extract_max_scene_raw.py | 38 ++++-------- .../max/plugins/publish/extract_model.py | 54 +++++++---------- .../max/plugins/publish/extract_model_fbx.py | 59 ++++++++----------- .../max/plugins/publish/extract_model_obj.py | 37 ++++++------ .../max/plugins/publish/extract_pointcache.py | 38 +++++------- 7 files changed, 131 insertions(+), 202 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_camera_abc.py b/openpype/hosts/max/plugins/publish/extract_camera_abc.py index 8c23ff9878..6b3bb178a3 100644 --- a/openpype/hosts/max/plugins/publish/extract_camera_abc.py +++ b/openpype/hosts/max/plugins/publish/extract_camera_abc.py @@ -1,18 +1,11 @@ import os import pyblish.api -from openpype.pipeline import ( - publish, - OptionalPyblishPluginMixin -) +from openpype.pipeline import publish, OptionalPyblishPluginMixin from pymxs import runtime as rt -from openpype.hosts.max.api import ( - maintained_selection, - get_all_children -) +from openpype.hosts.max.api import maintained_selection, get_all_children -class ExtractCameraAlembic(publish.Extractor, - OptionalPyblishPluginMixin): +class ExtractCameraAlembic(publish.Extractor, OptionalPyblishPluginMixin): """ Extract Camera with AlembicExport """ @@ -38,38 +31,33 @@ class ExtractCameraAlembic(publish.Extractor, path = os.path.join(stagingdir, filename) # We run the render - self.log.info("Writing alembic '%s' to '%s'" % (filename, - stagingdir)) + self.log.info("Writing alembic '%s' to '%s'" % (filename, stagingdir)) - export_cmd = ( - f""" -AlembicExport.ArchiveType = #ogawa -AlembicExport.CoordinateSystem = #maya -AlembicExport.StartFrame = {start} -AlembicExport.EndFrame = {end} -AlembicExport.CustomAttributes = true - -exportFile @"{path}" #noPrompt selectedOnly:on using:AlembicExport - - """) - - self.log.debug(f"Executing command: {export_cmd}") + rt.AlembicExport.ArchiveType = rt.name("ogawa") + rt.AlembicExport.CoordinateSystem = rt.name("maya") + rt.AlembicExport.StartFrame = start + rt.AlembicExport.EndFrame = end + rt.AlembicExport.CustomAttributes = True with maintained_selection(): # select and export rt.select(get_all_children(rt.getNodeByName(container))) - rt.execute(export_cmd) + rt.exportFile( + path, + rt.name("noPrompt"), + selectedOnly=True, + using=rt.AlembicExport, + ) self.log.info("Performing Extraction ...") if "representations" not in instance.data: instance.data["representations"] = [] representation = { - 'name': 'abc', - 'ext': 'abc', - 'files': filename, + "name": "abc", + "ext": "abc", + "files": filename, "stagingDir": stagingdir, } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s" % (instance.name, - path)) + self.log.info("Extracted instance '%s' to: %s" % (instance.name, path)) diff --git a/openpype/hosts/max/plugins/publish/extract_camera_fbx.py b/openpype/hosts/max/plugins/publish/extract_camera_fbx.py index 7e92f355ed..4b4b349e19 100644 --- a/openpype/hosts/max/plugins/publish/extract_camera_fbx.py +++ b/openpype/hosts/max/plugins/publish/extract_camera_fbx.py @@ -1,18 +1,11 @@ import os import pyblish.api -from openpype.pipeline import ( - publish, - OptionalPyblishPluginMixin -) +from openpype.pipeline import publish, OptionalPyblishPluginMixin from pymxs import runtime as rt -from openpype.hosts.max.api import ( - maintained_selection, - get_all_children -) +from openpype.hosts.max.api import maintained_selection, get_all_children -class ExtractCameraFbx(publish.Extractor, - OptionalPyblishPluginMixin): +class ExtractCameraFbx(publish.Extractor, OptionalPyblishPluginMixin): """ Extract Camera with FbxExporter """ @@ -33,43 +26,35 @@ class ExtractCameraFbx(publish.Extractor, filename = "{name}.fbx".format(**instance.data) filepath = os.path.join(stagingdir, filename) - self.log.info("Writing fbx file '%s' to '%s'" % (filename, - filepath)) + self.log.info("Writing fbx file '%s' to '%s'" % (filename, filepath)) - # Need to export: - # Animation = True - # Cameras = True - # AxisConversionMethod - fbx_export_cmd = ( - f""" - -FBXExporterSetParam "Animation" true -FBXExporterSetParam "Cameras" true -FBXExporterSetParam "AxisConversionMethod" "Animation" -FbxExporterSetParam "UpAxis" "Y" -FbxExporterSetParam "Preserveinstances" true - -exportFile @"{filepath}" #noPrompt selectedOnly:true using:FBXEXP - - """) - - self.log.debug(f"Executing command: {fbx_export_cmd}") + rt.FBXExporterSetParam("Animation", True) + rt.FBXExporterSetParam("Cameras", True) + rt.FBXExporterSetParam("AxisConversionMethod", "Animation") + rt.FBXExporterSetParam("UpAxis", "Y") + rt.FBXExporterSetParam("Preserveinstances", True) with maintained_selection(): # select and export rt.select(get_all_children(rt.getNodeByName(container))) - rt.execute(fbx_export_cmd) + rt.exportFile( + filepath, + rt.name("noPrompt"), + selectedOnly=True, + using=rt.FBXEXP, + ) self.log.info("Performing Extraction ...") if "representations" not in instance.data: instance.data["representations"] = [] representation = { - 'name': 'fbx', - 'ext': 'fbx', - 'files': filename, + "name": "fbx", + "ext": "fbx", + "files": filename, "stagingDir": stagingdir, } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s" % (instance.name, - filepath)) + self.log.info( + "Extracted instance '%s' to: %s" % (instance.name, filepath) + ) diff --git a/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py b/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py index c14fcdbd0b..f0c2aff7f3 100644 --- a/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py +++ b/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py @@ -1,18 +1,11 @@ import os import pyblish.api -from openpype.pipeline import ( - publish, - OptionalPyblishPluginMixin -) +from openpype.pipeline import publish, OptionalPyblishPluginMixin from pymxs import runtime as rt -from openpype.hosts.max.api import ( - maintained_selection, - get_all_children -) +from openpype.hosts.max.api import get_all_children -class ExtractMaxSceneRaw(publish.Extractor, - OptionalPyblishPluginMixin): +class ExtractMaxSceneRaw(publish.Extractor, OptionalPyblishPluginMixin): """ Extract Raw Max Scene with SaveSelected """ @@ -20,9 +13,7 @@ class ExtractMaxSceneRaw(publish.Extractor, order = pyblish.api.ExtractorOrder - 0.2 label = "Extract Max Scene (Raw)" hosts = ["max"] - families = ["camera", - "maxScene", - "model"] + families = ["camera", "maxScene", "model"] optional = True def process(self, instance): @@ -37,26 +28,23 @@ class ExtractMaxSceneRaw(publish.Extractor, filename = "{name}.max".format(**instance.data) max_path = os.path.join(stagingdir, filename) - self.log.info("Writing max file '%s' to '%s'" % (filename, - max_path)) + self.log.info("Writing max file '%s' to '%s'" % (filename, max_path)) if "representations" not in instance.data: instance.data["representations"] = [] - # saving max scene - with maintained_selection(): - # need to figure out how to select the camera - rt.select(get_all_children(rt.getNodeByName(container))) - rt.execute(f'saveNodes selection "{max_path}" quiet:true') + nodes = get_all_children(rt.getNodeByName(container)) + rt.saveNodes(nodes, max_path, quiet=True) self.log.info("Performing Extraction ...") representation = { - 'name': 'max', - 'ext': 'max', - 'files': filename, + "name": "max", + "ext": "max", + "files": filename, "stagingDir": stagingdir, } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s" % (instance.name, - max_path)) + self.log.info( + "Extracted instance '%s' to: %s" % (instance.name, max_path) + ) diff --git a/openpype/hosts/max/plugins/publish/extract_model.py b/openpype/hosts/max/plugins/publish/extract_model.py index 710ad5f97d..4c7c98e2cc 100644 --- a/openpype/hosts/max/plugins/publish/extract_model.py +++ b/openpype/hosts/max/plugins/publish/extract_model.py @@ -1,18 +1,11 @@ import os import pyblish.api -from openpype.pipeline import ( - publish, - OptionalPyblishPluginMixin -) +from openpype.pipeline import publish, OptionalPyblishPluginMixin from pymxs import runtime as rt -from openpype.hosts.max.api import ( - maintained_selection, - get_all_children -) +from openpype.hosts.max.api import maintained_selection, get_all_children -class ExtractModel(publish.Extractor, - OptionalPyblishPluginMixin): +class ExtractModel(publish.Extractor, OptionalPyblishPluginMixin): """ Extract Geometry in Alembic Format """ @@ -36,39 +29,36 @@ class ExtractModel(publish.Extractor, filepath = os.path.join(stagingdir, filename) # We run the render - self.log.info("Writing alembic '%s' to '%s'" % (filename, - stagingdir)) + self.log.info("Writing alembic '%s' to '%s'" % (filename, stagingdir)) - export_cmd = ( - f""" -AlembicExport.ArchiveType = #ogawa -AlembicExport.CoordinateSystem = #maya -AlembicExport.CustomAttributes = true -AlembicExport.UVs = true -AlembicExport.VertexColors = true -AlembicExport.PreserveInstances = true - -exportFile @"{filepath}" #noPrompt selectedOnly:on using:AlembicExport - - """) - - self.log.debug(f"Executing command: {export_cmd}") + rt.AlembicExport.ArchiveType = rt.name("ogawa") + rt.AlembicExport.CoordinateSystem = rt.name("maya") + rt.AlembicExport.CustomAttributes = True + rt.AlembicExport.UVs = True + rt.AlembicExport.VertexColors = True + rt.AlembicExport.PreserveInstances = True with maintained_selection(): # select and export rt.select(get_all_children(rt.getNodeByName(container))) - rt.execute(export_cmd) + rt.exportFile( + filepath, + rt.name("noPrompt"), + selectedOnly=True, + using=rt.AlembicExport, + ) self.log.info("Performing Extraction ...") if "representations" not in instance.data: instance.data["representations"] = [] representation = { - 'name': 'abc', - 'ext': 'abc', - 'files': filename, + "name": "abc", + "ext": "abc", + "files": filename, "stagingDir": stagingdir, } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s" % (instance.name, - filepath)) + self.log.info( + "Extracted instance '%s' to: %s" % (instance.name, filepath) + ) diff --git a/openpype/hosts/max/plugins/publish/extract_model_fbx.py b/openpype/hosts/max/plugins/publish/extract_model_fbx.py index ce58e8cc17..e6ccb24cdd 100644 --- a/openpype/hosts/max/plugins/publish/extract_model_fbx.py +++ b/openpype/hosts/max/plugins/publish/extract_model_fbx.py @@ -1,18 +1,11 @@ import os import pyblish.api -from openpype.pipeline import ( - publish, - OptionalPyblishPluginMixin -) +from openpype.pipeline import publish, OptionalPyblishPluginMixin from pymxs import runtime as rt -from openpype.hosts.max.api import ( - maintained_selection, - get_all_children -) +from openpype.hosts.max.api import maintained_selection, get_all_children -class ExtractModelFbx(publish.Extractor, - OptionalPyblishPluginMixin): +class ExtractModelFbx(publish.Extractor, OptionalPyblishPluginMixin): """ Extract Geometry in FBX Format """ @@ -33,42 +26,38 @@ class ExtractModelFbx(publish.Extractor, stagingdir = self.staging_dir(instance) filename = "{name}.fbx".format(**instance.data) - filepath = os.path.join(stagingdir, - filename) - self.log.info("Writing FBX '%s' to '%s'" % (filepath, - stagingdir)) + filepath = os.path.join(stagingdir, filename) + self.log.info("Writing FBX '%s' to '%s'" % (filepath, stagingdir)) - export_fbx_cmd = ( - f""" -FBXExporterSetParam "Animation" false -FBXExporterSetParam "Cameras" false -FBXExporterSetParam "Lights" false -FBXExporterSetParam "PointCache" false -FBXExporterSetParam "AxisConversionMethod" "Animation" -FbxExporterSetParam "UpAxis" "Y" -FbxExporterSetParam "Preserveinstances" true - -exportFile @"{filepath}" #noPrompt selectedOnly:true using:FBXEXP - - """) - - self.log.debug(f"Executing command: {export_fbx_cmd}") + rt.FBXExporterSetParam("Animation", False) + rt.FBXExporterSetParam("Cameras", False) + rt.FBXExporterSetParam("Lights", False) + rt.FBXExporterSetParam("PointCache", False) + rt.FBXExporterSetParam("AxisConversionMethod", "Animation") + rt.FBXExporterSetParam("UpAxis", "Y") + rt.FBXExporterSetParam("Preserveinstances", True) with maintained_selection(): # select and export rt.select(get_all_children(rt.getNodeByName(container))) - rt.execute(export_fbx_cmd) + rt.exportFile( + filepath, + rt.name("noPrompt"), + selectedOnly=True, + using=rt.FBXEXP, + ) self.log.info("Performing Extraction ...") if "representations" not in instance.data: instance.data["representations"] = [] representation = { - 'name': 'fbx', - 'ext': 'fbx', - 'files': filename, + "name": "fbx", + "ext": "fbx", + "files": filename, "stagingDir": stagingdir, } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s" % (instance.name, - filepath)) + self.log.info( + "Extracted instance '%s' to: %s" % (instance.name, filepath) + ) diff --git a/openpype/hosts/max/plugins/publish/extract_model_obj.py b/openpype/hosts/max/plugins/publish/extract_model_obj.py index 7bda237880..ed3d68c990 100644 --- a/openpype/hosts/max/plugins/publish/extract_model_obj.py +++ b/openpype/hosts/max/plugins/publish/extract_model_obj.py @@ -1,18 +1,11 @@ import os import pyblish.api -from openpype.pipeline import ( - publish, - OptionalPyblishPluginMixin -) +from openpype.pipeline import publish, OptionalPyblishPluginMixin from pymxs import runtime as rt -from openpype.hosts.max.api import ( - maintained_selection, - get_all_children -) +from openpype.hosts.max.api import maintained_selection, get_all_children -class ExtractModelObj(publish.Extractor, - OptionalPyblishPluginMixin): +class ExtractModelObj(publish.Extractor, OptionalPyblishPluginMixin): """ Extract Geometry in OBJ Format """ @@ -33,27 +26,31 @@ class ExtractModelObj(publish.Extractor, stagingdir = self.staging_dir(instance) filename = "{name}.obj".format(**instance.data) - filepath = os.path.join(stagingdir, - filename) - self.log.info("Writing OBJ '%s' to '%s'" % (filepath, - stagingdir)) + filepath = os.path.join(stagingdir, filename) + self.log.info("Writing OBJ '%s' to '%s'" % (filepath, stagingdir)) with maintained_selection(): # select and export rt.select(get_all_children(rt.getNodeByName(container))) - rt.execute(f'exportFile @"{filepath}" #noPrompt selectedOnly:true using:ObjExp') # noqa + rt.exportFile( + filepath, + rt.name("noPrompt"), + selectedOnly=True, + using=rt.ObjExp, + ) self.log.info("Performing Extraction ...") if "representations" not in instance.data: instance.data["representations"] = [] representation = { - 'name': 'obj', - 'ext': 'obj', - 'files': filename, + "name": "obj", + "ext": "obj", + "files": filename, "stagingDir": stagingdir, } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s" % (instance.name, - filepath)) + self.log.info( + "Extracted instance '%s' to: %s" % (instance.name, filepath) + ) diff --git a/openpype/hosts/max/plugins/publish/extract_pointcache.py b/openpype/hosts/max/plugins/publish/extract_pointcache.py index 75d8a7972c..8658cecb1b 100644 --- a/openpype/hosts/max/plugins/publish/extract_pointcache.py +++ b/openpype/hosts/max/plugins/publish/extract_pointcache.py @@ -41,10 +41,7 @@ import os import pyblish.api from openpype.pipeline import publish from pymxs import runtime as rt -from openpype.hosts.max.api import ( - maintained_selection, - get_all_children -) +from openpype.hosts.max.api import maintained_selection, get_all_children class ExtractAlembic(publish.Extractor): @@ -66,35 +63,30 @@ class ExtractAlembic(publish.Extractor): path = os.path.join(parent_dir, file_name) # We run the render - self.log.info("Writing alembic '%s' to '%s'" % (file_name, - parent_dir)) + self.log.info("Writing alembic '%s' to '%s'" % (file_name, parent_dir)) - abc_export_cmd = ( - f""" -AlembicExport.ArchiveType = #ogawa -AlembicExport.CoordinateSystem = #maya -AlembicExport.StartFrame = {start} -AlembicExport.EndFrame = {end} - -exportFile @"{path}" #noPrompt selectedOnly:on using:AlembicExport - - """) - - self.log.debug(f"Executing command: {abc_export_cmd}") + rt.AlembicExport.ArchiveType = rt.name("ogawa") + rt.AlembicExport.CoordinateSystem = rt.name("maya") + rt.AlembicExport.StartFrame = start + rt.AlembicExport.EndFrame = end with maintained_selection(): # select and export - rt.select(get_all_children(rt.getNodeByName(container))) - rt.execute(abc_export_cmd) + rt.exportFile( + path, + rt.name("noPrompt"), + selectedOnly=True, + using=rt.AlembicExport, + ) if "representations" not in instance.data: instance.data["representations"] = [] representation = { - 'name': 'abc', - 'ext': 'abc', - 'files': file_name, + "name": "abc", + "ext": "abc", + "files": file_name, "stagingDir": parent_dir, } instance.data["representations"].append(representation) From eea816f0353b4dbcfed745061d83f61eb08cbcb2 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Sat, 20 May 2023 03:25:02 +0000 Subject: [PATCH 305/360] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index 954cfa945b..8874eb510d 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.8-nightly.1" +__version__ = "3.15.8-nightly.2" From 0b8400bc8e5785cc23239bc432b10a7cc3de4f4c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 20 May 2023 03:25:46 +0000 Subject: [PATCH 306/360] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 0f58d61881..244eb1a363 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.15.8-nightly.2 - 3.15.8-nightly.1 - 3.15.7 - 3.15.7-nightly.3 @@ -134,7 +135,6 @@ body: - 3.14.2-nightly.2 - 3.14.2-nightly.1 - 3.14.1 - - 3.14.1-nightly.4 validations: required: true - type: dropdown From e178244d46401e58fa398501e584e3a6360c0200 Mon Sep 17 00:00:00 2001 From: Ember Light <49758407+EmberLightVFX@users.noreply.github.com> Date: Mon, 22 May 2023 10:43:11 +0200 Subject: [PATCH 307/360] Fusion - Loader plugins updates (#4920) * Added get_bmd_library to acces BMD's internal python library * Added the option to import image and online families. + black formatted * Added workfile loader To import the content of another workfile into your current comp * Fixed wrong family and extension in workfile loader * black formatting * Added missing formats to fbx importer Fusions fbx importer can import a bunch of different formats other then fbx (confusing I know but it's how it is) * Update openpype/hosts/fusion/plugins/load/load_workfile.py Co-authored-by: Roy Nieterau --------- Co-authored-by: Roy Nieterau --- openpype/hosts/fusion/api/__init__.py | 1 + openpype/hosts/fusion/api/lib.py | 6 ++ .../hosts/fusion/plugins/load/load_fbx.py | 34 +++++-- .../fusion/plugins/load/load_sequence.py | 92 +++++++++++-------- .../fusion/plugins/load/load_workfile.py | 32 +++++++ 5 files changed, 118 insertions(+), 47 deletions(-) create mode 100644 openpype/hosts/fusion/plugins/load/load_workfile.py diff --git a/openpype/hosts/fusion/api/__init__.py b/openpype/hosts/fusion/api/__init__.py index 495fe286d5..dba55a98d9 100644 --- a/openpype/hosts/fusion/api/__init__.py +++ b/openpype/hosts/fusion/api/__init__.py @@ -13,6 +13,7 @@ from .lib import ( update_frame_range, set_asset_framerange, get_current_comp, + get_bmd_library, comp_lock_and_undo_chunk ) diff --git a/openpype/hosts/fusion/api/lib.py b/openpype/hosts/fusion/api/lib.py index 40cc4d2963..c33209823e 100644 --- a/openpype/hosts/fusion/api/lib.py +++ b/openpype/hosts/fusion/api/lib.py @@ -309,6 +309,12 @@ def get_fusion_module(): return fusion +def get_bmd_library(): + """Get bmd library""" + bmd = getattr(sys.modules["__main__"], "bmd", None) + return bmd + + def get_current_comp(): """Get current comp in this session""" fusion = get_fusion_module() diff --git a/openpype/hosts/fusion/plugins/load/load_fbx.py b/openpype/hosts/fusion/plugins/load/load_fbx.py index b8f501ae7e..c73ad78394 100644 --- a/openpype/hosts/fusion/plugins/load/load_fbx.py +++ b/openpype/hosts/fusion/plugins/load/load_fbx.py @@ -1,4 +1,3 @@ - from openpype.pipeline import ( load, get_representation_path, @@ -6,7 +5,7 @@ from openpype.pipeline import ( from openpype.hosts.fusion.api import ( imprint_container, get_current_comp, - comp_lock_and_undo_chunk + comp_lock_and_undo_chunk, ) @@ -15,7 +14,21 @@ class FusionLoadFBXMesh(load.LoaderPlugin): families = ["*"] representations = ["*"] - extensions = {"fbx"} + extensions = { + "3ds", + "amc", + "aoa", + "asf", + "bvh", + "c3d", + "dae", + "dxf", + "fbx", + "htr", + "mcd", + "obj", + "trc", + } label = "Load FBX mesh" order = -10 @@ -27,23 +40,24 @@ class FusionLoadFBXMesh(load.LoaderPlugin): def load(self, context, name, namespace, data): # Fallback to asset name when namespace is None if namespace is None: - namespace = context['asset']['name'] + namespace = context["asset"]["name"] # Create the Loader with the filename path set comp = get_current_comp() with comp_lock_and_undo_chunk(comp, "Create tool"): - path = self.fname args = (-32768, -32768) tool = comp.AddTool(self.tool_type, *args) tool["ImportFile"] = path - imprint_container(tool, - name=name, - namespace=namespace, - context=context, - loader=self.__class__.__name__) + imprint_container( + tool, + name=name, + namespace=namespace, + context=context, + loader=self.__class__.__name__, + ) def switch(self, container, representation): self.update(container, representation) diff --git a/openpype/hosts/fusion/plugins/load/load_sequence.py b/openpype/hosts/fusion/plugins/load/load_sequence.py index 38fd41c8b2..552e282587 100644 --- a/openpype/hosts/fusion/plugins/load/load_sequence.py +++ b/openpype/hosts/fusion/plugins/load/load_sequence.py @@ -3,17 +3,14 @@ import contextlib import openpype.pipeline.load as load from openpype.pipeline.load import ( get_representation_context, - get_representation_path_from_context + get_representation_path_from_context, ) from openpype.hosts.fusion.api import ( imprint_container, get_current_comp, - comp_lock_and_undo_chunk -) -from openpype.lib.transcoding import ( - IMAGE_EXTENSIONS, - VIDEO_EXTENSIONS + comp_lock_and_undo_chunk, ) +from openpype.lib.transcoding import IMAGE_EXTENSIONS, VIDEO_EXTENSIONS comp = get_current_comp() @@ -57,20 +54,23 @@ def preserve_trim(loader, log=None): try: yield finally: - length = loader.GetAttrs()["TOOLIT_Clip_Length"][1] - 1 if trim_from_start > length: trim_from_start = length if log: - log.warning("Reducing trim in to %d " - "(because of less frames)" % trim_from_start) + log.warning( + "Reducing trim in to %d " + "(because of less frames)" % trim_from_start + ) remainder = length - trim_from_start if trim_from_end > remainder: trim_from_end = remainder if log: - log.warning("Reducing trim in to %d " - "(because of less frames)" % trim_from_end) + log.warning( + "Reducing trim in to %d " + "(because of less frames)" % trim_from_end + ) loader["ClipTimeStart"][time] = trim_from_start loader["ClipTimeEnd"][time] = length - trim_from_end @@ -109,11 +109,15 @@ def loader_shift(loader, frame, relative=True): # Shifting global in will try to automatically compensate for the change # in the "ClipTimeStart" and "HoldFirstFrame" inputs, so we preserve those # input values to "just shift" the clip - with preserve_inputs(loader, inputs=["ClipTimeStart", - "ClipTimeEnd", - "HoldFirstFrame", - "HoldLastFrame"]): - + with preserve_inputs( + loader, + inputs=[ + "ClipTimeStart", + "ClipTimeEnd", + "HoldFirstFrame", + "HoldLastFrame", + ], + ): # GlobalIn cannot be set past GlobalOut or vice versa # so we must apply them in the order of the shift. if shift > 0: @@ -129,7 +133,14 @@ def loader_shift(loader, frame, relative=True): class FusionLoadSequence(load.LoaderPlugin): """Load image sequence into Fusion""" - families = ["imagesequence", "review", "render", "plate"] + families = [ + "imagesequence", + "review", + "render", + "plate", + "image", + "onilne", + ] representations = ["*"] extensions = set( ext.lstrip(".") for ext in IMAGE_EXTENSIONS.union(VIDEO_EXTENSIONS) @@ -143,7 +154,7 @@ class FusionLoadSequence(load.LoaderPlugin): def load(self, context, name, namespace, data): # Fallback to asset name when namespace is None if namespace is None: - namespace = context['asset']['name'] + namespace = context["asset"]["name"] # Use the first file for now path = get_representation_path_from_context(context) @@ -151,7 +162,6 @@ class FusionLoadSequence(load.LoaderPlugin): # Create the Loader with the filename path set comp = get_current_comp() with comp_lock_and_undo_chunk(comp, "Create Loader"): - args = (-32768, -32768) tool = comp.AddTool("Loader", *args) tool["Clip"] = path @@ -160,11 +170,13 @@ class FusionLoadSequence(load.LoaderPlugin): start = self._get_start(context["version"], tool) loader_shift(tool, start, relative=False) - imprint_container(tool, - name=name, - namespace=namespace, - context=context, - loader=self.__class__.__name__) + imprint_container( + tool, + name=name, + namespace=namespace, + context=context, + loader=self.__class__.__name__, + ) def switch(self, container, representation): self.update(container, representation) @@ -222,24 +234,28 @@ class FusionLoadSequence(load.LoaderPlugin): start = self._get_start(context["version"], tool) with comp_lock_and_undo_chunk(comp, "Update Loader"): - # Update the loader's path whilst preserving some values with preserve_trim(tool, log=self.log): - with preserve_inputs(tool, - inputs=("HoldFirstFrame", - "HoldLastFrame", - "Reverse", - "Depth", - "KeyCode", - "TimeCodeOffset")): + with preserve_inputs( + tool, + inputs=( + "HoldFirstFrame", + "HoldLastFrame", + "Reverse", + "Depth", + "KeyCode", + "TimeCodeOffset", + ), + ): tool["Clip"] = path # Set the global in to the start frame of the sequence global_in_changed = loader_shift(tool, start, relative=False) if global_in_changed: # Log this change to the user - self.log.debug("Changed '%s' global in: %d" % (tool.Name, - start)) + self.log.debug( + "Changed '%s' global in: %d" % (tool.Name, start) + ) # Update the imprinted representation tool.SetData("avalon.representation", str(representation["_id"])) @@ -264,9 +280,11 @@ class FusionLoadSequence(load.LoaderPlugin): # Get frame start without handles start = data.get("frameStart") if start is None: - self.log.warning("Missing start frame for version " - "assuming starts at frame 0 for: " - "{}".format(tool.Name)) + self.log.warning( + "Missing start frame for version " + "assuming starts at frame 0 for: " + "{}".format(tool.Name) + ) return 0 # Use `handleStart` if the data is available diff --git a/openpype/hosts/fusion/plugins/load/load_workfile.py b/openpype/hosts/fusion/plugins/load/load_workfile.py new file mode 100644 index 0000000000..b49d104a15 --- /dev/null +++ b/openpype/hosts/fusion/plugins/load/load_workfile.py @@ -0,0 +1,32 @@ +"""Import workfiles into your current comp. +As all imported nodes are free floating and will probably be changed there +is no update or reload function added for this plugin +""" + +from openpype.pipeline import load + +from openpype.hosts.fusion.api import ( + get_current_comp, + get_bmd_library, +) + + +class FusionLoadWorkfile(load.LoaderPlugin): + """Load the content of a workfile into Fusion""" + + families = ["workfile"] + representations = ["*"] + extensions = {"comp"} + + label = "Load Workfile" + order = -10 + icon = "code-fork" + color = "orange" + + def load(self, context, name, namespace, data): + # Get needed elements + bmd = get_bmd_library() + comp = get_current_comp() + + # Paste the content of the file into the current comp + comp.Paste(bmd.readfile(self.fname)) From 136af34a7189e78fd8549ffe029285283b7997c8 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 22 May 2023 10:45:20 +0200 Subject: [PATCH 308/360] AfterEffects: set frame range and resolution (#4983) * OP-5660 - adding menu buttons to Set frame range in AE * OP-5660 - refactored location of scripts set_settings should be in lib as it is used elsewhere, but launch_logic and lib created circular dependency. Moved main to launch logic as it is actually handling launching. * OP-5660 - added set_settings to creator When instance gets created, set frame range and resolution from DB. * OP-5660 - minor fix * OP-5660 - updated extension zip * OP-5660 - updated documentation * OP-5660 - fixed missing exception * OP-5660 - fixed argument * OP-5560 - fix imports * OP-5660 - updated extension * OP-5660 - add js alert message for buttons * OP-5660 - repacked extension Without Anastasyi showed success, but extension wasn't loaded. * OP-5660 - make log message nicer * OP-5660 - added log if workfile not saved * OP-5660 - provide defaults to limit None exception * OP-5660 - updated error message --- openpype/hosts/aftereffects/api/__init__.py | 10 +- openpype/hosts/aftereffects/api/extension.zxp | Bin 101426 -> 101866 bytes .../api/extension/CSXS/manifest.xml | 4 +- .../aftereffects/api/extension/index.html | 72 +++++++--- .../aftereffects/api/extension/js/main.js | 56 +++++--- .../api/extension/jsx/hostscript.jsx | 134 ++++++++++++------ .../hosts/aftereffects/api/launch_logic.py | 86 ++++++++--- openpype/hosts/aftereffects/api/lib.py | 118 ++++++++------- openpype/hosts/aftereffects/api/pipeline.py | 6 +- openpype/hosts/aftereffects/api/ws_stub.py | 65 ++++++--- .../plugins/create/create_render.py | 19 ++- .../plugins/publish/collect_render.py | 16 +-- openpype/scripts/non_python_host_launch.py | 3 +- website/docs/artist_hosts_aftereffects.md | 37 ++++- .../docs/assets/aftereffects_extension.png | Bin 0 -> 12533 bytes 15 files changed, 414 insertions(+), 212 deletions(-) create mode 100644 website/docs/assets/aftereffects_extension.png diff --git a/openpype/hosts/aftereffects/api/__init__.py b/openpype/hosts/aftereffects/api/__init__.py index a7137ba8fb..28062cc35d 100644 --- a/openpype/hosts/aftereffects/api/__init__.py +++ b/openpype/hosts/aftereffects/api/__init__.py @@ -4,9 +4,8 @@ Anything that isn't defined here is INTERNAL and unreliable for external use. """ -from .launch_logic import ( +from .ws_stub import ( get_stub, - stub, ) from .pipeline import ( @@ -18,7 +17,8 @@ from .pipeline import ( from .lib import ( maintained_selection, get_extension_manifest_path, - get_asset_settings + get_asset_settings, + set_settings ) from .plugin import ( @@ -27,9 +27,8 @@ from .plugin import ( __all__ = [ - # launch_logic + # ws_stub "get_stub", - "stub", # pipeline "ls", @@ -39,6 +38,7 @@ __all__ = [ "maintained_selection", "get_extension_manifest_path", "get_asset_settings", + "set_settings", # plugin "AfterEffectsLoader" diff --git a/openpype/hosts/aftereffects/api/extension.zxp b/openpype/hosts/aftereffects/api/extension.zxp index b436f0ca0b67313cf4509d1ec5e7d4a1dfd2d106..50fda416f806515e8a6e90e745153e341f73c7b2 100644 GIT binary patch delta 14164 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 XY6T7RTV3K`-(5HW;4}pI>(~DQ$dj(< delta 13616 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 - + diff --git a/openpype/hosts/aftereffects/api/extension/index.html b/openpype/hosts/aftereffects/api/extension/index.html index 52a7c4964f..291965559f 100644 --- a/openpype/hosts/aftereffects/api/extension/index.html +++ b/openpype/hosts/aftereffects/api/extension/index.html @@ -2,7 +2,7 @@ - + @@ -25,11 +25,11 @@ - + - + - + - + - + + + + + + + - - + + + - - + @@ -107,6 +143,6 @@ - + - \ No newline at end of file + diff --git a/openpype/hosts/aftereffects/api/extension/js/main.js b/openpype/hosts/aftereffects/api/extension/js/main.js index bb0f3b1f0c..ffc41f0937 100644 --- a/openpype/hosts/aftereffects/api/extension/js/main.js +++ b/openpype/hosts/aftereffects/api/extension/js/main.js @@ -4,7 +4,7 @@ indent: 4, maxerr: 50 */ var csInterface = new CSInterface(); - + log.warn("script start"); WSRPC.DEBUG = false; @@ -14,7 +14,7 @@ WSRPC.TRACE = false; async function startUp(url){ promis = runEvalScript("getEnv('" + url + "')"); - var res = await promis; + var res = await promis; log.warn("res: " + res); promis = runEvalScript("getEnv('OPENPYPE_DEBUG')"); @@ -56,7 +56,7 @@ function get_extension_version(){ } function main(websocket_url){ - // creates connection to 'websocket_url', registers routes + // creates connection to 'websocket_url', registers routes var default_url = 'ws://localhost:8099/ws/'; if (websocket_url == ''){ @@ -66,7 +66,7 @@ function main(websocket_url){ RPC.connect(); - log.warn("connected"); + log.warn("connected"); RPC.addRoute('AfterEffects.open', function (data) { log.warn('Server called client route "open":', data); @@ -88,7 +88,7 @@ function main(websocket_url){ }); RPC.addRoute('AfterEffects.get_active_document_name', function (data) { - log.warn('Server called client route ' + + log.warn('Server called client route ' + '"get_active_document_name":', data); return runEvalScript("getActiveDocumentName()") .then(function(result){ @@ -98,7 +98,7 @@ function main(websocket_url){ }); RPC.addRoute('AfterEffects.get_active_document_full_name', function (data){ - log.warn('Server called client route ' + + log.warn('Server called client route ' + '"get_active_document_full_name":', data); return runEvalScript("getActiveDocumentFullName()") .then(function(result){ @@ -118,7 +118,7 @@ function main(websocket_url){ }); }); - + RPC.addRoute('AfterEffects.get_selected_items', function (data) { log.warn('Server called client route "get_selected_items":', data); return runEvalScript("getSelectedItems(" + data.comps + "," + @@ -194,23 +194,25 @@ function main(websocket_url){ }); }); - RPC.addRoute('AfterEffects.get_work_area', function (data) { - log.warn('Server called client route "get_work_area":', data); - return runEvalScript("getWorkArea(" + data.item_id + ")") + RPC.addRoute('AfterEffects.get_comp_properties', function (data) { + log.warn('Server called client route "get_comp_properties":', data); + return runEvalScript("getCompProperties(" + data.item_id + ")") .then(function(result){ - log.warn("getWorkArea: " + result); + log.warn("get_comp_properties: " + result); return result; }); }); - RPC.addRoute('AfterEffects.set_work_area', function (data) { + RPC.addRoute('AfterEffects.set_comp_properties', function (data) { log.warn('Server called client route "set_work_area":', data); - return runEvalScript("setWorkArea(" + data.item_id + ',' + + return runEvalScript("setCompProperties(" + data.item_id + ',' + data.start + ',' + data.duration + ',' + - data.frame_rate + ")") + data.frame_rate + ',' + + data.width + ',' + + data.height + ")") .then(function(result){ - log.warn("getWorkArea: " + result); + log.warn("set_comp_properties: " + result); return result; }); }); @@ -255,7 +257,7 @@ function main(websocket_url){ RPC.addRoute('AfterEffects.import_background', function (data) { log.warn('Server called client route "import_background":', data); - return runEvalScript("importBackground(" + data.comp_id + ", " + + return runEvalScript("importBackground(" + data.comp_id + ", " + "'" + data.comp_name + "', " + JSON.stringify(data.files) + ")") .then(function(result){ @@ -266,7 +268,7 @@ function main(websocket_url){ RPC.addRoute('AfterEffects.reload_background', function (data) { log.warn('Server called client route "reload_background":', data); - return runEvalScript("reloadBackground(" + data.comp_id + ", " + + return runEvalScript("reloadBackground(" + data.comp_id + ", " + "'" + data.comp_name + "', " + JSON.stringify(data.files) + ")") .then(function(result){ @@ -314,6 +316,16 @@ function main(websocket_url){ log.warn('Server called client route "close":', data); return runEvalScript("close()"); }); + + RPC.addRoute('AfterEffects.print_msg', function (data) { + log.warn('Server called client route "print_msg":', data); + var escaped_msg = EscapeStringForJSX(data.msg); + return runEvalScript("printMsg('" + escaped_msg +"')") + .then(function(result){ + log.warn("print_msg: " + result); + return result; + }); + }); } /** main entry point **/ @@ -323,17 +335,17 @@ startUp("WEBSOCKET_URL"); 'use strict'; var csInterface = new CSInterface(); - - + + function init() { - + themeManager.init(); - + $("#btn_test").click(function () { csInterface.evalScript('sayHello()'); }); } - + init(); }()); diff --git a/openpype/hosts/aftereffects/api/extension/jsx/hostscript.jsx b/openpype/hosts/aftereffects/api/extension/jsx/hostscript.jsx index 5c1d163439..7d0b20bbb4 100644 --- a/openpype/hosts/aftereffects/api/extension/jsx/hostscript.jsx +++ b/openpype/hosts/aftereffects/api/extension/jsx/hostscript.jsx @@ -1,7 +1,7 @@ /*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true, indent: 4, maxerr: 50 */ /*global $, Folder*/ -#include "../js/libs/json.js"; +//@include "../js/libs/json.js" /* All public API function should return JSON! */ @@ -29,13 +29,13 @@ function getEnv(variable){ function getMetadata(){ /** * Returns payload in 'Label' field of project's metadata - * + * **/ if (ExternalObject.AdobeXMPScript === undefined){ ExternalObject.AdobeXMPScript = new ExternalObject('lib:AdobeXMPScript'); } - + var proj = app.project; var meta = new XMPMeta(app.project.xmpPacket); var schemaNS = XMPMeta.getNamespaceURI("xmp"); @@ -53,7 +53,7 @@ function getMetadata(){ function imprint(payload){ /** * Stores payload in 'Label' field of project's metadata - * + * * Args: * payload (string): json content */ @@ -61,14 +61,14 @@ function imprint(payload){ ExternalObject.AdobeXMPScript = new ExternalObject('lib:AdobeXMPScript'); } - + var proj = app.project; var meta = new XMPMeta(app.project.xmpPacket); var schemaNS = XMPMeta.getNamespaceURI("xmp"); var label = "xmp:Label"; meta.setProperty(schemaNS, label, payload); - + app.project.xmpPacket = meta.serialize(); } @@ -116,14 +116,14 @@ function getItems(comps, folders, footages){ /** * Returns JSON representation of compositions and * if 'collectLayers' then layers in comps too. - * + * * Args: * comps (bool): return selected compositions * folders (bool): return folders * footages (bool): return FootageItem * Returns: * (list) of JSON items - */ + */ var items = [] for (i = 1; i <= app.project.items.length; ++i){ var item = app.project.items[i]; @@ -142,14 +142,14 @@ function getItems(comps, folders, footages){ function getSelectedItems(comps, folders, footages){ /** * Returns list of selected items from Project menu - * + * * Args: * comps (bool): return selected compositions * folders (bool): return folders * footages (bool): return FootageItem * Returns: * (list) of JSON items - */ + */ var items = [] for (i = 0; i < app.project.selection.length; ++i){ var item = app.project.selection[i]; @@ -166,9 +166,9 @@ function getSelectedItems(comps, folders, footages){ function _getItem(item, comps, folders, footages){ /** - * Auxiliary function as project items and selections + * Auxiliary function as project items and selections * are indexed in different way :/ - * Refactor + * Refactor */ var item_type = ''; if (item instanceof FolderItem){ @@ -189,7 +189,7 @@ function _getItem(item, comps, folders, footages){ return "{}"; } } - + var item = {"name": item.name, "id": item.id, "type": item_type}; @@ -200,7 +200,7 @@ function importFile(path, item_name, import_options){ /** * Imports file (image tested for now) as a FootageItem. * Creates new composition - * + * * Args: * path (string): absolute path to image file * item_name (string): label for composition @@ -218,7 +218,7 @@ function importFile(path, item_name, import_options){ app.beginUndoGroup("Import File"); fp = new File(path); if (fp.exists){ - try { + try { im_opt = new ImportOptions(fp); importAsType = import_options["ImportAsType"]; @@ -234,18 +234,18 @@ function importFile(path, item_name, import_options){ } if (importAsType.indexOf('PROJECT') > 0){ im_opt.importAs = ImportAsType.PROJECT; - } - + } + } if ('sequence' in import_options){ im_opt.sequence = true; } - + comp = app.project.importFile(im_opt); if (app.project.selection.length == 2 && app.project.selection[0] instanceof FolderItem){ - comp.parentFolder = app.project.selection[0] + comp.parentFolder = app.project.selection[0] } } catch (error) { return _prepareError(error.toString() + importOptions.file.fsName); @@ -283,14 +283,14 @@ function setLabelColor(comp_id, color_idx){ function replaceItem(comp_id, path, item_name){ /** * Replaces loaded file with new file and updates name - * + * * Args: * comp_id (int): id of composition, not a index! * path (string): absolute path to new file * item_name (string): new composition name */ app.beginUndoGroup("Replace File"); - + fp = new File(path); if (!fp.exists){ return _prepareError("File " + path + " not found."); @@ -303,7 +303,7 @@ function replaceItem(comp_id, path, item_name){ }else{ item.replace(fp); } - + item.name = item_name; } catch (error) { return _prepareError(error.toString() + path); @@ -319,7 +319,7 @@ function replaceItem(comp_id, path, item_name){ function renameItem(item_id, new_name){ /** * Renames item with 'item_id' to 'new_name' - * + * * Args: * item_id (int): id to search item * new_name (str) @@ -335,7 +335,7 @@ function renameItem(item_id, new_name){ function deleteItem(item_id){ /** * Delete any 'item_id' - * + * * Not restricted only to comp, it could delete * any item with 'id' */ @@ -347,38 +347,76 @@ function deleteItem(item_id){ } } -function getWorkArea(comp_id){ +function getCompProperties(comp_id){ /** - * Returns information about workarea - are that will be - * rendered. All calculation will be done in OpenPype, - * easier to modify without redeploy of extension. - * + * Returns information about composition - are that will be + * rendered. + * * Returns * (dict) */ - var item = app.project.itemByID(comp_id); - if (item){ - return JSON.stringify({ - "workAreaStart": item.displayStartFrame, - "workAreaDuration": item.duration, - "frameRate": item.frameRate}); - }else{ + var comp = app.project.itemByID(comp_id); + if (!comp){ return _prepareError("There is no composition with "+ comp_id); } + + return JSON.stringify({ + "id": comp.id, + "name": comp.name, + "frameStart": comp.displayStartFrame, + "framesDuration": comp.duration * comp.frameRate, + "frameRate": comp.frameRate, + "width": comp.width, + "height": comp.height}); } -function setWorkArea(comp_id, workAreaStart, workAreaDuration, frameRate){ +function setCompProperties(comp_id, frameStart, framesCount, frameRate, + width, height){ /** * Sets work area info from outside (from Ftrack via OpenPype) */ - var item = app.project.itemByID(comp_id); - if (item){ - item.displayStartTime = workAreaStart; - item.duration = workAreaDuration; - item.frameRate = frameRate; - }else{ + var comp = app.project.itemByID(comp_id); + if (!comp){ return _prepareError("There is no composition with "+ comp_id); } + + app.beginUndoGroup('change comp properties'); + if (frameStart && framesCount && frameRate){ + comp.displayStartFrame = frameStart; + comp.duration = framesCount / frameRate; + comp.frameRate = frameRate; + } + if (width && height){ + var widthOld = comp.width; + var widthNew = width; + var widthDelta = widthNew - widthOld; + + var heightOld = comp.height; + var heightNew = height; + var heightDelta = heightNew - heightOld; + + var offset = [widthDelta / 2, heightDelta / 2]; + + comp.width = widthNew; + comp.height = heightNew; + + for (var i = 1, il = comp.numLayers; i <= il; i++) { + var layer = comp.layer(i); + var positionProperty = layer.property('ADBE Transform Group').property('ADBE Position'); + + if (positionProperty.numKeys > 0) { + for (var j = 1, jl = positionProperty.numKeys; j <= jl; j++) { + var keyValue = positionProperty.keyValue(j); + positionProperty.setValueAtKey(j, keyValue + offset); + } + } else { + var positionValue = positionProperty.value; + positionProperty.setValue(positionValue + offset); + } + } + } + + app.endUndoGroup(); } function save(){ @@ -504,7 +542,7 @@ function addItemAsLayerToComp(comp_id, item_id, found_comp){ * Args: * comp_id (int): id of target composition * item_id (int): FootageItem.id - * found_comp (CompItem, optional): to limit querying if + * found_comp (CompItem, optional): to limit quering if * comp already found previously */ var comp = found_comp || app.project.itemByID(comp_id); @@ -749,7 +787,7 @@ function render(target_folder, comp_id){ var om1 = app.project.renderQueue.item(i).outputModule(1); var file_name = File.decode( om1.file.name ).replace('℗', ''); // Name contains special character, space? - + var omItem1_settable_str = app.project.renderQueue.item(i).outputModule(1).getSettings( GetSettingsFormat.STRING_SETTABLE ); var targetFolder = new Folder(target_folder); @@ -763,7 +801,7 @@ function render(target_folder, comp_id){ render_item.render = false; } } - + } app.beginSuppressDialogs(); app.project.renderQueue.render(); @@ -779,6 +817,10 @@ function getAppVersion(){ return _prepareSingleValue(app.version); } +function printMsg(msg){ + alert(msg); +} + function _prepareSingleValue(value){ return JSON.stringify({"result": value}) } diff --git a/openpype/hosts/aftereffects/api/launch_logic.py b/openpype/hosts/aftereffects/api/launch_logic.py index c428043d99..77c2b0b6ca 100644 --- a/openpype/hosts/aftereffects/api/launch_logic.py +++ b/openpype/hosts/aftereffects/api/launch_logic.py @@ -1,49 +1,77 @@ import os +import sys import subprocess import collections import logging import asyncio import functools +import traceback + from wsrpc_aiohttp import ( WebSocketRoute, WebSocketAsync ) -from qtpy import QtCore +from qtpy import QtCore, QtWidgets from openpype.lib import Logger -from openpype.pipeline import legacy_io from openpype.tools.utils import host_tools +from openpype.tests.lib import is_in_tests +from openpype.pipeline import install_host, legacy_io +from openpype.modules import ModulesManager from openpype.tools.adobe_webserver.app import WebServerTool -from .ws_stub import AfterEffectsServerStub +from .ws_stub import get_stub +from .lib import set_settings log = logging.getLogger(__name__) log.setLevel(logging.DEBUG) -class ConnectionNotEstablishedYet(Exception): - pass +def safe_excepthook(*args): + traceback.print_exception(*args) -def get_stub(): - """ - Convenience function to get server RPC stub to call methods directed - for host (Photoshop). - It expects already created connection, started from client. - Currently created when panel is opened (PS: Window>Extensions>Avalon) - :return: where functions could be called from - """ - ae_stub = AfterEffectsServerStub() - if not ae_stub.client: - raise ConnectionNotEstablishedYet("Connection is not created yet") +def main(*subprocess_args): + """Main entrypoint to AE launching, called from pre hook.""" + sys.excepthook = safe_excepthook - return ae_stub + from openpype.hosts.aftereffects.api import AfterEffectsHost + host = AfterEffectsHost() + install_host(host) -def stub(): - return get_stub() + os.environ["OPENPYPE_LOG_NO_COLORS"] = "False" + app = QtWidgets.QApplication([]) + app.setQuitOnLastWindowClosed(False) + + launcher = ProcessLauncher(subprocess_args) + launcher.start() + + if os.environ.get("HEADLESS_PUBLISH"): + manager = ModulesManager() + webpublisher_addon = manager["webpublisher"] + + launcher.execute_in_main_thread( + functools.partial( + webpublisher_addon.headless_publish, + log, + "CloseAE", + is_in_tests() + ) + ) + + elif os.environ.get("AVALON_PHOTOSHOP_WORKFILES_ON_LAUNCH", True): + save = False + if os.getenv("WORKFILES_SAVE_AS"): + save = True + + launcher.execute_in_main_thread( + lambda: host_tools.show_tool_by_name("workfiles", save=save) + ) + + sys.exit(app.exec_()) def show_tool_by_name(tool_name): @@ -55,6 +83,7 @@ def show_tool_by_name(tool_name): class ProcessLauncher(QtCore.QObject): + """Launches webserver, connects to it, runs main thread.""" route_name = "AfterEffects" _main_thread_callbacks = collections.deque() @@ -296,6 +325,15 @@ class AfterEffectsRoute(WebSocketRoute): async def sceneinventory_route(self): self._tool_route("sceneinventory") + async def setresolution_route(self): + self._settings_route(False, True) + + async def setframes_route(self): + self._settings_route(True, False) + + async def setall_route(self): + self._settings_route(True, True) + async def experimental_tools_route(self): self._tool_route("experimental_tools") @@ -309,3 +347,13 @@ class AfterEffectsRoute(WebSocketRoute): # Required return statement. return "nothing" + + def _settings_route(self, frames, resolution): + partial_method = functools.partial(set_settings, + frames, + resolution) + + ProcessLauncher.execute_in_main_thread(partial_method) + + # Required return statement. + return "nothing" diff --git a/openpype/hosts/aftereffects/api/lib.py b/openpype/hosts/aftereffects/api/lib.py index a39af5c81f..e8352c382b 100644 --- a/openpype/hosts/aftereffects/api/lib.py +++ b/openpype/hosts/aftereffects/api/lib.py @@ -1,69 +1,17 @@ import os -import sys import re import json import contextlib -import traceback import logging -from functools import partial -from qtpy import QtWidgets - -from openpype.pipeline import install_host -from openpype.modules import ModulesManager - -from openpype.tools.utils import host_tools -from openpype.tests.lib import is_in_tests -from .launch_logic import ProcessLauncher, get_stub +from openpype.pipeline.context_tools import get_current_context +from openpype.client import get_asset_by_name +from .ws_stub import get_stub log = logging.getLogger(__name__) log.setLevel(logging.DEBUG) -def safe_excepthook(*args): - traceback.print_exception(*args) - - -def main(*subprocess_args): - sys.excepthook = safe_excepthook - - from openpype.hosts.aftereffects.api import AfterEffectsHost - - host = AfterEffectsHost() - install_host(host) - - os.environ["OPENPYPE_LOG_NO_COLORS"] = "False" - app = QtWidgets.QApplication([]) - app.setQuitOnLastWindowClosed(False) - - launcher = ProcessLauncher(subprocess_args) - launcher.start() - - if os.environ.get("HEADLESS_PUBLISH"): - manager = ModulesManager() - webpublisher_addon = manager["webpublisher"] - - launcher.execute_in_main_thread( - partial( - webpublisher_addon.headless_publish, - log, - "CloseAE", - is_in_tests() - ) - ) - - elif os.environ.get("AVALON_PHOTOSHOP_WORKFILES_ON_LAUNCH", True): - save = False - if os.getenv("WORKFILES_SAVE_AS"): - save = True - - launcher.execute_in_main_thread( - lambda: host_tools.show_tool_by_name("workfiles", save=save) - ) - - sys.exit(app.exec_()) - - @contextlib.contextmanager def maintained_selection(): """Maintain selection during context.""" @@ -145,13 +93,13 @@ def get_asset_settings(asset_doc): """ asset_data = asset_doc["data"] - fps = asset_data.get("fps") - frame_start = asset_data.get("frameStart") - frame_end = asset_data.get("frameEnd") - handle_start = asset_data.get("handleStart") - handle_end = asset_data.get("handleEnd") - resolution_width = asset_data.get("resolutionWidth") - resolution_height = asset_data.get("resolutionHeight") + fps = asset_data.get("fps", 0) + frame_start = asset_data.get("frameStart", 0) + frame_end = asset_data.get("frameEnd", 0) + handle_start = asset_data.get("handleStart", 0) + handle_end = asset_data.get("handleEnd", 0) + resolution_width = asset_data.get("resolutionWidth", 0) + resolution_height = asset_data.get("resolutionHeight", 0) duration = (frame_end - frame_start + 1) + handle_start + handle_end return { @@ -164,3 +112,49 @@ def get_asset_settings(asset_doc): "resolutionHeight": resolution_height, "duration": duration } + + +def set_settings(frames, resolution, comp_ids=None, print_msg=True): + """Sets number of frames and resolution to selected comps. + + Args: + frames (bool): True if set frame info + resolution (bool): True if set resolution + comp_ids (list): specific composition ids, if empty + it tries to look for currently selected + print_msg (bool): True throw JS alert with msg + """ + frame_start = frames_duration = fps = width = height = None + current_context = get_current_context() + + asset_doc = get_asset_by_name(current_context["project_name"], + current_context["asset_name"]) + settings = get_asset_settings(asset_doc) + + msg = '' + if frames: + frame_start = settings["frameStart"] - settings["handleStart"] + frames_duration = settings["duration"] + fps = settings["fps"] + msg += f"frame start:{frame_start}, duration:{frames_duration}, "\ + f"fps:{fps}" + if resolution: + width = settings["resolutionWidth"] + height = settings["resolutionHeight"] + msg += f"width:{width} and height:{height}" + + stub = get_stub() + if not comp_ids: + comps = stub.get_selected_items(True, False, False) + comp_ids = [comp.id for comp in comps] + if not comp_ids: + stub.print_msg("Select at least one composition to apply settings.") + return + + for comp_id in comp_ids: + msg = f"Setting for comp {comp_id} " + msg + log.debug(msg) + stub.set_comp_properties(comp_id, frame_start, frames_duration, + fps, width, height) + if print_msg: + stub.print_msg(msg) diff --git a/openpype/hosts/aftereffects/api/pipeline.py b/openpype/hosts/aftereffects/api/pipeline.py index 020022e263..27aee8c7ce 100644 --- a/openpype/hosts/aftereffects/api/pipeline.py +++ b/openpype/hosts/aftereffects/api/pipeline.py @@ -8,10 +8,7 @@ from openpype.lib import Logger, register_event_callback from openpype.pipeline import ( register_loader_plugin_path, register_creator_plugin_path, - deregister_loader_plugin_path, - deregister_creator_plugin_path, AVALON_CONTAINER_ID, - legacy_io, ) from openpype.pipeline.load import any_outdated_containers import openpype.hosts.aftereffects @@ -23,7 +20,8 @@ from openpype.host import ( IPublishHost ) -from .launch_logic import get_stub, ConnectionNotEstablishedYet +from .launch_logic import get_stub +from .ws_stub import ConnectionNotEstablishedYet log = Logger.get_logger(__name__) diff --git a/openpype/hosts/aftereffects/api/ws_stub.py b/openpype/hosts/aftereffects/api/ws_stub.py index f094c7fa2a..576c997f49 100644 --- a/openpype/hosts/aftereffects/api/ws_stub.py +++ b/openpype/hosts/aftereffects/api/ws_stub.py @@ -11,6 +11,10 @@ from wsrpc_aiohttp import WebSocketAsync from openpype.tools.adobe_webserver.app import WebServerTool +class ConnectionNotEstablishedYet(Exception): + pass + + @attr.s class AEItem(object): """ @@ -24,8 +28,8 @@ class AEItem(object): # all imported elements, single for # regular image, array for Backgrounds members = attr.ib(factory=list) - workAreaStart = attr.ib(default=None) - workAreaDuration = attr.ib(default=None) + frameStart = attr.ib(default=None) + framesDuration = attr.ib(default=None) frameRate = attr.ib(default=None) file_name = attr.ib(default=None) instance_id = attr.ib(default=None) # New Publisher @@ -355,42 +359,50 @@ class AfterEffectsServerStub(): return self._handle_return(res) - def get_work_area(self, item_id): - """ Get work are information for render purposes + def get_comp_properties(self, comp_id): + """ Get composition information for render purposes + + Returns startFrame, frameDuration, fps, width, height. + Args: - item_id (int): + comp_id (int): Returns: (AEItem) """ res = self.websocketserver.call(self.client.call - ('AfterEffects.get_work_area', - item_id=item_id + ('AfterEffects.get_comp_properties', + item_id=comp_id )) records = self._to_records(self._handle_return(res)) if records: return records.pop() - def set_work_area(self, item, start, duration, frame_rate): + def set_comp_properties(self, comp_id, start, duration, frame_rate, + width, height): """ Set work area to predefined values (from Ftrack). Work area directs what gets rendered. Beware of rounding, AE expects seconds, not frames directly. Args: - item (dict): - start (float): workAreaStart in seconds - duration (float): in seconds + comp_id (int): + start (int): workAreaStart in frames + duration (int): in frames frame_rate (float): frames in seconds + width (int): resolution width + height (int): resolution height """ res = self.websocketserver.call(self.client.call - ('AfterEffects.set_work_area', - item_id=item.id, + ('AfterEffects.set_comp_properties', + item_id=comp_id, start=start, duration=duration, - frame_rate=frame_rate)) + frame_rate=frame_rate, + width=width, + height=height)) return self._handle_return(res) def save(self): @@ -554,6 +566,12 @@ class AfterEffectsServerStub(): return self._handle_return(res) + def print_msg(self, msg): + """Triggers Javascript alert dialog.""" + self.websocketserver.call(self.client.call + ('AfterEffects.print_msg', + msg=msg)) + def _handle_return(self, res): """Wraps return, throws ValueError if 'error' key is present.""" if res and isinstance(res, str) and res != "undefined": @@ -608,8 +626,8 @@ class AfterEffectsServerStub(): d.get('name'), d.get('type'), d.get('members'), - d.get('workAreaStart'), - d.get('workAreaDuration'), + d.get('frameStart'), + d.get('framesDuration'), d.get('frameRate'), d.get('file_name'), d.get("instance_id"), @@ -618,3 +636,18 @@ class AfterEffectsServerStub(): ret.append(item) return ret + + +def get_stub(): + """ + Convenience function to get server RPC stub to call methods directed + for host (Photoshop). + It expects already created connection, started from client. + Currently created when panel is opened (PS: Window>Extensions>Avalon) + :return: where functions could be called from + """ + ae_stub = AfterEffectsServerStub() + if not ae_stub.client: + raise ConnectionNotEstablishedYet("Connection is not created yet") + + return ae_stub diff --git a/openpype/hosts/aftereffects/plugins/create/create_render.py b/openpype/hosts/aftereffects/plugins/create/create_render.py index 171d7053ce..fa79fac78f 100644 --- a/openpype/hosts/aftereffects/plugins/create/create_render.py +++ b/openpype/hosts/aftereffects/plugins/create/create_render.py @@ -9,6 +9,7 @@ from openpype.pipeline import ( CreatorError ) from openpype.hosts.aftereffects.api.pipeline import cache_and_get_instances +from openpype.hosts.aftereffects.api.lib import set_settings from openpype.lib import prepare_template_data from openpype.pipeline.create import SUBSET_NAME_ALLOWED_SYMBOLS @@ -32,6 +33,14 @@ class RenderCreator(Creator): def create(self, subset_name_from_ui, data, pre_create_data): stub = api.get_stub() # only after After Effects is up + + try: + _ = stub.get_active_document_full_name() + except ValueError: + raise CreatorError( + "Please save workfile via Workfile app first!" + ) + if pre_create_data.get("use_selection"): comps = stub.get_selected_items( comps=True, folders=False, footages=False @@ -41,8 +50,8 @@ class RenderCreator(Creator): if not comps: raise CreatorError( - "Nothing to create. Select composition " - "if 'useSelection' or create at least " + "Nothing to create. Select composition in Project Bin if " + "'Use selection' is toggled or create at least " "one composition." ) use_composition_name = (pre_create_data.get("use_composition_name") or @@ -87,10 +96,14 @@ class RenderCreator(Creator): self._add_instance_to_context(new_instance) stub.rename_item(comp.id, subset_name) + set_settings(True, True, [comp.id], print_msg=False) def get_pre_create_attr_defs(self): output = [ - BoolDef("use_selection", default=True, label="Use selection"), + BoolDef("use_selection", + tooltip="Composition for publishable instance should be " + "selected by default.", + default=True, label="Use selection"), BoolDef("use_composition_name", label="Use composition name in subset"), UISeparatorDef(), diff --git a/openpype/hosts/aftereffects/plugins/publish/collect_render.py b/openpype/hosts/aftereffects/plugins/publish/collect_render.py index b01b707246..aa46461915 100644 --- a/openpype/hosts/aftereffects/plugins/publish/collect_render.py +++ b/openpype/hosts/aftereffects/plugins/publish/collect_render.py @@ -66,19 +66,19 @@ class CollectAERender(publish.AbstractCollectRender): comp_id = int(inst.data["members"][0]) - work_area_info = CollectAERender.get_stub().get_work_area(comp_id) + comp_info = CollectAERender.get_stub().get_comp_properties( + comp_id) - if not work_area_info: + if not comp_info: self.log.warning("Orphaned instance, deleting metadata") - inst_id = inst.get("instance_id") or str(comp_id) + inst_id = inst.data.get("instance_id") or str(comp_id) CollectAERender.get_stub().remove_instance(inst_id) continue - frame_start = work_area_info.workAreaStart - frame_end = round(work_area_info.workAreaStart + - float(work_area_info.workAreaDuration) * - float(work_area_info.frameRate)) - 1 - fps = work_area_info.frameRate + frame_start = comp_info.frameStart + frame_end = round(comp_info.frameStart + + comp_info.framesDuration) - 1 + fps = comp_info.frameRate # TODO add resolution when supported by extension task_name = inst.data.get("task") # legacy diff --git a/openpype/scripts/non_python_host_launch.py b/openpype/scripts/non_python_host_launch.py index 79fb1cbb52..c95a9df314 100644 --- a/openpype/scripts/non_python_host_launch.py +++ b/openpype/scripts/non_python_host_launch.py @@ -81,9 +81,10 @@ def main(argv): host_name = os.environ["AVALON_APP"].lower() if host_name == "photoshop": + # TODO refactor launch logic according to AE from openpype.hosts.photoshop.api.lib import main elif host_name == "aftereffects": - from openpype.hosts.aftereffects.api.lib import main + from openpype.hosts.aftereffects.api.launch_logic import main elif host_name == "harmony": from openpype.hosts.harmony.api.lib import main else: diff --git a/website/docs/artist_hosts_aftereffects.md b/website/docs/artist_hosts_aftereffects.md index d9522d5765..d415a1d47d 100644 --- a/website/docs/artist_hosts_aftereffects.md +++ b/website/docs/artist_hosts_aftereffects.md @@ -15,18 +15,18 @@ sidebar_label: AfterEffects ## Setup -To install the extension, download, install [Anastasyi's Extension Manager](https://install.anastasiy.com/). Open Anastasyi's Extension Manager and select AfterEffects in menu. Then go to `{path to pype}hosts/aftereffects/api/extension.zxp`. +To install the extension, download, install [Anastasyi's Extension Manager](https://install.anastasiy.com/). Open Anastasyi's Extension Manager and select AfterEffects in menu. Then go to `{path to pype}hosts/aftereffects/api/extension.zxp`. -Drag extension.zxp and drop it to Anastasyi's Extension Manager. The extension will install itself. +Drag extension.zxp and drop it to Anastasyi's Extension Manager. The extension will install itself. ## Implemented functionality AfterEffects implementation currently allows you to import and add various media to composition (image plates, renders, audio files, video files etc.) -and send prepared composition for rendering to Deadline or render locally. +and send prepared composition for rendering to Deadline or render locally. ## Usage -When you launch AfterEffects you will be met with the Workfiles app. If don't +When you launch AfterEffects you will be met with the Workfiles app. If don't have any previous workfiles, you can just close this window. Workfiles tools takes care of saving your .AEP files in the correct location and under @@ -34,7 +34,7 @@ a correct name. You should use it instead of standard file saving dialog. In AfterEffects you'll find the tools in the `OpenPype` extension: -![Extension](assets/photoshop_extension.png) +![Extension](assets/photoshop_extension.png) You can show the extension panel by going to `Window` > `Extensions` > `OpenPype`. @@ -58,6 +58,9 @@ Name of publishable instance (eg. subset name) could be configured with a templa Trash icon under the list of instances allows to delete any selected `render` instance. +Frame information (frame start, duration, fps) and resolution (width and height) is applied to selected composition from Asset Management System (Ftrack or DB) automatically! +(Eg. number of rendered frames is controlled by settings inserted from supervisor. Artist can override this by disabling validation only in special cases.) + Workfile instance will be automatically recreated though. If you do not want to publish it, use pill toggle on the instance item. If you would like to modify publishable instance, click on `Publish` tab at the top. This would allow you to change name of publishable @@ -67,7 +70,7 @@ Publisher allows publishing into different context, just click on any instance, #### RenderQueue -AE's Render Queue is required for publishing locally or on a farm. Artist needs to configure expected result format (extension, resolution) in the Render Queue in an Output module. +AE's Render Queue is required for publishing locally or on a farm. Artist needs to configure expected result format (extension, resolution) in the Render Queue in an Output module. Currently its expected to have only single render item per composition in the Render Queue. @@ -151,3 +154,25 @@ You can switch to a previous version of the image or update to the latest. ![Loader](assets/photoshop_manage_switch.gif) ![Loader](assets/photoshop_manage_update.gif) + + +### Setting section + +Composition properties should be controlled by state in Asset Management System (Ftrack etc). Extension provides couple of buttons to trigger this propagation. + +#### Set Resolution + +Set width and height from AMS to composition. + +#### Set Frame Range + +Start frame and duration in workarea is set according to the settings in AMS. Handles are incorporated (not inclusive). +It is expected that composition(s) is selected first before pushing this button! + +#### Apply All Settings + +Both previous settings are triggered at same time. + +### Experimental tools + +Currently empty. Could contain special tools available only for specific hosts for early access testing. diff --git a/website/docs/assets/aftereffects_extension.png b/website/docs/assets/aftereffects_extension.png new file mode 100644 index 0000000000000000000000000000000000000000..b14992471a1e42cb07c16e93858f180240f3f2e3 GIT binary patch literal 12533 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 Date: Mon, 22 May 2023 11:31:56 +0200 Subject: [PATCH 309/360] files are missing issue --- openpype/hosts/nuke/plugins/publish/collect_writes.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/collect_writes.py b/openpype/hosts/nuke/plugins/publish/collect_writes.py index 6697a1e59a..dbd0963f0a 100644 --- a/openpype/hosts/nuke/plugins/publish/collect_writes.py +++ b/openpype/hosts/nuke/plugins/publish/collect_writes.py @@ -133,11 +133,12 @@ class CollectNukeWrites(pyblish.api.InstancePlugin, else: representation['files'] = collected_frames - # inject colorspace data - self.set_representation_colorspace( - representation, instance.context, - colorspace=colorspace - ) + self.log.debug("_ representation: {}".format(representation)) + # inject colorspace data + self.set_representation_colorspace( + representation, instance.context, + colorspace=colorspace + ) instance.data["representations"].append(representation) self.log.info("Publishing rendered frames ...") From 30fe6759c32fb879fe9ddb3594aabe651c057697 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 22 May 2023 13:28:09 +0200 Subject: [PATCH 310/360] Publish: Enhance automated publish plugin settings (#4986) * prepared helper functions for custom settings apply method * publish plugin can have 'settings_category' attribute to define settings category * Better 'settings_category' comment Co-authored-by: Roy Nieterau * fix trailing spaces * added more information about pyblish plugins to dev docs --------- Co-authored-by: Roy Nieterau --- openpype/pipeline/publish/__init__.py | 6 ++ openpype/pipeline/publish/lib.py | 90 ++++++++++++++++++++------- website/docs/dev_publishing.md | 63 ++++++++++++++++++- 3 files changed, 136 insertions(+), 23 deletions(-) diff --git a/openpype/pipeline/publish/__init__.py b/openpype/pipeline/publish/__init__.py index 36252c9f3d..72f3774e1a 100644 --- a/openpype/pipeline/publish/__init__.py +++ b/openpype/pipeline/publish/__init__.py @@ -36,6 +36,9 @@ from .lib import ( context_plugin_should_run, get_instance_staging_dir, get_publish_repre_path, + + apply_plugin_settings_automatically, + get_plugin_settings, ) from .abstract_expected_files import ExpectedFiles @@ -80,6 +83,9 @@ __all__ = ( "get_instance_staging_dir", "get_publish_repre_path", + "apply_plugin_settings_automatically", + "get_plugin_settings", + "ExpectedFiles", "RenderInstance", diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index 8b6212b3ef..080f93e514 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -355,29 +355,55 @@ def publish_plugins_discover(paths=None): return result -def _get_plugin_settings(host_name, project_settings, plugin, log): +def get_plugin_settings(plugin, project_settings, log, category=None): """Get plugin settings based on host name and plugin name. + Note: + Default implementation of automated settings is passing host name + into 'category'. + Args: - host_name (str): Name of host. + plugin (pyblish.Plugin): Plugin where settings are applied. project_settings (dict[str, Any]): Project settings. - plugin (pyliblish.Plugin): Plugin where settings are applied. log (logging.Logger): Logger to log messages. + category (Optional[str]): Settings category key where to look + for plugin settings. Returns: dict[str, Any]: Plugin settings {'attribute': 'value'}. """ - # Use project settings from host name category when available - try: - return ( - project_settings - [host_name] - ["publish"] - [plugin.__name__] - ) - except KeyError: - pass + # Plugin can define settings category by class attribute + # - it's impossible to set `settings_category` via settings because + # obviously settings are not applied before it. + # - if `settings_category` is set the fallback category method is ignored + settings_category = getattr(plugin, "settings_category", None) + if settings_category: + try: + return ( + project_settings + [settings_category] + ["publish"] + [plugin.__name__] + ) + except KeyError: + log.warning(( + "Couldn't find plugin '{}' settings" + " under settings category '{}'" + ).format(plugin.__name__, settings_category)) + return {} + + # Use project settings based on a category name + if category: + try: + return ( + project_settings + [category] + ["publish"] + [plugin.__name__] + ) + except KeyError: + pass # Settings category determined from path # - usually path is './/plugins/publish/' @@ -386,9 +412,10 @@ def _get_plugin_settings(host_name, project_settings, plugin, log): split_path = filepath.rsplit(os.path.sep, 5) if len(split_path) < 4: - log.warning( - 'plugin path too short to extract host {}'.format(filepath) - ) + log.debug(( + "Plugin path is too short to automatically" + " extract settings category. {}" + ).format(filepath)) return {} category_from_file = split_path[-4] @@ -410,6 +437,28 @@ def _get_plugin_settings(host_name, project_settings, plugin, log): return {} +def apply_plugin_settings_automatically(plugin, settings, logger=None): + """Automatically apply plugin settings to a plugin object. + + Note: + This function was created to be able to use it in custom overrides of + 'apply_settings' class method. + + Args: + plugin (type[pyblish.api.Plugin]): Class of a plugin. + settings (dict[str, Any]): Plugin specific settings. + logger (Optional[logging.Logger]): Logger to log debug messages about + applied settings values. + """ + + for option, value in settings.items(): + if logger: + logger.debug("Plugin {} - Attr: {} -> {}".format( + option, value, plugin.__name__ + )) + setattr(plugin, option, value) + + def filter_pyblish_plugins(plugins): """Pyblish plugin filter which applies OpenPype settings. @@ -453,13 +502,10 @@ def filter_pyblish_plugins(plugins): ) else: # Automated - plugin_settins = _get_plugin_settings( - host_name, project_settings, plugin, log + plugin_settins = get_plugin_settings( + plugin, project_settings, log, host_name ) - for option, value in plugin_settins.items(): - log.info("setting {}:{} on plugin {}".format( - option, value, plugin.__name__)) - setattr(plugin, option, value) + apply_plugin_settings_automatically(plugin, plugin_settins, log) # Remove disabled plugins if getattr(plugin, "enabled", True) is False: diff --git a/website/docs/dev_publishing.md b/website/docs/dev_publishing.md index 2c57537223..3ef6272373 100644 --- a/website/docs/dev_publishing.md +++ b/website/docs/dev_publishing.md @@ -506,6 +506,67 @@ or the scene file was copy pasted from different context. #### *Known errors* When there is a known error that can't be fixed by the user (e.g. can't connect to deadline service, etc.) `KnownPublishError` should be raised. The only difference is that its message is shown in UI to the artist otherwise a neutral message without context is shown. +### Plugins +Plugin is a single processing unit that can work with publish context and instances. + +#### Plugin types +There are 2 types of plugins - `InstancePlugin` and `ContextPlugin`. Be aware that inheritance of plugin from `InstancePlugin` or `ContextPlugin` actually does not affect if plugin is instance or context plugin, that is affected by argument name in `process` method. + +```python +import pyblish.api + + +# Context plugin +class MyContextPlugin(pyblish.api.ContextPlugin): + def process(self, context): + ... + +# Instance plugin +class MyInstancePlugin(pyblish.api.InstancePlugin): + def process(self, instance): + ... + +# Still an instance plugin +class MyOtherInstancePlugin(pyblish.api.ContextPlugin): + def process(self, instance): + ... +``` + +#### Plugin filtering +By pyblish logic, plugins have predefined filtering class attributes `hosts`, `targets` and `families`. Filter by `hosts` and `targets` are filters that are applied for current publishing process. Both filters are registered in `pyblish` module, `hosts` filtering may not match OpenPype host name (e.g. farm publishing uses `shell` in pyblish). Filter `families` works only on instance plugins and is dynamic during publish process by changing families of an instance. + +All filters are list of a strings `families = ["image"]`. Empty list is invalid filter and plugin will be skipped, to allow plugin for all values use a start `families = ["*"]`. For more detailed filtering options check [pyblish documentation](https://api.pyblish.com/pluginsystem). + +Each plugin must have order, there are 4 order milestones - Collect, Validate, Extract, Integration. Any plugin below collection order won't be processed. for more details check [pyblish documentation](https://api.pyblish.com/ordering). + +#### Plugin settings +Pyblish plugins may have settings. There are 2 ways how settings are applied, first is automated, and it's logic is based on function `filter_pyblish_plugins` in `./openpype/pipeline/publish/lib.py`, second is explicit by implementing class method `apply_settings` on a plugin. + + +Automated logic is expecting specific structure of project settings `project_settings[{category}]["plugins"]["publish"][{plugin class name}]`. The category is a key in root of project settings. There are currently 3 ways how the category key is received. +1. Use `settings_category` class attribute value from plugin. If `settings_category` is not `None` there is not any fallback to other way. +2. Use currently registered pyblish host. This will be probably deprecated soon. +3. Use 3rd folder name from a plugin filepath. From path `./maya/plugins/publish/collect_render.py` is used `maya` as the key. + +For any other use-case is recommended to use explicit approach by implementing `apply_settings` method. Must use `@classmethod` decorator and expect arguments for project settings and system settings. We're planning to support single argument with only project settings. +```python +import pyblish.api + + +class MyPlugin(pyblish.api.InstancePlugin): + profiles = [] + + @classmethod + def apply_settings(cls, project_settings, system_settings): + cls.profiles = ( + project_settings + ["addon"] + ["plugins"] + ["publish"] + ["vfx_profiles"] + ) +``` + ### Plugin extension Publish plugins can be extended by additional logic when inheriting from `OpenPypePyblishPluginMixin` which can be used as mixin (additional inheritance of class). Publish plugins that inherit from this mixin can define attributes that will be shown in **CreatedInstance**. One of the most important usages is to be able turn on/off optional plugins. @@ -596,4 +657,4 @@ Publish attributes work the same way as create attributes but the source of attr ### Create dialog ![Publisher UI - Create dialog](assets/publisher_create_dialog.png) -Create dialog is used by artist to create new instances in a context. The context selection can be enabled/disabled by changing `create_allow_context_change` on [creator plugin](#creator). In the middle part the artist selects what will be created and what variant it is. On the right side is information about the selected creator and its pre-create attributes. There is also a question mark button which extends the window and displays more detailed information about the creator. \ No newline at end of file +Create dialog is used by artist to create new instances in a context. The context selection can be enabled/disabled by changing `create_allow_context_change` on [creator plugin](#creator). In the middle part the artist selects what will be created and what variant it is. On the right side is information about the selected creator and its pre-create attributes. There is also a question mark button which extends the window and displays more detailed information about the creator. From cfa64b58e84024d7ad3b7adef8e38dd4b5dfd493 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 22 May 2023 13:38:56 +0100 Subject: [PATCH 311/360] Fix the frame range when loading camera --- .../hosts/unreal/plugins/load/load_camera.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index 2303ed1ffc..84d025b37e 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -286,6 +286,26 @@ class CameraLoader(plugin.Loader): self.fname ) + # Set range of all sections + # Changing the range of the section is not enough. We need to change + # the frame of all the keys in the section. + for possessable in cam_seq.get_possessables(): + for tracks in possessable.get_tracks(): + for section in tracks.get_sections(): + section.set_range( + data.get('clipIn'), + data.get('clipOut') + 1) + for channel in section.get_all_channels(): + for key in channel.get_keys(): + old_time = key.get_time().get_editor_property( + 'frame_number') + old_time_value = old_time.get_editor_property( + 'value') + new_time = old_time_value + ( + data.get('clipIn') - data.get('frameStart') + ) + key.set_time(unreal.FrameNumber(value = new_time)) + # Create Asset Container unreal_pipeline.create_container( container=container_name, path=asset_dir) From bf77a9e5b9cf1c0be9b2988941c3e06378698bf5 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 22 May 2023 13:43:27 +0100 Subject: [PATCH 312/360] Hound fixes --- openpype/hosts/unreal/plugins/load/load_camera.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index 84d025b37e..1bd398349f 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -304,7 +304,7 @@ class CameraLoader(plugin.Loader): new_time = old_time_value + ( data.get('clipIn') - data.get('frameStart') ) - key.set_time(unreal.FrameNumber(value = new_time)) + key.set_time(unreal.FrameNumber(value=new_time)) # Create Asset Container unreal_pipeline.create_container( From 41fbe3031f67b9ed9141939c1e70a955b8240d4d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 22 May 2023 14:59:58 +0200 Subject: [PATCH 313/360] fusion: asset_db is collecting by default. --- .../fusion/plugins/publish/collect_instances.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/collect_instances.py b/openpype/hosts/fusion/plugins/publish/collect_instances.py index 458f00c7ed..6016baa2a9 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_instances.py +++ b/openpype/hosts/fusion/plugins/publish/collect_instances.py @@ -25,15 +25,16 @@ class CollectInstanceData(pyblish.api.InstancePlugin): frame_range_source = creator_attributes.get("frame_range_source") instance.data["frame_range_source"] = frame_range_source - if frame_range_source == "asset_db": - # get asset frame ranges - start = context.data["frameStart"] - end = context.data["frameEnd"] - handle_start = context.data["handleStart"] - handle_end = context.data["handleEnd"] - start_with_handle = start - handle_start - end_with_handle = end + handle_end + # get asset frame ranges to all instances + # render family instances `asset_db` render target + start = context.data["frameStart"] + end = context.data["frameEnd"] + handle_start = context.data["handleStart"] + handle_end = context.data["handleEnd"] + start_with_handle = start - handle_start + end_with_handle = end + handle_end + # conditions for render family instances if frame_range_source == "render_range": # set comp render frame ranges start = context.data["renderFrameStart"] From 72fee37af6293b93605b323cfd191b46130c812b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 22 May 2023 16:42:12 +0200 Subject: [PATCH 314/360] Allow to open with djv by extension instead of representation name (#5004) --- openpype/plugins/load/open_djv.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/load/open_djv.py b/openpype/plugins/load/open_djv.py index bc5fd64b87..5bb7a6aaa5 100644 --- a/openpype/plugins/load/open_djv.py +++ b/openpype/plugins/load/open_djv.py @@ -19,7 +19,8 @@ class OpenInDJV(load.LoaderPlugin): djv_list = existing_djv_path() families = ["*"] if djv_list else [] - representations = [ + representations = ["*"] + extensions = [ "cin", "dpx", "avi", "dv", "gif", "flv", "mkv", "mov", "mpg", "mpeg", "mp4", "m4v", "mxf", "iff", "z", "ifl", "jpeg", "jpg", "jfif", "lut", "1dl", "exr", "pic", "png", "ppm", "pnm", "pgm", "pbm", "rla", "rpf", From bad6aa2d96f86dee6291ed664a4ccf447242d821 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 22 May 2023 16:50:38 +0200 Subject: [PATCH 315/360] DJV open action `extensions` as `set` (#5005) * Allow to open with djv by extension instead of representation name * Turn extensions into `set` like on base loader class --------- Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/plugins/load/open_djv.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/load/open_djv.py b/openpype/plugins/load/open_djv.py index 5bb7a6aaa5..9c36e7f405 100644 --- a/openpype/plugins/load/open_djv.py +++ b/openpype/plugins/load/open_djv.py @@ -20,12 +20,12 @@ class OpenInDJV(load.LoaderPlugin): djv_list = existing_djv_path() families = ["*"] if djv_list else [] representations = ["*"] - extensions = [ + extensions = { "cin", "dpx", "avi", "dv", "gif", "flv", "mkv", "mov", "mpg", "mpeg", "mp4", "m4v", "mxf", "iff", "z", "ifl", "jpeg", "jpg", "jfif", "lut", "1dl", "exr", "pic", "png", "ppm", "pnm", "pgm", "pbm", "rla", "rpf", "sgi", "rgba", "rgb", "bw", "tga", "tiff", "tif", "img", "h264", - ] + } label = "Open in DJV" order = -10 From 267bad5ba6bd0d0b7558d6d3a106fd7c2c106998 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 22 May 2023 16:59:12 +0200 Subject: [PATCH 316/360] adding multiple reposition nodes attribute to settings --- .../defaults/project_settings/nuke.json | 38 +++++++++++++++++-- .../schemas/schema_nuke_publish.json | 35 ++++++++++++++++- 2 files changed, 69 insertions(+), 4 deletions(-) diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index 85dee73176..f01bdf7d50 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -358,12 +358,12 @@ "optional": true, "active": true }, - "ValidateGizmo": { + "ValidateBackdrop": { "enabled": true, "optional": true, "active": true }, - "ValidateBackdrop": { + "ValidateGizmo": { "enabled": true, "optional": true, "active": true @@ -401,7 +401,39 @@ false ] ] - } + }, + "reposition_nodes": [ + { + "node_class": "Reformat", + "knobs": [ + { + "type": "text", + "name": "type", + "value": "to format" + }, + { + "type": "text", + "name": "format", + "value": "HD_1080" + }, + { + "type": "text", + "name": "filter", + "value": "Lanczos6" + }, + { + "type": "bool", + "name": "black_outside", + "value": true + }, + { + "type": "bool", + "name": "pbb", + "value": false + } + ] + } + ] }, "ExtractReviewData": { "enabled": false 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 ce9fa04c6a..3019c9b1b5 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 @@ -158,10 +158,43 @@ "label": "Nodes", "collapsible": true, "children": [ + { + "type": "label", + "label": "Nodes attribute will be deprecated in future releases. Use reposition_nodes instead." + }, { "type": "raw-json", "key": "nodes", - "label": "Nodes" + "label": "Nodes [depricated]" + }, + { + "type": "label", + "label": "Reposition knobs supported only. You can add multiple reformat nodes
and set their knobs. Order of reformat nodes is important. First reformat node
will be applied first and last reformat node will be applied last." + }, + { + "key": "reposition_nodes", + "type": "list", + "label": "Reposition nodes", + "object_type": { + "type": "dict", + "children": [ + { + "key": "node_class", + "label": "Node class", + "type": "text" + }, + { + "type": "schema_template", + "name": "template_nuke_knob_inputs", + "template_data": [ + { + "label": "Node knobs", + "key": "knobs" + } + ] + } + ] + } } ] } From b62d066390861ac9aa13e10fb58f2e33d17bdb58 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 22 May 2023 17:30:09 +0200 Subject: [PATCH 317/360] adding multi repositional nodes support to thumbnail exporter --- .../nuke/plugins/publish/extract_thumbnail.py | 40 +++++++++++++------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py index f391ca1e7c..2336487b37 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py @@ -5,6 +5,8 @@ import pyblish.api from openpype.pipeline import publish from openpype.hosts.nuke import api as napi +from openpype.hosts.nuke.api.lib import set_node_knobs_from_settings + if sys.version_info[0] >= 3: unicode = str @@ -28,7 +30,7 @@ class ExtractThumbnail(publish.Extractor): bake_viewer_process = True bake_viewer_input_process = True nodes = {} - + reposition_nodes = [] def process(self, instance): if instance.data.get("farm"): @@ -123,18 +125,32 @@ class ExtractThumbnail(publish.Extractor): temporary_nodes.append(rnode) previous_node = rnode - reformat_node = nuke.createNode("Reformat") - ref_node = self.nodes.get("Reformat", None) - if ref_node: - for k, v in ref_node: - self.log.debug("k, v: {0}:{1}".format(k, v)) - if isinstance(v, unicode): - v = str(v) - reformat_node[k].setValue(v) + if not self.reposition_nodes: + # [deprecated] create reformat node old way + reformat_node = nuke.createNode("Reformat") + ref_node = self.nodes.get("Reformat", None) + if ref_node: + for k, v in ref_node: + self.log.debug("k, v: {0}:{1}".format(k, v)) + if isinstance(v, unicode): + v = str(v) + reformat_node[k].setValue(v) - reformat_node.setInput(0, previous_node) - previous_node = reformat_node - temporary_nodes.append(reformat_node) + reformat_node.setInput(0, previous_node) + previous_node = reformat_node + temporary_nodes.append(reformat_node) + else: + # create reformat node new way + for repo_node in self.reposition_nodes: + node_class = repo_node["node_class"] + knobs = repo_node["knobs"] + node = nuke.createNode(node_class) + set_node_knobs_from_settings(node, knobs) + + # connect in order + node.setInput(0, previous_node) + previous_node = node + temporary_nodes.append(node) # only create colorspace baking if toggled on if bake_viewer_process: From fa74cae511070afb4edd87028ecbcffd5c7f6142 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 22 May 2023 16:55:39 +0100 Subject: [PATCH 318/360] Implemented creator, loader and extractor for Unreal Levels --- .../unreal/plugins/create/create_umap.py | 46 ++++++ .../hosts/unreal/plugins/load/load_umap.py | 140 ++++++++++++++++++ .../publish/collect_instance_members.py | 2 +- .../unreal/plugins/publish/extract_umap.py | 48 ++++++ 4 files changed, 235 insertions(+), 1 deletion(-) create mode 100644 openpype/hosts/unreal/plugins/create/create_umap.py create mode 100644 openpype/hosts/unreal/plugins/load/load_umap.py create mode 100644 openpype/hosts/unreal/plugins/publish/extract_umap.py diff --git a/openpype/hosts/unreal/plugins/create/create_umap.py b/openpype/hosts/unreal/plugins/create/create_umap.py new file mode 100644 index 0000000000..34aa8cdc00 --- /dev/null +++ b/openpype/hosts/unreal/plugins/create/create_umap.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +from pathlib import Path + +import unreal + +from openpype.pipeline import CreatorError +from openpype.hosts.unreal.api.plugin import ( + UnrealAssetCreator, +) + + +class CreateUMap(UnrealAssetCreator): + """Create Level.""" + + identifier = "io.ayon.creators.unreal.umap" + label = "Level" + family = "uasset" + icon = "cube" + + def create(self, subset_name, instance_data, pre_create_data): + if pre_create_data.get("use_selection"): + ar = unreal.AssetRegistryHelpers.get_asset_registry() + + sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() + selection = [a.get_path_name() for a in sel_objects] + + if len(selection) != 1: + raise CreatorError("Please select only one object.") + + obj = selection[0] + + asset = ar.get_asset_by_object_path(obj).get_asset() + sys_path = unreal.SystemLibrary.get_system_path(asset) + + if not sys_path: + raise CreatorError( + f"{Path(obj).name} is not on the disk. Likely it needs to" + "be saved first.") + + if Path(sys_path).suffix != ".umap": + raise CreatorError(f"{Path(sys_path).name} is not a Level.") + + super(CreateUMap, self).create( + subset_name, + instance_data, + pre_create_data) diff --git a/openpype/hosts/unreal/plugins/load/load_umap.py b/openpype/hosts/unreal/plugins/load/load_umap.py new file mode 100644 index 0000000000..f467fe6b3b --- /dev/null +++ b/openpype/hosts/unreal/plugins/load/load_umap.py @@ -0,0 +1,140 @@ +# -*- coding: utf-8 -*- +"""Load Level.""" +from pathlib import Path +import shutil + +from openpype.pipeline import ( + get_representation_path, + AYON_CONTAINER_ID +) +from openpype.hosts.unreal.api import plugin +from openpype.hosts.unreal.api import pipeline as unreal_pipeline +import unreal # noqa + + +class UMapLoader(plugin.Loader): + """Load Level.""" + + families = ["uasset"] + label = "Load Level" + representations = ["umap"] + icon = "cube" + color = "orange" + + def load(self, context, name, namespace, options): + """Load and containerise representation into Content Browser. + + 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. + options (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 Ayon container + root = "/Game/Ayon/Assets" + asset = context.get('asset').get('name') + suffix = "_CON" + 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}/{asset}/{name}", suffix="" + ) + + container_name += suffix + + unreal.EditorAssetLibrary.make_directory(asset_dir) + + destination_path = asset_dir.replace( + "/Game", + Path(unreal.Paths.project_content_dir()).as_posix(), + 1) + + shutil.copy(self.fname, f"{destination_path}/{name}.uasset") + + # Create Asset Container + unreal_pipeline.create_container( + container=container_name, path=asset_dir) + + data = { + "schema": "ayon:container-2.0", + "id": AYON_CONTAINER_ID, + "asset": asset, + "namespace": asset_dir, + "container_name": container_name, + "asset_name": asset_name, + "loader": str(self.__class__.__name__), + "representation": context["representation"]["_id"], + "parent": context["representation"]["parent"], + "family": context["representation"]["context"]["family"] + } + unreal_pipeline.imprint(f"{asset_dir}/{container_name}", data) + + asset_content = unreal.EditorAssetLibrary.list_assets( + asset_dir, recursive=True, include_folder=True + ) + + for a in asset_content: + unreal.EditorAssetLibrary.save_asset(a) + + return asset_content + + def update(self, container, representation): + ar = unreal.AssetRegistryHelpers.get_asset_registry() + + asset_dir = container["namespace"] + name = representation["context"]["subset"] + + destination_path = asset_dir.replace( + "/Game", + Path(unreal.Paths.project_content_dir()).as_posix(), + 1) + + asset_content = unreal.EditorAssetLibrary.list_assets( + asset_dir, recursive=False, include_folder=True + ) + + for asset in asset_content: + obj = ar.get_asset_by_object_path(asset).get_asset() + if obj.get_class().get_name() != 'AyonAssetContainer': + unreal.EditorAssetLibrary.delete_asset(asset) + + update_filepath = get_representation_path(representation) + + shutil.copy(update_filepath, f"{destination_path}/{name}.umap") + + container_path = f'{container["namespace"]}/{container["objectName"]}' + # update metadata + unreal_pipeline.imprint( + container_path, + { + "representation": str(representation["_id"]), + "parent": str(representation["parent"]) + }) + + asset_content = unreal.EditorAssetLibrary.list_assets( + asset_dir, recursive=True, include_folder=True + ) + + for a in asset_content: + unreal.EditorAssetLibrary.save_asset(a) + + def remove(self, container): + path = container["namespace"] + parent_path = Path(path).parent.as_posix() + + unreal.EditorAssetLibrary.delete_directory(path) + + asset_content = unreal.EditorAssetLibrary.list_assets( + parent_path, recursive=False + ) + + if len(asset_content) == 0: + unreal.EditorAssetLibrary.delete_directory(parent_path) diff --git a/openpype/hosts/unreal/plugins/publish/collect_instance_members.py b/openpype/hosts/unreal/plugins/publish/collect_instance_members.py index 46ca51ab7e..de10e7b119 100644 --- a/openpype/hosts/unreal/plugins/publish/collect_instance_members.py +++ b/openpype/hosts/unreal/plugins/publish/collect_instance_members.py @@ -24,7 +24,7 @@ class CollectInstanceMembers(pyblish.api.InstancePlugin): ar = unreal.AssetRegistryHelpers.get_asset_registry() inst_path = instance.data.get('instance_path') - inst_name = instance.data.get('objectName') + inst_name = inst_path.split('/')[-1] pub_instance = ar.get_asset_by_object_path( f"{inst_path}.{inst_name}").get_asset() diff --git a/openpype/hosts/unreal/plugins/publish/extract_umap.py b/openpype/hosts/unreal/plugins/publish/extract_umap.py new file mode 100644 index 0000000000..3812834430 --- /dev/null +++ b/openpype/hosts/unreal/plugins/publish/extract_umap.py @@ -0,0 +1,48 @@ +from pathlib import Path +import shutil + +import unreal + +from openpype.pipeline import publish + + +class ExtractUMap(publish.Extractor): + """Extract a UMap.""" + + label = "Extract Level" + hosts = ["unreal"] + families = ["uasset"] + optional = True + + def process(self, instance): + ar = unreal.AssetRegistryHelpers.get_asset_registry() + + self.log.info("Performing extraction..") + + staging_dir = self.staging_dir(instance) + filename = f"{instance.name}.umap" + + members = instance.data.get("members", []) + + if not members: + raise RuntimeError("No members found in instance.") + + # UAsset publishing supports only one member + obj = members[0] + + asset = ar.get_asset_by_object_path(obj).get_asset() + sys_path = unreal.SystemLibrary.get_system_path(asset) + filename = Path(sys_path).name + + shutil.copy(sys_path, staging_dir) + + if "representations" not in instance.data: + instance.data["representations"] = [] + + representation = { + 'name': 'umap', + 'ext': 'umap', + 'files': filename, + "stagingDir": staging_dir, + } + instance.data["representations"].append(representation) From 341dc16701a147807388df3467ce6c84094f7c40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Tue, 23 May 2023 11:22:06 +0200 Subject: [PATCH 319/360] Update openpype/hosts/nuke/plugins/publish/collect_writes.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/nuke/plugins/publish/collect_writes.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/collect_writes.py b/openpype/hosts/nuke/plugins/publish/collect_writes.py index dbd0963f0a..2d1caacdc3 100644 --- a/openpype/hosts/nuke/plugins/publish/collect_writes.py +++ b/openpype/hosts/nuke/plugins/publish/collect_writes.py @@ -133,7 +133,6 @@ class CollectNukeWrites(pyblish.api.InstancePlugin, else: representation['files'] = collected_frames - self.log.debug("_ representation: {}".format(representation)) # inject colorspace data self.set_representation_colorspace( representation, instance.context, From a762b310e8407a233185b2de18de09c4b0f44a27 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 23 May 2023 11:51:23 +0200 Subject: [PATCH 320/360] inverting logic for `ignoreFrameHandleCheck` this was ignoring settings in frame range target. --- openpype/hosts/fusion/plugins/publish/collect_render.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/publish/collect_render.py b/openpype/hosts/fusion/plugins/publish/collect_render.py index d0b7f1c4ff..551a365099 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_render.py +++ b/openpype/hosts/fusion/plugins/publish/collect_render.py @@ -84,7 +84,7 @@ class CollectFusionRender( handleStart=inst.data["handleStart"], handleEnd=inst.data["handleEnd"], ignoreFrameHandleCheck=( - inst.data["frame_range_source"] == "render_range"), + inst.data["frame_range_source"] == "asset_db"), frameStep=1, fps=comp_frame_format_prefs.get("Rate"), app_version=comp.GetApp().Version, From a68aa029e4246aa77dcb67b003b3da7e7b650430 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 23 May 2023 11:52:09 +0200 Subject: [PATCH 321/360] Renaming attribute to make more sense in Fusion context --- openpype/hosts/fusion/plugins/create/create_saver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index f1e7791972..04898d0a45 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -233,7 +233,7 @@ class CreateSaver(NewCreator): def _get_frame_range_enum(self): frame_range_options = { "asset_db": "Current asset context", - "render_range": "From viewer render in/out", + "render_range": "From render in/out", "comp_range": "From composition timeline" } From 1b3b7b1a737004416042c00f8ab48ac263200351 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 23 May 2023 12:41:33 +0200 Subject: [PATCH 322/360] Render instances with their explicit frame ranges --- .../plugins/publish/extract_render_local.py | 125 ++++++++++++------ 1 file changed, 85 insertions(+), 40 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/extract_render_local.py b/openpype/hosts/fusion/plugins/publish/extract_render_local.py index f801f30577..1663ca04fa 100644 --- a/openpype/hosts/fusion/plugins/publish/extract_render_local.py +++ b/openpype/hosts/fusion/plugins/publish/extract_render_local.py @@ -1,6 +1,7 @@ import os import logging import contextlib +import collections import pyblish.api from openpype.pipeline import publish @@ -52,11 +53,14 @@ class FusionRenderLocal( hosts = ["fusion"] families = ["render.local"] + is_rendered_key = "_fusionrenderlocal_has_rendered" + def process(self, instance): - context = instance.context # Start render - self.render_once(context) + result = self.render(instance) + if result is False: + raise RuntimeError(f"Comp render failed for {instance}") self._add_representation(instance) @@ -69,52 +73,61 @@ class FusionRenderLocal( ) ) - def render_once(self, context): - """Render context comp only once, even with more render instances""" + def render(self, instance): + """Render instance. - # This plug-in assumes all render nodes get rendered at the same time - # to speed up the rendering. The check below makes sure that we only - # execute the rendering once and not for each instance. - key = f"__hasRun{self.__class__.__name__}" + We try to render the minimal amount of times by combining the instances + that have a matching frame range in one Fusion render. Then for the + batch of instances we store whether the - savers_to_render = [ - # Get the saver tool from the instance - instance.data["tool"] for instance in context if - # Only active instances - instance.data.get("publish", True) and - # Only render.local instances - "render.local" in instance.data.get("families", []) - ] + """ - if key not in context.data: - # We initialize as false to indicate it wasn't successful yet - # so we can keep track of whether Fusion succeeded - context.data[key] = False + if self.is_rendered_key in instance.data: + # This instance was already processed in batch with another + # instance, so we just return the render result directly + self.log.debug(f"Instance {instance} was already rendered") + return instance.data[self.is_rendered_key] - current_comp = context.data["currentComp"] - frame_start = context.data["frameStartHandle"] - frame_end = context.data["frameEndHandle"] + instances_by_frame_range = self.get_render_instances_by_frame_range( + instance.context + ) - self.log.info("Starting Fusion render") - self.log.info(f"Start frame: {frame_start}") - self.log.info(f"End frame: {frame_end}") - saver_names = ", ".join(saver.Name for saver in savers_to_render) - self.log.info(f"Rendering tools: {saver_names}") + # Render matching batch of instances that share the same frame range + frame_range = self.get_instance_render_frame_range(instance) + render_instances = instances_by_frame_range[frame_range] - with comp_lock_and_undo_chunk(current_comp): - with enabled_savers(current_comp, savers_to_render): - result = current_comp.Render( - { - "Start": frame_start, - "End": frame_end, - "Wait": True, - } - ) + # We initialize render state false to indicate it wasn't successful + # yet to keep track of whether Fusion succeeded. This is for cases + # where an error below this might cause the comp render result not + # to be stored for the instances of this batch + for render_instance in render_instances: + render_instance.data[self.is_rendered_key] = False - context.data[key] = bool(result) + savers_to_render = [inst.data["tool"] for inst in render_instances] + current_comp = instance.context.data["currentComp"] + frame_start, frame_end = frame_range - if context.data[key] is False: - raise RuntimeError("Comp render failed") + self.log.info( + f"Starting Fusion render frame range {frame_start}-{frame_end}" + ) + saver_names = ", ".join(saver.Name for saver in savers_to_render) + self.log.info(f"Rendering tools: {saver_names}") + + with comp_lock_and_undo_chunk(current_comp): + with enabled_savers(current_comp, savers_to_render): + result = current_comp.Render( + { + "Start": frame_start, + "End": frame_end, + "Wait": True, + } + ) + + # Store the render state for all the rendered instances + for render_instance in render_instances: + render_instance.data[self.is_rendered_key] = bool(result) + + return result def _add_representation(self, instance): """Add representation to instance""" @@ -151,3 +164,35 @@ class FusionRenderLocal( instance.data["representations"].append(repre) return instance + + def get_render_instances_by_frame_range(self, context): + """Return enabled render.local instances grouped by their frame range. + + Arguments: + context (pyblish.Context): The pyblish context + + Returns: + dict: (start, end): instances mapping + + """ + + instances_to_render = [ + instance for instance in context if + # Only active instances + instance.data.get("publish", True) and + # Only render.local instances + "render.local" in instance.data.get("families", []) + ] + + # Instances by frame ranges + instances_by_frame_range = collections.defaultdict(list) + for instance in instances_to_render: + start, end = self.get_instance_render_frame_range(instance) + instances_by_frame_range[(start, end)].append(instance) + + return dict(instances_by_frame_range) + + def get_instance_render_frame_range(self, instance): + start = instance.data["frameStartHandle"] + end = instance.data["frameEndHandle"] + return start, end From a2007887299de2f65b21e93522fbbe4de3277c63 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 23 May 2023 11:50:34 +0100 Subject: [PATCH 323/360] Removed get_full_name() calls because of unexpected behaviour --- openpype/hosts/unreal/plugins/load/load_animation.py | 4 ++-- openpype/hosts/unreal/plugins/load/load_camera.py | 6 +++--- openpype/hosts/unreal/plugins/load/load_layout.py | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_animation.py b/openpype/hosts/unreal/plugins/load/load_animation.py index 778ddf693d..a5ecb677e8 100644 --- a/openpype/hosts/unreal/plugins/load/load_animation.py +++ b/openpype/hosts/unreal/plugins/load/load_animation.py @@ -156,7 +156,7 @@ class AnimationFBXLoader(plugin.Loader): package_paths=[f"{root}/{hierarchy[0]}"], recursive_paths=False) levels = ar.get_assets(_filter) - master_level = levels[0].get_full_name() + master_level = levels[0].get_asset().get_path_name() hierarchy_dir = root for h in hierarchy: @@ -168,7 +168,7 @@ class AnimationFBXLoader(plugin.Loader): package_paths=[f"{hierarchy_dir}/"], recursive_paths=True) levels = ar.get_assets(_filter) - level = levels[0].get_full_name() + level = levels[0].get_asset().get_path_name() unreal.EditorLevelLibrary.save_all_dirty_levels() unreal.EditorLevelLibrary.load_level(level) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index 1bd398349f..072b3b1467 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -365,7 +365,7 @@ class CameraLoader(plugin.Loader): 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(maps[0].get_asset().get_path_name()) level_sequence = sequences[0].get_asset() @@ -513,7 +513,7 @@ class CameraLoader(plugin.Loader): map = maps[0] EditorLevelLibrary.save_all_dirty_levels() - EditorLevelLibrary.load_level(map.get_full_name()) + EditorLevelLibrary.load_level(map.get_asset().get_path_name()) # Remove the camera from the level. actors = EditorLevelLibrary.get_all_level_actors() @@ -523,7 +523,7 @@ class CameraLoader(plugin.Loader): EditorLevelLibrary.destroy_actor(a) EditorLevelLibrary.save_all_dirty_levels() - EditorLevelLibrary.load_level(world.get_full_name()) + EditorLevelLibrary.load_level(world.get_asset().get_path_name()) # There should be only one sequence in the path. sequence_name = sequences[0].asset_name diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index e5f32c3412..d94e6e5837 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -740,7 +740,7 @@ class LayoutLoader(plugin.Loader): loaded_assets = self._process(self.fname, asset_dir, shot) for s in sequences: - EditorAssetLibrary.save_asset(s.get_full_name()) + EditorAssetLibrary.save_asset(s.get_path_name()) EditorLevelLibrary.save_current_level() @@ -819,7 +819,7 @@ class LayoutLoader(plugin.Loader): recursive_paths=False) levels = ar.get_assets(filter) - layout_level = levels[0].get_full_name() + layout_level = levels[0].get_asset().get_path_name() EditorLevelLibrary.save_all_dirty_levels() EditorLevelLibrary.load_level(layout_level) @@ -919,7 +919,7 @@ class LayoutLoader(plugin.Loader): package_paths=[f"{root}/{ms_asset}"], recursive_paths=False) levels = ar.get_assets(_filter) - master_level = levels[0].get_full_name() + master_level = levels[0].get_asset().get_path_name() sequences = [master_sequence] From 915c0934854f53914013291ef7d91ee39a8a23f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Tue, 23 May 2023 13:55:58 +0200 Subject: [PATCH 324/360] Update openpype/hosts/resolve/hooks/pre_resolve_setup.py Co-authored-by: Roy Nieterau --- openpype/hosts/resolve/hooks/pre_resolve_setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/resolve/hooks/pre_resolve_setup.py b/openpype/hosts/resolve/hooks/pre_resolve_setup.py index 6747e773a3..d066fc2da2 100644 --- a/openpype/hosts/resolve/hooks/pre_resolve_setup.py +++ b/openpype/hosts/resolve/hooks/pre_resolve_setup.py @@ -101,7 +101,8 @@ class ResolvePrelaunch(PreLaunchHook): self.log.debug(f"PYTHONPATH: {self.launch_context.env['PYTHONPATH']}") - # add to the python path to PATH + # add the pythonhome folder to PATH because on Windows + # this is needed for Py3 to be correctly detected within Resolve env_path = self.launch_context.env["PATH"] self.log.info(f"Adding `{python3_home_str}` to the PATH variable") self.launch_context.env[ From 4f24356139425df00ad13e5dce943d739c2f63ee Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 23 May 2023 14:20:30 +0200 Subject: [PATCH 325/360] Add validator for instance frame range to be within comp global in/out --- .../publish/validate_instance_frame_range.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 openpype/hosts/fusion/plugins/publish/validate_instance_frame_range.py diff --git a/openpype/hosts/fusion/plugins/publish/validate_instance_frame_range.py b/openpype/hosts/fusion/plugins/publish/validate_instance_frame_range.py new file mode 100644 index 0000000000..06cd0ca186 --- /dev/null +++ b/openpype/hosts/fusion/plugins/publish/validate_instance_frame_range.py @@ -0,0 +1,41 @@ +import pyblish.api + +from openpype.pipeline import PublishValidationError + + +class ValidateInstanceFrameRange(pyblish.api.InstancePlugin): + """Validate instance frame range is within comp's global render range.""" + + order = pyblish.api.ValidatorOrder + label = "Validate Filename Has Extension" + families = ["render"] + hosts = ["fusion"] + + def process(self, instance): + + context = instance.context + global_start = context.data["compFrameStart"] + global_end = context.data["compFrameEnd"] + + render_start = instance.data["frameStartHandle"] + render_end = instance.data["frameEndHandle"] + + if render_start < global_start or render_end > global_end: + + message = ( + f"Instance {instance} render frame range " + f"({render_start}-{render_end}) is outside of the comp's " + f"global render range ({global_start}-{global_end}) and thus " + f"can't be rendered. " + ) + description = ( + f"{message}\n\n" + f"Either update the comp's global range or the instance's " + f"frame range to ensure the comp's frame range includes the " + f"to render frame range for the instance." + ) + raise PublishValidationError( + title="Frame range outside of comp range", + message=message, + description=description + ) From af6ce0bf9fd9ac34f9afb7dfd39168f3258ea76f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 23 May 2023 14:21:38 +0200 Subject: [PATCH 326/360] Fix docstring --- openpype/hosts/fusion/plugins/publish/extract_render_local.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/publish/extract_render_local.py b/openpype/hosts/fusion/plugins/publish/extract_render_local.py index 1663ca04fa..564dca1796 100644 --- a/openpype/hosts/fusion/plugins/publish/extract_render_local.py +++ b/openpype/hosts/fusion/plugins/publish/extract_render_local.py @@ -78,7 +78,7 @@ class FusionRenderLocal( We try to render the minimal amount of times by combining the instances that have a matching frame range in one Fusion render. Then for the - batch of instances we store whether the + batch of instances we store whether the render succeeded or failed. """ From 81d41bb0edbd74f8196072c59f773592c94242f8 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 23 May 2023 16:18:58 +0200 Subject: [PATCH 327/360] fixing frame range data passing from instance --- .../hosts/fusion/plugins/publish/collect_render.py | 6 ++++-- .../pipeline/publish/abstract_collect_render.py | 13 ++++++++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/collect_render.py b/openpype/hosts/fusion/plugins/publish/collect_render.py index 551a365099..a20a142701 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_render.py +++ b/openpype/hosts/fusion/plugins/publish/collect_render.py @@ -17,6 +17,8 @@ class FusionRenderInstance(RenderInstance): tool = attr.ib(default=None) workfileComp = attr.ib(default=None) publish_attributes = attr.ib(default={}) + frameStartHandle = attr.ib(default=None) + frameEndHandle = attr.ib(default=None) class CollectFusionRender( @@ -83,8 +85,8 @@ class CollectFusionRender( frameEnd=inst.data["frameEnd"], handleStart=inst.data["handleStart"], handleEnd=inst.data["handleEnd"], - ignoreFrameHandleCheck=( - inst.data["frame_range_source"] == "asset_db"), + frameStartHandle=inst.data["frameStartHandle"], + frameEndHandle=inst.data["frameEndHandle"], frameStep=1, fps=comp_frame_format_prefs.get("Rate"), app_version=comp.GetApp().Version, diff --git a/openpype/pipeline/publish/abstract_collect_render.py b/openpype/pipeline/publish/abstract_collect_render.py index fd35ddb719..1e392d25e3 100644 --- a/openpype/pipeline/publish/abstract_collect_render.py +++ b/openpype/pipeline/publish/abstract_collect_render.py @@ -167,16 +167,27 @@ class AbstractCollectRender(pyblish.api.ContextPlugin): frame_start_render = int(render_instance.frameStart) frame_end_render = int(render_instance.frameEnd) + """ TODO: Needs to be refofactored because this + seems to be very hacky + """ if (render_instance.ignoreFrameHandleCheck or int(context.data['frameStartHandle']) == frame_start_render and int(context.data['frameEndHandle']) == frame_end_render): # noqa: W503, E501 - + # only for Harmony where frame range cannot be set by DB handle_start = context.data['handleStart'] handle_end = context.data['handleEnd'] frame_start = context.data['frameStart'] frame_end = context.data['frameEnd'] frame_start_handle = context.data['frameStartHandle'] frame_end_handle = context.data['frameEndHandle'] + elif (hasattr(render_instance, "frameStartHandle") + and hasattr(render_instance, "frameEndHandle")): + handle_start = int(render_instance.handleStart) + handle_end = int(render_instance.handleEnd) + frame_start = int(render_instance.frameStart) + frame_end = int(render_instance.frameEnd) + frame_start_handle = int(render_instance.frameStartHandle) + frame_end_handle = int(render_instance.frameEndHandle) else: handle_start = 0 handle_end = 0 From 561b4cb5d52e2f201a82f029a09b9b450a02085b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Tue, 23 May 2023 16:31:28 +0200 Subject: [PATCH 328/360] Update openpype/pipeline/publish/abstract_collect_render.py Co-authored-by: Roy Nieterau --- openpype/pipeline/publish/abstract_collect_render.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/pipeline/publish/abstract_collect_render.py b/openpype/pipeline/publish/abstract_collect_render.py index 1e392d25e3..6877d556c3 100644 --- a/openpype/pipeline/publish/abstract_collect_render.py +++ b/openpype/pipeline/publish/abstract_collect_render.py @@ -167,9 +167,7 @@ class AbstractCollectRender(pyblish.api.ContextPlugin): frame_start_render = int(render_instance.frameStart) frame_end_render = int(render_instance.frameEnd) - """ TODO: Needs to be refofactored because this - seems to be very hacky - """ + # TODO: Refactor hacky frame range workaround below if (render_instance.ignoreFrameHandleCheck or int(context.data['frameStartHandle']) == frame_start_render and int(context.data['frameEndHandle']) == frame_end_render): # noqa: W503, E501 From b70051b768e1b4b126b3f17fd2a734ab486edc58 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 23 May 2023 17:55:54 +0200 Subject: [PATCH 329/360] Preserve comp frame range after rendering --- openpype/hosts/fusion/api/lib.py | 33 +++++++++++++++++-- .../plugins/publish/extract_render_local.py | 19 ++++++----- 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/fusion/api/lib.py b/openpype/hosts/fusion/api/lib.py index c33209823e..1c486783be 100644 --- a/openpype/hosts/fusion/api/lib.py +++ b/openpype/hosts/fusion/api/lib.py @@ -256,8 +256,11 @@ def switch_item(container, @contextlib.contextmanager -def maintained_selection(): - comp = get_current_comp() +def maintained_selection(comp=None): + """Reset comp selection from before the context after the context""" + if comp is None: + comp = get_current_comp() + previous_selection = comp.GetToolList(True).values() try: yield @@ -268,6 +271,32 @@ def maintained_selection(): for tool in previous_selection: flow.Select(tool, True) +@contextlib.contextmanager +def maintained_comp_range(comp=None, + global_start=True, + global_end=True, + render_start=True, + render_end=True): + """Reset comp frame ranges from before the context after the context""" + if comp is None: + comp = get_current_comp() + + comp_attrs = comp.GetAttrs() + preserve_attrs = {} + if global_start: + preserve_attrs["COMPN_GlobalStart"] = comp_attrs["COMPN_GlobalStart"] + if global_end: + preserve_attrs["COMPN_GlobalEnd"] = comp_attrs["COMPN_GlobalEnd"] + if render_start: + preserve_attrs["COMPN_RenderStart"] = comp_attrs["COMPN_RenderStart"] + if render_end: + preserve_attrs["COMPN_RenderEnd"] = comp_attrs["COMPN_RenderEnd"] + + try: + yield + finally: + comp.SetAttrs(preserve_attrs) + def get_frame_path(path): """Get filename for the Fusion Saver with padded number as '#' diff --git a/openpype/hosts/fusion/plugins/publish/extract_render_local.py b/openpype/hosts/fusion/plugins/publish/extract_render_local.py index 564dca1796..25c101cf00 100644 --- a/openpype/hosts/fusion/plugins/publish/extract_render_local.py +++ b/openpype/hosts/fusion/plugins/publish/extract_render_local.py @@ -6,7 +6,7 @@ import pyblish.api from openpype.pipeline import publish from openpype.hosts.fusion.api import comp_lock_and_undo_chunk -from openpype.hosts.fusion.api.lib import get_frame_path +from openpype.hosts.fusion.api.lib import get_frame_path, maintained_comp_range log = logging.getLogger(__name__) @@ -114,14 +114,15 @@ class FusionRenderLocal( self.log.info(f"Rendering tools: {saver_names}") with comp_lock_and_undo_chunk(current_comp): - with enabled_savers(current_comp, savers_to_render): - result = current_comp.Render( - { - "Start": frame_start, - "End": frame_end, - "Wait": True, - } - ) + with maintained_comp_range(current_comp): + with enabled_savers(current_comp, savers_to_render): + result = current_comp.Render( + { + "Start": frame_start, + "End": frame_end, + "Wait": True, + } + ) # Store the render state for all the rendered instances for render_instance in render_instances: From 409929c3b8233d922463410a12f452766ee94123 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 23 May 2023 17:57:23 +0200 Subject: [PATCH 330/360] Cosmetics --- openpype/hosts/fusion/api/lib.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/fusion/api/lib.py b/openpype/hosts/fusion/api/lib.py index 1c486783be..cba8c38c2f 100644 --- a/openpype/hosts/fusion/api/lib.py +++ b/openpype/hosts/fusion/api/lib.py @@ -271,6 +271,7 @@ def maintained_selection(comp=None): for tool in previous_selection: flow.Select(tool, True) + @contextlib.contextmanager def maintained_comp_range(comp=None, global_start=True, From a73d19b612c6c4d423a8784b0598e71263213c9f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 23 May 2023 18:16:05 +0200 Subject: [PATCH 331/360] Publisher: Show instances in report page (#4915) * renamed 'validations_widget.py' to 'report_page.py' * Implemented base logic and widgets for logs * make one report page * added missing imports * added missing constants * move and rename 'VerticallScrollArea' to 'VerticalScrollArea' * Validation erro item have id * use 'ReportPageWidget' in window * change 'bg-button-hover' key to 'bg-buttons-hover' in style colors * move publish actions widgets * Refactored how validation error title is showed * remove item id from validation error item but add id to group items * remove margins from actions widget * shrink publish frame on finished publishing * fix dash line draw * add missing styles * fix dash line in thumbnail widget * added crash widget and changed layout a little * added infor overlay message * export and copy report happens in main window * fix docstrings * added per plugin filtering for validation errors * added implementation of 'FlowLayout' * actions buttons are in flow layout * fix actions order * implemented expanding text edit widget * expand button has some signals and properties * description and details are separated widgets * fix typo * added constans to '__all__' * parse icon def is a function * change layout of widgets * fix log filtering * added state icon to instances * fix pyside6 issues * implemented 'ClassicExpandBtnLabel' with arrow images * modified details separator * added some spacing to layouts * fix syle of description inputs and progress color * removed unused import * add 'is_validation_error' to errored result * validation error has different icon in logs view * added plugin name to ValueError if happens * spacer before detail inputs moved out of detals widget * fix actions visible in craash report * ignore pyblish base classes * filter base plugins in discovery * use 'is' comparison instead of '__eq__' * fix action error handling * Fix handling of 'None' values in comparison * formatting fix * Report instance card have same margins as in create mode * publish instances are grouped by family * log messages are rstripped --- openpype/pipeline/publish/lib.py | 8 + openpype/style/data.json | 9 +- openpype/style/style.css | 64 +- openpype/tools/attribute_defs/files_widget.py | 24 +- openpype/tools/publisher/constants.py | 6 + openpype/tools/publisher/control.py | 41 +- .../publish_report_viewer/widgets.py | 6 +- openpype/tools/publisher/widgets/__init__.py | 4 +- .../publisher/widgets/card_view_widgets.py | 6 +- .../tools/publisher/widgets/images/error.png | Bin 0 -> 14667 bytes .../publisher/widgets/images/success.png | Bin 0 -> 14514 bytes .../publisher/widgets/images/warning.png | Bin 9748 -> 11546 bytes .../tools/publisher/widgets/publish_frame.py | 39 +- .../tools/publisher/widgets/report_page.py | 1876 +++++++++++++++++ .../publisher/widgets/thumbnail_widget.py | 21 +- .../publisher/widgets/validations_widget.py | 715 ------- openpype/tools/publisher/widgets/widgets.py | 65 +- openpype/tools/publisher/window.py | 58 +- openpype/tools/utils/__init__.py | 7 + openpype/tools/utils/layouts.py | 150 ++ openpype/tools/utils/widgets.py | 108 +- 21 files changed, 2373 insertions(+), 834 deletions(-) create mode 100644 openpype/tools/publisher/widgets/images/error.png create mode 100644 openpype/tools/publisher/widgets/images/success.png create mode 100644 openpype/tools/publisher/widgets/report_page.py delete mode 100644 openpype/tools/publisher/widgets/validations_widget.py create mode 100644 openpype/tools/utils/layouts.py diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index 080f93e514..40186238aa 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -320,6 +320,14 @@ def publish_plugins_discover(paths=None): continue for plugin in pyblish.plugin.plugins_from_module(module): + # Ignore base plugin classes + # NOTE 'pyblish.api.discover' does not ignore them! + if ( + plugin is pyblish.api.Plugin + or plugin is pyblish.api.ContextPlugin + or plugin is pyblish.api.InstancePlugin + ): + continue if not allow_duplicates and plugin.__name__ in plugin_names: result.duplicated_plugins.append(plugin) log.debug("Duplicate plug-in found: %s", plugin) diff --git a/openpype/style/data.json b/openpype/style/data.json index bea2a3d407..7389387d97 100644 --- a/openpype/style/data.json +++ b/openpype/style/data.json @@ -26,8 +26,8 @@ "bg": "#2C313A", "bg-inputs": "#21252B", - "bg-buttons": "#434a56", - "bg-button-hover": "rgb(81, 86, 97)", + "bg-buttons": "rgb(67, 74, 86)", + "bg-buttons-hover": "rgb(81, 86, 97)", "bg-inputs-disabled": "#2C313A", "bg-buttons-disabled": "#434a56", @@ -66,7 +66,9 @@ "bg-success": "#458056", "bg-success-hover": "#55a066", "bg-error": "#AD2E2E", - "bg-error-hover": "#C93636" + "bg-error-hover": "#C93636", + "bg-info": "rgb(63, 98, 121)", + "bg-info-hover": "rgb(81, 146, 181)" }, "tab-widget": { "bg": "#21252B", @@ -94,6 +96,7 @@ "crash": "#FF6432", "success": "#458056", "warning": "#ffc671", + "progress": "rgb(194, 226, 236)", "tab-bg": "#16191d", "list-view-group": { "bg": "#434a56", diff --git a/openpype/style/style.css b/openpype/style/style.css index 827b103f94..5ce55aa658 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -136,7 +136,7 @@ QPushButton { } QPushButton:hover { - background: {color:bg-button-hover}; + background: {color:bg-buttons-hover}; color: {color:font-hover}; } @@ -166,7 +166,7 @@ QToolButton { } QToolButton:hover { - background: {color:bg-button-hover}; + background: {color:bg-buttons-hover}; color: {color:font-hover}; } @@ -722,6 +722,13 @@ OverlayMessageWidget[type="error"]:hover { background: {color:overlay-messages:bg-error-hover}; } +OverlayMessageWidget[type="info"] { + background: {color:overlay-messages:bg-info}; +} +OverlayMessageWidget[type="info"]:hover { + background: {color:overlay-messages:bg-info-hover}; +} + OverlayMessageWidget QWidget { background: transparent; } @@ -749,10 +756,11 @@ OverlayMessageWidget QWidget { } #InfoText { - padding-left: 30px; - padding-top: 20px; + padding-left: 0px; + padding-top: 0px; + padding-right: 20px; background: transparent; - border: 1px solid {color:border}; + border: none; } #TypeEditor, #ToolEditor, #NameEditor, #NumberEditor { @@ -914,7 +922,7 @@ PixmapButton{ background: {color:bg-buttons}; } PixmapButton:hover { - background: {color:bg-button-hover}; + background: {color:bg-buttons-hover}; } PixmapButton:disabled { background: {color:bg-buttons-disabled}; @@ -925,7 +933,7 @@ PixmapButton:disabled { background: {color:bg-view}; } #ThumbnailPixmapHoverButton:hover { - background: {color:bg-button-hover}; + background: {color:bg-buttons-hover}; } #CreatorDetailedDescription { @@ -946,7 +954,7 @@ PixmapButton:disabled { } #CreateDialogHelpButton:hover { - background: {color:bg-button-hover}; + background: {color:bg-buttons-hover}; } #CreateDialogHelpButton QWidget { background: transparent; @@ -1005,7 +1013,7 @@ PixmapButton:disabled { border-radius: 0.2em; } #CardViewWidget:hover { - background: {color:bg-button-hover}; + background: {color:bg-buttons-hover}; } #CardViewWidget[state="selected"] { background: {color:bg-view-selection}; @@ -1032,7 +1040,7 @@ PixmapButton:disabled { } #PublishInfoFrame[state="3"], #PublishInfoFrame[state="4"] { - background: rgb(194, 226, 236); + background: {color:publisher:progress}; } #PublishInfoFrame QLabel { @@ -1040,6 +1048,11 @@ PixmapButton:disabled { font-style: bold; } +#PublishReportHeader { + font-size: 14pt; + font-weight: bold; +} + #PublishInfoMainLabel { font-size: 12pt; } @@ -1060,7 +1073,7 @@ ValidationArtistMessage QLabel { } #ValidationActionButton:hover { - background: {color:bg-button-hover}; + background: {color:bg-buttons-hover}; color: {color:font-hover}; } @@ -1090,6 +1103,35 @@ ValidationArtistMessage QLabel { border-left: 1px solid {color:border}; } +#PublishInstancesDetails { + border: 1px solid {color:border}; + border-radius: 0.3em; +} + +#InstancesLogsView { + border: 1px solid {color:border}; + background: {color:bg-view}; + border-radius: 0.3em; +} + +#PublishLogMessage { + font-family: "Noto Sans Mono"; +} + +#PublishInstanceLogsLabel { + font-weight: bold; +} + +#PublishCrashMainLabel{ + font-weight: bold; + font-size: 16pt; +} + +#PublishCrashReportLabel { + font-weight: bold; + font-size: 13pt; +} + #AssetNameInputWidget { background: {color:bg-inputs}; border: 1px solid {color:border}; diff --git a/openpype/tools/attribute_defs/files_widget.py b/openpype/tools/attribute_defs/files_widget.py index 067866035f..076b33fb7c 100644 --- a/openpype/tools/attribute_defs/files_widget.py +++ b/openpype/tools/attribute_defs/files_widget.py @@ -198,29 +198,33 @@ class DropEmpty(QtWidgets.QWidget): def paintEvent(self, event): super(DropEmpty, self).paintEvent(event) - painter = QtGui.QPainter(self) + pen = QtGui.QPen() - pen.setWidth(1) pen.setBrush(QtCore.Qt.darkGray) pen.setStyle(QtCore.Qt.DashLine) - painter.setPen(pen) - content_margins = self.layout().contentsMargins() + pen.setWidth(1) - left_m = content_margins.left() - top_m = content_margins.top() - rect = QtCore.QRect( + content_margins = self.layout().contentsMargins() + rect = self.rect() + left_m = content_margins.left() + pen.width() + top_m = content_margins.top() + pen.width() + new_rect = QtCore.QRect( left_m, top_m, ( - self.rect().width() + rect.width() - (left_m + content_margins.right() + pen.width()) ), ( - self.rect().height() + rect.height() - (top_m + content_margins.bottom() + pen.width()) ) ) - painter.drawRect(rect) + + painter = QtGui.QPainter(self) + painter.setRenderHint(QtGui.QPainter.Antialiasing) + painter.setPen(pen) + painter.drawRect(new_rect) class FilesModel(QtGui.QStandardItemModel): diff --git a/openpype/tools/publisher/constants.py b/openpype/tools/publisher/constants.py index 660fccecf1..4630eb144b 100644 --- a/openpype/tools/publisher/constants.py +++ b/openpype/tools/publisher/constants.py @@ -35,9 +35,13 @@ ResetKeySequence = QtGui.QKeySequence( __all__ = ( "CONTEXT_ID", + "CONTEXT_LABEL", "VARIANT_TOOLTIP", + "INPUTS_LAYOUT_HSPACING", + "INPUTS_LAYOUT_VSPACING", + "INSTANCE_ID_ROLE", "SORT_VALUE_ROLE", "IS_GROUP_ROLE", @@ -47,4 +51,6 @@ __all__ = ( "FAMILY_ROLE", "GROUP_ROLE", "CONVERTER_IDENTIFIER_ROLE", + + "ResetKeySequence", ) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 4b083d4bc8..8095d00103 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -47,6 +47,7 @@ PLUGIN_ORDER_OFFSET = 0.5 class CardMessageTypes: standard = None + info = "info" error = "error" @@ -220,7 +221,12 @@ class PublishReportMaker: def _add_plugin_data_item(self, plugin): if plugin in self._stored_plugins: - raise ValueError("Plugin is already stored") + # A plugin would be processed more than once. What can cause it: + # - there is a bug in controller + # - plugin class is imported into multiple files + # - this can happen even with base classes from 'pyblish' + raise ValueError( + "Plugin '{}' is already stored".format(str(plugin))) self._stored_plugins.append(plugin) @@ -239,6 +245,7 @@ class PublishReportMaker: label = plugin.label return { + "id": plugin.id, "name": plugin.__name__, "label": label, "order": plugin.order, @@ -324,7 +331,7 @@ class PublishReportMaker: "instances": instances_details, "context": self._extract_context_data(self._current_context), "crashed_file_paths": crashed_file_paths, - "id": str(uuid.uuid4()), + "id": uuid.uuid4().hex, "report_version": "1.0.0" } @@ -342,7 +349,9 @@ class PublishReportMaker: "label": instance.data.get("label"), "family": instance.data["family"], "families": instance.data.get("families") or [], - "exists": exists + "exists": exists, + "creator_identifier": instance.data.get("creator_identifier"), + "instance_id": instance.data.get("instance_id"), } def _extract_instance_log_items(self, result): @@ -388,8 +397,11 @@ class PublishReportMaker: exception = result.get("error") if exception: fname, line_no, func, exc = exception.traceback + # Action result does not have 'is_validation_error' + is_validation_error = result.get("is_validation_error", False) output.append({ "type": "error", + "is_validation_error": is_validation_error, "msg": str(exception), "filename": str(fname), "lineno": str(line_no), @@ -426,13 +438,15 @@ class PublishPluginsProxy: plugin_id = plugin.id plugins_by_id[plugin_id] = plugin - action_ids = set() + action_ids = [] action_ids_by_plugin_id[plugin_id] = action_ids actions = getattr(plugin, "actions", None) or [] for action in actions: action_id = action.id - action_ids.add(action_id) + if action_id in actions_by_id: + continue + action_ids.append(action_id) actions_by_id[action_id] = action self._plugins_by_id = plugins_by_id @@ -461,7 +475,7 @@ class PublishPluginsProxy: return plugin.id def get_plugin_action_items(self, plugin_id): - """Get plugin action items for plugin by it's id. + """Get plugin action items for plugin by its id. Args: plugin_id (str): Publish plugin id. @@ -568,7 +582,7 @@ class ValidationErrorItem: context_validation, title, description, - detail, + detail ): self.instance_id = instance_id self.instance_label = instance_label @@ -677,6 +691,8 @@ class PublishValidationErrorsReport: for title in titles: grouped_error_items.append({ + "id": uuid.uuid4().hex, + "plugin_id": plugin_id, "plugin_action_items": list(plugin_action_items), "error_items": error_items_by_title[title], "title": title @@ -2379,7 +2395,8 @@ class PublisherController(BasePublisherController): yield MainThreadItem(self.stop_publish) # Add plugin to publish report - self._publish_report.add_plugin_iter(plugin, self._publish_context) + self._publish_report.add_plugin_iter( + plugin, self._publish_context) # WARNING This is hack fix for optional plugins if not self._is_publish_plugin_active(plugin): @@ -2461,14 +2478,14 @@ class PublisherController(BasePublisherController): plugin, self._publish_context, instance ) - self._publish_report.add_result(result) - exception = result.get("error") if exception: + has_validation_error = False if ( isinstance(exception, PublishValidationError) and not self.publish_has_validated ): + has_validation_error = True self._add_validation_error(result) else: @@ -2482,6 +2499,10 @@ class PublisherController(BasePublisherController): self.publish_error_msg = msg self.publish_has_crashed = True + result["is_validation_error"] = has_validation_error + + self._publish_report.add_result(result) + self._publish_next_process() diff --git a/openpype/tools/publisher/publish_report_viewer/widgets.py b/openpype/tools/publisher/publish_report_viewer/widgets.py index dc449b6b69..02c9b63a4e 100644 --- a/openpype/tools/publisher/publish_report_viewer/widgets.py +++ b/openpype/tools/publisher/publish_report_viewer/widgets.py @@ -163,7 +163,11 @@ class ZoomPlainText(QtWidgets.QPlainTextEdit): super(ZoomPlainText, self).wheelEvent(event) return - degrees = float(event.delta()) / 8 + if hasattr(event, "angleDelta"): + delta = event.angleDelta().y() + else: + delta = event.delta() + degrees = float(delta) / 8 steps = int(ceil(degrees / 5)) self._scheduled_scalings += steps if (self._scheduled_scalings * steps < 0): diff --git a/openpype/tools/publisher/widgets/__init__.py b/openpype/tools/publisher/widgets/__init__.py index f18e6cc61e..87a5f3914a 100644 --- a/openpype/tools/publisher/widgets/__init__.py +++ b/openpype/tools/publisher/widgets/__init__.py @@ -18,7 +18,7 @@ from .help_widget import ( from .publish_frame import PublishFrame from .tabs_widget import PublisherTabsWidget from .overview_widget import OverviewWidget -from .validations_widget import ValidationsWidget +from .report_page import ReportPageWidget __all__ = ( @@ -40,5 +40,5 @@ __all__ = ( "PublisherTabsWidget", "OverviewWidget", - "ValidationsWidget", + "ReportPageWidget", ) diff --git a/openpype/tools/publisher/widgets/card_view_widgets.py b/openpype/tools/publisher/widgets/card_view_widgets.py index 13715bc73c..eae8e0420a 100644 --- a/openpype/tools/publisher/widgets/card_view_widgets.py +++ b/openpype/tools/publisher/widgets/card_view_widgets.py @@ -93,7 +93,7 @@ class BaseGroupWidget(QtWidgets.QWidget): return self._group def get_widget_by_item_id(self, item_id): - """Get instance widget by it's id.""" + """Get instance widget by its id.""" return self._widgets_by_id.get(item_id) @@ -702,8 +702,8 @@ class InstanceCardView(AbstractInstanceView): for group_name in sorted_group_names: group_icons = { - idenfier: self._controller.get_creator_icon(idenfier) - for idenfier in identifiers_by_group[group_name] + identifier: self._controller.get_creator_icon(identifier) + for identifier in identifiers_by_group[group_name] } if group_name in self._widgets_by_group: group_widget = self._widgets_by_group[group_name] diff --git a/openpype/tools/publisher/widgets/images/error.png b/openpype/tools/publisher/widgets/images/error.png new file mode 100644 index 0000000000000000000000000000000000000000..7b09a57d7dff44b31b5f18746fa22a42b7d9f15d GIT binary patch literal 14667 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%0DjRy{e1C?ePOpy6!-*e$z;|Xnn(?D zpXQkrLy+DjASj+-wx6t?@ebwUR`0uxctB97l~{FM{J+PyxI!(i%57)%H{wYr^v%Sc zuD@pzD{>YcX>>qj2wnglaUl5^|1)Jgx$pYJQuoBT27 z!HS(8dUMi^=z60EypoUky_~5%M$mxL8 z2R1h>A<69oL)PUk=F_}VsHXhV+s);w-(V*MX|aT0=GY2jm{4v#fZN-9d$Km~UqOX; zb4%faGqpee0khM`5FU*t_mgJ9$((#;S(c_SEOkqj11tove#5k(1E=MV zrt04k$G+Mx3_*o})b*Wd0Tx^67_|8Kbh~i57CU1&5hHe!NzG}N+55G7pG6cut$r(O z4`N7-ISsy2R9lg_^yq^L@IC1WEefb-@J^4rG0Uu=!QK&opnN zp9Ov5g677MUB?iLBadN4ze7gkKA(A3dw?-YbCO;^yE3E{k;+^}a=ZkyG3Er=WF2o% z_YoURGSAYFvox+4-E*~yQ2uE&P1>*h6y9kqk)6)^p_|)Dos;y%7NLy3$FvKj-p6qF zKbBh{;^VXEG-<+f=CI~1SATBqWEYk1_F^4+7DpOS@4{m7{!fL6P`W!M?$F{i_asl~ zQ852cI~rG;wI*`@!?9b@rR6o>c!JHN9iNnJ$}t!@#|ub$Plbj!n|9?_*e1zVzXp!< zkGb~@8NgR?I-Mzs=&?{Y0oZz`a za)sOnG`CdsbCC;y2$yf?K)_2})y=DD(K9a^h>ay?D@VxO4xY8!JkA6P@!|`(Naao) zz=C{oXdbkqw0X)xO)=U?V-Aq#6o8F5y{N`k=dA#023iQ}db)3j3&;G{ z?w@D<_AUX&p~aE|uZ zR?*dVYJ($J-9kr$N(2W5bWQNVL%zu=$)*07y{&LyzIKC)?Z}!t!CuxTQ*X5&j8||HAB_g{)ipOgnE^QtdUK57#q#rTJRX|9ioH!!{ zT42+F$UZ!5EC^V$_fW~>rJ=mnhS2w^$-D?E#Vted$&DFTMVO8X&uaM zFKtAPP<8iv?z{TS(6+$8 zsG#&F1r|-_tV=odOOeSW_LJ@!0E+tM{=d1zt>}-+*Psc&qiuBPmd10&gDOl7c{?AV zwaDkqZ91*NP3n#wTOg=7svLK5C8N{PfyeU zIJHO?boR3Z+?3n^qF?ayxa3#KoZzuXH{$0#PEr?1fcd!LTk;LR=?OfLov4PjNr0}N z(+5#zu)TKSRR`EJcWL2!Tcs{Jd-9^I^vgph=Q=_;71;2hO z5~HB@*;fYUBlW5atjaO=P&6pEr^WE-0=uFwD_VXpOD!(SK;?C;M zCdwc~`WSCt`V{cn=~>ZG%jk1YaP2~jBBQ1j zq`W5VQ201AOgVy8RU99DCzDjeXPFh?dnBw;>&Y-RE+$92Bo%Gf98o5Yp9m?a*lG-dU4LeVWjy^;ab* zkMsD z#hFH5zT)j5cpQB2jvMqK#D}Fdc|U8TZFkw}W+B|ApQ}lwMdno*|L)h&`WHq^VC$!m zef-xD(*iat+noRFk`41}j&uLAG{=P-Ki)VVua-Hy8I>Nl;qc>4&P*WUwqt{kBOBd3 zgZmL#)RHUi;-%8RJ?OcvOc~7{q|t?Rm4y&Bf7V!LQeGB|}rpv{T`-Q!kF=hAMZ=WmoNg-)j+rm`& z1nqA*`7e=1A|K8G-su!9bYkSz%{vybEp1(G7Tjin1#aG{(5i#7tCr$RJOx_?3$E|2 zx+H-)EqQpgws;E#-YSFpN;BGsA>JX<(1V}m@cN0i>YUw*!-_Ozzo=rgFKD5$kAV*T zZd(x$%bnWt(o6^mCGKL6@Pe3!pW9s7c@RAO#hbPz!yOZv?fUrwcAzY<3RBD3j}#JQ zd3bGY9_@P!l`lIbzqr18a(gW|A%07PQ+LHYA%X42(EsQD81vPM&7IXwpVa_16%lan O4Z@q-n^l_-F8mKWCTVE^ literal 0 HcmV?d00001 diff --git a/openpype/tools/publisher/widgets/images/warning.png b/openpype/tools/publisher/widgets/images/warning.png index 76d1e34b6ccf951b166db53cd59d8183cded7586..531f62b741804a2fa48769c17f9e29fbb0cd5429 100644 GIT binary patch literal 11546 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 E3l)7W-~a#s diff --git a/openpype/tools/publisher/widgets/publish_frame.py b/openpype/tools/publisher/widgets/publish_frame.py index e4e6740532..d21130deff 100644 --- a/openpype/tools/publisher/widgets/publish_frame.py +++ b/openpype/tools/publisher/widgets/publish_frame.py @@ -468,45 +468,14 @@ class PublishFrame(QtWidgets.QWidget): widget.setProperty("state", state) widget.style().polish(widget) - def _copy_report(self): - logs = self._controller.get_publish_report() - logs_string = json.dumps(logs, indent=4) - - mime_data = QtCore.QMimeData() - mime_data.setText(logs_string) - QtWidgets.QApplication.instance().clipboard().setMimeData( - mime_data - ) - - def _export_report(self): - default_filename = "publish-report-{}".format( - time.strftime("%y%m%d-%H-%M") - ) - default_filepath = os.path.join( - os.path.expanduser("~"), - default_filename - ) - new_filepath, ext = QtWidgets.QFileDialog.getSaveFileName( - self, "Save report", default_filepath, ".json" - ) - if not ext or not new_filepath: - return - - logs = self._controller.get_publish_report() - full_path = new_filepath + ext - dir_path = os.path.dirname(full_path) - if not os.path.exists(dir_path): - os.makedirs(dir_path) - - with open(full_path, "w") as file_stream: - json.dump(logs, file_stream) - def _on_report_triggered(self, identifier): if identifier == "export_report": - self._export_report() + self._controller.event_system.emit( + "export_report.request", {}, "publish_frame") elif identifier == "copy_report": - self._copy_report() + self._controller.event_system.emit( + "copy_report.request", {}, "publish_frame") elif identifier == "go_to_report": self.details_page_requested.emit() diff --git a/openpype/tools/publisher/widgets/report_page.py b/openpype/tools/publisher/widgets/report_page.py new file mode 100644 index 0000000000..50a619f0a8 --- /dev/null +++ b/openpype/tools/publisher/widgets/report_page.py @@ -0,0 +1,1876 @@ +# -*- coding: utf-8 -*- +import collections +import logging + +try: + import commonmark +except Exception: + commonmark = None + +from qtpy import QtWidgets, QtCore, QtGui + +from openpype.style import get_objected_colors +from openpype.tools.utils import ( + BaseClickableFrame, + ClickableFrame, + ExpandingTextEdit, + FlowLayout, + ClassicExpandBtn, + paint_image_with_color, + SeparatorWidget, +) +from .widgets import IconValuePixmapLabel +from .icons import ( + get_pixmap, + get_image, +) +from ..constants import ( + INSTANCE_ID_ROLE, + CONTEXT_ID, + CONTEXT_LABEL, +) + +LOG_DEBUG_VISIBLE = 1 << 0 +LOG_INFO_VISIBLE = 1 << 1 +LOG_WARNING_VISIBLE = 1 << 2 +LOG_ERROR_VISIBLE = 1 << 3 +LOG_CRITICAL_VISIBLE = 1 << 4 +ERROR_VISIBLE = 1 << 5 +INFO_VISIBLE = 1 << 6 + + +class VerticalScrollArea(QtWidgets.QScrollArea): + """Scroll area for validation error titles. + + The biggest difference is that the scroll area has scroll bar on left side + and resize of content will also resize scrollarea itself. + + Resize if deferred by 100ms because at the moment of resize are not yet + propagated sizes and visibility of scroll bars. + """ + + def __init__(self, *args, **kwargs): + super(VerticalScrollArea, self).__init__(*args, **kwargs) + + self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) + self.setLayoutDirection(QtCore.Qt.RightToLeft) + + self.setAttribute(QtCore.Qt.WA_TranslucentBackground) + # Background of scrollbar will be transparent + scrollbar_bg = self.verticalScrollBar().parent() + if scrollbar_bg: + scrollbar_bg.setAttribute(QtCore.Qt.WA_TranslucentBackground) + self.setViewportMargins(0, 0, 0, 0) + + self.verticalScrollBar().installEventFilter(self) + + # Timer with 100ms offset after changing size + size_changed_timer = QtCore.QTimer() + size_changed_timer.setInterval(100) + size_changed_timer.setSingleShot(True) + + size_changed_timer.timeout.connect(self._on_timer_timeout) + self._size_changed_timer = size_changed_timer + + def setVerticalScrollBar(self, widget): + old_widget = self.verticalScrollBar() + if old_widget: + old_widget.removeEventFilter(self) + + super(VerticalScrollArea, self).setVerticalScrollBar(widget) + if widget: + widget.installEventFilter(self) + + def setWidget(self, widget): + old_widget = self.widget() + if old_widget: + old_widget.removeEventFilter(self) + + super(VerticalScrollArea, self).setWidget(widget) + if widget: + widget.installEventFilter(self) + + def _on_timer_timeout(self): + width = self.widget().width() + if self.verticalScrollBar().isVisible(): + width += self.verticalScrollBar().width() + self.setMinimumWidth(width) + + def eventFilter(self, obj, event): + if ( + event.type() == QtCore.QEvent.Resize + and (obj is self.widget() or obj is self.verticalScrollBar()) + ): + self._size_changed_timer.start() + return super(VerticalScrollArea, self).eventFilter(obj, event) + + +# --- Publish actions widget --- +class ActionButton(BaseClickableFrame): + """Plugin's action callback button. + + Action may have label or icon or both. + + Args: + plugin_action_item (PublishPluginActionItem): Action item that can be + triggered by its id. + """ + + action_clicked = QtCore.Signal(str, str) + + def __init__(self, plugin_action_item, parent): + super(ActionButton, self).__init__(parent) + + self.setObjectName("ValidationActionButton") + + self.plugin_action_item = plugin_action_item + + action_label = plugin_action_item.label + action_icon = plugin_action_item.icon + label_widget = QtWidgets.QLabel(action_label, self) + icon_label = None + if action_icon: + icon_label = IconValuePixmapLabel(action_icon, self) + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(5, 0, 5, 0) + layout.addWidget(label_widget, 1) + if icon_label: + layout.addWidget(icon_label, 0) + + self.setSizePolicy( + QtWidgets.QSizePolicy.Minimum, + self.sizePolicy().verticalPolicy() + ) + + def _mouse_release_callback(self): + self.action_clicked.emit( + self.plugin_action_item.plugin_id, + self.plugin_action_item.action_id + ) + + +class ValidateActionsWidget(QtWidgets.QFrame): + """Wrapper widget for plugin actions. + + Change actions based on selected validation error. + """ + + def __init__(self, controller, parent): + super(ValidateActionsWidget, self).__init__(parent) + + self.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + content_widget = QtWidgets.QWidget(self) + content_layout = FlowLayout(content_widget) + content_layout.setContentsMargins(0, 0, 0, 0) + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(content_widget) + + self._controller = controller + self._content_widget = content_widget + self._content_layout = content_layout + + self._actions_mapping = {} + + self._visible_mode = True + + def _update_visibility(self): + self.setVisible( + self._visible_mode + and self._content_layout.count() > 0 + ) + + def set_visible_mode(self, visible): + if self._visible_mode is visible: + return + self._visible_mode = visible + self._update_visibility() + + def _clear(self): + """Remove actions from widget.""" + while self._content_layout.count(): + item = self._content_layout.takeAt(0) + widget = item.widget() + if widget: + widget.setVisible(False) + widget.deleteLater() + self._actions_mapping = {} + + def set_error_info(self, error_info): + """Set selected plugin and show it's actions. + + Clears current actions from widget and recreate them from the plugin. + + Args: + Dict[str, Any]: Object holding error items, title and possible + actions to run. + """ + + self._clear() + + if not error_info: + self.setVisible(False) + return + + plugin_action_items = error_info["plugin_action_items"] + for plugin_action_item in plugin_action_items: + if not plugin_action_item.active: + continue + + if plugin_action_item.on_filter not in ("failed", "all"): + continue + + action_id = plugin_action_item.action_id + self._actions_mapping[action_id] = plugin_action_item + + action_btn = ActionButton(plugin_action_item, self._content_widget) + action_btn.action_clicked.connect(self._on_action_click) + self._content_layout.addWidget(action_btn) + + self._update_visibility() + + def _on_action_click(self, plugin_id, action_id): + self._controller.run_action(plugin_id, action_id) + + +# --- Validation error titles --- +class ValidationErrorInstanceList(QtWidgets.QListView): + """List of publish instances that caused a validation error. + + Instances are collected per plugin's validation error title. + """ + def __init__(self, *args, **kwargs): + super(ValidationErrorInstanceList, self).__init__(*args, **kwargs) + + self.setObjectName("ValidationErrorInstanceList") + + self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) + + def minimumSizeHint(self): + return self.sizeHint() + + def sizeHint(self): + result = super(ValidationErrorInstanceList, self).sizeHint() + row_count = self.model().rowCount() + height = 0 + if row_count > 0: + height = self.sizeHintForRow(0) * row_count + result.setHeight(height) + return result + + +class ValidationErrorTitleWidget(QtWidgets.QWidget): + """Title of validation error. + + Widget is used as radio button so requires clickable functionality and + changing style on selection/deselection. + + Has toggle button to show/hide instances on which validation error happened + if there is a list (Valdation error may happen on context). + """ + + selected = QtCore.Signal(str) + instance_changed = QtCore.Signal(str) + + def __init__(self, title_id, error_info, parent): + super(ValidationErrorTitleWidget, self).__init__(parent) + + self._title_id = title_id + self._error_info = error_info + self._selected = False + + title_frame = ClickableFrame(self) + title_frame.setObjectName("ValidationErrorTitleFrame") + + toggle_instance_btn = QtWidgets.QToolButton(title_frame) + toggle_instance_btn.setObjectName("ArrowBtn") + toggle_instance_btn.setArrowType(QtCore.Qt.RightArrow) + toggle_instance_btn.setMaximumWidth(14) + + label_widget = QtWidgets.QLabel(error_info["title"], title_frame) + + title_frame_layout = QtWidgets.QHBoxLayout(title_frame) + title_frame_layout.addWidget(label_widget, 1) + title_frame_layout.addWidget(toggle_instance_btn, 0) + + instances_model = QtGui.QStandardItemModel() + + instance_ids = [] + + items = [] + context_validation = False + for error_item in error_info["error_items"]: + context_validation = error_item.context_validation + if context_validation: + toggle_instance_btn.setArrowType(QtCore.Qt.NoArrow) + instance_ids.append(CONTEXT_ID) + # Add fake item to have minimum size hint of view widget + items.append(QtGui.QStandardItem(CONTEXT_LABEL)) + continue + + label = error_item.instance_label + item = QtGui.QStandardItem(label) + item.setFlags( + QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable + ) + item.setData(label, QtCore.Qt.ToolTipRole) + item.setData(error_item.instance_id, INSTANCE_ID_ROLE) + items.append(item) + instance_ids.append(error_item.instance_id) + + if items: + root_item = instances_model.invisibleRootItem() + root_item.appendRows(items) + + instances_view = ValidationErrorInstanceList(self) + instances_view.setModel(instances_model) + + self.setLayoutDirection(QtCore.Qt.LeftToRight) + + view_widget = QtWidgets.QWidget(self) + view_layout = QtWidgets.QHBoxLayout(view_widget) + view_layout.setContentsMargins(0, 0, 0, 0) + view_layout.setSpacing(0) + view_layout.addSpacing(14) + view_layout.addWidget(instances_view, 0) + + layout = QtWidgets.QVBoxLayout(self) + layout.setSpacing(0) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(title_frame, 0) + layout.addWidget(view_widget, 0) + view_widget.setVisible(False) + + if not context_validation: + toggle_instance_btn.clicked.connect(self._on_toggle_btn_click) + + title_frame.clicked.connect(self._mouse_release_callback) + instances_view.selectionModel().selectionChanged.connect( + self._on_selection_change + ) + + self._title_frame = title_frame + + self._toggle_instance_btn = toggle_instance_btn + + self._view_widget = view_widget + + self._instances_model = instances_model + self._instances_view = instances_view + + self._context_validation = context_validation + + self._instance_ids = instance_ids + self._expanded = False + + def sizeHint(self): + result = super(ValidationErrorTitleWidget, self).sizeHint() + expected_width = max( + self._view_widget.minimumSizeHint().width(), + self._view_widget.sizeHint().width() + ) + + if expected_width < 200: + expected_width = 200 + + if result.width() < expected_width: + result.setWidth(expected_width) + + return result + + def minimumSizeHint(self): + return self.sizeHint() + + def _mouse_release_callback(self): + """Mark this widget as selected on click.""" + + self.set_selected(True) + + @property + def is_selected(self): + """Is widget marked a selected. + + Returns: + bool: Item is selected or not. + """ + + return self._selected + + @property + def id(self): + return self._title_id + + def _change_style_property(self, selected): + """Change style of widget based on selection.""" + + value = "1" if selected else "" + self._title_frame.setProperty("selected", value) + self._title_frame.style().polish(self._title_frame) + + def set_selected(self, selected=None): + """Change selected state of widget.""" + + if selected is None: + selected = not self._selected + + # Clear instance view selection on deselect + if not selected: + self._instances_view.clearSelection() + + # Skip if has same value + if selected == self._selected: + return + + self._selected = selected + self._change_style_property(selected) + if selected: + self.selected.emit(self._title_id) + self._set_expanded(True) + + def _on_toggle_btn_click(self): + """Show/hide instances list.""" + + self._set_expanded() + + def _set_expanded(self, expanded=None): + if expanded is None: + expanded = not self._expanded + + elif expanded is self._expanded: + return + + if expanded and self._context_validation: + return + + self._expanded = expanded + self._view_widget.setVisible(expanded) + if expanded: + self._toggle_instance_btn.setArrowType(QtCore.Qt.DownArrow) + else: + self._toggle_instance_btn.setArrowType(QtCore.Qt.RightArrow) + + def _on_selection_change(self): + self.instance_changed.emit(self._title_id) + + def get_selected_instances(self): + if self._context_validation: + return [CONTEXT_ID] + sel_model = self._instances_view.selectionModel() + return [ + index.data(INSTANCE_ID_ROLE) + for index in sel_model.selectedIndexes() + if index.isValid() + ] + + def get_available_instances(self): + return list(self._instance_ids) + + +class ValidationArtistMessage(QtWidgets.QWidget): + def __init__(self, message, parent): + super(ValidationArtistMessage, self).__init__(parent) + + artist_msg_label = QtWidgets.QLabel(message, self) + artist_msg_label.setAlignment(QtCore.Qt.AlignCenter) + + main_layout = QtWidgets.QHBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.addWidget( + artist_msg_label, 1, QtCore.Qt.AlignCenter + ) + + +class ValidationErrorsView(QtWidgets.QWidget): + selection_changed = QtCore.Signal() + + def __init__(self, parent): + super(ValidationErrorsView, self).__init__(parent) + + errors_scroll = VerticalScrollArea(self) + errors_scroll.setWidgetResizable(True) + + errors_widget = QtWidgets.QWidget(errors_scroll) + errors_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + errors_scroll.setWidget(errors_widget) + + errors_layout = QtWidgets.QVBoxLayout(errors_widget) + errors_layout.setContentsMargins(0, 0, 0, 0) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(errors_scroll, 1) + + self._errors_widget = errors_widget + self._errors_layout = errors_layout + self._title_widgets = {} + self._previous_select = None + + def _clear(self): + """Delete all dynamic widgets and hide all wrappers.""" + + self._title_widgets = {} + self._previous_select = None + while self._errors_layout.count(): + item = self._errors_layout.takeAt(0) + widget = item.widget() + if widget: + widget.deleteLater() + + def set_errors(self, grouped_error_items): + """Set errors into context and created titles. + + Args: + validation_error_report (PublishValidationErrorsReport): Report + with information about validation errors and publish plugin + actions. + """ + + self._clear() + + first_id = None + for title_item in grouped_error_items: + title_id = title_item["id"] + if first_id is None: + first_id = title_id + widget = ValidationErrorTitleWidget(title_id, title_item, self) + widget.selected.connect(self._on_select) + widget.instance_changed.connect(self._on_instance_change) + self._errors_layout.addWidget(widget) + self._title_widgets[title_id] = widget + + self._errors_layout.addStretch(1) + + if first_id: + self._title_widgets[first_id].set_selected(True) + else: + self.selection_changed.emit() + + self.updateGeometry() + + def _on_select(self, title_id): + if self._previous_select: + if self._previous_select.id == title_id: + return + self._previous_select.set_selected(False) + + self._previous_select = self._title_widgets[title_id] + self.selection_changed.emit() + + def _on_instance_change(self, title_id): + if self._previous_select and self._previous_select.id != title_id: + self._title_widgets[title_id].set_selected(True) + else: + self.selection_changed.emit() + + def get_selected_items(self): + if not self._previous_select: + return None, [] + + title_id = self._previous_select.id + instance_ids = self._previous_select.get_selected_instances() + if not instance_ids: + instance_ids = self._previous_select.get_available_instances() + return title_id, instance_ids + + +# ----- Publish instance report ----- +class _InstanceItem: + """Publish instance item for report UI. + + Contains only data related to an instance in publishing. Has implemented + sorting methods and prepares information, e.g. if contains error or + warnings. + """ + + _attrs = ( + "creator_identifier", + "family", + "label", + "name", + ) + + def __init__( + self, + instance_id, + creator_identifier, + family, + name, + label, + exists, + logs, + errored, + warned + ): + self.id = instance_id + self.creator_identifier = creator_identifier + self.family = family + self.name = name + self.label = label + self.exists = exists + self.logs = logs + self.errored = errored + self.warned = warned + + def __eq__(self, other): + for attr in self._attrs: + if getattr(self, attr) != getattr(other, attr): + return False + return True + + def __ne__(self, other): + return not self.__eq__(other) + + def __gt__(self, other): + for attr in self._attrs: + self_value = getattr(self, attr) + other_value = getattr(other, attr) + if self_value == other_value: + continue + values = [self_value, other_value] + values.sort() + return values[0] == other_value + return None + + def __lt__(self, other): + for attr in self._attrs: + self_value = getattr(self, attr) + other_value = getattr(other, attr) + if self_value == other_value: + continue + if self_value is None: + return False + if other_value is None: + return True + values = [self_value, other_value] + values.sort() + return values[0] == self_value + return None + + def __ge__(self, other): + if self == other: + return True + return self.__gt__(other) + + def __le__(self, other): + if self == other: + return True + return self.__lt__(other) + + @classmethod + def from_report(cls, instance_id, instance_data, logs): + errored, warned = cls.extract_basic_log_info(logs) + + return cls( + instance_id, + instance_data["creator_identifier"], + instance_data["family"], + instance_data["name"], + instance_data["label"], + instance_data["exists"], + logs, + errored, + warned, + ) + + @classmethod + def create_context_item(cls, context_label, logs): + errored, warned = cls.extract_basic_log_info(logs) + return cls( + CONTEXT_ID, + None, + "", + CONTEXT_LABEL, + context_label, + True, + logs, + errored, + warned + ) + + @staticmethod + def extract_basic_log_info(logs): + warned = False + errored = False + for log in logs: + if log["type"] == "error": + errored = True + elif log["type"] == "record": + level_no = log["levelno"] + if level_no and level_no >= logging.WARNING: + warned = True + + if warned and errored: + break + return errored, warned + + +class FamilyGroupLabel(QtWidgets.QWidget): + def __init__(self, family, parent): + super(FamilyGroupLabel, self).__init__(parent) + + self.setLayoutDirection(QtCore.Qt.LeftToRight) + + label_widget = QtWidgets.QLabel(family, self) + + line_widget = QtWidgets.QWidget(self) + line_widget.setObjectName("Separator") + line_widget.setMinimumHeight(2) + line_widget.setMaximumHeight(2) + + main_layout = QtWidgets.QHBoxLayout(self) + main_layout.setAlignment(QtCore.Qt.AlignVCenter) + main_layout.setSpacing(10) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.addWidget(label_widget, 0) + main_layout.addWidget(line_widget, 1) + + +class PublishInstanceCardWidget(BaseClickableFrame): + selection_requested = QtCore.Signal(str) + + _warning_pix = None + _error_pix = None + _success_pix = None + _in_progress_pix = None + + def __init__(self, instance, icon, publish_finished, parent): + super(PublishInstanceCardWidget, self).__init__(parent) + + self.setObjectName("CardViewWidget") + + icon_widget = IconValuePixmapLabel(icon, self) + icon_widget.setObjectName("FamilyIconLabel") + + label_widget = QtWidgets.QLabel(instance.label, self) + + if instance.errored: + state_pix = self.get_error_pix() + elif instance.warned: + state_pix = self.get_warning_pix() + elif publish_finished: + state_pix = self.get_success_pix() + else: + state_pix = self.get_in_progress_pix() + + state_label = IconValuePixmapLabel(state_pix, self) + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(10, 7, 10, 7) + layout.addWidget(icon_widget, 0) + layout.addWidget(label_widget, 1) + layout.addWidget(state_label, 0) + + # Change direction -> parent is scroll area where scrolls are on + # left side + self.setLayoutDirection(QtCore.Qt.LeftToRight) + + self._id = instance.id + + self._selected = False + + self._update_style_state() + + @classmethod + def _prepare_pixes(cls): + publisher_colors = get_objected_colors("publisher") + cls._warning_pix = paint_image_with_color( + get_image("warning"), + publisher_colors["warning"].get_qcolor() + ) + cls._error_pix = paint_image_with_color( + get_image("error"), + publisher_colors["error"].get_qcolor() + ) + cls._success_pix = paint_image_with_color( + get_image("success"), + publisher_colors["success"].get_qcolor() + ) + cls._in_progress_pix = paint_image_with_color( + get_image("success"), + publisher_colors["progress"].get_qcolor() + ) + + @classmethod + def get_warning_pix(cls): + if cls._warning_pix is None: + cls._prepare_pixes() + return cls._warning_pix + + @classmethod + def get_error_pix(cls): + if cls._error_pix is None: + cls._prepare_pixes() + return cls._error_pix + + @classmethod + def get_success_pix(cls): + if cls._success_pix is None: + cls._prepare_pixes() + return cls._success_pix + + @classmethod + def get_in_progress_pix(cls): + if cls._in_progress_pix is None: + cls._prepare_pixes() + return cls._in_progress_pix + + @property + def id(self): + """Id of card. + + Returns: + str: Id of item. + """ + + return self._id + + @property + def is_selected(self): + """Is card selected. + + Returns: + bool: Item widget is marked as selected. + """ + + return self._selected + + def set_selected(self, selected): + """Set card as selected. + + Args: + selected (bool): Item should be marked as selected. + """ + + if selected == self._selected: + return + self._selected = selected + self._update_style_state() + + def _update_style_state(self): + state = "" + if self._selected: + state = "selected" + + self.setProperty("state", state) + self.style().polish(self) + + def _mouse_release_callback(self): + """Trigger selected signal.""" + + self.selection_requested.emit(self.id) + + +class PublishInstancesViewWidget(QtWidgets.QWidget): + # Sane minimum width of instance cards - size calulated using font metrics + _min_width_measure_string = 24 * "O" + selection_changed = QtCore.Signal() + + def __init__(self, controller, parent): + super(PublishInstancesViewWidget, self).__init__(parent) + + scroll_area = VerticalScrollArea(self) + scroll_area.setWidgetResizable(True) + scroll_area.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + scroll_area.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) + scrollbar_bg = scroll_area.verticalScrollBar().parent() + if scrollbar_bg: + scrollbar_bg.setAttribute(QtCore.Qt.WA_TranslucentBackground) + scroll_area.setViewportMargins(0, 0, 0, 0) + + instance_view = QtWidgets.QWidget(scroll_area) + + scroll_area.setWidget(instance_view) + + instance_layout = QtWidgets.QVBoxLayout(instance_view) + instance_layout.setContentsMargins(0, 0, 0, 0) + instance_layout.addStretch(1) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(scroll_area, 1) + + self._controller = controller + self._scroll_area = scroll_area + self._instance_view = instance_view + self._instance_layout = instance_layout + + self._context_widget = None + + self._widgets_by_instance_id = {} + self._group_widgets = [] + self._ordered_widgets = [] + + self._explicitly_selected_instance_ids = [] + + self.setSizePolicy( + QtWidgets.QSizePolicy.Minimum, + self.sizePolicy().verticalPolicy() + ) + + def sizeHint(self): + """Modify sizeHint based on visibility of scroll bars.""" + # Calculate width hint by content widget and vertical scroll bar + scroll_bar = self._scroll_area.verticalScrollBar() + view_size = self._instance_view.sizeHint().width() + fm = self._instance_view.fontMetrics() + width = ( + max(view_size, fm.width(self._min_width_measure_string)) + + scroll_bar.sizeHint().width() + ) + + result = super(PublishInstancesViewWidget, self).sizeHint() + result.setWidth(width) + return result + + def _get_selected_widgets(self): + return [ + widget + for widget in self._ordered_widgets + if widget.is_selected + ] + + def get_selected_instance_ids(self): + return [ + widget.id + for widget in self._get_selected_widgets() + ] + + def clear(self): + """Remove actions from widget.""" + while self._instance_layout.count(): + item = self._instance_layout.takeAt(0) + widget = item.widget() + if widget: + widget.setVisible(False) + widget.deleteLater() + self._ordered_widgets = [] + self._group_widgets = [] + self._widgets_by_instance_id = {} + + def update_instances(self, instance_items): + self.clear() + identifiers = { + instance_item.creator_identifier + for instance_item in instance_items + } + identifier_icons = { + identifier: self._controller.get_creator_icon(identifier) + for identifier in identifiers + } + + widgets = [] + group_widgets = [] + + publish_finished = ( + self._controller.publish_has_crashed + or self._controller.publish_has_validation_errors + or self._controller.publish_has_finished + ) + instances_by_family = collections.defaultdict(list) + for instance_item in instance_items: + if not instance_item.exists: + continue + instances_by_family[instance_item.family].append(instance_item) + + sorted_by_family = sorted( + instances_by_family.items(), key=lambda i: i[0] + ) + for family, instance_items in sorted_by_family: + # Only instance without family is context + if family: + group_widget = FamilyGroupLabel(family, self._instance_view) + self._instance_layout.addWidget(group_widget, 0) + group_widgets.append(group_widget) + + sorted_items = sorted(instance_items, key=lambda i: i.label) + for instance_item in sorted_items: + icon = identifier_icons[instance_item.creator_identifier] + + widget = PublishInstanceCardWidget( + instance_item, icon, publish_finished, self._instance_view + ) + widget.selection_requested.connect(self._on_selection_request) + self._instance_layout.addWidget(widget, 0) + + widgets.append(widget) + self._widgets_by_instance_id[widget.id] = widget + self._instance_layout.addStretch(1) + self._ordered_widgets = widgets + self._group_widgets = group_widgets + + def _on_selection_request(self, instance_id): + instance_widget = self._widgets_by_instance_id[instance_id] + selected_widgets = self._get_selected_widgets() + if instance_widget in selected_widgets: + instance_widget.set_selected(False) + else: + instance_widget.set_selected(True) + for widget in selected_widgets: + widget.set_selected(False) + self.selection_changed.emit() + + +class LogIconFrame(QtWidgets.QFrame): + """Draw log item icon next to message. + + Todos: + Paint event could be slow, maybe we could cache the image into pixmaps + so each item does not have to redraw it again. + """ + + info_color = QtGui.QColor("#ffffff") + error_color = QtGui.QColor("#ff4a4a") + level_to_color = dict(( + (10, QtGui.QColor("#ff66e8")), + (20, QtGui.QColor("#66abff")), + (30, QtGui.QColor("#ffba66")), + (40, QtGui.QColor("#ff4d58")), + (50, QtGui.QColor("#ff4f75")), + )) + _error_pix = None + _validation_error_pix = None + + def __init__(self, parent, log_type, log_level, is_validation_error): + super(LogIconFrame, self).__init__(parent) + + self.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + self._is_record = log_type == "record" + self._is_error = log_type == "error" + self._is_validation_error = bool(is_validation_error) + self._log_color = self.level_to_color.get(log_level) + + @classmethod + def get_validation_error_icon(cls): + if cls._validation_error_pix is None: + cls._validation_error_pix = get_pixmap("warning") + return cls._validation_error_pix + + @classmethod + def get_error_icon(cls): + if cls._error_pix is None: + cls._error_pix = get_pixmap("error") + return cls._error_pix + + def minimumSizeHint(self): + fm = self.fontMetrics() + size = fm.height() + return QtCore.QSize(size, size) + + def paintEvent(self, event): + painter = QtGui.QPainter(self) + painter.setRenderHints( + QtGui.QPainter.Antialiasing + | QtGui.QPainter.SmoothPixmapTransform + ) + painter.setPen(QtCore.Qt.NoPen) + rect = self.rect() + new_size = min(rect.width(), rect.height()) + new_rect = QtCore.QRect(1, 1, new_size - 2, new_size - 2) + if self._is_error: + if self._is_validation_error: + error_icon = self.get_validation_error_icon() + else: + error_icon = self.get_error_icon() + scaled_error_icon = error_icon.scaled( + new_rect.size(), + QtCore.Qt.KeepAspectRatio, + QtCore.Qt.SmoothTransformation + ) + painter.drawPixmap(new_rect, scaled_error_icon) + + else: + if self._is_record: + color = self._log_color + else: + color = QtGui.QColor(255, 255, 255) + painter.setBrush(color) + painter.drawEllipse(new_rect) + painter.end() + + +class LogItemWidget(QtWidgets.QWidget): + log_level_to_flag = { + 10: LOG_DEBUG_VISIBLE, + 20: LOG_INFO_VISIBLE, + 30: LOG_WARNING_VISIBLE, + 40: LOG_ERROR_VISIBLE, + 50: LOG_CRITICAL_VISIBLE, + } + + def __init__(self, log, parent): + super(LogItemWidget, self).__init__(parent) + + type_flag, level_n = self._get_log_info(log) + icon_label = LogIconFrame( + self, log["type"], level_n, log.get("is_validation_error")) + message_label = QtWidgets.QLabel(log["msg"].rstrip(), self) + message_label.setObjectName("PublishLogMessage") + message_label.setTextInteractionFlags( + QtCore.Qt.TextBrowserInteraction) + message_label.setCursor(QtGui.QCursor(QtCore.Qt.IBeamCursor)) + message_label.setWordWrap(True) + + main_layout = QtWidgets.QHBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.setSpacing(8) + main_layout.addWidget(icon_label, 0) + main_layout.addWidget(message_label, 1) + + self._type_flag = type_flag + self._plugin_id = log["plugin_id"] + self._log_type_filtered = False + self._plugin_filtered = False + + @property + def type_flag(self): + return self._type_flag + + @property + def plugin_id(self): + return self._plugin_id + + def _get_log_info(self, log): + log_type = log["type"] + if log_type == "error": + return ERROR_VISIBLE, None + + if log_type != "record": + return INFO_VISIBLE, None + + level_n = log["levelno"] + if level_n < 10: + level_n = 10 + elif level_n % 10 != 0: + level_n -= (level_n % 10) + 10 + + flag = self.log_level_to_flag.get(level_n, LOG_CRITICAL_VISIBLE) + return flag, level_n + + def _update_visibility(self): + self.setVisible( + not self._log_type_filtered + and not self._plugin_filtered + ) + + def set_log_type_filtered(self, filtered): + if filtered is self._log_type_filtered: + return + self._log_type_filtered = filtered + self._update_visibility() + + def set_plugin_filtered(self, filtered): + if filtered is self._plugin_filtered: + return + self._plugin_filtered = filtered + self._update_visibility() + + +class LogsWithIconsView(QtWidgets.QWidget): + """Show logs in a grid with 2 columns. + + First column is for icon second is for message. + + Todos: + Add filtering by type (exception, debug, info, etc.). + """ + + def __init__(self, logs, parent): + super(LogsWithIconsView, self).__init__(parent) + self.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + logs_layout = QtWidgets.QVBoxLayout(self) + logs_layout.setContentsMargins(0, 0, 0, 0) + logs_layout.setSpacing(4) + + widgets_by_flag = collections.defaultdict(list) + widgets_by_plugins_id = collections.defaultdict(list) + + for log in logs: + widget = LogItemWidget(log, self) + widgets_by_flag[widget.type_flag].append(widget) + widgets_by_plugins_id[widget.plugin_id].append(widget) + logs_layout.addWidget(widget, 0) + + self._widgets_by_flag = widgets_by_flag + self._widgets_by_plugins_id = widgets_by_plugins_id + + self._visibility_by_flags = { + LOG_DEBUG_VISIBLE: True, + LOG_INFO_VISIBLE: True, + LOG_WARNING_VISIBLE: True, + LOG_ERROR_VISIBLE: True, + LOG_CRITICAL_VISIBLE: True, + ERROR_VISIBLE: True, + INFO_VISIBLE: True, + } + self._flags_filter = sum(self._visibility_by_flags.keys()) + self._plugin_ids_filter = None + + def _update_flags_filtering(self): + for flag in ( + LOG_DEBUG_VISIBLE, + LOG_INFO_VISIBLE, + LOG_WARNING_VISIBLE, + LOG_ERROR_VISIBLE, + LOG_CRITICAL_VISIBLE, + ERROR_VISIBLE, + INFO_VISIBLE, + ): + visible = (self._flags_filter & flag) != 0 + if visible is not self._visibility_by_flags[flag]: + self._visibility_by_flags[flag] = visible + for widget in self._widgets_by_flag[flag]: + widget.set_log_type_filtered(not visible) + + def _update_plugin_filtering(self): + if self._plugin_ids_filter is None: + for widgets in self._widgets_by_plugins_id.values(): + for widget in widgets: + widget.set_plugin_filtered(False) + + else: + for plugin_id, widgets in self._widgets_by_plugins_id.items(): + filtered = plugin_id not in self._plugin_ids_filter + for widget in widgets: + widget.set_plugin_filtered(filtered) + + def set_log_filters(self, visibility_filter, plugin_ids): + if self._flags_filter != visibility_filter: + self._flags_filter = visibility_filter + self._update_flags_filtering() + + if self._plugin_ids_filter != plugin_ids: + if plugin_ids is not None: + plugin_ids = set(plugin_ids) + self._plugin_ids_filter = plugin_ids + self._update_plugin_filtering() + + +class InstanceLogsWidget(QtWidgets.QWidget): + """Widget showing logs of one publish instance. + + Args: + instance (_InstanceItem): Item of instance used as data source. + parent (QtWidgets.QWidget): Parent widget. + """ + + def __init__(self, instance, parent): + super(InstanceLogsWidget, self).__init__(parent) + + self.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + label_widget = QtWidgets.QLabel(instance.label, self) + label_widget.setObjectName("PublishInstanceLogsLabel") + logs_grid = LogsWithIconsView(instance.logs, self) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(label_widget, 0) + layout.addWidget(logs_grid, 0) + + self._logs_grid = logs_grid + + def set_log_filters(self, visibility_filter, plugin_ids): + """Change logs filter. + + Args: + visibility_filter (int): Number contained of flags for each log + type and level. + plugin_ids (Iterable[str]): Plugin ids to which are logs filtered. + """ + + self._logs_grid.set_log_filters(visibility_filter, plugin_ids) + + +class InstancesLogsView(QtWidgets.QFrame): + """Publish instances logs view widget.""" + + def __init__(self, parent): + super(InstancesLogsView, self).__init__(parent) + self.setObjectName("InstancesLogsView") + + scroll_area = QtWidgets.QScrollArea(self) + scroll_area.setWidgetResizable(True) + scroll_area.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + scroll_area.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) + scroll_area.setAttribute(QtCore.Qt.WA_TranslucentBackground) + scrollbar_bg = scroll_area.verticalScrollBar().parent() + if scrollbar_bg: + scrollbar_bg.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + content_wrap_widget = QtWidgets.QWidget(scroll_area) + content_wrap_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + content_widget = QtWidgets.QWidget(content_wrap_widget) + content_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) + content_layout = QtWidgets.QVBoxLayout(content_widget) + content_layout.setSpacing(15) + + scroll_area.setWidget(content_wrap_widget) + + content_wrap_layout = QtWidgets.QVBoxLayout(content_wrap_widget) + content_wrap_layout.setContentsMargins(0, 0, 0, 0) + content_wrap_layout.addWidget(content_widget, 0) + content_wrap_layout.addStretch(1) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(scroll_area, 1) + + self._visible_filters = ( + LOG_INFO_VISIBLE + | LOG_WARNING_VISIBLE + | LOG_ERROR_VISIBLE + | LOG_CRITICAL_VISIBLE + | ERROR_VISIBLE + | INFO_VISIBLE + ) + + self._content_widget = content_widget + self._content_layout = content_layout + + self._instances_order = [] + self._instances_by_id = {} + self._views_by_instance_id = {} + self._is_showed = False + self._clear_needed = False + self._update_needed = False + self._instance_ids_filter = [] + self._plugin_ids_filter = None + + def showEvent(self, event): + super(InstancesLogsView, self).showEvent(event) + self._is_showed = True + self._update_instances() + + def hideEvent(self, event): + super(InstancesLogsView, self).hideEvent(event) + self._is_showed = False + + def closeEvent(self, event): + super(InstancesLogsView, self).closeEvent(event) + self._is_showed = False + + def _update_instances(self): + if not self._is_showed: + return + + if self._clear_needed: + self._clear_widgets() + self._clear_needed = False + + if not self._update_needed: + return + self._update_needed = False + + instance_ids = self._instance_ids_filter + to_hide = set() + if not instance_ids: + instance_ids = self._instances_by_id + else: + to_hide = set(self._instances_by_id) - set(instance_ids) + + for instance_id in instance_ids: + widget = self._views_by_instance_id.get(instance_id) + if widget is None: + instance = self._instances_by_id[instance_id] + widget = InstanceLogsWidget(instance, self._content_widget) + self._views_by_instance_id[instance_id] = widget + self._content_layout.addWidget(widget, 0) + + widget.setVisible(True) + widget.set_log_filters( + self._visible_filters, self._plugin_ids_filter + ) + + for instance_id in to_hide: + widget = self._views_by_instance_id.get(instance_id) + if widget is not None: + widget.setVisible(False) + + def _clear_widgets(self): + """Remove all widgets from layout and from cache.""" + + while self._content_layout.count(): + item = self._content_layout.takeAt(0) + widget = item.widget() + if widget: + widget.setVisible(False) + widget.deleteLater() + self._views_by_instance_id = {} + + def update_instances(self, instances): + """Update publish instance from report. + + Args: + instances (list[_InstanceItem]): Instance data from report. + """ + + self._instances_order = [ + instance.id for instance in instances + ] + self._instances_by_id = { + instance.id: instance + for instance in instances + } + self._instance_ids_filter = [] + self._plugin_ids_filter = None + self._clear_needed = True + self._update_needed = True + self._update_instances() + + def set_instances_filter(self, instance_ids=None): + """Set instance filter. + + Args: + instance_ids (Optional[list[str]]): List of instances to keep + visible. Pass empty list to hide all items. + """ + + self._instance_ids_filter = instance_ids + self._update_needed = True + self._update_instances() + + def set_plugins_filter(self, plugin_ids=None): + if self._plugin_ids_filter == plugin_ids: + return + self._plugin_ids_filter = plugin_ids + self._update_needed = True + self._update_instances() + + +class CrashWidget(QtWidgets.QWidget): + """Widget shown when publishing crashes. + + Contains only minimal information for artist with easy access to report + actions. + """ + + def __init__(self, controller, parent): + super(CrashWidget, self).__init__(parent) + + main_label = QtWidgets.QLabel("This is not your fault", self) + main_label.setAlignment(QtCore.Qt.AlignCenter) + main_label.setObjectName("PublishCrashMainLabel") + + report_label = QtWidgets.QLabel( + ( + "Please report the error to your pipeline support" + " using one of the options below." + ), + self + ) + report_label.setAlignment(QtCore.Qt.AlignCenter) + report_label.setWordWrap(True) + report_label.setObjectName("PublishCrashReportLabel") + + btns_widget = QtWidgets.QWidget(self) + copy_clipboard_btn = QtWidgets.QPushButton( + "Copy to clipboard", btns_widget) + save_to_disk_btn = QtWidgets.QPushButton( + "Save to disk", btns_widget) + + btns_layout = QtWidgets.QHBoxLayout(btns_widget) + btns_layout.addStretch(1) + btns_layout.addWidget(copy_clipboard_btn, 0) + btns_layout.addSpacing(20) + btns_layout.addWidget(save_to_disk_btn, 0) + btns_layout.addStretch(1) + + layout = QtWidgets.QVBoxLayout(self) + layout.addStretch(1) + layout.addWidget(main_label, 0) + layout.addSpacing(20) + layout.addWidget(report_label, 0) + layout.addSpacing(20) + layout.addWidget(btns_widget, 0) + layout.addStretch(2) + + copy_clipboard_btn.clicked.connect(self._on_copy_to_clipboard) + save_to_disk_btn.clicked.connect(self._on_save_to_disk_click) + + self._controller = controller + + def _on_copy_to_clipboard(self): + self._controller.event_system.emit( + "copy_report.request", {}, "report_page") + + def _on_save_to_disk_click(self): + self._controller.event_system.emit( + "export_report.request", {}, "report_page") + + +class ErrorDetailsWidget(QtWidgets.QWidget): + def __init__(self, parent): + super(ErrorDetailsWidget, self).__init__(parent) + + inputs_widget = QtWidgets.QWidget(self) + # Error 'Description' input + error_description_input = ExpandingTextEdit(inputs_widget) + error_description_input.setObjectName("InfoText") + error_description_input.setTextInteractionFlags( + QtCore.Qt.TextBrowserInteraction + ) + + # Error 'Details' widget -> Collapsible + error_details_widget = QtWidgets.QWidget(inputs_widget) + + error_details_top = ClickableFrame(error_details_widget) + + error_details_expand_btn = ClassicExpandBtn(error_details_top) + error_details_expand_label = QtWidgets.QLabel( + "Details", error_details_top) + + line_widget = SeparatorWidget(1, parent=error_details_top) + + error_details_top_l = QtWidgets.QHBoxLayout(error_details_top) + error_details_top_l.setContentsMargins(0, 0, 10, 0) + error_details_top_l.addWidget(error_details_expand_btn, 0) + error_details_top_l.addWidget(error_details_expand_label, 0) + error_details_top_l.addWidget(line_widget, 1) + + error_details_input = ExpandingTextEdit(error_details_widget) + error_details_input.setObjectName("InfoText") + error_details_input.setTextInteractionFlags( + QtCore.Qt.TextBrowserInteraction + ) + error_details_input.setVisible(not error_details_expand_btn.collapsed) + + error_details_layout = QtWidgets.QVBoxLayout(error_details_widget) + error_details_layout.setContentsMargins(0, 0, 0, 0) + error_details_layout.addWidget(error_details_top, 0) + error_details_layout.addWidget(error_details_input, 0) + error_details_layout.addStretch(1) + + # Description and Details layout + inputs_layout = QtWidgets.QVBoxLayout(inputs_widget) + inputs_layout.setContentsMargins(0, 0, 0, 0) + inputs_layout.setSpacing(10) + inputs_layout.addWidget(error_description_input, 0) + inputs_layout.addWidget(error_details_widget, 1) + + main_layout = QtWidgets.QHBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.addWidget(inputs_widget, 1) + + error_details_top.clicked.connect(self._on_detail_toggle) + + self._error_details_widget = error_details_widget + self._error_description_input = error_description_input + self._error_details_expand_btn = error_details_expand_btn + self._error_details_input = error_details_input + + def _on_detail_toggle(self): + self._error_details_expand_btn.set_collapsed() + self._error_details_input.setVisible( + not self._error_details_expand_btn.collapsed) + + def set_error_item(self, error_item): + detail = "" + description = "" + if error_item: + description = error_item.description or description + detail = error_item.detail or detail + + if commonmark: + self._error_description_input.setHtml( + commonmark.commonmark(description) + ) + self._error_details_input.setHtml( + commonmark.commonmark(detail) + ) + + elif hasattr(self._error_details_input, "setMarkdown"): + self._error_description_input.setMarkdown(description) + self._error_details_input.setMarkdown(detail) + + else: + self._error_description_input.setText(description) + self._error_details_input.setText(detail) + + self._error_details_widget.setVisible(bool(detail)) + + +class ReportsWidget(QtWidgets.QWidget): + """ + # Crash layout + ┌──────┬─────────┬─────────┐ + │Views │ Logs │ Details │ + │ │ │ │ + │ │ │ │ + └──────┴─────────┴─────────┘ + # Success layout + ┌──────┬───────────────────┐ + │View │ Logs │ + │ │ │ + │ │ │ + └──────┴───────────────────┘ + # Validation errors layout + ┌──────┬─────────┬─────────┐ + │Views │ Actions │ │ + │ ├─────────┤ Details │ + │ │ Logs │ │ + │ │ │ │ + └──────┴─────────┴─────────┘ + """ + + def __init__(self, controller, parent): + super(ReportsWidget, self).__init__(parent) + + # Instances view + views_widget = QtWidgets.QWidget(self) + + instances_view = PublishInstancesViewWidget(controller, views_widget) + + validation_error_view = ValidationErrorsView(views_widget) + + views_layout = QtWidgets.QStackedLayout(views_widget) + views_layout.setContentsMargins(0, 0, 0, 0) + views_layout.addWidget(instances_view) + views_layout.addWidget(validation_error_view) + + views_layout.setCurrentWidget(instances_view) + + # Error description with actions and optional detail + details_widget = QtWidgets.QFrame(self) + details_widget.setObjectName("PublishInstancesDetails") + + # Actions widget + actions_widget = ValidateActionsWidget(controller, details_widget) + + pages_widget = QtWidgets.QWidget(details_widget) + + # Logs view + logs_view = InstancesLogsView(pages_widget) + + # Validation details + # Description and details inputs are in scroll + # - single scroll for both inputs, they are forced to not use theirs + detail_inputs_spacer = QtWidgets.QWidget(pages_widget) + detail_inputs_spacer.setMinimumWidth(30) + detail_inputs_spacer.setMaximumWidth(30) + + detail_input_scroll = QtWidgets.QScrollArea(pages_widget) + + detail_inputs_widget = ErrorDetailsWidget(detail_input_scroll) + detail_inputs_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + detail_input_scroll.setWidget(detail_inputs_widget) + detail_input_scroll.setWidgetResizable(True) + detail_input_scroll.setViewportMargins(0, 0, 0, 0) + + # Crash information + crash_widget = CrashWidget(controller, details_widget) + + # Layout pages + pages_layout = QtWidgets.QHBoxLayout(pages_widget) + pages_layout.setContentsMargins(0, 0, 0, 0) + pages_layout.addWidget(logs_view, 1) + pages_layout.addWidget(detail_inputs_spacer, 0) + pages_layout.addWidget(detail_input_scroll, 1) + pages_layout.addWidget(crash_widget, 1) + + details_layout = QtWidgets.QVBoxLayout(details_widget) + margins = details_layout.contentsMargins() + margins.setTop(margins.top() * 2) + margins.setBottom(margins.bottom() * 2) + details_layout.setContentsMargins(margins) + details_layout.setSpacing(margins.top()) + details_layout.addWidget(actions_widget, 0) + details_layout.addWidget(pages_widget, 1) + + content_layout = QtWidgets.QHBoxLayout(self) + content_layout.setContentsMargins(0, 0, 0, 0) + content_layout.addWidget(views_widget, 0) + content_layout.addWidget(details_widget, 1) + + instances_view.selection_changed.connect(self._on_instance_selection) + validation_error_view.selection_changed.connect( + self._on_error_selection) + + self._views_layout = views_layout + self._instances_view = instances_view + self._validation_error_view = validation_error_view + + self._actions_widget = actions_widget + self._detail_inputs_widget = detail_inputs_widget + self._logs_view = logs_view + self._detail_inputs_spacer = detail_inputs_spacer + self._detail_input_scroll = detail_input_scroll + self._crash_widget = crash_widget + + self._controller = controller + + self._validation_errors_by_id = {} + + def _get_instance_items(self): + report = self._controller.get_publish_report() + context_label = report["context"]["label"] or CONTEXT_LABEL + instances_by_id = report["instances"] + plugins_info = report["plugins_data"] + logs_by_instance_id = collections.defaultdict(list) + for plugin_info in plugins_info: + plugin_id = plugin_info["id"] + for instance_info in plugin_info["instances_data"]: + instance_id = instance_info["id"] or CONTEXT_ID + for log in instance_info["logs"]: + log["plugin_id"] = plugin_id + logs_by_instance_id[instance_id].extend(instance_info["logs"]) + + context_item = _InstanceItem.create_context_item( + context_label, logs_by_instance_id[CONTEXT_ID]) + instance_items = [ + _InstanceItem.from_report( + instance_id, instance, logs_by_instance_id[instance_id] + ) + for instance_id, instance in instances_by_id.items() + if instance["exists"] + ] + instance_items.sort() + instance_items.insert(0, context_item) + return instance_items + + def update_data(self): + view = self._instances_view + validation_error_mode = False + if ( + not self._controller.publish_has_crashed + and self._controller.publish_has_validation_errors + ): + view = self._validation_error_view + validation_error_mode = True + + self._actions_widget.set_visible_mode(validation_error_mode) + self._detail_inputs_spacer.setVisible(validation_error_mode) + self._detail_input_scroll.setVisible(validation_error_mode) + self._views_layout.setCurrentWidget(view) + + self._crash_widget.setVisible(self._controller.publish_has_crashed) + self._logs_view.setVisible(not self._controller.publish_has_crashed) + + # Instance view & logs update + instance_items = self._get_instance_items() + self._instances_view.update_instances(instance_items) + self._logs_view.update_instances(instance_items) + + # Validation errors + validation_errors = self._controller.get_validation_errors() + grouped_error_items = validation_errors.group_items_by_title() + + validation_errors_by_id = { + title_item["id"]: title_item + for title_item in grouped_error_items + } + + self._validation_errors_by_id = validation_errors_by_id + self._validation_error_view.set_errors(grouped_error_items) + + def _on_instance_selection(self): + instance_ids = self._instances_view.get_selected_instance_ids() + self._logs_view.set_instances_filter(instance_ids) + + def _on_error_selection(self): + title_id, instance_ids = ( + self._validation_error_view.get_selected_items()) + error_info = self._validation_errors_by_id.get(title_id) + if error_info is None: + self._actions_widget.set_error_info(None) + self._detail_inputs_widget.set_error_item(None) + return + + self._logs_view.set_instances_filter(instance_ids) + self._logs_view.set_plugins_filter([error_info["plugin_id"]]) + + match_error_item = None + for error_item in error_info["error_items"]: + instance_id = error_item.instance_id or CONTEXT_ID + if instance_id in instance_ids: + match_error_item = error_item + break + + self._actions_widget.set_error_info(error_info) + self._detail_inputs_widget.set_error_item(match_error_item) + + +class ReportPageWidget(QtWidgets.QFrame): + """Widgets showing report for artis. + + There are 5 possible states: + 1. Publishing did not start yet. > Only label. + 2. Publishing is paused. ┐ + 3. Publishing successfully finished. │> Instances with logs. + 4. Publishing crashed. ┘ + 5. Crashed because of validation error. > Errors with logs. + + This widget is shown if validation errors happened during validation part. + + Shows validation error titles with instances on which they happened + and validation error detail with possible actions (repair). + """ + + def __init__(self, controller, parent): + super(ReportPageWidget, self).__init__(parent) + + header_label = QtWidgets.QLabel(self) + header_label.setAlignment(QtCore.Qt.AlignCenter) + header_label.setObjectName("PublishReportHeader") + + publish_instances_widget = ReportsWidget(controller, self) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(header_label, 0) + layout.addWidget(publish_instances_widget, 0) + + controller.event_system.add_callback( + "publish.process.started", self._on_publish_start + ) + controller.event_system.add_callback( + "publish.reset.finished", self._on_publish_reset + ) + controller.event_system.add_callback( + "publish.process.stopped", self._on_publish_stop + ) + + self._header_label = header_label + self._publish_instances_widget = publish_instances_widget + + self._controller = controller + + def _update_label(self): + if not self._controller.publish_has_started: + # This probably never happen when this widget is visible + header_label = "Nothing to report until you run publish" + elif self._controller.publish_has_crashed: + header_label = "Publish error report" + elif self._controller.publish_has_validation_errors: + header_label = "Publish validation report" + elif self._controller.publish_has_finished: + header_label = "Publish success report" + else: + header_label = "Publish report" + self._header_label.setText(header_label) + + def _update_state(self): + self._update_label() + publish_started = self._controller.publish_has_started + self._publish_instances_widget.setVisible(publish_started) + if publish_started: + self._publish_instances_widget.update_data() + + self.updateGeometry() + + def _on_publish_start(self): + self._update_state() + + def _on_publish_reset(self): + self._update_state() + + def _on_publish_stop(self): + self._update_state() diff --git a/openpype/tools/publisher/widgets/thumbnail_widget.py b/openpype/tools/publisher/widgets/thumbnail_widget.py index e234f4cdc1..b17ca0adc8 100644 --- a/openpype/tools/publisher/widgets/thumbnail_widget.py +++ b/openpype/tools/publisher/widgets/thumbnail_widget.py @@ -75,6 +75,7 @@ class ThumbnailPainterWidget(QtWidgets.QWidget): painter = QtGui.QPainter() painter.begin(self) + painter.setRenderHint(QtGui.QPainter.Antialiasing) painter.drawPixmap(0, 0, self._cached_pix) painter.end() @@ -183,6 +184,18 @@ class ThumbnailPainterWidget(QtWidgets.QWidget): backgrounded_images.append(new_pix) return backgrounded_images + def _paint_dash_line(self, painter, rect): + pen = QtGui.QPen() + pen.setWidth(1) + pen.setBrush(QtCore.Qt.darkGray) + pen.setStyle(QtCore.Qt.DashLine) + + new_rect = rect.adjusted(1, 1, -1, -1) + painter.setPen(pen) + painter.setBrush(QtCore.Qt.transparent) + # painter.drawRect(rect) + painter.drawRect(new_rect) + def _cache_pix(self): rect = self.rect() rect_width = rect.width() @@ -264,13 +277,7 @@ class ThumbnailPainterWidget(QtWidgets.QWidget): # Draw drop enabled dashes if used_default_pix: - pen = QtGui.QPen() - pen.setWidth(1) - pen.setBrush(QtCore.Qt.darkGray) - pen.setStyle(QtCore.Qt.DashLine) - final_painter.setPen(pen) - final_painter.setBrush(QtCore.Qt.transparent) - final_painter.drawRect(rect) + self._paint_dash_line(final_painter, rect) final_painter.end() diff --git a/openpype/tools/publisher/widgets/validations_widget.py b/openpype/tools/publisher/widgets/validations_widget.py deleted file mode 100644 index 0abe85c0b8..0000000000 --- a/openpype/tools/publisher/widgets/validations_widget.py +++ /dev/null @@ -1,715 +0,0 @@ -# -*- coding: utf-8 -*- -try: - import commonmark -except Exception: - commonmark = None - -from qtpy import QtWidgets, QtCore, QtGui - -from openpype.tools.utils import BaseClickableFrame, ClickableFrame -from .widgets import ( - IconValuePixmapLabel -) -from ..constants import ( - INSTANCE_ID_ROLE -) - - -class ValidationErrorInstanceList(QtWidgets.QListView): - """List of publish instances that caused a validation error. - - Instances are collected per plugin's validation error title. - """ - def __init__(self, *args, **kwargs): - super(ValidationErrorInstanceList, self).__init__(*args, **kwargs) - - self.setObjectName("ValidationErrorInstanceList") - - self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) - self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) - - def minimumSizeHint(self): - return self.sizeHint() - - def sizeHint(self): - result = super(ValidationErrorInstanceList, self).sizeHint() - row_count = self.model().rowCount() - height = 0 - if row_count > 0: - height = self.sizeHintForRow(0) * row_count - result.setHeight(height) - return result - - -class ValidationErrorTitleWidget(QtWidgets.QWidget): - """Title of validation error. - - Widget is used as radio button so requires clickable functionality and - changing style on selection/deselection. - - Has toggle button to show/hide instances on which validation error happened - if there is a list (Valdation error may happen on context). - """ - - selected = QtCore.Signal(int) - instance_changed = QtCore.Signal(int) - - def __init__(self, index, error_info, parent): - super(ValidationErrorTitleWidget, self).__init__(parent) - - self._index = index - self._error_info = error_info - self._selected = False - - title_frame = ClickableFrame(self) - title_frame.setObjectName("ValidationErrorTitleFrame") - - toggle_instance_btn = QtWidgets.QToolButton(title_frame) - toggle_instance_btn.setObjectName("ArrowBtn") - toggle_instance_btn.setArrowType(QtCore.Qt.RightArrow) - toggle_instance_btn.setMaximumWidth(14) - - label_widget = QtWidgets.QLabel(error_info["title"], title_frame) - - title_frame_layout = QtWidgets.QHBoxLayout(title_frame) - title_frame_layout.addWidget(label_widget, 1) - title_frame_layout.addWidget(toggle_instance_btn, 0) - - instances_model = QtGui.QStandardItemModel() - - help_text_by_instance_id = {} - - items = [] - context_validation = False - for error_item in error_info["error_items"]: - context_validation = error_item.context_validation - if context_validation: - toggle_instance_btn.setArrowType(QtCore.Qt.NoArrow) - description = self._prepare_description(error_item) - help_text_by_instance_id[None] = description - # Add fake item to have minimum size hint of view widget - items.append(QtGui.QStandardItem("Context")) - continue - - label = error_item.instance_label - item = QtGui.QStandardItem(label) - item.setFlags( - QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable - ) - item.setData(label, QtCore.Qt.ToolTipRole) - item.setData(error_item.instance_id, INSTANCE_ID_ROLE) - items.append(item) - description = self._prepare_description(error_item) - help_text_by_instance_id[error_item.instance_id] = description - - if items: - root_item = instances_model.invisibleRootItem() - root_item.appendRows(items) - - instances_view = ValidationErrorInstanceList(self) - instances_view.setModel(instances_model) - - self.setLayoutDirection(QtCore.Qt.LeftToRight) - - view_widget = QtWidgets.QWidget(self) - view_layout = QtWidgets.QHBoxLayout(view_widget) - view_layout.setContentsMargins(0, 0, 0, 0) - view_layout.setSpacing(0) - view_layout.addSpacing(14) - view_layout.addWidget(instances_view, 0) - - layout = QtWidgets.QVBoxLayout(self) - layout.setSpacing(0) - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(title_frame, 0) - layout.addWidget(view_widget, 0) - view_widget.setVisible(False) - - if not context_validation: - toggle_instance_btn.clicked.connect(self._on_toggle_btn_click) - - title_frame.clicked.connect(self._mouse_release_callback) - instances_view.selectionModel().selectionChanged.connect( - self._on_seleciton_change - ) - - self._title_frame = title_frame - - self._toggle_instance_btn = toggle_instance_btn - - self._view_widget = view_widget - - self._instances_model = instances_model - self._instances_view = instances_view - - self._context_validation = context_validation - self._help_text_by_instance_id = help_text_by_instance_id - - self._expanded = False - - def sizeHint(self): - result = super(ValidationErrorTitleWidget, self).sizeHint() - expected_width = max( - self._view_widget.minimumSizeHint().width(), - self._view_widget.sizeHint().width() - ) - - if expected_width < 200: - expected_width = 200 - - if result.width() < expected_width: - result.setWidth(expected_width) - - return result - - def minimumSizeHint(self): - return self.sizeHint() - - def _prepare_description(self, error_item): - """Prepare description text for detail intput. - - Args: - error_item (ValidationErrorItem): Item which hold information about - validation error. - - Returns: - str: Prepared detailed description. - """ - - dsc = error_item.description - detail = error_item.detail - if detail: - dsc += "

{}".format(detail) - - description = dsc - if commonmark: - description = commonmark.commonmark(dsc) - return description - - def _mouse_release_callback(self): - """Mark this widget as selected on click.""" - - self.set_selected(True) - - def current_description_text(self): - if self._context_validation: - return self._help_text_by_instance_id[None] - index = self._instances_view.currentIndex() - # TODO make sure instance is selected - if not index.isValid(): - index = self._instances_model.index(0, 0) - - indence_id = index.data(INSTANCE_ID_ROLE) - return self._help_text_by_instance_id[indence_id] - - @property - def is_selected(self): - """Is widget marked a selected. - - Returns: - bool: Item is selected or not. - """ - - return self._selected - - @property - def index(self): - """Widget's index set by parent. - - Returns: - int: Index of widget. - """ - - return self._index - - def set_index(self, index): - """Set index of widget (called by parent). - - Args: - int: New index of widget. - """ - - self._index = index - - def _change_style_property(self, selected): - """Change style of widget based on selection.""" - - value = "1" if selected else "" - self._title_frame.setProperty("selected", value) - self._title_frame.style().polish(self._title_frame) - - def set_selected(self, selected=None): - """Change selected state of widget.""" - - if selected is None: - selected = not self._selected - - # Clear instance view selection on deselect - if not selected: - self._instances_view.clearSelection() - - # Skip if has same value - if selected == self._selected: - return - - self._selected = selected - self._change_style_property(selected) - if selected: - self.selected.emit(self._index) - self._set_expanded(True) - - def _on_toggle_btn_click(self): - """Show/hide instances list.""" - - self._set_expanded() - - def _set_expanded(self, expanded=None): - if expanded is None: - expanded = not self._expanded - - elif expanded is self._expanded: - return - - if expanded and self._context_validation: - return - - self._expanded = expanded - self._view_widget.setVisible(expanded) - if expanded: - self._toggle_instance_btn.setArrowType(QtCore.Qt.DownArrow) - else: - self._toggle_instance_btn.setArrowType(QtCore.Qt.RightArrow) - - def _on_seleciton_change(self): - sel_model = self._instances_view.selectionModel() - if sel_model.selectedIndexes(): - self.instance_changed.emit(self._index) - - -class ActionButton(BaseClickableFrame): - """Plugin's action callback button. - - Action may have label or icon or both. - - Args: - plugin_action_item (PublishPluginActionItem): Action item that can be - triggered by it's id. - """ - - action_clicked = QtCore.Signal(str, str) - - def __init__(self, plugin_action_item, parent): - super(ActionButton, self).__init__(parent) - - self.setObjectName("ValidationActionButton") - - self.plugin_action_item = plugin_action_item - - action_label = plugin_action_item.label - action_icon = plugin_action_item.icon - label_widget = QtWidgets.QLabel(action_label, self) - icon_label = None - if action_icon: - icon_label = IconValuePixmapLabel(action_icon, self) - - layout = QtWidgets.QHBoxLayout(self) - layout.setContentsMargins(5, 0, 5, 0) - layout.addWidget(label_widget, 1) - if icon_label: - layout.addWidget(icon_label, 0) - - self.setSizePolicy( - QtWidgets.QSizePolicy.Minimum, - self.sizePolicy().verticalPolicy() - ) - - def _mouse_release_callback(self): - self.action_clicked.emit( - self.plugin_action_item.plugin_id, - self.plugin_action_item.action_id - ) - - -class ValidateActionsWidget(QtWidgets.QFrame): - """Wrapper widget for plugin actions. - - Change actions based on selected validation error. - """ - - def __init__(self, controller, parent): - super(ValidateActionsWidget, self).__init__(parent) - - self.setAttribute(QtCore.Qt.WA_TranslucentBackground) - - content_widget = QtWidgets.QWidget(self) - content_layout = QtWidgets.QVBoxLayout(content_widget) - - layout = QtWidgets.QHBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(content_widget) - - self._controller = controller - self._content_widget = content_widget - self._content_layout = content_layout - self._actions_mapping = {} - - def clear(self): - """Remove actions from widget.""" - while self._content_layout.count(): - item = self._content_layout.takeAt(0) - widget = item.widget() - if widget: - widget.setVisible(False) - widget.deleteLater() - self._actions_mapping = {} - - def set_error_item(self, error_item): - """Set selected plugin and show it's actions. - - Clears current actions from widget and recreate them from the plugin. - - Args: - Dict[str, Any]: Object holding error items, title and possible - actions to run. - """ - - self.clear() - - if not error_item: - self.setVisible(False) - return - - plugin_action_items = error_item["plugin_action_items"] - for plugin_action_item in plugin_action_items: - if not plugin_action_item.active: - continue - - if plugin_action_item.on_filter not in ("failed", "all"): - continue - - action_id = plugin_action_item.action_id - self._actions_mapping[action_id] = plugin_action_item - - action_btn = ActionButton(plugin_action_item, self._content_widget) - action_btn.action_clicked.connect(self._on_action_click) - self._content_layout.addWidget(action_btn) - - if self._content_layout.count() > 0: - self.setVisible(True) - self._content_layout.addStretch(1) - else: - self.setVisible(False) - - def _on_action_click(self, plugin_id, action_id): - self._controller.run_action(plugin_id, action_id) - - -class VerticallScrollArea(QtWidgets.QScrollArea): - """Scroll area for validation error titles. - - The biggest difference is that the scroll area has scroll bar on left side - and resize of content will also resize scrollarea itself. - - Resize if deferred by 100ms because at the moment of resize are not yet - propagated sizes and visibility of scroll bars. - """ - - def __init__(self, *args, **kwargs): - super(VerticallScrollArea, self).__init__(*args, **kwargs) - - self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) - self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) - self.setLayoutDirection(QtCore.Qt.RightToLeft) - - self.setAttribute(QtCore.Qt.WA_TranslucentBackground) - # Background of scrollbar will be transparent - scrollbar_bg = self.verticalScrollBar().parent() - if scrollbar_bg: - scrollbar_bg.setAttribute(QtCore.Qt.WA_TranslucentBackground) - self.setViewportMargins(0, 0, 0, 0) - - self.verticalScrollBar().installEventFilter(self) - - # Timer with 100ms offset after changing size - size_changed_timer = QtCore.QTimer() - size_changed_timer.setInterval(100) - size_changed_timer.setSingleShot(True) - - size_changed_timer.timeout.connect(self._on_timer_timeout) - self._size_changed_timer = size_changed_timer - - def setVerticalScrollBar(self, widget): - old_widget = self.verticalScrollBar() - if old_widget: - old_widget.removeEventFilter(self) - - super(VerticallScrollArea, self).setVerticalScrollBar(widget) - if widget: - widget.installEventFilter(self) - - def setWidget(self, widget): - old_widget = self.widget() - if old_widget: - old_widget.removeEventFilter(self) - - super(VerticallScrollArea, self).setWidget(widget) - if widget: - widget.installEventFilter(self) - - def _on_timer_timeout(self): - width = self.widget().width() - if self.verticalScrollBar().isVisible(): - width += self.verticalScrollBar().width() - self.setMinimumWidth(width) - - def eventFilter(self, obj, event): - if ( - event.type() == QtCore.QEvent.Resize - and (obj is self.widget() or obj is self.verticalScrollBar()) - ): - self._size_changed_timer.start() - return super(VerticallScrollArea, self).eventFilter(obj, event) - - -class ValidationArtistMessage(QtWidgets.QWidget): - def __init__(self, message, parent): - super(ValidationArtistMessage, self).__init__(parent) - - artist_msg_label = QtWidgets.QLabel(message, self) - artist_msg_label.setAlignment(QtCore.Qt.AlignCenter) - - main_layout = QtWidgets.QHBoxLayout(self) - main_layout.setContentsMargins(0, 0, 0, 0) - main_layout.addWidget( - artist_msg_label, 1, QtCore.Qt.AlignCenter - ) - - -class ValidationsWidget(QtWidgets.QFrame): - """Widgets showing validation error. - - This widget is shown if validation error/s happened during validation part. - - Shows validation error titles with instances on which happened and - validation error detail with possible actions (repair). - - ┌──────┬────────────────┬───────┐ - │titles│ │actions│ - │ │ │ │ - │ │ Error detail │ │ - │ │ │ │ - │ │ │ │ - └──────┴────────────────┴───────┘ - """ - - def __init__(self, controller, parent): - super(ValidationsWidget, self).__init__(parent) - - # Before publishing - before_publish_widget = ValidationArtistMessage( - "Nothing to report until you run publish", self - ) - # After success publishing - publish_started_widget = ValidationArtistMessage( - "So far so good", self - ) - # After success publishing - publish_stop_ok_widget = ValidationArtistMessage( - "Publishing finished successfully", self - ) - # After failed publishing (not with validation error) - publish_stop_fail_widget = ValidationArtistMessage( - "This is not your fault...", self - ) - - # Validation errors - validations_widget = QtWidgets.QWidget(self) - - content_widget = QtWidgets.QWidget(validations_widget) - - errors_scroll = VerticallScrollArea(content_widget) - errors_scroll.setWidgetResizable(True) - - errors_widget = QtWidgets.QWidget(errors_scroll) - errors_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) - errors_layout = QtWidgets.QVBoxLayout(errors_widget) - errors_layout.setContentsMargins(0, 0, 0, 0) - - errors_scroll.setWidget(errors_widget) - - error_details_frame = QtWidgets.QFrame(content_widget) - error_details_input = QtWidgets.QTextEdit(error_details_frame) - error_details_input.setObjectName("InfoText") - error_details_input.setTextInteractionFlags( - QtCore.Qt.TextBrowserInteraction - ) - - actions_widget = ValidateActionsWidget(controller, content_widget) - actions_widget.setMinimumWidth(140) - - error_details_layout = QtWidgets.QHBoxLayout(error_details_frame) - error_details_layout.addWidget(error_details_input, 1) - error_details_layout.addWidget(actions_widget, 0) - - content_layout = QtWidgets.QHBoxLayout(content_widget) - content_layout.setSpacing(0) - content_layout.setContentsMargins(0, 0, 0, 0) - - content_layout.addWidget(errors_scroll, 0) - content_layout.addWidget(error_details_frame, 1) - - top_label = QtWidgets.QLabel( - "Publish validation report", content_widget - ) - top_label.setObjectName("PublishInfoMainLabel") - top_label.setAlignment(QtCore.Qt.AlignCenter) - - validation_layout = QtWidgets.QVBoxLayout(validations_widget) - validation_layout.setContentsMargins(0, 0, 0, 0) - validation_layout.addWidget(top_label, 0) - validation_layout.addWidget(content_widget, 1) - - main_layout = QtWidgets.QStackedLayout(self) - main_layout.addWidget(before_publish_widget) - main_layout.addWidget(publish_started_widget) - main_layout.addWidget(publish_stop_ok_widget) - main_layout.addWidget(publish_stop_fail_widget) - main_layout.addWidget(validations_widget) - - main_layout.setCurrentWidget(before_publish_widget) - - controller.event_system.add_callback( - "publish.process.started", self._on_publish_start - ) - controller.event_system.add_callback( - "publish.reset.finished", self._on_publish_reset - ) - controller.event_system.add_callback( - "publish.process.stopped", self._on_publish_stop - ) - - self._main_layout = main_layout - - self._before_publish_widget = before_publish_widget - self._publish_started_widget = publish_started_widget - self._publish_stop_ok_widget = publish_stop_ok_widget - self._publish_stop_fail_widget = publish_stop_fail_widget - self._validations_widget = validations_widget - - self._top_label = top_label - self._errors_widget = errors_widget - self._errors_layout = errors_layout - self._error_details_frame = error_details_frame - self._error_details_input = error_details_input - self._actions_widget = actions_widget - - self._title_widgets = {} - self._error_info = {} - self._previous_select = None - - self._controller = controller - - def clear(self): - """Delete all dynamic widgets and hide all wrappers.""" - self._title_widgets = {} - self._error_info = {} - self._previous_select = None - while self._errors_layout.count(): - item = self._errors_layout.takeAt(0) - widget = item.widget() - if widget: - widget.deleteLater() - - self._top_label.setVisible(False) - self._error_details_frame.setVisible(False) - self._errors_widget.setVisible(False) - self._actions_widget.setVisible(False) - - def _set_errors(self, validation_error_report): - """Set errors into context and created titles. - - Args: - validation_error_report (PublishValidationErrorsReport): Report - with information about validation errors and publish plugin - actions. - """ - - self.clear() - if not validation_error_report: - return - - self._top_label.setVisible(True) - self._error_details_frame.setVisible(True) - self._errors_widget.setVisible(True) - - grouped_error_items = validation_error_report.group_items_by_title() - for idx, error_info in enumerate(grouped_error_items): - widget = ValidationErrorTitleWidget(idx, error_info, self) - widget.selected.connect(self._on_select) - widget.instance_changed.connect(self._on_instance_change) - self._errors_layout.addWidget(widget) - self._title_widgets[idx] = widget - self._error_info[idx] = error_info - - self._errors_layout.addStretch(1) - - if self._title_widgets: - self._title_widgets[0].set_selected(True) - - self.updateGeometry() - - def _set_current_widget(self, widget): - self._main_layout.setCurrentWidget(widget) - - def _on_publish_start(self): - self._set_current_widget(self._publish_started_widget) - - def _on_publish_reset(self): - self._set_current_widget(self._before_publish_widget) - - def _on_publish_stop(self): - if self._controller.publish_has_crashed: - self._set_current_widget(self._publish_stop_fail_widget) - return - - if self._controller.publish_has_validation_errors: - validation_errors = self._controller.get_validation_errors() - self._set_current_widget(self._validations_widget) - self._set_errors(validation_errors) - return - - if self._controller.publish_has_finished: - self._set_current_widget(self._publish_stop_ok_widget) - return - - self._set_current_widget(self._publish_started_widget) - - def _on_select(self, index): - if self._previous_select: - if self._previous_select.index == index: - return - self._previous_select.set_selected(False) - - self._previous_select = self._title_widgets[index] - - error_item = self._error_info[index] - - self._actions_widget.set_error_item(error_item) - - self._update_description() - - def _on_instance_change(self, index): - if self._previous_select and self._previous_select.index != index: - self._title_widgets[index].set_selected(True) - else: - self._update_description() - - def _update_description(self): - description = self._previous_select.current_description_text() - if commonmark: - html = commonmark.commonmark(description) - self._error_details_input.setHtml(html) - elif hasattr(self._error_details_input, "setMarkdown"): - self._error_details_input.setMarkdown(description) - else: - self._error_details_input.setText(description) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index cd1f1f5a96..0b13f26d57 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -40,6 +40,41 @@ from ..constants import ( INPUTS_LAYOUT_VSPACING, ) +FA_PREFIXES = ["", "fa.", "fa5.", "fa5b.", "fa5s.", "ei.", "mdi."] + + +def parse_icon_def( + icon_def, default_width=None, default_height=None, color=None +): + if not icon_def: + return None + + if isinstance(icon_def, QtGui.QPixmap): + return icon_def + + color = color or "white" + default_width = default_width or 512 + default_height = default_height or 512 + + if isinstance(icon_def, QtGui.QIcon): + return icon_def.pixmap(default_width, default_height) + + try: + if os.path.exists(icon_def): + return QtGui.QPixmap(icon_def) + except Exception: + # TODO logging + pass + + for prefix in FA_PREFIXES: + try: + icon_name = "{}{}".format(prefix, icon_def) + icon = qtawesome.icon(icon_name, color=color) + return icon.pixmap(default_width, default_height) + except Exception: + # TODO logging + continue + class PublishPixmapLabel(PixmapLabel): def _get_pix_size(self): @@ -54,7 +89,6 @@ class IconValuePixmapLabel(PublishPixmapLabel): Handle icon parsing from creators/instances. Using of QAwesome module of path to images. """ - fa_prefixes = ["", "fa."] default_size = 200 def __init__(self, icon_def, parent): @@ -77,31 +111,9 @@ class IconValuePixmapLabel(PublishPixmapLabel): return pix def _parse_icon_def(self, icon_def): - if not icon_def: - return self._default_pixmap() - - if isinstance(icon_def, QtGui.QPixmap): - return icon_def - - if isinstance(icon_def, QtGui.QIcon): - return icon_def.pixmap(self.default_size, self.default_size) - - try: - if os.path.exists(icon_def): - return QtGui.QPixmap(icon_def) - except Exception: - # TODO logging - pass - - for prefix in self.fa_prefixes: - try: - icon_name = "{}{}".format(prefix, icon_def) - icon = qtawesome.icon(icon_name, color="white") - return icon.pixmap(self.default_size, self.default_size) - except Exception: - # TODO logging - continue - + icon = parse_icon_def(icon_def, self.default_size, self.default_size) + if icon: + return icon return self._default_pixmap() @@ -692,6 +704,7 @@ class TasksCombobox(QtWidgets.QComboBox): style.drawControl( QtWidgets.QStyle.CE_ComboBoxLabel, opt, painter, self ) + painter.end() def is_valid(self): """Are all selected items valid.""" diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index b3471163ae..fc90e66f21 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -1,3 +1,6 @@ +import os +import json +import time import collections import copy from qtpy import QtWidgets, QtCore, QtGui @@ -15,10 +18,11 @@ from openpype.tools.utils import ( from .constants import ResetKeySequence from .publish_report_viewer import PublishReportViewerWidget +from .control import CardMessageTypes from .control_qt import QtPublisherController from .widgets import ( OverviewWidget, - ValidationsWidget, + ReportPageWidget, PublishFrame, PublisherTabsWidget, @@ -182,7 +186,7 @@ class PublisherWindow(QtWidgets.QDialog): controller, content_stacked_widget ) - report_widget = ValidationsWidget(controller, parent) + report_widget = ReportPageWidget(controller, parent) # Details - Publish details publish_details_widget = PublishReportViewerWidget( @@ -313,6 +317,13 @@ class PublisherWindow(QtWidgets.QDialog): controller.event_system.add_callback( "convertors.find.failed", self._on_convertor_error ) + controller.event_system.add_callback( + "export_report.request", self._export_report + ) + controller.event_system.add_callback( + "copy_report.request", self._copy_report + ) + # Store extra header widget for TrayPublisher # - can be used to add additional widgets to header between context @@ -825,6 +836,9 @@ class PublisherWindow(QtWidgets.QDialog): self._validate_btn.setEnabled(validate_enabled) self._publish_btn.setEnabled(publish_enabled) + if not publish_enabled: + self._publish_frame.set_shrunk_state(True) + self._update_publish_details_widget() def _validate_create_instances(self): @@ -941,6 +955,46 @@ class PublisherWindow(QtWidgets.QDialog): under_mouse = widget_x < global_pos.x() self._create_overlay_button.set_under_mouse(under_mouse) + def _copy_report(self): + logs = self._controller.get_publish_report() + logs_string = json.dumps(logs, indent=4) + + mime_data = QtCore.QMimeData() + mime_data.setText(logs_string) + QtWidgets.QApplication.instance().clipboard().setMimeData( + mime_data + ) + self._controller.emit_card_message( + "Report added to clipboard", + CardMessageTypes.info) + + def _export_report(self): + default_filename = "publish-report-{}".format( + time.strftime("%y%m%d-%H-%M") + ) + default_filepath = os.path.join( + os.path.expanduser("~"), + default_filename + ) + new_filepath, ext = QtWidgets.QFileDialog.getSaveFileName( + self, "Save report", default_filepath, ".json" + ) + if not ext or not new_filepath: + return + + logs = self._controller.get_publish_report() + full_path = new_filepath + ext + dir_path = os.path.dirname(full_path) + if not os.path.exists(dir_path): + os.makedirs(dir_path) + + with open(full_path, "w") as file_stream: + json.dump(logs, file_stream) + + self._controller.emit_card_message( + "Report saved", + CardMessageTypes.info) + class ErrorsMessageBox(ErrorMessageBox): def __init__(self, error_title, failed_info, message_start, parent): diff --git a/openpype/tools/utils/__init__.py b/openpype/tools/utils/__init__.py index 4149763f80..10bd527692 100644 --- a/openpype/tools/utils/__init__.py +++ b/openpype/tools/utils/__init__.py @@ -1,13 +1,16 @@ +from .layouts import FlowLayout from .widgets import ( FocusSpinBox, FocusDoubleSpinBox, ComboBox, CustomTextComboBox, PlaceholderLineEdit, + ExpandingTextEdit, BaseClickableFrame, ClickableFrame, ClickableLabel, ExpandBtn, + ClassicExpandBtn, PixmapLabel, IconButton, PixmapButton, @@ -37,15 +40,19 @@ from .overlay_messages import ( __all__ = ( + "FlowLayout", + "FocusSpinBox", "FocusDoubleSpinBox", "ComboBox", "CustomTextComboBox", "PlaceholderLineEdit", + "ExpandingTextEdit", "BaseClickableFrame", "ClickableFrame", "ClickableLabel", "ExpandBtn", + "ClassicExpandBtn", "PixmapLabel", "IconButton", "PixmapButton", diff --git a/openpype/tools/utils/layouts.py b/openpype/tools/utils/layouts.py new file mode 100644 index 0000000000..65ea087c27 --- /dev/null +++ b/openpype/tools/utils/layouts.py @@ -0,0 +1,150 @@ +from qtpy import QtWidgets, QtCore + + +class FlowLayout(QtWidgets.QLayout): + """Layout that organize widgets by minimum size into a flow layout. + + Layout is putting widget from left to right and top to bottom. When widget + can't fit a row it is added to next line. Minimum size matches widget with + biggest 'sizeHint' width and height using calculated geometry. + + Content margins are part of calculations. It is possible to define + horizontal and vertical spacing. + + Layout does not support stretch and spacing items. + + Todos: + Unified width concept -> use width of largest item so all of them are + same. This could allow to have minimum columns option too. + """ + + def __init__(self, parent=None): + super(FlowLayout, self).__init__(parent) + + # spaces between each item + self._horizontal_spacing = 5 + self._vertical_spacing = 5 + + self._items = [] + + def __del__(self): + while self.count(): + self.takeAt(0, False) + + def isEmpty(self): + for item in self._items: + if not item.isEmpty(): + return False + return True + + def setSpacing(self, spacing): + self._horizontal_spacing = spacing + self._vertical_spacing = spacing + self.invalidate() + + def setHorizontalSpacing(self, spacing): + self._horizontal_spacing = spacing + self.invalidate() + + def setVerticalSpacing(self, spacing): + self._vertical_spacing = spacing + self.invalidate() + + def addItem(self, item): + self._items.append(item) + self.invalidate() + + def count(self): + return len(self._items) + + def itemAt(self, index): + if 0 <= index < len(self._items): + return self._items[index] + return None + + def takeAt(self, index, invalidate=True): + if 0 <= index < len(self._items): + item = self._items.pop(index) + if invalidate: + self.invalidate() + return item + return None + + def expandingDirections(self): + return QtCore.Qt.Orientations(QtCore.Qt.Vertical) + + def hasHeightForWidth(self): + return True + + def heightForWidth(self, width): + return self._setup_geometry(QtCore.QRect(0, 0, width, 0), True) + + def setGeometry(self, rect): + super(FlowLayout, self).setGeometry(rect) + self._setup_geometry(rect) + + def sizeHint(self): + return self.minimumSize() + + def minimumSize(self): + size = QtCore.QSize(0, 0) + for item in self._items: + widget = item.widget() + if widget is not None: + parent = widget.parent() + if not widget.isVisibleTo(parent): + continue + size = size.expandedTo(item.minimumSize()) + + if size.width() < 1 or size.height() < 1: + return size + l_margin, t_margin, r_margin, b_margin = self.getContentsMargins() + size += QtCore.QSize(l_margin + r_margin, t_margin + b_margin) + return size + + def _setup_geometry(self, rect, only_calculate=False): + h_spacing = self._horizontal_spacing + v_spacing = self._vertical_spacing + l_margin, t_margin, r_margin, b_margin = self.getContentsMargins() + + left_x = rect.x() + l_margin + top_y = rect.y() + t_margin + pos_x = left_x + pos_y = top_y + row_height = 0 + for item in self._items: + item_hint = item.sizeHint() + item_width = item_hint.width() + item_height = item_hint.height() + if item_width < 1 or item_height < 1: + continue + + end_x = pos_x + item_width + + wrap = ( + row_height > 0 + and ( + end_x > rect.right() + or (end_x + r_margin) > rect.right() + ) + ) + if not wrap: + next_pos_x = end_x + h_spacing + else: + pos_x = left_x + pos_y += row_height + v_spacing + next_pos_x = pos_x + item_width + h_spacing + row_height = 0 + + if not only_calculate: + item.setGeometry( + QtCore.QRect(pos_x, pos_y, item_width, item_height) + ) + + pos_x = next_pos_x + row_height = max(row_height, item_height) + + height = (pos_y - top_y) + row_height + if height > 0: + height += b_margin + return height diff --git a/openpype/tools/utils/widgets.py b/openpype/tools/utils/widgets.py index bae89aeb09..5a8104611b 100644 --- a/openpype/tools/utils/widgets.py +++ b/openpype/tools/utils/widgets.py @@ -101,6 +101,46 @@ class PlaceholderLineEdit(QtWidgets.QLineEdit): self.setPalette(filter_palette) +class ExpandingTextEdit(QtWidgets.QTextEdit): + """QTextEdit which does not have sroll area but expands height.""" + + def __init__(self, parent=None): + super(ExpandingTextEdit, self).__init__(parent) + + size_policy = self.sizePolicy() + size_policy.setHeightForWidth(True) + size_policy.setVerticalPolicy(QtWidgets.QSizePolicy.Preferred) + self.setSizePolicy(size_policy) + + self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + + doc = self.document() + doc.contentsChanged.connect(self._on_doc_change) + + def _on_doc_change(self): + self.updateGeometry() + + def hasHeightForWidth(self): + return True + + def heightForWidth(self, width): + margins = self.contentsMargins() + + document_width = 0 + if width >= margins.left() + margins.right(): + document_width = width - margins.left() - margins.right() + + document = self.document().clone() + document.setTextWidth(document_width) + + return margins.top() + document.size().height() + margins.bottom() + + def sizeHint(self): + width = super(ExpandingTextEdit, self).sizeHint().width() + return QtCore.QSize(width, self.heightForWidth(width)) + + class BaseClickableFrame(QtWidgets.QFrame): """Widget that catch left mouse click and can trigger a callback. @@ -161,19 +201,34 @@ class ClickableLabel(QtWidgets.QLabel): class ExpandBtnLabel(QtWidgets.QLabel): """Label showing expand icon meant for ExpandBtn.""" + state_changed = QtCore.Signal() + + def __init__(self, parent): super(ExpandBtnLabel, self).__init__(parent) - self._source_collapsed_pix = QtGui.QPixmap( - get_style_image_path("branch_closed") - ) - self._source_expanded_pix = QtGui.QPixmap( - get_style_image_path("branch_open") - ) + self._source_collapsed_pix = self._create_collapsed_pixmap() + self._source_expanded_pix = self._create_expanded_pixmap() self._current_image = self._source_collapsed_pix self._collapsed = True - def set_collapsed(self, collapsed): + def _create_collapsed_pixmap(self): + return QtGui.QPixmap( + get_style_image_path("branch_closed") + ) + + def _create_expanded_pixmap(self): + return QtGui.QPixmap( + get_style_image_path("branch_open") + ) + + @property + def collapsed(self): + return self._collapsed + + def set_collapsed(self, collapsed=None): + if collapsed is None: + collapsed = not self._collapsed if self._collapsed == collapsed: return self._collapsed = collapsed @@ -182,6 +237,7 @@ class ExpandBtnLabel(QtWidgets.QLabel): else: self._current_image = self._source_expanded_pix self._set_resized_pix() + self.state_changed.emit() def resizeEvent(self, event): self._set_resized_pix() @@ -203,21 +259,55 @@ class ExpandBtnLabel(QtWidgets.QLabel): class ExpandBtn(ClickableFrame): + state_changed = QtCore.Signal() + def __init__(self, parent=None): super(ExpandBtn, self).__init__(parent) - pixmap_label = ExpandBtnLabel(self) + pixmap_label = self._create_pix_widget(self) layout = QtWidgets.QHBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(pixmap_label) + pixmap_label.state_changed.connect(self.state_changed) + self._pixmap_label = pixmap_label - def set_collapsed(self, collapsed): + def _create_pix_widget(self, parent=None): + if parent is None: + parent = self + return ExpandBtnLabel(parent) + + @property + def collapsed(self): + return self._pixmap_label.collapsed + + def set_collapsed(self, collapsed=None): self._pixmap_label.set_collapsed(collapsed) +class ClassicExpandBtnLabel(ExpandBtnLabel): + def _create_collapsed_pixmap(self): + return QtGui.QPixmap( + get_style_image_path("right_arrow") + ) + + def _create_expanded_pixmap(self): + return QtGui.QPixmap( + get_style_image_path("down_arrow") + ) + + +class ClassicExpandBtn(ExpandBtn): + """Same as 'ExpandBtn' but with arrow images.""" + + def _create_pix_widget(self, parent=None): + if parent is None: + parent = self + return ClassicExpandBtnLabel(parent) + + class ImageButton(QtWidgets.QPushButton): """PushButton with icon and size of font. From 96a4edf8cb412906047be7435a742ec80e2f4b94 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 23 May 2023 23:02:52 +0200 Subject: [PATCH 332/360] Resolve: fixing the issue with no active timeline during bootstrap of loader --- openpype/hosts/resolve/api/lib.py | 32 ++++++++++++++++--- .../hosts/resolve/plugins/load/load_clip.py | 1 + 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index b3ad20df39..1c33749a77 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -91,16 +91,39 @@ def get_current_project(): return self.project_manager.GetCurrentProject() -def get_current_timeline(new=False): +def get_current_timeline(any=False, new=False): + """Get current timeline object. + + Args: + any (bool, optional): return any even new if no timeline available. + Defaults to False. + new (bool, optional): return only new timeline. Defaults to False. + + Returns: + _type_: _description_ + """ # get current project project = get_current_project() + timeline = project.GetCurrentTimeline() + + # return current timeline only if it is not new + if timeline and not new: + return timeline + + # if any is True then return any timeline + if any: + timeline_count = project.GetTimelineCount() + if timeline_count == 0: + # if there is no timeline then create a new one + new = True + + # create new timeline if new is True if new: media_pool = project.GetMediaPool() new_timeline = media_pool.CreateEmptyTimeline(self.pype_timeline_name) project.SetCurrentTimeline(new_timeline) - - return project.GetCurrentTimeline() + return new_timeline def create_bin(name: str, root: object = None) -> object: @@ -312,7 +335,8 @@ def get_current_timeline_items( track_type = track_type or "video" selecting_color = selecting_color or "Chocolate" project = get_current_project() - timeline = get_current_timeline() + # make sure some timeline will be active with `any` argument + timeline = get_current_timeline(any=True) selected_clips = [] # get all tracks count filtered by track type diff --git a/openpype/hosts/resolve/plugins/load/load_clip.py b/openpype/hosts/resolve/plugins/load/load_clip.py index d30a7ea272..05bfb003d6 100644 --- a/openpype/hosts/resolve/plugins/load/load_clip.py +++ b/openpype/hosts/resolve/plugins/load/load_clip.py @@ -19,6 +19,7 @@ from openpype.lib.transcoding import ( IMAGE_EXTENSIONS ) + class LoadClip(plugin.TimelineItemLoader): """Load a subset to timeline as clip From 179dc65f501faa6e71d5e783ef0d01f0d1ac09aa Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 24 May 2023 03:25:50 +0000 Subject: [PATCH 333/360] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index 8874eb510d..3d7f64b991 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.8-nightly.2" +__version__ = "3.15.8-nightly.3" From ea2d87d903ed95200e1ceb121440e690b65907e0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 24 May 2023 03:26:39 +0000 Subject: [PATCH 334/360] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 244eb1a363..a9f1f1cc02 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.15.8-nightly.3 - 3.15.8-nightly.2 - 3.15.8-nightly.1 - 3.15.7 @@ -134,7 +135,6 @@ body: - 3.14.2-nightly.3 - 3.14.2-nightly.2 - 3.14.2-nightly.1 - - 3.14.1 validations: required: true - type: dropdown From fe02a093128b17a02d0c2e87405c8beca7bc23e4 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 24 May 2023 10:01:59 +0200 Subject: [PATCH 335/360] Deadline: fix selection from multiple webservices (#5015) * OP-4380 - override default DL from project settings * OP-4380 - updated documentation --- .../collect_default_deadline_server.py | 26 ++++++++++++++++++- website/docs/module_deadline.md | 3 +++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/openpype/modules/deadline/plugins/publish/collect_default_deadline_server.py b/openpype/modules/deadline/plugins/publish/collect_default_deadline_server.py index e6ad6a9aa1..cb2b0cf156 100644 --- a/openpype/modules/deadline/plugins/publish/collect_default_deadline_server.py +++ b/openpype/modules/deadline/plugins/publish/collect_default_deadline_server.py @@ -4,7 +4,18 @@ import pyblish.api class CollectDefaultDeadlineServer(pyblish.api.ContextPlugin): - """Collect default Deadline Webservice URL.""" + """Collect default Deadline Webservice URL. + + DL webservice addresses must be configured first in System Settings for + project settings enum to work. + + Default webservice could be overriden by + `project_settings/deadline/deadline_servers`. Currently only single url + is expected. + + This url could be overriden by some hosts directly on instances with + `CollectDeadlineServerFromInstance`. + """ order = pyblish.api.CollectorOrder + 0.410 label = "Default Deadline Webservice" @@ -23,3 +34,16 @@ class CollectDefaultDeadlineServer(pyblish.api.ContextPlugin): context.data["defaultDeadline"] = deadline_module.deadline_urls["default"] # noqa: E501 context.data["deadlinePassMongoUrl"] = self.pass_mongo_url + + deadline_servers = (context.data + ["project_settings"] + ["deadline"] + ["deadline_servers"]) + if deadline_servers: + deadline_server_name = deadline_servers[0] + deadline_webservice = deadline_module.deadline_urls.get( + deadline_server_name) + if deadline_webservice: + context.data["defaultDeadline"] = deadline_webservice + self.log.debug("Overriding from project settings with {}".format( # noqa: E501 + deadline_webservice)) diff --git a/website/docs/module_deadline.md b/website/docs/module_deadline.md index 94b6a381c2..bca2a83936 100644 --- a/website/docs/module_deadline.md +++ b/website/docs/module_deadline.md @@ -22,6 +22,9 @@ For [AWS Thinkbox Deadline](https://www.awsthinkbox.com/deadline) support you ne 5. Install our custom plugin and scripts to your deadline repository. It should be as simple as copying content of `openpype/modules/deadline/repository/custom` to `path/to/your/deadline/repository/custom`. +Multiple different DL webservice could be configured. First set them in point 4., then they could be configured per project in `project_settings/deadline/deadline_servers`. +Only single webservice could be a target of publish though. + ## Configuration From 248336bb0ddf2a61632e579600021b093c24440f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 24 May 2023 10:51:35 +0200 Subject: [PATCH 336/360] General: Lib code cleanup (#5003) * implemented 'is_func_signature_supported' function * 'WeakMethod' can be imported from 'python_2_comp' all the time * simplified events logic for callback registration * modified docstrings in publish lib * removed unused imports * fixed 'run_openpype_process' docstring --- openpype/lib/__init__.py | 6 ++- openpype/lib/events.py | 43 +++--------------- openpype/lib/execute.py | 2 +- openpype/lib/python_2_comp.py | 65 +++++++++++++++------------- openpype/lib/python_module_tools.py | 67 +++++++++++++++++++++++++++++ openpype/pipeline/publish/lib.py | 34 ++++++++------- 6 files changed, 129 insertions(+), 88 deletions(-) diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index 9eb7724a60..06de486f2e 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # flake8: noqa E402 -"""Pype module API.""" +"""OpenPype lib functions.""" # add vendor to sys path based on Python version import sys import os @@ -94,7 +94,8 @@ from .python_module_tools import ( modules_from_path, recursive_bases_from_class, classes_from_module, - import_module_from_dirpath + import_module_from_dirpath, + is_func_signature_supported, ) from .profiles_filtering import ( @@ -243,6 +244,7 @@ __all__ = [ "recursive_bases_from_class", "classes_from_module", "import_module_from_dirpath", + "is_func_signature_supported", "get_transcode_temp_directory", "should_convert_for_ffmpeg", diff --git a/openpype/lib/events.py b/openpype/lib/events.py index bed00fe659..dca58fcf93 100644 --- a/openpype/lib/events.py +++ b/openpype/lib/events.py @@ -6,10 +6,9 @@ import inspect import logging import weakref from uuid import uuid4 -try: - from weakref import WeakMethod -except Exception: - from openpype.lib.python_2_comp import WeakMethod + +from .python_2_comp import WeakMethod +from .python_module_tools import is_func_signature_supported class MissingEventSystem(Exception): @@ -80,40 +79,8 @@ class EventCallback(object): # Get expected arguments from function spec # - positional arguments are always preferred - expect_args = False - expect_kwargs = False - fake_event = "fake" - if hasattr(inspect, "signature"): - # Python 3 using 'Signature' object where we try to bind arg - # or kwarg. Using signature is recommended approach based on - # documentation. - sig = inspect.signature(func) - try: - sig.bind(fake_event) - expect_args = True - except TypeError: - pass - - try: - sig.bind(event=fake_event) - expect_kwargs = True - except TypeError: - pass - - else: - # In Python 2 'signature' is not available so 'getcallargs' is used - # - 'getcallargs' is marked as deprecated since Python 3.0 - try: - inspect.getcallargs(func, fake_event) - expect_args = True - except TypeError: - pass - - try: - inspect.getcallargs(func, event=fake_event) - expect_kwargs = True - except TypeError: - pass + expect_args = is_func_signature_supported(func, "fake") + expect_kwargs = is_func_signature_supported(func, event="fake") self._func_ref = func_ref self._func_name = func_name diff --git a/openpype/lib/execute.py b/openpype/lib/execute.py index ef456395e7..6f52efdfcc 100644 --- a/openpype/lib/execute.py +++ b/openpype/lib/execute.py @@ -190,7 +190,7 @@ def run_openpype_process(*args, **kwargs): Example: ``` - run_openpype_process("run", "") + run_detached_process("run", "") ``` Args: diff --git a/openpype/lib/python_2_comp.py b/openpype/lib/python_2_comp.py index d7137dbe9c..091c51a6f6 100644 --- a/openpype/lib/python_2_comp.py +++ b/openpype/lib/python_2_comp.py @@ -1,41 +1,44 @@ import weakref -class _weak_callable: - def __init__(self, obj, func): - self.im_self = obj - self.im_func = func +WeakMethod = getattr(weakref, "WeakMethod", None) - def __call__(self, *args, **kws): - if self.im_self is None: - return self.im_func(*args, **kws) - else: - return self.im_func(self.im_self, *args, **kws) +if WeakMethod is None: + class _WeakCallable: + def __init__(self, obj, func): + self.im_self = obj + self.im_func = func + + def __call__(self, *args, **kws): + if self.im_self is None: + return self.im_func(*args, **kws) + else: + return self.im_func(self.im_self, *args, **kws) -class WeakMethod: - """ Wraps a function or, more importantly, a bound method in - a way that allows a bound method's object to be GCed, while - providing the same interface as a normal weak reference. """ + class WeakMethod: + """ Wraps a function or, more importantly, a bound method in + a way that allows a bound method's object to be GCed, while + providing the same interface as a normal weak reference. """ - def __init__(self, fn): - try: - self._obj = weakref.ref(fn.im_self) - self._meth = fn.im_func - except AttributeError: - # It's not a bound method - self._obj = None - self._meth = fn + def __init__(self, fn): + try: + self._obj = weakref.ref(fn.im_self) + self._meth = fn.im_func + except AttributeError: + # It's not a bound method + self._obj = None + self._meth = fn - def __call__(self): - if self._dead(): - return None - return _weak_callable(self._getobj(), self._meth) + def __call__(self): + if self._dead(): + return None + return _WeakCallable(self._getobj(), self._meth) - def _dead(self): - return self._obj is not None and self._obj() is None + def _dead(self): + return self._obj is not None and self._obj() is None - def _getobj(self): - if self._obj is None: - return None - return self._obj() + def _getobj(self): + if self._obj is None: + return None + return self._obj() diff --git a/openpype/lib/python_module_tools.py b/openpype/lib/python_module_tools.py index 9e8e94842c..a10263f991 100644 --- a/openpype/lib/python_module_tools.py +++ b/openpype/lib/python_module_tools.py @@ -230,3 +230,70 @@ def import_module_from_dirpath(dirpath, folder_name, dst_module_name=None): dirpath, folder_name, dst_module_name ) return module + + +def is_func_signature_supported(func, *args, **kwargs): + """Check if a function signature supports passed args and kwargs. + + This check does not actually call the function, just look if function can + be called with the arguments. + + Notes: + This does NOT check if the function would work with passed arguments + only if they can be passed in. If function have *args, **kwargs + in paramaters, this will always return 'True'. + + Example: + >>> def my_function(my_number): + ... return my_number + 1 + ... + >>> is_func_signature_supported(my_function, 1) + True + >>> is_func_signature_supported(my_function, 1, 2) + False + >>> is_func_signature_supported(my_function, my_number=1) + True + >>> is_func_signature_supported(my_function, number=1) + False + >>> is_func_signature_supported(my_function, "string") + True + >>> def my_other_function(*args, **kwargs): + ... my_function(*args, **kwargs) + ... + >>> is_func_signature_supported( + ... my_other_function, + ... "string", + ... 1, + ... other=None + ... ) + True + + Args: + func (function): A function where the signature should be tested. + *args (tuple[Any]): Positional arguments for function signature. + **kwargs (dict[str, Any]): Keyword arguments for function signature. + + Returns: + bool: Function can pass in arguments. + """ + + if hasattr(inspect, "signature"): + # Python 3 using 'Signature' object where we try to bind arg + # or kwarg. Using signature is recommended approach based on + # documentation. + sig = inspect.signature(func) + try: + sig.bind(*args, **kwargs) + return True + except TypeError: + pass + + else: + # In Python 2 'signature' is not available so 'getcallargs' is used + # - 'getcallargs' is marked as deprecated since Python 3.0 + try: + inspect.getcallargs(func, *args, **kwargs) + return True + except TypeError: + pass + return False diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index 40186238aa..63a856e326 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -1,12 +1,10 @@ import os import sys -import types import inspect import copy import tempfile import xml.etree.ElementTree -import six import pyblish.util import pyblish.plugin import pyblish.api @@ -42,7 +40,9 @@ def get_template_name_profiles( Args: project_name (str): Name of project where to look for templates. - project_settings(Dic[str, Any]): Prepared project settings. + project_settings (Dict[str, Any]): Prepared project settings. + logger (Optional[logging.Logger]): Logger object to be used instead + of default logger. Returns: List[Dict[str, Any]]: Publish template profiles. @@ -103,7 +103,9 @@ def get_hero_template_name_profiles( Args: project_name (str): Name of project where to look for templates. - project_settings(Dic[str, Any]): Prepared project settings. + project_settings (Dict[str, Any]): Prepared project settings. + logger (Optional[logging.Logger]): Logger object to be used instead + of default logger. Returns: List[Dict[str, Any]]: Publish template profiles. @@ -172,9 +174,10 @@ def get_publish_template_name( project_name (str): Name of project where to look for settings. host_name (str): Name of host integration. family (str): Family for which should be found template. - task_name (str): Task name on which is intance working. - task_type (str): Task type on which is intance working. - project_setting (Dict[str, Any]): Prepared project settings. + task_name (str): Task name on which is instance working. + task_type (str): Task type on which is instance working. + project_settings (Dict[str, Any]): Prepared project settings. + hero (bool): Template is for hero version publishing. logger (logging.Logger): Custom logger used for 'filter_profiles' function. @@ -264,19 +267,18 @@ def load_help_content_from_plugin(plugin): def publish_plugins_discover(paths=None): """Find and return available pyblish plug-ins - Overridden function from `pyblish` module to be able collect crashed files - and reason of their crash. + Overridden function from `pyblish` module to be able to collect + crashed files and reason of their crash. Arguments: paths (list, optional): Paths to discover plug-ins from. If no paths are provided, all paths are searched. - """ # The only difference with `pyblish.api.discover` result = DiscoverResult(pyblish.api.Plugin) - plugins = dict() + plugins = {} plugin_names = [] allow_duplicates = pyblish.plugin.ALLOW_DUPLICATES @@ -302,7 +304,7 @@ def publish_plugins_discover(paths=None): mod_name, mod_ext = os.path.splitext(fname) - if not mod_ext == ".py": + if mod_ext != ".py": continue try: @@ -533,10 +535,10 @@ def find_close_plugin(close_plugin_name, log): def remote_publish(log, close_plugin_name=None, raise_error=False): """Loops through all plugins, logs to console. Used for tests. - Args: - log (openpype.lib.Logger) - close_plugin_name (str): name of plugin with responsibility to - close host app + Args: + log (Logger) + close_plugin_name (str): name of plugin with responsibility to + close host app """ # Error exit as soon as any error occurs. error_format = "Failed {plugin.__name__}: {error} -- {error.traceback}" From 17a38c32a4ba6e33798a097418c1f91c732d1fb8 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 24 May 2023 10:54:31 +0200 Subject: [PATCH 337/360] Enhancement: Improve logging levels and messages for artist facing publish reports (#5018) * Tweak log levels and message to be more informative to artist in report page * Tweak levels and clarity of logs * Tweak levels and clarity of logs + tweak grammar * Cosmetics * Improve logging * Simplify logging * Convert to debug log if it's skipping thumbnail integration if there's no thumbnail whatsoever to integrate * Tweak to debug since they only show representation ids hardly understandable to the artist * Match logging message across hosts + include filepath for full clarity * Tweak message to clarify it only starts checking and not that it requires filling + to debug log * Tweak to debug log if there's basically no thumbnail to integrate at the end * Tweak log levels - Artist doesn't care what's prepared, especially since afterwards it's logged what gets written to the database anyway * Log clearly it's processing a legacy instance * Cosmetics --- .../fusion/plugins/publish/collect_inputs.py | 2 +- .../fusion/plugins/publish/save_scene.py | 2 +- .../houdini/plugins/publish/collect_frames.py | 7 ++-- .../houdini/plugins/publish/collect_inputs.py | 2 +- .../plugins/publish/collect_instances.py | 4 +- .../plugins/publish/collect_workfile.py | 3 +- .../houdini/plugins/publish/save_scene.py | 2 +- .../publish/validate_workfile_paths.py | 41 ++++++++++++++----- .../maya/plugins/publish/collect_inputs.py | 2 +- .../hosts/maya/plugins/publish/save_scene.py | 2 +- .../plugins/publish/save_workfile.py | 5 ++- .../plugins/publish/submit_fusion_deadline.py | 2 +- .../plugins/publish/submit_nuke_deadline.py | 2 +- .../plugins/publish/submit_publish_job.py | 2 +- .../publish/validate_deadline_pools.py | 2 +- openpype/pipeline/publish/publish_plugins.py | 13 +++--- openpype/plugins/publish/cleanup.py | 9 ++-- .../publish/collect_anatomy_context_data.py | 5 ++- .../publish/collect_anatomy_instance_data.py | 8 ++-- .../plugins/publish/collect_anatomy_object.py | 2 +- .../publish/collect_custom_staging_dir.py | 2 +- .../publish/collect_from_create_context.py | 4 +- .../plugins/publish/collect_scene_version.py | 5 ++- openpype/plugins/publish/extract_burnin.py | 4 +- .../publish/extract_color_transcode.py | 6 +-- openpype/plugins/publish/extract_review.py | 6 +-- openpype/plugins/publish/extract_thumbnail.py | 14 +++---- .../publish/extract_thumbnail_from_source.py | 4 +- openpype/plugins/publish/integrate.py | 8 ++-- openpype/plugins/publish/integrate_legacy.py | 6 ++- .../plugins/publish/integrate_thumbnail.py | 4 +- 31 files changed, 106 insertions(+), 74 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/collect_inputs.py b/openpype/hosts/fusion/plugins/publish/collect_inputs.py index 1bb3cd1220..a6628300db 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_inputs.py +++ b/openpype/hosts/fusion/plugins/publish/collect_inputs.py @@ -113,4 +113,4 @@ class CollectUpstreamInputs(pyblish.api.InstancePlugin): inputs = [c["representation"] for c in containers] instance.data["inputRepresentations"] = inputs - self.log.info("Collected inputs: %s" % inputs) + self.log.debug("Collected inputs: %s" % inputs) diff --git a/openpype/hosts/fusion/plugins/publish/save_scene.py b/openpype/hosts/fusion/plugins/publish/save_scene.py index a249c453d8..0798e7c8b7 100644 --- a/openpype/hosts/fusion/plugins/publish/save_scene.py +++ b/openpype/hosts/fusion/plugins/publish/save_scene.py @@ -17,5 +17,5 @@ class FusionSaveComp(pyblish.api.ContextPlugin): current = comp.GetAttrs().get("COMPS_FileName", "") assert context.data['currentFile'] == current - self.log.info("Saving current file..") + self.log.info("Saving current file: {}".format(current)) comp.Save() diff --git a/openpype/hosts/houdini/plugins/publish/collect_frames.py b/openpype/hosts/houdini/plugins/publish/collect_frames.py index 6c695f64e9..059793e3c5 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_frames.py +++ b/openpype/hosts/houdini/plugins/publish/collect_frames.py @@ -8,7 +8,6 @@ import pyblish.api from openpype.hosts.houdini.api import lib - class CollectFrames(pyblish.api.InstancePlugin): """Collect all frames which would be saved from the ROP nodes""" @@ -34,8 +33,10 @@ class CollectFrames(pyblish.api.InstancePlugin): self.log.warning("Using current frame: {}".format(hou.frame())) output = output_parm.eval() - _, ext = lib.splitext(output, - allowed_multidot_extensions=[".ass.gz"]) + _, ext = lib.splitext( + output, + allowed_multidot_extensions=[".ass.gz"] + ) file_name = os.path.basename(output) result = file_name diff --git a/openpype/hosts/houdini/plugins/publish/collect_inputs.py b/openpype/hosts/houdini/plugins/publish/collect_inputs.py index 6411376ea3..e92a42f2e8 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_inputs.py +++ b/openpype/hosts/houdini/plugins/publish/collect_inputs.py @@ -117,4 +117,4 @@ class CollectUpstreamInputs(pyblish.api.InstancePlugin): inputs = [c["representation"] for c in containers] instance.data["inputRepresentations"] = inputs - self.log.info("Collected inputs: %s" % inputs) + self.log.debug("Collected inputs: %s" % inputs) diff --git a/openpype/hosts/houdini/plugins/publish/collect_instances.py b/openpype/hosts/houdini/plugins/publish/collect_instances.py index bb85630552..5d5347f96e 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_instances.py +++ b/openpype/hosts/houdini/plugins/publish/collect_instances.py @@ -55,7 +55,9 @@ class CollectInstances(pyblish.api.ContextPlugin): has_family = node.evalParm("family") assert has_family, "'%s' is missing 'family'" % node.name() - self.log.info("processing {}".format(node)) + self.log.info( + "Processing legacy instance node {}".format(node.path()) + ) data = lib.read(node) # Check bypass state and reverse diff --git a/openpype/hosts/houdini/plugins/publish/collect_workfile.py b/openpype/hosts/houdini/plugins/publish/collect_workfile.py index a6e94ec29e..aa533bcf1b 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_workfile.py +++ b/openpype/hosts/houdini/plugins/publish/collect_workfile.py @@ -32,5 +32,4 @@ class CollectWorkfile(pyblish.api.InstancePlugin): "stagingDir": folder, }] - self.log.info('Collected instance: {}'.format(file)) - self.log.info('staging Dir: {}'.format(folder)) + self.log.debug('Collected workfile instance: {}'.format(file)) diff --git a/openpype/hosts/houdini/plugins/publish/save_scene.py b/openpype/hosts/houdini/plugins/publish/save_scene.py index d6e07ccab0..703d3e4895 100644 --- a/openpype/hosts/houdini/plugins/publish/save_scene.py +++ b/openpype/hosts/houdini/plugins/publish/save_scene.py @@ -20,7 +20,7 @@ class SaveCurrentScene(pyblish.api.ContextPlugin): ) if host.has_unsaved_changes(): - self.log.info("Saving current file {}...".format(current_file)) + self.log.info("Saving current file: {}".format(current_file)) host.save_workfile(current_file) else: self.log.debug("No unsaved changes, skipping file save..") diff --git a/openpype/hosts/houdini/plugins/publish/validate_workfile_paths.py b/openpype/hosts/houdini/plugins/publish/validate_workfile_paths.py index 7707cc2dba..543c8e1407 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_workfile_paths.py +++ b/openpype/hosts/houdini/plugins/publish/validate_workfile_paths.py @@ -28,18 +28,37 @@ class ValidateWorkfilePaths( if not self.is_active(instance.data): return invalid = self.get_invalid() - self.log.info( - "node types to check: {}".format(", ".join(self.node_types))) - self.log.info( - "prohibited vars: {}".format(", ".join(self.prohibited_vars)) + self.log.debug( + "Checking node types: {}".format(", ".join(self.node_types))) + self.log.debug( + "Searching prohibited vars: {}".format( + ", ".join(self.prohibited_vars) + ) ) - if invalid: - for param in invalid: - self.log.error( - "{}: {}".format(param.path(), param.unexpandedString())) - raise PublishValidationError( - "Invalid paths found", title=self.label) + if invalid: + all_container_vars = set() + for param in invalid: + value = param.unexpandedString() + contained_vars = [ + var for var in self.prohibited_vars + if var in value + ] + all_container_vars.update(contained_vars) + + self.log.error( + "Parm {} contains prohibited vars {}: {}".format( + param.path(), + ", ".join(contained_vars), + value) + ) + + message = ( + "Prohibited vars {} found in parameter values".format( + ", ".join(all_container_vars) + ) + ) + raise PublishValidationError(message, title=self.label) @classmethod def get_invalid(cls): @@ -63,7 +82,7 @@ class ValidateWorkfilePaths( def repair(cls, instance): invalid = cls.get_invalid() for param in invalid: - cls.log.info("processing: {}".format(param.path())) + cls.log.info("Processing: {}".format(param.path())) cls.log.info("Replacing {} for {}".format( param.unexpandedString(), hou.text.expandString(param.unexpandedString()))) diff --git a/openpype/hosts/maya/plugins/publish/collect_inputs.py b/openpype/hosts/maya/plugins/publish/collect_inputs.py index 9c3f0f5efa..895c92762b 100644 --- a/openpype/hosts/maya/plugins/publish/collect_inputs.py +++ b/openpype/hosts/maya/plugins/publish/collect_inputs.py @@ -166,7 +166,7 @@ class CollectUpstreamInputs(pyblish.api.InstancePlugin): inputs = [c["representation"] for c in containers] instance.data["inputRepresentations"] = inputs - self.log.info("Collected inputs: %s" % inputs) + self.log.debug("Collected inputs: %s" % inputs) def _collect_renderlayer_inputs(self, scene_containers, instance): """Collects inputs from nodes in renderlayer, incl. shaders + camera""" diff --git a/openpype/hosts/maya/plugins/publish/save_scene.py b/openpype/hosts/maya/plugins/publish/save_scene.py index 45e62e7b44..495c339731 100644 --- a/openpype/hosts/maya/plugins/publish/save_scene.py +++ b/openpype/hosts/maya/plugins/publish/save_scene.py @@ -31,5 +31,5 @@ class SaveCurrentScene(pyblish.api.ContextPlugin): # remove lockfile before saving if is_workfile_lock_enabled("maya", project_name, project_settings): remove_workfile_lock(current) - self.log.info("Saving current file..") + self.log.info("Saving current file: {}".format(current)) cmds.file(save=True, force=True) diff --git a/openpype/hosts/substancepainter/plugins/publish/save_workfile.py b/openpype/hosts/substancepainter/plugins/publish/save_workfile.py index 4874b5e5c7..9662f31922 100644 --- a/openpype/hosts/substancepainter/plugins/publish/save_workfile.py +++ b/openpype/hosts/substancepainter/plugins/publish/save_workfile.py @@ -16,11 +16,12 @@ class SaveCurrentWorkfile(pyblish.api.ContextPlugin): def process(self, context): host = registered_host() - if context.data["currentFile"] != host.get_current_workfile(): + current = host.get_current_workfile() + if context.data["currentFile"] != current: raise KnownPublishError("Workfile has changed during publishing!") if host.has_unsaved_changes(): - self.log.info("Saving current file..") + self.log.info("Saving current file: {}".format(current)) host.save_workfile() else: self.log.debug("Skipping workfile save because there are no " diff --git a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py index 717391100d..a48596c6bf 100644 --- a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py @@ -73,7 +73,7 @@ class FusionSubmitDeadline( def process(self, instance): if not instance.data.get("farm"): - self.log.info("Skipping local instance.") + self.log.debug("Skipping local instance.") return attribute_values = self.get_attr_values_from_data( diff --git a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py index 5c598df94b..4900231783 100644 --- a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py @@ -86,7 +86,7 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin, def process(self, instance): if not instance.data.get("farm"): - self.log.info("Skipping local instance.") + self.log.debug("Skipping local instance.") return instance.data["attributeValues"] = self.get_attr_values_from_data( diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index eeb813cb62..68eb0a437d 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -762,7 +762,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): """ if not instance.data.get("farm"): - self.log.info("Skipping local instance.") + self.log.debug("Skipping local instance.") return data = instance.data.copy() diff --git a/openpype/modules/deadline/plugins/publish/validate_deadline_pools.py b/openpype/modules/deadline/plugins/publish/validate_deadline_pools.py index 7c8ab62d4d..e1c0595830 100644 --- a/openpype/modules/deadline/plugins/publish/validate_deadline_pools.py +++ b/openpype/modules/deadline/plugins/publish/validate_deadline_pools.py @@ -26,7 +26,7 @@ class ValidateDeadlinePools(OptionalPyblishPluginMixin, def process(self, instance): if not instance.data.get("farm"): - self.log.info("Skipping local instance.") + self.log.debug("Skipping local instance.") return # get default deadline webservice url from deadline module diff --git a/openpype/pipeline/publish/publish_plugins.py b/openpype/pipeline/publish/publish_plugins.py index a38896ec8e..a67c8397b1 100644 --- a/openpype/pipeline/publish/publish_plugins.py +++ b/openpype/pipeline/publish/publish_plugins.py @@ -379,7 +379,9 @@ class ColormanagedPyblishPluginMixin(object): # check if ext in lower case is in self.allowed_ext if ext.lstrip(".").lower() not in self.allowed_ext: - self.log.debug("Extension is not in allowed extensions.") + self.log.debug( + "Extension '{}' is not in allowed extensions.".format(ext) + ) return if colorspace_settings is None: @@ -393,8 +395,7 @@ class ColormanagedPyblishPluginMixin(object): self.log.warning("No colorspace management was defined") return - self.log.info("Config data is : `{}`".format( - config_data)) + self.log.debug("Config data is: `{}`".format(config_data)) project_name = context.data["projectName"] host_name = context.data["hostName"] @@ -405,8 +406,7 @@ class ColormanagedPyblishPluginMixin(object): if isinstance(filename, list): filename = filename[0] - self.log.debug("__ filename: `{}`".format( - filename)) + self.log.debug("__ filename: `{}`".format(filename)) # get matching colorspace from rules colorspace = colorspace or get_imageio_colorspace_from_filepath( @@ -415,8 +415,7 @@ class ColormanagedPyblishPluginMixin(object): file_rules=file_rules, project_settings=project_settings ) - self.log.debug("__ colorspace: `{}`".format( - colorspace)) + self.log.debug("__ colorspace: `{}`".format(colorspace)) # infuse data to representation if colorspace: diff --git a/openpype/plugins/publish/cleanup.py b/openpype/plugins/publish/cleanup.py index b90c88890d..57cc9c0ab5 100644 --- a/openpype/plugins/publish/cleanup.py +++ b/openpype/plugins/publish/cleanup.py @@ -81,7 +81,8 @@ class CleanUp(pyblish.api.InstancePlugin): staging_dir = instance.data.get("stagingDir", None) if not staging_dir: - self.log.info("Staging dir not set.") + self.log.debug("Skipping cleanup. Staging dir not set " + "on instance: {}.".format(instance)) return if not os.path.normpath(staging_dir).startswith(temp_root): @@ -90,7 +91,7 @@ class CleanUp(pyblish.api.InstancePlugin): return if not os.path.exists(staging_dir): - self.log.info("No staging directory found: %s" % staging_dir) + self.log.debug("No staging directory found at: %s" % staging_dir) return if instance.data.get("stagingDir_persistent"): @@ -131,7 +132,9 @@ class CleanUp(pyblish.api.InstancePlugin): try: os.remove(src) except PermissionError: - self.log.warning("Insufficient permission to delete {}".format(src)) + self.log.warning( + "Insufficient permission to delete {}".format(src) + ) continue # add dir for cleanup diff --git a/openpype/plugins/publish/collect_anatomy_context_data.py b/openpype/plugins/publish/collect_anatomy_context_data.py index 55ce8e06f4..508b01447b 100644 --- a/openpype/plugins/publish/collect_anatomy_context_data.py +++ b/openpype/plugins/publish/collect_anatomy_context_data.py @@ -67,5 +67,6 @@ class CollectAnatomyContextData(pyblish.api.ContextPlugin): # Store context.data["anatomyData"] = anatomy_data - self.log.info("Global anatomy Data collected") - self.log.debug(json.dumps(anatomy_data, indent=4)) + self.log.debug("Global Anatomy Context Data collected:\n{}".format( + json.dumps(anatomy_data, indent=4) + )) diff --git a/openpype/plugins/publish/collect_anatomy_instance_data.py b/openpype/plugins/publish/collect_anatomy_instance_data.py index 4fbb93324b..128ad90b4f 100644 --- a/openpype/plugins/publish/collect_anatomy_instance_data.py +++ b/openpype/plugins/publish/collect_anatomy_instance_data.py @@ -46,17 +46,17 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): follow_workfile_version = False def process(self, context): - self.log.info("Collecting anatomy data for all instances.") + self.log.debug("Collecting anatomy data for all instances.") project_name = context.data["projectName"] self.fill_missing_asset_docs(context, project_name) self.fill_latest_versions(context, project_name) self.fill_anatomy_data(context) - self.log.info("Anatomy Data collection finished.") + self.log.debug("Anatomy Data collection finished.") def fill_missing_asset_docs(self, context, project_name): - self.log.debug("Qeurying asset documents for instances.") + self.log.debug("Querying asset documents for instances.") context_asset_doc = context.data.get("assetEntity") @@ -271,7 +271,7 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): instance_name = instance.data["name"] instance_label = instance.data.get("label") if instance_label: - instance_name += "({})".format(instance_label) + instance_name += " ({})".format(instance_label) self.log.debug("Anatomy data for instance {}: {}".format( instance_name, json.dumps(anatomy_data, indent=4) diff --git a/openpype/plugins/publish/collect_anatomy_object.py b/openpype/plugins/publish/collect_anatomy_object.py index 725cae2b14..f792cf3abd 100644 --- a/openpype/plugins/publish/collect_anatomy_object.py +++ b/openpype/plugins/publish/collect_anatomy_object.py @@ -30,6 +30,6 @@ class CollectAnatomyObject(pyblish.api.ContextPlugin): context.data["anatomy"] = Anatomy(project_name) - self.log.info( + self.log.debug( "Anatomy object collected for project \"{}\".".format(project_name) ) diff --git a/openpype/plugins/publish/collect_custom_staging_dir.py b/openpype/plugins/publish/collect_custom_staging_dir.py index b749b251c0..669c4873e0 100644 --- a/openpype/plugins/publish/collect_custom_staging_dir.py +++ b/openpype/plugins/publish/collect_custom_staging_dir.py @@ -65,6 +65,6 @@ class CollectCustomStagingDir(pyblish.api.InstancePlugin): else: result_str = "Not adding" - self.log.info("{} custom staging dir for instance with '{}'".format( + self.log.debug("{} custom staging dir for instance with '{}'".format( result_str, family )) diff --git a/openpype/plugins/publish/collect_from_create_context.py b/openpype/plugins/publish/collect_from_create_context.py index 5fcf8feb56..4888476fff 100644 --- a/openpype/plugins/publish/collect_from_create_context.py +++ b/openpype/plugins/publish/collect_from_create_context.py @@ -92,5 +92,5 @@ class CollectFromCreateContext(pyblish.api.ContextPlugin): instance.data["transientData"] = transient_data - self.log.info("collected instance: {}".format(instance.data)) - self.log.info("parsing data: {}".format(in_data)) + self.log.debug("collected instance: {}".format(instance.data)) + self.log.debug("parsing data: {}".format(in_data)) diff --git a/openpype/plugins/publish/collect_scene_version.py b/openpype/plugins/publish/collect_scene_version.py index fdbcb3cb9d..cd3231a07d 100644 --- a/openpype/plugins/publish/collect_scene_version.py +++ b/openpype/plugins/publish/collect_scene_version.py @@ -48,10 +48,13 @@ class CollectSceneVersion(pyblish.api.ContextPlugin): if '' in filename: return + self.log.debug( + "Collecting scene version from filename: {}".format(filename) + ) + version = get_version_from_path(filename) assert version, "Cannot determine version" rootVersion = int(version) context.data['version'] = rootVersion - self.log.info("{}".format(type(rootVersion))) self.log.info('Scene Version: %s' % context.data.get('version')) diff --git a/openpype/plugins/publish/extract_burnin.py b/openpype/plugins/publish/extract_burnin.py index a12e8d18b4..10b366dcd6 100644 --- a/openpype/plugins/publish/extract_burnin.py +++ b/openpype/plugins/publish/extract_burnin.py @@ -517,8 +517,8 @@ class ExtractBurnin(publish.Extractor): """ if "burnin" not in (repre.get("tags") or []): - self.log.info(( - "Representation \"{}\" don't have \"burnin\" tag. Skipped." + self.log.debug(( + "Representation \"{}\" does not have \"burnin\" tag. Skipped." ).format(repre["name"])) return False diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 58e0350a2e..45b10620d1 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -336,13 +336,13 @@ class ExtractOIIOTranscode(publish.Extractor): if repre.get("ext") not in self.supported_exts: self.log.debug(( - "Representation '{}' of unsupported extension. Skipped." - ).format(repre["name"])) + "Representation '{}' has unsupported extension: '{}'. Skipped." + ).format(repre["name"], repre.get("ext"))) return False if not repre.get("files"): self.log.debug(( - "Representation '{}' have empty files. Skipped." + "Representation '{}' has empty files. Skipped." ).format(repre["name"])) return False diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index 1062683319..a68addda7d 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -92,8 +92,8 @@ class ExtractReview(pyblish.api.InstancePlugin): host_name = instance.context.data["hostName"] family = self.main_family_from_instance(instance) - self.log.info("Host: \"{}\"".format(host_name)) - self.log.info("Family: \"{}\"".format(family)) + self.log.debug("Host: \"{}\"".format(host_name)) + self.log.debug("Family: \"{}\"".format(family)) profile = filter_profiles( self.profiles, @@ -351,7 +351,7 @@ class ExtractReview(pyblish.api.InstancePlugin): temp_data = self.prepare_temp_data(instance, repre, output_def) files_to_clean = [] if temp_data["input_is_sequence"]: - self.log.info("Filling gaps in sequence.") + self.log.debug("Checking sequence to fill gaps in sequence..") files_to_clean = self.fill_sequence_gaps( files=temp_data["origin_repre"]["files"], staging_dir=new_repre["stagingDir"], diff --git a/openpype/plugins/publish/extract_thumbnail.py b/openpype/plugins/publish/extract_thumbnail.py index 54b933a76d..b98ab64f56 100644 --- a/openpype/plugins/publish/extract_thumbnail.py +++ b/openpype/plugins/publish/extract_thumbnail.py @@ -36,7 +36,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): ).format(subset_name)) return - self.log.info( + self.log.debug( "Processing instance with subset name {}".format(subset_name) ) @@ -89,13 +89,13 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): src_staging = os.path.normpath(repre["stagingDir"]) full_input_path = os.path.join(src_staging, input_file) - self.log.info("input {}".format(full_input_path)) + self.log.debug("input {}".format(full_input_path)) filename = os.path.splitext(input_file)[0] jpeg_file = filename + "_thumb.jpg" full_output_path = os.path.join(dst_staging, jpeg_file) if oiio_supported: - self.log.info("Trying to convert with OIIO") + self.log.debug("Trying to convert with OIIO") # If the input can read by OIIO then use OIIO method for # conversion otherwise use ffmpeg thumbnail_created = self.create_thumbnail_oiio( @@ -148,7 +148,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): def _already_has_thumbnail(self, repres): for repre in repres: - self.log.info("repre {}".format(repre)) + self.log.debug("repre {}".format(repre)) if repre["name"] == "thumbnail": return True return False @@ -173,20 +173,20 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): return filtered_repres def create_thumbnail_oiio(self, src_path, dst_path): - self.log.info("outputting {}".format(dst_path)) + self.log.info("Extracting thumbnail {}".format(dst_path)) oiio_tool_path = get_oiio_tools_path() oiio_cmd = [ oiio_tool_path, "-a", src_path, "-o", dst_path ] - self.log.info("running: {}".format(" ".join(oiio_cmd))) + self.log.debug("running: {}".format(" ".join(oiio_cmd))) try: run_subprocess(oiio_cmd, logger=self.log) return True except Exception: self.log.warning( - "Failed to create thubmnail using oiiotool", + "Failed to create thumbnail using oiiotool", exc_info=True ) return False diff --git a/openpype/plugins/publish/extract_thumbnail_from_source.py b/openpype/plugins/publish/extract_thumbnail_from_source.py index a92f762cde..a9c95d6065 100644 --- a/openpype/plugins/publish/extract_thumbnail_from_source.py +++ b/openpype/plugins/publish/extract_thumbnail_from_source.py @@ -39,7 +39,7 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): self._create_context_thumbnail(instance.context) subset_name = instance.data["subset"] - self.log.info( + self.log.debug( "Processing instance with subset name {}".format(subset_name) ) thumbnail_source = instance.data.get("thumbnailSource") @@ -104,7 +104,7 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): full_output_path = os.path.join(dst_staging, dst_filename) if oiio_supported: - self.log.info("Trying to convert with OIIO") + self.log.debug("Trying to convert with OIIO") # If the input can read by OIIO then use OIIO method for # conversion otherwise use ffmpeg thumbnail_created = self.create_thumbnail_oiio( diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 8e984a9e97..f392cf67f7 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -267,7 +267,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): instance_stagingdir = instance.data.get("stagingDir") if not instance_stagingdir: - self.log.info(( + self.log.debug(( "{0} is missing reference to staging directory." " Will try to get it from representation." ).format(instance)) @@ -480,7 +480,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): update_data ) - self.log.info("Prepared subset: {}".format(subset_name)) + self.log.debug("Prepared subset: {}".format(subset_name)) return subset_doc def prepare_version(self, instance, op_session, subset_doc, project_name): @@ -521,7 +521,9 @@ class IntegrateAsset(pyblish.api.InstancePlugin): project_name, version_doc["type"], version_doc ) - self.log.info("Prepared version: v{0:03d}".format(version_doc["name"])) + self.log.debug( + "Prepared version: v{0:03d}".format(version_doc["name"]) + ) return version_doc diff --git a/openpype/plugins/publish/integrate_legacy.py b/openpype/plugins/publish/integrate_legacy.py index c67ce62bf6..c238cca633 100644 --- a/openpype/plugins/publish/integrate_legacy.py +++ b/openpype/plugins/publish/integrate_legacy.py @@ -147,7 +147,9 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): def process(self, instance): if instance.data.get("processedWithNewIntegrator"): - self.log.info("Instance was already processed with new integrator") + self.log.debug( + "Instance was already processed with new integrator" + ) return for ef in self.exclude_families: @@ -274,7 +276,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): stagingdir = instance.data.get("stagingDir") if not stagingdir: - self.log.info(( + self.log.debug(( "{0} is missing reference to staging directory." " Will try to get it from representation." ).format(instance)) diff --git a/openpype/plugins/publish/integrate_thumbnail.py b/openpype/plugins/publish/integrate_thumbnail.py index 16cc47d432..f6d4f654f5 100644 --- a/openpype/plugins/publish/integrate_thumbnail.py +++ b/openpype/plugins/publish/integrate_thumbnail.py @@ -41,7 +41,7 @@ class IntegrateThumbnails(pyblish.api.ContextPlugin): # Filter instances which can be used for integration filtered_instance_items = self._prepare_instances(context) if not filtered_instance_items: - self.log.info( + self.log.debug( "All instances were filtered. Thumbnail integration skipped." ) return @@ -162,7 +162,7 @@ class IntegrateThumbnails(pyblish.api.ContextPlugin): # Skip instance if thumbnail path is not available for it if not thumbnail_path: - self.log.info(( + self.log.debug(( "Skipping thumbnail integration for instance \"{}\"." " Instance and context" " thumbnail paths are not available." From 22e7f9bd8497bdda99eea9e560788fdea35cb21d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Wed, 24 May 2023 10:58:40 +0200 Subject: [PATCH 338/360] Update openpype/hosts/nuke/plugins/publish/extract_thumbnail.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/nuke/plugins/publish/extract_thumbnail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py index 2336487b37..34f4b4e8cf 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py @@ -125,7 +125,7 @@ class ExtractThumbnail(publish.Extractor): temporary_nodes.append(rnode) previous_node = rnode - if not self.reposition_nodes: + if self.reposition_nodes is None: # [deprecated] create reformat node old way reformat_node = nuke.createNode("Reformat") ref_node = self.nodes.get("Reformat", None) From 084a15ec8c2433e2584dd2c0646417cf811262cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Wed, 24 May 2023 10:58:47 +0200 Subject: [PATCH 339/360] Update openpype/hosts/nuke/plugins/publish/extract_thumbnail.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/nuke/plugins/publish/extract_thumbnail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py index 34f4b4e8cf..21eefda249 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py @@ -30,7 +30,7 @@ class ExtractThumbnail(publish.Extractor): bake_viewer_process = True bake_viewer_input_process = True nodes = {} - reposition_nodes = [] + reposition_nodes = None def process(self, instance): if instance.data.get("farm"): From e5733450e428f7f26e5bfe76fc9fe1e80b42b9f2 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 24 May 2023 12:18:57 +0200 Subject: [PATCH 340/360] Global: plugins cleanup plugin will leave beauty rendered files (#4790) * OP-1066 - add expected files in Deadline into explicit cleanup Implicit cleanup doesn't work correctly, safest option is for DL submissions to mark only files that should be rendered to be deleted after successful publish. * OP-1066 - moved collecting of expected files into collector Parsing of json didn't have context implemented, it is easier to mark expected files in collector. * OP-4793 - delete full stagingDir Reviews might be extracted into staging dir, should be removed too. * Revert "OP-4793 - delete full stagingDir" This reverts commit 8b002191e1ad3b31a0cbe439ca1158946c43b049. * OP-1066 - added function to mark representation files to be cleaned up Should be applicable for all new representations, as reviews, thumbnails, to clean up their intermediate files. * OP-1066 - moved files to better file Cleaned up occurences where not necessary. * OP-1066 - removed unused import * OP-1066 - removed unnecessary setdefault * OP-1066 - removed unnecessary logging * OP-1066 - cleanup metadata json Try to cleanup parent folder if empty. --- openpype/pipeline/publish/lib.py | 19 +++++++++++++++++++ .../plugins/publish/collect_rendered_files.py | 6 ++++++ openpype/plugins/publish/extract_burnin.py | 3 +++ openpype/plugins/publish/extract_review.py | 3 +++ 4 files changed, 31 insertions(+) diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index 63a856e326..b55f813b5e 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -847,3 +847,22 @@ def _validate_transient_template(project_name, template_name, anatomy): raise ValueError(("There is not set \"folder\" template in \"{}\" anatomy" # noqa " for project \"{}\"." ).format(template_name, project_name)) + + +def add_repre_files_for_cleanup(instance, repre): + """ Explicitly mark repre files to be deleted. + + Should be used on intermediate files (eg. review, thumbnails) to be + explicitly deleted. + """ + files = repre["files"] + staging_dir = repre.get("stagingDir") + if not staging_dir: + return + + if isinstance(files, str): + files = [files] + + for file_name in files: + expected_file = os.path.join(staging_dir, file_name) + instance.context.data["cleanupFullPaths"].append(expected_file) diff --git a/openpype/plugins/publish/collect_rendered_files.py b/openpype/plugins/publish/collect_rendered_files.py index 8f8d0a5eeb..6c8d1e9ca5 100644 --- a/openpype/plugins/publish/collect_rendered_files.py +++ b/openpype/plugins/publish/collect_rendered_files.py @@ -13,6 +13,7 @@ import json import pyblish.api from openpype.pipeline import legacy_io, KnownPublishError +from openpype.pipeline.publish.lib import add_repre_files_for_cleanup class CollectRenderedFiles(pyblish.api.ContextPlugin): @@ -89,6 +90,7 @@ class CollectRenderedFiles(pyblish.api.ContextPlugin): # now we can just add instances from json file and we are done for instance_data in data.get("instances"): + self.log.info(" - processing instance for {}".format( instance_data.get("subset"))) instance = self._context.create_instance( @@ -107,6 +109,8 @@ class CollectRenderedFiles(pyblish.api.ContextPlugin): self._fill_staging_dir(repre_data, anatomy) representations.append(repre_data) + add_repre_files_for_cleanup(instance, repre_data) + instance.data["representations"] = representations # add audio if in metadata data @@ -157,6 +161,8 @@ class CollectRenderedFiles(pyblish.api.ContextPlugin): os.environ.update(session_data) session_is_set = True self._process_path(data, anatomy) + context.data["cleanupFullPaths"].append(path) + context.data["cleanupEmptyDirs"].append(os.path.dirname(path)) except Exception as e: self.log.error(e, exc_info=True) raise Exception("Error") from e diff --git a/openpype/plugins/publish/extract_burnin.py b/openpype/plugins/publish/extract_burnin.py index 10b366dcd6..6a8ae958d2 100644 --- a/openpype/plugins/publish/extract_burnin.py +++ b/openpype/plugins/publish/extract_burnin.py @@ -19,6 +19,7 @@ from openpype.lib import ( should_convert_for_ffmpeg ) from openpype.lib.profiles_filtering import filter_profiles +from openpype.pipeline.publish.lib import add_repre_files_for_cleanup class ExtractBurnin(publish.Extractor): @@ -353,6 +354,8 @@ class ExtractBurnin(publish.Extractor): # Add new representation to instance instance.data["representations"].append(new_repre) + add_repre_files_for_cleanup(instance, new_repre) + # Cleanup temp staging dir after procesisng of output definitions if do_convert: temp_dir = repre["stagingDir"] diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index a68addda7d..fa58c03df1 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -24,6 +24,7 @@ from openpype.lib.transcoding import ( get_transcode_temp_directory, ) from openpype.pipeline.publish import KnownPublishError +from openpype.pipeline.publish.lib import add_repre_files_for_cleanup class ExtractReview(pyblish.api.InstancePlugin): @@ -425,6 +426,8 @@ class ExtractReview(pyblish.api.InstancePlugin): ) instance.data["representations"].append(new_repre) + add_repre_files_for_cleanup(instance, new_repre) + def input_is_sequence(self, repre): """Deduce from representation data if input is sequence.""" # TODO GLOBAL ISSUE - Find better way how to find out if input From 8410055b2499e30c80cc7d3bd8c4d10cf76369bb Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 24 May 2023 10:21:38 +0000 Subject: [PATCH 341/360] [Automated] Release --- CHANGELOG.md | 298 ++++++++++++++++++++++++++++++++++++++++++++ openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 300 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bba6b64bfe..a33904735b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,304 @@ # Changelog +## [3.15.8](https://github.com/ynput/OpenPype/tree/3.15.8) + + +[Full Changelog](https://github.com/ynput/OpenPype/compare/3.15.7...3.15.8) + +### **🆕 New features** + + +

+Publisher: Show instances in report page #4915 + +Show publish instances in report page. Also added basic log view with logs grouped by instance. Validation error detail now have 2 colums, one with erro details second with logs. Crashed state shows fast access to report action buttons. Success will show only logs. Publish frame is shrunked automatically on publish stop. + + +___ + +
+ + +
+Fusion - Loader plugins updates #4920 + +Update to some Fusion loader plugins:The sequence loader can now load footage from the image and online family.The FBX loader can now import all formats Fusions FBX node can read.You can now import the content of another workfile into your current comp with the workfile loader. + + +___ + +
+ + +
+Fusion: deadline farm rendering #4955 + +Enabling Fusion for deadline farm rendering. + + +___ + +
+ + +
+AfterEffects: set frame range and resolution #4983 + +Frame information (frame start, duration, fps) and resolution (width and height) is applied to selected composition from Asset Management System (Ftrack or DB) automatically when published instance is created.It is also possible explicitly propagate both values from DB to selected composition by newly added menu buttons. + + +___ + +
+ + +
+Publish: Enhance automated publish plugin settings #4986 + +Added plugins option to define settings category where to look for settings of a plugin and added public helper functions to apply settings `get_plugin_settings` and `apply_plugin_settings_automatically`. + + +___ + +
+ +### **🚀 Enhancements** + + +
+Load Rig References - Change Rig to Animation in Animation instance #4877 + +We are using the template builder to build an animation scene. All the rig placeholders are imported correctly, but the automatically created animation instances retain the rig family in their names and subsets. In our example, we need animationMain instead of rigMain, because this name will be used in the following steps like lighting.Here is the result we need. I checked, and it's not a template builder problem, because even if I load a rig as a reference, the result is the same. For me, since we are in the animation instance, it makes more sense to have animation instead of rig in the name. The naming is just fine if we use create from the Openpype menu. + + +___ + +
+ + +
+Enhancement: Resolve prelaunch code refactoring and update defaults #4916 + +The main reason of this PR is wrong default settings in `openpype/settings/defaults/system_settings/applications.json` for Resolve host. The `bin` folder should not be a part of the macos and Linux `RESOLVE_PYTHON3_PATH` variable.The rest of this PR is some code cleanups for Resolve prelaunch hook to simplify further development.Also added a .gitignore for vscode workspace files. + + +___ + +
+ + +
+Unreal: 🚚 move Unreal plugin to separate repository #4980 + +To support Epic Marketplace have to move AYON Unreal integration plugins to separate repository. This is replacing current files with git submodule, so the change should be functionally without impact.New repository lives here: https://github.com/ynput/ayon-unreal-plugin + + +___ + +
+ + +
+General: Lib code cleanup #5003 + +Small cleanup in lib files in openpype. + + +___ + +
+ + +
+Allow to open with djv by extension instead of representation name #5004 + +Filter open in djv action by extension instead of representation. + + +___ + +
+ + +
+DJV open action `extensions` as `set` #5005 + +Change `extensions` attribute to `set`. + + +___ + +
+ + +
+Nuke: extract thumbnail with multiple reposition nodes #5011 + +Added support for multiple reposition nodes. + + +___ + +
+ + +
+Enhancement: Improve logging levels and messages for artist facing publish reports #5018 + +Tweak the logging levels and messages to try and only show those logs that an artist should see and could understand. Move anything that's slightly more involved into a "debug" message instead. + + +___ + +
+ +### **🐛 Bug fixes** + + +
+Bugfix/frame variable fix #4978 + +Renamed variables to match OpenPype terminology to reduce confusion and add consistency. +___ + +
+ + +
+Global: plugins cleanup plugin will leave beauty rendered files #4790 + +Attempt to mark more files to be cleaned up explicitly in intermediate `renders` folder in work area for farm jobs. + + +___ + +
+ + +
+Fix: Download last workfile doesn't work if not already downloaded #4942 + +Some optimization condition is messing with the feature: if the published workfile is not already downloaded, it won't download it... + + +___ + +
+ + +
+Unreal: Fix transform when loading layout to match existing assets #4972 + +Fixed transform when loading layout to match existing assets. + + +___ + +
+ + +
+fix the bug of fbx loaders in Max #4977 + +bug fix of fbx loaders for not being able to parent to the CON instances while importing cameras(and models) which is published from other DCCs such as Maya. + + +___ + +
+ + +
+AfterEffects: allow returning stub with not saved workfile #4984 + +Allows to use Workfile app to Save first empty workfile. + + +___ + +
+ + +
+Blender: Fix Alembic loading #4985 + +Fixed problem occurring when trying to load an Alembic model in Blender. + + +___ + +
+ + +
+Unreal: Addon Py2 compatibility #4994 + +Fixed Python 2 compatibility of unreal addon. + + +___ + +
+ + +
+Nuke: fixed missing files key in representation #4999 + +Issue with missing keys once rendering target set to existing frames is fixed. Instance has to be evaluated in validation for missing files. + + +___ + +
+ + +
+Unreal: Fix the frame range when loading camera #5002 + +The keyframes of the camera, when loaded, were not using the correct frame range. + + +___ + +
+ + +
+Fusion: fixing frame range targeting #5013 + +Frame range targeting at Rendering instances is now following configured options. + + +___ + +
+ + +
+Deadline: fix selection from multiple webservices #5015 + +Multiple different DL webservice could be configured. First they must by configured in System Settings., then they could be configured per project in `project_settings/deadline/deadline_servers`.Only single webservice could be a target of publish though. + + +___ + +
+ +### **Merged pull requests** + + +
+3dsmax: Refactored publish plugins to use proper implementation of pymxs #4988 + + +___ + +
+ + + + ## [3.15.7](https://github.com/ynput/OpenPype/tree/3.15.7) diff --git a/openpype/version.py b/openpype/version.py index 3d7f64b991..342bbfc85a 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.8-nightly.3" +__version__ = "3.15.8" diff --git a/pyproject.toml b/pyproject.toml index 190ecb9329..a72a3d66d7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.15.7" # OpenPype +version = "3.15.8" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 4647e39142da5c0a1a5cc62844a38c105c166dc9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 24 May 2023 10:22:35 +0000 Subject: [PATCH 342/360] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index a9f1f1cc02..4d7d06a2c8 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.15.8 - 3.15.8-nightly.3 - 3.15.8-nightly.2 - 3.15.8-nightly.1 @@ -134,7 +135,6 @@ body: - 3.14.2-nightly.4 - 3.14.2-nightly.3 - 3.14.2-nightly.2 - - 3.14.2-nightly.1 validations: required: true - type: dropdown From 7cfbb972a5ad41eea6e7bac6775ec8697edcb342 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 24 May 2023 12:40:28 +0100 Subject: [PATCH 343/360] Fix sequence frames validator to use correct data --- .../hosts/unreal/plugins/publish/validate_sequence_frames.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py b/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py index e6584e130f..76bb25fac3 100644 --- a/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py +++ b/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py @@ -31,8 +31,8 @@ class ValidateSequenceFrames(pyblish.api.InstancePlugin): frames = list(collection.indexes) current_range = (frames[0], frames[-1]) - required_range = (data["frameStart"], - data["frameEnd"]) + required_range = (data["clipIn"], + data["clipOut"]) if current_range != required_range: raise ValueError(f"Invalid frame range: {current_range} - " From 7e692ad5acc284540f1d29672ea86386b0f22468 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 24 May 2023 14:01:50 +0200 Subject: [PATCH 344/360] added option to nest settings templates --- openpype/settings/entities/lib.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index 1c7dc9bed0..93abc27b0e 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -323,7 +323,10 @@ class SchemasHub: filled_template = self._fill_template( schema_data, template_def ) - return filled_template + new_template_def = [] + for item in filled_template: + new_template_def.extend(self.resolve_schema_data(item)) + return new_template_def def create_schema_object(self, schema_data, *args, **kwargs): """Create entity for passed schema data. From 45e1dbc8410fb66dd7e685e77946db2952b35672 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 24 May 2023 13:14:46 +0100 Subject: [PATCH 345/360] Fix render instances collection to use correct data --- .../hosts/unreal/plugins/publish/collect_render_instances.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/unreal/plugins/publish/collect_render_instances.py b/openpype/hosts/unreal/plugins/publish/collect_render_instances.py index a352b2c3f3..dad0310dfc 100644 --- a/openpype/hosts/unreal/plugins/publish/collect_render_instances.py +++ b/openpype/hosts/unreal/plugins/publish/collect_render_instances.py @@ -103,8 +103,8 @@ class CollectRenderInstances(pyblish.api.InstancePlugin): new_instance.data["representations"] = [] repr = { - 'frameStart': s.get('frame_range')[0], - 'frameEnd': s.get('frame_range')[1], + 'frameStart': instance.data["frameStart"], + 'frameEnd': instance.data["frameEnd"], 'name': 'png', 'ext': 'png', 'files': frames, From d4212ef9918e805025fb93fdfdaf5b5fa82f2d7c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 24 May 2023 22:04:42 +0200 Subject: [PATCH 346/360] Return any timeline in case none is detected as active also adding in host test --- openpype/hosts/resolve/api/lib.py | 16 +++++++++------- .../utility_scripts/tests/testing_timeline_op.py | 13 +++++++++++++ 2 files changed, 22 insertions(+), 7 deletions(-) create mode 100644 openpype/hosts/resolve/utility_scripts/tests/testing_timeline_op.py diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index 1c33749a77..d42521200a 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -91,16 +91,16 @@ def get_current_project(): return self.project_manager.GetCurrentProject() -def get_current_timeline(any=False, new=False): +def get_current_timeline(new=False, get_any=False): """Get current timeline object. Args: - any (bool, optional): return any even new if no timeline available. - Defaults to False. new (bool, optional): return only new timeline. Defaults to False. + get_any (bool, optional): return any even new if no timeline available. + Defaults to False. Returns: - _type_: _description_ + object: resolve.Timeline """ # get current project project = get_current_project() @@ -111,12 +111,14 @@ def get_current_timeline(any=False, new=False): if timeline and not new: return timeline - # if any is True then return any timeline - if any: + # if get_any is True then return any timeline + if get_any: timeline_count = project.GetTimelineCount() if timeline_count == 0: # if there is no timeline then create a new one new = True + else: + return project.GetTimelineByIndex(1) # create new timeline if new is True if new: @@ -336,7 +338,7 @@ def get_current_timeline_items( selecting_color = selecting_color or "Chocolate" project = get_current_project() # make sure some timeline will be active with `any` argument - timeline = get_current_timeline(any=True) + timeline = get_current_timeline(get_any=True) selected_clips = [] # get all tracks count filtered by track type diff --git a/openpype/hosts/resolve/utility_scripts/tests/testing_timeline_op.py b/openpype/hosts/resolve/utility_scripts/tests/testing_timeline_op.py new file mode 100644 index 0000000000..8270496f64 --- /dev/null +++ b/openpype/hosts/resolve/utility_scripts/tests/testing_timeline_op.py @@ -0,0 +1,13 @@ +#! python3 +from openpype.pipeline import install_host +from openpype.hosts.resolve import api as bmdvr +from openpype.hosts.resolve.api.lib import get_current_project + +if __name__ == "__main__": + install_host(bmdvr) + project = get_current_project() + timeline_count = project.GetTimelineCount() + print(f"Timeline count: {timeline_count}") + timeline = project.GetTimelineByIndex(timeline_count) + print(f"Timeline name: {timeline.GetName()}") + print(timeline.GetTrackCount("video")) From 99a1be366e77db5549b294b8e37bb3089061cdd4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 24 May 2023 22:19:46 +0200 Subject: [PATCH 347/360] nuke: callback for dirmapping is on demand --- openpype/hosts/nuke/api/pipeline.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/api/pipeline.py b/openpype/hosts/nuke/api/pipeline.py index d649ffae7f..75b0f80d21 100644 --- a/openpype/hosts/nuke/api/pipeline.py +++ b/openpype/hosts/nuke/api/pipeline.py @@ -151,6 +151,7 @@ class NukeHost( def add_nuke_callbacks(): """ Adding all available nuke callbacks """ + nuke_settings = get_current_project_settings()["nuke"] workfile_settings = WorkfileSettings() # Set context settings. nuke.addOnCreate( @@ -169,7 +170,10 @@ def add_nuke_callbacks(): # # set apply all workfile settings on script load and save nuke.addOnScriptLoad(WorkfileSettings().set_context_settings) - nuke.addFilenameFilter(dirmap_file_name_filter) + if nuke_settings["nuke-dirmap"]["enabled"]: + log.info("Added Nuke's dirmaping callback ...") + # Add dirmap for file paths. + nuke.addFilenameFilter(dirmap_file_name_filter) log.info("Added Nuke callbacks ...") From 41ae41d7512ebdc3270d49904f264cdde6c44f75 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 25 May 2023 10:00:29 +0200 Subject: [PATCH 348/360] Enhancement/publisher: Remove "hit play to continue" label on continue (#5029) * Clear message label on publish so that on "continue" it does not persist the "Hit play to continue" message * Clear main label on reset since the label isn't visible anyway * Remove unnecessary comment --------- Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/tools/publisher/widgets/publish_frame.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/tools/publisher/widgets/publish_frame.py b/openpype/tools/publisher/widgets/publish_frame.py index d21130deff..d423f97047 100644 --- a/openpype/tools/publisher/widgets/publish_frame.py +++ b/openpype/tools/publisher/widgets/publish_frame.py @@ -310,7 +310,7 @@ class PublishFrame(QtWidgets.QWidget): self._set_success_property() self._set_progress_visibility(True) - self._main_label.setText("Hit publish (play button)! If you want") + self._main_label.setText("") self._message_label_top.setText("") self._reset_btn.setEnabled(True) @@ -331,6 +331,7 @@ class PublishFrame(QtWidgets.QWidget): self._set_success_property(3) self._set_progress_visibility(True) self._set_main_label("Publishing...") + self._message_label_top.setText("") self._reset_btn.setEnabled(False) self._stop_btn.setEnabled(True) From 55040e6f74116b23531fa87f8965b83ab68d1316 Mon Sep 17 00:00:00 2001 From: Thomas Fricard <51854004+friquette@users.noreply.github.com> Date: Thu, 25 May 2023 10:32:36 +0200 Subject: [PATCH 349/360] Drop-down menu to list all families in create placeholder (#4928) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * convert family text field to enum field * get families from loaders and not creators * refactor the list families part * remove discover_loader_plugins call since there is already a variable with loaders plugins --------- Co-authored-by: Thomas Fricard Co-authored-by: Clément Hector --- .../workfile/workfile_template_builder.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/openpype/pipeline/workfile/workfile_template_builder.py b/openpype/pipeline/workfile/workfile_template_builder.py index a3d7340367..896ed40f2d 100644 --- a/openpype/pipeline/workfile/workfile_template_builder.py +++ b/openpype/pipeline/workfile/workfile_template_builder.py @@ -43,6 +43,7 @@ from openpype.pipeline.load import ( get_contexts_for_repre_docs, load_with_repre_context, ) + from openpype.pipeline.create import ( discover_legacy_creator_plugins, CreateContext, @@ -1246,6 +1247,16 @@ class PlaceholderLoadMixin(object): loader_items = list(sorted(loader_items, key=lambda i: i["label"])) options = options or {} + + # Get families from all loaders excluding "*" + families = set() + for loader in loaders_by_name.values(): + families.update(loader.families) + families.discard("*") + + # Sort for readability + families = list(sorted(families)) + return [ attribute_definitions.UISeparatorDef(), attribute_definitions.UILabelDef("Main attributes"), @@ -1272,11 +1283,11 @@ class PlaceholderLoadMixin(object): " field \"inputLinks\"" ) ), - attribute_definitions.TextDef( + attribute_definitions.EnumDef( "family", label="Family", default=options.get("family"), - placeholder="model, look, ..." + items=families ), attribute_definitions.TextDef( "representation", From 124493affd3e3b34563e35c83e34c7c66b5ee99b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 25 May 2023 11:51:44 +0200 Subject: [PATCH 350/360] Publisher: UI works with instances without label (#5032) * implemented helper function to get instance label * use 'get_publish_instance_label' in some of existing plugins * use 'get_publish_instance_label' in publisher controller --- openpype/pipeline/publish/__init__.py | 2 ++ openpype/pipeline/publish/lib.py | 32 +++++++++++++++++++ openpype/plugins/publish/extract_review.py | 16 +++------- .../plugins/publish/integrate_thumbnail.py | 12 ++----- openpype/tools/publisher/control.py | 3 +- 5 files changed, 44 insertions(+), 21 deletions(-) diff --git a/openpype/pipeline/publish/__init__.py b/openpype/pipeline/publish/__init__.py index 72f3774e1a..0c57915c05 100644 --- a/openpype/pipeline/publish/__init__.py +++ b/openpype/pipeline/publish/__init__.py @@ -39,6 +39,7 @@ from .lib import ( apply_plugin_settings_automatically, get_plugin_settings, + get_publish_instance_label, ) from .abstract_expected_files import ExpectedFiles @@ -85,6 +86,7 @@ __all__ = ( "apply_plugin_settings_automatically", "get_plugin_settings", + "get_publish_instance_label", "ExpectedFiles", diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index b55f813b5e..e87b865dce 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -30,6 +30,8 @@ from .contants import ( TRANSIENT_DIR_TEMPLATE ) +_ARG_PLACEHOLDER = object() + def get_template_name_profiles( project_name, project_settings=None, logger=None @@ -866,3 +868,33 @@ def add_repre_files_for_cleanup(instance, repre): for file_name in files: expected_file = os.path.join(staging_dir, file_name) instance.context.data["cleanupFullPaths"].append(expected_file) + + +def get_publish_instance_label(instance, default=_ARG_PLACEHOLDER): + """Try to get label from pyblish instance. + + First are checked 'label' and 'name' keys in instance data. If are not set + a default value is returned. Instance object is converted to string + if default value is not specific. + + Todos: + Maybe 'subset' key could be used too. + + Args: + instance (pyblish.api.Instance): Pyblish instance. + default (Optional[Any]): Default value to return if any + + Returns: + Union[Any]: Instance label or default label. + """ + + label = ( + instance.data.get("label") + or instance.data.get("name") + ) + if label: + return label + + if default is _ARG_PLACEHOLDER: + return str(instance) + return default diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index fa58c03df1..d04893fa7e 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -23,7 +23,10 @@ from openpype.lib.transcoding import ( convert_input_paths_for_ffmpeg, get_transcode_temp_directory, ) -from openpype.pipeline.publish import KnownPublishError +from openpype.pipeline.publish import ( + KnownPublishError, + get_publish_instance_label, +) from openpype.pipeline.publish.lib import add_repre_files_for_cleanup @@ -203,17 +206,8 @@ class ExtractReview(pyblish.api.InstancePlugin): return filtered_defs - @staticmethod - def get_instance_label(instance): - return ( - getattr(instance, "label", None) - or instance.data.get("label") - or instance.data.get("name") - or str(instance) - ) - def main_process(self, instance): - instance_label = self.get_instance_label(instance) + instance_label = get_publish_instance_label(instance) self.log.debug("Processing instance \"{}\"".format(instance_label)) profile_outputs = self._get_outputs_for_instance(instance) if not profile_outputs: diff --git a/openpype/plugins/publish/integrate_thumbnail.py b/openpype/plugins/publish/integrate_thumbnail.py index f6d4f654f5..2e87d8fc86 100644 --- a/openpype/plugins/publish/integrate_thumbnail.py +++ b/openpype/plugins/publish/integrate_thumbnail.py @@ -20,6 +20,7 @@ import pyblish.api from openpype.client import get_versions from openpype.client.operations import OperationsSession, new_thumbnail_doc +from openpype.pipeline.publish import get_publish_instance_label InstanceFilterResult = collections.namedtuple( "InstanceFilterResult", @@ -133,7 +134,7 @@ class IntegrateThumbnails(pyblish.api.ContextPlugin): filtered_instances = [] for instance in context: - instance_label = self._get_instance_label(instance) + instance_label = get_publish_instance_label(instance) # Skip instances without published representations # - there is no place where to put the thumbnail published_repres = instance.data.get("published_representations") @@ -248,7 +249,7 @@ class IntegrateThumbnails(pyblish.api.ContextPlugin): for instance_item in filtered_instance_items: instance, thumbnail_path, version_id = instance_item - instance_label = self._get_instance_label(instance) + instance_label = get_publish_instance_label(instance) version_doc = version_docs_by_str_id.get(version_id) if not version_doc: self.log.warning(( @@ -339,10 +340,3 @@ class IntegrateThumbnails(pyblish.api.ContextPlugin): )) op_session.commit() - - def _get_instance_label(self, instance): - return ( - instance.data.get("label") - or instance.data.get("name") - or "N/A" - ) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 8095d00103..89c2343ef7 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -40,6 +40,7 @@ from openpype.pipeline.create.context import ( CreatorsOperationFailed, ConvertorsOperationFailed, ) +from openpype.pipeline.publish import get_publish_instance_label # Define constant for plugin orders offset PLUGIN_ORDER_OFFSET = 0.5 @@ -346,7 +347,7 @@ class PublishReportMaker: def _extract_instance_data(self, instance, exists): return { "name": instance.data.get("name"), - "label": instance.data.get("label"), + "label": get_publish_instance_label(instance), "family": instance.data["family"], "families": instance.data.get("families") or [], "exists": exists, From 48b4934ee36628f57a895ff9e8cf7f349ddcea6d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 25 May 2023 13:33:11 +0200 Subject: [PATCH 351/360] limit number of ftrack events to query at once (#5033) --- openpype/modules/ftrack/ftrack_server/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/ftrack_server/lib.py b/openpype/modules/ftrack/ftrack_server/lib.py index eb64063fab..2226c85ef9 100644 --- a/openpype/modules/ftrack/ftrack_server/lib.py +++ b/openpype/modules/ftrack/ftrack_server/lib.py @@ -196,7 +196,7 @@ class ProcessEventHub(SocketBaseEventHub): {"pype_data.is_processed": False} ).sort( [("pype_data.stored", pymongo.ASCENDING)] - ) + ).limit(100) found = False for event_data in not_processed_events: From 318237ded65c42e04a61cc38ba91886c0becf7a4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 25 May 2023 16:38:01 +0200 Subject: [PATCH 352/360] breaking get_current_timeline into more functions --- openpype/hosts/resolve/api/__init__.py | 4 ++ openpype/hosts/resolve/api/lib.py | 83 ++++++++++++++++---------- openpype/hosts/resolve/api/plugin.py | 5 +- 3 files changed, 59 insertions(+), 33 deletions(-) diff --git a/openpype/hosts/resolve/api/__init__.py b/openpype/hosts/resolve/api/__init__.py index 00a598548e..2b4546f8d6 100644 --- a/openpype/hosts/resolve/api/__init__.py +++ b/openpype/hosts/resolve/api/__init__.py @@ -24,6 +24,8 @@ from .lib import ( get_project_manager, get_current_project, get_current_timeline, + get_any_timeline, + get_new_timeline, create_bin, get_media_pool_item, create_media_pool_item, @@ -95,6 +97,8 @@ __all__ = [ "get_project_manager", "get_current_project", "get_current_timeline", + "get_any_timeline", + "get_new_timeline", "create_bin", "get_media_pool_item", "create_media_pool_item", diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index d42521200a..a44c527f13 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -15,6 +15,7 @@ log = Logger.get_logger(__name__) self = sys.modules[__name__] self.project_manager = None self.media_storage = None +self.current_project = None # OpenPype sequential rename variables self.rename_index = 0 @@ -85,47 +86,60 @@ def get_media_storage(): def get_current_project(): - # initialize project manager - get_project_manager() + """Get current project object. + """ + if not self.current_project: + self.current_project = get_project_manager().GetCurrentProject() - return self.project_manager.GetCurrentProject() + return self.current_project -def get_current_timeline(new=False, get_any=False): +def get_current_timeline(new=False): """Get current timeline object. Args: - new (bool, optional): return only new timeline. Defaults to False. - get_any (bool, optional): return any even new if no timeline available. - Defaults to False. + new (bool)[optional]: [DEPRECATED] if True it will create + new timeline if none exists + + Returns: + TODO: will need to reflect future `None` + object: resolve.Timeline + """ + project = get_current_project() + timeline = project.GetCurrentTimeline() + + # return current timeline if any + if timeline: + return timeline + + # TODO: [deprecated] and will be removed in future + if new: + return get_new_timeline() + + +def get_any_timeline(): + """Get any timeline object. + + Returns: + object | None: resolve.Timeline + """ + project = get_current_project() + timeline_count = project.GetTimelineCount() + if timeline_count > 0: + return project.GetTimelineByIndex(1) + + +def get_new_timeline(): + """Get new timeline object. Returns: object: resolve.Timeline """ - # get current project project = get_current_project() - - timeline = project.GetCurrentTimeline() - - # return current timeline only if it is not new - if timeline and not new: - return timeline - - # if get_any is True then return any timeline - if get_any: - timeline_count = project.GetTimelineCount() - if timeline_count == 0: - # if there is no timeline then create a new one - new = True - else: - return project.GetTimelineByIndex(1) - - # create new timeline if new is True - if new: - media_pool = project.GetMediaPool() - new_timeline = media_pool.CreateEmptyTimeline(self.pype_timeline_name) - project.SetCurrentTimeline(new_timeline) - return new_timeline + media_pool = project.GetMediaPool() + new_timeline = media_pool.CreateEmptyTimeline(self.pype_timeline_name) + project.SetCurrentTimeline(new_timeline) + return new_timeline def create_bin(name: str, root: object = None) -> object: @@ -337,8 +351,13 @@ def get_current_timeline_items( track_type = track_type or "video" selecting_color = selecting_color or "Chocolate" project = get_current_project() - # make sure some timeline will be active with `any` argument - timeline = get_current_timeline(get_any=True) + + # get timeline anyhow + timeline = ( + get_current_timeline() or + get_any_timeline() or + get_new_timeline() + ) selected_clips = [] # get all tracks count filtered by track type diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py index 609cff60f7..e5846c2fc2 100644 --- a/openpype/hosts/resolve/api/plugin.py +++ b/openpype/hosts/resolve/api/plugin.py @@ -327,7 +327,10 @@ class ClipLoader: self.active_timeline = options["timeline"] else: # create new sequence - self.active_timeline = lib.get_current_timeline(new=True) + self.active_timeline = ( + lib.get_current_timeline() or + lib.get_new_timeline() + ) else: self.active_timeline = lib.get_current_timeline() From c61dd1b24775c6438e3ba5844f5159ef1349b66a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 25 May 2023 17:18:23 +0200 Subject: [PATCH 353/360] utility scripts cosmetics only copy test and develop scripts if developer --- .../{__OpenPype__Menu__.py => OpenPype__Menu.py} | 0 openpype/hosts/resolve/utility_scripts/README.markdown | 1 - .../resolve/utility_scripts/{ => develop}/OTIO_export.py | 0 .../resolve/utility_scripts/{ => develop}/OTIO_import.py | 0 .../{ => develop}/OpenPype_sync_util_scripts.py | 0 openpype/hosts/resolve/utils.py | 9 ++++++++- 6 files changed, 8 insertions(+), 2 deletions(-) rename openpype/hosts/resolve/utility_scripts/{__OpenPype__Menu__.py => OpenPype__Menu.py} (100%) delete mode 100644 openpype/hosts/resolve/utility_scripts/README.markdown rename openpype/hosts/resolve/utility_scripts/{ => develop}/OTIO_export.py (100%) rename openpype/hosts/resolve/utility_scripts/{ => develop}/OTIO_import.py (100%) rename openpype/hosts/resolve/utility_scripts/{ => develop}/OpenPype_sync_util_scripts.py (100%) diff --git a/openpype/hosts/resolve/utility_scripts/__OpenPype__Menu__.py b/openpype/hosts/resolve/utility_scripts/OpenPype__Menu.py similarity index 100% rename from openpype/hosts/resolve/utility_scripts/__OpenPype__Menu__.py rename to openpype/hosts/resolve/utility_scripts/OpenPype__Menu.py diff --git a/openpype/hosts/resolve/utility_scripts/README.markdown b/openpype/hosts/resolve/utility_scripts/README.markdown deleted file mode 100644 index 8b13789179..0000000000 --- a/openpype/hosts/resolve/utility_scripts/README.markdown +++ /dev/null @@ -1 +0,0 @@ - diff --git a/openpype/hosts/resolve/utility_scripts/OTIO_export.py b/openpype/hosts/resolve/utility_scripts/develop/OTIO_export.py similarity index 100% rename from openpype/hosts/resolve/utility_scripts/OTIO_export.py rename to openpype/hosts/resolve/utility_scripts/develop/OTIO_export.py diff --git a/openpype/hosts/resolve/utility_scripts/OTIO_import.py b/openpype/hosts/resolve/utility_scripts/develop/OTIO_import.py similarity index 100% rename from openpype/hosts/resolve/utility_scripts/OTIO_import.py rename to openpype/hosts/resolve/utility_scripts/develop/OTIO_import.py diff --git a/openpype/hosts/resolve/utility_scripts/OpenPype_sync_util_scripts.py b/openpype/hosts/resolve/utility_scripts/develop/OpenPype_sync_util_scripts.py similarity index 100% rename from openpype/hosts/resolve/utility_scripts/OpenPype_sync_util_scripts.py rename to openpype/hosts/resolve/utility_scripts/develop/OpenPype_sync_util_scripts.py diff --git a/openpype/hosts/resolve/utils.py b/openpype/hosts/resolve/utils.py index 8e5dd9a188..9a161f4865 100644 --- a/openpype/hosts/resolve/utils.py +++ b/openpype/hosts/resolve/utils.py @@ -1,6 +1,6 @@ import os import shutil -from openpype.lib import Logger +from openpype.lib import Logger, is_running_from_build RESOLVE_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -41,6 +41,13 @@ def setup(env): # copy scripts into Resolve's utility scripts dir for directory, scripts in scripts.items(): for script in scripts: + if ( + is_running_from_build() and + script in ["tests", "develop"] + ): + # only copy those if started from build + continue + src = os.path.join(directory, script) dst = os.path.join(util_scripts_dir, script) log.info("Copying `{}` to `{}`...".format(src, dst)) From 9f7f22961b6b30c256c7a60d8f16ea18058e1a62 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 26 May 2023 10:43:54 +0100 Subject: [PATCH 354/360] Improved implementation of UMap to use UAsset base code --- .../unreal/plugins/create/create_uasset.py | 24 ++- .../unreal/plugins/create/create_umap.py | 46 ------ .../hosts/unreal/plugins/load/load_uasset.py | 28 ++-- .../hosts/unreal/plugins/load/load_umap.py | 140 ------------------ .../unreal/plugins/publish/extract_uasset.py | 15 +- .../unreal/plugins/publish/extract_umap.py | 48 ------ 6 files changed, 49 insertions(+), 252 deletions(-) delete mode 100644 openpype/hosts/unreal/plugins/create/create_umap.py delete mode 100644 openpype/hosts/unreal/plugins/load/load_umap.py delete mode 100644 openpype/hosts/unreal/plugins/publish/extract_umap.py diff --git a/openpype/hosts/unreal/plugins/create/create_uasset.py b/openpype/hosts/unreal/plugins/create/create_uasset.py index c78518e86b..f70ecc55b3 100644 --- a/openpype/hosts/unreal/plugins/create/create_uasset.py +++ b/openpype/hosts/unreal/plugins/create/create_uasset.py @@ -17,6 +17,8 @@ class CreateUAsset(UnrealAssetCreator): family = "uasset" icon = "cube" + extension = ".uasset" + def create(self, subset_name, instance_data, pre_create_data): if pre_create_data.get("use_selection"): ar = unreal.AssetRegistryHelpers.get_asset_registry() @@ -37,10 +39,28 @@ class CreateUAsset(UnrealAssetCreator): f"{Path(obj).name} is not on the disk. Likely it needs to" "be saved first.") - if Path(sys_path).suffix != ".uasset": - raise CreatorError(f"{Path(sys_path).name} is not a UAsset.") + if Path(sys_path).suffix != self.extension: + raise CreatorError( + f"{Path(sys_path).name} is not a {self.label}.") super(CreateUAsset, self).create( subset_name, instance_data, pre_create_data) + + +class CreateUMap(CreateUAsset): + """Create Level.""" + + identifier = "io.ayon.creators.unreal.umap" + label = "Level" + family = "uasset" + extension = ".umap" + + def create(self, subset_name, instance_data, pre_create_data): + instance_data["families"] = ["umap"] + + super(CreateUMap, self).create( + subset_name, + instance_data, + pre_create_data) diff --git a/openpype/hosts/unreal/plugins/create/create_umap.py b/openpype/hosts/unreal/plugins/create/create_umap.py deleted file mode 100644 index 34aa8cdc00..0000000000 --- a/openpype/hosts/unreal/plugins/create/create_umap.py +++ /dev/null @@ -1,46 +0,0 @@ -# -*- coding: utf-8 -*- -from pathlib import Path - -import unreal - -from openpype.pipeline import CreatorError -from openpype.hosts.unreal.api.plugin import ( - UnrealAssetCreator, -) - - -class CreateUMap(UnrealAssetCreator): - """Create Level.""" - - identifier = "io.ayon.creators.unreal.umap" - label = "Level" - family = "uasset" - icon = "cube" - - def create(self, subset_name, instance_data, pre_create_data): - if pre_create_data.get("use_selection"): - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() - selection = [a.get_path_name() for a in sel_objects] - - if len(selection) != 1: - raise CreatorError("Please select only one object.") - - obj = selection[0] - - asset = ar.get_asset_by_object_path(obj).get_asset() - sys_path = unreal.SystemLibrary.get_system_path(asset) - - if not sys_path: - raise CreatorError( - f"{Path(obj).name} is not on the disk. Likely it needs to" - "be saved first.") - - if Path(sys_path).suffix != ".umap": - raise CreatorError(f"{Path(sys_path).name} is not a Level.") - - super(CreateUMap, self).create( - subset_name, - instance_data, - pre_create_data) diff --git a/openpype/hosts/unreal/plugins/load/load_uasset.py b/openpype/hosts/unreal/plugins/load/load_uasset.py index 7606bc14e4..44c87593e9 100644 --- a/openpype/hosts/unreal/plugins/load/load_uasset.py +++ b/openpype/hosts/unreal/plugins/load/load_uasset.py @@ -21,6 +21,8 @@ class UAssetLoader(plugin.Loader): icon = "cube" color = "orange" + extension = "uasset" + def load(self, context, name, namespace, options): """Load and containerise representation into Content Browser. @@ -42,11 +44,7 @@ class UAssetLoader(plugin.Loader): root = "/Game/Ayon/Assets" 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}/{asset}/{name}", suffix="" @@ -61,7 +59,7 @@ class UAssetLoader(plugin.Loader): Path(unreal.Paths.project_content_dir()).as_posix(), 1) - shutil.copy(self.fname, f"{destination_path}/{name}.uasset") + shutil.copy(self.fname, f"{destination_path}/{name}.{self.extension}") # Create Asset Container unreal_pipeline.create_container( @@ -107,15 +105,15 @@ class UAssetLoader(plugin.Loader): for asset in asset_content: obj = ar.get_asset_by_object_path(asset).get_asset() - if not obj.get_class().get_name() == 'AyonAssetContainer': + if obj.get_class().get_name() != 'AyonAssetContainer': unreal.EditorAssetLibrary.delete_asset(asset) update_filepath = get_representation_path(representation) - shutil.copy(update_filepath, f"{destination_path}/{name}.uasset") + shutil.copy( + update_filepath, f"{destination_path}/{name}.{self.extension}") - container_path = "{}/{}".format(container["namespace"], - container["objectName"]) + container_path = f'{container["namespace"]}/{container["objectName"]}' # update metadata unreal_pipeline.imprint( container_path, @@ -143,3 +141,13 @@ class UAssetLoader(plugin.Loader): if len(asset_content) == 0: unreal.EditorAssetLibrary.delete_directory(parent_path) + + +class UMapLoader(UAssetLoader): + """Load Level.""" + + families = ["uasset"] + label = "Load Level" + representations = ["umap"] + + extension = "umap" diff --git a/openpype/hosts/unreal/plugins/load/load_umap.py b/openpype/hosts/unreal/plugins/load/load_umap.py deleted file mode 100644 index f467fe6b3b..0000000000 --- a/openpype/hosts/unreal/plugins/load/load_umap.py +++ /dev/null @@ -1,140 +0,0 @@ -# -*- coding: utf-8 -*- -"""Load Level.""" -from pathlib import Path -import shutil - -from openpype.pipeline import ( - get_representation_path, - AYON_CONTAINER_ID -) -from openpype.hosts.unreal.api import plugin -from openpype.hosts.unreal.api import pipeline as unreal_pipeline -import unreal # noqa - - -class UMapLoader(plugin.Loader): - """Load Level.""" - - families = ["uasset"] - label = "Load Level" - representations = ["umap"] - icon = "cube" - color = "orange" - - def load(self, context, name, namespace, options): - """Load and containerise representation into Content Browser. - - 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. - options (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 Ayon container - root = "/Game/Ayon/Assets" - asset = context.get('asset').get('name') - suffix = "_CON" - 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}/{asset}/{name}", suffix="" - ) - - container_name += suffix - - unreal.EditorAssetLibrary.make_directory(asset_dir) - - destination_path = asset_dir.replace( - "/Game", - Path(unreal.Paths.project_content_dir()).as_posix(), - 1) - - shutil.copy(self.fname, f"{destination_path}/{name}.uasset") - - # Create Asset Container - unreal_pipeline.create_container( - container=container_name, path=asset_dir) - - data = { - "schema": "ayon:container-2.0", - "id": AYON_CONTAINER_ID, - "asset": asset, - "namespace": asset_dir, - "container_name": container_name, - "asset_name": asset_name, - "loader": str(self.__class__.__name__), - "representation": context["representation"]["_id"], - "parent": context["representation"]["parent"], - "family": context["representation"]["context"]["family"] - } - unreal_pipeline.imprint(f"{asset_dir}/{container_name}", data) - - asset_content = unreal.EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=True - ) - - for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) - - return asset_content - - def update(self, container, representation): - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - asset_dir = container["namespace"] - name = representation["context"]["subset"] - - destination_path = asset_dir.replace( - "/Game", - Path(unreal.Paths.project_content_dir()).as_posix(), - 1) - - asset_content = unreal.EditorAssetLibrary.list_assets( - asset_dir, recursive=False, include_folder=True - ) - - for asset in asset_content: - obj = ar.get_asset_by_object_path(asset).get_asset() - if obj.get_class().get_name() != 'AyonAssetContainer': - unreal.EditorAssetLibrary.delete_asset(asset) - - update_filepath = get_representation_path(representation) - - shutil.copy(update_filepath, f"{destination_path}/{name}.umap") - - container_path = f'{container["namespace"]}/{container["objectName"]}' - # update metadata - unreal_pipeline.imprint( - container_path, - { - "representation": str(representation["_id"]), - "parent": str(representation["parent"]) - }) - - asset_content = unreal.EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=True - ) - - for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) - - def remove(self, container): - path = container["namespace"] - parent_path = Path(path).parent.as_posix() - - unreal.EditorAssetLibrary.delete_directory(path) - - asset_content = unreal.EditorAssetLibrary.list_assets( - parent_path, recursive=False - ) - - if len(asset_content) == 0: - unreal.EditorAssetLibrary.delete_directory(parent_path) diff --git a/openpype/hosts/unreal/plugins/publish/extract_uasset.py b/openpype/hosts/unreal/plugins/publish/extract_uasset.py index f719df2a82..48b62faa97 100644 --- a/openpype/hosts/unreal/plugins/publish/extract_uasset.py +++ b/openpype/hosts/unreal/plugins/publish/extract_uasset.py @@ -11,16 +11,17 @@ class ExtractUAsset(publish.Extractor): label = "Extract UAsset" hosts = ["unreal"] - families = ["uasset"] + families = ["uasset", "umap"] optional = True def process(self, instance): + extension = ( + "umap" if "umap" in instance.data.get("families") else "uasset") ar = unreal.AssetRegistryHelpers.get_asset_registry() self.log.info("Performing extraction..") - staging_dir = self.staging_dir(instance) - filename = "{}.uasset".format(instance.name) + filename = f"{instance.name}.{extension}" members = instance.data.get("members", []) @@ -36,13 +37,15 @@ class ExtractUAsset(publish.Extractor): shutil.copy(sys_path, staging_dir) + self.log.info(f"instance.data: {instance.data}") + if "representations" not in instance.data: instance.data["representations"] = [] representation = { - 'name': 'uasset', - 'ext': 'uasset', - 'files': filename, + "name": extension, + "ext": extension, + "files": filename, "stagingDir": staging_dir, } instance.data["representations"].append(representation) diff --git a/openpype/hosts/unreal/plugins/publish/extract_umap.py b/openpype/hosts/unreal/plugins/publish/extract_umap.py deleted file mode 100644 index 3812834430..0000000000 --- a/openpype/hosts/unreal/plugins/publish/extract_umap.py +++ /dev/null @@ -1,48 +0,0 @@ -from pathlib import Path -import shutil - -import unreal - -from openpype.pipeline import publish - - -class ExtractUMap(publish.Extractor): - """Extract a UMap.""" - - label = "Extract Level" - hosts = ["unreal"] - families = ["uasset"] - optional = True - - def process(self, instance): - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - self.log.info("Performing extraction..") - - staging_dir = self.staging_dir(instance) - filename = f"{instance.name}.umap" - - members = instance.data.get("members", []) - - if not members: - raise RuntimeError("No members found in instance.") - - # UAsset publishing supports only one member - obj = members[0] - - asset = ar.get_asset_by_object_path(obj).get_asset() - sys_path = unreal.SystemLibrary.get_system_path(asset) - filename = Path(sys_path).name - - shutil.copy(sys_path, staging_dir) - - if "representations" not in instance.data: - instance.data["representations"] = [] - - representation = { - 'name': 'umap', - 'ext': 'umap', - 'files': filename, - "stagingDir": staging_dir, - } - instance.data["representations"].append(representation) From 5f98c278361d9354565261174e51b43b5acaffa9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 26 May 2023 12:00:09 +0200 Subject: [PATCH 355/360] apply settings on publish plugins can expect only project settings (#5037) --- openpype/pipeline/publish/lib.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index e87b865dce..f228709b3b 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -12,7 +12,8 @@ import pyblish.api from openpype.lib import ( Logger, import_filepath, - filter_profiles + filter_profiles, + is_func_signature_supported, ) from openpype.settings import ( get_project_settings, @@ -498,12 +499,26 @@ def filter_pyblish_plugins(plugins): # iterate over plugins for plugin in plugins[:]: # Apply settings to plugins - if hasattr(plugin, "apply_settings"): + + apply_settings_func = getattr(plugin, "apply_settings", None) + if apply_settings_func is not None: # Use classmethod 'apply_settings' # - can be used to target settings from custom settings place # - skip default behavior when successful try: - plugin.apply_settings(project_settings, system_settings) + # Support to pass only project settings + # - make sure that both settings are passed, when can be + # - that covers cases when *args are in method parameters + both_supported = is_func_signature_supported( + apply_settings_func, project_settings, system_settings + ) + project_supported = is_func_signature_supported( + apply_settings_func, project_settings + ) + if not both_supported and project_supported: + plugin.apply_settings(project_settings) + else: + plugin.apply_settings(project_settings, system_settings) except Exception: log.warning( From 905c3dbd249bce6c7d229e10466b3035aa2a6d40 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 26 May 2023 12:01:36 +0100 Subject: [PATCH 356/360] Fix problem when trying to load the same level multiple times --- .../hosts/unreal/plugins/load/load_uasset.py | 35 ++++++++++++------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_uasset.py b/openpype/hosts/unreal/plugins/load/load_uasset.py index 44c87593e9..30f63abe39 100644 --- a/openpype/hosts/unreal/plugins/load/load_uasset.py +++ b/openpype/hosts/unreal/plugins/load/load_uasset.py @@ -50,16 +50,23 @@ class UAssetLoader(plugin.Loader): f"{root}/{asset}/{name}", suffix="" ) - container_name += suffix + unique_number = 1 + while unreal.EditorAssetLibrary.does_directory_exist( + f"{asset_dir}_{unique_number:02}" + ): + unique_number += 1 + + asset_dir = f"{asset_dir}_{unique_number:02}" + container_name = f"{container_name}_{unique_number:02}{suffix}" unreal.EditorAssetLibrary.make_directory(asset_dir) destination_path = asset_dir.replace( - "/Game", - Path(unreal.Paths.project_content_dir()).as_posix(), - 1) + "/Game", Path(unreal.Paths.project_content_dir()).as_posix(), 1) - shutil.copy(self.fname, f"{destination_path}/{name}.{self.extension}") + shutil.copy( + self.fname, + f"{destination_path}/{name}_{unique_number:02}.{self.extension}") # Create Asset Container unreal_pipeline.create_container( @@ -75,7 +82,7 @@ class UAssetLoader(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"], } unreal_pipeline.imprint(f"{asset_dir}/{container_name}", data) @@ -94,10 +101,10 @@ class UAssetLoader(plugin.Loader): asset_dir = container["namespace"] name = representation["context"]["subset"] + unique_number = container["container_name"].split("_")[-2] + destination_path = asset_dir.replace( - "/Game", - Path(unreal.Paths.project_content_dir()).as_posix(), - 1) + "/Game", Path(unreal.Paths.project_content_dir()).as_posix(), 1) asset_content = unreal.EditorAssetLibrary.list_assets( asset_dir, recursive=False, include_folder=True @@ -105,13 +112,14 @@ class UAssetLoader(plugin.Loader): for asset in asset_content: obj = ar.get_asset_by_object_path(asset).get_asset() - if obj.get_class().get_name() != 'AyonAssetContainer': + if obj.get_class().get_name() != "AyonAssetContainer": unreal.EditorAssetLibrary.delete_asset(asset) update_filepath = get_representation_path(representation) shutil.copy( - update_filepath, f"{destination_path}/{name}.{self.extension}") + update_filepath, + f"{destination_path}/{name}_{unique_number}.{self.extension}") container_path = f'{container["namespace"]}/{container["objectName"]}' # update metadata @@ -119,8 +127,9 @@ class UAssetLoader(plugin.Loader): container_path, { "representation": str(representation["_id"]), - "parent": str(representation["parent"]) - }) + "parent": str(representation["parent"]), + } + ) asset_content = unreal.EditorAssetLibrary.list_assets( asset_dir, recursive=True, include_folder=True From 6843ae85321ae617dfea564086c5d58fa34f7daf Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 26 May 2023 14:44:47 +0200 Subject: [PATCH 357/360] General: Small code cleanups (#5034) * make sure the message type is set and unset correctly * Update dummy data in readme * remove debug message from main thread callbacks * removed unused import * cleanup code in muster addon * simplified 'get_publish_instance_label' function * even better json file handling Co-authored-by: Roy Nieterau --------- Co-authored-by: Roy Nieterau --- openpype/modules/ftrack/tray/login_dialog.py | 2 -- openpype/modules/muster/muster.py | 14 +++++-------- openpype/pipeline/publish/lib.py | 21 ++++++-------------- openpype/tools/utils/lib.py | 1 - openpype/tools/utils/overlay_messages.py | 3 +-- tests/README.md | 18 ++++++++--------- 6 files changed, 20 insertions(+), 39 deletions(-) diff --git a/openpype/modules/ftrack/tray/login_dialog.py b/openpype/modules/ftrack/tray/login_dialog.py index f374a71178..a8abdaf191 100644 --- a/openpype/modules/ftrack/tray/login_dialog.py +++ b/openpype/modules/ftrack/tray/login_dialog.py @@ -1,5 +1,3 @@ -import os - import requests from qtpy import QtCore, QtGui, QtWidgets diff --git a/openpype/modules/muster/muster.py b/openpype/modules/muster/muster.py index 77b9214a5a..0cdb1230c8 100644 --- a/openpype/modules/muster/muster.py +++ b/openpype/modules/muster/muster.py @@ -1,7 +1,9 @@ import os import json + import appdirs import requests + from openpype.modules import OpenPypeModule, ITrayModule @@ -110,16 +112,10 @@ class MusterModule(OpenPypeModule, ITrayModule): self.save_credentials(token) def save_credentials(self, token): - """ - Save credentials to JSON file - """ - data = { - 'token': token - } + """Save credentials to JSON file.""" - file = open(self.cred_path, 'w') - file.write(json.dumps(data)) - file.close() + with open(self.cred_path, "w") as f: + json.dump({'token': token}, f) def show_login(self): """ diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index f228709b3b..471be5ddb8 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -31,8 +31,6 @@ from .contants import ( TRANSIENT_DIR_TEMPLATE ) -_ARG_PLACEHOLDER = object() - def get_template_name_profiles( project_name, project_settings=None, logger=None @@ -885,31 +883,24 @@ def add_repre_files_for_cleanup(instance, repre): instance.context.data["cleanupFullPaths"].append(expected_file) -def get_publish_instance_label(instance, default=_ARG_PLACEHOLDER): +def get_publish_instance_label(instance): """Try to get label from pyblish instance. - First are checked 'label' and 'name' keys in instance data. If are not set - a default value is returned. Instance object is converted to string - if default value is not specific. + First are used values in instance data under 'label' and 'name' keys. Then + is used string conversion of instance object -> 'instance._name'. Todos: Maybe 'subset' key could be used too. Args: instance (pyblish.api.Instance): Pyblish instance. - default (Optional[Any]): Default value to return if any Returns: - Union[Any]: Instance label or default label. + str: Instance label. """ - label = ( + return ( instance.data.get("label") or instance.data.get("name") + or str(instance) ) - if label: - return label - - if default is _ARG_PLACEHOLDER: - return str(instance) - return default diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py index 950c782727..58ece7c68f 100644 --- a/openpype/tools/utils/lib.py +++ b/openpype/tools/utils/lib.py @@ -872,7 +872,6 @@ class WrappedCallbackItem: self.log.warning("- item is already processed") return - self.log.debug("Running callback: {}".format(str(self._callback))) try: result = self._callback(*self._args, **self._kwargs) self._result = result diff --git a/openpype/tools/utils/overlay_messages.py b/openpype/tools/utils/overlay_messages.py index 180d7eae97..4da266bcf7 100644 --- a/openpype/tools/utils/overlay_messages.py +++ b/openpype/tools/utils/overlay_messages.py @@ -127,8 +127,7 @@ class OverlayMessageWidget(QtWidgets.QFrame): if timeout: self._timeout_timer.setInterval(timeout) - if message_type: - set_style_property(self, "type", message_type) + set_style_property(self, "type", message_type) self._timeout_timer.start() diff --git a/tests/README.md b/tests/README.md index d36b6534f8..20847b2449 100644 --- a/tests/README.md +++ b/tests/README.md @@ -15,16 +15,16 @@ Structure: - openpype/modules/MODULE_NAME - structure follow directory structure in code base - fixture - sample data `(MongoDB dumps, test files etc.)` - `tests.py` - single or more pytest files for MODULE_NAME -- unit - quick unit test - - MODULE_NAME +- unit - quick unit test + - MODULE_NAME - fixture - `tests.py` - + How to run: ---------- - use Openpype command 'runtests' from command line (`.venv` in ${OPENPYPE_ROOT} must be activated to use configured Python!) -- `python ${OPENPYPE_ROOT}/start.py runtests` - + By default, this command will run all tests in ${OPENPYPE_ROOT}/tests. Specific location could be provided to this command as an argument, either as absolute path, or relative path to ${OPENPYPE_ROOT}. @@ -41,17 +41,15 @@ In some cases your tests might be so localized, that you don't care about all en In that case you might add this dummy configuration BEFORE any imports in your test file ``` import os -os.environ["AVALON_MONGO"] = "mongodb://localhost:27017" +os.environ["OPENPYPE_DEBUG"] = "1" os.environ["OPENPYPE_MONGO"] = "mongodb://localhost:27017" -os.environ["AVALON_DB"] = "avalon" os.environ["OPENPYPE_DATABASE_NAME"] = "openpype" -os.environ["AVALON_TIMEOUT"] = '3000' -os.environ["OPENPYPE_DEBUG"] = "3" -os.environ["AVALON_CONFIG"] = "pype" +os.environ["AVALON_DB"] = "avalon" +os.environ["AVALON_TIMEOUT"] = "3000" os.environ["AVALON_ASSET"] = "Asset" os.environ["AVALON_PROJECT"] = "test_project" ``` (AVALON_ASSET and AVALON_PROJECT values should exist in your environment) This might be enough to run your test file separately. Do not commit this skeleton though. -Use only when you know what you are doing! \ No newline at end of file +Use only when you know what you are doing! From eb9d8942460f3640c9aeabd63e8fdd45d2e2e955 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Sat, 27 May 2023 03:25:05 +0000 Subject: [PATCH 358/360] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index 342bbfc85a..c24388b2ff 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.8" +__version__ = "3.15.9-nightly.1" From f8cb017e90490b80fb6f6470db685090a23e7211 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 27 May 2023 03:25:45 +0000 Subject: [PATCH 359/360] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 4d7d06a2c8..54a4ee6ac0 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.15.9-nightly.1 - 3.15.8 - 3.15.8-nightly.3 - 3.15.8-nightly.2 @@ -134,7 +135,6 @@ body: - 3.14.2-nightly.5 - 3.14.2-nightly.4 - 3.14.2-nightly.3 - - 3.14.2-nightly.2 validations: required: true - type: dropdown From 66070377104142edce133201389e896774d1f3f9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 29 May 2023 11:25:14 +0200 Subject: [PATCH 360/360] Publisher: Call explicitly prepared tab methods (#5044) * call explicitly prepared tab methods * add an overlay message --- openpype/tools/publisher/window.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index fc90e66f21..6ab444109e 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -676,7 +676,15 @@ class PublisherWindow(QtWidgets.QDialog): self._tabs_widget.set_current_tab(identifier) def set_current_tab(self, tab): - self._set_current_tab(tab) + if tab == "create": + self._go_to_create_tab() + elif tab == "publish": + self._go_to_publish_tab() + elif tab == "report": + self._go_to_report_tab() + elif tab == "details": + self._go_to_details_tab() + if not self._window_is_visible: self.set_tab_on_reset(tab) @@ -686,6 +694,12 @@ class PublisherWindow(QtWidgets.QDialog): def _go_to_create_tab(self): if self._create_tab.isEnabled(): self._set_current_tab("create") + return + + self._overlay_object.add_message( + "Can't switch to Create tab because publishing is paused.", + message_type="info" + ) def _go_to_publish_tab(self): self._set_current_tab("publish")