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 }}
+ @@ -1422,11 +1514,11 @@ const ResultView = {

候选分模方案

-
+
@@ -1438,6 +1530,10 @@ const ResultView = { 分型方向 {{ formatSchemeDirection(scheme) }}
+
+ 模具结构 + {{ scheme.mold_structure_type === 'two_half_cavity' ? '两板半腔(无独立模芯)' : '型腔 + 模芯' }} +
分型面位置 {{ scheme.offset_label || '中面' }} @@ -1470,8 +1566,25 @@ const ResultView = { 风险得分 {{ formatNumber(scheme.score_breakdown?.risk || 0) }}
+
+ DFM 违规项 + {{ scheme.dfm_violation_count || 0 }} +
+
+ 方案可信度 + {{ formatNumber(scheme.confidence_score || 0) }} +
+
+ 回退标记 + {{ scheme.is_fallback ? '是' : '否' }} +
+
+ 回退原因 + {{ scheme.fallback_reason }} +
- + +
当前仅生成 1 套可用分模方案
@@ -1486,6 +1599,8 @@ const ResultView = { 方向 位置 总分 + 可信度 + 回退 法向匹配 可制造性 分型质量 @@ -1499,6 +1614,8 @@ const ResultView = { {{ formatSchemeDirection(scheme) }} {{ scheme.offset_label || '中面' }} {{ formatNumber(scheme.score) }} + {{ formatNumber(scheme.confidence_score || 0) }} + {{ scheme.is_fallback ? '是' : '否' }} {{ formatNumber(scheme.normal_alignment_score || 0) }} {{ formatNumber(scheme.score_breakdown?.manufacturability || 0) }} {{ formatNumber(scheme.score_breakdown?.parting_quality || 0) }} @@ -1524,6 +1641,10 @@ const ResultView = { 材料 {{ selectedCavityData?.metadata?.material || selectedKeyInfo?.metadata?.material || 'N/A' }}
+
+ 结构判定 + {{ 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 计划”后,将返回工序计划、刀具建议与制造风险提示。
+
+ +

3D 预览

- +
+
预览加载失败
+
HTML 已生成但加载异常,请检查该链接是否可访问:{{ selectedHtmlFile }}
+
+ +
+ +
+

3D 预览

+
+
预览未生成
+
当前任务没有返回 HTML 预览链接,属于未生成状态。
+