EG/core/selection.py

1510 lines
60 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

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

"""
选择和变换系统模块
负责物体选择和变换相关功能:
- 选择框的创建和更新
- 坐标轴(Gizmo)系统
- 拖拽变换逻辑
- 射线检测和碰撞检测
"""
from 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
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
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