Spaces:
Running on Zero
Running on Zero
File size: 6,832 Bytes
01aaaaf | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 | """
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}")
|