Sfoglia il codice sorgente

提取模型折痕算法来夹逼出选色区域,面法向量改成点法向量加权平均渲染光滑曲面,问题:1. 2万面太卡 2. 曲面法线夹角不一致,圆弧表面有时夹逼不出整块区域

lstzcy 1 settimana fa
parent
commit
f608783dae

BIN
__pycache__/glWidget_simple.cpython-39.pyc


BIN
__pycache__/handle_simple.cpython-39.pyc


+ 296 - 54
glWidget_simple.py

@@ -17,13 +17,15 @@ class Simple3DWidget(QOpenGLWidget):
         self.colors = np.array([])  # 颜色数据
         self.triangles = np.array([], dtype=np.int32)  # 三角面数据 ← 新增
         self.normals = np.array([])  # 法线数据 ← 新增
+        self.edges = np.array([])  #轮廓边
+        self.face_normals = np.array([])
 
         self.rotation = [0.0, 0.0]  # 旋转角度 [俯仰, 偏航]
         self.zoom = -5.0  # 视距(负值表示拉远)
         self.pan = [0.0, 0.0]  # 平移偏移 [x, y]
         self.last_mouse_pos = None  # 鼠标位置
         self.setMouseTracking(True)  # 启用鼠标跟踪
-        self.display_mode = 'points'  # 默认显示模式: 点云
+        self.display_mode = 'surface'  # 默认显示模式: 点云
         self.axes_display_mode = False
         self.axes_world_display_mode = True
 
@@ -42,6 +44,10 @@ class Simple3DWidget(QOpenGLWidget):
         self.picking = False  # 是否处于拾取模式
         self.picking_color_map = {}  # 顶点索引 → 唯一颜色(用于反查)
 
+        #选色模式
+        self.set_color = False
+        self.highlighted_face_indices = []  # 存储所有要高亮的面索引
+
         self.setFont(QFont("SimHei", 10))  # 让 renderText 使用黑体
 
 
@@ -76,45 +82,37 @@ class Simple3DWidget(QOpenGLWidget):
         glMatrixMode(GL_MODELVIEW)
         glLoadIdentity()
 
-        # ✅ 使用 gluLookAt 构建轨道摄像机
-        # 视角参数
-        yaw = self.view_rotation[1]  # 偏航角(左右)
-        pitch = self.view_rotation[0]  # 俯仰角(上下)
+        # --- 设置摄像机 ---
+        yaw = self.view_rotation[1]
+        pitch = self.view_rotation[0]
         distance = self.view_distance
 
-        # 1. 计算摄像机位置(绕原点轨道)
-        radius = distance
-        cam_x = radius * np.cos(np.radians(yaw)) * np.cos(np.radians(pitch))
-        cam_y = radius * np.sin(np.radians(pitch))
-        cam_z = radius * np.sin(np.radians(yaw)) * np.cos(np.radians(pitch))
-
-        # 2. 目标点 = 世界原点 (0,0,0)
-        target_x, target_y, target_z = 0.0, 0.0, 0.0
-
-        # 3. 上方向(防止翻滚)
-        up_x, up_y, up_z = 0.0, 1.0, 0.0
+        cam_x = distance * np.cos(np.radians(yaw)) * np.cos(np.radians(pitch))
+        cam_y = distance * np.sin(np.radians(pitch))
+        cam_z = distance * np.sin(np.radians(yaw)) * np.cos(np.radians(pitch))
 
-        # ✅ 设置摄像机(绕原点轨道旋转)
         gluLookAt(
-            cam_x, cam_y, cam_z,  # 摄像机位置
-            target_x, target_y, target_z,  # 看向目标(世界原点)
-            up_x, up_y, up_z  # 上方向
+            cam_x, cam_y, cam_z,
+            0.0, 0.0, 0.0,
+            0.0, 1.0, 0.0
         )
 
-        # ✅ 应用视点平移(Ctrl+右键平移)
         glTranslatef(self.view_pan[0], self.view_pan[1], 0)
 
-        # ✅ 绘制世界坐标系(在原点)
+        # 绘制世界坐标轴
         if self.axes_world_display_mode:
             self.drawWorldAxes()
 
-        # ✅ 模型变换
+        # --- 模型变换 ---
         glPushMatrix()
         glTranslatef(self.model_pan[0], self.model_pan[1], 0)
         glRotatef(self.model_rotation[0], 1, 0, 0)
         glRotatef(self.model_rotation[1], 0, 1, 0)
         glScalef(self.model_scale, self.model_scale, self.model_scale)
 
+        # 所有轮廓线已在 load_data_from_file 中提取为 self.edges
+
+        # 绘制模型坐标轴
         if self.axes_display_mode:
             self.drawModelAxes()
 
@@ -123,67 +121,100 @@ class Simple3DWidget(QOpenGLWidget):
         glPopMatrix()
 
     def _draw_model(self):
-        """私有方法:绘制模型(含高亮)"""
+        """私有方法:绘制模型(含高亮 + 静态轮廓线)"""
         if len(self.vertices) == 0:
             return
 
         if self.display_mode == 'points':
-            glPointSize(8.0)
+            # ✅ 点云模式:只显示顶点
+            glDisable(GL_LIGHTING)
+            glPointSize(5.0)
+
             glBegin(GL_POINTS)
-            try:
-                for i, v in enumerate(self.vertices):
-                    # 判断是否是选中点
-                    if self.selected_point is not None and np.allclose(v, self.selected_point, atol=1e-6):
-                        glColor3f(1.0, 1.0, 0.0)  # 黄色高亮
+            for i, v in enumerate(self.vertices):
+                if self.selected_point is not None and np.allclose(v, self.selected_point, atol=1e-6):
+                    glColor3f(1.0, 1.0, 0.0)  # 黄色高亮
+                else:
+                    if i < len(self.colors):
+                        color = self.colors[i]
+                        glColor3f(color[0], color[1], color[2])
                     else:
-                        glColor3f(*self.colors[i])  # 原始颜色
-                    glVertex3f(v[0], v[1], v[2])
-            finally:
-                glEnd()  # ✅ 保证执行
+                        glColor3f(0.8, 0.8, 0.8)
+                glVertex3f(v[0], v[1], v[2])
+            glEnd()
 
-            # ✅ 在 glEnd() 之后再绘制文字!
+            # 额外高亮选中点
             if self.selected_point is not None:
-                x, y, z = self.selected_point
-                offset = 0.05
-                # 设置文字颜色
-                glColor3f(1.0, 1.0, 0.0)  # 黄色
-                self.renderText(x + offset, y + offset, z + offset, "Selected")
-
+                glPointSize(12.0)
+                glBegin(GL_POINTS)
+                glColor3f(1.0, 1.0, 0.0)
+                glVertex3f(self.selected_point[0], self.selected_point[1], self.selected_point[2])
+                glEnd()
 
         elif self.display_mode == 'surface' and len(self.triangles) > 0:
             # ✅ 开启光照
             glEnable(GL_LIGHTING)
             try:
-                # ✅ 预计算选中点的索引(避免在 glBegin 内部调用 np.allclose)
                 highlight_indices = set()
                 if self.selected_point is not None:
                     for idx, v in enumerate(self.vertices):
                         if np.allclose(v, self.selected_point, atol=1e-6):
                             highlight_indices.add(idx)
 
+                # === 绘制表面 ===
                 glBegin(GL_TRIANGLES)
                 try:
-                    for tri in self.triangles:
+                    # 将 triangles 转为 list of tuples 便于索引查找(只做一次)
+                    # 如果 self.triangles 是 numpy 数组,转换为列表
+                    triangles_list = [tuple(tri) for tri in self.triangles] if isinstance(self.triangles,
+                                                                                          np.ndarray) else self.triangles
+
+                    for tri_idx, tri in enumerate(self.triangles):
+                        # 判断当前三角形是否在高亮区域
+                        is_face_highlighted = (
+                                hasattr(self, 'highlighted_face_indices') and
+                                isinstance(self.highlighted_face_indices, (list, set)) and
+                                tri_idx in self.highlighted_face_indices
+                        )
+
                         for idx in tri:
-                            # 高亮三角形中包含选中点的顶点
-                            if idx in highlight_indices:
-                                glColor3f(1.0, 0.0, 0.0)
+                            # 优先级:共面高亮 > 选中点高亮 > 正常颜色
+                            if is_face_highlighted:
+                                glColor3f(1.0, 1.0, 1.0)  # 白色高亮(共面区域)
+                            elif idx in highlight_indices:
+                                glColor3f(1.0, 0.0, 0.0)  # 红色高亮(选中点)
                             else:
                                 glColor3f(*self.colors[idx])
 
-                            # 设置法线(如果存在且有效)
                             if len(self.normals) > idx:
                                 n = self.normals[idx]
                                 if not np.any(np.isnan(n)) and not np.any(np.isinf(n)):
                                     glNormal3f(*n)
 
                             glVertex3f(*self.vertices[idx])
+                except Exception as e:
+                    print(f"Error in drawing triangles: {e}")
                 finally:
-                    glEnd()  # ✅ 保证结束绘制
+                    glEnd()
+                # === 绘制轮廓线(静态,来自 self.edges)===
+                glDisable(GL_LIGHTING)
+                if len(self.edges) > 0:
+                    glLineWidth(2.5)
+                    glColor3f(0.0, 0.0, 0.0)  # 黑色轮廓线
+                    glBegin(GL_LINES)
+                    try:
+                        for edge in self.edges:
+                            v0 = self.vertices[edge[0]]
+                            v1 = self.vertices[edge[1]]
+                            glVertex3f(v0[0], v0[1], v0[2])
+                            glVertex3f(v1[0], v1[1], v1[2])
+                    finally:
+                        glEnd()
+
+                glEnable(GL_LIGHTING)
 
             finally:
-                glDisable(GL_LIGHTING)  # ✅ 保证关闭光照
-
+                glDisable(GL_LIGHTING)
     def renderText(self, x, y, z, text):
         """
         在指定的三维坐标位置渲染文本。
@@ -270,9 +301,8 @@ class Simple3DWidget(QOpenGLWidget):
 
         glPopMatrix()
 
-
-    def set_data(self, vertices, colors, triangles=None, normals=None):
-        """设置 3D 数据(支持 mesh)"""
+    def set_data(self, vertices, colors, triangles=None, normals=None, silhouette_edges=None):
+        """设置 3D 数据(支持 mesh 和轮廓线)"""
         self.vertices = np.array(vertices, dtype=np.float32)
         self.colors = np.array(colors, dtype=np.float32)
 
@@ -286,22 +316,211 @@ class Simple3DWidget(QOpenGLWidget):
         else:
             self.normals = np.array([])
 
+        if silhouette_edges is not None:
+            self.edges = np.array(silhouette_edges, dtype=np.int32)  # ← 存为 self.edges
+        else:
+            self.edges = np.array([])
+
+        # ✅ 修正:打印 self.edges,不是 self.silhouette_edges
         print(f"✅ 设置数据: {len(self.vertices)} 个顶点, {len(self.triangles)} 个三角面")
+        print(f"     轮廓线数量: {len(self.edges)} 条")  # ✅ 正确字段名
+
         self.update()
 
     def mousePressEvent(self, event):
         self.last_mouse_pos = event.pos()       #二维坐标,仅用来计算旋转/移动的量
         if event.button() == Qt.LeftButton:
+            if self.set_color == True:
+                self._do_color_pick(event.pos())
             if self.picking == True:
                 self._do_picking(event.pos())
         super().mousePressEvent(event)              #调用父类中默认的方法,获得预设的一些功能
 
+    def _do_color_pick(self, pos):
+        """
+        点击屏幕某点,找到对应的三角形,
+        然后通过“法线连通性”扩展出整个平面区域,
+        将该区域所有三角形的顶点颜色设为白色(或加深)
+        """
+        # 1. 射线拾取:获取被点击的三角形索引
+        picked_tri_idx = self._ray_cast_triangle_index(pos)
+        if picked_tri_idx is None:
+            return  # 没点中任何面
+
+        print(f"选中三角形: {picked_tri_idx}")
+
+        # 2. 获取该三角形的法线(作为基准)
+        base_normal = self.face_normals[picked_tri_idx]
+        tolerance = 0.05  # 法线夹角余弦容忍度(越小越严格)
+
+        # 3. 使用广度优先搜索(BFS)扩展所有“共面”三角形
+        from collections import deque
+        queue = deque([picked_tri_idx])
+        visited = set()
+        visited.add(picked_tri_idx)
+        region_triangles = []  # 存储属于这个“面区域”的所有三角面索引
+
+        while queue:
+            tri_idx = queue.popleft()
+            region_triangles.append(tri_idx)
+
+            # 获取当前三角形的三个顶点
+            v0, v1, v2 = self.triangles[tri_idx]
+            all_vertices = [v0, v1, v2]
+
+            # 遍历所有邻接三角形(共享边的三角形)
+            for i in range(3):
+                v_a = all_vertices[i]
+                v_b = all_vertices[(i + 1) % 3]
+                edge = tuple(sorted([v_a, v_b]))  # 边(无序)
+
+                # 查找共享这条边的其他三角形(需预建边到面的映射,或遍历)
+                # 这里简化:遍历所有三角形找邻接
+                for neighbor_idx in range(len(self.triangles)):
+                    if neighbor_idx in visited:
+                        continue
+
+                    tri = self.triangles[neighbor_idx]
+                    tri_edges = [
+                        tuple(sorted([tri[0], tri[1]])),
+                        tuple(sorted([tri[1], tri[2]])),
+                        tuple(sorted([tri[2], tri[0]])),
+                    ]
+
+                    if edge in tri_edges:
+                        # 是邻接三角形,检查法线是否接近
+                        neighbor_normal = self.face_normals[neighbor_idx]
+                        dot = np.dot(base_normal, neighbor_normal)
+                        if abs(dot) > (1 - tolerance):  # 法线夹角小
+                            visited.add(neighbor_idx)
+                            queue.append(neighbor_idx)
+
+        # ===================================
+        # ✅ 核心修改:不再修改顶点颜色
+        # 而是将共面区域的三角形索引保存到 self.highlighted_face_indices
+        # 由 paintGL 在渲染时决定是否高亮
+        # ===================================
+        self.highlighted_face_indices = region_triangles
+
+        # 可选:打印调试信息
+        print(f"✅ 共面区域包含 {len(self.highlighted_face_indices)} 个三角形")
+
+        # 触发重绘
+        self.update()  # 调用 paintGL 重新渲染
+
+    def _is_adjacent(self, tri_idx1, tri_idx2):
+        """判断两个三角形是否共享一条边(即邻接)"""
+        v1 = set(self.triangles[tri_idx1])
+        v2 = set(self.triangles[tri_idx2])
+        return len(v1 & v2) == 2  # 共享两个顶点 → 共享一条边
+
+    def _ray_cast_triangle_index(self, pos):
+        """
+        从屏幕点击位置发射一条射线,检测与哪个三角形相交(最近的)
+        返回相交的三角形索引,无则返回 None
+        """
+        width, height = self.width(), self.height()
+        x, y = pos.x(), pos.y()
+
+        # 1. 获取当前模型视图和投影矩阵
+        modelview = np.array(self.get_current_matrix()[0], dtype=np.float64)
+        projection = np.array(self.get_current_matrix()[1], dtype=np.float64)
+        viewport = (0, 0, width, height)
+
+        # 2. 将屏幕坐标转换为世界空间中的两个点(近平面和远平面)
+        try:
+            # 注意:OpenGL 的 Y 轴向下,所以要翻转 y
+            world_near = gluUnProject(x, height - y, 0.0, modelview, projection, viewport)
+            world_far = gluUnProject(x, height - y, 1.0, modelview, projection, viewport)
+        except Exception as e:
+            print(f"gluUnProject failed: {e}")
+            return None
+
+        # 3. 构造射线方向
+        ray_origin = np.array(world_near)
+        ray_direction = np.array(world_far) - np.array(world_near)
+        ray_direction = ray_direction / (np.linalg.norm(ray_direction) + 1e-8)
+
+        # 4. 遍历所有三角形,做射线相交检测
+        best_t = float('inf')
+        picked_idx = None
+
+        for idx, triangle in enumerate(self.triangles):
+            v0_idx, v1_idx, v2_idx = triangle
+            v0 = np.array(self.vertices[v0_idx])
+            v1 = np.array(self.vertices[v1_idx])
+            v2 = np.array(self.vertices[v2_idx])
+
+            result = self._ray_triangle_intersect(
+                ray_origin, ray_direction,
+                v0, v1, v2
+            )
+            if result is not None:
+                t, u, v = result
+                if t < best_t and t > 0:
+                    best_t = t
+                    picked_idx = idx
+
+        return picked_idx
+
+    def _ray_triangle_intersect(self, ray_origin, ray_direction, v0, v1, v2, eps=1e-6):
+        """
+        Möller–Trumbore 射线-三角形相交算法
+        返回: (t, u, v) 或 None(无交点)
+        t: 射线上交点距离
+        u,v: 重心坐标
+        """
+        # 三角形边向量
+        edge1 = v1 - v0
+        edge2 = v2 - v0
+
+        # P = ray_direction × edge2
+        P = np.cross(ray_direction, edge2)
+        det = np.dot(edge1, P)
+
+        if abs(det) < eps:
+            return None  # 射线与三角形平行
+
+        inv_det = 1.0 / det
+        T = ray_origin - v0
+        u = np.dot(T, P) * inv_det
+        if u < 0.0 or u > 1.0:
+            return None
+
+        Q = np.cross(T, edge1)
+        v = np.dot(ray_direction, Q) * inv_det
+        if v < 0.0 or u + v > 1.0:
+            return None
+
+        t = np.dot(edge2, Q) * inv_det
+        if t > eps:
+            return t, u, v  # 相交
+
+        return None
+
+    from OpenGL.GL import glGetDoublev, GL_MODELVIEW_MATRIX, GL_PROJECTION
+
+    def get_current_matrix(self):
+        """
+        安全获取当前 Modelview 和 Projection 矩阵
+        必须在 OpenGL 上下文中调用(比如在 paintGL 之后)
+        """
+        try:
+            modelview = glGetDoublev(GL_MODELVIEW_MATRIX)
+            projection = glGetDoublev(GL_PROJECTION_MATRIX)  # 注意:是 GL_PROJECTION_MATRIX,不是 GL_PROJECTION
+            return modelview, projection
+        except Exception as e:
+            print(f"获取矩阵失败: {e}")
+            return None, None
+
     def _do_picking(self, pos):
         x, y = pos.x(), pos.y()
         self.makeCurrent()
-        self._render_for_picking()
+        self._render_for_picking()  #再渲染一个当前的模型到gpu
 
+        #提取pos坐标像素点在窗口中的实际颜色piexel
         pixel = glReadPixels(x, self.height() - y - 1, 1, 1, GL_RGB, GL_FLOAT)
+        #将rgb分量的值提取出来
         r, g, b = pixel[0][0]
         print(f"Read color: ({r:.5f}, {g:.5f}, {b:.5f})")
 
@@ -396,20 +615,32 @@ class Simple3DWidget(QOpenGLWidget):
         if len(self.vertices) == 0:
             return
 
+        #设置断点::参数入栈函数,GL_ALL_ATTRIB_BITS:当前OPENGL的所有参数
         glPushAttrib(GL_ALL_ATTRIB_BITS)
+        #关闭光照计算,避免编码颜色改变
         glDisable(GL_LIGHTING)
+        #关闭纹理映射
         glDisable(GL_TEXTURE_2D)
+        #设置平面着色:一个三角面中的颜色保持一致
         glShadeModel(GL_FLAT)
+        #设置清屏颜色为黑色
         glClearColor(0, 0, 0, 0)
+        #清理颜色缓存和深度缓存
         glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
+        #开启深度测试
         glEnable(GL_DEPTH_TEST)
 
         # --- 复制 paintGL 的投影和视图变换 ---
+        #设置矩阵模式:投影矩阵
         glMatrixMode(GL_PROJECTION)
+        #重置投影矩阵为单位矩阵
         glLoadIdentity()
+        #设置透视投影
         gluPerspective(45, self.width() / max(self.height(), 1), 0.1, 100.0)
 
+        #切换矩阵模式:模型视图矩阵
         glMatrixMode(GL_MODELVIEW)
+        #重置当前矩阵为单位矩阵
         glLoadIdentity()
 
         yaw = self.view_rotation[1]
@@ -447,12 +678,23 @@ class Simple3DWidget(QOpenGLWidget):
         """切换选点模式"""
         if self.picking == False:
             self.picking = True
+            self.set_color = False
             print(f"已进入选点模式:{self.picking}")
         else:
             self.picking = False
             print(f"已退出选点模式:{self.picking}")
         # self.update()  # 切换模式后重新绘制
 
+    def color_mode(self):
+        """切换选色模式"""
+        if self.set_color == False:
+            self.set_color = True
+            self.picking = False #打开选色则关闭选点
+            print(f"已进入选色模式:{self.set_color}")
+        else:
+            self.set_color = False
+            print(f"已退出选色模式:{self.set_color}")
+
     #统一颜色显示方式
     def _get_picking_color(self, index):
         idx = index + 1

+ 170 - 17
handle_simple.py

@@ -1,7 +1,9 @@
 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
 
 
@@ -15,6 +17,7 @@ class Datahandle(QObject):
         self.normals = np.array([])  # 新增:法线
 
 
+
     def load_data(self):
         """
         加载 3D 数据(这里用随机点云代替实际文件)
@@ -47,28 +50,28 @@ class Dataload(QObject):
         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 格式(兼容旧版本)
+        使用 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([])
+            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([])
+            return np.array([]), np.array([]), np.array([]), np.array([]), np.array([])
 
         try:
-            from stl import mesh as stl_mesh
-
             # ✅ 自动识别格式(旧版本也支持)
             stl_data = stl_mesh.Mesh.from_file(file_path)
 
@@ -87,16 +90,8 @@ class Dataload(QObject):
 
             # 法线(每个三角面对应一个法线)
             normals = stl_data.normals.astype(np.float32)
-            vertex_normals = np.repeat(normals, 3, axis=0)  # 每个顶点复制一次
-            print(f"法线数量: {len(vertex_normals)}")
 
-            # 颜色:使用法线生成
-            colors = (vertex_normals + 1.0) / 2.0
-            if np.any(np.isnan(colors)) or np.any(np.isinf(colors)):
-                colors = np.ones_like(vertex_normals) * 0.8
-                self.colors = colors
-
-            # --- 归一化 ---
+            # --- 归一化(保持不变)---
             if len(vertices) > 0:
                 min_coords = np.min(vertices, axis=0)
                 max_coords = np.max(vertices, axis=0)
@@ -109,16 +104,127 @@ class Dataload(QObject):
                     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
-                        self.vertices = vertices
                         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()
-            return vertices, colors, triangles, vertex_normals
+            # 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([])
+            return np.array([]), np.array([]), np.array([]), np.array([]), np.array([])
 
     def _build_picking_color_map(self):
         self.picking_color_map.clear()
@@ -137,3 +243,50 @@ class Dataload(QObject):
         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

+ 9 - 4
main_simple.py

@@ -12,7 +12,7 @@ from handle_simple import Dataload
 class MainWindow(QMainWindow):
     def __init__(self):
         super().__init__()
-        self.setWindowTitle('3D模型查看器')
+        self.setWindowTitle('力山特喷涂机器人操作软件')
         self.setGeometry(100, 100, 1200, 800)
         
         # 创建中央部件
@@ -37,15 +37,18 @@ class MainWindow(QMainWindow):
         self.switch_button = QPushButton("切换显示模式")
         self.axes_button = QPushButton("坐标系显示")
         self.point_button = QPushButton("选点模式")
+        self.color_button = QPushButton("选色模式")
         self.open_button.clicked.connect(self.open_file)
         self.switch_button.clicked.connect(self.gl_widget.toggle_display_mode)
         self.axes_button.clicked.connect(self.gl_widget.toggle_axes_display_mode)
         self.point_button.clicked.connect(self.gl_widget.point_mode)
+        self.color_button.clicked.connect(self.gl_widget.color_mode)
 
         control_layout.addWidget(self.open_button)
         control_layout.addWidget(self.switch_button)
         control_layout.addWidget(self.axes_button)
         control_layout.addWidget(self.point_button)
+        control_layout.addWidget(self.color_button)
 
         # 添加一些空间
         control_layout.addStretch()
@@ -107,15 +110,17 @@ class MainWindow(QMainWindow):
         """加载STL文件"""
         print(f"📁 正在加载: {file_path}")
         
-        # 使用Dataload类加载数据
-        vertices, colors,triangles, normals = self.dataload.load_data_from_file(file_path)
+        # 使用Dataload类加载数据 
+        vertices, colors,triangles, normals, edges = self.dataload.load_data_from_file(file_path)
         
         if len(vertices) == 0:
             print("❌ 加载失败")
             return
         
         # 设置数据到渲染器
-        self.gl_widget.set_data(vertices, colors,triangles, normals)
+        self.gl_widget.face_normals = self.dataload.face_normals
+        self.gl_widget.original_colors = self.dataload.original_colors
+        self.gl_widget.set_data(vertices, colors,triangles, normals, edges)
         print("✅ 数据已设置到渲染器")
 
     def load_step_file(self, file_path):

BIN
models/客户工件.STL


BIN
models/工件.STL


+ 139 - 0
sdlpart_convert_stl.py

@@ -0,0 +1,139 @@
+# sldprt_to_stl_converter.py
+
+import os
+import tkinter as tk
+from tkinter import filedialog, messagebox
+import win32com.client
+import sys
+
+
+def convert_sldprt_to_stl(sldprt_path, stl_path):
+    """
+    使用 SolidWorks API 将 .SLDPRT 文件转换为 .STL 文件
+    :param sldprt_path: 输入的 SLDPRT 文件路径
+    :param stl_path: 输出的 STL 文件路径
+    :return: 是否成功
+    """
+    try:
+        # 检查输入文件是否存在
+        if not os.path.isfile(sldprt_path):
+            raise FileNotFoundError(f"找不到文件: {sldprt_path}")
+
+        # 启动 SolidWorks 应用(不显示界面)
+        swApp = win32com.client.Dispatch("SldWorks.Application")
+
+        if not swApp:
+            raise RuntimeError("无法启动 SolidWorks。请确保已安装并注册。")
+
+        # 打开 SLDPRT 文件(类型 1 = swDocumentTypes_e.swDocPART)
+        errors = 0
+        warnings = 0
+        part = swApp.OpenDoc6(sldprt_path, 1, 0, "", errors, warnings)
+
+        if not part:
+            raise RuntimeError("SolidWorks 无法打开该文件。可能是格式不兼容或损坏。")
+
+        # 设置保存选项(STL 选项)
+        # 0 = 默认精度,1 = 快速,2 = 高精度
+        save_options = 0  # 可改为 2 提高 STL 精度
+        export_options = 0
+
+        # 保存为 STL
+        result = part.SaveAs3(stl_path, save_options, export_options)
+
+        # 关闭文档(不保存更改)
+        swApp.CloseDoc(os.path.basename(sldprt_path))
+
+        if result != 0:
+            return True, f"成功导出到: {stl_path}"
+        else:
+            return False, "SolidWorks 保存失败,可能是权限或路径问题。"
+
+    except Exception as e:
+        return False, f"转换失败: {str(e)}"
+
+
+def select_and_convert():
+    """选择文件并执行转换"""
+    # 选择输入文件
+    sldprt_path = filedialog.askopenfilename(
+        title="选择 .SLDPRT 文件",
+        filetypes=[("SolidWorks Part Files", "*.SLDPRT"), ("All Files", "*.*")]
+    )
+    if not sldprt_path:
+        return  # 用户取消
+
+    # 设置输出 STL 路径(默认同名,同目录)
+    directory = os.path.dirname(sldprt_path)
+    filename = os.path.splitext(os.path.basename(sldprt_path))[0]
+    stl_path = os.path.join(directory, f"{filename}.STL")
+
+    # 询问是否覆盖
+    if os.path.exists(stl_path):
+        if not messagebox.askyesno("覆盖确认", f"{stl_path} 已存在,是否覆盖?"):
+            return
+
+    # 执行转换
+    success, message = convert_sldprt_to_stl(sldprt_path, stl_path)
+
+    # 显示结果
+    if success:
+        messagebox.showinfo("成功", message)
+    else:
+        messagebox.showerror("失败", message)
+
+
+def main():
+    """主函数:创建 GUI 界面"""
+    root = tk.Tk()
+    root.title("SLDPRT 转 STL 转换器")
+    root.geometry("400x200")
+    root.resizable(False, False)
+
+    # 标题标签
+    label = tk.Label(
+        root,
+        text="SolidWorks SLDPRT 转 STL",
+        font=("SimHei", 14, "bold"),
+        pady=20
+    )
+    label.pack()
+
+    # 说明
+    info = tk.Label(
+        root,
+        text="点击下方按钮选择 .SLDPRT 文件进行转换",
+        font=("微软雅黑", 10),
+        fg="gray"
+    )
+    info.pack()
+
+    # 转换按钮
+    convert_btn = tk.Button(
+        root,
+        text="选择文件并转换",
+        font=("微软雅黑", 12),
+        bg="#4CAF50",
+        fg="white",
+        padx=20,
+        pady=10,
+        command=select_and_convert
+    )
+    convert_btn.pack(pady=20)
+
+    # 启动主循环
+    root.mainloop()
+
+
+if __name__ == "__main__":
+    # 检查是否安装了 pywin32
+    try:
+        import win32com.client
+    except ImportError:
+        messagebox.showerror(
+            "缺少依赖",
+            "请先安装 pywin32:\n\npip install pywin32"
+        )
+        sys.exit(1)
+
+    main()