From d1a3f227851e63f6e90329bb019776fb7418364f Mon Sep 17 00:00:00 2001
From: SZCJW <792430652@qq.com>
Date: Sat, 2 May 2026 00:39:04 +0800
Subject: [PATCH] x
---
.../backfill_mold_cavity_confidence.py | 81 +++++
src/api/v1/__init__.py | 1 +
src/api/v1/cam_router.py | 103 +++++++
src/core/base_mold_generator.py | 38 ++-
src/core/multi_scheme_planner.py | 44 +++
src/core/parting_scheme_scorer.py | 121 +++++++-
src/database/init_db.py | 12 +
src/models/database.py | 6 +
src/services/calculation_service.py | 3 +
src/services/cam_bundle_service.py | 187 ++++++++++++
src/services/processing_service.py | 2 +
src/services/storage_integration_rustfs.py | 92 +++++-
src/services/task_query_service.py | 5 +
src/utils/html_generator.py | 288 ++++++++++++------
static/style.css | 23 +-
static/vue-app.js | 288 +++++++++++++++++-
16 files changed, 1181 insertions(+), 113 deletions(-)
create mode 100644 scripts/migrations/backfill_mold_cavity_confidence.py
create mode 100644 src/api/v1/cam_router.py
create mode 100644 src/services/cam_bundle_service.py
diff --git a/scripts/migrations/backfill_mold_cavity_confidence.py b/scripts/migrations/backfill_mold_cavity_confidence.py
new file mode 100644
index 0000000..a537791
--- /dev/null
+++ b/scripts/migrations/backfill_mold_cavity_confidence.py
@@ -0,0 +1,81 @@
+#!/usr/bin/env python3
+"""
+回填 mold_cavity_data 的多方案可信化字段。
+
+回填字段:
+- best_scheme_id
+- confidence_score
+- is_fallback
+- fallback_reason
+"""
+
+import asyncio
+import json
+import sys
+from pathlib import Path
+
+project_root = Path(__file__).resolve().parents[2]
+src_root = project_root / "src"
+sys.path.insert(0, str(project_root))
+sys.path.insert(0, str(src_root))
+
+from sqlalchemy import select
+
+from database.database import db_manager
+from models.database import MoldCavityData
+from storage.rustfs_storage import rustfs_manager
+from config.settings import settings
+from services.storage_integration_rustfs import StorageIntegrationService
+from utils.logger import get_logger
+
+logger = get_logger(__name__)
+
+
+async def backfill():
+ await db_manager.connect()
+ await rustfs_manager.connect(
+ endpoint=settings.RUSTFS_ENDPOINT,
+ access_key=settings.RUSTFS_ACCESS_KEY,
+ secret_key=settings.RUSTFS_SECRET_KEY,
+ timeout=settings.RUSTFS_TIMEOUT,
+ )
+
+ summary = {"total": 0, "updated": 0, "failed": 0}
+ try:
+ async with db_manager.session() as session:
+ result = await session.execute(select(MoldCavityData))
+ rows = result.scalars().all()
+ summary["total"] = len(rows)
+
+ for row in rows:
+ try:
+ cavity_bytes = await rustfs_manager.download_file(
+ file_type="mold_cavities",
+ object_key=row.detailed_object_key,
+ )
+ cavity_json = json.loads(cavity_bytes.decode("utf-8"))
+ payload = StorageIntegrationService._resolve_best_scheme_payload(cavity_json)
+ best_scheme = payload.get("best_scheme") or {}
+
+ row.best_scheme_id = payload.get("best_scheme_id")
+ row.confidence_score = best_scheme.get("confidence_score")
+ row.is_fallback = best_scheme.get("is_fallback")
+ row.fallback_reason = best_scheme.get("fallback_reason")
+ summary["updated"] += 1
+ except Exception as exc:
+ summary["failed"] += 1
+ logger.warning(f"回填失败 id={row.id}: {exc}")
+
+ await session.commit()
+ finally:
+ await rustfs_manager.close()
+ await db_manager.disconnect()
+
+ print("=== Backfill 完成 ===")
+ print(f"总记录: {summary['total']}")
+ print(f"成功更新: {summary['updated']}")
+ print(f"失败: {summary['failed']}")
+
+
+if __name__ == "__main__":
+ asyncio.run(backfill())
diff --git a/src/api/v1/__init__.py b/src/api/v1/__init__.py
index b2c46d8..b620afb 100644
--- a/src/api/v1/__init__.py
+++ b/src/api/v1/__init__.py
@@ -24,4 +24,5 @@ _safe_include("api.v1.upload_router", "上传")
_safe_include("api.v1.task_router", "任务")
_safe_include("api.v1.history_router", "历史")
_safe_include("api.v1.debug_router", "调试")
+_safe_include("api.v1.cam_router", "CAM")
_safe_include("api.v1.advanced_router", "高级")
diff --git a/src/api/v1/cam_router.py b/src/api/v1/cam_router.py
new file mode 100644
index 0000000..91219e7
--- /dev/null
+++ b/src/api/v1/cam_router.py
@@ -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 database.database import get_db_session
+from models.database import User, ProcessingTask
+from services.auth_service import get_current_active_user
+from services.cam_bundle_service import cam_bundle_service
+from services.task_query_service import TaskQueryService
+from 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}")
diff --git a/src/core/base_mold_generator.py b/src/core/base_mold_generator.py
index 98feff9..f7ee2a1 100644
--- a/src/core/base_mold_generator.py
+++ b/src/core/base_mold_generator.py
@@ -398,14 +398,42 @@ class BaseMoldGenerator:
core = self._subtract_product_from_plate(b_plate, shape, "B板(回退)")
return cavity or a_plate, core or b_plate
- logger.warning("回退方案也失败,使用最简方法")
- cavity_op = BRepAlgoAPI_Cut(mold_block, shape)
- cavity = cavity_op.Shape() if cavity_op.IsDone() else mold_block
- return cavity, shape
+ logger.warning("回退方案也失败,使用最简A/B板切分(避免返回产品本体)")
+ center_z = (zmin + zmax) / 2
+ margin = 20
+ a_plate = BRepPrimAPI_MakeBox(
+ gp_Pnt(xmin - margin, ymin - margin, center_z),
+ gp_Pnt(xmax + margin, ymax + margin, zmax + margin)
+ ).Shape()
+ b_plate = BRepPrimAPI_MakeBox(
+ gp_Pnt(xmin - margin, ymin - margin, zmin - margin),
+ gp_Pnt(xmax + margin, ymax + margin, center_z)
+ ).Shape()
+
+ cavity = self._subtract_product_from_plate(a_plate, shape, "A板(最简)")
+ core = self._subtract_product_from_plate(b_plate, shape, "B板(最简)")
+ return cavity or a_plate, core or b_plate
except Exception as e:
logger.error(f"分模回退方案失败: {e}")
- return shape, shape
+ # 最后兜底也返回模具半板,而不是产品本体,避免预览出现“三份产品”
+ try:
+ bbox = Bnd_Box()
+ brepbndlib.Add(shape, bbox)
+ xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
+ center_z = (zmin + zmax) / 2
+ margin = 20
+ a_plate = BRepPrimAPI_MakeBox(
+ gp_Pnt(xmin - margin, ymin - margin, center_z),
+ gp_Pnt(xmax + margin, ymax + margin, zmax + margin)
+ ).Shape()
+ b_plate = BRepPrimAPI_MakeBox(
+ gp_Pnt(xmin - margin, ymin - margin, zmin - margin),
+ gp_Pnt(xmax + margin, ymax + margin, center_z)
+ ).Shape()
+ return a_plate, b_plate
+ except Exception:
+ return shape, shape
def detect_insert_regions(self, shape: Any, analysis: Dict,
depth_threshold: float = 30.0,
diff --git a/src/core/multi_scheme_planner.py b/src/core/multi_scheme_planner.py
index 906685e..dd3ae63 100644
--- a/src/core/multi_scheme_planner.py
+++ b/src/core/multi_scheme_planner.py
@@ -113,6 +113,7 @@ class MultiSchemeMoldPlanner:
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)
@@ -150,6 +151,9 @@ class MultiSchemeMoldPlanner:
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"],
@@ -161,6 +165,9 @@ class MultiSchemeMoldPlanner:
"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,
@@ -273,3 +280,40 @@ class MultiSchemeMoldPlanner:
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,
+ }
diff --git a/src/core/parting_scheme_scorer.py b/src/core/parting_scheme_scorer.py
index 79a60d3..0f7b1b4 100644
--- a/src/core/parting_scheme_scorer.py
+++ b/src/core/parting_scheme_scorer.py
@@ -1,4 +1,4 @@
-from typing import Dict, Any, List
+from typing import Dict, Any, List, Optional, Tuple
import re
@@ -19,8 +19,19 @@ class PartingSchemeScorer:
)
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["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)
@@ -95,6 +106,98 @@ class PartingSchemeScorer:
"risk": round(risk, 2),
}
+ 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", {})
@@ -106,8 +209,10 @@ class PartingSchemeScorer:
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} 轴开模,分型面位置 {offset_label},倒扣 {undercut_count} 处,"
+ f"{axis} 轴开模,结构 {structure_text},分型面位置 {offset_label},倒扣 {undercut_count} 处,"
f"滑块 {slider_count} 组,斜顶 {lifter_count} 组,"
f"预估锁模力 {clamping_force}"
)
@@ -127,3 +232,15 @@ class PartingSchemeScorer:
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])
diff --git a/src/database/init_db.py b/src/database/init_db.py
index 55e1fc1..3c89860 100644
--- a/src/database/init_db.py
+++ b/src/database/init_db.py
@@ -185,6 +185,18 @@ async def ensure_schema_updates():
CREATE UNIQUE INDEX IF NOT EXISTS uq_product_material_unique
ON product_materials (finished_product_id, material_product_id)
"""))
+ await conn.execute(text("ALTER TABLE mold_cavity_data ADD COLUMN IF NOT EXISTS best_scheme_id VARCHAR(64)"))
+ await conn.execute(text("ALTER TABLE mold_cavity_data ADD COLUMN IF NOT EXISTS confidence_score DOUBLE PRECISION"))
+ await conn.execute(text("ALTER TABLE mold_cavity_data ADD COLUMN IF NOT EXISTS is_fallback BOOLEAN"))
+ await conn.execute(text("ALTER TABLE mold_cavity_data ADD COLUMN IF NOT EXISTS fallback_reason TEXT"))
+ await conn.execute(text("""
+ CREATE INDEX IF NOT EXISTS idx_mold_cavity_best_scheme_id
+ ON mold_cavity_data (best_scheme_id)
+ """))
+ await conn.execute(text("""
+ CREATE INDEX IF NOT EXISTS idx_mold_cavity_is_fallback
+ ON mold_cavity_data (is_fallback)
+ """))
if __name__ == "__main__":
diff --git a/src/models/database.py b/src/models/database.py
index 93d9f23..35728b8 100644
--- a/src/models/database.py
+++ b/src/models/database.py
@@ -346,6 +346,12 @@ class MoldCavityData(Base):
weld_line_risk = Column(String(50), nullable=True) # 熔接痕风险
sink_mark_risk = Column(String(50), nullable=True) # 缩痕风险
warpage_risk = Column(String(50), nullable=True) # 翘曲风险
+
+ # 多方案可信化摘要(第1周阶段1)
+ best_scheme_id = Column(String(64), nullable=True, index=True)
+ confidence_score = Column(Float, nullable=True)
+ is_fallback = Column(Boolean, nullable=True, index=True)
+ fallback_reason = Column(Text, nullable=True)
# 关联关系
stp_file = relationship("STPFile", back_populates="mold_cavity_data")
diff --git a/src/services/calculation_service.py b/src/services/calculation_service.py
index 7277637..b08b947 100644
--- a/src/services/calculation_service.py
+++ b/src/services/calculation_service.py
@@ -325,6 +325,9 @@ class CalculationService:
"title": "推荐方案",
"method": "legacy_fallback",
"score": 60.0,
+ "confidence_score": 45.0,
+ "is_fallback": True,
+ "fallback_reason": "多方案生成失败,已降级为兼容单方案输出",
"score_breakdown": {},
"summary": "当前模型未生成多方案,返回兼容单方案结果",
"parting": {
diff --git a/src/services/cam_bundle_service.py b/src/services/cam_bundle_service.py
new file mode 100644
index 0000000..24e536d
--- /dev/null
+++ b/src/services/cam_bundle_service.py
@@ -0,0 +1,187 @@
+from typing import Dict, Any, List, Optional
+
+from 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()
diff --git a/src/services/processing_service.py b/src/services/processing_service.py
index 4928a7a..b2075a7 100644
--- a/src/services/processing_service.py
+++ b/src/services/processing_service.py
@@ -180,6 +180,8 @@ class ProcessingService:
pointcloud_data = {
"points": mesh_result.get("points", []),
"normals": mesh_result.get("normals", []),
+ "vertices": mesh_result.get("vertices", []),
+ "faces": mesh_result.get("faces", []),
"point_count": mesh_result.get("point_count", 0),
}
diff --git a/src/services/storage_integration_rustfs.py b/src/services/storage_integration_rustfs.py
index 806336f..6408fe7 100644
--- a/src/services/storage_integration_rustfs.py
+++ b/src/services/storage_integration_rustfs.py
@@ -23,6 +23,63 @@ 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,
@@ -272,12 +329,26 @@ class StorageIntegrationService:
file_hash=file_hash
)
- # 3. 提取关键信息
- metadata = cavity_json.get('metadata', {})
- product_analysis = cavity_json.get('product_analysis', {})
- manufacturing_info = cavity_json.get('manufacturing_info', {})
+ # 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 = cavity_json.get('mold_cavities', {}).get('cavity_key_info', {})
+ 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(
@@ -286,9 +357,10 @@ class StorageIntegrationService:
storage_bucket=upload_result['bucket'],
# 模具参数
- mold_material=manufacturing_info.get('recommended_material', 'Aluminum Alloy 7075'),
+ 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,
@@ -306,7 +378,13 @@ class StorageIntegrationService:
# 质量评估
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')
+ 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)
diff --git a/src/services/task_query_service.py b/src/services/task_query_service.py
index 9188f8a..d7eba93 100644
--- a/src/services/task_query_service.py
+++ b/src/services/task_query_service.py
@@ -83,6 +83,10 @@ class TaskQueryService:
html_file_url = cavity_view.get("html_file")
# 构造与内存任务兼容的任务视图
+ cam_preferences = {}
+ if isinstance(processing_task.parameters, dict):
+ cam_preferences = processing_task.parameters.get("cam_preferences", {}) or {}
+
task_view = {
"task_id": processing_task.task_id,
"status": processing_task.status,
@@ -100,6 +104,7 @@ class TaskQueryService:
"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,
diff --git a/src/utils/html_generator.py b/src/utils/html_generator.py
index 501ec93..9067266 100644
--- a/src/utils/html_generator.py
+++ b/src/utils/html_generator.py
@@ -224,7 +224,7 @@ class HTMLGenerator:
function toFlatArray(data) {{
if (!Array.isArray(data)) return [];
if (data.length === 0) return [];
- return Array.isArray(data[0]) ? data.flat() : data;
+ return Array.isArray(data[0]) ? data.flat(Infinity) : data;
}}
function normalizePositions(rawPositions, center) {{
@@ -247,62 +247,137 @@ class HTMLGenerator:
}}
function fitCameraToScene() {{
- const sceneBox = new THREE.Box3().setFromObject(scene);
+ const objects = [productMesh, cavityMesh, coreMesh, partingMesh, pointcloudMesh].filter(Boolean);
+ if (!objects.length) return;
+ const sceneBox = new THREE.Box3();
+ objects.forEach(obj => sceneBox.expandByObject(obj));
if (sceneBox.isEmpty()) return;
const center = new THREE.Vector3();
const size = new THREE.Vector3();
sceneBox.getCenter(center);
sceneBox.getSize(size);
const maxDim = Math.max(size.x, size.y, size.z) || 100;
- const distance = maxDim * 2.2;
+ const distance = Math.max(maxDim * 1.8, 30);
+ camera.near = Math.max(maxDim / 2000, 0.01);
+ camera.far = Math.max(maxDim * 200, 5000);
+ camera.updateProjectionMatrix();
camera.position.set(center.x + distance, center.y + distance, center.z + distance);
controls.target.copy(center);
+ controls.minDistance = Math.max(maxDim * 0.03, 0.5);
+ controls.maxDistance = Math.max(maxDim * 30, 2000);
controls.update();
}}
+ function registerInitialPose(mesh) {{
+ if (!mesh) return;
+ mesh.userData.initialPosition = mesh.position.clone();
+ mesh.userData.initialVisible = mesh.visible;
+ }}
+
+ function computeBounds(rawPositionsList) {{
+ let minX = Infinity, minY = Infinity, minZ = Infinity;
+ let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
+ let hasPoint = false;
+ for (const raw of rawPositionsList) {{
+ const flat = toFlatArray(raw);
+ for (let i = 0; i + 2 < flat.length; i += 3) {{
+ const x = Number(flat[i]);
+ const y = Number(flat[i + 1]);
+ const z = Number(flat[i + 2]);
+ if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z)) continue;
+ hasPoint = true;
+ minX = Math.min(minX, x); minY = Math.min(minY, y); minZ = Math.min(minZ, z);
+ maxX = Math.max(maxX, x); maxY = Math.max(maxY, y); maxZ = Math.max(maxZ, z);
+ }}
+ }}
+ if (!hasPoint) return null;
+ return {{
+ center: [(minX + maxX) / 2, (minY + maxY) / 2, (minZ + maxZ) / 2],
+ dimensions: [Math.max(maxX - minX, 1), Math.max(maxY - minY, 1), Math.max(maxZ - minZ, 1)],
+ }};
+ }}
+
+ function isValidIndexedGeometry(positions, indices) {{
+ if (!positions || !indices) return false;
+ if (positions.length < 9 || indices.length < 3) return false;
+ if (positions.length % 3 !== 0 || indices.length % 3 !== 0) return false;
+ const vertexCount = positions.length / 3;
+ for (let i = 0; i < indices.length; i++) {{
+ const idx = Number(indices[i]);
+ if (!Number.isFinite(idx) || idx < 0 || idx >= vertexCount) return false;
+ }}
+ return true;
+ }}
+
// 根据边界框创建几何体
- const bbox = geometryData.bounding_box;
+ const fallbackBBox = computeBounds([
+ pointcloudData?.points,
+ cavityData?.mold_cavities?.cavity?.vertices,
+ cavityData?.mold_cavities?.core?.vertices
+ ]);
+ const bbox = geometryData.bounding_box || fallbackBBox || {{
+ center: [0, 0, 0],
+ dimensions: [100, 100, 100]
+ }};
if (bbox) {{
const centerOffset = bbox.center || [0, 0, 0];
const width = bbox.dimensions ? bbox.dimensions[0] : 100;
const height = bbox.dimensions ? bbox.dimensions[1] : 100;
const depth = bbox.dimensions ? bbox.dimensions[2] : 100;
+ const coreRequired = cavityData?.metadata?.core_required !== false;
// 如果有点云数据,创建点云模型
if (pointcloudData && pointcloudData.points && pointcloudData.points.length > 0) {{
- // 创建点云几何体
- const pointGeometry = new THREE.BufferGeometry();
- const positions = new Float32Array(normalizePositions(pointcloudData.points, centerOffset));
- pointGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
-
- // 添加法向量(如果有)
- if (pointcloudData.normals && pointcloudData.normals.length > 0) {{
- const normals = new Float32Array(toFlatArray(pointcloudData.normals));
- pointGeometry.setAttribute('normal', new THREE.BufferAttribute(normals, 3));
+ let productRendered = false;
+ // 优先用真实三角网格渲染产品
+ if (pointcloudData.vertices && pointcloudData.faces) {{
+ const productVerts = normalizePositions(pointcloudData.vertices, centerOffset);
+ const productFaces = toFlatArray(pointcloudData.faces);
+ if (productVerts.length >= 9 && productFaces.length >= 3) {{
+ const productGeometry = new THREE.BufferGeometry();
+ productGeometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(productVerts), 3));
+ productGeometry.setIndex(new THREE.BufferAttribute(new Uint32Array(productFaces), 1));
+ productGeometry.computeVertexNormals();
+ const productMaterial = new THREE.MeshPhongMaterial({{
+ color: 0x4CAF50,
+ transparent: true,
+ opacity: 0.45,
+ side: THREE.DoubleSide
+ }});
+ productMesh = new THREE.Mesh(productGeometry, productMaterial);
+ scene.add(productMesh);
+ registerInitialPose(productMesh);
+ productRendered = true;
+ }}
}}
- // 创建点云材质
- const pointMaterial = new THREE.PointsMaterial({{
- color: 0x4CAF50,
- size: 1.0,
- transparent: true,
- opacity: 0.9
- }});
-
- pointcloudMesh = new THREE.Points(pointGeometry, pointMaterial);
- scene.add(pointcloudMesh);
-
- // 计算中心点并调整相机位置
- const center = new THREE.Vector3();
- pointGeometry.computeBoundingBox();
- pointGeometry.boundingBox.getCenter(center);
- controls.target.set(center.x, center.y, center.z);
-
- // 根据边界框大小调整相机距离
- const boundingSphere = new THREE.Sphere();
- pointGeometry.boundingBox.getBoundingSphere(boundingSphere);
- const cameraDistance = boundingSphere.radius * 3;
- camera.position.set(cameraDistance, cameraDistance, cameraDistance);
+ // 网格不可用时回退点云
+ if (!productRendered) {{
+ const pointPositions = normalizePositions(pointcloudData.points, centerOffset);
+ if (pointPositions.length >= 3) {{
+ const pointGeometry = new THREE.BufferGeometry();
+ const positions = new Float32Array(pointPositions);
+ pointGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
+
+ if (pointcloudData.normals && pointcloudData.normals.length > 0) {{
+ const normals = new Float32Array(toFlatArray(pointcloudData.normals));
+ if (normals.length === positions.length) {{
+ pointGeometry.setAttribute('normal', new THREE.BufferAttribute(normals, 3));
+ }}
+ }}
+
+ const pointMaterial = new THREE.PointsMaterial({{
+ color: 0x4CAF50,
+ size: 1.0,
+ transparent: true,
+ opacity: 0.9
+ }});
+
+ pointcloudMesh = new THREE.Points(pointGeometry, pointMaterial);
+ scene.add(pointcloudMesh);
+ registerInitialPose(pointcloudMesh);
+ }}
+ }}
}} else {{
// 创建产品几何体(半透明绿色)
const productGeometry = new THREE.BoxGeometry(width * 0.9, height * 0.9, depth * 0.9);
@@ -316,6 +391,7 @@ class HTMLGenerator:
productMesh = new THREE.Mesh(productGeometry, productMaterial);
productMesh.position.set(0, 0, 0);
scene.add(productMesh);
+ registerInitialPose(productMesh);
}}
// 创建A板/定模(蓝色,分型面以上)
@@ -328,35 +404,36 @@ class HTMLGenerator:
const cavityGeometry = new THREE.BufferGeometry();
const positions = new Float32Array(normalizePositions(cavityVerts, centerOffset));
const indices = new Uint32Array(toFlatArray(cavityFaces));
- cavityGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
- cavityGeometry.setIndex(new THREE.BufferAttribute(indices, 1));
- cavityGeometry.computeVertexNormals();
-
- const cavityMaterial = new THREE.MeshPhongMaterial({{
- color: 0x2196F3,
- transparent: true,
- opacity: 0.5,
- wireframe: false,
- side: THREE.DoubleSide
- }});
-
- cavityMesh = new THREE.Mesh(cavityGeometry, cavityMaterial);
- scene.add(cavityMesh);
-
- // 添加线框
- const cavityWireframe = new THREE.WireframeGeometry(cavityGeometry);
- const cavityLine = new THREE.LineSegments(cavityWireframe);
- cavityLine.material.depthTest = false;
- cavityLine.material.opacity = 0.6;
- cavityLine.material.transparent = true;
- cavityLine.material.color = new THREE.Color(0x1565C0);
- cavityMesh.add(cavityLine);
-
- // 设置相机目标为型腔中心
- cavityGeometry.computeBoundingBox();
- const cavityCenter = new THREE.Vector3();
- cavityGeometry.boundingBox.getCenter(cavityCenter);
- controls.target.set(cavityCenter.x, cavityCenter.y, cavityCenter.z);
+ let cavityBuilt = false;
+ if (isValidIndexedGeometry(positions, indices)) {{
+ cavityGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
+ cavityGeometry.setIndex(new THREE.BufferAttribute(indices, 1));
+ cavityGeometry.computeVertexNormals();
+ cavityBuilt = true;
+ }}
+ if (cavityBuilt) {{
+ const cavityMaterial = new THREE.MeshPhongMaterial({{
+ color: 0x2196F3,
+ transparent: true,
+ opacity: 0.5,
+ wireframe: false,
+ side: THREE.DoubleSide
+ }});
+
+ cavityMesh = new THREE.Mesh(cavityGeometry, cavityMaterial);
+ scene.add(cavityMesh);
+ registerInitialPose(cavityMesh);
+
+ const cavityWireframe = new THREE.WireframeGeometry(cavityGeometry);
+ const cavityLine = new THREE.LineSegments(cavityWireframe);
+ cavityLine.material.depthTest = false;
+ cavityLine.material.opacity = 0.6;
+ cavityLine.material.transparent = true;
+ cavityLine.material.color = new THREE.Color(0x1565C0);
+ cavityMesh.add(cavityLine);
+ }} else {{
+ createSimpleCavity(width, height, depth);
+ }}
}} else {{
createSimpleCavity(width, height, depth);
}}
@@ -365,7 +442,7 @@ class HTMLGenerator:
}}
// 创建B板/动模(橙色,分型面以下)
- if (cavityData && cavityData.mold_cavities && cavityData.mold_cavities.core && cavityData.mold_cavities.core.vertices) {{
+ if (coreRequired && cavityData && cavityData.mold_cavities && cavityData.mold_cavities.core && cavityData.mold_cavities.core.vertices) {{
const coreVerts = cavityData.mold_cavities.core.vertices;
const coreFaces = cavityData.mold_cavities.core.faces;
@@ -373,33 +450,40 @@ class HTMLGenerator:
const coreGeometry = new THREE.BufferGeometry();
const positions = new Float32Array(normalizePositions(coreVerts, centerOffset));
const indices = new Uint32Array(toFlatArray(coreFaces));
- coreGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
- coreGeometry.setIndex(new THREE.BufferAttribute(indices, 1));
- coreGeometry.computeVertexNormals();
-
- const coreMaterial = new THREE.MeshPhongMaterial({{
- color: 0xFF9800,
- transparent: true,
- opacity: 0.5,
- wireframe: false,
- side: THREE.DoubleSide
- }});
-
- coreMesh = new THREE.Mesh(coreGeometry, coreMaterial);
- scene.add(coreMesh);
-
- // 添加线框
- const coreWireframe = new THREE.WireframeGeometry(coreGeometry);
- const coreLine = new THREE.LineSegments(coreWireframe);
- coreLine.material.depthTest = false;
- coreLine.material.opacity = 0.6;
- coreLine.material.transparent = true;
- coreLine.material.color = new THREE.Color(0xE65100);
- coreMesh.add(coreLine);
+ let coreBuilt = false;
+ if (isValidIndexedGeometry(positions, indices)) {{
+ coreGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
+ coreGeometry.setIndex(new THREE.BufferAttribute(indices, 1));
+ coreGeometry.computeVertexNormals();
+ coreBuilt = true;
+ }}
+ if (coreBuilt) {{
+ const coreMaterial = new THREE.MeshPhongMaterial({{
+ color: 0xFF9800,
+ transparent: true,
+ opacity: 0.5,
+ wireframe: false,
+ side: THREE.DoubleSide
+ }});
+
+ coreMesh = new THREE.Mesh(coreGeometry, coreMaterial);
+ scene.add(coreMesh);
+ registerInitialPose(coreMesh);
+
+ const coreWireframe = new THREE.WireframeGeometry(coreGeometry);
+ const coreLine = new THREE.LineSegments(coreWireframe);
+ coreLine.material.depthTest = false;
+ coreLine.material.opacity = 0.6;
+ coreLine.material.transparent = true;
+ coreLine.material.color = new THREE.Color(0xE65100);
+ coreMesh.add(coreLine);
+ }} else {{
+ createSimpleCore(width, height, depth);
+ }}
}} else {{
createSimpleCore(width, height, depth);
}}
- }} else {{
+ }} else if (coreRequired) {{
createSimpleCore(width, height, depth);
}}
@@ -415,6 +499,7 @@ class HTMLGenerator:
partingMesh = new THREE.Mesh(partingGeometry, partingMaterial);
partingMesh.position.set(0, 0, 0);
scene.add(partingMesh);
+ registerInitialPose(partingMesh);
// 添加产品线框
if (productMesh) {{
@@ -444,6 +529,7 @@ class HTMLGenerator:
// Z轴开模:A板放在分型面以上
cavityMesh.position.set(0, 0, halfDepth / 2 + 5);
scene.add(cavityMesh);
+ registerInitialPose(cavityMesh);
const cavityWireframe = new THREE.WireframeGeometry(cavityGeometry);
const cavityLine = new THREE.LineSegments(cavityWireframe);
@@ -470,6 +556,7 @@ class HTMLGenerator:
// Z轴开模:B板放在分型面以下
coreMesh.position.set(0, 0, -halfDepth / 2 - 5);
scene.add(coreMesh);
+ registerInitialPose(coreMesh);
const coreWireframe = new THREE.WireframeGeometry(coreGeometry);
const coreLine = new THREE.LineSegments(coreWireframe);
@@ -501,7 +588,28 @@ class HTMLGenerator:
// 控制函数
function resetView() {{
- controls.reset();
+ if (splitAnimId) {{
+ cancelAnimationFrame(splitAnimId);
+ splitAnimId = null;
+ }}
+ isSplit = false;
+ const btn = document.getElementById('splitBtn');
+ if (btn) btn.textContent = '分模拆分';
+
+ [productMesh, cavityMesh, coreMesh, partingMesh, pointcloudMesh].forEach(mesh => {{
+ if (!mesh) return;
+ if (mesh.userData.initialPosition) {{
+ mesh.position.copy(mesh.userData.initialPosition);
+ }} else {{
+ mesh.position.set(0, 0, 0);
+ }}
+ mesh.visible = mesh.userData.initialVisible !== false;
+ }});
+ if (partingMesh && partingMesh.material) {{
+ partingMesh.material.opacity = 0.3;
+ partingMesh.visible = true;
+ }}
+ fitCameraToScene();
}}
function toggleWireframe() {{
diff --git a/static/style.css b/static/style.css
index ffcb019..a1fd065 100644
--- a/static/style.css
+++ b/static/style.css
@@ -1191,6 +1191,25 @@ optgroup {
grid-column: 1 / -1;
}
+.single-scheme-grid {
+ grid-template-columns: 1fr !important;
+}
+
+.single-scheme-card {
+ max-width: 980px;
+ width: 100%;
+ margin: 0 auto;
+}
+
+.inline-note {
+ margin-top: var(--space-3);
+ font-size: var(--text-sm);
+ color: var(--text-secondary);
+ padding: var(--space-2) var(--space-3);
+ background: var(--bg-tertiary);
+ border-radius: var(--radius-md);
+}
+
/* ================================
信息列表
================================ */
@@ -2146,7 +2165,6 @@ select.form-input {
border-color: #f9a8d4;
}
-<<<<<<< HEAD
/* ================================
表格操作区
================================ */
@@ -2237,9 +2255,6 @@ select.form-input {
margin-top: var(--space-4);
border-top: 1px solid var(--border-light);
}
-
-=======
->>>>>>> 08b3d0a65d280ce8e456987c4eab146115fb51d8
/* ================================
用户区域
================================ */
diff --git a/static/vue-app.js b/static/vue-app.js
index 6fd6efe..c0226e4 100644
--- a/static/vue-app.js
+++ b/static/vue-app.js
@@ -1221,9 +1221,36 @@ const ResultView = {
task: null,
loading: true,
error: '',
- selectedSchemeId: null
+ selectedSchemeId: null,
+ previewStatus: 'idle',
+ camLoading: false,
+ camError: '',
+ camPlan: null,
+ camForm: {
+ mold_steel: 'P20',
+ surface_quality: 'standard',
+ controller: 'fanuc',
+ include_gcode: false
+ }
});
+ const camSteelOptions = [
+ { value: 'P20', label: 'P20' },
+ { value: '718H', label: '718H' },
+ { value: 'NAK80', label: 'NAK80' },
+ { value: 'S136', label: 'S136' },
+ { value: 'H13', label: 'H13' }
+ ];
+ const camSurfaceOptions = [
+ { value: 'standard', label: '标准' },
+ { value: 'fine', label: '精细' },
+ { value: 'mirror', label: '镜面' }
+ ];
+ const camControllerOptions = [
+ { value: 'fanuc', label: 'Fanuc' },
+ { value: 'siemens', label: 'Siemens' }
+ ];
+
const loadTask = async () => {
console.log('[ResultView] loadTask 开始, taskId:', route.params.taskId);
try {
@@ -1238,6 +1265,12 @@ const ResultView = {
} else if (state.task?.candidate_schemes?.length) {
state.selectedSchemeId = state.task.candidate_schemes[0].scheme_id;
}
+ const prefs = state.task?.cam_preferences || {};
+ state.camForm.mold_steel = prefs.mold_steel || 'P20';
+ state.camForm.surface_quality = prefs.surface_quality || 'standard';
+ state.camForm.controller = prefs.controller || 'fanuc';
+ state.camForm.include_gcode = Boolean(prefs.include_gcode || false);
+ state.previewStatus = 'loading';
} catch (e) {
state.error = handleApiError(e, '加载任务详情');
} finally {
@@ -1268,6 +1301,7 @@ const ResultView = {
};
const candidateSchemes = computed(() => state.task?.candidate_schemes || []);
+ const hasSingleScheme = computed(() => candidateSchemes.value.length === 1);
const selectedScheme = computed(() => {
if (!candidateSchemes.value.length) return null;
return candidateSchemes.value.find(s => s.scheme_id === state.selectedSchemeId) || candidateSchemes.value[0];
@@ -1275,9 +1309,21 @@ const ResultView = {
const selectedCavityData = computed(() => selectedScheme.value?.cavity_data || state.task?.cavity_data || null);
const selectedKeyInfo = computed(() => selectedScheme.value?.key_info || state.task?.key_info || null);
const selectedHtmlFile = computed(() => selectedScheme.value?.html_file || state.task?.html_file || '');
+ const selectedDfmViolations = computed(() => selectedScheme.value?.dfm_violations || []);
const selectScheme = (schemeId) => {
state.selectedSchemeId = schemeId;
+ state.previewStatus = 'loading';
+ state.camPlan = null;
+ state.camError = '';
+ };
+
+ const onPreviewLoad = () => {
+ state.previewStatus = 'loaded';
+ };
+
+ const onPreviewError = () => {
+ state.previewStatus = 'error';
};
const formatSchemeDirection = (scheme) => {
@@ -1319,21 +1365,64 @@ const ResultView = {
addNotification(`导出失败: ${e.message}`, 'error');
}
};
+
+ const generateCamPlan = async () => {
+ try {
+ state.camLoading = true;
+ state.camError = '';
+ const taskId = route.params.taskId;
+ const payload = {
+ task_id: taskId,
+ scheme_id: selectedScheme.value?.scheme_id,
+ mold_steel: state.camForm.mold_steel,
+ surface_quality: state.camForm.surface_quality,
+ controller: state.camForm.controller,
+ include_gcode: state.camForm.include_gcode
+ };
+ const result = await apiRequest('/api/cam/plan', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(payload)
+ });
+ state.camPlan = result?.data || null;
+ if (result?.cam_preferences) {
+ state.camForm.mold_steel = result.cam_preferences.mold_steel || state.camForm.mold_steel;
+ state.camForm.surface_quality = result.cam_preferences.surface_quality || state.camForm.surface_quality;
+ state.camForm.controller = result.cam_preferences.controller || state.camForm.controller;
+ state.camForm.include_gcode = Boolean(result.cam_preferences.include_gcode);
+ }
+ addNotification('CAM 计划生成完成', 'success');
+ } catch (e) {
+ state.camPlan = null;
+ state.camError = e.message || 'CAM 计划生成失败';
+ addNotification(`CAM 计划生成失败: ${state.camError}`, 'error');
+ } finally {
+ state.camLoading = false;
+ }
+ };
return {
state,
candidateSchemes,
+ hasSingleScheme,
selectedScheme,
selectedCavityData,
selectedKeyInfo,
selectedHtmlFile,
+ selectedDfmViolations,
formatFileSize,
formatDateTime,
formatNumber,
getPriorityText,
isFoamMaterial,
exportCAD,
+ generateCamPlan,
+ camSteelOptions,
+ camSurfaceOptions,
+ camControllerOptions,
selectScheme,
+ onPreviewLoad,
+ onPreviewError,
formatSchemeDirection
};
},
@@ -1359,6 +1448,9 @@ const ResultView = {
{{ state.task.status }}
+
+ 结构判定
+ {{ selectedScheme?.mold_structure_type === 'two_half_cavity' ? '两板半腔(无独立模芯)' : '型腔 + 模芯' }}
+
@@ -1537,10 +1658,167 @@ const ResultView = {
+
+
+
DFM 检查
+
+
未发现显著 DFM 违规项
+
当前选中方案未命中高风险规则,可进入下一步工艺评审。
+
+
+
+
+
+ | 规则 |
+ 级别 |
+ 说明 |
+
+
+
+
+ | {{ item.rule || 'N/A' }} |
+ {{ item.level || 'medium' }} |
+ {{ item.message || 'N/A' }} |
+
+
+
+
+
+
+
+
CAM 准备包
+
+
CAM 参数
+
+
+ 模具钢
+
+
+
+ 表面质量
+
+
+
+ 控制器
+
+
+
+ 包含G代码
+
+
+
+
+
+
CAM 计划生成失败
+
{{ state.camError }}
+
+
+
尚未生成 CAM 计划
+
点击上方“生成 CAM 计划”后,将返回工序计划、刀具建议与制造风险提示。
+
+
+
+
+
计划摘要
+
+
+ 方案ID
+ {{ state.camPlan.scheme_id || 'N/A' }}
+
+
+ 总工序数
+ {{ state.camPlan.summary?.total_operations || 0 }}
+
+
+ 预计总时长
+ {{ formatNumber(state.camPlan.summary?.total_estimated_time_min || 0) }} min
+
+
+ 方案可信度
+ {{ formatNumber(state.camPlan.confidence?.score || 0) }}
+
+
+
+
+
刀具建议
+
+
+ 粗加工刀具
+ {{ state.camPlan.tooling_suggestion?.roughing_tool?.tool_id || 'N/A' }}
+
+
+ 精加工刀具
+ {{ state.camPlan.tooling_suggestion?.finishing_tool?.tool_id || 'N/A' }}
+
+
+
+
+
+
工序计划
+
+
+
+ | 序号 |
+ 工序 |
+ 刀具 |
+ 主轴转速 |
+ 进给率 |
+ 预计时长(min) |
+
+
+
+
+ | {{ op.seq }} |
+ {{ op.operation }} |
+ {{ op.tool_id || 'N/A' }} |
+ {{ formatNumber(op.spindle_speed_rpm || 0) }} |
+ {{ formatNumber(op.feed_rate_mm_min || 0) }} |
+ {{ formatNumber(op.estimated_time_min || 0) }} |
+
+
+
+
+
+
+
3D 预览
-
+
+
预览加载失败
+
HTML 已生成但加载异常,请检查该链接是否可访问:{{ selectedHtmlFile }}
+
+
+
+
+
+
3D 预览
+
+
预览未生成
+
当前任务没有返回 HTML 预览链接,属于未生成状态。
+