From 5b7fdfdf42daeeaefd41559312ce63d57b052ac4 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 3 Jul 2024 00:56:42 +0200 Subject: [PATCH 01/15] Implement Generic ROP publish from https://github.com/ynput/ayon-core/pull/542 --- client/ayon_houdini/api/lib.py | 28 + client/ayon_houdini/api/node_wrap.py | 41 + client/ayon_houdini/api/plugin.py | 23 +- .../plugins/create/create_generic.py | 735 ++++++++++++++++++ .../plugins/publish/collect_farm_instances.py | 3 +- .../plugins/publish/collect_frames.py | 2 +- .../publish/collect_houdini_batch_families.py | 19 + .../plugins/publish/extract_rop.py | 12 +- client/ayon_houdini/startup/OPmenu.xml | 18 + .../ayon_houdini/startup/scripts/OnCreated.py | 18 + 10 files changed, 883 insertions(+), 16 deletions(-) create mode 100644 client/ayon_houdini/api/node_wrap.py create mode 100644 client/ayon_houdini/plugins/create/create_generic.py create mode 100644 client/ayon_houdini/plugins/publish/collect_houdini_batch_families.py create mode 100644 client/ayon_houdini/startup/scripts/OnCreated.py diff --git a/client/ayon_houdini/api/lib.py b/client/ayon_houdini/api/lib.py index eec3995821..a969ca144c 100644 --- a/client/ayon_houdini/api/lib.py +++ b/client/ayon_houdini/api/lib.py @@ -104,6 +104,21 @@ def get_output_parameter(node): return node.parm("outputimage") elif node_type == "vray_renderer": return node.parm("SettingsOutput_img_file_path") + elif node_type == "labs::karma::2.0": + return node.parm("picture") + + if isinstance(node, hou.RopNode): + # Use the parm name fallback that SideFX applies for detecting output + # files from PDG/TOPs graphs for ROP nodes. See #ayon-core/692 + parm_names = [ + "vm_picture", "sopoutput", "dopoutput", "lopoutput", "picture", + "copoutput", "filename", "usdfile", "file", "output", + "outputfilepath", "outputimage", "outfile" + ] + for name in parm_names: + parm = node.parm(name) + if parm: + return parm raise TypeError("Node type '%s' not supported" % node_type) @@ -1363,3 +1378,16 @@ def prompt_reset_context(): update_content_on_context_change() dialog.deleteLater() + + +@contextmanager +def no_auto_create_publishable(): + value = os.environ.get("AYON_HOUDINI_AUTOCREATE") + os.environ["AYON_HOUDINI_AUTOCREATE"] = "0" + try: + yield + finally: + if value is None: + del os.environ["AYON_HOUDINI_AUTOCREATE"] + else: + os.environ["AYON_HOUDINI_AUTOCREATE"] = value diff --git a/client/ayon_houdini/api/node_wrap.py b/client/ayon_houdini/api/node_wrap.py new file mode 100644 index 0000000000..e484bb5265 --- /dev/null +++ b/client/ayon_houdini/api/node_wrap.py @@ -0,0 +1,41 @@ +import hou + +from ayon_core.pipeline import registered_host +from ayon_core.pipeline.create import CreateContext + + +def make_publishable(node): + # TODO: Can we make this imprinting much faster? Unfortunately + # CreateContext initialization is very slow. + host = registered_host() + context = CreateContext(host) + + # Apply the instance creation to the node + context.create( + creator_identifier="io.ayon.creators.houdini.publish", + variant="__use_node_name__", + pre_create_data={ + "node": node + } + ) + + +# TODO: Move this choice of automatic 'imprint' to settings so studio can +# configure which nodes should get automatically imprinted on creation +# TODO: Do not import and reload the creator plugin file +from ayon_houdini.plugins.create import create_generic +import importlib +importlib.reload(create_generic) +AUTO_CREATE_NODE_TYPES = set( + create_generic.CreateHoudiniGeneric.node_type_product_types.keys() +) + + +def autocreate_publishable(node): + # For now only consider RopNode + if not isinstance(node, hou.RopNode): + return + + node_type = node.type().name() + if node_type in AUTO_CREATE_NODE_TYPES: + make_publishable(node) diff --git a/client/ayon_houdini/api/plugin.py b/client/ayon_houdini/api/plugin.py index 8a2344febb..7b19842194 100644 --- a/client/ayon_houdini/api/plugin.py +++ b/client/ayon_houdini/api/plugin.py @@ -19,7 +19,13 @@ ) from ayon_core.lib import BoolDef -from .lib import imprint, read, lsattr, add_self_publish_button +from .lib import ( + imprint, + read, + lsattr, + add_self_publish_button, + no_auto_create_publishable +) SETTINGS_CATEGORY = "houdini" @@ -122,13 +128,14 @@ def create(self, product_name, instance_data, pre_create_data): folder_path = instance_data["folderPath"] - instance_node = self.create_instance_node( - folder_path, - product_name, - "/out", - node_type, - pre_create_data - ) + with no_auto_create_publishable(): + instance_node = self.create_instance_node( + folder_path, + product_name, + "/out", + node_type, + pre_create_data + ) self.customize_node_look(instance_node) diff --git a/client/ayon_houdini/plugins/create/create_generic.py b/client/ayon_houdini/plugins/create/create_generic.py new file mode 100644 index 0000000000..343934b173 --- /dev/null +++ b/client/ayon_houdini/plugins/create/create_generic.py @@ -0,0 +1,735 @@ +import dataclasses +from typing import Dict, List, Optional + +from ayon_houdini.api import plugin +from ayon_houdini.api.lib import ( + lsattr, read +) +from ayon_core.pipeline.create import ( + CreatedInstance, + get_product_name +) +from ayon_api import get_folder_by_path, get_task_by_name +from ayon_core.lib import ( + AbstractAttrDef, + BoolDef, + NumberDef, + EnumDef, + TextDef, + UISeparatorDef, + UILabelDef, + FileDef +) + +import hou +import json + + +def attribute_def_to_parm_template(attribute_def, key=None): + """AYON Attribute Definition to Houdini Parm Template. + + Arguments: + attribute_def (AbstractAttrDef): Attribute Definition. + + Returns: + hou.ParmTemplate: Parm Template matching the Attribute Definition. + """ + + if key is None: + key = attribute_def.key + + if isinstance(attribute_def, BoolDef): + return hou.ToggleParmTemplate(name=key, + label=attribute_def.label, + default_value=attribute_def.default, + help=attribute_def.tooltip) + elif isinstance(attribute_def, NumberDef): + if attribute_def.decimals == 0: + return hou.IntParmTemplate( + name=key, + label=attribute_def.label, + default_value=(attribute_def.default,), + help=attribute_def.tooltip, + min=attribute_def.minimum, + max=attribute_def.maximum, + num_components=1 + ) + else: + return hou.FloatParmTemplate( + name=key, + label=attribute_def.label, + default_value=(attribute_def.default,), + help=attribute_def.tooltip, + min=attribute_def.minimum, + max=attribute_def.maximum, + num_components=1 + ) + elif isinstance(attribute_def, EnumDef): + # TODO: Support multiselection EnumDef + # We only support enums that do not allow multiselection + # as a dedicated houdini parm. + if not attribute_def.multiselection: + labels = [item["label"] for item in attribute_def.items] + values = [item["value"] for item in attribute_def.items] + return hou.StringParmTemplate( + name=key, + label=attribute_def.label, + default_value=(attribute_def.default,), + help=attribute_def.tooltip, + num_components=1, + menu_labels=labels, + menu_items=values, + menu_type=hou.menuType.Normal + ) + elif isinstance(attribute_def, TextDef): + return hou.StringParmTemplate( + name=key, + label=attribute_def.label, + default_value=(attribute_def.default,), + help=attribute_def.tooltip, + num_components=1 + ) + elif isinstance(attribute_def, UISeparatorDef): + return hou.SeparatorParmTemplate( + name=key, + label=attribute_def.label, + ) + elif isinstance(attribute_def, UILabelDef): + return hou.LabelParmTemplate( + name=key, + label=attribute_def.label, + ) + elif isinstance(attribute_def, FileDef): + # TODO: Support FileDef + pass + + # Unsupported attribute definition. We'll store value as JSON so just + # turn it into a string `JSON::` value + json_value = json.dumps(getattr(attribute_def, "default", None), + default=str) + return hou.StringParmTemplate( + name=key, + label=attribute_def.label, + default_value=f"JSON::{json_value}", + help=getattr(attribute_def, "tooltip", None), + num_components=1 + ) + + +def set_values(node: "hou.OpNode", values: dict): + """Set parm values only if both the raw value (e.g. expression) or the + evaluated value differ. This way we preserve expressions if they happen + to evaluate to a matching value. + + Parms must exist on the node already. + + """ + for key, value in values.items(): + + parm = node.parm(key) + + try: + unexpanded_value = parm.unexpandedString() + if unexpanded_value == value: + # Allow matching expressions + continue + except hou.OperationFailed: + pass + + if parm.rawValue() == value: + continue + + if parm.eval() == value: + # Needs no change + continue + + # TODO: Set complex data types as `JSON:` + parm.set(value) + + +@dataclasses.dataclass +class NodeTypeProductTypes: + """Product type settings for a node type. + + Define the available product types the user can set on a ROP based on + node type. + + When 'strict' an enum attribute is created and the user can not type a + custom product type, otherwise a string attribute is + created with a menu right hand side to help pick a type but allow custom + types. + """ + product_types: List[str] + default: Optional[str] = None + strict: bool = True + + +# Re-usable defaults +GEO_PRODUCT_TYPES = NodeTypeProductTypes( + product_types=["pointcache", "model"], + default="pointcache" +) +FBX_PRODUCT_TYPES = NodeTypeProductTypes( + product_types=["fbx", "pointcache", "model"], + default="fbx" +) +FBX_ONLY_PRODUCT_TYPES = NodeTypeProductTypes( + product_types=["fbx"], + default="fbx" +) +USD_PRODUCT_TYPES = NodeTypeProductTypes( + product_types=["usd", "pointcache"], + default="usd" +) +COMP_PRODUCT_TYPES = NodeTypeProductTypes( + product_types=["imagesequence", "render"], + default="imagesequence" +) +REVIEW_PRODUCT_TYPES = NodeTypeProductTypes( + product_types=["review"], + default="review" +) +RENDER_PRODUCT_TYPES = NodeTypeProductTypes( + product_types=["render", "prerender"], + default="render" +) +GLTF_PRODUCT_TYPES = NodeTypeProductTypes( + product_types=["gltf"], + default="gltf" +) + + +class CreateHoudiniGeneric(plugin.HoudiniCreator): + """Generic creator to ingest arbitrary products""" + + USE_DEFAULT_PRODUCT_TYPE = "__use_node_default__" + USE_DEFAULT_NODE_NAME = "__use_node_name__" + + host_name = "houdini" + + identifier = "io.ayon.creators.houdini.publish" + label = "Generic" + product_type = "generic" + icon = "male" + description = "Make any ROP node publishable." + + render_target = "local_no_render" + default_variant = USE_DEFAULT_NODE_NAME + default_variants = ["Main", USE_DEFAULT_NODE_NAME] + + # TODO: Move this to project settings + node_type_product_types: Dict[str, NodeTypeProductTypes] = { + "alembic": GEO_PRODUCT_TYPES, + "rop_alembic": GEO_PRODUCT_TYPES, + "geometry": GEO_PRODUCT_TYPES, + "rop_geometry": GEO_PRODUCT_TYPES, + "filmboxfbx": FBX_PRODUCT_TYPES, + "rop_fbx": FBX_PRODUCT_TYPES, + "usd": USD_PRODUCT_TYPES, + "usd_rop": USD_PRODUCT_TYPES, + "usdexport": USD_PRODUCT_TYPES, + "comp": COMP_PRODUCT_TYPES, + "opengl": REVIEW_PRODUCT_TYPES, + "arnold": RENDER_PRODUCT_TYPES, + "karma": RENDER_PRODUCT_TYPES, + "ifd": RENDER_PRODUCT_TYPES, + "usdrender": RENDER_PRODUCT_TYPES, + "usdrender_rop": RENDER_PRODUCT_TYPES, + "vray_renderer": RENDER_PRODUCT_TYPES, + "labs::karma::2.0": RENDER_PRODUCT_TYPES, + "kinefx::rop_fbxanimoutput": FBX_ONLY_PRODUCT_TYPES, + "kinefx::rop_fbxcharacteroutput": FBX_ONLY_PRODUCT_TYPES, + "kinefx::rop_gltfcharacteroutput": GLTF_PRODUCT_TYPES, + "rop_gltf": GLTF_PRODUCT_TYPES + } + + node_type_product_types_default = NodeTypeProductTypes( + product_types=list(sorted( + { + "ass", "pointcache", "model", "render", "camera", + "imagesequence", "review", "vdbcache", "fbx" + })), + default="pointcache", + strict=False + ) + + def get_detail_description(self): + return "Publish any ROP node." + + def create(self, product_name, instance_data, pre_create_data): + + product_type = pre_create_data.get("productType", + self.USE_DEFAULT_PRODUCT_TYPE) + instance_data["productType"] = product_type + + # Unfortunately the Create Context will provide the product name + # even before the `create` call without listening to pre create data + # or the instance data - so instead we ignore the product name here + # and redefine it ourselves based on the `variant` in instance data + project_name = self.create_context.project_name + folder_entity = get_folder_by_path(project_name, + instance_data["folderPath"]) + task_entity = get_task_by_name(project_name, + folder_id=folder_entity["id"], + task_name=instance_data["task"]) + + if pre_create_data.get("node"): + nodes = [pre_create_data.get("node")] + else: + nodes = hou.selectedNodes() + + source_variant = instance_data["variant"] + + for node in nodes: + if node.parm("AYON_creator_identifier"): + # Continue if already existing attributes + continue + + # Enforce new style instance id otherwise first save may adjust + # this to the `AVALON_INSTANCE_ID` instead + instance_data["id"] = plugin.AYON_INSTANCE_ID + + # When using default product type base it on node type settings + node_product_type = product_type + if node_product_type == self.USE_DEFAULT_PRODUCT_TYPE: + node_type = node.type().name() + node_product_type = self.node_type_product_types.get( + node_type, self.node_type_product_types_default + ).default + + # Allow variant to be based off of the created node name + variant = source_variant + if variant == self.USE_DEFAULT_NODE_NAME: + variant = node.name() + + product_name = self._get_product_name_dynamic( + self.create_context.project_name, + folder_entity=folder_entity, + task_entity=task_entity, + variant=variant, + product_type=node_product_type + ) + + instance_data["variant"] = variant + instance_data["instance_node"] = node.path() + instance_data["instance_id"] = node.path() + created_instance = CreatedInstance( + node_product_type, product_name, instance_data.copy(), self + ) + + # Add instance + self._add_instance_to_context(created_instance) + + # Imprint on the selected node + # NOTE: We imprint after `_add_instance_to_context` to ensure + # the imprinted data directly contains also the instance + # attributes for the product type. Otherwise, they will appear + # after first save. + self.imprint(created_instance, + values=created_instance.data_to_store(), + update=False) + + def collect_instances(self): + for node in lsattr("AYON_id", plugin.AYON_INSTANCE_ID): + + creator_identifier_parm = node.parm("AYON_creator_identifier") + if not creator_identifier_parm: + continue + + # creator instance + creator_id = creator_identifier_parm.eval() + if creator_id != self.identifier: + continue + + # Read all attributes starting with `ayon_` + node_data = { + key.removeprefix("AYON_"): value + for key, value in read(node).items() + if key.startswith("AYON_") + } + + # Node paths are always the full node path since that is unique + # Because it's the node's path it's not written into attributes + # but explicitly collected + node_path = node.path() + node_data["instance_id"] = node_path + node_data["instance_node"] = node_path + node_data["families"] = self.get_publish_families() + + # Read creator and publish attributes + publish_attributes = {} + creator_attributes = {} + for key, value in dict(node_data).items(): + if key.startswith("publish_attributes_"): + if value == 0 or value == 1: + value = bool(value) + plugin_name, plugin_key = key[len("publish_attributes_"):].split("_", 1) + publish_attributes.setdefault(plugin_name, {})[plugin_key] = value + del node_data[key] # remove from original + elif key.startswith("creator_attributes_"): + creator_key = key[len("creator_attributes_"):] + creator_attributes[creator_key] = value + del node_data[key] # remove from original + + node_data["creator_attributes"] = creator_attributes + node_data["publish_attributes"] = publish_attributes + + created_instance = CreatedInstance.from_existing( + node_data, self + ) + self._add_instance_to_context(created_instance) + + def update_instances(self, update_list): + # Overridden to pass `created_instance` to `self.imprint` + for created_inst, changes in update_list: + new_values = { + key: changes[key].new_value + for key in changes.changed_keys + } + # Update parm templates and values + self.imprint( + created_inst, + new_values, + update=True + ) + + def get_product_name( + self, + project_name, + folder_entity, + task_entity, + variant, + host_name=None, + instance=None + ): + if instance is not None: + self.product_type = instance.data["productType"] + product_name = super(CreateHoudiniGeneric, self).get_product_name( + project_name, + folder_entity, + task_entity, + variant, + host_name, + instance) + self.product_type = "generic" + return product_name + + else: + return "<-- defined on create -->" + + def create_attribute_def_parms(self, + node: "hou.OpNode", + created_instance: CreatedInstance): + # We imprint all the attributes into an AYON tab on the node in which + # we have a list folder called `attributes` in which we have + # - Instance Attributes + # - Creator Attributes + # - Publish Attributes + # With also a separate `advanced` section for specific attributes + parm_group = node.parmTemplateGroup() + + # Create default folder parm structure + ayon_folder = parm_group.findFolder("AYON") + if not ayon_folder: + ayon_folder = hou.FolderParmTemplate("folder", "AYON") + parm_group.addParmTemplate(ayon_folder) + + attributes_folder = parm_group.find("AYON_attributes") + if not attributes_folder: + attributes_folder = hou.FolderParmTemplate( + "AYON_attributes", + "Attributes", + folder_type=hou.folderType.Collapsible + ) + ayon_folder.addParmTemplate(attributes_folder) + + # Create Instance, Creator and Publish attributes folders + instance_attributes_folder = parm_group.find("AYON_instance_attributes") + if not instance_attributes_folder: + instance_attributes_folder = hou.FolderParmTemplate( + "AYON_instance_attributes", + "Instance Attributes", + folder_type=hou.folderType.Simple + ) + attributes_folder.addParmTemplate(instance_attributes_folder) + + creator_attributes_folder = parm_group.find("AYON_creator_attributes") + if not creator_attributes_folder: + creator_attributes_folder = hou.FolderParmTemplate( + "AYON_creator_attributes", + "Creator Attributes", + folder_type=hou.folderType.Simple + ) + attributes_folder.addParmTemplate(creator_attributes_folder) + + publish_attributes_folder = parm_group.find("AYON_publish_attributes") + if not publish_attributes_folder: + publish_attributes_folder = hou.FolderParmTemplate( + "AYON_publish_attributes", + "Publish Attributes", + folder_type=hou.folderType.Simple + ) + attributes_folder.addParmTemplate(publish_attributes_folder) + + # Create Advanced Folder + advanced_folder = parm_group.find("AYON_advanced") + if not advanced_folder: + advanced_folder = hou.FolderParmTemplate( + "AYON_advanced", + "Advanced", + folder_type=hou.folderType.Collapsible + ) + ayon_folder.addParmTemplate(advanced_folder) + + # Get the creator and publish attribute definitions so that we can + # generate matching Houdini parm types, including label, tooltips, etc. + creator_attribute_defs = created_instance.creator_attributes.attr_defs + for attr_def in creator_attribute_defs: + parm_template = attribute_def_to_parm_template( + attr_def, + key=f"AYON_creator_attributes_{attr_def.key}") + + name = parm_template.name() + existing = parm_group.find(name) + if existing: + # Remove from Parm Group - and also from the folder itself + # because that reference is not live anymore to the parm + # group itself so will still have the parm template + parm_group.remove(name) + creator_attributes_folder.setParmTemplates([ + t for t in creator_attributes_folder.parmTemplates() + if t.name() != name + ]) + creator_attributes_folder.addParmTemplate(parm_template) + + for plugin_name, plugin_attr_values in created_instance.publish_attributes.items(): + prefix = f"AYON_publish_attributes_{plugin_name}_" + for attr_def in plugin_attr_values.attr_defs: + parm_template = attribute_def_to_parm_template( + attr_def, + key=f"{prefix}{attr_def.key}" + ) + + name = parm_template.name() + existing = parm_group.find(name) + if existing: + # Remove from Parm Group - and also from the folder itself + # because that reference is not live anymore to the parm + # group itself so will still have the parm template + parm_group.remove(name) + publish_attributes_folder.setParmTemplates([ + t for t in publish_attributes_folder.parmTemplates() + if t.name() != name + ]) + publish_attributes_folder.addParmTemplate(parm_template) + + # Define the product types picker options + node_type_product_types: NodeTypeProductTypes = ( + self.node_type_product_types.get( + node.type().name(), self.node_type_product_types_default + )) + product_type_kwargs = { + "menu_items": node_type_product_types.product_types, + "default_value": (node_type_product_types.default,) + } + if node_type_product_types.strict: + product_type_kwargs["menu_type"] = hou.menuType.Normal + else: + product_type_kwargs["menu_type"] = hou.menuType.StringReplace + + # Add the Folder Path, Task Name, Product Type, Variant, Product Name + # and Active state in Instance attributes + for attribute in [ + hou.StringParmTemplate( + "AYON_folderPath", "Folder Path", + num_components=1, + default_value=("$AYON_FOLDER_PATH",) + ), + hou.StringParmTemplate( + "AYON_task", "Task Name", + num_components=1, + default_value=("$AYON_TASK_NAME",) + ), + hou.StringParmTemplate( + "AYON_productType", "Product Type", + num_components=1, + **product_type_kwargs + ), + hou.StringParmTemplate( + "AYON_variant", "Variant", + num_components=1, + default_value=("$OS",) + ), + hou.StringParmTemplate( + "AYON_productName", "Product Name", + num_components=1, + # TODO: This default value should adhere more to AYON's + # product name templates. We might need to avoid making + # this field editable and set up callbacks so it evaluates + # dynamically using the `get_product_name` logic based off + # of other attributes on the node + default_value=('`chs("AYON_productType")``chs("AYON_variant")`',) + ), + hou.ToggleParmTemplate( + "AYON_active", "Active", + default_value=True + ) + ]: + if not parm_group.find(attribute.name()): + instance_attributes_folder.addParmTemplate(attribute) + + # Add the Creator Identifier and ID in advanced + for attribute in [ + hou.StringParmTemplate( + "AYON_id", "ID", + num_components=1, + default_value=(plugin.AYON_INSTANCE_ID,) + ), + hou.StringParmTemplate( + "AYON_creator_identifier", "Creator Identifier", + num_components=1, + default_value=(self.identifier,) + ), + ]: + if not parm_group.find(attribute.name()): + advanced_folder.addParmTemplate(attribute) + + # Ensure all folders are up-to-date if they had previously existed + # already + for folder in [ayon_folder, + attributes_folder, + instance_attributes_folder, + publish_attributes_folder, + creator_attributes_folder, + advanced_folder]: + if parm_group.find(folder.name()): + parm_group.replace(folder.name(), folder) # replace + node.setParmTemplateGroup(parm_group) + + def imprint(self, + created_instance: CreatedInstance, + values: dict, + update=False): + + # Do not ever write these into the node. + values.pop("instance_node", None) + values.pop("instance_id", None) + values.pop("families", None) + if not values: + return + + instance_node = hou.node(created_instance.get("instance_node")) + + # Update attribute definition parms + self.create_attribute_def_parms(instance_node, created_instance) + + # Creator attributes to parms + creator_attributes = values.pop("creator_attributes", {}) + parm_values = {} + for attr, value in creator_attributes.items(): + key = f"AYON_creator_attributes_{attr}" + parm_values[key] = value + + # Publish attributes to parms + publish_attributes = values.pop("publish_attributes", {}) + for plugin_name, plugin_attr_values in publish_attributes.items(): + for attr, value in plugin_attr_values.items(): + key = f"AYON_publish_attributes_{plugin_name}_{attr}" + parm_values[key] = value + + # The remainder attributes are stored without any prefixes + # Prefix all values with `AYON_` + parm_values.update( + {f"AYON_{key}": value for key, value in values.items()} + ) + + set_values(instance_node, parm_values) + + # TODO: Update defaults for Product Name + # on the node so Houdini doesn't show them bold after save + + def get_publish_families(self): + return [self.product_type] + + def get_instance_attr_defs(self): + """get instance attribute definitions. + + Attributes defined in this method are exposed in + publish tab in the publisher UI. + """ + + render_target_items = { + "local": "Local machine rendering", + "local_no_render": "Use existing frames (local)", + "farm": "Farm Rendering", + } + + return [ + # TODO: This review toggle may be odd - because a regular + # pointcache creator does not have the review toggle but with + # this it does. Is that confusing? Can we make it so that `review` + # only shows when relevant? + BoolDef("review", + label="Review", + tooltip="Mark as reviewable", + default=True), + # TODO: This render target isn't actually 'accurate' for render + # instances that should also allow a split export+render workflow. + EnumDef("render_target", + items=render_target_items, + label="Render target", + default=self.render_target) + ] + + def get_pre_create_attr_defs(self): + return [ + TextDef("productType", + label="Product Type", + tooltip=( + "Publish product type. When set to " + f"{self.USE_DEFAULT_PRODUCT_TYPE} it will use " + "the project settings to define the default value."), + default=self.USE_DEFAULT_PRODUCT_TYPE) + ] + + def _get_product_name_dynamic( + self, + project_name, + folder_entity, + task_entity, + variant, + product_type, + host_name=None, + instance=None + ): + if host_name is None: + host_name = self.create_context.host_name + + task_name = task_type = None + if task_entity: + task_name = task_entity["name"] + task_type = task_entity["taskType"] + + dynamic_data = self.get_dynamic_data( + project_name, + folder_entity, + task_entity, + variant, + host_name, + instance + ) + + return get_product_name( + project_name, + task_name, + task_type, + host_name, + product_type, + variant, + dynamic_data=dynamic_data, + project_settings=self.project_settings + ) + + def get_network_categories(self): + # Do not show anywhere in TAB menus since it applies to existing nodes + return [] diff --git a/client/ayon_houdini/plugins/publish/collect_farm_instances.py b/client/ayon_houdini/plugins/publish/collect_farm_instances.py index f14ff65518..856a4e50aa 100644 --- a/client/ayon_houdini/plugins/publish/collect_farm_instances.py +++ b/client/ayon_houdini/plugins/publish/collect_farm_instances.py @@ -11,7 +11,8 @@ class CollectFarmInstances(plugin.HoudiniInstancePlugin): "redshift_rop", "arnold_rop", "vray_rop", - "usdrender"] + "usdrender", + "generic"] targets = ["local", "remote"] label = "Collect farm instances" diff --git a/client/ayon_houdini/plugins/publish/collect_frames.py b/client/ayon_houdini/plugins/publish/collect_frames.py index a442e74835..02fe1f7c0a 100644 --- a/client/ayon_houdini/plugins/publish/collect_frames.py +++ b/client/ayon_houdini/plugins/publish/collect_frames.py @@ -16,7 +16,7 @@ class CollectFrames(plugin.HoudiniInstancePlugin): label = "Collect Frames" families = ["camera", "vdbcache", "imagesequence", "ass", "redshiftproxy", "review", "pointcache", "fbx", - "model"] + "model", "rop"] def process(self, instance): diff --git a/client/ayon_houdini/plugins/publish/collect_houdini_batch_families.py b/client/ayon_houdini/plugins/publish/collect_houdini_batch_families.py new file mode 100644 index 0000000000..52f965db08 --- /dev/null +++ b/client/ayon_houdini/plugins/publish/collect_houdini_batch_families.py @@ -0,0 +1,19 @@ +import pyblish.api + + +class CollectNoProductTypeFamilyGeneric(pyblish.api.InstancePlugin): + """Collect data for caching to Deadline.""" + + order = pyblish.api.CollectorOrder - 0.49 + families = ["generic"] + hosts = ["houdini"] + targets = ["local", "remote"] + label = "Collect Data for Cache" + + def process(self, instance): + # Do not allow `productType` to creep into the pyblish families + # so that e.g. any regular plug-ins for `pointcache` or alike do + # not trigger. + instance.data["family"] = "generic" + # TODO: Do not add the dynamic 'rop' family in the collector? + instance.data["families"] = ["generic", "rop"] \ No newline at end of file diff --git a/client/ayon_houdini/plugins/publish/extract_rop.py b/client/ayon_houdini/plugins/publish/extract_rop.py index 62a38c0b93..7bea729e9b 100644 --- a/client/ayon_houdini/plugins/publish/extract_rop.py +++ b/client/ayon_houdini/plugins/publish/extract_rop.py @@ -14,7 +14,7 @@ class ExtractROP(plugin.HoudiniExtractorPlugin): order = pyblish.api.ExtractorOrder families = ["abc", "camera", "bgeo", "pointcache", "fbx", - "vdbcache", "ass", "redshiftproxy", "mantraifd"] + "vdbcache", "ass", "redshiftproxy", "mantraifd", "rop"] targets = ["local", "remote"] def process(self, instance: pyblish.api.Instance): @@ -22,8 +22,6 @@ def process(self, instance: pyblish.api.Instance): self.log.debug("Should be processed on farm, skipping.") return - rop_node = hou.node(instance.data["instance_node"]) - files = instance.data["frames"] first_file = files[0] if isinstance(files, (list, tuple)) else files _, ext = splitext( @@ -33,9 +31,11 @@ def process(self, instance: pyblish.api.Instance): ) ext = ext.lstrip(".") - self.log.debug(f"Rendering {rop_node.path()} to {first_file}..") - - render_rop(rop_node) + creator_attributes = instance.data.get("creator_attributes", {}) + if creator_attributes.get("render_target", "local") == "local": + rop_node = hou.node(instance.data.get("instance_node")) + self.log.debug(f"Rendering {rop_node.path()} to {first_file}..") + render_rop(rop_node) self.validate_expected_frames(instance) # In some cases representation name is not the the extension diff --git a/client/ayon_houdini/startup/OPmenu.xml b/client/ayon_houdini/startup/OPmenu.xml index 5637d2cf6a..56bf9a7f80 100644 --- a/client/ayon_houdini/startup/OPmenu.xml +++ b/client/ayon_houdini/startup/OPmenu.xml @@ -5,6 +5,24 @@ + + + + + hasattr(kwargs["node"], "render") + + + + + + + opmenu.unsynchronize diff --git a/client/ayon_houdini/startup/scripts/OnCreated.py b/client/ayon_houdini/startup/scripts/OnCreated.py new file mode 100644 index 0000000000..e73655207c --- /dev/null +++ b/client/ayon_houdini/startup/scripts/OnCreated.py @@ -0,0 +1,18 @@ +# Any code here will run on any node being created in Houdini +# As such, preferably the code here should run fast to avoid slowing down node +# creation. Note: It does not trigger on existing nodes for scene open nor on +# node copy-pasting. +from ayon_core.lib import env_value_to_bool, is_dev_mode_enabled +from ayon_houdini.api import node_wrap + + +# Allow easier development by automatic reloads +# TODO: remove this +if is_dev_mode_enabled(): + import importlib + importlib.reload(node_wrap) + + +if env_value_to_bool("AYON_HOUDINI_AUTOCREATE", default=True): + node = kwargs["node"] + node_wrap.autocreate_publishable(node) From b5d5e09a1fe507153823566f0842bf8915e22854 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 4 Jul 2024 00:10:42 +0200 Subject: [PATCH 02/15] Add argument to docstring --- client/ayon_houdini/plugins/create/create_generic.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/ayon_houdini/plugins/create/create_generic.py b/client/ayon_houdini/plugins/create/create_generic.py index 343934b173..0826d69c6b 100644 --- a/client/ayon_houdini/plugins/create/create_generic.py +++ b/client/ayon_houdini/plugins/create/create_generic.py @@ -30,6 +30,8 @@ def attribute_def_to_parm_template(attribute_def, key=None): Arguments: attribute_def (AbstractAttrDef): Attribute Definition. + key (Optional[str]): Name for the parm template. + If not provided defaults to the Attribute Definition's key. Returns: hou.ParmTemplate: Parm Template matching the Attribute Definition. From 5b6f0ecfdceb228fa00d80c03af4e86e2e0a3c9a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 11 Jul 2024 22:03:17 +0200 Subject: [PATCH 03/15] Add publish button --- .../ayon_houdini/plugins/create/create_generic.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/client/ayon_houdini/plugins/create/create_generic.py b/client/ayon_houdini/plugins/create/create_generic.py index 0826d69c6b..3e88d5101d 100644 --- a/client/ayon_houdini/plugins/create/create_generic.py +++ b/client/ayon_houdini/plugins/create/create_generic.py @@ -436,6 +436,19 @@ def create_attribute_def_parms(self, ayon_folder = hou.FolderParmTemplate("folder", "AYON") parm_group.addParmTemplate(ayon_folder) + # Add publish button + publish_button_parm = hou.ButtonParmTemplate( + "AYON_self_publish", + "Publish", + help="Directly publishes only this node and any inputs with a " + "comment entered in a popup prompt. All other instances will " + "be disabled in the publish.", + script_callback="from ayon_houdini.api.lib import " + "self_publish; self_publish()", + script_callback_language=hou.scriptLanguage.Python, + ) + ayon_folder.addParmTemplate(publish_button_parm) + attributes_folder = parm_group.find("AYON_attributes") if not attributes_folder: attributes_folder = hou.FolderParmTemplate( From 46da010eff673d2d44db92ced764a5d4bd9622d0 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 11 Jul 2024 22:05:50 +0200 Subject: [PATCH 04/15] Add help --- client/ayon_houdini/plugins/create/create_generic.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_houdini/plugins/create/create_generic.py b/client/ayon_houdini/plugins/create/create_generic.py index 3e88d5101d..b52128b475 100644 --- a/client/ayon_houdini/plugins/create/create_generic.py +++ b/client/ayon_houdini/plugins/create/create_generic.py @@ -587,7 +587,8 @@ def create_attribute_def_parms(self, ), hou.ToggleParmTemplate( "AYON_active", "Active", - default_value=True + default_value=True, + help="Whether the instance is enabled for publishing." ) ]: if not parm_group.find(attribute.name()): From 6934ba61b64e6f2465c9cead1d4b30f27fe95812 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 20 Sep 2024 06:52:38 +0200 Subject: [PATCH 05/15] Fix refactored merge conflict, skip non local renders like "local_no_render" to use existing frames --- client/ayon_houdini/plugins/publish/extract_rop.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/client/ayon_houdini/plugins/publish/extract_rop.py b/client/ayon_houdini/plugins/publish/extract_rop.py index 314528e421..97ee1ed52b 100644 --- a/client/ayon_houdini/plugins/publish/extract_rop.py +++ b/client/ayon_houdini/plugins/publish/extract_rop.py @@ -31,7 +31,14 @@ def process(self, instance: pyblish.api.Instance): ) ext = ext.lstrip(".") - self.render_rop(instance) + render_target: str = instance.data.get("creator_attributes", {}).get( + "render_target", "local") + if render_target == "local": + self.render_rop(instance) + else: + self.log.debug(f"Skipping render because render target is not " + f"'local' but '{render_target}") + self.validate_expected_frames(instance) # In some cases representation name is not the the extension From 3791df1e5933bf44394b197ec83bf88c60231cda Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 20 Sep 2024 06:57:29 +0200 Subject: [PATCH 06/15] Do not error on existing `ayon_self_publish` parm but log a warning --- client/ayon_houdini/api/lib.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/client/ayon_houdini/api/lib.py b/client/ayon_houdini/api/lib.py index f5b7b9e107..b9fb5ea0b6 100644 --- a/client/ayon_houdini/api/lib.py +++ b/client/ayon_houdini/api/lib.py @@ -1020,11 +1020,21 @@ def self_publish(): def add_self_publish_button(node): """Adds a self publish button to the rop node.""" + + parm_name = "ayon_self_publish" label = os.environ.get("AYON_MENU_LABEL") or "AYON" + template = node.parmTemplateGroup() + existing = template.find(parm_name) + if existing: + log.warning( + f"Self publish parm already found on {node.path()}. " + "Skipping creation..." + ) + return button_parm = hou.ButtonParmTemplate( - "ayon_self_publish", + parm_name, "{} Publish".format(label), script_callback="from ayon_houdini.api.lib import " "self_publish; self_publish()", @@ -1032,7 +1042,6 @@ def add_self_publish_button(node): join_with_next=True ) - template = node.parmTemplateGroup() template.insertBefore((0,), button_parm) node.setParmTemplateGroup(template) From 61268886e2858ce1d2ef3c4a50557ef57e2848b0 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 20 Sep 2024 06:59:50 +0200 Subject: [PATCH 07/15] Cosmetics --- client/ayon_houdini/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_houdini/api/lib.py b/client/ayon_houdini/api/lib.py index b9fb5ea0b6..5574bcc2e2 100644 --- a/client/ayon_houdini/api/lib.py +++ b/client/ayon_houdini/api/lib.py @@ -1020,7 +1020,7 @@ def self_publish(): def add_self_publish_button(node): """Adds a self publish button to the rop node.""" - + parm_name = "ayon_self_publish" label = os.environ.get("AYON_MENU_LABEL") or "AYON" From c14710cae9943d26d0a6bdd5cb1b6c2e45c8164b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 20 Sep 2024 07:12:54 +0200 Subject: [PATCH 08/15] Elaborate TODO --- .../plugins/publish/collect_houdini_batch_families.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/ayon_houdini/plugins/publish/collect_houdini_batch_families.py b/client/ayon_houdini/plugins/publish/collect_houdini_batch_families.py index 52f965db08..f39452e34b 100644 --- a/client/ayon_houdini/plugins/publish/collect_houdini_batch_families.py +++ b/client/ayon_houdini/plugins/publish/collect_houdini_batch_families.py @@ -15,5 +15,8 @@ def process(self, instance): # so that e.g. any regular plug-ins for `pointcache` or alike do # not trigger. instance.data["family"] = "generic" - # TODO: Do not add the dynamic 'rop' family in the collector? + # TODO: Do not add the dynamic 'rop' family in the collector but + # set it in th Creator? Unfortunately that's currently not possible + # due to logic in the ayon-core `CollectFromCreateContext` that + # sets `family` to `productType`. instance.data["families"] = ["generic", "rop"] \ No newline at end of file From db57f1e2ce693c98a45bc25787441c19e7621fcd Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 3 Oct 2024 15:05:22 +0200 Subject: [PATCH 09/15] Cosmetics --- client/ayon_houdini/plugins/create/create_generic.py | 6 ++++-- client/ayon_houdini/startup/scripts/OnCreated.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/client/ayon_houdini/plugins/create/create_generic.py b/client/ayon_houdini/plugins/create/create_generic.py index b52128b475..6256220460 100644 --- a/client/ayon_houdini/plugins/create/create_generic.py +++ b/client/ayon_houdini/plugins/create/create_generic.py @@ -1,5 +1,5 @@ import dataclasses -from typing import Dict, List, Optional +from typing import Dict, List, Optional, TYPE_CHECKING from ayon_houdini.api import plugin from ayon_houdini.api.lib import ( @@ -11,7 +11,6 @@ ) from ayon_api import get_folder_by_path, get_task_by_name from ayon_core.lib import ( - AbstractAttrDef, BoolDef, NumberDef, EnumDef, @@ -24,6 +23,9 @@ import hou import json +if TYPE_CHECKING: + from ayon_core.lib import AbstractAttrDef # noqa: F401 + def attribute_def_to_parm_template(attribute_def, key=None): """AYON Attribute Definition to Houdini Parm Template. diff --git a/client/ayon_houdini/startup/scripts/OnCreated.py b/client/ayon_houdini/startup/scripts/OnCreated.py index e73655207c..829380909d 100644 --- a/client/ayon_houdini/startup/scripts/OnCreated.py +++ b/client/ayon_houdini/startup/scripts/OnCreated.py @@ -14,5 +14,5 @@ if env_value_to_bool("AYON_HOUDINI_AUTOCREATE", default=True): - node = kwargs["node"] + node = kwargs["node"] # noqa: F821 node_wrap.autocreate_publishable(node) From 21ab75ec5b0693ea2a2bce0d63bd96e01793a9ec Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 3 Oct 2024 15:25:02 +0200 Subject: [PATCH 10/15] Move logic around, so we don't need to import the plugin. TODO: Still need to expose these to settings --- client/ayon_houdini/api/node_wrap.py | 102 ++++++++++++++++-- .../plugins/create/create_generic.py | 95 +--------------- 2 files changed, 96 insertions(+), 101 deletions(-) diff --git a/client/ayon_houdini/api/node_wrap.py b/client/ayon_houdini/api/node_wrap.py index e484bb5265..17d37c4d1c 100644 --- a/client/ayon_houdini/api/node_wrap.py +++ b/client/ayon_houdini/api/node_wrap.py @@ -1,9 +1,100 @@ +import dataclasses +from typing import Dict, List, Optional import hou from ayon_core.pipeline import registered_host from ayon_core.pipeline.create import CreateContext +@dataclasses.dataclass +class NodeTypeProductTypes: + """Product type settings for a node type. + + Define the available product types the user can set on a ROP based on + node type. + + When 'strict' an enum attribute is created and the user can not type a + custom product type, otherwise a string attribute is + created with a menu right hand side to help pick a type but allow custom + types. + """ + product_types: List[str] + default: Optional[str] = None + strict: bool = True + + +# Re-usable defaults +GEO_PRODUCT_TYPES = NodeTypeProductTypes( + product_types=["pointcache", "model"], + default="pointcache" +) +FBX_PRODUCT_TYPES = NodeTypeProductTypes( + product_types=["fbx", "pointcache", "model"], + default="fbx" +) +FBX_ONLY_PRODUCT_TYPES = NodeTypeProductTypes( + product_types=["fbx"], + default="fbx" +) +USD_PRODUCT_TYPES = NodeTypeProductTypes( + product_types=["usd", "pointcache"], + default="usd" +) +COMP_PRODUCT_TYPES = NodeTypeProductTypes( + product_types=["imagesequence", "render"], + default="imagesequence" +) +REVIEW_PRODUCT_TYPES = NodeTypeProductTypes( + product_types=["review"], + default="review" +) +RENDER_PRODUCT_TYPES = NodeTypeProductTypes( + product_types=["render", "prerender"], + default="render" +) +GLTF_PRODUCT_TYPES = NodeTypeProductTypes( + product_types=["gltf"], + default="gltf" +) + +# TODO: Move this to project settings +NODE_TYPE_PRODUCT_TYPES: Dict[str, NodeTypeProductTypes] = { + "alembic": GEO_PRODUCT_TYPES, + "rop_alembic": GEO_PRODUCT_TYPES, + "geometry": GEO_PRODUCT_TYPES, + "rop_geometry": GEO_PRODUCT_TYPES, + "filmboxfbx": FBX_PRODUCT_TYPES, + "rop_fbx": FBX_PRODUCT_TYPES, + "usd": USD_PRODUCT_TYPES, + "usd_rop": USD_PRODUCT_TYPES, + "usdexport": USD_PRODUCT_TYPES, + "comp": COMP_PRODUCT_TYPES, + "opengl": REVIEW_PRODUCT_TYPES, + "arnold": RENDER_PRODUCT_TYPES, + "karma": RENDER_PRODUCT_TYPES, + "ifd": RENDER_PRODUCT_TYPES, + "usdrender": RENDER_PRODUCT_TYPES, + "usdrender_rop": RENDER_PRODUCT_TYPES, + "vray_renderer": RENDER_PRODUCT_TYPES, + "labs::karma::2.0": RENDER_PRODUCT_TYPES, + "kinefx::rop_fbxanimoutput": FBX_ONLY_PRODUCT_TYPES, + "kinefx::rop_fbxcharacteroutput": FBX_ONLY_PRODUCT_TYPES, + "kinefx::rop_gltfcharacteroutput": GLTF_PRODUCT_TYPES, + "rop_gltf": GLTF_PRODUCT_TYPES +} + +NODE_TYPE_PRODUCT_TYPES_DEFAULT = NodeTypeProductTypes( + product_types=list(sorted( + { + "ass", "pointcache", "model", "render", "camera", + "imagesequence", "review", "vdbcache", "fbx" + })), + default="pointcache", + strict=False +) +AUTO_CREATE_NODE_TYPES = set(NODE_TYPE_PRODUCT_TYPES.keys()) + + def make_publishable(node): # TODO: Can we make this imprinting much faster? Unfortunately # CreateContext initialization is very slow. @@ -20,17 +111,6 @@ def make_publishable(node): ) -# TODO: Move this choice of automatic 'imprint' to settings so studio can -# configure which nodes should get automatically imprinted on creation -# TODO: Do not import and reload the creator plugin file -from ayon_houdini.plugins.create import create_generic -import importlib -importlib.reload(create_generic) -AUTO_CREATE_NODE_TYPES = set( - create_generic.CreateHoudiniGeneric.node_type_product_types.keys() -) - - def autocreate_publishable(node): # For now only consider RopNode if not isinstance(node, hou.RopNode): diff --git a/client/ayon_houdini/plugins/create/create_generic.py b/client/ayon_houdini/plugins/create/create_generic.py index 6256220460..48a1df5d8e 100644 --- a/client/ayon_houdini/plugins/create/create_generic.py +++ b/client/ayon_houdini/plugins/create/create_generic.py @@ -1,7 +1,6 @@ -import dataclasses -from typing import Dict, List, Optional, TYPE_CHECKING +from typing import Optional, TYPE_CHECKING -from ayon_houdini.api import plugin +from ayon_houdini.api import plugin, node_wrap from ayon_houdini.api.lib import ( lsattr, read ) @@ -151,58 +150,6 @@ def set_values(node: "hou.OpNode", values: dict): parm.set(value) -@dataclasses.dataclass -class NodeTypeProductTypes: - """Product type settings for a node type. - - Define the available product types the user can set on a ROP based on - node type. - - When 'strict' an enum attribute is created and the user can not type a - custom product type, otherwise a string attribute is - created with a menu right hand side to help pick a type but allow custom - types. - """ - product_types: List[str] - default: Optional[str] = None - strict: bool = True - - -# Re-usable defaults -GEO_PRODUCT_TYPES = NodeTypeProductTypes( - product_types=["pointcache", "model"], - default="pointcache" -) -FBX_PRODUCT_TYPES = NodeTypeProductTypes( - product_types=["fbx", "pointcache", "model"], - default="fbx" -) -FBX_ONLY_PRODUCT_TYPES = NodeTypeProductTypes( - product_types=["fbx"], - default="fbx" -) -USD_PRODUCT_TYPES = NodeTypeProductTypes( - product_types=["usd", "pointcache"], - default="usd" -) -COMP_PRODUCT_TYPES = NodeTypeProductTypes( - product_types=["imagesequence", "render"], - default="imagesequence" -) -REVIEW_PRODUCT_TYPES = NodeTypeProductTypes( - product_types=["review"], - default="review" -) -RENDER_PRODUCT_TYPES = NodeTypeProductTypes( - product_types=["render", "prerender"], - default="render" -) -GLTF_PRODUCT_TYPES = NodeTypeProductTypes( - product_types=["gltf"], - default="gltf" -) - - class CreateHoudiniGeneric(plugin.HoudiniCreator): """Generic creator to ingest arbitrary products""" @@ -222,40 +169,8 @@ class CreateHoudiniGeneric(plugin.HoudiniCreator): default_variants = ["Main", USE_DEFAULT_NODE_NAME] # TODO: Move this to project settings - node_type_product_types: Dict[str, NodeTypeProductTypes] = { - "alembic": GEO_PRODUCT_TYPES, - "rop_alembic": GEO_PRODUCT_TYPES, - "geometry": GEO_PRODUCT_TYPES, - "rop_geometry": GEO_PRODUCT_TYPES, - "filmboxfbx": FBX_PRODUCT_TYPES, - "rop_fbx": FBX_PRODUCT_TYPES, - "usd": USD_PRODUCT_TYPES, - "usd_rop": USD_PRODUCT_TYPES, - "usdexport": USD_PRODUCT_TYPES, - "comp": COMP_PRODUCT_TYPES, - "opengl": REVIEW_PRODUCT_TYPES, - "arnold": RENDER_PRODUCT_TYPES, - "karma": RENDER_PRODUCT_TYPES, - "ifd": RENDER_PRODUCT_TYPES, - "usdrender": RENDER_PRODUCT_TYPES, - "usdrender_rop": RENDER_PRODUCT_TYPES, - "vray_renderer": RENDER_PRODUCT_TYPES, - "labs::karma::2.0": RENDER_PRODUCT_TYPES, - "kinefx::rop_fbxanimoutput": FBX_ONLY_PRODUCT_TYPES, - "kinefx::rop_fbxcharacteroutput": FBX_ONLY_PRODUCT_TYPES, - "kinefx::rop_gltfcharacteroutput": GLTF_PRODUCT_TYPES, - "rop_gltf": GLTF_PRODUCT_TYPES - } - - node_type_product_types_default = NodeTypeProductTypes( - product_types=list(sorted( - { - "ass", "pointcache", "model", "render", "camera", - "imagesequence", "review", "vdbcache", "fbx" - })), - default="pointcache", - strict=False - ) + node_type_product_types = node_wrap.NODE_TYPE_PRODUCT_TYPES + node_type_product_types_default = node_wrap.NODE_TYPE_PRODUCT_TYPES_DEFAULT def get_detail_description(self): return "Publish any ROP node." @@ -541,7 +456,7 @@ def create_attribute_def_parms(self, publish_attributes_folder.addParmTemplate(parm_template) # Define the product types picker options - node_type_product_types: NodeTypeProductTypes = ( + node_type_product_types: node_wrap.NodeTypeProductTypes = ( self.node_type_product_types.get( node.type().name(), self.node_type_product_types_default )) From adf92366e667eb11d0865751758d78dcc95cadff Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 3 Oct 2024 22:12:53 +0200 Subject: [PATCH 11/15] Fix 0 and 1 integer values turning into `bool` incorrectly - now only convert `Toggle` parms. Also, add a TODO --- client/ayon_houdini/api/lib.py | 3 +++ client/ayon_houdini/plugins/create/create_generic.py | 10 ++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/client/ayon_houdini/api/lib.py b/client/ayon_houdini/api/lib.py index 5574bcc2e2..7c01b245d0 100644 --- a/client/ayon_houdini/api/lib.py +++ b/client/ayon_houdini/api/lib.py @@ -370,6 +370,9 @@ def read(node): except json.JSONDecodeError: # not a json pass + elif parameter.parmTemplate().type() == hou.parmTemplateType.Toggle: + # Toggles should be returned as boolean instead of int of 1 or 0 + value = bool(value) data[parameter.name()] = value return data diff --git a/client/ayon_houdini/plugins/create/create_generic.py b/client/ayon_houdini/plugins/create/create_generic.py index 48a1df5d8e..83ed07bd90 100644 --- a/client/ayon_houdini/plugins/create/create_generic.py +++ b/client/ayon_houdini/plugins/create/create_generic.py @@ -280,10 +280,12 @@ def collect_instances(self): creator_attributes = {} for key, value in dict(node_data).items(): if key.startswith("publish_attributes_"): - if value == 0 or value == 1: - value = bool(value) - plugin_name, plugin_key = key[len("publish_attributes_"):].split("_", 1) - publish_attributes.setdefault(plugin_name, {})[plugin_key] = value + # TODO: Technically this isn't entirely safe. We are + # splitting after the first `_` after + # `publish_attributes_` with the assumption that the + # plug-in class name never contains an underscore. + plugin_name, plugin_key = key[len("publish_attributes_"):].split("_", 1) # noqa: E501 + publish_attributes.setdefault(plugin_name, {})[plugin_key] = value # noqa: E501 del node_data[key] # remove from original elif key.startswith("creator_attributes_"): creator_key = key[len("creator_attributes_"):] From ddcf9082f56238d952241f4a7a6f8a1789ea2ebd Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 4 Oct 2024 11:58:12 +0200 Subject: [PATCH 12/15] Update client/ayon_houdini/api/lib.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_houdini/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_houdini/api/lib.py b/client/ayon_houdini/api/lib.py index 7c01b245d0..57a13d934e 100644 --- a/client/ayon_houdini/api/lib.py +++ b/client/ayon_houdini/api/lib.py @@ -370,7 +370,7 @@ def read(node): except json.JSONDecodeError: # not a json pass - elif parameter.parmTemplate().type() == hou.parmTemplateType.Toggle: + elif parameter.parmTemplate().type() is hou.parmTemplateType.Toggle: # Toggles should be returned as boolean instead of int of 1 or 0 value = bool(value) data[parameter.name()] = value From 41dfc254d65844d196f6395d2424c483030f8449 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 4 Oct 2024 13:36:51 +0200 Subject: [PATCH 13/15] Fix auto-create for LABS Karma node --- client/ayon_houdini/api/node_wrap.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/client/ayon_houdini/api/node_wrap.py b/client/ayon_houdini/api/node_wrap.py index 17d37c4d1c..c0a7fa60c3 100644 --- a/client/ayon_houdini/api/node_wrap.py +++ b/client/ayon_houdini/api/node_wrap.py @@ -112,10 +112,12 @@ def make_publishable(node): def autocreate_publishable(node): - # For now only consider RopNode - if not isinstance(node, hou.RopNode): + # For now only consider RopNode; the LABS Karma node is the odd one out + # here because it's not a RopNode but a LopNode but with an embedded + # RopNode. So we will allow only that for now. + node_type = node.type().name() + if not isinstance(node, hou.RopNode) and node_type != "labs::karma::2.0": return - node_type = node.type().name() if node_type in AUTO_CREATE_NODE_TYPES: make_publishable(node) From fae1a897f5a80bb794124e87e46963d10ea1b3e9 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 4 Oct 2024 13:37:26 +0200 Subject: [PATCH 14/15] Add todo --- client/ayon_houdini/api/node_wrap.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_houdini/api/node_wrap.py b/client/ayon_houdini/api/node_wrap.py index c0a7fa60c3..2730dbf191 100644 --- a/client/ayon_houdini/api/node_wrap.py +++ b/client/ayon_houdini/api/node_wrap.py @@ -115,6 +115,7 @@ def autocreate_publishable(node): # For now only consider RopNode; the LABS Karma node is the odd one out # here because it's not a RopNode but a LopNode but with an embedded # RopNode. So we will allow only that for now. + # TODO: Make this more explicit so we don't need the odd edge cases node_type = node.type().name() if not isinstance(node, hou.RopNode) and node_type != "labs::karma::2.0": return From 3f70401388300cbb40c28c35e3dfae0cd6e58e2b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 4 Oct 2024 13:43:49 +0200 Subject: [PATCH 15/15] Shush ruff linter --- client/ayon_houdini/plugins/create/create_generic.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_houdini/plugins/create/create_generic.py b/client/ayon_houdini/plugins/create/create_generic.py index 83ed07bd90..a7c18c335f 100644 --- a/client/ayon_houdini/plugins/create/create_generic.py +++ b/client/ayon_houdini/plugins/create/create_generic.py @@ -1,4 +1,4 @@ -from typing import Optional, TYPE_CHECKING +from typing import TYPE_CHECKING from ayon_houdini.api import plugin, node_wrap from ayon_houdini.api.lib import ( @@ -23,6 +23,7 @@ import json if TYPE_CHECKING: + from typing import Optional # noqa: F401 from ayon_core.lib import AbstractAttrDef # noqa: F401