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