344 lines
14 KiB
Python
344 lines
14 KiB
Python
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,
|
|
}
|