diff --git a/src/services/processing_service.py b/src/services/processing_service.py index adaf460..0f5f5e7 100644 --- a/src/services/processing_service.py +++ b/src/services/processing_service.py @@ -4,6 +4,7 @@ import asyncio import time import traceback +from concurrent.futures import ThreadPoolExecutor from datetime import datetime from pathlib import Path from typing import Optional, Dict, Any @@ -46,6 +47,7 @@ class ProcessingService: self.multi_scheme_planner = MultiSchemeMoldPlanner() self.cad_exporter = CADExporter() self._export_shapes_cache: Dict[str, Dict[str, Dict[str, Any]]] = {} + self._occ_executor = ThreadPoolExecutor(max_workers=2, thread_name_prefix="occ") # ─── 对外入口 ─── @@ -115,8 +117,13 @@ class ProcessingService: ) stage_started = time.perf_counter() - shape = self.stp_parser.load_step_file(Path(file_path)) - geometry_data = self.stp_parser.analyze_geometry(shape) + loop = asyncio.get_running_loop() + shape = await loop.run_in_executor( + self._occ_executor, self.stp_parser.load_step_file, Path(file_path) + ) + geometry_data = await loop.run_in_executor( + self._occ_executor, self.stp_parser.analyze_geometry, shape + ) stage_timings["parse_stp"] = round(time.perf_counter() - stage_started, 3) # 2. 生成网格数据并持久化 @@ -206,24 +213,21 @@ class ProcessingService: pointcloud_data = None lod_data = None if mesh_result: + lod0 = mesh_result.get("lods", {}).get("0", {}) pointcloud_data = { "points": mesh_result.get("points", []), "normals": mesh_result.get("normals", []), - "vertices": mesh_result.get("vertices", []), - "faces": mesh_result.get("faces", []), + "vertices": lod0.get("vertices", []), + "faces": lod0.get("faces", []), "point_count": mesh_result.get("point_count", 0), "vertex_count": mesh_result.get("vertex_count", 0), "face_count": mesh_result.get("face_count", 0), } - # 生成多级LOD数据(用于前端按距离切换精度) - try: - lod_result = self.mesh_generator.generate_multi_lod_mesh(shape) - if lod_result and lod_result.get("lods"): - lod_data = lod_result - logger.info(f"LOD数据生成成功: {len(lod_result['lods'])} 级 (面数: {[lod_result['lods'][k]['face_count'] for k in sorted(lod_result['lods'].keys())]})") - except Exception as lod_err: - logger.warning(f"LOD数据生成失败,使用单级精度: {lod_err}") + if mesh_result and mesh_result.get("lods"): + lods = mesh_result["lods"] + lod_data = mesh_result + logger.info(f"LOD数据复用成功: {len(lods)} 级 (面数: {[lods[k]['face_count'] for k in sorted(lods.keys())]})") detailed_cavity_json = await self._attach_scheme_previews( detailed_cavity_json=detailed_cavity_json, @@ -260,10 +264,14 @@ class ProcessingService: # 9. 分析模具设计 stage_started = time.perf_counter() - analysis_result = self.geometry_analyzer.analyze_mold_design( - geometry_data, - product_material=requested_material, - shape=shape, + loop = asyncio.get_running_loop() + analysis_result = await loop.run_in_executor( + self._occ_executor, + lambda: self.geometry_analyzer.analyze_mold_design( + geometry_data, + product_material=requested_material, + shape=shape, + ), ) if analysis_result: @@ -328,15 +336,14 @@ class ProcessingService: }, ) - # 更新任务缓存状态 + # 更新任务缓存状态(仅保留轻量摘要,完整数据由PG+RustFS持久化) await redis_task_manager.update_task(task_id, { + "status": ProcessingStatus.COMPLETED, + "completed_at": str(datetime.now()), "geometry_data": geometry_data, "analysis_result": analysis_result, - "plan_result": detailed_cavity_json, - "candidate_schemes": detailed_cavity_json.get("candidate_schemes", []), - "best_scheme_id": detailed_cavity_json.get("best_scheme_id"), - "cavity_data": best_cavity_data, "key_info": best_key_info, + "best_scheme_id": detailed_cavity_json.get("best_scheme_id"), "material": requested_material, "parameters": process_params, "stage_timings": stage_timings, @@ -344,8 +351,6 @@ class ProcessingService: "verification": verification_result, "llm_report": llm_report, "export_artifacts": export_artifacts, - "status": ProcessingStatus.COMPLETED, - "completed_at": str(datetime.now()), }) logger.info(f"模具型腔生成完成: {task_id}") @@ -375,18 +380,22 @@ class ProcessingService: self, shape, geometry_data: dict, file_path: str, db_session: AsyncSession, stp_file_id: int, task_id: str, ) -> Optional[Dict[str, Any]]: - """生成网格数据并持久化,失败不影响主流程""" + """生成多级LOD网格并持久化,一次OCC剖分+trimesh简化,失败不影响主流程""" mesh_result = None try: - mesh_result = self.mesh_generator.generate_mesh_from_shape(shape) + loop = asyncio.get_running_loop() + mesh_result = await loop.run_in_executor( + self._occ_executor, self.mesh_generator.generate_multi_lod_mesh, shape + ) - vertices = mesh_result.get("vertices", []) - faces = mesh_result.get("faces", []) + lod0 = mesh_result.get("lods", {}).get("0", {}) + vertices = lod0.get("vertices", []) + faces = lod0.get("faces", []) points = mesh_result.get("points", []) normals = mesh_result.get("normals", []) point_count = mesh_result.get("point_count", 0) - vertex_count = mesh_result.get("vertex_count", 0) - face_count = mesh_result.get("face_count", 0) + vertex_count = lod0.get("vertex_count", mesh_result.get("vertex_count", 0)) + face_count = lod0.get("face_count", mesh_result.get("face_count", 0)) if vertices and faces: bbox = geometry_data.get("bounding_box", {}) @@ -438,11 +447,15 @@ class ProcessingService: plan_result = None try: if shape: - plan_result = self.multi_scheme_planner.generate_plan( - shape=shape, - material=selected_material, - is_foam_material=is_foam_material, - process_params=process_params, + loop = asyncio.get_running_loop() + plan_result = await loop.run_in_executor( + self._occ_executor, + lambda: self.multi_scheme_planner.generate_plan( + shape=shape, + material=selected_material, + is_foam_material=is_foam_material, + process_params=process_params, + ), ) logger.info( f"多方案分模完成: 生成 {len(plan_result.get('candidate_schemes', []))} 套方案" diff --git a/src/services/task_query_service.py b/src/services/task_query_service.py index 57c28fa..fd9b1fc 100644 --- a/src/services/task_query_service.py +++ b/src/services/task_query_service.py @@ -26,11 +26,13 @@ class TaskQueryService: Returns: 任务视图字典,如果任务不存在返回 None """ - # 1. Redis/内存任务(进行中的任务) + # 1. Redis/内存任务(进行中的任务直接返回,已完成/失败的走DB路径获取完整数据) task = await redis_task_manager.get_task(task_id) if task: - logger.info(f"返回缓存任务状态:{task_id} - {task.get('status')}") - return task + status = task.get("status") + if status and status not in ("completed", "failed"): + logger.info(f"返回缓存任务状态:{task_id} - {status}") + return task # 2. 持久化任务(已完成/失败,或服务重启后的任务) storage_service = StorageIntegrationService() diff --git a/src/utils/html_generator.py b/src/utils/html_generator.py index f200607..ad330ee 100644 --- a/src/utils/html_generator.py +++ b/src/utils/html_generator.py @@ -35,8 +35,8 @@ class HTMLGenerator: self.output_dir = Path(output_dir) self.output_dir.mkdir(exist_ok=True) - def generate_3d_viewer_html(self, stp_filename: str, data_filename: str) -> str: - """生成3D可视化HTML页面 — 通过fetch异步加载companion JSON数据""" + def generate_3d_viewer_html(self, stp_filename: str, data_filename: str, summary_filename: str = "") -> str: + """生成3D可视化HTML页面 — 先加载摘要秒显信息面板,再加载网格数据""" cavity_html = self._build_cavity_info_panel_template() @@ -182,6 +182,7 @@ class HTMLGenerator: import * as THREE from 'three'; import {{ OrbitControls }} from 'three/addons/controls/OrbitControls.js'; + const SUMMARY_URL = '{summary_filename}'; const DATA_URL = '{data_filename}'; let productMesh, cavityMesh, coreMesh, partingMesh, pointcloudMesh; @@ -589,10 +590,48 @@ class HTMLGenerator: }} }} + function updateSummaryPanels(summary) {{ + const cd = summary.cavity || summary; + const verts = cd?.mold_cavities?.cavity?.vertex_count || '-'; + const faces = cd?.mold_cavities?.cavity?.face_count || '-'; + const vol = cd?.mold_cavities?.cavity_key_info?.geometric_characteristics?.product_volume || '-'; + document.getElementById('info-verts').textContent = typeof verts === 'number' ? verts.toLocaleString() : verts; + document.getElementById('info-faces').textContent = typeof faces === 'number' ? faces.toLocaleString() : faces; + document.getElementById('info-vol').textContent = vol; + + const panel = document.getElementById('cavity-info-panel'); + if (panel) {{ + panel.style.display = 'block'; + const meta = cd.metadata || {{}}; + const mfg = cd.manufacturing_info || {{}}; + const geo = cd.mold_cavities?.cavity_key_info?.geometric_characteristics || {{}}; + const setVal = (id, val) => {{ const el = document.getElementById(id); if (el) el.textContent = val || 'N/A'; }}; + setVal('cp-shrink', meta.shrinkage_rate); + setVal('cp-draft', meta.draft_angle != null ? meta.draft_angle + '°' : null); + setVal('cp-parting', mfg.parting_line_length); + setVal('cp-vol', geo.product_volume); + setVal('cp-weight', geo.product_weight); + setVal('cp-wall', geo.wall_thickness_range); + setVal('cp-material', mfg.mold_material); + setVal('cp-hardness', mfg.mold_hardness); + setVal('cp-finish', mfg.surface_finish); + setVal('cp-cycle', mfg.estimated_cycle_time); + }} + }} + async function loadData() {{ const statusEl = document.getElementById('loading-status'); try {{ - statusEl.textContent = '正在加载几何数据...'; + if (SUMMARY_URL) {{ + statusEl.textContent = '加载摘要...'; + const summaryResp = await fetch(SUMMARY_URL); + if (summaryResp.ok) {{ + const summary = await summaryResp.json(); + updateSummaryPanels(summary); + }} + }} + + statusEl.textContent = '加载几何数据...'; const resp = await fetch(DATA_URL); if (!resp.ok) throw new Error(`HTTP ${{resp.status}}`); const data = await resp.json(); @@ -775,6 +814,45 @@ class HTMLGenerator: } return data + @staticmethod + def _strip_heavy_geometry(cavity_data: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]: + """从型腔数据中移除顶点/面数组,仅保留元数据和统计信息""" + if not cavity_data: + return None + light: Dict[str, Any] = {} + for key in ("metadata", "manufacturing_info", "scheme_id", "best_scheme_id", "cavity_key_info"): + if key in cavity_data: + light[key] = cavity_data[key] + + mc = cavity_data.get("mold_cavities", {}) + if mc: + light_mc: Dict[str, Any] = {} + if "cavity_key_info" in mc: + light_mc["cavity_key_info"] = mc["cavity_key_info"] + for part_name in ("cavity", "core"): + part = mc.get(part_name, {}) + if part: + light_mc[part_name] = { + "vertex_count": part.get("vertex_count", 0), + "face_count": part.get("face_count", 0), + } + light["mold_cavities"] = light_mc + + return light + + def generate_3d_viewer_summary( + self, + geometry_data: Dict[str, Any], + cavity_data: Optional[Dict[str, Any]] = None, + ) -> Dict[str, Any]: + """生成轻量摘要JSON — 含几何摘要和型腔元数据,不含网格顶点数据""" + return { + "version": "4.1.0", + "generated_at": datetime.now().isoformat(), + "geometry": geometry_data, + "cavity": self._strip_heavy_geometry(cavity_data), + } + def save_html_file(self, html_content: str, filename: str) -> str: """保存HTML文件到磁盘""" try: @@ -806,7 +884,7 @@ class HTMLGenerator: suffix: Optional[str] = None, lod_data: Optional[Dict[str, Any]] = None, ) -> str: - """生成并保存可视化HTML + companion JSON数据文件。返回HTML文件路径(向后兼容)""" + """生成并保存可视化HTML + 摘要JSON + 完整数据JSON。返回HTML文件路径(向后兼容)""" try: base_stem = Path(stp_filename).stem.replace(" ", "_") ts = datetime.now().strftime("%Y%m%d_%H%M%S") @@ -814,14 +892,18 @@ class HTMLGenerator: base_name = f"mold_{base_stem}{suffix_part}_{ts}" html_filename = f"{base_name}.html" + summary_filename = f"{base_name}_summary.json" data_filename = f"{base_name}_data.json" + summary_content = self.generate_3d_viewer_summary(geometry_data, cavity_data) + self.save_data_file(summary_content, summary_filename) + data_content = self.generate_3d_viewer_data( geometry_data, cavity_data, pointcloud_data, lod_data=lod_data ) self.save_data_file(data_content, data_filename) - html_content = self.generate_3d_viewer_html(stp_filename, data_filename) + html_content = self.generate_3d_viewer_html(stp_filename, data_filename, summary_filename) html_file_path = self.save_html_file(html_content, html_filename) return html_file_path