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}")