From 9fa3767b6e1b527ac13db6cf04836b52ccccc362 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 10 Jul 2024 00:22:21 +0300 Subject: [PATCH 01/11] Add `Templated Workfile Build` settings --- server/settings/main.py | 12 +++++++- server/settings/templated_workfile_build.py | 34 +++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 server/settings/templated_workfile_build.py diff --git a/server/settings/main.py b/server/settings/main.py index 3acab0ce74..9444519867 100644 --- a/server/settings/main.py +++ b/server/settings/main.py @@ -16,6 +16,9 @@ PublishPluginsModel, DEFAULT_HOUDINI_PUBLISH_SETTINGS, ) +from .templated_workfile_build import ( + TemplatedWorkfileBuildModel +) class HoudiniSettings(BaseSettingsModel): @@ -39,6 +42,10 @@ class HoudiniSettings(BaseSettingsModel): default_factory=PublishPluginsModel, title="Publish Plugins", ) + templated_workfile_build: TemplatedWorkfileBuildModel = SettingsField( + title="Templated Workfile Build", + default_factory=TemplatedWorkfileBuildModel + ) DEFAULT_VALUES = { @@ -46,5 +53,8 @@ class HoudiniSettings(BaseSettingsModel): "imageio": DEFAULT_IMAGEIO_SETTINGS, "shelves": [], "create": DEFAULT_HOUDINI_CREATE_SETTINGS, - "publish": DEFAULT_HOUDINI_PUBLISH_SETTINGS + "publish": DEFAULT_HOUDINI_PUBLISH_SETTINGS, + "templated_workfile_build": { + "profiles": [] + } } diff --git a/server/settings/templated_workfile_build.py b/server/settings/templated_workfile_build.py new file mode 100644 index 0000000000..12ebedf570 --- /dev/null +++ b/server/settings/templated_workfile_build.py @@ -0,0 +1,34 @@ +from ayon_server.settings import ( + BaseSettingsModel, + SettingsField, + task_types_enum, +) + + +class TemplatedWorkfileProfileModel(BaseSettingsModel): + task_types: list[str] = SettingsField( + default_factory=list, + title="Task types", + enum_resolver=task_types_enum + ) + task_names: list[str] = SettingsField( + default_factory=list, + title="Task names" + ) + path: str = SettingsField( + title="Path to template" + ) + keep_placeholder: bool = SettingsField( + False, + title="Keep placeholders") + create_first_version: bool = SettingsField( + True, + title="Create first version" + ) + + +class TemplatedWorkfileBuildModel(BaseSettingsModel): + """Settings for templated workfile builder.""" + profiles: list[TemplatedWorkfileProfileModel] = SettingsField( + default_factory=list + ) From 0b0c80b62427f0b3da224df82d704cce352ce265 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 10 Jul 2024 00:25:20 +0300 Subject: [PATCH 02/11] Implement `workfile_template_builder.py` --- .../api/workfile_template_builder.py | 287 ++++++++++++++++++ 1 file changed, 287 insertions(+) create mode 100644 client/ayon_houdini/api/workfile_template_builder.py diff --git a/client/ayon_houdini/api/workfile_template_builder.py b/client/ayon_houdini/api/workfile_template_builder.py new file mode 100644 index 0000000000..7b99260033 --- /dev/null +++ b/client/ayon_houdini/api/workfile_template_builder.py @@ -0,0 +1,287 @@ +import os + +import hou + +from ayon_core.lib import ( + StringTemplate, + filter_profiles, +) +from ayon_core.pipeline import registered_host, Anatomy +from ayon_core.pipeline.workfile.workfile_template_builder import ( + AbstractTemplateBuilder, + PlaceholderPlugin, + TemplateProfileNotFound, + TemplateLoadFailed, + TemplateNotFound +) +from ayon_core.tools.workfile_template_build import ( + WorkfileBuildPlaceholderDialog, +) +from ayon_core.tools.utils import show_message_dialog + +from .lib import ( + imprint, + lsattr, + get_main_window +) +from .plugin import HoudiniCreator + + +class HoudiniTemplateBuilder(AbstractTemplateBuilder): + """Concrete implementation of AbstractTemplateBuilder for Houdini""" + + def get_template_preset(self): + """Unified way how template preset is received using settings. + + Method is dependent on '_get_build_profiles' which should return filter + profiles to resolve path to a template. Default implementation looks + into host settings: + - 'project_settings/{host name}/templated_workfile_build/profiles' + + Returns: + dict: Dictionary with `path`, `keep_placeholder` and + `create_first_version` settings from the template preset + for current context. + + Raises: + TemplateProfileNotFound: When profiles are not filled. + TemplateLoadFailed: Profile was found but path is not set. + TemplateNotFound: Path was set but file does not exist. + """ + + host_name = self.host_name + project_name = self.project_name + task_name = self.current_task_name + task_type = self.current_task_type + + build_profiles = self._get_build_profiles() + profile = filter_profiles( + build_profiles, + { + "task_types": task_type, + "task_names": task_name + } + ) + + if not profile: + raise TemplateProfileNotFound(( + "No matching profile found for task '{}' of type '{}' " + "with host '{}'" + ).format(task_name, task_type, host_name)) + + path = profile["path"] + + # switch to remove placeholders after they are used + keep_placeholder = profile.get("keep_placeholder") + create_first_version = profile.get("create_first_version") + + # backward compatibility, since default is True + if keep_placeholder is None: + keep_placeholder = True + + if not path: + raise TemplateLoadFailed(( + "Template path is not set.\n" + "Path need to be set in {}\\Template Workfile Build " + "Settings\\Profiles" + ).format(host_name.title())) + + # Try fill path with environments and anatomy roots + anatomy = Anatomy(project_name) + fill_data = { + key: value + for key, value in os.environ.items() + } + + fill_data["root"] = anatomy.roots + fill_data["project"] = { + "name": project_name, + "code": anatomy.project_code, + } + + result = StringTemplate.format_template(path, fill_data) + if result.solved: + path = result.normalized() + + # I copied the whole thing because I wanted to add some + # Houdini specific code here + path = hou.text.expandString(path) + + if path and os.path.exists(path): + self.log.info("Found template at: '{}'".format(path)) + return { + "path": path, + "keep_placeholder": keep_placeholder, + "create_first_version": create_first_version + } + + solved_path = None + while True: + try: + solved_path = anatomy.path_remapper(path) + except KeyError as missing_key: + raise KeyError( + "Could not solve key '{}' in template path '{}'".format( + missing_key, path)) + + if solved_path is None: + solved_path = path + if solved_path == path: + break + path = solved_path + + solved_path = os.path.normpath(solved_path) + if not os.path.exists(solved_path): + raise TemplateNotFound( + "Template found in AYON settings for task '{}' with host " + "'{}' does not exists. (Not found : {})".format( + task_name, host_name, solved_path)) + + self.log.info("Found template at: '{}'".format(solved_path)) + + return { + "path": solved_path, + "keep_placeholder": keep_placeholder, + "create_first_version": create_first_version + } + + def import_template(self, path): + """Import template into current scene. + Block if a template is already loaded. + + Args: + path (str): A path to current template (usually given by + get_template_preset implementation) + + Returns: + bool: Whether the template was successfully imported or not + """ + + # TODO Check if template is already imported + + # Load template workfile + self.host.open_workfile(path) + + return True + + +class HoudiniPlaceholderPlugin(PlaceholderPlugin): + """Base Placeholder Plugin for Houdini with one unified cache. + + Inherited classes must still implement `populate_placeholder` + """ + + def get_placeholder_node_name(self, placeholder_data): + return self.identifier.replace(".", "_") + + def create_placeholder_node(self, node_name=None): + """Create node to be used as placeholder. + + By default, it creates a null node in '/out'. + Feel free to override it in different workfile build plugins. + """ + + node = hou.node("/out").createNode("null", node_name) + node.parm("execute").hide(True) + node.parm("renderdialog").hide(True) + return node + + def create_placeholder(self, placeholder_data): + + node_name = self.get_placeholder_node_name(placeholder_data) + + placeholder_node = self.create_placeholder_node(node_name) + HoudiniCreator.customize_node_look(placeholder_node) + + placeholder_data["plugin_identifier"] = self.identifier + + imprint(placeholder_node, placeholder_data) + + def collect_scene_placeholders(self): + # Read the cache + placeholder_nodes = self.builder.get_shared_populate_data( + "placeholder_nodes" + ) + if placeholder_nodes is None: + placeholder_nodes = {} + + nodes = lsattr("plugin_identifier", self.identifier) + + for node in nodes: + placeholder_nodes[node.path()] = node + + # Set the cache + self.builder.set_shared_populate_data( + "placeholder_nodes", placeholder_nodes + ) + + return placeholder_nodes + + def update_placeholder(self, placeholder_item, placeholder_data): + placeholder_node = hou.node(placeholder_item.scene_identifier) + imprint(placeholder_node, placeholder_data, update=True) + + # Update node name + node_name = self.get_placeholder_node_name(placeholder_data) + placeholder_node.setName(node_name, unique_name=True) + + def delete_placeholder(self, placeholder): + placeholder_node = hou.node(placeholder.scene_identifier) + placeholder_node.destroy() + + +def build_workfile_template(*args): + # NOTE Should we inform users that they'll lose unsaved changes ? + builder = HoudiniTemplateBuilder(registered_host()) + builder.build_template() + + +def update_workfile_template(*args): + builder = HoudiniTemplateBuilder(registered_host()) + builder.rebuild_template() + + +def create_placeholder(*args): + host = registered_host() + builder = HoudiniTemplateBuilder(host) + window = WorkfileBuildPlaceholderDialog(host, builder, + parent=get_main_window()) + window.show() + + +def update_placeholder(*args): + host = registered_host() + builder = HoudiniTemplateBuilder(host) + placeholder_items_by_id = { + placeholder_item.scene_identifier: placeholder_item + for placeholder_item in builder.get_placeholders() + } + placeholder_items = [] + for node in hou.selectedNodes(): + if node.path() in placeholder_items_by_id: + placeholder_items.append(placeholder_items_by_id[node.path()]) + + if len(placeholder_items) == 0: + show_message_dialog( + "Workfile Placeholder Manager", + "Please select a placeholder node.", + "warning", + get_main_window() + ) + return + + if len(placeholder_items) > 1: + show_message_dialog( + "Workfile Placeholder Manager", + "Too many selected placeholder nodes.\n" + "Please, Select one placeholder node.", + "warning", + get_main_window() + ) + return + + placeholder_item = placeholder_items[0] + window = WorkfileBuildPlaceholderDialog(host, builder, + parent=get_main_window()) + window.set_update_mode(placeholder_item) + window.exec_() From 313b5e80a04f9f86541f2c66e418485d594fba95 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 10 Jul 2024 00:26:34 +0300 Subject: [PATCH 03/11] Implement `create_placeholder.py` --- .../workfile_build/create_placeholder.py | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 client/ayon_houdini/plugins/workfile_build/create_placeholder.py diff --git a/client/ayon_houdini/plugins/workfile_build/create_placeholder.py b/client/ayon_houdini/plugins/workfile_build/create_placeholder.py new file mode 100644 index 0000000000..48242742de --- /dev/null +++ b/client/ayon_houdini/plugins/workfile_build/create_placeholder.py @@ -0,0 +1,60 @@ +from ayon_core.pipeline.workfile.workfile_template_builder import ( + CreatePlaceholderItem, + PlaceholderCreateMixin, +) +from ayon_core.pipeline import registered_host +from ayon_core.pipeline.create import CreateContext + +from ayon_houdini.api.workfile_template_builder import ( + HoudiniPlaceholderPlugin +) +from ayon_houdini.api.lib import read + + +class HoudiniPlaceholderCreatePlugin( + HoudiniPlaceholderPlugin, PlaceholderCreateMixin +): + """Workfile template plugin to create "create placeholders". + + "create placeholders" will be replaced by publish instances. + + TODO: + Support imprint & read precreate data to instances. + """ + + identifier = "ayon.create.placeholder" + label = "Houdini Create" + + def populate_placeholder(self, placeholder): + self.populate_create_placeholder(placeholder) + + def repopulate_placeholder(self, placeholder): + self.populate_create_placeholder(placeholder) + + def get_placeholder_options(self, options=None): + return self.get_create_plugin_options(options) + + def get_placeholder_node_name(self, placeholder_data): + create_context = CreateContext(registered_host()) + creator = create_context.creators.get(placeholder_data["creator"]) + product_type = creator.product_type + node_name = "{}_{}".format(self.identifier.replace(".", "_"), product_type) + + return node_name + + def collect_placeholders(self): + output = [] + scene_placeholders = self.collect_scene_placeholders() + for node_name, node in scene_placeholders.items(): + plugin_identifier = node.evalParm("plugin_identifier") + if plugin_identifier != self.identifier: + continue + + placeholder_data = read(node) + + output.append( + CreatePlaceholderItem(node_name, placeholder_data, self) + ) + + return output + \ No newline at end of file From 2140bce5df0d1f8005569a44d4b6ddcc98ce5453 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 10 Jul 2024 00:27:27 +0300 Subject: [PATCH 04/11] `register_workfile_build_plugin_path` in `pipeline.py` --- client/ayon_houdini/api/pipeline.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/ayon_houdini/api/pipeline.py b/client/ayon_houdini/api/pipeline.py index c6fbbd5b62..3f2fe17f18 100644 --- a/client/ayon_houdini/api/pipeline.py +++ b/client/ayon_houdini/api/pipeline.py @@ -14,6 +14,7 @@ register_creator_plugin_path, register_loader_plugin_path, register_inventory_action_path, + register_workfile_build_plugin_path, AVALON_CONTAINER_ID, AYON_CONTAINER_ID, ) @@ -41,6 +42,7 @@ LOAD_PATH = os.path.join(PLUGINS_DIR, "load") CREATE_PATH = os.path.join(PLUGINS_DIR, "create") INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") +WORKFILE_BUILD_PATH = os.path.join(PLUGINS_DIR, "workfile_build") # Track whether the workfile tool is about to save _about_to_save = False @@ -63,6 +65,7 @@ def install(self): register_loader_plugin_path(LOAD_PATH) register_creator_plugin_path(CREATE_PATH) register_inventory_action_path(INVENTORY_PATH) + register_workfile_build_plugin_path(WORKFILE_BUILD_PATH) log.info("Installing callbacks ... ") # register_event_callback("init", on_init) From 7c3284e61ab05b0dfd30848c562a49aab6ab3b5d Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 10 Jul 2024 00:28:38 +0300 Subject: [PATCH 05/11] Add workfile actions to `AYON` menu --- .../ayon_houdini/startup/MainMenuCommon.xml | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/client/ayon_houdini/startup/MainMenuCommon.xml b/client/ayon_houdini/startup/MainMenuCommon.xml index 5b383f0085..636d438fea 100644 --- a/client/ayon_houdini/startup/MainMenuCommon.xml +++ b/client/ayon_houdini/startup/MainMenuCommon.xml @@ -104,6 +104,63 @@ parent = hou.qt.mainWindow() host_tools.show_experimental_tools_dialog(parent) ]]> + + + + + + + + + + + + + + + + + + + + + + + + + From b75b8cb1ec70d59612caeaabad9e551ed69fa8d8 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 10 Jul 2024 17:06:03 +0300 Subject: [PATCH 06/11] update 'collect_scene_placeholders' to cache by identifier --- client/ayon_houdini/api/workfile_template_builder.py | 12 ++++++------ .../plugins/workfile_build/create_placeholder.py | 10 +++------- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/client/ayon_houdini/api/workfile_template_builder.py b/client/ayon_houdini/api/workfile_template_builder.py index 7b99260033..d28f200ac2 100644 --- a/client/ayon_houdini/api/workfile_template_builder.py +++ b/client/ayon_houdini/api/workfile_template_builder.py @@ -198,21 +198,21 @@ def create_placeholder(self, placeholder_data): imprint(placeholder_node, placeholder_data) def collect_scene_placeholders(self): - # Read the cache + # Read the cache by identifier placeholder_nodes = self.builder.get_shared_populate_data( - "placeholder_nodes" + self.identifier ) if placeholder_nodes is None: - placeholder_nodes = {} + placeholder_nodes = [] nodes = lsattr("plugin_identifier", self.identifier) for node in nodes: - placeholder_nodes[node.path()] = node + placeholder_nodes.append(node) - # Set the cache + # Set the cache by identifier self.builder.set_shared_populate_data( - "placeholder_nodes", placeholder_nodes + self.identifier, placeholder_nodes ) return placeholder_nodes diff --git a/client/ayon_houdini/plugins/workfile_build/create_placeholder.py b/client/ayon_houdini/plugins/workfile_build/create_placeholder.py index 48242742de..ddbfa3362d 100644 --- a/client/ayon_houdini/plugins/workfile_build/create_placeholder.py +++ b/client/ayon_houdini/plugins/workfile_build/create_placeholder.py @@ -44,16 +44,12 @@ def get_placeholder_node_name(self, placeholder_data): def collect_placeholders(self): output = [] - scene_placeholders = self.collect_scene_placeholders() - for node_name, node in scene_placeholders.items(): - plugin_identifier = node.evalParm("plugin_identifier") - if plugin_identifier != self.identifier: - continue + create_placeholders = self.collect_scene_placeholders() + for node in create_placeholders: placeholder_data = read(node) - output.append( - CreatePlaceholderItem(node_name, placeholder_data, self) + CreatePlaceholderItem(node.path(), placeholder_data, self) ) return output From b9059535de5c339ab0d65f72e4f76bd6624e2eae Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 10 Jul 2024 18:11:30 +0300 Subject: [PATCH 07/11] implement load placeholder plugin --- .../workfile_build/load_placeholder.py | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 client/ayon_houdini/plugins/workfile_build/load_placeholder.py diff --git a/client/ayon_houdini/plugins/workfile_build/load_placeholder.py b/client/ayon_houdini/plugins/workfile_build/load_placeholder.py new file mode 100644 index 0000000000..fb957b1735 --- /dev/null +++ b/client/ayon_houdini/plugins/workfile_build/load_placeholder.py @@ -0,0 +1,52 @@ +from ayon_core.pipeline.workfile.workfile_template_builder import ( + LoadPlaceholderItem, + PlaceholderLoadMixin, +) + +from ayon_houdini.api.workfile_template_builder import ( + HoudiniPlaceholderPlugin +) +from ayon_houdini.api.lib import read, lsattr + + +class HoudiniPlaceholderLoadPlugin( + HoudiniPlaceholderPlugin, PlaceholderLoadMixin +): + """Workfile template plugin to create "load placeholders". + + "load placeholders" will be replaced by AYON products. + + """ + + identifier = "ayon.load.placeholder" + label = "Houdini Load" + + def populate_placeholder(self, placeholder): + self.populate_load_placeholder(placeholder) + + def repopulate_placeholder(self, placeholder): + self.populate_load_placeholder(placeholder) + + def get_placeholder_options(self, options=None): + return self.get_load_plugin_options(options) + + def get_placeholder_node_name(self, placeholder_data): + + node_name = "{}_{}".format( + self.identifier.replace(".", "_"), + placeholder_data["product_name"] + ) + return node_name + + def collect_placeholders(self): + output = [] + load_placeholders = self.collect_scene_placeholders() + + for node in load_placeholders: + placeholder_data = read(node) + output.append( + LoadPlaceholderItem(node.path(), placeholder_data, self) + ) + + return output + \ No newline at end of file From c92791c10661a80fb06ba3c6bb2ee48506f6d054 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Fri, 12 Jul 2024 18:24:03 +0300 Subject: [PATCH 08/11] hide parameters permanently and move to good possition --- client/ayon_houdini/api/workfile_template_builder.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/client/ayon_houdini/api/workfile_template_builder.py b/client/ayon_houdini/api/workfile_template_builder.py index d28f200ac2..ff0c52f531 100644 --- a/client/ayon_houdini/api/workfile_template_builder.py +++ b/client/ayon_houdini/api/workfile_template_builder.py @@ -182,8 +182,13 @@ def create_placeholder_node(self, node_name=None): """ node = hou.node("/out").createNode("null", node_name) - node.parm("execute").hide(True) - node.parm("renderdialog").hide(True) + node.moveToGoodPosition() + parms = node.parmTemplateGroup() + for parm in {"execute", "renderdialog"}: + p = parms.find(parm) + p.hide(True) + parms.replace(parm, p) + node.setParmTemplateGroup(parms) return node def create_placeholder(self, placeholder_data): From 0562616a0d7c34e659d5a47e596d960e267eba2c Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Fri, 12 Jul 2024 18:27:47 +0300 Subject: [PATCH 09/11] remove unused import --- client/ayon_houdini/plugins/workfile_build/load_placeholder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_houdini/plugins/workfile_build/load_placeholder.py b/client/ayon_houdini/plugins/workfile_build/load_placeholder.py index fb957b1735..c9f137fb8b 100644 --- a/client/ayon_houdini/plugins/workfile_build/load_placeholder.py +++ b/client/ayon_houdini/plugins/workfile_build/load_placeholder.py @@ -6,7 +6,7 @@ from ayon_houdini.api.workfile_template_builder import ( HoudiniPlaceholderPlugin ) -from ayon_houdini.api.lib import read, lsattr +from ayon_houdini.api.lib import read class HoudiniPlaceholderLoadPlugin( From 5a086216fe8c32ca1fb0244641350b206cd6b5e3 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Mon, 15 Jul 2024 13:12:11 +0300 Subject: [PATCH 10/11] update minimum required core version --- package.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.py b/package.py index 30713ef3a5..def4d72c57 100644 --- a/package.py +++ b/package.py @@ -5,6 +5,6 @@ client_dir = "ayon_houdini" ayon_required_addons = { - "core": ">0.4.0", + "core": ">0.4.1", } ayon_compatible_addons = {} From 9ac32ddf0769c642b96c616b61213a7eb8ccb9fb Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Mon, 15 Jul 2024 15:28:39 +0300 Subject: [PATCH 11/11] merge template path to current scene instead opening the template --- client/ayon_houdini/api/workfile_template_builder.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/client/ayon_houdini/api/workfile_template_builder.py b/client/ayon_houdini/api/workfile_template_builder.py index ff0c52f531..352dba546d 100644 --- a/client/ayon_houdini/api/workfile_template_builder.py +++ b/client/ayon_houdini/api/workfile_template_builder.py @@ -159,10 +159,12 @@ def import_template(self, path): # TODO Check if template is already imported - # Load template workfile - self.host.open_workfile(path) - - return True + # Merge (Load) template workfile in the current scene. + try: + hou.hipFile.merge(path, ignore_load_warnings=True) + return True + except hou.OperationFailed: + return False class HoudiniPlaceholderPlugin(PlaceholderPlugin):