1
0
forked from Rowland/EG

addRender #19

Merged
Hector merged 9 commits from addRender into main 2025-08-25 09:42:27 +00:00
14 changed files with 2644 additions and 340 deletions

File diff suppressed because one or more lines are too long

BIN
core/RotationHandleFull.fbx Executable file

Binary file not shown.

BIN
core/RotationHandleQuarter.fbx Executable file

Binary file not shown.

BIN
core/UniformScaleHandle.fbx Executable file

Binary file not shown.

View File

@ -398,12 +398,8 @@ class EventHandler:
if self.world.selection.gizmo and not self.world.selection.isDraggingGizmo:
x = evt.get('x', 0)
y = evt.get('y', 0)
# 只在前5次调用时输出调试信息避免刷屏
if not hasattr(self.world, '_highlight_debug_count'):
self.world._highlight_debug_count = 0
if self.world._highlight_debug_count < 5:
print(f"更新坐标轴高亮: 鼠标({x}, {y}), 坐标轴存在={bool(self.world.selection.gizmo)}")
self.world._highlight_debug_count += 1
# 减少高亮调试输出,只在需要时输出
# 已静默处理,避免控制台刷屏
self.world.selection.updateGizmoHighlight(x, y)
# 调用CoreWorld的父类方法处理基础的相机旋转

View File

@ -11,7 +11,7 @@ from PIL.ImageChops import lighter
from panda3d.core import (Vec3, Point3, Point2, LineSegs, ColorAttrib, RenderState,
DepthTestAttrib, CollisionTraverser, CollisionHandlerQueue,
CollisionNode, CollisionRay, GeomNode, BitMask32, Material, LColor, DepthWriteAttrib,
TransparencyAttrib, Vec4)
TransparencyAttrib, Vec4, CollisionCapsule)
from direct.task.TaskManagerGlobal import taskMgr
import math
@ -38,6 +38,9 @@ class SelectionSystem:
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
# 拖拽相关状态
@ -312,6 +315,8 @@ class SelectionSystem:
self._setupGizmoRendering()
self.setupGizmoCollision()
# 现在才显示坐标轴
self.gizmo.show()
@ -329,41 +334,120 @@ class SelectionSystem:
if not self.gizmo:
return
model_paths = [
"core/TranslateArrowHandle.fbx",
"EG/core/TranslateArrowHandle.fbx",
]
arrow_model = None
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
if is_scale_tool:
model_paths = [
"core/UniformScaleHandle.fbx",
]
elif is_rotate_tool:
model_paths = [
"core/RotationHandleQuarter.fbx",
]
else:
model_paths = [
"core/TranslateArrowHandle.fbx",
]
# model_paths = [
# "core/TranslateArrowHandle.fbx",
# "EG/core/TranslateArrowHandle.fbx",
# ]
gizmo_model = None
gizmoRot_model = None
for path in model_paths:
try:
arrow_model = self.world.loader.loadModel(path)
if arrow_model:
print(f"成功加载模型: {path}")
break
if is_rotate_tool:
gizmo_model = self.world.loader.loadModel("core/TranslateArrowHandle.fbx")
gizmoRot_model = self.world.loader.loadModel(path)
else:
gizmo_model = self.world.loader.loadModel(path)
if gizmo_model:
print(f"成功加载模型: {path}")
break
except:
continue
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_arrow = arrow_model.copyTo(self.gizmoXAxis)
x_arrow.setName("x_arrow")
x_arrow.setHpr(0,-90,0)
x_arrow.setScale(0.1,0.05,0.05)
x_arrow.setPos(0,0,0)
x_handle = gizmo_model.copyTo(self.gizmoXAxis)
x_handle.setName("x_handle")
self.gizmoYAxis = self.gizmo.attachNewNode("gizmo_y_axis")
y_arrow = arrow_model.copyTo(self.gizmoYAxis)
y_arrow.setName("y_arrow")
y_arrow.setHpr(90,0,0)
y_arrow.setScale(0.1,0.05,0.05)
y_arrow.setPos(0,0,0)
y_handle = gizmo_model.copyTo(self.gizmoYAxis)
y_handle.setName("y_handle")
# 创建Z轴蓝色
self.gizmoZAxis = self.gizmo.attachNewNode("gizmo_z_axis")
z_arrow = arrow_model.copyTo(self.gizmoZAxis)
z_arrow.setName("z_arrow")
# 旋转箭头使其指向Z轴正方向
z_arrow.setHpr(0, 0, -90) # 根据需要调整旋转
z_arrow.setScale(0.1,0.05,0.05)
z_arrow.setPos(0, 0, 0)
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"])
@ -379,6 +463,7 @@ class SelectionSystem:
def _setupGizmoRendering(self):
try:
axis_nodes = [self.gizmoXAxis,self.gizmoYAxis,self.gizmoZAxis]
axis_Rotnodes = [self.gizmoRotXAxis, self.gizmoRotYAxis, self.gizmoRotZAxis]
for axis_node in axis_nodes:
if axis_node:
@ -388,21 +473,45 @@ class SelectionSystem:
axis_node.setFogOff()
#设置渲染层级,确保大多数对象之前渲染
axis_node.setBin("fixed",30)
axis_node.setDepthWrite(False)
axis_node.setDepthTest(False)
#axis_node.setDepthWrite(False)
#axis_node.setDepthTest(True)
for axis_rotnode in axis_Rotnodes:
if axis_rotnode:
axis_rotnode.setLightOff()
axis_rotnode.setShaderOff()
axis_rotnode.setFogOff()
axis_rotnode.setBin("fixed",30)
#axis_rotnode.setDepthWrite(False)
#axis_rotnode.setDepthTest(True)
arrow_nodes = []
if self.gizmoXAxis:
x_arrow = self.gizmoXAxis.find("x_arrow")
if x_arrow:
arrow_nodes.append(x_arrow)
x_handle = self.gizmoXAxis.find("x_handle")
if x_handle:
arrow_nodes.append(x_handle)
if self.gizmoYAxis:
y_arrow = self.gizmoYAxis.find("y_arrow")
if y_arrow:
arrow_nodes.append(y_arrow)
y_handle = self.gizmoYAxis.find("y_handle")
if y_handle:
arrow_nodes.append(y_handle)
if self.gizmoZAxis:
z_arrow = self.gizmoZAxis.find("z_arrow")
if z_arrow:
arrow_nodes.append(z_arrow)
z_handle = self.gizmoZAxis.find("z_handle")
if z_handle:
arrow_nodes.append(z_handle)
rot_nodes = []
if self.gizmoRotXAxis:
x_rHandle = self.gizmoRotXAxis.find("x_handle")
if x_rHandle:
rot_nodes.append(x_rHandle)
if self.gizmoRotYAxis:
y_rHandle = self.gizmoRotYAxis.find("y_handle")
if y_rHandle:
rot_nodes.append(y_rHandle)
if self.gizmoRotZAxis:
z_rHandle = self.gizmoRotZAxis.find("z_handle")
if z_rHandle:
rot_nodes.append(z_rHandle)
for arrow_node in arrow_nodes:
if arrow_node:
@ -410,17 +519,30 @@ class SelectionSystem:
arrow_node.setShaderOff()
arrow_node.setFogOff()
arrow_node.setBin("fixed",31)
arrow_node.setDepthWrite(False)
arrow_node.setDepthTest(False)
#arrow_node.setDepthWrite(False)
#arrow_node.setDepthTest(False)
#启用透明度S
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",31)
#rot_node.setDepthWrite(False)
#rot_node.setDepthTest(False)
#启用透明度S
rot_node.setTransparency(TransparencyAttrib.MAlpha)
if self.gizmo:
self.gizmo.setLightOff()
self.gizmo.setShaderOff()
self.gizmo.setFogOff()
self.gizmo.setBin("fixed",29)
self.gizmo.setDepthWrite(False)
self.gizmo.setDepthTest(False)
# self.gizmo.setDepthWrite(False)
#self.gizmo.setDepthTest(False)
except Exception as e:
print(f"设置坐标轴渲染属性失败: {str(e)}")
@ -448,12 +570,49 @@ class SelectionSystem:
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:
light_pos = light_object.pos
self.gizmo.setPos(light_object.pos)
self.gizmoTarget.setPos(light_pos)
else:
# 只在必要时更新位置和朝向
self._updateGizmoPositionAndOrientation()
@ -486,13 +645,28 @@ class SelectionSystem:
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)
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:
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)
# 更新朝向
# 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):
"""动态调整坐标轴大小,保持固定的屏幕大小"""
@ -589,6 +763,97 @@ class SelectionSystem:
# except:
# pass
def setGizmoRotAxisColor(self, axis, color):
"""使用材质设置坐标轴颜色 - RenderPipeline兼容版本"""
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()
# 设置材质属性 - 使用自发光确保在RenderPipeline下可见
mat.setBaseColor(Vec4(color[0], color[1], color[2], color[3]))
mat.setDiffuse(Vec4(0, 0, 0, 1))
#mat.setEmission(Vec4(color[0], color[1], color[2], 1.0)) # 自发光
mat.setEmission(Vec4(1,1,1,1.0)) # 自发光
mat.set_roughness(1)
# 应用材质
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",31)
#arrow_node.setDepthWrite(False)
#arrow_node.setDepthTest(True)
# 保存材质引用以便后续修改
if axis == "x":
self.xMat = mat
elif axis == "y":
self.yMat = mat
elif axis == "z":
self.zMat = mat
axis_node.setLightOff()
axis_node.setShaderOff()
axis_node.setFogOff()
axis_node.setBin("fixed", 30)
axis_node.setDepthWrite(False)
axis_node.setDepthTest(True)
except Exception as e:
print(f"设置坐标轴颜色失败: {str(e)}")
# 回退到简单颜色设置
try:
axis_nodes = {
"x": self.gizmoXAxis,
"y": self.gizmoYAxis,
"z": self.gizmoZAxis
}
if axis in axis_nodes and axis_nodes[axis]:
axis_nodes[axis].setColor(color[0], color[1], color[2], color[3])
except:
pass
def setGizmoAxisColor(self, axis, color):
"""使用材质设置坐标轴颜色 - RenderPipeline兼容版本"""
try:
@ -606,17 +871,19 @@ class SelectionSystem:
axis_node = axis_nodes[axis]
# 查找箭头模型节点
arrow_node = None
if axis == "x":
arrow_node = axis_node.find("x_arrow")
elif axis == "y":
arrow_node = axis_node.find("y_arrow")
elif axis == "z":
arrow_node = axis_node.find("z_arrow")
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 arrow_node:
print(f"未找到{axis}轴的箭头模型")
#如果找不到特定名称的节点,尝试查找任何子节点
if not handle_node:
children = axis_node.getChildren()
if children.getNumPath()>0:
handle_node = children[0]
if not handle_node:
print(f"未找到{axis}轴的处理模型")
return
# 创建或获取材质
@ -630,20 +897,20 @@ class SelectionSystem:
mat.set_roughness(1)
# 应用材质
arrow_node.setMaterial(mat, 1)
handle_node.setMaterial(mat, 1)
# 设置透明度
if color[3] < 1.0:
arrow_node.setTransparency(TransparencyAttrib.MAlpha)
handle_node.setTransparency(TransparencyAttrib.MAlpha)
else:
arrow_node.setTransparency(TransparencyAttrib.MNone)
handle_node.setTransparency(TransparencyAttrib.MNone)
arrow_node.setLightOff() # 禁用光照影响
arrow_node.setShaderOff() # 禁用着色器
arrow_node.setFogOff() # 禁用雾效果
handle_node.setLightOff() # 禁用光照影响
handle_node.setShaderOff() # 禁用着色器
handle_node.setFogOff() # 禁用雾效果
arrow_node.setBin("fixed",31)
handle_node.setBin("fixed",31)
#arrow_node.setDepthWrite(False)
#arrow_node.setDepthTest(True)
@ -920,10 +1187,23 @@ class SelectionSystem:
# 获取坐标轴中心的世界坐标
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)
# 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):
@ -1019,7 +1299,8 @@ class SelectionSystem:
if not self.gizmo or self.isDraggingGizmo:
return
# 使用统一的检测方法
# 使用碰撞检测方法
#hoveredAxis = self.detectGizmoAxisWithCollision(mouseX, mouseY)
hoveredAxis = self.detectGizmoAxisAtMouse(mouseX, mouseY)
# 简化稳定性检测逻辑
@ -1037,6 +1318,11 @@ class SelectionSystem:
# 高亮新的轴
if hoveredAxis:
self.setGizmoAxisColor(hoveredAxis, self.gizmo_highlight_colors[hoveredAxis])
else:
# 如果没有悬停在任何轴上,确保所有轴都恢复原始颜色
for axis_name in ["x", "y", "z"]:
if axis_name != self.dragGizmoAxis: # 不要改变正在拖拽的轴的颜色
self.setGizmoAxisColor(axis_name, self.gizmo_colors[axis_name])
self.gizmoHighlightAxis = hoveredAxis
@ -1074,17 +1360,34 @@ class SelectionSystem:
return
self.isDraggingGizmo = True
# 使用当前高亮的轴,如果有的话
# 使用当前高亮的轴,如果有的话;否则使用传入的轴
if self.gizmoHighlightAxis:
self.dragGizmoAxis = self.gizmoHighlightAxis
else:
elif axis and axis in self.gizmo_colors:
self.dragGizmoAxis = axis
else:
# 如果没有明确指定轴,尝试通过鼠标位置检测
self.dragGizmoAxis = self.detectGizmoAxisAtMouse(mouseX, mouseY)
# 如果仍然无法确定拖拽轴,则取消拖拽
if not self.dragGizmoAxis:
print("开始拖拽失败: 无法确定拖拽轴")
self.isDraggingGizmo = False
return
self.dragStartMousePos = (mouseX, mouseY)
# 保存开始拖拽时目标节点的位置和坐标轴的位置
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:
# 先将所有轴恢复为正常颜色
@ -1094,13 +1397,15 @@ class SelectionSystem:
# 然后将当前拖动的轴设置为高亮颜色
self.setGizmoAxisColor(self.dragGizmoAxis, self.gizmo_highlight_colors[self.dragGizmoAxis])
self.gizmoHighlightAxis = self.dragGizmoAxis
elif axis and axis in self.gizmo_colors:
for axis_name in self.gizmo_colors.keys():
self.setGizmoAxisColor(axis_name, self.gizmo_colors[axis_name])
self.setGizmoAxisColor(axis, self.gizmo_highlight_colors[axis])
self.gizmoHighlightAxis = axis
# 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
print(
f"开始拖拽 {self.dragGizmoAxis} 轴 - 目标起始位置: {self.gizmoTargetStartPos}, 坐标轴位置: {self.gizmoStartPos}, 鼠标: ({mouseX}, {mouseY})")
@ -1130,10 +1435,55 @@ class SelectionSystem:
print("拖拽更新失败: 没有坐标轴起始位置")
return
is_scale_tool = self.world.tool_manager.isScaleTool()
is_rotate_tool = self.world.tool_manager.isRotateTool()
# 计算鼠标移动距离(屏幕像素)
mouseDeltaX = mouseX - self.dragStartMousePos[0]
mouseDeltaY = mouseY - self.dragStartMousePos[1]
if is_scale_tool:
scale_factor = 1.0 + (mouseDeltaX + mouseDeltaY) * 0.01
start_scale = getattr(self,'gizmoTargetStartScale',Vec3(1,1,1))
target_hpr = self.gizmoTarget.getHpr()
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 - (mouseDeltaX + mouseDeltaY)*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 * scale_factor,
start_scale.z * scale_factor)
#应用新缩放值
self.gizmoTarget.setScale(new_scale)
#实时更新属性面板
self.world.property_panel.refreshModelValues(self.gizmoTarget)
return
elif is_rotate_tool:
rotation_speed = 0.5
rotation_amount = (mouseDeltaX + mouseDeltaY) * 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.world.property_panel.refreshModelValues(self.gizmoTarget)
return
# 使用坐标轴的实际位置而不是目标节点位置来计算屏幕投影
gizmo_world_pos = self.gizmoStartPos
@ -1155,13 +1505,30 @@ class SelectionSystem:
print(f"拖拽更新失败: 未知轴类型 {self.dragGizmoAxis}")
return
# 确定轴向量的变换上下文
world_axis_vector = local_axis_vector
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)
try:
if parent_node.getTransform().hasMat():
transform_mat = parent_node.getMat(self.world.render)
if not transform_mat.isSingular():
world_axis_vector = transform_mat.xformVec(local_axis_vector)
else:
print("警告: 检测到奇异变换矩阵,使用默认轴向量")
else:
print("警告: 父节点没有有效的变换矩阵,使用默认轴向量")
except Exception as e:
print(f"变换计算出错: {e},使用默认轴向量")
else:
world_axis_vector = local_axis_vector
# 确定轴向量的变换上下文
# 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
# 投影到屏幕空间
@ -1184,21 +1551,6 @@ class SelectionSystem:
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("拖拽更新失败: 无法获取轴线屏幕坐标")
@ -1209,7 +1561,6 @@ class SelectionSystem:
axis_end_screen[1] - axis_start_screen[1]
)
# 归一化屏幕轴方向
import math
length = math.sqrt(screen_axis_dir[0]**2 + screen_axis_dir[1]**2)
@ -1243,50 +1594,36 @@ class SelectionSystem:
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()
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
#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:
break
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}")
#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}")
#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}")
#print(f"Z轴移动{currentPos.z} -> {newPos.z}")
else:
print(f"未知轴: {self.dragGizmoAxis}")
return
@ -1329,10 +1666,10 @@ class SelectionSystem:
def stopGizmoDrag(self):
"""停止坐标轴拖拽"""
print(f"停止坐标轴拖拽 - 轴: {self.dragGizmoAxis}")
if self.dragGizmoAxis and self.dragGizmoAxis in self.gizmo_colors:
self.setGizmoAxisColor(self.dragGizmoAxis, self.gizmo_colors[self.dragGizmoAxis])
# 不要将 gizmoHighlightAxis 设置为 None保持当前高亮轴的状态
# self.gizmoHighlightAxis = None
# 恢复所有轴的颜色
for axis_name in ["x", "y", "z"]:
self.setGizmoAxisColor(axis_name, self.gizmo_colors[axis_name])
self.isDraggingGizmo = False
self.dragGizmoAxis = None
@ -1341,6 +1678,13 @@ class SelectionSystem:
self.gizmoTargetStartPos = None
self.gizmoStartPos = None
if hasattr(self, 'gizmoTargetStartScale'):
delattr(self, 'gizmoTargetStartScale')
if hasattr(self, 'gizmoTargetStartHpr'):
delattr(self, 'gizmoTargetStartHpr')
# 重置高亮轴
self.gizmoHighlightAxis = None
# ==================== 选择管理 ====================
def updateSelection(self, nodePath):
@ -1393,3 +1737,164 @@ class SelectionSystem:
if self.selectionBoxTarget and self.selectionBoxTarget.isEmpty():
self.clearSelectionBox()
def setupGizmoCollision(self):
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):
# 为单个轴创建碰撞体
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):
# 使用碰撞体检测鼠标是否悬停在坐标轴上
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):
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()}轴节点不存在")

View File

@ -150,49 +150,3 @@ class ToolManager:
except Exception as e:
print(f"❌ 启动插件配置器失败: {e}")
return False
def cleanup_processes(self):
"""清理所有启动的进程"""
try:
# 清理插件配置器进程
if hasattr(self, '_plugin_configurator_process') and self._plugin_configurator_process:
if self._plugin_configurator_process.poll() is None:
print("🔄 正在关闭插件配置器...")
self._plugin_configurator_process.terminate()
try:
# 等待进程结束最多等待5秒
self._plugin_configurator_process.wait(timeout=5)
print("✅ 插件配置器已正常关闭")
except subprocess.TimeoutExpired:
print("⚠️ 插件配置器未响应,强制关闭...")
self._plugin_configurator_process.kill()
self._plugin_configurator_process.wait()
print("✅ 插件配置器已强制关闭")
self._plugin_configurator_process = None
# 清理材质编辑器进程(如果存在)
if hasattr(self, '_material_editor_process') and self._material_editor_process:
if self._material_editor_process.poll() is None:
print("🔄 正在关闭材质编辑器...")
self._material_editor_process.terminate()
try:
self._material_editor_process.wait(timeout=5)
print("✅ 材质编辑器已正常关闭")
except subprocess.TimeoutExpired:
print("⚠️ 材质编辑器未响应,强制关闭...")
self._material_editor_process.kill()
self._material_editor_process.wait()
print("✅ 材质编辑器已强制关闭")
self._material_editor_process = None
except Exception as e:
print(f"⚠️ 清理进程时出错: {e}")
def get_plugin_configurator_status(self):
"""获取插件配置器的运行状态"""
if hasattr(self, '_plugin_configurator_process') and self._plugin_configurator_process:
if self._plugin_configurator_process.poll() is None:
return "运行中"
else:
return "已停止"
return "未启动"

View File

@ -15,6 +15,14 @@ from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QFormLayout, QLineEdit,
QColorDialog, QLabel, QWidget, QGroupBox, QHBoxLayout)
from PyQt5.QtGui import QColor
from PyQt5.QtCore import Qt
# 尝试导入 QtWebEngineWidgets如果失败则设置为 None
try:
from PyQt5.QtWebEngineWidgets import QWebEngineView
WEB_ENGINE_AVAILABLE = True
except ImportError:
QWebEngineView = None
WEB_ENGINE_AVAILABLE = False
print("⚠️ QtWebEngineWidgets 不可用Cesium 集成功能将被禁用")
class GUIManager:
@ -139,37 +147,159 @@ class GUIManager:
print(f"✓ 创建GUI输入框: {placeholder} (逻辑位置: {pos}, 屏幕位置: {gui_pos})")
return entry
def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=0.5):
def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=1):
"""创建3D空间文本"""
from panda3d.core import TextNode
from panda3d.core import TextNode,Material,Vec4,ColorAttrib,TransparencyAttrib
textNode = TextNode(f'3d-text-{len(self.gui_elements)}')
textNode.setText(text)
textNode.setAlign(TextNode.ACenter)
if self.world.getChineseFont():
textNode.setFont(self.world.getChineseFont())
textNode.setTextColor(Vec4(1,1,1,1))
textNodePath = self.world.render.attachNewNode(textNode)
textNodePath.setPos(*pos)
textNodePath.setScale(size)
textNodePath.setColor(1, 1, 0, 1)
textNodePath.setBillboardAxis() # 让文本总是面向相机
textNodePath.setScale(size,size,size)
#textNodePath.setBillboardAxis() # 让文本总是面向相机
# 为3D文本创建默认材质
material = Material(f"text-material-{len(self.gui_elements)}")
material.setBaseColor(Vec4(1, 1, 1, 1)) # 白色
material.setDiffuse(Vec4(1, 1, 1, 1))
material.setAmbient(Vec4(0.5, 0.5, 0.5, 1))
material.setSpecular(Vec4(0.1, 0.1, 0.1, 1.0))
material.setShininess(10.0)
#material.setEmission(0,0,0,1)
textNodePath.setMaterial(material, 1)
textNodePath.setTransparency(TransparencyAttrib.MAlpha)
textNodePath.setAttrib(ColorAttrib.makeFlat(Vec4(1, 1, 1, 1)))
textNodePath.setLightOff()
# 为GUI元素添加标识
textNodePath.setTag("gui_type", "3d_text")
textNodePath.setTag("gui_id", f"3d_text_{len(self.gui_elements)}")
textNodePath.setTag("gui_text", text)
textNodePath.setTag("is_gui_element", "1")
textNodePath.setDepthWrite(True) # 确保深度写入
textNodePath.setDepthTest(True) # 启用深度测试
textNodePath.setBin("fixed", 0) # 设置渲染层级,避免被遮挡
# if hasattr(self, 'render_pipeline') and self.render_pipeline:
# try:
# self.render_pipeline.set_effect(
# textNodePath,
# "effects/default.yaml",
# {
# "normal_mapping": False,
# "render_gbuffer": False,
# "alpha_testing": True,
# "parallax_mapping": False,
# "render_shadow": False,
# "render_envmap": False
# },
# 50
# )
# except Exception as e:
# print(f"⚠️ PBR效果应用失败: {e}")
self.gui_elements.append(textNodePath)
# 安全地调用updateSceneTree
if hasattr(self.world, 'updateSceneTree'):
self.world.updateSceneTree()
print(f"✓ 创建3D文本: {text} (世界位置: {pos})")
return textNodePath
def createGUI3DImage(self, pos=(0, 0, 0), image_path=None, size=1.0):
from panda3d.core import CardMaker, Material, LColor,TransparencyAttrib
# 参数类型检查和转换
if isinstance(size, (list, tuple)):
if len(size) >= 2:
x_size, y_size = float(size[0]), float(size[1])
else:
x_size = y_size = float(size[0]) if size else 1.0
else:
x_size = y_size = float(size)
# 创建卡片
cm = CardMaker('gui_3d_image')
cm.setFrame(-x_size/2, x_size/2, -y_size/2, y_size/2)
# 创建3D图像节点
image_node = self.world.render.attachNewNode(cm.generate())
image_node.setPos(*pos)
# 设置面向摄像机
#image_node.setBillboardAxis() # 让图像总是面向相机
# 创建支持贴图的材质
# mat = Material()
# mat.setName("GUI3DImageMaterial")
# color = LColor(1, 1, 1, 1)
# mat.set_base_color(color)
# mat.set_roughness(0.5)
# mat.set_metallic(0.0)
# image_node.set_material(mat)
# 为3D图像创建独立的材质
material = Material(f"image-material-{len(self.gui_elements)}")
material.setBaseColor(LColor(1, 1, 1, 1))
material.setDiffuse(LColor(1, 1, 1, 1))
material.setAmbient(LColor(0.5, 0.5, 0.5, 1))
material.setSpecular(LColor(0.1, 0.1, 0.1, 1.0))
material.setShininess(10.0)
material.setEmission(LColor(0, 0, 0, 1)) # 无自发光
image_node.setMaterial(material, 1)
image_node.setTransparency(TransparencyAttrib.MAlpha)
# 如果提供了图像路径,则加载纹理
if image_path:
self.update3DImageTexture(image_node, image_path)
# 应用PBR效果如果可用
try:
if hasattr(self, 'render_pipeline') and self.render_pipeline:
self.render_pipeline.set_effect(
image_node,
"effects/default.yaml",
{
"normal_mapping": True,
"render_gbuffer": True,
"alpha_testing": False,
"parallax_mapping": False,
"render_shadow": False,
"render_envmap": True,
"disable_children_effects": True
},
50
)
print("✓ GUI 3D图像PBR效果已应用")
except Exception as e:
print(f"⚠️ GUI 3D图像PBR效果应用失败: {e}")
# 为GUI元素添加标识效仿3D文本方法
image_node.setTag("gui_type", "3d_image")
image_node.setTag("gui_id", f"3d_image_{len(self.gui_elements)}")
if image_path:
image_node.setTag("gui_image_path", image_path)
image_node.setTag("is_gui_element", "1")
self.gui_elements.append(image_node)
# 更新场景树
if hasattr(self.world, 'updateSceneTree'):
self.world.updateSceneTree()
print(f"✓ 3D图像创建完成: {image_path or '无纹理'} (世界位置: {pos})")
return image_node
def createGUIVirtualScreen(self, pos=(0, 0, 0), size=(2, 1), text="虚拟屏幕"):
@ -262,15 +392,16 @@ class GUIManager:
except Exception as e:
print(f"删除GUI元素失败: {str(e)}")
return False
# 在 gui_manager.py 中确保 editGUIElement 方法正确处理文本颜色
def editGUIElement(self, gui_element, property_name, value):
"""编辑GUI元素属性"""
try:
from panda3d.core import TextNode
gui_type = gui_element.getTag("gui_type") if hasattr(gui_element, 'getTag') else "unknown"
print(f"开始编辑GUI元素: 类型={gui_type}, 属性={property_name}, 值={value}")
if property_name == "text":
if gui_type in ["button", "label"]:
gui_element['text'] = value
@ -285,7 +416,7 @@ class GUIManager:
print(f"成功更新3D文本: {value}")
else:
print(f"警告: {gui_type}节点类型为{type(gui_element.node())}不是TextNode类型")
elif gui_type == "virtual_screen":
# 对于虚拟屏幕需要找到TextNode子节点
print(f"虚拟屏幕有 {gui_element.getNumChildren()} 个子节点")
@ -297,36 +428,53 @@ class GUIManager:
text_found = True
print(f"成功更新虚拟屏幕文本: {value}")
break
if not text_found:
print(f"警告: 在{gui_type}中未找到TextNode子节点")
gui_element.setTag("gui_text", value)
elif property_name == "color": # 添加颜色处理
if isinstance(value, (list, tuple)) and len(value) >= 3:
# 更新材质颜色
if not gui_element.hasMaterial():
material = Material(f"text-material-{gui_element.getName()}")
material.setBaseColor(Vec4(value[0], value[1], value[2], value[3] if len(value) > 3 else 1.0))
material.setDiffuse(Vec4(value[0], value[1], value[2], value[3] if len(value) > 3 else 1.0))
gui_element.setMaterial(material, 1)
else:
material = gui_element.getMaterial()
material.setBaseColor(Vec4(value[0], value[1], value[2], value[3] if len(value) > 3 else 1.0))
material.setDiffuse(Vec4(value[0], value[1], value[2], value[3] if len(value) > 3 else 1.0))
gui_element.setMaterial(material, 1)
# 更新 TextNode 的文本颜色
if isinstance(gui_element.node(), TextNode):
gui_element.node().setTextColor(
Vec4(value[0], value[1], value[2], value[3] if len(value) > 3 else 1.0))
# if gui_type in ["3d_text", "virtual_screen"]:
# gui_element.setColor(*value)
# elif gui_type in ["button", "label"]:
# gui_element['text_fg'] = value
elif property_name == "position":
if isinstance(value, (list, tuple)) and len(value) >= 3:
gui_element.setPos(*value[:3])
elif property_name == "scale":
if isinstance(value, (int, float)):
gui_element.setScale(value)
elif isinstance(value, (list, tuple)) and len(value) >= 3:
gui_element.setScale(*value[:3])
elif property_name == "color":
if isinstance(value, (list, tuple)) and len(value) >= 3:
if gui_type in ["button", "label"]:
gui_element['frameColor'] = value
else:
gui_element.setColor(*value)
print(f"编辑GUI元素 {gui_type}: {property_name} = {value}")
return True
except Exception as e:
print(f"编辑GUI元素失败: {str(e)}")
import traceback
traceback.print_exc()
return False
def duplicateGUIElement(self, gui_element):
"""复制GUI元素"""
try:
@ -346,6 +494,9 @@ class GUIManager:
self.createGUIEntry(new_pos, gui_text + "_副本")
elif gui_type == "3d_text":
self.createGUI3DText(new_pos, gui_text + "_副本")
elif gui_type == "3d_image":
image_path = gui_element.getTag("image_path")
self.createGUI3DImage(new_pos,image_path,size=(2,2))
elif gui_type == "virtual_screen":
self.createGUIVirtualScreen(new_pos, text=gui_text + "_副本")
@ -581,6 +732,19 @@ class GUIManager:
text_font=self.world.getChineseFont() if self.world.getChineseFont() else None
)
y_pos -= spacing
#3D图片工具
btn_image = DirectButton(
parent = self.guiEditPanel,
text="3D图片",
pos=(0,0,y_pos),
scale=0.04,
command=self.setGUICreateTool,
extraArgs=["3d_image"],
frameColor=(0.2,0.8,0.8,1),
text_font=self.world.getChineseFont() if self.world.getChineseFont() else None
)
y_pos -= spacing
# 虚拟屏幕工具
btn_screen = DirectButton(
@ -594,6 +758,43 @@ class GUIManager:
text_font=self.world.getChineseFont() if self.world.getChineseFont() else None
)
y_pos -= spacing
#Cesium 集成工具 仅在Webengine 可用时显示)
if WEB_ENGINE_AVAILABLE:
label_cesium = DirectLabel(
parent=self.guiEditPanel,
text="Cesium 集成",
pos=(0, 0, y_pos),
scale=0.04,
text_fg=(1, 1, 0, 1),
frameColor=(0, 0, 0, 0),
text_font=self.world.getChineseFont() if self.world.getChineseFont() else None
)
y_pos -= 0.08
# 切换 Cesium 视图按钮
btn_toggle_cesium = DirectButton(
parent=self.guiEditPanel,
text="切换地图视图",
pos=(0, 0, y_pos),
scale=0.04,
command=self.toggleCesiumView,
frameColor=(0.2, 0.8, 0.6, 1),
text_font=self.world.getChineseFont() if self.world.getChineseFont() else None
)
y_pos -= spacing
# 刷新 Cesium 视图按钮
btn_refresh_cesium = DirectButton(
parent=self.guiEditPanel,
text="刷新地图",
pos=(0, 0, y_pos),
scale=0.04,
command=self.refreshCesiumView,
frameColor=(0.6, 0.8, 0.2, 1),
text_font=self.world.getChineseFont() if self.world.getChineseFont() else None
)
y_pos -= spacing
# 分隔线
y_pos -= 0.1
@ -741,6 +942,8 @@ class GUIManager:
element = self.createGUIEntry(pos, f"输入框_{len(self.gui_elements)}")
elif gui_type == "3d_text":
element = self.createGUI3DText(pos, f"3D文本_{len(self.gui_elements)}")
elif gui_type == "3d_image":
element = self.createGUI3DImage(pos)
elif gui_type == "virtual_screen":
element = self.createGUIVirtualScreen(pos, text=f"屏幕_{len(self.gui_elements)}")
else:
@ -950,4 +1153,510 @@ class GUIManager:
print(f"更新2D GUI位置: {axis}轴 = {value} (屏幕坐标: {gui_element.getPos()})")
except Exception as e:
print(f"编辑2D GUI位置失败: {str(e)}")
print(f"编辑2D GUI位置失败: {str(e)}")
def update3DImageTexture(self, model_nodepath, image_path):
from panda3d.core import Texture
try:
# 加载新纹理
new_texture = self.world.loader.loadTexture(image_path)
if new_texture:
# 确保纹理过滤质量
new_texture.setMagfilter(Texture.FT_linear)
new_texture.setMinfilter(Texture.FT_linear_mipmap_linear)
# 应用纹理到模型
model_nodepath.setTexture(new_texture, 1)
# 更新标签
model_nodepath.setTag("gui_image_path", image_path)
# 确保材质设置正确
if not model_nodepath.has_material():
from panda3d.core import Material, LColor
mat = Material()
mat.setName(f"image-material-{id(model_nodepath)}")
mat.setBaseColor(LColor(1, 1, 1, 1))
mat.setDiffuse(LColor(1, 1, 1, 1))
mat.setAmbient(LColor(0.5, 0.5, 0.5, 1))
mat.setSpecular(LColor(0.1, 0.1, 0.1, 1.0))
mat.setShininess(10.0)
model_nodepath.setMaterial(mat, 1)
print(f"✅ 3D图像纹理已更新为: {image_path}")
else:
print(f"❌ 无法加载纹理: {image_path}")
except Exception as e:
print(f"❌ 更新纹理时出错: {e}")
# 替换现有的 createCesiumView 方法
def createCesiumView(self, main_window=None):
"""创建 Cesium 视图窗口(离线版本)"""
if not WEB_ENGINE_AVAILABLE:
print("❌ 无法创建Cesium视图: Web引擎不可用")
return None
try:
from PyQt5.QtWebEngineWidgets import QWebEngineView
from PyQt5.QtWidgets import QDockWidget
from PyQt5.QtCore import QUrl
import os
# 尝试获取主窗口引用
if main_window is None:
print("🔍 尝试获取主窗口引用...")
# 检查各种可能的主窗口引用
if hasattr(self.world, 'interface_manager'):
print(f" - interface_manager 存在: {self.world.interface_manager}")
if hasattr(self.world.interface_manager, 'main_window'):
main_window = self.world.interface_manager.main_window
print(f" - interface_manager.main_window: {main_window}")
if main_window is None and hasattr(self.world, 'main_window'):
main_window = self.world.main_window
print(f" - world.main_window: {main_window}")
# 如果仍然没有主窗口,尝试从树形控件获取
if main_window is None and self.world.treeWidget:
try:
main_window = self.world.treeWidget.window()
print(f" - 从 treeWidget 获取窗口: {main_window}")
except:
pass
if main_window is None:
print("✗ 无法获取主窗口引用")
return None
else:
print(f"✅ 使用传入的主窗口引用: {main_window}")
# 检查主窗口是否有效
if not hasattr(main_window, 'addDockWidget'):
print(f"✗ 主窗口引用无效,缺少 addDockWidget 方法")
return None
# 检查是否已经存在 Cesium 视图
for element in self.gui_elements:
if hasattr(element, 'objectName') and element.objectName() == "CesiumView":
print("⚠ Cesium 视图已经存在")
# 将其前置显示
element.show()
element.raise_()
return element
# 创建停靠窗口
print(f"🔧 创建 Cesium 停靠窗口,父窗口: {main_window}")
cesium_dock = QDockWidget("Cesium 地图视图(离线)", main_window)
cesium_dock.setObjectName("CesiumView")
# 创建 Web 视图
self.cesium_view = QWebEngineView()
# 使用本地 HTML 文件(离线模式)
local_html_path = os.path.abspath("./cesium_offline.html")
if os.path.exists(local_html_path):
print(f"🌐 加载离线 Cesium: file://{local_html_path}")
self.cesium_view.load(QUrl(f"file://{local_html_path}"))
else:
print("⚠️ 离线文件不存在,使用在线版本")
self.cesium_view.load(QUrl("http://localhost:8080/Apps/HelloWorld.html"))
# 设置内容
cesium_dock.setWidget(self.cesium_view)
# 添加到主窗口
print("📍 将 Cesium 视图添加到主窗口")
main_window.addDockWidget(Qt.RightDockWidgetArea, cesium_dock)
# 添加到GUI元素列表以便管理
self.gui_elements.append(cesium_dock)
print("✓ Cesium 离线视图已创建并集成到项目中")
return cesium_dock
except Exception as e:
print(f"✗ 创建 Cesium 视图失败: {str(e)}")
import traceback
traceback.print_exc()
return None
def toggleCesiumView(self):
"""切换 Cesium 视图显示状态"""
if not WEB_ENGINE_AVAILABLE:
print("✗ QtWebEngineWidgets 不可用,无法切换 Cesium 视图")
return None
try:
# 查找现有的 Cesium 视图
cesium_dock = None
cesium_index = -1
for i, element in enumerate(self.gui_elements):
if hasattr(element, 'objectName') and element.objectName() == "CesiumView":
cesium_dock = element
cesium_index = i
break
# 如果存在则移除,否则创建
if cesium_dock:
# 获取主窗口引用以正确移除停靠窗口
main_window = None
if (hasattr(self.world, 'interface_manager') and
hasattr(self.world.interface_manager, 'main_window') and
self.world.interface_manager.main_window):
main_window = self.world.interface_manager.main_window
elif hasattr(self.world, 'main_window') and self.world.main_window:
main_window = self.world.main_window
if main_window and hasattr(main_window, 'removeDockWidget'):
main_window.removeDockWidget(cesium_dock)
# 从列表中移除
if cesium_index >= 0:
self.gui_elements.pop(cesium_index)
print("✓ Cesium 视图已隐藏")
return None
else:
return self.createCesiumView()
except Exception as e:
print(f"✗ 切换 Cesium 视图失败: {str(e)}")
import traceback
traceback.print_exc()
return None
def refreshCesiumView(self):
"""刷新 Cesium 视图"""
if not WEB_ENGINE_AVAILABLE:
print("✗ QtWebEngineWidgets 不可用,无法刷新 Cesium 视图")
return False
try:
for element in self.gui_elements:
if hasattr(element, 'objectName') and element.objectName() == "CesiumView":
web_view = element.widget()
if isinstance(web_view, QWebEngineView):
web_view.reload()
print("✓ Cesium 视图已刷新")
return True
print("⚠ 未找到 Cesium 视图")
return False
except Exception as e:
print(f"✗ 刷新 Cesium 视图失败: {str(e)}")
return False
def updateCesiumURL(self, url):
"""更新 Cesium 视图的 URL"""
if not WEB_ENGINE_AVAILABLE:
print("✗ QtWebEngineWidgets 不可用,无法更新 Cesium URL")
return False
try:
for element in self.gui_elements:
if hasattr(element, 'objectName') and element.objectName() == "CesiumView":
web_view = element.widget()
if isinstance(web_view, QWebEngineView):
from PyQt5.QtCore import QUrl
web_view.load(QUrl(url))
print(f"✓ Cesium URL 已更新为: {url}")
return True
print("⚠ 未找到 Cesium 视图")
return False
except Exception as e:
print(f"✗ 更新 Cesium URL 失败: {str(e)}")
return False
# 在 GUIManager 类中添加以下方法
def addModelToCesium(self, model_id, model_url, longitude, latitude, height=0, scale=1.0):
"""向 Cesium 添加模型"""
if not WEB_ENGINE_AVAILABLE:
print("✗ QtWebEngineWidgets 不可用,无法操作 Cesium")
return False
try:
# 查找 Cesium 视图
cesium_view = None
for element in self.gui_elements:
if (hasattr(element, 'objectName') and
element.objectName() == "CesiumView" and
hasattr(element, 'widget')):
cesium_view = element.widget()
break
if not cesium_view:
print("✗ 未找到 Cesium 视图")
return False
# 转义特殊字符以防止 JavaScript 语法错误
escaped_model_id = str(model_id).replace("'", "\\'")
escaped_model_url = str(model_url).replace("'", "\\'").replace("\\", "/")
# 构造 JavaScript 调用
js_code = f"""
(function() {{
if (window.CesiumAPI && typeof window.CesiumAPI.addModel === 'function') {{
try {{
var result = window.CesiumAPI.addModel(
'{escaped_model_id}',
'{escaped_model_url}',
{{
longitude: {longitude},
latitude: {latitude},
height: {height}
}},
{scale}
);
console.log('Cesium 添加模型结果:', result);
return result || {{success: true, message: 'Model added'}};
}} catch (error) {{
console.error('JavaScript 错误:', error);
return {{success: false, message: 'JavaScript error: ' + error.message}};
}}
}} else {{
console.error('CesiumAPI.addModel 不可用');
return {{success: false, message: 'CesiumAPI.addModel not available'}};
}}
}})();
"""
# 定义回调函数处理结果
def handle_result(result):
try:
if isinstance(result, dict):
if result.get('success', False):
print(f"✓ 成功在 Cesium 中添加模型: {model_id}")
else:
print(f"✗ 在 Cesium 中添加模型失败: {result.get('message', 'Unknown error')}")
else:
print(f"✓ 已发送添加模型请求: {model_id}")
except Exception as callback_error:
print(f"✗ 处理回调结果时出错: {callback_error}")
# 执行 JavaScript 并获取结果
cesium_view.page().runJavaScript(js_code, handle_result)
return True
except Exception as e:
print(f"✗ 添加模型到 Cesium 失败: {e}")
import traceback
traceback.print_exc()
return False
# 添加新的方法来集成 Panda3D 场景中的 Cesium Tiles
def addCesiumTilesetToScene(self, tileset_name, tileset_url, position=(0, 0, 0)):
"""在 Panda3D 场景中添加 Cesium 3D Tiles"""
try:
# 使用场景管理器加载 tileset
tileset_node = self.world.scene_manager.load_cesium_tileset(tileset_url, position)
if tileset_node:
# 添加到 GUI 元素列表以便管理
self.gui_elements.append({
'type': 'cesium_tileset',
'name': tileset_name,
'node': tileset_node,
'url': tileset_url
})
print(f"✓ 在场景中添加 Cesium tileset: {tileset_name}")
return tileset_node
else:
print(f"✗ 在场景中添加 Cesium tileset 失败: {tileset_name}")
return None
except Exception as e:
print(f"✗ 在场景中添加 Cesium tileset 出错: {e}")
return None
def removeModelFromCesium(self, model_id):
"""从 Cesium 移除模型"""
if not WEB_ENGINE_AVAILABLE:
print("✗ QtWebEngineWidgets 不可用")
return False
try:
# 查找 Cesium 视图
cesium_view = None
for element in self.gui_elements:
if (hasattr(element, 'objectName') and
element.objectName() == "CesiumView" and
hasattr(element, 'widget')):
cesium_view = element.widget()
break
if not cesium_view:
print("✗ 未找到 Cesium 视图")
return False
# 构造 JavaScript 调用
js_code = f"""
if (window.CesiumAPI && typeof window.CesiumAPI.removeModel === 'function') {{
var result = window.CesiumAPI.removeModel('{model_id}');
result;
}} else {{
{{success: false, message: 'CesiumAPI.removeModel not available'}};
}}
"""
# 定义回调函数处理结果
def handle_result(result):
if result and isinstance(result, dict):
if result.get('success', False):
print(f"✓ 成功从 Cesium 中移除模型: {model_id}")
else:
print(f"✗ 从 Cesium 中移除模型失败: {result.get('message', 'Unknown error')}")
else:
print(f"✓ 已发送移除模型请求: {model_id} (无法获取详细结果)")
# 执行 JavaScript 并获取结果
cesium_view.page().runJavaScript(js_code, handle_result)
return True
except Exception as e:
print(f"✗ 从 Cesium 移除模型失败: {e}")
return False
def updateCesiumModelPosition(self, model_id, longitude, latitude, height=0):
"""更新 Cesium 中模型的位置"""
if not WEB_ENGINE_AVAILABLE:
print("✗ QtWebEngineWidgets 不可用")
return False
try:
# 查找 Cesium 视图
cesium_view = None
for element in self.gui_elements:
if (hasattr(element, 'objectName') and
element.objectName() == "CesiumView" and
hasattr(element, 'widget')):
cesium_view = element.widget()
break
if not cesium_view:
print("✗ 未找到 Cesium 视图")
return False
# 使用更安全的 JavaScript 字符串构造方式
escaped_model_id = model_id.replace("'", "\\'")
# 构造 JavaScript 调用
js_code = f"""
(function() {{
if (window.CesiumAPI && typeof window.CesiumAPI.updateModelPosition === 'function') {{
try {{
var result = window.CesiumAPI.updateModelPosition(
'{escaped_model_id}',
{{
longitude: {longitude},
latitude: {latitude},
height: {height}
}}
);
return result || {{success: true, message: 'Position updated'}};
}} catch (error) {{
return {{success: false, message: 'JavaScript error: ' + error.message}};
}}
}} else {{
return {{success: false, message: 'CesiumAPI.updateModelPosition not available'}};
}}
}})();
"""
# 定义回调函数处理结果
def handle_result(result):
try:
if isinstance(result, dict):
if result.get('success', False):
print(f"✓ 成功更新 Cesium 中模型位置: {model_id}")
else:
print(f"✗ 更新 Cesium 中模型位置失败: {result.get('message', 'Unknown error')}")
else:
print(f"✓ 已发送更新模型位置请求: {model_id}")
except Exception as callback_error:
print(f"✗ 处理回调结果时出错: {callback_error}")
# 执行 JavaScript 并获取结果
cesium_view.page().runJavaScript(js_code, handle_result)
return True
except Exception as e:
print(f"✗ 更新 Cesium 中模型位置失败: {e}")
return False
def getAllCesiumModels(self):
"""获取 Cesium 中所有模型的列表"""
if not WEB_ENGINE_AVAILABLE:
print("✗ QtWebEngineWidgets 不可用")
return None
try:
# 查找 Cesium 视图
cesium_view = None
for element in self.gui_elements:
if (hasattr(element, 'objectName') and
element.objectName() == "CesiumView" and
hasattr(element, 'widget')):
cesium_view = element.widget()
break
if not cesium_view:
print("✗ 未找到 Cesium 视图")
return None
# 构造 JavaScript 调用
js_code = """
if (window.CesiumAPI && typeof window.CesiumAPI.getAllModels === 'function') {
var result = window.CesiumAPI.getAllModels();
JSON.stringify(result);
} else {
JSON.stringify({success: false, message: 'CesiumAPI.getAllModels not available'});
}
"""
# 定义回调函数处理结果
def handle_result(result):
try:
if isinstance(result, str):
import json
result = json.loads(result)
if result and result.get('success', False):
models = result.get('models', [])
print(f"✓ Cesium 中的模型列表: {models}")
return models
else:
print(f"✗ 获取 Cesium 模型列表失败: {result.get('message', 'Unknown error')}")
return []
except Exception as e:
print(f"✗ 解析 Cesium 模型列表结果失败: {e}")
return []
# 执行 JavaScript 并获取结果
cesium_view.page().runJavaScript(js_code)
return None # 异步操作,实际结果通过回调处理
except Exception as e:
print(f"✗ 获取 Cesium 模型列表失败: {e}")
return None
# 添加一个便捷方法来加载本地模型文件
def addLocalModelToCesium(self, model_id, local_model_path, longitude, latitude, height=0, scale=1.0):
"""向 Cesium 添加本地模型文件"""
try:
# 将本地路径转换为相对路径或 URL
import os
if os.path.exists(local_model_path):
# 如果 Cesium 服务器可以访问该路径,可以直接使用
# 否则需要将模型文件放在 Cesium 的静态资源目录中
model_url = local_model_path.replace('\\', '/') # 确保使用正斜杠
return self.addModelToCesium(model_id, model_url, longitude, latitude, height, scale)
else:
print(f"✗ 模型文件不存在: {local_model_path}")
return False
except Exception as e:
print(f"✗ 添加本地模型失败: {e}")
return False

13
main.py
View File

@ -183,6 +183,10 @@ class MyWorld(CoreWorld):
"""创建3D空间文本"""
return self.gui_manager.createGUI3DText(pos, text, size)
def createGUI3DImage(self,pos=(0,0,0),text="3D图片",size=(2,2)):
"""创建3D图片"""
return self.gui_manager.createGUI3DImage(pos,text,size)
def createSpotLight(self,pos=(-20,0,5)):
"""创建聚光灯"""
return self.scene_manager.createSpotLight(pos)
@ -673,6 +677,15 @@ class MyWorld(CoreWorld):
"streaming_status": self.getALVRStreamingStatus() if self.isALVRConnected() else None
}
def loadCesiumTileset(self,tileset_url,position=(0,0,0)):
return self.scene_manager.load_cesium_tileset(tileset_url,position)
def addCesiumTileset(self,name,url,position=(0,0,0)):
if hasattr(self,'gui_manager') and self.gui_manager:
return self.gui_manager.addCesiumTilesetToScene(name,url,position)
else:
return self.scene_manager.load_cesium_tileset(url,position)
# ==================== 项目管理功能代理 ====================
# 以下函数代理到project_manager模块的对应功能

View File

@ -12,11 +12,57 @@ from panda3d.core import (
MaterialAttrib, ColorAttrib, Point3, CollisionNode, CollisionSphere,
BitMask32, TransparencyAttrib,LColor
)
import json
import aiohttp
import asyncio
from pathlib import Path
from panda3d.egg import EggData, EggVertexPool
from direct.actor.Actor import Actor
from QPanda3D.Panda3DWorld import get_render_pipeline
from scene import util
class CesiumIntegration:
def __init__(self, scene_manager):
self.scene_manager = scene_manager
self.world = scene_manager.world
self.tilesets = {}
def add_tileset(self,name,url,position=(0,0,0)):
try:
tileset_node = self.scene_manager.load_cesium_tileset(url,position)
if tileset_node:
self.tilesets[name] = {
'node':tileset_node,
'url':url,
'position':position
}
print(f"✓ 添加 Cesium tileset: {name}")
return tileset_node
else:
print(f"✗ 添加 Cesium tileset 失败: {name}")
return None
except Exception as e:
print(f"✗ 添加 Cesium tileset 出错: {e}")
return None
def remove_tileset(self, name):
"""移除 tileset"""
if name in self.tilesets:
tileset_info = self.tilesets[name]
tileset_info['node'].removeNode()
del self.tilesets[name]
print(f"✓ 移除 Cesium tileset: {name}")
return True
return False
def get_tileset(self, name):
"""获取 tileset"""
return self.tilesets.get(name, None)
def list_tilesets(self):
"""列出所有 tilesets"""
return list(self.tilesets.keys())
class SceneManager:
"""场景管理器 - 统一管理场景中的所有元素"""
@ -33,6 +79,8 @@ class SceneManager:
self.Spotlight = []
self.Pointlight = []
self.tilesets = [] #来存储tilesets
self.cesium_integration = CesiumIntegration(self)
print("✓ 场景管理系统初始化完成")
@ -1242,3 +1290,267 @@ except Exception as e:
print(f"[PyAssimp转换] 转换过程出错: {e}")
return False
def load_cesium_tileset(self, tileset_url, position=(0, 0, 0)):
try:
print(f"加载 Cesium 3D Tiles: {tileset_url}")
# 创建一个容器节点来管理tileset
node_name = f"cesium_tileset_{len(self.tilesets)}"
tileset_node = self.world.render.attachNewNode(node_name)
tileset_node.setPos(*position)
#添加标签以便场景树识别
tileset_node.setTag("is_scene_element","1")
tileset_node.setTag("element_type","cesium_tileset")
tileset_node.setTag("tileset_url",tileset_url)
tileset_node.setTag("file",f"tileset_{len(self.tilesets)}")
# 存储tileset信息
tileset_info = {
'url': tileset_url,
'node': tileset_node,
'position': position,
'tiles': {}
}
self.tilesets.append(tileset_info)
# 创建一个临时的可视化占位符,让用户能看到节点已添加
self._create_placeholder_geometry(tileset_node)
# 异步加载tileset数据
self._load_tileset_async(tileset_url, tileset_info)
# 更新场景树
self.updateSceneTree()
print(f"✓ Cesium 3D Tiles 加载请求已发送")
return tileset_node
except Exception as e:
print(f"❌ 加载 Cesium 3D Tiles 失败: {e}")
import traceback
traceback.print_exc()
return None
def _load_tileset_async(self, tileset_url, tileset_info):
"""异步加载 tileset 数据"""
async def load_tileset():
try:
async with aiohttp.ClientSession() as session:
async with session.get(tileset_url) as response:
if response.status == 200:
tileset_data = await response.json()
self._parse_tileset(tileset_data, tileset_info)
print(f"✓ Tileset 数据加载完成")
else:
print(f"✗ Tileset 加载失败: {response.status}")
except Exception as e:
print(f"✗ Tileset 加载出错: {e}")
# 在 Panda3D 的任务系统中运行异步任务
task = asyncio.ensure_future(load_tileset())
self._current_asyncio_task = task # 保存任务引用
self.world.taskMgr.add(self._check_async_task, "check_tileset_load", appendTask=True)
def _check_async_task(self, panda3d_task):
# 检查 asyncio 任务是否完成
if hasattr(self, '_current_asyncio_task'):
if self._current_asyncio_task.done():
try:
self._current_asyncio_task.result()
except Exception as e:
print(f"异步任务出错:{e}")
# 返回 Panda3D 任务管理器的完成状态
return panda3d_task.done # 注意是 done 而不是 DONE
# 返回 Panda3D 任务管理器的继续状态
return panda3d_task.cont # 注意是 cont 而不是 CONTINUE
def _parse_tileset(self,tileset_data,tileset_info):
try:
root = tileset_data.get('root',{})
self._parse_tile(root,tileset_info['node'],tileset_info)
print("✓ Tileset 解析完成")
except Exception as e:
print(f"✗ Tileset 解析出错: {e}")
def _parse_tile(self, tile_data, parent_node, tileset_info):
try:
# 获取tileID
tile_id = f"tile_{len(tileset_info['tiles'])}"
print(f"创建tile节点: {tile_id}")
# 创建tile节点
tile_node = parent_node.attachNewNode(tile_id)
tileset_info['tiles'][tile_id] = {
'node': tile_node,
'data': tile_data,
'loaded': False
}
# 如果有内容,创建占位几何体
if 'content' in tile_data:
print(f"为tile {tile_id} 创建几何体")
self._create_tile_geometry(tile_node)
# 递归解析子tiles
children = tile_data.get('children', [])
print(f"Tile {tile_id}{len(children)} 个子节点")
for child_data in children:
self._parse_tile(child_data, tile_node, tileset_info)
except Exception as e:
print(f"✗ Tile 解析出错: {e}")
import traceback
traceback.print_exc()
def _create_tile_geometry(self,parent_node):
"""为 tile 创建占位几何体"""
try:
# 创建一个简单的立方体作为占位符
from panda3d.core import GeomVertexFormat, GeomVertexData, GeomVertexWriter
from panda3d.core import Geom, GeomTriangles, GeomNode
format = GeomVertexFormat.getV3n3c4()
vdata = GeomVertexData('tile_cube', format, Geom.UHStatic)
vertex = GeomVertexWriter(vdata, 'vertex')
normal = GeomVertexWriter(vdata, 'normal')
color = GeomVertexWriter(vdata, 'color')
# 定义立方体顶点
vertices = [
(-0.5, -0.5, -0.5), (0.5, -0.5, -0.5), (0.5, 0.5, -0.5), (-0.5, 0.5, -0.5),
(-0.5, -0.5, 0.5), (0.5, -0.5, 0.5), (0.5, 0.5, 0.5), (-0.5, 0.5, 0.5)
]
for vert in vertices:
vertex.addData3f(*vert)
normal.addData3f(0, 0, 1)
color.addData4f(0.2, 0.6, 0.8, 1.0)
# 创建几何体
geom = Geom(vdata)
# 创建面
prim = GeomTriangles(Geom.UHStatic)
# 底面
prim.addVertices(0, 1, 2)
prim.addVertices(0, 2, 3)
# 顶面
prim.addVertices(4, 7, 6)
prim.addVertices(4, 6, 5)
# 前面
prim.addVertices(0, 4, 5)
prim.addVertices(0, 5, 1)
# 后面
prim.addVertices(2, 6, 7)
prim.addVertices(2, 7, 3)
# 左面
prim.addVertices(0, 3, 7)
prim.addVertices(0, 7, 4)
# 右面
prim.addVertices(1, 5, 6)
prim.addVertices(1, 6, 2)
prim.closePrimitive()
geom.addPrimitive(prim)
# 创建几何节点
geom_node = GeomNode('tile_geometry')
geom_node.addGeom(geom)
# 添加到场景
cube_node = parent_node.attachNewNode(geom_node)
cube_node.setScale(1000) # 放大以便观察
# 添加材质
material = Material()
material.setBaseColor((0.2, 0.6, 0.8, 1.0))
material.setSpecular((0.1, 0.1, 0.1, 1.0))
material.setShininess(10.0)
cube_node.setMaterial(material)
except Exception as e:
print(f"✗ 创建 tile 几何体出错: {e}")
def _create_placeholder_geometry(self, parent_node):
"""创建一个简单的占位符几何体,让用户能看到节点"""
try:
from panda3d.core import GeomVertexFormat, GeomVertexData, GeomVertexWriter
from panda3d.core import Geom, GeomTriangles, GeomNode
# 创建简单的立方体作为占位符
format = GeomVertexFormat.getV3n3c4()
vdata = GeomVertexData('placeholder_cube', format, Geom.UHStatic)
vertex = GeomVertexWriter(vdata, 'vertex')
normal = GeomVertexWriter(vdata, 'normal')
color = GeomVertexWriter(vdata, 'color')
# 定义立方体顶点(稍微大一些,便于识别)
size = 1.0
vertices = [
(-size, -size, -size), (size, -size, -size), (size, size, -size), (-size, size, -size),
(-size, -size, size), (size, -size, size), (size, size, size), (-size, size, size)
]
# 使用更鲜明的颜色
for vert in vertices:
vertex.addData3f(*vert)
normal.addData3f(0, 0, 1)
color.addData4f(0.0, 1.0, 1.0, 1.0) # 青色
# 创建几何体
geom = Geom(vdata)
# 创建面
prim = GeomTriangles(Geom.UHStatic)
# 底面
prim.addVertices(0, 1, 2)
prim.addVertices(0, 2, 3)
# 顶面
prim.addVertices(4, 7, 6)
prim.addVertices(4, 6, 5)
# 前面
prim.addVertices(0, 4, 5)
prim.addVertices(0, 5, 1)
# 后面
prim.addVertices(2, 6, 7)
prim.addVertices(2, 7, 3)
# 左面
prim.addVertices(0, 3, 7)
prim.addVertices(0, 7, 4)
# 右面
prim.addVertices(1, 5, 6)
prim.addVertices(1, 6, 2)
prim.closePrimitive()
geom.addPrimitive(prim)
# 创建几何节点
geom_node = GeomNode('tileset_placeholder')
geom_node.addGeom(geom)
# 添加到场景
cube_node = parent_node.attachNewNode(geom_node)
cube_node.setScale(5) # 适当大小
# 添加材质
material = Material()
material.setBaseColor((0.0, 1.0, 1.0, 1.0)) # 青色
material.setSpecular((0.5, 0.5, 0.5, 1.0))
material.setShininess(32.0)
cube_node.setMaterial(material)
# 添加标识标签
cube_node.setTag("element_type", "cesium_placeholder")
print("✓ 占位符几何体创建完成")
return cube_node
except Exception as e:
print(f"✗ 创建占位符几何体出错: {e}")
import traceback
traceback.print_exc()
return None

View File

@ -74,6 +74,10 @@ class InterfaceManager:
duplicateAction = menu.addAction("复制")
duplicateAction.triggered.connect(lambda: self.world.gui_manager.duplicateGUIElement(nodePath))
elif hasattr(nodePath,'getTag') and nodePath.getTag("element_type") == "cesium_tileset":
deleteAction = menu.addAction("删除 Cesium Tileset")
deleteAction.triggered.connect(lambda:self.deleteCesiumTileset(nodePath,item))
else:
# 为模型节点或其子节点添加删除选项
parentItem = item.parent()
@ -88,6 +92,40 @@ class InterfaceManager:
# 显示菜单
menu.exec_(self.treeWidget.viewport().mapToGlobal(position))
def deleteCesiumTileset(self, nodePath, item):
"""删除 Cesium tileset"""
try:
# 从场景中移除
nodePath.removeNode()
# 从 tilesets 列表中移除
if hasattr(self.world, 'scene_manager'):
tilesets_to_remove = []
for i, tileset_info in enumerate(self.world.scene_manager.tilesets):
if tileset_info['node'] == nodePath:
tilesets_to_remove.append(i)
# 从后往前删除,避免索引问题
for i in reversed(tilesets_to_remove):
del self.world.scene_manager.tilesets[i]
# 从树形控件中移除
parentItem = item.parent()
if parentItem:
parentItem.removeChild(item)
print(f"成功删除 Cesium tileset: {nodePath.getName()}")
# 清空属性面板和选择框
self.world.property_panel.clearPropertyPanel()
self.world.selection.updateSelection(None)
# 更新场景树
self.updateSceneTree()
except Exception as e:
print(f"删除 Cesium tileset 失败: {str(e)}")
def isModelOrChild(self, item):
"""检查是否是模型节点或其子节点"""
while item and item.parent():
@ -161,14 +199,14 @@ class InterfaceManager:
cameraItem.setData(0, Qt.UserRole, self.world.cam)
print("添加相机节点")
# 添加模型节点组
modelsItem = QTreeWidgetItem(sceneRoot, ['模型'])
print(f"模型列表中的模型数量: {len(self.world.models)}")
# 添加GUI元素节点组
guiItem = QTreeWidgetItem(sceneRoot, ['GUI元素'])
lightItem = QTreeWidgetItem(sceneRoot,['灯光'])
# # 添加模型节点组
# modelsItem = QTreeWidgetItem(sceneRoot, ['模型'])
# print(f"模型列表中的模型数量: {len(self.world.models)}")
#
# # 添加GUI元素节点组
# guiItem = QTreeWidgetItem(sceneRoot, ['GUI元素'])
#
# lightItem = QTreeWidgetItem(sceneRoot,['灯光'])
BLACK_LIST = {'','**','temp','collision'}
@ -207,17 +245,26 @@ class InterfaceManager:
# print(f"跳过节点: {child.getName()}")
for model in self.world.models:
addNodeToTree(model, modelsItem,force=True)
addNodeToTree(model, sceneRoot,force=True)
# 添加所有GUI元素
for gui in self.world.gui_elements:
gui_type = gui.getTag("gui_type") or "unknown"
gui_text = gui.getTag("gui_text") or "GUI元素"
item = QTreeWidgetItem(guiItem, [f"{gui_type}: {gui_text}"])
item = QTreeWidgetItem(sceneRoot, [f"{gui_type}: {gui_text}"])
item.setData(0, Qt.UserRole, gui)
#添加灯光节点
for light in self.world.Spotlight + self.world.Pointlight:
addNodeToTree(light, lightItem, force=True)
addNodeToTree(light, sceneRoot, force=True)
#添加 Cesium tilesets
if hasattr(self.world,'scene_manager') and hasattr(self.world.scene_manager,'tilesets'):
for i , tileset_info in enumerate(self.world.scene_manager.tilesets):
tileset_node = tileset_info['node']
tileset_url = tileset_info['url']
tileset_item = QTreeWidgetItem(sceneRoot,[f"Cesium Tileset {i}"])
tileset_item.setData(0,Qt.UserRole,tileset_node)
addNodeToTree(tileset_node,tileset_item,force=True)
# 添加地板节点
if hasattr(self.world, 'ground') and self.world.ground:

View File

@ -12,7 +12,7 @@ from PyQt5.QtWidgets import (QApplication, QMainWindow, QMenuBar, QMenu, QAction
QDockWidget, QTreeWidget, QListWidget, QWidget, QVBoxLayout, QTreeWidgetItem,
QLabel, QLineEdit, QFormLayout, QDoubleSpinBox, QScrollArea,
QFileSystemModel, QButtonGroup, QToolButton, QPushButton, QHBoxLayout,
QComboBox, QGroupBox, QInputDialog, QFileDialog, QMessageBox, QDesktopWidget)
QComboBox, QGroupBox, QInputDialog, QFileDialog, QMessageBox, QDesktopWidget,QDialog)
from PyQt5.QtCore import Qt, QDir, QTimer
from ui.widgets import CustomPanda3DWidget, CustomFileView, CustomTreeWidget
@ -92,18 +92,45 @@ class MainWindow(QMainWindow):
self.scaleAction = self.toolsMenu.addAction('缩放工具')
self.sunsetAction = self.toolsMenu.addAction('光照编辑')
self.pluginAction = self.toolsMenu.addAction('图形编辑')
# 创建菜单
self.createMenu = menubar.addMenu('创建')
self.createEnptyaddAction = self.createMenu.addAction('空对象')
self.create3dObjectaddMenu = self.createMenu.addMenu('3D对象')
self.create3dGUIaddMenu = self.createMenu.addMenu('3D GUI')
self.create3DTextAction = self.create3dGUIaddMenu.addAction('3D文本')
self.createGUIaddMenu = self.createMenu.addMenu('GUI')
self.createButtonAction = self.createGUIaddMenu.addAction('创建按钮')
self.createLabelAction = self.createGUIaddMenu.addAction('创建标签')
self.createEntryAction = self.createGUIaddMenu.addAction('创建输入框')
self.createGUIaddMenu.addSeparator()
self.createVirtualScreenAction = self.createGUIaddMenu.addAction('创建虚拟屏幕')
self.createCesiumViewAction = self.createGUIaddMenu.addAction('创建Cesium地图')
self.toggleCesiumViewAction = self.createGUIaddMenu.addAction('开关地图')
self.refreshCesiumViewAction = self.createGUIaddMenu.addAction('刷新地图')
self.createLightaddMenu = self.createMenu.addMenu('光源')
self.createSpotLightAction = self.createLightaddMenu.addAction('聚光灯')
self.createPointLightAction = self.createLightaddMenu.addAction('点光源')
# GUI菜单
self.guiMenu = menubar.addMenu('GUI')
self.guiEditModeAction = self.guiMenu.addAction('进入GUI编辑模式')
self.guiMenu.addSeparator()
self.createButtonAction = self.guiMenu.addAction('创建按钮')
self.createLabelAction = self.guiMenu.addAction('创建标签')
self.createEntryAction = self.guiMenu.addAction('创建输入框')
# self.createButtonAction = self.guiMenu.addAction('创建按钮')
# self.createLabelAction = self.guiMenu.addAction('创建标签')
# self.createEntryAction = self.guiMenu.addAction('创建输入框')
self.guiMenu.addAction(self.createButtonAction)
self.guiMenu.addAction(self.createLabelAction)
self.guiMenu.addAction(self.createEntryAction)
self.guiMenu.addSeparator()
self.create3DTextAction = self.guiMenu.addAction('创建3D文本')
self.createVirtualScreenAction = self.guiMenu.addAction('创建虚拟屏幕')
# self.create3DTextAction = self.guiMenu.addAction('创建3D文本')
self.guiMenu.addAction(self.create3DTextAction)
# self.createVirtualScreenAction = self.guiMenu.addAction('创建虚拟屏幕')
self.guiMenu.addAction(self.createVirtualScreenAction)
# 脚本菜单
self.scriptMenu = menubar.addMenu('脚本')
self.createScriptAction = self.scriptMenu.addAction('创建脚本...')
@ -115,6 +142,10 @@ class MainWindow(QMainWindow):
self.toggleHotReloadAction.setChecked(True) # 默认启用
self.scriptMenu.addSeparator()
self.openScriptsManagerAction = self.scriptMenu.addAction('脚本管理器')
self.cesiumMenu = menubar.addMenu('Cesium')
self.loadCesiumTilesetAction = self.cesiumMenu.addAction('加载3Dtiles')
self.loadCesiumTilesetAction.triggered.connect(self.onLoadCesiumTileset)
# 帮助菜单
self.helpMenu = menubar.addMenu('帮助')
@ -131,6 +162,7 @@ class MainWindow(QMainWindow):
# self.leftDock.setMinimumWidth(300)
self.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, self.leftDock)
# 创建右侧停靠窗口(属性窗口)
self.rightDock = QDockWidget("属性", self)
self.rightDock.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
@ -239,6 +271,10 @@ class MainWindow(QMainWindow):
self.create3DTextTool.setText("3D文本")
self.toolbar.addWidget(self.create3DTextTool)
self.create3DImageTool = QToolButton()
self.create3DImageTool.setText("3D图片")
self.toolbar.addWidget(self.create3DImageTool)
self.createSpotLight = QToolButton()
self.createSpotLight.setText("聚光灯")
self.toolbar.addWidget(self.createSpotLight)
@ -247,6 +283,22 @@ class MainWindow(QMainWindow):
self.createPointLight.setText("点光灯")
self.toolbar.addWidget(self.createPointLight)
# Cesium 工具按钮
self.cesiumViewTool = QToolButton()
self.cesiumViewTool.setText("地图视图")
self.cesiumViewTool.clicked.connect(self.onCreateCesiumView)
self.toolbar.addWidget(self.cesiumViewTool)
self.refreshCesiumTool = QToolButton()
self.refreshCesiumTool.setText("刷新地图")
self.refreshCesiumTool.clicked.connect(self.onRefreshCesiumView)
self.toolbar.addWidget(self.refreshCesiumTool)
self.addModelTool = QToolButton()
self.addModelTool.setText("添加模型")
self.addModelTool.clicked.connect(self.onAddModelClicked)
self.toolbar.addWidget(self.addModelTool)
# 默认选择"选择"工具
self.selectTool.setChecked(True)
self.world.setCurrentTool("选择")
@ -388,7 +440,11 @@ class MainWindow(QMainWindow):
# 连接GUI编辑模式事件
self.guiEditModeAction.triggered.connect(lambda: self.world.toggleGUIEditMode())
# 连接创建事件
# 连接光源创建按钮事件
self.createSpotLightAction.triggered.connect(lambda :self.world.createSpotLight())
self.createPointLightAction.triggered.connect(lambda :self.world.createPointLight())
# 连接GUI创建按钮事件
self.createButtonAction.triggered.connect(lambda: self.world.createGUIButton())
self.createLabelAction.triggered.connect(lambda: self.world.createGUILabel())
@ -396,11 +452,18 @@ class MainWindow(QMainWindow):
self.create3DTextAction.triggered.connect(lambda: self.world.createGUI3DText())
#self.createSpotLightAction.triggered.connect(lambda :self.world.createSpotLight())
self.createVirtualScreenAction.triggered.connect(lambda: self.world.createGUIVirtualScreen())
self.createCesiumViewAction.triggered.connect(self.onCreateCesiumView)
self.toggleCesiumViewAction.triggered.connect(self.onToggleCesiumView)
self.refreshCesiumViewAction.triggered.connect(self.onRefreshCesiumView)
self.createCesiumViewAction.triggered.connect(self.onCreateCesiumView)
self.toggleCesiumViewAction.triggered.connect(self.onToggleCesiumView)
self.refreshCesiumViewAction.triggered.connect(self.onRefreshCesiumView)
# 连接工具栏GUI创建按钮事件
self.createButtonTool.clicked.connect(lambda: self.world.createGUIButton())
self.createLabelTool.clicked.connect(lambda: self.world.createGUILabel())
self.create3DTextTool.clicked.connect(lambda: self.world.createGUI3DText())
self.create3DImageTool.clicked.connect(lambda: self.world.createGUI3DImage())
self.createSpotLight.clicked.connect(lambda :self.world.createSpotLight())
self.createPointLight.clicked.connect(lambda :self.world.createPointLight())
@ -418,7 +481,225 @@ class MainWindow(QMainWindow):
self.loadAllScriptsAction.triggered.connect(self.onReloadAllScripts)
self.toggleHotReloadAction.triggered.connect(self.onToggleHotReload)
self.openScriptsManagerAction.triggered.connect(self.onOpenScriptsManager)
def onCreateCesiumView(self):
if hasattr(self.world,'gui_manager') and self.world.gui_manager:
self.world.gui_manager.createCesiumView()
else:
QMessageBox.warning(self,"错误","GUI管理其不可用")
def onToggleCesiumView(self):
"""切换 Cesium 视图显示状态"""
if hasattr(self.world, 'gui_manager') and self.world.gui_manager:
self.world.gui_manager.toggleCesiumView()
else:
QMessageBox.warning(self, "错误", "GUI 管理器不可用")
def onRefreshCesiumView(self):
"""刷新 Cesium 视图"""
if hasattr(self.world, 'gui_manager') and self.world.gui_manager:
self.world.gui_manager.refreshCesiumView()
else:
QMessageBox.warning(self, "错误", "GUI 管理器不可用")
def onUpdateCesiumURL(self):
"""更新 Cesium URL"""
url, ok = QInputDialog.getText(self, "更新 Cesium URL", "输入新的 URL:",
QLineEdit.Normal, "http://localhost:8080/Apps/HelloWorld.html")
if ok and url:
if hasattr(self.world, 'gui_manager') and self.world.gui_manager:
self.world.gui_manager.updateCesiumURL(url)
else:
QMessageBox.warning(self, "错误", "GUI 管理器不可用")
def onAddModelClicked(self):
"""处理加入模型按钮点击事件"""
# 检查 Cesium 视图是否存在
cesium_view_exists = False
if hasattr(self.world, 'gui_manager') and self.world.gui_manager:
for element in self.world.gui_manager.gui_elements:
if hasattr(element, 'objectName') and element.objectName() == "CesiumView":
cesium_view_exists = True
break
if not cesium_view_exists:
reply = QMessageBox.question(
self,
'提示',
'Cesium 地图视图尚未打开,是否先打开地图视图?',
QMessageBox.Yes | QMessageBox.No,
QMessageBox.Yes
)
if reply == QMessageBox.Yes:
self.onCreateCesiumView()
# 给一点时间让 Cesium 视图加载
QTimer.singleShot(1000, self.showAddModelDialog)
return
else:
return
self.showAddModelDialog()
def showAddModelDialog(self):
"""显示添加模型对话框"""
# 打开文件选择对话框
file_path, _ = QFileDialog.getOpenFileName(
self,
"选择 3D 模型文件",
"",
"3D 模型文件 (*.glb *.gltf *.obj);;所有文件 (*)"
)
if file_path:
# 获取模型位置信息
coords, ok = self.getModelCoordinates()
if ok:
longitude, latitude, height, scale = coords
# 生成唯一的模型 ID
import uuid
model_id = f"model_{uuid.uuid4().hex[:8]}"
try:
# 添加模型到 Cesium
if hasattr(self.world, 'gui_manager') and self.world.gui_manager:
success = self.world.gui_manager.addModelToCesium(
model_id,
file_path,
longitude,
latitude,
height,
scale
)
if success:
QMessageBox.information(
self,
"成功",
f"模型已成功添加到地图!\n模型ID: {model_id}"
)
else:
QMessageBox.warning(
self,
"失败",
"添加模型失败,请检查控制台输出"
)
except Exception as e:
QMessageBox.critical(
self,
"错误",
f"添加模型时发生错误:\n{str(e)}"
)
def getModelCoordinates(self):
"""获取模型坐标信息的对话框"""
# 创建对话框
dialog = QDialog(self)
dialog.setWindowTitle("设置模型位置")
dialog.setModal(True)
dialog.resize(300, 200)
layout = QVBoxLayout(dialog)
# 经度
lon_layout = QHBoxLayout()
lon_layout.addWidget(QLabel("经度:"))
lon_spin = QDoubleSpinBox()
lon_spin.setRange(-180, 180)
lon_spin.setValue(116.3975) # 默认北京位置
lon_layout.addWidget(lon_spin)
layout.addLayout(lon_layout)
# 纬度
lat_layout = QHBoxLayout()
lat_layout.addWidget(QLabel("纬度:"))
lat_spin = QDoubleSpinBox()
lat_spin.setRange(-90, 90)
lat_spin.setValue(39.9085) # 默认北京位置
lat_layout.addWidget(lat_spin)
layout.addLayout(lat_layout)
# 高度
height_layout = QHBoxLayout()
height_layout.addWidget(QLabel("高度(米):"))
height_spin = QDoubleSpinBox()
height_spin.setRange(-10000, 100000)
height_spin.setValue(0)
height_layout.addWidget(height_spin)
layout.addLayout(height_layout)
# 缩放
scale_layout = QHBoxLayout()
scale_layout.addWidget(QLabel("缩放:"))
scale_spin = QDoubleSpinBox()
scale_spin.setRange(0.001, 100000)
scale_spin.setValue(1.0)
scale_spin.setSingleStep(0.1)
scale_layout.addWidget(scale_spin)
layout.addLayout(scale_layout)
# 按钮
button_layout = QHBoxLayout()
ok_button = QPushButton("确定")
cancel_button = QPushButton("取消")
button_layout.addWidget(ok_button)
button_layout.addWidget(cancel_button)
layout.addLayout(button_layout)
# 连接信号
ok_button.clicked.connect(dialog.accept)
cancel_button.clicked.connect(dialog.reject)
# 显示对话框
if dialog.exec_() == QDialog.Accepted:
return (
lon_spin.value(),
lat_spin.value(),
height_spin.value(),
scale_spin.value()
), True
else:
return None, False
def onLoadCesiumTileset(self):
url,ok = QInputDialog.getText(
self,
"加载 Cesium 3D Tiles",
"输入 tileset.json URL:",
QLineEdit.Normal,
"https://assets.ion.cesium.com/96128/tileset.json"
)
if ok and url:
try:
# 生成唯一的 tileset 名称
import uuid
tileset_name = f"tileset_{uuid.uuid4().hex[:8]}"
# 加载 tileset
if hasattr(self.world, 'addCesiumTileset'):
success = self.world.addCesiumTileset(tileset_name, url, (0, 0, 0))
if success:
QMessageBox.information(
self,
"成功",
f"Cesium 3D Tiles 已加载到场景中!\n名称: {tileset_name}"
)
else:
QMessageBox.warning(
self,
"失败",
"加载 Cesium 3D Tiles 失败"
)
except Exception as e:
QMessageBox.critical(
self,
"错误",
f"加载 Cesium 3D Tiles 时发生错误:\n{str(e)}"
)
def onToolChanged(self, button):
"""工具切换事件处理"""
if button.isChecked():
@ -428,7 +709,7 @@ class MainWindow(QMainWindow):
else:
self.world.setCurrentTool(None)
print("工具栏: 取消选择工具")
# ==================== 脚本管理事件处理 ====================
def refreshScriptsList(self):

View File

@ -22,6 +22,25 @@ class PropertyPanelManager:
self._propertyLayout = None
self._actor_cache={}
# 定义紧凑样式
self.compact_style = """
QDoubleSpinBox {
min-width: 45px;
}
QPushButton {
min-width: 10px;
}
QComboBox {
min-width: 60px;
}
QLineEdit {
min-width: 60px;
}
QCheckBox {
min-width: 20px;
}
"""
def setPropertyLayout(self, layout):
"""设置属性面板布局引用"""
print("开始设置属性布局")
@ -59,6 +78,10 @@ class PropertyPanelManager:
# 当节点被拖拽后,需要根据新父节点的状态来更新可见性
self._syncEffectiveVisibility(node)
self._syncSceneVisibility()
def _syncSceneVisibility(self):
scene_root = self.world.render
self._syncEffectiveVisibility(scene_root)
def updatePropertyPanel(self, item):
@ -69,6 +92,10 @@ class PropertyPanelManager:
self.clearPropertyPanel()
# 应用紧凑样式到属性面板容器
if self._propertyLayout.parent():
self._propertyLayout.parent().setStyleSheet(self.compact_style)
itemText = item.text(0)
# 如果点击的是场景根节点,显示提示信息
@ -114,10 +141,18 @@ class PropertyPanelManager:
# 获取节点对象
model = item.data(0, Qt.UserRole)
if model and hasattr(model,'getTag') and model.getTag("element_type") == "cesium_tileset":
self._showCesiumTilesetProperties(model,item)
# 检查是否是GUI元素
if model and hasattr(model, 'getTag') and model.getTag("gui_type"):
self.updateGUIPropertyPanel(model)
pass
elif model and hasattr(model, 'getTag') and model.getTag("gui_type"):
# gui_type = model.getTag("gui_type")
# if gui_type == "3d_image":
# self._updateGUIImagePropertyPanel(model)
# else:
# self.updateGUIPropertyPanel(model)
# pass
self.updateGUIPropertyPanel(model)
pass
elif model and hasattr(model, 'getTag') and model.getTag("light_type"):
self.updateLightPropertyPanel(model)
pass
@ -368,19 +403,27 @@ class PropertyPanelManager:
self.scale_y = QDoubleSpinBox()
self.scale_z = QDoubleSpinBox()
# 设置缩放控件属性
for scale_widget in [self.scale_x, self.scale_y, self.scale_z]:
scale_widget.setRange(0.01, 100)
scale_widget.setSingleStep(0.1)
current_scale = model.getScale()
self.scale_x.setValue(model.getScale().getX())
self.scale_y.setValue(model.getScale().getY())
self.scale_z.setValue(model.getScale().getZ())
# 设置缩放控件属性
for i, (scale_widget, scale_value) in enumerate(zip([self.scale_x, self.scale_y, self.scale_z],
[current_scale.getX(), current_scale.getY(),
current_scale.getZ()])):
scale_widget.setRange(-1000, 1000)
scale_widget.setSingleStep(0.1)
# 如果缩放值为0设置为一个很小的非零值
if scale_value == 0:
scale_value = 0.01 if scale_value >= 0 else -0.01
scale_widget.setValue(scale_value)
self.scale_x.valueChanged.connect(lambda value: self._onScaleValueChanged(self.scale_x, value))
self.scale_y.valueChanged.connect(lambda value: self._onScaleValueChanged(self.scale_y, value))
self.scale_z.valueChanged.connect(lambda value: self._onScaleValueChanged(self.scale_z, value))
# 连接缩放变化事件
self.scale_x.valueChanged.connect(lambda v: model.setScale(v, model.getScale().getY(), model.getScale().getZ()))
self.scale_y.valueChanged.connect(lambda v: model.setScale(model.getScale().getX(), v, model.getScale().getZ()))
self.scale_z.valueChanged.connect(lambda v: model.setScale(model.getScale().getX(), model.getScale().getY(), v))
self.scale_x.valueChanged.connect(lambda value: self._updateXScale(model, value))
self.scale_y.valueChanged.connect(lambda value: self._updateYScale(model, value))
self.scale_z.valueChanged.connect(lambda value: self._updateZScale(model, value))
# 创建并设置 X, Y, Z 标签居中
x_label3 = QLabel("X")
@ -407,6 +450,72 @@ class PropertyPanelManager:
# 材质属性组
self._updateModelMaterialPanel(model)
def _onScaleValueChanged(self, scale_widget, value):
"""确保缩放值不为0"""
if value == 0:
# 设置为一个很小的非零值,保持原有符号
if hasattr(scale_widget, 'value') and scale_widget.value() > 0:
scale_widget.setValue(0.01)
else:
scale_widget.setValue(-0.01)
def _updateXScale(self, model, value):
"""更新X轴缩放值"""
# 确保值不为0
if value == 0:
sender = None
# 通过遍历找到发出信号的控件
for widget in [self.scale_x, self.scale_y, self.scale_z]:
if widget.value() == value:
sender = widget
break
if sender:
self._onScaleValueChanged(sender, value)
return
# 更新模型的X轴缩放
current_scale = model.getScale()
model.setScale(value, current_scale.getY(), current_scale.getZ())
self.refreshModelValues(model)
def _updateYScale(self, model, value):
"""更新Y轴缩放值"""
# 确保值不为0
if value == 0:
sender = None
# 通过遍历找到发出信号的控件
for widget in [self.scale_x, self.scale_y, self.scale_z]:
if widget.value() == value:
sender = widget
break
if sender:
self._onScaleValueChanged(sender, value)
return
# 更新模型的Y轴缩放
current_scale = model.getScale()
model.setScale(current_scale.getX(), value, current_scale.getZ())
self.refreshModelValues(model)
def _updateZScale(self, model, value):
"""更新Z轴缩放值"""
# 确保值不为0
if value == 0:
sender = None
# 通过遍历找到发出信号的控件
for widget in [self.scale_x, self.scale_y, self.scale_z]:
if widget.value() == value:
sender = widget
break
if sender:
self._onScaleValueChanged(sender, value)
return
# 更新模型的Z轴缩放
current_scale = model.getScale()
model.setScale(current_scale.getX(), current_scale.getY(), value)
self.refreshModelValues(model)
def refreshModelValues(self,nodePath):
if not nodePath or self._propertyLayout is None:
return
@ -451,7 +560,6 @@ class PropertyPanelManager:
spin.blockSignals(False)
def updateGUIPropertyPanel(self, gui_element):
"""更新GUI元素属性面板"""
gui_type = gui_element.getTag("gui_type")
@ -467,6 +575,7 @@ class PropertyPanelManager:
# typeValue.setStyleSheet("color: #00AAFF; font-weight: bold;")
gui_info_layout.addWidget(typeValue, 0, 1)
# 修改 updateGUIPropertyPanel 中的文本属性部分
# 文本属性(如果适用)
if gui_type in ["button", "label", "entry", "3d_text", "virtual_screen"]:
gui_info_layout.addWidget(QLabel("文本:"), 1, 0)
@ -477,7 +586,8 @@ class PropertyPanelManager:
success = self.world.gui_manager.editGUIElement(gui_element, "text", text)
if success:
# 更新场景树显示的名称
self.world.scene_manager.updateSceneTree()
if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, 'updateSceneTree'):
self.world.scene_manager.updateSceneTree()
textEdit.textChanged.connect(updateText)
gui_info_layout.addWidget(textEdit, 1, 1)
@ -575,7 +685,6 @@ class PropertyPanelManager:
[pos.getX(), pos.getY(), v]))
transform_layout.addWidget(zPos, 1, 3)
# 缩放属性
if hasattr(gui_element, 'getScale'):
scale = gui_element.getScale()
@ -583,17 +692,51 @@ class PropertyPanelManager:
transform_layout.addWidget(QLabel("缩放"), row_offset, 0)
scaleSpinBox = QDoubleSpinBox()
scaleSpinBox.setRange(0.01, 10)
scaleSpinBox.setSingleStep(0.1)
scaleSpinBox.setValue(scale.getX())
scaleSpinBox.valueChanged.connect(
lambda v: self.world.gui_manager.editGUIElement(gui_element, "scale", v))
transform_layout.addWidget(scaleSpinBox, row_offset, 1)
# X缩放
transform_layout.addWidget(QLabel("长:"), row_offset, 1)
scaleXSpinBox = QDoubleSpinBox()
scaleXSpinBox.setRange(0.01, 1000)
scaleXSpinBox.setSingleStep(0.1)
scaleXSpinBox.setValue(scale.getX())
scaleXSpinBox.valueChanged.connect(lambda v: self._onScaleValueChanged(scaleXSpinBox, v))
scaleXSpinBox.valueChanged.connect(lambda v: self._updateGUIScaleX(gui_element, v))
transform_layout.addWidget(scaleXSpinBox, row_offset, 2)
row_offset += 1
transform_layout.addWidget(QLabel("宽:"), row_offset, 1)
scaleYSpinBox = QDoubleSpinBox()
scaleYSpinBox.setRange(0.01, 1000)
scaleYSpinBox.setSingleStep(0.1)
scaleYSpinBox.setValue(scale.getY())
scaleYSpinBox.valueChanged.connect(lambda v: self._onScaleValueChanged(scaleYSpinBox, v))
scaleYSpinBox.valueChanged.connect(lambda v: self._updateGUIScaleZ(gui_element, v))
transform_layout.addWidget(scaleYSpinBox, row_offset, 2)
# scaleSpinBox = QDoubleSpinBox()
# scaleSpinBox.setRange(0.01, 10)
# scaleSpinBox.setSingleStep(0.1)
# scaleSpinBox.setValue(scale.getX())
# scaleSpinBox.valueChanged.connect(
# lambda v: self.world.gui_manager.editGUIElement(gui_element, "scale", v))
# transform_layout.addWidget(scaleSpinBox, row_offset, 1)
transform_group.setLayout(transform_layout)
self._propertyLayout.addWidget(transform_group)
# 外观属性组 - 添加字体颜色选择
if gui_type in ["button", "label", "3d_text"]:
appearance_group = QGroupBox("外观属性")
appearance_layout = QGridLayout()
# 字体颜色选择
appearance_layout.addWidget(QLabel("字体颜色:"), 0, 0)
colorButton = QPushButton("选择颜色")
colorButton.clicked.connect(lambda checked, elem=gui_element: self._selectGUIColor(elem))
appearance_layout.addWidget(colorButton, 0, 1)
appearance_group.setLayout(appearance_layout)
self._propertyLayout.addWidget(appearance_group)
# 外观属性组
if gui_type in ["button", "label"]:
appearance_group = QGroupBox("外观属性")
@ -608,6 +751,200 @@ class PropertyPanelManager:
appearance_group.setLayout(appearance_layout)
self._propertyLayout.addWidget(appearance_group)
if gui_type == "3d_image":
image_group = QGroupBox("图片设置")
image_layout = QGridLayout()
# 当前图片路径标签
current_image_label = QLabel("当前图片:")
image_layout.addWidget(current_image_label, 0, 0)
# 显示当前贴图路径(简化显示)
current_texture_path = gui_element.getTag("texture_path") or "未设置"
texture_label = QLabel(current_texture_path)
texture_label.setWordWrap(True)
image_layout.addWidget(texture_label, 0, 1)
# 选择图片按钮
select_texture_button = QPushButton("选择图片...")
image_layout.addWidget(select_texture_button, 1, 0, 1, 2)
def onSelectTexture():
from PyQt5.QtWidgets import QFileDialog
file_path, _ = QFileDialog.getOpenFileName(
None,
"选择图片",
"",
"图像文件 (*.png *.jpg *.jpeg *.bmp *.tga *.dds)"
)
if file_path:
# 应用新纹理到 3D Image
success = self.world.gui_manager.update3DImageTexture(gui_element, file_path)
if success:
# 保存路径到 Tag
gui_element.setTag("texture_path", file_path)
# 更新显示
texture_label.setText(file_path)
# 可选:刷新场景树或其他 UI
self.world.scene_manager.updateSceneTree()
select_texture_button.clicked.connect(onSelectTexture)
image_group.setLayout(image_layout)
self._propertyLayout.addWidget(image_group)
# 添加弹性空间
self._propertyLayout.addStretch()
# 强制更新布局
if self._propertyLayout:
self._propertyLayout.update()
propertyWidget = self._propertyLayout.parentWidget()
if propertyWidget:
propertyWidget.update()
def _selectGUIColor(self, gui_element):
"""选择GUI元素的字体颜色"""
from PyQt5.QtWidgets import QColorDialog
from PyQt5.QtGui import QColor
from panda3d.core import Vec4
# 获取当前颜色(如果已设置)
current_color = QColor(255, 255, 255) # 默认白色
# 尝试获取当前设置的颜色
gui_type = gui_element.getTag("gui_type")
try:
if gui_type == "3d_text":
if gui_element.hasMaterial():
material = gui_element.getMaterial()
base_color = material.getBaseColor()
current_color = QColor(
int(base_color.getX() * 255),
int(base_color.getY() * 255),
int(base_color.getZ() * 255),
int(base_color.getW() * 255)
)
else:
# 从节点颜色获取
node_color = gui_element.getColor()
current_color = QColor(
int(node_color.getX() * 255),
int(node_color.getY() * 255),
int(node_color.getZ() * 255),
int(node_color.getW() * 255)
)
# current_color_obj = gui_element.getColor()
# current_color = QColor(
# int(current_color_obj[0] * 255),
# int(current_color_obj[1] * 255),
# int(current_color_obj[2] * 255)
# )
# 对于其他类型的元素,可以添加类似的获取当前颜色的逻辑
except:
pass # 使用默认颜色
color = QColorDialog.getColor(current_color, None, "选择字体颜色")
if color.isValid():
r, g, b = color.red() / 255.0, color.green() / 255.0, color.blue() / 255.0
self._updateGUITextColor(gui_element, (r, g, b, 1.0))
def _updateGUITextColor(self, gui_element, color):
"""更新GUI元素的字体颜色"""
try:
gui_type = gui_element.getTag("gui_type")
if gui_type in ["button", "label", "entry"]:
# 对于DirectGUI元素使用text_fg属性
gui_element['text_fg'] = color
print(f"✓ 更新DirectGUI元素字体颜色: {gui_type}")
elif gui_type == "3d_text":
# # 对于3D文本元素直接设置颜色
# gui_element.setColor(*color)
# print(f"✓ 更新3D文本字体颜色: {gui_type}")
from panda3d.core import Material,Vec4
if not gui_element.hasMaterial():
material = Material(f"text-material-{gui_element.getName()}")
material.setBaseColor(Vec4(color[0], color[1], color[2], color[3]))
material.setDiffuse(Vec4(color[0], color[1], color[2], color[3]))
material.setAmbient(Vec4(color[0] * 0.5, color[1] * 0.5, color[2] * 0.5, color[3]))
material.setSpecular(Vec4(0.1, 0.1, 0.1, 1.0))
material.setShininess(10.0)
gui_element.setMaterial(material, 1)
else:
# 更新现有材质
material = gui_element.getMaterial()
material.setBaseColor(Vec4(color[0], color[1], color[2], color[3]))
material.setDiffuse(Vec4(color[0], color[1], color[2], color[3]))
gui_element.setMaterial(material, 1)
print(f"✓ 更新3D文本材质颜色: {color}")
gui_element.setColor(*color)
elif gui_type == "3d_image":
# 对于3D图片如果有文本标签的话
# 这里可以根据需要添加特定处理
pass
print(f"✓ 更新GUI元素字体颜色: {gui_type}, 颜色: {color}")
except Exception as e:
print(f"✗ 更新GUI元素字体颜色失败: {e}")
import traceback
traceback.print_exc()
def _updateGUIScaleX(self, gui_element, scale_x):
"""更新GUI元素X轴缩放"""
try:
gui_type = gui_element.getTag("gui_type")
current_scale = gui_element.getScale()
# 对于不同的GUI类型使用不同的缩放方法
if gui_type in ["3d_text", "3d_image"]:
# 对于3D元素直接设置缩放
new_scale = (scale_x, current_scale.getY(), current_scale.getZ())
gui_element.setScale(*new_scale)
else:
# 对于2D元素保持原有的缩放方法
gui_element.setScale(scale_x)
print(f"✓ 更新GUI元素X轴缩放: {scale_x}")
except Exception as e:
print(f"✗ 更新GUI元素X轴缩放失败: {e}")
def _updateGUIScaleZ(self, gui_element, scale_z):
"""更新GUI元素Y轴缩放"""
try:
gui_type = gui_element.getTag("gui_type")
current_scale = gui_element.getScale()
# 对于不同的GUI类型使用不同的缩放方法
if gui_type in ["3d_text", "3d_image"]:
# 对于3D元素直接设置缩放
new_scale = (current_scale.getX(), current_scale.getZ(), scale_z)
gui_element.setScale(*new_scale)
else:
# 对于2D元素保持原有的缩放方法
gui_element.setScale(scale_z)
print(f"✓ 更新GUI元素Y轴缩放: {scale_z}")
except Exception as e:
print(f"✗ 更新GUI元素Y轴缩放失败: {e}")
def update3DImageTexture(self,nodepath,texture_path):
try:
tex = self.world.loader.loadTexture(texture_path)
if tex:
nodepath.setTexture(tex,1)
return True
else:
print(f"[警告] 无法加载贴图: {texture_path}")
return False
except Exception as e:
print(f"[错误] 更新 3D 图片纹理失败: {e}")
return False
def _updateScriptPropertyPanel(self, game_object):
"""更新脚本属性面板"""
# 获取对象上的脚本

View File

@ -15,7 +15,7 @@ from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QGroupBox, QHBoxLayout,
QTreeView, QTreeWidget, QTreeWidgetItem, QWidget,
QFileDialog, QMessageBox, QAbstractItemView)
from PyQt5.QtCore import Qt, QUrl
from PyQt5.QtGui import QDrag, QPainter, QPixmap
from PyQt5.QtGui import QDrag, QPainter, QPixmap, QPen, QBrush
from PyQt5.sip import wrapinstance
from QPanda3D.QPanda3DWidget import QPanda3DWidget
@ -319,118 +319,268 @@ class CustomTreeWidget(QTreeWidget):
self.setHeaderHidden(True)
# 启用多选和拖拽
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
self.setDropIndicatorShown(True)
self.setDropIndicatorShown(True) # 启用拖放指示线
def setupDragDrop(self):
"""设置拖拽功能"""
# 使用自定义拖拽模式
self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove) # 或者使用 DragDrop
self.setDefaultDropAction(Qt.DropAction.MoveAction)
self.setDragEnabled(True)
self.setAcceptDrops(True)
def dropEvent(self, event):
"""处理拖放事件"""
# 获取拖动的项和目标项
dragged_item = self.currentItem()
target_item = self.itemAt(event.pos())
if not dragged_item or not target_item:
event.ignore()
return
# 获取节点引用
if not self.isValidParentChild(dragged_item, target_item):
event.ignore()
return
dragged_node = dragged_item.data(0, Qt.UserRole)
amtarget_node = target_item.data(0, Qt.UserRole)
# 如果目标是模型根节点,使用 render 作为新父节点
if target_item.text(0) == "模型":
target_node = self.world.render
else:
target_node = target_item.data(0, Qt.UserRole)
target_node = target_item.data(0, Qt.UserRole)
if not dragged_node or not target_node:
event.ignore()
return
# 检查是否是有效的父子关系
if self.isValidParentChild(dragged_item, target_item):
# 保存当前的世界坐标
world_pos = dragged_node.getPos(self.world.render)
# 更新场景图中的父子关系
dragged_node.wrtReparentTo(target_node)
# 接受拖放事件,更新树形控件
super().dropEvent(event)
#self.world.property_panel.updateNodeVisibilityAfterDrag(dragged_item)
# 更新属性面板
self.world.updatePropertyPanel(dragged_item)
self.world.property_panel._syncEffectiveVisibility(dragged_node)
# # 检查是否是有效的父子关系
# if self.isValidParentChild(dragged_item, target_item):
# # 保存当前的世界坐标
# world_pos = dragged_node.getPos(self.world.render)
#
# # 更新场景图中的父子关系
# dragged_node.wrtReparentTo(target_node)
#
# # 接受拖放事件,更新树形控件
# super().dropEvent(event)
#
# #self.world.property_panel.updateNodeVisibilityAfterDrag(dragged_item)
# # 更新属性面板
# self.world.updatePropertyPanel(dragged_item)
# self.world.property_panel._syncEffectiveVisibility(dragged_node)
else:
event.ignore()
print(f"dragged_node: {dragged_node}, target_node: {target_node}")
# 记录拖拽前的父节点
old_parent_item = dragged_item.parent()
old_parent_node = old_parent_item.data(0, Qt.UserRole) if old_parent_item else None
# 执行Qt默认拖拽
super().dropEvent(event)
# 检查拖拽后的父节点
new_parent_item = dragged_item.parent()
new_parent_node = new_parent_item.data(0, Qt.UserRole) if new_parent_item else None
# 同步Panda3D场景图的父子关系
try:
# 检查是否是跨层级拖拽(父节点发生变化)
if old_parent_node != new_parent_node:
print(f"跨层级拖拽:从 {old_parent_node} 移动到 {new_parent_node}")
# 保存世界坐标位置
world_pos = dragged_node.getPos(self.world.render)
world_hpr = dragged_node.getHpr(self.world.render)
world_scale = dragged_node.getScale(self.world.render)
# 重新父化到新的父节点
if new_parent_node:
dragged_node.reparentTo(new_parent_node)
else:
# 如果新父节点为None重新父化到render
dragged_node.reparentTo(self.world.render)
# 恢复世界坐标位置
dragged_node.setPos(self.world.render, world_pos)
dragged_node.setHpr(self.world.render, world_hpr)
dragged_node.setScale(self.world.render, world_scale)
print(f"✅ Panda3D父子关系已更新")
else:
print(f"同层级移动父节点未变化跳过Panda3D重新父化")
except Exception as e:
print(f"⚠️ 同步Panda3D场景图失败: {e}")
# 不影响Qt树的更新继续执行
# 事后验证:确保节点仍在"场景"根节点下
self._ensureUnderSceneRoot(dragged_item)
self.world.property_panel._syncEffectiveVisibility(dragged_node)
event.accept()
# try:
# world_pos = dragged_node.getPos(self.world.render)
#
# parent_of_dragged = dragged_node.getParent()
# target_node.wrtReparentTo(parent_of_dragged)
#
# # 拖动节点到目标节点下
# dragged_node.wrtReparentTo(target_node)
# dragged_node.setPos(self.world.render, world_pos)
#
# # 更新 Qt 树控件
# super().dropEvent(event)
#
# # 更新属性面板
# self.world.updatePropertyPanel(dragged_item)
#
# event.accept()
#
# except Exception as e:
# print(f"重设父节点失败: {e}")
# event.ignore()
def _ensureUnderSceneRoot(self, item):
"""确保节点在场景根节点下,如果不是则自动修正"""
if not item:
return
# 检查是否成为了顶级节点
if not item.parent():
# 如果节点名称不是"场景",说明意外成为了顶级节点
if item.text(0) != "场景":
print(f"⚠️ 检测到节点 {item.text(0)} 意外成为顶级节点,正在修正...")
# 找到场景根节点
scene_root = None
for i in range(self.topLevelItemCount()):
top_item = self.topLevelItem(i)
if top_item.text(0) == "场景":
scene_root = top_item
break
if scene_root:
# 将节点移回场景根节点下
self.takeTopLevelItem(self.indexOfTopLevelItem(item))
scene_root.addChild(item)
print(f"✅ 已将节点 {item.text(0)} 移回场景根节点下")
def isValidParentChild(self, dragged_item, target_item):
"""检查是否是有效的父子关系"""
# 不能拖放到自己上
"""检查是否是有效的父子关系(防止循环)"""
# 1. 禁止拖放到自身
if dragged_item == target_item:
return False
# 不能拖放到自己的子节点上
parent = target_item
while parent:
if parent == dragged_item:
return False
parent = parent.parent()
# 检查目标项
if target_item.text(0) == "场景":
return False # 不能拖放到场景根节点
# 允许拖放到模型根节点或其他模型节点
if target_item.text(0) == "模型":
return True
# 检查目标项的父节点
target_parent = target_item.parent()
if not target_parent:
# 2. 禁止拖到根节点之外(根节点本身除外)
target_root = self._getRootNode(target_item)
if target_root != "场景":
print(f"❌ 目标节点 {target_item.text(0)} 不在场景下")
return False
# 允许在模型节点下的任何位置调整父子关系
while target_parent:
if target_parent.text(0) == "模型":
return True
target_parent = target_parent.parent()
return False
# 3. 禁止拖拽"场景"根节点
dragged_root = self._getRootNode(dragged_item)
if dragged_item.text(0) == "场景" or dragged_root != "场景":
print(f"❌ 禁止拖拽场景根节点或根节点外的节点")
return False
# 4. Qt 树循环检查
current = target_item
while current:
if current == dragged_item:
print(f"❌ Qt 树检测:{target_item.text(0)}{dragged_item.text(0)} 的后代")
return False
current = current.parent()
return True
def _getRootNode(self, item):
"""获取树中节点的根节点文本"""
current = item
while current.parent():
current = current.parent()
return current.text(0)
def dragEnterEvent(self, event):
"""处理拖入事件"""
if event.source() == self:
event.accept()
else:
event.ignore()
def dragMoveEvent(self, event):
"""处理拖动事件"""
if event.source() == self:
event.accept()
else:
if event.source() != self:
event.ignore()
return
# 获取当前拖拽的项目和目标位置
target_item = self.itemAt(event.pos())
selected_items = self.selectedItems()
# 检查是否拖拽到多选区域内的项目
if target_item and target_item in selected_items:
event.ignore()
return
# 检查其他禁止条件
if target_item and selected_items:
for dragged_item in selected_items:
if not self.isValidParentChild(dragged_item, target_item):
event.ignore()
return
super().dragMoveEvent(event)
event.accept()
def keyPressEvent(self, event):
"""处理键盘按键事件"""
if event.key() == Qt.Key_Delete:
currentItem = self.currentItem()
if currentItem and currentItem.parent():
# 检查是否是模型节点或其子节点
if self.world.interface_manager.isModelOrChild(currentItem):
nodePath = currentItem.data(0, Qt.UserRole)
if nodePath:
print("正在删除节点...")
self.world.interface_manager.deleteNode(nodePath, currentItem)
print("删除完成")
# currentItem = self.currentItem()
# if currentItem and currentItem.parent():
# # 检查是否是模型节点或其子节点
# if self.world.interface_manager.isModelOrChild(currentItem):
# nodePath = currentItem.data(0, Qt.UserRole)
# if nodePath:
# print("正在删除节点...")
# self.world.interface_manager.deleteNode(nodePath, currentItem)
# print("删除完成")
selected_items = self.selectedItems()
if selected_items:
# 执行删除操作
self.delete_items(selected_items)
else:
# 没有选中任何项目,执行默认操作
super().keyPressEvent(event)
else:
super().keyPressEvent(event)
super().keyPressEvent(event)
def delete_items(self, selected_items):
"""删除选中的项目"""
if not selected_items:
return
# 准备确认对话框的内容
item_count = len(selected_items)
if item_count == 1:
item_names = f'"{selected_items[0].text(0)}"'
title = "确认删除"
message = f"确定要删除节点 {item_names} 吗?"
else:
item_names = "".join([f'"{item.text(0)}"' for item in selected_items[:3]])
if item_count > 3:
item_names += f"{item_count} 个节点"
title = "确认批量删除"
message = f"确定要删除以下 {item_count} 个节点吗?\n\n{item_names}"
# 创建确认对话框
reply = QMessageBox.question(
self,
title,
message,
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No # 默认选择"取消",防止误删
)
# 只有用户确认后才执行删除
if reply == QMessageBox.Yes:
pass
print(f"✅ 已删除 {item_count} 个节点")