508 lines
19 KiB
Python
508 lines
19 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
STP文件几何验证脚本
|
|
使用FreeCAD命令行验证PythonOCC提取的几何数据是否正确
|
|
|
|
使用方法:
|
|
freecad -c verify_stp.py <stp_file_path>
|
|
|
|
或直接运行:
|
|
python verify_stp.py <stp_file_path>
|
|
"""
|
|
|
|
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 <stp_file_path>")
|
|
print(" 或: freecad -c verify_stp.py <stp_file_path>")
|
|
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)
|