from typing import Dict, Any, List, Optional from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeFace from OCC.Core.BRepGProp import brepgprop from OCC.Core.GProp import GProp_GProps from OCC.Core.gp import gp_Dir, gp_Pln, gp_Pnt from OCC.Core.TopAbs import TopAbs_FACE from OCC.Core.TopExp import TopExp_Explorer from OCC.Core.TopoDS import TopoDS_Face, topods from core.mold_generator import MoldCavityGenerator from core.aluminum_foam_mold import AluminumFoamMoldGenerator from core.parting_candidate_generator import PartingCandidateGenerator from core.parting_scheme_scorer import PartingSchemeScorer from utils.logger import get_logger logger = get_logger(__name__) class MultiSchemeMoldPlanner: """针对单个产品生成最多三套候选分模方案并排序。""" def __init__(self): self.candidate_generator = PartingCandidateGenerator() self.scheme_scorer = PartingSchemeScorer() self.mold_generator = MoldCavityGenerator(shrinkage_rate=0.005) self.aluminum_foam_generator = AluminumFoamMoldGenerator( shrinkage_rate=0.015, draft_angle=3.0, ) def generate_plan( self, shape: Any, material: Dict[str, Any], is_foam_material: bool = False, max_schemes: int = 3, process_params: Optional[Dict[str, Any]] = None, ) -> Dict[str, Any]: generator = self.aluminum_foam_generator if is_foam_material else self.mold_generator generator.set_material(material["name"]) self._apply_process_params(generator, material, process_params) analysis = generator._analyze_product_geometry(shape) analysis["axis_normal_stats"] = self._collect_axis_normal_stats(generator, shape) candidates = self.candidate_generator.generate_candidates( analysis=analysis, is_foam_material=is_foam_material, max_candidates=max_schemes, ) schemes = [] for candidate in candidates: for offset_variant in self._build_offset_variants(candidate, is_foam_material): try: scheme = self._build_scheme( generator=generator, shape=shape, analysis=analysis, candidate=offset_variant, is_foam_material=is_foam_material, ) if scheme is not None: schemes.append(scheme) except Exception as exc: logger.warning(f"候选方案 {offset_variant.get('scheme_id')} 生成失败: {exc}") if not schemes: raise ValueError("未能生成任何可用分模方案") scored_schemes = self.scheme_scorer.score_schemes(schemes)[:max_schemes] export_shapes = {} for idx, scheme in enumerate(scored_schemes, start=1): scheme["raw_scheme_id"] = scheme.get("scheme_id") scheme["scheme_id"] = f"scheme_{idx}" if scheme.get("cavity_data", {}).get("metadata") is not None: scheme["cavity_data"]["metadata"]["scheme_id"] = scheme["scheme_id"] scheme["cavity_data"]["metadata"]["process_parameters"] = dict(process_params or {}) export_shapes[scheme["scheme_id"]] = scheme.pop("_export_shapes", {}) best_scheme = scored_schemes[0] return { "best_scheme_id": best_scheme["scheme_id"], "candidate_schemes": scored_schemes, "_export_shapes": export_shapes, "global_summary": { "scheme_count": len(scored_schemes), "recommended_reason": best_scheme.get("summary", ""), }, } def _build_scheme( self, generator: Any, shape: Any, analysis: Dict[str, Any], candidate: Dict[str, Any], is_foam_material: bool, ) -> Optional[Dict[str, Any]]: parting_surface = self._build_parting_surface( generator, analysis, candidate["direction"], shape, candidate.get("offset_ratio", 0.0), candidate.get("opening_span_mm"), ) parting_line = generator.optimize_parting_line( generator._calculate_parting_line(shape, parting_surface) ) parting_direction = candidate["direction"] side_action_result = generator.side_action_designer.analyze_and_design( shape=shape, parting_direction=parting_direction, mold_size=generator._calculate_mold_size(analysis), parting_surface=parting_surface, ) undercut_regions = generator._build_undercut_regions( side_action_result.get("undercut_analysis", {}) ) mold_structure = self._determine_mold_structure(analysis, undercut_regions) scaled_shape = generator._apply_shrinkage_compensation(shape) drafted_shape = generator._apply_draft_angles(scaled_shape, parting_surface) cavity, core = generator._split_cavity_core(drafted_shape, parting_surface) cavity_result = { "cavity": cavity, "core": core, "parting_surface": parting_surface, "parting_line": parting_line, "analysis": analysis, "undercut_regions": undercut_regions, "side_actions": side_action_result, } if is_foam_material: cavity_result["mold_block"] = generator._generate_mold_block(cavity, analysis) cavity_result["parting_surfaces"] = { "primary_surface": parting_surface, "primary_line": parting_line, "primary_direction": parting_direction, "method": candidate["method"], "offset_ratio": candidate.get("offset_ratio", 0.0), } cavity_result["material"] = generator.foam_material cavity_result["shrinkage_applied"] = generator.shrinkage_rate cavity_result["draft_angle_applied"] = generator.draft_angle cavity_data = generator.generate_detailed_cavity_json(cavity_result) key_info = generator.generate_cavity_key_info(cavity_result) cavity_data.setdefault("metadata", {}) cavity_data["metadata"]["scheme_id"] = candidate["scheme_id"] cavity_data["metadata"]["scheme_method"] = candidate["method"] cavity_data["metadata"]["scheme_axis"] = candidate["axis"] cavity_data["metadata"]["scheme_reason"] = candidate["reason"] cavity_data["metadata"]["scheme_offset_ratio"] = candidate.get("offset_ratio", 0.0) cavity_data["metadata"]["scheme_offset_label"] = candidate.get("offset_label", "中面") cavity_data["metadata"]["mold_structure_type"] = mold_structure["mold_structure_type"] cavity_data["metadata"]["core_required"] = mold_structure["core_required"] cavity_data["metadata"]["structure_decision_reason"] = mold_structure["decision_reason"] return { "scheme_id": candidate["scheme_id"], "method": candidate["method"], "axis": candidate["axis"], "title": candidate["title"], "reason": candidate["reason"], "priority_score": candidate.get("priority_score"), "normal_alignment_score": candidate.get("normal_alignment_score"), "offset_ratio": candidate.get("offset_ratio", 0.0), "offset_label": candidate.get("offset_label", "中面"), "mold_structure_type": mold_structure["mold_structure_type"], "core_required": mold_structure["core_required"], "decision_reason": mold_structure["decision_reason"], "parting": { "axis": candidate["axis"], "direction": parting_direction, "line": parting_line, "surface": cavity_data.get("parting_surface", {}), }, "undercut_regions": undercut_regions, "side_actions": side_action_result, "cavity_data": cavity_data, "key_info": key_info, "_export_shapes": { "cavity": cavity, "core": core, "parting_surface": parting_surface, }, } @staticmethod def _apply_process_params(generator: Any, material: Dict[str, Any], process_params: Optional[Dict[str, Any]]): params = process_params or {} draft_angle = float(params.get("draft_angle", getattr(generator, "draft_angle", 2.0))) shrinkage_rate = float(params.get("shrinkage_rate", material.get("shrinkage", 0.005) * 100.0)) / 100.0 parting_precision = float(params.get("parting_precision", getattr(generator, "parting_line_tolerance", 0.1))) cavity_match = float(params.get("cavity_match", getattr(generator, "cavity_match_rate", 95.0))) generator.draft_angle = draft_angle generator.shrinkage_rate = shrinkage_rate generator.parting_line_tolerance = parting_precision generator.cavity_match_rate = cavity_match def _build_parting_surface( self, generator: Any, analysis: Dict[str, Any], direction_vector: List[float], shape: Any, offset_ratio: float = 0.0, opening_span_mm: Optional[float] = None, ) -> Any: center = analysis.get("bounding_box", {}).get("center", [0, 0, 0]) dims = analysis.get("bounding_box", {}).get("dimensions", [100, 100, 100]) span = max(dims) * 1.5 + 30 opening_span = opening_span_mm or max(dims) offset_distance = float(opening_span) * float(offset_ratio) origin = [ center[0] + direction_vector[0] * offset_distance, center[1] + direction_vector[1] * offset_distance, center[2] + direction_vector[2] * offset_distance, ] plane = gp_Pln( gp_Pnt(origin[0], origin[1], origin[2]), gp_Dir(direction_vector[0], direction_vector[1], direction_vector[2]), ) parting_surface = BRepBuilderAPI_MakeFace( plane, -span, span, -span, span, ).Face() return generator.extend_parting_surface(parting_surface, shape, extension=30.0) def _build_offset_variants( self, candidate: Dict[str, Any], is_foam_material: bool, ) -> List[Dict[str, Any]]: opening_span = float(candidate.get("opening_span_mm", 0.0)) if opening_span <= 0: return [dict(candidate)] ratios = [0.0, -0.12, 0.12] if is_foam_material and candidate.get("axis") == "Z": ratios = [0.0, -0.08, 0.08] variants = [] for ratio in ratios: variant = dict(candidate) label = "中面" id_label = "center" if ratio < 0: label = "偏下" if candidate.get("axis") == "Z" else "负向偏移" id_label = "neg" elif ratio > 0: label = "偏上" if candidate.get("axis") == "Z" else "正向偏移" id_label = "pos" variant["scheme_id"] = f"{candidate.get('axis', 'A').lower()}_{id_label}_{abs(ratio):.2f}" variant["offset_ratio"] = ratio variant["offset_label"] = label variant["reason"] = f"{candidate.get('reason', '')},分型面位置: {label}" variants.append(variant) return variants def _collect_axis_normal_stats(self, generator: Any, shape: Any) -> Dict[str, float]: """按坐标轴统计面法向分布强度,用于候选方向排序。""" stats = {"X": 0.0, "Y": 0.0, "Z": 0.0} explorer = TopExp_Explorer(shape, TopAbs_FACE) while explorer.More(): face = topods.Face(explorer.Current()) explorer.Next() try: normal = generator._get_face_normal(face) if normal is None: continue props = GProp_GProps() brepgprop.SurfaceProperties(face, props) area = max(float(props.Mass()), 1.0) stats["X"] += abs(float(normal.X())) * area stats["Y"] += abs(float(normal.Y())) * area stats["Z"] += abs(float(normal.Z())) * area except Exception as exc: logger.debug(f"统计面法向失败: {exc}") total = stats["X"] + stats["Y"] + stats["Z"] if total <= 0: return {"X": 33.3, "Y": 33.3, "Z": 33.4} return { axis: round(value / total * 100, 2) for axis, value in stats.items() } @staticmethod def _determine_mold_structure(analysis: Dict[str, Any], undercut_regions: List[Dict[str, Any]]) -> Dict[str, Any]: """ 判定是否需要独立模芯。 规则为工程启发式: - 实心度高 + 平均厚度占比高 + 无明显倒扣:倾向两板半腔(无独立凸芯) - 否则:采用型腔+模芯结构 """ dims = analysis.get("bounding_box", {}).get("dimensions", [0.0, 0.0, 0.0]) valid_dims = [float(d) for d in dims if float(d) > 1e-6] min_dim = min(valid_dims) if valid_dims else 1.0 bbox_volume = 1.0 for dim in valid_dims[:3]: bbox_volume *= dim if bbox_volume <= 0: bbox_volume = 1.0 volume = float(analysis.get("volume", 0.0)) surface_area = float(analysis.get("surface_area", 0.0)) solid_ratio = max(0.0, min(volume / bbox_volume, 1.0)) avg_wall = (2.0 * volume / surface_area) if surface_area > 1e-6 else min_dim wall_ratio = max(0.0, min(avg_wall / max(min_dim, 1e-6), 1.0)) undercut_count = len(undercut_regions or []) core_required = not (solid_ratio > 0.62 and wall_ratio > 0.38 and undercut_count == 0) mold_structure_type = "cavity_core" if core_required else "two_half_cavity" decision_reason = ( f"solid_ratio={solid_ratio:.2f}, wall_ratio={wall_ratio:.2f}, " f"undercut_count={undercut_count}" ) return { "core_required": core_required, "mold_structure_type": mold_structure_type, "decision_reason": decision_reason, }