from PyQt5.QtCore import QObject, pyqtSlot import open3d as o3d import numpy as np from stl import mesh as stl_mesh import os from trimesh import Trimesh from OCC.Core.TopoDS import TopoDS_Face class Datahandle(QObject): def __init__(self): super().__init__() self.qml_item = None self.vertices = np.array([]) self.colors = np.array([]) self.triangles = np.array([]) # 新增:三角面索引 self.normals = np.array([]) # 新增:法线 def load_data(self): """ 加载 3D 数据(这里用随机点云代替实际文件) 返回: vertices, colors (numpy arrays) """ pcd = o3d.geometry.PointCloud() pcd.points = o3d.utility.Vector3dVector(np.random.rand(5000, 3)) pcd.colors = o3d.utility.Vector3dVector(np.random.rand(5000, 3)) vertices = np.asarray(pcd.points) colors = np.asarray(pcd.colors) return vertices, colors @pyqtSlot('QVariant') def set3DItem(self, item): """接收 QML 中的 QML3DItem 实例""" self.qml_item = item if self.qml_item: self.qml_item.set_data(self.vertices, self.colors) print("✅ 数据已传递给 QML3DItem") @pyqtSlot(float, float, float) def onPointPicked(self, x, y, z): """接收从 QML3DItem 发出的拾取信号""" print(f"✅ 拾取到点: ({x:.3f}, {y:.3f}, {z:.3f})") # 这里可以扩展:保存坐标、发送到其他模块等 class Dataload(QObject): def __init__(self): super().__init__() self.qml_item = None self.vertices = np.array([]) self.colors = np.array([]) self.original_colors = np.array([]) self.triangles = np.array([]) # 新增:三角面索引 self.normals = np.array([]) # 新增:法线 self.picking_color_map = {} self.face_normals = np.array([]) # 面法线 def load_data_from_file(self, file_path: str): """ 使用 numpy-stl 自动识别 STL 格式,并提取轮廓线(边界边 + 锐角边)。 """ print(f"📁 正在加载: {file_path}") if not os.path.exists(file_path): print(f"❌ 文件不存在: {file_path}") return np.array([]), np.array([]), np.array([]), np.array([]), np.array([]) ext = os.path.splitext(file_path)[1].lower() if ext != '.stl': print(f"❌ 不支持的格式: {ext}") return np.array([]), np.array([]), np.array([]), np.array([]), np.array([]) try: # ✅ 自动识别格式(旧版本也支持) stl_data = stl_mesh.Mesh.from_file(file_path) # 获取三角面 (N, 3, 3) vectors = stl_data.vectors.astype(np.float32) print(f"✅ 加载 {len(vectors)} 个三角面") # 展平顶点 vertices = vectors.reshape(-1, 3) print(f"原始顶点数: {len(vertices)}") # 三角面索引 num_triangles = len(vectors) triangles = np.arange(num_triangles * 3, dtype=np.int32).reshape(-1, 3) print(f"三角面数量: {num_triangles}") # 法线(每个三角面对应一个法线) normals = stl_data.normals.astype(np.float32) # --- 归一化(保持不变)--- if len(vertices) > 0: min_coords = np.min(vertices, axis=0) max_coords = np.max(vertices, axis=0) center = (min_coords + max_coords) / 2.0 scale = np.max(max_coords - min_coords) if scale > 0: vertices = (vertices - center) / scale * 2.0 z_range = np.max(vertices[:, 2]) - np.min(vertices[:, 2]) if z_range < 0.1: z_center = (np.min(vertices[:, 2]) + np.max(vertices[:, 2])) / 2.0 vertices[:, 2] = (vertices[:, 2] - z_center) * (0.2 / z_range) + z_center print(f"调整Z轴范围: {z_range:.6f} -> 0.2") # =================================================================== # ✅ 新增:顶点去重 + 重建三角面索引 # =================================================================== print("🔍 正在合并重复顶点...") rounded_vertices = np.round(vertices, decimals=6) unique_vertices, unique_indices = np.unique(rounded_vertices, axis=0, return_inverse=True) # 重建 triangles:每个旧顶点 → 映射到新唯一顶点索引 new_triangles = unique_indices.reshape(-1, 3) # 每3个一组 vertices = unique_vertices.astype(np.float32) triangles = new_triangles.astype(np.int32) print(f"✅ 顶点数: {len(vertices)} (原: {len(unique_indices)})") print(f"✅ 三角面数: {len(triangles)}") # =================================================================== # ✅ 重新计算每个面的法线(用于边提取) # =================================================================== def compute_face_normal(v0, v1, v2): u = v1 - v0 v = v2 - v0 n = np.cross(u, v) norm = np.linalg.norm(n) return n / norm if norm > 1e-8 else np.array([0.0, 0.0, 1.0]) face_normals = [] for tri in triangles: v0, v1, v2 = vertices[tri[0]], vertices[tri[1]], vertices[tri[2]] face_normals.append(compute_face_normal(v0, v1, v2)) face_normals = np.array(face_normals) # =================================================================== # ✅ 构建边 -> 面映射(使用 (min, max) 保证方向一致) # =================================================================== from collections import defaultdict edge_faces = defaultdict(list) for tri_idx, tri in enumerate(triangles): a, b, c = tri edges = [ (min(a, b), max(a, b)), (min(b, c), max(b, c)), (min(c, a), max(c, a)) ] for edge in edges: if tri_idx not in edge_faces[edge]: edge_faces[edge].append(tri_idx) # =================================================================== # ✅ 提取轮廓线:边界边(1个面) + 锐角边(2个面且夹角大) # =================================================================== silhouette_edges = [] crease_threshold_rad = np.radians(30.0) # 30度 for (v0, v1), face_list in edge_faces.items(): if len(face_list) == 1: # 边界边 silhouette_edges.append([v0, v1]) elif len(face_list) == 2: # 锐角边 n1 = face_normals[face_list[0]] n2 = face_normals[face_list[1]] cos_angle = np.dot(n1, n2) if cos_angle < np.cos(crease_threshold_rad): silhouette_edges.append([v0, v1]) silhouette_edges = np.array(silhouette_edges, dtype=np.int32) print(f"✅ 提取 {len(silhouette_edges)} 条轮廓线(边界 + 折痕)") # =================================================================== # ✅ 重建 vertex_normals 和 colors(基于新顶点) # =================================================================== print("🎨 重建顶点法线和颜色...") num_vertices = len(vertices) vertex_normals = np.zeros((num_vertices, 3), dtype=np.float32) # 构建顶点 -> 面索引 映射 vertex_face_map = [[] for _ in range(num_vertices)] for tri_idx, tri in enumerate(triangles): for v_idx in tri: vertex_face_map[v_idx].append(tri_idx) # 计算每个顶点的平均法线(取相邻面法线平均) for i in range(num_vertices): if vertex_face_map[i]: avg_normal = np.mean(face_normals[vertex_face_map[i]], axis=0) norm = np.linalg.norm(avg_normal) if norm > 1e-8: vertex_normals[i] = avg_normal / norm else: vertex_normals[i] = np.array([0.0, 0.0, 1.0]) else: vertex_normals[i] = np.array([0.0, 0.0, 1.0]) # 生成颜色:法线映射到 [0,1] colors = (vertex_normals + 1.0) / 2.0 colors = np.clip(colors, 0.0, 1.0) if np.any(np.isnan(colors)) or np.any(np.isinf(colors)): colors = np.ones_like(vertex_normals) * 0.8 # 保存原始颜色,防止被高亮逻辑破坏 self.original_colors = self.colors.copy() # 可选:用于恢复 print(f"✅ 顶点法线数量: {len(vertex_normals)}") print(f"✅ 颜色数量: {len(colors)}") # =================================================================== # ✅ 加载成功 # =================================================================== print(f"✅ 加载成功: {file_path}") self.face_normals = face_normals self._build_picking_color_map() # print(f"131313131://///////////////////////////{vertices},{colors},{triangles},{vertex_normals},{silhouette_edges}") return vertices, colors, triangles, vertex_normals, silhouette_edges except Exception as e: print(f"❌ 加载失败: {e}") return np.array([]), np.array([]), np.array([]), np.array([]), np.array([]) def _build_picking_color_map(self): self.picking_color_map.clear() for i in range(len(self.vertices)): r, g, b = self._get_picking_color(i) key = (round(r, 5), round(g, 5), round(b, 5)) self.picking_color_map[key] = i print("✅ picking_color_map 构建完成") # 调试:打印前几个 print("📊 前5个映射:", list(self.picking_color_map.items())[:5]) def _get_picking_color(self, index): """根据顶点索引生成唯一颜色 (r,g,b 归一化到 0~1)""" idx = index + 1 # 避免 0,0,0 黑色 r = (idx) & 0xFF g = (idx >> 8) & 0xFF b = (idx >> 16) & 0xFF return (r / 255.0, g / 255.0, b / 255.0) def extract_edge_loops(vertices, triangles, boundary_only=False, crease_threshold_deg=30.0): """ 提取模型的边界边和/或锐角边(折痕边) 返回: list of [idx1, idx2] (顶点索引对) """ import numpy as np from collections import defaultdict # 构建边 -> 面列表 edge_faces = defaultdict(list) for tri_idx, (a, b, c) in enumerate(triangles): edges = [(a,b), (b,c), (c,a)] for u, v in edges: key = (min(u, v), max(u, v)) # 规范化边 edge_faces[key].append(tri_idx) # 计算每个面的法线 def compute_face_normal(v0, v1, v2): u = v1 - v0 v = v2 - v0 n = np.cross(u, v) norm = np.linalg.norm(n) return n / norm if norm > 1e-8 else np.array([0.0, 0.0, 1.0]) face_normals = [] for tri in triangles: v0, v1, v2 = vertices[tri[0]], vertices[tri[1]], vertices[tri[2]] face_normals.append(compute_face_normal(v0, v1, v2)) face_normals = np.array(face_normals) # 收集边 edges = [] threshold_rad = np.radians(crease_threshold_deg) for (v0, v1), face_list in edge_faces.items(): if len(face_list) == 1: # 边界边 edges.append([v0, v1]) elif not boundary_only and len(face_list) == 2: n1 = face_normals[face_list[0]] n2 = face_normals[face_list[1]] cos_angle = np.dot(n1, n2) if cos_angle < np.cos(threshold_rad): # 夹角大于阈值 edges.append([v0, v1]) return edges