From 06b3e54a39cbe3a21bf79e06d6536f4c10919488 Mon Sep 17 00:00:00 2001 From: SZCJW <792430652@qq.com> Date: Sat, 2 May 2026 02:38:56 +0800 Subject: [PATCH] =?UTF-8?q?UI=E5=8D=87=E7=BA=A7=E6=94=B9=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.txt | 1 + src/core/mesh_generator.py | 165 +++- src/services/processing_service.py | 16 + src/utils/html_generator.py | 1203 ++++++++++++++-------------- 4 files changed, 760 insertions(+), 625 deletions(-) diff --git a/requirements.txt b/requirements.txt index b88df5b..9002e61 100644 --- a/requirements.txt +++ b/requirements.txt @@ -59,6 +59,7 @@ email-validator>=2.0.0 # 工具库 # ============================================ aiofiles>=23.0.0 +orjson>=3.9.0 python-dotenv>=1.0.0 jinja2>=3.1.0 pyyaml>=6.0 diff --git a/src/core/mesh_generator.py b/src/core/mesh_generator.py index 5111e0b..acc1426 100644 --- a/src/core/mesh_generator.py +++ b/src/core/mesh_generator.py @@ -14,7 +14,7 @@ logger = logging.getLogger(__name__) class MeshGenerator: - """网格生成器 - 从PythonOCC形状生成点云""" + """网格生成器 - 从PythonOCC形状生成点云,支持多级LOD""" def __init__(self, quality: str = "medium"): self.quality_settings = { @@ -27,99 +27,84 @@ class MeshGenerator: def generate_mesh_from_shape(self, shape, num_points: int = 20000) -> Dict: """从PythonOCC形状生成点云数据""" try: - # 生成网格 - 使用更精细的网格 mesh = BRepMesh_IncrementalMesh(shape, self.quality, False, 0.5, True) mesh.Perform() logger.info(f"OCC网格生成完成, 网格状态: {mesh.IsDone()}") - - # 提取三角形面数据 + all_vertices = [] all_faces = [] vertex_offset = 0 - - # 遍历所有面 + explorer = TopExp_Explorer(shape, TopAbs_FACE) face_count = 0 - + while explorer.More(): face = explorer.Current() face_count += 1 - - # 获取面的三角形剖分 + location = TopLoc_Location() face_triangulation = BRep_Tool.Triangulation(face, location) - + if face_triangulation is None: logger.warning(f"面 {face_count} 没有三角剖分数据") explorer.Next() continue - - # 获取变换矩阵 + trsf = location.Transformation() - - # 获取顶点数量 + nb_nodes = face_triangulation.NbNodes() nb_triangles = face_triangulation.NbTriangles() - + logger.info(f"面 {face_count}: {nb_nodes} 个顶点, {nb_triangles} 个三角形") - - # 提取顶点并应用变换 + face_vertices = [] for i in range(1, nb_nodes + 1): pnt = face_triangulation.Node(i) - # 使用变换后的拷贝点,避免潜在的原地变换副作用 transformed = pnt.Transformed(trsf) face_vertices.append([ float(transformed.X()), float(transformed.Y()), float(transformed.Z()), ]) - - # 提取三角形索引 + face_indices = [] for i in range(1, nb_triangles + 1): tri = face_triangulation.Triangle(i) - # 三角形索引从1开始,需要转换为从0开始 idx1 = tri.Value(1) idx2 = tri.Value(2) idx3 = tri.Value(3) - # 转换为全局索引 face_indices.append([ vertex_offset + idx1 - 1, vertex_offset + idx2 - 1, vertex_offset + idx3 - 1 ]) - + all_vertices.extend(face_vertices) all_faces.extend(face_indices) vertex_offset += len(face_vertices) - + explorer.Next() - + if len(all_vertices) == 0: logger.warning("未提取到任何顶点,使用示例数据") return self._create_sample_pointcloud() - + vertices = np.array(all_vertices, dtype=np.float32) faces = np.array(all_faces, dtype=np.int32) - + logger.info(f"总共提取了 {len(vertices)} 个顶点, {len(faces)} 个三角形面, {face_count} 个面") - - # 创建Trimesh对象 + tri_mesh = trimesh.Trimesh(vertices=vertices, faces=faces, process=True) - - # 根据网格大小动态调整采样点数 + actual_num_points = min(num_points, len(faces) * 2) logger.info(f"采样点数: {actual_num_points}") - - # 在网格表面采样点云 + points, face_idx = trimesh.sample.sample_surface(tri_mesh, actual_num_points) - - # 获取法向量 + normals = tri_mesh.face_normals[face_idx] - + logger.info(f"生成了 {len(points)} 个点云点") - + return { "vertices": vertices.tolist(), "faces": faces.tolist(), @@ -129,19 +114,121 @@ class MeshGenerator: "vertex_count": int(len(vertices)), "face_count": int(len(faces)) } - + except Exception as e: logger.error(f"网格生成失败: {e}") import traceback logger.error(traceback.format_exc()) return self._create_sample_pointcloud() + def generate_multi_lod_mesh(self, shape) -> Dict: + """生成多级LOD网格 - 一次OCC剖分,trimesh简化,避免重复计算 + + 返回结构: + { + "lods": { + "0": { "vertices": [...], "faces": [...], "vertex_count": N, "face_count": N }, + "1": { ... 50%简化 ... }, + "2": { ... 80%简化 ... } + }, + "points": [...], "normals": [...], "point_count": N, + "vertex_count": N, "face_count": N + } + """ + try: + full_mesh_result = self.generate_mesh_from_shape(shape, num_points=20000) + + vertices = np.array(full_mesh_result["vertices"], dtype=np.float32) + faces = np.array(full_mesh_result["faces"], dtype=np.int32) + + if len(vertices) == 0 or len(faces) == 0: + sample = self._create_sample_pointcloud() + return self._wrap_sample_as_lod(sample) + + tri_mesh = trimesh.Trimesh(vertices=vertices, faces=faces, process=True) + full_face_count = len(tri_mesh.faces) + logger.info(f"全精度网格: {len(tri_mesh.vertices)} 顶点, {full_face_count} 面") + + lods = { + "0": self._mesh_to_lod_entry(tri_mesh, "LOD0-全精度") + } + + lod_ratios = {"1": 0.50, "2": 0.20} + for lod_level, ratio in lod_ratios.items(): + if full_face_count < 300: + lods[lod_level] = lods["0"] + continue + + target_faces = max(int(full_face_count * ratio), 200) + try: + simplified = tri_mesh.simplify_quadric_decimation(target_faces) + if simplified is None or len(simplified.faces) < 3: + simplified = self._fast_decimate(tri_mesh, target_faces) + lods[lod_level] = self._mesh_to_lod_entry(simplified, f"LOD{lod_level}-简化{int((1-ratio)*100)}%") + logger.info(f"LOD{lod_level}: {len(simplified.vertices)} 顶点, {len(simplified.faces)} 面 (目标{target_faces})") + except Exception as dec_err: + logger.warning(f"LOD{lod_level} 简化失败,回退到全精度: {dec_err}") + lods[lod_level] = lods["0"] + + result = { + "lods": lods, + "points": full_mesh_result["points"], + "normals": full_mesh_result["normals"], + "point_count": full_mesh_result["point_count"], + "vertex_count": full_mesh_result["vertex_count"], + "face_count": full_mesh_result["face_count"], + } + return result + + except Exception as e: + logger.error(f"多级LOD网格生成失败: {e}") + import traceback + logger.error(traceback.format_exc()) + sample = self._create_sample_pointcloud() + return self._wrap_sample_as_lod(sample) + + def _mesh_to_lod_entry(self, mesh: trimesh.Trimesh, label: str) -> Dict: + return { + "vertices": mesh.vertices.tolist(), + "faces": mesh.faces.tolist(), + "vertex_count": int(len(mesh.vertices)), + "face_count": int(len(mesh.faces)), + } + + def _fast_decimate(self, mesh: trimesh.Trimesh, target_faces: int) -> trimesh.Trimesh: + """快速回退降采样:按面索引均匀采样""" + if target_faces >= len(mesh.faces): + return mesh + step = max(len(mesh.faces) // target_faces, 1) + indices = np.arange(0, len(mesh.faces), step)[:target_faces] + return mesh.submesh([np.array(indices)], only_watertight=False, append=True) + + def _wrap_sample_as_lod(self, sample: Dict) -> Dict: + lods = { + "0": { + "vertices": sample["vertices"], + "faces": sample["faces"], + "vertex_count": sample["vertex_count"], + "face_count": sample["face_count"], + } + } + lods["1"] = lods["0"] + lods["2"] = lods["0"] + return { + "lods": lods, + "points": sample["points"], + "normals": sample["normals"], + "point_count": sample["point_count"], + "vertex_count": sample["vertex_count"], + "face_count": sample["face_count"], + } + def _create_sample_pointcloud(self) -> Dict: """创建示例点云(备用)""" mesh = trimesh.creation.box([100, 80, 50]) points, _ = trimesh.sample.sample_surface(mesh, 5000) normals = mesh.face_normals[:len(points)] - + return { "vertices": mesh.vertices.tolist(), "faces": mesh.faces.tolist(), diff --git a/src/services/processing_service.py b/src/services/processing_service.py index b2075a7..91f7d20 100644 --- a/src/services/processing_service.py +++ b/src/services/processing_service.py @@ -176,6 +176,7 @@ class ProcessingService: ) pointcloud_data = None + lod_data = None if mesh_result: pointcloud_data = { "points": mesh_result.get("points", []), @@ -183,13 +184,25 @@ class ProcessingService: "vertices": mesh_result.get("vertices", []), "faces": mesh_result.get("faces", []), "point_count": mesh_result.get("point_count", 0), + "vertex_count": mesh_result.get("vertex_count", 0), + "face_count": mesh_result.get("face_count", 0), } + # 生成多级LOD数据(用于前端按距离切换精度) + try: + lod_result = self.mesh_generator.generate_multi_lod_mesh(shape) + if lod_result and lod_result.get("lods"): + lod_data = lod_result + logger.info(f"LOD数据生成成功: {len(lod_result['lods'])} 级 (面数: {[lod_result['lods'][k]['face_count'] for k in sorted(lod_result['lods'].keys())]})") + except Exception as lod_err: + logger.warning(f"LOD数据生成失败,使用单级精度: {lod_err}") + detailed_cavity_json = await self._attach_scheme_previews( detailed_cavity_json=detailed_cavity_json, geometry_data=geometry_data, stp_filename=Path(file_path).name, pointcloud_data=pointcloud_data, + lod_data=lod_data, ) best_scheme = CalculationService.get_best_scheme(detailed_cavity_json) @@ -206,6 +219,7 @@ class ProcessingService: Path(file_path).name, cavity_data=best_cavity_data, pointcloud_data=pointcloud_data, + lod_data=lod_data, ) await self.storage_service.save_html_file( @@ -405,6 +419,7 @@ class ProcessingService: geometry_data: Dict[str, Any], stp_filename: str, pointcloud_data: Optional[Dict[str, Any]] = None, + lod_data: Optional[Dict[str, Any]] = None, ) -> Dict[str, Any]: """为每个候选分模方案生成独立HTML预览链接。""" candidate_schemes = detailed_cavity_json.get("candidate_schemes", []) @@ -422,6 +437,7 @@ class ProcessingService: cavity_data=cavity_data, pointcloud_data=pointcloud_data, suffix=suffix, + lod_data=lod_data, ) scheme["html_file"] = f"/html/{Path(html_path).name}" diff --git a/src/utils/html_generator.py b/src/utils/html_generator.py index 9067266..2bd5611 100644 --- a/src/utils/html_generator.py +++ b/src/utils/html_generator.py @@ -1,279 +1,274 @@ # utils/html_generator.py from pathlib import Path from typing import Dict, Any, Optional -import json 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文件生成器""" - + """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, - geometry_data: Dict[str, Any], - stp_filename: str, - cavity_data: Optional[Dict[str, Any]] = None, - pointcloud_data: Optional[Dict[str, Any]] = None - ) -> str: - """生成3D可视化HTML页面""" - - # 提取几何数据 - bounding_box = geometry_data.get("bounding_box", {}) - volume = geometry_data.get("volume", 0) or 0 - surface_area = geometry_data.get("surface_area", 0) or 0 - topology = geometry_data.get("topology", {}) - center_of_mass = geometry_data.get("center_of_mass", [0, 0, 0]) - cavity_html = "" - # 强制测试:无论cavity_data如何,都显示面板 - if True: - # 安全获取嵌套数据 - metadata = cavity_data.get("metadata", {}) if cavity_data else {} - manufacturing_info = cavity_data.get("manufacturing_info", {}) if cavity_data else {} - mold_cavities = cavity_data.get("mold_cavities", {}) if cavity_data else {} - cavity_key_info = mold_cavities.get("cavity_key_info", {}) - geo_chars = cavity_key_info.get("geometric_characteristics", {}) - logger.info(f"生成HTML - 收缩率: {metadata.get('shrinkage_rate')}") - logger.info(f"生成HTML - 型腔材料: {manufacturing_info.get('mold_material')}") - logger.info(f"生成HTML - 产品体积: {geo_chars.get('product_volume')}") + def generate_3d_viewer_html(self, stp_filename: str, data_filename: str) -> str: + """生成3D可视化HTML页面 — 通过fetch异步加载companion JSON数据""" - cavity_html = f""" -