784 lines
30 KiB
Python
784 lines
30 KiB
Python
"""
|
||
选择和变换系统模块
|
||
|
||
负责物体选择和变换相关功能:
|
||
- 选择框的创建和更新
|
||
- 坐标轴(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 |