"""用 SolidWorks evidence 的 document_truth 验收 output3 CDSL 重建结果。""" from __future__ import annotations import argparse import json import math from pathlib import Path from typing import Any from build123d import CenterOf, import_step FORBIDDEN_CDSL_KEYS = { "compiler_context", "entities", "contour_edges_mm", "contour_regions_mm", "_raw_entities", "vertices", } def _find_forbidden(value: Any, path: str = "$") -> list[str]: found: list[str] = [] if isinstance(value, dict): for key, item in value.items(): child = f"{path}.{key}" if key in FORBIDDEN_CDSL_KEYS: found.append(child) found.extend(_find_forbidden(item, child)) elif isinstance(value, list): for index, item in enumerate(value): found.extend(_find_forbidden(item, f"{path}[{index}]")) return found def validate(cdsl_path: Path, evidence_dir: Path) -> dict[str, Any]: cdsl = json.loads(cdsl_path.read_text(encoding="utf-8")) part_id = str(cdsl["part_id"]) source_name = str(cdsl["meta"]["source"]) evidence = json.loads((evidence_dir / source_name).read_text(encoding="utf-8")) truth = evidence["document_truth"] mass = truth["mass_properties"] step_path = cdsl_path.with_name(f"{part_id}_rebuilt.step") report_path = cdsl_path.with_name(f"{part_id}.rebuild_report.json") report = json.loads(report_path.read_text(encoding="utf-8")) solid = import_step(str(step_path)) truth_volume = float(mass["volume"]) * 1e9 truth_area = float(mass["surface_area"]) * 1e6 truth_com = [float(value) * 1000.0 for value in mass["center_of_mass"]] rebuilt_com_vector = solid.center(CenterOf.MASS) rebuilt_com = [rebuilt_com_vector.X, rebuilt_com_vector.Y, rebuilt_com_vector.Z] bbox = solid.bounding_box() rebuilt_bbox = [bbox.min.X, bbox.min.Y, bbox.min.Z, bbox.max.X, bbox.max.Y, bbox.max.Z] truth_bbox = [float(value) * 1000.0 for value in truth["geometry"]["bounding_box"]] volume_error_pct = abs(float(solid.volume) - truth_volume) / truth_volume * 100.0 area_error_pct = abs(float(solid.area) - truth_area) / truth_area * 100.0 com_delta_mm = math.dist(rebuilt_com, truth_com) bbox_max_delta_mm = max(abs(a - b) for a, b in zip(rebuilt_bbox, truth_bbox)) forbidden = _find_forbidden(cdsl) engine = report.get("engine_result", {}).get("engine") checks = { "engine_cdsl_only": engine == "cdsl_only", "no_forbidden_geometry_payload": not forbidden, "volume_error_le_1pct": volume_error_pct <= 1.0, "surface_area_error_le_1pct": area_error_pct <= 1.0, "center_of_mass_delta_le_0_1mm": com_delta_mm <= 0.1, "bbox_delta_le_0_01mm": bbox_max_delta_mm <= 0.01, } return { "part_id": part_id, "cdsl_path": str(cdsl_path), "rebuilt_step": str(step_path), "source_evidence": str(evidence_dir / source_name), "cdsl_lines": len(cdsl_path.read_text(encoding="utf-8").splitlines()), "feature_count": len(cdsl.get("features") or []), "sketch_count": len((cdsl.get("geometry") or {}).get("sketches") or []), "engine": engine, "forbidden_paths": forbidden, "metrics": { "truth_volume_mm3": truth_volume, "rebuilt_volume_mm3": float(solid.volume), "volume_error_pct": volume_error_pct, "truth_surface_area_mm2": truth_area, "rebuilt_surface_area_mm2": float(solid.area), "surface_area_error_pct": area_error_pct, "center_of_mass_delta_mm": com_delta_mm, "bbox_max_delta_mm": bbox_max_delta_mm, }, "checks": checks, "passed": all(checks.values()), } def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--output", type=Path, required=True) parser.add_argument("--evidence", type=Path, required=True) parser.add_argument("--report", type=Path, required=True) args = parser.parse_args() results = [ validate(path, args.evidence) for path in sorted(args.output.glob("cylinder_*/*.cdsl.json")) ] summary = { "schema": "cad.cdsl.output3.validation.v1", "count": len(results), "passed_count": sum(item["passed"] for item in results), "failed_count": sum(not item["passed"] for item in results), "max_volume_error_pct": max(item["metrics"]["volume_error_pct"] for item in results), "max_surface_area_error_pct": max(item["metrics"]["surface_area_error_pct"] for item in results), "max_center_of_mass_delta_mm": max(item["metrics"]["center_of_mass_delta_mm"] for item in results), "max_bbox_delta_mm": max(item["metrics"]["bbox_max_delta_mm"] for item in results), "results": results, } args.report.parent.mkdir(parents=True, exist_ok=True) args.report.write_text(json.dumps(summary, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") print( f"validated={summary['count']} passed={summary['passed_count']} " f"failed={summary['failed_count']} report={args.report}" ) if summary["failed_count"]: raise SystemExit(1) if __name__ == "__main__": main()