add freecad
This commit is contained in:
@@ -0,0 +1,258 @@
|
||||
"""
|
||||
几何验证服务
|
||||
使用 FreeCAD 和 PythonOCC 交叉验证几何数据准确性
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class GeometryVerificationService:
|
||||
"""几何验证服务"""
|
||||
|
||||
def __init__(self):
|
||||
self.verification_script = Path(__file__).parent.parent / "scripts" / "verify_stp.py"
|
||||
|
||||
async def verify_stp_file(self, stp_path: str) -> Dict[str, Any]:
|
||||
"""
|
||||
验证 STP 文件几何数据
|
||||
|
||||
Args:
|
||||
stp_path: STP 文件路径
|
||||
|
||||
Returns:
|
||||
验证结果字典
|
||||
"""
|
||||
try:
|
||||
logger.info(f"开始验证 STP 文件: {stp_path}")
|
||||
|
||||
# 运行验证脚本
|
||||
result = await self._run_verification_script(stp_path)
|
||||
|
||||
if result:
|
||||
logger.info(f"验证完成: {result.get('status', 'unknown')}")
|
||||
else:
|
||||
logger.warning("验证脚本未返回结果")
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"验证失败: {e}")
|
||||
return {
|
||||
"status": "error",
|
||||
"error": str(e),
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
async def _run_verification_script(self, stp_path: str) -> Optional[Dict[str, Any]]:
|
||||
"""运行验证脚本"""
|
||||
import tempfile
|
||||
import os
|
||||
|
||||
# 创建临时输出文件
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
||||
output_path = f.name
|
||||
|
||||
try:
|
||||
# 构建命令 - 使用 FreeCAD 命令行模式
|
||||
cmd = None
|
||||
|
||||
# 尝试1: freecad.cmd (snap 安装的命令行模式)
|
||||
for cmd_name in ['freecad.cmd', '/snap/bin/freecad.cmd', 'freecad-cmd']:
|
||||
try:
|
||||
result = subprocess.run(['which', cmd_name], capture_output=True, text=True)
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
cmd = [cmd_name, str(self.verification_script), stp_path]
|
||||
logger.debug(f"找到 FreeCAD 命令: {cmd_name}")
|
||||
break
|
||||
except:
|
||||
pass
|
||||
|
||||
# 尝试2: freecad with offscreen
|
||||
if not cmd:
|
||||
try:
|
||||
result = subprocess.run(['which', 'freecad'], capture_output=True, text=True)
|
||||
if result.returncode == 0:
|
||||
cmd = [
|
||||
'env', 'QT_QPA_PLATFORM=offscreen',
|
||||
'freecad', '-c',
|
||||
str(self.verification_script), stp_path
|
||||
]
|
||||
except:
|
||||
pass
|
||||
|
||||
# 尝试3: 使用 xvfb-run
|
||||
if not cmd:
|
||||
try:
|
||||
result = subprocess.run(['which', 'xvfb-run'], capture_output=True, text=True)
|
||||
if result.returncode == 0:
|
||||
cmd = ['xvfb-run', 'freecad', '-c', str(self.verification_script), stp_path]
|
||||
except:
|
||||
pass
|
||||
|
||||
if not cmd:
|
||||
logger.warning("FreeCAD 命令行工具不可用,跳过验证")
|
||||
return {
|
||||
"status": "skipped",
|
||||
"reason": "FreeCAD not available",
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
logger.info(f"执行验证命令: {' '.join(cmd)}")
|
||||
|
||||
# 异步运行子进程
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
*cmd,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
cwd=str(Path(__file__).parent.parent)
|
||||
)
|
||||
|
||||
stdout, stderr = await process.communicate()
|
||||
|
||||
if process.returncode == 0:
|
||||
# 尝试读取生成的报告文件
|
||||
report_path = Path(stp_path).stem + "_verification_report.json"
|
||||
if Path(report_path).exists():
|
||||
with open(report_path, 'r', encoding='utf-8') as f:
|
||||
result = json.load(f)
|
||||
# 删除临时报告文件
|
||||
Path(report_path).unlink()
|
||||
return result
|
||||
else:
|
||||
# 解析 stdout 获取结果
|
||||
return self._parse_verification_output(stdout.decode('utf-8'))
|
||||
else:
|
||||
logger.error(f"验证脚本执行失败 (returncode={process.returncode}): {stderr.decode('utf-8')}")
|
||||
return {
|
||||
"status": "error",
|
||||
"error": stderr.decode('utf-8'),
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
except FileNotFoundError:
|
||||
logger.warning("FreeCAD 命令行工具不可用,跳过验证")
|
||||
return {
|
||||
"status": "skipped",
|
||||
"reason": "FreeCAD not available",
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"运行验证脚本失败: {e}")
|
||||
return {
|
||||
"status": "error",
|
||||
"error": str(e),
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
finally:
|
||||
# 清理临时文件
|
||||
if os.path.exists(output_path):
|
||||
os.unlink(output_path)
|
||||
|
||||
def _parse_verification_output(self, output: str) -> Dict[str, Any]:
|
||||
"""解析验证脚本输出"""
|
||||
result = {
|
||||
"status": "unknown",
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
lines = output.split('\n')
|
||||
for line in lines:
|
||||
if '验证结果:' in line:
|
||||
if '✅ 通过' in line:
|
||||
result['status'] = 'passed'
|
||||
elif '❌ 失败' in line:
|
||||
result['status'] = 'failed'
|
||||
elif '体积对比:' in line:
|
||||
# 解析体积差异
|
||||
pass
|
||||
elif '表面积对比:' in line:
|
||||
# 解析表面积差异
|
||||
pass
|
||||
|
||||
return result
|
||||
|
||||
def verify_with_pythonocc(self, shape) -> Dict[str, Any]:
|
||||
"""
|
||||
使用 PythonOCC 验证几何数据(同步方法,用于内部验证)
|
||||
|
||||
Args:
|
||||
shape: OCC 形状对象
|
||||
|
||||
Returns:
|
||||
验证结果
|
||||
"""
|
||||
try:
|
||||
from OCC.Core.GProp import GProp_GProps
|
||||
from OCC.Core.BRepGProp import brepgprop_VolumeProperties, brepgprop_SurfaceProperties
|
||||
from OCC.Core.Bnd import Bnd_Box
|
||||
from OCC.Core.BRepBndLib import brepbndlib_Add
|
||||
from OCC.Core.TopExp import TopExp_Explorer
|
||||
from OCC.Core.TopAbs import TopAbs_FACE, TopAbs_EDGE, TopAbs_VERTEX, TopAbs_SOLID
|
||||
|
||||
# 计算体积
|
||||
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
|
||||
|
||||
return {
|
||||
"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": {
|
||||
"faces": count_topology(shape, TopAbs_FACE),
|
||||
"edges": count_topology(shape, TopAbs_EDGE),
|
||||
"vertices": count_topology(shape, TopAbs_VERTEX),
|
||||
"solids": count_topology(shape, TopAbs_SOLID)
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"PythonOCC 验证失败: {e}")
|
||||
return {"error": str(e)}
|
||||
|
||||
|
||||
# 单例实例
|
||||
verification_service = GeometryVerificationService()
|
||||
Reference in New Issue
Block a user