printhive/tools/fingerprint.py
dlawler489 fcb418f119 Show 3MF projects plate by plate in their filament colours
- 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>
2026-09-15 07:49:58 +10:00

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())