- Code\rebar_pipeline, review_pipeline, pier-program을 정션에서 실제 폴더로 전환 - Code\rhino_mcp 및 CLAUDE_rhino배경.md 추가, .mcp.json은 PYTHONPATH로 새 위치 사용 - pier-program 내부 .git 제거(이력은 backup\pier-program_20261028.bundle에 보관) - worktree 가이드 반영(CLAUDE.local.md, HANDOFF.md) - D:\Rhino-MCP 경로 참조를 새 위치로 치환 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
75 lines
3.7 KiB
Python
75 lines
3.7 KiB
Python
"""Regression check: compare two model fingerprints (fingerprint.py output) object by object.
|
|
|
|
usage: python compare_fingerprints.py <baseline.json> <candidate.json> [<report.json>]
|
|
Tolerances: bbox 0.011 mm (values rounded to 0.01), curve length 0.002 mm, volume 1e-6 relative.
|
|
Everything else (layer, type, visibility, colour, face count, solidity, user strings) must be equal.
|
|
Exit code 0 = identical within tolerance.
|
|
"""
|
|
import json
|
|
import sys
|
|
from collections import Counter
|
|
from pathlib import Path
|
|
|
|
|
|
def diff_obj(a, b):
|
|
out = []
|
|
for k in sorted(set(a) | set(b)):
|
|
va, vb = a.get(k), b.get(k)
|
|
if k == "bbox" and va and vb:
|
|
if max(abs(x - y) for x, y in zip(va, vb)) > 0.011:
|
|
out.append((k, va, vb))
|
|
elif k == "length" and va is not None and vb is not None:
|
|
if abs(va - vb) > 0.002:
|
|
out.append((k, va, vb))
|
|
elif k == "volume" and isinstance(va, float) and isinstance(vb, float):
|
|
if abs(va - vb) > 1e-6 * max(abs(va), 1.0):
|
|
out.append((k, va, vb))
|
|
elif k == "user":
|
|
for uk in sorted(set(va or {}) | set(vb or {})):
|
|
if (va or {}).get(uk) != (vb or {}).get(uk):
|
|
out.append((f"user.{uk}", (va or {}).get(uk), (vb or {}).get(uk)))
|
|
elif va != vb:
|
|
out.append((k, va, vb))
|
|
return out
|
|
|
|
|
|
def is_fast(fp):
|
|
return any(o.get("volume") == "skipped" for o in fp["objects"].values())
|
|
|
|
|
|
def compare(base, cand):
|
|
# A fast fingerprint skips pipe volumes, and Rhino then returns pipe bounding boxes without the pipe radius
|
|
# (2026-10-12: B9 top 28350.87 fast vs 28358.87 full on the same file) -> only compare like with like.
|
|
if is_fast(base) != is_fast(cand):
|
|
raise ValueError("fingerprint modes differ (FAST vs full): compare fingerprints made with the same mode")
|
|
A, B = base["objects"], cand["objects"]
|
|
only_a, only_b = sorted(set(A) - set(B)), sorted(set(B) - set(A))
|
|
diffs = {oid: d for oid in sorted(set(A) & set(B)) if (d := diff_obj(A[oid], B[oid]))}
|
|
fields = Counter(f for d in diffs.values() for f, _, _ in d)
|
|
layer_diff = {k: (base["layers"].get(k), cand["layers"].get(k))
|
|
for k in set(base["layers"]) | set(cand["layers"]) if base["layers"].get(k) != cand["layers"].get(k)}
|
|
rep = {"baseline": base["model"], "candidate": cand["model"],
|
|
"count": [base["count"], cand["count"]], "units": [base["units"], cand["units"]],
|
|
"tolerance": [base["tolerance"], cand["tolerance"]],
|
|
"only_in_baseline": only_a, "only_in_candidate": only_b,
|
|
"objects_with_diffs": len(diffs), "diff_fields": dict(fields), "layer_diffs": layer_diff,
|
|
"samples": {oid: d[:6] for oid, d in list(diffs.items())[:25]}}
|
|
rep["identical"] = (not only_a and not only_b and not diffs and not layer_diff
|
|
and rep["units"][0] == rep["units"][1])
|
|
return rep
|
|
|
|
|
|
if __name__ == "__main__":
|
|
base = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
|
|
cand = json.loads(Path(sys.argv[2]).read_text(encoding="utf-8"))
|
|
rep = compare(base, cand)
|
|
if len(sys.argv) > 3:
|
|
Path(sys.argv[3]).write_text(json.dumps(rep, ensure_ascii=False, indent=1), encoding="utf-8")
|
|
brief = {k: rep[k] for k in ("count", "units", "tolerance", "objects_with_diffs", "diff_fields", "identical")}
|
|
brief["only_in_baseline"], brief["only_in_candidate"] = rep["only_in_baseline"][:10], rep["only_in_candidate"][:10]
|
|
brief["layer_diffs"] = rep["layer_diffs"]
|
|
print(json.dumps(brief, ensure_ascii=False, indent=1))
|
|
for oid, d in list(rep["samples"].items())[:8]:
|
|
print(oid, d)
|
|
sys.exit(0 if rep["identical"] else 1)
|