""" 侧壁/倒扣面滑块机构检测与设计模块 功能: 1. 倒扣区域检测 - 识别无法直接脱模的侧壁凹槽 2. 滑块机构设计 - 侧向分型抽芯机构 3. 斜顶机构设计 - 内侧倒扣的斜顶脱模机构 4. 机构运动学分析 - 抽芯行程、脱模角度计算 倒扣检测原理: - 分型方向确定后,检查每个面的法向量 - 如果面的法向量与脱模方向的点积为负(面朝向脱模反方向) 且该面不在分型面上,则判定为倒扣面 - 根据倒扣面的位置(外侧/内侧)选择滑块或斜顶 滑块 vs 斜顶: - 滑块:外侧倒扣,沿导滑槽侧向运动 - 斜顶:内侧倒扣,沿斜导柱内侧运动 """ from typing import Dict, List, Any, Optional, Tuple import math import numpy as np from OCC.Core.TopoDS import TopoDS_Shape, TopoDS_Face from shared.utils.logger import get_logger logger = get_logger(__name__) class UndercutDetector: """倒扣区域检测器""" def detect_undercuts(self, shape: TopoDS_Shape, parting_direction: List[float], parting_surface: Optional[TopoDS_Face] = None) -> Dict[str, Any]: """ 检测产品中的倒扣区域 Args: shape: OCC 产品形状 parting_direction: 分型方向 [nx, ny, nz] parting_surface: 分型面(可选) Returns: { "undercut_faces": List[Dict], "slider_regions": List[Dict], "lifter_regions": List[Dict], "total_undercut_area": float, "requires_slider": bool, "requires_lifter": bool, "complexity": str } """ try: from OCC.Core.TopExp import TopExp_Explorer from OCC.Core.TopAbs import TopAbs_FACE from OCC.Core.TopoDS import TopoDS_Face, topods from OCC.Core.BRepAdaptor import BRepAdaptor_Surface from OCC.Core.GProp import GProp_GProps from OCC.Core.BRepGProp import brepgprop from OCC.Core.Bnd import Bnd_Box from OCC.Core.BRepBndLib import brepbndlib from OCC.Core.gp import gp_Dir dir_vec = np.array(parting_direction, dtype=np.float64) dir_norm = np.linalg.norm(dir_vec) if dir_norm < 1e-6: dir_vec = np.array([0, 0, 1]) else: dir_vec /= dir_norm parting_dir = gp_Dir(dir_vec[0], dir_vec[1], dir_vec[2]) undercut_faces = [] slider_regions = [] lifter_regions = [] total_undercut_area = 0.0 parting_z = 0.0 if parting_surface is not None: try: surface = BRepAdaptor_Surface(parting_surface) if surface.GetType() == 0: parting_z = surface.Plane().Location().Z() except Exception: pass explorer = TopExp_Explorer(shape, TopAbs_FACE) face_idx = 0 while explorer.More(): face = topods.Face(explorer.Current()) face_idx += 1 try: surface = BRepAdaptor_Surface(face) u = (surface.FirstUParameter() + surface.LastUParameter()) / 2 v = (surface.FirstVParameter() + surface.LastVParameter()) / 2 face_normal = None if surface.GetType() == 0: face_normal = surface.Plane().Position().Direction() else: from OCC.Core.BRepLProp import BRepLProp_SLProps props = BRepLProp_SLProps(surface, 1, 0.001) props.SetParameters(u, v) if props.IsNormalDefined(): face_normal = props.Normal() if face_normal is None: explorer.Next() continue dot = face_normal.Dot(parting_dir) face_props = GProp_GProps() brepgprop.SurfaceProperties(face, face_props) area = face_props.Mass() center = face_props.CentreOfMass() bbox = Bnd_Box() brepbndlib.Add(face, bbox) try: fxmin, fymin, fzmin, fxmax, fymax, fzmax = bbox.Get() except Exception: fxmin, fymin, fzmin, fxmax, fymax, fzmax = 0, 0, 0, 0, 0, 0 if dot < -0.1: face_center_z = center.Z() is_outer = face_center_z >= parting_z undercut_info = { "face_index": face_idx, "normal": [face_normal.X(), face_normal.Y(), face_normal.Z()], "dot_product": float(dot), "area": float(area), "center": [float(center.X()), float(center.Y()), float(center.Z())], "bbox": { "min": [float(fxmin), float(fymin), float(fzmin)], "max": [float(fxmax), float(fymax), float(fzmax)] }, "severity": "high" if dot < -0.5 else "medium", "is_outer": is_outer, } undercut_faces.append(undercut_info) total_undercut_area += area except Exception: pass explorer.Next() for uf in undercut_faces: normal = np.array(uf["normal"]) lateral_component = normal - np.dot(normal, dir_vec) * dir_vec lateral_norm = np.linalg.norm(lateral_component) if lateral_norm > 0.01: slide_direction = lateral_component / lateral_norm else: slide_direction = np.array([1, 0, 0]) mechanism = { "face_indices": [uf["face_index"]], "slide_direction": slide_direction.tolist(), "area": uf["area"], "center": uf["center"], "severity": uf["severity"], } if uf["is_outer"]: slider_regions.append(mechanism) else: lifter_regions.append(mechanism) requires_slider = len(slider_regions) > 0 requires_lifter = len(lifter_regions) > 0 total_count = len(slider_regions) + len(lifter_regions) if total_count == 0: complexity = "simple" elif total_count <= 2: complexity = "moderate" elif total_count <= 4: complexity = "complex" else: complexity = "very_complex" result = { "undercut_faces": undercut_faces, "slider_regions": slider_regions, "lifter_regions": lifter_regions, "total_undercut_area": total_undercut_area, "requires_slider": requires_slider, "requires_lifter": requires_lifter, "complexity": complexity, "parting_direction": parting_direction, } logger.info(f"倒扣检测完成: {len(undercut_faces)} 个倒扣面, " f"{len(slider_regions)} 个滑块, {len(lifter_regions)} 个斜顶, " f"复杂度={complexity}") return result except Exception as e: logger.error(f"倒扣检测失败: {e}") return { "undercut_faces": [], "slider_regions": [], "lifter_regions": [], "total_undercut_area": 0, "requires_slider": False, "requires_lifter": False, "complexity": "unknown", "parting_direction": parting_direction, } class SliderMechanismDesigner: """滑块机构设计器""" def design_slider(self, slider_region: Dict, mold_size: Dict, parting_direction: List[float]) -> Dict[str, Any]: """ 设计滑块机构 Args: slider_region: 倒扣区域信息 mold_size: 模具尺寸 parting_direction: 分型方向 Returns: 滑块机构设计方案 """ center = slider_region["center"] area = slider_region["area"] slide_dir = slider_region["slide_direction"] slide_stroke = self._calculate_slide_stroke(slider_region, mold_size) slide_angle = self._calculate_slide_angle(slide_dir, parting_direction) slide_block_size = self._calculate_slide_block_size(area, slide_stroke) guide_type = self._select_guide_type(slide_stroke, slide_angle) return { "type": "slider", "location": center, "slide_direction": slide_dir, "slide_stroke": slide_stroke, "slide_angle": slide_angle, "block_size": slide_block_size, "guide_type": guide_type, "locking_mechanism": self._select_locking(slide_angle), "actuation": "pneumatic" if slide_stroke > 50 else "mechanical", "components": self._generate_components(slide_block_size, guide_type), "manufacturing_notes": self._generate_slider_notes(slide_angle, slide_stroke), } def _calculate_slide_stroke(self, region: Dict, mold_size: Dict) -> float: """计算抽芯行程""" bbox = region.get("bbox", {}) if "max" in bbox and "min" in bbox: max_dim = max( abs(bbox["max"][0] - bbox["min"][0]), abs(bbox["max"][1] - bbox["min"][1]), abs(bbox["max"][2] - bbox["min"][2]) ) else: max_dim = 10.0 stroke = max_dim + 5.0 return round(max(stroke, 10.0), 1) def _calculate_slide_angle(self, slide_dir: List[float], parting_dir: List[float]) -> float: """计算滑块倾斜角度""" s = np.array(slide_dir) p = np.array(parting_dir) s_norm = np.linalg.norm(s) p_norm = np.linalg.norm(p) if s_norm < 1e-6 or p_norm < 1e-6: return 90.0 cos_angle = np.clip(np.dot(s, p) / (s_norm * p_norm), -1, 1) angle = math.degrees(math.acos(abs(cos_angle))) return round(angle, 1) def _calculate_slide_block_size(self, area: float, stroke: float) -> Dict[str, float]: """计算滑块尺寸""" width = max(math.sqrt(area) * 1.5, 15.0) height = max(math.sqrt(area) * 1.2, 12.0) length = stroke + width * 0.5 return { "width": round(width, 1), "height": round(height, 1), "length": round(length, 1), } def _select_guide_type(self, stroke: float, angle: float) -> str: """选择导滑方式""" if stroke > 80: return "T_slot_guide" elif angle > 20: return "angled_guide_pin" else: return "dovetail_guide" def _select_locking(self, angle: float) -> str: """选择锁紧方式""" if angle > 25: return "wedge_block" else: return "lock_block" def _generate_components(self, block_size: Dict, guide_type: str) -> List[Dict]: """生成滑块组件清单""" components = [ {"name": "slide_block", "material": "P20", "hardness": "HRC 28-32"}, {"name": "guide_strip", "material": "bronze", "hardness": "HB 80-100"}, {"name": "wear_plate", "material": "T8", "hardness": "HRC 45-50"}, {"name": "return_spring", "material": "spring_steel", "spec": "standard"}, ] if guide_type == "T_slot_guide": components.append({"name": "T_slot_insert", "material": "P20", "hardness": "HRC 28-32"}) elif guide_type == "angled_guide_pin": components.append({"name": "guide_pin", "material": "SUJ2", "hardness": "HRC 58-62"}) elif guide_type == "dovetail_guide": components.append({"name": "dovetail_block", "material": "P20", "hardness": "HRC 28-32"}) return components def _generate_slider_notes(self, angle: float, stroke: float) -> List[str]: """生成滑块加工注意事项""" notes = [] if angle > 25: notes.append("滑块角度较大,需确保锁紧可靠") if stroke > 50: notes.append("抽芯行程较长,建议使用气动抽芯") if stroke > 80: notes.append("大行程抽芯,需校核导滑槽强度") notes.append("滑块需设置限位装置,防止脱出") notes.append("配合面需做耐磨处理") return notes class LifterMechanismDesigner: """斜顶机构设计器""" def design_lifter(self, lifter_region: Dict, mold_size: Dict, parting_direction: List[float]) -> Dict[str, Any]: """ 设计斜顶机构 Args: lifter_region: 内侧倒扣区域信息 mold_size: 模具尺寸 parting_direction: 分型方向 Returns: 斜顶机构设计方案 """ center = lifter_region["center"] area = lifter_region["area"] lifter_angle = self._calculate_lifter_angle(lifter_region) lifter_stroke = self._calculate_lifter_stroke(lifter_region) lifter_size = self._calculate_lifter_size(area, lifter_stroke, lifter_angle) return { "type": "lifter", "location": center, "lifter_angle": lifter_angle, "lifter_stroke": lifter_stroke, "block_size": lifter_size, "guide_type": "angled_hole", "return_mechanism": "spring_return", "components": self._generate_lifter_components(lifter_size), "manufacturing_notes": self._generate_lifter_notes(lifter_angle), } def _calculate_lifter_angle(self, region: Dict) -> float: """基于倒扣深度和估算顶出行程计算斜顶角度(通常 5-15 度)。 angle = atan(undercut_depth / estimated_stroke) """ bbox = region.get("bbox", {}) if "max" in bbox and "min" in bbox: lateral = max( abs(bbox["max"][0] - bbox["min"][0]), abs(bbox["max"][1] - bbox["min"][1]), abs(bbox["max"][2] - bbox["min"][2]), ) else: lateral = 5.0 undercut_depth = lateral * 0.4 estimated_stroke = max(undercut_depth * 3, 20.0) angle = math.degrees(math.atan(undercut_depth / estimated_stroke)) return round(max(min(angle, 15.0), 5.0), 1) def _calculate_lifter_stroke(self, region: Dict) -> float: """计算斜顶行程""" bbox = region.get("bbox", {}) if "max" in bbox and "min" in bbox: max_dim = max( abs(bbox["max"][i] - bbox["min"][i]) for i in range(3) ) else: max_dim = 5.0 return round(max(max_dim + 3.0, 8.0), 1) def _calculate_lifter_size(self, area: float, stroke: float, angle: float) -> Dict[str, float]: """计算斜顶尺寸""" width = max(math.sqrt(area) * 1.2, 10.0) height = stroke / math.sin(math.radians(angle)) if angle > 0 else stroke * 3 thickness = max(width * 0.6, 8.0) return { "width": round(width, 1), "height": round(height, 1), "thickness": round(thickness, 1), } def _generate_lifter_components(self, size: Dict) -> List[Dict]: """生成斜顶组件清单""" return [ {"name": "lifter_body", "material": "P20", "hardness": "HRC 28-32"}, {"name": "guide_pin", "material": "SUJ2", "hardness": "HRC 58-62"}, {"name": "return_spring", "material": "spring_steel", "spec": "standard"}, {"name": "wear_bushing", "material": "bronze", "hardness": "HB 80-100"}, ] def _generate_lifter_notes(self, angle: float) -> List[str]: """生成斜顶加工注意事项""" notes = [] if angle > 12: notes.append("斜顶角度偏大,需校核脱模力") notes.append("斜顶导滑孔需精确加工") notes.append("斜顶头部需做耐磨处理") notes.append("需设置限位防止斜顶脱出") return notes class SideActionDesigner: """侧向分型机构综合设计器""" def __init__(self): self.undercut_detector = UndercutDetector() self.slider_designer = SliderMechanismDesigner() self.lifter_designer = LifterMechanismDesigner() def analyze_and_design(self, shape: TopoDS_Shape, parting_direction: List[float], mold_size: Dict, parting_surface: Optional[TopoDS_Face] = None) -> Dict[str, Any]: """ 综合分析倒扣并设计侧向分型机构 Returns: { "undercut_analysis": Dict, "slider_mechanisms": List[Dict], "lifter_mechanisms": List[Dict], "summary": Dict, "recommendations": List[str] } """ logger.info("开始侧向分型机构分析...") undercut_result = self.undercut_detector.detect_undercuts( shape, parting_direction, parting_surface ) slider_mechanisms = [] for region in undercut_result["slider_regions"]: slider = self.slider_designer.design_slider( region, mold_size, parting_direction ) slider_mechanisms.append(slider) lifter_mechanisms = [] for region in undercut_result["lifter_regions"]: lifter = self.lifter_designer.design_lifter( region, mold_size, parting_direction ) lifter_mechanisms.append(lifter) total_mechanisms = len(slider_mechanisms) + len(lifter_mechanisms) summary = { "total_undercut_faces": len(undercut_result["undercut_faces"]), "total_slider_count": len(slider_mechanisms), "total_lifter_count": len(lifter_mechanisms), "total_mechanism_count": total_mechanisms, "complexity": undercut_result["complexity"], } recommendations = self._generate_overall_recommendations(summary, undercut_result) result = { "undercut_analysis": undercut_result, "slider_mechanisms": slider_mechanisms, "lifter_mechanisms": lifter_mechanisms, "summary": summary, "recommendations": recommendations, } logger.info(f"侧向分型机构设计完成: {len(slider_mechanisms)} 个滑块, " f"{len(lifter_mechanisms)} 个斜顶") return result def _generate_overall_recommendations(self, summary: Dict, undercut: Dict) -> List[str]: """生成总体建议""" recs = [] if summary["total_mechanism_count"] == 0: recs.append("无倒扣区域,模具结构简单,无需侧向分型机构") return recs if summary["total_slider_count"] > 0: recs.append(f"需要 {summary['total_slider_count']} 个滑块机构处理外侧倒扣") if summary["total_lifter_count"] > 0: recs.append(f"需要 {summary['total_lifter_count']} 个斜顶机构处理内侧倒扣") if summary["total_slider_count"] > 0: recs.append("如存在大行程滑块,建议优先评估气动抽芯回路并预留稳定供气") if summary["complexity"] == "very_complex": recs.append("侧向分型机构复杂,建议评估是否可通过产品修改简化") recs.append("考虑使用二次分型或旋转脱模替代方案") if summary["total_mechanism_count"] > 3: recs.append("侧向机构较多,建议优化模具结构减少机构数量") recs.append("所有侧向机构需做运动仿真验证干涉") return recs