fix: 恢复 init_db.py 等全部缺失文件 + LLM 集成
This commit is contained in:
+85
@@ -0,0 +1,85 @@
|
||||
src/api/__init__.py
|
||||
src/api/aluminum_price_routes.py
|
||||
src/api/auth_routes.py
|
||||
src/api/inventory/__init__.py
|
||||
src/api/inventory/customer_routes.py
|
||||
src/api/inventory/dashboard_routes.py
|
||||
src/api/inventory/finance_routes.py
|
||||
src/api/inventory/inventory_routes.py
|
||||
src/api/inventory/material_routes.py
|
||||
src/api/inventory/product_routes.py
|
||||
src/api/inventory/purchase_order_routes.py
|
||||
src/api/inventory/sales_order_routes.py
|
||||
src/api/inventory/schemas/__init__.py
|
||||
src/api/inventory/schemas/common_schemas.py
|
||||
src/api/inventory/schemas/customer_schemas.py
|
||||
src/api/inventory/schemas/finance_schemas.py
|
||||
src/api/inventory/schemas/inventory_schemas.py
|
||||
src/api/inventory/schemas/material_schemas.py
|
||||
src/api/inventory/schemas/product_schemas.py
|
||||
src/api/inventory/schemas/purchase_order_schemas.py
|
||||
src/api/inventory/schemas/sales_order_schemas.py
|
||||
src/api/inventory/schemas/stock_movement_schemas.py
|
||||
src/api/inventory/schemas/supplier_schemas.py
|
||||
src/api/inventory/schemas/warehouse_schemas.py
|
||||
src/api/inventory/stock_movement_routes.py
|
||||
src/api/inventory/supplier_routes.py
|
||||
src/api/inventory/utils.py
|
||||
src/api/inventory/warehouse_routes.py
|
||||
src/api/routes.py
|
||||
src/api/v1/__init__.py
|
||||
src/api/v1/advanced_router.py
|
||||
src/api/v1/cam_router.py
|
||||
src/api/v1/debug_router.py
|
||||
src/api/v1/health_router.py
|
||||
src/api/v1/history_router.py
|
||||
src/api/v1/task_router.py
|
||||
src/api/v1/upload_router.py
|
||||
src/core/__init__.py
|
||||
src/core/ai_mold_assistant.py
|
||||
src/core/ai_parting_detector.py
|
||||
src/core/aluminum_foam_mold.py
|
||||
src/core/base_mold_generator.py
|
||||
src/core/cad_exporter.py
|
||||
src/core/cavity_layout_optimizer.py
|
||||
src/core/geometry_analyzer.py
|
||||
src/core/mesh_generator.py
|
||||
src/core/mold_cam.py
|
||||
src/core/mold_generator.py
|
||||
src/core/mold_machining.py
|
||||
src/core/mold_quality_inspector.py
|
||||
src/core/mold_system_designer.py
|
||||
src/core/multi_scheme_planner.py
|
||||
src/core/parting_candidate_generator.py
|
||||
src/core/parting_scheme_scorer.py
|
||||
src/core/side_action_designer.py
|
||||
src/core/stp_parser.py
|
||||
src/database/__init__.py
|
||||
src/database/database.py
|
||||
src/database/init_db.py
|
||||
src/database/migrate_db.py
|
||||
src/main.py
|
||||
src/models/database.py
|
||||
src/models/schemas.py
|
||||
src/scripts/create_admin.py
|
||||
src/services/__init__.py
|
||||
src/services/aluminum_price_service.py
|
||||
src/services/auth_service.py
|
||||
src/services/calculation_service.py
|
||||
src/services/cam_bundle_service.py
|
||||
src/services/material_service.py
|
||||
src/services/processing_service.py
|
||||
src/services/redis_task_manager.py
|
||||
src/services/storage_integration.py
|
||||
src/services/storage_integration_rustfs.py
|
||||
src/services/storage_service.py
|
||||
src/services/task_query_service.py
|
||||
src/services/verification_service.py
|
||||
src/storage/__init__.py
|
||||
src/storage/init_storage.py
|
||||
src/storage/object_storage.py
|
||||
src/storage/rustfs_storage.py
|
||||
src/utils/__init__.py
|
||||
src/utils/file_handler.py
|
||||
src/utils/html_generator.py
|
||||
src/utils/logger.py
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
src/main.py
|
||||
src/api/aluminum_price_routes.py
|
||||
src/api/auth_routes.py
|
||||
src/api/routes.py
|
||||
src/api/__init__.py
|
||||
src/api/inventory/customer_routes.py
|
||||
src/api/inventory/dashboard_routes.py
|
||||
src/api/inventory/finance_routes.py
|
||||
src/api/inventory/inventory_routes.py
|
||||
src/api/inventory/material_routes.py
|
||||
src/api/inventory/product_routes.py
|
||||
src/api/inventory/purchase_order_routes.py
|
||||
src/api/inventory/sales_order_routes.py
|
||||
src/api/inventory/stock_movement_routes.py
|
||||
src/api/inventory/supplier_routes.py
|
||||
src/api/inventory/utils.py
|
||||
src/api/inventory/warehouse_routes.py
|
||||
src/api/inventory/__init__.py
|
||||
src/api/inventory/schemas/common_schemas.py
|
||||
src/api/inventory/schemas/customer_schemas.py
|
||||
src/api/inventory/schemas/finance_schemas.py
|
||||
src/api/inventory/schemas/inventory_schemas.py
|
||||
src/api/inventory/schemas/material_schemas.py
|
||||
src/api/inventory/schemas/product_schemas.py
|
||||
src/api/inventory/schemas/purchase_order_schemas.py
|
||||
src/api/inventory/schemas/sales_order_schemas.py
|
||||
src/api/inventory/schemas/stock_movement_schemas.py
|
||||
src/api/inventory/schemas/supplier_schemas.py
|
||||
src/api/inventory/schemas/warehouse_schemas.py
|
||||
src/api/inventory/schemas/__init__.py
|
||||
src/api/inventory/__pycache__/__init__.cpython-312.pyc
|
||||
src/api/v1/advanced_router.py
|
||||
src/api/v1/cam_router.py
|
||||
src/api/v1/debug_router.py
|
||||
src/api/v1/health_router.py
|
||||
src/api/v1/history_router.py
|
||||
src/api/v1/task_router.py
|
||||
src/api/v1/upload_router.py
|
||||
src/api/v1/__init__.py
|
||||
src/api/v1/__pycache__/advanced_router.cpython-312.pyc
|
||||
src/api/v1/__pycache__/upload_router.cpython-312.pyc
|
||||
src/api/v1/__pycache__/__init__.cpython-312.pyc
|
||||
src/api/__pycache__/aluminum_price_routes.cpython-312.pyc
|
||||
src/api/__pycache__/routes.cpython-312.pyc
|
||||
src/api/__pycache__/__init__.cpython-312.pyc
|
||||
src/core/ai_mold_assistant.py
|
||||
src/core/ai_parting_detector.py
|
||||
src/core/aluminum_foam_mold.py
|
||||
src/core/cad_exporter.py
|
||||
src/core/cavity_layout_optimizer.py
|
||||
src/core/geometry_analyzer.py
|
||||
src/core/mesh_generator.py
|
||||
src/core/mold_cam.py
|
||||
src/core/mold_generator.py
|
||||
src/core/mold_machining.py
|
||||
src/core/mold_quality_inspector.py
|
||||
src/core/mold_system_designer.py
|
||||
src/core/multi_scheme_planner.py
|
||||
src/core/parting_candidate_generator.py
|
||||
src/core/parting_scheme_scorer.py
|
||||
src/core/side_action_designer.py
|
||||
src/core/stp_parser.py
|
||||
src/core/__init__.py
|
||||
src/core/__pycache__/ai_parting_detector.cpython-312.pyc
|
||||
src/core/__pycache__/aluminum_foam_mold.cpython-312.pyc
|
||||
src/core/__pycache__/base_mold_generator.cpython-312.pyc
|
||||
src/core/__pycache__/cad_exporter.cpython-312.pyc
|
||||
src/core/__pycache__/mold_generator.cpython-312.pyc
|
||||
src/core/__pycache__/multi_scheme_planner.cpython-312.pyc
|
||||
src/core/__pycache__/side_action_designer.cpython-312.pyc
|
||||
src/core/__pycache__/stp_parser.cpython-312.pyc
|
||||
src/core/__pycache__/__init__.cpython-312.pyc
|
||||
src/database/database.py
|
||||
src/database/migrate_db.py
|
||||
src/database/__init__.py
|
||||
src/database/__pycache__/database.cpython-312.pyc
|
||||
src/database/__pycache__/__init__.cpython-312.pyc
|
||||
src/models/database.py
|
||||
src/models/schemas.py
|
||||
src/models/__pycache__/schemas.cpython-312.pyc
|
||||
src/scripts/create_admin.py
|
||||
src/services/aluminum_price_service.py
|
||||
src/services/auth_service.py
|
||||
src/services/calculation_service.py
|
||||
src/services/cam_bundle_service.py
|
||||
src/services/llm_service.py
|
||||
src/services/material_service.py
|
||||
src/services/processing_service.py
|
||||
src/services/redis_task_manager.py
|
||||
src/services/storage_integration.py
|
||||
src/services/storage_integration_rustfs.py
|
||||
src/services/storage_service.py
|
||||
src/services/task_query_service.py
|
||||
src/services/verification_service.py
|
||||
src/services/__init__.py
|
||||
src/services/__pycache__/aluminum_price_service.cpython-312.pyc
|
||||
src/services/__pycache__/processing_service.cpython-312.pyc
|
||||
src/services/__pycache__/verification_service.cpython-312.pyc
|
||||
src/services/__pycache__/__init__.cpython-312.pyc
|
||||
src/storage/init_storage.py
|
||||
src/storage/object_storage.py
|
||||
src/storage/rustfs_storage.py
|
||||
src/storage/__init__.py
|
||||
src/utils/file_handler.py
|
||||
src/utils/logger.py
|
||||
src/utils/__init__.py
|
||||
src/utils/__pycache__/html_generator.cpython-312.pyc
|
||||
src/utils/__pycache__/logger.cpython-312.pyc
|
||||
src/utils/__pycache__/__init__.cpython-312.pyc
|
||||
src/__pycache__/main.cpython-312.pyc
|
||||
+2
-13
@@ -1116,20 +1116,10 @@ async def process_file_core(
|
||||
logger.info("FreeCAD验证已禁用(设置 ENABLE_FREECAD_VERIFICATION=true 启用)")
|
||||
verification_result = {"status": "disabled", "reason": "FreeCAD验证已禁用"}
|
||||
|
||||
# 9.8 LLM 增强分析(可选,不影响主流程)
|
||||
# 9.8 LLM 增强分析
|
||||
llm_report = None
|
||||
llm_parting = None
|
||||
if analysis_result:
|
||||
llm_report = await llm_service.generate_design_report(
|
||||
analysis_result, detailed_cavity_json
|
||||
)
|
||||
# 分型推荐:当前无候选方案时跳过
|
||||
candidate_schemes = detailed_cavity_json.get("candidate_schemes", [])
|
||||
if candidate_schemes:
|
||||
llm_parting = await llm_service.recommend_parting_direction(
|
||||
geometry_data, candidate_schemes, selected_material,
|
||||
cavity_count=cavity_count,
|
||||
)
|
||||
llm_report = await llm_service.generate_design_report(analysis_result, detailed_cavity_json)
|
||||
|
||||
# 10. 完成处理
|
||||
await storage_service.update_stp_file_status(db_session, stp_file_id, "completed")
|
||||
@@ -1146,7 +1136,6 @@ async def process_file_core(
|
||||
tasks[task_id]["html_file"] = f"/html/{Path(html_file_path).name}" # 只使用文件名
|
||||
tasks[task_id]["verification"] = verification_result # 添加验证结果
|
||||
tasks[task_id]["llm_report"] = llm_report
|
||||
tasks[task_id]["llm_parting_recommendation"] = llm_parting
|
||||
tasks[task_id]["status"] = ProcessingStatus.COMPLETED
|
||||
tasks[task_id]["completed_at"] = str(datetime.now())
|
||||
|
||||
|
||||
@@ -0,0 +1,905 @@
|
||||
from typing import Dict, List, Any, Tuple, Optional
|
||||
import math
|
||||
import numpy as np
|
||||
from OCC.Core.BRepOffsetAPI import BRepOffsetAPI_DraftAngle
|
||||
from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Cut, BRepAlgoAPI_Section, BRepAlgoAPI_Common
|
||||
from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeFace, BRepBuilderAPI_Transform
|
||||
from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox, BRepPrimAPI_MakeHalfSpace
|
||||
from OCC.Core.gp import gp_Pln, gp_Dir, gp_Pnt, gp_Trsf, gp_Ax2
|
||||
from OCC.Core.TopoDS import TopoDS_Face, topods
|
||||
from OCC.Core.BRep import BRep_Tool
|
||||
from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh
|
||||
from OCC.Core.GProp import GProp_GProps
|
||||
from OCC.Core.BRepGProp import brepgprop
|
||||
from OCC.Core.TopExp import TopExp_Explorer
|
||||
from OCC.Core.TopAbs import TopAbs_FACE, TopAbs_EDGE
|
||||
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface, BRepAdaptor_Curve
|
||||
from OCC.Core.Bnd import Bnd_Box
|
||||
from OCC.Core.BRepBndLib import brepbndlib
|
||||
from OCC.Core.TopLoc import TopLoc_Location
|
||||
|
||||
from models.schemas import create_mold_cavity_data, create_mold_key_info
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class BaseMoldGenerator:
|
||||
"""模具生成器基类 - 提供共用方法"""
|
||||
|
||||
def __init__(self, shrinkage_rate: float = 0.005, draft_angle: float = 2.0,
|
||||
material_density: float = 1.05):
|
||||
self.shrinkage_rate = shrinkage_rate
|
||||
self.draft_angle = draft_angle
|
||||
self.material_density = material_density
|
||||
|
||||
self.ai_parting_detector: Optional[Any] = None
|
||||
self.ai_draft_analyzer: Optional[Any] = None
|
||||
|
||||
def set_ai_model(self, parting_detector: Any = None, draft_analyzer: Any = None):
|
||||
self.ai_parting_detector = parting_detector
|
||||
self.ai_draft_analyzer = draft_analyzer
|
||||
logger.info("AI 模型接口已设置")
|
||||
|
||||
def _apply_shrinkage_compensation(self, shape: Any) -> Any:
|
||||
scale_factor = 1.0 + self.shrinkage_rate
|
||||
trsf = gp_Trsf()
|
||||
trsf.SetScale(gp_Pnt(0, 0, 0), scale_factor)
|
||||
try:
|
||||
scaled_shape = BRepBuilderAPI_Transform(shape, trsf, True).Shape()
|
||||
logger.info(f"收缩率补偿: {self.shrinkage_rate*100:.2f}%, 缩放因子: {scale_factor:.4f}")
|
||||
return scaled_shape
|
||||
except Exception as e:
|
||||
logger.warning(f"收缩率补偿失败: {e}")
|
||||
return shape
|
||||
|
||||
def _apply_draft_angles(self, shape: Any, parting_surface: Any) -> Any:
|
||||
try:
|
||||
draft_direction = self._get_draft_direction(parting_surface)
|
||||
if draft_direction is None:
|
||||
logger.warning("无法确定拔模方向,跳过拔模处理")
|
||||
return shape
|
||||
|
||||
draft_angle_rad = math.radians(self.draft_angle)
|
||||
draftable_faces = self._find_draftable_faces(shape, draft_direction)
|
||||
|
||||
if not draftable_faces:
|
||||
logger.info("未找到需要拔模的面,跳过拔模处理")
|
||||
return shape
|
||||
|
||||
logger.info(f"应用拔模角: {self.draft_angle}°, {len(draftable_faces)} 个面")
|
||||
|
||||
drafted_shape = self._execute_draft(shape, draftable_faces, draft_direction, draft_angle_rad)
|
||||
return drafted_shape
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"拔模角处理失败,返回原始形状: {e}")
|
||||
return shape
|
||||
|
||||
def _get_draft_direction(self, parting_surface: Any) -> Optional[gp_Dir]:
|
||||
try:
|
||||
surface = BRepAdaptor_Surface(parting_surface)
|
||||
if surface.GetType() == 0:
|
||||
return surface.Plane().Position().Direction()
|
||||
return gp_Dir(0, 0, 1)
|
||||
except Exception:
|
||||
return gp_Dir(0, 0, 1)
|
||||
|
||||
def _find_draftable_faces(self, shape: Any, draft_direction: gp_Dir) -> List[Any]:
|
||||
draftable = []
|
||||
explorer = TopExp_Explorer(shape, TopAbs_FACE)
|
||||
|
||||
while explorer.More():
|
||||
face = topods.Face(explorer.Current())
|
||||
normal = self._get_face_normal(face)
|
||||
|
||||
if normal is not None:
|
||||
dot = abs(normal.Dot(draft_direction))
|
||||
angle = math.degrees(math.acos(min(dot, 1.0)))
|
||||
if 5.0 < angle < 85.0:
|
||||
draftable.append(face)
|
||||
|
||||
explorer.Next()
|
||||
|
||||
return draftable
|
||||
|
||||
def _get_face_normal(self, face: Any) -> Optional[gp_Dir]:
|
||||
try:
|
||||
surface = BRepAdaptor_Surface(face)
|
||||
u = (surface.FirstUParameter() + surface.LastUParameter()) / 2
|
||||
v = (surface.FirstVParameter() + surface.LastVParameter()) / 2
|
||||
|
||||
if surface.GetType() == 0:
|
||||
return surface.Plane().Position().Direction()
|
||||
|
||||
from OCC.Core.BRepLProp import BRepLProp_SLProps
|
||||
props = BRepLProp_SLProps(surface, 1, 0.001)
|
||||
props.SetParameters(u, v)
|
||||
if props.IsNormalDefined():
|
||||
return props.Normal()
|
||||
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _execute_draft(self, shape: Any, faces: List[Any],
|
||||
draft_direction: gp_Dir, draft_angle_rad: float) -> Any:
|
||||
try:
|
||||
draft = BRepOffsetAPI_DraftAngle(shape)
|
||||
|
||||
for face in faces:
|
||||
try:
|
||||
normal = self._get_face_normal(face)
|
||||
if normal is None:
|
||||
continue
|
||||
|
||||
dot = normal.Dot(draft_direction)
|
||||
if dot > 0:
|
||||
face_dir = draft_direction
|
||||
else:
|
||||
face_dir = gp_Dir(-draft_direction.X(), -draft_direction.Y(), -draft_direction.Z())
|
||||
|
||||
draft.Add(face, face_dir, draft_angle_rad, True)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
draft.Build()
|
||||
|
||||
if draft.IsDone():
|
||||
logger.info(f"拔模角应用成功: {len(faces)} 个面, {self.draft_angle}°")
|
||||
return draft.Shape()
|
||||
else:
|
||||
logger.warning("BRepOffsetAPI_DraftAngle 构建失败,尝试逐面拔模")
|
||||
return self._draft_faces_sequentially(shape, faces, draft_direction, draft_angle_rad)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"拔模执行失败: {e}")
|
||||
return shape
|
||||
|
||||
def _draft_faces_sequentially(self, shape: Any, faces: List[Any],
|
||||
draft_direction: gp_Dir, draft_angle_rad: float) -> Any:
|
||||
current_shape = shape
|
||||
success_count = 0
|
||||
|
||||
for face in faces:
|
||||
try:
|
||||
draft = BRepOffsetAPI_DraftAngle(current_shape)
|
||||
normal = self._get_face_normal(face)
|
||||
if normal is None:
|
||||
continue
|
||||
|
||||
dot = normal.Dot(draft_direction)
|
||||
if dot > 0:
|
||||
face_dir = draft_direction
|
||||
else:
|
||||
face_dir = gp_Dir(-draft_direction.X(), -draft_direction.Y(), -draft_direction.Z())
|
||||
|
||||
draft.Add(face, face_dir, draft_angle_rad, True)
|
||||
draft.Build()
|
||||
|
||||
if draft.IsDone():
|
||||
current_shape = draft.Shape()
|
||||
success_count += 1
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if success_count > 0:
|
||||
logger.info(f"逐面拔模完成: {success_count}/{len(faces)} 个面成功")
|
||||
else:
|
||||
logger.warning("逐面拔模全部失败,返回原始形状")
|
||||
|
||||
return current_shape
|
||||
|
||||
def _analyze_product_geometry(self, shape: Any) -> Dict[str, Any]:
|
||||
try:
|
||||
props = GProp_GProps()
|
||||
brepgprop.VolumeProperties(shape, props)
|
||||
volume = props.Mass()
|
||||
|
||||
surface_props = GProp_GProps()
|
||||
brepgprop.SurfaceProperties(shape, surface_props)
|
||||
surface_area = surface_props.Mass()
|
||||
|
||||
center = props.CentreOfMass()
|
||||
|
||||
bbox = Bnd_Box()
|
||||
brepbndlib.Add(shape, bbox)
|
||||
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
|
||||
|
||||
inertia = props.MatrixOfInertia()
|
||||
|
||||
return {
|
||||
"volume": volume,
|
||||
"surface_area": surface_area,
|
||||
"center_of_mass": [float(center.X()), float(center.Y()), float(center.Z())],
|
||||
"bounding_box": {
|
||||
"min": [float(xmin), float(ymin), float(zmin)],
|
||||
"max": [float(xmax), float(ymax), float(zmax)],
|
||||
"center": [float((xmin+xmax)/2), float((ymin+ymax)/2), float((zmin+zmax)/2)],
|
||||
"dimensions": [float(xmax-xmin), float(ymax-ymin), float(zmax-zmin)]
|
||||
},
|
||||
"inertia_matrix": self._get_inertia_matrix(props)
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"产品几何分析失败: {e}")
|
||||
raise
|
||||
|
||||
def _split_cavity_core(self, shape: Any, parting_surface: Any, margin: int = 20) -> Tuple[Any, Any]:
|
||||
"""
|
||||
用分型面将模具块切分为A板(上模/型腔)和B板(下模/型芯),
|
||||
然后从每个板中减去产品形状的对应部分。
|
||||
|
||||
流程:
|
||||
1. 创建完整模具块(产品包围盒 + 全方向余量)
|
||||
2. 用分型面将模具块切分为 A板 和 B板
|
||||
3. A板 - 产品 = 型腔(凹模)
|
||||
4. B板 - 产品 = 型芯(凸模)
|
||||
"""
|
||||
try:
|
||||
bbox = Bnd_Box()
|
||||
brepbndlib.Add(shape, bbox)
|
||||
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
|
||||
|
||||
mold_xmin = xmin - margin
|
||||
mold_ymin = ymin - margin
|
||||
mold_zmin = zmin - margin
|
||||
mold_xmax = xmax + margin
|
||||
mold_ymax = ymax + margin
|
||||
mold_zmax = zmax + margin
|
||||
|
||||
mold_block = BRepPrimAPI_MakeBox(
|
||||
gp_Pnt(mold_xmin, mold_ymin, mold_zmin),
|
||||
gp_Pnt(mold_xmax, mold_ymax, mold_zmax)
|
||||
).Shape()
|
||||
|
||||
parting_plane = self._get_parting_plane(parting_surface, shape)
|
||||
if parting_plane is None:
|
||||
logger.warning("无法提取分型面平面,使用回退方案")
|
||||
center_z = (zmin + zmax) / 2
|
||||
parting_plane = gp_Pln(gp_Pnt(0, 0, center_z), gp_Dir(0, 0, 1))
|
||||
|
||||
a_plate, b_plate = self._split_mold_block_by_plane(mold_block, parting_plane)
|
||||
|
||||
if a_plate is not None and b_plate is not None:
|
||||
cavity = self._subtract_product_from_plate(a_plate, shape, "型腔(A板)")
|
||||
core = self._subtract_product_from_plate(b_plate, shape, "型芯(B板)")
|
||||
|
||||
if cavity is not None and core is not None:
|
||||
logger.info("型腔/型芯分离完成(分型面切分+布尔减)")
|
||||
return cavity, core
|
||||
elif cavity is not None:
|
||||
logger.warning("型芯生成失败,使用产品形状")
|
||||
return cavity, shape
|
||||
elif core is not None:
|
||||
logger.warning("型腔生成失败,使用模具块")
|
||||
return mold_block, core
|
||||
|
||||
logger.warning("A/B板切分不完全,回退到原方案")
|
||||
return self._split_cavity_core_fallback(shape, mold_block)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"型腔分离失败: {e}")
|
||||
return self._split_cavity_core_fallback(shape, None)
|
||||
|
||||
def _get_parting_plane(self, parting_surface: Any, shape: Any) -> Optional[gp_Pln]:
|
||||
"""从分型面提取平面方程"""
|
||||
try:
|
||||
surface = BRepAdaptor_Surface(parting_surface)
|
||||
if surface.GetType() == 0:
|
||||
return surface.Plane()
|
||||
|
||||
bbox = Bnd_Box()
|
||||
brepbndlib.Add(shape, bbox)
|
||||
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
|
||||
center_z = (zmin + zmax) / 2
|
||||
return gp_Pln(gp_Pnt(0, 0, center_z), gp_Dir(0, 0, 1))
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"分型面平面提取失败: {e}")
|
||||
return None
|
||||
|
||||
def _split_mold_block_by_plane(self, mold_block: Any,
|
||||
parting_plane: gp_Pln) -> Tuple[Any, Any]:
|
||||
"""
|
||||
用分型面将模具块切分为A板(上模)和B板(下模)
|
||||
|
||||
方法:使用半空间体与模具块的布尔交集运算
|
||||
- A板 = 模具块 ∩ 分型面上方半空间
|
||||
- B板 = 模具块 ∩ 分型面下方半空间
|
||||
"""
|
||||
try:
|
||||
plane_origin = parting_plane.Location()
|
||||
plane_normal = parting_plane.Axis().Direction()
|
||||
|
||||
ref_point_above = gp_Pnt(
|
||||
plane_origin.X() + plane_normal.X() * 10,
|
||||
plane_origin.Y() + plane_normal.Y() * 10,
|
||||
plane_origin.Z() + plane_normal.Z() * 10
|
||||
)
|
||||
ref_point_below = gp_Pnt(
|
||||
plane_origin.X() - plane_normal.X() * 10,
|
||||
plane_origin.Y() - plane_normal.Y() * 10,
|
||||
plane_origin.Z() - plane_normal.Z() * 10
|
||||
)
|
||||
|
||||
half_space_above = BRepPrimAPI_MakeHalfSpace(
|
||||
BRepBuilderAPI_MakeFace(parting_plane).Face(),
|
||||
ref_point_above
|
||||
).Shape()
|
||||
|
||||
half_space_below = BRepPrimAPI_MakeHalfSpace(
|
||||
BRepBuilderAPI_MakeFace(parting_plane).Face(),
|
||||
ref_point_below
|
||||
).Shape()
|
||||
|
||||
a_plate_op = BRepAlgoAPI_Common(mold_block, half_space_above)
|
||||
a_plate = None
|
||||
if a_plate_op.IsDone():
|
||||
a_plate = a_plate_op.Shape()
|
||||
logger.info("A板(上模)切分成功")
|
||||
else:
|
||||
logger.warning("A板切分失败")
|
||||
|
||||
b_plate_op = BRepAlgoAPI_Common(mold_block, half_space_below)
|
||||
b_plate = None
|
||||
if b_plate_op.IsDone():
|
||||
b_plate = b_plate_op.Shape()
|
||||
logger.info("B板(下模)切分成功")
|
||||
else:
|
||||
logger.warning("B板切分失败")
|
||||
|
||||
return a_plate, b_plate
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"A/B板分离失败: {e}")
|
||||
return None, None
|
||||
|
||||
def _subtract_product_from_plate(self, plate: Any, product: Any,
|
||||
plate_name: str) -> Any:
|
||||
"""从模板中减去产品形状,生成型腔或型芯"""
|
||||
try:
|
||||
cut_op = BRepAlgoAPI_Cut(plate, product)
|
||||
if cut_op.IsDone():
|
||||
result = cut_op.Shape()
|
||||
logger.info(f"{plate_name}减去产品成功")
|
||||
return result
|
||||
else:
|
||||
logger.warning(f"{plate_name}布尔减运算失败")
|
||||
return plate
|
||||
except Exception as e:
|
||||
logger.warning(f"{plate_name}减产品失败: {e}")
|
||||
return plate
|
||||
|
||||
def _split_cavity_core_fallback(self, shape: Any,
|
||||
mold_block: Optional[Any] = None) -> Tuple[Any, Any]:
|
||||
"""
|
||||
分模回退方案:用边界框中心面作为分型面切分模具块。
|
||||
"""
|
||||
logger.warning("使用分模回退方案")
|
||||
|
||||
try:
|
||||
bbox = Bnd_Box()
|
||||
brepbndlib.Add(shape, bbox)
|
||||
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
|
||||
|
||||
margin = 20
|
||||
if mold_block is None:
|
||||
mold_block = BRepPrimAPI_MakeBox(
|
||||
gp_Pnt(xmin - margin, ymin - margin, zmin - margin),
|
||||
gp_Pnt(xmax + margin, ymax + margin, zmax + margin)
|
||||
).Shape()
|
||||
|
||||
center_z = (zmin + zmax) / 2
|
||||
parting_plane = gp_Pln(gp_Pnt(0, 0, center_z), gp_Dir(0, 0, 1))
|
||||
a_plate, b_plate = self._split_mold_block_by_plane(mold_block, parting_plane)
|
||||
|
||||
if a_plate is not None and b_plate is not None:
|
||||
cavity = self._subtract_product_from_plate(a_plate, shape, "型腔(回退)")
|
||||
core = self._subtract_product_from_plate(b_plate, shape, "型芯(回退)")
|
||||
if cavity is not None and core is not None:
|
||||
logger.info("回退方案型腔/型芯分离完成")
|
||||
return cavity, core
|
||||
|
||||
cavity = self._subtract_product_from_plate(mold_block, shape, "型腔(兜底)")
|
||||
return cavity or mold_block, shape
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"分模回退方案失败: {e}")
|
||||
try:
|
||||
bbox = Bnd_Box()
|
||||
brepbndlib.Add(shape, bbox)
|
||||
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
|
||||
margin = 20
|
||||
cavity_block = BRepPrimAPI_MakeBox(
|
||||
gp_Pnt(xmin - margin, ymin - margin, zmin - margin),
|
||||
gp_Pnt(xmax + margin, ymax + margin, zmax + margin)
|
||||
).Shape()
|
||||
cavity = self._subtract_product_from_plate(cavity_block, shape, "型腔(兜底)")
|
||||
return cavity or cavity_block, shape
|
||||
except Exception:
|
||||
return shape, shape
|
||||
|
||||
def detect_insert_regions(self, shape: Any, analysis: Dict,
|
||||
depth_threshold: float = 30.0,
|
||||
aspect_threshold: float = 3.0) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
检测需要独立镶件的区域
|
||||
|
||||
镶件判定条件:
|
||||
1. 深腔区域(深度超过阈值)
|
||||
2. 细长特征(长径比超过阈值)
|
||||
3. 易磨损区域(尖锐角落、薄壁)
|
||||
4. 精密特征(高精度要求的局部区域)
|
||||
|
||||
Args:
|
||||
shape: 产品形状
|
||||
analysis: 几何分析结果
|
||||
depth_threshold: 深腔深度阈值 mm
|
||||
aspect_threshold: 长径比阈值
|
||||
|
||||
Returns:
|
||||
镶件区域列表
|
||||
"""
|
||||
inserts = []
|
||||
|
||||
try:
|
||||
bbox = analysis.get("bounding_box", {})
|
||||
dims = bbox.get("dimensions", [0, 0, 0])
|
||||
center = bbox.get("center", [0, 0, 0])
|
||||
|
||||
if dims[2] > depth_threshold:
|
||||
inserts.append({
|
||||
"type": "deep_cavity_insert",
|
||||
"location": center,
|
||||
"depth": dims[2],
|
||||
"reason": f"型腔深度 {dims[2]:.1f}mm 超过阈值 {depth_threshold}mm",
|
||||
"insert_type": "core_pin",
|
||||
"priority": "high"
|
||||
})
|
||||
|
||||
explorer = TopExp_Explorer(shape, TopAbs_FACE)
|
||||
face_idx = 0
|
||||
|
||||
while explorer.More():
|
||||
face = topods.Face(explorer.Current())
|
||||
face_idx += 1
|
||||
|
||||
try:
|
||||
surface = BRepAdaptor_Surface(face)
|
||||
|
||||
face_props = GProp_GProps()
|
||||
brepgprop.SurfaceProperties(face, face_props)
|
||||
area = face_props.Mass()
|
||||
|
||||
if area < 1.0 and area > 0.001:
|
||||
bbox_face = Bnd_Box()
|
||||
brepbndlib.Add(face, bbox_face)
|
||||
try:
|
||||
fxmin, fymin, fzmin, fxmax, fymax, fzmax = bbox_face.Get()
|
||||
f_dims = [fxmax - fxmin, fymax - fymin, fzmax - fzmin]
|
||||
max_dim = max(f_dims)
|
||||
min_dim = min(f_dims)
|
||||
|
||||
if min_dim > 0.01 and max_dim / min_dim > aspect_threshold:
|
||||
face_center = [
|
||||
float((fxmin + fxmax) / 2),
|
||||
float((fymin + fymax) / 2),
|
||||
float((fzmin + fzmax) / 2)
|
||||
]
|
||||
|
||||
inserts.append({
|
||||
"type": "slender_feature_insert",
|
||||
"location": face_center,
|
||||
"aspect_ratio": max_dim / min_dim,
|
||||
"reason": f"细长特征,长径比 {max_dim/min_dim:.1f}",
|
||||
"insert_type": "core_pin",
|
||||
"priority": "medium",
|
||||
"face_index": face_idx
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if surface.GetType() == 1:
|
||||
radius = surface.Cylinder().Radius()
|
||||
if radius < 3.0 and radius > 0.1:
|
||||
cyl_axis = surface.Cylinder().Position().Axis()
|
||||
cyl_loc = cyl_axis.Location()
|
||||
|
||||
inserts.append({
|
||||
"type": "small_hole_insert",
|
||||
"location": [float(cyl_loc.X()), float(cyl_loc.Y()), float(cyl_loc.Z())],
|
||||
"radius": float(radius),
|
||||
"reason": f"小孔特征,半径 {radius:.2f}mm",
|
||||
"insert_type": "core_pin",
|
||||
"priority": "high",
|
||||
"face_index": face_idx
|
||||
})
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
explorer.Next()
|
||||
|
||||
if not inserts:
|
||||
logger.info("未检测到需要镶件的区域")
|
||||
else:
|
||||
logger.info(f"检测到 {len(inserts)} 个镶件区域")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"镶件检测失败: {e}")
|
||||
|
||||
return inserts
|
||||
|
||||
def _extract_shape_geometry(self, shape: Any, shape_type: str) -> Dict[str, Any]:
|
||||
try:
|
||||
mesh = BRepMesh_IncrementalMesh(shape, 0.1)
|
||||
mesh.Perform()
|
||||
|
||||
vertices = []
|
||||
faces = []
|
||||
|
||||
explorer = TopExp_Explorer(shape, TopAbs_FACE)
|
||||
vertex_index = 0
|
||||
|
||||
while explorer.More():
|
||||
face = explorer.Current()
|
||||
location = TopLoc_Location()
|
||||
triangulation = BRep_Tool.Triangulation(face, location)
|
||||
|
||||
if triangulation:
|
||||
nb_nodes = triangulation.NbNodes()
|
||||
for i in range(1, nb_nodes + 1):
|
||||
node = triangulation.Node(i)
|
||||
transformed = node.Transformed(location.Transformation())
|
||||
vertices.extend([
|
||||
float(transformed.X()),
|
||||
float(transformed.Y()),
|
||||
float(transformed.Z())
|
||||
])
|
||||
|
||||
nb_triangles = triangulation.NbTriangles()
|
||||
for i in range(1, nb_triangles + 1):
|
||||
triangle = triangulation.Triangle(i)
|
||||
idx1 = triangle.Value(1) + vertex_index - 1
|
||||
idx2 = triangle.Value(2) + vertex_index - 1
|
||||
idx3 = triangle.Value(3) + vertex_index - 1
|
||||
faces.extend([int(idx1), int(idx2), int(idx3)])
|
||||
|
||||
vertex_index += nb_nodes
|
||||
|
||||
explorer.Next()
|
||||
|
||||
vertex_count = len(vertices) // 3
|
||||
face_count = len(faces) // 3
|
||||
|
||||
return {
|
||||
"type": shape_type,
|
||||
"vertices": vertices,
|
||||
"faces": faces,
|
||||
"vertex_count": vertex_count,
|
||||
"face_count": face_count,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"{shape_type}几何提取失败: {e}")
|
||||
return {
|
||||
"type": shape_type,
|
||||
"vertices": [],
|
||||
"faces": [],
|
||||
"vertex_count": 0,
|
||||
"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
|
||||
return f"{weight_g:.2f} g"
|
||||
|
||||
def _assess_warpage_risk(self, analysis: Dict) -> str:
|
||||
bbox = analysis.get("bounding_box", {}).get("dimensions", [1, 1, 1])
|
||||
aspect_ratio = max(bbox) / min(bbox) if min(bbox) > 0 else 1
|
||||
|
||||
if aspect_ratio > 5:
|
||||
return "高 - 建议增加加强筋"
|
||||
elif aspect_ratio > 3:
|
||||
return "中 - 需优化冷却"
|
||||
else:
|
||||
return "低"
|
||||
|
||||
def _get_inertia_matrix(self, props: GProp_GProps) -> List[List[float]]:
|
||||
inertia = props.MatrixOfInertia()
|
||||
return [
|
||||
[inertia.Value(1, 1), inertia.Value(1, 2), inertia.Value(1, 3)],
|
||||
[inertia.Value(2, 1), inertia.Value(2, 2), inertia.Value(2, 3)],
|
||||
[inertia.Value(3, 1), inertia.Value(3, 2), inertia.Value(3, 3)]
|
||||
]
|
||||
|
||||
def _calculate_parting_line_length(self, parting_line: List) -> float:
|
||||
if not parting_line or len(parting_line) < 2:
|
||||
return 0.0
|
||||
|
||||
total_length = 0.0
|
||||
for i in range(1, len(parting_line)):
|
||||
p1 = np.array(parting_line[i-1])
|
||||
p2 = np.array(parting_line[i])
|
||||
segment_length = np.linalg.norm(p2 - p1)
|
||||
total_length += segment_length
|
||||
|
||||
return total_length
|
||||
|
||||
def _calculate_parting_line(self, shape: Any, parting_surface: Any) -> List[List[float]]:
|
||||
try:
|
||||
section = BRepAlgoAPI_Section(shape, parting_surface)
|
||||
section.Build()
|
||||
|
||||
if not section.IsDone():
|
||||
logger.warning("截面运算未完成,使用简化分型线")
|
||||
return self._simple_parting_line(shape)
|
||||
|
||||
edges = []
|
||||
explorer = TopExp_Explorer(section.Shape(), TopAbs_EDGE)
|
||||
|
||||
while explorer.More():
|
||||
edge = explorer.Current()
|
||||
|
||||
curve = BRepAdaptor_Curve(edge)
|
||||
first_param = curve.FirstParameter()
|
||||
last_param = curve.LastParameter()
|
||||
|
||||
num_points = max(10, int((last_param - first_param) / 0.5))
|
||||
step = (last_param - first_param) / num_points
|
||||
|
||||
for i in range(num_points + 1):
|
||||
param = first_param + i * step
|
||||
point = curve.Value(param)
|
||||
edges.append([point.X(), point.Y(), point.Z()])
|
||||
|
||||
explorer.Next()
|
||||
|
||||
if not edges:
|
||||
logger.warning("未找到交线,使用简化分型线")
|
||||
return self._simple_parting_line(shape)
|
||||
|
||||
logger.info(f"计算得到 {len(edges)} 个分型线点")
|
||||
return edges
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"分型线计算失败: {e}")
|
||||
return self._simple_parting_line(shape)
|
||||
|
||||
def _simple_parting_line(self, shape: Any) -> List[List[float]]:
|
||||
try:
|
||||
bbox = Bnd_Box()
|
||||
brepbndlib.Add(shape, bbox)
|
||||
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
|
||||
center_z = (zmin + zmax) / 2
|
||||
|
||||
return [
|
||||
[xmin, ymin, center_z],
|
||||
[xmax, ymin, center_z],
|
||||
[xmax, ymax, center_z],
|
||||
[xmin, ymax, center_z],
|
||||
[xmin, ymin, center_z]
|
||||
]
|
||||
except Exception:
|
||||
return [[-50, -50, 0], [50, -50, 0], [50, 50, 0], [-50, 50, 0], [-50, -50, 0]]
|
||||
|
||||
def extend_parting_surface(self, parting_surface: Any, shape: Any,
|
||||
extension: float = 30.0) -> Any:
|
||||
"""
|
||||
将分型面延伸到模具块边界
|
||||
|
||||
分型面通常只覆盖产品轮廓,需要延伸到模具块边缘
|
||||
才能正确分离A板和B板
|
||||
|
||||
Args:
|
||||
parting_surface: 原始分型面
|
||||
shape: 产品形状
|
||||
extension: 延伸距离 mm
|
||||
|
||||
Returns:
|
||||
延伸后的分型面
|
||||
"""
|
||||
try:
|
||||
bbox = Bnd_Box()
|
||||
brepbndlib.Add(shape, bbox)
|
||||
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
|
||||
|
||||
surface = BRepAdaptor_Surface(parting_surface)
|
||||
if surface.GetType() != 0:
|
||||
logger.info("分型面非平面,延伸操作跳过")
|
||||
return parting_surface
|
||||
|
||||
plane = surface.Plane()
|
||||
origin = plane.Location()
|
||||
normal = plane.Axis().Direction()
|
||||
|
||||
extended_xmin = xmin - extension
|
||||
extended_ymin = ymin - extension
|
||||
extended_xmax = xmax + extension
|
||||
extended_ymax = ymax + extension
|
||||
|
||||
extended_plane = gp_Pln(origin, normal)
|
||||
extended_surface = BRepBuilderAPI_MakeFace(
|
||||
extended_plane,
|
||||
extended_xmin, extended_xmax,
|
||||
extended_ymin, extended_ymax
|
||||
).Face()
|
||||
|
||||
logger.info(f"分型面延伸完成: 延伸距离={extension}mm")
|
||||
return extended_surface
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"分型面延伸失败: {e}")
|
||||
return parting_surface
|
||||
|
||||
def optimize_parting_line(self, parting_line: List[List[float]],
|
||||
smooth_window: int = 5,
|
||||
min_segment_length: float = 0.5,
|
||||
angle_threshold: float = 150.0) -> List[List[float]]:
|
||||
"""
|
||||
优化分型线
|
||||
|
||||
优化内容:
|
||||
1. 平滑处理 - 消除噪声点
|
||||
2. 去除短线段 - 合并过短的线段
|
||||
3. 尖角处理 - 在尖角处添加过渡圆弧
|
||||
4. 点密度均匀化 - 重采样使点间距均匀
|
||||
|
||||
Args:
|
||||
parting_line: 原始分型线点列表
|
||||
smooth_window: 平滑窗口大小
|
||||
min_segment_length: 最小线段长度
|
||||
angle_threshold: 尖角判定角度(度)
|
||||
|
||||
Returns:
|
||||
优化后的分型线
|
||||
"""
|
||||
if len(parting_line) < 3:
|
||||
return parting_line
|
||||
|
||||
try:
|
||||
smoothed = self._smooth_parting_line(parting_line, smooth_window)
|
||||
|
||||
filtered = self._filter_short_segments(smoothed, min_segment_length)
|
||||
|
||||
optimized = self._round_sharp_corners(filtered, angle_threshold)
|
||||
|
||||
resampled = self._resample_parting_line(optimized, target_spacing=2.0)
|
||||
|
||||
logger.info(f"分型线优化: {len(parting_line)} → {len(resampled)} 点")
|
||||
return resampled
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"分型线优化失败: {e}")
|
||||
return parting_line
|
||||
|
||||
def _smooth_parting_line(self, points: List[List[float]],
|
||||
window: int = 5) -> List[List[float]]:
|
||||
"""移动平均平滑"""
|
||||
if len(points) < window:
|
||||
return points
|
||||
|
||||
arr = np.array(points, dtype=np.float64)
|
||||
smoothed = []
|
||||
|
||||
for i in range(len(arr)):
|
||||
start = max(0, i - window // 2)
|
||||
end = min(len(arr), i + window // 2 + 1)
|
||||
avg = np.mean(arr[start:end], axis=0)
|
||||
smoothed.append(avg.tolist())
|
||||
|
||||
return smoothed
|
||||
|
||||
def _filter_short_segments(self, points: List[List[float]],
|
||||
min_length: float) -> List[List[float]]:
|
||||
"""去除过短线段"""
|
||||
if not points:
|
||||
return points
|
||||
|
||||
filtered = [points[0]]
|
||||
for i in range(1, len(points)):
|
||||
dist = np.linalg.norm(np.array(points[i]) - np.array(filtered[-1]))
|
||||
if dist >= min_length:
|
||||
filtered.append(points[i])
|
||||
|
||||
return filtered
|
||||
|
||||
def _round_sharp_corners(self, points: List[List[float]],
|
||||
angle_threshold: float) -> List[List[float]]:
|
||||
"""在尖角处添加过渡点"""
|
||||
if len(points) < 3:
|
||||
return points
|
||||
|
||||
result = [points[0]]
|
||||
|
||||
for i in range(1, len(points) - 1):
|
||||
v1 = np.array(points[i]) - np.array(points[i - 1])
|
||||
v2 = np.array(points[i + 1]) - np.array(points[i])
|
||||
|
||||
len1 = np.linalg.norm(v1)
|
||||
len2 = np.linalg.norm(v2)
|
||||
|
||||
if len1 > 0.001 and len2 > 0.001:
|
||||
cos_angle = np.clip(np.dot(v1, v2) / (len1 * len2), -1, 1)
|
||||
angle = math.degrees(math.acos(cos_angle))
|
||||
|
||||
if angle < angle_threshold:
|
||||
mid1 = (np.array(points[i - 1]) + np.array(points[i])) / 2
|
||||
mid2 = (np.array(points[i]) + np.array(points[i + 1])) / 2
|
||||
result.append(mid1.tolist())
|
||||
result.append(mid2.tolist())
|
||||
else:
|
||||
result.append(points[i])
|
||||
else:
|
||||
result.append(points[i])
|
||||
|
||||
result.append(points[-1])
|
||||
return result
|
||||
|
||||
def _resample_parting_line(self, points: List[List[float]],
|
||||
target_spacing: float) -> List[List[float]]:
|
||||
"""重采样使点间距均匀"""
|
||||
if len(points) < 2:
|
||||
return points
|
||||
|
||||
arr = np.array(points, dtype=np.float64)
|
||||
|
||||
cumulative_dist = [0.0]
|
||||
for i in range(1, len(arr)):
|
||||
dist = np.linalg.norm(arr[i] - arr[i - 1])
|
||||
cumulative_dist.append(cumulative_dist[-1] + dist)
|
||||
|
||||
total_length = cumulative_dist[-1]
|
||||
if total_length < target_spacing:
|
||||
return points
|
||||
|
||||
num_points = max(3, int(total_length / target_spacing))
|
||||
new_distances = np.linspace(0, total_length, num_points)
|
||||
|
||||
resampled = []
|
||||
for d in new_distances:
|
||||
idx = np.searchsorted(cumulative_dist, d) - 1
|
||||
idx = max(0, min(idx, len(arr) - 2))
|
||||
|
||||
seg_start = cumulative_dist[idx]
|
||||
seg_end = cumulative_dist[idx + 1]
|
||||
seg_length = seg_end - seg_start
|
||||
|
||||
if seg_length > 0:
|
||||
t = (d - seg_start) / seg_length
|
||||
else:
|
||||
t = 0
|
||||
|
||||
point = arr[idx] + t * (arr[idx + 1] - arr[idx])
|
||||
resampled.append(point.tolist())
|
||||
|
||||
return resampled
|
||||
@@ -0,0 +1,256 @@
|
||||
import asyncio
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from sqlalchemy import text
|
||||
|
||||
project_root = Path(__file__).parent.parent.parent
|
||||
src_root = Path(__file__).parent.parent
|
||||
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 User, Role, Permission, UserRole, RolePermission
|
||||
from services.auth_service import get_password_hash
|
||||
from config.settings import settings
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
DEFAULT_PERMISSIONS = [
|
||||
{"code": "view_dashboard", "name": "查看仪表盘", "module": "dashboard"},
|
||||
{"code": "view_moldinsight", "name": "使用模具分析", "module": "moldinsight"},
|
||||
{"code": "upload_file", "name": "上传文件", "module": "moldinsight"},
|
||||
{"code": "view_history", "name": "查看历史记录", "module": "moldinsight"},
|
||||
{"code": "view_inventory", "name": "查看库存", "module": "inventory"},
|
||||
{"code": "manage_inventory", "name": "管理库存", "module": "inventory"},
|
||||
{"code": "view_products", "name": "查看产品", "module": "inventory"},
|
||||
{"code": "manage_products", "name": "管理产品", "module": "inventory"},
|
||||
{"code": "view_suppliers", "name": "查看供应商", "module": "inventory"},
|
||||
{"code": "manage_suppliers", "name": "管理供应商", "module": "inventory"},
|
||||
{"code": "view_customers", "name": "查看客户", "module": "inventory"},
|
||||
{"code": "manage_customers", "name": "管理客户", "module": "inventory"},
|
||||
{"code": "view_finance", "name": "查看财务", "module": "finance"},
|
||||
{"code": "manage_receipts", "name": "管理收款", "module": "finance"},
|
||||
{"code": "manage_payments", "name": "管理付款", "module": "finance"},
|
||||
{"code": "void_finance_transaction", "name": "作废财务单据", "module": "finance"},
|
||||
{"code": "view_users", "name": "查看用户", "module": "admin"},
|
||||
{"code": "manage_users", "name": "管理用户", "module": "admin"},
|
||||
{"code": "manage_roles", "name": "管理角色", "module": "admin"},
|
||||
]
|
||||
|
||||
DEFAULT_ROLES = [
|
||||
{"code": "admin", "name": "管理员", "description": "系统管理员,拥有所有权限", "is_system": True, "permissions": ["view_dashboard", "view_moldinsight", "upload_file", "view_history", "view_inventory", "manage_inventory", "view_products", "manage_products", "view_suppliers", "manage_suppliers", "view_customers", "manage_customers", "view_finance", "manage_receipts", "manage_payments", "void_finance_transaction", "view_users", "manage_users", "manage_roles"]},
|
||||
{"code": "user", "name": "普通用户", "description": "普通用户,可使用模具分析和查看库存", "is_system": False, "permissions": ["view_dashboard", "view_moldinsight", "upload_file", "view_history", "view_inventory", "view_products", "view_suppliers", "view_customers", "view_finance", "manage_receipts", "manage_payments"]},
|
||||
{"code": "viewer", "name": "只读用户", "description": "只读用户,只能查看数据", "is_system": False, "permissions": ["view_dashboard", "view_moldinsight", "view_history", "view_inventory", "view_products", "view_suppliers", "view_customers", "view_finance"]},
|
||||
]
|
||||
|
||||
|
||||
async def init_permissions(session):
|
||||
"""初始化权限"""
|
||||
result = await session.execute(select(Permission))
|
||||
existing_perms = result.scalars().all()
|
||||
|
||||
if existing_perms:
|
||||
logger.info("权限已初始化")
|
||||
return
|
||||
|
||||
perm_map = {}
|
||||
for perm_data in DEFAULT_PERMISSIONS:
|
||||
perm = Permission(**perm_data)
|
||||
session.add(perm)
|
||||
await session.flush()
|
||||
perm_map[perm.code] = perm.id
|
||||
|
||||
logger.info(f"创建了 {len(DEFAULT_PERMISSIONS)} 个权限")
|
||||
return perm_map
|
||||
|
||||
|
||||
async def init_roles(session, perm_map):
|
||||
"""初始化角色"""
|
||||
result = await session.execute(select(Role))
|
||||
existing_roles = result.scalars().all()
|
||||
|
||||
if existing_roles:
|
||||
logger.info("角色已初始化")
|
||||
return
|
||||
|
||||
for role_data in DEFAULT_ROLES:
|
||||
perm_ids = [perm_map[code] for code in role_data.pop("permissions")]
|
||||
role = Role(**role_data)
|
||||
session.add(role)
|
||||
await session.flush()
|
||||
|
||||
for perm_id in perm_ids:
|
||||
rp = RolePermission(role_id=role.id, permission_id=perm_id)
|
||||
session.add(rp)
|
||||
|
||||
logger.info(f"创建了 {len(DEFAULT_ROLES)} 个角色")
|
||||
|
||||
|
||||
async def create_admin_user(session):
|
||||
"""创建默认管理员"""
|
||||
result = await session.execute(select(User).where(User.username == settings.ADMIN_USERNAME))
|
||||
existing_admin = result.scalar_one_or_none()
|
||||
|
||||
if existing_admin:
|
||||
logger.info("管理员账户已存在")
|
||||
return
|
||||
|
||||
admin = User(
|
||||
username=settings.ADMIN_USERNAME,
|
||||
email=settings.ADMIN_EMAIL,
|
||||
hashed_password=get_password_hash(settings.ADMIN_PASSWORD),
|
||||
full_name=settings.ADMIN_FULL_NAME,
|
||||
is_active=True
|
||||
)
|
||||
session.add(admin)
|
||||
await session.flush()
|
||||
|
||||
result = await session.execute(select(Role).where(Role.code == "admin"))
|
||||
admin_role = result.scalar_one_or_none()
|
||||
|
||||
if admin_role:
|
||||
user_role = UserRole(user_id=admin.id, role_id=admin_role.id)
|
||||
session.add(user_role)
|
||||
|
||||
await session.commit()
|
||||
logger.info(f"创建了管理员账户: {settings.ADMIN_USERNAME}")
|
||||
|
||||
|
||||
async def init_database(keep_connected: bool = True):
|
||||
"""初始化数据库"""
|
||||
try:
|
||||
await db_manager.connect()
|
||||
await db_manager.create_tables()
|
||||
await ensure_schema_updates()
|
||||
|
||||
async with db_manager.session() as session:
|
||||
perm_map = await init_permissions(session)
|
||||
if perm_map is None:
|
||||
# Permissions already existed, fetch them from database
|
||||
result = await session.execute(select(Permission))
|
||||
perms = result.scalars().all()
|
||||
perm_map = {perm.code: perm.id for perm in perms}
|
||||
await init_roles(session, perm_map)
|
||||
await create_admin_user(session)
|
||||
|
||||
|
||||
logger.info("数据库初始化完成")
|
||||
print("=" * 60)
|
||||
print("数据库初始化成功!")
|
||||
print("=" * 60)
|
||||
print(f"管理员用户名: {settings.ADMIN_USERNAME}")
|
||||
print(f"管理员邮箱: {settings.ADMIN_EMAIL}")
|
||||
print("=" * 60)
|
||||
print("可以在 .env 文件中修改管理员配置:")
|
||||
print(" ADMIN_USERNAME")
|
||||
print(" ADMIN_EMAIL")
|
||||
print(" ADMIN_FULL_NAME")
|
||||
print("=" * 60)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"数据库初始化失败: {e}")
|
||||
print(f"数据库初始化失败: {e}")
|
||||
return False
|
||||
finally:
|
||||
if not keep_connected:
|
||||
await db_manager.disconnect()
|
||||
|
||||
|
||||
async def ensure_schema_updates():
|
||||
async with db_manager.engine.begin() as conn:
|
||||
await conn.execute(text("ALTER TABLE products ADD COLUMN IF NOT EXISTS item_type VARCHAR(20) DEFAULT 'finished'"))
|
||||
await conn.execute(text("UPDATE products SET item_type = 'finished' WHERE item_type IS NULL"))
|
||||
await conn.execute(text("ALTER TABLE sales_orders ADD COLUMN IF NOT EXISTS production_status VARCHAR(20) DEFAULT 'not_started'"))
|
||||
await conn.execute(text("ALTER TABLE sales_orders ADD COLUMN IF NOT EXISTS production_no VARCHAR(50)"))
|
||||
await conn.execute(text("ALTER TABLE sales_orders ADD COLUMN IF NOT EXISTS planned_material_cost DOUBLE PRECISION DEFAULT 0"))
|
||||
await conn.execute(text("ALTER TABLE sales_orders ADD COLUMN IF NOT EXISTS actual_material_cost DOUBLE PRECISION DEFAULT 0"))
|
||||
await conn.execute(text("ALTER TABLE purchase_orders ADD COLUMN IF NOT EXISTS received_date TIMESTAMP WITHOUT TIME ZONE"))
|
||||
await conn.execute(text("ALTER TABLE purchase_orders ADD COLUMN IF NOT EXISTS paid_date TIMESTAMP WITHOUT TIME ZONE"))
|
||||
await conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS product_materials (
|
||||
id SERIAL PRIMARY KEY,
|
||||
finished_product_id INTEGER NOT NULL REFERENCES products(id),
|
||||
material_product_id INTEGER NOT NULL REFERENCES products(id),
|
||||
quantity DOUBLE PRECISION NOT NULL,
|
||||
loss_rate DOUBLE PRECISION DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
)
|
||||
"""))
|
||||
await conn.execute(text("""
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_product_material_unique
|
||||
ON product_materials (finished_product_id, material_product_id)
|
||||
"""))
|
||||
await conn.execute(text("ALTER TABLE mold_cavity_data ADD COLUMN IF NOT EXISTS best_scheme_id VARCHAR(64)"))
|
||||
await conn.execute(text("ALTER TABLE mold_cavity_data ADD COLUMN IF NOT EXISTS confidence_score DOUBLE PRECISION"))
|
||||
await conn.execute(text("ALTER TABLE mold_cavity_data ADD COLUMN IF NOT EXISTS is_fallback BOOLEAN"))
|
||||
await conn.execute(text("ALTER TABLE mold_cavity_data ADD COLUMN IF NOT EXISTS fallback_reason TEXT"))
|
||||
await conn.execute(text("""
|
||||
CREATE INDEX IF NOT EXISTS idx_mold_cavity_best_scheme_id
|
||||
ON mold_cavity_data (best_scheme_id)
|
||||
"""))
|
||||
await conn.execute(text("""
|
||||
CREATE INDEX IF NOT EXISTS idx_mold_cavity_is_fallback
|
||||
ON mold_cavity_data (is_fallback)
|
||||
"""))
|
||||
await conn.execute(text("""
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = 'uq_inventory_product_warehouse'
|
||||
) THEN
|
||||
ALTER TABLE inventory
|
||||
ADD CONSTRAINT uq_inventory_product_warehouse UNIQUE (product_id, warehouse_id);
|
||||
END IF;
|
||||
END $$;
|
||||
"""))
|
||||
await conn.execute(text("""
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = 'ck_inventory_qty_nonnegative'
|
||||
) THEN
|
||||
ALTER TABLE inventory
|
||||
ADD CONSTRAINT ck_inventory_qty_nonnegative
|
||||
CHECK (quantity >= 0 AND locked_quantity >= 0 AND locked_quantity <= quantity);
|
||||
END IF;
|
||||
END $$;
|
||||
"""))
|
||||
await conn.execute(text("""
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = 'ck_purchase_order_items_qty'
|
||||
) THEN
|
||||
ALTER TABLE purchase_order_items
|
||||
ADD CONSTRAINT ck_purchase_order_items_qty
|
||||
CHECK (quantity > 0 AND received_quantity >= 0 AND received_quantity <= quantity);
|
||||
END IF;
|
||||
END $$;
|
||||
"""))
|
||||
await conn.execute(text("""
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = 'ck_sales_order_items_qty'
|
||||
) THEN
|
||||
ALTER TABLE sales_order_items
|
||||
ADD CONSTRAINT ck_sales_order_items_qty
|
||||
CHECK (quantity > 0 AND delivered_quantity >= 0 AND delivered_quantity <= quantity);
|
||||
END IF;
|
||||
END $$;
|
||||
"""))
|
||||
await conn.execute(text("ALTER TABLE sales_orders ADD COLUMN IF NOT EXISTS manufacturing_date TIMESTAMP WITHOUT TIME ZONE"))
|
||||
await conn.execute(text("ALTER TABLE sales_orders ALTER COLUMN manufacturing_date TYPE TIMESTAMP WITHOUT TIME ZONE"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(init_database(keep_connected=False))
|
||||
+43
-117
@@ -74,7 +74,7 @@ JSON 格式:
|
||||
"confidence": 0.85,
|
||||
"reasoning": "详细的中文推理过程...",
|
||||
"risk_notes": ["风险1", "风险2"],
|
||||
"rankings": [{"axis":"Z","rank":1,"score":92,"note":"..."},{"axis":"X","rank":2,"score":78,"note":"..."}]
|
||||
"rankings": [{"axis":"Z","rank":1,"score":92,"note":"..."}]
|
||||
}"""
|
||||
|
||||
_PARTING_USER = """请评估以下候选分模方向并推荐最优方案:
|
||||
@@ -126,11 +126,7 @@ class LLMService:
|
||||
|
||||
try:
|
||||
prompt = self._build_design_report_prompt(analysis_result, detailed_cavity_json)
|
||||
response = await self._chat(
|
||||
system=_DESIGN_REPORT_SYSTEM,
|
||||
user=prompt,
|
||||
max_tokens=self._max_tokens,
|
||||
)
|
||||
response = await self._chat(_DESIGN_REPORT_SYSTEM, prompt, self._max_tokens)
|
||||
if response:
|
||||
logger.info("LLM 设计报告生成成功 (%d 字符)", len(response))
|
||||
return response
|
||||
@@ -150,128 +146,74 @@ class LLMService:
|
||||
return None
|
||||
|
||||
try:
|
||||
prompt = self._build_parting_prompt(
|
||||
geometry_data, candidate_schemes, material, cavity_count,
|
||||
)
|
||||
response = await self._chat(
|
||||
system=_PARTING_SYSTEM,
|
||||
user=prompt,
|
||||
max_tokens=min(self._max_tokens, 1200),
|
||||
expect_json=True,
|
||||
)
|
||||
prompt = self._build_parting_prompt(geometry_data, candidate_schemes, material, cavity_count)
|
||||
response = await self._chat(_PARTING_SYSTEM, prompt, min(self._max_tokens, 1200), expect_json=True)
|
||||
if response:
|
||||
result = self._parse_json_response(response)
|
||||
if result:
|
||||
logger.info(
|
||||
"LLM 分型推荐: %s (置信度 %.2f)",
|
||||
result.get("recommended_axis", "?"),
|
||||
result.get("confidence", 0),
|
||||
)
|
||||
logger.info("LLM 分型推荐: %s (%.2f)", result.get("recommended_axis", "?"), result.get("confidence", 0))
|
||||
return result
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.warning("LLM 分型推荐失败(不影响主流程): %s", e)
|
||||
return None
|
||||
|
||||
def _build_design_report_prompt(
|
||||
self,
|
||||
analysis_result: Dict[str, Any],
|
||||
detailed_cavity_json: Optional[Dict[str, Any]],
|
||||
) -> str:
|
||||
detected_features = analysis_result.get("detected_features", [])
|
||||
quality_metrics = analysis_result.get("quality_metrics", {})
|
||||
recommendations = analysis_result.get("design_recommendations", [])
|
||||
|
||||
feature_text = json.dumps(detected_features, ensure_ascii=False, indent=2)
|
||||
if len(feature_text) > 4000:
|
||||
feature_text = feature_text[:4000] + "\n... (已截断)"
|
||||
def _build_design_report_prompt(self, analysis_result, detailed_cavity_json) -> str:
|
||||
features = json.dumps(analysis_result.get("detected_features", []), ensure_ascii=False, indent=2)
|
||||
if len(features) > 4000:
|
||||
features = features[:4000] + "\n... (已截断)"
|
||||
|
||||
schemes_text = ""
|
||||
if detailed_cavity_json:
|
||||
schemes = detailed_cavity_json.get("candidate_schemes", [])
|
||||
if schemes:
|
||||
schemes_text = json.dumps(
|
||||
[
|
||||
{
|
||||
"scheme_id": s.get("scheme_id"),
|
||||
"rank": s.get("rank"),
|
||||
"title": s.get("title"),
|
||||
"score": s.get("score"),
|
||||
"confidence_score": s.get("confidence_score"),
|
||||
"summary": s.get("summary"),
|
||||
schemes_text = json.dumps([{
|
||||
"scheme_id": s.get("scheme_id"), "rank": s.get("rank"), "title": s.get("title"),
|
||||
"score": s.get("score"), "summary": s.get("summary"),
|
||||
"parting_axis": s.get("parting", {}).get("axis"),
|
||||
"mold_structure_type": s.get("mold_structure_type"),
|
||||
"dfm_violations": s.get("dfm_violations", []),
|
||||
}
|
||||
for s in schemes
|
||||
],
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
)
|
||||
} for s in schemes], ensure_ascii=False, indent=2)
|
||||
|
||||
best_scheme = (
|
||||
detailed_cavity_json.get("candidate_schemes", [{}])[0]
|
||||
if detailed_cavity_json
|
||||
else {}
|
||||
)
|
||||
cavity_data = best_scheme.get("cavity_data", {}) if isinstance(best_scheme, dict) else {}
|
||||
mfg_info = cavity_data.get("manufacturing_info", {})
|
||||
metadata = cavity_data.get("metadata", {})
|
||||
best = detailed_cavity_json.get("candidate_schemes", [{}])[0] if detailed_cavity_json else {}
|
||||
cd = best.get("cavity_data", {}) if isinstance(best, dict) else {}
|
||||
mfg = cd.get("manufacturing_info", {})
|
||||
meta = cd.get("metadata", {})
|
||||
|
||||
return _DESIGN_REPORT_USER.format(
|
||||
filename=metadata.get("file_name", "unknown.stp"),
|
||||
material=metadata.get("selected_material", "ABS"),
|
||||
filename=meta.get("file_name", "unknown.stp"),
|
||||
material=meta.get("selected_material", "ABS"),
|
||||
volume=f"{analysis_result.get('geometry_data', {}).get('volume', 0):.1f} mm³",
|
||||
surface_area=f"{analysis_result.get('geometry_data', {}).get('surface_area', 0):.1f} mm²",
|
||||
bbox=json.dumps(analysis_result.get("geometry_data", {}).get("bounding_box", {}), ensure_ascii=False),
|
||||
features=feature_text or "无特征检测数据",
|
||||
quality_metrics=json.dumps(quality_metrics, ensure_ascii=False, indent=2),
|
||||
features=features or "无特征检测数据",
|
||||
quality_metrics=json.dumps(analysis_result.get("quality_metrics", {}), ensure_ascii=False, indent=2),
|
||||
schemes=schemes_text or "无分模方案数据",
|
||||
mold_material=mfg_info.get("mold_material", "自动选择"),
|
||||
mold_hardness=mfg_info.get("mold_hardness", "自动选择"),
|
||||
clamping_force=mfg_info.get("estimated_clamping_force", "自动计算"),
|
||||
mold_size=json.dumps(mfg_info.get("estimated_mold_size", {}), ensure_ascii=False),
|
||||
cycle_time=mfg_info.get("estimated_cycle_time", "自动计算"),
|
||||
draft_angle=f"{metadata.get('draft_angle', 2.0)}°",
|
||||
shrinkage_rate=f"{metadata.get('shrinkage_rate', 0.0) * 100:.2f}%"
|
||||
if isinstance(metadata.get("shrinkage_rate"), (int, float))
|
||||
else "自动计算",
|
||||
recommendations=json.dumps(recommendations, ensure_ascii=False, indent=2) if recommendations else "无",
|
||||
mold_material=mfg.get("mold_material", "自动选择"),
|
||||
mold_hardness=mfg.get("mold_hardness", "自动选择"),
|
||||
clamping_force=mfg.get("estimated_clamping_force", "自动计算"),
|
||||
mold_size=json.dumps(mfg.get("estimated_mold_size", {}), ensure_ascii=False),
|
||||
cycle_time=mfg.get("estimated_cycle_time", "自动计算"),
|
||||
draft_angle=f"{meta.get('draft_angle', 2.0)}°",
|
||||
shrinkage_rate="自动计算",
|
||||
recommendations=json.dumps(analysis_result.get("design_recommendations", []), ensure_ascii=False, indent=2) or "无",
|
||||
)
|
||||
|
||||
def _build_parting_prompt(
|
||||
self,
|
||||
geometry_data: Dict[str, Any],
|
||||
candidate_schemes: List[Dict[str, Any]],
|
||||
material: Dict[str, Any],
|
||||
cavity_count: int,
|
||||
) -> str:
|
||||
def _build_parting_prompt(self, geometry_data, candidate_schemes, material, cavity_count) -> str:
|
||||
bbox = geometry_data.get("bounding_box", {})
|
||||
axis_normal_stats = geometry_data.get("axis_normal_stats", {})
|
||||
inertia = geometry_data.get("inertia_matrix", [])
|
||||
inertia_diag = [
|
||||
inertia[i][i] if i < len(inertia) and i < len(inertia[i]) else 0.0
|
||||
for i in range(3)
|
||||
]
|
||||
inertia_diag = [inertia[i][i] if i < len(inertia) and i < len(inertia[i]) else 0.0 for i in range(3)]
|
||||
|
||||
schemes_text = json.dumps(
|
||||
[
|
||||
{
|
||||
schemes_text = json.dumps([{
|
||||
"axis": s.get("parting", {}).get("axis") or s.get("axis"),
|
||||
"score": s.get("score"),
|
||||
"confidence_score": s.get("confidence_score"),
|
||||
"summary": s.get("summary"),
|
||||
"score": s.get("score"), "summary": s.get("summary"),
|
||||
"mold_structure_type": s.get("mold_structure_type"),
|
||||
"core_required": s.get("core_required"),
|
||||
"dfm_violations": s.get("dfm_violations", []),
|
||||
"undercut_regions_count": len(s.get("undercut_regions", [])),
|
||||
"score_breakdown": s.get("score_breakdown", {}),
|
||||
}
|
||||
for s in candidate_schemes
|
||||
],
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
)
|
||||
} for s in candidate_schemes], ensure_ascii=False, indent=2)
|
||||
|
||||
return _PARTING_USER.format(
|
||||
bbox=json.dumps(bbox, ensure_ascii=False),
|
||||
@@ -284,47 +226,31 @@ class LLMService:
|
||||
schemes=schemes_text,
|
||||
)
|
||||
|
||||
async def _chat(
|
||||
self,
|
||||
system: str,
|
||||
user: str,
|
||||
max_tokens: int = 2000,
|
||||
expect_json: bool = False,
|
||||
temperature: float = 0.3,
|
||||
) -> Optional[str]:
|
||||
async def _chat(self, system, user, max_tokens=2000, expect_json=False, temperature=0.3):
|
||||
url = f"{self._api_url}/chat/completions"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self._api_key}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
headers = {"Authorization": f"Bearer {self._api_key}", "Content-Type": "application/json"}
|
||||
payload = {
|
||||
"model": self._model,
|
||||
"messages": [
|
||||
{"role": "system", "content": system},
|
||||
{"role": "user", "content": user},
|
||||
],
|
||||
"max_tokens": max_tokens,
|
||||
"temperature": temperature,
|
||||
"messages": [{"role": "system", "content": system}, {"role": "user", "content": user}],
|
||||
"max_tokens": max_tokens, "temperature": temperature,
|
||||
}
|
||||
if expect_json:
|
||||
payload["response_format"] = {"type": "json_object"}
|
||||
|
||||
async with httpx.AsyncClient(timeout=self._timeout) as client:
|
||||
resp = await client.post(url, json=payload, headers=headers)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
content = data["choices"][0]["message"]["content"]
|
||||
content = resp.json()["choices"][0]["message"]["content"]
|
||||
return content.strip() if content else None
|
||||
|
||||
@staticmethod
|
||||
def _parse_json_response(raw: str) -> Optional[Dict[str, Any]]:
|
||||
def _parse_json_response(raw):
|
||||
try:
|
||||
return json.loads(raw)
|
||||
except json.JSONDecodeError:
|
||||
match = re.search(r"\{[\s\S]*\}", raw)
|
||||
if match:
|
||||
m = re.search(r"\{[\s\S]*\}", raw)
|
||||
if m:
|
||||
try:
|
||||
return json.loads(match.group())
|
||||
return json.loads(m.group())
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
logger.warning("LLM JSON 解析失败: %s...", raw[:200])
|
||||
|
||||
@@ -259,24 +259,10 @@ class ProcessingService:
|
||||
file_path, db_session, task_id, stp_file_id, analysis_result
|
||||
)
|
||||
|
||||
# 9.8 LLM 增强分析(可选,不影响主流程)
|
||||
# 9.8 LLM 增强分析
|
||||
llm_report = None
|
||||
llm_parting = None
|
||||
if analysis_result:
|
||||
llm_report = await llm_service.generate_design_report(
|
||||
analysis_result, detailed_cavity_json
|
||||
)
|
||||
candidate_schemes = detailed_cavity_json.get("candidate_schemes", [])
|
||||
if candidate_schemes:
|
||||
cavity_count = detailed_cavity_json.get("mold_cavities", {}).get("cavity_count", 1)
|
||||
if isinstance(cavity_count, (int, float)):
|
||||
cavity_count = int(cavity_count)
|
||||
else:
|
||||
cavity_count = 1
|
||||
llm_parting = await llm_service.recommend_parting_direction(
|
||||
geometry_data, candidate_schemes, selected_material,
|
||||
cavity_count=cavity_count,
|
||||
)
|
||||
llm_report = await llm_service.generate_design_report(analysis_result, detailed_cavity_json)
|
||||
|
||||
# 10. 完成处理
|
||||
await self.storage_service.update_stp_file_status(db_session, stp_file_id, "completed")
|
||||
@@ -296,7 +282,6 @@ class ProcessingService:
|
||||
"html_file": best_scheme.get("html_file", f"/html/{Path(html_file_path).name}") if best_scheme else f"/html/{Path(html_file_path).name}",
|
||||
"verification": verification_result,
|
||||
"llm_report": llm_report,
|
||||
"llm_parting_recommendation": llm_parting,
|
||||
"status": ProcessingStatus.COMPLETED,
|
||||
"completed_at": str(datetime.now()),
|
||||
})
|
||||
|
||||
@@ -0,0 +1,831 @@
|
||||
# utils/html_generator.py
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Optional
|
||||
from datetime import datetime
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
try:
|
||||
import orjson
|
||||
|
||||
def _json_dumps(obj: Any) -> bytes:
|
||||
return orjson.dumps(obj, option=orjson.OPT_SERIALIZE_NUMPY | orjson.OPT_NON_STR_KEYS)
|
||||
|
||||
def _json_dumps_str(obj: Any) -> str:
|
||||
return orjson.dumps(obj, option=orjson.OPT_SERIALIZE_NUMPY | orjson.OPT_NON_STR_KEYS).decode("utf-8")
|
||||
|
||||
_JSON_FAST = True
|
||||
except ImportError:
|
||||
import json
|
||||
|
||||
def _json_dumps(obj: Any) -> bytes:
|
||||
return json.dumps(obj, ensure_ascii=False).encode("utf-8")
|
||||
|
||||
def _json_dumps_str(obj: Any) -> str:
|
||||
return json.dumps(obj, ensure_ascii=False)
|
||||
|
||||
_JSON_FAST = False
|
||||
|
||||
|
||||
class HTMLGenerator:
|
||||
"""HTML文件生成器 — 数据分离架构,Three.js 0.170 + PBR渲染"""
|
||||
|
||||
def __init__(self, output_dir: str = "./html_output"):
|
||||
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数据"""
|
||||
|
||||
cavity_html = self._build_cavity_info_panel_template()
|
||||
|
||||
html_content = f"""<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>3D模具几何可视化 - {stp_filename}</title>
|
||||
<script type="importmap">
|
||||
{{
|
||||
"imports": {{
|
||||
"three": "https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js",
|
||||
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/"
|
||||
}}
|
||||
}}
|
||||
</script>
|
||||
<style>
|
||||
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
||||
body {{ overflow: hidden; font-family: 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif; background: #f0f2f5; }}
|
||||
#container {{ position: relative; width: 100vw; height: 100vh; }}
|
||||
#canvas {{ display: block; }}
|
||||
|
||||
#loading-overlay {{
|
||||
position: absolute; inset: 0; display: flex; flex-direction: column;
|
||||
align-items: center; justify-content: center; background: rgba(240,242,245,0.95);
|
||||
z-index: 100; transition: opacity 0.4s;
|
||||
}}
|
||||
#loading-overlay.hidden {{ opacity: 0; pointer-events: none; }}
|
||||
.spinner {{
|
||||
width: 48px; height: 48px; border: 3px solid rgba(0,0,0,0.1);
|
||||
border-top-color: #4CAF50; border-radius: 50%; animation: spin 0.8s linear infinite;
|
||||
}}
|
||||
@keyframes spin {{ to {{ transform: rotate(360deg); }} }}
|
||||
.loading-text {{ color: #666; margin-top: 16px; font-size: 14px; }}
|
||||
|
||||
#info-panel {{
|
||||
position: absolute; top: 10px; left: 10px; background: rgba(255,255,255,0.92);
|
||||
color: #333; padding: 12px 16px; border-radius: 10px; font-size: 13px;
|
||||
max-width: 340px; backdrop-filter: blur(10px); border: 1px solid rgba(0,0,0,0.08);
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
|
||||
}}
|
||||
#info-panel h3 {{ margin: 0 0 6px 0; font-size: 14px; color: #2E7D32; }}
|
||||
#info-panel .info-row {{ display: flex; justify-content: space-between; padding: 2px 0; }}
|
||||
#info-panel .info-label {{ color: #888; }}
|
||||
#info-panel .info-value {{ color: #111; font-weight: 500; }}
|
||||
|
||||
#cavity-info-panel {{
|
||||
position: absolute; top: 10px; right: 10px; background: rgba(255,255,255,0.92);
|
||||
color: #333; padding: 15px; border-radius: 10px; font-size: 12px;
|
||||
max-width: 310px; backdrop-filter: blur(10px); border: 1px solid rgba(0,0,0,0.08);
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
|
||||
display: none;
|
||||
}}
|
||||
#cavity-info-panel h3 {{ margin: 0 0 10px 0; font-size: 14px; color: #E65100; }}
|
||||
#cavity-info-panel .metric {{ display: flex; justify-content: space-between; padding: 3px 0; }}
|
||||
#cavity-info-panel .metric-label {{ color: #888; }}
|
||||
#cavity-info-panel .metric-value {{ color: #111; font-weight: 500; }}
|
||||
|
||||
#toolbar {{
|
||||
position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%);
|
||||
display: flex; gap: 6px; background: rgba(255,255,255,0.92); padding: 8px 12px;
|
||||
border-radius: 24px; backdrop-filter: blur(10px); border: 1px solid rgba(0,0,0,0.08);
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
|
||||
flex-wrap: wrap; justify-content: center;
|
||||
}}
|
||||
#toolbar button {{
|
||||
background: rgba(0,0,0,0.04); color: #555; border: 1px solid rgba(0,0,0,0.1);
|
||||
padding: 6px 14px; border-radius: 18px; cursor: pointer; font-size: 12px;
|
||||
transition: all 0.2s; white-space: nowrap;
|
||||
}}
|
||||
#toolbar button:hover {{ background: rgba(0,0,0,0.1); color: #222; }}
|
||||
#toolbar button.active {{ background: rgba(76,175,80,0.18); border-color: #4CAF50; color: #2E7D32; }}
|
||||
#toolbar button.accent {{
|
||||
background: rgba(255,87,34,0.15); border-color: #FF5722; color: #D84315;
|
||||
font-weight: bold;
|
||||
}}
|
||||
#toolbar button.accent:hover {{ background: rgba(255,87,34,0.28); }}
|
||||
|
||||
#view-presets {{
|
||||
position: absolute; bottom: 75px; left: 50%; transform: translateX(-50%);
|
||||
display: flex; gap: 4px; background: rgba(255,255,255,0.88); padding: 6px 8px;
|
||||
border-radius: 20px; backdrop-filter: blur(8px); border: 1px solid rgba(0,0,0,0.06);
|
||||
box-shadow: 0 1px 8px rgba(0,0,0,0.06);
|
||||
}}
|
||||
#view-presets button {{
|
||||
background: rgba(0,0,0,0.03); color: #777; border: none;
|
||||
padding: 5px 10px; border-radius: 14px; cursor: pointer; font-size: 11px;
|
||||
transition: all 0.2s;
|
||||
}}
|
||||
#view-presets button:hover {{ background: rgba(0,0,0,0.1); color: #222; }}
|
||||
|
||||
@media (max-width: 768px) {{
|
||||
#info-panel {{ max-width: 240px; font-size: 11px; padding: 8px 12px; }}
|
||||
#cavity-info-panel {{ max-width: 220px; font-size: 10px; padding: 10px; }}
|
||||
#toolbar {{ gap: 3px; padding: 6px 8px; }}
|
||||
#toolbar button {{ padding: 5px 10px; font-size: 10px; }}
|
||||
#view-presets {{ bottom: 68px; }}
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container">
|
||||
<canvas id="canvas"></canvas>
|
||||
|
||||
<div id="loading-overlay">
|
||||
<div class="spinner"></div>
|
||||
<div class="loading-text" id="loading-status">加载几何数据...</div>
|
||||
</div>
|
||||
|
||||
<div id="info-panel">
|
||||
<h3>📐 {stp_filename}</h3>
|
||||
<div class="info-row"><span class="info-label">顶点</span><span class="info-value" id="info-verts">-</span></div>
|
||||
<div class="info-row"><span class="info-label">三角面</span><span class="info-value" id="info-faces">-</span></div>
|
||||
<div class="info-row"><span class="info-label">点云</span><span class="info-value" id="info-points">-</span></div>
|
||||
<div class="info-row"><span class="info-label">体积</span><span class="info-value" id="info-vol">-</span></div>
|
||||
</div>
|
||||
|
||||
{cavity_html}
|
||||
|
||||
<div id="view-presets">
|
||||
<button onclick="setView('front')" title="前视">前</button>
|
||||
<button onclick="setView('back')" title="后视">后</button>
|
||||
<button onclick="setView('left')" title="左视">左</button>
|
||||
<button onclick="setView('right')" title="右视">右</button>
|
||||
<button onclick="setView('top')" title="俯视">俯</button>
|
||||
<button onclick="setView('bottom')" title="仰视">仰</button>
|
||||
<button onclick="setView('iso')" title="等轴测" style="font-weight:bold;color:#FF9800;">3D</button>
|
||||
</div>
|
||||
|
||||
<div id="toolbar">
|
||||
<button id="btn-product" class="active" onclick="toggleProduct()">产品</button>
|
||||
<button id="btn-mold" class="active" onclick="toggleMold()">模具</button>
|
||||
<button id="btn-parting" class="active" onclick="toggleParting()">分型面</button>
|
||||
<button id="btn-pointcloud" onclick="togglePointcloud()">点云</button>
|
||||
<button onclick="toggleWireframe()">线框</button>
|
||||
<button onclick="resetView()">重置</button>
|
||||
<button id="splitBtn" class="accent" onclick="splitMold()">分模拆分</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import * as THREE from 'three';
|
||||
import {{ OrbitControls }} from 'three/addons/controls/OrbitControls.js';
|
||||
|
||||
const DATA_URL = '{data_filename}';
|
||||
|
||||
let productMesh, cavityMesh, coreMesh, partingMesh, pointcloudMesh;
|
||||
let productVisible = true, moldVisible = true, partingVisible = true, pointcloudVisible = false;
|
||||
let isSplit = false, splitAnimId = null;
|
||||
let sceneBox = null;
|
||||
let cavityDataGlobal = null;
|
||||
|
||||
const renderer = new THREE.WebGLRenderer({{ canvas: document.getElementById('canvas'), antialias: true, alpha: true }});
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
renderer.shadowMap.enabled = true;
|
||||
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
||||
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
||||
renderer.toneMappingExposure = 1.2;
|
||||
|
||||
const scene = new THREE.Scene();
|
||||
scene.background = new THREE.Color(0xf0f2f5);
|
||||
scene.fog = new THREE.Fog(0xf0f2f5, 500, 3000);
|
||||
|
||||
const camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight, 0.1, 10000);
|
||||
const controls = new OrbitControls(camera, renderer.domElement);
|
||||
controls.enableDamping = true;
|
||||
controls.dampingFactor = 0.12;
|
||||
controls.minDistance = 1;
|
||||
controls.maxDistance = 5000;
|
||||
controls.target.set(0, 0, 0);
|
||||
|
||||
function setupLighting() {{
|
||||
const ambient = new THREE.AmbientLight(0xccccdd, 4);
|
||||
scene.add(ambient);
|
||||
|
||||
const keyLight = new THREE.DirectionalLight(0xffffff, 7);
|
||||
keyLight.position.set(1, 1.2, 0.8);
|
||||
keyLight.castShadow = true;
|
||||
keyLight.shadow.mapSize.width = 2048;
|
||||
keyLight.shadow.mapSize.height = 2048;
|
||||
keyLight.shadow.camera.near = 0.5;
|
||||
keyLight.shadow.camera.far = 500;
|
||||
keyLight.shadow.bias = -0.0001;
|
||||
scene.add(keyLight);
|
||||
|
||||
const fillLight = new THREE.DirectionalLight(0xccddff, 3);
|
||||
fillLight.position.set(-0.6, 0.3, -0.4);
|
||||
scene.add(fillLight);
|
||||
|
||||
const rimLight = new THREE.DirectionalLight(0xffffff, 4);
|
||||
rimLight.position.set(0, -0.3, -1);
|
||||
scene.add(rimLight);
|
||||
|
||||
const bottomLight = new THREE.DirectionalLight(0x8899cc, 1.5);
|
||||
bottomLight.position.set(0, -1, 0.2);
|
||||
scene.add(bottomLight);
|
||||
|
||||
const pmremGenerator = new THREE.PMREMGenerator(renderer);
|
||||
pmremGenerator.compileEquirectangularShader();
|
||||
const envScene = new THREE.Scene();
|
||||
envScene.background = new THREE.Color(0xddeeff);
|
||||
const envMap = pmremGenerator.fromScene(envScene).texture;
|
||||
scene.environment = envMap;
|
||||
scene.background = new THREE.Color(0xf0f2f5);
|
||||
}}
|
||||
|
||||
setupLighting();
|
||||
|
||||
const axesHelper = new THREE.AxesHelper(50);
|
||||
scene.add(axesHelper);
|
||||
|
||||
const gridHelper = new THREE.GridHelper(400, 40, 0xccccdd, 0xe8e8f0);
|
||||
scene.add(gridHelper);
|
||||
|
||||
function toFlatArray(data) {{
|
||||
if (!Array.isArray(data)) return [];
|
||||
if (data.length === 0) return [];
|
||||
return Array.isArray(data[0]) ? data.flat(Infinity) : data;
|
||||
}}
|
||||
|
||||
function normalizePositions(rawPositions, center) {{
|
||||
const flat = toFlatArray(rawPositions);
|
||||
if (!flat.length) return [];
|
||||
const cx = Number(center[0] || 0), cy = Number(center[1] || 0), cz = Number(center[2] || 0);
|
||||
const normalized = [];
|
||||
for (let i = 0; i + 2 < flat.length; i += 3) {{
|
||||
const x = Number(flat[i]), y = Number(flat[i + 1]), z = Number(flat[i + 2]);
|
||||
if (Number.isFinite(x) && Number.isFinite(y) && Number.isFinite(z)) {{
|
||||
normalized.push(x - cx, y - cy, z - cz);
|
||||
}}
|
||||
}}
|
||||
return normalized;
|
||||
}}
|
||||
|
||||
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]), y = Number(flat[i + 1]), 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;
|
||||
}}
|
||||
|
||||
function createPBRMaterial(colorHex, opts = {{}}) {{
|
||||
return new THREE.MeshStandardMaterial({{
|
||||
color: new THREE.Color(colorHex),
|
||||
metalness: opts.metalness ?? 0.05,
|
||||
roughness: opts.roughness ?? 0.35,
|
||||
transparent: true,
|
||||
opacity: opts.opacity ?? 0.55,
|
||||
side: THREE.DoubleSide,
|
||||
depthWrite: opts.depthWrite ?? true,
|
||||
}});
|
||||
}}
|
||||
|
||||
function registerInitialPose(mesh) {{
|
||||
if (!mesh) return;
|
||||
mesh.userData.initialPosition = mesh.position.clone();
|
||||
mesh.userData.initialVisible = mesh.visible;
|
||||
}}
|
||||
|
||||
function fitCameraToScene() {{
|
||||
const objects = [productMesh, cavityMesh, coreMesh, partingMesh, pointcloudMesh].filter(Boolean);
|
||||
if (!objects.length) return;
|
||||
sceneBox = new THREE.Box3();
|
||||
objects.forEach(obj => sceneBox.expandByObject(obj));
|
||||
if (sceneBox.isEmpty()) return;
|
||||
const center = new THREE.Vector3();
|
||||
const size = new THREE.Vector3();
|
||||
sceneBox.getCenter(center);
|
||||
sceneBox.getSize(size);
|
||||
const maxDim = Math.max(size.x, size.y, size.z) || 100;
|
||||
const distance = Math.max(maxDim * 1.6, 30);
|
||||
camera.near = Math.max(maxDim / 2000, 0.01);
|
||||
camera.far = Math.max(maxDim * 200, 10000);
|
||||
camera.updateProjectionMatrix();
|
||||
camera.position.set(center.x + distance * 0.7, center.y + distance * 0.7, center.z + distance * 0.8);
|
||||
controls.target.copy(center);
|
||||
controls.minDistance = Math.max(maxDim * 0.03, 0.5);
|
||||
controls.maxDistance = Math.max(maxDim * 30, 5000);
|
||||
controls.update();
|
||||
}}
|
||||
|
||||
function setView(direction) {{
|
||||
if (!sceneBox) return;
|
||||
const center = new THREE.Vector3();
|
||||
const size = new THREE.Vector3();
|
||||
sceneBox.getCenter(center);
|
||||
sceneBox.getSize(size);
|
||||
const dist = Math.max(size.x, size.y, size.z) * 1.5;
|
||||
const positions = {{
|
||||
front: [0, 0, dist],
|
||||
back: [0, 0, -dist],
|
||||
left: [-dist, 0, 0],
|
||||
right: [dist, 0, 0],
|
||||
top: [0, dist, 0],
|
||||
bottom: [0, -dist, 0],
|
||||
iso: [dist * 0.7, dist * 0.7, dist * 0.8],
|
||||
}};
|
||||
const pos = positions[direction] || positions.iso;
|
||||
camera.position.set(center.x + pos[0], center.y + pos[1], center.z + pos[2]);
|
||||
controls.target.copy(center);
|
||||
controls.update();
|
||||
}}
|
||||
|
||||
window.setView = setView;
|
||||
|
||||
function buildScene(geometryData, cavityData, pointcloudData) {{
|
||||
cavityDataGlobal = cavityData;
|
||||
const bbox = geometryData.bounding_box || computeBounds([
|
||||
pointcloudData?.points,
|
||||
cavityData?.mold_cavities?.cavity?.vertices,
|
||||
cavityData?.mold_cavities?.core?.vertices
|
||||
]) || {{ center: [0, 0, 0], dimensions: [100, 100, 100] }};
|
||||
const centerOffset = bbox.center || [0, 0, 0];
|
||||
const width = (bbox.dimensions && bbox.dimensions[0]) || 100;
|
||||
const height = (bbox.dimensions && bbox.dimensions[1]) || 100;
|
||||
const depth = (bbox.dimensions && bbox.dimensions[2]) || 100;
|
||||
const coreRequired = cavityData?.metadata?.core_required !== false;
|
||||
const maxDim = Math.max(width, height, depth) || 100;
|
||||
|
||||
const lods = pointcloudData?.lods;
|
||||
if (lods && lods["0"] && lods["0"].vertices && lods["0"].faces) {{
|
||||
const lodGroup = new THREE.LOD();
|
||||
const lodKeys = Object.keys(lods).sort((a, b) => Number(a) - Number(b));
|
||||
for (const key of lodKeys) {{
|
||||
const entry = lods[key];
|
||||
if (!entry.vertices || !entry.faces || entry.vertices.length === 0 || entry.faces.length === 0) continue;
|
||||
const productVerts = normalizePositions(entry.vertices, centerOffset);
|
||||
const productFaces = toFlatArray(entry.faces);
|
||||
if (productVerts.length < 9 || productFaces.length < 3) continue;
|
||||
const lodGeo = new THREE.BufferGeometry();
|
||||
lodGeo.setAttribute('position', new THREE.BufferAttribute(new Float32Array(productVerts), 3));
|
||||
lodGeo.setIndex(new THREE.BufferAttribute(new Uint32Array(productFaces), 1));
|
||||
lodGeo.computeVertexNormals();
|
||||
const lodMat = createPBRMaterial(0xFFFFFF, {{ metalness: 0.0, roughness: 0.20, opacity: 0.55 }});
|
||||
const lodMesh = new THREE.Mesh(lodGeo, lodMat);
|
||||
lodMesh.castShadow = true;
|
||||
lodMesh.receiveShadow = true;
|
||||
const dist = key === "0" ? 0 : key === "1" ? maxDim * 3 : maxDim * 8;
|
||||
lodGroup.addLevel(lodMesh, dist);
|
||||
}}
|
||||
productMesh = lodGroup;
|
||||
scene.add(productMesh);
|
||||
registerInitialPose(productMesh);
|
||||
}} else if (pointcloudData && pointcloudData.vertices && pointcloudData.faces && pointcloudData.vertices.length > 0 && pointcloudData.faces.length > 0) {{
|
||||
const productVerts = normalizePositions(pointcloudData.vertices, centerOffset);
|
||||
const productFaces = toFlatArray(pointcloudData.faces);
|
||||
if (productVerts.length >= 9 && productFaces.length >= 3) {{
|
||||
const productGeometry = new THREE.BufferGeometry();
|
||||
productGeometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(productVerts), 3));
|
||||
productGeometry.setIndex(new THREE.BufferAttribute(new Uint32Array(productFaces), 1));
|
||||
productGeometry.computeVertexNormals();
|
||||
const productMaterial = createPBRMaterial(0xFFFFFF, {{ metalness: 0.0, roughness: 0.20, opacity: 0.55 }});
|
||||
productMesh = new THREE.Mesh(productGeometry, productMaterial);
|
||||
productMesh.castShadow = true;
|
||||
productMesh.receiveShadow = true;
|
||||
scene.add(productMesh);
|
||||
registerInitialPose(productMesh);
|
||||
}}
|
||||
}}
|
||||
|
||||
if (!productMesh && pointcloudData && pointcloudData.points && pointcloudData.points.length > 0) {{
|
||||
const pointPositions = normalizePositions(pointcloudData.points, centerOffset);
|
||||
if (pointPositions.length >= 3) {{
|
||||
const ptGeometry = new THREE.BufferGeometry();
|
||||
ptGeometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(pointPositions), 3));
|
||||
if (pointcloudData.normals && pointcloudData.normals.length > 0) {{
|
||||
const normals = new Float32Array(toFlatArray(pointcloudData.normals));
|
||||
if (normals.length === pointPositions.length) {{
|
||||
ptGeometry.setAttribute('normal', new THREE.BufferAttribute(normals, 3));
|
||||
}}
|
||||
}}
|
||||
const ptMaterial = new THREE.PointsMaterial({{
|
||||
color: 0xFFFFFF, size: 0.5, sizeAttenuation: true,
|
||||
transparent: true, opacity: 0.90, blending: THREE.NormalBlending,
|
||||
depthWrite: false,
|
||||
}});
|
||||
pointcloudMesh = new THREE.Points(ptGeometry, ptMaterial);
|
||||
pointcloudMesh.visible = pointcloudVisible;
|
||||
scene.add(pointcloudMesh);
|
||||
registerInitialPose(pointcloudMesh);
|
||||
}}
|
||||
}}
|
||||
|
||||
if (!productMesh && !pointcloudMesh) {{
|
||||
const productGeometry = new THREE.BoxGeometry(width * 0.9, height * 0.9, depth * 0.9);
|
||||
const productMaterial = createPBRMaterial(0xFFFFFF, {{ metalness: 0.0, roughness: 0.25, opacity: 0.65 }});
|
||||
productMesh = new THREE.Mesh(productGeometry, productMaterial);
|
||||
productMesh.position.set(0, 0, 0);
|
||||
scene.add(productMesh);
|
||||
registerInitialPose(productMesh);
|
||||
}}
|
||||
|
||||
if (cavityData && cavityData.mold_cavities && cavityData.mold_cavities.cavity) {{
|
||||
const cd = cavityData.mold_cavities.cavity;
|
||||
if (cd.vertices && cd.faces && cd.vertices.length > 0 && cd.faces.length > 0) {{
|
||||
const cavityGeometry = new THREE.BufferGeometry();
|
||||
const positions = new Float32Array(normalizePositions(cd.vertices, centerOffset));
|
||||
const indices = new Uint32Array(toFlatArray(cd.faces));
|
||||
if (isValidIndexedGeometry(positions, indices)) {{
|
||||
cavityGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
||||
cavityGeometry.setIndex(new THREE.BufferAttribute(indices, 1));
|
||||
cavityGeometry.computeVertexNormals();
|
||||
const cavityMaterial = createPBRMaterial(0x4488cc, {{ metalness: 0.7, roughness: 0.3, opacity: 0.45, depthWrite: false }});
|
||||
cavityMesh = new THREE.Mesh(cavityGeometry, cavityMaterial);
|
||||
cavityMesh.renderOrder = 1;
|
||||
scene.add(cavityMesh);
|
||||
registerInitialPose(cavityMesh);
|
||||
addWireframe(cavityMesh, cavityGeometry, 0x3388bb);
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
if (!cavityMesh) createSimpleCavity(width, height, depth, centerOffset);
|
||||
|
||||
if (coreRequired && cavityData && cavityData.mold_cavities && cavityData.mold_cavities.core) {{
|
||||
const cd = cavityData.mold_cavities.core;
|
||||
if (cd.vertices && cd.faces && cd.vertices.length > 0 && cd.faces.length > 0) {{
|
||||
const coreGeometry = new THREE.BufferGeometry();
|
||||
const positions = new Float32Array(normalizePositions(cd.vertices, centerOffset));
|
||||
const indices = new Uint32Array(toFlatArray(cd.faces));
|
||||
if (isValidIndexedGeometry(positions, indices)) {{
|
||||
coreGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
||||
coreGeometry.setIndex(new THREE.BufferAttribute(indices, 1));
|
||||
coreGeometry.computeVertexNormals();
|
||||
const coreMaterial = createPBRMaterial(0xdd8822, {{ metalness: 0.7, roughness: 0.3, opacity: 0.45, depthWrite: false }});
|
||||
coreMesh = new THREE.Mesh(coreGeometry, coreMaterial);
|
||||
coreMesh.renderOrder = 1;
|
||||
scene.add(coreMesh);
|
||||
registerInitialPose(coreMesh);
|
||||
addWireframe(coreMesh, coreGeometry, 0xcc6600);
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
if (!coreMesh && coreRequired) createSimpleCore(width, height, depth, centerOffset);
|
||||
|
||||
const partingGeometry = new THREE.PlaneGeometry(width * 1.2, height * 1.2);
|
||||
const partingMaterial = new THREE.MeshBasicMaterial({{
|
||||
color: 0xF44336, transparent: true, opacity: 0.25, side: THREE.DoubleSide, depthWrite: false,
|
||||
}});
|
||||
partingMesh = new THREE.Mesh(partingGeometry, partingMaterial);
|
||||
partingMesh.position.set(centerOffset[0], centerOffset[1], centerOffset[2]);
|
||||
partingMesh.renderOrder = 2;
|
||||
scene.add(partingMesh);
|
||||
registerInitialPose(partingMesh);
|
||||
|
||||
if (productMesh) {{
|
||||
if (productMesh.isLOD) {{
|
||||
productMesh.traverse(child => {{
|
||||
if (child.isMesh && child.geometry) {{
|
||||
addWireframe(child, child.geometry, 0xCCCCCC);
|
||||
}}
|
||||
}});
|
||||
}} else {{
|
||||
addWireframe(productMesh, productMesh.geometry, 0xCCCCCC);
|
||||
}}
|
||||
}}
|
||||
|
||||
updateInfoPanel(pointcloudData, cavityData);
|
||||
|
||||
fitCameraToScene();
|
||||
}}
|
||||
|
||||
function addWireframe(parent, geometry, colorHex) {{
|
||||
const wf = new THREE.WireframeGeometry(geometry);
|
||||
const line = new THREE.LineSegments(wf, new THREE.LineBasicMaterial({{
|
||||
color: colorHex, transparent: true, opacity: 0.25, depthTest: true, depthWrite: false,
|
||||
}}));
|
||||
line.renderOrder = 3;
|
||||
parent.add(line);
|
||||
}}
|
||||
|
||||
function createSimpleCavity(width, height, depth, center) {{
|
||||
const halfDepth = depth / 2;
|
||||
const cGeo = new THREE.BoxGeometry(width * 1.2, height * 1.2, halfDepth + 10);
|
||||
const cMat = createPBRMaterial(0x4488cc, {{ metalness: 0.6, roughness: 0.35, opacity: 0.35, depthWrite: false }});
|
||||
cavityMesh = new THREE.Mesh(cGeo, cMat);
|
||||
cavityMesh.position.set(center[0], center[1], center[2] + halfDepth / 2 + 5);
|
||||
cavityMesh.renderOrder = 1;
|
||||
scene.add(cavityMesh);
|
||||
registerInitialPose(cavityMesh);
|
||||
addWireframe(cavityMesh, cGeo, 0x3388bb);
|
||||
}}
|
||||
|
||||
function createSimpleCore(width, height, depth, center) {{
|
||||
const halfDepth = depth / 2;
|
||||
const cGeo = new THREE.BoxGeometry(width * 1.2, height * 1.2, halfDepth + 10);
|
||||
const cMat = createPBRMaterial(0xdd8822, {{ metalness: 0.6, roughness: 0.35, opacity: 0.35, depthWrite: false }});
|
||||
coreMesh = new THREE.Mesh(cGeo, cMat);
|
||||
coreMesh.position.set(center[0], center[1], center[2] - halfDepth / 2 - 5);
|
||||
coreMesh.renderOrder = 1;
|
||||
scene.add(coreMesh);
|
||||
registerInitialPose(coreMesh);
|
||||
addWireframe(coreMesh, cGeo, 0xcc6600);
|
||||
}}
|
||||
|
||||
function updateInfoPanel(pointcloudData, cavityData) {{
|
||||
const verts = pointcloudData?.vertex_count || cavityData?.mold_cavities?.cavity?.vertex_count || '-';
|
||||
const faces = pointcloudData?.face_count || cavityData?.mold_cavities?.cavity?.face_count || '-';
|
||||
const pts = pointcloudData?.point_count || '-';
|
||||
const vol = cavityData?.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-points').textContent = typeof pts === 'number' ? pts.toLocaleString() : pts;
|
||||
document.getElementById('info-vol').textContent = vol;
|
||||
|
||||
const panel = document.getElementById('cavity-info-panel');
|
||||
if (panel && cavityData) {{
|
||||
panel.style.display = 'block';
|
||||
const meta = cavityData.metadata || {{}};
|
||||
const mfg = cavityData.manufacturing_info || {{}};
|
||||
const geo = cavityData.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 = '正在加载几何数据...';
|
||||
const resp = await fetch(DATA_URL);
|
||||
if (!resp.ok) throw new Error(`HTTP ${{resp.status}}`);
|
||||
const data = await resp.json();
|
||||
statusEl.textContent = '正在构建3D场景...';
|
||||
|
||||
await new Promise(r => setTimeout(r, 30));
|
||||
|
||||
buildScene(
|
||||
data.geometry || {{}},
|
||||
data.cavity || null,
|
||||
data.pointcloud || null
|
||||
);
|
||||
|
||||
statusEl.textContent = '完成';
|
||||
document.getElementById('loading-overlay').classList.add('hidden');
|
||||
}} catch (err) {{
|
||||
console.error('数据加载失败:', err);
|
||||
statusEl.textContent = '加载失败: ' + err.message;
|
||||
statusEl.style.color = '#F44336';
|
||||
}}
|
||||
}}
|
||||
|
||||
function animate() {{
|
||||
requestAnimationFrame(animate);
|
||||
controls.update();
|
||||
renderer.render(scene, camera);
|
||||
}}
|
||||
|
||||
loadData().then(() => animate());
|
||||
|
||||
window.addEventListener('resize', () => {{
|
||||
camera.aspect = window.innerWidth / window.innerHeight;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
}});
|
||||
|
||||
window.resetView = function() {{
|
||||
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.25; partingMesh.visible = true; }}
|
||||
productVisible = true; moldVisible = true; partingVisible = true;
|
||||
document.getElementById('btn-product').classList.add('active');
|
||||
document.getElementById('btn-mold').classList.add('active');
|
||||
document.getElementById('btn-parting').classList.add('active');
|
||||
fitCameraToScene();
|
||||
}};
|
||||
|
||||
window.toggleWireframe = function() {{
|
||||
scene.traverse(child => {{ if (child.isMesh) child.material.wireframe = !child.material.wireframe; }});
|
||||
}};
|
||||
|
||||
window.toggleProduct = function() {{
|
||||
if (!productMesh && !pointcloudMesh) return;
|
||||
productVisible = !productVisible;
|
||||
if (productMesh) productMesh.visible = productVisible;
|
||||
document.getElementById('btn-product').classList.toggle('active', productVisible);
|
||||
}};
|
||||
|
||||
window.toggleMold = function() {{
|
||||
moldVisible = !moldVisible;
|
||||
if (cavityMesh) cavityMesh.visible = moldVisible;
|
||||
if (coreMesh) coreMesh.visible = moldVisible;
|
||||
document.getElementById('btn-mold').classList.toggle('active', moldVisible);
|
||||
}};
|
||||
|
||||
window.toggleParting = function() {{
|
||||
partingVisible = !partingVisible;
|
||||
if (partingMesh) partingMesh.visible = partingVisible;
|
||||
document.getElementById('btn-parting').classList.toggle('active', partingVisible);
|
||||
}};
|
||||
|
||||
window.togglePointcloud = function() {{
|
||||
pointcloudVisible = !pointcloudVisible;
|
||||
if (pointcloudMesh) pointcloudMesh.visible = pointcloudVisible;
|
||||
document.getElementById('btn-pointcloud').classList.toggle('active', pointcloudVisible);
|
||||
}};
|
||||
|
||||
function getPartingDirection() {{
|
||||
if (cavityDataGlobal?.metadata?.parting_direction) return cavityDataGlobal.metadata.parting_direction;
|
||||
if (cavityDataGlobal?.manufacturing_info?.parting_direction) return cavityDataGlobal.manufacturing_info.parting_direction;
|
||||
if (cavityDataGlobal?.metadata?.is_foam) return 'Z';
|
||||
return 'Z';
|
||||
}}
|
||||
|
||||
window.splitMold = function() {{
|
||||
if (!cavityMesh && !coreMesh) return;
|
||||
isSplit = !isSplit;
|
||||
const btn = document.getElementById('splitBtn');
|
||||
btn.textContent = isSplit ? '合模' : '分模拆分';
|
||||
|
||||
const dir = getPartingDirection();
|
||||
let splitDist, axis;
|
||||
if (dir === 'Z') {{ splitDist = (sceneBox ? sceneBox.getSize(new THREE.Vector3()).z : 100) * 0.4; axis = 'z'; }}
|
||||
else if (dir === 'Y') {{ splitDist = (sceneBox ? sceneBox.getSize(new THREE.Vector3()).y : 100) * 0.4; axis = 'y'; }}
|
||||
else {{ splitDist = (sceneBox ? sceneBox.getSize(new THREE.Vector3()).x : 100) * 0.4; axis = 'x'; }}
|
||||
|
||||
const partingTargetOpacity = isSplit ? 0 : 0.25;
|
||||
const cavityStart = cavityMesh ? cavityMesh.position[axis] : 0;
|
||||
const coreStart = coreMesh ? coreMesh.position[axis] : 0;
|
||||
const partingStartOpacity = partingMesh ? partingMesh.material.opacity : 0.25;
|
||||
const cavityTarget = isSplit ? splitDist : 0;
|
||||
const coreTarget = isSplit ? -splitDist : 0;
|
||||
|
||||
const duration = 900;
|
||||
const startTime = performance.now();
|
||||
if (splitAnimId) cancelAnimationFrame(splitAnimId);
|
||||
|
||||
function animateSplit(now) {{
|
||||
const elapsed = now - startTime;
|
||||
const t = Math.min(elapsed / duration, 1);
|
||||
const ease = t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
|
||||
if (cavityMesh) cavityMesh.position[axis] = cavityStart + (cavityTarget - cavityStart) * ease;
|
||||
if (coreMesh) coreMesh.position[axis] = coreStart + (coreTarget - coreStart) * ease;
|
||||
if (partingMesh) {{
|
||||
partingMesh.material.opacity = partingStartOpacity + (partingTargetOpacity - partingStartOpacity) * ease;
|
||||
partingMesh.visible = !(isSplit && t >= 1);
|
||||
}}
|
||||
if (t < 1) splitAnimId = requestAnimationFrame(animateSplit);
|
||||
else splitAnimId = null;
|
||||
}}
|
||||
splitAnimId = requestAnimationFrame(animateSplit);
|
||||
}};
|
||||
</script>
|
||||
</body>
|
||||
</html>"""
|
||||
return html_content
|
||||
|
||||
def _build_cavity_info_panel_template(self) -> str:
|
||||
"""构建型腔信息面板 — 由JS动态填充,这里放置容器"""
|
||||
return """
|
||||
<div id="cavity-info-panel">
|
||||
<h3>🔧 关键工艺参数</h3>
|
||||
<div style="margin: 10px 0; border-bottom: 1px solid rgba(255,255,255,0.1); padding-bottom: 5px;">
|
||||
<strong style="color: #FF9800;">模具参数</strong>
|
||||
</div>
|
||||
<div class="metric"><span class="metric-label">收缩率</span><span class="metric-value" id="cp-shrink">-</span></div>
|
||||
<div class="metric"><span class="metric-label">拔模角</span><span class="metric-value" id="cp-draft">-</span></div>
|
||||
<div class="metric"><span class="metric-label">分型线长度</span><span class="metric-value" id="cp-parting">-</span></div>
|
||||
<div style="margin: 10px 0; border-bottom: 1px solid rgba(255,255,255,0.1); padding-bottom: 5px;">
|
||||
<strong style="color: #FF9800;">几何特性</strong>
|
||||
</div>
|
||||
<div class="metric"><span class="metric-label">产品体积</span><span class="metric-value" id="cp-vol">-</span></div>
|
||||
<div class="metric"><span class="metric-label">产品重量</span><span class="metric-value" id="cp-weight">-</span></div>
|
||||
<div class="metric"><span class="metric-label">壁厚范围</span><span class="metric-value" id="cp-wall">-</span></div>
|
||||
<div style="margin: 10px 0; border-bottom: 1px solid rgba(255,255,255,0.1); padding-bottom: 5px;">
|
||||
<strong style="color: #FF9800;">制造要求</strong>
|
||||
</div>
|
||||
<div class="metric"><span class="metric-label">模仁材料</span><span class="metric-value" id="cp-material">-</span></div>
|
||||
<div class="metric"><span class="metric-label">硬度</span><span class="metric-value" id="cp-hardness">-</span></div>
|
||||
<div class="metric"><span class="metric-label">表面光洁度</span><span class="metric-value" id="cp-finish">-</span></div>
|
||||
<div class="metric"><span class="metric-label">预估周期</span><span class="metric-value" id="cp-cycle">-</span></div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
def generate_3d_viewer_data(
|
||||
self,
|
||||
geometry_data: Dict[str, Any],
|
||||
cavity_data: Optional[Dict[str, Any]] = None,
|
||||
pointcloud_data: Optional[Dict[str, Any]] = None,
|
||||
lod_data: Optional[Dict[str, Any]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""生成companion JSON数据文件内容,支持多级LOD"""
|
||||
pc = dict(pointcloud_data) if pointcloud_data else {}
|
||||
if lod_data and lod_data.get("lods"):
|
||||
pc["lods"] = lod_data["lods"]
|
||||
data = {
|
||||
"version": "4.0.0",
|
||||
"generated_at": datetime.now().isoformat(),
|
||||
"geometry": geometry_data,
|
||||
"cavity": cavity_data,
|
||||
"pointcloud": pc if pc else pointcloud_data,
|
||||
}
|
||||
return data
|
||||
|
||||
def save_html_file(self, html_content: str, filename: str) -> str:
|
||||
"""保存HTML文件到磁盘"""
|
||||
try:
|
||||
file_path = self.output_dir / filename
|
||||
file_path.write_text(html_content, encoding='utf-8')
|
||||
logger.info(f"HTML文件保存成功: {file_path}")
|
||||
return str(file_path)
|
||||
except Exception as e:
|
||||
logger.error(f"保存HTML文件失败: {e}")
|
||||
raise
|
||||
|
||||
def save_data_file(self, data_content: Dict[str, Any], filename: str) -> str:
|
||||
"""保存JSON数据文件到磁盘 — 使用orjson高速序列化"""
|
||||
try:
|
||||
file_path = self.output_dir / filename
|
||||
file_path.write_bytes(_json_dumps(data_content))
|
||||
logger.info(f"数据文件保存成功: {file_path} (orjson={_JSON_FAST})")
|
||||
return str(file_path)
|
||||
except Exception as e:
|
||||
logger.error(f"保存数据文件失败: {e}")
|
||||
raise
|
||||
|
||||
def generate_and_save_visualization(
|
||||
self,
|
||||
geometry_data: Dict[str, Any],
|
||||
stp_filename: str,
|
||||
cavity_data: Optional[Dict[str, Any]] = None,
|
||||
pointcloud_data: Optional[Dict[str, Any]] = None,
|
||||
suffix: Optional[str] = None,
|
||||
lod_data: Optional[Dict[str, Any]] = None,
|
||||
) -> str:
|
||||
"""生成并保存可视化HTML + companion JSON数据文件。返回HTML文件路径(向后兼容)"""
|
||||
try:
|
||||
base_stem = Path(stp_filename).stem.replace(" ", "_")
|
||||
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
suffix_part = f"_{suffix}" if suffix else ""
|
||||
base_name = f"mold_{base_stem}{suffix_part}_{ts}"
|
||||
|
||||
html_filename = f"{base_name}.html"
|
||||
data_filename = f"{base_name}_data.json"
|
||||
|
||||
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_file_path = self.save_html_file(html_content, html_filename)
|
||||
|
||||
return html_file_path
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"生成可视化文件失败: {e}")
|
||||
raise
|
||||
Reference in New Issue
Block a user