Improve validation report + allow to select the invalid node

This commit is contained in:
Roy Nieterau 2023-04-19 23:43:47 +02:00
parent 97f13a169b
commit b3044398fc
3 changed files with 135 additions and 48 deletions

View file

@ -0,0 +1,46 @@
import pyblish.api
import hou
from openpype.pipeline.publish import get_errored_instances_from_context
class SelectInvalidAction(pyblish.api.Action):
"""Select invalid nodes in Maya when plug-in failed.
To retrieve the invalid nodes this assumes a static `get_invalid()`
method is available on the plugin.
"""
label = "Select invalid"
on = "failed" # This action is only available on a failed plug-in
icon = "search" # Icon from Awesome Icon
def process(self, context, plugin):
errored_instances = get_errored_instances_from_context(context)
# Apply pyblish.logic to get the instances for the plug-in
instances = pyblish.api.instances_by_plugin(errored_instances, plugin)
# Get the invalid nodes for the plug-ins
self.log.info("Finding invalid nodes..")
invalid = list()
for instance in instances:
invalid_nodes = plugin.get_invalid(instance)
if invalid_nodes:
if isinstance(invalid_nodes, (list, tuple)):
invalid.extend(invalid_nodes)
else:
self.log.warning("Plug-in returned to be invalid, "
"but has no selectable nodes.")
hou.clearAllSelected()
if invalid:
self.log.info("Selecting invalid nodes: {}".format(
", ".join(node.path() for node in invalid)
))
for node in invalid:
node.setSelected(True)
node.setCurrent(True)
else:
self.log.info("No invalid nodes found.")

View file

@ -1,21 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<root>
<error id="main">
<title>Scene setting</title>
<title>Invalid VDB</title>
<description>
## Invalid input node
## Invalid VDB output
All primitives of the output geometry must be VDBs, no other primitive
types are allowed. That means that regardless of the amount of VDBs in the
geometry it will have an equal amount of VDBs, points, primitives and
vertices since each VDB primitive is one point, one vertex and one VDB.
This validation only checks the geometry on the first frame of the export
frame range.
VDB input must have the same number of VDBs, points, primitives and vertices as output.
</description>
<detail>
### __Detailed Info__ (optional)
### Detailed Info
ROP node `{rop_path}` is set to export SOP path `{sop_path}`.
{message}
A VDB is an inherited type of Prim, holds the following data:
- Primitives: 1
- Points: 1
- Vertices: 1
- VDBs: 1
</detail>
</error>
</root>

View file

@ -1,7 +1,9 @@
# -*- coding: utf-8 -*-
import pyblish.api
import hou
from openpype.pipeline import PublishXmlValidationError
from openpype.hosts.houdini.api.action import SelectInvalidAction
def group_consecutive_numbers(nums):
@ -40,8 +42,13 @@ def group_consecutive_numbers(nums):
class ValidateVDBOutputNode(pyblish.api.InstancePlugin):
"""Validate that the node connected to the output node is of type VDB.
Regardless of the amount of VDBs create the output will need to have an
equal amount of VDBs, points, primitives and vertices
All primitives of the output geometry must be VDBs, no other primitive
types are allowed. That means that regardless of the amount of VDBs in the
geometry it will have an equal amount of VDBs, points, primitives and
vertices since each VDB primitive is one point, one vertex and one VDB.
This validation only checks the geometry on the first frame of the export
frame range for optimization purposes.
A VDB is an inherited type of Prim, holds the following data:
- Primitives: 1
@ -55,64 +62,91 @@ class ValidateVDBOutputNode(pyblish.api.InstancePlugin):
families = ["vdbcache"]
hosts = ["houdini"]
label = "Validate Output Node (VDB)"
actions = [SelectInvalidAction]
def process(self, instance):
invalid = self.get_invalid(instance)
if invalid:
invalid_nodes, message = self.get_invalid_with_message(instance)
if invalid_nodes:
raise PublishXmlValidationError(
self,
"Node connected to the output node is not of type VDB."
"Node connected to the output node is not of type VDB.",
formatting_data={
"message": message,
"rop_path": instance.data.get("instance_node"),
"sop_path": instance.data.get("output_node")
}
)
@classmethod
def get_invalid(cls, instance):
def get_invalid_with_message(cls, instance):
node = instance.data.get("output_node")
if node is None:
cls.log.error(
instance_node = instance.data.get("instance_node")
error = (
"SOP path is not correctly set on "
"ROP node '%s'." % instance.data.get("instance_node")
"ROP node `%s`." % instance_node
)
return [instance]
return [instance_node, error]
frame = instance.data.get("frameStart", 0)
node.cook(force=True, frame_range=(frame, frame))
geometry = node.geometryAtFrame(frame)
if geometry is None:
# No geometry data on this node, maybe the node hasn't cooked?
cls.log.error(
error = (
"SOP node has no geometry data. "
"Is it cooked? %s" % node.path()
)
return [node]
return [node, error]
prims = geometry.prims()
nr_of_prims = len(prims)
# All primitives must be hou.VDB
invalid_prims = []
for prim in prims:
if not isinstance(prim, hou.VDB):
invalid_prims.append(prim)
if invalid_prims:
# Log prim numbers as consecutive ranges so logging isn't very
# slow for large number of primitives
cls.log.error(
"Found non-VDB primitives for '{}', "
"primitive indices: {}".format(
node.path(),
", ".join(group_consecutive_numbers(
prim.number() for prim in invalid_prims
))
)
num_prims = geometry.intrinsicValue("primitivecount")
num_points = geometry.intrinsicValue("pointcount")
if num_prims == 0 and num_points == 0:
# Since we are only checking the first frame it doesn't mean there
# won't be VDB prims in a few frames. As such we'll assume for now
# the user knows what he or she is doing
cls.log.warning(
"SOP node `{}` has no primitives on start frame {}. "
"Validation is skipped and it is assumed elsewhere in the "
"frame range VDB prims and only VDB prims will exist."
"".format(node.path(), int(frame))
)
return [instance]
return [None, None]
nr_of_points = len(geometry.points())
if nr_of_points != nr_of_prims:
cls.log.error("The number of primitives and points do not match")
return [instance]
num_vdb_prims = geometry.countPrimType(hou.primType.VDB)
cls.log.debug("Detected {} VDB primitives".format(num_vdb_prims))
if num_prims != num_vdb_prims:
# There's at least one primitive that is not a VDB.
# Search them and report them to the artist.
prims = geometry.prims()
invalid_prims = [prim for prim in prims
if not isinstance(prim, hou.VDB)]
if invalid_prims:
# Log prim numbers as consecutive ranges so logging isn't very
# slow for large number of primitives
error = (
"Found non-VDB primitives for `{}`. "
"Primitive indices {} are not VDB primitives.".format(
node.path(),
", ".join(group_consecutive_numbers(
prim.number() for prim in invalid_prims
))
)
)
return [node, error]
for prim in prims:
if prim.numVertices() != 1:
cls.log.error("Found primitive with more than 1 vertex!")
return [instance]
if num_points != num_vdb_prims:
# We have points unrelated to the VDB primitives.
error = (
"The number of primitives and points do not match in '{}'. "
"This likely means you have unconnected points, which we do "
"not allow in the VDB output.".format(node.path()))
return [node, error]
return [None, None]
@classmethod
def get_invalid(cls, instance):
nodes, _ = cls.get_invalid_with_message(instance)
return nodes