ai-3d-generator / mesh_utils.py
sacruise's picture
Upload mesh_utils.py with huggingface_hub
01aaaaf verified
Raw
History Blame Contribute Delete
6.83 kB
"""
Mesh Processing Utilities
=========================
Advanced mesh repair, manifold enforcement, scaling, and modular
decomposition utilities for 3D-print-ready output.
"""
import re
import logging
from typing import Optional
import numpy as np
import trimesh
logger = logging.getLogger(__name__)
# ---------------------------------------------------------------------------
# Manifold Repair
# ---------------------------------------------------------------------------
def make_manifold(mesh: trimesh.Trimesh, max_iterations: int = 5) -> trimesh.Trimesh:
"""
Aggressively repair a mesh to make it manifold (watertight).
Runs multiple repair passes until the mesh is watertight or max iterations hit.
"""
for i in range(max_iterations):
if mesh.is_watertight:
logger.info("Mesh is manifold after %d passes", i)
return mesh
# Fix winding and normals
trimesh.repair.fix_normals(mesh)
trimesh.repair.fix_inversion(mesh)
trimesh.repair.fix_winding(mesh)
# Remove degenerate faces
mask = mesh.nondegenerate_faces()
if not mask.all():
mesh.update_faces(mask)
# Merge close vertices (precision issues)
mesh.merge_vertices(merge_tex=True, merge_norm=True)
# Fill holes
mesh.fill_holes()
logger.info("Mesh repair done (watertight: %s) after %d passes",
mesh.is_watertight, max_iterations)
return mesh
def reduce_faces(mesh: trimesh.Trimesh, target_faces: int) -> trimesh.Trimesh:
"""Decimate a mesh to a target face count while preserving shape."""
if len(mesh.faces) <= target_faces:
return mesh
try:
# Use quadric decimation if available
simplified = mesh.simplify_quadric_decimation(target_faces)
logger.info("Decimated from %d to %d faces", len(mesh.faces), len(simplified.faces))
return simplified
except Exception:
logger.warning("Quadric decimation unavailable, returning original mesh")
return mesh
# ---------------------------------------------------------------------------
# Dimensional Scaling
# ---------------------------------------------------------------------------
UNIT_TO_MM = {
"mm": 1.0,
"cm": 10.0,
"m": 1000.0,
"in": 25.4,
"inch": 25.4,
"inches": 25.4,
"ft": 304.8,
"feet": 304.8,
}
def scale_to_real_dimensions(
mesh: trimesh.Trimesh,
target_size_mm: float,
axis: str = "auto",
) -> trimesh.Trimesh:
"""
Scale a mesh so that its extent along `axis` matches `target_size_mm`.
Args:
mesh: Input mesh
target_size_mm: Target dimension in millimeters
axis: 'x', 'y', 'z', or 'auto' (largest extent)
"""
extents = mesh.extents # [x, y, z]
if axis == "auto":
current_size = max(extents)
else:
idx = {"x": 0, "y": 1, "z": 2}[axis.lower()]
current_size = extents[idx]
if current_size <= 0:
return mesh
scale_factor = target_size_mm / current_size
mesh.apply_scale(scale_factor)
logger.info("Scaled mesh by %.4f (%.2f → %.2fmm)", scale_factor, current_size, target_size_mm)
return mesh
def center_mesh(mesh: trimesh.Trimesh) -> trimesh.Trimesh:
"""Center mesh at origin and place on ground plane (z=0)."""
mesh.vertices -= mesh.centroid
# Place on ground
min_z = mesh.vertices[:, 2].min()
mesh.vertices[:, 2] -= min_z
return mesh
# ---------------------------------------------------------------------------
# Modular Decomposition
# ---------------------------------------------------------------------------
def split_into_parts(mesh: trimesh.Trimesh) -> list[trimesh.Trimesh]:
"""
Split mesh into separate connected components.
Each component becomes an independent, manifold part.
"""
try:
parts = mesh.split(only_watertight=False)
if not parts:
return [mesh]
# Sort parts by volume (largest first)
parts_with_vol = []
for p in parts:
try:
vol = abs(p.volume) if p.is_watertight else p.area
except Exception:
vol = len(p.faces)
parts_with_vol.append((p, vol))
parts_with_vol.sort(key=lambda x: x[1], reverse=True)
result = [p for p, _ in parts_with_vol]
logger.info("Split mesh into %d parts (largest: %d faces)",
len(result), len(result[0].faces))
return result
except Exception as e:
logger.warning("Split failed: %s", e)
return [mesh]
def label_parts(parts: list[trimesh.Trimesh]) -> dict[str, trimesh.Trimesh]:
"""
Label parts by relative position and size.
Returns dict like: {"base_large": mesh, "top_small": mesh, ...}
"""
labeled = {}
for i, part in enumerate(parts):
centroid = part.centroid
# Determine position description
if centroid[2] < -0.3:
pos = "bottom"
elif centroid[2] > 0.3:
pos = "top"
else:
pos = "middle"
# Determine size description
size = "large" if len(part.faces) > len(parts[0].faces) * 0.3 else "small"
key = f"part_{i+1:02d}_{pos}_{size}"
labeled[key] = part
return labeled
# ---------------------------------------------------------------------------
# Quality Metrics
# ---------------------------------------------------------------------------
def mesh_quality_report(mesh: trimesh.Trimesh) -> dict:
"""Generate a quality report for a mesh."""
report = {
"vertices": len(mesh.vertices),
"faces": len(mesh.faces),
"is_watertight": mesh.is_watertight,
"is_volume": mesh.is_volume,
"euler_number": mesh.euler_number,
"extents_mm": mesh.extents.tolist(),
"bounding_box_mm": mesh.bounds.tolist(),
}
try:
report["volume_mm3"] = float(mesh.volume) if mesh.is_watertight else None
except Exception:
report["volume_mm3"] = None
report["surface_area_mm2"] = float(mesh.area)
# Check for degenerate faces
degen = ~mesh.nondegenerate_faces()
report["degenerate_faces"] = int(degen.sum())
return report
# ---------------------------------------------------------------------------
# CLI test
# ---------------------------------------------------------------------------
if __name__ == "__main__":
# Quick self-test
box = trimesh.creation.box(extents=[2, 3, 1])
box = make_manifold(box)
box = scale_to_real_dimensions(box, target_size_mm=100.0)
box = center_mesh(box)
report = mesh_quality_report(box)
print("Quality Report:")
for k, v in report.items():
print(f" {k}: {v}")
print(f"\nManifold: {box.is_watertight}")