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/aftereffects/client/ayon_aftereffects/api/workfile_template_builder.py b/server_addon/aftereffects/client/ayon_aftereffects/api/workfile_template_builder.py index 77fd1059b5..7fbd469851 100644 --- a/server_addon/aftereffects/client/ayon_aftereffects/api/workfile_template_builder.py +++ b/server_addon/aftereffects/client/ayon_aftereffects/api/workfile_template_builder.py @@ -92,7 +92,7 @@ class AEPlaceholderPlugin(PlaceholderPlugin): return None, None def _collect_scene_placeholders(self): - """" Cache placeholder data to shared data. + """Cache placeholder data to shared data. Returns: (list) of dicts """ diff --git a/server_addon/blender/client/ayon_blender/plugins/publish/extract_thumbnail.py b/server_addon/blender/client/ayon_blender/plugins/publish/extract_thumbnail.py index 40097aaa89..e3bce8bf73 100644 --- a/server_addon/blender/client/ayon_blender/plugins/publish/extract_thumbnail.py +++ b/server_addon/blender/client/ayon_blender/plugins/publish/extract_thumbnail.py @@ -83,7 +83,7 @@ class ExtractThumbnail(plugin.BlenderExtractor): instance.data["representations"].append(representation) def _fix_output_path(self, filepath): - """"Workaround to return correct filepath. + """Workaround to return correct filepath. To workaround this we just glob.glob() for any file extensions and assume the latest modified file is the correct file and return it. 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..9252fda3be 100644 --- a/server_addon/houdini/client/ayon_houdini/api/plugin.py +++ b/server_addon/houdini/client/ayon_houdini/api/plugin.py @@ -148,7 +148,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 +161,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. @@ -193,7 +198,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/maya/client/ayon_maya/plugins/create/create_setdress.py b/server_addon/maya/client/ayon_maya/plugins/create/create_setdress.py index 12532e0724..6e1c4e1c4f 100644 --- a/server_addon/maya/client/ayon_maya/plugins/create/create_setdress.py +++ b/server_addon/maya/client/ayon_maya/plugins/create/create_setdress.py @@ -9,11 +9,16 @@ class CreateSetDress(plugin.MayaCreator): label = "Set Dress" product_type = "setdress" icon = "cubes" + exactSetMembersOnly = True + shader = True default_variants = ["Main", "Anim"] def get_instance_attr_defs(self): return [ BoolDef("exactSetMembersOnly", label="Exact Set Members Only", - default=True) + default=self.exactSetMembersOnly), + BoolDef("shader", + label="Include shader", + default=self.shader) ] diff --git a/server_addon/maya/client/ayon_maya/plugins/publish/extract_maya_scene_raw.py b/server_addon/maya/client/ayon_maya/plugins/publish/extract_maya_scene_raw.py index 6e66353c7a..047b7f6e6c 100644 --- a/server_addon/maya/client/ayon_maya/plugins/publish/extract_maya_scene_raw.py +++ b/server_addon/maya/client/ayon_maya/plugins/publish/extract_maya_scene_raw.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- """Extract data as Maya scene (raw).""" import os - +import contextlib from ayon_core.lib import BoolDef from ayon_core.pipeline import AVALON_CONTAINER_ID, AYON_CONTAINER_ID from ayon_core.pipeline.publish import AYONPyblishPluginMixin -from ayon_maya.api.lib import maintained_selection +from ayon_maya.api.lib import maintained_selection, shader from ayon_maya.api import plugin from maya import cmds @@ -88,17 +88,21 @@ class ExtractMayaSceneRaw(plugin.MayaExtractorPlugin, AYONPyblishPluginMixin): ) with maintained_selection(): cmds.select(selection, noExpand=True) - cmds.file(path, - force=True, - typ="mayaAscii" if self.scene_type == "ma" else "mayaBinary", # noqa: E501 - exportSelected=True, - preserveReferences=attribute_values[ - "preserve_references" - ], - constructionHistory=True, - shader=True, - constraints=True, - expressions=True) + with contextlib.ExitStack() as stack: + if not instance.data.get("shader", True): + # Fix bug where export without shader may import the geometry 'green' + # due to the lack of any shader on import. + stack.enter_context(shader(selection, shadingEngine="initialShadingGroup")) + + cmds.file(path, + force=True, + typ="mayaAscii" if self.scene_type == "ma" else "mayaBinary", + exportSelected=True, + preserveReferences=attribute_values["preserve_references"], + constructionHistory=True, + shader=instance.data.get("shader", True), + constraints=True, + expressions=True) if "representations" not in instance.data: instance.data["representations"] = [] diff --git a/server_addon/maya/client/ayon_maya/version.py b/server_addon/maya/client/ayon_maya/version.py index c5fbef58fe..1f53dfa492 100644 --- a/server_addon/maya/client/ayon_maya/version.py +++ b/server_addon/maya/client/ayon_maya/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'maya' version.""" -__version__ = "0.2.6" +__version__ = "0.2.7" diff --git a/server_addon/maya/package.py b/server_addon/maya/package.py index 2f70b630d5..47aa8a4c0d 100644 --- a/server_addon/maya/package.py +++ b/server_addon/maya/package.py @@ -1,6 +1,6 @@ name = "maya" title = "Maya" -version = "0.2.6" +version = "0.2.7" client_dir = "ayon_maya" ayon_required_addons = { diff --git a/server_addon/maya/server/settings/creators.py b/server_addon/maya/server/settings/creators.py index 5f3b850a1f..ede33b6eec 100644 --- a/server_addon/maya/server/settings/creators.py +++ b/server_addon/maya/server/settings/creators.py @@ -124,6 +124,14 @@ class CreateVrayProxyModel(BaseSettingsModel): default_factory=list, title="Default Products") +class CreateSetDressModel(BaseSettingsModel): + enabled: bool = SettingsField(True) + exactSetMembersOnly: bool = SettingsField(title="Exact Set Members Only") + shader: bool = SettingsField(title="Include shader") + default_variants: list[str] = SettingsField( + default_factory=list, title="Default Products") + + class CreateMultishotLayout(BasicCreatorModel): shotParent: str = SettingsField(title="Shot Parent Folder") groupLoadedAssets: bool = SettingsField(title="Group Loaded Assets") @@ -217,8 +225,8 @@ class CreatorsModel(BaseSettingsModel): default_factory=BasicCreatorModel, title="Create Rig" ) - CreateSetDress: BasicCreatorModel = SettingsField( - default_factory=BasicCreatorModel, + CreateSetDress: CreateSetDressModel = SettingsField( + default_factory=CreateSetDressModel, title="Create Set Dress" ) CreateVrayProxy: CreateVrayProxyModel = SettingsField( @@ -396,6 +404,8 @@ DEFAULT_CREATORS_SETTINGS = { }, "CreateSetDress": { "enabled": True, + "exactSetMembersOnly": True, + "shader": True, "default_variants": [ "Main", "Anim"