""" 选择和变换系统模块 负责物体选择和变换相关功能: - 选择框的创建和更新 - 坐标轴(Gizmo)系统 - 拖拽变换逻辑 - 射线检测和碰撞检测 """ from PIL.ImageChops import lighter from panda3d.core import (Vec3, Point3, Point2, LineSegs, ColorAttrib, RenderState, DepthTestAttrib, CollisionTraverser, CollisionHandlerQueue, CollisionNode, CollisionRay, GeomNode, BitMask32,Material,LColor) from direct.task.TaskManagerGlobal import taskMgr 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.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), # 红色 "y": (0, 1, 0, 1), # 绿色 "z": (0, 0, 1, 1) # 蓝色 } self.gizmo_highlight_colors = { "x": (1, 1, 0, 1), # 黄色高亮 "y": (1, 1, 0, 1), # 黄色高亮 "z": (1, 1, 0, 1) # 黄色高亮 } print("✓ 选择和变换系统初始化完成") # ==================== 选择框系统 ==================== def createSelectionBox(self, nodePath): """为选中的节点创建选择框""" try: print(f" 开始创建选择框,目标节点: {nodePath.getName()}") # 如果已有选择框,先移除 if self.selectionBox: print(" 移除现有选择框") self.selectionBox.removeNode() self.selectionBox = None if not nodePath: print(" 目标节点为空,取消创建") return # 创建选择框作为render的子节点,但会实时跟踪目标节点 self.selectionBox = self.world.render.attachNewNode("selectionBox") self.selectionBoxTarget = nodePath # 保存目标节点引用 print(f" 选择框节点创建完成: {self.selectionBox}") # 启动选择框更新任务 taskMgr.add(self.updateSelectionBoxTask, "updateSelectionBox") print(" 选择框更新任务已启动") # 初始更新选择框 print(" 开始初始化选择框几何体...") self.updateSelectionBoxGeometry() print(f" ✓ 为节点 {nodePath.getName()} 创建了选择框") 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 # 清除现有的几何体 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: if not self.selectionBox or not self.selectionBoxTarget: return task.done # 结束任务 # 检查目标节点是否还存在 if self.selectionBoxTarget.isEmpty(): self.clearSelectionBox() return task.done # 获取目标节点在世界坐标系中的当前边界框(使用正确的API) currentMinPoint = Point3() currentMaxPoint = Point3() if not self.selectionBoxTarget.calcTightBounds(currentMinPoint, currentMaxPoint, self.world.render): return task.cont # 检查边界框是否发生变化(位置或大小) if (not hasattr(self, '_lastMinPoint') or not hasattr(self, '_lastMaxPoint') or self._lastMinPoint != currentMinPoint or self._lastMaxPoint != currentMaxPoint): # 更新选择框几何体 self.updateSelectionBoxGeometry() # 保存当前边界框信息 self._lastMinPoint = currentMinPoint self._lastMaxPoint = currentMaxPoint return task.cont # 继续任务 except Exception as e: print(f"选择框更新任务出错: {str(e)}") return task.done 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: print(" 移除现有坐标轴") self.gizmo.removeNode() self.gizmo = None if not nodePath: print(" 目标节点为空,取消创建") return # 创建坐标轴主节点 self.gizmo = self.world.render.attachNewNode("gizmo") self.gizmoTarget = nodePath print(f" 坐标轴主节点创建完成: {self.gizmo}") # 获取目标节点在世界坐标系中的边界框(使用正确的API) 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) print(f" 坐标轴位置设置为实体中心: {center}") else: print(" 目标节点边界框为空,使用默认位置") # 【关键修复】:设置坐标轴的朝向以反映父节点的旋转 parent_node = nodePath.getParent() if parent_node and parent_node != self.world.render: # 子节点:坐标轴应该和父节点保持相同的朝向 parent_hpr = parent_node.getHpr() self.gizmo.setHpr(parent_hpr) print(f" 子节点坐标轴 - 设置朝向与父节点一致: {parent_hpr}") else: # 顶级模型:使用世界坐标系朝向 self.gizmo.setHpr(0, 0, 0) print(f" 顶级模型坐标轴 - 使用世界坐标系朝向") # 创建坐标轴的几何体 print(" 开始创建坐标轴几何体...") self.createGizmoGeometry() # 启动坐标轴更新任务 taskMgr.add(self.updateGizmoTask, "updateGizmo") print(" 坐标轴更新任务已启动") print(f" ✓ 为节点 {nodePath.getName()} 创建了坐标轴") except Exception as e: print(f"创建坐标轴失败: {str(e)}") import traceback traceback.print_exc() def createGizmoGeometry(self): """创建坐标轴的几何体""" try: if not self.gizmo: return # 创建X轴(红色) x_lines = LineSegs("x_axis") x_lines.setThickness(6.0) x_lines.moveTo(0, 0, 0) x_lines.drawTo(self.axis_length, 0, 0) # 创建X轴箭头 x_lines.moveTo(self.axis_length - 0.5, -0.2, 0) x_lines.drawTo(self.axis_length, 0, 0) x_lines.drawTo(self.axis_length - 0.5, 0.2, 0) x_geom = x_lines.create() self.gizmoXAxis = self.gizmo.attachNewNode(x_geom) self.gizmoXAxis.setName("gizmo_x_axis") self.gizmoXAxis.setLightOff() # 创建Y轴(绿色) y_lines = LineSegs("y_axis") y_lines.setThickness(6.0) y_lines.moveTo(0, 0, 0) y_lines.drawTo(0, self.axis_length, 0) # 创建Y轴箭头 y_lines.moveTo(-0.2, self.axis_length - 0.5, 0) y_lines.drawTo(0, self.axis_length, 0) y_lines.drawTo(0.2, self.axis_length - 0.5, 0) y_geom = y_lines.create() self.gizmoYAxis = self.gizmo.attachNewNode(y_geom) self.gizmoYAxis.setName("gizmo_y_axis") self.gizmoYAxis.setLightOff() # 创建Z轴(蓝色) z_lines = LineSegs("z_axis") z_lines.setThickness(6.0) z_lines.moveTo(0, 0, 0) z_lines.drawTo(0, 0, self.axis_length) # 创建Z轴箭头 z_lines.moveTo(-0.2, 0, self.axis_length - 0.5) z_lines.drawTo(0, 0, self.axis_length) z_lines.drawTo(0.2, 0, self.axis_length - 0.5) z_geom = z_lines.create() self.gizmoZAxis = self.gizmo.attachNewNode(z_geom) self.gizmoZAxis.setName("gizmo_z_axis") self.gizmoZAxis.setLightOff() # 确保坐标轴不被光照影响 self.gizmo.setLightOff() # 使用最强的渲染设置,确保坐标轴绝对不会被遮挡 self.gizmo.setBin("gui-popup", 0) # 使用最高的GUI渲染层 self.gizmo.setDepthTest(False) # 完全禁用深度测试 self.gizmo.setDepthWrite(False) # 禁用深度写入 self.gizmo.setTwoSided(True) # 双面渲染 # 创建强制前景渲染状态 from panda3d.core import RenderModeAttrib, TransparencyAttrib foreground_state = RenderState.make( DepthTestAttrib.make(DepthTestAttrib.MNone), # 完全不进行深度测试 TransparencyAttrib.make(TransparencyAttrib.MAlpha) # 启用透明度混合 ) self.gizmo.setState(foreground_state) # 对每个坐标轴设置独立的最高渲染优先级 self.gizmoXAxis.setBin("gui-popup", 10) self.gizmoXAxis.setDepthTest(False) self.gizmoXAxis.setDepthWrite(False) self.gizmoXAxis.setLightOff() self.gizmoXAxis.setState(foreground_state) self.gizmoYAxis.setBin("gui-popup", 20) self.gizmoYAxis.setDepthTest(False) self.gizmoYAxis.setDepthWrite(False) self.gizmoYAxis.setLightOff() self.gizmoYAxis.setState(foreground_state) self.gizmoZAxis.setBin("gui-popup", 30) self.gizmoZAxis.setDepthTest(False) self.gizmoZAxis.setDepthWrite(False) self.gizmoZAxis.setLightOff() self.gizmoZAxis.setState(foreground_state) # 强制设置各轴的渲染状态,确保颜色可以变化 # 创建包含颜色和前景渲染的组合状态 red_state = RenderState.make( ColorAttrib.makeFlat((1, 0, 0, 1)), DepthTestAttrib.make(DepthTestAttrib.MNone), TransparencyAttrib.make(TransparencyAttrib.MAlpha) ) green_state = RenderState.make( ColorAttrib.makeFlat((0, 1, 0, 1)), DepthTestAttrib.make(DepthTestAttrib.MNone), TransparencyAttrib.make(TransparencyAttrib.MAlpha) ) blue_state = RenderState.make( ColorAttrib.makeFlat((0, 0, 1, 1)), DepthTestAttrib.make(DepthTestAttrib.MNone), TransparencyAttrib.make(TransparencyAttrib.MAlpha) ) self.gizmoXAxis.setState(red_state) self.gizmoYAxis.setState(green_state) self.gizmoZAxis.setState(blue_state) # 初始化高亮状态 self.gizmoHighlightAxis = None # 立即设置初始颜色,确保创建时就有正确的颜色 self.setGizmoAxisColor("x", self.gizmo_colors["x"]) self.setGizmoAxisColor("y", self.gizmo_colors["y"]) self.setGizmoAxisColor("z", self.gizmo_colors["z"]) print(f"✓ 坐标轴几何体创建完成,长度={self.axis_length}") except Exception as e: print(f"创建坐标轴几何体失败: {str(e)}") def updateGizmoTask(self, task): """坐标轴更新任务""" try: if not self.gizmo or not self.gizmoTarget: return task.done # 检查目标节点是否还存在 if self.gizmoTarget.isEmpty(): self.clearGizmo() return task.done 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: # 更新坐标轴位置,始终在目标节点中心 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) # 【关键修复】:更新坐标轴朝向以跟踪父节点的变化 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) return task.cont except Exception as e: print(f"坐标轴更新任务出错: {str(e)}") return task.done 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 print("清除了坐标轴") def setGizmoAxisColor(self, axis, color): """设置坐标轴颜色 - 使用前景渲染状态确保不被遮挡""" try: # 创建包含颜色和前景渲染的组合状态 from panda3d.core import TransparencyAttrib color_state = RenderState.make( ColorAttrib.makeFlat(color), DepthTestAttrib.make(DepthTestAttrib.MNone), TransparencyAttrib.make(TransparencyAttrib.MAlpha) ) if axis == "x" and self.gizmoXAxis: self.gizmoXAxis.setState(color_state) self.gizmoXAxis.setColor(*color) self.gizmoXAxis.setColorScale(*color) elif axis == "y" and self.gizmoYAxis: self.gizmoYAxis.setState(color_state) self.gizmoYAxis.setColor(*color) self.gizmoYAxis.setColorScale(*color) elif axis == "z" and self.gizmoZAxis: self.gizmoZAxis.setState(color_state) self.gizmoZAxis.setColor(*color) self.gizmoZAxis.setColorScale(*color) except Exception as e: print(f"设置坐标轴颜色失败: {str(e)}") # ==================== 射线检测和碰撞检测 ==================== def checkGizmoClick(self, mouseX, mouseY): """使用屏幕空间检测是否点击了坐标轴""" if not self.gizmo or not self.gizmoTarget: print("坐标轴点击检测:坐标轴或目标不存在") return None # 基本参数验证 if not isinstance(mouseX, (int, float)) or not isinstance(mouseY, (int, float)): print(f"坐标轴点击检测:无效的鼠标坐标 ({mouseX}, {mouseY})") return None try: print(f"\n=== 坐标轴点击检测 ===") print(f"鼠标位置: ({mouseX}, {mouseY})") # 获取坐标轴中心的世界坐标 gizmo_world_pos = self.gizmo.getPos(self.world.render) print(f"坐标轴世界位置: {gizmo_world_pos}") # 计算各轴端点的世界坐标 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: print("使用备用检测方法...") return self.checkGizmoClickFallback(mouseX, mouseY) # 计算点击阈值 click_threshold = 30 # 增大检测范围 # 检测各个轴,对于端点在屏幕外的轴提供回退方案 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 print("× 没有点击任何轴") return None except Exception as e: print(f"坐标轴点击检测失败: {str(e)}") import traceback traceback.print_exc() return None def checkGizmoClickFallback(self, mouseX, mouseY): """备用检测方法:使用固定的屏幕区域""" print("使用备用点击检测...") # 获取准确的窗口尺寸 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 updateGizmoHighlight(self, mouseX, mouseY): """更新坐标轴高亮状态""" if not self.gizmo or self.isDraggingGizmo: return # 检测鼠标悬停的轴(使用相同的检测逻辑但不输出调试信息) hoveredAxis = 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) # 将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) # 只要坐标轴中心在屏幕内,就进行检测 if gizmo_screen: click_threshold = 25 def isNearLine(mousePos, start, end, threshold): import math A = mousePos[1] - start[1] B = start[0] - mousePos[0] C = (end[1] - start[1]) * mousePos[0] + (start[0] - end[0]) * mousePos[1] + end[0] * start[1] - start[0] * end[1] length = math.sqrt((end[0] - start[0])**2 + (end[1] - start[1])**2) if length == 0: return False distance = abs(C) / length t = ((mousePos[0] - start[0]) * (end[0] - start[0]) + (mousePos[1] - start[1]) * (end[1] - start[1])) / (length * length) return distance < threshold and 0 <= t <= 1 mouse_pos = (mouseX, mouseY) # 分别检测每个轴,为在屏幕外的轴端点提供替代方案 # 按优先级检测轴(Z > X > Y) # 对于轴端点在屏幕外的情况,使用较短的轴段进行检测 def getAxisScreenPoint(axis_name, axis_screen_end): if axis_screen_end: return axis_screen_end # 如果端点在屏幕外,使用轴长度的一半作为检测点 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) return worldToScreen(half_end) # 获取有效的检测点(优先使用完整轴,备用使用半轴) z_detect_point = getAxisScreenPoint("z", z_screen) x_detect_point = getAxisScreenPoint("x", x_screen) y_detect_point = getAxisScreenPoint("y", y_screen) if z_detect_point and isNearLine(mouse_pos, gizmo_screen, z_detect_point, click_threshold): hoveredAxis = "z" elif x_detect_point and isNearLine(mouse_pos, gizmo_screen, x_detect_point, click_threshold): hoveredAxis = "x" elif y_detect_point and isNearLine(mouse_pos, gizmo_screen, y_detect_point, click_threshold): hoveredAxis = "y" except Exception as e: pass # 静默处理错误,避免频繁输出 # 如果高亮状态发生变化 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.gizmoHighlightAxis = hoveredAxis # ==================== 拖拽变换 ==================== def startGizmoDrag(self, axis, mouseX, mouseY): """开始坐标轴拖拽""" try: # 确保状态正确初始化 if not self.gizmoTarget: print("开始拖拽失败: 没有拖拽目标") return if not self.gizmo: print("开始拖拽失败: 没有坐标轴") return self.isDraggingGizmo = True self.dragGizmoAxis = axis self.dragStartMousePos = (mouseX, mouseY) # 保存开始拖拽时目标节点的位置和坐标轴的位置 self.gizmoTargetStartPos = self.gizmoTarget.getPos() self.gizmoStartPos = self.gizmo.getPos(self.world.render) # 坐标轴的世界位置 print(f"开始拖拽 {axis} 轴 - 目标起始位置: {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 # 计算鼠标移动距离(屏幕像素) mouseDeltaX = mouseX - self.dragStartMousePos[0] mouseDeltaY = mouseY - self.dragStartMousePos[1] # 使用坐标轴的实际位置而不是目标节点位置来计算屏幕投影 gizmo_world_pos = self.gizmoStartPos # 【关键修复】:获取正确的轴向量,考虑父节点的旋转 # 检查目标节点是否有父节点 parent_node = self.gizmoTarget.getParent() # 确定轴向量的变换上下文 if parent_node and parent_node != self.world.render: # 子节点:使用父节点的局部坐标系 print(f"子节点拖拽 - 父节点: {parent_node.getName()}, 父节点旋转: {parent_node.getHpr()}") transform_context = parent_node else: # 顶级模型:使用世界坐标系 print(f"顶级模型拖拽 - 使用世界坐标系") transform_context = self.world.render # 计算轴向量在正确坐标系中的方向 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 # 将局部轴向量转换到世界坐标系(用于屏幕投影) if transform_context != self.world.render: # 获取变换矩阵并应用到轴向量上 transform_mat = transform_context.getMat(self.world.render) # 只旋转向量,不平移 world_axis_vector = transform_mat.xformVec(local_axis_vector) world_axis_vector.normalize() # 归一化 print(f"转换后的轴向量: {local_axis_vector} -> {world_axis_vector}") else: # 顶级节点,直接使用世界轴向量 world_axis_vector = local_axis_vector print(f"世界轴向量: {world_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 gizmo_screen = worldToScreen(gizmo_world_pos) axis_screen = worldToScreen(axis_end) if not gizmo_screen: print("拖拽更新失败: 坐标轴中心不在屏幕内") return if not axis_screen: print("拖拽更新失败: 坐标轴端点不在屏幕内") return # 计算轴在屏幕空间的方向向量 screen_axis_dir = ( axis_screen[0] - gizmo_screen[0], axis_screen[1] - gizmo_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) else: print("拖拽更新失败: 屏幕轴方向长度为0") return # 将鼠标移动投影到轴方向上 projected_distance = (mouseDeltaX * screen_axis_dir[0] + mouseDeltaY * screen_axis_dir[1]) # 计算动态比例因子,基于相机距离和视野角度 cam_pos = self.world.cam.getPos() distance_to_object = (cam_pos - gizmo_world_pos).length() # 获取相机的视野角度 fov = self.world.cam.node().getLens().getFov()[0] # 水平视野角度 fov_radians = math.radians(fov) # 获取窗口尺寸 winWidth, winHeight = self.world.getWindowSize() # 计算一个像素在世界坐标系中的大小(在目标物体的距离处) # 使用透视投影公式:world_size = screen_size * distance * tan(fov/2) / (screen_width/2) pixel_to_world_ratio = distance_to_object * math.tan(fov_radians / 2) / (winWidth / 2) # 使用动态比例因子 scale_factor = pixel_to_world_ratio * 0.5 # 0.5是调整因子,可以根据需要调整 # 【关键修复】:在正确的坐标系中计算移动向量 # 计算移动距离(标量) movement_distance = projected_distance * scale_factor # 在正确的坐标系中计算移动向量 if transform_context != self.world.render: # 子节点:在父节点的局部坐标系中移动 if self.dragGizmoAxis == "x": movement_local = Vec3(movement_distance, 0, 0) elif self.dragGizmoAxis == "y": movement_local = Vec3(0, movement_distance, 0) elif self.dragGizmoAxis == "z": movement_local = Vec3(0, 0, movement_distance) # 将局部移动向量转换到父节点的坐标系中 # 由于我们要应用到目标节点上,而目标节点相对于父节点,我们直接使用局部移动 movement = movement_local print(f"子节点移动向量(局部): {movement}") else: # 顶级模型:在世界坐标系中移动 if self.dragGizmoAxis == "x": movement = Vec3(movement_distance, 0, 0) elif self.dragGizmoAxis == "y": movement = Vec3(0, movement_distance, 0) elif self.dragGizmoAxis == "z": movement = Vec3(0, 0, movement_distance) print(f"顶级模型移动向量(世界): {movement}") # 应用移动到目标节点 newPos = self.gizmoTargetStartPos + movement self.gizmoTarget.setPos(newPos) # 每次拖拽都输出调试信息(但限制频率) 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}, 距离:{distance_to_object:.2f}, 比例:{scale_factor:.6f}, 投影:{projected_distance:.2f}") self._last_drag_debug_time = current_time newPos = self.gizmoTargetStartPos + movement light_object = self.gizmoTarget.getPythonTag("rp_light_object") if light_object: light_object.pos = newPos self.gizmoTarget.setPos(newPos) else: self.gizmoTarget.setPos(newPos) self.gizmo.setPos(newPos) except Exception as e: print(f"更新坐标轴拖拽失败: {str(e)}") import traceback traceback.print_exc() def stopGizmoDrag(self): """停止坐标轴拖拽""" print(f"停止坐标轴拖拽 - 轴: {self.dragGizmoAxis}") self.isDraggingGizmo = False self.dragGizmoAxis = None self.dragStartMousePos = None # 清理拖拽状态,下次拖拽开始时重新设置 self.gizmoTargetStartPos = None self.gizmoStartPos = None # ==================== 选择管理 ==================== def updateSelection(self, nodePath): """更新选择状态""" print(f"\n=== 更新选择状态 ===") print(f"新选择的节点: {nodePath.getName() if nodePath else 'None'}") self.selectedNode = nodePath # 添加兼容性属性 self.selectedObject = nodePath if nodePath: print(f"开始为节点 {nodePath.getName()} 创建选择框和坐标轴...") # 创建选择框 print("创建选择框...") self.createSelectionBox(nodePath) if self.selectionBox: print(f"✓ 选择框创建成功: {self.selectionBox.getName()}") else: print("× 选择框创建失败") # 创建坐标轴 print("创建坐标轴...") self.createGizmo(nodePath) if self.gizmo: print(f"✓ 坐标轴创建成功: {self.gizmo.getName()}") else: print("× 坐标轴创建失败") print(f"✓ 选中了节点: {nodePath.getName()}") else: print("清除选择...") self.clearSelectionBox() self.clearGizmo() print("✓ 取消选择") print("=== 选择状态更新完成 ===\n") def getSelectedNode(self): """获取当前选中的节点""" return self.selectedNode def hasSelection(self): """检查是否有选中的节点""" return self.selectedNode is not None