3003 lines
118 KiB
Python
3003 lines
118 KiB
Python
"""
|
||
选择和变换系统模块
|
||
|
||
负责物体选择和变换相关功能:
|
||
- 选择框的创建和更新
|
||
- 坐标轴(Gizmo)系统
|
||
- 拖拽变换逻辑
|
||
- 射线检测和碰撞检测
|
||
"""
|
||
from direct.showbase.ShowBaseGlobal import globalClock
|
||
from panda3d.core import (Vec3, Point3, Point2, LineSegs, ColorAttrib, RenderState,
|
||
DepthTestAttrib, CollisionTraverser, CollisionHandlerQueue,
|
||
CollisionNode, CollisionRay, GeomNode, BitMask32, Material, LColor, DepthWriteAttrib,
|
||
TransparencyAttrib, Vec4, CollisionCapsule, WindowProperties)
|
||
from direct.task.TaskManagerGlobal import taskMgr
|
||
import math
|
||
from core.selection_outline import SelectionOutlineManager
|
||
from core.editor_context import get_editor_context
|
||
|
||
class SelectionSystem:
|
||
"""选择和变换系统类"""
|
||
|
||
def __init__(self, world):
|
||
"""初始化选择系统
|
||
|
||
Args:
|
||
world: 核心世界对象引用
|
||
"""
|
||
self.world = world
|
||
self._editor_context = get_editor_context(world)
|
||
|
||
# 选择相关状态
|
||
self.selectedNode = None
|
||
self.selectionBox = None # 选择框
|
||
self.selectionBoxTarget = None # 选择框跟踪的目标节点
|
||
self.show_selection_box = False
|
||
self.enable_unity_outline = True
|
||
self.outline_manager = getattr(self.world, "_selection_outline_manager", None)
|
||
if not self.outline_manager:
|
||
self.outline_manager = SelectionOutlineManager(
|
||
self.world,
|
||
enabled=self.enable_unity_outline,
|
||
)
|
||
setattr(self.world, "_selection_outline_manager", self.outline_manager)
|
||
else:
|
||
self.outline_manager.set_enabled(self.enable_unity_outline)
|
||
|
||
# 坐标轴工具(Gizmo)相关
|
||
self.gizmo = None # 坐标轴
|
||
self.gizmoTarget = None # 坐标轴跟踪的目标节点
|
||
self.gizmoXAxis = None # X轴
|
||
self.gizmoYAxis = None # Y轴
|
||
self.gizmoZAxis = None # Z轴
|
||
self.gizmoRotXAxis = None
|
||
self.gizmoRotYAxis = None
|
||
self.gizmoRotZAxis = None
|
||
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, 1.0), # 红色
|
||
"y": (0, 1, 0, 1.0), # 绿色
|
||
"z": (0, 0, 1, 1.00) # 蓝色
|
||
}
|
||
self.gizmo_highlight_colors = {
|
||
"x": (1.0, 1.0, 0.0, 1.0), # 黄色高亮
|
||
"y": (1.0, 1.0, 0.0, 1.0), # 黄色高亮
|
||
"z": (1.0, 1.0, 0.0, 1.0) # 黄色高亮
|
||
}
|
||
|
||
#性能优化相关
|
||
self._optimized_node = False
|
||
self._last_update_time = 0
|
||
self._cached_bounds = {}
|
||
self._gizmo_update_interval = 0.1
|
||
self._selection_box_update_interval = 0.2
|
||
|
||
self._current_cursor = None
|
||
self._default_cursor = None
|
||
|
||
self._last_click_time = 0
|
||
self._double_click_threshold = 0.3
|
||
self._last_clicked_node = None
|
||
self._double_click_task = None
|
||
|
||
print("✓ 选择和变换系统初始化完成")
|
||
|
||
def _is_valid_node(self, node, require_attached=False):
|
||
if node is None:
|
||
return False
|
||
try:
|
||
if node.isEmpty():
|
||
return False
|
||
except Exception:
|
||
return False
|
||
if require_attached:
|
||
try:
|
||
return bool(node.hasParent())
|
||
except Exception:
|
||
return False
|
||
return True
|
||
|
||
def _same_valid_node(self, left, right):
|
||
if left is None and right is None:
|
||
return True
|
||
if (left is None) != (right is None):
|
||
return False
|
||
if not self._is_valid_node(left) or not self._is_valid_node(right):
|
||
return False
|
||
try:
|
||
return left == right
|
||
except Exception:
|
||
return False
|
||
|
||
def _get_tree_widget(self):
|
||
"""统一获取场景树控件。"""
|
||
return self._editor_context.get_tree_widget()
|
||
|
||
def _clear_tree_selection(self):
|
||
"""清空场景树选中项。"""
|
||
tree_widget = self._get_tree_widget()
|
||
if not tree_widget:
|
||
return False
|
||
tree_widget.setCurrentItem(None)
|
||
return True
|
||
|
||
# ==================== 光标设置 ====================
|
||
def _setCursor(self,cursor_type):
|
||
try:
|
||
if self._current_cursor == cursor_type:
|
||
return
|
||
if not hasattr(self.world, "win") or not self.world.win:
|
||
return
|
||
|
||
# Panda3D 原生接口不支持直接切换系统光标形状,这里保留状态并确保光标可见。
|
||
props = WindowProperties()
|
||
props.setCursorHidden(False)
|
||
self.world.win.requestProperties(props)
|
||
self._current_cursor = cursor_type
|
||
except Exception as e:
|
||
print(f"设置光标失败{e}")
|
||
def _resetCursor(self):
|
||
self._setCursor("default")
|
||
|
||
def _has_new_transform_gizmo(self):
|
||
tg = getattr(self.world, "newTransform", None)
|
||
return tg is not None
|
||
|
||
def _has_attached_transform_gizmo(self, nodePath=None):
|
||
"""Return whether a transform gizmo is currently attached in either legacy or new mode."""
|
||
if self._has_new_transform_gizmo():
|
||
try:
|
||
target = getattr(self.world.newTransform, "target_node", None)
|
||
if nodePath is not None:
|
||
return target == nodePath
|
||
return target is not None and not target.isEmpty()
|
||
except Exception:
|
||
return False
|
||
|
||
if not self.gizmo:
|
||
return False
|
||
try:
|
||
if self.gizmo.isEmpty():
|
||
return False
|
||
except Exception:
|
||
return False
|
||
if nodePath is None:
|
||
return True
|
||
return self.gizmoTarget == nodePath
|
||
|
||
def _has_legacy_gizmo_input(self):
|
||
"""Legacy gizmo path is retired."""
|
||
return False
|
||
|
||
def _get_effective_selected_node(self):
|
||
"""Resolve the current editor selection across scene and SSBO sources."""
|
||
ssbo_editor = getattr(self.world, "ssbo_editor", None)
|
||
if ssbo_editor and hasattr(ssbo_editor, "has_active_selection"):
|
||
try:
|
||
if ssbo_editor.has_active_selection():
|
||
node = ssbo_editor.get_selection_scene_node()
|
||
if self._is_valid_node(node, require_attached=True):
|
||
return node
|
||
fallback_model = getattr(ssbo_editor, "model", None)
|
||
if self._is_valid_node(fallback_model, require_attached=True):
|
||
return fallback_model
|
||
return None
|
||
except Exception:
|
||
pass
|
||
|
||
node = self.selectedNode
|
||
if node is None:
|
||
return None
|
||
if not self._is_valid_node(node, require_attached=True):
|
||
return None
|
||
return node
|
||
|
||
def _sync_rp_light_position(self, light_node, light_object=None):
|
||
"""同步灯光包装节点与 RenderPipeline 灯光对象位置。"""
|
||
try:
|
||
if not light_node or light_node.isEmpty():
|
||
return False
|
||
if light_object is None:
|
||
light_object = light_node.getPythonTag("rp_light_object")
|
||
if not light_object and light_node.hasTag("light_type"):
|
||
# 兼容旧数据:节点存在 light_type 但未绑定 rp_light_object 时尝试重建绑定
|
||
scene_manager = getattr(self.world, "scene_manager", None)
|
||
if scene_manager:
|
||
try:
|
||
light_type = light_node.getTag("light_type")
|
||
if light_type == "spot_light" and hasattr(scene_manager, "_recreateSpotLight"):
|
||
scene_manager._recreateSpotLight(light_node)
|
||
elif light_type == "point_light" and hasattr(scene_manager, "_recreatePointLight"):
|
||
scene_manager._recreatePointLight(light_node)
|
||
light_object = light_node.getPythonTag("rp_light_object")
|
||
except Exception:
|
||
pass
|
||
if not light_object:
|
||
return False
|
||
|
||
world_pos = light_node.getPos(self.world.render)
|
||
|
||
# 优先使用 RP Light 的 setPos 接口
|
||
try:
|
||
light_object.setPos(world_pos)
|
||
except Exception:
|
||
try:
|
||
light_object.setPos(world_pos.x, world_pos.y, world_pos.z)
|
||
except Exception:
|
||
try:
|
||
light_object.pos = Point3(world_pos)
|
||
except Exception:
|
||
return False
|
||
return True
|
||
except Exception:
|
||
return False
|
||
|
||
def sync_transform_gizmo_mode(self):
|
||
"""Sync TransformGizmo mode with current tool."""
|
||
if not self._has_new_transform_gizmo():
|
||
return
|
||
try:
|
||
from TransformGizmo.events import TransformGizmoMode
|
||
tool_mgr = getattr(self.world, "tool_manager", None)
|
||
if not tool_mgr:
|
||
return
|
||
if tool_mgr.isRotateTool():
|
||
self.world.newTransform.set_mode(TransformGizmoMode.ROTATE)
|
||
elif tool_mgr.isScaleTool():
|
||
self.world.newTransform.set_mode(TransformGizmoMode.SCALE)
|
||
elif tool_mgr.isMoveTool() or tool_mgr.isSelectionTool():
|
||
self.world.newTransform.set_mode(TransformGizmoMode.MOVE)
|
||
else:
|
||
self.world.newTransform.set_mode(TransformGizmoMode.NONE)
|
||
except Exception as e:
|
||
print(f"sync transform gizmo mode failed: {e}")
|
||
|
||
# ==================== 选择框系统 ====================
|
||
|
||
def createSelectionBox(self, nodePath):
|
||
"""为选中的节点创建选择框"""
|
||
try:
|
||
if self.selectionBox:
|
||
#print(" 移除现有选择框")
|
||
self.selectionBox.removeNode()
|
||
self.selectionBox = None
|
||
|
||
if not nodePath:
|
||
print(" 目标节点为空,取消创建")
|
||
return
|
||
|
||
self.selectionBox = self.world.render.attachNewNode("selectionBox")
|
||
self.selectionBoxTarget = nodePath
|
||
|
||
#taskMgr.add(self.updateSelectionBoxTask, "updateSelectionBox")
|
||
#self.updateSelectionBoxGeometry()
|
||
|
||
except Exception as e:
|
||
print(f" ✗ 创建选择框失败: {str(e)}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
|
||
def _updateSelectionOutline(self, nodePath):
|
||
"""Update Unity-like selection outline visuals."""
|
||
try:
|
||
if not getattr(self, "outline_manager", None):
|
||
return
|
||
if not self.enable_unity_outline:
|
||
self.outline_manager.clear()
|
||
return
|
||
if nodePath and not nodePath.isEmpty():
|
||
self.outline_manager.set_targets([nodePath])
|
||
else:
|
||
self.outline_manager.clear()
|
||
except Exception as e:
|
||
print(f"update selection outline failed: {e}")
|
||
|
||
def updateSelectionBoxGeometry(self):
|
||
"""更新选择框的几何形状和位置"""
|
||
try:
|
||
if not self.selectionBox or not self.selectionBoxTarget:
|
||
return
|
||
|
||
if self.selectionBoxTarget.isEmpty():
|
||
return
|
||
|
||
minPoint = Point3()
|
||
maxPoint = Point3()
|
||
|
||
try:
|
||
has_bounds = self.selectionBoxTarget.calcTightBounds(minPoint, maxPoint, self.world.render)
|
||
if not has_bounds:
|
||
return
|
||
except:
|
||
return
|
||
|
||
# 检查边界框的有效性
|
||
if (minPoint.x > maxPoint.x or minPoint.y > maxPoint.y or minPoint.z > maxPoint.z or
|
||
abs(minPoint.x) > 1e10 or abs(minPoint.y) > 1e10 or abs(minPoint.z) > 1e10 or
|
||
abs(maxPoint.x) > 1e10 or abs(maxPoint.y) > 1e10 or abs(maxPoint.z) > 1e10):
|
||
print("警告: 检测到无效的边界框,跳过选择框更新")
|
||
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:
|
||
update_interval = 0.05
|
||
|
||
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 < update_interval:
|
||
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
|
||
|
||
# 检查目标节点是否发生了变化(位置、旋转、缩放)
|
||
current_transform = self._getNodeTransformKey(self.selectionBoxTarget)
|
||
|
||
if (not hasattr(self, '_last_transform_key') or
|
||
self._last_transform_key != current_transform):
|
||
# 节点发生了变化,更新选择框
|
||
self.updateSelectionBoxGeometry()
|
||
self._last_transform_key = current_transform
|
||
|
||
return task.cont
|
||
|
||
except Exception as e:
|
||
print(f"选择框更新任务出错: {str(e)}")
|
||
return task.done
|
||
|
||
def _getNodeTransformKey(self, node):
|
||
"""获取节点变换的关键信息,用于快速比较"""
|
||
try:
|
||
# 获取节点的关键变换信息
|
||
pos = node.getPos(self.world.render)
|
||
hpr = node.getHpr(self.world.render)
|
||
scale = node.getScale(self.world.render)
|
||
|
||
# 返回一个可以比较的元组
|
||
return (pos.x, pos.y, pos.z, hpr.x, hpr.y, hpr.z, scale.x, scale.y, scale.z)
|
||
except:
|
||
return None
|
||
|
||
def clearSelectionBox(self):
|
||
"""清除选择框"""
|
||
if self.selectionBox:
|
||
self.selectionBox.removeNode()
|
||
self.selectionBox = None
|
||
|
||
# 停止选择框更新任务
|
||
taskMgr.remove("updateSelectionBox")
|
||
|
||
# 清除目标节点引用
|
||
self.selectionBoxTarget = None
|
||
|
||
print("清除了选择框")
|
||
|
||
# ==================== 坐标轴(Gizmo)系统 ====================
|
||
|
||
def createGizmo(self, nodePath):
|
||
"""Attach the unified TransformGizmo to the selected node."""
|
||
self.gizmo = None
|
||
self.gizmoTarget = nodePath
|
||
if not nodePath or not self._has_new_transform_gizmo():
|
||
return
|
||
self.sync_transform_gizmo_mode()
|
||
try:
|
||
self.world.newTransform.attach(nodePath)
|
||
except Exception as e:
|
||
print(f"attach TransformGizmo failed: {e}")
|
||
def createGizmoGeometry(self):
|
||
"""创建坐标轴的几何体"""
|
||
return
|
||
from panda3d.core import Material
|
||
try:
|
||
if not self.gizmo:
|
||
return
|
||
|
||
is_scale_tool = self.world.tool_manager.isScaleTool() if self.world.tool_manager else False
|
||
is_rotate_tool = self.world.tool_manager.isRotateTool() if self.world.tool_manager else False
|
||
|
||
import os
|
||
from panda3d.core import getModelPath, Filename
|
||
|
||
# 获取项目根目录
|
||
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||
|
||
# 确保core目录在模型搜索路径中
|
||
model_path = getModelPath()
|
||
core_dir = os.path.join(base_dir, "core")
|
||
core_filename = Filename.from_os_specific(core_dir)
|
||
if not model_path.findFile(core_filename):
|
||
model_path.appendDirectory(core_filename)
|
||
print(f"✓ 添加core目录到模型搜索路径: {core_dir}")
|
||
|
||
if is_scale_tool:
|
||
model_filename = "core/UniformScaleHandle.fbx"
|
||
elif is_rotate_tool:
|
||
model_filename = "core/RotationHandleQuarter.fbx"
|
||
arrow_filename = "core/TranslateArrowHandle.fbx"
|
||
else:
|
||
model_filename = "core/TranslateArrowHandle.fbx"
|
||
|
||
# model_paths = [
|
||
# "core/TranslateArrowHandle.fbx",
|
||
# "EG/core/TranslateArrowHandle.fbx",
|
||
# ]
|
||
gizmo_model = None
|
||
gizmoRot_model = None
|
||
try:
|
||
if is_rotate_tool:
|
||
gizmo_model = self.world.loader.loadModel(arrow_filename)
|
||
gizmoRot_model = self.world.loader.loadModel(model_filename)
|
||
else:
|
||
gizmo_model = self.world.loader.loadModel(model_filename)
|
||
except Exception as e:
|
||
print(f"加载模型失败: {e}")
|
||
return
|
||
|
||
x_rHandle = None
|
||
y_rHandle = None
|
||
z_rHandle = None
|
||
|
||
if is_rotate_tool:
|
||
self.gizmoRotXAxis = self.gizmo.attachNewNode("gizmo_rot_x_axis")
|
||
x_rHandle = gizmoRot_model.copyTo(self.gizmoRotXAxis)
|
||
x_rHandle.setName("x_handle")
|
||
self.gizmoRotYAxis = self.gizmo.attachNewNode("gizmo_rot_y_axis")
|
||
y_rHandle = gizmoRot_model.copyTo(self.gizmoRotYAxis)
|
||
y_rHandle.setName("y_handle")
|
||
self.gizmoRotZAxis = self.gizmo.attachNewNode("gizmo_rot_z_axis")
|
||
z_rHandle = gizmoRot_model.copyTo(self.gizmoRotZAxis)
|
||
z_rHandle.setName("z_handle")
|
||
|
||
self.gizmoXAxis = self.gizmo.attachNewNode("gizmo_x_axis")
|
||
x_handle = gizmo_model.copyTo(self.gizmoXAxis)
|
||
x_handle.setName("x_handle")
|
||
|
||
self.gizmoYAxis = self.gizmo.attachNewNode("gizmo_y_axis")
|
||
y_handle = gizmo_model.copyTo(self.gizmoYAxis)
|
||
y_handle.setName("y_handle")
|
||
|
||
self.gizmoZAxis = self.gizmo.attachNewNode("gizmo_z_axis")
|
||
z_handle = gizmo_model.copyTo(self.gizmoZAxis)
|
||
z_handle.setName("z_handle")
|
||
|
||
if is_scale_tool:
|
||
x_handle.setHpr(0,-90,0)
|
||
x_handle.setScale(0.6,0.03,0.03)
|
||
x_handle.setPos(2.2,0,0)
|
||
|
||
y_handle.setHpr(90,0,0)
|
||
y_handle.setScale(0.6,0.03,0.03)
|
||
y_handle.setPos(0,2.2,0)
|
||
|
||
z_handle.setHpr(0,0,-90)
|
||
z_handle.setScale(0.6,0.03,0.03)
|
||
z_handle.setPos(0,0,2.2)
|
||
elif is_rotate_tool:
|
||
x_rHandle.setHpr(0,0,90)
|
||
x_rHandle.setScale(0.025,0.0125,0.0125)
|
||
x_rHandle.setPos(0,0,0)
|
||
|
||
y_rHandle.setHpr(0,0,0)
|
||
y_rHandle.setScale(0.025,0.0125,0.0125)
|
||
y_rHandle.setPos(0,0,0)
|
||
|
||
z_rHandle.setHpr(-90,0,0)
|
||
z_rHandle.setScale(0.025,0.0125,0.0125)
|
||
z_rHandle.setPos(0,0,0)
|
||
|
||
x_handle.setHpr(0, -90, 0)
|
||
x_handle.setScale(0.1, 0.05, 0.05)
|
||
x_handle.setPos(0, 0, 0)
|
||
|
||
y_handle.setHpr(90, 0, 0)
|
||
y_handle.setScale(0.1, 0.05, 0.05)
|
||
y_handle.setPos(0, 0, 0)
|
||
|
||
z_handle.setHpr(0, 0, -90)
|
||
z_handle.setScale(0.1, 0.05, 0.05)
|
||
z_handle.setPos(0, 0, 0)
|
||
|
||
self.setGizmoRotAxisColor("x", self.gizmo_colors["x"])
|
||
self.setGizmoRotAxisColor("y", self.gizmo_colors["y"])
|
||
self.setGizmoRotAxisColor("z", self.gizmo_colors["z"])
|
||
else:
|
||
x_handle.setHpr(0,-90,0)
|
||
x_handle.setScale(0.1,0.05,0.05)
|
||
x_handle.setPos(0,0,0)
|
||
|
||
y_handle.setHpr(90,0,0)
|
||
y_handle.setScale(0.1,0.05,0.05)
|
||
y_handle.setPos(0,0,0)
|
||
|
||
z_handle.setHpr(0,0,-90)
|
||
z_handle.setScale(0.1,0.05,0.05)
|
||
z_handle.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):
|
||
return
|
||
try:
|
||
axis_nodes = [self.gizmoXAxis,self.gizmoYAxis,self.gizmoZAxis]
|
||
axis_Rotnodes = [self.gizmoRotXAxis, self.gizmoRotYAxis, self.gizmoRotZAxis]
|
||
|
||
# 设置坐标轴主节点的渲染属性
|
||
if self.gizmo:
|
||
self.gizmo.setLightOff() # 禁用光照
|
||
self.gizmo.setShaderOff() # 禁用着色器
|
||
self.gizmo.setFogOff() # 禁用雾效
|
||
self.gizmo.setBin("fixed", 40) # 设置为fixed渲染层级,数值越大越优先
|
||
self.gizmo.setDepthWrite(False) # 禁用深度写入
|
||
self.gizmo.setDepthTest(False) # 禁用深度测试,确保始终可见
|
||
|
||
# 设置各轴节点的渲染属性
|
||
for axis_node in axis_nodes:
|
||
if axis_node:
|
||
axis_node.setLightOff()
|
||
axis_node.setShaderOff()
|
||
axis_node.setFogOff()
|
||
axis_node.setBin("fixed", 40) # 与主节点相同优先级
|
||
axis_node.setDepthWrite(False) # 禁用深度写入
|
||
axis_node.setDepthTest(False) # 禁用深度测试
|
||
|
||
# 设置旋转轴节点的渲染属性
|
||
for axis_rotnode in axis_Rotnodes:
|
||
if axis_rotnode:
|
||
axis_rotnode.setLightOff()
|
||
axis_rotnode.setShaderOff()
|
||
axis_rotnode.setFogOff()
|
||
axis_rotnode.setBin("fixed", 40) # 与主节点相同优先级
|
||
axis_rotnode.setDepthWrite(False) # 禁用深度写入
|
||
axis_rotnode.setDepthTest(False) # 禁用深度测试
|
||
|
||
# 收集所有handle节点
|
||
arrow_nodes = []
|
||
if self.gizmoXAxis:
|
||
x_handle = self.gizmoXAxis.find("x_handle")
|
||
if x_handle and not x_handle.isEmpty():
|
||
arrow_nodes.append(x_handle)
|
||
if self.gizmoYAxis:
|
||
y_handle = self.gizmoYAxis.find("y_handle")
|
||
if y_handle and not y_handle.isEmpty():
|
||
arrow_nodes.append(y_handle)
|
||
if self.gizmoZAxis:
|
||
z_handle = self.gizmoZAxis.find("z_handle")
|
||
if z_handle and not z_handle.isEmpty():
|
||
arrow_nodes.append(z_handle)
|
||
|
||
rot_nodes = []
|
||
if self.gizmoRotXAxis:
|
||
x_rHandle = self.gizmoRotXAxis.find("x_handle")
|
||
if x_rHandle and not x_rHandle.isEmpty():
|
||
rot_nodes.append(x_rHandle)
|
||
if self.gizmoRotYAxis:
|
||
y_rHandle = self.gizmoRotYAxis.find("y_handle")
|
||
if y_rHandle and not y_rHandle.isEmpty():
|
||
rot_nodes.append(y_rHandle)
|
||
if self.gizmoRotZAxis:
|
||
z_rHandle = self.gizmoRotZAxis.find("z_handle")
|
||
if z_rHandle and not z_rHandle.isEmpty():
|
||
rot_nodes.append(z_rHandle)
|
||
|
||
# 设置handle节点的渲染属性
|
||
for arrow_node in arrow_nodes:
|
||
if arrow_node:
|
||
arrow_node.setLightOff()
|
||
arrow_node.setShaderOff()
|
||
arrow_node.setFogOff()
|
||
arrow_node.setBin("fixed", 41) # 略高于主节点,确保最优先显示
|
||
arrow_node.setDepthWrite(False)
|
||
arrow_node.setDepthTest(False)
|
||
# 启用透明度支持
|
||
arrow_node.setTransparency(TransparencyAttrib.MAlpha)
|
||
|
||
for rot_node in rot_nodes:
|
||
if rot_node:
|
||
rot_node.setLightOff()
|
||
rot_node.setShaderOff()
|
||
rot_node.setFogOff()
|
||
rot_node.setBin("fixed", 41) # 略高于主节点,确保最优先显示
|
||
rot_node.setDepthWrite(False)
|
||
rot_node.setDepthTest(False)
|
||
# 启用透明度支持
|
||
rot_node.setTransparency(TransparencyAttrib.MAlpha)
|
||
|
||
except Exception as e:
|
||
print(f"设置坐标轴渲染属性失败: {str(e)}")
|
||
|
||
def updateGizmoTask(self, task):
|
||
"""坐标轴更新任务 - 包含固定大小功能"""
|
||
return task.done
|
||
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.5: # 每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
|
||
|
||
is_scale_tool = self.world.tool_manager.isScaleTool() if self.world.tool_manager else False
|
||
is_rotate_tool = self.world.tool_manager.isRotateTool() if self.world.tool_manager else False
|
||
was_scale_tool = getattr(self,'_last_tool_scale_state',False)
|
||
was_rotate_tool =getattr(self,'_last_tool_rotate_state',False)
|
||
|
||
tool_changed = (is_scale_tool!=was_scale_tool) or (is_rotate_tool != was_rotate_tool)
|
||
|
||
if tool_changed:
|
||
self._last_tool_scale_state = is_scale_tool
|
||
self._last_tool_rotate_state = is_rotate_tool
|
||
|
||
if self.gizmoXAxis:
|
||
self.gizmoXAxis.removeNode()
|
||
self.gizmoXAxis = None
|
||
if self.gizmoYAxis:
|
||
self.gizmoYAxis.removeNode()
|
||
self.gizmoYAxis = None
|
||
if self.gizmoZAxis:
|
||
self.gizmoZAxis.removeNode()
|
||
self.gizmoZAxis = None
|
||
if self.gizmoRotXAxis:
|
||
self.gizmoRotXAxis.removeNode()
|
||
self.gizmoRotXAxis = None
|
||
if self.gizmoRotYAxis:
|
||
self.gizmoRotYAxis.removeNode()
|
||
self.gizmoRotYAxis = None
|
||
if self.gizmoRotZAxis:
|
||
self.gizmoRotZAxis.removeNode()
|
||
self.gizmoRotZAxis = None
|
||
|
||
self.createGizmoGeometry()
|
||
|
||
self.setGizmoAxisColor("x",self.gizmo_colors["x"])
|
||
self.setGizmoAxisColor("y",self.gizmo_colors["y"])
|
||
self.setGizmoAxisColor("z",self.gizmo_colors["z"])
|
||
|
||
self.setupGizmoCollision()
|
||
|
||
light_object = self.gizmoTarget.getPythonTag("rp_light_object")
|
||
if light_object:
|
||
# 以节点位置为真值并回写 RP 灯光,避免“手柄能动但灯光不动”
|
||
self._sync_rp_light_position(self.gizmoTarget, light_object)
|
||
self.gizmo.setPos(self.gizmoTarget.getPos(self.world.render))
|
||
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()
|
||
# 添加异常处理
|
||
try:
|
||
if self.gizmoTarget.calcTightBounds(minPoint, maxPoint, self.world.render):
|
||
# 检查边界框的有效性
|
||
if (abs(minPoint.x) < 1e10 and abs(minPoint.y) < 1e10 and abs(minPoint.z) < 1e10 and
|
||
abs(maxPoint.x) < 1e10 and abs(maxPoint.y) < 1e10 and abs(maxPoint.z) < 1e10):
|
||
# 计算中心点
|
||
center = Point3((minPoint.x + maxPoint.x) * 0.5,
|
||
(minPoint.y + maxPoint.y) * 0.5,
|
||
(minPoint.z + maxPoint.z) * 0.5)
|
||
self.gizmo.setPos(center)
|
||
except Exception as e:
|
||
print(f"更新Gizmo位置时出错: {e}")
|
||
# 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
|
||
|
||
is_scale_tool = self.world.tool_manager.isScaleTool() if self.world.tool_manager else False
|
||
|
||
# 安区地更新朝向
|
||
|
||
if is_scale_tool:
|
||
#self.gizmo.setHpr(self.gizmoTarget.getHpr())
|
||
self.gizmo.setQuat(self.gizmoTarget.getQuat(self.world.render))
|
||
else:
|
||
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)
|
||
|
||
# 更新朝向
|
||
# 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.001
|
||
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):
|
||
"""Clear transform gizmo."""
|
||
if self._has_new_transform_gizmo():
|
||
try:
|
||
self.world.newTransform.detach()
|
||
except Exception as e:
|
||
print(f"detach TransformGizmo failed: {e}")
|
||
|
||
self.gizmoTarget = None
|
||
self.gizmo = 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
|
||
self._resetCursor()
|
||
|
||
# def setGizmoAxisColor(self, axis, color):
|
||
# """设置坐标轴颜色 - RenderPipeline 兼容版本"""
|
||
def setGizmoRotAxisColor(self, axis, color):
|
||
"""使用材质设置坐标轴颜色 - RenderPipeline兼容版本"""
|
||
return
|
||
try:
|
||
from panda3d.core import Material, Vec4,ColorWriteAttrib,DepthWriteAttrib,DepthTestAttrib,TransparencyAttrib
|
||
|
||
# 获取对应的轴节点
|
||
axis_nodes = {
|
||
"x": self.gizmoRotXAxis,
|
||
"y": self.gizmoRotYAxis,
|
||
"z": self.gizmoRotZAxis
|
||
}
|
||
|
||
if axis not in axis_nodes or not axis_nodes[axis]:
|
||
return
|
||
|
||
axis_node = axis_nodes[axis]
|
||
|
||
handle_node = None
|
||
handle_node = axis_node.find("x_handle") if axis == "x" else handle_node
|
||
handle_node = axis_node.find("y_handle") if axis == "y" else handle_node
|
||
handle_node = axis_node.find("z_handle") if axis == "z" else handle_node
|
||
|
||
#如果找不到特定名称的节点,尝试查找任何子节点
|
||
if not handle_node:
|
||
children = axis_node.getChildren()
|
||
if children.getNumPath()>0:
|
||
handle_node = children[0]
|
||
|
||
if not handle_node:
|
||
#print(f"未找到{axis}轴的处理模型")
|
||
return
|
||
|
||
# 创建或获取材质
|
||
mat = Material()
|
||
|
||
# 设置材质属性 - 使用更自然的颜色,避免过亮的自发光
|
||
adjusted_color = Vec4(
|
||
min(color[0]*20, 1.0),
|
||
min(color[1]*20, 1.0),
|
||
min(color[2]*20, 1.0),
|
||
color[3]
|
||
)
|
||
|
||
mat.setBaseColor(adjusted_color)
|
||
#mat.setEmission(Vec4(1, 1, 1, 1.0)) # 自发光
|
||
|
||
# 应用材质
|
||
handle_node.setMaterial(mat, 1)
|
||
|
||
|
||
# 设置透明度
|
||
if color[3] < 1.0:
|
||
handle_node.setTransparency(TransparencyAttrib.MAlpha)
|
||
else:
|
||
handle_node.setTransparency(TransparencyAttrib.MNone)
|
||
|
||
handle_node.setLightOff() # 禁用光照影响
|
||
handle_node.setShaderOff() # 禁用着色器
|
||
handle_node.setFogOff() # 禁用雾效果
|
||
|
||
handle_node.setBin("fixed",41)
|
||
handle_node.setDepthWrite(False)
|
||
handle_node.setDepthTest(False)
|
||
|
||
# 保存材质引用以便后续修改
|
||
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", 40)
|
||
axis_node.setDepthWrite(False)
|
||
axis_node.setDepthTest(False)
|
||
|
||
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 setGizmoAxisColor(self, axis, color):
|
||
"""使用材质设置坐标轴颜色 - RenderPipeline兼容版本"""
|
||
return
|
||
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]
|
||
|
||
if axis_node.isEmpty():
|
||
return
|
||
|
||
handle_node = None
|
||
handle_node = axis_node.find("x_handle") if axis == "x" else handle_node
|
||
handle_node = axis_node.find("y_handle") if axis == "y" else handle_node
|
||
handle_node = axis_node.find("z_handle") if axis == "z" else handle_node
|
||
|
||
# 如果找不到特定名称的节点,尝试查找任何子节点
|
||
if not handle_node or handle_node.isEmpty():
|
||
children = axis_node.getChildren()
|
||
if children.getNumPaths() > 0:
|
||
handle_node = children[0]
|
||
|
||
if not handle_node:
|
||
# print(f"未找到{axis}轴的处理模型")
|
||
return
|
||
|
||
# 创建或获取材质
|
||
mat = Material()
|
||
|
||
adjusted_color = Vec4(
|
||
min(color[0], 1.0),
|
||
min(color[1], 1.0),
|
||
min(color[2], 1.0),
|
||
color[3]
|
||
)
|
||
|
||
mat.setBaseColor(adjusted_color)
|
||
#mat.setEmission(Vec4(1, 1, 1, 1.0)) # 自发光
|
||
|
||
# 应用材质
|
||
handle_node.setMaterial(mat, 1)
|
||
|
||
# 设置透明度
|
||
if color[3] < 1.0:
|
||
handle_node.setTransparency(TransparencyAttrib.MAlpha)
|
||
else:
|
||
handle_node.setTransparency(TransparencyAttrib.MNone)
|
||
|
||
handle_node.setLightOff() # 禁用光照影响
|
||
handle_node.setShaderOff() # 禁用着色器
|
||
handle_node.setFogOff() # 禁用雾效果
|
||
|
||
handle_node.setBin("fixed",41)
|
||
handle_node.setDepthWrite(False)
|
||
handle_node.setDepthTest(False)
|
||
|
||
# 保存材质引用以便后续修改
|
||
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", 40)
|
||
axis_node.setDepthWrite(False)
|
||
axis_node.setDepthTest(False)
|
||
|
||
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)
|
||
|
||
self.gizmoHighlightAxis = axis
|
||
|
||
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)
|
||
|
||
if self.gizmoHighlightAxis == axis:
|
||
self.gizmoHighlightAxis = None
|
||
|
||
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):
|
||
"""使用屏幕空间检测是否点击了坐标轴"""
|
||
return None
|
||
if not self._has_legacy_gizmo_input():
|
||
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 = 15 # 增大检测范围
|
||
|
||
# 检测各个轴,对于端点在屏幕外的轴提供回退方案
|
||
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):
|
||
"""备用检测方法:使用固定的屏幕区域"""
|
||
return None
|
||
|
||
# 获取准确的窗口尺寸
|
||
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._has_legacy_gizmo_input():
|
||
return None
|
||
|
||
try:
|
||
# 获取坐标轴中心的世界坐标
|
||
gizmo_world_pos = self.gizmo.getPos(self.world.render)
|
||
|
||
#获取坐标轴的世界朝向(考虑旋转)
|
||
gizmo_world_quat = self.gizmo.getQuat(self.world.render)
|
||
|
||
#计算各轴在世界坐标系中的实际方向向量
|
||
x_axis_world = gizmo_world_quat.xform(Vec3(1,0,0))
|
||
y_axis_world = gizmo_world_quat.xform(Vec3(0,1,0))
|
||
z_axis_world = gizmo_world_quat.xform(Vec3(0,0,1))
|
||
|
||
x_end = gizmo_world_pos + x_axis_world * self.axis_length
|
||
y_end = gizmo_world_pos + y_axis_world * self.axis_length
|
||
z_end = gizmo_world_pos + z_axis_world * self.axis_length
|
||
|
||
|
||
# 计算各轴端点的世界坐标
|
||
# 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):
|
||
"""更新坐标轴高亮状态"""
|
||
self.gizmoHighlightAxis = None
|
||
self._resetCursor()
|
||
return
|
||
if not self._has_legacy_gizmo_input() or self.isDraggingGizmo:
|
||
self._resetCursor()
|
||
return
|
||
|
||
# 使用碰撞检测方法
|
||
#hoveredAxis = self.detectGizmoAxisWithCollision(mouseX, mouseY)
|
||
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._setCursor("pointing_hand")
|
||
else:
|
||
# 如果没有悬停在任何轴上,确保所有轴都恢复原始颜色
|
||
for axis_name in ["x", "y", "z"]:
|
||
if axis_name != self.dragGizmoAxis: # 不要改变正在拖拽的轴的颜色
|
||
self.setGizmoAxisColor(axis_name, self.gizmo_colors[axis_name])
|
||
self._resetCursor()
|
||
|
||
self.gizmoHighlightAxis = hoveredAxis
|
||
|
||
self._last_detected_axis = hoveredAxis
|
||
elif hoveredAxis is None:
|
||
self._resetCursor()
|
||
|
||
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):
|
||
"""开始坐标轴拖拽"""
|
||
return
|
||
try:
|
||
if not self._has_legacy_gizmo_input():
|
||
return
|
||
# 确保状态正确初始化
|
||
if not self.gizmoTarget:
|
||
print("开始拖拽失败: 没有拖拽目标")
|
||
return
|
||
if not self.gizmo:
|
||
print("开始拖拽失败: 没有坐标轴")
|
||
return
|
||
|
||
self.isDraggingGizmo = True
|
||
|
||
# 使用当前高亮的轴,如果有的话;否则使用传入的轴
|
||
if self.gizmoHighlightAxis:
|
||
self.dragGizmoAxis = self.gizmoHighlightAxis
|
||
elif axis and axis in self.gizmo_colors:
|
||
self.dragGizmoAxis = axis
|
||
else:
|
||
# 如果没有明确指定轴,尝试通过鼠标位置检测
|
||
self.dragGizmoAxis = self.detectGizmoAxisAtMouse(mouseX, mouseY)
|
||
|
||
# 启动拖拽更新任务
|
||
if hasattr(self.world, 'taskMgr') and self.world.taskMgr:
|
||
self.world.taskMgr.add(self._dragUpdateTask, "gizmoDragUpdate")
|
||
|
||
# 如果仍然无法确定拖拽轴,则取消拖拽
|
||
if not self.dragGizmoAxis:
|
||
print("开始拖拽失败: 无法确定拖拽轴")
|
||
self.isDraggingGizmo = False
|
||
return
|
||
|
||
self.dragStartMousePos = (mouseX, mouseY)
|
||
|
||
light_object = self.gizmoTarget.getPythonTag("rp_light_object")
|
||
if light_object:
|
||
# 起始位置统一使用节点世界坐标,避免依赖 light_object.pos 的陈旧值
|
||
self.gizmoTargetStartPos = Point3(self.gizmoTarget.getPos(self.world.render))
|
||
else:
|
||
self.gizmoTargetStartPos = self.gizmoTarget.getPos()
|
||
|
||
# 保存开始拖拽时目标节点的位置和坐标轴的位置
|
||
#self.gizmoTargetStartPos = self.gizmoTarget.getPos()
|
||
self.gizmoStartPos = self.gizmo.getPos(self.world.render) # 坐标轴的世界位置
|
||
|
||
# 添加对缩放的支持:保存初始缩放值
|
||
if self.world.tool_manager.isScaleTool():
|
||
self.gizmoTargetStartScale = self.gizmoTarget.getScale()
|
||
elif self.world.tool_manager.isRotateTool():
|
||
self.gizmoTargetStartHpr = self.gizmoTarget.getHpr()
|
||
|
||
# 确保正在拖动的轴保持高亮状态
|
||
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])
|
||
# elif axis and axis in self.gizmo_colors:
|
||
# for axis_name in self.gizmo_colors.keys():
|
||
# if axis_name != axis:
|
||
# self.setGizmoAxisColor(axis_name, self.gizmo_colors[axis_name])
|
||
#
|
||
# self.setGizmoAxisColor(axis, self.gizmo_highlight_colors[axis])
|
||
# self.dragGizmoAxis = axis
|
||
#
|
||
# self.gizmoHighlightAxis = self.dragGizmoAxis
|
||
# 设置拖拽光标
|
||
if self.dragGizmoAxis == "x":
|
||
self._setCursor("size_all") # 水平调整光标
|
||
elif self.dragGizmoAxis == "y":
|
||
self._setCursor("size_all") # 垂直调整光标
|
||
elif self.dragGizmoAxis == "z":
|
||
self._setCursor("size_all") # 全向调整光标
|
||
|
||
print(
|
||
f"开始拖拽 {self.dragGizmoAxis} 轴 - 目标起始位置: {self.gizmoTargetStartPos}, 坐标轴位置: {self.gizmoStartPos}, 鼠标: ({mouseX}, {mouseY})")
|
||
|
||
except Exception as e:
|
||
print(f"开始坐标轴拖拽失败: {str(e)}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
|
||
def _validate_gizmo_drag_state(self):
|
||
if not self.isDraggingGizmo:
|
||
print("拖拽更新失败: 不在拖拽状态")
|
||
return False
|
||
if not self.gizmoTarget:
|
||
print("拖拽更新失败: 没有拖拽目标")
|
||
return False
|
||
if not hasattr(self, 'dragStartMousePos') or not self.dragStartMousePos:
|
||
print("拖拽更新失败: 没有拖拽起始位置")
|
||
return False
|
||
if not hasattr(self, 'gizmoTargetStartPos') or not self.gizmoTargetStartPos:
|
||
print("拖拽更新失败: 没有目标起始位置")
|
||
return False
|
||
if not hasattr(self, 'gizmoStartPos') or not self.gizmoStartPos:
|
||
print("拖拽更新失败: 没有坐标轴起始位置")
|
||
return False
|
||
return True
|
||
|
||
def _refresh_gizmo_target_panel(self):
|
||
if hasattr(self.world, 'property_panel') and self.world.property_panel:
|
||
self.world.property_panel.refreshModelValues(self.gizmoTarget)
|
||
|
||
def _apply_scale_drag(self, mouse_delta_x, mouse_delta_y, is_gui_element):
|
||
scale_factor = 1.0 + (mouse_delta_x + mouse_delta_y) * 0.01
|
||
scale_factor = max(0.001, scale_factor)
|
||
start_scale = getattr(self, 'gizmoTargetStartScale', Vec3(1, 1, 1))
|
||
|
||
if is_gui_element:
|
||
if self.dragGizmoAxis == "x":
|
||
new_scale = Vec3(start_scale.x * scale_factor, start_scale.y, start_scale.z)
|
||
elif self.dragGizmoAxis == "y":
|
||
new_scale = Vec3(start_scale.x, start_scale.y * scale_factor, start_scale.z)
|
||
elif self.dragGizmoAxis == "z":
|
||
new_scale = Vec3(start_scale.x, start_scale.y, start_scale.z * scale_factor)
|
||
else:
|
||
new_scale = Vec3(
|
||
start_scale.x * scale_factor,
|
||
start_scale.y * scale_factor,
|
||
start_scale.z * scale_factor,
|
||
)
|
||
else:
|
||
if self.dragGizmoAxis == "x":
|
||
new_scale = Vec3(start_scale.x * scale_factor, start_scale.y, start_scale.z)
|
||
elif self.dragGizmoAxis == "y":
|
||
new_scale = Vec3(start_scale.x, start_scale.y * scale_factor, start_scale.z)
|
||
elif self.dragGizmoAxis == "z":
|
||
z_scale_factor = 1.0 - (mouse_delta_x + mouse_delta_y) * 0.01
|
||
new_scale = Vec3(start_scale.x, start_scale.y, start_scale.z * z_scale_factor)
|
||
else:
|
||
new_scale = Vec3(
|
||
start_scale.x * scale_factor,
|
||
start_scale.y * scale_factor,
|
||
start_scale.z * scale_factor,
|
||
)
|
||
|
||
new_scale = Vec3(
|
||
max(0.001, new_scale.x),
|
||
max(0.001, new_scale.y),
|
||
max(0.001, new_scale.z),
|
||
)
|
||
self.gizmoTarget.setScale(new_scale)
|
||
self._refresh_gizmo_target_panel()
|
||
|
||
def _apply_rotate_drag(self, mouse_delta_x, mouse_delta_y):
|
||
rotation_speed = 0.5
|
||
rotation_amount = (mouse_delta_x + mouse_delta_y) * rotation_speed
|
||
start_hpr = getattr(self, 'gizmoTargetStartHpr', self.gizmoTarget.getHpr())
|
||
|
||
if self.dragGizmoAxis == "x":
|
||
new_hpr = Vec3(start_hpr.x + rotation_amount, start_hpr.y, start_hpr.z)
|
||
elif self.dragGizmoAxis == "y":
|
||
new_hpr = Vec3(start_hpr.x, start_hpr.y - rotation_amount, start_hpr.z)
|
||
elif self.dragGizmoAxis == "z":
|
||
new_hpr = Vec3(start_hpr.x, start_hpr.y, start_hpr.z + rotation_amount)
|
||
else:
|
||
new_hpr = Vec3(
|
||
start_hpr.x + rotation_amount,
|
||
start_hpr.y + rotation_amount,
|
||
start_hpr.z + rotation_amount,
|
||
)
|
||
|
||
self.gizmoTarget.setHpr(new_hpr)
|
||
self._refresh_gizmo_target_panel()
|
||
|
||
def _get_local_axis_vector(self):
|
||
if self.dragGizmoAxis == "x":
|
||
return Vec3(1, 0, 0)
|
||
if self.dragGizmoAxis == "y":
|
||
return Vec3(0, 1, 0)
|
||
if self.dragGizmoAxis == "z":
|
||
return Vec3(0, 0, 1)
|
||
print(f"拖拽更新失败: 未知轴类型 {self.dragGizmoAxis}")
|
||
return None
|
||
|
||
def _compute_world_axis_vector(self, local_axis_vector):
|
||
world_axis_vector = local_axis_vector
|
||
parent_node = self.gizmoTarget.getParent()
|
||
|
||
if parent_node and parent_node != self.world.render:
|
||
try:
|
||
transfrom_mat = parent_node.getMat(self.world.render)
|
||
if transfrom_mat.is_identity() or self._isMatrixValid(transfrom_mat):
|
||
world_axis_vector = transfrom_mat.xformVec(local_axis_vector)
|
||
else:
|
||
print("警告: 检测到无效变换矩阵,使用默认轴向量")
|
||
except Exception as e:
|
||
print(f"变换计算出错: {e},使用默认轴向量")
|
||
|
||
return world_axis_vector
|
||
|
||
def _world_to_screen(self, world_pos):
|
||
try:
|
||
cam_pos = self.world.cam.getRelativePoint(self.world.render, world_pos)
|
||
if cam_pos.getY() <= 0:
|
||
return None
|
||
|
||
screen_pos = Point2()
|
||
if self.world.cam.node().getLens().project(cam_pos, screen_pos):
|
||
win_width, win_height = self.world.getWindowSize()
|
||
win_x = (screen_pos.x + 1) * 0.5 * win_width
|
||
win_y = (1 - screen_pos.y) * 0.5 * win_height
|
||
return win_x, win_y
|
||
return None
|
||
except Exception as e:
|
||
print(f"世界坐标转屏幕坐标失败: {e}")
|
||
return None
|
||
|
||
def _compute_parent_scale_factor(self):
|
||
total_scale_factor = 1.0
|
||
current_node = self.gizmoTarget.getParent()
|
||
|
||
while current_node and current_node != self.world.render:
|
||
try:
|
||
if not current_node.isEmpty():
|
||
node_scale = current_node.getScale()
|
||
if node_scale.x > 0 and node_scale.y > 0 and node_scale.z > 0:
|
||
avg_scale = (node_scale.x + node_scale.y + node_scale.z) / 3.0
|
||
total_scale_factor *= avg_scale
|
||
current_node = current_node.getParent()
|
||
else:
|
||
break
|
||
except Exception:
|
||
break
|
||
return total_scale_factor
|
||
|
||
def _compute_axis_movement_distance(self, mouse_delta_x, mouse_delta_y):
|
||
gizmo_world_pos = self.gizmoStartPos
|
||
local_axis_vector = self._get_local_axis_vector()
|
||
if local_axis_vector is None:
|
||
return None
|
||
|
||
world_axis_vector = self._compute_world_axis_vector(local_axis_vector)
|
||
axis_start_screen = self._world_to_screen(gizmo_world_pos)
|
||
axis_end_world = gizmo_world_pos + world_axis_vector
|
||
axis_end_screen = self._world_to_screen(axis_end_world)
|
||
|
||
if not axis_start_screen or not axis_end_screen:
|
||
print("拖拽更新失败: 无法获取轴线屏幕坐标")
|
||
return None
|
||
|
||
screen_axis_dir = (
|
||
axis_end_screen[0] - axis_start_screen[0],
|
||
axis_end_screen[1] - axis_start_screen[1],
|
||
)
|
||
length = math.sqrt(screen_axis_dir[0] ** 2 + screen_axis_dir[1] ** 2)
|
||
if length <= 0:
|
||
print("拖拽更新失败: 屏幕轴方向长度为0")
|
||
return None
|
||
|
||
screen_axis_dir = (
|
||
screen_axis_dir[0] / length,
|
||
screen_axis_dir[1] / length,
|
||
)
|
||
projected_distance = (
|
||
mouse_delta_x * screen_axis_dir[0] +
|
||
mouse_delta_y * 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]
|
||
win_width, _ = self.world.getWindowSize()
|
||
|
||
pixels_to_world_units = (2 * distance_to_object * math.tan(math.radians(fov / 2))) / win_width
|
||
movement_distance = projected_distance * pixels_to_world_units
|
||
|
||
total_scale_factor = self._compute_parent_scale_factor()
|
||
if total_scale_factor > 0:
|
||
movement_distance = movement_distance / total_scale_factor
|
||
|
||
return movement_distance
|
||
|
||
def _apply_translate_drag(self, movement_distance):
|
||
current_pos = self.gizmoTargetStartPos
|
||
|
||
if self.dragGizmoAxis == "x":
|
||
new_pos = Vec3(current_pos.x + movement_distance, current_pos.y, current_pos.z)
|
||
elif self.dragGizmoAxis == "y":
|
||
new_pos = Vec3(current_pos.x, current_pos.y + movement_distance, current_pos.z)
|
||
elif self.dragGizmoAxis == "z":
|
||
new_pos = Vec3(current_pos.x, current_pos.y, current_pos.z + movement_distance)
|
||
else:
|
||
print(f"未知轴: {self.dragGizmoAxis}")
|
||
return
|
||
|
||
light_object = self.gizmoTarget.getPythonTag("rp_light_object")
|
||
if light_object:
|
||
self.gizmoTarget.setPos(new_pos)
|
||
self._sync_rp_light_position(self.gizmoTarget, light_object)
|
||
print(f"🔄 光源拖拽移动: {current_pos} -> {new_pos}")
|
||
else:
|
||
self.gizmoTarget.setPos(new_pos)
|
||
print(f"🔄 节点拖拽移动: {current_pos} -> {new_pos} (轴: {self.dragGizmoAxis}, 距离: {movement_distance:.3f})")
|
||
|
||
self._refresh_gizmo_target_panel()
|
||
|
||
min_point = Point3()
|
||
max_point = Point3()
|
||
if self.gizmoTarget.calcTightBounds(min_point, max_point, self.world.render):
|
||
center = Point3(
|
||
(min_point.x + max_point.x) * 0.5,
|
||
(min_point.y + max_point.y) * 0.5,
|
||
(min_point.z + max_point.z) * 0.5,
|
||
)
|
||
self.gizmo.setPos(center)
|
||
|
||
self._refresh_gizmo_target_panel()
|
||
|
||
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:
|
||
self._last_drag_debug_time = current_time
|
||
|
||
def updateGizmoDrag(self, mouseX, mouseY):
|
||
"""更新坐标轴拖拽 - 使用正确的坐标系变换,支持旋转后的子节点拖拽"""
|
||
return
|
||
try:
|
||
if not self._validate_gizmo_drag_state():
|
||
return
|
||
|
||
is_scale_tool = self.world.tool_manager.isScaleTool() if self.world.tool_manager else False
|
||
is_rotate_tool = self.world.tool_manager.isRotateTool() if self.world.tool_manager else False
|
||
is_gui_element = (
|
||
hasattr(self.gizmoTarget, 'getTag') and
|
||
self.gizmoTarget.getTag("is_gui_element") == "1"
|
||
)
|
||
|
||
mouse_delta_x = mouseX - self.dragStartMousePos[0]
|
||
mouse_delta_y = mouseY - self.dragStartMousePos[1]
|
||
|
||
if is_scale_tool:
|
||
self._apply_scale_drag(mouse_delta_x, mouse_delta_y, is_gui_element)
|
||
return
|
||
if is_rotate_tool:
|
||
self._apply_rotate_drag(mouse_delta_x, mouse_delta_y)
|
||
return
|
||
|
||
movement_distance = self._compute_axis_movement_distance(mouse_delta_x, mouse_delta_y)
|
||
if movement_distance is None:
|
||
return
|
||
self._apply_translate_drag(movement_distance)
|
||
|
||
except Exception as e:
|
||
print(f"更新坐标轴拖拽失败: {str(e)}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
|
||
def _isMatrixValid(self, matrix):
|
||
"""检查矩阵是否有效,替代 isSingular 方法"""
|
||
try:
|
||
# 检查矩阵元素是否为有效数字
|
||
for i in range(4):
|
||
for j in range(4):
|
||
element = matrix.getCell(i, j)
|
||
# 检查是否为 NaN 或无穷大
|
||
if str(element) == 'nan' or str(element) == 'inf' or str(element) == '-inf':
|
||
return False
|
||
# 检查是否过大
|
||
if abs(element) > 1e10:
|
||
return False
|
||
return True
|
||
except:
|
||
return False
|
||
|
||
def stopGizmoDrag(self):
|
||
"""停止坐标轴拖拽并创建撤销命令"""
|
||
self.isDraggingGizmo = False
|
||
self.dragGizmoAxis = None
|
||
self.dragStartMousePos = None
|
||
self.gizmoTargetStartPos = None
|
||
self.gizmoTargetStartScale = None
|
||
self.gizmoTargetStartHpr = None
|
||
self.gizmoStartPos = None
|
||
self.gizmoHighlightAxis = None
|
||
self._resetCursor()
|
||
return
|
||
if not self.isDraggingGizmo:
|
||
return
|
||
|
||
if not self._has_legacy_gizmo_input():
|
||
self.isDraggingGizmo = False
|
||
self.dragGizmoAxis = None
|
||
self.dragStartMousePos = None
|
||
self.gizmoTargetStartPos = None
|
||
self.gizmoTargetStartScale = None
|
||
self.gizmoTargetStartHpr = None
|
||
self.gizmoStartPos = None
|
||
self.gizmoHighlightAxis = None
|
||
self._resetCursor()
|
||
return
|
||
|
||
print(f"停止坐标轴拖拽 - 轴: {self.dragGizmoAxis}")
|
||
|
||
# 移除拖拽更新任务
|
||
if hasattr(self.world, 'taskMgr') and self.world.taskMgr:
|
||
self.world.taskMgr.remove("gizmoDragUpdate")
|
||
|
||
# 创建撤销命令
|
||
if hasattr(self.world,'command_manager') and self.world.command_manager and self.gizmoTarget:
|
||
try:
|
||
# 检查是否是移动操作
|
||
if (hasattr(self,'gizmoTargetStartPos') and self.gizmoTargetStartPos):
|
||
current_pos = self.gizmoTarget.getPos()
|
||
if (abs(current_pos.x-self.gizmoTargetStartPos.x)>0.001 or
|
||
abs(current_pos.y-self.gizmoTargetStartPos.y)>0.001 or
|
||
abs(current_pos.z-self.gizmoTargetStartPos.z)>0.001):
|
||
from core.Command_System import MoveNodeCommand, MoveLightCommand
|
||
|
||
light_object = self.gizmoTarget.getPythonTag("rp_light_object")
|
||
if light_object:
|
||
command = MoveLightCommand(self.gizmoTarget,self.gizmoTargetStartPos,current_pos,light_object)
|
||
else:
|
||
command = MoveNodeCommand(self.gizmoTarget,self.gizmoTargetStartPos,current_pos)
|
||
self.world.command_manager.execute_command(command)
|
||
print(f"创建移动命令: {self.gizmoTargetStartPos} -> {current_pos}")
|
||
|
||
# 检查是否是缩放操作
|
||
if (hasattr(self, 'gizmoTargetStartScale') and self.gizmoTargetStartScale):
|
||
current_scale = self.gizmoTarget.getScale()
|
||
if (abs(current_scale.x - self.gizmoTargetStartScale.x) > 0.001 or
|
||
abs(current_scale.y - self.gizmoTargetStartScale.y) > 0.001 or
|
||
abs(current_scale.z - self.gizmoTargetStartScale.z) > 0.001):
|
||
from core.Command_System import ScaleNodeCommand
|
||
command = ScaleNodeCommand(self.gizmoTarget, self.gizmoTargetStartScale, current_scale)
|
||
self.world.command_manager.execute_command(command)
|
||
print(f"创建缩放命令: {self.gizmoTargetStartScale} -> {current_scale}")
|
||
|
||
# 检查是否是旋转操作
|
||
if (hasattr(self, 'gizmoTargetStartHpr') and self.gizmoTargetStartHpr):
|
||
current_hpr = self.gizmoTarget.getHpr()
|
||
if (abs(current_hpr.x - self.gizmoTargetStartHpr.x) > 0.001 or
|
||
abs(current_hpr.y - self.gizmoTargetStartHpr.y) > 0.001 or
|
||
abs(current_hpr.z - self.gizmoTargetStartHpr.z) > 0.001):
|
||
from core.Command_System import RotateNodeCommand
|
||
command = RotateNodeCommand(self.gizmoTarget, self.gizmoTargetStartHpr, current_hpr)
|
||
self.world.command_manager.execute_command(command)
|
||
print(f"创建旋转命令: {self.gizmoTargetStartHpr} -> {current_hpr}")
|
||
print(f"创建旋转命令: {self.gizmoTargetStartHpr} -> {current_hpr}")
|
||
|
||
except Exception as e:
|
||
print(f"创建撤销命令时出错: {e}")
|
||
|
||
# 同步碰撞体
|
||
try:
|
||
target = self.gizmoTarget
|
||
if target:
|
||
# 寻找它的所属模型根节点
|
||
root_model = target
|
||
while root_model and root_model != self.world.render:
|
||
model_list = self.world.models if hasattr(self.world, 'models') else []
|
||
if root_model in model_list or root_model.hasTag('is_model_root'):
|
||
break
|
||
root_model = root_model.getParent()
|
||
|
||
# 如果这个节点属于某个模型,或者是模型自己,更新该模型的碰撞边界
|
||
if root_model and hasattr(self.world, 'models') and root_model in self.world.models:
|
||
if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, 'refreshCollisionBounds'):
|
||
self.world.scene_manager.refreshCollisionBounds(root_model)
|
||
except Exception as e:
|
||
print(f"同步模型碰撞体失败: {e}")
|
||
|
||
# 恢复所有轴的颜色
|
||
for axis_name in ["x", "y", "z"]:
|
||
self.setGizmoAxisColor(axis_name, self.gizmo_colors[axis_name])
|
||
|
||
# 重置拖拽状态
|
||
self.isDraggingGizmo = False
|
||
self.dragGizmoAxis = None
|
||
self.dragStartMousePos = None
|
||
# 清理拖拽状态,下次拖拽开始时重新设置
|
||
self.gizmoTargetStartPos = None
|
||
self.gizmoTargetStartScale = None
|
||
self.gizmoTargetStartHpr = None
|
||
self.gizmoStartPos = None
|
||
|
||
def _dragUpdateTask(self, task):
|
||
"""拖拽更新任务 - 持续更新拖拽状态"""
|
||
return task.done
|
||
try:
|
||
if not self.isDraggingGizmo:
|
||
return task.done
|
||
|
||
# 检查鼠标是否仍然按下
|
||
if not self.world.mouseWatcherNode.hasMouse():
|
||
return task.cont
|
||
|
||
# 获取当前鼠标位置
|
||
mouse_x = self.world.mouseWatcherNode.getMouseX()
|
||
mouse_y = self.world.mouseWatcherNode.getMouseY()
|
||
|
||
# 转换为窗口坐标
|
||
winWidth, winHeight = self.world.getWindowSize()
|
||
window_x = (mouse_x + 1) * 0.5 * winWidth
|
||
window_y = (1 - mouse_y) * 0.5 * winHeight
|
||
|
||
# 调用拖拽更新
|
||
self.updateGizmoDrag(window_x, window_y)
|
||
|
||
return task.cont
|
||
|
||
except Exception as e:
|
||
print(f"拖拽更新任务错误: {e}")
|
||
return task.done
|
||
if hasattr(self, 'gizmoTargetStartScale'):
|
||
delattr(self, 'gizmoTargetStartScale')
|
||
if hasattr(self, 'gizmoTargetStartHpr'):
|
||
delattr(self, 'gizmoTargetStartHpr')
|
||
|
||
# 重置高亮轴
|
||
self.gizmoHighlightAxis = None
|
||
self._resetCursor()
|
||
# ==================== 选择管理 ====================
|
||
|
||
def updateSelection(self, nodePath):
|
||
try:
|
||
if self._same_valid_node(self.selectedNode, nodePath):
|
||
return
|
||
#print(f"\n=== 更新选择状态 ===")
|
||
|
||
# 如果正在删除节点,避免更新选择
|
||
if hasattr(self, '_deleting_node') and self._deleting_node:
|
||
print("正在删除节点,跳过选择更新")
|
||
#print("=== 选择状态更新完成 ===\n")
|
||
return
|
||
|
||
node_name = "None"
|
||
if self._is_valid_node(nodePath, require_attached=True):
|
||
node_name = nodePath.getName()
|
||
#print(f"新选择的节点: {node_name}")
|
||
|
||
ssbo_editor = getattr(self.world, "ssbo_editor", None)
|
||
if ssbo_editor:
|
||
try:
|
||
ssbo_editor.sync_scene_selection(nodePath)
|
||
except Exception as e:
|
||
print(f"同步 SSBO 选择状态失败: {e}")
|
||
|
||
effective_node = self._get_effective_selected_node()
|
||
if effective_node is None:
|
||
effective_node = nodePath
|
||
|
||
self.selectedNode = effective_node
|
||
# 添加兼容性属性
|
||
self.selectedObject = effective_node
|
||
|
||
if self._is_valid_node(effective_node, require_attached=True):
|
||
node_name = effective_node.getName()
|
||
#print(f"开始为节点 {node_name} 创建选择框和坐标轴...")
|
||
|
||
# 创建选择框
|
||
#print("创建选择框...")
|
||
if self.show_selection_box:
|
||
self.createSelectionBox(effective_node)
|
||
else:
|
||
self.clearSelectionBox()
|
||
|
||
if (not self.show_selection_box) or self.selectionBox:
|
||
box_name = "Unknown"
|
||
if self.selectionBox and not self.selectionBox.isEmpty():
|
||
box_name = self.selectionBox.getName()
|
||
#print(f"✓ 选择框创建成功: {box_name}")
|
||
else:
|
||
print("× 选择框创建失败")
|
||
|
||
# 创建坐标轴
|
||
#print("创建坐标轴...")
|
||
self._updateSelectionOutline(effective_node)
|
||
self.createGizmo(effective_node)
|
||
if self._has_attached_transform_gizmo(effective_node):
|
||
gizmo_name = "Unknown"
|
||
if self._has_new_transform_gizmo():
|
||
gizmo_name = getattr(effective_node, "getName", lambda: "TransformGizmo")()
|
||
elif self.gizmo and not self.gizmo.isEmpty():
|
||
gizmo_name = self.gizmo.getName()
|
||
#print(f"✓ 坐标轴创建成功: {gizmo_name}")
|
||
else:
|
||
print("× 坐标轴创建失败")
|
||
|
||
else:
|
||
print("清除选择...")
|
||
self.clearSelectionBox()
|
||
self._updateSelectionOutline(None)
|
||
self.clearGizmo()
|
||
print("✓ 取消选择")
|
||
|
||
#当取消选择时,同步清空树形控件的选中状态
|
||
if self._clear_tree_selection():
|
||
print("✓ 树形控件选中状态已清空")
|
||
|
||
#print("=== 选择状态更新完成 ===\n")
|
||
except Exception as e:
|
||
print(f"更新选择状态失败{str(e)}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
|
||
# def _updateSelectionVisuals(self, nodePath):
|
||
# """更新选择的视觉效果(选择框和坐标轴)"""
|
||
# try:
|
||
# if nodePath and not nodePath.isEmpty():
|
||
# node_name = nodePath.getName()
|
||
# print(f"开始为节点 {node_name} 创建选择框和坐标轴...")
|
||
#
|
||
# # 创建选择框
|
||
# print("创建选择框...")
|
||
# self.createSelectionBox(nodePath)
|
||
# if self.selectionBox:
|
||
# box_name = "Unknown"
|
||
# if self.selectionBox and not self.selectionBox.isEmpty():
|
||
# box_name = self.selectionBox.getName()
|
||
# print(f"✓ 选择框创建成功: {box_name}")
|
||
# else:
|
||
# print("× 选择框创建失败")
|
||
#
|
||
# # 创建坐标轴
|
||
# print("创建坐标轴...")
|
||
# self.createGizmo(nodePath)
|
||
# if self.gizmo:
|
||
# gizmo_name = "Unknown"
|
||
# if self.gizmo and not self.gizmo.isEmpty():
|
||
# gizmo_name = self.gizmo.getName()
|
||
# print(f"✓ 坐标轴创建成功: {gizmo_name}")
|
||
# else:
|
||
# print("× 坐标轴创建失败")
|
||
#
|
||
# print(f"✓ 选中了节点: {node_name}")
|
||
# else:
|
||
# print("清除选择...")
|
||
# self.clearSelectionBox()
|
||
# self.clearGizmo()
|
||
# print("✓ 取消选择")
|
||
#
|
||
# except Exception as e:
|
||
# print(f"更新选择视觉效果失败: {e}")
|
||
|
||
def getSelectedNode(self):
|
||
"""获取当前选中的节点"""
|
||
return self._get_effective_selected_node()
|
||
|
||
def deleteSelectedNode(self):
|
||
"""兼容旧接口:删除当前选中节点。"""
|
||
node = self._get_effective_selected_node()
|
||
if not node or node.isEmpty():
|
||
return False
|
||
|
||
try:
|
||
if node.getName() == "render":
|
||
return False
|
||
except Exception:
|
||
return False
|
||
|
||
self._deleting_node = True
|
||
try:
|
||
# 优先走应用层统一删除链路(命令系统/SSBO清理/消息等)。
|
||
if hasattr(self.world, "_delete_node") and callable(self.world._delete_node):
|
||
deleted = bool(self.world._delete_node(node))
|
||
else:
|
||
deleted = False
|
||
scene_manager = getattr(self.world, "scene_manager", None)
|
||
if scene_manager and hasattr(scene_manager, "models") and node in scene_manager.models:
|
||
scene_manager.models.remove(node)
|
||
try:
|
||
node.removeNode()
|
||
deleted = True
|
||
except Exception:
|
||
deleted = False
|
||
|
||
if deleted:
|
||
self.clearSelection()
|
||
return deleted
|
||
finally:
|
||
self._deleting_node = False
|
||
|
||
def hasSelection(self):
|
||
"""检查是否有选中的节点"""
|
||
return self._get_effective_selected_node() is not None
|
||
|
||
def checkAndClearIfTargetDeleted(self):
|
||
if self.gizmoTarget is not None and (not self._is_valid_node(self.gizmoTarget, require_attached=True)):
|
||
self.clearGizmo()
|
||
|
||
if self.selectionBoxTarget is not None and (not self._is_valid_node(self.selectionBoxTarget, require_attached=True)):
|
||
self.clearSelectionBox()
|
||
|
||
if self.selectedNode is not None and (not self._is_valid_node(self.selectedNode, require_attached=True)):
|
||
self.selectedNode = None
|
||
self.selectedObject = None
|
||
self._updateSelectionOutline(None)
|
||
|
||
def setupGizmoCollision(self):
|
||
return
|
||
if not self.gizmo or not self.gizmoXAxis or not self.gizmoYAxis or not self.gizmoZAxis:
|
||
return
|
||
|
||
# 清除现有的碰撞节点
|
||
for axis_name in ["x", "y", "z"]:
|
||
axis_node = getattr(self, f"gizmo{axis_name.upper()}Axis")
|
||
if axis_node:
|
||
# 查找并移除所有现有的碰撞节点
|
||
collision_nodes = axis_node.findAllMatches("**/gizmo_collision_*")
|
||
for collision_node in collision_nodes:
|
||
collision_node.removeNode()
|
||
|
||
# 为每个轴创建碰撞体
|
||
self.createAxisCollision("x", self.gizmoXAxis)
|
||
self.createAxisCollision("y", self.gizmoYAxis)
|
||
self.createAxisCollision("z", self.gizmoZAxis)
|
||
|
||
def createAxisCollision(self, axis_name, axis_node):
|
||
return
|
||
# 为单个轴创建碰撞体
|
||
try:
|
||
handle_node = axis_node.find(f"{axis_name}_handle")
|
||
if not handle_node or handle_node.isEmpty():
|
||
children = axis_node.getChildren()
|
||
if children.getNumPaths() > 0:
|
||
handle_node = children[0]
|
||
else:
|
||
print(f"警告: 未找到 {axis_name} 轴的 handle 节点")
|
||
return
|
||
|
||
collision_node = CollisionNode(f"gizmo_collision_{axis_name}")
|
||
collision_node.setIntoCollideMask(BitMask32.bit(1)) # 设置为into对象
|
||
collision_node.setFromCollideMask(BitMask32.allOff()) # 不作为from对象
|
||
|
||
# 调整碰撞尺寸以匹配实际的轴长度和坐标轴缩放
|
||
scale_factor = self.gizmo.getScale().x if self.gizmo else 1.0
|
||
axis_length = 2.0 * scale_factor
|
||
radius = 0.3 * scale_factor
|
||
|
||
# 根据轴的类型创建合适的碰撞体
|
||
if axis_name == "x":
|
||
capsule = CollisionCapsule(
|
||
Point3(0, 0, 0),
|
||
Point3(axis_length, 0, 0),
|
||
radius
|
||
)
|
||
collision_node.addSolid(capsule)
|
||
elif axis_name == "y":
|
||
capsule = CollisionCapsule(
|
||
Point3(0, 0, 0),
|
||
Point3(0, axis_length, 0),
|
||
radius
|
||
)
|
||
collision_node.addSolid(capsule)
|
||
elif axis_name == "z":
|
||
capsule = CollisionCapsule(
|
||
Point3(0, 0, 0),
|
||
Point3(0, 0, axis_length),
|
||
radius
|
||
)
|
||
collision_node.addSolid(capsule)
|
||
|
||
# 将碰撞节点附加到handle节点,使其与可视化几何体保持一致
|
||
collision_np = handle_node.attachNewNode(collision_node)
|
||
|
||
# 设置标签以便识别
|
||
collision_np.setTag("gizmo_axis", axis_name)
|
||
collision_np.setTag("pickable", "1")
|
||
|
||
collision_np.hide() # 隐藏碰撞体,只用于检测
|
||
|
||
#print(f"✓ 成功创建 {axis_name} 轴碰撞体")
|
||
|
||
except Exception as e:
|
||
print(f"创建{axis_name}轴碰撞体失败: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
|
||
def detectGizmoAxisWithCollision(self, mouseX, mouseY):
|
||
return None
|
||
# 使用碰撞体检测鼠标是否悬停在坐标轴上
|
||
if not self.gizmo:
|
||
return None
|
||
|
||
try:
|
||
ray = CollisionRay()
|
||
|
||
win_width, win_height = self.world.getWindowSize()
|
||
|
||
mouse_x_ndc = (mouseX / win_width) * 2.0 - 1.0
|
||
mouse_y_ndc = 1.0 - (mouseY / win_height) * 2.0
|
||
|
||
ray.setFromLens(self.world.cam.node(), mouse_x_ndc, mouse_y_ndc)
|
||
|
||
traverser = CollisionTraverser("gizmo_traverser")
|
||
handler = CollisionHandlerQueue()
|
||
|
||
# 创建射线节点
|
||
ray_node = CollisionNode('mouseRay')
|
||
ray_node.addSolid(ray)
|
||
ray_node.setFromCollideMask(BitMask32.bit(1)) # 射线作为from对象
|
||
ray_node.setIntoCollideMask(BitMask32.allOff()) # 射线不作为into对象
|
||
ray_np = self.world.render.attachNewNode(ray_node)
|
||
|
||
# 为所有轴的碰撞体设置正确的掩码并添加到遍历器
|
||
collision_found = False
|
||
for axis_name in ["x", "y", "z"]:
|
||
axis_node = getattr(self, f"gizmo{axis_name.upper()}Axis")
|
||
if axis_node:
|
||
collision_node_path = axis_node.find("**/gizmo_collision_*")
|
||
if not collision_node_path.isEmpty():
|
||
collision_node = collision_node_path.node()
|
||
collision_node.setFromCollideMask(BitMask32.allOff()) # 碰撞体不作为from对象
|
||
collision_node.setIntoCollideMask(BitMask32.bit(1)) # 碰撞体作为into对象
|
||
collision_found = True
|
||
|
||
if not collision_found:
|
||
ray_np.removeNode()
|
||
return None
|
||
|
||
# 执行碰撞检测 - 这里是关键修复点
|
||
traverser.addCollider(ray_np, handler)
|
||
traverser.traverse(self.world.render)
|
||
|
||
ray_np.removeNode()
|
||
|
||
# 检查是否有碰撞
|
||
if handler.getNumEntries() > 0:
|
||
handler.sortEntries()
|
||
closest_entry = handler.getEntry(0)
|
||
|
||
# 获取碰撞的对象
|
||
collided_object = closest_entry.getIntoNodePath()
|
||
axis_tag = collided_object.getTag("gizmo_axis")
|
||
|
||
if axis_tag in ["x", "y", "z"]:
|
||
return axis_tag
|
||
|
||
return None
|
||
|
||
except Exception as e:
|
||
print(f"使用碰撞体检测坐标轴失败: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
return None
|
||
|
||
def debugGizmoCollision(self):
|
||
return
|
||
print("===碰撞体调试信息===")
|
||
for axis_name in ["x","y","z"]:
|
||
axis_node = getattr(self,f"gizmo{axis_name.upper()}Axis")
|
||
if axis_node:
|
||
handle_node = axis_node.find(f"{axis_name}_handle")
|
||
collision_node = axis_node.find("**/gizmo_collision_*")
|
||
print(f"{axis_name.upper()}轴:")
|
||
print(f" - 轴节点: {axis_node}")
|
||
print(f" - Handle节点: {handle_node}")
|
||
print(f" - 碰撞节点: {collision_node}")
|
||
if not collision_node.isEmpty():
|
||
print(f" - 碰撞体标签: {collision_node.getTag('gizmo_axis')}")
|
||
else:
|
||
print(f"{axis_name.upper()}轴节点不存在")
|
||
|
||
def focusCameraOnSelectedNodeAdvanced(self):
|
||
"""高级版的摄像机聚焦功能,包含平滑动画效果"""
|
||
try:
|
||
selected_node = self._get_effective_selected_node()
|
||
if not selected_node or selected_node.isEmpty():
|
||
print("没有选中的节点,无法聚焦")
|
||
return False
|
||
|
||
# 获取选中节点的边界框
|
||
minPoint = Point3()
|
||
maxPoint = Point3()
|
||
|
||
if not selected_node.calcTightBounds(minPoint, maxPoint, self.world.render):
|
||
print("无法计算选中节点的边界框,使用节点为位置作为替代方案")
|
||
node_pos = selected_node.getPos(self.world.render)
|
||
optimal_distance = 10.0
|
||
current_cam_pos = self.world.cam.getPos()
|
||
view_direction = node_pos - current_cam_pos
|
||
if view_direction.length()<0.001:
|
||
view_direction = Vec3(5,-5,2)
|
||
view_direction.normalize()
|
||
target_cam_pos = node_pos - (view_direction * optimal_distance)
|
||
|
||
temp_node =self.world.render.attachNewNode("temp_lookat_target")
|
||
temp_node.setPos(node_pos)
|
||
dummy_cam = self.world.render.attachNewNode("dummy_camera")
|
||
dummy_cam.setPos(target_cam_pos)
|
||
dummy_cam.lookAt(temp_node)
|
||
target_cam_hpr = Vec3(dummy_cam.getHpr())
|
||
|
||
temp_node.removeNode()
|
||
dummy_cam.removeNode()
|
||
|
||
currrent_cam_pos = Point3(self.world.cam.getPos())
|
||
current_cam_hpr = Vec3(self.world.cam.getHpr())
|
||
self._startCameraFocusAnimation(current_cam_pos,target_cam_pos,current_cam_hpr,target_cam_hpr)
|
||
print(f"开始聚焦到节点(使用位置): {selected_node.getName()}")
|
||
return True
|
||
|
||
# 计算节点中心点和大小
|
||
center = Point3(
|
||
(minPoint.x + maxPoint.x) * 0.5,
|
||
(minPoint.y + maxPoint.y) * 0.5,
|
||
(minPoint.z + maxPoint.z) * 0.5
|
||
)
|
||
|
||
# 计算节点的对角线长度
|
||
size = (maxPoint - minPoint).length()
|
||
|
||
# 如果节点太小,使用默认大小
|
||
if size < 0.01:
|
||
size = 1.0
|
||
|
||
# 获取当前摄像机位置和朝向
|
||
current_cam_pos = Point3(self.world.cam.getPos())
|
||
current_cam_hpr = Vec3(self.world.cam.getHpr())
|
||
|
||
# 计算观察方向
|
||
view_direction = current_cam_pos - center
|
||
if view_direction.length() < 0.001:
|
||
view_direction = Vec3(5, -5, 2)
|
||
|
||
view_direction.normalize()
|
||
|
||
# 计算合适的观察距离
|
||
optimal_distance = max(size * 1, 1.0)
|
||
|
||
# 计算目标摄像机位置
|
||
target_cam_pos = center + (view_direction * optimal_distance)
|
||
|
||
# 计算目标朝向(不直接使用lookAt,而是计算目标HPR)
|
||
# 创建临时节点用于计算目标朝向
|
||
temp_node = self.world.render.attachNewNode("temp_lookat_target")
|
||
temp_node.setPos(center)
|
||
|
||
# 创建另一个临时节点用于计算朝向
|
||
dummy_cam = self.world.render.attachNewNode("dummy_camera")
|
||
dummy_cam.setPos(target_cam_pos)
|
||
dummy_cam.lookAt(temp_node)
|
||
target_cam_hpr = Vec3(dummy_cam.getHpr())
|
||
|
||
# 清理临时节点
|
||
temp_node.removeNode()
|
||
dummy_cam.removeNode()
|
||
|
||
# 使用任务来实现平滑移动动画
|
||
self._startCameraFocusAnimation(current_cam_pos, target_cam_pos,
|
||
current_cam_hpr, target_cam_hpr)
|
||
|
||
print(f"开始聚焦到节点: {selected_node.getName()}")
|
||
return True
|
||
|
||
except Exception as e:
|
||
print(f"高级聚焦功能失败: {str(e)}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
return False
|
||
|
||
def _startCameraFocusAnimation(self, start_pos, end_pos, start_hpr, end_hpr):
|
||
"""启动摄像机聚焦动画"""
|
||
try:
|
||
# 创建动画任务
|
||
class CameraFocusData:
|
||
def __init__(self, start_pos, end_pos, start_hpr, end_hpr):
|
||
self.start_pos = Point3(start_pos) # 确保是Point3类型
|
||
self.end_pos = Point3(end_pos) # 确保是Point3类型
|
||
self.start_hpr = Vec3(start_hpr) # 确保是Vec3类型
|
||
self.end_hpr = Vec3(end_hpr) # 确保是Vec3类型
|
||
self.elapsed_time = 0.0
|
||
self.duration = 0.8 # 增加动画持续时间到0.8秒,让动画更平滑
|
||
|
||
self._camera_focus_data = CameraFocusData(start_pos, end_pos, start_hpr, end_hpr)
|
||
|
||
# 移除之前的任务(如果存在)
|
||
taskMgr.remove("cameraFocusTask")
|
||
|
||
# 添加新任务
|
||
taskMgr.add(self._cameraFocusTask, "cameraFocusTask")
|
||
|
||
except Exception as e:
|
||
print(f"启动摄像机聚焦动画失败: {e}")
|
||
|
||
def _normalizeAngle(self, angle):
|
||
"""规范化角度到-180到180度之间"""
|
||
while angle > 180:
|
||
angle -= 360
|
||
while angle < -180:
|
||
angle += 360
|
||
return angle
|
||
|
||
def _cameraFocusTask(self, task):
|
||
"""摄像机聚焦动画任务"""
|
||
try:
|
||
if not hasattr(self, '_camera_focus_data'):
|
||
return task.done
|
||
|
||
data = self._camera_focus_data
|
||
data.elapsed_time += globalClock.getDt()
|
||
|
||
# 计算插值因子
|
||
t = min(1.0, data.elapsed_time / data.duration)
|
||
|
||
# 使用更平滑的插值函数
|
||
smooth_t = t * t * (3 - 2 * t) # 平滑步进插值
|
||
|
||
# 手动实现lerp功能
|
||
def lerp_point3(start, end, factor):
|
||
return Point3(
|
||
start.x + (end.x - start.x) * factor,
|
||
start.y + (end.y - start.y) * factor,
|
||
start.z + (end.z - start.z) * factor
|
||
)
|
||
|
||
# 角度插值需要特殊处理,确保选择最短路径
|
||
def lerp_hpr(start, end, factor):
|
||
# 规范化角度
|
||
start_x = self._normalizeAngle(start.x)
|
||
start_y = self._normalizeAngle(start.y)
|
||
start_z = self._normalizeAngle(start.z)
|
||
|
||
end_x = self._normalizeAngle(end.x)
|
||
end_y = self._normalizeAngle(end.y)
|
||
end_z = self._normalizeAngle(end.z)
|
||
|
||
# 计算最短旋转路径
|
||
diff_x = self._normalizeAngle(end_x - start_x)
|
||
diff_y = self._normalizeAngle(end_y - start_y)
|
||
diff_z = self._normalizeAngle(end_z - start_z)
|
||
|
||
# 插值
|
||
result_x = start_x + diff_x * factor
|
||
result_y = start_y + diff_y * factor
|
||
result_z = start_z + diff_z * factor
|
||
|
||
# 再次规范化
|
||
result_x = self._normalizeAngle(result_x)
|
||
result_y = self._normalizeAngle(result_y)
|
||
result_z = self._normalizeAngle(result_z)
|
||
|
||
return Vec3(result_x, result_y, result_z)
|
||
|
||
# 计算当前位置和朝向
|
||
current_pos = lerp_point3(data.start_pos, data.end_pos, smooth_t)
|
||
current_hpr = lerp_hpr(data.start_hpr, data.end_hpr, smooth_t)
|
||
|
||
# 应用到摄像机
|
||
self.world.cam.setPos(current_pos)
|
||
self.world.cam.setHpr(current_hpr)
|
||
|
||
# 检查是否完成
|
||
if t >= 1.0:
|
||
if hasattr(self, '_camera_focus_data'):
|
||
delattr(self, '_camera_focus_data')
|
||
print("摄像机聚焦动画完成")
|
||
return task.done
|
||
|
||
return task.cont
|
||
|
||
except Exception as e:
|
||
print(f"摄像机聚焦任务出错: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
return task.done
|
||
|
||
def _smoothCameraMoveTask(self, task):
|
||
"""平滑摄像机移动任务"""
|
||
try:
|
||
if not hasattr(self, '_smooth_camera_move_data'):
|
||
return task.done
|
||
|
||
data = self._smooth_camera_move_data
|
||
data.elapsed_time += globalClock.getDt()
|
||
|
||
# 计算插值因子
|
||
t = min(1.0, data.elapsed_time / data.duration)
|
||
|
||
# 使用平滑插值
|
||
smooth_t = t * t * (3 - 2 * t)
|
||
|
||
# 角度插值需要特殊处理
|
||
def lerp_hpr(start, end, factor):
|
||
# 规范化角度
|
||
start_x = self._normalizeAngle(start.x)
|
||
start_y = self._normalizeAngle(start.y)
|
||
start_z = self._normalizeAngle(start.z)
|
||
|
||
end_x = self._normalizeAngle(end.x)
|
||
end_y = self._normalizeAngle(end.y)
|
||
end_z = self._normalizeAngle(end.z)
|
||
|
||
# 计算最短旋转路径
|
||
diff_x = self._normalizeAngle(end_x - start_x)
|
||
diff_y = self._normalizeAngle(end_y - start_y)
|
||
diff_z = self._normalizeAngle(end_z - start_z)
|
||
|
||
# 插值
|
||
result_x = start_x + diff_x * factor
|
||
result_y = start_y + diff_y * factor
|
||
result_z = start_z + diff_z * factor
|
||
|
||
return Vec3(result_x, result_y, result_z)
|
||
|
||
# 计算当前位置和朝向
|
||
current_pos = Point3(
|
||
data.start_pos.x + (data.end_pos.x - data.start_pos.x) * smooth_t,
|
||
data.start_pos.y + (data.end_pos.y - data.start_pos.y) * smooth_t,
|
||
data.start_pos.z + (data.end_pos.z - data.start_pos.z) * smooth_t
|
||
)
|
||
|
||
current_hpr = lerp_hpr(data.start_hpr, data.end_hpr, smooth_t)
|
||
|
||
# 应用到摄像机
|
||
self.world.cam.setPos(current_pos)
|
||
self.world.cam.setHpr(current_hpr)
|
||
|
||
# 检查是否完成
|
||
if t >= 1.0:
|
||
if hasattr(self, '_smooth_camera_move_data'):
|
||
delattr(self, '_smooth_camera_move_data')
|
||
print("摄像机平滑移动完成")
|
||
return task.done
|
||
|
||
return task.cont
|
||
|
||
except Exception as e:
|
||
print(f"平滑摄像机移动任务出错: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
return task.done
|
||
|
||
def handleMouseClick(self, nodePath, mouseX=None, mouseY=None):
|
||
"""处理鼠标点击事件 - 支持坐标轴双击聚焦"""
|
||
try:
|
||
# 如果正在删除节点,忽略鼠标点击
|
||
if hasattr(self, '_deleting_node') and self._deleting_node:
|
||
print("正在删除节点,忽略鼠标点击")
|
||
return
|
||
|
||
import time
|
||
current_time = time.time()
|
||
|
||
# 检查是否点击了坐标轴
|
||
is_gizmo_click = False
|
||
target_node = nodePath
|
||
|
||
# 判断是否点击了坐标轴
|
||
if (nodePath and hasattr(nodePath, 'getName') and
|
||
(nodePath.getName().startswith("gizmo") or
|
||
"gizmo" in nodePath.getName().lower() or
|
||
(hasattr(nodePath, 'hasTag') and nodePath.hasTag("is_gizmo")))):
|
||
is_gizmo_click = True
|
||
# 如果有选中的模型,使用选中的模型作为聚焦目标
|
||
selected_node = self._get_effective_selected_node()
|
||
if selected_node and not selected_node.isEmpty():
|
||
target_node = selected_node
|
||
print(f"检测到坐标轴点击,使用目标节点: {target_node.getName() if target_node else 'None'}")
|
||
|
||
# 检查是否为双击(同一节点且在时间阈值内)
|
||
is_double_click = (self._last_clicked_node == target_node and
|
||
target_node is not None and
|
||
current_time - self._last_click_time < self._double_click_threshold)
|
||
|
||
if is_double_click:
|
||
# 双击 detected
|
||
node_name = target_node.getName() if target_node else "None"
|
||
print(f"检测到双击节点: {node_name}")
|
||
|
||
# 无论是点击模型还是坐标轴,都执行聚焦
|
||
if target_node and not target_node.isEmpty():
|
||
print(f"双击聚焦到节点: {target_node.getName()}")
|
||
if self._get_effective_selected_node() != target_node:
|
||
self.updateSelection(target_node)
|
||
else:
|
||
self.focusCameraOnSelectedNodeAdvanced()
|
||
else:
|
||
print("双击事件:没有有效的目标节点")
|
||
|
||
# 重置状态以避免三击等误触发
|
||
self._last_click_time = 0
|
||
self._last_clicked_node = None
|
||
else:
|
||
# 单击,更新状态
|
||
self._last_click_time = current_time
|
||
self._last_clicked_node = target_node
|
||
|
||
# 如果点击的是坐标轴,保持当前选择不变
|
||
if is_gizmo_click:
|
||
print("坐标轴单击,保持当前选择")
|
||
else:
|
||
# 正常的单击选择
|
||
self.updateSelection(nodePath)
|
||
|
||
except Exception as e:
|
||
print(f"处理鼠标点击事件失败: {e}")
|
||
|
||
def _onDoubleClick(self, nodePath):
|
||
"""双击事件处理"""
|
||
try:
|
||
# 获取实际要聚焦的目标节点
|
||
target_node = nodePath
|
||
|
||
# 如果是坐标轴,确保使用关联的模型作为目标
|
||
if (nodePath and hasattr(nodePath, 'hasTag') and
|
||
nodePath.hasTag("is_gizmo")):
|
||
selected_node = self._get_effective_selected_node()
|
||
if selected_node and not selected_node.isEmpty():
|
||
target_node = selected_node
|
||
print(f"坐标轴双击,聚焦到关联模型: {target_node.getName()}")
|
||
else:
|
||
print("坐标轴双击,但没有关联的选中模型")
|
||
return
|
||
|
||
if target_node and not target_node.isEmpty():
|
||
print(f"双击聚焦到节点: {target_node.getName()}")
|
||
# 更新选择(如果需要)
|
||
if self._get_effective_selected_node() != target_node:
|
||
self.updateSelection(target_node)
|
||
|
||
# 执行聚焦
|
||
self.focusCameraOnSelectedNodeAdvanced()
|
||
else:
|
||
print("双击事件:没有有效的目标节点")
|
||
|
||
except Exception as e:
|
||
print(f"双击事件处理失败: {e}")
|
||
|
||
# 添加一个更精确的双击检测方法
|
||
|
||
def checkDoubleClick(self, nodePath):
|
||
"""检查是否为双击,返回布尔值"""
|
||
try:
|
||
import time
|
||
current_time = time.time()
|
||
|
||
# 必须是同一节点且在时间阈值内
|
||
is_double_click = (self._last_clicked_node == nodePath and
|
||
nodePath is not None and
|
||
current_time - self._last_click_time < self._double_click_threshold)
|
||
|
||
if is_double_click:
|
||
# 重置状态
|
||
self._last_click_time = 0
|
||
self._last_clicked_node = None
|
||
return True
|
||
else:
|
||
# 更新状态
|
||
self._last_click_time = current_time
|
||
self._last_clicked_node = nodePath
|
||
return False
|
||
|
||
except Exception as e:
|
||
print(f"双击检测失败: {e}")
|
||
return False
|
||
|
||
# 添加一个定时重置方法,用于清除长时间未完成的双击状态
|
||
|
||
def _resetDoubleClickState(self):
|
||
"""重置双击状态"""
|
||
self._last_click_time = 0
|
||
self._last_clicked_node = None
|
||
|
||
# 添加一个任务来自动重置双击状态
|
||
|
||
def _startDoubleClickResetTask(self):
|
||
"""启动双击状态重置任务"""
|
||
if self._double_click_task:
|
||
taskMgr.remove(self._double_click_task)
|
||
|
||
self._double_click_task = taskMgr.doMethodLater(
|
||
self._double_click_threshold * 2, # 等待2倍阈值时间
|
||
self._resetDoubleClickStateTask,
|
||
"resetDoubleClickState"
|
||
)
|
||
|
||
def _resetDoubleClickStateTask(self, task):
|
||
"""任务:重置双击状态"""
|
||
self._resetDoubleClickState()
|
||
self._double_click_task = None
|
||
return task.done
|
||
|
||
# 修改 updateSelection 方法以集成双击检测
|
||
|
||
# def updateSelection(self, nodePath):
|
||
# """更新选择状态"""
|
||
# print(f"\n=== 更新选择状态 ===")
|
||
#
|
||
# # 如果正在删除节点,避免更新选择
|
||
# if hasattr(self, '_deleting_node') and self._deleting_node:
|
||
# print("正在删除节点,跳过选择更新")
|
||
# print("=== 选择状态更新完成 ===\n")
|
||
# return
|
||
#
|
||
# node_name = "None"
|
||
# if nodePath and not nodePath.isEmpty():
|
||
# node_name = nodePath.getName()
|
||
# print(f"新选择的节点: {node_name}")
|
||
#
|
||
# # 检查是否为双击
|
||
# is_double_click = self.checkDoubleClick(nodePath)
|
||
# if is_double_click:
|
||
# print(f"检测到双击 {node_name},执行聚焦")
|
||
# # 启动聚焦(在下一帧执行,确保选择状态已更新)
|
||
# taskMgr.doMethodLater(0.01, self._delayedFocusTask, "delayedFocus")
|
||
#
|
||
# self.selectedNode = nodePath
|
||
# # 添加兼容性属性
|
||
# self.selectedObject = nodePath
|
||
#
|
||
# if nodePath and not nodePath.isEmpty():
|
||
# node_name = nodePath.getName()
|
||
# print(f"开始为节点 {node_name} 创建选择框和坐标轴...")
|
||
#
|
||
# # 创建选择框
|
||
# print("创建选择框...")
|
||
# self.createSelectionBox(nodePath)
|
||
# if self.selectionBox:
|
||
# box_name = "Unknown"
|
||
# if self.selectionBox and not self.selectionBox.isEmpty():
|
||
# box_name = self.selectionBox.getName()
|
||
# print(f"✓ 选择框创建成功: {box_name}")
|
||
# else:
|
||
# print("× 选择框创建失败")
|
||
#
|
||
# # 创建坐标轴
|
||
# print("创建坐标轴...")
|
||
# self.createGizmo(nodePath)
|
||
# if self.gizmo:
|
||
# gizmo_name = "Unknown"
|
||
# if self.gizmo and not self.gizmo.isEmpty():
|
||
# gizmo_name = self.gizmo.getName()
|
||
# print(f"✓ 坐标轴创建成功: {gizmo_name}")
|
||
# else:
|
||
# print("× 坐标轴创建失败")
|
||
#
|
||
# print(f"✓ 选中了节点: {node_name}")
|
||
# else:
|
||
# print("清除选择...")
|
||
# self.clearSelectionBox()
|
||
# self.clearGizmo()
|
||
# print("✓ 取消选择")
|
||
#
|
||
# print("=== 选择状态更新完成 ===\n")
|
||
|
||
def _delayedFocusTask(self, task):
|
||
"""延迟执行聚焦任务"""
|
||
try:
|
||
self.focusCameraOnSelectedNodeAdvanced()
|
||
except Exception as e:
|
||
print(f"延迟聚焦任务失败: {e}")
|
||
return task.done
|
||
|
||
# 添加一个更智能的双击检测方法,考虑鼠标位置
|
||
|
||
def checkDoubleClickWithPosition(self, nodePath, mouse_x=None, mouse_y=None):
|
||
"""检查是否为双击,同时考虑鼠标位置"""
|
||
try:
|
||
import time
|
||
current_time = time.time()
|
||
|
||
# 如果没有提供鼠标位置,直接使用基本双击检测
|
||
if mouse_x is None or mouse_y is None:
|
||
return self.checkDoubleClick(nodePath)
|
||
|
||
# 检查节点和时间
|
||
time_diff = current_time - self._last_click_time
|
||
is_same_node = (self._last_clicked_node == nodePath)
|
||
|
||
# 如果是同一节点且在时间阈值内,认为是双击
|
||
if is_same_node and time_diff < self._double_click_threshold:
|
||
# 重置状态
|
||
self._last_click_time = 0
|
||
self._last_clicked_node = None
|
||
return True
|
||
else:
|
||
# 更新状态
|
||
self._last_click_time = current_time
|
||
self._last_clicked_node = nodePath
|
||
return False
|
||
|
||
except Exception as e:
|
||
print(f"带位置的双击检测失败: {e}")
|
||
return False
|
||
|
||
# 添加一个公共方法来设置双击阈值
|
||
|
||
def setDoubleClickThreshold(self, threshold_seconds):
|
||
"""设置双击时间阈值"""
|
||
if threshold_seconds > 0:
|
||
self._double_click_threshold = threshold_seconds
|
||
print(f"双击阈值已设置为: {threshold_seconds} 秒")
|
||
else:
|
||
print("无效的双击阈值")
|
||
|
||
# 添加一个方法来手动触发双击聚焦(可用于测试或其他触发方式)
|
||
|
||
def triggerDoubleClickFocus(self, nodePath=None):
|
||
"""手动触发双击聚焦"""
|
||
try:
|
||
target_node = nodePath if nodePath else self._get_effective_selected_node()
|
||
if target_node and not target_node.isEmpty():
|
||
print(f"手动触发聚焦到节点: {target_node.getName()}")
|
||
if self._get_effective_selected_node() != target_node:
|
||
self.updateSelection(target_node)
|
||
self.focusCameraOnSelectedNodeAdvanced()
|
||
return True
|
||
else:
|
||
print("没有有效的目标节点进行聚焦")
|
||
return False
|
||
except Exception as e:
|
||
print(f"手动触发聚焦失败: {e}")
|
||
return False
|
||
|
||
def cleanup(self):
|
||
"""清理选择系统资源"""
|
||
# 清理双击任务
|
||
if self._double_click_task:
|
||
taskMgr.remove(self._double_click_task)
|
||
self._double_click_task = None
|
||
|
||
# 清理其他资源
|
||
self.clearSelectionBox()
|
||
self._updateSelectionOutline(None)
|
||
self.clearGizmo()
|
||
def clearSelection(self):
|
||
"""清除当前选择"""
|
||
try:
|
||
ssbo_editor = getattr(self.world, "ssbo_editor", None)
|
||
if ssbo_editor:
|
||
try:
|
||
ssbo_editor.clear_selection(sync_world_selection=False)
|
||
except Exception as e:
|
||
print(f"清除 SSBO 选择失败: {e}")
|
||
self.selectedNode = None
|
||
self.selectedObject = None
|
||
self.clearSelectionBox()
|
||
self._updateSelectionOutline(None)
|
||
self.clearGizmo()
|
||
|
||
# 清除树形控件中的选择
|
||
self._clear_tree_selection()
|
||
|
||
print("已清除选择")
|
||
except Exception as e:
|
||
print(f"清除选择失败: {e}")
|
||
|