from panda3d.core import (Point3, Point2, CollisionTraverser, CollisionHandlerQueue, CollisionNode, CollisionRay, GeomNode, LineSegs, RenderState, DepthTestAttrib, ColorAttrib) class EventHandler: """事件处理器 - 处理鼠标和键盘输入事件""" def __init__(self, world): """初始化事件处理器""" self.world = world # 射线显示相关 self.showRay = False # 是否显示射线(默认关闭) self.rayNode = None # 当前显示的射线节点 self.rayLifetime = 2.0 # 射线显示时间(秒) def showClickRay(self, nearPoint, farPoint, hitPos=None): """显示鼠标点击的射线""" if not self.showRay: return try: # 清除之前的射线 self.clearRay() # 创建射线几何体 lines = LineSegs() lines.setThickness(3.0) # 设置射线颜色 if hitPos: # 有碰撞:射线分两段,起点到碰撞点为绿色,碰撞点到终点为红色 lines.setColor(0, 1, 0, 1) # 绿色 lines.moveTo(nearPoint) lines.drawTo(hitPos) lines.setColor(1, 0, 0, 1) # 红色 lines.moveTo(hitPos) lines.drawTo(farPoint) # 在碰撞点添加一个小球 lines.setColor(1, 1, 0, 1) # 黄色 self._addHitMarker(lines, hitPos) else: # 无碰撞:整条射线为蓝色 lines.setColor(0, 0, 1, 1) # 蓝色 lines.moveTo(nearPoint) lines.drawTo(farPoint) # 创建射线节点 geomNode = lines.create() self.rayNode = self.world.render.attachNewNode(geomNode) self.rayNode.setName("clickRay") # 设置渲染状态,确保射线总是可见 state = RenderState.make( DepthTestAttrib.make(DepthTestAttrib.MAlways), # 总是通过深度测试 ColorAttrib.makeFlat((1.0, 1.0, 1.0, 1.0)) ) self.rayNode.setState(state) self.rayNode.setLightOff() # 不受光照影响 # 设置自动清除任务(先清除可能存在的旧任务) from direct.task.TaskManagerGlobal import taskMgr taskMgr.remove("clearRay") # 清除可能存在的旧任务 taskMgr.doMethodLater(self.rayLifetime, self.clearRayTask, "clearRay") print(f"✓ 射线已显示,{self.rayLifetime}秒后自动清除") except Exception as e: print(f"显示射线失败: {str(e)}") def _addHitMarker(self, lines, hitPos): """在碰撞点添加标记""" # 创建一个小十字标记 marker_size = 0.5 # X方向线 lines.moveTo(hitPos.x - marker_size, hitPos.y, hitPos.z) lines.drawTo(hitPos.x + marker_size, hitPos.y, hitPos.z) # Y方向线 lines.moveTo(hitPos.x, hitPos.y - marker_size, hitPos.z) lines.drawTo(hitPos.x, hitPos.y + marker_size, hitPos.z) # Z方向线 lines.moveTo(hitPos.x, hitPos.y, hitPos.z - marker_size) lines.drawTo(hitPos.x, hitPos.y, hitPos.z + marker_size) def clearRay(self): """清除当前显示的射线""" if self.rayNode: self.rayNode.removeNode() self.rayNode = None # 同时清除可能存在的任务 from direct.task.TaskManagerGlobal import taskMgr taskMgr.remove("clearRay") def clearRayTask(self, task): """清除射线的任务回调""" self.clearRay() return task.done def toggleRayDisplay(self): """切换射线显示状态""" self.showRay = not self.showRay if not self.showRay: self.clearRay() print(f"射线显示: {'开启' if self.showRay else '关闭'}") return self.showRay def mousePressEventLeft(self, evt): """处理鼠标左键按下事件""" print("\n=== 开始处理鼠标左键事件 ===") print(f"当前工具: {self.world.currentTool}") if not evt: print("事件为空") return # 获取鼠标点击的位置 x = evt.get('x', 0) y = evt.get('y', 0) print(f"鼠标点击位置: ({x}, {y})") # 获取准确的窗口尺寸 winWidth, winHeight = self.world.getWindowSize() # 直接使用 x, y 创建鼠标位置 mx = 2.0 * x / float(winWidth) - 1.0 my = 1.0 - 2.0 * y / float(winHeight) print(f"转换后的坐标: ({mx}, {my})") # 创建射线 nearPoint = Point3() farPoint = Point3() self.world.cam.node().getLens().extrude(Point2(mx, my), nearPoint, farPoint) print(f"相机坐标系射线起点: {nearPoint}") print(f"相机坐标系射线终点: {farPoint}") # 将相机坐标系的点转换到世界坐标系 worldNearPoint = self.world.render.getRelativePoint(self.world.cam, nearPoint) worldFarPoint = self.world.render.getRelativePoint(self.world.cam, farPoint) print(f"世界坐标系射线起点: {worldNearPoint}") print(f"世界坐标系射线终点: {worldFarPoint}") # 进行射线检测 picker = CollisionTraverser() queue = CollisionHandlerQueue() pickerNode = CollisionNode('mouseRay') pickerNP = self.world.cam.attachNewNode(pickerNode) # 设置射线的碰撞掩码,匹配模型的碰撞掩码(第2位) from panda3d.core import BitMask32 pickerNode.setFromCollideMask(BitMask32.bit(2)) # 使用相机坐标系的点创建射线(因为pickerNP是相机的子节点) direction = farPoint - nearPoint direction.normalize() pickerNode.addSolid(CollisionRay(nearPoint, direction)) picker.addCollider(pickerNP, queue) picker.traverse(self.world.render) print(f"碰撞检测结果数量: {queue.getNumEntries()}") # 射线检测结果处理 hitPos = None hitNode = None if queue.getNumEntries() > 0: # 获取最近的碰撞点 entry = queue.getEntry(0) hitPos = entry.getSurfacePoint(self.world.render) hitNode = entry.getIntoNodePath() print(f"碰撞到节点: {hitNode.getName()}") # 显示射线(使用世界坐标系的点) self.showClickRay(worldNearPoint, worldFarPoint, hitPos) # 优先检查是否点击了坐标轴 print(f"检查坐标轴点击: 坐标轴存在={bool(self.world.selection.gizmo)}") if self.world.selection.gizmo: print("准备检查坐标轴点击...") try: gizmoAxis = self.world.selection.checkGizmoClick(x, y) if gizmoAxis: print(f"✓ 检测到坐标轴点击: {gizmoAxis}") # 开始坐标轴拖拽 self.world.selection.startGizmoDrag(gizmoAxis, x, y) pickerNP.removeNode() return else: print("× 没有点击到坐标轴") except Exception as e: print(f"❌ 坐标轴点击检测出现异常: {str(e)}") import traceback traceback.print_exc() print("继续处理模型选择...") print("继续处理碰撞结果...") if hitPos and hitNode: print(f"✓ 检测到碰撞,开始处理点击事件") print(f"GUI编辑模式: {self.world.guiEditMode}") print(f"当前工具: {self.world.currentTool}") # 处理GUI编辑模式 if self.world.guiEditMode: print("处理GUI编辑模式点击") # 检查是否点击了GUI元素 clickedGUI = self.world.gui_manager.findClickedGUI(hitNode) if clickedGUI: # 选中GUI元素 self.world.selection.updateSelection(clickedGUI) self.world.gui_manager.selectGUIInTree(clickedGUI) print(f"选中GUI元素: {clickedGUI.getTag('gui_text')}") elif hasattr(self.world, 'currentGUITool') and self.world.currentGUITool: # 在点击位置创建新GUI元素 self.world.gui_manager.createGUIAtPosition(hitPos, self.world.currentGUITool) pickerNP.removeNode() return # 根据当前工具处理点击事件 if self.world.currentTool == "选择": print("✓ 使用选择工具处理点击") try: self._handleSelectionClick(hitNode) print("✓ 选择处理完成") except Exception as e: print(f"❌ 选择处理出现异常: {str(e)}") import traceback traceback.print_exc() else: print(f"当前工具不是'选择',无法处理: {self.world.currentTool}") else: print("没有检测到碰撞") # 如果不在GUI编辑模式,清除选择 if not self.world.guiEditMode: self.world.selection.updateSelection(None) # 在GUI编辑模式下,即使没有碰撞也可以在空白区域创建GUI if (self.world.guiEditMode and hasattr(self.world, 'currentGUITool') and self.world.currentGUITool): # 使用默认的地面高度创建GUI default_height = 0.0 world_pos = Point3(mx * 10, 0, my * 10) # 简单的屏幕到世界坐标转换 world_pos.setZ(default_height) self.world.gui_manager.createGUIAtPosition(world_pos, self.world.currentGUITool) # 确保总是清理碰撞检测节点 try: pickerNP.removeNode() print("✓ 碰撞检测节点已清理") except Exception as e: print(f"清理碰撞检测节点失败: {str(e)}") print("=== 鼠标左键事件处理结束 ===\n") def _handleSelectionClick(self, hitNode): """处理选择工具的点击事件""" print(f"开始处理选择点击,碰撞节点: {hitNode.getName()}") # 查找对应的实际模型节点 selectedModel = None # 如果点击的是碰撞节点,找到它的父模型 if isinstance(hitNode.node(), CollisionNode): print(f"点击的是碰撞节点: {hitNode.getName()}") # 碰撞节点的父节点应该是模型 parent = hitNode.getParent() if parent in self.world.models: selectedModel = parent print(f"找到对应的模型: {selectedModel.getName()}") else: print(f"碰撞节点的父节点不是模型: {parent.getName()}") else: # 查找可选择的节点(模型或其子节点) current = hitNode while current != self.world.render: # 检查是否是模型 if current in self.world.models: selectedModel = current print(f"找到模型节点: {selectedModel.getName()}") break # 检查是否是模型的子节点 for model in self.world.models: if current.getParent() == model or current.isAncestorOf(model): selectedModel = model print(f"找到父模型: {selectedModel.getName()}") break if selectedModel: break current = current.getParent() if selectedModel: print(f"✓ 最终选中模型: {selectedModel.getName()}") # 更新选择状态并显示选择框和坐标轴 self.world.selection.updateSelection(selectedModel) # 在树形控件中查找并选中对应的项 if self.world.interface_manager.treeWidget: print("查找树形控件中的对应项...") root = self.world.interface_manager.treeWidget.invisibleRootItem() foundItem = None for i in range(root.childCount()): sceneItem = root.child(i) if sceneItem.text(0) == "场景": print(f"在场景节点下查找...") foundItem = self.world.interface_manager.findTreeItem(selectedModel, sceneItem) if foundItem: print(f"✓ 在树形控件中找到对应项: {foundItem.text(0)}") self.world.interface_manager.treeWidget.setCurrentItem(foundItem) self.world.property_panel.updatePropertyPanel(foundItem) else: print("× 在树形控件中没有找到对应项") break if not foundItem: print("× 没有找到场景节点或对应的树形项") else: print("× 树形控件不存在") else: print("× 没有找到可选择的模型节点") def mouseReleaseEventLeft(self, evt): """处理鼠标左键释放事件""" # 处理坐标轴拖拽结束 if self.world.selection.isDraggingGizmo: self.world.selection.stopGizmoDrag() return def wheelForward(self, data=None): """处理滚轮向前滚动(前进)""" # 调用CoreWorld的父类方法 super(type(self.world), self.world).wheelForward(data) # 更新属性面板 if (self.world.interface_manager.treeWidget and self.world.interface_manager.treeWidget.currentItem() and self.world.interface_manager.treeWidget.currentItem().text(0) == "相机"): self.world.property_panel.updatePropertyPanel( self.world.interface_manager.treeWidget.currentItem()) def wheelBackward(self, data=None): """处理滚轮向后滚动(后退)""" # 调用CoreWorld的父类方法 super(type(self.world), self.world).wheelBackward(data) # 更新属性面板 if (self.world.interface_manager.treeWidget and self.world.interface_manager.treeWidget.currentItem() and self.world.interface_manager.treeWidget.currentItem().text(0) == "相机"): self.world.property_panel.updatePropertyPanel( self.world.interface_manager.treeWidget.currentItem()) def mousePressEventMiddle(self, evt): """处理鼠标中键按下事件""" # 已移除原有的Z轴拖拽功能 pass def mouseReleaseEventMiddle(self, evt): """处理鼠标中键释放事件""" # 已移除原有的Z轴拖拽功能 pass def mouseMoveEvent(self, evt): """处理鼠标移动事件""" if not evt: return # 处理坐标轴拖拽 if self.world.selection.isDraggingGizmo: x = evt.get('x', 0) y = evt.get('y', 0) # 获取准确的窗口尺寸 winWidth, winHeight = self.world.getWindowSize() # 将屏幕坐标转换为世界坐标 mx = 2.0 * x / float(winWidth) - 1.0 my = 1.0 - 2.0 * y / float(winHeight) # 更新坐标轴拖拽 self.world.selection.updateGizmoDrag(x, y) return # 更新坐标轴高亮(鼠标悬停效果) if self.world.selection.gizmo and not self.world.selection.isDraggingGizmo: x = evt.get('x', 0) y = evt.get('y', 0) # 减少高亮调试输出,只在需要时输出 # 已静默处理,避免控制台刷屏 self.world.selection.updateGizmoHighlight(x, y) # 调用CoreWorld的父类方法处理基础的相机旋转 super(type(self.world), self.world).mouseMoveEvent(evt)