From 838604fddf947f934cd718bdd723bfbbc4f1421c Mon Sep 17 00:00:00 2001 From: chenjw28 <792430652@qq.com> Date: Thu, 30 Apr 2026 16:06:15 +0800 Subject: [PATCH 1/4] x --- src/core/aluminum_foam_mold.py | 30 +++++++++++++++++++++ src/core/base_mold_generator.py | 28 ++++++++++++++++++++ src/core/mold_generator.py | 46 +++++++++++++++++++++++++++++++++ 3 files changed, 104 insertions(+) diff --git a/src/core/aluminum_foam_mold.py b/src/core/aluminum_foam_mold.py index 90dae93..06825c3 100644 --- a/src/core/aluminum_foam_mold.py +++ b/src/core/aluminum_foam_mold.py @@ -298,6 +298,36 @@ class AluminumFoamMoldGenerator(BaseMoldGenerator): result["normal_statistics"] = self._analyze_parting_direction(shape) return result + def _analyze_parting_direction(self, shape: Any) -> Dict[str, float]: + """分析产品法向量分布,按面积加权统计各轴方向强度""" + stats = {"X": 0.0, "Y": 0.0, "Z": 0.0} + explorer = TopExp_Explorer(shape, TopAbs_FACE) + + while explorer.More(): + face = TopoDS_Face(explorer.Current()) + explorer.Next() + try: + normal = self._get_face_normal(face) + if normal is None: + continue + props = GProp_GProps() + brepgprop.SurfaceProperties(face, props) + area = max(float(props.Mass()), 1.0) + stats["X"] += abs(float(normal.X())) * area + stats["Y"] += abs(float(normal.Y())) * area + stats["Z"] += abs(float(normal.Z())) * area + except Exception: + continue + + total = stats["X"] + stats["Y"] + stats["Z"] + if total <= 0: + return {"X": 33.3, "Y": 33.3, "Z": 33.4} + + return { + axis: round(value / total * 100, 2) + for axis, value in stats.items() + } + def _split_cavity_core(self, shape: Any, parting_surface: Any) -> Tuple[Any, Any]: """分离型腔和型芯(铝泡沫使用更大余量)""" return super()._split_cavity_core(shape, parting_surface, margin=25) diff --git a/src/core/base_mold_generator.py b/src/core/base_mold_generator.py index 98feff9..3e99e2d 100644 --- a/src/core/base_mold_generator.py +++ b/src/core/base_mold_generator.py @@ -578,6 +578,34 @@ class BaseMoldGenerator: "face_count": 0, } + def _extract_plane_metadata(self, surface: Any) -> Dict[str, Any]: + """从分型面提取平面元数据(法向量、原点、边界)""" + metadata = { + "normal": [0.0, 0.0, 1.0], + "origin": [0.0, 0.0, 0.0], + "bounds": {"min": [0.0, 0.0, 0.0], "max": [0.0, 0.0, 0.0]}, + } + try: + surface_adaptor = BRepAdaptor_Surface(surface) + if surface_adaptor.GetType() == 0: + plane = surface_adaptor.Plane() + axis = plane.Axis() + normal = axis.Direction() + origin = plane.Location() + metadata["normal"] = [float(normal.X()), float(normal.Y()), float(normal.Z())] + metadata["origin"] = [float(origin.X()), float(origin.Y()), float(origin.Z())] + + bbox = Bnd_Box() + brepbndlib.Add(surface, bbox) + xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get() + metadata["bounds"] = { + "min": [float(xmin), float(ymin), float(zmin)], + "max": [float(xmax), float(ymax), float(zmax)], + } + except Exception as e: + logger.warning(f"提取平面元数据失败: {e}") + return metadata + def _calculate_product_weight(self, analysis: Dict) -> str: volume_cm3 = analysis.get("volume", 0) / 1000 weight_g = volume_cm3 * self.material_density diff --git a/src/core/mold_generator.py b/src/core/mold_generator.py index a9c3a57..a409202 100644 --- a/src/core/mold_generator.py +++ b/src/core/mold_generator.py @@ -219,6 +219,52 @@ class MoldCavityGenerator(BaseMoldGenerator): logger.info("使用简化方法检测分型面") return self._simple_parting_surface(shape, analysis) + def _detect_primary_parting(self, shape: Any, analysis: Dict) -> Dict[str, Any]: + """检测主分型面(AI优先 → 几何法向量 → 简化回退)""" + if self.ai_parting_detector is not None: + try: + ai_result = self.ai_parting_detector.detect(shape, analysis) + if ai_result is not None: + surface, line = self._create_parting_surface_from_ai(ai_result, analysis, shape) + return { + "surface": surface, + "line": line, + "direction": ai_result.get("normal", [0, 0, 1]), + "method": ai_result.get("method", "ai"), + "confidence": ai_result.get("confidence", 0.8), + } + except Exception as e: + logger.warning(f"AI 分型面检测失败: {e}") + + try: + normal_dir = self._analyze_face_normals(shape) + parting_plane = self._create_optimal_parting_plane(shape, analysis, normal_dir) + dims = analysis.get("bounding_box", {}).get("dimensions", [100, 100, 100]) + span = max(dims) * 1.5 + 30 + parting_surface = BRepBuilderAPI_MakeFace( + parting_plane, -span, span, -span, span + ).Face() + parting_surface = self.extend_parting_surface(parting_surface, shape, extension=30.0) + parting_line = self._calculate_parting_line(shape, parting_surface) + return { + "surface": parting_surface, + "line": parting_line, + "direction": [float(normal_dir.X()), float(normal_dir.Y()), float(normal_dir.Z())], + "method": "face_normal_analysis", + "confidence": 0.85, + } + except Exception as e: + logger.warning(f"法向量分析失败,使用简化方法:{e}") + + surface, line = self._simple_parting_surface(shape, analysis) + return { + "surface": surface, + "line": line, + "direction": [0, 0, 1], + "method": "simple", + "confidence": 0.6, + } + def _build_undercut_regions(self, undercut_analysis: Dict[str, Any]) -> List[Dict[str, Any]]: """将侧向机构分析结果转换为兼容旧结构的倒扣区域列表。""" undercut_faces = undercut_analysis.get("undercut_faces", []) From c67755015e51ee64fdd756ac3c9d3aa2b3d911a6 Mon Sep 17 00:00:00 2001 From: chenjw28 <792430652@qq.com> Date: Thu, 30 Apr 2026 17:01:43 +0800 Subject: [PATCH 2/4] x --- src/api/v1/advanced_router.py | 63 +++++++-- src/utils/html_generator.py | 241 ++++++++++++++++++++-------------- 2 files changed, 198 insertions(+), 106 deletions(-) diff --git a/src/api/v1/advanced_router.py b/src/api/v1/advanced_router.py index 1743073..d4e94fc 100644 --- a/src/api/v1/advanced_router.py +++ b/src/api/v1/advanced_router.py @@ -4,6 +4,7 @@ import os from fastapi import APIRouter, Depends, HTTPException, Request from services.auth_service import get_current_active_user +from services.redis_task_manager import redis_task_manager from models.database import User from utils.logger import get_logger from api.routes import ( @@ -24,6 +25,16 @@ logger = get_logger(__name__) router = APIRouter() +async def _get_task_data(task_id: str) -> dict: + """从 Redis 优先查找任务,回退到旧版内存字典""" + task = await redis_task_manager.get_task(task_id) + if task: + return task + if task_id in tasks: + return tasks[task_id] + return None + + @router.post("/optimize-layout") async def optimize_cavity_layout( request: Request, @@ -140,10 +151,13 @@ async def ai_parting_surface_detect( body = await request.json() task_id = body.get("task_id") - if not task_id or task_id not in tasks: + if not task_id: + raise HTTPException(404, "缺少 task_id") + + task_data = await _get_task_data(task_id) + if not task_data: raise HTTPException(404, "任务不存在") - task_data = tasks[task_id] geometry_data = task_data.get("geometry_data") if not geometry_data: raise HTTPException(400, "该任务尚未完成几何分析") @@ -166,10 +180,13 @@ async def detect_undercuts( parting_direction = body.get("parting_direction", [0, 0, 1]) mold_size = body.get("mold_size", {"length": 300, "width": 300, "height": 200}) - if not task_id or task_id not in tasks: + if not task_id: + raise HTTPException(404, "缺少 task_id") + + task_data = await _get_task_data(task_id) + if not task_data: raise HTTPException(404, "任务不存在") - task_data = tasks[task_id] geometry_data = task_data.get("geometry_data") if not geometry_data: raise HTTPException(400, "该任务尚未完成几何分析") @@ -288,15 +305,25 @@ async def export_mold_results( formats = body.get("formats", ["step", "stl"]) components = body.get("components", ["cavity", "core"]) - if not task_id or task_id not in tasks: + if not task_id: + raise HTTPException(404, "缺少 task_id") + + task_data = await _get_task_data(task_id) + if not task_data: raise HTTPException(404, "任务不存在") - task_data = tasks[task_id] cavity_shapes = task_data.get("cavity_shapes") - if not cavity_shapes: - raise HTTPException(400, "该任务尚未完成模具生成或形状数据不可用") + filename = task_data.get("filename", f"mold_{task_id}") - base_filename = Path(task_data.get("filename", f"mold_{task_id}")).stem + if not cavity_shapes: + file_path = task_data.get("file_path") + if file_path and os.path.exists(str(file_path)): + cavity_shapes = await _reparse_stp_for_export(str(file_path), task_data.get("material", "ABS")) + + if not cavity_shapes: + raise HTTPException(400, "该任务尚未完成模具生成或形状数据不可用,请等待处理完成后再导出") + + base_filename = Path(filename).stem result = cad_exporter.export_mold_results( cavity_data=cavity_shapes, base_filename=base_filename, @@ -350,3 +377,21 @@ async def get_export_recommendations( """获取导出格式建议(UG/FreeCAD/SolidWorks)""" result = cad_exporter.get_export_recommendations(target) return {"status": "success", "data": result} + + +async def _reparse_stp_for_export(file_path: str, material: str = "ABS") -> dict: + """从 STP 文件重新生成模具型腔数据用于导出""" + try: + from core.stp_parser import STPParser + from core.mold_generator import MoldCavityGenerator + + stp_parser = STPParser() + shape = stp_parser.load_step_file(Path(file_path)) + mold_gen = MoldCavityGenerator(shrinkage_rate=0.005) + mold_gen.set_material(material) + cavity_result = mold_gen.generate_mold_cavities(shape) + logger.info(f"重新解析 STP 用于导出: {file_path}") + return cavity_result + except Exception as e: + logger.warning(f"重新解析 STP 导出失败: {e}") + return None diff --git a/src/utils/html_generator.py b/src/utils/html_generator.py index 501ec93..df0245d 100644 --- a/src/utils/html_generator.py +++ b/src/utils/html_generator.py @@ -241,10 +241,42 @@ class HTMLGenerator: y - Number(center[1] || 0), z - Number(center[2] || 0) ); + }} else {{ + normalized.push(0, 0, 0); }} }} return normalized; }} + + function filterDegenerateFaces(vertices, faces) {{ + const count = vertices.length / 3; + const faceCount = faces.length / 3; + const goodFaces = []; + for (let f = 0; f < faceCount; f++) {{ + const a = faces[f * 3]; + const b = faces[f * 3 + 1]; + const c = faces[f * 3 + 2]; + if (a >= 0 && a < count && b >= 0 && b < count && c >= 0 && c < count && + a !== b && b !== c && a !== c) {{ + goodFaces.push(a, b, c); + }} + }} + return goodFaces; + }} + + function getPartingDirectionFromData() {{ + if (cavityData?.metadata?.scheme_axis) {{ + const ax = cavityData.metadata.scheme_axis; + if (ax === 'X') return {{ axis: 'x', normal: [1, 0, 0], planeRot: [0, 0, Math.PI / 2] }}; + if (ax === 'Y') return {{ axis: 'y', normal: [0, 1, 0], planeRot: [Math.PI / 2, 0, 0] }}; + }} + if (cavityData?.parting?.axis) {{ + const ax = cavityData.parting.axis; + if (ax === 'X') return {{ axis: 'x', normal: [1, 0, 0], planeRot: [0, 0, Math.PI / 2] }}; + if (ax === 'Y') return {{ axis: 'y', normal: [0, 1, 0], planeRot: [Math.PI / 2, 0, 0] }}; + }} + return {{ axis: 'z', normal: [0, 0, 1], planeRot: [0, 0, 0] }}; + }} function fitCameraToScene() {{ const sceneBox = new THREE.Box3().setFromObject(scene); @@ -318,50 +350,56 @@ class HTMLGenerator: scene.add(productMesh); }} + // 获取分模方向 + const partingInfo = getPartingDirectionFromData(); + // 创建A板/定模(蓝色,分型面以上) - // 尝试从后端数据获取实际几何,否则使用简化Box if (cavityData && cavityData.mold_cavities && cavityData.mold_cavities.cavity && cavityData.mold_cavities.cavity.vertices) {{ const cavityVerts = cavityData.mold_cavities.cavity.vertices; const cavityFaces = cavityData.mold_cavities.cavity.faces; if (cavityVerts.length > 0 && cavityFaces.length > 0) {{ - const cavityGeometry = new THREE.BufferGeometry(); const positions = new Float32Array(normalizePositions(cavityVerts, centerOffset)); - const indices = new Uint32Array(toFlatArray(cavityFaces)); - cavityGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); - cavityGeometry.setIndex(new THREE.BufferAttribute(indices, 1)); - cavityGeometry.computeVertexNormals(); + const rawIndices = toFlatArray(cavityFaces); + const goodFaces = filterDegenerateFaces(positions, rawIndices); - const cavityMaterial = new THREE.MeshPhongMaterial({{ - color: 0x2196F3, - transparent: true, - opacity: 0.5, - wireframe: false, - side: THREE.DoubleSide - }}); - - cavityMesh = new THREE.Mesh(cavityGeometry, cavityMaterial); - scene.add(cavityMesh); - - // 添加线框 - const cavityWireframe = new THREE.WireframeGeometry(cavityGeometry); - const cavityLine = new THREE.LineSegments(cavityWireframe); - cavityLine.material.depthTest = false; - cavityLine.material.opacity = 0.6; - cavityLine.material.transparent = true; - cavityLine.material.color = new THREE.Color(0x1565C0); - cavityMesh.add(cavityLine); - - // 设置相机目标为型腔中心 - cavityGeometry.computeBoundingBox(); - const cavityCenter = new THREE.Vector3(); - cavityGeometry.boundingBox.getCenter(cavityCenter); - controls.target.set(cavityCenter.x, cavityCenter.y, cavityCenter.z); + if (goodFaces.length >= 3) {{ + const cavityGeometry = new THREE.BufferGeometry(); + cavityGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); + cavityGeometry.setIndex(goodFaces); + cavityGeometry.computeVertexNormals(); + + const cavityMaterial = new THREE.MeshPhongMaterial({{ + color: 0x2196F3, + transparent: true, + opacity: 0.5, + wireframe: false, + side: THREE.DoubleSide + }}); + + cavityMesh = new THREE.Mesh(cavityGeometry, cavityMaterial); + scene.add(cavityMesh); + + const cavityWireframe = new THREE.WireframeGeometry(cavityGeometry); + const cavityLine = new THREE.LineSegments(cavityWireframe); + cavityLine.material.depthTest = false; + cavityLine.material.opacity = 0.6; + cavityLine.material.transparent = true; + cavityLine.material.color = new THREE.Color(0x1565C0); + cavityMesh.add(cavityLine); + + cavityGeometry.computeBoundingBox(); + const cavityCenter = new THREE.Vector3(); + cavityGeometry.boundingBox.getCenter(cavityCenter); + controls.target.set(cavityCenter.x, cavityCenter.y, cavityCenter.z); + }} else {{ + createSimpleCavity(width, height, depth, partingInfo); + }} }} else {{ - createSimpleCavity(width, height, depth); + createSimpleCavity(width, height, depth, partingInfo); }} }} else {{ - createSimpleCavity(width, height, depth); + createSimpleCavity(width, height, depth, partingInfo); }} // 创建B板/动模(橙色,分型面以下) @@ -370,41 +408,50 @@ class HTMLGenerator: const coreFaces = cavityData.mold_cavities.core.faces; if (coreVerts.length > 0 && coreFaces.length > 0) {{ - const coreGeometry = new THREE.BufferGeometry(); const positions = new Float32Array(normalizePositions(coreVerts, centerOffset)); - const indices = new Uint32Array(toFlatArray(coreFaces)); - coreGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); - coreGeometry.setIndex(new THREE.BufferAttribute(indices, 1)); - coreGeometry.computeVertexNormals(); + const rawIndices = toFlatArray(coreFaces); + const goodFaces = filterDegenerateFaces(positions, rawIndices); - const coreMaterial = new THREE.MeshPhongMaterial({{ - color: 0xFF9800, - transparent: true, - opacity: 0.5, - wireframe: false, - side: THREE.DoubleSide - }}); - - coreMesh = new THREE.Mesh(coreGeometry, coreMaterial); - scene.add(coreMesh); - - // 添加线框 - const coreWireframe = new THREE.WireframeGeometry(coreGeometry); - const coreLine = new THREE.LineSegments(coreWireframe); - coreLine.material.depthTest = false; - coreLine.material.opacity = 0.6; - coreLine.material.transparent = true; - coreLine.material.color = new THREE.Color(0xE65100); - coreMesh.add(coreLine); + if (goodFaces.length >= 3) {{ + const coreGeometry = new THREE.BufferGeometry(); + coreGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); + coreGeometry.setIndex(goodFaces); + coreGeometry.computeVertexNormals(); + + const coreMaterial = new THREE.MeshPhongMaterial({{ + color: 0xFF9800, + transparent: true, + opacity: 0.5, + wireframe: false, + side: THREE.DoubleSide + }}); + + coreMesh = new THREE.Mesh(coreGeometry, coreMaterial); + scene.add(coreMesh); + + const coreWireframe = new THREE.WireframeGeometry(coreGeometry); + const coreLine = new THREE.LineSegments(coreWireframe); + coreLine.material.depthTest = false; + coreLine.material.opacity = 0.6; + coreLine.material.transparent = true; + coreLine.material.color = new THREE.Color(0xE65100); + coreMesh.add(coreLine); + }} else {{ + createSimpleCore(width, height, depth, partingInfo); + }} }} else {{ - createSimpleCore(width, height, depth); + createSimpleCore(width, height, depth, partingInfo); }} }} else {{ - createSimpleCore(width, height, depth); + createSimpleCore(width, height, depth, partingInfo); }} - // 创建分型面(红色平面) - const partingGeometry = new THREE.PlaneGeometry(width * 1.2, height * 1.2); + // 创建分型面(红色平面)- 按分模方向旋转 + const partingInfo2 = getPartingDirectionFromData(); + let planeW = width * 1.2, planeH = height * 1.2; + if (partingInfo2.axis === 'x') {{ planeW = depth * 1.2; planeH = height * 1.2; }} + else if (partingInfo2.axis === 'y') {{ planeW = width * 1.2; planeH = depth * 1.2; }} + const partingGeometry = new THREE.PlaneGeometry(planeW, planeH); const partingMaterial = new THREE.MeshBasicMaterial({{ color: 0xF44336, transparent: true, @@ -413,6 +460,7 @@ class HTMLGenerator: }}); partingMesh = new THREE.Mesh(partingGeometry, partingMaterial); + partingMesh.rotation.set(partingInfo2.planeRot[0], partingInfo2.planeRot[1], partingInfo2.planeRot[2]); partingMesh.position.set(0, 0, 0); scene.add(partingMesh); @@ -425,26 +473,33 @@ class HTMLGenerator: productLine.material.transparent = true; productLine.material.color = new THREE.Color(0x2E7D32); productMesh.add(productLine); + }} else if (cavityMesh || coreMesh) {{ + // 有型腔/型芯但没有产品模型时,补一个参考产品框 + const prodBox = new THREE.BoxGeometry(width * 0.85, height * 0.85, depth * 0.85); + const prodMat = new THREE.MeshPhongMaterial({{ + color: 0x4CAF50, transparent: true, opacity: 0.35, wireframe: false + }}); + productMesh = new THREE.Mesh(prodBox, prodMat); + scene.add(productMesh); }} }} // 创建简化型腔/A板(定模,分型面以上)— 备用 - function createSimpleCavity(width, height, depth) {{ - // A板:分型面(Z中心)以上的上半模 - const halfDepth = depth / 2; - const cavityGeometry = new THREE.BoxGeometry(width * 1.2, height * 1.2, halfDepth + 10); + function createSimpleCavity(width, height, depth, partingInfo) {{ + const pi = partingInfo || {{ axis: 'z', normal: [0, 0, 1] }}; + let boxW = width * 1.2, boxH = height * 1.2, boxD = depth / 2 + 10; + let pos = [0, 0, boxD / 2]; + let rot = [0, 0, 0]; + if (pi.axis === 'x') {{ boxW = depth / 2 + 10; boxH = height * 1.2; boxD = width * 1.2; pos = [boxW / 2, 0, 0]; rot = [0, 0, Math.PI / 2]; }} + else if (pi.axis === 'y') {{ boxW = width * 1.2; boxH = depth / 2 + 10; boxD = height * 1.2; pos = [0, boxH / 2, 0]; rot = [Math.PI / 2, 0, 0]; }} + const cavityGeometry = new THREE.BoxGeometry(boxW, boxH, boxD); const cavityMaterial = new THREE.MeshPhongMaterial({{ - color: 0x2196F3, - transparent: true, - opacity: 0.4, - wireframe: false + color: 0x2196F3, transparent: true, opacity: 0.4, wireframe: false }}); - cavityMesh = new THREE.Mesh(cavityGeometry, cavityMaterial); - // Z轴开模:A板放在分型面以上 - cavityMesh.position.set(0, 0, halfDepth / 2 + 5); + cavityMesh.position.set(pos[0], pos[1], pos[2]); + cavityMesh.rotation.set(rot[0], rot[1], rot[2]); scene.add(cavityMesh); - const cavityWireframe = new THREE.WireframeGeometry(cavityGeometry); const cavityLine = new THREE.LineSegments(cavityWireframe); cavityLine.material.depthTest = false; @@ -455,22 +510,21 @@ class HTMLGenerator: }} // 创建简化型芯/B板(动模,分型面以下)— 备用 - function createSimpleCore(width, height, depth) {{ - // B板:分型面(Z中心)以下的下半模 - const halfDepth = depth / 2; - const coreGeometry = new THREE.BoxGeometry(width * 1.2, height * 1.2, halfDepth + 10); + function createSimpleCore(width, height, depth, partingInfo) {{ + const pi = partingInfo || {{ axis: 'z', normal: [0, 0, 1] }}; + let boxW = width * 1.2, boxH = height * 1.2, boxD = depth / 2 + 10; + let pos = [0, 0, -boxD / 2]; + let rot = [0, 0, 0]; + if (pi.axis === 'x') {{ boxW = depth / 2 + 10; boxH = height * 1.2; boxD = width * 1.2; pos = [-boxW / 2, 0, 0]; rot = [0, 0, Math.PI / 2]; }} + else if (pi.axis === 'y') {{ boxW = width * 1.2; boxH = depth / 2 + 10; boxD = height * 1.2; pos = [0, -boxH / 2, 0]; rot = [Math.PI / 2, 0, 0]; }} + const coreGeometry = new THREE.BoxGeometry(boxW, boxH, boxD); const coreMaterial = new THREE.MeshPhongMaterial({{ - color: 0xFF9800, - transparent: true, - opacity: 0.4, - wireframe: false + color: 0xFF9800, transparent: true, opacity: 0.4, wireframe: false }}); - coreMesh = new THREE.Mesh(coreGeometry, coreMaterial); - // Z轴开模:B板放在分型面以下 - coreMesh.position.set(0, 0, -halfDepth / 2 - 5); + coreMesh.position.set(pos[0], pos[1], pos[2]); + coreMesh.rotation.set(rot[0], rot[1], rot[2]); scene.add(coreMesh); - const coreWireframe = new THREE.WireframeGeometry(coreGeometry); const coreLine = new THREE.LineSegments(coreWireframe); coreLine.material.depthTest = false; @@ -547,18 +601,11 @@ class HTMLGenerator: // 根据材料类型确定分模方向 function getPartingDirection() {{ - // 优先使用后端传递的 parting_direction - if (cavityData?.metadata?.parting_direction) {{ - return cavityData.metadata.parting_direction; - }} - if (cavityData?.manufacturing_info?.parting_direction) {{ - return cavityData.manufacturing_info.parting_direction; - }} - // 泡沫材料默认 Z 轴 - if (cavityData?.metadata?.is_foam) {{ - return 'Z'; - }} - // 默认 Z 轴 + if (cavityData?.metadata?.scheme_axis) return cavityData.metadata.scheme_axis; + if (cavityData?.parting?.axis) return cavityData.parting.axis; + if (cavityData?.metadata?.parting_direction) return cavityData.metadata.parting_direction; + if (cavityData?.manufacturing_info?.parting_direction) return cavityData.manufacturing_info.parting_direction; + if (cavityData?.metadata?.is_foam) return 'Z'; return 'Z'; }} From 9fe4e5f0670c8f2ce5bc0e489be572645da8544f Mon Sep 17 00:00:00 2001 From: chenjw28 <792430652@qq.com> Date: Thu, 30 Apr 2026 17:41:47 +0800 Subject: [PATCH 3/4] x --- src/api/v1/advanced_router.py | 209 +++++++++++++++------------------- 1 file changed, 90 insertions(+), 119 deletions(-) diff --git a/src/api/v1/advanced_router.py b/src/api/v1/advanced_router.py index d4e94fc..0a279be 100644 --- a/src/api/v1/advanced_router.py +++ b/src/api/v1/advanced_router.py @@ -7,30 +7,45 @@ from services.auth_service import get_current_active_user from services.redis_task_manager import redis_task_manager from models.database import User from utils.logger import get_logger -from api.routes import ( - tasks, - cavity_layout_optimizer, - mold_system_designer, - side_action_designer, - mold_cam_designer, - collision_detector, - toolpath_optimizer, - edm_designer, - machining_simulator, - cad_exporter, -) logger = get_logger(__name__) router = APIRouter() +_api_routes_cache = {} + + +def _get_cached(key): + global _api_routes_cache + if key not in _api_routes_cache: + try: + from api import routes + except Exception as e: + logger.warning(f"api.routes 模块加载失败: {e}") + _api_routes_cache["__error__"] = str(e) + return None + _api_routes_cache.clear() + _api_routes_cache.update({ + "tasks": routes.tasks, + "cavity_layout_optimizer": routes.cavity_layout_optimizer, + "mold_system_designer": routes.mold_system_designer, + "side_action_designer": routes.side_action_designer, + "mold_cam_designer": routes.mold_cam_designer, + "collision_detector": routes.collision_detector, + "toolpath_optimizer": routes.toolpath_optimizer, + "edm_designer": routes.edm_designer, + "machining_simulator": routes.machining_simulator, + "cad_exporter": routes.cad_exporter, + }) + return _api_routes_cache.get(key) + async def _get_task_data(task_id: str) -> dict: - """从 Redis 优先查找任务,回退到旧版内存字典""" task = await redis_task_manager.get_task(task_id) if task: return task - if task_id in tasks: + tasks = _get_cached("tasks") + if tasks and task_id in tasks: return tasks[task_id] return None @@ -40,23 +55,22 @@ async def optimize_cavity_layout( request: Request, current_user: User = Depends(get_current_active_user), ): - """多型腔布局优化""" body = await request.json() product_bbox = body.get("product_bbox", {"dimensions": [100, 100, 50]}) cavity_count = body.get("cavity_count", 1) mold_base_size = body.get("mold_base_size") layout_type = body.get("layout_type", "auto") - if cavity_count < 1 or cavity_count > 64: raise HTTPException(400, "型腔数量必须在 1-64 之间") - - result = cavity_layout_optimizer.optimize_layout( + optimizer = _get_cached("cavity_layout_optimizer") + if not optimizer: + raise HTTPException(503, "服务不可用:核心模块未加载") + result = optimizer.optimize_layout( product_bbox=product_bbox, cavity_count=cavity_count, mold_base_size=mold_base_size, layout_type=layout_type, ) - return {"status": "success", "data": result} @@ -65,25 +79,19 @@ async def design_cooling_system( request: Request, current_user: User = Depends(get_current_active_user), ): - """冷却系统设计""" body = await request.json() mold_size = body.get("mold_size", {"length": 300, "width": 300, "height": 200}) product_bbox = body.get("product_bbox", {"dimensions": [100, 100, 50]}) material = body.get("material", "ABS") cavity_count = body.get("cavity_count", 1) cycle_time_target = body.get("cycle_time_target") - from core.mold_system_designer import CoolingSystemDesigner - designer = CoolingSystemDesigner() result = designer.design_cooling_system( - mold_size=mold_size, - product_bbox=product_bbox, - material=material, - cavity_count=cavity_count, + mold_size=mold_size, product_bbox=product_bbox, + material=material, cavity_count=cavity_count, cycle_time_target=cycle_time_target, ) - return {"status": "success", "data": result} @@ -92,25 +100,19 @@ async def design_gating_system( request: Request, current_user: User = Depends(get_current_active_user), ): - """浇注系统设计""" body = await request.json() product_bbox = body.get("product_bbox", {"dimensions": [100, 100, 50]}) material = body.get("material", "ABS") cavity_count = body.get("cavity_count", 1) gate_type = body.get("gate_type", "auto") layout_positions = body.get("layout_positions") - from core.mold_system_designer import GatingSystemDesigner - designer = GatingSystemDesigner() result = designer.design_gating_system( - product_bbox=product_bbox, - material=material, - cavity_count=cavity_count, - gate_type=gate_type, + product_bbox=product_bbox, material=material, + cavity_count=cavity_count, gate_type=gate_type, layout_positions=layout_positions, ) - return {"status": "success", "data": result} @@ -119,7 +121,6 @@ async def design_complete_mold_system( request: Request, current_user: User = Depends(get_current_active_user), ): - """综合模具系统设计(冷却+浇注)""" body = await request.json() mold_size = body.get("mold_size", {"length": 300, "width": 300, "height": 200}) product_bbox = body.get("product_bbox", {"dimensions": [100, 100, 50]}) @@ -128,17 +129,15 @@ async def design_complete_mold_system( gate_type = body.get("gate_type", "auto") cycle_time_target = body.get("cycle_time_target") layout_positions = body.get("layout_positions") - - result = mold_system_designer.design_complete_system( - mold_size=mold_size, - product_bbox=product_bbox, - material=material, - cavity_count=cavity_count, - gate_type=gate_type, - cycle_time_target=cycle_time_target, + ds = _get_cached("mold_system_designer") + if not ds: + raise HTTPException(503, "服务不可用:核心模块未加载") + result = ds.design_complete_system( + mold_size=mold_size, product_bbox=product_bbox, + material=material, cavity_count=cavity_count, + gate_type=gate_type, cycle_time_target=cycle_time_target, layout_positions=layout_positions, ) - return {"status": "success", "data": result} @@ -147,23 +146,17 @@ async def ai_parting_surface_detect( request: Request, current_user: User = Depends(get_current_active_user), ): - """AI 分型面检测""" body = await request.json() task_id = body.get("task_id") - if not task_id: raise HTTPException(404, "缺少 task_id") - task_data = await _get_task_data(task_id) if not task_data: raise HTTPException(404, "任务不存在") - geometry_data = task_data.get("geometry_data") if not geometry_data: raise HTTPException(400, "该任务尚未完成几何分析") - from core.ai_parting_detector import AIPartingSurfaceDetectorV2 - detector = AIPartingSurfaceDetectorV2(use_gnn=True) result = detector._detect_with_geometry(None, geometry_data) return {"status": "success", "data": result} @@ -174,27 +167,20 @@ async def detect_undercuts( request: Request, current_user: User = Depends(get_current_active_user), ): - """倒扣区域检测与滑块/斜顶机构设计""" body = await request.json() task_id = body.get("task_id") parting_direction = body.get("parting_direction", [0, 0, 1]) mold_size = body.get("mold_size", {"length": 300, "width": 300, "height": 200}) - if not task_id: raise HTTPException(404, "缺少 task_id") - task_data = await _get_task_data(task_id) if not task_data: raise HTTPException(404, "任务不存在") - - geometry_data = task_data.get("geometry_data") - if not geometry_data: - raise HTTPException(400, "该任务尚未完成几何分析") - - result = side_action_designer.analyze_and_design( - shape=None, - parting_direction=parting_direction, - mold_size=mold_size, + sd = _get_cached("side_action_designer") + if not sd: + raise HTTPException(503, "服务不可用:核心模块未加载") + result = sd.analyze_and_design( + shape=None, parting_direction=parting_direction, mold_size=mold_size, ) return {"status": "success", "data": result} @@ -204,22 +190,20 @@ async def design_mold_cam( request: Request, current_user: User = Depends(get_current_active_user), ): - """模具CAM刀路设计""" body = await request.json() cavity_bbox = body.get("cavity_bbox", {"dimensions": [100, 100, 50], "min": [-50, -50, -25], "max": [50, 50, 25]}) stock_bbox = body.get("stock_bbox", {"dimensions": [150, 150, 100], "min": [-75, -75, -50], "max": [75, 75, 50]}) mold_steel = body.get("mold_steel", "P20") surface_quality = body.get("surface_quality", "standard") controller = body.get("controller", "fanuc") - - result = mold_cam_designer.design_mold_cam( - cavity_bbox=cavity_bbox, - stock_bbox=stock_bbox, - mold_steel=mold_steel, - surface_quality=surface_quality, + cam = _get_cached("mold_cam_designer") + if not cam: + raise HTTPException(503, "服务不可用:核心模块未加载") + result = cam.design_mold_cam( + cavity_bbox=cavity_bbox, stock_bbox=stock_bbox, + mold_steel=mold_steel, surface_quality=surface_quality, controller=controller, ) - return {"status": "success", "data": result} @@ -228,16 +212,15 @@ async def check_toolpath_collision( request: Request, current_user: User = Depends(get_current_active_user), ): - """刀路碰撞检测""" body = await request.json() toolpath_points = body.get("toolpath_points", [[0, 0, 50], [10, 10, -5], [20, 20, -10]]) tool = body.get("tool", {"diameter": 10, "flute_length": 30, "shank_diameter": 10}) stock_bbox = body.get("stock_bbox", {"min": [-50, -50, -25], "max": [50, 50, 25]}) clamp_positions = body.get("clamp_positions") - - result = collision_detector.check_toolpath_safety( - toolpath_points, tool, stock_bbox, clamp_positions - ) + cd = _get_cached("collision_detector") + if not cd: + raise HTTPException(503, "服务不可用:核心模块未加载") + result = cd.check_toolpath_safety(toolpath_points, tool, stock_bbox, clamp_positions) return {"status": "success", "data": result} @@ -246,15 +229,14 @@ async def optimize_toolpath( request: Request, current_user: User = Depends(get_current_active_user), ): - """刀路优化""" body = await request.json() toolpath_points = body.get("toolpath_points", [[0, 0, 50], [10, 10, -5], [20, 20, -10]]) cutting_params = body.get("cutting_params", {"feed_rate_mm_min": 500}) stock_bbox = body.get("stock_bbox") - - result = toolpath_optimizer.optimize_toolpath( - toolpath_points, cutting_params, stock_bbox - ) + to = _get_cached("toolpath_optimizer") + if not to: + raise HTTPException(503, "服务不可用:核心模块未加载") + result = to.optimize_toolpath(toolpath_points, cutting_params, stock_bbox) return {"status": "success", "data": result} @@ -263,17 +245,16 @@ async def design_edm_electrodes( request: Request, current_user: User = Depends(get_current_active_user), ): - """EDM电极设计""" body = await request.json() undercut_regions = body.get("undercut_regions", [{"center": [0, 0, 0], "area": 100, "type": "undercut"}]) cavity_bbox = body.get("cavity_bbox", {"dimensions": [100, 100, 50]}) material = body.get("material", "copper") spark_gap = body.get("spark_gap", 0.05) overburn = body.get("overburn", 0.1) - - result = edm_designer.design_electrodes( - undercut_regions, cavity_bbox, material, spark_gap, overburn - ) + ed = _get_cached("edm_designer") + if not ed: + raise HTTPException(503, "服务不可用:核心模块未加载") + result = ed.design_electrodes(undercut_regions, cavity_bbox, material, spark_gap, overburn) return {"status": "success", "data": result} @@ -282,15 +263,14 @@ async def simulate_machining( request: Request, current_user: User = Depends(get_current_active_user), ): - """加工仿真""" body = await request.json() operations = body.get("operations", [{"strategy": "z_level_roughing", "levels": [{"z": -5}]}]) stock_bbox = body.get("stock_bbox", {"dimensions": [100, 100, 50], "min": [-50, -50, -25], "max": [50, 50, 25]}) resolution = body.get("resolution", 2.0) - - result = machining_simulator.simulate_machining( - operations, stock_bbox, resolution - ) + ms = _get_cached("machining_simulator") + if not ms: + raise HTTPException(503, "服务不可用:核心模块未加载") + result = ms.simulate_machining(operations, stock_bbox, resolution) return {"status": "success", "data": result} @@ -299,7 +279,6 @@ async def export_mold_results( request: Request, current_user: User = Depends(get_current_active_user), ): - """导出模具设计结果(STEP/IGES/STL/BRep)""" body = await request.json() task_id = body.get("task_id") formats = body.get("formats", ["step", "stl"]) @@ -321,16 +300,19 @@ async def export_mold_results( cavity_shapes = await _reparse_stp_for_export(str(file_path), task_data.get("material", "ABS")) if not cavity_shapes: - raise HTTPException(400, "该任务尚未完成模具生成或形状数据不可用,请等待处理完成后再导出") + raise HTTPException(400, "该任务尚未完成模具生成或形状数据不可用") + + exporter = _get_cached("cad_exporter") + if not exporter: + raise HTTPException(503, "服务不可用:核心模块未加载,请检查 PythonOCC 环境") base_filename = Path(filename).stem - result = cad_exporter.export_mold_results( + result = exporter.export_mold_results( cavity_data=cavity_shapes, base_filename=base_filename, formats=formats, components=components, ) - return {"status": "success", "data": result} @@ -339,34 +321,23 @@ async def download_export_file( filepath: str, current_user: User = Depends(get_current_active_user), ): - """下载导出的CAD文件""" from fastapi.responses import FileResponse - - full_path = os.path.join(cad_exporter.output_dir, filepath) - + exporter = _get_cached("cad_exporter") + if not exporter: + raise HTTPException(503, "服务不可用") + full_path = os.path.join(exporter.output_dir, filepath) if not os.path.exists(full_path): raise HTTPException(404, "文件不存在") - - if not os.path.abspath(full_path).startswith(os.path.abspath(cad_exporter.output_dir)): + if not os.path.abspath(full_path).startswith(os.path.abspath(exporter.output_dir)): raise HTTPException(403, "禁止访问") - media_types = { - ".step": "application/step", - ".stp": "application/step", - ".iges": "application/iges", - ".igs": "application/iges", - ".stl": "model/stl", - ".brep": "application/octet-stream", + ".step": "application/step", ".stp": "application/step", + ".iges": "application/iges", ".igs": "application/iges", + ".stl": "model/stl", ".brep": "application/octet-stream", } - ext = Path(full_path).suffix.lower() media_type = media_types.get(ext, "application/octet-stream") - - return FileResponse( - full_path, - media_type=media_type, - filename=os.path.basename(full_path), - ) + return FileResponse(full_path, media_type=media_type, filename=os.path.basename(full_path)) @router.get("/export-recommendations") @@ -374,17 +345,17 @@ async def get_export_recommendations( target: str = "ug", current_user: User = Depends(get_current_active_user), ): - """获取导出格式建议(UG/FreeCAD/SolidWorks)""" - result = cad_exporter.get_export_recommendations(target) + exporter = _get_cached("cad_exporter") + if not exporter: + raise HTTPException(503, "服务不可用") + result = exporter.get_export_recommendations(target) return {"status": "success", "data": result} async def _reparse_stp_for_export(file_path: str, material: str = "ABS") -> dict: - """从 STP 文件重新生成模具型腔数据用于导出""" try: from core.stp_parser import STPParser from core.mold_generator import MoldCavityGenerator - stp_parser = STPParser() shape = stp_parser.load_step_file(Path(file_path)) mold_gen = MoldCavityGenerator(shrinkage_rate=0.005) From 0f4c17e67fdd10d26d7c28b050c22f8861f4d28b Mon Sep 17 00:00:00 2001 From: chenjw28 <792430652@qq.com> Date: Thu, 30 Apr 2026 17:54:29 +0800 Subject: [PATCH 4/4] x --- src/utils/html_generator.py | 38 ++++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/src/utils/html_generator.py b/src/utils/html_generator.py index df0245d..616f000 100644 --- a/src/utils/html_generator.py +++ b/src/utils/html_generator.py @@ -537,6 +537,19 @@ class HTMLGenerator: // 统一按场景包围盒调整相机,避免模型错位/尺度不一致导致观感混乱 fitCameraToScene(); + // 保存各组件原始位置(分模动画合模时复原用) + const cavityHomePos = {{ + x: cavityMesh ? cavityMesh.position.x : 0, + y: cavityMesh ? cavityMesh.position.y : 0, + z: cavityMesh ? cavityMesh.position.z : 0, + }}; + const coreHomePos = {{ + x: coreMesh ? coreMesh.position.x : 0, + y: coreMesh ? coreMesh.position.y : 0, + z: coreMesh ? coreMesh.position.z : 0, + }}; + const partingHomeOpacity = partingMesh ? partingMesh.material.opacity : 0.3; + // 动画循环 function animate() {{ requestAnimationFrame(animate); @@ -610,7 +623,7 @@ class HTMLGenerator: }} function splitMold() {{ - if (!cavityMesh && !coreMesh) return; + if (!cavityMesh || !coreMesh) return; isSplit = !isSplit; const btn = document.getElementById('splitBtn'); btn.textContent = isSplit ? '合模' : '分模拆分'; @@ -618,33 +631,28 @@ class HTMLGenerator: const bboxDims = geometryData.bounding_box?.dimensions || [100, 100, 100]; const dir = getPartingDirection(); - // 根据分模方向确定拆分轴和距离 let splitDist, axis; if (dir === 'Z') {{ - // Z轴上下开模:拆分距离取 Z 方向高度的 40% splitDist = bboxDims[2] * 0.4; axis = 'z'; }} else if (dir === 'Y') {{ - // Y轴前后开模 splitDist = bboxDims[1] * 0.4; axis = 'y'; }} else {{ - // X轴左右开模 splitDist = bboxDims[0] * 0.4; axis = 'x'; }} - // 分型面动画目标透明度 - const partingTargetOpacity = isSplit ? 0 : 0.3; + const partingTargetOpacity = isSplit ? 0 : partingHomeOpacity; - // 记录起始位置 - const cavityStart = cavityMesh ? cavityMesh.position[axis] : 0; - const coreStart = coreMesh ? coreMesh.position[axis] : 0; + const cavityStart = cavityMesh.position[axis]; + const coreStart = coreMesh.position[axis]; + const cavityHome = cavityHomePos[axis]; + const coreHome = coreHomePos[axis]; const partingStartOpacity = partingMesh ? partingMesh.material.opacity : 0.3; - // 目标位置:型腔正向移动,型芯负向移动 - const cavityTarget = isSplit ? splitDist : 0; - const coreTarget = isSplit ? -splitDist : 0; + const cavityTarget = isSplit ? (cavityHome + splitDist) : cavityHome; + const coreTarget = isSplit ? (coreHome - splitDist) : coreHome; const duration = 800; // ms const startTime = performance.now(); @@ -665,17 +673,17 @@ class HTMLGenerator: }} if (partingMesh) {{ partingMesh.material.opacity = partingStartOpacity + (partingTargetOpacity - partingStartOpacity) * ease; - if (isSplit && t >= 1) partingMesh.visible = false; - else partingMesh.visible = true; }} if (t < 1) {{ splitAnimId = requestAnimationFrame(animateSplit); }} else {{ splitAnimId = null; + if (partingMesh) partingMesh.visible = !isSplit; }} }} + if (partingMesh) partingMesh.visible = true; splitAnimId = requestAnimationFrame(animateSplit); }}