后端模块拆分

This commit is contained in:
2026-05-29 18:10:08 +08:00
parent 5bb9bc84ea
commit 823a387118
93 changed files with 198 additions and 833 deletions
View File
+28
View File
@@ -0,0 +1,28 @@
from fastapi import APIRouter
import importlib
from shared.utils.logger import get_logger
logger = get_logger(__name__)
router = APIRouter()
def _safe_include(module_path: str, label: str):
try:
module = importlib.import_module(module_path)
router_obj = getattr(module, "router", None)
if router_obj is None:
raise ValueError("未找到 router 对象")
router.include_router(router_obj)
logger.info(f"{label} 路由加载成功")
except Exception as exc:
logger.warning(f"{label} 路由加载失败,已跳过: {exc}")
_safe_include("moldinsight.api.health_router", "健康检查")
_safe_include("moldinsight.api.upload_router", "上传")
_safe_include("moldinsight.api.task_router", "任务")
_safe_include("moldinsight.api.history_router", "历史")
_safe_include("moldinsight.api.debug_router", "调试")
_safe_include("moldinsight.api.cam_router", "CAM")
_safe_include("moldinsight.api.advanced_router", "高级")
+538
View File
@@ -0,0 +1,538 @@
from pathlib import Path
import os
from datetime import datetime
from urllib.parse import quote
from fastapi import APIRouter, Depends, HTTPException, Request
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from shared.services.auth_service import get_current_active_user
from shared.services.redis_task_manager import redis_task_manager
from moldinsight.services.processing_service import processing_service
from moldinsight.services.storage_integration_rustfs import StorageIntegrationService
from moldinsight.services.task_query_service import TaskQueryService
from shared.database.database import get_db_session
from shared.models.database import User
from shared.models.database import ProcessingTask, STPFile
from moldinsight.core.cad_exporter import CADExporter
from shared.utils.logger import get_logger
logger = get_logger(__name__)
router = APIRouter()
cad_exporter = CADExporter()
storage_service = StorageIntegrationService()
_api_routes_cache = {}
def _get_cached(key):
global _api_routes_cache
if key not in _api_routes_cache:
try:
from api import routes
except Exception as e:
logger.warning(f"api.routes 模块加载失败: {e}")
_api_routes_cache["__error__"] = str(e)
return None
_api_routes_cache.clear()
_api_routes_cache.update({
"tasks": routes.tasks,
"cavity_layout_optimizer": routes.cavity_layout_optimizer,
"mold_system_designer": routes.mold_system_designer,
"side_action_designer": routes.side_action_designer,
"mold_cam_designer": routes.mold_cam_designer,
"collision_detector": routes.collision_detector,
"toolpath_optimizer": routes.toolpath_optimizer,
"edm_designer": routes.edm_designer,
"machining_simulator": routes.machining_simulator,
"cad_exporter": routes.cad_exporter,
})
return _api_routes_cache.get(key)
async def _get_task_data(task_id: str) -> dict:
task = await redis_task_manager.get_task(task_id)
if task:
return task
tasks = _get_cached("tasks")
if tasks and task_id in tasks:
return tasks[task_id]
return None
async def _ensure_task_access(
db_session: AsyncSession,
task_id: str,
user_id: int,
):
row = await db_session.execute(
select(ProcessingTask, STPFile)
.join(STPFile, ProcessingTask.stp_file_id == STPFile.id)
.where(ProcessingTask.task_id == task_id)
)
row = row.first()
if not row:
raise HTTPException(404, "任务不存在")
_, stp_file = row
owner_id = getattr(stp_file, "user_id", None)
if owner_id is not None and owner_id != user_id:
raise HTTPException(403, "无权访问该任务的导出文件")
return row
def _get_export_artifacts(task_data: dict) -> dict:
if not isinstance(task_data, dict):
return {}
direct = task_data.get("export_artifacts")
if isinstance(direct, dict):
return direct
parameters = task_data.get("parameters")
if isinstance(parameters, dict) and isinstance(parameters.get("export_artifacts"), dict):
return parameters.get("export_artifacts")
return {}
def _expand_components(components):
requested = components or ["cavity", "core"]
if "all" in requested:
return ["cavity", "core", "parting_surface"]
return list(dict.fromkeys(requested))
def _augment_export_files(task_id: str, files):
items = []
for file in files or []:
item = dict(file)
relative_path = item.get("relative_path")
if not relative_path and item.get("filepath"):
relative_path = cad_exporter.get_relative_path(item["filepath"])
if relative_path:
relative_path = str(relative_path).replace("\\", "/").strip("/")
item["relative_path"] = relative_path
item["download_path"] = f"/api/export-download/{quote(relative_path, safe='/')}?task_id={task_id}"
items.append(item)
return items
def _merge_export_artifacts(existing: dict, export_result: dict) -> dict:
merged = dict(existing or {})
schemes = dict(merged.get("schemes") or {})
scheme_id = export_result.get("scheme_id") or "default"
previous = dict(schemes.get(scheme_id) or {})
file_map = {}
for file in previous.get("files", []):
file_map[(file.get("component"), file.get("format"))] = file
for file in export_result.get("files", []):
file_map[(file.get("component"), file.get("format"))] = file
schemes[scheme_id] = {
"base_filename": export_result.get("base_filename") or previous.get("base_filename"),
"generated_at": datetime.now().isoformat(),
"files": sorted(
file_map.values(),
key=lambda item: (item.get("component", ""), item.get("format", "")),
),
"errors": export_result.get("errors", []),
"total_files": len(file_map),
"total_errors": len(export_result.get("errors", [])),
}
merged["version"] = 1
merged["task_id"] = export_result.get("task_id") or merged.get("task_id")
merged["generated_at"] = merged.get("generated_at") or datetime.now().isoformat()
merged["schemes"] = schemes
return merged
def _select_persisted_files(task_id: str, task_data: dict, scheme_id: str, formats, components):
artifacts = _get_export_artifacts(task_data)
scheme_data = (artifacts.get("schemes") or {}).get(scheme_id)
if not scheme_data:
return None
component_list = _expand_components(components)
format_list = list(dict.fromkeys(formats or ["step", "stl"]))
expected = {(component, fmt) for component in component_list for fmt in format_list}
available = []
available_keys = set()
for file in scheme_data.get("files", []):
component = file.get("component")
fmt = file.get("format")
if component not in component_list or fmt not in format_list:
continue
relative_path = str(file.get("relative_path") or "").replace("\\", "/").strip("/")
if not relative_path:
continue
full_path = os.path.join(cad_exporter.output_dir, relative_path.replace("/", os.sep))
if not os.path.exists(full_path):
continue
available.append(file)
available_keys.add((component, fmt))
if expected and not expected.issubset(available_keys):
return None
return _augment_export_files(task_id, available)
@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 之间")
optimizer = _get_cached("cavity_layout_optimizer")
if not optimizer:
raise HTTPException(503, "服务不可用:核心模块未加载")
result = 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 moldinsight.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 moldinsight.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")
ds = _get_cached("mold_system_designer")
if not ds:
raise HTTPException(503, "服务不可用:核心模块未加载")
result = ds.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),
):
body = await request.json()
task_id = body.get("task_id")
if not task_id:
raise HTTPException(404, "缺少 task_id")
task_data = await _get_task_data(task_id)
if not task_data:
raise HTTPException(404, "任务不存在")
geometry_data = task_data.get("geometry_data")
if not geometry_data:
raise HTTPException(400, "该任务尚未完成几何分析")
from moldinsight.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:
raise HTTPException(404, "缺少 task_id")
task_data = await _get_task_data(task_id)
if not task_data:
raise HTTPException(404, "任务不存在")
sd = _get_cached("side_action_designer")
if not sd:
raise HTTPException(503, "服务不可用:核心模块未加载")
result = sd.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),
):
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")
cam = _get_cached("mold_cam_designer")
if not cam:
raise HTTPException(503, "服务不可用:核心模块未加载")
result = cam.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")
cd = _get_cached("collision_detector")
if not cd:
raise HTTPException(503, "服务不可用:核心模块未加载")
result = cd.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")
to = _get_cached("toolpath_optimizer")
if not to:
raise HTTPException(503, "服务不可用:核心模块未加载")
result = to.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),
):
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)
ed = _get_cached("edm_designer")
if not ed:
raise HTTPException(503, "服务不可用:核心模块未加载")
result = ed.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)
ms = _get_cached("machining_simulator")
if not ms:
raise HTTPException(503, "服务不可用:核心模块未加载")
result = ms.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),
db_session: AsyncSession = Depends(get_db_session),
):
body = await request.json()
task_id = body.get("task_id")
scheme_id = body.get("scheme_id")
formats = body.get("formats", ["step", "stl"])
components = body.get("components", ["cavity", "core"])
if not task_id:
raise HTTPException(404, "缺少 task_id")
await _ensure_task_access(db_session, task_id, current_user.id)
task_data = await TaskQueryService.get_task_view(db_session, task_id)
if not task_data:
raise HTTPException(404, "任务不存在")
resolved_scheme_id = scheme_id or task_data.get("best_scheme_id") or "default"
persisted_files = _select_persisted_files(
task_id=task_id,
task_data=task_data,
scheme_id=resolved_scheme_id,
formats=formats,
components=components,
)
if persisted_files:
return {
"status": "success",
"data": {
"base_filename": Path(task_data.get("filename", f"mold_{task_id}")).stem,
"task_id": task_id,
"scheme_id": resolved_scheme_id,
"files": persisted_files,
"errors": [],
"total_files": len(persisted_files),
"total_errors": 0,
"source": "persisted",
},
}
cavity_shapes = processing_service.get_export_shapes(
task_id,
resolved_scheme_id,
)
filename = task_data.get("filename", f"mold_{task_id}")
if not cavity_shapes:
raise HTTPException(
409,
"导出缓存已失效或任务尚未完成,请重新分析后再导出以保证方案一致性",
)
base_filename = Path(filename).stem
result = cad_exporter.export_mold_results(
cavity_data=cavity_shapes,
base_filename=base_filename,
formats=formats,
components=components,
task_id=task_id,
scheme_id=resolved_scheme_id,
)
result["files"] = _augment_export_files(task_id, result.get("files", []))
result["source"] = "generated"
merged_artifacts = _merge_export_artifacts(_get_export_artifacts(task_data), result)
await storage_service.update_task_parameters(
db_session,
task_id,
{"export_artifacts": merged_artifacts},
)
await redis_task_manager.update_task(task_id, {"export_artifacts": merged_artifacts})
return {"status": "success", "data": result}
@router.get("/export-download/{filepath:path}")
async def download_export_file(
filepath: str,
task_id: str,
current_user: User = Depends(get_current_active_user),
db_session: AsyncSession = Depends(get_db_session),
):
from fastapi.responses import FileResponse
if not task_id:
raise HTTPException(400, "缺少 task_id")
await _ensure_task_access(db_session, task_id, current_user.id)
task_data = await TaskQueryService.get_task_view(db_session, task_id)
if not task_data:
raise HTTPException(404, "任务不存在")
allowed_paths = set()
artifacts = _get_export_artifacts(task_data)
for scheme in (artifacts.get("schemes") or {}).values():
for file in scheme.get("files", []):
relative_path = str(file.get("relative_path") or "").replace("\\", "/").strip("/")
if relative_path:
allowed_paths.add(relative_path)
normalized_path = str(filepath or "").replace("\\", "/").strip("/")
if normalized_path not in allowed_paths:
raise HTTPException(403, "该文件不在任务允许下载清单中")
full_path = os.path.join(cad_exporter.output_dir, normalized_path.replace("/", os.sep))
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),
):
result = cad_exporter.get_export_recommendations(target)
return {"status": "success", "data": result}
@@ -0,0 +1,21 @@
"""
铝金属价格API路由
提供铝金属价格的当前报价和历史走势数据。
路由前缀: /api/aluminum-price
不需要认证,公开访问。
"""
from fastapi import APIRouter, Query
from moldinsight.services.aluminum_price_service import get_aluminum_current_price, get_aluminum_price_history
router = APIRouter(prefix="/aluminum-price", tags=["铝金属价格"])
@router.get("/current")
async def aluminum_current_price():
return get_aluminum_current_price()
@router.get("/history")
async def aluminum_price_history(days: int = Query(default=30, ge=7, le=365)):
return get_aluminum_price_history(days=days)
+103
View File
@@ -0,0 +1,103 @@
from fastapi import APIRouter, Depends, HTTPException, Request
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from datetime import datetime
from shared.database.database import get_db_session
from shared.models.database import User, ProcessingTask
from shared.services.auth_service import get_current_active_user
from moldinsight.services.cam_bundle_service import cam_bundle_service
from moldinsight.services.task_query_service import TaskQueryService
from shared.utils.logger import get_logger
logger = get_logger(__name__)
router = APIRouter()
DEFAULT_CAM_PREFERENCES = {
"mold_steel": "P20",
"surface_quality": "standard",
"controller": "fanuc",
"include_gcode": False,
}
@router.post("/cam/plan")
async def generate_cam_plan(
request: Request,
db_session: AsyncSession = Depends(get_db_session),
current_user: User = Depends(get_current_active_user),
):
"""基于任务分模结果生成 CAM 准备包(MVP)。"""
_ = current_user
body = await request.json()
task_id = body.get("task_id")
scheme_id = body.get("scheme_id")
if not task_id:
raise HTTPException(status_code=400, detail="缺少 task_id")
task_result = await db_session.execute(
select(ProcessingTask).where(ProcessingTask.task_id == task_id)
)
processing_task = task_result.scalar_one_or_none()
persisted_preferences = {}
if processing_task and isinstance(processing_task.parameters, dict):
persisted_preferences = (
processing_task.parameters.get("cam_preferences", {}) or {}
)
mold_steel = body.get(
"mold_steel",
persisted_preferences.get("mold_steel", DEFAULT_CAM_PREFERENCES["mold_steel"]),
)
surface_quality = body.get(
"surface_quality",
persisted_preferences.get("surface_quality", DEFAULT_CAM_PREFERENCES["surface_quality"]),
)
controller = body.get(
"controller",
persisted_preferences.get("controller", DEFAULT_CAM_PREFERENCES["controller"]),
)
include_gcode = bool(
body.get(
"include_gcode",
persisted_preferences.get("include_gcode", DEFAULT_CAM_PREFERENCES["include_gcode"]),
)
)
task_view = await TaskQueryService.get_task_view(db_session, task_id)
if not task_view:
raise HTTPException(status_code=404, detail="任务不存在")
if task_view.get("status") != "completed":
raise HTTPException(status_code=400, detail="任务尚未完成,无法生成CAM计划")
try:
data = cam_bundle_service.build_bundle(
task_view=task_view,
scheme_id=scheme_id,
mold_steel=mold_steel,
surface_quality=surface_quality,
controller=controller,
include_gcode=include_gcode,
)
cam_preferences = {
"mold_steel": mold_steel,
"surface_quality": surface_quality,
"controller": controller,
"include_gcode": include_gcode,
}
if processing_task:
parameters = processing_task.parameters if isinstance(processing_task.parameters, dict) else {}
parameters["cam_preferences"] = cam_preferences
parameters["cam_last_plan"] = {
"scheme_id": data.get("scheme_id"),
"generated_at": datetime.now().isoformat(),
}
processing_task.parameters = parameters
await db_session.commit()
return {"status": "success", "data": data, "cam_preferences": cam_preferences}
except Exception as exc:
logger.error(f"生成CAM准备包失败 task_id={task_id}: {exc}")
raise HTTPException(status_code=500, detail=f"生成CAM准备包失败: {exc}")
+18
View File
@@ -0,0 +1,18 @@
# api/v1/debug_router.py
from fastapi import APIRouter
from shared.services.redis_task_manager import redis_task_manager
router = APIRouter()
@router.get("/debug/tasks")
@router.post("/debug/tasks")
async def debug_tasks():
"""调试接口:查看所有任务"""
all_tasks = await redis_task_manager.get_all_tasks()
return {
"total_tasks": len(all_tasks),
"tasks": all_tasks,
"redis_connected": redis_task_manager.is_connected
}
+18
View File
@@ -0,0 +1,18 @@
# api/v1/health_router.py
from fastapi import APIRouter
from shared.services.redis_task_manager import redis_task_manager
router = APIRouter()
@router.get("/health")
@router.post("/health")
async def health():
task_count = await redis_task_manager.get_task_count()
return {
"status": "healthy",
"pythonocc": True,
"total_tasks": task_count,
"redis_connected": redis_task_manager.is_connected
}
+37
View File
@@ -0,0 +1,37 @@
# api/v1/history_router.py
from fastapi import APIRouter, Depends
import urllib.parse
from moldinsight.services.storage_integration_rustfs import StorageIntegrationService
from shared.database.database import get_db_session
from sqlalchemy.ext.asyncio import AsyncSession
router = APIRouter()
@router.get("/history")
@router.post("/history")
async def get_file_history(db_session: AsyncSession = Depends(get_db_session)):
"""获取按文件名分组的文件历史记录(支持多上传)"""
storage_service = StorageIntegrationService()
file_groups = await storage_service.get_all_file_groups(db_session)
return {
"total_files": len(file_groups),
"files": file_groups
}
@router.get("/history/{filename}")
@router.post("/history/{filename}")
async def get_file_records(filename: str, db_session: AsyncSession = Depends(get_db_session)):
"""获取指定文件名的所有上传记录(支持多上传历史)"""
decoded_filename = urllib.parse.unquote(filename)
storage_service = StorageIntegrationService()
file_records = await storage_service.get_file_history_by_filename(
db_session,
decoded_filename
)
return file_records
+79
View File
@@ -0,0 +1,79 @@
# api/v1/task_router.py
from fastapi import APIRouter, HTTPException, Request, Depends
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from moldinsight.services.task_query_service import TaskQueryService
from shared.database.database import get_db_session
from shared.utils.logger import get_logger
from shared.models.database import ProcessingTask, STPFile
logger = get_logger(__name__)
router = APIRouter()
@router.get("/status/{task_id}")
@router.post("/status/{task_id}")
async def get_status(task_id: str, db_session: AsyncSession = Depends(get_db_session)):
"""
获取任务状态
优先返回内存中的任务信息;
如果内存中不存在,则从 PostgreSQL + RustFS 组装一个持久化的任务视图,
结构与内存任务保持尽量一致,便于前端集中展示总结性信息。
"""
try:
task_view = await TaskQueryService.get_task_view(db_session, task_id)
if task_view is None:
raise HTTPException(404, "任务不存在")
return task_view
except HTTPException:
raise
except Exception as e:
logger.error(f"获取任务状态失败: {e}")
raise HTTPException(500, f"获取任务状态失败: {str(e)}")
@router.get("/result/{task_id}")
@router.post("/result/{task_id}")
async def result_page(request: Request, task_id: str, db_session: AsyncSession = Depends(get_db_session)):
"""结果详情页面"""
# 从数据库查询任务详情
result = await db_session.execute(
select(ProcessingTask, STPFile)
.join(STPFile, ProcessingTask.stp_file_id == STPFile.id)
.where(ProcessingTask.task_id == task_id)
)
task_record = result.first()
if not task_record:
raise HTTPException(404, "任务不存在")
task, stp_file = task_record
# 构建任务详情数据
task_data = {
"task_id": task.task_id,
"filename": stp_file.original_filename if stp_file else "",
"file_size": stp_file.file_size if stp_file else 0,
"status": task.status,
"progress": task.progress,
"current_step": task.current_step,
"created_at": task.created_time.isoformat() if task.created_time else "",
"completed_at": task.completed_time.isoformat() if task.completed_time else "",
"error": task.error_message if task.error_message else ""
}
from fastapi.templating import Jinja2Templates
import os
templates_dir = os.path.join(os.getcwd(), "templates")
templates = Jinja2Templates(directory=templates_dir)
return templates.TemplateResponse("result.html", {
"request": request,
"task": task_data,
"pythonocc_available": True,
"version": "3.0.0"
})
+122
View File
@@ -0,0 +1,122 @@
# api/v1/upload_router.py
from fastapi import APIRouter, UploadFile, File, HTTPException, Depends, Form
import uuid
from datetime import datetime
from pathlib import Path
from shared.models.schemas import ProcessingStatus, create_task_info
from shared.utils.file_handler import FileHandler
from moldinsight.services.storage_integration_rustfs import StorageIntegrationService
from shared.services.redis_task_manager import redis_task_manager
from shared.database.database import get_db_session
from shared.utils.logger import get_logger
from sqlalchemy.ext.asyncio import AsyncSession
from shared.services.auth_service import get_current_active_user
from shared.models.database import User
try:
from celery_tasks import process_stp_task
_use_celery = True
except ImportError:
process_stp_task = None
_use_celery = False
logger = get_logger(__name__)
router = APIRouter()
file_handler = FileHandler()
@router.post("/upload")
async def upload_stp(
file: UploadFile = File(...),
material: str = Form(...),
draft_angle: float = Form(...),
shrinkage_rate: float = Form(...),
parting_precision: float = Form(...),
cavity_match: int = Form(...),
db_session: AsyncSession = Depends(get_db_session),
current_user: User = Depends(get_current_active_user)
):
"""上传STP文件并存储到数据库"""
process_params = {
"material": material,
"draft_angle": float(draft_angle),
"shrinkage_rate": float(shrinkage_rate),
"parting_precision": float(parting_precision),
"cavity_match": int(cavity_match),
}
logger.info(
f"[UPLOAD] 用户={current_user.username}(id={current_user.id}) "
f"文件={file.filename} 参数={process_params} "
f"大小={file.size if hasattr(file, 'size') else 'unknown'}"
)
if not file.filename.lower().endswith(('.stp', '.step')):
logger.warning(f"[UPLOAD] 拒绝: 不支持的文件类型 - {file.filename}")
raise HTTPException(400, "只支持STP/STEP文件")
task_id = str(uuid.uuid4())
try:
file_path, file_size, file_meta = await file_handler.save_uploaded_file(file)
except ValueError as exc:
logger.warning(f"[UPLOAD] 拒绝非法文件: {file.filename}, 原因={exc}")
raise HTTPException(400, str(exc)) from exc
logger.info(f"[UPLOAD] 文件已保存: {file_path} ({file_size} bytes), task_id={task_id}")
storage_service = StorageIntegrationService()
stp_file = await storage_service.save_stp_file(
session=db_session,
file_path=file_path,
original_filename=file_meta["safe_original_name"],
user_id=current_user.id
)
logger.info(f"[UPLOAD] STP文件已存入RustFS+PG: stp_file.id={stp_file.id}")
await storage_service.create_processing_task(
db_session,
task_id,
stp_file.id,
parameters=process_params,
)
task_info = create_task_info(
task_id=task_id,
status=ProcessingStatus.PROCESSING,
filename=file.filename,
file_path=str(file_path),
file_size=file_size,
upload_time=str(datetime.now())
)
task_info["material"] = material
task_info["parameters"] = process_params
task_info["file_hash"] = file_meta["sha256"]
await redis_task_manager.set_task(task_id, task_info)
if _use_celery:
process_stp_task.delay(task_id, str(file_path), stp_file.id, process_params)
logger.info(f"[UPLOAD] Celery 任务已调度: task_id={task_id}")
else:
import asyncio
from moldinsight.services.processing_service import processing_service
asyncio.create_task(processing_service.process_file_with_storage(
task_id, str(file_path), stp_file.id, process_params
))
logger.info(f"[UPLOAD] 直接后台处理: task_id={task_id} (celery 未安装)")
return {
"task_id": task_id,
"status": "processing",
"message": "文件上传成功,开始处理并存储到数据库",
"file_info": {
"filename": file.filename,
"size": file_size,
"pythonocc_available": True,
"database_file_id": stp_file.id,
"sha256": file_meta["sha256"],
},
"parameters": process_params,
}
+1
View File
@@ -0,0 +1 @@
# Core 模块
+209
View File
@@ -0,0 +1,209 @@
"""
AI 分模辅助模型接口示例
此文件展示了如何创建 AI 模型来辅助分模过程。
实际使用时需要替换为真实的 AI 模型。
"""
from typing import Dict, Any, Optional
import numpy as np
from OCC.Core.TopoDS import TopoDS_Shape, TopoDS_Face
class AIPartingSurfaceDetector:
"""
AI 分型面检测器(示例接口)
功能:
- 分析产品 3D 几何
- 预测最优分型面位置和方向
- 识别倒扣区域
"""
def __init__(self, model_path: Optional[str] = None):
"""
初始化 AI 分型面检测器
Args:
model_path: 训练好的模型路径
"""
self.model_path = model_path
self.model = None
# 如果提供了模型路径,加载模型
if model_path:
self._load_model(model_path)
def _load_model(self, model_path: str):
"""加载训练好的 AI 模型"""
# TODO: 实现模型加载逻辑
# 示例:
# import torch
# self.model = torch.load(model_path)
print(f"AI 模型加载:{model_path}")
def detect(self, product_shape: TopoDS_Shape, analysis: Dict) -> Optional[Dict]:
"""
检测最优分型面
Args:
product_shape: OpenCASCADE 形状对象
analysis: 几何分析结果(包含 bounding_box, volume 等)
Returns:
{
"origin": [x, y, z], # 分型面原点
"normal": [nx, ny, nz], # 分型面法向量
"confidence": 0.95, # 置信度
"parting_line": [...] # 可选的分型线
}
"""
# TODO: 使用 AI 模型进行预测
# 这里是示例返回
# 1. 将产品形状转换为 AI 模型输入
# - 体素化 (voxelization)
# - 点云 (point cloud)
# - 多视图 (multi-view images)
input_data = self._preprocess_shape(product_shape, analysis)
# 2. 使用模型预测
# prediction = self.model.predict(input_data)
# 3. 返回预测结果
return {
"origin": [0, 0, analysis["bounding_box"]["center"][2]],
"normal": [0, 0, 1], # Z 方向
"confidence": 0.85,
"undercut_regions": [] # 倒扣区域
}
def _preprocess_shape(self, shape: TopoDS_Shape, analysis: Dict) -> TopoDS_Shape:
"""
预处理产品形状为 AI 模型输入
可能的预处理方式:
1. 体素化:将 3D 模型转换为 3D 网格
2. 点云:采样表面点
3. 多视图:渲染多个角度的 2D 图像
"""
# TODO: 实现预处理逻辑
return None
class AIDraftAnalyzer:
"""
AI 拔模分析器(示例接口)
功能:
- 分析哪些面需要拔模
- 预测最优拔模角度
- 检测脱模干涉
"""
def __init__(self, model_path: Optional[str] = None):
self.model_path = model_path
self.model = None
if model_path:
self._load_model(model_path)
def _load_model(self, model_path: str):
"""加载训练好的 AI 模型"""
print(f"AI 拔模分析模型加载:{model_path}")
def analyze(self, product_shape: TopoDS_Shape, parting_surface: TopoDS_Face,
base_draft_angle: float) -> Optional[Dict]:
"""
分析拔模需求
Args:
product_shape: 产品形状
parting_surface: 分型面
base_draft_angle: 基础拔模角(度)
Returns:
{
"drafted_shape": ..., # 应用拔模后的形状
"draft_angles": {...}, # 各面的拔模角
"interference_areas": [...], # 干涉区域
"recommendations": [...] # 优化建议
}
"""
# TODO: 使用 AI 模型分析拔模
# 示例返回
return {
"drafted_shape": product_shape, # 简化:返回原始形状
"draft_angles": {"default": base_draft_angle},
"interference_areas": [],
"recommendations": ["建议增加圆角", "壁厚均匀化"]
}
class AICavityLayoutOptimizer:
"""
AI 型腔布局优化器(示例接口)
功能:
- 优化多型腔排列
- 设计流道系统
- 平衡材料流动
"""
def __init__(self, model_path: Optional[str] = None):
self.model_path = model_path
self.model = None
if model_path:
self._load_model(model_path)
def optimize(self, product_shape: TopoDS_Shape, cavity_count: int,
mold_base_size: Dict) -> Optional[Dict]:
"""
优化型腔布局
Args:
product_shape: 产品形状
cavity_count: 型腔数量
mold_base_size: 模架尺寸
Returns:
{
"cavity_positions": [...], # 各型腔位置
"runner_system": {...}, # 流道系统设计
"balance_score": 0.92, # 流动平衡评分
"material_efficiency": 0.85 # 材料利用率
}
"""
# TODO: 使用 AI 优化型腔布局
return {
"cavity_positions": [[0, 0, 0]], # 示例
"runner_system": {"type": "cold_runner"},
"balance_score": 0.85,
"material_efficiency": 0.80
}
# ==================== 使用示例 ====================
if __name__ == "__main__":
# 示例:如何使用 AI 模型接口
# 1. 创建 AI 模型实例
parting_detector = AIPartingSurfaceDetector(model_path="models/parting_surface.pth")
draft_analyzer = AIDraftAnalyzer(model_path="models/draft_analysis.pth")
# 2. 设置到 MoldCavityGenerator
from moldinsight.core.mold_generator import MoldCavityGenerator
generator = MoldCavityGenerator()
generator.set_ai_model(
parting_detector=parting_detector,
draft_analyzer=draft_analyzer
)
# 3. 使用(AI 模型会自动介入)
# result = generator.generate_mold_cavities(product_shape)
print("AI 模型接口已配置,分模时将自动使用 AI 辅助")
+547
View File
@@ -0,0 +1,547 @@
"""
AI 分型面检测模块 - 基于 GNN 的分型面预测框架
架构设计:
1. ShapeGraphBuilder - 将 OCC 形状转换为图表示(面为节点,共享边为图边)
2. PartingSurfaceGNN - 图神经网络模型定义
3. AIPartingSurfaceDetectorV2 - 增强版分型面检测器(集成 GNN)
图构建策略:
- 节点:每个 TopoDS_Face 作为一个节点
- 节点特征:法向量(3) + 面积(1) + 曲率(2) + 面类型(1) = 7维
- 边:共享 TopoDS_Edge 的面之间建立边
- 边特征:共享边长度(1) + 二面角(1) = 2维
GNN 模型:
- 3层 GraphConv + 全局池化 + MLP 分类头
- 输出:每个面的分型面归属概率 + 分型方向
依赖:
- PyTorch + PyTorch Geometric(可选,缺失时回退到几何方法)
"""
from typing import Dict, List, Any, Optional, Tuple
import numpy as np
from OCC.Core.TopoDS import TopoDS_Shape
from shared.utils.logger import get_logger
logger = get_logger(__name__)
_TORCH_AVAILABLE = False
_TORCH_GEOMETRIC_AVAILABLE = False
try:
import torch
import torch.nn as nn
import torch.nn.functional as F
_TORCH_AVAILABLE = True
try:
from torch_geometric.nn import GCNConv, global_mean_pool
from torch_geometric.data import Data
_TORCH_GEOMETRIC_AVAILABLE = True
except ImportError:
logger.info("PyTorch Geometric 未安装,GNN 模型不可用")
except ImportError:
logger.info("PyTorch 未安装,AI 分型面检测将使用几何回退方法")
class ShapeGraphBuilder:
"""将 OCC 形状转换为图表示"""
def build_graph(self, shape: TopoDS_Shape) -> Optional[Dict]:
"""
从 OCC 形状构建图数据
Returns:
{
"node_features": np.ndarray (N, 7),
"edge_index": np.ndarray (2, E),
"edge_features": np.ndarray (E, 2),
"face_map": List[TopoDS_Face],
"num_nodes": int,
"num_edges": int
}
"""
try:
from OCC.Core.TopExp import TopExp_Explorer
from OCC.Core.TopAbs import TopAbs_FACE, TopAbs_EDGE
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface
from OCC.Core.GProp import GProp_GProps
from OCC.Core.BRepGProp import brepgprop
from OCC.Core.Bnd import Bnd_Box
from OCC.Core.BRepBndLib import brepbndlib
from OCC.Core.TopTools import TopTools_IndexedDataMapOfShapeListOfShape
from OCC.Core.TopExp import topexp_MapShapesAndAncestors
from OCC.Core.TopoDS import TopoDS_Face, TopoDS_Edge, topods
faces = []
face_features = []
explorer = TopExp_Explorer(shape, TopAbs_FACE)
while explorer.More():
face = topods.Face(explorer.Current())
features = self._extract_face_features(face)
if features is not None:
faces.append(face)
face_features.append(features)
explorer.Next()
if not faces:
logger.warning("未找到面,无法构建图")
return None
node_features = np.array(face_features, dtype=np.float32)
edge_map = TopTools_IndexedDataMapOfShapeListOfShape()
topexp_MapShapesAndAncestors(shape, TopAbs_EDGE, TopAbs_FACE, edge_map)
edge_list = []
edge_features_list = []
for i in range(1, edge_map.Extent() + 1):
edge = topods.Edge(edge_map.FindKey(i))
face_list = edge_map.FindFromIndex(i)
connected_faces = []
it = face_list.begin()
while it != face_list.end():
f = topods.Face(it.Value())
try:
idx = faces.index(f)
connected_faces.append(idx)
except ValueError:
pass
it.next_ptr()
if len(connected_faces) >= 2:
edge_feat = self._extract_edge_features(edge, connected_faces, faces)
for j in range(len(connected_faces)):
for k in range(j + 1, len(connected_faces)):
edge_list.append([connected_faces[j], connected_faces[k]])
edge_features_list.append(edge_feat)
if not edge_list:
logger.warning("未找到边连接,返回无图边的图")
edge_index = np.zeros((2, 0), dtype=np.int64)
edge_features_arr = np.zeros((0, 2), dtype=np.float32)
else:
edge_index = np.array(edge_list, dtype=np.int64).T
rev_edges = np.array([[e[1], e[0]] for e in edge_list], dtype=np.int64).T
edge_index = np.concatenate([edge_index, rev_edges], axis=1)
edge_features_arr = np.array(edge_features_list, dtype=np.float32)
edge_features_arr = np.concatenate([edge_features_arr, edge_features_arr], axis=0)
return {
"node_features": node_features,
"edge_index": edge_index,
"edge_features": edge_features_arr,
"face_map": faces,
"num_nodes": len(faces),
"num_edges": edge_index.shape[1]
}
except Exception as e:
logger.error(f"图构建失败: {e}")
return None
def _extract_face_features(self, face: Any) -> Optional[np.ndarray]:
"""
提取面特征:[nx, ny, nz, area, u_curvature, v_curvature, face_type]
"""
try:
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface
from OCC.Core.GProp import GProp_GProps
from OCC.Core.BRepGProp import brepgprop
surface = BRepAdaptor_Surface(face)
u = (surface.FirstUParameter() + surface.LastUParameter()) / 2
v = (surface.FirstVParameter() + surface.LastVParameter()) / 2
if surface.GetType() == 0:
normal = surface.Plane().Position().Direction()
face_type = 0.0
u_curv = 0.0
v_curv = 0.0
elif surface.GetType() == 1:
normal = surface.Cylinder().Position().Direction()
face_type = 1.0
radius = surface.Cylinder().Radius()
u_curv = 1.0 / radius if radius > 0.001 else 0.0
v_curv = 0.0
elif surface.GetType() == 2:
normal = surface.Cone().Position().Direction()
face_type = 2.0
u_curv = 0.0
v_curv = 0.0
elif surface.GetType() == 3:
normal = surface.Sphere().Position().Direction()
face_type = 3.0
radius = surface.Sphere().Radius()
u_curv = 1.0 / radius if radius > 0.001 else 0.0
v_curv = 1.0 / radius if radius > 0.001 else 0.0
elif surface.GetType() == 4:
normal = surface.Torus().Position().Direction()
face_type = 4.0
u_curv = 0.0
v_curv = 0.0
else:
from OCC.Core.BRepLProp import BRepLProp_SLProps
props = BRepLProp_SLProps(surface, 2, 0.001)
props.SetParameters(u, v)
if props.IsNormalDefined():
normal = props.Normal()
else:
normal = gp_Dir(0, 0, 1)
face_type = 5.0
u_curv = 0.0
v_curv = 0.0
face_props = GProp_GProps()
brepgprop.SurfaceProperties(face, face_props)
area = face_props.Mass()
return np.array([
normal.X(), normal.Y(), normal.Z(),
area,
u_curv, v_curv,
face_type
], dtype=np.float32)
except Exception as e:
logger.debug(f"面特征提取失败: {e}")
return None
def _extract_edge_features(self, edge: Any, connected_faces: List[int],
faces: List) -> np.ndarray:
"""
提取边特征:[edge_length, dihedral_angle]
"""
try:
from OCC.Core.BRepAdaptor import BRepAdaptor_Curve
from OCC.Core.GProp import GProp_GProps
from OCC.Core.BRepGProp import brepgprop
curve = BRepAdaptor_Curve(edge)
first = curve.FirstParameter()
last = curve.LastParameter()
edge_len = abs(last - first)
dihedral = 0.0
if len(connected_faces) >= 2:
n1 = self._get_face_normal_fast(faces[connected_faces[0]])
n2 = self._get_face_normal_fast(faces[connected_faces[1]])
if n1 is not None and n2 is not None:
dot = np.clip(np.dot(n1, n2), -1.0, 1.0)
dihedral = np.arccos(dot)
return np.array([edge_len, dihedral], dtype=np.float32)
except Exception:
return np.array([0.0, 0.0], dtype=np.float32)
def _get_face_normal_fast(self, face: Any) -> Optional[np.ndarray]:
"""快速获取面法向量(numpy数组)"""
try:
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface
surface = BRepAdaptor_Surface(face)
if surface.GetType() == 0:
n = surface.Plane().Position().Direction()
return np.array([n.X(), n.Y(), n.Z()])
return None
except Exception:
return None
if _TORCH_GEOMETRIC_AVAILABLE:
class PartingSurfaceGNN(nn.Module):
"""
分型面检测 GNN 模型
架构:
- 3层 GCNConv (hidden_dim=64)
- 全局平均池化
- 3层 MLP 分类头
- 输出:每个面的分型面归属概率 (0-1)
"""
def __init__(self, input_dim: int = 7, hidden_dim: int = 64,
num_layers: int = 3, dropout: float = 0.3):
super().__init__()
self.input_dim = input_dim
self.hidden_dim = hidden_dim
self.num_layers = num_layers
self.input_proj = nn.Linear(input_dim, hidden_dim)
self.convs = nn.ModuleList()
self.bns = nn.ModuleList()
for _ in range(num_layers):
self.convs.append(GCNConv(hidden_dim, hidden_dim))
self.bns.append(nn.BatchNorm1d(hidden_dim))
self.dropout = dropout
self.mlp = nn.Sequential(
nn.Linear(hidden_dim, hidden_dim),
nn.ReLU(),
nn.Dropout(dropout),
nn.Linear(hidden_dim, hidden_dim // 2),
nn.ReLU(),
nn.Dropout(dropout),
nn.Linear(hidden_dim // 2, 1),
)
def forward(self, data: Data) -> torch.Tensor:
x, edge_index = data.x, data.edge_index
x = self.input_proj(x)
x = F.relu(x)
for conv, bn in zip(self.convs, self.bns):
x = conv(x, edge_index)
x = bn(x)
x = F.relu(x)
x = F.dropout(x, p=self.dropout, training=self.training)
out = self.mlp(x)
return torch.sigmoid(out).squeeze(-1)
class PartingDirectionHead(nn.Module):
"""
分型方向预测头
基于全局池化的面特征,预测分型方向向量
"""
def __init__(self, hidden_dim: int = 64):
super().__init__()
self.direction_mlp = nn.Sequential(
nn.Linear(hidden_dim, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, 3),
)
def forward(self, node_embeddings: torch.Tensor,
batch: torch.Tensor) -> torch.Tensor:
pooled = global_mean_pool(node_embeddings, batch)
direction = self.direction_mlp(pooled)
direction = F.normalize(direction, p=2, dim=-1)
return direction
class AIPartingSurfaceDetectorV2:
"""
增强版 AI 分型面检测器
支持:
1. GNN 模型推理(需要 PyTorch + PyG)
2. 几何方法回退(无需任何 AI 依赖)
3. 模型训练数据收集
"""
def __init__(self, model_path: Optional[str] = None,
use_gnn: bool = True,
device: str = "cpu"):
self.model = None
self.direction_head = None
self.graph_builder = ShapeGraphBuilder()
self.device = device
self.use_gnn = use_gnn and _TORCH_GEOMETRIC_AVAILABLE
if model_path and self.use_gnn:
self._load_model(model_path)
def _load_model(self, model_path: str):
"""加载训练好的 GNN 模型"""
if not _TORCH_GEOMETRIC_AVAILABLE:
logger.warning("PyTorch Geometric 不可用,无法加载 GNN 模型")
return
try:
checkpoint = torch.load(model_path, map_location=self.device)
self.model = PartingSurfaceGNN(
input_dim=checkpoint.get("input_dim", 7),
hidden_dim=checkpoint.get("hidden_dim", 64),
)
self.model.load_state_dict(checkpoint["model_state_dict"])
self.model.to(self.device)
self.model.eval()
if "direction_head_state_dict" in checkpoint:
self.direction_head = PartingDirectionHead(
hidden_dim=checkpoint.get("hidden_dim", 64)
)
self.direction_head.load_state_dict(checkpoint["direction_head_state_dict"])
self.direction_head.to(self.device)
self.direction_head.eval()
logger.info(f"GNN 模型加载成功: {model_path}")
except Exception as e:
logger.error(f"GNN 模型加载失败: {e}")
self.model = None
def detect(self, product_shape: TopoDS_Shape, analysis: Dict) -> Optional[Dict]:
"""
检测最优分型面
Args:
product_shape: OpenCASCADE 形状对象
analysis: 几何分析结果
Returns:
{
"origin": [x, y, z],
"normal": [nx, ny, nz],
"confidence": float,
"parting_line": [...],
"method": "gnn" | "geometric"
}
"""
if self.use_gnn and self.model is not None:
result = self._detect_with_gnn(product_shape, analysis)
if result is not None:
return result
return self._detect_with_geometry(product_shape, analysis)
def _detect_with_gnn(self, shape: TopoDS_Shape, analysis: Dict) -> Optional[Dict]:
"""使用 GNN 模型检测分型面"""
if not _TORCH_GEOMETRIC_AVAILABLE:
return None
try:
graph_data = self.graph_builder.build_graph(shape)
if graph_data is None:
return None
node_features = torch.tensor(
graph_data["node_features"], dtype=torch.float32
).to(self.device)
edge_index = torch.tensor(
graph_data["edge_index"], dtype=torch.long
).to(self.device)
data = Data(x=node_features, edge_index=edge_index)
with torch.no_grad():
face_probs = self.model(data)
if self.direction_head is not None:
batch = torch.zeros(
data.num_nodes, dtype=torch.long, device=self.device
)
direction = self.direction_head(data.x, batch)
normal = direction.cpu().numpy().tolist()
else:
normal = [0, 0, 1]
parting_face_mask = face_probs.cpu().numpy() > 0.5
confidence = float(face_probs.mean().cpu().numpy())
bbox = analysis.get("bounding_box", {})
center = bbox.get("center", [0, 0, 0])
return {
"origin": center,
"normal": normal,
"confidence": confidence,
"method": "gnn",
"face_probabilities": face_probs.cpu().numpy().tolist(),
"parting_face_count": int(parting_face_mask.sum()),
}
except Exception as e:
logger.warning(f"GNN 检测失败,回退到几何方法: {e}")
return None
def _detect_with_geometry(self, shape: TopoDS_Shape, analysis: Dict) -> Dict:
"""几何方法回退:基于法向量统计的分型面检测"""
try:
graph_data = self.graph_builder.build_graph(shape)
if graph_data is not None:
node_features = graph_data["node_features"]
normals = node_features[:, :3]
areas = node_features[:, 3]
total_area = areas.sum()
if total_area > 0:
weights = areas / total_area
weighted_normal = np.sum(normals * weights[:, np.newaxis], axis=0)
else:
weighted_normal = np.mean(normals, axis=0)
length = np.linalg.norm(weighted_normal)
if length > 0.001:
weighted_normal /= length
else:
weighted_normal = np.array([0, 0, 1])
dot_products = np.abs(np.dot(normals, weighted_normal))
confidence = float(np.mean(dot_products))
bbox = analysis.get("bounding_box", {})
center = bbox.get("center", [0, 0, 0])
return {
"origin": center,
"normal": weighted_normal.tolist(),
"confidence": confidence,
"method": "geometric",
}
except Exception as e:
logger.warning(f"几何方法检测失败: {e}")
bbox = analysis.get("bounding_box", {})
center = bbox.get("center", [0, 0, 0])
return {
"origin": center,
"normal": [0, 0, 1],
"confidence": 0.5,
"method": "fallback",
}
def collect_training_sample(self, shape: TopoDS_Shape, analysis: Dict,
ground_truth_normal: List[float],
ground_truth_origin: List[float]) -> Optional[Dict]:
"""
收集训练样本
Args:
shape: OCC 形状
analysis: 几何分析
ground_truth_normal: 人工标注的分型方向
ground_truth_origin: 人工标注的分型面原点
Returns:
可序列化的训练样本
"""
graph_data = self.graph_builder.build_graph(shape)
if graph_data is None:
return None
return {
"node_features": graph_data["node_features"].tolist(),
"edge_index": graph_data["edge_index"].tolist(),
"edge_features": graph_data["edge_features"].tolist(),
"label_normal": ground_truth_normal,
"label_origin": ground_truth_origin,
"bounding_box": analysis.get("bounding_box", {}),
}
@staticmethod
def create_model(input_dim: int = 7, hidden_dim: int = 64,
num_layers: int = 3) -> Optional[Any]:
"""创建新的 GNN 模型实例"""
if not _TORCH_GEOMETRIC_AVAILABLE:
logger.warning("PyTorch Geometric 不可用,无法创建模型")
return None
return PartingSurfaceGNN(
input_dim=input_dim,
hidden_dim=hidden_dim,
num_layers=num_layers,
)
+625
View File
@@ -0,0 +1,625 @@
"""
增强版铝制家电包装泡沫模具分模算法
本模块实现了针对铝泡沫模具的优化分模算法,包括:
1. 改进的法向量分析 - 高斯权重、多点采样
2. 多分型面检测 - 支持复杂产品
3. 倒扣区域检测 - 自动识别
4. 铝泡沫收缩补偿 - 基于发泡倍率
5. 优化的型腔分离 - 精确布尔运算
6. 模具块生成 - A/B板结构
7. 分型线平滑处理 - B样条拟合
"""
from typing import Dict, List, Any, Tuple, Optional
import numpy as np
from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeFace
from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox
from OCC.Core.gp import gp_Pln, gp_Dir, gp_Pnt
from OCC.Core.TopoDS import TopoDS_Face, TopoDS_Shape, topods
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface
from OCC.Core.TopExp import TopExp_Explorer
from OCC.Core.TopAbs import TopAbs_FACE
from OCC.Core.Bnd import Bnd_Box
from OCC.Core.BRepBndLib import brepbndlib
from OCC.Core.GProp import GProp_GProps
from OCC.Core.BRepGProp import brepgprop
from shared.models.schemas import create_mold_cavity_data, create_mold_key_info
from shared.utils.logger import get_logger
from moldinsight.core.base_mold_generator import BaseMoldGenerator
from moldinsight.core.side_action_designer import SideActionDesigner
logger = get_logger(__name__)
class AluminumFoamMoldGenerator(BaseMoldGenerator):
"""铝制家电包装泡沫模具分模生成器"""
def __init__(self,
shrinkage_rate: float = 0.015,
draft_angle: float = 3.0,
material_density: float = 0.5,
foam_material: str = "AlSi10Mg"):
"""
初始化铝泡沫模具生成器
Args:
shrinkage_rate: 收缩率(铝泡沫默认 1.5%)
draft_angle: 拔模角(铝泡沫建议 3-5°)
material_density: 材料密度 g/cm³(铝泡沫 0.3-0.8)
foam_material: 泡沫材料类型
"""
super().__init__(shrinkage_rate, draft_angle, material_density)
self.foam_material = foam_material
self.foam_materials = {
"AlSi10Mg": {
"density": 0.45,
"expansion_ratio": 2.5,
"shrinkage_rate": 0.015,
"molding_temp": 380,
"description": "常用铝硅泡沫"
},
"AlSi12": {
"density": 0.50,
"expansion_ratio": 2.2,
"shrinkage_rate": 0.012,
"molding_temp": 360,
"description": "高强度铝泡沫"
},
"Pure Al Foam": {
"density": 0.35,
"expansion_ratio": 3.0,
"shrinkage_rate": 0.020,
"molding_temp": 400,
"description": "纯铝泡沫"
},
"AlSi7Mg": {
"density": 0.40,
"expansion_ratio": 2.8,
"shrinkage_rate": 0.018,
"molding_temp": 390,
"description": "轻质铝镁泡沫"
}
}
self.plastic_materials = {
"ABS": {"density": 1.05, "shrinkage": 0.005},
"PP": {"density": 0.90, "shrinkage": 0.016},
"PC": {"density": 1.20, "shrinkage": 0.005},
"PE": {"density": 0.95, "shrinkage": 0.025},
"PS": {"density": 1.05, "shrinkage": 0.004},
"PA": {"density": 1.14, "shrinkage": 0.015},
"POM": {"density": 1.42, "shrinkage": 0.020},
"PMMA": {"density": 1.18, "shrinkage": 0.004}
}
self.parting_line_tolerance = 0.1
self.max_draft_angle = 5.0
self.min_draft_angle = 1.0
self.cavity_count = 1
self.parting_precision = 0.1
self.cavity_match_rate = 95.0
self.side_action_designer = SideActionDesigner()
def set_foam_material(self, material: str):
"""设置铝泡沫材料"""
if material in self.foam_materials:
props = self.foam_materials[material]
self.foam_material = material
self.material_density = props["density"]
self.shrinkage_rate = props["shrinkage_rate"]
logger.info(f"铝泡沫材料设置为 {material}, 密度: {props['density']} g/cm³")
else:
logger.warning(f"未知材料 {material}, 使用当前设置")
def set_material(self, material: str):
"""设置材料(自动识别类型)"""
if material in self.foam_materials:
self.set_foam_material(material)
elif material in self.plastic_materials:
props = self.plastic_materials[material]
self.material_density = props["density"]
self.shrinkage_rate = props["shrinkage"]
logger.info(f"塑料材料设置为 {material}, 密度: {props['density']} g/cm³")
else:
logger.warning(f"未知材料 {material}")
def generate_mold_cavities(self, product_shape: TopoDS_Shape) -> Dict[str, Any]:
"""
从产品的3D模型生成型腔和型芯
完整流程:
1. 分析产品几何
2. 检测分型面(支持多分型面)
3. 检测倒扣区域
4. 应用收缩率补偿
5. 应用拔模角
6. 分离型腔和型芯
7. 生成模具块
"""
logger.info(f"开始生成铝泡沫模具型腔 (材料: {self.foam_material})...")
try:
analysis = self._analyze_product_geometry(product_shape)
parting_result = self._detect_parting_surfaces(product_shape, analysis)
primary_parting_surface = parting_result["primary_surface"]
primary_parting_line = parting_result["primary_line"]
primary_parting_direction = parting_result["primary_direction"]
side_action_result = self.side_action_designer.analyze_and_design(
shape=product_shape,
parting_direction=primary_parting_direction,
mold_size=self._calculate_mold_size(analysis),
parting_surface=primary_parting_surface,
)
undercut_regions = self._build_undercut_regions(
side_action_result.get("undercut_analysis", {})
)
scaled_shape = self._apply_shrinkage_compensation(product_shape)
drafted_shape = self._apply_draft_angles(scaled_shape, primary_parting_surface)
cavity, core = self._split_cavity_core(drafted_shape, primary_parting_surface)
mold_block = self._generate_mold_block(cavity, analysis)
smoothed_parting_line = self._smooth_parting_line(primary_parting_line)
logger.info("铝泡沫模具型腔生成完成")
return {
"cavity": cavity,
"core": core,
"parting_surface": primary_parting_surface,
"parting_line": smoothed_parting_line,
"mold_block": mold_block,
"analysis": analysis,
"undercut_regions": undercut_regions,
"side_actions": side_action_result,
"parting_surfaces": parting_result,
"material": self.foam_material,
"shrinkage_applied": self.shrinkage_rate,
"draft_angle_applied": self.draft_angle
}
except Exception as e:
logger.error(f"模具型腔生成失败: {e}")
raise
def generate_detailed_cavity_json(self, cavity_data: Dict) -> Dict[str, Any]:
"""生成详细的型腔三维JSON数据"""
cavity = cavity_data["cavity"]
core = cavity_data["core"]
parting_surface = cavity_data["parting_surface"]
analysis = cavity_data["analysis"]
cavity_geometry = self._extract_shape_geometry(cavity, "cavity")
core_geometry = self._extract_shape_geometry(core, "core")
parting_geometry = self._extract_parting_surface_geometry(parting_surface)
material_info = self.foam_materials.get(self.foam_material, {})
detailed_json = {
"metadata": {
"version": "3.0",
"generated_at": str(np.datetime64('now')),
"mold_type": "aluminum_foam",
"shrinkage_rate": self.shrinkage_rate,
"draft_angle": self.draft_angle,
"unit": "mm",
"foam_material": self.foam_material
},
"product_analysis": {
"bounding_box": analysis.get("bounding_box", {}),
"volume": analysis.get("volume", 0),
"surface_area": analysis.get("surface_area", 0),
"center_of_mass": analysis.get("center_of_mass", [0, 0, 0])
},
"mold_cavities": {
"cavity": cavity_geometry,
"core": core_geometry
},
"parting_surface": parting_geometry,
"manufacturing_info": {
"estimated_mold_size": self._calculate_mold_size(analysis),
"estimated_clamping_force": self._calculate_clamping_force(analysis),
"clamping_force_formula": "投影面积(cm²) × 0.3 (泡沫材料系数)",
"recommended_material": material_info.get("description", "Aluminum Foam Mold"),
"molding_temperature": material_info.get("molding_temp", 380),
"expansion_ratio": material_info.get("expansion_ratio", 2.5),
"parting_direction": "Z",
"parting_description": "Z轴上下开模,分型面位于包围盒Z中心",
},
"quality_checks": {
"undercut_regions": cavity_data.get("undercut_regions", []),
"side_actions": cavity_data.get("side_actions", {}),
"parting_line_smoothness": self._assess_parting_line_smoothness(
cavity_data.get("parting_line", [])
)
}
}
return detailed_json
def generate_cavity_key_info(self, cavity_data: Dict) -> Dict[str, Any]:
"""生成模具型腔的关键信息"""
analysis = cavity_data["analysis"]
material_info = self.foam_materials.get(self.foam_material, {})
key_info = {
"mold_parameters": {
"shrinkage_rate": f"{self.shrinkage_rate * 100:.2f}%",
"draft_angle": f"{self.draft_angle}°",
"parting_line_length": self._calculate_parting_line_length(
cavity_data.get("parting_line", [])
),
"cavity_depth": analysis.get("bounding_box", {}).get("dimensions", [0, 0, 0])[2],
"foam_material": self.foam_material,
"molding_temp": f"{material_info.get('molding_temp', 380)} °C"
},
"geometric_characteristics": {
"product_volume": f"{analysis.get('volume', 0) / 1000:.2f} cm³",
"product_weight": self._calculate_product_weight(analysis),
"wall_thickness_range": self._estimate_wall_thickness(analysis),
"complexity_score": self._calculate_complexity_score(analysis)
},
"manufacturing_requirements": {
"cavity_material": "Aluminum Alloy 7075",
"hardness": "HRC 30-35",
"surface_finish": "SPI A2",
"estimated_cycle_time": self._estimate_cycle_time(analysis),
"recommended_injection_pressure": "60-100 MPa",
"mold_base": "FUTABA standard"
},
"quality_considerations": {
"undercut_count": len(cavity_data.get("undercut_regions", [])),
"undercut_regions": cavity_data.get("undercut_regions", []),
"side_action_summary": cavity_data.get("side_actions", {}).get("summary", {}),
"sink_mark_risk": self._identify_sink_mark_risk(analysis),
"warpage_risk": self._assess_warpage_risk(analysis),
"venting_requirement": self._assess_venting_requirement(analysis)
}
}
return key_info
# ==================== 核心算法实现 ====================
def _analyze_product_geometry(self, shape: TopoDS_Shape) -> Dict[str, Any]:
"""分析产品几何属性(扩展基类版本,增加法向量统计)"""
result = super()._analyze_product_geometry(shape)
result["normal_statistics"] = self._analyze_parting_direction(shape)
return result
def _analyze_parting_direction(self, shape: TopoDS_Shape) -> 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 = self._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:
continue
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()
}
def _split_cavity_core(self, shape: TopoDS_Shape, parting_surface: TopoDS_Face) -> Tuple[TopoDS_Shape, TopoDS_Shape]:
"""分离型腔和型芯(铝泡沫使用更大余量)"""
return super()._split_cavity_core(shape, parting_surface, margin=25)
def _detect_parting_surfaces(self, shape: TopoDS_Shape, analysis: Dict) -> Dict[str, Any]:
"""
检测分型面(泡沫模具专用)
规则:
1. 优先选择 Z 轴方向分型(上下开模)
2. 分型面位置选在产品的最大轮廓处,即包围盒的 Z 方向中心
"""
bbox = analysis["bounding_box"]
center = bbox["center"]
primary_direction = [0, 0, 1] # Z 轴方向
# 分型面位于包围盒 Z 方向中心(最大轮廓处)
parting_z = center[2]
parting_plane = gp_Pln(gp_Pnt(center[0], center[1], parting_z), gp_Dir(0, 0, 1))
try:
parting_surface = BRepBuilderAPI_MakeFace(parting_plane).Face()
except Exception:
# 回退到默认平面
parting_plane = gp_Pln(gp_Pnt(0, 0, parting_z), gp_Dir(0, 0, 1))
parting_surface = BRepBuilderAPI_MakeFace(parting_plane).Face()
logger.info(f"泡沫模具 Z 轴分型面: Z={parting_z:.2f} mm (包围盒中心)")
parting_line = self.optimize_parting_line(
self._calculate_parting_line(shape, parting_surface)
)
additional_surfaces = []
dims = bbox["dimensions"]
max_dim = max(dims)
min_dim = min(dims)
if min_dim > 0 and max_dim / min_dim > 5:
vertical_plane = gp_Pln(
gp_Pnt(center[0], center[1], center[2]),
gp_Dir(1, 0, 0),
)
try:
vertical_surface = BRepBuilderAPI_MakeFace(vertical_plane).Face()
additional_surfaces.append({
"surface": vertical_surface,
"direction": [1, 0, 0],
"reason": "产品扁平,需要辅助垂直分型参考",
})
except Exception:
pass
return {
"primary_surface": parting_surface,
"primary_line": parting_line,
"primary_direction": primary_direction,
"confidence": 0.95, # Z 轴分型置信度高
"method": "z_axis_rule",
"additional_surfaces": additional_surfaces,
"surface_count": 1 + len(additional_surfaces),
"parting_direction": "Z",
"parting_position_z": parting_z,
}
def _build_undercut_regions(self, undercut_analysis: Dict[str, Any]) -> List[Dict[str, Any]]:
"""将侧向机构分析结果转换为兼容旧结构的倒扣区域列表。"""
undercut_faces = undercut_analysis.get("undercut_faces", [])
regions = []
for face in undercut_faces:
regions.append({
"type": "negative_draft",
"location": face.get("center", [0, 0, 0]),
"severity": face.get("severity", "medium"),
"area": face.get("area", 0),
"is_outer": face.get("is_outer", False),
"face_index": face.get("face_index"),
})
logger.info(f"转换得到 {len(regions)} 个兼容倒扣区域")
return regions
def _smooth_parting_line(self, parting_line: List[List[float]]) -> List[List[float]]:
"""
分型线平滑处理 - 使用B样条拟合
"""
if len(parting_line) < 4:
return parting_line
try:
points = np.array(parting_line)
smoothed = []
window_size = 3
for i in range(len(points)):
start = max(0, i - window_size // 2)
end = min(len(points), i + window_size // 2 + 1)
window = points[start:end]
if len(window) > 0:
avg = np.mean(window, axis=0)
smoothed.append(avg.tolist())
return smoothed
except Exception as e:
logger.warning(f"分型线平滑失败: {e}")
return parting_line
def _assess_parting_line_smoothness(self, parting_line: List[List[float]]) -> float:
"""评估分型线平滑度"""
if len(parting_line) < 3:
return 0.0
try:
points = np.array(parting_line)
angles = []
for i in range(1, len(points) - 1):
v1 = points[i] - points[i-1]
v2 = points[i+1] - points[i]
len1 = np.linalg.norm(v1)
len2 = np.linalg.norm(v2)
if len1 > 0.001 and len2 > 0.001:
cos_angle = np.dot(v1, v2) / (len1 * len2)
cos_angle = max(-1, min(1, cos_angle))
angle = np.arccos(cos_angle)
angles.append(np.degrees(angle))
if angles:
avg_angle_change = np.mean(angles)
smoothness = max(0, 100 - avg_angle_change * 2)
return smoothness
return 50.0
except Exception:
return 50.0
def _generate_mold_block(self, cavity: TopoDS_Shape, analysis: Dict) -> TopoDS_Shape:
"""生成完整的模具块(包含A/B板结构)"""
try:
bbox = analysis["bounding_box"]
dims = bbox["dimensions"]
margin = 30
length = dims[0] + 2 * margin
width = dims[1] + 2 * margin
height = dims[2] + margin + 80
mold_block = BRepPrimAPI_MakeBox(
gp_Pnt(-length/2, -width/2, -80),
gp_Pnt(length/2, width/2, height)
).Shape()
logger.info(f"模具块生成: {length}x{width}x{height} mm")
return mold_block
except Exception as e:
logger.error(f"模具块生成失败: {e}")
return cavity
def _extract_parting_surface_geometry(self, surface: TopoDS_Face) -> Dict[str, Any]:
"""提取分型面几何数据"""
metadata = self._extract_plane_metadata(surface)
return {
"type": "plane",
"normal": metadata["normal"],
"origin": metadata["origin"],
"bounds": metadata["bounds"],
}
def _create_parting_surface_from_ai(self, ai_result: Dict, analysis: Dict,
shape: Optional[TopoDS_Shape] = None) -> Dict:
"""从 AI 结果创建分型面"""
origin = ai_result.get("origin", [0, 0, 0])
normal = ai_result.get("normal", [0, 0, 1])
parting_plane = gp_Pln(
gp_Pnt(origin[0], origin[1], origin[2]),
gp_Dir(normal[0], normal[1], normal[2])
)
try:
parting_surface = BRepBuilderAPI_MakeFace(parting_plane).Face()
except Exception:
parting_plane = gp_Pln(gp_Pnt(0, 0, 0), gp_Dir(0, 0, 1))
parting_surface = BRepBuilderAPI_MakeFace(parting_plane).Face()
if shape is not None:
parting_line = self._calculate_parting_line(shape, parting_surface)
else:
parting_line = []
return {
"primary_surface": parting_surface,
"primary_line": parting_line,
"primary_direction": normal,
"confidence": ai_result.get("confidence", 0.8),
"additional_surfaces": [],
"surface_count": 1
}
# ==================== 辅助方法 ====================
def _calculate_mold_size(self, analysis: Dict) -> Dict[str, float]:
"""估算模具尺寸"""
dims = analysis["bounding_box"]["dimensions"]
margin = 30
return {
"length": dims[0] + 2 * margin,
"width": dims[1] + 2 * margin,
"height": dims[2] + margin + 80,
"margin": margin
}
def _calculate_clamping_force(self, analysis: Dict) -> str:
"""
估算锁模力(泡沫模具专用)
公式: 锁模力(吨) = 投影面积(cm²) × 0.3 (泡沫材料系数)
投影面积 = 长度 × 宽度 (Z轴开模)
"""
bbox = analysis.get("bounding_box", {})
dims = bbox.get("dimensions", [0, 0, 0])
# 投影面积 = 长度 × 宽度 (mm² → cm²)
projected_area_cm2 = (dims[0] * dims[1]) / 100 if len(dims) >= 2 else 0
# 锁模力(吨) = 投影面积(cm²) × 0.3
clamping_force_ton = int(projected_area_cm2 * 0.3)
clamping_force_ton = max(30, clamping_force_ton)
return f"{clamping_force_ton} 吨 (投影面积 {projected_area_cm2:.1f} cm² × 0.3)"
def _calculate_product_weight(self, analysis: Dict) -> str:
"""计算产品重量"""
volume_cm3 = analysis.get("volume", 0) / 1000
weight_g = volume_cm3 * self.material_density
return f"{weight_g:.2f} g"
def _estimate_wall_thickness(self, analysis: Dict) -> str:
"""估算壁厚范围"""
volume = analysis.get("volume", 0)
surface_area = analysis.get("surface_area", 0)
if surface_area > 0 and volume > 0:
avg_thickness = (volume / surface_area) * 0.6
return f"{avg_thickness * 0.7:.2f} - {avg_thickness * 1.3:.2f} mm"
return "10.0 - 30.0 mm (铝泡沫典型)"
def _calculate_complexity_score(self, analysis: Dict) -> float:
"""计算复杂度评分"""
volume = analysis.get("volume", 0)
surface_area = analysis.get("surface_area", 0)
if surface_area > 0 and volume > 0:
thickness_ratio = (volume / surface_area) * 0.6
complexity = min(thickness_ratio / 5.0, 10.0)
return round(complexity, 1)
return 5.0
def _estimate_cycle_time(self, analysis: Dict) -> str:
"""估算成型周期"""
volume_cm3 = analysis.get("volume", 0) / 1000
if volume_cm3 < 10:
return "60-90 秒"
elif volume_cm3 < 50:
return "90-120 秒"
elif volume_cm3 < 200:
return "120-180 秒"
else:
return "180-300 秒"
def _identify_sink_mark_risk(self, analysis: Dict) -> str:
"""识别缩痕风险"""
return "中 - 铝泡沫壁厚大,需控制发泡均匀性"
def _assess_venting_requirement(self, analysis: Dict) -> str:
"""评估排气需求"""
volume = analysis.get("volume", 0)
if volume > 50000000:
return "高 - 需要加强排气系统"
elif volume > 10000000:
return "中 - 建议标准排气"
else:
return "低 - 常规排气即可"
+993
View File
@@ -0,0 +1,993 @@
from typing import Dict, List, Any, Tuple, Optional, TYPE_CHECKING
import math
import numpy as np
from OCC.Core.BRepOffsetAPI import BRepOffsetAPI_DraftAngle
from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Cut, BRepAlgoAPI_Section, BRepAlgoAPI_Common, BRepAlgoAPI_Fuse
from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeFace, BRepBuilderAPI_Transform
from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox, BRepPrimAPI_MakeHalfSpace
from OCC.Core.gp import gp_Pln, gp_Dir, gp_Pnt, gp_Trsf, gp_Ax2
from OCC.Core.TopoDS import TopoDS_Shape, TopoDS_Face, TopoDS_Compound, topods
from OCC.Core.BRep import BRep_Tool, BRep_Builder
from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh
from OCC.Core.GProp import GProp_GProps
from OCC.Core.BRepGProp import brepgprop
from OCC.Core.TopExp import TopExp_Explorer
from OCC.Core.TopAbs import TopAbs_FACE, TopAbs_EDGE
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface, BRepAdaptor_Curve
from OCC.Core.Bnd import Bnd_Box
from OCC.Core.BRepBndLib import brepbndlib
from OCC.Core.TopLoc import TopLoc_Location
from shared.models.schemas import create_mold_cavity_data, create_mold_key_info
from shared.utils.logger import get_logger
logger = get_logger(__name__)
class BaseMoldGenerator:
"""模具生成器基类 - 提供共用方法"""
def __init__(self, shrinkage_rate: float = 0.005, draft_angle: float = 2.0,
material_density: float = 1.05):
self.shrinkage_rate = shrinkage_rate
self.draft_angle = draft_angle
self.material_density = material_density
self.ai_parting_detector: Optional[Any] = None
self.ai_draft_analyzer: Optional[Any] = None
def set_ai_model(self, parting_detector: Any = None, draft_analyzer: Any = None):
self.ai_parting_detector = parting_detector
self.ai_draft_analyzer = draft_analyzer
logger.info("AI 模型接口已设置")
def _apply_shrinkage_compensation(self, shape: TopoDS_Shape) -> TopoDS_Shape:
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}%, 缩放因子: {scale_factor:.4f}")
return scaled_shape
except Exception as e:
logger.warning(f"收缩率补偿失败: {e}")
return shape
def _apply_draft_angles(self, shape: TopoDS_Shape, parting_surface: TopoDS_Face) -> TopoDS_Shape:
try:
draft_direction = self._get_draft_direction(parting_surface)
if draft_direction is None:
logger.warning("无法确定拔模方向,跳过拔模处理")
return shape
draft_angle_rad = math.radians(self.draft_angle)
draftable_faces = self._find_draftable_faces(shape, draft_direction)
if not draftable_faces:
logger.info("未找到需要拔模的面,跳过拔模处理")
return shape
logger.info(f"应用拔模角: {self.draft_angle}°, {len(draftable_faces)} 个面")
drafted_shape = self._execute_draft(shape, draftable_faces, draft_direction, draft_angle_rad)
return drafted_shape
except Exception as e:
logger.warning(f"拔模角处理失败,返回原始形状: {e}")
return shape
def _get_draft_direction(self, parting_surface: TopoDS_Face) -> Optional[gp_Dir]:
try:
surface = BRepAdaptor_Surface(parting_surface)
if surface.GetType() == 0:
return surface.Plane().Position().Direction()
return gp_Dir(0, 0, 1)
except Exception:
return gp_Dir(0, 0, 1)
def _find_draftable_faces(self, shape: TopoDS_Shape, draft_direction: gp_Dir) -> List[TopoDS_Face]:
draftable = []
explorer = TopExp_Explorer(shape, TopAbs_FACE)
while explorer.More():
face = topods.Face(explorer.Current())
normal = self._get_face_normal(face)
if normal is not None:
dot = abs(normal.Dot(draft_direction))
angle = math.degrees(math.acos(min(dot, 1.0)))
if 5.0 < angle < 85.0:
draftable.append(face)
explorer.Next()
return draftable
def _get_face_normal(self, face: TopoDS_Face) -> Optional[gp_Dir]:
try:
surface = BRepAdaptor_Surface(face)
u = (surface.FirstUParameter() + surface.LastUParameter()) / 2
v = (surface.FirstVParameter() + surface.LastVParameter()) / 2
if surface.GetType() == 0:
return surface.Plane().Position().Direction()
from OCC.Core.BRepLProp import BRepLProp_SLProps
props = BRepLProp_SLProps(surface, 1, 0.001)
props.SetParameters(u, v)
if props.IsNormalDefined():
return props.Normal()
return None
except Exception:
return None
def _execute_draft(self, shape: TopoDS_Shape, faces: List[TopoDS_Face],
draft_direction: gp_Dir, draft_angle_rad: float) -> TopoDS_Shape:
try:
draft = BRepOffsetAPI_DraftAngle(shape)
for face in faces:
try:
normal = self._get_face_normal(face)
if normal is None:
continue
dot = normal.Dot(draft_direction)
if dot > 0:
face_dir = draft_direction
else:
face_dir = gp_Dir(-draft_direction.X(), -draft_direction.Y(), -draft_direction.Z())
draft.Add(face, face_dir, draft_angle_rad, True)
except Exception:
continue
draft.Build()
if draft.IsDone():
logger.info(f"拔模角应用成功: {len(faces)} 个面, {self.draft_angle}°")
return draft.Shape()
else:
logger.warning("BRepOffsetAPI_DraftAngle 构建失败,尝试逐面拔模")
return self._draft_faces_sequentially(shape, faces, draft_direction, draft_angle_rad)
except Exception as e:
logger.warning(f"拔模执行失败: {e}")
return shape
def _draft_faces_sequentially(self, shape: TopoDS_Shape, faces: List[TopoDS_Face],
draft_direction: gp_Dir, draft_angle_rad: float) -> TopoDS_Shape:
current_shape = shape
success_count = 0
for face in faces:
try:
draft = BRepOffsetAPI_DraftAngle(current_shape)
normal = self._get_face_normal(face)
if normal is None:
continue
dot = normal.Dot(draft_direction)
if dot > 0:
face_dir = draft_direction
else:
face_dir = gp_Dir(-draft_direction.X(), -draft_direction.Y(), -draft_direction.Z())
draft.Add(face, face_dir, draft_angle_rad, True)
draft.Build()
if draft.IsDone():
current_shape = draft.Shape()
success_count += 1
except Exception:
continue
if success_count > 0:
logger.info(f"逐面拔模完成: {success_count}/{len(faces)} 个面成功")
else:
logger.warning("逐面拔模全部失败,返回原始形状")
return current_shape
def _analyze_product_geometry(self, shape: TopoDS_Shape) -> Dict[str, Any]:
try:
props = GProp_GProps()
brepgprop.VolumeProperties(shape, props)
volume = props.Mass()
surface_props = GProp_GProps()
brepgprop.SurfaceProperties(shape, surface_props)
surface_area = surface_props.Mass()
center = props.CentreOfMass()
bbox = Bnd_Box()
brepbndlib.Add(shape, bbox)
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
inertia = props.MatrixOfInertia()
return {
"volume": volume,
"surface_area": surface_area,
"center_of_mass": [float(center.X()), float(center.Y()), float(center.Z())],
"bounding_box": {
"min": [float(xmin), float(ymin), float(zmin)],
"max": [float(xmax), float(ymax), float(zmax)],
"center": [float((xmin+xmax)/2), float((ymin+ymax)/2), float((zmin+zmax)/2)],
"dimensions": [float(xmax-xmin), float(ymax-ymin), float(zmax-zmin)]
},
"inertia_matrix": self._get_inertia_matrix(props)
}
except Exception as e:
logger.error(f"产品几何分析失败: {e}")
raise
def _split_cavity_core(self, shape: TopoDS_Shape, parting_surface: TopoDS_Face, margin: int = 20) -> Tuple[TopoDS_Shape, TopoDS_Shape]:
"""
分离型腔和型芯 — 完全嵌入 + 突出贴合方式。
型腔(凹模)= 完整模具块 - 产品 → 产品形状完全嵌入型腔块中
型芯(凸模)= 底座平板 + 产品融合 → 产品从底座面突出,与型腔凹入完美贴合
不再将模具块沿分型面一分为二。
"""
try:
bbox = Bnd_Box()
brepbndlib.Add(shape, bbox)
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
mold_xmin = xmin - margin
mold_ymin = ymin - margin
mold_zmin = zmin - margin
mold_xmax = xmax + margin
mold_ymax = ymax + margin
mold_zmax = zmax + margin
mold_block = BRepPrimAPI_MakeBox(
gp_Pnt(mold_xmin, mold_ymin, mold_zmin),
gp_Pnt(mold_xmax, mold_ymax, mold_zmax)
).Shape()
parting_plane = self._get_parting_plane(parting_surface, shape)
if parting_plane is None:
center_z = (zmin + zmax) / 2
parting_plane = gp_Pln(gp_Pnt(0, 0, center_z), gp_Dir(0, 0, 1))
cavity = self._subtract_product_from_plate(mold_block, shape, "型腔")
if cavity is None:
cavity = mold_block
core = self._build_core_with_base(
shape, mold_block, parting_plane,
mold_xmin, mold_ymin, mold_zmin,
mold_xmax, mold_ymax, mold_zmax,
xmin, ymin, zmin, xmax, ymax, zmax
)
logger.info("型腔/型芯分离完成(完全嵌入 + 突出贴合)")
return cavity, core
except Exception as e:
logger.error(f"型腔分离失败: {e}")
return self._split_cavity_core_fallback(shape, None)
@staticmethod
def _extract_parting_normal(parting_surface: TopoDS_Face) -> List[float]:
"""从分型面提取法向量"""
try:
surface = BRepAdaptor_Surface(parting_surface)
if surface.GetType() == 0:
plane = surface.Plane()
n = plane.Axis().Direction()
return [float(n.X()), float(n.Y()), float(n.Z())]
except Exception:
pass
return [0.0, 0.0, 1.0]
def _build_core_with_base(
self,
shape: TopoDS_Shape,
mold_block: TopoDS_Shape,
parting_plane: gp_Pln,
mold_xmin: float, mold_ymin: float, mold_zmin: float,
mold_xmax: float, mold_ymax: float, mold_zmax: float,
xmin: float, ymin: float, zmin: float,
xmax: float, ymax: float, zmax: float,
) -> TopoDS_Shape:
"""
构建带底座的型芯。
核心逻辑:底座平板沿分型方向覆盖模具半空间,
与产品形状做布尔融合,形成"底座+产品突出体"。
融合失败时用 TopoDS_Compound 兜底,确保底座永不会丢失。
"""
normal = parting_plane.Axis().Direction()
origin = parting_plane.Location()
nx, ny, nz = float(normal.X()), float(normal.Y()), float(normal.Z())
prod_span = max((xmax - xmin), (ymax - ymin), (zmax - zmin))
overlap = max(prod_span * 0.15, 8.0)
base_p1 = [mold_xmin, mold_ymin, mold_zmin]
base_p2 = [mold_xmax, mold_ymax, mold_zmax]
for i in range(3):
n = [nx, ny, nz][i]
o = [float(origin.X()), float(origin.Y()), float(origin.Z())][i]
if abs(n) < 0.001:
continue
if n > 0:
base_p2[i] = o + overlap
else:
base_p1[i] = o - overlap
base_plate = None
try:
base_plate = BRepPrimAPI_MakeBox(
gp_Pnt(base_p1[0], base_p1[1], base_p1[2]),
gp_Pnt(base_p2[0], base_p2[1], base_p2[2])
).Shape()
logger.info(f"型芯底座构建: 重叠量={overlap:.1f}mm")
except Exception as e:
logger.warning(f"底座构建失败: {e}")
return shape
try:
fuse_op = BRepAlgoAPI_Fuse(base_plate, shape)
if fuse_op.IsDone():
core = fuse_op.Shape()
explorer = TopExp_Explorer(core, TopAbs_FACE)
face_count = 0
while explorer.More():
face_count += 1
explorer.Next()
if face_count > 0:
logger.info(f"型芯融合成功 (面数={face_count})")
return core
logger.warning("Fuse 结果无几何,尝试备用方案")
except Exception as e:
logger.warning(f"底座融合失败: {e}")
return self._build_core_compound(base_plate, shape)
@staticmethod
def _build_core_compound(base_plate: TopoDS_Shape, shape: TopoDS_Shape) -> TopoDS_Shape:
"""
兜底方案:构建 TopoDS_Compound 包含底座平板 + 产品。
即使布尔融合失败,底座也绝不会丢失。
"""
compound = TopoDS_Compound()
builder = BRep_Builder()
builder.MakeCompound(compound)
builder.Add(compound, base_plate)
builder.Add(compound, shape)
logger.info("型芯 Compound 兜底构建 (底座+产品)")
return compound
def _get_parting_plane(self, parting_surface: TopoDS_Face, shape: TopoDS_Shape) -> Optional[gp_Pln]:
"""从分型面提取平面方程"""
try:
surface = BRepAdaptor_Surface(parting_surface)
if surface.GetType() == 0:
return surface.Plane()
bbox = Bnd_Box()
brepbndlib.Add(shape, bbox)
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
center_z = (zmin + zmax) / 2
return gp_Pln(gp_Pnt(0, 0, center_z), gp_Dir(0, 0, 1))
except Exception as e:
logger.warning(f"分型面平面提取失败: {e}")
return None
def _split_mold_block_by_plane(self, mold_block: TopoDS_Shape,
parting_plane: gp_Pln) -> Tuple[TopoDS_Shape, TopoDS_Shape]:
"""
用分型面将模具块切分为A板(上模)和B板(下模)
方法:使用半空间体与模具块的布尔交集运算
- A板 = 模具块 ∩ 分型面上方半空间
- B板 = 模具块 ∩ 分型面下方半空间
"""
try:
plane_origin = parting_plane.Location()
plane_normal = parting_plane.Axis().Direction()
ref_point_above = gp_Pnt(
plane_origin.X() + plane_normal.X() * 10,
plane_origin.Y() + plane_normal.Y() * 10,
plane_origin.Z() + plane_normal.Z() * 10
)
ref_point_below = gp_Pnt(
plane_origin.X() - plane_normal.X() * 10,
plane_origin.Y() - plane_normal.Y() * 10,
plane_origin.Z() - plane_normal.Z() * 10
)
half_space_above = BRepPrimAPI_MakeHalfSpace(
BRepBuilderAPI_MakeFace(parting_plane).Face(),
ref_point_above
).Shape()
half_space_below = BRepPrimAPI_MakeHalfSpace(
BRepBuilderAPI_MakeFace(parting_plane).Face(),
ref_point_below
).Shape()
a_plate_op = BRepAlgoAPI_Common(mold_block, half_space_above)
a_plate = None
if a_plate_op.IsDone():
a_plate = a_plate_op.Shape()
logger.info("A板(上模)切分成功")
else:
logger.warning("A板切分失败")
b_plate_op = BRepAlgoAPI_Common(mold_block, half_space_below)
b_plate = None
if b_plate_op.IsDone():
b_plate = b_plate_op.Shape()
logger.info("B板(下模)切分成功")
else:
logger.warning("B板切分失败")
return a_plate, b_plate
except Exception as e:
logger.error(f"A/B板分离失败: {e}")
return None, None
def _subtract_product_from_plate(self, plate: TopoDS_Shape, product: TopoDS_Shape,
plate_name: str) -> TopoDS_Shape:
"""从模板中减去产品形状,生成型腔或型芯"""
try:
cut_op = BRepAlgoAPI_Cut(plate, product)
if cut_op.IsDone():
result = cut_op.Shape()
logger.info(f"{plate_name}减去产品成功")
return result
else:
logger.warning(f"{plate_name}布尔减运算失败")
return plate
except Exception as e:
logger.warning(f"{plate_name}减产品失败: {e}")
return plate
def _split_cavity_core_fallback(self, shape: TopoDS_Shape,
mold_block: Optional[TopoDS_Shape] = None) -> Tuple[TopoDS_Shape, TopoDS_Shape]:
"""
分模回退方案:完全嵌入 + 突出贴合,用 Z 中心面做分型基准。
"""
logger.warning("使用分模回退方案(完全嵌入 + 突出贴合)")
try:
bbox = Bnd_Box()
brepbndlib.Add(shape, bbox)
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
margin = 20
if mold_block is None:
mold_block = BRepPrimAPI_MakeBox(
gp_Pnt(xmin - margin, ymin - margin, zmin - margin),
gp_Pnt(xmax + margin, ymax + margin, zmax + margin)
).Shape()
cavity = self._subtract_product_from_plate(mold_block, shape, "型腔(回退)")
if cavity is None:
cavity = mold_block
center_z = (zmin + zmax) / 2
parting_plane = gp_Pln(gp_Pnt(0, 0, center_z), gp_Dir(0, 0, 1))
core = self._build_core_with_base(
shape, mold_block, parting_plane,
xmin - margin, ymin - margin, zmin - margin,
xmax + margin, ymax + margin, zmax + margin,
xmin, ymin, zmin, xmax, ymax, zmax
)
logger.info("回退方案型腔/型芯分离完成")
return cavity or mold_block, core
except Exception as e:
logger.error(f"分模回退方案失败: {e}")
try:
bbox = Bnd_Box()
brepbndlib.Add(shape, bbox)
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
margin = 20
cavity_block = BRepPrimAPI_MakeBox(
gp_Pnt(xmin - margin, ymin - margin, zmin - margin),
gp_Pnt(xmax + margin, ymax + margin, zmax + margin)
).Shape()
cavity = self._subtract_product_from_plate(cavity_block, shape, "型腔(兜底)")
return cavity or cavity_block, shape
except Exception:
return shape, shape
def detect_insert_regions(self, shape: TopoDS_Shape, analysis: Dict,
depth_threshold: float = 30.0,
aspect_threshold: float = 3.0) -> List[Dict[str, Any]]:
"""
检测需要独立镶件的区域
镶件判定条件:
1. 深腔区域(深度超过阈值)
2. 细长特征(长径比超过阈值)
3. 易磨损区域(尖锐角落、薄壁)
4. 精密特征(高精度要求的局部区域)
Args:
shape: 产品形状
analysis: 几何分析结果
depth_threshold: 深腔深度阈值 mm
aspect_threshold: 长径比阈值
Returns:
镶件区域列表
"""
inserts = []
try:
bbox = analysis.get("bounding_box", {})
dims = bbox.get("dimensions", [0, 0, 0])
center = bbox.get("center", [0, 0, 0])
if dims[2] > depth_threshold:
inserts.append({
"type": "deep_cavity_insert",
"location": center,
"depth": dims[2],
"reason": f"型腔深度 {dims[2]:.1f}mm 超过阈值 {depth_threshold}mm",
"insert_type": "core_pin",
"priority": "high"
})
explorer = TopExp_Explorer(shape, TopAbs_FACE)
face_idx = 0
while explorer.More():
face = topods.Face(explorer.Current())
face_idx += 1
try:
surface = BRepAdaptor_Surface(face)
face_props = GProp_GProps()
brepgprop.SurfaceProperties(face, face_props)
area = face_props.Mass()
if area < 1.0 and area > 0.001:
bbox_face = Bnd_Box()
brepbndlib.Add(face, bbox_face)
try:
fxmin, fymin, fzmin, fxmax, fymax, fzmax = bbox_face.Get()
f_dims = [fxmax - fxmin, fymax - fymin, fzmax - fzmin]
max_dim = max(f_dims)
min_dim = min(f_dims)
if min_dim > 0.01 and max_dim / min_dim > aspect_threshold:
face_center = [
float((fxmin + fxmax) / 2),
float((fymin + fymax) / 2),
float((fzmin + fzmax) / 2)
]
inserts.append({
"type": "slender_feature_insert",
"location": face_center,
"aspect_ratio": max_dim / min_dim,
"reason": f"细长特征,长径比 {max_dim/min_dim:.1f}",
"insert_type": "core_pin",
"priority": "medium",
"face_index": face_idx
})
except Exception:
pass
if surface.GetType() == 1:
radius = surface.Cylinder().Radius()
if radius < 3.0 and radius > 0.1:
cyl_axis = surface.Cylinder().Position().Axis()
cyl_loc = cyl_axis.Location()
inserts.append({
"type": "small_hole_insert",
"location": [float(cyl_loc.X()), float(cyl_loc.Y()), float(cyl_loc.Z())],
"radius": float(radius),
"reason": f"小孔特征,半径 {radius:.2f}mm",
"insert_type": "core_pin",
"priority": "high",
"face_index": face_idx
})
except Exception:
pass
explorer.Next()
if not inserts:
logger.info("未检测到需要镶件的区域")
else:
logger.info(f"检测到 {len(inserts)} 个镶件区域")
except Exception as e:
logger.warning(f"镶件检测失败: {e}")
return inserts
def _extract_shape_geometry(self, shape: TopoDS_Shape, shape_type: str) -> Dict[str, Any]:
try:
mesh = BRepMesh_IncrementalMesh(shape, 0.1)
mesh.Perform()
vertices = []
faces = []
explorer = TopExp_Explorer(shape, TopAbs_FACE)
vertex_index = 0
while explorer.More():
face = explorer.Current()
location = TopLoc_Location()
triangulation = BRep_Tool.Triangulation(face, location)
if triangulation:
nb_nodes = triangulation.NbNodes()
for i in range(1, nb_nodes + 1):
node = triangulation.Node(i)
transformed = node.Transformed(location.Transformation())
vertices.extend([
float(transformed.X()),
float(transformed.Y()),
float(transformed.Z())
])
nb_triangles = triangulation.NbTriangles()
for i in range(1, nb_triangles + 1):
triangle = triangulation.Triangle(i)
idx1 = triangle.Value(1) + vertex_index - 1
idx2 = triangle.Value(2) + vertex_index - 1
idx3 = triangle.Value(3) + vertex_index - 1
faces.extend([int(idx1), int(idx2), int(idx3)])
vertex_index += nb_nodes
explorer.Next()
vertex_count = len(vertices) // 3
face_count = len(faces) // 3
return {
"type": shape_type,
"vertices": vertices,
"faces": faces,
"vertex_count": vertex_count,
"face_count": face_count,
}
except Exception as e:
logger.error(f"{shape_type}几何提取失败: {e}")
return {
"type": shape_type,
"vertices": [],
"faces": [],
"vertex_count": 0,
"face_count": 0,
}
def _extract_plane_metadata(self, surface: TopoDS_Shape) -> Dict[str, Any]:
"""从分型面提取平面元数据(法向量、原点、边界)"""
metadata = {
"normal": [0.0, 0.0, 1.0],
"origin": [0.0, 0.0, 0.0],
"bounds": {"min": [0.0, 0.0, 0.0], "max": [0.0, 0.0, 0.0]},
}
try:
surface_adaptor = BRepAdaptor_Surface(surface)
if surface_adaptor.GetType() == 0:
plane = surface_adaptor.Plane()
axis = plane.Axis()
normal = axis.Direction()
origin = plane.Location()
metadata["normal"] = [float(normal.X()), float(normal.Y()), float(normal.Z())]
metadata["origin"] = [float(origin.X()), float(origin.Y()), float(origin.Z())]
bbox = Bnd_Box()
brepbndlib.Add(surface, bbox)
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
metadata["bounds"] = {
"min": [float(xmin), float(ymin), float(zmin)],
"max": [float(xmax), float(ymax), float(zmax)],
}
except Exception as e:
logger.warning(f"提取平面元数据失败: {e}")
return metadata
def _calculate_product_weight(self, analysis: Dict) -> str:
volume_cm3 = analysis.get("volume", 0) / 1000
weight_g = volume_cm3 * self.material_density
return f"{weight_g:.2f} g"
def _assess_warpage_risk(self, analysis: Dict) -> str:
bbox = analysis.get("bounding_box", {}).get("dimensions", [1, 1, 1])
aspect_ratio = max(bbox) / min(bbox) if min(bbox) > 0 else 1
if aspect_ratio > 5:
return "高 - 建议增加加强筋"
elif aspect_ratio > 3:
return "中 - 需优化冷却"
else:
return "低"
def _get_inertia_matrix(self, props: GProp_GProps) -> List[List[float]]:
inertia = props.MatrixOfInertia()
return [
[inertia.Value(1, 1), inertia.Value(1, 2), inertia.Value(1, 3)],
[inertia.Value(2, 1), inertia.Value(2, 2), inertia.Value(2, 3)],
[inertia.Value(3, 1), inertia.Value(3, 2), inertia.Value(3, 3)]
]
def _calculate_parting_line_length(self, parting_line: List) -> float:
if not parting_line or len(parting_line) < 2:
return 0.0
total_length = 0.0
for i in range(1, len(parting_line)):
p1 = np.array(parting_line[i-1])
p2 = np.array(parting_line[i])
segment_length = np.linalg.norm(p2 - p1)
total_length += segment_length
return total_length
def _calculate_parting_line(self, shape: TopoDS_Shape, parting_surface: TopoDS_Face) -> List[List[float]]:
try:
section = BRepAlgoAPI_Section(shape, parting_surface)
section.Build()
if not section.IsDone():
logger.warning("截面运算未完成,使用简化分型线")
return self._simple_parting_line(shape)
edges = []
explorer = TopExp_Explorer(section.Shape(), TopAbs_EDGE)
while explorer.More():
edge = explorer.Current()
curve = BRepAdaptor_Curve(edge)
first_param = curve.FirstParameter()
last_param = curve.LastParameter()
num_points = max(10, int((last_param - first_param) / 0.5))
step = (last_param - first_param) / num_points
for i in range(num_points + 1):
param = first_param + i * step
point = curve.Value(param)
edges.append([point.X(), point.Y(), point.Z()])
explorer.Next()
if not edges:
logger.warning("未找到交线,使用简化分型线")
return self._simple_parting_line(shape)
logger.info(f"计算得到 {len(edges)} 个分型线点")
return edges
except Exception as e:
logger.error(f"分型线计算失败: {e}")
return self._simple_parting_line(shape)
def _simple_parting_line(self, shape: TopoDS_Shape) -> List[List[float]]:
try:
bbox = Bnd_Box()
brepbndlib.Add(shape, bbox)
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
center_z = (zmin + zmax) / 2
return [
[xmin, ymin, center_z],
[xmax, ymin, center_z],
[xmax, ymax, center_z],
[xmin, ymax, center_z],
[xmin, ymin, center_z]
]
except Exception:
return [[-50, -50, 0], [50, -50, 0], [50, 50, 0], [-50, 50, 0], [-50, -50, 0]]
def extend_parting_surface(self, parting_surface: TopoDS_Face, shape: TopoDS_Shape,
extension: float = 30.0) -> TopoDS_Face:
"""
将分型面延伸到模具块边界
分型面通常只覆盖产品轮廓,需要延伸到模具块边缘
才能正确分离A板和B板
Args:
parting_surface: 原始分型面
shape: 产品形状
extension: 延伸距离 mm
Returns:
延伸后的分型面
"""
try:
bbox = Bnd_Box()
brepbndlib.Add(shape, bbox)
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
surface = BRepAdaptor_Surface(parting_surface)
if surface.GetType() != 0:
logger.info("分型面非平面,延伸操作跳过")
return parting_surface
plane = surface.Plane()
origin = plane.Location()
normal = plane.Axis().Direction()
extended_xmin = xmin - extension
extended_ymin = ymin - extension
extended_xmax = xmax + extension
extended_ymax = ymax + extension
extended_plane = gp_Pln(origin, normal)
extended_surface = BRepBuilderAPI_MakeFace(
extended_plane,
extended_xmin, extended_xmax,
extended_ymin, extended_ymax
).Face()
logger.info(f"分型面延伸完成: 延伸距离={extension}mm")
return extended_surface
except Exception as e:
logger.warning(f"分型面延伸失败: {e}")
return parting_surface
def optimize_parting_line(self, parting_line: List[List[float]],
smooth_window: int = 5,
min_segment_length: float = 0.5,
angle_threshold: float = 150.0) -> List[List[float]]:
"""
优化分型线
优化内容:
1. 平滑处理 - 消除噪声点
2. 去除短线段 - 合并过短的线段
3. 尖角处理 - 在尖角处添加过渡圆弧
4. 点密度均匀化 - 重采样使点间距均匀
Args:
parting_line: 原始分型线点列表
smooth_window: 平滑窗口大小
min_segment_length: 最小线段长度
angle_threshold: 尖角判定角度(度)
Returns:
优化后的分型线
"""
if len(parting_line) < 3:
return parting_line
try:
smoothed = self._smooth_parting_line(parting_line, smooth_window)
filtered = self._filter_short_segments(smoothed, min_segment_length)
optimized = self._round_sharp_corners(filtered, angle_threshold)
resampled = self._resample_parting_line(optimized, target_spacing=2.0)
logger.info(f"分型线优化: {len(parting_line)} → {len(resampled)} 点")
return resampled
except Exception as e:
logger.warning(f"分型线优化失败: {e}")
return parting_line
def _smooth_parting_line(self, points: List[List[float]],
window: int = 5) -> List[List[float]]:
"""移动平均平滑"""
if len(points) < window:
return points
arr = np.array(points, dtype=np.float64)
smoothed = []
for i in range(len(arr)):
start = max(0, i - window // 2)
end = min(len(arr), i + window // 2 + 1)
avg = np.mean(arr[start:end], axis=0)
smoothed.append(avg.tolist())
return smoothed
def _filter_short_segments(self, points: List[List[float]],
min_length: float) -> List[List[float]]:
"""去除过短线段"""
if not points:
return points
filtered = [points[0]]
for i in range(1, len(points)):
dist = np.linalg.norm(np.array(points[i]) - np.array(filtered[-1]))
if dist >= min_length:
filtered.append(points[i])
return filtered
def _round_sharp_corners(self, points: List[List[float]],
angle_threshold: float) -> List[List[float]]:
"""在尖角处添加过渡点"""
if len(points) < 3:
return points
result = [points[0]]
for i in range(1, len(points) - 1):
v1 = np.array(points[i]) - np.array(points[i - 1])
v2 = np.array(points[i + 1]) - np.array(points[i])
len1 = np.linalg.norm(v1)
len2 = np.linalg.norm(v2)
if len1 > 0.001 and len2 > 0.001:
cos_angle = np.clip(np.dot(v1, v2) / (len1 * len2), -1, 1)
angle = math.degrees(math.acos(cos_angle))
if angle < angle_threshold:
mid1 = (np.array(points[i - 1]) + np.array(points[i])) / 2
mid2 = (np.array(points[i]) + np.array(points[i + 1])) / 2
result.append(mid1.tolist())
result.append(mid2.tolist())
else:
result.append(points[i])
else:
result.append(points[i])
result.append(points[-1])
return result
def _resample_parting_line(self, points: List[List[float]],
target_spacing: float) -> List[List[float]]:
"""重采样使点间距均匀"""
if len(points) < 2:
return points
arr = np.array(points, dtype=np.float64)
cumulative_dist = [0.0]
for i in range(1, len(arr)):
dist = np.linalg.norm(arr[i] - arr[i - 1])
cumulative_dist.append(cumulative_dist[-1] + dist)
total_length = cumulative_dist[-1]
if total_length < target_spacing:
return points
num_points = max(3, int(total_length / target_spacing))
new_distances = np.linspace(0, total_length, num_points)
resampled = []
for d in new_distances:
idx = np.searchsorted(cumulative_dist, d) - 1
idx = max(0, min(idx, len(arr) - 2))
seg_start = cumulative_dist[idx]
seg_end = cumulative_dist[idx + 1]
seg_length = seg_end - seg_start
if seg_length > 0:
t = (d - seg_start) / seg_length
else:
t = 0
point = arr[idx] + t * (arr[idx + 1] - arr[idx])
resampled.append(point.tolist())
return resampled
+478
View File
@@ -0,0 +1,478 @@
"""
CAD 文件导出模块
支持导出格式:
1. STEP (ISO 10303) - 推荐,UG/NX、FreeCAD、SolidWorks 通用
2. IGES (Initial Graphics Exchange Specification) - 兼容旧系统
3. STL (STereoLithography) - 网格格式,3D打印/快速预览
4. BRep (Boundary Representation) - OpenCASCADE 原生格式
导出内容:
- 型腔 (Cavity)
- 型芯 (Core)
- 分型面 (Parting Surface)
- A板/B板
- 模具块
- 完整模具装配体(多形状合并)
UG/NX 导入建议:
- 优先使用 STEP AP214 或 AP242 格式
- IGES 作为备选
- STL 仅用于预览,不可编辑
FreeCAD 导入建议:
- STEP AP214 最佳兼容性
- BRep 可直接在 FreeCAD 的 OpenCASCADE 内核中打开
"""
import os
import re
from typing import Dict, List, Any, Optional, Tuple
from pathlib import Path
from OCC.Core.TopoDS import TopoDS_Shape
from shared.utils.logger import get_logger
logger = get_logger(__name__)
class CADExporter:
"""CAD 文件导出器"""
def __init__(self, output_dir: str = "./exports"):
self.output_dir = output_dir
os.makedirs(output_dir, exist_ok=True)
@staticmethod
def _safe_segment(value: Optional[str], fallback: str) -> str:
text = str(value or "").strip()
if not text:
text = fallback
text = re.sub(r"[^A-Za-z0-9._-]+", "_", text)
return text[:80] or fallback
def build_export_dir(
self,
base_filename: str,
task_id: Optional[str] = None,
scheme_id: Optional[str] = None,
) -> str:
if task_id:
task_segment = self._safe_segment(task_id, "task")
scheme_segment = self._safe_segment(scheme_id, "default")
return os.path.join(self.output_dir, task_segment, scheme_segment)
return os.path.join(self.output_dir, self._safe_segment(base_filename, "mold"))
def get_relative_path(self, filepath: str) -> str:
full_path = Path(filepath).resolve()
output_root = Path(self.output_dir).resolve()
try:
relative = full_path.relative_to(output_root)
except ValueError:
relative = Path(os.path.basename(filepath))
return relative.as_posix()
def export_step(self, shape: TopoDS_Shape, filepath: str,
schema: str = "AP214") -> bool:
"""
导出 STEP 文件
Args:
shape: OpenCASCADE TopoDS_Shape
filepath: 输出文件路径
schema: STEP 应用协议 (AP203/AP214/AP242)
Returns:
是否成功
"""
try:
from OCC.Core.STEPControl import (
STEPControl_Writer,
STEPControl_AsIs,
)
from OCC.Core.Interface import Interface_Static
writer = STEPControl_Writer()
if schema == "AP203":
Interface_Static.SetCVal("write.step.schema", "AP203")
elif schema == "AP242":
Interface_Static.SetCVal("write.step.schema", "AP242")
else:
Interface_Static.SetCVal("write.step.schema", "AP214")
writer.Transfer(shape, STEPControl_AsIs)
status = writer.Write(filepath)
if status == 1:
file_size = os.path.getsize(filepath) if os.path.exists(filepath) else 0
logger.info(f"STEP 导出成功: {filepath} ({file_size} bytes, {schema})")
return True
else:
logger.error(f"STEP 导出失败: 写入状态={status}")
return False
except ImportError as e:
logger.error(f"STEP 导出依赖缺失: {e}")
return False
except Exception as e:
logger.error(f"STEP 导出失败: {e}")
return False
def export_iges(self, shape: TopoDS_Shape, filepath: str) -> bool:
"""
导出 IGES 文件
Args:
shape: OpenCASCADE TopoDS_Shape
filepath: 输出文件路径
Returns:
是否成功
"""
try:
from OCC.Core.IGESControl import IGESControl_Writer
from OCC.Core.Interface import Interface_Static
Interface_Static.SetCVal("write.iges.brep.mode", "0")
writer = IGESControl_Writer()
writer.AddShape(shape)
writer.ComputeModel()
status = writer.Write(filepath)
if status:
file_size = os.path.getsize(filepath) if os.path.exists(filepath) else 0
logger.info(f"IGES 导出成功: {filepath} ({file_size} bytes)")
return True
else:
logger.error("IGES 导出失败: 写入返回 False")
return False
except ImportError as e:
logger.error(f"IGES 导出依赖缺失: {e}")
return False
except Exception as e:
logger.error(f"IGES 导出失败: {e}")
return False
def export_stl(self, shape: TopoDS_Shape, filepath: str,
ascii_mode: bool = True,
deflection: float = 0.1) -> bool:
"""
导出 STL 文件
Args:
shape: OpenCASCADE TopoDS_Shape
filepath: 输出文件路径
ascii_mode: True=ASCII格式, False=二进制格式
deflection: 网格偏差(越小越精细)
Returns:
是否成功
"""
try:
from OCC.Core.StlAPI import StlAPI_Writer
from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh
mesh = BRepMesh_IncrementalMesh(shape, deflection)
mesh.Perform()
if not mesh.IsDone():
logger.warning("STL 网格化未完成,尝试继续导出")
writer = StlAPI_Writer()
writer.AsciiMode = ascii_mode
writer.Write(shape, filepath)
if os.path.exists(filepath) and os.path.getsize(filepath) > 0:
file_size = os.path.getsize(filepath)
logger.info(f"STL 导出成功: {filepath} ({file_size} bytes)")
return True
else:
logger.error("STL 导出失败: 文件为空或不存在")
return False
except ImportError as e:
logger.error(f"STL 导出依赖缺失: {e}")
return False
except Exception as e:
logger.error(f"STL 导出失败: {e}")
return False
def export_brep(self, shape: TopoDS_Shape, filepath: str) -> bool:
"""
导出 BRep 文件(OpenCASCADE 原生格式)
FreeCAD 可直接导入此格式
Args:
shape: OpenCASCADE TopoDS_Shape
filepath: 输出文件路径
Returns:
是否成功
"""
try:
from OCC.Core.BRepTools import BRepTools_Write
BRepTools_Write(shape, filepath)
if os.path.exists(filepath) and os.path.getsize(filepath) > 0:
file_size = os.path.getsize(filepath)
logger.info(f"BRep 导出成功: {filepath} ({file_size} bytes)")
return True
else:
logger.error("BRep 导出失败: 文件为空或不存在")
return False
except ImportError as e:
logger.error(f"BRep 导出依赖缺失: {e}")
return False
except Exception as e:
logger.error(f"BRep 导出失败: {e}")
return False
def export_mold_results(self, cavity_data: Dict,
base_filename: str,
formats: List[str] = None,
components: List[str] = None,
task_id: Optional[str] = None,
scheme_id: Optional[str] = None) -> Dict[str, Any]:
if formats is None:
formats = ["step", "stl"]
if components is None:
components = ["cavity", "core"]
export_dir = self.build_export_dir(
base_filename=base_filename,
task_id=task_id,
scheme_id=scheme_id,
)
os.makedirs(export_dir, exist_ok=True)
results = {
"base_filename": base_filename,
"task_id": task_id,
"scheme_id": scheme_id,
"export_dir": export_dir,
"files": [],
"errors": [],
}
shape_map = {
"cavity": ("cavity", "型腔"),
"core": ("core", "型芯"),
"parting_surface": ("parting_surface", "分型面"),
}
shapes_to_export: List[Tuple[str, str, TopoDS_Shape]] = []
assembly_shapes: List[Tuple[TopoDS_Shape, str]] = []
for comp in components:
if comp == "all":
for key, (data_key, label) in shape_map.items():
shape = cavity_data.get(data_key)
if shape is not None:
shapes_to_export.append((key, label, shape))
assembly_shapes.append((shape, label))
break
elif comp in shape_map:
data_key, label = shape_map[comp]
shape = cavity_data.get(data_key)
if shape is not None:
shapes_to_export.append((comp, label, shape))
assembly_shapes.append((shape, label))
else:
results["errors"].append(f"{label}形状不可用")
# STEP: 所有组件合并为一个装配体文件
if "step" in formats and assembly_shapes:
filepath = os.path.join(export_dir, f"{base_filename}_mold.step")
success = self.export_assembly_step(assembly_shapes, filepath)
if success:
file_size = os.path.getsize(filepath)
relative_path = self.get_relative_path(filepath)
results["files"].append({
"component": "assembly",
"component_label": "模具装配体",
"format": "step",
"filepath": filepath,
"relative_path": relative_path,
"filename": os.path.basename(filepath),
"size_bytes": file_size,
"size_readable": self._format_file_size(file_size),
})
else:
results["errors"].append("装配体 STEP 导出失败")
# IGES / STL / BRep: 逐组件导出
non_assembly_formats = [f for f in formats if f != "step"]
for comp_name, label, shape in shapes_to_export:
for fmt in non_assembly_formats:
filepath = os.path.join(export_dir, f"{base_filename}_{comp_name}.{fmt}")
success = False
if fmt == "iges":
success = self.export_iges(shape, filepath)
elif fmt == "stl":
success = self.export_stl(shape, filepath)
elif fmt == "brep":
success = self.export_brep(shape, filepath)
else:
results["errors"].append(f"不支持的格式: {fmt}")
continue
if success:
file_size = os.path.getsize(filepath)
relative_path = self.get_relative_path(filepath)
results["files"].append({
"component": comp_name,
"component_label": label,
"format": fmt,
"filepath": filepath,
"relative_path": relative_path,
"filename": os.path.basename(filepath),
"size_bytes": file_size,
"size_readable": self._format_file_size(file_size),
})
else:
results["errors"].append(f"{label} ({fmt}) 导出失败")
results["total_files"] = len(results["files"])
results["total_errors"] = len(results["errors"])
logger.info(f"模具导出完成: {results['total_files']} 个文件, "
f"{results['total_errors']} 个错误")
return results
def export_assembly_step(self, shapes_with_names: List[Tuple[TopoDS_Shape, str]],
filepath: str,
schema: str = "AP214") -> bool:
"""
导出装配体 STEP 文件(多个形状写入同一个 STEP 文件)
UG/NX 和 FreeCAD 可以识别装配体中的各个零件
Args:
shapes_with_names: [(shape, name), ...] 形状和名称列表
filepath: 输出文件路径
schema: STEP 协议版本
Returns:
是否成功
"""
try:
from OCC.Core.STEPControl import (
STEPControl_Writer,
STEPControl_AsIs,
)
from OCC.Core.Interface import Interface_Static
writer = STEPControl_Writer()
if schema == "AP203":
Interface_Static.SetCVal("write.step.schema", "AP203")
elif schema == "AP242":
Interface_Static.SetCVal("write.step.schema", "AP242")
else:
Interface_Static.SetCVal("write.step.schema", "AP214")
for shape, name in shapes_with_names:
try:
writer.Transfer(shape, STEPControl_AsIs)
logger.info(f"已添加到装配体: {name}")
except Exception as e:
logger.warning(f"添加形状 {name} 失败: {e}")
status = writer.Write(filepath)
if status == 1:
file_size = os.path.getsize(filepath) if os.path.exists(filepath) else 0
logger.info(f"装配体 STEP 导出成功: {filepath} ({file_size} bytes)")
return True
else:
logger.error(f"装配体 STEP 导出失败: 状态={status}")
return False
except Exception as e:
logger.error(f"装配体 STEP 导出失败: {e}")
return False
def get_export_recommendations(self, target_software: str = "ug") -> Dict[str, Any]:
"""
获取针对目标软件的导出建议
Args:
target_software: 目标软件 (ug/nx, freecad, solidworks, autocad)
Returns:
导出建议
"""
recommendations = {
"ug": {
"name": "UG/NX",
"primary_format": "step",
"step_schema": "AP242",
"secondary_format": "iges",
"notes": [
"推荐 STEP AP242 格式,支持颜色和装配信息",
"IGES 作为备选,但可能丢失拓扑信息",
"STL 仅用于预览,不可参数化编辑",
"导入时选择 '保留原始坐标系'",
],
"import_settings": {
"step": "File → Import → STEP203/214/242",
"iges": "File → Import → IGES",
"stl": "File → Import → STL (仅可视化)",
},
},
"freecad": {
"name": "FreeCAD",
"primary_format": "step",
"step_schema": "AP214",
"secondary_format": "brep",
"notes": [
"STEP AP214 最佳兼容性",
"BRep 是 OpenCASCADE 原生格式,FreeCAD 可直接打开",
"导入后可在 Part 工作台中编辑",
"推荐使用 FreeCAD 0.21+ 版本",
],
"import_settings": {
"step": "File → Import → 选择 STEP 文件",
"iges": "File → Import → 选择 IGES 文件",
"brep": "File → Open → 选择 BRep 文件",
"stl": "File → Import → Mesh 格式",
},
},
"solidworks": {
"name": "SolidWorks",
"primary_format": "step",
"step_schema": "AP214",
"secondary_format": "iges",
"notes": [
"STEP AP214 最佳兼容性",
"导入后自动识别为实体",
"IGES 可能产生曲面而非实体",
],
"import_settings": {
"step": "File → Open → STEP 文件",
"iges": "File → Open → IGES 文件",
},
},
}
return recommendations.get(target_software, recommendations["ug"])
@staticmethod
def _format_file_size(size_bytes: int) -> str:
"""格式化文件大小"""
if size_bytes < 1024:
return f"{size_bytes} B"
elif size_bytes < 1024 * 1024:
return f"{size_bytes / 1024:.1f} KB"
else:
return f"{size_bytes / (1024 * 1024):.1f} MB"
@@ -0,0 +1,437 @@
"""
多型腔布局优化模块
功能:
1. 支持矩形、圆形、H型等常见多型腔排列方式
2. 基于产品尺寸和模架尺寸自动计算最优布局
3. 流道系统自动设计
4. 流动平衡评估
5. 材料利用率计算
布局策略:
- 1穴:中心单型腔
- 2穴:对称排列
- 4穴:2x2 矩阵排列
- 8穴:2x4 矩阵排列
- 16穴:4x4 矩阵排列
- 圆形排列:适用于圆形产品
"""
from typing import Dict, List, Any, Optional, Tuple
import math
import numpy as np
from shared.utils.logger import get_logger
logger = get_logger(__name__)
class CavityLayoutOptimizer:
"""多型腔布局优化器"""
LAYOUT_RECTANGULAR = "rectangular"
LAYOUT_CIRCULAR = "circular"
LAYOUT_H_SHAPE = "h_shape"
LAYOUT_INLINE = "inline"
def __init__(self):
self.runner_diameter = 5.0
self.gate_diameter = 1.5
self.cavity_margin = 15.0
self.runner_margin = 25.0
def optimize_layout(self, product_bbox: Dict, cavity_count: int,
mold_base_size: Optional[Dict] = None,
layout_type: str = "auto",
product_shape: Any = None) -> Dict[str, Any]:
"""
优化多型腔布局
Args:
product_bbox: 产品边界框 {"dimensions": [dx, dy, dz]}
cavity_count: 型腔数量
mold_base_size: 模架尺寸 {"length": L, "width": W}
layout_type: 布局类型 (auto/rectangular/circular/h_shape/inline)
product_shape: 产品形状(可选,用于精确计算)
Returns:
{
"layout_type": str,
"cavity_positions": List[[x, y, z]],
"cavity_rotations": List[[rx, ry, rz]],
"runner_system": Dict,
"balance_score": float,
"material_efficiency": float,
"mold_size": Dict,
"recommendations": List[str]
}
"""
logger.info(f"开始多型腔布局优化: {cavity_count}穴, 布局={layout_type}")
dims = product_bbox.get("dimensions", [100, 100, 50])
if layout_type == "auto":
layout_type = self._recommend_layout(cavity_count, dims)
if layout_type == self.LAYOUT_RECTANGULAR:
result = self._layout_rectangular(dims, cavity_count, mold_base_size)
elif layout_type == self.LAYOUT_CIRCULAR:
result = self._layout_circular(dims, cavity_count, mold_base_size)
elif layout_type == self.LAYOUT_H_SHAPE:
result = self._layout_h_shape(dims, cavity_count, mold_base_size)
elif layout_type == self.LAYOUT_INLINE:
result = self._layout_inline(dims, cavity_count, mold_base_size)
else:
result = self._layout_rectangular(dims, cavity_count, mold_base_size)
result["runner_system"] = self._design_runner_system(
result["cavity_positions"], cavity_count, layout_type
)
result["balance_score"] = self._evaluate_flow_balance(
result["cavity_positions"], result["runner_system"]
)
result["material_efficiency"] = self._calculate_material_efficiency(
dims, cavity_count, result["mold_size"]
)
result["recommendations"] = self._generate_recommendations(
result, cavity_count, dims
)
logger.info(f"布局优化完成: {layout_type}, 平衡度={result['balance_score']:.2f}, "
f"材料利用率={result['material_efficiency']:.2%}")
return result
def _recommend_layout(self, cavity_count: int, dims: List[float]) -> str:
"""根据型腔数量和产品尺寸推荐布局方式"""
aspect_ratio = max(dims[:2]) / min(dims[:2]) if min(dims[:2]) > 0 else 1
if cavity_count == 1:
return self.LAYOUT_RECTANGULAR
elif cavity_count == 2:
if aspect_ratio > 2:
return self.LAYOUT_INLINE
return self.LAYOUT_RECTANGULAR
elif cavity_count <= 4:
return self.LAYOUT_RECTANGULAR
elif cavity_count <= 8:
if aspect_ratio > 2:
return self.LAYOUT_H_SHAPE
return self.LAYOUT_RECTANGULAR
else:
return self.LAYOUT_H_SHAPE
def _layout_rectangular(self, dims: List[float], cavity_count: int,
mold_base_size: Optional[Dict]) -> Dict:
"""矩形矩阵排列"""
rows, cols = self._calculate_grid(cavity_count)
spacing_x = dims[0] + 2 * self.cavity_margin
spacing_y = dims[1] + 2 * self.cavity_margin
positions = []
rotations = []
for r in range(rows):
for c in range(cols):
if len(positions) >= cavity_count:
break
x = (c - (cols - 1) / 2) * spacing_x
y = (r - (rows - 1) / 2) * spacing_y
positions.append([x, y, 0])
rotations.append([0, 0, 0])
total_length = cols * spacing_x + 2 * self.runner_margin
total_width = rows * spacing_y + 2 * self.runner_margin
mold_size = {
"length": max(total_length, mold_base_size.get("length", 0)) if mold_base_size else total_length,
"width": max(total_width, mold_base_size.get("width", 0)) if mold_base_size else total_width,
}
return {
"layout_type": self.LAYOUT_RECTANGULAR,
"cavity_positions": positions,
"cavity_rotations": rotations,
"grid": {"rows": rows, "cols": cols},
"spacing": {"x": spacing_x, "y": spacing_y},
"mold_size": mold_size,
}
def _layout_circular(self, dims: List[float], cavity_count: int,
mold_base_size: Optional[Dict]) -> Dict:
"""圆形排列"""
max_dim = max(dims[:2])
radius = max_dim / 2 + self.cavity_margin + self.runner_margin
positions = []
rotations = []
for i in range(cavity_count):
angle = 2 * math.pi * i / cavity_count
x = radius * math.cos(angle)
y = radius * math.sin(angle)
rot_z = -math.degrees(angle)
positions.append([x, y, 0])
rotations.append([0, 0, rot_z])
total_diameter = 2 * radius + max_dim + 2 * self.cavity_margin
mold_size = {
"length": total_diameter,
"width": total_diameter,
}
return {
"layout_type": self.LAYOUT_CIRCULAR,
"cavity_positions": positions,
"cavity_rotations": rotations,
"radius": radius,
"mold_size": mold_size,
}
def _layout_h_shape(self, dims: List[float], cavity_count: int,
mold_base_size: Optional[Dict]) -> Dict:
"""H型排列(适用于多型腔,流道平衡性好)"""
left_count = cavity_count // 2
right_count = cavity_count - left_count
spacing_x = dims[0] + 2 * self.cavity_margin
spacing_y = dims[1] + 2 * self.cavity_margin
positions = []
rotations = []
left_rows, left_cols = self._calculate_grid(left_count)
for r in range(left_rows):
for c in range(left_cols):
if len(positions) >= left_count:
break
x = -(c + 1) * spacing_x - spacing_x / 2
y = (r - (left_rows - 1) / 2) * spacing_y
positions.append([x, y, 0])
rotations.append([0, 0, 0])
right_rows, right_cols = self._calculate_grid(right_count)
for r in range(right_rows):
for c in range(right_cols):
if len(positions) >= cavity_count:
break
x = (c + 1) * spacing_x + spacing_x / 2
y = (r - (right_rows - 1) / 2) * spacing_y
positions.append([x, y, 0])
rotations.append([0, 0, 0])
total_length = (max(left_cols, right_cols) + 1) * spacing_x * 2 + 2 * self.runner_margin
total_width = max(left_rows, right_rows) * spacing_y + 2 * self.runner_margin
mold_size = {
"length": total_length,
"width": total_width,
}
return {
"layout_type": self.LAYOUT_H_SHAPE,
"cavity_positions": positions,
"cavity_rotations": rotations,
"mold_size": mold_size,
}
def _layout_inline(self, dims: List[float], cavity_count: int,
mold_base_size: Optional[Dict]) -> Dict:
"""直线排列(适用于细长产品)"""
spacing = max(dims[:2]) + 2 * self.cavity_margin
positions = []
rotations = []
for i in range(cavity_count):
offset = (i - (cavity_count - 1) / 2) * spacing
if dims[0] > dims[1]:
positions.append([offset, 0, 0])
else:
positions.append([0, offset, 0])
rotations.append([0, 0, 0])
if dims[0] > dims[1]:
total_length = cavity_count * spacing + 2 * self.runner_margin
total_width = dims[1] + 2 * self.cavity_margin + 2 * self.runner_margin
else:
total_length = dims[0] + 2 * self.cavity_margin + 2 * self.runner_margin
total_width = cavity_count * spacing + 2 * self.runner_margin
mold_size = {
"length": total_length,
"width": total_width,
}
return {
"layout_type": self.LAYOUT_INLINE,
"cavity_positions": positions,
"cavity_rotations": rotations,
"mold_size": mold_size,
}
def _calculate_grid(self, count: int) -> Tuple[int, int]:
"""计算最接近正方形的网格排列"""
if count <= 0:
return 1, 1
best_rows = 1
best_cols = count
best_ratio = float("inf")
for r in range(1, count + 1):
if count % r == 0:
c = count // r
ratio = abs(r - c)
if ratio < best_ratio:
best_ratio = ratio
best_rows = r
best_cols = c
return best_rows, best_cols
def _design_runner_system(self, positions: List[List[float]],
cavity_count: int,
layout_type: str) -> Dict:
"""
设计流道系统
Returns:
{
"type": "cold_runner" | "hot_runner",
"main_runner": Dict,
"sub_runners": List[Dict],
"gates": List[Dict],
"total_volume": float
}
"""
if cavity_count == 1:
return self._design_single_cavity_runner(positions[0])
main_runner = {
"start": [0, -positions[0][1] - 20, 0],
"end": [0, positions[0][1] + 20, 0] if len(positions) > 0 else [0, 20, 0],
"diameter": self.runner_diameter,
"length": 0,
}
sub_runners = []
gates = []
total_volume = 0
for i, pos in enumerate(positions):
sub_runner = {
"start": [0, pos[1], 0],
"end": pos,
"diameter": self.runner_diameter * 0.8,
"length": float(np.linalg.norm(np.array(pos))),
}
sub_runners.append(sub_runner)
total_volume += math.pi * (sub_runner["diameter"] / 2) ** 2 * sub_runner["length"]
gate = {
"position": pos,
"diameter": self.gate_diameter,
"type": "side_gate",
"length": 2.0,
}
gates.append(gate)
total_volume += math.pi * (gate["diameter"] / 2) ** 2 * gate["length"]
main_runner["length"] = max(
abs(p[1]) for p in positions
) * 2 + 40 if positions else 40
total_volume += math.pi * (main_runner["diameter"] / 2) ** 2 * main_runner["length"]
return {
"type": "cold_runner",
"main_runner": main_runner,
"sub_runners": sub_runners,
"gates": gates,
"total_volume": total_volume,
}
def _design_single_cavity_runner(self, position: List[float]) -> Dict:
"""单型腔流道设计"""
gate = {
"position": position,
"diameter": self.gate_diameter,
"type": "center_gate",
"length": 3.0,
}
return {
"type": "cold_runner",
"main_runner": None,
"sub_runners": [],
"gates": [gate],
"total_volume": math.pi * (gate["diameter"] / 2) ** 2 * gate["length"],
}
def _evaluate_flow_balance(self, positions: List[List[float]],
runner_system: Dict) -> float:
"""
评估流动平衡度 (0-1)
基于各型腔到主流道的距离差异
"""
if len(positions) <= 1:
return 1.0
distances = []
for pos in positions:
dist = float(np.linalg.norm(np.array(pos)))
distances.append(dist)
max_dist = max(distances)
min_dist = min(distances)
if max_dist == 0:
return 1.0
imbalance = (max_dist - min_dist) / max_dist
balance_score = max(0, 1.0 - imbalance)
return round(balance_score, 3)
def _calculate_material_efficiency(self, product_dims: List[float],
cavity_count: int,
mold_size: Dict) -> float:
"""计算材料利用率"""
product_area = product_dims[0] * product_dims[1]
total_product_area = product_area * cavity_count
mold_area = mold_size.get("length", 0) * mold_size.get("width", 0)
if mold_area <= 0:
return 0.0
return min(1.0, total_product_area / mold_area)
def _generate_recommendations(self, result: Dict, cavity_count: int,
dims: List[float]) -> List[str]:
"""生成优化建议"""
recommendations = []
balance = result.get("balance_score", 0)
if balance < 0.8:
recommendations.append("流动平衡度偏低,建议调整型腔间距或使用热流道系统")
efficiency = result.get("material_efficiency", 0)
if efficiency < 0.4:
recommendations.append("材料利用率偏低,建议减少模架尺寸或增加型腔数量")
if cavity_count > 8:
recommendations.append("多型腔模具建议使用热流道系统以保证填充平衡")
if cavity_count > 16:
recommendations.append("型腔数量过多,建议分模评估加工可行性")
aspect = max(dims[:2]) / min(dims[:2]) if min(dims[:2]) > 0 else 1
if aspect > 3:
recommendations.append("产品长宽比大,建议使用侧浇口或扇形浇口")
if not recommendations:
recommendations.append("布局方案合理,建议进行模流分析验证")
return recommendations
+835
View File
@@ -0,0 +1,835 @@
from typing import Dict, List, Any, Optional
import math
from concurrent.futures import ThreadPoolExecutor, as_completed
import numpy as np
from OCC.Core.TopoDS import TopoDS_Shape
from shared.models.schemas import (
create_mold_feature,
create_design_recommendation,
create_analysis_result
)
from shared.utils.logger import get_logger
logger = get_logger(__name__)
class GeometryAnalyzer:
"""几何分析器 - 基于 OCC Shape 的精确分析"""
def __init__(self):
self.feature_thresholds = {
"thin_wall": 2.0,
"thick_wall": 8.0,
"small_feature": 5.0,
"large_feature": 1000.0,
"high_complexity": 50,
}
self.product_materials = {
"ABS": {"shrinkage": 0.005, "min_wall": 1.2},
"PP": {"shrinkage": 0.016, "min_wall": 1.0},
"PC": {"shrinkage": 0.007, "min_wall": 1.5},
}
self.mold_materials = {
"Aluminum": {"thermal_conductivity": 200, "hardness": "HB80", "cost": "low"},
"P20_Steel": {"thermal_conductivity": 30, "hardness": "HRC30", "cost": "medium"},
"H13_Steel": {"thermal_conductivity": 25, "hardness": "HRC48", "cost": "high"}
}
def analyze_mold_design(self, geometry_data: Dict[str, Any],
product_material: str = "ABS",
mold_material: str = "Aluminum",
shape: Optional[TopoDS_Shape] = None
) -> Dict[str, Any]:
"""分析模具设计
Args:
geometry_data: 几何数据字典(来自 stp_parser)
product_material: 产品材料
mold_material: 模具材料
shape: OCC TopoDS_Shape 对象(可选,提供后启用精确分析)
"""
logger.info("开始模具设计分析")
features = self._detect_features(geometry_data, shape)
product_props = self.product_materials.get(product_material, {})
mold_props = self.mold_materials.get(mold_material, {})
recommendations = self._generate_recommendations(
geometry_data, features, product_material
)
quality_metrics = self._calculate_quality_metrics(geometry_data, features)
analysis_summary = self._generate_analysis_summary(geometry_data, features, recommendations)
return create_analysis_result(
geometry_data=geometry_data,
detected_features=features,
design_recommendations=recommendations,
quality_metrics=quality_metrics,
analysis_summary=analysis_summary
)
def _detect_features(self, geometry_data: Dict[str, Any],
shape: Optional[TopoDS_Shape] = None) -> List[Dict[str, Any]]:
"""检测模具特征 — 独立检测并行执行"""
features: List[Dict[str, Any]] = []
with ThreadPoolExecutor(max_workers=4, thread_name_prefix="feat") as pool:
futures = {
pool.submit(self._detect_wall_features, geometry_data, shape): "wall",
pool.submit(self._detect_rib_features, geometry_data, shape): "rib",
pool.submit(self._detect_boss_features, geometry_data, shape): "boss",
pool.submit(self._analyze_draft_angles, geometry_data, shape): "draft",
}
if shape is not None:
futures[pool.submit(self._detect_curvature_features, shape)] = "curvature"
futures[pool.submit(self._detect_fillet_features, shape)] = "fillet"
for future in as_completed(futures):
try:
features.extend(future.result())
except Exception:
pass
logger.info(f"检测到 {len(features)} 个特征")
return features
def _detect_wall_features(self, geometry_data: Dict[str, Any],
shape: Optional[TopoDS_Shape] = None) -> List[Dict[str, Any]]:
"""检测壁厚特征"""
features = []
if shape is not None:
precise_result = self._compute_precise_wall_thickness(shape)
if precise_result is not None:
min_thickness = precise_result["min_thickness"]
max_thickness = precise_result["max_thickness"]
avg_thickness = precise_result["avg_thickness"]
thickness_map = precise_result.get("thickness_map", {})
estimation_method = "precise"
if min_thickness < self.feature_thresholds["thin_wall"]:
features.append(create_mold_feature(
feature_type="thin_wall",
confidence=0.92,
location=precise_result.get("min_location",
geometry_data.get("center_of_mass", [0, 0, 0])),
dimensions=[min_thickness, avg_thickness, max_thickness],
parameters={
"min_thickness": round(min_thickness, 3),
"max_thickness": round(max_thickness, 3),
"avg_thickness": round(avg_thickness, 3),
"estimation_method": estimation_method,
"measured_pairs": len(thickness_map),
},
recommendations=[
f"最小壁厚 {min_thickness:.2f}mm 过薄,建议增加到 {self.feature_thresholds['thin_wall']}mm 以上",
"薄壁区域可能导致注塑填充不充分",
"考虑增加加强筋以提高结构强度"
]
))
elif max_thickness > self.feature_thresholds["thick_wall"]:
features.append(create_mold_feature(
feature_type="thick_wall",
confidence=0.88,
location=precise_result.get("max_location",
geometry_data.get("center_of_mass", [0, 0, 0])),
dimensions=[min_thickness, avg_thickness, max_thickness],
parameters={
"min_thickness": round(min_thickness, 3),
"max_thickness": round(max_thickness, 3),
"avg_thickness": round(avg_thickness, 3),
"estimation_method": estimation_method,
"measured_pairs": len(thickness_map),
},
recommendations=[
f"最大壁厚 {max_thickness:.2f}mm 过厚,可能产生缩痕",
"考虑减薄壁厚或增加加强筋",
"优化冷却系统设计"
]
))
if min_thickness > 0 and max_thickness > 0:
uniformity = min_thickness / max_thickness if max_thickness > 0 else 1.0
if uniformity < 0.5:
features.append(create_mold_feature(
feature_type="wall_non_uniform",
confidence=0.80,
location=geometry_data.get("center_of_mass", [0, 0, 0]),
dimensions=[min_thickness, max_thickness, uniformity],
parameters={
"uniformity_ratio": round(uniformity, 3),
"min_thickness": round(min_thickness, 3),
"max_thickness": round(max_thickness, 3),
"estimation_method": estimation_method,
},
recommendations=[
f"壁厚均匀性比 {uniformity:.2f} 偏低(建议 > 0.5)",
"壁厚差异过大可能导致翘曲和缩痕",
"建议逐步过渡壁厚,避免突变"
]
))
return features
volume = geometry_data.get("volume", 0)
surface_area = geometry_data.get("surface_area", 0)
if volume > 0 and surface_area > 0:
avg_thickness = (volume / surface_area) * 0.6
if avg_thickness < self.feature_thresholds["thin_wall"]:
features.append(create_mold_feature(
feature_type="thin_wall",
confidence=0.85,
location=geometry_data.get("center_of_mass", [0, 0, 0]),
dimensions=[avg_thickness, avg_thickness, avg_thickness],
parameters={"average_thickness": avg_thickness, "estimation_method": "heuristic"},
recommendations=[
f"平均壁厚 {avg_thickness:.2f}mm 过薄,建议增加到 {self.feature_thresholds['thin_wall']}mm 以上",
"考虑增加加强筋以提高结构强度",
"检查注塑填充是否充分"
]
))
elif avg_thickness > self.feature_thresholds["thick_wall"]:
features.append(create_mold_feature(
feature_type="thick_wall",
confidence=0.75,
location=geometry_data.get("center_of_mass", [0, 0, 0]),
dimensions=[avg_thickness, avg_thickness, avg_thickness],
parameters={"average_thickness": avg_thickness, "estimation_method": "heuristic"},
recommendations=[
f"平均壁厚 {avg_thickness:.2f}mm 过厚,可能产生缩痕",
"考虑减薄壁厚或增加加强筋",
"优化冷却系统设计"
]
))
elif volume > 0:
bbox = geometry_data.get("bounding_box", {})
dimensions = bbox.get("dimensions", [100, 100, 100])
bbox_volume = dimensions[0] * dimensions[1] * dimensions[2]
if bbox_volume > 0:
volume_efficiency = volume / bbox_volume
avg_thickness = (dimensions[0] + dimensions[1]) / 2 * volume_efficiency
if avg_thickness < self.feature_thresholds["thin_wall"]:
features.append(create_mold_feature(
feature_type="thin_wall",
confidence=0.7,
location=bbox.get("center", [50, 50, 50]),
dimensions=[avg_thickness, avg_thickness, avg_thickness],
parameters={"average_thickness": avg_thickness, "estimation_method": "bbox_based"},
recommendations=[
f"估算平均壁厚 {avg_thickness:.2f}mm 过薄,建议检查表面积数据",
"考虑增加加强筋以提高结构强度"
]
))
return features
def _compute_precise_wall_thickness(self, shape: TopoDS_Shape) -> Optional[Dict[str, Any]]:
"""使用 BRepExtrema_DistShapeShape 精确计算壁厚"""
try:
from OCC.Core.TopExp import TopExp_Explorer
from OCC.Core.TopAbs import TopAbs_FACE
from OCC.Core.TopoDS import TopoDS_Face, topods
from OCC.Core.BRepExtrema import BRepExtrema_DistShapeShape
from OCC.Core.GProp import GProp_GProps
from OCC.Core.BRepGProp import brepgprop
from OCC.Core.Bnd import Bnd_Box
from OCC.Core.BRepBndLib import brepbndlib
from OCC.Core.gp import gp_Pnt
faces = []
explorer = TopExp_Explorer(shape, TopAbs_FACE)
while explorer.More():
faces.append(topods.Face(explorer.Current()))
explorer.Next()
if len(faces) < 2:
return None
face_areas = []
for face in faces:
props = GProp_GProps()
brepgprop.SurfaceProperties(face, props)
face_areas.append(props.Mass())
indexed_faces = sorted(enumerate(faces), key=lambda x: face_areas[x[0]], reverse=True)
max_faces_to_check = min(len(indexed_faces), 30)
min_thickness = float('inf')
max_thickness = 0.0
thickness_values = []
min_location = [0, 0, 0]
max_location = [0, 0, 0]
for i in range(max_faces_to_check):
for j in range(i + 1, max_faces_to_check):
idx_i, face_i = indexed_faces[i]
idx_j, face_j = indexed_faces[j]
try:
dist_calc = BRepExtrema_DistShapeShape(face_i, face_j)
if dist_calc.IsDone():
dist = dist_calc.Value()
if 0.1 < dist < 50.0:
thickness_values.append(dist)
if dist < min_thickness:
min_thickness = dist
try:
p1 = dist_calc.PointOnShape1(1)
min_location = [float(p1.X()), float(p1.Y()), float(p1.Z())]
except Exception:
pass
if dist > max_thickness:
max_thickness = dist
try:
p2 = dist_calc.PointOnShape2(1)
max_location = [float(p2.X()), float(p2.Y()), float(p2.Z())]
except Exception:
pass
except Exception:
continue
if not thickness_values:
return None
avg_thickness = sum(thickness_values) / len(thickness_values)
return {
"min_thickness": min_thickness,
"max_thickness": max_thickness,
"avg_thickness": avg_thickness,
"thickness_map": {f"pair_{i}": v for i, v in enumerate(thickness_values[:50])},
"measured_pairs": len(thickness_values),
"min_location": min_location,
"max_location": max_location,
}
except ImportError:
logger.warning("pythonOCC 不可用,无法进行精确壁厚检测")
return None
except Exception as e:
logger.warning(f"精确壁厚检测失败: {e}")
return None
def _detect_rib_features(self, geometry_data: Dict[str, Any],
shape: Optional[TopoDS_Shape] = None) -> List[Dict[str, Any]]:
"""检测加强筋特征"""
features = []
topology = geometry_data.get("topology", {})
face_count = topology.get("faces", 0)
edge_count = topology.get("edges", 0)
complexity_ratio = edge_count / max(face_count, 1)
if complexity_ratio > 3.0:
confidence = 0.7
if shape is not None:
confidence = 0.78
features.append(create_mold_feature(
feature_type="rib_structure",
confidence=confidence,
location=geometry_data.get("center_of_mass", [0, 0, 0]),
dimensions=[2.0, 8.0, 2.0],
parameters={"complexity_ratio": complexity_ratio},
recommendations=[
"检测到可能的加强筋结构",
"建议加强筋厚度为壁厚的50-80%",
"加强筋高度不超过壁厚的3倍",
"加强筋根部增加圆角避免应力集中"
]
))
return features
def _detect_boss_features(self, geometry_data: Dict[str, Any],
shape: Optional[TopoDS_Shape] = None) -> List[Dict[str, Any]]:
"""检测BOSS柱特征"""
features = []
volume = geometry_data.get("volume", 0)
bbox = geometry_data.get("bounding_box", {})
dimensions = bbox.get("dimensions", [100, 100, 100])
volume_efficiency = volume / (dimensions[0] * dimensions[1] * dimensions[2])
if volume_efficiency < 0.3:
confidence = 0.65
if shape is not None:
confidence = 0.72
features.append(create_mold_feature(
feature_type="boss_feature",
confidence=confidence,
location=bbox.get("center", [50, 50, 50]),
dimensions=[6.0, 12.0, 6.0],
parameters={"volume_efficiency": volume_efficiency},
recommendations=[
"检测到可能的BOSS柱结构",
"建议BOSS柱外径为螺钉直径的2-2.5倍",
"BOSS柱高度不超过直径的2倍",
"增加拔模角度1-2度",
"根部增加圆角R0.5-R1.0"
]
))
return features
def _analyze_draft_angles(self, geometry_data: Dict[str, Any],
shape: Optional[TopoDS_Shape] = None) -> List[Dict[str, Any]]:
"""分析拔模角度"""
features = []
if shape is not None:
draft_result = self._compute_draft_angles_from_shape(shape)
if draft_result is not None:
min_draft = draft_result["min_draft_angle"]
max_draft = draft_result["max_draft_angle"]
undrafted_count = draft_result["undrafted_faces"]
total_side_faces = draft_result["total_side_faces"]
if undrafted_count > 0:
features.append(create_mold_feature(
feature_type="draft_angle",
confidence=0.90,
location=geometry_data.get("center_of_mass", [0, 0, 0]),
dimensions=[min_draft, max_draft, undrafted_count],
parameters={
"min_draft_angle": round(min_draft, 2),
"max_draft_angle": round(max_draft, 2),
"undrafted_faces": undrafted_count,
"total_side_faces": total_side_faces,
"estimation_method": "precise",
},
recommendations=[
f"检测到 {undrafted_count} 个面需要拔模(当前最小拔模角 {min_draft:.1f}°)",
"建议所有垂直面添加1-2度拔模角度",
"纹理表面需要3-5度拔模角度",
"深腔结构需要更大的拔模角度"
]
))
else:
features.append(create_mold_feature(
feature_type="draft_angle",
confidence=0.90,
location=geometry_data.get("center_of_mass", [0, 0, 0]),
dimensions=[min_draft, max_draft, 0],
parameters={
"min_draft_angle": round(min_draft, 2),
"max_draft_angle": round(max_draft, 2),
"undrafted_faces": 0,
"total_side_faces": total_side_faces,
"estimation_method": "precise",
},
recommendations=[
f"所有侧壁面已有拔模角(最小 {min_draft:.1f}°)",
"拔模角度满足要求"
]
))
return features
features.append(create_mold_feature(
feature_type="draft_angle",
confidence=0.8,
location=geometry_data.get("center_of_mass", [0, 0, 0]),
dimensions=[1.0, 2.0, 1.0],
parameters={"recommended_angle": 2.0, "estimation_method": "heuristic"},
recommendations=[
"建议所有垂直面添加1-2度拔模角度",
"纹理表面需要3-5度拔模角度",
"深腔结构需要更大的拔模角度"
]
))
return features
def _compute_draft_angles_from_shape(self, shape: TopoDS_Shape) -> Optional[Dict[str, Any]]:
"""基于面法向量分析计算各面的拔模角度"""
try:
from OCC.Core.TopExp import TopExp_Explorer
from OCC.Core.TopAbs import TopAbs_FACE
from OCC.Core.TopoDS import TopoDS_Face, topods
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface
from OCC.Core.BRepLProp import BRepLProp_SLProps
from OCC.Core.gp import gp_Dir
draft_direction = gp_Dir(0, 0, 1)
draft_angles = []
side_face_count = 0
undrafted_count = 0
explorer = TopExp_Explorer(shape, TopAbs_FACE)
while explorer.More():
face = topods.Face(explorer.Current())
surface = BRepAdaptor_Surface(face)
try:
u = (surface.FirstUParameter() + surface.LastUParameter()) / 2
v = (surface.FirstVParameter() + surface.LastVParameter()) / 2
normal = None
if surface.GetType() == 0:
normal = surface.Plane().Position().Direction()
else:
props = BRepLProp_SLProps(surface, 1, 0.001)
props.SetParameters(u, v)
if props.IsNormalDefined():
normal = props.Normal()
if normal is not None:
dot = abs(normal.Dot(draft_direction))
angle_from_vertical = math.degrees(math.acos(min(dot, 1.0)))
if 5.0 < angle_from_vertical < 85.0:
side_face_count += 1
draft_angle = 90.0 - angle_from_vertical
draft_angles.append(draft_angle)
if draft_angle < 0.5:
undrafted_count += 1
except Exception:
pass
explorer.Next()
if not draft_angles:
return None
return {
"min_draft_angle": min(draft_angles),
"max_draft_angle": max(draft_angles),
"avg_draft_angle": sum(draft_angles) / len(draft_angles),
"undrafted_faces": undrafted_count,
"total_side_faces": side_face_count,
}
except ImportError:
return None
except Exception as e:
logger.warning(f"拔模角度计算失败: {e}")
return None
def _detect_curvature_features(self, shape: TopoDS_Shape) -> List[Dict[str, Any]]:
"""检测高曲率区域(可能导致应力集中)"""
features = []
try:
from OCC.Core.TopExp import TopExp_Explorer
from OCC.Core.TopAbs import TopAbs_FACE
from OCC.Core.TopoDS import TopoDS_Face, topods
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface
from OCC.Core.BRepLProp import BRepLProp_SLProps
from OCC.Core.GProp import GProp_GProps
from OCC.Core.BRepGProp import brepgprop
high_curvature_count = 0
max_curvature_overall = 0.0
explorer = TopExp_Explorer(shape, TopAbs_FACE)
while explorer.More():
face = topods.Face(explorer.Current())
surface = BRepAdaptor_Surface(face)
if surface.GetType() == 0:
explorer.Next()
continue
try:
props = GProp_GProps()
brepgprop.SurfaceProperties(face, props)
face_area = props.Mass()
u_range = (surface.FirstUParameter(), surface.LastUParameter())
v_range = (surface.FirstVParameter(), surface.LastVParameter())
max_curvature = 0.0
sample_count = 5
for ui in range(sample_count):
for vi in range(sample_count):
u = u_range[0] + (u_range[1] - u_range[0]) * (ui + 0.5) / sample_count
v = v_range[0] + (v_range[1] - v_range[0]) * (vi + 0.5) / sample_count
try:
lprops = BRepLProp_SLProps(surface, 2, 0.001)
lprops.SetParameters(u, v)
if lprops.IsCurvatureDefined():
k1 = abs(lprops.MinCurvature())
k2 = abs(lprops.MaxCurvature())
max_curvature = max(max_curvature, k1, k2)
except Exception:
continue
if max_curvature > max_curvature_overall:
max_curvature_overall = max_curvature
if max_curvature > 0.5:
high_curvature_count += 1
except Exception:
pass
explorer.Next()
if high_curvature_count > 0:
risk_level = "high" if high_curvature_count > 5 else "medium"
features.append(create_mold_feature(
feature_type="high_curvature",
confidence=0.82,
location=[0, 0, 0],
dimensions=[high_curvature_count, max_curvature_overall, 0],
parameters={
"high_curvature_faces": high_curvature_count,
"max_curvature": round(max_curvature_overall, 4),
"risk_level": risk_level,
},
recommendations=[
f"检测到 {high_curvature_count} 个高曲率区域",
"高曲率区域可能导致应力集中和填充困难",
"建议增加圆角半径以降低曲率",
"注意这些区域的冷却设计"
]
))
except ImportError:
logger.debug("pythonOCC 不可用,跳过曲率检测")
except Exception as e:
logger.warning(f"曲率检测失败: {e}")
return features
def _detect_fillet_features(self, shape: TopoDS_Shape) -> List[Dict[str, Any]]:
"""检测圆角/倒角特征"""
features = []
try:
from OCC.Core.TopExp import TopExp_Explorer
from OCC.Core.TopAbs import TopAbs_EDGE
from OCC.Core.TopoDS import TopoDS_Edge, topods
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface, BRepAdaptor_Curve
fillet_count = 0
small_fillet_count = 0
min_fillet_radius = float('inf')
radii = []
edge_explorer = TopExp_Explorer(shape, TopAbs_EDGE)
while edge_explorer.More():
edge = topods.Edge(edge_explorer.Current())
try:
curve = BRepAdaptor_Curve(edge)
curve_type = curve.GetType()
if curve_type == 2: # GeomAbs_Circle
circle = curve.Circle()
radius = circle.Radius()
if 0.05 < radius < 50:
fillet_count += 1
radii.append(radius)
if radius < min_fillet_radius:
min_fillet_radius = radius
if radius < 0.5:
small_fillet_count += 1
except Exception:
pass
edge_explorer.Next()
if fillet_count > 0:
avg_radius = sum(radii) / len(radii)
features.append(create_mold_feature(
feature_type="fillet",
confidence=0.85,
location=[0, 0, 0],
dimensions=[min_fillet_radius, avg_radius, max(radii)],
parameters={
"fillet_count": fillet_count,
"min_radius": round(min_fillet_radius, 3),
"max_radius": round(max(radii), 3),
"avg_radius": round(avg_radius, 3),
"small_fillet_count": small_fillet_count,
},
recommendations=[
f"检测到 {fillet_count} 个圆角特征" +
(f",其中 {small_fillet_count} 个半径过小" if small_fillet_count > 0 else ""),
"小圆角(R<0.5mm)可能导致应力集中" if small_fillet_count > 0 else "",
"建议圆角半径不小于 0.5mm" if small_fillet_count > 0 else "",
] if small_fillet_count > 0 else [
f"检测到 {fillet_count} 个圆角特征",
"圆角半径范围合理"
]
))
except ImportError:
logger.debug("pythonOCC 不可用,跳过圆角检测")
except Exception as e:
logger.warning(f"圆角检测失败: {e}")
return features
def _generate_recommendations(self, geometry_data: Dict[str, Any],
features: List[Dict[str, Any]],
material: str) -> List[Dict[str, Any]]:
"""生成设计建议"""
recommendations = []
wall_rec = self._get_wall_thickness_recommendation(geometry_data, material, features)
if wall_rec:
recommendations.append(wall_rec)
recommendations.append(create_design_recommendation(
rec_type="draft_angle",
priority="high",
description="添加拔模角度",
parameters={"min_angle": 1.0, "preferred_angle": 2.0},
reason="确保顺利脱模"
))
for feature in features:
if feature["feature_type"] == "thin_wall":
params = feature.get("parameters", {})
current = params.get("min_thickness", params.get("average_thickness", 0))
rec = create_design_recommendation(
rec_type="wall_thickness",
priority="high",
description="增加壁厚",
parameters={
"current": current,
"recommended": self.feature_thresholds["thin_wall"]
},
reason="壁厚不足影响结构强度"
)
recommendations.append(rec)
elif feature["feature_type"] == "high_curvature":
recommendations.append(create_design_recommendation(
rec_type="curvature",
priority="medium",
description="优化高曲率区域",
parameters={"max_curvature": feature["parameters"].get("max_curvature", 0)},
reason="高曲率区域可能导致应力集中"
))
elif feature["feature_type"] == "fillet" and feature["parameters"].get("small_fillet_count", 0) > 0:
recommendations.append(create_design_recommendation(
rec_type="fillet",
priority="medium",
description="增大过小圆角半径",
parameters={"min_radius": feature["parameters"].get("min_radius", 0)},
reason="小圆角导致应力集中和加工困难"
))
return recommendations
def _get_wall_thickness_recommendation(self, geometry_data: Dict[str, Any],
material: str,
features: List[Dict[str, Any]] = None) -> Dict[str, Any]:
"""获取壁厚建议"""
avg_thickness = None
if features:
for f in features:
if f["feature_type"] in ("thin_wall", "thick_wall"):
params = f.get("parameters", {})
avg_thickness = params.get("avg_thickness", params.get("average_thickness"))
break
if avg_thickness is None:
volume = geometry_data.get("volume", 0)
surface_area = geometry_data.get("surface_area", 0)
if volume > 0 and surface_area > 0:
avg_thickness = (volume / surface_area) * 0.6
if avg_thickness is not None:
material_props = self.product_materials.get(material, self.product_materials["ABS"])
min_wall = material_props["min_wall"]
if avg_thickness < min_wall:
return create_design_recommendation(
rec_type="wall_thickness",
priority="high",
description=f"增加壁厚至{min_wall}mm以上",
parameters={"current": avg_thickness, "recommended": min_wall},
reason=f"{material}材料最小壁厚要求"
)
return None
def _calculate_quality_metrics(self, geometry_data: Dict[str, Any],
features: List[Dict[str, Any]]) -> Dict[str, float]:
"""计算质量指标"""
metrics = {}
bbox = geometry_data.get("bounding_box", {})
dimensions = bbox.get("dimensions", [100, 100, 100])
volume = geometry_data.get("volume", 0)
bbox_volume = dimensions[0] * dimensions[1] * dimensions[2]
metrics["volume_utilization"] = volume / bbox_volume if bbox_volume > 0 else 0
topology = geometry_data.get("topology", {})
face_count = topology.get("faces", 0)
metrics["topology_complexity"] = face_count / 100.0
wall_uniformity = 0.5
for f in features:
if f["feature_type"] in ("thin_wall", "thick_wall", "wall_non_uniform"):
params = f.get("parameters", {})
if "uniformity_ratio" in params:
wall_uniformity = params["uniformity_ratio"]
break
min_t = params.get("min_thickness", params.get("average_thickness", 0))
max_t = params.get("max_thickness", params.get("average_thickness", 0))
if min_t > 0 and max_t > 0:
wall_uniformity = min_t / max_t
break
if wall_uniformity == 0.5:
surface_area = geometry_data.get("surface_area", 0)
if volume > 0 and surface_area > 0:
thickness_ratio = (volume / surface_area) * 0.6
ideal_thickness = 3.0
wall_uniformity = 1.0 - abs(thickness_ratio - ideal_thickness) / ideal_thickness
metrics["wall_uniformity"] = max(0, min(1, wall_uniformity))
draft_score = 1.0
for f in features:
if f["feature_type"] == "draft_angle":
params = f.get("parameters", {})
undrafted = params.get("undrafted_faces", None)
total = params.get("total_side_faces", 1)
if undrafted is not None and total > 0:
draft_score = 1.0 - (undrafted / total)
break
metrics["draft_score"] = round(draft_score, 3)
return metrics
def _generate_analysis_summary(self, geometry_data: Dict[str, Any],
features: List[Dict[str, Any]],
recommendations: List[Dict[str, Any]]) -> str:
"""生成分析摘要"""
volume = geometry_data.get("volume", 0)
high_priority_recs = len([r for r in recommendations if r["priority"] == "high"])
summary_parts = []
if volume > 0:
summary_parts.append(f"模型体积: {volume / 1000:.1f} cm³")
if features:
feature_types = set(f["feature_type"] for f in features)
summary_parts.append(f"检测到 {len(feature_types)} 类特征")
precise_features = [f for f in features if f.get("parameters", {}).get("estimation_method") == "precise"]
if precise_features:
summary_parts.append(f"其中 {len(precise_features)} 个特征为精确检测")
if high_priority_recs > 0:
summary_parts.append(f"有 {high_priority_recs} 个高优先级建议")
return " | ".join(summary_parts) if summary_parts else "分析完成"
+241
View File
@@ -0,0 +1,241 @@
# src/core/mesh_generator.py
import logging
import numpy as np
from typing import Dict, List, Optional, Any
import trimesh
from trimesh import sample as trimesh_sample
from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh
from OCC.Core.TopExp import TopExp_Explorer
from OCC.Core.TopAbs import TopAbs_FACE
from OCC.Core.BRep import BRep_Tool
from OCC.Core.TopoDS import TopoDS_Shape
from OCC.Core.TopLoc import TopLoc_Location
logger = logging.getLogger(__name__)
class MeshGenerator:
"""网格生成器 - 从PythonOCC形状生成点云,支持多级LOD"""
def __init__(self, quality: str = "medium"):
self.quality_settings = {
"low": 1.0,
"medium": 0.3,
"high": 0.1
}
self.quality = self.quality_settings.get(quality, 0.3)
def generate_mesh_from_shape(self, shape: TopoDS_Shape, num_points: int = 20000) -> Dict:
"""从PythonOCC形状生成点云数据"""
try:
mesh = BRepMesh_IncrementalMesh(shape, self.quality, False, 0.5, True)
mesh.Perform()
logger.info(f"OCC网格生成完成, 网格状态: {mesh.IsDone()}")
all_vertices = []
all_faces = []
vertex_offset = 0
explorer = TopExp_Explorer(shape, TopAbs_FACE)
face_count = 0
while explorer.More():
face = explorer.Current()
face_count += 1
location = TopLoc_Location()
face_triangulation = BRep_Tool.Triangulation(face, location)
if face_triangulation is None:
logger.warning(f"面 {face_count} 没有三角剖分数据")
explorer.Next()
continue
trsf = location.Transformation()
nb_nodes = face_triangulation.NbNodes()
nb_triangles = face_triangulation.NbTriangles()
logger.debug(f"面 {face_count}: {nb_nodes} 个顶点, {nb_triangles} 个三角形")
face_vertices = []
for i in range(1, nb_nodes + 1):
pnt = face_triangulation.Node(i)
transformed = pnt.Transformed(trsf)
face_vertices.append([
float(transformed.X()),
float(transformed.Y()),
float(transformed.Z()),
])
face_indices = []
for i in range(1, nb_triangles + 1):
tri = face_triangulation.Triangle(i)
idx1 = tri.Value(1)
idx2 = tri.Value(2)
idx3 = tri.Value(3)
face_indices.append([
vertex_offset + idx1 - 1,
vertex_offset + idx2 - 1,
vertex_offset + idx3 - 1
])
all_vertices.extend(face_vertices)
all_faces.extend(face_indices)
vertex_offset += len(face_vertices)
explorer.Next()
if len(all_vertices) == 0:
logger.warning("未提取到任何顶点,使用示例数据")
return self._create_sample_pointcloud()
vertices = np.array(all_vertices, dtype=np.float32)
faces = np.array(all_faces, dtype=np.int32)
logger.info(f"总共提取了 {len(vertices)} 个顶点, {len(faces)} 个三角形面, {face_count} 个面")
tri_mesh = trimesh.Trimesh(vertices=vertices, faces=faces, process=True)
actual_num_points = min(num_points, len(faces) * 2)
logger.info(f"采样点数: {actual_num_points}")
points, face_idx = trimesh.sample.sample_surface(tri_mesh, actual_num_points)
normals = tri_mesh.face_normals[face_idx]
logger.info(f"生成了 {len(points)} 个点云点")
return {
"vertices": vertices.tolist(),
"faces": faces.tolist(),
"points": points.tolist(),
"normals": normals.tolist(),
"point_count": int(len(points)),
"vertex_count": int(len(vertices)),
"face_count": int(len(faces))
}
except Exception as e:
logger.error(f"网格生成失败: {e}")
import traceback
logger.error(traceback.format_exc())
return self._create_sample_pointcloud()
def generate_multi_lod_mesh(self, shape: TopoDS_Shape) -> Dict:
"""生成多级LOD网格 - 一次OCC剖分,trimesh简化,避免重复计算
返回结构:
{
"lods": {
"0": { "vertices": [...], "faces": [...], "vertex_count": N, "face_count": N },
"1": { ... 50%简化 ... },
"2": { ... 80%简化 ... }
},
"points": [...], "normals": [...], "point_count": N,
"vertex_count": N, "face_count": N
}
"""
try:
full_mesh_result = self.generate_mesh_from_shape(shape, num_points=20000)
vertices = np.array(full_mesh_result["vertices"], dtype=np.float32)
faces = np.array(full_mesh_result["faces"], dtype=np.int32)
if len(vertices) == 0 or len(faces) == 0:
sample = self._create_sample_pointcloud()
return self._wrap_sample_as_lod(sample)
tri_mesh = trimesh.Trimesh(vertices=vertices, faces=faces, process=True)
full_face_count = len(tri_mesh.faces)
logger.info(f"全精度网格: {len(tri_mesh.vertices)} 顶点, {full_face_count} 面")
lods = {
"0": self._mesh_to_lod_entry(tri_mesh, "LOD0-全精度")
}
lod_ratios = {"1": 0.50, "2": 0.20}
for lod_level, ratio in lod_ratios.items():
if full_face_count < 300:
lods[lod_level] = lods["0"]
continue
target_faces = max(int(full_face_count * ratio), 200)
try:
simplified = tri_mesh.simplify_quadric_decimation(target_faces)
if simplified is None or len(simplified.faces) < 3:
simplified = self._fast_decimate(tri_mesh, target_faces)
lods[lod_level] = self._mesh_to_lod_entry(simplified, f"LOD{lod_level}-简化{int((1-ratio)*100)}%")
logger.info(f"LOD{lod_level}: {len(simplified.vertices)} 顶点, {len(simplified.faces)} 面 (目标{target_faces})")
except Exception as dec_err:
logger.warning(f"LOD{lod_level} 简化失败,回退到全精度: {dec_err}")
lods[lod_level] = lods["0"]
result = {
"lods": lods,
"points": full_mesh_result["points"],
"normals": full_mesh_result["normals"],
"point_count": full_mesh_result["point_count"],
"vertex_count": full_mesh_result["vertex_count"],
"face_count": full_mesh_result["face_count"],
}
return result
except Exception as e:
logger.error(f"多级LOD网格生成失败: {e}")
import traceback
logger.error(traceback.format_exc())
sample = self._create_sample_pointcloud()
return self._wrap_sample_as_lod(sample)
def _mesh_to_lod_entry(self, mesh: trimesh.Trimesh, label: str) -> Dict:
return {
"vertices": mesh.vertices.tolist(),
"faces": mesh.faces.tolist(),
"vertex_count": int(len(mesh.vertices)),
"face_count": int(len(mesh.faces)),
}
def _fast_decimate(self, mesh: trimesh.Trimesh, target_faces: int) -> trimesh.Trimesh:
"""快速回退降采样:按面索引均匀采样"""
if target_faces >= len(mesh.faces):
return mesh
step = max(len(mesh.faces) // target_faces, 1)
indices = np.arange(0, len(mesh.faces), step)[:target_faces]
return mesh.submesh([np.array(indices)], only_watertight=False, append=True)
def _wrap_sample_as_lod(self, sample: Dict) -> Dict:
lods = {
"0": {
"vertices": sample["vertices"],
"faces": sample["faces"],
"vertex_count": sample["vertex_count"],
"face_count": sample["face_count"],
}
}
lods["1"] = lods["0"]
lods["2"] = lods["0"]
return {
"lods": lods,
"points": sample["points"],
"normals": sample["normals"],
"point_count": sample["point_count"],
"vertex_count": sample["vertex_count"],
"face_count": sample["face_count"],
}
def _create_sample_pointcloud(self) -> Dict:
"""创建示例点云(备用)"""
mesh = trimesh.creation.box([100, 80, 50])
points, _ = trimesh.sample.sample_surface(mesh, 5000)
normals = mesh.face_normals[:len(points)]
return {
"vertices": mesh.vertices.tolist(),
"faces": mesh.faces.tolist(),
"points": points.tolist(),
"normals": normals.tolist(),
"point_count": len(points),
"vertex_count": len(mesh.vertices),
"face_count": len(mesh.faces)
}
+646
View File
@@ -0,0 +1,646 @@
"""
模具刀路设计与G代码生成模块
架构:
1. ToolLibrary - 刀具库与切削参数管理
2. CuttingParamsCalculator - 切削参数自动计算
3. RoughingToolpathGenerator - 粗加工刀路生成
4. FinishingToolpathGenerator - 精加工刀路生成
5. GCodePostProcessor - G代码后处理器
6. MoldCAMDesigner - 模具CAM综合设计器
加工策略:
- 粗加工:Z层等高粗加工(自适应清根)
- 半精加工:等高线铣削
- 精加工:平行铣削/螺旋铣削/等高线精加工
- 清角:笔式清角
- 钻孔:冷却水路/顶针孔/螺丝孔
"""
from typing import Dict, List, Any, Optional, Tuple
import math
from shared.utils.logger import get_logger
logger = get_logger(__name__)
class ToolLibrary:
"""刀具库"""
TOOLS = {
"endmill_20mm": {
"type": "endmill", "diameter": 20.0, "flute_length": 60.0,
"cutting_edges": 4, "material": "carbide",
"corner_radius": 0.0,
"speeds_feeds": {
"cutting_speed": 100, "feed_per_tooth": 0.15,
"axial_depth": 10.0, "radial_depth": 15.0
}
},
"endmill_16mm": {
"type": "endmill", "diameter": 16.0, "flute_length": 50.0,
"cutting_edges": 4, "material": "carbide",
"corner_radius": 0.0,
"speeds_feeds": {
"cutting_speed": 120, "feed_per_tooth": 0.12,
"axial_depth": 8.0, "radial_depth": 12.0
}
},
"endmill_10mm": {
"type": "endmill", "diameter": 10.0, "flute_length": 35.0,
"cutting_edges": 4, "material": "carbide",
"corner_radius": 0.0,
"speeds_feeds": {
"cutting_speed": 130, "feed_per_tooth": 0.10,
"axial_depth": 5.0, "radial_depth": 8.0
}
},
"endmill_6mm": {
"type": "endmill", "diameter": 6.0, "flute_length": 22.0,
"cutting_edges": 3, "material": "carbide",
"corner_radius": 0.0,
"speeds_feeds": {
"cutting_speed": 140, "feed_per_tooth": 0.06,
"axial_depth": 3.0, "radial_depth": 4.0
}
},
"ballnose_10mm": {
"type": "ballnose", "diameter": 10.0, "flute_length": 30.0,
"cutting_edges": 2, "material": "carbide",
"corner_radius": 5.0,
"speeds_feeds": {
"cutting_speed": 150, "feed_per_tooth": 0.08,
"axial_depth": 0.5, "radial_depth": 1.0
}
},
"ballnose_6mm": {
"type": "ballnose", "diameter": 6.0, "flute_length": 22.0,
"cutting_edges": 2, "material": "carbide",
"corner_radius": 3.0,
"speeds_feeds": {
"cutting_speed": 160, "feed_per_tooth": 0.06,
"axial_depth": 0.3, "radial_depth": 0.5
}
},
"ballnose_3mm": {
"type": "ballnose", "diameter": 3.0, "flute_length": 12.0,
"cutting_edges": 2, "material": "carbide",
"corner_radius": 1.5,
"speeds_feeds": {
"cutting_speed": 180, "feed_per_tooth": 0.03,
"axial_depth": 0.15, "radial_depth": 0.3
}
},
"ballnose_1mm": {
"type": "ballnose", "diameter": 1.0, "flute_length": 5.0,
"cutting_edges": 2, "material": "carbide",
"corner_radius": 0.5,
"speeds_feeds": {
"cutting_speed": 200, "feed_per_tooth": 0.01,
"axial_depth": 0.05, "radial_depth": 0.1
}
},
"drill_8mm": {
"type": "drill", "diameter": 8.0, "flute_length": 50.0,
"cutting_edges": 2, "material": "carbide",
"corner_radius": 0.0,
"speeds_feeds": {
"cutting_speed": 80, "feed_per_tooth": 0.10,
"axial_depth": 50.0, "radial_depth": 0.0
}
},
"drill_5mm": {
"type": "drill", "diameter": 5.0, "flute_length": 35.0,
"cutting_edges": 2, "material": "carbide",
"corner_radius": 0.0,
"speeds_feeds": {
"cutting_speed": 90, "feed_per_tooth": 0.08,
"axial_depth": 35.0, "radial_depth": 0.0
}
},
}
MOLD_STEEL = {
"P20": {"hardness_hrc": 30, "cutting_speed_factor": 1.0, "feed_factor": 1.0},
"718H": {"hardness_hrc": 35, "cutting_speed_factor": 0.85, "feed_factor": 0.9},
"NAK80": {"hardness_hrc": 38, "cutting_speed_factor": 0.75, "feed_factor": 0.85},
"S136": {"hardness_hrc": 50, "cutting_speed_factor": 0.5, "feed_factor": 0.7},
"H13": {"hardness_hrc": 48, "cutting_speed_factor": 0.55, "feed_factor": 0.75},
"Al7075": {"hardness_hrc": 15, "cutting_speed_factor": 2.0, "feed_factor": 1.5},
}
@classmethod
def get_tool(cls, tool_id: str) -> Optional[Dict]:
return cls.TOOLS.get(tool_id)
@classmethod
def select_roughing_tool(cls, cavity_volume_mm3: float,
min_corner_radius: float = 0.0,
steel: str = "P20") -> Dict:
"""根据型腔体积和最小圆角选择粗加工刀具"""
if cavity_volume_mm3 > 500000:
tool_id = "endmill_20mm"
elif cavity_volume_mm3 > 100000:
tool_id = "endmill_16mm"
elif cavity_volume_mm3 > 20000:
tool_id = "endmill_10mm"
else:
tool_id = "endmill_6mm"
tool = cls.TOOLS[tool_id].copy()
steel_props = cls.MOLD_STEEL.get(steel, cls.MOLD_STEEL["P20"])
tool["speeds_feeds"] = cls._adjust_for_steel(tool["speeds_feeds"], steel_props)
tool["tool_id"] = tool_id
return tool
@classmethod
def select_finishing_tool(cls, surface_quality: str = "standard",
min_corner_radius: float = 0.0,
steel: str = "P20") -> Dict:
"""根据表面质量要求选择精加工刀具"""
if surface_quality == "mirror":
tool_id = "ballnose_3mm" if min_corner_radius <= 3 else "ballnose_6mm"
elif surface_quality == "fine":
tool_id = "ballnose_6mm" if min_corner_radius <= 6 else "ballnose_10mm"
else:
tool_id = "ballnose_10mm"
tool = cls.TOOLS[tool_id].copy()
steel_props = cls.MOLD_STEEL.get(steel, cls.MOLD_STEEL["P20"])
tool["speeds_feeds"] = cls._adjust_for_steel(tool["speeds_feeds"], steel_props)
tool["tool_id"] = tool_id
return tool
@classmethod
def _adjust_for_steel(cls, speeds_feeds: Dict, steel_props: Dict) -> Dict:
"""根据模具钢调整切削参数"""
adjusted = speeds_feeds.copy()
adjusted["cutting_speed"] *= steel_props["cutting_speed_factor"]
adjusted["feed_per_tooth"] *= steel_props["feed_factor"]
adjusted["axial_depth"] *= steel_props["feed_factor"]
adjusted["radial_depth"] *= steel_props["feed_factor"]
return adjusted
class CuttingParamsCalculator:
"""切削参数计算器"""
@staticmethod
def calculate_spindle_speed(cutting_speed_m_min: float, tool_diameter: float) -> int:
"""N = (1000 × Vc) / (π × D)"""
if tool_diameter <= 0:
return 1000
rpm = (1000 * cutting_speed_m_min) / (math.pi * tool_diameter)
return int(min(max(rpm, 500), 24000))
@staticmethod
def calculate_feed_rate(spindle_speed: int, feed_per_tooth: float,
cutting_edges: int) -> float:
"""F = N × fz × z"""
return spindle_speed * feed_per_tooth * cutting_edges
@staticmethod
def calculate_mrr(feed_rate: float, axial_depth: float,
radial_depth: float) -> float:
"""材料去除率 Q = ae × ap × F / 1000 (cm³/min)"""
return axial_depth * radial_depth * feed_rate / 1000
@staticmethod
def estimate_machining_time(toolpath_length: float, feed_rate: float,
rapid_distance: float = 0,
rapid_speed: float = 15000) -> float:
"""估算加工时间(分钟)"""
cutting_time = toolpath_length / feed_rate / 60 if feed_rate > 0 else 0
rapid_time = rapid_distance / rapid_speed / 60 if rapid_speed > 0 else 0
return cutting_time + rapid_time
@classmethod
def calculate_all(cls, tool: Dict) -> Dict:
"""计算完整切削参数"""
sf = tool["speeds_feeds"]
rpm = cls.calculate_spindle_speed(sf["cutting_speed"], tool["diameter"])
feed = cls.calculate_feed_rate(rpm, sf["feed_per_tooth"], tool["cutting_edges"])
mrr = cls.calculate_mrr(feed, sf["axial_depth"], sf["radial_depth"])
return {
"tool_id": tool.get("tool_id", "unknown"),
"tool_type": tool["type"],
"tool_diameter": tool["diameter"],
"spindle_speed_rpm": rpm,
"feed_rate_mm_min": round(feed, 1),
"axial_depth_mm": sf["axial_depth"],
"radial_depth_mm": sf["radial_depth"],
"material_removal_rate_cm3_min": round(mrr, 2),
"cutting_speed_m_min": round(sf["cutting_speed"], 1),
}
class RoughingToolpathGenerator:
"""粗加工刀路生成器"""
def generate_z_level_roughing(self, stock_bbox: Dict, cavity_bbox: Dict,
tool: Dict, cutting_params: Dict,
stock_allowance: float = 0.5) -> Dict[str, Any]:
"""
Z层等高粗加工
策略:从顶面逐层向下铣削,每层切深为 axial_depth
Args:
stock_bbox: 毛坯边界框
cavity_bbox: 型腔边界框
tool: 刀具参数
cutting_params: 切削参数
stock_allowance: 精加工余量 mm
Returns:
粗加工刀路方案
"""
z_min = cavity_bbox.get("min", [0, 0, 0])[2]
z_max = cavity_bbox.get("max", [0, 0, 0])[2]
total_depth = z_max - z_min
axial_depth = cutting_params["axial_depth_mm"]
num_levels = max(1, math.ceil(total_depth / axial_depth))
actual_depth = total_depth / num_levels
stepover = cutting_params["radial_depth_mm"]
levels = []
for i in range(num_levels):
z_level = z_max - (i + 1) * actual_depth + stock_allowance
levels.append({
"z": round(z_level, 2),
"depth": round(actual_depth, 2),
"level_index": i + 1,
})
toolpath_length = self._estimate_roughing_length(
cavity_bbox, num_levels, stepover
)
machining_time = CuttingParamsCalculator.estimate_machining_time(
toolpath_length, cutting_params["feed_rate_mm_min"]
)
return {
"strategy": "z_level_roughing",
"tool": cutting_params,
"levels": levels,
"num_levels": num_levels,
"stepover": stepover,
"stock_allowance": stock_allowance,
"total_depth": round(total_depth, 2),
"estimated_toolpath_length": round(toolpath_length, 1),
"estimated_time_min": round(machining_time, 1),
"approach_type": "helical_ramp",
"ramp_angle": 2.0,
}
def _estimate_roughing_length(self, cavity_bbox: Dict, num_levels: int,
stepover: float) -> float:
"""估算粗加工刀路总长度"""
dims = cavity_bbox.get("dimensions", [100, 100, 50])
width = dims[0]
length = dims[1]
passes_per_level = max(1, int(width / stepover))
length_per_pass = length
length_per_level = passes_per_level * length_per_pass * 1.1
return length_per_level * num_levels
class FinishingToolpathGenerator:
"""精加工刀路生成器"""
def generate_parallel_finishing(self, cavity_bbox: Dict, tool: Dict,
cutting_params: Dict,
stepover: float = 0.3,
angle: float = 0.0) -> Dict[str, Any]:
"""
平行铣削精加工
Args:
cavity_bbox: 型腔边界框
tool: 刀具参数
cutting_params: 切削参数
stepover: 步距 mm
angle: 加工角度
Returns:
精加工刀路方案
"""
dims = cavity_bbox.get("dimensions", [100, 100, 50])
width = dims[0]
length = dims[1]
num_passes = max(1, int(width / stepover) + 1)
surface_roughness = self._estimate_surface_roughness(
tool["diameter"], stepover
)
toolpath_length = num_passes * length * 1.05
machining_time = CuttingParamsCalculator.estimate_machining_time(
toolpath_length, cutting_params["feed_rate_mm_min"]
)
return {
"strategy": "parallel_finishing",
"tool": cutting_params,
"stepover": stepover,
"angle": angle,
"num_passes": num_passes,
"surface_roughness_ra": round(surface_roughness, 3),
"estimated_toolpath_length": round(toolpath_length, 1),
"estimated_time_min": round(machining_time, 1),
"cutting_direction": "one_way",
"stepover_type": "scallop",
}
def generate_contour_finishing(self, cavity_bbox: Dict, tool: Dict,
cutting_params: Dict,
z_step: float = 0.5) -> Dict[str, Any]:
"""
等高线精加工
Args:
cavity_bbox: 型腔边界框
tool: 刀具参数
cutting_params: 切削参数
z_step: Z方向步距 mm
Returns:
等高线精加工方案
"""
z_min = cavity_bbox.get("min", [0, 0, 0])[2]
z_max = cavity_bbox.get("max", [0, 0, 0])[2]
total_depth = z_max - z_min
num_levels = max(1, int(total_depth / z_step) + 1)
dims = cavity_bbox.get("dimensions", [100, 100, 50])
perimeter = 2 * (dims[0] + dims[1])
toolpath_length = num_levels * perimeter * 1.1
machining_time = CuttingParamsCalculator.estimate_machining_time(
toolpath_length, cutting_params["feed_rate_mm_min"]
)
return {
"strategy": "contour_finishing",
"tool": cutting_params,
"z_step": z_step,
"num_levels": num_levels,
"estimated_toolpath_length": round(toolpath_length, 1),
"estimated_time_min": round(machining_time, 1),
}
def _estimate_surface_roughness(self, tool_diameter: float,
stepover: float) -> float:
"""估算表面粗糙度 Ra"""
if tool_diameter <= 0:
return 1.0
r = tool_diameter / 2
h = stepover ** 2 / (8 * r) if r > 0 else stepover
return h * 0.25
class GCodePostProcessor:
"""G代码后处理器"""
def __init__(self, controller: str = "fanuc"):
self.controller = controller
self.dialects = {
"fanuc": {
"rapid": "G00", "linear": "G01",
"cw_arc": "G02", "ccw_arc": "G03",
"absolute": "G90", "incremental": "G91",
"tool_change": "M06", "spindle_on": "M03",
"spindle_off": "M05", "coolant_on": "M08",
"coolant_off": "M09", "program_end": "M30",
"length_comp": "G43", "xy_plane": "G17",
"cancel_comp": "G40", "cancel_canned": "G80",
},
"siemens": {
"rapid": "G00", "linear": "G01",
"cw_arc": "G02", "ccw_arc": "G03",
"absolute": "G90", "incremental": "G91",
"tool_change": "M06", "spindle_on": "M03",
"spindle_off": "M05", "coolant_on": "M08",
"coolant_off": "M09", "program_end": "M30",
"length_comp": "G43", "xy_plane": "G17",
"cancel_comp": "G40", "cancel_canned": "G80",
},
}
def generate_gcode(self, operations: List[Dict],
program_number: int = 1000,
program_name: str = "MOLD_CAVITY") -> str:
"""
生成完整G代码程序
Args:
operations: 加工操作列表
program_number: 程序号
program_name: 程序名
Returns:
G代码字符串
"""
d = self.dialects.get(self.controller, self.dialects["fanuc"])
lines = []
lines.append(f"%")
lines.append(f"O{program_number} ({program_name})")
lines.append(f"{d['xy_plane']} {d['cancel_comp']} {d['cancel_canned']} {d['absolute']}")
lines.append(f"G54")
lines.append("")
for op_idx, op in enumerate(operations):
strategy = op.get("strategy", "unknown")
tool_info = op.get("tool", {})
tool_id = tool_info.get("tool_id", "T01")
tool_num = op_idx + 1
lines.append(f"(=== 操作 {tool_num}: {strategy} ===)")
tool_type = tool_info.get("tool_type", "endmill")
tool_dia = tool_info.get("tool_diameter", 10)
lines.append(f"(刀具: {tool_type} D{tool_dia:.1f}mm)")
lines.append(f"T{tool_num:02d} {d['tool_change']}")
lines.append(f"{d['length_comp']} H{tool_num:02d} Z100.0")
rpm = tool_info.get("spindle_speed_rpm", 3000)
lines.append(f"S{rpm} {d['spindle_on']}")
lines.append(f"{d['rapid']} X0 Y0 Z10.0")
lines.append(f"{d['coolant_on']}")
lines.append("")
feed = tool_info.get("feed_rate_mm_min", 500)
levels = op.get("levels", [])
if strategy == "z_level_roughing" and levels:
for level in levels:
z = level["z"]
lines.append(f"(--- Z层 {level['level_index']}: Z={z:.2f} ---)")
lines.append(f"{d['linear']} Z{z:.2f} F{int(feed * 0.5)}")
lines.append(f"{d['linear']} X50.0 Y30.0 F{feed}")
lines.append(f"{d['linear']} X-50.0 Y30.0")
lines.append(f"{d['linear']} X-50.0 Y-30.0")
lines.append(f"{d['linear']} X50.0 Y-30.0")
lines.append(f"{d['rapid']} Z10.0")
lines.append("")
elif strategy in ("parallel_finishing", "contour_finishing"):
num_passes = op.get("num_passes", 10)
stepover = op.get("stepover", 0.3)
for i in range(num_passes):
y = i * stepover - 30
lines.append(f"{d['linear']} Z-5.0 F{int(feed * 0.3)}")
lines.append(f"{d['linear']} X50.0 Y{y:.2f} F{feed}")
lines.append(f"{d['linear']} X-50.0 Y{y:.2f}")
lines.append(f"{d['rapid']} Z5.0")
lines.append("")
else:
lines.append(f"(策略 {strategy} 的刀路数据)")
lines.append("")
lines.append(f"{d['coolant_off']}")
lines.append(f"{d['spindle_off']}")
lines.append(f"{d['rapid']} Z100.0")
lines.append("")
lines.append(f"{d['coolant_off']}")
lines.append(f"{d['spindle_off']}")
lines.append(f"G28 G91 Z0")
lines.append(f"G28 G91 X0 Y0")
lines.append(f"{d['program_end']}")
lines.append(f"%")
return "\n".join(lines)
class MoldCAMDesigner:
"""模具CAM综合设计器"""
def __init__(self):
self.tool_lib = ToolLibrary()
self.params_calc = CuttingParamsCalculator()
self.roughing_gen = RoughingToolpathGenerator()
self.finishing_gen = FinishingToolpathGenerator()
self.post_processor = GCodePostProcessor()
def design_mold_cam(self, cavity_bbox: Dict, stock_bbox: Dict,
mold_steel: str = "P20",
surface_quality: str = "standard",
controller: str = "fanuc",
program_number: int = 1000) -> Dict[str, Any]:
"""
综合设计模具CAM方案
Args:
cavity_bbox: 型腔边界框
stock_bbox: 毛坯边界框
mold_steel: 模具钢材料
surface_quality: 表面质量要求
controller: 数控系统
program_number: 程序号
Returns:
完整的CAM方案
"""
logger.info(f"开始模具CAM设计: 钢材={mold_steel}, 质量={surface_quality}")
roughing_tool = ToolLibrary.select_roughing_tool(
self._estimate_cavity_volume(cavity_bbox),
steel=mold_steel
)
roughing_params = CuttingParamsCalculator.calculate_all(roughing_tool)
finishing_tool = ToolLibrary.select_finishing_tool(
surface_quality=surface_quality,
steel=mold_steel
)
finishing_params = CuttingParamsCalculator.calculate_all(finishing_tool)
roughing_op = self.roughing_gen.generate_z_level_roughing(
stock_bbox, cavity_bbox, roughing_tool, roughing_params
)
finishing_op = self.finishing_gen.generate_parallel_finishing(
cavity_bbox, finishing_tool, finishing_params
)
operations = [roughing_op, finishing_op]
gcode = self.post_processor.generate_gcode(
operations, program_number=program_number
)
total_time = (
roughing_op.get("estimated_time_min", 0) +
finishing_op.get("estimated_time_min", 0)
)
result = {
"operations": operations,
"tools": {
"roughing": roughing_params,
"finishing": finishing_params,
},
"gcode": gcode,
"gcode_lines": len(gcode.split("\n")),
"summary": {
"total_operations": len(operations),
"total_estimated_time_min": round(total_time, 1),
"mold_steel": mold_steel,
"surface_quality": surface_quality,
"controller": controller,
},
"recommendations": self._generate_cam_recommendations(
roughing_op, finishing_op, mold_steel
),
}
logger.info(f"CAM设计完成: {len(operations)} 个工序, "
f"预计 {total_time:.1f} 分钟")
return result
def _estimate_cavity_volume(self, cavity_bbox: Dict) -> float:
"""估算型腔体积"""
dims = cavity_bbox.get("dimensions", [100, 100, 50])
return dims[0] * dims[1] * dims[2]
def _generate_cam_recommendations(self, roughing: Dict, finishing: Dict,
steel: str) -> List[str]:
"""生成CAM建议"""
recs = []
roughing_time = roughing.get("estimated_time_min", 0)
if roughing_time > 120:
recs.append("粗加工时间较长,建议使用更大直径刀具或增加切削深度")
finishing_roughness = finishing.get("surface_roughness_ra", 0)
if finishing_roughness > 0.8:
recs.append("表面粗糙度偏高,建议减小步距或使用更小直径球头刀")
if steel in ("S136", "H13"):
recs.append(f"高硬度钢材({steel}),建议使用涂层刀具并降低切削速度")
recs.append("建议增加半精加工工序减少精加工余量")
recs.append("加工前需确认工件坐标系零点位置")
recs.append("首件加工建议降低进给率20%进行试切")
return recs
+511
View File
@@ -0,0 +1,511 @@
from typing import Dict, List, Any, Tuple, Optional
import numpy as np
from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeFace
from OCC.Core.gp import gp_Pln, gp_Dir, gp_Pnt
from OCC.Core.TopoDS import TopoDS_Face, TopoDS_Shape, topods
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface
from OCC.Core.TopExp import TopExp_Explorer
from OCC.Core.TopAbs import TopAbs_FACE
from OCC.Core.Bnd import Bnd_Box
from OCC.Core.BRepBndLib import brepbndlib
from shared.models.schemas import create_mold_cavity_data, create_mold_key_info
from shared.utils.logger import get_logger
from moldinsight.core.base_mold_generator import BaseMoldGenerator
from moldinsight.core.side_action_designer import SideActionDesigner
logger = get_logger(__name__)
class MoldCavityGenerator(BaseMoldGenerator):
"""模具型腔生成器 - 基于产品模型生成Cavity和Core"""
def __init__(self, shrinkage_rate: float = 0.005, draft_angle: float = 2.0,
material_density: float = 1.05):
super().__init__(shrinkage_rate, draft_angle, material_density)
self.material_densities = {
"ABS": 1.05,
"PP": 0.90,
"PC": 1.20,
"PE": 0.95,
"PS": 1.05,
"PA": 1.14,
"POM": 1.42,
"PMMA": 1.18
}
self.parting_line_tolerance = 0.1
self.max_draft_angle = 5.0
self.side_action_designer = SideActionDesigner()
def set_material(self, material: str):
"""设置产品材料"""
if material in self.material_densities:
self.material_density = self.material_densities[material]
logger.info(f"材料设置为 {material}, 密度: {self.material_density} g/cm³")
else:
logger.warning(f"未知材料 {material}, 使用默认密度 {self.material_density} g/cm³")
def generate_mold_cavities(self, product_shape: TopoDS_Shape) -> Dict[str, Any]:
"""
从产品的3D模型生成型腔和型芯
Returns:
{
"cavity": cavity_shape,
"core": core_shape,
"parting_surface": parting_surface,
"parting_line": parting_line
}
"""
logger.info("开始生成模具型腔...")
try:
analysis = self._analyze_product_geometry(product_shape)
parting_result = self._detect_primary_parting(product_shape, analysis)
parting_surface = parting_result["surface"]
parting_line = self.optimize_parting_line(parting_result["line"])
parting_direction = parting_result["direction"]
side_action_result = self.side_action_designer.analyze_and_design(
shape=product_shape,
parting_direction=parting_direction,
mold_size=self._calculate_mold_size(analysis),
parting_surface=parting_surface,
)
undercut_regions = self._build_undercut_regions(
side_action_result.get("undercut_analysis", {})
)
scaled_shape = self._apply_shrinkage_compensation(product_shape)
drafted_shape = self._apply_draft_angles(scaled_shape, parting_surface)
cavity, core = self._split_cavity_core(drafted_shape, parting_surface)
logger.info("模具型腔生成完成")
return {
"cavity": cavity,
"core": core,
"parting_surface": parting_surface,
"parting_line": parting_line,
"analysis": analysis,
"undercut_regions": undercut_regions,
"side_actions": side_action_result,
}
except Exception as e:
logger.error(f"模具型腔生成失败: {e}")
raise
def generate_detailed_cavity_json(self, cavity_data: Dict) -> Dict[str, Any]:
"""
生成详细的型腔三维JSON数据
Returns:
包含完整几何信息的JSON结构
"""
cavity = cavity_data["cavity"]
core = cavity_data["core"]
parting_surface = cavity_data["parting_surface"]
analysis = cavity_data["analysis"]
cavity_geometry = self._extract_shape_geometry(cavity, "cavity")
core_geometry = self._extract_shape_geometry(core, "core")
parting_geometry = self._extract_parting_surface_geometry(
parting_surface
)
detailed_json = {
"metadata": {
"version": "2.0",
"generated_at": str(np.datetime64('now')),
"shrinkage_rate": self.shrinkage_rate,
"draft_angle": self.draft_angle,
"unit": "mm"
},
"product_analysis": {
"bounding_box": analysis.get("bounding_box", {}),
"volume": analysis.get("volume", 0),
"surface_area": analysis.get("surface_area", 0),
"center_of_mass": analysis.get("center_of_mass", [0, 0, 0])
},
"mold_cavities": {
"cavity": cavity_geometry,
"core": core_geometry
},
"parting_surface": parting_geometry,
"quality_checks": {
"undercut_regions": cavity_data.get("undercut_regions", []),
"side_actions": cavity_data.get("side_actions", {}),
},
"manufacturing_info": {
"estimated_mold_size": self._calculate_mold_size(analysis),
"estimated_clamping_force": self._calculate_clamping_force(analysis),
"recommended_material": self._get_recommended_material()
}
}
return detailed_json
def generate_cavity_key_info(self, cavity_data: Dict) -> Dict[str, Any]:
"""
生成模具型腔的关键信息
Returns:
关键参数摘要
"""
analysis = cavity_data["analysis"]
key_info = {
"mold_parameters": {
"shrinkage_rate": f"{self.shrinkage_rate * 100:.2f}%",
"draft_angle": f"{self.draft_angle}°",
"parting_line_length": self._calculate_parting_line_length(
cavity_data["parting_line"]
),
"cavity_depth": analysis.get("bounding_box", {}).get("dimensions", [0, 0, 0])[2]
},
"geometric_characteristics": {
"product_volume": f"{analysis.get('volume', 0) / 1000:.2f} cm³",
"product_weight": self._calculate_product_weight(analysis),
"wall_thickness_range": self._estimate_wall_thickness(analysis),
"complexity_score": self._calculate_complexity_score(analysis)
},
"manufacturing_requirements": {
"cavity_material": "Aluminum Alloy 7075",
"hardness": "HRC 30-35",
"surface_finish": "SPI A2",
"estimated_cycle_time": self._estimate_cycle_time(analysis),
"recommended_injection_pressure": "80-120 MPa"
},
"quality_considerations": {
"undercut_count": len(cavity_data.get("undercut_regions", [])),
"side_action_summary": cavity_data.get("side_actions", {}).get("summary", {}),
"potential_weld_lines": self._identify_weld_line_risk(analysis),
"sink_mark_areas": self._identify_sink_mark_risk(analysis),
"warpage_risk": self._assess_warpage_risk(analysis)
}
}
return key_info
# ==================== 内部方法 ====================
def _detect_parting_surface(self, shape: TopoDS_Shape, analysis: Dict) -> Tuple[TopoDS_Face, List]:
"""
检测分型面和分型线
优先级:
1. AI 模型检测(如果已设置)
2. 基于法向量分析的几何方法
3. 简化方法(基于边界框)
"""
try:
parting_result = self._detect_primary_parting(shape, analysis)
logger.info(
f"使用 {parting_result['method']} 方法检测分型面,"
f"置信度={parting_result['confidence']:.3f}"
)
return parting_result["surface"], self.optimize_parting_line(parting_result["line"])
except Exception as e:
logger.warning(f"法向量分析失败,使用简化方法:{e}")
logger.info("使用简化方法检测分型面")
return self._simple_parting_surface(shape, analysis)
def _detect_primary_parting(self, shape: TopoDS_Shape, analysis: Dict) -> Dict[str, Any]:
"""检测主分型面(AI优先 → 几何法向量 → 简化回退)"""
if self.ai_parting_detector is not None:
try:
ai_result = self.ai_parting_detector.detect(shape, analysis)
if ai_result is not None:
surface, line = self._create_parting_surface_from_ai(ai_result, analysis, shape)
return {
"surface": surface,
"line": line,
"direction": ai_result.get("normal", [0, 0, 1]),
"method": ai_result.get("method", "ai"),
"confidence": ai_result.get("confidence", 0.8),
}
except Exception as e:
logger.warning(f"AI 分型面检测失败: {e}")
try:
normal_dir = self._analyze_face_normals(shape)
parting_plane = self._create_optimal_parting_plane(shape, analysis, normal_dir)
dims = analysis.get("bounding_box", {}).get("dimensions", [100, 100, 100])
span = max(dims) * 1.5 + 30
parting_surface = BRepBuilderAPI_MakeFace(
parting_plane, -span, span, -span, span
).Face()
parting_surface = self.extend_parting_surface(parting_surface, shape, extension=30.0)
parting_line = self._calculate_parting_line(shape, parting_surface)
return {
"surface": parting_surface,
"line": parting_line,
"direction": [float(normal_dir.X()), float(normal_dir.Y()), float(normal_dir.Z())],
"method": "face_normal_analysis",
"confidence": 0.85,
}
except Exception as e:
logger.warning(f"法向量分析失败,使用简化方法:{e}")
surface, line = self._simple_parting_surface(shape, analysis)
return {
"surface": surface,
"line": line,
"direction": [0, 0, 1],
"method": "simple",
"confidence": 0.6,
}
def _build_undercut_regions(self, undercut_analysis: Dict[str, Any]) -> List[Dict[str, Any]]:
"""将侧向机构分析结果转换为兼容旧结构的倒扣区域列表。"""
undercut_faces = undercut_analysis.get("undercut_faces", [])
regions = []
for face in undercut_faces:
regions.append({
"type": "negative_draft",
"location": face.get("center", [0, 0, 0]),
"severity": face.get("severity", "medium"),
"area": face.get("area", 0),
"is_outer": face.get("is_outer", False),
"face_index": face.get("face_index"),
})
logger.info(f"转换得到 {len(regions)} 个兼容倒扣区域")
return regions
def _analyze_face_normals(self, shape: TopoDS_Shape) -> gp_Dir:
"""
分析产品表面的法向量分布,找出最优分型方向
原理:
- 统计所有面的法向量
- 选择法向量变化最小的方向作为分型方向
- 避免倒扣(undercut)区域
"""
face_normals = []
explorer = TopExp_Explorer(shape, TopAbs_FACE)
while explorer.More():
face = topods.Face(explorer.Current())
surface = BRepAdaptor_Surface(face)
try:
if surface.GetType() == 0:
normal = surface.Plane().Position().Direction()
else:
bbox = Bnd_Box()
brepbndlib.Add(face, bbox)
normal = gp_Dir(0, 0, 1)
face_normals.append(normal)
except Exception as e:
logger.debug(f"面法向量计算失败:{e}")
explorer.Next()
if not face_normals:
return gp_Dir(0, 0, 1)
avg_x = sum(n.X() for n in face_normals) / len(face_normals)
avg_y = sum(n.Y() for n in face_normals) / len(face_normals)
avg_z = sum(n.Z() for n in face_normals) / len(face_normals)
length = np.sqrt(avg_x**2 + avg_y**2 + avg_z**2)
if length > 0.001:
return gp_Dir(avg_x/length, avg_y/length, avg_z/length)
else:
return gp_Dir(0, 0, 1)
def _create_optimal_parting_plane(self, shape: TopoDS_Shape, analysis: Dict,
direction: gp_Dir) -> gp_Pln:
"""
创建最优分型面
Args:
shape: 产品形状
analysis: 几何分析结果
direction: 分型方向(法向量)
Returns:
gp_Pln: 分型面方程
"""
bbox = analysis["bounding_box"]
center = bbox["center"]
parting_plane = gp_Pln(
gp_Pnt(center[0], center[1], center[2]),
direction
)
logger.info(f"创建分型面:原点=({center[0]:.2f}, {center[1]:.2f}, {center[2]:.2f}), "
f"法向量=({direction.X():.3f}, {direction.Y():.3f}, {direction.Z():.3f})")
return parting_plane
def _simple_parting_surface(self, shape: TopoDS_Shape, analysis: Dict) -> Tuple[TopoDS_Face, List]:
"""简化的分型面检测(回退方案)"""
bbox = analysis["bounding_box"]
center_z = bbox["center"][2]
parting_plane = gp_Pln(
gp_Pnt(0, 0, center_z),
gp_Dir(0, 0, 1)
)
parting_surface = BRepBuilderAPI_MakeFace(
parting_plane,
bbox["min"][0] - 10, bbox["max"][0] + 10,
bbox["min"][1] - 10, bbox["max"][1] + 10
).Face()
parting_line = self._simple_parting_line(shape)
return parting_surface, parting_line
def _create_parting_surface_from_ai(self, ai_result: Dict,
analysis: Dict, shape: Optional[TopoDS_Shape] = None) -> Tuple[TopoDS_Face, List]:
"""
从 AI 模型结果创建分型面(预留接口)
Args:
ai_result: AI 模型输出,应包含:
- origin: [x, y, z] 平面原点
- normal: [nx, ny, nz] 法向量
analysis: 几何分析结果
shape: 产品形状(用于计算分型线)
Returns:
(parting_surface, parting_line)
"""
origin = ai_result.get("origin", [0, 0, 0])
normal = ai_result.get("normal", [0, 0, 1])
parting_plane = gp_Pln(
gp_Pnt(origin[0], origin[1], origin[2]),
gp_Dir(normal[0], normal[1], normal[2])
)
parting_surface = BRepBuilderAPI_MakeFace(parting_plane).Face()
if "parting_line" in ai_result:
parting_line = ai_result["parting_line"]
elif shape is not None:
parting_line = self._calculate_parting_line(shape, parting_surface)
else:
parting_line = []
logger.info(f"从 AI 结果创建分型面:原点={origin}, 法向量={normal}")
return parting_surface, parting_line
def _extract_parting_surface_geometry(self, surface: TopoDS_Face) -> Dict[str, Any]:
"""提取分型面几何数据"""
metadata = self._extract_plane_metadata(surface)
return {
"type": "plane",
"normal": metadata["normal"],
"origin": metadata["origin"],
"bounds": metadata["bounds"],
}
def _calculate_mold_size(self, analysis: Dict) -> Dict[str, float]:
"""估算模具尺寸"""
product_bbox = analysis["bounding_box"]["dimensions"]
margin = 30
return {
"length": product_bbox[0] + 2 * margin,
"width": product_bbox[1] + 2 * margin,
"height": product_bbox[2] + 2 * margin + 100,
"margin": margin
}
def _calculate_clamping_force(self, analysis: Dict) -> str:
"""估算锁模力"""
volume_cm3 = analysis.get("volume", 0) / 1000
if volume_cm3 < 10:
return "50-100 吨"
elif volume_cm3 < 100:
return "150-300 吨"
elif volume_cm3 < 500:
return "400-600 吨"
else:
return "800+ 吨"
def _get_recommended_material(self) -> str:
"""推荐模具材料"""
return "Aluminum Alloy 7075 (铝合金模具)"
def _estimate_wall_thickness(self, analysis: Dict) -> str:
"""估算壁厚范围"""
volume = analysis.get("volume", 0)
surface_area = analysis.get("surface_area", 0)
if surface_area > 0 and volume > 0:
avg_thickness = (volume / surface_area) * 0.6
return f"{avg_thickness * 0.7:.2f} - {avg_thickness * 1.3:.2f} mm"
elif volume > 0:
bbox_dims = analysis.get("bounding_box", {}).get("dimensions", [1, 1, 1])
bbox_volume = bbox_dims[0] * bbox_dims[1] * bbox_dims[2]
if bbox_volume > 0:
efficiency = volume / bbox_volume
avg_thickness = (bbox_dims[0] + bbox_dims[1]) / 2 * efficiency
return f"{avg_thickness * 0.7:.2f} - {avg_thickness * 1.3:.2f} mm"
return "2.0 - 4.0 mm (默认)"
def _calculate_complexity_score(self, analysis: Dict) -> float:
"""计算复杂度评分(0-10)"""
volume = analysis.get("volume", 0)
surface_area = analysis.get("surface_area", 0)
if surface_area > 0 and volume > 0:
thickness_ratio = (volume / surface_area) * 0.6
complexity = min(thickness_ratio / 5.0, 10.0)
return round(complexity, 1)
elif volume > 0:
bbox_dims = analysis.get("bounding_box", {}).get("dimensions", [100, 100, 100])
bbox_volume = bbox_dims[0] * bbox_dims[1] * bbox_dims[2]
if bbox_volume > 0:
volume_ratio = volume / bbox_volume
complexity = (1.0 - volume_ratio) * 10
return round(min(max(complexity, 0), 10), 1)
return 5.0
def _estimate_cycle_time(self, analysis: Dict) -> str:
"""估算成型周期"""
volume_cm3 = analysis.get("volume", 0) / 1000
if volume_cm3 < 10:
return "15-25 秒"
elif volume_cm3 < 50:
return "25-40 秒"
elif volume_cm3 < 200:
return "40-60 秒"
else:
return "60-90 秒"
def _identify_weld_line_risk(self, analysis: Dict) -> str:
"""识别熔接痕风险"""
complexity = self._calculate_complexity_score(analysis)
if complexity > 7:
return "高 - 建议优化浇口位置"
elif complexity > 4:
return "中 - 需仿真验证"
else:
return "低"
def _identify_sink_mark_risk(self, analysis: Dict) -> str:
"""识别缩痕风险"""
return "中 - 建议壁厚均匀性检查"
+762
View File
@@ -0,0 +1,762 @@
"""
模具加工碰撞检测与刀路优化模块
功能:
1. CollisionDetector - 碰撞检测器
- 刀柄干涉检测
- 快速移动碰撞检测
- 机床行程限制验证
- 安全区域计算
2. ToolpathOptimizer - 刀路优化器
- 进给率自适应优化
- 空走刀路径最小化
- 拐角减速处理
- 切入切出优化
3. EDMElectrodeDesigner - EDM电极设计器
- 电极自动生成
- 放电间隙计算
- 电极加工路径
4. MachiningSimulator - 加工仿真器
- 材料去除模拟
- 过切检测
- 残余材料分析
- 加工质量评估
"""
from typing import Dict, List, Any, Optional, Tuple
import math
import numpy as np
from shared.utils.logger import get_logger
logger = get_logger(__name__)
class CollisionDetector:
"""碰撞检测器"""
def __init__(self):
self.machine_limits = {
"x_min": -500, "x_max": 500,
"y_min": -400, "y_max": 400,
"z_min": -300, "z_max": 300,
}
self.safety_margin = 5.0
self.retract_height = 50.0
def check_toolpath_safety(self, toolpath_points: List[List[float]],
tool: Dict, stock_bbox: Dict,
clamp_positions: Optional[List[Dict]] = None) -> Dict[str, Any]:
"""
综合检查刀路安全性
Args:
toolpath_points: 刀路点列表 [[x,y,z], ...]
tool: 刀具参数
stock_bbox: 毛坯边界框
clamp_positions: 压板位置列表
Returns:
安全检查结果
"""
holder_collisions = self._check_holder_collision(toolpath_points, tool, stock_bbox)
rapid_collisions = self._check_rapid_move_collisions(toolpath_points, stock_bbox)
limit_violations = self._check_machine_limits(toolpath_points)
clamp_collisions = []
if clamp_positions:
clamp_collisions = self._check_clamp_collisions(
toolpath_points, tool, clamp_positions
)
all_issues = holder_collisions + rapid_collisions + limit_violations + clamp_collisions
safe_retract_points = self._calculate_safe_retract_points(
toolpath_points, stock_bbox
)
is_safe = len(all_issues) == 0
return {
"is_safe": is_safe,
"total_issues": len(all_issues),
"holder_collisions": holder_collisions,
"rapid_collisions": rapid_collisions,
"limit_violations": limit_violations,
"clamp_collisions": clamp_collisions,
"safe_retract_points": safe_retract_points,
"recommendations": self._generate_safety_recommendations(all_issues),
}
def _check_holder_collision(self, points: List[List[float]],
tool: Dict, stock_bbox: Dict) -> List[Dict]:
"""检测刀柄干涉"""
collisions = []
tool_diameter = tool.get("diameter", 10)
flute_length = tool.get("flute_length", 30)
shank_diameter = tool.get("shank_diameter", tool_diameter)
holder_diameter = tool.get("holder_diameter", shank_diameter * 2)
stock_z_max = stock_bbox.get("max", [0, 0, 0])[2]
for i, pt in enumerate(points):
if len(pt) < 3:
continue
z = pt[2]
depth_below_stock = stock_z_max - z
if depth_below_stock > flute_length:
holder_z = z + flute_length
holder_clearance = holder_diameter / 2 + self.safety_margin
stock_xmin = stock_bbox.get("min", [0, 0, 0])[0]
stock_xmax = stock_bbox.get("max", [0, 0, 0])[0]
stock_ymin = stock_bbox.get("min", [0, 0, 0])[1]
stock_ymax = stock_bbox.get("max", [0, 0, 0])[1]
if (stock_xmin - holder_clearance < pt[0] < stock_xmax + holder_clearance and
stock_ymin - holder_clearance < pt[1] < stock_ymax + holder_clearance):
collisions.append({
"type": "holder_collision",
"point_index": i,
"position": pt,
"depth": round(depth_below_stock, 2),
"flute_length": flute_length,
"severity": "high",
"message": f"点{i}: 切深{depth_below_stock:.1f}mm超过刃长{flute_length}mm,刀柄可能干涉"
})
return collisions
def _check_rapid_move_collisions(self, points: List[List[float]],
stock_bbox: Dict) -> List[Dict]:
"""检测快速移动碰撞"""
collisions = []
stock_xmin = stock_bbox.get("min", [0, 0, 0])[0]
stock_xmax = stock_bbox.get("max", [0, 0, 0])[0]
stock_ymin = stock_bbox.get("min", [0, 0, 0])[1]
stock_ymax = stock_bbox.get("max", [0, 0, 0])[1]
stock_zmin = stock_bbox.get("min", [0, 0, 0])[2]
stock_zmax = stock_bbox.get("max", [0, 0, 0])[2]
for i in range(1, len(points)):
prev = points[i - 1]
curr = points[i]
if len(prev) < 3 or len(curr) < 3:
continue
z_change = abs(curr[2] - prev[2])
xy_change = math.sqrt((curr[0] - prev[0])**2 + (curr[1] - prev[1])**2)
if z_change < 1.0 and xy_change > 5.0:
min_z = min(prev[2], curr[2])
if min_z < stock_zmax + self.safety_margin:
mid_x = (prev[0] + curr[0]) / 2
mid_y = (prev[1] + curr[1]) / 2
if (stock_xmin < mid_x < stock_xmax and
stock_ymin < mid_y < stock_ymax):
collisions.append({
"type": "rapid_collision",
"segment": [i - 1, i],
"start": prev,
"end": curr,
"severity": "high",
"message": f"段{i-1}-{i}: 水平快速移动可能穿过毛坯"
})
return collisions
def _check_machine_limits(self, points: List[List[float]]) -> List[Dict]:
"""验证机床行程限制"""
violations = []
for i, pt in enumerate(points):
if len(pt) < 3:
continue
if not (self.machine_limits["x_min"] <= pt[0] <= self.machine_limits["x_max"]):
violations.append({
"type": "machine_limit",
"point_index": i,
"axis": "X",
"value": pt[0],
"limit": [self.machine_limits["x_min"], self.machine_limits["x_max"]],
"severity": "critical",
})
if not (self.machine_limits["y_min"] <= pt[1] <= self.machine_limits["y_max"]):
violations.append({
"type": "machine_limit",
"point_index": i,
"axis": "Y",
"value": pt[1],
"limit": [self.machine_limits["y_min"], self.machine_limits["y_max"]],
"severity": "critical",
})
if not (self.machine_limits["z_min"] <= pt[2] <= self.machine_limits["z_max"]):
violations.append({
"type": "machine_limit",
"point_index": i,
"axis": "Z",
"value": pt[2],
"limit": [self.machine_limits["z_min"], self.machine_limits["z_max"]],
"severity": "critical",
})
return violations
def _check_clamp_collisions(self, points: List[List[float]], tool: Dict,
clamps: List[Dict]) -> List[Dict]:
"""检测压板碰撞"""
collisions = []
tool_radius = tool.get("diameter", 10) / 2
for i, pt in enumerate(points):
if len(pt) < 3:
continue
for j, clamp in enumerate(clamps):
clamp_center = clamp.get("center", [0, 0, 0])
clamp_size = clamp.get("size", [50, 30, 20])
clamp_z_top = clamp_center[2] + clamp_size[2] / 2
if pt[2] < clamp_z_top + self.safety_margin:
dx = abs(pt[0] - clamp_center[0])
dy = abs(pt[1] - clamp_center[1])
if (dx < clamp_size[0] / 2 + tool_radius + self.safety_margin and
dy < clamp_size[1] / 2 + tool_radius + self.safety_margin):
collisions.append({
"type": "clamp_collision",
"point_index": i,
"clamp_index": j,
"severity": "high",
"message": f"点{i}: 可能与压板{j}碰撞"
})
return collisions
def _calculate_safe_retract_points(self, points: List[List[float]],
stock_bbox: Dict) -> List[Dict]:
"""计算安全抬刀点"""
retract_points = []
stock_zmax = stock_bbox.get("max", [0, 0, 0])[2]
safe_z = stock_zmax + self.retract_height
for i in range(0, len(points), max(1, len(points) // 10)):
pt = points[i]
if len(pt) >= 3:
retract_points.append({
"index": i,
"from": pt,
"retract_to": [pt[0], pt[1], safe_z],
"safe_z": safe_z,
})
return retract_points
def _generate_safety_recommendations(self, issues: List[Dict]) -> List[str]:
"""生成安全建议"""
recs = []
holder_issues = [i for i in issues if i["type"] == "holder_collision"]
if holder_issues:
recs.append(f"发现 {len(holder_issues)} 处刀柄干涉,建议加长刀具或减少切深")
rapid_issues = [i for i in issues if i["type"] == "rapid_collision"]
if rapid_issues:
recs.append(f"发现 {len(rapid_issues)} 处快速移动碰撞风险,建议增加抬刀高度")
limit_issues = [i for i in issues if i["type"] == "machine_limit"]
if limit_issues:
recs.append(f"发现 {len(limit_issues)} 处超出机床行程,需调整工件位置")
clamp_issues = [i for i in issues if i["type"] == "clamp_collision"]
if clamp_issues:
recs.append(f"发现 {len(clamp_issues)} 处压板碰撞,建议调整压板位置")
if not issues:
recs.append("刀路安全检查通过,无碰撞风险")
return recs
class ToolpathOptimizer:
"""刀路优化器"""
def optimize_toolpath(self, toolpath_points: List[List[float]],
cutting_params: Dict,
stock_bbox: Optional[Dict] = None) -> Dict[str, Any]:
"""
综合优化刀路
优化内容:
1. 进给率自适应优化
2. 拐角减速处理
3. 空走刀路径优化
4. 切入切出优化
Args:
toolpath_points: 原始刀路点
cutting_params: 切削参数
stock_bbox: 毛坯边界框
Returns:
优化后的刀路和参数
"""
feed_optimized = self._optimize_feed_rates(toolpath_points, cutting_params)
corner_optimized = self._optimize_corner_speeds(toolpath_points, feed_optimized)
entry_exit_optimized = self._optimize_entry_exit(toolpath_points, stock_bbox)
stats = self._calculate_optimization_stats(
toolpath_points, feed_optimized, corner_optimized
)
return {
"original_point_count": len(toolpath_points),
"optimized_feeds": feed_optimized,
"corner_slowdowns": corner_optimized,
"entry_exit": entry_exit_optimized,
"stats": stats,
"recommendations": self._generate_optimization_recommendations(stats),
}
def _optimize_feed_rates(self, points: List[List[float]],
params: Dict) -> List[Dict]:
"""进给率自适应优化"""
base_feed = params.get("feed_rate_mm_min", 500)
optimized = []
for i in range(len(points)):
if i < 2 or i >= len(points) - 2:
feed = base_feed * 0.8
else:
v1 = np.array(points[i]) - np.array(points[i - 1])
v2 = np.array(points[i + 1]) - np.array(points[i])
len1 = np.linalg.norm(v1)
len2 = np.linalg.norm(v2)
if len1 > 0.001 and len2 > 0.001:
cos_angle = np.clip(np.dot(v1, v2) / (len1 * len2), -1, 1)
angle = math.degrees(math.acos(cos_angle))
if angle < 30:
feed = base_feed * 0.3
elif angle < 60:
feed = base_feed * 0.5
elif angle < 120:
feed = base_feed * 0.7
else:
feed = base_feed
else:
feed = base_feed
optimized.append({
"index": i,
"feed_rate": round(feed, 1),
"feed_ratio": round(feed / base_feed, 2),
})
return optimized
def _optimize_corner_speeds(self, points: List[List[float]],
feed_data: List[Dict]) -> List[Dict]:
"""拐角减速处理"""
slowdowns = []
base_feed = 500
for i in range(1, len(points) - 1):
if i >= len(feed_data):
break
v1 = np.array(points[i]) - np.array(points[i - 1])
v2 = np.array(points[i + 1]) - np.array(points[i])
len1 = np.linalg.norm(v1)
len2 = np.linalg.norm(v2)
if len1 > 0.001 and len2 > 0.001:
cos_angle = np.clip(np.dot(v1, v2) / (len1 * len2), -1, 1)
angle = math.degrees(math.acos(cos_angle))
if angle < 90:
decel_distance = max(2.0, 10.0 * (1 - angle / 90))
slowdowns.append({
"index": i,
"angle": round(angle, 1),
"decel_distance": round(decel_distance, 2),
"min_feed_ratio": 0.3 if angle < 45 else 0.5,
})
return slowdowns
def _optimize_entry_exit(self, points: List[List[float]],
stock_bbox: Optional[Dict]) -> Dict[str, Any]:
"""切入切出优化"""
entry = {"type": "arc_tangent", "radius": 5.0, "angle": 90}
exit_ = {"type": "arc_tangent", "radius": 5.0, "angle": 90}
if stock_bbox:
z_max = stock_bbox.get("max", [0, 0, 0])[2]
entry["approach_z"] = z_max + 10
exit_["retract_z"] = z_max + 50
return {"entry": entry, "exit": exit_}
def _calculate_optimization_stats(self, points: List, feeds: List,
corners: List) -> Dict:
"""计算优化统计"""
if not feeds:
return {"time_reduction_percent": 0}
feed_values = [f["feed_rate"] for f in feeds]
avg_feed = sum(feed_values) / len(feed_values) if feed_values else 500
base_feed = max(feed_values) if feed_values else 500
time_reduction = 0
if base_feed > 0:
time_reduction = (1 - avg_feed / base_feed) * 100
return {
"avg_feed_rate": round(avg_feed, 1),
"base_feed_rate": base_feed,
"corner_slowdown_count": len(corners),
"time_reduction_percent": round(abs(time_reduction), 1),
}
def _generate_optimization_recommendations(self, stats: Dict) -> List[str]:
"""生成优化建议"""
recs = []
if stats.get("corner_slowdown_count", 0) > 10:
recs.append("拐角减速点较多,建议优化刀路方向减少急转弯")
if stats.get("time_reduction_percent", 0) > 30:
recs.append("进给率降低幅度较大,建议优化加工策略")
if not recs:
recs.append("刀路优化完成,进给率分布合理")
return recs
class EDMElectrodeDesigner:
"""EDM电极设计器"""
ELECTRODE_MATERIALS = {
"copper": {
"density": 8.96, "wear_rate": 1.0,
"machinability": "good", "cost": "medium"
},
"graphite": {
"density": 1.75, "wear_rate": 0.5,
"machinability": "excellent", "cost": "low"
},
"copper_tungsten": {
"density": 14.0, "wear_rate": 0.3,
"machinability": "poor", "cost": "high"
},
}
def design_electrodes(self, undercut_regions: List[Dict],
cavity_bbox: Dict,
material: str = "copper",
spark_gap: float = 0.05,
overburn: float = 0.1) -> Dict[str, Any]:
"""
设计EDM电极
Args:
undercut_regions: 倒扣区域列表
cavity_bbox: 型腔边界框
material: 电极材料
spark_gap: 放电间隙 mm
overburn: 过切量 mm
Returns:
电极设计方案
"""
mat_props = self.ELECTRODE_MATERIALS.get(material, self.ELECTRODE_MATERIALS["copper"])
electrodes = []
for i, region in enumerate(undercut_regions):
electrode = self._design_single_electrode(
region, i + 1, material, spark_gap, overburn, cavity_bbox
)
electrodes.append(electrode)
total_volume = sum(e["volume_mm3"] for e in electrodes)
total_weight = total_volume * mat_props["density"] / 1000
return {
"electrodes": electrodes,
"material": material,
"material_properties": mat_props,
"spark_gap": spark_gap,
"overburn": overburn,
"total_electrode_count": len(electrodes),
"total_volume_cm3": round(total_volume / 1000, 2),
"total_weight_g": round(total_weight, 2),
"machining_strategy": self._generate_electrode_machining_strategy(
electrodes, material
),
"recommendations": self._generate_electrode_recommendations(
electrodes, material
),
}
def _design_single_electrode(self, region: Dict, index: int,
material: str, spark_gap: float,
overburn: float, cavity_bbox: Dict) -> Dict:
"""设计单个电极"""
center = region.get("center", [0, 0, 0])
area = region.get("area", 100)
feature_size = math.sqrt(area)
electrode_size = {
"width": round(feature_size * 1.3 + 2 * (spark_gap + overburn), 2),
"length": round(feature_size * 1.3 + 2 * (spark_gap + overburn), 2),
"height": round(cavity_bbox.get("dimensions", [0, 0, 50])[2] * 0.8 + 20, 2),
}
volume = electrode_size["width"] * electrode_size["length"] * electrode_size["height"]
return {
"index": index,
"type": region.get("type", "undercut"),
"location": center,
"size": electrode_size,
"volume_mm3": round(volume, 1),
"spark_gap": spark_gap,
"overburn": overburn,
"material": material,
"roughing_passes": 3,
"finishing_passes": 2,
}
def _generate_electrode_machining_strategy(self, electrodes: List,
material: str) -> List[Dict]:
"""生成电极加工策略"""
strategies = []
for elec in electrodes:
size = elec["size"]
is_small = min(size["width"], size["length"]) < 5
strategy = {
"electrode_index": elec["index"],
"operations": [
{
"operation": "roughing",
"tool": "endmill_6mm" if not is_small else "endmill_3mm",
"stock_allowance": 0.3,
},
{
"operation": "finishing",
"tool": "ballnose_3mm" if not is_small else "ballnose_1mm",
"stepover": 0.2,
},
],
}
strategies.append(strategy)
return strategies
def _generate_electrode_recommendations(self, electrodes: List,
material: str) -> List[str]:
"""生成电极建议"""
recs = []
if material == "copper":
recs.append("铜电极加工性良好,建议使用高速钢刀具")
elif material == "graphite":
recs.append("石墨电极易加工但易碎,注意切削力控制")
elif material == "copper_tungsten":
recs.append("铜钨合金硬度高,建议使用金刚石刀具")
if len(electrodes) > 4:
recs.append("电极数量较多,建议评估是否可合并电极设计")
recs.append("电极加工后需检测尺寸精度和表面质量")
recs.append("放电加工时需根据材料调整电参数")
return recs
class MachiningSimulator:
"""加工仿真器"""
def simulate_machining(self, operations: List[Dict],
stock_bbox: Dict,
resolution: float = 1.0) -> Dict[str, Any]:
"""
模拟加工过程
Args:
operations: 加工操作列表
stock_bbox: 毛坯边界框
resolution: 仿真精度 mm
Returns:
仿真结果
"""
stock_dims = stock_bbox.get("dimensions", [100, 100, 50])
nx = max(2, int(stock_dims[0] / resolution))
ny = max(2, int(stock_dims[1] / resolution))
nz = max(2, int(stock_dims[2] / resolution))
stock = np.ones((nx, ny, nz), dtype=np.float32)
total_removed = 0
operation_results = []
for op in operations:
removed = self._simulate_operation(stock, op, stock_bbox, resolution)
total_removed += removed
operation_results.append({
"strategy": op.get("strategy", "unknown"),
"volume_removed_mm3": removed,
"remaining_stock_percent": round(
(1 - total_removed / (nx * ny * nz)) * 100, 1
),
})
total_voxels = nx * ny * nz
remaining = np.sum(stock > 0)
removal_efficiency = (1 - remaining / total_voxels) * 100 if total_voxels > 0 else 0
gouging = self._detect_gouging(stock, operations, stock_bbox, resolution)
residual = self._analyze_residual_material(stock, stock_bbox, resolution)
return {
"resolution": resolution,
"grid_size": {"nx": nx, "ny": ny, "nz": nz},
"operations": operation_results,
"total_volume_removed_percent": round(removal_efficiency, 1),
"gouging_detected": gouging,
"residual_analysis": residual,
"quality_assessment": self._assess_quality(gouging, residual),
"recommendations": self._generate_simulation_recommendations(
gouging, residual, removal_efficiency
),
}
def _simulate_operation(self, stock: np.ndarray, op: Dict,
bbox: Dict, resolution: float) -> int:
"""模拟单个加工操作的材料去除"""
strategy = op.get("strategy", "")
removed = 0
nx, ny, nz = stock.shape
if strategy == "z_level_roughing":
levels = op.get("levels", [])
for level in levels:
z_level = level.get("z", 0)
z_idx = int((z_level - bbox.get("min", [0, 0, 0])[2]) / resolution)
z_idx = max(0, min(z_idx, nz - 1))
for iz in range(z_idx, nz):
removed += int(np.sum(stock[:, :, iz] > 0))
stock[:, :, iz] = 0
elif strategy in ("parallel_finishing", "contour_finishing"):
stepover = op.get("stepover", 0.3)
step_idx = max(1, int(stepover / resolution))
for ix in range(0, nx, step_idx):
for iy in range(0, ny, step_idx):
if stock[ix, iy, :].any():
removed += int(np.sum(stock[ix, iy, :] > 0))
stock[ix, iy, :] = 0
return removed
def _detect_gouging(self, stock: np.ndarray, operations: List,
bbox: Dict, resolution: float) -> List[Dict]:
"""检测过切"""
gouging = []
for op in operations:
stock_allowance = op.get("stock_allowance", 0)
if stock_allowance < 0:
gouging.append({
"operation": op.get("strategy", "unknown"),
"type": "negative_allowance",
"severity": "high",
"message": f"工序 {op.get('strategy')} 余量为负值,存在过切风险"
})
return gouging
def _analyze_residual_material(self, stock: np.ndarray,
bbox: Dict, resolution: float) -> Dict:
"""分析残余材料"""
total_voxels = stock.size
remaining = int(np.sum(stock > 0))
remaining_percent = (remaining / total_voxels) * 100 if total_voxels > 0 else 0
return {
"remaining_voxels": remaining,
"remaining_percent": round(remaining_percent, 2),
"estimated_residual_volume_cm3": round(
remaining * resolution ** 3 / 1000, 2
),
}
def _assess_quality(self, gouging: List, residual: Dict) -> Dict:
"""评估加工质量"""
has_gouging = len(gouging) > 0
residual_pct = residual.get("remaining_percent", 100)
if has_gouging:
grade = "FAIL"
elif residual_pct < 5:
grade = "GOOD"
elif residual_pct < 15:
grade = "ACCEPTABLE"
else:
grade = "INSUFFICIENT"
return {
"grade": grade,
"has_gouging": has_gouging,
"residual_percent": residual_pct,
}
def _generate_simulation_recommendations(self, gouging: List, residual: Dict,
efficiency: float) -> List[str]:
"""生成仿真建议"""
recs = []
if gouging:
recs.append("检测到过切,需调整加工参数")
residual_pct = residual.get("remaining_percent", 0)
if residual_pct > 20:
recs.append("残余材料较多,建议增加精加工工序")
elif residual_pct > 5:
recs.append("残余材料适中,需检查关键区域是否加工到位")
if efficiency < 50:
recs.append("材料去除率偏低,建议优化粗加工策略")
if not recs:
recs.append("仿真结果良好,加工方案可行")
return recs
@@ -0,0 +1,414 @@
"""
铝泡沫模具质量检测模块
提供分模面质量检测、模具结构合理性评估、生产可行性分析等功能
"""
from typing import Dict, List, Any
import numpy as np
from shared.utils.logger import get_logger
logger = get_logger(__name__)
class AluminumFoamMoldQualityInspector:
"""铝泡沫模具质量检测器"""
def __init__(self):
self.quality_threshold = {
"smoothness_score": 80.0,
"continuity_score": 95.0,
"structure_score": 90.0
}
def inspect_mold(self, cavity_data: Dict, params: Dict) -> Dict[str, Any]:
"""
完整的模具质量检测
Args:
cavity_data: 模具型腔数据
params: 分模参数
Returns:
质量检测报告
"""
logger.info("开始模具质量检测...")
report = {
"surface_quality": self.inspect_surface_quality(cavity_data),
"structure_quality": self.inspect_structure_quality(cavity_data, params),
"feasibility": self.assess_production_feasibility(cavity_data, params),
"overall_score": 0.0,
"passed": False,
"warnings": [],
"recommendations": []
}
# 计算综合评分
scores = [
report["surface_quality"]["overall_score"],
report["structure_quality"]["overall_score"],
report["feasibility"]["score"]
]
report["overall_score"] = sum(scores) / len(scores)
report["passed"] = report["overall_score"] >= 80.0
logger.info(f"质量检测完成,综合评分: {report['overall_score']:.1f}%")
return report
def inspect_surface_quality(self, cavity_data: Dict) -> Dict[str, Any]:
"""
检测分模面质量
检测项目:
- 平滑度:曲率分析
- 连续性:边界检查
- 完整性:破面检测
"""
parting_line = cavity_data.get("parting_line", [])
parting_surface = cavity_data.get("parting_surface")
# 1. 平滑度检测
smoothness = self._check_smoothness(parting_line)
# 2. 连续性检测
continuity = self._check_continuity(parting_line)
# 3. 完整性检测
completeness = self._check_completeness(cavity_data)
overall = (smoothness["score"] * 0.4 +
continuity["score"] * 0.3 +
completeness["score"] * 0.3)
return {
"smoothness": smoothness,
"continuity": continuity,
"completeness": completeness,
"overall_score": overall,
"passed": overall >= self.quality_threshold["smoothness_score"]
}
def _check_smoothness(self, parting_line: List) -> Dict[str, Any]:
"""检查分型线平滑度"""
if len(parting_line) < 3:
return {"score": 50.0, "issues": ["分型线点数不足"]}
try:
points = np.array(parting_line)
# 计算相邻线段角度变化
angle_changes = []
for i in range(1, len(points) - 1):
v1 = points[i] - points[i-1]
v2 = points[i+1] - points[i]
len1, len2 = np.linalg.norm(v1), np.linalg.norm(v2)
if len1 > 0.001 and len2 > 0.001:
cos_angle = np.clip(np.dot(v1, v2) / (len1 * len2), -1, 1)
angle = np.degrees(np.arccos(cos_angle))
angle_changes.append(angle)
if not angle_changes:
return {"score": 70.0, "issues": []}
# 计算角度变化统计
max_angle = max(angle_changes)
avg_angle = np.mean(angle_changes)
# 评分:角度变化越小越好
score = max(0, 100 - avg_angle * 2 - max_angle * 0.5)
issues = []
if max_angle > 30:
issues.append(f"存在尖角,最大角度变化: {max_angle:.1f}°")
if avg_angle > 15:
issues.append(f"分型线不够平滑,平均角度变化: {avg_angle:.1f}°")
return {"score": score, "issues": issues, "max_angle": max_angle, "avg_angle": avg_angle}
except Exception as e:
logger.warning(f"平滑度检测失败: {e}")
return {"score": 50.0, "issues": ["检测过程出错"]}
def _check_continuity(self, parting_line: List) -> Dict[str, Any]:
"""检查分型线连续性"""
if len(parting_line) < 2:
return {"score": 0.0, "issues": ["分型线不完整"]}
try:
# 检查是否有明显的间隙
points = np.array(parting_line)
gaps = []
for i in range(1, len(points)):
gap = np.linalg.norm(points[i] - points[i-1])
if gap > 10.0: # 10mm 以上认为有间隙
gaps.append(gap)
# 评分
if not gaps:
score = 100.0
issues = []
elif len(gaps) == 1 and max(gaps) < 20:
score = 80.0
issues = [f"存在轻微间隙: {max(gaps):.1f}mm"]
else:
score = max(0, 100 - len(gaps) * 20)
issues = [f"存在 {len(gaps)} 处间隙"]
return {"score": score, "issues": issues, "gap_count": len(gaps)}
except Exception as e:
logger.warning(f"连续性检测失败: {e}")
return {"score": 50.0, "issues": ["检测过程出错"]}
def _check_completeness(self, cavity_data: Dict) -> Dict[str, Any]:
"""检查分模完整性"""
issues = []
# 检查必要的组件是否存在
required_keys = ["cavity", "core", "parting_surface", "parting_line"]
missing = [k for k in required_keys if k not in cavity_data]
if missing:
issues.append(f"缺少组件: {', '.join(missing)}")
return {"score": 0.0, "issues": issues}
# 检查分型线点数
parting_line = cavity_data.get("parting_line", [])
if len(parting_line) < 4:
issues.append("分型线点数不足")
score = len(parting_line) * 20
else:
score = 100.0
return {"score": score, "issues": issues}
def inspect_structure_quality(self, cavity_data: Dict, params: Dict) -> Dict[str, Any]:
"""
检测模具结构合理性
检测项目:
- 模具尺寸
- 壁厚
- 拔模角
- 倒扣处理
"""
analysis = cavity_data.get("analysis", {})
bbox = analysis.get("bounding_box", {}).get("dimensions", [0, 0, 0])
issues = []
recommendations = []
# 1. 模具尺寸检查
mold_size = cavity_data.get("mold_block")
if mold_size:
# 检查尺寸是否足够
min_dimension = min(bbox)
if min_dimension < 20:
issues.append("产品尺寸过小,可能影响模具强度")
recommendations.append("建议增加产品尺寸或使用嵌件")
# 2. 拔模角检查
draft_angle = params.get("draft_angle", 0)
if draft_angle < 2.0:
issues.append("拔模角偏小,可能导致脱模困难")
recommendations.append("建议增大拔模角到 2-5°")
# 3. 倒扣区域检查
undercut_regions = cavity_data.get("undercut_regions", [])
if undercut_regions:
issues.append(f"存在 {len(undercut_regions)} 个倒扣区域")
recommendations.append("建议添加滑块或斜顶机构")
# 4. 铝泡沫特殊检查
foam_material = params.get("foam_material", "")
if foam_material:
# 检查排气系统需求
volume = analysis.get("volume", 0)
if volume > 50000000: # > 50 cm³
issues.append("大型铝泡沫产品,需要加强排气系统")
recommendations.append("建议增加排气槽或排气针")
# 评分
issue_count = len(issues)
score = max(0, 100 - issue_count * 15)
return {
"score": score,
"issues": issues,
"recommendations": recommendations,
"overall_score": score,
"passed": score >= self.quality_threshold["structure_score"]
}
def assess_production_feasibility(self, cavity_data: Dict, params: Dict) -> Dict[str, Any]:
"""
评估生产可行性
评估项目:
- 注塑压力
- 锁模力
- 成型周期
- 材料利用率
"""
analysis = cavity_data.get("analysis", {})
# 计算投影面积 (mm²)
bbox = analysis.get("bounding_box", {}).get("dimensions", [0, 0, 0])
projected_area = bbox[0] * bbox[1] # X * Y
# 体积 (mm³)
volume = analysis.get("volume", 0)
volume_cm3 = volume / 1000
# 1. 注塑压力估算 (MPa)
injection_pressure = 30 + projected_area / 1000 # 简化估算
# 2. 锁模力估算 (吨)
# 铝泡沫需要较低的压力
clamping_force_ton = projected_area * 0.0015 # 简化估算
# 3. 成型周期估算 (秒)
# 铝泡沫成型周期较长
if volume_cm3 < 10:
cycle_time = 60
elif volume_cm3 < 50:
cycle_time = 90
elif volume_cm3 < 200:
cycle_time = 120
else:
cycle_time = 180
# 4. 材料利用率
material_utilization = min(95, 85 + volume_cm3 / 10)
# 评估结果
feasibility_items = []
if injection_pressure < 100:
feasibility_items.append({
"item": "注塑压力",
"value": f"{injection_pressure:.1f} MPa",
"status": "ok",
"message": "压力在设备范围内"
})
else:
feasibility_items.append({
"item": "注塑压力",
"value": f"{injection_pressure:.1f} MPa",
"status": "warning",
"message": "压力较高,需要高压设备"
})
if clamping_force_ton < 300:
feasibility_items.append({
"item": "锁模力",
"value": f"{clamping_force_ton:.1f} 吨",
"status": "ok",
"message": "锁模力在设备范围内"
})
else:
feasibility_items.append({
"item": "锁模力",
"value": f"{clamping_force_ton:.1f} 吨",
"status": "warning",
"message": "需要大型注塑机"
})
feasibility_items.append({
"item": "成型周期",
"value": f"{cycle_time} 秒",
"status": "ok",
"message": "周期正常"
})
feasibility_items.append({
"item": "材料利用率",
"value": f"{material_utilization:.1f}%",
"status": "ok",
"message": "材料利用率良好" if material_utilization > 80 else "材料利用率偏低"
})
# 综合评分
ok_count = sum(1 for item in feasibility_items if item["status"] == "ok")
score = (ok_count / len(feasibility_items)) * 100
return {
"items": feasibility_items,
"score": score,
"projected_area": f"{projected_area:.0f} mm²",
"volume": f"{volume_cm3:.1f} cm³",
"injection_pressure": f"{injection_pressure:.1f} MPa",
"clamping_force": f"{clamping_force_ton:.1f} 吨",
"cycle_time": f"{cycle_time} 秒",
"material_utilization": f"{material_utilization:.1f}%",
"passed": score >= 75.0
}
def generate_quality_report(self, cavity_data: Dict, params: Dict) -> str:
"""
生成质量检测报告文本
Returns:
Markdown 格式的报告文本
"""
report = self.inspect_mold(cavity_data, params)
lines = [
"# 铝泡沫模具质量检测报告",
"",
f"**综合评分**: {report['overall_score']:.1f}%",
f"**检测结果**: {'✅ 通过' if report['passed'] else '❌ 未通过'}",
"",
"## 一、分模面质量",
"",
f"- 平滑度: {report['surface_quality']['smoothness']['score']:.1f}分",
f"- 连续性: {report['surface_quality']['continuity']['score']:.1f}分",
f"- 完整性: {report['surface_quality']['completeness']['score']:.1f}分",
"",
]
# 添加问题列表
if report["surface_quality"]["smoothness"].get("issues"):
lines.append("**发现的问题**:")
for issue in report["surface_quality"]["smoothness"]["issues"]:
lines.append(f"- {issue}")
lines.append("")
# 添加结构质量
lines.extend([
"## 二、模具结构质量",
"",
f"- 评分: {report['structure_quality']['score']:.1f}分",
"",
])
if report["structure_quality"].get("issues"):
lines.append("**结构问题**:")
for issue in report["structure_quality"]["issues"]:
lines.append(f"- {issue}")
lines.append("")
if report["structure_quality"].get("recommendations"):
lines.append("**改进建议**:")
for rec in report["structure_quality"]["recommendations"]:
lines.append(f"- {rec}")
lines.append("")
# 添加生产可行性
lines.extend([
"## 三、生产可行性",
"",
])
for item in report["feasibility"]["items"]:
status_icon = "✅" if item["status"] == "ok" else "⚠️"
lines.append(f"{status_icon} **{item['item']}**: {item['value']} - {item['message']}")
lines.append("")
return "\n".join(lines)
@@ -0,0 +1,588 @@
"""
冷却/浇注系统自动设计模块
功能:
1. 冷却系统设计 - 水路布局、直径、间距
2. 浇注系统设计 - 主流道、分流道、浇口
3. 热力学估算 - 冷却时间、温度分布
4. 排气系统设计 - 排气槽、排气针位置
设计依据:
- 模具尺寸和产品几何
- 材料热物性参数
- 生产节拍要求
- 行业标准规范
"""
from typing import Dict, List, Any, Optional
import math
from shared.utils.logger import get_logger
logger = get_logger(__name__)
class MaterialThermalDB:
"""材料热物性数据库"""
PLASTICS = {
"ABS": {"density": 1.05, "specific_heat": 1.47, "thermal_cond": 0.17,
"melt_temp": 230, "mold_temp": 60, "eject_temp": 85},
"PP": {"density": 0.90, "specific_heat": 1.90, "thermal_cond": 0.15,
"melt_temp": 220, "mold_temp": 40, "eject_temp": 80},
"PC": {"density": 1.20, "specific_heat": 1.25, "thermal_cond": 0.20,
"melt_temp": 300, "mold_temp": 80, "eject_temp": 120},
"PE": {"density": 0.95, "specific_heat": 2.30, "thermal_cond": 0.50,
"melt_temp": 200, "mold_temp": 30, "eject_temp": 70},
"PS": {"density": 1.05, "specific_heat": 1.34, "thermal_cond": 0.12,
"melt_temp": 220, "mold_temp": 50, "eject_temp": 80},
"PA": {"density": 1.14, "specific_heat": 1.70, "thermal_cond": 0.25,
"melt_temp": 260, "mold_temp": 70, "eject_temp": 100},
"POM": {"density": 1.42, "specific_heat": 1.47, "thermal_cond": 0.31,
"melt_temp": 200, "mold_temp": 70, "eject_temp": 100},
"PMMA": {"density": 1.18, "specific_heat": 1.47, "thermal_cond": 0.19,
"melt_temp": 240, "mold_temp": 60, "eject_temp": 90},
}
FOAM = {
"AlSi10Mg": {"density": 0.45, "specific_heat": 0.90, "thermal_cond": 0.05,
"melt_temp": 380, "mold_temp": 150, "eject_temp": 200},
"AlSi12": {"density": 0.50, "specific_heat": 0.88, "thermal_cond": 0.06,
"melt_temp": 360, "mold_temp": 140, "eject_temp": 190},
}
COOLANT = {
"water": {"specific_heat": 4.18, "density": 1.0, "thermal_cond": 0.60},
"oil": {"specific_heat": 2.00, "density": 0.85, "thermal_cond": 0.15},
}
@classmethod
def get_material(cls, material: str) -> Optional[Dict]:
if material in cls.PLASTICS:
return cls.PLASTICS[material]
if material in cls.FOAM:
return cls.FOAM[material]
return None
class CoolingSystemDesigner:
"""冷却系统设计器"""
def design_cooling_system(self, mold_size: Dict, product_bbox: Dict,
material: str = "ABS",
cavity_count: int = 1,
cycle_time_target: Optional[float] = None) -> Dict[str, Any]:
"""
设计冷却系统
Args:
mold_size: {"length": L, "width": W, "height": H}
product_bbox: {"dimensions": [dx, dy, dz]}
material: 材料名称
cavity_count: 型腔数量
cycle_time_target: 目标成型周期(秒)
Returns:
冷却系统设计方案
"""
logger.info(f"开始冷却系统设计: 材料={material}, {cavity_count}穴")
mat_props = MaterialThermalDB.get_material(material)
if mat_props is None:
mat_props = MaterialThermalDB.PLASTICS["ABS"]
logger.warning(f"未知材料 {material},使用 ABS 默认参数")
dims = product_bbox.get("dimensions", [100, 100, 50])
max_wall = max(dims) * 0.6
cooling_time = self._estimate_cooling_time(
max_wall, mat_props, mold_size.get("height", 100)
)
layout = self._design_channel_layout(mold_size, dims, cavity_count)
channels = self._generate_channel_positions(layout, mold_size, dims)
flow_rate = self._calculate_flow_rate(channels, mat_props)
thermal_check = self._check_thermal_performance(
cooling_time, channels, mat_props, mold_size, cycle_time_target
)
return {
"cooling_time": round(cooling_time, 1),
"channels": channels,
"layout": layout,
"flow_rate": flow_rate,
"thermal_check": thermal_check,
"material_properties": mat_props,
"recommendations": self._generate_cooling_recommendations(
cooling_time, thermal_check, channels, cycle_time_target
),
}
def _estimate_cooling_time(self, max_wall_thickness: float,
mat_props: Dict, mold_height: float) -> float:
"""估算冷却时间(基于一维热传导简化模型)"""
k = mat_props["thermal_cond"]
rho = mat_props["density"] * 1000
cp = mat_props["specific_heat"] * 1000
alpha = k / (rho * cp)
t_melt = mat_props["melt_temp"]
t_mold = mat_props["mold_temp"]
t_eject = mat_props["eject_temp"]
if t_melt <= t_eject:
return 10.0
theta = (t_eject - t_mold) / (t_melt - t_mold) if (t_melt - t_mold) != 0 else 0.5
theta = max(0.01, min(0.99, abs(theta)))
L = max_wall_thickness / 1000.0
cooling_time = (L ** 2 / (alpha * math.pi ** 2)) * math.log(4 / (math.pi * theta))
return max(5.0, cooling_time)
def _design_channel_layout(self, mold_size: Dict, dims: List[float],
cavity_count: int) -> Dict:
"""设计水路布局方案"""
length = mold_size.get("length", 300)
width = mold_size.get("width", 300)
channel_diameter = 8.0
channel_spacing = 30.0
wall_distance = 15.0
num_channels_length = max(2, int((width - 2 * wall_distance) / channel_spacing))
num_channels_width = max(2, int((length - 2 * wall_distance) / channel_spacing))
if cavity_count <= 4:
layout_type = "straight"
num_channels = num_channels_length
else:
layout_type = "spiral"
num_channels = max(num_channels_length, num_channels_width)
return {
"type": layout_type,
"diameter": channel_diameter,
"spacing": channel_spacing,
"wall_distance": wall_distance,
"num_channels": num_channels,
"num_channels_length": num_channels_length,
"num_channels_width": num_channels_width,
}
def _generate_channel_positions(self, layout: Dict, mold_size: Dict,
dims: List[float]) -> List[Dict]:
"""生成水路位置"""
channels = []
length = mold_size.get("length", 300)
width = mold_size.get("width", 300)
wall_dist = layout["wall_distance"]
diameter = layout["diameter"]
if layout["type"] == "straight":
num = layout["num_channels_length"]
spacing = (width - 2 * wall_dist) / max(num - 1, 1)
for i in range(num):
y = wall_dist + i * spacing - width / 2
channels.append({
"id": i + 1,
"type": "straight",
"start": [-length / 2 + wall_dist, y, 0],
"end": [length / 2 - wall_dist, y, 0],
"diameter": diameter,
"side": "A" if i % 2 == 0 else "B",
})
else:
num = layout["num_channels"]
for i in range(num):
offset = (i - (num - 1) / 2) * layout["spacing"]
channels.append({
"id": i + 1,
"type": "spiral",
"center": [0, offset, 0],
"radius": min(length, width) / 2 - wall_dist,
"diameter": diameter,
"side": "A" if i % 2 == 0 else "B",
})
return channels
def _calculate_flow_rate(self, channels: List[Dict],
mat_props: Dict) -> Dict:
"""计算冷却液流量"""
total_length = 0
diameter = 8.0
for ch in channels:
if ch["type"] == "straight":
start = ch["start"]
end = ch["end"]
total_length += math.sqrt(sum((s - e) ** 2 for s, e in zip(start, end)))
elif ch["type"] == "spiral":
total_length += 2 * math.pi * ch.get("radius", 100)
velocity = 1.5
area = math.pi * (diameter / 2 / 1000) ** 2
flow_rate_lpm = velocity * area * 60000
reynolds = 1000 * velocity * (diameter / 1000) / 0.001
return {
"velocity_m_s": velocity,
"flow_rate_lpm": round(flow_rate_lpm, 1),
"total_channel_length": round(total_length, 1),
"reynolds_number": round(reynolds, 0),
"flow_regime": "turbulent" if reynolds > 4000 else "laminar",
}
def _check_thermal_performance(self, cooling_time: float,
channels: List[Dict],
mat_props: Dict,
mold_size: Dict,
target_cycle: Optional[float]) -> Dict:
"""检查热力学性能"""
num_channels = len(channels)
total_heat = mat_props["specific_heat"] * mat_props["density"] * 100
heat_removal_rate = num_channels * 0.5 * 4.18 * 1.5 * 10
adequacy = "adequate" if num_channels >= 4 else "insufficient"
if target_cycle is not None:
if cooling_time <= target_cycle * 0.6:
adequacy = "excellent"
elif cooling_time <= target_cycle * 0.8:
adequacy = "adequate"
else:
adequacy = "insufficient"
return {
"cooling_time": round(cooling_time, 1),
"estimated_heat_removal_rate": round(heat_removal_rate, 1),
"channel_count": num_channels,
"adequacy": adequacy,
}
def _generate_cooling_recommendations(self, cooling_time: float,
thermal_check: Dict,
channels: List[Dict],
target_cycle: Optional[float]) -> List[str]:
"""生成冷却系统建议"""
recs = []
if thermal_check["adequacy"] == "insufficient":
recs.append("冷却能力不足,建议增加水路数量或增大水路直径")
recs.append("考虑使用铍铜镶件提高局部冷却效率")
if cooling_time > 30:
recs.append("冷却时间较长,建议优化水路布局使水路更靠近型腔")
if len(channels) < 4:
recs.append("水路数量偏少,建议至少4条水路")
flow_regime = "turbulent"
if flow_regime == "laminar":
recs.append("冷却液流速偏低,建议提高流速以达到湍流状态(Re>4000)")
if not recs:
recs.append("冷却系统设计合理,建议进行热分析验证")
return recs
class GatingSystemDesigner:
"""浇注系统设计器"""
def design_gating_system(self, product_bbox: Dict, material: str = "ABS",
cavity_count: int = 1,
gate_type: str = "auto",
layout_positions: Optional[List] = None) -> Dict[str, Any]:
"""
设计浇注系统
Args:
product_bbox: {"dimensions": [dx, dy, dz]}
material: 材料名称
cavity_count: 型腔数量
gate_type: 浇口类型 (auto/side/center/submarine/fan)
layout_positions: 型腔位置列表
Returns:
浇注系统设计方案
"""
logger.info(f"开始浇注系统设计: 材料={material}, {cavity_count}穴, 浇口={gate_type}")
mat_props = MaterialThermalDB.get_material(material)
if mat_props is None:
mat_props = MaterialThermalDB.PLASTICS["ABS"]
dims = product_bbox.get("dimensions", [100, 100, 50])
if gate_type == "auto":
gate_type = self._recommend_gate_type(dims, cavity_count)
sprue = self._design_sprue(dims, mat_props)
runner = self._design_runner(dims, cavity_count, layout_positions)
gate = self._design_gate(dims, gate_type, cavity_count, mat_props)
venting = self._design_venting(dims, cavity_count)
return {
"sprue": sprue,
"runner": runner,
"gate": gate,
"gate_type": gate_type,
"venting": venting,
"material": material,
"recommendations": self._generate_gating_recommendations(
gate_type, cavity_count, dims, mat_props
),
}
def _recommend_gate_type(self, dims: List[float], cavity_count: int) -> str:
"""推荐浇口类型"""
aspect = max(dims[:2]) / min(dims[:2]) if min(dims[:2]) > 0 else 1
if cavity_count == 1:
if aspect > 2:
return "side"
return "center"
else:
return "side"
def _design_sprue(self, dims: List[float], mat_props: Dict) -> Dict:
"""设计主流道"""
max_dim = max(dims)
volume = dims[0] * dims[1] * dims[2]
if volume > 500000:
sprue_d_top = 4.0
sprue_d_bottom = 8.0
elif volume > 50000:
sprue_d_top = 3.0
sprue_d_bottom = 6.0
else:
sprue_d_top = 2.5
sprue_d_bottom = 5.0
sprue_length = max_dim * 0.5 + 20
taper_angle = math.degrees(
math.atan((sprue_d_bottom / 2 - sprue_d_top / 2) / sprue_length)
)
return {
"diameter_top": sprue_d_top,
"diameter_bottom": sprue_d_bottom,
"length": round(sprue_length, 1),
"taper_angle": round(taper_angle, 2),
"volume": round(
math.pi / 3 * sprue_length * (
(sprue_d_top / 2) ** 2 + (sprue_d_top / 2) * (sprue_d_bottom / 2) + (sprue_d_bottom / 2) ** 2
), 1
),
}
def _design_runner(self, dims: List[float], cavity_count: int,
positions: Optional[List]) -> Dict:
"""设计分流道"""
if cavity_count <= 1:
return {
"type": "none",
"diameter": 0,
"total_length": 0,
"volume": 0,
}
runner_diameter = max(4.0, min(dims[:2]) * 0.04)
if positions and len(positions) > 1:
total_length = 0
for pos in positions:
total_length += 2 * math.sqrt(pos[0] ** 2 + pos[1] ** 2)
else:
total_length = cavity_count * max(dims[:2]) * 1.5
cross_area = math.pi * (runner_diameter / 2) ** 2
return {
"type": "trapezoid",
"diameter": round(runner_diameter, 1),
"total_length": round(total_length, 1),
"volume": round(cross_area * total_length, 1),
"cross_section": {
"top_width": round(runner_diameter * 1.2, 1),
"bottom_width": round(runner_diameter * 0.8, 1),
"depth": round(runner_diameter * 0.9, 1),
},
}
def _design_gate(self, dims: List[float], gate_type: str,
cavity_count: int, mat_props: Dict) -> Dict:
"""设计浇口"""
min_dim = min(dims[:2])
wall_thickness = dims[2] * 0.6
if gate_type == "center":
gate_diameter = max(1.0, wall_thickness * 0.5)
return {
"type": "center",
"diameter": round(gate_diameter, 1),
"length": 1.5,
"position": "top_center",
}
elif gate_type == "submarine":
gate_diameter = max(0.8, wall_thickness * 0.3)
return {
"type": "submarine",
"diameter": round(gate_diameter, 1),
"length": 2.0,
"angle": 45,
"position": "bottom_side",
}
elif gate_type == "fan":
return {
"type": "fan",
"width": round(min_dim * 0.3, 1),
"depth": round(wall_thickness * 0.5, 1),
"length": 1.5,
"position": "side",
}
else:
gate_diameter = max(1.0, wall_thickness * 0.4)
return {
"type": "side",
"diameter": round(gate_diameter, 1),
"length": 2.0,
"position": "side_center",
}
def _design_venting(self, dims: List[float], cavity_count: int) -> Dict:
"""设计排气系统"""
volume = dims[0] * dims[1] * dims[2]
if volume > 500000:
vent_count = max(4, cavity_count * 2)
vent_depth = 0.03
vent_width = 8.0
elif volume > 50000:
vent_count = max(2, cavity_count)
vent_depth = 0.02
vent_width = 5.0
else:
vent_count = cavity_count
vent_depth = 0.015
vent_width = 3.0
return {
"type": "vent_slot",
"count": vent_count,
"depth_mm": vent_depth,
"width_mm": vent_width,
"length_mm": 10.0,
"positions": "parting_line",
}
def _generate_gating_recommendations(self, gate_type: str, cavity_count: int,
dims: List[float], mat_props: Dict) -> List[str]:
"""生成浇注系统建议"""
recs = []
if cavity_count > 1:
recs.append("多型腔模具建议使用平衡式流道布局")
if mat_props.get("melt_temp", 0) > 260:
recs.append("高熔点材料,建议使用热流道系统减少废料")
if gate_type == "center":
recs.append("中心浇口适用于单型腔,注意浇口痕处理")
elif gate_type == "side":
recs.append("侧浇口适用于多型腔,需注意流动平衡")
aspect = max(dims[:2]) / min(dims[:2]) if min(dims[:2]) > 0 else 1
if aspect > 3:
recs.append("产品长宽比大,建议使用多点进浇或扇形浇口")
if not recs:
recs.append("浇注系统设计合理,建议进行模流分析验证")
return recs
class MoldSystemDesigner:
"""模具系统综合设计器(冷却+浇注)"""
def __init__(self):
self.cooling_designer = CoolingSystemDesigner()
self.gating_designer = GatingSystemDesigner()
def design_complete_system(self, mold_size: Dict, product_bbox: Dict,
material: str = "ABS",
cavity_count: int = 1,
gate_type: str = "auto",
cycle_time_target: Optional[float] = None,
layout_positions: Optional[List] = None) -> Dict[str, Any]:
"""
综合设计冷却和浇注系统
Returns:
{
"cooling": Dict,
"gating": Dict,
"overall_assessment": Dict,
"recommendations": List[str]
}
"""
cooling = self.cooling_designer.design_cooling_system(
mold_size, product_bbox, material, cavity_count, cycle_time_target
)
gating = self.gating_designer.design_gating_system(
product_bbox, material, cavity_count, gate_type, layout_positions
)
cooling_time = cooling["cooling_time"]
gating_fill_time = self._estimate_fill_time(product_bbox, material)
total_cycle = cooling_time + gating_fill_time + 5.0
assessment = {
"estimated_cycle_time": round(total_cycle, 1),
"cooling_time": cooling_time,
"fill_time": round(gating_fill_time, 1),
"ejection_time": 3.0,
"buffer_time": 2.0,
"meets_target": True if cycle_time_target is None else total_cycle <= cycle_time_target,
}
all_recs = cooling.get("recommendations", []) + gating.get("recommendations", [])
if assessment["meets_target"] is False:
all_recs.insert(0, f"成型周期({total_cycle:.0f}s)超出目标({cycle_time_target}s),需优化冷却系统")
return {
"cooling": cooling,
"gating": gating,
"overall_assessment": assessment,
"recommendations": all_recs,
}
def _estimate_fill_time(self, product_bbox: Dict, material: str) -> float:
"""估算填充时间"""
dims = product_bbox.get("dimensions", [100, 100, 50])
volume = dims[0] * dims[1] * dims[2]
mat_props = MaterialThermalDB.get_material(material)
if mat_props is None:
mat_props = MaterialThermalDB.PLASTICS["ABS"]
fill_rate = 50.0
fill_time = volume / fill_rate
return max(0.5, min(fill_time, 10.0))
@@ -0,0 +1,343 @@
from typing import Dict, Any, List, Optional
from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeFace
from OCC.Core.BRepGProp import brepgprop
from OCC.Core.GProp import GProp_GProps
from OCC.Core.gp import gp_Dir, gp_Pln, gp_Pnt
from OCC.Core.TopAbs import TopAbs_FACE
from OCC.Core.TopExp import TopExp_Explorer
from OCC.Core.TopoDS import TopoDS_Face, TopoDS_Shape, topods
from moldinsight.core.mold_generator import MoldCavityGenerator
from moldinsight.core.aluminum_foam_mold import AluminumFoamMoldGenerator
from moldinsight.core.parting_candidate_generator import PartingCandidateGenerator
from moldinsight.core.parting_scheme_scorer import PartingSchemeScorer
from shared.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: TopoDS_Shape,
material: Dict[str, Any],
is_foam_material: bool = False,
max_schemes: int = 3,
process_params: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
generator = self.aluminum_foam_generator if is_foam_material else self.mold_generator
generator.set_material(material["name"])
self._apply_process_params(generator, material, process_params)
analysis = generator._analyze_product_geometry(shape)
analysis["axis_normal_stats"] = self._collect_axis_normal_stats(generator, shape)
candidates = self.candidate_generator.generate_candidates(
analysis=analysis,
is_foam_material=is_foam_material,
max_candidates=max_schemes,
)
schemes = []
for candidate in candidates:
for offset_variant in self._build_offset_variants(candidate, is_foam_material):
try:
scheme = self._build_scheme(
generator=generator,
shape=shape,
analysis=analysis,
candidate=offset_variant,
is_foam_material=is_foam_material,
)
if scheme is not None:
schemes.append(scheme)
except Exception as exc:
logger.warning(f"候选方案 {offset_variant.get('scheme_id')} 生成失败: {exc}")
if not schemes:
raise ValueError("未能生成任何可用分模方案")
scored_schemes = self.scheme_scorer.score_schemes(schemes)[:max_schemes]
export_shapes = {}
for idx, scheme in enumerate(scored_schemes, start=1):
scheme["raw_scheme_id"] = scheme.get("scheme_id")
scheme["scheme_id"] = f"scheme_{idx}"
if scheme.get("cavity_data", {}).get("metadata") is not None:
scheme["cavity_data"]["metadata"]["scheme_id"] = scheme["scheme_id"]
scheme["cavity_data"]["metadata"]["process_parameters"] = dict(process_params or {})
export_shapes[scheme["scheme_id"]] = scheme.pop("_export_shapes", {})
best_scheme = scored_schemes[0]
return {
"best_scheme_id": best_scheme["scheme_id"],
"candidate_schemes": scored_schemes,
"_export_shapes": export_shapes,
"global_summary": {
"scheme_count": len(scored_schemes),
"recommended_reason": best_scheme.get("summary", ""),
},
}
def _build_scheme(
self,
generator: Any,
shape: TopoDS_Shape,
analysis: Dict[str, Any],
candidate: Dict[str, Any],
is_foam_material: bool,
) -> Optional[Dict[str, Any]]:
parting_surface = self._build_parting_surface(
generator,
analysis,
candidate["direction"],
shape,
candidate.get("offset_ratio", 0.0),
candidate.get("opening_span_mm"),
)
parting_line = generator.optimize_parting_line(
generator._calculate_parting_line(shape, parting_surface)
)
parting_direction = candidate["direction"]
side_action_result = generator.side_action_designer.analyze_and_design(
shape=shape,
parting_direction=parting_direction,
mold_size=generator._calculate_mold_size(analysis),
parting_surface=parting_surface,
)
undercut_regions = generator._build_undercut_regions(
side_action_result.get("undercut_analysis", {})
)
mold_structure = self._determine_mold_structure(analysis, undercut_regions)
scaled_shape = generator._apply_shrinkage_compensation(shape)
drafted_shape = generator._apply_draft_angles(scaled_shape, parting_surface)
cavity, core = generator._split_cavity_core(drafted_shape, parting_surface)
cavity_result = {
"cavity": cavity,
"core": core,
"parting_surface": parting_surface,
"parting_line": parting_line,
"analysis": analysis,
"undercut_regions": undercut_regions,
"side_actions": side_action_result,
}
if is_foam_material:
cavity_result["mold_block"] = generator._generate_mold_block(cavity, analysis)
cavity_result["parting_surfaces"] = {
"primary_surface": parting_surface,
"primary_line": parting_line,
"primary_direction": parting_direction,
"method": candidate["method"],
"offset_ratio": candidate.get("offset_ratio", 0.0),
}
cavity_result["material"] = generator.foam_material
cavity_result["shrinkage_applied"] = generator.shrinkage_rate
cavity_result["draft_angle_applied"] = generator.draft_angle
cavity_data = generator.generate_detailed_cavity_json(cavity_result)
key_info = generator.generate_cavity_key_info(cavity_result)
cavity_data.setdefault("metadata", {})
cavity_data["metadata"]["scheme_id"] = candidate["scheme_id"]
cavity_data["metadata"]["scheme_method"] = candidate["method"]
cavity_data["metadata"]["scheme_axis"] = candidate["axis"]
cavity_data["metadata"]["scheme_reason"] = candidate["reason"]
cavity_data["metadata"]["scheme_offset_ratio"] = candidate.get("offset_ratio", 0.0)
cavity_data["metadata"]["scheme_offset_label"] = candidate.get("offset_label", "中面")
cavity_data["metadata"]["mold_structure_type"] = mold_structure["mold_structure_type"]
cavity_data["metadata"]["core_required"] = mold_structure["core_required"]
cavity_data["metadata"]["structure_decision_reason"] = mold_structure["decision_reason"]
return {
"scheme_id": candidate["scheme_id"],
"method": candidate["method"],
"axis": candidate["axis"],
"title": candidate["title"],
"reason": candidate["reason"],
"priority_score": candidate.get("priority_score"),
"normal_alignment_score": candidate.get("normal_alignment_score"),
"offset_ratio": candidate.get("offset_ratio", 0.0),
"offset_label": candidate.get("offset_label", "中面"),
"mold_structure_type": mold_structure["mold_structure_type"],
"core_required": mold_structure["core_required"],
"decision_reason": mold_structure["decision_reason"],
"parting": {
"axis": candidate["axis"],
"direction": parting_direction,
"line": parting_line,
"surface": cavity_data.get("parting_surface", {}),
},
"undercut_regions": undercut_regions,
"side_actions": side_action_result,
"cavity_data": cavity_data,
"key_info": key_info,
"_export_shapes": {
"cavity": cavity,
"core": core,
"parting_surface": parting_surface,
},
}
@staticmethod
def _apply_process_params(generator: Any, material: Dict[str, Any], process_params: Optional[Dict[str, Any]]):
params = process_params or {}
draft_angle = float(params.get("draft_angle", getattr(generator, "draft_angle", 2.0)))
shrinkage_rate = float(params.get("shrinkage_rate", material.get("shrinkage", 0.005) * 100.0)) / 100.0
parting_precision = float(params.get("parting_precision", getattr(generator, "parting_line_tolerance", 0.1)))
cavity_match = float(params.get("cavity_match", getattr(generator, "cavity_match_rate", 95.0)))
generator.draft_angle = draft_angle
generator.shrinkage_rate = shrinkage_rate
generator.parting_line_tolerance = parting_precision
generator.cavity_match_rate = cavity_match
def _build_parting_surface(
self,
generator: Any,
analysis: Dict[str, Any],
direction_vector: List[float],
shape: TopoDS_Shape,
offset_ratio: float = 0.0,
opening_span_mm: Optional[float] = None,
) -> TopoDS_Face:
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: TopoDS_Shape) -> Dict[str, float]:
"""按坐标轴统计面法向分布强度,用于候选方向排序。"""
stats = {"X": 0.0, "Y": 0.0, "Z": 0.0}
explorer = TopExp_Explorer(shape, TopAbs_FACE)
while explorer.More():
face = topods.Face(explorer.Current())
explorer.Next()
try:
normal = generator._get_face_normal(face)
if normal is None:
continue
props = GProp_GProps()
brepgprop.SurfaceProperties(face, props)
area = max(float(props.Mass()), 1.0)
stats["X"] += abs(float(normal.X())) * area
stats["Y"] += abs(float(normal.Y())) * area
stats["Z"] += abs(float(normal.Z())) * area
except Exception as exc:
logger.debug(f"统计面法向失败: {exc}")
total = stats["X"] + stats["Y"] + stats["Z"]
if total <= 0:
return {"X": 33.3, "Y": 33.3, "Z": 33.4}
return {
axis: round(value / total * 100, 2)
for axis, value in stats.items()
}
@staticmethod
def _determine_mold_structure(analysis: Dict[str, Any], undercut_regions: List[Dict[str, Any]]) -> Dict[str, Any]:
"""
判定是否需要独立模芯。
规则为工程启发式:
- 实心度高 + 平均厚度占比高 + 无明显倒扣:倾向两板半腔(无独立凸芯)
- 否则:采用型腔+模芯结构
"""
dims = analysis.get("bounding_box", {}).get("dimensions", [0.0, 0.0, 0.0])
valid_dims = [float(d) for d in dims if float(d) > 1e-6]
min_dim = min(valid_dims) if valid_dims else 1.0
bbox_volume = 1.0
for dim in valid_dims[:3]:
bbox_volume *= dim
if bbox_volume <= 0:
bbox_volume = 1.0
volume = float(analysis.get("volume", 0.0))
surface_area = float(analysis.get("surface_area", 0.0))
solid_ratio = max(0.0, min(volume / bbox_volume, 1.0))
avg_wall = (2.0 * volume / surface_area) if surface_area > 1e-6 else min_dim
wall_ratio = max(0.0, min(avg_wall / max(min_dim, 1e-6), 1.0))
undercut_count = len(undercut_regions or [])
core_required = not (solid_ratio > 0.62 and wall_ratio > 0.38 and undercut_count == 0)
mold_structure_type = "cavity_core" if core_required else "two_half_cavity"
decision_reason = (
f"solid_ratio={solid_ratio:.2f}, wall_ratio={wall_ratio:.2f}, "
f"undercut_count={undercut_count}"
)
return {
"core_required": core_required,
"mold_structure_type": mold_structure_type,
"decision_reason": decision_reason,
}
@@ -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,300 @@
from typing import Dict, Any, List, Optional, Tuple
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)
undercut_priority_bonus = self._build_undercut_priority_bonus(scheme, score_breakdown)
total_score = round(
score_breakdown["manufacturability"] * 0.25
+ score_breakdown["undercut_complexity"] * 0.35
+ score_breakdown["parting_quality"] * 0.15
+ score_breakdown["machining_cost"] * 0.15
+ score_breakdown["risk"] * 0.10
+ undercut_priority_bonus,
2,
)
scored_scheme = dict(scheme)
fallback = self._assess_fallback(scored_scheme, score_breakdown)
scored_scheme["score_breakdown"] = score_breakdown
scored_scheme["score"] = total_score
scored_scheme["undercut_priority_bonus"] = round(undercut_priority_bonus, 2)
scored_scheme["is_fallback"] = fallback["is_fallback"]
scored_scheme["fallback_reason"] = fallback["fallback_reason"]
scored_scheme["dfm_violations"] = self._build_dfm_violations(scored_scheme)
scored_scheme["dfm_violation_count"] = len(scored_scheme["dfm_violations"])
scored_scheme["confidence_score"] = self._build_confidence_score(
total_score,
score_breakdown,
fallback["is_fallback"],
scored_scheme["dfm_violation_count"],
)
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", []))
total_mechanism_count = int(summary.get("total_mechanism_count", slider_count + lifter_count) or 0)
total_undercut_area = float(
(side_actions.get("undercut_analysis", {}) or {}).get("total_undercut_area")
or 0.0
)
has_pneumatic = any(
str(item.get("actuation", "")).lower() == "pneumatic"
for item in side_actions.get("slider_mechanisms", [])
)
undercut_count = len(undercut_regions)
complexity = str(summary.get("complexity", "")).lower()
undercut_penalty = 0.0
if total_mechanism_count > 0:
undercut_penalty += 20.0
undercut_penalty += undercut_count * 10.0
undercut_penalty += slider_count * 6.0
undercut_penalty += lifter_count * 5.0
if has_pneumatic:
undercut_penalty += 10.0
if complexity == "moderate":
undercut_penalty += 6.0
elif complexity == "complex":
undercut_penalty += 14.0
elif complexity == "very_complex":
undercut_penalty += 24.0
undercut_penalty += min(total_undercut_area / 500.0, 12.0)
undercut_complexity = max(20.0, 100.0 - undercut_penalty)
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),
}
@staticmethod
def _build_undercut_priority_bonus(
scheme: Dict[str, Any],
score_breakdown: Dict[str, float],
) -> float:
cavity_data = scheme.get("cavity_data", {})
quality_checks = cavity_data.get("quality_checks", {})
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", []))
total_mechanism_count = int(summary.get("total_mechanism_count", slider_count + lifter_count) or 0)
has_pneumatic = any(
str(item.get("actuation", "")).lower() == "pneumatic"
for item in side_actions.get("slider_mechanisms", [])
)
if total_mechanism_count == 0:
return 18.0
penalty = 12.0 + total_mechanism_count * 4.0
if has_pneumatic:
penalty += 8.0
if float(score_breakdown.get("manufacturability", 0.0)) < 80.0:
penalty += 4.0
return -penalty
def _assess_fallback(self, scheme: Dict[str, Any], score_breakdown: Dict[str, float]) -> Dict[str, Any]:
cavity_data = scheme.get("cavity_data", {})
mold_cavities = cavity_data.get("mold_cavities", {})
cavity_mesh = mold_cavities.get("cavity", {})
core_mesh = mold_cavities.get("core", {})
core_required = bool(scheme.get("core_required", True))
cavity_v = int(cavity_mesh.get("vertex_count", 0) or 0)
core_v = int(core_mesh.get("vertex_count", 0) or 0)
reasons = []
if cavity_v <= 0:
reasons.append("型腔网格为空")
if core_required and core_v <= 0:
reasons.append("型芯网格为空")
if float(score_breakdown.get("manufacturability", 0.0)) < 70.0:
reasons.append("可制造性评分偏低")
return {
"is_fallback": len(reasons) > 0,
"fallback_reason": ";".join(reasons) if reasons else "",
}
@staticmethod
def _build_confidence_score(
total_score: float,
score_breakdown: Dict[str, float],
is_fallback: bool,
dfm_violation_count: int = 0,
) -> float:
confidence = float(total_score)
confidence += (float(score_breakdown.get("manufacturability", 0.0)) - 70.0) * 0.25
confidence += (float(score_breakdown.get("parting_quality", 0.0)) - 70.0) * 0.15
if is_fallback:
confidence -= 18.0
confidence -= min(max(dfm_violation_count, 0) * 3.0, 15.0)
return round(max(20.0, min(99.0, confidence)), 2)
def _build_dfm_violations(self, scheme: Dict[str, Any]) -> List[Dict[str, str]]:
cavity_data = scheme.get("cavity_data", {})
manufacturing_info = cavity_data.get("manufacturing_info", {})
key_info = scheme.get("key_info", {})
geometric = key_info.get("geometric_characteristics", {})
quality = key_info.get("quality_considerations", {})
metadata = cavity_data.get("metadata", {})
violations: List[Dict[str, str]] = []
wall_min, wall_max = self._parse_wall_range(
geometric.get("wall_thickness_range", "")
)
if wall_min is not None and wall_min < 1.2:
violations.append({
"rule": "最小壁厚",
"level": "high",
"message": f"最小壁厚 {wall_min:.2f}mm 偏薄,可能导致短射/强度不足",
})
if wall_max is not None and wall_max > 6.0:
violations.append({
"rule": "最大壁厚",
"level": "medium",
"message": f"最大壁厚 {wall_max:.2f}mm 偏厚,存在缩痕与冷却不均风险",
})
draft_angle = self._parse_first_number(metadata.get("draft_angle"))
if draft_angle and draft_angle < 1.0:
violations.append({
"rule": "拔模角",
"level": "medium",
"message": f"拔模角 {draft_angle:.2f}° 偏小,脱模阻力较大",
})
warpage = str(quality.get("warpage_risk", "")).lower()
if "high" in warpage or "高" in warpage:
violations.append({
"rule": "翘曲风险",
"level": "high",
"message": "当前方案翘曲风险高,建议优化壁厚与浇口位置",
})
clamping_force = self._parse_first_number(
manufacturing_info.get("estimated_clamping_force")
)
if clamping_force > 1200:
violations.append({
"rule": "锁模力",
"level": "medium",
"message": f"预估锁模力 {clamping_force:.0f} 吨,设备适配窗口较窄",
})
return violations
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", "自动计算")
structure_type = scheme.get("mold_structure_type", "cavity_core")
structure_text = "型腔+模芯" if structure_type == "cavity_core" else "两板半腔(无独立模芯)"
return (
f"{axis} 轴开模,结构 {structure_text},分型面位置 {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
@staticmethod
def _parse_wall_range(value: Any) -> Tuple[Optional[float], Optional[float]]:
if value is None:
return None, None
nums = re.findall(r"\d+(?:\.\d+)?", str(value))
if not nums:
return None, None
if len(nums) == 1:
v = float(nums[0])
return v, v
return float(nums[0]), float(nums[1])
@@ -0,0 +1,533 @@
"""
侧壁/倒扣面滑块机构检测与设计模块
功能:
1. 倒扣区域检测 - 识别无法直接脱模的侧壁凹槽
2. 滑块机构设计 - 侧向分型抽芯机构
3. 斜顶机构设计 - 内侧倒扣的斜顶脱模机构
4. 机构运动学分析 - 抽芯行程、脱模角度计算
倒扣检测原理:
- 分型方向确定后,检查每个面的法向量
- 如果面的法向量与脱模方向的点积为负(面朝向脱模反方向)
且该面不在分型面上,则判定为倒扣面
- 根据倒扣面的位置(外侧/内侧)选择滑块或斜顶
滑块 vs 斜顶:
- 滑块:外侧倒扣,沿导滑槽侧向运动
- 斜顶:内侧倒扣,沿斜导柱内侧运动
"""
from typing import Dict, List, Any, Optional, Tuple
import math
import numpy as np
from OCC.Core.TopoDS import TopoDS_Shape, TopoDS_Face
from shared.utils.logger import get_logger
logger = get_logger(__name__)
class UndercutDetector:
"""倒扣区域检测器"""
def detect_undercuts(self, shape: TopoDS_Shape, parting_direction: List[float],
parting_surface: Optional[TopoDS_Face] = None) -> Dict[str, Any]:
"""
检测产品中的倒扣区域
Args:
shape: OCC 产品形状
parting_direction: 分型方向 [nx, ny, nz]
parting_surface: 分型面(可选)
Returns:
{
"undercut_faces": List[Dict],
"slider_regions": List[Dict],
"lifter_regions": List[Dict],
"total_undercut_area": float,
"requires_slider": bool,
"requires_lifter": bool,
"complexity": str
}
"""
try:
from OCC.Core.TopExp import TopExp_Explorer
from OCC.Core.TopAbs import TopAbs_FACE
from OCC.Core.TopoDS import TopoDS_Face, topods
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface
from OCC.Core.GProp import GProp_GProps
from OCC.Core.BRepGProp import brepgprop
from OCC.Core.Bnd import Bnd_Box
from OCC.Core.BRepBndLib import brepbndlib
from OCC.Core.gp import gp_Dir
dir_vec = np.array(parting_direction, dtype=np.float64)
dir_norm = np.linalg.norm(dir_vec)
if dir_norm < 1e-6:
dir_vec = np.array([0, 0, 1])
else:
dir_vec /= dir_norm
parting_dir = gp_Dir(dir_vec[0], dir_vec[1], dir_vec[2])
undercut_faces = []
slider_regions = []
lifter_regions = []
total_undercut_area = 0.0
parting_z = 0.0
if parting_surface is not None:
try:
surface = BRepAdaptor_Surface(parting_surface)
if surface.GetType() == 0:
parting_z = surface.Plane().Location().Z()
except Exception:
pass
explorer = TopExp_Explorer(shape, TopAbs_FACE)
face_idx = 0
while explorer.More():
face = topods.Face(explorer.Current())
face_idx += 1
try:
surface = BRepAdaptor_Surface(face)
u = (surface.FirstUParameter() + surface.LastUParameter()) / 2
v = (surface.FirstVParameter() + surface.LastVParameter()) / 2
face_normal = None
if surface.GetType() == 0:
face_normal = surface.Plane().Position().Direction()
else:
from OCC.Core.BRepLProp import BRepLProp_SLProps
props = BRepLProp_SLProps(surface, 1, 0.001)
props.SetParameters(u, v)
if props.IsNormalDefined():
face_normal = props.Normal()
if face_normal is None:
explorer.Next()
continue
dot = face_normal.Dot(parting_dir)
face_props = GProp_GProps()
brepgprop.SurfaceProperties(face, face_props)
area = face_props.Mass()
center = face_props.CentreOfMass()
bbox = Bnd_Box()
brepbndlib.Add(face, bbox)
try:
fxmin, fymin, fzmin, fxmax, fymax, fzmax = bbox.Get()
except Exception:
fxmin, fymin, fzmin, fxmax, fymax, fzmax = 0, 0, 0, 0, 0, 0
if dot < -0.1:
face_center_z = center.Z()
is_outer = face_center_z >= parting_z
undercut_info = {
"face_index": face_idx,
"normal": [face_normal.X(), face_normal.Y(), face_normal.Z()],
"dot_product": float(dot),
"area": float(area),
"center": [float(center.X()), float(center.Y()), float(center.Z())],
"bbox": {
"min": [float(fxmin), float(fymin), float(fzmin)],
"max": [float(fxmax), float(fymax), float(fzmax)]
},
"severity": "high" if dot < -0.5 else "medium",
"is_outer": is_outer,
}
undercut_faces.append(undercut_info)
total_undercut_area += area
except Exception:
pass
explorer.Next()
for uf in undercut_faces:
normal = np.array(uf["normal"])
lateral_component = normal - np.dot(normal, dir_vec) * dir_vec
lateral_norm = np.linalg.norm(lateral_component)
if lateral_norm > 0.01:
slide_direction = lateral_component / lateral_norm
else:
slide_direction = np.array([1, 0, 0])
mechanism = {
"face_indices": [uf["face_index"]],
"slide_direction": slide_direction.tolist(),
"area": uf["area"],
"center": uf["center"],
"severity": uf["severity"],
}
if uf["is_outer"]:
slider_regions.append(mechanism)
else:
lifter_regions.append(mechanism)
requires_slider = len(slider_regions) > 0
requires_lifter = len(lifter_regions) > 0
total_count = len(slider_regions) + len(lifter_regions)
if total_count == 0:
complexity = "simple"
elif total_count <= 2:
complexity = "moderate"
elif total_count <= 4:
complexity = "complex"
else:
complexity = "very_complex"
result = {
"undercut_faces": undercut_faces,
"slider_regions": slider_regions,
"lifter_regions": lifter_regions,
"total_undercut_area": total_undercut_area,
"requires_slider": requires_slider,
"requires_lifter": requires_lifter,
"complexity": complexity,
"parting_direction": parting_direction,
}
logger.info(f"倒扣检测完成: {len(undercut_faces)} 个倒扣面, "
f"{len(slider_regions)} 个滑块, {len(lifter_regions)} 个斜顶, "
f"复杂度={complexity}")
return result
except Exception as e:
logger.error(f"倒扣检测失败: {e}")
return {
"undercut_faces": [],
"slider_regions": [],
"lifter_regions": [],
"total_undercut_area": 0,
"requires_slider": False,
"requires_lifter": False,
"complexity": "unknown",
"parting_direction": parting_direction,
}
class SliderMechanismDesigner:
"""滑块机构设计器"""
def design_slider(self, slider_region: Dict, mold_size: Dict,
parting_direction: List[float]) -> Dict[str, Any]:
"""
设计滑块机构
Args:
slider_region: 倒扣区域信息
mold_size: 模具尺寸
parting_direction: 分型方向
Returns:
滑块机构设计方案
"""
center = slider_region["center"]
area = slider_region["area"]
slide_dir = slider_region["slide_direction"]
slide_stroke = self._calculate_slide_stroke(slider_region, mold_size)
slide_angle = self._calculate_slide_angle(slide_dir, parting_direction)
slide_block_size = self._calculate_slide_block_size(area, slide_stroke)
guide_type = self._select_guide_type(slide_stroke, slide_angle)
return {
"type": "slider",
"location": center,
"slide_direction": slide_dir,
"slide_stroke": slide_stroke,
"slide_angle": slide_angle,
"block_size": slide_block_size,
"guide_type": guide_type,
"locking_mechanism": self._select_locking(slide_angle),
"actuation": "pneumatic" if slide_stroke > 50 else "mechanical",
"components": self._generate_components(slide_block_size, guide_type),
"manufacturing_notes": self._generate_slider_notes(slide_angle, slide_stroke),
}
def _calculate_slide_stroke(self, region: Dict, mold_size: Dict) -> float:
"""计算抽芯行程"""
bbox = region.get("bbox", {})
if "max" in bbox and "min" in bbox:
max_dim = max(
abs(bbox["max"][0] - bbox["min"][0]),
abs(bbox["max"][1] - bbox["min"][1]),
abs(bbox["max"][2] - bbox["min"][2])
)
else:
max_dim = 10.0
stroke = max_dim + 5.0
return round(max(stroke, 10.0), 1)
def _calculate_slide_angle(self, slide_dir: List[float],
parting_dir: List[float]) -> float:
"""计算滑块倾斜角度"""
s = np.array(slide_dir)
p = np.array(parting_dir)
s_norm = np.linalg.norm(s)
p_norm = np.linalg.norm(p)
if s_norm < 1e-6 or p_norm < 1e-6:
return 90.0
cos_angle = np.clip(np.dot(s, p) / (s_norm * p_norm), -1, 1)
angle = math.degrees(math.acos(abs(cos_angle)))
return round(angle, 1)
def _calculate_slide_block_size(self, area: float, stroke: float) -> Dict[str, float]:
"""计算滑块尺寸"""
width = max(math.sqrt(area) * 1.5, 15.0)
height = max(math.sqrt(area) * 1.2, 12.0)
length = stroke + width * 0.5
return {
"width": round(width, 1),
"height": round(height, 1),
"length": round(length, 1),
}
def _select_guide_type(self, stroke: float, angle: float) -> str:
"""选择导滑方式"""
if stroke > 80:
return "T_slot_guide"
elif angle > 20:
return "angled_guide_pin"
else:
return "dovetail_guide"
def _select_locking(self, angle: float) -> str:
"""选择锁紧方式"""
if angle > 25:
return "wedge_block"
else:
return "lock_block"
def _generate_components(self, block_size: Dict, guide_type: str) -> List[Dict]:
"""生成滑块组件清单"""
components = [
{"name": "slide_block", "material": "P20", "hardness": "HRC 28-32"},
{"name": "guide_strip", "material": "bronze", "hardness": "HB 80-100"},
{"name": "wear_plate", "material": "T8", "hardness": "HRC 45-50"},
{"name": "return_spring", "material": "spring_steel", "spec": "standard"},
]
if guide_type == "T_slot_guide":
components.append({"name": "T_slot_insert", "material": "P20", "hardness": "HRC 28-32"})
elif guide_type == "angled_guide_pin":
components.append({"name": "guide_pin", "material": "SUJ2", "hardness": "HRC 58-62"})
elif guide_type == "dovetail_guide":
components.append({"name": "dovetail_block", "material": "P20", "hardness": "HRC 28-32"})
return components
def _generate_slider_notes(self, angle: float, stroke: float) -> List[str]:
"""生成滑块加工注意事项"""
notes = []
if angle > 25:
notes.append("滑块角度较大,需确保锁紧可靠")
if stroke > 50:
notes.append("抽芯行程较长,建议使用气动抽芯")
if stroke > 80:
notes.append("大行程抽芯,需校核导滑槽强度")
notes.append("滑块需设置限位装置,防止脱出")
notes.append("配合面需做耐磨处理")
return notes
class LifterMechanismDesigner:
"""斜顶机构设计器"""
def design_lifter(self, lifter_region: Dict, mold_size: Dict,
parting_direction: List[float]) -> Dict[str, Any]:
"""
设计斜顶机构
Args:
lifter_region: 内侧倒扣区域信息
mold_size: 模具尺寸
parting_direction: 分型方向
Returns:
斜顶机构设计方案
"""
center = lifter_region["center"]
area = lifter_region["area"]
lifter_angle = self._calculate_lifter_angle(lifter_region)
lifter_stroke = self._calculate_lifter_stroke(lifter_region)
lifter_size = self._calculate_lifter_size(area, lifter_stroke, lifter_angle)
return {
"type": "lifter",
"location": center,
"lifter_angle": lifter_angle,
"lifter_stroke": lifter_stroke,
"block_size": lifter_size,
"guide_type": "angled_hole",
"return_mechanism": "spring_return",
"components": self._generate_lifter_components(lifter_size),
"manufacturing_notes": self._generate_lifter_notes(lifter_angle),
}
def _calculate_lifter_angle(self, region: Dict) -> float:
"""计算斜顶角度(通常5-15度)"""
return 8.0
def _calculate_lifter_stroke(self, region: Dict) -> float:
"""计算斜顶行程"""
bbox = region.get("bbox", {})
if "max" in bbox and "min" in bbox:
max_dim = max(
abs(bbox["max"][i] - bbox["min"][i]) for i in range(3)
)
else:
max_dim = 5.0
return round(max(max_dim + 3.0, 8.0), 1)
def _calculate_lifter_size(self, area: float, stroke: float,
angle: float) -> Dict[str, float]:
"""计算斜顶尺寸"""
width = max(math.sqrt(area) * 1.2, 10.0)
height = stroke / math.sin(math.radians(angle)) if angle > 0 else stroke * 3
thickness = max(width * 0.6, 8.0)
return {
"width": round(width, 1),
"height": round(height, 1),
"thickness": round(thickness, 1),
}
def _generate_lifter_components(self, size: Dict) -> List[Dict]:
"""生成斜顶组件清单"""
return [
{"name": "lifter_body", "material": "P20", "hardness": "HRC 28-32"},
{"name": "guide_pin", "material": "SUJ2", "hardness": "HRC 58-62"},
{"name": "return_spring", "material": "spring_steel", "spec": "standard"},
{"name": "wear_bushing", "material": "bronze", "hardness": "HB 80-100"},
]
def _generate_lifter_notes(self, angle: float) -> List[str]:
"""生成斜顶加工注意事项"""
notes = []
if angle > 12:
notes.append("斜顶角度偏大,需校核脱模力")
notes.append("斜顶导滑孔需精确加工")
notes.append("斜顶头部需做耐磨处理")
notes.append("需设置限位防止斜顶脱出")
return notes
class SideActionDesigner:
"""侧向分型机构综合设计器"""
def __init__(self):
self.undercut_detector = UndercutDetector()
self.slider_designer = SliderMechanismDesigner()
self.lifter_designer = LifterMechanismDesigner()
def analyze_and_design(self, shape: TopoDS_Shape, parting_direction: List[float],
mold_size: Dict, parting_surface: Optional[TopoDS_Face] = None) -> Dict[str, Any]:
"""
综合分析倒扣并设计侧向分型机构
Returns:
{
"undercut_analysis": Dict,
"slider_mechanisms": List[Dict],
"lifter_mechanisms": List[Dict],
"summary": Dict,
"recommendations": List[str]
}
"""
logger.info("开始侧向分型机构分析...")
undercut_result = self.undercut_detector.detect_undercuts(
shape, parting_direction, parting_surface
)
slider_mechanisms = []
for region in undercut_result["slider_regions"]:
slider = self.slider_designer.design_slider(
region, mold_size, parting_direction
)
slider_mechanisms.append(slider)
lifter_mechanisms = []
for region in undercut_result["lifter_regions"]:
lifter = self.lifter_designer.design_lifter(
region, mold_size, parting_direction
)
lifter_mechanisms.append(lifter)
total_mechanisms = len(slider_mechanisms) + len(lifter_mechanisms)
summary = {
"total_undercut_faces": len(undercut_result["undercut_faces"]),
"total_slider_count": len(slider_mechanisms),
"total_lifter_count": len(lifter_mechanisms),
"total_mechanism_count": total_mechanisms,
"complexity": undercut_result["complexity"],
}
recommendations = self._generate_overall_recommendations(summary, undercut_result)
result = {
"undercut_analysis": undercut_result,
"slider_mechanisms": slider_mechanisms,
"lifter_mechanisms": lifter_mechanisms,
"summary": summary,
"recommendations": recommendations,
}
logger.info(f"侧向分型机构设计完成: {len(slider_mechanisms)} 个滑块, "
f"{len(lifter_mechanisms)} 个斜顶")
return result
def _generate_overall_recommendations(self, summary: Dict,
undercut: Dict) -> List[str]:
"""生成总体建议"""
recs = []
if summary["total_mechanism_count"] == 0:
recs.append("无倒扣区域,模具结构简单,无需侧向分型机构")
return recs
if summary["total_slider_count"] > 0:
recs.append(f"需要 {summary['total_slider_count']} 个滑块机构处理外侧倒扣")
if summary["total_lifter_count"] > 0:
recs.append(f"需要 {summary['total_lifter_count']} 个斜顶机构处理内侧倒扣")
if summary["total_slider_count"] > 0:
recs.append("如存在大行程滑块,建议优先评估气动抽芯回路并预留稳定供气")
if summary["complexity"] == "very_complex":
recs.append("侧向分型机构复杂,建议评估是否可通过产品修改简化")
recs.append("考虑使用二次分型或旋转脱模替代方案")
if summary["total_mechanism_count"] > 3:
recs.append("侧向机构较多,建议优化模具结构减少机构数量")
recs.append("所有侧向机构需做运动仿真验证干涉")
return recs
+289
View File
@@ -0,0 +1,289 @@
# core/stp_parser.py
from pathlib import Path
from typing import Dict, Any, Optional, List
import numpy as np
import json
from shared.utils.logger import get_logger
from OCC.Core.GProp import GProp_GProps
from OCC.Core.BRepGProp import brepgprop
from OCC.Core.TopoDS import TopoDS_Shape
logger = get_logger(__name__)
class STPParser:
"""STP文件解析器"""
def __init__(self):
# 强制要求PythonOCC必须可用
self._verify_occ_availability()
def _verify_occ_availability(self):
"""验证PythonOCC是否可用,不可用则抛出异常"""
try:
from OCC.Core.STEPControl import STEPControl_Reader
from OCC.Core.IFSelect import IFSelect_RetDone
logger.info("PythonOCC验证通过")
except ImportError as e:
logger.error("PythonOCC不可用,服务无法运行")
raise RuntimeError("PythonOCC未安装,请安装PythonOCC后再运行服务") from e
def load_step_file(self, file_path: Path) -> TopoDS_Shape:
"""加载STP文件"""
try:
from OCC.Core.STEPControl import STEPControl_Reader
from OCC.Core.IFSelect import IFSelect_RetDone
logger.info(f"加载STP文件: {file_path}")
reader = STEPControl_Reader()
status = reader.ReadFile(str(file_path))
if status == IFSelect_RetDone:
reader.TransferRoots()
shape = reader.OneShape()
logger.info("STP文件加载成功")
return shape
else:
raise ValueError(f"STP文件读取失败,状态码: {status}")
except Exception as e:
logger.error(f"STP解析失败: {e}")
raise
def analyze_geometry(self, shape: TopoDS_Shape) -> Dict[str, Any]:
"""分析几何属性"""
try:
from OCC.Core.GProp import GProp_GProps
from OCC.Core.BRepGProp import brepgprop
from OCC.Core.Bnd import Bnd_Box
from OCC.Core.BRepBndLib import brepbndlib
from OCC.Core.TopExp import TopExp_Explorer
from OCC.Core.TopAbs import TopAbs_FACE, TopAbs_EDGE, TopAbs_VERTEX
logger.info("开始几何分析...")
# 计算边界框
bbox = self._compute_bounding_box(shape)
# 计算体积和表面积
volume = self._compute_volume(shape)
area = self._compute_surface_area(shape)
# 分析拓扑
topology = self._analyze_topology(shape)
# 计算质心
center_of_mass = self._compute_center_of_mass(shape)
# 计算惯性属性
inertia_properties = self._compute_inertia_properties(shape)
result = {
"bounding_box": bbox,
"volume": float(volume),
"surface_area": float(area),
"topology": topology,
"center_of_mass": center_of_mass,
"inertia_properties": inertia_properties,
"analysis_method": "pythonocc"
}
logger.info("几何分析完成")
return result
except Exception as e:
logger.error(f"几何分析失败: {e}")
raise
def _compute_bounding_box(self, shape: TopoDS_Shape) -> Dict[str, Any]:
"""计算边界框"""
try:
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()
return {
"min": [float(xmin), float(ymin), float(zmin)],
"max": [float(xmax), float(ymax), float(zmax)],
"dimensions": [
float(xmax - xmin),
float(ymax - ymin),
float(zmax - zmin)
],
"center": [
float((xmin + xmax) / 2),
float((ymin + ymax) / 2),
float((zmin + zmax) / 2)
]
}
except Exception as e:
logger.error(f"边界框计算失败: {e}")
return self._default_bounding_box()
def _compute_volume(self, shape: TopoDS_Shape) -> float:
"""计算体积"""
try:
from OCC.Core.GProp import GProp_GProps
from OCC.Core.BRepGProp import brepgprop
props = GProp_GProps()
brepgprop.VolumeProperties(shape, props)
volume = props.Mass()
if volume <= 0:
raise ValueError("计算得到的体积为0或负数,形状可能无效")
return volume
except Exception as e:
logger.error(f"体积计算失败: {e}")
raise RuntimeError(f"体积计算失败: {e}") from e
def _compute_surface_area(self, shape: TopoDS_Shape) -> float:
"""计算表面积"""
try:
from OCC.Core.GProp import GProp_GProps
from OCC.Core.BRepGProp import brepgprop
props = GProp_GProps()
brepgprop.SurfaceProperties(shape, props)
area = props.Mass()
# 如果计算结果为0,使用备选估算方法
if area <= 0:
logger.warning("表面积计算结果为0,使用边界框估算")
raise ValueError("Surface area is zero")
return area
except ValueError:
# 基于边界框估算表面积
try:
bbox = self._compute_bounding_box(shape)
dims = bbox.get("dimensions", [0, 0, 0])
if any(d <= 0 for d in dims):
raise RuntimeError("边界框尺寸无效,无法估算表面积")
# 简化的估算公式:2*(lw + lh + wh)
estimated_area = 2 * (dims[0]*dims[1] + dims[0]*dims[2] + dims[1]*dims[2])
logger.warning(f"使用边界框估算表面积: {estimated_area:.2f} mm²")
return estimated_area
except Exception as e:
logger.error(f"表面积估算失败: {e}")
raise RuntimeError(f"表面积计算失败: {e}") from e
except Exception as e:
logger.error(f"表面积计算失败: {e}")
raise RuntimeError(f"表面积计算失败: {e}") from e
def _compute_center_of_mass(self, shape: TopoDS_Shape) -> List[float]:
"""计算质心"""
try:
from OCC.Core.GProp import GProp_GProps
from OCC.Core.BRepGProp import brepgprop
props = GProp_GProps()
brepgprop.VolumeProperties(shape, props)
center = props.CentreOfMass()
return [float(center.X()), float(center.Y()), float(center.Z())]
except Exception as e:
logger.error(f"质心计算失败: {e}")
# 回退到边界框中心
try:
bbox = self._compute_bounding_box(shape)
return bbox.get("center", [0.0, 0.0, 0.0])
except Exception:
raise RuntimeError(f"质心计算失败且边界框回退也失败: {e}") from e
def _compute_inertia_properties(self, shape: TopoDS_Shape) -> Dict[str, Any]:
"""计算惯性属性"""
try:
from OCC.Core.GProp import GProp_GProps
from OCC.Core.BRepGProp import brepgprop
props = GProp_GProps()
brepgprop.VolumeProperties(shape, props)
inertia = props.MatrixOfInertia()
return {
"mass": float(props.Mass()),
"moment_of_inertia": [
[float(inertia.Value(1, 1)), float(inertia.Value(1, 2)), float(inertia.Value(1, 3))],
[float(inertia.Value(2, 1)), float(inertia.Value(2, 2)), float(inertia.Value(2, 3))],
[float(inertia.Value(3, 1)), float(inertia.Value(3, 2)), float(inertia.Value(3, 3))]
]
}
except Exception as e:
logger.error(f"惯性属性计算失败: {e}")
return {}
def _analyze_topology(self, shape: TopoDS_Shape) -> Dict[str, int]:
"""分析拓扑"""
try:
from OCC.Core.TopExp import TopExp_Explorer
from OCC.Core.TopAbs import TopAbs_FACE, TopAbs_EDGE, TopAbs_VERTEX
def count_elements(element_type):
explorer = TopExp_Explorer(shape, element_type)
count = 0
while explorer.More():
count += 1
explorer.Next()
return count
return {
"faces": count_elements(TopAbs_FACE),
"edges": count_elements(TopAbs_EDGE),
"vertices": count_elements(TopAbs_VERTEX)
}
except Exception as e:
logger.error(f"拓扑分析失败: {e}")
raise
def _default_bounding_box(self) -> Dict[str, Any]:
"""默认边界框(边界框计算失败时的回退值,标注为估算)"""
return {
"min": [0.0, 0.0, 0.0],
"max": [0.0, 0.0, 0.0],
"dimensions": [0.0, 0.0, 0.0],
"center": [0.0, 0.0, 0.0],
"estimated": True
}
def export_to_json(self, geometry_data: Dict[str, Any], output_path: Path) -> str:
"""将几何数据导出为JSON文件"""
try:
# 确保输出目录存在
output_path.parent.mkdir(parents=True, exist_ok=True)
# 添加元数据
json_data = {
"metadata": {
"export_time": str(np.datetime64('now')),
"analysis_method": geometry_data.get("analysis_method", "unknown"),
"version": "1.0.0"
},
"geometry_data": geometry_data
}
# 保存JSON文件
with open(output_path, 'w', encoding='utf-8') as f:
json.dump(json_data, f, indent=2, ensure_ascii=False)
logger.info(f"几何数据已导出到: {output_path}")
return str(output_path)
except Exception as e:
logger.error(f"JSON导出失败: {e}")
raise
def get_json_data(self, geometry_data: Dict[str, Any]) -> Dict[str, Any]:
"""获取JSON格式的几何数据"""
return {
"metadata": {
"export_time": str(np.datetime64('now')),
"analysis_method": geometry_data.get("analysis_method", "unknown"),
"version": "1.0.0"
},
"geometry_data": geometry_data
}
+1
View File
@@ -0,0 +1 @@
# Services 模块
@@ -0,0 +1,93 @@
"""
铝金属价格数据服务
提供铝金属的当前价格和历史价格走势数据。
数据来源优先级:
1. 外部API(预留接口)
2. 模拟真实走势数据(当前使用)
数据基于上海期货交易所(SHFE)铝期货价格走势特征生成。
"""
import random
import hashlib
from datetime import datetime, timedelta
from typing import List, Dict, Optional
BASE_PRICE = 18950.0
PRICE_VOLATILITY = 120.0
TREND_DRIFT = 0.3
def _daily_seed(date_str: str) -> float:
h = hashlib.md5(date_str.encode()).hexdigest()
seed = int(h[:8], 16) / (16 ** 8)
return seed
def get_aluminum_current_price() -> Dict:
today = datetime.now().strftime("%Y-%m-%d")
seed = _daily_seed(today)
random.seed(int(seed * 1_000_000))
price = BASE_PRICE + (seed - 0.5) * PRICE_VOLATILITY * 2
price = round(price, 0)
yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
prev_seed = _daily_seed(yesterday)
prev_price = BASE_PRICE + (prev_seed - 0.5) * PRICE_VOLATILITY * 2
prev_price = round(prev_price, 0)
change = price - prev_price
change_percent = round((change / prev_price) * 100, 2)
week_ago = (datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d")
week_seed = _daily_seed(week_ago)
week_price = BASE_PRICE + (week_seed - 0.5) * PRICE_VOLATILITY * 2
random.seed()
return {
"price": price,
"unit": "元/吨",
"currency": "CNY",
"date": today,
"change": round(change, 0),
"change_percent": change_percent,
"open": round(price - random.uniform(10, 50), 0),
"high": round(price + random.uniform(10, 60), 0),
"low": round(price - random.uniform(10, 60), 0),
"prev_close": prev_price,
"week_ago_price": round(week_price, 0),
}
def get_aluminum_price_history(days: int = 30) -> List[Dict]:
history = []
random.seed(42)
price_line = BASE_PRICE
for i in range(days, -1, -1):
date = (datetime.now() - timedelta(days=i)).strftime("%Y-%m-%d")
date_seed = _daily_seed(date)
drift = (date_seed - 0.5) * TREND_DRIFT
noise = (date_seed - 0.5) * PRICE_VOLATILITY * 1.5
price_line = price_line + drift + noise * 0.3
price_line = max(18200, min(19800, price_line))
open_price = round(price_line + (date_seed - 0.5) * 80, 0)
high_price = round(open_price + abs(date_seed - 0.5) * 160, 0)
low_price = round(open_price - abs(date_seed - 0.5) * 140, 0)
close_price = round(price_line, 0)
history.append({
"date": date,
"open": open_price,
"high": high_price,
"low": low_price,
"close": close_price,
})
random.seed()
return history
@@ -0,0 +1,427 @@
# services/calculation_service.py
"""模具工程参数计算服务 — 从 process_file_core 中抽取的纯计算逻辑"""
from typing import Dict, Any, List, Optional
from datetime import datetime
from pathlib import Path
class CalculationService:
"""将 process_file_core 中的工程计算逻辑抽取为独立服务,方便单测和复用"""
# ─── 基础计算 ───
@staticmethod
def calculate_product_weight(volume_mm3: float, density: float) -> float:
"""计算产品重量(克)"""
volume_cm3 = volume_mm3 / 1000
return volume_cm3 * density
@staticmethod
def calculate_projected_area(bbox_dims: List[float], parting_direction: str = "Z") -> float:
"""
计算投影面积(cm²)
Args:
bbox_dims: [长度, 宽度, 高度] (mm)
parting_direction: 开模方向,"Z" 表示上下开模(投影到XY平面),
"Y" 表示前后开模(投影到XZ平面),
"X" 表示左右开模(投影到YZ平面)
"""
if len(bbox_dims) < 3:
return 0.0
if parting_direction == "Z":
# Z轴开模 → 投影面积 = 长度 × 宽度
return (bbox_dims[0] * bbox_dims[1]) / 100
elif parting_direction == "Y":
return (bbox_dims[0] * bbox_dims[2]) / 100
elif parting_direction == "X":
return (bbox_dims[1] * bbox_dims[2]) / 100
# 默认 Z 轴
return (bbox_dims[0] * bbox_dims[1]) / 100
@staticmethod
def calculate_cavity_count(product_weight_g: float, projected_area_cm2: float) -> int:
"""
计算最优型腔数量
基于产品重量和投影面积:
- 小产品(< 50g)可以多型腔
- 大产品(> 1000g)通常单型腔
"""
if product_weight_g < 50:
cavity_count = 8
elif product_weight_g < 100:
cavity_count = 4
elif product_weight_g < 300:
cavity_count = 2
else:
cavity_count = 1
# 根据投影面积调整
if projected_area_cm2 > 400:
cavity_count = 1
elif projected_area_cm2 > 200 and cavity_count > 2:
cavity_count = 2
return cavity_count
@staticmethod
def calculate_clamping_force(
projected_area_cm2: float,
cavity_count: int,
runner_ratio: float = 0.20,
injection_pressure: float = 700,
is_foam: bool = False,
) -> int:
"""
计算所需夹紧力(吨)
塑料模具: 锁模力 = 投影面积 × 型腔数 × (1+流道比) × 注塑压力 / 1000
泡沫模具: 锁模力 = 投影面积(cm²) × 0.3 (泡沫材料系数)
Args:
projected_area_cm2: 投影面积 cm²
cavity_count: 型腔数
runner_ratio: 流道系统占型腔投影面积比(0.15-0.25)
injection_pressure: 注塑压力 kg/cm²
is_foam: 是否泡沫材料
"""
if is_foam:
# 泡沫模具: 锁模力(吨) = 投影面积(cm²) × 0.3
clamping_force_ton = int(projected_area_cm2 * 0.3)
else:
total_projected_area = projected_area_cm2 * cavity_count * (1 + runner_ratio)
clamping_force_ton = int(total_projected_area * injection_pressure / 1000)
return max(50, min(clamping_force_ton, 3000))
@staticmethod
def calculate_wall_thickness(volume_mm3: float, surface_area_mm2: float) -> Dict[str, float]:
"""计算壁厚范围"""
if surface_area_mm2 > 0 and volume_mm3 > 0:
avg = (volume_mm3 / surface_area_mm2) * 0.6
return {
"avg_thickness_mm": avg,
"wall_thickness_min": avg * 0.7,
"wall_thickness_max": avg * 1.3,
}
return {
"avg_thickness_mm": 2.5,
"wall_thickness_min": 2.0,
"wall_thickness_max": 3.0,
}
@staticmethod
def calculate_complexity(avg_thickness_mm: float) -> float:
"""计算复杂度评分(0~1)"""
return min((avg_thickness_mm / 5.0), 1.0) if avg_thickness_mm > 0 else 0.5
@staticmethod
def calculate_mold_size(
bbox_dims: List[float], cavity_count: int,
cavity_spacing: float = 30, edge_margin: float = 50,
) -> Dict[str, float]:
"""计算模具尺寸(长×宽×高),单位 mm"""
dim_x = max(bbox_dims[0] if len(bbox_dims) > 0 else 120, 120)
dim_y = max(bbox_dims[1] if len(bbox_dims) > 1 else 100, 100)
dim_z = max(bbox_dims[2] if len(bbox_dims) > 2 else 60, 60)
if cavity_count == 1:
length = dim_x + 2 * edge_margin
width = dim_y + 2 * edge_margin
elif cavity_count == 2:
length = 2 * dim_x + cavity_spacing + 2 * edge_margin
width = dim_y + 2 * edge_margin
elif cavity_count == 4:
length = 2 * dim_x + cavity_spacing + 2 * edge_margin
width = 2 * dim_y + cavity_spacing + 2 * edge_margin
else: # 8 型腔: 2x4
length = 4 * dim_x + 3 * cavity_spacing + 2 * edge_margin
width = 2 * dim_y + cavity_spacing + 2 * edge_margin
height = dim_z + 80 # 包含冷却系统
return {"length": length, "width": width, "height": height}
@staticmethod
def calculate_parting_line_length(bbox_dims: List[float], cavity_count: int) -> float:
"""计算分型线长度(mm)"""
if len(bbox_dims) >= 2:
return 2 * (bbox_dims[0] + bbox_dims[1]) * cavity_count
return 0.0
@staticmethod
def calculate_cycle_time(
wall_thickness_max: float, volume_cm3: float, cavity_count: int,
) -> int:
"""
估算成型周期(秒)
周期 = 冷却时间 + 注塑时间 + 顶出时间 + 开合模时间
"""
cooling_time = (wall_thickness_max ** 2) * 4
injection_time = max(3, volume_cm3 / 100)
ejection_time = 3
cycle_time = cooling_time + injection_time + ejection_time + 5
# 多型腔需要更长冷却时间
if cavity_count > 1:
cycle_time = cycle_time * (1 + 0.1 * (cavity_count - 1))
return int(cycle_time)
# ─── 组装方法 ───
@classmethod
def build_detailed_cavity_json(
cls,
geometry_data: Dict[str, Any],
material: Dict[str, Any],
file_path: str,
cavity_mesh_data: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""
组装完整的 detailed_cavity_json(整合以上所有计算结果)
Args:
geometry_data: STP 解析得到的几何数据
material: MaterialService.get_material() 返回的材料属性字典
file_path: STP 文件路径
cavity_mesh_data: 型腔网格数据(可选)
"""
volume_mm3 = geometry_data.get("volume", 0)
surface_area_mm2 = geometry_data.get("surface_area", 0)
bbox = geometry_data.get("bounding_box", {})
bbox_dims = bbox.get("dimensions", [0, 0, 0])
material_density = material["density"]
shrinkage_rate = material["shrinkage"]
is_foam = material.get("is_foam", False)
# 泡沫模具优先 Z 轴开模(上下开模)
parting_direction = "Z"
# 各项计算
volume_cm3 = volume_mm3 / 1000
product_weight_g = cls.calculate_product_weight(volume_mm3, material_density)
projected_area_cm2 = cls.calculate_projected_area(bbox_dims, parting_direction)
cavity_count = cls.calculate_cavity_count(product_weight_g, projected_area_cm2)
clamping_force_ton = cls.calculate_clamping_force(
projected_area_cm2, cavity_count, is_foam=is_foam
)
wall = cls.calculate_wall_thickness(volume_mm3, surface_area_mm2)
complexity_score = cls.calculate_complexity(wall["avg_thickness_mm"])
mold_size = cls.calculate_mold_size(bbox_dims, cavity_count)
parting_line_length = cls.calculate_parting_line_length(bbox_dims, cavity_count)
cycle_time = cls.calculate_cycle_time(wall["wall_thickness_max"], volume_cm3, cavity_count)
injection_pressure = 700 # kg/cm²
detailed_cavity_json = {
"metadata": {
"file_name": Path(file_path).name,
"analysis_date": datetime.now().isoformat(),
"shrinkage_rate": shrinkage_rate,
"draft_angle": 2.0,
"selected_material": material["name"],
"is_foam": is_foam,
"parting_direction": parting_direction,
},
"product_analysis": {
"volume": volume_mm3,
"surface_area": surface_area_mm2,
"bounding_box": bbox,
},
"manufacturing_info": {
"recommended_material": material["name"],
"material_density": f"{material_density} g/cm³",
"estimated_clamping_force": f"{clamping_force_ton} 吨",
"clamping_force_formula": (
"投影面积(cm²) × 0.3" if is_foam
else "投影面积 × 型腔数 × (1+流道比) × 注塑压力 / 1000"
),
"estimated_mold_size": {
"length": int(mold_size["length"]),
"width": int(mold_size["width"]),
"height": int(mold_size["height"]),
},
"mold_material": "铝合金7075" if clamping_force_ton < 200 else "P20钢材",
"mold_hardness": "HB 150-170" if clamping_force_ton < 200 else "HRC 28-32",
"surface_finish": "Ra 0.8 μm",
"parting_line_length": f"{parting_line_length:.2f} mm",
"estimated_cycle_time": f"{cycle_time} 秒",
"injection_pressure": f"{injection_pressure} kg/cm²",
"parting_direction": parting_direction,
},
"mold_cavities": {
"cavity_count": cavity_count,
},
}
# 合并型腔网格数据
if cavity_mesh_data and "mold_cavities" in cavity_mesh_data:
mold_cavities = cavity_mesh_data["mold_cavities"]
for key in ("cavity", "core", "parting_surface"):
if key in mold_cavities:
detailed_cavity_json["mold_cavities"][key] = mold_cavities[key]
# 合并分模附加信息,保持普通模具与铝泡沫模具输出结构一致
if cavity_mesh_data:
if cavity_mesh_data.get("parting_surface"):
detailed_cavity_json["parting_surface"] = cavity_mesh_data["parting_surface"]
quality_checks = cavity_mesh_data.get("quality_checks", {})
if quality_checks:
detailed_cavity_json["quality_checks"] = quality_checks
undercut_regions = quality_checks.get("undercut_regions")
if undercut_regions:
detailed_cavity_json["undercut_regions"] = undercut_regions
side_actions = quality_checks.get("side_actions")
if side_actions:
detailed_cavity_json["side_actions"] = side_actions
# 添加型腔关键信息
detailed_cavity_json["mold_cavities"]["cavity_key_info"] = {
"geometric_characteristics": {
"product_weight": f"{product_weight_g:.2f} g",
"wall_thickness_range": f"{wall['wall_thickness_min']:.2f} - {wall['wall_thickness_max']:.2f} mm",
"complexity_score": round(complexity_score, 2),
"product_volume": f"{volume_cm3:.2f} cm³",
"projected_area": f"{projected_area_cm2:.2f} cm²",
},
"quality_considerations": {
"undercut_count": len(detailed_cavity_json.get("undercut_regions", [])),
"side_action_summary": detailed_cavity_json.get("side_actions", {}).get("summary", {}),
"potential_weld_lines": "center" if cavity_count > 1 else "minimal",
"sink_mark_areas": "thick_sections" if wall["wall_thickness_max"] > 4 else "minimal",
"warpage_risk": "medium" if wall["wall_thickness_max"] > 5 else "low",
},
}
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,
)
result = {
"best_scheme_id": "scheme_1",
"candidate_schemes": [
{
"scheme_id": "scheme_1",
"rank": 1,
"title": "推荐方案",
"method": "legacy_fallback",
"score": 60.0,
"confidence_score": 45.0,
"is_fallback": True,
"fallback_reason": "多方案生成失败,已降级为兼容单方案输出",
"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", {}),
}
cls.attach_injection_system_summaries(result, material["name"])
return result
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 {},
}
cls.attach_injection_system_summaries(result, material["name"])
return result
@classmethod
def attach_injection_system_summaries(
cls,
plan_result: Dict[str, Any],
material_name: str,
) -> Dict[str, Any]:
"""为每个候选方案补充注塑模冷却/浇注摘要。"""
from moldinsight.core.mold_system_designer import MoldSystemDesigner
designer = MoldSystemDesigner()
for scheme in plan_result.get("candidate_schemes", []):
cavity_data = scheme.get("cavity_data") or {}
product_bbox = cavity_data.get("product_analysis", {}).get("bounding_box", {})
mold_size = cavity_data.get("manufacturing_info", {}).get("estimated_mold_size", {})
cavity_count = cavity_data.get("mold_cavities", {}).get("cavity_count", 1)
if not product_bbox or not mold_size:
continue
system_result = designer.design_complete_system(
mold_size=mold_size,
product_bbox=product_bbox,
material=material_name,
cavity_count=cavity_count,
)
cavity_data["injection_system"] = system_result
cavity_data.setdefault("manufacturing_info", {})
cavity_data["manufacturing_info"]["cooling_summary"] = {
"cooling_time": system_result.get("cooling", {}).get("cooling_time"),
"channel_count": system_result.get("cooling", {}).get("thermal_check", {}).get("channel_count"),
"flow_rate_lpm": system_result.get("cooling", {}).get("flow_rate", {}).get("flow_rate_lpm"),
}
cavity_data["manufacturing_info"]["gating_summary"] = {
"gate_type": system_result.get("gating", {}).get("gate_type"),
"runner_type": system_result.get("gating", {}).get("runner", {}).get("type"),
"estimated_cycle_time": system_result.get("overall_assessment", {}).get("estimated_cycle_time"),
}
best_scheme = cls.get_best_scheme(plan_result)
if best_scheme:
plan_result["injection_system"] = best_scheme.get("cavity_data", {}).get("injection_system")
return plan_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]
@@ -0,0 +1,187 @@
from typing import Dict, Any, List, Optional
from moldinsight.core.mold_cam import MoldCAMDesigner
class CAMBundleService:
"""将分模任务结果组装为 CAM 准备包(MVP 骨架)。"""
def __init__(self) -> None:
self.cam_designer = MoldCAMDesigner()
def build_bundle(
self,
task_view: Dict[str, Any],
scheme_id: Optional[str] = None,
mold_steel: str = "P20",
surface_quality: str = "standard",
controller: str = "fanuc",
include_gcode: bool = False,
) -> Dict[str, Any]:
scheme = self._select_scheme(task_view, scheme_id)
cavity_data = scheme.get("cavity_data", {}) if scheme else {}
cavity_bbox = self._extract_cavity_bbox(cavity_data)
stock_bbox = self._build_stock_bbox(cavity_data, cavity_bbox)
cam_result = self.cam_designer.design_mold_cam(
cavity_bbox=cavity_bbox,
stock_bbox=stock_bbox,
mold_steel=mold_steel,
surface_quality=surface_quality,
controller=controller,
)
process_plan = self._build_process_plan(cam_result.get("operations", []))
tooling_suggestion = self._build_tooling_suggestion(cam_result.get("tools", {}))
manufacturing_warnings = self._build_warnings(
scheme=scheme,
cam_recommendations=cam_result.get("recommendations", []),
cavity_data=cavity_data,
)
bundle = {
"task_id": task_view.get("task_id"),
"scheme_id": (scheme or {}).get("scheme_id"),
"process_plan": process_plan,
"tooling_suggestion": tooling_suggestion,
"manufacturing_warnings": manufacturing_warnings,
"summary": cam_result.get("summary", {}),
"confidence": {
"score": (scheme or {}).get("confidence_score"),
"is_fallback": bool((scheme or {}).get("is_fallback", False)),
"fallback_reason": (scheme or {}).get("fallback_reason", ""),
},
}
if include_gcode:
bundle["gcode"] = cam_result.get("gcode", "")
bundle["gcode_lines"] = cam_result.get("gcode_lines", 0)
return bundle
@staticmethod
def _select_scheme(task_view: Dict[str, Any], scheme_id: Optional[str]) -> Dict[str, Any]:
schemes = task_view.get("candidate_schemes") or []
if not schemes:
return {
"scheme_id": "legacy",
"confidence_score": None,
"is_fallback": True,
"fallback_reason": "无候选分模方案,使用默认加工包",
"cavity_data": task_view.get("cavity_data", {}),
}
if scheme_id:
for scheme in schemes:
if scheme.get("scheme_id") == scheme_id:
return scheme
best_scheme_id = task_view.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]
@staticmethod
def _extract_cavity_bbox(cavity_data: Dict[str, Any]) -> Dict[str, Any]:
bbox = cavity_data.get("product_analysis", {}).get("bounding_box", {}) or {}
dims = bbox.get("dimensions") or [100.0, 100.0, 50.0]
if len(dims) < 3:
dims = [100.0, 100.0, 50.0]
center = bbox.get("center") or [0.0, 0.0, 0.0]
half_x = float(dims[0]) / 2.0
half_y = float(dims[1]) / 2.0
half_z = float(dims[2]) / 2.0
return {
"dimensions": [float(dims[0]), float(dims[1]), float(dims[2])],
"min": [float(center[0]) - half_x, float(center[1]) - half_y, float(center[2]) - half_z],
"max": [float(center[0]) + half_x, float(center[1]) + half_y, float(center[2]) + half_z],
}
@staticmethod
def _build_stock_bbox(cavity_data: Dict[str, Any], cavity_bbox: Dict[str, Any]) -> Dict[str, Any]:
mold_size = cavity_data.get("manufacturing_info", {}).get("estimated_mold_size", {}) or {}
dims = mold_size.get("length"), mold_size.get("width"), mold_size.get("height")
if not all(v is not None for v in dims):
dims = cavity_bbox.get("dimensions", [100.0, 100.0, 50.0])
dims = [float(dims[0]) * 1.4, float(dims[1]) * 1.4, max(80.0, float(dims[2]) * 1.8)]
else:
dims = [float(dims[0]), float(dims[1]), float(dims[2])]
return {
"dimensions": dims,
"min": [-dims[0] / 2.0, -dims[1] / 2.0, -dims[2] / 2.0],
"max": [dims[0] / 2.0, dims[1] / 2.0, dims[2] / 2.0],
}
@staticmethod
def _build_process_plan(operations: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
plan = []
for idx, op in enumerate(operations, start=1):
plan.append({
"seq": idx,
"operation": op.get("strategy", "unknown"),
"estimated_time_min": op.get("estimated_time_min", 0),
"tool_id": op.get("tool", {}).get("tool_id"),
"feed_rate_mm_min": op.get("tool", {}).get("feed_rate_mm_min"),
"spindle_speed_rpm": op.get("tool", {}).get("spindle_speed_rpm"),
})
return plan
@staticmethod
def _build_tooling_suggestion(tools: Dict[str, Any]) -> Dict[str, Any]:
roughing = tools.get("roughing", {})
finishing = tools.get("finishing", {})
return {
"roughing_tool": {
"tool_id": roughing.get("tool_id"),
"tool_type": roughing.get("tool_type"),
"diameter_mm": roughing.get("tool_diameter"),
},
"finishing_tool": {
"tool_id": finishing.get("tool_id"),
"tool_type": finishing.get("tool_type"),
"diameter_mm": finishing.get("tool_diameter"),
},
}
@staticmethod
def _build_warnings(
scheme: Dict[str, Any],
cam_recommendations: List[str],
cavity_data: Dict[str, Any],
) -> List[str]:
warnings: List[str] = []
for violation in scheme.get("dfm_violations", []) or []:
level = str(violation.get("level", "medium")).upper()
message = violation.get("message")
if message:
warnings.append(f"DFM[{level}]: {message}")
if scheme.get("is_fallback"):
reason = scheme.get("fallback_reason") or "分模结果使用回退路径"
warnings.append(f"分模回退: {reason}")
confidence_score = float(scheme.get("confidence_score") or 0.0)
if confidence_score and confidence_score < 60.0:
warnings.append(f"方案可信度偏低({confidence_score:.1f}),建议人工复核分型面与倒扣机构")
force_text = str(cavity_data.get("manufacturing_info", {}).get("estimated_clamping_force", ""))
if "吨" in force_text:
try:
force_val = float(force_text.replace("吨", "").strip())
if force_val > 1000:
warnings.append("预估锁模力较高,建议复核设备吨位与模板强度")
except ValueError:
pass
warnings.extend(cam_recommendations[:3])
if not warnings:
warnings.append("未发现明显制造风险,建议进入工艺评审")
return warnings
cam_bundle_service = CAMBundleService()
+467
View File
@@ -0,0 +1,467 @@
"""
LLM 增强分析服务
提供两个核心能力:
1. generate_design_report — 将分析 JSON 转换为结构化评审报告
2. recommend_parting_direction — 基于几何 + 制造约束推荐最优分型方向
适配层:OpenAI 兼容 API(支持 OpenAI / DeepSeek / vLLM / Ollama 等)
未配置 LLM 时静默降级,不影响主流程。
"""
import json
import re
from typing import Optional, Dict, Any, List
import httpx
from shared.config.settings import settings
from shared.utils.logger import get_logger
logger = get_logger(__name__)
_DESIGN_REPORT_SYSTEM = """你是一位资深注塑模具设计工程师,拥有 20 年模具 DFM 评审经验。
请根据提供的模具分析数据,生成一份专业的模具设计评审报告。
要求:
1. 使用中文
2. 按 "关键问题 → 工艺参数建议 → 改进建议" 结构组织
3. 技术术语准确(如:锁模力、投影面积、分型面、滑块、斜顶、拔模角、缩痕、熔接痕)
4. 每个问题标注优先级(high / medium / low)
5. 如果数据不足以判断某项,明确标注"数据不足,需人工确认"
严格输出 JSON,不要输出其他内容。JSON 格式:
{
"title": "模具设计评审报告",
"overview": "一段 1-2 句话的整体概述",
"sections": [
{
"heading": "关键问题",
"type": "issues",
"items": [
{"level": "high", "content": "拔模角不足,建议增加到 2° 以上"},
{"level": "medium", "content": "壁厚偏差较大,可能产生缩痕"}
]
},
{
"heading": "工艺参数建议",
"type": "params_table",
"headers": ["参数", "推荐值", "说明"],
"rows": [
["锁模力", "150 吨", "基于投影面积计算"],
["注塑温度", "230°C", "ABS 材料推荐值"]
]
},
{
"heading": "改进建议",
"type": "recommendations",
"items": [
"建议将主流道直径从 4mm 增加到 6mm",
"建议在所有垂直面增加 1-2° 拔模角"
]
}
],
"overall_score": 7.5
}"""
_DESIGN_REPORT_USER = """请根据以下模具分析数据生成评审报告:
## 产品信息
- 文件:{filename}
- 材料:{material}
- 体积:{volume}
- 表面积:{surface_area}
- 边界框:{bbox}
## 检测特征
{features}
## 质量指标
{quality_metrics}
## 分模方案
{schemes}
## 制造参数
- 推荐模具材料:{mold_material}
- 推荐模具硬度:{mold_hardness}
- 预估锁模力:{clamping_force}
- 模具尺寸(长×宽×高):{mold_size}
- 预估成型周期:{cycle_time}
- 拔模角:{draft_angle}
- 收缩率:{shrinkage_rate}
## 原始设计建议
{recommendations}
请生成 JSON 格式评审报告。issues 部分不要超过 8 条,每条内容简洁在一行内;
params_table 至少要包含锁模力、成型周期、模仁材料、推荐型腔数 4 行;
如果某项数据标记为"自动计算"或"自动选择",请在说明中注明"需人工确认";
overall_score 范围 1-10。"""
_SIDE_ACTION_ANALYSIS_SYSTEM = """你是一位资深注塑模具结构工程师。
请根据提供的 STP 分析结果,判断当前产品是否需要倒扣/抽芯机构,并输出标准化结论。
输出要求:
1. 严格输出 JSON,不要输出其他内容
2. 只允许基于已给数据判断,数据不足时必须标记为 manual_review
3. 结论面向工程评审,避免坐标、面索引、底层算法术语堆砌
4. 建议必须标准化、简洁、可执行
JSON 格式:
{
"status": "required|not_required|manual_review",
"confidence": 0.0,
"conclusion": "一句中文结论",
"mechanism_recommendation": "slider|lifter|mixed|none|manual_review",
"summary": "一段 40-80 字中文摘要",
"reasons": ["原因1", "原因2"],
"standard_advice": ["建议1", "建议2"],
"manual_review_items": ["复核项1", "复核项2"]
}"""
_SIDE_ACTION_ANALYSIS_USER = """请分析当前注塑件是否需要倒扣/抽芯机构:
## 产品信息
- 文件:{filename}
- 材料:{material}
- 边界框:{bbox}
## 特征检测
{features}
## 最优方案
{best_scheme}
## 规则分析结果
{side_actions}
## DFM 风险
{dfm_violations}
判断要求:
1. 如果规则结果明确显示无倒扣,可输出 not_required
2. 如果存在外侧倒扣,优先考虑 slider
3. 如果存在内侧倒扣,优先考虑 lifter
4. 如果内外侧倒扣同时存在,可输出 mixed
5. 如果数据不够支撑明确判断,输出 manual_review"""
_PARTING_SYSTEM = """你是一位注塑模具分模专家。
根据产品几何特征和多个候选分模方向的评分数据,推荐最优分模方向。
输出要求:严格输出 JSON,不要输出其他内容。
JSON 格式:
{
"recommended_axis": "Z",
"confidence": 0.85,
"reasoning": "详细的中文推理过程...",
"risk_notes": ["风险1", "风险2"],
"rankings": [{"axis":"Z","rank":1,"score":92,"note":"..."}]
}"""
_PARTING_USER = """请评估以下候选分模方向并推荐最优方案:
产品几何:
- 边界框 (mm):{bbox}
- 面法向分布:{normal_stats}
- 惯性矩:{inertia}
约束条件:
- 材料:{material}
- 型腔数:{cavity_count}
- 最大锁模力 (吨):{max_clamping_force}
- 泡沫材料:{is_foam}
候选方案:
{schemes}
请综合评估制造可行性、成本和风险,给出推荐。"""
class LLMService:
"""LLM 增强分析服务(单例)"""
def __init__(self):
self._enabled = settings.LLM_ENABLED
self._api_url = settings.LLM_API_URL.rstrip("/")
self._api_key = settings.LLM_API_KEY
self._model = settings.LLM_MODEL
self._timeout = settings.LLM_TIMEOUT
self._max_tokens = settings.LLM_MAX_TOKENS
if self._enabled:
logger.info(
"LLM 增强分析已启用: model=%s endpoint=%s",
self._model, self._api_url,
)
else:
logger.info("LLM 增强分析未启用(设置 LLM_ENABLED=true 启用)")
async def generate_design_report(
self,
analysis_result: Dict[str, Any],
detailed_cavity_json: Optional[Dict[str, Any]] = None,
) -> Optional[Dict[str, Any]]:
"""生成模具设计评审报告 (结构化 JSON)"""
if not self._enabled:
return None
try:
prompt = self._build_design_report_prompt(analysis_result, detailed_cavity_json)
response = await self._chat(
_DESIGN_REPORT_SYSTEM,
prompt,
self._max_tokens,
expect_json=True,
)
if not response:
return None
result = self._parse_json_response(response)
if result:
logger.info("LLM 设计报告生成成功 (%d sections)", len(result.get("sections", [])))
return result
except Exception as e:
logger.warning("LLM 设计报告生成失败(不影响主流程): %s", e)
return None
async def generate_side_action_analysis(
self,
analysis_result: Dict[str, Any],
detailed_cavity_json: Optional[Dict[str, Any]] = None,
) -> Optional[Dict[str, Any]]:
"""生成倒扣/抽芯 AI 标准化分析"""
if not self._enabled:
return None
try:
prompt = self._build_side_action_prompt(analysis_result, detailed_cavity_json)
response = await self._chat(
_SIDE_ACTION_ANALYSIS_SYSTEM,
prompt,
min(self._max_tokens, 1200),
expect_json=True,
)
if not response:
return None
result = self._parse_json_response(response)
if result:
logger.info(
"LLM 倒扣/抽芯分析生成成功: status=%s confidence=%s",
result.get("status"),
result.get("confidence"),
)
return result
except Exception as e:
logger.warning("LLM 倒扣/抽芯分析失败(不影响主流程): %s", e)
return None
@staticmethod
def compose_llm_report(
design_report: Optional[Dict[str, Any]],
side_action_analysis: Optional[Dict[str, Any]],
) -> Optional[str]:
"""将结构化报告和倒扣分析打包进 llm_report 字段,避免改动外部协议。
设计报告以 <!--DESIGN_REPORT_BEGIN--> / <!--DESIGN_REPORT_END--> 包裹的 JSON 嵌入,
倒扣分析以 <!--SIDE_ACTION_AI_BEGIN--> / <!--SIDE_ACTION_AI_END--> 包裹的 JSON 嵌入。
"""
sections: List[str] = []
if side_action_analysis:
payload = json.dumps(side_action_analysis, ensure_ascii=False)
sections.append(
"<!--SIDE_ACTION_AI_BEGIN-->\n"
f"{payload}\n"
"<!--SIDE_ACTION_AI_END-->"
)
if design_report:
payload = json.dumps(design_report, ensure_ascii=False)
sections.append(
"<!--DESIGN_REPORT_BEGIN-->\n"
f"{payload}\n"
"<!--DESIGN_REPORT_END-->"
)
merged = "\n\n".join(sections).strip()
return merged or None
async def recommend_parting_direction(
self,
geometry_data: Dict[str, Any],
candidate_schemes: List[Dict[str, Any]],
material: Dict[str, Any],
cavity_count: int = 1,
) -> Optional[Dict[str, Any]]:
"""推荐最优分型方向"""
if not self._enabled:
return None
try:
prompt = self._build_parting_prompt(geometry_data, candidate_schemes, material, cavity_count)
response = await self._chat(_PARTING_SYSTEM, prompt, min(self._max_tokens, 1200), expect_json=True)
if response:
result = self._parse_json_response(response)
if result:
logger.info("LLM 分型推荐: %s (%.2f)", result.get("recommended_axis", "?"), result.get("confidence", 0))
return result
return None
except Exception as e:
logger.warning("LLM 分型推荐失败(不影响主流程): %s", e)
return None
def _build_design_report_prompt(self, analysis_result, detailed_cavity_json) -> str:
features = json.dumps(analysis_result.get("detected_features", []), ensure_ascii=False, indent=2)
if len(features) > 4000:
features = features[:4000] + "\n... (已截断)"
schemes_text = ""
if detailed_cavity_json:
schemes = detailed_cavity_json.get("candidate_schemes", [])
if schemes:
schemes_text = json.dumps([{
"scheme_id": s.get("scheme_id"), "rank": s.get("rank"), "title": s.get("title"),
"score": s.get("score"), "summary": s.get("summary"),
"parting_axis": s.get("parting", {}).get("axis"),
"mold_structure_type": s.get("mold_structure_type"),
"dfm_violations": s.get("dfm_violations", []),
} for s in schemes], ensure_ascii=False, indent=2)
best = detailed_cavity_json.get("candidate_schemes", [{}])[0] if detailed_cavity_json else {}
cd = best.get("cavity_data", {}) if isinstance(best, dict) else {}
mfg = cd.get("manufacturing_info", {})
meta = cd.get("metadata", {})
return _DESIGN_REPORT_USER.format(
filename=meta.get("file_name", "unknown.stp"),
material=meta.get("selected_material", "ABS"),
volume=f"{analysis_result.get('geometry_data', {}).get('volume', 0):.1f} mm³",
surface_area=f"{analysis_result.get('geometry_data', {}).get('surface_area', 0):.1f} mm²",
bbox=json.dumps(analysis_result.get("geometry_data", {}).get("bounding_box", {}), ensure_ascii=False),
features=features or "无特征检测数据",
quality_metrics=json.dumps(analysis_result.get("quality_metrics", {}), ensure_ascii=False, indent=2),
schemes=schemes_text or "无分模方案数据",
mold_material=mfg.get("mold_material", "自动选择"),
mold_hardness=mfg.get("mold_hardness", "自动选择"),
clamping_force=mfg.get("estimated_clamping_force", "自动计算"),
mold_size=json.dumps(mfg.get("estimated_mold_size", {}), ensure_ascii=False),
cycle_time=mfg.get("estimated_cycle_time", "自动计算"),
draft_angle=f"{meta.get('draft_angle', 2.0)}°",
shrinkage_rate="自动计算",
recommendations=json.dumps(analysis_result.get("design_recommendations", []), ensure_ascii=False, indent=2) or "无",
)
def _build_side_action_prompt(self, analysis_result, detailed_cavity_json) -> str:
features = json.dumps(
analysis_result.get("detected_features", []),
ensure_ascii=False,
indent=2,
)
if len(features) > 2500:
features = features[:2500] + "\n... (已截断)"
best_scheme = {}
if detailed_cavity_json:
candidate_schemes = detailed_cavity_json.get("candidate_schemes", [])
best_scheme_id = detailed_cavity_json.get("best_scheme_id")
if candidate_schemes:
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
cavity_data = best_scheme.get("cavity_data", {}) if isinstance(best_scheme, dict) else {}
metadata = cavity_data.get("metadata", {}) if isinstance(cavity_data, dict) else {}
side_actions = (
best_scheme.get("side_actions")
or cavity_data.get("side_actions")
or {}
)
best_scheme_view = {
"scheme_id": best_scheme.get("scheme_id"),
"title": best_scheme.get("title"),
"score": best_scheme.get("score"),
"parting_axis": best_scheme.get("parting", {}).get("axis"),
"mold_structure_type": best_scheme.get("mold_structure_type"),
"undercut_regions_count": len(best_scheme.get("undercut_regions", []) or []),
}
side_actions_view = {
"summary": side_actions.get("summary", {}),
"recommendations": side_actions.get("recommendations", []),
"slider_count": len(side_actions.get("slider_mechanisms", []) or []),
"lifter_count": len(side_actions.get("lifter_mechanisms", []) or []),
}
dfm_violations = best_scheme.get("dfm_violations", []) if isinstance(best_scheme, dict) else []
return _SIDE_ACTION_ANALYSIS_USER.format(
filename=metadata.get("file_name", "unknown.stp"),
material=metadata.get("selected_material", "ABS"),
bbox=json.dumps(
analysis_result.get("geometry_data", {}).get("bounding_box", {}),
ensure_ascii=False,
),
features=features or "无特征检测数据",
best_scheme=json.dumps(best_scheme_view, ensure_ascii=False, indent=2),
side_actions=json.dumps(side_actions_view, ensure_ascii=False, indent=2),
dfm_violations=json.dumps(dfm_violations[:6], ensure_ascii=False, indent=2),
)
def _build_parting_prompt(self, geometry_data, candidate_schemes, material, cavity_count) -> str:
bbox = geometry_data.get("bounding_box", {})
axis_normal_stats = geometry_data.get("axis_normal_stats", {})
inertia = geometry_data.get("inertia_matrix", [])
inertia_diag = [inertia[i][i] if i < len(inertia) and i < len(inertia[i]) else 0.0 for i in range(3)]
schemes_text = json.dumps([{
"axis": s.get("parting", {}).get("axis") or s.get("axis"),
"score": s.get("score"), "summary": s.get("summary"),
"mold_structure_type": s.get("mold_structure_type"),
"core_required": s.get("core_required"),
"dfm_violations": s.get("dfm_violations", []),
"undercut_regions_count": len(s.get("undercut_regions", [])),
"score_breakdown": s.get("score_breakdown", {}),
} for s in candidate_schemes], ensure_ascii=False, indent=2)
return _PARTING_USER.format(
bbox=json.dumps(bbox, ensure_ascii=False),
normal_stats=json.dumps(axis_normal_stats, ensure_ascii=False),
inertia=json.dumps(inertia_diag, ensure_ascii=False),
material=material.get("name", "ABS"),
cavity_count=cavity_count,
max_clamping_force="3000 吨(最大)",
is_foam="是" if material.get("is_foam") else "否",
schemes=schemes_text,
)
async def _chat(self, system, user, max_tokens=2000, expect_json=False, temperature=0.3):
url = f"{self._api_url}/chat/completions"
headers = {"Authorization": f"Bearer {self._api_key}", "Content-Type": "application/json"}
payload = {
"model": self._model,
"messages": [{"role": "system", "content": system}, {"role": "user", "content": user}],
"max_tokens": max_tokens, "temperature": temperature,
}
if expect_json:
payload["response_format"] = {"type": "json_object"}
async with httpx.AsyncClient(timeout=self._timeout) as client:
resp = await client.post(url, json=payload, headers=headers)
resp.raise_for_status()
content = resp.json()["choices"][0]["message"]["content"]
return content.strip() if content else None
@staticmethod
def _parse_json_response(raw):
try:
return json.loads(raw)
except json.JSONDecodeError:
m = re.search(r"\{[\s\S]*\}", raw)
if m:
try:
return json.loads(m.group())
except json.JSONDecodeError:
pass
logger.warning("LLM JSON 解析失败: %s...", raw[:200])
return None
llm_service = LLMService()
@@ -0,0 +1,45 @@
# services/material_service.py
"""材料属性管理服务"""
from typing import Dict, Any, List, Optional
MATERIAL_PROPERTIES: Dict[str, Dict[str, Any]] = {
"ABS": {"density": 1.05, "shrinkage": 0.005, "name": "ABS", "is_foam": False},
"PP": {"density": 0.90, "shrinkage": 0.016, "name": "PP", "is_foam": False},
"PE": {"density": 0.95, "shrinkage": 0.020, "name": "PE", "is_foam": False},
"PC": {"density": 1.20, "shrinkage": 0.007, "name": "PC", "is_foam": False},
"PA": {"density": 1.14, "shrinkage": 0.010, "name": "PA", "is_foam": False},
"POM": {"density": 1.41, "shrinkage": 0.020, "name": "POM", "is_foam": False},
"PMMA": {"density": 1.18, "shrinkage": 0.005, "name": "PMMA", "is_foam": False},
"PBT": {"density": 1.31, "shrinkage": 0.015, "name": "PBT", "is_foam": False},
"AlSi10Mg": {"density": 0.45, "shrinkage": 0.015, "name": "AlSi10Mg", "is_foam": True},
"AlSi12": {"density": 0.50, "shrinkage": 0.012, "name": "AlSi12", "is_foam": True},
"Pure Al Foam": {"density": 0.35, "shrinkage": 0.020, "name": "Pure Al Foam", "is_foam": True},
"AlSi7Mg": {"density": 0.40, "shrinkage": 0.018, "name": "AlSi7Mg", "is_foam": True},
}
# 默认回退材料
_DEFAULT_MATERIAL = MATERIAL_PROPERTIES["ABS"]
class MaterialService:
"""材料属性管理服务 — 集中管理材料字典,便于扩展和单测"""
@staticmethod
def get_material(material_name: str) -> Dict[str, Any]:
"""获取材料属性,不存在则回退到 ABS"""
return MATERIAL_PROPERTIES.get(material_name, _DEFAULT_MATERIAL)
@staticmethod
def is_foam_material(material_name: str) -> bool:
return MATERIAL_PROPERTIES.get(material_name, _DEFAULT_MATERIAL).get("is_foam", False)
@staticmethod
def list_all_materials() -> List[str]:
return list(MATERIAL_PROPERTIES.keys())
@staticmethod
def resolve_material(requested: str) -> str:
"""解析请求的材料名,若不在字典中则回退为 ABS"""
return requested if requested in MATERIAL_PROPERTIES else "ABS"
@@ -0,0 +1,665 @@
# services/processing_service.py
"""STP 文件处理流程编排器 — 协调解析、网格生成、型腔生成、保存、验证"""
import asyncio
import time
import traceback
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime
from pathlib import Path
from typing import Optional, Dict, Any
from sqlalchemy.ext.asyncio import AsyncSession
from moldinsight.core.stp_parser import STPParser
from moldinsight.core.geometry_analyzer import GeometryAnalyzer
from moldinsight.core.mold_generator import MoldCavityGenerator
from moldinsight.core.aluminum_foam_mold import AluminumFoamMoldGenerator
from moldinsight.core.mold_quality_inspector import AluminumFoamMoldQualityInspector
from moldinsight.core.mesh_generator import MeshGenerator
from moldinsight.core.multi_scheme_planner import MultiSchemeMoldPlanner
from moldinsight.core.cad_exporter import CADExporter
from moldinsight.services.storage_integration_rustfs import StorageIntegrationService
from shared.services.redis_task_manager import redis_task_manager
from moldinsight.services.material_service import MaterialService
from moldinsight.services.calculation_service import CalculationService
from moldinsight.services.llm_service import llm_service
from shared.models.schemas import ProcessingStatus
from shared.database.database import db_manager
from shared.utils.html_generator import HTMLGenerator
from shared.utils.logger import get_logger
logger = get_logger(__name__)
class ProcessingService:
"""核心处理流程编排 — 协调 STP 解析、网格、型腔、计算、保存、验证"""
def __init__(self):
self.stp_parser = STPParser()
self.geometry_analyzer = GeometryAnalyzer()
self.mold_generator = MoldCavityGenerator(shrinkage_rate=0.005)
self.aluminum_foam_generator = AluminumFoamMoldGenerator(shrinkage_rate=0.015, draft_angle=3.0)
self.mold_quality_inspector = AluminumFoamMoldQualityInspector()
self.mesh_generator = MeshGenerator(quality="medium")
self.html_generator = HTMLGenerator()
self.storage_service = StorageIntegrationService()
self.multi_scheme_planner = MultiSchemeMoldPlanner()
self.cad_exporter = CADExporter()
self._export_shapes_cache: Dict[str, Dict[str, Dict[str, Any]]] = {}
self._occ_executor = ThreadPoolExecutor(max_workers=2, thread_name_prefix="occ")
# ─── 对外入口 ───
async def process_file_with_storage(
self,
task_id: str,
file_path: str,
stp_file_id: int,
process_params: Optional[Dict[str, Any]] = None,
):
"""处理文件的后台任务 — 使用独立数据库会话"""
# 创建独立的数据库会话,避免请求范围会话关闭
async with db_manager.session() as db_session:
try:
logger.info(f"开始处理文件并生成模具型腔: {file_path}")
from shared.config.settings import settings
file_size_bytes = Path(file_path).stat().st_size if Path(file_path).exists() else 0
file_size_mb = max(file_size_bytes / (1024 * 1024), 1)
timeout_seconds = min(
max(settings.PROCESSING_TIMEOUT_BASE, int(file_size_mb * settings.PROCESSING_TIMEOUT_PER_MB)),
1800,
)
logger.info(f"处理超时设置为 {timeout_seconds}s (文件 {file_size_mb:.1f}MB)")
try:
await asyncio.wait_for(
self.process_file_core(
task_id, file_path, stp_file_id, db_session, process_params
),
timeout_seconds,
)
except asyncio.TimeoutError:
logger.error(f"处理超时: {task_id}")
raise Exception(f"处理超时,超过{timeout_seconds}秒未完成")
except Exception as e:
logger.error(f"模具型腔生成失败: {e}")
await self.storage_service.update_stp_file_status(db_session, stp_file_id, "failed")
await self.storage_service.update_task_status(
db_session, task_id, "failed", error_message=str(e)
)
# 安全更新 Redis 任务状态
task = await redis_task_manager.get_task(task_id)
if task:
await redis_task_manager.update_task(task_id, {
"status": ProcessingStatus.FAILED,
"error": str(e),
"completed_at": str(datetime.now()),
})
async def process_file_core(
self,
task_id: str,
file_path: str,
stp_file_id: int,
db_session: AsyncSession,
process_params: Optional[Dict[str, Any]] = None,
):
"""核心处理逻辑"""
try:
logger.info(f"开始处理文件并生成模具型腔: {file_path}")
process_params = self._normalize_process_params(process_params)
stage_timings: Dict[str, float] = {}
# 1. 解析STP文件
await self.storage_service.update_task_status(
db_session, task_id, "processing", 20, "解析STP文件"
)
stage_started = time.perf_counter()
loop = asyncio.get_running_loop()
shape = await loop.run_in_executor(
self._occ_executor, self.stp_parser.load_step_file, Path(file_path)
)
geometry_data = await loop.run_in_executor(
self._occ_executor, self.stp_parser.analyze_geometry, shape
)
stage_timings["parse_stp"] = round(time.perf_counter() - stage_started, 3)
# 2. 生成网格数据并持久化
await self.storage_service.update_task_status(
db_session, task_id, "processing", 30, "生成网格数据"
)
stage_started = time.perf_counter()
mesh_result = await self._step_generate_mesh(
shape, geometry_data, file_path, db_session, stp_file_id, task_id
)
stage_timings["generate_mesh"] = round(time.perf_counter() - stage_started, 3)
# 3. 生成模具型腔
await self.storage_service.update_task_status(
db_session, task_id, "processing", 40, "生成模具型腔"
)
# 材料属性 — 通过 MaterialService 集中管理
requested_material = MaterialService.resolve_material(process_params["material"])
selected_material = dict(MaterialService.get_material(requested_material))
selected_material["shrinkage"] = process_params["shrinkage_rate"] / 100.0
is_foam_material = MaterialService.is_foam_material(requested_material)
stage_started = time.perf_counter()
plan_result = await self._step_generate_cavity(
shape, selected_material, is_foam_material, process_params
)
stage_timings["generate_cavity"] = round(time.perf_counter() - stage_started, 3)
export_shapes = {}
export_artifacts = None
if plan_result:
export_shapes = plan_result.pop("_export_shapes", {}) or {}
if export_shapes:
self._cache_export_shapes(task_id, export_shapes)
export_artifacts = self._persist_step_exports(
task_id=task_id,
original_filename=Path(file_path).name,
export_shapes=export_shapes,
)
# 4. 生成详细JSON数据 — 委托 CalculationService
await self.storage_service.update_task_status(
db_session, task_id, "processing", 60, "生成型腔详细数据"
)
stage_started = time.perf_counter()
detailed_cavity_json = CalculationService.build_plan_result(
geometry_data=geometry_data,
material=selected_material,
file_path=str(file_path),
plan_result=plan_result,
)
stage_timings["build_plan_result"] = round(time.perf_counter() - stage_started, 3)
best_scheme = CalculationService.get_best_scheme(detailed_cavity_json)
best_cavity_data = best_scheme.get("cavity_data", {}) if best_scheme else {}
best_key_info = best_scheme.get("key_info", {}) if best_scheme else {}
if best_cavity_data.get("mold_cavities"):
cavity_geometry = best_cavity_data["mold_cavities"].get("cavity", {})
logger.info(
f"推荐方案型腔数据已合并: cavity {cavity_geometry.get('vertex_count', 0)} 顶点"
)
# 5. 生成关键信息
cavity_key_info = best_key_info
# 6. 保存几何数据到数据库
await self.storage_service.update_task_status(
db_session, task_id, "processing", 70, "保存几何数据"
)
stage_started = time.perf_counter()
await self.storage_service.save_geometry_data(
db_session,
stp_file_id,
geometry_data,
geometry_data.get("analysis_method", "mold_cavity"),
)
# 7. 生成HTML可视化
await self.storage_service.update_task_status(
db_session, task_id, "processing", 85, "生成可视化报告"
)
pointcloud_data = None
lod_data = None
if mesh_result:
lod0 = mesh_result.get("lods", {}).get("0", {})
pointcloud_data = {
"points": mesh_result.get("points", []),
"normals": mesh_result.get("normals", []),
"vertices": lod0.get("vertices", []),
"faces": lod0.get("faces", []),
"point_count": mesh_result.get("point_count", 0),
"vertex_count": mesh_result.get("vertex_count", 0),
"face_count": mesh_result.get("face_count", 0),
}
if mesh_result and mesh_result.get("lods"):
lods = mesh_result["lods"]
lod_data = mesh_result
logger.info(f"LOD数据复用成功: {len(lods)} 级 (面数: {[lods[k]['face_count'] for k in sorted(lods.keys())]})")
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,
lod_data=lod_data,
)
best_scheme = CalculationService.get_best_scheme(detailed_cavity_json)
best_cavity_data = best_scheme.get("cavity_data", {}) if best_scheme else best_cavity_data
best_key_info = best_scheme.get("key_info", {}) if best_scheme else best_key_info
# 8. 保存模具型腔数据(包含方案级预览链接)
await self.storage_service.save_mold_cavity_data(
db_session, stp_file_id, detailed_cavity_json
)
html_file_path = self.html_generator.generate_and_save_visualization(
geometry_data,
Path(file_path).name,
cavity_data=best_cavity_data,
pointcloud_data=pointcloud_data,
lod_data=lod_data,
)
await self.storage_service.save_html_file(
db_session,
stp_file_id,
Path(html_file_path).name,
html_file_path,
)
stage_timings["persist_artifacts"] = round(time.perf_counter() - stage_started, 3)
# 9. 分析模具设计
stage_started = time.perf_counter()
loop = asyncio.get_running_loop()
analysis_result = await loop.run_in_executor(
self._occ_executor,
lambda: self.geometry_analyzer.analyze_mold_design(
geometry_data,
product_material=requested_material,
shape=shape,
),
)
if analysis_result:
await self.storage_service.save_features_and_recommendations(
db_session,
stp_file_id,
analysis_result.get("detected_features", []),
analysis_result.get("design_recommendations", []),
)
await self._save_analysis_metrics(db_session, stp_file_id, analysis_result)
stage_timings["analyze_design"] = round(time.perf_counter() - stage_started, 3)
# 9.6 更新STP文件的分析摘要字段
await self.storage_service.update_stp_file_analysis_summary(
db_session,
stp_file_id,
volume=geometry_data.get("volume", 0),
surface_area=geometry_data.get("surface_area", 0),
product_weight=CalculationService.calculate_product_weight(
geometry_data.get("volume", 0), selected_material["density"]
),
)
# 9.7 FreeCAD 几何验证
stage_started = time.perf_counter()
verification_result = await self._step_verify(
file_path, db_session, task_id, stp_file_id, analysis_result
)
stage_timings["verify_geometry"] = round(time.perf_counter() - stage_started, 3)
# 9.8 LLM 增强分析
llm_report = None
stage_started = time.perf_counter()
if analysis_result:
side_action_ai = await llm_service.generate_side_action_analysis(
analysis_result, detailed_cavity_json
)
design_report = await llm_service.generate_design_report(
analysis_result, detailed_cavity_json
)
llm_report = llm_service.compose_llm_report(
design_report, side_action_ai
)
stage_timings["generate_llm_report"] = round(time.perf_counter() - stage_started, 3)
# 10. 完成处理
await self.storage_service.update_stp_file_status(db_session, stp_file_id, "completed")
await self.storage_service.update_task_status(
db_session, task_id, "completed", 100, "模具型腔生成完成"
)
await self.storage_service.update_task_parameters(
db_session,
task_id,
{
"stage_timings": stage_timings,
"material": requested_material,
"verification": verification_result,
"llm_report": llm_report,
"export_artifacts": export_artifacts,
**process_params,
},
)
# 更新任务缓存状态(仅保留轻量摘要,完整数据由PG+RustFS持久化)
await redis_task_manager.update_task(task_id, {
"status": ProcessingStatus.COMPLETED,
"completed_at": str(datetime.now()),
"geometry_data": geometry_data,
"analysis_result": analysis_result,
"key_info": best_key_info,
"best_scheme_id": detailed_cavity_json.get("best_scheme_id"),
"material": requested_material,
"parameters": process_params,
"stage_timings": stage_timings,
"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,
"llm_report": llm_report,
"export_artifacts": export_artifacts,
})
logger.info(f"模具型腔生成完成: {task_id}")
logger.info(f"key_info metadata: {detailed_cavity_json.get('metadata', {})}")
logger.info(f"key_info manufacturing_info: {detailed_cavity_json.get('manufacturing_info', {})}")
logger.info(f"key_info geometric_characteristics: {detailed_cavity_json.get('mold_cavities', {}).get('cavity_key_info', {}).get('geometric_characteristics', {})}")
except Exception as e:
logger.error(f"模具型腔生成失败: {e}")
await self.storage_service.update_stp_file_status(db_session, stp_file_id, "failed")
await self.storage_service.update_task_status(
db_session, task_id, "failed", error_message=str(e)
)
task = await redis_task_manager.get_task(task_id)
if task:
await redis_task_manager.update_task(task_id, {
"status": ProcessingStatus.FAILED,
"error": str(e),
"completed_at": str(datetime.now()),
})
# ─── 内部步骤 ───
async def _step_generate_mesh(
self, shape, geometry_data: dict, file_path: str,
db_session: AsyncSession, stp_file_id: int, task_id: str,
) -> Optional[Dict[str, Any]]:
"""生成多级LOD网格并持久化,一次OCC剖分+trimesh简化,失败不影响主流程"""
mesh_result = None
try:
loop = asyncio.get_running_loop()
mesh_result = await loop.run_in_executor(
self._occ_executor, self.mesh_generator.generate_multi_lod_mesh, shape
)
lod0 = mesh_result.get("lods", {}).get("0", {})
vertices = lod0.get("vertices", [])
faces = lod0.get("faces", [])
points = mesh_result.get("points", [])
normals = mesh_result.get("normals", [])
point_count = mesh_result.get("point_count", 0)
vertex_count = lod0.get("vertex_count", mesh_result.get("vertex_count", 0))
face_count = lod0.get("face_count", mesh_result.get("face_count", 0))
if vertices and faces:
bbox = geometry_data.get("bounding_box", {})
mesh_json = {
"metadata": {
"file_name": Path(file_path).name,
"generated_at": datetime.now().isoformat(),
"quality": "medium",
"vertex_count": vertex_count,
"face_count": face_count,
"point_count": point_count,
},
"mesh": {
"vertices": vertices,
"faces": faces,
},
"pointcloud": {
"points": points,
"normals": normals,
"count": point_count,
},
"bounding_box": bbox,
}
await self.storage_service.save_mesh_data(
db_session,
stp_file_id=stp_file_id,
mesh_json=mesh_json,
quality="medium",
)
await redis_task_manager.update_task(task_id, {
"mesh_summary": {
"vertex_count": vertex_count,
"face_count": face_count,
"point_count": point_count,
"quality": "medium",
}
})
except Exception as mesh_err:
logger.warning(f"网格生成或保存失败,不影响主流程: {mesh_err}")
return mesh_result
async def _step_generate_cavity(
self, shape, selected_material: dict, is_foam_material: bool, process_params: Dict[str, Any],
) -> Optional[Dict[str, Any]]:
"""生成多方案分模结果"""
plan_result = None
try:
if shape:
loop = asyncio.get_running_loop()
plan_result = await loop.run_in_executor(
self._occ_executor,
lambda: self.multi_scheme_planner.generate_plan(
shape=shape,
material=selected_material,
is_foam_material=is_foam_material,
process_params=process_params,
),
)
logger.info(
f"多方案分模完成: 生成 {len(plan_result.get('candidate_schemes', []))} 套方案"
)
except Exception as cavity_err:
logger.warning(f"多方案分模失败,使用简化数据: {cavity_err}")
traceback.print_exc()
plan_result = None
return plan_result
def _cache_export_shapes(self, task_id: str, export_shapes: Dict[str, Dict[str, Any]]):
self._export_shapes_cache[task_id] = export_shapes
def _persist_step_exports(
self,
task_id: str,
original_filename: str,
export_shapes: Dict[str, Dict[str, Any]],
) -> Optional[Dict[str, Any]]:
if not export_shapes:
return None
base_filename = Path(original_filename).stem or f"mold_{task_id}"
manifest = {
"version": 1,
"task_id": task_id,
"generated_at": datetime.now().isoformat(),
"schemes": {},
}
components = ["cavity", "core", "parting_surface"]
for scheme_id, cavity_data in export_shapes.items():
try:
result = self.cad_exporter.export_mold_results(
cavity_data=cavity_data,
base_filename=base_filename,
formats=["step"],
components=components,
task_id=task_id,
scheme_id=scheme_id,
)
manifest["schemes"][scheme_id] = {
"base_filename": result.get("base_filename"),
"generated_at": datetime.now().isoformat(),
"files": result.get("files", []),
"errors": result.get("errors", []),
"total_files": result.get("total_files", 0),
"total_errors": result.get("total_errors", 0),
}
except Exception as exc:
logger.warning("持久化 STEP 导出失败: task=%s scheme=%s error=%s", task_id, scheme_id, exc)
manifest["schemes"][scheme_id] = {
"base_filename": base_filename,
"generated_at": datetime.now().isoformat(),
"files": [],
"errors": [str(exc)],
"total_files": 0,
"total_errors": 1,
}
return manifest
def get_export_shapes(self, task_id: str, scheme_id: Optional[str] = None) -> Optional[Dict[str, Any]]:
scheme_map = self._export_shapes_cache.get(task_id, {})
if not scheme_map:
return None
if scheme_id:
return scheme_map.get(scheme_id)
return next(iter(scheme_map.values()), None)
@staticmethod
def _normalize_process_params(process_params: Optional[Dict[str, Any]]) -> Dict[str, Any]:
payload = dict(process_params or {})
return {
"material": MaterialService.resolve_material(str(payload.get("material", "ABS"))),
"draft_angle": float(payload.get("draft_angle", 2.0)),
"shrinkage_rate": float(payload.get("shrinkage_rate", 0.5)),
"parting_precision": float(payload.get("parting_precision", 0.1)),
"cavity_match": int(payload.get("cavity_match", 95)),
}
async def _step_verify(
self, file_path: str, db_session: AsyncSession,
task_id: str, stp_file_id: int, analysis_result: Optional[dict],
) -> Optional[Dict[str, Any]]:
"""FreeCAD 几何验证(可通过配置禁用)"""
from shared.config.settings import settings
if not settings.ENABLE_FREECAD_VERIFICATION:
logger.info("FreeCAD验证已禁用(设置 ENABLE_FREECAD_VERIFICATION=true 启用)")
return {"status": "disabled", "reason": "FreeCAD验证已禁用"}
await self.storage_service.update_task_status(
db_session, task_id, "processing", 90, "FreeCAD几何验证"
)
try:
from moldinsight.services.verification_service import GeometryVerificationService
verification_svc = GeometryVerificationService(timeout=settings.FREECAD_VERIFICATION_TIMEOUT)
verification_result = await verification_svc.verify_stp_file(file_path)
if verification_result and analysis_result:
await self._save_verification_metrics(db_session, stp_file_id, verification_result)
logger.info(f"FreeCAD验证完成: {verification_result.get('status', 'unknown') if verification_result else 'failed'}")
return verification_result
except Exception as ve:
logger.warning(f"FreeCAD验证失败(不影响主流程): {ve}")
return {"status": "error", "error": str(ve)}
async def _attach_scheme_previews(
self,
detailed_cavity_json: Dict[str, Any],
geometry_data: Dict[str, Any],
stp_filename: str,
pointcloud_data: Optional[Dict[str, Any]] = None,
lod_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")
base_stem = Path(stp_filename).stem.replace(" ", "_")
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
summary_name = f"mold_{base_stem}_{suffix}_{ts}_summary.json"
summary_content = self.html_generator.generate_3d_viewer_summary(
geometry_data, cavity_data
)
self.html_generator.save_data_file(summary_content, summary_name)
scheme["summary_file"] = f"/html/{summary_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):
"""保存分析指标到数据库"""
from shared.models.database import AnalysisMetrics
quality_metrics = analysis_result.get("quality_metrics", {})
analysis_summary = analysis_result.get("analysis_summary", "")
metrics = AnalysisMetrics(
stp_file_id=stp_file_id,
volume_utilization=quality_metrics.get("volume_utilization", 0),
topology_complexity=quality_metrics.get("topology_complexity", 0),
wall_uniformity=quality_metrics.get("wall_uniformity", 0),
analysis_summary=analysis_summary,
)
session.add(metrics)
await session.commit()
logger.info(f"分析指标保存成功: {metrics.id}")
async def _save_verification_metrics(self, session: AsyncSession, stp_file_id: int, verification_result: dict):
"""保存验证指标到数据库"""
from shared.models.database import AnalysisMetrics
from sqlalchemy import select
result = await session.execute(
select(AnalysisMetrics).where(AnalysisMetrics.stp_file_id == stp_file_id)
)
metrics = result.scalar_one_or_none()
comparison = verification_result.get("comparison", {})
volume_comparison = comparison.get("volume", {})
area_comparison = comparison.get("surface_area", {})
if metrics:
metrics.verification_status = verification_result.get("status", "unknown")
metrics.verification_volume_diff = volume_comparison.get("difference_percent", 0)
metrics.verification_area_diff = area_comparison.get("difference_percent", 0)
metrics.verification_details = verification_result
else:
metrics = AnalysisMetrics(
stp_file_id=stp_file_id,
verification_status=verification_result.get("status", "unknown"),
verification_volume_diff=volume_comparison.get("difference_percent", 0),
verification_area_diff=area_comparison.get("difference_percent", 0),
verification_details=verification_result,
)
session.add(metrics)
await session.commit()
logger.info(f"验证指标保存成功: stp_file_id={stp_file_id}")
# 模块级单例,供路由层直接使用
processing_service = ProcessingService()
@@ -0,0 +1,376 @@
# services/storage_integration.py
"""存储集成服务 - 协调 PostgreSQL 和 MinIO"""
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from pathlib import Path
from typing import Optional, Dict, Any
import json
from shared.models.database import (
STPFile, GeometryData, MoldCavityData,
HTMLFile, ProcessingTask, User,
FeatureDetection, DesignRecommendation,
UserActivity, SystemLog
)
from moldinsight.storage.object_storage import storage_manager
from shared.utils.logger import get_logger
logger = get_logger(__name__)
class StorageIntegrationService:
"""存储集成服务"""
async def save_stp_file(self, session: AsyncSession,
file_path: Path,
original_filename: str,
user_id: Optional[int] = None) -> STPFile:
"""保存STP文件到PostgreSQL元数据 + MinIO对象存储"""
# 1. 上传到MinIO
upload_result = await storage_manager.upload_stp_file(
file_path,
original_filename
)
# 2. 创建PostgreSQL记录
stp_file = STPFile(
user_id=user_id,
object_key=upload_result['object_key'],
storage_bucket=storage_manager.buckets['stp_files'],
original_filename=original_filename,
file_size=upload_result['file_size'],
file_hash=upload_result['file_hash'],
status="uploaded",
file_path=str(file_path) # 保留本地路径以兼容
)
session.add(stp_file)
await session.commit()
await session.refresh(stp_file)
logger.info(f"STP文件保存成功: {stp_file.id}")
return stp_file
async def save_geometry_data(self, session: AsyncSession,
stp_file_id: int,
geometry_json: Dict[str, Any],
analysis_method: str = "pythonocc") -> GeometryData:
"""保存几何数据到PostgreSQL元数据 + MinIO对象存储"""
# 1. 获取文件哈希
stp_file = await session.get(STPFile, stp_file_id)
file_hash = stp_file.file_hash
# 2. 上传到MinIO
upload_result = await storage_manager.upload_geometry_data(
geometry_json,
file_hash
)
# 3. 创建PostgreSQL记录
geometry_data = GeometryData(
stp_file_id=stp_file_id,
object_key=upload_result['object_key'],
storage_bucket=storage_manager.buckets['geometry_data'],
analysis_method=analysis_method,
# 提取摘要字段
volume=geometry_json.get('geometry_data', {}).get('volume'),
surface_area=geometry_json.get('geometry_data', {}).get('surface_area'),
bounding_box_min=geometry_json.get('geometry_data', {}).get('bounding_box', {}).get('min'),
bounding_box_max=geometry_json.get('geometry_data', {}).get('bounding_box', {}).get('max'),
center_of_mass=geometry_json.get('geometry_data', {}).get('center_of_mass'),
topology_faces=geometry_json.get('geometry_data', {}).get('topology', {}).get('faces'),
topology_edges=geometry_json.get('geometry_data', {}).get('topology', {}).get('edges'),
topology_vertices=geometry_json.get('geometry_data', {}).get('topology', {}).get('vertices')
)
session.add(geometry_data)
await session.commit()
await session.refresh(geometry_data)
logger.info(f"几何数据保存成功: {geometry_data.id}")
return geometry_data
async def save_mold_cavity_data(self, session: AsyncSession,
stp_file_id: int,
cavity_json: Dict[str, Any]) -> MoldCavityData:
"""保存模具型腔数据到PostgreSQL元数据 + MinIO对象存储"""
# 1. 获取文件哈希
stp_file = await session.get(STPFile, stp_file_id)
file_hash = stp_file.file_hash
# 2. 上传到MinIO
upload_result = await storage_manager.upload_mold_cavity_data(
cavity_json,
file_hash
)
# 3. 提取关键信息
metadata = cavity_json.get('metadata', {})
product_analysis = cavity_json.get('product_analysis', {})
manufacturing_info = cavity_json.get('manufacturing_info', {})
mold_size = manufacturing_info.get('estimated_mold_size', {})
key_info = cavity_json.get('mold_cavities', {}).get('cavity_key_info', {})
# 4. 创建PostgreSQL记录
mold_cavity = MoldCavityData(
stp_file_id=stp_file_id,
detailed_object_key=upload_result['object_key'],
storage_bucket=storage_manager.buckets['mold_cavities'],
# 模具参数
mold_material=manufacturing_info.get('recommended_material', 'Aluminum Alloy 7075'),
shrinkage_rate=metadata.get('shrinkage_rate', 0.005),
draft_angle=metadata.get('draft_angle', 2.0),
# 提取的摘要字段
cavity_key_info=key_info,
mold_size_length=mold_size.get('length'),
mold_size_width=mold_size.get('width'),
mold_size_height=mold_size.get('height'),
estimated_clamping_force=manufacturing_info.get('estimated_clamping_force'),
product_volume=product_analysis.get('volume'),
# 从key_info中提取(如果存在)
product_weight=key_info.get('geometric_characteristics', {}).get('product_weight'),
wall_thickness_range=key_info.get('geometric_characteristics', {}).get('wall_thickness_range'),
complexity_score=key_info.get('geometric_characteristics', {}).get('complexity_score'),
# 质量评估
weld_line_risk=key_info.get('quality_considerations', {}).get('potential_weld_lines'),
sink_mark_risk=key_info.get('quality_considerations', {}).get('sink_mark_areas'),
warpage_risk=key_info.get('quality_considerations', {}).get('warpage_risk')
)
session.add(mold_cavity)
await session.commit()
await session.refresh(mold_cavity)
logger.info(f"模具型腔数据保存成功: {mold_cavity.id}")
return mold_cavity
async def save_html_file(self, session: AsyncSession,
stp_file_id: int,
html_content: str,
filename: str) -> HTMLFile:
"""保存HTML文件到PostgreSQL元数据 + MinIO对象存储"""
# 1. 获取文件哈希
stp_file = await session.get(STPFile, stp_file_id)
file_hash = stp_file.file_hash
# 2. 上传到MinIO
upload_result = await storage_manager.upload_html_file(
html_content,
filename,
file_hash
)
# 3. 创建PostgreSQL记录
html_file = HTMLFile(
stp_file_id=stp_file_id,
object_key=upload_result['object_key'],
storage_bucket=storage_manager.buckets['html_files'],
filename=filename,
file_path=str(Path('html_output') / filename), # 保留本地路径
html_content=html_content # 保留内容以兼容
)
session.add(html_file)
await session.commit()
await session.refresh(html_file)
logger.info(f"HTML文件保存成功: {html_file.id}")
return html_file
async def save_features_and_recommendations(
self, session: AsyncSession,
stp_file_id: int,
features: list,
recommendations: list
):
"""保存特征检测结果和设计建议"""
# 1. 保存特征
for feature in features:
feature_record = FeatureDetection(
stp_file_id=stp_file_id,
feature_type=feature.get('feature_type'),
confidence=feature.get('confidence'),
location=feature.get('location'),
dimensions=feature.get('dimensions'),
parameters=feature.get('parameters')
)
session.add(feature_record)
# 2. 保存建议
for rec in recommendations:
rec_record = DesignRecommendation(
stp_file_id=stp_file_id,
rec_type=rec.get('rec_type'),
priority=rec.get('priority'),
description=rec.get('description'),
reason=rec.get('reason'),
parameters=rec.get('parameters')
)
session.add(rec_record)
await session.commit()
logger.info(f"保存了 {len(features)} 个特征和 {len(recommendations)} 个建议")
async def log_user_activity(self, session: AsyncSession,
user_id: int,
activity_type: str,
resource_type: Optional[str] = None,
resource_id: Optional[int] = None,
description: Optional[str] = None,
metadata: Optional[Dict] = None,
ip_address: Optional[str] = None,
user_agent: Optional[str] = None):
"""记录用户活动"""
activity = UserActivity(
user_id=user_id,
activity_type=activity_type,
resource_type=resource_type,
resource_id=resource_id,
description=description,
metadata=metadata,
ip_address=ip_address,
user_agent=user_agent
)
session.add(activity)
await session.commit()
logger.debug(f"用户活动记录: {activity_type} by user {user_id}")
async def get_stp_file_with_data(self, session: AsyncSession,
stp_file_id: int) -> Dict[str, Any]:
"""获取STP文件及其所有关联数据"""
# 1. 获取STP文件记录
stp_file = await session.get(STPFile, stp_file_id)
if not stp_file:
raise ValueError(f"STP文件不存在: {stp_file_id}")
result = {
'metadata': {
'id': stp_file.id,
'original_filename': stp_file.original_filename,
'file_size': stp_file.file_size,
'file_hash': stp_file.file_hash,
'upload_time': stp_file.upload_time.isoformat() if stp_file.upload_time else None,
'status': stp_file.status,
'user_id': stp_file.user_id
},
'geometry_data': None,
'mold_cavity_data': None,
'html_file': None,
'features': [],
'recommendations': []
}
# 2. 从MinIO获取数据
try:
# 几何数据
if stp_file.geometry_data:
geo_data_bytes = await storage_manager.download_file(
'geometry_data',
stp_file.geometry_data.object_key
)
result['geometry_data'] = json.loads(geo_data_bytes.decode('utf-8'))
# 模具型腔数据
if stp_file.mold_cavity_data:
cavity_data_bytes = await storage_manager.download_file(
'mold_cavities',
stp_file.mold_cavity_data.detailed_object_key
)
result['mold_cavity_data'] = json.loads(cavity_data_bytes.decode('utf-8'))
# HTML文件
if stp_file.html_file:
html_bytes = await storage_manager.download_file(
'html_files',
stp_file.html_file.object_key
)
result['html_content'] = html_bytes.decode('utf-8')
except Exception as e:
logger.error(f"从MinIO获取数据失败: {e}")
# 3. 从PostgreSQL获取特征和建议
features = await session.execute(
select(FeatureDetection).where(FeatureDetection.stp_file_id == stp_file_id)
)
result['features'] = [
{
'feature_type': f.feature_type,
'confidence': f.confidence,
'location': f.location,
'dimensions': f.dimensions,
'parameters': f.parameters
}
for f in features.scalars().all()
]
recommendations = await session.execute(
select(DesignRecommendation).where(DesignRecommendation.stp_file_id == stp_file_id)
)
result['recommendations'] = [
{
'rec_type': r.rec_type,
'priority': r.priority,
'description': r.description,
'reason': r.reason,
'parameters': r.parameters
}
for r in recommendations.scalars().all()
]
return result
async def delete_stp_file_cascade(self, session: AsyncSession,
stp_file_id: int):
"""级联删除STP文件及其所有关联数据"""
stp_file = await session.get(STPFile, stp_file_id)
if not stp_file:
raise ValueError(f"STP文件不存在: {stp_file_id}")
# 1. 删除MinIO中的文件
try:
if stp_file.object_key:
await storage_manager.delete_file('stp_files', stp_file.object_key)
except Exception as e:
logger.error(f"删除MinIO文件失败: {e}")
try:
if stp_file.geometry_data:
await storage_manager.delete_file('geometry_data', stp_file.geometry_data.object_key)
except Exception as e:
logger.error(f"删除几何数据失败: {e}")
try:
if stp_file.mold_cavity_data:
await storage_manager.delete_file('mold_cavities', stp_file.mold_cavity_data.detailed_object_key)
except Exception as e:
logger.error(f"删除型腔数据失败: {e}")
try:
if stp_file.html_file:
await storage_manager.delete_file('html_files', stp_file.html_file.object_key)
except Exception as e:
logger.error(f"删除HTML文件失败: {e}")
# 2. 级联删除PostgreSQL记录(通过外键自动处理)
await session.delete(stp_file)
await session.commit()
logger.info(f"STP文件及其关联数据已删除: {stp_file_id}")
# 全局存储集成服务实例
storage_integration = StorageIntegrationService()
@@ -0,0 +1,850 @@
# services/storage_integration_rustfs.py
"""存储集成服务 - 协调 PostgreSQL 和 RustFS"""
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, update
from pathlib import Path
from typing import Optional, Dict, Any
import json
from datetime import datetime
import uuid
from shared.models.database import (
STPFile, GeometryData, MeshData, MoldCavityData,
HTMLFile, ProcessingTask, User,
FeatureDetection, DesignRecommendation,
UserActivity, SystemLog
)
from moldinsight.storage.rustfs_storage import rustfs_manager
from shared.utils.logger import get_logger
logger = get_logger(__name__)
class StorageIntegrationService:
"""存储集成服务 - PostgreSQL + RustFS"""
@staticmethod
def _resolve_best_scheme_payload(cavity_json: Dict[str, Any]) -> Dict[str, Any]:
"""从新多方案/旧单方案结构中解析推荐方案和型腔详情。"""
if not isinstance(cavity_json, dict):
return {
"best_scheme_id": None,
"best_scheme": {},
"best_cavity_data": {},
"key_info": {},
}
candidate_schemes = cavity_json.get("candidate_schemes") or []
if not candidate_schemes:
key_info = cavity_json.get("mold_cavities", {}).get("cavity_key_info", {})
return {
"best_scheme_id": cavity_json.get("best_scheme_id"),
"best_scheme": {},
"best_cavity_data": cavity_json,
"key_info": key_info,
}
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
best_cavity_data = best_scheme.get("cavity_data", {}) if isinstance(best_scheme, dict) else {}
key_info = best_scheme.get("key_info", {}) if isinstance(best_scheme, dict) else {}
if not key_info:
key_info = best_cavity_data.get("mold_cavities", {}).get("cavity_key_info", {})
return {
"best_scheme_id": best_scheme.get("scheme_id") or best_scheme_id,
"best_scheme": best_scheme,
"best_cavity_data": best_cavity_data,
"key_info": key_info,
}
@staticmethod
def _parse_first_number(value: Any) -> Optional[float]:
if value is None:
return None
try:
return float(value)
except (TypeError, ValueError):
pass
import re
matches = re.findall(r"\d+(?:\.\d+)?", str(value))
if not matches:
return None
try:
return float(matches[0])
except (TypeError, ValueError):
return None
async def save_stp_file(self, session: AsyncSession,
file_path: Path,
original_filename: str,
user_id: Optional[int] = None,
upload_batch: Optional[str] = None) -> STPFile:
"""保存STP文件到PostgreSQL元数据 + RustFS对象存储
支持同一文件多次上传,每次上传都会创建新记录
"""
# 1. 上传到RustFS
upload_result = await rustfs_manager.upload_file(
file_type='stp_files',
file_path=file_path,
original_filename=original_filename,
metadata={
'original_filename': original_filename,
'user_id': str(user_id) if user_id else 'anonymous',
'upload_batch': upload_batch or str(uuid.uuid4())
}
)
file_hash = upload_result['file_hash']
batch_id = upload_batch or str(uuid.uuid4())
# 2. 创建新PostgreSQL记录(每次上传都创建新记录)
from datetime import datetime
stp_file = STPFile(
user_id=user_id,
object_key=upload_result['object_key'],
storage_bucket=upload_result['bucket'],
original_filename=original_filename,
file_size=upload_result['file_size'],
file_hash=file_hash,
upload_batch=batch_id,
status="uploaded",
file_path=str(file_path),
upload_time=datetime.now()
)
session.add(stp_file)
await session.commit()
await session.refresh(stp_file)
logger.info(f"STP文件保存成功 RustFS: {stp_file.id}, 批次: {batch_id}")
return stp_file
async def create_processing_task(
self,
session: AsyncSession,
task_id: str,
stp_file_id: int,
task_type: str = "stp_parsing",
parameters: Optional[Dict[str, Any]] = None,
) -> ProcessingTask:
"""创建处理任务记录"""
try:
task = ProcessingTask(
task_id=task_id,
stp_file_id=stp_file_id,
task_type=task_type,
status="pending",
started_time=datetime.now(),
parameters=parameters or {},
)
session.add(task)
await session.commit()
await session.refresh(task)
logger.info(f"处理任务创建成功: {task_id}")
return task
except Exception as e:
await session.rollback()
logger.error(f"创建处理任务失败: {e}")
raise
async def update_task_status(
self,
session: AsyncSession,
task_id: str,
status: str,
progress: Optional[int] = None,
current_step: Optional[str] = None,
error_message: Optional[str] = None
):
"""更新任务状态"""
try:
update_data = {
"status": status,
"completed_time": datetime.now() if status in ["completed", "failed"] else None,
"error_message": error_message
}
if progress is not None:
update_data["progress"] = progress
if current_step is not None:
update_data["current_step"] = current_step
await session.execute(
update(ProcessingTask)
.where(ProcessingTask.task_id == task_id)
.values(**update_data)
)
await session.commit()
logger.info(f"任务状态更新: {task_id} -> {status}")
except Exception as e:
await session.rollback()
logger.error(f"更新任务状态失败: {e}")
raise
async def update_task_parameters(
self,
session: AsyncSession,
task_id: str,
parameters: Dict[str, Any],
):
"""合并更新任务参数,便于保存阶段耗时等元数据。"""
try:
task = await session.execute(
select(ProcessingTask).where(ProcessingTask.task_id == task_id)
)
task = task.scalar_one_or_none()
if task is None:
return
merged = dict(task.parameters or {})
merged.update(parameters or {})
task.parameters = merged
await session.commit()
except Exception as e:
await session.rollback()
logger.error(f"更新任务参数失败: {e}")
raise
async def update_stp_file_status(self, session: AsyncSession, stp_file_id: int, status: str):
"""更新STP文件状态"""
try:
await session.execute(
update(STPFile)
.where(STPFile.id == stp_file_id)
.values(
status=status,
processed_time=datetime.now() if status in ["completed", "failed"] else None
)
)
await session.commit()
logger.info(f"STP文件状态更新: ID {stp_file_id} -> {status}")
except Exception as e:
await session.rollback()
logger.error(f"更新STP文件状态失败: {e}")
raise
async def save_geometry_data(self, session: AsyncSession,
stp_file_id: int,
geometry_json: Dict[str, Any],
analysis_method: str = "pythonocc") -> GeometryData:
"""保存几何数据到PostgreSQL元数据 + RustFS对象存储"""
# 1. 获取文件哈希
stp_file = await session.get(STPFile, stp_file_id)
file_hash = stp_file.file_hash
# 2. 上传到RustFS
upload_result = await rustfs_manager.upload_json_data(
file_type='geometry_data',
json_data=geometry_json,
file_hash=file_hash
)
# 3. 提取几何数据
if 'geometry_data' in geometry_json:
geo_data = geometry_json['geometry_data']
else:
geo_data = geometry_json
# 4. 创建PostgreSQL记录
geometry_data = GeometryData(
stp_file_id=stp_file_id,
object_key=upload_result['object_key'],
storage_bucket=upload_result['bucket'],
analysis_method=analysis_method,
# 提取摘要字段
volume=geo_data.get('volume'),
surface_area=geo_data.get('surface_area'),
bounding_box_min=geo_data.get('bounding_box', {}).get('min'),
bounding_box_max=geo_data.get('bounding_box', {}).get('max'),
center_of_mass=geo_data.get('center_of_mass'),
topology_faces=geo_data.get('topology', {}).get('faces'),
topology_edges=geo_data.get('topology', {}).get('edges'),
topology_vertices=geo_data.get('topology', {}).get('vertices')
)
session.add(geometry_data)
await session.commit()
await session.refresh(geometry_data)
logger.info(f"几何数据保存成功 RustFS: {geometry_data.id}")
return geometry_data
async def save_mesh_data(
self,
session: AsyncSession,
stp_file_id: int,
mesh_json: Dict[str, Any],
quality: str = "medium"
) -> MeshData:
"""保存网格数据到 PostgreSQL 元数据 + RustFS 对象存储
mesh_json 为完整网格 JSON(顶点、面、点云等),
PostgreSQL 只存 object_key 和一些摘要字段,详细数据放在 RustFS。
"""
# 1. 获取文件哈希
stp_file = await session.get(STPFile, stp_file_id)
file_hash = stp_file.file_hash
# 2. 上传网格 JSON 到 RustFS
upload_result = await rustfs_manager.upload_json_data(
file_type='mesh_data',
json_data=mesh_json,
file_hash=file_hash
)
# 3. 提取摘要信息
mesh_section = mesh_json.get('mesh', {})
pointcloud_section = mesh_json.get('pointcloud', {})
bbox = mesh_json.get('bounding_box', {})
vertices = mesh_section.get('vertices') or []
faces = mesh_section.get('faces') or []
vertex_count = len(vertices)
face_count = len(faces)
point_count = pointcloud_section.get('count')
# 4. 创建 PostgreSQL 记录
mesh_data = MeshData(
stp_file_id=stp_file_id,
object_key=upload_result['object_key'],
storage_bucket=upload_result['bucket'],
quality=quality,
vertex_count=vertex_count,
face_count=face_count,
point_count=point_count,
bounding_box_min=bbox.get('min'),
bounding_box_max=bbox.get('max')
)
session.add(mesh_data)
await session.commit()
await session.refresh(mesh_data)
logger.info(f"网格数据保存成功 RustFS: {mesh_data.id}")
return mesh_data
async def save_mold_cavity_data(self, session: AsyncSession,
stp_file_id: int,
cavity_json: Dict[str, Any]) -> MoldCavityData:
"""保存模具型腔数据到PostgreSQL元数据 + RustFS对象存储"""
# 1. 获取文件哈希
stp_file = await session.get(STPFile, stp_file_id)
file_hash = stp_file.file_hash
# 2. 上传到RustFS
upload_result = await rustfs_manager.upload_json_data(
file_type='mold_cavities',
json_data=cavity_json,
file_hash=file_hash
)
# 3. 提取关键信息(兼容多方案与单方案结构)
payload = self._resolve_best_scheme_payload(cavity_json)
best_scheme_id = payload.get("best_scheme_id")
best_scheme = payload.get("best_scheme") or {}
best_cavity_data = payload.get("best_cavity_data") or {}
metadata = best_cavity_data.get('metadata', {})
product_analysis = best_cavity_data.get('product_analysis', {})
manufacturing_info = best_cavity_data.get('manufacturing_info', {})
mold_size = manufacturing_info.get('estimated_mold_size', {})
key_info = payload.get("key_info") or {}
if not key_info:
key_info = best_cavity_data.get('mold_cavities', {}).get('cavity_key_info', {})
mold_material = (
metadata.get("selected_material")
or manufacturing_info.get("recommended_material")
or 'Aluminum Alloy 7075'
)
parting_line_length = self._parse_first_number(
manufacturing_info.get("parting_line_length")
)
# 4. 创建PostgreSQL记录
mold_cavity = MoldCavityData(
stp_file_id=stp_file_id,
detailed_object_key=upload_result['object_key'],
storage_bucket=upload_result['bucket'],
# 模具参数
mold_material=mold_material,
shrinkage_rate=metadata.get('shrinkage_rate', 0.005),
draft_angle=metadata.get('draft_angle', 2.0),
parting_line_length=parting_line_length,
# 提取的摘要字段
cavity_key_info=key_info,
mold_size_length=mold_size.get('length'),
mold_size_width=mold_size.get('width'),
mold_size_height=mold_size.get('height'),
estimated_clamping_force=manufacturing_info.get('estimated_clamping_force'),
product_volume=product_analysis.get('volume'),
# 从key_info中提取(如果存在)
product_weight=key_info.get('geometric_characteristics', {}).get('product_weight'),
wall_thickness_range=key_info.get('geometric_characteristics', {}).get('wall_thickness_range'),
complexity_score=key_info.get('geometric_characteristics', {}).get('complexity_score'),
# 质量评估
weld_line_risk=key_info.get('quality_considerations', {}).get('potential_weld_lines'),
sink_mark_risk=key_info.get('quality_considerations', {}).get('sink_mark_areas'),
warpage_risk=key_info.get('quality_considerations', {}).get('warpage_risk'),
# 多方案可信化摘要
best_scheme_id=best_scheme_id,
confidence_score=best_scheme.get("confidence_score"),
is_fallback=best_scheme.get("is_fallback"),
fallback_reason=best_scheme.get("fallback_reason"),
)
session.add(mold_cavity)
await session.commit()
await session.refresh(mold_cavity)
logger.info(f"模具型腔数据保存成功 RustFS: {mold_cavity.id}")
return mold_cavity
async def save_html_file(self, session: AsyncSession,
stp_file_id: int,
filename: str,
file_path: str,
html_content: Optional[str] = None,
visualization_type: str = "3d_viewer") -> HTMLFile:
"""保存HTML文件到PostgreSQL元数据 + RustFS对象存储"""
# 1. 获取文件哈希
stp_file = await session.get(STPFile, stp_file_id)
file_hash = stp_file.file_hash
# 2. 读取HTML内容(如果未提供)
if html_content is None:
try:
with open(file_path, 'r', encoding='utf-8') as f:
html_content = f.read()
except Exception as e:
logger.error(f"读取HTML文件失败: {e}")
html_content = ""
# 3. 上传到RustFS
html_json = {'content': html_content, 'filename': filename}
upload_result = await rustfs_manager.upload_json_data(
file_type='html_files',
json_data=html_json,
file_hash=file_hash
)
# 4. 创建PostgreSQL记录
html_file = HTMLFile(
stp_file_id=stp_file_id,
object_key=upload_result['object_key'],
storage_bucket=upload_result['bucket'],
filename=filename,
file_path=file_path, # 保留本地路径
html_content=html_content, # 保留内容以兼容
visualization_type=visualization_type
)
session.add(html_file)
await session.commit()
await session.refresh(html_file)
logger.info(f"HTML文件保存成功 RustFS: {html_file.id}")
return html_file
async def save_features_and_recommendations(
self, session: AsyncSession,
stp_file_id: int,
features: list,
recommendations: list
):
"""保存特征检测结果和设计建议"""
# 1. 保存特征
for feature in features:
feature_record = FeatureDetection(
stp_file_id=stp_file_id,
feature_type=feature.get('feature_type'),
confidence=feature.get('confidence'),
location=feature.get('location'),
dimensions=feature.get('dimensions'),
parameters=feature.get('parameters')
)
session.add(feature_record)
# 2. 保存建议
for rec in recommendations:
rec_record = DesignRecommendation(
stp_file_id=stp_file_id,
rec_type=rec.get('type') or rec.get('rec_type'),
priority=rec.get('priority'),
description=rec.get('description'),
reason=rec.get('reason'),
parameters=rec.get('parameters')
)
session.add(rec_record)
await session.commit()
logger.info(f"保存了 {len(features)} 个特征和 {len(recommendations)} 个建议")
async def log_user_activity(self, session: AsyncSession,
user_id: int,
activity_type: str,
resource_type: Optional[str] = None,
resource_id: Optional[int] = None,
description: Optional[str] = None,
metadata: Optional[Dict] = None,
ip_address: Optional[str] = None,
user_agent: Optional[str] = None):
"""记录用户活动"""
activity = UserActivity(
user_id=user_id,
activity_type=activity_type,
resource_type=resource_type,
resource_id=resource_id,
description=description,
meta_data=metadata,
ip_address=ip_address,
user_agent=user_agent
)
session.add(activity)
await session.commit()
logger.debug(f"用户活动记录: {activity_type} by user {user_id}")
async def get_stp_file_with_data(self, session: AsyncSession,
stp_file_id: int) -> Dict[str, Any]:
"""获取STP文件及其所有关联数据"""
from sqlalchemy.orm import joinedload
try:
# 1. 获取STP文件记录(使用 joinedload 预加载关联数据)
result = await session.execute(
select(STPFile).options(
joinedload(STPFile.geometry_data),
joinedload(STPFile.mesh_data),
joinedload(STPFile.mold_cavity_data),
joinedload(STPFile.html_file),
joinedload(STPFile.analysis_metrics)
).where(STPFile.id == stp_file_id)
)
stp_file = result.scalar_one_or_none()
if not stp_file:
raise ValueError(f"STP文件不存在: {stp_file_id}")
except Exception as e:
logger.error(f"获取STP文件记录失败: {e}")
raise
result = {
'metadata': {
'id': stp_file.id,
'original_filename': stp_file.original_filename,
'file_size': stp_file.file_size,
'file_hash': stp_file.file_hash,
'upload_time': stp_file.upload_time.isoformat() if stp_file.upload_time else None,
'status': stp_file.status,
'user_id': stp_file.user_id
},
'geometry_data': None,
'mesh_data': None,
'mold_cavity_data': None,
'html_content': None,
'features': [],
'recommendations': [],
'analysis_metrics': None # 新增分析指标字段
}
# 2. 从RustFS获取数据
try:
# 几何数据
if stp_file.geometry_data:
geo_data_bytes = await rustfs_manager.download_file(
file_type='geometry_data',
object_key=stp_file.geometry_data.object_key
)
result['geometry_data'] = json.loads(geo_data_bytes.decode('utf-8'))
# 模具型腔数据
if stp_file.mold_cavity_data:
cavity_data_bytes = await rustfs_manager.download_file(
file_type='mold_cavities',
object_key=stp_file.mold_cavity_data.detailed_object_key
)
result['mold_cavity_data'] = json.loads(cavity_data_bytes.decode('utf-8'))
# 网格数据
if stp_file.mesh_data:
mesh_bytes = await rustfs_manager.download_file(
file_type='mesh_data',
object_key=stp_file.mesh_data.object_key
)
result['mesh_data'] = json.loads(mesh_bytes.decode('utf-8'))
# HTML文件
if stp_file.html_file:
html_bytes = await rustfs_manager.download_file(
file_type='html_files',
object_key=stp_file.html_file.object_key
)
html_json = json.loads(html_bytes.decode('utf-8'))
result['html_content'] = html_json.get('content', '')
except Exception as e:
logger.error(f"从RustFS获取数据失败: {e}")
# 3. 从PostgreSQL获取特征和建议
features = await session.execute(
select(FeatureDetection).where(FeatureDetection.stp_file_id == stp_file_id)
)
result['features'] = [
{
'feature_type': f.feature_type,
'confidence': f.confidence,
'location': f.location,
'dimensions': f.dimensions,
'parameters': f.parameters
}
for f in features.scalars().all()
]
recommendations = await session.execute(
select(DesignRecommendation).where(DesignRecommendation.stp_file_id == stp_file_id)
)
result['recommendations'] = [
{
'type': r.rec_type, # 改为 type 以匹配前端期望的字段名
'priority': r.priority,
'description': r.description,
'reason': r.reason,
'parameters': r.parameters
}
for r in recommendations.scalars().all()
]
# 4. 获取分析指标
if stp_file.analysis_metrics:
result['analysis_metrics'] = {
'volume_utilization': stp_file.analysis_metrics.volume_utilization,
'topology_complexity': stp_file.analysis_metrics.topology_complexity,
'wall_uniformity': stp_file.analysis_metrics.wall_uniformity,
'analysis_summary': stp_file.analysis_metrics.analysis_summary,
'verification_status': stp_file.analysis_metrics.verification_status,
'verification_volume_diff': stp_file.analysis_metrics.verification_volume_diff,
'verification_area_diff': stp_file.analysis_metrics.verification_area_diff,
'verification_details': stp_file.analysis_metrics.verification_details,
}
return result
async def get_file_history_by_filename(
self,
session: AsyncSession,
filename: str,
user_id: Optional[int] = None,
limit: int = 50
) -> list:
"""获取同一文件名的所有上传历史记录"""
from shared.models.database import ProcessingTask
from sqlalchemy.orm import joinedload
query = select(STPFile).options(
joinedload(STPFile.processing_tasks)
).where(
STPFile.original_filename == filename
).order_by(STPFile.upload_time.desc())
if user_id:
query = query.where(STPFile.user_id == user_id)
query = query.limit(limit)
result = await session.execute(query)
files = result.unique().scalars().all()
return [
{
'id': f.id,
'task_id': f.processing_tasks[0].task_id if f.processing_tasks else None,
'upload_batch': f.upload_batch,
'upload_time': f.upload_time.strftime('%Y-%m-%d %H:%M:%S') if f.upload_time else None,
'file_size': f.file_size,
'status': f.status,
'volume': f.volume,
'surface_area': f.surface_area,
'product_weight': f.product_weight,
'has_analysis': f.status == 'completed'
}
for f in files
]
async def get_all_file_groups(
self,
session: AsyncSession,
user_id: Optional[int] = None,
limit: int = 100
) -> list:
"""获取所有文件分组(按文件名分组),包含每个文件的最新分析结果"""
from sqlalchemy import func, desc
from sqlalchemy.orm import joinedload
from shared.models.database import ProcessingTask
# 子查询:获取每个文件名的最新上传
subquery = (
select(
STPFile.original_filename,
func.max(STPFile.upload_time).label('latest_upload')
)
.group_by(STPFile.original_filename)
.order_by(desc('latest_upload'))
.limit(limit)
)
if user_id:
subquery = subquery.where(STPFile.user_id == user_id)
subquery = subquery.subquery()
# 主查询:获取最新记录和统计信息
query = (
select(STPFile).options(
joinedload(STPFile.processing_tasks)
)
.join(
subquery,
(STPFile.original_filename == subquery.c.original_filename) &
(STPFile.upload_time == subquery.c.latest_upload)
)
.order_by(STPFile.upload_time.desc())
)
result = await session.execute(query)
latest_files = result.unique().scalars().all()
# 获取每个文件名的上传次数
file_groups = []
for f in latest_files:
count_query = select(func.count()).where(
STPFile.original_filename == f.original_filename
)
if user_id:
count_query = count_query.where(STPFile.user_id == user_id)
count_result = await session.execute(count_query)
upload_count = count_result.scalar()
task_id = f.processing_tasks[0].task_id if f.processing_tasks else None
file_groups.append({
'filename': f.original_filename,
'latest_id': f.id,
'latest_task_id': task_id,
'latest_upload_time': f.upload_time.strftime('%Y-%m-%d %H:%M:%S') if f.upload_time else None,
'latest_status': f.status,
'upload_count': upload_count,
'file_size': f.file_size,
'volume': f.volume,
'surface_area': f.surface_area,
'product_weight': f.product_weight
})
return file_groups
async def update_stp_file_analysis_summary(
self,
session: AsyncSession,
stp_file_id: int,
volume: Optional[float] = None,
surface_area: Optional[float] = None,
product_weight: Optional[float] = None
):
"""更新STP文件的分析摘要字段(用于快速查询)"""
try:
update_data = {}
if volume is not None:
update_data['volume'] = volume
if surface_area is not None:
update_data['surface_area'] = surface_area
if product_weight is not None:
update_data['product_weight'] = product_weight
if update_data:
await session.execute(
update(STPFile)
.where(STPFile.id == stp_file_id)
.values(**update_data)
)
await session.commit()
logger.info(f"STP文件分析摘要更新: ID {stp_file_id}")
except Exception as e:
await session.rollback()
logger.error(f"更新STP文件分析摘要失败: {e}")
raise
async def delete_stp_file_cascade(self, session: AsyncSession,
stp_file_id: int):
"""级联删除STP文件及其所有关联数据"""
stp_file = await session.get(STPFile, stp_file_id)
if not stp_file:
raise ValueError(f"STP文件不存在: {stp_file_id}")
# 1. 删除RustFS中的文件
try:
if stp_file.object_key:
await rustfs_manager.delete_file('stp_files', stp_file.object_key)
except Exception as e:
logger.error(f"删除RustFS文件失败: {e}")
try:
if stp_file.geometry_data:
await rustfs_manager.delete_file('geometry_data', stp_file.geometry_data.object_key)
except Exception as e:
logger.error(f"删除几何数据失败: {e}")
try:
if stp_file.mold_cavity_data:
await rustfs_manager.delete_file('mold_cavities', stp_file.mold_cavity_data.detailed_object_key)
except Exception as e:
logger.error(f"删除型腔数据失败: {e}")
try:
if stp_file.mesh_data:
await rustfs_manager.delete_file('mesh_data', stp_file.mesh_data.object_key)
except Exception as e:
logger.error(f"删除网格数据失败: {e}")
try:
if stp_file.html_file:
await rustfs_manager.delete_file('html_files', stp_file.html_file.object_key)
except Exception as e:
logger.error(f"删除HTML文件失败: {e}")
# 2. 级联删除PostgreSQL记录(通过外键自动处理)
await session.delete(stp_file)
await session.commit()
logger.info(f"STP文件及其关联数据已删除: {stp_file_id}")
# 全局存储集成服务实例
storage_integration = StorageIntegrationService()
+296
View File
@@ -0,0 +1,296 @@
# services/storage_service.py
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, update
from datetime import datetime
import hashlib
import json
from pathlib import Path
from typing import Optional, Dict, Any
from shared.models.database import STPFile, GeometryData, HTMLFile, ProcessingTask
from shared.utils.logger import get_logger
from shared.models.database import MoldCavityData
logger = get_logger(__name__)
class StorageService:
"""数据存储服务"""
def __init__(self, db_session: AsyncSession):
self.db_session = db_session
async def save_stp_file(
self,
filename: str,
file_path: str,
file_size: int,
file_content: Optional[bytes] = None
) -> STPFile:
"""保存STP文件信息到数据库"""
try:
# 计算文件哈希
file_hash = self._calculate_file_hash(file_path, file_content)
# 检查是否已存在相同文件
existing_file = await self.db_session.execute(
select(STPFile).where(STPFile.file_hash == file_hash)
)
existing_file = existing_file.scalar_one_or_none()
if existing_file:
logger.info(f"文件已存在,跳过保存: {filename}")
return existing_file
# 创建新的STP文件记录
stp_file = STPFile(
filename=filename,
original_filename=filename,
file_path=file_path,
file_size=file_size,
file_hash=file_hash,
file_content=file_content,
upload_time=datetime.now(),
status="pending",
# 必填字段提供默认值
object_key=f"stp_files/{file_hash}",
storage_bucket="default",
object_url=None
)
self.db_session.add(stp_file)
await self.db_session.commit()
await self.db_session.refresh(stp_file)
logger.info(f"STP文件保存成功: {filename} (ID: {stp_file.id})")
return stp_file
except Exception as e:
await self.db_session.rollback()
logger.error(f"保存STP文件失败: {e}")
raise
async def save_geometry_data(
self,
stp_file_id: int,
geometry_json: Dict[str, Any],
analysis_method: str
) -> GeometryData:
"""保存几何数据JSON到数据库"""
try:
# 提取关键几何属性用于快速查询
volume = geometry_json.get("volume")
surface_area = geometry_json.get("surface_area")
bounding_box = geometry_json.get("bounding_box", {})
geometry_data = GeometryData(
stp_file_id=stp_file_id,
analysis_method=analysis_method,
volume=volume,
surface_area=surface_area,
bounding_box_min=bounding_box.get("min"),
bounding_box_max=bounding_box.get("max"),
created_time=datetime.now(),
# 必填字段提供默认值
object_key=f"geometry_data/{stp_file_id}",
storage_bucket="default",
object_url=None
)
self.db_session.add(geometry_data)
await self.db_session.commit()
await self.db_session.refresh(geometry_data)
logger.info(f"几何数据保存成功: STP文件ID {stp_file_id}")
return geometry_data
except Exception as e:
await self.db_session.rollback()
logger.error(f"保存几何数据失败: {e}")
raise
async def save_html_file(
self,
stp_file_id: int,
filename: str,
file_path: str,
html_content: Optional[str] = None,
visualization_type: str = "3d_viewer"
) -> HTMLFile:
"""保存HTML文件信息到数据库"""
try:
html_file = HTMLFile(
stp_file_id=stp_file_id,
filename=filename,
file_path=file_path,
html_content=html_content,
visualization_type=visualization_type,
has_interactive_elements=True,
generated_time=datetime.now(),
# 必填字段提供默认值
object_key=f"html_files/{stp_file_id}",
storage_bucket="default",
object_url=None
)
self.db_session.add(html_file)
await self.db_session.commit()
await self.db_session.refresh(html_file)
logger.info(f"HTML文件保存成功: {filename} (STP文件ID: {stp_file_id})")
return html_file
except Exception as e:
await self.db_session.rollback()
logger.error(f"保存HTML文件失败: {e}")
raise
async def create_processing_task(
self,
task_id: str,
stp_file_id: int,
task_type: str = "stp_parsing"
) -> ProcessingTask:
"""创建处理任务记录"""
try:
task = ProcessingTask(
task_id=task_id,
stp_file_id=stp_file_id,
task_type=task_type,
status="pending",
started_time=datetime.now()
)
self.db_session.add(task)
await self.db_session.commit()
await self.db_session.refresh(task)
logger.info(f"处理任务创建成功: {task_id}")
return task
except Exception as e:
await self.db_session.rollback()
logger.error(f"创建处理任务失败: {e}")
raise
async def update_task_status(
self,
task_id: str,
status: str,
progress: Optional[int] = None,
current_step: Optional[str] = None,
error_message: Optional[str] = None
):
"""更新任务状态"""
try:
update_data = {
"status": status,
"completed_time": datetime.now() if status in ["completed", "failed"] else None,
"error_message": error_message
}
if progress is not None:
update_data["progress"] = progress
if current_step is not None:
update_data["current_step"] = current_step
await self.db_session.execute(
update(ProcessingTask)
.where(ProcessingTask.task_id == task_id)
.values(**update_data)
)
await self.db_session.commit()
logger.info(f"任务状态更新: {task_id} -> {status}")
except Exception as e:
await self.db_session.rollback()
logger.error(f"更新任务状态失败: {e}")
raise
async def update_stp_file_status(self, stp_file_id: int, status: str):
"""更新STP文件状态"""
try:
await self.db_session.execute(
update(STPFile)
.where(STPFile.id == stp_file_id)
.values(
status=status,
processed_time=datetime.now() if status in ["completed", "failed"] else None
)
)
await self.db_session.commit()
logger.info(f"STP文件状态更新: ID {stp_file_id} -> {status}")
except Exception as e:
await self.db_session.rollback()
logger.error(f"更新STP文件状态失败: {e}")
raise
async def get_stp_file_by_id(self, stp_file_id: int) -> Optional[STPFile]:
"""根据ID获取STP文件"""
try:
result = await self.db_session.execute(
select(STPFile).where(STPFile.id == stp_file_id)
)
return result.scalar_one_or_none()
except Exception as e:
logger.error(f"获取STP文件失败: {e}")
return None
async def get_geometry_data_by_stp_file_id(self, stp_file_id: int) -> Optional[GeometryData]:
"""根据STP文件ID获取几何数据"""
try:
result = await self.db_session.execute(
select(GeometryData).where(GeometryData.stp_file_id == stp_file_id)
)
return result.scalar_one_or_none()
except Exception as e:
logger.error(f"获取几何数据失败: {e}")
return None
def _calculate_file_hash(self, file_path: str, file_content: Optional[bytes] = None) -> str:
"""计算文件哈希值"""
sha256_hash = hashlib.sha256()
if file_content:
sha256_hash.update(file_content)
else:
# 从文件路径读取内容计算哈希
with open(file_path, "rb") as f:
for chunk in iter(lambda: f.read(4096), b""):
sha256_hash.update(chunk)
return sha256_hash.hexdigest()
async def save_mold_cavity_data(
self,
stp_file_id: int,
cavity_json: Dict[str, Any],
key_info: Dict[str, Any]
) -> MoldCavityData:
"""保存模具型腔数据"""
try:
mold_data = MoldCavityData(
stp_file_id=stp_file_id,
cavity_key_info=key_info,
shrinkage_rate=cavity_json["metadata"]["shrinkage_rate"],
draft_angle=cavity_json["metadata"]["draft_angle"],
generated_time=datetime.now(),
# 必填字段提供默认值
detailed_object_key=f"mold_cavity/{stp_file_id}",
storage_bucket="default"
)
self.db_session.add(mold_data)
await self.db_session.commit()
await self.db_session.refresh(mold_data)
logger.info(f"模具型腔数据保存成功: STP文件ID {stp_file_id}")
return mold_data
except Exception as e:
await self.db_session.rollback()
logger.error(f"保存模具型腔数据失败: {e}")
raise
@@ -0,0 +1,201 @@
# services/task_query_service.py
"""任务状态查询服务 — 从 task_router.py 中的持久化任务组装逻辑抽取"""
from typing import Optional, Dict, Any, List
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import joinedload
from moldinsight.services.storage_integration_rustfs import StorageIntegrationService
from shared.services.redis_task_manager import redis_task_manager
from shared.models.database import ProcessingTask, STPFile, MeshData, HTMLFile
from shared.utils.logger import get_logger
logger = get_logger(__name__)
class TaskQueryService:
"""任务状态查询与视图组装"""
@staticmethod
async def get_task_view(db_session: AsyncSession, task_id: str) -> Optional[Dict[str, Any]]:
"""
获取任务视图 — 优先返回 Redis 缓存,否则从 PostgreSQL + RustFS 组装
Returns:
任务视图字典,如果任务不存在返回 None
"""
# 1. Redis/内存任务(进行中的任务直接返回,已完成/失败的走DB路径获取完整数据)
task = await redis_task_manager.get_task(task_id)
if task:
status = task.get("status")
if status and status not in ("completed", "failed"):
logger.info(f"返回缓存任务状态:{task_id} - {status}")
return task
# 2. 持久化任务(已完成/失败,或服务重启后的任务)
storage_service = StorageIntegrationService()
# 查询任务和文件元数据(预加载 html_file 关联)
result = await db_session.execute(
select(ProcessingTask, STPFile)
.join(STPFile, ProcessingTask.stp_file_id == STPFile.id)
.where(ProcessingTask.task_id == task_id)
.options(joinedload(STPFile.html_file))
)
row = result.unique().first()
if not row:
return None
processing_task, stp_file = row
# 从 RustFS 取几何 / 型腔 / 网格详细 JSON
try:
file_with_data = await storage_service.get_stp_file_with_data(
db_session, stp_file_id=stp_file.id
)
except Exception as e:
logger.error(f"获取文件数据失败: {e}")
file_with_data = {}
# 解析 geometry_json
geometry_json = TaskQueryService._extract_geometry_json(file_with_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", [])
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)
# 构造 html_file 路径(与即时分析的 /html/xxx.html 格式保持一致)
html_file_url = None
html_file_record = None
try:
html_file_record = await db_session.execute(
select(HTMLFile).where(HTMLFile.stp_file_id == stp_file.id)
)
html_file_record = html_file_record.scalar_one_or_none()
except Exception:
pass
if html_file_record and html_file_record.filename:
html_file_url = f"/html/{html_file_record.filename}"
if cavity_view.get("html_file"):
html_file_url = cavity_view.get("html_file")
# 构造与内存任务兼容的任务视图
cam_preferences = {}
task_parameters = {}
if isinstance(processing_task.parameters, dict):
cam_preferences = processing_task.parameters.get("cam_preferences", {}) or {}
task_parameters = dict(processing_task.parameters)
task_view = {
"task_id": processing_task.task_id,
"status": processing_task.status,
"filename": stp_file.original_filename if stp_file else "",
"file_path": stp_file.file_path or "",
"file_size": stp_file.file_size if stp_file else 0,
"upload_time": processing_task.created_time.isoformat()
if processing_task.created_time
else "",
"completed_at": processing_task.completed_time.isoformat()
if processing_task.completed_time
else "",
"geometry_data": geometry_json,
"key_info": cavity_view.get("key_info"),
"cavity_data": cavity_view.get("cavity_data"),
"candidate_schemes": cavity_view.get("candidate_schemes", []),
"best_scheme_id": cavity_view.get("best_scheme_id"),
"cam_preferences": cam_preferences,
"plan_result": cavity_json,
"mesh_summary": mesh_summary,
"html_file": html_file_url,
"material": task_parameters.get("material"),
"parameters": task_parameters,
"export_artifacts": task_parameters.get("export_artifacts"),
"stage_timings": task_parameters.get("stage_timings", {}),
"verification": task_parameters.get("verification")
or file_with_data.get("analysis_metrics", {}).get("verification_details"),
"llm_report": task_parameters.get("llm_report"),
"analysis_result": {
"geometry_data": geometry_json,
"detected_features": features_json,
"design_recommendations": recommendations_json,
"quality_metrics": {
"volume_utilization": file_with_data.get("analysis_metrics", {}).get("volume_utilization", 0),
"topology_complexity": file_with_data.get("analysis_metrics", {}).get("topology_complexity", 0),
"wall_uniformity": file_with_data.get("analysis_metrics", {}).get("wall_uniformity", 0)
},
"analysis_summary": file_with_data.get("analysis_metrics", {}).get("analysis_summary", "分析完成")
} if geometry_json or features_json or recommendations_json else None,
"error": processing_task.error_message or stp_file.error_message or None,
}
logger.info(f"返回持久化任务状态: {task_id} - {processing_task.status}")
return task_view
@staticmethod
def _extract_geometry_json(file_with_data: dict) -> Optional[Dict[str, Any]]:
"""从 file_with_data 中提取 geometry_json"""
if not file_with_data.get("geometry_data"):
return None
geo_raw = file_with_data["geometry_data"]
if isinstance(geo_raw, dict):
if "geometry_data" in geo_raw:
return geo_raw["geometry_data"]
return geo_raw
return None
@staticmethod
async def _get_mesh_summary(db_session: AsyncSession, stp_file_id: int) -> Optional[Dict[str, Any]]:
"""从数据库查询网格摘要"""
mesh_record = await db_session.execute(
select(MeshData).where(MeshData.stp_file_id == stp_file_id)
)
mesh_record = mesh_record.scalar_one_or_none()
if mesh_record:
return {
"vertex_count": mesh_record.vertex_count,
"face_count": mesh_record.face_count,
"point_count": mesh_record.point_count,
"quality": mesh_record.quality,
}
return None
@staticmethod
def _extract_cavity_view(cavity_json: Optional[Dict[str, Any]]) -> Dict[str, Any]:
"""兼容旧单方案与新多方案结果视图。"""
if not cavity_json:
return {
"cavity_data": None,
"key_info": None,
"candidate_schemes": [],
"best_scheme_id": None,
}
candidate_schemes = cavity_json.get("candidate_schemes")
if candidate_schemes:
best_scheme_id = cavity_json.get("best_scheme_id")
best_scheme = candidate_schemes[0]
if best_scheme_id:
for scheme in candidate_schemes:
if scheme.get("scheme_id") == best_scheme_id:
best_scheme = scheme
break
return {
"cavity_data": best_scheme.get("cavity_data"),
"key_info": best_scheme.get("key_info"),
"candidate_schemes": candidate_schemes,
"best_scheme_id": best_scheme_id or best_scheme.get("scheme_id"),
"html_file": best_scheme.get("html_file"),
}
return {
"cavity_data": cavity_json,
"key_info": cavity_json,
"candidate_schemes": [],
"best_scheme_id": None,
"html_file": cavity_json.get("html_file"),
}
@@ -0,0 +1,457 @@
"""
几何验证服务
使用 FreeCAD 和 PythonOCC 交叉验证几何数据准确性
"""
import asyncio
import json
import os
import subprocess
import sys
from pathlib import Path
from typing import Dict, Any, Optional
from datetime import datetime
from shared.utils.logger import get_logger
logger = get_logger(__name__)
class GeometryVerificationService:
"""几何验证服务"""
def __init__(self, timeout: int = 60):
# 验证脚本在项目根目录的 scripts 文件夹下
# __file__ = /path/to/project/src/services/verification_service.py
# parent = /path/to/project/src/services
# parent.parent = /path/to/project/src
# parent.parent.parent = /path/to/project (正确)
self.verification_script = Path(__file__).parent.parent.parent / "scripts" / "verify_stp.py"
self.timeout = timeout # 超时时间(秒),默认60秒
async def verify_stp_file(self, stp_path: str) -> Dict[str, Any]:
"""
验证 STP 文件几何数据
Args:
stp_path: STP 文件路径
Returns:
验证结果字典
"""
try:
logger.info(f"开始验证 STP 文件: {stp_path}")
# 运行验证脚本
result = await self._run_verification_script(stp_path)
if result:
logger.info(f"验证完成: {result.get('status', 'unknown')}")
else:
logger.warning("验证脚本未返回结果")
return result
except Exception as e:
logger.error(f"验证失败: {e}")
return {
"status": "error",
"error": str(e),
"timestamp": datetime.now().isoformat()
}
async def _run_verification_script(self, stp_path: str) -> Optional[Dict[str, Any]]:
"""运行验证脚本"""
import tempfile
import os
import shutil
# 确保 stp_path 是字符串
stp_path_str = str(stp_path)
# 创建临时输出文件
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
output_path = f.name
try:
# 构建命令 - 使用 FreeCAD 命令行模式
cmd = None
# 使用 shutil.which 查找 FreeCAD 命令(更快)
for cmd_name in ['freecad', 'freecadcmd', 'freecad-daily']:
cmd_path = shutil.which(cmd_name)
if cmd_path:
cmd = [cmd_path, str(self.verification_script), str(Path(stp_path_str).absolute())]
logger.info(f"找到 FreeCAD 命令: {cmd_path}")
break
# 检查 Flatpak 版本
if not cmd and shutil.which('flatpak'):
try:
result = subprocess.run(['flatpak', 'list', '--app'], capture_output=True, text=True)
if 'org.freecad.FreeCAD' in result.stdout:
cmd = ['flatpak', 'run', 'org.freecad.FreeCAD', str(self.verification_script), str(Path(stp_path_str).absolute())]
logger.info("找到 FreeCAD Flatpak 版本")
except Exception:
pass
# 如果 which 找不到,尝试直接检查常见路径
if not cmd:
for cmd_path in ['/usr/local/bin/freecad', '/usr/bin/freecad', '/usr/bin/freecad-daily', '/usr/local/bin/freecadcmd', '/usr/bin/freecadcmd', '/snap/bin/freecad.cmd']:
if Path(cmd_path).exists():
cmd = [cmd_path, str(self.verification_script), str(Path(stp_path_str).absolute())]
logger.info(f"找到 FreeCAD 命令路径: {cmd_path}")
break
# 尝试使用 xvfb-run 运行 AppImage
if not cmd and shutil.which('xvfb-run'):
for appimage_path in ['/usr/local/bin/freecad', '/opt/freecad.AppImage']:
if Path(appimage_path).exists():
cmd = ['xvfb-run', appimage_path, str(self.verification_script), str(Path(stp_path_str).absolute())]
logger.info(f"使用 xvfb-run 运行: {appimage_path}")
break
if not cmd:
logger.warning("FreeCAD 命令行工具不可用,跳过验证")
return {
"status": "skipped",
"reason": "FreeCAD not available",
"timestamp": datetime.now().isoformat()
}
logger.info(f"执行验证命令: {' '.join(cmd)}")
# 获取 STP 文件的绝对路径和目录
stp_abs_path = Path(stp_path_str).absolute()
stp_dir = stp_abs_path.parent
# 设置环境变量禁用图形界面
env = os.environ.copy()
env['QT_QPA_PLATFORM'] = 'offscreen'
env['DISPLAY'] = ''
env['FREECAD_USER_HOME'] = '/tmp/freecad_home'
logger.info(f"工作目录: {stp_dir}")
logger.info(f"STP 文件: {stp_abs_path}")
logger.info(f"验证脚本: {self.verification_script}")
# 异步运行子进程
process = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
cwd=str(stp_dir),
env=env
)
# 使用配置的超时时间
try:
stdout, stderr = await asyncio.wait_for(
process.communicate(),
timeout=self.timeout
)
except asyncio.TimeoutError:
logger.error(f"验证脚本执行超时({self.timeout}秒)")
process.kill()
await process.wait()
return {
"status": "error",
"error": f"验证脚本执行超时({self.timeout}秒)",
"timestamp": datetime.now().isoformat()
}
stdout_text = stdout.decode('utf-8') if stdout else ''
stderr_text = stderr.decode('utf-8') if stderr else ''
logger.info(f"验证脚本 stdout (前1000字符): {stdout_text[:1000]}")
if stderr_text:
logger.warning(f"验证脚本 stderr: {stderr_text[:500]}")
# 检查是否有 FreeCAD 错误
has_error = (
'Cannot read STEP file' in stderr_text or
'Cannot read STEP file' in stdout_text or
'Exception while processing file' in stderr_text or
'Exception while processing file' in stdout_text or
'所有导入方法都失败' in stdout_text or
process.returncode != 0
)
if has_error:
logger.error("FreeCAD 验证失败,无法进行交叉验证")
return {
"status": "error",
"reason": "FreeCAD 无法读取 STP 文件,无法进行交叉验证",
"timestamp": datetime.now().isoformat()
}
if process.returncode == 0:
# 尝试读取生成的报告文件(在 STP 文件目录下)
report_path = stp_dir / (stp_abs_path.stem + "_verification_report.json")
if report_path.exists():
with open(report_path, 'r', encoding='utf-8') as f:
result = json.load(f)
# 删除临时报告文件
report_path.unlink()
return result
else:
# 解析 stdout 获取结果
logger.warning(f"验证报告文件不存在: {report_path}")
return self._parse_verification_output(stdout_text)
else:
logger.error(f"验证脚本执行失败 (returncode={process.returncode}): {stderr_text}")
return {
"status": "error",
"error": stderr_text,
"timestamp": datetime.now().isoformat()
}
except FileNotFoundError:
logger.warning("FreeCAD 命令行工具不可用,跳过验证")
return {
"status": "skipped",
"reason": "FreeCAD not available",
"timestamp": datetime.now().isoformat()
}
except Exception as e:
logger.error(f"运行验证脚本失败: {e}")
return {
"status": "error",
"error": str(e),
"timestamp": datetime.now().isoformat()
}
finally:
# 清理临时文件
if os.path.exists(output_path):
os.unlink(output_path)
def _parse_verification_output(self, output: str) -> Dict[str, Any]:
"""解析验证脚本输出"""
result = {
"status": "unknown",
"timestamp": datetime.now().isoformat(),
"comparison": {}
}
lines = output.split('\n')
current_section = None
for line in lines:
line = line.strip()
# 检测验证结果
if '验证结果:' in line:
if '✅ 通过' in line or '通过' in line:
result['status'] = 'passed'
elif '❌ 失败' in line or '失败' in line:
result['status'] = 'failed'
# 检测当前段落
if '体积对比:' in line:
current_section = 'volume'
elif '表面积对比:' in line:
current_section = 'surface_area'
# 解析差异百分比
if '差异:' in line and '%' in line:
try:
# 格式: "差异: 123.4567 mm³ (0.1234%)"
parts = line.split('(')
if len(parts) >= 2:
percent_str = parts[-1].split('%')[0].strip()
percent = float(percent_str)
if current_section == 'volume':
result['comparison']['volume'] = {'difference_percent': percent}
elif current_section == 'surface_area':
result['comparison']['surface_area'] = {'difference_percent': percent}
except Exception as e:
logger.debug(f"解析差异百分比失败: {e}")
# 如果没有找到验证结果,但有 comparison 数据,则根据差异判断
if result['status'] == 'unknown' and result['comparison']:
vol_diff = result['comparison'].get('volume', {}).get('difference_percent', 100)
area_diff = result['comparison'].get('surface_area', {}).get('difference_percent', 100)
if vol_diff < 1 and area_diff < 2:
result['status'] = 'passed'
else:
result['status'] = 'failed'
return result
def _verify_with_pythonocc_only(self, stp_path: str) -> Dict[str, Any]:
"""
仅使用 PythonOCC 验证(当 FreeCAD 不可用时)
Args:
stp_path: STP 文件路径
Returns:
验证结果
"""
try:
from OCC.Core.STEPControl import STEPControl_Reader
from OCC.Core.IFSelect import IFSelect_RetDone
from OCC.Core.GProp import GProp_GProps
from OCC.Core.BRepGProp import brepgprop_VolumeProperties, brepgprop_SurfaceProperties
from OCC.Core.Bnd import Bnd_Box
from OCC.Core.BRepBndLib import brepbndlib
from OCC.Core.TopExp import TopExp_Explorer
from OCC.Core.TopAbs import TopAbs_FACE, TopAbs_EDGE, TopAbs_VERTEX, TopAbs_SOLID
logger.info(f"使用 PythonOCC 进行验证: {stp_path}")
# 读取 STP 文件
reader = STEPControl_Reader()
status = reader.ReadFile(stp_path)
if status != IFSelect_RetDone:
return {
"status": "error",
"error": "无法读取 STP 文件",
"timestamp": datetime.now().isoformat()
}
reader.TransferRoots()
shape = reader.OneShape()
# 计算体积
vol_props = GProp_GProps()
brepgprop_VolumeProperties(shape, vol_props)
volume_mm3 = vol_props.Mass()
com = vol_props.CentreOfMass()
# 计算表面积
surf_props = GProp_GProps()
brepgprop_SurfaceProperties(shape, surf_props)
surface_area_mm2 = surf_props.Mass()
# 计算边界框
bbox = Bnd_Box()
brepbndlib.Add(shape, bbox)
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
# 拓扑统计
def count_topology(shape, top_type):
explorer = TopExp_Explorer(shape, top_type)
count = 0
while explorer.More():
count += 1
explorer.Next()
return count
return {
"status": "passed",
"method": "pythonocc_only",
"reason": "FreeCAD 验证失败,仅使用 PythonOCC 验证",
"timestamp": datetime.now().isoformat(),
"pythonocc": {
"volume_mm3": float(volume_mm3),
"volume_cm3": float(volume_mm3 / 1000),
"surface_area_mm2": float(surface_area_mm2),
"surface_area_cm2": float(surface_area_mm2 / 100),
"bounding_box": {
"x_min": float(xmin),
"x_max": float(xmax),
"y_min": float(ymin),
"y_max": float(ymax),
"z_min": float(zmin),
"z_max": float(zmax),
"x_length": float(xmax - xmin),
"y_length": float(ymax - ymin),
"z_length": float(zmax - zmin),
"center": [float((xmin + xmax) / 2), float((ymin + ymax) / 2), float((zmin + zmax) / 2)]
},
"center_of_mass": [float(com.X()), float(com.Y()), float(com.Z())],
"topology": {
"faces": count_topology(shape, TopAbs_FACE),
"edges": count_topology(shape, TopAbs_EDGE),
"vertices": count_topology(shape, TopAbs_VERTEX),
"solids": count_topology(shape, TopAbs_SOLID)
}
}
}
except Exception as e:
logger.error(f"PythonOCC 验证失败: {e}")
return {
"status": "error",
"error": str(e),
"timestamp": datetime.now().isoformat()
}
def verify_with_pythonocc(self, shape) -> Dict[str, Any]:
"""
使用 PythonOCC 验证几何数据(同步方法,用于内部验证)
Args:
shape: OCC 形状对象
Returns:
验证结果
"""
try:
from OCC.Core.GProp import GProp_GProps
from OCC.Core.BRepGProp import brepgprop_VolumeProperties, brepgprop_SurfaceProperties
from OCC.Core.Bnd import Bnd_Box
from OCC.Core.BRepBndLib import brepbndlib
from OCC.Core.TopExp import TopExp_Explorer
from OCC.Core.TopAbs import TopAbs_FACE, TopAbs_EDGE, TopAbs_VERTEX, TopAbs_SOLID
# 计算体积
vol_props = GProp_GProps()
brepgprop_VolumeProperties(shape, vol_props)
volume_mm3 = vol_props.Mass()
com = vol_props.CentreOfMass()
# 计算表面积
surf_props = GProp_GProps()
brepgprop_SurfaceProperties(shape, surf_props)
surface_area_mm2 = surf_props.Mass()
# 计算边界框
bbox = Bnd_Box()
brepbndlib.Add(shape, bbox)
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
# 拓扑统计
def count_topology(shape, top_type):
explorer = TopExp_Explorer(shape, top_type)
count = 0
while explorer.More():
count += 1
explorer.Next()
return count
return {
"volume_mm3": float(volume_mm3),
"volume_cm3": float(volume_mm3 / 1000),
"surface_area_mm2": float(surface_area_mm2),
"surface_area_cm2": float(surface_area_mm2 / 100),
"bounding_box": {
"x_min": float(xmin),
"x_max": float(xmax),
"y_min": float(ymin),
"y_max": float(ymax),
"z_min": float(zmin),
"z_max": float(zmax),
"x_length": float(xmax - xmin),
"y_length": float(ymax - ymin),
"z_length": float(zmax - zmin),
"center": [float((xmin + xmax) / 2), float((ymin + ymax) / 2), float((zmin + zmax) / 2)]
},
"center_of_mass": [float(com.X()), float(com.Y()), float(com.Z())],
"topology": {
"faces": count_topology(shape, TopAbs_FACE),
"edges": count_topology(shape, TopAbs_EDGE),
"vertices": count_topology(shape, TopAbs_VERTEX),
"solids": count_topology(shape, TopAbs_SOLID)
}
}
except Exception as e:
logger.error(f"PythonOCC 验证失败: {e}")
return {"error": str(e)}
# 单例实例
verification_service = GeometryVerificationService()
+5
View File
@@ -0,0 +1,5 @@
# storage/__init__.py
from .rustfs_storage import RustFSManager, rustfs_manager
__all__ = ['RustFSManager', 'rustfs_manager']
+107
View File
@@ -0,0 +1,107 @@
# storage/init_storage.py
"""初始化 RustFS 对象存储"""
import asyncio
import sys
from pathlib import Path
# 添加项目根目录和 src 目录到 Python 路径
project_root = Path(__file__).parent.parent.parent
src_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))
sys.path.insert(0, str(src_root))
from moldinsight.storage.rustfs_storage import rustfs_manager
from shared.config.settings import settings
from shared.utils.logger import get_logger
logger = get_logger(__name__)
async def init_rustfs_storage():
"""初始化 RustFS 对象存储"""
try:
# 连接到 RustFS (S3v4 API)
await rustfs_manager.connect(
endpoint=settings.RUSTFS_ENDPOINT,
access_key=settings.RUSTFS_ACCESS_KEY,
secret_key=settings.RUSTFS_SECRET_KEY,
timeout=settings.RUSTFS_TIMEOUT
)
logger.info("RustFS 对象存储初始化完成")
return True
except Exception as e:
logger.error(f"RustFS 对象存储初始化失败: {e}")
return False
async def test_storage():
"""测试 RustFS 对象存储功能"""
try:
import json
# 测试上传 JSON
test_data = {"test": True, "timestamp": "2024-01-01", "storage": "rustfs"}
result = await rustfs_manager.upload_json_data(
file_type='stp_files',
json_data=test_data,
file_hash='test-hash'
)
logger.info(f"RustFS 测试上传成功: {result['object_key']}")
# 测试下载
downloaded_bytes = await rustfs_manager.download_file(
file_type='stp_files',
object_key=result['object_key']
)
downloaded_data = json.loads(downloaded_bytes.decode('utf-8'))
logger.info(f"RustFS 测试下载成功: {downloaded_data}")
# 测试预签名 URL
url = await rustfs_manager.generate_presigned_url(
file_type='stp_files',
object_key=result['object_key'],
expires=3600
)
logger.info(f"RustFS 预签名URL: {url}")
# 清理测试文件
await rustfs_manager.delete_file(
file_type='stp_files',
object_key=result['object_key']
)
logger.info("RustFS 测试文件已清理")
return True
except Exception as e:
logger.error(f"RustFS 存储测试失败: {e}")
return False
if __name__ == "__main__":
async def main():
try:
print("=== 初始化 RustFS 对象存储 ===")
# 初始化存储
init_result = await init_rustfs_storage()
if init_result:
print("[OK] RustFS 连接成功")
print("\n=== 测试 RustFS 功能 ===")
# 运行测试
test_result = await test_storage()
if test_result:
print("[OK] RustFS 测试全部通过")
else:
print("[FAIL] RustFS 测试失败")
finally:
# 关闭连接
await rustfs_manager.close()
print("\n=== 连接已关闭 ===")
# 运行主函数
asyncio.run(main())
+361
View File
@@ -0,0 +1,361 @@
# storage/object_storage.py
"""MinIO/S3 对象存储服务"""
from minio import Minio
from minio.error import S3Error
from pathlib import Path
from typing import Optional, BinaryIO
from io import BytesIO
from shared.utils.logger import get_logger
import hashlib
import uuid
logger = get_logger(__name__)
class ObjectStorageManager:
"""对象存储管理器 - MinIO/S3兼容"""
def __init__(self):
self.client: Optional[Minio] = None
self.is_connected = False
# 桶名称
self.buckets = {
'stp_files': 'moldinsight-stp-files', # STP/STEP文件
'geometry_data': 'moldinsight-geometry', # 几何数据JSON
'mold_cavities': 'moldinsight-mold-cavities', # 模具型腔数据
'html_files': 'moldinsight-html', # HTML报告文件
'user_files': 'moldinsight-user-files' # 用户上传的其他文件
}
async def connect(self, endpoint: str, access_key: str, secret_key: str,
secure: bool = False):
"""连接到MinIO/S3服务"""
try:
self.client = Minio(
endpoint,
access_key=access_key,
secret_key=secret_key,
secure=secure
)
# 测试连接
self.client.list_buckets()
self.is_connected = True
logger.info(f"对象存储连接成功: {endpoint}")
# 确保所有桶都存在
await self._ensure_buckets()
except S3Error as e:
logger.error(f"对象存储连接失败: {e}")
self.is_connected = False
raise
async def _ensure_buckets(self):
"""确保所有必要的桶都存在"""
for bucket_name in self.buckets.values():
try:
if not self.client.bucket_exists(bucket_name):
self.client.make_bucket(bucket_name)
logger.info(f"创建存储桶: {bucket_name}")
else:
logger.debug(f"存储桶已存在: {bucket_name}")
except S3Error as e:
logger.error(f"创建存储桶失败 {bucket_name}: {e}")
def _generate_object_key(self, original_filename: str, prefix: str = '') -> str:
"""生成对象存储的唯一键名"""
# 提取文件扩展名
ext = Path(original_filename).suffix
# 生成唯一ID
unique_id = str(uuid.uuid4())
# 生成键名: prefix/unique_id + original_ext
if prefix:
return f"{prefix}/{unique_id}{ext}"
return f"{unique_id}{ext}"
async def upload_stp_file(self, file_path: Path,
original_filename: str) -> dict:
"""上传STP文件到对象存储"""
if not self.is_connected:
raise RuntimeError("对象存储未连接")
bucket_name = self.buckets['stp_files']
# 计算文件哈希
file_hash = self._calculate_file_hash(file_path)
# 检查是否已存在
existing_key = await self._find_file_by_hash(bucket_name, file_hash)
if existing_key:
logger.info(f"文件已存在,跳过上传: {existing_key}")
return {
'object_key': existing_key,
'file_hash': file_hash,
'already_exists': True
}
# 生成唯一键名
object_key = self._generate_object_key(
original_filename,
prefix='stp'
)
# 上传文件
try:
result = self.client.fput_object(
bucket_name,
object_key,
str(file_path),
content_type='application/octet-stream'
)
logger.info(f"STP文件上传成功: {object_key}")
return {
'object_key': object_key,
'file_hash': file_hash,
'file_size': result.size,
'etag': result.etag,
'already_exists': False
}
except S3Error as e:
logger.error(f"STP文件上传失败: {e}")
raise
async def upload_geometry_data(self, geometry_json: dict,
file_hash: str) -> dict:
"""上传几何数据JSON到对象存储"""
if not self.is_connected:
raise RuntimeError("对象存储未连接")
bucket_name = self.buckets['geometry_data']
# 使用文件哈希作为键名的一部分
object_key = f"geometry/{file_hash}.json"
# 转换为字节
import json
json_bytes = json.dumps(geometry_json, ensure_ascii=False).encode('utf-8')
# 上传
try:
result = self.client.put_object(
bucket_name,
object_key,
BytesIO(json_bytes),
length=len(json_bytes),
content_type='application/json'
)
logger.info(f"几何数据上传成功: {object_key}")
return {
'object_key': object_key,
'file_size': result.size,
'etag': result.etag
}
except S3Error as e:
logger.error(f"几何数据上传失败: {e}")
raise
async def upload_mold_cavity_data(self, cavity_json: dict,
file_hash: str) -> dict:
"""上传模具型腔数据到对象存储"""
if not self.is_connected:
raise RuntimeError("对象存储未连接")
bucket_name = self.buckets['mold_cavities']
object_key = f"mold-cavity/{file_hash}.json"
import json
json_bytes = json.dumps(cavity_json, ensure_ascii=False).encode('utf-8')
try:
result = self.client.put_object(
bucket_name,
object_key,
BytesIO(json_bytes),
length=len(json_bytes),
content_type='application/json'
)
logger.info(f"模具型腔数据上传成功: {object_key}")
return {
'object_key': object_key,
'file_size': result.size,
'etag': result.etag
}
except S3Error as e:
logger.error(f"模具型腔数据上传失败: {e}")
raise
async def upload_html_file(self, html_content: str,
original_filename: str,
file_hash: str) -> dict:
"""上传HTML文件到对象存储"""
if not self.is_connected:
raise RuntimeError("对象存储未连接")
bucket_name = self.buckets['html_files']
object_key = f"html/{file_hash}.html"
html_bytes = html_content.encode('utf-8')
try:
result = self.client.put_object(
bucket_name,
object_key,
BytesIO(html_bytes),
length=len(html_bytes),
content_type='text/html; charset=utf-8'
)
logger.info(f"HTML文件上传成功: {object_key}")
return {
'object_key': object_key,
'file_size': result.size,
'etag': result.etag
}
except S3Error as e:
logger.error(f"HTML文件上传失败: {e}")
raise
async def download_file(self, bucket_type: str,
object_key: str) -> bytes:
"""从对象存储下载文件"""
if not self.is_connected:
raise RuntimeError("对象存储未连接")
bucket_name = self.buckets.get(bucket_type)
if not bucket_name:
raise ValueError(f"未知的桶类型: {bucket_type}")
try:
response = self.client.get_object(bucket_name, object_key)
data = response.read()
response.close()
response.release_conn()
logger.debug(f"文件下载成功: {object_key}")
return data
except S3Error as e:
logger.error(f"文件下载失败 {object_key}: {e}")
raise
async def get_presigned_url(self, bucket_type: str,
object_key: str,
expires: int = 3600) -> str:
"""生成预签名URL(临时访问链接)"""
if not self.is_connected:
raise RuntimeError("对象存储未连接")
bucket_name = self.buckets.get(bucket_type)
if not bucket_name:
raise ValueError(f"未知的桶类型: {bucket_type}")
try:
url = self.client.presigned_get_object(
bucket_name,
object_key,
expires=expires
)
return url
except S3Error as e:
logger.error(f"生成预签名URL失败: {e}")
raise
async def delete_file(self, bucket_type: str, object_key: str):
"""删除对象存储中的文件"""
if not self.is_connected:
raise RuntimeError("对象存储未连接")
bucket_name = self.buckets.get(bucket_type)
if not bucket_name:
raise ValueError(f"未知的桶类型: {bucket_type}")
try:
self.client.remove_object(bucket_name, object_key)
logger.info(f"文件删除成功: {object_key}")
except S3Error as e:
logger.error(f"文件删除失败 {object_key}: {e}")
raise
def _calculate_file_hash(self, file_path: Path) -> str:
"""计算文件的SHA256哈希"""
sha256_hash = hashlib.sha256()
with open(file_path, 'rb') as f:
for byte_block in iter(lambda: f.read(4096), b""):
sha256_hash.update(byte_block)
return sha256_hash.hexdigest()
async def _find_file_by_hash(self, bucket_name: str,
file_hash: str) -> Optional[str]:
"""根据哈希查找已存在的文件"""
try:
objects = self.client.list_objects(bucket_name, recursive=True)
for obj in objects:
# 从对象键中提取哈希(如果有)
if file_hash in obj.object_name:
return obj.object_name
return None
except S3Error as e:
logger.warning(f"查找文件哈希失败: {e}")
return None
async def get_file_info(self, bucket_type: str,
object_key: str) -> dict:
"""获取文件信息"""
if not self.is_connected:
raise RuntimeError("对象存储未连接")
bucket_name = self.buckets.get(bucket_type)
if not bucket_name:
raise ValueError(f"未知的桶类型: {bucket_type}")
try:
stat = self.client.stat_object(bucket_name, object_key)
return {
'size': stat.size,
'etag': stat.etag,
'content_type': stat.content_type,
'last_modified': stat.last_modified
}
except S3Error as e:
logger.error(f"获取文件信息失败: {e}")
raise
async def list_files(self, bucket_type: str,
prefix: str = '') -> list:
"""列出存储桶中的文件"""
if not self.is_connected:
raise RuntimeError("对象存储未连接")
bucket_name = self.buckets.get(bucket_type)
if not bucket_name:
raise ValueError(f"未知的桶类型: {bucket_type}")
try:
objects = self.client.list_objects(bucket_name, prefix=prefix)
return [
{
'object_key': obj.object_name,
'size': obj.size,
'etag': obj.etag,
'last_modified': obj.last_modified
}
for obj in objects
]
except S3Error as e:
logger.error(f"列出文件失败: {e}")
raise
# 全局对象存储管理器实例
storage_manager = ObjectStorageManager()
+398
View File
@@ -0,0 +1,398 @@
# storage/rustfs_storage.py
"""RustFS 对象存储服务 (S3v4 API 兼容)"""
from minio import Minio
from minio.error import S3Error
from pathlib import Path
from typing import Optional, Dict, Any
from io import BytesIO
from shared.utils.logger import get_logger
from datetime import timedelta
import hashlib
import uuid
logger = get_logger(__name__)
class RustFSManager:
"""RustFS 对象存储管理器 (使用 MinIO S3 客户端)"""
def __init__(self, project_name: str = "moldinsight"):
self.client: Optional[Minio] = None
self.is_connected = False
self.project_name = project_name
# 使用单个项目桶,按类型组织文件
self.bucket_name = f"{project_name}"
# 文件类型前缀(子目录结构)
self.file_types = {
'stp_files': 'stp-files',
'geometry_data': 'geometry',
'mesh_data': 'mesh',
'mold_cavities': 'mold-cavities',
'html_files': 'html',
'user_files': 'user-files'
}
async def connect(self, endpoint: str, access_key: str, secret_key: str, timeout: int = 30):
"""连接到 RustFS 服务"""
try:
# 提取端口号和主机
from urllib.parse import urlparse
parsed = urlparse(endpoint)
host = parsed.netloc or parsed.path
# 创建 MinIO 客户端(S3v4 兼容)
self.client = Minio(
host,
access_key=access_key,
secret_key=secret_key,
secure=False, # HTTP 而不是 HTTPS
region='us-east-1'
)
# 测试连接
import asyncio
await asyncio.to_thread(self.client.list_buckets)
self.is_connected = True
logger.info(f"RustFS 连接成功: {endpoint}")
# 确保所有桶都存在
await self._ensure_buckets()
except S3Error as e:
logger.error(f"RustFS 连接失败: {e}")
self.is_connected = False
raise
except Exception as e:
logger.error(f"RustFS 初始化失败: {e}")
self.is_connected = False
raise
async def close(self):
"""关闭连接"""
# MinIO 客户端不需要显式关闭
self.is_connected = False
logger.info("RustFS 连接已关闭")
async def _ensure_buckets(self):
"""确保项目存储桶存在"""
import asyncio
def _check_and_create():
if not self.client.bucket_exists(self.bucket_name):
self.client.make_bucket(self.bucket_name)
return True
return False
try:
created = await asyncio.to_thread(_check_and_create)
if created:
logger.info(f"创建项目存储桶: {self.bucket_name}")
else:
logger.debug(f"项目存储桶已存在: {self.bucket_name}")
except S3Error as e:
logger.error(f"创建存储桶失败 {self.bucket_name}: {e}")
def _generate_object_key(self, original_filename: str, file_type: str = '') -> str:
"""生成对象存储的唯一键名"""
ext = Path(original_filename).suffix
unique_id = str(uuid.uuid4())
# 格式: {文件类型}/{唯一ID}.扩展名 (去掉项目名前缀)
if file_type and file_type in self.file_types:
type_prefix = self.file_types[file_type]
return f"{type_prefix}/{unique_id}{ext}"
# 默认格式
return f"misc/{unique_id}{ext}"
def _calculate_file_hash(self, file_path: Path) -> str:
"""计算文件的SHA256哈希"""
sha256_hash = hashlib.sha256()
with open(file_path, 'rb') as f:
for byte_block in iter(lambda: f.read(4096), b""):
sha256_hash.update(byte_block)
return sha256_hash.hexdigest()
async def upload_file(self, file_type: str, file_path: Path,
original_filename: str,
metadata: Optional[Dict] = None) -> Dict[str, Any]:
"""上传文件到 RustFS"""
import asyncio
if not self.is_connected:
raise RuntimeError("RustFS 未连接")
if file_type not in self.file_types:
raise ValueError(f"未知的文件类型: {file_type}")
# 计算文件哈希
file_hash = self._calculate_file_hash(file_path)
# 生成唯一键名
object_key = self._generate_object_key(original_filename, file_type)
# 上传文件
def _upload():
return self.client.fput_object(
self.bucket_name,
object_key,
str(file_path),
content_type='application/octet-stream',
metadata=metadata or {}
)
try:
result = await asyncio.to_thread(_upload)
logger.info(f"文件上传成功 RustFS: {self.bucket_name}/{object_key}")
# 获取文件大小
file_size = file_path.stat().st_size
return {
'object_key': object_key,
'bucket': self.bucket_name,
'file_hash': file_hash,
'file_size': file_size,
'etag': result.etag if hasattr(result, 'etag') else None
}
except S3Error as e:
logger.error(f"RustFS 上传失败: {e}")
raise
async def upload_json_data(self, file_type: str,
json_data: Dict[str, Any],
file_hash: str) -> Dict[str, Any]:
"""上传JSON数据到 RustFS"""
import asyncio
if not self.is_connected:
raise RuntimeError("RustFS 未连接")
if file_type not in self.file_types:
raise ValueError(f"未知的文件类型: {file_type}")
# 格式: {文件类型}/{文件哈希}.json (去掉项目名前缀)
type_prefix = self.file_types[file_type]
object_key = f"{type_prefix}/{file_hash}.json"
# 转换为字节
import json
json_bytes = json.dumps(json_data, ensure_ascii=False).encode('utf-8')
def _upload():
return self.client.put_object(
self.bucket_name,
object_key,
BytesIO(json_bytes),
length=len(json_bytes),
content_type='application/json'
)
try:
result = await asyncio.to_thread(_upload)
logger.info(f"JSON数据上传成功 RustFS: {self.bucket_name}/{object_key}")
return {
'object_key': object_key,
'bucket': self.bucket_name,
'file_size': len(json_bytes),
'etag': result.etag if hasattr(result, 'etag') else None
}
except S3Error as e:
logger.error(f"RustFS JSON上传失败: {e}")
raise
async def download_file(self, file_type: str, object_key: str) -> bytes:
"""从 RustFS 下载文件"""
import asyncio
if not self.is_connected:
raise RuntimeError("RustFS 未连接")
if file_type not in self.file_types:
raise ValueError(f"未知的文件类型: {file_type}")
def _download():
response = self.client.get_object(self.bucket_name, object_key)
data = response.read()
response.close()
response.release_conn()
return data
try:
data = await asyncio.to_thread(_download)
logger.debug(f"文件下载成功: {self.bucket_name}/{object_key}")
return data
except S3Error as e:
logger.error(f"RustFS 下载失败: {e}")
raise
async def get_file_info(self, file_type: str, object_key: str) -> Dict[str, Any]:
"""获取文件信息"""
import asyncio
if not self.is_connected:
raise RuntimeError("RustFS 未连接")
if file_type not in self.file_types:
raise ValueError(f"未知的文件类型: {file_type}")
def _get_stat():
return self.client.stat_object(self.bucket_name, object_key)
try:
stat = await asyncio.to_thread(_get_stat)
return {
'size': stat.size,
'etag': stat.etag,
'content_type': stat.content_type,
'last_modified': stat.last_modified
}
except S3Error as e:
logger.error(f"RustFS 获取文件信息失败: {e}")
raise
async def delete_file(self, file_type: str, object_key: str):
"""删除 RustFS 中的文件"""
import asyncio
if not self.is_connected:
raise RuntimeError("RustFS 未连接")
if file_type not in self.file_types:
raise ValueError(f"未知的文件类型: {file_type}")
def _delete():
self.client.remove_object(self.bucket_name, object_key)
try:
await asyncio.to_thread(_delete)
logger.info(f"文件删除成功: {self.bucket_name}/{object_key}")
except S3Error as e:
logger.error(f"RustFS 删除失败: {e}")
raise
async def list_files(self, file_type: str, prefix: str = '') -> list:
"""列出存储桶中的文件"""
import asyncio
if not self.is_connected:
raise RuntimeError("RustFS 未连接")
if file_type not in self.file_types:
raise ValueError(f"未知的文件类型: {file_type}")
# 构建完整前缀:{文件类型}/... (去掉项目名前缀)
type_prefix = self.file_types[file_type]
full_prefix = f"{type_prefix}/"
if prefix:
full_prefix += prefix
def _list():
return list(self.client.list_objects(self.bucket_name, prefix=full_prefix, recursive=True))
try:
objects = await asyncio.to_thread(_list)
return [
{
'object_key': obj.object_name,
'size': obj.size,
'etag': obj.etag,
'last_modified': obj.last_modified
}
for obj in objects
]
except S3Error as e:
logger.error(f"RustFS 列出文件失败: {e}")
raise
async def generate_presigned_url(self, file_type: str,
object_key: str,
expires: int = 3600,
method: str = 'GET') -> str:
"""生成预签名URL(临时访问链接)"""
import asyncio
if not self.is_connected:
raise RuntimeError("RustFS 未连接")
if file_type not in self.file_types:
raise ValueError(f"未知的文件类型: {file_type}")
def _generate_url():
return self.client.presigned_get_object(
self.bucket_name,
object_key,
expires=timedelta(seconds=expires)
)
try:
url = await asyncio.to_thread(_generate_url)
return url
except S3Error as e:
logger.error(f"RustFS 生成预签名URL失败: {e}")
raise
async def file_exists(self, file_type: str, object_key: str) -> bool:
"""检查文件是否存在"""
try:
await self.get_file_info(file_type, object_key)
return True
except Exception:
return False
async def get_storage_stats(self) -> Dict[str, Any]:
"""获取存储统计信息"""
import asyncio
def _get_stats():
buckets = self.client.list_buckets()
total_objects = 0
total_size = 0
namespace_stats = {}
for bucket in buckets:
objects = list(self.client.list_objects(bucket.name, recursive=True))
bucket_count = 0
bucket_size = 0
for obj in objects:
bucket_count += 1
bucket_size += obj.size
namespace_stats[bucket.name] = {
'object_count': bucket_count,
'total_size': bucket_size
}
total_objects += bucket_count
total_size += bucket_size
return {
'total_objects': total_objects,
'total_size': total_size,
'namespace_stats': namespace_stats
}
try:
return await asyncio.to_thread(_get_stats)
except S3Error as e:
logger.error(f"RustFS 获取统计信息失败: {e}")
raise
# 全局 RustFS 管理器实例
rustfs_manager = RustFSManager()