EG/core/event_handler.py
2025-09-17 17:48:06 +08:00

565 lines
23 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
if self.world.currentTool == "地形编辑":
self._handleTerrainEdit(evt,"add")
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 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 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)