分模候选方案
This commit is contained in:
+6
-4
@@ -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
@@ -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)
|
||||
|
||||
@@ -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}
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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]:
|
||||
"""
|
||||
分离型腔和型芯
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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}"
|
||||
)
|
||||
@@ -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
@@ -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}")
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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"),
|
||||
}
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user