diff --git a/openpype/hosts/houdini/plugins/publish/validate_fbx_hierarchy_path.py b/openpype/hosts/houdini/plugins/publish/validate_fbx_hierarchy_path.py deleted file mode 100644 index be73ccd223..0000000000 --- a/openpype/hosts/houdini/plugins/publish/validate_fbx_hierarchy_path.py +++ /dev/null @@ -1,235 +0,0 @@ -# -*- coding: utf-8 -*- -"""Validate path attribute for all primitives. - -Validators are used to verify the work of artists, -by running some checks which automates the approval process. - -It's almost the same as -'validate_primitive_hierarchy_paths.py' -however this one includes extra comments for demonstration. - -FYI, path for fbx behaves a little differently. -In maya terms: -in Filmbox FBX: it sets the name of the object -in Alembic ROP: it sets the name of the shape - -This plugin is part of publish process guide. -""" - -import pyblish.api -from openpype.pipeline import ( - PublishValidationError, - OptionalPyblishPluginMixin -) -from openpype.pipeline.publish import ( - ValidateContentsOrder, - RepairAction, -) -from openpype.hosts.houdini.api.action import ( - SelectInvalidAction, - SelectROPAction, -) - -import hou - - -# Each validation can have a single repair action -# which calls the repair method -class AddDefaultPathAction(RepairAction): - label = "Add a default path" - icon = "mdi.pencil-plus-outline" - - -class ValidateFBXPrimitiveHierarchyPaths(pyblish.api.InstancePlugin, - OptionalPyblishPluginMixin): - """Validate all primitives build hierarchy from attribute - when enabled. - - The name of the attribute must exist on the prims and have the - same name as Build Hierarchy from Attribute's `Path Attribute` - value on the FilmBox node. - This validation enables 'Build Hierarchy from Attribute' - by default. - """ - - families = ["fbx"] - hosts = ["houdini"] - label = "Validate Prims Hierarchy Path (FBX)" - - # Usually you will use this value as default - order = ValidateContentsOrder + 0.1 - - # Validation can have as many actions as you want - # all of these actions are defined in a seperate place - # unlike the repair action - actions = [SelectInvalidAction, AddDefaultPathAction, - SelectROPAction] - - # 'OptionalPyblishPluginMixin' adds the functionality to - # enable/disable plugins, It requires adding new settings. - optional = True - - # overrides InstancePlugin.process() - def process(self, instance): - invalid = self.get_invalid(instance) - if invalid: - nodes = [n.path() for n in invalid] - raise PublishValidationError( - "See log for details. " - "Invalid nodes: {0}".format(nodes), - title=self.label - ) - - # This method was named get_invalid as a convention - # it's also used by SelectInvalidAction to select - # the returned nodes - @classmethod - def get_invalid(cls, instance): - - output_node = instance.data.get("output_node") - rop_node = hou.node(instance.data["instance_node"]) - - if output_node is None: - cls.log.error( - "SOP Output node in '%s' does not exist. " - "Ensure a valid SOP output path is set.", - rop_node.path() - ) - - return [rop_node] - - build_from_path = rop_node.parm("buildfrompath").eval() - if not build_from_path: - cls.log.debug( - "Filmbox FBX has 'Build from Path' disabled. " - "Enbaling it as default." - ) - rop_node.parm("buildfrompath").set(1) - - path_attr = rop_node.parm("pathattrib").eval() - if not path_attr: - cls.log.debug( - "Filmbox FBX node has no Path Attribute" - "value set, setting it to 'path' as default." - ) - rop_node.parm("pathattrib").set("path") - - cls.log.debug("Checking for attribute: %s", path_attr) - - # Use current frame if "frameStart" doesn't exist - # This only happens when ""trange" is 0 - frame = instance.data.get("frameStart", hou.intFrame()) - - # Get Geo at that frame - geo = output_node.geometryAtFrame(frame) - if not geo: - cls.log.warning("No geometry found," - " skipping check..") - return - - # If there are no primitives on the current frame then - # we can't check whether the path names are correct. - # So we'll just issue a warning that the check can't - # be done consistently and skip validation. - - if len(geo.iterPrims()) == 0: - cls.log.warning( - "No primitives found on current frame." - " Validation for primitive hierarchy" - " paths will be skipped," - " thus can't be validated." - ) - return - - # Check if there are any values for the primitives - attrib = geo.findPrimAttrib(path_attr) - if not attrib: - cls.log.info( - "Geometry Primitives are missing " - "path attribute: `%s`", path_attr - ) - return [output_node] - - # Ensure at least a single string value is present - if not attrib.strings(): - cls.log.info( - "Primitive path attribute has no " - "string values: %s", path_attr - ) - return [output_node] - - paths = geo.primStringAttribValues(path_attr) - # Ensure all primitives are set to a valid path - # Collect all invalid primitive numbers - invalid_prims = [i for i, path in enumerate(paths) if not path] - if invalid_prims: - num_prims = len(geo.iterPrims()) # faster than len(geo.prims()) - cls.log.info( - "Prims have no value for attribute `%s` " - "(%s of %s prims)", - path_attr, len(invalid_prims), num_prims - ) - return [output_node] - - # what repair action expects to find and call - @classmethod - def repair(cls, instance): - """Add a default path attribute Action. - - It is a helper action more than a repair action, - used to add a default single value for the path. - """ - - rop_node = hou.node(instance.data["instance_node"]) - # I'm doing so because an artist may change output node - # before clicking the button. - output_node = rop_node.parm("startnode").evalAsNode() - - if not output_node: - cls.log.debug( - "Action isn't performed, invalid SOP Path on %s", - rop_node - ) - return - - # This check to prevent the action from running multiple times. - # git_invalid only returns [output_node] when - # path attribute is the problem - if cls.get_invalid(instance) != [output_node]: - return - - path_attr = rop_node.parm("pathattrib").eval() - - path_node = output_node.parent().createNode("name", - "AUTO_PATH") - path_node.parm("attribname").set(path_attr) - path_node.parm("name1").set('`opname("..")`_GEO') - - cls.log.debug( - "'%s' was created. It adds '%s' with a default" - " single value", path_node, path_attr - ) - - path_node.setGenericFlag(hou.nodeFlag.DisplayComment, True) - path_node.setComment( - 'Auto path node was created automatically by ' - '"Add a default path attribute"' - '\nFeel free to modify or replace it.' - ) - - if output_node.type().name() in ["null", "output"]: - # Connect before - path_node.setFirstInput(output_node.input(0)) - path_node.moveToGoodPosition() - output_node.setFirstInput(path_node) - output_node.moveToGoodPosition() - else: - # Connect after - path_node.setFirstInput(output_node) - rop_node.parm("startnode").set(path_node.path()) - path_node.moveToGoodPosition() - - cls.log.debug( - "SOP path on '%s' updated to new output node '%s'", - rop_node, path_node - ) diff --git a/openpype/hosts/houdini/plugins/publish/validate_mesh_is_static.py b/openpype/hosts/houdini/plugins/publish/validate_mesh_is_static.py deleted file mode 100644 index ac80fda537..0000000000 --- a/openpype/hosts/houdini/plugins/publish/validate_mesh_is_static.py +++ /dev/null @@ -1,129 +0,0 @@ -# -*- coding: utf-8 -*- -"""Validate mesh is static. - -This plugin is part of publish process guide. -""" - -import pyblish.api -from openpype.pipeline import PublishValidationError -from openpype.pipeline.publish import ( - ValidateContentsOrder, - RepairAction, -) -from openpype.hosts.houdini.api.action import ( - SelectInvalidAction, - SelectROPAction, -) - -import hou - - -# Each validation can have a single repair action -# which calls the repair method -class FreezeTimeAction(RepairAction): - label = "Freeze Time" - icon = "ei.pause-alt" - - -class ValidateMeshIsStatic(pyblish.api.InstancePlugin): - """Validate mesh is static. - - It checks if node is time dependant. - """ - - families = ["staticMesh"] - hosts = ["houdini"] - label = "Validate mesh is static" - - # Usually you will use this value as default - order = ValidateContentsOrder + 0.1 - - # Validation can have as many actions as you want - # all of these actions are defined in a seperate place - # unlike the repair action - actions = [FreezeTimeAction, SelectInvalidAction, - SelectROPAction] - - # overrides InstancePlugin.process() - def process(self, instance): - invalid = self.get_invalid(instance) - if invalid: - nodes = [n.path() for n in invalid] - raise PublishValidationError( - "See log for details. " - "Invalid nodes: {0}".format(nodes), - title=self.label - ) - - # This method was named get_invalid as a convention - # it's also used by SelectInvalidAction to select - # the returned nodes - @classmethod - def get_invalid(cls, instance): - - output_node = instance.data.get("output_node") - if output_node.isTimeDependent(): - cls.log.info("Mesh is not static!") - return [output_node] - - # what repair action expects to find and call - @classmethod - def repair(cls, instance): - """Adds a time shift node. - - It should kill time dependency. - """ - - rop_node = hou.node(instance.data["instance_node"]) - # I'm doing so because an artist may change output node - # before clicking the button. - output_node = rop_node.parm("startnode").evalAsNode() - - if not output_node: - cls.log.debug( - "Action isn't performed, invalid SOP Path on %s", - rop_node - ) - return - - # This check to prevent the action from running multiple times. - # git_invalid only returns [output_node] when - # path attribute is the problem - if cls.get_invalid(instance) != [output_node]: - return - - time_shift = output_node.parent().createNode("timeshift", - "freeze_time") - time_shift.parm("frame").deleteAllKeyframes() - - frame = instance.data.get("frameStart", hou.intFrame()) - time_shift.parm("frame").set(frame) - - cls.log.debug( - "'%s' was created. It will kill time dependency.", - time_shift - ) - - time_shift.setGenericFlag(hou.nodeFlag.DisplayComment, True) - time_shift.setComment( - 'This node was created automatically by ' - '"Freeze Time" Action' - '\nFeel free to modify or replace it.' - ) - - if output_node.type().name() in ["null", "output"]: - # Connect before - time_shift.setFirstInput(output_node.input(0)) - time_shift.moveToGoodPosition() - output_node.setFirstInput(time_shift) - output_node.moveToGoodPosition() - else: - # Connect after - time_shift.setFirstInput(output_node) - rop_node.parm("startnode").set(time_shift.path()) - time_shift.moveToGoodPosition() - - cls.log.debug( - "SOP path on '%s' updated to new output node '%s'", - rop_node, time_shift - ) diff --git a/openpype/hosts/houdini/plugins/publish/validate_unreal_staticmesh_naming.py b/openpype/hosts/houdini/plugins/publish/validate_unreal_staticmesh_naming.py new file mode 100644 index 0000000000..be450a0410 --- /dev/null +++ b/openpype/hosts/houdini/plugins/publish/validate_unreal_staticmesh_naming.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- +"""Validator for correct naming of Static Meshes.""" +import pyblish.api +from openpype.pipeline import ( + PublishValidationError, + OptionalPyblishPluginMixin +) +from openpype.pipeline.publish import ValidateContentsOrder + +from openpype.hosts.houdini.api.action import SelectInvalidAction + +import hou + + +class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): + """Validate name of Unreal Static Mesh + + This validator checks if output node name has a collision prefix: + - UBX + - UCP + - USP + - UCX + + This validator also checks if subset name is correct + - {static mesh prefix}_{Asset-Name}{Variant}. + + """ + + families = ["staticMesh"] + hosts = ["houdini"] + label = "Unreal Static Mesh Name (FBX)" + order = ValidateContentsOrder + 0.1 + actions = [SelectInvalidAction] + + optional = True + + @classmethod + def apply_settings(cls, project_settings, system_settings): + settings = ( + project_settings["houdini"]["create"]["CreateUnrealStaticMesh"] + ) + cls.collision_prefixes = settings["collision_prefixes"] + cls.static_mesh_prefix = settings["static_mesh_prefix"] + + def process(self, instance): + invalid = self.get_invalid(instance) + if invalid: + nodes = [n.path() for n in invalid if isinstance(n, hou.Node)] + raise PublishValidationError( + "See log for details. " + "Invalid nodes: {0}".format(nodes), + title=self.label + ) + + @classmethod + def get_invalid(cls, instance): + + invalid = [] + + rop_node = hou.node(instance.data["instance_node"]) + output_node = instance.data.get("output_node") + cls.log.debug(cls.collision_prefixes) + + # Check nodes names + if output_node.childTypeCategory() == hou.objNodeTypeCategory(): + for child in output_node.children(): + for prefix in cls.collision_prefixes: + if child.name().startswith(prefix): + invalid.append(child) + cls.log.error( + "Invalid name: Child node '%s' in '%s' " + "has a collision prefix '%s'" + , child.name(), output_node.path(), prefix + ) + break + else: + cls.log.debug(output_node.name()) + for prefix in cls.collision_prefixes: + if output_node.name().startswith(prefix): + invalid.append(output_node) + cls.log.error( + "Invalid name: output node '%s' " + "has a collision prefix '%s'" + , output_node.name(), prefix + ) + + # Check subset name + subset_name = "{}_{}{}".format( + cls.static_mesh_prefix, + instance.data["asset"], + instance.data.get("variant", "") + ) + + if instance.data.get("subset") != subset_name: + invalid.append(rop_node) + cls.log.error( + "Invalid subset name on rop node '%s' should be '%s'." + , rop_node.path(), subset_name + ) + + return invalid