printhive/tools/fingerprint.py
dlawler489 00a4b4ba07 Add duplicate finder, trash, notes, and library search/filters
- Fingerprint every file in the background: SHA-256 for all formats plus a
  trimesh geometry fingerprint for STL/OBJ/3MF, so renamed, re-exported or
  converted copies are still matched. Backfills existing files on startup.
- Duplicates view groups models that share files (exact copies vs same
  shape), recommends which to keep, merges tags/collections/notes into the
  kept model and trashes the rest; groups can be dismissed.
- Deleting now moves models to a restorable trash; files are only removed
  from disk on permanent delete or when the trash is emptied.
- Per-model notes with autosave.
- Search across names, notes, tags, files and designers; filter by format,
  source and license; include/exclude tags with any/all matching; sort and
  group by source or format. Preferences persist per browser.
- Add node:test unit tests for duplicate grouping and fingerprints.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 23:06:09 +10:00

40 lines
1 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
def measure(path):
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())