|
@@ -36,6 +36,12 @@ class Simple3DWidget(QOpenGLWidget):
|
|
self.view_pan = [0.0, 0.0] # 视点平移 (X, Y),用于 Ctrl+右键
|
|
self.view_pan = [0.0, 0.0] # 视点平移 (X, Y),用于 Ctrl+右键
|
|
self.view_distance = 8.0 # 视点到目标的距离
|
|
self.view_distance = 8.0 # 视点到目标的距离
|
|
|
|
|
|
|
|
+ #选点模式
|
|
|
|
+ self.selected_point = None # 存储选中的点坐标
|
|
|
|
+ self.picking = False # 是否处于拾取模式
|
|
|
|
+ self.picking_color_map = {} # 顶点索引 → 唯一颜色(用于反查)
|
|
|
|
+
|
|
|
|
+
|
|
|
|
|
|
|
|
|
|
def initializeGL(self):
|
|
def initializeGL(self):
|
|
@@ -114,32 +120,57 @@ class Simple3DWidget(QOpenGLWidget):
|
|
glPopMatrix()
|
|
glPopMatrix()
|
|
|
|
|
|
def _draw_model(self):
|
|
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)
|
|
glPointSize(8.0)
|
|
glBegin(GL_POINTS)
|
|
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:
|
|
elif self.display_mode == 'surface' and len(self.triangles) > 0:
|
|
|
|
+ # ✅ 开启光照
|
|
glEnable(GL_LIGHTING)
|
|
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):
|
|
def renderText(self, x, y, z, text):
|
|
"""
|
|
"""
|
|
@@ -157,36 +188,44 @@ class Simple3DWidget(QOpenGLWidget):
|
|
def drawWorldAxes(self):
|
|
def drawWorldAxes(self):
|
|
"""绘制固定的世界坐标系(左下角)"""
|
|
"""绘制固定的世界坐标系(左下角)"""
|
|
glPushMatrix()
|
|
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):
|
|
def drawModelAxes(self):
|
|
"""绘制随模型移动的坐标系"""
|
|
"""绘制随模型移动的坐标系"""
|
|
@@ -219,38 +258,6 @@ class Simple3DWidget(QOpenGLWidget):
|
|
|
|
|
|
glPopMatrix()
|
|
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):
|
|
def set_data(self, vertices, colors, triangles=None, normals=None):
|
|
"""设置 3D 数据(支持 mesh)"""
|
|
"""设置 3D 数据(支持 mesh)"""
|
|
@@ -271,7 +278,34 @@ class Simple3DWidget(QOpenGLWidget):
|
|
self.update()
|
|
self.update()
|
|
|
|
|
|
def mousePressEvent(self, event):
|
|
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):
|
|
def mouseMoveEvent(self, event):
|
|
if self.last_mouse_pos is None:
|
|
if self.last_mouse_pos is None:
|
|
@@ -308,7 +342,9 @@ class Simple3DWidget(QOpenGLWidget):
|
|
|
|
|
|
def wheelEvent(self, event):
|
|
def wheelEvent(self, event):
|
|
delta = event.angleDelta().y()
|
|
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.view_distance = max(1.0, min(50.0, self.view_distance))
|
|
self.update()
|
|
self.update()
|
|
|
|
|
|
@@ -327,3 +363,88 @@ class Simple3DWidget(QOpenGLWidget):
|
|
else:
|
|
else:
|
|
self.axes_display_mode = False
|
|
self.axes_display_mode = False
|
|
self.update()
|
|
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)
|