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 启用)")
|
logger.info("FreeCAD验证已禁用(设置 ENABLE_FREECAD_VERIFICATION=true 启用)")
|
||||||
verification_result = {"status": "disabled", "reason": "FreeCAD验证已禁用"}
|
verification_result = {"status": "disabled", "reason": "FreeCAD验证已禁用"}
|
||||||
|
|
||||||
# 9.8 LLM 增强分析(可选,不影响主流程)
|
# 9.8 LLM 增强分析
|
||||||
llm_report = None
|
llm_report = None
|
||||||
llm_parting = None
|
|
||||||
if analysis_result:
|
if analysis_result:
|
||||||
llm_report = await llm_service.generate_design_report(
|
llm_report = await llm_service.generate_design_report(analysis_result, detailed_cavity_json)
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
# 10. 完成处理
|
# 10. 完成处理
|
||||||
await storage_service.update_stp_file_status(db_session, stp_file_id, "completed")
|
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]["html_file"] = f"/html/{Path(html_file_path).name}" # 只使用文件名
|
||||||
tasks[task_id]["verification"] = verification_result # 添加验证结果
|
tasks[task_id]["verification"] = verification_result # 添加验证结果
|
||||||
tasks[task_id]["llm_report"] = llm_report
|
tasks[task_id]["llm_report"] = llm_report
|
||||||
tasks[task_id]["llm_parting_recommendation"] = llm_parting
|
|
||||||
tasks[task_id]["status"] = ProcessingStatus.COMPLETED
|
tasks[task_id]["status"] = ProcessingStatus.COMPLETED
|
||||||
tasks[task_id]["completed_at"] = str(datetime.now())
|
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,
|
"confidence": 0.85,
|
||||||
"reasoning": "详细的中文推理过程...",
|
"reasoning": "详细的中文推理过程...",
|
||||||
"risk_notes": ["风险1", "风险2"],
|
"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 = """请评估以下候选分模方向并推荐最优方案:
|
_PARTING_USER = """请评估以下候选分模方向并推荐最优方案:
|
||||||
@@ -126,11 +126,7 @@ class LLMService:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
prompt = self._build_design_report_prompt(analysis_result, detailed_cavity_json)
|
prompt = self._build_design_report_prompt(analysis_result, detailed_cavity_json)
|
||||||
response = await self._chat(
|
response = await self._chat(_DESIGN_REPORT_SYSTEM, prompt, self._max_tokens)
|
||||||
system=_DESIGN_REPORT_SYSTEM,
|
|
||||||
user=prompt,
|
|
||||||
max_tokens=self._max_tokens,
|
|
||||||
)
|
|
||||||
if response:
|
if response:
|
||||||
logger.info("LLM 设计报告生成成功 (%d 字符)", len(response))
|
logger.info("LLM 设计报告生成成功 (%d 字符)", len(response))
|
||||||
return response
|
return response
|
||||||
@@ -150,128 +146,74 @@ class LLMService:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
prompt = self._build_parting_prompt(
|
prompt = self._build_parting_prompt(geometry_data, candidate_schemes, material, cavity_count)
|
||||||
geometry_data, candidate_schemes, material, cavity_count,
|
response = await self._chat(_PARTING_SYSTEM, prompt, min(self._max_tokens, 1200), expect_json=True)
|
||||||
)
|
|
||||||
response = await self._chat(
|
|
||||||
system=_PARTING_SYSTEM,
|
|
||||||
user=prompt,
|
|
||||||
max_tokens=min(self._max_tokens, 1200),
|
|
||||||
expect_json=True,
|
|
||||||
)
|
|
||||||
if response:
|
if response:
|
||||||
result = self._parse_json_response(response)
|
result = self._parse_json_response(response)
|
||||||
if result:
|
if result:
|
||||||
logger.info(
|
logger.info("LLM 分型推荐: %s (%.2f)", result.get("recommended_axis", "?"), result.get("confidence", 0))
|
||||||
"LLM 分型推荐: %s (置信度 %.2f)",
|
|
||||||
result.get("recommended_axis", "?"),
|
|
||||||
result.get("confidence", 0),
|
|
||||||
)
|
|
||||||
return result
|
return result
|
||||||
return None
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning("LLM 分型推荐失败(不影响主流程): %s", e)
|
logger.warning("LLM 分型推荐失败(不影响主流程): %s", e)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _build_design_report_prompt(
|
def _build_design_report_prompt(self, analysis_result, detailed_cavity_json) -> str:
|
||||||
self,
|
features = json.dumps(analysis_result.get("detected_features", []), ensure_ascii=False, indent=2)
|
||||||
analysis_result: Dict[str, Any],
|
if len(features) > 4000:
|
||||||
detailed_cavity_json: Optional[Dict[str, Any]],
|
features = features[:4000] + "\n... (已截断)"
|
||||||
) -> 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... (已截断)"
|
|
||||||
|
|
||||||
schemes_text = ""
|
schemes_text = ""
|
||||||
if detailed_cavity_json:
|
if detailed_cavity_json:
|
||||||
schemes = detailed_cavity_json.get("candidate_schemes", [])
|
schemes = detailed_cavity_json.get("candidate_schemes", [])
|
||||||
if schemes:
|
if schemes:
|
||||||
schemes_text = json.dumps(
|
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"),
|
||||||
"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"),
|
|
||||||
"parting_axis": s.get("parting", {}).get("axis"),
|
"parting_axis": s.get("parting", {}).get("axis"),
|
||||||
"mold_structure_type": s.get("mold_structure_type"),
|
"mold_structure_type": s.get("mold_structure_type"),
|
||||||
"dfm_violations": s.get("dfm_violations", []),
|
"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 = (
|
best = detailed_cavity_json.get("candidate_schemes", [{}])[0] if detailed_cavity_json else {}
|
||||||
detailed_cavity_json.get("candidate_schemes", [{}])[0]
|
cd = best.get("cavity_data", {}) if isinstance(best, dict) else {}
|
||||||
if detailed_cavity_json
|
mfg = cd.get("manufacturing_info", {})
|
||||||
else {}
|
meta = cd.get("metadata", {})
|
||||||
)
|
|
||||||
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", {})
|
|
||||||
|
|
||||||
return _DESIGN_REPORT_USER.format(
|
return _DESIGN_REPORT_USER.format(
|
||||||
filename=metadata.get("file_name", "unknown.stp"),
|
filename=meta.get("file_name", "unknown.stp"),
|
||||||
material=metadata.get("selected_material", "ABS"),
|
material=meta.get("selected_material", "ABS"),
|
||||||
volume=f"{analysis_result.get('geometry_data', {}).get('volume', 0):.1f} mm³",
|
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²",
|
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),
|
bbox=json.dumps(analysis_result.get("geometry_data", {}).get("bounding_box", {}), ensure_ascii=False),
|
||||||
features=feature_text or "无特征检测数据",
|
features=features or "无特征检测数据",
|
||||||
quality_metrics=json.dumps(quality_metrics, ensure_ascii=False, indent=2),
|
quality_metrics=json.dumps(analysis_result.get("quality_metrics", {}), ensure_ascii=False, indent=2),
|
||||||
schemes=schemes_text or "无分模方案数据",
|
schemes=schemes_text or "无分模方案数据",
|
||||||
mold_material=mfg_info.get("mold_material", "自动选择"),
|
mold_material=mfg.get("mold_material", "自动选择"),
|
||||||
mold_hardness=mfg_info.get("mold_hardness", "自动选择"),
|
mold_hardness=mfg.get("mold_hardness", "自动选择"),
|
||||||
clamping_force=mfg_info.get("estimated_clamping_force", "自动计算"),
|
clamping_force=mfg.get("estimated_clamping_force", "自动计算"),
|
||||||
mold_size=json.dumps(mfg_info.get("estimated_mold_size", {}), ensure_ascii=False),
|
mold_size=json.dumps(mfg.get("estimated_mold_size", {}), ensure_ascii=False),
|
||||||
cycle_time=mfg_info.get("estimated_cycle_time", "自动计算"),
|
cycle_time=mfg.get("estimated_cycle_time", "自动计算"),
|
||||||
draft_angle=f"{metadata.get('draft_angle', 2.0)}°",
|
draft_angle=f"{meta.get('draft_angle', 2.0)}°",
|
||||||
shrinkage_rate=f"{metadata.get('shrinkage_rate', 0.0) * 100:.2f}%"
|
shrinkage_rate="自动计算",
|
||||||
if isinstance(metadata.get("shrinkage_rate"), (int, float))
|
recommendations=json.dumps(analysis_result.get("design_recommendations", []), ensure_ascii=False, indent=2) or "无",
|
||||||
else "自动计算",
|
|
||||||
recommendations=json.dumps(recommendations, ensure_ascii=False, indent=2) if recommendations else "无",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def _build_parting_prompt(
|
def _build_parting_prompt(self, geometry_data, candidate_schemes, material, cavity_count) -> str:
|
||||||
self,
|
|
||||||
geometry_data: Dict[str, Any],
|
|
||||||
candidate_schemes: List[Dict[str, Any]],
|
|
||||||
material: Dict[str, Any],
|
|
||||||
cavity_count: int,
|
|
||||||
) -> str:
|
|
||||||
bbox = geometry_data.get("bounding_box", {})
|
bbox = geometry_data.get("bounding_box", {})
|
||||||
axis_normal_stats = geometry_data.get("axis_normal_stats", {})
|
axis_normal_stats = geometry_data.get("axis_normal_stats", {})
|
||||||
inertia = geometry_data.get("inertia_matrix", [])
|
inertia = geometry_data.get("inertia_matrix", [])
|
||||||
inertia_diag = [
|
inertia_diag = [inertia[i][i] if i < len(inertia) and i < len(inertia[i]) else 0.0 for i in range(3)]
|
||||||
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"),
|
"axis": s.get("parting", {}).get("axis") or s.get("axis"),
|
||||||
"score": s.get("score"),
|
"score": s.get("score"), "summary": s.get("summary"),
|
||||||
"confidence_score": s.get("confidence_score"),
|
|
||||||
"summary": s.get("summary"),
|
|
||||||
"mold_structure_type": s.get("mold_structure_type"),
|
"mold_structure_type": s.get("mold_structure_type"),
|
||||||
"core_required": s.get("core_required"),
|
"core_required": s.get("core_required"),
|
||||||
"dfm_violations": s.get("dfm_violations", []),
|
"dfm_violations": s.get("dfm_violations", []),
|
||||||
"undercut_regions_count": len(s.get("undercut_regions", [])),
|
"undercut_regions_count": len(s.get("undercut_regions", [])),
|
||||||
"score_breakdown": s.get("score_breakdown", {}),
|
"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(
|
return _PARTING_USER.format(
|
||||||
bbox=json.dumps(bbox, ensure_ascii=False),
|
bbox=json.dumps(bbox, ensure_ascii=False),
|
||||||
@@ -284,47 +226,31 @@ class LLMService:
|
|||||||
schemes=schemes_text,
|
schemes=schemes_text,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _chat(
|
async def _chat(self, system, user, max_tokens=2000, expect_json=False, temperature=0.3):
|
||||||
self,
|
|
||||||
system: str,
|
|
||||||
user: str,
|
|
||||||
max_tokens: int = 2000,
|
|
||||||
expect_json: bool = False,
|
|
||||||
temperature: float = 0.3,
|
|
||||||
) -> Optional[str]:
|
|
||||||
url = f"{self._api_url}/chat/completions"
|
url = f"{self._api_url}/chat/completions"
|
||||||
headers = {
|
headers = {"Authorization": f"Bearer {self._api_key}", "Content-Type": "application/json"}
|
||||||
"Authorization": f"Bearer {self._api_key}",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
}
|
|
||||||
payload = {
|
payload = {
|
||||||
"model": self._model,
|
"model": self._model,
|
||||||
"messages": [
|
"messages": [{"role": "system", "content": system}, {"role": "user", "content": user}],
|
||||||
{"role": "system", "content": system},
|
"max_tokens": max_tokens, "temperature": temperature,
|
||||||
{"role": "user", "content": user},
|
|
||||||
],
|
|
||||||
"max_tokens": max_tokens,
|
|
||||||
"temperature": temperature,
|
|
||||||
}
|
}
|
||||||
if expect_json:
|
if expect_json:
|
||||||
payload["response_format"] = {"type": "json_object"}
|
payload["response_format"] = {"type": "json_object"}
|
||||||
|
|
||||||
async with httpx.AsyncClient(timeout=self._timeout) as client:
|
async with httpx.AsyncClient(timeout=self._timeout) as client:
|
||||||
resp = await client.post(url, json=payload, headers=headers)
|
resp = await client.post(url, json=payload, headers=headers)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
data = resp.json()
|
content = resp.json()["choices"][0]["message"]["content"]
|
||||||
content = data["choices"][0]["message"]["content"]
|
|
||||||
return content.strip() if content else None
|
return content.strip() if content else None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _parse_json_response(raw: str) -> Optional[Dict[str, Any]]:
|
def _parse_json_response(raw):
|
||||||
try:
|
try:
|
||||||
return json.loads(raw)
|
return json.loads(raw)
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
match = re.search(r"\{[\s\S]*\}", raw)
|
m = re.search(r"\{[\s\S]*\}", raw)
|
||||||
if match:
|
if m:
|
||||||
try:
|
try:
|
||||||
return json.loads(match.group())
|
return json.loads(m.group())
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
pass
|
pass
|
||||||
logger.warning("LLM JSON 解析失败: %s...", raw[:200])
|
logger.warning("LLM JSON 解析失败: %s...", raw[:200])
|
||||||
|
|||||||
@@ -259,24 +259,10 @@ class ProcessingService:
|
|||||||
file_path, db_session, task_id, stp_file_id, analysis_result
|
file_path, db_session, task_id, stp_file_id, analysis_result
|
||||||
)
|
)
|
||||||
|
|
||||||
# 9.8 LLM 增强分析(可选,不影响主流程)
|
# 9.8 LLM 增强分析
|
||||||
llm_report = None
|
llm_report = None
|
||||||
llm_parting = None
|
|
||||||
if analysis_result:
|
if analysis_result:
|
||||||
llm_report = await llm_service.generate_design_report(
|
llm_report = await llm_service.generate_design_report(analysis_result, detailed_cavity_json)
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
# 10. 完成处理
|
# 10. 完成处理
|
||||||
await self.storage_service.update_stp_file_status(db_session, stp_file_id, "completed")
|
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}",
|
"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,
|
"verification": verification_result,
|
||||||
"llm_report": llm_report,
|
"llm_report": llm_report,
|
||||||
"llm_parting_recommendation": llm_parting,
|
|
||||||
"status": ProcessingStatus.COMPLETED,
|
"status": ProcessingStatus.COMPLETED,
|
||||||
"completed_at": str(datetime.now()),
|
"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