diff --git a/server/settings/tools.py b/server/settings/tools.py index 1cb070e2af..3ed12d3d0a 100644 --- a/server/settings/tools.py +++ b/server/settings/tools.py @@ -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": [] diff --git a/server_addon/houdini/client/ayon_houdini/api/__init__.py b/server_addon/houdini/client/ayon_houdini/api/__init__.py index 2663a55f6f..358113a555 100644 --- a/server_addon/houdini/client/ayon_houdini/api/__init__.py +++ b/server_addon/houdini/client/ayon_houdini/api/__init__.py @@ -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", diff --git a/server_addon/houdini/client/ayon_houdini/api/pipeline.py b/server_addon/houdini/client/ayon_houdini/api/pipeline.py index 22a15605c7..5efbcc6ff9 100644 --- a/server_addon/houdini/client/ayon_houdini/api/pipeline.py +++ b/server_addon/houdini/client/ayon_houdini/api/pipeline.py @@ -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 diff --git a/server_addon/houdini/client/ayon_houdini/api/plugin.py b/server_addon/houdini/client/ayon_houdini/api/plugin.py index 9c6bba925a..2eb34bc727 100644 --- a/server_addon/houdini/client/ayon_houdini/api/plugin.py +++ b/server_addon/houdini/client/ayon_houdini/api/plugin.py @@ -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) diff --git a/server_addon/houdini/client/ayon_houdini/plugins/create/create_hda.py b/server_addon/houdini/client/ayon_houdini/plugins/create/create_hda.py index 694bc4f3c3..179a6c2b00 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/create/create_hda.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/create/create_hda.py @@ -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 diff --git a/server_addon/houdini/client/ayon_houdini/plugins/load/load_hda.py b/server_addon/houdini/client/ayon_houdini/plugins/load/load_hda.py index 5738ba7fab..fcf0e834f8 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/load/load_hda.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/load/load_hda.py @@ -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 diff --git a/server_addon/houdini/client/ayon_houdini/plugins/publish/validate_subset_name.py b/server_addon/houdini/client/ayon_houdini/plugins/publish/validate_subset_name.py index dfd353bddf..a63a4f16c7 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/publish/validate_subset_name.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/publish/validate_subset_name.py @@ -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 diff --git a/server_addon/houdini/client/ayon_houdini/version.py b/server_addon/houdini/client/ayon_houdini/version.py index 66f3ac59e7..b6b644f30e 100644 --- a/server_addon/houdini/client/ayon_houdini/version.py +++ b/server_addon/houdini/client/ayon_houdini/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'houdini' version.""" -__version__ = "0.3.4" +__version__ = "0.3.5" diff --git a/server_addon/houdini/package.py b/server_addon/houdini/package.py index 0c1b1fcf9b..5bdde038c2 100644 --- a/server_addon/houdini/package.py +++ b/server_addon/houdini/package.py @@ -1,6 +1,6 @@ name = "houdini" title = "Houdini" -version = "0.3.4" +version = "0.3.5" client_dir = "ayon_houdini"