#!/usr/bin/env python3 """ STP文件几何验证脚本 使用FreeCAD命令行验证PythonOCC提取的几何数据是否正确 使用方法: freecad -c verify_stp.py 或直接运行: python verify_stp.py """ import sys import os import json from datetime import datetime from pathlib import Path # 添加项目路径 PROJECT_ROOT = Path(__file__).parent.parent sys.path.insert(0, str(PROJECT_ROOT / "src")) # 全局结果字典 verification_result = { "file": "", "timestamp": "", "freecad": {}, "pythonocc": {}, "comparison": {}, "status": "pending" } def verify_with_freecad(stp_path): """使用FreeCAD验证STP文件""" try: import FreeCAD import Part import Mesh import MeshPart import Import print(f"\n{'='*70}") print(f"FreeCAD 验证") print(f"{'='*70}\n") # 创建新文档 doc = FreeCAD.newDocument("Verification") # 导入STP文件 - FreeCAD 0.21.2 兼容方式 imported = False # 方法1: Import.insert (FreeCAD 0.21+) try: Import.insert(stp_path, doc.Name) imported = True print("使用 Import.insert 导入成功") except Exception as e1: print(f"Import.insert 失败: {e1}") # 方法2: Import.open if not imported: try: Import.open(stp_path) imported = True doc = FreeCAD.ActiveDocument print("使用 Import.open 导入成功") except Exception as e2: print(f"Import.open 失败: {e2}") # 方法3: Part.read if not imported: try: shape = Part.read(stp_path) if shape and not shape.isNull(): imported = True Part.show(shape) print("使用 Part.read 导入成功") except Exception as e3: print(f"Part.read 失败: {e3}") # 方法4: Part.insert (旧版本) if not imported: try: Part.insert(stp_path, doc.Name) imported = True print("使用 Part.insert 导入成功") except Exception as e4: print(f"Part.insert 失败: {e4}") if not imported: print("❌ 所有导入方法都失败") return None # 获取所有形状对象 shapes = [] for obj in doc.Objects: if hasattr(obj, 'Shape') and not obj.Shape.isNull(): shapes.append(obj.Shape) if not shapes: print("❌ 未找到有效的几何形状") return None # 合并所有形状 compound = shapes[0] for s in shapes[1:]: compound = compound.fuse(s) # 提取几何数据 bbox = compound.BoundBox # 计算体积和表面积 volume_mm3 = compound.Volume surface_area_mm2 = compound.Area # 获取质心 com = compound.CenterOfMass # 拓扑信息 topology = { "faces": len(compound.Faces), "edges": len(compound.Edges), "vertices": len(compound.Vertexes), "wires": len(compound.Wires), "shells": len(compound.Shells), "solids": len(compound.Solids) } # 生成网格用于点云验证 mesh = MeshPart.meshFromShape(compound, LinearDeflection=0.1, AngularDeflection=0.5) result = { "volume_mm3": float(volume_mm3), "volume_cm3": float(volume_mm3 / 1000), "surface_area_mm2": float(surface_area_mm2), "surface_area_cm2": float(surface_area_mm2 / 100), "bounding_box": { "x_min": float(bbox.XMin), "x_max": float(bbox.XMax), "y_min": float(bbox.YMin), "y_max": float(bbox.YMax), "z_min": float(bbox.ZMin), "z_max": float(bbox.ZMax), "x_length": float(bbox.XLength), "y_length": float(bbox.YLength), "z_length": float(bbox.ZLength), "center": [float(bbox.Center.x), float(bbox.Center.y), float(bbox.Center.z)] }, "center_of_mass": [float(com.x), float(com.y), float(com.z)], "topology": topology, "mesh": { "points": mesh.CountPoints, "facets": mesh.CountFacets } } # 打印结果 print(f"体积: {result['volume_cm3']:.4f} cm³ ({result['volume_mm3']:.2f} mm³)") print(f"表面积: {result['surface_area_cm2']:.4f} cm² ({result['surface_area_mm2']:.2f} mm²)") print(f"\n边界框:") print(f" X: {result['bounding_box']['x_length']:.4f} mm ({result['bounding_box']['x_min']:.2f} ~ {result['bounding_box']['x_max']:.2f})") print(f" Y: {result['bounding_box']['y_length']:.4f} mm ({result['bounding_box']['y_min']:.2f} ~ {result['bounding_box']['y_max']:.2f})") print(f" Z: {result['bounding_box']['z_length']:.4f} mm ({result['bounding_box']['z_min']:.2f} ~ {result['bounding_box']['z_max']:.2f})") print(f" 中心: ({result['bounding_box']['center'][0]:.2f}, {result['bounding_box']['center'][1]:.2f}, {result['bounding_box']['center'][2]:.2f})") print(f"\n质心: ({result['center_of_mass'][0]:.4f}, {result['center_of_mass'][1]:.4f}, {result['center_of_mass'][2]:.4f})") print(f"\n拓扑信息:") print(f" 面: {topology['faces']}") print(f" 边: {topology['edges']}") print(f" 顶点: {topology['vertices']}") print(f" 线框: {topology['wires']}") print(f" 壳体: {topology['shells']}") print(f" 实体: {topology['solids']}") print(f"\n网格:") print(f" 点数: {result['mesh']['points']}") print(f" 面数: {result['mesh']['facets']}") # 关闭文档 FreeCAD.closeDocument("Verification") return result except Exception as e: print(f"❌ FreeCAD验证失败: {e}") import traceback traceback.print_exc() return None def verify_with_pythonocc(stp_path): """使用PythonOCC验证STP文件""" try: from OCC.Core.STEPControl import STEPControl_Reader from OCC.Core.IFSelect import IFSelect_RetDone from OCC.Core.BRepTools import breptools_Read from OCC.Core.TopExp import TopExp_Explorer from OCC.Core.TopAbs import TopAbs_SOLID, TopAbs_FACE, TopAbs_EDGE, TopAbs_VERTEX from OCC.Core.BRepGProp import brepgprop_VolumeProperties, brepgprop_SurfaceProperties from OCC.Core.GProp import GProp_GProps from OCC.Core.Bnd import Bnd_Box from OCC.Core.BRepBndLib import brepbndlib_Add from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh from OCC.Core.TopLoc import TopLoc_Location from OCC.Core.BRep import BRep_Tool print(f"\n{'='*70}") print(f"PythonOCC 验证") print(f"{'='*70}\n") # 读取STP文件 reader = STEPControl_Reader() status = reader.ReadFile(stp_path) if status != IFSelect_RetDone: print("❌ 无法读取STP文件") return None reader.TransferRoots() shape = reader.OneShape() # 生成网格 mesh_gen = BRepMesh_IncrementalMesh(shape, 0.1) mesh_gen.Perform() # 计算体积 vol_props = GProp_GProps() brepgprop_VolumeProperties(shape, vol_props) volume_mm3 = vol_props.Mass() com = vol_props.CentreOfMass() # 计算表面积 surf_props = GProp_GProps() brepgprop_SurfaceProperties(shape, surf_props) surface_area_mm2 = surf_props.Mass() # 计算边界框 bbox = Bnd_Box() brepbndlib_Add(shape, bbox) xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get() # 拓扑统计 def count_topology(shape, top_type): explorer = TopExp_Explorer(shape, top_type) count = 0 while explorer.More(): count += 1 explorer.Next() return count topology = { "faces": count_topology(shape, TopAbs_FACE), "edges": count_topology(shape, TopAbs_EDGE), "vertices": count_topology(shape, TopAbs_VERTEX), "solids": count_topology(shape, TopAbs_SOLID) } # 统计网格顶点和三角形 mesh_points = 0 mesh_facets = 0 explorer = TopExp_Explorer(shape, TopAbs_FACE) while explorer.More(): face = explorer.Current() location = TopLoc_Location() triangulation = BRep_Tool.Triangulation(face, location) if triangulation: mesh_points += triangulation.NbNodes() mesh_facets += triangulation.NbTriangles() explorer.Next() result = { "volume_mm3": float(volume_mm3), "volume_cm3": float(volume_mm3 / 1000), "surface_area_mm2": float(surface_area_mm2), "surface_area_cm2": float(surface_area_mm2 / 100), "bounding_box": { "x_min": float(xmin), "x_max": float(xmax), "y_min": float(ymin), "y_max": float(ymax), "z_min": float(zmin), "z_max": float(zmax), "x_length": float(xmax - xmin), "y_length": float(ymax - ymin), "z_length": float(zmax - zmin), "center": [float((xmin + xmax) / 2), float((ymin + ymax) / 2), float((zmin + zmax) / 2)] }, "center_of_mass": [float(com.X()), float(com.Y()), float(com.Z())], "topology": topology, "mesh": { "points": mesh_points, "facets": mesh_facets } } # 打印结果 print(f"体积: {result['volume_cm3']:.4f} cm³ ({result['volume_mm3']:.2f} mm³)") print(f"表面积: {result['surface_area_cm2']:.4f} cm² ({result['surface_area_mm2']:.2f} mm²)") print(f"\n边界框:") print(f" X: {result['bounding_box']['x_length']:.4f} mm ({result['bounding_box']['x_min']:.2f} ~ {result['bounding_box']['x_max']:.2f})") print(f" Y: {result['bounding_box']['y_length']:.4f} mm ({result['bounding_box']['y_min']:.2f} ~ {result['bounding_box']['y_max']:.2f})") print(f" Z: {result['bounding_box']['z_length']:.4f} mm ({result['bounding_box']['z_min']:.2f} ~ {result['bounding_box']['z_max']:.2f})") print(f" 中心: ({result['bounding_box']['center'][0]:.2f}, {result['bounding_box']['center'][1]:.2f}, {result['bounding_box']['center'][2]:.2f})") print(f"\n质心: ({result['center_of_mass'][0]:.4f}, {result['center_of_mass'][1]:.4f}, {result['center_of_mass'][2]:.4f})") print(f"\n拓扑信息:") print(f" 面: {topology['faces']}") print(f" 边: {topology['edges']}") print(f" 顶点: {topology['vertices']}") print(f" 实体: {topology['solids']}") print(f"\n网格:") print(f" 点数: {result['mesh']['points']}") print(f" 面数: {result['mesh']['facets']}") return result except Exception as e: print(f"❌ PythonOCC验证失败: {e}") import traceback traceback.print_exc() return None def compare_results(freecad_result, pythonocc_result): """对比FreeCAD和PythonOCC的结果""" print(f"\n{'='*70}") print(f"对比结果") print(f"{'='*70}\n") if not freecad_result or not pythonocc_result: print("❌ 无法对比,缺少数据") return None comparison = {} # 体积对比 vol_diff = abs(freecad_result['volume_mm3'] - pythonocc_result['volume_mm3']) vol_diff_percent = (vol_diff / freecad_result['volume_mm3']) * 100 if freecad_result['volume_mm3'] > 0 else 0 comparison['volume'] = { "freecad": freecad_result['volume_cm3'], "pythonocc": pythonocc_result['volume_cm3'], "difference_mm3": vol_diff, "difference_percent": vol_diff_percent } print(f"体积对比:") print(f" FreeCAD: {freecad_result['volume_cm3']:.4f} cm³") print(f" PythonOCC: {pythonocc_result['volume_cm3']:.4f} cm³") print(f" 差异: {vol_diff:.4f} mm³ ({vol_diff_percent:.4f}%)") print(f" 状态: {'✅ 通过' if vol_diff_percent < 1 else '❌ 超差'}") # 表面积对比 area_diff = abs(freecad_result['surface_area_mm2'] - pythonocc_result['surface_area_mm2']) area_diff_percent = (area_diff / freecad_result['surface_area_mm2']) * 100 if freecad_result['surface_area_mm2'] > 0 else 0 comparison['surface_area'] = { "freecad": freecad_result['surface_area_cm2'], "pythonocc": pythonocc_result['surface_area_cm2'], "difference_mm2": area_diff, "difference_percent": area_diff_percent } print(f"\n表面积对比:") print(f" FreeCAD: {freecad_result['surface_area_cm2']:.4f} cm²") print(f" PythonOCC: {pythonocc_result['surface_area_cm2']:.4f} cm²") print(f" 差异: {area_diff:.4f} mm² ({area_diff_percent:.4f}%)") print(f" 状态: {'✅ 通过' if area_diff_percent < 2 else '❌ 超差'}") # 边界框对比 bbox_fc = freecad_result['bounding_box'] bbox_occ = pythonocc_result['bounding_box'] x_diff = abs(bbox_fc['x_length'] - bbox_occ['x_length']) y_diff = abs(bbox_fc['y_length'] - bbox_occ['y_length']) z_diff = abs(bbox_fc['z_length'] - bbox_occ['z_length']) comparison['bounding_box'] = { "x": {"freecad": bbox_fc['x_length'], "pythonocc": bbox_occ['x_length'], "difference": x_diff}, "y": {"freecad": bbox_fc['y_length'], "pythonocc": bbox_occ['y_length'], "difference": y_diff}, "z": {"freecad": bbox_fc['z_length'], "pythonocc": bbox_occ['z_length'], "difference": z_diff} } print(f"\n边界框对比:") print(f" X轴: FreeCAD={bbox_fc['x_length']:.4f}, PythonOCC={bbox_occ['x_length']:.4f}, 差异={x_diff:.4f} mm") print(f" Y轴: FreeCAD={bbox_fc['y_length']:.4f}, PythonOCC={bbox_occ['y_length']:.4f}, 差异={y_diff:.4f} mm") print(f" Z轴: FreeCAD={bbox_fc['z_length']:.4f}, PythonOCC={bbox_occ['z_length']:.4f}, 差异={z_diff:.4f} mm") # 质心对比 com_fc = freecad_result['center_of_mass'] com_occ = pythonocc_result['center_of_mass'] com_diff = [ abs(com_fc[0] - com_occ[0]), abs(com_fc[1] - com_occ[1]), abs(com_fc[2] - com_occ[2]) ] comparison['center_of_mass'] = { "freecad": com_fc, "pythonocc": com_occ, "difference": com_diff } print(f"\n质心对比:") print(f" FreeCAD: ({com_fc[0]:.4f}, {com_fc[1]:.4f}, {com_fc[2]:.4f})") print(f" PythonOCC: ({com_occ[0]:.4f}, {com_occ[1]:.4f}, {com_occ[2]:.4f})") print(f" 差异: ({com_diff[0]:.4f}, {com_diff[1]:.4f}, {com_diff[2]:.4f}) mm") # 拓扑对比 topo_fc = freecad_result['topology'] topo_occ = pythonocc_result['topology'] comparison['topology'] = { "faces": {"freecad": topo_fc['faces'], "pythonocc": topo_occ['faces']}, "edges": {"freecad": topo_fc['edges'], "pythonocc": topo_occ['edges']}, "vertices": {"freecad": topo_fc['vertices'], "pythonocc": topo_occ['vertices']} } print(f"\n拓扑对比:") print(f" 面: FreeCAD={topo_fc['faces']}, PythonOCC={topo_occ['faces']}") print(f" 边: FreeCAD={topo_fc['edges']}, PythonOCC={topo_occ['edges']}") print(f" 顶点: FreeCAD={topo_fc['vertices']}, PythonOCC={topo_occ['vertices']}") # 总体评估 passed = vol_diff_percent < 1 and area_diff_percent < 2 print(f"\n{'='*70}") print(f"验证结果: {'✅ 通过' if passed else '❌ 失败'}") print(f"{'='*70}\n") comparison['overall_status'] = "passed" if passed else "failed" return comparison def main(): """主函数""" if len(sys.argv) < 2: print("用法: python verify_stp.py ") print(" 或: freecad -c verify_stp.py ") sys.exit(1) stp_path = sys.argv[1] # 检查是否跳过 PythonOCC 验证(默认跳过,因为主流程已用 PythonOCC 解析) skip_pythonocc = os.environ.get('SKIP_PYTHONOCC', 'true').lower() == 'true' if not os.path.exists(stp_path): print(f"❌ 文件不存在: {stp_path}") sys.exit(1) # 初始化结果 verification_result["file"] = stp_path verification_result["timestamp"] = datetime.now().isoformat() print(f"\n{'#'*70}") print(f"# STP文件几何验证报告") print(f"# 文件: {stp_path}") print(f"# 时间: {verification_result['timestamp']}") print(f"# PythonOCC验证: {'跳过' if skip_pythonocc else '启用'}") print(f"{'#'*70}") # FreeCAD验证 freecad_result = verify_with_freecad(stp_path) verification_result["freecad"] = freecad_result or {} # PythonOCC验证(可选,默认跳过以节省时间) pythonocc_result = None if not skip_pythonocc: pythonocc_result = verify_with_pythonocc(stp_path) verification_result["pythonocc"] = pythonocc_result or {} else: print("\n⏩ 跳过 PythonOCC 验证(主流程已使用 PythonOCC 解析)") verification_result["pythonocc"] = {"skipped": True, "reason": "主流程已使用PythonOCC解析"} # 对比结果 if pythonocc_result: comparison = compare_results(freecad_result, pythonocc_result) verification_result["comparison"] = comparison or {} verification_result["status"] = comparison.get('overall_status', 'failed') if comparison else 'failed' else: # 只有 FreeCAD 验证时,根据 FreeCAD 结果判断 if freecad_result: verification_result["status"] = "passed" verification_result["comparison"] = { "note": "仅执行 FreeCAD 验证", "freecad_volume_cm3": freecad_result.get('volume_cm3'), "freecad_surface_area_cm2": freecad_result.get('surface_area_cm2') } print(f"\n{'='*70}") print(f"验证结果: ✅ 通过 (仅FreeCAD验证)") print(f"{'='*70}\n") else: verification_result["status"] = "failed" verification_result["comparison"] = {"note": "FreeCAD 验证失败"} # 保存JSON报告 report_path = Path(stp_path).stem + "_verification_report.json" with open(report_path, 'w', encoding='utf-8') as f: json.dump(verification_result, f, indent=2, ensure_ascii=False) print(f"\n验证报告已保存: {report_path}") return verification_result if __name__ == "__main__": result = main() sys.exit(0 if result['status'] == 'passed' else 1)