EG/core/selection.py
2025-07-02 09:49:59 +08:00

784 lines
30 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

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

"""
选择和变换系统模块
负责物体选择和变换相关功能:
- 选择框的创建和更新
- 坐标轴(Gizmo)系统
- 拖拽变换逻辑
- 射线检测和碰撞检测
"""
from panda3d.core import (Vec3, Point3, Point2, LineSegs, ColorAttrib, RenderState,
DepthTestAttrib, CollisionTraverser, CollisionHandlerQueue,
CollisionNode, CollisionRay, GeomNode, BitMask32)
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 = 3.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:
# 如果已有选择框,先移除
if self.selectionBox:
self.selectionBox.removeNode()
self.selectionBox = None
if not nodePath:
return
# 创建选择框作为render的子节点但会实时跟踪目标节点
self.selectionBox = self.world.render.attachNewNode("selectionBox")
self.selectionBoxTarget = nodePath # 保存目标节点引用
# 启动选择框更新任务
taskMgr.add(self.updateSelectionBoxTask, "updateSelectionBox")
# 初始更新选择框
self.updateSelectionBoxGeometry()
print(f"为节点 {nodePath.getName()} 创建了选择框")
except Exception as e:
print(f"创建选择框失败: {str(e)}")
def updateSelectionBoxGeometry(self):
"""更新选择框的几何形状和位置"""
try:
if not self.selectionBox or not self.selectionBoxTarget:
return
# 清除现有的几何体
self.selectionBox.removeNode()
self.selectionBox = self.world.render.attachNewNode("selectionBox")
# 获取目标节点的世界边界框
bounds = self.selectionBoxTarget.getBounds()
if not bounds or bounds.isEmpty():
return
# 获取边界框的最小和最大点(世界坐标)
minPoint = bounds.getMin()
maxPoint = bounds.getMax()
# 创建线段对象
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)}")
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
# 获取目标节点的当前边界框
bounds = self.selectionBoxTarget.getBounds()
if not bounds or bounds.isEmpty():
return task.cont
# 获取当前边界框信息
currentMinPoint = bounds.getMin()
currentMaxPoint = bounds.getMax()
# 检查边界框是否发生变化(位置或大小)
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:
# 如果已有坐标轴,先移除
if self.gizmo:
self.gizmo.removeNode()
self.gizmo = None
if not nodePath:
return
# 创建坐标轴主节点
self.gizmo = self.world.render.attachNewNode("gizmo")
self.gizmoTarget = nodePath
# 获取目标节点的边界框
bounds = nodePath.getBounds()
if bounds and not bounds.isEmpty():
center = bounds.getCenter()
maxPoint = bounds.getMax()
# 将坐标轴放在实体的上方
gizmo_pos = Point3(center.x, center.y, maxPoint.z + 2.0)
self.gizmo.setPos(gizmo_pos)
# 创建坐标轴的几何体
self.createGizmoGeometry()
# 启动坐标轴更新任务
taskMgr.add(self.updateGizmoTask, "updateGizmo")
print(f"为节点 {nodePath.getName()} 创建了坐标轴")
except Exception as e:
print(f"创建坐标轴失败: {str(e)}")
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("fixed", 100) # 提高优先级
self.gizmo.setDepthTest(True) # 启用深度测试,但设置高优先级
self.gizmo.setDepthWrite(False) # 不写入深度缓冲
# 确保坐标轴总是可见
state = RenderState.make(
DepthTestAttrib.make(DepthTestAttrib.MAlways), # 总是通过深度测试
)
self.gizmo.setState(state)
# 强制设置各轴的渲染状态,确保颜色可以变化
red_state = RenderState.make(ColorAttrib.makeFlat((1, 0, 0, 1)))
green_state = RenderState.make(ColorAttrib.makeFlat((0, 1, 0, 1)))
blue_state = RenderState.make(ColorAttrib.makeFlat((0, 0, 1, 1)))
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
# 更新坐标轴位置,始终在目标节点上方
bounds = self.gizmoTarget.getBounds()
if bounds and not bounds.isEmpty():
center = bounds.getCenter()
maxPoint = bounds.getMax()
gizmo_pos = Point3(center.x, center.y, maxPoint.z + 2.0)
self.gizmo.setPos(gizmo_pos)
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
print("清除了坐标轴")
def setGizmoAxisColor(self, axis, color):
"""设置坐标轴颜色 - 使用RenderState强制覆盖"""
try:
# 创建强制颜色状态
color_state = RenderState.make(ColorAttrib.makeFlat(color))
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:
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 # 增大检测范围
# 检测各个轴
axes_data = [
("x", x_screen, "X轴"),
("y", y_screen, "Y轴"),
("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 all([gizmo_screen, x_screen, y_screen, z_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)
# 按优先级检测轴
if isNearLine(mouse_pos, gizmo_screen, z_screen, click_threshold):
hoveredAxis = "z"
elif isNearLine(mouse_pos, gizmo_screen, x_screen, click_threshold):
hoveredAxis = "x"
elif isNearLine(mouse_pos, gizmo_screen, y_screen, 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:
self.isDraggingGizmo = True
self.dragGizmoAxis = axis
self.dragStartMousePos = (mouseX, mouseY)
# 保存开始拖拽时目标节点的位置
if self.gizmoTarget:
self.gizmoTargetStartPos = self.gizmoTarget.getPos()
print(f"开始拖拽 {axis}")
except Exception as e:
print(f"开始坐标轴拖拽失败: {str(e)}")
def updateGizmoDrag(self, mouseX, mouseY):
"""更新坐标轴拖拽 - 使用屏幕空间投影"""
try:
if not self.isDraggingGizmo or not self.gizmoTarget or not hasattr(self, 'dragStartMousePos'):
return
# 计算鼠标移动距离(屏幕像素)
mouseDeltaX = mouseX - self.dragStartMousePos[0]
mouseDeltaY = mouseY - self.dragStartMousePos[1]
# 获取坐标轴在屏幕空间的方向向量
gizmo_world_pos = self.gizmoTargetStartPos
if self.dragGizmoAxis == "x":
axis_end = gizmo_world_pos + Vec3(1, 0, 0)
elif self.dragGizmoAxis == "y":
axis_end = gizmo_world_pos + Vec3(0, 1, 0)
elif self.dragGizmoAxis == "z":
axis_end = gizmo_world_pos + Vec3(0, 0, 1)
else:
return
# 投影到屏幕空间
def worldToScreen(worldPos):
# 先转换为相机坐标系
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
gizmo_screen = worldToScreen(gizmo_world_pos)
axis_screen = worldToScreen(axis_end)
if not gizmo_screen or not axis_screen:
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:
return
# 将鼠标移动投影到轴方向上
projected_distance = (mouseDeltaX * screen_axis_dir[0] +
mouseDeltaY * screen_axis_dir[1])
# 转换投影距离为世界坐标移动距离
# 这个比例因子需要根据相机距离和视野角度调整
scale_factor = 0.01 # 可以调整这个值来改变拖拽灵敏度
if self.dragGizmoAxis == "x":
movement = Vec3(projected_distance * scale_factor, 0, 0)
elif self.dragGizmoAxis == "y":
movement = Vec3(0, projected_distance * scale_factor, 0)
elif self.dragGizmoAxis == "z":
movement = Vec3(0, 0, projected_distance * scale_factor)
# 应用移动到目标节点
newPos = self.gizmoTargetStartPos + movement
self.gizmoTarget.setPos(newPos)
except Exception as e:
print(f"更新坐标轴拖拽失败: {str(e)}")
def stopGizmoDrag(self):
"""停止坐标轴拖拽"""
self.isDraggingGizmo = False
self.dragGizmoAxis = None
self.dragStartMousePos = None
self.gizmoTargetStartPos = None
print("停止坐标轴拖拽")
# ==================== 选择管理 ====================
def updateSelection(self, nodePath):
"""更新选择状态"""
self.selectedNode = nodePath
if nodePath:
self.createSelectionBox(nodePath)
# 自动显示坐标轴(无需移动工具)
self.createGizmo(nodePath)
print(f"选中了节点: {nodePath.getName()}")
else:
self.clearSelectionBox()
self.clearGizmo()
print("取消选择")
def getSelectedNode(self):
"""获取当前选中的节点"""
return self.selectedNode
def hasSelection(self):
"""检查是否有选中的节点"""
return self.selectedNode is not None