后端模块拆分
This commit is contained in:
@@ -0,0 +1 @@
|
||||
# Core 模块
|
||||
@@ -0,0 +1,209 @@
|
||||
"""
|
||||
AI 分模辅助模型接口示例
|
||||
|
||||
此文件展示了如何创建 AI 模型来辅助分模过程。
|
||||
实际使用时需要替换为真实的 AI 模型。
|
||||
"""
|
||||
from typing import Dict, Any, Optional
|
||||
import numpy as np
|
||||
from OCC.Core.TopoDS import TopoDS_Shape, TopoDS_Face
|
||||
|
||||
|
||||
class AIPartingSurfaceDetector:
|
||||
"""
|
||||
AI 分型面检测器(示例接口)
|
||||
|
||||
功能:
|
||||
- 分析产品 3D 几何
|
||||
- 预测最优分型面位置和方向
|
||||
- 识别倒扣区域
|
||||
"""
|
||||
|
||||
def __init__(self, model_path: Optional[str] = None):
|
||||
"""
|
||||
初始化 AI 分型面检测器
|
||||
|
||||
Args:
|
||||
model_path: 训练好的模型路径
|
||||
"""
|
||||
self.model_path = model_path
|
||||
self.model = None
|
||||
|
||||
# 如果提供了模型路径,加载模型
|
||||
if model_path:
|
||||
self._load_model(model_path)
|
||||
|
||||
def _load_model(self, model_path: str):
|
||||
"""加载训练好的 AI 模型"""
|
||||
# TODO: 实现模型加载逻辑
|
||||
# 示例:
|
||||
# import torch
|
||||
# self.model = torch.load(model_path)
|
||||
print(f"AI 模型加载:{model_path}")
|
||||
|
||||
def detect(self, product_shape: TopoDS_Shape, analysis: Dict) -> Optional[Dict]:
|
||||
"""
|
||||
检测最优分型面
|
||||
|
||||
Args:
|
||||
product_shape: OpenCASCADE 形状对象
|
||||
analysis: 几何分析结果(包含 bounding_box, volume 等)
|
||||
|
||||
Returns:
|
||||
{
|
||||
"origin": [x, y, z], # 分型面原点
|
||||
"normal": [nx, ny, nz], # 分型面法向量
|
||||
"confidence": 0.95, # 置信度
|
||||
"parting_line": [...] # 可选的分型线
|
||||
}
|
||||
"""
|
||||
# TODO: 使用 AI 模型进行预测
|
||||
# 这里是示例返回
|
||||
|
||||
# 1. 将产品形状转换为 AI 模型输入
|
||||
# - 体素化 (voxelization)
|
||||
# - 点云 (point cloud)
|
||||
# - 多视图 (multi-view images)
|
||||
input_data = self._preprocess_shape(product_shape, analysis)
|
||||
|
||||
# 2. 使用模型预测
|
||||
# prediction = self.model.predict(input_data)
|
||||
|
||||
# 3. 返回预测结果
|
||||
return {
|
||||
"origin": [0, 0, analysis["bounding_box"]["center"][2]],
|
||||
"normal": [0, 0, 1], # Z 方向
|
||||
"confidence": 0.85,
|
||||
"undercut_regions": [] # 倒扣区域
|
||||
}
|
||||
|
||||
def _preprocess_shape(self, shape: TopoDS_Shape, analysis: Dict) -> TopoDS_Shape:
|
||||
"""
|
||||
预处理产品形状为 AI 模型输入
|
||||
|
||||
可能的预处理方式:
|
||||
1. 体素化:将 3D 模型转换为 3D 网格
|
||||
2. 点云:采样表面点
|
||||
3. 多视图:渲染多个角度的 2D 图像
|
||||
"""
|
||||
# TODO: 实现预处理逻辑
|
||||
return None
|
||||
|
||||
|
||||
class AIDraftAnalyzer:
|
||||
"""
|
||||
AI 拔模分析器(示例接口)
|
||||
|
||||
功能:
|
||||
- 分析哪些面需要拔模
|
||||
- 预测最优拔模角度
|
||||
- 检测脱模干涉
|
||||
"""
|
||||
|
||||
def __init__(self, model_path: Optional[str] = None):
|
||||
self.model_path = model_path
|
||||
self.model = None
|
||||
|
||||
if model_path:
|
||||
self._load_model(model_path)
|
||||
|
||||
def _load_model(self, model_path: str):
|
||||
"""加载训练好的 AI 模型"""
|
||||
print(f"AI 拔模分析模型加载:{model_path}")
|
||||
|
||||
def analyze(self, product_shape: TopoDS_Shape, parting_surface: TopoDS_Face,
|
||||
base_draft_angle: float) -> Optional[Dict]:
|
||||
"""
|
||||
分析拔模需求
|
||||
|
||||
Args:
|
||||
product_shape: 产品形状
|
||||
parting_surface: 分型面
|
||||
base_draft_angle: 基础拔模角(度)
|
||||
|
||||
Returns:
|
||||
{
|
||||
"drafted_shape": ..., # 应用拔模后的形状
|
||||
"draft_angles": {...}, # 各面的拔模角
|
||||
"interference_areas": [...], # 干涉区域
|
||||
"recommendations": [...] # 优化建议
|
||||
}
|
||||
"""
|
||||
# TODO: 使用 AI 模型分析拔模
|
||||
|
||||
# 示例返回
|
||||
return {
|
||||
"drafted_shape": product_shape, # 简化:返回原始形状
|
||||
"draft_angles": {"default": base_draft_angle},
|
||||
"interference_areas": [],
|
||||
"recommendations": ["建议增加圆角", "壁厚均匀化"]
|
||||
}
|
||||
|
||||
|
||||
class AICavityLayoutOptimizer:
|
||||
"""
|
||||
AI 型腔布局优化器(示例接口)
|
||||
|
||||
功能:
|
||||
- 优化多型腔排列
|
||||
- 设计流道系统
|
||||
- 平衡材料流动
|
||||
"""
|
||||
|
||||
def __init__(self, model_path: Optional[str] = None):
|
||||
self.model_path = model_path
|
||||
self.model = None
|
||||
|
||||
if model_path:
|
||||
self._load_model(model_path)
|
||||
|
||||
def optimize(self, product_shape: TopoDS_Shape, cavity_count: int,
|
||||
mold_base_size: Dict) -> Optional[Dict]:
|
||||
"""
|
||||
优化型腔布局
|
||||
|
||||
Args:
|
||||
product_shape: 产品形状
|
||||
cavity_count: 型腔数量
|
||||
mold_base_size: 模架尺寸
|
||||
|
||||
Returns:
|
||||
{
|
||||
"cavity_positions": [...], # 各型腔位置
|
||||
"runner_system": {...}, # 流道系统设计
|
||||
"balance_score": 0.92, # 流动平衡评分
|
||||
"material_efficiency": 0.85 # 材料利用率
|
||||
}
|
||||
"""
|
||||
# TODO: 使用 AI 优化型腔布局
|
||||
|
||||
return {
|
||||
"cavity_positions": [[0, 0, 0]], # 示例
|
||||
"runner_system": {"type": "cold_runner"},
|
||||
"balance_score": 0.85,
|
||||
"material_efficiency": 0.80
|
||||
}
|
||||
|
||||
|
||||
# ==================== 使用示例 ====================
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 示例:如何使用 AI 模型接口
|
||||
|
||||
# 1. 创建 AI 模型实例
|
||||
parting_detector = AIPartingSurfaceDetector(model_path="models/parting_surface.pth")
|
||||
draft_analyzer = AIDraftAnalyzer(model_path="models/draft_analysis.pth")
|
||||
|
||||
# 2. 设置到 MoldCavityGenerator
|
||||
from moldinsight.core.mold_generator import MoldCavityGenerator
|
||||
|
||||
generator = MoldCavityGenerator()
|
||||
generator.set_ai_model(
|
||||
parting_detector=parting_detector,
|
||||
draft_analyzer=draft_analyzer
|
||||
)
|
||||
|
||||
# 3. 使用(AI 模型会自动介入)
|
||||
# result = generator.generate_mold_cavities(product_shape)
|
||||
|
||||
print("AI 模型接口已配置,分模时将自动使用 AI 辅助")
|
||||
@@ -0,0 +1,547 @@
|
||||
"""
|
||||
AI 分型面检测模块 - 基于 GNN 的分型面预测框架
|
||||
|
||||
架构设计:
|
||||
1. ShapeGraphBuilder - 将 OCC 形状转换为图表示(面为节点,共享边为图边)
|
||||
2. PartingSurfaceGNN - 图神经网络模型定义
|
||||
3. AIPartingSurfaceDetectorV2 - 增强版分型面检测器(集成 GNN)
|
||||
|
||||
图构建策略:
|
||||
- 节点:每个 TopoDS_Face 作为一个节点
|
||||
- 节点特征:法向量(3) + 面积(1) + 曲率(2) + 面类型(1) = 7维
|
||||
- 边:共享 TopoDS_Edge 的面之间建立边
|
||||
- 边特征:共享边长度(1) + 二面角(1) = 2维
|
||||
|
||||
GNN 模型:
|
||||
- 3层 GraphConv + 全局池化 + MLP 分类头
|
||||
- 输出:每个面的分型面归属概率 + 分型方向
|
||||
|
||||
依赖:
|
||||
- PyTorch + PyTorch Geometric(可选,缺失时回退到几何方法)
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
import numpy as np
|
||||
from OCC.Core.TopoDS import TopoDS_Shape
|
||||
from shared.utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
_TORCH_AVAILABLE = False
|
||||
_TORCH_GEOMETRIC_AVAILABLE = False
|
||||
|
||||
try:
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
_TORCH_AVAILABLE = True
|
||||
try:
|
||||
from torch_geometric.nn import GCNConv, global_mean_pool
|
||||
from torch_geometric.data import Data
|
||||
_TORCH_GEOMETRIC_AVAILABLE = True
|
||||
except ImportError:
|
||||
logger.info("PyTorch Geometric 未安装,GNN 模型不可用")
|
||||
except ImportError:
|
||||
logger.info("PyTorch 未安装,AI 分型面检测将使用几何回退方法")
|
||||
|
||||
|
||||
class ShapeGraphBuilder:
|
||||
"""将 OCC 形状转换为图表示"""
|
||||
|
||||
def build_graph(self, shape: TopoDS_Shape) -> Optional[Dict]:
|
||||
"""
|
||||
从 OCC 形状构建图数据
|
||||
|
||||
Returns:
|
||||
{
|
||||
"node_features": np.ndarray (N, 7),
|
||||
"edge_index": np.ndarray (2, E),
|
||||
"edge_features": np.ndarray (E, 2),
|
||||
"face_map": List[TopoDS_Face],
|
||||
"num_nodes": int,
|
||||
"num_edges": int
|
||||
}
|
||||
"""
|
||||
try:
|
||||
from OCC.Core.TopExp import TopExp_Explorer
|
||||
from OCC.Core.TopAbs import TopAbs_FACE, TopAbs_EDGE
|
||||
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface
|
||||
from OCC.Core.GProp import GProp_GProps
|
||||
from OCC.Core.BRepGProp import brepgprop
|
||||
from OCC.Core.Bnd import Bnd_Box
|
||||
from OCC.Core.BRepBndLib import brepbndlib
|
||||
from OCC.Core.TopTools import TopTools_IndexedDataMapOfShapeListOfShape
|
||||
from OCC.Core.TopExp import topexp_MapShapesAndAncestors
|
||||
from OCC.Core.TopoDS import TopoDS_Face, TopoDS_Edge, topods
|
||||
|
||||
faces = []
|
||||
face_features = []
|
||||
|
||||
explorer = TopExp_Explorer(shape, TopAbs_FACE)
|
||||
while explorer.More():
|
||||
face = topods.Face(explorer.Current())
|
||||
features = self._extract_face_features(face)
|
||||
if features is not None:
|
||||
faces.append(face)
|
||||
face_features.append(features)
|
||||
explorer.Next()
|
||||
|
||||
if not faces:
|
||||
logger.warning("未找到面,无法构建图")
|
||||
return None
|
||||
|
||||
node_features = np.array(face_features, dtype=np.float32)
|
||||
|
||||
edge_map = TopTools_IndexedDataMapOfShapeListOfShape()
|
||||
topexp_MapShapesAndAncestors(shape, TopAbs_EDGE, TopAbs_FACE, edge_map)
|
||||
|
||||
edge_list = []
|
||||
edge_features_list = []
|
||||
|
||||
for i in range(1, edge_map.Extent() + 1):
|
||||
edge = topods.Edge(edge_map.FindKey(i))
|
||||
face_list = edge_map.FindFromIndex(i)
|
||||
|
||||
connected_faces = []
|
||||
it = face_list.begin()
|
||||
while it != face_list.end():
|
||||
f = topods.Face(it.Value())
|
||||
try:
|
||||
idx = faces.index(f)
|
||||
connected_faces.append(idx)
|
||||
except ValueError:
|
||||
pass
|
||||
it.next_ptr()
|
||||
|
||||
if len(connected_faces) >= 2:
|
||||
edge_feat = self._extract_edge_features(edge, connected_faces, faces)
|
||||
for j in range(len(connected_faces)):
|
||||
for k in range(j + 1, len(connected_faces)):
|
||||
edge_list.append([connected_faces[j], connected_faces[k]])
|
||||
edge_features_list.append(edge_feat)
|
||||
|
||||
if not edge_list:
|
||||
logger.warning("未找到边连接,返回无图边的图")
|
||||
edge_index = np.zeros((2, 0), dtype=np.int64)
|
||||
edge_features_arr = np.zeros((0, 2), dtype=np.float32)
|
||||
else:
|
||||
edge_index = np.array(edge_list, dtype=np.int64).T
|
||||
rev_edges = np.array([[e[1], e[0]] for e in edge_list], dtype=np.int64).T
|
||||
edge_index = np.concatenate([edge_index, rev_edges], axis=1)
|
||||
edge_features_arr = np.array(edge_features_list, dtype=np.float32)
|
||||
edge_features_arr = np.concatenate([edge_features_arr, edge_features_arr], axis=0)
|
||||
|
||||
return {
|
||||
"node_features": node_features,
|
||||
"edge_index": edge_index,
|
||||
"edge_features": edge_features_arr,
|
||||
"face_map": faces,
|
||||
"num_nodes": len(faces),
|
||||
"num_edges": edge_index.shape[1]
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"图构建失败: {e}")
|
||||
return None
|
||||
|
||||
def _extract_face_features(self, face: Any) -> Optional[np.ndarray]:
|
||||
"""
|
||||
提取面特征:[nx, ny, nz, area, u_curvature, v_curvature, face_type]
|
||||
"""
|
||||
try:
|
||||
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface
|
||||
from OCC.Core.GProp import GProp_GProps
|
||||
from OCC.Core.BRepGProp import brepgprop
|
||||
|
||||
surface = BRepAdaptor_Surface(face)
|
||||
|
||||
u = (surface.FirstUParameter() + surface.LastUParameter()) / 2
|
||||
v = (surface.FirstVParameter() + surface.LastVParameter()) / 2
|
||||
|
||||
if surface.GetType() == 0:
|
||||
normal = surface.Plane().Position().Direction()
|
||||
face_type = 0.0
|
||||
u_curv = 0.0
|
||||
v_curv = 0.0
|
||||
elif surface.GetType() == 1:
|
||||
normal = surface.Cylinder().Position().Direction()
|
||||
face_type = 1.0
|
||||
radius = surface.Cylinder().Radius()
|
||||
u_curv = 1.0 / radius if radius > 0.001 else 0.0
|
||||
v_curv = 0.0
|
||||
elif surface.GetType() == 2:
|
||||
normal = surface.Cone().Position().Direction()
|
||||
face_type = 2.0
|
||||
u_curv = 0.0
|
||||
v_curv = 0.0
|
||||
elif surface.GetType() == 3:
|
||||
normal = surface.Sphere().Position().Direction()
|
||||
face_type = 3.0
|
||||
radius = surface.Sphere().Radius()
|
||||
u_curv = 1.0 / radius if radius > 0.001 else 0.0
|
||||
v_curv = 1.0 / radius if radius > 0.001 else 0.0
|
||||
elif surface.GetType() == 4:
|
||||
normal = surface.Torus().Position().Direction()
|
||||
face_type = 4.0
|
||||
u_curv = 0.0
|
||||
v_curv = 0.0
|
||||
else:
|
||||
from OCC.Core.BRepLProp import BRepLProp_SLProps
|
||||
props = BRepLProp_SLProps(surface, 2, 0.001)
|
||||
props.SetParameters(u, v)
|
||||
if props.IsNormalDefined():
|
||||
normal = props.Normal()
|
||||
else:
|
||||
normal = gp_Dir(0, 0, 1)
|
||||
face_type = 5.0
|
||||
u_curv = 0.0
|
||||
v_curv = 0.0
|
||||
|
||||
face_props = GProp_GProps()
|
||||
brepgprop.SurfaceProperties(face, face_props)
|
||||
area = face_props.Mass()
|
||||
|
||||
return np.array([
|
||||
normal.X(), normal.Y(), normal.Z(),
|
||||
area,
|
||||
u_curv, v_curv,
|
||||
face_type
|
||||
], dtype=np.float32)
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"面特征提取失败: {e}")
|
||||
return None
|
||||
|
||||
def _extract_edge_features(self, edge: Any, connected_faces: List[int],
|
||||
faces: List) -> np.ndarray:
|
||||
"""
|
||||
提取边特征:[edge_length, dihedral_angle]
|
||||
"""
|
||||
try:
|
||||
from OCC.Core.BRepAdaptor import BRepAdaptor_Curve
|
||||
from OCC.Core.GProp import GProp_GProps
|
||||
from OCC.Core.BRepGProp import brepgprop
|
||||
|
||||
curve = BRepAdaptor_Curve(edge)
|
||||
first = curve.FirstParameter()
|
||||
last = curve.LastParameter()
|
||||
|
||||
edge_len = abs(last - first)
|
||||
|
||||
dihedral = 0.0
|
||||
if len(connected_faces) >= 2:
|
||||
n1 = self._get_face_normal_fast(faces[connected_faces[0]])
|
||||
n2 = self._get_face_normal_fast(faces[connected_faces[1]])
|
||||
if n1 is not None and n2 is not None:
|
||||
dot = np.clip(np.dot(n1, n2), -1.0, 1.0)
|
||||
dihedral = np.arccos(dot)
|
||||
|
||||
return np.array([edge_len, dihedral], dtype=np.float32)
|
||||
|
||||
except Exception:
|
||||
return np.array([0.0, 0.0], dtype=np.float32)
|
||||
|
||||
def _get_face_normal_fast(self, face: Any) -> Optional[np.ndarray]:
|
||||
"""快速获取面法向量(numpy数组)"""
|
||||
try:
|
||||
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface
|
||||
surface = BRepAdaptor_Surface(face)
|
||||
if surface.GetType() == 0:
|
||||
n = surface.Plane().Position().Direction()
|
||||
return np.array([n.X(), n.Y(), n.Z()])
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
if _TORCH_GEOMETRIC_AVAILABLE:
|
||||
|
||||
class PartingSurfaceGNN(nn.Module):
|
||||
"""
|
||||
分型面检测 GNN 模型
|
||||
|
||||
架构:
|
||||
- 3层 GCNConv (hidden_dim=64)
|
||||
- 全局平均池化
|
||||
- 3层 MLP 分类头
|
||||
- 输出:每个面的分型面归属概率 (0-1)
|
||||
"""
|
||||
|
||||
def __init__(self, input_dim: int = 7, hidden_dim: int = 64,
|
||||
num_layers: int = 3, dropout: float = 0.3):
|
||||
super().__init__()
|
||||
|
||||
self.input_dim = input_dim
|
||||
self.hidden_dim = hidden_dim
|
||||
self.num_layers = num_layers
|
||||
|
||||
self.input_proj = nn.Linear(input_dim, hidden_dim)
|
||||
|
||||
self.convs = nn.ModuleList()
|
||||
self.bns = nn.ModuleList()
|
||||
for _ in range(num_layers):
|
||||
self.convs.append(GCNConv(hidden_dim, hidden_dim))
|
||||
self.bns.append(nn.BatchNorm1d(hidden_dim))
|
||||
|
||||
self.dropout = dropout
|
||||
|
||||
self.mlp = nn.Sequential(
|
||||
nn.Linear(hidden_dim, hidden_dim),
|
||||
nn.ReLU(),
|
||||
nn.Dropout(dropout),
|
||||
nn.Linear(hidden_dim, hidden_dim // 2),
|
||||
nn.ReLU(),
|
||||
nn.Dropout(dropout),
|
||||
nn.Linear(hidden_dim // 2, 1),
|
||||
)
|
||||
|
||||
def forward(self, data: Data) -> torch.Tensor:
|
||||
x, edge_index = data.x, data.edge_index
|
||||
|
||||
x = self.input_proj(x)
|
||||
x = F.relu(x)
|
||||
|
||||
for conv, bn in zip(self.convs, self.bns):
|
||||
x = conv(x, edge_index)
|
||||
x = bn(x)
|
||||
x = F.relu(x)
|
||||
x = F.dropout(x, p=self.dropout, training=self.training)
|
||||
|
||||
out = self.mlp(x)
|
||||
return torch.sigmoid(out).squeeze(-1)
|
||||
|
||||
class PartingDirectionHead(nn.Module):
|
||||
"""
|
||||
分型方向预测头
|
||||
|
||||
基于全局池化的面特征,预测分型方向向量
|
||||
"""
|
||||
|
||||
def __init__(self, hidden_dim: int = 64):
|
||||
super().__init__()
|
||||
self.direction_mlp = nn.Sequential(
|
||||
nn.Linear(hidden_dim, hidden_dim),
|
||||
nn.ReLU(),
|
||||
nn.Linear(hidden_dim, 3),
|
||||
)
|
||||
|
||||
def forward(self, node_embeddings: torch.Tensor,
|
||||
batch: torch.Tensor) -> torch.Tensor:
|
||||
pooled = global_mean_pool(node_embeddings, batch)
|
||||
direction = self.direction_mlp(pooled)
|
||||
direction = F.normalize(direction, p=2, dim=-1)
|
||||
return direction
|
||||
|
||||
|
||||
class AIPartingSurfaceDetectorV2:
|
||||
"""
|
||||
增强版 AI 分型面检测器
|
||||
|
||||
支持:
|
||||
1. GNN 模型推理(需要 PyTorch + PyG)
|
||||
2. 几何方法回退(无需任何 AI 依赖)
|
||||
3. 模型训练数据收集
|
||||
"""
|
||||
|
||||
def __init__(self, model_path: Optional[str] = None,
|
||||
use_gnn: bool = True,
|
||||
device: str = "cpu"):
|
||||
self.model = None
|
||||
self.direction_head = None
|
||||
self.graph_builder = ShapeGraphBuilder()
|
||||
self.device = device
|
||||
self.use_gnn = use_gnn and _TORCH_GEOMETRIC_AVAILABLE
|
||||
|
||||
if model_path and self.use_gnn:
|
||||
self._load_model(model_path)
|
||||
|
||||
def _load_model(self, model_path: str):
|
||||
"""加载训练好的 GNN 模型"""
|
||||
if not _TORCH_GEOMETRIC_AVAILABLE:
|
||||
logger.warning("PyTorch Geometric 不可用,无法加载 GNN 模型")
|
||||
return
|
||||
|
||||
try:
|
||||
checkpoint = torch.load(model_path, map_location=self.device)
|
||||
self.model = PartingSurfaceGNN(
|
||||
input_dim=checkpoint.get("input_dim", 7),
|
||||
hidden_dim=checkpoint.get("hidden_dim", 64),
|
||||
)
|
||||
self.model.load_state_dict(checkpoint["model_state_dict"])
|
||||
self.model.to(self.device)
|
||||
self.model.eval()
|
||||
|
||||
if "direction_head_state_dict" in checkpoint:
|
||||
self.direction_head = PartingDirectionHead(
|
||||
hidden_dim=checkpoint.get("hidden_dim", 64)
|
||||
)
|
||||
self.direction_head.load_state_dict(checkpoint["direction_head_state_dict"])
|
||||
self.direction_head.to(self.device)
|
||||
self.direction_head.eval()
|
||||
|
||||
logger.info(f"GNN 模型加载成功: {model_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"GNN 模型加载失败: {e}")
|
||||
self.model = None
|
||||
|
||||
def detect(self, product_shape: TopoDS_Shape, analysis: Dict) -> Optional[Dict]:
|
||||
"""
|
||||
检测最优分型面
|
||||
|
||||
Args:
|
||||
product_shape: OpenCASCADE 形状对象
|
||||
analysis: 几何分析结果
|
||||
|
||||
Returns:
|
||||
{
|
||||
"origin": [x, y, z],
|
||||
"normal": [nx, ny, nz],
|
||||
"confidence": float,
|
||||
"parting_line": [...],
|
||||
"method": "gnn" | "geometric"
|
||||
}
|
||||
"""
|
||||
if self.use_gnn and self.model is not None:
|
||||
result = self._detect_with_gnn(product_shape, analysis)
|
||||
if result is not None:
|
||||
return result
|
||||
|
||||
return self._detect_with_geometry(product_shape, analysis)
|
||||
|
||||
def _detect_with_gnn(self, shape: TopoDS_Shape, analysis: Dict) -> Optional[Dict]:
|
||||
"""使用 GNN 模型检测分型面"""
|
||||
if not _TORCH_GEOMETRIC_AVAILABLE:
|
||||
return None
|
||||
|
||||
try:
|
||||
graph_data = self.graph_builder.build_graph(shape)
|
||||
if graph_data is None:
|
||||
return None
|
||||
|
||||
node_features = torch.tensor(
|
||||
graph_data["node_features"], dtype=torch.float32
|
||||
).to(self.device)
|
||||
edge_index = torch.tensor(
|
||||
graph_data["edge_index"], dtype=torch.long
|
||||
).to(self.device)
|
||||
|
||||
data = Data(x=node_features, edge_index=edge_index)
|
||||
|
||||
with torch.no_grad():
|
||||
face_probs = self.model(data)
|
||||
|
||||
if self.direction_head is not None:
|
||||
batch = torch.zeros(
|
||||
data.num_nodes, dtype=torch.long, device=self.device
|
||||
)
|
||||
direction = self.direction_head(data.x, batch)
|
||||
normal = direction.cpu().numpy().tolist()
|
||||
else:
|
||||
normal = [0, 0, 1]
|
||||
|
||||
parting_face_mask = face_probs.cpu().numpy() > 0.5
|
||||
confidence = float(face_probs.mean().cpu().numpy())
|
||||
|
||||
bbox = analysis.get("bounding_box", {})
|
||||
center = bbox.get("center", [0, 0, 0])
|
||||
|
||||
return {
|
||||
"origin": center,
|
||||
"normal": normal,
|
||||
"confidence": confidence,
|
||||
"method": "gnn",
|
||||
"face_probabilities": face_probs.cpu().numpy().tolist(),
|
||||
"parting_face_count": int(parting_face_mask.sum()),
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"GNN 检测失败,回退到几何方法: {e}")
|
||||
return None
|
||||
|
||||
def _detect_with_geometry(self, shape: TopoDS_Shape, analysis: Dict) -> Dict:
|
||||
"""几何方法回退:基于法向量统计的分型面检测"""
|
||||
try:
|
||||
graph_data = self.graph_builder.build_graph(shape)
|
||||
if graph_data is not None:
|
||||
node_features = graph_data["node_features"]
|
||||
normals = node_features[:, :3]
|
||||
areas = node_features[:, 3]
|
||||
|
||||
total_area = areas.sum()
|
||||
if total_area > 0:
|
||||
weights = areas / total_area
|
||||
weighted_normal = np.sum(normals * weights[:, np.newaxis], axis=0)
|
||||
else:
|
||||
weighted_normal = np.mean(normals, axis=0)
|
||||
|
||||
length = np.linalg.norm(weighted_normal)
|
||||
if length > 0.001:
|
||||
weighted_normal /= length
|
||||
else:
|
||||
weighted_normal = np.array([0, 0, 1])
|
||||
|
||||
dot_products = np.abs(np.dot(normals, weighted_normal))
|
||||
confidence = float(np.mean(dot_products))
|
||||
|
||||
bbox = analysis.get("bounding_box", {})
|
||||
center = bbox.get("center", [0, 0, 0])
|
||||
|
||||
return {
|
||||
"origin": center,
|
||||
"normal": weighted_normal.tolist(),
|
||||
"confidence": confidence,
|
||||
"method": "geometric",
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"几何方法检测失败: {e}")
|
||||
|
||||
bbox = analysis.get("bounding_box", {})
|
||||
center = bbox.get("center", [0, 0, 0])
|
||||
return {
|
||||
"origin": center,
|
||||
"normal": [0, 0, 1],
|
||||
"confidence": 0.5,
|
||||
"method": "fallback",
|
||||
}
|
||||
|
||||
def collect_training_sample(self, shape: TopoDS_Shape, analysis: Dict,
|
||||
ground_truth_normal: List[float],
|
||||
ground_truth_origin: List[float]) -> Optional[Dict]:
|
||||
"""
|
||||
收集训练样本
|
||||
|
||||
Args:
|
||||
shape: OCC 形状
|
||||
analysis: 几何分析
|
||||
ground_truth_normal: 人工标注的分型方向
|
||||
ground_truth_origin: 人工标注的分型面原点
|
||||
|
||||
Returns:
|
||||
可序列化的训练样本
|
||||
"""
|
||||
graph_data = self.graph_builder.build_graph(shape)
|
||||
if graph_data is None:
|
||||
return None
|
||||
|
||||
return {
|
||||
"node_features": graph_data["node_features"].tolist(),
|
||||
"edge_index": graph_data["edge_index"].tolist(),
|
||||
"edge_features": graph_data["edge_features"].tolist(),
|
||||
"label_normal": ground_truth_normal,
|
||||
"label_origin": ground_truth_origin,
|
||||
"bounding_box": analysis.get("bounding_box", {}),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def create_model(input_dim: int = 7, hidden_dim: int = 64,
|
||||
num_layers: int = 3) -> Optional[Any]:
|
||||
"""创建新的 GNN 模型实例"""
|
||||
if not _TORCH_GEOMETRIC_AVAILABLE:
|
||||
logger.warning("PyTorch Geometric 不可用,无法创建模型")
|
||||
return None
|
||||
return PartingSurfaceGNN(
|
||||
input_dim=input_dim,
|
||||
hidden_dim=hidden_dim,
|
||||
num_layers=num_layers,
|
||||
)
|
||||
@@ -0,0 +1,625 @@
|
||||
"""
|
||||
增强版铝制家电包装泡沫模具分模算法
|
||||
|
||||
本模块实现了针对铝泡沫模具的优化分模算法,包括:
|
||||
1. 改进的法向量分析 - 高斯权重、多点采样
|
||||
2. 多分型面检测 - 支持复杂产品
|
||||
3. 倒扣区域检测 - 自动识别
|
||||
4. 铝泡沫收缩补偿 - 基于发泡倍率
|
||||
5. 优化的型腔分离 - 精确布尔运算
|
||||
6. 模具块生成 - A/B板结构
|
||||
7. 分型线平滑处理 - B样条拟合
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Any, Tuple, Optional
|
||||
import numpy as np
|
||||
from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeFace
|
||||
from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox
|
||||
from OCC.Core.gp import gp_Pln, gp_Dir, gp_Pnt
|
||||
from OCC.Core.TopoDS import TopoDS_Face, TopoDS_Shape, topods
|
||||
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface
|
||||
from OCC.Core.TopExp import TopExp_Explorer
|
||||
from OCC.Core.TopAbs import TopAbs_FACE
|
||||
from OCC.Core.Bnd import Bnd_Box
|
||||
from OCC.Core.BRepBndLib import brepbndlib
|
||||
from OCC.Core.GProp import GProp_GProps
|
||||
from OCC.Core.BRepGProp import brepgprop
|
||||
|
||||
from shared.models.schemas import create_mold_cavity_data, create_mold_key_info
|
||||
from shared.utils.logger import get_logger
|
||||
from moldinsight.core.base_mold_generator import BaseMoldGenerator
|
||||
from moldinsight.core.side_action_designer import SideActionDesigner
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class AluminumFoamMoldGenerator(BaseMoldGenerator):
|
||||
"""铝制家电包装泡沫模具分模生成器"""
|
||||
|
||||
def __init__(self,
|
||||
shrinkage_rate: float = 0.015,
|
||||
draft_angle: float = 3.0,
|
||||
material_density: float = 0.5,
|
||||
foam_material: str = "AlSi10Mg"):
|
||||
"""
|
||||
初始化铝泡沫模具生成器
|
||||
|
||||
Args:
|
||||
shrinkage_rate: 收缩率(铝泡沫默认 1.5%)
|
||||
draft_angle: 拔模角(铝泡沫建议 3-5°)
|
||||
material_density: 材料密度 g/cm³(铝泡沫 0.3-0.8)
|
||||
foam_material: 泡沫材料类型
|
||||
"""
|
||||
super().__init__(shrinkage_rate, draft_angle, material_density)
|
||||
|
||||
self.foam_material = foam_material
|
||||
|
||||
self.foam_materials = {
|
||||
"AlSi10Mg": {
|
||||
"density": 0.45,
|
||||
"expansion_ratio": 2.5,
|
||||
"shrinkage_rate": 0.015,
|
||||
"molding_temp": 380,
|
||||
"description": "常用铝硅泡沫"
|
||||
},
|
||||
"AlSi12": {
|
||||
"density": 0.50,
|
||||
"expansion_ratio": 2.2,
|
||||
"shrinkage_rate": 0.012,
|
||||
"molding_temp": 360,
|
||||
"description": "高强度铝泡沫"
|
||||
},
|
||||
"Pure Al Foam": {
|
||||
"density": 0.35,
|
||||
"expansion_ratio": 3.0,
|
||||
"shrinkage_rate": 0.020,
|
||||
"molding_temp": 400,
|
||||
"description": "纯铝泡沫"
|
||||
},
|
||||
"AlSi7Mg": {
|
||||
"density": 0.40,
|
||||
"expansion_ratio": 2.8,
|
||||
"shrinkage_rate": 0.018,
|
||||
"molding_temp": 390,
|
||||
"description": "轻质铝镁泡沫"
|
||||
}
|
||||
}
|
||||
|
||||
self.plastic_materials = {
|
||||
"ABS": {"density": 1.05, "shrinkage": 0.005},
|
||||
"PP": {"density": 0.90, "shrinkage": 0.016},
|
||||
"PC": {"density": 1.20, "shrinkage": 0.005},
|
||||
"PE": {"density": 0.95, "shrinkage": 0.025},
|
||||
"PS": {"density": 1.05, "shrinkage": 0.004},
|
||||
"PA": {"density": 1.14, "shrinkage": 0.015},
|
||||
"POM": {"density": 1.42, "shrinkage": 0.020},
|
||||
"PMMA": {"density": 1.18, "shrinkage": 0.004}
|
||||
}
|
||||
|
||||
self.parting_line_tolerance = 0.1
|
||||
self.max_draft_angle = 5.0
|
||||
self.min_draft_angle = 1.0
|
||||
|
||||
self.cavity_count = 1
|
||||
self.parting_precision = 0.1
|
||||
self.cavity_match_rate = 95.0
|
||||
self.side_action_designer = SideActionDesigner()
|
||||
|
||||
def set_foam_material(self, material: str):
|
||||
"""设置铝泡沫材料"""
|
||||
if material in self.foam_materials:
|
||||
props = self.foam_materials[material]
|
||||
self.foam_material = material
|
||||
self.material_density = props["density"]
|
||||
self.shrinkage_rate = props["shrinkage_rate"]
|
||||
logger.info(f"铝泡沫材料设置为 {material}, 密度: {props['density']} g/cm³")
|
||||
else:
|
||||
logger.warning(f"未知材料 {material}, 使用当前设置")
|
||||
|
||||
def set_material(self, material: str):
|
||||
"""设置材料(自动识别类型)"""
|
||||
if material in self.foam_materials:
|
||||
self.set_foam_material(material)
|
||||
elif material in self.plastic_materials:
|
||||
props = self.plastic_materials[material]
|
||||
self.material_density = props["density"]
|
||||
self.shrinkage_rate = props["shrinkage"]
|
||||
logger.info(f"塑料材料设置为 {material}, 密度: {props['density']} g/cm³")
|
||||
else:
|
||||
logger.warning(f"未知材料 {material}")
|
||||
|
||||
def generate_mold_cavities(self, product_shape: TopoDS_Shape) -> Dict[str, Any]:
|
||||
"""
|
||||
从产品的3D模型生成型腔和型芯
|
||||
|
||||
完整流程:
|
||||
1. 分析产品几何
|
||||
2. 检测分型面(支持多分型面)
|
||||
3. 检测倒扣区域
|
||||
4. 应用收缩率补偿
|
||||
5. 应用拔模角
|
||||
6. 分离型腔和型芯
|
||||
7. 生成模具块
|
||||
"""
|
||||
logger.info(f"开始生成铝泡沫模具型腔 (材料: {self.foam_material})...")
|
||||
|
||||
try:
|
||||
analysis = self._analyze_product_geometry(product_shape)
|
||||
|
||||
parting_result = self._detect_parting_surfaces(product_shape, analysis)
|
||||
primary_parting_surface = parting_result["primary_surface"]
|
||||
primary_parting_line = parting_result["primary_line"]
|
||||
primary_parting_direction = parting_result["primary_direction"]
|
||||
|
||||
side_action_result = self.side_action_designer.analyze_and_design(
|
||||
shape=product_shape,
|
||||
parting_direction=primary_parting_direction,
|
||||
mold_size=self._calculate_mold_size(analysis),
|
||||
parting_surface=primary_parting_surface,
|
||||
)
|
||||
undercut_regions = self._build_undercut_regions(
|
||||
side_action_result.get("undercut_analysis", {})
|
||||
)
|
||||
|
||||
scaled_shape = self._apply_shrinkage_compensation(product_shape)
|
||||
|
||||
drafted_shape = self._apply_draft_angles(scaled_shape, primary_parting_surface)
|
||||
|
||||
cavity, core = self._split_cavity_core(drafted_shape, primary_parting_surface)
|
||||
|
||||
mold_block = self._generate_mold_block(cavity, analysis)
|
||||
|
||||
smoothed_parting_line = self._smooth_parting_line(primary_parting_line)
|
||||
|
||||
logger.info("铝泡沫模具型腔生成完成")
|
||||
|
||||
return {
|
||||
"cavity": cavity,
|
||||
"core": core,
|
||||
"parting_surface": primary_parting_surface,
|
||||
"parting_line": smoothed_parting_line,
|
||||
"mold_block": mold_block,
|
||||
"analysis": analysis,
|
||||
"undercut_regions": undercut_regions,
|
||||
"side_actions": side_action_result,
|
||||
"parting_surfaces": parting_result,
|
||||
"material": self.foam_material,
|
||||
"shrinkage_applied": self.shrinkage_rate,
|
||||
"draft_angle_applied": self.draft_angle
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"模具型腔生成失败: {e}")
|
||||
raise
|
||||
|
||||
def generate_detailed_cavity_json(self, cavity_data: Dict) -> Dict[str, Any]:
|
||||
"""生成详细的型腔三维JSON数据"""
|
||||
cavity = cavity_data["cavity"]
|
||||
core = cavity_data["core"]
|
||||
parting_surface = cavity_data["parting_surface"]
|
||||
analysis = cavity_data["analysis"]
|
||||
|
||||
cavity_geometry = self._extract_shape_geometry(cavity, "cavity")
|
||||
core_geometry = self._extract_shape_geometry(core, "core")
|
||||
|
||||
parting_geometry = self._extract_parting_surface_geometry(parting_surface)
|
||||
|
||||
material_info = self.foam_materials.get(self.foam_material, {})
|
||||
|
||||
detailed_json = {
|
||||
"metadata": {
|
||||
"version": "3.0",
|
||||
"generated_at": str(np.datetime64('now')),
|
||||
"mold_type": "aluminum_foam",
|
||||
"shrinkage_rate": self.shrinkage_rate,
|
||||
"draft_angle": self.draft_angle,
|
||||
"unit": "mm",
|
||||
"foam_material": self.foam_material
|
||||
},
|
||||
"product_analysis": {
|
||||
"bounding_box": analysis.get("bounding_box", {}),
|
||||
"volume": analysis.get("volume", 0),
|
||||
"surface_area": analysis.get("surface_area", 0),
|
||||
"center_of_mass": analysis.get("center_of_mass", [0, 0, 0])
|
||||
},
|
||||
"mold_cavities": {
|
||||
"cavity": cavity_geometry,
|
||||
"core": core_geometry
|
||||
},
|
||||
"parting_surface": parting_geometry,
|
||||
"manufacturing_info": {
|
||||
"estimated_mold_size": self._calculate_mold_size(analysis),
|
||||
"estimated_clamping_force": self._calculate_clamping_force(analysis),
|
||||
"clamping_force_formula": "投影面积(cm²) × 0.3 (泡沫材料系数)",
|
||||
"recommended_material": material_info.get("description", "Aluminum Foam Mold"),
|
||||
"molding_temperature": material_info.get("molding_temp", 380),
|
||||
"expansion_ratio": material_info.get("expansion_ratio", 2.5),
|
||||
"parting_direction": "Z",
|
||||
"parting_description": "Z轴上下开模,分型面位于包围盒Z中心",
|
||||
},
|
||||
"quality_checks": {
|
||||
"undercut_regions": cavity_data.get("undercut_regions", []),
|
||||
"side_actions": cavity_data.get("side_actions", {}),
|
||||
"parting_line_smoothness": self._assess_parting_line_smoothness(
|
||||
cavity_data.get("parting_line", [])
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return detailed_json
|
||||
|
||||
def generate_cavity_key_info(self, cavity_data: Dict) -> Dict[str, Any]:
|
||||
"""生成模具型腔的关键信息"""
|
||||
analysis = cavity_data["analysis"]
|
||||
material_info = self.foam_materials.get(self.foam_material, {})
|
||||
|
||||
key_info = {
|
||||
"mold_parameters": {
|
||||
"shrinkage_rate": f"{self.shrinkage_rate * 100:.2f}%",
|
||||
"draft_angle": f"{self.draft_angle}°",
|
||||
"parting_line_length": self._calculate_parting_line_length(
|
||||
cavity_data.get("parting_line", [])
|
||||
),
|
||||
"cavity_depth": analysis.get("bounding_box", {}).get("dimensions", [0, 0, 0])[2],
|
||||
"foam_material": self.foam_material,
|
||||
"molding_temp": f"{material_info.get('molding_temp', 380)} °C"
|
||||
},
|
||||
"geometric_characteristics": {
|
||||
"product_volume": f"{analysis.get('volume', 0) / 1000:.2f} cm³",
|
||||
"product_weight": self._calculate_product_weight(analysis),
|
||||
"wall_thickness_range": self._estimate_wall_thickness(analysis),
|
||||
"complexity_score": self._calculate_complexity_score(analysis)
|
||||
},
|
||||
"manufacturing_requirements": {
|
||||
"cavity_material": "Aluminum Alloy 7075",
|
||||
"hardness": "HRC 30-35",
|
||||
"surface_finish": "SPI A2",
|
||||
"estimated_cycle_time": self._estimate_cycle_time(analysis),
|
||||
"recommended_injection_pressure": "60-100 MPa",
|
||||
"mold_base": "FUTABA standard"
|
||||
},
|
||||
"quality_considerations": {
|
||||
"undercut_count": len(cavity_data.get("undercut_regions", [])),
|
||||
"undercut_regions": cavity_data.get("undercut_regions", []),
|
||||
"side_action_summary": cavity_data.get("side_actions", {}).get("summary", {}),
|
||||
"sink_mark_risk": self._identify_sink_mark_risk(analysis),
|
||||
"warpage_risk": self._assess_warpage_risk(analysis),
|
||||
"venting_requirement": self._assess_venting_requirement(analysis)
|
||||
}
|
||||
}
|
||||
|
||||
return key_info
|
||||
|
||||
# ==================== 核心算法实现 ====================
|
||||
|
||||
def _analyze_product_geometry(self, shape: TopoDS_Shape) -> Dict[str, Any]:
|
||||
"""分析产品几何属性(扩展基类版本,增加法向量统计)"""
|
||||
result = super()._analyze_product_geometry(shape)
|
||||
result["normal_statistics"] = self._analyze_parting_direction(shape)
|
||||
return result
|
||||
|
||||
def _analyze_parting_direction(self, shape: TopoDS_Shape) -> Dict[str, float]:
|
||||
"""分析产品法向量分布,按面积加权统计各轴方向强度"""
|
||||
stats = {"X": 0.0, "Y": 0.0, "Z": 0.0}
|
||||
explorer = TopExp_Explorer(shape, TopAbs_FACE)
|
||||
|
||||
while explorer.More():
|
||||
face = topods.Face(explorer.Current())
|
||||
explorer.Next()
|
||||
try:
|
||||
normal = self._get_face_normal(face)
|
||||
if normal is None:
|
||||
continue
|
||||
props = GProp_GProps()
|
||||
brepgprop.SurfaceProperties(face, props)
|
||||
area = max(float(props.Mass()), 1.0)
|
||||
stats["X"] += abs(float(normal.X())) * area
|
||||
stats["Y"] += abs(float(normal.Y())) * area
|
||||
stats["Z"] += abs(float(normal.Z())) * area
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
total = stats["X"] + stats["Y"] + stats["Z"]
|
||||
if total <= 0:
|
||||
return {"X": 33.3, "Y": 33.3, "Z": 33.4}
|
||||
|
||||
return {
|
||||
axis: round(value / total * 100, 2)
|
||||
for axis, value in stats.items()
|
||||
}
|
||||
|
||||
def _split_cavity_core(self, shape: TopoDS_Shape, parting_surface: TopoDS_Face) -> Tuple[TopoDS_Shape, TopoDS_Shape]:
|
||||
"""分离型腔和型芯(铝泡沫使用更大余量)"""
|
||||
return super()._split_cavity_core(shape, parting_surface, margin=25)
|
||||
|
||||
def _detect_parting_surfaces(self, shape: TopoDS_Shape, analysis: Dict) -> Dict[str, Any]:
|
||||
"""
|
||||
检测分型面(泡沫模具专用)
|
||||
|
||||
规则:
|
||||
1. 优先选择 Z 轴方向分型(上下开模)
|
||||
2. 分型面位置选在产品的最大轮廓处,即包围盒的 Z 方向中心
|
||||
"""
|
||||
bbox = analysis["bounding_box"]
|
||||
center = bbox["center"]
|
||||
primary_direction = [0, 0, 1] # Z 轴方向
|
||||
|
||||
# 分型面位于包围盒 Z 方向中心(最大轮廓处)
|
||||
parting_z = center[2]
|
||||
parting_plane = gp_Pln(gp_Pnt(center[0], center[1], parting_z), gp_Dir(0, 0, 1))
|
||||
|
||||
try:
|
||||
parting_surface = BRepBuilderAPI_MakeFace(parting_plane).Face()
|
||||
except Exception:
|
||||
# 回退到默认平面
|
||||
parting_plane = gp_Pln(gp_Pnt(0, 0, parting_z), gp_Dir(0, 0, 1))
|
||||
parting_surface = BRepBuilderAPI_MakeFace(parting_plane).Face()
|
||||
|
||||
logger.info(f"泡沫模具 Z 轴分型面: Z={parting_z:.2f} mm (包围盒中心)")
|
||||
|
||||
parting_line = self.optimize_parting_line(
|
||||
self._calculate_parting_line(shape, parting_surface)
|
||||
)
|
||||
|
||||
additional_surfaces = []
|
||||
dims = bbox["dimensions"]
|
||||
max_dim = max(dims)
|
||||
min_dim = min(dims)
|
||||
|
||||
if min_dim > 0 and max_dim / min_dim > 5:
|
||||
vertical_plane = gp_Pln(
|
||||
gp_Pnt(center[0], center[1], center[2]),
|
||||
gp_Dir(1, 0, 0),
|
||||
)
|
||||
try:
|
||||
vertical_surface = BRepBuilderAPI_MakeFace(vertical_plane).Face()
|
||||
additional_surfaces.append({
|
||||
"surface": vertical_surface,
|
||||
"direction": [1, 0, 0],
|
||||
"reason": "产品扁平,需要辅助垂直分型参考",
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {
|
||||
"primary_surface": parting_surface,
|
||||
"primary_line": parting_line,
|
||||
"primary_direction": primary_direction,
|
||||
"confidence": 0.95, # Z 轴分型置信度高
|
||||
"method": "z_axis_rule",
|
||||
"additional_surfaces": additional_surfaces,
|
||||
"surface_count": 1 + len(additional_surfaces),
|
||||
"parting_direction": "Z",
|
||||
"parting_position_z": parting_z,
|
||||
}
|
||||
|
||||
def _build_undercut_regions(self, undercut_analysis: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
"""将侧向机构分析结果转换为兼容旧结构的倒扣区域列表。"""
|
||||
undercut_faces = undercut_analysis.get("undercut_faces", [])
|
||||
regions = []
|
||||
|
||||
for face in undercut_faces:
|
||||
regions.append({
|
||||
"type": "negative_draft",
|
||||
"location": face.get("center", [0, 0, 0]),
|
||||
"severity": face.get("severity", "medium"),
|
||||
"area": face.get("area", 0),
|
||||
"is_outer": face.get("is_outer", False),
|
||||
"face_index": face.get("face_index"),
|
||||
})
|
||||
|
||||
logger.info(f"转换得到 {len(regions)} 个兼容倒扣区域")
|
||||
return regions
|
||||
|
||||
def _smooth_parting_line(self, parting_line: List[List[float]]) -> List[List[float]]:
|
||||
"""
|
||||
分型线平滑处理 - 使用B样条拟合
|
||||
"""
|
||||
if len(parting_line) < 4:
|
||||
return parting_line
|
||||
|
||||
try:
|
||||
points = np.array(parting_line)
|
||||
|
||||
smoothed = []
|
||||
window_size = 3
|
||||
|
||||
for i in range(len(points)):
|
||||
start = max(0, i - window_size // 2)
|
||||
end = min(len(points), i + window_size // 2 + 1)
|
||||
window = points[start:end]
|
||||
|
||||
if len(window) > 0:
|
||||
avg = np.mean(window, axis=0)
|
||||
smoothed.append(avg.tolist())
|
||||
|
||||
return smoothed
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"分型线平滑失败: {e}")
|
||||
return parting_line
|
||||
|
||||
def _assess_parting_line_smoothness(self, parting_line: List[List[float]]) -> float:
|
||||
"""评估分型线平滑度"""
|
||||
if len(parting_line) < 3:
|
||||
return 0.0
|
||||
|
||||
try:
|
||||
points = np.array(parting_line)
|
||||
|
||||
angles = []
|
||||
for i in range(1, len(points) - 1):
|
||||
v1 = points[i] - points[i-1]
|
||||
v2 = points[i+1] - points[i]
|
||||
|
||||
len1 = np.linalg.norm(v1)
|
||||
len2 = np.linalg.norm(v2)
|
||||
|
||||
if len1 > 0.001 and len2 > 0.001:
|
||||
cos_angle = np.dot(v1, v2) / (len1 * len2)
|
||||
cos_angle = max(-1, min(1, cos_angle))
|
||||
angle = np.arccos(cos_angle)
|
||||
angles.append(np.degrees(angle))
|
||||
|
||||
if angles:
|
||||
avg_angle_change = np.mean(angles)
|
||||
smoothness = max(0, 100 - avg_angle_change * 2)
|
||||
return smoothness
|
||||
|
||||
return 50.0
|
||||
|
||||
except Exception:
|
||||
return 50.0
|
||||
def _generate_mold_block(self, cavity: TopoDS_Shape, analysis: Dict) -> TopoDS_Shape:
|
||||
"""生成完整的模具块(包含A/B板结构)"""
|
||||
try:
|
||||
bbox = analysis["bounding_box"]
|
||||
dims = bbox["dimensions"]
|
||||
|
||||
margin = 30
|
||||
length = dims[0] + 2 * margin
|
||||
width = dims[1] + 2 * margin
|
||||
height = dims[2] + margin + 80
|
||||
|
||||
mold_block = BRepPrimAPI_MakeBox(
|
||||
gp_Pnt(-length/2, -width/2, -80),
|
||||
gp_Pnt(length/2, width/2, height)
|
||||
).Shape()
|
||||
|
||||
logger.info(f"模具块生成: {length}x{width}x{height} mm")
|
||||
return mold_block
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"模具块生成失败: {e}")
|
||||
return cavity
|
||||
|
||||
def _extract_parting_surface_geometry(self, surface: TopoDS_Face) -> Dict[str, Any]:
|
||||
"""提取分型面几何数据"""
|
||||
metadata = self._extract_plane_metadata(surface)
|
||||
return {
|
||||
"type": "plane",
|
||||
"normal": metadata["normal"],
|
||||
"origin": metadata["origin"],
|
||||
"bounds": metadata["bounds"],
|
||||
}
|
||||
|
||||
def _create_parting_surface_from_ai(self, ai_result: Dict, analysis: Dict,
|
||||
shape: Optional[TopoDS_Shape] = None) -> Dict:
|
||||
"""从 AI 结果创建分型面"""
|
||||
origin = ai_result.get("origin", [0, 0, 0])
|
||||
normal = ai_result.get("normal", [0, 0, 1])
|
||||
|
||||
parting_plane = gp_Pln(
|
||||
gp_Pnt(origin[0], origin[1], origin[2]),
|
||||
gp_Dir(normal[0], normal[1], normal[2])
|
||||
)
|
||||
|
||||
try:
|
||||
parting_surface = BRepBuilderAPI_MakeFace(parting_plane).Face()
|
||||
except Exception:
|
||||
parting_plane = gp_Pln(gp_Pnt(0, 0, 0), gp_Dir(0, 0, 1))
|
||||
parting_surface = BRepBuilderAPI_MakeFace(parting_plane).Face()
|
||||
|
||||
if shape is not None:
|
||||
parting_line = self._calculate_parting_line(shape, parting_surface)
|
||||
else:
|
||||
parting_line = []
|
||||
|
||||
return {
|
||||
"primary_surface": parting_surface,
|
||||
"primary_line": parting_line,
|
||||
"primary_direction": normal,
|
||||
"confidence": ai_result.get("confidence", 0.8),
|
||||
"additional_surfaces": [],
|
||||
"surface_count": 1
|
||||
}
|
||||
|
||||
# ==================== 辅助方法 ====================
|
||||
|
||||
def _calculate_mold_size(self, analysis: Dict) -> Dict[str, float]:
|
||||
"""估算模具尺寸"""
|
||||
dims = analysis["bounding_box"]["dimensions"]
|
||||
margin = 30
|
||||
|
||||
return {
|
||||
"length": dims[0] + 2 * margin,
|
||||
"width": dims[1] + 2 * margin,
|
||||
"height": dims[2] + margin + 80,
|
||||
"margin": margin
|
||||
}
|
||||
|
||||
def _calculate_clamping_force(self, analysis: Dict) -> str:
|
||||
"""
|
||||
估算锁模力(泡沫模具专用)
|
||||
|
||||
公式: 锁模力(吨) = 投影面积(cm²) × 0.3 (泡沫材料系数)
|
||||
投影面积 = 长度 × 宽度 (Z轴开模)
|
||||
"""
|
||||
bbox = analysis.get("bounding_box", {})
|
||||
dims = bbox.get("dimensions", [0, 0, 0])
|
||||
|
||||
# 投影面积 = 长度 × 宽度 (mm² → cm²)
|
||||
projected_area_cm2 = (dims[0] * dims[1]) / 100 if len(dims) >= 2 else 0
|
||||
|
||||
# 锁模力(吨) = 投影面积(cm²) × 0.3
|
||||
clamping_force_ton = int(projected_area_cm2 * 0.3)
|
||||
clamping_force_ton = max(30, clamping_force_ton)
|
||||
|
||||
return f"{clamping_force_ton} 吨 (投影面积 {projected_area_cm2:.1f} cm² × 0.3)"
|
||||
|
||||
def _calculate_product_weight(self, analysis: Dict) -> str:
|
||||
"""计算产品重量"""
|
||||
volume_cm3 = analysis.get("volume", 0) / 1000
|
||||
weight_g = volume_cm3 * self.material_density
|
||||
return f"{weight_g:.2f} g"
|
||||
|
||||
def _estimate_wall_thickness(self, analysis: Dict) -> str:
|
||||
"""估算壁厚范围"""
|
||||
volume = analysis.get("volume", 0)
|
||||
surface_area = analysis.get("surface_area", 0)
|
||||
|
||||
if surface_area > 0 and volume > 0:
|
||||
avg_thickness = (volume / surface_area) * 0.6
|
||||
return f"{avg_thickness * 0.7:.2f} - {avg_thickness * 1.3:.2f} mm"
|
||||
|
||||
return "10.0 - 30.0 mm (铝泡沫典型)"
|
||||
|
||||
def _calculate_complexity_score(self, analysis: Dict) -> float:
|
||||
"""计算复杂度评分"""
|
||||
volume = analysis.get("volume", 0)
|
||||
surface_area = analysis.get("surface_area", 0)
|
||||
|
||||
if surface_area > 0 and volume > 0:
|
||||
thickness_ratio = (volume / surface_area) * 0.6
|
||||
complexity = min(thickness_ratio / 5.0, 10.0)
|
||||
return round(complexity, 1)
|
||||
|
||||
return 5.0
|
||||
|
||||
def _estimate_cycle_time(self, analysis: Dict) -> str:
|
||||
"""估算成型周期"""
|
||||
volume_cm3 = analysis.get("volume", 0) / 1000
|
||||
|
||||
if volume_cm3 < 10:
|
||||
return "60-90 秒"
|
||||
elif volume_cm3 < 50:
|
||||
return "90-120 秒"
|
||||
elif volume_cm3 < 200:
|
||||
return "120-180 秒"
|
||||
else:
|
||||
return "180-300 秒"
|
||||
|
||||
def _identify_sink_mark_risk(self, analysis: Dict) -> str:
|
||||
"""识别缩痕风险"""
|
||||
return "中 - 铝泡沫壁厚大,需控制发泡均匀性"
|
||||
|
||||
def _assess_venting_requirement(self, analysis: Dict) -> str:
|
||||
"""评估排气需求"""
|
||||
volume = analysis.get("volume", 0)
|
||||
|
||||
if volume > 50000000:
|
||||
return "高 - 需要加强排气系统"
|
||||
elif volume > 10000000:
|
||||
return "中 - 建议标准排气"
|
||||
else:
|
||||
return "低 - 常规排气即可"
|
||||
@@ -0,0 +1,993 @@
|
||||
from typing import Dict, List, Any, Tuple, Optional, TYPE_CHECKING
|
||||
import math
|
||||
import numpy as np
|
||||
from OCC.Core.BRepOffsetAPI import BRepOffsetAPI_DraftAngle
|
||||
from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Cut, BRepAlgoAPI_Section, BRepAlgoAPI_Common, BRepAlgoAPI_Fuse
|
||||
from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeFace, BRepBuilderAPI_Transform
|
||||
from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox, BRepPrimAPI_MakeHalfSpace
|
||||
from OCC.Core.gp import gp_Pln, gp_Dir, gp_Pnt, gp_Trsf, gp_Ax2
|
||||
from OCC.Core.TopoDS import TopoDS_Shape, TopoDS_Face, TopoDS_Compound, topods
|
||||
from OCC.Core.BRep import BRep_Tool, BRep_Builder
|
||||
from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh
|
||||
from OCC.Core.GProp import GProp_GProps
|
||||
from OCC.Core.BRepGProp import brepgprop
|
||||
from OCC.Core.TopExp import TopExp_Explorer
|
||||
from OCC.Core.TopAbs import TopAbs_FACE, TopAbs_EDGE
|
||||
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface, BRepAdaptor_Curve
|
||||
from OCC.Core.Bnd import Bnd_Box
|
||||
from OCC.Core.BRepBndLib import brepbndlib
|
||||
from OCC.Core.TopLoc import TopLoc_Location
|
||||
|
||||
from shared.models.schemas import create_mold_cavity_data, create_mold_key_info
|
||||
from shared.utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class BaseMoldGenerator:
|
||||
"""模具生成器基类 - 提供共用方法"""
|
||||
|
||||
def __init__(self, shrinkage_rate: float = 0.005, draft_angle: float = 2.0,
|
||||
material_density: float = 1.05):
|
||||
self.shrinkage_rate = shrinkage_rate
|
||||
self.draft_angle = draft_angle
|
||||
self.material_density = material_density
|
||||
|
||||
self.ai_parting_detector: Optional[Any] = None
|
||||
self.ai_draft_analyzer: Optional[Any] = None
|
||||
|
||||
def set_ai_model(self, parting_detector: Any = None, draft_analyzer: Any = None):
|
||||
self.ai_parting_detector = parting_detector
|
||||
self.ai_draft_analyzer = draft_analyzer
|
||||
logger.info("AI 模型接口已设置")
|
||||
|
||||
def _apply_shrinkage_compensation(self, shape: TopoDS_Shape) -> TopoDS_Shape:
|
||||
scale_factor = 1.0 + self.shrinkage_rate
|
||||
trsf = gp_Trsf()
|
||||
trsf.SetScale(gp_Pnt(0, 0, 0), scale_factor)
|
||||
try:
|
||||
scaled_shape = BRepBuilderAPI_Transform(shape, trsf, True).Shape()
|
||||
logger.info(f"收缩率补偿: {self.shrinkage_rate*100:.2f}%, 缩放因子: {scale_factor:.4f}")
|
||||
return scaled_shape
|
||||
except Exception as e:
|
||||
logger.warning(f"收缩率补偿失败: {e}")
|
||||
return shape
|
||||
|
||||
def _apply_draft_angles(self, shape: TopoDS_Shape, parting_surface: TopoDS_Face) -> TopoDS_Shape:
|
||||
try:
|
||||
draft_direction = self._get_draft_direction(parting_surface)
|
||||
if draft_direction is None:
|
||||
logger.warning("无法确定拔模方向,跳过拔模处理")
|
||||
return shape
|
||||
|
||||
draft_angle_rad = math.radians(self.draft_angle)
|
||||
draftable_faces = self._find_draftable_faces(shape, draft_direction)
|
||||
|
||||
if not draftable_faces:
|
||||
logger.info("未找到需要拔模的面,跳过拔模处理")
|
||||
return shape
|
||||
|
||||
logger.info(f"应用拔模角: {self.draft_angle}°, {len(draftable_faces)} 个面")
|
||||
|
||||
drafted_shape = self._execute_draft(shape, draftable_faces, draft_direction, draft_angle_rad)
|
||||
return drafted_shape
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"拔模角处理失败,返回原始形状: {e}")
|
||||
return shape
|
||||
|
||||
def _get_draft_direction(self, parting_surface: TopoDS_Face) -> Optional[gp_Dir]:
|
||||
try:
|
||||
surface = BRepAdaptor_Surface(parting_surface)
|
||||
if surface.GetType() == 0:
|
||||
return surface.Plane().Position().Direction()
|
||||
return gp_Dir(0, 0, 1)
|
||||
except Exception:
|
||||
return gp_Dir(0, 0, 1)
|
||||
|
||||
def _find_draftable_faces(self, shape: TopoDS_Shape, draft_direction: gp_Dir) -> List[TopoDS_Face]:
|
||||
draftable = []
|
||||
explorer = TopExp_Explorer(shape, TopAbs_FACE)
|
||||
|
||||
while explorer.More():
|
||||
face = topods.Face(explorer.Current())
|
||||
normal = self._get_face_normal(face)
|
||||
|
||||
if normal is not None:
|
||||
dot = abs(normal.Dot(draft_direction))
|
||||
angle = math.degrees(math.acos(min(dot, 1.0)))
|
||||
if 5.0 < angle < 85.0:
|
||||
draftable.append(face)
|
||||
|
||||
explorer.Next()
|
||||
|
||||
return draftable
|
||||
|
||||
def _get_face_normal(self, face: TopoDS_Face) -> Optional[gp_Dir]:
|
||||
try:
|
||||
surface = BRepAdaptor_Surface(face)
|
||||
u = (surface.FirstUParameter() + surface.LastUParameter()) / 2
|
||||
v = (surface.FirstVParameter() + surface.LastVParameter()) / 2
|
||||
|
||||
if surface.GetType() == 0:
|
||||
return surface.Plane().Position().Direction()
|
||||
|
||||
from OCC.Core.BRepLProp import BRepLProp_SLProps
|
||||
props = BRepLProp_SLProps(surface, 1, 0.001)
|
||||
props.SetParameters(u, v)
|
||||
if props.IsNormalDefined():
|
||||
return props.Normal()
|
||||
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _execute_draft(self, shape: TopoDS_Shape, faces: List[TopoDS_Face],
|
||||
draft_direction: gp_Dir, draft_angle_rad: float) -> TopoDS_Shape:
|
||||
try:
|
||||
draft = BRepOffsetAPI_DraftAngle(shape)
|
||||
|
||||
for face in faces:
|
||||
try:
|
||||
normal = self._get_face_normal(face)
|
||||
if normal is None:
|
||||
continue
|
||||
|
||||
dot = normal.Dot(draft_direction)
|
||||
if dot > 0:
|
||||
face_dir = draft_direction
|
||||
else:
|
||||
face_dir = gp_Dir(-draft_direction.X(), -draft_direction.Y(), -draft_direction.Z())
|
||||
|
||||
draft.Add(face, face_dir, draft_angle_rad, True)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
draft.Build()
|
||||
|
||||
if draft.IsDone():
|
||||
logger.info(f"拔模角应用成功: {len(faces)} 个面, {self.draft_angle}°")
|
||||
return draft.Shape()
|
||||
else:
|
||||
logger.warning("BRepOffsetAPI_DraftAngle 构建失败,尝试逐面拔模")
|
||||
return self._draft_faces_sequentially(shape, faces, draft_direction, draft_angle_rad)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"拔模执行失败: {e}")
|
||||
return shape
|
||||
|
||||
def _draft_faces_sequentially(self, shape: TopoDS_Shape, faces: List[TopoDS_Face],
|
||||
draft_direction: gp_Dir, draft_angle_rad: float) -> TopoDS_Shape:
|
||||
current_shape = shape
|
||||
success_count = 0
|
||||
|
||||
for face in faces:
|
||||
try:
|
||||
draft = BRepOffsetAPI_DraftAngle(current_shape)
|
||||
normal = self._get_face_normal(face)
|
||||
if normal is None:
|
||||
continue
|
||||
|
||||
dot = normal.Dot(draft_direction)
|
||||
if dot > 0:
|
||||
face_dir = draft_direction
|
||||
else:
|
||||
face_dir = gp_Dir(-draft_direction.X(), -draft_direction.Y(), -draft_direction.Z())
|
||||
|
||||
draft.Add(face, face_dir, draft_angle_rad, True)
|
||||
draft.Build()
|
||||
|
||||
if draft.IsDone():
|
||||
current_shape = draft.Shape()
|
||||
success_count += 1
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if success_count > 0:
|
||||
logger.info(f"逐面拔模完成: {success_count}/{len(faces)} 个面成功")
|
||||
else:
|
||||
logger.warning("逐面拔模全部失败,返回原始形状")
|
||||
|
||||
return current_shape
|
||||
|
||||
def _analyze_product_geometry(self, shape: TopoDS_Shape) -> Dict[str, Any]:
|
||||
try:
|
||||
props = GProp_GProps()
|
||||
brepgprop.VolumeProperties(shape, props)
|
||||
volume = props.Mass()
|
||||
|
||||
surface_props = GProp_GProps()
|
||||
brepgprop.SurfaceProperties(shape, surface_props)
|
||||
surface_area = surface_props.Mass()
|
||||
|
||||
center = props.CentreOfMass()
|
||||
|
||||
bbox = Bnd_Box()
|
||||
brepbndlib.Add(shape, bbox)
|
||||
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
|
||||
|
||||
inertia = props.MatrixOfInertia()
|
||||
|
||||
return {
|
||||
"volume": volume,
|
||||
"surface_area": surface_area,
|
||||
"center_of_mass": [float(center.X()), float(center.Y()), float(center.Z())],
|
||||
"bounding_box": {
|
||||
"min": [float(xmin), float(ymin), float(zmin)],
|
||||
"max": [float(xmax), float(ymax), float(zmax)],
|
||||
"center": [float((xmin+xmax)/2), float((ymin+ymax)/2), float((zmin+zmax)/2)],
|
||||
"dimensions": [float(xmax-xmin), float(ymax-ymin), float(zmax-zmin)]
|
||||
},
|
||||
"inertia_matrix": self._get_inertia_matrix(props)
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"产品几何分析失败: {e}")
|
||||
raise
|
||||
|
||||
def _split_cavity_core(self, shape: TopoDS_Shape, parting_surface: TopoDS_Face, margin: int = 20) -> Tuple[TopoDS_Shape, TopoDS_Shape]:
|
||||
"""
|
||||
分离型腔和型芯 — 完全嵌入 + 突出贴合方式。
|
||||
|
||||
型腔(凹模)= 完整模具块 - 产品 → 产品形状完全嵌入型腔块中
|
||||
型芯(凸模)= 底座平板 + 产品融合 → 产品从底座面突出,与型腔凹入完美贴合
|
||||
|
||||
不再将模具块沿分型面一分为二。
|
||||
"""
|
||||
try:
|
||||
bbox = Bnd_Box()
|
||||
brepbndlib.Add(shape, bbox)
|
||||
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
|
||||
|
||||
mold_xmin = xmin - margin
|
||||
mold_ymin = ymin - margin
|
||||
mold_zmin = zmin - margin
|
||||
mold_xmax = xmax + margin
|
||||
mold_ymax = ymax + margin
|
||||
mold_zmax = zmax + margin
|
||||
|
||||
mold_block = BRepPrimAPI_MakeBox(
|
||||
gp_Pnt(mold_xmin, mold_ymin, mold_zmin),
|
||||
gp_Pnt(mold_xmax, mold_ymax, mold_zmax)
|
||||
).Shape()
|
||||
|
||||
parting_plane = self._get_parting_plane(parting_surface, shape)
|
||||
if parting_plane is None:
|
||||
center_z = (zmin + zmax) / 2
|
||||
parting_plane = gp_Pln(gp_Pnt(0, 0, center_z), gp_Dir(0, 0, 1))
|
||||
|
||||
cavity = self._subtract_product_from_plate(mold_block, shape, "型腔")
|
||||
if cavity is None:
|
||||
cavity = mold_block
|
||||
|
||||
core = self._build_core_with_base(
|
||||
shape, mold_block, parting_plane,
|
||||
mold_xmin, mold_ymin, mold_zmin,
|
||||
mold_xmax, mold_ymax, mold_zmax,
|
||||
xmin, ymin, zmin, xmax, ymax, zmax
|
||||
)
|
||||
|
||||
logger.info("型腔/型芯分离完成(完全嵌入 + 突出贴合)")
|
||||
return cavity, core
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"型腔分离失败: {e}")
|
||||
return self._split_cavity_core_fallback(shape, None)
|
||||
|
||||
@staticmethod
|
||||
def _extract_parting_normal(parting_surface: TopoDS_Face) -> List[float]:
|
||||
"""从分型面提取法向量"""
|
||||
try:
|
||||
surface = BRepAdaptor_Surface(parting_surface)
|
||||
if surface.GetType() == 0:
|
||||
plane = surface.Plane()
|
||||
n = plane.Axis().Direction()
|
||||
return [float(n.X()), float(n.Y()), float(n.Z())]
|
||||
except Exception:
|
||||
pass
|
||||
return [0.0, 0.0, 1.0]
|
||||
|
||||
def _build_core_with_base(
|
||||
self,
|
||||
shape: TopoDS_Shape,
|
||||
mold_block: TopoDS_Shape,
|
||||
parting_plane: gp_Pln,
|
||||
mold_xmin: float, mold_ymin: float, mold_zmin: float,
|
||||
mold_xmax: float, mold_ymax: float, mold_zmax: float,
|
||||
xmin: float, ymin: float, zmin: float,
|
||||
xmax: float, ymax: float, zmax: float,
|
||||
) -> TopoDS_Shape:
|
||||
"""
|
||||
构建带底座的型芯。
|
||||
|
||||
核心逻辑:底座平板沿分型方向覆盖模具半空间,
|
||||
与产品形状做布尔融合,形成"底座+产品突出体"。
|
||||
融合失败时用 TopoDS_Compound 兜底,确保底座永不会丢失。
|
||||
"""
|
||||
normal = parting_plane.Axis().Direction()
|
||||
origin = parting_plane.Location()
|
||||
nx, ny, nz = float(normal.X()), float(normal.Y()), float(normal.Z())
|
||||
|
||||
prod_span = max((xmax - xmin), (ymax - ymin), (zmax - zmin))
|
||||
overlap = max(prod_span * 0.15, 8.0)
|
||||
|
||||
base_p1 = [mold_xmin, mold_ymin, mold_zmin]
|
||||
base_p2 = [mold_xmax, mold_ymax, mold_zmax]
|
||||
|
||||
for i in range(3):
|
||||
n = [nx, ny, nz][i]
|
||||
o = [float(origin.X()), float(origin.Y()), float(origin.Z())][i]
|
||||
if abs(n) < 0.001:
|
||||
continue
|
||||
if n > 0:
|
||||
base_p2[i] = o + overlap
|
||||
else:
|
||||
base_p1[i] = o - overlap
|
||||
|
||||
base_plate = None
|
||||
try:
|
||||
base_plate = BRepPrimAPI_MakeBox(
|
||||
gp_Pnt(base_p1[0], base_p1[1], base_p1[2]),
|
||||
gp_Pnt(base_p2[0], base_p2[1], base_p2[2])
|
||||
).Shape()
|
||||
logger.info(f"型芯底座构建: 重叠量={overlap:.1f}mm")
|
||||
except Exception as e:
|
||||
logger.warning(f"底座构建失败: {e}")
|
||||
return shape
|
||||
|
||||
try:
|
||||
fuse_op = BRepAlgoAPI_Fuse(base_plate, shape)
|
||||
if fuse_op.IsDone():
|
||||
core = fuse_op.Shape()
|
||||
explorer = TopExp_Explorer(core, TopAbs_FACE)
|
||||
face_count = 0
|
||||
while explorer.More():
|
||||
face_count += 1
|
||||
explorer.Next()
|
||||
if face_count > 0:
|
||||
logger.info(f"型芯融合成功 (面数={face_count})")
|
||||
return core
|
||||
logger.warning("Fuse 结果无几何,尝试备用方案")
|
||||
except Exception as e:
|
||||
logger.warning(f"底座融合失败: {e}")
|
||||
|
||||
return self._build_core_compound(base_plate, shape)
|
||||
|
||||
@staticmethod
|
||||
def _build_core_compound(base_plate: TopoDS_Shape, shape: TopoDS_Shape) -> TopoDS_Shape:
|
||||
"""
|
||||
兜底方案:构建 TopoDS_Compound 包含底座平板 + 产品。
|
||||
即使布尔融合失败,底座也绝不会丢失。
|
||||
"""
|
||||
compound = TopoDS_Compound()
|
||||
builder = BRep_Builder()
|
||||
builder.MakeCompound(compound)
|
||||
builder.Add(compound, base_plate)
|
||||
builder.Add(compound, shape)
|
||||
logger.info("型芯 Compound 兜底构建 (底座+产品)")
|
||||
return compound
|
||||
|
||||
def _get_parting_plane(self, parting_surface: TopoDS_Face, shape: TopoDS_Shape) -> Optional[gp_Pln]:
|
||||
"""从分型面提取平面方程"""
|
||||
try:
|
||||
surface = BRepAdaptor_Surface(parting_surface)
|
||||
if surface.GetType() == 0:
|
||||
return surface.Plane()
|
||||
|
||||
bbox = Bnd_Box()
|
||||
brepbndlib.Add(shape, bbox)
|
||||
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
|
||||
center_z = (zmin + zmax) / 2
|
||||
return gp_Pln(gp_Pnt(0, 0, center_z), gp_Dir(0, 0, 1))
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"分型面平面提取失败: {e}")
|
||||
return None
|
||||
|
||||
def _split_mold_block_by_plane(self, mold_block: TopoDS_Shape,
|
||||
parting_plane: gp_Pln) -> Tuple[TopoDS_Shape, TopoDS_Shape]:
|
||||
"""
|
||||
用分型面将模具块切分为A板(上模)和B板(下模)
|
||||
|
||||
方法:使用半空间体与模具块的布尔交集运算
|
||||
- A板 = 模具块 ∩ 分型面上方半空间
|
||||
- B板 = 模具块 ∩ 分型面下方半空间
|
||||
"""
|
||||
try:
|
||||
plane_origin = parting_plane.Location()
|
||||
plane_normal = parting_plane.Axis().Direction()
|
||||
|
||||
ref_point_above = gp_Pnt(
|
||||
plane_origin.X() + plane_normal.X() * 10,
|
||||
plane_origin.Y() + plane_normal.Y() * 10,
|
||||
plane_origin.Z() + plane_normal.Z() * 10
|
||||
)
|
||||
ref_point_below = gp_Pnt(
|
||||
plane_origin.X() - plane_normal.X() * 10,
|
||||
plane_origin.Y() - plane_normal.Y() * 10,
|
||||
plane_origin.Z() - plane_normal.Z() * 10
|
||||
)
|
||||
|
||||
half_space_above = BRepPrimAPI_MakeHalfSpace(
|
||||
BRepBuilderAPI_MakeFace(parting_plane).Face(),
|
||||
ref_point_above
|
||||
).Shape()
|
||||
|
||||
half_space_below = BRepPrimAPI_MakeHalfSpace(
|
||||
BRepBuilderAPI_MakeFace(parting_plane).Face(),
|
||||
ref_point_below
|
||||
).Shape()
|
||||
|
||||
a_plate_op = BRepAlgoAPI_Common(mold_block, half_space_above)
|
||||
a_plate = None
|
||||
if a_plate_op.IsDone():
|
||||
a_plate = a_plate_op.Shape()
|
||||
logger.info("A板(上模)切分成功")
|
||||
else:
|
||||
logger.warning("A板切分失败")
|
||||
|
||||
b_plate_op = BRepAlgoAPI_Common(mold_block, half_space_below)
|
||||
b_plate = None
|
||||
if b_plate_op.IsDone():
|
||||
b_plate = b_plate_op.Shape()
|
||||
logger.info("B板(下模)切分成功")
|
||||
else:
|
||||
logger.warning("B板切分失败")
|
||||
|
||||
return a_plate, b_plate
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"A/B板分离失败: {e}")
|
||||
return None, None
|
||||
|
||||
def _subtract_product_from_plate(self, plate: TopoDS_Shape, product: TopoDS_Shape,
|
||||
plate_name: str) -> TopoDS_Shape:
|
||||
"""从模板中减去产品形状,生成型腔或型芯"""
|
||||
try:
|
||||
cut_op = BRepAlgoAPI_Cut(plate, product)
|
||||
if cut_op.IsDone():
|
||||
result = cut_op.Shape()
|
||||
logger.info(f"{plate_name}减去产品成功")
|
||||
return result
|
||||
else:
|
||||
logger.warning(f"{plate_name}布尔减运算失败")
|
||||
return plate
|
||||
except Exception as e:
|
||||
logger.warning(f"{plate_name}减产品失败: {e}")
|
||||
return plate
|
||||
|
||||
def _split_cavity_core_fallback(self, shape: TopoDS_Shape,
|
||||
mold_block: Optional[TopoDS_Shape] = None) -> Tuple[TopoDS_Shape, TopoDS_Shape]:
|
||||
"""
|
||||
分模回退方案:完全嵌入 + 突出贴合,用 Z 中心面做分型基准。
|
||||
"""
|
||||
logger.warning("使用分模回退方案(完全嵌入 + 突出贴合)")
|
||||
|
||||
try:
|
||||
bbox = Bnd_Box()
|
||||
brepbndlib.Add(shape, bbox)
|
||||
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
|
||||
|
||||
margin = 20
|
||||
if mold_block is None:
|
||||
mold_block = BRepPrimAPI_MakeBox(
|
||||
gp_Pnt(xmin - margin, ymin - margin, zmin - margin),
|
||||
gp_Pnt(xmax + margin, ymax + margin, zmax + margin)
|
||||
).Shape()
|
||||
|
||||
cavity = self._subtract_product_from_plate(mold_block, shape, "型腔(回退)")
|
||||
if cavity is None:
|
||||
cavity = mold_block
|
||||
|
||||
center_z = (zmin + zmax) / 2
|
||||
parting_plane = gp_Pln(gp_Pnt(0, 0, center_z), gp_Dir(0, 0, 1))
|
||||
|
||||
core = self._build_core_with_base(
|
||||
shape, mold_block, parting_plane,
|
||||
xmin - margin, ymin - margin, zmin - margin,
|
||||
xmax + margin, ymax + margin, zmax + margin,
|
||||
xmin, ymin, zmin, xmax, ymax, zmax
|
||||
)
|
||||
|
||||
logger.info("回退方案型腔/型芯分离完成")
|
||||
return cavity or mold_block, core
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"分模回退方案失败: {e}")
|
||||
try:
|
||||
bbox = Bnd_Box()
|
||||
brepbndlib.Add(shape, bbox)
|
||||
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
|
||||
margin = 20
|
||||
cavity_block = BRepPrimAPI_MakeBox(
|
||||
gp_Pnt(xmin - margin, ymin - margin, zmin - margin),
|
||||
gp_Pnt(xmax + margin, ymax + margin, zmax + margin)
|
||||
).Shape()
|
||||
cavity = self._subtract_product_from_plate(cavity_block, shape, "型腔(兜底)")
|
||||
return cavity or cavity_block, shape
|
||||
except Exception:
|
||||
return shape, shape
|
||||
|
||||
def detect_insert_regions(self, shape: TopoDS_Shape, analysis: Dict,
|
||||
depth_threshold: float = 30.0,
|
||||
aspect_threshold: float = 3.0) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
检测需要独立镶件的区域
|
||||
|
||||
镶件判定条件:
|
||||
1. 深腔区域(深度超过阈值)
|
||||
2. 细长特征(长径比超过阈值)
|
||||
3. 易磨损区域(尖锐角落、薄壁)
|
||||
4. 精密特征(高精度要求的局部区域)
|
||||
|
||||
Args:
|
||||
shape: 产品形状
|
||||
analysis: 几何分析结果
|
||||
depth_threshold: 深腔深度阈值 mm
|
||||
aspect_threshold: 长径比阈值
|
||||
|
||||
Returns:
|
||||
镶件区域列表
|
||||
"""
|
||||
inserts = []
|
||||
|
||||
try:
|
||||
bbox = analysis.get("bounding_box", {})
|
||||
dims = bbox.get("dimensions", [0, 0, 0])
|
||||
center = bbox.get("center", [0, 0, 0])
|
||||
|
||||
if dims[2] > depth_threshold:
|
||||
inserts.append({
|
||||
"type": "deep_cavity_insert",
|
||||
"location": center,
|
||||
"depth": dims[2],
|
||||
"reason": f"型腔深度 {dims[2]:.1f}mm 超过阈值 {depth_threshold}mm",
|
||||
"insert_type": "core_pin",
|
||||
"priority": "high"
|
||||
})
|
||||
|
||||
explorer = TopExp_Explorer(shape, TopAbs_FACE)
|
||||
face_idx = 0
|
||||
|
||||
while explorer.More():
|
||||
face = topods.Face(explorer.Current())
|
||||
face_idx += 1
|
||||
|
||||
try:
|
||||
surface = BRepAdaptor_Surface(face)
|
||||
|
||||
face_props = GProp_GProps()
|
||||
brepgprop.SurfaceProperties(face, face_props)
|
||||
area = face_props.Mass()
|
||||
|
||||
if area < 1.0 and area > 0.001:
|
||||
bbox_face = Bnd_Box()
|
||||
brepbndlib.Add(face, bbox_face)
|
||||
try:
|
||||
fxmin, fymin, fzmin, fxmax, fymax, fzmax = bbox_face.Get()
|
||||
f_dims = [fxmax - fxmin, fymax - fymin, fzmax - fzmin]
|
||||
max_dim = max(f_dims)
|
||||
min_dim = min(f_dims)
|
||||
|
||||
if min_dim > 0.01 and max_dim / min_dim > aspect_threshold:
|
||||
face_center = [
|
||||
float((fxmin + fxmax) / 2),
|
||||
float((fymin + fymax) / 2),
|
||||
float((fzmin + fzmax) / 2)
|
||||
]
|
||||
|
||||
inserts.append({
|
||||
"type": "slender_feature_insert",
|
||||
"location": face_center,
|
||||
"aspect_ratio": max_dim / min_dim,
|
||||
"reason": f"细长特征,长径比 {max_dim/min_dim:.1f}",
|
||||
"insert_type": "core_pin",
|
||||
"priority": "medium",
|
||||
"face_index": face_idx
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if surface.GetType() == 1:
|
||||
radius = surface.Cylinder().Radius()
|
||||
if radius < 3.0 and radius > 0.1:
|
||||
cyl_axis = surface.Cylinder().Position().Axis()
|
||||
cyl_loc = cyl_axis.Location()
|
||||
|
||||
inserts.append({
|
||||
"type": "small_hole_insert",
|
||||
"location": [float(cyl_loc.X()), float(cyl_loc.Y()), float(cyl_loc.Z())],
|
||||
"radius": float(radius),
|
||||
"reason": f"小孔特征,半径 {radius:.2f}mm",
|
||||
"insert_type": "core_pin",
|
||||
"priority": "high",
|
||||
"face_index": face_idx
|
||||
})
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
explorer.Next()
|
||||
|
||||
if not inserts:
|
||||
logger.info("未检测到需要镶件的区域")
|
||||
else:
|
||||
logger.info(f"检测到 {len(inserts)} 个镶件区域")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"镶件检测失败: {e}")
|
||||
|
||||
return inserts
|
||||
|
||||
def _extract_shape_geometry(self, shape: TopoDS_Shape, shape_type: str) -> Dict[str, Any]:
|
||||
try:
|
||||
mesh = BRepMesh_IncrementalMesh(shape, 0.1)
|
||||
mesh.Perform()
|
||||
|
||||
vertices = []
|
||||
faces = []
|
||||
|
||||
explorer = TopExp_Explorer(shape, TopAbs_FACE)
|
||||
vertex_index = 0
|
||||
|
||||
while explorer.More():
|
||||
face = explorer.Current()
|
||||
location = TopLoc_Location()
|
||||
triangulation = BRep_Tool.Triangulation(face, location)
|
||||
|
||||
if triangulation:
|
||||
nb_nodes = triangulation.NbNodes()
|
||||
for i in range(1, nb_nodes + 1):
|
||||
node = triangulation.Node(i)
|
||||
transformed = node.Transformed(location.Transformation())
|
||||
vertices.extend([
|
||||
float(transformed.X()),
|
||||
float(transformed.Y()),
|
||||
float(transformed.Z())
|
||||
])
|
||||
|
||||
nb_triangles = triangulation.NbTriangles()
|
||||
for i in range(1, nb_triangles + 1):
|
||||
triangle = triangulation.Triangle(i)
|
||||
idx1 = triangle.Value(1) + vertex_index - 1
|
||||
idx2 = triangle.Value(2) + vertex_index - 1
|
||||
idx3 = triangle.Value(3) + vertex_index - 1
|
||||
faces.extend([int(idx1), int(idx2), int(idx3)])
|
||||
|
||||
vertex_index += nb_nodes
|
||||
|
||||
explorer.Next()
|
||||
|
||||
vertex_count = len(vertices) // 3
|
||||
face_count = len(faces) // 3
|
||||
|
||||
return {
|
||||
"type": shape_type,
|
||||
"vertices": vertices,
|
||||
"faces": faces,
|
||||
"vertex_count": vertex_count,
|
||||
"face_count": face_count,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"{shape_type}几何提取失败: {e}")
|
||||
return {
|
||||
"type": shape_type,
|
||||
"vertices": [],
|
||||
"faces": [],
|
||||
"vertex_count": 0,
|
||||
"face_count": 0,
|
||||
}
|
||||
|
||||
def _extract_plane_metadata(self, surface: TopoDS_Shape) -> Dict[str, Any]:
|
||||
"""从分型面提取平面元数据(法向量、原点、边界)"""
|
||||
metadata = {
|
||||
"normal": [0.0, 0.0, 1.0],
|
||||
"origin": [0.0, 0.0, 0.0],
|
||||
"bounds": {"min": [0.0, 0.0, 0.0], "max": [0.0, 0.0, 0.0]},
|
||||
}
|
||||
try:
|
||||
surface_adaptor = BRepAdaptor_Surface(surface)
|
||||
if surface_adaptor.GetType() == 0:
|
||||
plane = surface_adaptor.Plane()
|
||||
axis = plane.Axis()
|
||||
normal = axis.Direction()
|
||||
origin = plane.Location()
|
||||
metadata["normal"] = [float(normal.X()), float(normal.Y()), float(normal.Z())]
|
||||
metadata["origin"] = [float(origin.X()), float(origin.Y()), float(origin.Z())]
|
||||
|
||||
bbox = Bnd_Box()
|
||||
brepbndlib.Add(surface, bbox)
|
||||
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
|
||||
metadata["bounds"] = {
|
||||
"min": [float(xmin), float(ymin), float(zmin)],
|
||||
"max": [float(xmax), float(ymax), float(zmax)],
|
||||
}
|
||||
except Exception as e:
|
||||
logger.warning(f"提取平面元数据失败: {e}")
|
||||
return metadata
|
||||
|
||||
def _calculate_product_weight(self, analysis: Dict) -> str:
|
||||
volume_cm3 = analysis.get("volume", 0) / 1000
|
||||
weight_g = volume_cm3 * self.material_density
|
||||
return f"{weight_g:.2f} g"
|
||||
|
||||
def _assess_warpage_risk(self, analysis: Dict) -> str:
|
||||
bbox = analysis.get("bounding_box", {}).get("dimensions", [1, 1, 1])
|
||||
aspect_ratio = max(bbox) / min(bbox) if min(bbox) > 0 else 1
|
||||
|
||||
if aspect_ratio > 5:
|
||||
return "高 - 建议增加加强筋"
|
||||
elif aspect_ratio > 3:
|
||||
return "中 - 需优化冷却"
|
||||
else:
|
||||
return "低"
|
||||
|
||||
def _get_inertia_matrix(self, props: GProp_GProps) -> List[List[float]]:
|
||||
inertia = props.MatrixOfInertia()
|
||||
return [
|
||||
[inertia.Value(1, 1), inertia.Value(1, 2), inertia.Value(1, 3)],
|
||||
[inertia.Value(2, 1), inertia.Value(2, 2), inertia.Value(2, 3)],
|
||||
[inertia.Value(3, 1), inertia.Value(3, 2), inertia.Value(3, 3)]
|
||||
]
|
||||
|
||||
def _calculate_parting_line_length(self, parting_line: List) -> float:
|
||||
if not parting_line or len(parting_line) < 2:
|
||||
return 0.0
|
||||
|
||||
total_length = 0.0
|
||||
for i in range(1, len(parting_line)):
|
||||
p1 = np.array(parting_line[i-1])
|
||||
p2 = np.array(parting_line[i])
|
||||
segment_length = np.linalg.norm(p2 - p1)
|
||||
total_length += segment_length
|
||||
|
||||
return total_length
|
||||
|
||||
def _calculate_parting_line(self, shape: TopoDS_Shape, parting_surface: TopoDS_Face) -> List[List[float]]:
|
||||
try:
|
||||
section = BRepAlgoAPI_Section(shape, parting_surface)
|
||||
section.Build()
|
||||
|
||||
if not section.IsDone():
|
||||
logger.warning("截面运算未完成,使用简化分型线")
|
||||
return self._simple_parting_line(shape)
|
||||
|
||||
edges = []
|
||||
explorer = TopExp_Explorer(section.Shape(), TopAbs_EDGE)
|
||||
|
||||
while explorer.More():
|
||||
edge = explorer.Current()
|
||||
|
||||
curve = BRepAdaptor_Curve(edge)
|
||||
first_param = curve.FirstParameter()
|
||||
last_param = curve.LastParameter()
|
||||
|
||||
num_points = max(10, int((last_param - first_param) / 0.5))
|
||||
step = (last_param - first_param) / num_points
|
||||
|
||||
for i in range(num_points + 1):
|
||||
param = first_param + i * step
|
||||
point = curve.Value(param)
|
||||
edges.append([point.X(), point.Y(), point.Z()])
|
||||
|
||||
explorer.Next()
|
||||
|
||||
if not edges:
|
||||
logger.warning("未找到交线,使用简化分型线")
|
||||
return self._simple_parting_line(shape)
|
||||
|
||||
logger.info(f"计算得到 {len(edges)} 个分型线点")
|
||||
return edges
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"分型线计算失败: {e}")
|
||||
return self._simple_parting_line(shape)
|
||||
|
||||
def _simple_parting_line(self, shape: TopoDS_Shape) -> List[List[float]]:
|
||||
try:
|
||||
bbox = Bnd_Box()
|
||||
brepbndlib.Add(shape, bbox)
|
||||
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
|
||||
center_z = (zmin + zmax) / 2
|
||||
|
||||
return [
|
||||
[xmin, ymin, center_z],
|
||||
[xmax, ymin, center_z],
|
||||
[xmax, ymax, center_z],
|
||||
[xmin, ymax, center_z],
|
||||
[xmin, ymin, center_z]
|
||||
]
|
||||
except Exception:
|
||||
return [[-50, -50, 0], [50, -50, 0], [50, 50, 0], [-50, 50, 0], [-50, -50, 0]]
|
||||
|
||||
def extend_parting_surface(self, parting_surface: TopoDS_Face, shape: TopoDS_Shape,
|
||||
extension: float = 30.0) -> TopoDS_Face:
|
||||
"""
|
||||
将分型面延伸到模具块边界
|
||||
|
||||
分型面通常只覆盖产品轮廓,需要延伸到模具块边缘
|
||||
才能正确分离A板和B板
|
||||
|
||||
Args:
|
||||
parting_surface: 原始分型面
|
||||
shape: 产品形状
|
||||
extension: 延伸距离 mm
|
||||
|
||||
Returns:
|
||||
延伸后的分型面
|
||||
"""
|
||||
try:
|
||||
bbox = Bnd_Box()
|
||||
brepbndlib.Add(shape, bbox)
|
||||
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
|
||||
|
||||
surface = BRepAdaptor_Surface(parting_surface)
|
||||
if surface.GetType() != 0:
|
||||
logger.info("分型面非平面,延伸操作跳过")
|
||||
return parting_surface
|
||||
|
||||
plane = surface.Plane()
|
||||
origin = plane.Location()
|
||||
normal = plane.Axis().Direction()
|
||||
|
||||
extended_xmin = xmin - extension
|
||||
extended_ymin = ymin - extension
|
||||
extended_xmax = xmax + extension
|
||||
extended_ymax = ymax + extension
|
||||
|
||||
extended_plane = gp_Pln(origin, normal)
|
||||
extended_surface = BRepBuilderAPI_MakeFace(
|
||||
extended_plane,
|
||||
extended_xmin, extended_xmax,
|
||||
extended_ymin, extended_ymax
|
||||
).Face()
|
||||
|
||||
logger.info(f"分型面延伸完成: 延伸距离={extension}mm")
|
||||
return extended_surface
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"分型面延伸失败: {e}")
|
||||
return parting_surface
|
||||
|
||||
def optimize_parting_line(self, parting_line: List[List[float]],
|
||||
smooth_window: int = 5,
|
||||
min_segment_length: float = 0.5,
|
||||
angle_threshold: float = 150.0) -> List[List[float]]:
|
||||
"""
|
||||
优化分型线
|
||||
|
||||
优化内容:
|
||||
1. 平滑处理 - 消除噪声点
|
||||
2. 去除短线段 - 合并过短的线段
|
||||
3. 尖角处理 - 在尖角处添加过渡圆弧
|
||||
4. 点密度均匀化 - 重采样使点间距均匀
|
||||
|
||||
Args:
|
||||
parting_line: 原始分型线点列表
|
||||
smooth_window: 平滑窗口大小
|
||||
min_segment_length: 最小线段长度
|
||||
angle_threshold: 尖角判定角度(度)
|
||||
|
||||
Returns:
|
||||
优化后的分型线
|
||||
"""
|
||||
if len(parting_line) < 3:
|
||||
return parting_line
|
||||
|
||||
try:
|
||||
smoothed = self._smooth_parting_line(parting_line, smooth_window)
|
||||
|
||||
filtered = self._filter_short_segments(smoothed, min_segment_length)
|
||||
|
||||
optimized = self._round_sharp_corners(filtered, angle_threshold)
|
||||
|
||||
resampled = self._resample_parting_line(optimized, target_spacing=2.0)
|
||||
|
||||
logger.info(f"分型线优化: {len(parting_line)} → {len(resampled)} 点")
|
||||
return resampled
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"分型线优化失败: {e}")
|
||||
return parting_line
|
||||
|
||||
def _smooth_parting_line(self, points: List[List[float]],
|
||||
window: int = 5) -> List[List[float]]:
|
||||
"""移动平均平滑"""
|
||||
if len(points) < window:
|
||||
return points
|
||||
|
||||
arr = np.array(points, dtype=np.float64)
|
||||
smoothed = []
|
||||
|
||||
for i in range(len(arr)):
|
||||
start = max(0, i - window // 2)
|
||||
end = min(len(arr), i + window // 2 + 1)
|
||||
avg = np.mean(arr[start:end], axis=0)
|
||||
smoothed.append(avg.tolist())
|
||||
|
||||
return smoothed
|
||||
|
||||
def _filter_short_segments(self, points: List[List[float]],
|
||||
min_length: float) -> List[List[float]]:
|
||||
"""去除过短线段"""
|
||||
if not points:
|
||||
return points
|
||||
|
||||
filtered = [points[0]]
|
||||
for i in range(1, len(points)):
|
||||
dist = np.linalg.norm(np.array(points[i]) - np.array(filtered[-1]))
|
||||
if dist >= min_length:
|
||||
filtered.append(points[i])
|
||||
|
||||
return filtered
|
||||
|
||||
def _round_sharp_corners(self, points: List[List[float]],
|
||||
angle_threshold: float) -> List[List[float]]:
|
||||
"""在尖角处添加过渡点"""
|
||||
if len(points) < 3:
|
||||
return points
|
||||
|
||||
result = [points[0]]
|
||||
|
||||
for i in range(1, len(points) - 1):
|
||||
v1 = np.array(points[i]) - np.array(points[i - 1])
|
||||
v2 = np.array(points[i + 1]) - np.array(points[i])
|
||||
|
||||
len1 = np.linalg.norm(v1)
|
||||
len2 = np.linalg.norm(v2)
|
||||
|
||||
if len1 > 0.001 and len2 > 0.001:
|
||||
cos_angle = np.clip(np.dot(v1, v2) / (len1 * len2), -1, 1)
|
||||
angle = math.degrees(math.acos(cos_angle))
|
||||
|
||||
if angle < angle_threshold:
|
||||
mid1 = (np.array(points[i - 1]) + np.array(points[i])) / 2
|
||||
mid2 = (np.array(points[i]) + np.array(points[i + 1])) / 2
|
||||
result.append(mid1.tolist())
|
||||
result.append(mid2.tolist())
|
||||
else:
|
||||
result.append(points[i])
|
||||
else:
|
||||
result.append(points[i])
|
||||
|
||||
result.append(points[-1])
|
||||
return result
|
||||
|
||||
def _resample_parting_line(self, points: List[List[float]],
|
||||
target_spacing: float) -> List[List[float]]:
|
||||
"""重采样使点间距均匀"""
|
||||
if len(points) < 2:
|
||||
return points
|
||||
|
||||
arr = np.array(points, dtype=np.float64)
|
||||
|
||||
cumulative_dist = [0.0]
|
||||
for i in range(1, len(arr)):
|
||||
dist = np.linalg.norm(arr[i] - arr[i - 1])
|
||||
cumulative_dist.append(cumulative_dist[-1] + dist)
|
||||
|
||||
total_length = cumulative_dist[-1]
|
||||
if total_length < target_spacing:
|
||||
return points
|
||||
|
||||
num_points = max(3, int(total_length / target_spacing))
|
||||
new_distances = np.linspace(0, total_length, num_points)
|
||||
|
||||
resampled = []
|
||||
for d in new_distances:
|
||||
idx = np.searchsorted(cumulative_dist, d) - 1
|
||||
idx = max(0, min(idx, len(arr) - 2))
|
||||
|
||||
seg_start = cumulative_dist[idx]
|
||||
seg_end = cumulative_dist[idx + 1]
|
||||
seg_length = seg_end - seg_start
|
||||
|
||||
if seg_length > 0:
|
||||
t = (d - seg_start) / seg_length
|
||||
else:
|
||||
t = 0
|
||||
|
||||
point = arr[idx] + t * (arr[idx + 1] - arr[idx])
|
||||
resampled.append(point.tolist())
|
||||
|
||||
return resampled
|
||||
@@ -0,0 +1,478 @@
|
||||
"""
|
||||
CAD 文件导出模块
|
||||
|
||||
支持导出格式:
|
||||
1. STEP (ISO 10303) - 推荐,UG/NX、FreeCAD、SolidWorks 通用
|
||||
2. IGES (Initial Graphics Exchange Specification) - 兼容旧系统
|
||||
3. STL (STereoLithography) - 网格格式,3D打印/快速预览
|
||||
4. BRep (Boundary Representation) - OpenCASCADE 原生格式
|
||||
|
||||
导出内容:
|
||||
- 型腔 (Cavity)
|
||||
- 型芯 (Core)
|
||||
- 分型面 (Parting Surface)
|
||||
- A板/B板
|
||||
- 模具块
|
||||
- 完整模具装配体(多形状合并)
|
||||
|
||||
UG/NX 导入建议:
|
||||
- 优先使用 STEP AP214 或 AP242 格式
|
||||
- IGES 作为备选
|
||||
- STL 仅用于预览,不可编辑
|
||||
|
||||
FreeCAD 导入建议:
|
||||
- STEP AP214 最佳兼容性
|
||||
- BRep 可直接在 FreeCAD 的 OpenCASCADE 内核中打开
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
from pathlib import Path
|
||||
from OCC.Core.TopoDS import TopoDS_Shape
|
||||
from shared.utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class CADExporter:
|
||||
"""CAD 文件导出器"""
|
||||
|
||||
def __init__(self, output_dir: str = "./exports"):
|
||||
self.output_dir = output_dir
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
@staticmethod
|
||||
def _safe_segment(value: Optional[str], fallback: str) -> str:
|
||||
text = str(value or "").strip()
|
||||
if not text:
|
||||
text = fallback
|
||||
text = re.sub(r"[^A-Za-z0-9._-]+", "_", text)
|
||||
return text[:80] or fallback
|
||||
|
||||
def build_export_dir(
|
||||
self,
|
||||
base_filename: str,
|
||||
task_id: Optional[str] = None,
|
||||
scheme_id: Optional[str] = None,
|
||||
) -> str:
|
||||
if task_id:
|
||||
task_segment = self._safe_segment(task_id, "task")
|
||||
scheme_segment = self._safe_segment(scheme_id, "default")
|
||||
return os.path.join(self.output_dir, task_segment, scheme_segment)
|
||||
return os.path.join(self.output_dir, self._safe_segment(base_filename, "mold"))
|
||||
|
||||
def get_relative_path(self, filepath: str) -> str:
|
||||
full_path = Path(filepath).resolve()
|
||||
output_root = Path(self.output_dir).resolve()
|
||||
try:
|
||||
relative = full_path.relative_to(output_root)
|
||||
except ValueError:
|
||||
relative = Path(os.path.basename(filepath))
|
||||
return relative.as_posix()
|
||||
|
||||
def export_step(self, shape: TopoDS_Shape, filepath: str,
|
||||
schema: str = "AP214") -> bool:
|
||||
"""
|
||||
导出 STEP 文件
|
||||
|
||||
Args:
|
||||
shape: OpenCASCADE TopoDS_Shape
|
||||
filepath: 输出文件路径
|
||||
schema: STEP 应用协议 (AP203/AP214/AP242)
|
||||
|
||||
Returns:
|
||||
是否成功
|
||||
"""
|
||||
try:
|
||||
from OCC.Core.STEPControl import (
|
||||
STEPControl_Writer,
|
||||
STEPControl_AsIs,
|
||||
)
|
||||
from OCC.Core.Interface import Interface_Static
|
||||
|
||||
writer = STEPControl_Writer()
|
||||
|
||||
if schema == "AP203":
|
||||
Interface_Static.SetCVal("write.step.schema", "AP203")
|
||||
elif schema == "AP242":
|
||||
Interface_Static.SetCVal("write.step.schema", "AP242")
|
||||
else:
|
||||
Interface_Static.SetCVal("write.step.schema", "AP214")
|
||||
|
||||
writer.Transfer(shape, STEPControl_AsIs)
|
||||
|
||||
status = writer.Write(filepath)
|
||||
|
||||
if status == 1:
|
||||
file_size = os.path.getsize(filepath) if os.path.exists(filepath) else 0
|
||||
logger.info(f"STEP 导出成功: {filepath} ({file_size} bytes, {schema})")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"STEP 导出失败: 写入状态={status}")
|
||||
return False
|
||||
|
||||
except ImportError as e:
|
||||
logger.error(f"STEP 导出依赖缺失: {e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"STEP 导出失败: {e}")
|
||||
return False
|
||||
|
||||
def export_iges(self, shape: TopoDS_Shape, filepath: str) -> bool:
|
||||
"""
|
||||
导出 IGES 文件
|
||||
|
||||
Args:
|
||||
shape: OpenCASCADE TopoDS_Shape
|
||||
filepath: 输出文件路径
|
||||
|
||||
Returns:
|
||||
是否成功
|
||||
"""
|
||||
try:
|
||||
from OCC.Core.IGESControl import IGESControl_Writer
|
||||
from OCC.Core.Interface import Interface_Static
|
||||
|
||||
Interface_Static.SetCVal("write.iges.brep.mode", "0")
|
||||
|
||||
writer = IGESControl_Writer()
|
||||
writer.AddShape(shape)
|
||||
writer.ComputeModel()
|
||||
|
||||
status = writer.Write(filepath)
|
||||
|
||||
if status:
|
||||
file_size = os.path.getsize(filepath) if os.path.exists(filepath) else 0
|
||||
logger.info(f"IGES 导出成功: {filepath} ({file_size} bytes)")
|
||||
return True
|
||||
else:
|
||||
logger.error("IGES 导出失败: 写入返回 False")
|
||||
return False
|
||||
|
||||
except ImportError as e:
|
||||
logger.error(f"IGES 导出依赖缺失: {e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"IGES 导出失败: {e}")
|
||||
return False
|
||||
|
||||
def export_stl(self, shape: TopoDS_Shape, filepath: str,
|
||||
ascii_mode: bool = True,
|
||||
deflection: float = 0.1) -> bool:
|
||||
"""
|
||||
导出 STL 文件
|
||||
|
||||
Args:
|
||||
shape: OpenCASCADE TopoDS_Shape
|
||||
filepath: 输出文件路径
|
||||
ascii_mode: True=ASCII格式, False=二进制格式
|
||||
deflection: 网格偏差(越小越精细)
|
||||
|
||||
Returns:
|
||||
是否成功
|
||||
"""
|
||||
try:
|
||||
from OCC.Core.StlAPI import StlAPI_Writer
|
||||
from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh
|
||||
|
||||
mesh = BRepMesh_IncrementalMesh(shape, deflection)
|
||||
mesh.Perform()
|
||||
|
||||
if not mesh.IsDone():
|
||||
logger.warning("STL 网格化未完成,尝试继续导出")
|
||||
|
||||
writer = StlAPI_Writer()
|
||||
writer.AsciiMode = ascii_mode
|
||||
|
||||
writer.Write(shape, filepath)
|
||||
|
||||
if os.path.exists(filepath) and os.path.getsize(filepath) > 0:
|
||||
file_size = os.path.getsize(filepath)
|
||||
logger.info(f"STL 导出成功: {filepath} ({file_size} bytes)")
|
||||
return True
|
||||
else:
|
||||
logger.error("STL 导出失败: 文件为空或不存在")
|
||||
return False
|
||||
|
||||
except ImportError as e:
|
||||
logger.error(f"STL 导出依赖缺失: {e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"STL 导出失败: {e}")
|
||||
return False
|
||||
|
||||
def export_brep(self, shape: TopoDS_Shape, filepath: str) -> bool:
|
||||
"""
|
||||
导出 BRep 文件(OpenCASCADE 原生格式)
|
||||
|
||||
FreeCAD 可直接导入此格式
|
||||
|
||||
Args:
|
||||
shape: OpenCASCADE TopoDS_Shape
|
||||
filepath: 输出文件路径
|
||||
|
||||
Returns:
|
||||
是否成功
|
||||
"""
|
||||
try:
|
||||
from OCC.Core.BRepTools import BRepTools_Write
|
||||
|
||||
BRepTools_Write(shape, filepath)
|
||||
|
||||
if os.path.exists(filepath) and os.path.getsize(filepath) > 0:
|
||||
file_size = os.path.getsize(filepath)
|
||||
logger.info(f"BRep 导出成功: {filepath} ({file_size} bytes)")
|
||||
return True
|
||||
else:
|
||||
logger.error("BRep 导出失败: 文件为空或不存在")
|
||||
return False
|
||||
|
||||
except ImportError as e:
|
||||
logger.error(f"BRep 导出依赖缺失: {e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"BRep 导出失败: {e}")
|
||||
return False
|
||||
|
||||
def export_mold_results(self, cavity_data: Dict,
|
||||
base_filename: str,
|
||||
formats: List[str] = None,
|
||||
components: List[str] = None,
|
||||
task_id: Optional[str] = None,
|
||||
scheme_id: Optional[str] = None) -> Dict[str, Any]:
|
||||
if formats is None:
|
||||
formats = ["step", "stl"]
|
||||
if components is None:
|
||||
components = ["cavity", "core"]
|
||||
|
||||
export_dir = self.build_export_dir(
|
||||
base_filename=base_filename,
|
||||
task_id=task_id,
|
||||
scheme_id=scheme_id,
|
||||
)
|
||||
os.makedirs(export_dir, exist_ok=True)
|
||||
|
||||
results = {
|
||||
"base_filename": base_filename,
|
||||
"task_id": task_id,
|
||||
"scheme_id": scheme_id,
|
||||
"export_dir": export_dir,
|
||||
"files": [],
|
||||
"errors": [],
|
||||
}
|
||||
|
||||
shape_map = {
|
||||
"cavity": ("cavity", "型腔"),
|
||||
"core": ("core", "型芯"),
|
||||
"parting_surface": ("parting_surface", "分型面"),
|
||||
}
|
||||
|
||||
shapes_to_export: List[Tuple[str, str, TopoDS_Shape]] = []
|
||||
assembly_shapes: List[Tuple[TopoDS_Shape, str]] = []
|
||||
|
||||
for comp in components:
|
||||
if comp == "all":
|
||||
for key, (data_key, label) in shape_map.items():
|
||||
shape = cavity_data.get(data_key)
|
||||
if shape is not None:
|
||||
shapes_to_export.append((key, label, shape))
|
||||
assembly_shapes.append((shape, label))
|
||||
break
|
||||
elif comp in shape_map:
|
||||
data_key, label = shape_map[comp]
|
||||
shape = cavity_data.get(data_key)
|
||||
if shape is not None:
|
||||
shapes_to_export.append((comp, label, shape))
|
||||
assembly_shapes.append((shape, label))
|
||||
else:
|
||||
results["errors"].append(f"{label}形状不可用")
|
||||
|
||||
# STEP: 所有组件合并为一个装配体文件
|
||||
if "step" in formats and assembly_shapes:
|
||||
filepath = os.path.join(export_dir, f"{base_filename}_mold.step")
|
||||
success = self.export_assembly_step(assembly_shapes, filepath)
|
||||
if success:
|
||||
file_size = os.path.getsize(filepath)
|
||||
relative_path = self.get_relative_path(filepath)
|
||||
results["files"].append({
|
||||
"component": "assembly",
|
||||
"component_label": "模具装配体",
|
||||
"format": "step",
|
||||
"filepath": filepath,
|
||||
"relative_path": relative_path,
|
||||
"filename": os.path.basename(filepath),
|
||||
"size_bytes": file_size,
|
||||
"size_readable": self._format_file_size(file_size),
|
||||
})
|
||||
else:
|
||||
results["errors"].append("装配体 STEP 导出失败")
|
||||
|
||||
# IGES / STL / BRep: 逐组件导出
|
||||
non_assembly_formats = [f for f in formats if f != "step"]
|
||||
for comp_name, label, shape in shapes_to_export:
|
||||
for fmt in non_assembly_formats:
|
||||
filepath = os.path.join(export_dir, f"{base_filename}_{comp_name}.{fmt}")
|
||||
|
||||
success = False
|
||||
if fmt == "iges":
|
||||
success = self.export_iges(shape, filepath)
|
||||
elif fmt == "stl":
|
||||
success = self.export_stl(shape, filepath)
|
||||
elif fmt == "brep":
|
||||
success = self.export_brep(shape, filepath)
|
||||
else:
|
||||
results["errors"].append(f"不支持的格式: {fmt}")
|
||||
continue
|
||||
|
||||
if success:
|
||||
file_size = os.path.getsize(filepath)
|
||||
relative_path = self.get_relative_path(filepath)
|
||||
results["files"].append({
|
||||
"component": comp_name,
|
||||
"component_label": label,
|
||||
"format": fmt,
|
||||
"filepath": filepath,
|
||||
"relative_path": relative_path,
|
||||
"filename": os.path.basename(filepath),
|
||||
"size_bytes": file_size,
|
||||
"size_readable": self._format_file_size(file_size),
|
||||
})
|
||||
else:
|
||||
results["errors"].append(f"{label} ({fmt}) 导出失败")
|
||||
|
||||
results["total_files"] = len(results["files"])
|
||||
results["total_errors"] = len(results["errors"])
|
||||
|
||||
logger.info(f"模具导出完成: {results['total_files']} 个文件, "
|
||||
f"{results['total_errors']} 个错误")
|
||||
|
||||
return results
|
||||
|
||||
def export_assembly_step(self, shapes_with_names: List[Tuple[TopoDS_Shape, str]],
|
||||
filepath: str,
|
||||
schema: str = "AP214") -> bool:
|
||||
"""
|
||||
导出装配体 STEP 文件(多个形状写入同一个 STEP 文件)
|
||||
|
||||
UG/NX 和 FreeCAD 可以识别装配体中的各个零件
|
||||
|
||||
Args:
|
||||
shapes_with_names: [(shape, name), ...] 形状和名称列表
|
||||
filepath: 输出文件路径
|
||||
schema: STEP 协议版本
|
||||
|
||||
Returns:
|
||||
是否成功
|
||||
"""
|
||||
try:
|
||||
from OCC.Core.STEPControl import (
|
||||
STEPControl_Writer,
|
||||
STEPControl_AsIs,
|
||||
)
|
||||
from OCC.Core.Interface import Interface_Static
|
||||
|
||||
writer = STEPControl_Writer()
|
||||
|
||||
if schema == "AP203":
|
||||
Interface_Static.SetCVal("write.step.schema", "AP203")
|
||||
elif schema == "AP242":
|
||||
Interface_Static.SetCVal("write.step.schema", "AP242")
|
||||
else:
|
||||
Interface_Static.SetCVal("write.step.schema", "AP214")
|
||||
|
||||
for shape, name in shapes_with_names:
|
||||
try:
|
||||
writer.Transfer(shape, STEPControl_AsIs)
|
||||
logger.info(f"已添加到装配体: {name}")
|
||||
except Exception as e:
|
||||
logger.warning(f"添加形状 {name} 失败: {e}")
|
||||
|
||||
status = writer.Write(filepath)
|
||||
|
||||
if status == 1:
|
||||
file_size = os.path.getsize(filepath) if os.path.exists(filepath) else 0
|
||||
logger.info(f"装配体 STEP 导出成功: {filepath} ({file_size} bytes)")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"装配体 STEP 导出失败: 状态={status}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"装配体 STEP 导出失败: {e}")
|
||||
return False
|
||||
|
||||
def get_export_recommendations(self, target_software: str = "ug") -> Dict[str, Any]:
|
||||
"""
|
||||
获取针对目标软件的导出建议
|
||||
|
||||
Args:
|
||||
target_software: 目标软件 (ug/nx, freecad, solidworks, autocad)
|
||||
|
||||
Returns:
|
||||
导出建议
|
||||
"""
|
||||
recommendations = {
|
||||
"ug": {
|
||||
"name": "UG/NX",
|
||||
"primary_format": "step",
|
||||
"step_schema": "AP242",
|
||||
"secondary_format": "iges",
|
||||
"notes": [
|
||||
"推荐 STEP AP242 格式,支持颜色和装配信息",
|
||||
"IGES 作为备选,但可能丢失拓扑信息",
|
||||
"STL 仅用于预览,不可参数化编辑",
|
||||
"导入时选择 '保留原始坐标系'",
|
||||
],
|
||||
"import_settings": {
|
||||
"step": "File → Import → STEP203/214/242",
|
||||
"iges": "File → Import → IGES",
|
||||
"stl": "File → Import → STL (仅可视化)",
|
||||
},
|
||||
},
|
||||
"freecad": {
|
||||
"name": "FreeCAD",
|
||||
"primary_format": "step",
|
||||
"step_schema": "AP214",
|
||||
"secondary_format": "brep",
|
||||
"notes": [
|
||||
"STEP AP214 最佳兼容性",
|
||||
"BRep 是 OpenCASCADE 原生格式,FreeCAD 可直接打开",
|
||||
"导入后可在 Part 工作台中编辑",
|
||||
"推荐使用 FreeCAD 0.21+ 版本",
|
||||
],
|
||||
"import_settings": {
|
||||
"step": "File → Import → 选择 STEP 文件",
|
||||
"iges": "File → Import → 选择 IGES 文件",
|
||||
"brep": "File → Open → 选择 BRep 文件",
|
||||
"stl": "File → Import → Mesh 格式",
|
||||
},
|
||||
},
|
||||
"solidworks": {
|
||||
"name": "SolidWorks",
|
||||
"primary_format": "step",
|
||||
"step_schema": "AP214",
|
||||
"secondary_format": "iges",
|
||||
"notes": [
|
||||
"STEP AP214 最佳兼容性",
|
||||
"导入后自动识别为实体",
|
||||
"IGES 可能产生曲面而非实体",
|
||||
],
|
||||
"import_settings": {
|
||||
"step": "File → Open → STEP 文件",
|
||||
"iges": "File → Open → IGES 文件",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return recommendations.get(target_software, recommendations["ug"])
|
||||
|
||||
@staticmethod
|
||||
def _format_file_size(size_bytes: int) -> str:
|
||||
"""格式化文件大小"""
|
||||
if size_bytes < 1024:
|
||||
return f"{size_bytes} B"
|
||||
elif size_bytes < 1024 * 1024:
|
||||
return f"{size_bytes / 1024:.1f} KB"
|
||||
else:
|
||||
return f"{size_bytes / (1024 * 1024):.1f} MB"
|
||||
@@ -0,0 +1,437 @@
|
||||
"""
|
||||
多型腔布局优化模块
|
||||
|
||||
功能:
|
||||
1. 支持矩形、圆形、H型等常见多型腔排列方式
|
||||
2. 基于产品尺寸和模架尺寸自动计算最优布局
|
||||
3. 流道系统自动设计
|
||||
4. 流动平衡评估
|
||||
5. 材料利用率计算
|
||||
|
||||
布局策略:
|
||||
- 1穴:中心单型腔
|
||||
- 2穴:对称排列
|
||||
- 4穴:2x2 矩阵排列
|
||||
- 8穴:2x4 矩阵排列
|
||||
- 16穴:4x4 矩阵排列
|
||||
- 圆形排列:适用于圆形产品
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
import math
|
||||
import numpy as np
|
||||
from shared.utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class CavityLayoutOptimizer:
|
||||
"""多型腔布局优化器"""
|
||||
|
||||
LAYOUT_RECTANGULAR = "rectangular"
|
||||
LAYOUT_CIRCULAR = "circular"
|
||||
LAYOUT_H_SHAPE = "h_shape"
|
||||
LAYOUT_INLINE = "inline"
|
||||
|
||||
def __init__(self):
|
||||
self.runner_diameter = 5.0
|
||||
self.gate_diameter = 1.5
|
||||
self.cavity_margin = 15.0
|
||||
self.runner_margin = 25.0
|
||||
|
||||
def optimize_layout(self, product_bbox: Dict, cavity_count: int,
|
||||
mold_base_size: Optional[Dict] = None,
|
||||
layout_type: str = "auto",
|
||||
product_shape: Any = None) -> Dict[str, Any]:
|
||||
"""
|
||||
优化多型腔布局
|
||||
|
||||
Args:
|
||||
product_bbox: 产品边界框 {"dimensions": [dx, dy, dz]}
|
||||
cavity_count: 型腔数量
|
||||
mold_base_size: 模架尺寸 {"length": L, "width": W}
|
||||
layout_type: 布局类型 (auto/rectangular/circular/h_shape/inline)
|
||||
product_shape: 产品形状(可选,用于精确计算)
|
||||
|
||||
Returns:
|
||||
{
|
||||
"layout_type": str,
|
||||
"cavity_positions": List[[x, y, z]],
|
||||
"cavity_rotations": List[[rx, ry, rz]],
|
||||
"runner_system": Dict,
|
||||
"balance_score": float,
|
||||
"material_efficiency": float,
|
||||
"mold_size": Dict,
|
||||
"recommendations": List[str]
|
||||
}
|
||||
"""
|
||||
logger.info(f"开始多型腔布局优化: {cavity_count}穴, 布局={layout_type}")
|
||||
|
||||
dims = product_bbox.get("dimensions", [100, 100, 50])
|
||||
|
||||
if layout_type == "auto":
|
||||
layout_type = self._recommend_layout(cavity_count, dims)
|
||||
|
||||
if layout_type == self.LAYOUT_RECTANGULAR:
|
||||
result = self._layout_rectangular(dims, cavity_count, mold_base_size)
|
||||
elif layout_type == self.LAYOUT_CIRCULAR:
|
||||
result = self._layout_circular(dims, cavity_count, mold_base_size)
|
||||
elif layout_type == self.LAYOUT_H_SHAPE:
|
||||
result = self._layout_h_shape(dims, cavity_count, mold_base_size)
|
||||
elif layout_type == self.LAYOUT_INLINE:
|
||||
result = self._layout_inline(dims, cavity_count, mold_base_size)
|
||||
else:
|
||||
result = self._layout_rectangular(dims, cavity_count, mold_base_size)
|
||||
|
||||
result["runner_system"] = self._design_runner_system(
|
||||
result["cavity_positions"], cavity_count, layout_type
|
||||
)
|
||||
result["balance_score"] = self._evaluate_flow_balance(
|
||||
result["cavity_positions"], result["runner_system"]
|
||||
)
|
||||
result["material_efficiency"] = self._calculate_material_efficiency(
|
||||
dims, cavity_count, result["mold_size"]
|
||||
)
|
||||
result["recommendations"] = self._generate_recommendations(
|
||||
result, cavity_count, dims
|
||||
)
|
||||
|
||||
logger.info(f"布局优化完成: {layout_type}, 平衡度={result['balance_score']:.2f}, "
|
||||
f"材料利用率={result['material_efficiency']:.2%}")
|
||||
|
||||
return result
|
||||
|
||||
def _recommend_layout(self, cavity_count: int, dims: List[float]) -> str:
|
||||
"""根据型腔数量和产品尺寸推荐布局方式"""
|
||||
aspect_ratio = max(dims[:2]) / min(dims[:2]) if min(dims[:2]) > 0 else 1
|
||||
|
||||
if cavity_count == 1:
|
||||
return self.LAYOUT_RECTANGULAR
|
||||
elif cavity_count == 2:
|
||||
if aspect_ratio > 2:
|
||||
return self.LAYOUT_INLINE
|
||||
return self.LAYOUT_RECTANGULAR
|
||||
elif cavity_count <= 4:
|
||||
return self.LAYOUT_RECTANGULAR
|
||||
elif cavity_count <= 8:
|
||||
if aspect_ratio > 2:
|
||||
return self.LAYOUT_H_SHAPE
|
||||
return self.LAYOUT_RECTANGULAR
|
||||
else:
|
||||
return self.LAYOUT_H_SHAPE
|
||||
|
||||
def _layout_rectangular(self, dims: List[float], cavity_count: int,
|
||||
mold_base_size: Optional[Dict]) -> Dict:
|
||||
"""矩形矩阵排列"""
|
||||
rows, cols = self._calculate_grid(cavity_count)
|
||||
|
||||
spacing_x = dims[0] + 2 * self.cavity_margin
|
||||
spacing_y = dims[1] + 2 * self.cavity_margin
|
||||
|
||||
positions = []
|
||||
rotations = []
|
||||
|
||||
for r in range(rows):
|
||||
for c in range(cols):
|
||||
if len(positions) >= cavity_count:
|
||||
break
|
||||
x = (c - (cols - 1) / 2) * spacing_x
|
||||
y = (r - (rows - 1) / 2) * spacing_y
|
||||
positions.append([x, y, 0])
|
||||
rotations.append([0, 0, 0])
|
||||
|
||||
total_length = cols * spacing_x + 2 * self.runner_margin
|
||||
total_width = rows * spacing_y + 2 * self.runner_margin
|
||||
|
||||
mold_size = {
|
||||
"length": max(total_length, mold_base_size.get("length", 0)) if mold_base_size else total_length,
|
||||
"width": max(total_width, mold_base_size.get("width", 0)) if mold_base_size else total_width,
|
||||
}
|
||||
|
||||
return {
|
||||
"layout_type": self.LAYOUT_RECTANGULAR,
|
||||
"cavity_positions": positions,
|
||||
"cavity_rotations": rotations,
|
||||
"grid": {"rows": rows, "cols": cols},
|
||||
"spacing": {"x": spacing_x, "y": spacing_y},
|
||||
"mold_size": mold_size,
|
||||
}
|
||||
|
||||
def _layout_circular(self, dims: List[float], cavity_count: int,
|
||||
mold_base_size: Optional[Dict]) -> Dict:
|
||||
"""圆形排列"""
|
||||
max_dim = max(dims[:2])
|
||||
radius = max_dim / 2 + self.cavity_margin + self.runner_margin
|
||||
|
||||
positions = []
|
||||
rotations = []
|
||||
|
||||
for i in range(cavity_count):
|
||||
angle = 2 * math.pi * i / cavity_count
|
||||
x = radius * math.cos(angle)
|
||||
y = radius * math.sin(angle)
|
||||
rot_z = -math.degrees(angle)
|
||||
positions.append([x, y, 0])
|
||||
rotations.append([0, 0, rot_z])
|
||||
|
||||
total_diameter = 2 * radius + max_dim + 2 * self.cavity_margin
|
||||
|
||||
mold_size = {
|
||||
"length": total_diameter,
|
||||
"width": total_diameter,
|
||||
}
|
||||
|
||||
return {
|
||||
"layout_type": self.LAYOUT_CIRCULAR,
|
||||
"cavity_positions": positions,
|
||||
"cavity_rotations": rotations,
|
||||
"radius": radius,
|
||||
"mold_size": mold_size,
|
||||
}
|
||||
|
||||
def _layout_h_shape(self, dims: List[float], cavity_count: int,
|
||||
mold_base_size: Optional[Dict]) -> Dict:
|
||||
"""H型排列(适用于多型腔,流道平衡性好)"""
|
||||
left_count = cavity_count // 2
|
||||
right_count = cavity_count - left_count
|
||||
|
||||
spacing_x = dims[0] + 2 * self.cavity_margin
|
||||
spacing_y = dims[1] + 2 * self.cavity_margin
|
||||
|
||||
positions = []
|
||||
rotations = []
|
||||
|
||||
left_rows, left_cols = self._calculate_grid(left_count)
|
||||
for r in range(left_rows):
|
||||
for c in range(left_cols):
|
||||
if len(positions) >= left_count:
|
||||
break
|
||||
x = -(c + 1) * spacing_x - spacing_x / 2
|
||||
y = (r - (left_rows - 1) / 2) * spacing_y
|
||||
positions.append([x, y, 0])
|
||||
rotations.append([0, 0, 0])
|
||||
|
||||
right_rows, right_cols = self._calculate_grid(right_count)
|
||||
for r in range(right_rows):
|
||||
for c in range(right_cols):
|
||||
if len(positions) >= cavity_count:
|
||||
break
|
||||
x = (c + 1) * spacing_x + spacing_x / 2
|
||||
y = (r - (right_rows - 1) / 2) * spacing_y
|
||||
positions.append([x, y, 0])
|
||||
rotations.append([0, 0, 0])
|
||||
|
||||
total_length = (max(left_cols, right_cols) + 1) * spacing_x * 2 + 2 * self.runner_margin
|
||||
total_width = max(left_rows, right_rows) * spacing_y + 2 * self.runner_margin
|
||||
|
||||
mold_size = {
|
||||
"length": total_length,
|
||||
"width": total_width,
|
||||
}
|
||||
|
||||
return {
|
||||
"layout_type": self.LAYOUT_H_SHAPE,
|
||||
"cavity_positions": positions,
|
||||
"cavity_rotations": rotations,
|
||||
"mold_size": mold_size,
|
||||
}
|
||||
|
||||
def _layout_inline(self, dims: List[float], cavity_count: int,
|
||||
mold_base_size: Optional[Dict]) -> Dict:
|
||||
"""直线排列(适用于细长产品)"""
|
||||
spacing = max(dims[:2]) + 2 * self.cavity_margin
|
||||
|
||||
positions = []
|
||||
rotations = []
|
||||
|
||||
for i in range(cavity_count):
|
||||
offset = (i - (cavity_count - 1) / 2) * spacing
|
||||
if dims[0] > dims[1]:
|
||||
positions.append([offset, 0, 0])
|
||||
else:
|
||||
positions.append([0, offset, 0])
|
||||
rotations.append([0, 0, 0])
|
||||
|
||||
if dims[0] > dims[1]:
|
||||
total_length = cavity_count * spacing + 2 * self.runner_margin
|
||||
total_width = dims[1] + 2 * self.cavity_margin + 2 * self.runner_margin
|
||||
else:
|
||||
total_length = dims[0] + 2 * self.cavity_margin + 2 * self.runner_margin
|
||||
total_width = cavity_count * spacing + 2 * self.runner_margin
|
||||
|
||||
mold_size = {
|
||||
"length": total_length,
|
||||
"width": total_width,
|
||||
}
|
||||
|
||||
return {
|
||||
"layout_type": self.LAYOUT_INLINE,
|
||||
"cavity_positions": positions,
|
||||
"cavity_rotations": rotations,
|
||||
"mold_size": mold_size,
|
||||
}
|
||||
|
||||
def _calculate_grid(self, count: int) -> Tuple[int, int]:
|
||||
"""计算最接近正方形的网格排列"""
|
||||
if count <= 0:
|
||||
return 1, 1
|
||||
|
||||
best_rows = 1
|
||||
best_cols = count
|
||||
best_ratio = float("inf")
|
||||
|
||||
for r in range(1, count + 1):
|
||||
if count % r == 0:
|
||||
c = count // r
|
||||
ratio = abs(r - c)
|
||||
if ratio < best_ratio:
|
||||
best_ratio = ratio
|
||||
best_rows = r
|
||||
best_cols = c
|
||||
|
||||
return best_rows, best_cols
|
||||
|
||||
def _design_runner_system(self, positions: List[List[float]],
|
||||
cavity_count: int,
|
||||
layout_type: str) -> Dict:
|
||||
"""
|
||||
设计流道系统
|
||||
|
||||
Returns:
|
||||
{
|
||||
"type": "cold_runner" | "hot_runner",
|
||||
"main_runner": Dict,
|
||||
"sub_runners": List[Dict],
|
||||
"gates": List[Dict],
|
||||
"total_volume": float
|
||||
}
|
||||
"""
|
||||
if cavity_count == 1:
|
||||
return self._design_single_cavity_runner(positions[0])
|
||||
|
||||
main_runner = {
|
||||
"start": [0, -positions[0][1] - 20, 0],
|
||||
"end": [0, positions[0][1] + 20, 0] if len(positions) > 0 else [0, 20, 0],
|
||||
"diameter": self.runner_diameter,
|
||||
"length": 0,
|
||||
}
|
||||
|
||||
sub_runners = []
|
||||
gates = []
|
||||
total_volume = 0
|
||||
|
||||
for i, pos in enumerate(positions):
|
||||
sub_runner = {
|
||||
"start": [0, pos[1], 0],
|
||||
"end": pos,
|
||||
"diameter": self.runner_diameter * 0.8,
|
||||
"length": float(np.linalg.norm(np.array(pos))),
|
||||
}
|
||||
sub_runners.append(sub_runner)
|
||||
total_volume += math.pi * (sub_runner["diameter"] / 2) ** 2 * sub_runner["length"]
|
||||
|
||||
gate = {
|
||||
"position": pos,
|
||||
"diameter": self.gate_diameter,
|
||||
"type": "side_gate",
|
||||
"length": 2.0,
|
||||
}
|
||||
gates.append(gate)
|
||||
total_volume += math.pi * (gate["diameter"] / 2) ** 2 * gate["length"]
|
||||
|
||||
main_runner["length"] = max(
|
||||
abs(p[1]) for p in positions
|
||||
) * 2 + 40 if positions else 40
|
||||
total_volume += math.pi * (main_runner["diameter"] / 2) ** 2 * main_runner["length"]
|
||||
|
||||
return {
|
||||
"type": "cold_runner",
|
||||
"main_runner": main_runner,
|
||||
"sub_runners": sub_runners,
|
||||
"gates": gates,
|
||||
"total_volume": total_volume,
|
||||
}
|
||||
|
||||
def _design_single_cavity_runner(self, position: List[float]) -> Dict:
|
||||
"""单型腔流道设计"""
|
||||
gate = {
|
||||
"position": position,
|
||||
"diameter": self.gate_diameter,
|
||||
"type": "center_gate",
|
||||
"length": 3.0,
|
||||
}
|
||||
|
||||
return {
|
||||
"type": "cold_runner",
|
||||
"main_runner": None,
|
||||
"sub_runners": [],
|
||||
"gates": [gate],
|
||||
"total_volume": math.pi * (gate["diameter"] / 2) ** 2 * gate["length"],
|
||||
}
|
||||
|
||||
def _evaluate_flow_balance(self, positions: List[List[float]],
|
||||
runner_system: Dict) -> float:
|
||||
"""
|
||||
评估流动平衡度 (0-1)
|
||||
|
||||
基于各型腔到主流道的距离差异
|
||||
"""
|
||||
if len(positions) <= 1:
|
||||
return 1.0
|
||||
|
||||
distances = []
|
||||
for pos in positions:
|
||||
dist = float(np.linalg.norm(np.array(pos)))
|
||||
distances.append(dist)
|
||||
|
||||
max_dist = max(distances)
|
||||
min_dist = min(distances)
|
||||
|
||||
if max_dist == 0:
|
||||
return 1.0
|
||||
|
||||
imbalance = (max_dist - min_dist) / max_dist
|
||||
balance_score = max(0, 1.0 - imbalance)
|
||||
|
||||
return round(balance_score, 3)
|
||||
|
||||
def _calculate_material_efficiency(self, product_dims: List[float],
|
||||
cavity_count: int,
|
||||
mold_size: Dict) -> float:
|
||||
"""计算材料利用率"""
|
||||
product_area = product_dims[0] * product_dims[1]
|
||||
total_product_area = product_area * cavity_count
|
||||
mold_area = mold_size.get("length", 0) * mold_size.get("width", 0)
|
||||
|
||||
if mold_area <= 0:
|
||||
return 0.0
|
||||
|
||||
return min(1.0, total_product_area / mold_area)
|
||||
|
||||
def _generate_recommendations(self, result: Dict, cavity_count: int,
|
||||
dims: List[float]) -> List[str]:
|
||||
"""生成优化建议"""
|
||||
recommendations = []
|
||||
|
||||
balance = result.get("balance_score", 0)
|
||||
if balance < 0.8:
|
||||
recommendations.append("流动平衡度偏低,建议调整型腔间距或使用热流道系统")
|
||||
|
||||
efficiency = result.get("material_efficiency", 0)
|
||||
if efficiency < 0.4:
|
||||
recommendations.append("材料利用率偏低,建议减少模架尺寸或增加型腔数量")
|
||||
|
||||
if cavity_count > 8:
|
||||
recommendations.append("多型腔模具建议使用热流道系统以保证填充平衡")
|
||||
|
||||
if cavity_count > 16:
|
||||
recommendations.append("型腔数量过多,建议分模评估加工可行性")
|
||||
|
||||
aspect = max(dims[:2]) / min(dims[:2]) if min(dims[:2]) > 0 else 1
|
||||
if aspect > 3:
|
||||
recommendations.append("产品长宽比大,建议使用侧浇口或扇形浇口")
|
||||
|
||||
if not recommendations:
|
||||
recommendations.append("布局方案合理,建议进行模流分析验证")
|
||||
|
||||
return recommendations
|
||||
@@ -0,0 +1,835 @@
|
||||
from typing import Dict, List, Any, Optional
|
||||
import math
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
import numpy as np
|
||||
from OCC.Core.TopoDS import TopoDS_Shape
|
||||
from shared.models.schemas import (
|
||||
create_mold_feature,
|
||||
create_design_recommendation,
|
||||
create_analysis_result
|
||||
)
|
||||
from shared.utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class GeometryAnalyzer:
|
||||
"""几何分析器 - 基于 OCC Shape 的精确分析"""
|
||||
|
||||
def __init__(self):
|
||||
self.feature_thresholds = {
|
||||
"thin_wall": 2.0,
|
||||
"thick_wall": 8.0,
|
||||
"small_feature": 5.0,
|
||||
"large_feature": 1000.0,
|
||||
"high_complexity": 50,
|
||||
}
|
||||
|
||||
self.product_materials = {
|
||||
"ABS": {"shrinkage": 0.005, "min_wall": 1.2},
|
||||
"PP": {"shrinkage": 0.016, "min_wall": 1.0},
|
||||
"PC": {"shrinkage": 0.007, "min_wall": 1.5},
|
||||
}
|
||||
|
||||
self.mold_materials = {
|
||||
"Aluminum": {"thermal_conductivity": 200, "hardness": "HB80", "cost": "low"},
|
||||
"P20_Steel": {"thermal_conductivity": 30, "hardness": "HRC30", "cost": "medium"},
|
||||
"H13_Steel": {"thermal_conductivity": 25, "hardness": "HRC48", "cost": "high"}
|
||||
}
|
||||
|
||||
def analyze_mold_design(self, geometry_data: Dict[str, Any],
|
||||
product_material: str = "ABS",
|
||||
mold_material: str = "Aluminum",
|
||||
shape: Optional[TopoDS_Shape] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""分析模具设计
|
||||
|
||||
Args:
|
||||
geometry_data: 几何数据字典(来自 stp_parser)
|
||||
product_material: 产品材料
|
||||
mold_material: 模具材料
|
||||
shape: OCC TopoDS_Shape 对象(可选,提供后启用精确分析)
|
||||
"""
|
||||
logger.info("开始模具设计分析")
|
||||
|
||||
features = self._detect_features(geometry_data, shape)
|
||||
|
||||
product_props = self.product_materials.get(product_material, {})
|
||||
mold_props = self.mold_materials.get(mold_material, {})
|
||||
|
||||
recommendations = self._generate_recommendations(
|
||||
geometry_data, features, product_material
|
||||
)
|
||||
|
||||
quality_metrics = self._calculate_quality_metrics(geometry_data, features)
|
||||
|
||||
analysis_summary = self._generate_analysis_summary(geometry_data, features, recommendations)
|
||||
|
||||
return create_analysis_result(
|
||||
geometry_data=geometry_data,
|
||||
detected_features=features,
|
||||
design_recommendations=recommendations,
|
||||
quality_metrics=quality_metrics,
|
||||
analysis_summary=analysis_summary
|
||||
)
|
||||
|
||||
def _detect_features(self, geometry_data: Dict[str, Any],
|
||||
shape: Optional[TopoDS_Shape] = None) -> List[Dict[str, Any]]:
|
||||
"""检测模具特征 — 独立检测并行执行"""
|
||||
features: List[Dict[str, Any]] = []
|
||||
|
||||
with ThreadPoolExecutor(max_workers=4, thread_name_prefix="feat") as pool:
|
||||
futures = {
|
||||
pool.submit(self._detect_wall_features, geometry_data, shape): "wall",
|
||||
pool.submit(self._detect_rib_features, geometry_data, shape): "rib",
|
||||
pool.submit(self._detect_boss_features, geometry_data, shape): "boss",
|
||||
pool.submit(self._analyze_draft_angles, geometry_data, shape): "draft",
|
||||
}
|
||||
if shape is not None:
|
||||
futures[pool.submit(self._detect_curvature_features, shape)] = "curvature"
|
||||
futures[pool.submit(self._detect_fillet_features, shape)] = "fillet"
|
||||
|
||||
for future in as_completed(futures):
|
||||
try:
|
||||
features.extend(future.result())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
logger.info(f"检测到 {len(features)} 个特征")
|
||||
return features
|
||||
|
||||
def _detect_wall_features(self, geometry_data: Dict[str, Any],
|
||||
shape: Optional[TopoDS_Shape] = None) -> List[Dict[str, Any]]:
|
||||
"""检测壁厚特征"""
|
||||
features = []
|
||||
|
||||
if shape is not None:
|
||||
precise_result = self._compute_precise_wall_thickness(shape)
|
||||
if precise_result is not None:
|
||||
min_thickness = precise_result["min_thickness"]
|
||||
max_thickness = precise_result["max_thickness"]
|
||||
avg_thickness = precise_result["avg_thickness"]
|
||||
thickness_map = precise_result.get("thickness_map", {})
|
||||
estimation_method = "precise"
|
||||
|
||||
if min_thickness < self.feature_thresholds["thin_wall"]:
|
||||
features.append(create_mold_feature(
|
||||
feature_type="thin_wall",
|
||||
confidence=0.92,
|
||||
location=precise_result.get("min_location",
|
||||
geometry_data.get("center_of_mass", [0, 0, 0])),
|
||||
dimensions=[min_thickness, avg_thickness, max_thickness],
|
||||
parameters={
|
||||
"min_thickness": round(min_thickness, 3),
|
||||
"max_thickness": round(max_thickness, 3),
|
||||
"avg_thickness": round(avg_thickness, 3),
|
||||
"estimation_method": estimation_method,
|
||||
"measured_pairs": len(thickness_map),
|
||||
},
|
||||
recommendations=[
|
||||
f"最小壁厚 {min_thickness:.2f}mm 过薄,建议增加到 {self.feature_thresholds['thin_wall']}mm 以上",
|
||||
"薄壁区域可能导致注塑填充不充分",
|
||||
"考虑增加加强筋以提高结构强度"
|
||||
]
|
||||
))
|
||||
elif max_thickness > self.feature_thresholds["thick_wall"]:
|
||||
features.append(create_mold_feature(
|
||||
feature_type="thick_wall",
|
||||
confidence=0.88,
|
||||
location=precise_result.get("max_location",
|
||||
geometry_data.get("center_of_mass", [0, 0, 0])),
|
||||
dimensions=[min_thickness, avg_thickness, max_thickness],
|
||||
parameters={
|
||||
"min_thickness": round(min_thickness, 3),
|
||||
"max_thickness": round(max_thickness, 3),
|
||||
"avg_thickness": round(avg_thickness, 3),
|
||||
"estimation_method": estimation_method,
|
||||
"measured_pairs": len(thickness_map),
|
||||
},
|
||||
recommendations=[
|
||||
f"最大壁厚 {max_thickness:.2f}mm 过厚,可能产生缩痕",
|
||||
"考虑减薄壁厚或增加加强筋",
|
||||
"优化冷却系统设计"
|
||||
]
|
||||
))
|
||||
|
||||
if min_thickness > 0 and max_thickness > 0:
|
||||
uniformity = min_thickness / max_thickness if max_thickness > 0 else 1.0
|
||||
if uniformity < 0.5:
|
||||
features.append(create_mold_feature(
|
||||
feature_type="wall_non_uniform",
|
||||
confidence=0.80,
|
||||
location=geometry_data.get("center_of_mass", [0, 0, 0]),
|
||||
dimensions=[min_thickness, max_thickness, uniformity],
|
||||
parameters={
|
||||
"uniformity_ratio": round(uniformity, 3),
|
||||
"min_thickness": round(min_thickness, 3),
|
||||
"max_thickness": round(max_thickness, 3),
|
||||
"estimation_method": estimation_method,
|
||||
},
|
||||
recommendations=[
|
||||
f"壁厚均匀性比 {uniformity:.2f} 偏低(建议 > 0.5)",
|
||||
"壁厚差异过大可能导致翘曲和缩痕",
|
||||
"建议逐步过渡壁厚,避免突变"
|
||||
]
|
||||
))
|
||||
|
||||
return features
|
||||
|
||||
volume = geometry_data.get("volume", 0)
|
||||
surface_area = geometry_data.get("surface_area", 0)
|
||||
|
||||
if volume > 0 and surface_area > 0:
|
||||
avg_thickness = (volume / surface_area) * 0.6
|
||||
|
||||
if avg_thickness < self.feature_thresholds["thin_wall"]:
|
||||
features.append(create_mold_feature(
|
||||
feature_type="thin_wall",
|
||||
confidence=0.85,
|
||||
location=geometry_data.get("center_of_mass", [0, 0, 0]),
|
||||
dimensions=[avg_thickness, avg_thickness, avg_thickness],
|
||||
parameters={"average_thickness": avg_thickness, "estimation_method": "heuristic"},
|
||||
recommendations=[
|
||||
f"平均壁厚 {avg_thickness:.2f}mm 过薄,建议增加到 {self.feature_thresholds['thin_wall']}mm 以上",
|
||||
"考虑增加加强筋以提高结构强度",
|
||||
"检查注塑填充是否充分"
|
||||
]
|
||||
))
|
||||
elif avg_thickness > self.feature_thresholds["thick_wall"]:
|
||||
features.append(create_mold_feature(
|
||||
feature_type="thick_wall",
|
||||
confidence=0.75,
|
||||
location=geometry_data.get("center_of_mass", [0, 0, 0]),
|
||||
dimensions=[avg_thickness, avg_thickness, avg_thickness],
|
||||
parameters={"average_thickness": avg_thickness, "estimation_method": "heuristic"},
|
||||
recommendations=[
|
||||
f"平均壁厚 {avg_thickness:.2f}mm 过厚,可能产生缩痕",
|
||||
"考虑减薄壁厚或增加加强筋",
|
||||
"优化冷却系统设计"
|
||||
]
|
||||
))
|
||||
elif volume > 0:
|
||||
bbox = geometry_data.get("bounding_box", {})
|
||||
dimensions = bbox.get("dimensions", [100, 100, 100])
|
||||
bbox_volume = dimensions[0] * dimensions[1] * dimensions[2]
|
||||
if bbox_volume > 0:
|
||||
volume_efficiency = volume / bbox_volume
|
||||
avg_thickness = (dimensions[0] + dimensions[1]) / 2 * volume_efficiency
|
||||
if avg_thickness < self.feature_thresholds["thin_wall"]:
|
||||
features.append(create_mold_feature(
|
||||
feature_type="thin_wall",
|
||||
confidence=0.7,
|
||||
location=bbox.get("center", [50, 50, 50]),
|
||||
dimensions=[avg_thickness, avg_thickness, avg_thickness],
|
||||
parameters={"average_thickness": avg_thickness, "estimation_method": "bbox_based"},
|
||||
recommendations=[
|
||||
f"估算平均壁厚 {avg_thickness:.2f}mm 过薄,建议检查表面积数据",
|
||||
"考虑增加加强筋以提高结构强度"
|
||||
]
|
||||
))
|
||||
|
||||
return features
|
||||
|
||||
def _compute_precise_wall_thickness(self, shape: TopoDS_Shape) -> Optional[Dict[str, Any]]:
|
||||
"""使用 BRepExtrema_DistShapeShape 精确计算壁厚"""
|
||||
try:
|
||||
from OCC.Core.TopExp import TopExp_Explorer
|
||||
from OCC.Core.TopAbs import TopAbs_FACE
|
||||
from OCC.Core.TopoDS import TopoDS_Face, topods
|
||||
from OCC.Core.BRepExtrema import BRepExtrema_DistShapeShape
|
||||
from OCC.Core.GProp import GProp_GProps
|
||||
from OCC.Core.BRepGProp import brepgprop
|
||||
from OCC.Core.Bnd import Bnd_Box
|
||||
from OCC.Core.BRepBndLib import brepbndlib
|
||||
from OCC.Core.gp import gp_Pnt
|
||||
|
||||
faces = []
|
||||
explorer = TopExp_Explorer(shape, TopAbs_FACE)
|
||||
while explorer.More():
|
||||
faces.append(topods.Face(explorer.Current()))
|
||||
explorer.Next()
|
||||
|
||||
if len(faces) < 2:
|
||||
return None
|
||||
|
||||
face_areas = []
|
||||
for face in faces:
|
||||
props = GProp_GProps()
|
||||
brepgprop.SurfaceProperties(face, props)
|
||||
face_areas.append(props.Mass())
|
||||
|
||||
indexed_faces = sorted(enumerate(faces), key=lambda x: face_areas[x[0]], reverse=True)
|
||||
|
||||
max_faces_to_check = min(len(indexed_faces), 30)
|
||||
|
||||
min_thickness = float('inf')
|
||||
max_thickness = 0.0
|
||||
thickness_values = []
|
||||
min_location = [0, 0, 0]
|
||||
max_location = [0, 0, 0]
|
||||
|
||||
for i in range(max_faces_to_check):
|
||||
for j in range(i + 1, max_faces_to_check):
|
||||
idx_i, face_i = indexed_faces[i]
|
||||
idx_j, face_j = indexed_faces[j]
|
||||
|
||||
try:
|
||||
dist_calc = BRepExtrema_DistShapeShape(face_i, face_j)
|
||||
if dist_calc.IsDone():
|
||||
dist = dist_calc.Value()
|
||||
if 0.1 < dist < 50.0:
|
||||
thickness_values.append(dist)
|
||||
if dist < min_thickness:
|
||||
min_thickness = dist
|
||||
try:
|
||||
p1 = dist_calc.PointOnShape1(1)
|
||||
min_location = [float(p1.X()), float(p1.Y()), float(p1.Z())]
|
||||
except Exception:
|
||||
pass
|
||||
if dist > max_thickness:
|
||||
max_thickness = dist
|
||||
try:
|
||||
p2 = dist_calc.PointOnShape2(1)
|
||||
max_location = [float(p2.X()), float(p2.Y()), float(p2.Z())]
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if not thickness_values:
|
||||
return None
|
||||
|
||||
avg_thickness = sum(thickness_values) / len(thickness_values)
|
||||
|
||||
return {
|
||||
"min_thickness": min_thickness,
|
||||
"max_thickness": max_thickness,
|
||||
"avg_thickness": avg_thickness,
|
||||
"thickness_map": {f"pair_{i}": v for i, v in enumerate(thickness_values[:50])},
|
||||
"measured_pairs": len(thickness_values),
|
||||
"min_location": min_location,
|
||||
"max_location": max_location,
|
||||
}
|
||||
|
||||
except ImportError:
|
||||
logger.warning("pythonOCC 不可用,无法进行精确壁厚检测")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.warning(f"精确壁厚检测失败: {e}")
|
||||
return None
|
||||
|
||||
def _detect_rib_features(self, geometry_data: Dict[str, Any],
|
||||
shape: Optional[TopoDS_Shape] = None) -> List[Dict[str, Any]]:
|
||||
"""检测加强筋特征"""
|
||||
features = []
|
||||
topology = geometry_data.get("topology", {})
|
||||
|
||||
face_count = topology.get("faces", 0)
|
||||
edge_count = topology.get("edges", 0)
|
||||
complexity_ratio = edge_count / max(face_count, 1)
|
||||
|
||||
if complexity_ratio > 3.0:
|
||||
confidence = 0.7
|
||||
if shape is not None:
|
||||
confidence = 0.78
|
||||
|
||||
features.append(create_mold_feature(
|
||||
feature_type="rib_structure",
|
||||
confidence=confidence,
|
||||
location=geometry_data.get("center_of_mass", [0, 0, 0]),
|
||||
dimensions=[2.0, 8.0, 2.0],
|
||||
parameters={"complexity_ratio": complexity_ratio},
|
||||
recommendations=[
|
||||
"检测到可能的加强筋结构",
|
||||
"建议加强筋厚度为壁厚的50-80%",
|
||||
"加强筋高度不超过壁厚的3倍",
|
||||
"加强筋根部增加圆角避免应力集中"
|
||||
]
|
||||
))
|
||||
|
||||
return features
|
||||
|
||||
def _detect_boss_features(self, geometry_data: Dict[str, Any],
|
||||
shape: Optional[TopoDS_Shape] = None) -> List[Dict[str, Any]]:
|
||||
"""检测BOSS柱特征"""
|
||||
features = []
|
||||
volume = geometry_data.get("volume", 0)
|
||||
bbox = geometry_data.get("bounding_box", {})
|
||||
|
||||
dimensions = bbox.get("dimensions", [100, 100, 100])
|
||||
volume_efficiency = volume / (dimensions[0] * dimensions[1] * dimensions[2])
|
||||
|
||||
if volume_efficiency < 0.3:
|
||||
confidence = 0.65
|
||||
if shape is not None:
|
||||
confidence = 0.72
|
||||
|
||||
features.append(create_mold_feature(
|
||||
feature_type="boss_feature",
|
||||
confidence=confidence,
|
||||
location=bbox.get("center", [50, 50, 50]),
|
||||
dimensions=[6.0, 12.0, 6.0],
|
||||
parameters={"volume_efficiency": volume_efficiency},
|
||||
recommendations=[
|
||||
"检测到可能的BOSS柱结构",
|
||||
"建议BOSS柱外径为螺钉直径的2-2.5倍",
|
||||
"BOSS柱高度不超过直径的2倍",
|
||||
"增加拔模角度1-2度",
|
||||
"根部增加圆角R0.5-R1.0"
|
||||
]
|
||||
))
|
||||
|
||||
return features
|
||||
|
||||
def _analyze_draft_angles(self, geometry_data: Dict[str, Any],
|
||||
shape: Optional[TopoDS_Shape] = None) -> List[Dict[str, Any]]:
|
||||
"""分析拔模角度"""
|
||||
features = []
|
||||
|
||||
if shape is not None:
|
||||
draft_result = self._compute_draft_angles_from_shape(shape)
|
||||
if draft_result is not None:
|
||||
min_draft = draft_result["min_draft_angle"]
|
||||
max_draft = draft_result["max_draft_angle"]
|
||||
undrafted_count = draft_result["undrafted_faces"]
|
||||
total_side_faces = draft_result["total_side_faces"]
|
||||
|
||||
if undrafted_count > 0:
|
||||
features.append(create_mold_feature(
|
||||
feature_type="draft_angle",
|
||||
confidence=0.90,
|
||||
location=geometry_data.get("center_of_mass", [0, 0, 0]),
|
||||
dimensions=[min_draft, max_draft, undrafted_count],
|
||||
parameters={
|
||||
"min_draft_angle": round(min_draft, 2),
|
||||
"max_draft_angle": round(max_draft, 2),
|
||||
"undrafted_faces": undrafted_count,
|
||||
"total_side_faces": total_side_faces,
|
||||
"estimation_method": "precise",
|
||||
},
|
||||
recommendations=[
|
||||
f"检测到 {undrafted_count} 个面需要拔模(当前最小拔模角 {min_draft:.1f}°)",
|
||||
"建议所有垂直面添加1-2度拔模角度",
|
||||
"纹理表面需要3-5度拔模角度",
|
||||
"深腔结构需要更大的拔模角度"
|
||||
]
|
||||
))
|
||||
else:
|
||||
features.append(create_mold_feature(
|
||||
feature_type="draft_angle",
|
||||
confidence=0.90,
|
||||
location=geometry_data.get("center_of_mass", [0, 0, 0]),
|
||||
dimensions=[min_draft, max_draft, 0],
|
||||
parameters={
|
||||
"min_draft_angle": round(min_draft, 2),
|
||||
"max_draft_angle": round(max_draft, 2),
|
||||
"undrafted_faces": 0,
|
||||
"total_side_faces": total_side_faces,
|
||||
"estimation_method": "precise",
|
||||
},
|
||||
recommendations=[
|
||||
f"所有侧壁面已有拔模角(最小 {min_draft:.1f}°)",
|
||||
"拔模角度满足要求"
|
||||
]
|
||||
))
|
||||
|
||||
return features
|
||||
|
||||
features.append(create_mold_feature(
|
||||
feature_type="draft_angle",
|
||||
confidence=0.8,
|
||||
location=geometry_data.get("center_of_mass", [0, 0, 0]),
|
||||
dimensions=[1.0, 2.0, 1.0],
|
||||
parameters={"recommended_angle": 2.0, "estimation_method": "heuristic"},
|
||||
recommendations=[
|
||||
"建议所有垂直面添加1-2度拔模角度",
|
||||
"纹理表面需要3-5度拔模角度",
|
||||
"深腔结构需要更大的拔模角度"
|
||||
]
|
||||
))
|
||||
|
||||
return features
|
||||
|
||||
def _compute_draft_angles_from_shape(self, shape: TopoDS_Shape) -> Optional[Dict[str, Any]]:
|
||||
"""基于面法向量分析计算各面的拔模角度"""
|
||||
try:
|
||||
from OCC.Core.TopExp import TopExp_Explorer
|
||||
from OCC.Core.TopAbs import TopAbs_FACE
|
||||
from OCC.Core.TopoDS import TopoDS_Face, topods
|
||||
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface
|
||||
from OCC.Core.BRepLProp import BRepLProp_SLProps
|
||||
from OCC.Core.gp import gp_Dir
|
||||
|
||||
draft_direction = gp_Dir(0, 0, 1)
|
||||
|
||||
draft_angles = []
|
||||
side_face_count = 0
|
||||
undrafted_count = 0
|
||||
|
||||
explorer = TopExp_Explorer(shape, TopAbs_FACE)
|
||||
while explorer.More():
|
||||
face = topods.Face(explorer.Current())
|
||||
surface = BRepAdaptor_Surface(face)
|
||||
|
||||
try:
|
||||
u = (surface.FirstUParameter() + surface.LastUParameter()) / 2
|
||||
v = (surface.FirstVParameter() + surface.LastVParameter()) / 2
|
||||
|
||||
normal = None
|
||||
if surface.GetType() == 0:
|
||||
normal = surface.Plane().Position().Direction()
|
||||
else:
|
||||
props = BRepLProp_SLProps(surface, 1, 0.001)
|
||||
props.SetParameters(u, v)
|
||||
if props.IsNormalDefined():
|
||||
normal = props.Normal()
|
||||
|
||||
if normal is not None:
|
||||
dot = abs(normal.Dot(draft_direction))
|
||||
angle_from_vertical = math.degrees(math.acos(min(dot, 1.0)))
|
||||
|
||||
if 5.0 < angle_from_vertical < 85.0:
|
||||
side_face_count += 1
|
||||
draft_angle = 90.0 - angle_from_vertical
|
||||
draft_angles.append(draft_angle)
|
||||
if draft_angle < 0.5:
|
||||
undrafted_count += 1
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
explorer.Next()
|
||||
|
||||
if not draft_angles:
|
||||
return None
|
||||
|
||||
return {
|
||||
"min_draft_angle": min(draft_angles),
|
||||
"max_draft_angle": max(draft_angles),
|
||||
"avg_draft_angle": sum(draft_angles) / len(draft_angles),
|
||||
"undrafted_faces": undrafted_count,
|
||||
"total_side_faces": side_face_count,
|
||||
}
|
||||
|
||||
except ImportError:
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.warning(f"拔模角度计算失败: {e}")
|
||||
return None
|
||||
|
||||
def _detect_curvature_features(self, shape: TopoDS_Shape) -> List[Dict[str, Any]]:
|
||||
"""检测高曲率区域(可能导致应力集中)"""
|
||||
features = []
|
||||
try:
|
||||
from OCC.Core.TopExp import TopExp_Explorer
|
||||
from OCC.Core.TopAbs import TopAbs_FACE
|
||||
from OCC.Core.TopoDS import TopoDS_Face, topods
|
||||
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface
|
||||
from OCC.Core.BRepLProp import BRepLProp_SLProps
|
||||
from OCC.Core.GProp import GProp_GProps
|
||||
from OCC.Core.BRepGProp import brepgprop
|
||||
|
||||
high_curvature_count = 0
|
||||
max_curvature_overall = 0.0
|
||||
|
||||
explorer = TopExp_Explorer(shape, TopAbs_FACE)
|
||||
while explorer.More():
|
||||
face = topods.Face(explorer.Current())
|
||||
surface = BRepAdaptor_Surface(face)
|
||||
|
||||
if surface.GetType() == 0:
|
||||
explorer.Next()
|
||||
continue
|
||||
|
||||
try:
|
||||
props = GProp_GProps()
|
||||
brepgprop.SurfaceProperties(face, props)
|
||||
face_area = props.Mass()
|
||||
|
||||
u_range = (surface.FirstUParameter(), surface.LastUParameter())
|
||||
v_range = (surface.FirstVParameter(), surface.LastVParameter())
|
||||
|
||||
max_curvature = 0.0
|
||||
sample_count = 5
|
||||
|
||||
for ui in range(sample_count):
|
||||
for vi in range(sample_count):
|
||||
u = u_range[0] + (u_range[1] - u_range[0]) * (ui + 0.5) / sample_count
|
||||
v = v_range[0] + (v_range[1] - v_range[0]) * (vi + 0.5) / sample_count
|
||||
|
||||
try:
|
||||
lprops = BRepLProp_SLProps(surface, 2, 0.001)
|
||||
lprops.SetParameters(u, v)
|
||||
if lprops.IsCurvatureDefined():
|
||||
k1 = abs(lprops.MinCurvature())
|
||||
k2 = abs(lprops.MaxCurvature())
|
||||
max_curvature = max(max_curvature, k1, k2)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if max_curvature > max_curvature_overall:
|
||||
max_curvature_overall = max_curvature
|
||||
|
||||
if max_curvature > 0.5:
|
||||
high_curvature_count += 1
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
explorer.Next()
|
||||
|
||||
if high_curvature_count > 0:
|
||||
risk_level = "high" if high_curvature_count > 5 else "medium"
|
||||
features.append(create_mold_feature(
|
||||
feature_type="high_curvature",
|
||||
confidence=0.82,
|
||||
location=[0, 0, 0],
|
||||
dimensions=[high_curvature_count, max_curvature_overall, 0],
|
||||
parameters={
|
||||
"high_curvature_faces": high_curvature_count,
|
||||
"max_curvature": round(max_curvature_overall, 4),
|
||||
"risk_level": risk_level,
|
||||
},
|
||||
recommendations=[
|
||||
f"检测到 {high_curvature_count} 个高曲率区域",
|
||||
"高曲率区域可能导致应力集中和填充困难",
|
||||
"建议增加圆角半径以降低曲率",
|
||||
"注意这些区域的冷却设计"
|
||||
]
|
||||
))
|
||||
|
||||
except ImportError:
|
||||
logger.debug("pythonOCC 不可用,跳过曲率检测")
|
||||
except Exception as e:
|
||||
logger.warning(f"曲率检测失败: {e}")
|
||||
|
||||
return features
|
||||
|
||||
def _detect_fillet_features(self, shape: TopoDS_Shape) -> List[Dict[str, Any]]:
|
||||
"""检测圆角/倒角特征"""
|
||||
features = []
|
||||
try:
|
||||
from OCC.Core.TopExp import TopExp_Explorer
|
||||
from OCC.Core.TopAbs import TopAbs_EDGE
|
||||
from OCC.Core.TopoDS import TopoDS_Edge, topods
|
||||
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface, BRepAdaptor_Curve
|
||||
|
||||
fillet_count = 0
|
||||
small_fillet_count = 0
|
||||
min_fillet_radius = float('inf')
|
||||
radii = []
|
||||
|
||||
edge_explorer = TopExp_Explorer(shape, TopAbs_EDGE)
|
||||
while edge_explorer.More():
|
||||
edge = topods.Edge(edge_explorer.Current())
|
||||
try:
|
||||
curve = BRepAdaptor_Curve(edge)
|
||||
curve_type = curve.GetType()
|
||||
|
||||
if curve_type == 2: # GeomAbs_Circle
|
||||
circle = curve.Circle()
|
||||
radius = circle.Radius()
|
||||
if 0.05 < radius < 50:
|
||||
fillet_count += 1
|
||||
radii.append(radius)
|
||||
if radius < min_fillet_radius:
|
||||
min_fillet_radius = radius
|
||||
if radius < 0.5:
|
||||
small_fillet_count += 1
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
edge_explorer.Next()
|
||||
|
||||
if fillet_count > 0:
|
||||
avg_radius = sum(radii) / len(radii)
|
||||
features.append(create_mold_feature(
|
||||
feature_type="fillet",
|
||||
confidence=0.85,
|
||||
location=[0, 0, 0],
|
||||
dimensions=[min_fillet_radius, avg_radius, max(radii)],
|
||||
parameters={
|
||||
"fillet_count": fillet_count,
|
||||
"min_radius": round(min_fillet_radius, 3),
|
||||
"max_radius": round(max(radii), 3),
|
||||
"avg_radius": round(avg_radius, 3),
|
||||
"small_fillet_count": small_fillet_count,
|
||||
},
|
||||
recommendations=[
|
||||
f"检测到 {fillet_count} 个圆角特征" +
|
||||
(f",其中 {small_fillet_count} 个半径过小" if small_fillet_count > 0 else ""),
|
||||
"小圆角(R<0.5mm)可能导致应力集中" if small_fillet_count > 0 else "",
|
||||
"建议圆角半径不小于 0.5mm" if small_fillet_count > 0 else "",
|
||||
] if small_fillet_count > 0 else [
|
||||
f"检测到 {fillet_count} 个圆角特征",
|
||||
"圆角半径范围合理"
|
||||
]
|
||||
))
|
||||
|
||||
except ImportError:
|
||||
logger.debug("pythonOCC 不可用,跳过圆角检测")
|
||||
except Exception as e:
|
||||
logger.warning(f"圆角检测失败: {e}")
|
||||
|
||||
return features
|
||||
|
||||
def _generate_recommendations(self, geometry_data: Dict[str, Any],
|
||||
features: List[Dict[str, Any]],
|
||||
material: str) -> List[Dict[str, Any]]:
|
||||
"""生成设计建议"""
|
||||
recommendations = []
|
||||
|
||||
wall_rec = self._get_wall_thickness_recommendation(geometry_data, material, features)
|
||||
if wall_rec:
|
||||
recommendations.append(wall_rec)
|
||||
|
||||
recommendations.append(create_design_recommendation(
|
||||
rec_type="draft_angle",
|
||||
priority="high",
|
||||
description="添加拔模角度",
|
||||
parameters={"min_angle": 1.0, "preferred_angle": 2.0},
|
||||
reason="确保顺利脱模"
|
||||
))
|
||||
|
||||
for feature in features:
|
||||
if feature["feature_type"] == "thin_wall":
|
||||
params = feature.get("parameters", {})
|
||||
current = params.get("min_thickness", params.get("average_thickness", 0))
|
||||
rec = create_design_recommendation(
|
||||
rec_type="wall_thickness",
|
||||
priority="high",
|
||||
description="增加壁厚",
|
||||
parameters={
|
||||
"current": current,
|
||||
"recommended": self.feature_thresholds["thin_wall"]
|
||||
},
|
||||
reason="壁厚不足影响结构强度"
|
||||
)
|
||||
recommendations.append(rec)
|
||||
elif feature["feature_type"] == "high_curvature":
|
||||
recommendations.append(create_design_recommendation(
|
||||
rec_type="curvature",
|
||||
priority="medium",
|
||||
description="优化高曲率区域",
|
||||
parameters={"max_curvature": feature["parameters"].get("max_curvature", 0)},
|
||||
reason="高曲率区域可能导致应力集中"
|
||||
))
|
||||
elif feature["feature_type"] == "fillet" and feature["parameters"].get("small_fillet_count", 0) > 0:
|
||||
recommendations.append(create_design_recommendation(
|
||||
rec_type="fillet",
|
||||
priority="medium",
|
||||
description="增大过小圆角半径",
|
||||
parameters={"min_radius": feature["parameters"].get("min_radius", 0)},
|
||||
reason="小圆角导致应力集中和加工困难"
|
||||
))
|
||||
|
||||
return recommendations
|
||||
|
||||
def _get_wall_thickness_recommendation(self, geometry_data: Dict[str, Any],
|
||||
material: str,
|
||||
features: List[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
"""获取壁厚建议"""
|
||||
avg_thickness = None
|
||||
|
||||
if features:
|
||||
for f in features:
|
||||
if f["feature_type"] in ("thin_wall", "thick_wall"):
|
||||
params = f.get("parameters", {})
|
||||
avg_thickness = params.get("avg_thickness", params.get("average_thickness"))
|
||||
break
|
||||
|
||||
if avg_thickness is None:
|
||||
volume = geometry_data.get("volume", 0)
|
||||
surface_area = geometry_data.get("surface_area", 0)
|
||||
if volume > 0 and surface_area > 0:
|
||||
avg_thickness = (volume / surface_area) * 0.6
|
||||
|
||||
if avg_thickness is not None:
|
||||
material_props = self.product_materials.get(material, self.product_materials["ABS"])
|
||||
min_wall = material_props["min_wall"]
|
||||
|
||||
if avg_thickness < min_wall:
|
||||
return create_design_recommendation(
|
||||
rec_type="wall_thickness",
|
||||
priority="high",
|
||||
description=f"增加壁厚至{min_wall}mm以上",
|
||||
parameters={"current": avg_thickness, "recommended": min_wall},
|
||||
reason=f"{material}材料最小壁厚要求"
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
def _calculate_quality_metrics(self, geometry_data: Dict[str, Any],
|
||||
features: List[Dict[str, Any]]) -> Dict[str, float]:
|
||||
"""计算质量指标"""
|
||||
metrics = {}
|
||||
|
||||
bbox = geometry_data.get("bounding_box", {})
|
||||
dimensions = bbox.get("dimensions", [100, 100, 100])
|
||||
volume = geometry_data.get("volume", 0)
|
||||
bbox_volume = dimensions[0] * dimensions[1] * dimensions[2]
|
||||
|
||||
metrics["volume_utilization"] = volume / bbox_volume if bbox_volume > 0 else 0
|
||||
|
||||
topology = geometry_data.get("topology", {})
|
||||
face_count = topology.get("faces", 0)
|
||||
metrics["topology_complexity"] = face_count / 100.0
|
||||
|
||||
wall_uniformity = 0.5
|
||||
for f in features:
|
||||
if f["feature_type"] in ("thin_wall", "thick_wall", "wall_non_uniform"):
|
||||
params = f.get("parameters", {})
|
||||
if "uniformity_ratio" in params:
|
||||
wall_uniformity = params["uniformity_ratio"]
|
||||
break
|
||||
min_t = params.get("min_thickness", params.get("average_thickness", 0))
|
||||
max_t = params.get("max_thickness", params.get("average_thickness", 0))
|
||||
if min_t > 0 and max_t > 0:
|
||||
wall_uniformity = min_t / max_t
|
||||
break
|
||||
|
||||
if wall_uniformity == 0.5:
|
||||
surface_area = geometry_data.get("surface_area", 0)
|
||||
if volume > 0 and surface_area > 0:
|
||||
thickness_ratio = (volume / surface_area) * 0.6
|
||||
ideal_thickness = 3.0
|
||||
wall_uniformity = 1.0 - abs(thickness_ratio - ideal_thickness) / ideal_thickness
|
||||
|
||||
metrics["wall_uniformity"] = max(0, min(1, wall_uniformity))
|
||||
|
||||
draft_score = 1.0
|
||||
for f in features:
|
||||
if f["feature_type"] == "draft_angle":
|
||||
params = f.get("parameters", {})
|
||||
undrafted = params.get("undrafted_faces", None)
|
||||
total = params.get("total_side_faces", 1)
|
||||
if undrafted is not None and total > 0:
|
||||
draft_score = 1.0 - (undrafted / total)
|
||||
break
|
||||
metrics["draft_score"] = round(draft_score, 3)
|
||||
|
||||
return metrics
|
||||
|
||||
def _generate_analysis_summary(self, geometry_data: Dict[str, Any],
|
||||
features: List[Dict[str, Any]],
|
||||
recommendations: List[Dict[str, Any]]) -> str:
|
||||
"""生成分析摘要"""
|
||||
volume = geometry_data.get("volume", 0)
|
||||
high_priority_recs = len([r for r in recommendations if r["priority"] == "high"])
|
||||
|
||||
summary_parts = []
|
||||
|
||||
if volume > 0:
|
||||
summary_parts.append(f"模型体积: {volume / 1000:.1f} cm³")
|
||||
|
||||
if features:
|
||||
feature_types = set(f["feature_type"] for f in features)
|
||||
summary_parts.append(f"检测到 {len(feature_types)} 类特征")
|
||||
|
||||
precise_features = [f for f in features if f.get("parameters", {}).get("estimation_method") == "precise"]
|
||||
if precise_features:
|
||||
summary_parts.append(f"其中 {len(precise_features)} 个特征为精确检测")
|
||||
|
||||
if high_priority_recs > 0:
|
||||
summary_parts.append(f"有 {high_priority_recs} 个高优先级建议")
|
||||
|
||||
return " | ".join(summary_parts) if summary_parts else "分析完成"
|
||||
@@ -0,0 +1,241 @@
|
||||
# src/core/mesh_generator.py
|
||||
import logging
|
||||
import numpy as np
|
||||
from typing import Dict, List, Optional, Any
|
||||
import trimesh
|
||||
from trimesh import sample as trimesh_sample
|
||||
from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh
|
||||
from OCC.Core.TopExp import TopExp_Explorer
|
||||
from OCC.Core.TopAbs import TopAbs_FACE
|
||||
from OCC.Core.BRep import BRep_Tool
|
||||
from OCC.Core.TopoDS import TopoDS_Shape
|
||||
from OCC.Core.TopLoc import TopLoc_Location
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MeshGenerator:
|
||||
"""网格生成器 - 从PythonOCC形状生成点云,支持多级LOD"""
|
||||
|
||||
def __init__(self, quality: str = "medium"):
|
||||
self.quality_settings = {
|
||||
"low": 1.0,
|
||||
"medium": 0.3,
|
||||
"high": 0.1
|
||||
}
|
||||
self.quality = self.quality_settings.get(quality, 0.3)
|
||||
|
||||
def generate_mesh_from_shape(self, shape: TopoDS_Shape, num_points: int = 20000) -> Dict:
|
||||
"""从PythonOCC形状生成点云数据"""
|
||||
try:
|
||||
mesh = BRepMesh_IncrementalMesh(shape, self.quality, False, 0.5, True)
|
||||
mesh.Perform()
|
||||
logger.info(f"OCC网格生成完成, 网格状态: {mesh.IsDone()}")
|
||||
|
||||
all_vertices = []
|
||||
all_faces = []
|
||||
vertex_offset = 0
|
||||
|
||||
explorer = TopExp_Explorer(shape, TopAbs_FACE)
|
||||
face_count = 0
|
||||
|
||||
while explorer.More():
|
||||
face = explorer.Current()
|
||||
face_count += 1
|
||||
|
||||
location = TopLoc_Location()
|
||||
face_triangulation = BRep_Tool.Triangulation(face, location)
|
||||
|
||||
if face_triangulation is None:
|
||||
logger.warning(f"面 {face_count} 没有三角剖分数据")
|
||||
explorer.Next()
|
||||
continue
|
||||
|
||||
trsf = location.Transformation()
|
||||
|
||||
nb_nodes = face_triangulation.NbNodes()
|
||||
nb_triangles = face_triangulation.NbTriangles()
|
||||
|
||||
logger.debug(f"面 {face_count}: {nb_nodes} 个顶点, {nb_triangles} 个三角形")
|
||||
|
||||
face_vertices = []
|
||||
for i in range(1, nb_nodes + 1):
|
||||
pnt = face_triangulation.Node(i)
|
||||
transformed = pnt.Transformed(trsf)
|
||||
face_vertices.append([
|
||||
float(transformed.X()),
|
||||
float(transformed.Y()),
|
||||
float(transformed.Z()),
|
||||
])
|
||||
|
||||
face_indices = []
|
||||
for i in range(1, nb_triangles + 1):
|
||||
tri = face_triangulation.Triangle(i)
|
||||
idx1 = tri.Value(1)
|
||||
idx2 = tri.Value(2)
|
||||
idx3 = tri.Value(3)
|
||||
face_indices.append([
|
||||
vertex_offset + idx1 - 1,
|
||||
vertex_offset + idx2 - 1,
|
||||
vertex_offset + idx3 - 1
|
||||
])
|
||||
|
||||
all_vertices.extend(face_vertices)
|
||||
all_faces.extend(face_indices)
|
||||
vertex_offset += len(face_vertices)
|
||||
|
||||
explorer.Next()
|
||||
|
||||
if len(all_vertices) == 0:
|
||||
logger.warning("未提取到任何顶点,使用示例数据")
|
||||
return self._create_sample_pointcloud()
|
||||
|
||||
vertices = np.array(all_vertices, dtype=np.float32)
|
||||
faces = np.array(all_faces, dtype=np.int32)
|
||||
|
||||
logger.info(f"总共提取了 {len(vertices)} 个顶点, {len(faces)} 个三角形面, {face_count} 个面")
|
||||
|
||||
tri_mesh = trimesh.Trimesh(vertices=vertices, faces=faces, process=True)
|
||||
|
||||
actual_num_points = min(num_points, len(faces) * 2)
|
||||
logger.info(f"采样点数: {actual_num_points}")
|
||||
|
||||
points, face_idx = trimesh.sample.sample_surface(tri_mesh, actual_num_points)
|
||||
|
||||
normals = tri_mesh.face_normals[face_idx]
|
||||
|
||||
logger.info(f"生成了 {len(points)} 个点云点")
|
||||
|
||||
return {
|
||||
"vertices": vertices.tolist(),
|
||||
"faces": faces.tolist(),
|
||||
"points": points.tolist(),
|
||||
"normals": normals.tolist(),
|
||||
"point_count": int(len(points)),
|
||||
"vertex_count": int(len(vertices)),
|
||||
"face_count": int(len(faces))
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"网格生成失败: {e}")
|
||||
import traceback
|
||||
logger.error(traceback.format_exc())
|
||||
return self._create_sample_pointcloud()
|
||||
|
||||
def generate_multi_lod_mesh(self, shape: TopoDS_Shape) -> Dict:
|
||||
"""生成多级LOD网格 - 一次OCC剖分,trimesh简化,避免重复计算
|
||||
|
||||
返回结构:
|
||||
{
|
||||
"lods": {
|
||||
"0": { "vertices": [...], "faces": [...], "vertex_count": N, "face_count": N },
|
||||
"1": { ... 50%简化 ... },
|
||||
"2": { ... 80%简化 ... }
|
||||
},
|
||||
"points": [...], "normals": [...], "point_count": N,
|
||||
"vertex_count": N, "face_count": N
|
||||
}
|
||||
"""
|
||||
try:
|
||||
full_mesh_result = self.generate_mesh_from_shape(shape, num_points=20000)
|
||||
|
||||
vertices = np.array(full_mesh_result["vertices"], dtype=np.float32)
|
||||
faces = np.array(full_mesh_result["faces"], dtype=np.int32)
|
||||
|
||||
if len(vertices) == 0 or len(faces) == 0:
|
||||
sample = self._create_sample_pointcloud()
|
||||
return self._wrap_sample_as_lod(sample)
|
||||
|
||||
tri_mesh = trimesh.Trimesh(vertices=vertices, faces=faces, process=True)
|
||||
full_face_count = len(tri_mesh.faces)
|
||||
logger.info(f"全精度网格: {len(tri_mesh.vertices)} 顶点, {full_face_count} 面")
|
||||
|
||||
lods = {
|
||||
"0": self._mesh_to_lod_entry(tri_mesh, "LOD0-全精度")
|
||||
}
|
||||
|
||||
lod_ratios = {"1": 0.50, "2": 0.20}
|
||||
for lod_level, ratio in lod_ratios.items():
|
||||
if full_face_count < 300:
|
||||
lods[lod_level] = lods["0"]
|
||||
continue
|
||||
|
||||
target_faces = max(int(full_face_count * ratio), 200)
|
||||
try:
|
||||
simplified = tri_mesh.simplify_quadric_decimation(target_faces)
|
||||
if simplified is None or len(simplified.faces) < 3:
|
||||
simplified = self._fast_decimate(tri_mesh, target_faces)
|
||||
lods[lod_level] = self._mesh_to_lod_entry(simplified, f"LOD{lod_level}-简化{int((1-ratio)*100)}%")
|
||||
logger.info(f"LOD{lod_level}: {len(simplified.vertices)} 顶点, {len(simplified.faces)} 面 (目标{target_faces})")
|
||||
except Exception as dec_err:
|
||||
logger.warning(f"LOD{lod_level} 简化失败,回退到全精度: {dec_err}")
|
||||
lods[lod_level] = lods["0"]
|
||||
|
||||
result = {
|
||||
"lods": lods,
|
||||
"points": full_mesh_result["points"],
|
||||
"normals": full_mesh_result["normals"],
|
||||
"point_count": full_mesh_result["point_count"],
|
||||
"vertex_count": full_mesh_result["vertex_count"],
|
||||
"face_count": full_mesh_result["face_count"],
|
||||
}
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"多级LOD网格生成失败: {e}")
|
||||
import traceback
|
||||
logger.error(traceback.format_exc())
|
||||
sample = self._create_sample_pointcloud()
|
||||
return self._wrap_sample_as_lod(sample)
|
||||
|
||||
def _mesh_to_lod_entry(self, mesh: trimesh.Trimesh, label: str) -> Dict:
|
||||
return {
|
||||
"vertices": mesh.vertices.tolist(),
|
||||
"faces": mesh.faces.tolist(),
|
||||
"vertex_count": int(len(mesh.vertices)),
|
||||
"face_count": int(len(mesh.faces)),
|
||||
}
|
||||
|
||||
def _fast_decimate(self, mesh: trimesh.Trimesh, target_faces: int) -> trimesh.Trimesh:
|
||||
"""快速回退降采样:按面索引均匀采样"""
|
||||
if target_faces >= len(mesh.faces):
|
||||
return mesh
|
||||
step = max(len(mesh.faces) // target_faces, 1)
|
||||
indices = np.arange(0, len(mesh.faces), step)[:target_faces]
|
||||
return mesh.submesh([np.array(indices)], only_watertight=False, append=True)
|
||||
|
||||
def _wrap_sample_as_lod(self, sample: Dict) -> Dict:
|
||||
lods = {
|
||||
"0": {
|
||||
"vertices": sample["vertices"],
|
||||
"faces": sample["faces"],
|
||||
"vertex_count": sample["vertex_count"],
|
||||
"face_count": sample["face_count"],
|
||||
}
|
||||
}
|
||||
lods["1"] = lods["0"]
|
||||
lods["2"] = lods["0"]
|
||||
return {
|
||||
"lods": lods,
|
||||
"points": sample["points"],
|
||||
"normals": sample["normals"],
|
||||
"point_count": sample["point_count"],
|
||||
"vertex_count": sample["vertex_count"],
|
||||
"face_count": sample["face_count"],
|
||||
}
|
||||
|
||||
def _create_sample_pointcloud(self) -> Dict:
|
||||
"""创建示例点云(备用)"""
|
||||
mesh = trimesh.creation.box([100, 80, 50])
|
||||
points, _ = trimesh.sample.sample_surface(mesh, 5000)
|
||||
normals = mesh.face_normals[:len(points)]
|
||||
|
||||
return {
|
||||
"vertices": mesh.vertices.tolist(),
|
||||
"faces": mesh.faces.tolist(),
|
||||
"points": points.tolist(),
|
||||
"normals": normals.tolist(),
|
||||
"point_count": len(points),
|
||||
"vertex_count": len(mesh.vertices),
|
||||
"face_count": len(mesh.faces)
|
||||
}
|
||||
@@ -0,0 +1,646 @@
|
||||
"""
|
||||
模具刀路设计与G代码生成模块
|
||||
|
||||
架构:
|
||||
1. ToolLibrary - 刀具库与切削参数管理
|
||||
2. CuttingParamsCalculator - 切削参数自动计算
|
||||
3. RoughingToolpathGenerator - 粗加工刀路生成
|
||||
4. FinishingToolpathGenerator - 精加工刀路生成
|
||||
5. GCodePostProcessor - G代码后处理器
|
||||
6. MoldCAMDesigner - 模具CAM综合设计器
|
||||
|
||||
加工策略:
|
||||
- 粗加工:Z层等高粗加工(自适应清根)
|
||||
- 半精加工:等高线铣削
|
||||
- 精加工:平行铣削/螺旋铣削/等高线精加工
|
||||
- 清角:笔式清角
|
||||
- 钻孔:冷却水路/顶针孔/螺丝孔
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
import math
|
||||
from shared.utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class ToolLibrary:
|
||||
"""刀具库"""
|
||||
|
||||
TOOLS = {
|
||||
"endmill_20mm": {
|
||||
"type": "endmill", "diameter": 20.0, "flute_length": 60.0,
|
||||
"cutting_edges": 4, "material": "carbide",
|
||||
"corner_radius": 0.0,
|
||||
"speeds_feeds": {
|
||||
"cutting_speed": 100, "feed_per_tooth": 0.15,
|
||||
"axial_depth": 10.0, "radial_depth": 15.0
|
||||
}
|
||||
},
|
||||
"endmill_16mm": {
|
||||
"type": "endmill", "diameter": 16.0, "flute_length": 50.0,
|
||||
"cutting_edges": 4, "material": "carbide",
|
||||
"corner_radius": 0.0,
|
||||
"speeds_feeds": {
|
||||
"cutting_speed": 120, "feed_per_tooth": 0.12,
|
||||
"axial_depth": 8.0, "radial_depth": 12.0
|
||||
}
|
||||
},
|
||||
"endmill_10mm": {
|
||||
"type": "endmill", "diameter": 10.0, "flute_length": 35.0,
|
||||
"cutting_edges": 4, "material": "carbide",
|
||||
"corner_radius": 0.0,
|
||||
"speeds_feeds": {
|
||||
"cutting_speed": 130, "feed_per_tooth": 0.10,
|
||||
"axial_depth": 5.0, "radial_depth": 8.0
|
||||
}
|
||||
},
|
||||
"endmill_6mm": {
|
||||
"type": "endmill", "diameter": 6.0, "flute_length": 22.0,
|
||||
"cutting_edges": 3, "material": "carbide",
|
||||
"corner_radius": 0.0,
|
||||
"speeds_feeds": {
|
||||
"cutting_speed": 140, "feed_per_tooth": 0.06,
|
||||
"axial_depth": 3.0, "radial_depth": 4.0
|
||||
}
|
||||
},
|
||||
"ballnose_10mm": {
|
||||
"type": "ballnose", "diameter": 10.0, "flute_length": 30.0,
|
||||
"cutting_edges": 2, "material": "carbide",
|
||||
"corner_radius": 5.0,
|
||||
"speeds_feeds": {
|
||||
"cutting_speed": 150, "feed_per_tooth": 0.08,
|
||||
"axial_depth": 0.5, "radial_depth": 1.0
|
||||
}
|
||||
},
|
||||
"ballnose_6mm": {
|
||||
"type": "ballnose", "diameter": 6.0, "flute_length": 22.0,
|
||||
"cutting_edges": 2, "material": "carbide",
|
||||
"corner_radius": 3.0,
|
||||
"speeds_feeds": {
|
||||
"cutting_speed": 160, "feed_per_tooth": 0.06,
|
||||
"axial_depth": 0.3, "radial_depth": 0.5
|
||||
}
|
||||
},
|
||||
"ballnose_3mm": {
|
||||
"type": "ballnose", "diameter": 3.0, "flute_length": 12.0,
|
||||
"cutting_edges": 2, "material": "carbide",
|
||||
"corner_radius": 1.5,
|
||||
"speeds_feeds": {
|
||||
"cutting_speed": 180, "feed_per_tooth": 0.03,
|
||||
"axial_depth": 0.15, "radial_depth": 0.3
|
||||
}
|
||||
},
|
||||
"ballnose_1mm": {
|
||||
"type": "ballnose", "diameter": 1.0, "flute_length": 5.0,
|
||||
"cutting_edges": 2, "material": "carbide",
|
||||
"corner_radius": 0.5,
|
||||
"speeds_feeds": {
|
||||
"cutting_speed": 200, "feed_per_tooth": 0.01,
|
||||
"axial_depth": 0.05, "radial_depth": 0.1
|
||||
}
|
||||
},
|
||||
"drill_8mm": {
|
||||
"type": "drill", "diameter": 8.0, "flute_length": 50.0,
|
||||
"cutting_edges": 2, "material": "carbide",
|
||||
"corner_radius": 0.0,
|
||||
"speeds_feeds": {
|
||||
"cutting_speed": 80, "feed_per_tooth": 0.10,
|
||||
"axial_depth": 50.0, "radial_depth": 0.0
|
||||
}
|
||||
},
|
||||
"drill_5mm": {
|
||||
"type": "drill", "diameter": 5.0, "flute_length": 35.0,
|
||||
"cutting_edges": 2, "material": "carbide",
|
||||
"corner_radius": 0.0,
|
||||
"speeds_feeds": {
|
||||
"cutting_speed": 90, "feed_per_tooth": 0.08,
|
||||
"axial_depth": 35.0, "radial_depth": 0.0
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
MOLD_STEEL = {
|
||||
"P20": {"hardness_hrc": 30, "cutting_speed_factor": 1.0, "feed_factor": 1.0},
|
||||
"718H": {"hardness_hrc": 35, "cutting_speed_factor": 0.85, "feed_factor": 0.9},
|
||||
"NAK80": {"hardness_hrc": 38, "cutting_speed_factor": 0.75, "feed_factor": 0.85},
|
||||
"S136": {"hardness_hrc": 50, "cutting_speed_factor": 0.5, "feed_factor": 0.7},
|
||||
"H13": {"hardness_hrc": 48, "cutting_speed_factor": 0.55, "feed_factor": 0.75},
|
||||
"Al7075": {"hardness_hrc": 15, "cutting_speed_factor": 2.0, "feed_factor": 1.5},
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_tool(cls, tool_id: str) -> Optional[Dict]:
|
||||
return cls.TOOLS.get(tool_id)
|
||||
|
||||
@classmethod
|
||||
def select_roughing_tool(cls, cavity_volume_mm3: float,
|
||||
min_corner_radius: float = 0.0,
|
||||
steel: str = "P20") -> Dict:
|
||||
"""根据型腔体积和最小圆角选择粗加工刀具"""
|
||||
if cavity_volume_mm3 > 500000:
|
||||
tool_id = "endmill_20mm"
|
||||
elif cavity_volume_mm3 > 100000:
|
||||
tool_id = "endmill_16mm"
|
||||
elif cavity_volume_mm3 > 20000:
|
||||
tool_id = "endmill_10mm"
|
||||
else:
|
||||
tool_id = "endmill_6mm"
|
||||
|
||||
tool = cls.TOOLS[tool_id].copy()
|
||||
steel_props = cls.MOLD_STEEL.get(steel, cls.MOLD_STEEL["P20"])
|
||||
|
||||
tool["speeds_feeds"] = cls._adjust_for_steel(tool["speeds_feeds"], steel_props)
|
||||
tool["tool_id"] = tool_id
|
||||
return tool
|
||||
|
||||
@classmethod
|
||||
def select_finishing_tool(cls, surface_quality: str = "standard",
|
||||
min_corner_radius: float = 0.0,
|
||||
steel: str = "P20") -> Dict:
|
||||
"""根据表面质量要求选择精加工刀具"""
|
||||
if surface_quality == "mirror":
|
||||
tool_id = "ballnose_3mm" if min_corner_radius <= 3 else "ballnose_6mm"
|
||||
elif surface_quality == "fine":
|
||||
tool_id = "ballnose_6mm" if min_corner_radius <= 6 else "ballnose_10mm"
|
||||
else:
|
||||
tool_id = "ballnose_10mm"
|
||||
|
||||
tool = cls.TOOLS[tool_id].copy()
|
||||
steel_props = cls.MOLD_STEEL.get(steel, cls.MOLD_STEEL["P20"])
|
||||
|
||||
tool["speeds_feeds"] = cls._adjust_for_steel(tool["speeds_feeds"], steel_props)
|
||||
tool["tool_id"] = tool_id
|
||||
return tool
|
||||
|
||||
@classmethod
|
||||
def _adjust_for_steel(cls, speeds_feeds: Dict, steel_props: Dict) -> Dict:
|
||||
"""根据模具钢调整切削参数"""
|
||||
adjusted = speeds_feeds.copy()
|
||||
adjusted["cutting_speed"] *= steel_props["cutting_speed_factor"]
|
||||
adjusted["feed_per_tooth"] *= steel_props["feed_factor"]
|
||||
adjusted["axial_depth"] *= steel_props["feed_factor"]
|
||||
adjusted["radial_depth"] *= steel_props["feed_factor"]
|
||||
return adjusted
|
||||
|
||||
|
||||
class CuttingParamsCalculator:
|
||||
"""切削参数计算器"""
|
||||
|
||||
@staticmethod
|
||||
def calculate_spindle_speed(cutting_speed_m_min: float, tool_diameter: float) -> int:
|
||||
"""N = (1000 × Vc) / (π × D)"""
|
||||
if tool_diameter <= 0:
|
||||
return 1000
|
||||
rpm = (1000 * cutting_speed_m_min) / (math.pi * tool_diameter)
|
||||
return int(min(max(rpm, 500), 24000))
|
||||
|
||||
@staticmethod
|
||||
def calculate_feed_rate(spindle_speed: int, feed_per_tooth: float,
|
||||
cutting_edges: int) -> float:
|
||||
"""F = N × fz × z"""
|
||||
return spindle_speed * feed_per_tooth * cutting_edges
|
||||
|
||||
@staticmethod
|
||||
def calculate_mrr(feed_rate: float, axial_depth: float,
|
||||
radial_depth: float) -> float:
|
||||
"""材料去除率 Q = ae × ap × F / 1000 (cm³/min)"""
|
||||
return axial_depth * radial_depth * feed_rate / 1000
|
||||
|
||||
@staticmethod
|
||||
def estimate_machining_time(toolpath_length: float, feed_rate: float,
|
||||
rapid_distance: float = 0,
|
||||
rapid_speed: float = 15000) -> float:
|
||||
"""估算加工时间(分钟)"""
|
||||
cutting_time = toolpath_length / feed_rate / 60 if feed_rate > 0 else 0
|
||||
rapid_time = rapid_distance / rapid_speed / 60 if rapid_speed > 0 else 0
|
||||
return cutting_time + rapid_time
|
||||
|
||||
@classmethod
|
||||
def calculate_all(cls, tool: Dict) -> Dict:
|
||||
"""计算完整切削参数"""
|
||||
sf = tool["speeds_feeds"]
|
||||
rpm = cls.calculate_spindle_speed(sf["cutting_speed"], tool["diameter"])
|
||||
feed = cls.calculate_feed_rate(rpm, sf["feed_per_tooth"], tool["cutting_edges"])
|
||||
mrr = cls.calculate_mrr(feed, sf["axial_depth"], sf["radial_depth"])
|
||||
|
||||
return {
|
||||
"tool_id": tool.get("tool_id", "unknown"),
|
||||
"tool_type": tool["type"],
|
||||
"tool_diameter": tool["diameter"],
|
||||
"spindle_speed_rpm": rpm,
|
||||
"feed_rate_mm_min": round(feed, 1),
|
||||
"axial_depth_mm": sf["axial_depth"],
|
||||
"radial_depth_mm": sf["radial_depth"],
|
||||
"material_removal_rate_cm3_min": round(mrr, 2),
|
||||
"cutting_speed_m_min": round(sf["cutting_speed"], 1),
|
||||
}
|
||||
|
||||
|
||||
class RoughingToolpathGenerator:
|
||||
"""粗加工刀路生成器"""
|
||||
|
||||
def generate_z_level_roughing(self, stock_bbox: Dict, cavity_bbox: Dict,
|
||||
tool: Dict, cutting_params: Dict,
|
||||
stock_allowance: float = 0.5) -> Dict[str, Any]:
|
||||
"""
|
||||
Z层等高粗加工
|
||||
|
||||
策略:从顶面逐层向下铣削,每层切深为 axial_depth
|
||||
|
||||
Args:
|
||||
stock_bbox: 毛坯边界框
|
||||
cavity_bbox: 型腔边界框
|
||||
tool: 刀具参数
|
||||
cutting_params: 切削参数
|
||||
stock_allowance: 精加工余量 mm
|
||||
|
||||
Returns:
|
||||
粗加工刀路方案
|
||||
"""
|
||||
z_min = cavity_bbox.get("min", [0, 0, 0])[2]
|
||||
z_max = cavity_bbox.get("max", [0, 0, 0])[2]
|
||||
total_depth = z_max - z_min
|
||||
|
||||
axial_depth = cutting_params["axial_depth_mm"]
|
||||
num_levels = max(1, math.ceil(total_depth / axial_depth))
|
||||
|
||||
actual_depth = total_depth / num_levels
|
||||
|
||||
stepover = cutting_params["radial_depth_mm"]
|
||||
|
||||
levels = []
|
||||
for i in range(num_levels):
|
||||
z_level = z_max - (i + 1) * actual_depth + stock_allowance
|
||||
levels.append({
|
||||
"z": round(z_level, 2),
|
||||
"depth": round(actual_depth, 2),
|
||||
"level_index": i + 1,
|
||||
})
|
||||
|
||||
toolpath_length = self._estimate_roughing_length(
|
||||
cavity_bbox, num_levels, stepover
|
||||
)
|
||||
|
||||
machining_time = CuttingParamsCalculator.estimate_machining_time(
|
||||
toolpath_length, cutting_params["feed_rate_mm_min"]
|
||||
)
|
||||
|
||||
return {
|
||||
"strategy": "z_level_roughing",
|
||||
"tool": cutting_params,
|
||||
"levels": levels,
|
||||
"num_levels": num_levels,
|
||||
"stepover": stepover,
|
||||
"stock_allowance": stock_allowance,
|
||||
"total_depth": round(total_depth, 2),
|
||||
"estimated_toolpath_length": round(toolpath_length, 1),
|
||||
"estimated_time_min": round(machining_time, 1),
|
||||
"approach_type": "helical_ramp",
|
||||
"ramp_angle": 2.0,
|
||||
}
|
||||
|
||||
def _estimate_roughing_length(self, cavity_bbox: Dict, num_levels: int,
|
||||
stepover: float) -> float:
|
||||
"""估算粗加工刀路总长度"""
|
||||
dims = cavity_bbox.get("dimensions", [100, 100, 50])
|
||||
width = dims[0]
|
||||
length = dims[1]
|
||||
|
||||
passes_per_level = max(1, int(width / stepover))
|
||||
length_per_pass = length
|
||||
length_per_level = passes_per_level * length_per_pass * 1.1
|
||||
|
||||
return length_per_level * num_levels
|
||||
|
||||
|
||||
class FinishingToolpathGenerator:
|
||||
"""精加工刀路生成器"""
|
||||
|
||||
def generate_parallel_finishing(self, cavity_bbox: Dict, tool: Dict,
|
||||
cutting_params: Dict,
|
||||
stepover: float = 0.3,
|
||||
angle: float = 0.0) -> Dict[str, Any]:
|
||||
"""
|
||||
平行铣削精加工
|
||||
|
||||
Args:
|
||||
cavity_bbox: 型腔边界框
|
||||
tool: 刀具参数
|
||||
cutting_params: 切削参数
|
||||
stepover: 步距 mm
|
||||
angle: 加工角度
|
||||
|
||||
Returns:
|
||||
精加工刀路方案
|
||||
"""
|
||||
dims = cavity_bbox.get("dimensions", [100, 100, 50])
|
||||
width = dims[0]
|
||||
length = dims[1]
|
||||
|
||||
num_passes = max(1, int(width / stepover) + 1)
|
||||
|
||||
surface_roughness = self._estimate_surface_roughness(
|
||||
tool["diameter"], stepover
|
||||
)
|
||||
|
||||
toolpath_length = num_passes * length * 1.05
|
||||
|
||||
machining_time = CuttingParamsCalculator.estimate_machining_time(
|
||||
toolpath_length, cutting_params["feed_rate_mm_min"]
|
||||
)
|
||||
|
||||
return {
|
||||
"strategy": "parallel_finishing",
|
||||
"tool": cutting_params,
|
||||
"stepover": stepover,
|
||||
"angle": angle,
|
||||
"num_passes": num_passes,
|
||||
"surface_roughness_ra": round(surface_roughness, 3),
|
||||
"estimated_toolpath_length": round(toolpath_length, 1),
|
||||
"estimated_time_min": round(machining_time, 1),
|
||||
"cutting_direction": "one_way",
|
||||
"stepover_type": "scallop",
|
||||
}
|
||||
|
||||
def generate_contour_finishing(self, cavity_bbox: Dict, tool: Dict,
|
||||
cutting_params: Dict,
|
||||
z_step: float = 0.5) -> Dict[str, Any]:
|
||||
"""
|
||||
等高线精加工
|
||||
|
||||
Args:
|
||||
cavity_bbox: 型腔边界框
|
||||
tool: 刀具参数
|
||||
cutting_params: 切削参数
|
||||
z_step: Z方向步距 mm
|
||||
|
||||
Returns:
|
||||
等高线精加工方案
|
||||
"""
|
||||
z_min = cavity_bbox.get("min", [0, 0, 0])[2]
|
||||
z_max = cavity_bbox.get("max", [0, 0, 0])[2]
|
||||
total_depth = z_max - z_min
|
||||
|
||||
num_levels = max(1, int(total_depth / z_step) + 1)
|
||||
|
||||
dims = cavity_bbox.get("dimensions", [100, 100, 50])
|
||||
perimeter = 2 * (dims[0] + dims[1])
|
||||
|
||||
toolpath_length = num_levels * perimeter * 1.1
|
||||
|
||||
machining_time = CuttingParamsCalculator.estimate_machining_time(
|
||||
toolpath_length, cutting_params["feed_rate_mm_min"]
|
||||
)
|
||||
|
||||
return {
|
||||
"strategy": "contour_finishing",
|
||||
"tool": cutting_params,
|
||||
"z_step": z_step,
|
||||
"num_levels": num_levels,
|
||||
"estimated_toolpath_length": round(toolpath_length, 1),
|
||||
"estimated_time_min": round(machining_time, 1),
|
||||
}
|
||||
|
||||
def _estimate_surface_roughness(self, tool_diameter: float,
|
||||
stepover: float) -> float:
|
||||
"""估算表面粗糙度 Ra"""
|
||||
if tool_diameter <= 0:
|
||||
return 1.0
|
||||
r = tool_diameter / 2
|
||||
h = stepover ** 2 / (8 * r) if r > 0 else stepover
|
||||
return h * 0.25
|
||||
|
||||
|
||||
class GCodePostProcessor:
|
||||
"""G代码后处理器"""
|
||||
|
||||
def __init__(self, controller: str = "fanuc"):
|
||||
self.controller = controller
|
||||
self.dialects = {
|
||||
"fanuc": {
|
||||
"rapid": "G00", "linear": "G01",
|
||||
"cw_arc": "G02", "ccw_arc": "G03",
|
||||
"absolute": "G90", "incremental": "G91",
|
||||
"tool_change": "M06", "spindle_on": "M03",
|
||||
"spindle_off": "M05", "coolant_on": "M08",
|
||||
"coolant_off": "M09", "program_end": "M30",
|
||||
"length_comp": "G43", "xy_plane": "G17",
|
||||
"cancel_comp": "G40", "cancel_canned": "G80",
|
||||
},
|
||||
"siemens": {
|
||||
"rapid": "G00", "linear": "G01",
|
||||
"cw_arc": "G02", "ccw_arc": "G03",
|
||||
"absolute": "G90", "incremental": "G91",
|
||||
"tool_change": "M06", "spindle_on": "M03",
|
||||
"spindle_off": "M05", "coolant_on": "M08",
|
||||
"coolant_off": "M09", "program_end": "M30",
|
||||
"length_comp": "G43", "xy_plane": "G17",
|
||||
"cancel_comp": "G40", "cancel_canned": "G80",
|
||||
},
|
||||
}
|
||||
|
||||
def generate_gcode(self, operations: List[Dict],
|
||||
program_number: int = 1000,
|
||||
program_name: str = "MOLD_CAVITY") -> str:
|
||||
"""
|
||||
生成完整G代码程序
|
||||
|
||||
Args:
|
||||
operations: 加工操作列表
|
||||
program_number: 程序号
|
||||
program_name: 程序名
|
||||
|
||||
Returns:
|
||||
G代码字符串
|
||||
"""
|
||||
d = self.dialects.get(self.controller, self.dialects["fanuc"])
|
||||
lines = []
|
||||
|
||||
lines.append(f"%")
|
||||
lines.append(f"O{program_number} ({program_name})")
|
||||
lines.append(f"{d['xy_plane']} {d['cancel_comp']} {d['cancel_canned']} {d['absolute']}")
|
||||
lines.append(f"G54")
|
||||
lines.append("")
|
||||
|
||||
for op_idx, op in enumerate(operations):
|
||||
strategy = op.get("strategy", "unknown")
|
||||
tool_info = op.get("tool", {})
|
||||
tool_id = tool_info.get("tool_id", "T01")
|
||||
tool_num = op_idx + 1
|
||||
|
||||
lines.append(f"(=== 操作 {tool_num}: {strategy} ===)")
|
||||
|
||||
tool_type = tool_info.get("tool_type", "endmill")
|
||||
tool_dia = tool_info.get("tool_diameter", 10)
|
||||
lines.append(f"(刀具: {tool_type} D{tool_dia:.1f}mm)")
|
||||
|
||||
lines.append(f"T{tool_num:02d} {d['tool_change']}")
|
||||
lines.append(f"{d['length_comp']} H{tool_num:02d} Z100.0")
|
||||
|
||||
rpm = tool_info.get("spindle_speed_rpm", 3000)
|
||||
lines.append(f"S{rpm} {d['spindle_on']}")
|
||||
|
||||
lines.append(f"{d['rapid']} X0 Y0 Z10.0")
|
||||
lines.append(f"{d['coolant_on']}")
|
||||
lines.append("")
|
||||
|
||||
feed = tool_info.get("feed_rate_mm_min", 500)
|
||||
levels = op.get("levels", [])
|
||||
|
||||
if strategy == "z_level_roughing" and levels:
|
||||
for level in levels:
|
||||
z = level["z"]
|
||||
lines.append(f"(--- Z层 {level['level_index']}: Z={z:.2f} ---)")
|
||||
lines.append(f"{d['linear']} Z{z:.2f} F{int(feed * 0.5)}")
|
||||
lines.append(f"{d['linear']} X50.0 Y30.0 F{feed}")
|
||||
lines.append(f"{d['linear']} X-50.0 Y30.0")
|
||||
lines.append(f"{d['linear']} X-50.0 Y-30.0")
|
||||
lines.append(f"{d['linear']} X50.0 Y-30.0")
|
||||
lines.append(f"{d['rapid']} Z10.0")
|
||||
lines.append("")
|
||||
|
||||
elif strategy in ("parallel_finishing", "contour_finishing"):
|
||||
num_passes = op.get("num_passes", 10)
|
||||
stepover = op.get("stepover", 0.3)
|
||||
|
||||
for i in range(num_passes):
|
||||
y = i * stepover - 30
|
||||
lines.append(f"{d['linear']} Z-5.0 F{int(feed * 0.3)}")
|
||||
lines.append(f"{d['linear']} X50.0 Y{y:.2f} F{feed}")
|
||||
lines.append(f"{d['linear']} X-50.0 Y{y:.2f}")
|
||||
lines.append(f"{d['rapid']} Z5.0")
|
||||
lines.append("")
|
||||
|
||||
else:
|
||||
lines.append(f"(策略 {strategy} 的刀路数据)")
|
||||
lines.append("")
|
||||
|
||||
lines.append(f"{d['coolant_off']}")
|
||||
lines.append(f"{d['spindle_off']}")
|
||||
lines.append(f"{d['rapid']} Z100.0")
|
||||
lines.append("")
|
||||
|
||||
lines.append(f"{d['coolant_off']}")
|
||||
lines.append(f"{d['spindle_off']}")
|
||||
lines.append(f"G28 G91 Z0")
|
||||
lines.append(f"G28 G91 X0 Y0")
|
||||
lines.append(f"{d['program_end']}")
|
||||
lines.append(f"%")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
class MoldCAMDesigner:
|
||||
"""模具CAM综合设计器"""
|
||||
|
||||
def __init__(self):
|
||||
self.tool_lib = ToolLibrary()
|
||||
self.params_calc = CuttingParamsCalculator()
|
||||
self.roughing_gen = RoughingToolpathGenerator()
|
||||
self.finishing_gen = FinishingToolpathGenerator()
|
||||
self.post_processor = GCodePostProcessor()
|
||||
|
||||
def design_mold_cam(self, cavity_bbox: Dict, stock_bbox: Dict,
|
||||
mold_steel: str = "P20",
|
||||
surface_quality: str = "standard",
|
||||
controller: str = "fanuc",
|
||||
program_number: int = 1000) -> Dict[str, Any]:
|
||||
"""
|
||||
综合设计模具CAM方案
|
||||
|
||||
Args:
|
||||
cavity_bbox: 型腔边界框
|
||||
stock_bbox: 毛坯边界框
|
||||
mold_steel: 模具钢材料
|
||||
surface_quality: 表面质量要求
|
||||
controller: 数控系统
|
||||
program_number: 程序号
|
||||
|
||||
Returns:
|
||||
完整的CAM方案
|
||||
"""
|
||||
logger.info(f"开始模具CAM设计: 钢材={mold_steel}, 质量={surface_quality}")
|
||||
|
||||
roughing_tool = ToolLibrary.select_roughing_tool(
|
||||
self._estimate_cavity_volume(cavity_bbox),
|
||||
steel=mold_steel
|
||||
)
|
||||
roughing_params = CuttingParamsCalculator.calculate_all(roughing_tool)
|
||||
|
||||
finishing_tool = ToolLibrary.select_finishing_tool(
|
||||
surface_quality=surface_quality,
|
||||
steel=mold_steel
|
||||
)
|
||||
finishing_params = CuttingParamsCalculator.calculate_all(finishing_tool)
|
||||
|
||||
roughing_op = self.roughing_gen.generate_z_level_roughing(
|
||||
stock_bbox, cavity_bbox, roughing_tool, roughing_params
|
||||
)
|
||||
|
||||
finishing_op = self.finishing_gen.generate_parallel_finishing(
|
||||
cavity_bbox, finishing_tool, finishing_params
|
||||
)
|
||||
|
||||
operations = [roughing_op, finishing_op]
|
||||
|
||||
gcode = self.post_processor.generate_gcode(
|
||||
operations, program_number=program_number
|
||||
)
|
||||
|
||||
total_time = (
|
||||
roughing_op.get("estimated_time_min", 0) +
|
||||
finishing_op.get("estimated_time_min", 0)
|
||||
)
|
||||
|
||||
result = {
|
||||
"operations": operations,
|
||||
"tools": {
|
||||
"roughing": roughing_params,
|
||||
"finishing": finishing_params,
|
||||
},
|
||||
"gcode": gcode,
|
||||
"gcode_lines": len(gcode.split("\n")),
|
||||
"summary": {
|
||||
"total_operations": len(operations),
|
||||
"total_estimated_time_min": round(total_time, 1),
|
||||
"mold_steel": mold_steel,
|
||||
"surface_quality": surface_quality,
|
||||
"controller": controller,
|
||||
},
|
||||
"recommendations": self._generate_cam_recommendations(
|
||||
roughing_op, finishing_op, mold_steel
|
||||
),
|
||||
}
|
||||
|
||||
logger.info(f"CAM设计完成: {len(operations)} 个工序, "
|
||||
f"预计 {total_time:.1f} 分钟")
|
||||
|
||||
return result
|
||||
|
||||
def _estimate_cavity_volume(self, cavity_bbox: Dict) -> float:
|
||||
"""估算型腔体积"""
|
||||
dims = cavity_bbox.get("dimensions", [100, 100, 50])
|
||||
return dims[0] * dims[1] * dims[2]
|
||||
|
||||
def _generate_cam_recommendations(self, roughing: Dict, finishing: Dict,
|
||||
steel: str) -> List[str]:
|
||||
"""生成CAM建议"""
|
||||
recs = []
|
||||
|
||||
roughing_time = roughing.get("estimated_time_min", 0)
|
||||
if roughing_time > 120:
|
||||
recs.append("粗加工时间较长,建议使用更大直径刀具或增加切削深度")
|
||||
|
||||
finishing_roughness = finishing.get("surface_roughness_ra", 0)
|
||||
if finishing_roughness > 0.8:
|
||||
recs.append("表面粗糙度偏高,建议减小步距或使用更小直径球头刀")
|
||||
|
||||
if steel in ("S136", "H13"):
|
||||
recs.append(f"高硬度钢材({steel}),建议使用涂层刀具并降低切削速度")
|
||||
recs.append("建议增加半精加工工序减少精加工余量")
|
||||
|
||||
recs.append("加工前需确认工件坐标系零点位置")
|
||||
recs.append("首件加工建议降低进给率20%进行试切")
|
||||
|
||||
return recs
|
||||
@@ -0,0 +1,511 @@
|
||||
from typing import Dict, List, Any, Tuple, Optional
|
||||
import numpy as np
|
||||
from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeFace
|
||||
from OCC.Core.gp import gp_Pln, gp_Dir, gp_Pnt
|
||||
from OCC.Core.TopoDS import TopoDS_Face, TopoDS_Shape, topods
|
||||
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface
|
||||
from OCC.Core.TopExp import TopExp_Explorer
|
||||
from OCC.Core.TopAbs import TopAbs_FACE
|
||||
from OCC.Core.Bnd import Bnd_Box
|
||||
from OCC.Core.BRepBndLib import brepbndlib
|
||||
|
||||
from shared.models.schemas import create_mold_cavity_data, create_mold_key_info
|
||||
from shared.utils.logger import get_logger
|
||||
from moldinsight.core.base_mold_generator import BaseMoldGenerator
|
||||
from moldinsight.core.side_action_designer import SideActionDesigner
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class MoldCavityGenerator(BaseMoldGenerator):
|
||||
"""模具型腔生成器 - 基于产品模型生成Cavity和Core"""
|
||||
|
||||
def __init__(self, shrinkage_rate: float = 0.005, draft_angle: float = 2.0,
|
||||
material_density: float = 1.05):
|
||||
super().__init__(shrinkage_rate, draft_angle, material_density)
|
||||
|
||||
self.material_densities = {
|
||||
"ABS": 1.05,
|
||||
"PP": 0.90,
|
||||
"PC": 1.20,
|
||||
"PE": 0.95,
|
||||
"PS": 1.05,
|
||||
"PA": 1.14,
|
||||
"POM": 1.42,
|
||||
"PMMA": 1.18
|
||||
}
|
||||
|
||||
self.parting_line_tolerance = 0.1
|
||||
self.max_draft_angle = 5.0
|
||||
self.side_action_designer = SideActionDesigner()
|
||||
|
||||
def set_material(self, material: str):
|
||||
"""设置产品材料"""
|
||||
if material in self.material_densities:
|
||||
self.material_density = self.material_densities[material]
|
||||
logger.info(f"材料设置为 {material}, 密度: {self.material_density} g/cm³")
|
||||
else:
|
||||
logger.warning(f"未知材料 {material}, 使用默认密度 {self.material_density} g/cm³")
|
||||
|
||||
def generate_mold_cavities(self, product_shape: TopoDS_Shape) -> Dict[str, Any]:
|
||||
"""
|
||||
从产品的3D模型生成型腔和型芯
|
||||
|
||||
Returns:
|
||||
{
|
||||
"cavity": cavity_shape,
|
||||
"core": core_shape,
|
||||
"parting_surface": parting_surface,
|
||||
"parting_line": parting_line
|
||||
}
|
||||
"""
|
||||
logger.info("开始生成模具型腔...")
|
||||
|
||||
try:
|
||||
analysis = self._analyze_product_geometry(product_shape)
|
||||
|
||||
parting_result = self._detect_primary_parting(product_shape, analysis)
|
||||
parting_surface = parting_result["surface"]
|
||||
parting_line = self.optimize_parting_line(parting_result["line"])
|
||||
parting_direction = parting_result["direction"]
|
||||
|
||||
side_action_result = self.side_action_designer.analyze_and_design(
|
||||
shape=product_shape,
|
||||
parting_direction=parting_direction,
|
||||
mold_size=self._calculate_mold_size(analysis),
|
||||
parting_surface=parting_surface,
|
||||
)
|
||||
undercut_regions = self._build_undercut_regions(
|
||||
side_action_result.get("undercut_analysis", {})
|
||||
)
|
||||
|
||||
scaled_shape = self._apply_shrinkage_compensation(product_shape)
|
||||
|
||||
drafted_shape = self._apply_draft_angles(scaled_shape, parting_surface)
|
||||
|
||||
cavity, core = self._split_cavity_core(drafted_shape, parting_surface)
|
||||
|
||||
logger.info("模具型腔生成完成")
|
||||
|
||||
return {
|
||||
"cavity": cavity,
|
||||
"core": core,
|
||||
"parting_surface": parting_surface,
|
||||
"parting_line": parting_line,
|
||||
"analysis": analysis,
|
||||
"undercut_regions": undercut_regions,
|
||||
"side_actions": side_action_result,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"模具型腔生成失败: {e}")
|
||||
raise
|
||||
|
||||
def generate_detailed_cavity_json(self, cavity_data: Dict) -> Dict[str, Any]:
|
||||
"""
|
||||
生成详细的型腔三维JSON数据
|
||||
|
||||
Returns:
|
||||
包含完整几何信息的JSON结构
|
||||
"""
|
||||
cavity = cavity_data["cavity"]
|
||||
core = cavity_data["core"]
|
||||
parting_surface = cavity_data["parting_surface"]
|
||||
analysis = cavity_data["analysis"]
|
||||
|
||||
cavity_geometry = self._extract_shape_geometry(cavity, "cavity")
|
||||
core_geometry = self._extract_shape_geometry(core, "core")
|
||||
|
||||
parting_geometry = self._extract_parting_surface_geometry(
|
||||
parting_surface
|
||||
)
|
||||
|
||||
detailed_json = {
|
||||
"metadata": {
|
||||
"version": "2.0",
|
||||
"generated_at": str(np.datetime64('now')),
|
||||
"shrinkage_rate": self.shrinkage_rate,
|
||||
"draft_angle": self.draft_angle,
|
||||
"unit": "mm"
|
||||
},
|
||||
"product_analysis": {
|
||||
"bounding_box": analysis.get("bounding_box", {}),
|
||||
"volume": analysis.get("volume", 0),
|
||||
"surface_area": analysis.get("surface_area", 0),
|
||||
"center_of_mass": analysis.get("center_of_mass", [0, 0, 0])
|
||||
},
|
||||
"mold_cavities": {
|
||||
"cavity": cavity_geometry,
|
||||
"core": core_geometry
|
||||
},
|
||||
"parting_surface": parting_geometry,
|
||||
"quality_checks": {
|
||||
"undercut_regions": cavity_data.get("undercut_regions", []),
|
||||
"side_actions": cavity_data.get("side_actions", {}),
|
||||
},
|
||||
"manufacturing_info": {
|
||||
"estimated_mold_size": self._calculate_mold_size(analysis),
|
||||
"estimated_clamping_force": self._calculate_clamping_force(analysis),
|
||||
"recommended_material": self._get_recommended_material()
|
||||
}
|
||||
}
|
||||
|
||||
return detailed_json
|
||||
|
||||
def generate_cavity_key_info(self, cavity_data: Dict) -> Dict[str, Any]:
|
||||
"""
|
||||
生成模具型腔的关键信息
|
||||
|
||||
Returns:
|
||||
关键参数摘要
|
||||
"""
|
||||
analysis = cavity_data["analysis"]
|
||||
|
||||
key_info = {
|
||||
"mold_parameters": {
|
||||
"shrinkage_rate": f"{self.shrinkage_rate * 100:.2f}%",
|
||||
"draft_angle": f"{self.draft_angle}°",
|
||||
"parting_line_length": self._calculate_parting_line_length(
|
||||
cavity_data["parting_line"]
|
||||
),
|
||||
"cavity_depth": analysis.get("bounding_box", {}).get("dimensions", [0, 0, 0])[2]
|
||||
},
|
||||
"geometric_characteristics": {
|
||||
"product_volume": f"{analysis.get('volume', 0) / 1000:.2f} cm³",
|
||||
"product_weight": self._calculate_product_weight(analysis),
|
||||
"wall_thickness_range": self._estimate_wall_thickness(analysis),
|
||||
"complexity_score": self._calculate_complexity_score(analysis)
|
||||
},
|
||||
"manufacturing_requirements": {
|
||||
"cavity_material": "Aluminum Alloy 7075",
|
||||
"hardness": "HRC 30-35",
|
||||
"surface_finish": "SPI A2",
|
||||
"estimated_cycle_time": self._estimate_cycle_time(analysis),
|
||||
"recommended_injection_pressure": "80-120 MPa"
|
||||
},
|
||||
"quality_considerations": {
|
||||
"undercut_count": len(cavity_data.get("undercut_regions", [])),
|
||||
"side_action_summary": cavity_data.get("side_actions", {}).get("summary", {}),
|
||||
"potential_weld_lines": self._identify_weld_line_risk(analysis),
|
||||
"sink_mark_areas": self._identify_sink_mark_risk(analysis),
|
||||
"warpage_risk": self._assess_warpage_risk(analysis)
|
||||
}
|
||||
}
|
||||
|
||||
return key_info
|
||||
|
||||
# ==================== 内部方法 ====================
|
||||
|
||||
def _detect_parting_surface(self, shape: TopoDS_Shape, analysis: Dict) -> Tuple[TopoDS_Face, List]:
|
||||
"""
|
||||
检测分型面和分型线
|
||||
|
||||
优先级:
|
||||
1. AI 模型检测(如果已设置)
|
||||
2. 基于法向量分析的几何方法
|
||||
3. 简化方法(基于边界框)
|
||||
"""
|
||||
try:
|
||||
parting_result = self._detect_primary_parting(shape, analysis)
|
||||
logger.info(
|
||||
f"使用 {parting_result['method']} 方法检测分型面,"
|
||||
f"置信度={parting_result['confidence']:.3f}"
|
||||
)
|
||||
return parting_result["surface"], self.optimize_parting_line(parting_result["line"])
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"法向量分析失败,使用简化方法:{e}")
|
||||
|
||||
logger.info("使用简化方法检测分型面")
|
||||
return self._simple_parting_surface(shape, analysis)
|
||||
|
||||
def _detect_primary_parting(self, shape: TopoDS_Shape, analysis: Dict) -> Dict[str, Any]:
|
||||
"""检测主分型面(AI优先 → 几何法向量 → 简化回退)"""
|
||||
if self.ai_parting_detector is not None:
|
||||
try:
|
||||
ai_result = self.ai_parting_detector.detect(shape, analysis)
|
||||
if ai_result is not None:
|
||||
surface, line = self._create_parting_surface_from_ai(ai_result, analysis, shape)
|
||||
return {
|
||||
"surface": surface,
|
||||
"line": line,
|
||||
"direction": ai_result.get("normal", [0, 0, 1]),
|
||||
"method": ai_result.get("method", "ai"),
|
||||
"confidence": ai_result.get("confidence", 0.8),
|
||||
}
|
||||
except Exception as e:
|
||||
logger.warning(f"AI 分型面检测失败: {e}")
|
||||
|
||||
try:
|
||||
normal_dir = self._analyze_face_normals(shape)
|
||||
parting_plane = self._create_optimal_parting_plane(shape, analysis, normal_dir)
|
||||
dims = analysis.get("bounding_box", {}).get("dimensions", [100, 100, 100])
|
||||
span = max(dims) * 1.5 + 30
|
||||
parting_surface = BRepBuilderAPI_MakeFace(
|
||||
parting_plane, -span, span, -span, span
|
||||
).Face()
|
||||
parting_surface = self.extend_parting_surface(parting_surface, shape, extension=30.0)
|
||||
parting_line = self._calculate_parting_line(shape, parting_surface)
|
||||
return {
|
||||
"surface": parting_surface,
|
||||
"line": parting_line,
|
||||
"direction": [float(normal_dir.X()), float(normal_dir.Y()), float(normal_dir.Z())],
|
||||
"method": "face_normal_analysis",
|
||||
"confidence": 0.85,
|
||||
}
|
||||
except Exception as e:
|
||||
logger.warning(f"法向量分析失败,使用简化方法:{e}")
|
||||
|
||||
surface, line = self._simple_parting_surface(shape, analysis)
|
||||
return {
|
||||
"surface": surface,
|
||||
"line": line,
|
||||
"direction": [0, 0, 1],
|
||||
"method": "simple",
|
||||
"confidence": 0.6,
|
||||
}
|
||||
|
||||
def _build_undercut_regions(self, undercut_analysis: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
"""将侧向机构分析结果转换为兼容旧结构的倒扣区域列表。"""
|
||||
undercut_faces = undercut_analysis.get("undercut_faces", [])
|
||||
regions = []
|
||||
|
||||
for face in undercut_faces:
|
||||
regions.append({
|
||||
"type": "negative_draft",
|
||||
"location": face.get("center", [0, 0, 0]),
|
||||
"severity": face.get("severity", "medium"),
|
||||
"area": face.get("area", 0),
|
||||
"is_outer": face.get("is_outer", False),
|
||||
"face_index": face.get("face_index"),
|
||||
})
|
||||
|
||||
logger.info(f"转换得到 {len(regions)} 个兼容倒扣区域")
|
||||
return regions
|
||||
|
||||
def _analyze_face_normals(self, shape: TopoDS_Shape) -> gp_Dir:
|
||||
"""
|
||||
分析产品表面的法向量分布,找出最优分型方向
|
||||
|
||||
原理:
|
||||
- 统计所有面的法向量
|
||||
- 选择法向量变化最小的方向作为分型方向
|
||||
- 避免倒扣(undercut)区域
|
||||
"""
|
||||
face_normals = []
|
||||
explorer = TopExp_Explorer(shape, TopAbs_FACE)
|
||||
|
||||
while explorer.More():
|
||||
face = topods.Face(explorer.Current())
|
||||
surface = BRepAdaptor_Surface(face)
|
||||
|
||||
try:
|
||||
if surface.GetType() == 0:
|
||||
normal = surface.Plane().Position().Direction()
|
||||
else:
|
||||
bbox = Bnd_Box()
|
||||
brepbndlib.Add(face, bbox)
|
||||
normal = gp_Dir(0, 0, 1)
|
||||
|
||||
face_normals.append(normal)
|
||||
except Exception as e:
|
||||
logger.debug(f"面法向量计算失败:{e}")
|
||||
|
||||
explorer.Next()
|
||||
|
||||
if not face_normals:
|
||||
return gp_Dir(0, 0, 1)
|
||||
|
||||
avg_x = sum(n.X() for n in face_normals) / len(face_normals)
|
||||
avg_y = sum(n.Y() for n in face_normals) / len(face_normals)
|
||||
avg_z = sum(n.Z() for n in face_normals) / len(face_normals)
|
||||
|
||||
length = np.sqrt(avg_x**2 + avg_y**2 + avg_z**2)
|
||||
if length > 0.001:
|
||||
return gp_Dir(avg_x/length, avg_y/length, avg_z/length)
|
||||
else:
|
||||
return gp_Dir(0, 0, 1)
|
||||
|
||||
def _create_optimal_parting_plane(self, shape: TopoDS_Shape, analysis: Dict,
|
||||
direction: gp_Dir) -> gp_Pln:
|
||||
"""
|
||||
创建最优分型面
|
||||
|
||||
Args:
|
||||
shape: 产品形状
|
||||
analysis: 几何分析结果
|
||||
direction: 分型方向(法向量)
|
||||
|
||||
Returns:
|
||||
gp_Pln: 分型面方程
|
||||
"""
|
||||
bbox = analysis["bounding_box"]
|
||||
center = bbox["center"]
|
||||
|
||||
parting_plane = gp_Pln(
|
||||
gp_Pnt(center[0], center[1], center[2]),
|
||||
direction
|
||||
)
|
||||
|
||||
logger.info(f"创建分型面:原点=({center[0]:.2f}, {center[1]:.2f}, {center[2]:.2f}), "
|
||||
f"法向量=({direction.X():.3f}, {direction.Y():.3f}, {direction.Z():.3f})")
|
||||
|
||||
return parting_plane
|
||||
|
||||
def _simple_parting_surface(self, shape: TopoDS_Shape, analysis: Dict) -> Tuple[TopoDS_Face, List]:
|
||||
"""简化的分型面检测(回退方案)"""
|
||||
bbox = analysis["bounding_box"]
|
||||
center_z = bbox["center"][2]
|
||||
|
||||
parting_plane = gp_Pln(
|
||||
gp_Pnt(0, 0, center_z),
|
||||
gp_Dir(0, 0, 1)
|
||||
)
|
||||
parting_surface = BRepBuilderAPI_MakeFace(
|
||||
parting_plane,
|
||||
bbox["min"][0] - 10, bbox["max"][0] + 10,
|
||||
bbox["min"][1] - 10, bbox["max"][1] + 10
|
||||
).Face()
|
||||
|
||||
parting_line = self._simple_parting_line(shape)
|
||||
|
||||
return parting_surface, parting_line
|
||||
|
||||
def _create_parting_surface_from_ai(self, ai_result: Dict,
|
||||
analysis: Dict, shape: Optional[TopoDS_Shape] = None) -> Tuple[TopoDS_Face, List]:
|
||||
"""
|
||||
从 AI 模型结果创建分型面(预留接口)
|
||||
|
||||
Args:
|
||||
ai_result: AI 模型输出,应包含:
|
||||
- origin: [x, y, z] 平面原点
|
||||
- normal: [nx, ny, nz] 法向量
|
||||
analysis: 几何分析结果
|
||||
shape: 产品形状(用于计算分型线)
|
||||
|
||||
Returns:
|
||||
(parting_surface, parting_line)
|
||||
"""
|
||||
origin = ai_result.get("origin", [0, 0, 0])
|
||||
normal = ai_result.get("normal", [0, 0, 1])
|
||||
|
||||
parting_plane = gp_Pln(
|
||||
gp_Pnt(origin[0], origin[1], origin[2]),
|
||||
gp_Dir(normal[0], normal[1], normal[2])
|
||||
)
|
||||
parting_surface = BRepBuilderAPI_MakeFace(parting_plane).Face()
|
||||
|
||||
if "parting_line" in ai_result:
|
||||
parting_line = ai_result["parting_line"]
|
||||
elif shape is not None:
|
||||
parting_line = self._calculate_parting_line(shape, parting_surface)
|
||||
else:
|
||||
parting_line = []
|
||||
|
||||
logger.info(f"从 AI 结果创建分型面:原点={origin}, 法向量={normal}")
|
||||
return parting_surface, parting_line
|
||||
|
||||
def _extract_parting_surface_geometry(self, surface: TopoDS_Face) -> Dict[str, Any]:
|
||||
"""提取分型面几何数据"""
|
||||
metadata = self._extract_plane_metadata(surface)
|
||||
|
||||
return {
|
||||
"type": "plane",
|
||||
"normal": metadata["normal"],
|
||||
"origin": metadata["origin"],
|
||||
"bounds": metadata["bounds"],
|
||||
}
|
||||
|
||||
def _calculate_mold_size(self, analysis: Dict) -> Dict[str, float]:
|
||||
"""估算模具尺寸"""
|
||||
product_bbox = analysis["bounding_box"]["dimensions"]
|
||||
margin = 30
|
||||
|
||||
return {
|
||||
"length": product_bbox[0] + 2 * margin,
|
||||
"width": product_bbox[1] + 2 * margin,
|
||||
"height": product_bbox[2] + 2 * margin + 100,
|
||||
"margin": margin
|
||||
}
|
||||
|
||||
def _calculate_clamping_force(self, analysis: Dict) -> str:
|
||||
"""估算锁模力"""
|
||||
volume_cm3 = analysis.get("volume", 0) / 1000
|
||||
|
||||
if volume_cm3 < 10:
|
||||
return "50-100 吨"
|
||||
elif volume_cm3 < 100:
|
||||
return "150-300 吨"
|
||||
elif volume_cm3 < 500:
|
||||
return "400-600 吨"
|
||||
else:
|
||||
return "800+ 吨"
|
||||
|
||||
def _get_recommended_material(self) -> str:
|
||||
"""推荐模具材料"""
|
||||
return "Aluminum Alloy 7075 (铝合金模具)"
|
||||
|
||||
def _estimate_wall_thickness(self, analysis: Dict) -> str:
|
||||
"""估算壁厚范围"""
|
||||
volume = analysis.get("volume", 0)
|
||||
surface_area = analysis.get("surface_area", 0)
|
||||
|
||||
if surface_area > 0 and volume > 0:
|
||||
avg_thickness = (volume / surface_area) * 0.6
|
||||
return f"{avg_thickness * 0.7:.2f} - {avg_thickness * 1.3:.2f} mm"
|
||||
elif volume > 0:
|
||||
bbox_dims = analysis.get("bounding_box", {}).get("dimensions", [1, 1, 1])
|
||||
bbox_volume = bbox_dims[0] * bbox_dims[1] * bbox_dims[2]
|
||||
if bbox_volume > 0:
|
||||
efficiency = volume / bbox_volume
|
||||
avg_thickness = (bbox_dims[0] + bbox_dims[1]) / 2 * efficiency
|
||||
return f"{avg_thickness * 0.7:.2f} - {avg_thickness * 1.3:.2f} mm"
|
||||
|
||||
return "2.0 - 4.0 mm (默认)"
|
||||
|
||||
def _calculate_complexity_score(self, analysis: Dict) -> float:
|
||||
"""计算复杂度评分(0-10)"""
|
||||
volume = analysis.get("volume", 0)
|
||||
surface_area = analysis.get("surface_area", 0)
|
||||
|
||||
if surface_area > 0 and volume > 0:
|
||||
thickness_ratio = (volume / surface_area) * 0.6
|
||||
complexity = min(thickness_ratio / 5.0, 10.0)
|
||||
return round(complexity, 1)
|
||||
elif volume > 0:
|
||||
bbox_dims = analysis.get("bounding_box", {}).get("dimensions", [100, 100, 100])
|
||||
bbox_volume = bbox_dims[0] * bbox_dims[1] * bbox_dims[2]
|
||||
if bbox_volume > 0:
|
||||
volume_ratio = volume / bbox_volume
|
||||
complexity = (1.0 - volume_ratio) * 10
|
||||
return round(min(max(complexity, 0), 10), 1)
|
||||
|
||||
return 5.0
|
||||
|
||||
def _estimate_cycle_time(self, analysis: Dict) -> str:
|
||||
"""估算成型周期"""
|
||||
volume_cm3 = analysis.get("volume", 0) / 1000
|
||||
|
||||
if volume_cm3 < 10:
|
||||
return "15-25 秒"
|
||||
elif volume_cm3 < 50:
|
||||
return "25-40 秒"
|
||||
elif volume_cm3 < 200:
|
||||
return "40-60 秒"
|
||||
else:
|
||||
return "60-90 秒"
|
||||
|
||||
def _identify_weld_line_risk(self, analysis: Dict) -> str:
|
||||
"""识别熔接痕风险"""
|
||||
complexity = self._calculate_complexity_score(analysis)
|
||||
|
||||
if complexity > 7:
|
||||
return "高 - 建议优化浇口位置"
|
||||
elif complexity > 4:
|
||||
return "中 - 需仿真验证"
|
||||
else:
|
||||
return "低"
|
||||
|
||||
def _identify_sink_mark_risk(self, analysis: Dict) -> str:
|
||||
"""识别缩痕风险"""
|
||||
return "中 - 建议壁厚均匀性检查"
|
||||
@@ -0,0 +1,762 @@
|
||||
"""
|
||||
模具加工碰撞检测与刀路优化模块
|
||||
|
||||
功能:
|
||||
1. CollisionDetector - 碰撞检测器
|
||||
- 刀柄干涉检测
|
||||
- 快速移动碰撞检测
|
||||
- 机床行程限制验证
|
||||
- 安全区域计算
|
||||
|
||||
2. ToolpathOptimizer - 刀路优化器
|
||||
- 进给率自适应优化
|
||||
- 空走刀路径最小化
|
||||
- 拐角减速处理
|
||||
- 切入切出优化
|
||||
|
||||
3. EDMElectrodeDesigner - EDM电极设计器
|
||||
- 电极自动生成
|
||||
- 放电间隙计算
|
||||
- 电极加工路径
|
||||
|
||||
4. MachiningSimulator - 加工仿真器
|
||||
- 材料去除模拟
|
||||
- 过切检测
|
||||
- 残余材料分析
|
||||
- 加工质量评估
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
import math
|
||||
import numpy as np
|
||||
from shared.utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class CollisionDetector:
|
||||
"""碰撞检测器"""
|
||||
|
||||
def __init__(self):
|
||||
self.machine_limits = {
|
||||
"x_min": -500, "x_max": 500,
|
||||
"y_min": -400, "y_max": 400,
|
||||
"z_min": -300, "z_max": 300,
|
||||
}
|
||||
self.safety_margin = 5.0
|
||||
self.retract_height = 50.0
|
||||
|
||||
def check_toolpath_safety(self, toolpath_points: List[List[float]],
|
||||
tool: Dict, stock_bbox: Dict,
|
||||
clamp_positions: Optional[List[Dict]] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
综合检查刀路安全性
|
||||
|
||||
Args:
|
||||
toolpath_points: 刀路点列表 [[x,y,z], ...]
|
||||
tool: 刀具参数
|
||||
stock_bbox: 毛坯边界框
|
||||
clamp_positions: 压板位置列表
|
||||
|
||||
Returns:
|
||||
安全检查结果
|
||||
"""
|
||||
holder_collisions = self._check_holder_collision(toolpath_points, tool, stock_bbox)
|
||||
|
||||
rapid_collisions = self._check_rapid_move_collisions(toolpath_points, stock_bbox)
|
||||
|
||||
limit_violations = self._check_machine_limits(toolpath_points)
|
||||
|
||||
clamp_collisions = []
|
||||
if clamp_positions:
|
||||
clamp_collisions = self._check_clamp_collisions(
|
||||
toolpath_points, tool, clamp_positions
|
||||
)
|
||||
|
||||
all_issues = holder_collisions + rapid_collisions + limit_violations + clamp_collisions
|
||||
|
||||
safe_retract_points = self._calculate_safe_retract_points(
|
||||
toolpath_points, stock_bbox
|
||||
)
|
||||
|
||||
is_safe = len(all_issues) == 0
|
||||
|
||||
return {
|
||||
"is_safe": is_safe,
|
||||
"total_issues": len(all_issues),
|
||||
"holder_collisions": holder_collisions,
|
||||
"rapid_collisions": rapid_collisions,
|
||||
"limit_violations": limit_violations,
|
||||
"clamp_collisions": clamp_collisions,
|
||||
"safe_retract_points": safe_retract_points,
|
||||
"recommendations": self._generate_safety_recommendations(all_issues),
|
||||
}
|
||||
|
||||
def _check_holder_collision(self, points: List[List[float]],
|
||||
tool: Dict, stock_bbox: Dict) -> List[Dict]:
|
||||
"""检测刀柄干涉"""
|
||||
collisions = []
|
||||
tool_diameter = tool.get("diameter", 10)
|
||||
flute_length = tool.get("flute_length", 30)
|
||||
shank_diameter = tool.get("shank_diameter", tool_diameter)
|
||||
holder_diameter = tool.get("holder_diameter", shank_diameter * 2)
|
||||
|
||||
stock_z_max = stock_bbox.get("max", [0, 0, 0])[2]
|
||||
|
||||
for i, pt in enumerate(points):
|
||||
if len(pt) < 3:
|
||||
continue
|
||||
|
||||
z = pt[2]
|
||||
depth_below_stock = stock_z_max - z
|
||||
|
||||
if depth_below_stock > flute_length:
|
||||
holder_z = z + flute_length
|
||||
holder_clearance = holder_diameter / 2 + self.safety_margin
|
||||
|
||||
stock_xmin = stock_bbox.get("min", [0, 0, 0])[0]
|
||||
stock_xmax = stock_bbox.get("max", [0, 0, 0])[0]
|
||||
stock_ymin = stock_bbox.get("min", [0, 0, 0])[1]
|
||||
stock_ymax = stock_bbox.get("max", [0, 0, 0])[1]
|
||||
|
||||
if (stock_xmin - holder_clearance < pt[0] < stock_xmax + holder_clearance and
|
||||
stock_ymin - holder_clearance < pt[1] < stock_ymax + holder_clearance):
|
||||
collisions.append({
|
||||
"type": "holder_collision",
|
||||
"point_index": i,
|
||||
"position": pt,
|
||||
"depth": round(depth_below_stock, 2),
|
||||
"flute_length": flute_length,
|
||||
"severity": "high",
|
||||
"message": f"点{i}: 切深{depth_below_stock:.1f}mm超过刃长{flute_length}mm,刀柄可能干涉"
|
||||
})
|
||||
|
||||
return collisions
|
||||
|
||||
def _check_rapid_move_collisions(self, points: List[List[float]],
|
||||
stock_bbox: Dict) -> List[Dict]:
|
||||
"""检测快速移动碰撞"""
|
||||
collisions = []
|
||||
|
||||
stock_xmin = stock_bbox.get("min", [0, 0, 0])[0]
|
||||
stock_xmax = stock_bbox.get("max", [0, 0, 0])[0]
|
||||
stock_ymin = stock_bbox.get("min", [0, 0, 0])[1]
|
||||
stock_ymax = stock_bbox.get("max", [0, 0, 0])[1]
|
||||
stock_zmin = stock_bbox.get("min", [0, 0, 0])[2]
|
||||
stock_zmax = stock_bbox.get("max", [0, 0, 0])[2]
|
||||
|
||||
for i in range(1, len(points)):
|
||||
prev = points[i - 1]
|
||||
curr = points[i]
|
||||
|
||||
if len(prev) < 3 or len(curr) < 3:
|
||||
continue
|
||||
|
||||
z_change = abs(curr[2] - prev[2])
|
||||
xy_change = math.sqrt((curr[0] - prev[0])**2 + (curr[1] - prev[1])**2)
|
||||
|
||||
if z_change < 1.0 and xy_change > 5.0:
|
||||
min_z = min(prev[2], curr[2])
|
||||
|
||||
if min_z < stock_zmax + self.safety_margin:
|
||||
mid_x = (prev[0] + curr[0]) / 2
|
||||
mid_y = (prev[1] + curr[1]) / 2
|
||||
|
||||
if (stock_xmin < mid_x < stock_xmax and
|
||||
stock_ymin < mid_y < stock_ymax):
|
||||
collisions.append({
|
||||
"type": "rapid_collision",
|
||||
"segment": [i - 1, i],
|
||||
"start": prev,
|
||||
"end": curr,
|
||||
"severity": "high",
|
||||
"message": f"段{i-1}-{i}: 水平快速移动可能穿过毛坯"
|
||||
})
|
||||
|
||||
return collisions
|
||||
|
||||
def _check_machine_limits(self, points: List[List[float]]) -> List[Dict]:
|
||||
"""验证机床行程限制"""
|
||||
violations = []
|
||||
|
||||
for i, pt in enumerate(points):
|
||||
if len(pt) < 3:
|
||||
continue
|
||||
|
||||
if not (self.machine_limits["x_min"] <= pt[0] <= self.machine_limits["x_max"]):
|
||||
violations.append({
|
||||
"type": "machine_limit",
|
||||
"point_index": i,
|
||||
"axis": "X",
|
||||
"value": pt[0],
|
||||
"limit": [self.machine_limits["x_min"], self.machine_limits["x_max"]],
|
||||
"severity": "critical",
|
||||
})
|
||||
if not (self.machine_limits["y_min"] <= pt[1] <= self.machine_limits["y_max"]):
|
||||
violations.append({
|
||||
"type": "machine_limit",
|
||||
"point_index": i,
|
||||
"axis": "Y",
|
||||
"value": pt[1],
|
||||
"limit": [self.machine_limits["y_min"], self.machine_limits["y_max"]],
|
||||
"severity": "critical",
|
||||
})
|
||||
if not (self.machine_limits["z_min"] <= pt[2] <= self.machine_limits["z_max"]):
|
||||
violations.append({
|
||||
"type": "machine_limit",
|
||||
"point_index": i,
|
||||
"axis": "Z",
|
||||
"value": pt[2],
|
||||
"limit": [self.machine_limits["z_min"], self.machine_limits["z_max"]],
|
||||
"severity": "critical",
|
||||
})
|
||||
|
||||
return violations
|
||||
|
||||
def _check_clamp_collisions(self, points: List[List[float]], tool: Dict,
|
||||
clamps: List[Dict]) -> List[Dict]:
|
||||
"""检测压板碰撞"""
|
||||
collisions = []
|
||||
tool_radius = tool.get("diameter", 10) / 2
|
||||
|
||||
for i, pt in enumerate(points):
|
||||
if len(pt) < 3:
|
||||
continue
|
||||
|
||||
for j, clamp in enumerate(clamps):
|
||||
clamp_center = clamp.get("center", [0, 0, 0])
|
||||
clamp_size = clamp.get("size", [50, 30, 20])
|
||||
clamp_z_top = clamp_center[2] + clamp_size[2] / 2
|
||||
|
||||
if pt[2] < clamp_z_top + self.safety_margin:
|
||||
dx = abs(pt[0] - clamp_center[0])
|
||||
dy = abs(pt[1] - clamp_center[1])
|
||||
|
||||
if (dx < clamp_size[0] / 2 + tool_radius + self.safety_margin and
|
||||
dy < clamp_size[1] / 2 + tool_radius + self.safety_margin):
|
||||
collisions.append({
|
||||
"type": "clamp_collision",
|
||||
"point_index": i,
|
||||
"clamp_index": j,
|
||||
"severity": "high",
|
||||
"message": f"点{i}: 可能与压板{j}碰撞"
|
||||
})
|
||||
|
||||
return collisions
|
||||
|
||||
def _calculate_safe_retract_points(self, points: List[List[float]],
|
||||
stock_bbox: Dict) -> List[Dict]:
|
||||
"""计算安全抬刀点"""
|
||||
retract_points = []
|
||||
stock_zmax = stock_bbox.get("max", [0, 0, 0])[2]
|
||||
safe_z = stock_zmax + self.retract_height
|
||||
|
||||
for i in range(0, len(points), max(1, len(points) // 10)):
|
||||
pt = points[i]
|
||||
if len(pt) >= 3:
|
||||
retract_points.append({
|
||||
"index": i,
|
||||
"from": pt,
|
||||
"retract_to": [pt[0], pt[1], safe_z],
|
||||
"safe_z": safe_z,
|
||||
})
|
||||
|
||||
return retract_points
|
||||
|
||||
def _generate_safety_recommendations(self, issues: List[Dict]) -> List[str]:
|
||||
"""生成安全建议"""
|
||||
recs = []
|
||||
|
||||
holder_issues = [i for i in issues if i["type"] == "holder_collision"]
|
||||
if holder_issues:
|
||||
recs.append(f"发现 {len(holder_issues)} 处刀柄干涉,建议加长刀具或减少切深")
|
||||
|
||||
rapid_issues = [i for i in issues if i["type"] == "rapid_collision"]
|
||||
if rapid_issues:
|
||||
recs.append(f"发现 {len(rapid_issues)} 处快速移动碰撞风险,建议增加抬刀高度")
|
||||
|
||||
limit_issues = [i for i in issues if i["type"] == "machine_limit"]
|
||||
if limit_issues:
|
||||
recs.append(f"发现 {len(limit_issues)} 处超出机床行程,需调整工件位置")
|
||||
|
||||
clamp_issues = [i for i in issues if i["type"] == "clamp_collision"]
|
||||
if clamp_issues:
|
||||
recs.append(f"发现 {len(clamp_issues)} 处压板碰撞,建议调整压板位置")
|
||||
|
||||
if not issues:
|
||||
recs.append("刀路安全检查通过,无碰撞风险")
|
||||
|
||||
return recs
|
||||
|
||||
|
||||
class ToolpathOptimizer:
|
||||
"""刀路优化器"""
|
||||
|
||||
def optimize_toolpath(self, toolpath_points: List[List[float]],
|
||||
cutting_params: Dict,
|
||||
stock_bbox: Optional[Dict] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
综合优化刀路
|
||||
|
||||
优化内容:
|
||||
1. 进给率自适应优化
|
||||
2. 拐角减速处理
|
||||
3. 空走刀路径优化
|
||||
4. 切入切出优化
|
||||
|
||||
Args:
|
||||
toolpath_points: 原始刀路点
|
||||
cutting_params: 切削参数
|
||||
stock_bbox: 毛坯边界框
|
||||
|
||||
Returns:
|
||||
优化后的刀路和参数
|
||||
"""
|
||||
feed_optimized = self._optimize_feed_rates(toolpath_points, cutting_params)
|
||||
|
||||
corner_optimized = self._optimize_corner_speeds(toolpath_points, feed_optimized)
|
||||
|
||||
entry_exit_optimized = self._optimize_entry_exit(toolpath_points, stock_bbox)
|
||||
|
||||
stats = self._calculate_optimization_stats(
|
||||
toolpath_points, feed_optimized, corner_optimized
|
||||
)
|
||||
|
||||
return {
|
||||
"original_point_count": len(toolpath_points),
|
||||
"optimized_feeds": feed_optimized,
|
||||
"corner_slowdowns": corner_optimized,
|
||||
"entry_exit": entry_exit_optimized,
|
||||
"stats": stats,
|
||||
"recommendations": self._generate_optimization_recommendations(stats),
|
||||
}
|
||||
|
||||
def _optimize_feed_rates(self, points: List[List[float]],
|
||||
params: Dict) -> List[Dict]:
|
||||
"""进给率自适应优化"""
|
||||
base_feed = params.get("feed_rate_mm_min", 500)
|
||||
optimized = []
|
||||
|
||||
for i in range(len(points)):
|
||||
if i < 2 or i >= len(points) - 2:
|
||||
feed = base_feed * 0.8
|
||||
else:
|
||||
v1 = np.array(points[i]) - np.array(points[i - 1])
|
||||
v2 = np.array(points[i + 1]) - np.array(points[i])
|
||||
|
||||
len1 = np.linalg.norm(v1)
|
||||
len2 = np.linalg.norm(v2)
|
||||
|
||||
if len1 > 0.001 and len2 > 0.001:
|
||||
cos_angle = np.clip(np.dot(v1, v2) / (len1 * len2), -1, 1)
|
||||
angle = math.degrees(math.acos(cos_angle))
|
||||
|
||||
if angle < 30:
|
||||
feed = base_feed * 0.3
|
||||
elif angle < 60:
|
||||
feed = base_feed * 0.5
|
||||
elif angle < 120:
|
||||
feed = base_feed * 0.7
|
||||
else:
|
||||
feed = base_feed
|
||||
else:
|
||||
feed = base_feed
|
||||
|
||||
optimized.append({
|
||||
"index": i,
|
||||
"feed_rate": round(feed, 1),
|
||||
"feed_ratio": round(feed / base_feed, 2),
|
||||
})
|
||||
|
||||
return optimized
|
||||
|
||||
def _optimize_corner_speeds(self, points: List[List[float]],
|
||||
feed_data: List[Dict]) -> List[Dict]:
|
||||
"""拐角减速处理"""
|
||||
slowdowns = []
|
||||
base_feed = 500
|
||||
|
||||
for i in range(1, len(points) - 1):
|
||||
if i >= len(feed_data):
|
||||
break
|
||||
|
||||
v1 = np.array(points[i]) - np.array(points[i - 1])
|
||||
v2 = np.array(points[i + 1]) - np.array(points[i])
|
||||
|
||||
len1 = np.linalg.norm(v1)
|
||||
len2 = np.linalg.norm(v2)
|
||||
|
||||
if len1 > 0.001 and len2 > 0.001:
|
||||
cos_angle = np.clip(np.dot(v1, v2) / (len1 * len2), -1, 1)
|
||||
angle = math.degrees(math.acos(cos_angle))
|
||||
|
||||
if angle < 90:
|
||||
decel_distance = max(2.0, 10.0 * (1 - angle / 90))
|
||||
slowdowns.append({
|
||||
"index": i,
|
||||
"angle": round(angle, 1),
|
||||
"decel_distance": round(decel_distance, 2),
|
||||
"min_feed_ratio": 0.3 if angle < 45 else 0.5,
|
||||
})
|
||||
|
||||
return slowdowns
|
||||
|
||||
def _optimize_entry_exit(self, points: List[List[float]],
|
||||
stock_bbox: Optional[Dict]) -> Dict[str, Any]:
|
||||
"""切入切出优化"""
|
||||
entry = {"type": "arc_tangent", "radius": 5.0, "angle": 90}
|
||||
exit_ = {"type": "arc_tangent", "radius": 5.0, "angle": 90}
|
||||
|
||||
if stock_bbox:
|
||||
z_max = stock_bbox.get("max", [0, 0, 0])[2]
|
||||
entry["approach_z"] = z_max + 10
|
||||
exit_["retract_z"] = z_max + 50
|
||||
|
||||
return {"entry": entry, "exit": exit_}
|
||||
|
||||
def _calculate_optimization_stats(self, points: List, feeds: List,
|
||||
corners: List) -> Dict:
|
||||
"""计算优化统计"""
|
||||
if not feeds:
|
||||
return {"time_reduction_percent": 0}
|
||||
|
||||
feed_values = [f["feed_rate"] for f in feeds]
|
||||
avg_feed = sum(feed_values) / len(feed_values) if feed_values else 500
|
||||
base_feed = max(feed_values) if feed_values else 500
|
||||
|
||||
time_reduction = 0
|
||||
if base_feed > 0:
|
||||
time_reduction = (1 - avg_feed / base_feed) * 100
|
||||
|
||||
return {
|
||||
"avg_feed_rate": round(avg_feed, 1),
|
||||
"base_feed_rate": base_feed,
|
||||
"corner_slowdown_count": len(corners),
|
||||
"time_reduction_percent": round(abs(time_reduction), 1),
|
||||
}
|
||||
|
||||
def _generate_optimization_recommendations(self, stats: Dict) -> List[str]:
|
||||
"""生成优化建议"""
|
||||
recs = []
|
||||
|
||||
if stats.get("corner_slowdown_count", 0) > 10:
|
||||
recs.append("拐角减速点较多,建议优化刀路方向减少急转弯")
|
||||
|
||||
if stats.get("time_reduction_percent", 0) > 30:
|
||||
recs.append("进给率降低幅度较大,建议优化加工策略")
|
||||
|
||||
if not recs:
|
||||
recs.append("刀路优化完成,进给率分布合理")
|
||||
|
||||
return recs
|
||||
|
||||
|
||||
class EDMElectrodeDesigner:
|
||||
"""EDM电极设计器"""
|
||||
|
||||
ELECTRODE_MATERIALS = {
|
||||
"copper": {
|
||||
"density": 8.96, "wear_rate": 1.0,
|
||||
"machinability": "good", "cost": "medium"
|
||||
},
|
||||
"graphite": {
|
||||
"density": 1.75, "wear_rate": 0.5,
|
||||
"machinability": "excellent", "cost": "low"
|
||||
},
|
||||
"copper_tungsten": {
|
||||
"density": 14.0, "wear_rate": 0.3,
|
||||
"machinability": "poor", "cost": "high"
|
||||
},
|
||||
}
|
||||
|
||||
def design_electrodes(self, undercut_regions: List[Dict],
|
||||
cavity_bbox: Dict,
|
||||
material: str = "copper",
|
||||
spark_gap: float = 0.05,
|
||||
overburn: float = 0.1) -> Dict[str, Any]:
|
||||
"""
|
||||
设计EDM电极
|
||||
|
||||
Args:
|
||||
undercut_regions: 倒扣区域列表
|
||||
cavity_bbox: 型腔边界框
|
||||
material: 电极材料
|
||||
spark_gap: 放电间隙 mm
|
||||
overburn: 过切量 mm
|
||||
|
||||
Returns:
|
||||
电极设计方案
|
||||
"""
|
||||
mat_props = self.ELECTRODE_MATERIALS.get(material, self.ELECTRODE_MATERIALS["copper"])
|
||||
|
||||
electrodes = []
|
||||
for i, region in enumerate(undercut_regions):
|
||||
electrode = self._design_single_electrode(
|
||||
region, i + 1, material, spark_gap, overburn, cavity_bbox
|
||||
)
|
||||
electrodes.append(electrode)
|
||||
|
||||
total_volume = sum(e["volume_mm3"] for e in electrodes)
|
||||
total_weight = total_volume * mat_props["density"] / 1000
|
||||
|
||||
return {
|
||||
"electrodes": electrodes,
|
||||
"material": material,
|
||||
"material_properties": mat_props,
|
||||
"spark_gap": spark_gap,
|
||||
"overburn": overburn,
|
||||
"total_electrode_count": len(electrodes),
|
||||
"total_volume_cm3": round(total_volume / 1000, 2),
|
||||
"total_weight_g": round(total_weight, 2),
|
||||
"machining_strategy": self._generate_electrode_machining_strategy(
|
||||
electrodes, material
|
||||
),
|
||||
"recommendations": self._generate_electrode_recommendations(
|
||||
electrodes, material
|
||||
),
|
||||
}
|
||||
|
||||
def _design_single_electrode(self, region: Dict, index: int,
|
||||
material: str, spark_gap: float,
|
||||
overburn: float, cavity_bbox: Dict) -> Dict:
|
||||
"""设计单个电极"""
|
||||
center = region.get("center", [0, 0, 0])
|
||||
area = region.get("area", 100)
|
||||
|
||||
feature_size = math.sqrt(area)
|
||||
|
||||
electrode_size = {
|
||||
"width": round(feature_size * 1.3 + 2 * (spark_gap + overburn), 2),
|
||||
"length": round(feature_size * 1.3 + 2 * (spark_gap + overburn), 2),
|
||||
"height": round(cavity_bbox.get("dimensions", [0, 0, 50])[2] * 0.8 + 20, 2),
|
||||
}
|
||||
|
||||
volume = electrode_size["width"] * electrode_size["length"] * electrode_size["height"]
|
||||
|
||||
return {
|
||||
"index": index,
|
||||
"type": region.get("type", "undercut"),
|
||||
"location": center,
|
||||
"size": electrode_size,
|
||||
"volume_mm3": round(volume, 1),
|
||||
"spark_gap": spark_gap,
|
||||
"overburn": overburn,
|
||||
"material": material,
|
||||
"roughing_passes": 3,
|
||||
"finishing_passes": 2,
|
||||
}
|
||||
|
||||
def _generate_electrode_machining_strategy(self, electrodes: List,
|
||||
material: str) -> List[Dict]:
|
||||
"""生成电极加工策略"""
|
||||
strategies = []
|
||||
|
||||
for elec in electrodes:
|
||||
size = elec["size"]
|
||||
is_small = min(size["width"], size["length"]) < 5
|
||||
|
||||
strategy = {
|
||||
"electrode_index": elec["index"],
|
||||
"operations": [
|
||||
{
|
||||
"operation": "roughing",
|
||||
"tool": "endmill_6mm" if not is_small else "endmill_3mm",
|
||||
"stock_allowance": 0.3,
|
||||
},
|
||||
{
|
||||
"operation": "finishing",
|
||||
"tool": "ballnose_3mm" if not is_small else "ballnose_1mm",
|
||||
"stepover": 0.2,
|
||||
},
|
||||
],
|
||||
}
|
||||
strategies.append(strategy)
|
||||
|
||||
return strategies
|
||||
|
||||
def _generate_electrode_recommendations(self, electrodes: List,
|
||||
material: str) -> List[str]:
|
||||
"""生成电极建议"""
|
||||
recs = []
|
||||
|
||||
if material == "copper":
|
||||
recs.append("铜电极加工性良好,建议使用高速钢刀具")
|
||||
elif material == "graphite":
|
||||
recs.append("石墨电极易加工但易碎,注意切削力控制")
|
||||
elif material == "copper_tungsten":
|
||||
recs.append("铜钨合金硬度高,建议使用金刚石刀具")
|
||||
|
||||
if len(electrodes) > 4:
|
||||
recs.append("电极数量较多,建议评估是否可合并电极设计")
|
||||
|
||||
recs.append("电极加工后需检测尺寸精度和表面质量")
|
||||
recs.append("放电加工时需根据材料调整电参数")
|
||||
|
||||
return recs
|
||||
|
||||
|
||||
class MachiningSimulator:
|
||||
"""加工仿真器"""
|
||||
|
||||
def simulate_machining(self, operations: List[Dict],
|
||||
stock_bbox: Dict,
|
||||
resolution: float = 1.0) -> Dict[str, Any]:
|
||||
"""
|
||||
模拟加工过程
|
||||
|
||||
Args:
|
||||
operations: 加工操作列表
|
||||
stock_bbox: 毛坯边界框
|
||||
resolution: 仿真精度 mm
|
||||
|
||||
Returns:
|
||||
仿真结果
|
||||
"""
|
||||
stock_dims = stock_bbox.get("dimensions", [100, 100, 50])
|
||||
|
||||
nx = max(2, int(stock_dims[0] / resolution))
|
||||
ny = max(2, int(stock_dims[1] / resolution))
|
||||
nz = max(2, int(stock_dims[2] / resolution))
|
||||
|
||||
stock = np.ones((nx, ny, nz), dtype=np.float32)
|
||||
|
||||
total_removed = 0
|
||||
operation_results = []
|
||||
|
||||
for op in operations:
|
||||
removed = self._simulate_operation(stock, op, stock_bbox, resolution)
|
||||
total_removed += removed
|
||||
|
||||
operation_results.append({
|
||||
"strategy": op.get("strategy", "unknown"),
|
||||
"volume_removed_mm3": removed,
|
||||
"remaining_stock_percent": round(
|
||||
(1 - total_removed / (nx * ny * nz)) * 100, 1
|
||||
),
|
||||
})
|
||||
|
||||
total_voxels = nx * ny * nz
|
||||
remaining = np.sum(stock > 0)
|
||||
removal_efficiency = (1 - remaining / total_voxels) * 100 if total_voxels > 0 else 0
|
||||
|
||||
gouging = self._detect_gouging(stock, operations, stock_bbox, resolution)
|
||||
|
||||
residual = self._analyze_residual_material(stock, stock_bbox, resolution)
|
||||
|
||||
return {
|
||||
"resolution": resolution,
|
||||
"grid_size": {"nx": nx, "ny": ny, "nz": nz},
|
||||
"operations": operation_results,
|
||||
"total_volume_removed_percent": round(removal_efficiency, 1),
|
||||
"gouging_detected": gouging,
|
||||
"residual_analysis": residual,
|
||||
"quality_assessment": self._assess_quality(gouging, residual),
|
||||
"recommendations": self._generate_simulation_recommendations(
|
||||
gouging, residual, removal_efficiency
|
||||
),
|
||||
}
|
||||
|
||||
def _simulate_operation(self, stock: np.ndarray, op: Dict,
|
||||
bbox: Dict, resolution: float) -> int:
|
||||
"""模拟单个加工操作的材料去除"""
|
||||
strategy = op.get("strategy", "")
|
||||
removed = 0
|
||||
|
||||
nx, ny, nz = stock.shape
|
||||
|
||||
if strategy == "z_level_roughing":
|
||||
levels = op.get("levels", [])
|
||||
for level in levels:
|
||||
z_level = level.get("z", 0)
|
||||
z_idx = int((z_level - bbox.get("min", [0, 0, 0])[2]) / resolution)
|
||||
z_idx = max(0, min(z_idx, nz - 1))
|
||||
|
||||
for iz in range(z_idx, nz):
|
||||
removed += int(np.sum(stock[:, :, iz] > 0))
|
||||
stock[:, :, iz] = 0
|
||||
|
||||
elif strategy in ("parallel_finishing", "contour_finishing"):
|
||||
stepover = op.get("stepover", 0.3)
|
||||
step_idx = max(1, int(stepover / resolution))
|
||||
|
||||
for ix in range(0, nx, step_idx):
|
||||
for iy in range(0, ny, step_idx):
|
||||
if stock[ix, iy, :].any():
|
||||
removed += int(np.sum(stock[ix, iy, :] > 0))
|
||||
stock[ix, iy, :] = 0
|
||||
|
||||
return removed
|
||||
|
||||
def _detect_gouging(self, stock: np.ndarray, operations: List,
|
||||
bbox: Dict, resolution: float) -> List[Dict]:
|
||||
"""检测过切"""
|
||||
gouging = []
|
||||
|
||||
for op in operations:
|
||||
stock_allowance = op.get("stock_allowance", 0)
|
||||
if stock_allowance < 0:
|
||||
gouging.append({
|
||||
"operation": op.get("strategy", "unknown"),
|
||||
"type": "negative_allowance",
|
||||
"severity": "high",
|
||||
"message": f"工序 {op.get('strategy')} 余量为负值,存在过切风险"
|
||||
})
|
||||
|
||||
return gouging
|
||||
|
||||
def _analyze_residual_material(self, stock: np.ndarray,
|
||||
bbox: Dict, resolution: float) -> Dict:
|
||||
"""分析残余材料"""
|
||||
total_voxels = stock.size
|
||||
remaining = int(np.sum(stock > 0))
|
||||
remaining_percent = (remaining / total_voxels) * 100 if total_voxels > 0 else 0
|
||||
|
||||
return {
|
||||
"remaining_voxels": remaining,
|
||||
"remaining_percent": round(remaining_percent, 2),
|
||||
"estimated_residual_volume_cm3": round(
|
||||
remaining * resolution ** 3 / 1000, 2
|
||||
),
|
||||
}
|
||||
|
||||
def _assess_quality(self, gouging: List, residual: Dict) -> Dict:
|
||||
"""评估加工质量"""
|
||||
has_gouging = len(gouging) > 0
|
||||
residual_pct = residual.get("remaining_percent", 100)
|
||||
|
||||
if has_gouging:
|
||||
grade = "FAIL"
|
||||
elif residual_pct < 5:
|
||||
grade = "GOOD"
|
||||
elif residual_pct < 15:
|
||||
grade = "ACCEPTABLE"
|
||||
else:
|
||||
grade = "INSUFFICIENT"
|
||||
|
||||
return {
|
||||
"grade": grade,
|
||||
"has_gouging": has_gouging,
|
||||
"residual_percent": residual_pct,
|
||||
}
|
||||
|
||||
def _generate_simulation_recommendations(self, gouging: List, residual: Dict,
|
||||
efficiency: float) -> List[str]:
|
||||
"""生成仿真建议"""
|
||||
recs = []
|
||||
|
||||
if gouging:
|
||||
recs.append("检测到过切,需调整加工参数")
|
||||
|
||||
residual_pct = residual.get("remaining_percent", 0)
|
||||
if residual_pct > 20:
|
||||
recs.append("残余材料较多,建议增加精加工工序")
|
||||
elif residual_pct > 5:
|
||||
recs.append("残余材料适中,需检查关键区域是否加工到位")
|
||||
|
||||
if efficiency < 50:
|
||||
recs.append("材料去除率偏低,建议优化粗加工策略")
|
||||
|
||||
if not recs:
|
||||
recs.append("仿真结果良好,加工方案可行")
|
||||
|
||||
return recs
|
||||
@@ -0,0 +1,414 @@
|
||||
"""
|
||||
铝泡沫模具质量检测模块
|
||||
|
||||
提供分模面质量检测、模具结构合理性评估、生产可行性分析等功能
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Any
|
||||
import numpy as np
|
||||
|
||||
from shared.utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class AluminumFoamMoldQualityInspector:
|
||||
"""铝泡沫模具质量检测器"""
|
||||
|
||||
def __init__(self):
|
||||
self.quality_threshold = {
|
||||
"smoothness_score": 80.0,
|
||||
"continuity_score": 95.0,
|
||||
"structure_score": 90.0
|
||||
}
|
||||
|
||||
def inspect_mold(self, cavity_data: Dict, params: Dict) -> Dict[str, Any]:
|
||||
"""
|
||||
完整的模具质量检测
|
||||
|
||||
Args:
|
||||
cavity_data: 模具型腔数据
|
||||
params: 分模参数
|
||||
|
||||
Returns:
|
||||
质量检测报告
|
||||
"""
|
||||
logger.info("开始模具质量检测...")
|
||||
|
||||
report = {
|
||||
"surface_quality": self.inspect_surface_quality(cavity_data),
|
||||
"structure_quality": self.inspect_structure_quality(cavity_data, params),
|
||||
"feasibility": self.assess_production_feasibility(cavity_data, params),
|
||||
"overall_score": 0.0,
|
||||
"passed": False,
|
||||
"warnings": [],
|
||||
"recommendations": []
|
||||
}
|
||||
|
||||
# 计算综合评分
|
||||
scores = [
|
||||
report["surface_quality"]["overall_score"],
|
||||
report["structure_quality"]["overall_score"],
|
||||
report["feasibility"]["score"]
|
||||
]
|
||||
report["overall_score"] = sum(scores) / len(scores)
|
||||
report["passed"] = report["overall_score"] >= 80.0
|
||||
|
||||
logger.info(f"质量检测完成,综合评分: {report['overall_score']:.1f}%")
|
||||
|
||||
return report
|
||||
|
||||
def inspect_surface_quality(self, cavity_data: Dict) -> Dict[str, Any]:
|
||||
"""
|
||||
检测分模面质量
|
||||
|
||||
检测项目:
|
||||
- 平滑度:曲率分析
|
||||
- 连续性:边界检查
|
||||
- 完整性:破面检测
|
||||
"""
|
||||
parting_line = cavity_data.get("parting_line", [])
|
||||
parting_surface = cavity_data.get("parting_surface")
|
||||
|
||||
# 1. 平滑度检测
|
||||
smoothness = self._check_smoothness(parting_line)
|
||||
|
||||
# 2. 连续性检测
|
||||
continuity = self._check_continuity(parting_line)
|
||||
|
||||
# 3. 完整性检测
|
||||
completeness = self._check_completeness(cavity_data)
|
||||
|
||||
overall = (smoothness["score"] * 0.4 +
|
||||
continuity["score"] * 0.3 +
|
||||
completeness["score"] * 0.3)
|
||||
|
||||
return {
|
||||
"smoothness": smoothness,
|
||||
"continuity": continuity,
|
||||
"completeness": completeness,
|
||||
"overall_score": overall,
|
||||
"passed": overall >= self.quality_threshold["smoothness_score"]
|
||||
}
|
||||
|
||||
def _check_smoothness(self, parting_line: List) -> Dict[str, Any]:
|
||||
"""检查分型线平滑度"""
|
||||
if len(parting_line) < 3:
|
||||
return {"score": 50.0, "issues": ["分型线点数不足"]}
|
||||
|
||||
try:
|
||||
points = np.array(parting_line)
|
||||
|
||||
# 计算相邻线段角度变化
|
||||
angle_changes = []
|
||||
for i in range(1, len(points) - 1):
|
||||
v1 = points[i] - points[i-1]
|
||||
v2 = points[i+1] - points[i]
|
||||
|
||||
len1, len2 = np.linalg.norm(v1), np.linalg.norm(v2)
|
||||
if len1 > 0.001 and len2 > 0.001:
|
||||
cos_angle = np.clip(np.dot(v1, v2) / (len1 * len2), -1, 1)
|
||||
angle = np.degrees(np.arccos(cos_angle))
|
||||
angle_changes.append(angle)
|
||||
|
||||
if not angle_changes:
|
||||
return {"score": 70.0, "issues": []}
|
||||
|
||||
# 计算角度变化统计
|
||||
max_angle = max(angle_changes)
|
||||
avg_angle = np.mean(angle_changes)
|
||||
|
||||
# 评分:角度变化越小越好
|
||||
score = max(0, 100 - avg_angle * 2 - max_angle * 0.5)
|
||||
|
||||
issues = []
|
||||
if max_angle > 30:
|
||||
issues.append(f"存在尖角,最大角度变化: {max_angle:.1f}°")
|
||||
if avg_angle > 15:
|
||||
issues.append(f"分型线不够平滑,平均角度变化: {avg_angle:.1f}°")
|
||||
|
||||
return {"score": score, "issues": issues, "max_angle": max_angle, "avg_angle": avg_angle}
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"平滑度检测失败: {e}")
|
||||
return {"score": 50.0, "issues": ["检测过程出错"]}
|
||||
|
||||
def _check_continuity(self, parting_line: List) -> Dict[str, Any]:
|
||||
"""检查分型线连续性"""
|
||||
if len(parting_line) < 2:
|
||||
return {"score": 0.0, "issues": ["分型线不完整"]}
|
||||
|
||||
try:
|
||||
# 检查是否有明显的间隙
|
||||
points = np.array(parting_line)
|
||||
gaps = []
|
||||
|
||||
for i in range(1, len(points)):
|
||||
gap = np.linalg.norm(points[i] - points[i-1])
|
||||
if gap > 10.0: # 10mm 以上认为有间隙
|
||||
gaps.append(gap)
|
||||
|
||||
# 评分
|
||||
if not gaps:
|
||||
score = 100.0
|
||||
issues = []
|
||||
elif len(gaps) == 1 and max(gaps) < 20:
|
||||
score = 80.0
|
||||
issues = [f"存在轻微间隙: {max(gaps):.1f}mm"]
|
||||
else:
|
||||
score = max(0, 100 - len(gaps) * 20)
|
||||
issues = [f"存在 {len(gaps)} 处间隙"]
|
||||
|
||||
return {"score": score, "issues": issues, "gap_count": len(gaps)}
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"连续性检测失败: {e}")
|
||||
return {"score": 50.0, "issues": ["检测过程出错"]}
|
||||
|
||||
def _check_completeness(self, cavity_data: Dict) -> Dict[str, Any]:
|
||||
"""检查分模完整性"""
|
||||
issues = []
|
||||
|
||||
# 检查必要的组件是否存在
|
||||
required_keys = ["cavity", "core", "parting_surface", "parting_line"]
|
||||
missing = [k for k in required_keys if k not in cavity_data]
|
||||
|
||||
if missing:
|
||||
issues.append(f"缺少组件: {', '.join(missing)}")
|
||||
return {"score": 0.0, "issues": issues}
|
||||
|
||||
# 检查分型线点数
|
||||
parting_line = cavity_data.get("parting_line", [])
|
||||
if len(parting_line) < 4:
|
||||
issues.append("分型线点数不足")
|
||||
score = len(parting_line) * 20
|
||||
else:
|
||||
score = 100.0
|
||||
|
||||
return {"score": score, "issues": issues}
|
||||
|
||||
def inspect_structure_quality(self, cavity_data: Dict, params: Dict) -> Dict[str, Any]:
|
||||
"""
|
||||
检测模具结构合理性
|
||||
|
||||
检测项目:
|
||||
- 模具尺寸
|
||||
- 壁厚
|
||||
- 拔模角
|
||||
- 倒扣处理
|
||||
"""
|
||||
analysis = cavity_data.get("analysis", {})
|
||||
bbox = analysis.get("bounding_box", {}).get("dimensions", [0, 0, 0])
|
||||
|
||||
issues = []
|
||||
recommendations = []
|
||||
|
||||
# 1. 模具尺寸检查
|
||||
mold_size = cavity_data.get("mold_block")
|
||||
if mold_size:
|
||||
# 检查尺寸是否足够
|
||||
min_dimension = min(bbox)
|
||||
if min_dimension < 20:
|
||||
issues.append("产品尺寸过小,可能影响模具强度")
|
||||
recommendations.append("建议增加产品尺寸或使用嵌件")
|
||||
|
||||
# 2. 拔模角检查
|
||||
draft_angle = params.get("draft_angle", 0)
|
||||
if draft_angle < 2.0:
|
||||
issues.append("拔模角偏小,可能导致脱模困难")
|
||||
recommendations.append("建议增大拔模角到 2-5°")
|
||||
|
||||
# 3. 倒扣区域检查
|
||||
undercut_regions = cavity_data.get("undercut_regions", [])
|
||||
if undercut_regions:
|
||||
issues.append(f"存在 {len(undercut_regions)} 个倒扣区域")
|
||||
recommendations.append("建议添加滑块或斜顶机构")
|
||||
|
||||
# 4. 铝泡沫特殊检查
|
||||
foam_material = params.get("foam_material", "")
|
||||
if foam_material:
|
||||
# 检查排气系统需求
|
||||
volume = analysis.get("volume", 0)
|
||||
if volume > 50000000: # > 50 cm³
|
||||
issues.append("大型铝泡沫产品,需要加强排气系统")
|
||||
recommendations.append("建议增加排气槽或排气针")
|
||||
|
||||
# 评分
|
||||
issue_count = len(issues)
|
||||
score = max(0, 100 - issue_count * 15)
|
||||
|
||||
return {
|
||||
"score": score,
|
||||
"issues": issues,
|
||||
"recommendations": recommendations,
|
||||
"overall_score": score,
|
||||
"passed": score >= self.quality_threshold["structure_score"]
|
||||
}
|
||||
|
||||
def assess_production_feasibility(self, cavity_data: Dict, params: Dict) -> Dict[str, Any]:
|
||||
"""
|
||||
评估生产可行性
|
||||
|
||||
评估项目:
|
||||
- 注塑压力
|
||||
- 锁模力
|
||||
- 成型周期
|
||||
- 材料利用率
|
||||
"""
|
||||
analysis = cavity_data.get("analysis", {})
|
||||
|
||||
# 计算投影面积 (mm²)
|
||||
bbox = analysis.get("bounding_box", {}).get("dimensions", [0, 0, 0])
|
||||
projected_area = bbox[0] * bbox[1] # X * Y
|
||||
|
||||
# 体积 (mm³)
|
||||
volume = analysis.get("volume", 0)
|
||||
volume_cm3 = volume / 1000
|
||||
|
||||
# 1. 注塑压力估算 (MPa)
|
||||
injection_pressure = 30 + projected_area / 1000 # 简化估算
|
||||
|
||||
# 2. 锁模力估算 (吨)
|
||||
# 铝泡沫需要较低的压力
|
||||
clamping_force_ton = projected_area * 0.0015 # 简化估算
|
||||
|
||||
# 3. 成型周期估算 (秒)
|
||||
# 铝泡沫成型周期较长
|
||||
if volume_cm3 < 10:
|
||||
cycle_time = 60
|
||||
elif volume_cm3 < 50:
|
||||
cycle_time = 90
|
||||
elif volume_cm3 < 200:
|
||||
cycle_time = 120
|
||||
else:
|
||||
cycle_time = 180
|
||||
|
||||
# 4. 材料利用率
|
||||
material_utilization = min(95, 85 + volume_cm3 / 10)
|
||||
|
||||
# 评估结果
|
||||
feasibility_items = []
|
||||
|
||||
if injection_pressure < 100:
|
||||
feasibility_items.append({
|
||||
"item": "注塑压力",
|
||||
"value": f"{injection_pressure:.1f} MPa",
|
||||
"status": "ok",
|
||||
"message": "压力在设备范围内"
|
||||
})
|
||||
else:
|
||||
feasibility_items.append({
|
||||
"item": "注塑压力",
|
||||
"value": f"{injection_pressure:.1f} MPa",
|
||||
"status": "warning",
|
||||
"message": "压力较高,需要高压设备"
|
||||
})
|
||||
|
||||
if clamping_force_ton < 300:
|
||||
feasibility_items.append({
|
||||
"item": "锁模力",
|
||||
"value": f"{clamping_force_ton:.1f} 吨",
|
||||
"status": "ok",
|
||||
"message": "锁模力在设备范围内"
|
||||
})
|
||||
else:
|
||||
feasibility_items.append({
|
||||
"item": "锁模力",
|
||||
"value": f"{clamping_force_ton:.1f} 吨",
|
||||
"status": "warning",
|
||||
"message": "需要大型注塑机"
|
||||
})
|
||||
|
||||
feasibility_items.append({
|
||||
"item": "成型周期",
|
||||
"value": f"{cycle_time} 秒",
|
||||
"status": "ok",
|
||||
"message": "周期正常"
|
||||
})
|
||||
|
||||
feasibility_items.append({
|
||||
"item": "材料利用率",
|
||||
"value": f"{material_utilization:.1f}%",
|
||||
"status": "ok",
|
||||
"message": "材料利用率良好" if material_utilization > 80 else "材料利用率偏低"
|
||||
})
|
||||
|
||||
# 综合评分
|
||||
ok_count = sum(1 for item in feasibility_items if item["status"] == "ok")
|
||||
score = (ok_count / len(feasibility_items)) * 100
|
||||
|
||||
return {
|
||||
"items": feasibility_items,
|
||||
"score": score,
|
||||
"projected_area": f"{projected_area:.0f} mm²",
|
||||
"volume": f"{volume_cm3:.1f} cm³",
|
||||
"injection_pressure": f"{injection_pressure:.1f} MPa",
|
||||
"clamping_force": f"{clamping_force_ton:.1f} 吨",
|
||||
"cycle_time": f"{cycle_time} 秒",
|
||||
"material_utilization": f"{material_utilization:.1f}%",
|
||||
"passed": score >= 75.0
|
||||
}
|
||||
|
||||
def generate_quality_report(self, cavity_data: Dict, params: Dict) -> str:
|
||||
"""
|
||||
生成质量检测报告文本
|
||||
|
||||
Returns:
|
||||
Markdown 格式的报告文本
|
||||
"""
|
||||
report = self.inspect_mold(cavity_data, params)
|
||||
|
||||
lines = [
|
||||
"# 铝泡沫模具质量检测报告",
|
||||
"",
|
||||
f"**综合评分**: {report['overall_score']:.1f}%",
|
||||
f"**检测结果**: {'✅ 通过' if report['passed'] else '❌ 未通过'}",
|
||||
"",
|
||||
"## 一、分模面质量",
|
||||
"",
|
||||
f"- 平滑度: {report['surface_quality']['smoothness']['score']:.1f}分",
|
||||
f"- 连续性: {report['surface_quality']['continuity']['score']:.1f}分",
|
||||
f"- 完整性: {report['surface_quality']['completeness']['score']:.1f}分",
|
||||
"",
|
||||
]
|
||||
|
||||
# 添加问题列表
|
||||
if report["surface_quality"]["smoothness"].get("issues"):
|
||||
lines.append("**发现的问题**:")
|
||||
for issue in report["surface_quality"]["smoothness"]["issues"]:
|
||||
lines.append(f"- {issue}")
|
||||
lines.append("")
|
||||
|
||||
# 添加结构质量
|
||||
lines.extend([
|
||||
"## 二、模具结构质量",
|
||||
"",
|
||||
f"- 评分: {report['structure_quality']['score']:.1f}分",
|
||||
"",
|
||||
])
|
||||
|
||||
if report["structure_quality"].get("issues"):
|
||||
lines.append("**结构问题**:")
|
||||
for issue in report["structure_quality"]["issues"]:
|
||||
lines.append(f"- {issue}")
|
||||
lines.append("")
|
||||
|
||||
if report["structure_quality"].get("recommendations"):
|
||||
lines.append("**改进建议**:")
|
||||
for rec in report["structure_quality"]["recommendations"]:
|
||||
lines.append(f"- {rec}")
|
||||
lines.append("")
|
||||
|
||||
# 添加生产可行性
|
||||
lines.extend([
|
||||
"## 三、生产可行性",
|
||||
"",
|
||||
])
|
||||
|
||||
for item in report["feasibility"]["items"]:
|
||||
status_icon = "✅" if item["status"] == "ok" else "⚠️"
|
||||
lines.append(f"{status_icon} **{item['item']}**: {item['value']} - {item['message']}")
|
||||
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
@@ -0,0 +1,588 @@
|
||||
"""
|
||||
冷却/浇注系统自动设计模块
|
||||
|
||||
功能:
|
||||
1. 冷却系统设计 - 水路布局、直径、间距
|
||||
2. 浇注系统设计 - 主流道、分流道、浇口
|
||||
3. 热力学估算 - 冷却时间、温度分布
|
||||
4. 排气系统设计 - 排气槽、排气针位置
|
||||
|
||||
设计依据:
|
||||
- 模具尺寸和产品几何
|
||||
- 材料热物性参数
|
||||
- 生产节拍要求
|
||||
- 行业标准规范
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Any, Optional
|
||||
import math
|
||||
from shared.utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class MaterialThermalDB:
|
||||
"""材料热物性数据库"""
|
||||
|
||||
PLASTICS = {
|
||||
"ABS": {"density": 1.05, "specific_heat": 1.47, "thermal_cond": 0.17,
|
||||
"melt_temp": 230, "mold_temp": 60, "eject_temp": 85},
|
||||
"PP": {"density": 0.90, "specific_heat": 1.90, "thermal_cond": 0.15,
|
||||
"melt_temp": 220, "mold_temp": 40, "eject_temp": 80},
|
||||
"PC": {"density": 1.20, "specific_heat": 1.25, "thermal_cond": 0.20,
|
||||
"melt_temp": 300, "mold_temp": 80, "eject_temp": 120},
|
||||
"PE": {"density": 0.95, "specific_heat": 2.30, "thermal_cond": 0.50,
|
||||
"melt_temp": 200, "mold_temp": 30, "eject_temp": 70},
|
||||
"PS": {"density": 1.05, "specific_heat": 1.34, "thermal_cond": 0.12,
|
||||
"melt_temp": 220, "mold_temp": 50, "eject_temp": 80},
|
||||
"PA": {"density": 1.14, "specific_heat": 1.70, "thermal_cond": 0.25,
|
||||
"melt_temp": 260, "mold_temp": 70, "eject_temp": 100},
|
||||
"POM": {"density": 1.42, "specific_heat": 1.47, "thermal_cond": 0.31,
|
||||
"melt_temp": 200, "mold_temp": 70, "eject_temp": 100},
|
||||
"PMMA": {"density": 1.18, "specific_heat": 1.47, "thermal_cond": 0.19,
|
||||
"melt_temp": 240, "mold_temp": 60, "eject_temp": 90},
|
||||
}
|
||||
|
||||
FOAM = {
|
||||
"AlSi10Mg": {"density": 0.45, "specific_heat": 0.90, "thermal_cond": 0.05,
|
||||
"melt_temp": 380, "mold_temp": 150, "eject_temp": 200},
|
||||
"AlSi12": {"density": 0.50, "specific_heat": 0.88, "thermal_cond": 0.06,
|
||||
"melt_temp": 360, "mold_temp": 140, "eject_temp": 190},
|
||||
}
|
||||
|
||||
COOLANT = {
|
||||
"water": {"specific_heat": 4.18, "density": 1.0, "thermal_cond": 0.60},
|
||||
"oil": {"specific_heat": 2.00, "density": 0.85, "thermal_cond": 0.15},
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_material(cls, material: str) -> Optional[Dict]:
|
||||
if material in cls.PLASTICS:
|
||||
return cls.PLASTICS[material]
|
||||
if material in cls.FOAM:
|
||||
return cls.FOAM[material]
|
||||
return None
|
||||
|
||||
|
||||
class CoolingSystemDesigner:
|
||||
"""冷却系统设计器"""
|
||||
|
||||
def design_cooling_system(self, mold_size: Dict, product_bbox: Dict,
|
||||
material: str = "ABS",
|
||||
cavity_count: int = 1,
|
||||
cycle_time_target: Optional[float] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
设计冷却系统
|
||||
|
||||
Args:
|
||||
mold_size: {"length": L, "width": W, "height": H}
|
||||
product_bbox: {"dimensions": [dx, dy, dz]}
|
||||
material: 材料名称
|
||||
cavity_count: 型腔数量
|
||||
cycle_time_target: 目标成型周期(秒)
|
||||
|
||||
Returns:
|
||||
冷却系统设计方案
|
||||
"""
|
||||
logger.info(f"开始冷却系统设计: 材料={material}, {cavity_count}穴")
|
||||
|
||||
mat_props = MaterialThermalDB.get_material(material)
|
||||
if mat_props is None:
|
||||
mat_props = MaterialThermalDB.PLASTICS["ABS"]
|
||||
logger.warning(f"未知材料 {material},使用 ABS 默认参数")
|
||||
|
||||
dims = product_bbox.get("dimensions", [100, 100, 50])
|
||||
max_wall = max(dims) * 0.6
|
||||
|
||||
cooling_time = self._estimate_cooling_time(
|
||||
max_wall, mat_props, mold_size.get("height", 100)
|
||||
)
|
||||
|
||||
layout = self._design_channel_layout(mold_size, dims, cavity_count)
|
||||
|
||||
channels = self._generate_channel_positions(layout, mold_size, dims)
|
||||
|
||||
flow_rate = self._calculate_flow_rate(channels, mat_props)
|
||||
|
||||
thermal_check = self._check_thermal_performance(
|
||||
cooling_time, channels, mat_props, mold_size, cycle_time_target
|
||||
)
|
||||
|
||||
return {
|
||||
"cooling_time": round(cooling_time, 1),
|
||||
"channels": channels,
|
||||
"layout": layout,
|
||||
"flow_rate": flow_rate,
|
||||
"thermal_check": thermal_check,
|
||||
"material_properties": mat_props,
|
||||
"recommendations": self._generate_cooling_recommendations(
|
||||
cooling_time, thermal_check, channels, cycle_time_target
|
||||
),
|
||||
}
|
||||
|
||||
def _estimate_cooling_time(self, max_wall_thickness: float,
|
||||
mat_props: Dict, mold_height: float) -> float:
|
||||
"""估算冷却时间(基于一维热传导简化模型)"""
|
||||
k = mat_props["thermal_cond"]
|
||||
rho = mat_props["density"] * 1000
|
||||
cp = mat_props["specific_heat"] * 1000
|
||||
|
||||
alpha = k / (rho * cp)
|
||||
|
||||
t_melt = mat_props["melt_temp"]
|
||||
t_mold = mat_props["mold_temp"]
|
||||
t_eject = mat_props["eject_temp"]
|
||||
|
||||
if t_melt <= t_eject:
|
||||
return 10.0
|
||||
|
||||
theta = (t_eject - t_mold) / (t_melt - t_mold) if (t_melt - t_mold) != 0 else 0.5
|
||||
theta = max(0.01, min(0.99, abs(theta)))
|
||||
|
||||
L = max_wall_thickness / 1000.0
|
||||
|
||||
cooling_time = (L ** 2 / (alpha * math.pi ** 2)) * math.log(4 / (math.pi * theta))
|
||||
|
||||
return max(5.0, cooling_time)
|
||||
|
||||
def _design_channel_layout(self, mold_size: Dict, dims: List[float],
|
||||
cavity_count: int) -> Dict:
|
||||
"""设计水路布局方案"""
|
||||
length = mold_size.get("length", 300)
|
||||
width = mold_size.get("width", 300)
|
||||
|
||||
channel_diameter = 8.0
|
||||
channel_spacing = 30.0
|
||||
wall_distance = 15.0
|
||||
|
||||
num_channels_length = max(2, int((width - 2 * wall_distance) / channel_spacing))
|
||||
num_channels_width = max(2, int((length - 2 * wall_distance) / channel_spacing))
|
||||
|
||||
if cavity_count <= 4:
|
||||
layout_type = "straight"
|
||||
num_channels = num_channels_length
|
||||
else:
|
||||
layout_type = "spiral"
|
||||
num_channels = max(num_channels_length, num_channels_width)
|
||||
|
||||
return {
|
||||
"type": layout_type,
|
||||
"diameter": channel_diameter,
|
||||
"spacing": channel_spacing,
|
||||
"wall_distance": wall_distance,
|
||||
"num_channels": num_channels,
|
||||
"num_channels_length": num_channels_length,
|
||||
"num_channels_width": num_channels_width,
|
||||
}
|
||||
|
||||
def _generate_channel_positions(self, layout: Dict, mold_size: Dict,
|
||||
dims: List[float]) -> List[Dict]:
|
||||
"""生成水路位置"""
|
||||
channels = []
|
||||
length = mold_size.get("length", 300)
|
||||
width = mold_size.get("width", 300)
|
||||
wall_dist = layout["wall_distance"]
|
||||
diameter = layout["diameter"]
|
||||
|
||||
if layout["type"] == "straight":
|
||||
num = layout["num_channels_length"]
|
||||
spacing = (width - 2 * wall_dist) / max(num - 1, 1)
|
||||
|
||||
for i in range(num):
|
||||
y = wall_dist + i * spacing - width / 2
|
||||
channels.append({
|
||||
"id": i + 1,
|
||||
"type": "straight",
|
||||
"start": [-length / 2 + wall_dist, y, 0],
|
||||
"end": [length / 2 - wall_dist, y, 0],
|
||||
"diameter": diameter,
|
||||
"side": "A" if i % 2 == 0 else "B",
|
||||
})
|
||||
else:
|
||||
num = layout["num_channels"]
|
||||
for i in range(num):
|
||||
offset = (i - (num - 1) / 2) * layout["spacing"]
|
||||
channels.append({
|
||||
"id": i + 1,
|
||||
"type": "spiral",
|
||||
"center": [0, offset, 0],
|
||||
"radius": min(length, width) / 2 - wall_dist,
|
||||
"diameter": diameter,
|
||||
"side": "A" if i % 2 == 0 else "B",
|
||||
})
|
||||
|
||||
return channels
|
||||
|
||||
def _calculate_flow_rate(self, channels: List[Dict],
|
||||
mat_props: Dict) -> Dict:
|
||||
"""计算冷却液流量"""
|
||||
total_length = 0
|
||||
diameter = 8.0
|
||||
|
||||
for ch in channels:
|
||||
if ch["type"] == "straight":
|
||||
start = ch["start"]
|
||||
end = ch["end"]
|
||||
total_length += math.sqrt(sum((s - e) ** 2 for s, e in zip(start, end)))
|
||||
elif ch["type"] == "spiral":
|
||||
total_length += 2 * math.pi * ch.get("radius", 100)
|
||||
|
||||
velocity = 1.5
|
||||
area = math.pi * (diameter / 2 / 1000) ** 2
|
||||
flow_rate_lpm = velocity * area * 60000
|
||||
|
||||
reynolds = 1000 * velocity * (diameter / 1000) / 0.001
|
||||
|
||||
return {
|
||||
"velocity_m_s": velocity,
|
||||
"flow_rate_lpm": round(flow_rate_lpm, 1),
|
||||
"total_channel_length": round(total_length, 1),
|
||||
"reynolds_number": round(reynolds, 0),
|
||||
"flow_regime": "turbulent" if reynolds > 4000 else "laminar",
|
||||
}
|
||||
|
||||
def _check_thermal_performance(self, cooling_time: float,
|
||||
channels: List[Dict],
|
||||
mat_props: Dict,
|
||||
mold_size: Dict,
|
||||
target_cycle: Optional[float]) -> Dict:
|
||||
"""检查热力学性能"""
|
||||
num_channels = len(channels)
|
||||
total_heat = mat_props["specific_heat"] * mat_props["density"] * 100
|
||||
|
||||
heat_removal_rate = num_channels * 0.5 * 4.18 * 1.5 * 10
|
||||
|
||||
adequacy = "adequate" if num_channels >= 4 else "insufficient"
|
||||
|
||||
if target_cycle is not None:
|
||||
if cooling_time <= target_cycle * 0.6:
|
||||
adequacy = "excellent"
|
||||
elif cooling_time <= target_cycle * 0.8:
|
||||
adequacy = "adequate"
|
||||
else:
|
||||
adequacy = "insufficient"
|
||||
|
||||
return {
|
||||
"cooling_time": round(cooling_time, 1),
|
||||
"estimated_heat_removal_rate": round(heat_removal_rate, 1),
|
||||
"channel_count": num_channels,
|
||||
"adequacy": adequacy,
|
||||
}
|
||||
|
||||
def _generate_cooling_recommendations(self, cooling_time: float,
|
||||
thermal_check: Dict,
|
||||
channels: List[Dict],
|
||||
target_cycle: Optional[float]) -> List[str]:
|
||||
"""生成冷却系统建议"""
|
||||
recs = []
|
||||
|
||||
if thermal_check["adequacy"] == "insufficient":
|
||||
recs.append("冷却能力不足,建议增加水路数量或增大水路直径")
|
||||
recs.append("考虑使用铍铜镶件提高局部冷却效率")
|
||||
|
||||
if cooling_time > 30:
|
||||
recs.append("冷却时间较长,建议优化水路布局使水路更靠近型腔")
|
||||
|
||||
if len(channels) < 4:
|
||||
recs.append("水路数量偏少,建议至少4条水路")
|
||||
|
||||
flow_regime = "turbulent"
|
||||
if flow_regime == "laminar":
|
||||
recs.append("冷却液流速偏低,建议提高流速以达到湍流状态(Re>4000)")
|
||||
|
||||
if not recs:
|
||||
recs.append("冷却系统设计合理,建议进行热分析验证")
|
||||
|
||||
return recs
|
||||
|
||||
|
||||
class GatingSystemDesigner:
|
||||
"""浇注系统设计器"""
|
||||
|
||||
def design_gating_system(self, product_bbox: Dict, material: str = "ABS",
|
||||
cavity_count: int = 1,
|
||||
gate_type: str = "auto",
|
||||
layout_positions: Optional[List] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
设计浇注系统
|
||||
|
||||
Args:
|
||||
product_bbox: {"dimensions": [dx, dy, dz]}
|
||||
material: 材料名称
|
||||
cavity_count: 型腔数量
|
||||
gate_type: 浇口类型 (auto/side/center/submarine/fan)
|
||||
layout_positions: 型腔位置列表
|
||||
|
||||
Returns:
|
||||
浇注系统设计方案
|
||||
"""
|
||||
logger.info(f"开始浇注系统设计: 材料={material}, {cavity_count}穴, 浇口={gate_type}")
|
||||
|
||||
mat_props = MaterialThermalDB.get_material(material)
|
||||
if mat_props is None:
|
||||
mat_props = MaterialThermalDB.PLASTICS["ABS"]
|
||||
|
||||
dims = product_bbox.get("dimensions", [100, 100, 50])
|
||||
|
||||
if gate_type == "auto":
|
||||
gate_type = self._recommend_gate_type(dims, cavity_count)
|
||||
|
||||
sprue = self._design_sprue(dims, mat_props)
|
||||
runner = self._design_runner(dims, cavity_count, layout_positions)
|
||||
gate = self._design_gate(dims, gate_type, cavity_count, mat_props)
|
||||
|
||||
venting = self._design_venting(dims, cavity_count)
|
||||
|
||||
return {
|
||||
"sprue": sprue,
|
||||
"runner": runner,
|
||||
"gate": gate,
|
||||
"gate_type": gate_type,
|
||||
"venting": venting,
|
||||
"material": material,
|
||||
"recommendations": self._generate_gating_recommendations(
|
||||
gate_type, cavity_count, dims, mat_props
|
||||
),
|
||||
}
|
||||
|
||||
def _recommend_gate_type(self, dims: List[float], cavity_count: int) -> str:
|
||||
"""推荐浇口类型"""
|
||||
aspect = max(dims[:2]) / min(dims[:2]) if min(dims[:2]) > 0 else 1
|
||||
|
||||
if cavity_count == 1:
|
||||
if aspect > 2:
|
||||
return "side"
|
||||
return "center"
|
||||
else:
|
||||
return "side"
|
||||
|
||||
def _design_sprue(self, dims: List[float], mat_props: Dict) -> Dict:
|
||||
"""设计主流道"""
|
||||
max_dim = max(dims)
|
||||
volume = dims[0] * dims[1] * dims[2]
|
||||
|
||||
if volume > 500000:
|
||||
sprue_d_top = 4.0
|
||||
sprue_d_bottom = 8.0
|
||||
elif volume > 50000:
|
||||
sprue_d_top = 3.0
|
||||
sprue_d_bottom = 6.0
|
||||
else:
|
||||
sprue_d_top = 2.5
|
||||
sprue_d_bottom = 5.0
|
||||
|
||||
sprue_length = max_dim * 0.5 + 20
|
||||
|
||||
taper_angle = math.degrees(
|
||||
math.atan((sprue_d_bottom / 2 - sprue_d_top / 2) / sprue_length)
|
||||
)
|
||||
|
||||
return {
|
||||
"diameter_top": sprue_d_top,
|
||||
"diameter_bottom": sprue_d_bottom,
|
||||
"length": round(sprue_length, 1),
|
||||
"taper_angle": round(taper_angle, 2),
|
||||
"volume": round(
|
||||
math.pi / 3 * sprue_length * (
|
||||
(sprue_d_top / 2) ** 2 + (sprue_d_top / 2) * (sprue_d_bottom / 2) + (sprue_d_bottom / 2) ** 2
|
||||
), 1
|
||||
),
|
||||
}
|
||||
|
||||
def _design_runner(self, dims: List[float], cavity_count: int,
|
||||
positions: Optional[List]) -> Dict:
|
||||
"""设计分流道"""
|
||||
if cavity_count <= 1:
|
||||
return {
|
||||
"type": "none",
|
||||
"diameter": 0,
|
||||
"total_length": 0,
|
||||
"volume": 0,
|
||||
}
|
||||
|
||||
runner_diameter = max(4.0, min(dims[:2]) * 0.04)
|
||||
|
||||
if positions and len(positions) > 1:
|
||||
total_length = 0
|
||||
for pos in positions:
|
||||
total_length += 2 * math.sqrt(pos[0] ** 2 + pos[1] ** 2)
|
||||
else:
|
||||
total_length = cavity_count * max(dims[:2]) * 1.5
|
||||
|
||||
cross_area = math.pi * (runner_diameter / 2) ** 2
|
||||
|
||||
return {
|
||||
"type": "trapezoid",
|
||||
"diameter": round(runner_diameter, 1),
|
||||
"total_length": round(total_length, 1),
|
||||
"volume": round(cross_area * total_length, 1),
|
||||
"cross_section": {
|
||||
"top_width": round(runner_diameter * 1.2, 1),
|
||||
"bottom_width": round(runner_diameter * 0.8, 1),
|
||||
"depth": round(runner_diameter * 0.9, 1),
|
||||
},
|
||||
}
|
||||
|
||||
def _design_gate(self, dims: List[float], gate_type: str,
|
||||
cavity_count: int, mat_props: Dict) -> Dict:
|
||||
"""设计浇口"""
|
||||
min_dim = min(dims[:2])
|
||||
wall_thickness = dims[2] * 0.6
|
||||
|
||||
if gate_type == "center":
|
||||
gate_diameter = max(1.0, wall_thickness * 0.5)
|
||||
return {
|
||||
"type": "center",
|
||||
"diameter": round(gate_diameter, 1),
|
||||
"length": 1.5,
|
||||
"position": "top_center",
|
||||
}
|
||||
elif gate_type == "submarine":
|
||||
gate_diameter = max(0.8, wall_thickness * 0.3)
|
||||
return {
|
||||
"type": "submarine",
|
||||
"diameter": round(gate_diameter, 1),
|
||||
"length": 2.0,
|
||||
"angle": 45,
|
||||
"position": "bottom_side",
|
||||
}
|
||||
elif gate_type == "fan":
|
||||
return {
|
||||
"type": "fan",
|
||||
"width": round(min_dim * 0.3, 1),
|
||||
"depth": round(wall_thickness * 0.5, 1),
|
||||
"length": 1.5,
|
||||
"position": "side",
|
||||
}
|
||||
else:
|
||||
gate_diameter = max(1.0, wall_thickness * 0.4)
|
||||
return {
|
||||
"type": "side",
|
||||
"diameter": round(gate_diameter, 1),
|
||||
"length": 2.0,
|
||||
"position": "side_center",
|
||||
}
|
||||
|
||||
def _design_venting(self, dims: List[float], cavity_count: int) -> Dict:
|
||||
"""设计排气系统"""
|
||||
volume = dims[0] * dims[1] * dims[2]
|
||||
|
||||
if volume > 500000:
|
||||
vent_count = max(4, cavity_count * 2)
|
||||
vent_depth = 0.03
|
||||
vent_width = 8.0
|
||||
elif volume > 50000:
|
||||
vent_count = max(2, cavity_count)
|
||||
vent_depth = 0.02
|
||||
vent_width = 5.0
|
||||
else:
|
||||
vent_count = cavity_count
|
||||
vent_depth = 0.015
|
||||
vent_width = 3.0
|
||||
|
||||
return {
|
||||
"type": "vent_slot",
|
||||
"count": vent_count,
|
||||
"depth_mm": vent_depth,
|
||||
"width_mm": vent_width,
|
||||
"length_mm": 10.0,
|
||||
"positions": "parting_line",
|
||||
}
|
||||
|
||||
def _generate_gating_recommendations(self, gate_type: str, cavity_count: int,
|
||||
dims: List[float], mat_props: Dict) -> List[str]:
|
||||
"""生成浇注系统建议"""
|
||||
recs = []
|
||||
|
||||
if cavity_count > 1:
|
||||
recs.append("多型腔模具建议使用平衡式流道布局")
|
||||
|
||||
if mat_props.get("melt_temp", 0) > 260:
|
||||
recs.append("高熔点材料,建议使用热流道系统减少废料")
|
||||
|
||||
if gate_type == "center":
|
||||
recs.append("中心浇口适用于单型腔,注意浇口痕处理")
|
||||
elif gate_type == "side":
|
||||
recs.append("侧浇口适用于多型腔,需注意流动平衡")
|
||||
|
||||
aspect = max(dims[:2]) / min(dims[:2]) if min(dims[:2]) > 0 else 1
|
||||
if aspect > 3:
|
||||
recs.append("产品长宽比大,建议使用多点进浇或扇形浇口")
|
||||
|
||||
if not recs:
|
||||
recs.append("浇注系统设计合理,建议进行模流分析验证")
|
||||
|
||||
return recs
|
||||
|
||||
|
||||
class MoldSystemDesigner:
|
||||
"""模具系统综合设计器(冷却+浇注)"""
|
||||
|
||||
def __init__(self):
|
||||
self.cooling_designer = CoolingSystemDesigner()
|
||||
self.gating_designer = GatingSystemDesigner()
|
||||
|
||||
def design_complete_system(self, mold_size: Dict, product_bbox: Dict,
|
||||
material: str = "ABS",
|
||||
cavity_count: int = 1,
|
||||
gate_type: str = "auto",
|
||||
cycle_time_target: Optional[float] = None,
|
||||
layout_positions: Optional[List] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
综合设计冷却和浇注系统
|
||||
|
||||
Returns:
|
||||
{
|
||||
"cooling": Dict,
|
||||
"gating": Dict,
|
||||
"overall_assessment": Dict,
|
||||
"recommendations": List[str]
|
||||
}
|
||||
"""
|
||||
cooling = self.cooling_designer.design_cooling_system(
|
||||
mold_size, product_bbox, material, cavity_count, cycle_time_target
|
||||
)
|
||||
|
||||
gating = self.gating_designer.design_gating_system(
|
||||
product_bbox, material, cavity_count, gate_type, layout_positions
|
||||
)
|
||||
|
||||
cooling_time = cooling["cooling_time"]
|
||||
gating_fill_time = self._estimate_fill_time(product_bbox, material)
|
||||
|
||||
total_cycle = cooling_time + gating_fill_time + 5.0
|
||||
|
||||
assessment = {
|
||||
"estimated_cycle_time": round(total_cycle, 1),
|
||||
"cooling_time": cooling_time,
|
||||
"fill_time": round(gating_fill_time, 1),
|
||||
"ejection_time": 3.0,
|
||||
"buffer_time": 2.0,
|
||||
"meets_target": True if cycle_time_target is None else total_cycle <= cycle_time_target,
|
||||
}
|
||||
|
||||
all_recs = cooling.get("recommendations", []) + gating.get("recommendations", [])
|
||||
if assessment["meets_target"] is False:
|
||||
all_recs.insert(0, f"成型周期({total_cycle:.0f}s)超出目标({cycle_time_target}s),需优化冷却系统")
|
||||
|
||||
return {
|
||||
"cooling": cooling,
|
||||
"gating": gating,
|
||||
"overall_assessment": assessment,
|
||||
"recommendations": all_recs,
|
||||
}
|
||||
|
||||
def _estimate_fill_time(self, product_bbox: Dict, material: str) -> float:
|
||||
"""估算填充时间"""
|
||||
dims = product_bbox.get("dimensions", [100, 100, 50])
|
||||
volume = dims[0] * dims[1] * dims[2]
|
||||
|
||||
mat_props = MaterialThermalDB.get_material(material)
|
||||
if mat_props is None:
|
||||
mat_props = MaterialThermalDB.PLASTICS["ABS"]
|
||||
|
||||
fill_rate = 50.0
|
||||
|
||||
fill_time = volume / fill_rate
|
||||
|
||||
return max(0.5, min(fill_time, 10.0))
|
||||
@@ -0,0 +1,343 @@
|
||||
from typing import Dict, Any, List, Optional
|
||||
|
||||
from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeFace
|
||||
from OCC.Core.BRepGProp import brepgprop
|
||||
from OCC.Core.GProp import GProp_GProps
|
||||
from OCC.Core.gp import gp_Dir, gp_Pln, gp_Pnt
|
||||
from OCC.Core.TopAbs import TopAbs_FACE
|
||||
from OCC.Core.TopExp import TopExp_Explorer
|
||||
from OCC.Core.TopoDS import TopoDS_Face, TopoDS_Shape, topods
|
||||
|
||||
from moldinsight.core.mold_generator import MoldCavityGenerator
|
||||
from moldinsight.core.aluminum_foam_mold import AluminumFoamMoldGenerator
|
||||
from moldinsight.core.parting_candidate_generator import PartingCandidateGenerator
|
||||
from moldinsight.core.parting_scheme_scorer import PartingSchemeScorer
|
||||
from shared.utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class MultiSchemeMoldPlanner:
|
||||
"""针对单个产品生成最多三套候选分模方案并排序。"""
|
||||
|
||||
def __init__(self):
|
||||
self.candidate_generator = PartingCandidateGenerator()
|
||||
self.scheme_scorer = PartingSchemeScorer()
|
||||
self.mold_generator = MoldCavityGenerator(shrinkage_rate=0.005)
|
||||
self.aluminum_foam_generator = AluminumFoamMoldGenerator(
|
||||
shrinkage_rate=0.015,
|
||||
draft_angle=3.0,
|
||||
)
|
||||
|
||||
def generate_plan(
|
||||
self,
|
||||
shape: TopoDS_Shape,
|
||||
material: Dict[str, Any],
|
||||
is_foam_material: bool = False,
|
||||
max_schemes: int = 3,
|
||||
process_params: Optional[Dict[str, Any]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
generator = self.aluminum_foam_generator if is_foam_material else self.mold_generator
|
||||
generator.set_material(material["name"])
|
||||
self._apply_process_params(generator, material, process_params)
|
||||
|
||||
analysis = generator._analyze_product_geometry(shape)
|
||||
analysis["axis_normal_stats"] = self._collect_axis_normal_stats(generator, shape)
|
||||
candidates = self.candidate_generator.generate_candidates(
|
||||
analysis=analysis,
|
||||
is_foam_material=is_foam_material,
|
||||
max_candidates=max_schemes,
|
||||
)
|
||||
|
||||
schemes = []
|
||||
for candidate in candidates:
|
||||
for offset_variant in self._build_offset_variants(candidate, is_foam_material):
|
||||
try:
|
||||
scheme = self._build_scheme(
|
||||
generator=generator,
|
||||
shape=shape,
|
||||
analysis=analysis,
|
||||
candidate=offset_variant,
|
||||
is_foam_material=is_foam_material,
|
||||
)
|
||||
if scheme is not None:
|
||||
schemes.append(scheme)
|
||||
except Exception as exc:
|
||||
logger.warning(f"候选方案 {offset_variant.get('scheme_id')} 生成失败: {exc}")
|
||||
|
||||
if not schemes:
|
||||
raise ValueError("未能生成任何可用分模方案")
|
||||
|
||||
scored_schemes = self.scheme_scorer.score_schemes(schemes)[:max_schemes]
|
||||
export_shapes = {}
|
||||
for idx, scheme in enumerate(scored_schemes, start=1):
|
||||
scheme["raw_scheme_id"] = scheme.get("scheme_id")
|
||||
scheme["scheme_id"] = f"scheme_{idx}"
|
||||
if scheme.get("cavity_data", {}).get("metadata") is not None:
|
||||
scheme["cavity_data"]["metadata"]["scheme_id"] = scheme["scheme_id"]
|
||||
scheme["cavity_data"]["metadata"]["process_parameters"] = dict(process_params or {})
|
||||
export_shapes[scheme["scheme_id"]] = scheme.pop("_export_shapes", {})
|
||||
best_scheme = scored_schemes[0]
|
||||
|
||||
return {
|
||||
"best_scheme_id": best_scheme["scheme_id"],
|
||||
"candidate_schemes": scored_schemes,
|
||||
"_export_shapes": export_shapes,
|
||||
"global_summary": {
|
||||
"scheme_count": len(scored_schemes),
|
||||
"recommended_reason": best_scheme.get("summary", ""),
|
||||
},
|
||||
}
|
||||
|
||||
def _build_scheme(
|
||||
self,
|
||||
generator: Any,
|
||||
shape: TopoDS_Shape,
|
||||
analysis: Dict[str, Any],
|
||||
candidate: Dict[str, Any],
|
||||
is_foam_material: bool,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
parting_surface = self._build_parting_surface(
|
||||
generator,
|
||||
analysis,
|
||||
candidate["direction"],
|
||||
shape,
|
||||
candidate.get("offset_ratio", 0.0),
|
||||
candidate.get("opening_span_mm"),
|
||||
)
|
||||
parting_line = generator.optimize_parting_line(
|
||||
generator._calculate_parting_line(shape, parting_surface)
|
||||
)
|
||||
|
||||
parting_direction = candidate["direction"]
|
||||
side_action_result = generator.side_action_designer.analyze_and_design(
|
||||
shape=shape,
|
||||
parting_direction=parting_direction,
|
||||
mold_size=generator._calculate_mold_size(analysis),
|
||||
parting_surface=parting_surface,
|
||||
)
|
||||
undercut_regions = generator._build_undercut_regions(
|
||||
side_action_result.get("undercut_analysis", {})
|
||||
)
|
||||
mold_structure = self._determine_mold_structure(analysis, undercut_regions)
|
||||
|
||||
scaled_shape = generator._apply_shrinkage_compensation(shape)
|
||||
drafted_shape = generator._apply_draft_angles(scaled_shape, parting_surface)
|
||||
cavity, core = generator._split_cavity_core(drafted_shape, parting_surface)
|
||||
|
||||
cavity_result = {
|
||||
"cavity": cavity,
|
||||
"core": core,
|
||||
"parting_surface": parting_surface,
|
||||
"parting_line": parting_line,
|
||||
"analysis": analysis,
|
||||
"undercut_regions": undercut_regions,
|
||||
"side_actions": side_action_result,
|
||||
}
|
||||
|
||||
if is_foam_material:
|
||||
cavity_result["mold_block"] = generator._generate_mold_block(cavity, analysis)
|
||||
cavity_result["parting_surfaces"] = {
|
||||
"primary_surface": parting_surface,
|
||||
"primary_line": parting_line,
|
||||
"primary_direction": parting_direction,
|
||||
"method": candidate["method"],
|
||||
"offset_ratio": candidate.get("offset_ratio", 0.0),
|
||||
}
|
||||
cavity_result["material"] = generator.foam_material
|
||||
cavity_result["shrinkage_applied"] = generator.shrinkage_rate
|
||||
cavity_result["draft_angle_applied"] = generator.draft_angle
|
||||
|
||||
cavity_data = generator.generate_detailed_cavity_json(cavity_result)
|
||||
key_info = generator.generate_cavity_key_info(cavity_result)
|
||||
cavity_data.setdefault("metadata", {})
|
||||
cavity_data["metadata"]["scheme_id"] = candidate["scheme_id"]
|
||||
cavity_data["metadata"]["scheme_method"] = candidate["method"]
|
||||
cavity_data["metadata"]["scheme_axis"] = candidate["axis"]
|
||||
cavity_data["metadata"]["scheme_reason"] = candidate["reason"]
|
||||
cavity_data["metadata"]["scheme_offset_ratio"] = candidate.get("offset_ratio", 0.0)
|
||||
cavity_data["metadata"]["scheme_offset_label"] = candidate.get("offset_label", "中面")
|
||||
cavity_data["metadata"]["mold_structure_type"] = mold_structure["mold_structure_type"]
|
||||
cavity_data["metadata"]["core_required"] = mold_structure["core_required"]
|
||||
cavity_data["metadata"]["structure_decision_reason"] = mold_structure["decision_reason"]
|
||||
|
||||
return {
|
||||
"scheme_id": candidate["scheme_id"],
|
||||
"method": candidate["method"],
|
||||
"axis": candidate["axis"],
|
||||
"title": candidate["title"],
|
||||
"reason": candidate["reason"],
|
||||
"priority_score": candidate.get("priority_score"),
|
||||
"normal_alignment_score": candidate.get("normal_alignment_score"),
|
||||
"offset_ratio": candidate.get("offset_ratio", 0.0),
|
||||
"offset_label": candidate.get("offset_label", "中面"),
|
||||
"mold_structure_type": mold_structure["mold_structure_type"],
|
||||
"core_required": mold_structure["core_required"],
|
||||
"decision_reason": mold_structure["decision_reason"],
|
||||
"parting": {
|
||||
"axis": candidate["axis"],
|
||||
"direction": parting_direction,
|
||||
"line": parting_line,
|
||||
"surface": cavity_data.get("parting_surface", {}),
|
||||
},
|
||||
"undercut_regions": undercut_regions,
|
||||
"side_actions": side_action_result,
|
||||
"cavity_data": cavity_data,
|
||||
"key_info": key_info,
|
||||
"_export_shapes": {
|
||||
"cavity": cavity,
|
||||
"core": core,
|
||||
"parting_surface": parting_surface,
|
||||
},
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _apply_process_params(generator: Any, material: Dict[str, Any], process_params: Optional[Dict[str, Any]]):
|
||||
params = process_params or {}
|
||||
draft_angle = float(params.get("draft_angle", getattr(generator, "draft_angle", 2.0)))
|
||||
shrinkage_rate = float(params.get("shrinkage_rate", material.get("shrinkage", 0.005) * 100.0)) / 100.0
|
||||
parting_precision = float(params.get("parting_precision", getattr(generator, "parting_line_tolerance", 0.1)))
|
||||
cavity_match = float(params.get("cavity_match", getattr(generator, "cavity_match_rate", 95.0)))
|
||||
|
||||
generator.draft_angle = draft_angle
|
||||
generator.shrinkage_rate = shrinkage_rate
|
||||
generator.parting_line_tolerance = parting_precision
|
||||
generator.cavity_match_rate = cavity_match
|
||||
|
||||
def _build_parting_surface(
|
||||
self,
|
||||
generator: Any,
|
||||
analysis: Dict[str, Any],
|
||||
direction_vector: List[float],
|
||||
shape: TopoDS_Shape,
|
||||
offset_ratio: float = 0.0,
|
||||
opening_span_mm: Optional[float] = None,
|
||||
) -> TopoDS_Face:
|
||||
center = analysis.get("bounding_box", {}).get("center", [0, 0, 0])
|
||||
dims = analysis.get("bounding_box", {}).get("dimensions", [100, 100, 100])
|
||||
span = max(dims) * 1.5 + 30
|
||||
opening_span = opening_span_mm or max(dims)
|
||||
offset_distance = float(opening_span) * float(offset_ratio)
|
||||
origin = [
|
||||
center[0] + direction_vector[0] * offset_distance,
|
||||
center[1] + direction_vector[1] * offset_distance,
|
||||
center[2] + direction_vector[2] * offset_distance,
|
||||
]
|
||||
|
||||
plane = gp_Pln(
|
||||
gp_Pnt(origin[0], origin[1], origin[2]),
|
||||
gp_Dir(direction_vector[0], direction_vector[1], direction_vector[2]),
|
||||
)
|
||||
|
||||
parting_surface = BRepBuilderAPI_MakeFace(
|
||||
plane,
|
||||
-span,
|
||||
span,
|
||||
-span,
|
||||
span,
|
||||
).Face()
|
||||
|
||||
return generator.extend_parting_surface(parting_surface, shape, extension=30.0)
|
||||
|
||||
def _build_offset_variants(
|
||||
self,
|
||||
candidate: Dict[str, Any],
|
||||
is_foam_material: bool,
|
||||
) -> List[Dict[str, Any]]:
|
||||
opening_span = float(candidate.get("opening_span_mm", 0.0))
|
||||
if opening_span <= 0:
|
||||
return [dict(candidate)]
|
||||
|
||||
ratios = [0.0, -0.12, 0.12]
|
||||
if is_foam_material and candidate.get("axis") == "Z":
|
||||
ratios = [0.0, -0.08, 0.08]
|
||||
|
||||
variants = []
|
||||
for ratio in ratios:
|
||||
variant = dict(candidate)
|
||||
label = "中面"
|
||||
id_label = "center"
|
||||
if ratio < 0:
|
||||
label = "偏下" if candidate.get("axis") == "Z" else "负向偏移"
|
||||
id_label = "neg"
|
||||
elif ratio > 0:
|
||||
label = "偏上" if candidate.get("axis") == "Z" else "正向偏移"
|
||||
id_label = "pos"
|
||||
|
||||
variant["scheme_id"] = f"{candidate.get('axis', 'A').lower()}_{id_label}_{abs(ratio):.2f}"
|
||||
variant["offset_ratio"] = ratio
|
||||
variant["offset_label"] = label
|
||||
variant["reason"] = f"{candidate.get('reason', '')},分型面位置: {label}"
|
||||
variants.append(variant)
|
||||
|
||||
return variants
|
||||
|
||||
def _collect_axis_normal_stats(self, generator: Any, shape: TopoDS_Shape) -> Dict[str, float]:
|
||||
"""按坐标轴统计面法向分布强度,用于候选方向排序。"""
|
||||
stats = {"X": 0.0, "Y": 0.0, "Z": 0.0}
|
||||
explorer = TopExp_Explorer(shape, TopAbs_FACE)
|
||||
|
||||
while explorer.More():
|
||||
face = topods.Face(explorer.Current())
|
||||
explorer.Next()
|
||||
|
||||
try:
|
||||
normal = generator._get_face_normal(face)
|
||||
if normal is None:
|
||||
continue
|
||||
|
||||
props = GProp_GProps()
|
||||
brepgprop.SurfaceProperties(face, props)
|
||||
area = max(float(props.Mass()), 1.0)
|
||||
|
||||
stats["X"] += abs(float(normal.X())) * area
|
||||
stats["Y"] += abs(float(normal.Y())) * area
|
||||
stats["Z"] += abs(float(normal.Z())) * area
|
||||
except Exception as exc:
|
||||
logger.debug(f"统计面法向失败: {exc}")
|
||||
|
||||
total = stats["X"] + stats["Y"] + stats["Z"]
|
||||
if total <= 0:
|
||||
return {"X": 33.3, "Y": 33.3, "Z": 33.4}
|
||||
|
||||
return {
|
||||
axis: round(value / total * 100, 2)
|
||||
for axis, value in stats.items()
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _determine_mold_structure(analysis: Dict[str, Any], undercut_regions: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
"""
|
||||
判定是否需要独立模芯。
|
||||
规则为工程启发式:
|
||||
- 实心度高 + 平均厚度占比高 + 无明显倒扣:倾向两板半腔(无独立凸芯)
|
||||
- 否则:采用型腔+模芯结构
|
||||
"""
|
||||
dims = analysis.get("bounding_box", {}).get("dimensions", [0.0, 0.0, 0.0])
|
||||
valid_dims = [float(d) for d in dims if float(d) > 1e-6]
|
||||
min_dim = min(valid_dims) if valid_dims else 1.0
|
||||
bbox_volume = 1.0
|
||||
for dim in valid_dims[:3]:
|
||||
bbox_volume *= dim
|
||||
if bbox_volume <= 0:
|
||||
bbox_volume = 1.0
|
||||
|
||||
volume = float(analysis.get("volume", 0.0))
|
||||
surface_area = float(analysis.get("surface_area", 0.0))
|
||||
solid_ratio = max(0.0, min(volume / bbox_volume, 1.0))
|
||||
avg_wall = (2.0 * volume / surface_area) if surface_area > 1e-6 else min_dim
|
||||
wall_ratio = max(0.0, min(avg_wall / max(min_dim, 1e-6), 1.0))
|
||||
undercut_count = len(undercut_regions or [])
|
||||
|
||||
core_required = not (solid_ratio > 0.62 and wall_ratio > 0.38 and undercut_count == 0)
|
||||
mold_structure_type = "cavity_core" if core_required else "two_half_cavity"
|
||||
decision_reason = (
|
||||
f"solid_ratio={solid_ratio:.2f}, wall_ratio={wall_ratio:.2f}, "
|
||||
f"undercut_count={undercut_count}"
|
||||
)
|
||||
|
||||
return {
|
||||
"core_required": core_required,
|
||||
"mold_structure_type": mold_structure_type,
|
||||
"decision_reason": decision_reason,
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
from typing import Dict, Any, List
|
||||
|
||||
|
||||
class PartingCandidateGenerator:
|
||||
"""生成候选分型方向,供多方案分模规划器使用。"""
|
||||
|
||||
_AXIS_DEFS = {
|
||||
"X": {"direction": [1.0, 0.0, 0.0], "title": "X轴侧向开模方案"},
|
||||
"Y": {"direction": [0.0, 1.0, 0.0], "title": "Y轴侧向开模方案"},
|
||||
"Z": {"direction": [0.0, 0.0, 1.0], "title": "Z轴上下开模方案"},
|
||||
}
|
||||
|
||||
def generate_candidates(
|
||||
self,
|
||||
analysis: Dict[str, Any],
|
||||
is_foam_material: bool = False,
|
||||
max_candidates: int = 3,
|
||||
) -> List[Dict[str, Any]]:
|
||||
bbox_dims = analysis.get("bounding_box", {}).get("dimensions", [0, 0, 0])
|
||||
|
||||
axis_metrics = self._build_axis_metrics(bbox_dims, analysis, is_foam_material)
|
||||
axis_order = [item["axis"] for item in sorted(
|
||||
axis_metrics,
|
||||
key=lambda item: item["priority_score"],
|
||||
reverse=True,
|
||||
)]
|
||||
|
||||
candidates = []
|
||||
for idx, axis in enumerate(axis_order[:max_candidates], start=1):
|
||||
axis_def = self._AXIS_DEFS[axis]
|
||||
metrics = next(item for item in axis_metrics if item["axis"] == axis)
|
||||
candidates.append({
|
||||
"scheme_id": f"scheme_{idx}",
|
||||
"rank_hint": idx,
|
||||
"axis": axis,
|
||||
"direction": axis_def["direction"],
|
||||
"title": axis_def["title"] if idx > 1 else "推荐候选方向",
|
||||
"method": metrics["method"],
|
||||
"projected_area_cm2": metrics["projected_area_cm2"],
|
||||
"opening_span_mm": metrics["opening_span_mm"],
|
||||
"priority_score": metrics["priority_score"],
|
||||
"reason": self._build_reason(metrics, is_foam_material),
|
||||
})
|
||||
|
||||
return candidates
|
||||
|
||||
@staticmethod
|
||||
def _projected_area_for_axis(bbox_dims: List[float], axis: str) -> float:
|
||||
if len(bbox_dims) < 3:
|
||||
return 0.0
|
||||
|
||||
if axis == "X":
|
||||
return (bbox_dims[1] * bbox_dims[2]) / 100
|
||||
if axis == "Y":
|
||||
return (bbox_dims[0] * bbox_dims[2]) / 100
|
||||
return (bbox_dims[0] * bbox_dims[1]) / 100
|
||||
|
||||
def _build_axis_metrics(
|
||||
self,
|
||||
bbox_dims: List[float],
|
||||
analysis: Dict[str, Any],
|
||||
is_foam_material: bool,
|
||||
) -> List[Dict[str, Any]]:
|
||||
padded_dims = (bbox_dims + [0.0, 0.0, 0.0])[:3]
|
||||
max_dim = max(max(padded_dims), 1.0)
|
||||
max_area = max(
|
||||
self._projected_area_for_axis(padded_dims, axis)
|
||||
for axis in ("X", "Y", "Z")
|
||||
) or 1.0
|
||||
inertia_matrix = analysis.get("inertia_matrix", [])
|
||||
inertia_diag = [
|
||||
float(inertia_matrix[i][i]) if i < len(inertia_matrix) and i < len(inertia_matrix[i]) else 0.0
|
||||
for i in range(3)
|
||||
]
|
||||
max_inertia = max(max(inertia_diag), 1.0)
|
||||
axis_normal_stats = analysis.get("axis_normal_stats", {})
|
||||
|
||||
metrics = []
|
||||
for axis, idx in (("X", 0), ("Y", 1), ("Z", 2)):
|
||||
opening_span = float(padded_dims[idx])
|
||||
projected_area = self._projected_area_for_axis(padded_dims, axis)
|
||||
thin_axis_score = (max_dim - opening_span) / max_dim
|
||||
compact_projection_score = 1.0 - min(projected_area / max_area, 1.0)
|
||||
inertia_score = 1.0 - min((inertia_diag[idx] if idx < len(inertia_diag) else 0.0) / max_inertia, 1.0)
|
||||
normal_alignment_score = min(float(axis_normal_stats.get(axis, 0.0)) / 100.0, 1.0)
|
||||
|
||||
priority_score = (
|
||||
thin_axis_score * 0.30
|
||||
+ compact_projection_score * 0.25
|
||||
+ inertia_score * 0.15
|
||||
+ normal_alignment_score * 0.30
|
||||
)
|
||||
method = "geometric_primary"
|
||||
if normal_alignment_score >= thin_axis_score and normal_alignment_score >= compact_projection_score:
|
||||
method = "face_normal_primary"
|
||||
elif compact_projection_score >= thin_axis_score and compact_projection_score >= inertia_score:
|
||||
method = "projected_area_backup"
|
||||
elif inertia_score > thin_axis_score:
|
||||
method = "balanced_backup"
|
||||
|
||||
if is_foam_material and axis == "Z":
|
||||
priority_score += 0.25
|
||||
method = "foam_axis_rule"
|
||||
|
||||
metrics.append({
|
||||
"axis": axis,
|
||||
"opening_span_mm": round(opening_span, 2),
|
||||
"projected_area_cm2": round(projected_area, 2),
|
||||
"thin_axis_score": round(thin_axis_score * 100, 2),
|
||||
"compact_projection_score": round(compact_projection_score * 100, 2),
|
||||
"inertia_score": round(inertia_score * 100, 2),
|
||||
"normal_alignment_score": round(normal_alignment_score * 100, 2),
|
||||
"priority_score": round(priority_score * 100, 2),
|
||||
"method": method,
|
||||
})
|
||||
|
||||
return metrics
|
||||
|
||||
@staticmethod
|
||||
def _build_reason(metrics: Dict[str, Any], is_foam_material: bool) -> str:
|
||||
axis = metrics["axis"]
|
||||
projected_area = metrics["projected_area_cm2"]
|
||||
opening_span = metrics["opening_span_mm"]
|
||||
if is_foam_material and axis == "Z":
|
||||
return (
|
||||
f"泡沫模具优先上下开模,开模跨度 {opening_span:.2f} mm,"
|
||||
f"投影面积约 {projected_area:.2f} cm²"
|
||||
)
|
||||
return (
|
||||
f"{axis} 轴方向开模跨度 {opening_span:.2f} mm,"
|
||||
f"投影面积约 {projected_area:.2f} cm²,"
|
||||
f"法向匹配度 {metrics.get('normal_alignment_score', 0):.2f},"
|
||||
f"综合几何优先级 {metrics['priority_score']:.2f}"
|
||||
)
|
||||
@@ -0,0 +1,300 @@
|
||||
from typing import Dict, Any, List, Optional, Tuple
|
||||
import re
|
||||
|
||||
|
||||
class PartingSchemeScorer:
|
||||
"""对候选分模方案打分并排序。"""
|
||||
|
||||
def score_schemes(self, schemes: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
scored = []
|
||||
for scheme in schemes:
|
||||
score_breakdown = self._score_scheme(scheme)
|
||||
undercut_priority_bonus = self._build_undercut_priority_bonus(scheme, score_breakdown)
|
||||
total_score = round(
|
||||
score_breakdown["manufacturability"] * 0.25
|
||||
+ score_breakdown["undercut_complexity"] * 0.35
|
||||
+ score_breakdown["parting_quality"] * 0.15
|
||||
+ score_breakdown["machining_cost"] * 0.15
|
||||
+ score_breakdown["risk"] * 0.10
|
||||
+ undercut_priority_bonus,
|
||||
2,
|
||||
)
|
||||
|
||||
scored_scheme = dict(scheme)
|
||||
fallback = self._assess_fallback(scored_scheme, score_breakdown)
|
||||
scored_scheme["score_breakdown"] = score_breakdown
|
||||
scored_scheme["score"] = total_score
|
||||
scored_scheme["undercut_priority_bonus"] = round(undercut_priority_bonus, 2)
|
||||
scored_scheme["is_fallback"] = fallback["is_fallback"]
|
||||
scored_scheme["fallback_reason"] = fallback["fallback_reason"]
|
||||
scored_scheme["dfm_violations"] = self._build_dfm_violations(scored_scheme)
|
||||
scored_scheme["dfm_violation_count"] = len(scored_scheme["dfm_violations"])
|
||||
scored_scheme["confidence_score"] = self._build_confidence_score(
|
||||
total_score,
|
||||
score_breakdown,
|
||||
fallback["is_fallback"],
|
||||
scored_scheme["dfm_violation_count"],
|
||||
)
|
||||
scored_scheme["summary"] = self._build_summary(scored_scheme)
|
||||
scored.append(scored_scheme)
|
||||
|
||||
scored.sort(key=lambda item: item["score"], reverse=True)
|
||||
for rank, scheme in enumerate(scored, start=1):
|
||||
scheme["rank"] = rank
|
||||
scheme["title"] = "推荐方案" if rank == 1 else f"备选方案 {rank}"
|
||||
return scored
|
||||
|
||||
def _score_scheme(self, scheme: Dict[str, Any]) -> Dict[str, float]:
|
||||
cavity_data = scheme.get("cavity_data", {})
|
||||
key_info = scheme.get("key_info", {})
|
||||
candidate_priority = float(scheme.get("priority_score", 60.0))
|
||||
offset_ratio = abs(float(scheme.get("offset_ratio", 0.0)))
|
||||
|
||||
mold_cavities = cavity_data.get("mold_cavities", {})
|
||||
quality_checks = cavity_data.get("quality_checks", {})
|
||||
quality_considerations = key_info.get("quality_considerations", {})
|
||||
manufacturing_info = cavity_data.get("manufacturing_info", {})
|
||||
|
||||
cavity_vertices = mold_cavities.get("cavity", {}).get("vertex_count", 0)
|
||||
core_vertices = mold_cavities.get("core", {}).get("vertex_count", 0)
|
||||
manufacturability = 95.0 if cavity_vertices > 0 and core_vertices > 0 else 55.0
|
||||
|
||||
undercut_regions = quality_checks.get("undercut_regions") or cavity_data.get("undercut_regions", [])
|
||||
side_actions = quality_checks.get("side_actions") or cavity_data.get("side_actions", {})
|
||||
summary = side_actions.get("summary", {})
|
||||
slider_count = len(side_actions.get("slider_mechanisms", []))
|
||||
lifter_count = len(side_actions.get("lifter_mechanisms", []))
|
||||
total_mechanism_count = int(summary.get("total_mechanism_count", slider_count + lifter_count) or 0)
|
||||
total_undercut_area = float(
|
||||
(side_actions.get("undercut_analysis", {}) or {}).get("total_undercut_area")
|
||||
or 0.0
|
||||
)
|
||||
has_pneumatic = any(
|
||||
str(item.get("actuation", "")).lower() == "pneumatic"
|
||||
for item in side_actions.get("slider_mechanisms", [])
|
||||
)
|
||||
undercut_count = len(undercut_regions)
|
||||
complexity = str(summary.get("complexity", "")).lower()
|
||||
|
||||
undercut_penalty = 0.0
|
||||
if total_mechanism_count > 0:
|
||||
undercut_penalty += 20.0
|
||||
undercut_penalty += undercut_count * 10.0
|
||||
undercut_penalty += slider_count * 6.0
|
||||
undercut_penalty += lifter_count * 5.0
|
||||
if has_pneumatic:
|
||||
undercut_penalty += 10.0
|
||||
if complexity == "moderate":
|
||||
undercut_penalty += 6.0
|
||||
elif complexity == "complex":
|
||||
undercut_penalty += 14.0
|
||||
elif complexity == "very_complex":
|
||||
undercut_penalty += 24.0
|
||||
undercut_penalty += min(total_undercut_area / 500.0, 12.0)
|
||||
undercut_complexity = max(20.0, 100.0 - undercut_penalty)
|
||||
|
||||
parting_line = scheme.get("parting", {}).get("line", [])
|
||||
parting_length = self._calculate_polyline_length(parting_line)
|
||||
smoothness = quality_checks.get("parting_line_smoothness", 85.0)
|
||||
parting_quality = max(
|
||||
40.0,
|
||||
min(
|
||||
100.0,
|
||||
smoothness - min(parting_length / 100.0, 20.0) + 10.0 + candidate_priority * 0.10 - offset_ratio * 25.0
|
||||
)
|
||||
)
|
||||
|
||||
mold_size = manufacturing_info.get("estimated_mold_size", {})
|
||||
mold_volume_factor = (
|
||||
float(mold_size.get("length", 0))
|
||||
* float(mold_size.get("width", 0))
|
||||
* float(mold_size.get("height", 0))
|
||||
) / 1_000_000 if mold_size else 0.0
|
||||
machining_cost = max(35.0, 95.0 - min(mold_volume_factor / 10.0, 25.0) - slider_count * 5.0)
|
||||
|
||||
warpage_risk = str(quality_considerations.get("warpage_risk", "low")).lower()
|
||||
risk_base = 92.0
|
||||
if "高" in warpage_risk or "high" in warpage_risk:
|
||||
risk_base = 55.0
|
||||
elif "中" in warpage_risk or "medium" in warpage_risk:
|
||||
risk_base = 75.0
|
||||
clamping_force = self._parse_first_number(manufacturing_info.get("estimated_clamping_force", "0"))
|
||||
if clamping_force > 500:
|
||||
risk_base -= 8.0
|
||||
if scheme.get("method") == "foam_axis_rule":
|
||||
risk_base += 4.0
|
||||
risk = max(35.0, risk_base)
|
||||
|
||||
return {
|
||||
"manufacturability": round(manufacturability, 2),
|
||||
"undercut_complexity": round(undercut_complexity, 2),
|
||||
"parting_quality": round(parting_quality, 2),
|
||||
"machining_cost": round(machining_cost, 2),
|
||||
"risk": round(risk, 2),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _build_undercut_priority_bonus(
|
||||
scheme: Dict[str, Any],
|
||||
score_breakdown: Dict[str, float],
|
||||
) -> float:
|
||||
cavity_data = scheme.get("cavity_data", {})
|
||||
quality_checks = cavity_data.get("quality_checks", {})
|
||||
side_actions = quality_checks.get("side_actions") or cavity_data.get("side_actions", {})
|
||||
summary = side_actions.get("summary", {})
|
||||
slider_count = len(side_actions.get("slider_mechanisms", []))
|
||||
lifter_count = len(side_actions.get("lifter_mechanisms", []))
|
||||
total_mechanism_count = int(summary.get("total_mechanism_count", slider_count + lifter_count) or 0)
|
||||
has_pneumatic = any(
|
||||
str(item.get("actuation", "")).lower() == "pneumatic"
|
||||
for item in side_actions.get("slider_mechanisms", [])
|
||||
)
|
||||
|
||||
if total_mechanism_count == 0:
|
||||
return 18.0
|
||||
|
||||
penalty = 12.0 + total_mechanism_count * 4.0
|
||||
if has_pneumatic:
|
||||
penalty += 8.0
|
||||
if float(score_breakdown.get("manufacturability", 0.0)) < 80.0:
|
||||
penalty += 4.0
|
||||
return -penalty
|
||||
|
||||
def _assess_fallback(self, scheme: Dict[str, Any], score_breakdown: Dict[str, float]) -> Dict[str, Any]:
|
||||
cavity_data = scheme.get("cavity_data", {})
|
||||
mold_cavities = cavity_data.get("mold_cavities", {})
|
||||
cavity_mesh = mold_cavities.get("cavity", {})
|
||||
core_mesh = mold_cavities.get("core", {})
|
||||
core_required = bool(scheme.get("core_required", True))
|
||||
|
||||
cavity_v = int(cavity_mesh.get("vertex_count", 0) or 0)
|
||||
core_v = int(core_mesh.get("vertex_count", 0) or 0)
|
||||
reasons = []
|
||||
|
||||
if cavity_v <= 0:
|
||||
reasons.append("型腔网格为空")
|
||||
if core_required and core_v <= 0:
|
||||
reasons.append("型芯网格为空")
|
||||
if float(score_breakdown.get("manufacturability", 0.0)) < 70.0:
|
||||
reasons.append("可制造性评分偏低")
|
||||
|
||||
return {
|
||||
"is_fallback": len(reasons) > 0,
|
||||
"fallback_reason": ";".join(reasons) if reasons else "",
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _build_confidence_score(
|
||||
total_score: float,
|
||||
score_breakdown: Dict[str, float],
|
||||
is_fallback: bool,
|
||||
dfm_violation_count: int = 0,
|
||||
) -> float:
|
||||
confidence = float(total_score)
|
||||
confidence += (float(score_breakdown.get("manufacturability", 0.0)) - 70.0) * 0.25
|
||||
confidence += (float(score_breakdown.get("parting_quality", 0.0)) - 70.0) * 0.15
|
||||
if is_fallback:
|
||||
confidence -= 18.0
|
||||
confidence -= min(max(dfm_violation_count, 0) * 3.0, 15.0)
|
||||
return round(max(20.0, min(99.0, confidence)), 2)
|
||||
|
||||
def _build_dfm_violations(self, scheme: Dict[str, Any]) -> List[Dict[str, str]]:
|
||||
cavity_data = scheme.get("cavity_data", {})
|
||||
manufacturing_info = cavity_data.get("manufacturing_info", {})
|
||||
key_info = scheme.get("key_info", {})
|
||||
geometric = key_info.get("geometric_characteristics", {})
|
||||
quality = key_info.get("quality_considerations", {})
|
||||
metadata = cavity_data.get("metadata", {})
|
||||
|
||||
violations: List[Dict[str, str]] = []
|
||||
|
||||
wall_min, wall_max = self._parse_wall_range(
|
||||
geometric.get("wall_thickness_range", "")
|
||||
)
|
||||
if wall_min is not None and wall_min < 1.2:
|
||||
violations.append({
|
||||
"rule": "最小壁厚",
|
||||
"level": "high",
|
||||
"message": f"最小壁厚 {wall_min:.2f}mm 偏薄,可能导致短射/强度不足",
|
||||
})
|
||||
if wall_max is not None and wall_max > 6.0:
|
||||
violations.append({
|
||||
"rule": "最大壁厚",
|
||||
"level": "medium",
|
||||
"message": f"最大壁厚 {wall_max:.2f}mm 偏厚,存在缩痕与冷却不均风险",
|
||||
})
|
||||
|
||||
draft_angle = self._parse_first_number(metadata.get("draft_angle"))
|
||||
if draft_angle and draft_angle < 1.0:
|
||||
violations.append({
|
||||
"rule": "拔模角",
|
||||
"level": "medium",
|
||||
"message": f"拔模角 {draft_angle:.2f}° 偏小,脱模阻力较大",
|
||||
})
|
||||
|
||||
warpage = str(quality.get("warpage_risk", "")).lower()
|
||||
if "high" in warpage or "高" in warpage:
|
||||
violations.append({
|
||||
"rule": "翘曲风险",
|
||||
"level": "high",
|
||||
"message": "当前方案翘曲风险高,建议优化壁厚与浇口位置",
|
||||
})
|
||||
|
||||
clamping_force = self._parse_first_number(
|
||||
manufacturing_info.get("estimated_clamping_force")
|
||||
)
|
||||
if clamping_force > 1200:
|
||||
violations.append({
|
||||
"rule": "锁模力",
|
||||
"level": "medium",
|
||||
"message": f"预估锁模力 {clamping_force:.0f} 吨,设备适配窗口较窄",
|
||||
})
|
||||
|
||||
return violations
|
||||
|
||||
def _build_summary(self, scheme: Dict[str, Any]) -> str:
|
||||
cavity_data = scheme.get("cavity_data", {})
|
||||
quality_checks = cavity_data.get("quality_checks", {})
|
||||
manufacturing_info = cavity_data.get("manufacturing_info", {})
|
||||
side_actions = quality_checks.get("side_actions") or cavity_data.get("side_actions", {})
|
||||
undercut_count = len(quality_checks.get("undercut_regions") or cavity_data.get("undercut_regions", []))
|
||||
slider_count = len(side_actions.get("slider_mechanisms", []))
|
||||
lifter_count = len(side_actions.get("lifter_mechanisms", []))
|
||||
axis = scheme.get("parting", {}).get("axis", "Z")
|
||||
offset_label = scheme.get("offset_label", "中面")
|
||||
clamping_force = manufacturing_info.get("estimated_clamping_force", "自动计算")
|
||||
structure_type = scheme.get("mold_structure_type", "cavity_core")
|
||||
structure_text = "型腔+模芯" if structure_type == "cavity_core" else "两板半腔(无独立模芯)"
|
||||
return (
|
||||
f"{axis} 轴开模,结构 {structure_text},分型面位置 {offset_label},倒扣 {undercut_count} 处,"
|
||||
f"滑块 {slider_count} 组,斜顶 {lifter_count} 组,"
|
||||
f"预估锁模力 {clamping_force}"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _calculate_polyline_length(points: List[List[float]]) -> float:
|
||||
total = 0.0
|
||||
for idx in range(1, len(points)):
|
||||
p1 = points[idx - 1]
|
||||
p2 = points[idx]
|
||||
total += ((p2[0] - p1[0]) ** 2 + (p2[1] - p1[1]) ** 2 + (p2[2] - p1[2]) ** 2) ** 0.5
|
||||
return total
|
||||
|
||||
@staticmethod
|
||||
def _parse_first_number(value: Any) -> float:
|
||||
if value is None:
|
||||
return 0.0
|
||||
matches = re.findall(r"\d+(?:\.\d+)?", str(value))
|
||||
return float(matches[0]) if matches else 0.0
|
||||
|
||||
@staticmethod
|
||||
def _parse_wall_range(value: Any) -> Tuple[Optional[float], Optional[float]]:
|
||||
if value is None:
|
||||
return None, None
|
||||
nums = re.findall(r"\d+(?:\.\d+)?", str(value))
|
||||
if not nums:
|
||||
return None, None
|
||||
if len(nums) == 1:
|
||||
v = float(nums[0])
|
||||
return v, v
|
||||
return float(nums[0]), float(nums[1])
|
||||
@@ -0,0 +1,533 @@
|
||||
"""
|
||||
侧壁/倒扣面滑块机构检测与设计模块
|
||||
|
||||
功能:
|
||||
1. 倒扣区域检测 - 识别无法直接脱模的侧壁凹槽
|
||||
2. 滑块机构设计 - 侧向分型抽芯机构
|
||||
3. 斜顶机构设计 - 内侧倒扣的斜顶脱模机构
|
||||
4. 机构运动学分析 - 抽芯行程、脱模角度计算
|
||||
|
||||
倒扣检测原理:
|
||||
- 分型方向确定后,检查每个面的法向量
|
||||
- 如果面的法向量与脱模方向的点积为负(面朝向脱模反方向)
|
||||
且该面不在分型面上,则判定为倒扣面
|
||||
- 根据倒扣面的位置(外侧/内侧)选择滑块或斜顶
|
||||
|
||||
滑块 vs 斜顶:
|
||||
- 滑块:外侧倒扣,沿导滑槽侧向运动
|
||||
- 斜顶:内侧倒扣,沿斜导柱内侧运动
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
import math
|
||||
import numpy as np
|
||||
from OCC.Core.TopoDS import TopoDS_Shape, TopoDS_Face
|
||||
from shared.utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class UndercutDetector:
|
||||
"""倒扣区域检测器"""
|
||||
|
||||
def detect_undercuts(self, shape: TopoDS_Shape, parting_direction: List[float],
|
||||
parting_surface: Optional[TopoDS_Face] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
检测产品中的倒扣区域
|
||||
|
||||
Args:
|
||||
shape: OCC 产品形状
|
||||
parting_direction: 分型方向 [nx, ny, nz]
|
||||
parting_surface: 分型面(可选)
|
||||
|
||||
Returns:
|
||||
{
|
||||
"undercut_faces": List[Dict],
|
||||
"slider_regions": List[Dict],
|
||||
"lifter_regions": List[Dict],
|
||||
"total_undercut_area": float,
|
||||
"requires_slider": bool,
|
||||
"requires_lifter": bool,
|
||||
"complexity": str
|
||||
}
|
||||
"""
|
||||
try:
|
||||
from OCC.Core.TopExp import TopExp_Explorer
|
||||
from OCC.Core.TopAbs import TopAbs_FACE
|
||||
from OCC.Core.TopoDS import TopoDS_Face, topods
|
||||
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface
|
||||
from OCC.Core.GProp import GProp_GProps
|
||||
from OCC.Core.BRepGProp import brepgprop
|
||||
from OCC.Core.Bnd import Bnd_Box
|
||||
from OCC.Core.BRepBndLib import brepbndlib
|
||||
from OCC.Core.gp import gp_Dir
|
||||
|
||||
dir_vec = np.array(parting_direction, dtype=np.float64)
|
||||
dir_norm = np.linalg.norm(dir_vec)
|
||||
if dir_norm < 1e-6:
|
||||
dir_vec = np.array([0, 0, 1])
|
||||
else:
|
||||
dir_vec /= dir_norm
|
||||
|
||||
parting_dir = gp_Dir(dir_vec[0], dir_vec[1], dir_vec[2])
|
||||
|
||||
undercut_faces = []
|
||||
slider_regions = []
|
||||
lifter_regions = []
|
||||
total_undercut_area = 0.0
|
||||
|
||||
parting_z = 0.0
|
||||
if parting_surface is not None:
|
||||
try:
|
||||
surface = BRepAdaptor_Surface(parting_surface)
|
||||
if surface.GetType() == 0:
|
||||
parting_z = surface.Plane().Location().Z()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
explorer = TopExp_Explorer(shape, TopAbs_FACE)
|
||||
face_idx = 0
|
||||
|
||||
while explorer.More():
|
||||
face = topods.Face(explorer.Current())
|
||||
face_idx += 1
|
||||
|
||||
try:
|
||||
surface = BRepAdaptor_Surface(face)
|
||||
u = (surface.FirstUParameter() + surface.LastUParameter()) / 2
|
||||
v = (surface.FirstVParameter() + surface.LastVParameter()) / 2
|
||||
|
||||
face_normal = None
|
||||
if surface.GetType() == 0:
|
||||
face_normal = surface.Plane().Position().Direction()
|
||||
else:
|
||||
from OCC.Core.BRepLProp import BRepLProp_SLProps
|
||||
props = BRepLProp_SLProps(surface, 1, 0.001)
|
||||
props.SetParameters(u, v)
|
||||
if props.IsNormalDefined():
|
||||
face_normal = props.Normal()
|
||||
|
||||
if face_normal is None:
|
||||
explorer.Next()
|
||||
continue
|
||||
|
||||
dot = face_normal.Dot(parting_dir)
|
||||
|
||||
face_props = GProp_GProps()
|
||||
brepgprop.SurfaceProperties(face, face_props)
|
||||
area = face_props.Mass()
|
||||
center = face_props.CentreOfMass()
|
||||
|
||||
bbox = Bnd_Box()
|
||||
brepbndlib.Add(face, bbox)
|
||||
try:
|
||||
fxmin, fymin, fzmin, fxmax, fymax, fzmax = bbox.Get()
|
||||
except Exception:
|
||||
fxmin, fymin, fzmin, fxmax, fymax, fzmax = 0, 0, 0, 0, 0, 0
|
||||
|
||||
if dot < -0.1:
|
||||
face_center_z = center.Z()
|
||||
is_outer = face_center_z >= parting_z
|
||||
|
||||
undercut_info = {
|
||||
"face_index": face_idx,
|
||||
"normal": [face_normal.X(), face_normal.Y(), face_normal.Z()],
|
||||
"dot_product": float(dot),
|
||||
"area": float(area),
|
||||
"center": [float(center.X()), float(center.Y()), float(center.Z())],
|
||||
"bbox": {
|
||||
"min": [float(fxmin), float(fymin), float(fzmin)],
|
||||
"max": [float(fxmax), float(fymax), float(fzmax)]
|
||||
},
|
||||
"severity": "high" if dot < -0.5 else "medium",
|
||||
"is_outer": is_outer,
|
||||
}
|
||||
undercut_faces.append(undercut_info)
|
||||
total_undercut_area += area
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
explorer.Next()
|
||||
|
||||
for uf in undercut_faces:
|
||||
normal = np.array(uf["normal"])
|
||||
lateral_component = normal - np.dot(normal, dir_vec) * dir_vec
|
||||
lateral_norm = np.linalg.norm(lateral_component)
|
||||
|
||||
if lateral_norm > 0.01:
|
||||
slide_direction = lateral_component / lateral_norm
|
||||
else:
|
||||
slide_direction = np.array([1, 0, 0])
|
||||
|
||||
mechanism = {
|
||||
"face_indices": [uf["face_index"]],
|
||||
"slide_direction": slide_direction.tolist(),
|
||||
"area": uf["area"],
|
||||
"center": uf["center"],
|
||||
"severity": uf["severity"],
|
||||
}
|
||||
|
||||
if uf["is_outer"]:
|
||||
slider_regions.append(mechanism)
|
||||
else:
|
||||
lifter_regions.append(mechanism)
|
||||
|
||||
requires_slider = len(slider_regions) > 0
|
||||
requires_lifter = len(lifter_regions) > 0
|
||||
|
||||
total_count = len(slider_regions) + len(lifter_regions)
|
||||
if total_count == 0:
|
||||
complexity = "simple"
|
||||
elif total_count <= 2:
|
||||
complexity = "moderate"
|
||||
elif total_count <= 4:
|
||||
complexity = "complex"
|
||||
else:
|
||||
complexity = "very_complex"
|
||||
|
||||
result = {
|
||||
"undercut_faces": undercut_faces,
|
||||
"slider_regions": slider_regions,
|
||||
"lifter_regions": lifter_regions,
|
||||
"total_undercut_area": total_undercut_area,
|
||||
"requires_slider": requires_slider,
|
||||
"requires_lifter": requires_lifter,
|
||||
"complexity": complexity,
|
||||
"parting_direction": parting_direction,
|
||||
}
|
||||
|
||||
logger.info(f"倒扣检测完成: {len(undercut_faces)} 个倒扣面, "
|
||||
f"{len(slider_regions)} 个滑块, {len(lifter_regions)} 个斜顶, "
|
||||
f"复杂度={complexity}")
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"倒扣检测失败: {e}")
|
||||
return {
|
||||
"undercut_faces": [],
|
||||
"slider_regions": [],
|
||||
"lifter_regions": [],
|
||||
"total_undercut_area": 0,
|
||||
"requires_slider": False,
|
||||
"requires_lifter": False,
|
||||
"complexity": "unknown",
|
||||
"parting_direction": parting_direction,
|
||||
}
|
||||
|
||||
|
||||
class SliderMechanismDesigner:
|
||||
"""滑块机构设计器"""
|
||||
|
||||
def design_slider(self, slider_region: Dict, mold_size: Dict,
|
||||
parting_direction: List[float]) -> Dict[str, Any]:
|
||||
"""
|
||||
设计滑块机构
|
||||
|
||||
Args:
|
||||
slider_region: 倒扣区域信息
|
||||
mold_size: 模具尺寸
|
||||
parting_direction: 分型方向
|
||||
|
||||
Returns:
|
||||
滑块机构设计方案
|
||||
"""
|
||||
center = slider_region["center"]
|
||||
area = slider_region["area"]
|
||||
slide_dir = slider_region["slide_direction"]
|
||||
|
||||
slide_stroke = self._calculate_slide_stroke(slider_region, mold_size)
|
||||
|
||||
slide_angle = self._calculate_slide_angle(slide_dir, parting_direction)
|
||||
|
||||
slide_block_size = self._calculate_slide_block_size(area, slide_stroke)
|
||||
|
||||
guide_type = self._select_guide_type(slide_stroke, slide_angle)
|
||||
|
||||
return {
|
||||
"type": "slider",
|
||||
"location": center,
|
||||
"slide_direction": slide_dir,
|
||||
"slide_stroke": slide_stroke,
|
||||
"slide_angle": slide_angle,
|
||||
"block_size": slide_block_size,
|
||||
"guide_type": guide_type,
|
||||
"locking_mechanism": self._select_locking(slide_angle),
|
||||
"actuation": "pneumatic" if slide_stroke > 50 else "mechanical",
|
||||
"components": self._generate_components(slide_block_size, guide_type),
|
||||
"manufacturing_notes": self._generate_slider_notes(slide_angle, slide_stroke),
|
||||
}
|
||||
|
||||
def _calculate_slide_stroke(self, region: Dict, mold_size: Dict) -> float:
|
||||
"""计算抽芯行程"""
|
||||
bbox = region.get("bbox", {})
|
||||
if "max" in bbox and "min" in bbox:
|
||||
max_dim = max(
|
||||
abs(bbox["max"][0] - bbox["min"][0]),
|
||||
abs(bbox["max"][1] - bbox["min"][1]),
|
||||
abs(bbox["max"][2] - bbox["min"][2])
|
||||
)
|
||||
else:
|
||||
max_dim = 10.0
|
||||
|
||||
stroke = max_dim + 5.0
|
||||
return round(max(stroke, 10.0), 1)
|
||||
|
||||
def _calculate_slide_angle(self, slide_dir: List[float],
|
||||
parting_dir: List[float]) -> float:
|
||||
"""计算滑块倾斜角度"""
|
||||
s = np.array(slide_dir)
|
||||
p = np.array(parting_dir)
|
||||
|
||||
s_norm = np.linalg.norm(s)
|
||||
p_norm = np.linalg.norm(p)
|
||||
|
||||
if s_norm < 1e-6 or p_norm < 1e-6:
|
||||
return 90.0
|
||||
|
||||
cos_angle = np.clip(np.dot(s, p) / (s_norm * p_norm), -1, 1)
|
||||
angle = math.degrees(math.acos(abs(cos_angle)))
|
||||
return round(angle, 1)
|
||||
|
||||
def _calculate_slide_block_size(self, area: float, stroke: float) -> Dict[str, float]:
|
||||
"""计算滑块尺寸"""
|
||||
width = max(math.sqrt(area) * 1.5, 15.0)
|
||||
height = max(math.sqrt(area) * 1.2, 12.0)
|
||||
length = stroke + width * 0.5
|
||||
|
||||
return {
|
||||
"width": round(width, 1),
|
||||
"height": round(height, 1),
|
||||
"length": round(length, 1),
|
||||
}
|
||||
|
||||
def _select_guide_type(self, stroke: float, angle: float) -> str:
|
||||
"""选择导滑方式"""
|
||||
if stroke > 80:
|
||||
return "T_slot_guide"
|
||||
elif angle > 20:
|
||||
return "angled_guide_pin"
|
||||
else:
|
||||
return "dovetail_guide"
|
||||
|
||||
def _select_locking(self, angle: float) -> str:
|
||||
"""选择锁紧方式"""
|
||||
if angle > 25:
|
||||
return "wedge_block"
|
||||
else:
|
||||
return "lock_block"
|
||||
|
||||
def _generate_components(self, block_size: Dict, guide_type: str) -> List[Dict]:
|
||||
"""生成滑块组件清单"""
|
||||
components = [
|
||||
{"name": "slide_block", "material": "P20", "hardness": "HRC 28-32"},
|
||||
{"name": "guide_strip", "material": "bronze", "hardness": "HB 80-100"},
|
||||
{"name": "wear_plate", "material": "T8", "hardness": "HRC 45-50"},
|
||||
{"name": "return_spring", "material": "spring_steel", "spec": "standard"},
|
||||
]
|
||||
|
||||
if guide_type == "T_slot_guide":
|
||||
components.append({"name": "T_slot_insert", "material": "P20", "hardness": "HRC 28-32"})
|
||||
elif guide_type == "angled_guide_pin":
|
||||
components.append({"name": "guide_pin", "material": "SUJ2", "hardness": "HRC 58-62"})
|
||||
elif guide_type == "dovetail_guide":
|
||||
components.append({"name": "dovetail_block", "material": "P20", "hardness": "HRC 28-32"})
|
||||
|
||||
return components
|
||||
|
||||
def _generate_slider_notes(self, angle: float, stroke: float) -> List[str]:
|
||||
"""生成滑块加工注意事项"""
|
||||
notes = []
|
||||
if angle > 25:
|
||||
notes.append("滑块角度较大,需确保锁紧可靠")
|
||||
if stroke > 50:
|
||||
notes.append("抽芯行程较长,建议使用气动抽芯")
|
||||
if stroke > 80:
|
||||
notes.append("大行程抽芯,需校核导滑槽强度")
|
||||
notes.append("滑块需设置限位装置,防止脱出")
|
||||
notes.append("配合面需做耐磨处理")
|
||||
return notes
|
||||
|
||||
|
||||
class LifterMechanismDesigner:
|
||||
"""斜顶机构设计器"""
|
||||
|
||||
def design_lifter(self, lifter_region: Dict, mold_size: Dict,
|
||||
parting_direction: List[float]) -> Dict[str, Any]:
|
||||
"""
|
||||
设计斜顶机构
|
||||
|
||||
Args:
|
||||
lifter_region: 内侧倒扣区域信息
|
||||
mold_size: 模具尺寸
|
||||
parting_direction: 分型方向
|
||||
|
||||
Returns:
|
||||
斜顶机构设计方案
|
||||
"""
|
||||
center = lifter_region["center"]
|
||||
area = lifter_region["area"]
|
||||
|
||||
lifter_angle = self._calculate_lifter_angle(lifter_region)
|
||||
|
||||
lifter_stroke = self._calculate_lifter_stroke(lifter_region)
|
||||
|
||||
lifter_size = self._calculate_lifter_size(area, lifter_stroke, lifter_angle)
|
||||
|
||||
return {
|
||||
"type": "lifter",
|
||||
"location": center,
|
||||
"lifter_angle": lifter_angle,
|
||||
"lifter_stroke": lifter_stroke,
|
||||
"block_size": lifter_size,
|
||||
"guide_type": "angled_hole",
|
||||
"return_mechanism": "spring_return",
|
||||
"components": self._generate_lifter_components(lifter_size),
|
||||
"manufacturing_notes": self._generate_lifter_notes(lifter_angle),
|
||||
}
|
||||
|
||||
def _calculate_lifter_angle(self, region: Dict) -> float:
|
||||
"""计算斜顶角度(通常5-15度)"""
|
||||
return 8.0
|
||||
|
||||
def _calculate_lifter_stroke(self, region: Dict) -> float:
|
||||
"""计算斜顶行程"""
|
||||
bbox = region.get("bbox", {})
|
||||
if "max" in bbox and "min" in bbox:
|
||||
max_dim = max(
|
||||
abs(bbox["max"][i] - bbox["min"][i]) for i in range(3)
|
||||
)
|
||||
else:
|
||||
max_dim = 5.0
|
||||
|
||||
return round(max(max_dim + 3.0, 8.0), 1)
|
||||
|
||||
def _calculate_lifter_size(self, area: float, stroke: float,
|
||||
angle: float) -> Dict[str, float]:
|
||||
"""计算斜顶尺寸"""
|
||||
width = max(math.sqrt(area) * 1.2, 10.0)
|
||||
height = stroke / math.sin(math.radians(angle)) if angle > 0 else stroke * 3
|
||||
thickness = max(width * 0.6, 8.0)
|
||||
|
||||
return {
|
||||
"width": round(width, 1),
|
||||
"height": round(height, 1),
|
||||
"thickness": round(thickness, 1),
|
||||
}
|
||||
|
||||
def _generate_lifter_components(self, size: Dict) -> List[Dict]:
|
||||
"""生成斜顶组件清单"""
|
||||
return [
|
||||
{"name": "lifter_body", "material": "P20", "hardness": "HRC 28-32"},
|
||||
{"name": "guide_pin", "material": "SUJ2", "hardness": "HRC 58-62"},
|
||||
{"name": "return_spring", "material": "spring_steel", "spec": "standard"},
|
||||
{"name": "wear_bushing", "material": "bronze", "hardness": "HB 80-100"},
|
||||
]
|
||||
|
||||
def _generate_lifter_notes(self, angle: float) -> List[str]:
|
||||
"""生成斜顶加工注意事项"""
|
||||
notes = []
|
||||
if angle > 12:
|
||||
notes.append("斜顶角度偏大,需校核脱模力")
|
||||
notes.append("斜顶导滑孔需精确加工")
|
||||
notes.append("斜顶头部需做耐磨处理")
|
||||
notes.append("需设置限位防止斜顶脱出")
|
||||
return notes
|
||||
|
||||
|
||||
class SideActionDesigner:
|
||||
"""侧向分型机构综合设计器"""
|
||||
|
||||
def __init__(self):
|
||||
self.undercut_detector = UndercutDetector()
|
||||
self.slider_designer = SliderMechanismDesigner()
|
||||
self.lifter_designer = LifterMechanismDesigner()
|
||||
|
||||
def analyze_and_design(self, shape: TopoDS_Shape, parting_direction: List[float],
|
||||
mold_size: Dict, parting_surface: Optional[TopoDS_Face] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
综合分析倒扣并设计侧向分型机构
|
||||
|
||||
Returns:
|
||||
{
|
||||
"undercut_analysis": Dict,
|
||||
"slider_mechanisms": List[Dict],
|
||||
"lifter_mechanisms": List[Dict],
|
||||
"summary": Dict,
|
||||
"recommendations": List[str]
|
||||
}
|
||||
"""
|
||||
logger.info("开始侧向分型机构分析...")
|
||||
|
||||
undercut_result = self.undercut_detector.detect_undercuts(
|
||||
shape, parting_direction, parting_surface
|
||||
)
|
||||
|
||||
slider_mechanisms = []
|
||||
for region in undercut_result["slider_regions"]:
|
||||
slider = self.slider_designer.design_slider(
|
||||
region, mold_size, parting_direction
|
||||
)
|
||||
slider_mechanisms.append(slider)
|
||||
|
||||
lifter_mechanisms = []
|
||||
for region in undercut_result["lifter_regions"]:
|
||||
lifter = self.lifter_designer.design_lifter(
|
||||
region, mold_size, parting_direction
|
||||
)
|
||||
lifter_mechanisms.append(lifter)
|
||||
|
||||
total_mechanisms = len(slider_mechanisms) + len(lifter_mechanisms)
|
||||
|
||||
summary = {
|
||||
"total_undercut_faces": len(undercut_result["undercut_faces"]),
|
||||
"total_slider_count": len(slider_mechanisms),
|
||||
"total_lifter_count": len(lifter_mechanisms),
|
||||
"total_mechanism_count": total_mechanisms,
|
||||
"complexity": undercut_result["complexity"],
|
||||
}
|
||||
|
||||
recommendations = self._generate_overall_recommendations(summary, undercut_result)
|
||||
|
||||
result = {
|
||||
"undercut_analysis": undercut_result,
|
||||
"slider_mechanisms": slider_mechanisms,
|
||||
"lifter_mechanisms": lifter_mechanisms,
|
||||
"summary": summary,
|
||||
"recommendations": recommendations,
|
||||
}
|
||||
|
||||
logger.info(f"侧向分型机构设计完成: {len(slider_mechanisms)} 个滑块, "
|
||||
f"{len(lifter_mechanisms)} 个斜顶")
|
||||
|
||||
return result
|
||||
|
||||
def _generate_overall_recommendations(self, summary: Dict,
|
||||
undercut: Dict) -> List[str]:
|
||||
"""生成总体建议"""
|
||||
recs = []
|
||||
|
||||
if summary["total_mechanism_count"] == 0:
|
||||
recs.append("无倒扣区域,模具结构简单,无需侧向分型机构")
|
||||
return recs
|
||||
|
||||
if summary["total_slider_count"] > 0:
|
||||
recs.append(f"需要 {summary['total_slider_count']} 个滑块机构处理外侧倒扣")
|
||||
|
||||
if summary["total_lifter_count"] > 0:
|
||||
recs.append(f"需要 {summary['total_lifter_count']} 个斜顶机构处理内侧倒扣")
|
||||
|
||||
if summary["total_slider_count"] > 0:
|
||||
recs.append("如存在大行程滑块,建议优先评估气动抽芯回路并预留稳定供气")
|
||||
|
||||
if summary["complexity"] == "very_complex":
|
||||
recs.append("侧向分型机构复杂,建议评估是否可通过产品修改简化")
|
||||
recs.append("考虑使用二次分型或旋转脱模替代方案")
|
||||
|
||||
if summary["total_mechanism_count"] > 3:
|
||||
recs.append("侧向机构较多,建议优化模具结构减少机构数量")
|
||||
|
||||
recs.append("所有侧向机构需做运动仿真验证干涉")
|
||||
|
||||
return recs
|
||||
@@ -0,0 +1,289 @@
|
||||
# core/stp_parser.py
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Optional, List
|
||||
import numpy as np
|
||||
import json
|
||||
from shared.utils.logger import get_logger
|
||||
from OCC.Core.GProp import GProp_GProps
|
||||
from OCC.Core.BRepGProp import brepgprop
|
||||
from OCC.Core.TopoDS import TopoDS_Shape
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class STPParser:
|
||||
"""STP文件解析器"""
|
||||
|
||||
def __init__(self):
|
||||
# 强制要求PythonOCC必须可用
|
||||
self._verify_occ_availability()
|
||||
|
||||
def _verify_occ_availability(self):
|
||||
"""验证PythonOCC是否可用,不可用则抛出异常"""
|
||||
try:
|
||||
from OCC.Core.STEPControl import STEPControl_Reader
|
||||
from OCC.Core.IFSelect import IFSelect_RetDone
|
||||
logger.info("PythonOCC验证通过")
|
||||
except ImportError as e:
|
||||
logger.error("PythonOCC不可用,服务无法运行")
|
||||
raise RuntimeError("PythonOCC未安装,请安装PythonOCC后再运行服务") from e
|
||||
|
||||
|
||||
|
||||
def load_step_file(self, file_path: Path) -> TopoDS_Shape:
|
||||
"""加载STP文件"""
|
||||
try:
|
||||
from OCC.Core.STEPControl import STEPControl_Reader
|
||||
from OCC.Core.IFSelect import IFSelect_RetDone
|
||||
|
||||
logger.info(f"加载STP文件: {file_path}")
|
||||
reader = STEPControl_Reader()
|
||||
status = reader.ReadFile(str(file_path))
|
||||
|
||||
if status == IFSelect_RetDone:
|
||||
reader.TransferRoots()
|
||||
shape = reader.OneShape()
|
||||
logger.info("STP文件加载成功")
|
||||
return shape
|
||||
else:
|
||||
raise ValueError(f"STP文件读取失败,状态码: {status}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"STP解析失败: {e}")
|
||||
raise
|
||||
|
||||
def analyze_geometry(self, shape: TopoDS_Shape) -> Dict[str, Any]:
|
||||
"""分析几何属性"""
|
||||
|
||||
try:
|
||||
from OCC.Core.GProp import GProp_GProps
|
||||
from OCC.Core.BRepGProp import brepgprop
|
||||
from OCC.Core.Bnd import Bnd_Box
|
||||
from OCC.Core.BRepBndLib import brepbndlib
|
||||
from OCC.Core.TopExp import TopExp_Explorer
|
||||
from OCC.Core.TopAbs import TopAbs_FACE, TopAbs_EDGE, TopAbs_VERTEX
|
||||
|
||||
logger.info("开始几何分析...")
|
||||
|
||||
# 计算边界框
|
||||
bbox = self._compute_bounding_box(shape)
|
||||
|
||||
# 计算体积和表面积
|
||||
volume = self._compute_volume(shape)
|
||||
area = self._compute_surface_area(shape)
|
||||
|
||||
# 分析拓扑
|
||||
topology = self._analyze_topology(shape)
|
||||
|
||||
# 计算质心
|
||||
center_of_mass = self._compute_center_of_mass(shape)
|
||||
|
||||
# 计算惯性属性
|
||||
inertia_properties = self._compute_inertia_properties(shape)
|
||||
|
||||
result = {
|
||||
"bounding_box": bbox,
|
||||
"volume": float(volume),
|
||||
"surface_area": float(area),
|
||||
"topology": topology,
|
||||
"center_of_mass": center_of_mass,
|
||||
"inertia_properties": inertia_properties,
|
||||
"analysis_method": "pythonocc"
|
||||
}
|
||||
|
||||
logger.info("几何分析完成")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"几何分析失败: {e}")
|
||||
raise
|
||||
|
||||
def _compute_bounding_box(self, shape: TopoDS_Shape) -> Dict[str, Any]:
|
||||
"""计算边界框"""
|
||||
try:
|
||||
from OCC.Core.Bnd import Bnd_Box
|
||||
from OCC.Core.BRepBndLib import brepbndlib
|
||||
|
||||
bbox = Bnd_Box()
|
||||
brepbndlib.Add(shape, bbox)
|
||||
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
|
||||
|
||||
return {
|
||||
"min": [float(xmin), float(ymin), float(zmin)],
|
||||
"max": [float(xmax), float(ymax), float(zmax)],
|
||||
"dimensions": [
|
||||
float(xmax - xmin),
|
||||
float(ymax - ymin),
|
||||
float(zmax - zmin)
|
||||
],
|
||||
"center": [
|
||||
float((xmin + xmax) / 2),
|
||||
float((ymin + ymax) / 2),
|
||||
float((zmin + zmax) / 2)
|
||||
]
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"边界框计算失败: {e}")
|
||||
return self._default_bounding_box()
|
||||
|
||||
def _compute_volume(self, shape: TopoDS_Shape) -> float:
|
||||
"""计算体积"""
|
||||
try:
|
||||
from OCC.Core.GProp import GProp_GProps
|
||||
from OCC.Core.BRepGProp import brepgprop
|
||||
|
||||
props = GProp_GProps()
|
||||
brepgprop.VolumeProperties(shape, props)
|
||||
volume = props.Mass()
|
||||
if volume <= 0:
|
||||
raise ValueError("计算得到的体积为0或负数,形状可能无效")
|
||||
return volume
|
||||
except Exception as e:
|
||||
logger.error(f"体积计算失败: {e}")
|
||||
raise RuntimeError(f"体积计算失败: {e}") from e
|
||||
|
||||
def _compute_surface_area(self, shape: TopoDS_Shape) -> float:
|
||||
"""计算表面积"""
|
||||
try:
|
||||
from OCC.Core.GProp import GProp_GProps
|
||||
from OCC.Core.BRepGProp import brepgprop
|
||||
|
||||
props = GProp_GProps()
|
||||
brepgprop.SurfaceProperties(shape, props)
|
||||
area = props.Mass()
|
||||
|
||||
# 如果计算结果为0,使用备选估算方法
|
||||
if area <= 0:
|
||||
logger.warning("表面积计算结果为0,使用边界框估算")
|
||||
raise ValueError("Surface area is zero")
|
||||
|
||||
return area
|
||||
except ValueError:
|
||||
# 基于边界框估算表面积
|
||||
try:
|
||||
bbox = self._compute_bounding_box(shape)
|
||||
dims = bbox.get("dimensions", [0, 0, 0])
|
||||
if any(d <= 0 for d in dims):
|
||||
raise RuntimeError("边界框尺寸无效,无法估算表面积")
|
||||
# 简化的估算公式:2*(lw + lh + wh)
|
||||
estimated_area = 2 * (dims[0]*dims[1] + dims[0]*dims[2] + dims[1]*dims[2])
|
||||
logger.warning(f"使用边界框估算表面积: {estimated_area:.2f} mm²")
|
||||
return estimated_area
|
||||
except Exception as e:
|
||||
logger.error(f"表面积估算失败: {e}")
|
||||
raise RuntimeError(f"表面积计算失败: {e}") from e
|
||||
except Exception as e:
|
||||
logger.error(f"表面积计算失败: {e}")
|
||||
raise RuntimeError(f"表面积计算失败: {e}") from e
|
||||
|
||||
def _compute_center_of_mass(self, shape: TopoDS_Shape) -> List[float]:
|
||||
"""计算质心"""
|
||||
try:
|
||||
from OCC.Core.GProp import GProp_GProps
|
||||
from OCC.Core.BRepGProp import brepgprop
|
||||
|
||||
props = GProp_GProps()
|
||||
brepgprop.VolumeProperties(shape, props)
|
||||
center = props.CentreOfMass()
|
||||
return [float(center.X()), float(center.Y()), float(center.Z())]
|
||||
except Exception as e:
|
||||
logger.error(f"质心计算失败: {e}")
|
||||
# 回退到边界框中心
|
||||
try:
|
||||
bbox = self._compute_bounding_box(shape)
|
||||
return bbox.get("center", [0.0, 0.0, 0.0])
|
||||
except Exception:
|
||||
raise RuntimeError(f"质心计算失败且边界框回退也失败: {e}") from e
|
||||
|
||||
def _compute_inertia_properties(self, shape: TopoDS_Shape) -> Dict[str, Any]:
|
||||
"""计算惯性属性"""
|
||||
try:
|
||||
from OCC.Core.GProp import GProp_GProps
|
||||
from OCC.Core.BRepGProp import brepgprop
|
||||
|
||||
props = GProp_GProps()
|
||||
brepgprop.VolumeProperties(shape, props)
|
||||
|
||||
inertia = props.MatrixOfInertia()
|
||||
return {
|
||||
"mass": float(props.Mass()),
|
||||
"moment_of_inertia": [
|
||||
[float(inertia.Value(1, 1)), float(inertia.Value(1, 2)), float(inertia.Value(1, 3))],
|
||||
[float(inertia.Value(2, 1)), float(inertia.Value(2, 2)), float(inertia.Value(2, 3))],
|
||||
[float(inertia.Value(3, 1)), float(inertia.Value(3, 2)), float(inertia.Value(3, 3))]
|
||||
]
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"惯性属性计算失败: {e}")
|
||||
return {}
|
||||
|
||||
def _analyze_topology(self, shape: TopoDS_Shape) -> Dict[str, int]:
|
||||
"""分析拓扑"""
|
||||
try:
|
||||
from OCC.Core.TopExp import TopExp_Explorer
|
||||
from OCC.Core.TopAbs import TopAbs_FACE, TopAbs_EDGE, TopAbs_VERTEX
|
||||
|
||||
def count_elements(element_type):
|
||||
explorer = TopExp_Explorer(shape, element_type)
|
||||
count = 0
|
||||
while explorer.More():
|
||||
count += 1
|
||||
explorer.Next()
|
||||
return count
|
||||
|
||||
return {
|
||||
"faces": count_elements(TopAbs_FACE),
|
||||
"edges": count_elements(TopAbs_EDGE),
|
||||
"vertices": count_elements(TopAbs_VERTEX)
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"拓扑分析失败: {e}")
|
||||
raise
|
||||
|
||||
def _default_bounding_box(self) -> Dict[str, Any]:
|
||||
"""默认边界框(边界框计算失败时的回退值,标注为估算)"""
|
||||
return {
|
||||
"min": [0.0, 0.0, 0.0],
|
||||
"max": [0.0, 0.0, 0.0],
|
||||
"dimensions": [0.0, 0.0, 0.0],
|
||||
"center": [0.0, 0.0, 0.0],
|
||||
"estimated": True
|
||||
}
|
||||
|
||||
def export_to_json(self, geometry_data: Dict[str, Any], output_path: Path) -> str:
|
||||
"""将几何数据导出为JSON文件"""
|
||||
try:
|
||||
# 确保输出目录存在
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 添加元数据
|
||||
json_data = {
|
||||
"metadata": {
|
||||
"export_time": str(np.datetime64('now')),
|
||||
"analysis_method": geometry_data.get("analysis_method", "unknown"),
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"geometry_data": geometry_data
|
||||
}
|
||||
|
||||
# 保存JSON文件
|
||||
with open(output_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(json_data, f, indent=2, ensure_ascii=False)
|
||||
|
||||
logger.info(f"几何数据已导出到: {output_path}")
|
||||
return str(output_path)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"JSON导出失败: {e}")
|
||||
raise
|
||||
|
||||
def get_json_data(self, geometry_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""获取JSON格式的几何数据"""
|
||||
return {
|
||||
"metadata": {
|
||||
"export_time": str(np.datetime64('now')),
|
||||
"analysis_method": geometry_data.get("analysis_method", "unknown"),
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"geometry_data": geometry_data
|
||||
}
|
||||
Reference in New Issue
Block a user