1523 lines
60 KiB
Python
1523 lines
60 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)
|
||
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*10, 0, 0, 1), # 红色
|
||
"y": (0, 1*10, 0, 1), # 绿色
|
||
"z": (0, 0, 1*10, 1) # 蓝色
|
||
}
|
||
self.gizmo_highlight_colors = {
|
||
"x": (1.5*20, 1.5*20, 0, 1), # 黄色高亮
|
||
"y": (1.5*20, 1.5*20, 0, 1), # 黄色高亮
|
||
"z": (1.5*20, 1.5*20, 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
|
||
|
||
# 检查是否需要重新计算边界框
|
||
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"])
|
||
|
||
# 只启动一次更新任务
|
||
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
|
||
|
||
# 创建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)
|
||
|
||
|
||
|
||
# 初始化高亮状态
|
||
self.gizmoHighlightAxis = None
|
||
|
||
# 立即设置初始颜色,确保创建时就有正确的颜色
|
||
self.setGizmoAxisColor("z", self.gizmo_colors["z"])
|
||
self.setGizmoAxisColor("x", self.gizmo_colors["x"])
|
||
self.setGizmoAxisColor("y", self.gizmo_colors["y"])
|
||
|
||
|
||
print(f"✓ 坐标轴几何体创建完成,长度={self.axis_length}")
|
||
|
||
# 为 RenderPipeline 环境设置正确的渲染状态
|
||
self._setupRenderPipelineCompatibleGizmo()
|
||
|
||
self._setupEmissiveMaterials()
|
||
self._setupGizmoRendering()
|
||
|
||
except Exception as e:
|
||
print(f"创建坐标轴几何体失败: {str(e)}")
|
||
|
||
def _setupEmissiveMaterials(self):
|
||
try:
|
||
from panda3d.core import Material,Vec4
|
||
materials ={
|
||
"x":(Vec4(1,0,0,1),Vec4(2.0,0,0,1)),
|
||
"y":(Vec4(0,1,0,1),Vec4(0,2.0,0,1)),
|
||
"z":(Vec4(0,0,1,1),Vec4(0,0,2.0,1))
|
||
}
|
||
axis_nodes ={
|
||
"x":self.gizmoXAxis,
|
||
"y":self.gizmoYAxis,
|
||
"z":self.gizmoZAxis
|
||
}
|
||
for axis,(base_color,emission_color) in materials.items():
|
||
if axis_nodes[axis]:
|
||
material=Material(f"gizmo_{axis}_material")
|
||
material.setBaseColor(base_color)
|
||
material.setEmission(emission_color)
|
||
material.setRoughness(1.0)
|
||
material.setMetallic(0.0)
|
||
axis_nodes[axis].setMaterial(material)
|
||
except Exception as e:
|
||
print(f"自发光材质设置失败: {str(e)}")
|
||
|
||
def _setupGizmoRendering(self):
|
||
"""设置坐标轴渲染属性"""
|
||
try:
|
||
# 设置渲染优先级,确保在最前面显示
|
||
self.gizmo.setBin("gui-popup", 1000)
|
||
self.gizmo.setDepthTest(False)
|
||
self.gizmo.setDepthWrite(False)
|
||
self.gizmo.setLightOff()
|
||
|
||
# 为每个轴设置独立的渲染属性
|
||
for i, axis_node in enumerate([self.gizmoXAxis, self.gizmoYAxis, self.gizmoZAxis]):
|
||
if axis_node:
|
||
axis_node.setBin("gui-popup", 1001 + i)
|
||
axis_node.setDepthTest(False)
|
||
axis_node.setDepthWrite(False)
|
||
axis_node.setLightOff()
|
||
|
||
except Exception as e:
|
||
print(f"设置坐标轴渲染失败!!!!!!: {str(e)}")
|
||
|
||
def _setupRenderPipelineCompatibleGizmo(self):
|
||
"""为 RenderPipeline 环境设置兼容的坐标轴渲染 - 激进修复版本"""
|
||
try:
|
||
from panda3d.core import (ShaderAttrib, MaterialAttrib, RenderModeAttrib,
|
||
AntialiasAttrib, TransparencyAttrib, CullFaceAttrib,
|
||
RescaleNormalAttrib, TextureAttrib)
|
||
|
||
# 第一步:完全隔离坐标轴,避免被任何系统处理
|
||
self._isolateGizmoFromRenderPipeline()
|
||
|
||
# 第二步:使用最简单的渲染方式
|
||
self._setupMinimalGizmoRendering()
|
||
|
||
# 第三步:强制设置每个轴的独立渲染
|
||
self._forceAxisIndependentRendering()
|
||
|
||
|
||
except Exception as e:
|
||
# 使用最后的备用方案
|
||
self._setupUltimateGizmoFallback()
|
||
|
||
def _isolateGizmoFromRenderPipeline(self):
|
||
"""完全隔离坐标轴,避免被 RenderPipeline 处理"""
|
||
try:
|
||
# 设置所有可能的隔离标签
|
||
isolation_tags = [
|
||
("no_shadow", "1"),
|
||
("no_lighting", "1"),
|
||
("no_material", "1"),
|
||
("no_shader", "1"),
|
||
("no_fog", "1"),
|
||
("no_texture", "1"),
|
||
("gui_element", "1"),
|
||
("bypass_rp", "1"),
|
||
("fixed_pipeline", "1"),
|
||
("ignore_all", "1")
|
||
]
|
||
|
||
for tag, value in isolation_tags:
|
||
self.gizmo.setTag(tag, value)
|
||
|
||
# 完全禁用所有高级功能,使用最高优先级
|
||
self.gizmo.setShaderOff(10000)
|
||
self.gizmo.setLightOff(10000)
|
||
self.gizmo.setFogOff(10000)
|
||
self.gizmo.setMaterialOff(10000)
|
||
self.gizmo.setTextureOff(10000)
|
||
|
||
# 禁用所有可能的渲染特性
|
||
from panda3d.core import RenderModeAttrib, CullFaceAttrib
|
||
# self.gizmo.setRenderModeWireframe()
|
||
# self.gizmo.setTwoSided(True)
|
||
self.gizmo.setRenderMode(RenderModeAttrib.MFilled)
|
||
self.gizmo.setTwoSided(True)
|
||
|
||
self.gizmo.setColorScale(2.0,2.0,2.0,1.0)
|
||
for axis_node in [self.gizmoXAxis,self.gizmoYAxis,self.gizmoZAxis]:
|
||
if axis_node:
|
||
axis_node.setTag("emissive","1")
|
||
axis_node.setTag("unlit","1")
|
||
axis_node.setColorScale(2.0,2.0,2.0,1.0)
|
||
|
||
except Exception as e:
|
||
print(f" ❌ 隔离失败: {e}")
|
||
|
||
def _setupMinimalGizmoRendering(self):
|
||
"""设置最简单的渲染方式"""
|
||
try:
|
||
from panda3d.core import (ShaderAttrib, MaterialAttrib, TextureAttrib,
|
||
CullFaceAttrib, RescaleNormalAttrib)
|
||
|
||
# 使用最高优先级的 GUI 渲染 bin
|
||
self.gizmo.setBin("gui-popup", 10000)
|
||
self.gizmo.setDepthTest(False)
|
||
self.gizmo.setDepthWrite(False)
|
||
|
||
# 创建最简单的渲染状态
|
||
minimal_state = RenderState.make(
|
||
ShaderAttrib.makeOff(10000), # 完全禁用着色器
|
||
MaterialAttrib.makeOff(), # 禁用材质
|
||
TextureAttrib.makeOff(), # 禁用纹理
|
||
CullFaceAttrib.make(CullFaceAttrib.MCullNone), # 禁用面剔除
|
||
RescaleNormalAttrib.makeOff() # 禁用法线重缩放
|
||
)
|
||
self.gizmo.setState(minimal_state, 10000)
|
||
|
||
except Exception as e:
|
||
print(f" ❌ 最简渲染设置失败: {e}")
|
||
|
||
def _forceAxisIndependentRendering(self):
|
||
"""强制设置每个轴的独立渲染"""
|
||
try:
|
||
# 轴配置
|
||
axis_configs = [
|
||
(self.gizmoXAxis, "X轴", (1.0, 0.0, 0.0, 1.0), 0),
|
||
(self.gizmoYAxis, "Y轴", (0.0, 1.0, 0.0, 1.0), 0),
|
||
(self.gizmoZAxis, "Z轴", (0.0, 0.0, 1.0, 1.0), 0)
|
||
]
|
||
|
||
for axis_node, name, color, priority in axis_configs:
|
||
if axis_node:
|
||
# 每个轴都完全独立设置
|
||
self._setupSingleAxisRendering(axis_node, name, color, 0)
|
||
|
||
|
||
except Exception as e:
|
||
print(f" ❌ 独立轴渲染设置失败: {e}")
|
||
|
||
def _setupSingleAxisRendering(self, axis_node, name, color, priority):
|
||
"""为单个轴设置完全独立的渲染"""
|
||
try:
|
||
from panda3d.core import (LVecBase4f, RenderState, ColorAttrib,
|
||
TransparencyAttrib, LColor, AntialiasAttrib,
|
||
RenderModeAttrib, CullFaceAttrib, AuxBitplaneAttrib,
|
||
LightRampAttrib)
|
||
|
||
# 转换颜色为LColor并增加亮度
|
||
base_color = LColor(*color)
|
||
emissive_color = LColor(base_color[0], base_color[1], base_color[2], 1.0)
|
||
|
||
# 完全禁用所有高级渲染功能
|
||
axis_node.clearShader()
|
||
axis_node.clearTexture()
|
||
axis_node.clearMaterial()
|
||
axis_node.setLightOff()
|
||
axis_node.setFogOff()
|
||
axis_node.setAttrib(RenderModeAttrib.make(RenderModeAttrib.MWireframe, 6.0))
|
||
axis_node.setAttrib(AntialiasAttrib.make(AntialiasAttrib.MLine))
|
||
axis_node.setBin("gui-popup", 0)
|
||
axis_node.setDepthTest(False)
|
||
axis_node.setDepthWrite(False)
|
||
axis_node.setTwoSided(True)
|
||
|
||
# 强制设置自发光颜色
|
||
axis_node.setColor(*color)
|
||
axis_node.setColorScale(1.0, 1.0, 1.0, 1.0) # 增加整体亮度
|
||
|
||
except Exception as e:
|
||
print(" ❌ {} 轴渲染设置失败: {}".format(name, str(e)))
|
||
raise e
|
||
|
||
def _setupUltimateGizmoFallback(self):
|
||
"""最后的备用方案 - 最简单的渲染"""
|
||
try:
|
||
# 最简单的设置
|
||
self.gizmo.setLightOff()
|
||
self.gizmo.setFogOff()
|
||
self.gizmo.setBin("gui-popup", 20000)
|
||
self.gizmo.setDepthTest(False)
|
||
self.gizmo.setDepthWrite(False)
|
||
|
||
# 直接设置颜色,不使用复杂的渲染状态
|
||
if self.gizmoXAxis:
|
||
self.gizmoXAxis.setColor(1, 0, 0, 1)
|
||
self.gizmoXAxis.setLightOff()
|
||
self.gizmoXAxis.setBin("gui-popup", 20001)
|
||
self.gizmoXAxis.setDepthTest(False)
|
||
|
||
if self.gizmoYAxis:
|
||
self.gizmoYAxis.setColor(0, 1, 0, 1)
|
||
self.gizmoYAxis.setLightOff()
|
||
self.gizmoYAxis.setBin("gui-popup", 20002)
|
||
self.gizmoYAxis.setDepthTest(False)
|
||
|
||
if self.gizmoZAxis:
|
||
self.gizmoZAxis.setColor(0, 0, 1, 1)
|
||
self.gizmoZAxis.setLightOff()
|
||
self.gizmoZAxis.setBin("gui-popup", 20003)
|
||
self.gizmoZAxis.setDepthTest(False)
|
||
|
||
except Exception as e:
|
||
print(f"❌ 最后备用方案也失败: {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()
|
||
# # 更新坐标轴位置,始终在目标节点中心
|
||
# 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)
|
||
|
||
# 【新功能】:动态调整坐标轴大小,保持固定的屏幕大小
|
||
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.1
|
||
max_scale = 10.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]*20.0,color[1]*20.0,color[2]*20.0,color[3])
|
||
axis_node.setShaderOff(10000)
|
||
axis_node.setLightOff(10000)
|
||
axis_node.setMaterialOff(10000)
|
||
axis_node.setTextureOff(1000)
|
||
axis_node.setFogOff(10000)
|
||
|
||
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 _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 updateGizmoHighlight(self, mouseX, mouseY):
|
||
"""更新坐标轴高亮状态"""
|
||
if not self.gizmo or self.isDraggingGizmo:
|
||
return
|
||
|
||
import time
|
||
current_time = time.time()
|
||
if not hasattr(self,'_last_highlight_time'):
|
||
self._last_highlight_time = 0
|
||
|
||
if current_time - self._last_highlight_time<0.05:
|
||
return
|
||
self._last_highlight_time = current_time
|
||
|
||
hoveredAxis = self._detectHoveredAxis(mouseX, mouseY)
|
||
|
||
if not hasattr(self,'_hover_stability_counter'):
|
||
self._hover_stability_counter = {}
|
||
self._last_detected_axis = None
|
||
|
||
if hoveredAxis !=self._last_detected_axis:
|
||
self._hover_stability_counter[hoveredAxis]=1
|
||
self._last_detected_axis = hoveredAxis
|
||
else:
|
||
self._hover_stability_counter[hoveredAxis] = self._hover_stability_counter.get(hoveredAxis,0)+1
|
||
|
||
if self._hover_stability_counter.get(hoveredAxis,0)>=2:
|
||
if hoveredAxis != self.gizmoHighlightAxis:
|
||
self._updateAxisHighlight(hoveredAxis)
|
||
|
||
# 检测鼠标悬停的轴(使用相同的检测逻辑但不输出调试信息)
|
||
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 _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
|
||
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 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}")
|
||
|
||
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()
|