File size: 8,218 Bytes
8f1f637
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Examples library — reference objects with GROUND TRUTH for quality evaluation.

Two kinds of ground truth (per datasets research):
  - Standard parts (LEGO 2×4, ISO 7089 washer, DIN 934 nut, 16 mm die) have
    PUBLISHED EXACT DIMENSIONS → numeric "accurate to X mm" ground truth.
  - A reference mesh is also generated so we can report Chamfer / voxel-IoU.

We build the reference meshes parametrically from published specs and render a
clean preview that doubles as a demo input image (the live app also takes real
webcam/upload photos — photograph the real part and the published dims still apply).

`build_library(out_dir)` writes per-object reference.stl + preview.png + manifest.json.
"""
from __future__ import annotations

import json
import os
import urllib.request

import numpy as np
import trimesh

from .render import render_single

# Google Scanned Objects (CC-BY 4.0) via the kevinzakka/mujoco_scanned_objects mirror.
# Real-world 3D scans — used as ground-truth reference meshes (Chamfer/voxel-IoU) and to
# show the fidelity gap vs our parametric clones. Scans are in metres → scaled to mm.
GSO_BASE = "https://raw.githubusercontent.com/kevinzakka/mujoco_scanned_objects/main/models/{}/model.obj"
GSO = [
    {"name": "gso_mug", "gso": "ACE_Coffee_Mug_Kristen_16_oz_cup",
     "title": "Coffee mug (GSO scan)", "category": "real · medium",
     "goal": "Clone this mug as a 3D-printable model"},
    {"name": "gso_teapot", "gso": "Threshold_Porcelain_Teapot_White",
     "title": "Porcelain teapot (GSO scan)", "category": "real · complex",
     "goal": "Clone this teapot as a 3D-printable model"},
    {"name": "gso_panda", "gso": "Android_Figure_Panda",
     "title": "Android panda figure (GSO scan)", "category": "real · complex",
     "goal": "Clone this figurine as a 3D-printable model"},
]
GSO_ATTRIB = "Google Scanned Objects (CC-BY 4.0)"


def _load_gso(gso_name: str, dl_dir: str) -> trimesh.Trimesh:
    """Download a GSO model.obj (cached), scale m→mm, center, decimate if large."""
    os.makedirs(dl_dir, exist_ok=True)
    path = os.path.join(dl_dir, gso_name + ".obj")
    if not os.path.exists(path):
        urllib.request.urlretrieve(GSO_BASE.format(gso_name), path)
    m = trimesh.load(path, force="mesh")
    m.apply_scale(1000.0)
    m.apply_translation(-m.bounding_box.centroid)
    if len(m.faces) > 12000:
        try:
            m = m.simplify_quadric_decimation(face_count=12000)
        except Exception:  # noqa: BLE001
            pass
    return m


# --- reference mesh builders (dims in mm, from published specs) -------------
def _lego_2x4():
    """LEGO brick 3001: body 31.8×15.8×9.6, studs Ø4.8 h1.8, pitch 8.0."""
    body = trimesh.creation.box(extents=[31.8, 15.8, 9.6])
    parts = [body]
    x0, y0 = -3 * 8.0 / 2, -8.0 / 2
    for ix in range(4):
        for iy in range(2):
            stud = trimesh.creation.cylinder(radius=2.4, height=1.8, sections=32)
            stud.apply_translation([x0 + ix * 8.0, y0 + iy * 8.0, 9.6 / 2 + 0.9])
            parts.append(stud)
    return trimesh.boolean.union(parts), "LEGO 2×4 brick (3001)"


def _washer_m10():
    """ISO 7089 M10: ID 10.5, OD 20, thickness 2.0."""
    outer = trimesh.creation.cylinder(radius=10.0, height=2.0, sections=64)
    inner = trimesh.creation.cylinder(radius=5.25, height=4.0, sections=64)
    return trimesh.boolean.difference([outer, inner]), "ISO 7089 flat washer (M10)"


def _hex_nut_m8():
    """DIN 934 M8: width across flats 13 (circumradius 7.51), height 6.5, bore 8."""
    body = trimesh.creation.cylinder(radius=7.506, height=6.5, sections=6)
    bore = trimesh.creation.cylinder(radius=4.0, height=10.0, sections=48)
    return trimesh.boolean.difference([body, bore]), "DIN 934 hex nut (M8)"


def _die_16():
    """16 mm six-sided die with shallow pips."""
    cube = trimesh.creation.box(extents=[16, 16, 16])
    return cube, "Six-sided die (16 mm)"


def _mug():
    outer = trimesh.creation.cylinder(radius=40, height=95, sections=64)
    inner = trimesh.creation.cylinder(radius=34, height=85, sections=64)
    inner.apply_translation([0, 0, 8])
    body = trimesh.boolean.difference([outer, inner])
    handle = trimesh.creation.torus(major_radius=22, minor_radius=6)
    handle.apply_translation([44, 0, 0])
    return trimesh.boolean.union([body, handle]), "Coffee mug"


def _spur_gear():
    """Complex case: 20-tooth spur gear — stresses primitive decomposition."""
    base = trimesh.creation.cylinder(radius=18, height=6, sections=64)
    parts = [base]
    for i in range(20):
        a = 2 * np.pi * i / 20
        tooth = trimesh.creation.box(extents=[5, 3, 6])
        tooth.apply_translation([20, 0, 0])
        tooth.apply_transform(trimesh.transformations.rotation_matrix(a, [0, 0, 1]))
        parts.append(tooth)
    bore = trimesh.creation.cylinder(radius=5, height=8, sections=48)
    return trimesh.boolean.difference([trimesh.boolean.union(parts), bore]), "20-tooth spur gear"


REGISTRY = [
    {"name": "washer_m10", "build": _washer_m10, "category": "simple",
     "goal": "Clone this washer as a 3D-printable model",
     "note": "ISO 7089 M10: OD 20, ID 10.5, thickness 2.0 mm"},
    {"name": "hex_nut_m8", "build": _hex_nut_m8, "category": "simple",
     "goal": "Clone this hex nut as a 3D-printable model",
     "note": "DIN 934 M8: across-flats 13, height 6.5 mm"},
    {"name": "die_16", "build": _die_16, "category": "simple",
     "goal": "Clone this die as a 3D-printable model",
     "note": "Standard 16 mm cube"},
    {"name": "lego_2x4", "build": _lego_2x4, "category": "medium",
     "goal": "Clone this LEGO brick as a 3D-printable model",
     "note": "LEGO 3001: 31.8×15.8×9.6 mm + 8 studs"},
    {"name": "mug", "build": _mug, "category": "medium",
     "goal": "Clone this mug as a 3D-printable model",
     "note": "~95 mm tall, 80 mm dia (approx)"},
    {"name": "spur_gear", "build": _spur_gear, "category": "complex",
     "goal": "Clone this gear as a 3D-printable model",
     "note": "20-tooth gear — shows the fidelity gap on complex geometry"},
]


def _write_entry(out_dir, name, mesh, title, category, goal, note):
    d = os.path.join(out_dir, name)
    os.makedirs(d, exist_ok=True)
    ref_stl = os.path.join(d, "reference.stl")
    preview = os.path.join(d, "preview.png")
    mesh.export(ref_stl)
    render_single(mesh, preview, view="iso")
    dims = sorted(round(float(x), 1) for x in (mesh.bounds[1] - mesh.bounds[0]))
    return {"name": name, "title": title, "category": category, "goal": goal,
            "note": note, "reference_stl": ref_stl, "preview": preview, "dims_mm": dims}


def build_library(out_dir: str = "data/examples", include_gso: bool = True) -> dict:
    """Generate reference meshes + previews + manifest. GSO scans are best-effort
    (skipped if offline). Returns the manifest dict."""
    os.makedirs(out_dir, exist_ok=True)
    manifest = []
    for spec in REGISTRY:
        mesh, title = spec["build"]()
        manifest.append(_write_entry(out_dir, spec["name"], mesh, title,
                                     spec["category"], spec["goal"], spec["note"]))
    if include_gso:
        dl = os.path.join(out_dir, "_gso_cache")
        for spec in GSO:
            try:
                mesh = _load_gso(spec["gso"], dl)
                manifest.append(_write_entry(out_dir, spec["name"], mesh, spec["title"],
                                             spec["category"], spec["goal"], GSO_ATTRIB))
            except Exception as e:  # noqa: BLE001 — offline / fetch failure is non-fatal
                print(f"  (skipped GSO {spec['name']}: {e})")
    with open(os.path.join(out_dir, "manifest.json"), "w") as f:
        json.dump(manifest, f, indent=2)
    return {"out_dir": out_dir, "items": manifest}


def load_manifest(out_dir: str = "data/examples") -> list[dict]:
    path = os.path.join(out_dir, "manifest.json")
    if not os.path.exists(path):
        return build_library(out_dir)["items"]
    with open(path) as f:
        return json.load(f)


if __name__ == "__main__":
    lib = build_library()
    for it in lib["items"]:
        print(f"{it['category']:8} {it['name']:12} dims={it['dims_mm']} -> {it['preview']}")