x
This commit is contained in:
@@ -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())
|
||||||
@@ -24,4 +24,5 @@ _safe_include("api.v1.upload_router", "上传")
|
|||||||
_safe_include("api.v1.task_router", "任务")
|
_safe_include("api.v1.task_router", "任务")
|
||||||
_safe_include("api.v1.history_router", "历史")
|
_safe_include("api.v1.history_router", "历史")
|
||||||
_safe_include("api.v1.debug_router", "调试")
|
_safe_include("api.v1.debug_router", "调试")
|
||||||
|
_safe_include("api.v1.cam_router", "CAM")
|
||||||
_safe_include("api.v1.advanced_router", "高级")
|
_safe_include("api.v1.advanced_router", "高级")
|
||||||
|
|||||||
@@ -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}")
|
||||||
@@ -398,14 +398,42 @@ class BaseMoldGenerator:
|
|||||||
core = self._subtract_product_from_plate(b_plate, shape, "B板(回退)")
|
core = self._subtract_product_from_plate(b_plate, shape, "B板(回退)")
|
||||||
return cavity or a_plate, core or b_plate
|
return cavity or a_plate, core or b_plate
|
||||||
|
|
||||||
logger.warning("回退方案也失败,使用最简方法")
|
logger.warning("回退方案也失败,使用最简A/B板切分(避免返回产品本体)")
|
||||||
cavity_op = BRepAlgoAPI_Cut(mold_block, shape)
|
center_z = (zmin + zmax) / 2
|
||||||
cavity = cavity_op.Shape() if cavity_op.IsDone() else mold_block
|
margin = 20
|
||||||
return cavity, shape
|
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:
|
except Exception as e:
|
||||||
logger.error(f"分模回退方案失败: {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,
|
def detect_insert_regions(self, shape: Any, analysis: Dict,
|
||||||
depth_threshold: float = 30.0,
|
depth_threshold: float = 30.0,
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ class MultiSchemeMoldPlanner:
|
|||||||
undercut_regions = generator._build_undercut_regions(
|
undercut_regions = generator._build_undercut_regions(
|
||||||
side_action_result.get("undercut_analysis", {})
|
side_action_result.get("undercut_analysis", {})
|
||||||
)
|
)
|
||||||
|
mold_structure = self._determine_mold_structure(analysis, undercut_regions)
|
||||||
|
|
||||||
scaled_shape = generator._apply_shrinkage_compensation(shape)
|
scaled_shape = generator._apply_shrinkage_compensation(shape)
|
||||||
drafted_shape = generator._apply_draft_angles(scaled_shape, parting_surface)
|
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_reason"] = candidate["reason"]
|
||||||
cavity_data["metadata"]["scheme_offset_ratio"] = candidate.get("offset_ratio", 0.0)
|
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"]["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 {
|
return {
|
||||||
"scheme_id": candidate["scheme_id"],
|
"scheme_id": candidate["scheme_id"],
|
||||||
@@ -161,6 +165,9 @@ class MultiSchemeMoldPlanner:
|
|||||||
"normal_alignment_score": candidate.get("normal_alignment_score"),
|
"normal_alignment_score": candidate.get("normal_alignment_score"),
|
||||||
"offset_ratio": candidate.get("offset_ratio", 0.0),
|
"offset_ratio": candidate.get("offset_ratio", 0.0),
|
||||||
"offset_label": candidate.get("offset_label", "中面"),
|
"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": {
|
"parting": {
|
||||||
"axis": candidate["axis"],
|
"axis": candidate["axis"],
|
||||||
"direction": parting_direction,
|
"direction": parting_direction,
|
||||||
@@ -273,3 +280,40 @@ class MultiSchemeMoldPlanner:
|
|||||||
axis: round(value / total * 100, 2)
|
axis: round(value / total * 100, 2)
|
||||||
for axis, value in stats.items()
|
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,
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from typing import Dict, Any, List
|
from typing import Dict, Any, List, Optional, Tuple
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
|
||||||
@@ -19,8 +19,19 @@ class PartingSchemeScorer:
|
|||||||
)
|
)
|
||||||
|
|
||||||
scored_scheme = dict(scheme)
|
scored_scheme = dict(scheme)
|
||||||
|
fallback = self._assess_fallback(scored_scheme, score_breakdown)
|
||||||
scored_scheme["score_breakdown"] = score_breakdown
|
scored_scheme["score_breakdown"] = score_breakdown
|
||||||
scored_scheme["score"] = total_score
|
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_scheme["summary"] = self._build_summary(scored_scheme)
|
||||||
scored.append(scored_scheme)
|
scored.append(scored_scheme)
|
||||||
|
|
||||||
@@ -95,6 +106,98 @@ class PartingSchemeScorer:
|
|||||||
"risk": round(risk, 2),
|
"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:
|
def _build_summary(self, scheme: Dict[str, Any]) -> str:
|
||||||
cavity_data = scheme.get("cavity_data", {})
|
cavity_data = scheme.get("cavity_data", {})
|
||||||
quality_checks = cavity_data.get("quality_checks", {})
|
quality_checks = cavity_data.get("quality_checks", {})
|
||||||
@@ -106,8 +209,10 @@ class PartingSchemeScorer:
|
|||||||
axis = scheme.get("parting", {}).get("axis", "Z")
|
axis = scheme.get("parting", {}).get("axis", "Z")
|
||||||
offset_label = scheme.get("offset_label", "中面")
|
offset_label = scheme.get("offset_label", "中面")
|
||||||
clamping_force = manufacturing_info.get("estimated_clamping_force", "自动计算")
|
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 (
|
return (
|
||||||
f"{axis} 轴开模,分型面位置 {offset_label},倒扣 {undercut_count} 处,"
|
f"{axis} 轴开模,结构 {structure_text},分型面位置 {offset_label},倒扣 {undercut_count} 处,"
|
||||||
f"滑块 {slider_count} 组,斜顶 {lifter_count} 组,"
|
f"滑块 {slider_count} 组,斜顶 {lifter_count} 组,"
|
||||||
f"预估锁模力 {clamping_force}"
|
f"预估锁模力 {clamping_force}"
|
||||||
)
|
)
|
||||||
@@ -127,3 +232,15 @@ class PartingSchemeScorer:
|
|||||||
return 0.0
|
return 0.0
|
||||||
matches = re.findall(r"\d+(?:\.\d+)?", str(value))
|
matches = re.findall(r"\d+(?:\.\d+)?", str(value))
|
||||||
return float(matches[0]) if matches else 0.0
|
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])
|
||||||
|
|||||||
@@ -185,6 +185,18 @@ async def ensure_schema_updates():
|
|||||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_product_material_unique
|
CREATE UNIQUE INDEX IF NOT EXISTS uq_product_material_unique
|
||||||
ON product_materials (finished_product_id, material_product_id)
|
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__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -346,6 +346,12 @@ class MoldCavityData(Base):
|
|||||||
weld_line_risk = Column(String(50), nullable=True) # 熔接痕风险
|
weld_line_risk = Column(String(50), nullable=True) # 熔接痕风险
|
||||||
sink_mark_risk = Column(String(50), nullable=True) # 缩痕风险
|
sink_mark_risk = Column(String(50), nullable=True) # 缩痕风险
|
||||||
warpage_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")
|
stp_file = relationship("STPFile", back_populates="mold_cavity_data")
|
||||||
|
|||||||
@@ -325,6 +325,9 @@ class CalculationService:
|
|||||||
"title": "推荐方案",
|
"title": "推荐方案",
|
||||||
"method": "legacy_fallback",
|
"method": "legacy_fallback",
|
||||||
"score": 60.0,
|
"score": 60.0,
|
||||||
|
"confidence_score": 45.0,
|
||||||
|
"is_fallback": True,
|
||||||
|
"fallback_reason": "多方案生成失败,已降级为兼容单方案输出",
|
||||||
"score_breakdown": {},
|
"score_breakdown": {},
|
||||||
"summary": "当前模型未生成多方案,返回兼容单方案结果",
|
"summary": "当前模型未生成多方案,返回兼容单方案结果",
|
||||||
"parting": {
|
"parting": {
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -180,6 +180,8 @@ class ProcessingService:
|
|||||||
pointcloud_data = {
|
pointcloud_data = {
|
||||||
"points": mesh_result.get("points", []),
|
"points": mesh_result.get("points", []),
|
||||||
"normals": mesh_result.get("normals", []),
|
"normals": mesh_result.get("normals", []),
|
||||||
|
"vertices": mesh_result.get("vertices", []),
|
||||||
|
"faces": mesh_result.get("faces", []),
|
||||||
"point_count": mesh_result.get("point_count", 0),
|
"point_count": mesh_result.get("point_count", 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,63 @@ logger = get_logger(__name__)
|
|||||||
class StorageIntegrationService:
|
class StorageIntegrationService:
|
||||||
"""存储集成服务 - PostgreSQL + RustFS"""
|
"""存储集成服务 - 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,
|
async def save_stp_file(self, session: AsyncSession,
|
||||||
file_path: Path,
|
file_path: Path,
|
||||||
original_filename: str,
|
original_filename: str,
|
||||||
@@ -272,12 +329,26 @@ class StorageIntegrationService:
|
|||||||
file_hash=file_hash
|
file_hash=file_hash
|
||||||
)
|
)
|
||||||
|
|
||||||
# 3. 提取关键信息
|
# 3. 提取关键信息(兼容多方案与单方案结构)
|
||||||
metadata = cavity_json.get('metadata', {})
|
payload = self._resolve_best_scheme_payload(cavity_json)
|
||||||
product_analysis = cavity_json.get('product_analysis', {})
|
best_scheme_id = payload.get("best_scheme_id")
|
||||||
manufacturing_info = cavity_json.get('manufacturing_info', {})
|
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', {})
|
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记录
|
# 4. 创建PostgreSQL记录
|
||||||
mold_cavity = MoldCavityData(
|
mold_cavity = MoldCavityData(
|
||||||
@@ -286,9 +357,10 @@ class StorageIntegrationService:
|
|||||||
storage_bucket=upload_result['bucket'],
|
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),
|
shrinkage_rate=metadata.get('shrinkage_rate', 0.005),
|
||||||
draft_angle=metadata.get('draft_angle', 2.0),
|
draft_angle=metadata.get('draft_angle', 2.0),
|
||||||
|
parting_line_length=parting_line_length,
|
||||||
|
|
||||||
# 提取的摘要字段
|
# 提取的摘要字段
|
||||||
cavity_key_info=key_info,
|
cavity_key_info=key_info,
|
||||||
@@ -306,7 +378,13 @@ class StorageIntegrationService:
|
|||||||
# 质量评估
|
# 质量评估
|
||||||
weld_line_risk=key_info.get('quality_considerations', {}).get('potential_weld_lines'),
|
weld_line_risk=key_info.get('quality_considerations', {}).get('potential_weld_lines'),
|
||||||
sink_mark_risk=key_info.get('quality_considerations', {}).get('sink_mark_areas'),
|
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)
|
session.add(mold_cavity)
|
||||||
|
|||||||
@@ -83,6 +83,10 @@ class TaskQueryService:
|
|||||||
html_file_url = cavity_view.get("html_file")
|
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_view = {
|
||||||
"task_id": processing_task.task_id,
|
"task_id": processing_task.task_id,
|
||||||
"status": processing_task.status,
|
"status": processing_task.status,
|
||||||
@@ -100,6 +104,7 @@ class TaskQueryService:
|
|||||||
"cavity_data": cavity_view.get("cavity_data"),
|
"cavity_data": cavity_view.get("cavity_data"),
|
||||||
"candidate_schemes": cavity_view.get("candidate_schemes", []),
|
"candidate_schemes": cavity_view.get("candidate_schemes", []),
|
||||||
"best_scheme_id": cavity_view.get("best_scheme_id"),
|
"best_scheme_id": cavity_view.get("best_scheme_id"),
|
||||||
|
"cam_preferences": cam_preferences,
|
||||||
"plan_result": cavity_json,
|
"plan_result": cavity_json,
|
||||||
"mesh_summary": mesh_summary,
|
"mesh_summary": mesh_summary,
|
||||||
"html_file": html_file_url,
|
"html_file": html_file_url,
|
||||||
|
|||||||
+198
-90
@@ -224,7 +224,7 @@ class HTMLGenerator:
|
|||||||
function toFlatArray(data) {{
|
function toFlatArray(data) {{
|
||||||
if (!Array.isArray(data)) return [];
|
if (!Array.isArray(data)) return [];
|
||||||
if (data.length === 0) 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) {{
|
function normalizePositions(rawPositions, center) {{
|
||||||
@@ -247,62 +247,137 @@ class HTMLGenerator:
|
|||||||
}}
|
}}
|
||||||
|
|
||||||
function fitCameraToScene() {{
|
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;
|
if (sceneBox.isEmpty()) return;
|
||||||
const center = new THREE.Vector3();
|
const center = new THREE.Vector3();
|
||||||
const size = new THREE.Vector3();
|
const size = new THREE.Vector3();
|
||||||
sceneBox.getCenter(center);
|
sceneBox.getCenter(center);
|
||||||
sceneBox.getSize(size);
|
sceneBox.getSize(size);
|
||||||
const maxDim = Math.max(size.x, size.y, size.z) || 100;
|
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);
|
camera.position.set(center.x + distance, center.y + distance, center.z + distance);
|
||||||
controls.target.copy(center);
|
controls.target.copy(center);
|
||||||
|
controls.minDistance = Math.max(maxDim * 0.03, 0.5);
|
||||||
|
controls.maxDistance = Math.max(maxDim * 30, 2000);
|
||||||
controls.update();
|
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) {{
|
if (bbox) {{
|
||||||
const centerOffset = bbox.center || [0, 0, 0];
|
const centerOffset = bbox.center || [0, 0, 0];
|
||||||
const width = bbox.dimensions ? bbox.dimensions[0] : 100;
|
const width = bbox.dimensions ? bbox.dimensions[0] : 100;
|
||||||
const height = bbox.dimensions ? bbox.dimensions[1] : 100;
|
const height = bbox.dimensions ? bbox.dimensions[1] : 100;
|
||||||
const depth = bbox.dimensions ? bbox.dimensions[2] : 100;
|
const depth = bbox.dimensions ? bbox.dimensions[2] : 100;
|
||||||
|
const coreRequired = cavityData?.metadata?.core_required !== false;
|
||||||
|
|
||||||
// 如果有点云数据,创建点云模型
|
// 如果有点云数据,创建点云模型
|
||||||
if (pointcloudData && pointcloudData.points && pointcloudData.points.length > 0) {{
|
if (pointcloudData && pointcloudData.points && pointcloudData.points.length > 0) {{
|
||||||
// 创建点云几何体
|
let productRendered = false;
|
||||||
const pointGeometry = new THREE.BufferGeometry();
|
// 优先用真实三角网格渲染产品
|
||||||
const positions = new Float32Array(normalizePositions(pointcloudData.points, centerOffset));
|
if (pointcloudData.vertices && pointcloudData.faces) {{
|
||||||
pointGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
const productVerts = normalizePositions(pointcloudData.vertices, centerOffset);
|
||||||
|
const productFaces = toFlatArray(pointcloudData.faces);
|
||||||
// 添加法向量(如果有)
|
if (productVerts.length >= 9 && productFaces.length >= 3) {{
|
||||||
if (pointcloudData.normals && pointcloudData.normals.length > 0) {{
|
const productGeometry = new THREE.BufferGeometry();
|
||||||
const normals = new Float32Array(toFlatArray(pointcloudData.normals));
|
productGeometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(productVerts), 3));
|
||||||
pointGeometry.setAttribute('normal', new THREE.BufferAttribute(normals, 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({{
|
if (!productRendered) {{
|
||||||
color: 0x4CAF50,
|
const pointPositions = normalizePositions(pointcloudData.points, centerOffset);
|
||||||
size: 1.0,
|
if (pointPositions.length >= 3) {{
|
||||||
transparent: true,
|
const pointGeometry = new THREE.BufferGeometry();
|
||||||
opacity: 0.9
|
const positions = new Float32Array(pointPositions);
|
||||||
}});
|
pointGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
||||||
|
|
||||||
pointcloudMesh = new THREE.Points(pointGeometry, pointMaterial);
|
if (pointcloudData.normals && pointcloudData.normals.length > 0) {{
|
||||||
scene.add(pointcloudMesh);
|
const normals = new Float32Array(toFlatArray(pointcloudData.normals));
|
||||||
|
if (normals.length === positions.length) {{
|
||||||
// 计算中心点并调整相机位置
|
pointGeometry.setAttribute('normal', new THREE.BufferAttribute(normals, 3));
|
||||||
const center = new THREE.Vector3();
|
}}
|
||||||
pointGeometry.computeBoundingBox();
|
}}
|
||||||
pointGeometry.boundingBox.getCenter(center);
|
|
||||||
controls.target.set(center.x, center.y, center.z);
|
const pointMaterial = new THREE.PointsMaterial({{
|
||||||
|
color: 0x4CAF50,
|
||||||
// 根据边界框大小调整相机距离
|
size: 1.0,
|
||||||
const boundingSphere = new THREE.Sphere();
|
transparent: true,
|
||||||
pointGeometry.boundingBox.getBoundingSphere(boundingSphere);
|
opacity: 0.9
|
||||||
const cameraDistance = boundingSphere.radius * 3;
|
}});
|
||||||
camera.position.set(cameraDistance, cameraDistance, cameraDistance);
|
|
||||||
|
pointcloudMesh = new THREE.Points(pointGeometry, pointMaterial);
|
||||||
|
scene.add(pointcloudMesh);
|
||||||
|
registerInitialPose(pointcloudMesh);
|
||||||
|
}}
|
||||||
|
}}
|
||||||
}} else {{
|
}} else {{
|
||||||
// 创建产品几何体(半透明绿色)
|
// 创建产品几何体(半透明绿色)
|
||||||
const productGeometry = new THREE.BoxGeometry(width * 0.9, height * 0.9, depth * 0.9);
|
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 = new THREE.Mesh(productGeometry, productMaterial);
|
||||||
productMesh.position.set(0, 0, 0);
|
productMesh.position.set(0, 0, 0);
|
||||||
scene.add(productMesh);
|
scene.add(productMesh);
|
||||||
|
registerInitialPose(productMesh);
|
||||||
}}
|
}}
|
||||||
|
|
||||||
// 创建A板/定模(蓝色,分型面以上)
|
// 创建A板/定模(蓝色,分型面以上)
|
||||||
@@ -328,35 +404,36 @@ class HTMLGenerator:
|
|||||||
const cavityGeometry = new THREE.BufferGeometry();
|
const cavityGeometry = new THREE.BufferGeometry();
|
||||||
const positions = new Float32Array(normalizePositions(cavityVerts, centerOffset));
|
const positions = new Float32Array(normalizePositions(cavityVerts, centerOffset));
|
||||||
const indices = new Uint32Array(toFlatArray(cavityFaces));
|
const indices = new Uint32Array(toFlatArray(cavityFaces));
|
||||||
cavityGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
let cavityBuilt = false;
|
||||||
cavityGeometry.setIndex(new THREE.BufferAttribute(indices, 1));
|
if (isValidIndexedGeometry(positions, indices)) {{
|
||||||
cavityGeometry.computeVertexNormals();
|
cavityGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
||||||
|
cavityGeometry.setIndex(new THREE.BufferAttribute(indices, 1));
|
||||||
const cavityMaterial = new THREE.MeshPhongMaterial({{
|
cavityGeometry.computeVertexNormals();
|
||||||
color: 0x2196F3,
|
cavityBuilt = true;
|
||||||
transparent: true,
|
}}
|
||||||
opacity: 0.5,
|
if (cavityBuilt) {{
|
||||||
wireframe: false,
|
const cavityMaterial = new THREE.MeshPhongMaterial({{
|
||||||
side: THREE.DoubleSide
|
color: 0x2196F3,
|
||||||
}});
|
transparent: true,
|
||||||
|
opacity: 0.5,
|
||||||
cavityMesh = new THREE.Mesh(cavityGeometry, cavityMaterial);
|
wireframe: false,
|
||||||
scene.add(cavityMesh);
|
side: THREE.DoubleSide
|
||||||
|
}});
|
||||||
// 添加线框
|
|
||||||
const cavityWireframe = new THREE.WireframeGeometry(cavityGeometry);
|
cavityMesh = new THREE.Mesh(cavityGeometry, cavityMaterial);
|
||||||
const cavityLine = new THREE.LineSegments(cavityWireframe);
|
scene.add(cavityMesh);
|
||||||
cavityLine.material.depthTest = false;
|
registerInitialPose(cavityMesh);
|
||||||
cavityLine.material.opacity = 0.6;
|
|
||||||
cavityLine.material.transparent = true;
|
const cavityWireframe = new THREE.WireframeGeometry(cavityGeometry);
|
||||||
cavityLine.material.color = new THREE.Color(0x1565C0);
|
const cavityLine = new THREE.LineSegments(cavityWireframe);
|
||||||
cavityMesh.add(cavityLine);
|
cavityLine.material.depthTest = false;
|
||||||
|
cavityLine.material.opacity = 0.6;
|
||||||
// 设置相机目标为型腔中心
|
cavityLine.material.transparent = true;
|
||||||
cavityGeometry.computeBoundingBox();
|
cavityLine.material.color = new THREE.Color(0x1565C0);
|
||||||
const cavityCenter = new THREE.Vector3();
|
cavityMesh.add(cavityLine);
|
||||||
cavityGeometry.boundingBox.getCenter(cavityCenter);
|
}} else {{
|
||||||
controls.target.set(cavityCenter.x, cavityCenter.y, cavityCenter.z);
|
createSimpleCavity(width, height, depth);
|
||||||
|
}}
|
||||||
}} else {{
|
}} else {{
|
||||||
createSimpleCavity(width, height, depth);
|
createSimpleCavity(width, height, depth);
|
||||||
}}
|
}}
|
||||||
@@ -365,7 +442,7 @@ class HTMLGenerator:
|
|||||||
}}
|
}}
|
||||||
|
|
||||||
// 创建B板/动模(橙色,分型面以下)
|
// 创建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 coreVerts = cavityData.mold_cavities.core.vertices;
|
||||||
const coreFaces = cavityData.mold_cavities.core.faces;
|
const coreFaces = cavityData.mold_cavities.core.faces;
|
||||||
|
|
||||||
@@ -373,33 +450,40 @@ class HTMLGenerator:
|
|||||||
const coreGeometry = new THREE.BufferGeometry();
|
const coreGeometry = new THREE.BufferGeometry();
|
||||||
const positions = new Float32Array(normalizePositions(coreVerts, centerOffset));
|
const positions = new Float32Array(normalizePositions(coreVerts, centerOffset));
|
||||||
const indices = new Uint32Array(toFlatArray(coreFaces));
|
const indices = new Uint32Array(toFlatArray(coreFaces));
|
||||||
coreGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
let coreBuilt = false;
|
||||||
coreGeometry.setIndex(new THREE.BufferAttribute(indices, 1));
|
if (isValidIndexedGeometry(positions, indices)) {{
|
||||||
coreGeometry.computeVertexNormals();
|
coreGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
||||||
|
coreGeometry.setIndex(new THREE.BufferAttribute(indices, 1));
|
||||||
const coreMaterial = new THREE.MeshPhongMaterial({{
|
coreGeometry.computeVertexNormals();
|
||||||
color: 0xFF9800,
|
coreBuilt = true;
|
||||||
transparent: true,
|
}}
|
||||||
opacity: 0.5,
|
if (coreBuilt) {{
|
||||||
wireframe: false,
|
const coreMaterial = new THREE.MeshPhongMaterial({{
|
||||||
side: THREE.DoubleSide
|
color: 0xFF9800,
|
||||||
}});
|
transparent: true,
|
||||||
|
opacity: 0.5,
|
||||||
coreMesh = new THREE.Mesh(coreGeometry, coreMaterial);
|
wireframe: false,
|
||||||
scene.add(coreMesh);
|
side: THREE.DoubleSide
|
||||||
|
}});
|
||||||
// 添加线框
|
|
||||||
const coreWireframe = new THREE.WireframeGeometry(coreGeometry);
|
coreMesh = new THREE.Mesh(coreGeometry, coreMaterial);
|
||||||
const coreLine = new THREE.LineSegments(coreWireframe);
|
scene.add(coreMesh);
|
||||||
coreLine.material.depthTest = false;
|
registerInitialPose(coreMesh);
|
||||||
coreLine.material.opacity = 0.6;
|
|
||||||
coreLine.material.transparent = true;
|
const coreWireframe = new THREE.WireframeGeometry(coreGeometry);
|
||||||
coreLine.material.color = new THREE.Color(0xE65100);
|
const coreLine = new THREE.LineSegments(coreWireframe);
|
||||||
coreMesh.add(coreLine);
|
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 {{
|
}} else {{
|
||||||
createSimpleCore(width, height, depth);
|
createSimpleCore(width, height, depth);
|
||||||
}}
|
}}
|
||||||
}} else {{
|
}} else if (coreRequired) {{
|
||||||
createSimpleCore(width, height, depth);
|
createSimpleCore(width, height, depth);
|
||||||
}}
|
}}
|
||||||
|
|
||||||
@@ -415,6 +499,7 @@ class HTMLGenerator:
|
|||||||
partingMesh = new THREE.Mesh(partingGeometry, partingMaterial);
|
partingMesh = new THREE.Mesh(partingGeometry, partingMaterial);
|
||||||
partingMesh.position.set(0, 0, 0);
|
partingMesh.position.set(0, 0, 0);
|
||||||
scene.add(partingMesh);
|
scene.add(partingMesh);
|
||||||
|
registerInitialPose(partingMesh);
|
||||||
|
|
||||||
// 添加产品线框
|
// 添加产品线框
|
||||||
if (productMesh) {{
|
if (productMesh) {{
|
||||||
@@ -444,6 +529,7 @@ class HTMLGenerator:
|
|||||||
// Z轴开模:A板放在分型面以上
|
// Z轴开模:A板放在分型面以上
|
||||||
cavityMesh.position.set(0, 0, halfDepth / 2 + 5);
|
cavityMesh.position.set(0, 0, halfDepth / 2 + 5);
|
||||||
scene.add(cavityMesh);
|
scene.add(cavityMesh);
|
||||||
|
registerInitialPose(cavityMesh);
|
||||||
|
|
||||||
const cavityWireframe = new THREE.WireframeGeometry(cavityGeometry);
|
const cavityWireframe = new THREE.WireframeGeometry(cavityGeometry);
|
||||||
const cavityLine = new THREE.LineSegments(cavityWireframe);
|
const cavityLine = new THREE.LineSegments(cavityWireframe);
|
||||||
@@ -470,6 +556,7 @@ class HTMLGenerator:
|
|||||||
// Z轴开模:B板放在分型面以下
|
// Z轴开模:B板放在分型面以下
|
||||||
coreMesh.position.set(0, 0, -halfDepth / 2 - 5);
|
coreMesh.position.set(0, 0, -halfDepth / 2 - 5);
|
||||||
scene.add(coreMesh);
|
scene.add(coreMesh);
|
||||||
|
registerInitialPose(coreMesh);
|
||||||
|
|
||||||
const coreWireframe = new THREE.WireframeGeometry(coreGeometry);
|
const coreWireframe = new THREE.WireframeGeometry(coreGeometry);
|
||||||
const coreLine = new THREE.LineSegments(coreWireframe);
|
const coreLine = new THREE.LineSegments(coreWireframe);
|
||||||
@@ -501,7 +588,28 @@ class HTMLGenerator:
|
|||||||
|
|
||||||
// 控制函数
|
// 控制函数
|
||||||
function resetView() {{
|
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() {{
|
function toggleWireframe() {{
|
||||||
|
|||||||
+19
-4
@@ -1191,6 +1191,25 @@ optgroup {
|
|||||||
grid-column: 1 / -1;
|
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;
|
border-color: #f9a8d4;
|
||||||
}
|
}
|
||||||
|
|
||||||
<<<<<<< HEAD
|
|
||||||
/* ================================
|
/* ================================
|
||||||
表格操作区
|
表格操作区
|
||||||
================================ */
|
================================ */
|
||||||
@@ -2237,9 +2255,6 @@ select.form-input {
|
|||||||
margin-top: var(--space-4);
|
margin-top: var(--space-4);
|
||||||
border-top: 1px solid var(--border-light);
|
border-top: 1px solid var(--border-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
=======
|
|
||||||
>>>>>>> 08b3d0a65d280ce8e456987c4eab146115fb51d8
|
|
||||||
/* ================================
|
/* ================================
|
||||||
用户区域
|
用户区域
|
||||||
================================ */
|
================================ */
|
||||||
|
|||||||
+283
-5
@@ -1221,9 +1221,36 @@ const ResultView = {
|
|||||||
task: null,
|
task: null,
|
||||||
loading: true,
|
loading: true,
|
||||||
error: '',
|
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 () => {
|
const loadTask = async () => {
|
||||||
console.log('[ResultView] loadTask 开始, taskId:', route.params.taskId);
|
console.log('[ResultView] loadTask 开始, taskId:', route.params.taskId);
|
||||||
try {
|
try {
|
||||||
@@ -1238,6 +1265,12 @@ const ResultView = {
|
|||||||
} else if (state.task?.candidate_schemes?.length) {
|
} else if (state.task?.candidate_schemes?.length) {
|
||||||
state.selectedSchemeId = state.task.candidate_schemes[0].scheme_id;
|
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) {
|
} catch (e) {
|
||||||
state.error = handleApiError(e, '加载任务详情');
|
state.error = handleApiError(e, '加载任务详情');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -1268,6 +1301,7 @@ const ResultView = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const candidateSchemes = computed(() => state.task?.candidate_schemes || []);
|
const candidateSchemes = computed(() => state.task?.candidate_schemes || []);
|
||||||
|
const hasSingleScheme = computed(() => candidateSchemes.value.length === 1);
|
||||||
const selectedScheme = computed(() => {
|
const selectedScheme = computed(() => {
|
||||||
if (!candidateSchemes.value.length) return null;
|
if (!candidateSchemes.value.length) return null;
|
||||||
return candidateSchemes.value.find(s => s.scheme_id === state.selectedSchemeId) || candidateSchemes.value[0];
|
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 selectedCavityData = computed(() => selectedScheme.value?.cavity_data || state.task?.cavity_data || null);
|
||||||
const selectedKeyInfo = computed(() => selectedScheme.value?.key_info || state.task?.key_info || 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 selectedHtmlFile = computed(() => selectedScheme.value?.html_file || state.task?.html_file || '');
|
||||||
|
const selectedDfmViolations = computed(() => selectedScheme.value?.dfm_violations || []);
|
||||||
|
|
||||||
const selectScheme = (schemeId) => {
|
const selectScheme = (schemeId) => {
|
||||||
state.selectedSchemeId = 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) => {
|
const formatSchemeDirection = (scheme) => {
|
||||||
@@ -1319,21 +1365,64 @@ const ResultView = {
|
|||||||
addNotification(`导出失败: ${e.message}`, 'error');
|
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 {
|
return {
|
||||||
state,
|
state,
|
||||||
candidateSchemes,
|
candidateSchemes,
|
||||||
|
hasSingleScheme,
|
||||||
selectedScheme,
|
selectedScheme,
|
||||||
selectedCavityData,
|
selectedCavityData,
|
||||||
selectedKeyInfo,
|
selectedKeyInfo,
|
||||||
selectedHtmlFile,
|
selectedHtmlFile,
|
||||||
|
selectedDfmViolations,
|
||||||
formatFileSize,
|
formatFileSize,
|
||||||
formatDateTime,
|
formatDateTime,
|
||||||
formatNumber,
|
formatNumber,
|
||||||
getPriorityText,
|
getPriorityText,
|
||||||
isFoamMaterial,
|
isFoamMaterial,
|
||||||
exportCAD,
|
exportCAD,
|
||||||
|
generateCamPlan,
|
||||||
|
camSteelOptions,
|
||||||
|
camSurfaceOptions,
|
||||||
|
camControllerOptions,
|
||||||
selectScheme,
|
selectScheme,
|
||||||
|
onPreviewLoad,
|
||||||
|
onPreviewError,
|
||||||
formatSchemeDirection
|
formatSchemeDirection
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -1359,6 +1448,9 @@ const ResultView = {
|
|||||||
{{ state.task.status }}
|
{{ state.task.status }}
|
||||||
</span>
|
</span>
|
||||||
<div v-if="state.task.status === 'completed'" class="export-buttons">
|
<div v-if="state.task.status === 'completed'" class="export-buttons">
|
||||||
|
<button class="btn-sm btn-primary" :disabled="state.camLoading" @click="generateCamPlan()" title="基于当前选中方案生成CAM工艺计划">
|
||||||
|
{{ state.camLoading ? '生成中...' : '生成 CAM 计划' }}
|
||||||
|
</button>
|
||||||
<button class="btn-sm btn-primary" @click="exportCAD('step')" title="导出STEP格式(UG/FreeCAD/SolidWorks通用)">
|
<button class="btn-sm btn-primary" @click="exportCAD('step')" title="导出STEP格式(UG/FreeCAD/SolidWorks通用)">
|
||||||
导出 STEP
|
导出 STEP
|
||||||
</button>
|
</button>
|
||||||
@@ -1422,11 +1514,11 @@ const ResultView = {
|
|||||||
|
|
||||||
<div v-if="candidateSchemes.length" class="viewer-section">
|
<div v-if="candidateSchemes.length" class="viewer-section">
|
||||||
<h3>候选分模方案</h3>
|
<h3>候选分模方案</h3>
|
||||||
<div class="result-grid">
|
<div :class="['result-grid', hasSingleScheme ? 'single-scheme-grid' : '']">
|
||||||
<div
|
<div
|
||||||
v-for="scheme in candidateSchemes"
|
v-for="scheme in candidateSchemes"
|
||||||
:key="scheme.scheme_id"
|
:key="scheme.scheme_id"
|
||||||
class="result-card"
|
:class="['result-card', hasSingleScheme ? 'single-scheme-card' : '']"
|
||||||
:style="state.selectedSchemeId === scheme.scheme_id ? 'border: 2px solid var(--primary-color);' : ''"
|
:style="state.selectedSchemeId === scheme.scheme_id ? 'border: 2px solid var(--primary-color);' : ''"
|
||||||
>
|
>
|
||||||
<div class="summary-header">
|
<div class="summary-header">
|
||||||
@@ -1438,6 +1530,10 @@ const ResultView = {
|
|||||||
<span class="info-label">分型方向</span>
|
<span class="info-label">分型方向</span>
|
||||||
<span class="info-value">{{ formatSchemeDirection(scheme) }}</span>
|
<span class="info-value">{{ formatSchemeDirection(scheme) }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">模具结构</span>
|
||||||
|
<span class="info-value">{{ scheme.mold_structure_type === 'two_half_cavity' ? '两板半腔(无独立模芯)' : '型腔 + 模芯' }}</span>
|
||||||
|
</div>
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="info-label">分型面位置</span>
|
<span class="info-label">分型面位置</span>
|
||||||
<span class="info-value">{{ scheme.offset_label || '中面' }}</span>
|
<span class="info-value">{{ scheme.offset_label || '中面' }}</span>
|
||||||
@@ -1470,8 +1566,25 @@ const ResultView = {
|
|||||||
<span class="info-label">风险得分</span>
|
<span class="info-label">风险得分</span>
|
||||||
<span class="info-value">{{ formatNumber(scheme.score_breakdown?.risk || 0) }}</span>
|
<span class="info-value">{{ formatNumber(scheme.score_breakdown?.risk || 0) }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">DFM 违规项</span>
|
||||||
|
<span class="info-value">{{ scheme.dfm_violation_count || 0 }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">方案可信度</span>
|
||||||
|
<span class="info-value">{{ formatNumber(scheme.confidence_score || 0) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">回退标记</span>
|
||||||
|
<span class="info-value">{{ scheme.is_fallback ? '是' : '否' }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="scheme.fallback_reason" class="info-item">
|
||||||
|
<span class="info-label">回退原因</span>
|
||||||
|
<span class="info-value">{{ scheme.fallback_reason }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn-sm btn-primary" @click="selectScheme(scheme.scheme_id)">查看此方案</button>
|
<button v-if="candidateSchemes.length > 1" class="btn-sm btn-primary" @click="selectScheme(scheme.scheme_id)">查看此方案</button>
|
||||||
|
<div v-else class="inline-note">当前仅生成 1 套可用分模方案</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1486,6 +1599,8 @@ const ResultView = {
|
|||||||
<th>方向</th>
|
<th>方向</th>
|
||||||
<th>位置</th>
|
<th>位置</th>
|
||||||
<th>总分</th>
|
<th>总分</th>
|
||||||
|
<th>可信度</th>
|
||||||
|
<th>回退</th>
|
||||||
<th>法向匹配</th>
|
<th>法向匹配</th>
|
||||||
<th>可制造性</th>
|
<th>可制造性</th>
|
||||||
<th>分型质量</th>
|
<th>分型质量</th>
|
||||||
@@ -1499,6 +1614,8 @@ const ResultView = {
|
|||||||
<td>{{ formatSchemeDirection(scheme) }}</td>
|
<td>{{ formatSchemeDirection(scheme) }}</td>
|
||||||
<td>{{ scheme.offset_label || '中面' }}</td>
|
<td>{{ scheme.offset_label || '中面' }}</td>
|
||||||
<td>{{ formatNumber(scheme.score) }}</td>
|
<td>{{ formatNumber(scheme.score) }}</td>
|
||||||
|
<td>{{ formatNumber(scheme.confidence_score || 0) }}</td>
|
||||||
|
<td>{{ scheme.is_fallback ? '是' : '否' }}</td>
|
||||||
<td>{{ formatNumber(scheme.normal_alignment_score || 0) }}</td>
|
<td>{{ formatNumber(scheme.normal_alignment_score || 0) }}</td>
|
||||||
<td>{{ formatNumber(scheme.score_breakdown?.manufacturability || 0) }}</td>
|
<td>{{ formatNumber(scheme.score_breakdown?.manufacturability || 0) }}</td>
|
||||||
<td>{{ formatNumber(scheme.score_breakdown?.parting_quality || 0) }}</td>
|
<td>{{ formatNumber(scheme.score_breakdown?.parting_quality || 0) }}</td>
|
||||||
@@ -1524,6 +1641,10 @@ const ResultView = {
|
|||||||
<span class="info-label">材料</span>
|
<span class="info-label">材料</span>
|
||||||
<span class="info-value">{{ selectedCavityData?.metadata?.material || selectedKeyInfo?.metadata?.material || 'N/A' }}</span>
|
<span class="info-value">{{ selectedCavityData?.metadata?.material || selectedKeyInfo?.metadata?.material || 'N/A' }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">结构判定</span>
|
||||||
|
<span class="info-value">{{ selectedScheme?.mold_structure_type === 'two_half_cavity' ? '两板半腔(无独立模芯)' : '型腔 + 模芯' }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="result-card" v-if="selectedCavityData?.mold_cavities || selectedKeyInfo?.mold_cavities">
|
<div class="result-card" v-if="selectedCavityData?.mold_cavities || selectedKeyInfo?.mold_cavities">
|
||||||
@@ -1537,10 +1658,167 @@ const ResultView = {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="viewer-section">
|
||||||
|
<h3>DFM 检查</h3>
|
||||||
|
<div v-if="!selectedDfmViolations.length" class="inline-alert inline-alert-warning">
|
||||||
|
<div class="inline-alert-title">未发现显著 DFM 违规项</div>
|
||||||
|
<div class="inline-alert-message">当前选中方案未命中高风险规则,可进入下一步工艺评审。</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="result-card full-width">
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>规则</th>
|
||||||
|
<th>级别</th>
|
||||||
|
<th>说明</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(item, idx) in selectedDfmViolations" :key="'dfm-' + idx">
|
||||||
|
<td>{{ item.rule || 'N/A' }}</td>
|
||||||
|
<td>{{ item.level || 'medium' }}</td>
|
||||||
|
<td>{{ item.message || 'N/A' }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="viewer-section">
|
||||||
|
<h3>CAM 准备包</h3>
|
||||||
|
<div class="result-card" style="margin-bottom: var(--space-4);">
|
||||||
|
<h4>CAM 参数</h4>
|
||||||
|
<div class="info-list">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">模具钢</span>
|
||||||
|
<select v-model="state.camForm.mold_steel" class="form-select">
|
||||||
|
<option v-for="opt in camSteelOptions" :key="'steel-' + opt.value" :value="opt.value">{{ opt.label }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">表面质量</span>
|
||||||
|
<select v-model="state.camForm.surface_quality" class="form-select">
|
||||||
|
<option v-for="opt in camSurfaceOptions" :key="'surf-' + opt.value" :value="opt.value">{{ opt.label }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">控制器</span>
|
||||||
|
<select v-model="state.camForm.controller" class="form-select">
|
||||||
|
<option v-for="opt in camControllerOptions" :key="'ctrl-' + opt.value" :value="opt.value">{{ opt.label }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">包含G代码</span>
|
||||||
|
<label class="checkbox-group">
|
||||||
|
<input type="checkbox" class="checkbox-input" v-model="state.camForm.include_gcode" />
|
||||||
|
<span class="checkbox-label">返回G代码文本</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="state.camError" class="inline-alert inline-alert-warning">
|
||||||
|
<div class="inline-alert-title">CAM 计划生成失败</div>
|
||||||
|
<div class="inline-alert-message">{{ state.camError }}</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="!state.camPlan" class="inline-alert inline-alert-warning">
|
||||||
|
<div class="inline-alert-title">尚未生成 CAM 计划</div>
|
||||||
|
<div class="inline-alert-message">点击上方“生成 CAM 计划”后,将返回工序计划、刀具建议与制造风险提示。</div>
|
||||||
|
</div>
|
||||||
|
<template v-else>
|
||||||
|
<div class="result-grid">
|
||||||
|
<div class="result-card">
|
||||||
|
<h4>计划摘要</h4>
|
||||||
|
<div class="info-list">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">方案ID</span>
|
||||||
|
<span class="info-value">{{ state.camPlan.scheme_id || 'N/A' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">总工序数</span>
|
||||||
|
<span class="info-value">{{ state.camPlan.summary?.total_operations || 0 }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">预计总时长</span>
|
||||||
|
<span class="info-value">{{ formatNumber(state.camPlan.summary?.total_estimated_time_min || 0) }} min</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">方案可信度</span>
|
||||||
|
<span class="info-value">{{ formatNumber(state.camPlan.confidence?.score || 0) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="result-card">
|
||||||
|
<h4>刀具建议</h4>
|
||||||
|
<div class="info-list">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">粗加工刀具</span>
|
||||||
|
<span class="info-value">{{ state.camPlan.tooling_suggestion?.roughing_tool?.tool_id || 'N/A' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">精加工刀具</span>
|
||||||
|
<span class="info-value">{{ state.camPlan.tooling_suggestion?.finishing_tool?.tool_id || 'N/A' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="result-card full-width">
|
||||||
|
<h4>工序计划</h4>
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>序号</th>
|
||||||
|
<th>工序</th>
|
||||||
|
<th>刀具</th>
|
||||||
|
<th>主轴转速</th>
|
||||||
|
<th>进给率</th>
|
||||||
|
<th>预计时长(min)</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="op in (state.camPlan.process_plan || [])" :key="'cam-op-' + op.seq">
|
||||||
|
<td>{{ op.seq }}</td>
|
||||||
|
<td>{{ op.operation }}</td>
|
||||||
|
<td>{{ op.tool_id || 'N/A' }}</td>
|
||||||
|
<td>{{ formatNumber(op.spindle_speed_rpm || 0) }}</td>
|
||||||
|
<td>{{ formatNumber(op.feed_rate_mm_min || 0) }}</td>
|
||||||
|
<td>{{ formatNumber(op.estimated_time_min || 0) }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="result-card full-width">
|
||||||
|
<h4>制造风险提示</h4>
|
||||||
|
<div class="recommendations-list">
|
||||||
|
<div class="recommendation-item medium" v-for="(warn, idx) in (state.camPlan.manufacturing_warnings || [])" :key="'cam-warn-' + idx">
|
||||||
|
<div class="recommendation-text">{{ warn }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="selectedHtmlFile" class="viewer-section">
|
<div v-if="selectedHtmlFile" class="viewer-section">
|
||||||
<h3>3D 预览</h3>
|
<h3>3D 预览</h3>
|
||||||
<iframe :src="selectedHtmlFile" class="viewer-frame"></iframe>
|
<div v-if="state.previewStatus === 'error'" class="inline-alert inline-alert-warning" style="margin-bottom: var(--space-3);">
|
||||||
|
<div class="inline-alert-title">预览加载失败</div>
|
||||||
|
<div class="inline-alert-message">HTML 已生成但加载异常,请检查该链接是否可访问:{{ selectedHtmlFile }}</div>
|
||||||
|
</div>
|
||||||
|
<iframe
|
||||||
|
:key="selectedHtmlFile"
|
||||||
|
:src="selectedHtmlFile"
|
||||||
|
class="viewer-frame"
|
||||||
|
@load="onPreviewLoad"
|
||||||
|
@error="onPreviewError"
|
||||||
|
></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="viewer-section">
|
||||||
|
<h3>3D 预览</h3>
|
||||||
|
<div class="inline-alert inline-alert-warning">
|
||||||
|
<div class="inline-alert-title">预览未生成</div>
|
||||||
|
<div class="inline-alert-message">当前任务没有返回 HTML 预览链接,属于未生成状态。</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="state.task.analysis_result" class="viewer-section">
|
<div v-if="state.task.analysis_result" class="viewer-section">
|
||||||
|
|||||||
Reference in New Issue
Block a user