Merge branch 'develop' into enhancement/hero_version_disable_hardlinks_setting

This commit is contained in:
Roy Nieterau 2024-06-25 10:12:56 +02:00 committed by GitHub
commit 2166c009df
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 269 additions and 66 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

@ -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
"""

View file

@ -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.

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

@ -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)

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

@ -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)
]

View file

@ -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"] = []

View file

@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
"""Package declaring AYON addon 'maya' version."""
__version__ = "0.2.6"
__version__ = "0.2.7"

View file

@ -1,6 +1,6 @@
name = "maya"
title = "Maya"
version = "0.2.6"
version = "0.2.7"
client_dir = "ayon_maya"
ayon_required_addons = {

View file

@ -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"