216 lines
6.2 KiB
Python
216 lines
6.2 KiB
Python
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
# Copyright (C) 2025 ComfyUI-GeometryPack Contributors
|
|
|
|
"""
|
|
Mesh validation utilities for workflow testing.
|
|
|
|
Provides validators to check mesh outputs from workflow execution.
|
|
"""
|
|
|
|
import numpy as np
|
|
import trimesh
|
|
|
|
|
|
class MeshValidator:
|
|
"""Validates mesh geometry and topology."""
|
|
|
|
@staticmethod
|
|
def is_valid_mesh(mesh):
|
|
"""
|
|
Check if a mesh has valid basic geometry.
|
|
|
|
Args:
|
|
mesh: trimesh.Trimesh object
|
|
|
|
Returns:
|
|
tuple: (is_valid, error_message)
|
|
"""
|
|
if not isinstance(mesh, trimesh.Trimesh):
|
|
return False, f"Not a Trimesh object: {type(mesh)}"
|
|
|
|
if len(mesh.vertices) == 0:
|
|
return False, "Mesh has no vertices"
|
|
|
|
if len(mesh.faces) == 0:
|
|
return False, "Mesh has no faces"
|
|
|
|
if np.any(np.isnan(mesh.vertices)):
|
|
return False, "Mesh contains NaN vertices"
|
|
|
|
if np.any(np.isinf(mesh.vertices)):
|
|
return False, "Mesh contains infinite vertices"
|
|
|
|
return True, None
|
|
|
|
@staticmethod
|
|
def check_vertex_count(mesh, min_vertices=3, max_vertices=None):
|
|
"""
|
|
Check if mesh has acceptable vertex count.
|
|
|
|
Args:
|
|
mesh: trimesh.Trimesh object
|
|
min_vertices: Minimum vertex count
|
|
max_vertices: Maximum vertex count (None = no limit)
|
|
|
|
Returns:
|
|
tuple: (is_valid, error_message)
|
|
"""
|
|
count = len(mesh.vertices)
|
|
|
|
if count < min_vertices:
|
|
return False, f"Too few vertices: {count} < {min_vertices}"
|
|
|
|
if max_vertices and count > max_vertices:
|
|
return False, f"Too many vertices: {count} > {max_vertices}"
|
|
|
|
return True, None
|
|
|
|
@staticmethod
|
|
def check_face_count(mesh, min_faces=1, max_faces=None):
|
|
"""
|
|
Check if mesh has acceptable face count.
|
|
|
|
Args:
|
|
mesh: trimesh.Trimesh object
|
|
min_faces: Minimum face count
|
|
max_faces: Maximum face count (None = no limit)
|
|
|
|
Returns:
|
|
tuple: (is_valid, error_message)
|
|
"""
|
|
count = len(mesh.faces)
|
|
|
|
if count < min_faces:
|
|
return False, f"Too few faces: {count} < {min_faces}"
|
|
|
|
if max_faces and count > max_faces:
|
|
return False, f"Too many faces: {count} > {max_faces}"
|
|
|
|
return True, None
|
|
|
|
@staticmethod
|
|
def check_manifold(mesh):
|
|
"""
|
|
Check if mesh is manifold (watertight, no edges shared by >2 faces).
|
|
|
|
Args:
|
|
mesh: trimesh.Trimesh object
|
|
|
|
Returns:
|
|
tuple: (is_manifold, error_message)
|
|
"""
|
|
try:
|
|
is_manifold = mesh.is_watertight
|
|
if not is_manifold:
|
|
return False, "Mesh is not watertight/manifold"
|
|
return True, None
|
|
except Exception as e:
|
|
return False, f"Error checking manifold: {e}"
|
|
|
|
@staticmethod
|
|
def check_bounding_box(mesh, expected_min=None, expected_max=None, tolerance=0.1):
|
|
"""
|
|
Check if mesh bounding box is within expected range.
|
|
|
|
Args:
|
|
mesh: trimesh.Trimesh object
|
|
expected_min: Expected minimum bounds (x, y, z)
|
|
expected_max: Expected maximum bounds (x, y, z)
|
|
tolerance: Tolerance for comparison
|
|
|
|
Returns:
|
|
tuple: (is_valid, error_message)
|
|
"""
|
|
bounds = mesh.bounds
|
|
actual_min = bounds[0]
|
|
actual_max = bounds[1]
|
|
|
|
if expected_min is not None:
|
|
diff = np.abs(actual_min - np.array(expected_min))
|
|
if np.any(diff > tolerance):
|
|
return False, f"Bounds min mismatch: {actual_min} vs {expected_min}"
|
|
|
|
if expected_max is not None:
|
|
diff = np.abs(actual_max - np.array(expected_max))
|
|
if np.any(diff > tolerance):
|
|
return False, f"Bounds max mismatch: {actual_max} vs {expected_max}"
|
|
|
|
return True, None
|
|
|
|
@staticmethod
|
|
def check_has_uv_coordinates(mesh):
|
|
"""
|
|
Check if mesh has UV coordinates.
|
|
|
|
Args:
|
|
mesh: trimesh.Trimesh object
|
|
|
|
Returns:
|
|
tuple: (has_uvs, error_message)
|
|
"""
|
|
if not hasattr(mesh.visual, 'uv'):
|
|
return False, "Mesh has no UV coordinates"
|
|
|
|
if mesh.visual.uv is None or len(mesh.visual.uv) == 0:
|
|
return False, "Mesh UV coordinates are empty"
|
|
|
|
return True, None
|
|
|
|
@staticmethod
|
|
def check_vertex_attributes(mesh, expected_attributes):
|
|
"""
|
|
Check if mesh has expected vertex attributes.
|
|
|
|
Args:
|
|
mesh: trimesh.Trimesh object
|
|
expected_attributes: List of attribute names
|
|
|
|
Returns:
|
|
tuple: (has_attributes, error_message)
|
|
"""
|
|
if not hasattr(mesh, 'vertex_attributes'):
|
|
return False, "Mesh has no vertex_attributes"
|
|
|
|
for attr in expected_attributes:
|
|
if attr not in mesh.vertex_attributes:
|
|
return False, f"Missing vertex attribute: {attr}"
|
|
|
|
return True, None
|
|
|
|
@classmethod
|
|
def validate_all(cls, mesh, checks=None):
|
|
"""
|
|
Run all specified validation checks on a mesh.
|
|
|
|
Args:
|
|
mesh: trimesh.Trimesh object
|
|
checks: List of check names to run (None = run basic checks)
|
|
|
|
Returns:
|
|
tuple: (all_valid, list of error messages)
|
|
"""
|
|
if checks is None:
|
|
checks = ['is_valid_mesh', 'check_vertex_count', 'check_face_count']
|
|
|
|
errors = []
|
|
|
|
for check_name in checks:
|
|
if not hasattr(cls, check_name):
|
|
errors.append(f"Unknown check: {check_name}")
|
|
continue
|
|
|
|
check_method = getattr(cls, check_name)
|
|
|
|
try:
|
|
# Call with just mesh for basic checks
|
|
is_valid, error = check_method(mesh)
|
|
if not is_valid:
|
|
errors.append(f"{check_name}: {error}")
|
|
except TypeError:
|
|
# Check requires additional parameters, skip
|
|
continue
|
|
except Exception as e:
|
|
errors.append(f"{check_name} raised exception: {e}")
|
|
|
|
return len(errors) == 0, errors
|