Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support publishing and loading node presets #70

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions client/ayon_houdini/api/lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -1389,3 +1389,99 @@ 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)


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"
)
105 changes: 105 additions & 0 deletions client/ayon_houdini/plugins/create/create_node_preset.py
Original file line number Diff line number Diff line change
@@ -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):
Copy link
Member

@moonyuet moonyuet Aug 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A question: Does this function imprint the data into the instance_node already? (Or it is like the temp data publisher and it won't imprint the instance data?)
i.e. some variables you can find in almost every creator and they are missing in this creator.

            instance = CreatedInstance(
                self.product_type,
                product_name,
                instance_data,
                self)
            self._add_instance_to_context(instance)
            self.imprint(instance_node, instance.data_to_store())

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method _update_node_parmtemplate doesn't create the extra ayon parameters created by imprint.
It actually doesn't create any parameters at all.
It only captures the values of any existent parameters (including ayon extra parameters) from the source node and reassigns these values to the target node.


I have that feeling that the method name should be refactored but I couldn't come up with a better name :/

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remember @BigRoy has written a one-time publish node(along with the generic nodes for publishing). Maybe he can give some maps on this.

"""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()
]
80 changes: 80 additions & 0 deletions client/ayon_houdini/plugins/load/load_filepath.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ class FilePathLoader(plugin.HoudiniLoader):
product_types = {"*"}
representations = {"*"}

def _add_more_node_params(self, attr_folder, node):
moonyuet marked this conversation as resolved.
Show resolved Hide resolved
# allow subclasses to add more params.
pass

def load(self, context, name=None, namespace=None, data=None):

# Get the root node
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -128,3 +136,75 @@ 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" : """
from ayon_houdini.api.lib import node_preset_validate_target
node_preset_validate_target(hou.pwd())
""",
"script_callback_language" : "python",
}
)

apply_template = hou.ButtonParmTemplate(
name="apply_preset",
label="Apply Preset",
tags= {
"script_callback" : """
from ayon_houdini.api.lib import read_and_apply_preset
read_and_apply_preset(hou.pwd())
""",
"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" : """
from ayon_houdini.api.lib import read_and_apply_preset
read_and_apply_preset(hou.pwd(), 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)
61 changes: 61 additions & 0 deletions client/ayon_houdini/plugins/publish/extract_render_setup.py
Original file line number Diff line number Diff line change
@@ -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)