forked from Rowland/EG
1396 lines
53 KiB
Python
1396 lines
53 KiB
Python
"""
|
||
选择和变换系统模块
|
||
|
||
负责物体选择和变换相关功能:
|
||
- 选择框的创建和更新
|
||
- 坐标轴(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, DepthWriteAttrib,
|
||
TransparencyAttrib, Vec4)
|
||
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.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, 0), # 红色
|
||
"y": (0, 1, 0, 0), # 绿色
|
||
"z": (0, 0, 1, 0) # 蓝色
|
||
}
|
||
self.gizmo_highlight_colors = {
|
||
"x": (1, 1, 0, 0), # 黄色高亮
|
||
"y": (1, 1, 0, 0), # 黄色高亮
|
||
"z": (1, 1, 0, 0) # 黄色高亮
|
||
}
|
||
|
||
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
|
||
|
||
# 检查是否需要重新计算边界框
|
||
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:
|
||
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 < 0.1:
|
||
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
|
||
|
||
# 获取目标节点在世界坐标系中的当前边界框(使用正确的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:
|
||
self.gizmo.removeNode()
|
||
self.gizmo = None
|
||
taskMgr.remove("updateGizmo")
|
||
|
||
if not nodePath:
|
||
return
|
||
|
||
# 创建坐标轴主节点
|
||
self.gizmo = self.world.render.attachNewNode("gizmo")
|
||
self.gizmoTarget = nodePath
|
||
|
||
# 设置位置和朝向
|
||
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.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
|
||
|
||
model_paths = [
|
||
"core/TranslateArrowHandle.fbx",
|
||
"EG/core/TranslateArrowHandle.fbx",
|
||
]
|
||
arrow_model = None
|
||
for path in model_paths:
|
||
try:
|
||
arrow_model = self.world.loader.loadModel(path)
|
||
if arrow_model:
|
||
print(f"成功加载模型: {path}")
|
||
break
|
||
except:
|
||
continue
|
||
self.gizmoXAxis = self.gizmo.attachNewNode("gizmo_x_axis")
|
||
x_arrow = arrow_model.copyTo(self.gizmoXAxis)
|
||
x_arrow.setName("x_arrow")
|
||
x_arrow.setHpr(0,-90,0)
|
||
x_arrow.setScale(0.1,0.05,0.05)
|
||
x_arrow.setPos(0,0,0)
|
||
|
||
self.gizmoYAxis = self.gizmo.attachNewNode("gizmo_y_axis")
|
||
y_arrow = arrow_model.copyTo(self.gizmoYAxis)
|
||
y_arrow.setName("y_arrow")
|
||
y_arrow.setHpr(90,0,0)
|
||
y_arrow.setScale(0.1,0.05,0.05)
|
||
y_arrow.setPos(0,0,0)
|
||
|
||
# 创建Z轴(蓝色)
|
||
self.gizmoZAxis = self.gizmo.attachNewNode("gizmo_z_axis")
|
||
z_arrow = arrow_model.copyTo(self.gizmoZAxis)
|
||
z_arrow.setName("z_arrow")
|
||
# 旋转箭头使其指向Z轴正方向
|
||
z_arrow.setHpr(0, 0, -90) # 根据需要调整旋转
|
||
z_arrow.setScale(0.1,0.05,0.05)
|
||
z_arrow.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]
|
||
|
||
for axis_node in axis_nodes:
|
||
if axis_node:
|
||
#禁用光照和阴影
|
||
axis_node.setLightOff()
|
||
axis_node.setShaderOff()
|
||
axis_node.setFogOff()
|
||
#设置渲染层级,确保大多数对象之前渲染
|
||
axis_node.setBin("fixed",30)
|
||
axis_node.setDepthWrite(False)
|
||
axis_node.setDepthTest(False)
|
||
arrow_nodes = []
|
||
if self.gizmoXAxis:
|
||
x_arrow = self.gizmoXAxis.find("x_arrow")
|
||
if x_arrow:
|
||
arrow_nodes.append(x_arrow)
|
||
if self.gizmoYAxis:
|
||
y_arrow = self.gizmoYAxis.find("y_arrow")
|
||
if y_arrow:
|
||
arrow_nodes.append(y_arrow)
|
||
if self.gizmoZAxis:
|
||
z_arrow = self.gizmoZAxis.find("z_arrow")
|
||
if z_arrow:
|
||
arrow_nodes.append(z_arrow)
|
||
|
||
for arrow_node in arrow_nodes:
|
||
if arrow_node:
|
||
arrow_node.setLightOff()
|
||
arrow_node.setShaderOff()
|
||
arrow_node.setFogOff()
|
||
arrow_node.setBin("fixed",31)
|
||
arrow_node.setDepthWrite(False)
|
||
arrow_node.setDepthTest(False)
|
||
#启用透明度S
|
||
arrow_node.setTransparency(TransparencyAttrib.MAlpha)
|
||
if self.gizmo:
|
||
self.gizmo.setLightOff()
|
||
self.gizmo.setShaderOff()
|
||
self.gizmo.setFogOff()
|
||
self.gizmo.setBin("fixed",29)
|
||
self.gizmo.setDepthWrite(False)
|
||
self.gizmo.setDepthTest(False)
|
||
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.05: # 每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
|
||
|
||
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()
|
||
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
|
||
|
||
# 更新朝向
|
||
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.08
|
||
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
|
||
|
||
|
||
# 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 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]
|
||
|
||
# 查找箭头模型节点
|
||
arrow_node = None
|
||
if axis == "x":
|
||
arrow_node = axis_node.find("x_arrow")
|
||
elif axis == "y":
|
||
arrow_node = axis_node.find("y_arrow")
|
||
elif axis == "z":
|
||
arrow_node = axis_node.find("z_arrow")
|
||
|
||
if not arrow_node:
|
||
print(f"未找到{axis}轴的箭头模型")
|
||
return
|
||
|
||
# 创建或获取材质
|
||
mat = Material()
|
||
|
||
# 设置材质属性 - 使用自发光确保在RenderPipeline下可见
|
||
mat.setBaseColor(Vec4(color[0], color[1], color[2], color[3]))
|
||
mat.setDiffuse(Vec4(0, 0, 0, 1))
|
||
#mat.setEmission(Vec4(color[0], color[1], color[2], 1.0)) # 自发光
|
||
mat.setEmission(Vec4(1,1,1,1.0)) # 自发光
|
||
mat.set_roughness(1)
|
||
|
||
# 应用材质
|
||
arrow_node.setMaterial(mat, 1)
|
||
|
||
|
||
# 设置透明度
|
||
if color[3] < 1.0:
|
||
arrow_node.setTransparency(TransparencyAttrib.MAlpha)
|
||
else:
|
||
arrow_node.setTransparency(TransparencyAttrib.MNone)
|
||
|
||
arrow_node.setLightOff() # 禁用光照影响
|
||
arrow_node.setShaderOff() # 禁用着色器
|
||
arrow_node.setFogOff() # 禁用雾效果
|
||
|
||
arrow_node.setBin("fixed",31)
|
||
#arrow_node.setDepthWrite(False)
|
||
#arrow_node.setDepthTest(True)
|
||
|
||
# 保存材质引用以便后续修改
|
||
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", 30)
|
||
#axis_node.setDepthWrite(False)
|
||
#axis_node.setDepthTest(True)
|
||
|
||
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)
|
||
|
||
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)
|
||
|
||
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 = 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
|
||
|
||
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)
|
||
|
||
# 计算各轴端点的世界坐标
|
||
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:
|
||
return
|
||
|
||
# 使用统一的检测方法
|
||
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.gizmoHighlightAxis = hoveredAxis
|
||
|
||
self._last_detected_axis = hoveredAxis
|
||
|
||
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
|
||
else:
|
||
self.dragGizmoAxis = axis
|
||
self.dragStartMousePos = (mouseX, mouseY)
|
||
|
||
# 保存开始拖拽时目标节点的位置和坐标轴的位置
|
||
self.gizmoTargetStartPos = self.gizmoTarget.getPos()
|
||
self.gizmoStartPos = self.gizmo.getPos(self.world.render) # 坐标轴的世界位置
|
||
|
||
# 确保正在拖动的轴保持高亮状态
|
||
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])
|
||
self.gizmoHighlightAxis = self.dragGizmoAxis
|
||
elif axis and axis in self.gizmo_colors:
|
||
for axis_name in self.gizmo_colors.keys():
|
||
self.setGizmoAxisColor(axis_name, self.gizmo_colors[axis_name])
|
||
|
||
self.setGizmoAxisColor(axis, self.gizmo_highlight_colors[axis])
|
||
self.gizmoHighlightAxis = axis
|
||
|
||
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
|
||
|
||
# 计算鼠标移动距离(屏幕像素)
|
||
mouseDeltaX = mouseX - self.dragStartMousePos[0]
|
||
mouseDeltaY = mouseY - self.dragStartMousePos[1]
|
||
|
||
# 使用坐标轴的实际位置而不是目标节点位置来计算屏幕投影
|
||
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
|
||
|
||
# 确定轴向量的变换上下文
|
||
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)
|
||
#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]
|
||
# )
|
||
|
||
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:
|
||
node_scale = current_node.getScale()
|
||
avg_scale = (node_scale.x+node_scale.y + node_scale.z) / 3.0
|
||
total_scale_factor *= avg_scale
|
||
current_node = current_node.getParent()
|
||
|
||
if total_scale_factor > 0:
|
||
movement_distance = movement_distance / total_scale_factor
|
||
|
||
currentPos = self.gizmoTargetStartPos
|
||
|
||
# scale_adjustment = 1.0
|
||
# if parent_node and parent_node!= self.world.render:
|
||
# current_node = parent_node
|
||
# total_scale = 1.0
|
||
# while current_node and current_node != self.world.render:
|
||
# node_scale = current_node.getScale()
|
||
# avg_scale = (node_scale.x+node_scale.y + node_scale.z)/3.0
|
||
# total_scale *= avg_scale
|
||
# current_node = current_node.getParent()
|
||
# if total_scale>0:
|
||
# scale_adjustment = 1.0 / total_scale
|
||
# # parent_scale = parent_node.getScale()
|
||
# # avg_scale = (parent_scale.x+parent_scale.y+parent_scale.z)/3.0
|
||
# # if avg_scale>0:
|
||
# # scale_adjustment = 1.0 / avg_scale
|
||
#
|
||
#
|
||
# fixed_pixel_to_world_ratio = 0.01 # 1像素 = 0.01世界单位
|
||
# scale_factor = fixed_pixel_to_world_ratio * scale_adjustment
|
||
#
|
||
# movement_distance = projected_distance * 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 stopGizmoDrag(self):
|
||
"""停止坐标轴拖拽"""
|
||
print(f"停止坐标轴拖拽 - 轴: {self.dragGizmoAxis}")
|
||
if self.dragGizmoAxis and self.dragGizmoAxis in self.gizmo_colors:
|
||
self.setGizmoAxisColor(self.dragGizmoAxis, self.gizmo_colors[self.dragGizmoAxis])
|
||
# 不要将 gizmoHighlightAxis 设置为 None,保持当前高亮轴的状态
|
||
# self.gizmoHighlightAxis = None
|
||
|
||
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
|
||
|
||
def checkAndClearIfTargetDeleted(self):
|
||
if self.gizmoTarget and self.gizmoTarget.isEmpty():
|
||
self.clearGizmo()
|
||
|
||
if self.selectionBoxTarget and self.selectionBoxTarget.isEmpty():
|
||
self.clearSelectionBox()
|