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=== EventHandler开始处理鼠标左键事件 ===") print(f"🔧 当前工具: {getattr(self.world, 'currentTool', '未知')}") if not evt: print("❌ 事件为空") return if self.world.currentTool == "地形编辑": print("🔧 地形编辑模式,调用地形编辑处理") self._handleTerrainEdit(evt,"add") return # 获取鼠标点击的位置 x = evt.get('x', 0) y = evt.get('y', 0) print(f"📍 EventHandler收到鼠标点击位置: ({x:.1f}, {y:.1f})") # 获取准确的窗口尺寸 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) # 处理ImGui鼠标点击 if hasattr(self.world, 'processImGuiMouseClick'): if self.world.processImGuiMouseClick(x, y): # 如果ImGui处理了点击,不再进行其他处理 pickerNP.removeNode() return # 优先检查是否点击了坐标轴 #print(f"检查坐标轴点击: 坐标轴存在={bool(self.world.selection.gizmo)}") if self.world.selection.gizmo: #print("准备检查坐标轴点击...") try: highlighted_axis = self.world.selection.gizmoHighlightAxis if highlighted_axis: print(f"✓ 检测到高亮轴: {highlighted_axis},直接开始拖拽") # 直接使用高亮轴开始拖拽 self.world.selection.startGizmoDrag(highlighted_axis, x, y) pickerNP.removeNode() return # 如果没有高亮轴,再尝试检测点击 gizmoAxis = self.world.selection.checkGizmoClick(x, y) if gizmoAxis: print(f"✓ 检测到坐标轴点击: {gizmoAxis}") # 开始坐标轴拖拽 self.world.selection.startGizmoDrag(gizmoAxis, x, y) pickerNP.removeNode() return else: print("× 没有点击到坐标轴") # 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 in ("选择", "移动", "旋转", "缩放"): 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 mousePressEventRight(self,evt): """处理鼠标右键按下事件""" print(f"当前工具: {self.world.currentTool}") # 检查是否是地形编辑模式 if self.world.currentTool == "地形编辑": self._handleTerrainEdit(evt, "subtract") # 降低地形 return # 其他右键处理逻辑可以在这里添加 print("鼠标右键事件处理") def _handleTerrainEdit(self, evt, operation): """处理地形编辑""" try: 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) nearPoint = Point3() farPoint = Point3() self.world.cam.node().getLens().extrude(Point2(mx, my), nearPoint, farPoint) worldNearPoint = self.world.render.getRelativePoint(self.world.cam, nearPoint) worldFarPoint = self.world.render.getRelativePoint(self.world.cam, farPoint) picker = CollisionTraverser() queue = CollisionHandlerQueue() pickerNode = CollisionNode('terrain_edit_ray') pickerNP = self.world.cam.attachNewNode(pickerNode) from panda3d.core import BitMask32 # 确保我们检测所有碰撞体 pickerNode.setFromCollideMask(BitMask32.bit(2)) # 使用相机坐标系的点创建射线 direction = farPoint - nearPoint direction.normalize() pickerNode.addSolid(CollisionRay(nearPoint, direction)) picker.addCollider(pickerNP, queue) picker.traverse(self.world.render) print(f"地形碰撞检测结果数量: {queue.getNumEntries()}") # 添加调试信息,显示所有场景中的碰撞体 print("场景中的碰撞体:") def show_colliders(node, level=0): indent = " " * level if not node.isEmpty(): if isinstance(node.node(), CollisionNode): print(f"{indent}CollisionNode: {node.getName()}") for child in node.getChildren(): show_colliders(child, level + 1) show_colliders(self.world.render) # 射线检测结果处理 hitPos = None hitNode = None hitTerrainInfo = None if queue.getNumEntries() > 0: queue.sortEntries() # 遍历所有碰撞结果,找到地形节点 for i in range(queue.getNumEntries()): entry = queue.getEntry(i) collided_node = entry.getIntoNodePath() print(f"碰撞到节点: {collided_node.getName()}") # 检查是否是地形节点 for terrain_info in self.world.terrain_manager.terrains: terrain_node = terrain_info['node'] if collided_node == terrain_node or terrain_node.isAncestorOf(collided_node): hitPos = entry.getSurfacePoint(self.world.render) hitNode = collided_node hitTerrainInfo = terrain_info print(f"找到地形节点: {terrain_node.getName()}") break if hitPos: break if hitPos and hitTerrainInfo: # 修改地形高度 x_pos, y_pos = hitPos.getX(), hitPos.getY() # 使用 getattr 获取地形编辑参数 radius = getattr(self.world, 'terrain_edit_radius', 3.0) strength = getattr(self.world, 'terrain_edit_strength', 0.3) print(f"准备编辑地形: 位置({x_pos:.2f}, {y_pos:.2f}), 半径{radius}, 强度{strength}, 操作{operation}") # 添加更多调试信息 print(f"地形信息: {hitTerrainInfo}") if 'heightmap' in hitTerrainInfo: print(f"高度图路径: {hitTerrainInfo['heightmap']}") try: # 检查 terrain_manager 是否有 modifyTerrainHeight 方法 if not hasattr(self.world.terrain_manager, 'modifyTerrainHeight'): print("✗ 错误: terrain_manager 没有 modifyTerrainHeight 方法") self.showClickRay(worldNearPoint, worldFarPoint, hitPos) return # 调用地形编辑方法 success = self.world.terrain_manager.modifyTerrainHeight( hitTerrainInfo, x_pos, y_pos, radius=radius, strength=strength, operation=operation) if success: print(f"✓ 地形编辑成功: {operation} at ({x_pos:.2f}, {y_pos:.2f})") # 显示射线 self.showClickRay(worldNearPoint, worldFarPoint, hitPos) # 强制刷新地形显示 if 'terrain' in hitTerrainInfo: hitTerrainInfo['terrain'].generate() print("✓ 地形已刷新") else: print("✗ 地形编辑失败") # 即使编辑失败也显示射线 self.showClickRay(worldNearPoint, worldFarPoint, hitPos) except Exception as e: print(f"✗ 地形编辑过程中出现异常: {e}") import traceback traceback.print_exc() # 即使编辑失败也显示射线 self.showClickRay(worldNearPoint, worldFarPoint, hitPos) else: print("没有检测到地形碰撞") # 显示射线(无碰撞) self.showClickRay(worldNearPoint, worldFarPoint) # 清理碰撞检测节点 pickerNP.removeNode() print("地形编辑处理完成") except Exception as e: print(f"地形编辑处理出错: {e}") import traceback traceback.print_exc() 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.handleMouseClick(selectedModel) # 更新选择状态并显示选择框和坐标轴 self.world.selection.updateSelection(selectedModel) # 在树形控件中查找并选中对应的项 if hasattr(self.world, 'interface_manager') and self.world.interface_manager and hasattr(self.world.interface_manager, 'treeWidget') and 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)}") try: self.world.interface_manager.treeWidget.itemClicked.disconnect() except TypeError: pass self.world.interface_manager.treeWidget.setCurrentItem(foundItem) self.world.interface_manager.treeWidget.itemClicked.connect( self.world.interface_manager.onTreeItemClicked) else: print("× 在树形控件中没有找到对应项") break if not foundItem: print("× 没有找到场景节点或对应的树形项") else: print("× 树形控件不存在") else: print("× 没有找到可选择的模型节点") self.world.selection.updateSelection(None) 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)