分模候选方案
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)
|
await storage_service.create_processing_task(db_session, task_id, stp_file.id)
|
||||||
|
|
||||||
# 创建任务记录(Redis 为主,内存为兼容回退)
|
# 创建任务记录(Redis 为主,内存作为兼容回退)
|
||||||
task_info = create_task_info(
|
task_info = create_task_info(
|
||||||
task_id=task_id,
|
task_id=task_id,
|
||||||
status=ProcessingStatus.PROCESSING,
|
status=ProcessingStatus.PROCESSING,
|
||||||
@@ -114,10 +114,13 @@ async def upload_stp(
|
|||||||
await redis_task_manager.set_task(task_id, task_info)
|
await redis_task_manager.set_task(task_id, task_info)
|
||||||
tasks[task_id] = task_info
|
tasks[task_id] = task_info
|
||||||
|
|
||||||
# 后台处理(统一走 ProcessingService,使用独立数据库会话)
|
# 后台处理走统一编排服务,避免请求会话在后台失效
|
||||||
background_tasks.add_task(
|
background_tasks.add_task(
|
||||||
processing_service.process_file_with_storage,
|
processing_service.process_file_with_storage,
|
||||||
task_id, file_path, stp_file.id, material
|
task_id,
|
||||||
|
file_path,
|
||||||
|
stp_file.id,
|
||||||
|
material,
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -146,7 +149,6 @@ async def get_status(task_id: str, db_session: AsyncSession = Depends(get_db_ses
|
|||||||
try:
|
try:
|
||||||
task_view = await TaskQueryService.get_task_view(db_session, task_id)
|
task_view = await TaskQueryService.get_task_view(db_session, task_id)
|
||||||
if task_view is None:
|
if task_view is None:
|
||||||
# 兼容老的仅内存任务
|
|
||||||
if task_id in tasks:
|
if task_id in tasks:
|
||||||
return tasks[task_id]
|
return tasks[task_id]
|
||||||
raise HTTPException(404, "任务不存在")
|
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.auth_service import get_current_active_user
|
||||||
from services.processing_service import processing_service
|
from services.processing_service import processing_service
|
||||||
from models.database import User
|
from models.database import User
|
||||||
|
from api.routes import tasks as legacy_tasks
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
@@ -65,6 +66,7 @@ async def upload_stp(
|
|||||||
upload_time=str(datetime.now())
|
upload_time=str(datetime.now())
|
||||||
)
|
)
|
||||||
await redis_task_manager.set_task(task_id, task_info)
|
await redis_task_manager.set_task(task_id, task_info)
|
||||||
|
legacy_tasks[task_id] = task_info
|
||||||
|
|
||||||
# 后台处理(使用独立数据库会话,避免请求会话关闭问题)
|
# 后台处理(使用独立数据库会话,避免请求会话关闭问题)
|
||||||
background_tasks.add_task(
|
background_tasks.add_task(
|
||||||
|
|||||||
+26
-178
@@ -310,43 +310,6 @@ class AluminumFoamMoldGenerator(BaseMoldGenerator):
|
|||||||
1. 优先选择 Z 轴方向分型(上下开模)
|
1. 优先选择 Z 轴方向分型(上下开模)
|
||||||
2. 分型面位置选在产品的最大轮廓处,即包围盒的 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"]
|
bbox = analysis["bounding_box"]
|
||||||
center = bbox["center"]
|
center = bbox["center"]
|
||||||
primary_direction = [0, 0, 1] # Z 轴方向
|
primary_direction = [0, 0, 1] # Z 轴方向
|
||||||
@@ -364,26 +327,40 @@ class AluminumFoamMoldGenerator(BaseMoldGenerator):
|
|||||||
|
|
||||||
logger.info(f"泡沫模具 Z 轴分型面: Z={parting_z:.2f} mm (包围盒中心)")
|
logger.info(f"泡沫模具 Z 轴分型面: Z={parting_z:.2f} mm (包围盒中心)")
|
||||||
|
|
||||||
# 3. 计算分型线
|
parting_line = self.optimize_parting_line(
|
||||||
parting_line = self._calculate_parting_line(shape, parting_surface)
|
self._calculate_parting_line(shape, parting_surface)
|
||||||
>>>>>>> 77c4885a4020b609f4964184661f42e936814c4f
|
)
|
||||||
|
|
||||||
|
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 {
|
return {
|
||||||
"primary_surface": parting_surface,
|
"primary_surface": parting_surface,
|
||||||
"primary_line": parting_line,
|
"primary_line": parting_line,
|
||||||
"primary_direction": primary_direction,
|
"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 轴分型置信度高
|
"confidence": 0.95, # Z 轴分型置信度高
|
||||||
"additional_surfaces": [],
|
"method": "z_axis_rule",
|
||||||
"surface_count": 1,
|
"additional_surfaces": additional_surfaces,
|
||||||
|
"surface_count": 1 + len(additional_surfaces),
|
||||||
"parting_direction": "Z",
|
"parting_direction": "Z",
|
||||||
"parting_position_z": parting_z,
|
"parting_position_z": parting_z,
|
||||||
>>>>>>> 77c4885a4020b609f4964184661f42e936814c4f
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def _build_undercut_regions(self, undercut_analysis: Dict[str, Any]) -> List[Dict[str, Any]]:
|
def _build_undercut_regions(self, undercut_analysis: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||||
@@ -463,120 +440,6 @@ class AluminumFoamMoldGenerator(BaseMoldGenerator):
|
|||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
return 50.0
|
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:
|
def _generate_mold_block(self, cavity: Any, analysis: Dict) -> Any:
|
||||||
"""生成完整的模具块(包含A/B板结构)"""
|
"""生成完整的模具块(包含A/B板结构)"""
|
||||||
try:
|
try:
|
||||||
@@ -656,20 +519,6 @@ class AluminumFoamMoldGenerator(BaseMoldGenerator):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def _calculate_clamping_force(self, analysis: Dict) -> str:
|
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+ 吨"
|
|
||||||
|
|
||||||
=======
|
|
||||||
"""
|
"""
|
||||||
估算锁模力(泡沫模具专用)
|
估算锁模力(泡沫模具专用)
|
||||||
|
|
||||||
@@ -694,7 +543,6 @@ class AluminumFoamMoldGenerator(BaseMoldGenerator):
|
|||||||
weight_g = volume_cm3 * self.material_density
|
weight_g = volume_cm3 * self.material_density
|
||||||
return f"{weight_g:.2f} g"
|
return f"{weight_g:.2f} g"
|
||||||
|
|
||||||
>>>>>>> 77c4885a4020b609f4964184661f42e936814c4f
|
|
||||||
def _estimate_wall_thickness(self, analysis: Dict) -> str:
|
def _estimate_wall_thickness(self, analysis: Dict) -> str:
|
||||||
"""估算壁厚范围"""
|
"""估算壁厚范围"""
|
||||||
volume = analysis.get("volume", 0)
|
volume = analysis.get("volume", 0)
|
||||||
|
|||||||
@@ -224,180 +224,6 @@ class BaseMoldGenerator:
|
|||||||
logger.error(f"产品几何分析失败: {e}")
|
logger.error(f"产品几何分析失败: {e}")
|
||||||
raise
|
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]:
|
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)} 个兼容倒扣区域")
|
logger.info(f"转换得到 {len(regions)} 个兼容倒扣区域")
|
||||||
return regions
|
return regions
|
||||||
|
|
||||||
<<<<<<< HEAD
|
|
||||||
def _analyze_face_normals(self, shape: Any) -> gp_Dir:
|
def _analyze_face_normals(self, shape: Any) -> gp_Dir:
|
||||||
"""
|
"""
|
||||||
分析产品表面的法向量分布,找出最优分型方向
|
分析产品表面的法向量分布,找出最优分型方向
|
||||||
@@ -249,87 +248,6 @@ class MoldCavityGenerator(BaseMoldGenerator):
|
|||||||
"""
|
"""
|
||||||
face_normals = []
|
face_normals = []
|
||||||
explorer = TopExp_Explorer(shape, TopAbs_FACE)
|
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():
|
while explorer.More():
|
||||||
face = TopoDS_Face(explorer.Current())
|
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(auth_router)
|
||||||
app.include_router(inventory_router)
|
app.include_router(inventory_router)
|
||||||
try:
|
try:
|
||||||
from api.routes import router as moldinsight_router
|
from api.v1 import router as moldinsight_router
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
moldinsight_router = None
|
moldinsight_router = None
|
||||||
print(f"[WARN] MoldInsight路由未加载: {e}")
|
print(f"[WARN] MoldInsight路由未加载: {e}")
|
||||||
|
|||||||
@@ -299,3 +299,78 @@ class CalculationService:
|
|||||||
}
|
}
|
||||||
|
|
||||||
return detailed_cavity_json
|
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.aluminum_foam_mold import AluminumFoamMoldGenerator
|
||||||
from core.mold_quality_inspector import AluminumFoamMoldQualityInspector
|
from core.mold_quality_inspector import AluminumFoamMoldQualityInspector
|
||||||
from core.mesh_generator import MeshGenerator
|
from core.mesh_generator import MeshGenerator
|
||||||
|
from core.multi_scheme_planner import MultiSchemeMoldPlanner
|
||||||
from services.storage_integration_rustfs import StorageIntegrationService
|
from services.storage_integration_rustfs import StorageIntegrationService
|
||||||
from services.redis_task_manager import redis_task_manager
|
from services.redis_task_manager import redis_task_manager
|
||||||
from services.material_service import MaterialService
|
from services.material_service import MaterialService
|
||||||
@@ -39,6 +40,7 @@ class ProcessingService:
|
|||||||
self.mesh_generator = MeshGenerator(quality="medium")
|
self.mesh_generator = MeshGenerator(quality="medium")
|
||||||
self.html_generator = HTMLGenerator()
|
self.html_generator = HTMLGenerator()
|
||||||
self.storage_service = StorageIntegrationService()
|
self.storage_service = StorageIntegrationService()
|
||||||
|
self.multi_scheme_planner = MultiSchemeMoldPlanner()
|
||||||
|
|
||||||
# ─── 对外入口 ───
|
# ─── 对外入口 ───
|
||||||
|
|
||||||
@@ -127,7 +129,7 @@ class ProcessingService:
|
|||||||
selected_material = MaterialService.get_material(requested_material)
|
selected_material = MaterialService.get_material(requested_material)
|
||||||
is_foam_material = MaterialService.is_foam_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
|
shape, selected_material, is_foam_material
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -136,19 +138,25 @@ class ProcessingService:
|
|||||||
db_session, task_id, "processing", 60, "生成型腔详细数据"
|
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,
|
geometry_data=geometry_data,
|
||||||
material=selected_material,
|
material=selected_material,
|
||||||
file_path=str(file_path),
|
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:
|
best_scheme = CalculationService.get_best_scheme(detailed_cavity_json)
|
||||||
mold_cavities = cavity_mesh_data["mold_cavities"]
|
best_cavity_data = best_scheme.get("cavity_data", {}) if best_scheme else {}
|
||||||
logger.info(f"型腔网格数据已合并: cavity {mold_cavities.get('cavity', {}).get('vertex_count', 0)} 顶点")
|
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. 生成关键信息
|
# 5. 生成关键信息
|
||||||
cavity_key_info = detailed_cavity_json["mold_cavities"]["cavity_key_info"]
|
cavity_key_info = best_key_info
|
||||||
|
|
||||||
# 6. 保存几何数据到数据库
|
# 6. 保存几何数据到数据库
|
||||||
await self.storage_service.update_task_status(
|
await self.storage_service.update_task_status(
|
||||||
@@ -162,12 +170,7 @@ class ProcessingService:
|
|||||||
geometry_data.get("analysis_method", "mold_cavity"),
|
geometry_data.get("analysis_method", "mold_cavity"),
|
||||||
)
|
)
|
||||||
|
|
||||||
# 7. 保存模具型腔数据
|
# 7. 生成HTML可视化
|
||||||
await self.storage_service.save_mold_cavity_data(
|
|
||||||
db_session, stp_file_id, detailed_cavity_json
|
|
||||||
)
|
|
||||||
|
|
||||||
# 8. 生成HTML可视化
|
|
||||||
await self.storage_service.update_task_status(
|
await self.storage_service.update_task_status(
|
||||||
db_session, task_id, "processing", 85, "生成可视化报告"
|
db_session, task_id, "processing", 85, "生成可视化报告"
|
||||||
)
|
)
|
||||||
@@ -180,10 +183,26 @@ class ProcessingService:
|
|||||||
"point_count": mesh_result.get("point_count", 0),
|
"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(
|
html_file_path = self.html_generator.generate_and_save_visualization(
|
||||||
geometry_data,
|
geometry_data,
|
||||||
Path(file_path).name,
|
Path(file_path).name,
|
||||||
cavity_data=detailed_cavity_json,
|
cavity_data=best_cavity_data,
|
||||||
pointcloud_data=pointcloud_data,
|
pointcloud_data=pointcloud_data,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -233,9 +252,12 @@ class ProcessingService:
|
|||||||
await redis_task_manager.update_task(task_id, {
|
await redis_task_manager.update_task(task_id, {
|
||||||
"geometry_data": geometry_data,
|
"geometry_data": geometry_data,
|
||||||
"analysis_result": analysis_result,
|
"analysis_result": analysis_result,
|
||||||
"cavity_data": detailed_cavity_json,
|
"plan_result": detailed_cavity_json,
|
||||||
"key_info": detailed_cavity_json,
|
"candidate_schemes": detailed_cavity_json.get("candidate_schemes", []),
|
||||||
"html_file": f"/html/{Path(html_file_path).name}",
|
"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,
|
"verification": verification_result,
|
||||||
"status": ProcessingStatus.COMPLETED,
|
"status": ProcessingStatus.COMPLETED,
|
||||||
"completed_at": str(datetime.now()),
|
"completed_at": str(datetime.now()),
|
||||||
@@ -327,28 +349,24 @@ class ProcessingService:
|
|||||||
async def _step_generate_cavity(
|
async def _step_generate_cavity(
|
||||||
self, shape, selected_material: dict, is_foam_material: bool,
|
self, shape, selected_material: dict, is_foam_material: bool,
|
||||||
) -> Optional[Dict[str, Any]]:
|
) -> Optional[Dict[str, Any]]:
|
||||||
"""生成模具型腔数据"""
|
"""生成多方案分模结果"""
|
||||||
cavity_mesh_data = None
|
plan_result = None
|
||||||
try:
|
try:
|
||||||
if shape:
|
if shape:
|
||||||
if is_foam_material:
|
plan_result = self.multi_scheme_planner.generate_plan(
|
||||||
self.aluminum_foam_generator.set_material(selected_material["name"])
|
shape=shape,
|
||||||
cavity_result = self.aluminum_foam_generator.generate_mold_cavities(shape)
|
material=selected_material,
|
||||||
cavity_mesh_data = self.aluminum_foam_generator.generate_detailed_cavity_json(cavity_result)
|
is_foam_material=is_foam_material,
|
||||||
logger.info(f"使用铝泡沫模具生成器: {selected_material['name']}")
|
)
|
||||||
else:
|
logger.info(
|
||||||
cavity_result = self.mold_generator.generate_mold_cavities(shape)
|
f"多方案分模完成: 生成 {len(plan_result.get('candidate_schemes', []))} 套方案"
|
||||||
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)} 顶点")
|
|
||||||
except Exception as cavity_err:
|
except Exception as cavity_err:
|
||||||
logger.warning(f"型腔生成失败,使用简化数据: {cavity_err}")
|
logger.warning(f"多方案分模失败,使用简化数据: {cavity_err}")
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
cavity_mesh_data = None
|
plan_result = None
|
||||||
|
|
||||||
return cavity_mesh_data
|
return plan_result
|
||||||
|
|
||||||
async def _step_verify(
|
async def _step_verify(
|
||||||
self, file_path: str, db_session: AsyncSession,
|
self, file_path: str, db_session: AsyncSession,
|
||||||
@@ -379,6 +397,38 @@ class ProcessingService:
|
|||||||
logger.warning(f"FreeCAD验证失败(不影响主流程): {ve}")
|
logger.warning(f"FreeCAD验证失败(不影响主流程): {ve}")
|
||||||
return {"status": "error", "error": str(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):
|
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")
|
cavity_json: Optional[Dict[str, Any]] = file_with_data.get("mold_cavity_data")
|
||||||
features_json: List[Dict[str, Any]] = file_with_data.get("features", [])
|
features_json: List[Dict[str, Any]] = file_with_data.get("features", [])
|
||||||
recommendations_json: List[Dict[str, Any]] = file_with_data.get("recommendations", [])
|
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)
|
mesh_summary = await TaskQueryService._get_mesh_summary(db_session, stp_file.id)
|
||||||
@@ -78,6 +79,8 @@ class TaskQueryService:
|
|||||||
pass
|
pass
|
||||||
if html_file_record and html_file_record.filename:
|
if html_file_record and html_file_record.filename:
|
||||||
html_file_url = f"/html/{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 = {
|
task_view = {
|
||||||
@@ -93,8 +96,11 @@ class TaskQueryService:
|
|||||||
if processing_task.completed_time
|
if processing_task.completed_time
|
||||||
else "",
|
else "",
|
||||||
"geometry_data": geometry_json,
|
"geometry_data": geometry_json,
|
||||||
"key_info": cavity_json,
|
"key_info": cavity_view.get("key_info"),
|
||||||
"cavity_data": cavity_json,
|
"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,
|
"mesh_summary": mesh_summary,
|
||||||
"html_file": html_file_url,
|
"html_file": html_file_url,
|
||||||
"analysis_result": {
|
"analysis_result": {
|
||||||
@@ -141,3 +147,39 @@ class TaskQueryService:
|
|||||||
"quality": mesh_record.quality,
|
"quality": mesh_record.quality,
|
||||||
}
|
}
|
||||||
return None
|
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],
|
geometry_data: Dict[str, Any],
|
||||||
stp_filename: str,
|
stp_filename: str,
|
||||||
cavity_data: Optional[Dict[str, Any]] = None,
|
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:
|
) -> str:
|
||||||
"""生成并保存可视化HTML文件"""
|
"""生成并保存可视化HTML文件"""
|
||||||
try:
|
try:
|
||||||
@@ -635,7 +636,8 @@ class HTMLGenerator:
|
|||||||
# 创建文件名
|
# 创建文件名
|
||||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
safe_filename = stp_filename.replace('.', '_').replace(' ', '_')
|
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)
|
file_path = self.save_html_file(html_content, html_filename)
|
||||||
|
|||||||
+152
-17
@@ -1220,7 +1220,8 @@ const ResultView = {
|
|||||||
const state = reactive({
|
const state = reactive({
|
||||||
task: null,
|
task: null,
|
||||||
loading: true,
|
loading: true,
|
||||||
error: ''
|
error: '',
|
||||||
|
selectedSchemeId: null
|
||||||
});
|
});
|
||||||
|
|
||||||
const loadTask = async () => {
|
const loadTask = async () => {
|
||||||
@@ -1232,6 +1233,11 @@ const ResultView = {
|
|||||||
console.log('cavity_data:', state.task.cavity_data);
|
console.log('cavity_data:', state.task.cavity_data);
|
||||||
console.log('analysis_result:', state.task.analysis_result);
|
console.log('analysis_result:', state.task.analysis_result);
|
||||||
console.log('geometry_data:', state.task.geometry_data);
|
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) {
|
} catch (e) {
|
||||||
state.error = handleApiError(e, '加载任务详情');
|
state.error = handleApiError(e, '加载任务详情');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -1261,6 +1267,27 @@ const ResultView = {
|
|||||||
return material === 'aluminum_foam';
|
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) => {
|
const exportCAD = async (format) => {
|
||||||
try {
|
try {
|
||||||
const taskId = route.params.taskId;
|
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: `
|
template: `
|
||||||
<div class="page-container">
|
<div class="page-container">
|
||||||
@@ -1378,7 +1420,97 @@ const ResultView = {
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
<h3>模具信息</h3>
|
||||||
<div class="result-grid">
|
<div class="result-grid">
|
||||||
<div class="result-card">
|
<div class="result-card">
|
||||||
@@ -1386,29 +1518,29 @@ const ResultView = {
|
|||||||
<div class="info-list">
|
<div class="info-list">
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="info-label">零件名称</span>
|
<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>
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="info-label">材料</span>
|
<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>
|
||||||
</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>
|
<h4>型腔信息</h4>
|
||||||
<div class="info-list">
|
<div class="info-list">
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="info-label">型腔数量</span>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="state.task.html_file" class="viewer-section">
|
<div v-if="selectedHtmlFile" class="viewer-section">
|
||||||
<h3>3D 预览</h3>
|
<h3>3D 预览</h3>
|
||||||
<iframe :src="state.task.html_file" class="viewer-frame"></iframe>
|
<iframe :src="selectedHtmlFile" class="viewer-frame"></iframe>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="state.task.analysis_result" class="viewer-section">
|
<div v-if="state.task.analysis_result" class="viewer-section">
|
||||||
@@ -1544,15 +1676,18 @@ const ResultView = {
|
|||||||
<div class="info-list">
|
<div class="info-list">
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="info-label">型腔数量建议</span>
|
<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>
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="info-label">模架尺寸</span>
|
<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>
|
||||||
<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-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>
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="info-label">顶出系统</span>
|
<span class="info-label">顶出系统</span>
|
||||||
@@ -1578,13 +1713,13 @@ const ResultView = {
|
|||||||
<span class="info-label">壁厚均匀性</span>
|
<span class="info-label">壁厚均匀性</span>
|
||||||
<span class="info-value">{{ (state.task.analysis_result.quality_metrics.wall_uniformity * 100)?.toFixed(1) || 'N/A' }}%</span>
|
<span class="info-value">{{ (state.task.analysis_result.quality_metrics.wall_uniformity * 100)?.toFixed(1) || 'N/A' }}%</span>
|
||||||
</div>
|
</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-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>
|
||||||
<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-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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user