Skip to content

Commit

Permalink
Merge pull request #265 from BigRoy/enhancement/maya_validate_non_man…
Browse files Browse the repository at this point in the history
…ifold_repair

Maya: Validate Mesh Non-Manifold improve artist report + add repair
  • Loading branch information
tokejepsen authored Mar 29, 2024
2 parents ecc9ad4 + 954c129 commit 0a75032
Show file tree
Hide file tree
Showing 2 changed files with 154 additions and 11 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<root>
<error id="main">
<title>Non-Manifold Edges/Vertices</title>
<description>## Non-Manifold Edges/Vertices

Meshes found with non-manifold edges or vertices.

### How to repair?

Run select invalid to select the invalid components.

You can also try the _cleanup matching polygons_ action which will perform a
cleanup like Maya's `Mesh > Cleanup...` modeling tool.

It is recommended to always select the invalid to see where the issue is
because if you run any repair on it you will need to double check the topology
is still like you wanted.

</description>
<detail>
### What is non-manifold topology?

_Non-manifold topology_ polygons have a configuration that cannot be unfolded
into a continuous flat piece, for example:

- Three or more faces share an edge
- Two or more faces share a single vertex but no edge.
- Adjacent faces have opposite normals

</detail>
</error>
</root>
Original file line number Diff line number Diff line change
@@ -1,14 +1,99 @@
from maya import cmds
from maya import cmds, mel

import pyblish.api
import ayon_core.hosts.maya.api.action
from ayon_core.pipeline.publish import (
ValidateMeshOrder,
PublishValidationError,
PublishXmlValidationError,
RepairAction,
OptionalPyblishPluginMixin
)


def poly_cleanup(version=4,
meshes=None,
# Version 1
all_meshes=False,
select_only=False,
history_on=True,
quads=False,
nsided=False,
concave=False,
holed=False,
nonplanar=False,
zeroGeom=False,
zeroGeomTolerance=1e-05,
zeroEdge=False,
zeroEdgeTolerance=1e-05,
zeroMap=False,
zeroMapTolerance=1e-05,
# Version 2
shared_uvs=False,
non_manifold=False,
# Version 3
lamina=False,
# Version 4
invalid_components=False):
"""Wrapper around `polyCleanupArgList` mel command"""

# Get all inputs named as `dict` to easily do conversions and formatting
values = locals()

# Convert booleans to 1 or 0
for key in [
"all_meshes",
"select_only",
"history_on",
"quads",
"nsided",
"concave",
"holed",
"nonplanar",
"zeroGeom",
"zeroEdge",
"zeroMap",
"shared_uvs",
"non_manifold",
"lamina",
"invalid_components",
]:
values[key] = 1 if values[key] else 0

cmd = (
'polyCleanupArgList {version} {{ '
'"{all_meshes}",' # 0: All selectable meshes
'"{select_only}",' # 1: Only perform a selection
'"{history_on}",' # 2: Keep construction history
'"{quads}",' # 3: Check for quads polys
'"{nsided}",' # 4: Check for n-sides polys
'"{concave}",' # 5: Check for concave polys
'"{holed}",' # 6: Check for holed polys
'"{nonplanar}",' # 7: Check for non-planar polys
'"{zeroGeom}",' # 8: Check for 0 area faces
'"{zeroGeomTolerance}",' # 9: Tolerance for face areas
'"{zeroEdge}",' # 10: Check for 0 length edges
'"{zeroEdgeTolerance}",' # 11: Tolerance for edge length
'"{zeroMap}",' # 12: Check for 0 uv face area
'"{zeroMapTolerance}",' # 13: Tolerance for uv face areas
'"{shared_uvs}",' # 14: Unshare uvs that are shared
# across vertices
'"{non_manifold}",' # 15: Check for nonmanifold polys
'"{lamina}",' # 16: Check for lamina polys
'"{invalid_components}"' # 17: Remove invalid components
' }};'.format(**values)
)

mel.eval("source polyCleanupArgList")
if not all_meshes and meshes:
# Allow to specify meshes to run over by selecting them
cmds.select(meshes, replace=True)
mel.eval(cmd)


class CleanupMatchingPolygons(RepairAction):
label = "Cleanup matching polygons"


def _as_report_list(values, prefix="- ", suffix="\n"):
"""Return list as bullet point list for a report"""
if not values:
Expand All @@ -29,7 +114,8 @@ class ValidateMeshNonManifold(pyblish.api.InstancePlugin,
hosts = ['maya']
families = ['model']
label = 'Mesh Non-Manifold Edges/Vertices'
actions = [ayon_core.hosts.maya.api.action.SelectInvalidAction]
actions = [ayon_core.hosts.maya.api.action.SelectInvalidAction,
CleanupMatchingPolygons]
optional = True

@staticmethod
Expand All @@ -39,22 +125,46 @@ def get_invalid(instance):

invalid = []
for mesh in meshes:
if (cmds.polyInfo(mesh, nonManifoldVertices=True) or
cmds.polyInfo(mesh, nonManifoldEdges=True)):
invalid.append(mesh)
components = cmds.polyInfo(mesh,
nonManifoldVertices=True,
nonManifoldEdges=True)
if components:
invalid.extend(components)

return invalid

def process(self, instance):
"""Process all the nodes in the instance 'objectSet'"""
if not self.is_active(instance.data):
return

invalid = self.get_invalid(instance)

if invalid:
raise PublishValidationError(
"Meshes found with non-manifold edges/vertices:\n\n{0}".format(
_as_report_list(sorted(invalid))
),
title="Non-Manifold Edges/Vertices"
# Report only the meshes instead of all component indices
invalid_meshes = {
component.split(".", 1)[0] for component in invalid
}
invalid_meshes = _as_report_list(sorted(invalid_meshes))

raise PublishXmlValidationError(
plugin=self,
message=(
"Meshes found with non-manifold "
"edges/vertices:\n\n{0}".format(invalid_meshes)
)
)

@classmethod
def repair(cls, instance):
invalid_components = cls.get_invalid(instance)
if not invalid_components:
cls.log.info("No invalid components found to cleanup.")
return

invalid_meshes = {
component.split(".", 1)[0] for component in invalid_components
}
poly_cleanup(meshes=list(invalid_meshes),
select_only=True,
non_manifold=True)

0 comments on commit 0a75032

Please sign in to comment.