From 58d0d979fa20de6f602c3726c7b056524eb80070 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Fri, 9 Aug 2024 19:12:57 +0300 Subject: [PATCH 1/4] add support for publishing node presets --- .../plugins/create/create_node_preset.py | 105 ++++++++++++++++++ .../plugins/publish/extract_render_setup.py | 61 ++++++++++ 2 files changed, 166 insertions(+) create mode 100644 client/ayon_houdini/plugins/create/create_node_preset.py create mode 100644 client/ayon_houdini/plugins/publish/extract_render_setup.py diff --git a/client/ayon_houdini/plugins/create/create_node_preset.py b/client/ayon_houdini/plugins/create/create_node_preset.py new file mode 100644 index 0000000000..042f5d0926 --- /dev/null +++ b/client/ayon_houdini/plugins/create/create_node_preset.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- +"""Creator plugin for creating houdini node presets.""" +from ayon_houdini.api import plugin +import hou + + +def _update_node_parmtemplate(node, defaults): + """update node parm template. + + It adds a new folder parm that includes + filepath and operator node selector. + """ + parm_group = node.parmTemplateGroup() + + # Hide unnecessary parameters + for parm in {"execute", "renderdialog"}: + p = parm_group.find(parm) + p.hide(True) + parm_group.replace(parm, p) + + # Create essential parameters + folder_template = hou.FolderParmTemplate( + name="main", + label="Main", + folder_type=hou.folderType.Tabs + ) + + filepath_template = hou.StringParmTemplate( + name="filepath", + label="Preset File", + num_components=1, + default_value=(defaults.get("filepath", ""),), + string_type=hou.stringParmType.FileReference, + tags= { + "filechooser_pattern" : "*.json", + } + ) + + operatore_template = hou.StringParmTemplate( + name="source_node", + label="Source Node", + num_components=1, + default_value=(defaults.get("source_node", ""),), + string_type=hou.stringParmType.NodeReference, + tags= { + "oprelative" : "." + } + ) + + folder_template.addParmTemplate(filepath_template) + folder_template.addParmTemplate(operatore_template) + + # TODO: make the Main and Extra Tab next to each other. + parm_group.insertBefore((0,), folder_template) + + node.setParmTemplateGroup(parm_group) + + +class CreateNodePreset(plugin.HoudiniCreator): + """NodePreset creator. + + Node Presets capture the parameters of the source node. + """ + identifier = "io.ayon.creators.houdini.node_preset" + label = "Node Preset" + product_type = "node_preset" + icon = "gears" + + def create(self, product_name, instance_data, pre_create_data): + + instance_data.update({"node_type": "null"}) + + instance = super(CreateNodePreset, self).create( + product_name, + instance_data, + pre_create_data) + + instance_node = hou.node(instance.get("instance_node")) + + + filepath = "{}{}".format( + hou.text.expandString("$HIP/pyblish/"), + f"{product_name}.json" + ) + source_node = "" + + if self.selected_nodes: + source_node = self.selected_nodes[0].path() + + defaults= { + "filepath": filepath, + "source_node": source_node + } + _update_node_parmtemplate(instance_node, defaults) + + + def get_pre_create_attr_defs(self): + attrs = super().get_pre_create_attr_defs() + + return attrs + self.get_instance_attr_defs() + + def get_network_categories(self): + return [ + hou.ropNodeTypeCategory() + ] diff --git a/client/ayon_houdini/plugins/publish/extract_render_setup.py b/client/ayon_houdini/plugins/publish/extract_render_setup.py new file mode 100644 index 0000000000..b8c7ecc32f --- /dev/null +++ b/client/ayon_houdini/plugins/publish/extract_render_setup.py @@ -0,0 +1,61 @@ +import os +import hou +import json +import pyblish.api + +from ayon_houdini.api import plugin + + +def getparms(node): + + parameters = node.parms() + parameters += node.spareParms() + param_data = {} + + for param in parameters: + if param.parmTemplate().type().name() == 'FolderSet': + continue + + # Add parameter data to the dictionary + # FIXME: I also evaluate expressions. + param_data[param.name()] = param.eval() + + return param_data + + +class ExtractNodePreset(plugin.HoudiniExtractorPlugin): + """Node Preset Extractor for any node.""" + label = "Extract Node Preset" + order = pyblish.api.ExtractorOrder + + families = ["node_preset"] + targets = ["local", "remote"] + + def process(self, instance: pyblish.api.Instance): + if instance.data.get("farm"): + self.log.debug("Should be processed on farm, skipping.") + return + + instance_node = hou.node(instance.data["instance_node"]) + + source_node = instance_node.parm("source_node").evalAsNode() + json_path = instance_node.evalParm("filepath") + + param_data = getparms(source_node) + node_preset = { + "metadata":{ + "type": source_node.type().name() + }, + "param_data": param_data + } + with open(json_path, "w+") as f: + json.dump(node_preset, fp=f, indent=2, sort_keys=True) + + representation = { + "name": "json", + "ext": "json", + "files": os.path.basename(json_path), + "stagingDir": os.path.dirname(json_path), + } + + instance.data.setdefault("representations", []).append(representation) From fea866bada5a3ed2716fa7a3c79abbb1f01ac4dd Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Fri, 9 Aug 2024 19:13:17 +0300 Subject: [PATCH 2/4] add support for loading node presets --- client/ayon_houdini/api/lib.py | 41 +++++++ .../plugins/load/load_filepath.py | 116 ++++++++++++++++++ 2 files changed, 157 insertions(+) diff --git a/client/ayon_houdini/api/lib.py b/client/ayon_houdini/api/lib.py index 3c2520250d..e90234ccb2 100644 --- a/client/ayon_houdini/api/lib.py +++ b/client/ayon_houdini/api/lib.py @@ -5,6 +5,7 @@ import re import logging import json +import time from contextlib import contextmanager import six @@ -1360,3 +1361,43 @@ def prompt_reset_context(): update_content_on_context_change() dialog.deleteLater() + + +def load_node_preset(target_node, node_preset, update_locked=False, log=None): + """Load node preset. + + It's used to load node presets generated by ayon. + This function is similar to `imprint` but generic. + + Args: + node(hou.Node): node object from Houdini + data(dict): collection of attributes and their value + update_locked (bool, optional): if set, it will update locked parameters. + + Returns: + None + """ + + if log is None: + log = self.log + + param_data = node_preset["param_data"] + + for param_name in param_data: + param = target_node.parm(param_name) + if not param: + log.debug(f"Skipping, '{param_name}' doesn't exist in '{target_node.path()}'.") + continue + + parm_locked = param.isLocked() + if parm_locked: + if not update_locked: + log.debug(f"Skipping '{param_name}', it's locked.") + continue + param.lock(False) + + # FIXME: I don't work with animated parms. + param.set(param_data[param_name]) + + if update_locked and parm_locked: + param.lock(True) diff --git a/client/ayon_houdini/plugins/load/load_filepath.py b/client/ayon_houdini/plugins/load/load_filepath.py index 2ce9bd7ffb..d17c59fe0b 100644 --- a/client/ayon_houdini/plugins/load/load_filepath.py +++ b/client/ayon_houdini/plugins/load/load_filepath.py @@ -25,6 +25,10 @@ class FilePathLoader(plugin.HoudiniLoader): product_types = {"*"} representations = {"*"} + def _add_more_node_params(self, attr_folder, node): + # allow subclasses to add more params. + pass + def load(self, context, name=None, namespace=None, data=None): # Get the root node @@ -53,6 +57,10 @@ def load(self, context, name=None, namespace=None, data=None): num_components=1, default_value=(filepath,)) attr_folder.addParmTemplate(parm) + + # Call add more node params. + self._add_more_node_params(attr_folder, container) + parm_template_group.append(attr_folder) # Hide some default labels @@ -128,3 +136,111 @@ def format_path(path: str, representation: dict) -> str: path = re.sub(pattern, ".{}.{}".format(token, ext), path) return os.path.normpath(path).replace("\\", "/") + + +class NodePresetLoader(FilePathLoader): + """Load node presets. + + It works the same as FilePathLoader, except its extra parameters, + 2 buttons and target node field. + Buttons are used apply the node preset to the target node. + """ + + label = "Load Node Preset" + order = 9 + icon = "link" + color = "white" + product_types = {"node_preset"} + representations = {"json"} + + # TODO: + # 1. Find a way to cache the node preset, instead of reading the file every time. + # 2. Notify the user with the results of Apply button (succeeded, failed and why). + # Note: + # So far we manage the node preset, but we don't manage setting the node preset. + def _add_more_node_params(self, attr_folder, node): + # allow subclasses to add more params. + + operatore_template = hou.StringParmTemplate( + name="target_node", + label="Target Node", + num_components=1, + default_value=("",), + string_type=hou.stringParmType.NodeReference, + tags= { + "oprelative" : ".", + "script_callback" : """ +import json +from ayon_houdini.api.lib import load_node_preset + +json_path = hou.parm("./filepath").eval() +target_node = hou.parm("./target_node").evalAsNode() +node_preset = {} +with open(json_path, "r") as f: + node_preset = json.load(f) + +node_type = node_preset["metadata"]["type"] + +hou.pwd().setColor(hou.Color(0.7, 0.8, 0.87)) +hou.pwd().setComment("") +hou.pwd().setGenericFlag(hou.nodeFlag.DisplayComment, True) +if target_node and target_node.type().name() != node_type: + hou.pwd().setColor(hou.Color(0.8, 0.45, 0.1)) + hou.pwd().setComment( + f"Target Node type '{target_node.type().name()}' doesn't match the loaded preset type '{node_type}'." + "Please note, Applying the preset skips parameters that doesn't exist" + ) +""", + "script_callback_language" : "python", + } + ) + + apply_template = hou.ButtonParmTemplate( + name="apply_preset", + label="Apply Preset", + tags= { + "script_callback" : """ +import json +from ayon_houdini.api.lib import load_node_preset + +json_path = hou.parm("./filepath").eval() +target_node = hou.parm("./target_node").evalAsNode() +if target_node: + node_preset = {} + with open(json_path, "r") as f: + node_preset = json.load(f) + + load_node_preset(target_node, node_preset) +""", + "script_callback_language" : "python", + }, + help=("Apply render preset to the target node." + "Skip updating locked parameters.") + ) + + force_apply_template = hou.ButtonParmTemplate( + name="force_apply_preset", + label="Force Apply Preset", + tags= { + "script_callback" : """ +import json +from ayon_houdini.api.lib import load_node_preset + +json_path = hou.parm("./filepath").eval() +target_node = hou.parm("./target_node").evalAsNode() +if target_node: + node_preset = {} + with open(json_path, "r") as f: + node_preset = json.load(f) + + load_node_preset(target_node, node_preset, update_locked=True) +""", + "script_callback_language" : "python", + }, + help=("Apply render preset to the target node." + "Update also locked parameters.") + ) + + attr_folder.addParmTemplate(operatore_template) + attr_folder.addParmTemplate(apply_template) + attr_folder.addParmTemplate(force_apply_template) From 3818b57bda704dce2a41f44e149c6e9bf228dba2 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Mon, 2 Sep 2024 14:06:17 +0300 Subject: [PATCH 3/4] move functions defs to lib --- client/ayon_houdini/api/lib.py | 56 +++++++++++++++++++ .../plugins/load/load_filepath.py | 48 ++-------------- 2 files changed, 62 insertions(+), 42 deletions(-) diff --git a/client/ayon_houdini/api/lib.py b/client/ayon_houdini/api/lib.py index df0cb6c277..861e06ae24 100644 --- a/client/ayon_houdini/api/lib.py +++ b/client/ayon_houdini/api/lib.py @@ -1430,3 +1430,59 @@ def load_node_preset(target_node, node_preset, update_locked=False, log=None): if update_locked and parm_locked: param.lock(True) + + +def read_and_apply_preset(node, update_locked=False): + """Read and apply a node preset. + + This function expects the following parameters to be present on the node: + 1. filepath: Path to the preset file node. + 2. target_node: The node that will receive the preset settings. + + Args: + node(hou.Node): file path node + update_locked (bool, optional): if set, it will update locked parameters. + + Returns: + None + """ + + json_path = node.parm("filepath").eval() + target_node = node.parm("target_node").evalAsNode() + if target_node: + node_preset = {} + with open(json_path, "r") as f: + node_preset = json.load(f) + + load_node_preset(target_node, node_preset, update_locked=update_locked) + + +def node_preset_validate_target(node): + """Validates the type of the target node. + + This function provides visual confirmation when the target node's type + aligns with the type used to create the node preset. + + This function expects the following parameters to be present on the node: + 1. filepath: Path to the preset file node. + 2. target_node: The node that will receive the preset settings. + + """ + + json_path = node.parm("filepath").eval() + target_node = node.parm("target_node").evalAsNode() + node_preset = {} + with open(json_path, "r") as f: + node_preset = json.load(f) + + node_type = node_preset["metadata"]["type"] + + node.setColor(hou.Color(0.7, 0.8, 0.87)) + node.setComment("") + node.setGenericFlag(hou.nodeFlag.DisplayComment, True) + if target_node and target_node.type().name() != node_type: + node.setColor(hou.Color(0.8, 0.45, 0.1)) + node.setComment( + f"Target Node type '{target_node.type().name()}' doesn't match the loaded preset type '{node_type}'." + "Please note, Applying the preset skips parameters that doesn't exist" + ) \ No newline at end of file diff --git a/client/ayon_houdini/plugins/load/load_filepath.py b/client/ayon_houdini/plugins/load/load_filepath.py index d17c59fe0b..2b4fb636e7 100644 --- a/client/ayon_houdini/plugins/load/load_filepath.py +++ b/client/ayon_houdini/plugins/load/load_filepath.py @@ -170,26 +170,8 @@ def _add_more_node_params(self, attr_folder, node): tags= { "oprelative" : ".", "script_callback" : """ -import json -from ayon_houdini.api.lib import load_node_preset - -json_path = hou.parm("./filepath").eval() -target_node = hou.parm("./target_node").evalAsNode() -node_preset = {} -with open(json_path, "r") as f: - node_preset = json.load(f) - -node_type = node_preset["metadata"]["type"] - -hou.pwd().setColor(hou.Color(0.7, 0.8, 0.87)) -hou.pwd().setComment("") -hou.pwd().setGenericFlag(hou.nodeFlag.DisplayComment, True) -if target_node and target_node.type().name() != node_type: - hou.pwd().setColor(hou.Color(0.8, 0.45, 0.1)) - hou.pwd().setComment( - f"Target Node type '{target_node.type().name()}' doesn't match the loaded preset type '{node_type}'." - "Please note, Applying the preset skips parameters that doesn't exist" - ) +from ayon_houdini.api.lib import node_preset_validate_target +node_preset_validate_target(hou.pwd()) """, "script_callback_language" : "python", } @@ -200,17 +182,8 @@ def _add_more_node_params(self, attr_folder, node): label="Apply Preset", tags= { "script_callback" : """ -import json -from ayon_houdini.api.lib import load_node_preset - -json_path = hou.parm("./filepath").eval() -target_node = hou.parm("./target_node").evalAsNode() -if target_node: - node_preset = {} - with open(json_path, "r") as f: - node_preset = json.load(f) - - load_node_preset(target_node, node_preset) +from ayon_houdini.api.lib import read_and_apply_preset +read_and_apply_preset(hou.pwd()) """, "script_callback_language" : "python", }, @@ -223,17 +196,8 @@ def _add_more_node_params(self, attr_folder, node): label="Force Apply Preset", tags= { "script_callback" : """ -import json -from ayon_houdini.api.lib import load_node_preset - -json_path = hou.parm("./filepath").eval() -target_node = hou.parm("./target_node").evalAsNode() -if target_node: - node_preset = {} - with open(json_path, "r") as f: - node_preset = json.load(f) - - load_node_preset(target_node, node_preset, update_locked=True) +from ayon_houdini.api.lib import read_and_apply_preset +read_and_apply_preset(hou.pwd(), update_locked=True) """, "script_callback_language" : "python", }, From 2e15b960261564a86df8caa4e7381c570b878e2c Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Tue, 3 Sep 2024 18:18:38 +0300 Subject: [PATCH 4/4] remove unused import --- client/ayon_houdini/api/lib.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_houdini/api/lib.py b/client/ayon_houdini/api/lib.py index 861e06ae24..a80cde1575 100644 --- a/client/ayon_houdini/api/lib.py +++ b/client/ayon_houdini/api/lib.py @@ -5,7 +5,6 @@ import re import logging import json -import time from contextlib import contextmanager import six