- threemf_stream reads Bambu Studio / OrcaSlicer project metadata: plates
(name, objects, preview images), filaments, and each part's type and
extruder. Placements now record which build item and part they belong to.
- convert_3mf.py leaves out negative/modifier/support parts, colours each
part by its filament (a part's extruder 0 falls back to its object's), and
takes --plate N. Geometry is still stored once per (part, colour).
- New threemf_info.py lists plates and filaments without loading geometry
and extracts plate preview images; served via /api/{files,entries}/:id/project.
- /glb?plate=N builds and caches a per-plate preview. Cached GLB names carry
a converter version, so previews made before colours are rebuilt.
- Model detail: plate chips (with the slicer's plate images) under the
viewer — multi-plate projects open on plate 1 — plus "All plates", and a
Filaments section with colour swatches.
- Fingerprints leave out negative/modifier parts, so a project matches the
model it prints.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
53 lines
1.5 KiB
Python
53 lines
1.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Measure mesh files for geometry fingerprints (duplicate detection).
|
|
|
|
Usage: fingerprint.py <file> [<file> ...]
|
|
Prints one JSON line per file: {path, fp: {faces, area, volume, extents}} or {path, error}.
|
|
Multi-object files (e.g. 3MF projects) are measured as one combined mesh.
|
|
"""
|
|
import sys
|
|
import json
|
|
|
|
import trimesh
|
|
|
|
import threemf_stream
|
|
|
|
|
|
def measure(path):
|
|
if path.lower().endswith(".3mf"):
|
|
# Leave out negative/modifier/support parts, so a project matches the
|
|
# model it prints (e.g. its STL export).
|
|
meshes, placements = threemf_stream.load(path)
|
|
placements = threemf_stream.printable(threemf_stream.read_project(path), placements)
|
|
stats = threemf_stream.measure(meshes, placements)
|
|
if not stats["faces"]:
|
|
raise ValueError("no geometry found")
|
|
return stats
|
|
|
|
mesh = trimesh.load(path, force="mesh")
|
|
if mesh is None or len(mesh.faces) == 0:
|
|
raise ValueError("no geometry found")
|
|
try:
|
|
volume = abs(float(mesh.volume))
|
|
except Exception:
|
|
volume = 0.0
|
|
return {
|
|
"faces": int(len(mesh.faces)),
|
|
"area": float(mesh.area),
|
|
"volume": volume,
|
|
"extents": [float(x) for x in mesh.extents],
|
|
}
|
|
|
|
|
|
def main():
|
|
for path in sys.argv[1:]:
|
|
try:
|
|
out = {"path": path, "fp": measure(path)}
|
|
except Exception as exc:
|
|
out = {"path": path, "error": str(exc)[:200]}
|
|
print(json.dumps(out), flush=True)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|