EG/core/event_handler.py

512 lines
20 KiB
Python
Raw Permalink 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)
from core.editor_context import get_editor_context
class EventHandler:
"""事件处理器 - 处理鼠标和键盘输入事件"""
def __init__(self, world):
"""初始化事件处理器"""
self.world = world
self._editor_context = get_editor_context(world)
# 射线显示相关
self.showRay = False # 是否显示射线(默认关闭)
self.rayNode = None # 当前显示的射线节点
self.rayLifetime = 2.0 # 射线显示时间(秒)
def _get_editor_context(self):
if not getattr(self, "_editor_context", None):
self._editor_context = get_editor_context(self.world)
return self._editor_context
def _get_tree_widget(self):
"""Qt tree has been removed in the ImGui editor."""
return self._get_editor_context().get_tree_widget()
def _get_gui_manager(self):
"""安全获取 GUI 管理器。"""
return self._get_editor_context().get_gui_manager()
def _sync_tree_selection(self, selected_model):
"""Scene tree selection is rendered directly from editor state in ImGui."""
return False
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
if hitPos and hitNode:
#print(f"✓ 检测到碰撞,开始处理点击事件")
#print(f"当前工具: {self.world.currentTool}")
# 根据当前工具处理点击事件
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("没有检测到碰撞")
self.world.selection.updateSelection(None)
# 确保总是清理碰撞检测节点
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()}")
def _is_valid_node(node):
if not node:
return False
try:
return not node.isEmpty()
except Exception:
return False
def _is_helper_node(node):
if not _is_valid_node(node):
return True
try:
name = node.getName() or ""
except Exception:
name = ""
lowered = name.lower()
return (
isinstance(node.node(), CollisionNode)
or lowered.startswith("modelcollision_")
or lowered.startswith("collision_")
or lowered.startswith("gizmo")
)
def _is_selectable_scene_node(node):
if not _is_valid_node(node) or node == self.world.render:
return False
if _is_helper_node(node):
return False
try:
if node.hasTag("is_scene_element") or node.hasTag("is_model_root"):
return True
except Exception:
pass
return True
def _resolve_pick_target(node):
current = node
fallback_model_root = None
model_list = self.world.models if hasattr(self.world, "models") else []
while _is_valid_node(current) and current != self.world.render:
try:
if current in model_list or current.hasTag("is_model_root"):
fallback_model_root = current
if not _is_helper_node(current):
return current
if _is_selectable_scene_node(current):
return current
current = current.getParent()
except Exception:
break
return fallback_model_root
selected_node = _resolve_pick_target(hitNode)
if selected_node:
print(f"最终选中节点: {selected_node.getName()}")
self.world.selection.handleMouseClick(selected_node)
# 更新选择状态并显示选择框和坐标轴
self.world.selection.updateSelection(selected_node)
# 在树形控件中查找并选中对应的项
if self._sync_tree_selection(selected_node):
tree_widget = self._get_tree_widget()
if tree_widget and tree_widget.currentItem():
print(f"✓ 在树形控件中找到对应项: {tree_widget.currentItem().text(0)}")
else:
print("× 场景树未同步(接口不可用或未找到对应项)")
else:
print("× 没有找到可选择的模型节点")
self.world.selection.updateSelection(None)
def mouseReleaseEventLeft(self, evt):
"""处理鼠标左键释放事件"""
return
def wheelForward(self, data=None):
"""处理滚轮向前滚动(前进)"""
# 调用CoreWorld的父类方法
super(type(self.world), self.world).wheelForward(data)
# 更新属性面板
tree_widget = self._get_tree_widget()
current_item = tree_widget.currentItem() if tree_widget else None
if (current_item and
current_item.text(0) == "相机" and
hasattr(self.world, "property_panel")):
self.world.property_panel.updatePropertyPanel(current_item)
def wheelBackward(self, data=None):
"""处理滚轮向后滚动(后退)"""
# 调用CoreWorld的父类方法
super(type(self.world), self.world).wheelBackward(data)
# 更新属性面板
tree_widget = self._get_tree_widget()
current_item = tree_widget.currentItem() if tree_widget else None
if (current_item and
current_item.text(0) == "相机" and
hasattr(self.world, "property_panel")):
self.world.property_panel.updatePropertyPanel(current_item)
def mousePressEventMiddle(self, evt):
"""处理鼠标中键按下事件"""
# 已移除原有的Z轴拖拽功能
pass
def mouseReleaseEventMiddle(self, evt):
"""处理鼠标中键释放事件"""
# 已移除原有的Z轴拖拽功能
pass
def mouseMoveEvent(self, evt):
"""处理鼠标移动事件"""
if not evt:
return
# 调用CoreWorld的父类方法处理基础的相机旋转
super(type(self.world), self.world).mouseMoveEvent(evt)