Browse Source

点云/模型显示切换、模型坐标系显示切换、选点功能、ctrl控制世界坐标系

lstzcy 1 tháng trước cách đây
mục cha
commit
8e60f8b6a9

+ 6 - 0
.idea/vcs.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="VcsDirectoryMappings">
+    <mapping directory="" vcs="Git" />
+  </component>
+</project>

BIN
__pycache__/glWidget_simple.cpython-39.pyc


BIN
__pycache__/handle_simple.cpython-39.pyc


+ 201 - 80
glWidget_simple.py

@@ -36,6 +36,12 @@ class Simple3DWidget(QOpenGLWidget):
         self.view_pan = [0.0, 0.0]  # 视点平移 (X, Y),用于 Ctrl+右键
         self.view_distance = 8.0  # 视点到目标的距离
 
+        #选点模式
+        self.selected_point = None  # 存储选中的点坐标
+        self.picking = False  # 是否处于拾取模式
+        self.picking_color_map = {}  # 顶点索引 → 唯一颜色(用于反查)
+
+
 
 
     def initializeGL(self):
@@ -114,32 +120,57 @@ class Simple3DWidget(QOpenGLWidget):
         glPopMatrix()
 
     def _draw_model(self):
-        """私有方法:绘制模型"""
-        if self.display_mode == 'points' and len(self.vertices) > 0:
+        """私有方法:绘制模型(含高亮)"""
+        if len(self.vertices) == 0:
+            return
+
+        if self.display_mode == 'points':
             glPointSize(8.0)
             glBegin(GL_POINTS)
-            for i, v in enumerate(self.vertices):
-                if i < len(self.colors):
-                    glColor3f(*self.colors[i])
-                glVertex3f(v[0], v[1], v[2])
-            glEnd()
+            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, 0.0, 0.0)  # 红色高亮
+                    else:
+                        glColor3f(*self.colors[i])  # 原始颜色
+                    glVertex3f(v[0], v[1], v[2])
+            finally:
+                glEnd()  # ✅ 保证执行
 
         elif self.display_mode == 'surface' and len(self.triangles) > 0:
+            # ✅ 开启光照
             glEnable(GL_LIGHTING)
-            glEnable(GL_LIGHT0)
-            glBegin(GL_TRIANGLES)
-            for tri in self.triangles:
-                for idx in tri:
-                    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)
-                    if len(self.colors) > idx:
-                        glColor3f(*self.colors[idx])
-                    glVertex3f(*self.vertices[idx])
-            glEnd()
-            glDisable(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:
+                        for idx in tri:
+                            # 高亮三角形中包含选中点的顶点
+                            if 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])
+                finally:
+                    glEnd()  # ✅ 保证结束绘制
+
+            finally:
+                glDisable(GL_LIGHTING)  # ✅ 保证关闭光照
 
     def renderText(self, x, y, z, text):
         """
@@ -157,36 +188,44 @@ class Simple3DWidget(QOpenGLWidget):
     def drawWorldAxes(self):
         """绘制固定的世界坐标系(左下角)"""
         glPushMatrix()
-        # 移动到左下角
-        glTranslatef(-4.0, -4.0, -5.0)
+        try:
+            # 移动到左下角
+            glTranslatef(-4.0, -4.0, -5.0)
 
-        glLineWidth(2.0)
-        glBegin(GL_LINES)
-        # X (红)
-        glColor3f(1, 0, 0)
-        glVertex3f(0, 0, 0);
-        glVertex3f(1000, 0, 0)
-        # Y (绿)
-        glColor3f(0, 1, 0)
-        glVertex3f(0, 0, 0);
-        glVertex3f(0, 1000, 0)
-        # Z (蓝)
-        glColor3f(0, 0, 1)
-        glVertex3f(0, 0, 0);
-        glVertex3f(0, 0, 1000)
-        glEnd()
+            glLineWidth(2.0)
+            glBegin(GL_LINES)
 
-        # 绘制文本标签
-        glColor3f(1.0, 0.0, 0.0)  # 设置颜色为红色
-        self.renderText(1.5, 0, 0, 'X')  # X轴标签
+            # X (红)
+            glColor3f(1, 0, 0)
+            glVertex3f(0, 0, 0)
+            glVertex3f(1000, 0, 0)
 
-        glColor3f(0.0, 1.0, 0.0)  # 设置颜色为绿色
-        self.renderText(0, 1.5, 0, 'Y')  # Y轴标签
+            # Y (绿)
+            glColor3f(0, 1, 0)
+            glVertex3f(0, 0, 0)
+            glVertex3f(0, 1000, 0)
 
-        glColor3f(0.0, 0.0, 1.0)  # 设置颜色为蓝色
-        self.renderText(0, 0, 1.5, 'Z')  # Z轴标签
+            # Z (蓝)
+            glColor3f(0, 0, 1)
+            glVertex3f(0, 0, 0)
+            glVertex3f(0, 0, 1000)
 
-        glPopMatrix()
+            glEnd()  # 结束绘制
+
+            # ✅ 确保 glEnd() 后再绘制文本,避免 OpenGL 状态混乱
+            # 绘制文本标签
+            glColor3f(1.0, 0.0, 0.0)
+            self.renderText(1.5, 0, 0, 'X')
+
+            glColor3f(0.0, 1.0, 0.0)
+            self.renderText(0, 1.5, 0, 'Y')
+
+            glColor3f(0.0, 0.0, 1.0)
+            self.renderText(0, 0, 1.5, 'Z')
+
+        finally:
+            # ✅ 无论是否出错,都确保弹出矩阵栈
+            glPopMatrix()
 
     def drawModelAxes(self):
         """绘制随模型移动的坐标系"""
@@ -219,38 +258,6 @@ class Simple3DWidget(QOpenGLWidget):
 
         glPopMatrix()
 
-        # if hasattr(self, 'model_scale'):
-        #     glScalef(self.model_scale, self.model_scale, self.model_scale)
-
-    # def paintGLAxes(self):
-    #     """绘制坐标轴"""
-    #     glLineWidth(3.0)
-    #     glBegin(GL_LINES)
-    #     # X轴 - 红色
-    #     glColor3f(1.0, 0.0, 0.0)
-    #     glVertex3f(0.0, 0.0, 0.0)
-    #     glVertex3f(100.0, 0.0, 0.0)
-    #
-    #     # Y轴 - 绿色
-    #     glColor3f(0.0, 1.0, 0.0)
-    #     glVertex3f(0.0, 0.0, 0.0)
-    #     glVertex3f(0.0, 100.0, 0.0)
-    #
-    #     # Z轴 - 蓝色
-    #     glColor3f(0.0, 0.0, 1.0)
-    #     glVertex3f(0.0, 0.0, 0.0)
-    #     glVertex3f(0.0, 0.0, 100.0)
-    #     glEnd()
-    #
-    #     # 绘制文本标签
-    #     glColor3f(1.0, 0.0, 0.0)  # 设置颜色为红色
-    #     self.renderText(1.5, 0, 0, 'X')  # X轴标签
-    #
-    #     glColor3f(0.0, 1.0, 0.0)  # 设置颜色为绿色
-    #     self.renderText(0, 1.5, 0, 'Y')  # Y轴标签
-    #
-    #     glColor3f(0.0, 0.0, 1.0)  # 设置颜色为蓝色
-    #     self.renderText(0, 0, 1.5, 'Z')  # Z轴标签
 
     def set_data(self, vertices, colors, triangles=None, normals=None):
         """设置 3D 数据(支持 mesh)"""
@@ -271,7 +278,34 @@ class Simple3DWidget(QOpenGLWidget):
         self.update()
 
     def mousePressEvent(self, event):
-        self.last_mouse_pos = event.pos()
+        self.last_mouse_pos = event.pos()       #二维坐标,仅用来计算旋转/移动的量
+        if event.button() == Qt.LeftButton:
+            if self.picking == True:
+                self._do_picking(event.pos())
+        super().mousePressEvent(event)              #调用父类中默认的方法,获得预设的一些功能
+
+    def _do_picking(self, pos):
+        x, y = pos.x(), pos.y()
+        self.makeCurrent()
+        self._render_for_picking()
+
+        pixel = glReadPixels(x, self.height() - y - 1, 1, 1, GL_RGB, GL_FLOAT)
+        r, g, b = pixel[0][0]
+        print(f"Read color: ({r:.5f}, {g:.5f}, {b:.5f})")
+
+        # ✅ 关键修复:转成 float 再 round,避免虽然值一样,但 Python 字典认为 np.float32(0.01176) ≠ float(0.01176)!
+        key = (round(float(r), 5), round(float(g), 5), round(float(b), 5))
+
+        if key in self.picking_color_map:
+            index = self.picking_color_map[key]
+            picked_point = self.vertices[index]
+            self.selected_point = picked_point
+            print(f"🎯 拾取到点: {picked_point}, 索引: {index}")
+            self.pointPicked.emit(*picked_point)
+        else:
+            print(f"❌ 未找到对应点,key={key} 不在 map 中")
+
+        self.update()
 
     def mouseMoveEvent(self, event):
         if self.last_mouse_pos is None:
@@ -308,7 +342,9 @@ class Simple3DWidget(QOpenGLWidget):
 
     def wheelEvent(self, event):
         delta = event.angleDelta().y()
-        self.view_distance -= delta * 0.05
+        #view_distance 相机到模型的距离,delta:鼠标滚动的值
+        self.view_distance -= delta * 0.005
+        #距离的范围限定
         self.view_distance = max(1.0, min(50.0, self.view_distance))
         self.update()
 
@@ -327,3 +363,88 @@ class Simple3DWidget(QOpenGLWidget):
         else:
             self.axes_display_mode = False
         self.update()
+
+    def _generate_picking_colors(self):
+        """为每个顶点生成唯一颜色(用于拾取)"""
+        if len(self.vertices) == 0:
+            return np.array([])
+
+        colors = np.zeros((len(self.vertices), 3), dtype=np.float32)
+        for i in range(len(self.vertices)):
+            # 用整数编码成 RGB(最多支持 ~1677 万个点)
+            color_id = i + 1  # 从 1 开始,避免 0,0,0(黑色)误判
+            r = (color_id & 0xFF) / 255.0
+            g = ((color_id >> 8) & 0xFF) / 255.0
+            b = ((color_id >> 16) & 0xFF) / 255.0
+            colors[i] = [r, g, b]
+            self.picking_color_map[(r, g, b)] = i  # 反向映射
+        return colors
+
+    def _render_for_picking(self):
+        if len(self.vertices) == 0:
+            return
+
+        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]
+        pitch = self.view_rotation[0]
+        distance = self.view_distance
+        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, 0, 0, 0, 0, 1, 0)
+
+        glTranslatef(self.view_pan[0], self.view_pan[1], 0)
+
+        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)
+
+        # ✅ 用唯一颜色绘制顶点
+        glPointSize(8.0)
+        glBegin(GL_POINTS)
+        for i, v in enumerate(self.vertices):
+            idx = i + 1
+            r = (idx) & 0xFF
+            g = (idx >> 8) & 0xFF
+            b = (idx >> 16) & 0xFF
+            glColor3f(r / 255.0, g / 255.0, b / 255.0)
+            glVertex3f(v[0], v[1], v[2])
+        glEnd()
+
+        glPopMatrix()
+        glPopAttrib()
+
+    def point_mode(self):
+        """切换选点模式"""
+        if self.picking == False:
+            self.picking = True
+            print(f"已进入选点模式:{self.picking}")
+        else:
+            self.picking = False
+            print(f"已退出选点模式:{self.picking}")
+        # self.update()  # 切换模式后重新绘制
+
+    #统一颜色显示方式
+    def _get_picking_color(self, index):
+        idx = index + 1
+        r = (idx) & 0xFF
+        g = (idx >> 8) & 0xFF
+        b = (idx >> 16) & 0xFF
+        return (r / 255.0, g / 255.0, b / 255.0)

+ 24 - 1
handle_simple.py

@@ -9,11 +9,12 @@ class Datahandle(QObject):
     def __init__(self):
         super().__init__()
         self.qml_item = None
-        self.vertices, self.colors = self.load_data()  # 预加载数据
+        self.vertices = np.array([])
         self.colors = np.array([])
         self.triangles = np.array([])  # 新增:三角面索引
         self.normals = np.array([])  # 新增:法线
 
+
     def load_data(self):
         """
         加载 3D 数据(这里用随机点云代替实际文件)
@@ -48,6 +49,7 @@ class Dataload(QObject):
         self.colors = np.array([])
         self.triangles = np.array([])  # 新增:三角面索引
         self.normals = np.array([])  # 新增:法线
+        self.picking_color_map = {}
 
     def load_data_from_file(self, file_path: str):
         """
@@ -92,6 +94,7 @@ class Dataload(QObject):
             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:
@@ -106,11 +109,31 @@ 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(f"✅ 加载成功: {file_path}")
+            self._build_picking_color_map()
             return vertices, colors, triangles, vertex_normals
 
         except Exception as e:
             print(f"❌ 加载失败: {e}")
             return 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)

+ 4 - 0
main_simple.py

@@ -36,13 +36,16 @@ class MainWindow(QMainWindow):
         self.open_button = QPushButton("打开STL文件")
         self.switch_button = QPushButton("切换显示模式")
         self.axes_button = QPushButton("坐标系显示")
+        self.point_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)
 
         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.addStretch()
@@ -57,6 +60,7 @@ class MainWindow(QMainWindow):
         
         # 创建数据加载器
         self.dataload = Dataload()
+        self.gl_widget.picking_color_map = self.dataload.picking_color_map
 
     def open_file(self):
         """打开文件对话框,选择 3D 模型文件并根据类型调用对应加载函数"""