后端模块拆分

This commit is contained in:
2026-05-29 18:10:08 +08:00
parent 5bb9bc84ea
commit 823a387118
93 changed files with 198 additions and 833 deletions
+1
View File
@@ -0,0 +1 @@
# Core 模块
+209
View File
@@ -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 辅助")
+547
View File
@@ -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,
)
+625
View File
@@ -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 "低 - 常规排气即可"
+993
View File
@@ -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
+478
View File
@@ -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
+835
View File
@@ -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 "分析完成"
+241
View File
@@ -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)
}
+646
View File
@@ -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
+511
View File
@@ -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 "中 - 建议壁厚均匀性检查"
+762
View File
@@ -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
+289
View File
@@ -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
}