分模候选方案

This commit is contained in:
2026-04-23 23:37:39 +08:00
parent 0bdfc7f7d5
commit c1f6b6e827
16 changed files with 1302 additions and 497 deletions
+6 -4
View File
@@ -102,7 +102,7 @@ async def upload_stp(
# 创建处理任务记录
await storage_service.create_processing_task(db_session, task_id, stp_file.id)
# 创建任务记录(Redis 为主,内存为兼容回退)
# 创建任务记录(Redis 为主,内存作为兼容回退)
task_info = create_task_info(
task_id=task_id,
status=ProcessingStatus.PROCESSING,
@@ -114,10 +114,13 @@ async def upload_stp(
await redis_task_manager.set_task(task_id, task_info)
tasks[task_id] = task_info
# 后台处理(统一走 ProcessingService,使用独立数据库会话)
# 后台处理走统一编排服务,避免请求会话在后台失效
background_tasks.add_task(
processing_service.process_file_with_storage,
task_id, file_path, stp_file.id, material
task_id,
file_path,
stp_file.id,
material,
)
return {
@@ -146,7 +149,6 @@ async def get_status(task_id: str, db_session: AsyncSession = Depends(get_db_ses
try:
task_view = await TaskQueryService.get_task_view(db_session, task_id)
if task_view is None:
# 兼容老的仅内存任务
if task_id in tasks:
return tasks[task_id]
raise HTTPException(404, "任务不存在")
+16 -1
View File
@@ -1 +1,16 @@
# api/v1/__init__.py
from fastapi import APIRouter
from api.v1.health_router import router as health_router
from api.v1.upload_router import router as upload_router
from api.v1.task_router import router as task_router
from api.v1.history_router import router as history_router
from api.v1.debug_router import router as debug_router
from api.v1.advanced_router import router as advanced_router
router = APIRouter()
router.include_router(health_router)
router.include_router(upload_router)
router.include_router(task_router)
router.include_router(history_router)
router.include_router(debug_router)
router.include_router(advanced_router)
+352
View File
@@ -0,0 +1,352 @@
from pathlib import Path
import os
from fastapi import APIRouter, Depends, HTTPException, Request
from services.auth_service import get_current_active_user
from models.database import User
from utils.logger import get_logger
from api.routes import (
tasks,
cavity_layout_optimizer,
mold_system_designer,
side_action_designer,
mold_cam_designer,
collision_detector,
toolpath_optimizer,
edm_designer,
machining_simulator,
cad_exporter,
)
logger = get_logger(__name__)
router = APIRouter()
@router.post("/optimize-layout")
async def optimize_cavity_layout(
request: Request,
current_user: User = Depends(get_current_active_user),
):
"""多型腔布局优化"""
body = await request.json()
product_bbox = body.get("product_bbox", {"dimensions": [100, 100, 50]})
cavity_count = body.get("cavity_count", 1)
mold_base_size = body.get("mold_base_size")
layout_type = body.get("layout_type", "auto")
if cavity_count < 1 or cavity_count > 64:
raise HTTPException(400, "型腔数量必须在 1-64 之间")
result = cavity_layout_optimizer.optimize_layout(
product_bbox=product_bbox,
cavity_count=cavity_count,
mold_base_size=mold_base_size,
layout_type=layout_type,
)
return {"status": "success", "data": result}
@router.post("/design-cooling")
async def design_cooling_system(
request: Request,
current_user: User = Depends(get_current_active_user),
):
"""冷却系统设计"""
body = await request.json()
mold_size = body.get("mold_size", {"length": 300, "width": 300, "height": 200})
product_bbox = body.get("product_bbox", {"dimensions": [100, 100, 50]})
material = body.get("material", "ABS")
cavity_count = body.get("cavity_count", 1)
cycle_time_target = body.get("cycle_time_target")
from core.mold_system_designer import CoolingSystemDesigner
designer = CoolingSystemDesigner()
result = designer.design_cooling_system(
mold_size=mold_size,
product_bbox=product_bbox,
material=material,
cavity_count=cavity_count,
cycle_time_target=cycle_time_target,
)
return {"status": "success", "data": result}
@router.post("/design-gating")
async def design_gating_system(
request: Request,
current_user: User = Depends(get_current_active_user),
):
"""浇注系统设计"""
body = await request.json()
product_bbox = body.get("product_bbox", {"dimensions": [100, 100, 50]})
material = body.get("material", "ABS")
cavity_count = body.get("cavity_count", 1)
gate_type = body.get("gate_type", "auto")
layout_positions = body.get("layout_positions")
from core.mold_system_designer import GatingSystemDesigner
designer = GatingSystemDesigner()
result = designer.design_gating_system(
product_bbox=product_bbox,
material=material,
cavity_count=cavity_count,
gate_type=gate_type,
layout_positions=layout_positions,
)
return {"status": "success", "data": result}
@router.post("/design-mold-system")
async def design_complete_mold_system(
request: Request,
current_user: User = Depends(get_current_active_user),
):
"""综合模具系统设计(冷却+浇注)"""
body = await request.json()
mold_size = body.get("mold_size", {"length": 300, "width": 300, "height": 200})
product_bbox = body.get("product_bbox", {"dimensions": [100, 100, 50]})
material = body.get("material", "ABS")
cavity_count = body.get("cavity_count", 1)
gate_type = body.get("gate_type", "auto")
cycle_time_target = body.get("cycle_time_target")
layout_positions = body.get("layout_positions")
result = mold_system_designer.design_complete_system(
mold_size=mold_size,
product_bbox=product_bbox,
material=material,
cavity_count=cavity_count,
gate_type=gate_type,
cycle_time_target=cycle_time_target,
layout_positions=layout_positions,
)
return {"status": "success", "data": result}
@router.post("/ai-parting-detect")
async def ai_parting_surface_detect(
request: Request,
current_user: User = Depends(get_current_active_user),
):
"""AI 分型面检测"""
body = await request.json()
task_id = body.get("task_id")
if not task_id or task_id not in tasks:
raise HTTPException(404, "任务不存在")
task_data = tasks[task_id]
geometry_data = task_data.get("geometry_data")
if not geometry_data:
raise HTTPException(400, "该任务尚未完成几何分析")
from core.ai_parting_detector import AIPartingSurfaceDetectorV2
detector = AIPartingSurfaceDetectorV2(use_gnn=True)
result = detector._detect_with_geometry(None, geometry_data)
return {"status": "success", "data": result}
@router.post("/detect-undercuts")
async def detect_undercuts(
request: Request,
current_user: User = Depends(get_current_active_user),
):
"""倒扣区域检测与滑块/斜顶机构设计"""
body = await request.json()
task_id = body.get("task_id")
parting_direction = body.get("parting_direction", [0, 0, 1])
mold_size = body.get("mold_size", {"length": 300, "width": 300, "height": 200})
if not task_id or task_id not in tasks:
raise HTTPException(404, "任务不存在")
task_data = tasks[task_id]
geometry_data = task_data.get("geometry_data")
if not geometry_data:
raise HTTPException(400, "该任务尚未完成几何分析")
result = side_action_designer.analyze_and_design(
shape=None,
parting_direction=parting_direction,
mold_size=mold_size,
)
return {"status": "success", "data": result}
@router.post("/design-cam")
async def design_mold_cam(
request: Request,
current_user: User = Depends(get_current_active_user),
):
"""模具CAM刀路设计"""
body = await request.json()
cavity_bbox = body.get("cavity_bbox", {"dimensions": [100, 100, 50], "min": [-50, -50, -25], "max": [50, 50, 25]})
stock_bbox = body.get("stock_bbox", {"dimensions": [150, 150, 100], "min": [-75, -75, -50], "max": [75, 75, 50]})
mold_steel = body.get("mold_steel", "P20")
surface_quality = body.get("surface_quality", "standard")
controller = body.get("controller", "fanuc")
result = mold_cam_designer.design_mold_cam(
cavity_bbox=cavity_bbox,
stock_bbox=stock_bbox,
mold_steel=mold_steel,
surface_quality=surface_quality,
controller=controller,
)
return {"status": "success", "data": result}
@router.post("/check-collision")
async def check_toolpath_collision(
request: Request,
current_user: User = Depends(get_current_active_user),
):
"""刀路碰撞检测"""
body = await request.json()
toolpath_points = body.get("toolpath_points", [[0, 0, 50], [10, 10, -5], [20, 20, -10]])
tool = body.get("tool", {"diameter": 10, "flute_length": 30, "shank_diameter": 10})
stock_bbox = body.get("stock_bbox", {"min": [-50, -50, -25], "max": [50, 50, 25]})
clamp_positions = body.get("clamp_positions")
result = collision_detector.check_toolpath_safety(
toolpath_points, tool, stock_bbox, clamp_positions
)
return {"status": "success", "data": result}
@router.post("/optimize-toolpath")
async def optimize_toolpath(
request: Request,
current_user: User = Depends(get_current_active_user),
):
"""刀路优化"""
body = await request.json()
toolpath_points = body.get("toolpath_points", [[0, 0, 50], [10, 10, -5], [20, 20, -10]])
cutting_params = body.get("cutting_params", {"feed_rate_mm_min": 500})
stock_bbox = body.get("stock_bbox")
result = toolpath_optimizer.optimize_toolpath(
toolpath_points, cutting_params, stock_bbox
)
return {"status": "success", "data": result}
@router.post("/design-electrodes")
async def design_edm_electrodes(
request: Request,
current_user: User = Depends(get_current_active_user),
):
"""EDM电极设计"""
body = await request.json()
undercut_regions = body.get("undercut_regions", [{"center": [0, 0, 0], "area": 100, "type": "undercut"}])
cavity_bbox = body.get("cavity_bbox", {"dimensions": [100, 100, 50]})
material = body.get("material", "copper")
spark_gap = body.get("spark_gap", 0.05)
overburn = body.get("overburn", 0.1)
result = edm_designer.design_electrodes(
undercut_regions, cavity_bbox, material, spark_gap, overburn
)
return {"status": "success", "data": result}
@router.post("/simulate-machining")
async def simulate_machining(
request: Request,
current_user: User = Depends(get_current_active_user),
):
"""加工仿真"""
body = await request.json()
operations = body.get("operations", [{"strategy": "z_level_roughing", "levels": [{"z": -5}]}])
stock_bbox = body.get("stock_bbox", {"dimensions": [100, 100, 50], "min": [-50, -50, -25], "max": [50, 50, 25]})
resolution = body.get("resolution", 2.0)
result = machining_simulator.simulate_machining(
operations, stock_bbox, resolution
)
return {"status": "success", "data": result}
@router.post("/export-mold")
async def export_mold_results(
request: Request,
current_user: User = Depends(get_current_active_user),
):
"""导出模具设计结果(STEP/IGES/STL/BRep)"""
body = await request.json()
task_id = body.get("task_id")
formats = body.get("formats", ["step", "stl"])
components = body.get("components", ["cavity", "core"])
if not task_id or task_id not in tasks:
raise HTTPException(404, "任务不存在")
task_data = tasks[task_id]
cavity_shapes = task_data.get("cavity_shapes")
if not cavity_shapes:
raise HTTPException(400, "该任务尚未完成模具生成或形状数据不可用")
base_filename = Path(task_data.get("filename", f"mold_{task_id}")).stem
result = cad_exporter.export_mold_results(
cavity_data=cavity_shapes,
base_filename=base_filename,
formats=formats,
components=components,
)
return {"status": "success", "data": result}
@router.get("/export-download/{filepath:path}")
async def download_export_file(
filepath: str,
current_user: User = Depends(get_current_active_user),
):
"""下载导出的CAD文件"""
from fastapi.responses import FileResponse
full_path = os.path.join(cad_exporter.output_dir, filepath)
if not os.path.exists(full_path):
raise HTTPException(404, "文件不存在")
if not os.path.abspath(full_path).startswith(os.path.abspath(cad_exporter.output_dir)):
raise HTTPException(403, "禁止访问")
media_types = {
".step": "application/step",
".stp": "application/step",
".iges": "application/iges",
".igs": "application/iges",
".stl": "model/stl",
".brep": "application/octet-stream",
}
ext = Path(full_path).suffix.lower()
media_type = media_types.get(ext, "application/octet-stream")
return FileResponse(
full_path,
media_type=media_type,
filename=os.path.basename(full_path),
)
@router.get("/export-recommendations")
async def get_export_recommendations(
target: str = "ug",
current_user: User = Depends(get_current_active_user),
):
"""获取导出格式建议(UG/FreeCAD/SolidWorks)"""
result = cad_exporter.get_export_recommendations(target)
return {"status": "success", "data": result}
+2
View File
@@ -15,6 +15,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from services.auth_service import get_current_active_user
from services.processing_service import processing_service
from models.database import User
from api.routes import tasks as legacy_tasks
logger = get_logger(__name__)
@@ -65,6 +66,7 @@ async def upload_stp(
upload_time=str(datetime.now())
)
await redis_task_manager.set_task(task_id, task_info)
legacy_tasks[task_id] = task_info
# 后台处理(使用独立数据库会话,避免请求会话关闭问题)
background_tasks.add_task(
+28 -180
View File
@@ -310,43 +310,6 @@ class AluminumFoamMoldGenerator(BaseMoldGenerator):
1. 优先选择 Z 轴方向分型(上下开模)
2. 分型面位置选在产品的最大轮廓处,即包围盒的 Z 方向中心
"""
<<<<<<< HEAD
primary_parting = self._detect_primary_parting(shape, analysis)
parting_surface = primary_parting["surface"]
parting_line = self.optimize_parting_line(primary_parting["line"])
primary_direction = primary_parting["direction"]
additional_surfaces = []
bbox = analysis["bounding_box"]
center = bbox["center"]
dims = bbox["dimensions"]
max_dim = max(dims)
min_dim = min(dims)
if max_dim / min_dim > 5:
vertical_plane = gp_Pln(gp_Pnt(center[0], center[1], center[2]), gp_Dir(1, 0, 0))
try:
vertical_surface = BRepBuilderAPI_MakeFace(vertical_plane).Face()
additional_surfaces.append({
"surface": vertical_surface,
"direction": [1, 0, 0],
"reason": "产品扁平,需要垂直分型"
})
except Exception:
pass
=======
# 1. 尝试 AI 模型
if self.ai_parting_detector is not None:
try:
ai_result = self.ai_parting_detector.detect(shape, analysis)
if ai_result:
return self._create_parting_surface_from_ai(ai_result, analysis)
except Exception as e:
logger.warning(f"AI 分型面检测失败: {e}")
# 2. 泡沫模具强制 Z 轴方向分型(上下开模)
bbox = analysis["bounding_box"]
center = bbox["center"]
primary_direction = [0, 0, 1] # Z 轴方向
@@ -364,26 +327,40 @@ class AluminumFoamMoldGenerator(BaseMoldGenerator):
logger.info(f"泡沫模具 Z 轴分型面: Z={parting_z:.2f} mm (包围盒中心)")
# 3. 计算分型线
parting_line = self._calculate_parting_line(shape, parting_surface)
>>>>>>> 77c4885a4020b609f4964184661f42e936814c4f
parting_line = self.optimize_parting_line(
self._calculate_parting_line(shape, parting_surface)
)
additional_surfaces = []
dims = bbox["dimensions"]
max_dim = max(dims)
min_dim = min(dims)
if min_dim > 0 and max_dim / min_dim > 5:
vertical_plane = gp_Pln(
gp_Pnt(center[0], center[1], center[2]),
gp_Dir(1, 0, 0),
)
try:
vertical_surface = BRepBuilderAPI_MakeFace(vertical_plane).Face()
additional_surfaces.append({
"surface": vertical_surface,
"direction": [1, 0, 0],
"reason": "产品扁平,需要辅助垂直分型参考",
})
except Exception:
pass
return {
"primary_surface": parting_surface,
"primary_line": parting_line,
"primary_direction": primary_direction,
<<<<<<< HEAD
"confidence": primary_parting["confidence"],
"method": primary_parting["method"],
"additional_surfaces": additional_surfaces,
"surface_count": 1 + len(additional_surfaces)
=======
"confidence": 0.95, # Z 轴分型置信度高
"additional_surfaces": [],
"surface_count": 1,
"method": "z_axis_rule",
"additional_surfaces": additional_surfaces,
"surface_count": 1 + len(additional_surfaces),
"parting_direction": "Z",
"parting_position_z": parting_z,
>>>>>>> 77c4885a4020b609f4964184661f42e936814c4f
}
def _build_undercut_regions(self, undercut_analysis: Dict[str, Any]) -> List[Dict[str, Any]]:
@@ -463,120 +440,6 @@ class AluminumFoamMoldGenerator(BaseMoldGenerator):
except Exception:
return 50.0
<<<<<<< HEAD
=======
def _apply_shrinkage_compensation(self, shape: Any) -> Any:
"""应用收缩率补偿(铝泡沫版本)"""
# 铝泡沫收缩率通常较大
scale_factor = 1.0 + self.shrinkage_rate
trsf = gp_Trsf()
trsf.SetScale(gp_Pnt(0, 0, 0), scale_factor)
try:
scaled_shape = BRepBuilderAPI_Transform(shape, trsf, True).Shape()
logger.info(f"收缩率补偿应用: {self.shrinkage_rate*100:.2f}%")
return scaled_shape
except Exception as e:
logger.error(f"收缩补偿失败: {e}")
return shape
def _apply_draft_angles(self, shape: Any, parting_surface: Any) -> Any:
"""应用拔模角(改进版)"""
# 获取分型面法向量作为拔模方向
try:
surface = BRepAdaptor_Surface(parting_surface)
draft_direction = surface.Plane().Position().Direction()
logger.info(f"应用拔模角: {self.draft_angle}°, 方向: ({draft_direction.X():.3f}, {draft_direction.Y():.3f}, {draft_direction.Z():.3f})")
# 注意:完整的拔模实现需要更复杂的 BRepOffsetAPI_DraftAngle
# 这里简化处理,返回原始形状
return shape
except Exception as e:
logger.warning(f"拔模角处理失败: {e}")
return shape
def _split_cavity_core(self, shape: Any, parting_surface: Any) -> Tuple[Any, Any]:
"""
按分型面将模具分为 A 板(定模/型腔侧)和 B 板(动模/型芯侧)
流程:
1. 创建模具块(包围产品的长方体)
2. 用分型面水平切出上半块和下半块
3. 从上半块减去产品 → A 板(含型腔负形)
4. 从下半块减去产品 → B 板(含型芯负形)
"""
try:
# 获取产品边界框
bbox = Bnd_Box()
brepbndlib_Add(shape, bbox)
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
# 获取分型面 Z 位置(包围盒 Z 中心)
parting_z = (zmin + zmax) / 2
# 模具块尺寸(比产品大一圈余量)
margin = 25
mold_xmin = xmin - margin
mold_ymin = ymin - margin
mold_zmin = zmin - margin
mold_xmax = xmax + margin
mold_ymax = ymax + margin
mold_zmax = zmax + margin
# Step 1: 创建整块模具
mold_block = BRepPrimAPI_MakeBox(
gp_Pnt(mold_xmin, mold_ymin, mold_zmin),
gp_Pnt(mold_xmax, mold_ymax, mold_zmax)
).Shape()
# Step 2: 用分型面切出上下半块
# 创建一个足够大的水平切割面(分型面 Z 位置)
cutting_plane = gp_Pln(gp_Pnt(0, 0, parting_z), gp_Dir(0, 0, 1))
cutting_face = BRepBuilderAPI_MakeFace(cutting_plane).Face()
# 创建上半空间和下半空间的实体
# 上半块: 从 parting_z 到 mold_zmax
upper_block = BRepPrimAPI_MakeBox(
gp_Pnt(mold_xmin, mold_ymin, parting_z),
gp_Pnt(mold_xmax, mold_ymax, mold_zmax)
).Shape()
# 下半块: 从 mold_zmin 到 parting_z
lower_block = BRepPrimAPI_MakeBox(
gp_Pnt(mold_xmin, mold_ymin, mold_zmin),
gp_Pnt(mold_xmax, mold_ymax, parting_z)
).Shape()
# Step 3: 从上半块减去产品 → A 板(定模)
a_plate_op = BRepAlgoAPI_Cut(upper_block, shape)
if a_plate_op.IsDone():
a_plate = a_plate_op.Shape()
logger.info(f"A板(定模)生成成功: Z={parting_z:.1f} ~ {mold_zmax:.1f}")
else:
logger.warning("A板布尔减法失败,使用上半块")
a_plate = upper_block
# Step 4: 从下半块减去产品 → B 板(动模)
b_plate_op = BRepAlgoAPI_Cut(lower_block, shape)
if b_plate_op.IsDone():
b_plate = b_plate_op.Shape()
logger.info(f"B板(动模)生成成功: Z={mold_zmin:.1f} ~ {parting_z:.1f}")
else:
logger.warning("B板布尔减法失败,使用下半块")
b_plate = lower_block
return a_plate, b_plate
except Exception as e:
logger.error(f"分模失败: {e}")
return shape, shape
>>>>>>> 77c4885a4020b609f4964184661f42e936814c4f
def _generate_mold_block(self, cavity: Any, analysis: Dict) -> Any:
"""生成完整的模具块(包含A/B板结构)"""
try:
@@ -656,20 +519,6 @@ class AluminumFoamMoldGenerator(BaseMoldGenerator):
}
def _calculate_clamping_force(self, analysis: Dict) -> str:
<<<<<<< HEAD
"""估算锁模力"""
volume_cm3 = analysis.get("volume", 0) / 1000
if volume_cm3 < 10:
return "30-50 吨"
elif volume_cm3 < 50:
return "50-100 吨"
elif volume_cm3 < 200:
return "100-200 吨"
else:
return "200+ 吨"
=======
"""
估算锁模力(泡沫模具专用)
@@ -687,14 +536,13 @@ class AluminumFoamMoldGenerator(BaseMoldGenerator):
clamping_force_ton = max(30, clamping_force_ton)
return f"{clamping_force_ton} 吨 (投影面积 {projected_area_cm2:.1f} cm² × 0.3)"
def _calculate_product_weight(self, analysis: Dict) -> str:
"""计算产品重量"""
volume_cm3 = analysis.get("volume", 0) / 1000
weight_g = volume_cm3 * self.material_density
return f"{weight_g:.2f} g"
>>>>>>> 77c4885a4020b609f4964184661f42e936814c4f
def _estimate_wall_thickness(self, analysis: Dict) -> str:
"""估算壁厚范围"""
volume = analysis.get("volume", 0)
-174
View File
@@ -224,180 +224,6 @@ class BaseMoldGenerator:
logger.error(f"产品几何分析失败: {e}")
raise
def _analyze_parting_direction(self, shape: Any) -> Dict[str, Any]:
"""
分析主分型方向。
使用面积加权的面法向统计,作为普通模具与铝泡沫模具的统一几何回退。
"""
face_normals = []
face_areas = []
explorer = TopExp_Explorer(shape, TopAbs_FACE)
while explorer.More():
face = TopoDS_Face(explorer.Current())
explorer.Next()
try:
normal = self._get_face_normal(face)
if normal is None:
continue
face_props = GProp_GProps()
brepgprop.SurfaceProperties(face, face_props)
area = max(float(face_props.Mass()), 1e-6)
face_normals.append(np.array([normal.X(), normal.Y(), normal.Z()], dtype=np.float64))
face_areas.append(area)
except Exception as e:
logger.debug(f"分型方向面分析失败: {e}")
if not face_normals:
return {
"primary_direction": [0.0, 0.0, 1.0],
"confidence": 0.5,
"face_count": 0,
}
normals = np.array(face_normals, dtype=np.float64)
areas = np.array(face_areas, dtype=np.float64)
total_area = float(areas.sum())
if total_area > 1e-6:
weights = areas / total_area
weighted_normal = np.sum(normals * weights[:, np.newaxis], axis=0)
else:
weighted_normal = np.mean(normals, axis=0)
norm = np.linalg.norm(weighted_normal)
if norm > 1e-6:
weighted_normal /= norm
else:
weighted_normal = np.array([0.0, 0.0, 1.0], dtype=np.float64)
confidence = float(np.mean(np.abs(np.dot(normals, weighted_normal))))
return {
"primary_direction": weighted_normal.tolist(),
"confidence": confidence,
"face_count": len(face_normals),
"normal_distribution": normals.tolist(),
}
def _create_parting_surface_from_direction(
self,
shape: Any,
analysis: Dict[str, Any],
direction: Any,
extension: float = 10.0,
) -> Any:
"""按给定方向创建覆盖产品边界的分型面。"""
bbox = analysis.get("bounding_box", {})
center = bbox.get("center", [0.0, 0.0, 0.0])
dims = bbox.get("dimensions", [100.0, 100.0, 100.0])
dir_obj = self._normalize_direction(direction)
plane = gp_Pln(gp_Pnt(center[0], center[1], center[2]), dir_obj)
span = max(max(dims), 1.0) + extension * 2
try:
return BRepBuilderAPI_MakeFace(plane, -span, span, -span, span).Face()
except Exception:
return BRepBuilderAPI_MakeFace(plane).Face()
def _normalize_direction(self, direction: Any) -> gp_Dir:
"""归一化分型方向,异常时回退到 Z 轴。"""
try:
if isinstance(direction, gp_Dir):
return direction
if isinstance(direction, np.ndarray):
values = direction.tolist()
else:
values = list(direction)
if len(values) < 3:
raise ValueError("direction 维度不足")
vec = np.array(values[:3], dtype=np.float64)
norm = np.linalg.norm(vec)
if norm <= 1e-6:
raise ValueError("direction 长度为 0")
vec /= norm
return gp_Dir(float(vec[0]), float(vec[1]), float(vec[2]))
except Exception:
return gp_Dir(0, 0, 1)
def _detect_primary_parting(self, shape: Any, analysis: Dict[str, Any]) -> Dict[str, Any]:
"""
统一主分型面检测。
返回统一结构,便于子类按需追加多分型面、倒扣与平滑逻辑。
"""
if self.ai_parting_detector is not None:
try:
ai_result = self.ai_parting_detector.detect(shape, analysis)
if ai_result:
parting_surface = self._create_parting_surface_from_direction(
shape,
analysis,
ai_result.get("normal", [0, 0, 1]),
)
parting_line = ai_result.get("parting_line") or self._calculate_parting_line(shape, parting_surface)
return {
"surface": parting_surface,
"line": parting_line,
"direction": ai_result.get("normal", [0, 0, 1]),
"confidence": ai_result.get("confidence", 0.8),
"method": ai_result.get("method", "ai"),
}
except Exception as e:
logger.warning(f"AI 分型面检测失败,回退到几何方法:{e}")
normal_stats = self._analyze_parting_direction(shape)
parting_surface = self._create_parting_surface_from_direction(
shape,
analysis,
normal_stats.get("primary_direction", [0, 0, 1]),
)
parting_line = self._calculate_parting_line(shape, parting_surface)
return {
"surface": parting_surface,
"line": parting_line,
"direction": normal_stats.get("primary_direction", [0, 0, 1]),
"confidence": normal_stats.get("confidence", 0.5),
"method": "geometric",
}
def _extract_plane_metadata(self, surface: Any, analysis: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
"""提取分型面法向、原点和参数范围。"""
default_origin = [0.0, 0.0, 0.0]
if analysis:
default_origin = analysis.get("bounding_box", {}).get("center", default_origin)
try:
adaptor = BRepAdaptor_Surface(surface)
plane = adaptor.Plane()
origin = plane.Location()
normal = plane.Axis().Direction()
return {
"normal": [float(normal.X()), float(normal.Y()), float(normal.Z())],
"origin": [float(origin.X()), float(origin.Y()), float(origin.Z())],
"bounds": {
"u_range": [float(adaptor.FirstUParameter()), float(adaptor.LastUParameter())],
"v_range": [float(adaptor.FirstVParameter()), float(adaptor.LastVParameter())],
},
}
except Exception as e:
logger.warning(f"分型面几何提取失败,使用默认值: {e}")
return {
"normal": [0.0, 0.0, 1.0],
"origin": [float(default_origin[0]), float(default_origin[1]), float(default_origin[2])],
"bounds": {"u_range": [-200.0, 200.0], "v_range": [-200.0, 200.0]},
}
def _split_cavity_core(self, shape: Any, parting_surface: Any, margin: int = 20) -> Tuple[Any, Any]:
"""
分离型腔和型芯
-82
View File
@@ -237,7 +237,6 @@ class MoldCavityGenerator(BaseMoldGenerator):
logger.info(f"转换得到 {len(regions)} 个兼容倒扣区域")
return regions
<<<<<<< HEAD
def _analyze_face_normals(self, shape: Any) -> gp_Dir:
"""
分析产品表面的法向量分布,找出最优分型方向
@@ -249,87 +248,6 @@ class MoldCavityGenerator(BaseMoldGenerator):
"""
face_normals = []
explorer = TopExp_Explorer(shape, TopAbs_FACE)
=======
return scaled_shape
def _apply_draft_angles(self, shape: Any, parting_surface: Any) -> Any:
"""添加拔模角(简化实现)"""
# 实际实现需要复杂的拔模面处理
# 这里返回原始形状(假设已在CAD中处理)
logger.warning("拔模角处理为简化实现,建议在设计阶段处理")
return shape
def _split_cavity_core(self, shape: Any, parting_surface: Any) -> Tuple[Any, Any]:
"""
按分型面将模具分为 A 板(定模/型腔侧)和 B 板(动模/型芯侧)
流程:
1. 创建模具块(包围产品的长方体)
2. 用分型面 Z 位置切出上半块和下半块
3. 从上半块减去产品 → A 板(含型腔负形)
4. 从下半块减去产品 → B 板(含型芯负形)
"""
try:
from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox
from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Cut
from OCC.Core.TopExp import TopExp_Explorer
from OCC.Core.TopAbs import TopAbs_SOLID
# 获取产品边界框
from OCC.Core.Bnd import Bnd_Box
from OCC.Core.BRepBndLib import brepbndlib
bbox = Bnd_Box()
brepbndlib.Add(shape, bbox)
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
# 获取分型面 Z 位置(包围盒 Z 中心)
parting_z = (zmin + zmax) / 2
# 计算模具块尺寸(比产品大一定余量)
margin = 20 # mm
mold_xmin = xmin - margin
mold_ymin = ymin - margin
mold_zmin = zmin - margin
mold_xmax = xmax + margin
mold_ymax = ymax + margin
mold_zmax = zmax + margin
# 上半块: 从 parting_z 到 mold_zmax
upper_block = BRepPrimAPI_MakeBox(
gp_Pnt(mold_xmin, mold_ymin, parting_z),
gp_Pnt(mold_xmax, mold_ymax, mold_zmax)
).Shape()
# 下半块: 从 mold_zmin 到 parting_z
lower_block = BRepPrimAPI_MakeBox(
gp_Pnt(mold_xmin, mold_ymin, mold_zmin),
gp_Pnt(mold_xmax, mold_ymax, parting_z)
).Shape()
# A 板(定模)= 上半块 - 产品
a_plate_op = BRepAlgoAPI_Cut(upper_block, shape)
if a_plate_op.IsDone():
a_plate = a_plate_op.Shape()
logger.info(f"A板(定模)生成成功: Z={parting_z:.1f} ~ {mold_zmax:.1f}")
else:
logger.warning("A板布尔减法失败,使用上半块")
a_plate = upper_block
# B 板(动模)= 下半块 - 产品
b_plate_op = BRepAlgoAPI_Cut(lower_block, shape)
if b_plate_op.IsDone():
b_plate = b_plate_op.Shape()
logger.info(f"B板(动模)生成成功: Z={mold_zmin:.1f} ~ {parting_z:.1f}")
else:
logger.warning("B板布尔减法失败,使用下半块")
b_plate = lower_block
return a_plate, b_plate
except Exception as e:
logger.error(f"分模失败: {e}")
return shape, shape
>>>>>>> 77c4885a4020b609f4964184661f42e936814c4f
while explorer.More():
face = TopoDS_Face(explorer.Current())
+275
View File
@@ -0,0 +1,275 @@
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
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,
) -> Dict[str, Any]:
generator = self.aluminum_foam_generator if is_foam_material else self.mold_generator
generator.set_material(material["name"])
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]
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"]
best_scheme = scored_schemes[0]
return {
"best_scheme_id": best_scheme["scheme_id"],
"candidate_schemes": scored_schemes,
"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", {})
)
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", "中面")
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", "中面"),
"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,
}
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()
}
+134
View File
@@ -0,0 +1,134 @@
from typing import Dict, Any, List
class PartingCandidateGenerator:
"""生成候选分型方向,供多方案分模规划器使用。"""
_AXIS_DEFS = {
"X": {"direction": [1.0, 0.0, 0.0], "title": "X轴侧向开模方案"},
"Y": {"direction": [0.0, 1.0, 0.0], "title": "Y轴侧向开模方案"},
"Z": {"direction": [0.0, 0.0, 1.0], "title": "Z轴上下开模方案"},
}
def generate_candidates(
self,
analysis: Dict[str, Any],
is_foam_material: bool = False,
max_candidates: int = 3,
) -> List[Dict[str, Any]]:
bbox_dims = analysis.get("bounding_box", {}).get("dimensions", [0, 0, 0])
axis_metrics = self._build_axis_metrics(bbox_dims, analysis, is_foam_material)
axis_order = [item["axis"] for item in sorted(
axis_metrics,
key=lambda item: item["priority_score"],
reverse=True,
)]
candidates = []
for idx, axis in enumerate(axis_order[:max_candidates], start=1):
axis_def = self._AXIS_DEFS[axis]
metrics = next(item for item in axis_metrics if item["axis"] == axis)
candidates.append({
"scheme_id": f"scheme_{idx}",
"rank_hint": idx,
"axis": axis,
"direction": axis_def["direction"],
"title": axis_def["title"] if idx > 1 else "推荐候选方向",
"method": metrics["method"],
"projected_area_cm2": metrics["projected_area_cm2"],
"opening_span_mm": metrics["opening_span_mm"],
"priority_score": metrics["priority_score"],
"reason": self._build_reason(metrics, is_foam_material),
})
return candidates
@staticmethod
def _projected_area_for_axis(bbox_dims: List[float], axis: str) -> float:
if len(bbox_dims) < 3:
return 0.0
if axis == "X":
return (bbox_dims[1] * bbox_dims[2]) / 100
if axis == "Y":
return (bbox_dims[0] * bbox_dims[2]) / 100
return (bbox_dims[0] * bbox_dims[1]) / 100
def _build_axis_metrics(
self,
bbox_dims: List[float],
analysis: Dict[str, Any],
is_foam_material: bool,
) -> List[Dict[str, Any]]:
padded_dims = (bbox_dims + [0.0, 0.0, 0.0])[:3]
max_dim = max(max(padded_dims), 1.0)
max_area = max(
self._projected_area_for_axis(padded_dims, axis)
for axis in ("X", "Y", "Z")
) or 1.0
inertia_matrix = analysis.get("inertia_matrix", [])
inertia_diag = [
float(inertia_matrix[i][i]) if i < len(inertia_matrix) and i < len(inertia_matrix[i]) else 0.0
for i in range(3)
]
max_inertia = max(max(inertia_diag), 1.0)
axis_normal_stats = analysis.get("axis_normal_stats", {})
metrics = []
for axis, idx in (("X", 0), ("Y", 1), ("Z", 2)):
opening_span = float(padded_dims[idx])
projected_area = self._projected_area_for_axis(padded_dims, axis)
thin_axis_score = (max_dim - opening_span) / max_dim
compact_projection_score = 1.0 - min(projected_area / max_area, 1.0)
inertia_score = 1.0 - min((inertia_diag[idx] if idx < len(inertia_diag) else 0.0) / max_inertia, 1.0)
normal_alignment_score = min(float(axis_normal_stats.get(axis, 0.0)) / 100.0, 1.0)
priority_score = (
thin_axis_score * 0.30
+ compact_projection_score * 0.25
+ inertia_score * 0.15
+ normal_alignment_score * 0.30
)
method = "geometric_primary"
if normal_alignment_score >= thin_axis_score and normal_alignment_score >= compact_projection_score:
method = "face_normal_primary"
elif compact_projection_score >= thin_axis_score and compact_projection_score >= inertia_score:
method = "projected_area_backup"
elif inertia_score > thin_axis_score:
method = "balanced_backup"
if is_foam_material and axis == "Z":
priority_score += 0.25
method = "foam_axis_rule"
metrics.append({
"axis": axis,
"opening_span_mm": round(opening_span, 2),
"projected_area_cm2": round(projected_area, 2),
"thin_axis_score": round(thin_axis_score * 100, 2),
"compact_projection_score": round(compact_projection_score * 100, 2),
"inertia_score": round(inertia_score * 100, 2),
"normal_alignment_score": round(normal_alignment_score * 100, 2),
"priority_score": round(priority_score * 100, 2),
"method": method,
})
return metrics
@staticmethod
def _build_reason(metrics: Dict[str, Any], is_foam_material: bool) -> str:
axis = metrics["axis"]
projected_area = metrics["projected_area_cm2"]
opening_span = metrics["opening_span_mm"]
if is_foam_material and axis == "Z":
return (
f"泡沫模具优先上下开模,开模跨度 {opening_span:.2f} mm,"
f"投影面积约 {projected_area:.2f} cm²"
)
return (
f"{axis} 轴方向开模跨度 {opening_span:.2f} mm,"
f"投影面积约 {projected_area:.2f} cm²,"
f"法向匹配度 {metrics.get('normal_alignment_score', 0):.2f},"
f"综合几何优先级 {metrics['priority_score']:.2f}"
)
+129
View File
@@ -0,0 +1,129 @@
from typing import Dict, Any, List
import re
class PartingSchemeScorer:
"""对候选分模方案打分并排序。"""
def score_schemes(self, schemes: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
scored = []
for scheme in schemes:
score_breakdown = self._score_scheme(scheme)
total_score = round(
score_breakdown["manufacturability"] * 0.30
+ score_breakdown["undercut_complexity"] * 0.25
+ score_breakdown["parting_quality"] * 0.20
+ score_breakdown["machining_cost"] * 0.15
+ score_breakdown["risk"] * 0.10,
2,
)
scored_scheme = dict(scheme)
scored_scheme["score_breakdown"] = score_breakdown
scored_scheme["score"] = total_score
scored_scheme["summary"] = self._build_summary(scored_scheme)
scored.append(scored_scheme)
scored.sort(key=lambda item: item["score"], reverse=True)
for rank, scheme in enumerate(scored, start=1):
scheme["rank"] = rank
scheme["title"] = "推荐方案" if rank == 1 else f"备选方案 {rank}"
return scored
def _score_scheme(self, scheme: Dict[str, Any]) -> Dict[str, float]:
cavity_data = scheme.get("cavity_data", {})
key_info = scheme.get("key_info", {})
candidate_priority = float(scheme.get("priority_score", 60.0))
offset_ratio = abs(float(scheme.get("offset_ratio", 0.0)))
mold_cavities = cavity_data.get("mold_cavities", {})
quality_checks = cavity_data.get("quality_checks", {})
quality_considerations = key_info.get("quality_considerations", {})
manufacturing_info = cavity_data.get("manufacturing_info", {})
cavity_vertices = mold_cavities.get("cavity", {}).get("vertex_count", 0)
core_vertices = mold_cavities.get("core", {}).get("vertex_count", 0)
manufacturability = 95.0 if cavity_vertices > 0 and core_vertices > 0 else 55.0
undercut_regions = quality_checks.get("undercut_regions") or cavity_data.get("undercut_regions", [])
side_actions = quality_checks.get("side_actions") or cavity_data.get("side_actions", {})
summary = side_actions.get("summary", {})
slider_count = len(side_actions.get("slider_mechanisms", []))
lifter_count = len(side_actions.get("lifter_mechanisms", []))
undercut_count = len(undercut_regions)
undercut_complexity = max(35.0, 100.0 - undercut_count * 12.0 - slider_count * 8.0 - lifter_count * 6.0)
if summary.get("complexity") == "high":
undercut_complexity = max(30.0, undercut_complexity - 10.0)
parting_line = scheme.get("parting", {}).get("line", [])
parting_length = self._calculate_polyline_length(parting_line)
smoothness = quality_checks.get("parting_line_smoothness", 85.0)
parting_quality = max(
40.0,
min(
100.0,
smoothness - min(parting_length / 100.0, 20.0) + 10.0 + candidate_priority * 0.10 - offset_ratio * 25.0
)
)
mold_size = manufacturing_info.get("estimated_mold_size", {})
mold_volume_factor = (
float(mold_size.get("length", 0))
* float(mold_size.get("width", 0))
* float(mold_size.get("height", 0))
) / 1_000_000 if mold_size else 0.0
machining_cost = max(35.0, 95.0 - min(mold_volume_factor / 10.0, 25.0) - slider_count * 5.0)
warpage_risk = str(quality_considerations.get("warpage_risk", "low")).lower()
risk_base = 92.0
if "高" in warpage_risk or "high" in warpage_risk:
risk_base = 55.0
elif "中" in warpage_risk or "medium" in warpage_risk:
risk_base = 75.0
clamping_force = self._parse_first_number(manufacturing_info.get("estimated_clamping_force", "0"))
if clamping_force > 500:
risk_base -= 8.0
if scheme.get("method") == "foam_axis_rule":
risk_base += 4.0
risk = max(35.0, risk_base)
return {
"manufacturability": round(manufacturability, 2),
"undercut_complexity": round(undercut_complexity, 2),
"parting_quality": round(parting_quality, 2),
"machining_cost": round(machining_cost, 2),
"risk": round(risk, 2),
}
def _build_summary(self, scheme: Dict[str, Any]) -> str:
cavity_data = scheme.get("cavity_data", {})
quality_checks = cavity_data.get("quality_checks", {})
manufacturing_info = cavity_data.get("manufacturing_info", {})
side_actions = quality_checks.get("side_actions") or cavity_data.get("side_actions", {})
undercut_count = len(quality_checks.get("undercut_regions") or cavity_data.get("undercut_regions", []))
slider_count = len(side_actions.get("slider_mechanisms", []))
lifter_count = len(side_actions.get("lifter_mechanisms", []))
axis = scheme.get("parting", {}).get("axis", "Z")
offset_label = scheme.get("offset_label", "中面")
clamping_force = manufacturing_info.get("estimated_clamping_force", "自动计算")
return (
f"{axis} 轴开模,分型面位置 {offset_label},倒扣 {undercut_count} 处,"
f"滑块 {slider_count} 组,斜顶 {lifter_count} 组,"
f"预估锁模力 {clamping_force}"
)
@staticmethod
def _calculate_polyline_length(points: List[List[float]]) -> float:
total = 0.0
for idx in range(1, len(points)):
p1 = points[idx - 1]
p2 = points[idx]
total += ((p2[0] - p1[0]) ** 2 + (p2[1] - p1[1]) ** 2 + (p2[2] - p1[2]) ** 2) ** 0.5
return total
@staticmethod
def _parse_first_number(value: Any) -> float:
if value is None:
return 0.0
matches = re.findall(r"\d+(?:\.\d+)?", str(value))
return float(matches[0]) if matches else 0.0
+1 -1
View File
@@ -124,7 +124,7 @@ app.mount("/html", StaticFiles(directory=html_output_dir), name="html")
app.include_router(auth_router)
app.include_router(inventory_router)
try:
from api.routes import router as moldinsight_router
from api.v1 import router as moldinsight_router
except Exception as e:
moldinsight_router = None
print(f"[WARN] MoldInsight路由未加载: {e}")
+75
View File
@@ -299,3 +299,78 @@ class CalculationService:
}
return detailed_cavity_json
@classmethod
def build_plan_result(
cls,
geometry_data: Dict[str, Any],
material: Dict[str, Any],
file_path: str,
plan_result: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""构建多方案分模结果,并保留单方案兼容字段。"""
if not plan_result or not plan_result.get("candidate_schemes"):
legacy = cls.build_detailed_cavity_json(
geometry_data=geometry_data,
material=material,
file_path=file_path,
cavity_mesh_data=None,
)
return {
"best_scheme_id": "scheme_1",
"candidate_schemes": [
{
"scheme_id": "scheme_1",
"rank": 1,
"title": "推荐方案",
"method": "legacy_fallback",
"score": 60.0,
"score_breakdown": {},
"summary": "当前模型未生成多方案,返回兼容单方案结果",
"parting": {
"axis": legacy.get("metadata", {}).get("parting_direction", "Z"),
"direction": None,
"surface": legacy.get("parting_surface", {}),
"line": [],
},
"cavity_data": legacy,
"key_info": legacy.get("mold_cavities", {}).get("cavity_key_info", {}),
"undercut_regions": legacy.get("undercut_regions", []),
"side_actions": legacy.get("side_actions", {}),
}
],
"global_summary": {
"scheme_count": 1,
"recommended_reason": "兼容旧版单方案结果",
},
"cavity_data": legacy,
"key_info": legacy.get("mold_cavities", {}).get("cavity_key_info", {}),
}
candidate_schemes = plan_result.get("candidate_schemes", [])
best_scheme = cls.get_best_scheme(plan_result)
result = {
"best_scheme_id": plan_result.get("best_scheme_id"),
"candidate_schemes": candidate_schemes,
"global_summary": plan_result.get("global_summary", {}),
"cavity_data": best_scheme.get("cavity_data", {}) if best_scheme else {},
"key_info": best_scheme.get("key_info", {}) if best_scheme else {},
}
return result
@staticmethod
def get_best_scheme(plan_result: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
if not plan_result:
return None
schemes = plan_result.get("candidate_schemes", [])
if not schemes:
return None
best_scheme_id = plan_result.get("best_scheme_id")
if best_scheme_id:
for scheme in schemes:
if scheme.get("scheme_id") == best_scheme_id:
return scheme
return schemes[0]
+84 -34
View File
@@ -15,6 +15,7 @@ from core.mold_generator import MoldCavityGenerator
from core.aluminum_foam_mold import AluminumFoamMoldGenerator
from core.mold_quality_inspector import AluminumFoamMoldQualityInspector
from core.mesh_generator import MeshGenerator
from core.multi_scheme_planner import MultiSchemeMoldPlanner
from services.storage_integration_rustfs import StorageIntegrationService
from services.redis_task_manager import redis_task_manager
from services.material_service import MaterialService
@@ -39,6 +40,7 @@ class ProcessingService:
self.mesh_generator = MeshGenerator(quality="medium")
self.html_generator = HTMLGenerator()
self.storage_service = StorageIntegrationService()
self.multi_scheme_planner = MultiSchemeMoldPlanner()
# ─── 对外入口 ───
@@ -127,7 +129,7 @@ class ProcessingService:
selected_material = MaterialService.get_material(requested_material)
is_foam_material = MaterialService.is_foam_material(requested_material)
cavity_mesh_data = await self._step_generate_cavity(
plan_result = await self._step_generate_cavity(
shape, selected_material, is_foam_material
)
@@ -136,19 +138,25 @@ class ProcessingService:
db_session, task_id, "processing", 60, "生成型腔详细数据"
)
detailed_cavity_json = CalculationService.build_detailed_cavity_json(
detailed_cavity_json = CalculationService.build_plan_result(
geometry_data=geometry_data,
material=selected_material,
file_path=str(file_path),
cavity_mesh_data=cavity_mesh_data,
plan_result=plan_result,
)
if cavity_mesh_data and "mold_cavities" in cavity_mesh_data:
mold_cavities = cavity_mesh_data["mold_cavities"]
logger.info(f"型腔网格数据已合并: cavity {mold_cavities.get('cavity', {}).get('vertex_count', 0)} 顶点")
best_scheme = CalculationService.get_best_scheme(detailed_cavity_json)
best_cavity_data = best_scheme.get("cavity_data", {}) if best_scheme else {}
best_key_info = best_scheme.get("key_info", {}) if best_scheme else {}
if best_cavity_data.get("mold_cavities"):
cavity_geometry = best_cavity_data["mold_cavities"].get("cavity", {})
logger.info(
f"推荐方案型腔数据已合并: cavity {cavity_geometry.get('vertex_count', 0)} 顶点"
)
# 5. 生成关键信息
cavity_key_info = detailed_cavity_json["mold_cavities"]["cavity_key_info"]
cavity_key_info = best_key_info
# 6. 保存几何数据到数据库
await self.storage_service.update_task_status(
@@ -162,12 +170,7 @@ class ProcessingService:
geometry_data.get("analysis_method", "mold_cavity"),
)
# 7. 保存模具型腔数据
await self.storage_service.save_mold_cavity_data(
db_session, stp_file_id, detailed_cavity_json
)
# 8. 生成HTML可视化
# 7. 生成HTML可视化
await self.storage_service.update_task_status(
db_session, task_id, "processing", 85, "生成可视化报告"
)
@@ -180,10 +183,26 @@ class ProcessingService:
"point_count": mesh_result.get("point_count", 0),
}
detailed_cavity_json = await self._attach_scheme_previews(
detailed_cavity_json=detailed_cavity_json,
geometry_data=geometry_data,
stp_filename=Path(file_path).name,
pointcloud_data=pointcloud_data,
)
best_scheme = CalculationService.get_best_scheme(detailed_cavity_json)
best_cavity_data = best_scheme.get("cavity_data", {}) if best_scheme else best_cavity_data
best_key_info = best_scheme.get("key_info", {}) if best_scheme else best_key_info
# 8. 保存模具型腔数据(包含方案级预览链接)
await self.storage_service.save_mold_cavity_data(
db_session, stp_file_id, detailed_cavity_json
)
html_file_path = self.html_generator.generate_and_save_visualization(
geometry_data,
Path(file_path).name,
cavity_data=detailed_cavity_json,
cavity_data=best_cavity_data,
pointcloud_data=pointcloud_data,
)
@@ -233,9 +252,12 @@ class ProcessingService:
await redis_task_manager.update_task(task_id, {
"geometry_data": geometry_data,
"analysis_result": analysis_result,
"cavity_data": detailed_cavity_json,
"key_info": detailed_cavity_json,
"html_file": f"/html/{Path(html_file_path).name}",
"plan_result": detailed_cavity_json,
"candidate_schemes": detailed_cavity_json.get("candidate_schemes", []),
"best_scheme_id": detailed_cavity_json.get("best_scheme_id"),
"cavity_data": best_cavity_data,
"key_info": best_key_info,
"html_file": best_scheme.get("html_file", f"/html/{Path(html_file_path).name}") if best_scheme else f"/html/{Path(html_file_path).name}",
"verification": verification_result,
"status": ProcessingStatus.COMPLETED,
"completed_at": str(datetime.now()),
@@ -327,28 +349,24 @@ class ProcessingService:
async def _step_generate_cavity(
self, shape, selected_material: dict, is_foam_material: bool,
) -> Optional[Dict[str, Any]]:
"""生成模具型腔数据"""
cavity_mesh_data = None
"""生成多方案分模结果"""
plan_result = None
try:
if shape:
if is_foam_material:
self.aluminum_foam_generator.set_material(selected_material["name"])
cavity_result = self.aluminum_foam_generator.generate_mold_cavities(shape)
cavity_mesh_data = self.aluminum_foam_generator.generate_detailed_cavity_json(cavity_result)
logger.info(f"使用铝泡沫模具生成器: {selected_material['name']}")
else:
cavity_result = self.mold_generator.generate_mold_cavities(shape)
cavity_mesh_data = self.mold_generator.generate_detailed_cavity_json(cavity_result)
logger.info(f"使用普通塑料模具生成器: {selected_material['name']}")
if cavity_mesh_data:
logger.info(f"型腔网格数据生成完成: {cavity_mesh_data.get('mold_cavities', {}).get('cavity', {}).get('vertex_count', 0)} 顶点")
plan_result = self.multi_scheme_planner.generate_plan(
shape=shape,
material=selected_material,
is_foam_material=is_foam_material,
)
logger.info(
f"多方案分模完成: 生成 {len(plan_result.get('candidate_schemes', []))} 套方案"
)
except Exception as cavity_err:
logger.warning(f"型腔生成失败,使用简化数据: {cavity_err}")
logger.warning(f"多方案分模失败,使用简化数据: {cavity_err}")
traceback.print_exc()
cavity_mesh_data = None
plan_result = None
return cavity_mesh_data
return plan_result
async def _step_verify(
self, file_path: str, db_session: AsyncSession,
@@ -379,6 +397,38 @@ class ProcessingService:
logger.warning(f"FreeCAD验证失败(不影响主流程): {ve}")
return {"status": "error", "error": str(ve)}
async def _attach_scheme_previews(
self,
detailed_cavity_json: Dict[str, Any],
geometry_data: Dict[str, Any],
stp_filename: str,
pointcloud_data: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""为每个候选分模方案生成独立HTML预览链接。"""
candidate_schemes = detailed_cavity_json.get("candidate_schemes", [])
if not candidate_schemes:
return detailed_cavity_json
for scheme in candidate_schemes:
cavity_data = scheme.get("cavity_data")
if not cavity_data:
continue
suffix = scheme.get("scheme_id")
html_path = self.html_generator.generate_and_save_visualization(
geometry_data,
stp_filename,
cavity_data=cavity_data,
pointcloud_data=pointcloud_data,
suffix=suffix,
)
scheme["html_file"] = f"/html/{Path(html_path).name}"
best_scheme = CalculationService.get_best_scheme(detailed_cavity_json)
if best_scheme:
detailed_cavity_json["html_file"] = best_scheme.get("html_file")
return detailed_cavity_json
# ─── 指标持久化 ───
async def _save_analysis_metrics(self, session: AsyncSession, stp_file_id: int, analysis_result: dict):
+44 -2
View File
@@ -62,6 +62,7 @@ class TaskQueryService:
cavity_json: Optional[Dict[str, Any]] = file_with_data.get("mold_cavity_data")
features_json: List[Dict[str, Any]] = file_with_data.get("features", [])
recommendations_json: List[Dict[str, Any]] = file_with_data.get("recommendations", [])
cavity_view = TaskQueryService._extract_cavity_view(cavity_json)
# 组装网格摘要
mesh_summary = await TaskQueryService._get_mesh_summary(db_session, stp_file.id)
@@ -78,6 +79,8 @@ class TaskQueryService:
pass
if html_file_record and html_file_record.filename:
html_file_url = f"/html/{html_file_record.filename}"
if cavity_view.get("html_file"):
html_file_url = cavity_view.get("html_file")
# 构造与内存任务兼容的任务视图
task_view = {
@@ -93,8 +96,11 @@ class TaskQueryService:
if processing_task.completed_time
else "",
"geometry_data": geometry_json,
"key_info": cavity_json,
"cavity_data": cavity_json,
"key_info": cavity_view.get("key_info"),
"cavity_data": cavity_view.get("cavity_data"),
"candidate_schemes": cavity_view.get("candidate_schemes", []),
"best_scheme_id": cavity_view.get("best_scheme_id"),
"plan_result": cavity_json,
"mesh_summary": mesh_summary,
"html_file": html_file_url,
"analysis_result": {
@@ -141,3 +147,39 @@ class TaskQueryService:
"quality": mesh_record.quality,
}
return None
@staticmethod
def _extract_cavity_view(cavity_json: Optional[Dict[str, Any]]) -> Dict[str, Any]:
"""兼容旧单方案与新多方案结果视图。"""
if not cavity_json:
return {
"cavity_data": None,
"key_info": None,
"candidate_schemes": [],
"best_scheme_id": None,
}
candidate_schemes = cavity_json.get("candidate_schemes")
if candidate_schemes:
best_scheme_id = cavity_json.get("best_scheme_id")
best_scheme = candidate_schemes[0]
if best_scheme_id:
for scheme in candidate_schemes:
if scheme.get("scheme_id") == best_scheme_id:
best_scheme = scheme
break
return {
"cavity_data": best_scheme.get("cavity_data"),
"key_info": best_scheme.get("key_info"),
"candidate_schemes": candidate_schemes,
"best_scheme_id": best_scheme_id or best_scheme.get("scheme_id"),
"html_file": best_scheme.get("html_file"),
}
return {
"cavity_data": cavity_json,
"key_info": cavity_json,
"candidate_schemes": [],
"best_scheme_id": None,
"html_file": cavity_json.get("html_file"),
}
+4 -2
View File
@@ -620,7 +620,8 @@ class HTMLGenerator:
geometry_data: Dict[str, Any],
stp_filename: str,
cavity_data: Optional[Dict[str, Any]] = None,
pointcloud_data: Optional[Dict[str, Any]] = None
pointcloud_data: Optional[Dict[str, Any]] = None,
suffix: Optional[str] = None,
) -> str:
"""生成并保存可视化HTML文件"""
try:
@@ -635,7 +636,8 @@ class HTMLGenerator:
# 创建文件名
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
safe_filename = stp_filename.replace('.', '_').replace(' ', '_')
html_filename = f"{safe_filename}_{timestamp}.html"
suffix_part = f"_{suffix}" if suffix else ""
html_filename = f"{safe_filename}{suffix_part}_{timestamp}.html"
# 保存文件
file_path = self.save_html_file(html_content, html_filename)
+152 -17
View File
@@ -1220,7 +1220,8 @@ const ResultView = {
const state = reactive({
task: null,
loading: true,
error: ''
error: '',
selectedSchemeId: null
});
const loadTask = async () => {
@@ -1232,6 +1233,11 @@ const ResultView = {
console.log('cavity_data:', state.task.cavity_data);
console.log('analysis_result:', state.task.analysis_result);
console.log('geometry_data:', state.task.geometry_data);
if (state.task?.best_scheme_id) {
state.selectedSchemeId = state.task.best_scheme_id;
} else if (state.task?.candidate_schemes?.length) {
state.selectedSchemeId = state.task.candidate_schemes[0].scheme_id;
}
} catch (e) {
state.error = handleApiError(e, '加载任务详情');
} finally {
@@ -1261,6 +1267,27 @@ const ResultView = {
return material === 'aluminum_foam';
};
const candidateSchemes = computed(() => state.task?.candidate_schemes || []);
const selectedScheme = computed(() => {
if (!candidateSchemes.value.length) return null;
return candidateSchemes.value.find(s => s.scheme_id === state.selectedSchemeId) || candidateSchemes.value[0];
});
const selectedCavityData = computed(() => selectedScheme.value?.cavity_data || state.task?.cavity_data || null);
const selectedKeyInfo = computed(() => selectedScheme.value?.key_info || state.task?.key_info || null);
const selectedHtmlFile = computed(() => selectedScheme.value?.html_file || state.task?.html_file || '');
const selectScheme = (schemeId) => {
state.selectedSchemeId = schemeId;
};
const formatSchemeDirection = (scheme) => {
if (!scheme) return 'N/A';
if (scheme.parting?.axis) return `${scheme.parting.axis} 轴`;
const dir = scheme.parting?.direction;
if (!Array.isArray(dir)) return 'N/A';
return `[${dir.map(v => Number(v).toFixed(2)).join(', ')}]`;
};
const exportCAD = async (format) => {
try {
const taskId = route.params.taskId;
@@ -1293,7 +1320,22 @@ const ResultView = {
}
};
return { state, formatFileSize, formatDateTime, formatNumber, getPriorityText, isFoamMaterial, exportCAD };
return {
state,
candidateSchemes,
selectedScheme,
selectedCavityData,
selectedKeyInfo,
selectedHtmlFile,
formatFileSize,
formatDateTime,
formatNumber,
getPriorityText,
isFoamMaterial,
exportCAD,
selectScheme,
formatSchemeDirection
};
},
template: `
<div class="page-container">
@@ -1378,7 +1420,97 @@ const ResultView = {
</div>
</div>
<div v-if="state.task.cavity_data || state.task.key_info" class="viewer-section">
<div v-if="candidateSchemes.length" class="viewer-section">
<h3>候选分模方案</h3>
<div class="result-grid">
<div
v-for="scheme in candidateSchemes"
:key="scheme.scheme_id"
class="result-card"
:style="state.selectedSchemeId === scheme.scheme_id ? 'border: 2px solid var(--primary-color);' : ''"
>
<div class="summary-header">
<h4>{{ scheme.title || scheme.scheme_id }}</h4>
<span class="badge badge-info">总分 {{ formatNumber(scheme.score) }}</span>
</div>
<div class="info-list">
<div class="info-item">
<span class="info-label">分型方向</span>
<span class="info-value">{{ formatSchemeDirection(scheme) }}</span>
</div>
<div class="info-item">
<span class="info-label">分型面位置</span>
<span class="info-value">{{ scheme.offset_label || '中面' }}</span>
</div>
<div class="info-item">
<span class="info-label">候选优先级</span>
<span class="info-value">{{ formatNumber(scheme.priority_score || 0) }}</span>
</div>
<div class="info-item">
<span class="info-label">法向匹配度</span>
<span class="info-value">{{ formatNumber(scheme.normal_alignment_score || 0) }}</span>
</div>
<div class="info-item">
<span class="info-label">方法</span>
<span class="info-value">{{ scheme.method || 'rule_based' }}</span>
</div>
<div class="info-item">
<span class="info-label">方案说明</span>
<span class="info-value">{{ scheme.summary || scheme.reason || '暂无说明' }}</span>
</div>
<div class="info-item">
<span class="info-label">可制造性</span>
<span class="info-value">{{ formatNumber(scheme.score_breakdown?.manufacturability || 0) }}</span>
</div>
<div class="info-item">
<span class="info-label">分型质量</span>
<span class="info-value">{{ formatNumber(scheme.score_breakdown?.parting_quality || 0) }}</span>
</div>
<div class="info-item">
<span class="info-label">风险得分</span>
<span class="info-value">{{ formatNumber(scheme.score_breakdown?.risk || 0) }}</span>
</div>
</div>
<button class="btn-sm btn-primary" @click="selectScheme(scheme.scheme_id)">查看此方案</button>
</div>
</div>
</div>
<div v-if="candidateSchemes.length > 1" class="viewer-section">
<h3>方案对比</h3>
<div class="result-card full-width">
<table class="data-table">
<thead>
<tr>
<th>方案</th>
<th>方向</th>
<th>位置</th>
<th>总分</th>
<th>法向匹配</th>
<th>可制造性</th>
<th>分型质量</th>
<th>倒扣数</th>
<th>锁模力</th>
</tr>
</thead>
<tbody>
<tr v-for="scheme in candidateSchemes" :key="'cmp-' + scheme.scheme_id">
<td>{{ scheme.title || scheme.scheme_id }}</td>
<td>{{ formatSchemeDirection(scheme) }}</td>
<td>{{ scheme.offset_label || '中面' }}</td>
<td>{{ formatNumber(scheme.score) }}</td>
<td>{{ formatNumber(scheme.normal_alignment_score || 0) }}</td>
<td>{{ formatNumber(scheme.score_breakdown?.manufacturability || 0) }}</td>
<td>{{ formatNumber(scheme.score_breakdown?.parting_quality || 0) }}</td>
<td>{{ scheme.key_info?.quality_considerations?.undercut_count || 0 }}</td>
<td>{{ scheme.cavity_data?.manufacturing_info?.estimated_clamping_force || 'N/A' }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<div v-if="selectedCavityData || selectedKeyInfo" class="viewer-section">
<h3>模具信息</h3>
<div class="result-grid">
<div class="result-card">
@@ -1386,29 +1518,29 @@ const ResultView = {
<div class="info-list">
<div class="info-item">
<span class="info-label">零件名称</span>
<span class="info-value">{{ state.task.cavity_data?.metadata?.part_name || state.task.key_info?.metadata?.part_name || 'N/A' }}</span>
<span class="info-value">{{ selectedCavityData?.metadata?.part_name || selectedKeyInfo?.metadata?.part_name || 'N/A' }}</span>
</div>
<div class="info-item">
<span class="info-label">材料</span>
<span class="info-value">{{ state.task.cavity_data?.metadata?.material || state.task.key_info?.metadata?.material || 'N/A' }}</span>
<span class="info-value">{{ selectedCavityData?.metadata?.material || selectedKeyInfo?.metadata?.material || 'N/A' }}</span>
</div>
</div>
</div>
<div class="result-card" v-if="state.task.cavity_data?.mold_cavities || state.task.key_info?.mold_cavities">
<div class="result-card" v-if="selectedCavityData?.mold_cavities || selectedKeyInfo?.mold_cavities">
<h4>型腔信息</h4>
<div class="info-list">
<div class="info-item">
<span class="info-label">型腔数量</span>
<span class="info-value">{{ Object.keys(state.task.cavity_data?.mold_cavities || state.task.key_info?.mold_cavities || {}).length }}</span>
<span class="info-value">{{ Object.keys(selectedCavityData?.mold_cavities || selectedKeyInfo?.mold_cavities || {}).length }}</span>
</div>
</div>
</div>
</div>
</div>
<div v-if="state.task.html_file" class="viewer-section">
<div v-if="selectedHtmlFile" class="viewer-section">
<h3>3D 预览</h3>
<iframe :src="state.task.html_file" class="viewer-frame"></iframe>
<iframe :src="selectedHtmlFile" class="viewer-frame"></iframe>
</div>
<div v-if="state.task.analysis_result" class="viewer-section">
@@ -1544,15 +1676,18 @@ const ResultView = {
<div class="info-list">
<div class="info-item">
<span class="info-label">型腔数量建议</span>
<span class="info-value">{{ state.task.cavity_data?.mold_cavities ? Object.keys(state.task.cavity_data.mold_cavities).length : 1 }} 腔</span>
<span class="info-value">{{ selectedCavityData?.mold_cavities ? Object.keys(selectedCavityData.mold_cavities).length : 1 }} 腔</span>
</div>
<div class="info-item">
<span class="info-label">模架尺寸</span>
<span class="info-value">{{ state.task.key_info?.mold_parameters?.mold_size || '基于泡沫外形自动计算' }}</span>
<span class="info-value">{{ selectedCavityData?.manufacturing_info?.estimated_mold_size ?
(selectedCavityData.manufacturing_info.estimated_mold_size.length || 0) + ' × ' +
(selectedCavityData.manufacturing_info.estimated_mold_size.width || 0) + ' × ' +
(selectedCavityData.manufacturing_info.estimated_mold_size.height || 0) + ' mm' : '基于泡沫外形自动计算' }}</span>
</div>
<div class="info-item" v-if="state.task.key_info?.geometric_characteristics">
<div class="info-item" v-if="selectedCavityData?.manufacturing_info || selectedKeyInfo?.geometric_characteristics">
<span class="info-label">预估锁模力</span>
<span class="info-value">{{ state.task.key_info.geometric_characteristics.estimated_clamping_force || '自动计算' }} 吨</span>
<span class="info-value">{{ selectedCavityData?.manufacturing_info?.estimated_clamping_force || '自动计算' }}</span>
</div>
<div class="info-item">
<span class="info-label">顶出系统</span>
@@ -1578,13 +1713,13 @@ const ResultView = {
<span class="info-label">壁厚均匀性</span>
<span class="info-value">{{ (state.task.analysis_result.quality_metrics.wall_uniformity * 100)?.toFixed(1) || 'N/A' }}%</span>
</div>
<div class="info-item" v-if="state.task.key_info?.quality_considerations">
<div class="info-item" v-if="selectedKeyInfo?.quality_considerations">
<span class="info-label">翘曲风险</span>
<span class="info-value">{{ state.task.key_info.quality_considerations.warpage_risk || '低风险' }}</span>
<span class="info-value">{{ selectedKeyInfo.quality_considerations.warpage_risk || '低风险' }}</span>
</div>
<div class="info-item" v-if="state.task.key_info?.quality_considerations">
<div class="info-item" v-if="selectedKeyInfo?.quality_considerations">
<span class="info-label">潜在焊缝线</span>
<span class="info-value">{{ state.task.key_info.quality_considerations.potential_weld_lines || 0 }} 条</span>
<span class="info-value">{{ selectedKeyInfo.quality_considerations.potential_weld_lines || 0 }} 条</span>
</div>
</div>
</div>