Merge branch 'develop' into feature/AY-5635_Action-to-update-to-latest-approved-version

This commit is contained in:
Jakub Trllo 2024-06-25 11:38:33 +02:00 committed by GitHub
commit 01f43d4551
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 234 additions and 131 deletions

View file

@ -448,6 +448,17 @@ DEFAULT_TOOLS_VALUES = {
"task_types": [],
"tasks": [],
"template": "SK_{folder[name]}{variant}"
},
{
"product_types": [
"hda"
],
"hosts": [
"houdini"
],
"task_types": [],
"tasks": [],
"template": "{folder[name]}_{variant}"
}
],
"filter_creator_profiles": []

View file

@ -4,10 +4,6 @@ from .pipeline import (
containerise
)
from .plugin import (
Creator,
)
from .lib import (
lsattr,
lsattrs,
@ -23,8 +19,6 @@ __all__ = [
"ls",
"containerise",
"Creator",
# Utility functions
"lsattr",
"lsattrs",

View file

@ -221,12 +221,8 @@ def containerise(name,
"""
# Ensure AVALON_CONTAINERS subnet exists
subnet = hou.node(AVALON_CONTAINERS)
if subnet is None:
obj_network = hou.node("/obj")
subnet = obj_network.createNode("subnet",
node_name="AVALON_CONTAINERS")
# Get AVALON_CONTAINERS subnet
subnet = get_or_create_avalon_container()
# Create proper container name
container_name = "{}_{}".format(name, suffix or "CON")
@ -401,6 +397,18 @@ def on_new():
_enforce_start_frame()
def get_or_create_avalon_container() -> "hou.OpNode":
avalon_container = hou.node(AVALON_CONTAINERS)
if avalon_container:
return avalon_container
parent_path, name = AVALON_CONTAINERS.rsplit("/", 1)
parent = hou.node(parent_path)
return parent.createNode(
"subnet", node_name=name
)
def _set_context_settings():
"""Apply the project settings from the project definition

View file

@ -10,8 +10,7 @@ import hou
import pyblish.api
from ayon_core.pipeline import (
CreatorError,
LegacyCreator,
Creator as NewCreator,
Creator,
CreatedInstance,
AYON_INSTANCE_ID,
AVALON_INSTANCE_ID,
@ -26,80 +25,6 @@ from .lib import imprint, read, lsattr, add_self_publish_button
SETTINGS_CATEGORY = "houdini"
class Creator(LegacyCreator):
"""Creator plugin to create instances in Houdini
To support the wide range of node types for render output (Alembic, VDB,
Mantra) the Creator needs a node type to create the correct instance
By default, if none is given, is `geometry`. An example of accepted node
types: geometry, alembic, ifd (mantra)
Please check the Houdini documentation for more node types.
Tip: to find the exact node type to create press the `i` left of the node
when hovering over a node. The information is visible under the name of
the node.
Deprecated:
This creator is deprecated and will be removed in future version.
"""
defaults = ['Main']
def __init__(self, *args, **kwargs):
super(Creator, self).__init__(*args, **kwargs)
self.nodes = []
def process(self):
"""This is the base functionality to create instances in Houdini
The selected nodes are stored in self to be used in an override method.
This is currently necessary in order to support the multiple output
types in Houdini which can only be rendered through their own node.
Default node type if none is given is `geometry`
It also makes it easier to apply custom settings per instance type
Example of override method for Alembic:
def process(self):
instance = super(CreateEpicNode, self, process()
# Set parameters for Alembic node
instance.setParms(
{"sop_path": "$HIP/%s.abc" % self.nodes[0]}
)
Returns:
hou.Node
"""
try:
if (self.options or {}).get("useSelection"):
self.nodes = hou.selectedNodes()
# Get the node type and remove it from the data, not needed
node_type = self.data.pop("node_type", None)
if node_type is None:
node_type = "geometry"
# Get out node
out = hou.node("/out")
instance = out.createNode(node_type, node_name=self.name)
instance.moveToGoodPosition()
imprint(instance, self.data)
self._process(instance)
except hou.Error as er:
six.reraise(
CreatorError,
CreatorError("Creator error: {}".format(er)),
sys.exc_info()[2])
class HoudiniCreatorBase(object):
@staticmethod
def cache_instance_data(shared_data):
@ -148,7 +73,11 @@ class HoudiniCreatorBase(object):
@staticmethod
def create_instance_node(
folder_path, node_name, parent, node_type="geometry"
folder_path,
node_name,
parent,
node_type="geometry",
pre_create_data=None
):
"""Create node representing instance.
@ -157,6 +86,7 @@ class HoudiniCreatorBase(object):
node_name (str): Name of the new node.
parent (str): Name of the parent node.
node_type (str, optional): Type of the node.
pre_create_data (Optional[Dict]): Pre create data.
Returns:
hou.Node: Newly created instance node.
@ -170,7 +100,7 @@ class HoudiniCreatorBase(object):
@six.add_metaclass(ABCMeta)
class HoudiniCreator(NewCreator, HoudiniCreatorBase):
class HoudiniCreator(Creator, HoudiniCreatorBase):
"""Base class for most of the Houdini creator plugins."""
selected_nodes = []
settings_name = None
@ -193,7 +123,12 @@ class HoudiniCreator(NewCreator, HoudiniCreatorBase):
folder_path = instance_data["folderPath"]
instance_node = self.create_instance_node(
folder_path, product_name, "/out", node_type)
folder_path,
product_name,
"/out",
node_type,
pre_create_data
)
self.customize_node_look(instance_node)

View file

@ -1,13 +1,19 @@
# -*- coding: utf-8 -*-
"""Creator plugin for creating publishable Houdini Digital Assets."""
import ayon_api
import hou
from assettools import setToolSubmenu
import ayon_api
from ayon_core.pipeline import (
CreatorError,
get_current_project_name
)
from ayon_core.lib import (
get_ayon_username,
BoolDef
)
from ayon_houdini.api import plugin
import hou
class CreateHDA(plugin.HoudiniCreator):
@ -37,19 +43,38 @@ class CreateHDA(plugin.HoudiniCreator):
return product_name.lower() in existing_product_names_low
def create_instance_node(
self, folder_path, node_name, parent, node_type="geometry"
self,
folder_path,
node_name,
parent,
node_type="geometry",
pre_create_data=None
):
if pre_create_data is None:
pre_create_data = {}
parent_node = hou.node("/obj")
if self.selected_nodes:
# if we have `use selection` enabled, and we have some
# selected nodes ...
subnet = parent_node.collapseIntoSubnet(
self.selected_nodes,
subnet_name="{}_subnet".format(node_name))
subnet.moveToGoodPosition()
to_hda = subnet
if self.selected_nodes[0].type().name() == "subnet":
to_hda = self.selected_nodes[0]
to_hda.setName("{}_subnet".format(node_name), unique_name=True)
else:
parent_node = self.selected_nodes[0].parent()
subnet = parent_node.collapseIntoSubnet(
self.selected_nodes,
subnet_name="{}_subnet".format(node_name))
subnet.moveToGoodPosition()
to_hda = subnet
else:
# Use Obj as the default path
parent_node = hou.node("/obj")
# Find and return the NetworkEditor pane tab with the minimum index
pane = hou.ui.paneTabOfType(hou.paneTabType.NetworkEditor)
if isinstance(pane, hou.NetworkEditor):
# Use the NetworkEditor pane path as the parent path.
parent_node = pane.pwd()
to_hda = parent_node.createNode(
"subnet", node_name="{}_subnet".format(node_name))
if not to_hda.type().definition():
@ -71,7 +96,8 @@ class CreateHDA(plugin.HoudiniCreator):
hda_node = to_hda.createDigitalAsset(
name=type_name,
description=node_name,
hda_file_name="$HIP/{}.hda".format(node_name)
hda_file_name="$HIP/{}.hda".format(node_name),
ignore_external_references=True
)
hda_node.layoutChildren()
elif self._check_existing(folder_path, node_name):
@ -81,21 +107,92 @@ class CreateHDA(plugin.HoudiniCreator):
else:
hda_node = to_hda
hda_node.setName(node_name)
# If user tries to create the same HDA instance more than
# once, then all of them will have the same product name and
# point to the same hda_file_name. But, their node names will
# be incremented.
hda_node.setName(node_name, unique_name=True)
self.customize_node_look(hda_node)
# Set Custom settings.
hda_def = hda_node.type().definition()
if pre_create_data.get("set_user"):
hda_def.setUserInfo(get_ayon_username())
if pre_create_data.get("use_project"):
setToolSubmenu(hda_def, "AYON/{}".format(self.project_name))
return hda_node
def create(self, product_name, instance_data, pre_create_data):
instance_data.pop("active", None)
instance = super(CreateHDA, self).create(
return super(CreateHDA, self).create(
product_name,
instance_data,
pre_create_data)
return instance
def get_network_categories(self):
# Houdini allows creating sub-network nodes inside
# these categories.
# Therefore this plugin can work in these categories.
return [
hou.objNodeTypeCategory()
hou.chopNodeTypeCategory(),
hou.cop2NodeTypeCategory(),
hou.dopNodeTypeCategory(),
hou.ropNodeTypeCategory(),
hou.lopNodeTypeCategory(),
hou.objNodeTypeCategory(),
hou.sopNodeTypeCategory(),
hou.topNodeTypeCategory(),
hou.vopNodeTypeCategory()
]
def get_pre_create_attr_defs(self):
attrs = super(CreateHDA, self).get_pre_create_attr_defs()
return attrs + [
BoolDef("set_user",
tooltip="Set current user as the author of the HDA",
default=False,
label="Set Current User"),
BoolDef("use_project",
tooltip="Use project name as tab submenu path.\n"
"The location in TAB Menu will be\n"
"'AYON/project_name/your_HDA_name'",
default=True,
label="Use Project as menu entry"),
]
def get_dynamic_data(
self,
project_name,
folder_entity,
task_entity,
variant,
host_name,
instance
):
"""
Pass product name from product name templates as dynamic data.
"""
dynamic_data = super(CreateHDA, self).get_dynamic_data(
project_name,
folder_entity,
task_entity,
variant,
host_name,
instance
)
dynamic_data.update(
{
"asset": folder_entity["name"],
"folder": {
"label": folder_entity["label"],
"name": folder_entity["name"]
}
}
)
return dynamic_data

View file

@ -1,8 +1,13 @@
# -*- coding: utf-8 -*-
import os
from ayon_core.pipeline import get_representation_path
import hou
from ayon_core.pipeline import (
get_representation_path,
AVALON_CONTAINER_ID
)
from ayon_core.pipeline.load import LoadError
from ayon_houdini.api import (
lib,
pipeline,
plugin
)
@ -19,42 +24,43 @@ class HdaLoader(plugin.HoudiniLoader):
color = "orange"
def load(self, context, name=None, namespace=None, data=None):
import hou
# Format file name, Houdini only wants forward slashes
file_path = self.filepath_from_context(context)
file_path = os.path.normpath(file_path)
file_path = file_path.replace("\\", "/")
# Get the root node
obj = hou.node("/obj")
namespace = namespace or context["folder"]["name"]
node_name = "{}_{}".format(namespace, name) if namespace else name
hou.hda.installFile(file_path)
# Get the type name from the HDA definition.
hda_defs = hou.hda.definitionsInFile(file_path)
if not hda_defs:
raise LoadError(f"No HDA definitions found in file: {file_path}")
type_name = hda_defs[0].nodeTypeName()
hda_node = obj.createNode(type_name, node_name)
parent_node = self._create_dedicated_parent_node(hda_defs[-1])
self[:] = [hda_node]
# Get the type name from the HDA definition.
type_name = hda_defs[-1].nodeTypeName()
hda_node = parent_node.createNode(type_name, node_name)
hda_node.moveToGoodPosition()
return pipeline.containerise(
node_name,
namespace,
[hda_node],
context,
self.__class__.__name__,
suffix="",
)
# Imprint it manually
data = {
"schema": "openpype:container-2.0",
"id": AVALON_CONTAINER_ID,
"name": node_name,
"namespace": namespace,
"loader": self.__class__.__name__,
"representation": context["representation"]["id"],
}
lib.imprint(hda_node, data)
return hda_node
def update(self, container, context):
import hou
repre_entity = context["representation"]
hda_node = container["node"]
@ -71,4 +77,45 @@ class HdaLoader(plugin.HoudiniLoader):
def remove(self, container):
node = container["node"]
parent = node.parent()
node.destroy()
if parent.path() == pipeline.AVALON_CONTAINERS:
return
# Remove parent if empty.
if not parent.children():
parent.destroy()
def _create_dedicated_parent_node(self, hda_def):
# Get the root node
parent_node = pipeline.get_or_create_avalon_container()
node = None
node_type = None
if hda_def.nodeTypeCategory() == hou.objNodeTypeCategory():
return parent_node
elif hda_def.nodeTypeCategory() == hou.chopNodeTypeCategory():
node_type, node_name = "chopnet", "MOTION"
elif hda_def.nodeTypeCategory() == hou.cop2NodeTypeCategory():
node_type, node_name = "cop2net", "IMAGES"
elif hda_def.nodeTypeCategory() == hou.dopNodeTypeCategory():
node_type, node_name = "dopnet", "DOPS"
elif hda_def.nodeTypeCategory() == hou.ropNodeTypeCategory():
node_type, node_name = "ropnet", "ROPS"
elif hda_def.nodeTypeCategory() == hou.lopNodeTypeCategory():
node_type, node_name = "lopnet", "LOPS"
elif hda_def.nodeTypeCategory() == hou.sopNodeTypeCategory():
node_type, node_name = "geo", "SOPS"
elif hda_def.nodeTypeCategory() == hou.topNodeTypeCategory():
node_type, node_name = "topnet", "TOPS"
# TODO: Create a dedicated parent node based on Vop Node vex context.
elif hda_def.nodeTypeCategory() == hou.vopNodeTypeCategory():
node_type, node_name = "matnet", "MATSandVOPS"
node = parent_node.node(node_name)
if not node:
node = parent_node.createNode(node_type, node_name)
node.moveToGoodPosition()
return node

View file

@ -10,10 +10,9 @@ from ayon_core.pipeline.publish import (
ValidateContentsOrder,
RepairAction,
)
from ayon_core.pipeline.create import get_product_name
from ayon_houdini.api import plugin
from ayon_houdini.api.action import SelectInvalidAction
from ayon_core.pipeline.create import get_product_name
class FixProductNameAction(RepairAction):
@ -26,7 +25,7 @@ class ValidateSubsetName(plugin.HoudiniInstancePlugin,
"""
families = ["staticMesh"]
families = ["staticMesh", "hda"]
label = "Validate Product Name"
order = ValidateContentsOrder + 0.1
actions = [FixProductNameAction, SelectInvalidAction]
@ -67,7 +66,13 @@ class ValidateSubsetName(plugin.HoudiniInstancePlugin,
instance.context.data["hostName"],
instance.data["productType"],
variant=instance.data["variant"],
dynamic_data={"asset": folder_entity["name"]}
dynamic_data={
"asset": folder_entity["name"],
"folder": {
"label": folder_entity["label"],
"name": folder_entity["name"]
}
}
)
if instance.data.get("productName") != product_name:
@ -97,7 +102,13 @@ class ValidateSubsetName(plugin.HoudiniInstancePlugin,
instance.context.data["hostName"],
instance.data["productType"],
variant=instance.data["variant"],
dynamic_data={"asset": folder_entity["name"]}
dynamic_data={
"asset": folder_entity["name"],
"folder": {
"label": folder_entity["label"],
"name": folder_entity["name"]
}
}
)
instance.data["productName"] = product_name

View file

@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
"""Package declaring AYON addon 'houdini' version."""
__version__ = "0.3.4"
__version__ = "0.3.5"

View file

@ -1,6 +1,6 @@
name = "houdini"
title = "Houdini"
version = "0.3.4"
version = "0.3.5"
client_dir = "ayon_houdini"