sacruise commited on
Commit
01aaaaf
·
verified ·
1 Parent(s): 8a554fb

Upload mesh_utils.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. mesh_utils.py +222 -0
mesh_utils.py ADDED
@@ -0,0 +1,222 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Mesh Processing Utilities
3
+ =========================
4
+ Advanced mesh repair, manifold enforcement, scaling, and modular
5
+ decomposition utilities for 3D-print-ready output.
6
+ """
7
+
8
+ import re
9
+ import logging
10
+ from typing import Optional
11
+
12
+ import numpy as np
13
+ import trimesh
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ # ---------------------------------------------------------------------------
19
+ # Manifold Repair
20
+ # ---------------------------------------------------------------------------
21
+ def make_manifold(mesh: trimesh.Trimesh, max_iterations: int = 5) -> trimesh.Trimesh:
22
+ """
23
+ Aggressively repair a mesh to make it manifold (watertight).
24
+ Runs multiple repair passes until the mesh is watertight or max iterations hit.
25
+ """
26
+ for i in range(max_iterations):
27
+ if mesh.is_watertight:
28
+ logger.info("Mesh is manifold after %d passes", i)
29
+ return mesh
30
+
31
+ # Fix winding and normals
32
+ trimesh.repair.fix_normals(mesh)
33
+ trimesh.repair.fix_inversion(mesh)
34
+ trimesh.repair.fix_winding(mesh)
35
+
36
+ # Remove degenerate faces
37
+ mask = mesh.nondegenerate_faces()
38
+ if not mask.all():
39
+ mesh.update_faces(mask)
40
+
41
+ # Merge close vertices (precision issues)
42
+ mesh.merge_vertices(merge_tex=True, merge_norm=True)
43
+
44
+ # Fill holes
45
+ mesh.fill_holes()
46
+
47
+ logger.info("Mesh repair done (watertight: %s) after %d passes",
48
+ mesh.is_watertight, max_iterations)
49
+ return mesh
50
+
51
+
52
+ def reduce_faces(mesh: trimesh.Trimesh, target_faces: int) -> trimesh.Trimesh:
53
+ """Decimate a mesh to a target face count while preserving shape."""
54
+ if len(mesh.faces) <= target_faces:
55
+ return mesh
56
+
57
+ try:
58
+ # Use quadric decimation if available
59
+ simplified = mesh.simplify_quadric_decimation(target_faces)
60
+ logger.info("Decimated from %d to %d faces", len(mesh.faces), len(simplified.faces))
61
+ return simplified
62
+ except Exception:
63
+ logger.warning("Quadric decimation unavailable, returning original mesh")
64
+ return mesh
65
+
66
+
67
+ # ---------------------------------------------------------------------------
68
+ # Dimensional Scaling
69
+ # ---------------------------------------------------------------------------
70
+ UNIT_TO_MM = {
71
+ "mm": 1.0,
72
+ "cm": 10.0,
73
+ "m": 1000.0,
74
+ "in": 25.4,
75
+ "inch": 25.4,
76
+ "inches": 25.4,
77
+ "ft": 304.8,
78
+ "feet": 304.8,
79
+ }
80
+
81
+
82
+ def scale_to_real_dimensions(
83
+ mesh: trimesh.Trimesh,
84
+ target_size_mm: float,
85
+ axis: str = "auto",
86
+ ) -> trimesh.Trimesh:
87
+ """
88
+ Scale a mesh so that its extent along `axis` matches `target_size_mm`.
89
+
90
+ Args:
91
+ mesh: Input mesh
92
+ target_size_mm: Target dimension in millimeters
93
+ axis: 'x', 'y', 'z', or 'auto' (largest extent)
94
+ """
95
+ extents = mesh.extents # [x, y, z]
96
+
97
+ if axis == "auto":
98
+ current_size = max(extents)
99
+ else:
100
+ idx = {"x": 0, "y": 1, "z": 2}[axis.lower()]
101
+ current_size = extents[idx]
102
+
103
+ if current_size <= 0:
104
+ return mesh
105
+
106
+ scale_factor = target_size_mm / current_size
107
+ mesh.apply_scale(scale_factor)
108
+ logger.info("Scaled mesh by %.4f (%.2f → %.2fmm)", scale_factor, current_size, target_size_mm)
109
+ return mesh
110
+
111
+
112
+ def center_mesh(mesh: trimesh.Trimesh) -> trimesh.Trimesh:
113
+ """Center mesh at origin and place on ground plane (z=0)."""
114
+ mesh.vertices -= mesh.centroid
115
+ # Place on ground
116
+ min_z = mesh.vertices[:, 2].min()
117
+ mesh.vertices[:, 2] -= min_z
118
+ return mesh
119
+
120
+
121
+ # ---------------------------------------------------------------------------
122
+ # Modular Decomposition
123
+ # ---------------------------------------------------------------------------
124
+ def split_into_parts(mesh: trimesh.Trimesh) -> list[trimesh.Trimesh]:
125
+ """
126
+ Split mesh into separate connected components.
127
+ Each component becomes an independent, manifold part.
128
+ """
129
+ try:
130
+ parts = mesh.split(only_watertight=False)
131
+ if not parts:
132
+ return [mesh]
133
+
134
+ # Sort parts by volume (largest first)
135
+ parts_with_vol = []
136
+ for p in parts:
137
+ try:
138
+ vol = abs(p.volume) if p.is_watertight else p.area
139
+ except Exception:
140
+ vol = len(p.faces)
141
+ parts_with_vol.append((p, vol))
142
+
143
+ parts_with_vol.sort(key=lambda x: x[1], reverse=True)
144
+ result = [p for p, _ in parts_with_vol]
145
+
146
+ logger.info("Split mesh into %d parts (largest: %d faces)",
147
+ len(result), len(result[0].faces))
148
+ return result
149
+
150
+ except Exception as e:
151
+ logger.warning("Split failed: %s", e)
152
+ return [mesh]
153
+
154
+
155
+ def label_parts(parts: list[trimesh.Trimesh]) -> dict[str, trimesh.Trimesh]:
156
+ """
157
+ Label parts by relative position and size.
158
+ Returns dict like: {"base_large": mesh, "top_small": mesh, ...}
159
+ """
160
+ labeled = {}
161
+ for i, part in enumerate(parts):
162
+ centroid = part.centroid
163
+ # Determine position description
164
+ if centroid[2] < -0.3:
165
+ pos = "bottom"
166
+ elif centroid[2] > 0.3:
167
+ pos = "top"
168
+ else:
169
+ pos = "middle"
170
+
171
+ # Determine size description
172
+ size = "large" if len(part.faces) > len(parts[0].faces) * 0.3 else "small"
173
+
174
+ key = f"part_{i+1:02d}_{pos}_{size}"
175
+ labeled[key] = part
176
+
177
+ return labeled
178
+
179
+
180
+ # ---------------------------------------------------------------------------
181
+ # Quality Metrics
182
+ # ---------------------------------------------------------------------------
183
+ def mesh_quality_report(mesh: trimesh.Trimesh) -> dict:
184
+ """Generate a quality report for a mesh."""
185
+ report = {
186
+ "vertices": len(mesh.vertices),
187
+ "faces": len(mesh.faces),
188
+ "is_watertight": mesh.is_watertight,
189
+ "is_volume": mesh.is_volume,
190
+ "euler_number": mesh.euler_number,
191
+ "extents_mm": mesh.extents.tolist(),
192
+ "bounding_box_mm": mesh.bounds.tolist(),
193
+ }
194
+
195
+ try:
196
+ report["volume_mm3"] = float(mesh.volume) if mesh.is_watertight else None
197
+ except Exception:
198
+ report["volume_mm3"] = None
199
+
200
+ report["surface_area_mm2"] = float(mesh.area)
201
+
202
+ # Check for degenerate faces
203
+ degen = ~mesh.nondegenerate_faces()
204
+ report["degenerate_faces"] = int(degen.sum())
205
+
206
+ return report
207
+
208
+
209
+ # ---------------------------------------------------------------------------
210
+ # CLI test
211
+ # ---------------------------------------------------------------------------
212
+ if __name__ == "__main__":
213
+ # Quick self-test
214
+ box = trimesh.creation.box(extents=[2, 3, 1])
215
+ box = make_manifold(box)
216
+ box = scale_to_real_dimensions(box, target_size_mm=100.0)
217
+ box = center_mesh(box)
218
+ report = mesh_quality_report(box)
219
+ print("Quality Report:")
220
+ for k, v in report.items():
221
+ print(f" {k}: {v}")
222
+ print(f"\nManifold: {box.is_watertight}")