Files
PozzettiAndrea-ComfyUI-Geom…/tests/workflow_tests/mesh_validators.py
T
2026-02-12 21:19:40 +01:00

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