mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 12:54:40 +01:00
Merge branch 'develop' into enhancement/hero_version_disable_hardlinks_setting
This commit is contained in:
commit
2166c009df
13 changed files with 269 additions and 66 deletions
|
|
@ -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": []
|
||||
|
|
|
|||
|
|
@ -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
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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"] = []
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Package declaring AYON addon 'maya' version."""
|
||||
__version__ = "0.2.6"
|
||||
__version__ = "0.2.7"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
name = "maya"
|
||||
title = "Maya"
|
||||
version = "0.2.6"
|
||||
version = "0.2.7"
|
||||
client_dir = "ayon_maya"
|
||||
|
||||
ayon_required_addons = {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue