diff --git a/core/CustomMouseController.py b/core/CustomMouseController.py index 9c0d7e45..ac4132e9 100644 --- a/core/CustomMouseController.py +++ b/core/CustomMouseController.py @@ -6,7 +6,7 @@ class CustomMouseController: self.showbase = showbase # This is used to store which keys are currently pressed. self.keyMap = { - "mouse1": 0, + # "mouse1": 0, # 移除左键,用于选择功能 "mouse3": 0, # 右键 "cam-forward": 0, "cam-backward": 0, @@ -16,9 +16,9 @@ class CustomMouseController: "cam-down": 0 } - # 添加鼠标控制 - self.showbase.accept("mouse1", self.setKey, ["mouse1", True]) - self.showbase.accept("mouse1-up", self.setKey, ["mouse1", False]) + # 添加鼠标控制 - 只保留右键控制,左键用于选择 + # self.showbase.accept("mouse1", self.setKey, ["mouse1", True]) # 移除左键绑定 + # self.showbase.accept("mouse1-up", self.setKey, ["mouse1", False]) # 移除左键绑定 self.showbase.accept("mouse3", self.setKey, ["mouse3", True]) # 右键 self.showbase.accept("mouse3-up", self.setKey, ["mouse3", False]) # 右键释放 @@ -49,7 +49,8 @@ class CustomMouseController: def setKey(self, key, value, arg: str = None): self.keyMap[key] = value - if (key == "mouse1" or key == "mouse3") and value == True: + # 只在右键按下时记录鼠标位置 + if key == "mouse3" and value == True: mouse_pos = self.showbase.mouseWatcherNode.getMouse() if mouse_pos: self.last_mouse_x = mouse_pos.get_x() @@ -72,7 +73,7 @@ class CustomMouseController: self.showbase.camera.setZ(self.showbase.camera, +self.move_speed * dt) if self.keyMap["cam-down"]: self.showbase.camera.setZ(self.showbase.camera, -self.move_speed * dt) - if self.keyMap["mouse1"] or self.keyMap["mouse3"]: # 左键或右键按下 + if self.keyMap["mouse3"]: # 只使用右键控制视角旋转 try: # 检查是否应该处理鼠标事件(避免与ImGui冲突) if self._should_handle_mouse(): diff --git a/core/event_handler.py b/core/event_handler.py index 08326d03..a2425c97 100644 --- a/core/event_handler.py +++ b/core/event_handler.py @@ -113,20 +113,21 @@ class EventHandler: def mousePressEventLeft(self, evt): """处理鼠标左键按下事件""" - print("\n=== 开始处理鼠标左键事件 ===") - #print(f"当前工具: {self.world.currentTool}") + print("\n=== EventHandler开始处理鼠标左键事件 ===") + print(f"🔧 当前工具: {getattr(self.world, 'currentTool', '未知')}") if not evt: - print("事件为空") + print("❌ 事件为空") return if self.world.currentTool == "地形编辑": + print("🔧 地形编辑模式,调用地形编辑处理") self._handleTerrainEdit(evt,"add") return # 获取鼠标点击的位置 x = evt.get('x', 0) y = evt.get('y', 0) - #print(f"鼠标点击位置: ({x}, {y})") + print(f"📍 EventHandler收到鼠标点击位置: ({x:.1f}, {y:.1f})") # 获取准确的窗口尺寸 winWidth, winHeight = self.world.getWindowSize() @@ -184,7 +185,7 @@ class EventHandler: self.showClickRay(worldNearPoint, worldFarPoint, hitPos) # 处理ImGui鼠标点击 - if hasattr(self.world, 'imgui_manager') and self.world.imgui_manager: + if hasattr(self.world, 'processImGuiMouseClick'): if self.world.processImGuiMouseClick(x, y): # 如果ImGui处理了点击,不再进行其他处理 pickerNP.removeNode() @@ -484,7 +485,7 @@ class EventHandler: self.world.selection.updateSelection(selectedModel) # 在树形控件中查找并选中对应的项 - if self.world.interface_manager.treeWidget: + 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 diff --git a/core/selection.py b/core/selection.py index 3371d6d0..f341c8b3 100644 --- a/core/selection.py +++ b/core/selection.py @@ -1,2782 +1,2826 @@ -""" -选择和变换系统模块 - -负责物体选择和变换相关功能: -- 选择框的创建和更新 -- 坐标轴(Gizmo)系统 -- 拖拽变换逻辑 -- 射线检测和碰撞检测 -""" -from direct.showbase.ShowBaseGlobal import globalClock -from panda3d.core import (Vec3, Point3, Point2, LineSegs, ColorAttrib, RenderState, - DepthTestAttrib, CollisionTraverser, CollisionHandlerQueue, - CollisionNode, CollisionRay, GeomNode, BitMask32, Material, LColor, DepthWriteAttrib, - TransparencyAttrib, Vec4, CollisionCapsule) -from direct.task.TaskManagerGlobal import taskMgr -import math - -class SelectionSystem: - """选择和变换系统类""" - - def __init__(self, world): - """初始化选择系统 - - Args: - world: 核心世界对象引用 - """ - self.world = world - - # 选择相关状态 - self.selectedNode = None - self.selectionBox = None # 选择框 - self.selectionBoxTarget = None # 选择框跟踪的目标节点 - - # 坐标轴工具(Gizmo)相关 - self.gizmo = None # 坐标轴 - self.gizmoTarget = None # 坐标轴跟踪的目标节点 - self.gizmoXAxis = None # X轴 - self.gizmoYAxis = None # Y轴 - self.gizmoZAxis = None # Z轴 - self.gizmoRotXAxis = None - self.gizmoRotYAxis = None - self.gizmoRotZAxis = None - self.axis_length = 5.0 # 坐标轴长度(增加到5.0) - - # 拖拽相关状态 - self.isDraggingGizmo = False # 是否正在拖拽坐标轴 - self.dragGizmoAxis = None # 当前拖拽的轴("x", "y", "z") - self.gizmoStartPos = None # 拖拽开始时坐标轴的位置 - self.gizmoTargetStartPos = None # 拖拽开始时目标节点的位置 - self.dragStartMousePos = None # 拖拽开始时的鼠标位置 - - # 高亮相关 - self.gizmoHighlightAxis = None - self.gizmo_colors = { - "x": (1, 0, 0, 1.0), # 红色 - "y": (0, 1, 0, 1.0), # 绿色 - "z": (0, 0, 1, 1.00) # 蓝色 - } - self.gizmo_highlight_colors = { - "x": (1.0, 1.0, 0.0, 1.0), # 黄色高亮 - "y": (1.0, 1.0, 0.0, 1.0), # 黄色高亮 - "z": (1.0, 1.0, 0.0, 1.0) # 黄色高亮 - } - - #性能优化相关 - self._optimized_node = False - self._last_update_time = 0 - self._cached_bounds = {} - self._gizmo_update_interval = 0.1 - self._selection_box_update_interval = 0.2 - - self._current_cursor = None - self._default_cursor = None - - self._last_click_time = 0 - self._double_click_threshold = 0.3 - self._last_clicked_node = None - self._double_click_task = None - - print("✓ 选择和变换系统初始化完成") - # ==================== 光标设置 ==================== - def _setCursor(self,cursor_type): - try: - from PyQt5.QtCore import Qt - if self._current_cursor == cursor_type: - return - if hasattr(self.world,'main_window') and self.world.main_window: - main_window = self.world.main_window - else: - from PyQt5.QtWidgets import QApplication - main_window = QApplication.activeWindow() - if not main_window: - windows = QApplication.topLevelWindows() - for window in windows: - if hasattr(window,'isVisible') and window.isVisible(): - main_window = window - break - if main_window: - if cursor_type == "crosshair": - main_window.setCursor(Qt.CrossCursor) - elif cursor_type == "size_hor": - main_window.setCursor(Qt.SizeHorCursor) - elif cursor_type == "size_ver": - main_window.setCursor(Qt.SizeVerCursor) - elif cursor_type == "size_all": - main_window.setCursor(Qt.SizeAllCursor) - elif cursor_type == "pointing_hand": - main_window.setCursor(Qt.PointingHandCursor) - else: - main_window.unsetCursor() - self._current_cursor = cursor_type - #print(f"光标已设置:{cursor_type}") - self._current_cursor = cursor_type - else: - print("警告:无法获取主窗口,光标设置失败") - except Exception as e: - print(f"设置光标失败{e}") - def _resetCursor(self): - self._setCursor("default") - - # ==================== 选择框系统 ==================== - - def createSelectionBox(self, nodePath): - """为选中的节点创建选择框""" - try: - if self.selectionBox: - #print(" 移除现有选择框") - self.selectionBox.removeNode() - self.selectionBox = None - - if not nodePath: - print(" 目标节点为空,取消创建") - return - - self.selectionBox = self.world.render.attachNewNode("selectionBox") - self.selectionBoxTarget = nodePath - - taskMgr.add(self.updateSelectionBoxTask, "updateSelectionBox") - self.updateSelectionBoxGeometry() - - except Exception as e: - print(f" ✗ 创建选择框失败: {str(e)}") - import traceback - traceback.print_exc() - - def updateSelectionBoxGeometry(self): - """更新选择框的几何形状和位置""" - try: - if not self.selectionBox or not self.selectionBoxTarget: - return - - if self.selectionBoxTarget.isEmpty(): - return - - minPoint = Point3() - maxPoint = Point3() - - try: - has_bounds = self.selectionBoxTarget.calcTightBounds(minPoint, maxPoint, self.world.render) - if not has_bounds: - return - except: - return - - # 检查边界框的有效性 - if (minPoint.x > maxPoint.x or minPoint.y > maxPoint.y or minPoint.z > maxPoint.z or - abs(minPoint.x) > 1e10 or abs(minPoint.y) > 1e10 or abs(minPoint.z) > 1e10 or - abs(maxPoint.x) > 1e10 or abs(maxPoint.y) > 1e10 or abs(maxPoint.z) > 1e10): - print("警告: 检测到无效的边界框,跳过选择框更新") - return - - # 检查是否需要重新计算边界框 - if not hasattr(self, '_bounds_cache'): - self._bounds_cache = {} - - node_name = self.selectionBoxTarget.getName() - import time - current_time = time.time() - - # 如果缓存存在且未过期,则使用缓存 - if (node_name in self._bounds_cache and - current_time - self._bounds_cache[node_name]['time'] < 0.1): - minPoint, maxPoint = self._bounds_cache[node_name]['bounds'] - else: - # 计算新的边界框并缓存 - minPoint = Point3() - maxPoint = Point3() - if not self.selectionBoxTarget.calcTightBounds(minPoint, maxPoint, self.world.render): - return - - # 缓存结果 - self._bounds_cache[node_name] = { - 'bounds': (minPoint, maxPoint), - 'time': current_time - } - - # 清理旧缓存 - expired_keys = [k for k, v in self._bounds_cache.items() - if current_time - v['time'] > 1.0] - for key in expired_keys: - del self._bounds_cache[key] - - # 清除现有的几何体 - self.selectionBox.removeNode() - self.selectionBox = self.world.render.attachNewNode("selectionBox") - - # 获取目标节点在世界坐标系中的边界框(使用正确的API) - minPoint = Point3() - maxPoint = Point3() - if not self.selectionBoxTarget.calcTightBounds(minPoint, maxPoint, self.world.render): - return - - # 获取边界框的最小和最大点(世界坐标) - #print(f"世界边界框: min={minPoint}, max={maxPoint}") - - # 创建线段对象 - lines = LineSegs() - lines.setThickness(2.0) - - # 定义立方体的8个顶点 - vertices = [ - (minPoint.x, minPoint.y, minPoint.z), # 0: 前下左 - (maxPoint.x, minPoint.y, minPoint.z), # 1: 前下右 - (maxPoint.x, maxPoint.y, minPoint.z), # 2: 后下右 - (minPoint.x, maxPoint.y, minPoint.z), # 3: 后下左 - (minPoint.x, minPoint.y, maxPoint.z), # 4: 前上左 - (maxPoint.x, minPoint.y, maxPoint.z), # 5: 前上右 - (maxPoint.x, maxPoint.y, maxPoint.z), # 6: 后上右 - (minPoint.x, maxPoint.y, maxPoint.z), # 7: 后上左 - ] - - # 定义立方体的边(连接顶点的线段) - edges = [ - # 底面 - (0, 1), (1, 2), (2, 3), (3, 0), - # 顶面 - (4, 5), (5, 6), (6, 7), (7, 4), - # 垂直边 - (0, 4), (1, 5), (2, 6), (3, 7) - ] - - # 绘制所有边 - for start, end in edges: - lines.moveTo(*vertices[start]) - lines.drawTo(*vertices[end]) - - # 创建选择框几何体 - geomNode = lines.create() - self.selectionBox.attachNewNode(geomNode) - - # 设置选择框的颜色为亮橙色 - self.selectionBox.setColor(1.0, 0.5, 0.0, 1.0) - - # 设置渲染状态,确保线框总是在最前面显示 - state = RenderState.make( - DepthTestAttrib.make(DepthTestAttrib.MLess), - ColorAttrib.makeFlat((1.0, 0.5, 0.0, 1.0)) - ) - self.selectionBox.setState(state) - - # 确保选择框不被光照影响 - self.selectionBox.setLightOff() - - # 让选择框稍微大一点,避免与模型重叠 - self.selectionBox.setScale(1.01) - - except Exception as e: - print(f"更新选择框几何体失败: {str(e)}") - import traceback - traceback.print_exc() - - def updateSelectionBoxTask(self, task): - """选择框更新任务 - 平衡性能和实时性""" - try: - update_interval = 0.05 - - if not hasattr(self, '_last_selection_box_update'): - self._last_selection_box_update = 0 - - import time - current_time = time.time() - if current_time - self._last_selection_box_update < update_interval: - return task.cont - self._last_selection_box_update = current_time - - # 检查目标节点是否已被删除 - self.checkAndClearIfTargetDeleted() - - if not self.selectionBox or not self.selectionBoxTarget: - return task.done - - # 检查目标节点是否还存在 - if self.selectionBoxTarget.isEmpty(): - self.clearSelectionBox() - return task.done - - # 检查目标节点是否发生了变化(位置、旋转、缩放) - current_transform = self._getNodeTransformKey(self.selectionBoxTarget) - - if (not hasattr(self, '_last_transform_key') or - self._last_transform_key != current_transform): - # 节点发生了变化,更新选择框 - self.updateSelectionBoxGeometry() - self._last_transform_key = current_transform - - return task.cont - - except Exception as e: - print(f"选择框更新任务出错: {str(e)}") - return task.done - - def _getNodeTransformKey(self, node): - """获取节点变换的关键信息,用于快速比较""" - try: - # 获取节点的关键变换信息 - pos = node.getPos(self.world.render) - hpr = node.getHpr(self.world.render) - scale = node.getScale(self.world.render) - - # 返回一个可以比较的元组 - return (pos.x, pos.y, pos.z, hpr.x, hpr.y, hpr.z, scale.x, scale.y, scale.z) - except: - return None - - def clearSelectionBox(self): - """清除选择框""" - if self.selectionBox: - self.selectionBox.removeNode() - self.selectionBox = None - - # 停止选择框更新任务 - taskMgr.remove("updateSelectionBox") - - # 清除目标节点引用 - self.selectionBoxTarget = None - - print("清除了选择框") - - # ==================== 坐标轴(Gizmo)系统 ==================== - - def createGizmo(self, nodePath): - """为选中的节点创建坐标轴工具 - 保留箭头版本""" - try: - #print(f" 开始创建坐标轴,目标节点: {nodePath.getName()}") - - # 如果已有坐标轴,先移除 - if self.gizmo: - self.gizmo.removeNode() - self.gizmo = None - taskMgr.remove("updateGizmo") - - if not nodePath: - return - - # 创建坐标轴主节点 - self.gizmo = self.world.render.attachNewNode("gizmo") - self.gizmoTarget = nodePath - - # 添加标识标签,便于识别 - self.gizmo.setTag("is_gizmo", "1") - if hasattr(nodePath, 'getName'): - self.gizmo.setTag("gizmo_target", nodePath.getName()) - - # 为各轴添加标签 - if hasattr(self, 'gizmoXAxis') and self.gizmoXAxis: - self.gizmoXAxis.setTag("is_gizmo", "1") - self.gizmoXAxis.setTag("gizmo_axis", "x") - if hasattr(self, 'gizmoYAxis') and self.gizmoYAxis: - self.gizmoYAxis.setTag("is_gizmo", "1") - self.gizmoYAxis.setTag("gizmo_axis", "y") - if hasattr(self, 'gizmoZAxis') and self.gizmoZAxis: - self.gizmoZAxis.setTag("is_gizmo", "1") - self.gizmoZAxis.setTag("gizmo_axis", "z") - - # 设置位置和朝向 - minPoint = Point3() - maxPoint = Point3() - if nodePath.calcTightBounds(minPoint, maxPoint, self.world.render): - center = Point3((minPoint.x + maxPoint.x) * 0.5, - (minPoint.y + maxPoint.y) * 0.5, - (minPoint.z + maxPoint.z) * 0.5) - self.gizmo.setPos(center) - - parent_node = nodePath.getParent() - if parent_node and parent_node != self.world.render: - self.gizmo.setHpr(parent_node.getHpr()) - else: - self.gizmo.setHpr(0, 0, 0) - - # 只调用一次几何体创建 - self.createGizmoGeometry() - - #只调用一次颜色设置 - self.setGizmoAxisColor("x", self.gizmo_colors["x"]) - self.setGizmoAxisColor("y", self.gizmo_colors["y"]) - self.setGizmoAxisColor("z", self.gizmo_colors["z"]) - - self._updateGizmoScreenSize() - - self._setupGizmoRendering() - - self.setupGizmoCollision() - - # 现在才显示坐标轴 - self.gizmo.show() - - # 只启动一次更新任务 - taskMgr.add(self.updateGizmoTask, "updateGizmo") - - #print(f" ✓ 为节点 {nodePath.getName()} 创建了坐标轴") - - except Exception as e: - print(f"创建坐标轴失败: {str(e)}") - def createGizmoGeometry(self): - """创建坐标轴的几何体""" - from panda3d.core import Material - try: - if not self.gizmo: - return - - is_scale_tool = self.world.tool_manager.isScaleTool() if self.world.tool_manager else False - is_rotate_tool = self.world.tool_manager.isRotateTool() if self.world.tool_manager else False - - import os - from panda3d.core import getModelPath, Filename - - # 获取项目根目录 - base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - - # 确保core目录在模型搜索路径中 - model_path = getModelPath() - core_dir = os.path.join(base_dir, "core") - core_filename = Filename.from_os_specific(core_dir) - if not model_path.findFile(core_filename): - model_path.appendDirectory(core_filename) - print(f"✓ 添加core目录到模型搜索路径: {core_dir}") - - if is_scale_tool: - model_filename = "core/UniformScaleHandle.fbx" - elif is_rotate_tool: - model_filename = "core/RotationHandleQuarter.fbx" - arrow_filename = "core/TranslateArrowHandle.fbx" - else: - model_filename = "core/TranslateArrowHandle.fbx" - - # model_paths = [ - # "core/TranslateArrowHandle.fbx", - # "EG/core/TranslateArrowHandle.fbx", - # ] - gizmo_model = None - gizmoRot_model = None - try: - if is_rotate_tool: - gizmo_model = self.world.loader.loadModel(arrow_filename) - gizmoRot_model = self.world.loader.loadModel(model_filename) - else: - gizmo_model = self.world.loader.loadModel(model_filename) - except Exception as e: - print(f"加载模型失败: {e}") - return - - x_rHandle = None - y_rHandle = None - z_rHandle = None - - if is_rotate_tool: - self.gizmoRotXAxis = self.gizmo.attachNewNode("gizmo_rot_x_axis") - x_rHandle = gizmoRot_model.copyTo(self.gizmoRotXAxis) - x_rHandle.setName("x_handle") - self.gizmoRotYAxis = self.gizmo.attachNewNode("gizmo_rot_y_axis") - y_rHandle = gizmoRot_model.copyTo(self.gizmoRotYAxis) - y_rHandle.setName("y_handle") - self.gizmoRotZAxis = self.gizmo.attachNewNode("gizmo_rot_z_axis") - z_rHandle = gizmoRot_model.copyTo(self.gizmoRotZAxis) - z_rHandle.setName("z_handle") - - self.gizmoXAxis = self.gizmo.attachNewNode("gizmo_x_axis") - x_handle = gizmo_model.copyTo(self.gizmoXAxis) - x_handle.setName("x_handle") - - self.gizmoYAxis = self.gizmo.attachNewNode("gizmo_y_axis") - y_handle = gizmo_model.copyTo(self.gizmoYAxis) - y_handle.setName("y_handle") - - self.gizmoZAxis = self.gizmo.attachNewNode("gizmo_z_axis") - z_handle = gizmo_model.copyTo(self.gizmoZAxis) - z_handle.setName("z_handle") - - if is_scale_tool: - x_handle.setHpr(0,-90,0) - x_handle.setScale(0.6,0.03,0.03) - x_handle.setPos(2.2,0,0) - - y_handle.setHpr(90,0,0) - y_handle.setScale(0.6,0.03,0.03) - y_handle.setPos(0,2.2,0) - - z_handle.setHpr(0,0,-90) - z_handle.setScale(0.6,0.03,0.03) - z_handle.setPos(0,0,2.2) - elif is_rotate_tool: - x_rHandle.setHpr(0,0,90) - x_rHandle.setScale(0.025,0.0125,0.0125) - x_rHandle.setPos(0,0,0) - - y_rHandle.setHpr(0,0,0) - y_rHandle.setScale(0.025,0.0125,0.0125) - y_rHandle.setPos(0,0,0) - - z_rHandle.setHpr(-90,0,0) - z_rHandle.setScale(0.025,0.0125,0.0125) - z_rHandle.setPos(0,0,0) - - x_handle.setHpr(0, -90, 0) - x_handle.setScale(0.1, 0.05, 0.05) - x_handle.setPos(0, 0, 0) - - y_handle.setHpr(90, 0, 0) - y_handle.setScale(0.1, 0.05, 0.05) - y_handle.setPos(0, 0, 0) - - z_handle.setHpr(0, 0, -90) - z_handle.setScale(0.1, 0.05, 0.05) - z_handle.setPos(0, 0, 0) - - self.setGizmoRotAxisColor("x", self.gizmo_colors["x"]) - self.setGizmoRotAxisColor("y", self.gizmo_colors["y"]) - self.setGizmoRotAxisColor("z", self.gizmo_colors["z"]) - else: - x_handle.setHpr(0,-90,0) - x_handle.setScale(0.1,0.05,0.05) - x_handle.setPos(0,0,0) - - y_handle.setHpr(90,0,0) - y_handle.setScale(0.1,0.05,0.05) - y_handle.setPos(0,0,0) - - z_handle.setHpr(0,0,-90) - z_handle.setScale(0.1,0.05,0.05) - z_handle.setPos(0,0,0) - - # 设置初始颜色 - self.setGizmoAxisColor("x", self.gizmo_colors["x"]) - self.setGizmoAxisColor("y", self.gizmo_colors["y"]) - self.setGizmoAxisColor("z", self.gizmo_colors["z"]) - - #设置渲染属性,解决模型遮挡和阴影问题 - self._setupGizmoRendering() - - except Exception as e: - print(f"创建坐标轴几何体失败: {str(e)}") - - def _setupGizmoRendering(self): - try: - axis_nodes = [self.gizmoXAxis,self.gizmoYAxis,self.gizmoZAxis] - axis_Rotnodes = [self.gizmoRotXAxis, self.gizmoRotYAxis, self.gizmoRotZAxis] - - # 设置坐标轴主节点的渲染属性 - if self.gizmo: - self.gizmo.setLightOff() # 禁用光照 - self.gizmo.setShaderOff() # 禁用着色器 - self.gizmo.setFogOff() # 禁用雾效 - self.gizmo.setBin("fixed", 40) # 设置为fixed渲染层级,数值越大越优先 - self.gizmo.setDepthWrite(False) # 禁用深度写入 - self.gizmo.setDepthTest(False) # 禁用深度测试,确保始终可见 - - # 设置各轴节点的渲染属性 - for axis_node in axis_nodes: - if axis_node: - axis_node.setLightOff() - axis_node.setShaderOff() - axis_node.setFogOff() - axis_node.setBin("fixed", 40) # 与主节点相同优先级 - axis_node.setDepthWrite(False) # 禁用深度写入 - axis_node.setDepthTest(False) # 禁用深度测试 - - # 设置旋转轴节点的渲染属性 - for axis_rotnode in axis_Rotnodes: - if axis_rotnode: - axis_rotnode.setLightOff() - axis_rotnode.setShaderOff() - axis_rotnode.setFogOff() - axis_rotnode.setBin("fixed", 40) # 与主节点相同优先级 - axis_rotnode.setDepthWrite(False) # 禁用深度写入 - axis_rotnode.setDepthTest(False) # 禁用深度测试 - - # 收集所有handle节点 - arrow_nodes = [] - if self.gizmoXAxis: - x_handle = self.gizmoXAxis.find("x_handle") - if x_handle and not x_handle.isEmpty(): - arrow_nodes.append(x_handle) - if self.gizmoYAxis: - y_handle = self.gizmoYAxis.find("y_handle") - if y_handle and not y_handle.isEmpty(): - arrow_nodes.append(y_handle) - if self.gizmoZAxis: - z_handle = self.gizmoZAxis.find("z_handle") - if z_handle and not z_handle.isEmpty(): - arrow_nodes.append(z_handle) - - rot_nodes = [] - if self.gizmoRotXAxis: - x_rHandle = self.gizmoRotXAxis.find("x_handle") - if x_rHandle and not x_rHandle.isEmpty(): - rot_nodes.append(x_rHandle) - if self.gizmoRotYAxis: - y_rHandle = self.gizmoRotYAxis.find("y_handle") - if y_rHandle and not y_rHandle.isEmpty(): - rot_nodes.append(y_rHandle) - if self.gizmoRotZAxis: - z_rHandle = self.gizmoRotZAxis.find("z_handle") - if z_rHandle and not z_rHandle.isEmpty(): - rot_nodes.append(z_rHandle) - - # 设置handle节点的渲染属性 - for arrow_node in arrow_nodes: - if arrow_node: - arrow_node.setLightOff() - arrow_node.setShaderOff() - arrow_node.setFogOff() - arrow_node.setBin("fixed", 41) # 略高于主节点,确保最优先显示 - arrow_node.setDepthWrite(False) - arrow_node.setDepthTest(False) - # 启用透明度支持 - arrow_node.setTransparency(TransparencyAttrib.MAlpha) - - for rot_node in rot_nodes: - if rot_node: - rot_node.setLightOff() - rot_node.setShaderOff() - rot_node.setFogOff() - rot_node.setBin("fixed", 41) # 略高于主节点,确保最优先显示 - rot_node.setDepthWrite(False) - rot_node.setDepthTest(False) - # 启用透明度支持 - rot_node.setTransparency(TransparencyAttrib.MAlpha) - - except Exception as e: - print(f"设置坐标轴渲染属性失败: {str(e)}") - - def updateGizmoTask(self, task): - """坐标轴更新任务 - 包含固定大小功能""" - try: - # 限制更新频率 - if not hasattr(self, '_last_gizmo_update'): - self._last_gizmo_update = 0 - - import time - current_time = time.time() - if current_time - self._last_gizmo_update < 0.5: # 每0.05秒更新一次 - return task.cont - self._last_gizmo_update = current_time - - #检查目标节点是否已被删除 - self.checkAndClearIfTargetDeleted() - - if not self.gizmo or not self.gizmoTarget: - return task.done - - # 检查目标节点是否还存在 - if self.gizmoTarget.isEmpty(): - self.clearGizmo() - return task.done - - is_scale_tool = self.world.tool_manager.isScaleTool() if self.world.tool_manager else False - is_rotate_tool = self.world.tool_manager.isRotateTool() if self.world.tool_manager else False - was_scale_tool = getattr(self,'_last_tool_scale_state',False) - was_rotate_tool =getattr(self,'_last_tool_rotate_state',False) - - tool_changed = (is_scale_tool!=was_scale_tool) or (is_rotate_tool != was_rotate_tool) - - if tool_changed: - self._last_tool_scale_state = is_scale_tool - self._last_tool_rotate_state = is_rotate_tool - - if self.gizmoXAxis: - self.gizmoXAxis.removeNode() - self.gizmoXAxis = None - if self.gizmoYAxis: - self.gizmoYAxis.removeNode() - self.gizmoYAxis = None - if self.gizmoZAxis: - self.gizmoZAxis.removeNode() - self.gizmoZAxis = None - if self.gizmoRotXAxis: - self.gizmoRotXAxis.removeNode() - self.gizmoRotXAxis = None - if self.gizmoRotYAxis: - self.gizmoRotYAxis.removeNode() - self.gizmoRotYAxis = None - if self.gizmoRotZAxis: - self.gizmoRotZAxis.removeNode() - self.gizmoRotZAxis = None - - self.createGizmoGeometry() - - self.setGizmoAxisColor("x",self.gizmo_colors["x"]) - self.setGizmoAxisColor("y",self.gizmo_colors["y"]) - self.setGizmoAxisColor("z",self.gizmo_colors["z"]) - - self.setupGizmoCollision() - - light_object = self.gizmoTarget.getPythonTag("rp_light_object") - if light_object: - light_pos = light_object.pos - self.gizmo.setPos(light_object.pos) - self.gizmoTarget.setPos(light_pos) - else: - # 只在必要时更新位置和朝向 - self._updateGizmoPositionAndOrientation() - - # 【新功能】:动态调整坐标轴大小,保持固定的屏幕大小 - self._updateGizmoScreenSize() - - return task.cont - - except Exception as e: - print(f"坐标轴更新任务出错: {str(e)}") - return task.done - - def _updateGizmoPositionAndOrientation(self): - """优化的Gizmo位置和朝向更新""" - # 只在必要时重新计算边界框 - if not hasattr(self, '_last_gizmo_bounds_update'): - self._last_gizmo_bounds_update = 0 - - import time - current_time = time.time() - if current_time - self._last_gizmo_bounds_update > 0.2: # 每0.2秒计算一次边界框 - minPoint = Point3() - maxPoint = Point3() - # 添加异常处理 - try: - if self.gizmoTarget.calcTightBounds(minPoint, maxPoint, self.world.render): - # 检查边界框的有效性 - if (abs(minPoint.x) < 1e10 and abs(minPoint.y) < 1e10 and abs(minPoint.z) < 1e10 and - abs(maxPoint.x) < 1e10 and abs(maxPoint.y) < 1e10 and abs(maxPoint.z) < 1e10): - # 计算中心点 - center = Point3((minPoint.x + maxPoint.x) * 0.5, - (minPoint.y + maxPoint.y) * 0.5, - (minPoint.z + maxPoint.z) * 0.5) - self.gizmo.setPos(center) - except Exception as e: - print(f"更新Gizmo位置时出错: {e}") - # if self.gizmoTarget.calcTightBounds(minPoint, maxPoint, self.world.render): - # # 计算中心点 - # center = Point3((minPoint.x + maxPoint.x) * 0.5, - # (minPoint.y + maxPoint.y) * 0.5, - # (minPoint.z + maxPoint.z) * 0.5) - # self.gizmo.setPos(center) - self._last_gizmo_bounds_update = current_time - - is_scale_tool = self.world.tool_manager.isScaleTool() if self.world.tool_manager else False - - # 安区地更新朝向 - - if is_scale_tool: - #self.gizmo.setHpr(self.gizmoTarget.getHpr()) - self.gizmo.setQuat(self.gizmoTarget.getQuat(self.world.render)) - else: - parent_node = self.gizmoTarget.getParent() - if parent_node and parent_node != self.world.render: - parent_hpr = parent_node.getHpr() - self.gizmo.setHpr(parent_hpr) - else: - self.gizmo.setHpr(0,0,0) - - # 更新朝向 - # parent_node = self.gizmoTarget.getParent() - # if parent_node and parent_node != self.world.render: - # parent_hpr = parent_node.getHpr() - # self.gizmo.setHpr(parent_hpr) - # else: - # self.gizmo.setHpr(0, 0, 0) - - def _updateGizmoScreenSize(self): - """动态调整坐标轴大小,保持固定的屏幕大小""" - try: - if not self.gizmo or not self.gizmoTarget: - return - - # 计算相机到坐标轴的距离 - gizmo_world_pos = self.gizmo.getPos(self.world.render) - cam_pos = self.world.cam.getPos() - distance_to_gizmo = (cam_pos - gizmo_world_pos).length() - - # 获取相机视野角度和窗口尺寸 - fov = self.world.cam.node().getLens().getFov()[0] # 水平视野角度 - fov_radians = math.radians(fov) - winWidth, winHeight = self.world.getWindowSize() - - # 计算一个像素在坐标轴距离处对应的世界坐标大小 - pixel_to_world_ratio = distance_to_gizmo * math.tan(fov_radians / 2) / (winWidth / 2) - - # 设定坐标轴在屏幕上的期望像素长度(固定值) - desired_screen_length = 120 # 像素 - - # 计算世界坐标系中的轴长度 - world_axis_length = desired_screen_length * pixel_to_world_ratio - - # 计算缩放比例(相对于基础轴长度) - scale_factor = world_axis_length / self.axis_length - - # 应用缩放到坐标轴 - self.gizmo.setScale(scale_factor) - - # 限制缩放范围,避免过大或过小 - min_scale = 0.001 - max_scale = 100.0 - final_scale = max(min_scale, min(max_scale, scale_factor)) - - if final_scale != scale_factor: - self.gizmo.setScale(final_scale) - - except Exception as e: - # 静默处理错误,避免频繁输出 - pass - - def clearGizmo(self): - """清除坐标轴""" - if self.gizmo: - self.gizmo.removeNode() - self.gizmo = None - - # 停止坐标轴更新任务 - taskMgr.remove("updateGizmo") - - # 清除坐标轴相关引用 - self.gizmoTarget = None - self.gizmoXAxis = None - self.gizmoYAxis = None - self.gizmoZAxis = None - self.isDraggingGizmo = False - self.dragGizmoAxis = None - self.dragStartMousePos = None - self.gizmoTargetStartPos = None - self.gizmoStartPos = None - self._resetCursor() - - - # def setGizmoAxisColor(self, axis, color): - # """设置坐标轴颜色 - RenderPipeline 兼容版本""" - # try: - # from panda3d.core import AntialiasAttrib,TransparencyAttrib - # - # axis_nodes = { - # "x": self.gizmoXAxis, - # "y": self.gizmoYAxis, - # "z": self.gizmoZAxis - # } - # - # if axis in axis_nodes and axis_nodes[axis]: - # axis_node = axis_nodes[axis] - # - # axis_node.setColor(color[0]*20.0,color[1]*20.0,color[2]*20.0,color[3]) - # axis_node.setColorScale(color[0]*10.0,color[1]*10.0,color[2]*10.0,color[3]) - # axis_node.setShaderOff(10000) - # axis_node.setLightOff() - # axis_node.setMaterialOff() - # axis_node.setTextureOff() - # axis_node.setFogOff() - # - # except Exception as e: - # print(f"设置坐标轴颜色失败: {str(e)}") - # # 回退到简单的颜色设置 - # try: - # if axis in axis_nodes and axis_nodes[axis]: - # axis_nodes[axis].setColor(*color) - # except: - # pass - - - def setGizmoRotAxisColor(self, axis, color): - """使用材质设置坐标轴颜色 - RenderPipeline兼容版本""" - try: - from panda3d.core import Material, Vec4,ColorWriteAttrib,DepthWriteAttrib,DepthTestAttrib,TransparencyAttrib - - # 获取对应的轴节点 - axis_nodes = { - "x": self.gizmoRotXAxis, - "y": self.gizmoRotYAxis, - "z": self.gizmoRotZAxis - } - - if axis not in axis_nodes or not axis_nodes[axis]: - return - - axis_node = axis_nodes[axis] - - handle_node = None - handle_node = axis_node.find("x_handle") if axis == "x" else handle_node - handle_node = axis_node.find("y_handle") if axis == "y" else handle_node - handle_node = axis_node.find("z_handle") if axis == "z" else handle_node - - #如果找不到特定名称的节点,尝试查找任何子节点 - if not handle_node: - children = axis_node.getChildren() - if children.getNumPath()>0: - handle_node = children[0] - - if not handle_node: - #print(f"未找到{axis}轴的处理模型") - return - - # 创建或获取材质 - mat = Material() - - # 设置材质属性 - 使用更自然的颜色,避免过亮的自发光 - adjusted_color = Vec4( - min(color[0]*20, 1.0), - min(color[1]*20, 1.0), - min(color[2]*20, 1.0), - color[3] - ) - - mat.setBaseColor(adjusted_color) - #mat.setEmission(Vec4(1, 1, 1, 1.0)) # 自发光 - - # 应用材质 - handle_node.setMaterial(mat, 1) - - - # 设置透明度 - if color[3] < 1.0: - handle_node.setTransparency(TransparencyAttrib.MAlpha) - else: - handle_node.setTransparency(TransparencyAttrib.MNone) - - handle_node.setLightOff() # 禁用光照影响 - handle_node.setShaderOff() # 禁用着色器 - handle_node.setFogOff() # 禁用雾效果 - - handle_node.setBin("fixed",41) - handle_node.setDepthWrite(False) - handle_node.setDepthTest(False) - - # 保存材质引用以便后续修改 - if axis == "x": - self.xMat = mat - elif axis == "y": - self.yMat = mat - elif axis == "z": - self.zMat = mat - - axis_node.setLightOff() - axis_node.setShaderOff() - axis_node.setFogOff() - axis_node.setBin("fixed", 40) - axis_node.setDepthWrite(False) - axis_node.setDepthTest(False) - - except Exception as e: - print(f"设置坐标轴颜色失败: {str(e)}") - # 回退到简单颜色设置 - try: - axis_nodes = { - "x": self.gizmoXAxis, - "y": self.gizmoYAxis, - "z": self.gizmoZAxis - } - - if axis in axis_nodes and axis_nodes[axis]: - axis_nodes[axis].setColor(color[0], color[1], color[2], color[3]) - except: - pass - - def setGizmoAxisColor(self, axis, color): - """使用材质设置坐标轴颜色 - RenderPipeline兼容版本""" - try: - from panda3d.core import Material, Vec4,ColorWriteAttrib,DepthWriteAttrib,DepthTestAttrib,TransparencyAttrib - - # 获取对应的轴节点 - axis_nodes = { - "x": self.gizmoXAxis, - "y": self.gizmoYAxis, - "z": self.gizmoZAxis - } - - if axis not in axis_nodes or not axis_nodes[axis]: - return - - axis_node = axis_nodes[axis] - - if axis_node.isEmpty(): - return - - handle_node = None - handle_node = axis_node.find("x_handle") if axis == "x" else handle_node - handle_node = axis_node.find("y_handle") if axis == "y" else handle_node - handle_node = axis_node.find("z_handle") if axis == "z" else handle_node - - # 如果找不到特定名称的节点,尝试查找任何子节点 - if not handle_node or handle_node.isEmpty(): - children = axis_node.getChildren() - if children.getNumPaths() > 0: - handle_node = children[0] - - if not handle_node: - # print(f"未找到{axis}轴的处理模型") - return - - # 创建或获取材质 - mat = Material() - - adjusted_color = Vec4( - min(color[0], 1.0), - min(color[1], 1.0), - min(color[2], 1.0), - color[3] - ) - - mat.setBaseColor(adjusted_color) - #mat.setEmission(Vec4(1, 1, 1, 1.0)) # 自发光 - - # 应用材质 - handle_node.setMaterial(mat, 1) - - # 设置透明度 - if color[3] < 1.0: - handle_node.setTransparency(TransparencyAttrib.MAlpha) - else: - handle_node.setTransparency(TransparencyAttrib.MNone) - - handle_node.setLightOff() # 禁用光照影响 - handle_node.setShaderOff() # 禁用着色器 - handle_node.setFogOff() # 禁用雾效果 - - handle_node.setBin("fixed",41) - handle_node.setDepthWrite(False) - handle_node.setDepthTest(False) - - # 保存材质引用以便后续修改 - if axis == "x": - self.xMat = mat - elif axis == "y": - self.yMat = mat - elif axis == "z": - self.zMat = mat - - axis_node.setLightOff() - axis_node.setShaderOff() - axis_node.setFogOff() - axis_node.setBin("fixed", 40) - axis_node.setDepthWrite(False) - axis_node.setDepthTest(False) - - except Exception as e: - print(f"设置坐标轴颜色失败: {str(e)}") - # 回退到简单颜色设置 - try: - axis_nodes = { - "x": self.gizmoXAxis, - "y": self.gizmoYAxis, - "z": self.gizmoZAxis - } - - if axis in axis_nodes and axis_nodes[axis]: - axis_nodes[axis].setColor(color[0], color[1], color[2], color[3]) - except: - pass - - # ==================== 鼠标交互处理 ==================== - - def _onGizmoMouseEnter(self, axis): - """鼠标进入坐标轴时的处理 - RenderPipeline 兼容版本""" - try: - # 黄色高亮,增加透明度以确保在 RenderPipeline 下可见 - highlight_color = (1.0, 1.0, 0.0, 1) - self.setGizmoAxisColor(axis, highlight_color) - - # 额外的视觉反馈 - axis_nodes = { - "x": self.gizmoXAxis, - "y": self.gizmoYAxis, - "z": self.gizmoZAxis - } - - if axis in axis_nodes and axis_nodes[axis]: - # 稍微放大以增强视觉效果 - axis_nodes[axis].setScale(1.1) - - self.gizmoHighlightAxis = axis - - except Exception as e: - print(f"鼠标进入坐标轴处理失败: {e}") - - def _onGizmoMouseLeave(self, axis): - """鼠标离开坐标轴时的处理 - RenderPipeline 兼容版本""" - try: - # 恢复原始颜色 - original_colors = { - "x": (1.0, 0.0, 0.0, 1.0), # 红色 - "y": (0.0, 1.0, 0.0, 1.0), # 绿色 - "z": (0.0, 0.0, 1.0, 1.0) # 蓝色 - } - - if axis in original_colors: - self.setGizmoAxisColor(axis, original_colors[axis]) - - # 恢复原始大小 - axis_nodes = { - "x": self.gizmoXAxis, - "y": self.gizmoYAxis, - "z": self.gizmoZAxis - } - - if axis in axis_nodes and axis_nodes[axis]: - axis_nodes[axis].setScale(1.0) - - if self.gizmoHighlightAxis == axis: - self.gizmoHighlightAxis = None - - except Exception as e: - print(f"鼠标离开坐标轴处理失败: {e}") - - def setupGizmoMouseEvents(self): - """设置坐标轴的鼠标事件""" - try: - from direct.showbase.DirectObject import DirectObject - - # 为每个轴设置鼠标事件 - axis_nodes = { - "x": self.gizmoXAxis, - "y": self.gizmoYAxis, - "z": self.gizmoZAxis - } - - for axis_name, axis_node in axis_nodes.items(): - if axis_node: - # 设置碰撞检测标签 - axis_node.setTag("gizmo_axis", axis_name) - axis_node.setTag("pickable", "1") - - - except Exception as e: - print(f"设置坐标轴鼠标事件失败: {e}") - - # ==================== 射线检测和碰撞检测 ==================== - - def checkGizmoClick(self, mouseX, mouseY): - """使用屏幕空间检测是否点击了坐标轴""" - if not self.gizmo or not self.gizmoTarget: - return None - - # 基本参数验证 - if not isinstance(mouseX, (int, float)) or not isinstance(mouseY, (int, float)): - return None - - try: - # 获取坐标轴中心的世界坐标 - gizmo_world_pos = self.gizmo.getPos(self.world.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) - - # 使用Panda3D的内置投影方法 - def worldToScreen(world_pos): - """将世界坐标转换为屏幕坐标""" - # 将世界坐标转换为相机空间 - cam_space_pos = self.world.cam.getRelativePoint(self.world.render, world_pos) - - # 检查是否在相机前方 - if cam_space_pos.getY() <= 0: - return None - - # 使用相机的镜头进行投影 - screen_pos = Point2() - if self.world.cam.node().getLens().project(cam_space_pos, screen_pos): - # 获取准确的窗口尺寸 - win_width, win_height = self.world.getWindowSize() - - # 转换为窗口像素坐标 - win_x = (screen_pos.getX() + 1.0) * 0.5 * win_width - win_y = (1.0 - screen_pos.getY()) * 0.5 * win_height - return (win_x, win_y) - return None - - # 投影各个关键点 - center_screen = worldToScreen(gizmo_world_pos) - x_screen = worldToScreen(x_end) - y_screen = worldToScreen(y_end) - z_screen = worldToScreen(z_end) - - # 如果无法获得屏幕坐标,使用备用方法 - if not center_screen: - return self.checkGizmoClickFallback(mouseX, mouseY) - - # 计算点击阈值 - click_threshold = 15 # 增大检测范围 - - # 检测各个轴,对于端点在屏幕外的轴提供回退方案 - def getClickDetectionPoint(axis_name, original_screen_pos): - if original_screen_pos: - return original_screen_pos - # 如果端点在屏幕外,使用轴长度的一半作为检测点 - if axis_name == "x": - half_end = gizmo_world_pos + Vec3(self.axis_length * 0.5, 0, 0) - elif axis_name == "y": - half_end = gizmo_world_pos + Vec3(0, self.axis_length * 0.5, 0) - elif axis_name == "z": - half_end = gizmo_world_pos + Vec3(0, 0, self.axis_length * 0.5) - else: - return None - return worldToScreen(half_end) - - axes_data = [ - ("x", getClickDetectionPoint("x", x_screen), "X轴"), - ("y", getClickDetectionPoint("y", y_screen), "Y轴"), - ("z", getClickDetectionPoint("z", z_screen), "Z轴") - ] - - for axis_name, axis_screen, axis_label in axes_data: - if axis_screen: - # 计算鼠标到轴线的距离 - distance = self.distanceToLine( - (mouseX, mouseY), center_screen, axis_screen - ) - #print(f"{axis_label}距离: {distance:.2f}") - - if distance < click_threshold: - #print(f"✓ 点击了{axis_label}") - return axis_name - - return None - - except Exception as e: - print(f"坐标轴点击检测失败: {str(e)}") - import traceback - traceback.print_exc() - return None - - def checkGizmoClickFallback(self, mouseX, mouseY): - """备用检测方法:使用固定的屏幕区域""" - - # 获取准确的窗口尺寸 - win_width, win_height = self.world.getWindowSize() - - # 获取窗口中心作为参考点 - center_x = win_width // 2 - center_y = win_height // 2 - - # 定义相对于中心的轴区域(简化假设坐标轴在屏幕中心附近) - axis_length_pixels = 100 # 假设轴长度在屏幕上约100像素 - - # X轴:从中心向右 - x_start = (center_x, center_y) - x_end = (center_x + axis_length_pixels, center_y) - - # Y轴:从中心向上(注意Y轴方向) - y_start = (center_x, center_y) - y_end = (center_x, center_y - axis_length_pixels) - - # Z轴:从中心向右上45度 - z_start = (center_x, center_y) - z_end = (center_x + axis_length_pixels * 0.7, center_y - axis_length_pixels * 0.7) - - threshold = 25 - - # 检测各轴 - if self.distanceToLine((mouseX, mouseY), x_start, x_end) < threshold: - print("✓ 备用方法检测到X轴") - return "x" - elif self.distanceToLine((mouseX, mouseY), y_start, y_end) < threshold: - print("✓ 备用方法检测到Y轴") - return "y" - elif self.distanceToLine((mouseX, mouseY), z_start, z_end) < threshold: - print("✓ 备用方法检测到Z轴") - return "z" - - print("× 备用方法也没有检测到") - return None - - def distanceToLine(self, point, line_start, line_end): - """计算点到线段的距离""" - try: - px, py = point - x1, y1 = line_start - x2, y2 = line_end - - # 计算线段长度 - line_length = ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5 - if line_length == 0: - return ((px - x1) ** 2 + (py - y1) ** 2) ** 0.5 - - # 计算点到线的距离 - t = max(0, min(1, ((px - x1) * (x2 - x1) + (py - y1) * (y2 - y1)) / (line_length ** 2))) - projection_x = x1 + t * (x2 - x1) - projection_y = y1 + t * (y2 - y1) - - distance = ((px - projection_x) ** 2 + (py - projection_y) ** 2) ** 0.5 - return distance - - except Exception as e: - print(f"距离计算错误: {e}") - return float('inf') - - # ==================== 高亮和交互 ==================== - - def detectGizmoAxisAtMouse(self, mouseX, mouseY): - """统一的坐标轴检测方法 - 同时用于高亮和点击检测""" - if not self.gizmo or not self.gizmoTarget: - return None - - try: - # 获取坐标轴中心的世界坐标 - gizmo_world_pos = self.gizmo.getPos(self.world.render) - - #获取坐标轴的世界朝向(考虑旋转) - gizmo_world_quat = self.gizmo.getQuat(self.world.render) - - #计算各轴在世界坐标系中的实际方向向量 - x_axis_world = gizmo_world_quat.xform(Vec3(1,0,0)) - y_axis_world = gizmo_world_quat.xform(Vec3(0,1,0)) - z_axis_world = gizmo_world_quat.xform(Vec3(0,0,1)) - - x_end = gizmo_world_pos + x_axis_world * self.axis_length - y_end = gizmo_world_pos + y_axis_world * self.axis_length - z_end = gizmo_world_pos + z_axis_world * self.axis_length - - - # 计算各轴端点的世界坐标 - # 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) - - # 将3D坐标投影到屏幕坐标 - def worldToScreen(worldPos): - try: - # 转换为相机坐标系 - camPos = self.world.cam.getRelativePoint(self.world.render, worldPos) - - # 检查点是否在相机前方 - if camPos.getY() <= 0: - return None - - # 使用相机lens进行投影 - screenPos = Point2() - lens = self.world.cam.node().getLens() - - if lens.project(camPos, screenPos): - # 获取准确的窗口尺寸 - winWidth, winHeight = self.world.getWindowSize() - - # 转换为像素坐标 - winX = (screenPos.x + 1) * 0.5 * winWidth - winY = (1 - screenPos.y) * 0.5 * winHeight - return (winX, winY) - return None - except: - return None - - # 获取各坐标轴的屏幕投影 - gizmo_screen = worldToScreen(gizmo_world_pos) - x_screen = worldToScreen(x_end) - y_screen = worldToScreen(y_end) - z_screen = worldToScreen(z_end) - - # 如果坐标轴中心不在屏幕内,返回None - if not gizmo_screen: - return None - - # 设置检测阈值 - click_threshold = 35 # 统一使用25像素的检测阈值 - - # 更准确的点到线段距离计算方法 - def distanceToLineSegment(mousePos, start, end): - import math - mx, my = mousePos - x1, y1 = start - x2, y2 = end - - # 线段向量 - dx = x2 - x1 - dy = y2 - y1 - - # 线段长度平方 - length_sq = dx * dx + dy * dy - - if length_sq == 0: - # 线段退化为点 - return math.sqrt((mx - x1) ** 2 + (my - y1) ** 2) - - # 投影参数 - t = max(0, min(1, ((mx - x1) * dx + (my - y1) * dy) / length_sq)) - - # 投影点坐标 - proj_x = x1 + t * dx - proj_y = y1 + t * dy - - # 返回点到投影点的距离 - return math.sqrt((mx - proj_x) ** 2 + (my - proj_y) ** 2) - - mouse_pos = (mouseX, mouseY) - - # 检测各个轴 - 按优先级检测(Z > X > Y) - axes_to_check = [ - ("z", z_screen), - ("x", x_screen), - ("y", y_screen) - ] - - for axis_name, axis_end in axes_to_check: - if axis_end: - distance = distanceToLineSegment(mouse_pos, gizmo_screen, axis_end) - if distance < click_threshold: - return axis_name - - # 如果没有检测到,返回None - return None - - except Exception as e: - # 静默处理错误,避免频繁输出 - return None - - def updateGizmoHighlight(self, mouseX, mouseY): - """更新坐标轴高亮状态""" - if not self.gizmo or self.isDraggingGizmo: - self._resetCursor() - return - - # 使用碰撞检测方法 - #hoveredAxis = self.detectGizmoAxisWithCollision(mouseX, mouseY) - hoveredAxis = self.detectGizmoAxisAtMouse(mouseX, mouseY) - - # 简化稳定性检测逻辑 - if not hasattr(self, '_last_detected_axis'): - self._last_detected_axis = None - - # 如果检测结果发生变化,立即更新高亮 - if hoveredAxis != self._last_detected_axis: - # 更新轴的高亮状态 - if hoveredAxis != self.gizmoHighlightAxis: - # 恢复之前高亮的轴 - if self.gizmoHighlightAxis: - self.setGizmoAxisColor(self.gizmoHighlightAxis, self.gizmo_colors[self.gizmoHighlightAxis]) - - # 高亮新的轴 - if hoveredAxis: - self.setGizmoAxisColor(hoveredAxis, self.gizmo_highlight_colors[hoveredAxis]) - self._setCursor("pointing_hand") - else: - # 如果没有悬停在任何轴上,确保所有轴都恢复原始颜色 - for axis_name in ["x", "y", "z"]: - if axis_name != self.dragGizmoAxis: # 不要改变正在拖拽的轴的颜色 - self.setGizmoAxisColor(axis_name, self.gizmo_colors[axis_name]) - self._resetCursor() - - self.gizmoHighlightAxis = hoveredAxis - - self._last_detected_axis = hoveredAxis - elif hoveredAxis is None: - self._resetCursor() - - def _detectHoveredAxis(self, mouseX, mouseY): - """检测鼠标悬停的轴 - 提取为独立方法""" - # 将原来 updateGizmoHighlight 中的检测逻辑移到这里 - # ... 你原来的检测代码 ... - pass - - def _updateAxisHighlight(self, hoveredAxis): - """更新轴高亮状态 - 确保原子性操作""" - # 恢复之前高亮的轴 - if self.gizmoHighlightAxis: - self.setGizmoAxisColor(self.gizmoHighlightAxis, self.gizmo_colors[self.gizmoHighlightAxis]) - - # 高亮新的轴 - if hoveredAxis: - self.setGizmoAxisColor(hoveredAxis, self.gizmo_highlight_colors[hoveredAxis]) - - self.gizmoHighlightAxis = hoveredAxis - - # ==================== 拖拽变换 ==================== - - def startGizmoDrag(self, axis, mouseX, mouseY): - """开始坐标轴拖拽""" - try: - # 确保状态正确初始化 - if not self.gizmoTarget: - print("开始拖拽失败: 没有拖拽目标") - return - if not self.gizmo: - print("开始拖拽失败: 没有坐标轴") - return - - self.isDraggingGizmo = True - - # 使用当前高亮的轴,如果有的话;否则使用传入的轴 - if self.gizmoHighlightAxis: - self.dragGizmoAxis = self.gizmoHighlightAxis - elif axis and axis in self.gizmo_colors: - self.dragGizmoAxis = axis - else: - # 如果没有明确指定轴,尝试通过鼠标位置检测 - self.dragGizmoAxis = self.detectGizmoAxisAtMouse(mouseX, mouseY) - - # 如果仍然无法确定拖拽轴,则取消拖拽 - if not self.dragGizmoAxis: - print("开始拖拽失败: 无法确定拖拽轴") - self.isDraggingGizmo = False - return - - self.dragStartMousePos = (mouseX, mouseY) - - light_object = self.gizmoTarget.getPythonTag("rp_light_object") - if light_object: - self.gizmoTargetStartPos = Point3(light_object.pos) - else: - self.gizmoTargetStartPos = self.gizmoTarget.getPos() - - # 保存开始拖拽时目标节点的位置和坐标轴的位置 - #self.gizmoTargetStartPos = self.gizmoTarget.getPos() - self.gizmoStartPos = self.gizmo.getPos(self.world.render) # 坐标轴的世界位置 - - # 添加对缩放的支持:保存初始缩放值 - if self.world.tool_manager.isScaleTool(): - self.gizmoTargetStartScale = self.gizmoTarget.getScale() - elif self.world.tool_manager.isRotateTool(): - self.gizmoTargetStartHpr = self.gizmoTarget.getHpr() - - # 确保正在拖动的轴保持高亮状态 - if self.dragGizmoAxis and self.dragGizmoAxis in self.gizmo_colors: - # 先将所有轴恢复为正常颜色 - for axis_name in self.gizmo_colors.keys(): - if axis_name != self.dragGizmoAxis: - self.setGizmoAxisColor(axis_name, self.gizmo_colors[axis_name]) - - # 然后将当前拖动的轴设置为高亮颜色 - self.setGizmoAxisColor(self.dragGizmoAxis, self.gizmo_highlight_colors[self.dragGizmoAxis]) - # elif axis and axis in self.gizmo_colors: - # for axis_name in self.gizmo_colors.keys(): - # if axis_name != axis: - # self.setGizmoAxisColor(axis_name, self.gizmo_colors[axis_name]) - # - # self.setGizmoAxisColor(axis, self.gizmo_highlight_colors[axis]) - # self.dragGizmoAxis = axis - # - # self.gizmoHighlightAxis = self.dragGizmoAxis - # 设置拖拽光标 - if self.dragGizmoAxis == "x": - self._setCursor("size_all") # 水平调整光标 - elif self.dragGizmoAxis == "y": - self._setCursor("size_all") # 垂直调整光标 - elif self.dragGizmoAxis == "z": - self._setCursor("size_all") # 全向调整光标 - - print( - f"开始拖拽 {self.dragGizmoAxis} 轴 - 目标起始位置: {self.gizmoTargetStartPos}, 坐标轴位置: {self.gizmoStartPos}, 鼠标: ({mouseX}, {mouseY})") - - except Exception as e: - print(f"开始坐标轴拖拽失败: {str(e)}") - import traceback - traceback.print_exc() - - def updateGizmoDrag(self, mouseX, mouseY): - """更新坐标轴拖拽 - 使用正确的坐标系变换,支持旋转后的子节点拖拽""" - try: - # 添加详细的状态检查和调试信息 - if not self.isDraggingGizmo: - print("拖拽更新失败: 不在拖拽状态") - return - if not self.gizmoTarget: - print("拖拽更新失败: 没有拖拽目标") - return - if not hasattr(self, 'dragStartMousePos') or not self.dragStartMousePos: - print("拖拽更新失败: 没有拖拽起始位置") - return - if not hasattr(self, 'gizmoTargetStartPos') or not self.gizmoTargetStartPos: - print("拖拽更新失败: 没有目标起始位置") - return - if not hasattr(self, 'gizmoStartPos') or not self.gizmoStartPos: - print("拖拽更新失败: 没有坐标轴起始位置") - return - - is_scale_tool = self.world.tool_manager.isScaleTool() if self.world.tool_manager else False - is_rotate_tool = self.world.tool_manager.isRotateTool() if self.world.tool_manager else False - - is_gui_element = (hasattr(self.gizmoTarget,'getTag') and - self.gizmoTarget.getTag("is_gui_element") == "1") - - - # 计算鼠标移动距离(屏幕像素) - mouseDeltaX = mouseX - self.dragStartMousePos[0] - mouseDeltaY = mouseY - self.dragStartMousePos[1] - - if is_scale_tool: - scale_factor = 1.0 + (mouseDeltaX + mouseDeltaY) * 0.01 - - scale_factor = max(0.001, scale_factor) - - start_scale = getattr(self,'gizmoTargetStartScale',Vec3(1,1,1)) - - if is_gui_element: - if self.dragGizmoAxis == "x": - new_scale = Vec3(start_scale.x * scale_factor,start_scale.y,start_scale.z) - elif self.dragGizmoAxis == "y": - new_scale = Vec3(start_scale.x, start_scale.y * scale_factor, start_scale.z) - elif self.dragGizmoAxis == "z": - new_scale = Vec3(start_scale.x, start_scale.y, start_scale.z * scale_factor) - else: - new_scale = Vec3(start_scale.x * scale_factor, - start_scale.y * scale_factor, - start_scale.z * scale_factor) - else: - # 普通3D模型的缩放处理 - if self.dragGizmoAxis == "x": - new_scale = Vec3(start_scale.x * scale_factor, start_scale.y, start_scale.z) - elif self.dragGizmoAxis == "y": - new_scale = Vec3(start_scale.x, start_scale.y * scale_factor, start_scale.z) - elif self.dragGizmoAxis == "z": - z_scale_factor = 1.0 - (mouseDeltaX + mouseDeltaY) * 0.01 - new_scale = Vec3(start_scale.x, start_scale.y, start_scale.z * z_scale_factor) - else: - new_scale = Vec3(start_scale.x * scale_factor, - start_scale.y * scale_factor, - start_scale.z * scale_factor) - - new_scale = Vec3( - max(0.001,new_scale.x), - max(0.001,new_scale.y), - max(0.001,new_scale.z) - ) - - # 应用新缩放值 - self.gizmoTarget.setScale(new_scale) - self.world.property_panel.refreshModelValues(self.gizmoTarget) - return - elif is_rotate_tool: - rotation_speed = 0.5 - rotation_amount = (mouseDeltaX + mouseDeltaY) * rotation_speed - start_hpr = getattr(self,'gizmoTargetStartHpr',self.gizmoTarget.getHpr()) - - if self.dragGizmoAxis == "x": - new_hpr = Vec3(start_hpr.x+rotation_amount,start_hpr.y,start_hpr.z) - elif self.dragGizmoAxis == "y": - new_hpr = Vec3(start_hpr.x,start_hpr.y-rotation_amount,start_hpr.z) - elif self.dragGizmoAxis == "z": - new_hpr = Vec3(start_hpr.x,start_hpr.y,start_hpr.z+rotation_amount) - else: - # 默认绕所有轴旋转 - new_hpr = Vec3(start_hpr.x + rotation_amount, - start_hpr.y + rotation_amount, - start_hpr.z + rotation_amount) - self.gizmoTarget.setHpr(new_hpr) - self.world.property_panel.refreshModelValues(self.gizmoTarget) - return - - # 使用坐标轴的实际位置而不是目标节点位置来计算屏幕投影 - gizmo_world_pos = self.gizmoStartPos - - # 【关键修复】:获取正确的轴向量,考虑父节点的旋转 - # 检查目标节点是否有父节点 - parent_node = self.gizmoTarget.getParent() - - # 计算轴向量在正确坐标系中的方向 - if self.dragGizmoAxis == "x": - # 在局部坐标系中的X轴方向 - local_axis_vector = Vec3(1, 0, 0) - elif self.dragGizmoAxis == "y": - # 在局部坐标系中的Y轴方向 - local_axis_vector = Vec3(0, 1, 0) - elif self.dragGizmoAxis == "z": - # 在局部坐标系中的Z轴方向 - local_axis_vector = Vec3(0, 0, 1) - else: - print(f"拖拽更新失败: 未知轴类型 {self.dragGizmoAxis}") - return - - world_axis_vector = local_axis_vector - - if parent_node and parent_node != self.world.render: - try: - #获取变换矩阵 - transfrom_mat = parent_node.getMat(self.world.render) - if transfrom_mat.is_identity() or self._isMatrixValid(transfrom_mat): - world_axis_vector = transfrom_mat.xformVec(local_axis_vector) - else: - print("警告: 检测到无效变换矩阵,使用默认轴向量") - except Exception as e: - print(f"变换计算出错: {e},使用默认轴向量") - else: - world_axis_vector = local_axis_vector - - # 确定轴向量的变换上下文 - # if parent_node and parent_node != self.world.render: - # transform_mat = parent_node.getMat(self.world.render) - # world_axis_vector = transform_mat.xformVec(local_axis_vector) - # else: - # world_axis_vector = local_axis_vector - - #axis_end = gizmo_world_pos + world_axis_vector - - # 投影到屏幕空间 - def worldToScreen(worldPos): - try: - camPos = self.world.cam.getRelativePoint(self.world.render, worldPos) - if camPos.getY() <= 0: - return None - - screenPos = Point2() - if self.world.cam.node().getLens().project(camPos, screenPos): - winWidth, winHeight = self.world.getWindowSize() - winX = (screenPos.x + 1) * 0.5 * winWidth - winY = (1 - screenPos.y) * 0.5 * winHeight - return (winX, winY) - return None - except Exception as e: - print(f"世界坐标转屏幕坐标失败: {e}") - return None - axis_start_screen = worldToScreen(gizmo_world_pos) - axis_end_world = gizmo_world_pos + world_axis_vector - axis_end_screen = worldToScreen(axis_end_world) - - if not axis_start_screen or not axis_end_screen: - print("拖拽更新失败: 无法获取轴线屏幕坐标") - return - - screen_axis_dir = ( - axis_end_screen[0] - axis_start_screen[0], - axis_end_screen[1] - axis_start_screen[1] - ) - - # 归一化屏幕轴方向 - import math - length = math.sqrt(screen_axis_dir[0]**2 + screen_axis_dir[1]**2) - if length > 0: - #screen_axis_dir = (screen_axis_dir[0] / length, screen_axis_dir[1] / length) - screen_axis_dir = ( - screen_axis_dir[0] / length, - screen_axis_dir[1] / length - ) - - else: - print("拖拽更新失败: 屏幕轴方向长度为0") - return - - # 将鼠标移动投影到轴方向上 - projected_distance = (mouseDeltaX * screen_axis_dir[0] + - mouseDeltaY * screen_axis_dir[1]) - - cam_pos = self.world.cam.getPos(self.world.render) - distance_to_object = (cam_pos - gizmo_world_pos).length() - - lens = self.world.cam.node().getLens() - fov = lens.getFov()[0] - winWidth,winHeight = self.world.getWindowSize() - - pixels_to_world_units = (2*distance_to_object*math.tan(math.radians(fov/2)))/winWidth - - movement_distance = projected_distance * pixels_to_world_units - - total_scale_factor = 1.0 - current_node = self.gizmoTarget.getParent() - - while current_node and current_node != self.world.render: - try: - if not current_node.isEmpty(): - node_scale = current_node.getScale() - if node_scale.x > 0 and node_scale.y >0 and node_scale.z >0 : - avg_scale = (node_scale.x + node_scale.y + node_scale.z)/3.0 - total_scale_factor *= avg_scale - #avg_scale = (node_scale.x+node_scale.y + node_scale.z) / 3.0 - #total_scale_factor *= avg_scale - current_node = current_node.getParent() - else: - break - except: - break - - - if total_scale_factor > 0: - movement_distance = movement_distance / total_scale_factor - - currentPos = self.gizmoTargetStartPos - - # 根据拖拽的轴,只修改对应的坐标分量 - if self.dragGizmoAxis == "x": - newPos = Vec3(currentPos.x + movement_distance, currentPos.y, currentPos.z) - #print(f"X轴移动:{currentPos.x} -> {newPos.x}") - elif self.dragGizmoAxis == "y": - newPos = Vec3(currentPos.x, currentPos.y + movement_distance, currentPos.z) - #print(f"Y轴移动:{currentPos.y} -> {newPos.y}") - elif self.dragGizmoAxis == "z": - newPos = Vec3(currentPos.x, currentPos.y, currentPos.z + movement_distance) - #print(f"Z轴移动:{currentPos.z} -> {newPos.z}") - else: - print(f"未知轴: {self.dragGizmoAxis}") - return - - # 应用新位置到目标节点 - light_object = self.gizmoTarget.getPythonTag("rp_light_object") - if light_object: - light_object.pos = newPos - self.gizmoTarget.setPos(newPos) - else: - self.gizmoTarget.setPos(newPos) - - # 更新坐标轴位置 - 计算新的中心位置 - minPoint = Point3() - maxPoint = Point3() - if self.gizmoTarget.calcTightBounds(minPoint, maxPoint, self.world.render): - center = Point3((minPoint.x + maxPoint.x) * 0.5, - (minPoint.y + maxPoint.y) * 0.5, - (minPoint.z + maxPoint.z) * 0.5) - self.gizmo.setPos(center) - - # 实时更新属性面板 - self.world.property_panel.refreshModelValues(self.gizmoTarget) - - # 每次拖拽都输出调试信息(但限制频率) - if not hasattr(self, '_last_drag_debug_time'): - self._last_drag_debug_time = 0 - - import time - current_time = time.time() - if current_time - self._last_drag_debug_time > 0.1: # 每0.1秒最多输出一次 - #print(f"拖拽更新成功 - 轴:{self.dragGizmoAxis}, 比例:{scale_factor:.6f}, 投影:{projected_distance:.2f}") - self._last_drag_debug_time = current_time - - except Exception as e: - print(f"更新坐标轴拖拽失败: {str(e)}") - import traceback - traceback.print_exc() - - def _isMatrixValid(self, matrix): - """检查矩阵是否有效,替代 isSingular 方法""" - try: - # 检查矩阵元素是否为有效数字 - for i in range(4): - for j in range(4): - element = matrix.getCell(i, j) - # 检查是否为 NaN 或无穷大 - if str(element) == 'nan' or str(element) == 'inf' or str(element) == '-inf': - return False - # 检查是否过大 - if abs(element) > 1e10: - return False - return True - except: - return False - - def stopGizmoDrag(self): - """停止坐标轴拖拽""" - print(f"停止坐标轴拖拽 - 轴: {self.dragGizmoAxis}") - - if hasattr(self.world,'command_manager') and self.world.command_manager and self.gizmoTarget: - current_pos = self.gizmoTarget.getPos() - - if (hasattr(self,'gizmoTargetStartPos') and self.gizmoTargetStartPos and - (abs(current_pos.x-self.gizmoTargetStartPos.x)>0.001 or - abs(current_pos.y-self.gizmoTargetStartPos.y)>0.001 or - abs(current_pos.z-self.gizmoTargetStartPos.z)>0.001)): - from core.Command_System import MoveNodeCommand - from core.Command_System import MoveLightCommand - - light_object = self.gizmoTarget.getPythonTag("rp_light_object") - if light_object: - command = MoveLightCommand(self.gizmoTarget,self.gizmoTargetStartPos,current_pos,light_object) - else: - command = MoveNodeCommand(self.gizmoTarget,self.gizmoTargetStartPos,current_pos) - self.world.command_manager.execute_command(command) - # 如果是缩放操作且缩放发生了变化,则创建缩放命令 - elif (hasattr(self, 'gizmoTargetStartScale') and hasattr(self, 'gizmoTargetStartScale') and - self.gizmoTargetStartScale): - current_scale = self.gizmoTarget.getScale() - if (abs(current_scale.x - self.gizmoTargetStartScale.x) > 0.001 or - abs(current_scale.y - self.gizmoTargetStartScale.y) > 0.001 or - abs(current_scale.z - self.gizmoTargetStartScale.z) > 0.001): - from core.Command_System import ScaleNodeCommand - command = ScaleNodeCommand(self.gizmoTarget, self.gizmoTargetStartScale, current_scale) - self.world.command_manager.execute_command(command) - # 如果是旋转操作且旋转发生了变化,则创建旋转命令 - elif (hasattr(self, 'gizmoTargetStartHpr') and hasattr(self, 'gizmoTargetStartHpr') and - self.gizmoTargetStartHpr): - current_hpr = self.gizmoTarget.getHpr() - if (abs(current_hpr.x - self.gizmoTargetStartHpr.x) > 0.001 or - abs(current_hpr.y - self.gizmoTargetStartHpr.y) > 0.001 or - abs(current_hpr.z - self.gizmoTargetStartHpr.z) > 0.001): - from core.Command_System import RotateNodeCommand - command = RotateNodeCommand(self.gizmoTarget, self.gizmoTargetStartHpr, current_hpr) - self.world.command_manager.execute_command(command) - - # 恢复所有轴的颜色 - for axis_name in ["x", "y", "z"]: - self.setGizmoAxisColor(axis_name, self.gizmo_colors[axis_name]) - - self.isDraggingGizmo = False - self.dragGizmoAxis = None - self.dragStartMousePos = None - # 清理拖拽状态,下次拖拽开始时重新设置 - self.gizmoTargetStartPos = None - self.gizmoStartPos = None - - if hasattr(self, 'gizmoTargetStartScale'): - delattr(self, 'gizmoTargetStartScale') - if hasattr(self, 'gizmoTargetStartHpr'): - delattr(self, 'gizmoTargetStartHpr') - - # 重置高亮轴 - self.gizmoHighlightAxis = None - self._resetCursor() - # ==================== 选择管理 ==================== - - def updateSelection(self, nodePath): - try: - if self.selectedNode == nodePath: - return - #print(f"\n=== 更新选择状态 ===") - - # 如果正在删除节点,避免更新选择 - if hasattr(self, '_deleting_node') and self._deleting_node: - print("正在删除节点,跳过选择更新") - #print("=== 选择状态更新完成 ===\n") - return - - node_name = "None" - if nodePath and not nodePath.isEmpty(): - node_name = nodePath.getName() - #print(f"新选择的节点: {node_name}") - - self.selectedNode = nodePath - # 添加兼容性属性 - self.selectedObject = nodePath - - if nodePath and not nodePath.isEmpty(): - node_name = nodePath.getName() - #print(f"开始为节点 {node_name} 创建选择框和坐标轴...") - - # 创建选择框 - #print("创建选择框...") - self.createSelectionBox(nodePath) - if self.selectionBox: - box_name = "Unknown" - if self.selectionBox and not self.selectionBox.isEmpty(): - box_name = self.selectionBox.getName() - #print(f"✓ 选择框创建成功: {box_name}") - else: - print("× 选择框创建失败") - - # 创建坐标轴 - #print("创建坐标轴...") - self.createGizmo(nodePath) - if self.gizmo: - gizmo_name = "Unknown" - if self.gizmo and not self.gizmo.isEmpty(): - gizmo_name = self.gizmo.getName() - #print(f"✓ 坐标轴创建成功: {gizmo_name}") - else: - print("× 坐标轴创建失败") - - else: - print("清除选择...") - self.clearSelectionBox() - self.clearGizmo() - print("✓ 取消选择") - - #当取消选择时,同步清空树形控件的选中状态 - if (hasattr(self.world,'interface_manager')and - self.world.interface_manager and - self.world.interface_manager.treeWidget): - self.world.interface_manager.treeWidget.setCurrentItem(None) - print("✓ 树形控件选中状态已清空") - - #print("=== 选择状态更新完成 ===\n") - except Exception as e: - print(f"更新选择状态失败{str(e)}") - import traceback - traceback.print_exc() - - def _reparentTreeItem(self, item, new_parent_item): - """将树项重新父化到新的父项下""" - if not item or not new_parent_item: - return - - # 从当前父项中移除 - current_parent = item.parent() - if current_parent: - current_parent.removeChild(item) - else: - # 如果是顶级项 - index = self.indexOfTopLevelItem(item) - if index >= 0: - self.takeTopLevelItem(index) - - # 添加到新父项 - new_parent_item.addChild(item) - - # def _updateSelectionVisuals(self, nodePath): - # """更新选择的视觉效果(选择框和坐标轴)""" - # try: - # if nodePath and not nodePath.isEmpty(): - # node_name = nodePath.getName() - # print(f"开始为节点 {node_name} 创建选择框和坐标轴...") - # - # # 创建选择框 - # print("创建选择框...") - # self.createSelectionBox(nodePath) - # if self.selectionBox: - # box_name = "Unknown" - # if self.selectionBox and not self.selectionBox.isEmpty(): - # box_name = self.selectionBox.getName() - # print(f"✓ 选择框创建成功: {box_name}") - # else: - # print("× 选择框创建失败") - # - # # 创建坐标轴 - # print("创建坐标轴...") - # self.createGizmo(nodePath) - # if self.gizmo: - # gizmo_name = "Unknown" - # if self.gizmo and not self.gizmo.isEmpty(): - # gizmo_name = self.gizmo.getName() - # print(f"✓ 坐标轴创建成功: {gizmo_name}") - # else: - # print("× 坐标轴创建失败") - # - # print(f"✓ 选中了节点: {node_name}") - # else: - # print("清除选择...") - # self.clearSelectionBox() - # self.clearGizmo() - # print("✓ 取消选择") - # - # except Exception as e: - # print(f"更新选择视觉效果失败: {e}") - - def getSelectedNode(self): - """获取当前选中的节点""" - return self.selectedNode - - def hasSelection(self): - """检查是否有选中的节点""" - return self.selectedNode is not None - - def checkAndClearIfTargetDeleted(self): - if (self.gizmoTarget and self.gizmoTarget.isEmpty()): - self.clearGizmo() - - if (self.selectionBoxTarget and self.selectionBoxTarget.isEmpty()): - self.clearSelectionBox() - - def setupGizmoCollision(self): - if not self.gizmo or not self.gizmoXAxis or not self.gizmoYAxis or not self.gizmoZAxis: - return - - # 清除现有的碰撞节点 - for axis_name in ["x", "y", "z"]: - axis_node = getattr(self, f"gizmo{axis_name.upper()}Axis") - if axis_node: - # 查找并移除所有现有的碰撞节点 - collision_nodes = axis_node.findAllMatches("**/gizmo_collision_*") - for collision_node in collision_nodes: - collision_node.removeNode() - - # 为每个轴创建碰撞体 - self.createAxisCollision("x", self.gizmoXAxis) - self.createAxisCollision("y", self.gizmoYAxis) - self.createAxisCollision("z", self.gizmoZAxis) - - def createAxisCollision(self, axis_name, axis_node): - # 为单个轴创建碰撞体 - try: - handle_node = axis_node.find(f"{axis_name}_handle") - if not handle_node or handle_node.isEmpty(): - children = axis_node.getChildren() - if children.getNumPaths() > 0: - handle_node = children[0] - else: - print(f"警告: 未找到 {axis_name} 轴的 handle 节点") - return - - collision_node = CollisionNode(f"gizmo_collision_{axis_name}") - collision_node.setIntoCollideMask(BitMask32.bit(1)) # 设置为into对象 - collision_node.setFromCollideMask(BitMask32.allOff()) # 不作为from对象 - - # 调整碰撞尺寸以匹配实际的轴长度和坐标轴缩放 - scale_factor = self.gizmo.getScale().x if self.gizmo else 1.0 - axis_length = 2.0 * scale_factor - radius = 0.3 * scale_factor - - # 根据轴的类型创建合适的碰撞体 - if axis_name == "x": - capsule = CollisionCapsule( - Point3(0, 0, 0), - Point3(axis_length, 0, 0), - radius - ) - collision_node.addSolid(capsule) - elif axis_name == "y": - capsule = CollisionCapsule( - Point3(0, 0, 0), - Point3(0, axis_length, 0), - radius - ) - collision_node.addSolid(capsule) - elif axis_name == "z": - capsule = CollisionCapsule( - Point3(0, 0, 0), - Point3(0, 0, axis_length), - radius - ) - collision_node.addSolid(capsule) - - # 将碰撞节点附加到handle节点,使其与可视化几何体保持一致 - collision_np = handle_node.attachNewNode(collision_node) - - # 设置标签以便识别 - collision_np.setTag("gizmo_axis", axis_name) - collision_np.setTag("pickable", "1") - - collision_np.hide() # 隐藏碰撞体,只用于检测 - - #print(f"✓ 成功创建 {axis_name} 轴碰撞体") - - except Exception as e: - print(f"创建{axis_name}轴碰撞体失败: {e}") - import traceback - traceback.print_exc() - - def detectGizmoAxisWithCollision(self, mouseX, mouseY): - # 使用碰撞体检测鼠标是否悬停在坐标轴上 - if not self.gizmo: - return None - - try: - ray = CollisionRay() - - win_width, win_height = self.world.getWindowSize() - - mouse_x_ndc = (mouseX / win_width) * 2.0 - 1.0 - mouse_y_ndc = 1.0 - (mouseY / win_height) * 2.0 - - ray.setFromLens(self.world.cam.node(), mouse_x_ndc, mouse_y_ndc) - - traverser = CollisionTraverser("gizmo_traverser") - handler = CollisionHandlerQueue() - - # 创建射线节点 - ray_node = CollisionNode('mouseRay') - ray_node.addSolid(ray) - ray_node.setFromCollideMask(BitMask32.bit(1)) # 射线作为from对象 - ray_node.setIntoCollideMask(BitMask32.allOff()) # 射线不作为into对象 - ray_np = self.world.render.attachNewNode(ray_node) - - # 为所有轴的碰撞体设置正确的掩码并添加到遍历器 - collision_found = False - for axis_name in ["x", "y", "z"]: - axis_node = getattr(self, f"gizmo{axis_name.upper()}Axis") - if axis_node: - collision_node_path = axis_node.find("**/gizmo_collision_*") - if not collision_node_path.isEmpty(): - collision_node = collision_node_path.node() - collision_node.setFromCollideMask(BitMask32.allOff()) # 碰撞体不作为from对象 - collision_node.setIntoCollideMask(BitMask32.bit(1)) # 碰撞体作为into对象 - collision_found = True - - if not collision_found: - ray_np.removeNode() - return None - - # 执行碰撞检测 - 这里是关键修复点 - traverser.addCollider(ray_np, handler) - traverser.traverse(self.world.render) - - ray_np.removeNode() - - # 检查是否有碰撞 - if handler.getNumEntries() > 0: - handler.sortEntries() - closest_entry = handler.getEntry(0) - - # 获取碰撞的对象 - collided_object = closest_entry.getIntoNodePath() - axis_tag = collided_object.getTag("gizmo_axis") - - if axis_tag in ["x", "y", "z"]: - return axis_tag - - return None - - except Exception as e: - print(f"使用碰撞体检测坐标轴失败: {e}") - import traceback - traceback.print_exc() - return None - - def debugGizmoCollision(self): - print("===碰撞体调试信息===") - for axis_name in ["x","y","z"]: - axis_node = getattr(self,f"gizmo{axis_name.upper()}Axis") - if axis_node: - handle_node = axis_node.find(f"{axis_name}_handle") - collision_node = axis_node.find("**/gizmo_collision_*") - print(f"{axis_name.upper()}轴:") - print(f" - 轴节点: {axis_node}") - print(f" - Handle节点: {handle_node}") - print(f" - 碰撞节点: {collision_node}") - if not collision_node.isEmpty(): - print(f" - 碰撞体标签: {collision_node.getTag('gizmo_axis')}") - else: - print(f"{axis_name.upper()}轴节点不存在") - - def focusCameraOnSelectedNodeAdvanced(self): - """高级版的摄像机聚焦功能,包含平滑动画效果""" - try: - if not self.selectedNode or self.selectedNode.isEmpty(): - print("没有选中的节点,无法聚焦") - return False - - # 获取选中节点的边界框 - minPoint = Point3() - maxPoint = Point3() - - if not self.selectedNode.calcTightBounds(minPoint, maxPoint, self.world.render): - print("无法计算选中节点的边界框,使用节点为位置作为替代方案") - node_pos = self.selectedNode.getPos(self.world.render) - optimal_distance = 10.0 - current_cam_pos = self.world.cam.getPos() - view_direction = node_pos - current_cam_pos - if view_direction.length()<0.001: - view_direction = Vec3(5,-5,2) - view_direction.normalize() - target_cam_pos = node_pos - (view_direction * optimal_distance) - - temp_node =self.world.render.attachNewNode("temp_lookat_target") - temp_node.setPos(node_pos) - dummy_cam = self.world.render.attachNewNode("dummy_camera") - dummy_cam.setPos(target_cam_pos) - dummy_cam.lookAt(temp_node) - target_cam_hpr = Vec3(dummy_cam.getHpr()) - - temp_node.removeNode() - dummy_cam.removeNode() - - currrent_cam_pos = Point3(self.world.cam.getPos()) - current_cam_hpr = Vec3(self.world.cam.getHpr()) - self._startCameraFocusAnimation(current_cam_pos,target_cam_pos,current_cam_hpr,target_cam_hpr) - print(f"开始聚焦到节点(使用位置): {self.selectedNode.getName()}") - return True - - # 计算节点中心点和大小 - center = Point3( - (minPoint.x + maxPoint.x) * 0.5, - (minPoint.y + maxPoint.y) * 0.5, - (minPoint.z + maxPoint.z) * 0.5 - ) - - # 计算节点的对角线长度 - size = (maxPoint - minPoint).length() - - # 如果节点太小,使用默认大小 - if size < 0.01: - size = 1.0 - - # 获取当前摄像机位置和朝向 - current_cam_pos = Point3(self.world.cam.getPos()) - current_cam_hpr = Vec3(self.world.cam.getHpr()) - - # 计算观察方向 - view_direction = current_cam_pos - center - if view_direction.length() < 0.001: - view_direction = Vec3(5, -5, 2) - - view_direction.normalize() - - # 计算合适的观察距离 - optimal_distance = max(size * 1, 1.0) - - # 计算目标摄像机位置 - target_cam_pos = center + (view_direction * optimal_distance) - - # 计算目标朝向(不直接使用lookAt,而是计算目标HPR) - # 创建临时节点用于计算目标朝向 - temp_node = self.world.render.attachNewNode("temp_lookat_target") - temp_node.setPos(center) - - # 创建另一个临时节点用于计算朝向 - dummy_cam = self.world.render.attachNewNode("dummy_camera") - dummy_cam.setPos(target_cam_pos) - dummy_cam.lookAt(temp_node) - target_cam_hpr = Vec3(dummy_cam.getHpr()) - - # 清理临时节点 - temp_node.removeNode() - dummy_cam.removeNode() - - # 使用任务来实现平滑移动动画 - self._startCameraFocusAnimation(current_cam_pos, target_cam_pos, - current_cam_hpr, target_cam_hpr) - - print(f"开始聚焦到节点: {self.selectedNode.getName()}") - return True - - except Exception as e: - print(f"高级聚焦功能失败: {str(e)}") - import traceback - traceback.print_exc() - return False - - def _startCameraFocusAnimation(self, start_pos, end_pos, start_hpr, end_hpr): - """启动摄像机聚焦动画""" - try: - # 创建动画任务 - class CameraFocusData: - def __init__(self, start_pos, end_pos, start_hpr, end_hpr): - self.start_pos = Point3(start_pos) # 确保是Point3类型 - self.end_pos = Point3(end_pos) # 确保是Point3类型 - self.start_hpr = Vec3(start_hpr) # 确保是Vec3类型 - self.end_hpr = Vec3(end_hpr) # 确保是Vec3类型 - self.elapsed_time = 0.0 - self.duration = 0.8 # 增加动画持续时间到0.8秒,让动画更平滑 - - self._camera_focus_data = CameraFocusData(start_pos, end_pos, start_hpr, end_hpr) - - # 移除之前的任务(如果存在) - taskMgr.remove("cameraFocusTask") - - # 添加新任务 - taskMgr.add(self._cameraFocusTask, "cameraFocusTask") - - except Exception as e: - print(f"启动摄像机聚焦动画失败: {e}") - - def _normalizeAngle(self, angle): - """规范化角度到-180到180度之间""" - while angle > 180: - angle -= 360 - while angle < -180: - angle += 360 - return angle - - def _cameraFocusTask(self, task): - """摄像机聚焦动画任务""" - try: - if not hasattr(self, '_camera_focus_data'): - return task.done - - data = self._camera_focus_data - data.elapsed_time += globalClock.getDt() - - # 计算插值因子 - t = min(1.0, data.elapsed_time / data.duration) - - # 使用更平滑的插值函数 - smooth_t = t * t * (3 - 2 * t) # 平滑步进插值 - - # 手动实现lerp功能 - def lerp_point3(start, end, factor): - return Point3( - start.x + (end.x - start.x) * factor, - start.y + (end.y - start.y) * factor, - start.z + (end.z - start.z) * factor - ) - - # 角度插值需要特殊处理,确保选择最短路径 - def lerp_hpr(start, end, factor): - # 规范化角度 - start_x = self._normalizeAngle(start.x) - start_y = self._normalizeAngle(start.y) - start_z = self._normalizeAngle(start.z) - - end_x = self._normalizeAngle(end.x) - end_y = self._normalizeAngle(end.y) - end_z = self._normalizeAngle(end.z) - - # 计算最短旋转路径 - diff_x = self._normalizeAngle(end_x - start_x) - diff_y = self._normalizeAngle(end_y - start_y) - diff_z = self._normalizeAngle(end_z - start_z) - - # 插值 - result_x = start_x + diff_x * factor - result_y = start_y + diff_y * factor - result_z = start_z + diff_z * factor - - # 再次规范化 - result_x = self._normalizeAngle(result_x) - result_y = self._normalizeAngle(result_y) - result_z = self._normalizeAngle(result_z) - - return Vec3(result_x, result_y, result_z) - - # 计算当前位置和朝向 - current_pos = lerp_point3(data.start_pos, data.end_pos, smooth_t) - current_hpr = lerp_hpr(data.start_hpr, data.end_hpr, smooth_t) - - # 应用到摄像机 - self.world.cam.setPos(current_pos) - self.world.cam.setHpr(current_hpr) - - # 检查是否完成 - if t >= 1.0: - if hasattr(self, '_camera_focus_data'): - delattr(self, '_camera_focus_data') - print("摄像机聚焦动画完成") - return task.done - - return task.cont - - except Exception as e: - print(f"摄像机聚焦任务出错: {e}") - import traceback - traceback.print_exc() - return task.done - - def _smoothCameraMoveTask(self, task): - """平滑摄像机移动任务""" - try: - if not hasattr(self, '_smooth_camera_move_data'): - return task.done - - data = self._smooth_camera_move_data - data.elapsed_time += globalClock.getDt() - - # 计算插值因子 - t = min(1.0, data.elapsed_time / data.duration) - - # 使用平滑插值 - smooth_t = t * t * (3 - 2 * t) - - # 角度插值需要特殊处理 - def lerp_hpr(start, end, factor): - # 规范化角度 - start_x = self._normalizeAngle(start.x) - start_y = self._normalizeAngle(start.y) - start_z = self._normalizeAngle(start.z) - - end_x = self._normalizeAngle(end.x) - end_y = self._normalizeAngle(end.y) - end_z = self._normalizeAngle(end.z) - - # 计算最短旋转路径 - diff_x = self._normalizeAngle(end_x - start_x) - diff_y = self._normalizeAngle(end_y - start_y) - diff_z = self._normalizeAngle(end_z - start_z) - - # 插值 - result_x = start_x + diff_x * factor - result_y = start_y + diff_y * factor - result_z = start_z + diff_z * factor - - return Vec3(result_x, result_y, result_z) - - # 计算当前位置和朝向 - current_pos = Point3( - data.start_pos.x + (data.end_pos.x - data.start_pos.x) * smooth_t, - data.start_pos.y + (data.end_pos.y - data.start_pos.y) * smooth_t, - data.start_pos.z + (data.end_pos.z - data.start_pos.z) * smooth_t - ) - - current_hpr = lerp_hpr(data.start_hpr, data.end_hpr, smooth_t) - - # 应用到摄像机 - self.world.cam.setPos(current_pos) - self.world.cam.setHpr(current_hpr) - - # 检查是否完成 - if t >= 1.0: - if hasattr(self, '_smooth_camera_move_data'): - delattr(self, '_smooth_camera_move_data') - print("摄像机平滑移动完成") - return task.done - - return task.cont - - except Exception as e: - print(f"平滑摄像机移动任务出错: {e}") - import traceback - traceback.print_exc() - return task.done - - def handleMouseClick(self, nodePath, mouseX=None, mouseY=None): - """处理鼠标点击事件 - 支持坐标轴双击聚焦""" - try: - # 如果正在删除节点,忽略鼠标点击 - if hasattr(self, '_deleting_node') and self._deleting_node: - print("正在删除节点,忽略鼠标点击") - return - - import time - current_time = time.time() - - # 检查是否点击了坐标轴 - is_gizmo_click = False - target_node = nodePath - - # 判断是否点击了坐标轴 - if (nodePath and hasattr(nodePath, 'getName') and - (nodePath.getName().startswith("gizmo") or - "gizmo" in nodePath.getName().lower() or - (hasattr(nodePath, 'hasTag') and nodePath.hasTag("is_gizmo")))): - is_gizmo_click = True - # 如果有选中的模型,使用选中的模型作为聚焦目标 - if self.selectedNode and not self.selectedNode.isEmpty(): - target_node = self.selectedNode - print(f"检测到坐标轴点击,使用目标节点: {target_node.getName() if target_node else 'None'}") - - # 检查是否为双击(同一节点且在时间阈值内) - is_double_click = (self._last_clicked_node == target_node and - target_node is not None and - current_time - self._last_click_time < self._double_click_threshold) - - if is_double_click: - # 双击 detected - node_name = target_node.getName() if target_node else "None" - print(f"检测到双击节点: {node_name}") - - # 无论是点击模型还是坐标轴,都执行聚焦 - if target_node and not target_node.isEmpty(): - print(f"双击聚焦到节点: {target_node.getName()}") - if self.selectedNode != target_node: - self.updateSelection(target_node) - else: - self.focusCameraOnSelectedNodeAdvanced() - else: - print("双击事件:没有有效的目标节点") - - # 重置状态以避免三击等误触发 - self._last_click_time = 0 - self._last_clicked_node = None - else: - # 单击,更新状态 - self._last_click_time = current_time - self._last_clicked_node = target_node - - # 如果点击的是坐标轴,保持当前选择不变 - if is_gizmo_click: - print("坐标轴单击,保持当前选择") - else: - # 正常的单击选择 - self.updateSelection(nodePath) - - except Exception as e: - print(f"处理鼠标点击事件失败: {e}") - - def _onDoubleClick(self, nodePath): - """双击事件处理""" - try: - # 获取实际要聚焦的目标节点 - target_node = nodePath - - # 如果是坐标轴,确保使用关联的模型作为目标 - if (nodePath and hasattr(nodePath, 'hasTag') and - nodePath.hasTag("is_gizmo")): - if self.selectedNode and not self.selectedNode.isEmpty(): - target_node = self.selectedNode - print(f"坐标轴双击,聚焦到关联模型: {target_node.getName()}") - else: - print("坐标轴双击,但没有关联的选中模型") - return - - if target_node and not target_node.isEmpty(): - print(f"双击聚焦到节点: {target_node.getName()}") - # 更新选择(如果需要) - if self.selectedNode != target_node: - self.updateSelection(target_node) - - # 执行聚焦 - self.focusCameraOnSelectedNodeAdvanced() - else: - print("双击事件:没有有效的目标节点") - - except Exception as e: - print(f"双击事件处理失败: {e}") - - # 添加一个更精确的双击检测方法 - - def checkDoubleClick(self, nodePath): - """检查是否为双击,返回布尔值""" - try: - import time - current_time = time.time() - - # 必须是同一节点且在时间阈值内 - is_double_click = (self._last_clicked_node == nodePath and - nodePath is not None and - current_time - self._last_click_time < self._double_click_threshold) - - if is_double_click: - # 重置状态 - self._last_click_time = 0 - self._last_clicked_node = None - return True - else: - # 更新状态 - self._last_click_time = current_time - self._last_clicked_node = nodePath - return False - - except Exception as e: - print(f"双击检测失败: {e}") - return False - - # 添加一个定时重置方法,用于清除长时间未完成的双击状态 - - def _resetDoubleClickState(self): - """重置双击状态""" - self._last_click_time = 0 - self._last_clicked_node = None - - # 添加一个任务来自动重置双击状态 - - def _startDoubleClickResetTask(self): - """启动双击状态重置任务""" - if self._double_click_task: - taskMgr.remove(self._double_click_task) - - self._double_click_task = taskMgr.doMethodLater( - self._double_click_threshold * 2, # 等待2倍阈值时间 - self._resetDoubleClickStateTask, - "resetDoubleClickState" - ) - - def _resetDoubleClickStateTask(self, task): - """任务:重置双击状态""" - self._resetDoubleClickState() - self._double_click_task = None - return task.done - - # 修改 updateSelection 方法以集成双击检测 - - # def updateSelection(self, nodePath): - # """更新选择状态""" - # print(f"\n=== 更新选择状态 ===") - # - # # 如果正在删除节点,避免更新选择 - # if hasattr(self, '_deleting_node') and self._deleting_node: - # print("正在删除节点,跳过选择更新") - # print("=== 选择状态更新完成 ===\n") - # return - # - # node_name = "None" - # if nodePath and not nodePath.isEmpty(): - # node_name = nodePath.getName() - # print(f"新选择的节点: {node_name}") - # - # # 检查是否为双击 - # is_double_click = self.checkDoubleClick(nodePath) - # if is_double_click: - # print(f"检测到双击 {node_name},执行聚焦") - # # 启动聚焦(在下一帧执行,确保选择状态已更新) - # taskMgr.doMethodLater(0.01, self._delayedFocusTask, "delayedFocus") - # - # self.selectedNode = nodePath - # # 添加兼容性属性 - # self.selectedObject = nodePath - # - # if nodePath and not nodePath.isEmpty(): - # node_name = nodePath.getName() - # print(f"开始为节点 {node_name} 创建选择框和坐标轴...") - # - # # 创建选择框 - # print("创建选择框...") - # self.createSelectionBox(nodePath) - # if self.selectionBox: - # box_name = "Unknown" - # if self.selectionBox and not self.selectionBox.isEmpty(): - # box_name = self.selectionBox.getName() - # print(f"✓ 选择框创建成功: {box_name}") - # else: - # print("× 选择框创建失败") - # - # # 创建坐标轴 - # print("创建坐标轴...") - # self.createGizmo(nodePath) - # if self.gizmo: - # gizmo_name = "Unknown" - # if self.gizmo and not self.gizmo.isEmpty(): - # gizmo_name = self.gizmo.getName() - # print(f"✓ 坐标轴创建成功: {gizmo_name}") - # else: - # print("× 坐标轴创建失败") - # - # print(f"✓ 选中了节点: {node_name}") - # else: - # print("清除选择...") - # self.clearSelectionBox() - # self.clearGizmo() - # print("✓ 取消选择") - # - # print("=== 选择状态更新完成 ===\n") - - def _delayedFocusTask(self, task): - """延迟执行聚焦任务""" - try: - self.focusCameraOnSelectedNodeAdvanced() - except Exception as e: - print(f"延迟聚焦任务失败: {e}") - return task.done - - # 添加一个更智能的双击检测方法,考虑鼠标位置 - - def checkDoubleClickWithPosition(self, nodePath, mouse_x=None, mouse_y=None): - """检查是否为双击,同时考虑鼠标位置""" - try: - import time - current_time = time.time() - - # 如果没有提供鼠标位置,直接使用基本双击检测 - if mouse_x is None or mouse_y is None: - return self.checkDoubleClick(nodePath) - - # 检查节点和时间 - time_diff = current_time - self._last_click_time - is_same_node = (self._last_clicked_node == nodePath) - - # 如果是同一节点且在时间阈值内,认为是双击 - if is_same_node and time_diff < self._double_click_threshold: - # 重置状态 - self._last_click_time = 0 - self._last_clicked_node = None - return True - else: - # 更新状态 - self._last_click_time = current_time - self._last_clicked_node = nodePath - return False - - except Exception as e: - print(f"带位置的双击检测失败: {e}") - return False - - # 添加一个公共方法来设置双击阈值 - - def setDoubleClickThreshold(self, threshold_seconds): - """设置双击时间阈值""" - if threshold_seconds > 0: - self._double_click_threshold = threshold_seconds - print(f"双击阈值已设置为: {threshold_seconds} 秒") - else: - print("无效的双击阈值") - - # 添加一个方法来手动触发双击聚焦(可用于测试或其他触发方式) - - def triggerDoubleClickFocus(self, nodePath=None): - """手动触发双击聚焦""" - try: - target_node = nodePath if nodePath else self.selectedNode - if target_node and not target_node.isEmpty(): - print(f"手动触发聚焦到节点: {target_node.getName()}") - if self.selectedNode != target_node: - self.updateSelection(target_node) - self.focusCameraOnSelectedNodeAdvanced() - return True - else: - print("没有有效的目标节点进行聚焦") - return False - except Exception as e: - print(f"手动触发聚焦失败: {e}") - return False - - def cleanup(self): - """清理选择系统资源""" - # 清理双击任务 - if self._double_click_task: - taskMgr.remove(self._double_click_task) - self._double_click_task = None - - # 清理其他资源 - self.clearSelectionBox() - self.clearGizmo() - def clearSelection(self): - """清除当前选择""" - try: - self.selectedNode = None - self.selectedObject = None - self.clearSelectionBox() - self.clearGizmo() - - # 清除树形控件中的选择 - if (hasattr(self.world, 'interface_manager') and - self.world.interface_manager and - hasattr(self.world.interface_manager, 'treeWidget') and - self.world.interface_manager.treeWidget): - self.world.interface_manager.treeWidget.setCurrentItem(None) - - print("已清除选择") - except Exception as e: - print(f"清除选择失败: {e}") - +""" +选择和变换系统模块 + +负责物体选择和变换相关功能: +- 选择框的创建和更新 +- 坐标轴(Gizmo)系统 +- 拖拽变换逻辑 +- 射线检测和碰撞检测 +""" +from direct.showbase.ShowBaseGlobal import globalClock +from panda3d.core import (Vec3, Point3, Point2, LineSegs, ColorAttrib, RenderState, + DepthTestAttrib, CollisionTraverser, CollisionHandlerQueue, + CollisionNode, CollisionRay, GeomNode, BitMask32, Material, LColor, DepthWriteAttrib, + TransparencyAttrib, Vec4, CollisionCapsule) +from direct.task.TaskManagerGlobal import taskMgr +import math + +class SelectionSystem: + """选择和变换系统类""" + + def __init__(self, world): + """初始化选择系统 + + Args: + world: 核心世界对象引用 + """ + self.world = world + + # 选择相关状态 + self.selectedNode = None + self.selectionBox = None # 选择框 + self.selectionBoxTarget = None # 选择框跟踪的目标节点 + + # 坐标轴工具(Gizmo)相关 + self.gizmo = None # 坐标轴 + self.gizmoTarget = None # 坐标轴跟踪的目标节点 + self.gizmoXAxis = None # X轴 + self.gizmoYAxis = None # Y轴 + self.gizmoZAxis = None # Z轴 + self.gizmoRotXAxis = None + self.gizmoRotYAxis = None + self.gizmoRotZAxis = None + self.axis_length = 5.0 # 坐标轴长度(增加到5.0) + + # 拖拽相关状态 + self.isDraggingGizmo = False # 是否正在拖拽坐标轴 + self.dragGizmoAxis = None # 当前拖拽的轴("x", "y", "z") + self.gizmoStartPos = None # 拖拽开始时坐标轴的位置 + self.gizmoTargetStartPos = None # 拖拽开始时目标节点的位置 + self.dragStartMousePos = None # 拖拽开始时的鼠标位置 + + # 高亮相关 + self.gizmoHighlightAxis = None + self.gizmo_colors = { + "x": (1, 0, 0, 1.0), # 红色 + "y": (0, 1, 0, 1.0), # 绿色 + "z": (0, 0, 1, 1.00) # 蓝色 + } + self.gizmo_highlight_colors = { + "x": (1.0, 1.0, 0.0, 1.0), # 黄色高亮 + "y": (1.0, 1.0, 0.0, 1.0), # 黄色高亮 + "z": (1.0, 1.0, 0.0, 1.0) # 黄色高亮 + } + + #性能优化相关 + self._optimized_node = False + self._last_update_time = 0 + self._cached_bounds = {} + self._gizmo_update_interval = 0.1 + self._selection_box_update_interval = 0.2 + + self._current_cursor = None + self._default_cursor = None + + self._last_click_time = 0 + self._double_click_threshold = 0.3 + self._last_clicked_node = None + self._double_click_task = None + + print("✓ 选择和变换系统初始化完成") + # ==================== 光标设置 ==================== + def _setCursor(self,cursor_type): + try: + from PyQt5.QtCore import Qt + if self._current_cursor == cursor_type: + return + if hasattr(self.world,'main_window') and self.world.main_window: + main_window = self.world.main_window + else: + from PyQt5.QtWidgets import QApplication + main_window = QApplication.activeWindow() + if not main_window: + windows = QApplication.topLevelWindows() + for window in windows: + if hasattr(window,'isVisible') and window.isVisible(): + main_window = window + break + if main_window: + if cursor_type == "crosshair": + main_window.setCursor(Qt.CrossCursor) + elif cursor_type == "size_hor": + main_window.setCursor(Qt.SizeHorCursor) + elif cursor_type == "size_ver": + main_window.setCursor(Qt.SizeVerCursor) + elif cursor_type == "size_all": + main_window.setCursor(Qt.SizeAllCursor) + elif cursor_type == "pointing_hand": + main_window.setCursor(Qt.PointingHandCursor) + else: + main_window.unsetCursor() + self._current_cursor = cursor_type + #print(f"光标已设置:{cursor_type}") + self._current_cursor = cursor_type + else: + print("警告:无法获取主窗口,光标设置失败") + except Exception as e: + print(f"设置光标失败{e}") + def _resetCursor(self): + self._setCursor("default") + + # ==================== 选择框系统 ==================== + + def createSelectionBox(self, nodePath): + """为选中的节点创建选择框""" + try: + if self.selectionBox: + #print(" 移除现有选择框") + self.selectionBox.removeNode() + self.selectionBox = None + + if not nodePath: + print(" 目标节点为空,取消创建") + return + + self.selectionBox = self.world.render.attachNewNode("selectionBox") + self.selectionBoxTarget = nodePath + + taskMgr.add(self.updateSelectionBoxTask, "updateSelectionBox") + self.updateSelectionBoxGeometry() + + except Exception as e: + print(f" ✗ 创建选择框失败: {str(e)}") + import traceback + traceback.print_exc() + + def updateSelectionBoxGeometry(self): + """更新选择框的几何形状和位置""" + try: + if not self.selectionBox or not self.selectionBoxTarget: + return + + if self.selectionBoxTarget.isEmpty(): + return + + minPoint = Point3() + maxPoint = Point3() + + try: + has_bounds = self.selectionBoxTarget.calcTightBounds(minPoint, maxPoint, self.world.render) + if not has_bounds: + return + except: + return + + # 检查边界框的有效性 + if (minPoint.x > maxPoint.x or minPoint.y > maxPoint.y or minPoint.z > maxPoint.z or + abs(minPoint.x) > 1e10 or abs(minPoint.y) > 1e10 or abs(minPoint.z) > 1e10 or + abs(maxPoint.x) > 1e10 or abs(maxPoint.y) > 1e10 or abs(maxPoint.z) > 1e10): + print("警告: 检测到无效的边界框,跳过选择框更新") + return + + # 检查是否需要重新计算边界框 + if not hasattr(self, '_bounds_cache'): + self._bounds_cache = {} + + node_name = self.selectionBoxTarget.getName() + import time + current_time = time.time() + + # 如果缓存存在且未过期,则使用缓存 + if (node_name in self._bounds_cache and + current_time - self._bounds_cache[node_name]['time'] < 0.1): + minPoint, maxPoint = self._bounds_cache[node_name]['bounds'] + else: + # 计算新的边界框并缓存 + minPoint = Point3() + maxPoint = Point3() + if not self.selectionBoxTarget.calcTightBounds(minPoint, maxPoint, self.world.render): + return + + # 缓存结果 + self._bounds_cache[node_name] = { + 'bounds': (minPoint, maxPoint), + 'time': current_time + } + + # 清理旧缓存 + expired_keys = [k for k, v in self._bounds_cache.items() + if current_time - v['time'] > 1.0] + for key in expired_keys: + del self._bounds_cache[key] + + # 清除现有的几何体 + self.selectionBox.removeNode() + self.selectionBox = self.world.render.attachNewNode("selectionBox") + + # 获取目标节点在世界坐标系中的边界框(使用正确的API) + minPoint = Point3() + maxPoint = Point3() + if not self.selectionBoxTarget.calcTightBounds(minPoint, maxPoint, self.world.render): + return + + # 获取边界框的最小和最大点(世界坐标) + #print(f"世界边界框: min={minPoint}, max={maxPoint}") + + # 创建线段对象 + lines = LineSegs() + lines.setThickness(2.0) + + # 定义立方体的8个顶点 + vertices = [ + (minPoint.x, minPoint.y, minPoint.z), # 0: 前下左 + (maxPoint.x, minPoint.y, minPoint.z), # 1: 前下右 + (maxPoint.x, maxPoint.y, minPoint.z), # 2: 后下右 + (minPoint.x, maxPoint.y, minPoint.z), # 3: 后下左 + (minPoint.x, minPoint.y, maxPoint.z), # 4: 前上左 + (maxPoint.x, minPoint.y, maxPoint.z), # 5: 前上右 + (maxPoint.x, maxPoint.y, maxPoint.z), # 6: 后上右 + (minPoint.x, maxPoint.y, maxPoint.z), # 7: 后上左 + ] + + # 定义立方体的边(连接顶点的线段) + edges = [ + # 底面 + (0, 1), (1, 2), (2, 3), (3, 0), + # 顶面 + (4, 5), (5, 6), (6, 7), (7, 4), + # 垂直边 + (0, 4), (1, 5), (2, 6), (3, 7) + ] + + # 绘制所有边 + for start, end in edges: + lines.moveTo(*vertices[start]) + lines.drawTo(*vertices[end]) + + # 创建选择框几何体 + geomNode = lines.create() + self.selectionBox.attachNewNode(geomNode) + + # 设置选择框的颜色为亮橙色 + self.selectionBox.setColor(1.0, 0.5, 0.0, 1.0) + + # 设置渲染状态,确保线框总是在最前面显示 + state = RenderState.make( + DepthTestAttrib.make(DepthTestAttrib.MLess), + ColorAttrib.makeFlat((1.0, 0.5, 0.0, 1.0)) + ) + self.selectionBox.setState(state) + + # 确保选择框不被光照影响 + self.selectionBox.setLightOff() + + # 让选择框稍微大一点,避免与模型重叠 + self.selectionBox.setScale(1.01) + + except Exception as e: + print(f"更新选择框几何体失败: {str(e)}") + import traceback + traceback.print_exc() + + def updateSelectionBoxTask(self, task): + """选择框更新任务 - 平衡性能和实时性""" + try: + update_interval = 0.05 + + if not hasattr(self, '_last_selection_box_update'): + self._last_selection_box_update = 0 + + import time + current_time = time.time() + if current_time - self._last_selection_box_update < update_interval: + return task.cont + self._last_selection_box_update = current_time + + # 检查目标节点是否已被删除 + self.checkAndClearIfTargetDeleted() + + if not self.selectionBox or not self.selectionBoxTarget: + return task.done + + # 检查目标节点是否还存在 + if self.selectionBoxTarget.isEmpty(): + self.clearSelectionBox() + return task.done + + # 检查目标节点是否发生了变化(位置、旋转、缩放) + current_transform = self._getNodeTransformKey(self.selectionBoxTarget) + + if (not hasattr(self, '_last_transform_key') or + self._last_transform_key != current_transform): + # 节点发生了变化,更新选择框 + self.updateSelectionBoxGeometry() + self._last_transform_key = current_transform + + return task.cont + + except Exception as e: + print(f"选择框更新任务出错: {str(e)}") + return task.done + + def _getNodeTransformKey(self, node): + """获取节点变换的关键信息,用于快速比较""" + try: + # 获取节点的关键变换信息 + pos = node.getPos(self.world.render) + hpr = node.getHpr(self.world.render) + scale = node.getScale(self.world.render) + + # 返回一个可以比较的元组 + return (pos.x, pos.y, pos.z, hpr.x, hpr.y, hpr.z, scale.x, scale.y, scale.z) + except: + return None + + def clearSelectionBox(self): + """清除选择框""" + if self.selectionBox: + self.selectionBox.removeNode() + self.selectionBox = None + + # 停止选择框更新任务 + taskMgr.remove("updateSelectionBox") + + # 清除目标节点引用 + self.selectionBoxTarget = None + + print("清除了选择框") + + # ==================== 坐标轴(Gizmo)系统 ==================== + + def createGizmo(self, nodePath): + """为选中的节点创建坐标轴工具 - 保留箭头版本""" + try: + #print(f" 开始创建坐标轴,目标节点: {nodePath.getName()}") + + # 如果已有坐标轴,先移除 + if self.gizmo: + self.gizmo.removeNode() + self.gizmo = None + taskMgr.remove("updateGizmo") + + if not nodePath: + return + + # 创建坐标轴主节点 + self.gizmo = self.world.render.attachNewNode("gizmo") + self.gizmoTarget = nodePath + + # 添加标识标签,便于识别 + self.gizmo.setTag("is_gizmo", "1") + if hasattr(nodePath, 'getName'): + self.gizmo.setTag("gizmo_target", nodePath.getName()) + + # 为各轴添加标签 + if hasattr(self, 'gizmoXAxis') and self.gizmoXAxis: + self.gizmoXAxis.setTag("is_gizmo", "1") + self.gizmoXAxis.setTag("gizmo_axis", "x") + if hasattr(self, 'gizmoYAxis') and self.gizmoYAxis: + self.gizmoYAxis.setTag("is_gizmo", "1") + self.gizmoYAxis.setTag("gizmo_axis", "y") + if hasattr(self, 'gizmoZAxis') and self.gizmoZAxis: + self.gizmoZAxis.setTag("is_gizmo", "1") + self.gizmoZAxis.setTag("gizmo_axis", "z") + + # 设置位置和朝向 + minPoint = Point3() + maxPoint = Point3() + if nodePath.calcTightBounds(minPoint, maxPoint, self.world.render): + center = Point3((minPoint.x + maxPoint.x) * 0.5, + (minPoint.y + maxPoint.y) * 0.5, + (minPoint.z + maxPoint.z) * 0.5) + self.gizmo.setPos(center) + + parent_node = nodePath.getParent() + if parent_node and parent_node != self.world.render: + self.gizmo.setHpr(parent_node.getHpr()) + else: + self.gizmo.setHpr(0, 0, 0) + + # 只调用一次几何体创建 + self.createGizmoGeometry() + + #只调用一次颜色设置 + self.setGizmoAxisColor("x", self.gizmo_colors["x"]) + self.setGizmoAxisColor("y", self.gizmo_colors["y"]) + self.setGizmoAxisColor("z", self.gizmo_colors["z"]) + + self._updateGizmoScreenSize() + + self._setupGizmoRendering() + + self.setupGizmoCollision() + + # 现在才显示坐标轴 + self.gizmo.show() + + # 只启动一次更新任务 + taskMgr.add(self.updateGizmoTask, "updateGizmo") + + #print(f" ✓ 为节点 {nodePath.getName()} 创建了坐标轴") + + except Exception as e: + print(f"创建坐标轴失败: {str(e)}") + def createGizmoGeometry(self): + """创建坐标轴的几何体""" + from panda3d.core import Material + try: + if not self.gizmo: + return + + is_scale_tool = self.world.tool_manager.isScaleTool() if self.world.tool_manager else False + is_rotate_tool = self.world.tool_manager.isRotateTool() if self.world.tool_manager else False + + import os + from panda3d.core import getModelPath, Filename + + # 获取项目根目录 + base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + # 确保core目录在模型搜索路径中 + model_path = getModelPath() + core_dir = os.path.join(base_dir, "core") + core_filename = Filename.from_os_specific(core_dir) + if not model_path.findFile(core_filename): + model_path.appendDirectory(core_filename) + print(f"✓ 添加core目录到模型搜索路径: {core_dir}") + + if is_scale_tool: + model_filename = "core/UniformScaleHandle.fbx" + elif is_rotate_tool: + model_filename = "core/RotationHandleQuarter.fbx" + arrow_filename = "core/TranslateArrowHandle.fbx" + else: + model_filename = "core/TranslateArrowHandle.fbx" + + # model_paths = [ + # "core/TranslateArrowHandle.fbx", + # "EG/core/TranslateArrowHandle.fbx", + # ] + gizmo_model = None + gizmoRot_model = None + try: + if is_rotate_tool: + gizmo_model = self.world.loader.loadModel(arrow_filename) + gizmoRot_model = self.world.loader.loadModel(model_filename) + else: + gizmo_model = self.world.loader.loadModel(model_filename) + except Exception as e: + print(f"加载模型失败: {e}") + return + + x_rHandle = None + y_rHandle = None + z_rHandle = None + + if is_rotate_tool: + self.gizmoRotXAxis = self.gizmo.attachNewNode("gizmo_rot_x_axis") + x_rHandle = gizmoRot_model.copyTo(self.gizmoRotXAxis) + x_rHandle.setName("x_handle") + self.gizmoRotYAxis = self.gizmo.attachNewNode("gizmo_rot_y_axis") + y_rHandle = gizmoRot_model.copyTo(self.gizmoRotYAxis) + y_rHandle.setName("y_handle") + self.gizmoRotZAxis = self.gizmo.attachNewNode("gizmo_rot_z_axis") + z_rHandle = gizmoRot_model.copyTo(self.gizmoRotZAxis) + z_rHandle.setName("z_handle") + + self.gizmoXAxis = self.gizmo.attachNewNode("gizmo_x_axis") + x_handle = gizmo_model.copyTo(self.gizmoXAxis) + x_handle.setName("x_handle") + + self.gizmoYAxis = self.gizmo.attachNewNode("gizmo_y_axis") + y_handle = gizmo_model.copyTo(self.gizmoYAxis) + y_handle.setName("y_handle") + + self.gizmoZAxis = self.gizmo.attachNewNode("gizmo_z_axis") + z_handle = gizmo_model.copyTo(self.gizmoZAxis) + z_handle.setName("z_handle") + + if is_scale_tool: + x_handle.setHpr(0,-90,0) + x_handle.setScale(0.6,0.03,0.03) + x_handle.setPos(2.2,0,0) + + y_handle.setHpr(90,0,0) + y_handle.setScale(0.6,0.03,0.03) + y_handle.setPos(0,2.2,0) + + z_handle.setHpr(0,0,-90) + z_handle.setScale(0.6,0.03,0.03) + z_handle.setPos(0,0,2.2) + elif is_rotate_tool: + x_rHandle.setHpr(0,0,90) + x_rHandle.setScale(0.025,0.0125,0.0125) + x_rHandle.setPos(0,0,0) + + y_rHandle.setHpr(0,0,0) + y_rHandle.setScale(0.025,0.0125,0.0125) + y_rHandle.setPos(0,0,0) + + z_rHandle.setHpr(-90,0,0) + z_rHandle.setScale(0.025,0.0125,0.0125) + z_rHandle.setPos(0,0,0) + + x_handle.setHpr(0, -90, 0) + x_handle.setScale(0.1, 0.05, 0.05) + x_handle.setPos(0, 0, 0) + + y_handle.setHpr(90, 0, 0) + y_handle.setScale(0.1, 0.05, 0.05) + y_handle.setPos(0, 0, 0) + + z_handle.setHpr(0, 0, -90) + z_handle.setScale(0.1, 0.05, 0.05) + z_handle.setPos(0, 0, 0) + + self.setGizmoRotAxisColor("x", self.gizmo_colors["x"]) + self.setGizmoRotAxisColor("y", self.gizmo_colors["y"]) + self.setGizmoRotAxisColor("z", self.gizmo_colors["z"]) + else: + x_handle.setHpr(0,-90,0) + x_handle.setScale(0.1,0.05,0.05) + x_handle.setPos(0,0,0) + + y_handle.setHpr(90,0,0) + y_handle.setScale(0.1,0.05,0.05) + y_handle.setPos(0,0,0) + + z_handle.setHpr(0,0,-90) + z_handle.setScale(0.1,0.05,0.05) + z_handle.setPos(0,0,0) + + # 设置初始颜色 + self.setGizmoAxisColor("x", self.gizmo_colors["x"]) + self.setGizmoAxisColor("y", self.gizmo_colors["y"]) + self.setGizmoAxisColor("z", self.gizmo_colors["z"]) + + #设置渲染属性,解决模型遮挡和阴影问题 + self._setupGizmoRendering() + + except Exception as e: + print(f"创建坐标轴几何体失败: {str(e)}") + + def _setupGizmoRendering(self): + try: + axis_nodes = [self.gizmoXAxis,self.gizmoYAxis,self.gizmoZAxis] + axis_Rotnodes = [self.gizmoRotXAxis, self.gizmoRotYAxis, self.gizmoRotZAxis] + + # 设置坐标轴主节点的渲染属性 + if self.gizmo: + self.gizmo.setLightOff() # 禁用光照 + self.gizmo.setShaderOff() # 禁用着色器 + self.gizmo.setFogOff() # 禁用雾效 + self.gizmo.setBin("fixed", 40) # 设置为fixed渲染层级,数值越大越优先 + self.gizmo.setDepthWrite(False) # 禁用深度写入 + self.gizmo.setDepthTest(False) # 禁用深度测试,确保始终可见 + + # 设置各轴节点的渲染属性 + for axis_node in axis_nodes: + if axis_node: + axis_node.setLightOff() + axis_node.setShaderOff() + axis_node.setFogOff() + axis_node.setBin("fixed", 40) # 与主节点相同优先级 + axis_node.setDepthWrite(False) # 禁用深度写入 + axis_node.setDepthTest(False) # 禁用深度测试 + + # 设置旋转轴节点的渲染属性 + for axis_rotnode in axis_Rotnodes: + if axis_rotnode: + axis_rotnode.setLightOff() + axis_rotnode.setShaderOff() + axis_rotnode.setFogOff() + axis_rotnode.setBin("fixed", 40) # 与主节点相同优先级 + axis_rotnode.setDepthWrite(False) # 禁用深度写入 + axis_rotnode.setDepthTest(False) # 禁用深度测试 + + # 收集所有handle节点 + arrow_nodes = [] + if self.gizmoXAxis: + x_handle = self.gizmoXAxis.find("x_handle") + if x_handle and not x_handle.isEmpty(): + arrow_nodes.append(x_handle) + if self.gizmoYAxis: + y_handle = self.gizmoYAxis.find("y_handle") + if y_handle and not y_handle.isEmpty(): + arrow_nodes.append(y_handle) + if self.gizmoZAxis: + z_handle = self.gizmoZAxis.find("z_handle") + if z_handle and not z_handle.isEmpty(): + arrow_nodes.append(z_handle) + + rot_nodes = [] + if self.gizmoRotXAxis: + x_rHandle = self.gizmoRotXAxis.find("x_handle") + if x_rHandle and not x_rHandle.isEmpty(): + rot_nodes.append(x_rHandle) + if self.gizmoRotYAxis: + y_rHandle = self.gizmoRotYAxis.find("y_handle") + if y_rHandle and not y_rHandle.isEmpty(): + rot_nodes.append(y_rHandle) + if self.gizmoRotZAxis: + z_rHandle = self.gizmoRotZAxis.find("z_handle") + if z_rHandle and not z_rHandle.isEmpty(): + rot_nodes.append(z_rHandle) + + # 设置handle节点的渲染属性 + for arrow_node in arrow_nodes: + if arrow_node: + arrow_node.setLightOff() + arrow_node.setShaderOff() + arrow_node.setFogOff() + arrow_node.setBin("fixed", 41) # 略高于主节点,确保最优先显示 + arrow_node.setDepthWrite(False) + arrow_node.setDepthTest(False) + # 启用透明度支持 + arrow_node.setTransparency(TransparencyAttrib.MAlpha) + + for rot_node in rot_nodes: + if rot_node: + rot_node.setLightOff() + rot_node.setShaderOff() + rot_node.setFogOff() + rot_node.setBin("fixed", 41) # 略高于主节点,确保最优先显示 + rot_node.setDepthWrite(False) + rot_node.setDepthTest(False) + # 启用透明度支持 + rot_node.setTransparency(TransparencyAttrib.MAlpha) + + except Exception as e: + print(f"设置坐标轴渲染属性失败: {str(e)}") + + def updateGizmoTask(self, task): + """坐标轴更新任务 - 包含固定大小功能""" + try: + # 限制更新频率 + if not hasattr(self, '_last_gizmo_update'): + self._last_gizmo_update = 0 + + import time + current_time = time.time() + if current_time - self._last_gizmo_update < 0.5: # 每0.05秒更新一次 + return task.cont + self._last_gizmo_update = current_time + + #检查目标节点是否已被删除 + self.checkAndClearIfTargetDeleted() + + if not self.gizmo or not self.gizmoTarget: + return task.done + + # 检查目标节点是否还存在 + if self.gizmoTarget.isEmpty(): + self.clearGizmo() + return task.done + + is_scale_tool = self.world.tool_manager.isScaleTool() if self.world.tool_manager else False + is_rotate_tool = self.world.tool_manager.isRotateTool() if self.world.tool_manager else False + was_scale_tool = getattr(self,'_last_tool_scale_state',False) + was_rotate_tool =getattr(self,'_last_tool_rotate_state',False) + + tool_changed = (is_scale_tool!=was_scale_tool) or (is_rotate_tool != was_rotate_tool) + + if tool_changed: + self._last_tool_scale_state = is_scale_tool + self._last_tool_rotate_state = is_rotate_tool + + if self.gizmoXAxis: + self.gizmoXAxis.removeNode() + self.gizmoXAxis = None + if self.gizmoYAxis: + self.gizmoYAxis.removeNode() + self.gizmoYAxis = None + if self.gizmoZAxis: + self.gizmoZAxis.removeNode() + self.gizmoZAxis = None + if self.gizmoRotXAxis: + self.gizmoRotXAxis.removeNode() + self.gizmoRotXAxis = None + if self.gizmoRotYAxis: + self.gizmoRotYAxis.removeNode() + self.gizmoRotYAxis = None + if self.gizmoRotZAxis: + self.gizmoRotZAxis.removeNode() + self.gizmoRotZAxis = None + + self.createGizmoGeometry() + + self.setGizmoAxisColor("x",self.gizmo_colors["x"]) + self.setGizmoAxisColor("y",self.gizmo_colors["y"]) + self.setGizmoAxisColor("z",self.gizmo_colors["z"]) + + self.setupGizmoCollision() + + light_object = self.gizmoTarget.getPythonTag("rp_light_object") + if light_object: + light_pos = light_object.pos + self.gizmo.setPos(light_object.pos) + self.gizmoTarget.setPos(light_pos) + else: + # 只在必要时更新位置和朝向 + self._updateGizmoPositionAndOrientation() + + # 【新功能】:动态调整坐标轴大小,保持固定的屏幕大小 + self._updateGizmoScreenSize() + + return task.cont + + except Exception as e: + print(f"坐标轴更新任务出错: {str(e)}") + return task.done + + def _updateGizmoPositionAndOrientation(self): + """优化的Gizmo位置和朝向更新""" + # 只在必要时重新计算边界框 + if not hasattr(self, '_last_gizmo_bounds_update'): + self._last_gizmo_bounds_update = 0 + + import time + current_time = time.time() + if current_time - self._last_gizmo_bounds_update > 0.2: # 每0.2秒计算一次边界框 + minPoint = Point3() + maxPoint = Point3() + # 添加异常处理 + try: + if self.gizmoTarget.calcTightBounds(minPoint, maxPoint, self.world.render): + # 检查边界框的有效性 + if (abs(minPoint.x) < 1e10 and abs(minPoint.y) < 1e10 and abs(minPoint.z) < 1e10 and + abs(maxPoint.x) < 1e10 and abs(maxPoint.y) < 1e10 and abs(maxPoint.z) < 1e10): + # 计算中心点 + center = Point3((minPoint.x + maxPoint.x) * 0.5, + (minPoint.y + maxPoint.y) * 0.5, + (minPoint.z + maxPoint.z) * 0.5) + self.gizmo.setPos(center) + except Exception as e: + print(f"更新Gizmo位置时出错: {e}") + # if self.gizmoTarget.calcTightBounds(minPoint, maxPoint, self.world.render): + # # 计算中心点 + # center = Point3((minPoint.x + maxPoint.x) * 0.5, + # (minPoint.y + maxPoint.y) * 0.5, + # (minPoint.z + maxPoint.z) * 0.5) + # self.gizmo.setPos(center) + self._last_gizmo_bounds_update = current_time + + is_scale_tool = self.world.tool_manager.isScaleTool() if self.world.tool_manager else False + + # 安区地更新朝向 + + if is_scale_tool: + #self.gizmo.setHpr(self.gizmoTarget.getHpr()) + self.gizmo.setQuat(self.gizmoTarget.getQuat(self.world.render)) + else: + parent_node = self.gizmoTarget.getParent() + if parent_node and parent_node != self.world.render: + parent_hpr = parent_node.getHpr() + self.gizmo.setHpr(parent_hpr) + else: + self.gizmo.setHpr(0,0,0) + + # 更新朝向 + # parent_node = self.gizmoTarget.getParent() + # if parent_node and parent_node != self.world.render: + # parent_hpr = parent_node.getHpr() + # self.gizmo.setHpr(parent_hpr) + # else: + # self.gizmo.setHpr(0, 0, 0) + + def _updateGizmoScreenSize(self): + """动态调整坐标轴大小,保持固定的屏幕大小""" + try: + if not self.gizmo or not self.gizmoTarget: + return + + # 计算相机到坐标轴的距离 + gizmo_world_pos = self.gizmo.getPos(self.world.render) + cam_pos = self.world.cam.getPos() + distance_to_gizmo = (cam_pos - gizmo_world_pos).length() + + # 获取相机视野角度和窗口尺寸 + fov = self.world.cam.node().getLens().getFov()[0] # 水平视野角度 + fov_radians = math.radians(fov) + winWidth, winHeight = self.world.getWindowSize() + + # 计算一个像素在坐标轴距离处对应的世界坐标大小 + pixel_to_world_ratio = distance_to_gizmo * math.tan(fov_radians / 2) / (winWidth / 2) + + # 设定坐标轴在屏幕上的期望像素长度(固定值) + desired_screen_length = 120 # 像素 + + # 计算世界坐标系中的轴长度 + world_axis_length = desired_screen_length * pixel_to_world_ratio + + # 计算缩放比例(相对于基础轴长度) + scale_factor = world_axis_length / self.axis_length + + # 应用缩放到坐标轴 + self.gizmo.setScale(scale_factor) + + # 限制缩放范围,避免过大或过小 + min_scale = 0.001 + max_scale = 100.0 + final_scale = max(min_scale, min(max_scale, scale_factor)) + + if final_scale != scale_factor: + self.gizmo.setScale(final_scale) + + except Exception as e: + # 静默处理错误,避免频繁输出 + pass + + def clearGizmo(self): + """清除坐标轴""" + if self.gizmo: + self.gizmo.removeNode() + self.gizmo = None + + # 停止坐标轴更新任务 + taskMgr.remove("updateGizmo") + + # 清除坐标轴相关引用 + self.gizmoTarget = None + self.gizmoXAxis = None + self.gizmoYAxis = None + self.gizmoZAxis = None + self.isDraggingGizmo = False + self.dragGizmoAxis = None + self.dragStartMousePos = None + self.gizmoTargetStartPos = None + self.gizmoStartPos = None + self._resetCursor() + + + # def setGizmoAxisColor(self, axis, color): + # """设置坐标轴颜色 - RenderPipeline 兼容版本""" + # try: + # from panda3d.core import AntialiasAttrib,TransparencyAttrib + # + # axis_nodes = { + # "x": self.gizmoXAxis, + # "y": self.gizmoYAxis, + # "z": self.gizmoZAxis + # } + # + # if axis in axis_nodes and axis_nodes[axis]: + # axis_node = axis_nodes[axis] + # + # axis_node.setColor(color[0]*20.0,color[1]*20.0,color[2]*20.0,color[3]) + # axis_node.setColorScale(color[0]*10.0,color[1]*10.0,color[2]*10.0,color[3]) + # axis_node.setShaderOff(10000) + # axis_node.setLightOff() + # axis_node.setMaterialOff() + # axis_node.setTextureOff() + # axis_node.setFogOff() + # + # except Exception as e: + # print(f"设置坐标轴颜色失败: {str(e)}") + # # 回退到简单的颜色设置 + # try: + # if axis in axis_nodes and axis_nodes[axis]: + # axis_nodes[axis].setColor(*color) + # except: + # pass + + + def setGizmoRotAxisColor(self, axis, color): + """使用材质设置坐标轴颜色 - RenderPipeline兼容版本""" + try: + from panda3d.core import Material, Vec4,ColorWriteAttrib,DepthWriteAttrib,DepthTestAttrib,TransparencyAttrib + + # 获取对应的轴节点 + axis_nodes = { + "x": self.gizmoRotXAxis, + "y": self.gizmoRotYAxis, + "z": self.gizmoRotZAxis + } + + if axis not in axis_nodes or not axis_nodes[axis]: + return + + axis_node = axis_nodes[axis] + + handle_node = None + handle_node = axis_node.find("x_handle") if axis == "x" else handle_node + handle_node = axis_node.find("y_handle") if axis == "y" else handle_node + handle_node = axis_node.find("z_handle") if axis == "z" else handle_node + + #如果找不到特定名称的节点,尝试查找任何子节点 + if not handle_node: + children = axis_node.getChildren() + if children.getNumPath()>0: + handle_node = children[0] + + if not handle_node: + #print(f"未找到{axis}轴的处理模型") + return + + # 创建或获取材质 + mat = Material() + + # 设置材质属性 - 使用更自然的颜色,避免过亮的自发光 + adjusted_color = Vec4( + min(color[0]*20, 1.0), + min(color[1]*20, 1.0), + min(color[2]*20, 1.0), + color[3] + ) + + mat.setBaseColor(adjusted_color) + #mat.setEmission(Vec4(1, 1, 1, 1.0)) # 自发光 + + # 应用材质 + handle_node.setMaterial(mat, 1) + + + # 设置透明度 + if color[3] < 1.0: + handle_node.setTransparency(TransparencyAttrib.MAlpha) + else: + handle_node.setTransparency(TransparencyAttrib.MNone) + + handle_node.setLightOff() # 禁用光照影响 + handle_node.setShaderOff() # 禁用着色器 + handle_node.setFogOff() # 禁用雾效果 + + handle_node.setBin("fixed",41) + handle_node.setDepthWrite(False) + handle_node.setDepthTest(False) + + # 保存材质引用以便后续修改 + if axis == "x": + self.xMat = mat + elif axis == "y": + self.yMat = mat + elif axis == "z": + self.zMat = mat + + axis_node.setLightOff() + axis_node.setShaderOff() + axis_node.setFogOff() + axis_node.setBin("fixed", 40) + axis_node.setDepthWrite(False) + axis_node.setDepthTest(False) + + except Exception as e: + print(f"设置坐标轴颜色失败: {str(e)}") + # 回退到简单颜色设置 + try: + axis_nodes = { + "x": self.gizmoXAxis, + "y": self.gizmoYAxis, + "z": self.gizmoZAxis + } + + if axis in axis_nodes and axis_nodes[axis]: + axis_nodes[axis].setColor(color[0], color[1], color[2], color[3]) + except: + pass + + def setGizmoAxisColor(self, axis, color): + """使用材质设置坐标轴颜色 - RenderPipeline兼容版本""" + try: + from panda3d.core import Material, Vec4,ColorWriteAttrib,DepthWriteAttrib,DepthTestAttrib,TransparencyAttrib + + # 获取对应的轴节点 + axis_nodes = { + "x": self.gizmoXAxis, + "y": self.gizmoYAxis, + "z": self.gizmoZAxis + } + + if axis not in axis_nodes or not axis_nodes[axis]: + return + + axis_node = axis_nodes[axis] + + if axis_node.isEmpty(): + return + + handle_node = None + handle_node = axis_node.find("x_handle") if axis == "x" else handle_node + handle_node = axis_node.find("y_handle") if axis == "y" else handle_node + handle_node = axis_node.find("z_handle") if axis == "z" else handle_node + + # 如果找不到特定名称的节点,尝试查找任何子节点 + if not handle_node or handle_node.isEmpty(): + children = axis_node.getChildren() + if children.getNumPaths() > 0: + handle_node = children[0] + + if not handle_node: + # print(f"未找到{axis}轴的处理模型") + return + + # 创建或获取材质 + mat = Material() + + adjusted_color = Vec4( + min(color[0], 1.0), + min(color[1], 1.0), + min(color[2], 1.0), + color[3] + ) + + mat.setBaseColor(adjusted_color) + #mat.setEmission(Vec4(1, 1, 1, 1.0)) # 自发光 + + # 应用材质 + handle_node.setMaterial(mat, 1) + + # 设置透明度 + if color[3] < 1.0: + handle_node.setTransparency(TransparencyAttrib.MAlpha) + else: + handle_node.setTransparency(TransparencyAttrib.MNone) + + handle_node.setLightOff() # 禁用光照影响 + handle_node.setShaderOff() # 禁用着色器 + handle_node.setFogOff() # 禁用雾效果 + + handle_node.setBin("fixed",41) + handle_node.setDepthWrite(False) + handle_node.setDepthTest(False) + + # 保存材质引用以便后续修改 + if axis == "x": + self.xMat = mat + elif axis == "y": + self.yMat = mat + elif axis == "z": + self.zMat = mat + + axis_node.setLightOff() + axis_node.setShaderOff() + axis_node.setFogOff() + axis_node.setBin("fixed", 40) + axis_node.setDepthWrite(False) + axis_node.setDepthTest(False) + + except Exception as e: + print(f"设置坐标轴颜色失败: {str(e)}") + # 回退到简单颜色设置 + try: + axis_nodes = { + "x": self.gizmoXAxis, + "y": self.gizmoYAxis, + "z": self.gizmoZAxis + } + + if axis in axis_nodes and axis_nodes[axis]: + axis_nodes[axis].setColor(color[0], color[1], color[2], color[3]) + except: + pass + + # ==================== 鼠标交互处理 ==================== + + def _onGizmoMouseEnter(self, axis): + """鼠标进入坐标轴时的处理 - RenderPipeline 兼容版本""" + try: + # 黄色高亮,增加透明度以确保在 RenderPipeline 下可见 + highlight_color = (1.0, 1.0, 0.0, 1) + self.setGizmoAxisColor(axis, highlight_color) + + # 额外的视觉反馈 + axis_nodes = { + "x": self.gizmoXAxis, + "y": self.gizmoYAxis, + "z": self.gizmoZAxis + } + + if axis in axis_nodes and axis_nodes[axis]: + # 稍微放大以增强视觉效果 + axis_nodes[axis].setScale(1.1) + + self.gizmoHighlightAxis = axis + + except Exception as e: + print(f"鼠标进入坐标轴处理失败: {e}") + + def _onGizmoMouseLeave(self, axis): + """鼠标离开坐标轴时的处理 - RenderPipeline 兼容版本""" + try: + # 恢复原始颜色 + original_colors = { + "x": (1.0, 0.0, 0.0, 1.0), # 红色 + "y": (0.0, 1.0, 0.0, 1.0), # 绿色 + "z": (0.0, 0.0, 1.0, 1.0) # 蓝色 + } + + if axis in original_colors: + self.setGizmoAxisColor(axis, original_colors[axis]) + + # 恢复原始大小 + axis_nodes = { + "x": self.gizmoXAxis, + "y": self.gizmoYAxis, + "z": self.gizmoZAxis + } + + if axis in axis_nodes and axis_nodes[axis]: + axis_nodes[axis].setScale(1.0) + + if self.gizmoHighlightAxis == axis: + self.gizmoHighlightAxis = None + + except Exception as e: + print(f"鼠标离开坐标轴处理失败: {e}") + + def setupGizmoMouseEvents(self): + """设置坐标轴的鼠标事件""" + try: + from direct.showbase.DirectObject import DirectObject + + # 为每个轴设置鼠标事件 + axis_nodes = { + "x": self.gizmoXAxis, + "y": self.gizmoYAxis, + "z": self.gizmoZAxis + } + + for axis_name, axis_node in axis_nodes.items(): + if axis_node: + # 设置碰撞检测标签 + axis_node.setTag("gizmo_axis", axis_name) + axis_node.setTag("pickable", "1") + + + except Exception as e: + print(f"设置坐标轴鼠标事件失败: {e}") + + # ==================== 射线检测和碰撞检测 ==================== + + def checkGizmoClick(self, mouseX, mouseY): + """使用屏幕空间检测是否点击了坐标轴""" + if not self.gizmo or not self.gizmoTarget: + return None + + # 基本参数验证 + if not isinstance(mouseX, (int, float)) or not isinstance(mouseY, (int, float)): + return None + + try: + # 获取坐标轴中心的世界坐标 + gizmo_world_pos = self.gizmo.getPos(self.world.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) + + # 使用Panda3D的内置投影方法 + def worldToScreen(world_pos): + """将世界坐标转换为屏幕坐标""" + # 将世界坐标转换为相机空间 + cam_space_pos = self.world.cam.getRelativePoint(self.world.render, world_pos) + + # 检查是否在相机前方 + if cam_space_pos.getY() <= 0: + return None + + # 使用相机的镜头进行投影 + screen_pos = Point2() + if self.world.cam.node().getLens().project(cam_space_pos, screen_pos): + # 获取准确的窗口尺寸 + win_width, win_height = self.world.getWindowSize() + + # 转换为窗口像素坐标 + win_x = (screen_pos.getX() + 1.0) * 0.5 * win_width + win_y = (1.0 - screen_pos.getY()) * 0.5 * win_height + return (win_x, win_y) + return None + + # 投影各个关键点 + center_screen = worldToScreen(gizmo_world_pos) + x_screen = worldToScreen(x_end) + y_screen = worldToScreen(y_end) + z_screen = worldToScreen(z_end) + + # 如果无法获得屏幕坐标,使用备用方法 + if not center_screen: + return self.checkGizmoClickFallback(mouseX, mouseY) + + # 计算点击阈值 + click_threshold = 15 # 增大检测范围 + + # 检测各个轴,对于端点在屏幕外的轴提供回退方案 + def getClickDetectionPoint(axis_name, original_screen_pos): + if original_screen_pos: + return original_screen_pos + # 如果端点在屏幕外,使用轴长度的一半作为检测点 + if axis_name == "x": + half_end = gizmo_world_pos + Vec3(self.axis_length * 0.5, 0, 0) + elif axis_name == "y": + half_end = gizmo_world_pos + Vec3(0, self.axis_length * 0.5, 0) + elif axis_name == "z": + half_end = gizmo_world_pos + Vec3(0, 0, self.axis_length * 0.5) + else: + return None + return worldToScreen(half_end) + + axes_data = [ + ("x", getClickDetectionPoint("x", x_screen), "X轴"), + ("y", getClickDetectionPoint("y", y_screen), "Y轴"), + ("z", getClickDetectionPoint("z", z_screen), "Z轴") + ] + + for axis_name, axis_screen, axis_label in axes_data: + if axis_screen: + # 计算鼠标到轴线的距离 + distance = self.distanceToLine( + (mouseX, mouseY), center_screen, axis_screen + ) + #print(f"{axis_label}距离: {distance:.2f}") + + if distance < click_threshold: + #print(f"✓ 点击了{axis_label}") + return axis_name + + return None + + except Exception as e: + print(f"坐标轴点击检测失败: {str(e)}") + import traceback + traceback.print_exc() + return None + + def checkGizmoClickFallback(self, mouseX, mouseY): + """备用检测方法:使用固定的屏幕区域""" + + # 获取准确的窗口尺寸 + win_width, win_height = self.world.getWindowSize() + + # 获取窗口中心作为参考点 + center_x = win_width // 2 + center_y = win_height // 2 + + # 定义相对于中心的轴区域(简化假设坐标轴在屏幕中心附近) + axis_length_pixels = 100 # 假设轴长度在屏幕上约100像素 + + # X轴:从中心向右 + x_start = (center_x, center_y) + x_end = (center_x + axis_length_pixels, center_y) + + # Y轴:从中心向上(注意Y轴方向) + y_start = (center_x, center_y) + y_end = (center_x, center_y - axis_length_pixels) + + # Z轴:从中心向右上45度 + z_start = (center_x, center_y) + z_end = (center_x + axis_length_pixels * 0.7, center_y - axis_length_pixels * 0.7) + + threshold = 25 + + # 检测各轴 + if self.distanceToLine((mouseX, mouseY), x_start, x_end) < threshold: + print("✓ 备用方法检测到X轴") + return "x" + elif self.distanceToLine((mouseX, mouseY), y_start, y_end) < threshold: + print("✓ 备用方法检测到Y轴") + return "y" + elif self.distanceToLine((mouseX, mouseY), z_start, z_end) < threshold: + print("✓ 备用方法检测到Z轴") + return "z" + + print("× 备用方法也没有检测到") + return None + + def distanceToLine(self, point, line_start, line_end): + """计算点到线段的距离""" + try: + px, py = point + x1, y1 = line_start + x2, y2 = line_end + + # 计算线段长度 + line_length = ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5 + if line_length == 0: + return ((px - x1) ** 2 + (py - y1) ** 2) ** 0.5 + + # 计算点到线的距离 + t = max(0, min(1, ((px - x1) * (x2 - x1) + (py - y1) * (y2 - y1)) / (line_length ** 2))) + projection_x = x1 + t * (x2 - x1) + projection_y = y1 + t * (y2 - y1) + + distance = ((px - projection_x) ** 2 + (py - projection_y) ** 2) ** 0.5 + return distance + + except Exception as e: + print(f"距离计算错误: {e}") + return float('inf') + + # ==================== 高亮和交互 ==================== + + def detectGizmoAxisAtMouse(self, mouseX, mouseY): + """统一的坐标轴检测方法 - 同时用于高亮和点击检测""" + if not self.gizmo or not self.gizmoTarget: + return None + + try: + # 获取坐标轴中心的世界坐标 + gizmo_world_pos = self.gizmo.getPos(self.world.render) + + #获取坐标轴的世界朝向(考虑旋转) + gizmo_world_quat = self.gizmo.getQuat(self.world.render) + + #计算各轴在世界坐标系中的实际方向向量 + x_axis_world = gizmo_world_quat.xform(Vec3(1,0,0)) + y_axis_world = gizmo_world_quat.xform(Vec3(0,1,0)) + z_axis_world = gizmo_world_quat.xform(Vec3(0,0,1)) + + x_end = gizmo_world_pos + x_axis_world * self.axis_length + y_end = gizmo_world_pos + y_axis_world * self.axis_length + z_end = gizmo_world_pos + z_axis_world * self.axis_length + + + # 计算各轴端点的世界坐标 + # 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) + + # 将3D坐标投影到屏幕坐标 + def worldToScreen(worldPos): + try: + # 转换为相机坐标系 + camPos = self.world.cam.getRelativePoint(self.world.render, worldPos) + + # 检查点是否在相机前方 + if camPos.getY() <= 0: + return None + + # 使用相机lens进行投影 + screenPos = Point2() + lens = self.world.cam.node().getLens() + + if lens.project(camPos, screenPos): + # 获取准确的窗口尺寸 + winWidth, winHeight = self.world.getWindowSize() + + # 转换为像素坐标 + winX = (screenPos.x + 1) * 0.5 * winWidth + winY = (1 - screenPos.y) * 0.5 * winHeight + return (winX, winY) + return None + except: + return None + + # 获取各坐标轴的屏幕投影 + gizmo_screen = worldToScreen(gizmo_world_pos) + x_screen = worldToScreen(x_end) + y_screen = worldToScreen(y_end) + z_screen = worldToScreen(z_end) + + # 如果坐标轴中心不在屏幕内,返回None + if not gizmo_screen: + return None + + # 设置检测阈值 + click_threshold = 35 # 统一使用25像素的检测阈值 + + # 更准确的点到线段距离计算方法 + def distanceToLineSegment(mousePos, start, end): + import math + mx, my = mousePos + x1, y1 = start + x2, y2 = end + + # 线段向量 + dx = x2 - x1 + dy = y2 - y1 + + # 线段长度平方 + length_sq = dx * dx + dy * dy + + if length_sq == 0: + # 线段退化为点 + return math.sqrt((mx - x1) ** 2 + (my - y1) ** 2) + + # 投影参数 + t = max(0, min(1, ((mx - x1) * dx + (my - y1) * dy) / length_sq)) + + # 投影点坐标 + proj_x = x1 + t * dx + proj_y = y1 + t * dy + + # 返回点到投影点的距离 + return math.sqrt((mx - proj_x) ** 2 + (my - proj_y) ** 2) + + mouse_pos = (mouseX, mouseY) + + # 检测各个轴 - 按优先级检测(Z > X > Y) + axes_to_check = [ + ("z", z_screen), + ("x", x_screen), + ("y", y_screen) + ] + + for axis_name, axis_end in axes_to_check: + if axis_end: + distance = distanceToLineSegment(mouse_pos, gizmo_screen, axis_end) + if distance < click_threshold: + return axis_name + + # 如果没有检测到,返回None + return None + + except Exception as e: + # 静默处理错误,避免频繁输出 + return None + + def updateGizmoHighlight(self, mouseX, mouseY): + """更新坐标轴高亮状态""" + if not self.gizmo or self.isDraggingGizmo: + self._resetCursor() + return + + # 使用碰撞检测方法 + #hoveredAxis = self.detectGizmoAxisWithCollision(mouseX, mouseY) + hoveredAxis = self.detectGizmoAxisAtMouse(mouseX, mouseY) + + # 简化稳定性检测逻辑 + if not hasattr(self, '_last_detected_axis'): + self._last_detected_axis = None + + # 如果检测结果发生变化,立即更新高亮 + if hoveredAxis != self._last_detected_axis: + # 更新轴的高亮状态 + if hoveredAxis != self.gizmoHighlightAxis: + # 恢复之前高亮的轴 + if self.gizmoHighlightAxis: + self.setGizmoAxisColor(self.gizmoHighlightAxis, self.gizmo_colors[self.gizmoHighlightAxis]) + + # 高亮新的轴 + if hoveredAxis: + self.setGizmoAxisColor(hoveredAxis, self.gizmo_highlight_colors[hoveredAxis]) + self._setCursor("pointing_hand") + else: + # 如果没有悬停在任何轴上,确保所有轴都恢复原始颜色 + for axis_name in ["x", "y", "z"]: + if axis_name != self.dragGizmoAxis: # 不要改变正在拖拽的轴的颜色 + self.setGizmoAxisColor(axis_name, self.gizmo_colors[axis_name]) + self._resetCursor() + + self.gizmoHighlightAxis = hoveredAxis + + self._last_detected_axis = hoveredAxis + elif hoveredAxis is None: + self._resetCursor() + + def _detectHoveredAxis(self, mouseX, mouseY): + """检测鼠标悬停的轴 - 提取为独立方法""" + # 将原来 updateGizmoHighlight 中的检测逻辑移到这里 + # ... 你原来的检测代码 ... + pass + + def _updateAxisHighlight(self, hoveredAxis): + """更新轴高亮状态 - 确保原子性操作""" + # 恢复之前高亮的轴 + if self.gizmoHighlightAxis: + self.setGizmoAxisColor(self.gizmoHighlightAxis, self.gizmo_colors[self.gizmoHighlightAxis]) + + # 高亮新的轴 + if hoveredAxis: + self.setGizmoAxisColor(hoveredAxis, self.gizmo_highlight_colors[hoveredAxis]) + + self.gizmoHighlightAxis = hoveredAxis + + # ==================== 拖拽变换 ==================== + + def startGizmoDrag(self, axis, mouseX, mouseY): + """开始坐标轴拖拽""" + try: + # 确保状态正确初始化 + if not self.gizmoTarget: + print("开始拖拽失败: 没有拖拽目标") + return + if not self.gizmo: + print("开始拖拽失败: 没有坐标轴") + return + + self.isDraggingGizmo = True + + # 使用当前高亮的轴,如果有的话;否则使用传入的轴 + if self.gizmoHighlightAxis: + self.dragGizmoAxis = self.gizmoHighlightAxis + elif axis and axis in self.gizmo_colors: + self.dragGizmoAxis = axis + else: + # 如果没有明确指定轴,尝试通过鼠标位置检测 + self.dragGizmoAxis = self.detectGizmoAxisAtMouse(mouseX, mouseY) + + # 启动拖拽更新任务 + if hasattr(self.world, 'taskMgr') and self.world.taskMgr: + self.world.taskMgr.add(self._dragUpdateTask, "gizmoDragUpdate") + + # 如果仍然无法确定拖拽轴,则取消拖拽 + if not self.dragGizmoAxis: + print("开始拖拽失败: 无法确定拖拽轴") + self.isDraggingGizmo = False + return + + self.dragStartMousePos = (mouseX, mouseY) + + light_object = self.gizmoTarget.getPythonTag("rp_light_object") + if light_object: + self.gizmoTargetStartPos = Point3(light_object.pos) + else: + self.gizmoTargetStartPos = self.gizmoTarget.getPos() + + # 保存开始拖拽时目标节点的位置和坐标轴的位置 + #self.gizmoTargetStartPos = self.gizmoTarget.getPos() + self.gizmoStartPos = self.gizmo.getPos(self.world.render) # 坐标轴的世界位置 + + # 添加对缩放的支持:保存初始缩放值 + if self.world.tool_manager.isScaleTool(): + self.gizmoTargetStartScale = self.gizmoTarget.getScale() + elif self.world.tool_manager.isRotateTool(): + self.gizmoTargetStartHpr = self.gizmoTarget.getHpr() + + # 确保正在拖动的轴保持高亮状态 + if self.dragGizmoAxis and self.dragGizmoAxis in self.gizmo_colors: + # 先将所有轴恢复为正常颜色 + for axis_name in self.gizmo_colors.keys(): + if axis_name != self.dragGizmoAxis: + self.setGizmoAxisColor(axis_name, self.gizmo_colors[axis_name]) + + # 然后将当前拖动的轴设置为高亮颜色 + self.setGizmoAxisColor(self.dragGizmoAxis, self.gizmo_highlight_colors[self.dragGizmoAxis]) + # elif axis and axis in self.gizmo_colors: + # for axis_name in self.gizmo_colors.keys(): + # if axis_name != axis: + # self.setGizmoAxisColor(axis_name, self.gizmo_colors[axis_name]) + # + # self.setGizmoAxisColor(axis, self.gizmo_highlight_colors[axis]) + # self.dragGizmoAxis = axis + # + # self.gizmoHighlightAxis = self.dragGizmoAxis + # 设置拖拽光标 + if self.dragGizmoAxis == "x": + self._setCursor("size_all") # 水平调整光标 + elif self.dragGizmoAxis == "y": + self._setCursor("size_all") # 垂直调整光标 + elif self.dragGizmoAxis == "z": + self._setCursor("size_all") # 全向调整光标 + + print( + f"开始拖拽 {self.dragGizmoAxis} 轴 - 目标起始位置: {self.gizmoTargetStartPos}, 坐标轴位置: {self.gizmoStartPos}, 鼠标: ({mouseX}, {mouseY})") + + except Exception as e: + print(f"开始坐标轴拖拽失败: {str(e)}") + import traceback + traceback.print_exc() + + def updateGizmoDrag(self, mouseX, mouseY): + """更新坐标轴拖拽 - 使用正确的坐标系变换,支持旋转后的子节点拖拽""" + try: + # 添加详细的状态检查和调试信息 + if not self.isDraggingGizmo: + print("拖拽更新失败: 不在拖拽状态") + return + if not self.gizmoTarget: + print("拖拽更新失败: 没有拖拽目标") + return + if not hasattr(self, 'dragStartMousePos') or not self.dragStartMousePos: + print("拖拽更新失败: 没有拖拽起始位置") + return + if not hasattr(self, 'gizmoTargetStartPos') or not self.gizmoTargetStartPos: + print("拖拽更新失败: 没有目标起始位置") + return + if not hasattr(self, 'gizmoStartPos') or not self.gizmoStartPos: + print("拖拽更新失败: 没有坐标轴起始位置") + return + + is_scale_tool = self.world.tool_manager.isScaleTool() if self.world.tool_manager else False + is_rotate_tool = self.world.tool_manager.isRotateTool() if self.world.tool_manager else False + + is_gui_element = (hasattr(self.gizmoTarget,'getTag') and + self.gizmoTarget.getTag("is_gui_element") == "1") + + + # 计算鼠标移动距离(屏幕像素) + mouseDeltaX = mouseX - self.dragStartMousePos[0] + mouseDeltaY = mouseY - self.dragStartMousePos[1] + + if is_scale_tool: + scale_factor = 1.0 + (mouseDeltaX + mouseDeltaY) * 0.01 + + scale_factor = max(0.001, scale_factor) + + start_scale = getattr(self,'gizmoTargetStartScale',Vec3(1,1,1)) + + if is_gui_element: + if self.dragGizmoAxis == "x": + new_scale = Vec3(start_scale.x * scale_factor,start_scale.y,start_scale.z) + elif self.dragGizmoAxis == "y": + new_scale = Vec3(start_scale.x, start_scale.y * scale_factor, start_scale.z) + elif self.dragGizmoAxis == "z": + new_scale = Vec3(start_scale.x, start_scale.y, start_scale.z * scale_factor) + else: + new_scale = Vec3(start_scale.x * scale_factor, + start_scale.y * scale_factor, + start_scale.z * scale_factor) + else: + # 普通3D模型的缩放处理 + if self.dragGizmoAxis == "x": + new_scale = Vec3(start_scale.x * scale_factor, start_scale.y, start_scale.z) + elif self.dragGizmoAxis == "y": + new_scale = Vec3(start_scale.x, start_scale.y * scale_factor, start_scale.z) + elif self.dragGizmoAxis == "z": + z_scale_factor = 1.0 - (mouseDeltaX + mouseDeltaY) * 0.01 + new_scale = Vec3(start_scale.x, start_scale.y, start_scale.z * z_scale_factor) + else: + new_scale = Vec3(start_scale.x * scale_factor, + start_scale.y * scale_factor, + start_scale.z * scale_factor) + + new_scale = Vec3( + max(0.001,new_scale.x), + max(0.001,new_scale.y), + max(0.001,new_scale.z) + ) + + # 应用新缩放值 + self.gizmoTarget.setScale(new_scale) + if hasattr(self.world, 'property_panel') and self.world.property_panel: + self.world.property_panel.refreshModelValues(self.gizmoTarget) + return + elif is_rotate_tool: + rotation_speed = 0.5 + rotation_amount = (mouseDeltaX + mouseDeltaY) * rotation_speed + start_hpr = getattr(self,'gizmoTargetStartHpr',self.gizmoTarget.getHpr()) + + if self.dragGizmoAxis == "x": + new_hpr = Vec3(start_hpr.x+rotation_amount,start_hpr.y,start_hpr.z) + elif self.dragGizmoAxis == "y": + new_hpr = Vec3(start_hpr.x,start_hpr.y-rotation_amount,start_hpr.z) + elif self.dragGizmoAxis == "z": + new_hpr = Vec3(start_hpr.x,start_hpr.y,start_hpr.z+rotation_amount) + else: + # 默认绕所有轴旋转 + new_hpr = Vec3(start_hpr.x + rotation_amount, + start_hpr.y + rotation_amount, + start_hpr.z + rotation_amount) + self.gizmoTarget.setHpr(new_hpr) + if hasattr(self.world, 'property_panel') and self.world.property_panel: + self.world.property_panel.refreshModelValues(self.gizmoTarget) + return + + # 使用坐标轴的实际位置而不是目标节点位置来计算屏幕投影 + gizmo_world_pos = self.gizmoStartPos + + # 【关键修复】:获取正确的轴向量,考虑父节点的旋转 + # 检查目标节点是否有父节点 + parent_node = self.gizmoTarget.getParent() + + # 计算轴向量在正确坐标系中的方向 + if self.dragGizmoAxis == "x": + # 在局部坐标系中的X轴方向 + local_axis_vector = Vec3(1, 0, 0) + elif self.dragGizmoAxis == "y": + # 在局部坐标系中的Y轴方向 + local_axis_vector = Vec3(0, 1, 0) + elif self.dragGizmoAxis == "z": + # 在局部坐标系中的Z轴方向 + local_axis_vector = Vec3(0, 0, 1) + else: + print(f"拖拽更新失败: 未知轴类型 {self.dragGizmoAxis}") + return + + world_axis_vector = local_axis_vector + + if parent_node and parent_node != self.world.render: + try: + #获取变换矩阵 + transfrom_mat = parent_node.getMat(self.world.render) + if transfrom_mat.is_identity() or self._isMatrixValid(transfrom_mat): + world_axis_vector = transfrom_mat.xformVec(local_axis_vector) + else: + print("警告: 检测到无效变换矩阵,使用默认轴向量") + except Exception as e: + print(f"变换计算出错: {e},使用默认轴向量") + else: + world_axis_vector = local_axis_vector + + # 确定轴向量的变换上下文 + # if parent_node and parent_node != self.world.render: + # transform_mat = parent_node.getMat(self.world.render) + # world_axis_vector = transform_mat.xformVec(local_axis_vector) + # else: + # world_axis_vector = local_axis_vector + + #axis_end = gizmo_world_pos + world_axis_vector + + # 投影到屏幕空间 + def worldToScreen(worldPos): + try: + camPos = self.world.cam.getRelativePoint(self.world.render, worldPos) + if camPos.getY() <= 0: + return None + + screenPos = Point2() + if self.world.cam.node().getLens().project(camPos, screenPos): + winWidth, winHeight = self.world.getWindowSize() + winX = (screenPos.x + 1) * 0.5 * winWidth + winY = (1 - screenPos.y) * 0.5 * winHeight + return (winX, winY) + return None + except Exception as e: + print(f"世界坐标转屏幕坐标失败: {e}") + return None + axis_start_screen = worldToScreen(gizmo_world_pos) + axis_end_world = gizmo_world_pos + world_axis_vector + axis_end_screen = worldToScreen(axis_end_world) + + if not axis_start_screen or not axis_end_screen: + print("拖拽更新失败: 无法获取轴线屏幕坐标") + return + + screen_axis_dir = ( + axis_end_screen[0] - axis_start_screen[0], + axis_end_screen[1] - axis_start_screen[1] + ) + + # 归一化屏幕轴方向 + import math + length = math.sqrt(screen_axis_dir[0]**2 + screen_axis_dir[1]**2) + if length > 0: + #screen_axis_dir = (screen_axis_dir[0] / length, screen_axis_dir[1] / length) + screen_axis_dir = ( + screen_axis_dir[0] / length, + screen_axis_dir[1] / length + ) + + else: + print("拖拽更新失败: 屏幕轴方向长度为0") + return + + # 将鼠标移动投影到轴方向上 + projected_distance = (mouseDeltaX * screen_axis_dir[0] + + mouseDeltaY * screen_axis_dir[1]) + + cam_pos = self.world.cam.getPos(self.world.render) + distance_to_object = (cam_pos - gizmo_world_pos).length() + + lens = self.world.cam.node().getLens() + fov = lens.getFov()[0] + winWidth,winHeight = self.world.getWindowSize() + + pixels_to_world_units = (2*distance_to_object*math.tan(math.radians(fov/2)))/winWidth + + movement_distance = projected_distance * pixels_to_world_units + + total_scale_factor = 1.0 + current_node = self.gizmoTarget.getParent() + + while current_node and current_node != self.world.render: + try: + if not current_node.isEmpty(): + node_scale = current_node.getScale() + if node_scale.x > 0 and node_scale.y >0 and node_scale.z >0 : + avg_scale = (node_scale.x + node_scale.y + node_scale.z)/3.0 + total_scale_factor *= avg_scale + #avg_scale = (node_scale.x+node_scale.y + node_scale.z) / 3.0 + #total_scale_factor *= avg_scale + current_node = current_node.getParent() + else: + break + except: + break + + + if total_scale_factor > 0: + movement_distance = movement_distance / total_scale_factor + + currentPos = self.gizmoTargetStartPos + + # 根据拖拽的轴,只修改对应的坐标分量 + if self.dragGizmoAxis == "x": + newPos = Vec3(currentPos.x + movement_distance, currentPos.y, currentPos.z) + #print(f"X轴移动:{currentPos.x} -> {newPos.x}") + elif self.dragGizmoAxis == "y": + newPos = Vec3(currentPos.x, currentPos.y + movement_distance, currentPos.z) + #print(f"Y轴移动:{currentPos.y} -> {newPos.y}") + elif self.dragGizmoAxis == "z": + newPos = Vec3(currentPos.x, currentPos.y, currentPos.z + movement_distance) + #print(f"Z轴移动:{currentPos.z} -> {newPos.z}") + else: + print(f"未知轴: {self.dragGizmoAxis}") + return + + # 应用新位置到目标节点 + light_object = self.gizmoTarget.getPythonTag("rp_light_object") + if light_object: + light_object.pos = newPos + self.gizmoTarget.setPos(newPos) + print(f"🔄 光源拖拽移动: {currentPos} -> {newPos}") + else: + self.gizmoTarget.setPos(newPos) + print(f"🔄 节点拖拽移动: {currentPos} -> {newPos} (轴: {self.dragGizmoAxis}, 距离: {movement_distance:.3f})") + + # 更新属性面板 + if hasattr(self.world, 'property_panel') and self.world.property_panel: + self.world.property_panel.refreshModelValues(self.gizmoTarget) + + # 更新坐标轴位置 - 计算新的中心位置 + minPoint = Point3() + maxPoint = Point3() + if self.gizmoTarget.calcTightBounds(minPoint, maxPoint, self.world.render): + center = Point3((minPoint.x + maxPoint.x) * 0.5, + (minPoint.y + maxPoint.y) * 0.5, + (minPoint.z + maxPoint.z) * 0.5) + self.gizmo.setPos(center) + + # 实时更新属性面板 + self.world.property_panel.refreshModelValues(self.gizmoTarget) + + # 每次拖拽都输出调试信息(但限制频率) + if not hasattr(self, '_last_drag_debug_time'): + self._last_drag_debug_time = 0 + + import time + current_time = time.time() + if current_time - self._last_drag_debug_time > 0.1: # 每0.1秒最多输出一次 + #print(f"拖拽更新成功 - 轴:{self.dragGizmoAxis}, 比例:{scale_factor:.6f}, 投影:{projected_distance:.2f}") + self._last_drag_debug_time = current_time + + except Exception as e: + print(f"更新坐标轴拖拽失败: {str(e)}") + import traceback + traceback.print_exc() + + def _isMatrixValid(self, matrix): + """检查矩阵是否有效,替代 isSingular 方法""" + try: + # 检查矩阵元素是否为有效数字 + for i in range(4): + for j in range(4): + element = matrix.getCell(i, j) + # 检查是否为 NaN 或无穷大 + if str(element) == 'nan' or str(element) == 'inf' or str(element) == '-inf': + return False + # 检查是否过大 + if abs(element) > 1e10: + return False + return True + except: + return False + + def stopGizmoDrag(self): + """停止坐标轴拖拽""" + print(f"停止坐标轴拖拽 - 轴: {self.dragGizmoAxis}") + + # 移除拖拽更新任务 + if hasattr(self.world, 'taskMgr') and self.world.taskMgr: + self.world.taskMgr.remove("gizmoDragUpdate") + + def _dragUpdateTask(self, task): + """拖拽更新任务 - 持续更新拖拽状态""" + try: + if not self.isDraggingGizmo: + return task.done + + # 检查鼠标是否仍然按下 + if not self.world.mouseWatcherNode.hasMouse(): + return task.cont + + # 获取当前鼠标位置 + mouse_x = self.world.mouseWatcherNode.getMouseX() + mouse_y = self.world.mouseWatcherNode.getMouseY() + + # 转换为窗口坐标 + winWidth, winHeight = self.world.getWindowSize() + window_x = (mouse_x + 1) * 0.5 * winWidth + window_y = (1 - mouse_y) * 0.5 * winHeight + + # 调用拖拽更新 + self.updateGizmoDrag(window_x, window_y) + + return task.cont + + except Exception as e: + print(f"拖拽更新任务错误: {e}") + return task.done + + if hasattr(self.world,'command_manager') and self.world.command_manager and self.gizmoTarget: + current_pos = self.gizmoTarget.getPos() + + if (hasattr(self,'gizmoTargetStartPos') and self.gizmoTargetStartPos and + (abs(current_pos.x-self.gizmoTargetStartPos.x)>0.001 or + abs(current_pos.y-self.gizmoTargetStartPos.y)>0.001 or + abs(current_pos.z-self.gizmoTargetStartPos.z)>0.001)): + from core.Command_System import MoveNodeCommand + from core.Command_System import MoveLightCommand + + light_object = self.gizmoTarget.getPythonTag("rp_light_object") + if light_object: + command = MoveLightCommand(self.gizmoTarget,self.gizmoTargetStartPos,current_pos,light_object) + else: + command = MoveNodeCommand(self.gizmoTarget,self.gizmoTargetStartPos,current_pos) + self.world.command_manager.execute_command(command) + # 如果是缩放操作且缩放发生了变化,则创建缩放命令 + elif (hasattr(self, 'gizmoTargetStartScale') and hasattr(self, 'gizmoTargetStartScale') and + self.gizmoTargetStartScale): + current_scale = self.gizmoTarget.getScale() + if (abs(current_scale.x - self.gizmoTargetStartScale.x) > 0.001 or + abs(current_scale.y - self.gizmoTargetStartScale.y) > 0.001 or + abs(current_scale.z - self.gizmoTargetStartScale.z) > 0.001): + from core.Command_System import ScaleNodeCommand + command = ScaleNodeCommand(self.gizmoTarget, self.gizmoTargetStartScale, current_scale) + self.world.command_manager.execute_command(command) + # 如果是旋转操作且旋转发生了变化,则创建旋转命令 + elif (hasattr(self, 'gizmoTargetStartHpr') and hasattr(self, 'gizmoTargetStartHpr') and + self.gizmoTargetStartHpr): + current_hpr = self.gizmoTarget.getHpr() + if (abs(current_hpr.x - self.gizmoTargetStartHpr.x) > 0.001 or + abs(current_hpr.y - self.gizmoTargetStartHpr.y) > 0.001 or + abs(current_hpr.z - self.gizmoTargetStartHpr.z) > 0.001): + from core.Command_System import RotateNodeCommand + command = RotateNodeCommand(self.gizmoTarget, self.gizmoTargetStartHpr, current_hpr) + self.world.command_manager.execute_command(command) + + # 恢复所有轴的颜色 + for axis_name in ["x", "y", "z"]: + self.setGizmoAxisColor(axis_name, self.gizmo_colors[axis_name]) + + self.isDraggingGizmo = False + self.dragGizmoAxis = None + self.dragStartMousePos = None + # 清理拖拽状态,下次拖拽开始时重新设置 + self.gizmoTargetStartPos = None + self.gizmoStartPos = None + + if hasattr(self, 'gizmoTargetStartScale'): + delattr(self, 'gizmoTargetStartScale') + if hasattr(self, 'gizmoTargetStartHpr'): + delattr(self, 'gizmoTargetStartHpr') + + # 重置高亮轴 + self.gizmoHighlightAxis = None + self._resetCursor() + # ==================== 选择管理 ==================== + + def updateSelection(self, nodePath): + try: + if self.selectedNode == nodePath: + return + #print(f"\n=== 更新选择状态 ===") + + # 如果正在删除节点,避免更新选择 + if hasattr(self, '_deleting_node') and self._deleting_node: + print("正在删除节点,跳过选择更新") + #print("=== 选择状态更新完成 ===\n") + return + + node_name = "None" + if nodePath and not nodePath.isEmpty(): + node_name = nodePath.getName() + #print(f"新选择的节点: {node_name}") + + self.selectedNode = nodePath + # 添加兼容性属性 + self.selectedObject = nodePath + + if nodePath and not nodePath.isEmpty(): + node_name = nodePath.getName() + #print(f"开始为节点 {node_name} 创建选择框和坐标轴...") + + # 创建选择框 + #print("创建选择框...") + self.createSelectionBox(nodePath) + if self.selectionBox: + box_name = "Unknown" + if self.selectionBox and not self.selectionBox.isEmpty(): + box_name = self.selectionBox.getName() + #print(f"✓ 选择框创建成功: {box_name}") + else: + print("× 选择框创建失败") + + # 创建坐标轴 + #print("创建坐标轴...") + self.createGizmo(nodePath) + if self.gizmo: + gizmo_name = "Unknown" + if self.gizmo and not self.gizmo.isEmpty(): + gizmo_name = self.gizmo.getName() + #print(f"✓ 坐标轴创建成功: {gizmo_name}") + else: + print("× 坐标轴创建失败") + + else: + print("清除选择...") + self.clearSelectionBox() + self.clearGizmo() + print("✓ 取消选择") + + #当取消选择时,同步清空树形控件的选中状态 + if (hasattr(self.world,'interface_manager')and + self.world.interface_manager and + self.world.interface_manager.treeWidget): + self.world.interface_manager.treeWidget.setCurrentItem(None) + print("✓ 树形控件选中状态已清空") + + #print("=== 选择状态更新完成 ===\n") + except Exception as e: + print(f"更新选择状态失败{str(e)}") + import traceback + traceback.print_exc() + + def _reparentTreeItem(self, item, new_parent_item): + """将树项重新父化到新的父项下""" + if not item or not new_parent_item: + return + + # 从当前父项中移除 + current_parent = item.parent() + if current_parent: + current_parent.removeChild(item) + else: + # 如果是顶级项 + index = self.indexOfTopLevelItem(item) + if index >= 0: + self.takeTopLevelItem(index) + + # 添加到新父项 + new_parent_item.addChild(item) + + # def _updateSelectionVisuals(self, nodePath): + # """更新选择的视觉效果(选择框和坐标轴)""" + # try: + # if nodePath and not nodePath.isEmpty(): + # node_name = nodePath.getName() + # print(f"开始为节点 {node_name} 创建选择框和坐标轴...") + # + # # 创建选择框 + # print("创建选择框...") + # self.createSelectionBox(nodePath) + # if self.selectionBox: + # box_name = "Unknown" + # if self.selectionBox and not self.selectionBox.isEmpty(): + # box_name = self.selectionBox.getName() + # print(f"✓ 选择框创建成功: {box_name}") + # else: + # print("× 选择框创建失败") + # + # # 创建坐标轴 + # print("创建坐标轴...") + # self.createGizmo(nodePath) + # if self.gizmo: + # gizmo_name = "Unknown" + # if self.gizmo and not self.gizmo.isEmpty(): + # gizmo_name = self.gizmo.getName() + # print(f"✓ 坐标轴创建成功: {gizmo_name}") + # else: + # print("× 坐标轴创建失败") + # + # print(f"✓ 选中了节点: {node_name}") + # else: + # print("清除选择...") + # self.clearSelectionBox() + # self.clearGizmo() + # print("✓ 取消选择") + # + # except Exception as e: + # print(f"更新选择视觉效果失败: {e}") + + def getSelectedNode(self): + """获取当前选中的节点""" + return self.selectedNode + + def hasSelection(self): + """检查是否有选中的节点""" + return self.selectedNode is not None + + def checkAndClearIfTargetDeleted(self): + if (self.gizmoTarget and self.gizmoTarget.isEmpty()): + self.clearGizmo() + + if (self.selectionBoxTarget and self.selectionBoxTarget.isEmpty()): + self.clearSelectionBox() + + def setupGizmoCollision(self): + if not self.gizmo or not self.gizmoXAxis or not self.gizmoYAxis or not self.gizmoZAxis: + return + + # 清除现有的碰撞节点 + for axis_name in ["x", "y", "z"]: + axis_node = getattr(self, f"gizmo{axis_name.upper()}Axis") + if axis_node: + # 查找并移除所有现有的碰撞节点 + collision_nodes = axis_node.findAllMatches("**/gizmo_collision_*") + for collision_node in collision_nodes: + collision_node.removeNode() + + # 为每个轴创建碰撞体 + self.createAxisCollision("x", self.gizmoXAxis) + self.createAxisCollision("y", self.gizmoYAxis) + self.createAxisCollision("z", self.gizmoZAxis) + + def createAxisCollision(self, axis_name, axis_node): + # 为单个轴创建碰撞体 + try: + handle_node = axis_node.find(f"{axis_name}_handle") + if not handle_node or handle_node.isEmpty(): + children = axis_node.getChildren() + if children.getNumPaths() > 0: + handle_node = children[0] + else: + print(f"警告: 未找到 {axis_name} 轴的 handle 节点") + return + + collision_node = CollisionNode(f"gizmo_collision_{axis_name}") + collision_node.setIntoCollideMask(BitMask32.bit(1)) # 设置为into对象 + collision_node.setFromCollideMask(BitMask32.allOff()) # 不作为from对象 + + # 调整碰撞尺寸以匹配实际的轴长度和坐标轴缩放 + scale_factor = self.gizmo.getScale().x if self.gizmo else 1.0 + axis_length = 2.0 * scale_factor + radius = 0.3 * scale_factor + + # 根据轴的类型创建合适的碰撞体 + if axis_name == "x": + capsule = CollisionCapsule( + Point3(0, 0, 0), + Point3(axis_length, 0, 0), + radius + ) + collision_node.addSolid(capsule) + elif axis_name == "y": + capsule = CollisionCapsule( + Point3(0, 0, 0), + Point3(0, axis_length, 0), + radius + ) + collision_node.addSolid(capsule) + elif axis_name == "z": + capsule = CollisionCapsule( + Point3(0, 0, 0), + Point3(0, 0, axis_length), + radius + ) + collision_node.addSolid(capsule) + + # 将碰撞节点附加到handle节点,使其与可视化几何体保持一致 + collision_np = handle_node.attachNewNode(collision_node) + + # 设置标签以便识别 + collision_np.setTag("gizmo_axis", axis_name) + collision_np.setTag("pickable", "1") + + collision_np.hide() # 隐藏碰撞体,只用于检测 + + #print(f"✓ 成功创建 {axis_name} 轴碰撞体") + + except Exception as e: + print(f"创建{axis_name}轴碰撞体失败: {e}") + import traceback + traceback.print_exc() + + def detectGizmoAxisWithCollision(self, mouseX, mouseY): + # 使用碰撞体检测鼠标是否悬停在坐标轴上 + if not self.gizmo: + return None + + try: + ray = CollisionRay() + + win_width, win_height = self.world.getWindowSize() + + mouse_x_ndc = (mouseX / win_width) * 2.0 - 1.0 + mouse_y_ndc = 1.0 - (mouseY / win_height) * 2.0 + + ray.setFromLens(self.world.cam.node(), mouse_x_ndc, mouse_y_ndc) + + traverser = CollisionTraverser("gizmo_traverser") + handler = CollisionHandlerQueue() + + # 创建射线节点 + ray_node = CollisionNode('mouseRay') + ray_node.addSolid(ray) + ray_node.setFromCollideMask(BitMask32.bit(1)) # 射线作为from对象 + ray_node.setIntoCollideMask(BitMask32.allOff()) # 射线不作为into对象 + ray_np = self.world.render.attachNewNode(ray_node) + + # 为所有轴的碰撞体设置正确的掩码并添加到遍历器 + collision_found = False + for axis_name in ["x", "y", "z"]: + axis_node = getattr(self, f"gizmo{axis_name.upper()}Axis") + if axis_node: + collision_node_path = axis_node.find("**/gizmo_collision_*") + if not collision_node_path.isEmpty(): + collision_node = collision_node_path.node() + collision_node.setFromCollideMask(BitMask32.allOff()) # 碰撞体不作为from对象 + collision_node.setIntoCollideMask(BitMask32.bit(1)) # 碰撞体作为into对象 + collision_found = True + + if not collision_found: + ray_np.removeNode() + return None + + # 执行碰撞检测 - 这里是关键修复点 + traverser.addCollider(ray_np, handler) + traverser.traverse(self.world.render) + + ray_np.removeNode() + + # 检查是否有碰撞 + if handler.getNumEntries() > 0: + handler.sortEntries() + closest_entry = handler.getEntry(0) + + # 获取碰撞的对象 + collided_object = closest_entry.getIntoNodePath() + axis_tag = collided_object.getTag("gizmo_axis") + + if axis_tag in ["x", "y", "z"]: + return axis_tag + + return None + + except Exception as e: + print(f"使用碰撞体检测坐标轴失败: {e}") + import traceback + traceback.print_exc() + return None + + def debugGizmoCollision(self): + print("===碰撞体调试信息===") + for axis_name in ["x","y","z"]: + axis_node = getattr(self,f"gizmo{axis_name.upper()}Axis") + if axis_node: + handle_node = axis_node.find(f"{axis_name}_handle") + collision_node = axis_node.find("**/gizmo_collision_*") + print(f"{axis_name.upper()}轴:") + print(f" - 轴节点: {axis_node}") + print(f" - Handle节点: {handle_node}") + print(f" - 碰撞节点: {collision_node}") + if not collision_node.isEmpty(): + print(f" - 碰撞体标签: {collision_node.getTag('gizmo_axis')}") + else: + print(f"{axis_name.upper()}轴节点不存在") + + def focusCameraOnSelectedNodeAdvanced(self): + """高级版的摄像机聚焦功能,包含平滑动画效果""" + try: + if not self.selectedNode or self.selectedNode.isEmpty(): + print("没有选中的节点,无法聚焦") + return False + + # 获取选中节点的边界框 + minPoint = Point3() + maxPoint = Point3() + + if not self.selectedNode.calcTightBounds(minPoint, maxPoint, self.world.render): + print("无法计算选中节点的边界框,使用节点为位置作为替代方案") + node_pos = self.selectedNode.getPos(self.world.render) + optimal_distance = 10.0 + current_cam_pos = self.world.cam.getPos() + view_direction = node_pos - current_cam_pos + if view_direction.length()<0.001: + view_direction = Vec3(5,-5,2) + view_direction.normalize() + target_cam_pos = node_pos - (view_direction * optimal_distance) + + temp_node =self.world.render.attachNewNode("temp_lookat_target") + temp_node.setPos(node_pos) + dummy_cam = self.world.render.attachNewNode("dummy_camera") + dummy_cam.setPos(target_cam_pos) + dummy_cam.lookAt(temp_node) + target_cam_hpr = Vec3(dummy_cam.getHpr()) + + temp_node.removeNode() + dummy_cam.removeNode() + + currrent_cam_pos = Point3(self.world.cam.getPos()) + current_cam_hpr = Vec3(self.world.cam.getHpr()) + self._startCameraFocusAnimation(current_cam_pos,target_cam_pos,current_cam_hpr,target_cam_hpr) + print(f"开始聚焦到节点(使用位置): {self.selectedNode.getName()}") + return True + + # 计算节点中心点和大小 + center = Point3( + (minPoint.x + maxPoint.x) * 0.5, + (minPoint.y + maxPoint.y) * 0.5, + (minPoint.z + maxPoint.z) * 0.5 + ) + + # 计算节点的对角线长度 + size = (maxPoint - minPoint).length() + + # 如果节点太小,使用默认大小 + if size < 0.01: + size = 1.0 + + # 获取当前摄像机位置和朝向 + current_cam_pos = Point3(self.world.cam.getPos()) + current_cam_hpr = Vec3(self.world.cam.getHpr()) + + # 计算观察方向 + view_direction = current_cam_pos - center + if view_direction.length() < 0.001: + view_direction = Vec3(5, -5, 2) + + view_direction.normalize() + + # 计算合适的观察距离 + optimal_distance = max(size * 1, 1.0) + + # 计算目标摄像机位置 + target_cam_pos = center + (view_direction * optimal_distance) + + # 计算目标朝向(不直接使用lookAt,而是计算目标HPR) + # 创建临时节点用于计算目标朝向 + temp_node = self.world.render.attachNewNode("temp_lookat_target") + temp_node.setPos(center) + + # 创建另一个临时节点用于计算朝向 + dummy_cam = self.world.render.attachNewNode("dummy_camera") + dummy_cam.setPos(target_cam_pos) + dummy_cam.lookAt(temp_node) + target_cam_hpr = Vec3(dummy_cam.getHpr()) + + # 清理临时节点 + temp_node.removeNode() + dummy_cam.removeNode() + + # 使用任务来实现平滑移动动画 + self._startCameraFocusAnimation(current_cam_pos, target_cam_pos, + current_cam_hpr, target_cam_hpr) + + print(f"开始聚焦到节点: {self.selectedNode.getName()}") + return True + + except Exception as e: + print(f"高级聚焦功能失败: {str(e)}") + import traceback + traceback.print_exc() + return False + + def _startCameraFocusAnimation(self, start_pos, end_pos, start_hpr, end_hpr): + """启动摄像机聚焦动画""" + try: + # 创建动画任务 + class CameraFocusData: + def __init__(self, start_pos, end_pos, start_hpr, end_hpr): + self.start_pos = Point3(start_pos) # 确保是Point3类型 + self.end_pos = Point3(end_pos) # 确保是Point3类型 + self.start_hpr = Vec3(start_hpr) # 确保是Vec3类型 + self.end_hpr = Vec3(end_hpr) # 确保是Vec3类型 + self.elapsed_time = 0.0 + self.duration = 0.8 # 增加动画持续时间到0.8秒,让动画更平滑 + + self._camera_focus_data = CameraFocusData(start_pos, end_pos, start_hpr, end_hpr) + + # 移除之前的任务(如果存在) + taskMgr.remove("cameraFocusTask") + + # 添加新任务 + taskMgr.add(self._cameraFocusTask, "cameraFocusTask") + + except Exception as e: + print(f"启动摄像机聚焦动画失败: {e}") + + def _normalizeAngle(self, angle): + """规范化角度到-180到180度之间""" + while angle > 180: + angle -= 360 + while angle < -180: + angle += 360 + return angle + + def _cameraFocusTask(self, task): + """摄像机聚焦动画任务""" + try: + if not hasattr(self, '_camera_focus_data'): + return task.done + + data = self._camera_focus_data + data.elapsed_time += globalClock.getDt() + + # 计算插值因子 + t = min(1.0, data.elapsed_time / data.duration) + + # 使用更平滑的插值函数 + smooth_t = t * t * (3 - 2 * t) # 平滑步进插值 + + # 手动实现lerp功能 + def lerp_point3(start, end, factor): + return Point3( + start.x + (end.x - start.x) * factor, + start.y + (end.y - start.y) * factor, + start.z + (end.z - start.z) * factor + ) + + # 角度插值需要特殊处理,确保选择最短路径 + def lerp_hpr(start, end, factor): + # 规范化角度 + start_x = self._normalizeAngle(start.x) + start_y = self._normalizeAngle(start.y) + start_z = self._normalizeAngle(start.z) + + end_x = self._normalizeAngle(end.x) + end_y = self._normalizeAngle(end.y) + end_z = self._normalizeAngle(end.z) + + # 计算最短旋转路径 + diff_x = self._normalizeAngle(end_x - start_x) + diff_y = self._normalizeAngle(end_y - start_y) + diff_z = self._normalizeAngle(end_z - start_z) + + # 插值 + result_x = start_x + diff_x * factor + result_y = start_y + diff_y * factor + result_z = start_z + diff_z * factor + + # 再次规范化 + result_x = self._normalizeAngle(result_x) + result_y = self._normalizeAngle(result_y) + result_z = self._normalizeAngle(result_z) + + return Vec3(result_x, result_y, result_z) + + # 计算当前位置和朝向 + current_pos = lerp_point3(data.start_pos, data.end_pos, smooth_t) + current_hpr = lerp_hpr(data.start_hpr, data.end_hpr, smooth_t) + + # 应用到摄像机 + self.world.cam.setPos(current_pos) + self.world.cam.setHpr(current_hpr) + + # 检查是否完成 + if t >= 1.0: + if hasattr(self, '_camera_focus_data'): + delattr(self, '_camera_focus_data') + print("摄像机聚焦动画完成") + return task.done + + return task.cont + + except Exception as e: + print(f"摄像机聚焦任务出错: {e}") + import traceback + traceback.print_exc() + return task.done + + def _smoothCameraMoveTask(self, task): + """平滑摄像机移动任务""" + try: + if not hasattr(self, '_smooth_camera_move_data'): + return task.done + + data = self._smooth_camera_move_data + data.elapsed_time += globalClock.getDt() + + # 计算插值因子 + t = min(1.0, data.elapsed_time / data.duration) + + # 使用平滑插值 + smooth_t = t * t * (3 - 2 * t) + + # 角度插值需要特殊处理 + def lerp_hpr(start, end, factor): + # 规范化角度 + start_x = self._normalizeAngle(start.x) + start_y = self._normalizeAngle(start.y) + start_z = self._normalizeAngle(start.z) + + end_x = self._normalizeAngle(end.x) + end_y = self._normalizeAngle(end.y) + end_z = self._normalizeAngle(end.z) + + # 计算最短旋转路径 + diff_x = self._normalizeAngle(end_x - start_x) + diff_y = self._normalizeAngle(end_y - start_y) + diff_z = self._normalizeAngle(end_z - start_z) + + # 插值 + result_x = start_x + diff_x * factor + result_y = start_y + diff_y * factor + result_z = start_z + diff_z * factor + + return Vec3(result_x, result_y, result_z) + + # 计算当前位置和朝向 + current_pos = Point3( + data.start_pos.x + (data.end_pos.x - data.start_pos.x) * smooth_t, + data.start_pos.y + (data.end_pos.y - data.start_pos.y) * smooth_t, + data.start_pos.z + (data.end_pos.z - data.start_pos.z) * smooth_t + ) + + current_hpr = lerp_hpr(data.start_hpr, data.end_hpr, smooth_t) + + # 应用到摄像机 + self.world.cam.setPos(current_pos) + self.world.cam.setHpr(current_hpr) + + # 检查是否完成 + if t >= 1.0: + if hasattr(self, '_smooth_camera_move_data'): + delattr(self, '_smooth_camera_move_data') + print("摄像机平滑移动完成") + return task.done + + return task.cont + + except Exception as e: + print(f"平滑摄像机移动任务出错: {e}") + import traceback + traceback.print_exc() + return task.done + + def handleMouseClick(self, nodePath, mouseX=None, mouseY=None): + """处理鼠标点击事件 - 支持坐标轴双击聚焦""" + try: + # 如果正在删除节点,忽略鼠标点击 + if hasattr(self, '_deleting_node') and self._deleting_node: + print("正在删除节点,忽略鼠标点击") + return + + import time + current_time = time.time() + + # 检查是否点击了坐标轴 + is_gizmo_click = False + target_node = nodePath + + # 判断是否点击了坐标轴 + if (nodePath and hasattr(nodePath, 'getName') and + (nodePath.getName().startswith("gizmo") or + "gizmo" in nodePath.getName().lower() or + (hasattr(nodePath, 'hasTag') and nodePath.hasTag("is_gizmo")))): + is_gizmo_click = True + # 如果有选中的模型,使用选中的模型作为聚焦目标 + if self.selectedNode and not self.selectedNode.isEmpty(): + target_node = self.selectedNode + print(f"检测到坐标轴点击,使用目标节点: {target_node.getName() if target_node else 'None'}") + + # 检查是否为双击(同一节点且在时间阈值内) + is_double_click = (self._last_clicked_node == target_node and + target_node is not None and + current_time - self._last_click_time < self._double_click_threshold) + + if is_double_click: + # 双击 detected + node_name = target_node.getName() if target_node else "None" + print(f"检测到双击节点: {node_name}") + + # 无论是点击模型还是坐标轴,都执行聚焦 + if target_node and not target_node.isEmpty(): + print(f"双击聚焦到节点: {target_node.getName()}") + if self.selectedNode != target_node: + self.updateSelection(target_node) + else: + self.focusCameraOnSelectedNodeAdvanced() + else: + print("双击事件:没有有效的目标节点") + + # 重置状态以避免三击等误触发 + self._last_click_time = 0 + self._last_clicked_node = None + else: + # 单击,更新状态 + self._last_click_time = current_time + self._last_clicked_node = target_node + + # 如果点击的是坐标轴,保持当前选择不变 + if is_gizmo_click: + print("坐标轴单击,保持当前选择") + else: + # 正常的单击选择 + self.updateSelection(nodePath) + + except Exception as e: + print(f"处理鼠标点击事件失败: {e}") + + def _onDoubleClick(self, nodePath): + """双击事件处理""" + try: + # 获取实际要聚焦的目标节点 + target_node = nodePath + + # 如果是坐标轴,确保使用关联的模型作为目标 + if (nodePath and hasattr(nodePath, 'hasTag') and + nodePath.hasTag("is_gizmo")): + if self.selectedNode and not self.selectedNode.isEmpty(): + target_node = self.selectedNode + print(f"坐标轴双击,聚焦到关联模型: {target_node.getName()}") + else: + print("坐标轴双击,但没有关联的选中模型") + return + + if target_node and not target_node.isEmpty(): + print(f"双击聚焦到节点: {target_node.getName()}") + # 更新选择(如果需要) + if self.selectedNode != target_node: + self.updateSelection(target_node) + + # 执行聚焦 + self.focusCameraOnSelectedNodeAdvanced() + else: + print("双击事件:没有有效的目标节点") + + except Exception as e: + print(f"双击事件处理失败: {e}") + + # 添加一个更精确的双击检测方法 + + def checkDoubleClick(self, nodePath): + """检查是否为双击,返回布尔值""" + try: + import time + current_time = time.time() + + # 必须是同一节点且在时间阈值内 + is_double_click = (self._last_clicked_node == nodePath and + nodePath is not None and + current_time - self._last_click_time < self._double_click_threshold) + + if is_double_click: + # 重置状态 + self._last_click_time = 0 + self._last_clicked_node = None + return True + else: + # 更新状态 + self._last_click_time = current_time + self._last_clicked_node = nodePath + return False + + except Exception as e: + print(f"双击检测失败: {e}") + return False + + # 添加一个定时重置方法,用于清除长时间未完成的双击状态 + + def _resetDoubleClickState(self): + """重置双击状态""" + self._last_click_time = 0 + self._last_clicked_node = None + + # 添加一个任务来自动重置双击状态 + + def _startDoubleClickResetTask(self): + """启动双击状态重置任务""" + if self._double_click_task: + taskMgr.remove(self._double_click_task) + + self._double_click_task = taskMgr.doMethodLater( + self._double_click_threshold * 2, # 等待2倍阈值时间 + self._resetDoubleClickStateTask, + "resetDoubleClickState" + ) + + def _resetDoubleClickStateTask(self, task): + """任务:重置双击状态""" + self._resetDoubleClickState() + self._double_click_task = None + return task.done + + # 修改 updateSelection 方法以集成双击检测 + + # def updateSelection(self, nodePath): + # """更新选择状态""" + # print(f"\n=== 更新选择状态 ===") + # + # # 如果正在删除节点,避免更新选择 + # if hasattr(self, '_deleting_node') and self._deleting_node: + # print("正在删除节点,跳过选择更新") + # print("=== 选择状态更新完成 ===\n") + # return + # + # node_name = "None" + # if nodePath and not nodePath.isEmpty(): + # node_name = nodePath.getName() + # print(f"新选择的节点: {node_name}") + # + # # 检查是否为双击 + # is_double_click = self.checkDoubleClick(nodePath) + # if is_double_click: + # print(f"检测到双击 {node_name},执行聚焦") + # # 启动聚焦(在下一帧执行,确保选择状态已更新) + # taskMgr.doMethodLater(0.01, self._delayedFocusTask, "delayedFocus") + # + # self.selectedNode = nodePath + # # 添加兼容性属性 + # self.selectedObject = nodePath + # + # if nodePath and not nodePath.isEmpty(): + # node_name = nodePath.getName() + # print(f"开始为节点 {node_name} 创建选择框和坐标轴...") + # + # # 创建选择框 + # print("创建选择框...") + # self.createSelectionBox(nodePath) + # if self.selectionBox: + # box_name = "Unknown" + # if self.selectionBox and not self.selectionBox.isEmpty(): + # box_name = self.selectionBox.getName() + # print(f"✓ 选择框创建成功: {box_name}") + # else: + # print("× 选择框创建失败") + # + # # 创建坐标轴 + # print("创建坐标轴...") + # self.createGizmo(nodePath) + # if self.gizmo: + # gizmo_name = "Unknown" + # if self.gizmo and not self.gizmo.isEmpty(): + # gizmo_name = self.gizmo.getName() + # print(f"✓ 坐标轴创建成功: {gizmo_name}") + # else: + # print("× 坐标轴创建失败") + # + # print(f"✓ 选中了节点: {node_name}") + # else: + # print("清除选择...") + # self.clearSelectionBox() + # self.clearGizmo() + # print("✓ 取消选择") + # + # print("=== 选择状态更新完成 ===\n") + + def _delayedFocusTask(self, task): + """延迟执行聚焦任务""" + try: + self.focusCameraOnSelectedNodeAdvanced() + except Exception as e: + print(f"延迟聚焦任务失败: {e}") + return task.done + + # 添加一个更智能的双击检测方法,考虑鼠标位置 + + def checkDoubleClickWithPosition(self, nodePath, mouse_x=None, mouse_y=None): + """检查是否为双击,同时考虑鼠标位置""" + try: + import time + current_time = time.time() + + # 如果没有提供鼠标位置,直接使用基本双击检测 + if mouse_x is None or mouse_y is None: + return self.checkDoubleClick(nodePath) + + # 检查节点和时间 + time_diff = current_time - self._last_click_time + is_same_node = (self._last_clicked_node == nodePath) + + # 如果是同一节点且在时间阈值内,认为是双击 + if is_same_node and time_diff < self._double_click_threshold: + # 重置状态 + self._last_click_time = 0 + self._last_clicked_node = None + return True + else: + # 更新状态 + self._last_click_time = current_time + self._last_clicked_node = nodePath + return False + + except Exception as e: + print(f"带位置的双击检测失败: {e}") + return False + + # 添加一个公共方法来设置双击阈值 + + def setDoubleClickThreshold(self, threshold_seconds): + """设置双击时间阈值""" + if threshold_seconds > 0: + self._double_click_threshold = threshold_seconds + print(f"双击阈值已设置为: {threshold_seconds} 秒") + else: + print("无效的双击阈值") + + # 添加一个方法来手动触发双击聚焦(可用于测试或其他触发方式) + + def triggerDoubleClickFocus(self, nodePath=None): + """手动触发双击聚焦""" + try: + target_node = nodePath if nodePath else self.selectedNode + if target_node and not target_node.isEmpty(): + print(f"手动触发聚焦到节点: {target_node.getName()}") + if self.selectedNode != target_node: + self.updateSelection(target_node) + self.focusCameraOnSelectedNodeAdvanced() + return True + else: + print("没有有效的目标节点进行聚焦") + return False + except Exception as e: + print(f"手动触发聚焦失败: {e}") + return False + + def cleanup(self): + """清理选择系统资源""" + # 清理双击任务 + if self._double_click_task: + taskMgr.remove(self._double_click_task) + self._double_click_task = None + + # 清理其他资源 + self.clearSelectionBox() + self.clearGizmo() + def clearSelection(self): + """清除当前选择""" + try: + self.selectedNode = None + self.selectedObject = None + self.clearSelectionBox() + self.clearGizmo() + + # 清除树形控件中的选择 + if (hasattr(self.world, 'interface_manager') and + self.world.interface_manager and + hasattr(self.world.interface_manager, 'treeWidget') and + self.world.interface_manager.treeWidget): + self.world.interface_manager.treeWidget.setCurrentItem(None) + + print("已清除选择") + except Exception as e: + print(f"清除选择失败: {e}") + diff --git a/demo.py b/demo.py index ef858637..83da4fbd 100644 --- a/demo.py +++ b/demo.py @@ -122,6 +122,13 @@ class MyWorld(CoreWorld): self.accept("p",self.onPatrolKeyPressed) self.accept("P",self.onPatrolKeyPressed) + # 绑定鼠标事件用于3D场景选择 + self.accept("mouse1", self.onMouseClick) + self.accept("mouse1-up", self.onMouseRelease) + # 尝试多种鼠标移动事件绑定方式 + self.accept("mouse-move", self.onMouseMove) + self.accept("drag", self.onMouseMove) + # 初始化事件处理系统 self.event_handler = EventHandler(self) @@ -480,6 +487,103 @@ class MyWorld(CoreWorld): except Exception as e: print(f"处理 F 键事件失败: {e}") + def onMouseClick(self): + """处理鼠标点击事件""" + print("\n=== 鼠标点击事件触发 ===") + try: + # 检查鼠标是否有效 + if not self.mouseWatcherNode.hasMouse(): + print("❌ 鼠标无效或不在窗口内") + return + + print("✓ 鼠标位置有效") + + # 获取鼠标位置 + mouse_x = self.mouseWatcherNode.getMouseX() + mouse_y = self.mouseWatcherNode.getMouseY() + print(f"📍 鼠标标准化坐标: ({mouse_x:.3f}, {mouse_y:.3f})") + + # 转换为窗口坐标 + winWidth, winHeight = self.win.getSize() + window_x = (mouse_x + 1) * 0.5 * winWidth + window_y = (1 - mouse_y) * 0.5 * winHeight + print(f"📍 鼠标窗口坐标: ({window_x:.1f}, {window_y:.1f})") + print(f"📐 窗口尺寸: {winWidth} x {winHeight}") + + # 检查ImGui是否捕获了鼠标 + imgui_captured = self.processImGuiMouseClick(window_x, window_y) + print(f"🖱️ ImGui捕获状态: {imgui_captured}") + if imgui_captured: + print("❌ ImGui处理了该事件,跳过3D场景选择") + return + + # 调用事件处理器进行射线检测和选择 + if hasattr(self, 'event_handler'): + print("✓ 找到事件处理器,开始处理选择") + self.event_handler.mousePressEventLeft({ + 'x': window_x, + 'y': window_y + }) + else: + print("❌ 未找到事件处理器") + + except Exception as e: + print(f"❌ 处理鼠标点击事件失败: {e}") + import traceback + traceback.print_exc() + + def onMouseRelease(self): + """处理鼠标释放事件""" + try: + # 检查鼠标是否有效 + if not self.mouseWatcherNode.hasMouse(): + return + + # 获取鼠标位置 + mouse_x = self.mouseWatcherNode.getMouseX() + mouse_y = self.mouseWatcherNode.getMouseY() + + # 转换为窗口坐标 + winWidth, winHeight = self.win.getSize() + window_x = (mouse_x + 1) * 0.5 * winWidth + window_y = (1 - mouse_y) * 0.5 * winHeight + + # 调用事件处理器 + if hasattr(self, 'event_handler'): + self.event_handler.mouseReleaseEventLeft({ + 'x': window_x, + 'y': window_y + }) + + except Exception as e: + print(f"处理鼠标释放事件失败: {e}") + + def onMouseMove(self): + """处理鼠标移动事件""" + try: + # 检查鼠标是否有效 + if not self.mouseWatcherNode.hasMouse(): + return + + # 获取鼠标位置 + mouse_x = self.mouseWatcherNode.getMouseX() + mouse_y = self.mouseWatcherNode.getMouseY() + + # 转换为窗口坐标 + winWidth, winHeight = self.win.getSize() + window_x = (mouse_x + 1) * 0.5 * winWidth + window_y = (1 - mouse_y) * 0.5 * winHeight + + # 调用事件处理器 + if hasattr(self, 'event_handler'): + self.event_handler.mouseMoveEvent({ + 'x': window_x, + 'y': window_y + }) + + except Exception as e: + print(f"处理鼠标移动事件失败: {e}") + def onPatrolKeyPressed(self): """处理 P 键按下事件 - 控制巡检系统""" try: @@ -904,15 +1008,7 @@ class MyWorld(CoreWorld): imgui.separator() imgui.same_line() - # 其他工具按钮 - if imgui.button("导入"): - print("导入模型") - imgui.same_line() - if imgui.button("保存"): - print("保存场景") - imgui.same_line() - if imgui.button("播放"): - print("播放动画") + # 工具按钮已移除(导入、保存、播放) def _draw_scene_tree(self): """绘制场景树面板""" @@ -4944,6 +5040,32 @@ class MyWorld(CoreWorld): print(f"ImGui界面检测失败: {e}") return False + def processImGuiMouseClick(self, x, y): + """处理ImGui鼠标点击事件,返回是否消费了该事件""" + try: + # ImGui优先策略:如果ImGui想要捕获鼠标,则由ImGui处理 + if hasattr(imgui, 'get_io') and imgui.get_io().want_capture_mouse: + return True + + # 检查是否有任何ImGui窗口悬停 + try: + if imgui.is_any_window_hovered(): + return True + except AttributeError: + # 如果方法不存在,跳过这个检查 + pass + + # 检查鼠标是否在ImGui界面区域内 + if self._is_mouse_over_imgui(): + return True + + # 如果以上条件都不满足,则让3D场景处理该事件 + return False + + except Exception as e: + print(f"ImGui鼠标点击处理失败: {e}") + return False + # ==================== 消息系统 ==================== def add_message(self, text, color=(1.0, 1.0, 1.0, 1.0)):