Spaces:
Running on Zero
Running on Zero
| """ | |
| 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}") | |