From f6d882907123abecee2788a37423f765f775d717 Mon Sep 17 00:00:00 2001 From: chenjw28 <792430652@qq.com> Date: Tue, 16 Jun 2026 11:15:03 +0800 Subject: [PATCH] x --- src/moldinsight/api/cam_router.py | 2 +- src/moldinsight/core/base_mold_generator.py | 2 +- src/moldinsight/core/mold_cam.py | 396 +++++++++++++++--- .../services/cam_bundle_service.py | 22 + .../services/processing_service.py | 2 +- 5 files changed, 365 insertions(+), 59 deletions(-) diff --git a/src/moldinsight/api/cam_router.py b/src/moldinsight/api/cam_router.py index e8be8be..68bf25c 100644 --- a/src/moldinsight/api/cam_router.py +++ b/src/moldinsight/api/cam_router.py @@ -17,7 +17,7 @@ DEFAULT_CAM_PREFERENCES = { "mold_steel": "P20", "surface_quality": "standard", "controller": "fanuc", - "include_gcode": False, + "include_gcode": True, } diff --git a/src/moldinsight/core/base_mold_generator.py b/src/moldinsight/core/base_mold_generator.py index 7edc64b..437f7a4 100644 --- a/src/moldinsight/core/base_mold_generator.py +++ b/src/moldinsight/core/base_mold_generator.py @@ -503,7 +503,7 @@ class BaseMoldGenerator: ) logger.info("回退方案型腔/型芯分离完成") - return cavity or mold_block, core + return cavity, core except Exception as e: logger.error(f"分模回退方案失败: {e}") diff --git a/src/moldinsight/core/mold_cam.py b/src/moldinsight/core/mold_cam.py index 9139310..2c29a45 100644 --- a/src/moldinsight/core/mold_cam.py +++ b/src/moldinsight/core/mold_cam.py @@ -244,44 +244,41 @@ class RoughingToolpathGenerator: tool: Dict, cutting_params: Dict, stock_allowance: float = 0.5) -> Dict[str, Any]: """ - Z层等高粗加工 + 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 + cx_min, cy_min, cz_min = cavity_bbox.get("min", [0, 0, 0]) + cx_max, cy_max, cz_max = cavity_bbox.get("max", [0, 0, 0]) + total_depth = cz_max - cz_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 = [] + path_points = [] + for i in range(num_levels): - z_level = z_max - (i + 1) * actual_depth + stock_allowance + z_level = cz_max - (i + 1) * actual_depth + stock_allowance + rounded_z = round(z_level, 2) levels.append({ - "z": round(z_level, 2), + "z": rounded_z, "depth": round(actual_depth, 2), "level_index": i + 1, }) + layer_points = self._generate_roughing_layer_points( + x_min=cx_min, x_max=cx_max, + y_min=cy_min, y_max=cy_max, + z=rounded_z, + stepover=stepover, + ) + path_points.extend(layer_points) 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"] ) @@ -298,8 +295,35 @@ class RoughingToolpathGenerator: "estimated_time_min": round(machining_time, 1), "approach_type": "helical_ramp", "ramp_angle": 2.0, + "path_points": path_points, } + @staticmethod + def _generate_roughing_layer_points( + x_min: float, x_max: float, + y_min: float, y_max: float, + z: float, stepover: float, + ) -> List[List[float]]: + """为单个 Z 层生成往复走刀坐标点序列。""" + points = [] + y_positions = [] + y = y_min + while y <= y_max + 0.001: + y_positions.append(round(y, 2)) + y += stepover + + for idx, y_pos in enumerate(y_positions): + if idx % 2 == 0: + # 正向走刀: x_min → x_max + points.append([round(x_min, 2), y_pos, z]) + points.append([round(x_max, 2), y_pos, z]) + else: + # 反向走刀: x_max → x_min + points.append([round(x_max, 2), y_pos, z]) + points.append([round(x_min, 2), y_pos, z]) + + return points + def _estimate_roughing_length(self, cavity_bbox: Dict, num_levels: int, stepover: float) -> float: """估算粗加工刀路总长度""" @@ -313,6 +337,70 @@ class RoughingToolpathGenerator: return length_per_level * num_levels + def generate_core_roughing(self, stock_bbox: Dict, core_bbox: Dict, + tool: Dict, cutting_params: Dict, + stock_allowance: float = 0.5) -> Dict[str, Any]: + """ + 型芯粗加工 — 从外轮廓向内逐层等高铣削。 + + 型芯是凸出的几何体,策略是从毛坯外边界向内逐层收缩切削, + 每层保持同一 Z 高度,环绕型芯轮廓走刀。 + """ + sx_min, sy_min, sz_min = stock_bbox.get("min", [0, 0, 0]) + sx_max, sy_max, sz_max = stock_bbox.get("max", [0, 0, 0]) + cx_min, cy_min, cz_min = core_bbox.get("min", [0, 0, 0]) + cx_max, cy_max, cz_max = core_bbox.get("max", [0, 0, 0]) + + axial_depth = cutting_params["axial_depth_mm"] + radial_depth = cutting_params["radial_depth_mm"] + total_depth = cz_max - cz_min + num_levels = max(1, math.ceil(total_depth / axial_depth)) + + levels = [{ + "z": round(cz_max - (i + 1) * total_depth / num_levels, 2), + "depth": round(total_depth / num_levels, 2), + "level_index": i + 1, + } for i in range(num_levels)] + + path_points = [] + margin = 1.0 + for level in levels: + z = level["z"] + outer_xmin, outer_xmax = sx_min + margin, sx_max - margin + outer_ymin, outer_ymax = sy_min + margin, sy_max - margin + num_contours = max(1, int(min(outer_xmax - cx_max, outer_ymax - cy_max) / radial_depth)) + for c in range(num_contours): + inset = c * radial_depth + x0 = max(cx_min, outer_xmin + inset) + x1 = min(cx_max, outer_xmax - inset) + y0 = max(cy_min, outer_ymin + inset) + y1 = min(cy_max, outer_ymax - inset) + contour = [ + [round(x0, 2), round(y0, 2), z], + [round(x1, 2), round(y0, 2), z], + [round(x1, 2), round(y1, 2), z], + [round(x0, 2), round(y1, 2), z], + ] + path_points.extend(contour) + + dims = core_bbox.get("dimensions", [100, 100, 50]) + toolpath_length = num_levels * 2 * (dims[0] + dims[1]) * 2.0 + machining_time = CuttingParamsCalculator.estimate_machining_time( + toolpath_length, cutting_params["feed_rate_mm_min"] + ) + + return { + "strategy": "core_roughing", + "tool": cutting_params, + "levels": levels, + "num_levels": num_levels, + "stepover": radial_depth, + "stock_allowance": stock_allowance, + "estimated_toolpath_length": round(toolpath_length, 1), + "estimated_time_min": round(machining_time, 1), + "path_points": path_points, + } + class FinishingToolpathGenerator: """精加工刀路生成器""" @@ -322,30 +410,36 @@ class FinishingToolpathGenerator: stepover: float = 0.3, angle: float = 0.0) -> Dict[str, Any]: """ - 平行铣削精加工 + 平行铣削精加工 — 生成真实刀路坐标点。 - Args: - cavity_bbox: 型腔边界框 - tool: 刀具参数 - cutting_params: 切削参数 - stepover: 步距 mm - angle: 加工角度 - - Returns: - 精加工刀路方案 + 在型腔顶部沿 Y 方向往复走刀,覆盖整个型腔 XY 区域。 """ dims = cavity_bbox.get("dimensions", [100, 100, 50]) + cx_min, cy_min, cz_min = cavity_bbox.get("min", [0, 0, 0]) + cx_max, cy_max, cz_max = cavity_bbox.get("max", [0, 0, 0]) width = dims[0] length = dims[1] + finish_z = cz_max - 0.1 # 型腔顶面 num_passes = max(1, int(width / stepover) + 1) + path_points = [] + + y = cy_min + for i in range(num_passes): + y_pos = round(y + i * stepover, 2) + if y_pos > cy_max: + y_pos = round(cy_max, 2) + if i % 2 == 0: + path_points.append([round(cx_min, 2), y_pos, finish_z]) + path_points.append([round(cx_max, 2), y_pos, finish_z]) + else: + path_points.append([round(cx_max, 2), y_pos, finish_z]) + path_points.append([round(cx_min, 2), y_pos, finish_z]) 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"] ) @@ -361,6 +455,7 @@ class FinishingToolpathGenerator: "estimated_time_min": round(machining_time, 1), "cutting_direction": "one_way", "stepover_type": "scallop", + "path_points": path_points, } def generate_contour_finishing(self, cavity_bbox: Dict, tool: Dict, @@ -411,6 +506,62 @@ class FinishingToolpathGenerator: h = stepover ** 2 / (8 * r) if r > 0 else stepover return h * 0.25 + def generate_3d_surface_finishing(self, cavity_bbox: Dict, tool: Dict, + cutting_params: Dict, + stepover: float = 0.5, + angle: float = 0.0) -> Dict[str, Any]: + """ + Ballnose 三维曲面精加工 — Z 随形走刀。 + + 在型腔边界框内,沿 Y 方向逐行走刀,Z 高度按余弦曲线随 Y 变化, + 模拟曲面轮廓加工。适用于球头刀对型腔底面的随形精加工。 + """ + dims = cavity_bbox.get("dimensions", [100, 100, 50]) + cx_min, cy_min, cz_min = cavity_bbox.get("min", [0, 0, 0]) + cx_max, cy_max, cz_max = cavity_bbox.get("max", [0, 0, 0]) + width = dims[0] + length = dims[1] + depth = dims[2] + mid_y = (cy_min + cy_max) / 2.0 + + num_passes = max(1, int(width / stepover) + 1) + path_points = [] + + y = cy_min + for i in range(num_passes): + y_pos = round(y + i * stepover, 2) + if y_pos > cy_max: + y_pos = round(cy_max, 2) + # Z 随 Y 位置按余弦曲线变化,模拟中心最深、边缘最浅的曲面 + ratio = (y_pos - mid_y) / max((cy_max - cy_min) / 2, 1.0) + z_depth = cz_max - depth * 0.85 * (1.0 - min(abs(ratio), 1.0)) + z_finish = round(max(cz_min + 0.1, z_depth), 2) + if i % 2 == 0: + path_points.append([round(cx_min, 2), y_pos, z_finish]) + path_points.append([round(cx_max, 2), y_pos, z_finish]) + else: + path_points.append([round(cx_max, 2), y_pos, z_finish]) + path_points.append([round(cx_min, 2), y_pos, z_finish]) + + surface_roughness = self._estimate_surface_roughness(tool["diameter"], stepover) + toolpath_length = num_passes * length * 1.1 + machining_time = CuttingParamsCalculator.estimate_machining_time( + toolpath_length, cutting_params["feed_rate_mm_min"] + ) + + return { + "strategy": "3d_surface_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), + "note": "3D 曲面随形加工,Z 高度沿 Y 方向余弦变化", + "path_points": path_points, + } + class GCodePostProcessor: """G代码后处理器""" @@ -487,31 +638,66 @@ class GCodePostProcessor: feed = tool_info.get("feed_rate_mm_min", 500) levels = op.get("levels", []) + path_points = op.get("path_points", []) 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") + # 筛选当前 Z 层的刀路点 + layer_points = [p for p in path_points if abs(p[2] - z) < 0.01] + if layer_points: + for pt in layer_points: + lines.append(f"{d['linear']} X{pt[0]:.2f} Y{pt[1]:.2f} F{feed}") + else: + # 无精细刀路点时退化为矩形扫面 + cx_min = min(p[0] for p in path_points) if path_points else -50.0 + cx_max = max(p[0] for p in path_points) if path_points else 50.0 + cy_min = min(p[1] for p in path_points) if path_points else -30.0 + cy_max = max(p[1] for p in path_points) if path_points else 30.0 + lines.append(f"{d['linear']} X{cx_max:.2f} Y{cy_min:.2f} F{feed}") + lines.append(f"{d['linear']} X{cx_min:.2f} Y{cy_min:.2f}") + lines.append(f"{d['linear']} X{cx_min:.2f} Y{cy_max:.2f}") + lines.append(f"{d['linear']} X{cx_max:.2f} Y{cy_max:.2f}") 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}") + finish_z = path_points[0][2] if path_points else -5.0 + lines.append(f"{d['linear']} Z{finish_z:.2f} F{int(feed * 0.3)}") + if path_points: + for pt in path_points: + lines.append(f"{d['linear']} X{pt[0]:.2f} Y{pt[1]:.2f} F{feed}") + else: + 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']} 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("") + elif strategy == "core_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)}") + layer_points = [p for p in path_points if abs(p[2] - z) < 0.01] + for pt in layer_points: + lines.append(f"{d['linear']} X{pt[0]:.2f} Y{pt[1]:.2f} F{feed}") + lines.append(f"{d['rapid']} Z10.0") + lines.append("") + + elif strategy == "3d_surface_finishing": + lines.append(f"(--- 3D 曲面精加工,球头刀随形 ---)") + if path_points: + for pt in path_points: + lines.append(f"{d['linear']} X{pt[0]:.2f} Y{pt[1]:.2f} Z{pt[2]:.2f} F{feed}") + lines.append(f"{d['rapid']} Z10.0") + lines.append("") + else: lines.append(f"(策略 {strategy} 的刀路数据)") lines.append("") @@ -540,25 +726,50 @@ class MoldCAMDesigner: self.roughing_gen = RoughingToolpathGenerator() self.finishing_gen = FinishingToolpathGenerator() self.post_processor = GCodePostProcessor() + self._collision_detector = None + self._toolpath_optimizer = None + self._edm_designer = None + self._machining_simulator = None + + def _get_collision_detector(self): + if self._collision_detector is None: + from moldinsight.core.mold_machining import CollisionDetector + self._collision_detector = CollisionDetector() + return self._collision_detector + + def _get_toolpath_optimizer(self): + if self._toolpath_optimizer is None: + from moldinsight.core.mold_machining import ToolpathOptimizer + self._toolpath_optimizer = ToolpathOptimizer() + return self._toolpath_optimizer + + def _get_edm_designer(self): + if self._edm_designer is None: + from moldinsight.core.mold_machining import EDMElectrodeDesigner + self._edm_designer = EDMElectrodeDesigner() + return self._edm_designer + + def _get_machining_simulator(self): + if self._machining_simulator is None: + from moldinsight.core.mold_machining import MachiningSimulator + self._machining_simulator = MachiningSimulator() + return self._machining_simulator 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]: + program_number: int = 1000, + core_bbox: Optional[Dict] = None, + undercut_regions: Optional[List[Dict]] = None) -> Dict[str, Any]: """ - 综合设计模具CAM方案 + 综合设计模具CAM方案(型腔 + 型芯 + 3D 曲面 + EDM + 仿真)。 Args: cavity_bbox: 型腔边界框 stock_bbox: 毛坯边界框 - mold_steel: 模具钢材料 - surface_quality: 表面质量要求 - controller: 数控系统 - program_number: 程序号 - - Returns: - 完整的CAM方案 + core_bbox: 型芯边界框(可选,提供则生成型芯刀路) + undercut_regions: 倒扣区域列表(可选,提供则生成 EDM 方案) """ logger.info(f"开始模具CAM设计: 钢材={mold_steel}, 质量={surface_quality}") @@ -584,6 +795,79 @@ class MoldCAMDesigner: operations = [roughing_op, finishing_op] + # 型芯粗加工(提供 core_bbox 时) + core_op = None + if core_bbox: + core_op = self.roughing_gen.generate_core_roughing( + stock_bbox, core_bbox, roughing_tool, roughing_params + ) + operations.append(core_op) + + # 3D 曲面精加工(ballnose 随形) + surface_op = self.finishing_gen.generate_3d_surface_finishing( + cavity_bbox, finishing_tool, finishing_params + ) + operations.append(surface_op) + + # 刀路优化与碰撞检测 + manufacturing_warnings = self._generate_cam_recommendations( + roughing_op, finishing_op, mold_steel + ) + try: + optimizer = self._get_toolpath_optimizer() + for op in operations: + pts = op.get("path_points", []) + if pts and len(pts) >= 2: + optimized = optimizer.optimize_toolpath( + pts, + {"feed_rate_mm_min": op.get("tool", {}).get("feed_rate_mm_min", 500)}, + stock_bbox, + ) + if optimized and isinstance(optimized, dict): + op["optimized"] = True + op["optimization_metadata"] = { + "original_point_count": optimized.get("original_point_count"), + "corner_slowdowns": optimized.get("corner_slowdowns"), + "recommendations": optimized.get("recommendations"), + } + + detector = self._get_collision_detector() + tool_rep = {"diameter": roughing_tool["diameter"], "flute_length": roughing_tool["flute_length"]} + collision_result = detector.check_toolpath_safety( + roughing_op.get("path_points", [[0, 0, 50]]), + tool_rep, + stock_bbox, + ) + if collision_result and not collision_result.get("is_safe", True): + manufacturing_warnings.append( + f"碰撞检测发现 {collision_result.get('total_issues', 0)} 处潜在碰撞风险,请人工复核安全平面" + ) + except Exception as exc: + logger.debug(f"刀路优化/碰撞检测跳过(非致命): {exc}") + + # EDM 电极设计 + edm_result = None + if undercut_regions: + try: + edm = self._get_edm_designer() + edm_result = edm.design_electrodes( + undercut_regions=undercut_regions, + cavity_bbox=cavity_bbox, + material="copper", + ) + except Exception as exc: + logger.debug(f"EDM 电极设计跳过(非致命): {exc}") + + # 加工仿真 + simulation_result = None + try: + sim = self._get_machining_simulator() + simulation_result = sim.simulate_machining(operations, stock_bbox) + if simulation_result and simulation_result.get("gouging_detected"): + manufacturing_warnings.append("仿真检测到过切风险,请人工复核刀路") + except Exception as exc: + logger.debug(f"加工仿真跳过(非致命): {exc}") + gcode = self.post_processor.generate_gcode( operations, program_number=program_number ) @@ -608,9 +892,9 @@ class MoldCAMDesigner: "surface_quality": surface_quality, "controller": controller, }, - "recommendations": self._generate_cam_recommendations( - roughing_op, finishing_op, mold_steel - ), + "recommendations": manufacturing_warnings, + "edm": edm_result, + "simulation": simulation_result, } logger.info(f"CAM设计完成: {len(operations)} 个工序, " diff --git a/src/moldinsight/services/cam_bundle_service.py b/src/moldinsight/services/cam_bundle_service.py index 4b50b35..76c0124 100644 --- a/src/moldinsight/services/cam_bundle_service.py +++ b/src/moldinsight/services/cam_bundle_service.py @@ -29,6 +29,8 @@ class CAMBundleService: mold_steel=mold_steel, surface_quality=surface_quality, controller=controller, + core_bbox=cavity_bbox, # MVP: 型芯 bbox 复用型腔 bbox + undercut_regions=scheme.get("undercut_regions") if scheme else None, ) process_plan = self._build_process_plan(cam_result.get("operations", [])) @@ -55,6 +57,26 @@ class CAMBundleService: if include_gcode: bundle["gcode"] = cam_result.get("gcode", "") bundle["gcode_lines"] = cam_result.get("gcode_lines", 0) + + # 可选:EDM 电极方案 + edm = cam_result.get("edm") + if edm and edm.get("electrodes"): + bundle["edm"] = { + "electrode_count": edm.get("total_electrode_count", 0), + "total_volume_cm3": edm.get("total_volume_cm3", 0), + "material": edm.get("material", "copper"), + "recommendations": edm.get("recommendations", []), + } + + # 可选:加工仿真摘要 + sim = cam_result.get("simulation") + if sim: + bundle["simulation"] = { + "removed_percent": sim.get("total_volume_removed_percent", 0), + "gouging_detected": bool(sim.get("gouging_detected")), + "quality": sim.get("quality_assessment", {}), + } + return bundle @staticmethod diff --git a/src/moldinsight/services/processing_service.py b/src/moldinsight/services/processing_service.py index 9ffb2cb..810f29c 100644 --- a/src/moldinsight/services/processing_service.py +++ b/src/moldinsight/services/processing_service.py @@ -494,7 +494,7 @@ class ProcessingService: "generated_at": datetime.now().isoformat(), "schemes": {}, } - components = ["cavity", "core", "parting_surface"] + components = ["cavity", "core", "parting_surface", "product", "a_plate", "b_plate"] for scheme_id, cavity_data in export_shapes.items(): try: