#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 独立的坐标轴点击测试demo 专门用于测试和调试坐标轴的点击检测和拖拽约束功能 """ import sys import math from direct.showbase.ShowBase import ShowBase from panda3d.core import ( Vec3, Vec4, Point3, Point2, LineSegs, AmbientLight, DirectionalLight, TextNode, CollisionTraverser, CollisionHandlerQueue, CollisionNode, CollisionRay, GeomNode, CardMaker, Material ) from direct.task import Task from direct.gui.DirectGui import DirectFrame, DirectLabel class GizmoTest(ShowBase): def __init__(self): ShowBase.__init__(self) # 关闭默认的鼠标控制 self.disableMouse() print("坐标轴测试程序启动") # 设置背景色 self.setBackgroundColor(0.2, 0.2, 0.3) # 设置相机位置 self.cam.setPos(10, -20, 10) self.cam.lookAt(0, 0, 0) # 添加光照 self.setupLighting() # 创建参考立方体 self.createReferenceCube() # 创建坐标轴 self.createGizmo() # 设置点击检测 self.setupClickDetection() # 创建信息显示 self.createInfoDisplay() # 拖拽状态变量 self.isDragging = False self.dragAxis = None self.dragStartMousePos = None self.dragStartCubePos = None # 高亮状态 self.highlightAxis = None # 设置事件处理 self.accept("mouse1", self.onMouseClick) self.accept("mouse1-up", self.onMouseRelease) self.taskMgr.add(self.mouseTask, "mouseTask") # 添加颜色测试任务 self.taskMgr.add(self.colorTestTask, "colorTest") self.test_time = 0 print("=== Gizmo Click Test Demo Started ===") print("Instructions:") print("- ONLY click on red/green/blue gizmo axes to drag") print("- Each axis constrains movement to that direction only") print("- Click elsewhere should do nothing") print("- Real-time display of mouse and projection info") print("- Color test will cycle through colors every 3 seconds") def setupLighting(self): """设置光照""" # 环境光 alight = AmbientLight('alight') alight.setColor((0.3, 0.3, 0.3, 1)) alnp = self.render.attachNewNode(alight) self.render.setLight(alnp) # 定向光 dlight = DirectionalLight('dlight') dlight.setColor((0.8, 0.8, 0.8, 1)) dlnp = self.render.attachNewNode(dlight) dlnp.setHpr(45, -45, 0) self.render.setLight(dlnp) def createReferenceCube(self): """创建参考立方体""" # 创建立方体 cm = CardMaker('cube') cm.setFrame(-1, 1, -1, 1) # 创建6个面 faces = [ ('front', (0, 1, 0), (0, 0, 0), (1, 0, 0, 1)), # 红色前面 ('back', (0, -1, 0), (0, 0, 180), (0, 1, 0, 1)), # 绿色后面 ('left', (-1, 0, 0), (0, 0, 90), (0, 0, 1, 1)), # 蓝色左面 ('right', (1, 0, 0), (0, 0, -90), (1, 1, 0, 1)), # 黄色右面 ('top', (0, 0, 1), (-90, 0, 0), (1, 0, 1, 1)), # 紫色顶面 ('bottom', (0, 0, -1), (90, 0, 0), (0, 1, 1, 1)) # 青色底面 ] self.cube = self.render.attachNewNode("cube") for name, pos, hpr, color in faces: face = self.cube.attachNewNode(cm.generate()) face.setPos(*pos) face.setHpr(*hpr) face.setColor(*color) face.setName(name) print(f"✓ 参考立方体创建完成,位置: {self.cube.getPos()}") def createGizmo(self): """创建坐标轴""" self.gizmo = self.render.attachNewNode("gizmo") self.gizmo.setPos(0, 0, 3) # 放在立方体上方 self.axis_length = 3.0 self.click_threshold = 25 # 点击检测阈值(像素) # 创建X轴(不在LineSegs级别设置颜色) x_lines = LineSegs("x_axis") x_lines.setThickness(6.0) # 不在LineSegs级别设置颜色! x_lines.moveTo(0, 0, 0) x_lines.drawTo(self.axis_length, 0, 0) # 箭头 x_lines.moveTo(self.axis_length - 0.3, -0.1, 0) x_lines.drawTo(self.axis_length, 0, 0) x_lines.drawTo(self.axis_length - 0.3, 0.1, 0) x_geom = x_lines.create() self.gizmoXAxis = self.gizmo.attachNewNode(x_geom) self.gizmoXAxis.setLightOff() # 使用RenderState强制设置颜色 from panda3d.core import RenderState, ColorAttrib red_state = RenderState.make(ColorAttrib.makeFlat((1, 0, 0, 1))) self.gizmoXAxis.setState(red_state) self.gizmoXAxis.setColor(1, 0, 0, 1) # 创建Y轴(不在LineSegs级别设置颜色) y_lines = LineSegs("y_axis") y_lines.setThickness(6.0) # 不在LineSegs级别设置颜色! y_lines.moveTo(0, 0, 0) y_lines.drawTo(0, self.axis_length, 0) # 箭头 y_lines.moveTo(-0.1, self.axis_length - 0.3, 0) y_lines.drawTo(0, self.axis_length, 0) y_lines.drawTo(0.1, self.axis_length - 0.3, 0) y_geom = y_lines.create() self.gizmoYAxis = self.gizmo.attachNewNode(y_geom) self.gizmoYAxis.setLightOff() # 使用RenderState强制设置颜色 green_state = RenderState.make(ColorAttrib.makeFlat((0, 1, 0, 1))) self.gizmoYAxis.setState(green_state) self.gizmoYAxis.setColor(0, 1, 0, 1) # 创建Z轴(不在LineSegs级别设置颜色) z_lines = LineSegs("z_axis") z_lines.setThickness(6.0) # 不在LineSegs级别设置颜色! z_lines.moveTo(0, 0, 0) z_lines.drawTo(0, 0, self.axis_length) # 箭头 z_lines.moveTo(-0.1, 0, self.axis_length - 0.3) z_lines.drawTo(0, 0, self.axis_length) z_lines.drawTo(0.1, 0, self.axis_length - 0.3) z_geom = z_lines.create() self.gizmoZAxis = self.gizmo.attachNewNode(z_geom) self.gizmoZAxis.setLightOff() # 使用RenderState强制设置颜色 blue_state = RenderState.make(ColorAttrib.makeFlat((0, 0, 1, 1))) self.gizmoZAxis.setState(blue_state) self.gizmoZAxis.setColor(0, 0, 1, 1) # 轴颜色字典 self.axis_colors = { "x": (1, 0, 0, 1), # 红色 "y": (0, 1, 0, 1), # 绿色 "z": (0, 0, 1, 1) # 蓝色 } print(f"✓ 坐标轴创建完成,长度={self.axis_length}") print(f"X轴节点颜色: {self.gizmoXAxis.getColor()}") print(f"Y轴节点颜色: {self.gizmoYAxis.getColor()}") print(f"Z轴节点颜色: {self.gizmoZAxis.getColor()}") def setupClickDetection(self): """设置点击检测""" self.picker = CollisionTraverser() self.pq = CollisionHandlerQueue() self.pickerNode = CollisionNode('mouseRay') self.pickerNP = self.camera.attachNewNode(self.pickerNode) self.pickerNode.setFromCollideMask(GeomNode.getDefaultCollideMask()) self.picker.addCollider(self.pickerNP, self.pq) def createInfoDisplay(self): """创建信息显示面板""" self.infoPanel = DirectFrame( frameColor=(0, 0, 0, 0.7), frameSize=(-0.4, 0.4, -0.3, 0.3), pos=(-0.6, 0, 0.8) ) self.infoText = DirectLabel( parent=self.infoPanel, text="Mouse Info\nWaiting...", text_scale=0.05, text_fg=(1, 1, 1, 1), frameColor=(0, 0, 0, 0), pos=(0, 0, 0) ) def worldToScreen(self, world_pos): """将世界坐标转换为屏幕坐标""" try: # 转换为相机坐标系 cam_pos = self.cam.getRelativePoint(self.render, world_pos) # 检查是否在相机前方 if cam_pos.getY() <= 0: return None # 使用相机lens进行投影 screen_pos = Point2() if self.cam.node().getLens().project(cam_pos, screen_pos): # 转换为像素坐标 win_x = (screen_pos.getX() + 1.0) * 0.5 * self.win.getXSize() win_y = (1.0 - screen_pos.getY()) * 0.5 * self.win.getYSize() return (win_x, win_y) return None except: return None def distanceToLine(self, point, line_start, line_end): """计算点到线段的距离""" try: px, py = point x1, y1 = line_start x2, y2 = line_end # 计算线段长度 line_length_sq = (x2 - x1) ** 2 + (y2 - y1) ** 2 if line_length_sq < 0.001: return math.sqrt((px - x1) ** 2 + (py - y1) ** 2) # 计算投影参数 t = max(0, min(1, ((px - x1) * (x2 - x1) + (py - y1) * (y2 - y1)) / line_length_sq)) # 计算投影点 projection_x = x1 + t * (x2 - x1) projection_y = y1 + t * (y2 - y1) # 返回距离 distance = math.sqrt((px - projection_x) ** 2 + (py - projection_y) ** 2) return distance except: return float('inf') def checkGizmoClick(self, mouse_x, mouse_y): """检查是否点击了坐标轴""" if not self.gizmo: return None # 获取坐标轴中心的世界坐标 gizmo_world_pos = self.gizmo.getPos(self.render) # 计算各轴端点的世界坐标 x_end = gizmo_world_pos + Vec3(self.axis_length, 0, 0) y_end = gizmo_world_pos + Vec3(0, self.axis_length, 0) z_end = gizmo_world_pos + Vec3(0, 0, self.axis_length) # 投影到屏幕坐标 center_screen = self.worldToScreen(gizmo_world_pos) x_screen = self.worldToScreen(x_end) y_screen = self.worldToScreen(y_end) z_screen = self.worldToScreen(z_end) if not center_screen: return None # 检测各个轴 axes_data = [ ("x", x_screen, "X轴"), ("y", y_screen, "Y轴"), ("z", z_screen, "Z轴") ] closest_axis = None closest_distance = float('inf') for axis_name, axis_screen, axis_label in axes_data: if axis_screen: distance = self.distanceToLine( (mouse_x, mouse_y), center_screen, axis_screen ) if distance < self.click_threshold and distance < closest_distance: closest_axis = axis_name closest_distance = distance return closest_axis def setAxisColor(self, axis, color): """设置坐标轴颜色 - 使用RenderState强制覆盖""" from panda3d.core import RenderState, ColorAttrib # 创建强制颜色状态 color_state = RenderState.make(ColorAttrib.makeFlat(color)) if axis == "x" and self.gizmoXAxis: self.gizmoXAxis.setState(color_state) self.gizmoXAxis.setColor(*color) self.gizmoXAxis.setColorScale(*color) print(f"✓ 强制设置X轴颜色: {color}") elif axis == "y" and self.gizmoYAxis: self.gizmoYAxis.setState(color_state) self.gizmoYAxis.setColor(*color) self.gizmoYAxis.setColorScale(*color) print(f"✓ 强制设置Y轴颜色: {color}") elif axis == "z" and self.gizmoZAxis: self.gizmoZAxis.setState(color_state) self.gizmoZAxis.setColor(*color) self.gizmoZAxis.setColorScale(*color) print(f"✓ 强制设置Z轴颜色: {color}") def mouseTask(self, task): """鼠标任务 - 处理高亮和拖拽""" if not self.mouseWatcherNode.hasMouse(): return task.cont # 获取鼠标屏幕坐标 mouse_x = (self.mouseWatcherNode.getMouseX() + 1.0) * 0.5 * self.win.getXSize() mouse_y = (1.0 - self.mouseWatcherNode.getMouseY()) * 0.5 * self.win.getYSize() # 检查是否在颜色测试周期内 cycle_time = int(self.test_time) % 15 is_color_testing = (cycle_time >= 3 and cycle_time < 12) # 在黄、白、橙色测试期间 # 更新信息显示 info_text = f"Mouse Info:\n" info_text += f"Normalized: ({self.mouseWatcherNode.getMouseX():.3f}, {self.mouseWatcherNode.getMouseY():.3f})\n" info_text += f"Pixels: ({mouse_x:.1f}, {mouse_y:.1f})\n" info_text += f"Window Size: {self.win.getXSize()} x {self.win.getYSize()}\n" if is_color_testing: info_text += f"Color Test Active (cycle: {cycle_time})\n" # 只在不拖拽且不在颜色测试时检测高亮 if not self.isDragging and not is_color_testing: # 简化的高亮检测 hovered_axis = None if self.gizmo: # 获取坐标轴中心的世界坐标 gizmo_world_pos = self.gizmo.getPos(self.render) # 计算各轴端点的世界坐标 x_end = gizmo_world_pos + Vec3(self.axis_length, 0, 0) y_end = gizmo_world_pos + Vec3(0, self.axis_length, 0) z_end = gizmo_world_pos + Vec3(0, 0, self.axis_length) # 投影到屏幕坐标 center_screen = self.worldToScreen(gizmo_world_pos) x_screen = self.worldToScreen(x_end) y_screen = self.worldToScreen(y_end) z_screen = self.worldToScreen(z_end) if center_screen: # 检测距离最近的轴 min_distance = float('inf') for axis_name, axis_screen in [("x", x_screen), ("y", y_screen), ("z", z_screen)]: if axis_screen: distance = self.distanceToLine((mouse_x, mouse_y), center_screen, axis_screen) if distance < self.click_threshold and distance < min_distance: min_distance = distance hovered_axis = axis_name # 处理高亮状态变化 if hovered_axis != self.highlightAxis: # 恢复之前高亮的轴 if self.highlightAxis: self.setAxisColor(self.highlightAxis, self.axis_colors[self.highlightAxis]) print(f"恢复 {self.highlightAxis} 轴原色") # 高亮新的轴 if hovered_axis: self.setAxisColor(hovered_axis, (1, 1, 0, 1)) # 纯黄色高亮 print(f"高亮 {hovered_axis} 轴为黄色") self.highlightAxis = hovered_axis # 更新信息显示 if hovered_axis: info_text += f"Hover: {hovered_axis.upper()} axis (YELLOW)" else: info_text += "Hover: None" elif is_color_testing: info_text += "Hover: Disabled (Color Test)" else: info_text += f"Dragging: {self.dragAxis.upper()} axis" # 处理拖拽 if self.isDragging: self.handleDrag(mouse_x, mouse_y) self.infoText.setText(info_text) return task.cont def onMouseClick(self): """鼠标点击事件 - 只有点击坐标轴才开始拖拽""" if not self.mouseWatcherNode.hasMouse(): return # 检查是否在颜色测试期间 cycle_time = int(self.test_time) % 15 is_color_testing = (cycle_time >= 3 and cycle_time < 12) if is_color_testing: print("点击被禁用 - 颜色测试进行中") return # 获取鼠标坐标 mouse_x = (self.mouseWatcherNode.getMouseX() + 1.0) * 0.5 * self.win.getXSize() mouse_y = (1.0 - self.mouseWatcherNode.getMouseY()) * 0.5 * self.win.getYSize() print(f"\n=== Mouse Click ===") print(f"Pixel Coords: ({mouse_x:.1f}, {mouse_y:.1f})") # 检测坐标轴点击 clicked_axis = self.checkGizmoClick(mouse_x, mouse_y) if clicked_axis: print(f"✓ Clicked {clicked_axis.upper()} Axis - Starting drag") # 只有真正点击到轴才开始拖拽 self.isDragging = True self.dragAxis = clicked_axis self.dragStartMousePos = (mouse_x, mouse_y) self.dragStartCubePos = self.cube.getPos() # 显示拖拽颜色 - 使用更明显的橙色 self.setAxisColor(clicked_axis, (1, 0.5, 0, 1)) # 更亮的橙色表示拖拽 print(f" 开始位置: {self.dragStartCubePos}") else: print("✗ No axis clicked - No action") # 确保没有点击到轴时不开始拖拽 self.isDragging = False self.dragAxis = None def onMouseRelease(self): """鼠标释放事件""" if self.isDragging and self.dragAxis: print(f"Stop dragging {self.dragAxis.upper()} axis") # 恢复轴颜色 self.setAxisColor(self.dragAxis, self.axis_colors[self.dragAxis]) # 清理所有拖拽状态 self.isDragging = False self.dragAxis = None self.dragStartMousePos = None self.dragStartCubePos = None def handleDrag(self, mouse_x, mouse_y): """处理拖拽 - 真正的轴约束拖拽""" if not self.dragAxis or not self.dragStartMousePos or not self.dragStartCubePos: return # 计算从拖拽开始到现在的总鼠标移动距离 total_dx = mouse_x - self.dragStartMousePos[0] total_dy = mouse_y - self.dragStartMousePos[1] # 根据拖拽的轴,确定世界空间中的轴方向向量 if self.dragAxis == "x": world_axis_dir = Vec3(1, 0, 0) # X轴方向 elif self.dragAxis == "y": world_axis_dir = Vec3(0, 1, 0) # Y轴方向 elif self.dragAxis == "z": world_axis_dir = Vec3(0, 0, 1) # Z轴方向 else: return # 将世界轴方向投影到屏幕空间 axis_start_world = self.dragStartCubePos axis_end_world = self.dragStartCubePos + world_axis_dir axis_start_screen = self.worldToScreen(axis_start_world) axis_end_screen = self.worldToScreen(axis_end_world) if not axis_start_screen or not axis_end_screen: return # 计算轴在屏幕空间的方向向量 screen_axis_dir = ( axis_end_screen[0] - axis_start_screen[0], axis_end_screen[1] - axis_start_screen[1] ) # 归一化屏幕轴方向 screen_axis_length = math.sqrt(screen_axis_dir[0]**2 + screen_axis_dir[1]**2) if screen_axis_length > 0.001: screen_axis_dir = ( screen_axis_dir[0] / screen_axis_length, screen_axis_dir[1] / screen_axis_length ) else: return # 将总鼠标移动向量投影到屏幕轴方向上 projected_distance = ( total_dx * screen_axis_dir[0] + total_dy * screen_axis_dir[1] ) # 将投影距离转换为世界空间移动量 move_scale = 0.02 # 移动灵敏度 world_movement = world_axis_dir * projected_distance * move_scale # 从拖拽开始位置计算新位置(确保只沿选定轴移动) new_cube_pos = self.dragStartCubePos + world_movement self.cube.setPos(new_cube_pos) # 坐标轴跟随立方体移动 self.gizmo.setPos(new_cube_pos + Vec3(0, 0, 3)) def colorTestTask(self, task): """颜色测试任务 - 每3秒循环测试坐标轴颜色""" from panda3d.core import RenderState, ColorAttrib self.test_time += globalClock.getDt() # 每3秒切换一次测试 cycle_time = int(self.test_time) % 15 # 15秒一个完整周期 if cycle_time < 3: # 正常颜色 if hasattr(self, 'gizmoXAxis'): self.setAxisColor("x", (1, 0, 0, 1)) # 红色 self.setAxisColor("y", (0, 1, 0, 1)) # 绿色 self.setAxisColor("z", (0, 0, 1, 1)) # 蓝色 elif cycle_time < 6: # 全部黄色 if hasattr(self, 'gizmoXAxis'): self.setAxisColor("x", (1, 1, 0, 1)) self.setAxisColor("y", (1, 1, 0, 1)) self.setAxisColor("z", (1, 1, 0, 1)) elif cycle_time < 9: # 全部白色 if hasattr(self, 'gizmoXAxis'): self.setAxisColor("x", (1, 1, 1, 1)) self.setAxisColor("y", (1, 1, 1, 1)) self.setAxisColor("z", (1, 1, 1, 1)) elif cycle_time < 12: # 全部橙色 if hasattr(self, 'gizmoXAxis'): self.setAxisColor("x", (1, 0.5, 0, 1)) self.setAxisColor("y", (1, 0.5, 0, 1)) self.setAxisColor("z", (1, 0.5, 0, 1)) else: # 恢复正常颜色 if hasattr(self, 'gizmoXAxis'): self.setAxisColor("x", (1, 0, 0, 1)) # 红色 self.setAxisColor("y", (0, 1, 0, 1)) # 绿色 self.setAxisColor("z", (0, 0, 1, 1)) # 蓝色 return task.cont if __name__ == "__main__": app = GizmoTest() app.run()