This commit is contained in:
2026-04-19 23:41:35 +08:00
parent 7660cda647
commit ef257e7120
31 changed files with 6317 additions and 1332 deletions
+546
View File
@@ -0,0 +1,546 @@
"""
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 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: Any) -> 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_Add
from OCC.Core.TopTools import TopTools_IndexedDataMapOfShapeListOfShape
from OCC.Core.TopExp import topexp_MapShapesAndAncestors
from OCC.Core.TopoDS import TopoDS_Face, TopoDS_Edge
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: Any, 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: Any, 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: Any, 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: Any, 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,
)
File diff suppressed because it is too large Load Diff
+865
View File
@@ -0,0 +1,865 @@
from typing import Dict, List, Any, Tuple, Optional
import math
import numpy as np
from OCC.Core.BRepOffsetAPI import BRepOffsetAPI_DraftAngle
from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Cut, BRepAlgoAPI_Section, BRepAlgoAPI_Common
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_Face
from OCC.Core.BRep import BRep_Tool
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 models.schemas import create_mold_cavity_data, create_mold_key_info
from 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: Any) -> Any:
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: Any, parting_surface: Any) -> Any:
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: Any) -> 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: Any, draft_direction: gp_Dir) -> List[Any]:
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: Any) -> 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: Any, faces: List[Any],
draft_direction: gp_Dir, draft_angle_rad: float) -> Any:
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: Any, faces: List[Any],
draft_direction: gp_Dir, draft_angle_rad: float) -> Any:
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: Any) -> 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: Any, parting_surface: Any, margin: int = 20) -> Tuple[Any, Any]:
"""
分离型腔和型芯
正确流程:
1. 创建模具块(产品边界框 + 余量)
2. 用分型面将模具块切分为 A板(上模)和 B板(下模)
3. A板减去产品 → 型腔(凹模)
4. B板减去产品 → 型芯(凸模)
"""
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:
logger.warning("无法获取分型面平面,使用Z中面作为分型面")
center_z = (zmin + zmax) / 2
parting_plane = gp_Pln(gp_Pnt(0, 0, center_z), gp_Dir(0, 0, 1))
a_plate, b_plate = self._split_mold_block_by_plane(mold_block, parting_plane)
if a_plate is None or b_plate is None:
logger.warning("A/B板分离失败,回退到简化方法")
return self._split_cavity_core_fallback(shape, mold_block)
cavity = self._subtract_product_from_plate(a_plate, shape, "A板")
core = self._subtract_product_from_plate(b_plate, shape, "B板")
if cavity is None:
cavity = a_plate
if core is None:
core = b_plate
logger.info("型腔/型芯分离完成(基于分型面A/B板切分)")
return cavity, core
except Exception as e:
logger.error(f"型腔分离失败: {e}")
return self._split_cavity_core_fallback(shape, None)
def _get_parting_plane(self, parting_surface: Any, shape: Any) -> 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: Any,
parting_plane: gp_Pln) -> Tuple[Any, Any]:
"""
用分型面将模具块切分为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: Any, product: Any,
plate_name: str) -> Any:
"""从模板中减去产品形状,生成型腔或型芯"""
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: Any,
mold_block: Optional[Any] = None) -> Tuple[Any, Any]:
"""
分模回退方案:当分型面切分失败时使用
使用Z中面将模具块简单切分为上下两半
"""
logger.warning("使用分模回退方案(Z中面切分)")
try:
bbox = Bnd_Box()
brepbndlib.Add(shape, bbox)
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
if mold_block is None:
margin = 20
mold_block = BRepPrimAPI_MakeBox(
gp_Pnt(xmin - margin, ymin - margin, zmin - margin),
gp_Pnt(xmax + margin, ymax + margin, zmax + margin)
).Shape()
center_z = (zmin + zmax) / 2
parting_plane = gp_Pln(gp_Pnt(0, 0, center_z), gp_Dir(0, 0, 1))
a_plate, b_plate = self._split_mold_block_by_plane(mold_block, parting_plane)
if a_plate is not None and b_plate is not None:
cavity = self._subtract_product_from_plate(a_plate, shape, "A板(回退)")
core = self._subtract_product_from_plate(b_plate, shape, "B板(回退)")
return cavity or a_plate, core or b_plate
logger.warning("回退方案也失败,使用最简方法")
cavity_op = BRepAlgoAPI_Cut(mold_block, shape)
cavity = cavity_op.Shape() if cavity_op.IsDone() else mold_block
return cavity, shape
except Exception as e:
logger.error(f"分模回退方案失败: {e}")
return shape, shape
def detect_insert_regions(self, shape: Any, 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: Any, 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 _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: Any, parting_surface: Any) -> 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: Any) -> 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: Any, shape: Any,
extension: float = 30.0) -> Any:
"""
将分型面延伸到模具块边界
分型面通常只覆盖产品轮廓,需要延伸到模具块边缘
才能正确分离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
+426
View File
@@ -0,0 +1,426 @@
"""
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
from typing import Dict, List, Any, Optional
from pathlib import Path
from 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)
def export_step(self, shape: Any, 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: Any, 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: Any, 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: Any, 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) -> Dict[str, Any]:
"""
批量导出模具设计结果
Args:
cavity_data: generate_mold_cavities() 的返回结果
base_filename: 基础文件名(不含扩展名)
formats: 导出格式列表 ["step", "iges", "stl", "brep"]
components: 导出组件列表 ["cavity", "core", "parting_surface", "all"]
Returns:
导出结果摘要
"""
if formats is None:
formats = ["step", "stl"]
if components is None:
components = ["cavity", "core"]
export_dir = os.path.join(self.output_dir, base_filename)
os.makedirs(export_dir, exist_ok=True)
results = {
"base_filename": base_filename,
"export_dir": export_dir,
"files": [],
"errors": [],
}
shape_map = {
"cavity": ("cavity", "型腔"),
"core": ("core", "型芯"),
"parting_surface": ("parting_surface", "分型面"),
}
shapes_to_export = []
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))
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))
else:
results["errors"].append(f"{label}形状不可用")
for comp_name, label, shape in shapes_to_export:
for fmt in formats:
filepath = os.path.join(export_dir, f"{base_filename}_{comp_name}.{fmt}")
success = False
if fmt == "step":
success = self.export_step(shape, filepath)
elif 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)
results["files"].append({
"component": comp_name,
"component_label": label,
"format": fmt,
"filepath": filepath,
"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[Any, 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"
+437
View File
@@ -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 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
+555 -60
View File
@@ -1,7 +1,5 @@
# core/geometry_analyzer.py
from typing import Dict, List, Any
# import features # 暂时注释掉,避免导入错误
from typing import Dict, List, Any, Optional
import math
import numpy as np
from models.schemas import (
create_mold_feature,
@@ -14,7 +12,7 @@ logger = get_logger(__name__)
class GeometryAnalyzer:
"""几何分析器 - 简化版"""
"""几何分析器 - 基于 OCC Shape 的精确分析"""
def __init__(self):
self.feature_thresholds = {
@@ -39,31 +37,30 @@ class GeometryAnalyzer:
def analyze_mold_design(self, geometry_data: Dict[str, Any],
product_material: str = "ABS",
mold_material: str = "Aluminum"
mold_material: str = "Aluminum",
shape: Any = 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)
features = self._detect_features(geometry_data, shape)
# 使用产品材料属性
product_props = self.product_materials.get(product_material, {})
shrinkage = product_props.get("shrinkage", 0.005)
# 使用模具材料属性
mold_props = self.mold_materials.get(mold_material, {})
thermal_cond = mold_props.get("thermal_conductivity", 200)
# 生成设计建议
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(
@@ -74,32 +71,111 @@ class GeometryAnalyzer:
analysis_summary=analysis_summary
)
def _detect_features(self, geometry_data: Dict[str, Any]) -> List[Dict[str, Any]]:
def _detect_features(self, geometry_data: Dict[str, Any],
shape: Any = None) -> List[Dict[str, Any]]:
"""检测模具特征"""
features = []
# 壁厚分析
wall_features = self._detect_wall_features(geometry_data)
wall_features = self._detect_wall_features(geometry_data, shape)
features.extend(wall_features)
# 加强筋检测
rib_features = self._detect_rib_features(geometry_data)
rib_features = self._detect_rib_features(geometry_data, shape)
features.extend(rib_features)
# BOSS柱检测
boss_features = self._detect_boss_features(geometry_data)
boss_features = self._detect_boss_features(geometry_data, shape)
features.extend(boss_features)
# 拔模角度分析
draft_features = self._analyze_draft_angles(geometry_data)
draft_features = self._analyze_draft_angles(geometry_data, shape)
features.extend(draft_features)
if shape is not None:
curvature_features = self._detect_curvature_features(shape)
features.extend(curvature_features)
fillet_features = self._detect_fillet_features(shape)
features.extend(fillet_features)
logger.info(f"检测到 {len(features)} 个特征")
return features
def _detect_wall_features(self, geometry_data: Dict[str, Any]) -> List[Dict[str, Any]]:
def _detect_wall_features(self, geometry_data: Dict[str, Any],
shape: Any = 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)
@@ -112,7 +188,7 @@ class GeometryAnalyzer:
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},
parameters={"average_thickness": avg_thickness, "estimation_method": "heuristic"},
recommendations=[
f"平均壁厚 {avg_thickness:.2f}mm 过薄,建议增加到 {self.feature_thresholds['thin_wall']}mm 以上",
"考虑增加加强筋以提高结构强度",
@@ -125,7 +201,7 @@ class GeometryAnalyzer:
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},
parameters={"average_thickness": avg_thickness, "estimation_method": "heuristic"},
recommendations=[
f"平均壁厚 {avg_thickness:.2f}mm 过厚,可能产生缩痕",
"考虑减薄壁厚或增加加强筋",
@@ -133,7 +209,6 @@ class GeometryAnalyzer:
]
))
elif volume > 0:
# 如果没有surface_area,基于边界框估算壁厚
bbox = geometry_data.get("bounding_box", {})
dimensions = bbox.get("dimensions", [100, 100, 100])
bbox_volume = dimensions[0] * dimensions[1] * dimensions[2]
@@ -155,7 +230,96 @@ class GeometryAnalyzer:
return features
def _detect_rib_features(self, geometry_data: Dict[str, Any]) -> List[Dict[str, Any]]:
def _compute_precise_wall_thickness(self, shape: Any) -> 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
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: Any = None) -> List[Dict[str, Any]]:
"""检测加强筋特征"""
features = []
topology = geometry_data.get("topology", {})
@@ -165,9 +329,13 @@ class GeometryAnalyzer:
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=0.7,
confidence=confidence,
location=geometry_data.get("center_of_mass", [0, 0, 0]),
dimensions=[2.0, 8.0, 2.0],
parameters={"complexity_ratio": complexity_ratio},
@@ -181,7 +349,8 @@ class GeometryAnalyzer:
return features
def _detect_boss_features(self, geometry_data: Dict[str, Any]) -> List[Dict[str, Any]]:
def _detect_boss_features(self, geometry_data: Dict[str, Any],
shape: Any = None) -> List[Dict[str, Any]]:
"""检测BOSS柱特征"""
features = []
volume = geometry_data.get("volume", 0)
@@ -191,9 +360,13 @@ class GeometryAnalyzer:
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=0.65,
confidence=confidence,
location=bbox.get("center", [50, 50, 50]),
dimensions=[6.0, 12.0, 6.0],
parameters={"volume_efficiency": volume_efficiency},
@@ -208,16 +381,66 @@ class GeometryAnalyzer:
return features
def _analyze_draft_angles(self, geometry_data: Dict[str, Any]) -> List[Dict[str, Any]]:
def _analyze_draft_angles(self, geometry_data: Dict[str, Any],
shape: Any = 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},
parameters={"recommended_angle": 2.0, "estimation_method": "heuristic"},
recommendations=[
"建议所有垂直面添加1-2度拔模角度",
"纹理表面需要3-5度拔模角度",
@@ -227,18 +450,238 @@ class GeometryAnalyzer:
return features
def _compute_draft_angles_from_shape(self, shape: Any) -> 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
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: Any) -> 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
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: Any) -> 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
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)
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",
@@ -247,31 +690,60 @@ class GeometryAnalyzer:
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": feature["parameters"]["average_thickness"],
"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) -> Dict[str, Any]:
material: str,
features: List[Dict[str, Any]] = None) -> Dict[str, Any]:
"""获取壁厚建议"""
volume = geometry_data.get("volume", 0)
surface_area = geometry_data.get("surface_area", 0)
avg_thickness = None
if volume > 0 and surface_area > 0:
avg_thickness = (volume / surface_area) * 0.6
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"]
@@ -291,7 +763,6 @@ class GeometryAnalyzer:
"""计算质量指标"""
metrics = {}
# 体积利用率
bbox = geometry_data.get("bounding_box", {})
dimensions = bbox.get("dimensions", [100, 100, 100])
volume = geometry_data.get("volume", 0)
@@ -299,22 +770,42 @@ class GeometryAnalyzer:
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
# 壁厚均匀性评分
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
metrics["wall_uniformity"] = 1.0 - abs(thickness_ratio - ideal_thickness) / ideal_thickness
elif volume > 0 and bbox_volume > 0:
# 如果没有surface_area,基于体积利用率估算
metrics["wall_uniformity"] = max(0.5, metrics["volume_utilization"])
else:
metrics["wall_uniformity"] = 0.5
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
@@ -334,7 +825,11 @@ class GeometryAnalyzer:
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 "分析完成"
return " | ".join(summary_parts) if summary_parts else "分析完成"
+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 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
+137 -570
View File
@@ -1,48 +1,28 @@
# src/core/mold_generator.py
from pathlib import Path
from typing import Dict, List, Any, Tuple, Optional, Callable
from typing import Dict, List, Any, Tuple, Optional
import numpy as np
from OCC.Core.BRepOffsetAPI import BRepOffsetAPI_MakeThickSolid
from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Cut, BRepAlgoAPI_Fuse, BRepAlgoAPI_Section
from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeFace, BRepBuilderAPI_Transform
from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox
from OCC.Core.Geom import Geom_Plane
from OCC.Core.gp import gp_Pln, gp_Dir, gp_Pnt, gp_Vec, gp_Trsf
from OCC.Core.TopTools import TopTools_ListOfShape
from OCC.Core.TopoDS import TopoDS_Face, TopoDS_Shape, TopoDS_Edge, TopoDS_Vertex
from OCC.Core.BRep import BRep_Tool
from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh
from OCC.Core.BRepExtrema import BRepExtrema_DistShapeShape
from OCC.Core.GProp import GProp_GProps
from OCC.Core.BRepGProp import brepgprop
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
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface
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.TopAbs import TopAbs_FACE
from OCC.Core.Bnd import Bnd_Box
from OCC.Core.BRepBndLib import brepbndlib_Add
from models.schemas import create_mold_cavity_data, create_mold_key_info
from utils.logger import get_logger
from core.base_mold_generator import BaseMoldGenerator
logger = get_logger(__name__)
class MoldCavityGenerator:
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)
Args:
shrinkage_rate: 收缩率(默认0.5% for ABS)
draft_angle: 拔模角(默认2度)
material_density: 材料密度 g/cm³(默认1.05 for ABS)
"""
self.shrinkage_rate = shrinkage_rate
self.draft_angle = draft_angle # 度
self.material_density = material_density # g/cm³
# 常用塑料材料密度(g/cm³)
self.material_densities = {
"ABS": 1.05,
"PP": 0.90,
@@ -54,25 +34,8 @@ class MoldCavityGenerator:
"PMMA": 1.18
}
# 分型面检测参数
self.parting_line_tolerance = 0.1
self.max_draft_angle = 5.0
# AI 模型接口(预留)
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):
"""
设置 AI 模型接口(预留)
Args:
parting_detector: 分型面检测 AI 模型
draft_analyzer: 拔模分析 AI 模型
"""
self.ai_parting_detector = parting_detector
self.ai_draft_analyzer = draft_analyzer
logger.info("AI 模型接口已设置")
def set_material(self, material: str):
"""设置产品材料"""
@@ -88,30 +51,25 @@ class MoldCavityGenerator:
Returns:
{
"cavity": cavity_shape, # 型腔(产品外部)
"core": core_shape, # 型芯(产品内部)
"parting_surface": parting_surface, # 分型面
"parting_line": parting_line # 分型线
"cavity": cavity_shape,
"core": core_shape,
"parting_surface": parting_surface,
"parting_line": parting_line
}
"""
logger.info("开始生成模具型腔...")
try:
# Step 1: 分析产品几何
analysis = self._analyze_product_geometry(product_shape)
# Step 2: 检测分型面和分型线
parting_surface, parting_line = self._detect_parting_surface(
product_shape, analysis
)
# Step 3: 应用收缩率补偿
scaled_shape = self._apply_shrinkage_compensation(product_shape)
# Step 4: 添加拔模角
drafted_shape = self._apply_draft_angles(scaled_shape, parting_surface)
# Step 5: 分离型腔和型芯
cavity, core = self._split_cavity_core(drafted_shape, parting_surface)
logger.info("模具型腔生成完成")
@@ -140,11 +98,9 @@ class MoldCavityGenerator:
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
)
@@ -158,10 +114,10 @@ class MoldCavityGenerator:
"unit": "mm"
},
"product_analysis": {
"bounding_box": analysis.get("bounding_box", {}), # 使用get方法
"volume": analysis.get("volume", 0), # 使用get方法
"surface_area": analysis.get("surface_area", 0), # 使用get方法
"center_of_mass": analysis.get("center_of_mass", [0, 0, 0]) # 使用get方法
"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,
@@ -219,61 +175,24 @@ class MoldCavityGenerator:
# ==================== 内部方法 ====================
def _analyze_product_geometry(self, shape: Any) -> Dict[str, Any]:
"""分析产品几何属性"""
# 计算体积属性
volume_props = GProp_GProps()
brepgprop.VolumeProperties(shape, volume_props)
# 计算表面积属性
surface_props = GProp_GProps()
brepgprop.SurfaceProperties(shape, surface_props)
# 计算边界框
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 {
"volume": volume_props.Mass(),
"surface_area": surface_props.Mass(),
"center_of_mass": [
volume_props.CentreOfMass().X(),
volume_props.CentreOfMass().Y(),
volume_props.CentreOfMass().Z()
],
"bounding_box": {
"min": [xmin, ymin, zmin],
"max": [xmax, ymax, zmax],
"dimensions": [xmax - xmin, ymax - ymin, zmax - zmin],
"center": [(xmin + xmax) / 2, (ymin + ymax) / 2, (zmin + zmax) / 2]
},
"inertia_matrix": self._get_inertia_matrix(volume_props)
}
def _detect_parting_surface(self, shape: Any, analysis: Dict) -> Tuple[Any, List]:
"""
检测分型面和分型线
优先级:
1. AI 模型检测(如果已设置)
2. 基于法向量分析的几何方法
3. 简化方法(基于边界框)
"""
# 1. 尝试使用 AI 模型
if self.ai_parting_detector is not None:
try:
logger.info("使用 AI 模型检测分型面")
ai_result = self.ai_parting_detector.detect(shape, analysis)
if ai_result:
return self._create_parting_surface_from_ai(ai_result, analysis)
return self._create_parting_surface_from_ai(ai_result, analysis, shape)
except Exception as e:
logger.warning(f"AI 分型面检测失败,回退到几何方法:{e}")
# 2. 基于法向量分析的几何方法
try:
logger.info("使用法向量分析检测分型面")
optimal_direction = self._analyze_face_normals(shape)
@@ -281,173 +200,142 @@ class MoldCavityGenerator:
shape, analysis, optimal_direction
)
parting_surface = BRepBuilderAPI_MakeFace(parting_plane).Face()
# 计算真实分型线(产品与分型面的交线)
parting_line = self._calculate_parting_line(shape, parting_surface)
return parting_surface, parting_line
except Exception as e:
logger.warning(f"法向量分析失败,使用简化方法:{e}")
# 3. 简化方法(回退)
logger.info("使用简化方法检测分型面")
return self._simple_parting_surface(analysis)
return self._simple_parting_surface(shape, analysis)
def _apply_shrinkage_compensation(self, shape: Any) -> Any:
"""应用收缩率补偿(放大模型)"""
scale_factor = 1.0 + self.shrinkage_rate
# 创建缩放变换
trsf = gp_Trsf()
trsf.SetScale(gp_Pnt(0, 0, 0), scale_factor)
from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_Transform
scaled_shape = BRepBuilderAPI_Transform(shape, trsf, True).Shape()
return scaled_shape
def _apply_draft_angles(self, shape: Any, parting_surface: Any) -> Any:
"""添加拔模角(简化实现)"""
# 实际实现需要复杂的拔模面处理
# 这里返回原始形状(假设已在CAD中处理)
logger.warning("拔模角处理为简化实现,建议在设计阶段处理")
return shape
def _split_cavity_core(self, shape: Any, parting_surface: Any) -> Tuple[Any, Any]:
"""分离型腔和型芯
型腔(Cavity): 模具中形成产品外表面的部分,是产品形状的负形
型芯(Core): 模具中形成产品内表面的部分,是产品形状的正形
def _analyze_face_normals(self, shape: Any) -> gp_Dir:
"""
try:
from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox
from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Cut
from OCC.Core.TopExp import TopExp_Explorer
from OCC.Core.TopAbs import TopAbs_SOLID
# 获取产品边界框
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()
# 计算模具块尺寸(比产品大一定余量)
margin = 20 # mm
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()
# 型腔 = 模具块 - 产品(布尔减法)
cavity_operation = BRepAlgoAPI_Cut(mold_block, shape)
if cavity_operation.IsDone():
cavity = cavity_operation.Shape()
logger.info("型腔生成成功(模具块减去产品)")
else:
logger.warning("型腔布尔运算失败,使用原始形状")
cavity = mold_block
# 型芯 = 产品形状本身(收缩补偿后)
core = shape
logger.info("型芯 = 产品形状")
return cavity, core
分析产品表面的法向量分布,找出最优分型方向
except Exception as e:
logger.error(f"型腔分离失败: {e}")
return shape, shape
原理:
- 统计所有面的法向量
- 选择法向量变化最小的方向作为分型方向
- 避免倒扣(undercut)区域
"""
face_normals = []
explorer = TopExp_Explorer(shape, TopAbs_FACE)
def _extract_shape_geometry(self, shape: Any, shape_type: str) -> Dict[str, Any]:
"""提取形状几何数据为JSON格式"""
try:
# 网格化
mesh = BRepMesh_IncrementalMesh(shape, 0.1)
mesh.Perform()
while explorer.More():
face = TopoDS_Face(explorer.Current())
surface = BRepAdaptor_Surface(face)
# 提取顶点和面
from OCC.Core.TopExp import TopExp_Explorer
from OCC.Core.TopAbs import TopAbs_FACE
from OCC.Core.BRep import BRep_Tool
from OCC.Core.Poly import Poly_Triangulation
from OCC.Core.TopLoc import TopLoc_Location
try:
if surface.GetType() == 0:
normal = surface.Plane().Position().Direction()
else:
bbox = Bnd_Box()
brepbndlib_Add(face, bbox)
normal = gp_Dir(0, 0, 1)
vertices = []
faces = []
face_normals.append(normal)
except Exception as e:
logger.debug(f"面法向量计算失败:{e}")
explorer = TopExp_Explorer(shape, TopAbs_FACE)
vertex_index = 0
explorer.Next()
while explorer.More():
# 使用 explorer.Current() 直接获取面
face = explorer.Current()
location = TopLoc_Location()
triangulation = BRep_Tool.Triangulation(face, location)
if not face_normals:
return gp_Dir(0, 0, 1)
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())
])
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)
# 提取三角形面
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)])
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)
vertex_index += nb_nodes
def _create_optimal_parting_plane(self, shape: Any, analysis: Dict,
direction: gp_Dir) -> gp_Pln:
"""
创建最优分型面
explorer.Next()
Args:
shape: 产品形状
analysis: 几何分析结果
direction: 分型方向(法向量)
vertex_count = len(vertices) // 3
face_count = len(faces) // 3
Returns:
gp_Pln: 分型面方程
"""
bbox = analysis["bounding_box"]
center = bbox["center"]
return {
"type": shape_type,
"vertices": vertices,
"faces": faces,
"vertex_count": vertex_count,
"face_count": face_count,
"triangulation": "BRepMesh三角化"
}
parting_plane = gp_Pln(
gp_Pnt(center[0], center[1], center[2]),
direction
)
except Exception as e:
logger.error(f"{shape_type}几何提取失败: {e}")
return {
"type": shape_type,
"vertices": [],
"faces": [],
"vertex_count": 0,
"face_count": 0,
"triangulation": f"提取失败: {str(e)}"
}
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: Any, analysis: Dict) -> Tuple[Any, 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: Any = None) -> Tuple[Any, 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: Any) -> Dict[str, Any]:
"""提取分型面几何数据"""
# 尝试从surface获取边界信息,失败则使用默认值
try:
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface
adaptor = BRepAdaptor_Surface(surface)
u_min, u_max = adaptor.FirstUParameter(), adaptor.LastUParameter()
v_min, v_max = adaptor.FirstVParameter(), adaptor.LastVParameter()
@@ -463,14 +351,6 @@ class MoldCavityGenerator:
"v_range": [-200, 200]
}
# 分型面是水平面,法向量为 [0, 0, 1],原点在 Z 轴中心
return {
"type": "plane",
"normal": [0, 0, 1],
"origin": [0, 0, 0],
"bounds": bounds
}
return {
"type": "plane",
"normal": [0, 0, 1],
@@ -481,23 +361,19 @@ class MoldCavityGenerator:
def _calculate_mold_size(self, analysis: Dict) -> Dict[str, float]:
"""估算模具尺寸"""
product_bbox = analysis["bounding_box"]["dimensions"]
# 模具通常比产品大20-50mm
margin = 30 # mm
margin = 30
return {
"length": product_bbox[0] + 2 * margin,
"width": product_bbox[1] + 2 * margin,
"height": product_bbox[2] + 2 * margin + 100, # 增加100mm用于模架
"height": product_bbox[2] + 2 * margin + 100,
"margin": margin
}
def _calculate_clamping_force(self, analysis: Dict) -> str:
"""估算锁模力"""
volume_cm3 = analysis.get("volume", 0) / 1000 # mm³ → cm³
volume_cm3 = analysis.get("volume", 0) / 1000
# 经验公式: 锁模力 ≈ 投影面积 × 压力 × 安全系数
# 简化估算
if volume_cm3 < 10:
return "50-100 吨"
elif volume_cm3 < 100:
@@ -509,18 +385,8 @@ class MoldCavityGenerator:
def _get_recommended_material(self) -> str:
"""推荐模具材料"""
# 根据产品产量推荐模具材料
# 小批量 (<5000件): 铝合金
# 中批量 (5000-50000件): P20钢
# 大批量 (>50000件): H13钢
return "Aluminum Alloy 7075 (铝合金模具)"
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)
@@ -530,7 +396,6 @@ class MoldCavityGenerator:
avg_thickness = (volume / surface_area) * 0.6
return f"{avg_thickness * 0.7:.2f} - {avg_thickness * 1.3:.2f} mm"
elif volume > 0:
# 如果没有surface_area,基于体积估算
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:
@@ -542,7 +407,6 @@ class MoldCavityGenerator:
def _calculate_complexity_score(self, analysis: Dict) -> float:
"""计算复杂度评分(0-10)"""
# 基于体积、表面积比、边界框等
volume = analysis.get("volume", 0)
surface_area = analysis.get("surface_area", 0)
@@ -551,7 +415,6 @@ class MoldCavityGenerator:
complexity = min(thickness_ratio / 5.0, 10.0)
return round(complexity, 1)
elif volume > 0:
# 如果没有surface_area,基于拓扑复杂度评分
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:
@@ -576,7 +439,6 @@ class MoldCavityGenerator:
def _identify_weld_line_risk(self, analysis: Dict) -> str:
"""识别熔接痕风险"""
# 基于几何复杂度判断
complexity = self._calculate_complexity_score(analysis)
if complexity > 7:
@@ -588,299 +450,4 @@ class MoldCavityGenerator:
def _identify_sink_mark_risk(self, analysis: Dict) -> str:
"""识别缩痕风险"""
thickness = self._estimate_wall_thickness(analysis)
# 简化的风险评估
return "中 - 建议壁厚均匀性检查"
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 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 _analyze_face_normals(self, shape: Any) -> gp_Dir:
"""
分析产品表面的法向量分布,找出最优分型方向
原理:
- 统计所有面的法向量
- 选择法向量变化最小的方向作为分型方向
- 避免倒扣(undercut)区域
"""
from OCC.Core.TopoDS import TopoDS_Compound
from OCC.Core.TopTools import TopTools_IndexedMapOfShape
# 收集所有面的法向量
face_normals = []
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 = gp_Dir()
# 从曲面获取法向量
if surface.GetType() == 0: # Plane
normal = surface.Plane().Position().Direction()
else:
# 对于非平面,使用微分几何计算法向量
from OCC.Core.GCPnts import GCPnts_AbscissaPoint
from OCC.Core.BRepGProp import brepgprop_VolumeProperties
# 简化:使用面的边界框中心法向量
from OCC.Core.Bnd import Bnd_Box
from OCC.Core.BRepBndLib import brepbndlib_Add
bbox = Bnd_Box()
brepbndlib_Add(face, bbox)
center = bbox.Center()
# 估算面法向量(简化)
normal = gp_Dir(0, 0, 1) # 默认 Z 方向
face_normals.append(normal)
except Exception as e:
logger.debug(f"面法向量计算失败:{e}")
explorer.Next()
# 如果没有法向量,返回默认 Z 方向
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: Any, 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 _calculate_parting_line(self, shape: Any, parting_surface: Any) -> List[List[float]]:
"""
计算真实的分型线(产品与分型面的交线)
使用 BRepAlgoAPI_Section 进行布尔运算求交
"""
try:
# 创建截面运算
section = BRepAlgoAPI_Section(shape, parting_surface)
section.Build()
if not section.IsDone():
logger.warning("截面运算未完成,使用简化分型线")
return self._simple_parting_line(
parting_surface,
{"bounding_box": {"min": [-50, -50, 0], "max": [50, 50, 100]}}
)
# 提取交线(边)
edges = []
explorer = TopExp_Explorer(section.Shape(), TopAbs_EDGE)
while explorer.More():
edge = TopoDS_Edge(explorer.Current())
# 从边提取点
curve = BRepAdaptor_Curve(edge)
first_param = curve.FirstParameter()
last_param = curve.LastParameter()
# 采样点(至少 10 个点)
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(
parting_surface,
{"bounding_box": {"min": [-50, -50, 0], "max": [50, 50, 100]}}
)
logger.info(f"计算得到 {len(edges)} 个分型线点")
return edges
except Exception as e:
logger.error(f"分型线计算失败:{e}")
return self._simple_parting_line(
parting_surface,
{"bounding_box": {"min": [-50, -50, 0], "max": [50, 50, 100]}}
)
def _simple_parting_surface(self, analysis: Dict) -> Tuple[Any, List]:
"""简化的分型面检测(回退方案)"""
bbox = analysis["bounding_box"]
center_z = bbox["center"][2]
# 创建分型面(XY 平面)
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(parting_surface, analysis)
return parting_surface, parting_line
def _simple_parting_line(self, parting_surface: Any, analysis: Dict) -> List[List[float]]:
"""简化的分型线(矩形)"""
bbox = analysis["bounding_box"]
center_z = bbox["center"][2]
return [
[bbox["min"][0], bbox["min"][1], center_z],
[bbox["max"][0], bbox["min"][1], center_z],
[bbox["max"][0], bbox["max"][1], center_z],
[bbox["min"][0], bbox["max"][1], center_z],
[bbox["min"][0], bbox["min"][1], center_z]
]
def _create_parting_surface_from_ai(self, ai_result: Dict,
analysis: Dict) -> Tuple[Any, List]:
"""
从 AI 模型结果创建分型面(预留接口)
Args:
ai_result: AI 模型输出,应包含:
- origin: [x, y, z] 平面原点
- normal: [nx, ny, nz] 法向量
analysis: 几何分析结果
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()
# 分型线可以使用 AI 结果或重新计算
if "parting_line" in ai_result:
parting_line = ai_result["parting_line"]
else:
parting_line = self._simple_parting_line(parting_surface, analysis)
logger.info(f"从 AI 结果创建分型面:原点={origin}, 法向量={normal}")
return parting_surface, parting_line
def _apply_draft_angles(self, shape: Any, parting_surface: Any) -> Any:
"""
添加拔模角
使用 OpenCASCADE 的拔模功能
"""
# 1. 尝试使用 AI 模型
if self.ai_draft_analyzer is not None:
try:
logger.info("使用 AI 模型分析拔模角")
ai_result = self.ai_draft_analyzer.analyze(shape, parting_surface, self.draft_angle)
if ai_result and "drafted_shape" in ai_result:
logger.info("AI 拔模分析成功")
return ai_result["drafted_shape"]
except Exception as e:
logger.warning(f"AI 拔模分析失败,回退到几何方法:{e}")
# 2. 几何方法(简化实现)
try:
# 获取分型面的法向量作为拔模方向
surface_adaptor = BRepAdaptor_Surface(parting_surface)
draft_direction = surface_adaptor.Plane().Position().Direction()
# 使用 BRepOffsetAPI_ThickSolid 创建拔模
# 注意:完整的拔模需要更复杂的实现,这里简化处理
logger.info(f"使用几何方法添加拔模角:{self.draft_angle}度,方向=({draft_direction.X():.3f}, {draft_direction.Y():.3f}, {draft_direction.Z():.3f})")
# 简化:直接返回原始形状(拔模已在 CAD 中处理)
# 完整实现需要使用 BRepOffsetAPI_DraftAngle
return shape
except Exception as e:
logger.warning(f"拔模角处理失败:{e}")
return shape
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
+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 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
+1 -9
View File
@@ -4,16 +4,8 @@
提供分模面质量检测、模具结构合理性评估、生产可行性分析等功能
"""
from typing import Dict, List, Any, Tuple
from typing import Dict, List, Any
import numpy as np
from OCC.Core.BRep import BRep_Tool
from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh
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_Add
from OCC.Core.gp import gp_Dir
from utils.logger import get_logger
+588
View File
@@ -0,0 +1,588 @@
"""
冷却/浇注系统自动设计模块
功能:
1. 冷却系统设计 - 水路布局、直径、间距
2. 浇注系统设计 - 主流道、分流道、浇口
3. 热力学估算 - 冷却时间、温度分布
4. 排气系统设计 - 排气槽、排气针位置
设计依据:
- 模具尺寸和产品几何
- 材料热物性参数
- 生产节拍要求
- 行业标准规范
"""
from typing import Dict, List, Any, Optional
import math
from 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))
+533
View File
@@ -0,0 +1,533 @@
"""
侧壁/倒扣面滑块机构检测与设计模块
功能:
1. 倒扣区域检测 - 识别无法直接脱模的侧壁凹槽
2. 滑块机构设计 - 侧向分型抽芯机构
3. 斜顶机构设计 - 内侧倒扣的斜顶脱模机构
4. 机构运动学分析 - 抽芯行程、脱模角度计算
倒扣检测原理:
- 分型方向确定后,检查每个面的法向量
- 如果面的法向量与脱模方向的点积为负(面朝向脱模反方向)
且该面不在分型面上,则判定为倒扣面
- 根据倒扣面的位置(外侧/内侧)选择滑块或斜顶
滑块 vs 斜顶:
- 滑块:外侧倒扣,沿导滑槽侧向运动
- 斜顶:内侧倒扣,沿斜导柱内侧运动
"""
from typing import Dict, List, Any, Optional, Tuple
import math
import numpy as np
from utils.logger import get_logger
logger = get_logger(__name__)
class UndercutDetector:
"""倒扣区域检测器"""
def detect_undercuts(self, shape: Any, parting_direction: List[float],
parting_surface: Any = 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
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_Add
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": "hydraulic" 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: Any, parting_direction: List[float],
mold_size: Dict, parting_surface: Any = 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"],
"has_hydraulic": any(s.get("actuation") == "hydraulic" for s in slider_mechanisms),
}
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["has_hydraulic"]:
recs.append("大行程抽芯需使用液压系统,需配置液压站")
if summary["complexity"] == "very_complex":
recs.append("侧向分型机构复杂,建议评估是否可通过产品修改简化")
recs.append("考虑使用二次分型或旋转脱模替代方案")
if summary["total_mechanism_count"] > 3:
recs.append("侧向机构较多,建议优化模具结构减少机构数量")
recs.append("所有侧向机构需做运动仿真验证干涉")
return recs